[PATCH 2/4 rhel7-branch] A nice decorator making Anaconda's GUI more responsive

Radek Vykydal rvykydal at redhat.com
Mon Sep 15 11:58:07 UTC 2014


From: Vratislav Podzimek <vpodzime at redhat.com>

Related: rhbz#1065716

In many cases we don't want to run expensive callbacks every time a signal is
emitted. We can just schedule the callback to be run later and let the changes
accumulate in the meantime.

For example in the AddLayout dialog, we don't want to refilter the list after
every single change of the entry. Doing so freezes GUI for quite a long time
leaving user with what seems to be a hanged installer.

This way we can schedule actions in a nice way, keeping quite short delay
between changes and callbacks and ensuring callbacks are run with some given
upper bound. Also, by letting the main loop run before the callback, we may set
the busy cursor to indicate something is being processed.

Port of commit 3e8b073ce8859b69e8b53d35a19615c3323fa500 from master.
---
 pyanaconda/ui/gui/__init__.py        | 13 +-----
 pyanaconda/ui/gui/spokes/filter.py   |  3 +-
 pyanaconda/ui/gui/spokes/keyboard.py |  3 +-
 pyanaconda/ui/gui/utils.py           | 91 +++++++++++++++++++++++++++++++++++-
 4 files changed, 96 insertions(+), 14 deletions(-)

diff --git a/pyanaconda/ui/gui/__init__.py b/pyanaconda/ui/gui/__init__.py
index 2c387c4..b64d983 100644
--- a/pyanaconda/ui/gui/__init__.py
+++ b/pyanaconda/ui/gui/__init__.py
@@ -28,13 +28,13 @@ from pyanaconda.constants import IPMI_ABORTED
 from pyanaconda import product, iutil
 
 from pyanaconda.ui import UserInterface, common
-from pyanaconda.ui.gui.utils import enlightbox, gtk_action_wait
+from pyanaconda.ui.gui.utils import enlightbox, gtk_action_wait, busyCursor, unbusyCursor
 import os.path
 
 import logging
 log = logging.getLogger("anaconda")
 
-__all__ = ["GraphicalUserInterface", "GUIObject", "busyCursor", "unbusyCursor", "QuitDialog"]
+__all__ = ["GraphicalUserInterface", "GUIObject", "QuitDialog"]
 
 _screenshotIndex = 0
 _last_screenshot_timestamp = 0
@@ -547,12 +547,3 @@ class GraphicalExceptionHandlingIface(meh.ui.gui.GraphicalIntf):
 
         return exc_window
 
-def busyCursor():
-    window = Gdk.get_default_root_window()
-    window.set_cursor(Gdk.Cursor(Gdk.CursorType.WATCH))
-
-def unbusyCursor():
-    window = Gdk.get_default_root_window()
-    window.set_cursor(Gdk.Cursor(Gdk.CursorType.ARROW))
-
-
diff --git a/pyanaconda/ui/gui/spokes/filter.py b/pyanaconda/ui/gui/spokes/filter.py
index af2662c..8c63816 100644
--- a/pyanaconda/ui/gui/spokes/filter.py
+++ b/pyanaconda/ui/gui/spokes/filter.py
@@ -33,7 +33,7 @@ from pyanaconda.flags import flags
 from pyanaconda.i18n import N_, P_
 
 from pyanaconda.ui.lib.disks import getDisks, size_str
-from pyanaconda.ui.gui.utils import enlightbox, escape_markup
+from pyanaconda.ui.gui.utils import enlightbox, escape_markup, timed_action
 from pyanaconda.ui.gui.spokes import NormalSpoke
 from pyanaconda.ui.gui.spokes.advstorage.fcoe import FCoEDialog
 from pyanaconda.ui.gui.spokes.advstorage.iscsi import ISCSIDialog
@@ -644,6 +644,7 @@ class FilterSpoke(NormalSpoke):
 
         self._update_summary()
 
+    @timed_action(delay=50, threshold=100)
     def on_refresh_clicked(self, widget, *args):
         self.storage.devicetree.populate()
         self.refresh()
diff --git a/pyanaconda/ui/gui/spokes/keyboard.py b/pyanaconda/ui/gui/spokes/keyboard.py
index 632d0fc..d306ba6 100644
--- a/pyanaconda/ui/gui/spokes/keyboard.py
+++ b/pyanaconda/ui/gui/spokes/keyboard.py
@@ -25,7 +25,7 @@ from gi.repository import Gkbd, Gtk, Gdk
 from pyanaconda.ui.gui import GUIObject
 from pyanaconda.ui.gui.spokes import NormalSpoke
 from pyanaconda.ui.gui.categories.localization import LocalizationCategory
-from pyanaconda.ui.gui.utils import enlightbox, gtk_call_once, escape_markup, override_cell_property
+from pyanaconda.ui.gui.utils import enlightbox, gtk_call_once, escape_markup, override_cell_property, timed_action
 from pyanaconda import keyboard
 from pyanaconda import flags
 from pyanaconda.i18n import _, N_
@@ -144,6 +144,7 @@ class AddLayoutDialog(GUIObject):
         selected = selection.count_selected_rows()
         self._confirmAddButton.set_sensitive(selected)
 
+    @timed_action()
     def on_entry_changed(self, *args):
         self._treeModelFilter.refilter()
 
diff --git a/pyanaconda/ui/gui/utils.py b/pyanaconda/ui/gui/utils.py
index 46e3232..c0682ea 100644
--- a/pyanaconda/ui/gui/utils.py
+++ b/pyanaconda/ui/gui/utils.py
@@ -24,7 +24,8 @@
 from pyanaconda.threads import threadMgr
 
 from contextlib import contextmanager
-from gi.repository import Gtk, GLib
+from gi.repository import Gdk, Gtk, GLib
+import time
 import Queue
 
 def gtk_call_once(func, *args):
@@ -92,6 +93,94 @@ def gtk_action_nowait(func):
     return _call_method
 
 
+def timed_action(delay=300, threshold=750, busy_cursor=True):
+    """
+    Function returning decorator for decorating often repeated actions that need
+    to happen in the main loop (entry/slider change callbacks, typically), but
+    that may take a long time causing the GUI freeze for a noticeable time.
+
+    :param delay: number of milliseconds to wait for another invocation of the
+                  decorated function before it is actually called
+    :type delay: int
+    :param threshold: upper bound (in milliseconds) to wait for the decorated
+                      function to be called from the first/last time
+    :type threshold: int
+    :param busy_cursor: whether the cursor should be made busy or not in the
+                        meantime of the decorated function being invocated from
+                        outside and it actually being called
+    :type busy_cursor: bool
+
+    """
+
+    class TimedAction(object):
+        """Class making the timing work."""
+
+        def __init__(self, func):
+            self._func = func
+            self._last_start = None
+            self._timer_id = None
+
+        def _run_once_one_arg(self, (args, kwargs)):
+            # run the function and clear stored values
+            self._func(*args, **kwargs)
+            self._last_start = None
+            self._timer_id = None
+            if busy_cursor:
+                unbusyCursor()
+
+            # function run, no need to schedule it again (return True would do)
+            return False
+
+        def run_func(self, *args, **kwargs):
+            # get timestamps from the first or/and current run
+            self._last_start = self._last_start or time.time()
+            tstamp = time.time()
+
+            if self._timer_id:
+                # remove the old timer scheduling the function
+                GLib.source_remove(self._timer_id)
+                self._timer_id = None
+
+            # are we over the threshold?
+            if (tstamp - self._last_start) * 1000 > threshold:
+                # over threshold, run the function right now and clear the
+                # timestamp
+                self._func(*args, **kwargs)
+                self._last_start = None
+
+            # schedule the function to be run later to allow additional changes
+            # in the meantime
+            if busy_cursor:
+                busyCursor()
+            self._timer_id = GLib.timeout_add(delay, self._run_once_one_arg,
+                                              (args, kwargs))
+
+    def decorator(func):
+        """
+        Decorator replacing the function with its timed version using an
+        instance of the TimedAction class.
+
+        :param func: the decorated function
+
+        """
+
+        ta = TimedAction(func)
+
+        def inner_func(*args, **kwargs):
+            ta.run_func(*args, **kwargs)
+
+        return inner_func
+
+    return decorator
+
+def busyCursor():
+    window = Gdk.get_default_root_window()
+    window.set_cursor(Gdk.Cursor(Gdk.CursorType.WATCH))
+
+def unbusyCursor():
+    window = Gdk.get_default_root_window()
+    window.set_cursor(Gdk.Cursor(Gdk.CursorType.ARROW))
+
 @contextmanager
 def enlightbox(mainWindow, dialog):
     from pyanaconda.ui.gui import ANACONDA_WINDOW_GROUP
-- 
1.9.3



More information about the anaconda-patches mailing list