Source code for labscript_utils.qtwidgets.dragdroptab

#####################################################################
#                                                                   #
# dragdroptab.py                                                    #
#                                                                   #
# Copyright 2013, Monash University                                 #
#                                                                   #
# This file is part of the labscript suite (see                     #
# http://labscriptsuite.org) and is licensed under the Simplified   #
# BSD License. See the license.txt file in the root of the project  #
# for the full license.                                             #
#                                                                   #
#####################################################################

import weakref
from collections import namedtuple, defaultdict

from qtutils.qt.QtGui import *
from qtutils.qt.QtWidgets import *
from qtutils.qt.QtCore import *


[docs]class debug(object): DEBUG = False depth = 0
[docs] @classmethod def trace(cls, f): """decorator to print function entries and exits""" if not cls.DEBUG: return f def wrapper(*args, **kwargs): print(' '*cls.depth + '->', f.__name__) try: cls.depth += 1 return f(*args, **kwargs) finally: cls.depth -= 1 print(' '*cls.depth + '<-', f.__name__) return wrapper
if debug.DEBUG: import sys print('sys.version:', sys.version) print('PyQt4:', 'PyQt4' in sys.modules) print('PyQt5:', 'PyQt5' in sys.modules) print('PySide:', 'PySide' in sys.modules) print('qtutils:', 'qtutils' in sys.modules) print('qtutils.qt:', 'qtutils.qt' in sys.modules) Tab = namedtuple('Tab', ['widget', 'text', 'data', 'text_color', 'tooltip', 'whats_this', 'button_left', 'button_right', 'icon']) class _BaseDragDropTabBar(QTabBar): """Base class for Limbo and DragDropTabBar containing the common class attributes and methods""" # The QPoint of the mouse relative to the top left corner the tab at the # time the drag began. Shared by all instances: _dragged_tab_grab_point = None @property def dragged_tab_grab_point(self): return self._dragged_tab_grab_point @dragged_tab_grab_point.setter def dragged_tab_grab_point(self, value): # Setter specifies the class because we want subclasses to all share # it: _BaseDragDropTabBar._dragged_tab_grab_point = value @debug.trace def remove_dragged_tab(self, index): """Remove the tab at the given index and return all its configuration""" tab = Tab(widget=self.parent().widget(index), text=self.tabText(index), data=self.tabData(index), text_color=self.tabTextColor(index), tooltip=self.tabToolTip(index), whats_this=self.tabWhatsThis(index), button_left=self.tabButton(index, QTabBar.LeftSide), button_right=self.tabButton(index, QTabBar.RightSide), icon=self.tabIcon(index)) self.parent().removeTab(index) if self.prev_active_tab is not None: self.setCurrentIndex(self.prev_active_tab) self.prev_active_tab = None return tab @debug.trace def add_dragged_tab(self, index, tab): """Insert the tab at the given index and set all of its configuration""" self.prev_active_tab = self.currentIndex() self.parent().insertTab(index, tab.widget, tab.text) self.setCurrentIndex(index) if tab.data: self.setTabData(index, tab.data) self.setTabTextColor(index, tab.text_color) if tab.tooltip: self.setTabToolTip(index, tab.tooltip) if tab.whats_this: self.setTabWhatsThis(index, tab.whats_this) if tab.button_left: self.setTabButton(index, QTabBar.LeftSide, tab.button_left) if tab.button_right: self.setTabButton(index, QTabBar.RightSide, tab.button_right) if tab.icon: self.setTabIcon(index, tab.icon) class _Limbo(_BaseDragDropTabBar): """A floating TabBar to be the parent of the tab when it is not in a DragDropTabBar""" def __init__(self): self.parent_tabwidget = QTabWidget() _BaseDragDropTabBar.__init__(self, self.parent_tabwidget) self.parent_tabwidget.setTabBar(self) self.previous_parent = None self.previous_index = None self.prev_active_tab = None self.setWindowFlags(Qt.ToolTip) self.setUsesScrollButtons(False) # For storing a pixmap to render during animations when we no longer # own the tab: self.pixmap = None self.animation_in_progress = False @debug.trace def remove_dragged_tab(self, index): # Grab a pixmap of our current contents for rendering in case there is # animation of the tab flying back to a tab bar: self.pixmap = QPixmap(self.size()) self.render(self.pixmap, QPoint(), QRegion(self.rect())) result = _BaseDragDropTabBar.remove_dragged_tab(self, index) self.hide() return result @debug.trace def animation_starting(self): self.animation_in_progress = True self.show() @debug.trace def animation_over(self): if self.animation_in_progress: self.pixmap = None self.animation_in_progress = False self.hide() @debug.trace def add_dragged_tab(self, index, tab): self.animation_over() # cancel any animation in progress result = _BaseDragDropTabBar.add_dragged_tab(self, index, tab) self.show() return result @debug.trace def update_tab_index(self, index, pos): """We only have one tab index, so it's not going to change.""" return index @debug.trace def tabLayoutChange(self): initial_size = self.size() if self.count(): self.resize(self.tabSizeHint(0)) self.update() @debug.trace def insertion_index_at(self, pos): # Only ever insert at zero: return 0 @debug.trace def update(self): """Move to keep the tab grabbed by the mouse. grab_point is the position on the tab relative to its top left corner where it is grabbed by the mouse. Use current mouse position rather than that associated with any event triggering this, for maximal responsiveness.""" if self.dragged_tab_grab_point is not None: self.move(QCursor.pos() - self.dragged_tab_grab_point) _BaseDragDropTabBar.update(self) @debug.trace def paintEvent(self, event): if self.animation_in_progress: # Just draw the pixmap we've been given painter = QPainter(self) painter.drawPixmap(QPoint(), self.pixmap) painter.end() else: _BaseDragDropTabBar.paintEvent(self, event)
[docs]class TabAnimation(QAbstractAnimation): # We move tabs with speed proportional to the distance from their target. # Animation timescale - has units of time (milliseconds), but can # be thought of as the velocity in pixels per millisecond per pixel of # displacement that the object is from its target. tau = float(60)
[docs] def __init__(self, parent): QAbstractAnimation.__init__(self, parent) # The left edges of where the tabs will be drawn. This animates over # time to approach the left edge as returned by parent.tabRect(). self.positions = [] # The position of the floating limbo tab, if it's in the process of being # sucked back into the tab bar that owns this animation object: self.limbo_position = None self.limbo_target_tab = None self.limbo = None self.previous_time = 0 # A flag to set to avoid recursion when we ask widgets to update: self.callback_in_progress = False
[docs] @debug.trace def duration(self): return -1
[docs] @debug.trace def ensure_running(self): # Don't recurse: if self.callback_in_progress: return if self.state() == QAbstractAnimation.Stopped: self.start()
[docs] @debug.trace def target(self, i): """Return the target position we are animating toward for a tab""" return self.parent().tabRect(i).left()
[docs] @debug.trace def tabInserted(self, index): self.positions.insert(index, self.parent().tabRect(index).left()) self.ensure_running()
[docs] @debug.trace def tabRemoved(self, index): del self.positions[index] self.ensure_running()
[docs] @debug.trace def on_tab_moved(self, source_index, dest_index): self.positions.insert(dest_index, self.positions.pop(source_index)) self.ensure_running()
[docs] @debug.trace def animate_limbo(self, limbo, index): """If the floating tab in limbo is being sucked back into one of our tabs, then we can animate that by hiding the relevant tab rect off to the side somwhere whilst the floating tab swoops in.""" # floating tab doesn't own it anymore. self.limbo = limbo self.limbo.animation_starting() self.limbo_position = self.parent().mapFromGlobal(limbo.pos()) self.limbo_target_tab = index self.ensure_running()
[docs] @debug.trace def updateCurrentTime(self, current_time): dt = current_time - self.previous_time self.previous_time = current_time finished = True # Move tabs toward their target: for i, pos in enumerate(self.positions): target_pos = self.target(i) dx = target_pos - pos # Animate while the tab is more than a pixel away from its target: if abs(dx) > 1: finished = False new_pos = pos + dx * dt / self.tau # Check for overshoot: if dx * (target_pos - new_pos) < 0: new_pos = target_pos self.positions[i] = new_pos else: # Once it's close enough, snap it to the final value self.positions[i] = target_pos # move the floating tab toward its target, if applicable: if self.limbo is not None: pos_x = self.limbo_position.x() pos_y = self.limbo_position.y() target_pos = self.parent().tabRect(self.limbo_target_tab).topLeft() target_pos_x = target_pos.x() target_pos_y = target_pos.y() dx = target_pos_x - pos_x dy = target_pos_y - pos_y # Animate while the floating tab is more 15 pixels of taxicab- # metric distance away from its target: if abs(dx) + abs(dy) > 15: finished = False new_pos_x = pos_x + dx * dt / self.tau new_pos_y = pos_y + dy * dt / self.tau # Check for overshoot: if dx * (target_pos_x - new_pos_x) < 0: new_pos_x = target_pos_x if dy * (target_pos_y - new_pos_y) < 0: new_pos_y = target_pos_y self.limbo_position = QPoint( int(round(new_pos_x)), int(round(new_pos_y)) ) else: self.limbo.animation_over() self.limbo = None self.limbo_position = None self.limbo_target_tab = None if finished: self.previous_time = 0 self.stop() # Update the parent whilst blocking signals back to us to prevent # recursion: self.callback_in_progress = True if self.limbo is not None: self.limbo.move(self.parent().mapToGlobal(self.limbo_position)) self.limbo.update() self.parent().update() self.callback_in_progress = False
[docs]class DragDropTabBar(_BaseDragDropTabBar): # Keeping track of which tab widgets are in each group (that is, share a # common group_id): tab_widgets = defaultdict(weakref.WeakSet) # Whether or not a drag is in progress. It is important to have this in # addition to the below information so that we can set it to False when a # drag is about to be cancelled, even though we are not going to set the # below variables to None until after some processing. During that # processing, re-entrant event processing can see that there is no drag in # progress even though the below variables are still not None, and know # not to act as if there is a drag in progress (since the drag is in the # process of being cancelled). _drag_in_progress = False # The index and parent TabBar of the dragged tab, or None if no drag is in # progress. Shared by all instances: _dragged_tab_index = None _dragged_tab_parent = None # A TabWidget to hold the tab being dragged. Shared by all instances, but # not instantiated until first instance is created, since there may not be # a QApplication at import time. limbo = None SCROLL_BUTTON_WIDTH = 15 FLUSH_GAP = 5 SCROLL_BUTTON_GAP = 2
[docs] def __init__(self, parent, group_id): _BaseDragDropTabBar.__init__(self, parent) self.group_id = group_id self.tab_widgets[group_id].add(self.parent()) self.prev_active_tab = None if self.limbo is None: # One Limbo object for all instances: self.__class__.limbo = _Limbo() self.animation = TabAnimation(self) self.tabMoved.connect(self.animation.on_tab_moved) self.scroll_offset = 0 self.left_scrollbutton = QToolButton(self) self.right_scrollbutton = QToolButton(self) self.left_scrollbutton.setArrowType(Qt.LeftArrow) self.right_scrollbutton.setArrowType(Qt.RightArrow) self.left_scrollbutton.setAutoRepeat(True) self.right_scrollbutton.setAutoRepeat(True) self.left_scrollbutton.clicked.connect( lambda: self.on_scroll_button_clicked(self.left_scrollbutton)) self.right_scrollbutton.clicked.connect( lambda: self.on_scroll_button_clicked(self.right_scrollbutton)) self.currentChanged.connect(self.ensure_visible) self.left_scrollbutton.hide() self.right_scrollbutton.hide() _BaseDragDropTabBar.setUsesScrollButtons(self, False) self.setElideMode(Qt.ElideRight) self.uses_scrollbuttons = False
[docs] @debug.trace def setUsesScrollButtons(self, uses_scrollbuttons): self.uses_scrollbuttons = uses_scrollbuttons if uses_scrollbuttons: # No elision if we are scrollable: self.setElideMode(Qt.ElideNone) else: self.setElideMode(Qt.ElideRight) self.update_scroll_button_state() self.update()
def _leftflush(self): """The position a tab's left edge should be when it is otherwise' made flush with the left of the tabBar""" return self.FLUSH_GAP def _rightflush(self): """The position a tab's right edge should be when it is otherwise made flush with the right edge of the tabBar. Includes space for the scroll buttons""" return (self.width() - 2 * self.SCROLL_BUTTON_WIDTH - self.FLUSH_GAP - self.SCROLL_BUTTON_GAP)
[docs] @debug.trace def ensure_visible(self, index, prefer_left=True): # Ensure the tab is visible if we're using scrollbuttons: if index == -1 or not self.uses_scrollbuttons: return left_protruding_width = -self.tabRect(index).left() + self._leftflush() right_protruding_width = self.tabRect(index).right() - self._rightflush() if left_protruding_width > 0 and not right_protruding_width > 0: self.scroll_offset -= left_protruding_width elif right_protruding_width > 0 and not left_protruding_width > 0: self.scroll_offset += right_protruding_width elif left_protruding_width > 0 and right_protruding_width > 0: # Both edges are out of bounds. Which way do we move? if prefer_left: self.scroll_offset -= left_protruding_width else: self.scroll_offset += right_protruding_width self.update_scroll_button_state() self.update()
[docs] @debug.trace def sizeHint(self): hint = _BaseDragDropTabBar.sizeHint(self) hint.setWidth(self.parent().width()) return hint
[docs] @debug.trace def minimumSizeHint(self): hint = _BaseDragDropTabBar.minimumSizeHint(self) if self.uses_scrollbuttons: # No minimum if we are scrollable hint.setWidth(0) return hint
[docs] @debug.trace def update(self): _BaseDragDropTabBar.update(self) self.animation.ensure_running()
# Setters and getters for the class variables: @property def drag_in_progress(self): return self._drag_in_progress @drag_in_progress.setter def drag_in_progress(self, value): self.__class__._drag_in_progress = value @property def dragged_tab_index(self): return self._dragged_tab_index @dragged_tab_index.setter def dragged_tab_index(self, value): self.__class__._dragged_tab_index = value @property def dragged_tab_parent(self): return self._dragged_tab_parent @dragged_tab_parent.setter def dragged_tab_parent(self, value): self.__class__._dragged_tab_parent = value
[docs] @debug.trace def set_tab_parent(self, dest, index=None, pos=None): """Move the tab to the given parent DragDropTabBar if it's not already there. index=None will determined the insertion index from the given mouse position.""" if index is None: assert pos is not None index = dest.insertion_index_at(self.mapToGlobal(pos - self.dragged_tab_grab_point)) if self.dragged_tab_parent != dest: tab = self.dragged_tab_parent.remove_dragged_tab(self.dragged_tab_index) dest.add_dragged_tab(index, tab) if dest is self.limbo: self.limbo.previous_parent = self.dragged_tab_parent self.limbo.previous_index = self.dragged_tab_index self.dragged_tab_parent = dest self.dragged_tab_index = index # Tell parent to redraw to reflect the new position: dest.update()
[docs] @debug.trace def insertion_index_at(self, pos): """Compute at which index the tab with given upper left corner position in global coordinates should be inserted into the tabBar.""" left = self.mapFromGlobal(pos).x() for other in range(self.count()): other_midpoint = self.tabRect(other).center().x() if other_midpoint > left: return other return self.count()
[docs] @debug.trace def update_dragged_tab_animation_pos(self, pos): # update the animation position of the dragged tab so that it can be # correctly animated once released. assert self.dragged_tab_parent is self pinned_rect = self.tabRect(self.dragged_tab_index) pinned_rect.translate(pos - self.dragged_tab_grab_point - pinned_rect.topLeft()) left = pinned_rect.left() self.animation.positions[self.dragged_tab_index] = left
[docs] @debug.trace def update_tab_index(self, index, pos): """Check if the tab at the given index, being dragged by the mouse at the given position, needs to be moved. Move it and return the new index.""" # If the tab rect were pinned to the mouse at the point it was # grabbed, where would it be? pinned_rect = self.tabRect(index) pinned_rect.translate(pos-self.dragged_tab_grab_point - pinned_rect.topLeft()) left = pinned_rect.left() self.animation.positions[index] = left right = pinned_rect.right() move_target = None for other in range(self.count()): other_midpoint = self.tabRect(other).center().x() if other < index and left < other_midpoint: move_target = other # break to move as far left as warranted: break elif other > index and right > other_midpoint: move_target = other # Don't break because we might move further right if move_target is not None: self.moveTab(index, move_target) # Workaround for bug in PyQt4 - the tabWdiget does not update its # child StackedWidget's current widget when it gets a tabMoved # signal durint a mouseMove event: if self.currentIndex() == move_target: stack = self.parent().findChild(QStackedWidget, 'qt_tabwidget_stackedwidget') stack.setCurrentWidget(self.parent().widget(move_target)) return move_target return index
[docs] @debug.trace def widgetAt(self, pos): """If the given position is over a DragDropTabBar belonging to the current group, return the DragDropTabBar. If it is over a TabWidget in the same group that has no tabs, or the dragged tab as its only tab, return its DragDropTabBar. Otherwise return the limbo object.""" for tab_widget in self.tab_widgets[self.group_id]: count = tab_widget.tabBar().count() if tab_widget.accept_drops_bar_only: if count == 0: widget = tab_widget rect = widget.rect() # The region at the top of the TabWidget equal to the height # of a tab: height = self.dragged_tab_parent.tabRect(self.dragged_tab_index).height() rect.setHeight(height) else: widget = tab_widget.tabBar() rect = widget.rect() # Include the whole horizontal part of the tabBar: rect.setLeft(widget.parent().rect().left()) rect.setRight(widget.parent().rect().right()) # If we're leaving, add a buffer region so that we don't leave # until we have passed a certain distance: if self.drag_in_progress and self.dragged_tab_parent is widget: rect.setLeft(rect.left() - 10) rect.setRight(rect.right() + 10) # No buffer in the vertical directions, but make the tab bars # a slightly bigger target for both coming and going: rect.setTop(rect.top() - 10) rect.setBottom(rect.bottom() + 10) else: widget = tab_widget rect = widget.rect() # If we're leaving, add a buffer region so that we don't leave # until we have passed a certain distance: if self.drag_in_progress and self.dragged_tab_parent is widget: rect.setLeft(rect.left() - 10) rect.setRight(rect.right() + 10) rect.setTop(rect.top() - 10) rect.setBottom(rect.bottom() + 10) other_local_pos = widget.mapFromGlobal(self.mapToGlobal(pos)) if rect.contains(other_local_pos): return tab_widget.tabBar() return self.limbo
[docs] @debug.trace def tabRect(self, index): rect = _BaseDragDropTabBar.tabRect(self, index) rect.translate(-self.scroll_offset, 0) return rect
[docs] @debug.trace def tabAt(self, pos): for i in range(self.count()): if self.tabRect(i).contains(pos): return i return -1
[docs] @debug.trace def mousePressEvent(self, event): """Take note of the tab that was clicked so it can be dragged on mouseMoveEvents""" if event.button() != Qt.LeftButton: return event.accept() index = self.tabAt(event.pos()) if index == -1: return self.setCurrentIndex(index) self.ensure_visible(index) self.drag_in_progress = True # Ensure we get all mouse events until the mouse is released: self.grabMouse() self.dragged_tab_index = index self.dragged_tab_parent = self self.dragged_tab_grab_point = (event.pos() - self.tabRect(self.dragged_tab_index).topLeft())
[docs] @debug.trace def mouseMoveEvent(self, event): """Update the parent of the tab to be the DragDropTabWidget under the mouse, if any, otherwise update it to the limbo object. Update the position of the tab in the widget it's in.""" _BaseDragDropTabBar.mouseMoveEvent(self, event) if not self.drag_in_progress: self.update() return event.accept() if self.group_id is not None: widget = self.widgetAt(event.pos()) self.set_tab_parent(widget, pos=event.pos()) other_local_pos = widget.mapFromGlobal(self.mapToGlobal(event.pos())) self.dragged_tab_index = widget.update_tab_index(self.dragged_tab_index, other_local_pos) if self.dragged_tab_parent is not self.limbo: self.dragged_tab_parent.update_dragged_tab_animation_pos(other_local_pos) widget.update()
[docs] @debug.trace def mouseReleaseEvent(self, event): """Same as mouseMove event - update the DragDropTabWidget and position of the tab to the current mouse position. Unless the mouse position is outside of any widgets at the time of mouse release, in which case move the tab to its last known parent and position.""" _BaseDragDropTabBar.mouseReleaseEvent(self, event) if not self.drag_in_progress or event.button() != Qt.LeftButton: return event.accept() # Cancel the drag: self.drag_in_progress = False widget = self.widgetAt(event.pos()) # Tell Qt to no longer send all mouse events to this widget: self.releaseMouse() # If the tab and the mouse are both in limbo, then put the tab # back at its last known place: if widget is self.limbo and self.dragged_tab_parent is self.limbo: index = self.limbo.previous_index self.set_tab_parent(self.limbo.previous_parent, index) self.limbo.previous_parent.animation.animate_limbo(self.limbo, index) # But if we're above a tab bar that it's not already in, put it there. # Otherwise leave it where it is (don't move it into limbo) elif widget is not self.limbo and widget is not self.dragged_tab_parent: self.set_tab_parent(widget, pos=event.pos()) else: # It's already in this widget. Store the position of the tab so it # can animate: other_local_pos = self.dragged_tab_parent.mapFromGlobal( self.mapToGlobal(event.pos())) self.dragged_tab_parent.update_dragged_tab_animation_pos(other_local_pos) # Tell the parent to redraw the tabs: self.dragged_tab_parent.update() # Clear the variables about which tab is being dragged: self.dragged_tab_index = None self.dragged_tab_parent = None self.dragged_tab_grab_point = None
[docs] @debug.trace def is_dragged_tab(self, index): """Return whether the tab at the given index is currently being dragged""" return (self.drag_in_progress and self.dragged_tab_parent is self and self.dragged_tab_index == index)
[docs] @debug.trace def tabInserted(self, index): _BaseDragDropTabBar.tabInserted(self, index) self.animation.tabInserted(index)
[docs] @debug.trace def tabRemoved(self, index): _BaseDragDropTabBar.tabRemoved(self, index) self.animation.tabRemoved(index)
[docs] @debug.trace def tabLayoutChange(self): _BaseDragDropTabBar.tabLayoutChange(self) self.animation.ensure_running() self.update_scroll_button_state() self.ensure_visible(self.currentIndex())
[docs] @debug.trace def update_scroll_button_state(self): if not self.uses_scrollbuttons: self.left_scrollbutton.hide() self.right_scrollbutton.hide() return all_tabs_right_edge = self.tabRect(self.count() - 1).right() all_tabs_left_edge = self.tabRect(0).left() total_width_of_all_tabs = all_tabs_right_edge - all_tabs_left_edge min_scroll_offset = 0 max_scroll_offset = max(0, total_width_of_all_tabs - self.parent().width() + 2 * self.SCROLL_BUTTON_WIDTH + 2 * self.SCROLL_BUTTON_GAP) if self.scroll_offset <= min_scroll_offset: self.scroll_offset = min_scroll_offset self.left_scrollbutton.setEnabled(False) else: self.left_scrollbutton.setEnabled(True) if self.scroll_offset >= max_scroll_offset: self.scroll_offset = max_scroll_offset self.right_scrollbutton.setEnabled(False) else: self.right_scrollbutton.setEnabled(True) self.left_scrollbutton.resize(self.SCROLL_BUTTON_WIDTH, self.height() - 6) self.right_scrollbutton.resize(self.SCROLL_BUTTON_WIDTH, self.height() - 6) self.left_scrollbutton.move(self.parent().width() - 2*self.SCROLL_BUTTON_WIDTH, 3) self.right_scrollbutton.move(self.parent().width() - self.SCROLL_BUTTON_WIDTH, 3) if self.left_scrollbutton.isEnabled() or self.right_scrollbutton.isEnabled(): self.left_scrollbutton.show() self.right_scrollbutton.show() else: self.left_scrollbutton.hide() self.right_scrollbutton.hide()
[docs] @debug.trace def on_scroll_button_clicked(self, button): if button is self.left_scrollbutton: for i in range(self.count() - 1, -1, -1): if self.tabRect(i).left() < self._leftflush(): self.ensure_visible(i, prefer_left=True) break elif button is self.right_scrollbutton: for i in range(self.count()): if self.tabRect(i).right() > self._rightflush(): self.ensure_visible(i, prefer_left=False) break self.update_scroll_button_state() self.update()
[docs] @debug.trace def paint_tab(self, index, painter, option): # Don't paint the tab if it's the floating tab's target whilst it is # animated flying in: if index == self.animation.limbo_target_tab: return painter.save() if self.is_dragged_tab(index): # The dragged tab is pinned to the mouse: xpos = self.mapFromGlobal(QCursor.pos()).x() - self.dragged_tab_grab_point.x() else: # Other tabs are at their current animated position: xpos = self.animation.positions[index] tabrect = self.tabRect(index) painter.translate(xpos - tabrect.left() - self.scroll_offset, 0) self.initStyleOption(option, index) painter.drawControl(QStyle.CE_TabBarTab, option) painter.restore()
[docs] @debug.trace def paintEvent(self, event): painter = QStylePainter(self) option = QStyleOptionTab() # Draw in reverse order so if there is overlap, tabs to the left are # on top: for index in range(self.count() - 1, -1, -1): if self.currentIndex() == index: # Draw the active tab last so it's on top: continue self.paint_tab(index, painter, option) if self.currentIndex() != -1: self.paint_tab(self.currentIndex(), painter, option) # Erase the region where the buttons need to be: if self.left_scrollbutton.isEnabled() or self.right_scrollbutton.isEnabled(): rect = self.rect() rect.moveLeft(self.width() - 2 * self.SCROLL_BUTTON_WIDTH - self.SCROLL_BUTTON_GAP) rect.moveBottom(rect.bottom() - 2) painter.eraseRect(rect) painter.end()
[docs]class DragDropTabWidget(QTabWidget): """A tab widget that supports dragging and dropping of tabs between tab widgets that share a group_id. a group_id of None indicates that tab dragging is disabled."""
[docs] def __init__(self, group_id=None, accept_drops_bar_only=False): QTabWidget.__init__(self) self.setTabBar(DragDropTabBar(self, group_id)) self.tabBar().setExpanding(False) self.tab_bar = self.tabBar() # Backward compatibility for BLACS # Whether to accept drops only on the tab bar at the top, # as opposed to accepting them anywhere on the tabWidget: self.accept_drops_bar_only = accept_drops_bar_only # Use scroll buttons by default: self.setUsesScrollButtons(True)
[docs] def setUsesScrollButtons(self, uses_scrollbuttons): self.tabBar().setUsesScrollButtons(uses_scrollbuttons)
[docs] def setElideMode(self, mode): # Ignored. Elision is set entirely depending on whether scroll buttons # are used (ElideRight if they are not, no elision if they are) pass
if __name__ == '__main__': class ViewPort(object): def __init__(self, id, container_layout,i): #ui = UiLoader().load('viewport.ui') self.tab_widget = DragDropTabWidget(id) container_layout.addWidget(self.tab_widget) self.tab_widget.addTab(QLabel("foo %d"%i), 'foo %d 1' % i) self.tab_widget.addTab(QLabel("bar %d"%i), 'bar bar bar %d 1' % i) self.tab_widget.tabBar().setTabTextColor(0, QColor(255, 0, 0)) self.tab_widget.tabBar().setTabTextColor(1, QColor(0, 255, 0)) self.tab_widget.tabBar().setTabIcon(1, lyse_icon) self.tab_widget.tabBar().setTabIcon(0, runmanager_icon) class RunViewer(object): def __init__(self): # Load the gui: self.moving_tab = None self.moving_tab_index = -1 self.window = QWidget() container = QVBoxLayout(self.window) self.viewports = [] for i in range(3): viewport = ViewPort(3,container,i) self.viewports.append(viewport) #button = QPushButton("launch iPython") #button.clicked.connect(embed) #ui.verticalLayout_6.addWidget(button) self.window.show() qapplication = QApplication([]) import qtutils.icons lyse_icon = QIcon(':/qtutils/custom/lyse') runmanager_icon = QIcon(':/qtutils/custom/runmanager') app = RunViewer() timer = QTimer() timer.start(500) import time def change_text(): if DragDropTabBar.limbo is not None: limbo = DragDropTabBar.limbo if limbo.count(): tab_text = limbo.tabText(0) split = str(tab_text).split() text = ' '.join(split[:-1]) number = int(split[-1]) number = number + 1 limbo.setTabText(0, text + ' ' + str(number)) if number % 2: limbo.setTabIcon(0, lyse_icon) else: limbo.setTabIcon(0, runmanager_icon) timer.timeout.connect(change_text) qapplication.exec_()