def mouseMoveEvent(self, event): """ Handle the mouse move event for the tab bar. This handler will undock the tab if the mouse is held and the drag leaves the boundary of the container by the application drag distance amount. """ super(QDockTabBar, self).mouseMoveEvent(event) if not self._has_mouse: return pos = event.pos() if self.rect().contains(pos): return x = max(0, min(pos.x(), self.width())) y = max(0, min(pos.y(), self.height())) dist = (QPoint(x, y) - pos).manhattanLength() if dist > QApplication.startDragDistance(): # Fake a mouse release event so that the tab resets its # internal state and finalizes the animation for the tab. # The button must be Qt.LeftButton, not event.button(). btn = Qt.LeftButton mod = event.modifiers() evt = QMouseEvent(QEvent.MouseButtonRelease, pos, btn, btn, mod) QApplication.sendEvent(self, evt) container = self.parent().widget(self.currentIndex()) container.untab(event.globalPos()) self._has_mouse = False
def titleBarMouseMoveEvent(self, event): """ Handle a mouse move event on the title bar. Returns ------- result : bool True if the event is handled, False otherwise. """ state = self.frame_state if state.press_pos is None: return False # If dragging and floating, move the container's position and # notify the manager of that the container was mouse moved. If # the container is maximized, it is first restored before. global_pos = event.globalPos() if state.dragging: if self.isWindow(): target_pos = global_pos - state.press_pos self.manager().drag_move_frame(self, target_pos, global_pos) return True # Ensure the drag has crossed the app drag threshold. dist = (event.pos() - state.press_pos).manhattanLength() if dist <= QApplication.startDragDistance(): return True # If the container is already floating, ensure that it is shown # normal size. The next move event will move the window. state.dragging = True if self.isWindow(): state.frame_was_maximized = self.isMaximized() if state.frame_was_maximized: coeff = state.press_pos.x() / float(self.width()) self.showNormal() state.press_pos = _computePressPos(self, coeff) return True # Restore a maximized dock item before unplugging. if state.item_is_maximized: bar = self.dockItem().titleBarWidget() coeff = state.press_pos.x() / float(bar.width()) self.showNormal() state.press_pos = _computePressPos(self, coeff) # Unplug the container from the layout before floating so # that layout widgets can clean themselves up when empty. if not self.unplug(): return False self.postUndockedEvent() # Make the container a toplevel frame, update it's Z-order, # and grab the mouse to continue processing drag events. self.float() self.raiseFrame() margins = self.layout().contentsMargins() state.press_pos += QPoint(0, margins.top()) state.start_pos = global_pos - state.press_pos self.move(state.start_pos) self.show() self.grabMouse() self.activateWindow() self.raise_() return True
class DockManager(Atom): """ A class which manages the docking behavior of a dock area. """ #: The handler which holds the primary dock area. _dock_area = Typed(QDockArea) #: The overlay used when hovering over a dock area. _overlay = Typed(DockOverlay, ()) #: The list of QDockFrame instances maintained by the manager. The #: QDockFrame class maintains this list in proper Z-order. _dock_frames = List() #: The set of QDockItem instances added to the manager. _dock_items = Typed(set, ()) #: The distance to use for snapping floating dock frames. _snap_dist = Int(factory=lambda: QApplication.startDragDistance() * 2) #: A proximity handler which manages proximal floating frames. _proximity_handler = Typed(ProximityHandler, ()) #: A container monitor which tracks toplevel container changes. _container_monitor = Typed(DockContainerMonitor) def _default__container_monitor(self): return DockContainerMonitor(self) def __init__(self, dock_area): """ Initialize a DockingManager. Parameters ---------- dock_area : QDockArea The primary dock area to be managed. Docking will be restricted to this area and to windows spawned by the area. """ assert dock_area is not None self._dock_area = dock_area self._overlay = DockOverlay(dock_area) #-------------------------------------------------------------------------- # Public API #-------------------------------------------------------------------------- def dock_area(self): """ Get the dock area to which the manager is attached. Returns ------- result : QDockArea The dock area to which the manager is attached. """ return self._dock_area def add_item(self, item): """ Add a dock item to the dock manager. If the item has already been added, this is a no-op. Parameters ---------- items : QDockItem The item to be managed by this dock manager. It will be reparented to a dock container and made available to the the layout system. """ if item in self._dock_items: return self._dock_items.add(item) item._manager = self container = QDockContainer(self, self._dock_area) container.setDockItem(item) container.setObjectName(item.objectName()) monitor = self._container_monitor container.topLevelChanged.connect(monitor.onTopLevelChanged) self._dock_frames.append(container) def remove_item(self, item): """ Remove a dock item from the dock manager. If the item has not been added to the manager, this is a no-op. Parameters ---------- items : QDockItem The item to remove from the dock manager. It will be hidden and unparented, but not destroyed. """ if item not in self._dock_items: return item._manager = None for container in self.dock_containers(): if container.dockItem() is item: if not container.isWindow(): container.unplug() container.hide() self._free_container(container) break def save_layout(self): """ Get the current layout of the dock area. Returns ------- result : docklayout A docklayout instance which represents the current layout state. """ items = [self._dock_area] + self.floating_frames() return DockLayout(*map(LayoutSaver(), items)) def apply_layout(self, layout): """ Apply a layout to the dock area. Parameters ---------- layout : DockLayout The DockLayout to apply to the managed area. """ LayoutBuilder(self)(layout) def update_layout(self, ops): """ Update the layout for a list of layout operations. Parameters ---------- ops : list A list of LayoutOp objects to use for updating the layout. """ builder = LayoutBuilder(self) for op in ops: builder(op) def destroy(self): """ Destroy the dock manager. This method will free all of the resources held by the dock manager. The primary dock area and dock items will not be destroyed. After the method is called, the dock manager is invalid and should no longer be used. """ for frame in self._dock_frames: if isinstance(frame, QDockContainer): frame.setDockItem(None) frame.setParent(None, Qt.Widget) frame.hide() for frame in self._dock_frames: if isinstance(frame, QDockWindow): frame.setParent(None, Qt.Widget) frame.hide() for item in self._dock_items: item._manager = None self._dock_area.setCentralWidget(None) self._dock_area.setMaximizedWidget(None) del self._dock_area del self._dock_frames del self._dock_items del self._proximity_handler del self._container_monitor del self._overlay #-------------------------------------------------------------------------- # Framework API #-------------------------------------------------------------------------- def dock_containers(self): """ Get an iterable of QDockContainer instances. This method is called by the framework at the appropriate times and should not be called directly by user code. Returns ------- result : list A list of QDockContainer instances owned by this dock manager. """ f = lambda w: isinstance(w, QDockContainer) return filter(f, self._dock_frames) def dock_windows(self): """ Get an iterable of QDockWindow instances. This method is called by the framework at the appropriate times and should not be called directly by user code. Returns ------- result : list A list of QDockWindow instances owned by this dock manager. """ f = lambda w: isinstance(w, QDockWindow) return filter(f, self._dock_frames) def floating_frames(self): """ Get an iterable of floating dock frames. This method is called by the framework at the appropriate times and should not be called directly by user code. Returns ------- result : list A list toplevel QDockFrame instances. """ f = lambda w: w.isWindow() return filter(f, self._dock_frames) def add_window(self, window): """ Add a floating QDockWindow to the dock manager. This method is called by the framework at the appropriate times and should not be called directly by user code. Parameters ---------- window : QDockWindow A newly created dock window which should be tracked by the dock manager. """ self._dock_frames.append(window) self._proximity_handler.addFrame(window) def close_container(self, container, event): """ Handle a close request for a QDockContainer. This method is called by the framework at the appropriate times and should not be called directly by user code. Parameters ---------- window : QDockContainer The dock container to close. event : QCloseEvent The close event passed to the event handler. """ item = container.dockItem() if item is None or item.close(): if not container.isWindow(): container.unplug() self._free_container(container) else: event.ignore() 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 raise_frame(self, frame): """ Raise a frame to the top of the Z-order. This method is called by the framework at the appropriate times and should not be called directly by user code. Parameters ---------- frame : QDockFrame The frame to raise to the top of the Z-order. """ frames = self._dock_frames handler = self._proximity_handler if handler.hasLinkedFrames(frame): linked = set(handler.linkedFrames(frame)) ordered = [f for f in frames if f in linked] for other in ordered: frames.remove(other) frames.append(other) other.raise_() frame.raise_() frames.remove(frame) frames.append(frame) def frame_resized(self, frame): """ Handle the post-processing for a resized floating frame. This method is called by the framework at the appropriate times and should not be called directly by user code. Parameters ---------- frame : QDockFrame The frame which has been resized. """ # If the frame is linked, the resize may have changed the frame # geometry such that the existing links are no longer valid. # The links are refreshed and the link button state is updated. if frame.isLinked(): handler = self._proximity_handler handler.updateLinks(frame) if not handler.hasLinkedFrames(frame): frame.setLinked(False) 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 = filter(filt, ( o_x - f_x, o_x - (f_x + f_w), o_right - f_x, o_right - (f_x + f_w), )) if dx: f_x += min(dx) dy = filter(filt, ( o_y - f_y, o_y - (f_y + f_h), o_bottom - f_y, o_bottom - (f_y + f_h), )) 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 drag_release_frame(self, frame, pos): """ Handle the dock frame being released by the user. This method is called by the framework at the appropriate times and should not be called directly by user code. It will redock a floating dock item if it is released over a dock guide. Parameters ---------- frame : QDockFrame The dock frame being dragged by the user. pos : QPoint The global coordinates of the mouse position. """ # Docking is disallowed for frames which have linked proximal # frames, or if the target dock area has a maximized widget. # This prevents a situation where the docking logic would be # non-sensical and maintains a consistent user experience. overlay = self._overlay overlay.hide() guide = overlay.guide_at(pos) if guide == QGuideRose.Guide.NoGuide: return if self._proximity_handler.hasLinkedFrames(frame): return builder = LayoutBuilder(self) target = self._dock_target(frame, pos) if isinstance(target, QDockArea): if target.maximizedWidget() is not None: return with builder.drop_frame(frame): local = target.mapFromGlobal(pos) widget = layout_hit_test(target, local) plug_frame(target, widget, frame, guide) elif isinstance(target, QDockContainer): with builder.dock_context(target): with builder.drop_frame(frame): area = target.parentDockArea() if area is not None: plug_frame(area, target, frame, guide) #-------------------------------------------------------------------------- # Private API #-------------------------------------------------------------------------- def _free_container(self, container): """ Free the resources attached to the container. Parameters ---------- container : QDockContainer The container which should be cleaned up. It should be unplugged from any layout before being passed to this method. """ item = container.dockItem() container.setParent(None) container.setDockItem(None) container._manager = None self._dock_items.discard(item) self._dock_frames.remove(container) self._proximity_handler.removeFrame(container) def _free_window(self, window): """ Free the resources attached to the window. Parameters ---------- window : QDockWindow The Window which should be cleaned up. """ window.setParent(None) window.setDockArea(None) window._manager = None self._dock_frames.remove(window) self._proximity_handler.removeFrame(window) def _iter_dock_targets(self, frame): """ Get an iterable of potential dock targets. Parameters ---------- frame : QDockFrame The frame which is being docked, and therefore excluded from the target search. Returns ------- result : generator A generator which yields the dock container and dock area instances which are potential dock targets. """ for target in reversed(self._dock_frames): if target is not frame and target.isWindow(): if isinstance(target, QDockContainer): yield target elif isinstance(target, QDockWindow): yield target.dockArea() yield self._dock_area def _dock_target(self, frame, pos): """ Get the dock target for the given frame and position. Parameters ---------- frame : QDockFrame The dock frame which should be docked. pos : QPoint The global mouse position. Returns ------- result : QDockArea, QDockContainer, or None The potential dock target for the frame and position. """ for target in self._iter_dock_targets(frame): # Hit test the central pane instead of the entire dock area # so that mouse movement over the dock bars is ignored. if isinstance(target, QDockArea): pane = target.centralPane() local = pane.mapFromGlobal(pos) if pane.rect().contains(local): return target else: local = target.mapFromGlobal(pos) if target.rect().contains(local): return target def _update_drag_overlay(self, frame, pos): """ Update the overlay for a dragged frame. Parameters ---------- frame : QDockFrame The dock frame being dragged by the user. pos : QPoint The global coordinates of the mouse position. """ overlay = self._overlay target = self._dock_target(frame, pos) if isinstance(target, QDockContainer): local = target.mapFromGlobal(pos) overlay.mouse_over_widget(target, local) elif isinstance(target, QDockArea): # Disallow docking onto an area with a maximized widget. # This prevents a non-intuitive user experience. if target.maximizedWidget() is not None: overlay.hide() return local = target.mapFromGlobal(pos) widget = layout_hit_test(target, local) overlay.mouse_over_area(target, widget, local) else: overlay.hide()
def titleBarMouseMoveEvent(self, event): """ Handle a mouse move event on the title bar. Returns ------- result : bool True if the event is handled, False otherwise. """ state = self.frame_state if state.press_pos is None: return False # If dragging and floating, move the container's position and # notify the manager of that the container was mouse moved. If # the container is maximized, it is first restored before. global_pos = event.globalPos() if state.dragging: if self.isWindow(): target_pos = global_pos - state.press_pos self.manager().drag_move_frame(self, target_pos, global_pos) return True # Ensure the drag has crossed the app drag threshold. dist = (event.pos() - state.press_pos).manhattanLength() if dist <= QApplication.startDragDistance(): return True # If the container is already floating, ensure that it is shown # normal size. The next move event will move the window. state.dragging = True if self.isWindow(): state.frame_was_maximized = self.isMaximized(); if state.frame_was_maximized: coeff = state.press_pos.x() / float(self.width()) self.showNormal() state.press_pos = _computePressPos(self, coeff) return True # Restore a maximized dock item before unplugging. if state.item_is_maximized: bar = self.dockItem().titleBarWidget() coeff = state.press_pos.x() / float(bar.width()) self.showNormal() state.press_pos = _computePressPos(self, coeff) # Unplug the container from the layout before floating so # that layout widgets can clean themselves up when empty. if not self.unplug(): return False self.postUndockedEvent() # Make the container a toplevel frame, update it's Z-order, # and grab the mouse to continue processing drag events. self.float() self.raiseFrame() margins = self.layout().contentsMargins() state.press_pos += QPoint(0, margins.top()) state.start_pos = global_pos - state.press_pos self.move(state.start_pos) self.show() self.grabMouse() self.activateWindow() self.raise_() return True