Skip to content
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

Merged
merged 8 commits into from
May 3, 2015
171 changes: 167 additions & 4 deletions spyderlib/plugins/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Copy link
Member

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 ...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure 👍

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
Copy link
Member

Choose a reason for hiding this comment

The 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?

Copy link
Member Author

Choose a reason for hiding this comment

The 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 ....

http://doc.qt.io/qt-4.8/eventsandfilters.html#event-filters

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will add some info to the docsting though

Copy link
Member

Choose a reason for hiding this comment

The 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:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not a regular and, instead of & here?

Copy link
Member Author

Choose a reason for hiding this comment

The 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

Copy link
Member Author

Choose a reason for hiding this comment

The 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...

http://doc.qt.io/qt-4.8/qmouseevent.html#buttons

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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this signal needed if nothing connects to it?

Copy link
Member Author

Choose a reason for hiding this comment

The 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...

Copy link
Member

Choose a reason for hiding this comment

The 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.

Copy link
Member Author

Choose a reason for hiding this comment

The 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."""

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aren't these two last methods missing a pass?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could add them, but the docstring replaces the need for a pass... I can add... or leave it like it is

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add pass to them. It feels weird without it :-)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done


class SpyderDockWidget(QDockWidget):
"""Subclass to override needed methods"""
DARWIN_STYLE = """
Expand Down Expand Up @@ -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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you tried this in PyQt5? Is it working correctly there?

Copy link
Member Author

Choose a reason for hiding this comment

The 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

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will check later

Copy link
Member

Choose a reason for hiding this comment

The 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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this necessary?

Copy link
Member Author

Choose a reason for hiding this comment

The 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
This allows for correct filter installation on startup, the overhead is not noticeable.

If I use dockLocationChanged signal it will only work when rearranging panes, or activating a new layout, but will not work for a restart.

Copy link
Member

Choose a reason for hiding this comment

The 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?

Copy link
Member Author

Choose a reason for hiding this comment

The 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.

Copy link
Member

Choose a reason for hiding this comment

The 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):
"""
Expand Down