source: orange/Orange/OrangeCanvas/application/canvasmain.py @ 11545:0281bd5902b0

Revision 11545:0281bd5902b0, 66.1 KB checked in by Ales Erjavec <ales.erjavec@…>, 11 months ago (diff)

Refactored scheme save methods. Added common error handing.

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