source: orange/Orange/OrangeCanvas/document/quickmenu.py @ 11420:58c976c2d17f

Revision 11420:58c976c2d17f, 32.4 KB checked in by Ales Erjavec <ales.erjavec@…>, 13 months ago (diff)

Added sphinx documentation for QuickMenu

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