[PATCH 1/4] A nice decorator making Anaconda's GUI more responsive

Vratislav Podzimek vpodzime at redhat.com
Wed Nov 13 12:46:50 UTC 2013


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.

Signed-off-by: Vratislav Podzimek <vpodzime at redhat.com>
---
 pyanaconda/ui/gui/__init__.py        | 12 +----
 pyanaconda/ui/gui/spokes/keyboard.py |  3 +-
 pyanaconda/ui/gui/utils.py           | 90 +++++++++++++++++++++++++++++++++++-
 3 files changed, 93 insertions(+), 12 deletions(-)

diff --git a/pyanaconda/ui/gui/__init__.py b/pyanaconda/ui/gui/__init__.py
index 645c8e8..92a40de 100644
--- a/pyanaconda/ui/gui/__init__.py
+++ b/pyanaconda/ui/gui/__init__.py
@@ -27,13 +27,13 @@ from pyanaconda.i18n import _
 from pyanaconda import product
 
 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", "busyCursor", "unbusyCursor", "QuitDialog"]
+__all__ = ["GraphicalUserInterface", "QuitDialog"]
 
 _screenshotIndex = 0
 
@@ -795,14 +795,6 @@ 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))
-
 def check_re(editable, data):
     """Perform an input validation check against a regular expression.
 
diff --git a/pyanaconda/ui/gui/spokes/keyboard.py b/pyanaconda/ui/gui/spokes/keyboard.py
index 1f35cd6..609bbaa 100644
--- a/pyanaconda/ui/gui/spokes/keyboard.py
+++ b/pyanaconda/ui/gui/spokes/keyboard.py
@@ -25,7 +25,7 @@ from gi.repository import Gkbd, Gdk, Gtk
 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, gtk_batch_map
+from pyanaconda.ui.gui.utils import enlightbox, gtk_call_once, escape_markup, gtk_batch_map, timed_action
 from pyanaconda import keyboard
 from pyanaconda import flags
 from pyanaconda.i18n import _, N_, CN_
@@ -164,6 +164,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 b0b5c0c..4c48844 100644
--- a/pyanaconda/ui/gui/utils.py
+++ b/pyanaconda/ui/gui/utils.py
@@ -25,7 +25,7 @@ from pyanaconda.threads import threadMgr, AnacondaThread
 
 from pyanaconda.constants import NOTICEABLE_FREEZE
 from contextlib import contextmanager
-from gi.repository import Gtk, GLib, AnacondaWidgets
+from gi.repository import Gdk, Gtk, GLib, AnacondaWidgets
 import Queue
 import gettext
 import time
@@ -179,6 +179,94 @@ def gtk_batch_map(action, items, args=(), pre_func=None, batch_size=1):
     done_event.wait()
     log.debug("Finished applying %s on %s", action, object.__repr__(items))
 
+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):
     # importing globally would cause a circular dependency
-- 
1.8.4.2



More information about the anaconda-patches mailing list