source: orange/Orange/OrangeCanvas/document/quickmenu.py @ 11371:31dcb6f6e0a0

Revision 11371:31dcb6f6e0a0, 31.3 KB checked in by Ales Erjavec <ales.erjavec@…>, 14 months ago (diff)

Docstring fixes in document package.

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