source: orange/Orange/OrangeCanvas/application/canvasmain.py @ 11726:f1cafef0bde2

Revision 11726:f1cafef0bde2, 67.5 KB checked in by Ales Erjavec <ales.erjavec@…>, 6 months ago (diff)

Added a menu action to show the Report view.

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