source: orange/Orange/OrangeCanvas/application/canvasmain.py @ 11218:6dbefbcaba66

Revision 11218:6dbefbcaba66, 46.7 KB checked in by Ales Erjavec <ales.erjavec@…>, 17 months ago (diff)

Recent menu actions consistency after saving.

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