source: orange/Orange/OrangeCanvas/application/canvasmain.py @ 11245:98f95d6d6bff

Revision 11245:98f95d6d6bff, 48.7 KB checked in by Ales Erjavec <ales.erjavec@…>, 16 months ago (diff)

Fixed freeze/edit actions state update when setting a new scheme.

Line 
1"""
2Orange Canvas Main Window
3
4"""
5import os
6import sys
7import logging
8import operator
9
10import pkg_resources
11
12from PyQt4.QtGui import (
13    QMainWindow, QWidget, QAction, QActionGroup, QMenu, QMenuBar, QDialog,
14    QFileDialog, QMessageBox, QVBoxLayout, QSizePolicy, QColor, QKeySequence,
15    QIcon, QToolBar, QToolButton, QDockWidget, QDesktopServices,
16    QApplication
17)
18
19from PyQt4.QtCore import (
20    Qt, QEvent, QSize, QUrl, QSettings, QTimer, QFile
21)
22
23from PyQt4.QtCore import pyqtProperty as Property
24
25
26from ..gui.dropshadow import DropShadowFrame
27from ..gui.dock import CollapsibleDockWidget
28from ..gui.quickhelp import QuickHelpTipEvent
29from ..gui.utils import message_critical, message_question, message_information
30
31from .canvastooldock import CanvasToolDock, QuickCategoryToolbar
32from .aboutdialog import AboutDialog
33from .schemeinfo import SchemeInfoDialog
34from .outputview import OutputText
35from ..document.schemeedit import SchemeEditWidget
36
37from ..scheme import widgetsscheme
38
39from . import welcomedialog
40from ..preview import previewdialog, previewmodel
41
42from .. import config
43
44from . import tutorials
45
46log = logging.getLogger(__name__)
47
48# TODO: Orange Version in the base link
49
50BASE_LINK = "http://orange.biolab.si/"
51
52LINKS = \
53    {"start-using": BASE_LINK + "start-using/",
54     "tutorial": BASE_LINK + "tutorial/",
55     "reference": BASE_LINK + "doc/"
56     }
57
58
59def style_icons(widget, standard_pixmap):
60    """Return the Qt standard pixmap icon.
61    """
62    return QIcon(widget.style().standardPixmap(standard_pixmap))
63
64
65def canvas_icons(name):
66    """Return the named canvas icon.
67    """
68    icon_file = QFile("canvas_icons:" + name)
69    if icon_file.exists():
70        return QIcon("canvas_icons:" + name)
71    else:
72        return QIcon(pkg_resources.resource_filename(
73                      config.__name__,
74                      os.path.join("icons", name))
75                     )
76
77
78class FakeToolBar(QToolBar):
79    """A Toolbar with no contents (used to reserve top and bottom margins
80    on the main window).
81
82    """
83    def __init__(self, *args, **kwargs):
84        QToolBar.__init__(self, *args, **kwargs)
85        self.setFloatable(False)
86        self.setMovable(False)
87
88        # Don't show the tool bar action in the main window's
89        # context menu.
90        self.toggleViewAction().setVisible(False)
91
92    def paintEvent(self, event):
93        # Do nothing.
94        pass
95
96
97class CanvasMainWindow(QMainWindow):
98    SETTINGS_VERSION = 2
99
100    def __init__(self, *args):
101        QMainWindow.__init__(self, *args)
102
103        self.__scheme_margins_enabled = True
104        self.__document_title = "untitled"
105
106        self.widget_registry = None
107        self.last_scheme_dir = None
108
109        self.recent_schemes = config.recent_schemes()
110
111        self.setup_actions()
112        self.setup_ui()
113        self.setup_menu()
114
115        self.restore()
116
117        self.resize(800, 600)
118
119    def setup_ui(self):
120        """Setup main canvas ui
121        """
122        QSettings.setDefaultFormat(QSettings.IniFormat)
123        settings = QSettings()
124        settings.beginGroup("canvasmainwindow")
125
126        log.info("Setting up Canvas main window.")
127
128        # Two dummy tool bars to reserve space
129        self.__dummy_top_toolbar = FakeToolBar(
130                            objectName="__dummy_top_toolbar")
131        self.__dummy_bottom_toolbar = FakeToolBar(
132                            objectName="__dummy_bottom_toolbar")
133
134        self.__dummy_top_toolbar.setFixedHeight(20)
135        self.__dummy_bottom_toolbar.setFixedHeight(20)
136
137        self.addToolBar(Qt.TopToolBarArea, self.__dummy_top_toolbar)
138        self.addToolBar(Qt.BottomToolBarArea, self.__dummy_bottom_toolbar)
139
140        self.setCorner(Qt.BottomLeftCorner, Qt.LeftDockWidgetArea)
141        self.setCorner(Qt.BottomRightCorner, Qt.RightDockWidgetArea)
142
143        # Create an empty initial scheme inside a container with fixed
144        # margins.
145        w = QWidget()
146        w.setLayout(QVBoxLayout())
147        w.layout().setContentsMargins(20, 0, 10, 0)
148
149        self.scheme_widget = SchemeEditWidget()
150        self.scheme_widget.setScheme(widgetsscheme.WidgetsScheme())
151
152        w.layout().addWidget(self.scheme_widget)
153
154        self.setCentralWidget(w)
155
156        # Drop shadow around the scheme document
157        frame = DropShadowFrame(radius=15)
158        frame.setColor(QColor(0, 0, 0, 100))
159        frame.setWidget(self.scheme_widget)
160
161        # Main window title and title icon.
162        self.set_document_title(self.scheme_widget.scheme().title)
163        self.scheme_widget.titleChanged.connect(self.set_document_title)
164        self.scheme_widget.modificationChanged.connect(self.setWindowModified)
165
166        self.setWindowIcon(canvas_icons("Get Started.svg"))
167
168        # QMainWindow's Dock widget
169        self.dock_widget = CollapsibleDockWidget(objectName="main-area-dock")
170        self.dock_widget.setFeatures(QDockWidget.DockWidgetMovable | \
171                                     QDockWidget.DockWidgetClosable)
172        self.dock_widget.setAllowedAreas(Qt.LeftDockWidgetArea | \
173                                         Qt.RightDockWidgetArea)
174
175        # Main canvas tool dock (with widget toolbox, common actions.
176        # This is the widget that is shown when the dock is expanded.
177        canvas_tool_dock = CanvasToolDock(objectName="canvas-tool-dock")
178        canvas_tool_dock.setSizePolicy(QSizePolicy.Fixed,
179                                       QSizePolicy.MinimumExpanding)
180
181        # Bottom tool bar
182        self.canvas_toolbar = canvas_tool_dock.toolbar
183        self.canvas_toolbar.setIconSize(QSize(25, 25))
184        self.canvas_toolbar.setFixedHeight(28)
185        self.canvas_toolbar.layout().setSpacing(1)
186
187        # Widgets tool box
188        self.widgets_tool_box = canvas_tool_dock.toolbox
189        self.widgets_tool_box.setObjectName("canvas-toolbox")
190        self.widgets_tool_box.setTabButtonHeight(30)
191        self.widgets_tool_box.setTabIconSize(QSize(26, 26))
192        self.widgets_tool_box.setButtonSize(QSize(64, 84))
193        self.widgets_tool_box.setIconSize(QSize(48, 48))
194
195        self.widgets_tool_box.triggered.connect(
196            self.on_tool_box_widget_activated
197        )
198
199        self.dock_help = canvas_tool_dock.help
200        self.dock_help.setMaximumHeight(150)
201        self.dock_help.document().setDefaultStyleSheet("h3 {color: orange;}")
202
203        self.dock_help_action = canvas_tool_dock.toogleQuickHelpAction()
204        self.dock_help_action.setText(self.tr("Show Help"))
205        self.dock_help_action.setIcon(canvas_icons("Info.svg"))
206
207        self.canvas_tool_dock = canvas_tool_dock
208
209        # Dock contents when collapsed (a quick category tool bar, ...)
210        dock2 = QWidget(objectName="canvas-quick-dock")
211        dock2.setLayout(QVBoxLayout())
212        dock2.layout().setContentsMargins(0, 0, 0, 0)
213        dock2.layout().setSpacing(0)
214        dock2.layout().setSizeConstraint(QVBoxLayout.SetFixedSize)
215
216        self.quick_category = QuickCategoryToolbar()
217        self.quick_category.setButtonSize(QSize(38, 30))
218        self.quick_category.actionTriggered.connect(
219            self.on_quick_category_action
220        )
221
222        tool_actions = self.current_document().toolbarActions()
223
224        (self.canvas_zoom_action, self.canvas_align_to_grid_action,
225         self.canvas_text_action, self.canvas_arrow_action,) = tool_actions
226
227        self.canvas_zoom_action.setIcon(canvas_icons("Search.svg"))
228        self.canvas_align_to_grid_action.setIcon(canvas_icons("Grid.svg"))
229        self.canvas_text_action.setIcon(canvas_icons("Text Size.svg"))
230        self.canvas_arrow_action.setIcon(canvas_icons("Arrow.svg"))
231
232        dock_actions = [self.show_properties_action] + \
233                       tool_actions + \
234                       [self.freeze_action,
235                        self.dock_help_action]
236
237        # Tool bar in the collapsed dock state (has the same actions as
238        # the tool bar in the CanvasToolDock
239        actions_toolbar = QToolBar(orientation=Qt.Vertical)
240        actions_toolbar.setFixedWidth(38)
241        actions_toolbar.layout().setSpacing(0)
242
243        actions_toolbar.setToolButtonStyle(Qt.ToolButtonIconOnly)
244
245        for action in dock_actions:
246            self.canvas_toolbar.addAction(action)
247            button = self.canvas_toolbar.widgetForAction(action)
248            button.setPopupMode(QToolButton.DelayedPopup)
249
250            actions_toolbar.addAction(action)
251            button = actions_toolbar.widgetForAction(action)
252            button.setFixedSize(38, 30)
253            button.setPopupMode(QToolButton.DelayedPopup)
254
255        dock2.layout().addWidget(self.quick_category)
256        dock2.layout().addWidget(actions_toolbar)
257
258        self.dock_widget.setAnimationEnabled(False)
259        self.dock_widget.setExpandedWidget(self.canvas_tool_dock)
260        self.dock_widget.setCollapsedWidget(dock2)
261        self.dock_widget.setExpanded(True)
262
263        self.addDockWidget(Qt.RightDockWidgetArea, self.dock_widget)
264        self.dock_widget.dockLocationChanged.connect(
265            self._on_dock_location_changed
266        )
267
268        self.output_dock = QDockWidget(self.tr("Output"),
269                                       objectName="output-dock")
270        self.output_dock.setAllowedAreas(Qt.BottomDockWidgetArea)
271
272        self.addDockWidget(Qt.BottomDockWidgetArea, self.output_dock)
273        self.output_dock.setFloating(True)
274
275        self.output_dock.hide()
276
277        output_view = OutputText()
278        self.output_dock.setWidget(output_view)
279
280        self.setMinimumSize(600, 500)
281
282    def setup_actions(self):
283        """Initialize main window actions.
284        """
285
286        self.new_action = \
287            QAction(self.tr("New"), self,
288                    objectName="action-new",
289                    toolTip=self.tr("Open a new scheme."),
290                    triggered=self.new_scheme,
291                    shortcut=QKeySequence.New,
292                    icon=canvas_icons("New.svg")
293                    )
294
295        self.open_action = \
296            QAction(self.tr("Open"), self,
297                    objectName="action-open",
298                    toolTip=self.tr("Open a scheme."),
299                    triggered=self.open_scheme,
300                    shortcut=QKeySequence.Open,
301                    icon=canvas_icons("Open.svg")
302                    )
303
304        self.save_action = \
305            QAction(self.tr("Save"), self,
306                    objectName="action-save",
307                    toolTip=self.tr("Save current scheme."),
308                    triggered=self.save_scheme,
309                    shortcut=QKeySequence.Save,
310                    )
311
312        self.save_as_action = \
313            QAction(self.tr("Save As ..."), self,
314                    objectName="action-save-as",
315                    toolTip=self.tr("Save current scheme as."),
316                    triggered=self.save_scheme_as,
317                    shortcut=QKeySequence.SaveAs,
318                    )
319
320        self.quit_action = \
321            QAction(self.tr("Quit"), self,
322                    objectName="quit-action",
323                    toolTip=self.tr("Quit Orange Canvas."),
324                    triggered=self.quit,
325                    menuRole=QAction.QuitRole,
326                    shortcut=QKeySequence.Quit,
327                    )
328
329        self.welcome_action = \
330            QAction(self.tr("Welcome"), self,
331                    objectName="welcome-action",
332                    toolTip=self.tr("Show welcome screen."),
333                    triggered=self.welcome_dialog,
334                    )
335
336        self.get_started_action = \
337            QAction(self.tr("Get Started"), self,
338                    objectName="get-started-action",
339                    toolTip=self.tr("View a 'Getting Started' video."),
340                    triggered=self.get_started,
341                    icon=canvas_icons("Get Started.svg")
342                    )
343
344        self.tutorials_action = \
345            QAction(self.tr("Tutorials"), self,
346                    objectName="tutorial-action",
347                    toolTip=self.tr("Browse tutorials."),
348                    triggered=self.tutorial_scheme,
349                    icon=canvas_icons("Tutorials.svg")
350                    )
351
352        self.documentation_action = \
353            QAction(self.tr("Documentation"), self,
354                    objectName="documentation-action",
355                    toolTip=self.tr("View reference documentation."),
356                    triggered=self.documentation,
357                    icon=canvas_icons("Documentation.svg")
358                    )
359
360        self.about_action = \
361            QAction(self.tr("About"), self,
362                    objectName="about-action",
363                    toolTip=self.tr("Show about dialog."),
364                    triggered=self.open_about,
365                    menuRole=QAction.AboutRole,
366                    )
367
368        # Action group for for recent scheme actions
369        self.recent_scheme_action_group = \
370            QActionGroup(self, exclusive=False,
371                         objectName="recent-action-group",
372                         triggered=self._on_recent_scheme_action)
373
374        self.recent_action = \
375            QAction(self.tr("Browse Recent"), self,
376                    objectName="recent-action",
377                    toolTip=self.tr("Browse and open a recent scheme."),
378                    triggered=self.recent_scheme,
379                    shortcut=QKeySequence(Qt.ControlModifier | \
380                                          (Qt.ShiftModifier | Qt.Key_R)),
381                    icon=canvas_icons("Recent.svg")
382                    )
383
384        self.reload_last_action = \
385            QAction(self.tr("Reload Last Scheme"), self,
386                    objectName="reload-last-action",
387                    toolTip=self.tr("Reload last open scheme."),
388                    triggered=self.reload_last,
389                    shortcut=QKeySequence(Qt.ControlModifier | Qt.Key_R)
390                    )
391
392        self.clear_recent_action = \
393            QAction(self.tr("Clear Menu"), self,
394                    objectName="clear-recent-menu-action",
395                    toolTip=self.tr("Clear recent menu."),
396                    triggered=self.clear_recent_schemes
397                    )
398
399        self.show_properties_action = \
400            QAction(self.tr("Show Properties"), self,
401                    objectName="show-properties-action",
402                    toolTip=self.tr("Show scheme properties."),
403                    triggered=self.show_scheme_properties,
404                    shortcut=QKeySequence(Qt.ControlModifier | Qt.Key_I),
405                    icon=canvas_icons("Document Info.svg")
406                    )
407
408        self.canvas_settings_action = \
409            QAction(self.tr("Settings"), self,
410                    objectName="canvas-settings-action",
411                    toolTip=self.tr("Set application settings."),
412                    triggered=self.open_canvas_settings,
413                    menuRole=QAction.PreferencesRole,
414                    shortcut=QKeySequence.Preferences
415                    )
416
417        self.show_output_action = \
418            QAction(self.tr("Show Output View"), self,
419                    toolTip=self.tr("Show application output."),
420                    triggered=self.show_output_view,
421                    )
422
423        if sys.platform == "darwin":
424            # Actions for native Mac OSX look and feel.
425            self.minimize_action = \
426                QAction(self.tr("Minimize"), self,
427                        triggered=self.showMinimized,
428                        shortcut=QKeySequence(Qt.ControlModifier | Qt.Key_M)
429                        )
430
431            self.zoom_action = \
432                QAction(self.tr("Zoom"), self,
433                        objectName="application-zoom",
434                        triggered=self.toggleMaximized,
435                        )
436
437        self.freeze_action = \
438            QAction(self.tr("Freeze"), self,
439                    objectName="signal-freeze-action",
440                    checkable=True,
441                    toolTip=self.tr("Freeze signal propagation."),
442                    triggered=self.set_signal_freeze,
443                    icon=canvas_icons("Pause.svg")
444                    )
445
446        # Gets assigned in setup_ui (the action is defined in CanvasToolDock)
447        # TODO: This is bad (should be moved here).
448        self.dock_help_action = None
449
450        self.toogle_margins_action = \
451            QAction(self.tr("Show Scheme Margins"), self,
452                    checkable=True,
453                    checked=True,
454                    toolTip=self.tr("Show margins around the scheme view."),
455                    toggled=self.set_scheme_margins_enabled
456                    )
457
458    def setup_menu(self):
459        menu_bar = QMenuBar()
460
461        # File menu
462        file_menu = QMenu(self.tr("&File"), menu_bar)
463        file_menu.addAction(self.new_action)
464        file_menu.addAction(self.open_action)
465        file_menu.addAction(self.reload_last_action)
466
467        # File -> Open Recent submenu
468        self.recent_menu = QMenu(self.tr("Open Recent"), file_menu)
469        file_menu.addMenu(self.recent_menu)
470        file_menu.addSeparator()
471        file_menu.addAction(self.save_action)
472        file_menu.addAction(self.save_as_action)
473        file_menu.addSeparator()
474        file_menu.addAction(self.show_properties_action)
475        file_menu.addAction(self.quit_action)
476
477        self.recent_menu.addAction(self.recent_action)
478
479        # Store the reference to separator for inserting recent
480        # schemes into the menu in `add_recent_scheme`.
481        self.recent_menu_begin = self.recent_menu.addSeparator()
482
483        # Add recent items.
484        for title, filename in self.recent_schemes:
485            action = QAction(title or self.tr("untitled"), self,
486                             toolTip=filename)
487
488            action.setData(filename)
489            self.recent_menu.addAction(action)
490            self.recent_scheme_action_group.addAction(action)
491
492        self.recent_menu.addSeparator()
493        self.recent_menu.addAction(self.clear_recent_action)
494        menu_bar.addMenu(file_menu)
495
496        editor_menus = self.scheme_widget.menuBarActions()
497
498        # WARNING: Hard coded order, should lookup the action text
499        # and determine the proper order
500        self.edit_menu = editor_menus[0].menu()
501        self.widget_menu = editor_menus[1].menu()
502
503        # Edit menu
504        menu_bar.addMenu(self.edit_menu)
505
506        # View menu
507        self.view_menu = QMenu(self.tr("&View"), self)
508        self.toolbox_menu = QMenu(self.tr("Widget Toolbox Style"),
509                                  self.view_menu)
510        self.toolbox_menu_group = \
511            QActionGroup(self, objectName="toolbox-menu-group")
512
513        a1 = self.toolbox_menu.addAction(self.tr("Tool Box"))
514        a2 = self.toolbox_menu.addAction(self.tr("Tool List"))
515        self.toolbox_menu_group.addAction(a1)
516        self.toolbox_menu_group.addAction(a2)
517
518        self.view_menu.addMenu(self.toolbox_menu)
519        self.view_menu.addSeparator()
520        self.view_menu.addAction(self.toogle_margins_action)
521        menu_bar.addMenu(self.view_menu)
522
523        # Options menu
524        self.options_menu = QMenu(self.tr("&Options"), self)
525        self.options_menu.addAction(self.show_output_action)
526#        self.options_menu.addAction("Add-ons")
527#        self.options_menu.addAction("Developers")
528#        self.options_menu.addAction("Run Discovery")
529#        self.options_menu.addAction("Show Canvas Log")
530#        self.options_menu.addAction("Attach Python Console")
531        self.options_menu.addSeparator()
532        self.options_menu.addAction(self.canvas_settings_action)
533
534        # Widget menu
535        menu_bar.addMenu(self.widget_menu)
536
537        if sys.platform == "darwin":
538            # Mac OS X native look and feel.
539            self.window_menu = QMenu(self.tr("Window"), self)
540            self.window_menu.addAction(self.minimize_action)
541            self.window_menu.addAction(self.zoom_action)
542            menu_bar.addMenu(self.window_menu)
543
544        menu_bar.addMenu(self.options_menu)
545
546        # Help menu.
547        self.help_menu = QMenu(self.tr("&Help"), self)
548        self.help_menu.addAction(self.about_action)
549        self.help_menu.addAction(self.welcome_action)
550        self.help_menu.addAction(self.tutorials_action)
551        self.help_menu.addAction(self.documentation_action)
552        menu_bar.addMenu(self.help_menu)
553
554        self.setMenuBar(menu_bar)
555
556    def restore(self):
557        """Restore the main window state from saved settings.
558        """
559        QSettings.setDefaultFormat(QSettings.IniFormat)
560        settings = QSettings()
561        settings.beginGroup("canvasmainwindow")
562
563        state = settings.value("state")
564        if state.isValid():
565            self.restoreState(state.toByteArray(),
566                              version=self.SETTINGS_VERSION)
567
568        self.dock_widget.setExpanded(
569            settings.value("canvasdock/expanded", True).toBool()
570        )
571
572        self.toogle_margins_action.setChecked(
573            settings.value("scheme_margins_enabled", True).toBool()
574        )
575
576        self.last_scheme_dir = \
577            settings.value("last_scheme_dir", None).toPyObject()
578
579        if self.last_scheme_dir is not None and \
580                not os.path.exists(self.last_scheme_dir):
581            # if directory no longer exists reset the saved location.
582            self.last_scheme_dir = None
583
584        self.canvas_tool_dock.setQuickHelpVisible(
585            settings.value("quick-help/visible", True).toBool()
586        )
587
588    def set_document_title(self, title):
589        """Set the document title (and the main window title). If `title`
590        is an empty string a default 'untitled' placeholder will be used.
591
592        """
593        if self.__document_title != title:
594            self.__document_title = title
595
596            if not title:
597                # TODO: should the default name be platform specific
598                title = self.tr("untitled")
599
600            self.setWindowTitle(title + "[*]")
601
602    def document_title(self):
603        """Return the document title.
604        """
605        return self.__document_title
606
607    def set_widget_registry(self, widget_registry):
608        """Set widget registry.
609        """
610        if self.widget_registry is not None:
611            # Clear the dock widget and popup.
612            pass
613
614        self.widget_registry = widget_registry
615        self.widgets_tool_box.setModel(widget_registry.model())
616        self.quick_category.setModel(widget_registry.model())
617
618        self.scheme_widget.setRegistry(widget_registry)
619
620        # Restore possibly saved widget toolbox tab states
621        settings = QSettings()
622        state = settings.value("canvasmainwindow/widgettoolbox/state",
623                                defaultValue=None)
624        state = state.toPyObject()
625        if state:
626            self.widgets_tool_box.restoreState(state)
627
628    def set_quick_help_text(self, text):
629        self.canvas_tool_dock.help.setText(text)
630
631    def current_document(self):
632        return self.scheme_widget
633
634    def on_tool_box_widget_activated(self, action):
635        """A widget action in the widget toolbox has been activated.
636        """
637        widget_desc = action.data().toPyObject()
638        if widget_desc:
639            scheme_widget = self.current_document()
640            if scheme_widget:
641                scheme_widget.createNewNode(widget_desc)
642
643    def on_quick_category_action(self, action):
644        """The quick category menu action triggered.
645        """
646        category = action.text()
647        for i in range(self.widgets_tool_box.count()):
648            cat_act = self.widgets_tool_box.tabAction(i)
649            if cat_act.text() == category:
650                if not cat_act.isChecked():
651                    # Trigger the action to expand the tool grid contained
652                    # within.
653                    cat_act.trigger()
654
655            else:
656                if cat_act.isChecked():
657                    # Trigger the action to hide the tool grid contained
658                    # within.
659                    cat_act.trigger()
660
661        self.dock_widget.expand()
662
663    def set_scheme_margins_enabled(self, enabled):
664        """Enable/disable the margins around the scheme document.
665        """
666        if self.__scheme_margins_enabled != enabled:
667            self.__scheme_margins_enabled = enabled
668            self.__update_scheme_margins()
669
670    def scheme_margins_enabled(self):
671        return self.__scheme_margins_enabled
672
673    scheme_margins_enabled = Property(bool,
674                                      fget=scheme_margins_enabled,
675                                      fset=set_scheme_margins_enabled)
676
677    def __update_scheme_margins(self):
678        """Update the margins around the scheme document.
679        """
680        enabled = self.__scheme_margins_enabled
681        self.__dummy_top_toolbar.setVisible(enabled)
682        self.__dummy_bottom_toolbar.setVisible(enabled)
683        central = self.centralWidget()
684
685        margin = 20 if enabled else 0
686
687        if self.dockWidgetArea(self.dock_widget) == Qt.LeftDockWidgetArea:
688            margins = (margin / 2, 0, margin, 0)
689        else:
690            margins = (margin, 0, margin / 2, 0)
691
692        central.layout().setContentsMargins(*margins)
693
694    #################
695    # Action handlers
696    #################
697    def new_scheme(self):
698        """New scheme. Return QDialog.Rejected if the user canceled
699        the operation and QDialog.Accepted otherwise.
700
701        """
702        document = self.current_document()
703        if document.isModifiedStrict():
704            # Ask for save changes
705            if self.ask_save_changes() == QDialog.Rejected:
706                return QDialog.Rejected
707
708        new_scheme = widgetsscheme.WidgetsScheme()
709
710        settings = QSettings()
711        show = settings.value("schemeinfo/show-at-new-scheme", True).toBool()
712
713        if show:
714            status = self.show_scheme_properties_for(
715                new_scheme, self.tr("New Scheme")
716            )
717
718            if status == QDialog.Rejected:
719                return QDialog.Rejected
720
721        self.set_new_scheme(new_scheme)
722
723        return QDialog.Accepted
724
725    def open_scheme(self):
726        """Open a new scheme. Return QDialog.Rejected if the user canceled
727        the operation and QDialog.Accepted otherwise.
728
729        """
730        document = self.current_document()
731        if document.isModifiedStrict():
732            if self.ask_save_changes() == QDialog.Rejected:
733                return QDialog.Rejected
734
735        if self.last_scheme_dir is None:
736            # Get user 'Documents' folder
737            start_dir = QDesktopServices.storageLocation(
738                            QDesktopServices.DocumentsLocation)
739        else:
740            start_dir = self.last_scheme_dir
741
742        # TODO: Use a dialog instance and use 'addSidebarUrls' to
743        # set one or more extra sidebar locations where Schemes are stored.
744        # Also use setHistory
745        filename = QFileDialog.getOpenFileName(
746            self, self.tr("Open Orange Scheme File"),
747            start_dir, self.tr("Orange Scheme (*.ows)"),
748        )
749
750        if filename:
751            self.load_scheme(filename)
752            return QDialog.Accepted
753        else:
754            return QDialog.Rejected
755
756    def load_scheme(self, filename):
757        """Load a scheme from a file (`filename`) into the current
758        document updates the recent scheme list and the loaded scheme path
759        property.
760
761        """
762        filename = unicode(filename)
763        dirname = os.path.dirname(filename)
764
765        self.last_scheme_dir = dirname
766
767        new_scheme = self.new_scheme_from(filename)
768
769        self.set_new_scheme(new_scheme)
770
771        scheme_doc_widget = self.current_document()
772        scheme_doc_widget.setPath(filename)
773
774        self.add_recent_scheme(new_scheme.title, filename)
775
776    def new_scheme_from(self, filename):
777        """Create and return a new :class:`widgetsscheme.WidgetsScheme`
778        from a saved `filename`.
779
780        """
781        new_scheme = widgetsscheme.WidgetsScheme()
782        try:
783            new_scheme.load_from(open(filename, "rb"))
784        except Exception:
785            message_critical(
786                 self.tr("Could not load Orange Scheme file"),
787                 title=self.tr("Error"),
788                 informative_text=self.tr("An unexpected error occurred"),
789                 exc_info=True,
790                 parent=self)
791            return None
792
793        return new_scheme
794
795    def reload_last(self):
796        """Reload last opened scheme. Return QDialog.Rejected if the
797        user canceled the operation and QDialog.Accepted otherwise.
798
799        """
800        document = self.current_document()
801        if document.isModifiedStrict():
802            if self.ask_save_changes() == QDialog.Rejected:
803                return QDialog.Rejected
804
805        # TODO: Search for a temp backup scheme with per process
806        # locking.
807        if self.recent_schemes:
808            self.load_scheme(self.recent_schemes[0][1])
809
810        return QDialog.Accepted
811
812    def set_new_scheme(self, new_scheme):
813        """Set new_scheme as the current shown scheme.
814        """
815        scheme_doc = self.current_document()
816        old_scheme = scheme_doc.scheme()
817
818        manager = new_scheme.signal_manager
819        if self.freeze_action.isChecked():
820            manager.freeze().push()
821
822        scheme_doc.setScheme(new_scheme)
823
824        old_scheme.save_widget_settings()
825        old_scheme.deleteLater()
826
827    def ask_save_changes(self):
828        """Ask the user to save the changes to the current scheme.
829        Return QDialog.Accepted if the scheme was successfully saved
830        or the user selected to discard the changes. Otherwise return
831        QDialog.Rejected.
832
833        """
834        document = self.current_document()
835
836        selected = message_question(
837            self.tr("Do you want to save the changes you made to scheme %r?") \
838                    % document.scheme().title,
839            self.tr("Save Changes?"),
840            self.tr("If you do not save your changes will be lost"),
841            buttons=QMessageBox.Save | QMessageBox.Cancel | \
842                    QMessageBox.Discard,
843            default_button=QMessageBox.Save,
844            parent=self)
845
846        if selected == QMessageBox.Save:
847            return self.save_scheme()
848        elif selected == QMessageBox.Discard:
849            return QDialog.Accepted
850        elif selected == QMessageBox.Cancel:
851            return QDialog.Rejected
852
853    def save_scheme(self):
854        """Save the current scheme. If the scheme does not have an associated
855        path then prompt the user to select a scheme file. Return
856        QDialog.Accepted if the scheme was successfully saved and
857        QDialog.Rejected if the user canceled the file selection.
858
859        """
860        document = self.current_document()
861        curr_scheme = document.scheme()
862
863        if document.path():
864            curr_scheme.save_to(open(document.path(), "wb"))
865            document.setModified(False)
866            self.add_recent_scheme(curr_scheme.title, document.path())
867            return QDialog.Accepted
868        else:
869            return self.save_scheme_as()
870
871    def save_scheme_as(self):
872        """Save the current scheme by asking the user for a filename.
873        Return QFileDialog.Accepted if the scheme was saved successfully
874        and QFileDialog.Rejected if not.
875
876        """
877        document = self.current_document()
878        curr_scheme = document.scheme()
879
880        if document.path():
881            start_dir = document.path()
882        else:
883            if self.last_scheme_dir is not None:
884                start_dir = self.last_scheme_dir
885            else:
886                start_dir = QDesktopServices.storageLocation(
887                    QDesktopServices.DocumentsLocation
888                )
889
890            title = curr_scheme.title or "untitled"
891            start_dir = os.path.join(unicode(start_dir), title + ".ows")
892
893        filename = QFileDialog.getSaveFileName(
894            self, self.tr("Save Orange Scheme File"),
895            start_dir, self.tr("Orange Scheme (*.ows)")
896        )
897
898        if filename:
899            filename = unicode(filename)
900            dirname, basename = os.path.split(filename)
901            self.last_scheme_dir = dirname
902
903            try:
904                curr_scheme.save_to(open(filename, "wb"))
905            except Exception:
906                log.error("Error saving %r to %r", curr_scheme, filename,
907                          exc_info=True)
908                # Also show a message box
909                # TODO: should handle permission errors with a
910                # specialized messages.
911                message_critical(
912                     self.tr("An error occurred while trying to save the %r "
913                             "scheme to %r" % \
914                             (curr_scheme.title, basename)),
915                     title=self.tr("Error saving %r") % basename,
916                     exc_info=True,
917                     parent=self)
918                return QFileDialog.Rejected
919
920            document.setPath(filename)
921
922            document.setModified(False)
923            self.add_recent_scheme(curr_scheme.title, document.path())
924            return QFileDialog.Accepted
925        else:
926            return QFileDialog.Rejected
927
928    def get_started(self, *args):
929        """Show getting started video
930        """
931        url = QUrl(LINKS["start-using"])
932        QDesktopServices.openUrl(url)
933
934    def tutorial(self, *args):
935        """Show tutorial.
936        """
937        url = QUrl(LINKS["tutorial"])
938        QDesktopServices.openUrl(url)
939
940    def documentation(self, *args):
941        """Show reference documentation.
942        """
943        url = QUrl(LINKS["tutorial"])
944        QDesktopServices.openUrl(url)
945
946    def recent_scheme(self, *args):
947        """Browse recent schemes. Return QDialog.Rejected if the user
948        canceled the operation and QDialog.Accepted otherwise.
949
950        """
951        items = [previewmodel.PreviewItem(name=title, path=path)
952                 for title, path in self.recent_schemes]
953        model = previewmodel.PreviewModel(items=items)
954
955        dialog = previewdialog.PreviewDialog(self)
956        title = self.tr("Recent Schemes")
957        dialog.setWindowTitle(title)
958        template = ('<h3 style="font-size: 26px">\n'
959                    #'<img height="26" src="canvas_icons:Recent.svg">\n'
960                    '{0}\n'
961                    '</h3>')
962        dialog.setHeading(template.format(title))
963        dialog.setModel(model)
964
965        model.delayedScanUpdate()
966
967        status = dialog.exec_()
968
969        index = dialog.currentIndex()
970
971        dialog.deleteLater()
972
973        if status == QDialog.Accepted:
974            doc = self.current_document()
975            if doc.isModifiedStrict():
976                if self.ask_save_changes() == QDialog.Rejected:
977                    return QDialog.Rejected
978
979            selected = model.item(index)
980
981            self.load_scheme(unicode(selected.path()))
982
983        return status
984
985    def tutorial_scheme(self, *args):
986        """Browse a collection of tutorial schemes. Returns QDialog.Rejected
987        if the user canceled the dialog else loads the selected scheme into
988        the canvas and returns QDialog.Accepted.
989
990        """
991        tutors = tutorials.tutorials()
992        items = [previewmodel.PreviewItem(path=t.abspath()) for t in tutors]
993        model = previewmodel.PreviewModel(items=items)
994        dialog = previewdialog.PreviewDialog(self)
995        title = self.tr("Tutorials")
996        dialog.setWindowTitle(title)
997        template = ('<h3 style="font-size: 26px">\n'
998                    #'<img height="26" src="canvas_icons:Tutorials.svg">\n'
999                    '{0}\n'
1000                    '</h3>')
1001
1002        dialog.setHeading(template.format(title))
1003        dialog.setModel(model)
1004
1005        model.delayedScanUpdate()
1006        status = dialog.exec_()
1007        index = dialog.currentIndex()
1008
1009        dialog.deleteLater()
1010
1011        if status == QDialog.Accepted:
1012            doc = self.current_document()
1013            if doc.isModifiedStrict():
1014                if self.ask_save_changes() == QDialog.Rejected:
1015                    return QDialog.Rejected
1016
1017            selected = model.item(index)
1018
1019            new_scheme = self.new_scheme_from(unicode(selected.path()))
1020
1021            self.set_new_scheme(new_scheme)
1022
1023        return status
1024
1025    def welcome_dialog(self):
1026        """Show a modal welcome dialog for Orange Canvas.
1027        """
1028
1029        dialog = welcomedialog.WelcomeDialog(self)
1030        dialog.setWindowTitle(self.tr("Welcome to Orange Data Mining"))
1031
1032        def new_scheme():
1033            if self.new_scheme() == QDialog.Accepted:
1034                dialog.accept()
1035
1036        def open_scheme():
1037            if self.open_scheme() == QDialog.Accepted:
1038                dialog.accept()
1039
1040        def open_recent():
1041            if self.recent_scheme() == QDialog.Accepted:
1042                dialog.accept()
1043
1044        def tutorial():
1045            if self.tutorial_scheme() == QDialog.Accepted:
1046                dialog.accept()
1047
1048        new_action = \
1049            QAction(self.tr("New"), dialog,
1050                    toolTip=self.tr("Open a new scheme."),
1051                    triggered=new_scheme,
1052                    shortcut=QKeySequence.New,
1053                    icon=canvas_icons("New.svg")
1054                    )
1055
1056        open_action = \
1057            QAction(self.tr("Open"), dialog,
1058                    objectName="welcome-action-open",
1059                    toolTip=self.tr("Open a scheme."),
1060                    triggered=open_scheme,
1061                    shortcut=QKeySequence.Open,
1062                    icon=canvas_icons("Open.svg")
1063                    )
1064
1065        recent_action = \
1066            QAction(self.tr("Recent"), dialog,
1067                    objectName="welcome-recent-action",
1068                    toolTip=self.tr("Browse and open a recent scheme."),
1069                    triggered=open_recent,
1070                    shortcut=QKeySequence(Qt.ControlModifier | \
1071                                          (Qt.ShiftModifier | Qt.Key_R)),
1072                    icon=canvas_icons("Recent.svg")
1073                    )
1074
1075        tutorials_action = \
1076            QAction(self.tr("Tutorial"), dialog,
1077                    objectName="welcome-tutorial-action",
1078                    toolTip=self.tr("Browse tutorial schemes."),
1079                    triggered=tutorial,
1080                    icon=canvas_icons("Tutorials.svg")
1081                    )
1082
1083        top_row = [self.get_started_action, tutorials_action,
1084                   self.documentation_action]
1085
1086        self.new_action.triggered.connect(dialog.accept)
1087        bottom_row = [new_action, open_action, recent_action]
1088
1089        dialog.addRow(top_row, background="light-grass")
1090        dialog.addRow(bottom_row, background="light-orange")
1091
1092        settings = QSettings()
1093
1094        dialog.setShowAtStartup(
1095            settings.value("welcomedialog/show-at-startup", True).toBool()
1096        )
1097
1098        status = dialog.exec_()
1099
1100        settings.setValue("welcomedialog/show-at-startup",
1101                          dialog.showAtStartup())
1102
1103        dialog.deleteLater()
1104
1105        return status
1106
1107    def scheme_properties_dialog(self):
1108        """Return an empty `SchemeInfo` dialog instance.
1109        """
1110        settings = QSettings()
1111        value_key = "schemeinfo/show-at-new-scheme"
1112
1113        dialog = SchemeInfoDialog(self)
1114
1115        dialog.setWindowTitle(self.tr("Scheme Info"))
1116        dialog.setFixedSize(725, 450)
1117
1118        dialog.setDontShowAtNewScheme(
1119            not settings.value(value_key, True).toBool()
1120        )
1121
1122        return dialog
1123
1124    def show_scheme_properties(self):
1125        """Show current scheme properties.
1126        """
1127        settings = QSettings()
1128        value_key = "schemeinfo/show-at-new-scheme"
1129
1130        current_doc = self.current_document()
1131        scheme = current_doc.scheme()
1132        dlg = self.scheme_properties_dialog()
1133        dlg.setAutoCommit(False)
1134        dlg.setScheme(scheme)
1135        status = dlg.exec_()
1136
1137        if status == QDialog.Accepted:
1138            editor = dlg.editor
1139            stack = current_doc.undoStack()
1140            stack.beginMacro(self.tr("Change Info"))
1141            current_doc.setTitle(editor.title())
1142            current_doc.setDescription(editor.description())
1143            stack.endMacro()
1144
1145            # Store the check state.
1146            settings.setValue(value_key, not dlg.dontShowAtNewScheme())
1147        return status
1148
1149    def show_scheme_properties_for(self, scheme, window_title=None):
1150        """Show scheme properties for `scheme` with `window_title (if None
1151        a default 'Scheme Info' title will be used.
1152
1153        """
1154        settings = QSettings()
1155        value_key = "schemeinfo/show-at-new-scheme"
1156
1157        dialog = self.scheme_properties_dialog()
1158
1159        if window_title is not None:
1160            dialog.setWindowTitle(window_title)
1161
1162        dialog.setScheme(scheme)
1163
1164        status = dialog.exec_()
1165        if status == QDialog.Accepted:
1166            # Store the check state.
1167            settings.setValue(value_key, not dialog.dontShowAtNewScheme())
1168
1169        dialog.deleteLater()
1170
1171        return status
1172
1173    def set_signal_freeze(self, freeze):
1174        scheme = self.current_document().scheme()
1175        if freeze:
1176            scheme.signal_manager.freeze().push()
1177        else:
1178            scheme.signal_manager.freeze().pop()
1179
1180    def remove_selected(self):
1181        """Remove current scheme selection.
1182        """
1183        self.current_document().removeSelected()
1184
1185    def quit(self):
1186        """Quit the application.
1187        """
1188        self.close()
1189
1190    def select_all(self):
1191        self.current_document().selectAll()
1192
1193    def open_widget(self):
1194        """Open/raise selected widget's GUI.
1195        """
1196        self.current_document().openSelected()
1197
1198    def rename_widget(self):
1199        """Rename the current focused widget.
1200        """
1201        doc = self.current_document()
1202        nodes = doc.selectedNodes()
1203        if len(nodes) == 1:
1204            doc.editNodeTitle(nodes[0])
1205
1206    def widget_help(self):
1207        """Open widget help page.
1208        """
1209        doc = self.current_document()
1210        nodes = doc.selectedNodes()
1211        help_url = None
1212        if len(nodes) == 1:
1213            node = nodes[0]
1214            desc = node.description
1215            if desc.help:
1216                help_url = desc.help
1217
1218        if help_url is not None:
1219            QDesktopServices.openUrl(QUrl(help_url))
1220        else:
1221            message_information(
1222                self.tr("Sorry there is documentation available for "
1223                        "this widget."),
1224                parent=self)
1225
1226    def open_canvas_settings(self):
1227        """Open canvas settings/preferences dialog
1228        """
1229        pass
1230
1231    def show_output_view(self):
1232        """Show a window with application output.
1233        """
1234        self.output_dock.show()
1235
1236    def output_view(self):
1237        """Return the output text widget.
1238        """
1239        return self.output_dock.widget()
1240
1241    def open_about(self):
1242        """Open the about dialog.
1243        """
1244        dlg = AboutDialog(self)
1245        dlg.setAttribute(Qt.WA_DeleteOnClose)
1246        dlg.exec_()
1247
1248    def add_recent_scheme(self, title, path):
1249        """Add an entry (`title`, `path`) to the list of recent schemes.
1250        """
1251        if not path:
1252            # No associated persistent path so we can't do anything.
1253            return
1254
1255        if title is None:
1256            title = os.path.basename(path)
1257            title, _ = os.path.splitext(title)
1258
1259        filename = os.path.abspath(os.path.realpath(path))
1260        filename = os.path.normpath(filename)
1261
1262        actions_by_filename = {}
1263        for action in self.recent_scheme_action_group.actions():
1264            path = unicode(action.data().toString())
1265            actions_by_filename[path] = action
1266
1267        if filename in actions_by_filename:
1268            # Remove the title/filename (so it can be reinserted)
1269            recent_index = index(self.recent_schemes, filename,
1270                                 key=operator.itemgetter(1))
1271            self.recent_schemes.pop(recent_index)
1272
1273            action = actions_by_filename[filename]
1274            self.recent_menu.removeAction(action)
1275            action.setText(title or self.tr("untitled"))
1276        else:
1277            action = QAction(title or self.tr("untitled"), self,
1278                             toolTip=filename)
1279            action.setData(filename)
1280
1281        # Find the separator action in the menu (after 'Browse Recent')
1282        recent_actions = self.recent_menu.actions()
1283        begin_index = index(recent_actions, self.recent_menu_begin)
1284        action_before = recent_actions[begin_index + 1]
1285
1286        self.recent_menu.insertAction(action_before, action)
1287        self.recent_scheme_action_group.addAction(action)
1288        self.recent_schemes.insert(0, (title, filename))
1289
1290        config.save_recent_scheme_list(self.recent_schemes)
1291
1292    def clear_recent_schemes(self):
1293        """Clear list of recent schemes
1294        """
1295        actions = list(self.recent_menu.actions())
1296
1297        # Exclude permanent actions (Browse Recent, separators, Clear List)
1298        actions_to_remove = [action for action in actions \
1299                             if unicode(action.data().toString())]
1300
1301        for action in actions_to_remove:
1302            self.recent_menu.removeAction(action)
1303
1304        self.recent_schemes = []
1305        config.save_recent_scheme_list([])
1306
1307    def _on_recent_scheme_action(self, action):
1308        """A recent scheme action was triggered by the user
1309        """
1310        document = self.current_document()
1311        if document.isModifiedStrict():
1312            if self.ask_save_changes() == QDialog.Rejected:
1313                return
1314
1315        filename = unicode(action.data().toString())
1316        self.load_scheme(filename)
1317
1318    def _on_dock_location_changed(self, location):
1319        """Location of the dock_widget has changed, fix the margins
1320        if necessary.
1321
1322        """
1323        self.__update_scheme_margins()
1324
1325    def createPopupMenu(self):
1326        # Override the default context menu popup (we don't want the user to
1327        # be able to hide the tool dock widget).
1328        return None
1329
1330    def closeEvent(self, event):
1331        """Close the main window.
1332        """
1333        document = self.current_document()
1334        if document.isModifiedStrict():
1335            if self.ask_save_changes() == QDialog.Rejected:
1336                # Reject the event
1337                event.ignore()
1338                return
1339
1340        scheme = document.scheme()
1341        scheme.save_widget_settings()
1342
1343        # Set an empty scheme to clear the document
1344        document.setScheme(widgetsscheme.WidgetsScheme())
1345        document.deleteLater()
1346
1347        config.save_config()
1348
1349        geometry = self.saveGeometry()
1350        state = self.saveState(version=self.SETTINGS_VERSION)
1351        settings = QSettings()
1352        settings.beginGroup("canvasmainwindow")
1353        settings.setValue("geometry", geometry)
1354        settings.setValue("state", state)
1355        settings.setValue("canvasdock/expanded",
1356                          self.dock_widget.expanded())
1357        settings.setValue("scheme_margins_enabled",
1358                          self.scheme_margins_enabled)
1359
1360        settings.setValue("last_scheme_dir", self.last_scheme_dir)
1361        settings.setValue("widgettoolbox/state",
1362                          self.widgets_tool_box.saveState())
1363
1364        settings.setValue("quick-help/visible",
1365                          self.canvas_tool_dock.quickHelpVisible())
1366
1367        settings.endGroup()
1368
1369        event.accept()
1370
1371        # Close any windows left.
1372        application = QApplication.instance()
1373        QTimer.singleShot(0, application.closeAllWindows)
1374
1375    def showEvent(self, event):
1376        settings = QSettings()
1377        geom_data = settings.value("canvasmainwindow/geometry")
1378        if geom_data.isValid():
1379            self.restoreGeometry(geom_data.toByteArray())
1380
1381        return QMainWindow.showEvent(self, event)
1382
1383    def event(self, event):
1384        if event.type() == QEvent.StatusTip and \
1385                isinstance(event, QuickHelpTipEvent):
1386            if event.priority() == QuickHelpTipEvent.Normal:
1387                self.dock_help.showHelp(event.html())
1388            elif event.priority() == QuickHelpTipEvent.Temporary:
1389                self.dock_help.showHelp(event.html(), event.timeout())
1390            elif event.priority() == QuickHelpTipEvent.Permanent:
1391                self.dock_help.showPermanentHelp(event.html())
1392            return True
1393
1394        return QMainWindow.event(self, event)
1395
1396    # Mac OS X
1397    if sys.platform == "darwin":
1398        def toggleMaximized(self):
1399            """Toggle normal/maximized window state.
1400            """
1401            if self.isMinimized():
1402                # Do nothing if window is minimized
1403                return
1404
1405            if self.isMaximized():
1406                self.showNormal()
1407            else:
1408                self.showMaximized()
1409
1410        def changeEvent(self, event):
1411            if event.type() == QEvent.WindowStateChange:
1412                # Enable/disable window menu based on minimized state
1413                self.window_menu.setEnabled(not self.isMinimized())
1414            QMainWindow.changeEvent(self, event)
1415
1416    def tr(self, sourceText, disambiguation=None, n=-1):
1417        """Translate the string.
1418        """
1419        return unicode(QMainWindow.tr(self, sourceText, disambiguation, n))
1420
1421
1422def identity(item):
1423    return item
1424
1425
1426def index(sequence, *what, **kwargs):
1427    """index(sequence, what, [key=None, [predicate=None]])
1428    Return index of `what` in `sequence`.
1429    """
1430    what = what[0]
1431    key = kwargs.get("key", identity)
1432    predicate = kwargs.get("predicate", operator.eq)
1433    for i, item in enumerate(sequence):
1434        item_key = key(item)
1435        if predicate(what, item_key):
1436            return i
1437    raise ValueError("%r not in sequence" % what)
Note: See TracBrowser for help on using the repository browser.