source: orange/Orange/OrangeCanvas/application/canvasmain.py @ 11188:88e8e2e844b8

Revision 11188:88e8e2e844b8, 50.0 KB checked in by Ales Erjavec <ales.erjavec@…>, 17 months ago (diff)

Do not create a new scheme when the user cancels the 'Scheme Info' dialog.

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