Hi,
I'm writing an application where a user can create multiple charts to plot data. For each chart dock he creates, there is a corresponding "settings" dock created. These "settings" docks are auto hidden in the bottom sidebar so it's not always displayed.
When the user has created his workspace, he can save it and be able to restore it later.
The problem I'm facing is, when a user wants to load a workspace with docks having the same name as the current ones, the auto-hidden one is not deleted.
Here is how to reproduce the bug:
Load the app.
Add a new chart named "chart1". It creates two docks named "chart1 (Chart)" and "chart1 (Controls)".
Save the workspace with any name.
Load the workspace. The regular dock "chart1 (Chart)" has been replaced by the new one, but the auto-hidden dock "chart1 (Controls)" hasn't, so we have now 2 docks "chart1 (Controls)" with the QWidget inside, even if the parent has been deleted.
I've only had this bug when adding the AutoHide feature, and if the dock "chart1 (Controls)" has been moved from auto-hidden to floating, it is deleted accordingly, so the bugs appears only on hidden docks.
Here is a minimal code to reproduce the bug:
import sys
import re
import json
import os
from functools import partial
from glob import glob
from os import makedirs, path, listdir
from PySide6.QtGui import QAction
import PySide6QtAds as QtAds
from PySide6.QtCore import (
QSettings,
QObject
)
from PySide6.QtWidgets import (
QApplication,
QMainWindow,
QInputDialog,
QMessageBox,
QDialog,
QLineEdit,
QVBoxLayout,
QLabel,
QWidget,
QMenu,
QDialogButtonBox,
)
class GManager(QObject):
def __init__(self, settings_file, panel_name):
self.panel_name = panel_name
if settings_file == "":
self.settings = {
"some_key": "some_default_setting"
}
else:
self.import_settings(settings_file)
self.chart = QWidget()
self.settings_window = QWidget()
def export_settings(self, workspace_name):
try:
if not os.path.exists(f"workspaces/{workspace_name}/vues/"):
os.makedirs(f"workspaces/{workspace_name}/vues/")
with open(f"workspaces/{workspace_name}/vues/{self.panel_name}.json", 'w') as file:
json.dump(self.settings, file)
return True
except Exception as e:
print(f"Erreur lors de l'exportation de la représentation vers le fichier JSON : {str(e)}")
return False
def import_settings(self, file_name):
try:
with open(file_name, 'r') as file:
representation_data = json.load(file)
self.settings = representation_data
return True
except Exception as e:
print(f"Erreur lors de l'importation de la représentation depuis le fichier JSON : {str(e)}")
return False
class PopupCreateGM(QDialog):
def __init__(self):
super().__init__()
layout = QVBoxLayout()
self.window_name = ""
name_label = QLabel("Dock label:")
layout.addWidget(name_label)
name_input = QLineEdit()
layout.addWidget(name_input)
button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
button_box.accepted.connect(lambda: self.accept_with_validation(name_input.text()))
button_box.rejected.connect(self.reject)
layout.addWidget(button_box)
self.setLayout(layout)
def accept_with_validation(self, window_name):
if not window_name:
QMessageBox.warning(self, "Error", "Please enter a name for the Dock.")
return
if not self.is_valid_filename(window_name):
invalid_chars = ''.join(re.findall(r'[^a-zA-Z0-9-_]', window_name))
QMessageBox.warning(self, "Error", f"The dock label contains invalid characters: {invalid_chars}")
return
self.window_name = window_name
super().accept()
def is_valid_filename(self, filename):
return re.match(r'^[a-zA-Z0-9_-]+$', filename) is not None
class MainWindow(QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
# AutoHide
QtAds.CDockManager.setAutoHideConfigFlag(QtAds.CDockManager.DefaultAutoHideConfig)
# Create the dock manager. Because the parent parameter is a QMainWindow
# the dock manager registers itself as the central widget.
self.dock_manager = QtAds.CDockManager(self)
self.chart_managers=[]
self.create_toolbar()
def create_toolbar(self):
menubar = self.menuBar()
save_workspace_action = QAction("Save Workspace", self)
save_workspace_action.triggered.connect(self.save_workspace)
self.load_workspace_action = QAction("Load Workspace", self)
load_workspace_menu = QMenu(self)
self.load_workspace_action.setMenu(load_workspace_menu)
load_workspace_menu.aboutToShow.connect(self.populate_workspace_menu)
create_chart_action = QAction("Create Chart", self)
create_chart_action.triggered.connect(self.add_chart_manager)
file_menu = menubar.addMenu("File")
file_menu.addAction(save_workspace_action)
file_menu.addAction(self.load_workspace_action)
chart_menu = menubar.addMenu("Chart")
chart_menu.addAction(create_chart_action)
def populate_workspace_menu(self):
"""
Populates the "Load Workspace" menu with existing workspaces.
"""
load_workspace_menu = self.load_workspace_action.menu()
load_workspace_menu.clear() # Clear previous entries
workspace_path = "workspaces/"
if path.exists(workspace_path):
workspaces = [name for name in listdir(workspace_path) if path.isdir(path.join(workspace_path, name))]
for workspace in workspaces:
action = load_workspace_menu.addAction(workspace)
action.triggered.connect(partial(self.restore_workspace, workspace))
def add_chart_manager(self, panel_name="", settings_file=""):
if settings_file == "":
dialog = PopupCreateGM()
if dialog.exec() != QDialog.Accepted:
return
panel_name = dialog.window_name
new_gm = GManager(settings_file, panel_name)
self.chart_managers.append(new_gm)
# Create docks
chart_dock = QtAds.CDockWidget(panel_name + " (Chart)")
chart_dock.setWidget(new_gm.chart)
self.dock_manager.addDockWidget(QtAds.DockWidgetArea.CenterDockWidgetArea, chart_dock)
settings_dock = QtAds.CDockWidget(panel_name + " (Settings)")
settings_dock.setWidget(
new_gm.settings_window
)
self.dock_manager.addAutoHideDockWidget(
QtAds.PySide6QtAds.ads.SideBarLocation.SideBarBottom,
settings_dock
)
chart_dock.setFeature(QtAds.CDockWidget.CustomCloseHandling, True)
settings_dock.setFeature(QtAds.CDockWidget.CustomCloseHandling, True)
chart_dock.setFeature(QtAds.CDockWidget.DockWidgetDeleteOnClose, True)
settings_dock.setFeature(QtAds.CDockWidget.DockWidgetDeleteOnClose, True)
chart_dock.closeRequested.connect(lambda manager=new_gm: self.on_dock_close_requested([
chart_dock,
settings_dock
],
manager)
)
settings_dock.closeRequested.connect(lambda manager=new_gm: self.on_dock_close_requested([
chart_dock,
settings_dock
],
manager)
)
def restore_workspace(self, workspace_name):
self.chart_managers = []
file_pattern = f"workspaces/{workspace_name}/vues/*.json"
file_list = glob(file_pattern)
for file_name in file_list:
try:
panel_name = file_name.split(f"workspaces/{workspace_name}/vues\\")[1].split(".json")[0]
self.add_chart_manager(panel_name=panel_name,settings_file=file_name)
except Exception as e:
print(f"An error occurred in restore_workspace: {str(e)}")
QMessageBox.critical(self, "Error", f"An error occurred in restore_workspace: {str(e)}")
# Load states
self.restore_workspace_state(workspace_name)
def save_workspace_state(self, workspace_name):
'''
Saves the dock manager state and the main window geometry.
'''
settings = QSettings("Settings.ini", QSettings.IniFormat)
settings.setValue(f"{workspace_name}/Geometry", self.saveGeometry())
settings.setValue(f"{workspace_name}/State", self.saveState())
settings.setValue(f"{workspace_name}/DockingState", self.dock_manager.saveState())
def restore_workspace_state(self, workspace_name):
'''
Restores the dock manager state
'''
try:
settings = QSettings("Settings.ini", QSettings.IniFormat)
geom = settings.value(f"{workspace_name}/Geometry")
if geom is not None:
self.restoreGeometry(geom)
state = settings.value(f"{workspace_name}/State")
if state is not None:
self.restoreState(state)
state = settings.value(f"{workspace_name}/DockingState")
if state is not None:
self.dock_manager.restoreState(state)
except Exception as e:
print(f"An error occurred in restore_workspace_state: {str(e)}")
QMessageBox.critical(self, "Error", f"An error occurred in restore_workspace_state: {str(e)}")
def save_workspace(self, workspace_name):
if not path.exists("workspaces"):
makedirs("workspaces")
workspace_name, ok = QInputDialog.getText(self, "Save perspective", "Enter unique name:")
if ok and workspace_name:
if not path.exists(f"workspaces/{workspace_name}"):
makedirs(f"workspaces/{workspace_name}/vues")
for i, graph_manager in enumerate(self.chart_managers):
graph_manager.export_settings(workspace_name)
# Save shapes
self.save_workspace_state(workspace_name)
QMessageBox.information(self, "Success", f"Workspace '{workspace_name}' created successfully!")
def on_dock_close_requested(self, list_of_docks, manager):
try:
result = QMessageBox.question(None, "Close Editor",
"Close this window? The associated window will also be deleted")
if result == QMessageBox.Yes:
for dock_widget in list_of_docks:
dock_widget.closeDockWidget()
del dock_widget
self.chart_managers.remove(manager)
except RuntimeError as e:
QMessageBox.critical(None, "Error", f"An error occurred in on_dock_close_requested: {str(e)}")
if __name__ == "__main__":
# pyQt app
app = QApplication(sys.argv)
app.setStyle('Fusion')
widget = MainWindow()
widget.show()
exit(app.exec())
Hi, I'm writing an application where a user can create multiple charts to plot data. For each chart dock he creates, there is a corresponding "settings" dock created. These "settings" docks are auto hidden in the bottom sidebar so it's not always displayed. When the user has created his workspace, he can save it and be able to restore it later. The problem I'm facing is, when a user wants to load a workspace with docks having the same name as the current ones, the auto-hidden one is not deleted. Here is how to reproduce the bug:
I've only had this bug when adding the AutoHide feature, and if the dock "chart1 (Controls)" has been moved from auto-hidden to floating, it is deleted accordingly, so the bugs appears only on hidden docks.
Here is a minimal code to reproduce the bug:
Thanks in advance.