source: orange/Orange/OrangeCanvas/gui/toolbox.py @ 11246:c0308af21d80

Revision 11246:c0308af21d80, 15.5 KB checked in by Ales Erjavec <ales.erjavec@…>, 16 months ago (diff)

Fixed Toolbox exclusive tab state updates.

Line 
1"""
2==============
3ToolBox Widget
4==============
5
6A reimplementation of the QToolBox widget but with all the tabs
7in a single QScrollArea and support for multiple open tabs.
8
9"""
10
11from collections import namedtuple
12from operator import eq, attrgetter
13
14from PyQt4.QtGui import (
15    QWidget, QFrame, QSizePolicy, QIcon, QFontMetrics, QPainter, QStyle,
16    QStyleOptionToolButton, QStyleOptionToolBoxV2, QPalette, QBrush, QPen,
17    QLinearGradient, QColor, QScrollArea, QVBoxLayout, QToolButton,
18    QAction, QActionGroup
19)
20
21from PyQt4.QtCore import Qt, QObject, QSize, QRect, QPoint, QSignalMapper
22from PyQt4.QtCore import pyqtSignal as Signal, pyqtProperty as Property
23
24from .utils import brush_darker
25
26_ToolBoxPage = namedtuple(
27    "_ToolBoxPage",
28    ["index",
29     "widget",
30     "action",
31     "button"]
32    )
33
34
35FOCUS_OUTLINE_COLOR = "#609ED7"
36
37
38def create_tab_gradient(base_color):
39    """Create a default background gradient for a tab button from a single
40    color.
41
42    """
43    grad = QLinearGradient(0, 0, 0, 1)
44    grad.setStops([(0.0, base_color),
45                   (0.5, base_color),
46                   (0.8, base_color.darker(105)),
47                   (1.0, base_color.darker(110)),
48                   ])
49    grad.setCoordinateMode(QLinearGradient.ObjectBoundingMode)
50    return grad
51
52
53class ToolBoxTabButton(QToolButton):
54    """A tab button for an item in a ToolBox.
55    """
56
57    def setNativeStyling(self, state):
58        """Render tab buttons as native QToolButtons.
59        """
60        self.__nativeStyling = state
61        self.update()
62
63    def nativeStyling(self):
64        """Use native QStyle's QToolButton look.
65        """
66        return self.__nativeStyling
67
68    nativeStyling_ = Property(bool,
69                              fget=nativeStyling,
70                              fset=setNativeStyling,
71                              designable=True)
72
73    def __init__(self, *args, **kwargs):
74        self.__nativeStyling = False
75        self.position = QStyleOptionToolBoxV2.OnlyOneTab
76        self.selected = QStyleOptionToolBoxV2.NotAdjacent
77
78        QToolButton.__init__(self, *args, **kwargs)
79
80    def paintEvent(self, event):
81        if self.__nativeStyling:
82            QToolButton.paintEvent(self, event)
83        else:
84            self.__paintEventNoStyle()
85
86    def __paintEventNoStyle(self):
87        p = QPainter(self)
88        opt = QStyleOptionToolButton()
89        self.initStyleOption(opt)
90
91        fm = QFontMetrics(opt.font)
92        palette = opt.palette
93
94        # highlight brush is used as the background for the icon and background
95        # when the tab is expanded and as mouse hover color (lighter).
96        brush_highlight = palette.highlight()
97        if opt.state & QStyle.State_Sunken:
98            # State 'down' pressed during a mouse press (slightly darker).
99            background_brush = brush_darker(brush_highlight, 110)
100        elif opt.state & QStyle.State_MouseOver:
101            background_brush = brush_darker(brush_highlight, 95)
102        elif opt.state & QStyle.State_On:
103            background_brush = brush_highlight
104        else:
105            # The default button brush.
106            background_brush = palette.button()
107
108        rect = opt.rect
109        icon = opt.icon
110        icon_size = opt.iconSize
111
112        # TODO: add shift for pressed as set by the style (PM_ButtonShift...)
113
114        pm = None
115        if not icon.isNull():
116            if opt.state & QStyle.State_Enabled:
117                mode = QIcon.Normal
118            else:
119                mode = QIcon.Disabled
120
121            pm = opt.icon.pixmap(
122                    rect.size().boundedTo(icon_size), mode,
123                    QIcon.On if opt.state & QStyle.State_On else QIcon.Off)
124
125        icon_area_rect = QRect(rect)
126        icon_area_rect.setRight(int(icon_area_rect.height() * 1.26))
127
128        text_rect = QRect(rect)
129        text_rect.setLeft(icon_area_rect.right() + 10)
130
131        # Background  (TODO: Should the tab button have native
132        # toolbutton shape, drawn using PE_PanelButtonTool or even
133        # QToolBox tab shape)
134
135        # Default outline pen
136        pen = QPen(palette.color(QPalette.Mid))
137
138        p.save()
139        p.setPen(Qt.NoPen)
140        p.setBrush(QBrush(background_brush))
141        p.drawRect(rect)
142
143        # Draw the background behind the icon if the background_brush
144        # is different.
145        if not opt.state & QStyle.State_On:
146            p.setBrush(brush_highlight)
147            p.drawRect(icon_area_rect)
148            # Line between the icon and text
149            p.setPen(pen)
150            p.drawLine(icon_area_rect.topRight(),
151                       icon_area_rect.bottomRight())
152
153        if opt.state & QStyle.State_HasFocus:
154            # Set the focus frame pen and draw the border
155            pen = QPen(QColor(FOCUS_OUTLINE_COLOR))
156            p.setPen(pen)
157            p.setBrush(Qt.NoBrush)
158            # Adjust for pen
159            rect = rect.adjusted(0, 0, -1, -1)
160            p.drawRect(rect)
161
162        else:
163            p.setPen(pen)
164            # Draw the top/bottom border
165            if self.position == QStyleOptionToolBoxV2.OnlyOneTab or \
166                    self.position == QStyleOptionToolBoxV2.Beginning or \
167                    self.selected & \
168                        QStyleOptionToolBoxV2.PreviousIsSelected:
169
170                p.drawLine(rect.topLeft(), rect.topRight())
171
172            p.drawLine(rect.bottomLeft(), rect.bottomRight())
173
174        p.restore()
175
176        p.save()
177        text = fm.elidedText(opt.text, Qt.ElideRight, text_rect.width())
178        p.setPen(QPen(palette.color(QPalette.ButtonText)))
179        p.setFont(opt.font)
180
181        p.drawText(text_rect,
182                   int(Qt.AlignVCenter | Qt.AlignLeft) | \
183                   int(Qt.TextSingleLine),
184                   text)
185        if pm:
186            pm_rect = QRect(QPoint(0, 0), pm.size())
187            centered_rect = QRect(pm_rect)
188            centered_rect.moveCenter(icon_area_rect.center())
189            p.drawPixmap(centered_rect, pm, pm_rect)
190        p.restore()
191
192
193class ToolBox(QFrame):
194    """A tool box widget.
195    """
196    tabToogled = Signal(int, bool)
197
198    def setExclusive(self, exclusive):
199        """Set exclusive tabs (only one tab can be open at a time).
200        """
201        if self.__exclusive != exclusive:
202            self.__exclusive = exclusive
203            self.__tabActionGroup.setExclusive(exclusive)
204
205    def exclusive(self):
206        return self.__exclusive
207
208    exclusive_ = Property(bool,
209                         fget=exclusive,
210                         fset=setExclusive,
211                         designable=True)
212
213    def __init__(self, parent=None, **kwargs):
214        QFrame.__init__(self, parent, **kwargs)
215
216        self.__pages = []
217        self.__tabButtonHeight = -1
218        self.__tabIconSize = QSize()
219        self.__exclusive = False
220        self.__setupUi()
221
222    def __setupUi(self):
223        layout = QVBoxLayout()
224        layout.setContentsMargins(0, 0, 0, 0)
225
226        # Scroll area for the contents.
227        self.__scrollArea = \
228                QScrollArea(self, objectName="toolbox-scroll-area")
229
230        self.__scrollArea.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
231        self.__scrollArea.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
232        self.__scrollArea.setSizePolicy(QSizePolicy.MinimumExpanding,
233                                        QSizePolicy.MinimumExpanding)
234        self.__scrollArea.setFrameStyle(QScrollArea.NoFrame)
235        self.__scrollArea.setWidgetResizable(True)
236
237        # A widget with all of the contents.
238        # The tabs/contents are placed in the layout inside this widget
239        self.__contents = QWidget(self.__scrollArea,
240                                  objectName="toolbox-contents")
241
242        # The layout where all the tab/pages are placed
243        self.__contentsLayout = QVBoxLayout()
244        self.__contentsLayout.setContentsMargins(0, 0, 0, 0)
245        self.__contentsLayout.setSizeConstraint(QVBoxLayout.SetMinAndMaxSize)
246        self.__contentsLayout.setSpacing(0)
247
248        self.__contents.setLayout(self.__contentsLayout)
249
250        self.__scrollArea.setWidget(self.__contents)
251
252        layout.addWidget(self.__scrollArea)
253
254        self.setLayout(layout)
255        self.setSizePolicy(QSizePolicy.Fixed,
256                           QSizePolicy.MinimumExpanding)
257
258        self.__tabActionGroup = \
259                QActionGroup(self, objectName="toolbox-tab-action-group")
260
261        self.__tabActionGroup.setExclusive(self.__exclusive)
262
263        self.__actionMapper = QSignalMapper(self)
264        self.__actionMapper.mapped[QObject].connect(self.__onTabActionToogled)
265
266    def setTabButtonHeight(self, height):
267        """Set the tab button height.
268        """
269        if self.__tabButtonHeight != height:
270            self.__tabButtonHeight = height
271            for page in self.__pages:
272                page.button.setFixedHeight(height)
273
274    def tabButtonHeight(self):
275        return self.__tabButtonHeight
276
277    def setTabIconSize(self, size):
278        """Set the tab button icon size.
279        """
280        if self.__tabIconSize != size:
281            self.__tabIconSize = size
282            for page in self.__pages:
283                page.button.setIconSize(size)
284
285    def tabIconSize(self):
286        return self.__tabIconSize
287
288    def tabButton(self, i):
289        """Return the tab button for the `i`-th item.
290        """
291        return self.__pages[i].button
292
293    def tabAction(self, i):
294        """Return open/close action for the `i`-th tab.
295        """
296        return self.__pages[i].action
297
298    def addItem(self, widget, text, icon=None, toolTip=None):
299        """Add the `widget` in a new tab. Return the index of the new tab.
300        """
301        return self.insertItem(self.count(), widget, text, icon, toolTip)
302
303    def insertItem(self, index, widget, text, icon=None, toolTip=None):
304        """Insert the `widget` in a new tab at position `index`.
305        """
306        button = self.createTabButton(widget, text, icon, toolTip)
307
308        self.__contentsLayout.insertWidget(index * 2, button)
309        self.__contentsLayout.insertWidget(index * 2 + 1, widget)
310
311        widget.hide()
312
313        page = _ToolBoxPage(index, widget, button.defaultAction(), button)
314        self.__pages.insert(index, page)
315
316        for i in range(index + 1, self.count()):
317            self.__pages[i] = self.__pages[i]._replace(index=i)
318
319        self.__updatePositions()
320
321        # Show (open) the first tab.
322        if self.count() == 1 and index == 0:
323            page.action.trigger()
324
325        self.__updateSelected()
326
327        self.updateGeometry()
328        return index
329
330    def removeItem(self, index):
331        self.__contentsLayout.takeAt(2 * index + 1)
332        self.__contentsLayout.takeAt(2 * index)
333        page = self.__pages.pop(index)
334
335        for i in range(index, self.count()):
336            self.__pages[i] = self.__pages[i]._replace(index=i)
337
338        page.button.deleteLater()
339        page.widget.deleteLater()
340
341        self.__updatePositions()
342        self.__updateSelected()
343
344        self.updateGeometry()
345
346    def count(self):
347        return len(self.__pages)
348
349    def widget(self, index):
350        """Return the widget at index.
351        """
352        return self.__pages[index].widget
353
354    def createTabButton(self, widget, text, icon=None, toolTip=None):
355        """Create the tab button for `widget`.
356        """
357        action = QAction(text, self)
358        action.setCheckable(True)
359
360        if icon:
361            action.setIcon(icon)
362
363        if toolTip:
364            action.setToolTip(toolTip)
365        self.__tabActionGroup.addAction(action)
366        self.__actionMapper.setMapping(action, action)
367        action.toggled.connect(self.__actionMapper.map)
368
369        button = ToolBoxTabButton(self, objectName="toolbox-tab-button")
370        button.setDefaultAction(action)
371        button.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
372        button.setSizePolicy(QSizePolicy.Expanding,
373                             QSizePolicy.Fixed)
374
375        if self.__tabIconSize.isValid():
376            button.setIconSize(self.__tabIconSize)
377
378        if self.__tabButtonHeight > 0:
379            button.setFixedHeight(self.__tabButtonHeight)
380
381        return button
382
383    def ensureWidgetVisible(self, child, xmargin=50, ymargin=50):
384        """Scroll the contents so child widget instance is visible inside
385        the viewport.
386
387        """
388        self.__scrollArea.ensureWidgetVisible(child, xmargin, ymargin)
389
390    def sizeHint(self):
391        hint = self.__contentsLayout.sizeHint()
392
393        if self.count():
394            # Compute max width of hidden widgets also.
395            scroll = self.__scrollArea
396            scroll_w = scroll.verticalScrollBar().sizeHint().width()
397            frame_w = self.frameWidth() * 2 + scroll.frameWidth() * 2
398            max_w = max([p.widget.sizeHint().width() for p in self.__pages])
399            hint = QSize(max(max_w, hint.width()) + scroll_w + frame_w,
400                         hint.height())
401
402        return QSize(200, 200).expandedTo(hint)
403
404    def __onTabActionToogled(self, action):
405        page = find(self.__pages, action, key=attrgetter("action"))
406        on = action.isChecked()
407        page.widget.setVisible(on)
408        index = page.index
409
410        if index > 0:
411            # Update the `previous` tab buttons style hints
412            previous = self.__pages[index - 1].button
413            flag = QStyleOptionToolBoxV2.NextIsSelected
414            if on:
415                previous.selected |= flag
416            else:
417                previous.selected &= ~flag
418
419            previous.update()
420
421        if index < self.count() - 1:
422            next = self.__pages[index + 1].button
423            flag = QStyleOptionToolBoxV2.PreviousIsSelected
424            if on:
425                next.selected |= flag
426            else:
427                next.selected &= ~flag
428
429            next.update()
430
431        self.tabToogled.emit(index, on)
432
433        self.__contentsLayout.invalidate()
434
435    def __updateSelected(self):
436        """Update the tab buttons selected style flags.
437        """
438        if self.count() == 0:
439            return
440
441        opt = QStyleOptionToolBoxV2
442
443        def update(button, next_sel, prev_sel):
444            if next_sel:
445                button.selected |= opt.NextIsSelected
446            else:
447                button.selected &= ~opt.NextIsSelected
448
449            if prev_sel:
450                button.selected |= opt.PreviousIsSelected
451            else:
452                button.selected &= ~ opt.PreviousIsSelected
453
454            button.update()
455
456        if self.count() == 1:
457            update(self.__pages[0].button, False, False)
458        elif self.count() >= 2:
459            pages = self.__pages
460            for i in range(1, self.count() - 1):
461                update(pages[i].button,
462                       pages[i + 1].action.isChecked(),
463                       pages[i - 1].action.isChecked())
464
465    def __updatePositions(self):
466        """Update the tab buttons position style flags.
467        """
468        if self.count() == 0:
469            return
470        elif self.count() == 1:
471            self.__pages[0].button.position = QStyleOptionToolBoxV2.OnlyOneTab
472        else:
473            self.__pages[0].button.position = QStyleOptionToolBoxV2.Beginning
474            self.__pages[-1].button.position = QStyleOptionToolBoxV2.End
475            for p in self.__pages[1:-1]:
476                p.button.position = QStyleOptionToolBoxV2.Middle
477
478        for p in self.__pages:
479            p.button.update()
480
481
482def identity(arg):
483    return arg
484
485
486def find(iterable, *what, **kwargs):
487    """find(iterable, [what, [key=None, [predicate=operator.eq]]])
488    """
489    if what:
490        what = what[0]
491    key, predicate = kwargs.get("key", identity), kwargs.get("predicate", eq)
492    for item in iterable:
493        item_key = key(item)
494        if predicate(item_key, what):
495            return item
496    else:
497        raise ValueError(what)
Note: See TracBrowser for help on using the repository browser.