source: orange/Orange/OrangeCanvas/application/canvasmain.py @ 11495:12e60f527869

Revision 11495:12e60f527869, 61.4 KB checked in by Ales Erjavec <ales.erjavec@…>, 12 months ago (diff)

Added a popup menu class with widgets displayed in a list.

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