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