source: orange/Orange/OrangeCanvas/application/canvasmain.py @ 11487:912e57db9317

Revision 11487:912e57db9317, 61.0 KB checked in by Ales Erjavec <ales.erjavec@…>, 12 months ago (diff)

Finalise the WidgetsScheme on a 'Close' event.

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