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

Martin Kolman mkolman at redhat.com
Wed Nov 13 12:09:40 UTC 2013


Martin Kolman píše v Po 11. 11. 2013 v 18:14 +0100:
> From: Vratislav Podzimek <vpodzime at redhat.com>
> 
> 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 3a47f0b..a42cf2b 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_
> @@ -161,6 +161,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 877515e..42c3d71 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
> @@ -173,6 +173,94 @@ def gtk_batch_map(action, items, args=(), pre_func=None, batch_size=10):
>      GLib.idle_add(process_one_batch, (item_queue, action, done_queue))
>      done_queue.get()
>  
> +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
Nice and convenient, ACK!



More information about the anaconda-patches mailing list