From 7b2cc67c74a02f0aa3822074446ba099b1e5c830 Mon Sep 17 00:00:00 2001 From: Chris Colbert Date: Mon, 23 Sep 2013 18:55:12 -0400 Subject: [PATCH 1/3] update the popup view example this updates the example to use reasonable parameters for the cursor mode. --- examples/widgets/popup_view.enaml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/examples/widgets/popup_view.enaml b/examples/widgets/popup_view.enaml index cfa1c1251..c8fc0b981 100644 --- a/examples/widgets/popup_view.enaml +++ b/examples/widgets/popup_view.enaml @@ -130,4 +130,10 @@ enamldef Main(Window): win: clicked :: NotificationPopup().show() PushButton: text = 'Show Mouse Notification' - clicked :: NotificationPopup(anchor_mode='cursor').show() + clicked :: + popup = NotificationPopup() + popup.anchor_mode = 'cursor' + popup.anchor = (0.0, 0.0) + popup.offset = (0, 0) + popup.timeout = 1 + popup.show() From 06acf8093eb58144c6dc766fb39e8ecf5c8b3a7c Mon Sep 17 00:00:00 2001 From: Chris Colbert Date: Mon, 23 Sep 2013 19:28:14 -0400 Subject: [PATCH 2/3] wip commit --- enaml/qt/q_popup_view.py | 698 ++++++++++++++++++++++---------------- enaml/qt/qt_popup_view.py | 15 +- 2 files changed, 421 insertions(+), 292 deletions(-) diff --git a/enaml/qt/q_popup_view.py b/enaml/qt/q_popup_view.py index a67496592..ace7e1cd0 100644 --- a/enaml/qt/q_popup_view.py +++ b/enaml/qt/q_popup_view.py @@ -5,11 +5,11 @@ # # The full license is in the file COPYING.txt, distributed with this software. #------------------------------------------------------------------------------ -from atom.api import Atom, Typed, Float, Int +from atom.api import Atom, Typed, Float, Int, IntEnum from .QtCore import ( - Qt, QPoint, QPointF, QSize, QMargins, QPropertyAnimation, QTimer, QEvent, - Signal + Qt, QPoint, QPointF, QSize, QRect,QMargins, QPropertyAnimation, QTimer, + QEvent, Signal ) from .QtGui import ( QApplication, QWidget, QLayout, QPainter, QPainterPath, QRegion, QPen, @@ -19,96 +19,335 @@ from .q_single_widget_layout import QSingleWidgetLayout -class QPopupView(QWidget): - """ A custom QWidget which implements a framless popup widget. - - It is useful for showing transient configuration dialogs as well - as temporary notification windows. +class AnchorMode(IntEnum): + """ An IntEnum defining the various popup anchor modes. """ - #: A signal emitted when the popup is fully closed. - closed = Signal() + #: Anchor to the parent widget. + Parent = 0 + #: Anchor to current snapped mouse position. + Cursor = 1 + + +class ArrowEdge(IntEnum): + """ An IntEnum defining the edge location of the popup arrow. + + """ #: The left edge of the popup view. - LeftEdge = 0 + Left = 0 #: The right edge of the popup view. - RightEdge = 1 + Right = 1 #: The top edge of the popup view. - TopEdge = 2 + Top = 2 #: The bottom edge of the popup view. - BottomEdge = 3 + Bottom = 3 + + +class PopupState(Atom): + """ A class which maintains the public state for a popup view. + + """ + #: The anchor location on the view. The default anchors + #: the top center of the view to the center of the parent. + anchor = Typed(QPointF, factory=lambda: QPointF(0.5, 0.0)) + + #: The anchor location on the parent. The default anchors + #: the top center of the view to the center of the parent. + parent_anchor = Typed(QPointF, factory=lambda: QPointF(0.5, 0.5)) + + #: The offset of the popup view with respect to the anchor. + offset = Typed(QPoint, factory=lambda: QPoint(0, 0)) + + #: The mode to use when computing the anchored position. + anchor_mode = Typed(AnchorMode, factory=lambda: AnchorMode.Parent) + + #: The edge location of the arrow for the view. + arrow_edge = Typed(ArrowEdge, factory=lambda: ArrowEdge.Left) + + #: The size of the arrow for the view. + arrow_size = Int(0) + + #: The position of the arrow for the view. + arrow_position = Float(0.5) - #: Anchor to parent (which can be None) - AnchorParent = 0 + #: The timeout value to use when closing the view, in seconds. + timeout = Float(0.0) - #: Anchor to mouse - AnchorCursor = 1 + #: The duration for the fade in. + fade_in_duration = Int(100) - class ViewState(Atom): - """ A private class used to manage the state of a popup view. + #: The duration for the fade out. + fade_out_duration = Int(100) + + #: The computed path to use when drawing the view. + path = Typed(QPainterPath, factory=lambda: QPainterPath()) + + #: The animator to use when showing the view. + fade_in_animator = Typed(QPropertyAnimation, ()) + + #: The animator to use when hiding the view. + fade_out_animator = Typed(QPropertyAnimation, ()) + + #: The timeout timer to use for closing the view. + close_timer = Typed(QTimer, ()) + + def init(self, widget): + """ Initialize the state for the owner widget. """ - #: The anchor location on the view. The default anchors - #: the top center of the view to the center of the parent. - anchor = Typed(QPointF, factory=lambda: QPointF(0.5, 0.0)) + fade_in = self.fade_in_animator + fade_in.setTargetObject(widget) + fade_in.setPropertyName('windowOpacity') + fade_out = self.fade_out_animator + fade_out.setTargetObject(widget) + fade_out.setPropertyName('windowOpacity') + fade_out.finished.connect(widget.close) + close_timer = self.close_timer + close_timer.setSingleShot(True) + close_timer.timeout.connect(widget.close) + + +class LayoutData(Atom): + """ An object which holds the data for popup view layout. - #: The anchor location on the parent. The default anchors - #: the top center of the view to the center of the parent. - parent_anchor = Typed(QPointF, factory=lambda: QPointF(0.5, 0.5)) + """ + #: The position of the popup view. + pos = Typed(QPoint) + + #: The size of the popup view. + size = Typed(QSize) + + #: The size of the arrow in pixels. + arrow_size = Int(0) + + #: The edge location of the arrow. + arrow_edge = Typed(ArrowEdge) + + #: The position of the arrow on the edge. + arrow_position = Float(0.0) + + +def make_path(layout_data): + """ Create the painter path for the arrow. + + Parameters + ---------- + layout_data : LayoutData + The layout data object describing the path to generate. + + Returns + ------- + result : QPainterPath + The painter path for the view. + + """ + def arrow_offset(length, height, pos): + base = 2 * height + pos = max(0.0, min(1.0, pos)) + base = min(length, base) + return int(pos * (length - base)) + base / 2 + arrow_size = layout_data.arrow_size + arrow_pos = layout_data.arrow_position + edge = layout_data.arrow_edge + w = layout_data.size.width() + h = layout_data.size.height() + path = QPainterPath() + if arrow_size <= 0: + path.moveTo(0, 0) + path.lineTo(w, 0) + path.lineTo(w, h) + path.lineTo(0, h) + path.lineTo(0, 0) + elif edge == ArrowEdge.Bottom: + offset = arrow_offset(w, arrow_size, arrow_pos) + ledge = h - arrow_size + path.moveTo(0, 0) + path.lineTo(w, 0) + path.lineTo(w, ledge) + path.lineTo(offset + arrow_size, ledge) + path.lineTo(offset, h) + path.lineTo(offset - arrow_size, ledge) + path.lineTo(0, ledge) + path.lineTo(0, 0) + elif edge == ArrowEdge.Top: + offset = arrow_offset(w, arrow_size, arrow_pos) + path.moveTo(0, arrow_size) + path.lineTo(offset - arrow_size, arrow_size) + path.lineTo(offset, 0) + path.lineTo(offset + arrow_size, arrow_size) + path.lineTo(w, arrow_size) + path.lineTo(w, h) + path.lineTo(0, h) + path.lineTo(0, arrow_size) + elif edge == ArrowEdge.Left: + offset = arrow_offset(h, arrow_size, arrow_pos) + path.moveTo(arrow_size, 0) + path.lineTo(w, 0) + path.lineTo(w, h) + path.lineTo(arrow_size, h) + path.lineTo(arrow_size, offset + arrow_size) + path.lineTo(0, offset) + path.lineTo(arrow_size, offset - arrow_size) + path.lineTo(arrow_size, 0) + else: + offset = arrow_offset(h, arrow_size, arrow_pos) + ledge = w - arrow_size + path.moveTo(0, 0) + path.lineTo(ledge, 0) + path.lineTo(ledge, offset - arrow_size) + path.lineTo(w, offset) + path.lineTo(ledge, offset + arrow_size) + path.lineTo(ledge, h) + path.lineTo(0, h) + path.lineTo(0, 0) + return path + + +def edge_margins(size, edge): + """ Get the contents margins for a given arrow size and edge. + + Parameters + ---------- + size : int + The size of the arrow in pixels. + + edge : ArrowEdge + The edge location of the arrow. + + """ + margins = QMargins() + if size > 0: + if edge == ArrowEdge.Right: + margins.setRight(size) + elif edge == ArrowEdge.Bottom: + margins.setBottom(size) + elif edge == ArrowEdge.Left: + margins.setLeft(size) + else: + margins.setTop(size) + return margins + + +def target_global_pos(parent, mode, anchor): + """ Get the global position of the parent anchor. + + Parameters + ---------- + parent : QWidget or None + The parent widget for the view. - #: Anchor to parent or cursor - anchor_mode = Int(0) # AnchorParent + mode : AnchorMode + The anchor mode for the view. - #: The size of the arrow for the view. - arrow_size = Int(0) + anchor : QPoint + The anchor location on the parent. - #: The edge location of the arrow for the view. - arrow_edge = Int(0) # LeftEdge + Returns + ------- + result : QPoint + The global coordinates of the target parent anchor. - #: The position of the arrow for the view. - arrow_position = Float(0.5) + """ + if mode == AnchorMode.Cursor: + origin = QCursor.pos() + size = QSize() + else: + if parent is None: + desktop = QApplication.desktop() + geo = desktop.availableGeometry() + origin = geo.topLeft() + size = geo.size() + else: + origin = parent.mapToGlobal(QPoint(0, 0)) + size = parent.size() + px = int(anchor.x() * size.width()) + py = int(anchor.y() * size.height()) + return origin + QPoint(px, py) - #: The offset of the view wrt to the anchor. - offset = Typed(QPoint, factory=lambda: QPoint(0, 0)) - #: The timeout value to use when closing the view, in seconds. - timeout = Float(0.0) +def popup_offset(size, anchor, offset): + """ Get the offset to apply to the target global pos. - #: The path to use when drawing the view. - path = Typed(QPainterPath, factory=QPainterPath) + Parameters + ---------- + size : QSize + The size of the popup view. - #: The animator to use when showing the view. - fade_in_animator = Typed(QPropertyAnimation, ()) + anchor : QPoint + The anchor for the popup view. - #: The animator to use when hiding the view. - fade_out_animator = Typed(QPropertyAnimation, ()) + offset : QPoint + The additional offset for the popup view. - #: The duration for the fade in. - fade_in_duration = Int(100) + Returns + ------- + result : QPoint + The offset to apply to the global target position to + move the popup to the correct location. - #: The duration for the fade out. - fade_out_duration = Int(100) + """ + px = int(anchor.x() * size.width()) + py = int(anchor.y() * size.height()) + return QPoint(px, py) - offset - #: The timeout timer to use for closing the view. - close_timer = Typed(QTimer, ()) - def init(self, widget): - """ Initialize the state for the owner widget. +def arrow_tip_pos(layout_data): + """ Compute the position of the arrow point. - """ - fade_in = self.fade_in_animator - fade_in.setTargetObject(widget) - fade_in.setPropertyName('windowOpacity') - fade_out = self.fade_out_animator - fade_out.setTargetObject(widget) - fade_out.setPropertyName('windowOpacity') - fade_out.finished.connect(widget.close) - close_timer = self.close_timer - close_timer.setSingleShot(True) - close_timer.timeout.connect(widget.close) + """ + pos = QPoint(layout_data.pos) + size = layout_data.size + width = size.width() + height = size.height() + arrow_edge = layout_data.arrow_edge + arrow_pos = layout_data.arrow_position + if arrow_edge == ArrowEdge.Top: + pos += QPoint(width * arrow_pos, 0) + elif arrow_edge == ArrowEdge.Right: + pos += QPoint(width, height * arrow_pos) + elif arrow_edge == ArrowEdge.Bottom: + pos += QPoint(width * arrow_pos, height) + else: + pos += QPoint(0, height * arrow_pos) + return pos + + +def ensure_on_screen(layout_data): + """ + + """ + return False + # tip_pos = arrow_tip_pos(layout_data) + # screen_geo = QApplication.desktop().availableGeometry(tip_pos) + # view_geo = QRect(layout_data.pos, layout_data.size) + # if screen_geo.contains(view_geo): + # return False + # sides = [] + # size = layout_data.size + # if _test_top(view_geo, ) + # if view_geo.width() > screen_geo.width(): + # return False + # if view_geo.height() > screen_geo.height(): + # return False + # if view_geo.x() < screen_geo.x(): + # dx = screen_geo.x() - view_geo.x() + + + + +class QPopupView(QWidget): + """ A custom QWidget which implements a framless popup widget. + + It is useful for showing transient configuration dialogs as well + as temporary notification windows. + + """ + #: A signal emitted when the popup is fully closed. + closed = Signal() def __init__(self, parent=None, flags=Qt.Popup): """ Initialize a QPopupView. @@ -129,7 +368,7 @@ def __init__(self, parent=None, flags=Qt.Popup): layout = QSingleWidgetLayout() layout.setSizeConstraint(QLayout.SetMinAndMaxSize) self.setLayout(layout) - self._state = self.ViewState() + self._state = PopupState() self._state.init(self) if parent is not None: parent.installEventFilter(self) @@ -183,85 +422,82 @@ def setAnchor(self, anchor): state = self._state if anchor != state.anchor: state.anchor = anchor - self._updatePosition() + self._refreshGeometry() - def anchorMode(self): - """ Get the anchor mode for the popup view + def parentAnchor(self): + """ Get the parent anchor position for the popup view. Returns ------- - result : int - An enum value describing the anchor mode of the popup. + result : QPointF + The parent anchor point for the view. """ - return self._state.anchor_mode + return self._state.parent_anchor - def setAnchorMode(self, mode): - """ Set the anchor mode for the popup view + def setParentAnchor(self, anchor): + """ Set the parent anchor position for the popup view. Parameters ---------- - mode : int - The anchor mode (can be AnchorParent or AnchorCursor) + anchor : QPointF + The parent anchor point for the view. """ state = self._state - if mode != state.anchor_mode: - state.anchor_mode = mode - self._updatePosition() + if anchor != state.parent_anchor: + state.parent_anchor = anchor + self._refreshGeometry() - def parentAnchor(self): - """ Get the parent anchor position for the popup view. + def offset(self): + """ Get the offset of the view from the anchors. Returns ------- - result : QPointF - The parent anchor point for the view. + result : QPoint + The offset of the view from the anchors. """ - return self._state.parent_anchor + return self._state.offset - def setParentAnchor(self, anchor): - """ Set the parent anchor position for the popup view. + def setOffset(self, offset): + """ Set the offset of the view from the anchors. Parameters ---------- - anchor : QPointF - The parent anchor point for the view. + offset : QPoint + The offset of the view from the anchors. """ state = self._state - if anchor != state.parent_anchor: - state.parent_anchor = anchor - self._updatePosition() + if offset != state.offset: + state.offset = offset + self._refreshGeometry() - def arrowSize(self): - """ Get the size of the popup arrow. + def anchorMode(self): + """ Get the anchor mode for the popup view Returns ------- result : int - The size of the arrow in pixels. + An enum value describing the anchor mode of the popup. """ - return self._arrow_size.height() + return self._state.anchor_mode - def setArrowSize(self, size): - """ Set size of the popup arrow. + def setAnchorMode(self, mode): + """ Set the anchor mode for the popup view Parameters ---------- - size : int - The size of the popup arrow, in pixels. A size of zero - indicates that no arrow is to be used. + mode : int + The anchor mode (can be AnchorParent or AnchorCursor) """ state = self._state - if size != state.arrow_size: - state.arrow_size = size - self._updateMargins() - self._updateMask() - self._updatePosition() + if mode != state.anchor_mode: + state.anchor_mode = mode + self._refreshGeometry() def arrowEdge(self): """ Get edge for the popup arrow. @@ -286,60 +522,58 @@ def setArrowEdge(self, edge): state = self._state if edge != state.arrow_edge: state.arrow_edge = edge - self._updateMargins() - self._updateMask() + self._refreshGeometry() - def arrowPosition(self): - """ Get the position of the popup arrow. + def arrowSize(self): + """ Get the size of the popup arrow. Returns ------- - result : float - The position of the arrow along its edge. + result : int + The size of the arrow in pixels. """ - return self._state.arrow_position + return self._arrow_size.height() - def setArrowPosition(self, pos): - """ Set the position of the popup arrow. + def setArrowSize(self, size): + """ Set size of the popup arrow. Parameters ---------- - pos : float - The position of the popup arrow along its edge. + size : int + The size of the popup arrow, in pixels. A size of zero + indicates that no arrow is to be used. """ state = self._state - if pos != state.arrow_position: - state.arrow_position = pos - self._updateMask() # This does not generate a paint event. - if self.isVisible(): - self.update() + if size != state.arrow_size: + state.arrow_size = size + self._refreshGeometry() - def offset(self): - """ Get the offset of the view from the anchors. + def arrowPosition(self): + """ Get the position of the popup arrow. Returns ------- - result : QPoint - The offset of the view from the anchors. + result : float + The position of the arrow along its edge. """ - return self._state.offset + return self._state.arrow_position - def setOffset(self, offset): - """ Set the offset of the view from the anchors. + def setArrowPosition(self, pos): + """ Set the position of the popup arrow. Parameters ---------- - offset : QPoint - The offset of the view from the anchors. + pos : float + The position of the popup arrow along its edge. """ state = self._state - if offset != state.offset: - state.offset = offset - self._updatePosition() + if pos != state.arrow_position: + state.arrow_position = pos + self._refreshGeometry() def timeout(self): """ Get the timeout for the view. @@ -420,7 +654,8 @@ def eventFilter(self, obj, event): evt_type = event.type() if evt_type == QEvent.Move or evt_type == QEvent.Resize: if obj is self.parent(): - self._updatePosition() + #self._updatePosition() + self._refreshGeometry() return False def mousePressEvent(self, event): @@ -453,6 +688,7 @@ def showEvent(self, event): manages the timeout for the popup. """ + self._refreshGeometry(force=True) state = self._state if state.timeout > 0.0: state.close_timer.start(int(state.timeout * 1000)) @@ -500,8 +736,7 @@ def resizeEvent(self, event): """ super(QPopupView, self).resizeEvent(event) - self._updateMask() - self._updatePosition() + self._refreshGeometry() def paintEvent(self, event): """ Handle the paint event for the popup view. @@ -523,167 +758,60 @@ def paintEvent(self, event): #-------------------------------------------------------------------------- # Private API #-------------------------------------------------------------------------- - @staticmethod - def _arrowOffset(length, height, pos): - """ Compute the offset for an arrow from parameters. + def _refreshGeometry(self, force=False): + """ Refresh the geometry for the popup using the current state. Parameters ---------- - length : int - The length of the edge on which the arrow is being drawn. - - height : int - The height of the arrow. - - pos : float - The position of the arrow along the edge. - - Returns - ------- - result : int - The offset from the start of the edge to the center of - the base of the arrow. + force : bool, optional + Wether or not to force the computation even if the view is + not visible. The default is False. """ - base = 2 * height - pos = max(0.0, min(1.0, pos)) - base = min(length, base) - return int(pos * (length - base)) + base / 2 - - def _updateMargins(self): - """ Update the contents margins for the popup view. + if not force and not self.isVisible(): + return - """ + # Compute the margins as specified by the state. state = self._state - margins = QMargins() - if state.arrow_size > 0: - size = state.arrow_size - edge = state.arrow_edge - if edge == QPopupView.RightEdge: - margins.setRight(size) - elif edge == QPopupView.BottomEdge: - margins.setBottom(size) - elif edge == QPopupView.LeftEdge: - margins.setLeft(size) - else: - margins.setTop(size) - self.setContentsMargins(margins) + arrow_edge = state.arrow_edge + arrow_size = state.arrow_size + margins = edge_margins(arrow_size, arrow_edge) - def _updateMask(self): - """ Update the mask and painter path for the popup view. - - """ + # Compute the hypothetical view size. + self.setContentsMargins(margins) size = self.size() - state = self._state - asize = state.arrow_size - apos = state.arrow_position - edge = state.arrow_edge - path = QPainterPath() - w = size.width() - h = size.height() - path = QPainterPath() - if asize <= 0: - path.moveTo(0, 0) - path.lineTo(w, 0) - path.lineTo(w, h) - path.lineTo(0, h) - path.lineTo(0, 0) - elif edge == QPopupView.BottomEdge: - offset = self._arrowOffset(w, asize, apos) - ledge = h - asize - path.moveTo(0, 0) - path.lineTo(w, 0) - path.lineTo(w, ledge) - path.lineTo(offset + asize, ledge) - path.lineTo(offset, h) - path.lineTo(offset - asize, ledge) - path.lineTo(0, ledge) - path.lineTo(0, 0) - elif edge == QPopupView.TopEdge: - offset = self._arrowOffset(w, asize, apos) - path.moveTo(0, asize) - path.lineTo(offset - asize, asize) - path.lineTo(offset, 0) - path.lineTo(offset + asize, asize) - path.lineTo(w, asize) - path.lineTo(w, h) - path.lineTo(0, h) - path.lineTo(0, asize) - elif edge == QPopupView.LeftEdge: - offset = self._arrowOffset(h, asize, apos) - path.moveTo(asize, 0) - path.lineTo(w, 0) - path.lineTo(w, h) - path.lineTo(asize, h) - path.lineTo(asize, offset + asize) - path.lineTo(0, offset) - path.lineTo(asize, offset - asize) - path.lineTo(asize, 0) - else: - offset = self._arrowOffset(h, asize, apos) - ledge = w - asize - path.moveTo(0, 0) - path.lineTo(ledge, 0) - path.lineTo(ledge, offset - asize) - path.lineTo(w, offset) - path.lineTo(ledge, offset + asize) - path.lineTo(ledge, h) - path.lineTo(0, h) - path.lineTo(0, 0) + + # Compute the hypothetical view position. + anchor_mode = state.anchor_mode + parent_anchor = state.parent_anchor + pos = target_global_pos(self.parent(), anchor_mode, parent_anchor) + pos -= popup_offset(size, state.anchor, state.offset) + + # Create the layout data and ensure it the view is on screen. + layout_data = LayoutData() + layout_data.pos = pos + layout_data.size = size + layout_data.arrow_size = arrow_size + layout_data.arrow_edge = arrow_edge + layout_data.arrow_position = state.arrow_position + changed = ensure_on_screen(layout_data) + + # If the layout has changed, apply the update. + if changed: + arrow_size = layout_data.arrow_size + arrow_edge = layout_data.arrow_edge + margins = edge_margins(arrow_size, arrow_edge) + self.setContentsMargins(margins) + layout_data.size = self.size() + + # Compute the painter path for the view and set the mask. + path = make_path(layout_data) state.path = path mask = QRegion(path.toFillPolygon().toPolygon()) self.setMask(mask) - def _targetGlobalPos(self): - """ Get the global position of the parent anchor. - - Returns - ------- - result : QPoint - The global coordinates of the target parent anchor. - - """ - state = self._state - if state.anchor_mode == QPopupView.AnchorCursor: - origin = QCursor.pos() - size = QSize() - else: - parent = self.parent() - if parent is None: - # FIXME expose something other than the primary screen. - desktop = QApplication.desktop() - geo = desktop.availableGeometry() - origin = geo.topLeft() - size = geo.size() - else: - origin = parent.mapToGlobal(QPoint(0, 0)) - size = parent.size() - anchor = state.parent_anchor - px = int(anchor.x() * size.width()) - py = int(anchor.y() * size.height()) - return origin + QPoint(px, py) - - def _popupOffset(self): - """ Get the offset to apply to the target global pos. - - Returns - ------- - result : QPoint - The offset to apply to the global target position to - move the popup to the correct location. - - """ - state = self._state - size = self.size() - anchor = state.anchor - px = int(anchor.x() * size.width()) - py = int(anchor.y() * size.height()) - return QPoint(px, py) - state.offset - - def _updatePosition(self): - """ Update the position of the popup view. - - """ - target = self._targetGlobalPos() - offset = self._popupOffset() - self.move(target - offset) + # Move the widget into position and update it. The update is + # necessary in the case where only the mask has changed, which + # does not automatically generate a paint event. + self.move(layout_data.pos) + self.update() diff --git a/enaml/qt/qt_popup_view.py b/enaml/qt/qt_popup_view.py index 1bfab73aa..6014dead2 100644 --- a/enaml/qt/qt_popup_view.py +++ b/enaml/qt/qt_popup_view.py @@ -11,15 +11,15 @@ from .QtCore import Qt, QPointF, QPoint -from .q_popup_view import QPopupView +from .q_popup_view import QPopupView, ArrowEdge, AnchorMode from .qt_widget import QtWidget EDGES = { - 'left': QPopupView.LeftEdge, - 'right': QPopupView.RightEdge, - 'top': QPopupView.TopEdge, - 'bottom': QPopupView.BottomEdge, + 'left': ArrowEdge.Left, + 'right': ArrowEdge.Right, + 'top': ArrowEdge.Top, + 'bottom': ArrowEdge.Bottom, } @@ -28,9 +28,10 @@ 'tool_tip': Qt.ToolTip, } + ANCHOR_MODE = { - 'parent': QPopupView.AnchorParent, - 'cursor': QPopupView.AnchorCursor, + 'parent': AnchorMode.Parent, + 'cursor': AnchorMode.Cursor, } From 61284fd7b48b586ccaa9c9f1c316268d8daa5334 Mon Sep 17 00:00:00 2001 From: Chris Colbert Date: Tue, 24 Sep 2013 18:32:27 -0400 Subject: [PATCH 3/3] update the popup view this update adds logic for the popup view to automatically reposition itself if it were to otherwise be rendered off-screen. It also adds a 'close_on_click' attribute to allow disabling of that default functionality. --- enaml/qt/q_popup_view.py | 681 ++++++++++++++++++++---------- enaml/qt/qt_popup_view.py | 14 +- enaml/widgets/popup_view.py | 59 +-- examples/widgets/popup_view.enaml | 10 +- 4 files changed, 498 insertions(+), 266 deletions(-) diff --git a/enaml/qt/q_popup_view.py b/enaml/qt/q_popup_view.py index ace7e1cd0..a54e4278a 100644 --- a/enaml/qt/q_popup_view.py +++ b/enaml/qt/q_popup_view.py @@ -5,7 +5,7 @@ # # The full license is in the file COPYING.txt, distributed with this software. #------------------------------------------------------------------------------ -from atom.api import Atom, Typed, Float, Int, IntEnum +from atom.api import Atom, Bool, Typed, Float, Int, IntEnum from .QtCore import ( Qt, QPoint, QPointF, QSize, QRect,QMargins, QPropertyAnimation, QTimer, @@ -51,29 +51,26 @@ class PopupState(Atom): """ A class which maintains the public state for a popup view. """ - #: The anchor location on the view. The default anchors - #: the top center of the view to the center of the parent. - anchor = Typed(QPointF, factory=lambda: QPointF(0.5, 0.0)) + #: The mode to use when computing the anchored position. + anchor_mode = Typed(AnchorMode, factory=lambda: AnchorMode.Parent) #: The anchor location on the parent. The default anchors #: the top center of the view to the center of the parent. parent_anchor = Typed(QPointF, factory=lambda: QPointF(0.5, 0.5)) + #: The anchor location on the view. The default anchors + #: the top center of the view to the center of the parent. + anchor = Typed(QPointF, factory=lambda: QPointF(0.5, 0.0)) + #: The offset of the popup view with respect to the anchor. offset = Typed(QPoint, factory=lambda: QPoint(0, 0)) - #: The mode to use when computing the anchored position. - anchor_mode = Typed(AnchorMode, factory=lambda: AnchorMode.Parent) - #: The edge location of the arrow for the view. arrow_edge = Typed(ArrowEdge, factory=lambda: ArrowEdge.Left) #: The size of the arrow for the view. arrow_size = Int(0) - #: The position of the arrow for the view. - arrow_position = Float(0.5) - #: The timeout value to use when closing the view, in seconds. timeout = Float(0.0) @@ -95,6 +92,12 @@ class PopupState(Atom): #: The timeout timer to use for closing the view. close_timer = Typed(QTimer, ()) + #: Whether or not the view closes on click. + close_on_click = Bool(True) + + #: Whether or not the view is currently in a resize event. + in_resize_event = Bool(False) + def init(self, widget): """ Initialize the state for the owner widget. @@ -111,33 +114,22 @@ def init(self, widget): close_timer.timeout.connect(widget.close) -class LayoutData(Atom): - """ An object which holds the data for popup view layout. - - """ - #: The position of the popup view. - pos = Typed(QPoint) - - #: The size of the popup view. - size = Typed(QSize) - - #: The size of the arrow in pixels. - arrow_size = Int(0) - - #: The edge location of the arrow. - arrow_edge = Typed(ArrowEdge) +def make_path(size, arrow_size, arrow_edge, offset): + """ Create the painter path for the view with an edge arrow. - #: The position of the arrow on the edge. - arrow_position = Float(0.0) + Parameters + ---------- + size : QSize + The size of the view. + arrow_size : int + The size of the arrow. -def make_path(layout_data): - """ Create the painter path for the arrow. + arrow_edge : ArrowEdge + The edge location of the arrow. - Parameters - ---------- - layout_data : LayoutData - The layout data object describing the path to generate. + offset : int + The offset along the arrow edge to the arrow center. Returns ------- @@ -145,16 +137,8 @@ def make_path(layout_data): The painter path for the view. """ - def arrow_offset(length, height, pos): - base = 2 * height - pos = max(0.0, min(1.0, pos)) - base = min(length, base) - return int(pos * (length - base)) + base / 2 - arrow_size = layout_data.arrow_size - arrow_pos = layout_data.arrow_position - edge = layout_data.arrow_edge - w = layout_data.size.width() - h = layout_data.size.height() + w = size.width() + h = size.height() path = QPainterPath() if arrow_size <= 0: path.moveTo(0, 0) @@ -162,8 +146,7 @@ def arrow_offset(length, height, pos): path.lineTo(w, h) path.lineTo(0, h) path.lineTo(0, 0) - elif edge == ArrowEdge.Bottom: - offset = arrow_offset(w, arrow_size, arrow_pos) + elif arrow_edge == ArrowEdge.Bottom: ledge = h - arrow_size path.moveTo(0, 0) path.lineTo(w, 0) @@ -173,8 +156,7 @@ def arrow_offset(length, height, pos): path.lineTo(offset - arrow_size, ledge) path.lineTo(0, ledge) path.lineTo(0, 0) - elif edge == ArrowEdge.Top: - offset = arrow_offset(w, arrow_size, arrow_pos) + elif arrow_edge == ArrowEdge.Top: path.moveTo(0, arrow_size) path.lineTo(offset - arrow_size, arrow_size) path.lineTo(offset, 0) @@ -183,8 +165,7 @@ def arrow_offset(length, height, pos): path.lineTo(w, h) path.lineTo(0, h) path.lineTo(0, arrow_size) - elif edge == ArrowEdge.Left: - offset = arrow_offset(h, arrow_size, arrow_pos) + elif arrow_edge == ArrowEdge.Left: path.moveTo(arrow_size, 0) path.lineTo(w, 0) path.lineTo(w, h) @@ -194,7 +175,6 @@ def arrow_offset(length, height, pos): path.lineTo(arrow_size, offset - arrow_size) path.lineTo(arrow_size, 0) else: - offset = arrow_offset(h, arrow_size, arrow_pos) ledge = w - arrow_size path.moveTo(0, 0) path.lineTo(ledge, 0) @@ -207,136 +187,304 @@ def arrow_offset(length, height, pos): return path -def edge_margins(size, edge): +def edge_margins(arrow_size, arrow_edge): """ Get the contents margins for a given arrow size and edge. Parameters ---------- - size : int + arrow_size : int The size of the arrow in pixels. - edge : ArrowEdge + arrow_edge : ArrowEdge The edge location of the arrow. + Returns + ------- + result : QMargins + The contents margins for the given arrow spec. + """ margins = QMargins() - if size > 0: - if edge == ArrowEdge.Right: - margins.setRight(size) - elif edge == ArrowEdge.Bottom: - margins.setBottom(size) - elif edge == ArrowEdge.Left: - margins.setLeft(size) + if arrow_size > 0: + if arrow_edge == ArrowEdge.Right: + margins.setRight(arrow_size) + elif arrow_edge == ArrowEdge.Bottom: + margins.setBottom(arrow_size) + elif arrow_edge == ArrowEdge.Left: + margins.setLeft(arrow_size) else: - margins.setTop(size) + margins.setTop(arrow_size) return margins -def target_global_pos(parent, mode, anchor): - """ Get the global position of the parent anchor. +def is_fully_on_screen(rect): + """ Get whether or not a rect is fully contained on the screen. Parameters ---------- - parent : QWidget or None - The parent widget for the view. + rect : QRect + The rect of interest. - mode : AnchorMode - The anchor mode for the view. + Returns + ------- + result : bool + True if the rect is fully contained on the screen, False + otherwise. + + """ + desktop = QApplication.desktop() + desk_geo = desktop.availableGeometry(rect.topLeft()) + if not desk_geo.contains(rect.topLeft()): + return False + desk_geo = desktop.availableGeometry(rect.topRight()) + if not desk_geo.contains(rect.topRight()): + return False + desk_geo = desktop.availableGeometry(rect.bottomLeft()) + if not desk_geo.contains(rect.bottomLeft()): + return False + desk_geo = desktop.availableGeometry(rect.bottomRight()) + if not desk_geo.contains(rect.bottomRight()): + return False + return True - anchor : QPoint - The anchor location on the parent. + +def left_screen_edge(desktop, rect): + """ Get the x-coordinate of the effective left screen edge. + + Parameters + ---------- + desktop : QDesktopWidget + The desktop widget for the application. + + rect : QRect + The rect of interest. Returns ------- - result : QPoint - The global coordinates of the target parent anchor. + result : int + the x-coordinate of the effective left screen edge. """ - if mode == AnchorMode.Cursor: - origin = QCursor.pos() - size = QSize() - else: - if parent is None: - desktop = QApplication.desktop() - geo = desktop.availableGeometry() - origin = geo.topLeft() - size = geo.size() - else: - origin = parent.mapToGlobal(QPoint(0, 0)) - size = parent.size() - px = int(anchor.x() * size.width()) - py = int(anchor.y() * size.height()) - return origin + QPoint(px, py) + p1 = rect.topLeft() + p2 = rect.bottomLeft() + g1 = desktop.availableGeometry(p1) + g2 = desktop.availableGeometry(p2) + return max(p1.x(), g1.left(), g2.left()) -def popup_offset(size, anchor, offset): - """ Get the offset to apply to the target global pos. +def right_screen_edge(desktop, rect): + """ Get the x-coordinate of the effective right screen edge. Parameters ---------- - size : QSize - The size of the popup view. + desktop : QDesktopWidget + The desktop widget for the application. - anchor : QPoint - The anchor for the popup view. - - offset : QPoint - The additional offset for the popup view. + rect : QRect + The rect of interest. Returns ------- - result : QPoint - The offset to apply to the global target position to - move the popup to the correct location. + result : int + the x-coordinate of the effective right screen edge. """ - px = int(anchor.x() * size.width()) - py = int(anchor.y() * size.height()) - return QPoint(px, py) - offset + p1 = rect.topRight() + p2 = rect.bottomRight() + g1 = desktop.availableGeometry(p1) + g2 = desktop.availableGeometry(p2) + return min(p1.x(), g1.right(), g2.right()) + + +def top_screen_edge(desktop, rect): + """ Get the y-coordinate of the effective top screen edge. + + Parameters + ---------- + desktop : QDesktopWidget + The desktop widget for the application. + rect : QRect + The rect of interest. -def arrow_tip_pos(layout_data): - """ Compute the position of the arrow point. + Returns + ------- + result : int + the y-coordinate of the effective top screen edge. """ - pos = QPoint(layout_data.pos) - size = layout_data.size - width = size.width() - height = size.height() - arrow_edge = layout_data.arrow_edge - arrow_pos = layout_data.arrow_position - if arrow_edge == ArrowEdge.Top: - pos += QPoint(width * arrow_pos, 0) - elif arrow_edge == ArrowEdge.Right: - pos += QPoint(width, height * arrow_pos) - elif arrow_edge == ArrowEdge.Bottom: - pos += QPoint(width * arrow_pos, height) - else: - pos += QPoint(0, height * arrow_pos) - return pos + p1 = rect.topLeft() + p2 = rect.topRight() + g1 = desktop.availableGeometry(p1) + g2 = desktop.availableGeometry(p2) + return max(p1.y(), g1.top(), g2.top()) -def ensure_on_screen(layout_data): +def bottom_screen_edge(desktop, rect): + """ Get the y-coordinate of the effective bottom screen edge. + + Parameters + ---------- + desktop : QDesktopWidget + The desktop widget for the application. + + rect : QRect + The rect of interest. + + Returns + ------- + result : int + the y-coordinate of the effective bottom screen edge. + """ + p1 = rect.bottomLeft() + p2 = rect.bottomRight() + g1 = desktop.availableGeometry(p1) + g2 = desktop.availableGeometry(p2) + return min(p1.y(), g1.bottom(), g2.bottom()) + + +def ensure_on_screen(rect): + """ Ensure that the given rectangle is fully on-screen. + + If the given rectangle does fit on the screen its position will be + adjusted so that it fits on screen as close as possible to its + original position. Rects which are bigger than the screen size + will necessarily still incur clipping. + + Parameters + ---------- + rect : QRect + The global geometry rectangle of interest. + + Returns + ------- + result : QRect + A potentially adjust rect which best fits on the screen. """ - return False - # tip_pos = arrow_tip_pos(layout_data) - # screen_geo = QApplication.desktop().availableGeometry(tip_pos) - # view_geo = QRect(layout_data.pos, layout_data.size) - # if screen_geo.contains(view_geo): - # return False - # sides = [] - # size = layout_data.size - # if _test_top(view_geo, ) - # if view_geo.width() > screen_geo.width(): - # return False - # if view_geo.height() > screen_geo.height(): - # return False - # if view_geo.x() < screen_geo.x(): - # dx = screen_geo.x() - view_geo.x() + rect = QRect(rect) + desktop = QApplication.desktop() + desk_geo = desktop.availableGeometry(rect.topLeft()) + if desk_geo.contains(rect): + return rect + bottom_edge = bottom_screen_edge(desktop, rect) + if rect.bottom() > bottom_edge: + rect.moveBottom(bottom_edge) + right_edge = right_screen_edge(desktop, rect) + if rect.right() > right_edge: + rect.moveRight(right_edge) + top_edge = top_screen_edge(desktop, rect) + if rect.top() < top_edge: + rect.moveTop(top_edge) + left_edge = left_screen_edge(desktop, rect) + if rect.left() < left_edge: + rect.moveLeft(left_edge) + return rect + + +def adjust_arrow_rect(rect, arrow_edge, target_pos, offset): + """ Adjust an arrow rectangle to fit on the screen. + + Parameters + ---------- + rect : QRect + The rect of interest. + + arrow_edge : ArrowEdge + The edge on which the arrow will be rendered. + target_pos : QPoint + The global target position of the parent anchor. + offset : QPoint + The offset to apply to the parent anchor. + + Returns + ------- + result : tuple + A 4-tuple of (QRect, ArrowEdge, int, int) which represent the + adjusted rect, the new arrow edge, and x and y deltas to apply + to the arrow position. + + """ + ax = ay = 0 + rect = QRect(rect) + desktop = QApplication.desktop() + + if arrow_edge == ArrowEdge.Left: + bottom_edge = bottom_screen_edge(desktop, rect) + if rect.bottom() > bottom_edge: + ay += rect.bottom() - bottom_edge + rect.moveBottom(bottom_edge) + top_edge = top_screen_edge(desktop, rect) + if rect.top() < top_edge: + ay -= top_edge - rect.top() + rect.moveTop(top_edge) + left_edge = left_screen_edge(desktop, rect) + if rect.left() < left_edge: + rect.moveLeft(left_edge) + right_edge = right_screen_edge(desktop, rect) + if rect.right() > right_edge: + arrow_edge = ArrowEdge.Right + right = target_pos.x() - offset.x() + rect.moveRight(min(right, right_edge)) + + elif arrow_edge == ArrowEdge.Top: + right_edge = right_screen_edge(desktop, rect) + if rect.right() > right_edge: + ax += rect.right() - right_edge + rect.moveRight(right_edge) + left_edge = left_screen_edge(desktop, rect) + if rect.left() < left_edge: + ax -= left_edge - rect.left() + rect.moveLeft(left_edge) + top_edge = top_screen_edge(desktop, rect) + if rect.top() < top_edge: + rect.moveTop(top_edge) + bottom_edge = bottom_screen_edge(desktop, rect) + if rect.bottom() > bottom_edge: + arrow_edge = ArrowEdge.Bottom + bottom = target_pos.y() - offset.y() + rect.moveBottom(min(bottom, bottom_edge)) + + elif arrow_edge == ArrowEdge.Right: + bottom_edge = bottom_screen_edge(desktop, rect) + if rect.bottom() > bottom_edge: + ay += rect.bottom() - bottom_edge + rect.moveBottom(bottom_edge) + top_edge = top_screen_edge(desktop, rect) + if rect.top() < top_edge: + ay -= top_edge - rect.top() + rect.moveTop(top_edge) + right_edge = right_screen_edge(desktop, rect) + if rect.right() > right_edge: + rect.moveRight(right_edge) + left_edge = left_screen_edge(desktop, rect) + if rect.left() < left_edge: + arrow_edge = ArrowEdge.Left + left = target_pos.x() - offset.x() + rect.moveLeft(max(left, left_edge)) + + else: # ArrowEdge.Bottom + right_edge = right_screen_edge(desktop, rect) + if rect.right() > right_edge: + ax += rect.right() - right_edge + rect.moveRight(right_edge) + left_edge = left_screen_edge(desktop, rect) + if rect.left() < left_edge: + ax -= left_edge - rect.left() + rect.moveLeft(left_edge) + bottom_edge = bottom_screen_edge(desktop, rect) + if rect.bottom() > bottom_edge: + rect.moveBottom(bottom_edge) + top_edge = top_screen_edge(desktop, rect) + if rect.top() < top_edge: + arrow_edge = ArrowEdge.Top + top = target_pos.y() - offset.y() + rect.moveTop(max(top, top_edge)) + + return rect, arrow_edge, ax, ay class QPopupView(QWidget): @@ -372,6 +520,8 @@ def __init__(self, parent=None, flags=Qt.Popup): self._state.init(self) if parent is not None: parent.installEventFilter(self) + if not parent.isWindow(): + parent.window().installEventFilter(self) #-------------------------------------------------------------------------- # Public API @@ -399,54 +549,54 @@ def setCentralWidget(self, widget): """ self.layout().setWidget(widget) - def anchor(self): - """ Get the anchor position for the popup view. + def parentAnchor(self): + """ Get the parent anchor position for the popup view. Returns ------- result : QPointF - The anchor point for the view. + The parent anchor point for the view. """ - return self._state.anchor + return self._state.parent_anchor - def setAnchor(self, anchor): - """ Set the anchor position for the popup view. + def setParentAnchor(self, anchor): + """ Set the parent anchor position for the popup view. Parameters ---------- anchor : QPointF - The anchor point for the view. + The parent anchor point for the view. """ state = self._state - if anchor != state.anchor: - state.anchor = anchor + if anchor != state.parent_anchor: + state.parent_anchor = anchor self._refreshGeometry() - def parentAnchor(self): - """ Get the parent anchor position for the popup view. + def anchor(self): + """ Get the anchor position for the popup view. Returns ------- result : QPointF - The parent anchor point for the view. + The anchor point for the view. """ - return self._state.parent_anchor + return self._state.anchor - def setParentAnchor(self, anchor): - """ Set the parent anchor position for the popup view. + def setAnchor(self, anchor): + """ Set the anchor position for the popup view. Parameters ---------- anchor : QPointF - The parent anchor point for the view. + The anchor point for the view. """ state = self._state - if anchor != state.parent_anchor: - state.parent_anchor = anchor + if anchor != state.anchor: + state.anchor = anchor self._refreshGeometry() def offset(self): @@ -550,31 +700,6 @@ def setArrowSize(self, size): state.arrow_size = size self._refreshGeometry() - def arrowPosition(self): - """ Get the position of the popup arrow. - - Returns - ------- - result : float - The position of the arrow along its edge. - - """ - return self._state.arrow_position - - def setArrowPosition(self, pos): - """ Set the position of the popup arrow. - - Parameters - ---------- - pos : float - The position of the popup arrow along its edge. - - """ - state = self._state - if pos != state.arrow_position: - state.arrow_position = pos - self._refreshGeometry() - def timeout(self): """ Get the timeout for the view. @@ -641,6 +766,29 @@ def setFadeOutDuration(self, duration): """ self._state.fade_out_duration = duration + def closeOnClick(self): + """ Get whether or not close on click is enabled. + + Returns + ------- + result : bool + True if close on click is enabled, False otherwise. The + default value is True. + + """ + return self._state.close_on_click + + def setCloseOnClick(self, enable): + """ Set whether or not close on click is enabled. + + Parameters + ---------- + enable : bool + True if close on click should be enabled, False otherwise. + + """ + self._state.close_on_click = enable + #-------------------------------------------------------------------------- # Event Handlers #-------------------------------------------------------------------------- @@ -653,9 +801,7 @@ def eventFilter(self, obj, event): """ evt_type = event.type() if evt_type == QEvent.Move or evt_type == QEvent.Resize: - if obj is self.parent(): - #self._updatePosition() - self._refreshGeometry() + self._refreshGeometry() return False def mousePressEvent(self, event): @@ -665,21 +811,25 @@ def mousePressEvent(self, event): cases, when the superclass method doesn't handle it. """ - super(QPopupView, self).mousePressEvent(event) - if self.isVisible(): + event.ignore() + state = self._state + if state.close_on_click: + path = state.path pos = event.pos() rect = self.rect() - if rect.contains(pos): - pt = QPointF(pos.x(), pos.y()) - win_type = self.windowType() - if win_type == Qt.ToolTip: - if self._state.path.contains(pt): - event.accept() - self.close() - elif win_type == Qt.Popup: - if not self._state.path.contains(pt): + win_type = self.windowType() + if win_type == Qt.Popup: + if not rect.contains(pos): + super(QPopupView, self).mousePressEvent(event) + else: + path = state.path + if not path.isEmpty() and not path.contains(pos): event.accept() self.close() + elif win_type == Qt.ToolTip: + if path.isEmpty() or path.contains(pos): + event.accept() + self.close() def showEvent(self, event): """ Handle the show event for the popup view. @@ -736,7 +886,13 @@ def resizeEvent(self, event): """ super(QPopupView, self).resizeEvent(event) - self._refreshGeometry() + if self._state.in_resize_event: + return + self._state.in_resize_event = True + try: + self._refreshGeometry() + finally: + self._state.in_resize_event = False def paintEvent(self, event): """ Handle the paint event for the popup view. @@ -770,48 +926,123 @@ def _refreshGeometry(self, force=False): """ if not force and not self.isVisible(): return + if self._state.arrow_size <= 0: + self._layoutPlainRect() + else: + self._layoutArrowRect() - # Compute the margins as specified by the state. + def _layoutPlainRect(self): + """ Layout the widget with no edge arrow. + + """ + self.setContentsMargins(QMargins()) + self.clearMask() + target_pos = self._targetPos() + anchor_pos = self._anchorPos() + offset = self._state.offset + trial_pos = target_pos + offset - anchor_pos + trial_geo = QRect(trial_pos, self.size()) + geo = ensure_on_screen(trial_geo) + self.setGeometry(geo) + + def _layoutArrowRect(self): + """ Layout the widget with the edge arrow. + + """ + # Setup the initial contents margins. state = self._state - arrow_edge = state.arrow_edge arrow_size = state.arrow_size + arrow_edge = state.arrow_edge margins = edge_margins(arrow_size, arrow_edge) - - # Compute the hypothetical view size. self.setContentsMargins(margins) + + # Use the current size to compute the arrow position. + ax = ay = 0 size = self.size() + anchor = state.anchor + if arrow_edge == ArrowEdge.Left or arrow_edge == ArrowEdge.Right: + ay = int(anchor.y() * size.height()) + ay = max(arrow_size, min(size.height() - arrow_size, ay)) + if arrow_edge == ArrowEdge.Right: + ax = size.width() + else: + ax = int(anchor.x() * size.width()) + ax = max(arrow_size, min(size.width() - arrow_size, ax)) + if arrow_edge == ArrowEdge.Bottom: + ay = size.height() + + # Compute the view rect and adjust it if it falls off screen. + target_pos = self._targetPos() + pos = target_pos + state.offset - QPoint(ax, ay) + rect = QRect(pos, size) + if not is_fully_on_screen(rect): + rect, new_edge, d_ax, d_ay = adjust_arrow_rect( + rect, arrow_edge, target_pos, state.offset + ) + ax = max(arrow_size, min(size.width() - arrow_size, ax + d_ax)) + ay = max(arrow_size, min(size.height() - arrow_size, ay + d_ay)) + if new_edge != arrow_edge: + arrow_edge = new_edge + margins = edge_margins(arrow_size, new_edge) + self.setContentsMargins(margins) + + # Use the final geometry to get the path for the arrow. + if arrow_edge == ArrowEdge.Left or arrow_edge == ArrowEdge.Right: + path = make_path(rect.size(), arrow_size, arrow_edge, ay) + else: + path = make_path(rect.size(), arrow_size, arrow_edge, ax) - # Compute the hypothetical view position. - anchor_mode = state.anchor_mode - parent_anchor = state.parent_anchor - pos = target_global_pos(self.parent(), anchor_mode, parent_anchor) - pos -= popup_offset(size, state.anchor, state.offset) - - # Create the layout data and ensure it the view is on screen. - layout_data = LayoutData() - layout_data.pos = pos - layout_data.size = size - layout_data.arrow_size = arrow_size - layout_data.arrow_edge = arrow_edge - layout_data.arrow_position = state.arrow_position - changed = ensure_on_screen(layout_data) - - # If the layout has changed, apply the update. - if changed: - arrow_size = layout_data.arrow_size - arrow_edge = layout_data.arrow_edge - margins = edge_margins(arrow_size, arrow_edge) - self.setContentsMargins(margins) - layout_data.size = self.size() - - # Compute the painter path for the view and set the mask. - path = make_path(layout_data) + # Store the path for painting and update the widget mask. state.path = path mask = QRegion(path.toFillPolygon().toPolygon()) self.setMask(mask) - # Move the widget into position and update it. The update is - # necessary in the case where only the mask has changed, which - # does not automatically generate a paint event. - self.move(layout_data.pos) + # Set the geometry of the view and update. The update is needed + # for the case where the only change was the widget mask, which + # will not generate a paint event. Qt collapses paint events, + # so the cost of this is minimal. + self.setGeometry(rect) self.update() + + def _targetPos(self): + """ Get the global position of the parent anchor. + + Returns + ------- + result : QPoint + The global coordinates of the target parent anchor. + + """ + state = self._state + if state.anchor_mode == AnchorMode.Cursor: + origin = QCursor.pos() + size = QSize() + else: + parent = self.parent() + if parent is None: + desktop = QApplication.desktop() + geo = desktop.availableGeometry() + origin = geo.topLeft() + size = geo.size() + else: + origin = parent.mapToGlobal(QPoint(0, 0)) + size = parent.size() + anchor = state.parent_anchor + px = int(anchor.x() * size.width()) + py = int(anchor.y() * size.height()) + return origin + QPoint(px, py) + + def _anchorPos(self): + """ Get the position of the anchor in local coordinates. + + Returns + ------- + result : QPoint + The anchor position on the view in local coordinates. + + """ + size = self.size() + anchor = self._state.anchor + px = int(anchor.x() * size.width()) + py = int(anchor.y() * size.height()) + return QPoint(px, py) diff --git a/enaml/qt/qt_popup_view.py b/enaml/qt/qt_popup_view.py index 6014dead2..0a219788b 100644 --- a/enaml/qt/qt_popup_view.py +++ b/enaml/qt/qt_popup_view.py @@ -66,11 +66,11 @@ def init_widget(self): self.set_parent_anchor(d.parent_anchor) self.set_arrow_size(d.arrow_size) self.set_arrow_edge(d.arrow_edge) - self.set_arrow_position(d.arrow_position) self.set_offset(d.offset) self.set_timeout(d.timeout) self.set_fade_in_duration(d.fade_in_duration) self.set_fade_out_duration(d.fade_out_duration) + self.set_close_on_click(d.close_on_click) self.widget.closed.connect(self.on_closed) def init_layout(self): @@ -144,12 +144,6 @@ def set_arrow_edge(self, edge): """ self.widget.setArrowEdge(EDGES[edge]) - def set_arrow_position(self, pos): - """ Set the position of the arrow on the underlying widget. - - """ - self.widget.setArrowPosition(pos) - def set_offset(self, offset): """ Set the offset of the underlying widget. @@ -174,6 +168,12 @@ def set_fade_out_duration(self, duration): """ self.widget.setFadeOutDuration(duration) + def set_close_on_click(self, enable): + """ Set the close on click flag for the underlying widget. + + """ + self.widget.setCloseOnClick(enable) + def close(self): """ Close the underlying popup widget. diff --git a/enaml/widgets/popup_view.py b/enaml/widgets/popup_view.py index 50be4eb03..7f3d82331 100644 --- a/enaml/widgets/popup_view.py +++ b/enaml/widgets/popup_view.py @@ -64,9 +64,6 @@ def set_arrow_size(self, size): def set_arrow_edge(self, edget): raise NotImplementedError - def set_arrow_position(self, pos): - raise NotImplementedError - def set_offset(self, pos): raise NotImplementedError @@ -79,6 +76,9 @@ def set_fade_in_duration(self, duration): def set_fade_out_duration(self, duration): raise NotImplementedError + def set_close_on_click(self, enable): + raise NotImplementedError + def close(self): raise NotImplementedError @@ -105,15 +105,9 @@ class PopupView(Widget): #: The window type cannot be changed once the widget is created. window_type = d_(Enum('popup', 'tool_tip')) - #: The relative position on the view to use as the anchor. This - #: anchor will be aligned with the parent anchor to position the - #: popup view. It is expressed as a percentage of the view size. - #: The default anchors will position the popup just below the - #: center of the parent widget. - anchor = d_(Coerced(PosF, (0.5, 0.0), coercer=coerce_posf)) - - #: If the anchor mode is cursor, ignore the parent and use the cursor - #: position for the popup + #: The mode to use for anchoring. The 'parent' mode uses a point + #: on the parent or the desktop as the target anchor, the 'cursor' + #: mode uses the current cursor position as the target anchor. anchor_mode = d_(Enum('parent', 'cursor')) #: The relative position on the parent to use as the anchor. This @@ -123,19 +117,26 @@ class PopupView(Widget): #: center of the parent widget. parent_anchor = d_(Coerced(PosF, (0.5, 0.5), coercer=coerce_posf)) - #: The size of the arrow in pixels. Zero size indicates no anchor. - arrow_size = d_(Int(0)) + #: The relative position on the view to use as the view anchor. + #: This anchor will be aligned with the parent anchor to position + #: the popup view. It is expressed as a percentage of the view + #: size. The default anchors will position the popup just below + #: the center of the parent widget. + anchor = d_(Coerced(PosF, (0.5, 0.0), coercer=coerce_posf)) - #: The edge of the popup view to use for drawing the arrow. - arrow_edge = d_(Enum('top', 'bottom', 'left', 'right')) + #: The offset to apply between the two anchors, in pixels. + offset = d_(Coerced(Pos, (0, 0), coercer=coerce_pos)) - #: The position of the arrow along the arrow edge. This is expressed - #: as a percentage of the arrow edge size. - arrow_position = d_(Float(0.5)) + #: The edge of the popup view to use for rendering the arrow. + arrow_edge = d_(Enum('top', 'bottom', 'left', 'right')) - #: The adjustment to apply to the final anchored position. This - #: is expressed as an offset in pixel coordinates. - offset = d_(Coerced(Pos, (0, 0), coercer=coerce_pos)) + #: The size of the arrow in pixels. If this value is > 0, the view + #: anchor is taken to be the point of the arrow. If the arrow edge + #: is 'left' or 'right', the anchor's y-coordinate is used to set + #: the arrow position, and the x-coordinate is ignored. If the + #: arrow edge is 'top' or 'bottom', the anchor's x-coordinate is + #: used to set the arrow position, and the y-coordinate is ignored. + arrow_size = d_(Int(0)) #: The timeout, in seconds, before automatically closing the popup. #: A value less than or equal to zero means no timeout. This is @@ -150,6 +151,11 @@ class PopupView(Widget): #: or equal to zero means no fade. fade_out_duration = d_(Int(100)) + #: Whether or not close the popup view on a mouse click. For 'popup' + #: windows, this means clicking outside of the view. For 'tool_tip' + #: windows, this means clicking inside of the view. + close_on_click = d_(Bool(True)) + #: Whether or not the background of the popup view is translucent. #: This must be True in order to use background colors with alpha #: and for the fade in and out animation to have effect. This value @@ -167,6 +173,9 @@ class PopupView(Widget): #: A reference to the ProxyPopupView object. proxy = Typed(ProxyPopupView) + #: This attribute is deprecated and will be removed in Enaml 1.0 + arrow_position = d_(Float(0.5)) + #-------------------------------------------------------------------------- # Public API #-------------------------------------------------------------------------- @@ -208,9 +217,9 @@ def close(self): #-------------------------------------------------------------------------- # Observers #-------------------------------------------------------------------------- - @observe('anchor', 'anchor_mode', 'parent_anchor', 'arrow_size', - 'arrow_edge', 'arrow_position', 'offset', 'timeout', - 'fade_in_duration', 'fade_out_duration') + @observe('anchor', 'anchor_mode', 'parent_anchor', 'arrow_size', 'offset', + 'arrow_edge', 'timeout', 'fade_in_duration', 'fade_out_duration', + 'close_on_click') def _update_proxy(self, change): """ Update the proxy when the PopupView data changes. diff --git a/examples/widgets/popup_view.enaml b/examples/widgets/popup_view.enaml index c8fc0b981..a3ffe1a8d 100644 --- a/examples/widgets/popup_view.enaml +++ b/examples/widgets/popup_view.enaml @@ -38,7 +38,6 @@ enamldef ConfigPopup(PopupView): popup: anchor << POSITIONS[view_box.selected_item] arrow_size << sizer.value arrow_edge << arrow_edge.selected_item - arrow_position << positioner.value / 100.0 offset << (offset_x.value, offset_y.value) Form: padding = 20 @@ -46,16 +45,9 @@ enamldef ConfigPopup(PopupView): popup: foreground = 'white' text = 'Arrow Size' Slider: sizer: - minimum = 0 + minimum = 5 maximum = 30 value = 20 - Label: - foreground = 'white' - text = 'Arrow Position' - Slider: positioner: - minimum = 0 - maximum = 100 - value = 50 Label: foreground = 'white' text = 'Arrow Edge'