From 4dea32287b9d426ec2845eee42583a0836bd5fde Mon Sep 17 00:00:00 2001 From: Chris Colbert Date: Tue, 28 May 2013 21:23:22 -0400 Subject: [PATCH 01/11] api cleanup --- enaml/qt/docking/dock_manager.py | 285 ++++++++++++++++----------- enaml/qt/docking/q_dock_container.py | 10 +- enaml/qt/docking/q_dock_frame.py | 4 +- enaml/qt/docking/q_dock_window.py | 6 +- 4 files changed, 184 insertions(+), 121 deletions(-) diff --git a/enaml/qt/docking/dock_manager.py b/enaml/qt/docking/dock_manager.py index 6c3c26443..448b980d5 100644 --- a/enaml/qt/docking/dock_manager.py +++ b/enaml/qt/docking/dock_manager.py @@ -56,7 +56,7 @@ def ensure_on_screen(rect): return rect -class QDockAreaFilter(QObject): +class DockAreaFilter(QObject): """ An event filter to listen for content changes in a dock area. """ @@ -97,11 +97,8 @@ def processArea(self, area): widget.show() widget.setAttribute(attr, old) manager = widget.manager() - # Stack the last widget under the toplevel frame. if manager is not None: - frames = manager.dock_frames - frames.remove(widget) - frames.insert(-1, widget) + manager.stack_under_top(widget) # Hide before closing, or the window will steal mouse # events from the container being dragged, event though # the container has grabbed the mouse. @@ -115,22 +112,26 @@ def processArea(self, area): class DockManager(Atom): """ A class which manages the docking behavior of a dock area. + The manager is used by attaching it to a QDockArea and then adding + dock items via the 'add_item' method and then setting a layout via + the 'apply_layout' method. See the docstring of the public api methods for more functionality. + """ #: The handler which holds the primary dock area. - dock_area = Typed(QDockArea) + _dock_area = Typed(QDockArea) #: The overlay used when hovering over a dock area. - overlay = Typed(DockOverlay, ()) + _overlay = Typed(DockOverlay, ()) #: The dock area filter installed on floating dock windows. - area_filter = Typed(QDockAreaFilter, ()) + _area_filter = Typed(DockAreaFilter, ()) #: The list of QDockFrame instances maintained by the manager. The #: QDockFrame class maintains this list in proper Z-order. - dock_frames = List() + _dock_frames = List() #: The set of QDockItem instances added to the manager. - dock_items = Typed(set, ()) + _dock_items = Typed(set, ()) def __init__(self, dock_area): """ Initialize a DockingManager. @@ -144,11 +145,22 @@ def __init__(self, dock_area): """ assert dock_area is not None - self.dock_area = dock_area + self._dock_area = 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. @@ -162,13 +174,13 @@ def add_item(self, item): the layout system. """ - if item in self.dock_items: + if item in self._dock_items: return - self.dock_items.add(item) - container = QDockContainer(self, self.dock_area) + self._dock_items.add(item) + container = QDockContainer(self, self._dock_area) container.setDockItem(item) container.setObjectName(item.objectName()) - self.dock_frames.append(container) + self._dock_frames.append(container) def remove_item(self, item): """ Remove a dock item from the dock manager. @@ -182,7 +194,7 @@ def remove_item(self, item): and unparented, but not destroyed. """ - if item not in self.dock_items: + if item not in self._dock_items: return container = self._find_container(item.objectName()) if container is not None: @@ -198,19 +210,53 @@ def clear_items(self): were previously added to the dock manager. """ - windows = [] - containers = [] - for frame in self.dock_frames: + for frame in self._dock_frames[:]: if isinstance(frame, QDockContainer): - containers.append(frame) + self._free_container(frame) else: - windows.append(frame) - for frame in containers: - self._free_container(frame) - for frame in windows: - self._free_window(frame) - del self.dock_frames - self.dock_area.setLayoutWidget(None) + self._free_window(frame) + del self._dock_frames + self._dock_area.setLayoutWidget(None) + + def save_layout(self): + """ Get the current layout of the dock area. + + Returns + ------- + result : docklayout + A docklayout instance which represents the current layout + state. + + """ + primary = None + secondary = [] + + area = self._dock_area + widget = area.layoutWidget() + if widget is not None: + primary = dockarea(save_layout(widget)) + maxed = area.maximizedWidget() + if maxed is not None: + primary.maximized_item = maxed.objectName() + + for frame in self._floating_frames(): + if isinstance(frame, QDockWindow): + area = frame.dockArea() + item = dockarea(save_layout(area.layoutWidget())) + maxed = area.maximizedWidget() + if maxed is not None: + item.maximized_item = maxed.objectName() + else: + item = save_layout(frame) + item.maximized = frame.isMaximized() + if frame.isMaximized(): + geo = frame.normalGeometry() + else: + geo = frame.geometry() + item.geometry = (geo.x(), geo.y(), geo.width(), geo.height()) + secondary.append(item) + + return docklayout(primary, *secondary) def apply_layout(self, layout): """ Apply a layout to the dock area. @@ -226,14 +272,13 @@ def apply_layout(self, layout): # 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) + widget = self._dock_area.layoutWidget() + self._dock_area.setLayoutWidget(None) containers = list(self._dock_containers()) for container in containers: container.reset() - for frame in self.dock_frames[:]: - if isinstance(frame, QDockWindow): - frame.close() + 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. @@ -257,10 +302,10 @@ def popuplate_area(area, layout): primary = layout.primary if primary is not None: if isinstance(primary, dockarea): - popuplate_area(self.dock_area, primary) + popuplate_area(self._dock_area, primary) else: widget = build_layout(primary, containers) - self.dock_area.setLayoutWidget(widget) + self._dock_area.setLayoutWidget(widget) # Setup the layout for the secondary floating dock area. This # classifies the secondary items according to their type as @@ -288,11 +333,11 @@ def popuplate_area(area, layout): target.float() targets.append((target, item)) for item in multi_areas: - target = QDockWindow.create(self, self.dock_area) + 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) + win_area.installEventFilter(self._area_filter) + self._dock_frames.append(target) targets.append((target, item)) for target, item in targets: @@ -304,47 +349,6 @@ def popuplate_area(area, layout): if item.maximized: target.showMaximized() - def save_layout(self): - """ Get the current layout of the dock area. - - Returns - ------- - result : docklayout - A docklayout instance which represents the current layout - state. - - """ - primary = None - secondary = [] - - area = self.dock_area - widget = area.layoutWidget() - if widget is not None: - primary = dockarea(save_layout(widget)) - maxed = area.maximizedWidget() - if maxed is not None: - primary.maximized_item = maxed.objectName() - - for frame in self.dock_frames: - if frame.isWindow(): - if isinstance(frame, QDockWindow): - area = frame.dockArea() - item = dockarea(save_layout(area.layoutWidget())) - maxed = area.maximizedWidget() - if maxed is not None: - item.maximized_item = maxed.objectName() - else: - item = save_layout(frame) - item.maximized = frame.isMaximized() - if frame.isMaximized(): - geo = frame.normalGeometry() - else: - geo = frame.geometry() - item.geometry = (geo.x(), geo.y(), geo.width(), geo.height()) - secondary.append(item) - - return docklayout(primary, *secondary) - def apply_layout_op(self, op, direction, *item_names): """ Apply a layout operation to the managed items. @@ -370,11 +374,44 @@ def apply_layout_op(self, op, direction, *item_names): #-------------------------------------------------------------------------- # Framework API #-------------------------------------------------------------------------- - def _frame_moved(self, frame, pos): + 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 + frames.remove(frame) + frames.append(frame) + + def stack_under_top(self, frame): + """ Stack the given frame under the top frame in 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 stack under the top frame in the Z-order. + + """ + frames = self._dock_frames + frames.remove(frame) + frames.insert(-1, frame) + + def frame_moved(self, frame, pos): """ Handle a dock frame being moved by the user. - This method is called by a floating dock frame as it is dragged - by the user. It shows the dock overlay at the proper location. + This method is called by the framework at the appropriate times + and should not be called directly by user code. It ensures that + the dock overlay guides are shown and hidden appropriately. Parameters ---------- @@ -385,31 +422,32 @@ def _frame_moved(self, frame, pos): The global coordinates of the mouse position. """ + overlay = self._overlay target = self._dock_target(frame, pos) if isinstance(target, QDockContainer): local = target.mapFromGlobal(pos) - self.overlay.mouse_over_widget(target, local) + 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: - self.overlay.hide() + overlay.hide() return local = target.mapFromGlobal(pos) if target.layoutWidget() is None: - self.overlay.mouse_over_widget(target, local, empty=True) + overlay.mouse_over_widget(target, local, empty=True) else: widget = layout_hit_test(target, local) - self.overlay.mouse_over_area(target, widget, local) + overlay.mouse_over_area(target, widget, local) else: - self.overlay.hide() + overlay.hide() - def _frame_released(self, frame, pos): + def frame_released(self, frame, pos): """ Handle the dock frame being released by the user. - This method is called by a floating dock frame when the user - has completed the drag operation. It will hide the overlay and - redock the frame if the drag ended over a valid dock guide. + 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 ---------- @@ -420,7 +458,7 @@ def _frame_released(self, frame, pos): The global coordinates of the mouse position. """ - overlay = self.overlay + overlay = self._overlay overlay.hide() guide = overlay.guide_at(pos) if guide == QGuideRose.Guide.NoGuide: @@ -442,10 +480,11 @@ def _frame_released(self, frame, pos): if area is not None: plug_frame(area, target, frame, guide) - def _close_container(self, container, event): - """ Close a QDockContainer. + def close_container(self, container, event): + """ Handle a close request for a QDockContainer. - This is called by a QDockContainer from its close event handler. + This method is called by the framework at the appropriate times + and should not be called directly by user code. Parameters ---------- @@ -464,12 +503,11 @@ def _close_container(self, container, event): else: event.ignore() - def _close_window(self, window, event): - """ Close a QDockWindow. + def close_window(self, window, event): + """ Handle a close request for a QDockWindow. - This is called by a QDockWindow from its close event handler - or from the dock area filter when all items have been removed - from the dock window. + This method is called by the framework at the appropriate times + and should not be called directly by user code. Parameters ---------- @@ -482,7 +520,7 @@ def _close_window(self, window, event): """ area = window.dockArea() if area is not None: - area.removeEventFilter(self.area_filter) + area.removeEventFilter(self._area_filter) containers = list(iter_containers(area)) geometries = {} for container in containers: @@ -515,8 +553,8 @@ def _free_container(self, container): container.setParent(None) container.setDockItem(None) container._manager = None - self.dock_items.discard(item) - self.dock_frames.remove(container) + self._dock_items.discard(item) + self._dock_frames.remove(container) def _free_window(self, window): """ Free the resources attached to the window. @@ -530,7 +568,7 @@ def _free_window(self, window): window.setParent(None) window.setDockArea(None) window._manager = None - self.dock_frames.remove(window) + self._dock_frames.remove(window) QDockWindow.free(window) def _dock_containers(self): @@ -543,10 +581,37 @@ def _dock_containers(self): by this dock manager. """ - for frame in self.dock_frames: + for frame in self._dock_frames: if isinstance(frame, QDockContainer): yield frame + def _dock_windows(self): + """ Get an iterable of QDockWindow instances. + + Returns + ------- + result : generator + A generator which yields the QDockWindow instances owned + by this dock manager. + + """ + for frame in self._dock_frames: + if isinstance(frame, QDockWindow): + yield frame + + def _floating_frames(self): + """ Get an iterable of floating dock frames. + + Returns + ------- + result : generator + A generator which yield toplevel QDockFrame instances. + + """ + for frame in self._dock_frames: + if frame.isWindow(): + yield frame + def _find_container(self, name): """ Find the dock container with the given object name. @@ -602,13 +667,13 @@ def _iter_dock_targets(self): instances which are potential dock targets. """ - for target in reversed(self.dock_frames): + for target in reversed(self._dock_frames): if target.isWindow(): if isinstance(target, QDockContainer): yield target elif isinstance(target, QDockWindow): yield target.dockArea() - yield self.dock_area + yield self._dock_area def _dock_target(self, frame, pos): """ Get the dock target for the given frame and position. @@ -656,14 +721,14 @@ def _dock_context(self, container): is_maxed = container.isMaximized() if is_maxed: container.showNormal() - window = QDockWindow.create(self, self.dock_area) - self.dock_frames.append(window) + window = QDockWindow.create(self, self._dock_area) + self._dock_frames.append(window) window.setGeometry(container.geometry()) win_area = window.dockArea() plug_frame(win_area, None, container, QGuideRose.Guide.AreaCenter) yield if is_window: - win_area.installEventFilter(self.area_filter) + win_area.installEventFilter(self._area_filter) window.show() if is_maxed: window.showMaximized() @@ -823,7 +888,7 @@ def missing(name): if reverse: containers.reverse() - area = self.dock_area + area = self._dock_area for container in containers: container.unplug() if area.layoutWidget() is None: diff --git a/enaml/qt/docking/q_dock_container.py b/enaml/qt/docking/q_dock_container.py index b2313376a..572cb04f0 100644 --- a/enaml/qt/docking/q_dock_container.py +++ b/enaml/qt/docking/q_dock_container.py @@ -278,7 +278,7 @@ def float(self): self.hide() self.setAttribute(Qt.WA_Hover, True) flags = Qt.Tool | Qt.FramelessWindowHint - self.setParent(self.manager().dock_area, flags) + self.setParent(self.manager().dock_area(), flags) self.layout().setContentsMargins(QMargins(5, 5, 5, 5)) self.setProperty('floating', True) repolish(self) @@ -290,7 +290,7 @@ def unfloat(self): self.hide() self.setAttribute(Qt.WA_Hover, False) flags = Qt.Widget - self.setParent(self.manager().dock_area, flags) + self.setParent(self.manager().dock_area(), flags) self.layout().setContentsMargins(QMargins(0, 0, 0, 0)) self.unsetCursor() self.setProperty('floating', False) @@ -436,7 +436,7 @@ def closeEvent(self, event): """ Handle the close event for the dock container. """ - self.manager()._close_container(self, event) + self.manager().close_container(self, event) def titleBarMousePressEvent(self, event): """ Handle a mouse press event on the title bar. @@ -474,7 +474,7 @@ def titleBarMouseMoveEvent(self, event): if state.dragging: if self.isWindow(): self.move(global_pos - state.press_pos) - self.manager()._frame_moved(self, global_pos) + self.manager().frame_moved(self, global_pos) return True # Ensure the drag has crossed the app drag threshold. @@ -531,7 +531,7 @@ def titleBarMouseReleaseEvent(self, event): if state.press_pos is not None: self.releaseMouse() if self.isWindow(): - self.manager()._frame_released(self, event.globalPos()) + self.manager().frame_released(self, event.globalPos()) state.dragging = False state.press_pos = None return True diff --git a/enaml/qt/docking/q_dock_frame.py b/enaml/qt/docking/q_dock_frame.py index f4cbe523b..f78a5fd65 100644 --- a/enaml/qt/docking/q_dock_frame.py +++ b/enaml/qt/docking/q_dock_frame.py @@ -115,9 +115,7 @@ def raiseFrame(self): """ manager = self._manager if manager is not None: - frames = manager.dock_frames - frames.remove(self) - frames.append(self) + manager.raise_frame(self) def titleBarGeometry(self): """ Get the geometry rect for the title bar. diff --git a/enaml/qt/docking/q_dock_window.py b/enaml/qt/docking/q_dock_window.py index 7cf0996f4..58b5ca142 100644 --- a/enaml/qt/docking/q_dock_window.py +++ b/enaml/qt/docking/q_dock_window.py @@ -311,7 +311,7 @@ def closeEvent(self, event): """ Handle a close event for the window. """ - self.manager()._close_window(self, event) + self.manager().close_window(self, event) def resizeEvent(self, event): """ Handle the resize event for the dock window. @@ -377,7 +377,7 @@ def titleBarMouseMoveEvent(self, event): state.press_pos.setX(new_x) state.press_pos.setY(margins.top() / 2) self.move(global_pos - state.press_pos) - self.manager()._frame_moved(self, global_pos) + self.manager().frame_moved(self, global_pos) return True return False @@ -393,7 +393,7 @@ def titleBarMouseReleaseEvent(self, event): if event.button() == Qt.LeftButton: state = self.frame_state if state.press_pos is not None: - self.manager()._frame_released(self, event.globalPos()) + self.manager().frame_released(self, event.globalPos()) state.dragging = False state.press_pos = None return True From 34c3366def20ff0c14d7009d231e0d36ea0d3f6c Mon Sep 17 00:00:00 2001 From: Chris Colbert Date: Tue, 28 May 2013 23:14:26 -0400 Subject: [PATCH 02/11] snap floating dock frames when near one another --- enaml/qt/docking/dock_manager.py | 75 +++++++++++++++++++++++----- enaml/qt/docking/q_dock_container.py | 5 +- enaml/qt/docking/q_dock_window.py | 7 ++- 3 files changed, 72 insertions(+), 15 deletions(-) diff --git a/enaml/qt/docking/dock_manager.py b/enaml/qt/docking/dock_manager.py index 448b980d5..9e1239236 100644 --- a/enaml/qt/docking/dock_manager.py +++ b/enaml/qt/docking/dock_manager.py @@ -11,7 +11,7 @@ from PyQt4.QtCore import Qt, QPoint, QRect, QObject, QMetaObject from PyQt4.QtGui import QApplication -from atom.api import Atom, Typed, List +from atom.api import Atom, Int, Typed, List from enaml.layout.dock_layout import docklayout, dockarea, dockitem @@ -112,10 +112,6 @@ def processArea(self, area): class DockManager(Atom): """ A class which manages the docking behavior of a dock area. - The manager is used by attaching it to a QDockArea and then adding - dock items via the 'add_item' method and then setting a layout via - the 'apply_layout' method. See the docstring of the public api methods for more functionality. - """ #: The handler which holds the primary dock area. _dock_area = Typed(QDockArea) @@ -133,6 +129,9 @@ class DockManager(Atom): #: 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) + def __init__(self, dock_area): """ Initialize a DockingManager. @@ -406,6 +405,54 @@ def stack_under_top(self, frame): frames.remove(frame) frames.insert(-1, frame) + def snap_adjust(self, frame, pos): + """ Adjust the snap position for a dock frame. + + This method computes a target move position given a potential + move position for a free floating dock frame. It takes into + account the other floating windows in the neighborhood and + computes a snap position if there is a window within range. + + Parameters + ---------- + frame : QDockFrame + The free floating dock frame being dragged by the user. + + pos : QPoint + The global proposed new position of the frame. + + Returns + ------- + result : QPoint + The adjusted global position to use as the goal for the + move operation. + + """ + dist = self._snap_dist + frame_pos = QPoint(pos) + frame_size = frame.frameGeometry().size() + for other in self._floating_frames(): + if other is not frame: + frame_geo = QRect(frame_pos, frame_size) + other_geo = other.frameGeometry() + boundary = other_geo.adjusted(-dist, -dist, dist, dist) + if frame_geo.intersects(boundary): + dx = other_geo.left() - (frame_geo.right() + 1) + if dx > -dist: + frame_pos.setX(frame_pos.x() + dx) + else: + dx = frame_geo.left() - (other_geo.right() + 1) + if dx > -dist: + frame_pos.setX(frame_pos.x() - dx) + dy = other_geo.top() - (frame_geo.bottom() + 1) + if dy > -dist: + frame_pos.setY(frame_pos.y() + dy) + else: + dy = frame_geo.top() - (other_geo.bottom() + 1) + if dy > -dist: + frame_pos.setY(frame_pos.y() - dy) + return frame_pos + def frame_moved(self, frame, pos): """ Handle a dock frame being moved by the user. @@ -657,9 +704,14 @@ def _find_containers(self, names, missing=None): missing(name) return res - def _iter_dock_targets(self): + 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 @@ -668,7 +720,7 @@ def _iter_dock_targets(self): """ for target in reversed(self._dock_frames): - if target.isWindow(): + if target is not frame and target.isWindow(): if isinstance(target, QDockContainer): yield target elif isinstance(target, QDockWindow): @@ -692,11 +744,10 @@ def _dock_target(self, frame, pos): The potential dock target for the frame and position. """ - for target in self._iter_dock_targets(): - if target is not frame: - local = target.mapFromGlobal(pos) - if target.rect().contains(local): - return target + for target in self._iter_dock_targets(frame): + local = target.mapFromGlobal(pos) + if target.rect().contains(local): + return target @contextmanager def _dock_context(self, container): diff --git a/enaml/qt/docking/q_dock_container.py b/enaml/qt/docking/q_dock_container.py index 572cb04f0..395ef21c9 100644 --- a/enaml/qt/docking/q_dock_container.py +++ b/enaml/qt/docking/q_dock_container.py @@ -473,7 +473,10 @@ def titleBarMouseMoveEvent(self, event): global_pos = event.globalPos() if state.dragging: if self.isWindow(): - self.move(global_pos - state.press_pos) + manager = self.manager() + pos = global_pos - state.press_pos + pos = manager.snap_adjust(self, pos) + self.move(pos) self.manager().frame_moved(self, global_pos) return True diff --git a/enaml/qt/docking/q_dock_window.py b/enaml/qt/docking/q_dock_window.py index 58b5ca142..13c1c1c53 100644 --- a/enaml/qt/docking/q_dock_window.py +++ b/enaml/qt/docking/q_dock_window.py @@ -376,8 +376,11 @@ def titleBarMouseMoveEvent(self, event): new_x = max(5, min(test_x, max_x)) state.press_pos.setX(new_x) state.press_pos.setY(margins.top() / 2) - self.move(global_pos - state.press_pos) - self.manager().frame_moved(self, global_pos) + manager = self.manager() + pos = global_pos - state.press_pos + pos = manager.snap_adjust(self, pos) + self.move(pos) + manager.frame_moved(self, global_pos) return True return False From 85212ef874ee1c84a77971c6b7561d10c797f743 Mon Sep 17 00:00:00 2001 From: Chris Colbert Date: Wed, 29 May 2013 12:08:18 -0400 Subject: [PATCH 03/11] add linked and unlinked button bitmaps --- enaml/qt/docking/xbms.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/enaml/qt/docking/xbms.py b/enaml/qt/docking/xbms.py index 7c06196b3..ecb4f4730 100644 --- a/enaml/qt/docking/xbms.py +++ b/enaml/qt/docking/xbms.py @@ -94,3 +94,29 @@ def toBitmap(self): 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, ]) + + +LINKED_BUTTON = XBM(10, 9, [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, + 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, + 1, 0, 1, 1, 1, 1, 1, 1, 0, 1, + 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, + 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, +]) + + +UNLINKED_BUTTON = XBM(10, 9, [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, + 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, + 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, + 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, + 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, +]) From fd885201c3e722bcab5aa2f2d97950b13f2e602f Mon Sep 17 00:00:00 2001 From: Chris Colbert Date: Wed, 29 May 2013 12:09:03 -0400 Subject: [PATCH 04/11] add a checkable bitmap button class --- enaml/qt/docking/q_bitmap_button.py | 77 +++++++++++++++++++++++------ 1 file changed, 63 insertions(+), 14 deletions(-) diff --git a/enaml/qt/docking/q_bitmap_button.py b/enaml/qt/docking/q_bitmap_button.py index 155421dc9..2095f86c5 100644 --- a/enaml/qt/docking/q_bitmap_button.py +++ b/enaml/qt/docking/q_bitmap_button.py @@ -89,7 +89,7 @@ def styleOption(self): opt.state |= QStyle.State_Sunken return opt - def drawBitmap(self, opt, painter): + def drawBitmap(self, bmp, opt, painter): """ Draw the bitmap for the button. The bitmap will be drawn with the foreground color set by @@ -97,6 +97,9 @@ def drawBitmap(self, opt, painter): Parameters ---------- + bmp : QBitmap + The bitmap to draw. + opt : QStyleOption The style option to use for drawing. @@ -104,20 +107,61 @@ def drawBitmap(self, opt, painter): 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 paintEvent(self, event): + """ Handle the paint event for the button. + + """ + painter = QPainter(self) + opt = self.styleOption() + self.style().drawPrimitive(QStyle.PE_Widget, opt, painter, self) bmp = self._bitmap if bmp is not None: - # 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) + self.drawBitmap(bmp, opt, painter) + + +class QCheckedBitmapButton(QBitmapButton): + """ A bitmap button subclass which supports a checked bitmap. + + """ + _checked_bitmap = None + + def __init__(self, parent=None): + """ Initialize a QCheckedBitmapButton. + + Parameters + ---------- + parent : QWidget or None + The parent widget of the button. + + """ + super(QCheckedBitmapButton, self).__init__(parent) + self.setCheckable(True) + + def checkedBitmap(self): + """ Get the bitmap associated with the button checked state. + + """ + return self._checked_bitmap + + def setCheckedBitmap(self, bitmap): + """ Set the bitmap associate with the button checked state. + + """ + self._checked_bitmap = bitmap + self.update() def paintEvent(self, event): """ Handle the paint event for the button. @@ -126,4 +170,9 @@ def paintEvent(self, event): painter = QPainter(self) opt = self.styleOption() self.style().drawPrimitive(QStyle.PE_Widget, opt, painter, self) - self.drawBitmap(opt, painter) + if self.isChecked(): + bmp = self._checked_bitmap or self._bitmap + else: + bmp = self._bitmap + if bmp is not None: + self.drawBitmap(bmp, opt, painter) From 720a30f47bcc99f80b9653f9891ffa1db17fbbc4 Mon Sep 17 00:00:00 2001 From: Chris Colbert Date: Wed, 29 May 2013 12:20:54 -0400 Subject: [PATCH 05/11] minor code cleanup --- enaml/qt/docking/q_dock_tab_widget.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/enaml/qt/docking/q_dock_tab_widget.py b/enaml/qt/docking/q_dock_tab_widget.py index a8093e839..dfa6ae49e 100644 --- a/enaml/qt/docking/q_dock_tab_widget.py +++ b/enaml/qt/docking/q_dock_tab_widget.py @@ -120,7 +120,7 @@ def tabInserted(self, index): """ button = QDockTabCloseButton(self) - button.setObjectName("docktab-close-button") + button.setObjectName('docktab-close-button') button.setBitmap(CLOSE_BUTTON.toBitmap()) button.setIconSize(QSize(14, 13)) button.clicked.connect(self._onCloseButtonClicked) From 42ae4273aeba0ca66f453759c73b2bdf25988522 Mon Sep 17 00:00:00 2001 From: Chris Colbert Date: Wed, 29 May 2013 12:40:37 -0400 Subject: [PATCH 06/11] add a link button to the dock window --- enaml/qt/docking/q_dock_window.py | 108 ++++++++++++++++++++++++------ 1 file changed, 86 insertions(+), 22 deletions(-) diff --git a/enaml/qt/docking/q_dock_window.py b/enaml/qt/docking/q_dock_window.py index 13c1c1c53..1a45f5f5f 100644 --- a/enaml/qt/docking/q_dock_window.py +++ b/enaml/qt/docking/q_dock_window.py @@ -10,11 +10,14 @@ from atom.api import Bool, Typed -from .q_bitmap_button import QBitmapButton +from .q_bitmap_button import QBitmapButton, QCheckedBitmapButton from .q_dock_area import QDockArea from .q_dock_frame import QDockFrame from .q_dock_frame_layout import QDockFrameLayout -from .xbms import CLOSE_BUTTON, MAXIMIZE_BUTTON, RESTORE_BUTTON +from .xbms import ( + CLOSE_BUTTON, MAXIMIZE_BUTTON, RESTORE_BUTTON, LINKED_BUTTON, + UNLINKED_BUTTON +) #: The maximum number of free windows to keep in the free list. @@ -30,13 +33,16 @@ class QDockWindowButtons(QFrame): """ #: A signal emitted when the maximize button is clicked. - maximizeButtonClicked = pyqtSignal() + maximizeButtonClicked = pyqtSignal(bool) #: A signal emitted when the restore button is clicked. - restoreButtonClicked = pyqtSignal() + restoreButtonClicked = pyqtSignal(bool) #: A signal emitted when the close button is closed. - closeButtonClicked = pyqtSignal() + closeButtonClicked = pyqtSignal(bool) + + #: A signal emitted when the link button is toggled. + linkButtonToggled = pyqtSignal(bool) #: Do not show any buttons in the widget. NoButtons = 0x0 @@ -50,6 +56,9 @@ class QDockWindowButtons(QFrame): #: Show the close button in the widget. CloseButton = 0x4 + #: Show the link button in the widget. + LinkButton = 0x8 + def __init__(self, parent=None): """ Initialize a QDockWindowButtons instance. @@ -60,30 +69,40 @@ def __init__(self, parent=None): """ super(QDockWindowButtons, self).__init__(parent) - self._buttons = self.CloseButton | self.MaximizeButton + self._buttons = ( + self.CloseButton | self.MaximizeButton | self.LinkButton + ) max_button = self._max_button = QBitmapButton(self) - max_button.setObjectName("dockwindow-maximize-button") + max_button.setObjectName('dockwindow-maximize-button') max_button.setBitmap(MAXIMIZE_BUTTON.toBitmap()) max_button.setIconSize(QSize(20, 15)) max_button.setVisible(self._buttons & self.MaximizeButton) restore_button = self._restore_button = QBitmapButton(self) - restore_button.setObjectName("dockwindow-restore-button") + restore_button.setObjectName('dockwindow-restore-button') restore_button.setBitmap(RESTORE_BUTTON.toBitmap()) restore_button.setIconSize(QSize(20, 15)) restore_button.setVisible(self._buttons & self.RestoreButton) close_button = self._close_button = QBitmapButton(self) - close_button.setObjectName("dockwindow-close-button") + close_button.setObjectName('dockwindow-close-button') close_button.setBitmap(CLOSE_BUTTON.toBitmap()) close_button.setIconSize(QSize(34, 15)) close_button.setVisible(self._buttons & self.CloseButton) + link_button = self._link_button = QCheckedBitmapButton(self) + link_button.setObjectName('dockwindow-link-button') + link_button.setBitmap(UNLINKED_BUTTON.toBitmap()) + link_button.setCheckedBitmap(LINKED_BUTTON.toBitmap()) + link_button.setIconSize(QSize(20, 15)) + link_button.setVisible(self._buttons & self.LinkButton) + layout = QHBoxLayout() layout.setContentsMargins(QMargins(0, 0, 0, 0)) layout.setSpacing(1) + layout.addWidget(link_button) layout.addWidget(max_button) layout.addWidget(restore_button) layout.addWidget(close_button) @@ -93,6 +112,7 @@ def __init__(self, parent=None): max_button.clicked.connect(self.maximizeButtonClicked) restore_button.clicked.connect(self.restoreButtonClicked) close_button.clicked.connect(self.closeButtonClicked) + link_button.toggled.connect(self.linkButtonToggled) #-------------------------------------------------------------------------- # Public API @@ -121,6 +141,29 @@ def setButtons(self, buttons): self._max_button.setVisible(buttons & self.MaximizeButton) self._restore_button.setVisible(buttons & self.RestoreButton) self._close_button.setVisible(buttons & self.CloseButton) + self._link_button.setVisible(buttons & self.LinkButton) + + def isLinked(self): + """ Get whether the link button is checked. + + Returns + ------- + result : bool + True if the link button is checked, False otherwise. + + """ + return self._link_button.isChecked() + + def setLinked(self, linked): + """ Set whether or not the link button is checked. + + Parameters + ---------- + linked : bool + True if the link button should be checked, False otherwise. + + """ + self._link_button.setChecked(linked) class QDockWindow(QDockFrame): @@ -226,7 +269,10 @@ def showMaximized(self): buttons = title_buttons.buttons() buttons |= title_buttons.RestoreButton buttons &= ~title_buttons.MaximizeButton + buttons &= ~title_buttons.LinkButton title_buttons.setButtons(buttons) + title_buttons.setLinked(False) + self._updateButtonGeometry() def showNormal(self): """ Handle a show normal request for the window. @@ -234,6 +280,7 @@ def showNormal(self): """ super(QDockWindow, self).showNormal() self.applyNormalState() + self._updateButtonGeometry() def applyNormalState(self): """ Apply the proper state for normal window geometry. @@ -243,8 +290,10 @@ def applyNormalState(self): title_buttons = self._title_buttons buttons = title_buttons.buttons() buttons |= title_buttons.MaximizeButton + buttons |= title_buttons.LinkButton buttons &= ~title_buttons.RestoreButton title_buttons.setButtons(buttons) + title_buttons.setLinked(False) def titleBarGeometry(self): """ Get the geometry rect for the title bar. @@ -304,6 +353,12 @@ def setDockArea(self, dock_area): """ self.layout().setWidget(dock_area) + def isLinked(self): + """ Get whether or not the window is linked. + + """ + return self._title_buttons.isLinked() + #-------------------------------------------------------------------------- # Event Handlers #-------------------------------------------------------------------------- @@ -318,13 +373,7 @@ def resizeEvent(self, event): """ super(QDockWindow, self).resizeEvent(event) - 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) + self._updateButtonGeometry() def hoverMoveEvent(self, event): """ Handle the hover move event for the dock window. @@ -376,11 +425,8 @@ def titleBarMouseMoveEvent(self, event): new_x = max(5, min(test_x, max_x)) state.press_pos.setX(new_x) state.press_pos.setY(margins.top() / 2) - manager = self.manager() - pos = global_pos - state.press_pos - pos = manager.snap_adjust(self, pos) - self.move(pos) - manager.frame_moved(self, global_pos) + target_pos = global_pos - state.press_pos + self.manager().drag_move_frame(self, target_pos, global_pos) return True return False @@ -396,8 +442,26 @@ def titleBarMouseReleaseEvent(self, event): if event.button() == Qt.LeftButton: state = self.frame_state if state.press_pos is not None: - self.manager().frame_released(self, event.globalPos()) + self.manager().drag_release_frame(self, event.globalPos()) state.dragging = False state.press_pos = None return True return False + + #-------------------------------------------------------------------------- + # Private API + #-------------------------------------------------------------------------- + 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) From 02381e0379f938af463843475787abfb7723effb Mon Sep 17 00:00:00 2001 From: Chris Colbert Date: Wed, 29 May 2013 12:40:59 -0400 Subject: [PATCH 07/11] add a link button to the title bar --- enaml/qt/docking/q_dock_title_bar.py | 82 +++++++++++++++++++++++++--- 1 file changed, 74 insertions(+), 8 deletions(-) diff --git a/enaml/qt/docking/q_dock_title_bar.py b/enaml/qt/docking/q_dock_title_bar.py index ee89822fd..5383e5ff2 100644 --- a/enaml/qt/docking/q_dock_title_bar.py +++ b/enaml/qt/docking/q_dock_title_bar.py @@ -8,10 +8,13 @@ from PyQt4.QtCore import QSize, QMargins, pyqtSignal from PyQt4.QtGui import QWidget, QFrame, QHBoxLayout -from .q_bitmap_button import QBitmapButton +from .q_bitmap_button import QBitmapButton, QCheckedBitmapButton from .q_icon_widget import QIconWidget from .q_text_label import QTextLabel -from .xbms import CLOSE_BUTTON, MAXIMIZE_BUTTON, RESTORE_BUTTON +from .xbms import ( + CLOSE_BUTTON, MAXIMIZE_BUTTON, RESTORE_BUTTON, LINKED_BUTTON, + UNLINKED_BUTTON +) class IDockTitleBar(QWidget): @@ -19,13 +22,16 @@ class IDockTitleBar(QWidget): """ #: A signal emitted when the maximize button is clicked. - maximizeButtonClicked = pyqtSignal() + maximizeButtonClicked = pyqtSignal(bool) #: A signal emitted when the restore button is clicked. - restoreButtonClicked = pyqtSignal() + restoreButtonClicked = pyqtSignal(bool) #: A signal emitted when the close button is clicked. - closeButtonClicked = pyqtSignal() + closeButtonClicked = pyqtSignal(bool) + + #: A signal emitted when the link button is toggled. + linkButtonToggled = pyqtSignal(bool) #: Do not show any buttons in the title bar. NoButtons = 0x0 @@ -39,6 +45,9 @@ class IDockTitleBar(QWidget): #: Show the close button in the title bar. CloseButton = 0x4 + #: Show the link button in the title bar. + LinkButton = 0x8 + def buttons(self): """ Get the buttons to show in the title bar. @@ -128,6 +137,28 @@ def setIconSize(self, size): """ raise NotImplementedError + def isLinked(self): + """ Get whether the link button is checked. + + Returns + ------- + result : bool + True if the link button is checked, False otherwise. + + """ + raise NotImplementedError + + def setLinked(self, linked): + """ Set whether or not the link button is checked. + + Parameters + ---------- + linked : bool + True if the link button should be checked, False otherwise. + + """ + raise NotImplementedError + class QDockTitleBar(QFrame, IDockTitleBar): """ A concrete implementation of IDockTitleBar. @@ -136,13 +167,16 @@ class QDockTitleBar(QFrame, IDockTitleBar): """ #: A signal emitted when the maximize button is clicked. - maximizeButtonClicked = pyqtSignal() + maximizeButtonClicked = pyqtSignal(bool) #: A signal emitted when the restore button is clicked. - restoreButtonClicked = pyqtSignal() + restoreButtonClicked = pyqtSignal(bool) #: A signal emitted when the close button is clicked. - closeButtonClicked = pyqtSignal() + closeButtonClicked = pyqtSignal(bool) + + #: A signal emitted when the link button is toggled. + linkButtonToggled = pyqtSignal(bool) def __init__(self, parent=None): """ Initialize a QDockTitleBar. @@ -181,6 +215,13 @@ def __init__(self, parent=None): close_button.setIconSize(btn_size) close_button.setVisible(self._buttons & self.CloseButton) + link_button = self._link_button = QCheckedBitmapButton(self) + link_button.setObjectName('docktitlebar-link-button') + link_button.setBitmap(UNLINKED_BUTTON.toBitmap()) + link_button.setCheckedBitmap(LINKED_BUTTON.toBitmap()) + link_button.setIconSize(btn_size) + link_button.setVisible(self._buttons & self.LinkButton) + layout = QHBoxLayout() layout.setContentsMargins(QMargins(5, 2, 5, 2)) layout.setSpacing(1) @@ -188,6 +229,7 @@ def __init__(self, parent=None): layout.addSpacing(0) layout.addWidget(title_label, 1) layout.addSpacing(4) + layout.addWidget(link_button) layout.addWidget(max_button) layout.addWidget(restore_button) layout.addWidget(close_button) @@ -197,6 +239,7 @@ def __init__(self, parent=None): max_button.clicked.connect(self.maximizeButtonClicked) restore_button.clicked.connect(self.restoreButtonClicked) close_button.clicked.connect(self.closeButtonClicked) + link_button.toggled.connect(self.linkButtonToggled) #-------------------------------------------------------------------------- # IDockItemTitleBar API @@ -225,6 +268,7 @@ def setButtons(self, buttons): self._max_button.setVisible(buttons & self.MaximizeButton) self._restore_button.setVisible(buttons & self.RestoreButton) self._close_button.setVisible(buttons & self.CloseButton) + self._link_button.setVisible(buttons & self.LinkButton) def title(self): """ Get the title string of the title bar. @@ -298,3 +342,25 @@ def setIconSize(self, size): """ self._title_icon.setIconSize(size) + + def isLinked(self): + """ Get whether the link button is checked. + + Returns + ------- + result : bool + True if the link button is checked, False otherwise. + + """ + return self._link_button.isChecked() + + def setLinked(self, linked): + """ Set whether or not the link button is checked. + + Parameters + ---------- + linked : bool + True if the link button should be checked, False otherwise. + + """ + self._link_button.setChecked(linked) From fb4f58fd625b3c8da48a3ca5bd6167c3a6363130 Mon Sep 17 00:00:00 2001 From: Chris Colbert Date: Wed, 29 May 2013 12:41:13 -0400 Subject: [PATCH 08/11] proxy the link button state --- enaml/qt/docking/q_dock_item.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/enaml/qt/docking/q_dock_item.py b/enaml/qt/docking/q_dock_item.py index 9691f3c4b..4efbdcdd0 100644 --- a/enaml/qt/docking/q_dock_item.py +++ b/enaml/qt/docking/q_dock_item.py @@ -353,6 +353,28 @@ def setIconSize(self, size): """ self.titleBarWidget().setIconSize(size) + def isLinked(self): + """ Get whether or not this dock item is linked. + + Returns + ------- + result : bool + True if the item is linked, False otherwise. + + """ + return self.titleBarWidget().isLinked() + + def setLinked(self, linked): + """ Set whether or not the dock item is linked. + + Parameters + ---------- + linked : bool + True if the dock item should be linked, False otherwise. + + """ + self.titleBarWidget().setLinked(linked) + def closable(self): """ Get whether or not the dock item is closable. From 2162c9fa5e03e88bfcf00795bd1d6c5214b08c45 Mon Sep 17 00:00:00 2001 From: Chris Colbert Date: Wed, 29 May 2013 12:41:34 -0400 Subject: [PATCH 09/11] handle the link button in the title bar --- enaml/qt/docking/q_dock_container.py | 68 ++++++++++++++++++++++++---- 1 file changed, 58 insertions(+), 10 deletions(-) diff --git a/enaml/qt/docking/q_dock_container.py b/enaml/qt/docking/q_dock_container.py index 395ef21c9..80391c0a0 100644 --- a/enaml/qt/docking/q_dock_container.py +++ b/enaml/qt/docking/q_dock_container.py @@ -133,14 +133,17 @@ def showMaximized(self): """ Handle the show maximized request for the dock container. """ - def update_buttons(bar): + def update_buttons(bar, link=False): buttons = bar.buttons() buttons |= bar.RestoreButton buttons &= ~bar.MaximizeButton + if link: + buttons &= ~bar.LinkButton bar.setButtons(buttons) if self.isWindow(): super(QDockContainer, self).showMaximized() - update_buttons(self.dockItem().titleBarWidget()) + self.setLinked(False) + update_buttons(self.dockItem().titleBarWidget(), link=True) else: area = self.parentDockArea() if area is not None: @@ -154,14 +157,17 @@ def showNormal(self): """ Handle the show normal request for the dock container. """ - def update_buttons(bar): + def update_buttons(bar, link=False): buttons = bar.buttons() buttons |= bar.MaximizeButton buttons &= ~bar.RestoreButton + if link: + buttons |= bar.LinkButton bar.setButtons(buttons) if self.isWindow(): super(QDockContainer, self).showNormal() - update_buttons(self.dockItem().titleBarWidget()) + self.setLinked(False) + update_buttons(self.dockItem().titleBarWidget(), link=True) elif self.frame_state.item_is_maximized: item = self.dockItem() update_buttons(item.titleBarWidget()) @@ -238,6 +244,27 @@ def closable(self): return item.closable() return True + def isLinked(self): + """ Get whether or not the container is linked. + + This proxies the call to the underlying dock item. + + """ + item = self.dockItem() + if item is not None: + return item.isLinked() + return False + + def setLinked(self, linked): + """ Set whether or not the container should be linked. + + This proxies the call to the underlying dock item. + + """ + item = self.dockItem() + if item is not None: + item.setLinked(linked) + def showTitleBar(self): """ Show the title bar for the container. @@ -258,6 +285,24 @@ def hideTitleBar(self): if item is not None: item.titleBarWidget().hide() + def showLinkButton(self): + """ Show the link button on the title bar. + + """ + item = self.dockItem() + if item is not None: + bar = item.titleBarWidget() + bar.setButtons(bar.buttons() | bar.LinkButton) + + def hideLinkButton(self): + """ Show the link button on the title bar. + + """ + item = self.dockItem() + if item is not None: + bar = item.titleBarWidget() + bar.setButtons(bar.buttons() & ~bar.LinkButton) + def reset(self): """ Reset the container to the initial pre-docked state. @@ -267,6 +312,8 @@ def reset(self): state.press_pos = None self.showNormal() self.unfloat() + self.hideLinkButton() + self.setLinked(False) self.showTitleBar() self.setAttribute(Qt.WA_WState_ExplicitShowHide, False) self.setAttribute(Qt.WA_WState_Hidden, False) @@ -281,6 +328,8 @@ def float(self): self.setParent(self.manager().dock_area(), flags) self.layout().setContentsMargins(QMargins(5, 5, 5, 5)) self.setProperty('floating', True) + self.setLinked(False) + self.showLinkButton() repolish(self) def unfloat(self): @@ -294,6 +343,8 @@ def unfloat(self): self.layout().setContentsMargins(QMargins(0, 0, 0, 0)) self.unsetCursor() self.setProperty('floating', False) + self.setLinked(False) + self.hideLinkButton() repolish(self) def parentDockArea(self): @@ -473,11 +524,8 @@ def titleBarMouseMoveEvent(self, event): global_pos = event.globalPos() if state.dragging: if self.isWindow(): - manager = self.manager() - pos = global_pos - state.press_pos - pos = manager.snap_adjust(self, pos) - self.move(pos) - self.manager().frame_moved(self, global_pos) + 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. @@ -534,7 +582,7 @@ def titleBarMouseReleaseEvent(self, event): if state.press_pos is not None: self.releaseMouse() if self.isWindow(): - self.manager().frame_released(self, event.globalPos()) + self.manager().drag_release_frame(self, event.globalPos()) state.dragging = False state.press_pos = None return True From 186b8a300db99db7ad3af9c798a88b2e2eb80ef1 Mon Sep 17 00:00:00 2001 From: Chris Colbert Date: Wed, 29 May 2013 12:42:02 -0400 Subject: [PATCH 10/11] move linked floating frames as a single unit --- enaml/qt/docking/dock_manager.py | 183 ++++++++++++++++++------------- 1 file changed, 107 insertions(+), 76 deletions(-) diff --git a/enaml/qt/docking/dock_manager.py b/enaml/qt/docking/dock_manager.py index 9e1239236..cae9ac724 100644 --- a/enaml/qt/docking/dock_manager.py +++ b/enaml/qt/docking/dock_manager.py @@ -405,91 +405,42 @@ def stack_under_top(self, frame): frames.remove(frame) frames.insert(-1, frame) - def snap_adjust(self, frame, pos): - """ Adjust the snap position for a dock frame. + def drag_move_frame(self, frame, target_pos, mouse_pos): + """ Move the floating frame to the target position. - This method computes a target move position given a potential - move position for a free floating dock frame. It takes into - account the other floating windows in the neighborhood and - computes a snap position if there is a window within range. + 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 + methos should not be called by user code. Parameters ---------- frame : QDockFrame - The free floating dock frame being dragged by the user. + The floating QDockFrame which should be moved. - pos : QPoint - The global proposed new position of the frame. + target_pos : QPoint + The global position which is the target of the move. - Returns - ------- - result : QPoint - The adjusted global position to use as the goal for the - move operation. - - """ - dist = self._snap_dist - frame_pos = QPoint(pos) - frame_size = frame.frameGeometry().size() - for other in self._floating_frames(): - if other is not frame: - frame_geo = QRect(frame_pos, frame_size) - other_geo = other.frameGeometry() - boundary = other_geo.adjusted(-dist, -dist, dist, dist) - if frame_geo.intersects(boundary): - dx = other_geo.left() - (frame_geo.right() + 1) - if dx > -dist: - frame_pos.setX(frame_pos.x() + dx) - else: - dx = frame_geo.left() - (other_geo.right() + 1) - if dx > -dist: - frame_pos.setX(frame_pos.x() - dx) - dy = other_geo.top() - (frame_geo.bottom() + 1) - if dy > -dist: - frame_pos.setY(frame_pos.y() + dy) - else: - dy = frame_geo.top() - (other_geo.bottom() + 1) - if dy > -dist: - frame_pos.setY(frame_pos.y() - dy) - return frame_pos - - def frame_moved(self, frame, pos): - """ Handle a dock frame being moved by the user. - - This method is called by the framework at the appropriate times - and should not be called directly by user code. It ensures that - the dock overlay guides are shown and hidden appropriately. - - Parameters - ---------- - frame : QDockFrame - The dock frame being dragged by the user. - - pos : QPoint - The global coordinates of the mouse position. + mouse_pos : QPoint + The global 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) - if target.layoutWidget() is None: - overlay.mouse_over_widget(target, local, empty=True) - else: - widget = layout_hit_test(target, local) - overlay.mouse_over_area(target, widget, local) - else: - overlay.hide() - - def frame_released(self, frame, pos): + # If the frame is unlinked, the target position is adjusted to + # snap to nearby neighbors. If the frame is linked, all other + # linked frames are moved by the same delta distance. + if not frame.isLinked(): + target_pos = self._snap_adjust(frame, target_pos) + delta = target_pos - frame.pos() + frame.move(target_pos) + if frame.isLinked(): + for other in self._floating_frames(): + if other is not frame and other.isLinked(): + other.move(other.pos() + delta) + self._update_drag_overlay(frame, mouse_pos) + + 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 @@ -749,6 +700,86 @@ def _dock_target(self, frame, pos): if target.rect().contains(local): return target + def _snap_adjust(self, frame, pos): + """ Adjust the snap position for a dock frame. + + This method computes a target move position given a potential + move position for a free floating dock frame. It takes into + account the other floating windows in the neighborhood and + computes a snap position if there is a window within range. + + Parameters + ---------- + frame : QDockFrame + The free floating dock frame being dragged by the user. + + pos : QPoint + The global target position of the frame. + + Returns + ------- + result : QPoint + The adjusted global position to use as the goal for the + move operation. + + """ + dist = self._snap_dist + frame_pos = QPoint(pos) + frame_size = frame.frameGeometry().size() + for other in self._floating_frames(): + if other is not frame: + frame_geo = QRect(frame_pos, frame_size) + other_geo = other.frameGeometry() + boundary = other_geo.adjusted(-dist, -dist, dist, dist) + if frame_geo.intersects(boundary): + dx = other_geo.left() - (frame_geo.right() + 1) + if dx > -dist: + frame_pos.setX(frame_pos.x() + dx) + else: + dx = frame_geo.left() - (other_geo.right() + 1) + if dx > -dist: + frame_pos.setX(frame_pos.x() - dx) + dy = other_geo.top() - (frame_geo.bottom() + 1) + if dy > -dist: + frame_pos.setY(frame_pos.y() + dy) + else: + dy = frame_geo.top() - (other_geo.bottom() + 1) + if dy > -dist: + frame_pos.setY(frame_pos.y() - dy) + return frame_pos + + 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) + if target.layoutWidget() is None: + overlay.mouse_over_widget(target, local, empty=True) + else: + widget = layout_hit_test(target, local) + overlay.mouse_over_area(target, widget, local) + else: + overlay.hide() + @contextmanager def _dock_context(self, container): """ Setup a dock context for a dock container. From 3a31646a52bb91f3c72cb8b1b06b9e71bc588b63 Mon Sep 17 00:00:00 2001 From: Chris Colbert Date: Wed, 29 May 2013 12:42:14 -0400 Subject: [PATCH 11/11] style the link buttons --- enaml/qt/docking/style_sheets.py | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/enaml/qt/docking/style_sheets.py b/enaml/qt/docking/style_sheets.py index d912cb25a..5a360c5a2 100644 --- a/enaml/qt/docking/style_sheets.py +++ b/enaml/qt/docking/style_sheets.py @@ -154,12 +154,14 @@ def register_style_sheet(name, sheet): } QBitmapButton#dockwindow-maximize-button:hover, - QBitmapButton#dockwindow-restore-button:hover { + QBitmapButton#dockwindow-restore-button:hover, + QBitmapButton#dockwindow-link-button:hover { background: #3665B3; } QBitmapButton#dockwindow-maximize-button:pressed, - QBitmapButton#dockwindow-restore-button:pressed { + QBitmapButton#dockwindow-restore-button:pressed, + QBitmapButton#dockwindow-link-button:pressed { background: #3D6099; } @@ -293,12 +295,14 @@ def register_style_sheet(name, sheet): } QBitmapButton#dockwindow-maximize-button:hover, - QBitmapButton#dockwindow-restore-button:hover { + QBitmapButton#dockwindow-restore-button:hover, + QBitmapButton#dockwindow-link-button:hover { background: rgb(175, 178, 183); } QBitmapButton#dockwindow-maximize-button:pressed, - QBitmapButton#dockwindow-restore-button:pressed { + QBitmapButton#dockwindow-restore-button:pressed, + QBitmapButton#dockwindow-link-button:pressed { background: rgb(144, 144, 152); } """) @@ -416,12 +420,14 @@ def register_style_sheet(name, sheet): } QBitmapButton#dockwindow-maximize-button:hover, - QBitmapButton#dockwindow-restore-button:hover { + QBitmapButton#dockwindow-restore-button:hover, + QBitmapButton#dockwindow-link-button:hover { background: #9E935D; } QBitmapButton#dockwindow-maximize-button:pressed, - QBitmapButton#dockwindow-restore-button:pressed { + QBitmapButton#dockwindow-restore-button:pressed, + QBitmapButton#dockwindow-link-button:pressed { background: rgb(105, 103, 88); } """) @@ -535,13 +541,15 @@ def register_style_sheet(name, sheet): } QBitmapButton#dockwindow-maximize-button:hover, - QBitmapButton#dockwindow-restore-button:hover { + QBitmapButton#dockwindow-restore-button:hover, + QBitmapButton#dockwindow-link-button:hover { background: #3665B3; color: white; } QBitmapButton#dockwindow-maximize-button:pressed, - QBitmapButton#dockwindow-restore-button:pressed { + QBitmapButton#dockwindow-restore-button:pressed, + QBitmapButton#dockwindow-link-button:pressed { background: #3D6099; } """)