From 69fc48b850e20f7ae87c6c8514e96b4a4586ecdd Mon Sep 17 00:00:00 2001 From: SinghCod3r Date: Thu, 10 Sep 2026 18:18:46 +0530 Subject: [PATCH 1/8] Standardize GTK spinner size for cross-theme consistency --- newprinter.py | 5 +++-- ui/NewPrinterWindow.ui | 4 ++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/newprinter.py b/newprinter.py index fdab9df98..296fe9fd0 100644 --- a/newprinter.py +++ b/newprinter.py @@ -414,7 +414,8 @@ def __init__(self): self.ntbkNPDownloadableDriverProperties.set_show_tabs(False) self.spinner_count = 0 - + self.spinner.set_size_request(58, 58) + self.spinner.get_style_context().add_class("large-spinner") # Set up OpenPrinting widgets. self.opreq = None self.opreq_handlers = None @@ -475,7 +476,7 @@ def protect_toggle (toggle_widget): self._searching_spinner = Gtk.Spinner () self._searching_spinner.set_halign (Gtk.Align.CENTER) self._searching_spinner.set_valign (Gtk.Align.CENTER) - self._searching_spinner.set_size_request (32, 32) + self._searching_spinner.set_size_request (58, 58) self._searching_overlay.add_overlay (self._searching_spinner) self._searching_overlay.show_all () self._searching_spinner.hide () diff --git a/ui/NewPrinterWindow.ui b/ui/NewPrinterWindow.ui index 774960f09..2e05cfcc9 100644 --- a/ui/NewPrinterWindow.ui +++ b/ui/NewPrinterWindow.ui @@ -3335,7 +3335,11 @@ ipp://printer.mydomain/ipp 12 + True + False + 44 + 44 False From 34fe640051c1845eb29a63b66ded4557a0360e30 Mon Sep 17 00:00:00 2001 From: SinghCod3r Date: Mon, 14 Sep 2026 11:13:18 +0530 Subject: [PATCH 2/8] added css to make the loader consistent across all distros --- newprinter.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/newprinter.py b/newprinter.py index 296fe9fd0..3a162de09 100644 --- a/newprinter.py +++ b/newprinter.py @@ -416,6 +416,19 @@ def __init__(self): self.spinner_count = 0 self.spinner.set_size_request(58, 58) self.spinner.get_style_context().add_class("large-spinner") + self._spinner_css_provider = Gtk.CssProvider () + self._spinner_css_provider.load_from_data (b""" + .scp-searching-spinner { + min-width: 48px; + min-height: 48px; + } + """) + Gtk.StyleContext.add_provider_for_screen ( + Gdk.Screen.get_default (), + self._spinner_css_provider, + Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION) + # self.spinner.set_size_request(58, 58) + self.spinner.get_style_context().add_class("scp-searching-spinner") # Set up OpenPrinting widgets. self.opreq = None self.opreq_handlers = None @@ -477,6 +490,8 @@ def protect_toggle (toggle_widget): self._searching_spinner.set_halign (Gtk.Align.CENTER) self._searching_spinner.set_valign (Gtk.Align.CENTER) self._searching_spinner.set_size_request (58, 58) + # self._searching_spinner.set_size_request (58, 58) + self._searching_spinner.get_style_context().add_class("scp-searching-spinner") self._searching_overlay.add_overlay (self._searching_spinner) self._searching_overlay.show_all () self._searching_spinner.hide () From ad597a323d724ce0120c83168445e4e573ad3564 Mon Sep 17 00:00:00 2001 From: SinghCod3r Date: Fri, 11 Sep 2026 12:44:54 +0530 Subject: [PATCH 3/8] Fix GUI freezes and GTK crashes during driver installation - Fix SIGSEGV by wrapping OpenPrinting signal emissions in GLib.idle_add - Fix GTK-CRITICAL layout errors by deferring search dialog destruction - Prevent PackageKit DBus activation from blocking the main GTK loop - Prevent PackageKit DBus polling from freezing the GUI by using select - Replace deprecated modal searching dialogs with inline spinners --- OpenPrintingRequest.py | 12 ++++---- newprinter.py | 64 ++++++++++++++++++------------------------ ppdsloader.py | 45 +++++++++++++++-------------- 3 files changed, 55 insertions(+), 66 deletions(-) diff --git a/OpenPrintingRequest.py b/OpenPrintingRequest.py index 4f1a1f177..1426b893b 100644 --- a/OpenPrintingRequest.py +++ b/OpenPrintingRequest.py @@ -27,7 +27,7 @@ import cupshelpers from debug import * -from gi.repository import GObject +from gi.repository import GObject, GLib class OpenPrintingRequest(GObject.GObject): __gsignals__ = { @@ -79,7 +79,7 @@ def _printers_got (self, status, user_data, printers): self._handle = None if status != 0: debugprint ("%s -> 'error'" % self) - self.emit ('error', status, printers) + GLib.idle_add (self.emit, 'error', status, printers) return self.downloadable_printers_unchecked = [(x, printers[x]) @@ -130,7 +130,7 @@ def _printer_drivers_got (self, status, user_data, drivers): self._handle = None if status != 0: debugprint ("%s -> 'error'" % self) - self.emit ('error', status, drivers) + GLib.idle_add (self.emit, 'error', status, drivers) return if drivers: @@ -163,9 +163,9 @@ def _printer_drivers_got (self, status, user_data, drivers): def _drivers_got (self): self._handle = None debugprint ("%s -> 'finished'" % self) - self.emit ('finished', - self.downloadable_printers, - self.downloadable_drivers) + GLib.idle_add (self.emit, 'finished', + self.downloadable_printers, + self.downloadable_drivers) if __name__ == '__main__': from pprint import pprint diff --git a/newprinter.py b/newprinter.py index 3a162de09..337610231 100644 --- a/newprinter.py +++ b/newprinter.py @@ -878,6 +878,7 @@ def on_ppdsloader_finished_next (self, ppdsloader): # get PPDs def _getPPDs_reply (self, ppdsloader): + self._hide_searching_spinner() exc = ppdsloader.get_error () if exc: ppdsloader.destroy () @@ -963,7 +964,8 @@ def on_NPCancel(self, widget, event=None): self.dec_spinner_task () self.NewPrinterWindow.hide() - if self.opreq is not None: + self._hide_searching_spinner() + if getattr(self, 'opreq', None) is not None: for handler in self.opreq_handlers: self.opreq.disconnect (handler) @@ -1047,28 +1049,29 @@ def do_installdriverpackage(self, name, repo, keyid): done = False pbar = self._installdialog._progress_bar + import select while self.p.poll() is None: - line = stdout.readline ().strip() - if (len(line) > 0): - if line == "done": - done = True - break - elif line.startswith(b"P"): - try: - percentage = float(line[1:]) - if percentage >= 0: - pbar.set_fraction(percentage/100) - else: - pbar.set_pulse_step(-percentage/100) - pbar.pulse() - except: - pass - else: - self.installed_driver_files.append(line.decode("utf-8")); + rlist, _, _ = select.select([stdout], [], [], 0.1) + if rlist: + line = stdout.readline ().strip() + if (len(line) > 0): + if line == b"done": + done = True + break + elif line.startswith(b"P"): + try: + percentage = float(line[1:]) + if percentage >= 0: + pbar.set_fraction(percentage/100) + else: + pbar.set_pulse_step(-percentage/100) + pbar.pulse() + except: + pass + else: + self.installed_driver_files.append(line.decode("utf-8")) while Gtk.events_pending (): Gtk.main_iteration () - if not line: - time.sleep (0.1) if self.p.returncode != 0 and not done: ret = False except: @@ -1580,6 +1583,7 @@ def _installHPScannerFilesIfNeeded (self): def _loadPPDsForDevice (self, devid, uri): debugprint ("nextNPTab: need PPDs loaded") + self._show_searching_spinner() p = ppdsloader.PPDsLoader (device_id=devid, device_uri=uri, parent=self.NewPrinterWindow, @@ -1726,22 +1730,9 @@ def _installPrinterOrSearchForDriver (self, devid, ppdname, status, page_nr, ste self.searchedfordriverpackages = True self._searchdialog_canceled = False - fmt = _("Searching") - self._searchdialog = Gtk.MessageDialog ( - parent=self.NewPrinterWindow, - modal=True, - destroy_with_parent=True, - message_type=Gtk.MessageType.INFO, - buttons=Gtk.ButtonsType.CANCEL, - text=fmt) - - self._searchdialog.format_secondary_text ( - _("Searching for drivers")) + self._show_searching_spinner() self.opreq = OpenPrintingRequest () - self._searchdialog.connect ( - "response", self._searchdialog_response) - self._searchdialog.show_all () self.opreq_handlers = [] self.opreq_handlers.append ( @@ -1792,9 +1783,7 @@ def opreq_id_search_done (self, opreq, printers, drivers): self.opreq_user_search = False self.opreq_handlers = None self.opreq = None - self._searchdialog.hide () - self._searchdialog.destroy () - self._searchdialog = None + self._hide_searching_spinner() # Check whether we have found something @@ -4449,6 +4438,7 @@ def on_btnNPApply_clicked(self, widget): if self.ppdsloader: self.ppdsloader.destroy () self.ppdsloader = None + self._hide_searching_spinner() if self.printer_finder: self.printer_finder.cancel () diff --git a/ppdsloader.py b/ppdsloader.py index 7be92eedc..edaf623bf 100644 --- a/ppdsloader.py +++ b/ppdsloader.py @@ -20,7 +20,7 @@ ## Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. import dbus -from gi.repository import GObject +from gi.repository import GObject, GLib from gi.repository import Gtk import cupshelpers @@ -83,19 +83,9 @@ def __init__ (self, device_id=None, parent=None, device_uri=None, debugprint ("Failed to get session bus") self._bus = None - fmt = _("Searching") - self._dialog = Gtk.MessageDialog (parent=parent, - modal=True, destroy_with_parent=True, - message_type=Gtk.MessageType.INFO, - buttons=Gtk.ButtonsType.CANCEL, - text=fmt) - - self._dialog.format_secondary_text (_("Searching for drivers")) - - self._dialog.connect ("response", self._dialog_response) + self._dialog = None def run (self): - self._dialog.show_all () if self._device_id: self._devid_dict = cupshelpers.parseDeviceID (self._device_id) @@ -215,9 +205,22 @@ def _cups_error (self, conn, exc): def _query_packagekit (self): debugprint ("Asking PackageKit to install drivers") + import threading + def worker(): + try: + obj = self._bus.get_object ("org.freedesktop.PackageKit", + "/org/freedesktop/PackageKit") + GLib.idle_add (self._query_packagekit_got_obj, obj, None) + except Exception as e: + GLib.idle_add (self._query_packagekit_got_obj, None, e) + threading.Thread(target=worker, daemon=True).start() + + def _query_packagekit_got_obj(self, obj, exc): + if exc is not None: + debugprint ("Failed to talk to PackageKit: %s" % repr (exc)) + self._query_cups () + return try: - obj = self._bus.get_object ("org.freedesktop.PackageKit", - "/org/freedesktop/PackageKit") proxy = dbus.Interface (obj, "org.freedesktop.PackageKit.Modify") resources = [self._gpk_device_id] interaction = "hide-finished" @@ -230,22 +233,18 @@ def _query_packagekit (self): timeout=3600) except Exception as e: debugprint ("Failed to talk to PackageKit: %s" % repr (e)) - if self._dialog: - self._dialog.show_all () - self._query_cups () + self._query_cups () def _packagekit_reply (self): debugprint ("Got PackageKit reply") self._need_requery_cups = True - if self._dialog: - self._dialog.show_all () - self._query_cups () + pass + self._query_cups () def _packagekit_error (self, exc): debugprint ("Got PackageKit error: %s" % repr (exc)) - if self._dialog: - self._dialog.show_all () - self._query_cups () + pass + self._query_cups () def _query_jockey (self): debugprint ("Asking Jockey to install drivers") From 194eb840786c36dad8161ace7149c90d92ad45bd Mon Sep 17 00:00:00 2001 From: SinghCod3r Date: Sat, 12 Sep 2026 12:58:17 +0530 Subject: [PATCH 4/8] Fix remaining GUI freezes during driver installation --- newprinter.py | 192 +++++++++++++++++++++++++-------------- ppdsloader.py | 102 ++++++++++----------- system-config-printer.py | 5 + timedops.py | 5 - 4 files changed, 181 insertions(+), 123 deletions(-) diff --git a/newprinter.py b/newprinter.py index 337610231..b4b86532a 100644 --- a/newprinter.py +++ b/newprinter.py @@ -237,7 +237,6 @@ def __init__(self): self.nextnptab_rerun = False self.printers = {} # set in init() self.recommended_model_selected = False - self._searchdialog = None self._installdialog = None self.getWidgets({"NewPrinterWindow": @@ -486,15 +485,19 @@ def protect_toggle (toggle_widget): parent_box.pack_start (self._searching_overlay, True, True, 0) parent_box.reorder_child (self._searching_overlay, 0) + self._searching_vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12) + self._searching_vbox.set_halign(Gtk.Align.CENTER) + self._searching_vbox.set_valign(Gtk.Align.CENTER) self._searching_spinner = Gtk.Spinner () - self._searching_spinner.set_halign (Gtk.Align.CENTER) - self._searching_spinner.set_valign (Gtk.Align.CENTER) - self._searching_spinner.set_size_request (58, 58) # self._searching_spinner.set_size_request (58, 58) self._searching_spinner.get_style_context().add_class("scp-searching-spinner") - self._searching_overlay.add_overlay (self._searching_spinner) + self._searching_label = Gtk.Label() + self._searching_label.set_use_markup(True) + self._searching_vbox.pack_start(self._searching_spinner, False, False, 0) + self._searching_vbox.pack_start(self._searching_label, False, False, 0) + self._searching_overlay.add_overlay (self._searching_vbox) self._searching_overlay.show_all () - self._searching_spinner.hide () + self._searching_vbox.hide () else: self._searching_spinner = None @@ -957,6 +960,7 @@ def on_NPCancel(self, widget, event=None): if self.ppdsloader: self.ppdsloader.destroy () self.ppdsloader = None + self._hide_searching_spinner() if self.printer_finder: self.printer_finder.cancel () @@ -1043,43 +1047,112 @@ def do_installdriverpackage(self, name, repo, keyid): self.p = subprocess.Popen (args, env=new_environ, close_fds=True, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE) - # Keep the UI refreshed while we wait for - # the drivers query to complete. - (stdout, stderr) = (self.p.stdout, self.p.stderr) - done = False + # Use a nested main loop and IO watch for event-driven async I/O + loop = GLib.MainLoop() pbar = self._installdialog._progress_bar - import select - while self.p.poll() is None: - rlist, _, _ = select.select([stdout], [], [], 0.1) - if rlist: - line = stdout.readline ().strip() - if (len(line) > 0): - if line == b"done": - done = True - break - elif line.startswith(b"P"): - try: - percentage = float(line[1:]) - if percentage >= 0: - pbar.set_fraction(percentage/100) - else: - pbar.set_pulse_step(-percentage/100) - pbar.pulse() - except: - pass + + # Make stdout non-blocking + import fcntl + flags = fcntl.fcntl(self.p.stdout.fileno(), fcntl.F_GETFL) + fcntl.fcntl(self.p.stdout.fileno(), fcntl.F_SETFL, flags | os.O_NONBLOCK) + + done_received = False + child_exited = False + eof_reached = False + io_error = False + exit_status = -1 + + def check_done(): + if child_exited and (eof_reached or io_error): + loop.quit() + + def process_line(line): + nonlocal done_received + line = line.strip() + if not line: + return + if line == b"done": + done_received = True + elif line.startswith(b"P"): + try: + percentage = float(line[1:]) + if percentage >= 0: + pbar.set_fraction(percentage/100) else: - self.installed_driver_files.append(line.decode("utf-8")) - while Gtk.events_pending (): - Gtk.main_iteration () - if self.p.returncode != 0 and not done: + pbar.set_pulse_step(-percentage/100) + pbar.pulse() + except: + pass + else: + self.installed_driver_files.append(line.decode("utf-8", errors="replace")) + + buffer = b"" + def on_stdout_ready(source, condition): + nonlocal buffer, eof_reached, io_error + import os as _os + fd = source if isinstance(source, int) else source.fileno() + + while True: + try: + data = _os.read(fd, 4096) + if not data: + eof_reached = True + break + buffer += data + while b"\n" in buffer: + line, buffer = buffer.split(b"\n", 1) + process_line(line) + except BlockingIOError: + break + except Exception as e: + debugprint("Unexpected IO error reading from subprocess: %s" % e) + io_error = True + break + + if condition & GLib.IOCondition.ERR: + debugprint("GLib.IOCondition.ERR received on subprocess stdout") + io_error = True + + if eof_reached or io_error or (condition & GLib.IOCondition.HUP): + if buffer: + process_line(buffer) + buffer = b"" + if condition & GLib.IOCondition.HUP: + eof_reached = True + check_done() + return False + return True + + def on_child_exit(pid, status): + nonlocal child_exited, exit_status + child_exited = True + exit_status = status + check_done() + + watch_id = GLib.io_add_watch(self.p.stdout, + GLib.PRIORITY_DEFAULT, + GLib.IOCondition.IN | GLib.IOCondition.HUP | GLib.IOCondition.ERR, + on_stdout_ready) + child_watch_id = GLib.child_watch_add(self.p.pid, on_child_exit) + + # Block here, processing UI events and I/O until both EOF and child exit occur + loop.run() + + # Convert exit status to return code. waitpid returns a 16-bit status. + import os as _os + returncode = _os.waitstatus_to_exitcode(exit_status) if hasattr(_os, 'waitstatus_to_exitcode') else (exit_status >> 8) + if returncode != 0 and not done_received: ret = False - except: - # Problem executing command. + if io_error: + ret = False + + except Exception as e: + debugprint("Error in do_installdriverpackage: %s" % e) ret = False if not ret: - self.installed_driver_files = []; + self.installed_driver_files = [] return ret @@ -1204,7 +1277,7 @@ def nextNPTab(self, step=1): if (self.device.id and not self.device.type in ("socket", "lpd", "ipp", "http", "https", "bluetooth")): - name = "%s %s" % (self.device.id_dict["MFG"], + name = "%s %s" % (self.device.id_dict["MFG"], self.device.id_dict["MDL"]) except: nonfatalException () @@ -1583,7 +1656,7 @@ def _installHPScannerFilesIfNeeded (self): def _loadPPDsForDevice (self, devid, uri): debugprint ("nextNPTab: need PPDs loaded") - self._show_searching_spinner() + self._show_searching_spinner(_("Searching for drivers")) p = ppdsloader.PPDsLoader (device_id=devid, device_uri=uri, parent=self.NewPrinterWindow, @@ -1729,10 +1802,8 @@ def _installPrinterOrSearchForDriver (self, devid, ppdname, status, page_nr, ste debugprint ('nextNPTab: Searching for "%s"' % devid) self.searchedfordriverpackages = True - self._searchdialog_canceled = False - self._show_searching_spinner() - self.opreq = OpenPrintingRequest () + self._show_searching_spinner(_("Searching for drivers")) self.opreq_handlers = [] self.opreq_handlers.append ( @@ -1760,22 +1831,6 @@ def _installPrinterOrSearchForDriver (self, devid, ppdname, status, page_nr, ste # No operations are pending if reached. return self.INSTALL_RESULT_DONE - def _searchdialog_response (self, dialog, response): - # Cancel clicked while performing openprinting search - - self.btnNPDownloadableDriverSearch.set_sensitive (True) - self.btnNPDownloadableDriverSearch_label.set_text (_("Search")) - - self.installed_driver_files = [] - self.searchedfordriverpackages = True - self.founddownloadabledrivers = False - self.founddownloadableppd = False - - ready (self.NewPrinterWindow) - - # Cancel the openprinting request. - GLib.idle_add (self.opreq.cancel) - def opreq_id_search_done (self, opreq, printers, drivers): for handler in self.opreq_handlers: opreq.disconnect (handler) @@ -1804,7 +1859,7 @@ def opreq_id_search_done (self, opreq, printers, drivers): try: self.NewPrinterWindow.show() self.setNPButtons() - + if not self.fillDownloadableDrivers(): ready(self.NewPrinterWindow) self.founddownloadabledrivers = False @@ -2363,27 +2418,32 @@ def adjust_firewall_response (self, dialog, response): self.firewall.write () debugprint ("Fetching network devices after firewall dialog response") - self._show_searching_spinner () + self._show_searching_spinner (_("Searching for printers")) self.fetchDevices_conn = asyncconn.Connection () self.fetchDevices_conn._begin_operation (_("fetching device list")) self.fetchDevices (network=True) def start_fetching_devices (self): - self._show_searching_spinner () + self._show_searching_spinner (_("Searching for printers")) self.fetchDevices_conn = asyncconn.Connection () self.fetchDevices_conn._begin_operation (_("fetching device list")) self.fetchDevices (network=False, current_uri=self.current_uri) del self.current_uri - def _show_searching_spinner (self): - if self._searching_spinner is not None: + def _show_searching_spinner (self, text=""): + if getattr(self, '_searching_vbox', None) is not None: + if text: + self._searching_label.set_markup("%s" % text) + self._searching_label.show() + else: + self._searching_label.hide() self._searching_spinner.start () - self._searching_spinner.show () + self._searching_vbox.show () def _hide_searching_spinner (self): - if self._searching_spinner is not None: - self._searching_spinner.hide () + if getattr(self, '_searching_vbox', None) is not None: + self._searching_vbox.hide () self._searching_spinner.stop () def add_devices (self, devices, current_uri, no_more=False): @@ -2445,7 +2505,7 @@ def replace_generic (device): else: device2.uri = "delete" devices = [x for x in devices if x.uri not in ("hp", "hpfax", - "hal", "beh", "smb", + "hal", "beh", "smb", "scsi", "http", "bjnp", "delete")] diff --git a/ppdsloader.py b/ppdsloader.py index edaf623bf..3942fe5b3 100644 --- a/ppdsloader.py +++ b/ppdsloader.py @@ -19,12 +19,14 @@ ## along with this program; if not, write to the Free Software ## Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +import threading + import dbus from gi.repository import GObject, GLib from gi.repository import Gtk -import cupshelpers import cups +import cupshelpers cups.require ("1.9.52") import asyncconn @@ -74,6 +76,7 @@ def __init__ (self, device_id=None, parent=None, device_uri=None, self._ppdsmatch_result = None self._jockey_queried = False self._jockey_has_answered = False + self._destroyed = False self._local_cups = (self._host is None or self._host == "localhost" or self._host[0] == '/') @@ -83,7 +86,6 @@ def __init__ (self, device_id=None, parent=None, device_uri=None, debugprint ("Failed to get session bus") self._bus = None - self._dialog = None def run (self): @@ -106,9 +108,7 @@ def __del__ (self): def destroy (self): debugprint ("DESTROY: %s" % self) - if self._dialog: - self._dialog.destroy () - self._dialog = None + self._destroyed = True self._parent = None @@ -129,11 +129,6 @@ def get_error (self): debugprint ("%s: stored error is %s" % (self, repr (self._exc))) return self._exc - def _dialog_response (self, dialog, response): - dialog.destroy () - self._dialog = None - self.emit ('finished') - def _query_cups (self): debugprint ("Asking CUPS for PPDs") if (not self._conn): @@ -146,11 +141,17 @@ def _query_cups (self): self._cups_connect_reply(self._conn, None) def _cups_connect_reply (self, conn, UNUSED): + if self._destroyed: + return + conn._begin_operation (_("fetching PPDs")) conn.getPPDs2 (reply_handler=self._cups_reply, error_handler=self._cups_error) def _cups_reply (self, conn, result): + if self._destroyed: + return + ppds = cupshelpers.ppds.PPDs (result, language=self._language) self._ppds = ppds self._need_requery_cups = False @@ -186,65 +187,59 @@ def _cups_reply (self, conn, result): conn.destroy () self._conn = None - if self._dialog is not None: - self._dialog.destroy () - self._dialog = None self.emit ('finished') def _cups_error (self, conn, exc): + if self._destroyed: + return + conn.destroy () self._conn = None self._ppds = None self._exc = exc - if self._dialog is not None: - self._dialog.destroy () - self._dialog = None self.emit ('finished') def _query_packagekit (self): debugprint ("Asking PackageKit to install drivers") - import threading + + gpk_device_id = self._gpk_device_id + def worker(): + bus = None + success = False try: - obj = self._bus.get_object ("org.freedesktop.PackageKit", - "/org/freedesktop/PackageKit") - GLib.idle_add (self._query_packagekit_got_obj, obj, None) + bus = dbus.SessionBus(private=True) + obj = bus.get_object("org.freedesktop.PackageKit", + "/org/freedesktop/PackageKit") + proxy = dbus.Interface(obj, "org.freedesktop.PackageKit.Modify") + resources = [gpk_device_id] + interaction = "hide-finished" + debugprint("Calling InstallPrinterDrivers in worker") + proxy.InstallPrinterDrivers(dbus.UInt32(0), resources, interaction, timeout=3600) + success = True except Exception as e: - GLib.idle_add (self._query_packagekit_got_obj, None, e) + debugprint("Got PackageKit error in worker: %s" % repr(e)) + finally: + if bus is not None: + bus.close() + GLib.idle_add(self._on_packagekit_done, success) + threading.Thread(target=worker, daemon=True).start() - def _query_packagekit_got_obj(self, obj, exc): - if exc is not None: - debugprint ("Failed to talk to PackageKit: %s" % repr (exc)) - self._query_cups () - return - try: - proxy = dbus.Interface (obj, "org.freedesktop.PackageKit.Modify") - resources = [self._gpk_device_id] - interaction = "hide-finished" - debugprint ("Calling InstallPrinterDrivers (%s, %s, %s)" % - (repr (0), repr (resources), repr (interaction))) - proxy.InstallPrinterDrivers (dbus.UInt32 (0), - resources, interaction, - reply_handler=self._packagekit_reply, - error_handler=self._packagekit_error, - timeout=3600) - except Exception as e: - debugprint ("Failed to talk to PackageKit: %s" % repr (e)) - self._query_cups () + def _on_packagekit_done(self, success): + if self._destroyed: + return False - def _packagekit_reply (self): - debugprint ("Got PackageKit reply") - self._need_requery_cups = True - pass - self._query_cups () + if not success: + debugprint("PackageKit installation failed or returned error") + else: + debugprint("Got PackageKit reply") + self._need_requery_cups = True - def _packagekit_error (self, exc): - debugprint ("Got PackageKit error: %s" % repr (exc)) - pass - self._query_cups () + self._query_cups() + return False def _query_jockey (self): debugprint ("Asking Jockey to install drivers") @@ -259,6 +254,9 @@ def _query_jockey (self): self._jockey_error (e) def _jockey_reply (self, conn, result): + if self._destroyed: + return + debugprint ("Got Jockey result: %s" % repr (result)) self._jockey_has_answered = True try: @@ -268,6 +266,9 @@ def _jockey_reply (self, conn, result): self._query_cups () def _jockey_error (self, exc): + if self._destroyed: + return + debugprint ("Got Jockey error: %s" % repr (exc)) if self._need_requery_cups: self._query_cups () @@ -276,9 +277,6 @@ def _jockey_error (self, exc): self._conn.destroy () self._conn = None - if self._dialog is not None: - self._dialog.destroy () - self._dialog = None self.emit ('finished') diff --git a/system-config-printer.py b/system-config-printer.py index 521abc6f0..087f11043 100755 --- a/system-config-printer.py +++ b/system-config-printer.py @@ -27,6 +27,11 @@ import sys, os, time, re import _thread import dbus +import dbus.mainloop.glib + +# Must be initialized before any threads use D-Bus. +dbus.mainloop.glib.threads_init() + import gi try: gi.require_version('Polkit', '1.0') diff --git a/timedops.py b/timedops.py index 4af184c8c..9c3f38725 100644 --- a/timedops.py +++ b/timedops.py @@ -18,7 +18,6 @@ ## along with this program; if not, write to the Free Software ## Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -import dbus.mainloop.glib from gi.repository import GObject from gi.repository import GLib from gi.repository import Gdk @@ -30,10 +29,6 @@ gettext.install(domain=config.PACKAGE, localedir=config.localedir) from debug import * -# Initialise threading for D-Bus. This is needed as long as it is -# used from two separate threads. We only do this in a few places -# now, but in particular the troubleshooter does this (bug #662047). -dbus.mainloop.glib.threads_init () class OperationCanceled(RuntimeError): pass From 8063ed4eccf337bb3fd93098ef59c03ad64fb27c Mon Sep 17 00:00:00 2001 From: SinghCod3r Date: Sat, 12 Sep 2026 13:22:54 +0530 Subject: [PATCH 5/8] Disable Forward while driver search is pending --- newprinter.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/newprinter.py b/newprinter.py index b4b86532a..d5869a4a6 100644 --- a/newprinter.py +++ b/newprinter.py @@ -866,6 +866,7 @@ def on_ppdsloader_finished_next (self, ppdsloader): self._getPPDs_reply (ppdsloader) if not self.ppds: + self.setNPButtons() return if ppdsloader._jockey_has_answered: @@ -1223,6 +1224,7 @@ def nextNPTab(self, step=1): if not keep_going: debugprint ('Interrupting execution of nextNPTab(): Operations pending') + self.btnNPForward.set_sensitive (False) return order = self._getPagesOrderForDialogMode () From b6c167395a90672f6fd0cb36d78b3b3e7bd5a86a Mon Sep 17 00:00:00 2001 From: SinghCod3r Date: Tue, 15 Sep 2026 09:54:28 +0530 Subject: [PATCH 6/8] fixed the spinner position to the right --- newprinter.py | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/newprinter.py b/newprinter.py index d5869a4a6..3ee8618f9 100644 --- a/newprinter.py +++ b/newprinter.py @@ -475,19 +475,14 @@ def protect_toggle (toggle_widget): self.tvNPDevices.connect ("row-activated", self.device_row_activated) self.tvNPDevices.connect ("row-expanded", self.device_row_expanded) - # inline searching spinner - scrolled = self.tvNPDevices.get_parent () - parent_box = scrolled.get_parent () - if parent_box is not None: - self._searching_overlay = Gtk.Overlay () - parent_box.remove (scrolled) - self._searching_overlay.add (scrolled) - parent_box.pack_start (self._searching_overlay, True, True, 0) - parent_box.reorder_child (self._searching_overlay, 0) - + # Searching spinner — placed in the right-side panel (vbNPDevices) + # next to the Description/device-type notebook, per reviewer feedback. + vbNPDevices = self.ntbkNPType.get_parent () + if vbNPDevices is not None: self._searching_vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12) self._searching_vbox.set_halign(Gtk.Align.CENTER) self._searching_vbox.set_valign(Gtk.Align.CENTER) + self._searching_vbox.set_vexpand(True) self._searching_spinner = Gtk.Spinner () # self._searching_spinner.set_size_request (58, 58) self._searching_spinner.get_style_context().add_class("scp-searching-spinner") @@ -495,8 +490,8 @@ def protect_toggle (toggle_widget): self._searching_label.set_use_markup(True) self._searching_vbox.pack_start(self._searching_spinner, False, False, 0) self._searching_vbox.pack_start(self._searching_label, False, False, 0) - self._searching_overlay.add_overlay (self._searching_vbox) - self._searching_overlay.show_all () + vbNPDevices.pack_start(self._searching_vbox, True, True, 0) + self._searching_vbox.show_all () self._searching_vbox.hide () else: self._searching_spinner = None @@ -2440,6 +2435,7 @@ def _show_searching_spinner (self, text=""): self._searching_label.show() else: self._searching_label.hide() + self.ntbkNPType.hide () self._searching_spinner.start () self._searching_vbox.show () @@ -2447,6 +2443,7 @@ def _hide_searching_spinner (self): if getattr(self, '_searching_vbox', None) is not None: self._searching_vbox.hide () self._searching_spinner.stop () + self.ntbkNPType.show () def add_devices (self, devices, current_uri, no_more=False): if no_more: @@ -2590,7 +2587,7 @@ def replace_generic (device): row=[info, device, False] if network: if devs[0].uri != devs[0].type: - # An actual network printer device. Put this at the top. + # Show discovered network printers as selectable top-level devices. iter = model.insert_before (network_iter, find_nw_iter, row=row) if device == current_device: From f08273132e08d39a827694d6d846c10d83b872ed Mon Sep 17 00:00:00 2001 From: SinghCod3r Date: Tue, 15 Sep 2026 10:26:45 +0530 Subject: [PATCH 7/8] added vectorspinner.py file to use the new spinner logic and used across all the distors --- newprinter.py | 36 ++++++++++---------- vectorspinner.py | 89 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 107 insertions(+), 18 deletions(-) create mode 100644 vectorspinner.py diff --git a/newprinter.py b/newprinter.py index 3ee8618f9..7e54f6ae8 100644 --- a/newprinter.py +++ b/newprinter.py @@ -63,6 +63,7 @@ import ppdsloader import dnssdresolve import installpackage +from vectorspinner import VectorSpinner import gettext gettext.install(domain=config.PACKAGE, localedir=config.localedir) @@ -413,21 +414,15 @@ def __init__(self): self.ntbkNPDownloadableDriverProperties.set_show_tabs(False) self.spinner_count = 0 - self.spinner.set_size_request(58, 58) - self.spinner.get_style_context().add_class("large-spinner") - self._spinner_css_provider = Gtk.CssProvider () - self._spinner_css_provider.load_from_data (b""" - .scp-searching-spinner { - min-width: 48px; - min-height: 48px; - } - """) - Gtk.StyleContext.add_provider_for_screen ( - Gdk.Screen.get_default (), - self._spinner_css_provider, - Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION) - # self.spinner.set_size_request(58, 58) - self.spinner.get_style_context().add_class("scp-searching-spinner") + # Replace the UI-file GtkSpinner with our custom VectorSpinner + # so the spinner renders identically across all GTK themes. + old_spinner = self.spinner + spinner_parent = old_spinner.get_parent () + if spinner_parent is not None: + spinner_parent.remove (old_spinner) + self.spinner = VectorSpinner (size=32) + spinner_parent.pack_start (self.spinner, False, True, 0) + spinner_parent.reorder_child (self.spinner, 0) # Set up OpenPrinting widgets. self.opreq = None self.opreq_handlers = None @@ -483,11 +478,15 @@ def protect_toggle (toggle_widget): self._searching_vbox.set_halign(Gtk.Align.CENTER) self._searching_vbox.set_valign(Gtk.Align.CENTER) self._searching_vbox.set_vexpand(True) - self._searching_spinner = Gtk.Spinner () - # self._searching_spinner.set_size_request (58, 58) - self._searching_spinner.get_style_context().add_class("scp-searching-spinner") + self._searching_vbox.set_hexpand(True) + + self._searching_spinner = VectorSpinner (size=48) + self._searching_spinner.set_halign(Gtk.Align.CENTER) + self._searching_label = Gtk.Label() self._searching_label.set_use_markup(True) + self._searching_label.set_halign(Gtk.Align.CENTER) + self._searching_label.set_justify(Gtk.Justification.CENTER) self._searching_vbox.pack_start(self._searching_spinner, False, False, 0) self._searching_vbox.pack_start(self._searching_label, False, False, 0) vbNPDevices.pack_start(self._searching_vbox, True, True, 0) @@ -2436,6 +2435,7 @@ def _show_searching_spinner (self, text=""): else: self._searching_label.hide() self.ntbkNPType.hide () + self._searching_spinner.show () self._searching_spinner.start () self._searching_vbox.show () diff --git a/vectorspinner.py b/vectorspinner.py new file mode 100644 index 000000000..3a752a277 --- /dev/null +++ b/vectorspinner.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python3 +## vectorspinner.py - Custom Cairo-drawn vector spinner widget + +## A theme-independent spinner that renders identically across all +## GTK themes and Linux distributions. +## Authors: +## Alexander Pevzner +## Ayush Singh + +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published by +## the Free Software Foundation; either version 2 of the License, or +## (at your option) any later version. + +## If any error is found in this code, please report it to the author at ayushsinghceee@gmail.com + +import math +import cairo + +from gi.repository import Gtk +from gi.repository import GLib + + +class VectorSpinner(Gtk.DrawingArea): + """A custom spinner widget drawn with Cairo vectors. + + Unlike Gtk.Spinner, this renders identically across all GTK themes + and distributions since it draws its own animation frames using + Cairo vector paths. + + The spinner consists of evenly-spaced radial lines arranged in a + circle. Each frame, the "bright" line advances one position, + creating the classic rotating spinner effect through opacity fade. + """ + + def __init__(self, size=32, interval=20, num_lines=12): + """ + Args: + size: Widget width and height in pixels. + interval: Animation frame interval in milliseconds. + num_lines: Number of radial lines in the spinner. + """ + super().__init__() + self._size = size + self._interval = interval + self._num_lines = num_lines + self._step = 0 + self._timer_id = None + + self.set_size_request(size, size) + self.connect("draw", self._on_draw) + + def start(self): + """Start the spinner animation.""" + if self._timer_id is None: + self._timer_id = GLib.timeout_add(self._interval, self._tick) + + def stop(self): + """Stop the spinner animation.""" + if self._timer_id is not None: + GLib.source_remove(self._timer_id) + self._timer_id = None + + def _tick(self): + """Advance one animation frame smoothly.""" + + self._step = (self._step + 1) % 36 + self.queue_draw() + return True + + def _on_draw(self, widget, cr): + """Draw a modern, continuous rotating ring.""" + size = self._size + center = size / 2.0 + radius = size * 0.35 + line_width = max(size * 0.08, 2.0) + + cr.set_line_width(line_width) + cr.set_line_cap(cairo.LINE_CAP_ROUND) + cr.set_source_rgba(0.4, 0.4, 0.4, 1.0) + angle_offset = self._step * (2 * math.pi / 36) + start_angle = angle_offset + end_angle = angle_offset + (math.pi * 1.5) # 270 degree solid arc + + cr.arc(center, center, radius, start_angle, end_angle) + cr.stroke() + + return False + From f3fdf659caa0e47107e7a4c495a37eeb2f004148 Mon Sep 17 00:00:00 2001 From: SinghCod3r Date: Tue, 15 Sep 2026 22:20:44 +0530 Subject: [PATCH 8/8] made the panel stick to one place when searching happens --- newprinter.py | 36 ++++++++++++++++++++++-------------- vectorspinner.py | 19 +++++++++++++------ 2 files changed, 35 insertions(+), 20 deletions(-) diff --git a/newprinter.py b/newprinter.py index 7e54f6ae8..d6daec94c 100644 --- a/newprinter.py +++ b/newprinter.py @@ -414,8 +414,6 @@ def __init__(self): self.ntbkNPDownloadableDriverProperties.set_show_tabs(False) self.spinner_count = 0 - # Replace the UI-file GtkSpinner with our custom VectorSpinner - # so the spinner renders identically across all GTK themes. old_spinner = self.spinner spinner_parent = old_spinner.get_parent () if spinner_parent is not None: @@ -470,10 +468,18 @@ def protect_toggle (toggle_widget): self.tvNPDevices.connect ("row-activated", self.device_row_activated) self.tvNPDevices.connect ("row-expanded", self.device_row_expanded) - # Searching spinner — placed in the right-side panel (vbNPDevices) - # next to the Description/device-type notebook, per reviewer feedback. + # Searching spinner — placed in a Gtk.Stack alongside the Description + # notebook. This prevents the left and right panes from resizing when + # switching between them, as the Stack maintains the max size of both. vbNPDevices = self.ntbkNPType.get_parent () if vbNPDevices is not None: + self._searching_stack = Gtk.Stack() + self._searching_stack.set_transition_type(Gtk.StackTransitionType.CROSSFADE) + self._searching_stack.set_homogeneous(True) + + vbNPDevices.remove(self.ntbkNPType) + self._searching_stack.add_named(self.ntbkNPType, "notebook") + self._searching_vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12) self._searching_vbox.set_halign(Gtk.Align.CENTER) self._searching_vbox.set_valign(Gtk.Align.CENTER) @@ -489,9 +495,14 @@ def protect_toggle (toggle_widget): self._searching_label.set_justify(Gtk.Justification.CENTER) self._searching_vbox.pack_start(self._searching_spinner, False, False, 0) self._searching_vbox.pack_start(self._searching_label, False, False, 0) - vbNPDevices.pack_start(self._searching_vbox, True, True, 0) - self._searching_vbox.show_all () - self._searching_vbox.hide () + + self._searching_stack.add_named(self._searching_vbox, "spinner") + vbNPDevices.pack_start(self._searching_stack, True, True, 0) + vbNPDevices.reorder_child(self._searching_stack, 0) + self._searching_stack.show_all () + + # Start with the notebook visible + self._searching_stack.set_visible_child_name("notebook") else: self._searching_spinner = None @@ -2428,22 +2439,19 @@ def start_fetching_devices (self): del self.current_uri def _show_searching_spinner (self, text=""): - if getattr(self, '_searching_vbox', None) is not None: + if getattr(self, '_searching_stack', None) is not None: if text: self._searching_label.set_markup("%s" % text) self._searching_label.show() else: self._searching_label.hide() - self.ntbkNPType.hide () - self._searching_spinner.show () self._searching_spinner.start () - self._searching_vbox.show () + self._searching_stack.set_visible_child_name("spinner") def _hide_searching_spinner (self): - if getattr(self, '_searching_vbox', None) is not None: - self._searching_vbox.hide () + if getattr(self, '_searching_stack', None) is not None: self._searching_spinner.stop () - self.ntbkNPType.show () + self._searching_stack.set_visible_child_name("notebook") def add_devices (self, devices, current_uri, no_more=False): if no_more: diff --git a/vectorspinner.py b/vectorspinner.py index 3a752a277..01e6ca851 100644 --- a/vectorspinner.py +++ b/vectorspinner.py @@ -4,7 +4,7 @@ ## A theme-independent spinner that renders identically across all ## GTK themes and Linux distributions. ## Authors: -## Alexander Pevzner +## Alexander Pevzner ## Ayush Singh ## This program is free software; you can redistribute it and/or modify @@ -12,7 +12,14 @@ ## the Free Software Foundation; either version 2 of the License, or ## (at your option) any later version. -## If any error is found in this code, please report it to the author at ayushsinghceee@gmail.com +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. + +## You should have received a copy of the GNU General Public License +## along with this program; if not, write to the Free Software +## Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. import math import cairo @@ -24,13 +31,13 @@ class VectorSpinner(Gtk.DrawingArea): """A custom spinner widget drawn with Cairo vectors. + Note: We must use a custom implementation because the standard Gtk.Spinner + is unfortunately not customizable, at least in some Linux distributions + and system themes where CSS overrides for size are ignored. + Unlike Gtk.Spinner, this renders identically across all GTK themes and distributions since it draws its own animation frames using Cairo vector paths. - - The spinner consists of evenly-spaced radial lines arranged in a - circle. Each frame, the "bright" line advances one position, - creating the classic rotating spinner effect through opacity fade. """ def __init__(self, size=32, interval=20, num_lines=12):