source: orange/Orange/OrangeCanvas/application/canvasmain.py @ 11166:52723f19e7a2

Revision 11166:52723f19e7a2, 48.0 KB checked in by Ales Erjavec <ales.erjavec@…>, 18 months ago (diff)

Moved the saving of "Don't show ..." check state out of SchemeInfoDialog widget.

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