source: orange/Orange/OrangeCanvas/application/canvasmain.py @ 11253:4d431978c303

Revision 11253:4d431978c303, 49.8 KB checked in by Ales Erjavec <ales.erjavec@…>, 15 months ago (diff)

Fixed the use of obsolete setting keys.

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