source: orange/Orange/OrangeCanvas/gui/toolgrid.py @ 11425:c202b2d41718

Revision 11425:c202b2d41718, 14.2 KB checked in by Ales Erjavec <ales.erjavec@…>, 13 months ago (diff)

Fixed some sphinx warnings.

RevLine 
[11100]1"""
[11366]2A widget containing a grid of clickable actions/buttons.
[11177]3
[11100]4"""
5from collections import namedtuple, deque
6
7from PyQt4.QtGui import (
[11345]8    QFrame, QAction, QToolButton, QGridLayout, QFontMetrics,
[11100]9    QSizePolicy, QStyleOptionToolButton, QStylePainter, QStyle
10)
11
[11103]12from PyQt4.QtCore import Qt, QObject, QSize, QVariant, QEvent, QSignalMapper
[11100]13from PyQt4.QtCore import pyqtSignal as Signal
14
15from . import utils
16
17
18_ToolGridSlot = namedtuple(
19    "_ToolGridSlot",
20    ["button",
21     "action",
22     "row",
23     "column"
24     ]
25    )
26
27
28class _ToolGridButton(QToolButton):
29    def __init__(self, *args, **kwargs):
30        QToolButton.__init__(self, *args, **kwargs)
31
32        self.__text = ""
33
34    def actionEvent(self, event):
35        QToolButton.actionEvent(self, event)
36        if event.type() == QEvent.ActionChanged or \
37                event.type() == QEvent.ActionAdded:
38            self.__textLayout()
39
40    def resizeEvent(self, event):
41        QToolButton.resizeEvent(self, event)
42        self.__textLayout()
43
44    def __textLayout(self):
45        fm = QFontMetrics(self.font())
46        text = unicode(self.defaultAction().iconText())
47        words = deque(text.split())
48
49        lines = []
50        curr_line = ""
51        curr_line_word_count = 0
52
[11176]53        option = QStyleOptionToolButton()
54        option.initFrom(self)
55
56        margin = self.style().pixelMetric(QStyle.PM_ButtonMargin, option, self)
57        width = self.width() - 2 * margin
[11100]58
59        while words:
60            w = words.popleft()
61
62            if curr_line_word_count:
63                line_extended = " ".join([curr_line, w])
64            else:
65                line_extended = w
66
67            line_w = fm.boundingRect(line_extended).width()
68
69            if line_w >= width:
70                if curr_line_word_count == 0 or len(lines) == 1:
71                    # A single word that is too long must be elided.
72                    # Also if the text overflows 2 lines
73                    # Warning: hardcoded max lines
74                    curr_line = fm.elidedText(line_extended, Qt.ElideRight,
75                                              width)
76                    curr_line = unicode(curr_line)
77                else:
78                    # Put the word back
79                    words.appendleft(w)
80
81                lines.append(curr_line)
82                curr_line = ""
83                curr_line_word_count = 0
84                if len(lines) == 2:
85                    break
86            else:
87                curr_line = line_extended
88                curr_line_word_count += 1
89
90        if curr_line:
91            lines.append(curr_line)
92
93        text = "\n".join(lines)
94
95        self.__text = text
96
97    def paintEvent(self, event):
[11177]98        p = QStylePainter(self)
99        opt = QStyleOptionToolButton()
100        self.initStyleOption(opt)
101        if self.__text:
102            # Replace the text
103            opt.text = self.__text
104        p.drawComplexControl(QStyle.CC_ToolButton, opt)
[11100]105        p.end()
106
107
[11345]108class ToolGrid(QFrame):
[11366]109    """
110    A widget containing a grid of actions/buttons.
[11177]111
[11425]112    Actions can be added using standard :func:`QWidget.addAction(QAction)`
113    and :func:`QWidget.insertAction(int, QAction)` methods.
[11366]114
115    Parameters
116    ----------
117    parent : :class:`QWidget`
118        Parent widget.
119    columns : int
120        Number of columns in the grid layout.
121    buttonSize : :class:`QSize`, optional
122        Size of tool buttons in the grid.
123    iconSize : :class:`QSize`, optional
124        Size of icons in the buttons.
125    toolButtonStyle : :class:`Qt.ToolButtonStyle`
126        Tool button style.
[11177]127
[11100]128    """
[11366]129
[11100]130    actionTriggered = Signal(QAction)
131    actionHovered = Signal(QAction)
132
133    def __init__(self, parent=None, columns=4, buttonSize=None,
134                 iconSize=None, toolButtonStyle=Qt.ToolButtonTextUnderIcon):
[11345]135        QFrame.__init__(self, parent)
[11100]136
[11177]137        if buttonSize is not None:
138            buttonSize = QSize(buttonSize)
[11100]139
[11177]140        if iconSize is not None:
141            iconSize = QSize(iconSize)
142
143        self.__columns = columns
144        self.__buttonSize = buttonSize or QSize(50, 50)
145        self.__iconSize = iconSize or QSize(26, 26)
146        self.__toolButtonStyle = toolButtonStyle
147
148        self.__gridSlots = []
149
150        self.__buttonListener = ToolButtonEventListener(self)
151        self.__buttonListener.buttonRightClicked.connect(
152                self.__onButtonRightClick)
153
154        self.__buttonListener.buttonEnter.connect(
155                self.__onButtonEnter)
[11100]156
[11103]157        self.__mapper = QSignalMapper()
158        self.__mapper.mapped[QObject].connect(self.__onClicked)
159
[11177]160        self.__setupUi()
[11100]161
[11177]162    def __setupUi(self):
[11100]163        layout = QGridLayout()
164        layout.setContentsMargins(0, 0, 0, 0)
165        layout.setSpacing(0)
166        layout.setSizeConstraint(QGridLayout.SetFixedSize)
167        self.setLayout(layout)
168        self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.MinimumExpanding)
169
170    def setButtonSize(self, size):
[11366]171        """
172        Set the button size.
[11100]173        """
[11177]174        if self.__buttonSize != size:
175            self.__buttonSize = size
176            for slot in self.__gridSlots:
177                slot.button.setFixedSize(size)
178
179    def buttonSize(self):
[11366]180        """
181        Return the button size.
182        """
[11177]183        return QSize(self.__buttonSize)
[11100]184
185    def setIconSize(self, size):
[11366]186        """
187        Set the button icon size.
[11100]188        """
[11177]189        if self.__iconSize != size:
190            self.__iconSize = size
191            for slot in self.__gridSlots:
192                slot.button.setIconSize(size)
193
194    def iconSize(self):
[11366]195        """
196        Return the icon size
197        """
[11177]198        return QSize(self.__iconSize)
[11100]199
200    def setToolButtonStyle(self, style):
[11366]201        """
202        Set the tool button style.
[11100]203        """
[11177]204        if self.__toolButtonStyle != style:
205            self.__toolButtonStyle = style
206            for slot in self.__gridSlots:
207                slot.button.setToolButtonStyle(style)
[11100]208
[11177]209    def toolButtonStyle(self):
[11366]210        """
211        Return the tool button style.
212        """
[11177]213        return self.__toolButtonStyle
[11100]214
215    def setColumnCount(self, columns):
[11366]216        """
217        Set the number of button/action columns.
[11100]218        """
[11177]219        if self.__columns != columns:
220            self.__columns = columns
221            self.__relayout()
222
223    def columns(self):
[11366]224        """
225        Return the number of columns in the grid.
226        """
[11177]227        return self.__columns
[11100]228
229    def clear(self):
[11366]230        """
231        Clear all actions/buttons.
[11100]232        """
[11177]233        for slot in reversed(list(self.__gridSlots)):
[11100]234            self.removeAction(slot.action)
[11177]235        self.__gridSlots = []
[11100]236
[11177]237    def insertAction(self, before, action):
[11366]238        """
239        Insert a new action at the position currently occupied
[11177]240        by `before` (can also be an index).
[11100]241
[11366]242        Parameters
243        ----------
244        before : :class:`QAction` or int
245            Position where the `action` should be inserted.
246        action : :class:`QAction`
247            Action to insert
248
[11177]249        """
250        if isinstance(before, int):
251            actions = list(self.actions())
252            if len(actions) == 0 or before >= len(actions):
[11366]253                # Insert as the first action or the last action.
[11177]254                return self.addAction(action)
[11100]255
[11177]256            before = actions[before]
[11100]257
[11345]258        return QFrame.insertAction(self, before, action)
[11100]259
260    def setActions(self, actions):
[11366]261        """
262        Clear the grid and add `actions`.
[11100]263        """
264        self.clear()
265
266        for action in actions:
267            self.addAction(action)
268
269    def buttonForAction(self, action):
[11366]270        """
271        Return the :class:`QToolButton` instance button for `action`.
[11100]272        """
[11177]273        actions = [slot.action for slot in self.__gridSlots]
[11100]274        index = actions.index(action)
[11177]275        return self.__gridSlots[index].button
[11100]276
277    def createButtonForAction(self, action):
[11366]278        """
279        Create and return a :class:`QToolButton` for action.
[11100]280        """
281        button = _ToolGridButton(self)
282        button.setDefaultAction(action)
283
[11177]284        if self.__buttonSize.isValid():
285            button.setFixedSize(self.__buttonSize)
286        if self.__iconSize.isValid():
287            button.setIconSize(self.__iconSize)
[11100]288
[11177]289        button.setToolButtonStyle(self.__toolButtonStyle)
[11100]290        button.setProperty("tool-grid-button", QVariant(True))
291        return button
292
293    def count(self):
[11366]294        """
295        Return the number of buttons/actions in the grid.
296        """
[11177]297        return len(self.__gridSlots)
[11100]298
[11177]299    def actionEvent(self, event):
[11345]300        QFrame.actionEvent(self, event)
[11177]301
302        if event.type() == QEvent.ActionAdded:
303            # Note: the action is already in the self.actions() list.
304            actions = list(self.actions())
305            index = actions.index(event.action())
306            self.__insertActionButton(index, event.action())
307
308        elif event.type() == QEvent.ActionRemoved:
309            self.__removeActionButton(event.action())
310
311    def __insertActionButton(self, index, action):
312        """Create a button for the action and add it to the layout
313        at index.
314
315        """
316        self.__shiftGrid(index, 1)
317        button = self.createButtonForAction(action)
318
319        row = index / self.__columns
320        column = index % self.__columns
321
322        self.layout().addWidget(
323            button, row, column,
324            Qt.AlignLeft | Qt.AlignTop
325        )
326
327        self.__gridSlots.insert(
328            index, _ToolGridSlot(button, action, row, column)
329        )
330
331        self.__mapper.setMapping(button, action)
332        button.clicked.connect(self.__mapper.map)
333        button.installEventFilter(self.__buttonListener)
334        button.installEventFilter(self)
335
336    def __removeActionButton(self, action):
337        """Remove the button for the action from the layout and delete it.
338        """
339        actions = [slot.action for slot in self.__gridSlots]
340        index = actions.index(action)
341        slot = self.__gridSlots.pop(index)
342
343        slot.button.removeEventFilter(self.__buttonListener)
344        slot.button.removeEventFilter(self)
345        self.__mapper.removeMappings(slot.button)
346
347        self.layout().removeWidget(slot.button)
348        self.__shiftGrid(index + 1, -1)
349
350        slot.button.deleteLater()
351
352    def __shiftGrid(self, start, count=1):
[11100]353        """Shift all buttons starting at index `start` by `count` cells.
354        """
355        button_count = self.layout().count()
356        direction = 1 if count >= 0 else -1
357        if direction == 1:
358            start, end = button_count - 1, start - 1
359        else:
360            start, end = start, button_count
361
362        for index in range(start, end, -direction):
[11177]363            item = self.layout().itemAtPosition(index / self.__columns,
364                                                index % self.__columns)
[11100]365            if item:
366                button = item.widget()
367                new_index = index + count
[11177]368                self.layout().addWidget(button, new_index / self.__columns,
369                                        new_index % self.__columns,
[11100]370                                        Qt.AlignLeft | Qt.AlignTop)
371
[11177]372    def __relayout(self):
[11100]373        """Relayout the buttons.
374        """
375        for i in reversed(range(self.layout().count())):
376            self.layout().takeAt(i)
377
[11177]378        self.__gridSlots = [_ToolGridSlot(slot.button, slot.action,
379                                          i / self.__columns,
380                                          i % self.__columns)
381                            for i, slot in enumerate(self.__gridSlots)]
[11100]382
[11177]383        for slot in self.__gridSlots:
[11100]384            self.layout().addWidget(slot.button, slot.row, slot.column,
385                                    Qt.AlignLeft | Qt.AlignTop)
386
[11177]387    def __indexOf(self, button):
[11100]388        """Return the index of button widget.
389        """
[11177]390        buttons = [slot.button for slot in self.__gridSlots]
[11100]391        return buttons.index(button)
392
[11177]393    def __onButtonRightClick(self, button):
[11366]394        pass
[11100]395
[11177]396    def __onButtonEnter(self, button):
[11100]397        action = button.defaultAction()
398        self.actionHovered.emit(action)
399
[11103]400    def __onClicked(self, action):
401        self.actionTriggered.emit(action)
402
[11176]403    def paintEvent(self, event):
404        return utils.StyledWidget_paintEvent(self, event)
405
406    def eventFilter(self, obj, event):
407        etype = event.type()
408        if etype == QEvent.KeyPress and obj.hasFocus():
409            key = event.key()
410            if key in [Qt.Key_Up, Qt.Key_Down, Qt.Key_Left, Qt.Key_Right]:
411                if self.__focusMove(obj, key):
412                    event.accept()
413                    return True
414
[11345]415        return QFrame.eventFilter(self, obj, event)
[11176]416
417    def __focusMove(self, focus, key):
418        assert(focus is self.focusWidget())
419        try:
[11177]420            index = self.__indexOf(focus)
[11176]421        except IndexError:
422            return False
423
424        if key == Qt.Key_Down:
[11177]425            index += self.__columns
[11176]426        elif key == Qt.Key_Up:
[11177]427            index -= self.__columns
[11176]428        elif key == Qt.Key_Left:
429            index -= 1
430        elif key == Qt.Key_Right:
431            index += 1
432
433        if index >= 0 and index < self.count():
[11177]434            button = self.__gridSlots[index].button
[11176]435            button.setFocus(Qt.TabFocusReason)
436            return True
437        else:
438            return False
[11100]439
440
441class ToolButtonEventListener(QObject):
[11366]442    """
443    An event listener(filter) for :class:`QToolButtons`.
[11100]444    """
445    buttonLeftClicked = Signal(QToolButton)
446    buttonRightClicked = Signal(QToolButton)
447    buttonEnter = Signal(QToolButton)
448    buttonLeave = Signal(QToolButton)
449
450    def __init__(self, parent=None):
451        QObject.__init__(self, parent)
452        self.button_down = None
453        self.button = None
454        self.button_down_pos = None
455
456    def eventFilter(self, obj, event):
457        if not isinstance(obj, QToolButton):
458            return False
459
460        if event.type() == QEvent.MouseButtonPress:
461            self.button = obj
462            self.button_down = event.button()
463            self.button_down_pos = event.pos()
464
465        elif event.type() == QEvent.MouseButtonRelease:
466            if self.button.underMouse():
467                if event.button() == Qt.RightButton:
468                    self.buttonRightClicked.emit(self.button)
469                elif event.button() == Qt.LeftButton:
470                    self.buttonLeftClicked.emit(self.button)
471
472        elif event.type() == QEvent.Enter:
473            self.buttonEnter.emit(obj)
474
475        elif event.type() == QEvent.Leave:
476            self.buttonLeave.emit(obj)
477
478        return False
Note: See TracBrowser for help on using the repository browser.