source: orange/Orange/OrangeCanvas/gui/toolbox.py @ 11116:ca441c4e3c4d

Revision 11116:ca441c4e3c4d, 15.2 KB checked in by Ales Erjavec <ales.erjavec@…>, 18 months ago (diff)

Fixed return value from 'widget' method, extended tests for ToolGrid.

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