source: orange/Orange/OrangeCanvas/gui/toolgrid.py @ 11176:4ed54725b91a

Revision 11176:4ed54725b91a, 11.9 KB checked in by Ales Erjavec <ales.erjavec@…>, 18 months ago (diff)

Added keyboard directional navigation to ToolGrid.

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