source: orange/Orange/OrangeCanvas/application/canvasmain.py @ 11518:25344bdd7781

Revision 11518:25344bdd7781, 62.9 KB checked in by Ales Erjavec <ales.erjavec@…>, 12 months ago (diff)

Changed the main window default size.

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