source: orange/Orange/OrangeCanvas/gui/toolgrid.py @ 11100:cf6f6744dd9b

Revision 11100:cf6f6744dd9b, 11.6 KB checked in by Ales Erjavec <ales.erjavec@…>, 19 months ago (diff)

Added gui widget toolkit.

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
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.setupUi()
132
133    def setupUi(self):
134        layout = QGridLayout()
135        layout.setContentsMargins(0, 0, 0, 0)
136        layout.setSpacing(0)
137        layout.setSizeConstraint(QGridLayout.SetFixedSize)
138        self.setLayout(layout)
139        self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.MinimumExpanding)
140
141    def setButtonSize(self, size):
142        """Set the button size.
143        """
144        for slot in self._gridSlots:
145            slot.button.setFixedSize(size)
146        self.buttonSize = size
147
148    def setIconSize(self, size):
149        """Set the button icon size.
150        """
151        for slot in self._gridSlots:
152            slot.button.setIconSize(size)
153        self.iconSize = size
154
155    def setToolButtonStyle(self, style):
156        """Set the tool button style.
157        """
158        for slot in self._gridSlots:
159            slot.button.setToolButtonStyle(style)
160
161        self.toolButtonStyle = style
162
163    def setColumnCount(self, columns):
164        """Set the number of button/action columns.
165        """
166        if self.columns != columns:
167            self.columns = columns
168            self._relayout()
169
170    def clear(self):
171        """Clear all actions.
172        """
173        layout = self.layout()
174        for slot in self._gridSlots:
175            self.removeAction(slot.action)
176            index = layout.indexOf(slot.button)
177            layout.takeAt(index)
178            slot.button.deleteLater()
179
180        self._gridSlots = []
181
182    # TODO: Move the add/insert/remove code in actionEvent, preserve the
183    # default Qt widget action interface.
184
185    def addAction(self, action):
186        """Append a new action to the ToolGrid.
187        """
188        self.insertAction(len(self._gridSlots), action)
189
190    def insertAction(self, index, action):
191        """Insert a new action at index.
192        """
193        self._shiftGrid(index, 1)
194        button = self.createButtonForAction(action)
195        row = index / self.columns
196        column = index % self.columns
197        self.layout().addWidget(
198            button, row, column,
199            Qt.AlignLeft | Qt.AlignTop
200        )
201        self._gridSlots.insert(
202            index, _ToolGridSlot(button, action, row, column)
203        )
204
205        button.triggered.connect(self.actionTriggered)
206        button.installEventFilter(self._buttonListener)
207
208    def setActions(self, actions):
209        """Clear the grid and add actions.
210        """
211        self.clear()
212
213        for action in actions:
214            self.addAction(action)
215
216    def removeAction(self, action):
217        """Remove action from the widget.
218        """
219        actions = [slot.action for slot in self._gridSlots]
220        index = actions.index(action)
221        slot = self._gridSlots.pop(index)
222
223        slot.button.removeEventFilter(self._buttonListener)
224
225        self.layout().removeWidget(slot.button)
226        self._shiftGrid(index + 1, -1)
227
228        slot.button.triggered.disconnect(self.actionTriggered)
229        slot.button.deleteLater()
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 = QToolButton(self)
242        button = _ToolGridButton(self)
243        button.setDefaultAction(action)
244#        button.setText(action.text())
245#        button.setIcon(action.icon())
246
247        if self.buttonSize.isValid():
248            button.setFixedSize(self.buttonSize)
249        if self.iconSize.isValid():
250            button.setIconSize(self.iconSize)
251
252        button.setToolButtonStyle(self.toolButtonStyle)
253        button.setProperty("tool-grid-button", QVariant(True))
254        return button
255
256    def count(self):
257        return len(self._gridSlots)
258
259    def _shiftGrid(self, start, count=1):
260        """Shift all buttons starting at index `start` by `count` cells.
261        """
262        button_count = self.layout().count()
263        direction = 1 if count >= 0 else -1
264        if direction == 1:
265            start, end = button_count - 1, start - 1
266        else:
267            start, end = start, button_count
268
269        for index in range(start, end, -direction):
270            item = self.layout().itemAtPosition(index / self.columns,
271                                                index % self.columns)
272            if item:
273                button = item.widget()
274                new_index = index + count
275                self.layout().addWidget(button, new_index / self.columns,
276                                        new_index % self.columns,
277                                        Qt.AlignLeft | Qt.AlignTop)
278
279    def _relayout(self):
280        """Relayout the buttons.
281        """
282        for i in reversed(range(self.layout().count())):
283            self.layout().takeAt(i)
284
285        self._gridSlots = [_ToolGridSlot(slot.button, slot.action,
286                                         i / self.columns, i % self.columns)
287                           for i, slot in enumerate(self._gridSlots)]
288
289        for slot in self._gridSlots:
290            self.layout().addWidget(slot.button, slot.row, slot.column,
291                                    Qt.AlignLeft | Qt.AlignTop)
292
293    def _indexOf(self, button):
294        """Return the index of button widget.
295        """
296        buttons = [slot.button for slot in self._gridSlots]
297        return buttons.index(button)
298
299    def paintEvent(self, event):
300        return utils.StyledWidget_paintEvent(self, event)
301
302    def focusNextPrevChild(self, next):
303        focus = self.focusWidget()
304        try:
305            index = self._indexOf(focus)
306        except IndexError:
307            return False
308
309        if next:
310            index += 1
311        else:
312            index -= 1
313        if index == -1 or index == self.count():
314            return False
315
316        button = self._gridSlots[index].button
317        button.setFocus(Qt.TabFocusReason if next else Qt.BacktabFocusReason)
318        return True
319
320    def _onButtonRightClick(self, button):
321        print button
322
323    def _onButtonEnter(self, button):
324        action = button.defaultAction()
325        self.actionHovered.emit(action)
326
327#    def keyPressEvent(self, event):
328#        key = event.key()
329#        focus = self.focusWidget()
330#        print key, focus
331#        if key == Qt.Key_Down or key == Qt.Key_Up:
332#            try:
333#                index = self._indexOf(focus)
334#            except IndexError:
335#                return
336#            if key == Qt.Key_Down:
337#                index += self.columns
338#            else:
339#                index -= self.columns
340#            if index >= 0 and index < self.count():
341#                button = self._gridSlots[index].button
342#                button.setFocus(Qt.TabFocusReason)
343#            event.accept()
344#        else:
345#            return QWidget.keyPressEvent(self, event)
346
347
348class ToolButtonEventListener(QObject):
349    """An event listener(filter) for QToolButtons.
350    """
351    buttonLeftClicked = Signal(QToolButton)
352    buttonRightClicked = Signal(QToolButton)
353    buttonEnter = Signal(QToolButton)
354    buttonLeave = Signal(QToolButton)
355
356    def __init__(self, parent=None):
357        QObject.__init__(self, parent)
358        self.button_down = None
359        self.button = None
360        self.button_down_pos = None
361
362    def eventFilter(self, obj, event):
363        if not isinstance(obj, QToolButton):
364            return False
365
366        if event.type() == QEvent.MouseButtonPress:
367            self.button = obj
368            self.button_down = event.button()
369            self.button_down_pos = event.pos()
370
371        elif event.type() == QEvent.MouseButtonRelease:
372            if self.button.underMouse():
373                if event.button() == Qt.RightButton:
374                    self.buttonRightClicked.emit(self.button)
375                elif event.button() == Qt.LeftButton:
376                    self.buttonLeftClicked.emit(self.button)
377
378        elif event.type() == QEvent.Enter:
379            self.buttonEnter.emit(obj)
380
381        elif event.type() == QEvent.Leave:
382            self.buttonLeave.emit(obj)
383
384        return False
Note: See TracBrowser for help on using the repository browser.