Updating from skeleton
authorEd Page <eopage@byu.net>
Thu, 7 Jul 2011 01:55:09 +0000 (20:55 -0500)
committerEd Page <eopage@byu.net>
Thu, 7 Jul 2011 01:55:09 +0000 (20:55 -0500)
16 files changed:
src/gonvert.py
src/gonvert_qt.py
src/util/algorithms.py
src/util/concurrent.py
src/util/go_utils.py
src/util/gtk_utils.py [new file with mode: 0644]
src/util/hildonize.py [new file with mode: 0644]
src/util/io.py
src/util/misc.py
src/util/qore_utils.py
src/util/qt_compat.py [new file with mode: 0644]
src/util/qtpie.py
src/util/qtpieboard.py
src/util/qui_utils.py
src/util/qwrappers.py
support/py2deb.py

index 1ee52af..10c789e 100755 (executable)
@@ -1,10 +1,8 @@
 #!/usr/bin/python
 
 import sys
-import logging
 
 
-_moduleLogger = logging.getLogger(__name__)
 sys.path.append("/opt/gonvert/lib")
 
 
index 481d9ea..58ddf58 100755 (executable)
@@ -12,8 +12,9 @@ import simplejson
 import logging
 import logging.handlers
 
-from PyQt4 import QtGui
-from PyQt4 import QtCore
+import util.qt_compat as qt_compat
+QtCore = qt_compat.QtCore
+QtGui = qt_compat.import_module("QtGui")
 
 import constants
 from util import qui_utils
index 7052b98..e94fb61 100644 (file)
@@ -8,6 +8,8 @@ import itertools
 import functools
 import datetime
 import types
+import array
+import random
 
 
 def ordered_itr(collection):
@@ -41,6 +43,17 @@ def itercat(*iterators):
                        yield x
 
 
+def product(*args, **kwds):
+       # product('ABCD', 'xy') --> Ax Ay Bx By Cx Cy Dx Dy
+       # product(range(2), repeat=3) --> 000 001 010 011 100 101 110 111
+       pools = map(tuple, args) * kwds.get('repeat', 1)
+       result = [[]]
+       for pool in pools:
+               result = [x+[y] for x in result for y in pool]
+       for prod in result:
+               yield tuple(prod)
+
+
 def iterwhile(func, iterator):
        """
        Iterate for as long as func(value) returns true.
@@ -579,6 +592,73 @@ def itr_available(queue, initiallyBlock = False):
                yield queue.get_nowait()
 
 
+class BloomFilter(object):
+       """
+       http://en.wikipedia.org/wiki/Bloom_filter
+       Sources:
+       http://code.activestate.com/recipes/577684-bloom-filter/
+       http://code.activestate.com/recipes/577686-bloom-filter/
+
+       >>> from random import sample
+       >>> from string import ascii_letters
+       >>> states = '''Alabama Alaska Arizona Arkansas California Colorado Connecticut
+       ... Delaware Florida Georgia Hawaii Idaho Illinois Indiana Iowa Kansas
+       ... Kentucky Louisiana Maine Maryland Massachusetts Michigan Minnesota
+       ... Mississippi Missouri Montana Nebraska Nevada NewHampshire NewJersey
+       ... NewMexico NewYork NorthCarolina NorthDakota Ohio Oklahoma Oregon
+       ... Pennsylvania RhodeIsland SouthCarolina SouthDakota Tennessee Texas Utah
+       ... Vermont Virginia Washington WestVirginia Wisconsin Wyoming'''.split()
+       >>> bf = BloomFilter(num_bits=1000, num_probes=14)
+       >>> for state in states:
+       ...     bf.add(state)
+       >>> numStatesFound = sum(state in bf for state in states)
+       >>> numStatesFound, len(states)
+       (50, 50)
+       >>> trials = 100
+       >>> numGarbageFound = sum(''.join(sample(ascii_letters, 5)) in bf for i in range(trials))
+       >>> numGarbageFound, trials
+       (0, 100)
+       """
+
+       def __init__(self, num_bits, num_probes):
+               num_words = (num_bits + 31) // 32
+               self._arr = array.array('B', [0]) * num_words
+               self._num_probes = num_probes
+
+       def add(self, key):
+               for i, mask in self._get_probes(key):
+                       self._arr[i] |= mask
+
+       def union(self, bfilter):
+               if self._match_template(bfilter):
+                       for i, b in enumerate(bfilter._arr):
+                               self._arr[i] |= b
+               else:
+                       # Union b/w two unrelated bloom filter raises this
+                       raise ValueError("Mismatched bloom filters")
+
+       def intersection(self, bfilter):
+               if self._match_template(bfilter):
+                       for i, b in enumerate(bfilter._arr):
+                               self._arr[i] &= b
+               else:
+                       # Intersection b/w two unrelated bloom filter raises this
+                       raise ValueError("Mismatched bloom filters")
+
+       def __contains__(self, key):
+               return all(self._arr[i] & mask for i, mask in self._get_probes(key))
+
+       def _match_template(self, bfilter):
+               return self.num_bits == bfilter.num_bits and self.num_probes == bfilter.num_probes
+
+       def _get_probes(self, key):
+               hasher = random.Random(key).randrange
+               for _ in range(self._num_probes):
+                       array_index = hasher(len(self._arr))
+                       bit_index = hasher(32)
+                       yield array_index, 1 << bit_index
+
+
 if __name__ == "__main__":
        import doctest
        print doctest.testmod()
index a6499fe..f5f6e1d 100644 (file)
@@ -15,12 +15,33 @@ import misc
 _moduleLogger = logging.getLogger(__name__)
 
 
-class AsyncLinearExecution(object):
+class AsyncTaskQueue(object):
+
+       def __init__(self, taskPool):
+               self._asyncs = []
+               self._taskPool = taskPool
+
+       def add_async(self, func):
+               self.flush()
+               a = AsyncGeneratorTask(self._taskPool, func)
+               self._asyncs.append(a)
+               return a
+
+       def flush(self):
+               self._asyncs = [a for a in self._asyncs if not a.isDone]
+
+
+class AsyncGeneratorTask(object):
 
        def __init__(self, pool, func):
                self._pool = pool
                self._func = func
                self._run = None
+               self._isDone = False
+
+       @property
+       def isDone(self):
+               return self._isDone
 
        def start(self, *args, **kwds):
                assert self._run is None, "Task already started"
@@ -40,7 +61,7 @@ class AsyncLinearExecution(object):
                try:
                        trampoline, args, kwds = self._run.send(result)
                except StopIteration, e:
-                       pass
+                       self._isDone = True
                else:
                        self._pool.add_task(
                                trampoline,
@@ -56,7 +77,7 @@ class AsyncLinearExecution(object):
                try:
                        trampoline, args, kwds = self._run.throw(error)
                except StopIteration, e:
-                       pass
+                       self._isDone = True
                else:
                        self._pool.add_task(
                                trampoline,
index eaa2fe1..61e731d 100644 (file)
@@ -138,7 +138,7 @@ class Timeout(object):
 _QUEUE_EMPTY = object()
 
 
-class AsyncPool(object):
+class FutureThread(object):
 
        def __init__(self):
                self.__workQueue = Queue.Queue()
diff --git a/src/util/gtk_utils.py b/src/util/gtk_utils.py
new file mode 100644 (file)
index 0000000..342feae
--- /dev/null
@@ -0,0 +1,34 @@
+#!/usr/bin/env python
+
+from __future__ import with_statement
+from __future__ import division
+
+import contextlib
+import logging
+
+import gtk
+
+
+_moduleLogger = logging.getLogger(__name__)
+
+
+@contextlib.contextmanager
+def gtk_lock():
+       gtk.gdk.threads_enter()
+       try:
+               yield
+       finally:
+               gtk.gdk.threads_leave()
+
+
+def find_parent_window(widget):
+       while True:
+               parent = widget.get_parent()
+               if isinstance(parent, gtk.Window):
+                       return parent
+               widget = parent
+
+
+if __name__ == "__main__":
+       pass
+
diff --git a/src/util/hildonize.py b/src/util/hildonize.py
new file mode 100644 (file)
index 0000000..339eb2a
--- /dev/null
@@ -0,0 +1,766 @@
+#!/usr/bin/env python
+
+"""
+Open Issues
+       @bug not all of a message is shown
+       @bug Buttons are too small
+"""
+
+
+import gobject
+import gtk
+import dbus
+
+
+class _NullHildonModule(object):
+       pass
+
+
+try:
+       import hildon as _hildon
+       hildon  = _hildon # Dumb but gets around pyflakiness
+except (ImportError, OSError):
+       hildon = _NullHildonModule
+
+
+IS_HILDON_SUPPORTED = hildon is not _NullHildonModule
+
+
+class _NullHildonProgram(object):
+
+       def add_window(self, window):
+               pass
+
+
+def _hildon_get_app_class():
+       return hildon.Program
+
+
+def _null_get_app_class():
+       return _NullHildonProgram
+
+
+try:
+       hildon.Program
+       get_app_class = _hildon_get_app_class
+except AttributeError:
+       get_app_class = _null_get_app_class
+
+
+def _hildon_set_application_name(name):
+       gtk.set_application_name(name)
+
+
+def _null_set_application_name(name):
+       pass
+
+
+try:
+       gtk.set_application_name
+       set_application_name = _hildon_set_application_name
+except AttributeError:
+       set_application_name = _null_set_application_name
+
+
+def _fremantle_hildonize_window(app, window):
+       oldWindow = window
+       newWindow = hildon.StackableWindow()
+       if oldWindow.get_child() is not None:
+               oldWindow.get_child().reparent(newWindow)
+       app.add_window(newWindow)
+       return newWindow
+
+
+def _hildon_hildonize_window(app, window):
+       oldWindow = window
+       newWindow = hildon.Window()
+       if oldWindow.get_child() is not None:
+               oldWindow.get_child().reparent(newWindow)
+       app.add_window(newWindow)
+       return newWindow
+
+
+def _null_hildonize_window(app, window):
+       return window
+
+
+try:
+       hildon.StackableWindow
+       hildonize_window = _fremantle_hildonize_window
+except AttributeError:
+       try:
+               hildon.Window
+               hildonize_window = _hildon_hildonize_window
+       except AttributeError:
+               hildonize_window = _null_hildonize_window
+
+
+def _fremantle_hildonize_menu(window, gtkMenu):
+       appMenu = hildon.AppMenu()
+       window.set_app_menu(appMenu)
+       gtkMenu.get_parent().remove(gtkMenu)
+       return appMenu
+
+
+def _hildon_hildonize_menu(window, gtkMenu):
+       hildonMenu = gtk.Menu()
+       for child in gtkMenu.get_children():
+               child.reparent(hildonMenu)
+       window.set_menu(hildonMenu)
+       gtkMenu.destroy()
+       return hildonMenu
+
+
+def _null_hildonize_menu(window, gtkMenu):
+       return gtkMenu
+
+
+try:
+       hildon.AppMenu
+       GTK_MENU_USED = False
+       IS_FREMANTLE_SUPPORTED = True
+       hildonize_menu = _fremantle_hildonize_menu
+except AttributeError:
+       GTK_MENU_USED = True
+       IS_FREMANTLE_SUPPORTED = False
+       if IS_HILDON_SUPPORTED:
+               hildonize_menu = _hildon_hildonize_menu
+       else:
+               hildonize_menu = _null_hildonize_menu
+
+
+def _hildon_set_button_auto_selectable(button):
+       button.set_theme_size(hildon.HILDON_SIZE_AUTO_HEIGHT)
+
+
+def _null_set_button_auto_selectable(button):
+       pass
+
+
+try:
+       hildon.HILDON_SIZE_AUTO_HEIGHT
+       gtk.Button.set_theme_size
+       set_button_auto_selectable = _hildon_set_button_auto_selectable
+except AttributeError:
+       set_button_auto_selectable = _null_set_button_auto_selectable
+
+
+def _hildon_set_button_finger_selectable(button):
+       button.set_theme_size(hildon.HILDON_SIZE_FINGER_HEIGHT)
+
+
+def _null_set_button_finger_selectable(button):
+       pass
+
+
+try:
+       hildon.HILDON_SIZE_FINGER_HEIGHT
+       gtk.Button.set_theme_size
+       set_button_finger_selectable = _hildon_set_button_finger_selectable
+except AttributeError:
+       set_button_finger_selectable = _null_set_button_finger_selectable
+
+
+def _hildon_set_button_thumb_selectable(button):
+       button.set_theme_size(hildon.HILDON_SIZE_THUMB_HEIGHT)
+
+
+def _null_set_button_thumb_selectable(button):
+       pass
+
+
+try:
+       hildon.HILDON_SIZE_THUMB_HEIGHT
+       gtk.Button.set_theme_size
+       set_button_thumb_selectable = _hildon_set_button_thumb_selectable
+except AttributeError:
+       set_button_thumb_selectable = _null_set_button_thumb_selectable
+
+
+def _hildon_set_cell_thumb_selectable(renderer):
+       renderer.set_property("scale", 1.5)
+
+
+def _null_set_cell_thumb_selectable(renderer):
+       pass
+
+
+if IS_HILDON_SUPPORTED:
+       set_cell_thumb_selectable = _hildon_set_cell_thumb_selectable
+else:
+       set_cell_thumb_selectable = _null_set_cell_thumb_selectable
+
+
+def _hildon_set_pix_cell_thumb_selectable(renderer):
+       renderer.set_property("stock-size", 48)
+
+
+def _null_set_pix_cell_thumb_selectable(renderer):
+       pass
+
+
+if IS_HILDON_SUPPORTED:
+       set_pix_cell_thumb_selectable = _hildon_set_pix_cell_thumb_selectable
+else:
+       set_pix_cell_thumb_selectable = _null_set_pix_cell_thumb_selectable
+
+
+def _fremantle_show_information_banner(parent, message):
+       hildon.hildon_banner_show_information(parent, "", message)
+
+
+def _hildon_show_information_banner(parent, message):
+       hildon.hildon_banner_show_information(parent, None, message)
+
+
+def _null_show_information_banner(parent, message):
+       pass
+
+
+if IS_FREMANTLE_SUPPORTED:
+       show_information_banner = _fremantle_show_information_banner
+else:
+       try:
+               hildon.hildon_banner_show_information
+               show_information_banner = _hildon_show_information_banner
+       except AttributeError:
+               show_information_banner = _null_show_information_banner
+
+
+def _fremantle_show_busy_banner_start(parent, message):
+       hildon.hildon_gtk_window_set_progress_indicator(parent, True)
+       return parent
+
+
+def _fremantle_show_busy_banner_end(banner):
+       hildon.hildon_gtk_window_set_progress_indicator(banner, False)
+
+
+def _hildon_show_busy_banner_start(parent, message):
+       return hildon.hildon_banner_show_animation(parent, None, message)
+
+
+def _hildon_show_busy_banner_end(banner):
+       banner.destroy()
+
+
+def _null_show_busy_banner_start(parent, message):
+       return None
+
+
+def _null_show_busy_banner_end(banner):
+       assert banner is None
+
+
+try:
+       hildon.hildon_gtk_window_set_progress_indicator
+       show_busy_banner_start = _fremantle_show_busy_banner_start
+       show_busy_banner_end = _fremantle_show_busy_banner_end
+except AttributeError:
+       try:
+               hildon.hildon_banner_show_animation
+               show_busy_banner_start = _hildon_show_busy_banner_start
+               show_busy_banner_end = _hildon_show_busy_banner_end
+       except AttributeError:
+               show_busy_banner_start = _null_show_busy_banner_start
+               show_busy_banner_end = _null_show_busy_banner_end
+
+
+def _hildon_hildonize_text_entry(textEntry):
+       textEntry.set_property('hildon-input-mode', 7)
+
+
+def _null_hildonize_text_entry(textEntry):
+       pass
+
+
+if IS_HILDON_SUPPORTED:
+       hildonize_text_entry = _hildon_hildonize_text_entry
+else:
+       hildonize_text_entry = _null_hildonize_text_entry
+
+
+def _hildon_window_to_portrait(window):
+       # gtk documentation is unclear whether this does a "=" or a "|="
+       flags = hildon.PORTRAIT_MODE_SUPPORT | hildon.PORTRAIT_MODE_REQUEST
+       hildon.hildon_gtk_window_set_portrait_flags(window, flags)
+
+
+def _hildon_window_to_landscape(window):
+       # gtk documentation is unclear whether this does a "=" or a "&= ~"
+       flags = hildon.PORTRAIT_MODE_SUPPORT
+       hildon.hildon_gtk_window_set_portrait_flags(window, flags)
+
+
+def _null_window_to_portrait(window):
+       pass
+
+
+def _null_window_to_landscape(window):
+       pass
+
+
+try:
+       hildon.PORTRAIT_MODE_SUPPORT
+       hildon.PORTRAIT_MODE_REQUEST
+       hildon.hildon_gtk_window_set_portrait_flags
+
+       window_to_portrait = _hildon_window_to_portrait
+       window_to_landscape = _hildon_window_to_landscape
+except AttributeError:
+       window_to_portrait = _null_window_to_portrait
+       window_to_landscape = _null_window_to_landscape
+
+
+def get_device_orientation():
+       bus = dbus.SystemBus()
+       try:
+               rawMceRequest = bus.get_object("com.nokia.mce", "/com/nokia/mce/request")
+               mceRequest = dbus.Interface(rawMceRequest, dbus_interface="com.nokia.mce.request")
+               orientation, standState, faceState, xAxis, yAxis, zAxis = mceRequest.get_device_orientation()
+       except dbus.exception.DBusException:
+               # catching for documentation purposes that when a system doesn't
+               # support this, this is what to expect
+               raise
+
+       if orientation == "":
+               return gtk.ORIENTATION_HORIZONTAL
+       elif orientation == "":
+               return gtk.ORIENTATION_VERTICAL
+       else:
+               raise RuntimeError("Unknown orientation: %s" % orientation)
+
+
+def _hildon_hildonize_password_entry(textEntry):
+       textEntry.set_property('hildon-input-mode', 7 | (1 << 29))
+
+
+def _null_hildonize_password_entry(textEntry):
+       pass
+
+
+if IS_HILDON_SUPPORTED:
+       hildonize_password_entry = _hildon_hildonize_password_entry
+else:
+       hildonize_password_entry = _null_hildonize_password_entry
+
+
+def _hildon_hildonize_combo_entry(comboEntry):
+       comboEntry.set_property('hildon-input-mode', 1 << 4)
+
+
+def _null_hildonize_combo_entry(textEntry):
+       pass
+
+
+if IS_HILDON_SUPPORTED:
+       hildonize_combo_entry = _hildon_hildonize_combo_entry
+else:
+       hildonize_combo_entry = _null_hildonize_combo_entry
+
+
+def _null_create_seekbar():
+       adjustment = gtk.Adjustment(0, 0, 101, 1, 5, 1)
+       seek = gtk.HScale(adjustment)
+       seek.set_draw_value(False)
+       return seek
+
+
+def _fremantle_create_seekbar():
+       seek = hildon.Seekbar()
+       seek.set_range(0.0, 100)
+       seek.set_draw_value(False)
+       seek.set_update_policy(gtk.UPDATE_DISCONTINUOUS)
+       return seek
+
+
+try:
+       hildon.Seekbar
+       create_seekbar = _fremantle_create_seekbar
+except AttributeError:
+       create_seekbar = _null_create_seekbar
+
+
+def _fremantle_hildonize_scrollwindow(scrolledWindow):
+       pannableWindow = hildon.PannableArea()
+
+       child = scrolledWindow.get_child()
+       scrolledWindow.remove(child)
+       pannableWindow.add(child)
+
+       parent = scrolledWindow.get_parent()
+       if parent is not None:
+               parent.remove(scrolledWindow)
+               parent.add(pannableWindow)
+
+       return pannableWindow
+
+
+def _hildon_hildonize_scrollwindow(scrolledWindow):
+       hildon.hildon_helper_set_thumb_scrollbar(scrolledWindow, True)
+       return scrolledWindow
+
+
+def _null_hildonize_scrollwindow(scrolledWindow):
+       return scrolledWindow
+
+
+try:
+       hildon.PannableArea
+       hildonize_scrollwindow = _fremantle_hildonize_scrollwindow
+       hildonize_scrollwindow_with_viewport = _hildon_hildonize_scrollwindow
+except AttributeError:
+       try:
+               hildon.hildon_helper_set_thumb_scrollbar
+               hildonize_scrollwindow = _hildon_hildonize_scrollwindow
+               hildonize_scrollwindow_with_viewport = _hildon_hildonize_scrollwindow
+       except AttributeError:
+               hildonize_scrollwindow = _null_hildonize_scrollwindow
+               hildonize_scrollwindow_with_viewport = _null_hildonize_scrollwindow
+
+
+def _hildon_request_number(parent, title, range, default):
+       spinner = hildon.NumberEditor(*range)
+       spinner.set_value(default)
+
+       dialog = gtk.Dialog(
+               title,
+               parent,
+               gtk.DIALOG_MODAL|gtk.DIALOG_DESTROY_WITH_PARENT,
+               (gtk.STOCK_OK, gtk.RESPONSE_OK, gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL),
+       )
+       dialog.set_default_response(gtk.RESPONSE_CANCEL)
+       dialog.get_child().add(spinner)
+
+       try:
+               dialog.show_all()
+               response = dialog.run()
+
+               if response == gtk.RESPONSE_OK:
+                       return spinner.get_value()
+               elif response == gtk.RESPONSE_CANCEL or response == gtk.RESPONSE_DELETE_EVENT:
+                       raise RuntimeError("User cancelled request")
+               else:
+                       raise RuntimeError("Unrecognized response %r", response)
+       finally:
+               dialog.hide()
+               dialog.destroy()
+
+
+def _null_request_number(parent, title, range, default):
+       adjustment = gtk.Adjustment(default, range[0], range[1], 1, 5, 0)
+       spinner = gtk.SpinButton(adjustment, 0, 0)
+       spinner.set_wrap(False)
+
+       dialog = gtk.Dialog(
+               title,
+               parent,
+               gtk.DIALOG_MODAL|gtk.DIALOG_DESTROY_WITH_PARENT,
+               (gtk.STOCK_OK, gtk.RESPONSE_OK, gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL),
+       )
+       dialog.set_default_response(gtk.RESPONSE_CANCEL)
+       dialog.get_child().add(spinner)
+
+       try:
+               dialog.show_all()
+               response = dialog.run()
+
+               if response == gtk.RESPONSE_OK:
+                       return spinner.get_value_as_int()
+               elif response == gtk.RESPONSE_CANCEL or response == gtk.RESPONSE_DELETE_EVENT:
+                       raise RuntimeError("User cancelled request")
+               else:
+                       raise RuntimeError("Unrecognized response %r", response)
+       finally:
+               dialog.hide()
+               dialog.destroy()
+
+
+try:
+       hildon.NumberEditor # TODO deprecated in fremantle
+       request_number = _hildon_request_number
+except AttributeError:
+       request_number = _null_request_number
+
+
+def _hildon_touch_selector(parent, title, items, defaultIndex):
+       model = gtk.ListStore(gobject.TYPE_STRING)
+       for item in items:
+               model.append((item, ))
+
+       selector = hildon.TouchSelector()
+       selector.append_text_column(model, True)
+       selector.set_column_selection_mode(hildon.TOUCH_SELECTOR_SELECTION_MODE_SINGLE)
+       selector.set_active(0, defaultIndex)
+
+       dialog = hildon.PickerDialog(parent)
+       dialog.set_selector(selector)
+
+       try:
+               dialog.show_all()
+               response = dialog.run()
+
+               if response == gtk.RESPONSE_OK:
+                       return selector.get_active(0)
+               elif response == gtk.RESPONSE_CANCEL or response == gtk.RESPONSE_DELETE_EVENT:
+                       raise RuntimeError("User cancelled request")
+               else:
+                       raise RuntimeError("Unrecognized response %r", response)
+       finally:
+               dialog.hide()
+               dialog.destroy()
+
+
+def _on_null_touch_selector_activated(treeView, path, column, dialog, pathData):
+       dialog.response(gtk.RESPONSE_OK)
+       pathData[0] = path
+
+
+def _null_touch_selector(parent, title, items, defaultIndex = -1):
+       parentSize = parent.get_size()
+
+       model = gtk.ListStore(gobject.TYPE_STRING)
+       for item in items:
+               model.append((item, ))
+
+       cell = gtk.CellRendererText()
+       set_cell_thumb_selectable(cell)
+       column = gtk.TreeViewColumn(title)
+       column.pack_start(cell, expand=True)
+       column.add_attribute(cell, "text", 0)
+
+       treeView = gtk.TreeView()
+       treeView.set_model(model)
+       treeView.append_column(column)
+       selection = treeView.get_selection()
+       selection.set_mode(gtk.SELECTION_SINGLE)
+       if 0 < defaultIndex:
+               selection.select_path((defaultIndex, ))
+
+       scrolledWin = gtk.ScrolledWindow()
+       scrolledWin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
+       scrolledWin.add(treeView)
+
+       dialog = gtk.Dialog(
+               title,
+               parent,
+               gtk.DIALOG_MODAL|gtk.DIALOG_DESTROY_WITH_PARENT,
+               (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL),
+       )
+       dialog.set_default_response(gtk.RESPONSE_CANCEL)
+       dialog.get_child().add(scrolledWin)
+       dialog.resize(parentSize[0], max(parentSize[1]-100, 100))
+
+       scrolledWin = hildonize_scrollwindow(scrolledWin)
+       pathData = [None]
+       treeView.connect("row-activated", _on_null_touch_selector_activated, dialog, pathData)
+
+       try:
+               dialog.show_all()
+               response = dialog.run()
+
+               if response == gtk.RESPONSE_OK:
+                       if pathData[0] is None:
+                               raise RuntimeError("No selection made")
+                       return pathData[0][0]
+               elif response == gtk.RESPONSE_CANCEL or response == gtk.RESPONSE_DELETE_EVENT:
+                       raise RuntimeError("User cancelled request")
+               else:
+                       raise RuntimeError("Unrecognized response %r", response)
+       finally:
+               dialog.hide()
+               dialog.destroy()
+
+
+try:
+       hildon.PickerDialog
+       hildon.TouchSelector
+       touch_selector = _hildon_touch_selector
+except AttributeError:
+       touch_selector = _null_touch_selector
+
+
+def _hildon_touch_selector_entry(parent, title, items, defaultItem):
+       # Got a segfault when using append_text_column with TouchSelectorEntry, so using this way
+       try:
+               selector = hildon.TouchSelectorEntry(text=True)
+       except TypeError:
+               selector = hildon.hildon_touch_selector_entry_new_text()
+       defaultIndex = -1
+       for i, item in enumerate(items):
+               selector.append_text(item)
+               if item == defaultItem:
+                       defaultIndex = i
+
+       dialog = hildon.PickerDialog(parent)
+       dialog.set_selector(selector)
+
+       if 0 < defaultIndex:
+               selector.set_active(0, defaultIndex)
+       else:
+               selector.get_entry().set_text(defaultItem)
+
+       try:
+               dialog.show_all()
+               response = dialog.run()
+       finally:
+               dialog.hide()
+
+       if response == gtk.RESPONSE_OK:
+               return selector.get_entry().get_text()
+       elif response == gtk.RESPONSE_CANCEL or response == gtk.RESPONSE_DELETE_EVENT:
+               raise RuntimeError("User cancelled request")
+       else:
+               raise RuntimeError("Unrecognized response %r", response)
+
+
+def _on_null_touch_selector_entry_entry_changed(entry, result, selection, defaultIndex):
+       custom = entry.get_text().strip()
+       if custom:
+               result[0] = custom
+               selection.unselect_all()
+       else:
+               result[0] = None
+               selection.select_path((defaultIndex, ))
+
+
+def _on_null_touch_selector_entry_entry_activated(customEntry, dialog, result):
+       dialog.response(gtk.RESPONSE_OK)
+       result[0] = customEntry.get_text()
+
+
+def _on_null_touch_selector_entry_tree_activated(treeView, path, column, dialog, result):
+       dialog.response(gtk.RESPONSE_OK)
+       model = treeView.get_model()
+       itr = model.get_iter(path)
+       if itr is not None:
+               result[0] = model.get_value(itr, 0)
+
+
+def _null_touch_selector_entry(parent, title, items, defaultItem):
+       parentSize = parent.get_size()
+
+       model = gtk.ListStore(gobject.TYPE_STRING)
+       defaultIndex = -1
+       for i, item in enumerate(items):
+               model.append((item, ))
+               if item == defaultItem:
+                       defaultIndex = i
+
+       cell = gtk.CellRendererText()
+       set_cell_thumb_selectable(cell)
+       column = gtk.TreeViewColumn(title)
+       column.pack_start(cell, expand=True)
+       column.add_attribute(cell, "text", 0)
+
+       treeView = gtk.TreeView()
+       treeView.set_model(model)
+       treeView.append_column(column)
+       selection = treeView.get_selection()
+       selection.set_mode(gtk.SELECTION_SINGLE)
+
+       scrolledWin = gtk.ScrolledWindow()
+       scrolledWin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
+       scrolledWin.add(treeView)
+
+       customEntry = gtk.Entry()
+
+       layout = gtk.VBox()
+       layout.pack_start(customEntry, expand=False)
+       layout.pack_start(scrolledWin)
+
+       dialog = gtk.Dialog(
+               title,
+               parent,
+               gtk.DIALOG_MODAL|gtk.DIALOG_DESTROY_WITH_PARENT,
+               (gtk.STOCK_OK, gtk.RESPONSE_OK, gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL),
+       )
+       dialog.set_default_response(gtk.RESPONSE_CANCEL)
+       dialog.get_child().add(layout)
+       dialog.resize(parentSize[0], max(parentSize[1]-100, 100))
+
+       scrolledWin = hildonize_scrollwindow(scrolledWin)
+
+       result = [None]
+       if 0 < defaultIndex:
+               selection.select_path((defaultIndex, ))
+               result[0] = defaultItem
+       else:
+               customEntry.set_text(defaultItem)
+
+       customEntry.connect("activate", _on_null_touch_selector_entry_entry_activated, dialog, result)
+       customEntry.connect("changed", _on_null_touch_selector_entry_entry_changed, result, selection, defaultIndex)
+       treeView.connect("row-activated", _on_null_touch_selector_entry_tree_activated, dialog, result)
+
+       try:
+               dialog.show_all()
+               response = dialog.run()
+
+               if response == gtk.RESPONSE_OK:
+                       _, itr = selection.get_selected()
+                       if itr is not None:
+                               return model.get_value(itr, 0)
+                       else:
+                               enteredText = customEntry.get_text().strip()
+                               if enteredText:
+                                       return enteredText
+                               elif result[0] is not None:
+                                       return result[0]
+                               else:
+                                       raise RuntimeError("No selection made")
+               elif response == gtk.RESPONSE_CANCEL or response == gtk.RESPONSE_DELETE_EVENT:
+                       raise RuntimeError("User cancelled request")
+               else:
+                       raise RuntimeError("Unrecognized response %r", response)
+       finally:
+               dialog.hide()
+               dialog.destroy()
+
+
+try:
+       hildon.PickerDialog
+       hildon.TouchSelectorEntry
+       touch_selector_entry = _hildon_touch_selector_entry
+except AttributeError:
+       touch_selector_entry = _null_touch_selector_entry
+
+
+if __name__ == "__main__":
+       app = get_app_class()()
+
+       label = gtk.Label("Hello World from a Label!")
+
+       win = gtk.Window()
+       win.add(label)
+       win = hildonize_window(app, win)
+       if False and IS_FREMANTLE_SUPPORTED:
+               appMenu = hildon.AppMenu()
+               for i in xrange(5):
+                       b = gtk.Button(str(i))
+                       appMenu.append(b)
+               win.set_app_menu(appMenu)
+               win.show_all()
+               appMenu.show_all()
+               gtk.main()
+       elif False:
+               print touch_selector(win, "Test", ["A", "B", "C", "D"], 2)
+       elif False:
+               print touch_selector_entry(win, "Test", ["A", "B", "C", "D"], "C")
+               print touch_selector_entry(win, "Test", ["A", "B", "C", "D"], "Blah")
+       elif False:
+               import pprint
+               name, value = "", ""
+               goodLocals = [
+                       (name, value) for (name, value) in locals().iteritems()
+                       if not name.startswith("_")
+               ]
+               pprint.pprint(goodLocals)
+       elif False:
+               import time
+               show_information_banner(win, "Hello World")
+               time.sleep(5)
+       elif False:
+               import time
+               banner = show_busy_banner_start(win, "Hello World")
+               time.sleep(5)
+               show_busy_banner_end(banner)
index aac896d..4198f4b 100644 (file)
@@ -8,6 +8,7 @@ import pickle
 import contextlib
 import itertools
 import codecs
+from xml.sax import saxutils
 import csv
 try:
        import cStringIO as StringIO
@@ -207,3 +208,24 @@ def unicode_csv_reader(unicode_csv_data, dialect=csv.excel, **kwargs):
 def utf_8_encoder(unicode_csv_data):
        for line in unicode_csv_data:
                yield line.encode('utf-8')
+
+
+_UNESCAPE_ENTITIES = {
+ "&quot;": '"',
+ "&nbsp;": " ",
+ "&#39;": "'",
+}
+
+
+_ESCAPE_ENTITIES = dict((v, k) for (v, k) in zip(_UNESCAPE_ENTITIES.itervalues(), _UNESCAPE_ENTITIES.iterkeys()))
+del _ESCAPE_ENTITIES[" "]
+
+
+def unescape(text):
+       plain = saxutils.unescape(text, _UNESCAPE_ENTITIES)
+       return plain
+
+
+def escape(text):
+       fancy = saxutils.escape(text, _ESCAPE_ENTITIES)
+       return fancy
index c0a70a9..9b8d88c 100644 (file)
@@ -649,6 +649,35 @@ def call_trace(f):
 
 
 @contextlib.contextmanager
+def nested_break():
+       """
+       >>> with nested_break() as mylabel:
+       ...     for i in xrange(3):
+       ...             print "Outer", i
+       ...             for j in xrange(3):
+       ...                     if i == 2: raise mylabel
+       ...                     if j == 2: break
+       ...                     print "Inner", j
+       ...             print "more processing"
+       Outer 0
+       Inner 0
+       Inner 1
+       Outer 1
+       Inner 0
+       Inner 1
+       Outer 2
+       """
+
+       class NestedBreakException(Exception):
+               pass
+
+       try:
+               yield NestedBreakException
+       except NestedBreakException:
+               pass
+
+
+@contextlib.contextmanager
 def lexical_scope(*args):
        """
        @note Source: http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/520586
index 491c96d..153558d 100644 (file)
@@ -1,6 +1,7 @@
 import logging
 
-from PyQt4 import QtCore
+import qt_compat
+QtCore = qt_compat.QtCore
 
 import misc
 
@@ -22,40 +23,23 @@ class QThread44(QtCore.QThread):
                self.exec_()
 
 
-class _ParentThread(QtCore.QObject):
-
-       def __init__(self, pool):
-               QtCore.QObject.__init__(self)
-               self._pool = pool
-
-       @QtCore.pyqtSlot(object)
-       @misc.log_exception(_moduleLogger)
-       def _on_task_complete(self, taskResult):
-               on_success, on_error, isError, result = taskResult
-               if not self._pool._isRunning:
-                       if isError:
-                               _moduleLogger.error("Masking: %s" % (result, ))
-                       isError = True
-                       result = StopIteration("Cancelling all callbacks")
-               callback = on_success if not isError else on_error
-               try:
-                       callback(result)
-               except Exception:
-                       _moduleLogger.exception("Callback errored")
-
-
 class _WorkerThread(QtCore.QObject):
 
-       taskComplete  = QtCore.pyqtSignal(object)
+       _taskComplete  = qt_compat.Signal(object)
 
-       def __init__(self, pool):
+       def __init__(self, futureThread):
                QtCore.QObject.__init__(self)
-               self._pool = pool
+               self._futureThread = futureThread
+               self._futureThread._addTask.connect(self._on_task_added)
+               self._taskComplete.connect(self._futureThread._on_task_complete)
 
-       @QtCore.pyqtSlot(object)
-       @misc.log_exception(_moduleLogger)
+       @qt_compat.Slot(object)
        def _on_task_added(self, task):
-               if not self._pool._isRunning:
+               self.__on_task_added(task)
+
+       @misc.log_exception(_moduleLogger)
+       def __on_task_added(self, task):
+               if not self._futureThread._isRunning:
                        _moduleLogger.error("Dropping task")
 
                func, args, kwds, on_success, on_error = task
@@ -69,39 +53,47 @@ class _WorkerThread(QtCore.QObject):
                        isError = True
 
                taskResult = on_success, on_error, isError, result
-               self.taskComplete.emit(taskResult)
-
-       @QtCore.pyqtSlot()
-       @misc.log_exception(_moduleLogger)
-       def _on_stop_requested(self):
-               self._pool._thread.quit()
+               self._taskComplete.emit(taskResult)
 
 
-class AsyncPool(QtCore.QObject):
+class FutureThread(QtCore.QObject):
 
-       _addTask = QtCore.pyqtSignal(object)
-       _stopPool = QtCore.pyqtSignal()
+       _addTask = qt_compat.Signal(object)
 
        def __init__(self):
                QtCore.QObject.__init__(self)
                self._thread = QThread44()
-               self._isRunning = True
-               self._parent = _ParentThread(self)
+               self._isRunning = False
                self._worker = _WorkerThread(self)
                self._worker.moveToThread(self._thread)
 
-               self._addTask.connect(self._worker._on_task_added)
-               self._worker.taskComplete.connect(self._parent._on_task_complete)
-               self._stopPool.connect(self._worker._on_stop_requested)
-
        def start(self):
                self._thread.start()
+               self._isRunning = True
 
        def stop(self):
                self._isRunning = False
-               self._stopPool.emit()
+               self._thread.quit()
 
        def add_task(self, func, args, kwds, on_success, on_error):
                assert self._isRunning, "Task queue not started"
                task = func, args, kwds, on_success, on_error
                self._addTask.emit(task)
+
+       @qt_compat.Slot(object)
+       def _on_task_complete(self, taskResult):
+               self.__on_task_complete(taskResult)
+
+       @misc.log_exception(_moduleLogger)
+       def __on_task_complete(self, taskResult):
+               on_success, on_error, isError, result = taskResult
+               if not self._isRunning:
+                       if isError:
+                               _moduleLogger.error("Masking: %s" % (result, ))
+                       isError = True
+                       result = StopIteration("Cancelling all callbacks")
+               callback = on_success if not isError else on_error
+               try:
+                       callback(result)
+               except Exception:
+                       _moduleLogger.exception("Callback errored")
diff --git a/src/util/qt_compat.py b/src/util/qt_compat.py
new file mode 100644 (file)
index 0000000..2ab7fa4
--- /dev/null
@@ -0,0 +1,46 @@
+#!/usr/bin/env python
+
+from __future__ import with_statement
+from __future__ import division
+
+#try:
+#      import PySide.QtCore as _QtCore
+#      QtCore = _QtCore
+#      USES_PYSIDE = True
+#except ImportError:
+if True:
+       import sip
+       sip.setapi('QString', 2)
+       sip.setapi('QVariant', 2)
+       import PyQt4.QtCore as _QtCore
+       QtCore = _QtCore
+       USES_PYSIDE = False
+
+
+def _pyside_import_module(moduleName):
+       pyside = __import__('PySide', globals(), locals(), [moduleName], -1)
+       return getattr(pyside, moduleName)
+
+
+def _pyqt4_import_module(moduleName):
+       pyside = __import__('PyQt4', globals(), locals(), [moduleName], -1)
+       return getattr(pyside, moduleName)
+
+
+if USES_PYSIDE:
+       import_module = _pyside_import_module
+
+       Signal = QtCore.Signal
+       Slot = QtCore.Slot
+       Property = QtCore.Property
+else:
+       import_module = _pyqt4_import_module
+
+       Signal = QtCore.pyqtSignal
+       Slot = QtCore.pyqtSlot
+       Property = QtCore.pyqtProperty
+
+
+if __name__ == "__main__":
+       pass
+
index c23e512..6b77d5d 100755 (executable)
@@ -3,8 +3,9 @@
 import math
 import logging
 
-from PyQt4 import QtGui
-from PyQt4 import QtCore
+import qt_compat
+QtCore = qt_compat.QtCore
+QtGui = qt_compat.import_module("QtGui")
 
 import misc as misc_utils
 
@@ -496,11 +497,11 @@ class QPieDisplay(QtGui.QWidget):
 
 class QPieButton(QtGui.QWidget):
 
-       activated = QtCore.pyqtSignal(int)
-       highlighted = QtCore.pyqtSignal(int)
-       canceled = QtCore.pyqtSignal()
-       aboutToShow = QtCore.pyqtSignal()
-       aboutToHide = QtCore.pyqtSignal()
+       activated = qt_compat.Signal(int)
+       highlighted = qt_compat.Signal(int)
+       canceled = qt_compat.Signal()
+       aboutToShow = qt_compat.Signal()
+       aboutToHide = qt_compat.Signal()
 
        BUTTON_RADIUS = 24
        DELAY = 250
@@ -778,11 +779,11 @@ class QPieButton(QtGui.QWidget):
 
 class QPieMenu(QtGui.QWidget):
 
-       activated = QtCore.pyqtSignal(int)
-       highlighted = QtCore.pyqtSignal(int)
-       canceled = QtCore.pyqtSignal()
-       aboutToShow = QtCore.pyqtSignal()
-       aboutToHide = QtCore.pyqtSignal()
+       activated = qt_compat.Signal(int)
+       highlighted = qt_compat.Signal(int)
+       canceled = qt_compat.Signal()
+       aboutToShow = qt_compat.Signal()
+       aboutToHide = qt_compat.Signal()
 
        def __init__(self, parent = None):
                QtGui.QWidget.__init__(self, parent)
index c7094f4..50ae9ae 100755 (executable)
@@ -6,7 +6,8 @@ from __future__ import division
 import os
 import warnings
 
-from PyQt4 import QtGui
+import qt_compat
+QtGui = qt_compat.import_module("QtGui")
 
 import qtpie
 
index 56a3408..11b3453 100644 (file)
@@ -3,8 +3,9 @@ import contextlib
 import datetime
 import logging
 
-from PyQt4 import QtCore
-from PyQt4 import QtGui
+import qt_compat
+QtCore = qt_compat.QtCore
+QtGui = qt_compat.import_module("QtGui")
 
 import misc
 
@@ -54,8 +55,8 @@ class ErrorMessage(object):
 
 class QErrorLog(QtCore.QObject):
 
-       messagePushed = QtCore.pyqtSignal()
-       messagePopped = QtCore.pyqtSignal()
+       messagePushed = qt_compat.Signal()
+       messagePopped = qt_compat.Signal()
 
        def __init__(self):
                QtCore.QObject.__init__(self)
@@ -113,21 +114,7 @@ class ErrorDisplay(object):
                self._errorLog.messagePushed.connect(self._on_message_pushed)
                self._errorLog.messagePopped.connect(self._on_message_popped)
 
-               self._icons = {
-                       ErrorMessage.LEVEL_BUSY:
-                               get_theme_icon(
-                                       #("process-working", "view-refresh", "general_refresh", "gtk-refresh")
-                                       ("view-refresh", "general_refresh", "gtk-refresh", )
-                               ).pixmap(32, 32),
-                       ErrorMessage.LEVEL_INFO:
-                               get_theme_icon(
-                                       ("dialog-information", "general_notes", "gtk-info")
-                               ).pixmap(32, 32),
-                       ErrorMessage.LEVEL_ERROR:
-                               get_theme_icon(
-                                       ("dialog-error", "app_install_error", "gtk-dialog-error")
-                               ).pixmap(32, 32),
-               }
+               self._icons = None
                self._severityLabel = QtGui.QLabel()
                self._severityLabel.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter)
 
@@ -136,17 +123,11 @@ class ErrorDisplay(object):
                self._message.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter)
                self._message.setWordWrap(True)
 
-               closeIcon = get_theme_icon(("window-close", "general_close", "gtk-close"), self._SENTINEL_ICON)
-               if closeIcon is not self._SENTINEL_ICON:
-                       self._closeLabel = QtGui.QPushButton(closeIcon, "")
-               else:
-                       self._closeLabel = QtGui.QPushButton("X")
-               self._closeLabel.clicked.connect(self._on_close)
+               self._closeLabel = None
 
                self._controlLayout = QtGui.QHBoxLayout()
                self._controlLayout.addWidget(self._severityLabel, 1, QtCore.Qt.AlignCenter)
                self._controlLayout.addWidget(self._message, 1000)
-               self._controlLayout.addWidget(self._closeLabel, 1, QtCore.Qt.AlignCenter)
 
                self._widget = QtGui.QWidget()
                self._widget.setLayout(self._controlLayout)
@@ -157,23 +138,47 @@ class ErrorDisplay(object):
                return self._widget
 
        def _show_error(self):
+               if self._icons is None:
+                       self._icons = {
+                               ErrorMessage.LEVEL_BUSY:
+                                       get_theme_icon(
+                                               #("process-working", "view-refresh", "general_refresh", "gtk-refresh")
+                                               ("view-refresh", "general_refresh", "gtk-refresh", )
+                                       ).pixmap(32, 32),
+                               ErrorMessage.LEVEL_INFO:
+                                       get_theme_icon(
+                                               ("dialog-information", "general_notes", "gtk-info")
+                                       ).pixmap(32, 32),
+                               ErrorMessage.LEVEL_ERROR:
+                                       get_theme_icon(
+                                               ("dialog-error", "app_install_error", "gtk-dialog-error")
+                                       ).pixmap(32, 32),
+                       }
+               if self._closeLabel is None:
+                       closeIcon = get_theme_icon(("window-close", "general_close", "gtk-close"), self._SENTINEL_ICON)
+                       if closeIcon is not self._SENTINEL_ICON:
+                               self._closeLabel = QtGui.QPushButton(closeIcon, "")
+                       else:
+                               self._closeLabel = QtGui.QPushButton("X")
+                       self._closeLabel.clicked.connect(self._on_close)
+                       self._controlLayout.addWidget(self._closeLabel, 1, QtCore.Qt.AlignCenter)
                error = self._errorLog.peek_message()
                self._message.setText(error.message)
                self._severityLabel.setPixmap(self._icons[error.level])
                self._widget.show()
 
-       @QtCore.pyqtSlot()
-       @QtCore.pyqtSlot(bool)
+       @qt_compat.Slot()
+       @qt_compat.Slot(bool)
        @misc.log_exception(_moduleLogger)
        def _on_close(self, checked = False):
                self._errorLog.pop()
 
-       @QtCore.pyqtSlot()
+       @qt_compat.Slot()
        @misc.log_exception(_moduleLogger)
        def _on_message_pushed(self):
                self._show_error()
 
-       @QtCore.pyqtSlot()
+       @qt_compat.Slot()
        @misc.log_exception(_moduleLogger)
        def _on_message_popped(self):
                if len(self._errorLog) == 0:
@@ -231,9 +236,17 @@ class QHtmlDelegate(QtGui.QStyledItemDelegate):
                doc.documentLayout().draw(painter, ctx)
                painter.restore()
 
-       def setWidth(self, width):
-               # @bug we need to be emitting sizeHintChanged but it requires an index
+       def setWidth(self, width, model):
+               if self._width == width:
+                       return
                self._width = width
+               for c in xrange(model.rowCount()):
+                       cItem = model.item(c, 0)
+                       for r in xrange(model.rowCount()):
+                               rItem = cItem.child(r, 0)
+                               rIndex = model.indexFromItem(rItem)
+                               self.sizeHintChanged.emit(rIndex)
+                               return
 
        def sizeHint(self, option, index):
                newOption = QtGui.QStyleOptionViewItemV4(option)
@@ -250,6 +263,45 @@ class QHtmlDelegate(QtGui.QStyledItemDelegate):
                return size
 
 
+class QSignalingMainWindow(QtGui.QMainWindow):
+
+       closed = qt_compat.Signal()
+       hidden = qt_compat.Signal()
+       shown = qt_compat.Signal()
+       resized = qt_compat.Signal()
+
+       def __init__(self, *args, **kwd):
+               QtGui.QMainWindow.__init__(*((self, )+args), **kwd)
+
+       def closeEvent(self, event):
+               val = QtGui.QMainWindow.closeEvent(self, event)
+               self.closed.emit()
+               return val
+
+       def hideEvent(self, event):
+               val = QtGui.QMainWindow.hideEvent(self, event)
+               self.hidden.emit()
+               return val
+
+       def showEvent(self, event):
+               val = QtGui.QMainWindow.showEvent(self, event)
+               self.shown.emit()
+               return val
+
+       def resizeEvent(self, event):
+               val = QtGui.QMainWindow.resizeEvent(self, event)
+               self.resized.emit()
+               return val
+
+def set_current_index(selector, itemText, default = 0):
+       for i in xrange(selector.count()):
+               if selector.itemText(i) == itemText:
+                       selector.setCurrentIndex(i)
+                       break
+       else:
+               itemText.setCurrentIndex(default)
+
+
 def _null_set_stackable(window, isStackable):
        pass
 
@@ -265,12 +317,12 @@ except AttributeError:
        set_stackable = _null_set_stackable
 
 
-def _null_set_autorient(window, isStackable):
+def _null_set_autorient(window, doAutoOrient):
        pass
 
 
-def _maemo_set_autorient(window, isStackable):
-       window.setAttribute(QtCore.Qt.WA_Maemo5AutoOrientation, isStackable)
+def _maemo_set_autorient(window, doAutoOrient):
+       window.setAttribute(QtCore.Qt.WA_Maemo5AutoOrientation, doAutoOrient)
 
 
 try:
@@ -294,13 +346,16 @@ def _null_set_window_orientation(window, orientation):
 
 def _maemo_set_window_orientation(window, orientation):
        if orientation == QtCore.Qt.Vertical:
-               oldHint = QtCore.Qt.WA_Maemo5LandscapeOrientation
-               newHint = QtCore.Qt.WA_Maemo5PortraitOrientation
+               window.setAttribute(QtCore.Qt.WA_Maemo5LandscapeOrientation, False)
+               window.setAttribute(QtCore.Qt.WA_Maemo5PortraitOrientation, True)
        elif orientation == QtCore.Qt.Horizontal:
-               oldHint = QtCore.Qt.WA_Maemo5PortraitOrientation
-               newHint = QtCore.Qt.WA_Maemo5LandscapeOrientation
-       window.setAttribute(oldHint, False)
-       window.setAttribute(newHint, True)
+               window.setAttribute(QtCore.Qt.WA_Maemo5LandscapeOrientation, True)
+               window.setAttribute(QtCore.Qt.WA_Maemo5PortraitOrientation, False)
+       elif orientation is None:
+               window.setAttribute(QtCore.Qt.WA_Maemo5LandscapeOrientation, False)
+               window.setAttribute(QtCore.Qt.WA_Maemo5PortraitOrientation, False)
+       else:
+               raise RuntimeError("Unknown orientation: %r" % orientation)
 
 
 try:
index b84e8ff..2c50c8a 100644 (file)
@@ -5,8 +5,9 @@ from __future__ import division
 
 import logging
 
-from PyQt4 import QtGui
-from PyQt4 import QtCore
+import qt_compat
+QtCore = qt_compat.QtCore
+QtGui = qt_compat.import_module("QtGui")
 
 from util import qui_utils
 from util import misc as misc_utils
@@ -17,6 +18,11 @@ _moduleLogger = logging.getLogger(__name__)
 
 class ApplicationWrapper(object):
 
+       DEFAULT_ORIENTATION = "Default"
+       AUTO_ORIENTATION = "Auto"
+       LANDSCAPE_ORIENTATION = "Landscape"
+       PORTRAIT_ORIENTATION = "Portrait"
+
        def __init__(self, qapp, constants):
                self._constants = constants
                self._qapp = qapp
@@ -31,11 +37,12 @@ class ApplicationWrapper(object):
                self._fullscreenAction.setShortcut(QtGui.QKeySequence("CTRL+Enter"))
                self._fullscreenAction.toggled.connect(self._on_toggle_fullscreen)
 
+               self._orientation = self.DEFAULT_ORIENTATION
                self._orientationAction = QtGui.QAction(None)
-               self._orientationAction.setText("Orientation")
+               self._orientationAction.setText("Next Orientation")
                self._orientationAction.setCheckable(True)
                self._orientationAction.setShortcut(QtGui.QKeySequence("CTRL+o"))
-               self._orientationAction.toggled.connect(self._on_toggle_orientation)
+               self._orientationAction.triggered.connect(self._on_next_orientation)
 
                self._logAction = QtGui.QAction(None)
                self._logAction.setText("Log")
@@ -61,7 +68,7 @@ class ApplicationWrapper(object):
                self._idleDelay = QtCore.QTimer()
                self._idleDelay.setSingleShot(True)
                self._idleDelay.setInterval(0)
-               self._idleDelay.timeout.connect(lambda: self._mainWindow.start())
+               self._idleDelay.timeout.connect(self._on_delayed_start)
                self._idleDelay.start()
 
        def load_settings(self):
@@ -94,6 +101,10 @@ class ApplicationWrapper(object):
                return self._orientationAction
 
        @property
+       def orientation(self):
+               return self._orientation
+
+       @property
        def logAction(self):
                return self._logAction
 
@@ -105,6 +116,19 @@ class ApplicationWrapper(object):
        def quitAction(self):
                return self._quitAction
 
+       def set_orientation(self, orientation):
+               self._orientation = orientation
+               self._mainWindow.update_orientation(self._orientation)
+
+       @classmethod
+       def _next_orientation(cls, current):
+               return {
+                       cls.DEFAULT_ORIENTATION: cls.AUTO_ORIENTATION,
+                       cls.AUTO_ORIENTATION: cls.LANDSCAPE_ORIENTATION,
+                       cls.LANDSCAPE_ORIENTATION: cls.PORTRAIT_ORIENTATION,
+                       cls.PORTRAIT_ORIENTATION: cls.DEFAULT_ORIENTATION,
+               }[current]
+
        def _close_windows(self):
                if self._mainWindow is not None:
                        self.save_settings()
@@ -113,6 +137,10 @@ class ApplicationWrapper(object):
                        self._mainWindow = None
 
        @misc_utils.log_exception(_moduleLogger)
+       def _on_delayed_start(self):
+               self._mainWindow.start()
+
+       @misc_utils.log_exception(_moduleLogger)
        def _on_app_quit(self, checked = False):
                if self._mainWindow is not None:
                        self.save_settings()
@@ -130,9 +158,9 @@ class ApplicationWrapper(object):
                        self._mainWindow.set_fullscreen(checked)
 
        @misc_utils.log_exception(_moduleLogger)
-       def _on_toggle_orientation(self, checked = False):
+       def _on_next_orientation(self, checked = False):
                with qui_utils.notify_error(self._errorLog):
-                       self._mainWindow.set_orientation(checked)
+                       self.set_orientation(self._next_orientation(self._orientation))
 
        @misc_utils.log_exception(_moduleLogger)
        def _on_about(self, checked = True):
@@ -171,7 +199,7 @@ class WindowWrapper(object):
                centralWidget.setLayout(self._superLayout)
                centralWidget.setContentsMargins(0, 0, 0, 0)
 
-               self._window = QtGui.QMainWindow(parent)
+               self._window = qui_utils.QSignalingMainWindow(parent)
                self._window.setAttribute(QtCore.Qt.WA_DeleteOnClose, True)
                qui_utils.set_stackable(self._window, True)
                self._window.setCentralWidget(centralWidget)
@@ -191,6 +219,28 @@ class WindowWrapper(object):
        def window(self):
                return self._window
 
+       @property
+       def windowOrientation(self):
+               geom = self._window.size()
+               if geom.width() <= geom.height():
+                       return QtCore.Qt.Vertical
+               else:
+                       return QtCore.Qt.Horizontal
+
+       @property
+       def idealWindowOrientation(self):
+               if self._app.orientation ==  self._app.AUTO_ORIENTATION:
+                       windowOrientation = self.windowOrientation
+               elif self._app.orientation ==  self._app.DEFAULT_ORIENTATION:
+                       windowOrientation = qui_utils.screen_orientation()
+               elif self._app.orientation ==  self._app.LANDSCAPE_ORIENTATION:
+                       windowOrientation = QtCore.Qt.Horizontal
+               elif self._app.orientation ==  self._app.PORTRAIT_ORIENTATION:
+                       windowOrientation = QtCore.Qt.Vertical
+               else:
+                       raise RuntimeError("Bad! No %r for you" % self._app.orientation)
+               return windowOrientation
+
        def walk_children(self):
                return ()
 
@@ -207,10 +257,10 @@ class WindowWrapper(object):
                pass
 
        def show(self):
-               self.set_fullscreen(self._app.fullscreenAction.isChecked())
                self._window.show()
                for child in self.walk_children():
                        child.show()
+               self.set_fullscreen(self._app.fullscreenAction.isChecked())
 
        def hide(self):
                for child in self.walk_children():
@@ -218,23 +268,34 @@ class WindowWrapper(object):
                self._window.hide()
 
        def set_fullscreen(self, isFullscreen):
-               if isFullscreen:
-                       self._window.showFullScreen()
-               else:
-                       self._window.showNormal()
+               if self._window.isVisible():
+                       if isFullscreen:
+                               self._window.showFullScreen()
+                       else:
+                               self._window.showNormal()
                for child in self.walk_children():
                        child.set_fullscreen(isFullscreen)
 
-       def set_orientation(self, isPortrait):
-               if isPortrait:
+       def update_orientation(self, orientation):
+               if orientation == self._app.DEFAULT_ORIENTATION:
+                       qui_utils.set_autorient(self.window, False)
+                       qui_utils.set_window_orientation(self.window, None)
+               elif orientation == self._app.AUTO_ORIENTATION:
+                       qui_utils.set_autorient(self.window, True)
+                       qui_utils.set_window_orientation(self.window, None)
+               elif orientation == self._app.LANDSCAPE_ORIENTATION:
+                       qui_utils.set_autorient(self.window, False)
+                       qui_utils.set_window_orientation(self.window, QtCore.Qt.Horizontal)
+               elif orientation == self._app.PORTRAIT_ORIENTATION:
+                       qui_utils.set_autorient(self.window, False)
                        qui_utils.set_window_orientation(self.window, QtCore.Qt.Vertical)
                else:
-                       qui_utils.set_window_orientation(self.window, QtCore.Qt.Horizontal)
+                       raise RuntimeError("Unknown orientation: %r" % orientation)
                for child in self.walk_children():
-                       child.set_orientation(isPortrait)
+                       child.update_orientation(orientation)
 
        @misc_utils.log_exception(_moduleLogger)
-       def _on_child_close(self):
+       def _on_child_close(self, obj = None):
                raise NotImplementedError("Booh")
 
        @misc_utils.log_exception(_moduleLogger)
index 4a47513..0518480 100644 (file)
@@ -473,6 +473,9 @@ class Py2deb(object):
 
         self.__files[path]=nfiles
 
+    def __getitem__(self, k):
+        return self.__files[k]
+
     def __delitem__(self, k):
         del self.__files[k]