source: orange/Orange/OrangeCanvas/application/canvasmain.py @ 11250:3b499a088cfc

Revision 11250:3b499a088cfc, 49.2 KB checked in by Ales Erjavec <ales.erjavec@…>, 18 months ago (diff)

Added user settings/preferences dialog.

Line 
1"""
2Orange Canvas Main Window
3
4"""
5import os
6import sys
7import logging
8import operator
9
10import pkg_resources
11
12from PyQt4.QtGui import (
13    QMainWindow, QWidget, QAction, QActionGroup, QMenu, QMenuBar, QDialog,
14    QFileDialog, QMessageBox, QVBoxLayout, QSizePolicy, QColor, QKeySequence,
15    QIcon, QToolBar, QToolButton, QDockWidget, QDesktopServices,
16    QApplication
17)
18
19from PyQt4.QtCore import (
20    Qt, QEvent, QSize, QUrl, QSettings, QTimer, QFile
21)
22
23from PyQt4.QtCore import pyqtProperty as Property
24
25
26from ..gui.dropshadow import DropShadowFrame
27from ..gui.dock import CollapsibleDockWidget
28from ..gui.quickhelp import QuickHelpTipEvent
29from ..gui.utils import message_critical, message_question, message_information
30
31from .canvastooldock import CanvasToolDock, QuickCategoryToolbar
32from .aboutdialog import AboutDialog
33from .schemeinfo import SchemeInfoDialog
34from .outputview import OutputText
35from .settings import UserSettingsDialog
36from ..document.schemeedit import SchemeEditWidget
37
38from ..scheme import widgetsscheme
39
40from . import welcomedialog
41from ..preview import previewdialog, previewmodel
42
43from .. import config
44
45from . import tutorials
46
47log = logging.getLogger(__name__)
48
49# TODO: Orange Version in the base link
50
51BASE_LINK = "http://orange.biolab.si/"
52
53LINKS = \
54    {"start-using": BASE_LINK + "start-using/",
55     "tutorial": BASE_LINK + "tutorial/",
56     "reference": BASE_LINK + "doc/"
57     }
58
59
60def style_icons(widget, standard_pixmap):
61    """Return the Qt standard pixmap icon.
62    """
63    return QIcon(widget.style().standardPixmap(standard_pixmap))
64
65
66def canvas_icons(name):
67    """Return the named canvas icon.
68    """
69    icon_file = QFile("canvas_icons:" + name)
70    if icon_file.exists():
71        return QIcon("canvas_icons:" + name)
72    else:
73        return QIcon(pkg_resources.resource_filename(
74                      config.__name__,
75                      os.path.join("icons", name))
76                     )
77
78
79class FakeToolBar(QToolBar):
80    """A Toolbar with no contents (used to reserve top and bottom margins
81    on the main window).
82
83    """
84    def __init__(self, *args, **kwargs):
85        QToolBar.__init__(self, *args, **kwargs)
86        self.setFloatable(False)
87        self.setMovable(False)
88
89        # Don't show the tool bar action in the main window's
90        # context menu.
91        self.toggleViewAction().setVisible(False)
92
93    def paintEvent(self, event):
94        # Do nothing.
95        pass
96
97
98class CanvasMainWindow(QMainWindow):
99    SETTINGS_VERSION = 2
100
101    def __init__(self, *args):
102        QMainWindow.__init__(self, *args)
103
104        self.__scheme_margins_enabled = True
105        self.__document_title = "untitled"
106
107        self.widget_registry = None
108        self.last_scheme_dir = None
109
110        self.recent_schemes = config.recent_schemes()
111
112        self.setup_actions()
113        self.setup_ui()
114        self.setup_menu()
115
116        self.restore()
117
118        self.resize(800, 600)
119
120    def setup_ui(self):
121        """Setup main canvas ui
122        """
123        QSettings.setDefaultFormat(QSettings.IniFormat)
124        settings = QSettings()
125        settings.beginGroup("mainwindow")
126
127        log.info("Setting up Canvas main window.")
128
129        # Two dummy tool bars to reserve space
130        self.__dummy_top_toolbar = FakeToolBar(
131                            objectName="__dummy_top_toolbar")
132        self.__dummy_bottom_toolbar = FakeToolBar(
133                            objectName="__dummy_bottom_toolbar")
134
135        self.__dummy_top_toolbar.setFixedHeight(20)
136        self.__dummy_bottom_toolbar.setFixedHeight(20)
137
138        self.addToolBar(Qt.TopToolBarArea, self.__dummy_top_toolbar)
139        self.addToolBar(Qt.BottomToolBarArea, self.__dummy_bottom_toolbar)
140
141        self.setCorner(Qt.BottomLeftCorner, Qt.LeftDockWidgetArea)
142        self.setCorner(Qt.BottomRightCorner, Qt.RightDockWidgetArea)
143
144        # Create an empty initial scheme inside a container with fixed
145        # margins.
146        w = QWidget()
147        w.setLayout(QVBoxLayout())
148        w.layout().setContentsMargins(20, 0, 10, 0)
149
150        self.scheme_widget = SchemeEditWidget()
151        self.scheme_widget.setScheme(widgetsscheme.WidgetsScheme())
152
153        w.layout().addWidget(self.scheme_widget)
154
155        self.setCentralWidget(w)
156
157        # Drop shadow around the scheme document
158        frame = DropShadowFrame(radius=15)
159        frame.setColor(QColor(0, 0, 0, 100))
160        frame.setWidget(self.scheme_widget)
161
162        # Main window title and title icon.
163        self.set_document_title(self.scheme_widget.scheme().title)
164        self.scheme_widget.titleChanged.connect(self.set_document_title)
165        self.scheme_widget.modificationChanged.connect(self.setWindowModified)
166
167        self.setWindowIcon(canvas_icons("Get Started.svg"))
168
169        # QMainWindow's Dock widget
170        self.dock_widget = CollapsibleDockWidget(objectName="main-area-dock")
171        self.dock_widget.setFeatures(QDockWidget.DockWidgetMovable | \
172                                     QDockWidget.DockWidgetClosable)
173
174        self.dock_widget.setAllowedAreas(Qt.LeftDockWidgetArea | \
175                                         Qt.RightDockWidgetArea)
176
177        # Main canvas tool dock (with widget toolbox, common actions.
178        # This is the widget that is shown when the dock is expanded.
179        canvas_tool_dock = CanvasToolDock(objectName="canvas-tool-dock")
180        canvas_tool_dock.setSizePolicy(QSizePolicy.Fixed,
181                                       QSizePolicy.MinimumExpanding)
182
183        # Bottom tool bar
184        self.canvas_toolbar = canvas_tool_dock.toolbar
185        self.canvas_toolbar.setIconSize(QSize(25, 25))
186        self.canvas_toolbar.setFixedHeight(28)
187        self.canvas_toolbar.layout().setSpacing(1)
188
189        # Widgets tool box
190        self.widgets_tool_box = canvas_tool_dock.toolbox
191        self.widgets_tool_box.setObjectName("canvas-toolbox")
192        self.widgets_tool_box.setTabButtonHeight(30)
193        self.widgets_tool_box.setTabIconSize(QSize(26, 26))
194        self.widgets_tool_box.setButtonSize(QSize(64, 84))
195        self.widgets_tool_box.setIconSize(QSize(48, 48))
196
197        self.widgets_tool_box.triggered.connect(
198            self.on_tool_box_widget_activated
199        )
200
201        self.dock_help = canvas_tool_dock.help
202        self.dock_help.setMaximumHeight(150)
203        self.dock_help.document().setDefaultStyleSheet("h3 {color: orange;}")
204
205        self.dock_help_action = canvas_tool_dock.toogleQuickHelpAction()
206        self.dock_help_action.setText(self.tr("Show Help"))
207        self.dock_help_action.setIcon(canvas_icons("Info.svg"))
208
209        self.canvas_tool_dock = canvas_tool_dock
210
211        # Dock contents when collapsed (a quick category tool bar, ...)
212        dock2 = QWidget(objectName="canvas-quick-dock")
213        dock2.setLayout(QVBoxLayout())
214        dock2.layout().setContentsMargins(0, 0, 0, 0)
215        dock2.layout().setSpacing(0)
216        dock2.layout().setSizeConstraint(QVBoxLayout.SetFixedSize)
217
218        self.quick_category = QuickCategoryToolbar()
219        self.quick_category.setButtonSize(QSize(38, 30))
220        self.quick_category.actionTriggered.connect(
221            self.on_quick_category_action
222        )
223
224        tool_actions = self.current_document().toolbarActions()
225
226        (self.canvas_zoom_action, self.canvas_align_to_grid_action,
227         self.canvas_text_action, self.canvas_arrow_action,) = tool_actions
228
229        self.canvas_zoom_action.setIcon(canvas_icons("Search.svg"))
230        self.canvas_align_to_grid_action.setIcon(canvas_icons("Grid.svg"))
231        self.canvas_text_action.setIcon(canvas_icons("Text Size.svg"))
232        self.canvas_arrow_action.setIcon(canvas_icons("Arrow.svg"))
233
234        dock_actions = [self.show_properties_action] + \
235                       tool_actions + \
236                       [self.freeze_action,
237                        self.dock_help_action]
238
239        # Tool bar in the collapsed dock state (has the same actions as
240        # the tool bar in the CanvasToolDock
241        actions_toolbar = QToolBar(orientation=Qt.Vertical)
242        actions_toolbar.setFixedWidth(38)
243        actions_toolbar.layout().setSpacing(0)
244
245        actions_toolbar.setToolButtonStyle(Qt.ToolButtonIconOnly)
246
247        for action in dock_actions:
248            self.canvas_toolbar.addAction(action)
249            button = self.canvas_toolbar.widgetForAction(action)
250            button.setPopupMode(QToolButton.DelayedPopup)
251
252            actions_toolbar.addAction(action)
253            button = actions_toolbar.widgetForAction(action)
254            button.setFixedSize(38, 30)
255            button.setPopupMode(QToolButton.DelayedPopup)
256
257        dock2.layout().addWidget(self.quick_category)
258        dock2.layout().addWidget(actions_toolbar)
259
260        self.dock_widget.setAnimationEnabled(False)
261        self.dock_widget.setExpandedWidget(self.canvas_tool_dock)
262        self.dock_widget.setCollapsedWidget(dock2)
263        self.dock_widget.setExpanded(True)
264
265        self.addDockWidget(Qt.RightDockWidgetArea, self.dock_widget)
266        self.dock_widget.dockLocationChanged.connect(
267            self._on_dock_location_changed
268        )
269
270        self.output_dock = QDockWidget(self.tr("Output"),
271                                       objectName="output-dock")
272        self.output_dock.setAllowedAreas(Qt.BottomDockWidgetArea)
273
274        self.addDockWidget(Qt.BottomDockWidgetArea, self.output_dock)
275        self.output_dock.setFloating(True)
276
277        self.output_dock.hide()
278
279        output_view = OutputText()
280        self.output_dock.setWidget(output_view)
281
282        self.setMinimumSize(600, 500)
283
284    def setup_actions(self):
285        """Initialize main window actions.
286        """
287
288        self.new_action = \
289            QAction(self.tr("New"), self,
290                    objectName="action-new",
291                    toolTip=self.tr("Open a new scheme."),
292                    triggered=self.new_scheme,
293                    shortcut=QKeySequence.New,
294                    icon=canvas_icons("New.svg")
295                    )
296
297        self.open_action = \
298            QAction(self.tr("Open"), self,
299                    objectName="action-open",
300                    toolTip=self.tr("Open a scheme."),
301                    triggered=self.open_scheme,
302                    shortcut=QKeySequence.Open,
303                    icon=canvas_icons("Open.svg")
304                    )
305
306        self.save_action = \
307            QAction(self.tr("Save"), self,
308                    objectName="action-save",
309                    toolTip=self.tr("Save current scheme."),
310                    triggered=self.save_scheme,
311                    shortcut=QKeySequence.Save,
312                    )
313
314        self.save_as_action = \
315            QAction(self.tr("Save As ..."), self,
316                    objectName="action-save-as",
317                    toolTip=self.tr("Save current scheme as."),
318                    triggered=self.save_scheme_as,
319                    shortcut=QKeySequence.SaveAs,
320                    )
321
322        self.quit_action = \
323            QAction(self.tr("Quit"), self,
324                    objectName="quit-action",
325                    toolTip=self.tr("Quit Orange Canvas."),
326                    triggered=self.quit,
327                    menuRole=QAction.QuitRole,
328                    shortcut=QKeySequence.Quit,
329                    )
330
331        self.welcome_action = \
332            QAction(self.tr("Welcome"), self,
333                    objectName="welcome-action",
334                    toolTip=self.tr("Show welcome screen."),
335                    triggered=self.welcome_dialog,
336                    )
337
338        self.get_started_action = \
339            QAction(self.tr("Get Started"), self,
340                    objectName="get-started-action",
341                    toolTip=self.tr("View a 'Getting Started' video."),
342                    triggered=self.get_started,
343                    icon=canvas_icons("Get Started.svg")
344                    )
345
346        self.tutorials_action = \
347            QAction(self.tr("Tutorials"), self,
348                    objectName="tutorial-action",
349                    toolTip=self.tr("Browse tutorials."),
350                    triggered=self.tutorial_scheme,
351                    icon=canvas_icons("Tutorials.svg")
352                    )
353
354        self.documentation_action = \
355            QAction(self.tr("Documentation"), self,
356                    objectName="documentation-action",
357                    toolTip=self.tr("View reference documentation."),
358                    triggered=self.documentation,
359                    icon=canvas_icons("Documentation.svg")
360                    )
361
362        self.about_action = \
363            QAction(self.tr("About"), self,
364                    objectName="about-action",
365                    toolTip=self.tr("Show about dialog."),
366                    triggered=self.open_about,
367                    menuRole=QAction.AboutRole,
368                    )
369
370        # Action group for for recent scheme actions
371        self.recent_scheme_action_group = \
372            QActionGroup(self, exclusive=False,
373                         objectName="recent-action-group",
374                         triggered=self._on_recent_scheme_action)
375
376        self.recent_action = \
377            QAction(self.tr("Browse Recent"), self,
378                    objectName="recent-action",
379                    toolTip=self.tr("Browse and open a recent scheme."),
380                    triggered=self.recent_scheme,
381                    shortcut=QKeySequence(Qt.ControlModifier | \
382                                          (Qt.ShiftModifier | Qt.Key_R)),
383                    icon=canvas_icons("Recent.svg")
384                    )
385
386        self.reload_last_action = \
387            QAction(self.tr("Reload Last Scheme"), self,
388                    objectName="reload-last-action",
389                    toolTip=self.tr("Reload last open scheme."),
390                    triggered=self.reload_last,
391                    shortcut=QKeySequence(Qt.ControlModifier | Qt.Key_R)
392                    )
393
394        self.clear_recent_action = \
395            QAction(self.tr("Clear Menu"), self,
396                    objectName="clear-recent-menu-action",
397                    toolTip=self.tr("Clear recent menu."),
398                    triggered=self.clear_recent_schemes
399                    )
400
401        self.show_properties_action = \
402            QAction(self.tr("Show Properties"), self,
403                    objectName="show-properties-action",
404                    toolTip=self.tr("Show scheme properties."),
405                    triggered=self.show_scheme_properties,
406                    shortcut=QKeySequence(Qt.ControlModifier | Qt.Key_I),
407                    icon=canvas_icons("Document Info.svg")
408                    )
409
410        self.canvas_settings_action = \
411            QAction(self.tr("Settings"), self,
412                    objectName="canvas-settings-action",
413                    toolTip=self.tr("Set application settings."),
414                    triggered=self.open_canvas_settings,
415                    menuRole=QAction.PreferencesRole,
416                    shortcut=QKeySequence.Preferences
417                    )
418
419        self.show_output_action = \
420            QAction(self.tr("Show Output View"), self,
421                    toolTip=self.tr("Show application output."),
422                    triggered=self.show_output_view,
423                    )
424
425        if sys.platform == "darwin":
426            # Actions for native Mac OSX look and feel.
427            self.minimize_action = \
428                QAction(self.tr("Minimize"), self,
429                        triggered=self.showMinimized,
430                        shortcut=QKeySequence(Qt.ControlModifier | Qt.Key_M)
431                        )
432
433            self.zoom_action = \
434                QAction(self.tr("Zoom"), self,
435                        objectName="application-zoom",
436                        triggered=self.toggleMaximized,
437                        )
438
439        self.freeze_action = \
440            QAction(self.tr("Freeze"), self,
441                    objectName="signal-freeze-action",
442                    checkable=True,
443                    toolTip=self.tr("Freeze signal propagation."),
444                    triggered=self.set_signal_freeze,
445                    icon=canvas_icons("Pause.svg")
446                    )
447
448        # Gets assigned in setup_ui (the action is defined in CanvasToolDock)
449        # TODO: This is bad (should be moved here).
450        self.dock_help_action = None
451
452        self.toogle_margins_action = \
453            QAction(self.tr("Show Scheme Margins"), self,
454                    checkable=True,
455                    checked=True,
456                    toolTip=self.tr("Show margins around the scheme view."),
457                    toggled=self.set_scheme_margins_enabled
458                    )
459
460    def setup_menu(self):
461        menu_bar = QMenuBar()
462
463        # File menu
464        file_menu = QMenu(self.tr("&File"), menu_bar)
465        file_menu.addAction(self.new_action)
466        file_menu.addAction(self.open_action)
467        file_menu.addAction(self.reload_last_action)
468
469        # File -> Open Recent submenu
470        self.recent_menu = QMenu(self.tr("Open Recent"), file_menu)
471        file_menu.addMenu(self.recent_menu)
472        file_menu.addSeparator()
473        file_menu.addAction(self.save_action)
474        file_menu.addAction(self.save_as_action)
475        file_menu.addSeparator()
476        file_menu.addAction(self.show_properties_action)
477        file_menu.addAction(self.quit_action)
478
479        self.recent_menu.addAction(self.recent_action)
480
481        # Store the reference to separator for inserting recent
482        # schemes into the menu in `add_recent_scheme`.
483        self.recent_menu_begin = self.recent_menu.addSeparator()
484
485        # Add recent items.
486        for title, filename in self.recent_schemes:
487            action = QAction(title or self.tr("untitled"), self,
488                             toolTip=filename)
489
490            action.setData(filename)
491            self.recent_menu.addAction(action)
492            self.recent_scheme_action_group.addAction(action)
493
494        self.recent_menu.addSeparator()
495        self.recent_menu.addAction(self.clear_recent_action)
496        menu_bar.addMenu(file_menu)
497
498        editor_menus = self.scheme_widget.menuBarActions()
499
500        # WARNING: Hard coded order, should lookup the action text
501        # and determine the proper order
502        self.edit_menu = editor_menus[0].menu()
503        self.widget_menu = editor_menus[1].menu()
504
505        # Edit menu
506        menu_bar.addMenu(self.edit_menu)
507
508        # View menu
509        self.view_menu = QMenu(self.tr("&View"), self)
510        self.toolbox_menu = QMenu(self.tr("Widget Toolbox Style"),
511                                  self.view_menu)
512        self.toolbox_menu_group = \
513            QActionGroup(self, objectName="toolbox-menu-group")
514
515        a1 = self.toolbox_menu.addAction(self.tr("Tool Box"))
516        a2 = self.toolbox_menu.addAction(self.tr("Tool List"))
517        self.toolbox_menu_group.addAction(a1)
518        self.toolbox_menu_group.addAction(a2)
519
520        self.view_menu.addMenu(self.toolbox_menu)
521        self.view_menu.addSeparator()
522        self.view_menu.addAction(self.toogle_margins_action)
523        menu_bar.addMenu(self.view_menu)
524
525        # Options menu
526        self.options_menu = QMenu(self.tr("&Options"), self)
527        self.options_menu.addAction(self.show_output_action)
528#        self.options_menu.addAction("Add-ons")
529#        self.options_menu.addAction("Developers")
530#        self.options_menu.addAction("Run Discovery")
531#        self.options_menu.addAction("Show Canvas Log")
532#        self.options_menu.addAction("Attach Python Console")
533        self.options_menu.addSeparator()
534        self.options_menu.addAction(self.canvas_settings_action)
535
536        # Widget menu
537        menu_bar.addMenu(self.widget_menu)
538
539        if sys.platform == "darwin":
540            # Mac OS X native look and feel.
541            self.window_menu = QMenu(self.tr("Window"), self)
542            self.window_menu.addAction(self.minimize_action)
543            self.window_menu.addAction(self.zoom_action)
544            menu_bar.addMenu(self.window_menu)
545
546        menu_bar.addMenu(self.options_menu)
547
548        # Help menu.
549        self.help_menu = QMenu(self.tr("&Help"), self)
550        self.help_menu.addAction(self.about_action)
551        self.help_menu.addAction(self.welcome_action)
552        self.help_menu.addAction(self.tutorials_action)
553        self.help_menu.addAction(self.documentation_action)
554        menu_bar.addMenu(self.help_menu)
555
556        self.setMenuBar(menu_bar)
557
558    def restore(self):
559        """Restore the main window state from saved settings.
560        """
561        QSettings.setDefaultFormat(QSettings.IniFormat)
562        settings = QSettings()
563        settings.beginGroup("mainwindow")
564
565        state = settings.value("state")
566        if state.isValid():
567            self.restoreState(state.toByteArray(),
568                              version=self.SETTINGS_VERSION)
569
570        self.dock_widget.setExpanded(
571            settings.value("canvasdock/expanded", True).toBool()
572        )
573
574        floatable = settings.value("toolbox-dock-floatable", False).toBool()
575        if floatable:
576            self.dock_widget.setFeatures(self.dock_widget.features() | \
577                                         QDockWidget.DockWidgetFloatable)
578
579        self.widgets_tool_box.setExclusive(
580            settings.value("toolbox-dock-exclusive", False).toBool()
581        )
582
583        self.toogle_margins_action.setChecked(
584            settings.value("scheme-margins-enabled", True).toBool()
585        )
586
587        self.last_scheme_dir = \
588            settings.value("last-scheme-dir", None).toPyObject()
589
590        if self.last_scheme_dir is not None and \
591                not os.path.exists(self.last_scheme_dir):
592            # if directory no longer exists reset the saved location.
593            self.last_scheme_dir = None
594
595        self.canvas_tool_dock.setQuickHelpVisible(
596            settings.value("quick-help/visible", True).toBool()
597        )
598
599    def set_document_title(self, title):
600        """Set the document title (and the main window title). If `title`
601        is an empty string a default 'untitled' placeholder will be used.
602
603        """
604        if self.__document_title != title:
605            self.__document_title = title
606
607            if not title:
608                # TODO: should the default name be platform specific
609                title = self.tr("untitled")
610
611            self.setWindowTitle(title + "[*]")
612
613    def document_title(self):
614        """Return the document title.
615        """
616        return self.__document_title
617
618    def set_widget_registry(self, widget_registry):
619        """Set widget registry.
620        """
621        if self.widget_registry is not None:
622            # Clear the dock widget and popup.
623            pass
624
625        self.widget_registry = widget_registry
626        self.widgets_tool_box.setModel(widget_registry.model())
627        self.quick_category.setModel(widget_registry.model())
628
629        self.scheme_widget.setRegistry(widget_registry)
630
631        # Restore possibly saved widget toolbox tab states
632        settings = QSettings()
633        state = settings.value("canvasmainwindow/widgettoolbox/state",
634                                defaultValue=None)
635        state = state.toPyObject()
636        if state:
637            self.widgets_tool_box.restoreState(state)
638
639    def set_quick_help_text(self, text):
640        self.canvas_tool_dock.help.setText(text)
641
642    def current_document(self):
643        return self.scheme_widget
644
645    def on_tool_box_widget_activated(self, action):
646        """A widget action in the widget toolbox has been activated.
647        """
648        widget_desc = action.data().toPyObject()
649        if widget_desc:
650            scheme_widget = self.current_document()
651            if scheme_widget:
652                scheme_widget.createNewNode(widget_desc)
653
654    def on_quick_category_action(self, action):
655        """The quick category menu action triggered.
656        """
657        category = action.text()
658        for i in range(self.widgets_tool_box.count()):
659            cat_act = self.widgets_tool_box.tabAction(i)
660            if cat_act.text() == category:
661                if not cat_act.isChecked():
662                    # Trigger the action to expand the tool grid contained
663                    # within.
664                    cat_act.trigger()
665
666            else:
667                if cat_act.isChecked():
668                    # Trigger the action to hide the tool grid contained
669                    # within.
670                    cat_act.trigger()
671
672        self.dock_widget.expand()
673
674    def set_scheme_margins_enabled(self, enabled):
675        """Enable/disable the margins around the scheme document.
676        """
677        if self.__scheme_margins_enabled != enabled:
678            self.__scheme_margins_enabled = enabled
679            self.__update_scheme_margins()
680
681    def scheme_margins_enabled(self):
682        return self.__scheme_margins_enabled
683
684    scheme_margins_enabled = Property(bool,
685                                      fget=scheme_margins_enabled,
686                                      fset=set_scheme_margins_enabled)
687
688    def __update_scheme_margins(self):
689        """Update the margins around the scheme document.
690        """
691        enabled = self.__scheme_margins_enabled
692        self.__dummy_top_toolbar.setVisible(enabled)
693        self.__dummy_bottom_toolbar.setVisible(enabled)
694        central = self.centralWidget()
695
696        margin = 20 if enabled else 0
697
698        if self.dockWidgetArea(self.dock_widget) == Qt.LeftDockWidgetArea:
699            margins = (margin / 2, 0, margin, 0)
700        else:
701            margins = (margin, 0, margin / 2, 0)
702
703        central.layout().setContentsMargins(*margins)
704
705    #################
706    # Action handlers
707    #################
708    def new_scheme(self):
709        """New scheme. Return QDialog.Rejected if the user canceled
710        the operation and QDialog.Accepted otherwise.
711
712        """
713        document = self.current_document()
714        if document.isModifiedStrict():
715            # Ask for save changes
716            if self.ask_save_changes() == QDialog.Rejected:
717                return QDialog.Rejected
718
719        new_scheme = widgetsscheme.WidgetsScheme()
720
721        settings = QSettings()
722        show = settings.value("schemeinfo/show-at-new-scheme", True).toBool()
723
724        if show:
725            status = self.show_scheme_properties_for(
726                new_scheme, self.tr("New Scheme")
727            )
728
729            if status == QDialog.Rejected:
730                return QDialog.Rejected
731
732        self.set_new_scheme(new_scheme)
733
734        return QDialog.Accepted
735
736    def open_scheme(self):
737        """Open a new scheme. Return QDialog.Rejected if the user canceled
738        the operation and QDialog.Accepted otherwise.
739
740        """
741        document = self.current_document()
742        if document.isModifiedStrict():
743            if self.ask_save_changes() == QDialog.Rejected:
744                return QDialog.Rejected
745
746        if self.last_scheme_dir is None:
747            # Get user 'Documents' folder
748            start_dir = QDesktopServices.storageLocation(
749                            QDesktopServices.DocumentsLocation)
750        else:
751            start_dir = self.last_scheme_dir
752
753        # TODO: Use a dialog instance and use 'addSidebarUrls' to
754        # set one or more extra sidebar locations where Schemes are stored.
755        # Also use setHistory
756        filename = QFileDialog.getOpenFileName(
757            self, self.tr("Open Orange Scheme File"),
758            start_dir, self.tr("Orange Scheme (*.ows)"),
759        )
760
761        if filename:
762            self.load_scheme(filename)
763            return QDialog.Accepted
764        else:
765            return QDialog.Rejected
766
767    def load_scheme(self, filename):
768        """Load a scheme from a file (`filename`) into the current
769        document updates the recent scheme list and the loaded scheme path
770        property.
771
772        """
773        filename = unicode(filename)
774        dirname = os.path.dirname(filename)
775
776        self.last_scheme_dir = dirname
777
778        new_scheme = self.new_scheme_from(filename)
779
780        self.set_new_scheme(new_scheme)
781
782        scheme_doc_widget = self.current_document()
783        scheme_doc_widget.setPath(filename)
784
785        self.add_recent_scheme(new_scheme.title, filename)
786
787    def new_scheme_from(self, filename):
788        """Create and return a new :class:`widgetsscheme.WidgetsScheme`
789        from a saved `filename`.
790
791        """
792        new_scheme = widgetsscheme.WidgetsScheme()
793        try:
794            new_scheme.load_from(open(filename, "rb"))
795        except Exception:
796            message_critical(
797                 self.tr("Could not load Orange Scheme file"),
798                 title=self.tr("Error"),
799                 informative_text=self.tr("An unexpected error occurred"),
800                 exc_info=True,
801                 parent=self)
802            return None
803
804        return new_scheme
805
806    def reload_last(self):
807        """Reload last opened scheme. Return QDialog.Rejected if the
808        user canceled the operation and QDialog.Accepted otherwise.
809
810        """
811        document = self.current_document()
812        if document.isModifiedStrict():
813            if self.ask_save_changes() == QDialog.Rejected:
814                return QDialog.Rejected
815
816        # TODO: Search for a temp backup scheme with per process
817        # locking.
818        if self.recent_schemes:
819            self.load_scheme(self.recent_schemes[0][1])
820
821        return QDialog.Accepted
822
823    def set_new_scheme(self, new_scheme):
824        """Set new_scheme as the current shown scheme.
825        """
826        scheme_doc = self.current_document()
827        old_scheme = scheme_doc.scheme()
828
829        manager = new_scheme.signal_manager
830        if self.freeze_action.isChecked():
831            manager.freeze().push()
832
833        scheme_doc.setScheme(new_scheme)
834
835        old_scheme.save_widget_settings()
836        old_scheme.deleteLater()
837
838    def ask_save_changes(self):
839        """Ask the user to save the changes to the current scheme.
840        Return QDialog.Accepted if the scheme was successfully saved
841        or the user selected to discard the changes. Otherwise return
842        QDialog.Rejected.
843
844        """
845        document = self.current_document()
846
847        selected = message_question(
848            self.tr("Do you want to save the changes you made to scheme %r?") \
849                    % document.scheme().title,
850            self.tr("Save Changes?"),
851            self.tr("If you do not save your changes will be lost"),
852            buttons=QMessageBox.Save | QMessageBox.Cancel | \
853                    QMessageBox.Discard,
854            default_button=QMessageBox.Save,
855            parent=self)
856
857        if selected == QMessageBox.Save:
858            return self.save_scheme()
859        elif selected == QMessageBox.Discard:
860            return QDialog.Accepted
861        elif selected == QMessageBox.Cancel:
862            return QDialog.Rejected
863
864    def save_scheme(self):
865        """Save the current scheme. If the scheme does not have an associated
866        path then prompt the user to select a scheme file. Return
867        QDialog.Accepted if the scheme was successfully saved and
868        QDialog.Rejected if the user canceled the file selection.
869
870        """
871        document = self.current_document()
872        curr_scheme = document.scheme()
873
874        if document.path():
875            curr_scheme.save_to(open(document.path(), "wb"))
876            document.setModified(False)
877            self.add_recent_scheme(curr_scheme.title, document.path())
878            return QDialog.Accepted
879        else:
880            return self.save_scheme_as()
881
882    def save_scheme_as(self):
883        """Save the current scheme by asking the user for a filename.
884        Return QFileDialog.Accepted if the scheme was saved successfully
885        and QFileDialog.Rejected if not.
886
887        """
888        document = self.current_document()
889        curr_scheme = document.scheme()
890
891        if document.path():
892            start_dir = document.path()
893        else:
894            if self.last_scheme_dir is not None:
895                start_dir = self.last_scheme_dir
896            else:
897                start_dir = QDesktopServices.storageLocation(
898                    QDesktopServices.DocumentsLocation
899                )
900
901            title = curr_scheme.title or "untitled"
902            start_dir = os.path.join(unicode(start_dir), title + ".ows")
903
904        filename = QFileDialog.getSaveFileName(
905            self, self.tr("Save Orange Scheme File"),
906            start_dir, self.tr("Orange Scheme (*.ows)")
907        )
908
909        if filename:
910            filename = unicode(filename)
911            dirname, basename = os.path.split(filename)
912            self.last_scheme_dir = dirname
913
914            try:
915                curr_scheme.save_to(open(filename, "wb"))
916            except Exception:
917                log.error("Error saving %r to %r", curr_scheme, filename,
918                          exc_info=True)
919                # Also show a message box
920                # TODO: should handle permission errors with a
921                # specialized messages.
922                message_critical(
923                     self.tr("An error occurred while trying to save the %r "
924                             "scheme to %r" % \
925                             (curr_scheme.title, basename)),
926                     title=self.tr("Error saving %r") % basename,
927                     exc_info=True,
928                     parent=self)
929                return QFileDialog.Rejected
930
931            document.setPath(filename)
932
933            document.setModified(False)
934            self.add_recent_scheme(curr_scheme.title, document.path())
935            return QFileDialog.Accepted
936        else:
937            return QFileDialog.Rejected
938
939    def get_started(self, *args):
940        """Show getting started video
941        """
942        url = QUrl(LINKS["start-using"])
943        QDesktopServices.openUrl(url)
944
945    def tutorial(self, *args):
946        """Show tutorial.
947        """
948        url = QUrl(LINKS["tutorial"])
949        QDesktopServices.openUrl(url)
950
951    def documentation(self, *args):
952        """Show reference documentation.
953        """
954        url = QUrl(LINKS["tutorial"])
955        QDesktopServices.openUrl(url)
956
957    def recent_scheme(self, *args):
958        """Browse recent schemes. Return QDialog.Rejected if the user
959        canceled the operation and QDialog.Accepted otherwise.
960
961        """
962        items = [previewmodel.PreviewItem(name=title, path=path)
963                 for title, path in self.recent_schemes]
964        model = previewmodel.PreviewModel(items=items)
965
966        dialog = previewdialog.PreviewDialog(self)
967        title = self.tr("Recent Schemes")
968        dialog.setWindowTitle(title)
969        template = ('<h3 style="font-size: 26px">\n'
970                    #'<img height="26" src="canvas_icons:Recent.svg">\n'
971                    '{0}\n'
972                    '</h3>')
973        dialog.setHeading(template.format(title))
974        dialog.setModel(model)
975
976        model.delayedScanUpdate()
977
978        status = dialog.exec_()
979
980        index = dialog.currentIndex()
981
982        dialog.deleteLater()
983
984        if status == QDialog.Accepted:
985            doc = self.current_document()
986            if doc.isModifiedStrict():
987                if self.ask_save_changes() == QDialog.Rejected:
988                    return QDialog.Rejected
989
990            selected = model.item(index)
991
992            self.load_scheme(unicode(selected.path()))
993
994        return status
995
996    def tutorial_scheme(self, *args):
997        """Browse a collection of tutorial schemes. Returns QDialog.Rejected
998        if the user canceled the dialog else loads the selected scheme into
999        the canvas and returns QDialog.Accepted.
1000
1001        """
1002        tutors = tutorials.tutorials()
1003        items = [previewmodel.PreviewItem(path=t.abspath()) for t in tutors]
1004        model = previewmodel.PreviewModel(items=items)
1005        dialog = previewdialog.PreviewDialog(self)
1006        title = self.tr("Tutorials")
1007        dialog.setWindowTitle(title)
1008        template = ('<h3 style="font-size: 26px">\n'
1009                    #'<img height="26" src="canvas_icons:Tutorials.svg">\n'
1010                    '{0}\n'
1011                    '</h3>')
1012
1013        dialog.setHeading(template.format(title))
1014        dialog.setModel(model)
1015
1016        model.delayedScanUpdate()
1017        status = dialog.exec_()
1018        index = dialog.currentIndex()
1019
1020        dialog.deleteLater()
1021
1022        if status == QDialog.Accepted:
1023            doc = self.current_document()
1024            if doc.isModifiedStrict():
1025                if self.ask_save_changes() == QDialog.Rejected:
1026                    return QDialog.Rejected
1027
1028            selected = model.item(index)
1029
1030            new_scheme = self.new_scheme_from(unicode(selected.path()))
1031
1032            self.set_new_scheme(new_scheme)
1033
1034        return status
1035
1036    def welcome_dialog(self):
1037        """Show a modal welcome dialog for Orange Canvas.
1038        """
1039
1040        dialog = welcomedialog.WelcomeDialog(self)
1041        dialog.setWindowTitle(self.tr("Welcome to Orange Data Mining"))
1042
1043        def new_scheme():
1044            if self.new_scheme() == QDialog.Accepted:
1045                dialog.accept()
1046
1047        def open_scheme():
1048            if self.open_scheme() == QDialog.Accepted:
1049                dialog.accept()
1050
1051        def open_recent():
1052            if self.recent_scheme() == QDialog.Accepted:
1053                dialog.accept()
1054
1055        def tutorial():
1056            if self.tutorial_scheme() == QDialog.Accepted:
1057                dialog.accept()
1058
1059        new_action = \
1060            QAction(self.tr("New"), dialog,
1061                    toolTip=self.tr("Open a new scheme."),
1062                    triggered=new_scheme,
1063                    shortcut=QKeySequence.New,
1064                    icon=canvas_icons("New.svg")
1065                    )
1066
1067        open_action = \
1068            QAction(self.tr("Open"), dialog,
1069                    objectName="welcome-action-open",
1070                    toolTip=self.tr("Open a scheme."),
1071                    triggered=open_scheme,
1072                    shortcut=QKeySequence.Open,
1073                    icon=canvas_icons("Open.svg")
1074                    )
1075
1076        recent_action = \
1077            QAction(self.tr("Recent"), dialog,
1078                    objectName="welcome-recent-action",
1079                    toolTip=self.tr("Browse and open a recent scheme."),
1080                    triggered=open_recent,
1081                    shortcut=QKeySequence(Qt.ControlModifier | \
1082                                          (Qt.ShiftModifier | Qt.Key_R)),
1083                    icon=canvas_icons("Recent.svg")
1084                    )
1085
1086        tutorials_action = \
1087            QAction(self.tr("Tutorial"), dialog,
1088                    objectName="welcome-tutorial-action",
1089                    toolTip=self.tr("Browse tutorial schemes."),
1090                    triggered=tutorial,
1091                    icon=canvas_icons("Tutorials.svg")
1092                    )
1093
1094        top_row = [self.get_started_action, tutorials_action,
1095                   self.documentation_action]
1096
1097        self.new_action.triggered.connect(dialog.accept)
1098        bottom_row = [new_action, open_action, recent_action]
1099
1100        dialog.addRow(top_row, background="light-grass")
1101        dialog.addRow(bottom_row, background="light-orange")
1102
1103        settings = QSettings()
1104
1105        dialog.setShowAtStartup(
1106            settings.value("startup/show-welcome-screen", True).toBool()
1107        )
1108
1109        status = dialog.exec_()
1110
1111        settings.setValue("startup/show-welcome-screen",
1112                          dialog.showAtStartup())
1113
1114        dialog.deleteLater()
1115
1116        return status
1117
1118    def scheme_properties_dialog(self):
1119        """Return an empty `SchemeInfo` dialog instance.
1120        """
1121        settings = QSettings()
1122        value_key = "schemeinfo/show-at-new-scheme"
1123
1124        dialog = SchemeInfoDialog(self)
1125
1126        dialog.setWindowTitle(self.tr("Scheme Info"))
1127        dialog.setFixedSize(725, 450)
1128
1129        dialog.setDontShowAtNewScheme(
1130            not settings.value(value_key, True).toBool()
1131        )
1132
1133        return dialog
1134
1135    def show_scheme_properties(self):
1136        """Show current scheme properties.
1137        """
1138        settings = QSettings()
1139        value_key = "schemeinfo/show-at-new-scheme"
1140
1141        current_doc = self.current_document()
1142        scheme = current_doc.scheme()
1143        dlg = self.scheme_properties_dialog()
1144        dlg.setAutoCommit(False)
1145        dlg.setScheme(scheme)
1146        status = dlg.exec_()
1147
1148        if status == QDialog.Accepted:
1149            editor = dlg.editor
1150            stack = current_doc.undoStack()
1151            stack.beginMacro(self.tr("Change Info"))
1152            current_doc.setTitle(editor.title())
1153            current_doc.setDescription(editor.description())
1154            stack.endMacro()
1155
1156            # Store the check state.
1157            settings.setValue(value_key, not dlg.dontShowAtNewScheme())
1158        return status
1159
1160    def show_scheme_properties_for(self, scheme, window_title=None):
1161        """Show scheme properties for `scheme` with `window_title (if None
1162        a default 'Scheme Info' title will be used.
1163
1164        """
1165        settings = QSettings()
1166        value_key = "schemeinfo/show-at-new-scheme"
1167
1168        dialog = self.scheme_properties_dialog()
1169
1170        if window_title is not None:
1171            dialog.setWindowTitle(window_title)
1172
1173        dialog.setScheme(scheme)
1174
1175        status = dialog.exec_()
1176        if status == QDialog.Accepted:
1177            # Store the check state.
1178            settings.setValue(value_key, not dialog.dontShowAtNewScheme())
1179
1180        dialog.deleteLater()
1181
1182        return status
1183
1184    def set_signal_freeze(self, freeze):
1185        scheme = self.current_document().scheme()
1186        if freeze:
1187            scheme.signal_manager.freeze().push()
1188        else:
1189            scheme.signal_manager.freeze().pop()
1190
1191    def remove_selected(self):
1192        """Remove current scheme selection.
1193        """
1194        self.current_document().removeSelected()
1195
1196    def quit(self):
1197        """Quit the application.
1198        """
1199        self.close()
1200
1201    def select_all(self):
1202        self.current_document().selectAll()
1203
1204    def open_widget(self):
1205        """Open/raise selected widget's GUI.
1206        """
1207        self.current_document().openSelected()
1208
1209    def rename_widget(self):
1210        """Rename the current focused widget.
1211        """
1212        doc = self.current_document()
1213        nodes = doc.selectedNodes()
1214        if len(nodes) == 1:
1215            doc.editNodeTitle(nodes[0])
1216
1217    def widget_help(self):
1218        """Open widget help page.
1219        """
1220        doc = self.current_document()
1221        nodes = doc.selectedNodes()
1222        help_url = None
1223        if len(nodes) == 1:
1224            node = nodes[0]
1225            desc = node.description
1226            if desc.help:
1227                help_url = desc.help
1228
1229        if help_url is not None:
1230            QDesktopServices.openUrl(QUrl(help_url))
1231        else:
1232            message_information(
1233                self.tr("Sorry there is documentation available for "
1234                        "this widget."),
1235                parent=self)
1236
1237    def open_canvas_settings(self):
1238        """Open canvas settings/preferences dialog
1239        """
1240        dlg = UserSettingsDialog(self)
1241        dlg.show()
1242        status = dlg.exec_()
1243
1244        if status == QDialog.Accepted:
1245            self.__update_from_settings()
1246
1247    def show_output_view(self):
1248        """Show a window with application output.
1249        """
1250        self.output_dock.show()
1251
1252    def output_view(self):
1253        """Return the output text widget.
1254        """
1255        return self.output_dock.widget()
1256
1257    def open_about(self):
1258        """Open the about dialog.
1259        """
1260        dlg = AboutDialog(self)
1261        dlg.setAttribute(Qt.WA_DeleteOnClose)
1262        dlg.exec_()
1263
1264    def add_recent_scheme(self, title, path):
1265        """Add an entry (`title`, `path`) to the list of recent schemes.
1266        """
1267        if not path:
1268            # No associated persistent path so we can't do anything.
1269            return
1270
1271        if title is None:
1272            title = os.path.basename(path)
1273            title, _ = os.path.splitext(title)
1274
1275        filename = os.path.abspath(os.path.realpath(path))
1276        filename = os.path.normpath(filename)
1277
1278        actions_by_filename = {}
1279        for action in self.recent_scheme_action_group.actions():
1280            path = unicode(action.data().toString())
1281            actions_by_filename[path] = action
1282
1283        if filename in actions_by_filename:
1284            # Remove the title/filename (so it can be reinserted)
1285            recent_index = index(self.recent_schemes, filename,
1286                                 key=operator.itemgetter(1))
1287            self.recent_schemes.pop(recent_index)
1288
1289            action = actions_by_filename[filename]
1290            self.recent_menu.removeAction(action)
1291            action.setText(title or self.tr("untitled"))
1292        else:
1293            action = QAction(title or self.tr("untitled"), self,
1294                             toolTip=filename)
1295            action.setData(filename)
1296
1297        # Find the separator action in the menu (after 'Browse Recent')
1298        recent_actions = self.recent_menu.actions()
1299        begin_index = index(recent_actions, self.recent_menu_begin)
1300        action_before = recent_actions[begin_index + 1]
1301
1302        self.recent_menu.insertAction(action_before, action)
1303        self.recent_scheme_action_group.addAction(action)
1304        self.recent_schemes.insert(0, (title, filename))
1305
1306        config.save_recent_scheme_list(self.recent_schemes)
1307
1308    def clear_recent_schemes(self):
1309        """Clear list of recent schemes
1310        """
1311        actions = list(self.recent_menu.actions())
1312
1313        # Exclude permanent actions (Browse Recent, separators, Clear List)
1314        actions_to_remove = [action for action in actions \
1315                             if unicode(action.data().toString())]
1316
1317        for action in actions_to_remove:
1318            self.recent_menu.removeAction(action)
1319
1320        self.recent_schemes = []
1321        config.save_recent_scheme_list([])
1322
1323    def _on_recent_scheme_action(self, action):
1324        """A recent scheme action was triggered by the user
1325        """
1326        document = self.current_document()
1327        if document.isModifiedStrict():
1328            if self.ask_save_changes() == QDialog.Rejected:
1329                return
1330
1331        filename = unicode(action.data().toString())
1332        self.load_scheme(filename)
1333
1334    def _on_dock_location_changed(self, location):
1335        """Location of the dock_widget has changed, fix the margins
1336        if necessary.
1337
1338        """
1339        self.__update_scheme_margins()
1340
1341    def createPopupMenu(self):
1342        # Override the default context menu popup (we don't want the user to
1343        # be able to hide the tool dock widget).
1344        return None
1345
1346    def closeEvent(self, event):
1347        """Close the main window.
1348        """
1349        document = self.current_document()
1350        if document.isModifiedStrict():
1351            if self.ask_save_changes() == QDialog.Rejected:
1352                # Reject the event
1353                event.ignore()
1354                return
1355
1356        scheme = document.scheme()
1357        scheme.save_widget_settings()
1358
1359        # Set an empty scheme to clear the document
1360        document.setScheme(widgetsscheme.WidgetsScheme())
1361        document.deleteLater()
1362
1363        config.save_config()
1364
1365        geometry = self.saveGeometry()
1366        state = self.saveState(version=self.SETTINGS_VERSION)
1367        settings = QSettings()
1368        settings.beginGroup("mainwindow")
1369        settings.setValue("geometry", geometry)
1370        settings.setValue("state", state)
1371        settings.setValue("canvasdock/expanded",
1372                          self.dock_widget.expanded())
1373        settings.setValue("scheme-margins-enabled",
1374                          self.scheme_margins_enabled)
1375
1376        settings.setValue("last-scheme-dir", self.last_scheme_dir)
1377        settings.setValue("widgettoolbox/state",
1378                          self.widgets_tool_box.saveState())
1379
1380        settings.setValue("quick-help/visible",
1381                          self.canvas_tool_dock.quickHelpVisible())
1382
1383        settings.endGroup()
1384
1385        event.accept()
1386
1387        # Close any windows left.
1388        application = QApplication.instance()
1389        QTimer.singleShot(0, application.closeAllWindows)
1390
1391    def showEvent(self, event):
1392        settings = QSettings()
1393        geom_data = settings.value("canvasmainwindow/geometry")
1394        if geom_data.isValid():
1395            self.restoreGeometry(geom_data.toByteArray())
1396
1397        return QMainWindow.showEvent(self, event)
1398
1399    def event(self, event):
1400        if event.type() == QEvent.StatusTip and \
1401                isinstance(event, QuickHelpTipEvent):
1402            if event.priority() == QuickHelpTipEvent.Normal:
1403                self.dock_help.showHelp(event.html())
1404            elif event.priority() == QuickHelpTipEvent.Temporary:
1405                self.dock_help.showHelp(event.html(), event.timeout())
1406            elif event.priority() == QuickHelpTipEvent.Permanent:
1407                self.dock_help.showPermanentHelp(event.html())
1408            return True
1409
1410        return QMainWindow.event(self, event)
1411
1412    # Mac OS X
1413    if sys.platform == "darwin":
1414        def toggleMaximized(self):
1415            """Toggle normal/maximized window state.
1416            """
1417            if self.isMinimized():
1418                # Do nothing if window is minimized
1419                return
1420
1421            if self.isMaximized():
1422                self.showNormal()
1423            else:
1424                self.showMaximized()
1425
1426        def changeEvent(self, event):
1427            if event.type() == QEvent.WindowStateChange:
1428                # Enable/disable window menu based on minimized state
1429                self.window_menu.setEnabled(not self.isMinimized())
1430            QMainWindow.changeEvent(self, event)
1431
1432    def tr(self, sourceText, disambiguation=None, n=-1):
1433        """Translate the string.
1434        """
1435        return unicode(QMainWindow.tr(self, sourceText, disambiguation, n))
1436
1437
1438def identity(item):
1439    return item
1440
1441
1442def index(sequence, *what, **kwargs):
1443    """index(sequence, what, [key=None, [predicate=None]])
1444    Return index of `what` in `sequence`.
1445    """
1446    what = what[0]
1447    key = kwargs.get("key", identity)
1448    predicate = kwargs.get("predicate", operator.eq)
1449    for i, item in enumerate(sequence):
1450        item_key = key(item)
1451        if predicate(what, item_key):
1452            return i
1453    raise ValueError("%r not in sequence" % what)
Note: See TracBrowser for help on using the repository browser.