[PATCH] Add entries with completions to the comboboxes (DatetimeSpoke)

Vratislav Podzimek vpodzime at redhat.com
Tue Feb 5 15:25:53 UTC 2013


Some regions have so many timezones (cities), that the combobox doesn't fit
in the screen and is hard to search through. This patch modifies the city
and region comboboxes to have text entry, that can be used to choose timezone.

There are five ways of choosing timezone:
1) click on the map
2) popup comboboxes and choose region and city
3) type to the region and city comboboxes and choose from the completions
4) type a whole region or city name to the combobox and hit ENTER
5) type a whole region or city name to the combobox and click somewhere else
   or hit TAB

Signed-off-by: Vratislav Podzimek <vpodzime at redhat.com>
---
 pyanaconda/ui/gui/spokes/datetime_spoke.glade | 152 +++++++++++++-------------
 pyanaconda/ui/gui/spokes/datetime_spoke.py    |  87 +++++++++++++--
 2 files changed, 155 insertions(+), 84 deletions(-)

diff --git a/pyanaconda/ui/gui/spokes/datetime_spoke.glade b/pyanaconda/ui/gui/spokes/datetime_spoke.glade
index d2b9825..393f886 100644
--- a/pyanaconda/ui/gui/spokes/datetime_spoke.glade
+++ b/pyanaconda/ui/gui/spokes/datetime_spoke.glade
@@ -2,28 +2,36 @@
 <interface>
   <!-- interface-requires gtk+ 3.0 -->
   <!-- interface-requires AnacondaWidgets 1.0 -->
+  <object class="GtkImage" id="addImage">
+    <property name="visible">True</property>
+    <property name="can_focus">False</property>
+    <property name="icon_name">list-add-symbolic</property>
+  </object>
+  <object class="GtkImage" id="configImage">
+    <property name="visible">True</property>
+    <property name="can_focus">False</property>
+    <property name="icon_name">system-run-symbolic</property>
+  </object>
+  <object class="GtkTreeModelFilter" id="daysFilter">
+    <property name="child_model">days</property>
+  </object>
   <object class="GtkListStore" id="cities">
     <columns>
       <!-- column-name name -->
       <column type="gchararray"/>
     </columns>
   </object>
+  <object class="GtkEntryCompletion" id="cityCompletion">
+    <property name="model">citiesSort</property>
+    <property name="text_column">0</property>
+    <signal name="match-selected" handler="on_completion_match_selected" object="cityCombobox" swapped="no"/>
+  </object>
   <object class="GtkTreeModelFilter" id="citiesFilter">
     <property name="child_model">cities</property>
   </object>
   <object class="GtkTreeModelSort" id="citiesSort">
     <property name="model">citiesFilter</property>
   </object>
-  <object class="GtkImage" id="configImage">
-    <property name="visible">True</property>
-    <property name="can_focus">False</property>
-    <property name="icon_name">system-run-symbolic</property>
-  </object>
-  <object class="GtkImage" id="addImage">
-    <property name="visible">True</property>
-    <property name="can_focus">False</property>
-    <property name="icon_name">list-add-symbolic</property>
-  </object>
   <object class="AnacondaSpokeWindow" id="datetimeWindow">
     <property name="startup_id">filler</property>
     <property name="can_focus">False</property>
@@ -35,18 +43,9 @@
         <property name="can_focus">False</property>
         <property name="orientation">vertical</property>
         <property name="spacing">6</property>
-        <child internal-child="nav_box">
-          <object class="GtkEventBox" id="AnacondaSpokeWindow-nav_box1">
-            <child internal-child="nav_area">
-              <object class="GtkGrid" id="AnacondaSpokeWindow-nav_area1">
-                <property name="can_focus">False</property>
-                <property name="margin_left">6</property>
-                <property name="margin_right">6</property>
-                <property name="margin_top">6</property>
-                <property name="n_rows">2</property>
-                <property name="n_columns">2</property>
-              </object>
-            </child>
+        <child internal-child="nav_area">
+          <object class="GtkGrid" id="AnacondaSpokeWindow-nav_area1">
+            <property name="can_focus">False</property>
           </object>
           <packing>
             <property name="expand">False</property>
@@ -100,12 +99,19 @@
                                 <property name="visible">True</property>
                                 <property name="can_focus">False</property>
                                 <property name="model">regions</property>
-                                <signal name="changed" handler="on_region_changed" swapped="no"/>
-                                <child>
-                                  <object class="GtkCellRendererText" id="regionsComboRenderer"/>
-                                  <attributes>
-                                    <attribute name="text">0</attribute>
-                                  </attributes>
+                                <property name="has_entry">True</property>
+                                <property name="entry_text_column">0</property>
+                                <signal name="changed" handler="on_region_changed" object="regionsEntry" swapped="no"/>
+                                <child internal-child="entry">
+                                  <object class="GtkEntry" id="regionsEntry">
+                                    <property name="can_focus">True</property>
+                                    <property name="events">GDK_KEY_RELEASE_MASK | GDK_STRUCTURE_MASK</property>
+                                    <property name="width_chars">20</property>
+                                    <property name="completion">regionCompletion</property>
+                                    <signal name="key-release-event" handler="on_city_region_key_released" swapped="no"/>
+                                    <signal name="activate" handler="on_city_region_text_entry_activated" swapped="no"/>
+                                    <signal name="focus-out-event" handler="on_entry_left" swapped="no"/>
+                                  </object>
                                 </child>
                               </object>
                               <packing>
@@ -135,12 +141,19 @@
                                 <property name="visible">True</property>
                                 <property name="can_focus">False</property>
                                 <property name="model">citiesSort</property>
-                                <signal name="changed" handler="on_city_changed" swapped="no"/>
-                                <child>
-                                  <object class="GtkCellRendererText" id="citiesComboRenderer"/>
-                                  <attributes>
-                                    <attribute name="text">0</attribute>
-                                  </attributes>
+                                <property name="has_entry">True</property>
+                                <property name="entry_text_column">0</property>
+                                <signal name="changed" handler="on_city_changed" object="citiesEntry" swapped="no"/>
+                                <child internal-child="entry">
+                                  <object class="GtkEntry" id="citiesEntry">
+                                    <property name="can_focus">True</property>
+                                    <property name="events">GDK_KEY_RELEASE_MASK | GDK_STRUCTURE_MASK</property>
+                                    <property name="width_chars">20</property>
+                                    <property name="completion">cityCompletion</property>
+                                    <signal name="key-release-event" handler="on_city_region_key_released" swapped="no"/>
+                                    <signal name="activate" handler="on_city_region_text_entry_activated" swapped="no"/>
+                                    <signal name="focus-out-event" handler="on_entry_left" swapped="no"/>
+                                  </object>
                                 </child>
                               </object>
                               <packing>
@@ -182,7 +195,6 @@
                                         <property name="can_focus">True</property>
                                         <property name="halign">end</property>
                                         <property name="valign">start</property>
-                                        <property name="use_action_appearance">False</property>
                                         <signal name="notify::active" handler="on_ntp_switched" swapped="no"/>
                                       </object>
                                       <packing>
@@ -197,7 +209,6 @@
                                         <property name="visible">True</property>
                                         <property name="can_focus">True</property>
                                         <property name="receives_default">True</property>
-                                        <property name="use_action_appearance">False</property>
                                         <property name="image">configImage</property>
                                         <signal name="clicked" handler="on_ntp_config_clicked" swapped="no"/>
                                       </object>
@@ -309,7 +320,6 @@
                                     <property name="visible">True</property>
                                     <property name="can_focus">True</property>
                                     <property name="receives_default">True</property>
-                                    <property name="use_action_appearance">False</property>
                                     <property name="image">upImage</property>
                                     <signal name="clicked" handler="on_up_hours_clicked" swapped="no"/>
                                   </object>
@@ -325,7 +335,6 @@
                                     <property name="visible">True</property>
                                     <property name="can_focus">True</property>
                                     <property name="receives_default">True</property>
-                                    <property name="use_action_appearance">False</property>
                                     <property name="image">downImage</property>
                                     <signal name="clicked" handler="on_down_hours_clicked" swapped="no"/>
                                   </object>
@@ -341,7 +350,6 @@
                                     <property name="visible">True</property>
                                     <property name="can_focus">True</property>
                                     <property name="receives_default">True</property>
-                                    <property name="use_action_appearance">False</property>
                                     <property name="image">upImage1</property>
                                     <signal name="clicked" handler="on_up_ampm_clicked" swapped="no"/>
                                   </object>
@@ -357,7 +365,6 @@
                                     <property name="visible">True</property>
                                     <property name="can_focus">True</property>
                                     <property name="receives_default">True</property>
-                                    <property name="use_action_appearance">False</property>
                                     <property name="image">upImage2</property>
                                     <signal name="clicked" handler="on_up_minutes_clicked" swapped="no"/>
                                   </object>
@@ -373,7 +380,6 @@
                                     <property name="visible">True</property>
                                     <property name="can_focus">True</property>
                                     <property name="receives_default">True</property>
-                                    <property name="use_action_appearance">False</property>
                                     <property name="image">downImage1</property>
                                     <signal name="clicked" handler="on_down_ampm_clicked" swapped="no"/>
                                   </object>
@@ -389,7 +395,6 @@
                                     <property name="visible">True</property>
                                     <property name="can_focus">True</property>
                                     <property name="receives_default">True</property>
-                                    <property name="use_action_appearance">False</property>
                                     <property name="image">downImage2</property>
                                     <signal name="clicked" handler="on_down_minutes_clicked" swapped="no"/>
                                   </object>
@@ -623,8 +628,37 @@
       <column type="guint"/>
     </columns>
   </object>
-  <object class="GtkTreeModelFilter" id="daysFilter">
-    <property name="child_model">days</property>
+  <object class="GtkImage" id="downImage">
+    <property name="visible">True</property>
+    <property name="can_focus">False</property>
+    <property name="icon_name">go-down-symbolic</property>
+  </object>
+  <object class="GtkImage" id="downImage1">
+    <property name="visible">True</property>
+    <property name="can_focus">False</property>
+    <property name="icon_name">go-down-symbolic</property>
+  </object>
+  <object class="GtkImage" id="downImage2">
+    <property name="visible">True</property>
+    <property name="can_focus">False</property>
+    <property name="icon_name">go-down-symbolic</property>
+  </object>
+  <object class="GtkListStore" id="regions">
+    <columns>
+      <!-- column-name name -->
+      <column type="gchararray"/>
+    </columns>
+  </object>
+  <object class="GtkEntryCompletion" id="regionCompletion">
+    <property name="model">regions</property>
+    <property name="text_column">0</property>
+    <signal name="match-selected" handler="on_completion_match_selected" object="regionCombobox" swapped="no"/>
+  </object>
+  <object class="GtkListStore" id="months">
+    <columns>
+      <!-- column-name name -->
+      <column type="gchararray"/>
+    </columns>
   </object>
   <object class="GtkDialog" id="ntpConfigDialog">
     <property name="can_focus">False</property>
@@ -694,7 +728,6 @@
                                 <property name="visible">True</property>
                                 <property name="can_focus">True</property>
                                 <property name="receives_default">True</property>
-                                <property name="use_action_appearance">False</property>
                                 <property name="image">addImage</property>
                                 <signal name="clicked" handler="on_add_clicked" swapped="no"/>
                               </object>
@@ -815,7 +848,6 @@
                 <property name="visible">True</property>
                 <property name="can_focus">True</property>
                 <property name="receives_default">True</property>
-                <property name="use_action_appearance">False</property>
                 <property name="use_stock">True</property>
               </object>
               <packing>
@@ -830,7 +862,6 @@
                 <property name="visible">True</property>
                 <property name="can_focus">True</property>
                 <property name="receives_default">True</property>
-                <property name="use_action_appearance">False</property>
                 <property name="use_stock">True</property>
               </object>
               <packing>
@@ -854,33 +885,6 @@
       <action-widget response="1">okButton</action-widget>
     </action-widgets>
   </object>
-  <object class="GtkImage" id="downImage">
-    <property name="visible">True</property>
-    <property name="can_focus">False</property>
-    <property name="icon_name">go-down-symbolic</property>
-  </object>
-  <object class="GtkImage" id="downImage1">
-    <property name="visible">True</property>
-    <property name="can_focus">False</property>
-    <property name="icon_name">go-down-symbolic</property>
-  </object>
-  <object class="GtkImage" id="downImage2">
-    <property name="visible">True</property>
-    <property name="can_focus">False</property>
-    <property name="icon_name">go-down-symbolic</property>
-  </object>
-  <object class="GtkListStore" id="months">
-    <columns>
-      <!-- column-name name -->
-      <column type="gchararray"/>
-    </columns>
-  </object>
-  <object class="GtkListStore" id="regions">
-    <columns>
-      <!-- column-name name -->
-      <column type="gchararray"/>
-    </columns>
-  </object>
   <object class="GtkListStore" id="serversStore">
     <columns>
       <!-- column-name hostname -->
diff --git a/pyanaconda/ui/gui/spokes/datetime_spoke.py b/pyanaconda/ui/gui/spokes/datetime_spoke.py
index b9b59bb..5746ce7 100644
--- a/pyanaconda/ui/gui/spokes/datetime_spoke.py
+++ b/pyanaconda/ui/gui/spokes/datetime_spoke.py
@@ -27,7 +27,7 @@ import logging
 log = logging.getLogger("anaconda")
 
 # pylint: disable-msg=E0611
-from gi.repository import AnacondaWidgets, GLib, Gtk
+from gi.repository import AnacondaWidgets, GLib, Gtk, Gdk
 
 from pyanaconda.ui.gui import GUIObject
 from pyanaconda.ui.gui.spokes import NormalSpoke
@@ -285,6 +285,7 @@ class DatetimeSpoke(NormalSpoke):
                       "upImage", "upImage1", "upImage2", "downImage",
                       "downImage1", "downImage2", "downImage3", "configImage",
                       "citiesFilter", "daysFilter", "citiesSort", "regionsSort",
+                      "cityCompletion", "regionCompletion",
                       ]
 
     mainWidgetName = "datetimeWindow"
@@ -328,6 +329,10 @@ class DatetimeSpoke(NormalSpoke):
             for city in self._regions_zones[region]:
                 self.add_to_store(self._citiesStore, city)
 
+        # we need to know it the new value is the same as previous or not
+        self._old_region = None
+        self._old_city = None
+
         self._regionCombo = self.builder.get_object("regionCombobox")
         self._cityCombo = self.builder.get_object("cityCombobox")
         self._monthCombo = self.builder.get_object("monthCombobox")
@@ -428,8 +433,8 @@ class DatetimeSpoke(NormalSpoke):
 
     @property
     def mandatory(self):
-        return True        
-    
+        return True
+
     def refresh(self):
         #update the displayed time
         self._update_datetime_timer_id = GLib.timeout_add_seconds(1,
@@ -663,6 +668,11 @@ class DatetimeSpoke(NormalSpoke):
 
         return model[itr][0]
 
+    def _restore_old_city_region(self):
+        """Restore stored "old" (or last valid) values."""
+
+        self._set_combo_selection(self._regionCombo, self._old_region)
+        self._set_combo_selection(self._cityCombo, self._old_city)
 
     def on_up_hours_clicked(self, *args):
         self._stop_and_maybe_start_time_updating()
@@ -729,27 +739,49 @@ class DatetimeSpoke(NormalSpoke):
         else:
             self._amPmLabel.set_text("AM")
 
-    def on_region_changed(self, *args):
-        self._citiesFilter.refilter()
+    def on_region_changed(self, combo, *args):
+        """
+        @see: on_city_changed
+
+        """
 
-        # Attempt to set the city to the first one available in this newly
-        # selected region.
         region = self._get_active_region()
-        if not region:
+        if not region or region == self._old_region:
+            # region entry being edited or old_value chosen, no action needed
+            # @see: on_city_changed
             return
 
+        self._citiesFilter.refilter()
+
+        # Set the city to the first one available in this newly selected region.
         zone = self._regions_zones[region]
         firstCity = sorted(list(zone))[0]
 
         self._set_combo_selection(self._cityCombo, firstCity)
-        self._cityCombo.emit("changed")
+        self._old_region = region
+        self._old_city = firstCity
+
+    def on_city_changed(self, combo, *args):
+        """
+        ComboBox emits ::changed signal not only when something is selected, but
+        also when its entry's text is changed. We need to distinguish between
+        those two cases ('London' typed in the entry => no action until ENTER is
+        hit etc.; 'London' chosen in the expanded combobox => update timezone
+        map and do all necessary actions). Fortunately when entry is being
+        edited, self._get_active_city returns None.
+
+        """
 
-    def on_city_changed(self, *args):
         timezone = None
 
         region = self._get_active_region()
         city = self._get_active_city()
 
+        if not region or not city or (region == self._old_region and
+                                      city == self._old_city):
+            # entry being edited or no change, no actions needed
+            return
+
         if city and region:
             timezone = region + "/" + city
 
@@ -765,6 +797,41 @@ class DatetimeSpoke(NormalSpoke):
         elif timezone and (self._tzmap.get_timezone() != timezone):
             self._tzmap.set_timezone(timezone)
 
+        # update "old" values
+        self._old_city = city
+        print "Storing %s" % city
+
+    def on_entry_left(self, entry, *args):
+        # user clicked somewhere else or hit TAB => finished editing
+        entry.emit("activate")
+
+    def on_city_region_key_released(self, entry, event, *args):
+        if event.type == Gdk.EventType.KEY_RELEASE and \
+                event.keyval == Gdk.KEY_Escape:
+            # editing canceled
+            self._restore_old_city_region()
+
+    def on_completion_match_selected(self, combo, model, itr):
+        item = None
+        if model and itr:
+            item = model[itr][0]
+        if item:
+            self._set_combo_selection(combo, item)
+
+    def on_city_region_text_entry_activated(self, entry):
+        combo = entry.get_parent()
+        model = combo.get_model()
+        itr = model.get_iter_first()
+        entry_text = entry.get_text().lower()
+
+        for row in model:
+            if entry_text == row[0].lower():
+                self._set_combo_selection(combo, row[0])
+                return
+
+        # non-matching value entered, reset to old values
+        self._restore_old_city_region()
+
     def on_month_changed(self, *args):
         self._stop_and_maybe_start_time_updating(interval=5)
         self._daysFilter.refilter()
-- 
1.7.11.7



More information about the anaconda-patches mailing list