From 7c7cf777ed5a70550f8a7becca6a81515fb1e4b6 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Tue, 28 Dec 2010 21:38:23 -0600 Subject: [PATCH] Pulling in latest skeleton changes --- src/maeqt.py | 31 ++ src/util/go_utils.py | 6 +- src/util/io.py | 82 +++- src/util/linux.py | 74 +++- src/util/qtpie.py | 1108 ++++++++++++++++++++++++++++++++++++++++++++++++ src/util/qtpieboard.py | 187 ++++++++ src/util/time_utils.py | 94 ++++ src/util/tp_utils.py | 2 +- 8 files changed, 1575 insertions(+), 9 deletions(-) create mode 100755 src/util/qtpie.py create mode 100755 src/util/qtpieboard.py create mode 100644 src/util/time_utils.py diff --git a/src/maeqt.py b/src/maeqt.py index d61c669..d5eb18b 100644 --- a/src/maeqt.py +++ b/src/maeqt.py @@ -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 diff --git a/src/util/go_utils.py b/src/util/go_utils.py index d066542..97d671c 100644 --- a/src/util/go_utils.py +++ b/src/util/go_utils.py @@ -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: diff --git a/src/util/io.py b/src/util/io.py index aece2dd..aac896d 100644 --- a/src/util/io.py +++ b/src/util/io.py @@ -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') diff --git a/src/util/linux.py b/src/util/linux.py index 4837f2a..4e77445 100644 --- a/src/util/linux.py +++ b/src/util/linux.py @@ -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 index 0000000..005050f --- /dev/null +++ b/src/util/qtpie.py @@ -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 index 0000000..7d79c60 --- /dev/null +++ b/src/util/qtpieboard.py @@ -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 index 0000000..90ec84d --- /dev/null +++ b/src/util/time_utils.py @@ -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") diff --git a/src/util/tp_utils.py b/src/util/tp_utils.py index 1c6cbc8..30d7629 100644 --- a/src/util/tp_utils.py +++ b/src/util/tp_utils.py @@ -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", -- 1.7.9.5