source: orange/Orange/OrangeCanvas/gui/toolgrid.py @ 11177:9d471e1fca3e

Revision 11177:9d471e1fca3e, 13.2 KB checked in by Ales Erjavec <ales.erjavec@…>, 18 months ago (diff)

Cleanup of the ToolGrid code and interface.

Moved the add/remove action code to actionEvent handler, also added
more tests.

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