source: orange/Orange/OrangeCanvas/gui/toolgrid.py @ 11103:132819392fe4

Revision 11103:132819392fe4, 11.8 KB checked in by Ales Erjavec <ales.erjavec@…>, 19 months ago (diff)

Use button's clicked signal instead of triggered.

triggered gets emmited whenever the action is triggered not just
when the button is clicked.

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        # TODO: Get margins from the style
55        width = self.width() - 4
56
57        while words:
58            w = words.popleft()
59
60            if curr_line_word_count:
61                line_extended = " ".join([curr_line, w])
62            else:
63                line_extended = w
64
65            line_w = fm.boundingRect(line_extended).width()
66
67            if line_w >= width:
68                if curr_line_word_count == 0 or len(lines) == 1:
69                    # A single word that is too long must be elided.
70                    # Also if the text overflows 2 lines
71                    # Warning: hardcoded max lines
72                    curr_line = fm.elidedText(line_extended, Qt.ElideRight,
73                                              width)
74                    curr_line = unicode(curr_line)
75                else:
76                    # Put the word back
77                    words.appendleft(w)
78
79                lines.append(curr_line)
80                curr_line = ""
81                curr_line_word_count = 0
82                if len(lines) == 2:
83                    break
84            else:
85                curr_line = line_extended
86                curr_line_word_count += 1
87
88        if curr_line:
89            lines.append(curr_line)
90
91        text = "\n".join(lines)
92
93        self.__text = text
94
95    def paintEvent(self, event):
96        try:
97            p = QStylePainter(self)
98            opt = QStyleOptionToolButton()
99            self.initStyleOption(opt)
100            if self.__text:
101                # Replace the text
102                opt.text = self.__text
103            p.drawComplexControl(QStyle.CC_ToolButton, opt)
104        except Exception, ex:
105            print ex
106        p.end()
107
108
109class ToolGrid(QWidget):
110    """A widget containing a grid of actions/buttons.
111    """
112    actionTriggered = Signal(QAction)
113    actionHovered = Signal(QAction)
114
115    def __init__(self, parent=None, columns=4, buttonSize=None,
116                 iconSize=None, toolButtonStyle=Qt.ToolButtonTextUnderIcon):
117        QWidget.__init__(self, parent)
118        self.columns = columns
119        self.buttonSize = buttonSize or QSize(50, 50)
120        self.iconSize = iconSize or QSize(26, 26)
121        self.toolButtonStyle = toolButtonStyle
122        self._gridSlots = []
123
124        self._buttonListener = ToolButtonEventListener(self)
125        self._buttonListener.buttonRightClicked.connect(
126                self._onButtonRightClick)
127
128        self._buttonListener.buttonEnter.connect(
129                self._onButtonEnter)
130
131        self.__mapper = QSignalMapper()
132        self.__mapper.mapped[QObject].connect(self.__onClicked)
133
134        self.setupUi()
135
136    def setupUi(self):
137        layout = QGridLayout()
138        layout.setContentsMargins(0, 0, 0, 0)
139        layout.setSpacing(0)
140        layout.setSizeConstraint(QGridLayout.SetFixedSize)
141        self.setLayout(layout)
142        self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.MinimumExpanding)
143
144    def setButtonSize(self, size):
145        """Set the button size.
146        """
147        for slot in self._gridSlots:
148            slot.button.setFixedSize(size)
149        self.buttonSize = size
150
151    def setIconSize(self, size):
152        """Set the button icon size.
153        """
154        for slot in self._gridSlots:
155            slot.button.setIconSize(size)
156        self.iconSize = size
157
158    def setToolButtonStyle(self, style):
159        """Set the tool button style.
160        """
161        for slot in self._gridSlots:
162            slot.button.setToolButtonStyle(style)
163
164        self.toolButtonStyle = style
165
166    def setColumnCount(self, columns):
167        """Set the number of button/action columns.
168        """
169        if self.columns != columns:
170            self.columns = columns
171            self._relayout()
172
173    def clear(self):
174        """Clear all actions.
175        """
176        layout = self.layout()
177        for slot in self._gridSlots:
178            self.removeAction(slot.action)
179            index = layout.indexOf(slot.button)
180            layout.takeAt(index)
181            slot.button.deleteLater()
182
183        self._gridSlots = []
184
185    # TODO: Move the add/insert/remove code in actionEvent, preserve the
186    # default Qt widget action interface.
187
188    def addAction(self, action):
189        """Append a new action to the ToolGrid.
190        """
191        self.insertAction(len(self._gridSlots), action)
192
193    def insertAction(self, index, action):
194        """Insert a new action at index.
195        """
196        self._shiftGrid(index, 1)
197        button = self.createButtonForAction(action)
198        row = index / self.columns
199        column = index % self.columns
200        self.layout().addWidget(
201            button, row, column,
202            Qt.AlignLeft | Qt.AlignTop
203        )
204        self._gridSlots.insert(
205            index, _ToolGridSlot(button, action, row, column)
206        )
207
208        self.__mapper.setMapping(button, action)
209        button.clicked.connect(self.__mapper.map)
210        button.installEventFilter(self._buttonListener)
211
212    def setActions(self, actions):
213        """Clear the grid and add actions.
214        """
215        self.clear()
216
217        for action in actions:
218            self.addAction(action)
219
220    def removeAction(self, action):
221        """Remove action from the widget.
222        """
223        actions = [slot.action for slot in self._gridSlots]
224        index = actions.index(action)
225        slot = self._gridSlots.pop(index)
226
227        slot.button.removeEventFilter(self._buttonListener)
228        self.__mapper.removeMappings(slot.button)
229
230        self.layout().removeWidget(slot.button)
231        self._shiftGrid(index + 1, -1)
232
233        slot.button.deleteLater()
234
235    def buttonForAction(self, action):
236        """Return the `QToolButton` instance button for `action`.
237        """
238        actions = [slot.action for slot in self._gridSlots]
239        index = actions.index(action)
240        return self._gridSlots[index].button
241
242    def createButtonForAction(self, action):
243        """Create and return a QToolButton for action.
244        """
245#        button = QToolButton(self)
246        button = _ToolGridButton(self)
247        button.setDefaultAction(action)
248#        button.setText(action.text())
249#        button.setIcon(action.icon())
250
251        if self.buttonSize.isValid():
252            button.setFixedSize(self.buttonSize)
253        if self.iconSize.isValid():
254            button.setIconSize(self.iconSize)
255
256        button.setToolButtonStyle(self.toolButtonStyle)
257        button.setProperty("tool-grid-button", QVariant(True))
258        return button
259
260    def count(self):
261        return len(self._gridSlots)
262
263    def _shiftGrid(self, start, count=1):
264        """Shift all buttons starting at index `start` by `count` cells.
265        """
266        button_count = self.layout().count()
267        direction = 1 if count >= 0 else -1
268        if direction == 1:
269            start, end = button_count - 1, start - 1
270        else:
271            start, end = start, button_count
272
273        for index in range(start, end, -direction):
274            item = self.layout().itemAtPosition(index / self.columns,
275                                                index % self.columns)
276            if item:
277                button = item.widget()
278                new_index = index + count
279                self.layout().addWidget(button, new_index / self.columns,
280                                        new_index % self.columns,
281                                        Qt.AlignLeft | Qt.AlignTop)
282
283    def _relayout(self):
284        """Relayout the buttons.
285        """
286        for i in reversed(range(self.layout().count())):
287            self.layout().takeAt(i)
288
289        self._gridSlots = [_ToolGridSlot(slot.button, slot.action,
290                                         i / self.columns, i % self.columns)
291                           for i, slot in enumerate(self._gridSlots)]
292
293        for slot in self._gridSlots:
294            self.layout().addWidget(slot.button, slot.row, slot.column,
295                                    Qt.AlignLeft | Qt.AlignTop)
296
297    def _indexOf(self, button):
298        """Return the index of button widget.
299        """
300        buttons = [slot.button for slot in self._gridSlots]
301        return buttons.index(button)
302
303    def paintEvent(self, event):
304        return utils.StyledWidget_paintEvent(self, event)
305
306    def focusNextPrevChild(self, next):
307        focus = self.focusWidget()
308        try:
309            index = self._indexOf(focus)
310        except IndexError:
311            return False
312
313        if next:
314            index += 1
315        else:
316            index -= 1
317        if index == -1 or index == self.count():
318            return False
319
320        button = self._gridSlots[index].button
321        button.setFocus(Qt.TabFocusReason if next else Qt.BacktabFocusReason)
322        return True
323
324    def _onButtonRightClick(self, button):
325        print button
326
327    def _onButtonEnter(self, button):
328        action = button.defaultAction()
329        self.actionHovered.emit(action)
330
331    def __onClicked(self, action):
332        self.actionTriggered.emit(action)
333
334#    def keyPressEvent(self, event):
335#        key = event.key()
336#        focus = self.focusWidget()
337#        print key, focus
338#        if key == Qt.Key_Down or key == Qt.Key_Up:
339#            try:
340#                index = self._indexOf(focus)
341#            except IndexError:
342#                return
343#            if key == Qt.Key_Down:
344#                index += self.columns
345#            else:
346#                index -= self.columns
347#            if index >= 0 and index < self.count():
348#                button = self._gridSlots[index].button
349#                button.setFocus(Qt.TabFocusReason)
350#            event.accept()
351#        else:
352#            return QWidget.keyPressEvent(self, event)
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.