source: orange/Orange/OrangeCanvas/application/canvasmain.py @ 11194:12d76d869aba

Revision 11194:12d76d869aba, 48.6 KB checked in by Ales Erjavec <ales.erjavec@…>, 17 months ago (diff)

Moved tool actions from main window to SchemeEditWidget.

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        elif self.last_scheme_dir is not None:
997            start_dir = self.last_scheme_dir
998        else:
999            start_dir = QDesktopServices.storageLocation(
1000                            QDesktopServices.DocumentsLocation)
1001
1002        filename = QFileDialog.getSaveFileName(
1003            self, self.tr("Save Orange Scheme File"),
1004            start_dir, self.tr("Orange Scheme (*.ows)")
1005        )
1006
1007        if filename:
1008            filename = unicode(filename)
1009            dirname, basename = os.path.split(filename)
1010            self.last_scheme_dir = dirname
1011
1012            try:
1013                curr_scheme.save_to(open(filename, "wb"))
1014            except Exception:
1015                log.error("Error saving %r to %r", curr_scheme, filename,
1016                          exc_info=True)
1017                # Also show a message box
1018                # TODO: should handle permission errors with a
1019                # specialized messages.
1020                message_critical(
1021                     self.tr("An error occurred while trying to save the %r "
1022                             "scheme to %r" % \
1023                             (curr_scheme.title, basename)),
1024                     title=self.tr("Error saving %r") % basename,
1025                     exc_info=True,
1026                     parent=self)
1027                return QFileDialog.Rejected
1028
1029            curr_scheme.path = filename
1030            if not curr_scheme.title:
1031                curr_scheme.title = os.path.splitext(basename)[0]
1032
1033            self.add_recent_scheme(curr_scheme)
1034            document.setModified(False)
1035            return QFileDialog.Accepted
1036        else:
1037            return QFileDialog.Rejected
1038
1039    def get_started(self, *args):
1040        """Show getting started video
1041        """
1042        url = QUrl(LINKS["start-using"])
1043        QDesktopServices.openUrl(url)
1044
1045    def tutorial(self, *args):
1046        """Show tutorial.
1047        """
1048        url = QUrl(LINKS["tutorial"])
1049        QDesktopServices.openUrl(url)
1050
1051    def documentation(self, *args):
1052        """Show reference documentation.
1053        """
1054        url = QUrl(LINKS["tutorial"])
1055        QDesktopServices.openUrl(url)
1056
1057    def recent_scheme(self, *args):
1058        """Browse recent schemes. Return QDialog.Rejected if the user
1059        canceled the operation and QDialog.Accepted otherwise.
1060
1061        """
1062        items = [previewmodel.PreviewItem(name=title, path=path)
1063                 for title, path in self.recent_schemes]
1064        model = previewmodel.PreviewModel(items=items)
1065
1066        dialog = previewdialog.PreviewDialog(self)
1067        dialog.setWindowTitle(self.tr("Recent Schemes"))
1068        dialog.setModel(model)
1069
1070        model.delayedScanUpdate()
1071
1072        status = dialog.exec_()
1073
1074        if status == QDialog.Accepted:
1075            doc = self.current_document()
1076            if doc.isModified():
1077                if self.ask_save_changes() == QDialog.Rejected:
1078                    return QDialog.Rejected
1079
1080            index = dialog.currentIndex()
1081            selected = model.item(index)
1082
1083            self.load_scheme(unicode(selected.path()))
1084        return status
1085
1086    def welcome_dialog(self):
1087        """Show a modal welcome dialog for Orange Canvas.
1088        """
1089
1090        dialog = welcomedialog.WelcomeDialog(self)
1091        dialog.setWindowTitle(self.tr("Welcome to Orange Data Mining"))
1092        top_row = [self.get_started_action, self.tutorials_action,
1093                   self.documentation_action]
1094
1095        def new_scheme():
1096            if self.new_scheme() == QDialog.Accepted:
1097                dialog.accept()
1098
1099        def open_scheme():
1100            if self.open_scheme() == QDialog.Accepted:
1101                dialog.accept()
1102
1103        def open_recent():
1104            if self.recent_scheme() == QDialog.Accepted:
1105                dialog.accept()
1106
1107        new_action = \
1108            QAction(self.tr("New"), dialog,
1109                    toolTip=self.tr("Open a new scheme."),
1110                    triggered=new_scheme,
1111                    shortcut=QKeySequence.New,
1112                    icon=canvas_icons("New.svg")
1113                    )
1114
1115        open_action = \
1116            QAction(self.tr("Open"), dialog,
1117                    objectName="welcome-action-open",
1118                    toolTip=self.tr("Open a scheme."),
1119                    triggered=open_scheme,
1120                    shortcut=QKeySequence.Open,
1121                    icon=canvas_icons("Open.svg")
1122                    )
1123
1124        recent_action = \
1125            QAction(self.tr("Recent"), dialog,
1126                    objectName="welcome-recent-action",
1127                    toolTip=self.tr("Browse and open a recent scheme."),
1128                    triggered=open_recent,
1129                    shortcut=QKeySequence(Qt.ControlModifier | \
1130                                          (Qt.ShiftModifier | Qt.Key_R)),
1131                    icon=canvas_icons("Recent.svg")
1132                    )
1133
1134        self.new_action.triggered.connect(dialog.accept)
1135        bottom_row = [new_action, open_action, recent_action]
1136
1137        dialog.addRow(top_row, background="light-grass")
1138        dialog.addRow(bottom_row, background="light-orange")
1139
1140        settings = QSettings()
1141
1142        dialog.setShowAtStartup(
1143            settings.value("welcomedialog/show-at-startup", True).toBool()
1144        )
1145
1146        status = dialog.exec_()
1147
1148        settings.setValue("welcomedialog/show-at-startup",
1149                          dialog.showAtStartup())
1150        return status
1151
1152    def show_scheme_properties(self):
1153        """Show current scheme properties.
1154        """
1155        current_doc = self.current_document()
1156        scheme = current_doc.scheme()
1157        return self.show_scheme_properties_for(scheme)
1158
1159    def show_scheme_properties_for(self, scheme, window_title=None):
1160        """Show scheme properties for `scheme` with `window_title (if None
1161        a default 'Scheme Info' title will be used.
1162
1163        """
1164        settings = QSettings()
1165        value_key = "schemeinfo/show-at-new-scheme"
1166
1167        dialog = SchemeInfoDialog(self)
1168
1169        if window_title is None:
1170            window_title = self.tr("Scheme Info")
1171
1172        dialog.setWindowTitle(window_title)
1173        dialog.setFixedSize(725, 450)
1174
1175        dialog.setDontShowAtNewScheme(
1176            not settings.value(value_key, True).toBool()
1177        )
1178
1179        dialog.setScheme(scheme)
1180
1181        status = dialog.exec_()
1182        if status == QDialog.Accepted:
1183            # Store the check state.
1184            settings.setValue(value_key, not dialog.dontShowAtNewScheme())
1185
1186        return status
1187
1188    def set_signal_freeze(self, freeze):
1189        scheme = self.current_document().scheme()
1190        if freeze:
1191            scheme.signal_manager.freeze().push()
1192        else:
1193            scheme.signal_manager.freeze().pop()
1194
1195    def remove_selected(self):
1196        """Remove current scheme selection.
1197        """
1198        self.current_document().removeSelected()
1199
1200    def quit(self):
1201        """Quit the application.
1202        """
1203        self.close()
1204
1205    def select_all(self):
1206        self.current_document().selectAll()
1207
1208    def open_widget(self):
1209        """Open/raise selected widget's GUI.
1210        """
1211        self.current_document().openSelected()
1212
1213    def rename_widget(self):
1214        """Rename the current focused widget.
1215        """
1216        doc = self.current_document()
1217        nodes = doc.selectedNodes()
1218        if len(nodes) == 1:
1219            doc.editNodeTitle(nodes[0])
1220
1221    def widget_help(self):
1222        """Open widget help page.
1223        """
1224        doc = self.current_document()
1225        nodes = doc.selectedNodes()
1226        help_url = None
1227        if len(nodes) == 1:
1228            node = nodes[0]
1229            desc = node.description
1230            if desc.help:
1231                help_url = desc.help
1232
1233        if help_url is not None:
1234            QDesktopServices.openUrl(QUrl(help_url))
1235        else:
1236            message_information(
1237                self.tr("Sorry there is documentation available for "
1238                        "this widget."),
1239                parent=self)
1240
1241    def open_canvas_settings(self):
1242        """Open canvas settings/preferences dialog
1243        """
1244        pass
1245
1246    def show_output_view(self):
1247        """Show a window with application output.
1248        """
1249        self.output_dock.show()
1250
1251    def output_view(self):
1252        """Return the output text widget.
1253        """
1254        return self.output_dock.widget()
1255
1256    def open_about(self):
1257        """Open the about dialog.
1258        """
1259        dlg = AboutDialog(self)
1260        dlg.exec_()
1261
1262    def add_recent_scheme(self, scheme):
1263        """Add `scheme` to the list of recent schemes.
1264        """
1265        if not scheme.path:
1266            return
1267
1268        title = scheme.title
1269        path = scheme.path
1270
1271        if title is None:
1272            title = os.path.basename(path)
1273            title, _ = os.path.splitext(title)
1274
1275        filename = os.path.abspath(os.path.realpath(path))
1276        filename = os.path.normpath(filename)
1277
1278        actions_by_filename = {}
1279        for action in self.recent_scheme_action_group.actions():
1280            path = unicode(action.data().toString())
1281            actions_by_filename[path] = action
1282
1283        if (title, filename) in self.recent_schemes:
1284            # Remove the title/filename (so it can be reinserted)
1285            recent_index = self.recent_schemes.index((title, filename))
1286            self.recent_schemes.pop(recent_index)
1287
1288        if filename in actions_by_filename:
1289            action = actions_by_filename[filename]
1290            self.recent_menu.removeAction(action)
1291        else:
1292            action = QAction(title, self, toolTip=filename)
1293            action.setData(filename)
1294
1295        self.recent_schemes.insert(0, (title, filename))
1296
1297        recent_actions = self.recent_menu.actions()
1298        begin_index = index(recent_actions, self.recent_menu_begin)
1299        action_before = recent_actions[begin_index + 1]
1300
1301        self.recent_menu.insertAction(action_before, action)
1302        self.recent_scheme_action_group.addAction(action)
1303
1304        config.save_recent_scheme_list(self.recent_schemes)
1305
1306    def clear_recent_schemes(self):
1307        """Clear list of recent schemes
1308        """
1309        actions = list(self.recent_menu.actions())
1310
1311        # Exclude permanent actions (Browse Recent, separators, Clear List)
1312        actions_to_remove = [action for action in actions \
1313                             if unicode(action.data().toString())]
1314
1315        for action in actions_to_remove:
1316            self.recent_menu.removeAction(action)
1317
1318        self.recent_schemes = []
1319        config.save_recent_scheme_list([])
1320
1321    def _on_recent_scheme_action(self, action):
1322        """A recent scheme action was triggered by the user
1323        """
1324        document = self.current_document()
1325        if document.isModified():
1326            if self.ask_save_changes() == QDialog.Rejected:
1327                return
1328
1329        filename = unicode(action.data().toString())
1330        self.load_scheme(filename)
1331
1332    def _on_dock_location_changed(self, location):
1333        """Location of the dock_widget has changed, fix the margins
1334        if necessary.
1335
1336        """
1337        self.__update_scheme_margins()
1338
1339    def createPopupMenu(self):
1340        # Override the default context menu popup (we don't want the user to
1341        # be able to hide the tool dock widget).
1342        return None
1343
1344    def closeEvent(self, event):
1345        """Close the main window.
1346        """
1347        document = self.current_document()
1348        if document.isModified():
1349            if self.ask_save_changes() == QDialog.Rejected:
1350                # Reject the event
1351                event.ignore()
1352                return
1353
1354        # Set an empty scheme to clear the document
1355        document.setScheme(widgetsscheme.WidgetsScheme())
1356        document.deleteLater()
1357
1358        config.save_config()
1359
1360        geometry = self.saveGeometry()
1361        state = self.saveState(version=self.SETTINGS_VERSION)
1362        settings = QSettings()
1363        settings.beginGroup("canvasmainwindow")
1364        settings.setValue("geometry", geometry)
1365        settings.setValue("state", state)
1366        settings.setValue("canvasdock/expanded",
1367                          self.dock_widget.expanded())
1368        settings.setValue("scheme_margins_enabled",
1369                          self.scheme_margins_enabled)
1370
1371        settings.setValue("last_scheme_dir", self.last_scheme_dir)
1372        settings.endGroup()
1373
1374        event.accept()
1375
1376        # Close any windows left.
1377        application = QApplication.instance()
1378        QTimer.singleShot(0, application.closeAllWindows)
1379
1380    def showEvent(self, event):
1381        settings = QSettings()
1382        geom_data = settings.value("canvasmainwindow/geometry")
1383        if geom_data.isValid():
1384            self.restoreGeometry(geom_data.toByteArray())
1385
1386        return QMainWindow.showEvent(self, event)
1387
1388    # Mac OS X
1389    if sys.platform == "darwin":
1390        def toggleMaximized(self):
1391            """Toggle normal/maximized window state.
1392            """
1393            if self.isMinimized():
1394                # Do nothing if window is minimized
1395                return
1396
1397            if self.isMaximized():
1398                self.showNormal()
1399            else:
1400                self.showMaximized()
1401
1402        def changeEvent(self, event):
1403            if event.type() == QEvent.WindowStateChange:
1404                # Enable/disable window menu based on minimized state
1405                self.window_menu.setEnabled(not self.isMinimized())
1406            QMainWindow.changeEvent(self, event)
1407
1408    def tr(self, sourceText, disambiguation=None, n=-1):
1409        """Translate the string.
1410        """
1411        return unicode(QMainWindow.tr(self, sourceText, disambiguation, n))
1412
1413
1414def identity(item):
1415    return item
1416
1417
1418def index(sequence, *what, **kwargs):
1419    """index(sequence, what, [key=None, [predicate=None]])
1420    Return index of `what` in `sequence`.
1421    """
1422    what = what[0]
1423    key = kwargs.get("key", identity)
1424    predicate = kwargs.get("predicate", operator.eq)
1425    for i, item in enumerate(sequence):
1426        item_key = key(item)
1427        if predicate(what, item_key):
1428            return i
1429    raise ValueError("%r not in sequence" % what)
Note: See TracBrowser for help on using the repository browser.