eric7/EricWidgets/EricSideBar.py

branch
eric7
changeset 8358
144a6b854f70
parent 8356
68ec9c3d4de5
child 8366
2a9f5153c438
equal deleted inserted replaced
8357:a081458cc57b 8358:144a6b854f70
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2008 - 2021 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing a sidebar class.
8 """
9
10 import enum
11 import json
12
13 from PyQt6.QtCore import QEvent, QSize, Qt, QTimer
14 from PyQt6.QtWidgets import (
15 QTabBar, QWidget, QStackedWidget, QBoxLayout, QToolButton, QSizePolicy
16 )
17
18 from EricWidgets.EricApplication import ericApp
19
20 import UI.PixmapCache
21
22
23 class EricSideBarSide(enum.Enum):
24 """
25 Class defining the sidebar sides.
26 """
27 NORTH = 0
28 EAST = 1
29 SOUTH = 2
30 WEST = 3
31
32
33 class EricSideBar(QWidget):
34 """
35 Class implementing a sidebar with a widget area, that is hidden or shown,
36 if the current tab is clicked again.
37 """
38 Version = 2
39
40 def __init__(self, orientation=None, delay=200, parent=None):
41 """
42 Constructor
43
44 @param orientation orientation of the sidebar widget
45 @type EricSideBarSide
46 @param delay value for the expand/shrink delay in milliseconds
47 @type int
48 @param parent parent widget
49 @type QWidget
50 """
51 super().__init__(parent)
52
53 self.__tabBar = QTabBar()
54 self.__tabBar.setDrawBase(True)
55 self.__tabBar.setShape(QTabBar.Shape.RoundedNorth)
56 self.__tabBar.setUsesScrollButtons(True)
57 self.__tabBar.setDrawBase(False)
58 self.__stackedWidget = QStackedWidget(self)
59 self.__stackedWidget.setContentsMargins(0, 0, 0, 0)
60 self.__autoHideButton = QToolButton()
61 self.__autoHideButton.setCheckable(True)
62 self.__autoHideButton.setIcon(
63 UI.PixmapCache.getIcon("autoHideOff"))
64 self.__autoHideButton.setChecked(True)
65 self.__autoHideButton.setToolTip(
66 self.tr("Deselect to activate automatic collapsing"))
67 self.barLayout = QBoxLayout(QBoxLayout.Direction.LeftToRight)
68 self.barLayout.setContentsMargins(0, 0, 0, 0)
69 self.layout = QBoxLayout(QBoxLayout.Direction.TopToBottom)
70 self.layout.setContentsMargins(0, 0, 0, 0)
71 self.layout.setSpacing(0)
72 self.barLayout.addWidget(self.__autoHideButton)
73 self.barLayout.addWidget(self.__tabBar)
74 self.layout.addLayout(self.barLayout)
75 self.layout.addWidget(self.__stackedWidget)
76 self.setLayout(self.layout)
77
78 # initialize the delay timer
79 self.__actionMethod = None
80 self.__delayTimer = QTimer(self)
81 self.__delayTimer.setSingleShot(True)
82 self.__delayTimer.setInterval(delay)
83 self.__delayTimer.timeout.connect(self.__delayedAction)
84
85 self.__minimized = False
86 self.__minSize = 0
87 self.__maxSize = 0
88 self.__bigSize = QSize()
89
90 self.splitter = None
91 self.splitterSizes = []
92
93 self.__hasFocus = False
94 # flag storing if this widget or any child has the focus
95 self.__autoHide = False
96
97 self.__tabBar.installEventFilter(self)
98
99 self.__orientation = EricSideBarSide.NORTH
100 if orientation is None:
101 orientation = EricSideBarSide.NORTH
102 self.setOrientation(orientation)
103
104 self.__tabBar.currentChanged[int].connect(
105 self.__stackedWidget.setCurrentIndex)
106 ericApp().focusChanged.connect(self.__appFocusChanged)
107 self.__autoHideButton.toggled[bool].connect(self.__autoHideToggled)
108
109 def setSplitter(self, splitter):
110 """
111 Public method to set the splitter managing the sidebar.
112
113 @param splitter reference to the splitter (QSplitter)
114 """
115 self.splitter = splitter
116 self.splitter.splitterMoved.connect(self.__splitterMoved)
117 self.splitter.setChildrenCollapsible(False)
118 index = self.splitter.indexOf(self)
119 self.splitter.setCollapsible(index, False)
120
121 def __splitterMoved(self, pos, index):
122 """
123 Private slot to react on splitter moves.
124
125 @param pos new position of the splitter handle (integer)
126 @param index index of the splitter handle (integer)
127 """
128 if self.splitter:
129 self.splitterSizes = self.splitter.sizes()
130
131 def __delayedAction(self):
132 """
133 Private slot to handle the firing of the delay timer.
134 """
135 if self.__actionMethod is not None:
136 self.__actionMethod()
137
138 def setDelay(self, delay):
139 """
140 Public method to set the delay value for the expand/shrink delay in
141 milliseconds.
142
143 @param delay value for the expand/shrink delay in milliseconds
144 (integer)
145 """
146 self.__delayTimer.setInterval(delay)
147
148 def delay(self):
149 """
150 Public method to get the delay value for the expand/shrink delay in
151 milliseconds.
152
153 @return value for the expand/shrink delay in milliseconds (integer)
154 """
155 return self.__delayTimer.interval()
156
157 def __cancelDelayTimer(self):
158 """
159 Private method to cancel the current delay timer.
160 """
161 self.__delayTimer.stop()
162 self.__actionMethod = None
163
164 def shrink(self):
165 """
166 Public method to record a shrink request.
167 """
168 self.__delayTimer.stop()
169 self.__actionMethod = self.__shrinkIt
170 self.__delayTimer.start()
171
172 def __shrinkIt(self):
173 """
174 Private method to shrink the sidebar.
175 """
176 self.__minimized = True
177 self.__bigSize = self.size()
178 if self.__orientation in (EricSideBarSide.NORTH, EricSideBarSide.SOUTH):
179 self.__minSize = self.minimumSizeHint().height()
180 self.__maxSize = self.maximumHeight()
181 else:
182 self.__minSize = self.minimumSizeHint().width()
183 self.__maxSize = self.maximumWidth()
184 if self.splitter:
185 self.splitterSizes = self.splitter.sizes()
186
187 self.__stackedWidget.hide()
188
189 if self.__orientation in (EricSideBarSide.NORTH, EricSideBarSide.SOUTH):
190 self.setFixedHeight(self.__tabBar.minimumSizeHint().height())
191 else:
192 self.setFixedWidth(self.__tabBar.minimumSizeHint().width())
193
194 self.__actionMethod = None
195
196 def expand(self):
197 """
198 Public method to record a expand request.
199 """
200 self.__delayTimer.stop()
201 self.__actionMethod = self.__expandIt
202 self.__delayTimer.start()
203
204 def __expandIt(self):
205 """
206 Private method to expand the sidebar.
207 """
208 self.__minimized = False
209 self.__stackedWidget.show()
210 self.resize(self.__bigSize)
211 if self.__orientation in (EricSideBarSide.NORTH, EricSideBarSide.SOUTH):
212 minSize = max(self.__minSize, self.minimumSizeHint().height())
213 self.setMinimumHeight(minSize)
214 self.setMaximumHeight(self.__maxSize)
215 else:
216 minSize = max(self.__minSize, self.minimumSizeHint().width())
217 self.setMinimumWidth(minSize)
218 self.setMaximumWidth(self.__maxSize)
219 if self.splitter:
220 self.splitter.setSizes(self.splitterSizes)
221
222 self.__actionMethod = None
223
224 def isMinimized(self):
225 """
226 Public method to check the minimized state.
227
228 @return flag indicating the minimized state (boolean)
229 """
230 return self.__minimized
231
232 def isAutoHiding(self):
233 """
234 Public method to check, if the auto hide function is active.
235
236 @return flag indicating the state of auto hiding (boolean)
237 """
238 return self.__autoHide
239
240 def eventFilter(self, obj, evt):
241 """
242 Public method to handle some events for the tabbar.
243
244 @param obj reference to the object (QObject)
245 @param evt reference to the event object (QEvent)
246 @return flag indicating, if the event was handled (boolean)
247 """
248 if obj == self.__tabBar:
249 if evt.type() == QEvent.Type.MouseButtonPress:
250 pos = evt.position().toPoint()
251 for i in range(self.__tabBar.count()):
252 if self.__tabBar.tabRect(i).contains(pos):
253 break
254
255 if i == self.__tabBar.currentIndex():
256 if self.isMinimized():
257 self.expand()
258 else:
259 self.shrink()
260 return True
261 elif self.isMinimized():
262 self.expand()
263 elif evt.type() == QEvent.Type.Wheel:
264 delta = evt.angleDelta().y()
265 if delta > 0:
266 self.prevTab()
267 else:
268 self.nextTab()
269 return True
270
271 return QWidget.eventFilter(self, obj, evt)
272
273 def addTab(self, widget, iconOrLabel, label=None):
274 """
275 Public method to add a tab to the sidebar.
276
277 @param widget reference to the widget to add (QWidget)
278 @param iconOrLabel reference to the icon or the label text of the tab
279 (QIcon, string)
280 @param label the labeltext of the tab (string) (only to be
281 used, if the second parameter is a QIcon)
282 """
283 if label:
284 index = self.__tabBar.addTab(iconOrLabel, label)
285 self.__tabBar.setTabToolTip(index, label)
286 else:
287 index = self.__tabBar.addTab(iconOrLabel)
288 self.__tabBar.setTabToolTip(index, iconOrLabel)
289 self.__stackedWidget.addWidget(widget)
290 if self.__orientation in (EricSideBarSide.NORTH, EricSideBarSide.SOUTH):
291 self.__minSize = self.minimumSizeHint().height()
292 else:
293 self.__minSize = self.minimumSizeHint().width()
294
295 def insertTab(self, index, widget, iconOrLabel, label=None):
296 """
297 Public method to insert a tab into the sidebar.
298
299 @param index the index to insert the tab at (integer)
300 @param widget reference to the widget to insert (QWidget)
301 @param iconOrLabel reference to the icon or the labeltext of the tab
302 (QIcon, string)
303 @param label the labeltext of the tab (string) (only to be
304 used, if the second parameter is a QIcon)
305 """
306 if label:
307 index = self.__tabBar.insertTab(index, iconOrLabel, label)
308 self.__tabBar.setTabToolTip(index, label)
309 else:
310 index = self.__tabBar.insertTab(index, iconOrLabel)
311 self.__tabBar.setTabToolTip(index, iconOrLabel)
312 self.__stackedWidget.insertWidget(index, widget)
313 if self.__orientation in (EricSideBarSide.NORTH, EricSideBarSide.SOUTH):
314 self.__minSize = self.minimumSizeHint().height()
315 else:
316 self.__minSize = self.minimumSizeHint().width()
317
318 def removeTab(self, index):
319 """
320 Public method to remove a tab.
321
322 @param index the index of the tab to remove (integer)
323 """
324 self.__stackedWidget.removeWidget(self.__stackedWidget.widget(index))
325 self.__tabBar.removeTab(index)
326 if self.__orientation in (EricSideBarSide.NORTH, EricSideBarSide.SOUTH):
327 self.__minSize = self.minimumSizeHint().height()
328 else:
329 self.__minSize = self.minimumSizeHint().width()
330
331 def clear(self):
332 """
333 Public method to remove all tabs.
334 """
335 while self.count() > 0:
336 self.removeTab(0)
337
338 def prevTab(self):
339 """
340 Public slot used to show the previous tab.
341 """
342 ind = self.currentIndex() - 1
343 if ind == -1:
344 ind = self.count() - 1
345
346 self.setCurrentIndex(ind)
347 self.currentWidget().setFocus()
348
349 def nextTab(self):
350 """
351 Public slot used to show the next tab.
352 """
353 ind = self.currentIndex() + 1
354 if ind == self.count():
355 ind = 0
356
357 self.setCurrentIndex(ind)
358 self.currentWidget().setFocus()
359
360 def count(self):
361 """
362 Public method to get the number of tabs.
363
364 @return number of tabs in the sidebar (integer)
365 """
366 return self.__tabBar.count()
367
368 def currentIndex(self):
369 """
370 Public method to get the index of the current tab.
371
372 @return index of the current tab (integer)
373 """
374 return self.__stackedWidget.currentIndex()
375
376 def setCurrentIndex(self, index):
377 """
378 Public slot to set the current index.
379
380 @param index the index to set as the current index (integer)
381 """
382 self.__tabBar.setCurrentIndex(index)
383 self.__stackedWidget.setCurrentIndex(index)
384 if self.isMinimized():
385 self.expand()
386
387 def currentWidget(self):
388 """
389 Public method to get a reference to the current widget.
390
391 @return reference to the current widget (QWidget)
392 """
393 return self.__stackedWidget.currentWidget()
394
395 def setCurrentWidget(self, widget):
396 """
397 Public slot to set the current widget.
398
399 @param widget reference to the widget to become the current widget
400 (QWidget)
401 """
402 self.__stackedWidget.setCurrentWidget(widget)
403 self.__tabBar.setCurrentIndex(self.__stackedWidget.currentIndex())
404 if self.isMinimized():
405 self.expand()
406
407 def indexOf(self, widget):
408 """
409 Public method to get the index of the given widget.
410
411 @param widget reference to the widget to get the index of (QWidget)
412 @return index of the given widget (integer)
413 """
414 return self.__stackedWidget.indexOf(widget)
415
416 def isTabEnabled(self, index):
417 """
418 Public method to check, if a tab is enabled.
419
420 @param index index of the tab to check (integer)
421 @return flag indicating the enabled state (boolean)
422 """
423 return self.__tabBar.isTabEnabled(index)
424
425 def setTabEnabled(self, index, enabled):
426 """
427 Public method to set the enabled state of a tab.
428
429 @param index index of the tab to set (integer)
430 @param enabled enabled state to set (boolean)
431 """
432 self.__tabBar.setTabEnabled(index, enabled)
433
434 def orientation(self):
435 """
436 Public method to get the orientation of the sidebar.
437
438 @return orientation of the sidebar
439 @rtype EricSideBarSide
440 """
441 return self.__orientation
442
443 def setOrientation(self, orient):
444 """
445 Public method to set the orientation of the sidebar.
446
447 @param orient orientation of the sidebar
448 @type EricSideBarSide
449 """
450 if orient == EricSideBarSide.NORTH:
451 self.__tabBar.setShape(QTabBar.Shape.RoundedNorth)
452 self.__tabBar.setSizePolicy(
453 QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred)
454 self.barLayout.setDirection(QBoxLayout.Direction.LeftToRight)
455 self.layout.setDirection(QBoxLayout.Direction.TopToBottom)
456 self.layout.setAlignment(self.barLayout,
457 Qt.AlignmentFlag.AlignLeft)
458 elif orient == EricSideBarSide.EAST:
459 self.__tabBar.setShape(QTabBar.Shape.RoundedEast)
460 self.__tabBar.setSizePolicy(
461 QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Expanding)
462 self.barLayout.setDirection(QBoxLayout.Direction.TopToBottom)
463 self.layout.setDirection(QBoxLayout.Direction.RightToLeft)
464 self.layout.setAlignment(self.barLayout, Qt.AlignmentFlag.AlignTop)
465 elif orient == EricSideBarSide.SOUTH:
466 self.__tabBar.setShape(QTabBar.Shape.RoundedSouth)
467 self.__tabBar.setSizePolicy(
468 QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred)
469 self.barLayout.setDirection(QBoxLayout.Direction.LeftToRight)
470 self.layout.setDirection(QBoxLayout.Direction.BottomToTop)
471 self.layout.setAlignment(self.barLayout,
472 Qt.AlignmentFlag.AlignLeft)
473 elif orient == EricSideBarSide.WEST:
474 self.__tabBar.setShape(QTabBar.Shape.RoundedWest)
475 self.__tabBar.setSizePolicy(
476 QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Expanding)
477 self.barLayout.setDirection(QBoxLayout.Direction.TopToBottom)
478 self.layout.setDirection(QBoxLayout.Direction.LeftToRight)
479 self.layout.setAlignment(self.barLayout, Qt.AlignmentFlag.AlignTop)
480 self.__orientation = orient
481
482 def tabIcon(self, index):
483 """
484 Public method to get the icon of a tab.
485
486 @param index index of the tab (integer)
487 @return icon of the tab (QIcon)
488 """
489 return self.__tabBar.tabIcon(index)
490
491 def setTabIcon(self, index, icon):
492 """
493 Public method to set the icon of a tab.
494
495 @param index index of the tab (integer)
496 @param icon icon to be set (QIcon)
497 """
498 self.__tabBar.setTabIcon(index, icon)
499
500 def tabText(self, index):
501 """
502 Public method to get the text of a tab.
503
504 @param index index of the tab (integer)
505 @return text of the tab (string)
506 """
507 return self.__tabBar.tabText(index)
508
509 def setTabText(self, index, text):
510 """
511 Public method to set the text of a tab.
512
513 @param index index of the tab (integer)
514 @param text text to set (string)
515 """
516 self.__tabBar.setTabText(index, text)
517
518 def tabToolTip(self, index):
519 """
520 Public method to get the tooltip text of a tab.
521
522 @param index index of the tab (integer)
523 @return tooltip text of the tab (string)
524 """
525 return self.__tabBar.tabToolTip(index)
526
527 def setTabToolTip(self, index, tip):
528 """
529 Public method to set the tooltip text of a tab.
530
531 @param index index of the tab (integer)
532 @param tip tooltip text to set (string)
533 """
534 self.__tabBar.setTabToolTip(index, tip)
535
536 def tabWhatsThis(self, index):
537 """
538 Public method to get the WhatsThis text of a tab.
539
540 @param index index of the tab (integer)
541 @return WhatsThis text of the tab (string)
542 """
543 return self.__tabBar.tabWhatsThis(index)
544
545 def setTabWhatsThis(self, index, text):
546 """
547 Public method to set the WhatsThis text of a tab.
548
549 @param index index of the tab (integer)
550 @param text WhatsThis text to set (string)
551 """
552 self.__tabBar.setTabWhatsThis(index, text)
553
554 def widget(self, index):
555 """
556 Public method to get a reference to the widget associated with a tab.
557
558 @param index index of the tab (integer)
559 @return reference to the widget (QWidget)
560 """
561 return self.__stackedWidget.widget(index)
562
563 def saveState(self):
564 """
565 Public method to save the state of the sidebar.
566
567 @return saved state as a byte array (QByteArray)
568 """
569 if len(self.splitterSizes) == 0:
570 if self.splitter:
571 self.splitterSizes = self.splitter.sizes()
572 self.__bigSize = self.size()
573 if self.__orientation in (
574 EricSideBarSide.NORTH, EricSideBarSide.SOUTH
575 ):
576 self.__minSize = self.minimumSizeHint().height()
577 self.__maxSize = self.maximumHeight()
578 else:
579 self.__minSize = self.minimumSizeHint().width()
580 self.__maxSize = self.maximumWidth()
581
582 dataDict = {
583 "version": self.Version,
584 "minimized": self.__minimized,
585 "big_size": [self.__bigSize.width(), self.__bigSize.height()],
586 "min_size": self.__minSize,
587 "max_size": self.__maxSize,
588 "splitter_sizes": self.splitterSizes,
589 "auto_hide": self.__autoHide
590 }
591 data = json.dumps(dataDict)
592
593 return data
594
595 def restoreState(self, state):
596 """
597 Public method to restore the state of the sidebar.
598
599 @param state byte array containing the saved state (QByteArray)
600 @return flag indicating success (boolean)
601 """
602 if not isinstance(state, str) or state == "":
603 return False
604
605 try:
606 stateDict = json.loads(state)
607 except json.JSONDecodeError:
608 return False
609
610 if not stateDict:
611 return False
612
613 if self.__orientation in (EricSideBarSide.NORTH, EricSideBarSide.SOUTH):
614 minSize = self.layout.minimumSize().height()
615 maxSize = self.maximumHeight()
616 else:
617 minSize = self.layout.minimumSize().width()
618 maxSize = self.maximumWidth()
619
620 if stateDict["version"] == 2:
621 if stateDict["minimized"] and not self.__minimized:
622 self.shrink()
623
624 self.__bigSize = QSize(*stateDict["big_size"])
625 self.__minSize = max(stateDict["min_size"], minSize)
626 self.__maxSize = max(stateDict["max_size"], maxSize)
627 self.splitterSizes = stateDict["splitter_sizes"]
628
629 self.__autoHide = stateDict["auto_hide"]
630 self.__autoHideButton.setChecked(not self.__autoHide)
631
632 if not stateDict["minimized"]:
633 self.expand()
634
635 return True
636
637 return False
638
639 #######################################################################
640 ## methods below implement the autohide functionality
641 #######################################################################
642
643 def __autoHideToggled(self, checked):
644 """
645 Private slot to handle the toggling of the autohide button.
646
647 @param checked flag indicating the checked state of the button
648 (boolean)
649 """
650 self.__autoHide = not checked
651 if self.__autoHide:
652 self.__autoHideButton.setIcon(
653 UI.PixmapCache.getIcon("autoHideOn"))
654 else:
655 self.__autoHideButton.setIcon(
656 UI.PixmapCache.getIcon("autoHideOff"))
657
658 def __appFocusChanged(self, old, now):
659 """
660 Private slot to handle a change of the focus.
661
662 @param old reference to the widget, that lost focus (QWidget or None)
663 @param now reference to the widget having the focus (QWidget or None)
664 """
665 if isinstance(now, QWidget):
666 self.__hasFocus = self.isAncestorOf(now)
667 if (
668 self.__autoHide and
669 not self.__hasFocus and
670 not self.isMinimized()
671 ):
672 self.shrink()
673 elif self.__autoHide and self.__hasFocus and self.isMinimized():
674 self.expand()
675
676 def enterEvent(self, event):
677 """
678 Protected method to handle the mouse entering this widget.
679
680 @param event reference to the event (QEvent)
681 """
682 if self.__autoHide and self.isMinimized():
683 self.expand()
684 else:
685 self.__cancelDelayTimer()
686
687 def leaveEvent(self, event):
688 """
689 Protected method to handle the mouse leaving this widget.
690
691 @param event reference to the event (QEvent)
692 """
693 if self.__autoHide and not self.__hasFocus and not self.isMinimized():
694 self.shrink()
695 else:
696 self.__cancelDelayTimer()
697
698 def shutdown(self):
699 """
700 Public method to shut down the object.
701
702 This method does some preparations so the object can be deleted
703 properly. It disconnects from the focusChanged signal in order to
704 avoid trouble later on.
705 """
706 ericApp().focusChanged.disconnect(self.__appFocusChanged)

eric ide

mercurial