source: orange/Orange/OrangeCanvas/application/canvasmain.py @ 11496:28c3d24e59ec

Revision 11496:28c3d24e59ec, 62.7 KB checked in by Ales Erjavec <ales.erjavec@…>, 11 months ago (diff)

Merge

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