source: orange/Orange/OrangeCanvas/application/canvasmain.py @ 11479:69e2cdaf858a

Revision 11479:69e2cdaf858a, 62.3 KB checked in by markotoplak, 12 months ago (diff)

Add-on update interface available from the new canvas.

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