|
1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2021 - 2022 Detlev Offenbach <detlev@die-offenbachs.de> |
|
4 # |
|
5 |
|
6 """ |
|
7 Module implementing a bar widget showing just icons. |
|
8 """ |
|
9 |
|
10 import contextlib |
|
11 |
|
12 from PyQt6.QtCore import pyqtSignal, pyqtSlot, Qt, QCoreApplication |
|
13 from PyQt6.QtGui import QColor, QIcon, QCursor, QPalette |
|
14 from PyQt6.QtWidgets import QWidget, QBoxLayout, QWIDGETSIZE_MAX, QMenu |
|
15 |
|
16 from EricWidgets.EricApplication import ericApp |
|
17 |
|
18 from .EricClickableLabel import EricClickableLabel |
|
19 |
|
20 import UI.PixmapCache |
|
21 |
|
22 |
|
23 class EricIconBar(QWidget): |
|
24 """ |
|
25 Class implementing a bar widget showing just icons. |
|
26 |
|
27 @signal currentChanged(index) emitted to indicate a change of the current |
|
28 index |
|
29 @signal currentClicked(index) emitted to indicate, that the current icon |
|
30 was clicked |
|
31 """ |
|
32 BarSizes = { |
|
33 # tuples with (icon size, border size, translated size string) |
|
34 "xs": ( |
|
35 16, 1, |
|
36 QCoreApplication.translate("EricIconBar", "extra small") |
|
37 ), |
|
38 "sm": ( |
|
39 22, 1, |
|
40 QCoreApplication.translate("EricIconBar", "small") |
|
41 ), |
|
42 "md": ( |
|
43 32, 2, |
|
44 QCoreApplication.translate("EricIconBar", "medium") |
|
45 ), |
|
46 "lg": ( |
|
47 48, 2, |
|
48 QCoreApplication.translate("EricIconBar", "large") |
|
49 ), |
|
50 "xl": ( |
|
51 64, 3, |
|
52 QCoreApplication.translate("EricIconBar", "extra large") |
|
53 ), |
|
54 "xxl": ( |
|
55 96, 3, |
|
56 QCoreApplication.translate("EricIconBar", "very large") |
|
57 ), |
|
58 } |
|
59 DefaultBarSize = "md" |
|
60 |
|
61 MoreLabelAspect = 36 / 96 |
|
62 |
|
63 MenuStyleSheetTemplate = ( |
|
64 "QMenu {{ background-color: {0}; " |
|
65 "selection-background-color: {1}; " |
|
66 "border: 1px solid; }}" |
|
67 ) |
|
68 WidgetStyleSheetTemplate = "QWidget {{ background-color: {0}; }}" |
|
69 LabelStyleSheetTemplate = "QLabel {{ background-color: {0}; }}" |
|
70 |
|
71 currentChanged = pyqtSignal(int) |
|
72 currentClicked = pyqtSignal(int) |
|
73 |
|
74 def __init__(self, orientation=Qt.Orientation.Horizontal, |
|
75 barSize=DefaultBarSize, parent=None): |
|
76 """ |
|
77 Constructor |
|
78 |
|
79 @param orientation orientation for the widget |
|
80 @type Qt.Orientation |
|
81 @param barSize size category for the bar (one of 'xs', 'sm', 'md', |
|
82 'lg', 'xl', 'xxl') |
|
83 @type str |
|
84 @param parent reference to the parent widget (defaults to None) |
|
85 @type QWidget (optional) |
|
86 """ |
|
87 super().__init__(parent) |
|
88 |
|
89 try: |
|
90 self.__barSize, self.__borderSize = ( |
|
91 EricIconBar.BarSizes[barSize][:2]) |
|
92 self.__barSizeKey = barSize |
|
93 except KeyError: |
|
94 self.__barSize, self.__borderSize = ( |
|
95 EricIconBar.BarSizes[EricIconBar.DefaultBarSize][:2]) |
|
96 self.__fixedHeightWidth = ( |
|
97 self.__barSize + 2 * self.__borderSize |
|
98 ) |
|
99 self.__minimumHeightWidth = int( |
|
100 self.__barSize * self.MoreLabelAspect) + 2 * self.__borderSize |
|
101 |
|
102 # set initial values |
|
103 self.__color = QColor("#008800") |
|
104 self.__orientation = Qt.Orientation.Horizontal |
|
105 self.__currentIndex = -1 |
|
106 self.__icons = [] |
|
107 |
|
108 # initialize with horizontal layout and change later if needed |
|
109 self.setAttribute(Qt.WidgetAttribute.WA_StyledBackground, True) |
|
110 self.setFixedHeight(self.__fixedHeightWidth) |
|
111 self.setMinimumWidth(self.__minimumHeightWidth) |
|
112 |
|
113 self.__layout = QBoxLayout(QBoxLayout.Direction.LeftToRight) |
|
114 self.__layout.setContentsMargins( |
|
115 self.__borderSize, self.__borderSize, |
|
116 self.__borderSize, self.__borderSize) |
|
117 |
|
118 self.__layout.addStretch() |
|
119 |
|
120 self.setLayout(self.__layout) |
|
121 |
|
122 if orientation != self.__orientation: |
|
123 self.setOrientation(orientation) |
|
124 |
|
125 self.setColor(self.__color) |
|
126 |
|
127 self.__createAndAddMoreLabel() |
|
128 |
|
129 self.__adjustIconLabels() |
|
130 |
|
131 def setOrientation(self, orientation): |
|
132 """ |
|
133 Public method to set the widget orientation. |
|
134 |
|
135 @param orientation orientation to be set |
|
136 @type Qt.Orientation |
|
137 """ |
|
138 # reset list widget size constraints |
|
139 self.setFixedSize(QWIDGETSIZE_MAX, QWIDGETSIZE_MAX) |
|
140 |
|
141 # remove the 'More' icon |
|
142 itm = self.__layout.takeAt(self.__layout.count() - 1) |
|
143 itm.widget().deleteLater() |
|
144 del itm |
|
145 |
|
146 if orientation == Qt.Orientation.Horizontal: |
|
147 self.setFixedHeight(self.__fixedHeightWidth) |
|
148 self.setMinimumWidth(self.__minimumHeightWidth) |
|
149 self.__layout.setDirection(QBoxLayout.Direction.LeftToRight) |
|
150 elif orientation == Qt.Orientation.Vertical: |
|
151 self.setFixedWidth(self.__fixedHeightWidth) |
|
152 self.setMinimumHeight(self.__minimumHeightWidth) |
|
153 self.__layout.setDirection(QBoxLayout.Direction.TopToBottom) |
|
154 |
|
155 self.__orientation = orientation |
|
156 |
|
157 self.__createAndAddMoreLabel() |
|
158 |
|
159 self.__adjustIconLabels() |
|
160 |
|
161 def orientation(self): |
|
162 """ |
|
163 Public method to get the orientation of the widget. |
|
164 |
|
165 @return orientation of the widget |
|
166 @rtype Qt.Orientation |
|
167 """ |
|
168 return self.__orientation |
|
169 |
|
170 def setBarSize(self, barSize): |
|
171 """ |
|
172 Public method to set the icon bar size. |
|
173 |
|
174 @param barSize size category for the bar (one of 'xs', 'sm', 'md', |
|
175 'lg', 'xl', 'xxl') |
|
176 @type str |
|
177 """ |
|
178 # remove the 'More' icon |
|
179 itm = self.__layout.takeAt(self.__layout.count() - 1) |
|
180 itm.widget().deleteLater() |
|
181 del itm |
|
182 |
|
183 self.__barSize, self.__borderSize = ( |
|
184 EricIconBar.BarSizes[barSize][:2]) |
|
185 self.__barSizeKey = barSize |
|
186 self.__fixedHeightWidth = ( |
|
187 self.__barSize + 2 * self.__borderSize |
|
188 ) |
|
189 self.__minimumHeightWidth = int( |
|
190 self.__barSize * self.MoreLabelAspect) + 2 * self.__borderSize |
|
191 |
|
192 if self.__orientation == Qt.Orientation.Horizontal: |
|
193 self.setFixedHeight(self.__fixedHeightWidth) |
|
194 self.setMinimumWidth(self.__minimumHeightWidth) |
|
195 elif self.__orientation == Qt.Orientation.Vertical: |
|
196 self.setFixedWidth(self.__fixedHeightWidth) |
|
197 self.setMinimumHeight(self.__minimumHeightWidth) |
|
198 |
|
199 self.__layout.setContentsMargins( |
|
200 self.__borderSize, self.__borderSize, |
|
201 self.__borderSize, self.__borderSize) |
|
202 |
|
203 for index, icon in enumerate(self.__icons): |
|
204 iconLabel = self.__layout.itemAt(index) |
|
205 if iconLabel: |
|
206 widget = iconLabel.widget() |
|
207 widget.setFixedSize(self.__barSize, self.__barSize) |
|
208 widget.setPixmap( |
|
209 icon.pixmap(self.__barSize, self.__barSize)) |
|
210 |
|
211 self.__createAndAddMoreLabel() |
|
212 |
|
213 self.__adjustIconLabels() |
|
214 |
|
215 def barSize(self): |
|
216 """ |
|
217 Public method to get the icon bar size. |
|
218 |
|
219 @return barSize size category for the bar (one of 'xs', 'sm', 'md', |
|
220 'lg', 'xl', 'xxl') |
|
221 @rtype str |
|
222 """ |
|
223 return self.__barSizeKey |
|
224 |
|
225 def setColor(self, color): |
|
226 """ |
|
227 Public method to set the color of the widget. |
|
228 |
|
229 @param color color of the widget |
|
230 @type QColor |
|
231 """ |
|
232 self.__color = color |
|
233 self.__highlightColor = color.darker() |
|
234 |
|
235 self.setStyleSheet( |
|
236 EricIconBar.WidgetStyleSheetTemplate.format(color.name())) |
|
237 |
|
238 label = self.__layout.itemAt(self.__currentIndex) |
|
239 if label: |
|
240 label.widget().setStyleSheet( |
|
241 EricIconBar.LabelStyleSheetTemplate |
|
242 .format(self.__highlightColor.name()) |
|
243 ) |
|
244 |
|
245 def color(self): |
|
246 """ |
|
247 Public method to return the current color. |
|
248 |
|
249 @return current color |
|
250 @rtype QColor |
|
251 """ |
|
252 return self.__color |
|
253 |
|
254 def __createIcon(self, icon, label=""): |
|
255 """ |
|
256 Private method to creat an icon label. |
|
257 |
|
258 @param icon reference to the icon |
|
259 @type QIcon |
|
260 @param label label text to be shown as a tooltip (defaults to "") |
|
261 @type str (optional) |
|
262 @return created and connected label |
|
263 @rtype EricClickableLabel |
|
264 """ |
|
265 iconLabel = EricClickableLabel(self) |
|
266 iconLabel.setFixedSize(self.__barSize, self.__barSize) |
|
267 iconLabel.setAlignment(Qt.AlignmentFlag.AlignCenter) |
|
268 iconLabel.setPixmap(icon.pixmap(self.__barSize, self.__barSize)) |
|
269 if label: |
|
270 iconLabel.setToolTip(label) |
|
271 |
|
272 iconLabel.clicked.connect(lambda: self.__iconClicked(iconLabel)) |
|
273 |
|
274 return iconLabel |
|
275 |
|
276 def __createAndAddMoreLabel(self): |
|
277 """ |
|
278 Private method to create the label to be shown for too many icons. |
|
279 """ |
|
280 self.__moreLabel = EricClickableLabel(self) |
|
281 self.__moreLabel.setAlignment(Qt.AlignmentFlag.AlignCenter) |
|
282 if self.__orientation == Qt.Orientation.Horizontal: |
|
283 self.__moreLabel.setFixedSize( |
|
284 int(self.__barSize * self.MoreLabelAspect), self.__barSize) |
|
285 self.__moreLabel.setPixmap( |
|
286 UI.PixmapCache.getIcon("sbDotsH96").pixmap( |
|
287 int(self.__barSize * self.MoreLabelAspect), self.__barSize |
|
288 ) |
|
289 ) |
|
290 else: |
|
291 self.__moreLabel.setFixedSize( |
|
292 self.__barSize, int(self.__barSize * self.MoreLabelAspect)) |
|
293 self.__moreLabel.setPixmap( |
|
294 UI.PixmapCache.getIcon("sbDotsV96").pixmap( |
|
295 self.__barSize, int(self.__barSize * self.MoreLabelAspect) |
|
296 ) |
|
297 ) |
|
298 |
|
299 self.__layout.addWidget(self.__moreLabel) |
|
300 |
|
301 self.__moreLabel.clicked.connect(self.__moreLabelClicked) |
|
302 |
|
303 def addIcon(self, icon, label=""): |
|
304 """ |
|
305 Public method to add an icon to the bar. |
|
306 |
|
307 @param icon reference to the icon |
|
308 @type QIcon |
|
309 @param label label text to be shown as a tooltip (defaults to "") |
|
310 @type str (optional) |
|
311 """ |
|
312 # the stretch item is always the last one |
|
313 self.insertIcon(self.count(), icon, label=label) |
|
314 |
|
315 def insertIcon(self, index, icon, label=""): |
|
316 """ |
|
317 Public method to insert an icon into the bar. |
|
318 |
|
319 @param index position to insert the icon at |
|
320 @type int |
|
321 @param icon reference to the icon |
|
322 @type QIcon |
|
323 @param label label text to be shown as a tooltip (defaults to "") |
|
324 @type str (optional) |
|
325 """ |
|
326 iconLabel = self.__createIcon(icon, label=label) |
|
327 self.__layout.insertWidget(index, iconLabel) |
|
328 self.__icons.insert(index, QIcon(icon)) |
|
329 |
|
330 if self.__currentIndex < 0: |
|
331 self.setCurrentIndex(index) |
|
332 elif index <= self.__currentIndex: |
|
333 self.setCurrentIndex(self.__currentIndex + 1) |
|
334 |
|
335 self.__adjustIconLabels() |
|
336 |
|
337 def removeIcon(self, index): |
|
338 """ |
|
339 Public method to remove an icon from the bar. |
|
340 |
|
341 @param index index of the icon to be removed |
|
342 @type int |
|
343 """ |
|
344 label = self.__layout.itemAt(index) |
|
345 if label: |
|
346 with contextlib.suppress(IndexError): |
|
347 del self.__icons[index] |
|
348 itm = self.__layout.takeAt(index) |
|
349 itm.widget().deleteLater() |
|
350 del itm |
|
351 |
|
352 if index == self.__currentIndex: |
|
353 self.setCurrentIndex(index) |
|
354 elif index < self.__currentIndex: |
|
355 self.setCurrentIndex(self.__currentIndex - 1) |
|
356 |
|
357 self.__adjustIconLabels() |
|
358 |
|
359 @pyqtSlot() |
|
360 def __iconClicked(self, label): |
|
361 """ |
|
362 Private slot to handle an icon been clicked. |
|
363 |
|
364 @param label reference to the clicked label |
|
365 @type EricClickableLabel |
|
366 """ |
|
367 index = self.__layout.indexOf(label) |
|
368 if index >= 0: |
|
369 if index == self.__currentIndex: |
|
370 self.currentClicked.emit(self.__currentIndex) |
|
371 else: |
|
372 self.setCurrentIndex(index) |
|
373 |
|
374 def setCurrentIndex(self, index): |
|
375 """ |
|
376 Public method to set the current index. |
|
377 |
|
378 @param index current index to be set |
|
379 @type int |
|
380 """ |
|
381 if index >= self.count(): |
|
382 index = -1 |
|
383 |
|
384 if index != self.__currentIndex and index >= 0: |
|
385 # reset style of previous current icon |
|
386 oldLabel = self.__layout.itemAt(self.__currentIndex) |
|
387 if oldLabel: |
|
388 widget = oldLabel.widget() |
|
389 if widget is not None: |
|
390 widget.setStyleSheet("") |
|
391 |
|
392 # set style of new current icon |
|
393 newLabel = self.__layout.itemAt(index) |
|
394 if newLabel: |
|
395 newLabel.widget().setStyleSheet( |
|
396 EricIconBar.LabelStyleSheetTemplate |
|
397 .format(self.__highlightColor.name()) |
|
398 ) |
|
399 |
|
400 self.__currentIndex = index |
|
401 self.currentChanged.emit(self.__currentIndex) |
|
402 |
|
403 def currentIndex(self): |
|
404 """ |
|
405 Public method to get the current index. |
|
406 |
|
407 @return current index |
|
408 @rtype int |
|
409 """ |
|
410 return self.__currentIndex |
|
411 |
|
412 def count(self): |
|
413 """ |
|
414 Public method to get the number of icon labels. |
|
415 |
|
416 @return number of icon labels |
|
417 @rtype int |
|
418 """ |
|
419 return len(self.__icons) |
|
420 |
|
421 def wheelEvent(self, evt): |
|
422 """ |
|
423 Protected method to handle a wheel event. |
|
424 |
|
425 @param evt reference to the wheel event |
|
426 @type QWheelEvent |
|
427 """ |
|
428 delta = evt.angleDelta().y() |
|
429 if delta > 0: |
|
430 self.previousIcon() |
|
431 else: |
|
432 self.nextIcon() |
|
433 |
|
434 @pyqtSlot() |
|
435 def previousIcon(self): |
|
436 """ |
|
437 Public slot to set the icon before the current one. |
|
438 """ |
|
439 index = self.__currentIndex - 1 |
|
440 if index < 0: |
|
441 # wrap around |
|
442 index = self.count() - 1 |
|
443 |
|
444 self.setCurrentIndex(index) |
|
445 |
|
446 @pyqtSlot() |
|
447 def nextIcon(self): |
|
448 """ |
|
449 Public slot to set the icon after the current one. |
|
450 """ |
|
451 index = self.__currentIndex + 1 |
|
452 if index == self.count(): |
|
453 # wrap around |
|
454 index = 0 |
|
455 |
|
456 self.setCurrentIndex(index) |
|
457 |
|
458 @pyqtSlot() |
|
459 def __moreLabelClicked(self): |
|
460 """ |
|
461 Private slot to handle a click onto the 'More' label. |
|
462 """ |
|
463 menu = QMenu(self) |
|
464 baseColor = ericApp().palette().color( |
|
465 QPalette.ColorRole.Base) |
|
466 highlightColor = ericApp().palette().color( |
|
467 QPalette.ColorRole.Highlight) |
|
468 menu.setStyleSheet( |
|
469 EricIconBar.MenuStyleSheetTemplate.format( |
|
470 baseColor.name(), highlightColor.name())) |
|
471 |
|
472 for index in range(self.count()): |
|
473 iconLabel = self.__layout.itemAt(index) |
|
474 if iconLabel: |
|
475 widget = iconLabel.widget() |
|
476 if not widget.isVisible(): |
|
477 act = menu.addAction(widget.toolTip()) |
|
478 act.setData(index) |
|
479 |
|
480 selectedAction = menu.exec(QCursor.pos()) |
|
481 if selectedAction is not None: |
|
482 index = selectedAction.data() |
|
483 if index >= 0: |
|
484 if index == self.__currentIndex: |
|
485 self.currentClicked.emit(self.__currentIndex) |
|
486 else: |
|
487 self.setCurrentIndex(index) |
|
488 |
|
489 def resizeEvent(self, evt): |
|
490 """ |
|
491 Protected method to handle resizing of the icon bar. |
|
492 |
|
493 @param evt reference to the event object |
|
494 @type QResizeEvent |
|
495 """ |
|
496 self.__adjustIconLabels() |
|
497 |
|
498 def __adjustIconLabels(self): |
|
499 """ |
|
500 Private method to adjust the visibility of the icon labels. |
|
501 """ |
|
502 size = ( |
|
503 self.width() |
|
504 if self.orientation() == Qt.Orientation.Horizontal else |
|
505 self.height() |
|
506 ) - 2 * self.__borderSize |
|
507 |
|
508 iconsSize = self.count() * self.__barSize |
|
509 |
|
510 if size < iconsSize: |
|
511 self.__moreLabel.show() |
|
512 iconsSize += int(self.__barSize * self.MoreLabelAspect) |
|
513 for index in range(self.count() - 1, -1, -1): |
|
514 iconLabel = self.__layout.itemAt(index) |
|
515 if iconLabel: |
|
516 if size < iconsSize: |
|
517 iconLabel.widget().hide() |
|
518 iconsSize -= self.__barSize |
|
519 else: |
|
520 iconLabel.widget().show() |
|
521 else: |
|
522 self.__moreLabel.hide() |
|
523 for index in range(self.count()): |
|
524 iconLabel = self.__layout.itemAt(index) |
|
525 if iconLabel: |
|
526 iconLabel.widget().show() |