0101100101 revised this gist . Go to revision
No changes
0101100101 revised this gist . Go to revision
1 file changed, 0 insertions, 0 deletions
FoldableDockWidget;py renamed to FoldableDockWidget.py
File renamed without changes
0101100101 revised this gist . Go to revision
1 file changed, 290 insertions
FoldableDockWidget;py(file created)
@@ -0,0 +1,290 @@ | |||
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 | + | ||
5 | + | import sys | |
6 | + | from random import randint | |
7 | + | ||
8 | + | from PySide6.QtCore import QSize, Qt | |
9 | + | from PySide6.QtGui import QIcon, QMouseEvent | |
10 | + | from PySide6.QtWidgets import QDockWidget, QWidget, QToolButton, QStyle, QHBoxLayout, QStyleFactory, QSizePolicy, \ | |
11 | + | QApplication, QVBoxLayout, QLabel, QSlider, QMainWindow, QSizeGrip | |
12 | + | ||
13 | + | ||
14 | + | class 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 ---------- | |
249 | + | if __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()) |