source: orange/Orange/OrangeCanvas/application/canvasmain.py @ 11598:4837bf270e01

Revision 11598:4837bf270e01, 67.2 KB checked in by Ales Erjavec <ales.erjavec@…>, 10 months ago (diff)

Added 'Open and Freeze' menu action.

Line 
1"""
2Orange Canvas Main Window
3
4"""
5import os
6import sys
7import logging
8import operator
9from functools import partial
10
11import pkg_resources
12
13import Orange.utils.addons
14
15from PyQt4.QtGui import (
16    QMainWindow, QWidget, QAction, QActionGroup, QMenu, QMenuBar, QDialog,
17    QFileDialog, QMessageBox, QVBoxLayout, QSizePolicy, QColor, QKeySequence,
18    QIcon, QToolBar, QToolButton, QDockWidget, QDesktopServices, QApplication,
19)
20
21from PyQt4.QtCore import (
22    Qt, QEvent, QSize, QUrl, QTimer, QFile, QByteArray
23)
24
25from PyQt4.QtNetwork import QNetworkDiskCache
26
27from PyQt4.QtWebKit import QWebView
28
29from PyQt4.QtCore import pyqtProperty as Property
30
31# Compatibility with PyQt < v4.8.3
32from ..utils.qtcompat import QSettings
33
34from ..gui.dropshadow import DropShadowFrame
35from ..gui.dock import CollapsibleDockWidget
36from ..gui.quickhelp import QuickHelpTipEvent
37from ..gui.utils import message_critical, message_question, \
38                        message_warning, message_information
39
40from ..help import HelpManager
41
42from .canvastooldock import CanvasToolDock, QuickCategoryToolbar, \
43                            CategoryPopupMenu, popup_position_from_source
44from .aboutdialog import AboutDialog
45from .schemeinfo import SchemeInfoDialog
46from .outputview import OutputView
47from .settings import UserSettingsDialog
48from .addons import AddOnManagerDialog
49
50from ..document.schemeedit import SchemeEditWidget
51
52from ..scheme import widgetsscheme
53from ..scheme.readwrite import parse_scheme, sniff_version
54
55from . import welcomedialog
56from ..preview import previewdialog, previewmodel
57
58from .. import config
59
60from . import tutorials
61
62log = logging.getLogger(__name__)
63
64# TODO: Orange Version in the base link
65
66BASE_LINK = "http://orange.biolab.si/"
67
68LINKS = \
69    {"start-using": BASE_LINK + "start-using/",
70     "tutorial": BASE_LINK + "tutorial/",
71     "reference": BASE_LINK + "doc/"
72     }
73
74
75def style_icons(widget, standard_pixmap):
76    """Return the Qt standard pixmap icon.
77    """
78    return QIcon(widget.style().standardPixmap(standard_pixmap))
79
80
81def canvas_icons(name):
82    """Return the named canvas icon.
83    """
84    icon_file = QFile("canvas_icons:" + name)
85    if icon_file.exists():
86        return QIcon("canvas_icons:" + name)
87    else:
88        return QIcon(pkg_resources.resource_filename(
89                      config.__name__,
90                      os.path.join("icons", name))
91                     )
92
93
94class FakeToolBar(QToolBar):
95    """A Toolbar with no contents (used to reserve top and bottom margins
96    on the main window).
97
98    """
99    def __init__(self, *args, **kwargs):
100        QToolBar.__init__(self, *args, **kwargs)
101        self.setFloatable(False)
102        self.setMovable(False)
103
104        # Don't show the tool bar action in the main window's
105        # context menu.
106        self.toggleViewAction().setVisible(False)
107
108    def paintEvent(self, event):
109        # Do nothing.
110        pass
111
112
113class DockableWindow(QDockWidget):
114    def __init__(self, *args, **kwargs):
115        QDockWidget.__init__(self, *args, **kwargs)
116
117        # Fist show after floating
118        self.__firstShow = True
119        # Flags to use while floating
120        self.__windowFlags = Qt.Window
121        self.setWindowFlags(self.__windowFlags)
122        self.topLevelChanged.connect(self.__on_topLevelChanged)
123        self.visibilityChanged.connect(self.__on_visbilityChanged)
124
125        self.__closeAction = QAction(self.tr("Close"), self,
126                                     shortcut=QKeySequence.Close,
127                                     triggered=self.close,
128                                     enabled=self.isFloating())
129        self.topLevelChanged.connect(self.__closeAction.setEnabled)
130        self.addAction(self.__closeAction)
131
132    def setFloatingWindowFlags(self, flags):
133        """
134        Set `windowFlags` to use while the widget is floating (undocked).
135        """
136        if self.__windowFlags != flags:
137            self.__windowFlags = flags
138            if self.isFloating():
139                self.__fixWindowFlags()
140
141    def floatingWindowFlags(self):
142        """
143        Return the `windowFlags` used when the widget is floating.
144        """
145        return self.__windowFlags
146
147    def __fixWindowFlags(self):
148        if self.isFloating():
149            update_window_flags(self, self.__windowFlags)
150
151    def __on_topLevelChanged(self, floating):
152        if floating:
153            self.__firstShow = True
154            self.__fixWindowFlags()
155
156    def __on_visbilityChanged(self, visible):
157        if visible and self.isFloating() and self.__firstShow:
158            self.__firstShow = False
159            self.__fixWindowFlags()
160
161
162def update_window_flags(widget, flags):
163    currflags = widget.windowFlags()
164    if int(flags) != int(currflags):
165        hidden = widget.isHidden()
166        widget.setWindowFlags(flags)
167        # setting the flags hides the widget
168        if not hidden:
169            widget.show()
170
171
172class CanvasMainWindow(QMainWindow):
173    SETTINGS_VERSION = 2
174
175    def __init__(self, *args):
176        QMainWindow.__init__(self, *args)
177
178        self.__scheme_margins_enabled = True
179        self.__document_title = "untitled"
180        self.__first_show = True
181
182        self.widget_registry = None
183
184        self.last_scheme_dir = QDesktopServices.StandardLocation(
185            QDesktopServices.DocumentsLocation
186        )
187
188        self.recent_schemes = config.recent_schemes()
189        self.num_recent_schemes = 15
190
191        self.open_in_external_browser = False
192        self.help = HelpManager(self)
193
194        self.setup_actions()
195        self.setup_ui()
196        self.setup_menu()
197
198        self.restore()
199
200    def setup_ui(self):
201        """Setup main canvas ui
202        """
203
204        log.info("Setting up Canvas main window.")
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        self.setCorner(Qt.BottomLeftCorner, Qt.LeftDockWidgetArea)
219        self.setCorner(Qt.BottomRightCorner, Qt.RightDockWidgetArea)
220
221        self.setDockOptions(QMainWindow.AnimatedDocks)
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(parent=self))
230
231        w.layout().addWidget(self.scheme_widget)
232
233        self.setCentralWidget(w)
234
235        # Drop shadow around the scheme document
236        frame = DropShadowFrame(radius=15)
237        frame.setColor(QColor(0, 0, 0, 100))
238        frame.setWidget(self.scheme_widget)
239
240        # Main window title and title icon.
241        self.set_document_title(self.scheme_widget.scheme().title)
242        self.scheme_widget.titleChanged.connect(self.set_document_title)
243        self.scheme_widget.modificationChanged.connect(self.setWindowModified)
244
245        self.setWindowIcon(canvas_icons("Get Started.svg"))
246
247        # QMainWindow's Dock widget
248        self.dock_widget = CollapsibleDockWidget(objectName="main-area-dock")
249        self.dock_widget.setFeatures(QDockWidget.DockWidgetMovable | \
250                                     QDockWidget.DockWidgetClosable)
251
252        self.dock_widget.setAllowedAreas(Qt.LeftDockWidgetArea | \
253                                         Qt.RightDockWidgetArea)
254
255        # Main canvas tool dock (with widget toolbox, common actions.
256        # This is the widget that is shown when the dock is expanded.
257        canvas_tool_dock = CanvasToolDock(objectName="canvas-tool-dock")
258        canvas_tool_dock.setSizePolicy(QSizePolicy.Fixed,
259                                       QSizePolicy.MinimumExpanding)
260
261        # Bottom tool bar
262        self.canvas_toolbar = canvas_tool_dock.toolbar
263        self.canvas_toolbar.setIconSize(QSize(25, 25))
264        self.canvas_toolbar.setFixedHeight(28)
265        self.canvas_toolbar.layout().setSpacing(1)
266
267        # Widgets tool box
268        self.widgets_tool_box = canvas_tool_dock.toolbox
269        self.widgets_tool_box.setObjectName("canvas-toolbox")
270        self.widgets_tool_box.setTabButtonHeight(30)
271        self.widgets_tool_box.setTabIconSize(QSize(26, 26))
272        self.widgets_tool_box.setButtonSize(QSize(64, 84))
273        self.widgets_tool_box.setIconSize(QSize(48, 48))
274
275        self.widgets_tool_box.triggered.connect(
276            self.on_tool_box_widget_activated
277        )
278
279        self.dock_help = canvas_tool_dock.help
280        self.dock_help.setMaximumHeight(150)
281        self.dock_help.document().setDefaultStyleSheet("h3, a {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        tool_actions = self.current_document().toolbarActions()
303
304        (self.canvas_zoom_action, self.canvas_align_to_grid_action,
305         self.canvas_text_action, self.canvas_arrow_action,) = tool_actions
306
307        self.canvas_zoom_action.setIcon(canvas_icons("Search.svg"))
308        self.canvas_align_to_grid_action.setIcon(canvas_icons("Grid.svg"))
309        self.canvas_text_action.setIcon(canvas_icons("Text Size.svg"))
310        self.canvas_arrow_action.setIcon(canvas_icons("Arrow.svg"))
311
312        dock_actions = [self.show_properties_action] + \
313                       tool_actions + \
314                       [self.freeze_action,
315                        self.dock_help_action]
316
317
318        def addOnRefreshCallback():
319            pass #TODO add new category
320
321        Orange.utils.addons.addon_refresh_callback.append(addOnRefreshCallback)
322
323        # Tool bar in the collapsed dock state (has the same actions as
324        # the tool bar in the CanvasToolDock
325        actions_toolbar = QToolBar(orientation=Qt.Vertical)
326        actions_toolbar.setFixedWidth(38)
327        actions_toolbar.layout().setSpacing(0)
328
329        actions_toolbar.setToolButtonStyle(Qt.ToolButtonIconOnly)
330
331        for action in dock_actions:
332            self.canvas_toolbar.addAction(action)
333            button = self.canvas_toolbar.widgetForAction(action)
334            button.setPopupMode(QToolButton.DelayedPopup)
335
336            actions_toolbar.addAction(action)
337            button = actions_toolbar.widgetForAction(action)
338            button.setFixedSize(38, 30)
339            button.setPopupMode(QToolButton.DelayedPopup)
340
341        dock2.layout().addWidget(self.quick_category)
342        dock2.layout().addWidget(actions_toolbar)
343
344        self.dock_widget.setAnimationEnabled(False)
345        self.dock_widget.setExpandedWidget(self.canvas_tool_dock)
346        self.dock_widget.setCollapsedWidget(dock2)
347        self.dock_widget.setExpanded(True)
348        self.dock_widget.expandedChanged.connect(self._on_tool_dock_expanded)
349
350        self.addDockWidget(Qt.LeftDockWidgetArea, self.dock_widget)
351        self.dock_widget.dockLocationChanged.connect(
352            self._on_dock_location_changed
353        )
354
355        self.output_dock = DockableWindow(self.tr("Output"), self,
356                                          objectName="output-dock")
357        self.output_dock.setAllowedAreas(Qt.BottomDockWidgetArea)
358        output_view = OutputView()
359        # Set widget before calling addDockWidget, otherwise the dock
360        # does not resize properly on first undock
361        self.output_dock.setWidget(output_view)
362        self.output_dock.hide()
363
364        self.help_dock = DockableWindow(self.tr("Help"), self,
365                                        objectName="help-dock")
366        self.help_dock.setAllowedAreas(Qt.NoDockWidgetArea)
367        self.help_view = QWebView()
368        manager = self.help_view.page().networkAccessManager()
369        cache = QNetworkDiskCache()
370        cache.setCacheDirectory(
371            os.path.join(config.cache_dir(), "help", "help-view-cache")
372        )
373        manager.setCache(cache)
374        self.help_dock.setWidget(self.help_view)
375        self.help_dock.hide()
376
377        self.setMinimumSize(600, 500)
378
379    def setup_actions(self):
380        """Initialize main window actions.
381        """
382
383        self.new_action = \
384            QAction(self.tr("New"), self,
385                    objectName="action-new",
386                    toolTip=self.tr("Open a new scheme."),
387                    triggered=self.new_scheme,
388                    shortcut=QKeySequence.New,
389                    icon=canvas_icons("New.svg")
390                    )
391
392        self.open_action = \
393            QAction(self.tr("Open"), self,
394                    objectName="action-open",
395                    toolTip=self.tr("Open a scheme."),
396                    triggered=self.open_scheme,
397                    shortcut=QKeySequence.Open,
398                    icon=canvas_icons("Open.svg")
399                    )
400
401        self.open_and_freeze_action = \
402            QAction(self.tr("Open and Freeze"), self,
403                    objectName="action-open-and-freeze",
404                    toolTip=self.tr("Open a new scheme and freeze signal "
405                                    "propagation."),
406                    triggered=self.open_and_freeze_scheme
407                    )
408
409        self.save_action = \
410            QAction(self.tr("Save"), self,
411                    objectName="action-save",
412                    toolTip=self.tr("Save current scheme."),
413                    triggered=self.save_scheme,
414                    shortcut=QKeySequence.Save,
415                    )
416
417        self.save_as_action = \
418            QAction(self.tr("Save As ..."), self,
419                    objectName="action-save-as",
420                    toolTip=self.tr("Save current scheme as."),
421                    triggered=self.save_scheme_as,
422                    shortcut=QKeySequence.SaveAs,
423                    )
424
425        self.quit_action = \
426            QAction(self.tr("Quit"), self,
427                    objectName="quit-action",
428                    toolTip=self.tr("Quit Orange Canvas."),
429                    triggered=self.quit,
430                    menuRole=QAction.QuitRole,
431                    shortcut=QKeySequence.Quit,
432                    )
433
434        self.welcome_action = \
435            QAction(self.tr("Welcome"), self,
436                    objectName="welcome-action",
437                    toolTip=self.tr("Show welcome screen."),
438                    triggered=self.welcome_dialog,
439                    )
440
441        self.get_started_action = \
442            QAction(self.tr("Get Started"), self,
443                    objectName="get-started-action",
444                    toolTip=self.tr("View a 'Get Started' introduction."),
445                    triggered=self.get_started,
446                    icon=canvas_icons("Get Started.svg")
447                    )
448
449        self.tutorials_action = \
450            QAction(self.tr("Tutorials"), self,
451                    objectName="tutorial-action",
452                    toolTip=self.tr("Browse tutorials."),
453                    triggered=self.tutorial_scheme,
454                    icon=canvas_icons("Tutorials.svg")
455                    )
456
457        self.documentation_action = \
458            QAction(self.tr("Documentation"), self,
459                    objectName="documentation-action",
460                    toolTip=self.tr("View reference documentation."),
461                    triggered=self.documentation,
462                    icon=canvas_icons("Documentation.svg")
463                    )
464
465        self.about_action = \
466            QAction(self.tr("About"), self,
467                    objectName="about-action",
468                    toolTip=self.tr("Show about dialog."),
469                    triggered=self.open_about,
470                    menuRole=QAction.AboutRole,
471                    )
472
473        # Action group for for recent scheme actions
474        self.recent_scheme_action_group = \
475            QActionGroup(self, exclusive=False,
476                         objectName="recent-action-group",
477                         triggered=self._on_recent_scheme_action)
478
479        self.recent_action = \
480            QAction(self.tr("Browse Recent"), self,
481                    objectName="recent-action",
482                    toolTip=self.tr("Browse and open a recent scheme."),
483                    triggered=self.recent_scheme,
484                    shortcut=QKeySequence(Qt.ControlModifier | \
485                                          (Qt.ShiftModifier | Qt.Key_R)),
486                    icon=canvas_icons("Recent.svg")
487                    )
488
489        self.reload_last_action = \
490            QAction(self.tr("Reload Last Scheme"), self,
491                    objectName="reload-last-action",
492                    toolTip=self.tr("Reload last open scheme."),
493                    triggered=self.reload_last,
494                    shortcut=QKeySequence(Qt.ControlModifier | Qt.Key_R)
495                    )
496
497        self.clear_recent_action = \
498            QAction(self.tr("Clear Menu"), self,
499                    objectName="clear-recent-menu-action",
500                    toolTip=self.tr("Clear recent menu."),
501                    triggered=self.clear_recent_schemes
502                    )
503
504        self.show_properties_action = \
505            QAction(self.tr("Scheme Info"), self,
506                    objectName="show-properties-action",
507                    toolTip=self.tr("Show scheme properties."),
508                    triggered=self.show_scheme_properties,
509                    shortcut=QKeySequence(Qt.ControlModifier | Qt.Key_I),
510                    icon=canvas_icons("Document Info.svg")
511                    )
512
513        self.canvas_settings_action = \
514            QAction(self.tr("Settings"), self,
515                    objectName="canvas-settings-action",
516                    toolTip=self.tr("Set application settings."),
517                    triggered=self.open_canvas_settings,
518                    menuRole=QAction.PreferencesRole,
519                    shortcut=QKeySequence.Preferences
520                    )
521
522        self.canvas_addons_action = \
523            QAction(self.tr("&Add-ons..."), self,
524                    objectName="canvas-addons-action",
525                    toolTip=self.tr("Manage add-ons."),
526                    triggered=self.open_addons,
527                    )
528
529        self.show_output_action = \
530            QAction(self.tr("Show Output View"), self,
531                    toolTip=self.tr("Show application output."),
532                    triggered=self.show_output_view,
533                    )
534
535        if sys.platform == "darwin":
536            # Actions for native Mac OSX look and feel.
537            self.minimize_action = \
538                QAction(self.tr("Minimize"), self,
539                        triggered=self.showMinimized,
540                        shortcut=QKeySequence(Qt.ControlModifier | Qt.Key_M)
541                        )
542
543            self.zoom_action = \
544                QAction(self.tr("Zoom"), self,
545                        objectName="application-zoom",
546                        triggered=self.toggleMaximized,
547                        )
548
549        self.freeze_action = \
550            QAction(self.tr("Freeze"), self,
551                    objectName="signal-freeze-action",
552                    checkable=True,
553                    toolTip=self.tr("Freeze signal propagation."),
554                    triggered=self.set_signal_freeze,
555                    icon=canvas_icons("Pause.svg")
556                    )
557
558        self.toggle_tool_dock_expand = \
559            QAction(self.tr("Expand Tool Dock"), self,
560                    objectName="toggle-tool-dock-expand",
561                    checkable=True,
562                    checked=True,
563                    shortcut=QKeySequence(Qt.ControlModifier |
564                                          (Qt.ShiftModifier | Qt.Key_D)),
565                    triggered=self.set_tool_dock_expanded)
566
567        # Gets assigned in setup_ui (the action is defined in CanvasToolDock)
568        # TODO: This is bad (should be moved here).
569        self.dock_help_action = None
570
571        self.toogle_margins_action = \
572            QAction(self.tr("Show Scheme Margins"), self,
573                    checkable=True,
574                    checked=True,
575                    toolTip=self.tr("Show margins around the scheme view."),
576                    toggled=self.set_scheme_margins_enabled
577                    )
578
579    def setup_menu(self):
580        menu_bar = QMenuBar()
581
582        # File menu
583        file_menu = QMenu(self.tr("&File"), menu_bar)
584        file_menu.addAction(self.new_action)
585        file_menu.addAction(self.open_action)
586        file_menu.addAction(self.open_and_freeze_action)
587        file_menu.addAction(self.reload_last_action)
588
589        # File -> Open Recent submenu
590        self.recent_menu = QMenu(self.tr("Open Recent"), file_menu)
591        file_menu.addMenu(self.recent_menu)
592        file_menu.addSeparator()
593        file_menu.addAction(self.save_action)
594        file_menu.addAction(self.save_as_action)
595        file_menu.addSeparator()
596        file_menu.addAction(self.show_properties_action)
597        file_menu.addAction(self.quit_action)
598
599        self.recent_menu.addAction(self.recent_action)
600
601        # Store the reference to separator for inserting recent
602        # schemes into the menu in `add_recent_scheme`.
603        self.recent_menu_begin = self.recent_menu.addSeparator()
604
605        # Add recent items.
606        for title, filename in self.recent_schemes:
607            action = QAction(title or self.tr("untitled"), self,
608                             toolTip=filename)
609
610            action.setData(filename)
611            self.recent_menu.addAction(action)
612            self.recent_scheme_action_group.addAction(action)
613
614        self.recent_menu.addSeparator()
615        self.recent_menu.addAction(self.clear_recent_action)
616        menu_bar.addMenu(file_menu)
617
618        editor_menus = self.scheme_widget.menuBarActions()
619
620        # WARNING: Hard coded order, should lookup the action text
621        # and determine the proper order
622        self.edit_menu = editor_menus[0].menu()
623        self.widget_menu = editor_menus[1].menu()
624
625        # Edit menu
626        menu_bar.addMenu(self.edit_menu)
627
628        # View menu
629        self.view_menu = QMenu(self.tr("&View"), self)
630        self.toolbox_menu = QMenu(self.tr("Widget Toolbox Style"),
631                                  self.view_menu)
632        self.toolbox_menu_group = \
633            QActionGroup(self, objectName="toolbox-menu-group")
634
635        self.view_menu.addAction(self.toggle_tool_dock_expand)
636
637        self.view_menu.addSeparator()
638        self.view_menu.addAction(self.toogle_margins_action)
639        menu_bar.addMenu(self.view_menu)
640
641        # Options menu
642        self.options_menu = QMenu(self.tr("&Options"), self)
643        self.options_menu.addAction(self.show_output_action)
644#        self.options_menu.addAction("Add-ons")
645#        self.options_menu.addAction("Developers")
646#        self.options_menu.addAction("Run Discovery")
647#        self.options_menu.addAction("Show Canvas Log")
648#        self.options_menu.addAction("Attach Python Console")
649        self.options_menu.addSeparator()
650        self.options_menu.addAction(self.canvas_settings_action)
651        self.options_menu.addAction(self.canvas_addons_action)
652
653        # Widget menu
654        menu_bar.addMenu(self.widget_menu)
655
656        if sys.platform == "darwin":
657            # Mac OS X native look and feel.
658            self.window_menu = QMenu(self.tr("Window"), self)
659            self.window_menu.addAction(self.minimize_action)
660            self.window_menu.addAction(self.zoom_action)
661            menu_bar.addMenu(self.window_menu)
662
663        menu_bar.addMenu(self.options_menu)
664
665        # Help menu.
666        self.help_menu = QMenu(self.tr("&Help"), self)
667        self.help_menu.addAction(self.about_action)
668        self.help_menu.addAction(self.welcome_action)
669        self.help_menu.addAction(self.tutorials_action)
670        self.help_menu.addAction(self.documentation_action)
671        menu_bar.addMenu(self.help_menu)
672
673        self.setMenuBar(menu_bar)
674
675    def restore(self):
676        """Restore the main window state from saved settings.
677        """
678        QSettings.setDefaultFormat(QSettings.IniFormat)
679        settings = QSettings()
680        settings.beginGroup("mainwindow")
681
682        self.dock_widget.setExpanded(
683            settings.value("canvasdock/expanded", True, type=bool)
684        )
685
686        floatable = settings.value("toolbox-dock-floatable", False, type=bool)
687        if floatable:
688            self.dock_widget.setFeatures(self.dock_widget.features() | \
689                                         QDockWidget.DockWidgetFloatable)
690
691        self.widgets_tool_box.setExclusive(
692            settings.value("toolbox-dock-exclusive", True, type=bool)
693        )
694
695        self.toogle_margins_action.setChecked(
696            settings.value("scheme-margins-enabled", False, type=bool)
697        )
698
699        default_dir = QDesktopServices.storageLocation(
700            QDesktopServices.DocumentsLocation
701        )
702
703        self.last_scheme_dir = settings.value("last-scheme-dir", default_dir,
704                                              type=unicode)
705
706        if not os.path.exists(self.last_scheme_dir):
707            # if directory no longer exists reset the saved location.
708            self.last_scheme_dir = default_dir
709
710        self.canvas_tool_dock.setQuickHelpVisible(
711            settings.value("quick-help/visible", True, type=bool)
712        )
713
714        self.__update_from_settings()
715
716    def set_document_title(self, title):
717        """Set the document title (and the main window title). If `title`
718        is an empty string a default 'untitled' placeholder will be used.
719
720        """
721        if self.__document_title != title:
722            self.__document_title = title
723
724            if not title:
725                # TODO: should the default name be platform specific
726                title = self.tr("untitled")
727
728            self.setWindowTitle(title + "[*]")
729
730    def document_title(self):
731        """Return the document title.
732        """
733        return self.__document_title
734
735    def set_widget_registry(self, widget_registry):
736        """Set widget registry.
737        """
738        if self.widget_registry is not None:
739            # Clear the dock widget and popup.
740            pass
741
742        self.widget_registry = widget_registry
743        self.widgets_tool_box.setModel(widget_registry.model())
744        self.quick_category.setModel(widget_registry.model())
745
746        self.scheme_widget.setRegistry(widget_registry)
747
748        self.help.set_registry(widget_registry)
749
750        # Restore possibly saved widget toolbox tab states
751        settings = QSettings()
752
753        state = settings.value("mainwindow/widgettoolbox/state",
754                                defaultValue=QByteArray(),
755                                type=QByteArray)
756        if state:
757            self.widgets_tool_box.restoreState(state)
758
759    def set_quick_help_text(self, text):
760        self.canvas_tool_dock.help.setText(text)
761
762    def current_document(self):
763        return self.scheme_widget
764
765    def on_tool_box_widget_activated(self, action):
766        """A widget action in the widget toolbox has been activated.
767        """
768        widget_desc = action.data().toPyObject()
769        if widget_desc:
770            scheme_widget = self.current_document()
771            if scheme_widget:
772                scheme_widget.createNewNode(widget_desc)
773
774    def on_quick_category_action(self, action):
775        """The quick category menu action triggered.
776        """
777        category = action.text()
778        if self.use_popover:
779            # Show a popup menu with the widgets in the category
780            popup = CategoryPopupMenu(self.quick_category)
781            reg = self.widget_registry.model()
782            i = index(self.widget_registry.categories(), category,
783                      predicate=lambda name, cat: cat.name == name)
784            if i != -1:
785                popup.setCategoryItem(reg.item(i))
786                button = self.quick_category.buttonForAction(action)
787                pos = popup_position_from_source(popup, button)
788                action = popup.exec_(pos)
789                if action is not None:
790                    self.on_tool_box_widget_activated(action)
791
792        else:
793            for i in range(self.widgets_tool_box.count()):
794                cat_act = self.widgets_tool_box.tabAction(i)
795                cat_act.setChecked(cat_act.text() == category)
796
797            self.dock_widget.expand()
798
799    def set_scheme_margins_enabled(self, enabled):
800        """Enable/disable the margins around the scheme document.
801        """
802        if self.__scheme_margins_enabled != enabled:
803            self.__scheme_margins_enabled = enabled
804            self.__update_scheme_margins()
805
806    def scheme_margins_enabled(self):
807        return self.__scheme_margins_enabled
808
809    scheme_margins_enabled = Property(bool,
810                                      fget=scheme_margins_enabled,
811                                      fset=set_scheme_margins_enabled)
812
813    def __update_scheme_margins(self):
814        """Update the margins around the scheme document.
815        """
816        enabled = self.__scheme_margins_enabled
817        self.__dummy_top_toolbar.setVisible(enabled)
818        self.__dummy_bottom_toolbar.setVisible(enabled)
819        central = self.centralWidget()
820
821        margin = 20 if enabled else 0
822
823        if self.dockWidgetArea(self.dock_widget) == Qt.LeftDockWidgetArea:
824            margins = (margin / 2, 0, margin, 0)
825        else:
826            margins = (margin, 0, margin / 2, 0)
827
828        central.layout().setContentsMargins(*margins)
829
830    #################
831    # Action handlers
832    #################
833    def new_scheme(self):
834        """New scheme. Return QDialog.Rejected if the user canceled
835        the operation and QDialog.Accepted otherwise.
836
837        """
838        document = self.current_document()
839        if document.isModifiedStrict():
840            # Ask for save changes
841            if self.ask_save_changes() == QDialog.Rejected:
842                return QDialog.Rejected
843
844        new_scheme = widgetsscheme.WidgetsScheme(parent=self)
845
846        settings = QSettings()
847        show = settings.value("schemeinfo/show-at-new-scheme", True,
848                              type=bool)
849
850        if show:
851            status = self.show_scheme_properties_for(
852                new_scheme, self.tr("New Scheme")
853            )
854
855            if status == QDialog.Rejected:
856                return QDialog.Rejected
857
858        self.set_new_scheme(new_scheme)
859
860        return QDialog.Accepted
861
862    def open_scheme(self):
863        """Open a new scheme. Return QDialog.Rejected if the user canceled
864        the operation and QDialog.Accepted otherwise.
865
866        """
867        document = self.current_document()
868        if document.isModifiedStrict():
869            if self.ask_save_changes() == QDialog.Rejected:
870                return QDialog.Rejected
871
872        if self.last_scheme_dir is None:
873            # Get user 'Documents' folder
874            start_dir = QDesktopServices.storageLocation(
875                            QDesktopServices.DocumentsLocation)
876        else:
877            start_dir = self.last_scheme_dir
878
879        # TODO: Use a dialog instance and use 'addSidebarUrls' to
880        # set one or more extra sidebar locations where Schemes are stored.
881        # Also use setHistory
882        filename = QFileDialog.getOpenFileName(
883            self, self.tr("Open Orange Scheme File"),
884            start_dir, self.tr("Orange Scheme (*.ows)"),
885        )
886
887        if filename:
888            self.load_scheme(filename)
889            return QDialog.Accepted
890        else:
891            return QDialog.Rejected
892
893    def open_and_freeze_scheme(self):
894        """
895        Open a new scheme and freeze signal propagation. Return
896        QDialog.Rejected if the user canceled the operation and
897        QDialog.Accepted otherwise.
898
899        """
900        frozen = self.freeze_action.isChecked()
901        if not frozen:
902            self.freeze_action.trigger()
903
904        state = self.open_scheme()
905        if state == QDialog.Rejected:
906            # If the action was rejected restore the original frozen state
907            if not frozen:
908                self.freeze_action.trigger()
909        return state
910
911    def open_scheme_file(self, filename):
912        """
913        Open and load a scheme file.
914        """
915        if isinstance(filename, QUrl):
916            filename = filename.toLocalFile()
917
918        document = self.current_document()
919        if document.isModifiedStrict():
920            if self.ask_save_changes() == QDialog.Rejected:
921                return QDialog.Rejected
922
923        self.load_scheme(filename)
924        return QDialog.Accepted
925
926    def load_scheme(self, filename):
927        """Load a scheme from a file (`filename`) into the current
928        document updates the recent scheme list and the loaded scheme path
929        property.
930
931        """
932        filename = unicode(filename)
933        dirname = os.path.dirname(filename)
934
935        self.last_scheme_dir = dirname
936
937        new_scheme = self.new_scheme_from(filename)
938        if new_scheme is not None:
939            self.set_new_scheme(new_scheme)
940
941            scheme_doc_widget = self.current_document()
942            scheme_doc_widget.setPath(filename)
943
944            self.add_recent_scheme(new_scheme.title, filename)
945
946    def new_scheme_from(self, filename):
947        """Create and return a new :class:`widgetsscheme.WidgetsScheme`
948        from a saved `filename`. Return `None` if an error occurs.
949
950        """
951        new_scheme = widgetsscheme.WidgetsScheme(parent=self)
952        errors = []
953        try:
954            parse_scheme(new_scheme, open(filename, "rb"),
955                         error_handler=errors.append,
956                         allow_pickle_data=True)
957        except Exception:
958            message_critical(
959                 self.tr("Could not load an Orange Scheme file"),
960                 title=self.tr("Error"),
961                 informative_text=self.tr("An unexpected error occurred "
962                                          "while loading '%s'.") % filename,
963                 exc_info=True,
964                 parent=self)
965            return None
966        if errors:
967            message_warning(
968                self.tr("Errors occurred while loading the scheme."),
969                title=self.tr("Problem"),
970                informative_text=self.tr(
971                     "There were problems loading some "
972                     "of the widgets/links in the "
973                     "scheme."
974                ),
975                details="\n".join(map(repr, errors))
976            )
977        return new_scheme
978
979    def reload_last(self):
980        """Reload last opened scheme. Return QDialog.Rejected if the
981        user canceled the operation and QDialog.Accepted otherwise.
982
983        """
984        document = self.current_document()
985        if document.isModifiedStrict():
986            if self.ask_save_changes() == QDialog.Rejected:
987                return QDialog.Rejected
988
989        # TODO: Search for a temp backup scheme with per process
990        # locking.
991        if self.recent_schemes:
992            self.load_scheme(self.recent_schemes[0][1])
993
994        return QDialog.Accepted
995
996    def set_new_scheme(self, new_scheme):
997        """
998        Set new_scheme as the current shown scheme. The old scheme
999        will be deleted.
1000
1001        """
1002        scheme_doc = self.current_document()
1003        old_scheme = scheme_doc.scheme()
1004
1005        manager = new_scheme.signal_manager
1006        if self.freeze_action.isChecked():
1007            manager.pause()
1008
1009        scheme_doc.setScheme(new_scheme)
1010
1011        # Send a close event to the Scheme, it is responsible for
1012        # closing/clearing all resources (widgets).
1013        QApplication.sendEvent(old_scheme, QEvent(QEvent.Close))
1014
1015        old_scheme.deleteLater()
1016
1017    def ask_save_changes(self):
1018        """Ask the user to save the changes to the current scheme.
1019        Return QDialog.Accepted if the scheme was successfully saved
1020        or the user selected to discard the changes. Otherwise return
1021        QDialog.Rejected.
1022
1023        """
1024        document = self.current_document()
1025        title = document.scheme().title or "untitled"
1026        selected = message_question(
1027            self.tr('Do you want to save the changes you made to scheme "%s"?')
1028                    % title,
1029            self.tr("Save Changes?"),
1030            self.tr("Your changes will be lost if you do not save them."),
1031            buttons=QMessageBox.Save | QMessageBox.Cancel | \
1032                    QMessageBox.Discard,
1033            default_button=QMessageBox.Save,
1034            parent=self)
1035
1036        if selected == QMessageBox.Save:
1037            return self.save_scheme()
1038        elif selected == QMessageBox.Discard:
1039            return QDialog.Accepted
1040        elif selected == QMessageBox.Cancel:
1041            return QDialog.Rejected
1042
1043    def check_can_save(self, document, path):
1044        """
1045        Check if saving the document to `path` would prevent it from
1046        being read by the version 1.0 of scheme parser. Return ``True``
1047        if the existing scheme is version 1.0 else show a message box and
1048        return ``False``
1049
1050        .. note::
1051            In case of an error (opening, parsing), this method will return
1052            ``True``, so the
1053
1054        """
1055        if path and os.path.exists(path):
1056            try:
1057                version = sniff_version(open(path, "rb"))
1058            except (IOError, OSError):
1059                log.error("Error opening '%s'", path, exc_info=True)
1060                # The client should fail attempting to write.
1061                return True
1062            except Exception:
1063                log.error("Error sniffing scheme version in '%s'", path,
1064                          exc_info=True)
1065                # Malformed .ows file, ...
1066                return True
1067
1068            if version == "1.0":
1069                # TODO: Ask for overwrite confirmation instead
1070                message_information(
1071                    self.tr("Can not overwrite a version 1.0 ows file. "
1072                            "Please save your work to a new file"),
1073                    title="Info",
1074                    parent=self)
1075                return False
1076        return True
1077
1078    def save_scheme(self):
1079        """Save the current scheme. If the scheme does not have an associated
1080        path then prompt the user to select a scheme file. Return
1081        QDialog.Accepted if the scheme was successfully saved and
1082        QDialog.Rejected if the user canceled the file selection.
1083
1084        """
1085        document = self.current_document()
1086        curr_scheme = document.scheme()
1087        path = document.path()
1088
1089        if path and self.check_can_save(document, path):
1090            if self.save_scheme_to(curr_scheme, path):
1091                document.setModified(False)
1092                self.add_recent_scheme(curr_scheme.title, document.path())
1093                return QDialog.Accepted
1094            else:
1095                return QDialog.Rejected
1096        else:
1097            return self.save_scheme_as()
1098
1099    def save_scheme_as(self):
1100        """
1101        Save the current scheme by asking the user for a filename. Return
1102        `QFileDialog.Accepted` if the scheme was saved successfully and
1103        `QFileDialog.Rejected` if not.
1104
1105        """
1106        document = self.current_document()
1107        curr_scheme = document.scheme()
1108        title = curr_scheme.title or "untitled"
1109
1110        if document.path():
1111            start_dir = document.path()
1112        else:
1113            if self.last_scheme_dir is not None:
1114                start_dir = self.last_scheme_dir
1115            else:
1116                start_dir = QDesktopServices.storageLocation(
1117                    QDesktopServices.DocumentsLocation
1118                )
1119
1120            start_dir = os.path.join(unicode(start_dir), title + ".ows")
1121
1122        filename = QFileDialog.getSaveFileName(
1123            self, self.tr("Save Orange Scheme File"),
1124            start_dir, self.tr("Orange Scheme (*.ows)")
1125        )
1126
1127        if filename:
1128            filename = unicode(filename)
1129            if not self.check_can_save(document, filename):
1130                return QDialog.Rejected
1131
1132            self.last_scheme_dir = os.path.dirname(filename)
1133
1134            if self.save_scheme_to(curr_scheme, filename):
1135                document.setPath(filename)
1136                document.setModified(False)
1137                self.add_recent_scheme(curr_scheme.title, document.path())
1138
1139                return QFileDialog.Accepted
1140
1141        return QFileDialog.Rejected
1142
1143    def save_scheme_to(self, scheme, filename):
1144        """
1145        Save a Scheme instance `scheme` to `filename`. On success return
1146        `True`, else show a message to the user explaining the error and
1147        return `False`.
1148
1149        """
1150        dirname, basename = os.path.split(filename)
1151        self.last_scheme_dir = dirname
1152        title = scheme.title or "untitled"
1153        try:
1154            scheme.save_to(open(filename, "wb"),
1155                           pretty=True, pickle_fallback=True)
1156            return True
1157        except (IOError, OSError) as ex:
1158            log.error("%s saving '%s'", type(ex).__name__, filename,
1159                      exc_info=True)
1160            if ex.errno == 2:
1161                # user might enter a string containing a path separator
1162                message_warning(
1163                    self.tr('Scheme "%s" could not be saved. The path does '
1164                            'not exist') % title,
1165                    title="",
1166                    informative_text=self.tr("Choose another location."),
1167                    parent=self
1168                )
1169            elif ex.errno == 13:
1170                message_warning(
1171                    self.tr('Scheme "%s" could not be saved. You do not '
1172                            'have write permissions.') % title,
1173                    title="",
1174                    informative_text=self.tr(
1175                        "Change the file system permissions or choose "
1176                        "another location."),
1177                    parent=self
1178                )
1179            else:
1180                message_warning(
1181                    self.tr('Scheme "%s" could not be saved.') % title,
1182                    title="",
1183                    informative_text=ex.strerror,
1184                    exc_info=True,
1185                    parent=self
1186                )
1187            return False
1188
1189        except Exception:
1190            log.error("Error saving %r to %r", scheme, filename, exc_info=True)
1191            message_critical(
1192                self.tr('An error occurred while trying to save scheme '
1193                        '"%s" to "%s"') % (title, basename),
1194                title=self.tr("Error saving %s") % basename,
1195                exc_info=True,
1196                parent=self
1197            )
1198            return False
1199
1200    def get_started(self, *args):
1201        """Show getting started video
1202        """
1203        url = QUrl(LINKS["start-using"])
1204        QDesktopServices.openUrl(url)
1205
1206    def tutorial(self, *args):
1207        """Show tutorial.
1208        """
1209        url = QUrl(LINKS["tutorial"])
1210        QDesktopServices.openUrl(url)
1211
1212    def documentation(self, *args):
1213        """Show reference documentation.
1214        """
1215        url = QUrl(LINKS["tutorial"])
1216        QDesktopServices.openUrl(url)
1217
1218    def recent_scheme(self, *args):
1219        """Browse recent schemes. Return QDialog.Rejected if the user
1220        canceled the operation and QDialog.Accepted otherwise.
1221
1222        """
1223        items = [previewmodel.PreviewItem(name=title, path=path)
1224                 for title, path in self.recent_schemes]
1225        model = previewmodel.PreviewModel(items=items)
1226
1227        dialog = previewdialog.PreviewDialog(self)
1228        title = self.tr("Recent Schemes")
1229        dialog.setWindowTitle(title)
1230        template = ('<h3 style="font-size: 26px">\n'
1231                    #'<img height="26" src="canvas_icons:Recent.svg">\n'
1232                    '{0}\n'
1233                    '</h3>')
1234        dialog.setHeading(template.format(title))
1235        dialog.setModel(model)
1236
1237        model.delayedScanUpdate()
1238
1239        status = dialog.exec_()
1240
1241        index = dialog.currentIndex()
1242
1243        dialog.deleteLater()
1244        model.deleteLater()
1245
1246        if status == QDialog.Accepted:
1247            doc = self.current_document()
1248            if doc.isModifiedStrict():
1249                if self.ask_save_changes() == QDialog.Rejected:
1250                    return QDialog.Rejected
1251
1252            selected = model.item(index)
1253
1254            self.load_scheme(unicode(selected.path()))
1255
1256        return status
1257
1258    def tutorial_scheme(self, *args):
1259        """Browse a collection of tutorial schemes. Returns QDialog.Rejected
1260        if the user canceled the dialog else loads the selected scheme into
1261        the canvas and returns QDialog.Accepted.
1262
1263        """
1264        tutors = tutorials.tutorials()
1265        items = [previewmodel.PreviewItem(path=t.abspath()) for t in tutors]
1266        model = previewmodel.PreviewModel(items=items)
1267        dialog = previewdialog.PreviewDialog(self)
1268        title = self.tr("Tutorials")
1269        dialog.setWindowTitle(title)
1270        template = ('<h3 style="font-size: 26px">\n'
1271                    #'<img height="26" src="canvas_icons:Tutorials.svg">\n'
1272                    '{0}\n'
1273                    '</h3>')
1274
1275        dialog.setHeading(template.format(title))
1276        dialog.setModel(model)
1277
1278        model.delayedScanUpdate()
1279        status = dialog.exec_()
1280        index = dialog.currentIndex()
1281
1282        dialog.deleteLater()
1283
1284        if status == QDialog.Accepted:
1285            doc = self.current_document()
1286            if doc.isModifiedStrict():
1287                if self.ask_save_changes() == QDialog.Rejected:
1288                    return QDialog.Rejected
1289
1290            selected = model.item(index)
1291
1292            new_scheme = self.new_scheme_from(unicode(selected.path()))
1293            if new_scheme is not None:
1294                self.set_new_scheme(new_scheme)
1295
1296        return status
1297
1298    def welcome_dialog(self):
1299        """Show a modal welcome dialog for Orange Canvas.
1300        """
1301
1302        dialog = welcomedialog.WelcomeDialog(self)
1303        dialog.setWindowTitle(self.tr("Welcome to Orange Data Mining"))
1304
1305        def new_scheme():
1306            if self.new_scheme() == QDialog.Accepted:
1307                dialog.accept()
1308
1309        def open_scheme():
1310            if self.open_scheme() == QDialog.Accepted:
1311                dialog.accept()
1312
1313        def open_recent():
1314            if self.recent_scheme() == QDialog.Accepted:
1315                dialog.accept()
1316
1317        def tutorial():
1318            if self.tutorial_scheme() == QDialog.Accepted:
1319                dialog.accept()
1320
1321        new_action = \
1322            QAction(self.tr("New"), dialog,
1323                    toolTip=self.tr("Open a new scheme."),
1324                    triggered=new_scheme,
1325                    shortcut=QKeySequence.New,
1326                    icon=canvas_icons("New.svg")
1327                    )
1328
1329        open_action = \
1330            QAction(self.tr("Open"), dialog,
1331                    objectName="welcome-action-open",
1332                    toolTip=self.tr("Open a scheme."),
1333                    triggered=open_scheme,
1334                    shortcut=QKeySequence.Open,
1335                    icon=canvas_icons("Open.svg")
1336                    )
1337
1338        recent_action = \
1339            QAction(self.tr("Recent"), dialog,
1340                    objectName="welcome-recent-action",
1341                    toolTip=self.tr("Browse and open a recent scheme."),
1342                    triggered=open_recent,
1343                    shortcut=QKeySequence(Qt.ControlModifier | \
1344                                          (Qt.ShiftModifier | Qt.Key_R)),
1345                    icon=canvas_icons("Recent.svg")
1346                    )
1347
1348        tutorials_action = \
1349            QAction(self.tr("Tutorial"), dialog,
1350                    objectName="welcome-tutorial-action",
1351                    toolTip=self.tr("Browse tutorial schemes."),
1352                    triggered=tutorial,
1353                    icon=canvas_icons("Tutorials.svg")
1354                    )
1355
1356        bottom_row = [self.get_started_action, tutorials_action,
1357                      self.documentation_action]
1358
1359        self.new_action.triggered.connect(dialog.accept)
1360        top_row = [new_action, open_action, recent_action]
1361
1362        dialog.addRow(top_row, background="light-grass")
1363        dialog.addRow(bottom_row, background="light-orange")
1364
1365        settings = QSettings()
1366
1367        dialog.setShowAtStartup(
1368            settings.value("startup/show-welcome-screen", True, type=bool)
1369        )
1370
1371        status = dialog.exec_()
1372
1373        settings.setValue("startup/show-welcome-screen",
1374                          dialog.showAtStartup())
1375
1376        dialog.deleteLater()
1377
1378        return status
1379
1380    def scheme_properties_dialog(self):
1381        """Return an empty `SchemeInfo` dialog instance.
1382        """
1383        settings = QSettings()
1384        value_key = "schemeinfo/show-at-new-scheme"
1385
1386        dialog = SchemeInfoDialog(self)
1387
1388        dialog.setWindowTitle(self.tr("Scheme Info"))
1389        dialog.setFixedSize(725, 450)
1390
1391        dialog.setShowAtNewScheme(
1392            settings.value(value_key, True, type=bool)
1393        )
1394
1395        return dialog
1396
1397    def show_scheme_properties(self):
1398        """Show current scheme properties.
1399        """
1400        settings = QSettings()
1401        value_key = "schemeinfo/show-at-new-scheme"
1402
1403        current_doc = self.current_document()
1404        scheme = current_doc.scheme()
1405        dlg = self.scheme_properties_dialog()
1406        dlg.setAutoCommit(False)
1407        dlg.setScheme(scheme)
1408        status = dlg.exec_()
1409
1410        if status == QDialog.Accepted:
1411            editor = dlg.editor
1412            stack = current_doc.undoStack()
1413            stack.beginMacro(self.tr("Change Info"))
1414            current_doc.setTitle(editor.title())
1415            current_doc.setDescription(editor.description())
1416            stack.endMacro()
1417
1418            # Store the check state.
1419            settings.setValue(value_key, dlg.showAtNewScheme())
1420        return status
1421
1422    def show_scheme_properties_for(self, scheme, window_title=None):
1423        """Show scheme properties for `scheme` with `window_title (if None
1424        a default 'Scheme Info' title will be used.
1425
1426        """
1427        settings = QSettings()
1428        value_key = "schemeinfo/show-at-new-scheme"
1429
1430        dialog = self.scheme_properties_dialog()
1431
1432        if window_title is not None:
1433            dialog.setWindowTitle(window_title)
1434
1435        dialog.setScheme(scheme)
1436
1437        status = dialog.exec_()
1438        if status == QDialog.Accepted:
1439            # Store the check state.
1440            settings.setValue(value_key, dialog.showAtNewScheme())
1441
1442        dialog.deleteLater()
1443
1444        return status
1445
1446    def set_signal_freeze(self, freeze):
1447        scheme = self.current_document().scheme()
1448        manager = scheme.signal_manager
1449        if freeze:
1450            manager.pause()
1451        else:
1452            manager.resume()
1453
1454    def remove_selected(self):
1455        """Remove current scheme selection.
1456        """
1457        self.current_document().removeSelected()
1458
1459    def quit(self):
1460        """Quit the application.
1461        """
1462        if QApplication.activePopupWidget():
1463            # On OSX the actions in the global menu bar are triggered
1464            # even if an popup widget is running it's own event loop
1465            # (in exec_)
1466            log.debug("Ignoring a quit shortcut during an active "
1467                      "popup dialog.")
1468        else:
1469            self.close()
1470
1471    def select_all(self):
1472        self.current_document().selectAll()
1473
1474    def open_widget(self):
1475        """Open/raise selected widget's GUI.
1476        """
1477        self.current_document().openSelected()
1478
1479    def rename_widget(self):
1480        """Rename the current focused widget.
1481        """
1482        doc = self.current_document()
1483        nodes = doc.selectedNodes()
1484        if len(nodes) == 1:
1485            doc.editNodeTitle(nodes[0])
1486
1487    def open_canvas_settings(self):
1488        """Open canvas settings/preferences dialog
1489        """
1490        dlg = UserSettingsDialog(self)
1491        dlg.setWindowTitle(self.tr("Preferences"))
1492        dlg.show()
1493        status = dlg.exec_()
1494        if status == 0:
1495            self.__update_from_settings()
1496
1497    def open_addons(self):
1498
1499        def getlr():
1500            settings = QSettings()
1501            settings.beginGroup("addons")
1502            lastRefresh = settings.value("addons-last-refresh",
1503                          defaultValue=0, type=int)
1504            settings.endGroup()
1505            return lastRefresh
1506       
1507        def setlr(v):
1508            settings = QSettings()
1509            settings.beginGroup("addons")
1510            lastRefresh = settings.setValue("addons-last-refresh", int(v))
1511            settings.endGroup()
1512           
1513        dlg = AddOnManagerDialog(self, self)
1514        dlg.loadtimefn = getlr
1515        dlg.savetimefn = setlr
1516        dlg.show()
1517        dlg.reloadQ()
1518        status = dlg.exec_()
1519
1520    def show_output_view(self):
1521        """Show a window with application output.
1522        """
1523        self.output_dock.show()
1524
1525    def output_view(self):
1526        """Return the output text widget.
1527        """
1528        return self.output_dock.widget()
1529
1530    def open_about(self):
1531        """Open the about dialog.
1532        """
1533        dlg = AboutDialog(self)
1534        dlg.setAttribute(Qt.WA_DeleteOnClose)
1535        dlg.exec_()
1536
1537    def add_recent_scheme(self, title, path):
1538        """Add an entry (`title`, `path`) to the list of recent schemes.
1539        """
1540        if not path:
1541            # No associated persistent path so we can't do anything.
1542            return
1543
1544        if not title:
1545            title = os.path.basename(path)
1546
1547        filename = os.path.abspath(os.path.realpath(path))
1548        filename = os.path.normpath(filename)
1549
1550        actions_by_filename = {}
1551        for action in self.recent_scheme_action_group.actions():
1552            path = unicode(action.data().toString())
1553            actions_by_filename[path] = action
1554
1555        if filename in actions_by_filename:
1556            # Remove the title/filename (so it can be reinserted)
1557            recent_index = index(self.recent_schemes, filename,
1558                                 key=operator.itemgetter(1))
1559            self.recent_schemes.pop(recent_index)
1560
1561            action = actions_by_filename[filename]
1562            self.recent_menu.removeAction(action)
1563            self.recent_scheme_action_group.removeAction(action)
1564            action.setText(title or self.tr("untitled"))
1565        else:
1566            action = QAction(title or self.tr("untitled"), self,
1567                             toolTip=filename)
1568            action.setData(filename)
1569
1570        # Find the separator action in the menu (after 'Browse Recent')
1571        recent_actions = self.recent_menu.actions()
1572        begin_index = index(recent_actions, self.recent_menu_begin)
1573        action_before = recent_actions[begin_index + 1]
1574
1575        self.recent_menu.insertAction(action_before, action)
1576        self.recent_scheme_action_group.addAction(action)
1577        self.recent_schemes.insert(0, (title, filename))
1578
1579        if len(self.recent_schemes) > max(self.num_recent_schemes, 1):
1580            title, filename = self.recent_schemes.pop(-1)
1581            action = actions_by_filename[filename]
1582            self.recent_menu.removeAction(action)
1583            self.recent_scheme_action_group.removeAction(action)
1584
1585        config.save_recent_scheme_list(self.recent_schemes)
1586
1587    def clear_recent_schemes(self):
1588        """Clear list of recent schemes
1589        """
1590        actions = list(self.recent_menu.actions())
1591
1592        # Exclude permanent actions (Browse Recent, separators, Clear List)
1593        actions_to_remove = [action for action in actions \
1594                             if unicode(action.data().toString())]
1595
1596        for action in actions_to_remove:
1597            self.recent_menu.removeAction(action)
1598            self.recent_scheme_action_group.removeAction(action)
1599
1600        self.recent_schemes = []
1601        config.save_recent_scheme_list([])
1602
1603    def _on_recent_scheme_action(self, action):
1604        """A recent scheme action was triggered by the user
1605        """
1606        document = self.current_document()
1607        if document.isModifiedStrict():
1608            if self.ask_save_changes() == QDialog.Rejected:
1609                return
1610
1611        filename = unicode(action.data().toString())
1612        self.load_scheme(filename)
1613
1614    def _on_dock_location_changed(self, location):
1615        """Location of the dock_widget has changed, fix the margins
1616        if necessary.
1617
1618        """
1619        self.__update_scheme_margins()
1620
1621    def set_tool_dock_expanded(self, expanded):
1622        """
1623        Set the dock widget expanded state.
1624        """
1625        self.dock_widget.setExpanded(expanded)
1626
1627    def _on_tool_dock_expanded(self, expanded):
1628        """
1629        'dock_widget' widget was expanded/collapsed.
1630        """
1631        if expanded != self.toggle_tool_dock_expand.isChecked():
1632            self.toggle_tool_dock_expand.setChecked(expanded)
1633
1634    def createPopupMenu(self):
1635        # Override the default context menu popup (we don't want the user to
1636        # be able to hide the tool dock widget).
1637        return None
1638
1639    def closeEvent(self, event):
1640        """Close the main window.
1641        """
1642        document = self.current_document()
1643        if document.isModifiedStrict():
1644            if self.ask_save_changes() == QDialog.Rejected:
1645                # Reject the event
1646                event.ignore()
1647                return
1648
1649        old_scheme = document.scheme()
1650
1651        # Set an empty scheme to clear the document
1652        document.setScheme(widgetsscheme.WidgetsScheme())
1653
1654        QApplication.sendEvent(old_scheme, QEvent(QEvent.Close))
1655
1656        old_scheme.deleteLater()
1657
1658        config.save_config()
1659
1660        geometry = self.saveGeometry()
1661        state = self.saveState(version=self.SETTINGS_VERSION)
1662        settings = QSettings()
1663        settings.beginGroup("mainwindow")
1664        settings.setValue("geometry", geometry)
1665        settings.setValue("state", state)
1666        settings.setValue("canvasdock/expanded",
1667                          self.dock_widget.expanded())
1668        settings.setValue("scheme-margins-enabled",
1669                          self.scheme_margins_enabled)
1670
1671        settings.setValue("last-scheme-dir", self.last_scheme_dir)
1672        settings.setValue("widgettoolbox/state",
1673                          self.widgets_tool_box.saveState())
1674
1675        settings.setValue("quick-help/visible",
1676                          self.canvas_tool_dock.quickHelpVisible())
1677
1678        settings.endGroup()
1679
1680        event.accept()
1681
1682        # Close any windows left.
1683        application = QApplication.instance()
1684        QTimer.singleShot(0, application.closeAllWindows)
1685
1686    def showEvent(self, event):
1687        if self.__first_show:
1688            settings = QSettings()
1689            settings.beginGroup("mainwindow")
1690
1691            # Restore geometry and dock/toolbar state
1692            state = settings.value("state", QByteArray(), type=QByteArray)
1693            if state:
1694                self.restoreState(state, version=self.SETTINGS_VERSION)
1695
1696            geom_data = settings.value("geometry", QByteArray(),
1697                                       type=QByteArray)
1698            if geom_data:
1699                self.restoreGeometry(geom_data)
1700
1701            self.__first_show = False
1702
1703        return QMainWindow.showEvent(self, event)
1704
1705    def event(self, event):
1706        if event.type() == QEvent.StatusTip and \
1707                isinstance(event, QuickHelpTipEvent):
1708            # Using singleShot to update the text browser.
1709            # If updating directly the application experiences strange random
1710            # segfaults (in ~StatusTipEvent in QTextLayout or event just normal
1711            # event loop), but only when the contents are larger then the
1712            # QTextBrowser's viewport.
1713            if event.priority() == QuickHelpTipEvent.Normal:
1714                QTimer.singleShot(0, partial(self.dock_help.showHelp,
1715                                             event.html()))
1716            elif event.priority() == QuickHelpTipEvent.Temporary:
1717                QTimer.singleShot(0, partial(self.dock_help.showHelp,
1718                                             event.html(), event.timeout()))
1719            elif event.priority() == QuickHelpTipEvent.Permanent:
1720                QTimer.singleShot(0, partial(self.dock_help.showPermanentHelp,
1721                                             event.html()))
1722
1723            return True
1724
1725        elif event.type() == QEvent.WhatsThisClicked:
1726            ref = event.href()
1727            url = QUrl(ref)
1728
1729            if url.scheme() == "help" and url.authority() == "search":
1730                try:
1731                    url = self.help.search(url)
1732                except KeyError:
1733                    url = None
1734                    log.info("No help topic found for %r", url)
1735
1736            if url:
1737                self.show_help(url)
1738            else:
1739                message_information(
1740                    self.tr("Sorry there is no documentation available for "
1741                            "this widget."),
1742                    parent=self)
1743
1744            return True
1745
1746        return QMainWindow.event(self, event)
1747
1748    def show_help(self, url):
1749        """
1750        Show `url` in a help window.
1751        """
1752        log.info("Setting help to url: %r", url)
1753        if self.open_in_external_browser:
1754            url = QUrl(url)
1755            if not QDesktopServices.openUrl(url):
1756                # Try fixing some common problems.
1757                url = QUrl.fromUserInput(url.toString())
1758                # 'fromUserInput' includes possible fragment into the path
1759                # (which prevents it to open local files) so we reparse it
1760                # again.
1761                url = QUrl(url.toString())
1762                QDesktopServices.openUrl(url)
1763        else:
1764            self.help_view.load(QUrl(url))
1765            self.help_dock.show()
1766            self.help_dock.raise_()
1767
1768    # Mac OS X
1769    if sys.platform == "darwin":
1770        def toggleMaximized(self):
1771            """Toggle normal/maximized window state.
1772            """
1773            if self.isMinimized():
1774                # Do nothing if window is minimized
1775                return
1776
1777            if self.isMaximized():
1778                self.showNormal()
1779            else:
1780                self.showMaximized()
1781
1782        def changeEvent(self, event):
1783            if event.type() == QEvent.WindowStateChange:
1784                # Can get 'Qt.WindowNoState' before the widget is fully
1785                # initialized
1786                if hasattr(self, "window_state"):
1787                    # Enable/disable window menu based on minimized state
1788                    self.window_menu.setEnabled(not self.isMinimized())
1789
1790            QMainWindow.changeEvent(self, event)
1791
1792    def sizeHint(self):
1793        """
1794        Reimplemented from QMainWindow.sizeHint
1795        """
1796        hint = QMainWindow.sizeHint(self)
1797        return hint.expandedTo(QSize(1024, 720))
1798
1799    def tr(self, sourceText, disambiguation=None, n=-1):
1800        """Translate the string.
1801        """
1802        return unicode(QMainWindow.tr(self, sourceText, disambiguation, n))
1803
1804    def __update_from_settings(self):
1805        settings = QSettings()
1806        settings.beginGroup("mainwindow")
1807        toolbox_floatable = settings.value("toolbox-dock-floatable",
1808                                           defaultValue=False,
1809                                           type=bool)
1810
1811        features = self.dock_widget.features()
1812        features = updated_flags(features, QDockWidget.DockWidgetFloatable,
1813                                 toolbox_floatable)
1814        self.dock_widget.setFeatures(features)
1815
1816        toolbox_exclusive = settings.value("toolbox-dock-exclusive",
1817                                           defaultValue=True,
1818                                           type=bool)
1819        self.widgets_tool_box.setExclusive(toolbox_exclusive)
1820
1821        self.num_recent_schemes = settings.value("num-recent-schemes",
1822                                                 defaultValue=15,
1823                                                 type=int)
1824
1825        settings.endGroup()
1826        settings.beginGroup("quickmenu")
1827
1828        triggers = 0
1829        dbl_click = settings.value("trigger-on-double-click",
1830                                   defaultValue=True,
1831                                   type=bool)
1832        if dbl_click:
1833            triggers |= SchemeEditWidget.DoubleClicked
1834
1835        right_click = settings.value("trigger-on-right-click",
1836                                    defaultValue=True,
1837                                    type=bool)
1838        if right_click:
1839            triggers |= SchemeEditWidget.RightClicked
1840
1841        space_press = settings.value("trigger-on-space-key",
1842                                     defaultValue=True,
1843                                     type=bool)
1844        if space_press:
1845            triggers |= SchemeEditWidget.SpaceKey
1846
1847        any_press = settings.value("trigger-on-any-key",
1848                                   defaultValue=False,
1849                                   type=bool)
1850        if any_press:
1851            triggers |= SchemeEditWidget.AnyKey
1852
1853        self.scheme_widget.setQuickMenuTriggers(triggers)
1854
1855        settings.endGroup()
1856        settings.beginGroup("schemeedit")
1857        show_channel_names = settings.value("show-channel-names",
1858                                            defaultValue=True,
1859                                            type=bool)
1860        self.scheme_widget.setChannelNamesVisible(show_channel_names)
1861
1862        node_animations = settings.value("enable-node-animations",
1863                                         defaultValue=False,
1864                                         type=bool)
1865        self.scheme_widget.setNodeAnimationEnabled(node_animations)
1866        settings.endGroup()
1867
1868        settings.beginGroup("output")
1869        stay_on_top = settings.value("stay-on-top", defaultValue=True,
1870                                     type=bool)
1871        if stay_on_top:
1872            self.output_dock.setFloatingWindowFlags(Qt.Tool)
1873        else:
1874            self.output_dock.setFloatingWindowFlags(Qt.Window)
1875
1876        dockable = settings.value("dockable", defaultValue=True,
1877                                  type=bool)
1878        if dockable:
1879            self.output_dock.setAllowedAreas(Qt.BottomDockWidgetArea)
1880        else:
1881            self.output_dock.setAllowedAreas(Qt.NoDockWidgetArea)
1882
1883        settings.endGroup()
1884
1885        settings.beginGroup("help")
1886        stay_on_top = settings.value("stay-on-top", defaultValue=True,
1887                                     type=bool)
1888        if stay_on_top:
1889            self.help_dock.setFloatingWindowFlags(Qt.Tool)
1890        else:
1891            self.help_dock.setFloatingWindowFlags(Qt.Window)
1892
1893        dockable = settings.value("dockable", defaultValue=False,
1894                                  type=bool)
1895        if dockable:
1896            self.help_dock.setAllowedAreas(Qt.LeftDockWidgetArea | \
1897                                           Qt.RightDockWidgetArea)
1898        else:
1899            self.help_dock.setAllowedAreas(Qt.NoDockWidgetArea)
1900
1901        self.open_in_external_browser = \
1902            settings.value("open-in-external-browser", defaultValue=False,
1903                           type=bool)
1904
1905        self.use_popover = \
1906            settings.value("toolbox-dock-use-popover-menu", defaultValue=True,
1907                           type=bool)
1908
1909
1910def updated_flags(flags, mask, state):
1911    if state:
1912        flags |= mask
1913    else:
1914        flags &= ~mask
1915    return flags
1916
1917
1918def identity(item):
1919    return item
1920
1921
1922def index(sequence, *what, **kwargs):
1923    """index(sequence, what, [key=None, [predicate=None]])
1924
1925    Return index of `what` in `sequence`.
1926
1927    """
1928    what = what[0]
1929    key = kwargs.get("key", identity)
1930    predicate = kwargs.get("predicate", operator.eq)
1931    for i, item in enumerate(sequence):
1932        item_key = key(item)
1933        if predicate(what, item_key):
1934            return i
1935    raise ValueError("%r not in sequence" % what)
Note: See TracBrowser for help on using the repository browser.