--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric7/EricWidgets/EricTabWidget.py Sat May 22 19:58:24 2021 +0200 @@ -0,0 +1,358 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2005 - 2021 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a TabWidget class substituting QTabWidget. +""" + +import contextlib + +from PyQt6.QtCore import pyqtSignal, Qt, QPoint, QMimeData +from PyQt6.QtGui import QDrag +from PyQt6.QtWidgets import QTabWidget, QTabBar, QApplication, QStyle + +from EricWidgets.EricAnimatedLabel import EricAnimatedLabel + + +class EricWheelTabBar(QTabBar): + """ + Class implementing a tab bar class substituting QTabBar to support wheel + events. + """ + def __init__(self, parent=None): + """ + Constructor + + @param parent reference to the parent widget (QWidget) + """ + super().__init__(parent) + self._tabWidget = parent + + def wheelEvent(self, event): + """ + Protected slot to support wheel events. + + @param event reference to the wheel event (QWheelEvent) + """ + with contextlib.suppress(AttributeError): + delta = event.angleDelta().y() + if delta > 0: + self._tabWidget.prevTab() + elif delta < 0: + self._tabWidget.nextTab() + + event.accept() + + +class EricDnDTabBar(EricWheelTabBar): + """ + Class implementing a tab bar class substituting QTabBar. + + @signal tabMoveRequested(int, int) emitted to signal a tab move request + giving the old and new index position + """ + tabMoveRequested = pyqtSignal(int, int) + + def __init__(self, parent=None): + """ + Constructor + + @param parent reference to the parent widget (QWidget) + """ + EricWheelTabBar.__init__(self, parent) + self.setAcceptDrops(True) + + self.__dragStartPos = QPoint() + + def mousePressEvent(self, event): + """ + Protected method to handle mouse press events. + + @param event reference to the mouse press event (QMouseEvent) + """ + if event.button() == Qt.MouseButton.LeftButton: + self.__dragStartPos = QPoint(event.position().toPoint()) + EricWheelTabBar.mousePressEvent(self, event) + + def mouseMoveEvent(self, event): + """ + Protected method to handle mouse move events. + + @param event reference to the mouse move event (QMouseEvent) + """ + if ( + event.buttons() == Qt.MouseButton.LeftButton and + (event.position().toPoint() - self.__dragStartPos) + .manhattanLength() > QApplication.startDragDistance() + ): + drag = QDrag(self) + mimeData = QMimeData() + index = self.tabAt(event.position().toPoint()) + mimeData.setText(self.tabText(index)) + mimeData.setData("action", b"tab-reordering") + mimeData.setData("tabbar-id", str(id(self)).encode("utf-8")) + drag.setMimeData(mimeData) + drag.exec() + EricWheelTabBar.mouseMoveEvent(self, event) + + def dragEnterEvent(self, event): + """ + Protected method to handle drag enter events. + + @param event reference to the drag enter event (QDragEnterEvent) + """ + mimeData = event.mimeData() + formats = mimeData.formats() + if ( + "action" in formats and + mimeData.data("action") == b"tab-reordering" and + "tabbar-id" in formats and + int(mimeData.data("tabbar-id")) == id(self) + ): + event.acceptProposedAction() + EricWheelTabBar.dragEnterEvent(self, event) + + def dropEvent(self, event): + """ + Protected method to handle drop events. + + @param event reference to the drop event (QDropEvent) + """ + fromIndex = self.tabAt(self.__dragStartPos) + toIndex = self.tabAt(event.position().toPoint()) + if fromIndex != toIndex: + self.tabMoveRequested.emit(fromIndex, toIndex) + event.acceptProposedAction() + EricWheelTabBar.dropEvent(self, event) + + +class EricTabWidget(QTabWidget): + """ + Class implementing a tab widget class substituting QTabWidget. + + It provides slots to show the previous and next tab and give + them the input focus and it allows to have a context menu for the tabs. + + @signal customTabContextMenuRequested(const QPoint & point, int index) + emitted when a context menu for a tab is requested + """ + customTabContextMenuRequested = pyqtSignal(QPoint, int) + + def __init__(self, parent=None, dnd=False): + """ + Constructor + + @param parent reference to the parent widget (QWidget) + @param dnd flag indicating the support for Drag & Drop (boolean) + """ + super().__init__(parent) + + if dnd: + if not hasattr(self, 'setMovable'): + self.__tabBar = EricDnDTabBar(self) + self.__tabBar.tabMoveRequested.connect(self.moveTab) + self.setTabBar(self.__tabBar) + else: + self.__tabBar = EricWheelTabBar(self) + self.setTabBar(self.__tabBar) + self.setMovable(True) + else: + self.__tabBar = EricWheelTabBar(self) + self.setTabBar(self.__tabBar) + + self.__lastCurrentIndex = -1 + self.__currentIndex = -1 + self.currentChanged.connect(self.__currentChanged) + + def setCustomTabBar(self, dnd, tabBar): + """ + Public method to set a custom tab bar. + + @param dnd flag indicating the support for Drag & Drop (boolean) + @param tabBar reference to the tab bar to set (QTabBar) + """ + self.__tabBar = tabBar + self.setTabBar(self.__tabBar) + if dnd: + if isinstance(tabBar, EricDnDTabBar): + self.__tabBar.tabMoveRequested.connect(self.moveTab) + else: + self.setMovable(True) + + def __currentChanged(self, index): + """ + Private slot to handle the currentChanged signal. + + @param index index of the current tab + """ + if index == -1: + self.__lastCurrentIndex = -1 + else: + self.__lastCurrentIndex = self.__currentIndex + self.__currentIndex = index + + def switchTab(self): + """ + Public slot used to switch between the current and the previous + current tab. + """ + if self.__lastCurrentIndex == -1 or self.__currentIndex == -1: + return + + self.setCurrentIndex(self.__lastCurrentIndex) + self.currentWidget().setFocus() + + def nextTab(self): + """ + Public slot used to show the next tab. + """ + ind = self.currentIndex() + 1 + if ind == self.count(): + ind = 0 + + self.setCurrentIndex(ind) + self.currentWidget().setFocus() + + def prevTab(self): + """ + Public slot used to show the previous tab. + """ + ind = self.currentIndex() - 1 + if ind == -1: + ind = self.count() - 1 + + self.setCurrentIndex(ind) + self.currentWidget().setFocus() + + def setTabContextMenuPolicy(self, policy): + """ + Public method to set the context menu policy of the tab. + + @param policy context menu policy to set (Qt.ContextMenuPolicy) + """ + self.tabBar().setContextMenuPolicy(policy) + if policy == Qt.ContextMenuPolicy.CustomContextMenu: + self.tabBar().customContextMenuRequested.connect( + self.__handleTabCustomContextMenuRequested) + else: + self.tabBar().customContextMenuRequested.disconnect( + self.__handleTabCustomContextMenuRequested) + + def __handleTabCustomContextMenuRequested(self, point): + """ + Private slot to handle the context menu request for the tabbar. + + @param point point the context menu was requested (QPoint) + """ + _tabbar = self.tabBar() + for index in range(_tabbar.count()): + rect = _tabbar.tabRect(index) + if rect.contains(point): + self.customTabContextMenuRequested.emit( + _tabbar.mapToParent(point), index) + return + + self.customTabContextMenuRequested.emit(_tabbar.mapToParent(point), -1) + + def selectTab(self, pos): + """ + Public method to get the index of a tab given a position. + + @param pos position determining the tab index (QPoint) + @return index of the tab (integer) + """ + _tabbar = self.tabBar() + for index in range(_tabbar.count()): + rect = _tabbar.tabRect(index) + if rect.contains(pos): + return index + + return -1 + + def moveTab(self, curIndex, newIndex): + """ + Public method to move a tab to a new index. + + @param curIndex index of tab to be moved (integer) + @param newIndex index the tab should be moved to (integer) + """ + # step 1: save the tab data of tab to be moved + toolTip = self.tabToolTip(curIndex) + text = self.tabText(curIndex) + icon = self.tabIcon(curIndex) + whatsThis = self.tabWhatsThis(curIndex) + widget = self.widget(curIndex) + curWidget = self.currentWidget() + + # step 2: move the tab + self.removeTab(curIndex) + self.insertTab(newIndex, widget, icon, text) + + # step 3: set the tab data again + self.setTabToolTip(newIndex, toolTip) + self.setTabWhatsThis(newIndex, whatsThis) + + # step 4: set current widget + self.setCurrentWidget(curWidget) + + def __freeSide(self): + """ + Private method to determine the free side of a tab. + + @return free side (QTabBar.ButtonPosition) + """ + side = self.__tabBar.style().styleHint( + QStyle.StyleHint.SH_TabBar_CloseButtonPosition, + None, None, None) + side = ( + QTabBar.ButtonPosition.RightSide + if side == QTabBar.ButtonPosition.LeftSide else + QTabBar.ButtonPosition.LeftSide + ) + return side + + def animationLabel(self, index, animationFile, interval=100): + """ + Public slot to set an animated icon. + + @param index tab index + @type int + @param animationFile name of the file containing the animation + @type str + @param interval interval in milliseconds between animation frames + @type int + @return reference to the created label + @rtype EricAnimatedLabel + """ + if index == -1: + return None + + if hasattr(self.__tabBar, 'setTabButton'): + side = self.__freeSide() + animation = EricAnimatedLabel( + self, animationFile=animationFile, interval=interval) + self.__tabBar.setTabButton(index, side, None) + self.__tabBar.setTabButton(index, side, animation) + animation.start() + return animation + else: + return None + + def resetAnimation(self, index): + """ + Public slot to reset an animated icon. + + @param index tab index (integer) + """ + if index == -1: + return + + if hasattr(self.__tabBar, 'tabButton'): + side = self.__freeSide() + animation = self.__tabBar.tabButton(index, side) + if animation is not None: + animation.stop() + self.__tabBar.setTabButton(index, side, None) + del animation