source: orange/Orange/OrangeCanvas/document/quickmenu.py @ 11229:4197f3447c83

Revision 11229:4197f3447c83, 27.7 KB checked in by Ales Erjavec <ales.erjavec@…>, 16 months ago (diff)

Disable items in the quick menu that cannot be connected.

Line 
1"""
2Quick widget selector menu for the canvas.
3
4"""
5import sys
6import logging
7
8from collections import namedtuple, Callable
9
10import numpy
11
12from PyQt4.QtGui import (
13    QWidget, QFrame, QToolButton, QAbstractButton, QAction, QIcon, QTreeView,
14    QButtonGroup, QStackedWidget, QHBoxLayout, QVBoxLayout, QSizePolicy,
15    QStandardItemModel, QSortFilterProxyModel, QStyleOptionToolButton,
16    QStylePainter, QStyle, QApplication, QStyledItemDelegate,
17    QStyleOptionViewItemV4, QSizeGrip
18)
19
20from PyQt4.QtCore import pyqtSignal as Signal
21from PyQt4.QtCore import pyqtProperty as Property
22
23from PyQt4.QtCore import (
24    Qt, QObject, QPoint, QSize, QRect, QEventLoop, QEvent, QModelIndex
25)
26
27
28from ..gui.framelesswindow import FramelessWindow
29from ..gui.lineedit import LineEdit
30from ..gui.tooltree import ToolTree, FlattenedTreeItemModel
31from ..gui.utils import StyledWidget_paintEvent
32
33from ..registry.qt import QtWidgetRegistry
34
35from ..resources import icon_loader
36
37log = logging.getLogger(__name__)
38
39
40class SearchWidget(LineEdit):
41    def __init__(self, parent=None, **kwargs):
42        LineEdit.__init__(self, parent, **kwargs)
43        self.__setupUi()
44
45    def __setupUi(self):
46        icon = icon_loader().get("icons/Search.svg")
47        action = QAction(icon, "Search", self)
48        self.setAction(action, LineEdit.LeftPosition)
49
50
51class MenuStackWidget(QStackedWidget):
52    """Stack widget for the menu pages (ToolTree instances).
53    """
54
55    def sizeHint(self):
56        """Size hint is the median size hint of the widgets contained
57        within.
58
59        """
60        default_size = QSize(200, 400)
61        widget_hints = [default_size]
62        for i in range(self.count()):
63            w = self.widget(i)
64            if isinstance(w, ToolTree):
65                hint = self.__sizeHintForTreeView(w.view())
66            else:
67                hint = w.sizeHint()
68            widget_hints.append(hint)
69        width = max([s.width() for s in widget_hints])
70        # Take the median for the height
71        height = numpy.median([s.height() for s in widget_hints])
72
73        return QSize(width, int(height))
74
75    def __sizeHintForTreeView(self, view):
76        hint = view.sizeHint()
77        model = view.model()
78
79        count = model.rowCount()
80        width = view.sizeHintForColumn(0)
81
82        if count:
83            height = view.sizeHintForRow(0)
84            height = height * count
85        else:
86            height = hint.height()
87
88        return QSize(max(width, hint.width()), max(height, hint.height()))
89
90
91class TabButton(QToolButton):
92    def __init__(self, parent=None, **kwargs):
93        QToolButton.__init__(self, parent, **kwargs)
94        self.setToolButtonStyle(Qt.ToolButtonIconOnly)
95        self.setCheckable(True)
96
97        self.__flat = True
98
99    def setFlat(self, flat):
100        if self.__flat != flat:
101            self.__flat = flat
102            self.update()
103
104    def flat(self):
105        return self.__flat
106
107    flat_ = Property(bool, fget=flat, fset=setFlat,
108                     designable=True)
109
110    def paintEvent(self, event):
111        if self.__flat:
112            # Use default widget background/border styling.
113            StyledWidget_paintEvent(self, event)
114
115            opt = QStyleOptionToolButton()
116            self.initStyleOption(opt)
117            p = QStylePainter(self)
118            p.drawControl(QStyle.CE_ToolButtonLabel, opt)
119        else:
120            QToolButton.paintEvent(self, event)
121
122
123_Tab = \
124    namedtuple(
125        "_Tab",
126        ["text",
127         "icon",
128         "toolTip",
129         "button",
130         "data",
131         "palette"])
132
133
134class TabBarWidget(QWidget):
135    """A tab bar widget using tool buttons as tabs.
136
137    """
138    # TODO: A uniform size box layout.
139
140    currentChanged = Signal(int)
141
142    def __init__(self, parent=None, **kwargs):
143        QWidget.__init__(self, parent, **kwargs)
144        layout = QHBoxLayout()
145        layout.setContentsMargins(0, 0, 0, 0)
146        layout.setSpacing(0)
147        self.setLayout(layout)
148
149        self.setSizePolicy(QSizePolicy.Expanding,
150                           QSizePolicy.Fixed)
151        self.__tabs = []
152        self.__currentIndex = -1
153        self.__group = QButtonGroup(self, exclusive=True)
154        self.__group.buttonPressed[QAbstractButton].connect(
155            self.__onButtonPressed
156        )
157
158    def count(self):
159        """Return the number of tabs in the widget.
160        """
161        return len(self.__tabs)
162
163    def addTab(self, text, icon=None, toolTip=None):
164        """Add a tab and return it's index.
165        """
166        return self.insertTab(self.count(), text, icon, toolTip)
167
168    def insertTab(self, index, text, icon, toolTip):
169        """Insert a tab at `index`
170        """
171        button = TabButton(self, objectName="tab-button")
172        button.setSizePolicy(QSizePolicy.Expanding,
173                             QSizePolicy.Expanding)
174
175        self.__group.addButton(button)
176        tab = _Tab(text, icon, toolTip, button, None, None)
177        self.layout().insertWidget(index, button)
178
179        self.__tabs.insert(index, tab)
180        self.__updateTab(index)
181
182        if self.currentIndex() == -1:
183            self.setCurrentIndex(0)
184        return index
185
186    def removeTab(self, index):
187        if index >= 0 and index < self.count():
188            self.layout().takeItem(index)
189            tab = self.__tabs.pop(index)
190            self.__group.removeButton(tab.button)
191            tab.button.deleteLater()
192
193            if self.currentIndex() == index:
194                if self.count():
195                    self.setCurrentIndex(max(index - 1, 0))
196                else:
197                    self.setCurrentIndex(-1)
198
199    def setTabIcon(self, index, icon):
200        """Set the `icon` for tab at `index`.
201        """
202        self.__tabs[index] = self.__tabs[index]._replace(icon=icon)
203        self.__updateTab(index)
204
205    def setTabToolTip(self, index, toolTip):
206        """Set `toolTip` for tab at `index`.
207        """
208        self.__tabs[index] = self.__tabs[index]._replace(toolTip=toolTip)
209        self.__updateTab(index)
210
211    def setTabText(self, index, text):
212        """Set tab `text` for tab at `index`
213        """
214        self.__tabs[index] = self.__tabs[index]._replace(text=text)
215        self.__updateTab(index)
216
217    def setTabPalette(self, index, palette):
218        """Set the tab button palette.
219        """
220        self.__tabs[index] = self.__tabs[index]._replace(palette=palette)
221        self.__updateTab(index)
222
223    def setCurrentIndex(self, index):
224        if self.__currentIndex != index:
225            self.__currentIndex = index
226
227            if index != -1:
228                self.__tabs[index].button.setChecked(True)
229
230            self.currentChanged.emit(index)
231
232    def button(self, index):
233        """Return the `TabButton` instance for index.
234        """
235        return self.__tabs[index].button
236
237    def currentIndex(self):
238        """Return the current index.
239        """
240        return self.__currentIndex
241
242    def __updateTab(self, index):
243        """Update the tab button.
244        """
245        tab = self.__tabs[index]
246        b = tab.button
247
248        if tab.text:
249            b.setText(tab.text)
250
251        if tab.icon is not None and not tab.icon.isNull():
252            b.setIcon(tab.icon)
253
254        if tab.toolTip:
255            b.setToolTip(tab.toolTip)
256
257        if tab.palette:
258            b.setPalette(tab.palette)
259
260    def __onButtonPressed(self, button):
261        for i, tab in enumerate(self.__tabs):
262            if tab.button is button:
263                self.setCurrentIndex(i)
264                break
265
266
267class PagedMenu(QWidget):
268    """Tabed container for `ToolTree` instances.
269    """
270    triggered = Signal(QAction)
271    hovered = Signal(QAction)
272
273    currentChanged = Signal(int)
274
275    def __init__(self, parent=None, **kwargs):
276        QWidget.__init__(self, parent, **kwargs)
277
278        self.__pages = []
279        self.__currentIndex = -1
280
281        layout = QVBoxLayout()
282        layout.setContentsMargins(0, 0, 0, 0)
283        layout.setSpacing(0)
284
285        self.__tab = TabBarWidget(self)
286        self.__tab.setFixedHeight(25)
287        self.__tab.currentChanged.connect(self.setCurrentIndex)
288
289        self.__stack = MenuStackWidget(self)
290
291        layout.addWidget(self.__tab)
292        layout.addWidget(self.__stack)
293
294        self.setLayout(layout)
295
296    def addPage(self, page, title, icon=None, toolTip=None):
297        """Add a `page` to the menu and return its index.
298        """
299        return self.insertPage(self.count(), page, title, icon, toolTip)
300
301    def insertPage(self, index, page, title, icon=None, toolTip=None):
302        """Insert `page` at `index`.
303        """
304        page.triggered.connect(self.triggered)
305        page.hovered.connect(self.hovered)
306
307        self.__stack.insertWidget(index, page)
308        self.__tab.insertTab(index, title, icon, toolTip)
309        return index
310
311    def page(self, index):
312        """Return the page at index.
313        """
314        return self.__stack.widget(index)
315
316    def removePage(self, index):
317        """Remove the page at `index`.
318        """
319        page = self.__stack.widget(index)
320        page.triggered.disconnect(self.triggered)
321        page.hovered.disconnect(self.hovered)
322
323        self.__stack.removeWidget(page)
324        self.__tab.removeTab(index)
325
326    def count(self):
327        """Return the number of pages.
328        """
329        return self.__stack.count()
330
331    def setCurrentIndex(self, index):
332        """Set the current page index.
333        """
334        if self.__currentIndex != index:
335            self.__currentIndex = index
336            self.__tab.setCurrentIndex(index)
337            self.__stack.setCurrentIndex(index)
338            self.currentChanged.emit(index)
339
340    def currentIndex(self):
341        """Return the index of the current page.
342        """
343        return self.__currentIndex
344
345    def setCurrentPage(self, page):
346        """Set `page` to be the current shown page.
347        """
348        index = self.__stack.indexOf(page)
349        self.setCurrentIndex(index)
350
351    def currentPage(self):
352        """Return the current page.
353        """
354        return self.__stack.currentWidget()
355
356    def indexOf(self, page):
357        """Return the index of `page`.
358        """
359        return self.__stack.indexOf(page)
360
361    def tabButton(self, index):
362        """Return the tab button instance for index.
363        """
364        return self.__tab.button(index)
365
366
367class ItemDisableFilter(QSortFilterProxyModel):
368    def __init__(self, parent=None):
369        QSortFilterProxyModel.__init__(self, parent)
370
371        self.__filterFunc = None
372
373    def setFilterFunc(self, func):
374        if not (isinstance(func, Callable) or func is None):
375            raise ValueError("A callable object or None expected.")
376
377        if self.__filterFunc != func:
378            self.__filterFunc = func
379            # Mark the whole model as changed.
380            self.dataChanged.emit(self.index(0, 0),
381                                  self.index(self.rowCount(), 0))
382
383    def flags(self, index):
384        source = self.mapToSource(index)
385        flags = source.flags()
386
387        if self.__filterFunc is not None:
388            enabled = flags & Qt.ItemIsEnabled
389            if enabled and not self.__filterFunc(source):
390                flags ^= Qt.ItemIsEnabled
391
392        return flags
393
394
395class MenuPage(ToolTree):
396    def __init__(self, *args, **kwargs):
397        ToolTree.__init__(self, *args, **kwargs)
398
399        # Make sure the initial model is wrapped in a ItemDisableFilter.
400        self.setModel(self.model())
401
402    def setFilterFunc(self, func):
403        proxyModel = self.view().model()
404        proxyModel.setFilterFunc(func)
405
406    def setModel(self, model):
407        proxyModel = ItemDisableFilter(self)
408        proxyModel.setSourceModel(model)
409        ToolTree.setModel(self, proxyModel)
410
411    def setRootIndex(self, index):
412        proxyModel = self.view().model()
413        mappedIndex = proxyModel.mapFromSource(index)
414        ToolTree.setRootIndex(self, mappedIndex)
415
416    def rootIndex(self):
417        proxyModel = self.view().model()
418        return proxyModel.mapToSource(ToolTree.rootIndex(self))
419
420
421class SortFilterProxyModel(QSortFilterProxyModel):
422    def __init__(self, parent=None):
423        QSortFilterProxyModel.__init__(self, parent)
424
425        self.__filterFunc = None
426
427    def setFilterFunc(self, func):
428        if not (isinstance(func, Callable) or func is None):
429            raise ValueError("A callable object or None expected.")
430
431        if self.__filterFunc is not func:
432            self.__filterFunc = func
433            self.invalidateFilter()
434
435    def filterFunc(self):
436        return self.__filterFunc
437
438    def filterAcceptsRow(self, row, parent=QModelIndex()):
439        accepted = QSortFilterProxyModel.filterAcceptsRow(self, row, parent)
440        if accepted and self.__filterFunc is not None:
441            model = self.sourceModel()
442            index = model.index(row, self.filterKeyColumn(), parent)
443            return self.__filterFunc(index)
444        else:
445            return accepted
446
447
448class SuggestMenuPage(ToolTree):
449    def __init__(self, *args, **kwargs):
450        ToolTree.__init__(self, *args, **kwargs)
451
452        # Make sure the initial model is wrapped in a FlattenedTreeItemModel.
453        self.setModel(self.model())
454
455    def setModel(self, model):
456        flat = FlattenedTreeItemModel(self)
457        flat.setSourceModel(model)
458        flat.setFlatteningMode(flat.InternalNodesDisabled)
459        flat.setFlatteningMode(flat.LeavesOnly)
460        proxy = SortFilterProxyModel(self)
461        proxy.setFilterCaseSensitivity(False)
462        proxy.setSourceModel(flat)
463        ToolTree.setModel(self, proxy)
464        self.ensureCurrent()
465
466    def setFilterFixedString(self, pattern):
467        proxy = self.view().model()
468        proxy.setFilterFixedString(pattern)
469        self.ensureCurrent()
470
471    def setFilterRegExp(self, pattern):
472        filter_proxy = self.view().model()
473        filter_proxy.setFilterRegExp(pattern)
474        self.ensureCurrent()
475
476    def setFilterWildCard(self, pattern):
477        filter_proxy = self.view().model()
478        filter_proxy.setFilterWildCard(pattern)
479        self.ensureCurrent()
480
481    def setFilterFunc(self, func):
482        filter_proxy = self.view().model()
483        filter_proxy.setFilterFunc(func)
484
485
486class QuickMenu(FramelessWindow):
487    """A quick menu popup for the widgets.
488
489    The widgets are set using setModel which must be a
490    model as returned by QtWidgetRegistry.model()
491
492    """
493
494    triggered = Signal(QAction)
495    hovered = Signal(QAction)
496
497    def __init__(self, parent=None, **kwargs):
498        FramelessWindow.__init__(self, parent, **kwargs)
499        self.setWindowFlags(Qt.Popup)
500
501        self.__filterFunc = None
502
503        self.__setupUi()
504
505        self.__loop = None
506        self.__model = QStandardItemModel()
507        self.__triggeredAction = None
508
509    def __setupUi(self):
510        self.setLayout(QVBoxLayout(self))
511        self.layout().setContentsMargins(6, 6, 6, 6)
512
513        self.__frame = QFrame(self, objectName="menu-frame")
514        layout = QVBoxLayout()
515        layout.setContentsMargins(1, 1, 1, 1)
516        layout.setSpacing(2)
517        self.__frame.setLayout(layout)
518
519        self.layout().addWidget(self.__frame)
520
521        self.__pages = PagedMenu(self, objectName="paged-menu")
522        self.__pages.currentChanged.connect(self.setCurrentIndex)
523        self.__pages.triggered.connect(self.triggered)
524        self.__pages.hovered.connect(self.hovered)
525
526        self.__frame.layout().addWidget(self.__pages)
527
528        self.__search = SearchWidget(self, objectName="search-line")
529
530        self.__search.setPlaceholderText(
531            self.tr("Search for widget or select from the list.")
532        )
533
534        self.layout().addWidget(self.__search)
535        self.setSizePolicy(QSizePolicy.Fixed,
536                           QSizePolicy.Expanding)
537
538        self.__suggestPage = SuggestMenuPage(self, objectName="suggest-page")
539        self.__suggestPage.setActionRole(QtWidgetRegistry.WIDGET_ACTION_ROLE)
540        self.__suggestPage.setIcon(icon_loader().get("icons/Search.svg"))
541
542        if sys.platform == "darwin":
543            view = self.__suggestPage.view()
544            view.verticalScrollBar().setAttribute(Qt.WA_MacMiniSize, True)
545            # Don't show the focus frame because it expands into the tab
546            # bar at the top.
547            view.setAttribute(Qt.WA_MacShowFocusRect, False)
548
549        self.addPage(self.tr("Quick Search"), self.__suggestPage)
550
551        self.__search.textEdited.connect(self.__on_textEdited)
552
553        self.__navigator = ItemViewKeyNavigator(self)
554        self.__navigator.setView(self.__suggestPage.view())
555        self.__search.installEventFilter(self.__navigator)
556
557        self.__grip = WindowSizeGrip(self)
558        self.__grip.raise_()
559
560    def setSizeGripEnabled(self, enabled):
561        """Enable the resizing of the menu with a size grip in a bottom
562        right corner (enabled by default).
563
564        """
565        if bool(enabled) != bool(self.__grip):
566            if self.__grip:
567                self.__grip.deleteLater()
568                self.__grip = None
569            else:
570                self.__grip = WindowSizeGrip(self)
571                self.__grip.raise_()
572
573    def sizeGripEnabled(self):
574        """Is the size grip enabled.
575        """
576        return bool(self.__grip)
577
578    def addPage(self, name, page):
579        """Add the page and return it's index.
580        """
581        icon = page.icon()
582
583        tip = name
584        if page.toolTip():
585            tip = page.toolTip()
586
587        index = self.__pages.addPage(page, name, icon, tip)
588        # TODO: get the background.
589
590        # Route the page's signals
591        page.triggered.connect(self.__onTriggered)
592        page.hovered.connect(self.hovered)
593
594        # Install event filter to process key presses.
595        page.view().installEventFilter(self)
596
597        return index
598
599    def createPage(self, index):
600        page = MenuPage(self)
601        view = page.view()
602        delegate = WidgetItemDelegate(view)
603        view.setItemDelegate(delegate)
604
605        page.setModel(index.model())
606        page.setRootIndex(index)
607
608        view = page.view()
609
610        if sys.platform == "darwin":
611            view.verticalScrollBar().setAttribute(Qt.WA_MacMiniSize, True)
612            # Don't show the focus frame because it expands into the tab
613            # bar at the top.
614            view.setAttribute(Qt.WA_MacShowFocusRect, False)
615
616        name = unicode(index.data(Qt.DisplayRole))
617        page.setTitle(name)
618
619        icon = index.data(Qt.DecorationRole).toPyObject()
620        if isinstance(icon, QIcon):
621            page.setIcon(icon)
622
623        page.setToolTip(index.data(Qt.ToolTipRole).toPyObject())
624        return page
625
626    def setModel(self, model):
627        root = model.invisibleRootItem()
628        for i in range(root.rowCount()):
629            item = root.child(i)
630            index = item.index()
631            page = self.createPage(index)
632            page.setActionRole(QtWidgetRegistry.WIDGET_ACTION_ROLE)
633            i = self.addPage(page.title(), page)
634
635            brush = index.data(QtWidgetRegistry.BACKGROUND_ROLE)
636
637            if brush.isValid():
638                brush = brush.toPyObject()
639                button = self.__pages.tabButton(i)
640                palette = button.palette()
641                button.setStyleSheet(
642                    "TabButton {\n"
643                    "    qproperty-flat_: false;"
644                    "    background-color: %s;\n"
645                    "    border: none;\n"
646                    "}\n"
647                    "TabButton:checked {\n"
648                    "    border: 1px solid %s;\n"
649                    "}" % (brush.color().name(),
650                           palette.color(palette.Mid).name())
651                )
652
653        self.__model = model
654        self.__suggestPage.setModel(model)
655
656    def setFilterFunc(self, func):
657        if func != self.__filterFunc:
658            self.__filterFunc = func
659            for i in range(0, self.__pages.count()):
660                self.__pages.page(i).setFilterFunc(func)
661
662    def popup(self, pos=None):
663        """Popup the menu at `pos` (in screen coordinates)..
664        """
665        if pos is None:
666            pos = QPoint()
667
668        self.__search.setText("")
669        self.__suggestPage.setFilterFixedString("")
670
671        self.ensurePolished()
672
673        if self.testAttribute(Qt.WA_Resized) and self.sizeGripEnabled():
674            size = self.size()
675        else:
676            size = self.sizeHint()
677
678        desktop = QApplication.desktop()
679        screen_geom = desktop.availableGeometry(pos)
680
681        # Adjust the size to fit inside the screen.
682        if size.height() > screen_geom.height():
683            size.setHeight(screen_geom.height())
684        if size.width() > screen_geom.width():
685            size.setWidth(screen_geom.width())
686
687        geom = QRect(pos, size)
688
689        if geom.top() < screen_geom.top():
690            geom.setTop(screen_geom.top())
691
692        if geom.left() < screen_geom.left():
693            geom.setLeft(screen_geom.left())
694
695        bottom_margin = screen_geom.bottom() - geom.bottom()
696        right_margin = screen_geom.right() - geom.right()
697        if bottom_margin < 0:
698            # Falls over the bottom of the screen, move it up.
699            geom.translate(0, bottom_margin)
700
701        # TODO: right to left locale
702        if right_margin < 0:
703            # Falls over the right screen edge, move the menu to the
704            # other side of pos.
705            geom.translate(-size.width(), 0)
706
707        self.setGeometry(geom)
708
709        self.show()
710
711    def exec_(self, pos=None):
712        self.popup(pos)
713        self.setFocus(Qt.PopupFocusReason)
714
715        self.__triggeredAction = None
716        self.__loop = QEventLoop()
717        self.__loop.exec_()
718        self.__loop.deleteLater()
719        self.__loop = None
720
721        action = self.__triggeredAction
722        self.__triggeredAction = None
723        return action
724
725    def hideEvent(self, event):
726        FramelessWindow.hideEvent(self, event)
727        if self.__loop:
728            self.__loop.exit()
729
730    def setCurrentPage(self, page):
731        self.__pages.setCurrentPage(page)
732
733    def setCurrentIndex(self, index):
734        self.__pages.setCurrentIndex(index)
735
736    def __onTriggered(self, action):
737        """Re-emit the action from the page.
738        """
739        self.__triggeredAction = action
740
741        # Hide and exit the event loop if necessary.
742        self.hide()
743        self.triggered.emit(action)
744
745    def __on_textEdited(self, text):
746        self.__suggestPage.setFilterFixedString(text)
747        self.__pages.setCurrentPage(self.__suggestPage)
748
749    def triggerSearch(self):
750        self.__pages.setCurrentPage(self.__suggestPage)
751        self.__search.setFocus(Qt.ShortcutFocusReason)
752
753        # Make sure that the first enabled item is set current.
754        self.__suggestPage.ensureCurrent()
755
756    def keyPressEvent(self, event):
757        if event.text():
758            # Ignore modifiers, ...
759            self.__search.setFocus(Qt.ShortcutFocusReason)
760            self.setCurrentIndex(0)
761            self.__search.keyPressEvent(event)
762
763        FramelessWindow.keyPressEvent(self, event)
764        event.accept()
765
766    def eventFilter(self, obj, event):
767        if isinstance(obj, QTreeView):
768            etype = event.type()
769            if etype == QEvent.KeyPress:
770                # ignore modifiers non printable characters, Enter, ...
771                if event.text() and event.key() not in \
772                        [Qt.Key_Enter, Qt.Key_Return]:
773                    self.__search.setFocus(Qt.ShortcutFocusReason)
774                    self.setCurrentIndex(0)
775                    self.__search.keyPressEvent(event)
776                    return True
777
778        return FramelessWindow.eventFilter(self, obj, event)
779
780
781class WidgetItemDelegate(QStyledItemDelegate):
782    def __init__(self, parent=None):
783        QStyledItemDelegate.__init__(self, parent)
784
785    def sizeHint(self, option, index):
786        option = QStyleOptionViewItemV4(option)
787        self.initStyleOption(option, index)
788        size = QStyledItemDelegate.sizeHint(self, option, index)
789        size.setHeight(max(size.height(), 25))
790        return size
791
792
793class ItemViewKeyNavigator(QObject):
794    def __init__(self, parent=None):
795        QObject.__init__(self, parent)
796        self.__view = None
797
798    def setView(self, view):
799        if self.__view != view:
800            self.__view = view
801
802    def view(self):
803        return self.__view
804
805    def eventFilter(self, obj, event):
806        etype = event.type()
807        if etype == QEvent.KeyPress:
808            key = event.key()
809            if key == Qt.Key_Down:
810                self.moveCurrent(1, 0)
811                return True
812            elif key == Qt.Key_Up:
813                self.moveCurrent(-1, 0)
814                return True
815            elif key == Qt.Key_Tab:
816                self.moveCurrent(0, 1)
817                return  True
818            elif key == Qt.Key_Enter or key == Qt.Key_Return:
819                self.activateCurrent()
820                return True
821
822        return QObject.eventFilter(self, obj, event)
823
824    def moveCurrent(self, rows, columns=0):
825        """Move the current index by rows, columns.
826        """
827        if self.__view is not None:
828            view = self.__view
829            model = view.model()
830
831            curr = view.currentIndex()
832            curr_row, curr_col = curr.row(), curr.column()
833
834            sign = 1 if rows >= 0 else -1
835            row = curr_row + rows
836
837            row_count = model.rowCount()
838            for i in range(row_count):
839                index = model.index((row + sign * i) % row_count, 0)
840                if index.flags() & Qt.ItemIsEnabled:
841                    view.setCurrentIndex(index)
842                    break
843            # TODO: move by columns
844
845    def activateCurrent(self):
846        """Activate the current index.
847        """
848        if self.__view is not None:
849            curr = self.__view.currentIndex()
850            if curr.isValid():
851                # TODO: Does this work
852                self.__view.activated.emit(curr)
853
854    def ensureCurrent(self):
855        """Ensure the view has a current item if one is available.
856        """
857        if self.__view is not None:
858            model = self.__view.model()
859            curr = self.__view.currentIndex()
860            if not curr.isValid():
861                for i in range(model.rowCount()):
862                    index = model.index(i, 0)
863                    if index.flags() & Qt.ItemIsEnabled:
864                        self.__view.setCurrentIndex(index)
865                        break
866
867
868class WindowSizeGrip(QSizeGrip):
869    """Automatically positioning SizeGrip.
870    """
871    def __init__(self, parent):
872        QSizeGrip.__init__(self, parent)
873        self.__corner = Qt.BottomRightCorner
874
875        self.resize(self.sizeHint())
876
877        self.__updatePos()
878
879    def setCorner(self, corner):
880        """Set the corner where the size grip should position itself.
881        """
882        if corner not in [Qt.TopLeftCorner, Qt.TopRightCorner,
883                          Qt.BottomLeftCorner, Qt.BottomRightCorner]:
884            raise ValueError("Qt.Corner flag expected")
885
886        if self.__corner != corner:
887            self.__corner = corner
888            self.__updatePos()
889
890    def corner(self):
891        """Return the corner where the size grip is positioned.
892        """
893        return self.__corner
894
895    def eventFilter(self, obj, event):
896        if obj is self.window():
897            if event.type() == QEvent.Resize:
898                self.__updatePos()
899
900        return QSizeGrip.eventFilter(self, obj, event)
901
902    def showEvent(self, event):
903        if self.window() != self.parent():
904            log.error("%s: Can only show on a top level window.",
905                      type(self).__name__)
906
907        return QSizeGrip.showEvent(self, event)
908
909    def __updatePos(self):
910        window = self.window()
911
912        if window is not self.parent():
913            return
914
915        corner = self.__corner
916        size = self.sizeHint()
917
918        window_geom = window.geometry()
919        window_size = window_geom.size()
920
921        if corner in [Qt.TopLeftCorner, Qt.BottomLeftCorner]:
922            x = 0
923        else:
924            x = window_geom.width() - size.width()
925
926        if corner in [Qt.TopLeftCorner, Qt.TopRightCorner]:
927            y = 0
928        else:
929            y = window_size.height() - size.height()
930
931        self.move(x, y)
Note: See TracBrowser for help on using the repository browser.