source: orange/Orange/OrangeCanvas/application/canvasmain.py @ 11544:989ba559a51d

Revision 11544:989ba559a51d, 64.6 KB checked in by Ales Erjavec <ales.erjavec@…>, 11 months ago (diff)

Added error checking in 'check_can_save'.

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