-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add drag support for dockwidgets sharing same position #2369
Changes from 6 commits
e042cfc
6c3774b
0ad02d0
47f7121
0c2e1d6
e5d6dde
bf7ba2f
c22bc48
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -21,8 +21,10 @@ | |
# Qt imports | ||
from spyderlib.qt import PYQT5 | ||
from spyderlib.qt.QtGui import (QDockWidget, QWidget, QShortcut, QCursor, | ||
QKeySequence, QMainWindow, QApplication) | ||
from spyderlib.qt.QtCore import Qt, Signal | ||
QKeySequence, QMainWindow, QApplication, | ||
QTabBar) | ||
from spyderlib.qt.QtCore import Qt, Signal, QObject, QEvent, QPoint | ||
|
||
|
||
# Stdlib imports | ||
import sys | ||
|
@@ -55,6 +57,136 @@ def get_icon(self): | |
return self.plugin.get_plugin_icon() | ||
|
||
|
||
class TabFilter(QObject): | ||
""" | ||
Filter event attached to each QTabBar that holds 2 or more dockwidgets in | ||
charge of handling tab rearangement. | ||
|
||
This filter also holds the methods needed for the detection of a drag and | ||
the movement of tabs. | ||
""" | ||
sig_tab_moved = Signal(int, int) | ||
|
||
def __init__(self, dock_tabbar, main): | ||
QObject.__init__(self) | ||
self.dock_tabbar = dock_tabbar | ||
self.main = main | ||
self.moving = False | ||
self.from_index = None | ||
self.to_index = None | ||
|
||
# Helper methods | ||
def _get_plugin(self, index): | ||
"""Get plugin reference based on tab index.""" | ||
for plugin in self.main.widgetlist: | ||
if plugin.get_plugin_title() == self.dock_tabbar.tabText(index): | ||
return plugin | ||
|
||
def _get_plugins(self): | ||
""" | ||
Get a list of all the plugins references in the QTabBar to which this | ||
event filter was attached. | ||
""" | ||
plugins = [] | ||
for index in range(self.dock_tabbar.count()): | ||
plugin = self._get_plugin(index) | ||
plugins.append(plugin) | ||
return plugins | ||
|
||
def _fix_cursor(self, from_index, to_index): | ||
"""Fix mouse cursor position to adjust for different tab sizes.""" | ||
direction = abs(to_index - from_index)/(to_index - from_index) | ||
tab_width = self.dock_tabbar.tabRect(to_index).width() | ||
tab_x_min = self.dock_tabbar.tabRect(to_index).x() | ||
tab_x_max = tab_x_min + tab_width | ||
previous_width = self.dock_tabbar.tabRect(to_index - direction).width() | ||
|
||
delta = previous_width - tab_width | ||
if delta > 0: | ||
delta = delta * direction | ||
else: | ||
delta = 0 | ||
cursor = QCursor() | ||
pos = self.dock_tabbar.mapFromGlobal(cursor.pos()) | ||
x, y = pos.x(), pos.y() | ||
if x < tab_x_min or x > tab_x_max: | ||
new_pos = self.dock_tabbar.mapToGlobal(QPoint(x + delta, y)) | ||
cursor.setPos(new_pos) | ||
|
||
def eventFilter(self, obj, event): | ||
"""Filter mouse press events.""" | ||
event_type = event.type() | ||
if event_type == QEvent.MouseButtonPress: | ||
self.tab_pressed(event) | ||
return False | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why these return values are needed? Could you add a comment about their need and why sometimes are True and others False? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. An event filter intercepts the fired events, and it has to return True, if it propagates the events... or False if it captures the event.... It is the normal behavior .... There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I will add some info to the docsting though There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok, thanks! |
||
if event_type == QEvent.MouseMove: | ||
self.tab_moved(event) | ||
return True | ||
if event_type == QEvent.MouseButtonRelease: | ||
self.tab_released(event) | ||
return True | ||
return False | ||
|
||
def tab_pressed(self, event): | ||
"""Method called when a tab from a QTabBar has been pressed.""" | ||
self.from_index = self.dock_tabbar.tabAt(event.pos()) | ||
|
||
if event.button() == Qt.RightButton: | ||
if self.from_index == -1: | ||
self.show_nontab_menu() | ||
else: | ||
self.show_tab_menu() | ||
|
||
def tab_moved(self, event): | ||
"""Method called when a tab from a QTabBar has been moved.""" | ||
# If the left button isn't pressed anymore then return | ||
if not event.buttons() & Qt.LeftButton: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why not a regular There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. hmm True, proabably a leftover form the C++ code I took as example There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No I checked... buttons is an Or combination so I need the Binary And there... |
||
self.to_index = None | ||
return | ||
|
||
self.to_index = self.dock_tabbar.tabAt(event.pos()) | ||
|
||
if not self.moving and self.from_index != -1 and self.to_index != -1: | ||
QApplication.setOverrideCursor(Qt.ClosedHandCursor) | ||
self.moving = True | ||
|
||
if self.to_index == -1: | ||
self.to_index = self.from_index | ||
|
||
from_index, to_index = self.from_index, self.to_index | ||
if from_index != to_index and from_index != -1 and to_index != -1: | ||
self.sig_tab_moved.emit(from_index, to_index) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why is this signal needed if nothing connects to it? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. At this moment it does nothing, but I guess it could be useful if some behavior wants to be executed whenever a the tabs are reorganized. It is for completeness... There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Then please remove it :-) When (if) it's needed, it can easily be added. Right now it's just noise and adds confusion to the code. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done |
||
self.move_tab(from_index, to_index) | ||
self._fix_cursor(from_index, to_index) | ||
self.from_index = to_index | ||
|
||
def tab_released(self, event): | ||
"""Method called when a tab from a QTabBar has been released.""" | ||
QApplication.restoreOverrideCursor() | ||
self.moving = False | ||
|
||
def move_tab(self, from_index, to_index): | ||
"""Move a tab from a given index to a given index position.""" | ||
plugins = self._get_plugins() | ||
from_plugin = self._get_plugin(from_index) | ||
to_plugin = self._get_plugin(to_index) | ||
|
||
from_idx = plugins.index(from_plugin) | ||
to_idx = plugins.index(to_plugin) | ||
|
||
plugins[from_idx], plugins[to_idx] = plugins[to_idx], plugins[from_idx] | ||
|
||
for i in range(len(plugins)-1): | ||
self.main.tabify_plugins(plugins[i], plugins[i+1]) | ||
from_plugin.dockwidget.raise_() | ||
|
||
def show_tab_menu(self): | ||
"""Show the context menu assigned to tabs.""" | ||
|
||
def show_nontab_menu(self): | ||
"""Show the context menu assigned to nontabs section.""" | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Aren't these two last methods missing a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I could add them, but the docstring replaces the need for a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please add There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done |
||
|
||
class SpyderDockWidget(QDockWidget): | ||
"""Subclass to override needed methods""" | ||
DARWIN_STYLE = """ | ||
|
@@ -119,18 +251,49 @@ class SpyderDockWidget(QDockWidget): | |
|
||
plugin_closed = Signal() | ||
|
||
def __init__(self, *args, **kwargs): | ||
super(SpyderDockWidget, self).__init__(*args, **kwargs) | ||
def __init__(self, title, parent): | ||
super(SpyderDockWidget, self).__init__(title, parent) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Have you tried this in PyQt5? Is it working correctly there? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not really, but I do not see why it would not work with PyQt5 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I will check later There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sorry, problems with PyQt5 only happen with mixins. So, there shouldn't be any problem with this one :-) |
||
if sys.platform == 'darwin': | ||
self.setStyleSheet(self.DARWIN_STYLE) | ||
|
||
# Needed for the installation of the event filter | ||
self.title = title | ||
self.main = parent | ||
self.dock_tabbar = None | ||
|
||
# To track dockwidget changes the filter is installed when dockwidget | ||
# visibility changes. This installs the filter on startup and also | ||
# on dockwidgets that are undocked and then docked to a new location. | ||
self.visibilityChanged.connect(self.install_tab_event_filter) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why is this necessary? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When you move a dockwidget to a new position (location that was not existing before) this event gets triggered so I can add the filter to the QTabBar that gets created. Was the only way I could think of so far... Update If I use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hadn't you mentioned that you were going to use a different signal here? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I did, but then I remembered that it did not work properly, and I revert to this one. The problem of using the other signal is that it does not work when a dockwidget is moved to a new position. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok, no problem. |
||
|
||
def closeEvent(self, event): | ||
""" | ||
Reimplement Qt method to send a signal on close so that "Panes" main | ||
window menu can be updated correctly | ||
""" | ||
self.plugin_closed.emit() | ||
|
||
def install_tab_event_filter(self, value): | ||
""" | ||
Install an event filter to capture mouse events in the tabs of a | ||
QTabBar holding tabified dockwidgets. | ||
""" | ||
dock_tabbar = None | ||
tabbars = self.main.findChildren(QTabBar) | ||
for tabbar in tabbars: | ||
for tab in range(tabbar.count()): | ||
title = tabbar.tabText(tab) | ||
if title == self.title: | ||
dock_tabbar = tabbar | ||
break | ||
if dock_tabbar is not None: | ||
self.dock_tabbar = dock_tabbar | ||
# Install filter only once per QTabBar | ||
if getattr(self.dock_tabbar, 'filter', None) is None: | ||
self.dock_tabbar.filter = TabFilter(self.dock_tabbar, | ||
self.main) | ||
self.dock_tabbar.installEventFilter(self.dock_tabbar.filter) | ||
|
||
|
||
class SpyderPluginMixin(object): | ||
""" | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please change this to
Get a list of all plugin references in the QTabbar ...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure 👍