source: orange/Orange/OrangeCanvas/application/canvasmain.py @ 11219:d24b63d1c4db

Revision 11219:d24b63d1c4db, 46.5 KB checked in by Ales Erjavec <ales.erjavec@…>, 17 months ago (diff)

Removed 'path' property from 'Scheme' class and added it to SchemeEditWidget.

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        scheme_doc_widget.setPath(filename)
774
775        self.add_recent_scheme(new_scheme.title, filename)
776
777    def new_scheme_from(self, filename):
778        """Create and return a new :class:`widgetsscheme.WidgetsScheme`
779        from a saved `filename`.
780
781        """
782        new_scheme = widgetsscheme.WidgetsScheme()
783        try:
784            new_scheme.load_from(open(filename, "rb"))
785        except Exception:
786            message_critical(
787                 self.tr("Could not load Orange Scheme file"),
788                 title=self.tr("Error"),
789                 informative_text=self.tr("An unexpected error occurred"),
790                 exc_info=True,
791                 parent=self)
792            return None
793
794        return new_scheme
795
796    def reload_last(self):
797        """Reload last opened scheme. Return QDialog.Rejected if the
798        user canceled the operation and QDialog.Accepted otherwise.
799
800        """
801        document = self.current_document()
802        if document.isModified():
803            if self.ask_save_changes() == QDialog.Rejected:
804                return QDialog.Rejected
805
806        # TODO: Search for a temp backup scheme with per process
807        # locking.
808        if self.recent_schemes:
809            self.load_scheme(self.recent_schemes[0][1])
810
811        return QDialog.Accepted
812
813    def ask_save_changes(self):
814        """Ask the user to save the changes to the current scheme.
815        Return QDialog.Accepted if the scheme was successfully saved
816        or the user selected to discard the changes. Otherwise return
817        QDialog.Rejected.
818
819        """
820        document = self.current_document()
821
822        selected = message_question(
823            self.tr("Do you want to save the changes you made to scheme %r?") \
824                    % document.scheme().title,
825            self.tr("Save Changes?"),
826            self.tr("If you do not save your changes will be lost"),
827            buttons=QMessageBox.Save | QMessageBox.Cancel | \
828                    QMessageBox.Discard,
829            default_button=QMessageBox.Save,
830            parent=self)
831
832        if selected == QMessageBox.Save:
833            return self.save_scheme()
834        elif selected == QMessageBox.Discard:
835            return QDialog.Accepted
836        elif selected == QMessageBox.Cancel:
837            return QDialog.Rejected
838
839    def save_scheme(self):
840        """Save the current scheme. If the scheme does not have an associated
841        path then prompt the user to select a scheme file. Return
842        QDialog.Accepted if the scheme was successfully saved and
843        QDialog.Rejected if the user canceled the file selection.
844
845        """
846        document = self.current_document()
847        curr_scheme = document.scheme()
848
849        if document.path():
850            curr_scheme.save_to(open(document.path(), "wb"))
851            document.setModified(False)
852            self.add_recent_scheme(curr_scheme.title, document.path())
853            return QDialog.Accepted
854        else:
855            return self.save_scheme_as()
856
857    def save_scheme_as(self):
858        """Save the current scheme by asking the user for a filename.
859        Return QFileDialog.Accepted if the scheme was saved successfully
860        and QFileDialog.Rejected if not.
861
862        """
863        document = self.current_document()
864        curr_scheme = document.scheme()
865
866        if document.path():
867            start_dir = document.path()
868        else:
869            if self.last_scheme_dir is not None:
870                start_dir = self.last_scheme_dir
871            else:
872                start_dir = QDesktopServices.storageLocation(
873                    QDesktopServices.DocumentsLocation
874                )
875
876            title = curr_scheme.title or "untitled"
877            start_dir = os.path.join(unicode(start_dir), title + ".ows")
878
879        filename = QFileDialog.getSaveFileName(
880            self, self.tr("Save Orange Scheme File"),
881            start_dir, self.tr("Orange Scheme (*.ows)")
882        )
883
884        if filename:
885            filename = unicode(filename)
886            dirname, basename = os.path.split(filename)
887            self.last_scheme_dir = dirname
888
889            try:
890                curr_scheme.save_to(open(filename, "wb"))
891            except Exception:
892                log.error("Error saving %r to %r", curr_scheme, filename,
893                          exc_info=True)
894                # Also show a message box
895                # TODO: should handle permission errors with a
896                # specialized messages.
897                message_critical(
898                     self.tr("An error occurred while trying to save the %r "
899                             "scheme to %r" % \
900                             (curr_scheme.title, basename)),
901                     title=self.tr("Error saving %r") % basename,
902                     exc_info=True,
903                     parent=self)
904                return QFileDialog.Rejected
905
906            document.setPath(filename)
907
908            document.setModified(False)
909            self.add_recent_scheme(curr_scheme.title, document.path())
910            return QFileDialog.Accepted
911        else:
912            return QFileDialog.Rejected
913
914    def get_started(self, *args):
915        """Show getting started video
916        """
917        url = QUrl(LINKS["start-using"])
918        QDesktopServices.openUrl(url)
919
920    def tutorial(self, *args):
921        """Show tutorial.
922        """
923        url = QUrl(LINKS["tutorial"])
924        QDesktopServices.openUrl(url)
925
926    def documentation(self, *args):
927        """Show reference documentation.
928        """
929        url = QUrl(LINKS["tutorial"])
930        QDesktopServices.openUrl(url)
931
932    def recent_scheme(self, *args):
933        """Browse recent schemes. Return QDialog.Rejected if the user
934        canceled the operation and QDialog.Accepted otherwise.
935
936        """
937        items = [previewmodel.PreviewItem(name=title, path=path)
938                 for title, path in self.recent_schemes]
939        model = previewmodel.PreviewModel(items=items)
940
941        dialog = previewdialog.PreviewDialog(self)
942        title = self.tr("Recent Schemes")
943        dialog.setWindowTitle(title)
944        template = ('<h3 style="font-size: 26px">\n'
945                    #'<img height="26" src="canvas_icons:Recent.svg">\n'
946                    '{0}\n'
947                    '</h3>')
948        dialog.setHeading(template.format(title))
949        dialog.setModel(model)
950
951        model.delayedScanUpdate()
952
953        status = dialog.exec_()
954
955        if status == QDialog.Accepted:
956            doc = self.current_document()
957            if doc.isModified():
958                if self.ask_save_changes() == QDialog.Rejected:
959                    return QDialog.Rejected
960
961            index = dialog.currentIndex()
962            selected = model.item(index)
963
964            self.load_scheme(unicode(selected.path()))
965
966        return status
967
968    def tutorial_scheme(self, *args):
969        """Browse a collection of tutorial schemes. Returns QDialog.Rejected
970        if the user canceled the dialog else loads the selected scheme into
971        the canvas and returns QDialog.Accepted.
972
973        """
974        tutors = tutorials.tutorials()
975        items = [previewmodel.PreviewItem(path=t.abspath()) for t in tutors]
976        model = previewmodel.PreviewModel(items=items)
977        dialog = previewdialog.PreviewDialog(self)
978        title = self.tr("Tutorials")
979        dialog.setWindowTitle(title)
980        template = ('<h3 style="font-size: 26px">\n'
981                    #'<img height="26" src="canvas_icons:Tutorials.svg">\n'
982                    '{0}\n'
983                    '</h3>')
984
985        dialog.setHeading(template.format(title))
986        dialog.setModel(model)
987
988        model.delayedScanUpdate()
989        status = dialog.exec_()
990
991        if status == QDialog.Accepted:
992            doc = self.current_document()
993            if doc.isModified():
994                if self.ask_save_changes() == QDialog.Rejected:
995                    return QDialog.Rejected
996
997            index = dialog.currentIndex()
998            selected = model.item(index)
999
1000            new_scheme = self.new_scheme_from(unicode(selected.path()))
1001            document = self.current_document()
1002            document.setScheme(new_scheme)
1003
1004        return status
1005
1006    def welcome_dialog(self):
1007        """Show a modal welcome dialog for Orange Canvas.
1008        """
1009
1010        dialog = welcomedialog.WelcomeDialog(self)
1011        dialog.setWindowTitle(self.tr("Welcome to Orange Data Mining"))
1012
1013        def new_scheme():
1014            if self.new_scheme() == QDialog.Accepted:
1015                dialog.accept()
1016
1017        def open_scheme():
1018            if self.open_scheme() == QDialog.Accepted:
1019                dialog.accept()
1020
1021        def open_recent():
1022            if self.recent_scheme() == QDialog.Accepted:
1023                dialog.accept()
1024
1025        def tutorial():
1026            if self.tutorial_scheme() == QDialog.Accepted:
1027                dialog.accept()
1028
1029        new_action = \
1030            QAction(self.tr("New"), dialog,
1031                    toolTip=self.tr("Open a new scheme."),
1032                    triggered=new_scheme,
1033                    shortcut=QKeySequence.New,
1034                    icon=canvas_icons("New.svg")
1035                    )
1036
1037        open_action = \
1038            QAction(self.tr("Open"), dialog,
1039                    objectName="welcome-action-open",
1040                    toolTip=self.tr("Open a scheme."),
1041                    triggered=open_scheme,
1042                    shortcut=QKeySequence.Open,
1043                    icon=canvas_icons("Open.svg")
1044                    )
1045
1046        recent_action = \
1047            QAction(self.tr("Recent"), dialog,
1048                    objectName="welcome-recent-action",
1049                    toolTip=self.tr("Browse and open a recent scheme."),
1050                    triggered=open_recent,
1051                    shortcut=QKeySequence(Qt.ControlModifier | \
1052                                          (Qt.ShiftModifier | Qt.Key_R)),
1053                    icon=canvas_icons("Recent.svg")
1054                    )
1055
1056        tutorials_action = \
1057            QAction(self.tr("Tutorial"), dialog,
1058                    objectName="welcome-tutorial-action",
1059                    toolTip=self.tr("Browse tutorial schemes."),
1060                    triggered=tutorial,
1061                    icon=canvas_icons("Tutorials.svg")
1062                    )
1063
1064        top_row = [self.get_started_action, tutorials_action,
1065                   self.documentation_action]
1066
1067        self.new_action.triggered.connect(dialog.accept)
1068        bottom_row = [new_action, open_action, recent_action]
1069
1070        dialog.addRow(top_row, background="light-grass")
1071        dialog.addRow(bottom_row, background="light-orange")
1072
1073        settings = QSettings()
1074
1075        dialog.setShowAtStartup(
1076            settings.value("welcomedialog/show-at-startup", True).toBool()
1077        )
1078
1079        status = dialog.exec_()
1080
1081        settings.setValue("welcomedialog/show-at-startup",
1082                          dialog.showAtStartup())
1083        return status
1084
1085    def show_scheme_properties(self):
1086        """Show current scheme properties.
1087        """
1088        current_doc = self.current_document()
1089        scheme = current_doc.scheme()
1090        return self.show_scheme_properties_for(scheme)
1091
1092    def show_scheme_properties_for(self, scheme, window_title=None):
1093        """Show scheme properties for `scheme` with `window_title (if None
1094        a default 'Scheme Info' title will be used.
1095
1096        """
1097        settings = QSettings()
1098        value_key = "schemeinfo/show-at-new-scheme"
1099
1100        dialog = SchemeInfoDialog(self)
1101
1102        if window_title is None:
1103            window_title = self.tr("Scheme Info")
1104
1105        dialog.setWindowTitle(window_title)
1106        dialog.setFixedSize(725, 450)
1107
1108        dialog.setDontShowAtNewScheme(
1109            not settings.value(value_key, True).toBool()
1110        )
1111
1112        dialog.setScheme(scheme)
1113
1114        status = dialog.exec_()
1115        if status == QDialog.Accepted:
1116            # Store the check state.
1117            settings.setValue(value_key, not dialog.dontShowAtNewScheme())
1118
1119        return status
1120
1121    def set_signal_freeze(self, freeze):
1122        scheme = self.current_document().scheme()
1123        if freeze:
1124            scheme.signal_manager.freeze().push()
1125        else:
1126            scheme.signal_manager.freeze().pop()
1127
1128    def remove_selected(self):
1129        """Remove current scheme selection.
1130        """
1131        self.current_document().removeSelected()
1132
1133    def quit(self):
1134        """Quit the application.
1135        """
1136        self.close()
1137
1138    def select_all(self):
1139        self.current_document().selectAll()
1140
1141    def open_widget(self):
1142        """Open/raise selected widget's GUI.
1143        """
1144        self.current_document().openSelected()
1145
1146    def rename_widget(self):
1147        """Rename the current focused widget.
1148        """
1149        doc = self.current_document()
1150        nodes = doc.selectedNodes()
1151        if len(nodes) == 1:
1152            doc.editNodeTitle(nodes[0])
1153
1154    def widget_help(self):
1155        """Open widget help page.
1156        """
1157        doc = self.current_document()
1158        nodes = doc.selectedNodes()
1159        help_url = None
1160        if len(nodes) == 1:
1161            node = nodes[0]
1162            desc = node.description
1163            if desc.help:
1164                help_url = desc.help
1165
1166        if help_url is not None:
1167            QDesktopServices.openUrl(QUrl(help_url))
1168        else:
1169            message_information(
1170                self.tr("Sorry there is documentation available for "
1171                        "this widget."),
1172                parent=self)
1173
1174    def open_canvas_settings(self):
1175        """Open canvas settings/preferences dialog
1176        """
1177        pass
1178
1179    def show_output_view(self):
1180        """Show a window with application output.
1181        """
1182        self.output_dock.show()
1183
1184    def output_view(self):
1185        """Return the output text widget.
1186        """
1187        return self.output_dock.widget()
1188
1189    def open_about(self):
1190        """Open the about dialog.
1191        """
1192        dlg = AboutDialog(self)
1193        dlg.exec_()
1194
1195    def add_recent_scheme(self, title, path):
1196        """Add an entry (`title`, `path`) to the list of recent schemes.
1197        """
1198        if not path:
1199            # No associated persistent path so we can't do anything.
1200            return
1201
1202        if title is None:
1203            title = os.path.basename(path)
1204            title, _ = os.path.splitext(title)
1205
1206        filename = os.path.abspath(os.path.realpath(path))
1207        filename = os.path.normpath(filename)
1208
1209        actions_by_filename = {}
1210        for action in self.recent_scheme_action_group.actions():
1211            path = unicode(action.data().toString())
1212            actions_by_filename[path] = action
1213
1214        if filename in actions_by_filename:
1215            # Remove the title/filename (so it can be reinserted)
1216            recent_index = index(self.recent_schemes, filename,
1217                                 key=operator.itemgetter(1))
1218            self.recent_schemes.pop(recent_index)
1219
1220            action = actions_by_filename[filename]
1221            self.recent_menu.removeAction(action)
1222            action.setText(title or self.tr("untitled"))
1223        else:
1224            action = QAction(title or self.tr("untitled"), self,
1225                             toolTip=filename)
1226            action.setData(filename)
1227
1228        # Find the separator action in the menu (after 'Browse Recent')
1229        recent_actions = self.recent_menu.actions()
1230        begin_index = index(recent_actions, self.recent_menu_begin)
1231        action_before = recent_actions[begin_index + 1]
1232
1233        self.recent_menu.insertAction(action_before, action)
1234        self.recent_scheme_action_group.addAction(action)
1235        self.recent_schemes.insert(0, (title, filename))
1236
1237        config.save_recent_scheme_list(self.recent_schemes)
1238
1239    def clear_recent_schemes(self):
1240        """Clear list of recent schemes
1241        """
1242        actions = list(self.recent_menu.actions())
1243
1244        # Exclude permanent actions (Browse Recent, separators, Clear List)
1245        actions_to_remove = [action for action in actions \
1246                             if unicode(action.data().toString())]
1247
1248        for action in actions_to_remove:
1249            self.recent_menu.removeAction(action)
1250
1251        self.recent_schemes = []
1252        config.save_recent_scheme_list([])
1253
1254    def _on_recent_scheme_action(self, action):
1255        """A recent scheme action was triggered by the user
1256        """
1257        document = self.current_document()
1258        if document.isModified():
1259            if self.ask_save_changes() == QDialog.Rejected:
1260                return
1261
1262        filename = unicode(action.data().toString())
1263        self.load_scheme(filename)
1264
1265    def _on_dock_location_changed(self, location):
1266        """Location of the dock_widget has changed, fix the margins
1267        if necessary.
1268
1269        """
1270        self.__update_scheme_margins()
1271
1272    def createPopupMenu(self):
1273        # Override the default context menu popup (we don't want the user to
1274        # be able to hide the tool dock widget).
1275        return None
1276
1277    def closeEvent(self, event):
1278        """Close the main window.
1279        """
1280        document = self.current_document()
1281        if document.isModified():
1282            if self.ask_save_changes() == QDialog.Rejected:
1283                # Reject the event
1284                event.ignore()
1285                return
1286
1287        # Set an empty scheme to clear the document
1288        document.setScheme(widgetsscheme.WidgetsScheme())
1289        document.deleteLater()
1290
1291        config.save_config()
1292
1293        geometry = self.saveGeometry()
1294        state = self.saveState(version=self.SETTINGS_VERSION)
1295        settings = QSettings()
1296        settings.beginGroup("canvasmainwindow")
1297        settings.setValue("geometry", geometry)
1298        settings.setValue("state", state)
1299        settings.setValue("canvasdock/expanded",
1300                          self.dock_widget.expanded())
1301        settings.setValue("scheme_margins_enabled",
1302                          self.scheme_margins_enabled)
1303
1304        settings.setValue("last_scheme_dir", self.last_scheme_dir)
1305        settings.endGroup()
1306
1307        event.accept()
1308
1309        # Close any windows left.
1310        application = QApplication.instance()
1311        QTimer.singleShot(0, application.closeAllWindows)
1312
1313    def showEvent(self, event):
1314        settings = QSettings()
1315        geom_data = settings.value("canvasmainwindow/geometry")
1316        if geom_data.isValid():
1317            self.restoreGeometry(geom_data.toByteArray())
1318
1319        return QMainWindow.showEvent(self, event)
1320
1321    # Mac OS X
1322    if sys.platform == "darwin":
1323        def toggleMaximized(self):
1324            """Toggle normal/maximized window state.
1325            """
1326            if self.isMinimized():
1327                # Do nothing if window is minimized
1328                return
1329
1330            if self.isMaximized():
1331                self.showNormal()
1332            else:
1333                self.showMaximized()
1334
1335        def changeEvent(self, event):
1336            if event.type() == QEvent.WindowStateChange:
1337                # Enable/disable window menu based on minimized state
1338                self.window_menu.setEnabled(not self.isMinimized())
1339            QMainWindow.changeEvent(self, event)
1340
1341    def tr(self, sourceText, disambiguation=None, n=-1):
1342        """Translate the string.
1343        """
1344        return unicode(QMainWindow.tr(self, sourceText, disambiguation, n))
1345
1346
1347def identity(item):
1348    return item
1349
1350
1351def index(sequence, *what, **kwargs):
1352    """index(sequence, what, [key=None, [predicate=None]])
1353    Return index of `what` in `sequence`.
1354    """
1355    what = what[0]
1356    key = kwargs.get("key", identity)
1357    predicate = kwargs.get("predicate", operator.eq)
1358    for i, item in enumerate(sequence):
1359        item_key = key(item)
1360        if predicate(what, item_key):
1361            return i
1362    raise ValueError("%r not in sequence" % what)
Note: See TracBrowser for help on using the repository browser.