First attempt at splitting out main GUI code into WimpWorks
authorAndrew Flegg <andrew@bleb.org>
Wed, 28 Oct 2009 16:27:10 +0000 (16:27 +0000)
committerAndrew Flegg <andrew@bleb.org>
Wed, 28 Oct 2009 16:27:10 +0000 (16:27 +0000)
package/src/gui.py
package/src/wimpworks.py [new file with mode: 0644]

index 4c0e365..d1c92cc 100755 (executable)
@@ -2,18 +2,18 @@
 
 import gtk, gobject
 import gnome.gconf
-import hildon, osso
 import traceback
 import time
 import thread
 import os.path
 import contactview
-import urllib2 
+import urllib2
+from wimpworks import WimpWorks
 from hermes import Hermes
 
 gobject.threads_init()
 
-class HermesGUI:
+class HermesGUI(WimpWorks):
   """Provides the GUI for Hermes, allowing the syncing of Facebook and
      Twitter friends' information with the Evolution contacts' database.
        
@@ -23,14 +23,73 @@ class HermesGUI:
 
   # -----------------------------------------------------------------------
   def __init__(self):
-    """Constructor. Initialises the gconf connection."""
-    self.gc = gnome.gconf.client_get_default()
+    WimpWorks.__init__(self, 'Hermes', version = '0.1.1', 'org.maemo.hermes')
+    self.set_background('background.png')
+    
+    layout = wimporks.HildonMainScreenLayout(offset = 0.8, container = self)
+    layout.add_button('Retrieve', "Get contacts' missing info")
+    layout.add_button('Refresh', "Update contacts' info")
 
+    self.add_menu_action("Accounts")
+    self.menu.show_all()
 
+  
   # -----------------------------------------------------------------------
-  def doSync(self, widget, force, main = True):
+  def do_retrieve(self, widget):
+    self.sync(widget, False)
+    
+    
+  # -----------------------------------------------------------------------
+  def do_refresh(self, widget):
+    self.sync(widget, True)
+
+
+  # -----------------------------------------------------------------------
+  def do_accounts(self, widget = None):
+    dialog = gtk.Dialog('Accounts', self.window)
+    dialog.add_button('Save', gtk.RESPONSE_OK)
+
+    #pa = hildon.PannableArea()
+    #dialog.vbox.add(pa)
+    content = dialog.vbox 
+    #content = gtk.VBox()
+    #pa.add(content)
+    #pa.set_size_request(600, 380)
+
+    use_facebook = self.new_checkbox('Use Facebook', content)
+    use_facebook.set_active(self.get_use_facebook())
+
+    indent = self.new_indent(content)
+    self.link_control(use_facebook, gtk.Label('Note: authentication via web page'), indent)
+    
+    fb_empty = self.link_control(use_facebook, self.new_checkbox('Create birthday-only contacts'), indent)
+    fb_empty.set_active(self.get_create_empty())
+
+    use_twitter = self.new_checkbox('Use Twitter', content)
+    use_twitter.set_active(self.get_use_twitter())
+
+    indent = self.new_indent(content)
+    tw_user = self.link_control(use_twitter, self.new_input('Twitter username'), indent)
+    tw_user.set_text(self.get_twitter_credentials()[0])
+
+    tw_pass = self.link_control(indent, use_twitter, self.new_input('Twitter password', password = True), indent)
+    tw_pass.set_text(self.get_twitter_credentials()[1])
+
+    dialog.show_all()
+    result = dialog.run()
+    dialog.hide()
+    if result == gtk.RESPONSE_OK:
+      self.set_use_facebook(use_facebook.get_active())
+      self.set_create_empty(fb_empty.get_active())
+      self.set_use_twitter(use_twitter.get_active(), tw_user.get_text(), tw_pass.get_text())
+
+    return result
+   
+
+  # -----------------------------------------------------------------------
+  def sync(self, widget, force, main = True):
     if main and not self.get_use_facebook() and not self.get_use_twitter():
-      saved = self.open_prefs()
+      saved = self.do_accounts()
       if saved == gtk.RESPONSE_DELETE_EVENT:
         return
 
@@ -110,93 +169,7 @@ class HermesGUI:
   def map_contact(self, widget, contact):
     print widget, contact
 
-
-  # -----------------------------------------------------------------------
-  def open_prefs(self, widget = None):
-    dialog = gtk.Dialog('Accounts', self.window)
-    dialog.add_button('Save', gtk.RESPONSE_OK)
-
-    #pa = hildon.PannableArea()
-    #dialog.vbox.add(pa)
-    content = dialog.vbox 
-    #content = gtk.VBox()
-    #pa.add(content)
-    #pa.set_size_request(600, 380)
-
-    use_facebook = self.new_checkbox('Use Facebook', content)
-    use_facebook.set_active(self.get_use_facebook())
-
-    indent = self.new_indent(content)
-    fb_msg = self.add_linked(indent, use_facebook, gtk.Label('Note: authentication via web page'))
-    fb_msg.set_property('justify', gtk.JUSTIFY_LEFT)
     
-    fb_empty = self.add_linked(indent, use_facebook, self.new_checkbox('Create birthday-only contacts'))
-    fb_empty.set_active(self.get_create_empty())
-
-    use_twitter = self.new_checkbox('Use Twitter', content)
-    use_twitter.set_active(self.get_use_twitter())
-
-    indent = self.new_indent(content)
-    tw_user = self.add_linked(indent, use_twitter, self.new_input('Twitter username'))
-    tw_user.set_text(self.get_twitter_credentials()[0])
-
-    tw_pass = self.add_linked(indent, use_twitter, self.new_input('Twitter password'))
-    tw_pass.set_property('hildon-input-mode', gtk.HILDON_GTK_INPUT_MODE_FULL |
-                                              gtk.HILDON_GTK_INPUT_MODE_INVISIBLE)
-    tw_pass.set_text(self.get_twitter_credentials()[1])
-
-    dialog.show_all()
-    result = dialog.run()
-    dialog.hide()
-    if result == gtk.RESPONSE_OK:
-      self.set_use_facebook(use_facebook.get_active())
-      self.set_create_empty(fb_empty.get_active())
-      self.set_use_twitter(use_twitter.get_active(), tw_user.get_text(), tw_pass.get_text())
-
-    return result
-  # -----------------------------------------------------------------------
-  def new_checkbox(self, label, box = None):
-    checkbox = hildon.CheckButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
-    checkbox.set_label(label)
-    if box:
-      box.add(checkbox)
-    return checkbox
-
-
-  # -----------------------------------------------------------------------
-  def new_indent(self, box):
-    outer = gtk.HBox()
-    indent = gtk.VBox()
-    outer.pack_start(indent, padding=48)
-    box.add(outer)
-    return indent
-
-  # -----------------------------------------------------------------------
-  def new_input(self, text, box = None):
-    input = hildon.Entry(gtk.HILDON_SIZE_FINGER_HEIGHT)
-    input.set_property('hildon-input-mode', gtk.HILDON_GTK_INPUT_MODE_FULL)
-    input.set_placeholder(text)
-    input.set_property('is-focus', False)
-    if box:
-      box.add(input)
-    return input
-
-
-  # -----------------------------------------------------------------------
-  def add_linked(self, box, ctrl, to_add):
-    box.add(to_add)
-    self.sync_edit(ctrl, to_add)
-    ctrl.connect('toggled', self.sync_edit, to_add)
-    return to_add
-  
-    
-  # -----------------------------------------------------------------------
-  def sync_edit(self, use_twitter, edit):
-    edit.set_property('sensitive', use_twitter.get_active())
-
-
   # -----------------------------------------------------------------------
   def need_auth(self, main = False):
     if main:
@@ -260,66 +233,9 @@ class HermesGUI:
 
     hildon.hildon_banner_show_information(self.window, '', e)
     if prefs:
-      self.open_prefs()
-
-
-  # -----------------------------------------------------------------------
-  def take_screenshot(self, event = None, data = None):
-    self.window.disconnect(self.expose_hid)
-    if not os.path.isfile("/home/user/.cache/launch/org.maemo.hermes.pvr"):
-      gobject.timeout_add(80, hildon.hildon_gtk_window_take_screenshot, self.window, True)
-
-
-  # -----------------------------------------------------------------------
-  def main(self):
-    # -- Window and app...
-    #
-    self.app = hildon.Program()
-    self.window = hildon.Window()
-    gtk.set_application_name('Hermes')
-    self.osso_context = osso.Context('org.maemo.hermes', '0.0.5', False)
-    self.app.add_window(self.window)
-
-    self.window.connect("delete-event", gtk.main_quit)
-    self.expose_hid = self.window.connect('expose-event', self.take_screenshot)
-
-    # -- Main window buttons...
-    #
-    self.background, mask = gtk.gdk.pixbuf_new_from_file('/opt/hermes/share/background.png').render_pixmap_and_mask()
-    self.window.realize()
-    self.window.window.set_back_pixmap(self.background, False)
-    alignment = gtk.Alignment(xalign=0.5, yalign=0.8, xscale=0.8)
-    box = gtk.HButtonBox()
-    alignment.add(box)
-    self.window.add(alignment)
-    
-    box.set_property('layout-style', gtk.BUTTONBOX_SPREAD)
-    button = hildon.Button(gtk.HILDON_SIZE_THUMB_HEIGHT, hildon.BUTTON_ARRANGEMENT_VERTICAL,
-                           title = 'Retrieve', value = "Get contacts' missing info")
-    button.set_property('width-request', 250)
-    button.connect('clicked', self.doSync, False)
-    box.add(button)
-
-    button = hildon.Button(gtk.HILDON_SIZE_THUMB_HEIGHT, hildon.BUTTON_ARRANGEMENT_VERTICAL,
-                           title = 'Update', value = "Update contacts' info")
-    button.set_property('width-request', 250)
-    button.connect('clicked', self.doSync, True)
-    box.add(button)
-
-    # -- Application menu...
-    #
-    menu = hildon.AppMenu()
-    button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
-    button.set_label("Accounts")
-    button.connect("clicked", self.open_prefs)
-    menu.append(button)
-    menu.show_all()
-    self.window.set_app_menu(menu)
-  
-    self.window.show_all()
-    gtk.main()
-
+      self.do_accounts()
 
+      
   def get_use_facebook(self):
     return self.gc.get_bool("/apps/maemo/hermes/use_facebook")
 
@@ -352,5 +268,6 @@ class HermesGUI:
 
 # -------------------------------------------------------------------------
 if __name__ == '__main__':
-  HermesGUI().main()
+  gui = HermesGUI()
+  gui.run()
 
diff --git a/package/src/wimpworks.py b/package/src/wimpworks.py
new file mode 100644 (file)
index 0000000..6986ed7
--- /dev/null
@@ -0,0 +1,272 @@
+#
+# WimpWorks (for Python)                (c) Andrew Flegg 2009.
+# ~~~~~~~~~~~~~~~~~~~~~~                Released under the Artistic Licence.
+#                                       http://www.bleb.org/
+
+import gtk, gobject
+import gnome.gconf
+import hildon, osso
+import traceback
+import time
+import re
+import thread
+import os.path
+
+# -- Work out environment...
+#
+try:
+  import hildon
+  _have_hildon = True
+except ImportError:
+  _have_hildon = False
+  
+try:
+  import osso
+  _have_osso = True
+except ImportError:
+  _have_osso = False
+
+gobject.threads_init()
+
+# -- Main class...
+#
+class Wimpworks:
+  '''A framework for creating easy-to-use graphical user interfaces using
+     GTK+, Python, DBus and more.
+     
+     This is the base class. It should be constructed with a DBus name
+     and a version.
+       
+     Copyright (c) Andrew Flegg <andrew@bleb.org> 2009.
+     Released under the Artistic Licence.'''
+
+
+  # -----------------------------------------------------------------------
+  def __init__(self, application, version = '1.0.0', dbus_name = None):
+    '''Constructor. Initialises the gconf connection, DBus, OSSO and more.
+    
+       @param application User-facing name of the application.
+       @param version Version string of the application.
+       @param dbus_name Name to register with DBus. If unspecified, no
+              DBus registration will be performed.'''
+    
+    self.gconf = gnome.gconf.client_get_default()
+    self.name = application
+    self.dbus_name = dbus_name
+    self.menu = None
+    
+    if _have_hildon:
+      self.app = hildon.Program()
+      self.main_window = hildon.Window()
+    else:
+      self.app = None
+      self.main_window = gtk.Window()
+      
+    gtk.set_application_name(application)
+    self.window.connect("delete-event", gtk.main_quit)
+    
+    if _have_osso and dbus_name:
+      self.osso_context = osso.Context(dbus_name, version, False)
+      
+    if self.app:
+      self.app.add_window(self.main_window)
+      
+    if _have_hildon:
+      self._expose_hid = self.window.connect('expose-event', self._take_screenshot)
+
+      
+  # -----------------------------------------------------------------------
+  def set_background(self, file, window = None):
+    '''Set the background of the given (or main) window to that contained in
+       'file'.
+       
+       @param file File name to set. If not an absolute path, typical application
+                   directories will be checked.
+       @param window Window to set background of. If unset, will default to the
+                     main application window.'''
+    
+    file = "/opt/%s/share/%s" % (self.name, file) # TODO Handle other forms of path
+    if not window:
+      window = self.main_window
+      
+    self._background, mask = gtk.gdk.pixbuf_new_from_file(file).render_pixmap_and_mask()
+    window.realize()
+    window.window.set_back_pixmap(self._background, False)
+
+    
+  # -----------------------------------------------------------------------
+  def add_menu_action(self, title, window = None):
+    '''Add a menu action to the given (or main) window. Once add_menu_action()
+       has been called with all the properties, 'self.menu.show_all()' should be
+       called.'''
+
+    if not window:
+      window = self.main_Window
+      
+    if not self.menu:
+      if _have_hildon:
+        self.menu = hildon.AppMenu()
+        window.set_app_menu(self.menu)
+      else:
+        raise Exception("Menu needs to be created, and no Hildon present")
+        
+    button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
+    button.set_label(title)
+    button.connect("clicked", self.callback, title)
+    self.menu.append(button)
+
+
+  # -----------------------------------------------------------------------
+  def run(self):
+    '''Once the application has been initialised, this will show the main window
+       and run the mainloop.'''
+       
+    self.window.show_all()
+    gtk.main()
+
+
+  # -----------------------------------------------------------------------
+  def _take_screenshot(self, event = None, data = None):
+    '''Used to provide a quick-loading screen.
+    
+       @see http://maemo.org/api_refs/5.0/5.0-final/hildon/hildon-Additions-to-GTK+.html#hildon-gtk-window-take-screenshot'''
+    
+    self.window.disconnect(self._expose_hid)
+    if not os.path.isfile("/home/user/.cache/launch/%s.pvr" % (self.dbus_name)):
+      gobject.timeout_add(80, hildon.hildon_gtk_window_take_screenshot, self.main_window, True)
+
+    
+  # -----------------------------------------------------------------------
+  def callback(self, event, method):
+    '''Call a method on this object, using the given string to derive
+       the name. If no method is found, no action is taken.
+       
+       @param event Event which triggered the callback.
+       @param method String which will be lowercased to form a method
+              called 'do_method'.'''
+
+    method = re.sub('[^a-z0-9_]', '', method.lower())
+    self.attr("do_%s" % (method))(event.window)
+
+    
+  # -----------------------------------------------------------------------
+  def new_checkbox(self, label, box = None):
+    '''Create a new checkbox, adding it to the given container.
+    
+       @param label Label for the checkbox.
+       @param box Optional container to add the created checkbox to.
+       @return The newly created checkbox.'''
+       
+    checkbox = hildon.CheckButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
+    checkbox.set_label(label)
+    if box:
+      box.add(checkbox)
+    return checkbox
+
+
+  # -----------------------------------------------------------------------
+  def new_indent(self, box):
+    '''Create an indent which can be used to show items related to each other.
+    
+       @param box Container to add the indent to.'''
+       
+    outer = gtk.HBox()
+    indent = gtk.VBox()
+    outer.pack_start(indent, padding=48)
+    box.add(outer)
+    return indent
+
+  # -----------------------------------------------------------------------
+  def new_input(self, label, box = None, password = False):
+    '''Create a new input with the given label, optionally adding it to a
+       container.
+       
+       @param label Text describing the purpose of the input field.
+       @param box Optional container to add the input to.
+       @param password Boolean indicating if the input is used for passwords.
+       @return The newly created input.'''
+       
+    input = hildon.Entry(gtk.HILDON_SIZE_FINGER_HEIGHT)
+    input.set_placeholder(label)
+    input.set_property('is-focus', False)
+    
+    if password:
+      input.set_property('hildon-input-mode', gtk.HILDON_GTK_INPUT_MODE_FULL | gtk.HILDON_GTK_INPUT_MODE_INVISIBLE)
+    else:
+      input.set_property('hildon-input-mode', gtk.HILDON_GTK_INPUT_MODE_FULL)
+      
+    if box:
+      box.add(input)
+    return input
+
+
+  # -----------------------------------------------------------------------
+  def link_control(self, checkbox, ctrl, box = None):
+    '''Link a checkbox to a control, such that the editability of the
+       control is determined by the checkbox state.
+       
+       @param checkbox Checkbox which will control the state.
+       @param ctrl Control to add.
+       @param box Optional container to add 'ctrl' to.
+       @return The added control.'''
+       
+    if box:
+      box.add(ctrl)
+      
+    self._sync_edit(checkbox, ctrl)
+    checkbox.connect('toggled', self._sync_edit, ctrl)
+    return ctrl
+  
+    
+  # -----------------------------------------------------------------------
+  def _sync_edit(self, checkbox, edit):
+    edit.set_property('sensitive', checkbox.get_active())
+    
+
+      
+# -----------------------------------------------------------------------
+class HildonMainScreenLayout():
+    '''Provides a mechanism for creating a traditional multi-button button
+       selection, as made popular by Maemo 5's Media Player, Clock, Application
+       Manager and HIG.
+       
+       This does *not* require Hildon, however.
+    '''
+
+    # ---------------------------------------------------------------------
+    def __init__(self, container, offset = 0.5):
+      '''Create a new layout.
+      
+         @param container Container to add layout to. If unspecified,
+                the application's main window will be used.
+         @param offset The vertical offset for the buttons. If unspecified,
+                they will be centred. Ranges from 0.0 (top) to 1.0 (bottom).'''
+                
+      print self
+      self._container = container
+      alignment = gtk.Alignment(xalign=0.5, yalign=0.8, xscale=0.8)
+      self._box = gtk.HButtonBox()
+      alignment.add(self._box)
+      container.add(alignment)
+      self._box.set_property('layout-style', gtk.BUTTONBOX_SPREAD)
+
+      
+    # ---------------------------------------------------------------------
+    def add_button(self, title, subtitle = ''):
+      '''Add a button to the layout with the specified title. Upon clicking
+         the button, a method of the name 'do_title' will be invoked on the
+         main class.
+         
+         @param title Value of the button, and used to derive the callback method.
+         @param subtitle An optional subtitle containing more information.'''
+
+    if _have_hildon:         
+      button = hildon.Button(gtk.HILDON_SIZE_THUMB_HEIGHT, hildon.BUTTON_ARRANGEMENT_VERTICAL,
+                             title = title, value = subtitle)
+    else:
+      button = gtk.Button(title)
+
+    button.set_property('width-request', 250)
+    button.connect('clicked', self.wimpworks.callback, title)
+    self._box.add(button)