source: orange/Orange/OrangeCanvas/document/quickmenu.py @ 11370:03af193a01d3

Revision 11370:03af193a01d3, 28.6 KB checked in by Ales Erjavec <ales.erjavec@…>, 14 months ago (diff)

Moved 'icon' and 'title' property from ToolTree widget, to quickmenu.MenuPage.

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