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

RevLine 
[11108]1"""
[11370]2==========
3Quick Menu
4==========
5
6A :class:`QuickMenu` widget provides lists of actions organized in tabs
7with a quick search functionality.
[11108]8
9"""
[11370]10
[11131]11import sys
12import logging
[11108]13
[11229]14from collections import namedtuple, Callable
[11108]15
[11223]16import numpy
17
[11108]18from PyQt4.QtGui import (
[11187]19    QWidget, QFrame, QToolButton, QAbstractButton, QAction, QIcon, QTreeView,
[11108]20    QButtonGroup, QStackedWidget, QHBoxLayout, QVBoxLayout, QSizePolicy,
21    QStandardItemModel, QSortFilterProxyModel, QStyleOptionToolButton,
[11187]22    QStylePainter, QStyle, QApplication, QStyledItemDelegate,
[11223]23    QStyleOptionViewItemV4, QSizeGrip
[11108]24)
25
26from PyQt4.QtCore import pyqtSignal as Signal
27from PyQt4.QtCore import pyqtProperty as Property
28
29from PyQt4.QtCore import (
[11164]30    Qt, QObject, QPoint, QSize, QRect, QEventLoop, QEvent, QModelIndex
[11108]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
[11370]46class MenuPage(ToolTree):
47    """
[11371]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
[11420]50    :func:`setFilterFunc`.
[11371]51
[11370]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
[11420]82    title_ = Property(unicode, fget=title, fset=setTitle,
83                      doc="Title of the page.")
[11370]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
[11420]99    icon_ = Property(QIcon, fget=icon, fset=setIcon,
100                     doc="Page icon")
[11370]101
102    def setFilterFunc(self, func):
[11371]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        """
[11370]110        proxyModel = self.view().model()
111        proxyModel.setFilterFunc(func)
112
113    def setModel(self, model):
[11371]114        """
[11420]115        Reimplemented from :func:`ToolTree.setModel`.
[11371]116        """
[11370]117        proxyModel = ItemDisableFilter(self)
118        proxyModel.setSourceModel(model)
119        ToolTree.setModel(self, proxyModel)
120
121    def setRootIndex(self, index):
[11371]122        """
[11420]123        Reimplemented from :func:`ToolTree.setRootIndex`
[11371]124        """
[11370]125        proxyModel = self.view().model()
126        mappedIndex = proxyModel.mapFromSource(index)
127        ToolTree.setRootIndex(self, mappedIndex)
128
129    def rootIndex(self):
[11371]130        """
[11420]131        Reimplemented from :func:`ToolTree.rootIndex`
[11371]132        """
[11370]133        proxyModel = self.view().model()
134        return proxyModel.mapToSource(ToolTree.rootIndex(self))
135
136
137class ItemDisableFilter(QSortFilterProxyModel):
[11371]138    """
139    An filter proxy model used to disable selected items based on
140    a filtering function.
141
142    """
[11370]143    def __init__(self, parent=None):
144        QSortFilterProxyModel.__init__(self, parent)
145
146        self.__filterFunc = None
147
148    def setFilterFunc(self, func):
[11371]149        """
150        Set the filtering function.
151        """
[11370]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):
[11371]162        """
163        Reimplemented from :class:`QSortFilterProxyModel.flags`
164        """
[11370]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):
[11371]177    """
178    A MenuMage for the QuickMenu widget supporting item filtering
179    (searching).
180
181    """
[11370]182    def __init__(self, *args, **kwargs):
183        MenuPage.__init__(self, *args, **kwargs)
184
185    def setModel(self, model):
[11371]186        """
187        Reimplmemented from :ref:`MenuPage.setModel`.
188        """
[11370]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):
[11371]200        """
201        Set the fixed string filtering pattern. Only items which contain the
202        `pattern` string will be shown.
203
204        """
[11370]205        proxy = self.view().model()
206        proxy.setFilterFixedString(pattern)
207        self.ensureCurrent()
208
209    def setFilterRegExp(self, pattern):
[11371]210        """
211        Set the regular expression filtering pattern. Only items matching
212        the `pattern` expression will be shown.
213
214        """
[11370]215        filter_proxy = self.view().model()
216        filter_proxy.setFilterRegExp(pattern)
217        self.ensureCurrent()
218
219    def setFilterWildCard(self, pattern):
[11371]220        """
221        Set a wildcard filtering pattern.
222        """
[11370]223        filter_proxy = self.view().model()
224        filter_proxy.setFilterWildCard(pattern)
225        self.ensureCurrent()
226
227    def setFilterFunc(self, func):
[11371]228        """
229        Set a filtering function.
230        """
[11370]231        filter_proxy = self.view().model()
232        filter_proxy.setFilterFunc(func)
233
234
235class SortFilterProxyModel(QSortFilterProxyModel):
[11371]236    """
237    An filter proxy model used to filter items based on a filtering
238    function.
239
240    """
[11370]241    def __init__(self, parent=None):
242        QSortFilterProxyModel.__init__(self, parent)
243
244        self.__filterFunc = None
245
246    def setFilterFunc(self, func):
[11371]247        """
248        Set the filtering function.
249        """
[11370]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
[11108]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):
[11370]282    """
283    Stack widget for the menu pages.
[11108]284    """
285
286    def sizeHint(self):
[11370]287        """
288        Size hint is the maximum width and median height of the widgets
289        contained in the stack.
[11108]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
[11223]303        height = numpy.median([s.height() for s in widget_hints])
304
305        return QSize(width, int(height))
[11108]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
[11110]317        else:
318            height = hint.height()
[11108]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
[11371]366# TODO: ..application.canvastooldock.QuickCategoryToolbar is very similar,
367#       to TobBarWidget. Maybe common functionality could factored our.
368
[11108]369class TabBarWidget(QWidget):
[11371]370    """
371    A tab bar widget using tool buttons as tabs.
[11108]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):
[11371]394        """
395        Return the number of tabs in the widget.
[11108]396        """
397        return len(self.__tabs)
398
399    def addTab(self, text, icon=None, toolTip=None):
[11371]400        """
401        Add a new tab and return it's index.
[11108]402        """
403        return self.insertTab(self.count(), text, icon, toolTip)
404
[11371]405    def insertTab(self, index, text, icon=None, toolTip=None):
406        """
407        Insert a tab at `index`
[11108]408        """
409        button = TabButton(self, objectName="tab-button")
[11131]410        button.setSizePolicy(QSizePolicy.Expanding,
411                             QSizePolicy.Expanding)
[11108]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):
[11371]425        """
426        Remove a tab at `index`.
427        """
[11108]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):
[11371]441        """
442        Set the `icon` for tab at `index`.
[11108]443        """
444        self.__tabs[index] = self.__tabs[index]._replace(icon=icon)
445        self.__updateTab(index)
446
447    def setTabToolTip(self, index, toolTip):
[11371]448        """
449        Set `toolTip` for tab at `index`.
[11108]450        """
451        self.__tabs[index] = self.__tabs[index]._replace(toolTip=toolTip)
452        self.__updateTab(index)
453
454    def setTabText(self, index, text):
[11371]455        """
456        Set tab `text` for tab at `index`
[11108]457        """
458        self.__tabs[index] = self.__tabs[index]._replace(text=text)
459        self.__updateTab(index)
460
461    def setTabPalette(self, index, palette):
[11371]462        """
463        Set the tab button palette.
[11108]464        """
465        self.__tabs[index] = self.__tabs[index]._replace(palette=palette)
466        self.__updateTab(index)
467
468    def setCurrentIndex(self, index):
[11371]469        """
470        Set the current tab index.
471        """
[11108]472        if self.__currentIndex != index:
473            self.__currentIndex = index
[11131]474
475            if index != -1:
476                self.__tabs[index].button.setChecked(True)
477
[11108]478            self.currentChanged.emit(index)
479
[11371]480    def currentIndex(self):
481        """
482        Return the current index.
483        """
484        return self.__currentIndex
485
[11108]486    def button(self, index):
[11371]487        """
488        Return the `TabButton` instance for index.
[11108]489        """
490        return self.__tabs[index].button
491
[11371]492    def __updateTab(self, index):
[11108]493        """
[11371]494        Update the tab button.
[11108]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):
[11370]519    """
520    Tabbed container for :class:`MenuPage` instances.
[11108]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):
[11371]549        """
550        Add a `page` to the menu and return its index.
[11108]551        """
552        return self.insertPage(self.count(), page, title, icon, toolTip)
553
554    def insertPage(self, index, page, title, icon=None, toolTip=None):
[11371]555        """
556        Insert `page` at `index`.
[11108]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)
[11131]563        return index
[11108]564
565    def page(self, index):
[11371]566        """
567        Return the page at index.
[11108]568        """
569        return self.__stack.widget(index)
570
571    def removePage(self, index):
[11371]572        """
573        Remove the page at `index`.
[11108]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):
[11371]583        """
584        Return the number of pages.
[11108]585        """
586        return self.__stack.count()
587
588    def setCurrentIndex(self, index):
[11371]589        """
590        Set the current page index.
[11108]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):
[11371]599        """
600        Return the index of the current page.
[11108]601        """
602        return self.__currentIndex
603
604    def setCurrentPage(self, page):
[11371]605        """
606        Set `page` to be the current shown page.
[11108]607        """
608        index = self.__stack.indexOf(page)
609        self.setCurrentIndex(index)
610
611    def currentPage(self):
[11371]612        """
613        Return the current page.
[11108]614        """
615        return self.__stack.currentWidget()
616
617    def indexOf(self, page):
[11371]618        """
619        Return the index of `page`.
[11108]620        """
621        return self.__stack.indexOf(page)
622
[11131]623    def tabButton(self, index):
[11371]624        """
625        Return the tab button instance for index.
[11131]626        """
627        return self.__tab.button(index)
628
[11108]629
630class QuickMenu(FramelessWindow):
[11371]631    """
632    A quick menu popup for the widgets.
[11108]633
[11420]634    The widgets are set using :func:`QuickMenu.setModel` which must be a
635    model as returned by :func:`QtWidgetRegistry.model`
[11108]636
637    """
638
[11420]639    #: An action has been triggered in the menu.
[11108]640    triggered = Signal(QAction)
[11420]641
642    #: An action has been hovered in the menu
[11108]643    hovered = Signal(QAction)
644
645    def __init__(self, parent=None, **kwargs):
646        FramelessWindow.__init__(self, parent, **kwargs)
647        self.setWindowFlags(Qt.Popup)
648
[11229]649        self.__filterFunc = None
650
[11108]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
[11229]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
[11164]697        self.addPage(self.tr("Quick Search"), self.__suggestPage)
[11108]698
[11229]699        self.__search.textEdited.connect(self.__on_textEdited)
[11108]700
701        self.__navigator = ItemViewKeyNavigator(self)
702        self.__navigator.setView(self.__suggestPage.view())
703        self.__search.installEventFilter(self.__navigator)
704
[11223]705        self.__grip = WindowSizeGrip(self)
706        self.__grip.raise_()
707
708    def setSizeGripEnabled(self, enabled):
[11371]709        """
710        Enable the resizing of the menu with a size grip in a bottom
[11223]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):
[11371]723        """
724        Is the size grip enabled.
[11223]725        """
726        return bool(self.__grip)
727
[11108]728    def addPage(self, name, page):
[11371]729        """
[11420]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
[11108]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)
[11187]745
[11420]746        # Install event filter to intercept key presses.
[11187]747        page.view().installEventFilter(self)
748
[11108]749        return index
750
751    def createPage(self, index):
[11371]752        """
753        Create a new page based on the contents of an index
754        (:class:`QModeIndex`) item.
755
756        """
[11229]757        page = MenuPage(self)
[11187]758        view = page.view()
759        delegate = WidgetItemDelegate(view)
760        view.setItemDelegate(delegate)
761
[11108]762        page.setModel(index.model())
763        page.setRootIndex(index)
764
[11131]765        view = page.view()
766
767        if sys.platform == "darwin":
768            view.verticalScrollBar().setAttribute(Qt.WA_MacMiniSize, True)
[11187]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)
[11131]772
[11108]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):
[11371]784        """
785        Set the model containing the actions.
786        """
[11108]787        root = model.invisibleRootItem()
788        for i in range(root.rowCount()):
789            item = root.child(i)
[11131]790            index = item.index()
791            page = self.createPage(index)
[11108]792            page.setActionRole(QtWidgetRegistry.WIDGET_ACTION_ROLE)
[11131]793            i = self.addPage(page.title(), page)
794
[11133]795            brush = index.data(QtWidgetRegistry.BACKGROUND_ROLE)
796
[11131]797            if brush.isValid():
798                brush = brush.toPyObject()
799                button = self.__pages.tabButton(i)
800                palette = button.palette()
801                button.setStyleSheet(
[11223]802                    "TabButton {\n"
[11371]803                    "    qproperty-flat_: false;\n"
[11131]804                    "    background-color: %s;\n"
805                    "    border: none;\n"
806                    "}\n"
[11223]807                    "TabButton:checked {\n"
[11131]808                    "    border: 1px solid %s;\n"
809                    "}" % (brush.color().name(),
810                           palette.color(palette.Mid).name())
811                )
812
[11108]813        self.__model = model
814        self.__suggestPage.setModel(model)
815
[11164]816    def setFilterFunc(self, func):
[11371]817        """
818        Set a filter function.
819        """
[11229]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)
[11164]824
[11108]825    def popup(self, pos=None):
[11371]826        """
827        Popup the menu at `pos` (in screen coordinates).
[11108]828        """
829        if pos is None:
830            pos = QPoint()
831
[11131]832        self.__search.setText("")
833        self.__suggestPage.setFilterFixedString("")
834
[11108]835        self.ensurePolished()
[11223]836
837        if self.testAttribute(Qt.WA_Resized) and self.sizeGripEnabled():
838            size = self.size()
839        else:
840            size = self.sizeHint()
841
[11108]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):
[11420]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        """
[11108]882        self.popup(pos)
[11131]883        self.setFocus(Qt.PopupFocusReason)
884
[11108]885        self.__triggeredAction = None
[11209]886        self.__loop = QEventLoop()
[11108]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):
[11420]896        """
897        Reimplemented from :class:`QWidget`
898        """
[11108]899        FramelessWindow.hideEvent(self, event)
900        if self.__loop:
901            self.__loop.exit()
902
903    def setCurrentPage(self, page):
[11371]904        """
905        Set the current shown page to `page`.
906        """
[11108]907        self.__pages.setCurrentPage(page)
908
909    def setCurrentIndex(self, index):
[11371]910        """
911        Set the current page index.
912        """
[11108]913        self.__pages.setCurrentIndex(index)
914
915    def __onTriggered(self, action):
[11371]916        """
917        Re-emit the action from the page.
[11108]918        """
919        self.__triggeredAction = action
920
921        # Hide and exit the event loop if necessary.
922        self.hide()
923        self.triggered.emit(action)
924
[11187]925    def __on_textEdited(self, text):
926        self.__suggestPage.setFilterFixedString(text)
927        self.__pages.setCurrentPage(self.__suggestPage)
928
[11108]929    def triggerSearch(self):
[11371]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        """
[11187]935        self.__pages.setCurrentPage(self.__suggestPage)
[11108]936        self.__search.setFocus(Qt.ShortcutFocusReason)
[11131]937
[11108]938        # Make sure that the first enabled item is set current.
939        self.__suggestPage.ensureCurrent()
940
941    def keyPressEvent(self, event):
[11131]942        if event.text():
[11187]943            # Ignore modifiers, ...
[11131]944            self.__search.setFocus(Qt.ShortcutFocusReason)
945            self.setCurrentIndex(0)
946            self.__search.keyPressEvent(event)
947
[11108]948        FramelessWindow.keyPressEvent(self, event)
[11187]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
[11108]976
977
978class ItemViewKeyNavigator(QObject):
[11420]979    """
980    A event filter class listening to key press events and responding
981    by moving 'currentItem` on a :class:`QListView`.
982
983    """
[11108]984    def __init__(self, parent=None):
985        QObject.__init__(self, parent)
986        self.__view = None
987
988    def setView(self, view):
[11420]989        """
990        Set the QListView.
991        """
[11108]992        if self.__view != view:
993            self.__view = view
994
995    def view(self):
[11420]996        """
997        Return the view
998        """
[11108]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):
[11371]1021        """
1022        Move the current index by rows, columns.
[11108]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):
[11371]1043        """
1044        Activate the current index.
[11108]1045        """
1046        if self.__view is not None:
1047            curr = self.__view.currentIndex()
1048            if curr.isValid():
[11420]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.
[11108]1053                self.__view.activated.emit(curr)
1054
1055    def ensureCurrent(self):
[11371]1056        """
1057        Ensure the view has a current item if one is available.
[11108]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
[11223]1068
1069
1070class WindowSizeGrip(QSizeGrip):
[11371]1071    """
1072    Automatically positioning :class:`QSizeGrip`.
[11420]1073    The widget automatically maintains its position in the window
1074    corner during resize events.
1075
[11223]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):
[11371]1086        """
[11420]1087        Set the corner (:class:`Qt.Corner`) where the size grip should
1088        position itself.
1089
[11223]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):
[11371]1100        """
1101        Return the corner where the size grip is positioned.
[11223]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.