source: orange/Orange/OrangeCanvas/application/canvasmain.py @ 11509:80bfa2c2b257

Revision 11509:80bfa2c2b257, 62.8 KB checked in by Ales Erjavec <ales.erjavec@…>, 11 months ago (diff)

Removed menu role for add-on action.

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