source: orange/Orange/OrangeCanvas/application/canvasmain.py @ 11158:a33b36d86a18

Revision 11158:a33b36d86a18, 47.7 KB checked in by Ales Erjavec <ales.erjavec@…>, 18 months ago (diff)

Override the default QMainWindow context popup.

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