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.

Line 
1"""
2A widget containing a grid of clickable actions/buttons.
3
4"""
5from collections import namedtuple, deque
6
7from PyQt4.QtGui import (
8    QFrame, QAction, QToolButton, QGridLayout, QFontMetrics,
9    QSizePolicy, QStyleOptionToolButton, QStylePainter, QStyle
10)
11
12from PyQt4.QtCore import Qt, QObject, QSize, QVariant, QEvent, QSignalMapper
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
53        option = QStyleOptionToolButton()
54        option.initFrom(self)
55
56        margin = self.style().pixelMetric(QStyle.PM_ButtonMargin, option, self)
57        width = self.width() - 2 * margin
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):
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)
105        p.end()
106
107
108class ToolGrid(QFrame):
109    """
110    A widget containing a grid of actions/buttons.
111
112    Actions can be added using standard :func:`QWidget.addAction(QAction)`
113    and :func:`QWidget.insertAction(int, QAction)` methods.
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.
127
128    """
129
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):
135        QFrame.__init__(self, parent)
136
137        if buttonSize is not None:
138            buttonSize = QSize(buttonSize)
139
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)
156
157        self.__mapper = QSignalMapper()
158        self.__mapper.mapped[QObject].connect(self.__onClicked)
159
160        self.__setupUi()
161
162    def __setupUi(self):
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):
171        """
172        Set the button size.
173        """
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):
180        """
181        Return the button size.
182        """
183        return QSize(self.__buttonSize)
184
185    def setIconSize(self, size):
186        """
187        Set the button icon size.
188        """
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):
195        """
196        Return the icon size
197        """
198        return QSize(self.__iconSize)
199
200    def setToolButtonStyle(self, style):
201        """
202        Set the tool button style.
203        """
204        if self.__toolButtonStyle != style:
205            self.__toolButtonStyle = style
206            for slot in self.__gridSlots:
207                slot.button.setToolButtonStyle(style)
208
209    def toolButtonStyle(self):
210        """
211        Return the tool button style.
212        """
213        return self.__toolButtonStyle
214
215    def setColumnCount(self, columns):
216        """
217        Set the number of button/action columns.
218        """
219        if self.__columns != columns:
220            self.__columns = columns
221            self.__relayout()
222
223    def columns(self):
224        """
225        Return the number of columns in the grid.
226        """
227        return self.__columns
228
229    def clear(self):
230        """
231        Clear all actions/buttons.
232        """
233        for slot in reversed(list(self.__gridSlots)):
234            self.removeAction(slot.action)
235        self.__gridSlots = []
236
237    def insertAction(self, before, action):
238        """
239        Insert a new action at the position currently occupied
240        by `before` (can also be an index).
241
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
249        """
250        if isinstance(before, int):
251            actions = list(self.actions())
252            if len(actions) == 0 or before >= len(actions):
253                # Insert as the first action or the last action.
254                return self.addAction(action)
255
256            before = actions[before]
257
258        return QFrame.insertAction(self, before, action)
259
260    def setActions(self, actions):
261        """
262        Clear the grid and add `actions`.
263        """
264        self.clear()
265
266        for action in actions:
267            self.addAction(action)
268
269    def buttonForAction(self, action):
270        """
271        Return the :class:`QToolButton` instance button for `action`.
272        """
273        actions = [slot.action for slot in self.__gridSlots]
274        index = actions.index(action)
275        return self.__gridSlots[index].button
276
277    def createButtonForAction(self, action):
278        """
279        Create and return a :class:`QToolButton` for action.
280        """
281        button = _ToolGridButton(self)
282        button.setDefaultAction(action)
283
284        if self.__buttonSize.isValid():
285            button.setFixedSize(self.__buttonSize)
286        if self.__iconSize.isValid():
287            button.setIconSize(self.__iconSize)
288
289        button.setToolButtonStyle(self.__toolButtonStyle)
290        button.setProperty("tool-grid-button", QVariant(True))
291        return button
292
293    def count(self):
294        """
295        Return the number of buttons/actions in the grid.
296        """
297        return len(self.__gridSlots)
298
299    def actionEvent(self, event):
300        QFrame.actionEvent(self, event)
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):
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):
363            item = self.layout().itemAtPosition(index / self.__columns,
364                                                index % self.__columns)
365            if item:
366                button = item.widget()
367                new_index = index + count
368                self.layout().addWidget(button, new_index / self.__columns,
369                                        new_index % self.__columns,
370                                        Qt.AlignLeft | Qt.AlignTop)
371
372    def __relayout(self):
373        """Relayout the buttons.
374        """
375        for i in reversed(range(self.layout().count())):
376            self.layout().takeAt(i)
377
378        self.__gridSlots = [_ToolGridSlot(slot.button, slot.action,
379                                          i / self.__columns,
380                                          i % self.__columns)
381                            for i, slot in enumerate(self.__gridSlots)]
382
383        for slot in self.__gridSlots:
384            self.layout().addWidget(slot.button, slot.row, slot.column,
385                                    Qt.AlignLeft | Qt.AlignTop)
386
387    def __indexOf(self, button):
388        """Return the index of button widget.
389        """
390        buttons = [slot.button for slot in self.__gridSlots]
391        return buttons.index(button)
392
393    def __onButtonRightClick(self, button):
394        pass
395
396    def __onButtonEnter(self, button):
397        action = button.defaultAction()
398        self.actionHovered.emit(action)
399
400    def __onClicked(self, action):
401        self.actionTriggered.emit(action)
402
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
415        return QFrame.eventFilter(self, obj, event)
416
417    def __focusMove(self, focus, key):
418        assert(focus is self.focusWidget())
419        try:
420            index = self.__indexOf(focus)
421        except IndexError:
422            return False
423
424        if key == Qt.Key_Down:
425            index += self.__columns
426        elif key == Qt.Key_Up:
427            index -= self.__columns
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():
434            button = self.__gridSlots[index].button
435            button.setFocus(Qt.TabFocusReason)
436            return True
437        else:
438            return False
439
440
441class ToolButtonEventListener(QObject):
442    """
443    An event listener(filter) for :class:`QToolButtons`.
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.