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.

Line 
1"""
2==========
3Quick Menu
4==========
5
6A :class:`QuickMenu` widget provides lists of actions organized in tabs
7with a quick search functionality.
8
9"""
10
11import sys
12import logging
13
14from collections import namedtuple, Callable
15
16import numpy
17
18from PyQt4.QtGui import (
19    QWidget, QFrame, QToolButton, QAbstractButton, QAction, QIcon, QTreeView,
20    QButtonGroup, QStackedWidget, QHBoxLayout, QVBoxLayout, QSizePolicy,
21    QStandardItemModel, QSortFilterProxyModel, QStyleOptionToolButton,
22    QStylePainter, QStyle, QApplication, QStyledItemDelegate,
23    QStyleOptionViewItemV4, QSizeGrip
24)
25
26from PyQt4.QtCore import pyqtSignal as Signal
27from PyQt4.QtCore import pyqtProperty as Property
28
29from PyQt4.QtCore import (
30    Qt, QObject, QPoint, QSize, QRect, QEventLoop, QEvent, QModelIndex
31)
32
33
34from ..gui.framelesswindow import FramelessWindow
35from ..gui.lineedit import LineEdit
36from ..gui.tooltree import ToolTree, FlattenedTreeItemModel
37from ..gui.utils import StyledWidget_paintEvent
38
39from ..registry.qt import QtWidgetRegistry
40
41from ..resources import icon_loader
42
43log = logging.getLogger(__name__)
44
45
46class MenuPage(ToolTree):
47    """
48    A menu page in a :class:`QuickMenu` widget, showing a list of actions.
49    Shown actions can be disabled by setting a filtering function using the
50    :ref:`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
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):
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        """
108        proxyModel = self.view().model()
109        proxyModel.setFilterFunc(func)
110
111    def setModel(self, model):
112        """
113        Reimplemented from :ref:`ToolTree.setModel`.
114        """
115        proxyModel = ItemDisableFilter(self)
116        proxyModel.setSourceModel(model)
117        ToolTree.setModel(self, proxyModel)
118
119    def setRootIndex(self, index):
120        """
121        Reimplemented from :ref:`ToolTree.setRootIndex`
122        """
123        proxyModel = self.view().model()
124        mappedIndex = proxyModel.mapFromSource(index)
125        ToolTree.setRootIndex(self, mappedIndex)
126
127    def rootIndex(self):
128        """
129        Reimplemented from :ref:`ToolTree.rootIndex`
130        """
131        proxyModel = self.view().model()
132        return proxyModel.mapToSource(ToolTree.rootIndex(self))
133
134
135class ItemDisableFilter(QSortFilterProxyModel):
136    """
137    An filter proxy model used to disable selected items based on
138    a filtering function.
139
140    """
141    def __init__(self, parent=None):
142        QSortFilterProxyModel.__init__(self, parent)
143
144        self.__filterFunc = None
145
146    def setFilterFunc(self, func):
147        """
148        Set the filtering function.
149        """
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):
160        """
161        Reimplemented from :class:`QSortFilterProxyModel.flags`
162        """
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):
175    """
176    A MenuMage for the QuickMenu widget supporting item filtering
177    (searching).
178
179    """
180    def __init__(self, *args, **kwargs):
181        MenuPage.__init__(self, *args, **kwargs)
182
183    def setModel(self, model):
184        """
185        Reimplmemented from :ref:`MenuPage.setModel`.
186        """
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):
198        """
199        Set the fixed string filtering pattern. Only items which contain the
200        `pattern` string will be shown.
201
202        """
203        proxy = self.view().model()
204        proxy.setFilterFixedString(pattern)
205        self.ensureCurrent()
206
207    def setFilterRegExp(self, pattern):
208        """
209        Set the regular expression filtering pattern. Only items matching
210        the `pattern` expression will be shown.
211
212        """
213        filter_proxy = self.view().model()
214        filter_proxy.setFilterRegExp(pattern)
215        self.ensureCurrent()
216
217    def setFilterWildCard(self, pattern):
218        """
219        Set a wildcard filtering pattern.
220        """
221        filter_proxy = self.view().model()
222        filter_proxy.setFilterWildCard(pattern)
223        self.ensureCurrent()
224
225    def setFilterFunc(self, func):
226        """
227        Set a filtering function.
228        """
229        filter_proxy = self.view().model()
230        filter_proxy.setFilterFunc(func)
231
232
233class SortFilterProxyModel(QSortFilterProxyModel):
234    """
235    An filter proxy model used to filter items based on a filtering
236    function.
237
238    """
239    def __init__(self, parent=None):
240        QSortFilterProxyModel.__init__(self, parent)
241
242        self.__filterFunc = None
243
244    def setFilterFunc(self, func):
245        """
246        Set the filtering function.
247        """
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
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):
280    """
281    Stack widget for the menu pages.
282    """
283
284    def sizeHint(self):
285        """
286        Size hint is the maximum width and median height of the widgets
287        contained in the stack.
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
301        height = numpy.median([s.height() for s in widget_hints])
302
303        return QSize(width, int(height))
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
315        else:
316            height = hint.height()
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
364# TODO: ..application.canvastooldock.QuickCategoryToolbar is very similar,
365#       to TobBarWidget. Maybe common functionality could factored our.
366
367class TabBarWidget(QWidget):
368    """
369    A tab bar widget using tool buttons as tabs.
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):
392        """
393        Return the number of tabs in the widget.
394        """
395        return len(self.__tabs)
396
397    def addTab(self, text, icon=None, toolTip=None):
398        """
399        Add a new tab and return it's index.
400        """
401        return self.insertTab(self.count(), text, icon, toolTip)
402
403    def insertTab(self, index, text, icon=None, toolTip=None):
404        """
405        Insert a tab at `index`
406        """
407        button = TabButton(self, objectName="tab-button")
408        button.setSizePolicy(QSizePolicy.Expanding,
409                             QSizePolicy.Expanding)
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):
423        """
424        Remove a tab at `index`.
425        """
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):
439        """
440        Set the `icon` for tab at `index`.
441        """
442        self.__tabs[index] = self.__tabs[index]._replace(icon=icon)
443        self.__updateTab(index)
444
445    def setTabToolTip(self, index, toolTip):
446        """
447        Set `toolTip` for tab at `index`.
448        """
449        self.__tabs[index] = self.__tabs[index]._replace(toolTip=toolTip)
450        self.__updateTab(index)
451
452    def setTabText(self, index, text):
453        """
454        Set tab `text` for tab at `index`
455        """
456        self.__tabs[index] = self.__tabs[index]._replace(text=text)
457        self.__updateTab(index)
458
459    def setTabPalette(self, index, palette):
460        """
461        Set the tab button palette.
462        """
463        self.__tabs[index] = self.__tabs[index]._replace(palette=palette)
464        self.__updateTab(index)
465
466    def setCurrentIndex(self, index):
467        """
468        Set the current tab index.
469        """
470        if self.__currentIndex != index:
471            self.__currentIndex = index
472
473            if index != -1:
474                self.__tabs[index].button.setChecked(True)
475
476            self.currentChanged.emit(index)
477
478    def currentIndex(self):
479        """
480        Return the current index.
481        """
482        return self.__currentIndex
483
484    def button(self, index):
485        """
486        Return the `TabButton` instance for index.
487        """
488        return self.__tabs[index].button
489
490    def __updateTab(self, index):
491        """
492        Update the tab button.
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):
517    """
518    Tabbed container for :class:`MenuPage` instances.
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):
547        """
548        Add a `page` to the menu and return its index.
549        """
550        return self.insertPage(self.count(), page, title, icon, toolTip)
551
552    def insertPage(self, index, page, title, icon=None, toolTip=None):
553        """
554        Insert `page` at `index`.
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)
561        return index
562
563    def page(self, index):
564        """
565        Return the page at index.
566        """
567        return self.__stack.widget(index)
568
569    def removePage(self, index):
570        """
571        Remove the page at `index`.
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):
581        """
582        Return the number of pages.
583        """
584        return self.__stack.count()
585
586    def setCurrentIndex(self, index):
587        """
588        Set the current page index.
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):
597        """
598        Return the index of the current page.
599        """
600        return self.__currentIndex
601
602    def setCurrentPage(self, page):
603        """
604        Set `page` to be the current shown page.
605        """
606        index = self.__stack.indexOf(page)
607        self.setCurrentIndex(index)
608
609    def currentPage(self):
610        """
611        Return the current page.
612        """
613        return self.__stack.currentWidget()
614
615    def indexOf(self, page):
616        """
617        Return the index of `page`.
618        """
619        return self.__stack.indexOf(page)
620
621    def tabButton(self, index):
622        """
623        Return the tab button instance for index.
624        """
625        return self.__tab.button(index)
626
627
628class QuickMenu(FramelessWindow):
629    """
630    A quick menu popup for the widgets.
631
632    The widgets are set using :ref:`setModel` which must be a
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
644        self.__filterFunc = None
645
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
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
692        self.addPage(self.tr("Quick Search"), self.__suggestPage)
693
694        self.__search.textEdited.connect(self.__on_textEdited)
695
696        self.__navigator = ItemViewKeyNavigator(self)
697        self.__navigator.setView(self.__suggestPage.view())
698        self.__search.installEventFilter(self.__navigator)
699
700        self.__grip = WindowSizeGrip(self)
701        self.__grip.raise_()
702
703    def setSizeGripEnabled(self, enabled):
704        """
705        Enable the resizing of the menu with a size grip in a bottom
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):
718        """
719        Is the size grip enabled.
720        """
721        return bool(self.__grip)
722
723    def addPage(self, name, page):
724        """
725        Add the page and return it's index.
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)
739
740        # Install event filter to process key presses.
741        page.view().installEventFilter(self)
742
743        return index
744
745    def createPage(self, index):
746        """
747        Create a new page based on the contents of an index
748        (:class:`QModeIndex`) item.
749
750        """
751        page = MenuPage(self)
752        view = page.view()
753        delegate = WidgetItemDelegate(view)
754        view.setItemDelegate(delegate)
755
756        page.setModel(index.model())
757        page.setRootIndex(index)
758
759        view = page.view()
760
761        if sys.platform == "darwin":
762            view.verticalScrollBar().setAttribute(Qt.WA_MacMiniSize, True)
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)
766
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):
778        """
779        Set the model containing the actions.
780        """
781        root = model.invisibleRootItem()
782        for i in range(root.rowCount()):
783            item = root.child(i)
784            index = item.index()
785            page = self.createPage(index)
786            page.setActionRole(QtWidgetRegistry.WIDGET_ACTION_ROLE)
787            i = self.addPage(page.title(), page)
788
789            brush = index.data(QtWidgetRegistry.BACKGROUND_ROLE)
790
791            if brush.isValid():
792                brush = brush.toPyObject()
793                button = self.__pages.tabButton(i)
794                palette = button.palette()
795                button.setStyleSheet(
796                    "TabButton {\n"
797                    "    qproperty-flat_: false;\n"
798                    "    background-color: %s;\n"
799                    "    border: none;\n"
800                    "}\n"
801                    "TabButton:checked {\n"
802                    "    border: 1px solid %s;\n"
803                    "}" % (brush.color().name(),
804                           palette.color(palette.Mid).name())
805                )
806
807        self.__model = model
808        self.__suggestPage.setModel(model)
809
810    def setFilterFunc(self, func):
811        """
812        Set a filter function.
813        """
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)
818
819    def popup(self, pos=None):
820        """
821        Popup the menu at `pos` (in screen coordinates).
822        """
823        if pos is None:
824            pos = QPoint()
825
826        self.__search.setText("")
827        self.__suggestPage.setFilterFixedString("")
828
829        self.ensurePolished()
830
831        if self.testAttribute(Qt.WA_Resized) and self.sizeGripEnabled():
832            size = self.size()
833        else:
834            size = self.sizeHint()
835
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)
871        self.setFocus(Qt.PopupFocusReason)
872
873        self.__triggeredAction = None
874        self.__loop = QEventLoop()
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):
889        """
890        Set the current shown page to `page`.
891        """
892        self.__pages.setCurrentPage(page)
893
894    def setCurrentIndex(self, index):
895        """
896        Set the current page index.
897        """
898        self.__pages.setCurrentIndex(index)
899
900    def __onTriggered(self, action):
901        """
902        Re-emit the action from the page.
903        """
904        self.__triggeredAction = action
905
906        # Hide and exit the event loop if necessary.
907        self.hide()
908        self.triggered.emit(action)
909
910    def __on_textEdited(self, text):
911        self.__suggestPage.setFilterFixedString(text)
912        self.__pages.setCurrentPage(self.__suggestPage)
913
914    def triggerSearch(self):
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        """
920        self.__pages.setCurrentPage(self.__suggestPage)
921        self.__search.setFocus(Qt.ShortcutFocusReason)
922
923        # Make sure that the first enabled item is set current.
924        self.__suggestPage.ensureCurrent()
925
926    def keyPressEvent(self, event):
927        if event.text():
928            # Ignore modifiers, ...
929            self.__search.setFocus(Qt.ShortcutFocusReason)
930            self.setCurrentIndex(0)
931            self.__search.keyPressEvent(event)
932
933        FramelessWindow.keyPressEvent(self, event)
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
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):
995        """
996        Move the current index by rows, columns.
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):
1017        """
1018        Activate the current index.
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):
1027        """
1028        Ensure the view has a current item if one is available.
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
1039
1040
1041class WindowSizeGrip(QSizeGrip):
1042    """
1043    Automatically positioning :class:`QSizeGrip`.
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):
1054        """
1055        Set the corner where the size grip should position itself.
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):
1066        """
1067        Return the corner where the size grip is positioned.
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.