From: David Shea dshea@redhat.com
Add a method to TimedAction to cancel the timer and execute the function early, and a property to determine whether a timer is currently active.
Rearrange the guts of the decorator so that the TimedAction object is accessible as the decorated function (mostly), and is callable as the decorated function. This removes the need for the multi-layered inner functions and replaces them with opaque python magic. --- pyanaconda/ui/gui/utils.py | 59 ++++++++++++++++++++++++++++++++-------------- 1 file changed, 41 insertions(+), 18 deletions(-)
diff --git a/pyanaconda/ui/gui/utils.py b/pyanaconda/ui/gui/utils.py index 9c2105c..b4902f3 100644 --- a/pyanaconda/ui/gui/utils.py +++ b/pyanaconda/ui/gui/utils.py @@ -30,6 +30,7 @@ import queue import time import threading +import functools
import logging log = logging.getLogger("anaconda") @@ -216,6 +217,17 @@ def timed_action(delay=300, threshold=750, busy_cursor=True): 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.
+ The return value of the decorator function returned by this function--i.e., + the value of timed_action()(function_to_be_decorated)--is an instance of + the TimedAction class, which besides being callable provides a run_now + method to shortcut the timer and run the action immediately. run_now will + also be run in the main loop. + + If timed_action is used to decorate a method of a class, the decorated + method will actually be a functools.partial instance. In this case, the + TimedAction instance is accessible as the "func" property of the decorated + method. Note that the func property will not have self applied. + :param delay: number of milliseconds to wait for another invocation of the decorated function before it is actually called :type delay: int @@ -237,6 +249,11 @@ def __init__(self, func): self._last_start = None self._timer_id = None
+ @property + def timer_active(self): + """Whether there is a pending timer for this action.""" + return self._timer_id is not None + def _run_once_one_arg(self, arguments): (args, kwargs) = arguments # run the function and clear stored values @@ -249,7 +266,17 @@ def _run_once_one_arg(self, arguments): # function run, no need to schedule it again (return True would do) return False
- def run_func(self, *args, **kwargs): + @gtk_action_wait + def run_now(self, *args, **kwargs): + # Remove the old timer + if self._timer_id: + GLib.source_remove(self._timer_id) + self._timer_id = None + + # Run the function immediately + self._run_once_one_arg((args, kwargs)) + + def __call__(self, *args, **kwargs): # get timestamps from the first or/and current run self._last_start = self._last_start or time.time() tstamp = time.time() @@ -273,23 +300,19 @@ def run_func(self, *args, **kwargs): 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 + # This method is used by python to bind a class attribute to an + # instance of that class, so in the case of functions this is what + # converts a regular function into an instance method. Bind to the + # instance of whatever is being decorated by returning a curried version + # of ourself with the instance applied as the first argument. + def __get__(self, instance, owner): + return functools.partial(self, instance) + + # Return TimedAction as the decorator function. The constructor will be + # called with the function to be decorated as the argument, returning a + # TimedAction instance as the decorated function, and TimedAction.__call__ + # will be used for the calls to the decorated function. + return TimedAction
@contextmanager def blockedHandler(obj, func):
From: David Shea dshea@redhat.com
Modify GUIInputCheckHandler to use a timed_action between the changed signal and the validation function. Execute any pending timers before returning the status of a timed check, so the decision for whether to leave a spoke or dialog can be made based on current input. --- pyanaconda/ui/gui/helpers.py | 37 ++++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-)
diff --git a/pyanaconda/ui/gui/helpers.py b/pyanaconda/ui/gui/helpers.py index 2c9de61..37f042a 100644 --- a/pyanaconda/ui/gui/helpers.py +++ b/pyanaconda/ui/gui/helpers.py @@ -26,6 +26,34 @@ from gi.repository import Gtk
from pyanaconda.ui.helpers import InputCheck, InputCheckHandler +from pyanaconda.ui.gui.utils import timed_action + +class GUIInputCheck(InputCheck): + """ Add timer awareness to an InputCheck. + + Add a delay before running the validation function so that the + function is not run for every keystroke. Run any pending actions + before returning a status. + """ + + def __init__(self, parent, input_obj, run_check, data=None): + InputCheck.__init__(self, parent, input_obj, run_check, data) + + # Add the timer here instead of decorating a method so that a new + # TimedAction is created for every instance + self.update_check_status = timed_action(busy_cursor=False)(self.update_check_status) + + @property + def check_status(self): + if self.update_check_status.timer_active: + # The timer is hooked up to update_check_status, which takes no arguments. + # Since the timed_action wrapper was made around the bound method of a + # GUIInputCheck instance and not the function of a GUIInputCheck class, + # self is already applied and update_check_status is just a regular TimedAction + # object, not a curried function around the object. + self.update_check_status.run_now() + + return super(GUIInputCheck, self).check_status
# Inherit abstract methods from InputCheckHandler # pylint: disable=abstract-method @@ -43,8 +71,15 @@ def get_input(self, input_obj): return input_obj.get_text()
def add_check(self, input_obj, run_check, data=None): - checkRef = InputCheckHandler.add_check(self, input_obj, run_check, data) + # Use a GUIInputCheck to run the validation in a GLib timer + checkRef = GUIInputCheck(self, input_obj, run_check, data) + + # Start a new timer on each keystroke input_obj.connect_after("changed", self._update_check_status, checkRef) + + # Add the InputCheck to the parent class's list of checks + self._check_list.append(checkRef) + return checkRef
class GUIDialogInputCheckHandler(GUIInputCheckHandler, metaclass=ABCMeta):
From: David Shea dshea@redhat.com
This moves all the xalign properties around because properties are alphabetical now I guess, and removes a placeholder from somewhere.
Also removed that xalign=0.50<garbage> property since it was either an errant scroll or an old rounding error. --- .../ui/gui/spokes/lib/custom_storage_helpers.glade | 14 ++++++------- pyanaconda/ui/gui/spokes/lib/passphrase.glade | 10 ++++----- pyanaconda/ui/gui/spokes/password.glade | 6 +++--- pyanaconda/ui/gui/spokes/source.glade | 24 +++++++++------------- 4 files changed, 25 insertions(+), 29 deletions(-)
diff --git a/pyanaconda/ui/gui/spokes/lib/custom_storage_helpers.glade b/pyanaconda/ui/gui/spokes/lib/custom_storage_helpers.glade index d90f853..bc814e4 100644 --- a/pyanaconda/ui/gui/spokes/lib/custom_storage_helpers.glade +++ b/pyanaconda/ui/gui/spokes/lib/custom_storage_helpers.glade @@ -59,9 +59,9 @@ <object class="GtkLabel" id="confirmLabel"> <property name="visible">True</property> <property name="can_focus">False</property> + <property name="wrap">True</property> <property name="xalign">0</property> <property name="yalign">0</property> - <property name="wrap">True</property> <attributes> <attribute name="font-desc" value="Cantarell 11"/> </attributes> @@ -172,8 +172,8 @@ <object class="GtkLabel" id="disk_selection_title_label"> <property name="visible">True</property> <property name="can_focus">False</property> - <property name="xalign">0</property> <property name="label" translatable="yes">CONFIGURE MOUNT POINT</property> + <property name="xalign">0</property> <attributes> <attribute name="weight" value="bold"/> </attributes> @@ -365,10 +365,10 @@ <property name="visible">True</property> <property name="can_focus">False</property> <property name="margin_top">6</property> - <property name="xalign">1</property> <property name="label" translatable="yes" context="GUI|Custom Partitioning|Add Dialog">_Desired Capacity:</property> <property name="use_underline">True</property> <property name="mnemonic_widget">addSizeEntry</property> + <property name="xalign">1</property> <attributes> <attribute name="weight" value="bold"/> </attributes> @@ -382,10 +382,10 @@ <object class="GtkLabel" id="label6"> <property name="visible">True</property> <property name="can_focus">False</property> - <property name="xalign">0</property> <property name="label" translatable="yes" context="GUI|Custom Partitioning|Add Dialog">Mount _Point:</property> <property name="use_underline">True</property> <property name="mnemonic_widget">addMountPointEntry</property> + <property name="xalign">0</property> <attributes> <attribute name="weight" value="bold"/> </attributes> @@ -400,8 +400,8 @@ <property name="visible">True</property> <property name="can_focus">False</property> <property name="margin_bottom">12</property> - <property name="xalign">0</property> <property name="label" translatable="yes">ADD A NEW MOUNT POINT</property> + <property name="xalign">0</property> <attributes> <attribute name="weight" value="bold"/> <attribute name="scale" value="1.2"/> @@ -625,8 +625,8 @@ use. Try something else?</property> <object class="GtkLabel" id="container_dialog_title_label"> <property name="visible">True</property> <property name="can_focus">False</property> - <property name="xalign">0</property> <property name="label" translatable="yes">CONFIGURE CONTAINER</property> + <property name="xalign">0</property> <attributes> <attribute name="weight" value="bold"/> </attributes> @@ -777,8 +777,8 @@ use. Try something else?</property> <object class="GtkLabel" id="containerRaidLevelLabel"> <property name="visible">True</property> <property name="can_focus">False</property> - <property name="xalign">0</property> <property name="label" translatable="yes">RAID Level:</property> + <property name="xalign">0</property> </object> <packing> <property name="expand">False</property> diff --git a/pyanaconda/ui/gui/spokes/lib/passphrase.glade b/pyanaconda/ui/gui/spokes/lib/passphrase.glade index 57e4870..d87076c 100644 --- a/pyanaconda/ui/gui/spokes/lib/passphrase.glade +++ b/pyanaconda/ui/gui/spokes/lib/passphrase.glade @@ -61,9 +61,9 @@ <object class="GtkLabel" id="passphrase_dialog_title_label"> <property name="visible">True</property> <property name="can_focus">False</property> - <property name="xalign">0</property> <property name="ypad">1</property> <property name="label" translatable="yes">DISK ENCRYPTION PASSPHRASE</property> + <property name="xalign">0</property> <attributes> <attribute name="weight" value="bold"/> </attributes> @@ -78,9 +78,9 @@ <object class="GtkLabel" id="passphrase_description_label"> <property name="visible">True</property> <property name="can_focus">False</property> - <property name="xalign">0</property> <property name="label" translatable="yes">You have chosen to encrypt some of your data. You will need to create a passphrase that you will use to access your data when you start your computer.</property> <property name="wrap">True</property> + <property name="xalign">0</property> </object> <packing> <property name="expand">False</property> @@ -99,10 +99,10 @@ <object class="GtkLabel" id="passphrase_entry_label"> <property name="visible">True</property> <property name="can_focus">False</property> - <property name="xalign">0</property> <property name="label" translatable="yes" context="GUI|Passphrase Dialog">_Passphrase:</property> <property name="use_underline">True</property> <property name="mnemonic_widget">passphrase_entry</property> + <property name="xalign">0</property> </object> <packing> <property name="left_attach">0</property> @@ -128,10 +128,10 @@ <object class="GtkLabel" id="confirm_entry_label"> <property name="visible">True</property> <property name="can_focus">False</property> - <property name="xalign">0</property> <property name="label" translatable="yes" context="GUI|Passphrase Dialog">Con_firm:</property> <property name="use_underline">True</property> <property name="mnemonic_widget">confirm_pw_entry</property> + <property name="xalign">0</property> </object> <packing> <property name="left_attach">0</property> @@ -222,9 +222,9 @@ <object class="GtkLabel" id="passphrase_warning_label"> <property name="can_focus">False</property> <property name="hexpand">True</property> - <property name="xalign">0</property> <property name="wrap">True</property> <property name="max_width_chars">1</property> + <property name="xalign">0</property> </object> <packing> <property name="expand">False</property> diff --git a/pyanaconda/ui/gui/spokes/password.glade b/pyanaconda/ui/gui/spokes/password.glade index 6ea7cb9..67a8a6e 100644 --- a/pyanaconda/ui/gui/spokes/password.glade +++ b/pyanaconda/ui/gui/spokes/password.glade @@ -1,5 +1,5 @@ <?xml version="1.0" encoding="UTF-8"?> -<!-- Generated with glade 3.18.1 --> +<!-- Generated with glade 3.18.3 --> <interface> <requires lib="gtk+" version="3.6"/> <requires lib="AnacondaWidgets" version="1.0"/> @@ -50,10 +50,10 @@ <object class="GtkLabel" id="pwlabel"> <property name="visible">True</property> <property name="can_focus">False</property> - <property name="xalign">0</property> <property name="label" translatable="yes" context="GUI|Password">_Root Password:</property> <property name="use_underline">True</property> <property name="mnemonic_widget">pw</property> + <property name="xalign">0</property> </object> <packing> <property name="left_attach">0</property> @@ -64,10 +64,10 @@ <object class="GtkLabel" id="confirmlabel"> <property name="visible">True</property> <property name="can_focus">False</property> - <property name="xalign">0</property> <property name="label" translatable="yes" context="GUI|Password">_Confirm:</property> <property name="use_underline">True</property> <property name="mnemonic_widget">confirmPW</property> + <property name="xalign">0</property> </object> <packing> <property name="left_attach">0</property> diff --git a/pyanaconda/ui/gui/spokes/source.glade b/pyanaconda/ui/gui/spokes/source.glade index fe81465..0abbe9f 100644 --- a/pyanaconda/ui/gui/spokes/source.glade +++ b/pyanaconda/ui/gui/spokes/source.glade @@ -296,10 +296,10 @@ <object class="GtkLabel" id="label5"> <property name="visible">True</property> <property name="can_focus">False</property> - <property name="xalign">0</property> <property name="label" translatable="yes" context="GUI|Software Source|Proxy Dialog">_Proxy URL:</property> <property name="use_underline">True</property> <property name="mnemonic_widget">proxyURLEntry</property> + <property name="xalign">0</property> <attributes> <attribute name="weight" value="bold"/> </attributes> @@ -313,9 +313,9 @@ <object class="GtkLabel" id="label7"> <property name="visible">True</property> <property name="can_focus">False</property> - <property name="xalign">0</property> <property name="label" translatable="yes"><span size="small"><b>Example:</b> squid.mysite.org:3128</span></property> <property name="use_markup">True</property> + <property name="xalign">0</property> </object> <packing> <property name="left_attach">1</property> @@ -371,10 +371,10 @@ <object class="GtkLabel" id="label8"> <property name="visible">True</property> <property name="can_focus">False</property> - <property name="xalign">0</property> <property name="label" translatable="yes" context="GUI|Software Source|Proxy Dialog">User _name:</property> <property name="use_underline">True</property> <property name="mnemonic_widget">proxyUsernameEntry</property> + <property name="xalign">0</property> <attributes> <attribute name="weight" value="bold"/> </attributes> @@ -388,10 +388,10 @@ <object class="GtkLabel" id="label9"> <property name="visible">True</property> <property name="can_focus">False</property> - <property name="xalign">0</property> <property name="label" translatable="yes" context="GUI|Software Source|Proxy Dialog">Pass_word:</property> <property name="use_underline">True</property> <property name="mnemonic_widget">proxyPasswordEntry</property> + <property name="xalign">0</property> <attributes> <attribute name="weight" value="bold"/> </attributes> @@ -480,9 +480,6 @@ <child internal-child="nav_area"> <object class="GtkGrid" id="AnacondaSpokeWindow-nav_area1"> <property name="can_focus">False</property> - <child> - <placeholder/> - </child> </object> </child> </object> @@ -567,8 +564,8 @@ <object class="GtkLabel" id="autodetectDeviceLabel"> <property name="visible">True</property> <property name="can_focus">False</property> - <property name="xalign">0</property> <property name="label" translatable="yes">Device:</property> + <property name="xalign">0</property> </object> <packing> <property name="left_attach">0</property> @@ -579,8 +576,8 @@ <object class="GtkLabel" id="autodetectLabel"> <property name="visible">True</property> <property name="can_focus">False</property> - <property name="xalign">0</property> <property name="label" translatable="yes">Label:</property> + <property name="xalign">0</property> </object> <packing> <property name="left_attach">0</property> @@ -929,7 +926,6 @@ <property name="visible">True</property> <property name="can_focus">False</property> <property name="halign">start</property> - <property name="xalign">0.50999999046325684</property> <property name="label" translatable="yes">Additional repositories</property> <attributes> <attribute name="font-desc" value="Cantarell 10"/> @@ -1156,10 +1152,10 @@ <object class="GtkLabel" id="label14"> <property name="visible">True</property> <property name="can_focus">False</property> - <property name="xalign">0</property> <property name="label" translatable="yes" context="GUI|Software Source">_Name:</property> <property name="use_underline">True</property> <property name="mnemonic_widget">repoNameEntry</property> + <property name="xalign">0</property> <attributes> <attribute name="weight" value="bold"/> </attributes> @@ -1194,10 +1190,10 @@ <object class="GtkLabel" id="label15"> <property name="visible">True</property> <property name="can_focus">False</property> - <property name="xalign">0</property> <property name="label" translatable="yes" context="GUI|Software Source">Pro_xy URL:</property> <property name="use_underline">True</property> <property name="mnemonic_widget">repoProxyUrlEntry</property> + <property name="xalign">0</property> <attributes> <attribute name="weight" value="bold"/> </attributes> @@ -1211,10 +1207,10 @@ <object class="GtkLabel" id="label16"> <property name="visible">True</property> <property name="can_focus">False</property> - <property name="xalign">0</property> <property name="label" translatable="yes" context="GUI|Software Source">U_ser name:</property> <property name="use_underline">True</property> <property name="mnemonic_widget">repoProxyUsernameEntry</property> + <property name="xalign">0</property> <attributes> <attribute name="weight" value="bold"/> </attributes> @@ -1228,10 +1224,10 @@ <object class="GtkLabel" id="label17"> <property name="visible">True</property> <property name="can_focus">False</property> - <property name="xalign">0</property> <property name="label" translatable="yes" context="GUI|Software Source">Pass_word:</property> <property name="use_underline">True</property> <property name="mnemonic_widget">repoProxyPasswordEntry</property> + <property name="xalign">0</property> <attributes> <attribute name="weight" value="bold"/> </attributes>
From: David Shea dshea@redhat.com
This is the code that automatically removes the http:// part of the URL when you paste in a URL. This should be run right away instead of waiting for the input validation timer, so move it out of the validation function and into a separate signal handler. --- pyanaconda/ui/gui/spokes/source.glade | 1 + pyanaconda/ui/gui/spokes/source.py | 45 +++++++++++++++++++++++------------ 2 files changed, 31 insertions(+), 15 deletions(-)
diff --git a/pyanaconda/ui/gui/spokes/source.glade b/pyanaconda/ui/gui/spokes/source.glade index 0abbe9f..e8c3640 100644 --- a/pyanaconda/ui/gui/spokes/source.glade +++ b/pyanaconda/ui/gui/spokes/source.glade @@ -765,6 +765,7 @@ <property name="visible">True</property> <property name="can_focus">True</property> <property name="hexpand">True</property> + <signal name="changed" handler="on_urlEntry_changed" swapped="no"/> </object> <packing> <property name="left_attach">1</property> diff --git a/pyanaconda/ui/gui/spokes/source.py b/pyanaconda/ui/gui/spokes/source.py index ed259a3..120d7da 100644 --- a/pyanaconda/ui/gui/spokes/source.py +++ b/pyanaconda/ui/gui/spokes/source.py @@ -926,22 +926,11 @@ def _checkURL(self, inputcheck, combo): if not m: return _("Invalid URL")
- # If there is a protocol in the URL, and the protocol matches the - # combo box, just remove it. This makes it more convenient to paste - # in URLs. It'll probably freak out people who are typing out http:// - # in the box themselves, but why would you do that? Don't do that. - # If the protocols don't match, complain. + # Matching protocols in the URL should already have been removed + # by _removeUrlPrefix. If there's still one there, it's wrong. url_protocol = m.group('protocol') if url_protocol: - if (url_protocol == 'http://' and combo_protocol == PROTOCOL_HTTP) or \ - (url_protocol == 'https://' and combo_protocol == PROTOCOL_HTTPS) or \ - (url_protocol == 'ftp://' and combo_protocol == PROTOCOL_FTP): - # Disable the check to block a recursive check call - inputcheck.enabled = False - inputcheck.input_obj.set_text(url_string[len(url_protocol):]) - inputcheck.enabled = True - else: - return _("Protocol in URL does not match selected protocol") + return _("Protocol in URL does not match selected protocol") elif combo_protocol == PROTOCOL_NFS: if not url_string: return _("NFS server is empty") @@ -1305,6 +1294,29 @@ def _update_repo_info(self, repo): log.error("Failed to parse proxy for repo %s: %s", repo.name, e) return
+ def _removeUrlPrefix(self, editable, combo, handler): + # If there is a protocol in the URL, and the protocol matches the + # combo box, just remove it. This makes it more convenient to paste + # in URLs. It'll probably freak out people who are typing out http:// + # in the box themselves, but why would you do that? Don't do that. + + combo_protocol = combo.get_active_id() + if combo_protocol in (PROTOCOL_HTTP, PROTOCOL_HTTPS, PROTOCOL_FTP): + url_string = editable.get_text() + m = URL_PARSE.match(url_string) + if m: + url_protocol = m.group('protocol') + if (url_protocol == 'http://' and combo_protocol == PROTOCOL_HTTP) or \ + (url_protocol == 'https://' and combo_protocol == PROTOCOL_HTTPS) or \ + (url_protocol == 'ftp://' and combo_protocol == PROTOCOL_FTP): + # URL protocol matches. Block the changed signal and remove it + with blockedHandler(editable, handler): + editable.set_text(url_string[len(url_protocol):]) + + def on_urlEntry_changed(self, editable, data=None): + # Check for and remove a URL prefix that matches the protocol dropdown + self._removeUrlPrefix(editable, self._protocolComboBox, self.on_urlEntry_changed) + def on_noUpdatesCheckbox_toggled(self, *args): """ Toggle the enable state of the updates repo
@@ -1376,7 +1388,7 @@ def on_repoNameEntry_changed(self, entry): del self._repoChecks[old_name] self._repoChecks[name].name_check.update_check_status()
- def on_repoUrl_changed(self, *args): + def on_repoUrl_changed(self, editable, data=None): """ proxy url or protocol changed """ itr = self._repoSelection.get_selected()[1] @@ -1393,6 +1405,9 @@ def on_repoUrl_changed(self, *args):
self._repoChecks[repo.name].url_check.update_check_status()
+ # Check for and remove a URL prefix that matches the protocol dropdown + self._removeUrlPrefix(editable, self._repoProtocolComboBox, self.on_repoUrl_changed) + def on_repoMirrorlistCheckbox_toggled(self, *args): """ mirror state changed """
From: David Shea dshea@redhat.com
Add some functionality to GUIDialogInputCheckHandler to handle setting button sensitivity, and remove the abstractmethod decorator from set_status so that classes using GUIDialogInputCheckHandler can use the new, improved set_status as-is without implementing their own.
Rearrange the running and destroying of GtkDialogs with input validation to handle the possibility of the dialog returning a response while input is valid. Move all state saving out of button clicked handlers and into the return from gtk_dialog_run, ensure that the action buttons return different response for OK and cancel, and re-run gtk_dialog_run if the dialog returned a response while input is invalid. --- pyanaconda/ui/gui/helpers.py | 51 +++++++++++-- pyanaconda/ui/gui/spokes/datetime_spoke.py | 13 ++-- .../ui/gui/spokes/lib/custom_storage_helpers.glade | 1 - .../ui/gui/spokes/lib/custom_storage_helpers.py | 35 +++++---- pyanaconda/ui/gui/spokes/lib/passphrase.glade | 2 +- pyanaconda/ui/gui/spokes/lib/passphrase.py | 30 +++++++- pyanaconda/ui/gui/spokes/source.glade | 4 +- pyanaconda/ui/gui/spokes/source.py | 49 ++++++++---- pyanaconda/ui/gui/spokes/user.py | 89 +++++++++++----------- 9 files changed, 179 insertions(+), 95 deletions(-)
diff --git a/pyanaconda/ui/gui/helpers.py b/pyanaconda/ui/gui/helpers.py index 2c45a80..deed2b5 100644 --- a/pyanaconda/ui/gui/helpers.py +++ b/pyanaconda/ui/gui/helpers.py @@ -85,13 +85,32 @@ def add_check(self, input_obj, run_check, data=None): class GUIDialogInputCheckHandler(GUIInputCheckHandler, metaclass=ABCMeta): """Provide InputCheckHandler functionality for Gtk dialogs.
- This class provides a helper method for setting an error message - on an entry field. Implementors of this class must still provide - a set_status method in order to control the sensitivty of widgets or - ignore activated signals. + If an OK button is provided in the constructor, this class will + handle setting the sensitivity of the button to match the input + check result. A method on_ok_clicked is provided to determine whether + the dialog can be exited, similar to on_back_clicked for spokes. + + It's not possible (or at least not easy) to prent a GtkDialog from + returning a response, so the caller of gtk_dialog_run needs to check + whether the input is valid and decide based on that whether to destroy + the dialog or call gtk_dialog_run again. """
- @abstractmethod + def __init__(self, ok_button=None): + GUIInputCheckHandler.__init__(self) + self._ok_button = ok_button + + def _update_check_status(self, editable, inputcheck): + # If an OK button was provided, set it to sensitive on any change in + # input. This way if a user changes invalid input to valid, they can + # immediately leave the dialog. This also means that there will be a + # period in which the user is not prented from leaving with empty input, + # and this condition needs to be checked. + if self._ok_button: + self._ok_button.set_sensitive(True) + + return super(GUIDialogInputCheckHandler, self)._update_check_status(editable, inputcheck) + def set_status(self, inputcheck): if inputcheck.check_status in (InputCheck.CHECK_OK, InputCheck.CHECK_SILENT): inputcheck.input_obj.set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY, None) @@ -102,6 +121,28 @@ def set_status(self, inputcheck): inputcheck.input_obj.set_icon_tooltip_text(Gtk.EntryIconPosition.SECONDARY, inputcheck.check_status)
+ # Update the ok button sensitivity based on the check status. + # If the result is CHECK_OK, set_sensitive(True) still needs to be + # called, even though the changed handler above also makes the button + # sensitive. A direct call to update_check_status may have bypassed the + # changed signal. + if self._ok_button: + self._ok_button.set_sensitive(inputcheck.check_status == InputCheck.CHECK_OK) + + def on_ok_clicked(self): + """Return whether the input validation checks allow the dialog to be exited. + + Unlike GUISpokeInputCheckHandler.on_back_clicked, it is not expected that + subclasses will implement this method. + """ + failed_check = next(self.failed_checks, None) + + if failed_check: + failed_check.input_obj.grab_focus() + return False + else: + return True + class GUISpokeInputCheckHandler(GUIInputCheckHandler, metaclass=ABCMeta): """Provide InputCheckHandler functionality for graphical spokes.
diff --git a/pyanaconda/ui/gui/spokes/datetime_spoke.py b/pyanaconda/ui/gui/spokes/datetime_spoke.py index d014589..cc88c4f 100644 --- a/pyanaconda/ui/gui/spokes/datetime_spoke.py +++ b/pyanaconda/ui/gui/spokes/datetime_spoke.py @@ -155,7 +155,11 @@ class NTPconfigDialog(GUIObject, GUIDialogInputCheckHandler):
def __init__(self, *args): GUIObject.__init__(self, *args) - GUIDialogInputCheckHandler.__init__(self) + + # Use GUIDIalogInputCheckHandler to manipulate the sensitivity of the + # add button, and check for valid input in on_entry_activated + add_button = self.builder.get_object("addButton") + GUIDialogInputCheckHandler.__init__(self, add_button)
#epoch is increased when serversStore is repopulated self._epoch = 0 @@ -246,13 +250,6 @@ def _validateServer(self, inputcheck): else: return InputCheck.CHECK_OK
- def set_status(self, inputcheck): - # Use GUIDialogInputCheckHandler to set the error message - GUIDialogInputCheckHandler.set_status(self, inputcheck) - - # Set the sensitivity of the add button based on the result - self._addButton.set_sensitive(inputcheck.check_status == InputCheck.CHECK_OK) - def refresh(self): self._serverEntry.grab_focus()
diff --git a/pyanaconda/ui/gui/spokes/lib/custom_storage_helpers.glade b/pyanaconda/ui/gui/spokes/lib/custom_storage_helpers.glade index bc814e4..364a6ce 100644 --- a/pyanaconda/ui/gui/spokes/lib/custom_storage_helpers.glade +++ b/pyanaconda/ui/gui/spokes/lib/custom_storage_helpers.glade @@ -599,7 +599,6 @@ use. Try something else?</property> <property name="can_focus">True</property> <property name="receives_default">True</property> <property name="use_underline">True</property> - <signal name="clicked" handler="on_save_clicked" swapped="no"/> </object> <packing> <property name="expand">False</property> diff --git a/pyanaconda/ui/gui/spokes/lib/custom_storage_helpers.py b/pyanaconda/ui/gui/spokes/lib/custom_storage_helpers.py index 7510ddb..2c363cf 100644 --- a/pyanaconda/ui/gui/spokes/lib/custom_storage_helpers.py +++ b/pyanaconda/ui/gui/spokes/lib/custom_storage_helpers.py @@ -470,8 +470,6 @@ class ContainerDialog(GUIObject, GUIDialogInputCheckHandler): MIN_SIZE_ENTRY = Size("1 MiB")
def __init__(self, *args, **kwargs): - GUIDialogInputCheckHandler.__init__(self) - # these are all absolutely required. not getting them is fatal. self._disks = kwargs.pop("disks") free = kwargs.pop("free") @@ -492,6 +490,7 @@ def __init__(self, *args, **kwargs): GUIObject.__init__(self, *args, **kwargs)
self._grabObjects() + GUIDialogInputCheckHandler.__init__(self, self._save_button)
# set up the dialog labels with device-type-specific text container_type = get_container_type(self.device_type) @@ -579,9 +578,8 @@ def _get_disk_by_id(self, disk_id): if disk.id == disk_id: return disk
- def on_save_clicked(self, button): + def _save_clicked(self): if self.exists: - self.window.destroy() return
model, paths = self._treeview.get_selection().get_selected_rows() @@ -628,14 +626,30 @@ def on_save_clicked(self, button): self.size_policy = size
self._error_label.set_text("") - self.window.destroy()
def run(self): while True: self._error = None rc = self.window.run() - if not self._error: - return rc + if rc == 1: + # Save clicked and input validation passed, try saving it + if self.on_ok_clicked(): + self._save_clicked() + + # If that failed, try again + if self._error: + continue + else: + break + # Save clicked with invalid input, try again + else: + continue + else: + # Cancel or something similar, just exit + break + + self.window.destroy() + return rc
def on_size_changed(self, combo): active_index = combo.get_active() @@ -686,10 +700,3 @@ def _checkNameEntry(self, inputcheck): return _("Invalid container name")
return InputCheck.CHECK_OK - - def set_status(self, inputcheck): - # Use the superclass set_status to set the error message - GUIDialogInputCheckHandler.set_status(self, inputcheck) - - # Change the sensitivity of the Save button - self._save_button.set_sensitive(next(self.failed_checks, None) == None) diff --git a/pyanaconda/ui/gui/spokes/lib/passphrase.glade b/pyanaconda/ui/gui/spokes/lib/passphrase.glade index d87076c..f2ea51f 100644 --- a/pyanaconda/ui/gui/spokes/lib/passphrase.glade +++ b/pyanaconda/ui/gui/spokes/lib/passphrase.glade @@ -41,7 +41,6 @@ <property name="can_default">True</property> <property name="receives_default">True</property> <property name="use_underline">True</property> - <signal name="clicked" handler="on_save_clicked" swapped="no"/> </object> <packing> <property name="expand">False</property> @@ -146,6 +145,7 @@ <property name="invisible_char">●</property> <property name="width_chars">32</property> <signal name="activate" handler="on_entry_activated" swapped="no"/> + <signal name="changed" handler="on_confirm_changed" swapped="no"/> </object> <packing> <property name="left_attach">1</property> diff --git a/pyanaconda/ui/gui/spokes/lib/passphrase.py b/pyanaconda/ui/gui/spokes/lib/passphrase.py index 7b04653..8264ab1 100644 --- a/pyanaconda/ui/gui/spokes/lib/passphrase.py +++ b/pyanaconda/ui/gui/spokes/lib/passphrase.py @@ -108,7 +108,26 @@ def refresh(self): def run(self): self.refresh() self.window.show_all() - rc = self.window.run() + + while True: + rc = self.window.run() + if rc == 1: + # Force an update of all the checks and then see what set_status + # did to the sensitivity of the save button + for check in self.checks: + check.update_check_status.run_now() + + if self._save_button.get_sensitive(): + # Input ok, save the passphrase + self.passphrase = self._passphrase_entry.get_text() + break + else: + # Input not ok, try again + continue + else: + # Cancel, destroy the window + break + self.window.destroy() return rc
@@ -188,8 +207,13 @@ def on_passphrase_changed(self, entry): # Update the match check for changes in the main passphrase field self._passphrase_match_check.update_check_status()
- def on_save_clicked(self, button): - self.passphrase = self._passphrase_entry.get_text() + # Set the OK button to sensitive any time the passphrase or confirm + # entries change, so that the user can attempt to exit the dialog + # without waiting for the input validation timer + self._save_button.set_sensitive(True) + + def on_confirm_changed(self, entry): + self._save_button.set_sensitive(True)
def on_entry_activated(self, entry): if self._save_button.get_sensitive() and \ diff --git a/pyanaconda/ui/gui/spokes/source.glade b/pyanaconda/ui/gui/spokes/source.glade index e8c3640..1d4260c 100644 --- a/pyanaconda/ui/gui/spokes/source.glade +++ b/pyanaconda/ui/gui/spokes/source.glade @@ -226,7 +226,6 @@ <property name="can_focus">True</property> <property name="receives_default">True</property> <property name="use_underline">True</property> - <signal name="clicked" handler="on_proxy_cancel_clicked" swapped="no"/> </object> <packing> <property name="expand">False</property> @@ -241,7 +240,6 @@ <property name="can_focus">True</property> <property name="receives_default">True</property> <property name="use_underline">True</property> - <signal name="clicked" handler="on_proxy_ok_clicked" swapped="no"/> </object> <packing> <property name="expand">False</property> @@ -450,7 +448,7 @@ </child> <action-widgets> <action-widget response="0">proxyCancelButton</action-widget> - <action-widget response="0">proxyOkButton</action-widget> + <action-widget response="1">proxyOkButton</action-widget> </action-widgets> </object> <object class="GtkListStore" id="repoStore"> diff --git a/pyanaconda/ui/gui/spokes/source.py b/pyanaconda/ui/gui/spokes/source.py index 120d7da..7ad6a44 100644 --- a/pyanaconda/ui/gui/spokes/source.py +++ b/pyanaconda/ui/gui/spokes/source.py @@ -110,9 +110,11 @@ class ProxyDialog(GUIObject, GUIDialogInputCheckHandler):
def __init__(self, data, proxy_url): GUIObject.__init__(self, data) - GUIDialogInputCheckHandler.__init__(self) - self.proxyUrl = proxy_url
+ self._proxyOkButton = self.builder.get_object("proxyOkButton") + GUIDialogInputCheckHandler.__init__(self, self._proxyOkButton) + + self.proxyUrl = proxy_url self._proxyCheck = self.builder.get_object("enableProxyCheck") self._proxyInfoBox = self.builder.get_object("proxyInfoBox") self._authCheck = self.builder.get_object("enableAuthCheck") @@ -121,7 +123,6 @@ def __init__(self, data, proxy_url): self._proxyURLEntry = self.builder.get_object("proxyURLEntry") self._proxyUsernameEntry = self.builder.get_object("proxyUsernameEntry") self._proxyPasswordEntry = self.builder.get_object("proxyPasswordEntry") - self._proxyOkButton = self.builder.get_object("proxyOkButton")
self._proxyValidate = self.add_check(self._proxyURLEntry, self._checkProxyURL) self._proxyValidate.update_check_status() @@ -129,7 +130,7 @@ def __init__(self, data, proxy_url): def _checkProxyURL(self, inputcheck): proxy_string = self.get_input(inputcheck.input_obj)
- # Don't set an error icon on empty input, but keep the add button insensitive. + # Don't set an error icon on empty input, but still consider it an error if not proxy_string: return InputCheck.CHECK_SILENT
@@ -138,13 +139,6 @@ def _checkProxyURL(self, inputcheck):
return _validateProxy(proxy_string, username_set, password_set)
- def set_status(self, inputcheck): - # Use the superclass set_status to set the error message - GUIDialogInputCheckHandler.set_status(self, inputcheck) - - # Change the sensitivity of the Add button - self._proxyOkButton.set_sensitive(inputcheck.check_status == InputCheck.CHECK_OK) - # Update the proxy validation check on username and password changes to catch # changes in duplicated authentication data def on_proxyUsernameEntry_changed(self, entry, user_data=None): @@ -153,9 +147,6 @@ def on_proxyUsernameEntry_changed(self, entry, user_data=None): def on_proxyPasswordEntry_changed(self, entry, user_data=None): self._proxyValidate.update_check_status()
- def on_proxy_cancel_clicked(self, *args): - self.window.destroy() - def on_proxy_ok_clicked(self, *args): if self._proxyCheck.get_active(): url = self._proxyURLEntry.get_text() @@ -213,7 +204,35 @@ def refresh(self): self.on_proxy_auth_toggled(self._authCheck)
def run(self): - self.window.run() + while True: + response = self.window.run() + + if response == 1: + if self.on_ok_clicked(): + # Ok clicked with valid input, save the proxy data + if self._proxyCheck.get_active(): + url = self._proxyURLEntry.get_text() + + if self._authCheck.get_active(): + username = self._proxyUsernameEntry.get_text() + password = self._proxyPasswordEntry.get_text() + else: + username = None + password = None + + proxy = ProxyString(url=url, username=username, password=password) + self.proxyUrl = proxy.url + else: + self.proxyUrl = "" + break + else: + # Ok clicked with invalid input, keep running the dialog + continue + else: + # Cancel or Esc, just exit + break + + self.window.destroy()
class MediaCheckDialog(GUIObject): builderObjects = ["mediaCheckDialog"] diff --git a/pyanaconda/ui/gui/spokes/user.py b/pyanaconda/ui/gui/spokes/user.py index e837d88..7a41966 100644 --- a/pyanaconda/ui/gui/spokes/user.py +++ b/pyanaconda/ui/gui/spokes/user.py @@ -48,16 +48,6 @@ class AdvancedUserDialog(GUIObject, GUIDialogInputCheckHandler): mainWidgetName = "advancedUserDialog" uiFile = "spokes/advanced_user.glade"
- def set_status(self, inputcheck): - # Use the superclass set_status to set the error message - GUIDialogInputCheckHandler.set_status(self, inputcheck) - - # Make the save button insensitive if the check fails - if inputcheck.check_status == InputCheck.CHECK_OK: - self._saveButton.set_sensitive(True) - else: - self._saveButton.set_sensitive(False) - def _validateGroups(self, inputcheck): groups_string = self.get_input(inputcheck.input_obj)
@@ -75,7 +65,10 @@ def _validateGroups(self, inputcheck):
def __init__(self, user, groupDict, data): GUIObject.__init__(self, data) - GUIDialogInputCheckHandler.__init__(self) + + self._saveButton = self.builder.get_object("save_button") + GUIDialogInputCheckHandler.__init__(self, self._saveButton) + self._user = user self._groupDict = groupDict
@@ -94,7 +87,6 @@ def _grabObjects(self): self._spinGid = self.builder.get_object("spin_gid") self._uid = self.builder.get_object("uid") self._gid = self.builder.get_object("gid") - self._saveButton = self.builder.get_object("save_button")
def initialize(self): GUIObject.initialize(self) @@ -163,42 +155,49 @@ def refresh(self):
def run(self): self.window.show() - rc = self.window.run() - self.window.hide() - - #OK clicked - if rc == 1: - # If the user changed the home directory input, either this time or - # during any earlier run of the dialog, set homedir to the value - # in the input box. - homedir = self._tHome.get_text() - if not os.path.isabs(homedir): - homedir = "/" + homedir - if self._homeSet or self._origHome != homedir: - self._homeSet = True - self._user.homedir = homedir - - if self._cUid.get_active(): - self._user.uid = int(self._uid.get_value()) - else: - self._user.uid = None + while True: + rc = self.window.run() + + #OK clicked + if rc == 1: + # Input checks pass + if self.on_ok_clicked(): + # If the user changed the home directory input, either this time or + # during any earlier run of the dialog, set homedir to the value + # in the input box. + homedir = self._tHome.get_text() + if not os.path.isabs(homedir): + homedir = "/" + homedir + if self._homeSet or self._origHome != homedir: + self._homeSet = True + self._user.homedir = homedir + + if self._cUid.get_active(): + self._user.uid = int(self._uid.get_value()) + else: + self._user.uid = None + + if self._cGid.get_active(): + self._user.gid = int(self._gid.get_value()) + else: + self._user.gid = None + + groups = self._parse_groups() + self._user.groups = [] + self._groupDict.clear() + for group in groups: + self._groupDict[group.name] = group + self._user.groups.append(group.name) + break + # Input checks fail, try again + else: + continue
- if self._cGid.get_active(): - self._user.gid = int(self._gid.get_value()) + #Cancel clicked, window destroyed... else: - self._user.gid = None - - groups = self._parse_groups() - self._user.groups = [] - self._groupDict.clear() - for group in groups: - self._groupDict[group.name] = group - self._user.groups.append(group.name) - - #Cancel clicked, window destroyed... - else: - pass + break
+ self.window.hide() return rc
class UserSpoke(FirstbootSpokeMixIn, NormalSpoke, GUISpokeInputCheckHandler):
From: David Shea dshea@redhat.com
The capslock warning is handled by Gtk, and this label was never hooked up to anything anyway. --- pyanaconda/ui/gui/spokes/password.glade | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-)
diff --git a/pyanaconda/ui/gui/spokes/password.glade b/pyanaconda/ui/gui/spokes/password.glade index 67a8a6e..a146741 100644 --- a/pyanaconda/ui/gui/spokes/password.glade +++ b/pyanaconda/ui/gui/spokes/password.glade @@ -111,17 +111,6 @@ </packing> </child> <child> - <object class="GtkLabel" id="capslock"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="use_markup">True</property> - </object> - <packing> - <property name="left_attach">1</property> - <property name="top_attach">3</property> - </packing> - </child> - <child> <object class="GtkBox" id="box2"> <property name="visible">True</property> <property name="can_focus">False</property> @@ -180,6 +169,9 @@ <child> <placeholder/> </child> + <child> + <placeholder/> + </child> </object> <packing> <property name="expand">False</property>
From: David Shea dshea@redhat.com
Having set_status called for every call of update_check_status makes things work a bit nicer in a world where there may be a delay between an update_check_status call and the status actually updating.
The only thing that really cared about avoiding set_status calls for repeated statuses was the spoke checker, so track whether the status is changing inside the spoke checker. --- pyanaconda/ui/gui/helpers.py | 15 +++++++++++++-- pyanaconda/ui/helpers.py | 8 ++------ 2 files changed, 15 insertions(+), 8 deletions(-)
diff --git a/pyanaconda/ui/gui/helpers.py b/pyanaconda/ui/gui/helpers.py index 37f042a..2c45a80 100644 --- a/pyanaconda/ui/gui/helpers.py +++ b/pyanaconda/ui/gui/helpers.py @@ -110,14 +110,25 @@ class GUISpokeInputCheckHandler(GUIInputCheckHandler, metaclass=ABCMeta): prevent the user from exiting a spoke with bad input. """
+ def __init__(self): + GUIInputCheckHandler.__init__(self) + + # Store the previous status to avoid setting the info bar to the same + # message multiple times + self._prev_status = None + def set_status(self, inputcheck): """Update the warning with the input validation error from the first error message. """ failed_check = next(self.failed_checks_with_message, None)
- self.clear_info() - if failed_check: + if not failed_check: + self.clear_info() + self._prev_status = None + elif failed_check.check_status != self._prev_status: + self._prev_status = failed_check.check_status + self.clear_info() self.set_warning(failed_check.check_status)
# Implemented by GUIObject diff --git a/pyanaconda/ui/helpers.py b/pyanaconda/ui/helpers.py index 0730afb..724834f 100644 --- a/pyanaconda/ui/helpers.py +++ b/pyanaconda/ui/helpers.py @@ -243,12 +243,8 @@ def update_check_status(self): if not self.enabled: return
- new_check_status = self._run_check(self) - check_status_changed = (self.check_status != new_check_status) - self._check_status = new_check_status - - if check_status_changed: - self._parent.set_status(self) + self._check_status = self._run_check(self) + self._parent.set_status(self)
@property def enabled(self):
From: David Shea dshea@redhat.com
For each of the password windows, only use one InputCheck instance to validate the match, attached to confirm field so that the confirm field is focused on errors. Add a changed handler to update the InputCheck whenever the main password field changes. --- pyanaconda/ui/gui/spokes/lib/passphrase.py | 23 ++++++++--------------- pyanaconda/ui/gui/spokes/password.glade | 2 +- pyanaconda/ui/gui/spokes/password.py | 28 ++++++++++------------------ pyanaconda/ui/gui/spokes/user.py | 23 ++++++----------------- 4 files changed, 25 insertions(+), 51 deletions(-)
diff --git a/pyanaconda/ui/gui/spokes/lib/passphrase.py b/pyanaconda/ui/gui/spokes/lib/passphrase.py index c104194..7b04653 100644 --- a/pyanaconda/ui/gui/spokes/lib/passphrase.py +++ b/pyanaconda/ui/gui/spokes/lib/passphrase.py @@ -70,8 +70,10 @@ def __init__(self, data): self._pwq_error = None self.passphrase = ""
- self._passphrase_match_check = self.add_check(self._passphrase_entry, self._checkMatch) - self._confirm_match_check = self.add_check(self._confirm_entry, self._checkMatch) + # the passphrase confirmation needs to be checked whenever either of the password + # fields change. attach to the confirm field and check changes to the + # password field in on_passphrase_changed + self._passphrase_match_check = self.add_check(self._confirm_entry, self._checkMatch) self._strength_check = self.add_check(self._passphrase_entry, self._checkStrength) self._ascii_check = self.add_check(self._passphrase_entry, self._checkASCII)
@@ -98,11 +100,10 @@ def refresh(self):
self._update_passphrase_strength()
- # Update the check states and force a status update + # Update the check states self._passphrase_match_check.update_check_status() self._strength_check.update_check_status() self._ascii_check.update_check_status() - self.set_status(None)
def run(self): self.refresh() @@ -152,7 +153,6 @@ def set_status(self, inputcheck):
# The save button should only be sensitive if the match check passes if self._passphrase_match_check.check_status == InputCheck.CHECK_OK and \ - self._confirm_match_check.check_status == InputCheck.CHECK_OK and \ (not self.policy.strict or self._strength_check.check_status == InputCheck.CHECK_OK): self._save_button.set_sensitive(True) else: @@ -180,21 +180,14 @@ def _checkMatch(self, inputcheck): else: result = InputCheck.CHECK_OK
- # If the check succeeded, reset the status of the other check object - # Disable the current check to prevent a cycle - if result == InputCheck.CHECK_OK: - inputcheck.enabled = False - if inputcheck == self._passphrase_match_check: - self._confirm_match_check.update_check_status() - else: - self._passphrase_match_check.update_check_status() - inputcheck.enabled = True - return result
def on_passphrase_changed(self, entry): self._update_passphrase_strength()
+ # Update the match check for changes in the main passphrase field + self._passphrase_match_check.update_check_status() + def on_save_clicked(self, button): self.passphrase = self._passphrase_entry.get_text()
diff --git a/pyanaconda/ui/gui/spokes/password.glade b/pyanaconda/ui/gui/spokes/password.glade index a146741..c0b38f6 100644 --- a/pyanaconda/ui/gui/spokes/password.glade +++ b/pyanaconda/ui/gui/spokes/password.glade @@ -80,7 +80,7 @@ <property name="can_focus">True</property> <property name="visibility">False</property> <property name="invisible_char">●</property> - <signal name="changed" handler="_updatePwQuality" swapped="no"/> + <signal name="changed" handler="on_password_changed" swapped="no"/> <child internal-child="accessible"> <object class="AtkObject" id="pw-atkobject"> <property name="AtkObject::accessible-name" translatable="yes">Password</property> diff --git a/pyanaconda/ui/gui/spokes/password.py b/pyanaconda/ui/gui/spokes/password.py index 1eeb426..b0b8265 100644 --- a/pyanaconda/ui/gui/spokes/password.py +++ b/pyanaconda/ui/gui/spokes/password.py @@ -70,14 +70,10 @@ def initialize(self): # - Is there any data in the confirm box? self.add_check(self.pw, self._checkPasswordEmpty)
- # The password confirmation needs to be checked whenever either of the password - # fields change. Separate checks are created for each field so that edits on either - # will trigger a new check and so that the last edited field will get focus when - # Done is clicked. The checks are saved here so that either check can trigger the - # other check in order to reset the status on both when either field is changed. - # The check_data field is used as a flag to prevent infinite recursion. + # the password confirmation needs to be checked whenever either of the password + # fields change. attach to the confirm field so that errors focus on confirm, + # and check changes to the password field in on_password_changed self._confirm_check = self.add_check(self.confirm, self._checkPasswordConfirm) - self._password_check = self.add_check(self.pw, self._checkPasswordConfirm)
# Keep a reference for these checks, since they have to be manually run for the # click Done twice check. @@ -193,19 +189,9 @@ def _checkPasswordConfirm(self, inputcheck): else: result = InputCheck.CHECK_OK
- # If the check succeeded, reset the status of the other check object - # Disable the current check to prevent a cycle - inputcheck.enabled = False - if result == InputCheck.CHECK_OK: - if inputcheck == self._confirm_check: - self._password_check.update_check_status() - else: - self._confirm_check.update_check_status() - inputcheck.enabled = True - return result
- def _updatePwQuality(self, editable=None, data=None): + def _updatePwQuality(self): """Update the password quality information.
This function is called by the ::changed signal handler on the @@ -235,6 +221,12 @@ def _updatePwQuality(self, editable=None, data=None): self.pw_bar.set_value(val) self.pw_label.set_text(text)
+ def on_password_changed(self, editable, data=None): + # Update the password/confirm match check on changes to the main password field + self._confirm_check.update_check_status() + + self._updatePwQuality() + def _checkPasswordStrength(self, inputcheck): """Update the error message based on password strength.
diff --git a/pyanaconda/ui/gui/spokes/user.py b/pyanaconda/ui/gui/spokes/user.py index 947e281..e837d88 100644 --- a/pyanaconda/ui/gui/spokes/user.py +++ b/pyanaconda/ui/gui/spokes/user.py @@ -308,14 +308,10 @@ def initialize(self): # - if a password is required, is there any data in the confirm box? self.add_check(self.pw, self._checkPasswordEmpty)
- # The password confirmation needs to be checked whenever either of the password - # fields change. Separate checks are created on each field so that edits on - # either will trigger a check and so that the last edited field will get the focus - # when Done is clicked. Whichever check is run needs to run the other check in - # order to reset the status. The check_data field is used as a flag to prevent - # infinite recursion. + # the password confirmation needs to be checked whenever either of the password + # fields change. attach to the confirm field so that errors focus on confirm, + # and check changes to the password field in password_changed self._confirm_check = self.add_check(self.confirm, self._checkPasswordConfirm) - self._password_check = self.add_check(self.pw, self._checkPasswordConfirm)
# Keep a reference to these checks, since they have to be manually run for the # click Done twice check. @@ -476,6 +472,9 @@ def password_changed(self, editable=None, data=None): """Update the password strength level bar""" self._updatePwQuality()
+ # Update the password/confirm match check on changes to the main password field + self._confirm_check.update_check_status() + def username_changed(self, editable=None, data=None): """Called by Gtk callback when the username or hostname entry changes. It disables the guess algorithm if the @@ -538,16 +537,6 @@ def _checkPasswordConfirm(self, inputcheck): else: result = InputCheck.CHECK_OK
- # If the check succeeded, reset the status of the other check object - # Disable the current check to prevent a cycle - inputcheck.enabled = False - if result == InputCheck.CHECK_OK: - if inputcheck == self._confirm_check: - self._password_check.update_check_status() - else: - self._confirm_check.update_check_status() - inputcheck.enabled = True - return result
def _checkPasswordStrength(self, inputcheck):
These all look good to me. Quite brave, but good. :)
Added label: ACK.
Closed.
Pushed
anaconda-patches@lists.fedorahosted.org