def ensure_on_screen(rect): """ Ensure that the given rect is contained on screen. If the origin of the rect is not contained within the closest desktop screen, the rect will be moved so that it is fully on the closest screen. If the rect is larger than the closest screen, the origin will never be less than the screen origin. Parameters ---------- rect : QRect The geometry rect of interest. Returns ------- result : QRect The potentially adjusted QRect which fits on the screen. """ d = QApplication.desktop() pos = rect.topLeft() drect = d.screenGeometry(pos) if not drect.contains(pos): x = pos.x() if x < drect.x() or x > drect.right(): dw = drect.width() - rect.width() x = max(drect.x(), drect.x() + dw) y = pos.y() if x < drect.top() or y > drect.bottom(): dh = drect.height() - rect.height() y = max(drect.y(), drect.y() + dh) rect = QRect(x, y, rect.width(), rect.height()) return rect
def drawBitmap(self, bmp, opt, painter): """ Draw the bitmap for the button. The bitmap will be drawn with the foreground color set by the style sheet and the style option. Parameters ---------- bmp : QBitmap The bitmap to draw. opt : QStyleOption The style option to use for drawing. painter : QPainter The painter to use for drawing. """ # hack to get the current stylesheet foreground color hint = QStyle.SH_GroupBox_TextLabelColor fg = self.style().styleHint(hint, opt, self) # mask signed to unsigned which 'fromRgba' requires painter.setPen(QColor.fromRgba(0xffffffff & fg)) size = self.size() im_size = bmp.size() x = size.width() / 2 - im_size.width() / 2 y = size.height() / 2 - im_size.height() / 2 source = QRect(QPoint(0, 0), im_size) dest = QRect(QPoint(x, y), im_size) painter.drawPixmap(dest, bmp, source)
def close_window(self, window, event): """ Handle a close request for a QDockWindow. This method is called by the framework at the appropriate times and should not be called directly by user code. Parameters ---------- window : QDockWindow The dock window to close. event : QCloseEvent The close event passed to the event handler. """ area = window.dockArea() if area is not None: containers = list(iter_containers(area)) geometries = {} for container in containers: pos = container.mapToGlobal(QPoint(0, 0)) size = container.size() geometries[container] = QRect(pos, size) for container, ignored in area.dockBarContainers(): containers.append(container) size = container.sizeHint() geometries[container] = QRect(window.pos(), size) for container in containers: if not container.close(): container.unplug() container.float() container.setGeometry(geometries[container]) container.show() self._free_window(window)
def layout(self, pos): """ Layout the guides for the given position. Parameters ---------- pos : QPoint The center point of the guide. """ x = pos.x() y = pos.y() self._guide.rect = QRect(x - 15, y - 15, 31, 31) self._box.rect = QRect(x - 20, y - 20, 41, 41)
def paint(self, painter, option, index): self.initStyleOption(option, index) painter.save() left_width = int(option.rect.width() * self.model.cellFrac(index)) right_width = option.rect.width() - left_width left_rect = QRect(option.rect.left(), option.rect.top(), left_width, option.rect.height()) right_rect = QRect(option.rect.left(), option.rect.top(), right_width, option.rect.height()) left_brush = QBrush(self.model.cellColor(index)) painter.fillRect(left_rect, left_brush) painter.fillRect(right_rect, Qt.NoBrush) painter.restore() option.backgroundBrush = QBrush(Qt.NoBrush) super().paint(painter, option, index)
def titleBarGeometry(self): """ Get the geometry rect for the title bar. Returns ------- result : QRect The geometry rect for the title bar, expressed in frame coordinates. An invalid rect is returned if title bar should not be active. """ title_bar = self.dockItem().titleBarWidget() if title_bar.isHidden(): return QRect() pt = title_bar.mapTo(self, QPoint(0, 0)) return QRect(pt, title_bar.size())
def _onHandleMoved(self, delta): """ Handle the 'handleMoved' signal on the item handle. This handler resizes the item by the delta and then updates the internal user size. The resize is bounded by the limits of the widget and the parent dock area size. Resizing is disabled if an animation is running. """ animation = self._animation if animation and animation.state() == animation.Running: return p = self.position() if p == QDockBar.North: delta = QSize(0, delta.y()) elif p == QDockBar.East: delta = QSize(-delta.x(), 0) elif p == QDockBar.South: delta = QSize(0, -delta.y()) else: delta = QSize(delta.x(), 0) user_size = self.size() + delta user_size = user_size.expandedTo(self.minimumSize()) parent = self.parent() if parent is not None: user_size = user_size.boundedTo(parent.size()) self._user_size = user_size if p == QDockBar.East or p == QDockBar.South: d = user_size - self.size() p = self.pos() - QPoint(d.width(), d.height()) self.setGeometry(QRect(p, user_size)) else: self.resize(user_size)
def mouse_over_widget(self, widget, pos, empty=False): """ Update the overlays based on the mouse position. This handler should be invoked when the mouse hovers over a single widget (such as a floating dock container) as opposed to an area of docked widgets. The guide rose will be displayed in the center of the widget with no border guides. Parameters ---------- widget : QWidget The widget under the mouse. pos : QPoint The hover position, expressed in the local coordinates of the widget. empty : bool, optional Whether the widget represents an empty widget. If this is True, a single center guide will be shown instead of the guide rose. """ Mode = QGuideRose.Mode rose = self._rose target_mode = Mode.AreaCenter if empty else Mode.CompassEx self._target_rose_mode = target_mode if rose.mode() != target_mode: rose.setMode(Mode.NoMode) self._rose_timer.start(self.rose_delay) self._band_timer.start(self.band_delay) origin = widget.mapToGlobal(QPoint(0, 0)) geo = QRect(origin, widget.size()) dirty = rose.geometry() != geo if dirty: rose.hide() rose.setMode(Mode.NoMode) rose.setGeometry(geo) guide = rose.guideAt(pos, target_mode) if dirty or guide != self._last_guide: self._last_guide = guide self._target_band_geo = self._band_geometry(widget, guide) self._band_timer.start(self.band_delay) rose.setCenterPoint(QPoint(geo.width() / 2, geo.height() / 2)) rose.mouseOver(pos) rose.show()
def paintEvent(self, event): self.proxy.ais_context.UpdateCurrentViewer() # important to allow overpainting of the OCC OpenGL context in Qt if self._drawbox: painter = QPainter(self) painter.setPen(self._select_pen) painter.drawRect(QRect(*self._drawbox))
def drawBox(self): if self._drawbox: self.makeCurrent() painter = QPainter(self) painter.setPen(self._select_pen) painter.drawRect(QRect(*self._drawbox)) painter.end() self.doneCurrent()
def titleBarGeometry(self): """ Get the geometry rect for the title bar. Returns ------- result : QRect The geometry rect for the title bar, expressed in frame coordinates. An invalid rect is returned if title bar should not be active. """ cmargins = self.layout().contentsMargins() if self.isMaximized(): return QRect(0, 0, self.width(), cmargins.top()) rmargins = self.ResizeMargins width = self.width() - (cmargins.left() + cmargins.right()) height = cmargins.top() - rmargins.top() return QRect(cmargins.left(), rmargins.top(), width, height)
def setGeometry(self, rect): """ Set the geometry for the items in the layout. """ super(QDockItemLayout, self).setGeometry(rect) title = self._title_bar widget = self._dock_widget title_rect = QRect(rect) widget_rect = QRect(rect) if title is not None and not title.isHidden(): msh = title.minimumSizeHint() title_rect.setHeight(msh.height()) widget_rect.setTop(title_rect.bottom() + 1) title.setGeometry(title_rect) if widget is not None and not widget.isHidden(): widget.setGeometry(widget_rect)
def dockBarGeometry(self, position): """ Get the geometry of the dock bar at the given position. Parameters ---------- position : QDockBar.Position The enum value specifying the dock bar of interest. Returns ------- result : QRect The geometry of the given dock bar expressed in area coordinates. If no dock bar exists at the given position, an invalid QRect will be returned. """ bar = self._getDockBar(position, create=False) if bar is None: return QRect() pos = bar.mapTo(self.parent(), QPoint(0, 0)) return QRect(pos, bar.size())
def titleBarGeometry(self): """ Get the geometry rect for the title bar. Returns ------- result : QRect The geometry rect for the title bar, expressed in frame coordinates. An invalid rect should be returned if title bar should not be active. """ return QRect()
def layout(self, pos): """ Layout the guides for the extended compass. Parameters ---------- pos : QPoint The center point of the compass. """ x = pos.x() y = pos.y() Guide = QGuideRose.Guide guides = self._guides guides[Guide.CompassNorth].rect = QRect(x - 15, y - 64, 31, 31) guides[Guide.CompassEast].rect = QRect(x + 34, y - 15, 31, 31) guides[Guide.CompassSouth].rect = QRect(x - 15, y + 34, 31, 31) guides[Guide.CompassWest].rect = QRect(x - 64, y - 15, 31, 31) guides[Guide.CompassCenter].rect = QRect(x - 15, y - 15, 31, 31) guides[Guide.CompassExNorth].rect = QRect(x - 15, y - 29, 31, 10) guides[Guide.CompassExEast].rect = QRect(x + 20, y - 15, 10, 31) guides[Guide.CompassExSouth].rect = QRect(x - 15, y + 20, 31, 10) guides[Guide.CompassExWest].rect = QRect(x - 29, y - 15, 10, 31) self._box.rect = QRect(x - 69, y - 69, 139, 139)
def _updateButtonGeometry(self): """ Update the geometry of the window buttons. This method will set the geometry of the window buttons according to the current window size. """ title_buttons = self._title_buttons size = title_buttons.minimumSizeHint() margins = self.layout().contentsMargins() offset = max(self.MinButtonOffset, margins.right()) x = self.width() - size.width() - offset rect = QRect(x, 1, size.width(), size.height()) title_buttons.setGeometry(rect)
def _animationGeo(self, item): """ Get the animation geometry for the given item. Parameters ---------- item : QDockBarItem The dock bar item to be animated. Returns ------- result : tuple A 2-tuple of QRect objects representing the start and end geometries for the animation assuming a slide out effect. """ pane = self.parent().centralPane() hint = item.sizeHint().boundedTo(pane.size()) position = item.position() if position == QDockBar.North: start_pos = QPoint(0, -hint.height()) end_pos = QPoint(0, 0) size = QSize(pane.width(), hint.height()) elif position == QDockBar.East: start_pos = QPoint(pane.width(), 0) end_pos = QPoint(pane.width() - hint.width(), 0) size = QSize(hint.width(), pane.height()) elif position == QDockBar.South: start_pos = QPoint(0, pane.height()) end_pos = QPoint(0, pane.height() - hint.height()) size = QSize(pane.width(), hint.height()) else: start_pos = QPoint(-hint.width(), 0) end_pos = QPoint(0, 0) size = QSize(hint.width(), pane.height()) start_geo = QRect(start_pos, size) end_geo = QRect(end_pos, size) return start_geo, end_geo
def init_floating_frame(self, frame, layout): """ Initialize a floating frame. This initializer sets up the geometry, maximized state, and linked state for the floating frame. Parameters ---------- frame : QDockFrame The floating dock frame of interest. layout : ItemLayout or AreaLayout The layout describing the floating state of the frame. """ rect = QRect(*layout.geometry) if rect.isValid(): rect = ensure_on_screen(rect) frame.setGeometry(rect) frame.show() if layout.linked: frame.setLinked(True) if layout.maximized: frame.showMaximized()
def layout(self, pos): """ Layout the guides for the given position. Parameters ---------- pos : QPoint The center point of the compass. """ x = pos.x() y = pos.y() Guide = QGuideRose.Guide guides = self._guides guides[Guide.CompassNorth].rect = QRect(x - 15, y - 50, 31, 31) guides[Guide.CompassEast].rect = QRect(x + 20, y - 15, 31, 31) guides[Guide.CompassSouth].rect = QRect(x - 15, y + 20, 31, 31) guides[Guide.CompassWest].rect = QRect(x - 50, y - 15, 31, 31) guides[Guide.CompassCenter].rect = QRect(x - 15, y - 15, 31, 31) self._box.rect = QRect(x - 55, y - 55, 111, 111)
class GuideImage(Atom): """ A class which manages the painting of a guide image. """ #: The default alpha value for guide transparency. TRANSPARENT = 0.60 #: The default alpha value for no guide transparency. OPAQUE = 1.0 #: The QImage to use when painting the guide. image = Typed(QImage, factory=lambda: QImage()) #: The QRect specifying where to draw the image. rect = Typed(QRect, factory=lambda: QRect()) #: The opacity to use when drawing the image. opacity = Float(TRANSPARENT) #: A cache of QImage instances for the loaded guide images. _images = {} @classmethod def load_image(cls, name): """ Load the guide image for the given name into a QImage. This function is hard-coded to return the named .png image from the ./dockguides directory located alongside this file. It is not a generic image loading routine. """ image = cls._images.get(name) if image is None: image = QImage(':dock_images/%s.png' % name) cls._images[name] = image return image def __init__(self, name): """ Initialize a GuideImage. Parameters ---------- name : string The name of the image to load for the guide. """ self.image = self.load_image(name) def opacify(self): """ Make the guide image opaque. """ self.opacity = self.OPAQUE def transparentize(self): """ Make the guide image transparent. """ self.opacity = self.TRANSPARENT def contains(self, point): """ Test whether the image contains a point. Parameters ---------- rect : QPoint The rect to test for containment. Returns ------- result : bool True if the image contains the point, False otherwise. """ return self.rect.contains(point) def paint(self, painter): """ Paint the image using the given painter. Parameters ---------- painter : QPainter An active QPainter to use for drawing the image. If the image is a null image, painting will be skipped. """ image = self.image if image.isNull(): return painter.save() painter.setOpacity(self.opacity) painter.drawImage(self.rect, image) painter.restore()
def apply_layout(self, layout): """ Apply a layout to the dock area. Parameters ---------- layout : docklayout The docklayout to apply to the managed area. """ # Remove the layout widget before resetting the handlers. This # prevents a re-used container from being hidden by the call to # setLayoutWidget after it has already been reset. The reference # is held to the old widget so the containers are not destroyed # before they are reset. widget = self._dock_area.layoutWidget() self._dock_area.setLayoutWidget(None) containers = list(self._dock_containers()) for container in containers: container.reset() for window in list(self._dock_windows()): window.close() # Emit a warning for an item referenced in the layout which # has not been added to the dock manager. names = set(container.objectName() for container in containers) filter_func = lambda item: isinstance(item, dockitem) for item in filter(filter_func, layout.traverse()): if item.name not in names: msg = "dock item '%s' was not found in the dock manager" warnings.warn(msg % item.name, stacklevel=2) # A convenience closure for populating a dock area. def popuplate_area(area, layout): widget = build_layout(layout.child, containers) area.setLayoutWidget(widget) if layout.maximized_item: maxed = self._find_container(layout.maximized_item) if maxed is not None: maxed.showMaximized() # Setup the layout for the primary dock area widget. primary = layout.primary if primary is not None: if isinstance(primary, dockarea): popuplate_area(self._dock_area, primary) else: widget = build_layout(primary, containers) self._dock_area.setLayoutWidget(widget) # Setup the layout for the secondary floating dock area. This # classifies the secondary items according to their type as # each type has subtle differences in how they area handled. single_items = [] single_areas = [] multi_areas = [] for secondary in layout.secondary: if isinstance(secondary, dockitem): single_items.append(secondary) elif isinstance(secondary.child, dockitem): single_areas.append(secondary) else: multi_areas.append(secondary) targets = [] for item in single_items: target = self._find_container(item.name) if target is not None: target.float() targets.append((target, item)) for item in single_areas: target = self._find_container(item.child.name) if target is not None: target.float() targets.append((target, item)) for item in multi_areas: target = QDockWindow.create(self, self._dock_area) win_area = target.dockArea() popuplate_area(win_area, item) win_area.installEventFilter(self._area_filter) self._dock_frames.append(target) self._proximity_handler.addFrame(target) targets.append((target, item)) for target, item in targets: rect = QRect(*item.geometry) if rect.isValid(): rect = ensure_on_screen(rect) target.setGeometry(rect) target.show() if item.linked: target.setLinked(True) if item.maximized: target.showMaximized()
def _band_geometry(self, widget, guide): """ Compute the geometry for an overlay rubber band. Parameters ---------- widget : QWidget The widget to which the band geometry should be fit. guide : Guide The rose guide under the mouse. This determines how the geometry of the band will be fit to the widget. """ Guide = QGuideRose.Guide if guide == Guide.NoGuide: return QRect() # border hits border_size = self.border_size rect = widget.contentsRect() if guide == Guide.BorderNorth: rect.setHeight(border_size) elif guide == Guide.BorderEast: rect.setLeft(rect.right() + 1 - border_size) elif guide == Guide.BorderSouth: rect.setTop(rect.bottom() + 1 - border_size) elif guide == Guide.BorderWest: rect.setWidth(border_size) # For the next 4 conditions `widget` will be a QDockArea elif guide == Guide.BorderExNorth: bar_rect = widget.dockBarGeometry(QDockBar.North) if bar_rect.isValid(): rect = bar_rect else: rect.setHeight(border_size / 2) elif guide == Guide.BorderExEast: bar_rect = widget.dockBarGeometry(QDockBar.East) if bar_rect.isValid(): rect = bar_rect else: rect.setLeft(rect.right() + 1 - border_size / 2) elif guide == Guide.BorderExSouth: bar_rect = widget.dockBarGeometry(QDockBar.South) if bar_rect.isValid(): rect = bar_rect else: rect.setTop(rect.bottom() + 1 - border_size / 2) elif guide == Guide.BorderExWest: bar_rect = widget.dockBarGeometry(QDockBar.West) if bar_rect.isValid(): rect = bar_rect else: rect.setWidth(border_size / 2) # compass hits elif guide == Guide.CompassNorth: rect.setHeight(rect.height() / 3) elif guide == Guide.CompassEast: rect.setLeft(2 * rect.width() / 3) elif guide == Guide.CompassSouth: rect.setTop(2 * rect.height() / 3) elif guide == Guide.CompassWest: rect.setWidth(rect.width() / 3) elif guide == Guide.CompassCenter: pass # nothing to do elif guide == Guide.CompassExNorth: pass # nothing to do elif guide == Guide.CompassExEast: pass # nothing to do elif guide == Guide.CompassExSouth: pass # nothing to do elif guide == Guide.CompassExWest: pass # nothing to do # splitter handle hits elif guide == Guide.SplitHorizontal: wo, r = divmod(border_size - rect.width(), 2) rect.setWidth(2 * (wo + r) + rect.width()) rect.moveLeft(rect.x() - (wo + r)) elif guide == Guide.SplitVertical: ho, r = divmod(border_size - widget.height(), 2) rect.setHeight(2 * (ho + r) + rect.height()) rect.moveTop(rect.y() - (ho + r)) # single center elif guide == Guide.AreaCenter: pass # nothing to do # default no-op else: return QRect() pt = widget.mapToGlobal(rect.topLeft()) return QRect(pt, rect.size())
def mouse_over_area(self, area, widget, pos): """ Update the overlays based on the mouse position. Parameters ---------- area : QDockArea The dock area which contains the dock items onto which the overlay will be displayed. widget : QWidget The dock widget in the area which is under the mouse, or None if there is no relevant widget. pos : QPoint The hover position, expressed in the local coordinates of the overlayed dock area. """ Mode = QGuideRose.Mode Guide = QGuideRose.Guide pane = area.centralPane() pos = pane.mapFrom(area, pos) if widget is None: if area.centralWidget() is None: self.mouse_over_widget(pane, pos, empty=True) return # Compute the target mode for the guide rose based on the dock # widget which lies under the mouse position. target_mode = Mode.Border if isinstance(widget, QDockContainer): target_mode |= Mode.CompassEx elif isinstance(widget, QDockTabWidget): target_mode |= Mode.Compass elif isinstance(widget, QDockSplitterHandle): if widget.orientation() == Qt.Horizontal: target_mode |= Mode.SplitHorizontal else: target_mode |= Mode.SplitVertical # Get the local area coordinates for the center of the widget. center = widget.mapTo(pane, QPoint(0, 0)) center += QPoint(widget.width() / 2, widget.height() / 2) # Update the state of the rose. If it is to be hidden, it is # done so immediately. If the target mode is different from # the current mode, the rose is hidden and the state changes # are collapsed on a timer. rose = self._rose self._hover_pos = pos self._show_band = True self._target_rose_mode = target_mode if target_mode != rose.mode(): rose.setMode(Mode.Border) self._rose_timer.start(self.rose_delay) self._show_band = False # Update the geometry of the rose if needed. This ensures that # the rose does not change geometry while visible. origin = pane.mapToGlobal(QPoint(0, 0)) geo = QRect(origin, pane.size()) dirty = rose.geometry() != geo if dirty: rose.hide() rose.setMode(Mode.NoMode) rose.setGeometry(geo) # Hit test the rose and update the target geometry for the # rubber band if the target guide has changed. rose.setCenterPoint(center) guide = rose.guideAt(pos, target_mode) if dirty or guide != self._last_guide: self._last_guide = guide if guide >= Guide.BorderNorth and guide <= Guide.BorderWest: band_geo = self._band_geometry(pane, guide) elif guide >= Guide.BorderExNorth and guide <= Guide.BorderExWest: band_geo = self._band_geometry(area, guide) else: band_geo = self._band_geometry(widget, guide) self._target_band_geo = band_geo self._band_timer.start(self.band_delay) # Finally, make the rose visible and issue a mouseover command # so that the guides are highlighted. rose.mouseOver(pos) rose.show()
class DockOverlay(Atom): """ An object which manages the overlays for dock widgets. This manager handles the state transitions for the overlays. The transitions are performed on a slightly-delayed timer to provide a more fluid user interaction experience. """ # PySide requires weakrefs for using bound methods as slots. # PyQt doesn't, but executes unsafe code if not using weakrefs. __slots__ = '__weakref__' #: The size of the rubber band when docking on the border, in px. border_size = Int(60) #: The delay to use when triggering the rose timer, in ms. rose_delay = Int(30) #: The delay to use when triggering the band timer, in ms. band_delay = Int(50) #: The target opacity to use when making the band visible. band_target_opacity = Float(1.0) #: The duration of the band visibilty animation, in ms. band_vis_duration = Int(100) #: the duration of the band geometry animation, in ms. band_geo_duration = Int(100) #: The overlayed guide rose. _rose = Typed(QGuideRose, ()) #: The overlayed rubber band. _band = Typed(QDockRubberBand, ()) #: The property animator for the rubber band geometry. _geo_animator = Typed(QPropertyAnimation) #: The property animator for the rubber band visibility. _vis_animator = Typed(QPropertyAnimation) #: The target mode to apply to the rose on timeout. _target_rose_mode = Int(QGuideRose.Mode.NoMode) #: The target geometry to apply to rubber band on timeout. _target_band_geo = Typed(QRect, factory=lambda: QRect()) #: The value of the last guide which was hit in the rose. _last_guide = Int(-1) #: A flag indicating whether it is safe to show the band. _show_band = Bool(False) #: The hover position of the mouse to use for state changes. _hover_pos = Typed(QPoint, factory=lambda: QPoint()) #: The timer for changing the state of the rose. _rose_timer = Typed(QTimer) #: The timer for changing the state of the band. _band_timer = Typed(QTimer) def __init__(self, parent=None): """ Initialize a DockOverlay. Parameters ---------- parent : QWidget, optional The parent of the overlay. This will be used as the parent widget for the dock rubber band. The overlay guides do not have a parent. """ self._band = QDockRubberBand(parent) #-------------------------------------------------------------------------- # Default Value Methods #-------------------------------------------------------------------------- def _default__rose_timer(self): """ Create the default timer for the rose state changes. """ timer = QTimer() timer.setSingleShot(True) timer.timeout.connect(self._on_rose_timer) return timer def _default__band_timer(self): """ Create the default timer for the band state changes. """ timer = QTimer() timer.setSingleShot(True) timer.timeout.connect(self._on_band_timer) return timer def _default__geo_animator(self): """ Create the default property animator for the rubber band. """ p = QPropertyAnimation(self._band, b'geometry') p.setDuration(self.band_geo_duration) return p def _default__vis_animator(self): """ Create the default property animator for the rubber band. """ p = QPropertyAnimation(self._band, b'windowOpacity') p.setDuration(self.band_vis_duration) p.finished.connect(self._on_vis_finished) return p #-------------------------------------------------------------------------- # Timer Handlers #-------------------------------------------------------------------------- def _on_rose_timer(self): """ Handle the timeout event for the internal rose timer. This handler transitions the rose to its new state and updates the position of the rubber band. """ rose = self._rose rose.setMode(self._target_rose_mode) rose.mouseOver(self._hover_pos) self._show_band = True self._update_band_state() def _on_band_timer(self): """ Handle the timeout event for the internal band timer. This handler updates the position of the rubber band. """ self._update_band_state() #-------------------------------------------------------------------------- # Animation Handlers #-------------------------------------------------------------------------- def _on_vis_finished(self): """ Handle the 'finished' signal from the visibility animator. This handle will hide the rubber band when its opacity is 0. """ band = self._band if band.windowOpacity() == 0.0: band.hide() #-------------------------------------------------------------------------- # Private API #-------------------------------------------------------------------------- def _update_band_state(self): """ Refresh the geometry and visible state of the rubber band. The state will be updated using animated properties to provide a nice fluid user experience. """ # A valid geometry indicates that the rubber should be shown on # the screen. An invalid geometry means it should be hidden. If # the validity is changed during animation, the animators are # restarted using the current state as their starting point. band = self._band geo = self._target_band_geo if geo.isValid() and self._show_band: # If the band is already hidden, the geometry animation can # be bypassed since the band can be located anywhere. if band.isHidden(): band.setGeometry(geo) self._start_vis_animator(self.band_target_opacity) self._rose.raise_() else: self._start_vis_animator(self.band_target_opacity) self._start_geo_animator(geo) else: self._start_vis_animator(0.0) def _start_vis_animator(self, opacity): """ (Re)start the visibility animator. Parameters ---------- opacity : float The target opacity of the target object. """ animator = self._vis_animator if animator.state() == animator.Running: animator.stop() target = animator.targetObject() if target.isHidden() and opacity != 0.0: target.setWindowOpacity(0.0) target.show() animator.setStartValue(target.windowOpacity()) animator.setEndValue(opacity) animator.start() def _start_geo_animator(self, geo): """ (Re)start the visibility animator. Parameters ---------- geo : QRect The target geometry for the target object. """ animator = self._geo_animator if animator.state() == animator.Running: animator.stop() target = animator.targetObject() animator.setStartValue(target.geometry()) animator.setEndValue(geo) animator.start() def _band_geometry(self, widget, guide): """ Compute the geometry for an overlay rubber band. Parameters ---------- widget : QWidget The widget to which the band geometry should be fit. guide : Guide The rose guide under the mouse. This determines how the geometry of the band will be fit to the widget. """ Guide = QGuideRose.Guide if guide == Guide.NoGuide: return QRect() # border hits border_size = self.border_size rect = widget.contentsRect() if guide == Guide.BorderNorth: rect.setHeight(border_size) elif guide == Guide.BorderEast: rect.setLeft(rect.right() + 1 - border_size) elif guide == Guide.BorderSouth: rect.setTop(rect.bottom() + 1 - border_size) elif guide == Guide.BorderWest: rect.setWidth(border_size) # For the next 4 conditions `widget` will be a QDockArea elif guide == Guide.BorderExNorth: bar_rect = widget.dockBarGeometry(QDockBar.North) if bar_rect.isValid(): rect = bar_rect else: rect.setHeight(border_size / 2) elif guide == Guide.BorderExEast: bar_rect = widget.dockBarGeometry(QDockBar.East) if bar_rect.isValid(): rect = bar_rect else: rect.setLeft(rect.right() + 1 - border_size / 2) elif guide == Guide.BorderExSouth: bar_rect = widget.dockBarGeometry(QDockBar.South) if bar_rect.isValid(): rect = bar_rect else: rect.setTop(rect.bottom() + 1 - border_size / 2) elif guide == Guide.BorderExWest: bar_rect = widget.dockBarGeometry(QDockBar.West) if bar_rect.isValid(): rect = bar_rect else: rect.setWidth(border_size / 2) # compass hits elif guide == Guide.CompassNorth: rect.setHeight(rect.height() / 3) elif guide == Guide.CompassEast: rect.setLeft(2 * rect.width() / 3) elif guide == Guide.CompassSouth: rect.setTop(2 * rect.height() / 3) elif guide == Guide.CompassWest: rect.setWidth(rect.width() / 3) elif guide == Guide.CompassCenter: pass # nothing to do elif guide == Guide.CompassExNorth: pass # nothing to do elif guide == Guide.CompassExEast: pass # nothing to do elif guide == Guide.CompassExSouth: pass # nothing to do elif guide == Guide.CompassExWest: pass # nothing to do # splitter handle hits elif guide == Guide.SplitHorizontal: wo, r = divmod(border_size - rect.width(), 2) rect.setWidth(2 * (wo + r) + rect.width()) rect.moveLeft(rect.x() - (wo + r)) elif guide == Guide.SplitVertical: ho, r = divmod(border_size - widget.height(), 2) rect.setHeight(2 * (ho + r) + rect.height()) rect.moveTop(rect.y() - (ho + r)) # single center elif guide == Guide.AreaCenter: pass # nothing to do # default no-op else: return QRect() pt = widget.mapToGlobal(rect.topLeft()) return QRect(pt, rect.size()) #-------------------------------------------------------------------------- # Public API #-------------------------------------------------------------------------- def guide_at(self, pos): """ Get the dock guide for a given position. Parameters ---------- pos : QPoint The position of interest, expressed in global coordinates. Returns ------- result : Guide The guide enum which lies under the given point. """ rose = self._rose pos = rose.mapFromGlobal(pos) return rose.guideAt(pos) def hide(self): """ Hide the overlay. This method will stop the timers and set the visibility of the guide rose and the rubber band to False. """ self._rose_timer.stop() self._band_timer.stop() self._rose.hide() self._band.hide() def mouse_over_widget(self, widget, pos, empty=False): """ Update the overlays based on the mouse position. This handler should be invoked when the mouse hovers over a single widget (such as a floating dock container) as opposed to an area of docked widgets. The guide rose will be displayed in the center of the widget with no border guides. Parameters ---------- widget : QWidget The widget under the mouse. pos : QPoint The hover position, expressed in the local coordinates of the widget. empty : bool, optional Whether the widget represents an empty widget. If this is True, a single center guide will be shown instead of the guide rose. """ Mode = QGuideRose.Mode rose = self._rose target_mode = Mode.AreaCenter if empty else Mode.CompassEx self._target_rose_mode = target_mode if rose.mode() != target_mode: rose.setMode(Mode.NoMode) self._rose_timer.start(self.rose_delay) self._band_timer.start(self.band_delay) origin = widget.mapToGlobal(QPoint(0, 0)) geo = QRect(origin, widget.size()) dirty = rose.geometry() != geo if dirty: rose.hide() rose.setMode(Mode.NoMode) rose.setGeometry(geo) guide = rose.guideAt(pos, target_mode) if dirty or guide != self._last_guide: self._last_guide = guide self._target_band_geo = self._band_geometry(widget, guide) self._band_timer.start(self.band_delay) rose.setCenterPoint(QPoint(geo.width() / 2, geo.height() / 2)) rose.mouseOver(pos) rose.show() def mouse_over_area(self, area, widget, pos): """ Update the overlays based on the mouse position. Parameters ---------- area : QDockArea The dock area which contains the dock items onto which the overlay will be displayed. widget : QWidget The dock widget in the area which is under the mouse, or None if there is no relevant widget. pos : QPoint The hover position, expressed in the local coordinates of the overlayed dock area. """ Mode = QGuideRose.Mode Guide = QGuideRose.Guide pane = area.centralPane() pos = pane.mapFrom(area, pos) if widget is None: if area.centralWidget() is None: self.mouse_over_widget(pane, pos, empty=True) return # Compute the target mode for the guide rose based on the dock # widget which lies under the mouse position. target_mode = Mode.Border if isinstance(widget, QDockContainer): target_mode |= Mode.CompassEx elif isinstance(widget, QDockTabWidget): target_mode |= Mode.Compass elif isinstance(widget, QDockSplitterHandle): if widget.orientation() == Qt.Horizontal: target_mode |= Mode.SplitHorizontal else: target_mode |= Mode.SplitVertical # Get the local area coordinates for the center of the widget. center = widget.mapTo(pane, QPoint(0, 0)) center += QPoint(widget.width() / 2, widget.height() / 2) # Update the state of the rose. If it is to be hidden, it is # done so immediately. If the target mode is different from # the current mode, the rose is hidden and the state changes # are collapsed on a timer. rose = self._rose self._hover_pos = pos self._show_band = True self._target_rose_mode = target_mode if target_mode != rose.mode(): rose.setMode(Mode.Border) self._rose_timer.start(self.rose_delay) self._show_band = False # Update the geometry of the rose if needed. This ensures that # the rose does not change geometry while visible. origin = pane.mapToGlobal(QPoint(0, 0)) geo = QRect(origin, pane.size()) dirty = rose.geometry() != geo if dirty: rose.hide() rose.setMode(Mode.NoMode) rose.setGeometry(geo) # Hit test the rose and update the target geometry for the # rubber band if the target guide has changed. rose.setCenterPoint(center) guide = rose.guideAt(pos, target_mode) if dirty or guide != self._last_guide: self._last_guide = guide if guide >= Guide.BorderNorth and guide <= Guide.BorderWest: band_geo = self._band_geometry(pane, guide) elif guide >= Guide.BorderExNorth and guide <= Guide.BorderExWest: band_geo = self._band_geometry(area, guide) else: band_geo = self._band_geometry(widget, guide) self._target_band_geo = band_geo self._band_timer.start(self.band_delay) # Finally, make the rose visible and issue a mouseover command # so that the guides are highlighted. rose.mouseOver(pos) rose.show()
def drag_move_frame(self, frame, target_pos, mouse_pos): """ Move the floating frame to the target position. This method is called by a floating frame in response to a user moving it by dragging on it's title bar. It takes into account neighboring windows and will snap the frame edge to another window if it comes close to the boundary. It also ensures that the guide overlays are shown at the proper position. This method should not be called by user code. Parameters ---------- frame : QDockFrame The floating QDockFrame which should be moved. target_pos : QPoint The global position which is the target of the move. mouse_pos : QPoint The global mouse position. """ # If the frame is linked, it and any of its linked frames are # moved the same amount with no snapping. An unlinked window # is free to move and will snap to any other floating window # that has an opposite edge lying within the snap distance. # The overlay is hidden when the frame has proximal frames # since such a frame is not allowed to be docked. show_drag_overlay = True handler = self._proximity_handler if frame.isLinked(): delta = target_pos - frame.pos() frame.move(target_pos) if handler.hasLinkedFrames(frame): show_drag_overlay = False for other in handler.linkedFrames(frame): other.move(other.pos() + delta) else: f_size = frame.frameGeometry().size() f_rect = QRect(target_pos, f_size) f_x = target_pos.x() f_y = target_pos.y() f_w = f_size.width() f_h = f_size.height() dist = self._snap_dist filt = lambda n: -dist < n < dist for other in handler.proximalFrames(f_rect, dist): if other is not frame: o_geo = other.frameGeometry() o_x = o_geo.left() o_y = o_geo.top() o_right = o_x + o_geo.width() o_bottom = o_y + o_geo.height() dx = [ c for c in (o_x - f_x, o_x - (f_x + f_w), o_right - f_x, o_right - (f_x + f_w)) if filt(c) ] if dx: f_x += min(dx) dy = [ c for c in (o_y - f_y, o_y - (f_y + f_h), o_bottom - f_y, o_bottom - (f_y + f_h)) if filt(c) ] if dy: f_y += min(dy) frame.move(f_x, f_y) if show_drag_overlay: self._update_drag_overlay(frame, mouse_pos) else: self._overlay.hide()
def layout(self, rect): """ Layout the guides for the given rect. Parameters ---------- rect : QRect The rectangle in which to layout the border guides. """ boxes = self._boxes guides = self._guides w = rect.width() h = rect.height() cx = rect.left() + w / 2 cy = rect.top() + h / 2 Guide = QGuideRose.Guide guides[Guide.BorderNorth].rect = QRect(cx - 15, 27, 31, 19) guides[Guide.BorderExNorth].rect = QRect(cx - 15, 15, 31, 10) boxes[Guide.BorderNorth].rect = QRect(cx - 20, 10, 41, 41) guides[Guide.BorderEast].rect = QRect(w - 45, cy - 15, 19, 31) guides[Guide.BorderExEast].rect = QRect(w - 24, cy - 15, 10, 31) boxes[Guide.BorderEast].rect = QRect(w - 50, cy - 20, 41, 41) guides[Guide.BorderSouth].rect = QRect(cx - 15, h - 45, 31, 19) guides[Guide.BorderExSouth].rect = QRect(cx - 15, h - 24, 31, 10) boxes[Guide.BorderSouth].rect = QRect(cx - 20, h - 50, 41, 41) guides[Guide.BorderWest].rect = QRect(27, cy - 15, 19, 31) guides[Guide.BorderExWest].rect = QRect(15, cy - 15, 10, 31) boxes[Guide.BorderWest].rect = QRect(10, cy - 20, 41, 41)