source: orange/Orange/OrangeCanvas/application/canvasmain.py @ 11770:bdf2527fe917

Revision 11770:bdf2527fe917, 68.3 KB checked in by Ales Erjavec <ales.erjavec@…>, 5 months ago (diff)

Do not truncate an existing file in case of an error in 'save_scheme'.

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