From 23853a8c342362a86157a318d3a5f9d4c917f57e Mon Sep 17 00:00:00 2001 From: Ed Page Date: Tue, 24 Aug 2010 20:36:57 -0500 Subject: [PATCH] Moving pie menus into the utils --- src/ejpi_qt.py | 4 +- src/libraries/__init__.py | 6 - src/libraries/qtpie.py | 1069 ------------------------------------------- src/libraries/qtpieboard.py | 207 --------- src/plugin_utils.py | 2 +- src/util/qtpie.py | 1069 +++++++++++++++++++++++++++++++++++++++++++ src/util/qtpieboard.py | 207 +++++++++ 7 files changed, 1279 insertions(+), 1285 deletions(-) delete mode 100644 src/libraries/__init__.py delete mode 100755 src/libraries/qtpie.py delete mode 100755 src/libraries/qtpieboard.py create mode 100755 src/util/qtpie.py create mode 100755 src/util/qtpieboard.py diff --git a/src/ejpi_qt.py b/src/ejpi_qt.py index 8992f72..05b597f 100755 --- a/src/ejpi_qt.py +++ b/src/ejpi_qt.py @@ -16,8 +16,8 @@ import constants import maeqt from util import misc as misc_utils -from libraries import qtpie -from libraries import qtpieboard +from util import qtpie +from util import qtpieboard import plugin_utils import history import qhistory diff --git a/src/libraries/__init__.py b/src/libraries/__init__.py deleted file mode 100644 index 3e621cd..0000000 --- a/src/libraries/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env python - -from pkgutil import extend_path - - -#__path__ = extend_path(__path__, __name__) diff --git a/src/libraries/qtpie.py b/src/libraries/qtpie.py deleted file mode 100755 index 884d5ce..0000000 --- a/src/libraries/qtpie.py +++ /dev/null @@ -1,1069 +0,0 @@ -#!/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 - - -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/libraries/qtpieboard.py b/src/libraries/qtpieboard.py deleted file mode 100755 index 80c43d0..0000000 --- a/src/libraries/qtpieboard.py +++ /dev/null @@ -1,207 +0,0 @@ -#!/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 = -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() - 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) - 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/plugin_utils.py b/src/plugin_utils.py index f5bef68..087f8db 100644 --- a/src/plugin_utils.py +++ b/src/plugin_utils.py @@ -9,7 +9,7 @@ import os import inspect import ConfigParser -from libraries import qtpieboard +from util import qtpieboard from util import io import operation diff --git a/src/util/qtpie.py b/src/util/qtpie.py new file mode 100755 index 0000000..884d5ce --- /dev/null +++ b/src/util/qtpie.py @@ -0,0 +1,1069 @@ +#!/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 + + +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..80c43d0 --- /dev/null +++ b/src/util/qtpieboard.py @@ -0,0 +1,207 @@ +#!/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 = -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() + 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) + 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() -- 1.7.9.5