diff --git a/.github/workflows/linux-builds.yml b/.github/workflows/linux-builds.yml index 4be2a29..f3e2492 100644 --- a/.github/workflows/linux-builds.yml +++ b/.github/workflows/linux-builds.yml @@ -6,7 +6,7 @@ jobs: build: strategy: matrix: - os: [ubuntu-latest, ubuntu-18.04, ubuntu-16.04] + os: [ubuntu-latest, ubuntu-20.04, ubuntu-18.04] runs-on: ${{ matrix.os }} @@ -16,7 +16,7 @@ jobs: run: | sudo apt-get update --fix-missing sudo apt-get install qt5-default - sudo apt-get install libqt5x11extras5-dev + sudo apt-get install qtbase5-private-dev - name: qmake run: qmake - name: make diff --git a/.gitignore b/.gitignore index 05aa783..edb77ec 100644 --- a/.gitignore +++ b/.gitignore @@ -383,3 +383,4 @@ FodyWeavers.xsd / build /Settings.ini .vscode/settings.json +/.settings diff --git a/.settings/language.settings.xml b/.settings/language.settings.xml deleted file mode 100644 index c8d87f4..0000000 --- a/.settings/language.settings.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/.travis.yml b/.travis.yml index d1cf9e6..38db61f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,8 +9,6 @@ matrix: os: linux dist: trusty group: stable - before_install: - - sudo apt-get -y install libqt5x11extras5-dev addons: apt: sources: @@ -20,8 +18,6 @@ matrix: packages: - qt55base - qt55tools - - qt55x11extras - - libqt5x11extras5-dev - gcc-9 - g++-9 script: @@ -39,8 +35,6 @@ matrix: services: - xvfb compiler: gcc - before_install: - - sudo apt-get -y install libqt5x11extras5-dev addons: apt: sources: @@ -50,8 +44,6 @@ matrix: packages: - qt514base - qt514tools - - qt514x11extras - - libqt5x11extras5-dev - gcc-9 - g++-9 - libc6-i386 @@ -73,8 +65,6 @@ matrix: services: - xvfb compiler: gcc - before_install: - - sudo apt-get -y install libqt5x11extras5-dev addons: apt: sources: @@ -84,8 +74,6 @@ matrix: packages: - qt514base - qt514tools - - qt514x11extras - - libqt5x11extras5-dev - gcc-9 - g++-9 - libc6-i386 @@ -107,8 +95,6 @@ matrix: services: - xvfb compiler: gcc - before_install: - - sudo apt-get -y install libqt5x11extras5-dev addons: apt: sources: @@ -118,8 +104,6 @@ matrix: packages: - qt514base - qt514tools - - qt514x11extras - - libqt5x11extras5-dev - gcc-9 - g++-9 - libc6-i386 @@ -147,8 +131,6 @@ matrix: services: - xvfb compiler: gcc - before_install: - - sudo apt-get -y install libqt5x11extras5-dev addons: apt: sources: @@ -158,8 +140,6 @@ matrix: packages: - qt514base - qt514tools - - qt514x11extras - - libqt5x11extras5-dev - gcc-9 - g++-9 - libc6-i386 diff --git a/README.md b/README.md index e497234..71bfe1a 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,13 @@ integrated development environments (IDEs) such as Visual Studio. ## New and Noteworthy +The [release 3.8](https://github.com/githubuser0xFFFF/Qt-Advanced-Docking-System/releases/tag/3.8.0) +adds the following features: + +- option to close tabs with the middle mouse button +- `DeleteContentOnClose` flag for dynamic deletion and creation of dock widget + content + The [release 3.7](https://github.com/githubuser0xFFFF/Qt-Advanced-Docking-System/releases/tag/3.7.0) adds the following features: @@ -81,6 +88,8 @@ know it from Visual Studio. - [ezEditor](#ezeditor) - [D-Tect X](#d-tect-x) - [HiveWE](#hivewe) + - [Ramses Composer](#ramses-composer) + - [Plot Juggler](#plot-juggler) ### Docking everywhere - no central widget @@ -219,7 +228,13 @@ Screenshot Ubuntu: ## Build -Open the `ads.pro` with QtCreator and start the build, that's it. +The Linux build requires private header files. Make sure that they are installed: + +```bash +sudo apt install qtbase5-private-dev +``` + +Open the `ads.pro` file with QtCreator and start the build, that's it. You can run the demo project and test it yourself. ## Getting started / Example @@ -391,3 +406,38 @@ of the open editor windows. [learn more...](https://github.com/stijnherfst/HiveWE) ![HiveWE](doc/showcase_hivewe.png) + +### [Ramses Composer](https://github.com/GENIVI/ramses-composer) + +Ramses Composer is the authoring tool for the open source [RAMSES](https://github.com/GENIVI/ramses) +rendering ecosystem. + +Ramses is a low-level rendering engine which is optimized for embedded hardware +mobile devices, automotive ECUs, IoT electronics. Ramses was initially developed +at the BMW Group and open-sourced in 2018 as part of a collaboration initiative +with the Genivi Alliance. It is an important part of the BMW infotainment cluster +and digital portfolio. + +[learn more...](https://github.com/GENIVI/ramses-composer) + +![RamsesComposer](doc/showcase_ramses_composer.png) + +### [Plot Juggler](https://github.com/facontidavide/PlotJuggler) + +PlotJuggler is a fast, powerful and intuitive tool to visualize time series. +It makes it easy to visualize data but also to analyze it. You can manipulate +your time series using a simple and extendable Transform Editor. Some of the +highlights are: + +- Simple Drag & Drop user interface. +- Load data from file. +- Connect to live streaming of data. +- Save the visualization layout and configurations to re-use them later. +- Fast OpenGL visualization. +- Can handle thousands of timeseries and millions of data points. +- Transform your data using a simple editor: derivative, moving average, integral, etc… +- PlotJuggler can be easily extended using plugins. + +[read more...](https://github.com/facontidavide/PlotJuggler) + +[![Plot Juggler](doc/showcase_plot_juggler.png)](https://vimeo.com/480588113#t=46s) \ No newline at end of file diff --git a/ads.pri b/ads.pri index e75733f..61a4599 100644 --- a/ads.pri +++ b/ads.pri @@ -1,28 +1,25 @@ -lessThan(QT_MAJOR_VERSION, 6) { - CONFIG(debug, debug|release){ - win32 { - LIBS += -lqtadvanceddockingd - } - else:mac { - LIBS += -lqtadvanceddocking_debug - } - else { - LIBS += -lqtadvanceddocking - } +CONFIG(debug, debug|release){ + win32 { + versionAtLeast(QT_VERSION, 5.15.0) { + LIBS += -lqtadvanceddocking + } + else { + LIBS += -lqtadvanceddockingd + } } - else{ + else:mac { + LIBS += -lqtadvanceddocking_debug + } + else { LIBS += -lqtadvanceddocking } } -else { - # qt$$qtLibraryTarget(qtadvanceddocking) does not produce an advanceddockingd.dll file on Windows - # for Qt6 - I don't know if this is a bug and I have to investigate +else{ LIBS += -lqtadvanceddocking } unix:!macx { LIBS += -lxcb - QT += x11extras } diff --git a/adsConfig.cmake b/adsConfig.cmake index 9f9b307..542e5c9 100644 --- a/adsConfig.cmake +++ b/adsConfig.cmake @@ -2,7 +2,4 @@ include(CMakeFindDependencyMacro) find_dependency(Qt5Core ${REQUIRED_QT_VERSION} REQUIRED) find_dependency(Qt5Gui ${REQUIRED_QT_VERSION} REQUIRED) find_dependency(Qt5Widgets ${REQUIRED_QT_VERSION} REQUIRED) -if(UNIX AND NOT APPLE) - find_dependency(Qt5X11Extras ${REQUIRED_QT_VERSION} REQUIRED) -endif() include("${CMAKE_CURRENT_LIST_DIR}/adsTargets.cmake") \ No newline at end of file diff --git a/demo/CMakeLists.txt b/demo/CMakeLists.txt index d2bc9e0..6b4504c 100644 --- a/demo/CMakeLists.txt +++ b/demo/CMakeLists.txt @@ -1,9 +1,10 @@ cmake_minimum_required(VERSION 3.5) project(ads_demo VERSION ${VERSION_SHORT}) -find_package(Qt5 5.5 COMPONENTS Core Gui Widgets REQUIRED) -if(WIN32) - find_package(Qt5 5.5 COMPONENTS AxContainer REQUIRED) +find_package(QT NAMES Qt6 Qt5 COMPONENTS Core REQUIRED) +find_package(Qt${QT_VERSION_MAJOR} 5.5 COMPONENTS Core Gui Widgets REQUIRED) +if(WIN32 AND QT_VERSION_MAJOR LESS 6) + find_package(Qt${QT_VERSION_MAJOR} COMPONENTS AxContainer REQUIRED) endif() set(CMAKE_INCLUDE_CURRENT_DIR ON) set(ads_demo_SRCS @@ -16,9 +17,11 @@ set(ads_demo_SRCS ) add_executable(AdvancedDockingSystemDemo WIN32 ${ads_demo_SRCS}) target_include_directories(AdvancedDockingSystemDemo PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/../src") -target_link_libraries(AdvancedDockingSystemDemo PUBLIC Qt5::Core Qt5::Gui Qt5::Widgets) -if(WIN32) - target_link_libraries(AdvancedDockingSystemDemo PUBLIC Qt5::AxContainer) +target_link_libraries(AdvancedDockingSystemDemo PUBLIC Qt${QT_VERSION_MAJOR}::Core + Qt${QT_VERSION_MAJOR}::Gui + Qt${QT_VERSION_MAJOR}::Widgets) +if(WIN32 AND QT_VERSION_MAJOR LESS 6) + target_link_libraries(AdvancedDockingSystemDemo PUBLIC Qt${QT_VERSION_MAJOR}::AxContainer) endif() target_link_libraries(AdvancedDockingSystemDemo PRIVATE qtadvanceddocking) set_target_properties(AdvancedDockingSystemDemo PROPERTIES diff --git a/demo/MainWindow.cpp b/demo/MainWindow.cpp index 63eaa45..9745755 100644 --- a/demo/MainWindow.cpp +++ b/demo/MainWindow.cpp @@ -79,7 +79,7 @@ #include "FloatingDockContainer.h" #include "DockComponentsFactory.h" #include "StatusDialog.h" - +#include "DockSplitter.h" /** @@ -417,6 +417,22 @@ void MainWindowPrivate::createContent() DockWidget = createCalendarDockWidget(); DockWidget->setTabToolTip(QString("Tab ToolTip\nHodie est dies magna")); auto DockArea = DockManager->addDockWidget(ads::CenterDockWidgetArea, DockWidget, TopDockArea); + // Now we create a action to test resizing of DockArea widget + auto Action = ui.menuTests->addAction(QString("Resize %1").arg(DockWidget->windowTitle())); + QObject::connect(Action, &QAction::triggered, [DockArea]() + { + // Resizing only works, if the Splitter is visible and has a valid + // sizes + auto Splitter = ads::internal::findParent(DockArea); + if (!Splitter) + { + return; + } + // We change the sizes of the splitter that contains the Calendar 1 widget + // to resize the dock widget + int Width = Splitter->width(); + Splitter->setSizes({Width * 2/3, Width * 1/3}); + }); // Now we add a custom button to the dock area title bar that will create // new editor widgets when clicked @@ -443,13 +459,14 @@ void MainWindowPrivate::createContent() DockManager->addDockWidget(ads::CenterDockWidgetArea, createLongTextLabelDockWidget(), RighDockArea); DockManager->addDockWidget(ads::CenterDockWidgetArea, createLongTextLabelDockWidget(), BottomDockArea); - auto Action = ui.menuTests->addAction(QString("Set %1 Floating").arg(DockWidget->windowTitle())); + Action = ui.menuTests->addAction(QString("Set %1 Floating").arg(DockWidget->windowTitle())); DockWidget->connect(Action, SIGNAL(triggered()), SLOT(setFloating())); Action = ui.menuTests->addAction(QString("Set %1 As Current Tab").arg(DockWidget->windowTitle())); DockWidget->connect(Action, SIGNAL(triggered()), SLOT(setAsCurrentTab())); Action = ui.menuTests->addAction(QString("Raise %1").arg(DockWidget->windowTitle())); DockWidget->connect(Action, SIGNAL(triggered()), SLOT(raise())); + #ifdef Q_OS_WIN #if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) if (!ads::CDockManager::testConfigFlag(ads::CDockManager::OpaqueUndocking)) @@ -613,6 +630,9 @@ CMainWindow::CMainWindow(QWidget *parent) : // uncomment if you would like to enable an equal distribution of the // available size of a splitter to all contained dock widgets // CDockManager::setConfigFlag(CDockManager::EqualSplitOnInsertion, true); + + // uncomment if you would like to close tabs with the middle mouse button, web browser style + // CDockManager::setConfigFlag(CDockManager::MiddleMouseButtonClosesTab, true); // Now create the dock manager and its content d->DockManager = new CDockManager(this); 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/doc/cfg_flag_MiddleMouseButtonClosesTab.gif b/doc/cfg_flag_MiddleMouseButtonClosesTab.gif new file mode 100644 index 0000000..a6b0097 Binary files /dev/null and b/doc/cfg_flag_MiddleMouseButtonClosesTab.gif differ diff --git a/doc/showcase_plot_juggler.png b/doc/showcase_plot_juggler.png new file mode 100644 index 0000000..a4f074b Binary files /dev/null and b/doc/showcase_plot_juggler.png differ diff --git a/doc/showcase_ramses_composer.png b/doc/showcase_ramses_composer.png new file mode 100644 index 0000000..1fd2ddd Binary files /dev/null and b/doc/showcase_ramses_composer.png differ diff --git a/doc/user-guide.md b/doc/user-guide.md index a7492ca..531184f 100644 --- a/doc/user-guide.md +++ b/doc/user-guide.md @@ -27,6 +27,17 @@ - [`EqualSplitOnInsertion`](#equalsplitoninsertion) - [`FloatingContainerForceNativeTitleBar` (Linux only)](#floatingcontainerforcenativetitlebar-linux-only) - [`FloatingContainerForceQWidgetTitleBar` (Linux only)](#floatingcontainerforceqwidgettitlebar-linux-only) + - [`MiddleMouseButtonClosesTab`](#middlemousebuttonclosestab) +- [DockWidget Feature Flags](#dockwidget-feature-flags) + - [`DockWidgetClosable`](#dockwidgetclosable) + - [`DockWidgetMovable`](#dockwidgetmovable) + - [`DockWidgetFloatable`](#dockwidgetfloatable) + - [`DockWidgetDeleteOnClose`](#dockwidgetdeleteonclose) + - [`CustomCloseHandling`](#customclosehandling) + - [`DockWidgetFocusable`](#dockwidgetfocusable) + - [`DockWidgetForceCloseWithArea`](#dockwidgetforceclosewitharea) + - [`NoTab`](#notab) + - [`DeleteContentOnClose`](#deletecontentonclose) - [Central Widget](#central-widget) - [Empty Dock Area](#empty-dock-area) - [Custom Close Handling](#custom-close-handling) @@ -464,6 +475,69 @@ If you would like to overwrite autodetection, then you can activate this flag to force QWidget based title bars. You can overwrite autodetection and this flag, if you set the environment variable `ADS_UseNativeTitle` to 0 or 1. +### `MiddleMouseButtonClosesTab` + +If the flag is set, the user can use the mouse middle button to close the tab +under the mouse. So you do not need to exactly hit the tab close button to +close tab. Just click with the middle mouse button on a tab like this is +possible in various web browsers. + +![MiddleMouseButtonClosesTab true](cfg_flag_MiddleMouseButtonClosesTab.gif) + +## DockWidget Feature Flags + +### `DockWidgetClosable` + +If set, the dock widget will have a close button. + +### `DockWidgetMovable` + +If a dock widget is movable, then it and can be moved to a new position in the +current dock container. Disable this flag to prevent moving of a dock widget +via mouse. If the `OpaqueUndocking` configuration flag is set, then dock widgets +are immediately undocked into floating widgets. That means, moving is only +possible in this case, if the dock widget is also floatable (feature flag +`DockWidgetFloatable` is set). + +### `DockWidgetFloatable` + +If set, a dock widget can be dragged into a floating window. + +### `DockWidgetDeleteOnClose` + +Deletes the dock widget and its content when it is closed. + +### `CustomCloseHandling` + +Clicking the close button will not close the dock widget but emits the +`closeRequested()` signal instead. This allows the application to implement +a custom close handling. + +### `DockWidgetFocusable` + +If this is enabled, a dock widget can get focus highlighting. + +### `DockWidgetForceCloseWithArea` + +A dock widget will be closed when the dock area hosting it is closed. If the +`DockWidgetDeleteOnClose` feature is enabled for a dock widget, then it will +be deleted, if the user clicks the close button of this dock widget. If the +user clicks the close button of the dock area that contains this widget, +then only the visibility of the dock widget is toggled. If this feature flag +is set, the closing the dock area also closes the dock widget. That means, if +the dock widget feature `DockWidgetDeleteOnClose` is set for the dock widgets +in a dock area, then all dock widgets will be deleted if the dock area is closed. + +### `NoTab` + +A dock widget tab will never be shown if this flag is set. + +### `DeleteContentOnClose` + +Deletes only the contained widget on close, keeping the dock widget intact and +in place. Attempts to rebuild the contents widget on show if there is a widget +factory set. See [issue #365](https://github.com/githubuser0xFFFF/Qt-Advanced-Docking-System/pull/365) for more details. + ## Central Widget The Advanced Docking System has been developed to overcome the limitations of diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index bc17f01..a7ff41e 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -4,4 +4,5 @@ add_subdirectory(simple) add_subdirectory(sidebar) add_subdirectory(deleteonclose) add_subdirectory(centralwidget) -add_subdirectory(emptydockarea) \ No newline at end of file +add_subdirectory(emptydockarea) +add_subdirectory(dockindock) \ No newline at end of file diff --git a/examples/centralwidget/CMakeLists.txt b/examples/centralwidget/CMakeLists.txt index aa69603..7f57821 100644 --- a/examples/centralwidget/CMakeLists.txt +++ b/examples/centralwidget/CMakeLists.txt @@ -1,6 +1,7 @@ cmake_minimum_required(VERSION 3.5) project(ads_example_centralwidget VERSION ${VERSION_SHORT}) -find_package(Qt5 5.5 COMPONENTS Core Gui Widgets REQUIRED) +find_package(QT NAMES Qt6 Qt5 COMPONENTS Core REQUIRED) +find_package(Qt${QT_VERSION_MAJOR} 5.5 COMPONENTS Core Gui Widgets REQUIRED) set(CMAKE_INCLUDE_CURRENT_DIR ON) add_executable(CentralWidgetExample WIN32 main.cpp @@ -9,7 +10,9 @@ add_executable(CentralWidgetExample WIN32 ) target_include_directories(CentralWidgetExample PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/../../src") target_link_libraries(CentralWidgetExample PRIVATE qtadvanceddocking) -target_link_libraries(CentralWidgetExample PUBLIC Qt5::Core Qt5::Gui Qt5::Widgets) +target_link_libraries(CentralWidgetExample PUBLIC Qt${QT_VERSION_MAJOR}::Core + Qt${QT_VERSION_MAJOR}::Gui + Qt${QT_VERSION_MAJOR}::Widgets) set_target_properties(CentralWidgetExample PROPERTIES AUTOMOC ON AUTORCC ON 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/CMakeLists.txt b/examples/deleteonclose/CMakeLists.txt index 0aa8630..d329704 100644 --- a/examples/deleteonclose/CMakeLists.txt +++ b/examples/deleteonclose/CMakeLists.txt @@ -1,13 +1,16 @@ cmake_minimum_required(VERSION 3.5) project(ads_example_deleteonclose VERSION ${VERSION_SHORT}) -find_package(Qt5 5.5 COMPONENTS Core Gui Widgets REQUIRED) +find_package(QT NAMES Qt6 Qt5 COMPONENTS Core REQUIRED) +find_package(Qt${QT_VERSION_MAJOR} 5.5 COMPONENTS Core Gui Widgets REQUIRED) set(CMAKE_INCLUDE_CURRENT_DIR ON) add_executable(DeleteOnCloseTest WIN32 main.cpp ) target_include_directories(DeleteOnCloseTest PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/../../src") target_link_libraries(DeleteOnCloseTest PRIVATE qtadvanceddocking) -target_link_libraries(DeleteOnCloseTest PUBLIC Qt5::Core Qt5::Gui Qt5::Widgets) +target_link_libraries(DeleteOnCloseTest PUBLIC Qt${QT_VERSION_MAJOR}::Core + Qt${QT_VERSION_MAJOR}::Gui + Qt${QT_VERSION_MAJOR}::Widgets) set_target_properties(DeleteOnCloseTest PROPERTIES AUTOMOC ON CXX_STANDARD 14 diff --git a/examples/deleteonclose/main.cpp b/examples/deleteonclose/main.cpp index 14b0216..25f8ed1 100644 --- a/examples/deleteonclose/main.cpp +++ b/examples/deleteonclose/main.cpp @@ -41,19 +41,39 @@ int main(int argc, char *argv[]) now->widget()->setFocus(); }); - QAction *action = new QAction("New Delete On Close", &w); + QAction *action = new QAction("New [DockWidgetDeleteOnClose]", &w); w.menuBar()->addAction(action); int i = 0; QObject::connect(action, &QAction::triggered, [&]() { - auto dw = new ads::CDockWidget(QStringLiteral("test doc %1").arg(i++), &w); + auto dw = new ads::CDockWidget(QStringLiteral("test %1 [DockWidgetDeleteOnClose]").arg(i++), &w); auto editor = new QTextEdit(QStringLiteral("lorem ipsum..."), dw); dw->setWidget(editor); dw->setFeature(ads::CDockWidget::DockWidgetDeleteOnClose, true); auto area = dockManager->addDockWidgetTab(ads::CenterDockWidgetArea, dw); qDebug() << "doc dock widget created!" << dw << area; }); - + + auto dw = new ads::CDockWidget(QStringLiteral("test %1 [DeleteContentOnClose]").arg(i++), &w); + auto editor = new QTextEdit(QStringLiteral("recreated lorem ipsum......"), dw); + dw->setWidget(editor); + dw->setFeature(ads::CDockWidget::DeleteContentOnClose, true); + dw->setWidgetFactory([](QWidget* dw) + { + static int timesRecreated = 0; + return new QTextEdit(QStringLiteral("recreated lorem ipsum... times %1").arg(++timesRecreated), dw); + }); + auto area = dockManager->addDockWidgetTab(ads::CenterDockWidgetArea, dw); + qDebug() << "DeleteContentOnClose dock widget created!" << dw << area; + + action = new QAction("Toggle [DeleteContentOnClose]", &w); + w.menuBar()->addAction(action); + + QObject::connect(action, &QAction::triggered, [dw]() { + dw->toggleView(dw->isClosed()); + qDebug() << QString("dock widget %1! contents widget %2!").arg(dw->isClosed() ? "closed" : "open", dw->widget() ? "created" : "deleted"); + }); + action = new QAction("New", &w); w.menuBar()->addAction(action); QObject::connect(action, &QAction::triggered, [&]() { 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/CMakeLists.txt b/examples/dockindock/CMakeLists.txt index c2a70b6..3bf489e 100644 --- a/examples/dockindock/CMakeLists.txt +++ b/examples/dockindock/CMakeLists.txt @@ -1,8 +1,9 @@ cmake_minimum_required(VERSION 3.5) -project(ads_example_simple VERSION ${VERSION_SHORT}) -find_package(Qt5 5.5 COMPONENTS Core Gui Widgets REQUIRED) +project(ads_example_dockindock VERSION ${VERSION_SHORT}) +find_package(QT NAMES Qt6 Qt5 COMPONENTS Core REQUIRED) +find_package(Qt${QT_VERSION_MAJOR} 5.5 COMPONENTS Core Gui Widgets REQUIRED) set(CMAKE_INCLUDE_CURRENT_DIR ON) -add_executable(DockInDock WIN32 +add_executable(DockInDockExample WIN32 dockindock.cpp dockindockmanager.cpp perspectiveactions.cpp @@ -10,10 +11,12 @@ add_executable(DockInDock WIN32 main.cpp mainframe.cpp ) -target_include_directories(SimpleExample PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/../../src") -target_link_libraries(SimpleExample PRIVATE qtadvanceddocking) -target_link_libraries(SimpleExample PUBLIC Qt5::Core Qt5::Gui Qt5::Widgets) -set_target_properties(SimpleExample PROPERTIES +target_include_directories(DockInDockExample PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/../../src") +target_link_libraries(DockInDockExample PRIVATE qtadvanceddocking) +target_link_libraries(DockInDockExample PUBLIC Qt${QT_VERSION_MAJOR}::Core + Qt${QT_VERSION_MAJOR}::Gui + Qt${QT_VERSION_MAJOR}::Widgets) +set_target_properties(DockInDockExample PROPERTIES AUTOMOC ON AUTORCC ON AUTOUIC ON 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/CMakeLists.txt b/examples/emptydockarea/CMakeLists.txt index 94e851d..baed967 100644 --- a/examples/emptydockarea/CMakeLists.txt +++ b/examples/emptydockarea/CMakeLists.txt @@ -1,6 +1,7 @@ cmake_minimum_required(VERSION 3.5) project(ads_example_centralwidget VERSION ${VERSION_SHORT}) -find_package(Qt5 5.5 COMPONENTS Core Gui Widgets REQUIRED) +find_package(QT NAMES Qt6 Qt5 COMPONENTS Core REQUIRED) +find_package(Qt${QT_VERSION_MAJOR} 5.5 COMPONENTS Core Gui Widgets REQUIRED) set(CMAKE_INCLUDE_CURRENT_DIR ON) add_executable(EmptyDockAreaExample WIN32 main.cpp @@ -9,7 +10,9 @@ add_executable(EmptyDockAreaExample WIN32 ) target_include_directories(EmptyDockAreaExample PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/../../src") target_link_libraries(EmptyDockAreaExample PRIVATE qtadvanceddocking) -target_link_libraries(EmptyDockAreaExample PUBLIC Qt5::Core Qt5::Gui Qt5::Widgets) +target_link_libraries(EmptyDockAreaExample PUBLIC Qt${QT_VERSION_MAJOR}::Core + Qt${QT_VERSION_MAJOR}::Gui + Qt${QT_VERSION_MAJOR}::Widgets) set_target_properties(EmptyDockAreaExample PROPERTIES AUTOMOC ON AUTORCC ON 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/CMakeLists.txt b/examples/sidebar/CMakeLists.txt index 6940583..24b1954 100644 --- a/examples/sidebar/CMakeLists.txt +++ b/examples/sidebar/CMakeLists.txt @@ -1,6 +1,7 @@ cmake_minimum_required(VERSION 3.5) project(ads_example_sidebar VERSION ${VERSION_SHORT}) -find_package(Qt5 5.5 COMPONENTS Core Gui Widgets REQUIRED) +find_package(QT NAMES Qt6 Qt5 COMPONENTS Core REQUIRED) +find_package(Qt${QT_VERSION_MAJOR} 5.5 COMPONENTS Core Gui Widgets REQUIRED) set(CMAKE_INCLUDE_CURRENT_DIR ON) add_executable(SidebarExample WIN32 main.cpp @@ -9,7 +10,9 @@ add_executable(SidebarExample WIN32 ) target_include_directories(SidebarExample PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/../../src") target_link_libraries(SidebarExample PRIVATE qtadvanceddocking) -target_link_libraries(SidebarExample PUBLIC Qt5::Core Qt5::Gui Qt5::Widgets) +target_link_libraries(SidebarExample PUBLIC Qt${QT_VERSION_MAJOR}::Core + Qt${QT_VERSION_MAJOR}::Gui + Qt${QT_VERSION_MAJOR}::Widgets) set_target_properties(SidebarExample PROPERTIES AUTOMOC ON AUTORCC ON 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/CMakeLists.txt b/examples/simple/CMakeLists.txt index 8c66a5a..027ed0f 100644 --- a/examples/simple/CMakeLists.txt +++ b/examples/simple/CMakeLists.txt @@ -1,6 +1,7 @@ cmake_minimum_required(VERSION 3.5) project(ads_example_simple VERSION ${VERSION_SHORT}) -find_package(Qt5 5.5 COMPONENTS Core Gui Widgets REQUIRED) +find_package(QT NAMES Qt6 Qt5 COMPONENTS Core REQUIRED) +find_package(Qt${QT_VERSION_MAJOR} 5.5 COMPONENTS Core Gui Widgets REQUIRED) set(CMAKE_INCLUDE_CURRENT_DIR ON) add_executable(SimpleExample WIN32 main.cpp @@ -9,7 +10,9 @@ add_executable(SimpleExample WIN32 ) target_include_directories(SimpleExample PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/../../src") target_link_libraries(SimpleExample PRIVATE qtadvanceddocking) -target_link_libraries(SimpleExample PUBLIC Qt5::Core Qt5::Gui Qt5::Widgets) +target_link_libraries(SimpleExample PUBLIC Qt${QT_VERSION_MAJOR}::Core + Qt${QT_VERSION_MAJOR}::Gui + Qt${QT_VERSION_MAJOR}::Widgets) set_target_properties(SimpleExample PROPERTIES AUTOMOC ON AUTORCC ON 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/setup.py b/setup.py index 31546a9..1986df7 100644 --- a/setup.py +++ b/setup.py @@ -227,9 +227,7 @@ class build_ext(sipdistutils.build_ext): extension.extra_link_args += ['-F' + self.qtconfig.QT_INSTALL_LIBS, '-mmacosx-version-min=10.9'] elif sys.platform == 'linux': - extension.extra_compile_args += ['-D', 'QT_X11EXTRAS_LIB', '-std=c++11'] - extension.include_dirs += [os.path.join(self.qt_include_dir, 'QtX11Extras')] - extension.libraries += ['Qt5X11Extras' + self.qt_libinfix] + extension.extra_compile_args += ['-std=c++11'] return super().swig_sources(sources, extension) 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..91df1c7 100644 --- a/sip/ads_globals.sip +++ b/sip/ads_globals.sip @@ -1,60 +1,138 @@ -%Import QtWidgets/QtWidgetsmod.sip - -%If (Qt_5_0_0 -) - -namespace ads -{ - %TypeHeaderCode - #include - %End - - enum DockWidgetArea - { - NoDockWidgetArea, - LeftDockWidgetArea, - RightDockWidgetArea, - TopDockWidgetArea, - BottomDockWidgetArea, - CenterDockWidgetArea, - - InvalidDockWidgetArea, - OuterDockAreas, - AllDockAreas - }; - typedef QFlags DockWidgetAreas; - - - enum TitleBarButton - { - TitleBarButtonTabsMenu, - TitleBarButtonUndock, - TitleBarButtonClose - }; - - enum eDragState - { - DraggingInactive, - DraggingMousePressed, - DraggingTab, - DraggingFloatingWidget - }; - - enum eIcon - { - TabCloseIcon, - DockAreaMenuIcon, - DockAreaUndockIcon, - DockAreaCloseIcon, - - IconCount, - }; - - enum eBitwiseOperator - { - BitwiseAnd, - BitwiseOr - }; - -}; - -%End +%Import QtWidgets/QtWidgetsmod.sip + +%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 (PyObject_IsInstance(ParentImpl, type)) + return ParentImpl; + + Py_DECREF(ParentImpl); + + parentWidget = parentWidget->parentWidget(); + } + + Py_INCREF(Py_None); + return Py_None; +} +%End + +namespace ads +{ + %TypeHeaderCode + #include + %End + + enum DockWidgetArea + { + NoDockWidgetArea, + LeftDockWidgetArea, + RightDockWidgetArea, + TopDockWidgetArea, + BottomDockWidgetArea, + CenterDockWidgetArea, + + InvalidDockWidgetArea, + OuterDockAreas, + AllDockAreas + }; + typedef QFlags DockWidgetAreas; + + + enum TitleBarButton + { + TitleBarButtonTabsMenu, + TitleBarButtonUndock, + TitleBarButtonClose + }; + + enum eDragState + { + DraggingInactive, + DraggingMousePressed, + DraggingTab, + DraggingFloatingWidget + }; + + enum eIcon + { + TabCloseIcon, + DockAreaMenuIcon, + DockAreaUndockIcon, + DockAreaCloseIcon, + + IconCount, + }; + + enum eBitwiseOperator + { + 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); + }; + +}; + +%End diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 0ca84f0..49cd799 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,8 +1,9 @@ cmake_minimum_required(VERSION 3.5) project(QtAdvancedDockingSystem LANGUAGES CXX VERSION ${VERSION_SHORT}) -find_package(Qt5 5.5 COMPONENTS Core Gui Widgets REQUIRED) +find_package(QT NAMES Qt6 Qt5 COMPONENTS Core REQUIRED) +find_package(Qt${QT_VERSION_MAJOR} 5.5 COMPONENTS Core Gui Widgets REQUIRED) if (UNIX AND NOT APPLE) - find_package(Qt5 5.5 COMPONENTS X11Extras REQUIRED) + include_directories(${Qt${QT_VERSION_MAJOR}Gui_PRIVATE_INCLUDE_DIRS}) endif() set(CMAKE_INCLUDE_CURRENT_DIR ON) if(BUILD_STATIC) @@ -60,12 +61,10 @@ else() add_library(qtadvanceddocking SHARED ${ads_SRCS} ${ads_HEADERS}) target_compile_definitions(qtadvanceddocking PRIVATE ADS_SHARED_EXPORT) endif() -target_link_libraries(qtadvanceddocking PUBLIC Qt5::Core Qt5::Gui Qt5::Widgets) -if(UNIX AND NOT APPLE) - target_link_libraries(qtadvanceddocking PUBLIC Qt5::X11Extras) - target_link_libraries(qtadvanceddocking PRIVATE xcb) -endif() -set_target_properties(qtadvanceddocking PROPERTIES +target_link_libraries(qtadvanceddocking PUBLIC Qt${QT_VERSION_MAJOR}::Core + Qt${QT_VERSION_MAJOR}::Gui + Qt${QT_VERSION_MAJOR}::Widgets) +set_target_properties(qtadvanceddocking PROPERTIES AUTOMOC ON AUTORCC ON CXX_STANDARD 14 @@ -84,13 +83,13 @@ write_basic_package_version_file( COMPATIBILITY SameMajorVersion ) install(FILES ${ads_HEADERS} - DESTINATION include + DESTINATION include COMPONENT headers ) -install(FILES +install(FILES "${CMAKE_SOURCE_DIR}/LICENSE" "${CMAKE_SOURCE_DIR}/gnu-lgpl-v2.1.md" - DESTINATION license + DESTINATION license COMPONENT license ) install(TARGETS qtadvanceddocking diff --git a/src/DockAreaTitleBar.cpp b/src/DockAreaTitleBar.cpp index 7fbb01a..8651d29 100644 --- a/src/DockAreaTitleBar.cpp +++ b/src/DockAreaTitleBar.cpp @@ -51,6 +51,7 @@ #include "DockAreaTabBar.h" #include "IconProvider.h" #include "DockComponentsFactory.h" +#include "DockFocusController.h" #include @@ -471,7 +472,8 @@ void CDockAreaTitleBar::mousePressEvent(QMouseEvent* ev) if (CDockManager::testConfigFlag(CDockManager::FocusHighlighting)) { - d->TabBar->currentTab()->setFocus(Qt::OtherFocusReason); + //d->TabBar->currentTab()->setFocus(Qt::OtherFocusReason); + d->dockManager()->dockFocusController()->setDockWidgetTabFocused(d->TabBar->currentTab()); } return; } diff --git a/src/DockAreaWidget.cpp b/src/DockAreaWidget.cpp index 01914aa..09fd8fd 100644 --- a/src/DockAreaWidget.cpp +++ b/src/DockAreaWidget.cpp @@ -1030,6 +1030,21 @@ void CDockAreaWidget::onDockWidgetFeaturesChanged() } +#ifdef Q_OS_WIN +//============================================================================ +bool CDockAreaWidget::event(QEvent *e) +{ + switch (e->type()) + { + case QEvent::PlatformSurface: return true; + default: + break; + } + + return Super::event(e); +} +#endif + } // namespace ads //--------------------------------------------------------------------------- diff --git a/src/DockAreaWidget.h b/src/DockAreaWidget.h index a702370..eaf290c 100644 --- a/src/DockAreaWidget.h +++ b/src/DockAreaWidget.h @@ -77,6 +77,17 @@ private Q_SLOTS: void reorderDockWidget(int fromIndex, int toIndex); protected: + +#ifdef Q_OS_WIN + /** + * Reimplements QWidget::event to handle QEvent::PlatformSurface + * This is here to fix issue #294 Tab refresh problem with a QGLWidget + * that exists since Qt version 5.12.7. So this function is here to + * work around a Qt issue. + */ + virtual bool event(QEvent *event) override; +#endif + /** * Inserts a dock widget into dock area. * All dockwidgets in the dock area tabified in a stacked layout with tabs. @@ -356,7 +367,8 @@ Q_SIGNALS: */ void viewToggled(bool Open); }; // class DockAreaWidget -} - // namespace ads +} // namespace ads + +Q_DECLARE_OPERATORS_FOR_FLAGS(ads::CDockAreaWidget::DockAreaFlags) //----------------------------------------------------------------------------- #endif // DockAreaWidgetH diff --git a/src/DockContainerWidget.cpp b/src/DockContainerWidget.cpp index 0868c86..afd2b9c 100644 --- a/src/DockContainerWidget.cpp +++ b/src/DockContainerWidget.cpp @@ -1565,8 +1565,9 @@ void CDockContainerWidget::dropFloatingWidget(CFloatingDockContainer* FloatingWi } if (Dropped) - { - FloatingWidget->deleteLater(); + { + // Fix https://github.com/githubuser0xFFFF/Qt-Advanced-Docking-System/issues/351 + FloatingWidget->hideAndDeleteLater(); // If we dropped a floating widget with only one single dock widget, then we // drop a top level widget that changes from floating to docked now @@ -1679,7 +1680,10 @@ bool CDockContainerWidget::restoreState(CDockingStateReader& s, bool Testing) if (!Testing) { CFloatingDockContainer* FloatingWidget = floatingWidget(); - FloatingWidget->restoreGeometry(Geometry); + if (FloatingWidget) + { + FloatingWidget->restoreGeometry(Geometry); + } } } diff --git a/src/DockFocusController.cpp b/src/DockFocusController.cpp index c36f91b..595c9fa 100644 --- a/src/DockFocusController.cpp +++ b/src/DockFocusController.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include "DockWidget.h" #include "DockAreaWidget.h" @@ -31,6 +32,8 @@ namespace ads { +static const char* const FocusedDockWidgetProperty = "FocusedDockWidget"; + /** * Private data class of CDockFocusController class (pimpl) */ @@ -56,8 +59,8 @@ struct DockFocusControllerPrivate * the dock area that it belongs to */ void updateDockWidgetFocus(CDockWidget* DockWidget); -}; -// struct DockFocusControllerPrivate +}; // struct DockFocusControllerPrivate + //=========================================================================== @@ -115,6 +118,17 @@ void DockFocusControllerPrivate::updateDockWidgetFocus(CDockWidget* DockWidget) return; } + QWindow* Window = nullptr; + auto DockContainer = DockWidget->dockContainer(); + if (DockContainer) + { + Window = DockContainer->window()->windowHandle(); + } + + if (Window) + { + Window->setProperty(FocusedDockWidgetProperty, QVariant::fromValue(QPointer(DockWidget))); + } CDockAreaWidget* NewFocusedDockArea = nullptr; if (FocusedDockWidget) { @@ -139,31 +153,36 @@ void DockFocusControllerPrivate::updateDockWidgetFocus(CDockWidget* DockWidget) } - auto NewFloatingWidget = FocusedDockWidget->dockContainer()->floatingWidget(); + + CFloatingDockContainer* NewFloatingWidget = nullptr; + DockContainer = FocusedDockWidget->dockContainer(); + if (DockContainer) + { + NewFloatingWidget = DockContainer->floatingWidget(); + } + if (NewFloatingWidget) { - NewFloatingWidget->setProperty("FocusedDockWidget", QVariant::fromValue(DockWidget)); + NewFloatingWidget->setProperty(FocusedDockWidgetProperty, QVariant::fromValue(QPointer(DockWidget))); } #ifdef Q_OS_LINUX // This code is required for styling the floating widget titlebar for linux // depending on the current focus state - if (FloatingWidget == NewFloatingWidget) - { - return; - } + if (FloatingWidget != NewFloatingWidget) + { + if (FloatingWidget) + { + updateFloatingWidgetFocusStyle(FloatingWidget, false); + } + FloatingWidget = NewFloatingWidget; - if (FloatingWidget) - { - updateFloatingWidgetFocusStyle(FloatingWidget, false); - } - FloatingWidget = NewFloatingWidget; - - if (FloatingWidget) - { - updateFloatingWidgetFocusStyle(FloatingWidget, true); - } + if (FloatingWidget) + { + updateFloatingWidgetFocusStyle(FloatingWidget, true); + } + } #endif if (old == DockWidget && !ForceFocusChangedSignal) @@ -206,6 +225,8 @@ CDockFocusController::CDockFocusController(CDockManager* DockManager) : d->DockManager = DockManager; connect(QApplication::instance(), SIGNAL(focusChanged(QWidget*, QWidget*)), this, SLOT(onApplicationFocusChanged(QWidget*, QWidget*))); + connect(QApplication::instance(), SIGNAL(focusWindowChanged(QWindow*)), + this, SLOT(onFocusWindowChanged(QWindow*))); connect(d->DockManager, SIGNAL(stateRestored()), SLOT(onStateRestored())); } @@ -216,9 +237,35 @@ CDockFocusController::~CDockFocusController() } +//============================================================================ +void CDockFocusController::onFocusWindowChanged(QWindow *focusWindow) +{ + if (!focusWindow) + { + return; + } + + auto vDockWidget = focusWindow->property(FocusedDockWidgetProperty); + if (!vDockWidget.isValid()) + { + return; + } + + auto DockWidget = vDockWidget.value>(); + if (!DockWidget) + { + return; + } + + d->updateDockWidgetFocus(DockWidget); +} + + //=========================================================================== void CDockFocusController::onApplicationFocusChanged(QWidget* focusedOld, QWidget* focusedNow) { + Q_UNUSED(focusedOld); + if (d->DockManager->isRestoringState()) { return; @@ -231,47 +278,7 @@ void CDockFocusController::onApplicationFocusChanged(QWidget* focusedOld, QWidge return; } - // If the close button in another tab steals the focus from the current - // active dock widget content, i.e. if the user clicks its close button, - // then we immediately give the focus back to the previous focused widget - // focusedOld - if (CDockManager::testConfigFlag(CDockManager::AllTabsHaveCloseButton)) - { - auto OtherDockWidgetTab = internal::findParent(focusedNow); - if (OtherDockWidgetTab && focusedOld) - { - auto OldFocusedDockWidget = internal::findParent(focusedOld); - if (OldFocusedDockWidget) - { - focusedOld->setFocus(); - } - return; - } - } - - CDockWidget* DockWidget = nullptr; - auto DockWidgetTab = qobject_cast(focusedNow); - if (DockWidgetTab) - { - DockWidget = DockWidgetTab->dockWidget(); - // If the DockWidgetTab "steals" the focus from a widget in the same - // DockWidget, then we immediately give the focus back to the previous - // focused widget focusedOld - if (focusedOld) - { - auto OldFocusedDockWidget = internal::findParent(focusedOld); - if (OldFocusedDockWidget && OldFocusedDockWidget == DockWidget) - { - focusedOld->setFocus(); - } - } - } - - if (!DockWidget) - { - DockWidget = qobject_cast(focusedNow); - } - + CDockWidget* DockWidget = qobject_cast(focusedNow); if (!DockWidget) { DockWidget = internal::findParent(focusedNow); @@ -293,6 +300,17 @@ void CDockFocusController::onApplicationFocusChanged(QWidget* focusedOld, QWidge } +//=========================================================================== +void CDockFocusController::setDockWidgetTabFocused(CDockWidgetTab* Tab) +{ + auto DockWidget = Tab->dockWidget(); + if (DockWidget) + { + d->updateDockWidgetFocus(DockWidget); + } +} + + //=========================================================================== void CDockFocusController::setDockWidgetFocused(CDockWidget* focusedNow) { @@ -320,7 +338,7 @@ void CDockFocusController::onFocusedDockAreaViewToggled(bool Open) return; } - CDockManager::setWidgetFocus(OpenedDockAreas[0]->currentDockWidget()->tabWidget()); + d->updateDockWidgetFocus(OpenedDockAreas[0]->currentDockWidget()); } @@ -348,7 +366,7 @@ void CDockFocusController::notifyWidgetOrAreaRelocation(QWidget* DroppedWidget) } d->ForceFocusChangedSignal = true; - CDockManager::setWidgetFocus(DockWidget->tabWidget()); + CDockManager::setWidgetFocus(DockWidget); } @@ -360,18 +378,17 @@ void CDockFocusController::notifyFloatingWidgetDrop(CFloatingDockContainer* Floa return; } - auto vDockWidget = FloatingWidget->property("FocusedDockWidget"); + auto vDockWidget = FloatingWidget->property(FocusedDockWidgetProperty); if (!vDockWidget.isValid()) { return; } - auto DockWidget = vDockWidget.value(); + auto DockWidget = vDockWidget.value>(); if (DockWidget) { - d->FocusedDockWidget = nullptr; DockWidget->dockAreaWidget()->setCurrentDockWidget(DockWidget); - CDockManager::setWidgetFocus(DockWidget->tabWidget()); + CDockManager::setWidgetFocus(DockWidget); } } diff --git a/src/DockFocusController.h b/src/DockFocusController.h index 0a354ef..b080f3b 100644 --- a/src/DockFocusController.h +++ b/src/DockFocusController.h @@ -32,6 +32,7 @@ private: private Q_SLOTS: void onApplicationFocusChanged(QWidget *old, QWidget *now); + void onFocusWindowChanged(QWindow *focusWindow); void onFocusedDockAreaViewToggled(bool Open); void onStateRestored(); void onDockWidgetVisibilityChanged(bool Visible); @@ -48,21 +49,6 @@ public: */ virtual ~CDockFocusController(); - /** - * Helper function to set focus depending on the configuration of the - * FocusStyling flag - */ - template - static void setWidgetFocus(QWidgetPtr widget) - { - if (!CDockManager::testConfigFlag(CDockManager::FocusHighlighting)) - { - return; - } - - widget->setFocus(Qt::OtherFocusReason); - } - /** * A container needs to call this function if a widget has been dropped * into it @@ -83,6 +69,12 @@ public: */ CDockWidget* focusedDockWidget() const; + /** + * Request focus highlighting for the given dock widget assigned to the tab + * given in Tab parameter + */ + void setDockWidgetTabFocused(CDockWidgetTab* Tab); + public Q_SLOTS: /** * Request a focus change to the given dock widget diff --git a/src/DockManager.cpp b/src/DockManager.cpp index 42f61b8..9df3a5a 100644 --- a/src/DockManager.cpp +++ b/src/DockManager.cpp @@ -1140,6 +1140,13 @@ void CDockManager::setSplitterSizes(CDockAreaWidget *ContainedArea, const QList< } } + +//=========================================================================== +CDockFocusController* CDockManager::dockFocusController() const +{ + return d->FocusController; +} + } // namespace ads //--------------------------------------------------------------------------- diff --git a/src/DockManager.h b/src/DockManager.h index 390e283..9ebf8a2 100644 --- a/src/DockManager.h +++ b/src/DockManager.h @@ -53,6 +53,7 @@ struct DockWidgetTabPrivate; struct DockAreaWidgetPrivate; class CIconProvider; class CDockComponentsFactory; +class CDockFocusController; /** * The central dock manager that maintains the complete docking system. @@ -134,12 +135,18 @@ protected: */ void notifyFloatingWidgetDrop(CFloatingDockContainer* FloatingWidget); - /** * Show the floating widgets that has been created floating */ virtual void showEvent(QShowEvent *event) override; + /** + * Acces for the internal dock focus controller. + * This function only returns a valid object, if the FocusHighlighting + * flag is set. + */ + CDockFocusController* dockFocusController() const; + public: using Super = CDockContainerWidget; @@ -188,6 +195,7 @@ public: FloatingContainerForceQWidgetTitleBar = 0x1000000,//!< Linux only ! Forces all FloatingContainer to use a QWidget based title bar. //!< If neither this nor FloatingContainerForceNativeTitleBar is set (the default) native titlebars are used except on known bad systems. //! Users can overwrite this by setting the environment variable ADS_UseNativeTitle to "1" or "0". + MiddleMouseButtonClosesTab = 0x2000000, //! If the flag is set, the user can use the mouse middle button to close the tab under the mouse DefaultDockAreaButtons = DockAreaHasCloseButton | DockAreaHasUndockButton @@ -609,5 +617,7 @@ Q_SIGNALS: void focusedDockWidgetChanged(ads::CDockWidget* old, ads::CDockWidget* now); }; // class DockManager } // namespace ads + +Q_DECLARE_OPERATORS_FOR_FLAGS(ads::CDockManager::ConfigFlags) //----------------------------------------------------------------------------- #endif // DockManagerH diff --git a/src/DockOverlay.cpp b/src/DockOverlay.cpp index 5fd92b3..4b65373 100644 --- a/src/DockOverlay.cpp +++ b/src/DockOverlay.cpp @@ -806,7 +806,7 @@ void CDockOverlayCross::setIconColors(const QString& Colors) {"Arrow", CDockOverlayCross::ArrowColor}, {"Shadow", CDockOverlayCross::ShadowColor}}; -#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) +#if (QT_VERSION < QT_VERSION_CHECK(5, 14, 0)) auto SkipEmptyParts = QString::SkipEmptyParts; #else auto SkipEmptyParts = Qt::SkipEmptyParts; diff --git a/src/DockWidget.cpp b/src/DockWidget.cpp index 1d251a0..1d4515c 100644 --- a/src/DockWidget.cpp +++ b/src/DockWidget.cpp @@ -66,6 +66,12 @@ namespace ads */ struct DockWidgetPrivate { + struct WidgetFactory + { + CDockWidget::FactoryFunc createWidget; + CDockWidget::eInsertMode insertMode; + }; + CDockWidget* _this = nullptr; QBoxLayout* Layout = nullptr; QWidget* Widget = nullptr; @@ -84,7 +90,8 @@ struct DockWidgetPrivate bool IsFloatingTopLevel = false; QList TitleBarActions; CDockWidget::eMinimumSizeHintMode MinimumSizeHintMode = CDockWidget::MinimumSizeHintFromDockWidget; - + WidgetFactory* Factory = nullptr; + /** * Private data constructor */ @@ -116,6 +123,12 @@ struct DockWidgetPrivate * Setup the main scroll area */ void setupScrollArea(); + + /** + * Creates the content widget with the registered widget factory and + * returns true on success. + */ + bool createWidgetFromFactory(); }; // struct DockWidgetPrivate @@ -130,10 +143,23 @@ DockWidgetPrivate::DockWidgetPrivate(CDockWidget* _public) : //============================================================================ void DockWidgetPrivate::showDockWidget() { + if (!Widget) + { + if (!createWidgetFromFactory()) + { + Q_ASSERT(!Features.testFlag(CDockWidget::DeleteContentOnClose) + && "DeleteContentOnClose flag was set, but the widget " + "factory is missing or it doesn't return a valid QWidget."); + return; + } + } + if (!DockArea) { CFloatingDockContainer* FloatingWidget = new CFloatingDockContainer(_this); - FloatingWidget->resize(_this->size()); + // We use the size hint of the content widget to provide a good + // initial size + FloatingWidget->resize(Widget ? Widget->sizeHint() : _this->sizeHint()); TabWidget->show(); FloatingWidget->show(); } @@ -165,6 +191,12 @@ void DockWidgetPrivate::hideDockWidget() { TabWidget->hide(); updateParentDockArea(); + + if (Features.testFlag(CDockWidget::DeleteContentOnClose)) + { + Widget->deleteLater(); + Widget = nullptr; + } } @@ -218,6 +250,30 @@ void DockWidgetPrivate::setupScrollArea() } +//============================================================================ +bool DockWidgetPrivate::createWidgetFromFactory() +{ + if (!Features.testFlag(CDockWidget::DeleteContentOnClose)) + { + return false; + } + + if (!Factory) + { + return false; + } + + QWidget* w = Factory->createWidget(_this); + if (!w) + { + return false; + } + + _this->setWidget(w, Factory->insertMode); + return true; +} + + //============================================================================ CDockWidget::CDockWidget(const QString &title, QWidget *parent) : QFrame(parent), @@ -288,6 +344,17 @@ void CDockWidget::setWidget(QWidget* widget, eInsertMode InsertMode) d->Widget->setProperty("dockWidgetContent", true); } +//============================================================================ +void CDockWidget::setWidgetFactory(FactoryFunc createWidget, eInsertMode insertMode) +{ + if (d->Factory) + { + delete d->Factory; + } + + d->Factory = new DockWidgetPrivate::WidgetFactory { createWidget, insertMode }; +} + //============================================================================ QWidget* CDockWidget::takeWidget() @@ -530,7 +597,8 @@ void CDockWidget::toggleViewInternal(bool Open) CDockWidget* TopLevelDockWidgetAfter = DockContainer ? DockContainer->topLevelDockWidget() : nullptr; CDockWidget::emitTopLevelEventForWidget(TopLevelDockWidgetAfter, true); - CFloatingDockContainer* FloatingContainer = DockContainer->floatingWidget(); + CFloatingDockContainer* FloatingContainer = DockContainer + ? DockContainer->floatingWidget() : nullptr; if (FloatingContainer) { FloatingContainer->updateWindowTitle(); diff --git a/src/DockWidget.h b/src/DockWidget.h index 4d2328e..c20028b 100644 --- a/src/DockWidget.h +++ b/src/DockWidget.h @@ -147,18 +147,19 @@ public: enum DockWidgetFeature { - DockWidgetClosable = 0x01,///< dock widget has a close button - DockWidgetMovable = 0x02,///< dock widget is movable and can be moved to a new position in the current dock container - DockWidgetFloatable = 0x04,///< dock widget can be dragged into a floating window - DockWidgetDeleteOnClose = 0x08, ///< deletes the dock widget when it is closed - CustomCloseHandling = 0x10, ///< clicking the close button will not close the dock widget but emits the closeRequested() signal instead - DockWidgetFocusable = 0x20, ///< if this is enabled, a dock widget can get focus highlighting - DockWidgetForceCloseWithArea = 0x40, ///< dock widget will be closed when the dock area hosting it is closed - NoTab = 0x80, ///< dock widget tab will never be shown if this flag is set + DockWidgetClosable = 0x001,///< dock widget has a close button + DockWidgetMovable = 0x002,///< dock widget is movable and can be moved to a new position in the current dock container + DockWidgetFloatable = 0x004,///< dock widget can be dragged into a floating window + DockWidgetDeleteOnClose = 0x008, ///< deletes the dock widget when it is closed + CustomCloseHandling = 0x010, ///< clicking the close button will not close the dock widget but emits the closeRequested() signal instead + DockWidgetFocusable = 0x020, ///< if this is enabled, a dock widget can get focus highlighting + DockWidgetForceCloseWithArea = 0x040, ///< dock widget will be closed when the dock area hosting it is closed + NoTab = 0x080, ///< dock widget tab will never be shown if this flag is set + DeleteContentOnClose = 0x100, ///< deletes only the contained widget on close, keeping the dock widget intact and in place. Attempts to rebuild the contents widget on show if there is a widget factory set. DefaultDockWidgetFeatures = DockWidgetClosable | DockWidgetMovable | DockWidgetFloatable | DockWidgetFocusable, AllDockWidgetFeatures = DefaultDockWidgetFeatures | DockWidgetDeleteOnClose | CustomCloseHandling, DockWidgetAlwaysCloseAndDelete = DockWidgetForceCloseWithArea | DockWidgetDeleteOnClose, - NoDockWidgetFeatures = 0x00 + NoDockWidgetFeatures = 0x000 }; Q_DECLARE_FLAGS(DockWidgetFeatures, DockWidgetFeature) @@ -254,7 +255,7 @@ public: /** * Sets the widget for the dock widget to widget. * The InsertMode defines how the widget is inserted into the dock widget. - * The content of a dock widget should be resizable do a very small size to + * The content of a dock widget should be resizable to a very small size to * prevent the dock widget from blocking the resizing. To ensure, that a * dock widget can be resized very well, it is better to insert the content+ * widget into a scroll area or to provide a widget that is already a scroll @@ -269,7 +270,19 @@ public: * provide the InsertMode ForceNoScrollArea */ void setWidget(QWidget* widget, eInsertMode InsertMode = AutoScrollArea); - + + /** + * Only used when the feature flag DeleteContentOnClose is set. + * Using the flag and setting a widget factory allows to free the resources + * of the widget of your application while retaining the position the next + * time you want to show your widget, unlike the flag DockWidgetDeleteOnClose + * which deletes the dock widget itself. Since we keep the dock widget, all + * regular features of ADS should work as normal, including saving and + * restoring the state of the docking system and using perspectives. + */ + using FactoryFunc = std::function; + void setWidgetFactory(FactoryFunc createWidget, eInsertMode InsertMode = AutoScrollArea); + /** * Remove the widget from the dock and give ownership back to the caller */ @@ -587,7 +600,8 @@ Q_SIGNALS: */ void featuresChanged(ads::CDockWidget::DockWidgetFeatures features); }; // class DockWidget -} - // namespace ads +} // namespace ads + +Q_DECLARE_OPERATORS_FOR_FLAGS(ads::CDockWidget::DockWidgetFeatures) //----------------------------------------------------------------------------- #endif // DockWidgetH diff --git a/src/DockWidgetTab.cpp b/src/DockWidgetTab.cpp index 2f1113f..ece821c 100644 --- a/src/DockWidgetTab.cpp +++ b/src/DockWidgetTab.cpp @@ -50,6 +50,7 @@ #include "DockOverlay.h" #include "DockManager.h" #include "IconProvider.h" +#include "DockFocusController.h" namespace ads @@ -207,6 +208,14 @@ struct DockWidgetTabPrivate IconLabel->setVisible(true); } + /** + * Convenience function for access to the dock manager dock focus controller + */ + CDockFocusController* focusController() const + { + return DockWidget->dockManager()->dockFocusController(); + } + }; // struct DockWidgetTabPrivate @@ -234,6 +243,7 @@ void DockWidgetTabPrivate::createLayout() CloseButton->setObjectName("tabCloseButton"); internal::setButtonIcon(CloseButton, QStyle::SP_TitleBarCloseButton, TabCloseIcon); CloseButton->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); + CloseButton->setFocusPolicy(Qt::NoFocus); updateCloseButtonSizePolicy(); internal::setToolTip(CloseButton, QObject::tr("Close Tab")); _this->connect(CloseButton, SIGNAL(clicked()), SIGNAL(closeRequested())); @@ -331,10 +341,11 @@ CDockWidgetTab::CDockWidgetTab(CDockWidget* DockWidget, QWidget *parent) : setAttribute(Qt::WA_NoMousePropagation, true); d->DockWidget = DockWidget; d->createLayout(); - if (CDockManager::testConfigFlag(CDockManager::FocusHighlighting)) + setFocusPolicy(Qt::NoFocus); + /*if (CDockManager::testConfigFlag(CDockManager::FocusHighlighting)) { setFocusPolicy(Qt::ClickFocus); - } + }*/ } //============================================================================ @@ -353,6 +364,10 @@ void CDockWidgetTab::mousePressEvent(QMouseEvent* ev) ev->accept(); d->saveDragStartMousePosition(internal::globalPositionOf(ev)); d->DragState = DraggingMousePressed; + if (CDockManager::testConfigFlag(CDockManager::FocusHighlighting)) + { + d->focusController()->setDockWidgetTabFocused(this); + } Q_EMIT clicked(); return; } @@ -377,16 +392,30 @@ void CDockWidgetTab::mouseReleaseEvent(QMouseEvent* ev) // End of tab moving, emit signal if (d->DockArea) { + ev->accept(); Q_EMIT moved(internal::globalPositionOf(ev)); } break; case DraggingFloatingWidget: + ev->accept(); d->FloatingWidget->finishDragging(); break; default:; // do nothing } + } + else if (ev->button() == Qt::MiddleButton) + { + if (CDockManager::testConfigFlag(CDockManager::MiddleMouseButtonClosesTab) && d->DockWidget->features().testFlag(CDockWidget::DockWidgetClosable)) + { + // Only attempt to close if the mouse is still + // on top of the widget, to allow the user to cancel. + if (rect().contains(mapFromGlobal(QCursor::pos()))) { + ev->accept(); + Q_EMIT closeRequested(); + } + } } Super::mouseReleaseEvent(ev); @@ -515,7 +544,8 @@ void CDockWidgetTab::setActiveTab(bool active) bool UpdateFocusStyle = false; if (active && !hasFocus()) { - setFocus(Qt::OtherFocusReason); + //setFocus(Qt::OtherFocusReason); + d->focusController()->setDockWidgetTabFocused(this); UpdateFocusStyle = true; } @@ -612,14 +642,18 @@ QString CDockWidgetTab::text() const //============================================================================ void CDockWidgetTab::mouseDoubleClickEvent(QMouseEvent *event) { - // If this is the last dock area in a dock container it does not make - // sense to move it to a new floating widget and leave this one - // empty - if ((!d->DockArea->dockContainer()->isFloating() || d->DockArea->dockWidgetsCount() > 1) - && d->DockWidget->features().testFlag(CDockWidget::DockWidgetFloatable)) + if (event->button() == Qt::LeftButton) { - d->saveDragStartMousePosition(internal::globalPositionOf(event)); - d->startFloating(DraggingInactive); + // If this is the last dock area in a dock container it does not make + // sense to move it to a new floating widget and leave this one + // empty + if ((!d->DockArea->dockContainer()->isFloating() || d->DockArea->dockWidgetsCount() > 1) + && d->DockWidget->features().testFlag(CDockWidget::DockWidgetFloatable)) + { + event->accept(); + d->saveDragStartMousePosition(internal::globalPositionOf(event)); + d->startFloating(DraggingInactive); + } } Super::mouseDoubleClickEvent(event); @@ -677,6 +711,9 @@ bool CDockWidgetTab::event(QEvent *e) { const auto text = toolTip(); d->TitleLabel->setToolTip(text); + if (d->IconLabel) { + d->IconLabel->setToolTip(text); + } } #endif return Super::event(e); diff --git a/src/ElidingLabel.cpp b/src/ElidingLabel.cpp index 1aab918..ac52122 100644 --- a/src/ElidingLabel.cpp +++ b/src/ElidingLabel.cpp @@ -163,8 +163,8 @@ void CElidingLabel::resizeEvent(QResizeEvent *event) //============================================================================ QSize CElidingLabel::minimumSizeHint() const { -#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) - bool HasPixmap = !pixmap().isNull(); +#if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)) + bool HasPixmap = !pixmap(Qt::ReturnByValue).isNull(); #else bool HasPixmap = (pixmap() != nullptr); #endif @@ -185,8 +185,8 @@ QSize CElidingLabel::minimumSizeHint() const //============================================================================ QSize CElidingLabel::sizeHint() const { -#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) - bool HasPixmap = !pixmap().isNull(); +#if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)) + bool HasPixmap = !pixmap(Qt::ReturnByValue).isNull(); #else bool HasPixmap = (pixmap() != nullptr); #endif diff --git a/src/FloatingDockContainer.cpp b/src/FloatingDockContainer.cpp index 69d4aa0..ceb308a 100644 --- a/src/FloatingDockContainer.cpp +++ b/src/FloatingDockContainer.cpp @@ -373,6 +373,7 @@ struct FloatingDockContainerPrivate CDockAreaWidget *SingleDockArea = nullptr; QPoint DragStartPos; bool Hiding = false; + bool AutoHideChildren = true; #ifdef Q_OS_LINUX QWidget* MouseEventHandler = nullptr; CFloatingWidgetTitleBar* TitleBar = nullptr; @@ -841,15 +842,18 @@ void CFloatingDockContainer::hideEvent(QHideEvent *event) return; } - d->Hiding = true; - for (auto DockArea : d->DockContainer->openedDockAreas()) + if ( d->AutoHideChildren ) { - for (auto DockWidget : DockArea->openedDockWidgets()) + d->Hiding = true; + for ( auto DockArea : d->DockContainer->openedDockAreas() ) { - DockWidget->toggleView(false); + for ( auto DockWidget : DockArea->openedDockWidgets() ) + { + DockWidget->toggleView( false ); + } } + d->Hiding = false; } - d->Hiding = false; } @@ -1035,6 +1039,18 @@ QList CFloatingDockContainer::dockWidgets() const return d->DockContainer->dockWidgets(); } +//============================================================================ +void CFloatingDockContainer::hideAndDeleteLater() +{ + // Widget has been redocked, so it must be hidden right way (see + // https://github.com/githubuser0xFFFF/Qt-Advanced-Docking-System/issues/351) + // but AutoHideChildren must be set to false because "this" still contains + // dock widgets that shall not be toggled hidden. + d->AutoHideChildren = false; + hide(); + deleteLater(); +} + //============================================================================ void CFloatingDockContainer::finishDragging() { diff --git a/src/FloatingDockContainer.h b/src/FloatingDockContainer.h index 559cf2f..c08155c 100644 --- a/src/FloatingDockContainer.h +++ b/src/FloatingDockContainer.h @@ -65,7 +65,7 @@ class CDockingStateReader; * This interface is used for opaque and non-opaque undocking. If opaque * undocking is used, the a real CFloatingDockContainer widget will be created */ -class IFloatingWidget +class ADS_EXPORT IFloatingWidget { public: virtual ~IFloatingWidget() = default; @@ -258,6 +258,11 @@ public: */ QList dockWidgets() const; + /** + * This function hides the floating bar instantely and delete it later. + */ + void hideAndDeleteLater(); + #ifdef Q_OS_LINUX /** * This is a function that responds to FloatingWidgetTitleBar::maximizeRequest() diff --git a/src/ads_globals.cpp b/src/ads_globals.cpp index 322046e..8eca159 100644 --- a/src/ads_globals.cpp +++ b/src/ads_globals.cpp @@ -39,13 +39,11 @@ #include "ads_globals.h" #ifdef Q_OS_LINUX -#include #include #include -#endif - - #include +#include +#endif namespace ads { @@ -57,10 +55,31 @@ static QString _window_manager; static QHash _xcb_atom_cache; +//============================================================================ + bool is_platform_x11() +{ + return QGuiApplication::platformName() == QLatin1String("xcb"); +} + + +//============================================================================ +xcb_connection_t* x11_connection() +{ + if (!qApp) + return nullptr; + QPlatformNativeInterface *native = qApp->platformNativeInterface(); + if (!native) + return nullptr; + + void *connection = native->nativeResourceForIntegration(QByteArray("connection")); + return reinterpret_cast(connection); +} + + //============================================================================ xcb_atom_t xcb_get_atom(const char *name) { - if (!QX11Info::isPlatformX11()) + if (!is_platform_x11()) { return XCB_ATOM_NONE; } @@ -69,7 +88,7 @@ xcb_atom_t xcb_get_atom(const char *name) { return _xcb_atom_cache[key]; } - xcb_connection_t *connection = QX11Info::connection(); + xcb_connection_t *connection = x11_connection(); xcb_intern_atom_cookie_t request = xcb_intern_atom(connection, 1, strlen(name), name); xcb_intern_atom_reply_t *reply = xcb_intern_atom_reply(connection, request, NULL); if (!reply) @@ -93,7 +112,7 @@ xcb_atom_t xcb_get_atom(const char *name) //============================================================================ void xcb_update_prop(bool set, WId window, const char *type, const char *prop, const char *prop2) { - auto connection = QX11Info::connection(); + auto connection = x11_connection(); xcb_atom_t type_atom = xcb_get_atom(type); xcb_atom_t prop_atom = xcb_get_atom(prop); xcb_client_message_event_t event; @@ -118,11 +137,11 @@ void xcb_update_prop(bool set, WId window, const char *type, const char *prop, c //============================================================================ xcb_get_property_reply_t* _xcb_get_props(WId window, const char *type, unsigned int atom_type) { - if (!QX11Info::isPlatformX11()) + if (!is_platform_x11()) { return nullptr; } - xcb_connection_t *connection = QX11Info::connection(); + xcb_connection_t *connection = x11_connection(); xcb_atom_t type_atom = xcb_get_atom(type); if (type_atom == XCB_ATOM_NONE) { @@ -191,7 +210,7 @@ bool xcb_dump_props(WId window, const char *type) QVector atoms; xcb_get_prop_list(window, type, atoms, XCB_ATOM_ATOM); qDebug() << "\n\n!!!" << type << " - " << atoms.length(); - xcb_connection_t *connection = QX11Info::connection(); + xcb_connection_t *connection = x11_connection(); for (auto atom : atoms) { auto foo = xcb_get_atom_name(connection, atom); @@ -206,7 +225,7 @@ bool xcb_dump_props(WId window, const char *type) //============================================================================ void xcb_add_prop(bool state, WId window, const char *type, const char *prop) { - if (!QX11Info::isPlatformX11()) + if (!is_platform_x11()) { return; } @@ -227,7 +246,7 @@ void xcb_add_prop(bool state, WId window, const char *type, const char *prop) { atoms.remove(index); } - xcb_connection_t *connection = QX11Info::connection(); + xcb_connection_t *connection = x11_connection(); xcb_change_property(connection, XCB_PROP_MODE_REPLACE, window, type_atom, XCB_ATOM_ATOM, 32, atoms.count(), atoms.constData()); xcb_flush(connection); } @@ -238,11 +257,11 @@ QString detectWindowManagerX11() { // Tries to detect the windowmanager via X11. // See: https://specifications.freedesktop.org/wm-spec/1.3/ar01s03.html#idm46018259946000 - if (!QX11Info::isPlatformX11()) + if (!is_platform_x11()) { return "UNKNOWN"; } - xcb_connection_t *connection = QX11Info::connection(); + xcb_connection_t *connection = x11_connection(); xcb_screen_t *first_screen = xcb_setup_roots_iterator (xcb_get_setup (connection)).data; if(!first_screen) { diff --git a/src/ads_globals.h b/src/ads_globals.h index 9705544..777bd82 100644 --- a/src/ads_globals.h +++ b/src/ads_globals.h @@ -308,5 +308,6 @@ void repolishStyle(QWidget* w, eRepolishChildOptions Options = RepolishIgnoreChi } // namespace internal } // namespace ads +Q_DECLARE_OPERATORS_FOR_FLAGS(ads::DockWidgetAreas) //--------------------------------------------------------------------------- #endif // ads_globalsH diff --git a/src/src.pro b/src/src.pro index 7144c6c..9f6d265 100644 --- a/src/src.pro +++ b/src/src.pro @@ -73,8 +73,8 @@ SOURCES += \ unix:!macx { HEADERS += linux/FloatingWidgetTitleBar.h SOURCES += linux/FloatingWidgetTitleBar.cpp -QT += x11extras LIBS += -lxcb +QT += gui-private } isEmpty(PREFIX){