diff --git a/demo/demo.py b/demo/main.py similarity index 97% rename from demo/demo.py rename to demo/main.py index 1254eef..c0de017 100644 --- a/demo/demo.py +++ b/demo/main.py @@ -215,6 +215,17 @@ class MainWindow(MainWindowUI, MainWindowBase): dock_widget = self.create_calendar_dock_widget() dock_widget.setTabToolTip("Tab ToolTip\nHodie est dies magna") dock_area = self.dock_manager.addDockWidget(QtAds.CenterDockWidgetArea, dock_widget, top_dock_area) + # Now we create a action to test resizing of DockArea widget + action = self.menuTests.addAction("Resize {}".format(dock_widget.windowTitle())) + def action_triggered(): + splitter = QtAds.internal.findParent(QtAds.CDockSplitter, dock_area) + if not splitter: + return + # We change the sizes of the splitter that contains the Calendar 1 widget + # to resize the dock widget + width = splitter.width() + splitter.setSizes([width * 2/3, width * 1/3]) + action.triggered.connect(action_triggered) # Now we add a custom button to the dock area title bar that will create # new editor widgets when clicked diff --git a/examples/centralwidget/centralWidget.py b/examples/centralwidget/main.py similarity index 76% rename from examples/centralwidget/centralWidget.py rename to examples/centralwidget/main.py index 581b00d..5fe6a49 100644 --- a/examples/centralwidget/centralWidget.py +++ b/examples/centralwidget/main.py @@ -12,19 +12,7 @@ from PyQtAds import QtAds UI_FILE = os.path.join(os.path.dirname(__file__), 'mainwindow.ui') MainWindowUI, MainWindowBase = uic.loadUiType(UI_FILE) - -import demo_rc # pyrcc5 demo\demo.qrc -o examples\centralWidget\demo_rc.py - - -def svg_icon(filename: str): - '''Helper function to create an SVG icon''' - # This is a workaround, because because in item views SVG icons are not - # properly scaled and look blurry or pixelate - icon = QIcon(filename) - icon.addPixmap(icon.pixmap(92)) - return icon - class MainWindow(MainWindowUI, MainWindowBase): def __init__(self, parent=None): @@ -46,27 +34,26 @@ class MainWindow(MainWindowUI, MainWindowBase): central_dock_area.setAllowedAreas(QtAds.DockWidgetArea.OuterDockAreas) # create other dock widgets - file_tree = QTreeView() - file_tree.setFrameShape(QFrame.NoFrame) - file_model = QFileSystemModel(file_tree) - file_model.setRootPath(QDir.currentPath()) - file_tree.setModel(file_model) - data_dock_widget = QtAds.CDockWidget("File system") - data_dock_widget.setWidget(file_tree) - data_dock_widget.resize(150, 250) - data_dock_widget.setMinimumSize(100, 250) - file_area = self.dock_manager.addDockWidget(QtAds.DockWidgetArea.LeftDockWidgetArea, data_dock_widget, central_dock_area) - self.menuView.addAction(data_dock_widget.toggleViewAction()) - table = QTableWidget() table.setColumnCount(3) table.setRowCount(10) - table_dock_widget = QtAds.CDockWidget("Table") + table_dock_widget = QtAds.CDockWidget("Table 1") table_dock_widget.setWidget(table) table_dock_widget.setMinimumSizeHintMode(QtAds.CDockWidget.MinimumSizeHintFromDockWidget) table_dock_widget.resize(250, 150) table_dock_widget.setMinimumSize(200, 150) - self.dock_manager.addDockWidget(QtAds.DockWidgetArea.BottomDockWidgetArea, table_dock_widget, file_area) + table_area = self.dock_manager.addDockWidget(QtAds.DockWidgetArea.LeftDockWidgetArea, table_dock_widget) + self.menuView.addAction(table_dock_widget.toggleViewAction()) + + table = QTableWidget() + table.setColumnCount(5) + table.setRowCount(1020) + table_dock_widget = QtAds.CDockWidget("Table 2") + table_dock_widget.setWidget(table) + table_dock_widget.setMinimumSizeHintMode(QtAds.CDockWidget.MinimumSizeHintFromDockWidget) + table_dock_widget.resize(250, 150) + table_dock_widget.setMinimumSize(200, 150) + table_area = self.dock_manager.addDockWidget(QtAds.DockWidgetArea.BottomDockWidgetArea, table_dock_widget, table_area) self.menuView.addAction(table_dock_widget.toggleViewAction()) properties_table = QTableWidget() @@ -76,7 +63,7 @@ class MainWindow(MainWindowUI, MainWindowBase): properties_dock_widget.setWidget(properties_table) properties_dock_widget.setMinimumSizeHintMode(QtAds.CDockWidget.MinimumSizeHintFromDockWidget) properties_dock_widget.resize(250, 150) - properties_dock_widget.setMinimumSize(200,150) + properties_dock_widget.setMinimumSize(200, 150) self.dock_manager.addDockWidget(QtAds.DockWidgetArea.RightDockWidgetArea, properties_dock_widget, central_dock_area) self.menuView.addAction(properties_dock_widget.toggleViewAction()) @@ -84,7 +71,6 @@ class MainWindow(MainWindowUI, MainWindowBase): def create_perspective_ui(self): save_perspective_action = QAction("Create Perspective", self) - save_perspective_action.setIcon(svg_icon(":/adsdemo/images/picture_in_picture.svg")) save_perspective_action.triggered.connect(self.save_perspective) perspective_list_action = QWidgetAction(self) self.perspective_combobox = QComboBox(self) diff --git a/examples/deleteonclose/deleteonclose.py b/examples/deleteonclose/main.py similarity index 100% rename from examples/deleteonclose/deleteonclose.py rename to examples/deleteonclose/main.py diff --git a/examples/dockindock/dockindock.py b/examples/dockindock/dockindock.py new file mode 100644 index 0000000..4853b57 --- /dev/null +++ b/examples/dockindock/dockindock.py @@ -0,0 +1,203 @@ +import sys + +from PyQt5.QtWidgets import (QApplication, QWidget, QVBoxLayout, QMessageBox, + QInputDialog, QMenu, QLineEdit) +from PyQt5.QtGui import QIcon +from PyQtAds import QtAds + +from dockindockmanager import DockInDockManager +from perspectiveactions import LoadPerspectiveAction, RemovePerspectiveAction + + +class DockInDockWidget(QWidget): + def __init__(self, parent, perspectives_manager: 'PerspectivesManager', can_create_new_groups: bool = False, top_level_widget = None): + super().__init__(parent) + + if top_level_widget is not None: + self.__can_create_new_groups = top_level_widget.can_create_new_groups + else: + self.__can_create_new_groups = can_create_new_groups + self.__top_level_dock_widget = top_level_widget if top_level_widget else self + self.__perspectives_manager = perspectives_manager + self.__new_perspective_default_name: str = '' + + layout = QVBoxLayout(self) + layout.setContentsMargins(0,0,0,0) + self.__mgr = DockInDockManager(self) + layout.addWidget(self.__mgr) + + def getManager(self) -> 'DockInDockManager': + return self.__mgr + + def getTopLevelDockWidget(self) -> 'DockInDockWidget': + return self.__top_level_dock_widget + + def canCreateNewGroups(self) -> bool: + return self.__can_create_new_groups + + def getPerspectivesManager(self) -> 'PerspectivesManager': + return self.__perspectives_manager + + def addTabWidget(self, widget: QWidget, name: str, after: QtAds.CDockAreaWidget, icon = QIcon()) -> QtAds.CDockAreaWidget: + for existing in self.getTopLevelDockWidget().getManager().allDockWidgets(True, True): + if existing[1].objectName() == name: + QMessageBox.critical(self, "Error", "Name '" + name + "' already in use") + return + + dock_widget = QtAds.CDockWidget(name) + dock_widget.setWidget(widget) + dock_widget.setIcon(icon) + + # Add the dock widget to the top dock widget area + return self.__mgr.addDockWidget(QtAds.CenterDockWidgetArea, dock_widget, after) + + def isTopLevel(self) -> bool: + return not self.objectName() + + def getGroupNameError(self, group_name: str) -> str: + if not group_name: + return "Group must have a non-empty name" + + dock_managers = self.__mgr.allManagers(True, True) + for mgr in dock_managers: + if mgr.getGroupName() == group_name: + return "Group name '" + group_name + "' already used" + + return "" + + def createGroup(self, group_name: str, insert_pos: QtAds.CDockAreaWidget, icon = QIcon()) -> 'DockInDockWidget': + error = self.getGroupNameError(group_name) + if error: + QMessageBox.critical(None, "Error", error) + return + + child = DockInDockWidget(self, self.__top_level_dock_widget, self.__perspectives_manager) + child.setObjectName(group_name) + + dock_widget = QtAds.CDockWidget(group_name) + dock_widget.setWidget(child) + dock_widget.setIcon(icon) + + insert_pos = self.__mgr.addDockWidget(QtAds.CenterDockWidgetArea, dock_widget, insert_pos) + + return child, insert_pos + + def destroyGroup(self, widget_to_remove: 'DockInDockWidget') -> None: + top_level_widget = widget_to_remove.getTopLevelDockWidget() + + if top_level_widget and top_level_widget != widget_to_remove: + for dock_widget in widget_to_remove.getManager().getWidgetsInGUIOrder(): #don't use allDockWidgets to preserve sub-groups + MoveDockWidgetAction.move(dock_widget, top_level_widget.getManager()) + assert not widget_to_remove.getManager().allDockWidgets(True, True) + + # find widget's parent: + for dock_widget in top_level_widget.getManager().allDockWidgets(True, True): + if dockwidget[1].widget() == widget_to_remove: + dockwidget[0].removeDockWidget(dockwidget[1]) + del dockwidget[1] + # delete widgetToRemove; automatically deleted when dockWidget is deleted + widget_to_remove = None + break + + assert widget_to_remove == None + else: + assert False + + def attachViewMenu(self, menu: QMenu) -> None: + menu.aboutToShow.connect(self.autoFillAttachedViewMenu) + + def autoFillAttachedViewMenu(self) -> None: + menu = self.sender() + + if menu: + menu.clear() + self.setupViewMenu(menu) + else: + assert False + + def setupViewMenu(self, menu): + dock_managers = self.__mgr.allManagers(True, True) + + has_perspectives_menu = False + if self.getTopLevelDockWidget() == self: + has_perspectives_menu = (self.__perspectives_manager != None) + else: + assert False + + organize = menu + if has_perspectives_menu: + organize = menu.addMenu("Organize") + + self.setupMenu(organize, dock_managers) + + if has_perspectives_menu: + perspectives = menu.addMenu("Perspectives") + self.fillPerspectivesMenu(perspectives) + + def setupMenu(self, menu: QMenu, move_to: 'list[DockInDockManager]') -> None: + self.__mgr.fillViewMenu(menu, move_to) + menu.addSeparator() + move_menu = menu.addMenu("Move") + self.__mgr.fillMoveMenu(move_menu, move_to) + + def fillPerspectivesMenu(self, menu: QMenu): + menu.addAction("Create perspective...", self.createPerspective) + perspectives_names = [] + if self.__perspectives_manager: + perspectives_names = self.__perspectives_manager.perspectiveNames() + + if perspectives_names: + load = menu.addMenu("Load perspective") + for name in perspectives_names: + load.addAction(LoadPerspectiveAction(load, name, self)) + remove = menu.addMenu("Remove perspective") + for name in perspectives_names: + remove.addAction(RemovePerspectiveAction(remove, name, self)) + + def setNewPerspectiveDefaultName(default_name: str) -> None: + self.__new_perspective_default_name = default_name + + def createPerspective(self) -> None: + if not self.__perspectives_manager: + return + + name = self.__new_perspective_default_name + if self.__new_perspective_default_name: + index = 2 + while name in self.__perspectives_manager.perspectiveNames(): + name = f"{self.__new_perspective_default_name}({index})" + index += 1 + + while True: + name, ok = QInputDialog.getText(None, "Create perspective", "Enter perspective name", QLineEdit.Normal, name) + if ok: + if not name: + QMessageBox.critical(None, "Error", "Perspective name cannot be empty") + continue + elif name in self.__perspectives_manager.perspectiveNames(): + if QMessageBox.critical(None, "Error", f"Perspective '{name}' already exists, overwrite it?", QMessageBox.Yes | QMessageBox.No, QMessageBox.No) == QMessageBox.No: + continue + + self.__perspectives_manager.addPerspective(name, self) + break + else: + break + + def dumpStatus(self, echo: callable = print, widget: QtAds.CDockWidget = None, tab: str = '', suffix: str = '') -> str: + if widget is not None: + as_mgr = DockInDockManager.dockInAManager(widget) + if as_mgr: + as_mgr.parent().dumpStatus(tab=tab) + else: + echo(tab + widget.objectName() + suffix) + else: + echo(tab + "Group: " + self.getManager().getGroupName()) + tab += " " + visible_widgets = set() + for widget in self.getManager().getWidgetsInGUIOrder(): + visible_widgets.add(widget) + self.dumpStatus(widget=widget, tab=tab) + + for closed in self.getManager().dockWidgetsMap().values(): + if not closed in visible_widgets: + self.dumpStatus(widget=closed, tab=tab, suffix=" (closed)") \ No newline at end of file diff --git a/examples/dockindock/dockindockmanager.py b/examples/dockindock/dockindockmanager.py new file mode 100644 index 0000000..c6f47cb --- /dev/null +++ b/examples/dockindock/dockindockmanager.py @@ -0,0 +1,214 @@ +from PyQt5.QtWidgets import QAction, QMenu, QInputDialog, QLineEdit +from PyQt5.QtCore import QSettings + +from PyQtAds import QtAds + +CHILD_PREFIX = "Child-" + +class DockInDockManager(QtAds.CDockManager): + def __init__(self, parent: 'DockInDockWidget'): + super().__init__() + self.__parent = parent + + def parent(self) -> 'DockInDockWidget': + return self.__parent + + def fillViewMenu(self, menu: QMenu, move_to: 'dict[DockInDockManager]') -> None: + from dockindock import DockInDockWidget # Prevent cyclic import + + widgets_map = self.dockWidgetsMap() + for key, value in widgets_map.items(): + widget = value.widget() + action = value.toggleViewAction() + + if isinstance(widget, DockInDockWidget): + sub_menu = menu.addMenu(key) + + sub_menu.addAction(action) + sub_menu.addSeparator() + + widget.setupMenu(sub_menu, move_to) + else: + menu.addAction(action) + + if self.parent().canCreateNewGroups(): + # see how this works, to create it in the right place, + # and also to have load perspective work when some groups are missing + menu.addSeparator() + menu.addAction(CreateChildDockAction(self.__parent, menu)) + + if self.parent().getTopLevelDockWidget().getManager() != self: + menu.addAction(DestroyGroupAction( self.parent, menu)) + + def fillMoveMenu(self, menu: QMenu, move_to: 'list[DockInDockManager]') -> None: + widgets_map = self.dockWidgetsMap() + for key, value in widgets_map.items(): + sub_menu = menu.addMenu(key) + + for mgr in move_to: + # iterate over all possible target managers + if mgr == self: + pass # if dock is already in mgr, no reason to move it there + elif mgr == DockInDockManager.dockInAManager(value): + pass # if target is the group itself, can't move it there, would make no sense + else: + sub_menu.addAction(MoveDockWidgetAction(value, mgr, sub_menu)) + + def addPerspectiveRec(self, name: str) -> None: + managers = self.allManagers(True, True) + + for child in managers: + child.addPerspective(name) + + def openPerspectiveRec(self, name: str) -> None: + managers = self.allManagers(True, True) + + for child in managers: + child.openPerspective(name) + + def getGroupName(self) -> str: + return self.parent().objectName() + + def getPersistGroupName(self) -> str: + group = "Top" + if self.getGroupName(): + group = CHILD_PREFIX + self.getGroupName() + return group + + def getGroupNameFromPersistGroupName(self, persist_group_name) -> str: + if persist_group_name.startswith(CHILD_PREFIX): + persist_group_name = persist_group_name[len(CHILD_PREFIX):] + else: + assert False + return persist_group_name + + def loadPerspectivesRec(self, settings: QSettings) -> None: + children = self.allManagers(True, True) + + for mgr in children: + settings.beginGroup(mgr.getPersistGroupName()) + mgr.loadPerspectives(settings) + settings.endGroup() + + def savePerspectivesRec(self, settings: QSettings) -> None: + children = self.allManagers(True, True) + + for mgr in children: + settings.beginGroup(mgr.getPersistGroupName()) + mgr.savePerspectives(settings) + settings.endGroup() + + def removePerspectivesRec(self, settings: QSettings) -> None: + children = self.allManagers(True, True) + + for mgr in children: + child.removePerspectives(child.perspectiveNames()) + + @staticmethod + def dockInAManager(widget) -> 'DockInDockManager': + from dockindock import DockInDockWidget # Prevent cyclic import + + dock_widget = widget.widget() if widget else None + return dock_widget.getManager() if isinstance(dock_widget, DockInDockWidget) else None + + def childManagers(self, managers: 'list[DockInDockManager]', rec: bool) -> None: + widgets = self.getWidgetsInGUIOrder() + for widget in widgets: + as_mgr = DockInDockManager.dockInAManager(widget) + if as_mgr: + managers.append(as_mgr) + if rec: + as_mgr.childManagers(managers, rec) + + def allManagers(self, include_self: bool, rec: bool) -> 'list[DockInDockManager]': + managers = [] + if include_self: + managers.append(self) + self.childManagers(managers, rec) + return managers + + def allDockWidgets(self, include_self: bool, rec: bool) -> 'list[tuple[DockInDockManager, QtAds.CDockWidget]]': + widgets = [] + for mgr in self.allManagers(include_self, rec): + for widget in mgr.getWidgetsInGUIOrder(): + widgets.append((mgr, widget)) + return widgets + + def getGroupContents(self) -> 'dict[str, list[str]]': + result = {} + managers = self.allManagers(True, True) + for mgr in managers: + result[mgr.getPersistGroupName()] = mgr.dockWidgetsMap().keys() + return result + + def getInsertDefaultPos(self) -> QtAds.CDockAreaWidget: + default_pos = None + if self.dockAreaCount() != 0: + default_pos = self.dockArea(self.dockAreaCount()-1) + return default_pos + + def getWidgetsInGUIOrder(self) -> 'list[QtAds.CDockWidget]': + result = [] + for i in range(self.dockAreaCount()): + for widget in self.dockArea(i).dockWidgets(): + result.append(widget) + return result + + +class CreateChildDockAction(QAction): + def __init__(self, dock_in_dock: 'DockInDockWidget', menu: QMenu): + super().__init__("New group...", menu) + self.__dock_in_dock = dock_in_dock + self.triggered.connect(self.createGroup) + + def createGroup(self) -> None: + name = "" + while True: + name, ok = QInputDialog.getText(None, self.text(), "Enter group name", QLineEdit.Normal, name) + if ok: + error = "" + if self.__dock_in_dock.getTopLevelDockWidget(): + error = self.__dock_in_dock.getTopLevelDockWidget().getGroupNameError(name) + else: + assert False + + if not error: + self.__dock_in_dock.createGroup(name, None) + break + else: + QMessageBox.critical(None, "Error", error) + continue + else: + break + +class DestroyGroupAction(QAction): + def __init__(self, widget: 'DockInDockWidget', menu: QMenu): + super().__init__("Destroy" + widget.getManager().getGroupName(), menu) + self.__widget = widget + self.triggered.connect(self.destroyGroup) + + def destroyGroup(self) -> None: + self.__widget.getTopLevelDockWidget().destroyGroup(self.__widget) + + +class MoveDockWidgetAction(QAction): + def __init__(self, widget: 'DockInDockWidget', move_to: DockInDockManager, menu: QMenu): + super().__init__(menu) + self.__widget = widget + self.__move_to = move_to + + if move_to.parent().isTopLevel(): + self.setText("To top") + else: + self.setText(f"To {move_to.parent().objectName()}") + self.triggered.connect(self._move) + + def _move(self) -> None: + self.move(self.__widget, self.__move_to) + + def move(self, widget: QtAds.CDockWidget, move_to: QtAds.CDockManager) -> None: + if widget and move_to: + widget.dockManager().removeDockWidget(widget) + move_to.addDockWidget(QtAds.CenterDockWidgetArea, widget, move_to.getInsertDefaultPos()) + else: + assert False \ No newline at end of file diff --git a/examples/dockindock/main.py b/examples/dockindock/main.py new file mode 100644 index 0000000..5b6e478 --- /dev/null +++ b/examples/dockindock/main.py @@ -0,0 +1,72 @@ +import sys +import os +import atexit + +from PyQt5.QtWidgets import QApplication, QMainWindow, QLabel +from PyQt5.QtCore import Qt +from PyQtAds import QtAds + +from perspectives import PerspectivesManager +from dockindock import DockInDockWidget + +class MainWindow(QMainWindow): + def __init__(self, parent=None): + super().__init__(parent) + + self.perspectives_manager = PerspectivesManager("persist") + self.resize(400, 400) + self.dock_manager = DockInDockWidget(self, self.perspectives_manager, can_create_new_groups=True) + self.setCentralWidget(self.dock_manager) + self.dock_manager.attachViewMenu(self.menuBar().addMenu("View")) + + previous_dock_widget = None + for i in range(3): + l = QLabel() + l.setWordWrap(True) + l.setAlignment(Qt.AlignTop | Qt.AlignLeft) + l.setText("Lorem ipsum dolor sit amet, consectetuer adipiscing elit. ") + + previous_dock_widget = self.dock_manager.addTabWidget(l, f"Top label {i}", previous_dock_widget) + + last_top_level_dock = previous_dock_widget + + for j in range(2): + group_manager, _ = self.dock_manager.createGroup(f"Group {j}", last_top_level_dock) + + previous_dock_widget = None + + for i in range(3): + # Create example content label - this can be any application specific widget + l = QLabel() + l.setWordWrap(True) + l.setAlignment(Qt.AlignTop | Qt.AlignLeft) + l.setText("Lorem ipsum dolor sit amet, consectetuer adipiscing elit. ") + + previous_dock_widget = group_manager.addTabWidget(l, f"ZInner {j}/{i}", previous_dock_widget) + + # create sub-group + sub_group, _ = group_manager.createGroup(f"SubGroup {j}", previous_dock_widget) + previous_dock_widget = None + for i in range(3): + # Create example content label - this can be any application specific widget + l = QLabel() + l.setWordWrap(True) + l.setAlignment(Qt.AlignTop | Qt.AlignLeft) + l.setText("Lorem ipsum dolor sit amet, consectetuer adipiscing elit. ") + + previous_dock_widget = sub_group.addTabWidget(l, f"SubInner {j}/{i}", previous_dock_widget) + + self.perspectives_manager.loadPerspectives() + + atexit.register(self.cleanup) + + def cleanup(self): + self.perspectives_manager.savePerspectives() + + +if __name__ == '__main__': + app = QApplication(sys.argv) + + w = MainWindow() + w.show() + app.exec_() \ No newline at end of file diff --git a/examples/dockindock/perspectiveactions.py b/examples/dockindock/perspectiveactions.py new file mode 100644 index 0000000..9328855 --- /dev/null +++ b/examples/dockindock/perspectiveactions.py @@ -0,0 +1,25 @@ +from PyQt5.QtWidgets import QAction, QMenu + + +class LoadPerspectiveAction(QAction): + def __init__(self, parent: QMenu, name: str, dock_manager: 'DockInDockWidget'): + super().__init__(name, parent) + self.name = name + self.dock_manager = dock_manager + + self.triggered.connect(self.load) + + def load(self): + self.dock_manager.getPerspectivesManager().openPerspective(self.name, self.dock_manager) + + +class RemovePerspectiveAction(QAction): + def __init__(self, parent: QMenu, name: str, dock_manager: 'DockInDockWidget'): + super().__init__(name, parent) + self.name = name + self.dock_manager = dock_manager + + self.triggered.connect(self.remove) + + def remove(self): + self.dock_manager.getPerspectivesManager().removePerspective(self.name) \ No newline at end of file diff --git a/examples/dockindock/perspectives.py b/examples/dockindock/perspectives.py new file mode 100644 index 0000000..ab9dddb --- /dev/null +++ b/examples/dockindock/perspectives.py @@ -0,0 +1,203 @@ +import os +import tempfile +import shutil +import atexit + +from PyQt5.QtCore import pyqtSignal, QSettings, QObject +from PyQtAds import QtAds + +from dockindockmanager import DockInDockManager +from dockindock import DockInDockWidget + +GROUP_PREFIX = "Group" + +def findWidget(name, managers: 'list[DockInDockManager]') -> QtAds.CDockWidget: + for mgr in managers: + widget = mgr.findDockWidget(name) + if widget: + return widget + + +class PerspectiveInfo: + # Partially bypass ADS perspective management, store list here + # and then ADS will only have one perspective loaded + # this is because all docking widgets must exist when a perspective is loaded + # we will guarantee that! + + settings = QSettings() + groups: 'dict[str, list[str]]' = {} + + +class PerspectivesManager(QObject): + perspectivesListChanged = pyqtSignal() + openingPerspective = pyqtSignal() + openedPerspective = pyqtSignal() + + def __init__(self, perspectives_folder): + super().__init__() + self.__perspectives_folder = perspectives_folder + self.__perspectives = {} + atexit.register(self.cleanup) + + def cleanup(self): + for perspective in self.__perspectives.values(): + filename = perspective.settings.fileName() + try: + os.remove(filename) + except FileNotFoundError: + pass + + def perspectiveNames(self) -> 'list[str]': + return self.__perspectives.keys() + + def addPerspective(self, name: str, widget: DockInDockWidget) -> None: + if self.__perspectives_folder: + self.__perspectives[name] = perspective = PerspectiveInfo() + perspective.settings = self.getSettingsObject(self.getSettingsFileName(name, True)) + perspective.groups = widget.getManager().getGroupContents() + + # save perspective internally + widget.getManager().addPerspectiveRec(name) + # store it in QSettings object + widget.getManager().savePerspectivesRec(perspective.settings) + # remove internal perspectives + widget.getManager().removePerspectives(widget.getManager().perspectiveNames()) + + self.perspectivesListChanged.emit() + + def openPerspective(name: str, widget: DockInDockWidget) -> None: + assert widget.getTopLevelDockWidget() == widget + + if self.__perspectives_folder: + if name in self.__perspectives: + self.openingPerspective.emit() + + if widget.canCreateNewGroups(): + cur_groups = widget.getManager().allManagers(True, True) + for group in self.__perspectives[name].groups.keys(): + found = False + for curgroup in cur_groups: + if curgroup.getPerspectiveGroupName() == group: + found = True + break + if not found: + group = DockInDockManager.getGroupNameFromPersistGroupName(group) + + # restore group in file but not in GUI yet + widget.createGroup(group, None) + + cur_groups = widget.getManager().allManagers(False, True) + for curgroup in cur_groups: + if curgroup.getPersistGroupName() not in self.__perspectives[name].groups.keys(): + widget.destroyGroup(curgroup.parent()) + + managers = widget.getManager().allManagers(True, True) + for group in self.__perspectives[name].groups().keys(): + for mgr in managers: + if mgr.getPersistGroupName() == group: + for widget_name in self.__perspectives[name].groups[group]: + widget = findWidget(widget_name, [mgr]) + if widget: + pass # OK, widget is already in the good manager! + else: + widget = findWidget(widget_name, managers) + if widget: + # move dock widget in the same group as it used to be when perspective was saved + # this guarantee load/open perspectives will work smartly + MoveDockWidgetAction.move(widget, mgr) + + # internally load perspectives from QSettings + widget.getManager().loadPerspectivesRec(self.__perspectives[name].settings) + # load perspective (update GUI) + widget.getManager().openPerspectiveRec(name) + # remove internal perspectives + widget.getManager().removePerspectives(widget.getManager().perspectiveNames()) + + self.openedPerspective().emit() + else: + assert False + + def removePerspectives(self) -> None: + self.__perspectives.clear() + self.perspectivesListChanged.emit() + + def removePerspective(self, name: str) -> None: + del self.__perspectives[name] + self.perspectivesListChanged.emit() + + def getSettingsFileName(self, perspective: str, temp: bool) -> str: + name = "perspectives.ini" if not perspective else f"perspectives_{perspective + '.tmp' if temp else perspective + '.ini'}" + + return os.path.join(self.__perspectives_folder, name) + + def getSettingsObject(self, file_path: str) -> QSettings: + return QSettings(file_path, QSettings.IniFormat) + + def loadPerspectives(self) -> None: + if self.__perspectives_folder: + tempfile.mktemp(dir=self.__perspectives_folder) + + self.__perspectives.clear() + + main_settings = self.getSettingsObject(self.getSettingsFileName("", False)) + debug = main_settings.fileName() + + size = main_settings.beginReadArray("Perspectives") + + for i in range(0, size): + main_settings.setArrayIndex(i) + perspective = main_settings.value("Name") + + if perspective: + to_load = self.getSettingsFileName(perspective, False) + loaded = self.getSettingsFileName(perspective, True) + + try: + os.remove(loaded) + except FileNotFoundError: + pass + if not shutil.copy(to_load, loaded): + assert False + + self.__perspectives[perspective] = PerspectiveInfo() + self.__perspectives[perspective].settings = self.getSettingsObject(loaded) + + # load group info: + main_settings.beginGroup(GROUP_PREFIX) + for key in main_settings.allKeys(): + self.__perspectives[perspective].groups[key] = main_settings.value(key) + main_settings.endGroup() + else: + assert False + + main_settings.endArray() + + self.perspectivesListChanged.emit() + + def savePerspectives(self) -> None: + if self.__perspectives_folder: + main_settings = self.getSettingsObject(self.getSettingsFileName("", False)) + + # Save list of perspective and group organization + main_settings.beginWriteArray("Perspectives", len(self.__perspectives)) + for i, perspective in enumerate(self.__perspectives.keys()): + main_settings.setArrayIndex(i) + main_settings.setValue("Name", perspective) + main_settings.beginGroup(GROUP_PREFIX) + for group in self.__perspectives[perspective].groups.keys(): + main_settings.setValue(group, list(self.__perspectives[perspective].groups[group])) + main_settings.endGroup() + main_settings.endArray() + + # Save perspectives themselves + for perspective_name in self.__perspectives.keys(): + to_save = self.getSettingsFileName(perspective_name, False) + settings = self.__perspectives[perspective_name].settings + settings.sync() + + try: + os.remove(to_save) + except FileNotFoundError: + pass + if not shutil.copy(settings.fileName(), to_save): + assert False \ No newline at end of file diff --git a/examples/emptydockarea/main.py b/examples/emptydockarea/main.py new file mode 100644 index 0000000..046d45f --- /dev/null +++ b/examples/emptydockarea/main.py @@ -0,0 +1,108 @@ +import sys +import os + +from PyQt5 import uic +from PyQt5.QtCore import Qt, QSignalBlocker +from PyQt5.QtWidgets import (QApplication, QMainWindow, QLabel, QComboBox, QTableWidget, + QAction, QWidgetAction, QSizePolicy, QInputDialog) +from PyQt5.QtGui import QCloseEvent +from PyQtAds import QtAds + + +UI_FILE = os.path.join(os.path.dirname(__file__), 'mainwindow.ui') +MainWindowUI, MainWindowBase = uic.loadUiType(UI_FILE) + + +class CMainWindow(MainWindowUI, MainWindowBase): + def __init__(self, parent=None): + super().__init__(parent) + + self.setupUi(self) + + QtAds.CDockManager.setConfigFlag(QtAds.CDockManager.OpaqueSplitterResize, True) + QtAds.CDockManager.setConfigFlag(QtAds.CDockManager.XmlCompressionEnabled, False) + QtAds.CDockManager.setConfigFlag(QtAds.CDockManager.FocusHighlighting, True) + self.dock_manager = QtAds.CDockManager(self) + + # Set central widget + label = QLabel() + label.setText("This is a DockArea which is always visible, even if it does not contain any DockWidgets.") + label.setAlignment(Qt.AlignCenter) + central_dock_widget = QtAds.CDockWidget("CentralWidget") + central_dock_widget.setWidget(label) + central_dock_widget.setFeature(QtAds.CDockWidget.NoTab, True) + central_dock_area = self.dock_manager.setCentralWidget(central_dock_widget) + + # create other dock widgets + table = QTableWidget() + table.setColumnCount(3) + table.setRowCount(10) + table_dock_widget = QtAds.CDockWidget("Table 1") + table_dock_widget.setWidget(table) + table_dock_widget.setMinimumSizeHintMode(QtAds.CDockWidget.MinimumSizeHintFromDockWidget) + table_dock_widget.resize(250, 150) + table_dock_widget.setMinimumSize(200,150) + self.dock_manager.addDockWidgetTabToArea(table_dock_widget, central_dock_area) + table_area = self.dock_manager.addDockWidget(QtAds.DockWidgetArea.LeftDockWidgetArea, table_dock_widget) + self.menuView.addAction(table_dock_widget.toggleViewAction()) + + table = QTableWidget() + table.setColumnCount(5) + table.setRowCount(1020) + table_dock_widget = QtAds.CDockWidget("Table 2") + table_dock_widget.setWidget(table) + table_dock_widget.setMinimumSizeHintMode(QtAds.CDockWidget.MinimumSizeHintFromDockWidget) + table_dock_widget.resize(250, 150) + table_dock_widget.setMinimumSize(200,150) + self.dock_manager.addDockWidget(QtAds.DockWidgetArea.BottomDockWidgetArea, table_dock_widget, table_area) + self.menuView.addAction(table_dock_widget.toggleViewAction()) + + properties_table = QTableWidget() + properties_table.setColumnCount(3) + properties_table.setRowCount(10) + properties_dock_widget = QtAds.CDockWidget("Properties") + properties_dock_widget.setWidget(properties_table) + properties_dock_widget.setMinimumSizeHintMode(QtAds.CDockWidget.MinimumSizeHintFromDockWidget) + properties_dock_widget.resize(250, 150) + properties_dock_widget.setMinimumSize(200,150) + self.dock_manager.addDockWidget(QtAds.DockWidgetArea.RightDockWidgetArea, properties_dock_widget, central_dock_area) + self.menuView.addAction(properties_dock_widget.toggleViewAction()) + + self.createPerspectiveUi() + + def createPerspectiveUi(self): + save_perspective_action = QAction("Create Perspective", self) + save_perspective_action.triggered.connect(self.savePerspective) + perspective_list_action = QWidgetAction(self) + self.perspective_combo_box = QComboBox(self) + self.perspective_combo_box.setSizeAdjustPolicy(QComboBox.AdjustToContents) + self.perspective_combo_box.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred) + self.perspective_combo_box.activated[str].connect(self.dock_manager.openPerspective) + perspective_list_action.setDefaultWidget(self.perspective_combo_box) + self.toolBar.addSeparator() + self.toolBar.addAction(perspective_list_action) + self.toolBar.addAction(save_perspective_action) + + def savePerspective(self): + perspective_name, ok = QInputDialog.getText(self, "Save Perspective", "Enter unique name:") + if not perspective_name or not ok: + return + + self.dock_manager.addPerspective(perspective_name) + blocker = QSignalBlocker(self.perspective_combo_box) + self.perspective_combo_box.clear() + self.perspective_combo_box.addItems(self.dock_manager.perspectiveNames()) + self.perspective_combo_box.setCurrentText(perspective_name) + + def closeEvent(self, event: QCloseEvent): + # Delete dock manager here to delete all floating widgets. This ensures + # that all top level windows of the dock manager are properly closed + self.dock_manager.deleteLater() + super().closeEvent(event) + +if __name__ == '__main__': + app = QApplication(sys.argv) + + w = CMainWindow() + w.show() + app.exec_() \ No newline at end of file diff --git a/examples/sidebar/sidebar.py b/examples/sidebar/main.py similarity index 100% rename from examples/sidebar/sidebar.py rename to examples/sidebar/main.py diff --git a/examples/simple/simple.py b/examples/simple/main.py similarity index 100% rename from examples/simple/simple.py rename to examples/simple/main.py diff --git a/sip/DockFocusController.sip b/sip/DockFocusController.sip index 1f45699..a701096 100644 --- a/sip/DockFocusController.sip +++ b/sip/DockFocusController.sip @@ -21,6 +21,7 @@ public: void notifyWidgetOrAreaRelocation(QWidget* RelocatedWidget); void notifyFloatingWidgetDrop(ads::CFloatingDockContainer* FloatingWidget); ads::CDockWidget* focusedDockWidget() const; + void setDockWidgetTabFocused(ads::CDockWidgetTab* Tab); public slots: void setDockWidgetFocused(ads::CDockWidget* focusedNow); diff --git a/sip/DockWidget.sip b/sip/DockWidget.sip index 5aa4b83..f580b1b 100644 --- a/sip/DockWidget.sip +++ b/sip/DockWidget.sip @@ -33,8 +33,10 @@ public: CustomCloseHandling, DockWidgetFocusable, DockWidgetForceCloseWithArea, + NoTab, DefaultDockWidgetFeatures, AllDockWidgetFeatures, + DockWidgetAlwaysCloseAndDelete, NoDockWidgetFeatures }; typedef QFlags DockWidgetFeatures; diff --git a/sip/ads_globals.sip b/sip/ads_globals.sip index ead6b0e..7bbce94 100644 --- a/sip/ads_globals.sip +++ b/sip/ads_globals.sip @@ -2,6 +2,40 @@ %If (Qt_5_0_0 -) +%ModuleHeaderCode +PyObject *qtads_FindParent(PyObject* type, const QWidget *child); +%End + +%ModuleCode +PyObject *qtads_FindParent(PyObject* type, const QWidget *w) +{ + // Check that the types checking was successful. + if (!type) + return 0; + + QWidget* parentWidget = w->parentWidget(); + + while (parentWidget) + { + PyObject *ParentImpl = sipConvertFromType(parentWidget, sipType_QObject, 0); + if (!ParentImpl) + { + return 0; + } + + if (PyType_IsSubtype((PyTypeObject *)type, Py_TYPE(ParentImpl))) + return ParentImpl; + + Py_DECREF(ParentImpl); + + parentWidget = parentWidget->parentWidget(); + } + + Py_INCREF(Py_None); + return Py_None; +} +%End + namespace ads { %TypeHeaderCode @@ -54,6 +88,50 @@ namespace ads BitwiseAnd, BitwiseOr }; + + namespace internal + { + void replaceSplitterWidget(QSplitter* Splitter, QWidget* From, QWidget* To); + void hideEmptyParentSplitters(ads::CDockSplitter* FirstParentSplitter); + + class CDockInsertParam + { + %TypeHeaderCode + #include + %End + + public: + Qt::Orientation orientation() const; + bool append() const; + int insertOffset() const; + }; + ads::internal::CDockInsertParam dockAreaInsertParameters(ads::DockWidgetArea Area); + + SIP_PYOBJECT findParent(SIP_PYTYPE type, const QWidget *w) const /TypeHint="QObject"/; + %MethodCode + sipRes = qtads_FindParent(a0, a1); + + if (!sipRes) + { + sipIsErr = 1; + } + %End + + QPixmap createTransparentPixmap(const QPixmap& Source, qreal Opacity); + + QPoint globalPositionOf(QMouseEvent* ev); + + void setButtonIcon(QAbstractButton* Button, QStyle::StandardPixmap StandarPixmap, ads::eIcon CustomIconId); + + enum eRepolishChildOptions + { + RepolishIgnoreChildren, + RepolishDirectChildren, + RepolishChildrenRecursively + }; + + void repolishStyle(QWidget* w, ads::internal::eRepolishChildOptions Options = ads::internal::RepolishIgnoreChildren); + }; };