source: orange/Orange/OrangeCanvas/application/canvasmain.py @ 11557:2ac1b27290d5

Revision 11557:2ac1b27290d5, 66.2 KB checked in by Ales Erjavec <ales.erjavec@…>, 11 months ago (diff)

Changed the "Get Started" action tooltip

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