Pulling in latest skeleton changes
authorEd Page <eopage@byu.net>
Wed, 29 Dec 2010 03:38:23 +0000 (21:38 -0600)
committerEd Page <eopage@byu.net>
Wed, 29 Dec 2010 13:40:55 +0000 (07:40 -0600)
src/maeqt.py
src/util/go_utils.py
src/util/io.py
src/util/linux.py
src/util/qtpie.py [new file with mode: 0755]
src/util/qtpieboard.py [new file with mode: 0755]
src/util/time_utils.py [new file with mode: 0644]
src/util/tp_utils.py

index d61c669..d5eb18b 100644 (file)
@@ -1,4 +1,5 @@
 from PyQt4 import QtCore
+from PyQt4 import QtGui
 
 
 def _null_set_stackable(window, isStackable):
@@ -89,3 +90,33 @@ try:
        mark_numbers_preferred = _newqt_mark_numbers_preferred
 except AttributeError:
        mark_numbers_preferred = _null_mark_numbers_preferred
+
+
+def screen_orientation():
+       geom = QtGui.QApplication.desktop().screenGeometry()
+       if geom.width() <= geom.height():
+               return QtCore.Qt.Vertical
+       else:
+               return QtCore.Qt.Horizontal
+
+
+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
index d066542..97d671c 100644 (file)
@@ -191,7 +191,7 @@ class AsyncPool(object):
                                result = func(*args, **kwds)
                                isError = False
                        except Exception, e:
-                               _moduleLogger.error("Error, passing it back to the main thread")
+                               _moduleLogger.exception("Error, passing it back to the main thread")
                                result = e
                                isError = True
                        self.__workQueue.task_done()
@@ -221,7 +221,7 @@ class AsyncLinearExecution(object):
 
        @misc.log_exception(_moduleLogger)
        def on_success(self, result):
-               _moduleLogger.debug("Processing success for: %r", self._func)
+               #_moduleLogger.debug("Processing success for: %r", self._func)
                try:
                        trampoline, args, kwds = self._run.send(result)
                except StopIteration, e:
@@ -237,7 +237,7 @@ class AsyncLinearExecution(object):
 
        @misc.log_exception(_moduleLogger)
        def on_error(self, error):
-               _moduleLogger.debug("Processing error for: %r", self._func)
+               #_moduleLogger.debug("Processing error for: %r", self._func)
                try:
                        trampoline, args, kwds = self._run.throw(error)
                except StopIteration, e:
index aece2dd..aac896d 100644 (file)
@@ -7,7 +7,12 @@ import os
 import pickle
 import contextlib
 import itertools
-import functools
+import codecs
+import csv
+try:
+       import cStringIO as StringIO
+except ImportError:
+       import StringIO
 
 
 @contextlib.contextmanager
@@ -127,3 +132,78 @@ def relpath(p1, p2):
                return os.path.join(*relParts)
        else:
                return "."+os.sep
+
+
+class UTF8Recoder(object):
+       """
+       Iterator that reads an encoded stream and reencodes the input to UTF-8
+       """
+       def __init__(self, f, encoding):
+               self.reader = codecs.getreader(encoding)(f)
+
+       def __iter__(self):
+               return self
+
+       def next(self):
+               return self.reader.next().encode("utf-8")
+
+
+class UnicodeReader(object):
+       """
+       A CSV reader which will iterate over lines in the CSV file "f",
+       which is encoded in the given encoding.
+       """
+
+       def __init__(self, f, dialect=csv.excel, encoding="utf-8", **kwds):
+               f = UTF8Recoder(f, encoding)
+               self.reader = csv.reader(f, dialect=dialect, **kwds)
+
+       def next(self):
+               row = self.reader.next()
+               return [unicode(s, "utf-8") for s in row]
+
+       def __iter__(self):
+               return self
+
+class UnicodeWriter(object):
+       """
+       A CSV writer which will write rows to CSV file "f",
+       which is encoded in the given encoding.
+       """
+
+       def __init__(self, f, dialect=csv.excel, encoding="utf-8", **kwds):
+               # Redirect output to a queue
+               self.queue = StringIO.StringIO()
+               self.writer = csv.writer(self.queue, dialect=dialect, **kwds)
+               self.stream = f
+               self.encoder = codecs.getincrementalencoder(encoding)()
+
+       def writerow(self, row):
+               self.writer.writerow([s.encode("utf-8") for s in row])
+               # Fetch UTF-8 output from the queue ...
+               data = self.queue.getvalue()
+               data = data.decode("utf-8")
+               # ... and reencode it into the target encoding
+               data = self.encoder.encode(data)
+               # write to the target stream
+               self.stream.write(data)
+               # empty queue
+               self.queue.truncate(0)
+
+       def writerows(self, rows):
+               for row in rows:
+                       self.writerow(row)
+
+
+def unicode_csv_reader(unicode_csv_data, dialect=csv.excel, **kwargs):
+       # csv.py doesn't do Unicode; encode temporarily as UTF-8:
+       csv_reader = csv.reader(utf_8_encoder(unicode_csv_data),
+                                                       dialect=dialect, **kwargs)
+       for row in csv_reader:
+               # decode UTF-8 back to Unicode, cell by cell:
+               yield [unicode(cell, 'utf-8') for cell in row]
+
+
+def utf_8_encoder(unicode_csv_data):
+       for line in unicode_csv_data:
+               yield line.encode('utf-8')
index 4837f2a..4e77445 100644 (file)
@@ -1,13 +1,79 @@
 #!/usr/bin/env python
 
 
+import os
 import logging
 
+try:
+       from xdg import BaseDirectory as _BaseDirectory
+       BaseDirectory = _BaseDirectory
+except ImportError:
+       BaseDirectory = None
+
+
+_moduleLogger = logging.getLogger(__name__)
+
+
+_libc = None
+
 
 def set_process_name(name):
        try: # change process name for killall
-               import ctypes
-               libc = ctypes.CDLL('libc.so.6')
-               libc.prctl(15, name, 0, 0, 0)
+               global _libc
+               if _libc is None:
+                       import ctypes
+                       _libc = ctypes.CDLL('libc.so.6')
+               _libc.prctl(15, name, 0, 0, 0)
        except Exception, e:
-               logging.warning('Unable to set processName: %s" % e')
+               _moduleLogger.warning('Unable to set processName: %s" % e')
+
+
+def get_new_resource(resourceType, resource, name):
+       if BaseDirectory is not None:
+               if resourceType == "data":
+                       base = BaseDirectory.xdg_data_home
+                       if base == "/usr/share/mime":
+                               # Ugly hack because somehow Maemo 4.1 seems to be set to this
+                               base = os.path.join(os.path.expanduser("~"), ".%s" % resource)
+               elif resourceType == "config":
+                       base = BaseDirectory.xdg_config_home
+               elif resourceType == "cache":
+                       base = BaseDirectory.xdg_cache_home
+               else:
+                       raise RuntimeError("Unknown type: "+resourceType)
+       else:
+               base = os.path.join(os.path.expanduser("~"), ".%s" % resource)
+
+       filePath = os.path.join(base, resource, name)
+       dirPath = os.path.dirname(filePath)
+       if not os.path.exists(dirPath):
+               # Looking before I leap to not mask errors
+               os.makedirs(dirPath)
+
+       return filePath
+
+
+def get_existing_resource(resourceType, resource, name):
+       if BaseDirectory is not None:
+               if resourceType == "data":
+                       base = BaseDirectory.xdg_data_home
+               elif resourceType == "config":
+                       base = BaseDirectory.xdg_config_home
+               elif resourceType == "cache":
+                       base = BaseDirectory.xdg_cache_home
+               else:
+                       raise RuntimeError("Unknown type: "+resourceType)
+       else:
+               base = None
+
+       if base is not None:
+               finalPath = os.path.join(base, name)
+               if os.path.exists(finalPath):
+                       return finalPath
+
+       altBase = os.path.join(os.path.expanduser("~"), ".%s" % resource)
+       finalPath = os.path.join(altBase, name)
+       if os.path.exists(finalPath):
+               return finalPath
+       else:
+               raise RuntimeError("Resource not found: %r" % ((resourceType, resource, name), ))
diff --git a/src/util/qtpie.py b/src/util/qtpie.py
new file mode 100755 (executable)
index 0000000..005050f
--- /dev/null
@@ -0,0 +1,1108 @@
+#!/usr/bin/env python
+
+import math
+import logging
+
+from PyQt4 import QtGui
+from PyQt4 import QtCore
+
+try:
+       from util import misc as misc_utils
+except ImportError:
+       class misc_utils(object):
+
+               @staticmethod
+               def log_exception(logger):
+
+                       def wrapper(func):
+                               return func
+                       return wrapper
+
+
+_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()
+       yDelta = delta.y()
+
+       radius = math.sqrt(xDelta ** 2 + yDelta ** 2)
+       return radius
+
+
+def _angle_at(center, pos):
+       delta = pos - center
+       xDelta = delta.x()
+       yDelta = delta.y()
+
+       radius = math.sqrt(xDelta ** 2 + yDelta ** 2)
+       angle = math.acos(xDelta / radius)
+       if 0 <= yDelta:
+               angle = _TWOPI - angle
+
+       return angle
+
+
+class QActionPieItem(object):
+
+       def __init__(self, action, weight = 1):
+               self._action = action
+               self._weight = weight
+
+       def action(self):
+               return self._action
+
+       def setWeight(self, weight):
+               self._weight = weight
+
+       def weight(self):
+               return self._weight
+
+       def setEnabled(self, enabled = True):
+               self._action.setEnabled(enabled)
+
+       def isEnabled(self):
+               return self._action.isEnabled()
+
+
+class PieFiling(object):
+
+       INNER_RADIUS_DEFAULT = 64
+       OUTER_RADIUS_DEFAULT = 192
+
+       SELECTION_CENTER = -1
+       SELECTION_NONE = -2
+
+       NULL_CENTER = QActionPieItem(QtGui.QAction(None))
+
+       def __init__(self):
+               self._innerRadius = self.INNER_RADIUS_DEFAULT
+               self._outerRadius = self.OUTER_RADIUS_DEFAULT
+               self._children = []
+               self._center = self.NULL_CENTER
+
+               self._cacheIndexToAngle = {}
+               self._cacheTotalWeight = 0
+
+       def insertItem(self, item, index = -1):
+               self._children.insert(index, item)
+               self._invalidate_cache()
+
+       def removeItemAt(self, index):
+               item = self._children.pop(index)
+               self._invalidate_cache()
+
+       def set_center(self, item):
+               if item is None:
+                       item = self.NULL_CENTER
+               self._center = item
+
+       def center(self):
+               return self._center
+
+       def clear(self):
+               del self._children[:]
+               self._center = self.NULL_CENTER
+               self._invalidate_cache()
+
+       def itemAt(self, index):
+               return self._children[index]
+
+       def indexAt(self, center, point):
+               return self._angle_to_index(_angle_at(center, point))
+
+       def innerRadius(self):
+               return self._innerRadius
+
+       def setInnerRadius(self, radius):
+               self._innerRadius = radius
+
+       def outerRadius(self):
+               return self._outerRadius
+
+       def setOuterRadius(self, radius):
+               self._outerRadius = radius
+
+       def __iter__(self):
+               return iter(self._children)
+
+       def __len__(self):
+               return len(self._children)
+
+       def __getitem__(self, index):
+               return self._children[index]
+
+       def _invalidate_cache(self):
+               self._cacheIndexToAngle.clear()
+               self._cacheTotalWeight = sum(child.weight() for child in self._children)
+               if self._cacheTotalWeight == 0:
+                       self._cacheTotalWeight = 1
+
+       def _index_to_angle(self, index, isShifted):
+               key = index, isShifted
+               if key in self._cacheIndexToAngle:
+                       return self._cacheIndexToAngle[key]
+               index = index % len(self._children)
+
+               baseAngle = _TWOPI / self._cacheTotalWeight
+
+               angle = math.pi / 2
+               if isShifted:
+                       if self._children:
+                               angle -= (self._children[0].weight() * baseAngle) / 2
+                       else:
+                               angle -= baseAngle / 2
+               while angle < 0:
+                       angle += _TWOPI
+
+               for i, child in enumerate(self._children):
+                       if index < i:
+                               break
+                       angle += child.weight() * baseAngle
+               while _TWOPI < angle:
+                       angle -= _TWOPI
+
+               self._cacheIndexToAngle[key] = angle
+               return angle
+
+       def _angle_to_index(self, angle):
+               numChildren = len(self._children)
+               if numChildren == 0:
+                       return self.SELECTION_CENTER
+
+               baseAngle = _TWOPI / self._cacheTotalWeight
+
+               iterAngle = math.pi / 2 - (self.itemAt(0).weight() * baseAngle) / 2
+               while iterAngle < 0:
+                       iterAngle += _TWOPI
+
+               oldIterAngle = iterAngle
+               for index, child in enumerate(self._children):
+                       iterAngle += child.weight() * baseAngle
+                       if oldIterAngle < angle and angle <= iterAngle:
+                               return index - 1 if index != 0 else numChildren - 1
+                       elif oldIterAngle < (angle + _TWOPI) and (angle + _TWOPI <= iterAngle):
+                               return index - 1 if index != 0 else numChildren - 1
+                       oldIterAngle = iterAngle
+
+
+class PieArtist(object):
+
+       ICON_SIZE_DEFAULT = 48
+
+       SHAPE_CIRCLE = "circle"
+       SHAPE_SQUARE = "square"
+       DEFAULT_SHAPE = SHAPE_SQUARE
+
+       def __init__(self, filing):
+               self._filing = filing
+
+               self._cachedOuterRadius = self._filing.outerRadius()
+               self._cachedInnerRadius = self._filing.innerRadius()
+               canvasSize = self._cachedOuterRadius * 2 + 1
+               self._canvas = QtGui.QPixmap(canvasSize, canvasSize)
+               self._mask = None
+               self.palette = None
+
+       def pieSize(self):
+               diameter = self._filing.outerRadius() * 2 + 1
+               return QtCore.QSize(diameter, diameter)
+
+       def centerSize(self):
+               painter = QtGui.QPainter(self._canvas)
+               text = self._filing.center().action().text()
+               fontMetrics = painter.fontMetrics()
+               if text:
+                       textBoundingRect = fontMetrics.boundingRect(text)
+               else:
+                       textBoundingRect = QtCore.QRect()
+               textWidth = textBoundingRect.width()
+               textHeight = textBoundingRect.height()
+
+               return QtCore.QSize(
+                       textWidth + self.ICON_SIZE_DEFAULT,
+                       max(textHeight, self.ICON_SIZE_DEFAULT),
+               )
+
+       def show(self, palette):
+               self.palette = palette
+
+               if (
+                       self._cachedOuterRadius != self._filing.outerRadius() or
+                       self._cachedInnerRadius != self._filing.innerRadius()
+               ):
+                       self._cachedOuterRadius = self._filing.outerRadius()
+                       self._cachedInnerRadius = self._filing.innerRadius()
+                       self._canvas = self._canvas.scaled(self.pieSize())
+
+               if self._mask is None:
+                       self._mask = QtGui.QBitmap(self._canvas.size())
+                       self._mask.fill(QtCore.Qt.color0)
+                       self._generate_mask(self._mask)
+                       self._canvas.setMask(self._mask)
+               return self._mask
+
+       def hide(self):
+               self.palette = None
+
+       def paint(self, selectionIndex):
+               painter = QtGui.QPainter(self._canvas)
+               painter.setRenderHint(QtGui.QPainter.Antialiasing, True)
+
+               adjustmentRect = self._canvas.rect().adjusted(0, 0, -1, -1)
+
+               numChildren = len(self._filing)
+               if numChildren == 0:
+                       if selectionIndex == PieFiling.SELECTION_CENTER and self._filing.center().isEnabled():
+                               painter.setBrush(self.palette.highlight())
+                       else:
+                               painter.setBrush(self.palette.window())
+                       painter.setPen(self.palette.mid().color())
+
+                       painter.drawRect(self._canvas.rect())
+                       self._paint_center_foreground(painter, selectionIndex)
+                       return self._canvas
+               elif numChildren == 1:
+                       if selectionIndex == 0 and self._filing[0].isEnabled():
+                               painter.setBrush(self.palette.highlight())
+                       else:
+                               painter.setBrush(self.palette.window())
+
+                       painter.fillRect(self._canvas.rect(), painter.brush())
+               else:
+                       for i in xrange(len(self._filing)):
+                               self._paint_slice_background(painter, adjustmentRect, i, selectionIndex)
+
+               self._paint_center_background(painter, adjustmentRect, selectionIndex)
+               self._paint_center_foreground(painter, selectionIndex)
+
+               for i in xrange(len(self._filing)):
+                       self._paint_slice_foreground(painter, i, selectionIndex)
+
+               return self._canvas
+
+       def _generate_mask(self, mask):
+               """
+               Specifies on the mask the shape of the pie menu
+               """
+               painter = QtGui.QPainter(mask)
+               painter.setPen(QtCore.Qt.color1)
+               painter.setBrush(QtCore.Qt.color1)
+               if self.DEFAULT_SHAPE == self.SHAPE_SQUARE:
+                       painter.drawRect(mask.rect())
+               elif self.DEFAULT_SHAPE == self.SHAPE_CIRCLE:
+                       painter.drawEllipse(mask.rect().adjusted(0, 0, -1, -1))
+               else:
+                       raise NotImplementedError(self.DEFAULT_SHAPE)
+
+       def _paint_slice_background(self, painter, adjustmentRect, i, selectionIndex):
+               if self.DEFAULT_SHAPE == self.SHAPE_SQUARE:
+                       currentWidth = adjustmentRect.width()
+                       newWidth = math.sqrt(2) * currentWidth
+                       dx = (newWidth - currentWidth) / 2
+                       adjustmentRect = adjustmentRect.adjusted(-dx, -dx, dx, dx)
+               elif self.DEFAULT_SHAPE == self.SHAPE_CIRCLE:
+                       pass
+               else:
+                       raise NotImplementedError(self.DEFAULT_SHAPE)
+
+               if i == selectionIndex and self._filing[i].isEnabled():
+                       painter.setBrush(self.palette.highlight())
+               else:
+                       painter.setBrush(self.palette.window())
+               painter.setPen(self.palette.mid().color())
+
+               a = self._filing._index_to_angle(i, True)
+               b = self._filing._index_to_angle(i + 1, True)
+               if b < a:
+                       b += _TWOPI
+               size = b - a
+               if size < 0:
+                       size += _TWOPI
+
+               startAngleInDeg = (a * 360 * 16) / _TWOPI
+               sizeInDeg = (size * 360 * 16) / _TWOPI
+               painter.drawPie(adjustmentRect, int(startAngleInDeg), int(sizeInDeg))
+
+       def _paint_slice_foreground(self, painter, i, selectionIndex):
+               child = self._filing[i]
+
+               a = self._filing._index_to_angle(i, True)
+               b = self._filing._index_to_angle(i + 1, True)
+               if b < a:
+                       b += _TWOPI
+               middleAngle = (a + b) / 2
+               averageRadius = (self._cachedInnerRadius + self._cachedOuterRadius) / 2
+
+               sliceX = averageRadius * math.cos(middleAngle)
+               sliceY = - averageRadius * math.sin(middleAngle)
+
+               piePos = self._canvas.rect().center()
+               pieX = piePos.x()
+               pieY = piePos.y()
+               self._paint_label(
+                       painter, child.action(), i == selectionIndex, pieX+sliceX, pieY+sliceY
+               )
+
+       def _paint_label(self, painter, action, isSelected, x, y):
+               text = action.text()
+               fontMetrics = painter.fontMetrics()
+               if text:
+                       textBoundingRect = fontMetrics.boundingRect(text)
+               else:
+                       textBoundingRect = QtCore.QRect()
+               textWidth = textBoundingRect.width()
+               textHeight = textBoundingRect.height()
+
+               icon = action.icon().pixmap(
+                       QtCore.QSize(self.ICON_SIZE_DEFAULT, self.ICON_SIZE_DEFAULT),
+                       QtGui.QIcon.Normal,
+                       QtGui.QIcon.On,
+               )
+               iconWidth = icon.width()
+               iconHeight = icon.width()
+               averageWidth = (iconWidth + textWidth)/2
+               if not icon.isNull():
+                       iconRect = QtCore.QRect(
+                               x - averageWidth,
+                               y - iconHeight/2,
+                               iconWidth,
+                               iconHeight,
+                       )
+
+                       painter.drawPixmap(iconRect, icon)
+
+               if text:
+                       if isSelected:
+                               if action.isEnabled():
+                                       pen = self.palette.highlightedText()
+                                       brush = self.palette.highlight()
+                               else:
+                                       pen = self.palette.mid()
+                                       brush = self.palette.window()
+                       else:
+                               if action.isEnabled():
+                                       pen = self.palette.windowText()
+                               else:
+                                       pen = self.palette.mid()
+                               brush = self.palette.window()
+
+                       leftX = x - averageWidth + iconWidth
+                       topY = y + textHeight/2
+                       painter.setPen(pen.color())
+                       painter.setBrush(brush)
+                       painter.drawText(leftX, topY, text)
+
+       def _paint_center_background(self, painter, adjustmentRect, selectionIndex):
+               dark = self.palette.mid().color()
+               light = self.palette.light().color()
+               if selectionIndex == PieFiling.SELECTION_CENTER and self._filing.center().isEnabled():
+                       background = self.palette.highlight().color()
+               else:
+                       background = self.palette.window().color()
+
+               innerRadius = self._cachedInnerRadius
+               adjustmentCenterPos = adjustmentRect.center()
+               innerRect = QtCore.QRect(
+                       adjustmentCenterPos.x() - innerRadius,
+                       adjustmentCenterPos.y() - innerRadius,
+                       innerRadius * 2 + 1,
+                       innerRadius * 2 + 1,
+               )
+
+               painter.setPen(QtCore.Qt.NoPen)
+               painter.setBrush(background)
+               painter.drawPie(innerRect, 0, 360 * 16)
+
+               painter.setPen(QtGui.QPen(dark, 1))
+               painter.setBrush(QtCore.Qt.NoBrush)
+               painter.drawEllipse(innerRect)
+
+               if self.DEFAULT_SHAPE == self.SHAPE_SQUARE:
+                       pass
+               elif self.DEFAULT_SHAPE == self.SHAPE_CIRCLE:
+                       painter.setPen(QtGui.QPen(dark, 1))
+                       painter.setBrush(QtCore.Qt.NoBrush)
+                       painter.drawEllipse(adjustmentRect)
+               else:
+                       raise NotImplementedError(self.DEFAULT_SHAPE)
+
+       def _paint_center_foreground(self, painter, selectionIndex):
+               centerPos = self._canvas.rect().center()
+               pieX = centerPos.x()
+               pieY = centerPos.y()
+
+               x = pieX
+               y = pieY
+
+               self._paint_label(
+                       painter,
+                       self._filing.center().action(),
+                       selectionIndex == PieFiling.SELECTION_CENTER,
+                       x, y
+               )
+
+
+class QPieDisplay(QtGui.QWidget):
+
+       def __init__(self, filing, parent = None, flags = QtCore.Qt.Window):
+               QtGui.QWidget.__init__(self, parent, flags)
+               self._filing = filing
+               self._artist = PieArtist(self._filing)
+               self._selectionIndex = PieFiling.SELECTION_NONE
+
+       def popup(self, pos):
+               self._update_selection(pos)
+               self.show()
+
+       def sizeHint(self):
+               return self._artist.pieSize()
+
+       @misc_utils.log_exception(_moduleLogger)
+       def showEvent(self, showEvent):
+               mask = self._artist.show(self.palette())
+               self.setMask(mask)
+
+               QtGui.QWidget.showEvent(self, showEvent)
+
+       @misc_utils.log_exception(_moduleLogger)
+       def hideEvent(self, hideEvent):
+               self._artist.hide()
+               self._selectionIndex = PieFiling.SELECTION_NONE
+               QtGui.QWidget.hideEvent(self, hideEvent)
+
+       @misc_utils.log_exception(_moduleLogger)
+       def paintEvent(self, paintEvent):
+               canvas = self._artist.paint(self._selectionIndex)
+
+               screen = QtGui.QPainter(self)
+               screen.drawPixmap(QtCore.QPoint(0, 0), canvas)
+
+               QtGui.QWidget.paintEvent(self, paintEvent)
+
+       def selectAt(self, index):
+               oldIndex = self._selectionIndex
+               self._selectionIndex = index
+               if self.isVisible():
+                       self.update()
+
+
+class QPieButton(QtGui.QWidget):
+
+       activated = QtCore.pyqtSignal(int)
+       highlighted = QtCore.pyqtSignal(int)
+       canceled = QtCore.pyqtSignal()
+       aboutToShow = QtCore.pyqtSignal()
+       aboutToHide = QtCore.pyqtSignal()
+
+       BUTTON_RADIUS = 24
+       DELAY = 250
+
+       def __init__(self, buttonSlice, parent = 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)
+               self._cachedCenterPosition = self.rect().center()
+
+               self._filing = PieFiling()
+               self._display = QPieDisplay(self._filing, None, QtCore.Qt.SplashScreen)
+               self._selectionIndex = PieFiling.SELECTION_NONE
+
+               self._buttonFiling = PieFiling()
+               self._buttonFiling.set_center(buttonSlice)
+               self._buttonFiling.setOuterRadius(self.BUTTON_RADIUS)
+               self._buttonArtist = PieArtist(self._buttonFiling)
+               self._poppedUp = False
+
+               self._delayPopupTimer = QtCore.QTimer()
+               self._delayPopupTimer.setInterval(self.DELAY)
+               self._delayPopupTimer.setSingleShot(True)
+               self._delayPopupTimer.timeout.connect(self._on_delayed_popup)
+               self._popupLocation = None
+
+               self._mousePosition = None
+               self.setFocusPolicy(QtCore.Qt.StrongFocus)
+
+       def insertItem(self, item, index = -1):
+               self._filing.insertItem(item, index)
+
+       def removeItemAt(self, index):
+               self._filing.removeItemAt(index)
+
+       def set_center(self, item):
+               self._filing.set_center(item)
+
+       def set_button(self, item):
+               self.update()
+
+       def clear(self):
+               self._filing.clear()
+
+       def itemAt(self, index):
+               return self._filing.itemAt(index)
+
+       def indexAt(self, point):
+               return self._filing.indexAt(self._cachedCenterPosition, point)
+
+       def innerRadius(self):
+               return self._filing.innerRadius()
+
+       def setInnerRadius(self, radius):
+               self._filing.setInnerRadius(radius)
+
+       def outerRadius(self):
+               return self._filing.outerRadius()
+
+       def setOuterRadius(self, radius):
+               self._filing.setOuterRadius(radius)
+
+       def buttonRadius(self):
+               return self._buttonFiling.outerRadius()
+
+       def setButtonRadius(self, radius):
+               self._buttonFiling.setOuterRadius(radius)
+               self._buttonArtist.show(self.palette())
+
+       def minimumSizeHint(self):
+               return self._buttonArtist.centerSize()
+
+       @misc_utils.log_exception(_moduleLogger)
+       def mousePressEvent(self, mouseEvent):
+               lastSelection = self._selectionIndex
+
+               lastMousePos = mouseEvent.pos()
+               self._mousePosition = lastMousePos
+               self._update_selection(self._cachedCenterPosition)
+
+               self.highlighted.emit(self._selectionIndex)
+
+               self._display.selectAt(self._selectionIndex)
+               self._popupLocation = mouseEvent.globalPos()
+               self._delayPopupTimer.start()
+
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_delayed_popup(self):
+               assert self._popupLocation is not None
+               self._popup_child(self._popupLocation)
+
+       @misc_utils.log_exception(_moduleLogger)
+       def mouseMoveEvent(self, mouseEvent):
+               lastSelection = self._selectionIndex
+
+               lastMousePos = mouseEvent.pos()
+               if self._mousePosition is None:
+                       # Absolute
+                       self._update_selection(lastMousePos)
+               else:
+                       # Relative
+                       self._update_selection(
+                               self._cachedCenterPosition + (lastMousePos - self._mousePosition),
+                               ignoreOuter = True,
+                       )
+
+               if lastSelection != self._selectionIndex:
+                       self.highlighted.emit(self._selectionIndex)
+                       self._display.selectAt(self._selectionIndex)
+
+               if self._selectionIndex != PieFiling.SELECTION_CENTER and self._delayPopupTimer.isActive():
+                       self._on_delayed_popup()
+
+       @misc_utils.log_exception(_moduleLogger)
+       def mouseReleaseEvent(self, mouseEvent):
+               self._delayPopupTimer.stop()
+               self._popupLocation = None
+
+               lastSelection = self._selectionIndex
+
+               lastMousePos = mouseEvent.pos()
+               if self._mousePosition is None:
+                       # Absolute
+                       self._update_selection(lastMousePos)
+               else:
+                       # Relative
+                       self._update_selection(
+                               self._cachedCenterPosition + (lastMousePos - self._mousePosition),
+                               ignoreOuter = True,
+                       )
+               self._mousePosition = None
+
+               self._activate_at(self._selectionIndex)
+               self._hide_child()
+
+       @misc_utils.log_exception(_moduleLogger)
+       def keyPressEvent(self, keyEvent):
+               if keyEvent.key() in [QtCore.Qt.Key_Right, QtCore.Qt.Key_Down, QtCore.Qt.Key_Tab]:
+                       self._popup_child(QtGui.QCursor.pos())
+                       if self._selectionIndex != len(self._filing) - 1:
+                               nextSelection = self._selectionIndex + 1
+                       else:
+                               nextSelection = 0
+                       self._select_at(nextSelection)
+                       self._display.selectAt(self._selectionIndex)
+               elif keyEvent.key() in [QtCore.Qt.Key_Left, QtCore.Qt.Key_Up, QtCore.Qt.Key_Backtab]:
+                       self._popup_child(QtGui.QCursor.pos())
+                       if 0 < self._selectionIndex:
+                               nextSelection = self._selectionIndex - 1
+                       else:
+                               nextSelection = len(self._filing) - 1
+                       self._select_at(nextSelection)
+                       self._display.selectAt(self._selectionIndex)
+               elif keyEvent.key() in [QtCore.Qt.Key_Space]:
+                       self._popup_child(QtGui.QCursor.pos())
+                       self._select_at(PieFiling.SELECTION_CENTER)
+                       self._display.selectAt(self._selectionIndex)
+               elif keyEvent.key() in [QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter, QtCore.Qt.Key_Space]:
+                       self._delayPopupTimer.stop()
+                       self._popupLocation = None
+                       self._activate_at(self._selectionIndex)
+                       self._hide_child()
+               elif keyEvent.key() in [QtCore.Qt.Key_Escape, QtCore.Qt.Key_Backspace]:
+                       self._delayPopupTimer.stop()
+                       self._popupLocation = None
+                       self._activate_at(PieFiling.SELECTION_NONE)
+                       self._hide_child()
+               else:
+                       QtGui.QWidget.keyPressEvent(self, keyEvent)
+
+       @misc_utils.log_exception(_moduleLogger)
+       def resizeEvent(self, resizeEvent):
+               self.setButtonRadius(min(resizeEvent.size().width(), resizeEvent.size().height()) / 2 - 1)
+               QtGui.QWidget.resizeEvent(self, resizeEvent)
+
+       @misc_utils.log_exception(_moduleLogger)
+       def showEvent(self, showEvent):
+               self._buttonArtist.show(self.palette())
+               self._cachedCenterPosition = self.rect().center()
+
+               QtGui.QWidget.showEvent(self, showEvent)
+
+       @misc_utils.log_exception(_moduleLogger)
+       def hideEvent(self, hideEvent):
+               self._display.hide()
+               self._select_at(PieFiling.SELECTION_NONE)
+               QtGui.QWidget.hideEvent(self, hideEvent)
+
+       @misc_utils.log_exception(_moduleLogger)
+       def paintEvent(self, paintEvent):
+               self.setButtonRadius(min(self.rect().width(), self.rect().height()) / 2 - 1)
+               if self._poppedUp:
+                       canvas = self._buttonArtist.paint(PieFiling.SELECTION_CENTER)
+               else:
+                       canvas = self._buttonArtist.paint(PieFiling.SELECTION_NONE)
+
+               screen = QtGui.QPainter(self)
+               screen.drawPixmap(QtCore.QPoint(0, 0), canvas)
+
+               QtGui.QWidget.paintEvent(self, paintEvent)
+
+       def __iter__(self):
+               return iter(self._filing)
+
+       def __len__(self):
+               return len(self._filing)
+
+       def _popup_child(self, position):
+               self._poppedUp = True
+               self.aboutToShow.emit()
+
+               self._delayPopupTimer.stop()
+               self._popupLocation = None
+
+               position = position - QtCore.QPoint(self._filing.outerRadius(), self._filing.outerRadius())
+               self._display.move(position)
+               self._display.show()
+
+               self.update()
+
+       def _hide_child(self):
+               self._poppedUp = False
+               self.aboutToHide.emit()
+               self._display.hide()
+               self.update()
+
+       def _select_at(self, index):
+               self._selectionIndex = index
+
+       def _update_selection(self, lastMousePos, ignoreOuter = False):
+               radius = _radius_at(self._cachedCenterPosition, lastMousePos)
+               if radius < self._filing.innerRadius():
+                       self._select_at(PieFiling.SELECTION_CENTER)
+               elif radius <= self._filing.outerRadius() or ignoreOuter:
+                       self._select_at(self.indexAt(lastMousePos))
+               else:
+                       self._select_at(PieFiling.SELECTION_NONE)
+
+       def _activate_at(self, index):
+               if index == PieFiling.SELECTION_NONE:
+                       self.canceled.emit()
+                       return
+               elif index == PieFiling.SELECTION_CENTER:
+                       child = self._filing.center()
+               else:
+                       child = self.itemAt(index)
+
+               if child.action().isEnabled():
+                       child.action().trigger()
+                       self.activated.emit(index)
+               else:
+                       self.canceled.emit()
+
+
+class QPieMenu(QtGui.QWidget):
+
+       activated = QtCore.pyqtSignal(int)
+       highlighted = QtCore.pyqtSignal(int)
+       canceled = QtCore.pyqtSignal()
+       aboutToShow = QtCore.pyqtSignal()
+       aboutToHide = QtCore.pyqtSignal()
+
+       def __init__(self, parent = None):
+               QtGui.QWidget.__init__(self, parent)
+               self._cachedCenterPosition = self.rect().center()
+
+               self._filing = PieFiling()
+               self._artist = PieArtist(self._filing)
+               self._selectionIndex = PieFiling.SELECTION_NONE
+
+               self._mousePosition = ()
+               self.setFocusPolicy(QtCore.Qt.StrongFocus)
+
+       def popup(self, pos):
+               self._update_selection(pos)
+               self.show()
+
+       def insertItem(self, item, index = -1):
+               self._filing.insertItem(item, index)
+               self.update()
+
+       def removeItemAt(self, index):
+               self._filing.removeItemAt(index)
+               self.update()
+
+       def set_center(self, item):
+               self._filing.set_center(item)
+               self.update()
+
+       def clear(self):
+               self._filing.clear()
+               self.update()
+
+       def itemAt(self, index):
+               return self._filing.itemAt(index)
+
+       def indexAt(self, point):
+               return self._filing.indexAt(self._cachedCenterPosition, point)
+
+       def innerRadius(self):
+               return self._filing.innerRadius()
+
+       def setInnerRadius(self, radius):
+               self._filing.setInnerRadius(radius)
+               self.update()
+
+       def outerRadius(self):
+               return self._filing.outerRadius()
+
+       def setOuterRadius(self, radius):
+               self._filing.setOuterRadius(radius)
+               self.update()
+
+       def sizeHint(self):
+               return self._artist.pieSize()
+
+       @misc_utils.log_exception(_moduleLogger)
+       def mousePressEvent(self, mouseEvent):
+               lastSelection = self._selectionIndex
+
+               lastMousePos = mouseEvent.pos()
+               self._update_selection(lastMousePos)
+               self._mousePosition = lastMousePos
+
+               if lastSelection != self._selectionIndex:
+                       self.highlighted.emit(self._selectionIndex)
+                       self.update()
+
+       @misc_utils.log_exception(_moduleLogger)
+       def mouseMoveEvent(self, mouseEvent):
+               lastSelection = self._selectionIndex
+
+               lastMousePos = mouseEvent.pos()
+               self._update_selection(lastMousePos)
+
+               if lastSelection != self._selectionIndex:
+                       self.highlighted.emit(self._selectionIndex)
+                       self.update()
+
+       @misc_utils.log_exception(_moduleLogger)
+       def mouseReleaseEvent(self, mouseEvent):
+               lastSelection = self._selectionIndex
+
+               lastMousePos = mouseEvent.pos()
+               self._update_selection(lastMousePos)
+               self._mousePosition = ()
+
+               self._activate_at(self._selectionIndex)
+               self.update()
+
+       @misc_utils.log_exception(_moduleLogger)
+       def keyPressEvent(self, keyEvent):
+               if keyEvent.key() in [QtCore.Qt.Key_Right, QtCore.Qt.Key_Down, QtCore.Qt.Key_Tab]:
+                       if self._selectionIndex != len(self._filing) - 1:
+                               nextSelection = self._selectionIndex + 1
+                       else:
+                               nextSelection = 0
+                       self._select_at(nextSelection)
+                       self.update()
+               elif keyEvent.key() in [QtCore.Qt.Key_Left, QtCore.Qt.Key_Up, QtCore.Qt.Key_Backtab]:
+                       if 0 < self._selectionIndex:
+                               nextSelection = self._selectionIndex - 1
+                       else:
+                               nextSelection = len(self._filing) - 1
+                       self._select_at(nextSelection)
+                       self.update()
+               elif keyEvent.key() in [QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter, QtCore.Qt.Key_Space]:
+                       self._activate_at(self._selectionIndex)
+               elif keyEvent.key() in [QtCore.Qt.Key_Escape, QtCore.Qt.Key_Backspace]:
+                       self._activate_at(PieFiling.SELECTION_NONE)
+               else:
+                       QtGui.QWidget.keyPressEvent(self, keyEvent)
+
+       @misc_utils.log_exception(_moduleLogger)
+       def showEvent(self, showEvent):
+               self.aboutToShow.emit()
+               self._cachedCenterPosition = self.rect().center()
+
+               mask = self._artist.show(self.palette())
+               self.setMask(mask)
+
+               lastMousePos = self.mapFromGlobal(QtGui.QCursor.pos())
+               self._update_selection(lastMousePos)
+
+               QtGui.QWidget.showEvent(self, showEvent)
+
+       @misc_utils.log_exception(_moduleLogger)
+       def hideEvent(self, hideEvent):
+               self._artist.hide()
+               self._selectionIndex = PieFiling.SELECTION_NONE
+               QtGui.QWidget.hideEvent(self, hideEvent)
+
+       @misc_utils.log_exception(_moduleLogger)
+       def paintEvent(self, paintEvent):
+               canvas = self._artist.paint(self._selectionIndex)
+
+               screen = QtGui.QPainter(self)
+               screen.drawPixmap(QtCore.QPoint(0, 0), canvas)
+
+               QtGui.QWidget.paintEvent(self, paintEvent)
+
+       def __iter__(self):
+               return iter(self._filing)
+
+       def __len__(self):
+               return len(self._filing)
+
+       def _select_at(self, index):
+               self._selectionIndex = index
+
+       def _update_selection(self, lastMousePos):
+               radius = _radius_at(self._cachedCenterPosition, lastMousePos)
+               if radius < self._filing.innerRadius():
+                       self._selectionIndex = PieFiling.SELECTION_CENTER
+               elif radius <= self._filing.outerRadius():
+                       self._select_at(self.indexAt(lastMousePos))
+               else:
+                       self._selectionIndex = PieFiling.SELECTION_NONE
+
+       def _activate_at(self, index):
+               if index == PieFiling.SELECTION_NONE:
+                       self.canceled.emit()
+                       self.aboutToHide.emit()
+                       self.hide()
+                       return
+               elif index == PieFiling.SELECTION_CENTER:
+                       child = self._filing.center()
+               else:
+                       child = self.itemAt(index)
+
+               if child.isEnabled():
+                       child.action().trigger()
+                       self.activated.emit(index)
+               else:
+                       self.canceled.emit()
+               self.aboutToHide.emit()
+               self.hide()
+
+
+def init_pies():
+       PieFiling.NULL_CENTER.setEnabled(False)
+
+
+def _print(msg):
+       print msg
+
+
+def _on_about_to_hide(app):
+       app.exit()
+
+
+if __name__ == "__main__":
+       app = QtGui.QApplication([])
+       init_pies()
+
+       if False:
+               pie = QPieMenu()
+               pie.show()
+
+       if False:
+               singleAction = QtGui.QAction(None)
+               singleAction.setText("Boo")
+               singleItem = QActionPieItem(singleAction)
+               spie = QPieMenu()
+               spie.insertItem(singleItem)
+               spie.show()
+
+       if False:
+               oneAction = QtGui.QAction(None)
+               oneAction.setText("Chew")
+               oneItem = QActionPieItem(oneAction)
+               twoAction = QtGui.QAction(None)
+               twoAction.setText("Foo")
+               twoItem = QActionPieItem(twoAction)
+               iconTextAction = QtGui.QAction(None)
+               iconTextAction.setText("Icon")
+               iconTextAction.setIcon(QtGui.QIcon.fromTheme("gtk-close"))
+               iconTextItem = QActionPieItem(iconTextAction)
+               mpie = QPieMenu()
+               mpie.insertItem(oneItem)
+               mpie.insertItem(twoItem)
+               mpie.insertItem(oneItem)
+               mpie.insertItem(iconTextItem)
+               mpie.show()
+
+       if True:
+               oneAction = QtGui.QAction(None)
+               oneAction.setText("Chew")
+               oneAction.triggered.connect(lambda: _print("Chew"))
+               oneItem = QActionPieItem(oneAction)
+               twoAction = QtGui.QAction(None)
+               twoAction.setText("Foo")
+               twoAction.triggered.connect(lambda: _print("Foo"))
+               twoItem = QActionPieItem(twoAction)
+               iconAction = QtGui.QAction(None)
+               iconAction.setIcon(QtGui.QIcon.fromTheme("gtk-open"))
+               iconAction.triggered.connect(lambda: _print("Icon"))
+               iconItem = QActionPieItem(iconAction)
+               iconTextAction = QtGui.QAction(None)
+               iconTextAction.setText("Icon")
+               iconTextAction.setIcon(QtGui.QIcon.fromTheme("gtk-close"))
+               iconTextAction.triggered.connect(lambda: _print("Icon and text"))
+               iconTextItem = QActionPieItem(iconTextAction)
+               mpie = QPieMenu()
+               mpie.set_center(iconItem)
+               mpie.insertItem(oneItem)
+               mpie.insertItem(twoItem)
+               mpie.insertItem(oneItem)
+               mpie.insertItem(iconTextItem)
+               mpie.show()
+               mpie.aboutToHide.connect(lambda: _on_about_to_hide(app))
+               mpie.canceled.connect(lambda: _print("Canceled"))
+
+       if False:
+               oneAction = QtGui.QAction(None)
+               oneAction.setText("Chew")
+               oneAction.triggered.connect(lambda: _print("Chew"))
+               oneItem = QActionPieItem(oneAction)
+               twoAction = QtGui.QAction(None)
+               twoAction.setText("Foo")
+               twoAction.triggered.connect(lambda: _print("Foo"))
+               twoItem = QActionPieItem(twoAction)
+               iconAction = QtGui.QAction(None)
+               iconAction.setIcon(QtGui.QIcon.fromTheme("gtk-open"))
+               iconAction.triggered.connect(lambda: _print("Icon"))
+               iconItem = QActionPieItem(iconAction)
+               iconTextAction = QtGui.QAction(None)
+               iconTextAction.setText("Icon")
+               iconTextAction.setIcon(QtGui.QIcon.fromTheme("gtk-close"))
+               iconTextAction.triggered.connect(lambda: _print("Icon and text"))
+               iconTextItem = QActionPieItem(iconTextAction)
+               pieFiling = PieFiling()
+               pieFiling.set_center(iconItem)
+               pieFiling.insertItem(oneItem)
+               pieFiling.insertItem(twoItem)
+               pieFiling.insertItem(oneItem)
+               pieFiling.insertItem(iconTextItem)
+               mpie = QPieDisplay(pieFiling)
+               mpie.show()
+
+       if False:
+               oneAction = QtGui.QAction(None)
+               oneAction.setText("Chew")
+               oneAction.triggered.connect(lambda: _print("Chew"))
+               oneItem = QActionPieItem(oneAction)
+               twoAction = QtGui.QAction(None)
+               twoAction.setText("Foo")
+               twoAction.triggered.connect(lambda: _print("Foo"))
+               twoItem = QActionPieItem(twoAction)
+               iconAction = QtGui.QAction(None)
+               iconAction.setIcon(QtGui.QIcon.fromTheme("gtk-open"))
+               iconAction.triggered.connect(lambda: _print("Icon"))
+               iconItem = QActionPieItem(iconAction)
+               iconTextAction = QtGui.QAction(None)
+               iconTextAction.setText("Icon")
+               iconTextAction.setIcon(QtGui.QIcon.fromTheme("gtk-close"))
+               iconTextAction.triggered.connect(lambda: _print("Icon and text"))
+               iconTextItem = QActionPieItem(iconTextAction)
+               mpie = QPieButton(iconItem)
+               mpie.set_center(iconItem)
+               mpie.insertItem(oneItem)
+               mpie.insertItem(twoItem)
+               mpie.insertItem(oneItem)
+               mpie.insertItem(iconTextItem)
+               mpie.show()
+               mpie.aboutToHide.connect(lambda: _on_about_to_hide(app))
+               mpie.canceled.connect(lambda: _print("Canceled"))
+
+       app.exec_()
diff --git a/src/util/qtpieboard.py b/src/util/qtpieboard.py
new file mode 100755 (executable)
index 0000000..7d79c60
--- /dev/null
@@ -0,0 +1,187 @@
+#!/usr/bin/env python
+
+
+from __future__ import division
+
+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
+
+       def __init__(self):
+               self._layout = QtGui.QGridLayout()
+               self._widget = QtGui.QWidget()
+               self._widget.setLayout(self._layout)
+
+               self.__cells = {}
+
+       @property
+       def toplevel(self):
+               return self._widget
+
+       def add_pie(self, row, column, pieButton):
+               assert len(pieButton) == 8
+               self._layout.addWidget(pieButton, row, column, QtCore.Qt.AlignCenter)
+               self.__cells[(row, column)] = pieButton
+
+       def get_pie(self, row, column):
+               return self.__cells[(row, column)]
+
+
+class KeyboardModifier(object):
+
+       def __init__(self, name):
+               self.name = name
+               self.lock = False
+               self.once = False
+
+       @property
+       def isActive(self):
+               return self.lock or self.once
+
+       def on_toggle_lock(self, *args, **kwds):
+               self.lock = not self.lock
+
+       def on_toggle_once(self, *args, **kwds):
+               self.once = not self.once
+
+       def reset_once(self):
+               self.once = False
+
+
+def parse_keyboard_data(text):
+       return eval(text)
+
+
+def _enumerate_pie_slices(pieData, iconPaths):
+       for direction, directionName in zip(
+               PieKeyboard.SLICE_DIRECTIONS, PieKeyboard.SLICE_DIRECTION_NAMES
+       ):
+               if directionName in pieData:
+                       sliceData = pieData[directionName]
+
+                       action = QtGui.QAction(None)
+                       try:
+                               action.setText(sliceData["text"])
+                       except KeyError:
+                               pass
+                       try:
+                               relativeIconPath = sliceData["path"]
+                       except KeyError:
+                               pass
+                       else:
+                               for iconPath in iconPaths:
+                                       absIconPath = os.path.join(iconPath, relativeIconPath)
+                                       if os.path.exists(absIconPath):
+                                               action.setIcon(QtGui.QIcon(absIconPath))
+                                               break
+                       pieItem = qtpie.QActionPieItem(action)
+                       actionToken = sliceData["action"]
+               else:
+                       pieItem = qtpie.PieFiling.NULL_CENTER
+                       actionToken = ""
+               yield direction, pieItem, actionToken
+
+
+def load_keyboard(keyboardName, dataTree, keyboard, keyboardHandler, iconPaths):
+       for (row, column), pieData in dataTree.iteritems():
+               pieItems = list(_enumerate_pie_slices(pieData, iconPaths))
+               assert pieItems[0][0] == PieKeyboard.SLICE_CENTER, pieItems[0]
+               _, center, centerAction = pieItems.pop(0)
+
+               pieButton = qtpie.QPieButton(center)
+               pieButton.set_center(center)
+               keyboardHandler.map_slice_action(center, centerAction)
+               for direction, pieItem, action in pieItems:
+                       pieButton.insertItem(pieItem)
+                       keyboardHandler.map_slice_action(pieItem, action)
+               keyboard.add_pie(row, column, pieButton)
+
+
+class KeyboardHandler(object):
+
+       def __init__(self, keyhandler):
+               self.__keyhandler = keyhandler
+               self.__commandHandlers = {}
+               self.__modifiers = {}
+               self.__sliceActions = {}
+
+               self.register_modifier("Shift")
+               self.register_modifier("Super")
+               self.register_modifier("Control")
+               self.register_modifier("Alt")
+
+       def register_command_handler(self, command, handler):
+               # @todo Look into hooking these up directly to the pie actions
+               self.__commandHandlers["[%s]" % command] = handler
+
+       def unregister_command_handler(self, command):
+               # @todo Look into hooking these up directly to the pie actions
+               del self.__commandHandlers["[%s]" % command]
+
+       def register_modifier(self, modifierName):
+               mod = KeyboardModifier(modifierName)
+               self.register_command_handler(modifierName, mod.on_toggle_lock)
+               self.__modifiers["<%s>" % modifierName] = mod
+
+       def unregister_modifier(self, modifierName):
+               self.unregister_command_handler(modifierName)
+               del self.__modifiers["<%s>" % modifierName]
+
+       def map_slice_action(self, slice, action):
+               callback = lambda direction: self(direction, action)
+               slice.action().triggered.connect(callback)
+               self.__sliceActions[slice] = (action, callback)
+
+       def __call__(self, direction, action):
+               activeModifiers = [
+                       mod.name
+                       for mod in self.__modifiers.itervalues()
+                               if mod.isActive
+               ]
+
+               needResetOnce = False
+               if action.startswith("[") and action.endswith("]"):
+                       commandName = action[1:-1]
+                       if action in self.__commandHandlers:
+                               self.__commandHandlers[action](commandName, activeModifiers)
+                               needResetOnce = True
+                       else:
+                               warnings.warn("Unknown command: [%s]" % commandName)
+               elif action.startswith("<") and action.endswith(">"):
+                       modName = action[1:-1]
+                       for mod in self.__modifiers.itervalues():
+                               if mod.name == modName:
+                                       mod.on_toggle_once()
+                                       break
+                       else:
+                               warnings.warn("Unknown modifier: <%s>" % modName)
+               else:
+                       self.__keyhandler(action, activeModifiers)
+                       needResetOnce = True
+
+               if needResetOnce:
+                       for mod in self.__modifiers.itervalues():
+                               mod.reset_once()
diff --git a/src/util/time_utils.py b/src/util/time_utils.py
new file mode 100644 (file)
index 0000000..90ec84d
--- /dev/null
@@ -0,0 +1,94 @@
+from datetime import tzinfo, timedelta, datetime
+
+ZERO = timedelta(0)
+HOUR = timedelta(hours=1)
+
+
+def first_sunday_on_or_after(dt):
+       days_to_go = 6 - dt.weekday()
+       if days_to_go:
+               dt += timedelta(days_to_go)
+       return dt
+
+
+# US DST Rules
+#
+# This is a simplified (i.e., wrong for a few cases) set of rules for US
+# DST start and end times. For a complete and up-to-date set of DST rules
+# and timezone definitions, visit the Olson Database (or try pytz):
+# http://www.twinsun.com/tz/tz-link.htm
+# http://sourceforge.net/projects/pytz/ (might not be up-to-date)
+#
+# In the US, since 2007, DST starts at 2am (standard time) on the second
+# Sunday in March, which is the first Sunday on or after Mar 8.
+DSTSTART_2007 = datetime(1, 3, 8, 2)
+# and ends at 2am (DST time; 1am standard time) on the first Sunday of Nov.
+DSTEND_2007 = datetime(1, 11, 1, 1)
+# From 1987 to 2006, DST used to start at 2am (standard time) on the first
+# Sunday in April and to end at 2am (DST time; 1am standard time) on the last
+# Sunday of October, which is the first Sunday on or after Oct 25.
+DSTSTART_1987_2006 = datetime(1, 4, 1, 2)
+DSTEND_1987_2006 = datetime(1, 10, 25, 1)
+# From 1967 to 1986, DST used to start at 2am (standard time) on the last
+# Sunday in April (the one on or after April 24) and to end at 2am (DST time;
+# 1am standard time) on the last Sunday of October, which is the first Sunday
+# on or after Oct 25.
+DSTSTART_1967_1986 = datetime(1, 4, 24, 2)
+DSTEND_1967_1986 = DSTEND_1987_2006
+
+
+class USTimeZone(tzinfo):
+
+       def __init__(self, hours, reprname, stdname, dstname):
+               self.stdoffset = timedelta(hours=hours)
+               self.reprname = reprname
+               self.stdname = stdname
+               self.dstname = dstname
+
+       def __repr__(self):
+               return self.reprname
+
+       def tzname(self, dt):
+               if self.dst(dt):
+                       return self.dstname
+               else:
+                       return self.stdname
+
+       def utcoffset(self, dt):
+               return self.stdoffset + self.dst(dt)
+
+       def dst(self, dt):
+               if dt is None or dt.tzinfo is None:
+                       # An exception may be sensible here, in one or both cases.
+                       # It depends on how you want to treat them.  The default
+                       # fromutc() implementation (called by the default astimezone()
+                       # implementation) passes a datetime with dt.tzinfo is self.
+                       return ZERO
+               assert dt.tzinfo is self
+
+               # Find start and end times for US DST. For years before 1967, return
+               # ZERO for no DST.
+               if 2006 < dt.year:
+                       dststart, dstend = DSTSTART_2007, DSTEND_2007
+               elif 1986 < dt.year < 2007:
+                       dststart, dstend = DSTSTART_1987_2006, DSTEND_1987_2006
+               elif 1966 < dt.year < 1987:
+                       dststart, dstend = DSTSTART_1967_1986, DSTEND_1967_1986
+               else:
+                       return ZERO
+
+               start = first_sunday_on_or_after(dststart.replace(year=dt.year))
+               end = first_sunday_on_or_after(dstend.replace(year=dt.year))
+
+               # Can't compare naive to aware objects, so strip the timezone from
+               # dt first.
+               if start <= dt.replace(tzinfo=None) < end:
+                       return HOUR
+               else:
+                       return ZERO
+
+
+Eastern  = USTimeZone(-5, "Eastern",  "EST", "EDT")
+Central  = USTimeZone(-6, "Central",  "CST", "CDT")
+Mountain = USTimeZone(-7, "Mountain", "MST", "MDT")
+Pacific  = USTimeZone(-8, "Pacific",  "PST", "PDT")
index 1c6cbc8..30d7629 100644 (file)
@@ -25,7 +25,7 @@ class WasMissedCall(object):
                self._didReport = False
 
                self._onTimeout = gobject_utils.Timeout(self._on_timeout)
-               self._onTimeout.start(seconds=10)
+               self._onTimeout.start(seconds=60)
 
                chan[telepathy.interfaces.CHANNEL_INTERFACE_GROUP].connect_to_signal(
                        "MembersChanged",