source: orange/Orange/OrangeCanvas/document/quickmenu.py @ 11489:4c0367175371

Revision 11489:4c0367175371, 33.0 KB checked in by Ales Erjavec <ales.erjavec@…>, 12 months ago (diff)

Added a 'searchText' parameter to quick menu popup/exec methods.

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, QKeySequence
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, searchText=""):
826        """
827        Popup the menu at `pos` (in screen coordinates). 'Search' text field
828        is initialized with `searchText` if provided.
829        """
830        if pos is None:
831            pos = QPoint()
832
833        self.__search.setText(searchText)
834        self.__suggestPage.setFilterFixedString(searchText)
835
836        self.ensurePolished()
837
838        if self.testAttribute(Qt.WA_Resized) and self.sizeGripEnabled():
839            size = self.size()
840        else:
841            size = self.sizeHint()
842
843        desktop = QApplication.desktop()
844        screen_geom = desktop.availableGeometry(pos)
845
846        # Adjust the size to fit inside the screen.
847        if size.height() > screen_geom.height():
848            size.setHeight(screen_geom.height())
849        if size.width() > screen_geom.width():
850            size.setWidth(screen_geom.width())
851
852        geom = QRect(pos, size)
853
854        if geom.top() < screen_geom.top():
855            geom.setTop(screen_geom.top())
856
857        if geom.left() < screen_geom.left():
858            geom.setLeft(screen_geom.left())
859
860        bottom_margin = screen_geom.bottom() - geom.bottom()
861        right_margin = screen_geom.right() - geom.right()
862        if bottom_margin < 0:
863            # Falls over the bottom of the screen, move it up.
864            geom.translate(0, bottom_margin)
865
866        # TODO: right to left locale
867        if right_margin < 0:
868            # Falls over the right screen edge, move the menu to the
869            # other side of pos.
870            geom.translate(-size.width(), 0)
871
872        self.setGeometry(geom)
873
874        self.show()
875
876        if searchText:
877            self.setFocusProxy(self.__search)
878        else:
879            self.setFocusProxy(None)
880
881    def exec_(self, pos=None, searchText=""):
882        """
883        Execute the menu at position `pos` (in global screen coordinates).
884        Return the triggered :class:`QAction` or `None` if no action was
885        triggered. 'Search' text field is initialized with `searchText` if
886        provided.
887
888        """
889        self.popup(pos, searchText)
890        self.setFocus(Qt.PopupFocusReason)
891
892        self.__triggeredAction = None
893        self.__loop = QEventLoop()
894        self.__loop.exec_()
895        self.__loop.deleteLater()
896        self.__loop = None
897
898        action = self.__triggeredAction
899        self.__triggeredAction = None
900        return action
901
902    def hideEvent(self, event):
903        """
904        Reimplemented from :class:`QWidget`
905        """
906        FramelessWindow.hideEvent(self, event)
907        if self.__loop:
908            self.__loop.exit()
909
910    def setCurrentPage(self, page):
911        """
912        Set the current shown page to `page`.
913        """
914        self.__pages.setCurrentPage(page)
915
916    def setCurrentIndex(self, index):
917        """
918        Set the current page index.
919        """
920        self.__pages.setCurrentIndex(index)
921
922    def __onTriggered(self, action):
923        """
924        Re-emit the action from the page.
925        """
926        self.__triggeredAction = action
927
928        # Hide and exit the event loop if necessary.
929        self.hide()
930        self.triggered.emit(action)
931
932    def __on_textEdited(self, text):
933        self.__suggestPage.setFilterFixedString(text)
934        self.__pages.setCurrentPage(self.__suggestPage)
935
936    def triggerSearch(self):
937        """
938        Trigger action search. This changes to current page to the
939        'Suggest' page and sets the keyboard focus to the search line edit.
940
941        """
942        self.__pages.setCurrentPage(self.__suggestPage)
943        self.__search.setFocus(Qt.ShortcutFocusReason)
944
945        # Make sure that the first enabled item is set current.
946        self.__suggestPage.ensureCurrent()
947
948    def keyPressEvent(self, event):
949        if event.text():
950            # Ignore modifiers, ...
951            self.__search.setFocus(Qt.ShortcutFocusReason)
952            self.setCurrentIndex(0)
953            self.__search.keyPressEvent(event)
954
955        FramelessWindow.keyPressEvent(self, event)
956        event.accept()
957
958    def event(self, event):
959        if event.type() == QEvent.ShortcutOverride:
960            log.debug("Overriding shortcuts")
961            event.accept()
962            return True
963        return FramelessWindow.event(self, event)
964
965    def eventFilter(self, obj, event):
966        if isinstance(obj, QTreeView):
967            etype = event.type()
968            if etype == QEvent.KeyPress:
969                # ignore modifiers non printable characters, Enter, ...
970                if event.text() and event.key() not in \
971                        [Qt.Key_Enter, Qt.Key_Return]:
972                    self.__search.setFocus(Qt.ShortcutFocusReason)
973                    self.setCurrentIndex(0)
974                    self.__search.keyPressEvent(event)
975                    return True
976
977        return FramelessWindow.eventFilter(self, obj, event)
978
979
980class WidgetItemDelegate(QStyledItemDelegate):
981    def __init__(self, parent=None):
982        QStyledItemDelegate.__init__(self, parent)
983
984    def sizeHint(self, option, index):
985        option = QStyleOptionViewItemV4(option)
986        self.initStyleOption(option, index)
987        size = QStyledItemDelegate.sizeHint(self, option, index)
988        size.setHeight(max(size.height(), 25))
989        return size
990
991
992class ItemViewKeyNavigator(QObject):
993    """
994    A event filter class listening to key press events and responding
995    by moving 'currentItem` on a :class:`QListView`.
996
997    """
998    def __init__(self, parent=None):
999        QObject.__init__(self, parent)
1000        self.__view = None
1001
1002    def setView(self, view):
1003        """
1004        Set the QListView.
1005        """
1006        if self.__view != view:
1007            self.__view = view
1008
1009    def view(self):
1010        """
1011        Return the view
1012        """
1013        return self.__view
1014
1015    def eventFilter(self, obj, event):
1016        etype = event.type()
1017        if etype == QEvent.KeyPress:
1018            key = event.key()
1019            if key == Qt.Key_Down:
1020                self.moveCurrent(1, 0)
1021                return True
1022            elif key == Qt.Key_Up:
1023                self.moveCurrent(-1, 0)
1024                return True
1025            elif key == Qt.Key_Tab:
1026                self.moveCurrent(0, 1)
1027                return  True
1028            elif key == Qt.Key_Enter or key == Qt.Key_Return:
1029                self.activateCurrent()
1030                return True
1031
1032        return QObject.eventFilter(self, obj, event)
1033
1034    def moveCurrent(self, rows, columns=0):
1035        """
1036        Move the current index by rows, columns.
1037        """
1038        if self.__view is not None:
1039            view = self.__view
1040            model = view.model()
1041
1042            curr = view.currentIndex()
1043            curr_row, curr_col = curr.row(), curr.column()
1044
1045            sign = 1 if rows >= 0 else -1
1046            row = curr_row + rows
1047
1048            row_count = model.rowCount()
1049            for i in range(row_count):
1050                index = model.index((row + sign * i) % row_count, 0)
1051                if index.flags() & Qt.ItemIsEnabled:
1052                    view.setCurrentIndex(index)
1053                    break
1054            # TODO: move by columns
1055
1056    def activateCurrent(self):
1057        """
1058        Activate the current index.
1059        """
1060        if self.__view is not None:
1061            curr = self.__view.currentIndex()
1062            if curr.isValid():
1063                # TODO: Does this work? We are emitting signals that are
1064                # defined by a different class. This might break some things.
1065                # Should we just send the keyPress events to the view, and let
1066                # it handle them.
1067                self.__view.activated.emit(curr)
1068
1069    def ensureCurrent(self):
1070        """
1071        Ensure the view has a current item if one is available.
1072        """
1073        if self.__view is not None:
1074            model = self.__view.model()
1075            curr = self.__view.currentIndex()
1076            if not curr.isValid():
1077                for i in range(model.rowCount()):
1078                    index = model.index(i, 0)
1079                    if index.flags() & Qt.ItemIsEnabled:
1080                        self.__view.setCurrentIndex(index)
1081                        break
1082
1083
1084class WindowSizeGrip(QSizeGrip):
1085    """
1086    Automatically positioning :class:`QSizeGrip`.
1087    The widget automatically maintains its position in the window
1088    corner during resize events.
1089
1090    """
1091    def __init__(self, parent):
1092        QSizeGrip.__init__(self, parent)
1093        self.__corner = Qt.BottomRightCorner
1094
1095        self.resize(self.sizeHint())
1096
1097        self.__updatePos()
1098
1099    def setCorner(self, corner):
1100        """
1101        Set the corner (:class:`Qt.Corner`) where the size grip should
1102        position itself.
1103
1104        """
1105        if corner not in [Qt.TopLeftCorner, Qt.TopRightCorner,
1106                          Qt.BottomLeftCorner, Qt.BottomRightCorner]:
1107            raise ValueError("Qt.Corner flag expected")
1108
1109        if self.__corner != corner:
1110            self.__corner = corner
1111            self.__updatePos()
1112
1113    def corner(self):
1114        """
1115        Return the corner where the size grip is positioned.
1116        """
1117        return self.__corner
1118
1119    def eventFilter(self, obj, event):
1120        if obj is self.window():
1121            if event.type() == QEvent.Resize:
1122                self.__updatePos()
1123
1124        return QSizeGrip.eventFilter(self, obj, event)
1125
1126    def showEvent(self, event):
1127        if self.window() != self.parent():
1128            log.error("%s: Can only show on a top level window.",
1129                      type(self).__name__)
1130
1131        return QSizeGrip.showEvent(self, event)
1132
1133    def __updatePos(self):
1134        window = self.window()
1135
1136        if window is not self.parent():
1137            return
1138
1139        corner = self.__corner
1140        size = self.sizeHint()
1141
1142        window_geom = window.geometry()
1143        window_size = window_geom.size()
1144
1145        if corner in [Qt.TopLeftCorner, Qt.BottomLeftCorner]:
1146            x = 0
1147        else:
1148            x = window_geom.width() - size.width()
1149
1150        if corner in [Qt.TopLeftCorner, Qt.TopRightCorner]:
1151            y = 0
1152        else:
1153            y = window_size.height() - size.height()
1154
1155        self.move(x, y)
Note: See TracBrowser for help on using the repository browser.