source: orange/Orange/OrangeCanvas/application/canvasmain.py @ 11197:629ac047bc70

Revision 11197:629ac047bc70, 48.8 KB checked in by Ales Erjavec <ales.erjavec@…>, 17 months ago (diff)

Use scheme title as the default filename on first save.

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