source: orange/Orange/OrangeCanvas/application/canvasmain.py @ 11527:c12e64d1b72b

Revision 11527:c12e64d1b72b, 64.0 KB checked in by Ales Erjavec <ales.erjavec@…>, 11 months ago (diff)

Limit the number of recent schemes shown.

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 %r?") \
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.
1020
1021        """
1022        if path and os.path.exists(path):
1023            version = sniff_version(open(path, "rb"))
1024            if version == "1.0":
1025                message_information(
1026                    self.tr("Can not overwrite a version 1.0 ows file. "
1027                            "Please save your work to a new file"),
1028                    title="Info",
1029                    parent=self)
1030                return False
1031        return True
1032
1033    def save_scheme(self):
1034        """Save the current scheme. If the scheme does not have an associated
1035        path then prompt the user to select a scheme file. Return
1036        QDialog.Accepted if the scheme was successfully saved and
1037        QDialog.Rejected if the user canceled the file selection.
1038
1039        """
1040        document = self.current_document()
1041        curr_scheme = document.scheme()
1042
1043        if document.path() and self.check_can_save(document, document.path()):
1044            curr_scheme.save_to(open(document.path(), "wb"),
1045                                pretty=True, pickle_fallback=True)
1046
1047            document.setModified(False)
1048            self.add_recent_scheme(curr_scheme.title, document.path())
1049            return QDialog.Accepted
1050        else:
1051            return self.save_scheme_as()
1052
1053    def save_scheme_as(self):
1054        """Save the current scheme by asking the user for a filename.
1055        Return QFileDialog.Accepted if the scheme was saved successfully
1056        and QFileDialog.Rejected if not.
1057
1058        """
1059        document = self.current_document()
1060        curr_scheme = document.scheme()
1061
1062        if document.path():
1063            start_dir = document.path()
1064        else:
1065            if self.last_scheme_dir is not None:
1066                start_dir = self.last_scheme_dir
1067            else:
1068                start_dir = QDesktopServices.storageLocation(
1069                    QDesktopServices.DocumentsLocation
1070                )
1071
1072            title = curr_scheme.title or "untitled"
1073            start_dir = os.path.join(unicode(start_dir), title + ".ows")
1074
1075        filename = QFileDialog.getSaveFileName(
1076            self, self.tr("Save Orange Scheme File"),
1077            start_dir, self.tr("Orange Scheme (*.ows)")
1078        )
1079
1080        if filename:
1081            filename = unicode(filename)
1082            if not self.check_can_save(document, filename):
1083                return QDialog.Rejected
1084
1085            dirname, basename = os.path.split(filename)
1086            self.last_scheme_dir = dirname
1087
1088            try:
1089                curr_scheme.save_to(open(filename, "wb"),
1090                                    pretty=True, pickle_fallback=True)
1091            except Exception:
1092                log.error("Error saving %r to %r", curr_scheme, filename,
1093                          exc_info=True)
1094                # Also show a message box
1095                # TODO: should handle permission errors with a
1096                # specialized messages.
1097                message_critical(
1098                     self.tr("An error occurred while trying to save the %r "
1099                             "scheme to %r" % \
1100                             (curr_scheme.title, basename)),
1101                     title=self.tr("Error saving %r") % basename,
1102                     exc_info=True,
1103                     parent=self)
1104                return QFileDialog.Rejected
1105
1106            document.setPath(filename)
1107
1108            document.setModified(False)
1109            self.add_recent_scheme(curr_scheme.title, document.path())
1110            return QFileDialog.Accepted
1111        else:
1112            return QFileDialog.Rejected
1113
1114    def get_started(self, *args):
1115        """Show getting started video
1116        """
1117        url = QUrl(LINKS["start-using"])
1118        QDesktopServices.openUrl(url)
1119
1120    def tutorial(self, *args):
1121        """Show tutorial.
1122        """
1123        url = QUrl(LINKS["tutorial"])
1124        QDesktopServices.openUrl(url)
1125
1126    def documentation(self, *args):
1127        """Show reference documentation.
1128        """
1129        url = QUrl(LINKS["tutorial"])
1130        QDesktopServices.openUrl(url)
1131
1132    def recent_scheme(self, *args):
1133        """Browse recent schemes. Return QDialog.Rejected if the user
1134        canceled the operation and QDialog.Accepted otherwise.
1135
1136        """
1137        items = [previewmodel.PreviewItem(name=title, path=path)
1138                 for title, path in self.recent_schemes]
1139        model = previewmodel.PreviewModel(items=items)
1140
1141        dialog = previewdialog.PreviewDialog(self)
1142        title = self.tr("Recent Schemes")
1143        dialog.setWindowTitle(title)
1144        template = ('<h3 style="font-size: 26px">\n'
1145                    #'<img height="26" src="canvas_icons:Recent.svg">\n'
1146                    '{0}\n'
1147                    '</h3>')
1148        dialog.setHeading(template.format(title))
1149        dialog.setModel(model)
1150
1151        model.delayedScanUpdate()
1152
1153        status = dialog.exec_()
1154
1155        index = dialog.currentIndex()
1156
1157        dialog.deleteLater()
1158        model.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            self.load_scheme(unicode(selected.path()))
1169
1170        return status
1171
1172    def tutorial_scheme(self, *args):
1173        """Browse a collection of tutorial schemes. Returns QDialog.Rejected
1174        if the user canceled the dialog else loads the selected scheme into
1175        the canvas and returns QDialog.Accepted.
1176
1177        """
1178        tutors = tutorials.tutorials()
1179        items = [previewmodel.PreviewItem(path=t.abspath()) for t in tutors]
1180        model = previewmodel.PreviewModel(items=items)
1181        dialog = previewdialog.PreviewDialog(self)
1182        title = self.tr("Tutorials")
1183        dialog.setWindowTitle(title)
1184        template = ('<h3 style="font-size: 26px">\n'
1185                    #'<img height="26" src="canvas_icons:Tutorials.svg">\n'
1186                    '{0}\n'
1187                    '</h3>')
1188
1189        dialog.setHeading(template.format(title))
1190        dialog.setModel(model)
1191
1192        model.delayedScanUpdate()
1193        status = dialog.exec_()
1194        index = dialog.currentIndex()
1195
1196        dialog.deleteLater()
1197
1198        if status == QDialog.Accepted:
1199            doc = self.current_document()
1200            if doc.isModifiedStrict():
1201                if self.ask_save_changes() == QDialog.Rejected:
1202                    return QDialog.Rejected
1203
1204            selected = model.item(index)
1205
1206            new_scheme = self.new_scheme_from(unicode(selected.path()))
1207            if new_scheme is not None:
1208                self.set_new_scheme(new_scheme)
1209
1210        return status
1211
1212    def welcome_dialog(self):
1213        """Show a modal welcome dialog for Orange Canvas.
1214        """
1215
1216        dialog = welcomedialog.WelcomeDialog(self)
1217        dialog.setWindowTitle(self.tr("Welcome to Orange Data Mining"))
1218
1219        def new_scheme():
1220            if self.new_scheme() == QDialog.Accepted:
1221                dialog.accept()
1222
1223        def open_scheme():
1224            if self.open_scheme() == QDialog.Accepted:
1225                dialog.accept()
1226
1227        def open_recent():
1228            if self.recent_scheme() == QDialog.Accepted:
1229                dialog.accept()
1230
1231        def tutorial():
1232            if self.tutorial_scheme() == QDialog.Accepted:
1233                dialog.accept()
1234
1235        new_action = \
1236            QAction(self.tr("New"), dialog,
1237                    toolTip=self.tr("Open a new scheme."),
1238                    triggered=new_scheme,
1239                    shortcut=QKeySequence.New,
1240                    icon=canvas_icons("New.svg")
1241                    )
1242
1243        open_action = \
1244            QAction(self.tr("Open"), dialog,
1245                    objectName="welcome-action-open",
1246                    toolTip=self.tr("Open a scheme."),
1247                    triggered=open_scheme,
1248                    shortcut=QKeySequence.Open,
1249                    icon=canvas_icons("Open.svg")
1250                    )
1251
1252        recent_action = \
1253            QAction(self.tr("Recent"), dialog,
1254                    objectName="welcome-recent-action",
1255                    toolTip=self.tr("Browse and open a recent scheme."),
1256                    triggered=open_recent,
1257                    shortcut=QKeySequence(Qt.ControlModifier | \
1258                                          (Qt.ShiftModifier | Qt.Key_R)),
1259                    icon=canvas_icons("Recent.svg")
1260                    )
1261
1262        tutorials_action = \
1263            QAction(self.tr("Tutorial"), dialog,
1264                    objectName="welcome-tutorial-action",
1265                    toolTip=self.tr("Browse tutorial schemes."),
1266                    triggered=tutorial,
1267                    icon=canvas_icons("Tutorials.svg")
1268                    )
1269
1270        bottom_row = [self.get_started_action, tutorials_action,
1271                      self.documentation_action]
1272
1273        self.new_action.triggered.connect(dialog.accept)
1274        top_row = [new_action, open_action, recent_action]
1275
1276        dialog.addRow(top_row, background="light-grass")
1277        dialog.addRow(bottom_row, background="light-orange")
1278
1279        settings = QSettings()
1280
1281        dialog.setShowAtStartup(
1282            settings.value("startup/show-welcome-screen", True, type=bool)
1283        )
1284
1285        status = dialog.exec_()
1286
1287        settings.setValue("startup/show-welcome-screen",
1288                          dialog.showAtStartup())
1289
1290        dialog.deleteLater()
1291
1292        return status
1293
1294    def scheme_properties_dialog(self):
1295        """Return an empty `SchemeInfo` dialog instance.
1296        """
1297        settings = QSettings()
1298        value_key = "schemeinfo/show-at-new-scheme"
1299
1300        dialog = SchemeInfoDialog(self)
1301
1302        dialog.setWindowTitle(self.tr("Scheme Info"))
1303        dialog.setFixedSize(725, 450)
1304
1305        dialog.setShowAtNewScheme(
1306            settings.value(value_key, True, type=bool)
1307        )
1308
1309        return dialog
1310
1311    def show_scheme_properties(self):
1312        """Show current scheme properties.
1313        """
1314        settings = QSettings()
1315        value_key = "schemeinfo/show-at-new-scheme"
1316
1317        current_doc = self.current_document()
1318        scheme = current_doc.scheme()
1319        dlg = self.scheme_properties_dialog()
1320        dlg.setAutoCommit(False)
1321        dlg.setScheme(scheme)
1322        status = dlg.exec_()
1323
1324        if status == QDialog.Accepted:
1325            editor = dlg.editor
1326            stack = current_doc.undoStack()
1327            stack.beginMacro(self.tr("Change Info"))
1328            current_doc.setTitle(editor.title())
1329            current_doc.setDescription(editor.description())
1330            stack.endMacro()
1331
1332            # Store the check state.
1333            settings.setValue(value_key, dlg.showAtNewScheme())
1334        return status
1335
1336    def show_scheme_properties_for(self, scheme, window_title=None):
1337        """Show scheme properties for `scheme` with `window_title (if None
1338        a default 'Scheme Info' title will be used.
1339
1340        """
1341        settings = QSettings()
1342        value_key = "schemeinfo/show-at-new-scheme"
1343
1344        dialog = self.scheme_properties_dialog()
1345
1346        if window_title is not None:
1347            dialog.setWindowTitle(window_title)
1348
1349        dialog.setScheme(scheme)
1350
1351        status = dialog.exec_()
1352        if status == QDialog.Accepted:
1353            # Store the check state.
1354            settings.setValue(value_key, dialog.showAtNewScheme())
1355
1356        dialog.deleteLater()
1357
1358        return status
1359
1360    def set_signal_freeze(self, freeze):
1361        scheme = self.current_document().scheme()
1362        manager = scheme.signal_manager
1363        if freeze:
1364            manager.pause()
1365        else:
1366            manager.resume()
1367
1368    def remove_selected(self):
1369        """Remove current scheme selection.
1370        """
1371        self.current_document().removeSelected()
1372
1373    def quit(self):
1374        """Quit the application.
1375        """
1376        if QApplication.activePopupWidget():
1377            # On OSX the actions in the global menu bar are triggered
1378            # even if an popup widget is running it's own event loop
1379            # (in exec_)
1380            log.debug("Ignoring a quit shortcut during an active "
1381                      "popup dialog.")
1382        else:
1383            self.close()
1384
1385    def select_all(self):
1386        self.current_document().selectAll()
1387
1388    def open_widget(self):
1389        """Open/raise selected widget's GUI.
1390        """
1391        self.current_document().openSelected()
1392
1393    def rename_widget(self):
1394        """Rename the current focused widget.
1395        """
1396        doc = self.current_document()
1397        nodes = doc.selectedNodes()
1398        if len(nodes) == 1:
1399            doc.editNodeTitle(nodes[0])
1400
1401    def open_canvas_settings(self):
1402        """Open canvas settings/preferences dialog
1403        """
1404        dlg = UserSettingsDialog(self)
1405        dlg.show()
1406        status = dlg.exec_()
1407        if status == 0:
1408            self.__update_from_settings()
1409
1410    def open_addons(self):
1411
1412        def getlr():
1413            settings = QSettings()
1414            settings.beginGroup("addons")
1415            lastRefresh = settings.value("addons-last-refresh",
1416                          defaultValue=0, type=int)
1417            settings.endGroup()
1418            return lastRefresh
1419       
1420        def setlr(v):
1421            settings = QSettings()
1422            settings.beginGroup("addons")
1423            lastRefresh = settings.setValue("addons-last-refresh", int(v))
1424            settings.endGroup()
1425           
1426        dlg = AddOnManagerDialog(self, self)
1427        dlg.loadtimefn = getlr
1428        dlg.savetimefn = setlr
1429        dlg.show()
1430        dlg.reloadQ()
1431        status = dlg.exec_()
1432
1433    def show_output_view(self):
1434        """Show a window with application output.
1435        """
1436        self.output_dock.show()
1437
1438    def output_view(self):
1439        """Return the output text widget.
1440        """
1441        return self.output_dock.widget()
1442
1443    def open_about(self):
1444        """Open the about dialog.
1445        """
1446        dlg = AboutDialog(self)
1447        dlg.setAttribute(Qt.WA_DeleteOnClose)
1448        dlg.exec_()
1449
1450    def add_recent_scheme(self, title, path):
1451        """Add an entry (`title`, `path`) to the list of recent schemes.
1452        """
1453        if not path:
1454            # No associated persistent path so we can't do anything.
1455            return
1456
1457        if not title:
1458            title = os.path.basename(path)
1459
1460        filename = os.path.abspath(os.path.realpath(path))
1461        filename = os.path.normpath(filename)
1462
1463        actions_by_filename = {}
1464        for action in self.recent_scheme_action_group.actions():
1465            path = unicode(action.data().toString())
1466            actions_by_filename[path] = action
1467
1468        if filename in actions_by_filename:
1469            # Remove the title/filename (so it can be reinserted)
1470            recent_index = index(self.recent_schemes, filename,
1471                                 key=operator.itemgetter(1))
1472            self.recent_schemes.pop(recent_index)
1473
1474            action = actions_by_filename[filename]
1475            self.recent_menu.removeAction(action)
1476            self.recent_scheme_action_group.removeAction(action)
1477            action.setText(title or self.tr("untitled"))
1478        else:
1479            action = QAction(title or self.tr("untitled"), self,
1480                             toolTip=filename)
1481            action.setData(filename)
1482
1483        # Find the separator action in the menu (after 'Browse Recent')
1484        recent_actions = self.recent_menu.actions()
1485        begin_index = index(recent_actions, self.recent_menu_begin)
1486        action_before = recent_actions[begin_index + 1]
1487
1488        self.recent_menu.insertAction(action_before, action)
1489        self.recent_scheme_action_group.addAction(action)
1490        self.recent_schemes.insert(0, (title, filename))
1491
1492        if len(self.recent_schemes) > max(self.num_recent_schemes, 1):
1493            title, filename = self.recent_schemes.pop(-1)
1494            action = actions_by_filename[filename]
1495            self.recent_menu.removeAction(action)
1496            self.recent_scheme_action_group.removeAction(action)
1497
1498        config.save_recent_scheme_list(self.recent_schemes)
1499
1500    def clear_recent_schemes(self):
1501        """Clear list of recent schemes
1502        """
1503        actions = list(self.recent_menu.actions())
1504
1505        # Exclude permanent actions (Browse Recent, separators, Clear List)
1506        actions_to_remove = [action for action in actions \
1507                             if unicode(action.data().toString())]
1508
1509        for action in actions_to_remove:
1510            self.recent_menu.removeAction(action)
1511            self.recent_scheme_action_group.removeAction(action)
1512
1513        self.recent_schemes = []
1514        config.save_recent_scheme_list([])
1515
1516    def _on_recent_scheme_action(self, action):
1517        """A recent scheme action was triggered by the user
1518        """
1519        document = self.current_document()
1520        if document.isModifiedStrict():
1521            if self.ask_save_changes() == QDialog.Rejected:
1522                return
1523
1524        filename = unicode(action.data().toString())
1525        self.load_scheme(filename)
1526
1527    def _on_dock_location_changed(self, location):
1528        """Location of the dock_widget has changed, fix the margins
1529        if necessary.
1530
1531        """
1532        self.__update_scheme_margins()
1533
1534    def set_tool_dock_expanded(self, expanded):
1535        """
1536        Set the dock widget expanded state.
1537        """
1538        self.dock_widget.setExpanded(expanded)
1539
1540    def _on_tool_dock_expanded(self, expanded):
1541        """
1542        'dock_widget' widget was expanded/collapsed.
1543        """
1544        if expanded != self.toggle_tool_dock_expand.isChecked():
1545            self.toggle_tool_dock_expand.setChecked(expanded)
1546
1547    def createPopupMenu(self):
1548        # Override the default context menu popup (we don't want the user to
1549        # be able to hide the tool dock widget).
1550        return None
1551
1552    def closeEvent(self, event):
1553        """Close the main window.
1554        """
1555        document = self.current_document()
1556        if document.isModifiedStrict():
1557            if self.ask_save_changes() == QDialog.Rejected:
1558                # Reject the event
1559                event.ignore()
1560                return
1561
1562        old_scheme = document.scheme()
1563
1564        # Set an empty scheme to clear the document
1565        document.setScheme(widgetsscheme.WidgetsScheme())
1566
1567        QApplication.sendEvent(old_scheme, QEvent(QEvent.Close))
1568
1569        old_scheme.deleteLater()
1570
1571        config.save_config()
1572
1573        geometry = self.saveGeometry()
1574        state = self.saveState(version=self.SETTINGS_VERSION)
1575        settings = QSettings()
1576        settings.beginGroup("mainwindow")
1577        settings.setValue("geometry", geometry)
1578        settings.setValue("state", state)
1579        settings.setValue("canvasdock/expanded",
1580                          self.dock_widget.expanded())
1581        settings.setValue("scheme-margins-enabled",
1582                          self.scheme_margins_enabled)
1583
1584        settings.setValue("last-scheme-dir", self.last_scheme_dir)
1585        settings.setValue("widgettoolbox/state",
1586                          self.widgets_tool_box.saveState())
1587
1588        settings.setValue("quick-help/visible",
1589                          self.canvas_tool_dock.quickHelpVisible())
1590
1591        settings.endGroup()
1592
1593        event.accept()
1594
1595        # Close any windows left.
1596        application = QApplication.instance()
1597        QTimer.singleShot(0, application.closeAllWindows)
1598
1599    def showEvent(self, event):
1600        if self.__first_show:
1601            settings = QSettings()
1602            settings.beginGroup("mainwindow")
1603
1604            # Restore geometry and dock/toolbar state
1605            state = settings.value("state", QByteArray(), type=QByteArray)
1606            if state:
1607                self.restoreState(state, version=self.SETTINGS_VERSION)
1608
1609            geom_data = settings.value("geometry", QByteArray(),
1610                                       type=QByteArray)
1611            if geom_data:
1612                self.restoreGeometry(geom_data)
1613
1614            self.__first_show = False
1615
1616        return QMainWindow.showEvent(self, event)
1617
1618    def event(self, event):
1619        if event.type() == QEvent.StatusTip and \
1620                isinstance(event, QuickHelpTipEvent):
1621            # Using singleShot to update the text browser.
1622            # If updating directly the application experiences strange random
1623            # segfaults (in ~StatusTipEvent in QTextLayout or event just normal
1624            # event loop), but only when the contents are larger then the
1625            # QTextBrowser's viewport.
1626            if event.priority() == QuickHelpTipEvent.Normal:
1627                QTimer.singleShot(0, partial(self.dock_help.showHelp,
1628                                             event.html()))
1629            elif event.priority() == QuickHelpTipEvent.Temporary:
1630                QTimer.singleShot(0, partial(self.dock_help.showHelp,
1631                                             event.html(), event.timeout()))
1632            elif event.priority() == QuickHelpTipEvent.Permanent:
1633                QTimer.singleShot(0, partial(self.dock_help.showPermanentHelp,
1634                                             event.html()))
1635
1636            return True
1637
1638        elif event.type() == QEvent.WhatsThisClicked:
1639            ref = event.href()
1640            url = QUrl(ref)
1641
1642            if url.scheme() == "help" and url.authority() == "search":
1643                try:
1644                    url = self.help.search(url)
1645                except KeyError:
1646                    url = None
1647                    log.info("No help topic found for %r", url)
1648
1649            if url:
1650                self.show_help(url)
1651            else:
1652                message_information(
1653                    self.tr("Sorry there is no documentation available for "
1654                            "this widget."),
1655                    parent=self)
1656
1657            return True
1658
1659        return QMainWindow.event(self, event)
1660
1661    def show_help(self, url):
1662        """
1663        Show `url` in a help window.
1664        """
1665        log.info("Setting help to url: %r", url)
1666        if self.open_in_external_browser:
1667            url = QUrl(url)
1668            if not QDesktopServices.openUrl(url):
1669                # Try fixing some common problems.
1670                url = QUrl.fromUserInput(url.toString())
1671                # 'fromUserInput' includes possible fragment into the path
1672                # (which prevents it to open local files) so we reparse it
1673                # again.
1674                url = QUrl(url.toString())
1675                QDesktopServices.openUrl(url)
1676        else:
1677            self.help_view.load(QUrl(url))
1678            self.help_dock.show()
1679            self.help_dock.raise_()
1680
1681    # Mac OS X
1682    if sys.platform == "darwin":
1683        def toggleMaximized(self):
1684            """Toggle normal/maximized window state.
1685            """
1686            if self.isMinimized():
1687                # Do nothing if window is minimized
1688                return
1689
1690            if self.isMaximized():
1691                self.showNormal()
1692            else:
1693                self.showMaximized()
1694
1695        def changeEvent(self, event):
1696            if event.type() == QEvent.WindowStateChange:
1697                # Can get 'Qt.WindowNoState' before the widget is fully
1698                # initialized
1699                if hasattr(self, "window_state"):
1700                    # Enable/disable window menu based on minimized state
1701                    self.window_menu.setEnabled(not self.isMinimized())
1702
1703            QMainWindow.changeEvent(self, event)
1704
1705    def sizeHint(self):
1706        """
1707        Reimplemented from QMainWindow.sizeHint
1708        """
1709        hint = QMainWindow.sizeHint(self)
1710        return hint.expandedTo(QSize(1024, 720))
1711
1712    def tr(self, sourceText, disambiguation=None, n=-1):
1713        """Translate the string.
1714        """
1715        return unicode(QMainWindow.tr(self, sourceText, disambiguation, n))
1716
1717    def __update_from_settings(self):
1718        settings = QSettings()
1719        settings.beginGroup("mainwindow")
1720        toolbox_floatable = settings.value("toolbox-dock-floatable",
1721                                           defaultValue=False,
1722                                           type=bool)
1723
1724        features = self.dock_widget.features()
1725        features = updated_flags(features, QDockWidget.DockWidgetFloatable,
1726                                 toolbox_floatable)
1727        self.dock_widget.setFeatures(features)
1728
1729        toolbox_exclusive = settings.value("toolbox-dock-exclusive",
1730                                           defaultValue=True,
1731                                           type=bool)
1732        self.widgets_tool_box.setExclusive(toolbox_exclusive)
1733
1734        self.num_recent_schemes = settings.value("num-recent-schemes",
1735                                                 defaultValue=15,
1736                                                 type=int)
1737
1738        settings.endGroup()
1739        settings.beginGroup("quickmenu")
1740
1741        triggers = 0
1742        dbl_click = settings.value("trigger-on-double-click",
1743                                   defaultValue=True,
1744                                   type=bool)
1745        if dbl_click:
1746            triggers |= SchemeEditWidget.DoubleClicked
1747
1748        right_click = settings.value("trigger-on-right-click",
1749                                    defaultValue=True,
1750                                    type=bool)
1751        if right_click:
1752            triggers |= SchemeEditWidget.RightClicked
1753
1754        space_press = settings.value("trigger-on-space-key",
1755                                     defaultValue=True,
1756                                     type=bool)
1757        if space_press:
1758            triggers |= SchemeEditWidget.SpaceKey
1759
1760        any_press = settings.value("trigger-on-any-key",
1761                                   defaultValue=False,
1762                                   type=bool)
1763        if any_press:
1764            triggers |= SchemeEditWidget.AnyKey
1765
1766        self.scheme_widget.setQuickMenuTriggers(triggers)
1767
1768        settings.endGroup()
1769        settings.beginGroup("schemeedit")
1770        show_channel_names = settings.value("show-channel-names",
1771                                            defaultValue=True,
1772                                            type=bool)
1773        self.scheme_widget.setChannelNamesVisible(show_channel_names)
1774
1775        node_animations = settings.value("enable-node-animations",
1776                                         defaultValue=False,
1777                                         type=bool)
1778        self.scheme_widget.setNodeAnimationEnabled(node_animations)
1779        settings.endGroup()
1780
1781        settings.beginGroup("output")
1782        stay_on_top = settings.value("stay-on-top", defaultValue=True,
1783                                     type=bool)
1784        if stay_on_top:
1785            self.output_dock.setFloatingWindowFlags(Qt.Tool)
1786        else:
1787            self.output_dock.setFloatingWindowFlags(Qt.Window)
1788
1789        dockable = settings.value("dockable", defaultValue=True,
1790                                  type=bool)
1791        if dockable:
1792            self.output_dock.setAllowedAreas(Qt.BottomDockWidgetArea)
1793        else:
1794            self.output_dock.setAllowedAreas(Qt.NoDockWidgetArea)
1795
1796        settings.endGroup()
1797
1798        settings.beginGroup("help")
1799        stay_on_top = settings.value("stay-on-top", defaultValue=True,
1800                                     type=bool)
1801        if stay_on_top:
1802            self.help_dock.setFloatingWindowFlags(Qt.Tool)
1803        else:
1804            self.help_dock.setFloatingWindowFlags(Qt.Window)
1805
1806        dockable = settings.value("dockable", defaultValue=False,
1807                                  type=bool)
1808        if dockable:
1809            self.help_dock.setAllowedAreas(Qt.LeftDockWidgetArea | \
1810                                           Qt.RightDockWidgetArea)
1811        else:
1812            self.help_dock.setAllowedAreas(Qt.NoDockWidgetArea)
1813
1814        self.open_in_external_browser = \
1815            settings.value("open-in-external-browser", defaultValue=False,
1816                           type=bool)
1817
1818        self.use_popover = \
1819            settings.value("toolbox-dock-use-popover-menu", defaultValue=True,
1820                           type=bool)
1821
1822
1823def updated_flags(flags, mask, state):
1824    if state:
1825        flags |= mask
1826    else:
1827        flags &= ~mask
1828    return flags
1829
1830
1831def identity(item):
1832    return item
1833
1834
1835def index(sequence, *what, **kwargs):
1836    """index(sequence, what, [key=None, [predicate=None]])
1837
1838    Return index of `what` in `sequence`.
1839
1840    """
1841    what = what[0]
1842    key = kwargs.get("key", identity)
1843    predicate = kwargs.get("predicate", operator.eq)
1844    for i, item in enumerate(sequence):
1845        item_key = key(item)
1846        if predicate(what, item_key):
1847            return i
1848    raise ValueError("%r not in sequence" % what)
Note: See TracBrowser for help on using the repository browser.