[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