source: orange/Orange/OrangeCanvas/application/canvasmain.py @ 11167:74773cf295d6

Revision 11167:74773cf295d6, 49.4 KB checked in by Ales Erjavec <ales.erjavec@…>, 18 months ago (diff)

Added 'Output' window showing redirected stdout/stderr.

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