Pulling in skeleton code
authorEd Page <eopage@byu.net>
Tue, 11 Jan 2011 04:17:00 +0000 (22:17 -0600)
committerEd Page <eopage@byu.net>
Wed, 12 Jan 2011 00:45:59 +0000 (18:45 -0600)
src/constants.py
src/ejpi_qt.py
src/util/concurrent.py
src/util/go_utils.py
src/util/misc.py
src/util/qore_utils.py [new file with mode: 0644]
src/util/qtpie.py
src/util/qtpieboard.py
src/util/qui_utils.py [new file with mode: 0644]
src/util/qwrappers.py [new file with mode: 0644]
src/util/tp_utils.py

index 8b1e506..d89c394 100644 (file)
@@ -8,3 +8,4 @@ __app_magic__ = 0xdeadbeef
 _data_path_ = os.path.join(os.path.expanduser("~"), ".%s" % __app_name__)
 _user_settings_ = "%s/settings.ini" % _data_path_
 _user_logpath_ = "%s/%s.log" % (_data_path_, __app_name__)
+IS_MAEMO = True
index a1d92f4..7ef9d78 100755 (executable)
@@ -26,9 +26,6 @@ import qhistory
 _moduleLogger = logging.getLogger(__name__)
 
 
-IS_MAEMO = True
-
-
 class Calculator(object):
 
        def __init__(self, app):
@@ -37,6 +34,7 @@ class Calculator(object):
                self._hiddenCategories = set()
                self._hiddenUnits = {}
                self._clipboard = QtGui.QApplication.clipboard()
+
                self._mainWindow = None
 
                self._fullscreenAction = QtGui.QAction(None)
@@ -277,7 +275,7 @@ class MainWindow(object):
 
                self._window = QtGui.QMainWindow(parent)
                self._window.setAttribute(QtCore.Qt.WA_DeleteOnClose, True)
-               #maeqt.set_autorient(self._window, True)
+               maeqt.set_autorient(self._window, True)
                maeqt.set_stackable(self._window, True)
                self._window.setWindowTitle("%s" % constants.__pretty_app_name__)
                self._window.setCentralWidget(centralWidget)
index 503a1b4..a6499fe 100644 (file)
@@ -7,6 +7,76 @@ import errno
 import time
 import functools
 import contextlib
+import logging
+
+import misc
+
+
+_moduleLogger = logging.getLogger(__name__)
+
+
+class AsyncLinearExecution(object):
+
+       def __init__(self, pool, func):
+               self._pool = pool
+               self._func = func
+               self._run = None
+
+       def start(self, *args, **kwds):
+               assert self._run is None, "Task already started"
+               self._run = self._func(*args, **kwds)
+               trampoline, args, kwds = self._run.send(None) # priming the function
+               self._pool.add_task(
+                       trampoline,
+                       args,
+                       kwds,
+                       self.on_success,
+                       self.on_error,
+               )
+
+       @misc.log_exception(_moduleLogger)
+       def on_success(self, result):
+               _moduleLogger.debug("Processing success for: %r", self._func)
+               try:
+                       trampoline, args, kwds = self._run.send(result)
+               except StopIteration, e:
+                       pass
+               else:
+                       self._pool.add_task(
+                               trampoline,
+                               args,
+                               kwds,
+                               self.on_success,
+                               self.on_error,
+                       )
+
+       @misc.log_exception(_moduleLogger)
+       def on_error(self, error):
+               _moduleLogger.debug("Processing error for: %r", self._func)
+               try:
+                       trampoline, args, kwds = self._run.throw(error)
+               except StopIteration, e:
+                       pass
+               else:
+                       self._pool.add_task(
+                               trampoline,
+                               args,
+                               kwds,
+                               self.on_success,
+                               self.on_error,
+                       )
+
+       def __repr__(self):
+               return "<async %s at 0x%x>" % (self._func.__name__, id(self))
+
+       def __hash__(self):
+               return hash(self._func)
+
+       def __eq__(self, other):
+               return self._func == other._func
+
+       def __ne__(self, other):
+               return self._func != other._func
 
 
 def synchronized(lock):
index 97d671c..eaa2fe1 100644 (file)
@@ -191,7 +191,7 @@ class AsyncPool(object):
                                result = func(*args, **kwds)
                                isError = False
                        except Exception, e:
-                               _moduleLogger.exception("Error, passing it back to the main thread")
+                               _moduleLogger.error("Error, passing it back to the main thread")
                                result = e
                                isError = True
                        self.__workQueue.task_done()
@@ -200,58 +200,6 @@ class AsyncPool(object):
                _moduleLogger.debug("Shutting down worker thread")
 
 
-class AsyncLinearExecution(object):
-
-       def __init__(self, pool, func):
-               self._pool = pool
-               self._func = func
-               self._run = None
-
-       def start(self, *args, **kwds):
-               assert self._run is None
-               self._run = self._func(*args, **kwds)
-               trampoline, args, kwds = self._run.send(None) # priming the function
-               self._pool.add_task(
-                       trampoline,
-                       args,
-                       kwds,
-                       self.on_success,
-                       self.on_error,
-               )
-
-       @misc.log_exception(_moduleLogger)
-       def on_success(self, result):
-               #_moduleLogger.debug("Processing success for: %r", self._func)
-               try:
-                       trampoline, args, kwds = self._run.send(result)
-               except StopIteration, e:
-                       pass
-               else:
-                       self._pool.add_task(
-                               trampoline,
-                               args,
-                               kwds,
-                               self.on_success,
-                               self.on_error,
-                       )
-
-       @misc.log_exception(_moduleLogger)
-       def on_error(self, error):
-               #_moduleLogger.debug("Processing error for: %r", self._func)
-               try:
-                       trampoline, args, kwds = self._run.throw(error)
-               except StopIteration, e:
-                       pass
-               else:
-                       self._pool.add_task(
-                               trampoline,
-                               args,
-                               kwds,
-                               self.on_success,
-                               self.on_error,
-                       )
-
-
 class AutoSignal(object):
 
        def __init__(self, toplevel):
index cf5c22a..c0a70a9 100644 (file)
@@ -16,6 +16,11 @@ import warnings
 import string
 
 
+class AnyData(object):
+
+       pass
+
+
 _indentationLevel = [0]
 
 
@@ -697,16 +702,14 @@ def normalize_number(prettynumber):
        uglynumber = re.sub('[^0-9+]', '', prettynumber)
        if uglynumber.startswith("+"):
                pass
-       elif uglynumber.startswith("1") and len(uglynumber) == 11:
+       elif uglynumber.startswith("1"):
                uglynumber = "+"+uglynumber
-       elif len(uglynumber) == 10:
+       elif 10 <= len(uglynumber):
+               assert uglynumber[0] not in ("+", "1"), "Number format confusing"
                uglynumber = "+1"+uglynumber
        else:
                pass
 
-       #validateRe = re.compile("^\+?[0-9]{10,}$")
-       #assert validateRe.match(uglynumber) is not None
-
        return uglynumber
 
 
@@ -720,6 +723,117 @@ def is_valid_number(number):
        return _VALIDATE_RE.match(number) is not None
 
 
+def make_ugly(prettynumber):
+       """
+       function to take a phone number and strip out all non-numeric
+       characters
+
+       >>> make_ugly("+012-(345)-678-90")
+       '+01234567890'
+       """
+       return normalize_number(prettynumber)
+
+
+def _make_pretty_with_areacode(phonenumber):
+       prettynumber = "(%s)" % (phonenumber[0:3], )
+       if 3 < len(phonenumber):
+               prettynumber += " %s" % (phonenumber[3:6], )
+               if 6 < len(phonenumber):
+                       prettynumber += "-%s" % (phonenumber[6:], )
+       return prettynumber
+
+
+def _make_pretty_local(phonenumber):
+       prettynumber = "%s" % (phonenumber[0:3], )
+       if 3 < len(phonenumber):
+               prettynumber += "-%s" % (phonenumber[3:], )
+       return prettynumber
+
+
+def _make_pretty_international(phonenumber):
+       prettynumber = phonenumber
+       if phonenumber.startswith("1"):
+               prettynumber = "1 "
+               prettynumber += _make_pretty_with_areacode(phonenumber[1:])
+       return prettynumber
+
+
+def make_pretty(phonenumber):
+       """
+       Function to take a phone number and return the pretty version
+       pretty numbers:
+               if phonenumber begins with 0:
+                       ...-(...)-...-....
+               if phonenumber begins with 1: ( for gizmo callback numbers )
+                       1 (...)-...-....
+               if phonenumber is 13 digits:
+                       (...)-...-....
+               if phonenumber is 10 digits:
+                       ...-....
+       >>> make_pretty("12")
+       '12'
+       >>> make_pretty("1234567")
+       '123-4567'
+       >>> make_pretty("2345678901")
+       '+1 (234) 567-8901'
+       >>> make_pretty("12345678901")
+       '+1 (234) 567-8901'
+       >>> make_pretty("01234567890")
+       '+012 (345) 678-90'
+       >>> make_pretty("+01234567890")
+       '+012 (345) 678-90'
+       >>> make_pretty("+12")
+       '+1 (2)'
+       >>> make_pretty("+123")
+       '+1 (23)'
+       >>> make_pretty("+1234")
+       '+1 (234)'
+       """
+       if phonenumber is None or phonenumber == "":
+               return ""
+
+       phonenumber = normalize_number(phonenumber)
+
+       if phonenumber == "":
+               return ""
+       elif phonenumber[0] == "+":
+               prettynumber = _make_pretty_international(phonenumber[1:])
+               if not prettynumber.startswith("+"):
+                       prettynumber = "+"+prettynumber
+       elif 8 < len(phonenumber) and phonenumber[0] in ("1", ):
+               prettynumber = _make_pretty_international(phonenumber)
+       elif 7 < len(phonenumber):
+               prettynumber = _make_pretty_with_areacode(phonenumber)
+       elif 3 < len(phonenumber):
+               prettynumber = _make_pretty_local(phonenumber)
+       else:
+               prettynumber = phonenumber
+       return prettynumber.strip()
+
+
+def similar_ugly_numbers(lhs, rhs):
+       return (
+               lhs == rhs or
+               lhs[1:] == rhs and lhs.startswith("1") or
+               lhs[2:] == rhs and lhs.startswith("+1") or
+               lhs == rhs[1:] and rhs.startswith("1") or
+               lhs == rhs[2:] and rhs.startswith("+1")
+       )
+
+
+def abbrev_relative_date(date):
+       """
+       >>> abbrev_relative_date("42 hours ago")
+       '42 h'
+       >>> abbrev_relative_date("2 days ago")
+       '2 d'
+       >>> abbrev_relative_date("4 weeks ago")
+       '4 w'
+       """
+       parts = date.split(" ")
+       return "%s %s" % (parts[0], parts[1][0])
+
+
 def parse_version(versionText):
        """
        >>> parse_version("0.5.2")
diff --git a/src/util/qore_utils.py b/src/util/qore_utils.py
new file mode 100644 (file)
index 0000000..491c96d
--- /dev/null
@@ -0,0 +1,107 @@
+import logging
+
+from PyQt4 import QtCore
+
+import misc
+
+
+_moduleLogger = logging.getLogger(__name__)
+
+
+class QThread44(QtCore.QThread):
+       """
+       This is to imitate QThread in Qt 4.4+ for when running on older version
+       See http://labs.trolltech.com/blogs/2010/06/17/youre-doing-it-wrong
+       (On Lucid I have Qt 4.7 and this is still an issue)
+       """
+
+       def __init__(self, parent = None):
+               QtCore.QThread.__init__(self, parent)
+
+       def run(self):
+               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)
+
+       def __init__(self, pool):
+               QtCore.QObject.__init__(self)
+               self._pool = pool
+
+       @QtCore.pyqtSlot(object)
+       @misc.log_exception(_moduleLogger)
+       def _on_task_added(self, task):
+               if not self._pool._isRunning:
+                       _moduleLogger.error("Dropping task")
+
+               func, args, kwds, on_success, on_error = task
+
+               try:
+                       result = func(*args, **kwds)
+                       isError = False
+               except Exception, e:
+                       _moduleLogger.error("Error, passing it back to the main thread")
+                       result = e
+                       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()
+
+
+class AsyncPool(QtCore.QObject):
+
+       _addTask = QtCore.pyqtSignal(object)
+       _stopPool = QtCore.pyqtSignal()
+
+       def __init__(self):
+               QtCore.QObject.__init__(self)
+               self._thread = QThread44()
+               self._isRunning = True
+               self._parent = _ParentThread(self)
+               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()
+
+       def stop(self):
+               self._isRunning = False
+               self._stopPool.emit()
+
+       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)
index 005050f..f840ed2 100755 (executable)
@@ -25,45 +25,6 @@ _moduleLogger = logging.getLogger(__name__)
 _TWOPI = 2 * math.pi
 
 
-class EIGHT_SLICE_PIE(object):
-
-       SLICE_CENTER = -1
-       SLICE_NORTH = 0
-       SLICE_NORTH_WEST = 1
-       SLICE_WEST = 2
-       SLICE_SOUTH_WEST = 3
-       SLICE_SOUTH = 4
-       SLICE_SOUTH_EAST = 5
-       SLICE_EAST = 6
-       SLICE_NORTH_EAST = 7
-
-       MAX_ANGULAR_SLICES = 8
-
-       SLICE_DIRECTIONS = [
-               SLICE_CENTER,
-               SLICE_NORTH,
-               SLICE_NORTH_WEST,
-               SLICE_WEST,
-               SLICE_SOUTH_WEST,
-               SLICE_SOUTH,
-               SLICE_SOUTH_EAST,
-               SLICE_EAST,
-               SLICE_NORTH_EAST,
-       ]
-
-       SLICE_DIRECTION_NAMES = [
-               "CENTER",
-               "NORTH",
-               "NORTH_WEST",
-               "WEST",
-               "SOUTH_WEST",
-               "SOUTH",
-               "SOUTH_EAST",
-               "EAST",
-               "NORTH_EAST",
-       ]
-
-
 def _radius_at(center, pos):
        delta = pos - center
        xDelta = delta.x()
@@ -541,7 +502,7 @@ class QPieButton(QtGui.QWidget):
        BUTTON_RADIUS = 24
        DELAY = 250
 
-       def __init__(self, buttonSlice, parent = None):
+       def __init__(self, buttonSlice, parent = None, buttonSlices = None):
                # @bug Artifacts on Maemo 5 due to window 3D effects, find way to disable them for just these?
                # @bug The pie's are being pushed back on screen on Maemo, leading to coordinate issues
                QtGui.QWidget.__init__(self, parent)
@@ -553,6 +514,9 @@ class QPieButton(QtGui.QWidget):
 
                self._buttonFiling = PieFiling()
                self._buttonFiling.set_center(buttonSlice)
+               if buttonSlices is not None:
+                       for slice in buttonSlices:
+                               self._buttonFiling.insertItem(slice)
                self._buttonFiling.setOuterRadius(self.BUTTON_RADIUS)
                self._buttonArtist = PieArtist(self._buttonFiling)
                self._poppedUp = False
@@ -565,6 +529,12 @@ class QPieButton(QtGui.QWidget):
 
                self._mousePosition = None
                self.setFocusPolicy(QtCore.Qt.StrongFocus)
+               self.setSizePolicy(
+                       QtGui.QSizePolicy(
+                               QtGui.QSizePolicy.MinimumExpanding,
+                               QtGui.QSizePolicy.MinimumExpanding,
+                       )
+               )
 
        def insertItem(self, item, index = -1):
                self._filing.insertItem(item, index)
@@ -604,8 +574,12 @@ class QPieButton(QtGui.QWidget):
 
        def setButtonRadius(self, radius):
                self._buttonFiling.setOuterRadius(radius)
+               self._buttonFiling.setInnerRadius(radius / 2)
                self._buttonArtist.show(self.palette())
 
+       def sizeHint(self):
+               return self._buttonArtist.pieSize()
+
        def minimumSizeHint(self):
                return self._buttonArtist.centerSize()
 
@@ -625,7 +599,7 @@ class QPieButton(QtGui.QWidget):
 
        @misc_utils.log_exception(_moduleLogger)
        def _on_delayed_popup(self):
-               assert self._popupLocation is not None
+               assert self._popupLocation is not None, "Widget location abuse"
                self._popup_child(self._popupLocation)
 
        @misc_utils.log_exception(_moduleLogger)
index 7d79c60..c7094f4 100755 (executable)
@@ -7,28 +7,47 @@ import os
 import warnings
 
 from PyQt4 import QtGui
-from PyQt4 import QtCore
 
 import qtpie
 
 
 class PieKeyboard(object):
 
-       SLICE_CENTER = qtpie.EIGHT_SLICE_PIE.SLICE_CENTER
-       SLICE_NORTH = qtpie.EIGHT_SLICE_PIE.SLICE_NORTH
-       SLICE_NORTH_WEST = qtpie.EIGHT_SLICE_PIE.SLICE_NORTH_WEST
-       SLICE_WEST = qtpie.EIGHT_SLICE_PIE.SLICE_WEST
-       SLICE_SOUTH_WEST = qtpie.EIGHT_SLICE_PIE.SLICE_SOUTH_WEST
-       SLICE_SOUTH = qtpie.EIGHT_SLICE_PIE.SLICE_SOUTH
-       SLICE_SOUTH_EAST = qtpie.EIGHT_SLICE_PIE.SLICE_SOUTH_EAST
-       SLICE_EAST = qtpie.EIGHT_SLICE_PIE.SLICE_EAST
-       SLICE_NORTH_EAST = qtpie.EIGHT_SLICE_PIE.SLICE_NORTH_EAST
-
-       MAX_ANGULAR_SLICES = qtpie.EIGHT_SLICE_PIE.MAX_ANGULAR_SLICES
-
-       SLICE_DIRECTIONS = qtpie.EIGHT_SLICE_PIE.SLICE_DIRECTIONS
-
-       SLICE_DIRECTION_NAMES = qtpie.EIGHT_SLICE_PIE.SLICE_DIRECTION_NAMES
+       SLICE_CENTER = -1
+       SLICE_NORTH = 0
+       SLICE_NORTH_WEST = 1
+       SLICE_WEST = 2
+       SLICE_SOUTH_WEST = 3
+       SLICE_SOUTH = 4
+       SLICE_SOUTH_EAST = 5
+       SLICE_EAST = 6
+       SLICE_NORTH_EAST = 7
+
+       MAX_ANGULAR_SLICES = 8
+
+       SLICE_DIRECTIONS = [
+               SLICE_CENTER,
+               SLICE_NORTH,
+               SLICE_NORTH_WEST,
+               SLICE_WEST,
+               SLICE_SOUTH_WEST,
+               SLICE_SOUTH,
+               SLICE_SOUTH_EAST,
+               SLICE_EAST,
+               SLICE_NORTH_EAST,
+       ]
+
+       SLICE_DIRECTION_NAMES = [
+               "CENTER",
+               "NORTH",
+               "NORTH_WEST",
+               "WEST",
+               "SOUTH_WEST",
+               "SOUTH",
+               "SOUTH_EAST",
+               "EAST",
+               "NORTH_EAST",
+       ]
 
        def __init__(self):
                self._layout = QtGui.QGridLayout()
@@ -43,7 +62,7 @@ class PieKeyboard(object):
 
        def add_pie(self, row, column, pieButton):
                assert len(pieButton) == 8
-               self._layout.addWidget(pieButton, row, column, QtCore.Qt.AlignCenter)
+               self._layout.addWidget(pieButton, row, column)
                self.__cells[(row, column)] = pieButton
 
        def get_pie(self, row, column):
diff --git a/src/util/qui_utils.py b/src/util/qui_utils.py
new file mode 100644 (file)
index 0000000..56a3408
--- /dev/null
@@ -0,0 +1,364 @@
+import sys
+import contextlib
+import datetime
+import logging
+
+from PyQt4 import QtCore
+from PyQt4 import QtGui
+
+import misc
+
+
+_moduleLogger = logging.getLogger(__name__)
+
+
+@contextlib.contextmanager
+def notify_error(log):
+       try:
+               yield
+       except:
+               log.push_exception()
+
+
+@contextlib.contextmanager
+def notify_busy(log, message):
+       log.push_busy(message)
+       try:
+               yield
+       finally:
+               log.pop(message)
+
+
+class ErrorMessage(object):
+
+       LEVEL_ERROR = 0
+       LEVEL_BUSY = 1
+       LEVEL_INFO = 2
+
+       def __init__(self, message, level):
+               self._message = message
+               self._level = level
+               self._time = datetime.datetime.now()
+
+       @property
+       def level(self):
+               return self._level
+
+       @property
+       def message(self):
+               return self._message
+
+       def __repr__(self):
+               return "%s.%s(%r, %r)" % (__name__, self.__class__.__name__, self._message, self._level)
+
+
+class QErrorLog(QtCore.QObject):
+
+       messagePushed = QtCore.pyqtSignal()
+       messagePopped = QtCore.pyqtSignal()
+
+       def __init__(self):
+               QtCore.QObject.__init__(self)
+               self._messages = []
+
+       def push_busy(self, message):
+               _moduleLogger.info("Entering state: %s" % message)
+               self._push_message(message, ErrorMessage.LEVEL_BUSY)
+
+       def push_message(self, message):
+               self._push_message(message, ErrorMessage.LEVEL_INFO)
+
+       def push_error(self, message):
+               self._push_message(message, ErrorMessage.LEVEL_ERROR)
+
+       def push_exception(self):
+               userMessage = str(sys.exc_info()[1])
+               _moduleLogger.exception(userMessage)
+               self.push_error(userMessage)
+
+       def pop(self, message = None):
+               if message is None:
+                       del self._messages[0]
+               else:
+                       _moduleLogger.info("Exiting state: %s" % message)
+                       messageIndex = [
+                               i
+                               for (i, error) in enumerate(self._messages)
+                               if error.message == message
+                       ]
+                       # Might be removed out of order
+                       if messageIndex:
+                               del self._messages[messageIndex[0]]
+               self.messagePopped.emit()
+
+       def peek_message(self):
+               return self._messages[0]
+
+       def _push_message(self, message, level):
+               self._messages.append(ErrorMessage(message, level))
+               # Sort is defined as stable, so this should be fine
+               self._messages.sort(key=lambda x: x.level)
+               self.messagePushed.emit()
+
+       def __len__(self):
+               return len(self._messages)
+
+
+class ErrorDisplay(object):
+
+       _SENTINEL_ICON = QtGui.QIcon()
+
+       def __init__(self, errorLog):
+               self._errorLog = errorLog
+               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._severityLabel = QtGui.QLabel()
+               self._severityLabel.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter)
+
+               self._message = QtGui.QLabel()
+               self._message.setText("Boo")
+               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._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)
+               self._widget.hide()
+
+       @property
+       def toplevel(self):
+               return self._widget
+
+       def _show_error(self):
+               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)
+       @misc.log_exception(_moduleLogger)
+       def _on_close(self, checked = False):
+               self._errorLog.pop()
+
+       @QtCore.pyqtSlot()
+       @misc.log_exception(_moduleLogger)
+       def _on_message_pushed(self):
+               self._show_error()
+
+       @QtCore.pyqtSlot()
+       @misc.log_exception(_moduleLogger)
+       def _on_message_popped(self):
+               if len(self._errorLog) == 0:
+                       self._message.setText("")
+                       self._widget.hide()
+               else:
+                       self._show_error()
+
+
+class QHtmlDelegate(QtGui.QStyledItemDelegate):
+
+       UNDEFINED_SIZE = -1
+
+       def __init__(self, *args, **kwd):
+               QtGui.QStyledItemDelegate.__init__(*((self, ) + args), **kwd)
+               self._width = self.UNDEFINED_SIZE
+
+       def paint(self, painter, option, index):
+               newOption = QtGui.QStyleOptionViewItemV4(option)
+               self.initStyleOption(newOption, index)
+               if newOption.widget is not None:
+                       style = newOption.widget.style()
+               else:
+                       style = QtGui.QApplication.style()
+
+               doc = QtGui.QTextDocument()
+               doc.setHtml(newOption.text)
+               doc.setTextWidth(newOption.rect.width())
+
+               newOption.text = ""
+               style.drawControl(QtGui.QStyle.CE_ItemViewItem, newOption, painter)
+
+               ctx = QtGui.QAbstractTextDocumentLayout.PaintContext()
+               if newOption.state & QtGui.QStyle.State_Selected:
+                       ctx.palette.setColor(
+                               QtGui.QPalette.Text,
+                               newOption.palette.color(
+                                       QtGui.QPalette.Active,
+                                       QtGui.QPalette.HighlightedText
+                               )
+                       )
+               else:
+                       ctx.palette.setColor(
+                               QtGui.QPalette.Text,
+                               newOption.palette.color(
+                                       QtGui.QPalette.Active,
+                                       QtGui.QPalette.Text
+                               )
+                       )
+
+               textRect = style.subElementRect(QtGui.QStyle.SE_ItemViewItemText, newOption)
+               painter.save()
+               painter.translate(textRect.topLeft())
+               painter.setClipRect(textRect.translated(-textRect.topLeft()))
+               doc.documentLayout().draw(painter, ctx)
+               painter.restore()
+
+       def setWidth(self, width):
+               # @bug we need to be emitting sizeHintChanged but it requires an index
+               self._width = width
+
+       def sizeHint(self, option, index):
+               newOption = QtGui.QStyleOptionViewItemV4(option)
+               self.initStyleOption(newOption, index)
+
+               doc = QtGui.QTextDocument()
+               doc.setHtml(newOption.text)
+               if self._width != self.UNDEFINED_SIZE:
+                       width = self._width
+               else:
+                       width = newOption.rect.width()
+               doc.setTextWidth(width)
+               size = QtCore.QSize(doc.idealWidth(), doc.size().height())
+               return size
+
+
+def _null_set_stackable(window, isStackable):
+       pass
+
+
+def _maemo_set_stackable(window, isStackable):
+       window.setAttribute(QtCore.Qt.WA_Maemo5StackedWindow, isStackable)
+
+
+try:
+       QtCore.Qt.WA_Maemo5StackedWindow
+       set_stackable = _maemo_set_stackable
+except AttributeError:
+       set_stackable = _null_set_stackable
+
+
+def _null_set_autorient(window, isStackable):
+       pass
+
+
+def _maemo_set_autorient(window, isStackable):
+       window.setAttribute(QtCore.Qt.WA_Maemo5AutoOrientation, isStackable)
+
+
+try:
+       QtCore.Qt.WA_Maemo5AutoOrientation
+       set_autorient = _maemo_set_autorient
+except AttributeError:
+       set_autorient = _null_set_autorient
+
+
+def screen_orientation():
+       geom = QtGui.QApplication.desktop().screenGeometry()
+       if geom.width() <= geom.height():
+               return QtCore.Qt.Vertical
+       else:
+               return QtCore.Qt.Horizontal
+
+
+def _null_set_window_orientation(window, orientation):
+       pass
+
+
+def _maemo_set_window_orientation(window, orientation):
+       if orientation == QtCore.Qt.Vertical:
+               oldHint = QtCore.Qt.WA_Maemo5LandscapeOrientation
+               newHint = QtCore.Qt.WA_Maemo5PortraitOrientation
+       elif orientation == QtCore.Qt.Horizontal:
+               oldHint = QtCore.Qt.WA_Maemo5PortraitOrientation
+               newHint = QtCore.Qt.WA_Maemo5LandscapeOrientation
+       window.setAttribute(oldHint, False)
+       window.setAttribute(newHint, True)
+
+
+try:
+       QtCore.Qt.WA_Maemo5LandscapeOrientation
+       QtCore.Qt.WA_Maemo5PortraitOrientation
+       set_window_orientation = _maemo_set_window_orientation
+except AttributeError:
+       set_window_orientation = _null_set_window_orientation
+
+
+def _null_show_progress_indicator(window, isStackable):
+       pass
+
+
+def _maemo_show_progress_indicator(window, isStackable):
+       window.setAttribute(QtCore.Qt.WA_Maemo5ShowProgressIndicator, isStackable)
+
+
+try:
+       QtCore.Qt.WA_Maemo5ShowProgressIndicator
+       show_progress_indicator = _maemo_show_progress_indicator
+except AttributeError:
+       show_progress_indicator = _null_show_progress_indicator
+
+
+def _null_mark_numbers_preferred(widget):
+       pass
+
+
+def _newqt_mark_numbers_preferred(widget):
+       widget.setInputMethodHints(QtCore.Qt.ImhPreferNumbers)
+
+
+try:
+       QtCore.Qt.ImhPreferNumbers
+       mark_numbers_preferred = _newqt_mark_numbers_preferred
+except AttributeError:
+       mark_numbers_preferred = _null_mark_numbers_preferred
+
+
+def _null_get_theme_icon(iconNames, fallback = None):
+       icon = fallback if fallback is not None else QtGui.QIcon()
+       return icon
+
+
+def _newqt_get_theme_icon(iconNames, fallback = None):
+       for iconName in iconNames:
+               if QtGui.QIcon.hasThemeIcon(iconName):
+                       icon = QtGui.QIcon.fromTheme(iconName)
+                       break
+       else:
+               icon = fallback if fallback is not None else QtGui.QIcon()
+       return icon
+
+
+try:
+       QtGui.QIcon.fromTheme
+       get_theme_icon = _newqt_get_theme_icon
+except AttributeError:
+       get_theme_icon = _null_get_theme_icon
+
diff --git a/src/util/qwrappers.py b/src/util/qwrappers.py
new file mode 100644 (file)
index 0000000..9527dc6
--- /dev/null
@@ -0,0 +1,230 @@
+#!/usr/bin/env python
+
+from __future__ import with_statement
+from __future__ import division
+
+import logging
+
+from PyQt4 import QtGui
+from PyQt4 import QtCore
+
+from util import qui_utils
+from util import misc as misc_utils
+
+
+_moduleLogger = logging.getLogger(__name__)
+
+
+class ApplicationWrapper(object):
+
+       def __init__(self, qapp, constants):
+               self._constants = constants
+               self._qapp = qapp
+               self._clipboard = QtGui.QApplication.clipboard()
+
+               self._errorLog = qui_utils.QErrorLog()
+               self._mainWindow = None
+
+               self._fullscreenAction = QtGui.QAction(None)
+               self._fullscreenAction.setText("Fullscreen")
+               self._fullscreenAction.setCheckable(True)
+               self._fullscreenAction.setShortcut(QtGui.QKeySequence("CTRL+Enter"))
+               self._fullscreenAction.toggled.connect(self._on_toggle_fullscreen)
+
+               self._orientationAction = QtGui.QAction(None)
+               self._orientationAction.setText("Orientation")
+               self._orientationAction.setCheckable(True)
+               self._orientationAction.setShortcut(QtGui.QKeySequence("CTRL+o"))
+               self._orientationAction.toggled.connect(self._on_toggle_orientation)
+
+               self._logAction = QtGui.QAction(None)
+               self._logAction.setText("Log")
+               self._logAction.setShortcut(QtGui.QKeySequence("CTRL+l"))
+               self._logAction.triggered.connect(self._on_log)
+
+               self._quitAction = QtGui.QAction(None)
+               self._quitAction.setText("Quit")
+               self._quitAction.setShortcut(QtGui.QKeySequence("CTRL+q"))
+               self._quitAction.triggered.connect(self._on_quit)
+
+               self._aboutAction = QtGui.QAction(None)
+               self._aboutAction.setText("About")
+               self._aboutAction.triggered.connect(self._on_about)
+
+               self._qapp.lastWindowClosed.connect(self._on_app_quit)
+               self._mainWindow = self._new_main_window()
+               self._mainWindow.window.destroyed.connect(self._on_child_close)
+
+               self.load_settings()
+
+               self._mainWindow.show()
+               self._idleDelay = QtCore.QTimer()
+               self._idleDelay.setSingleShot(True)
+               self._idleDelay.setInterval(0)
+               self._idleDelay.timeout.connect(lambda: self._mainWindow.start())
+               self._idleDelay.start()
+
+       def load_settings(self):
+               raise NotImplementedError("Booh")
+
+       def save_settings(self):
+               raise NotImplementedError("Booh")
+
+       def _new_main_window(self):
+               raise NotImplementedError("Booh")
+
+       @property
+       def constants(self):
+               return self._constants
+
+       @property
+       def errorLog(self):
+               return self._errorLog
+
+       @property
+       def fullscreenAction(self):
+               return self._fullscreenAction
+
+       @property
+       def orientationAction(self):
+               return self._orientationAction
+
+       @property
+       def logAction(self):
+               return self._logAction
+
+       @property
+       def aboutAction(self):
+               return self._aboutAction
+
+       @property
+       def quitAction(self):
+               return self._quitAction
+
+       def _close_windows(self):
+               if self._mainWindow is not None:
+                       self.save_settings()
+                       self._mainWindow.window.destroyed.disconnect(self._on_child_close)
+                       self._mainWindow.close()
+                       self._mainWindow = None
+
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_app_quit(self, checked = False):
+               if self._mainWindow is not None:
+                       self.save_settings()
+                       self._mainWindow.destroy()
+
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_child_close(self, obj = None):
+               if self._mainWindow is not None:
+                       self.save_settings()
+                       self._mainWindow = None
+
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_toggle_fullscreen(self, checked = False):
+               self._mainWindow.set_fullscreen(checked)
+
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_toggle_orientation(self, checked = False):
+               self._mainWindow.set_orientation(checked)
+
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_about(self, checked = True):
+               raise NotImplementedError("Booh")
+
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_log(self, checked = False):
+               with open(self._constants._user_logpath_, "r") as f:
+                       logLines = f.xreadlines()
+                       log = "".join(logLines)
+                       self._clipboard.setText(log)
+
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_quit(self, checked = False):
+               self._close_windows()
+
+
+class WindowWrapper(object):
+
+       def __init__(self, parent, app):
+               self._app = app
+
+               self._errorDisplay = qui_utils.ErrorDisplay(self._app.errorLog)
+
+               self._layout = QtGui.QBoxLayout(QtGui.QBoxLayout.LeftToRight)
+               self._layout.setContentsMargins(0, 0, 0, 0)
+               self._layout.addWidget(self._errorDisplay.toplevel)
+
+               centralWidget = QtGui.QWidget()
+               centralWidget.setLayout(self._layout)
+               centralWidget.setContentsMargins(0, 0, 0, 0)
+
+               self._window = QtGui.QMainWindow(parent)
+               self._window.setAttribute(QtCore.Qt.WA_DeleteOnClose, True)
+               qui_utils.set_stackable(self._window, True)
+               self._window.setCentralWidget(centralWidget)
+
+               self._closeWindowAction = QtGui.QAction(None)
+               self._closeWindowAction.setText("Close")
+               self._closeWindowAction.setShortcut(QtGui.QKeySequence("CTRL+w"))
+               self._closeWindowAction.triggered.connect(self._on_close_window)
+
+               self._window.addAction(self._closeWindowAction)
+               self._window.addAction(self._app.quitAction)
+               self._window.addAction(self._app.fullscreenAction)
+               self._window.addAction(self._app.orientationAction)
+               self._window.addAction(self._app.logAction)
+
+       @property
+       def window(self):
+               return self._window
+
+       def walk_children(self):
+               return ()
+
+       def start(self):
+               pass
+
+       def close(self):
+               for child in self.walk_children():
+                       child.window.destroyed.disconnect(self._on_child_close)
+                       child.close()
+               self._window.close()
+
+       def destroy(self):
+               pass
+
+       def show(self):
+               self.set_fullscreen(self._app.fullscreenAction.isChecked())
+               self._window.show()
+               for child in self.walk_children():
+                       child.show()
+
+       def hide(self):
+               for child in self.walk_children():
+                       child.hide()
+               self._window.hide()
+
+       def set_fullscreen(self, isFullscreen):
+               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:
+                       qui_utils.set_window_orientation(self.window, QtCore.Qt.Vertical)
+               else:
+                       qui_utils.set_window_orientation(self.window, QtCore.Qt.Horizontal)
+               for child in self.walk_children():
+                       child.set_orientation(isPortrait)
+
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_child_close(self):
+               raise NotImplementedError("Booh")
+
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_close_window(self, checked = True):
+               self.close()
index 30d7629..7c55c42 100644 (file)
@@ -61,13 +61,13 @@ class WasMissedCall(object):
                                self._report_error("closed too early")
 
        def _report_success(self):
-               assert not self._didReport
+               assert not self._didReport, "Double reporting a missed call"
                self._didReport = True
                self._onTimeout.cancel()
                self.__on_success(self)
 
        def _report_error(self, reason):
-               assert not self._didReport
+               assert not self._didReport, "Double reporting a missed call"
                self._didReport = True
                self._onTimeout.cancel()
                self.__on_error(self, reason)