Last active 1739128532

Revision 316cd2e5bef9545d69fab5f67a481ad5a48d73c4

FoldableDockWidget.py Raw
1# todo when creating the window, need to be able to set whether can resize height or not? e.g. image / chart
2# todo why is there a margin between bottom and top of widgets?
3# todo the QLabel should have a width that spans to multilines where necessary instead of just increasing width
4
5import sys
6from random import randint
7
8from PySide6.QtCore import QSize, Qt
9from PySide6.QtGui import QIcon, QMouseEvent
10from PySide6.QtWidgets import QDockWidget, QWidget, QToolButton, QStyle, QHBoxLayout, QStyleFactory, QSizePolicy, \
11 QApplication, QVBoxLayout, QLabel, QSlider, QMainWindow, QSizeGrip
12
13
14class FoldableDockWidget(QDockWidget):
15 """
16 A simple Qt Widget that adds a 'minimise' button that vertically reduces the dock widget to just the titlebar to
17 allow the dock widget to still take up minimal screen real estate
18 """
19
20 class TitleBarWidget(QWidget):
21 def __init__(self, title:str, parent:QWidget=None):
22 """
23 We create a custom title bar using QWidget as the base. The title bar has to be wrapped in another widget
24 so that its background can be styled easier, otherwise it's impossible to style
25 :param title: the title to appear in the title bar
26 """
27
28 super().__init__(parent)
29
30 # set the default icons
31 self._init_titlebar_icons()
32
33 # set up local properties for the custom title bar
34 self.title_label = QLabel(title)
35
36 # button for floating the dock widget
37 self.float_button = QToolButton()
38
39 # button for "folding" the dock widget into just the title bar
40 self.minimize_button = QToolButton()
41
42 # button for closing the dock widget
43 self.close_button = QToolButton()
44
45 self.float_button.setIcon(self.icon_float)
46 self.minimize_button.setIcon(self.icon_minimize)
47 self.close_button.setIcon(self.icon_close)
48
49 # set fixed sizes for the icons
50 self.float_button.setIconSize(QSize(12, 12))
51 self.minimize_button.setIconSize(QSize(12, 12))
52 self.close_button.setIconSize(QSize(12, 12))
53
54 # we define the widget inner layout
55 titlebar_layout = QHBoxLayout()
56 titlebar_layout.setContentsMargins(1, 1, 1, 1) # L,T,R,B
57 titlebar_layout.setSpacing(0)
58 titlebar_layout.addWidget(self.title_label)
59 titlebar_layout.addWidget(self.float_button)
60 titlebar_layout.addWidget(self.minimize_button)
61 titlebar_layout.addWidget(self.close_button)
62
63 # define the inner widget
64 titlebar_widget_inner = QWidget(self) # the inner widget that houses the buttons
65 titlebar_widget_inner.setObjectName("titlebar") # use this #id to actually target the inner widget when styling
66 titlebar_widget_inner.setLayout(titlebar_layout)
67
68 # assign the inner widget to the layout of this class
69 wrapper_layout = QVBoxLayout()
70 wrapper_layout.addWidget(titlebar_widget_inner)
71 wrapper_layout.setSpacing(0)
72 wrapper_layout.setContentsMargins(0, 0, 0, 0)
73 self.setLayout(wrapper_layout)
74
75 def _init_titlebar_icons(self):
76 """
77 this can be overridden to custom style the title bar of the foldable dock widget
78 """
79 style = QStyleFactory.create("Fusion")
80 self.icon_minimize = style.standardIcon(QStyle.StandardPixmap.SP_TitleBarMinButton) # minimise icon
81 self.icon_restore = QIcon.fromTheme('view-restore') # restore after minimise icon
82 self.icon_dock = style.standardIcon(QStyle.StandardPixmap.SP_TitleBarShadeButton) # dock icon
83 self.icon_float = style.standardIcon(QStyle.StandardPixmap.SP_TitleBarUnshadeButton) # float icon
84 self.icon_close = style.standardIcon(QStyle.StandardPixmap.SP_DockWidgetCloseButton) # close the icon
85
86 def _set_float_icon(self, is_floating: bool):
87 """ set the float icon depending on the status of the parent dock widget """
88 if is_floating:
89 self.float_button.setIcon(self.icon_dock)
90 else:
91 self.float_button.setIcon(self.icon_float)
92
93 def _set_minimize_icon(self, is_minimized: bool):
94 """ set the minimised icon depending on the status of the parent dock widget """
95 if is_minimized:
96 self.minimize_button.setIcon(self.icon_restore) # SP_TitleBarRestoreButton))
97 else:
98 self.minimize_button.setIcon(self.icon_minimize)
99
100
101 def __init__(self, title:str, minimize_on_titlebar_double_click=True, show_size_grip=True, parent=None):
102 """
103 Initialise a Foldable Dock Widget
104
105 :param title: The title of the dock widget
106 :param minimize_on_titlebar_double_click: when the titlebar is double-clicked, do we want to toggle the minimise/fold status or toggle float/docked
107 :param parent: The parent widget
108 """
109 super().__init__(parent)
110
111 self.minimize_on_double_click = minimize_on_titlebar_double_click
112
113 self.setObjectName('foldable-dock-widget')
114 self.setWindowTitle(title)
115
116 self.title_bar = FoldableDockWidget.TitleBarWidget(title)
117 self.setTitleBarWidget(self.title_bar)
118
119 # a dirty hack to tell the evant listeners to ignore the visibility change as we're changing it manually
120 # when the user drags the dock widget from the dock area to elsewhere, this will fire, but will also fire when
121 # toggled between float/docked so we use this to
122 self.ignore_visibility_change = False
123
124 # is the widget "folded" or not?
125 self.is_minimized = False
126
127 self.show_size_grip = show_size_grip
128
129 # event listener to set icon when user drags the parent widget away from the docked region (floating) or back into the docked region
130 self.visibilityChanged.connect(self._on_visibility_change)
131
132 # handle the closing of the widget
133 self.title_bar.close_button.clicked.connect(lambda event: self.close()) # close the docked widget
134
135 # handle the double click of the titlebar using the mode type defined in this constructor
136 self.title_bar.mouseDoubleClickEvent = lambda event: self._handle_mouse_double_click()
137
138 # set up events to handle minimising / restoring and dock/undock on relevant button clicks
139 self.title_bar.float_button.clicked.connect(self._toggle_floating) # float / dock as necessary
140 self.title_bar.minimize_button.clicked.connect(self._toggle_minimize) # minimise / restore as necessary
141
142 def setWidget(self, widget:QWidget):
143 """
144 We need to set a custom size policy on the inside widget that's set as the body so that it can be folded and expanded as necessary
145 :param widget:
146 """
147
148 super().setWidget(widget)
149 widget.setObjectName('central-widget')
150
151 # todo some widgets should be expanding and others not - so I think this size policy would change depending on the flags set
152 sp = QSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
153 widget.setSizePolicy(sp)
154
155 # todo document that this helps resize much easier than default resizing where you have to place the cursor precisely
156 if self.show_size_grip:
157 self.size_grip = QSizeGrip(self)
158 widget.layout().addWidget(self.size_grip, alignment=Qt.AlignmentFlag.AlignBottom | Qt.AlignmentFlag.AlignRight)
159
160 # we style the whole widget only after the content of the widget has been assinged
161 self._set_styles()
162
163 def _handle_mouse_double_click(self):
164 """
165 we listen to double-clicks on the title bar widget and depending on how the widget has been defined, we toggle
166 either fold/expanded or float/docked
167 """
168 if self.minimize_on_double_click:
169 self._toggle_minimize()
170 else:
171 self._toggle_floating()
172
173 def _on_visibility_change(self):
174 """ triggered when the user drags the widget from the dock area to elsewhere, or vice versa """
175 self.title_bar._set_float_icon(not self.isFloating()) # we need to inverse because this fires before the widget has had its float/dock status changed
176 self.title_bar._set_minimize_icon(self.is_minimized)
177
178 def _toggle_floating(self):
179 """ float / dock the widget as necessary and toggle the floating/docked icon as necessary """
180
181 if self.ignore_visibility_change:
182 return
183
184 self.ignore_visibility_change = True
185
186 is_floating = self.isFloating()
187 self.setFloating(not is_floating) # toggle the float/docked status
188 self.title_bar._set_float_icon(is_floating) # set relevant icon
189 self.ignore_visibility_change = False
190
191 def _toggle_minimize(self):
192 if not self.is_minimized:
193 # "fold" the widget
194 current_width = self.widget().width()
195 self.widget().hide()
196 self.setMinimumWidth(current_width)
197 # self.widget().resize(current_width, 0) # todo nope, this didn't work but don't understand why not!
198 else:
199 # to expand the widget
200 self.resize(10, 10) # this resizes to the minimum size needed
201 self.widget().show()
202
203 self.title_bar._set_minimize_icon(self.is_minimized)
204
205 # invert the flag
206 self.is_minimized = not self.is_minimized
207
208 def _set_styles(self):
209 """
210 Set default styles to the foldable dock widget
211 """
212
213 # todo document that setting border: none on the base widget removes the ability to resize
214 self.setStyleSheet("""
215 FoldableDockWidget {
216 /* sets the background of the title bar widget and you also need to explicitly set the title bar background colour */
217 background-color: #aaa;
218 border: 2px solid #333;
219 border-bottom: 3px solid #222;
220 }
221 FoldableDockWidget #titlebar {
222 background-color: #ccc;
223 color: #d8dee9;
224 }
225 FoldableDockWidget #titlebar QLabel{
226 color: #333;
227 margin-right: 10px;
228 /*text-transform: uppercase;*/
229 font-weight:bold;
230 }
231 /*
232 FoldableDockWidget #titlebar QPushButton {
233 border: 1px solid red;
234 padding:0;
235 }
236 FoldableDockWidget #titlebar QPushButton::hover {
237 border: 1px solid red;
238 background-color: #eee;
239 }*/
240 FoldableDockWidget #central-widget {
241 background-color: #fff;
242 border: 1px solid #aaa;
243 margin:0;
244 }
245 """)
246
247
248# ----- sample implementation code below ----------
249if __name__ == "__main__":
250
251 class MainWindow(QMainWindow):
252 def __init__(self):
253 super(MainWindow, self).__init__()
254
255 self.dock_widget1 = self._create_dock_widget('really long dock widget 1', 'label for dock widget 1')
256 self.dock_widget2 = self._create_dock_widget('dock widget 2', 'label for dock widget 2')
257 self.dock_widget3 = self._create_dock_widget('dock widget 3', 'label for dock widget 3')
258 self.dock_widget4 = self._create_dock_widget('dock widget 4', 'label for dock widget 4')
259 self.dock_widget5 = self._create_dock_widget('dock widget 5', 'label for dock widget 5')
260
261 self.setCentralWidget(QWidget()) # set a dummy widget as the central widget so things can be docked properly
262 self.resize(800,800)
263
264 self.addDockWidget(Qt.DockWidgetArea.RightDockWidgetArea, self.dock_widget1)
265 self.addDockWidget(Qt.DockWidgetArea.RightDockWidgetArea, self.dock_widget2)
266 self.addDockWidget(Qt.DockWidgetArea.RightDockWidgetArea, self.dock_widget3)
267 self.addDockWidget(Qt.DockWidgetArea.RightDockWidgetArea, self.dock_widget4)
268 self.addDockWidget(Qt.DockWidgetArea.RightDockWidgetArea, self.dock_widget5)
269
270 def _create_dock_widget(self, title, text):
271 dw = FoldableDockWidget(title=f'{title}', minimize_on_titlebar_double_click=True, show_size_grip=True, parent=self)
272 w = QWidget() # layout widget
273
274 num_sliders = randint(2,5)
275
276 layout = QVBoxLayout()
277 layout.addWidget(QLabel(text))
278 for j in range(num_sliders):
279 layout.addWidget(QLabel(f'Slider {j}'))
280 layout.addWidget(QSlider(Qt.Horizontal))
281
282 w.setLayout(layout) # assign layout to widget
283 dw.setWidget(w) # assign widget with sliders and labels to dock widget
284
285 return dw
286# -----
287 app = QApplication(sys.argv)
288 window = MainWindow()
289 window.show()
290 sys.exit(app.exec())
291