Last active 1739128532

0101100101 revised this gist 1739128532. Go to revision

No changes

0101100101 revised this gist 1739128513. Go to revision

1 file changed, 0 insertions, 0 deletions

FoldableDockWidget;py renamed to FoldableDockWidget.py

File renamed without changes

0101100101 revised this gist 1739128491. 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())
Newer Older