source: orange/Orange/OrangeCanvas/application/canvasmain.py @ 11462:fbd2b8d69e16

Revision 11462:fbd2b8d69e16, 60.9 KB checked in by Ales Erjavec <ales.erjavec@…>, 12 months ago (diff)

Override the 'Quit' keyboard shortcut when a popup window is shown.

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