source: orange/Orange/OrangeCanvas/application/canvasmain.py @ 11760:8bed2f6c2782

Revision 11760:8bed2f6c2782, 67.7 KB checked in by Ales Erjavec <ales.erjavec@…>, 5 months ago (diff)

Handle an exception if loading of recent scheme list fails.

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