source: orange/Orange/OrangeCanvas/application/canvasmain.py @ 11208:b2091aba2a49

Revision 11208:b2091aba2a49, 46.4 KB checked in by Ales Erjavec <ales.erjavec@…>, 17 months ago (diff)

Moved widget actions definitions into 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("Tutorial"), 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, self, toolTip=filename)
508            action.setData(filename)
509            self.recent_menu.addAction(action)
510            self.recent_scheme_action_group.addAction(action)
511
512        self.recent_menu.addSeparator()
513        self.recent_menu.addAction(self.clear_recent_action)
514        menu_bar.addMenu(file_menu)
515
516        editor_menus = self.scheme_widget.menuBarActions()
517
518        # WARNING: Hard coded order, should lookup the action text
519        # and determine the proper order
520        self.edit_menu = editor_menus[0].menu()
521        self.widget_menu = editor_menus[1].menu()
522
523        # Edit menu
524        menu_bar.addMenu(self.edit_menu)
525
526        # View menu
527        self.view_menu = QMenu(self.tr("&View"), self)
528        self.toolbox_menu = QMenu(self.tr("Widget Toolbox Style"),
529                                  self.view_menu)
530        self.toolbox_menu_group = \
531            QActionGroup(self, objectName="toolbox-menu-group")
532
533        a1 = self.toolbox_menu.addAction(self.tr("Tool Box"))
534        a2 = self.toolbox_menu.addAction(self.tr("Tool List"))
535        self.toolbox_menu_group.addAction(a1)
536        self.toolbox_menu_group.addAction(a2)
537
538        self.view_menu.addMenu(self.toolbox_menu)
539        self.view_menu.addSeparator()
540        self.view_menu.addAction(self.toogle_margins_action)
541        menu_bar.addMenu(self.view_menu)
542
543        # Options menu
544        self.options_menu = QMenu(self.tr("&Options"), self)
545        self.options_menu.addAction(self.show_output_action)
546#        self.options_menu.addAction("Add-ons")
547#        self.options_menu.addAction("Developers")
548#        self.options_menu.addAction("Run Discovery")
549#        self.options_menu.addAction("Show Canvas Log")
550#        self.options_menu.addAction("Attach Python Console")
551        self.options_menu.addSeparator()
552        self.options_menu.addAction(self.canvas_settings_action)
553
554        # Widget menu
555        menu_bar.addMenu(self.widget_menu)
556
557        if sys.platform == "darwin":
558            # Mac OS X native look and feel.
559            self.window_menu = QMenu(self.tr("Window"), self)
560            self.window_menu.addAction(self.minimize_action)
561            self.window_menu.addAction(self.zoom_action)
562            menu_bar.addMenu(self.window_menu)
563
564        menu_bar.addMenu(self.options_menu)
565
566        # Help menu.
567        self.help_menu = QMenu(self.tr("&Help"), self)
568        self.help_menu.addAction(self.about_action)
569        self.help_menu.addAction(self.welcome_action)
570        self.help_menu.addAction(self.tutorials_action)
571        self.help_menu.addAction(self.documentation_action)
572        menu_bar.addMenu(self.help_menu)
573
574        self.setMenuBar(menu_bar)
575
576    def set_document_title(self, title):
577        """Set the document title (and the main window title). If `title`
578        is an empty string a default 'untitled' placeholder will be used.
579
580        """
581        if self.__document_title != title:
582            self.__document_title = title
583
584            if not title:
585                # TODO: should the default name be platform specific
586                title = self.tr("untitled")
587
588            self.setWindowTitle(title + "[*]")
589
590    def document_title(self):
591        """Return the document title.
592        """
593        return self.__document_title
594
595    def set_widget_registry(self, widget_registry):
596        """Set widget registry.
597        """
598        if self.widget_registry is not None:
599            # Clear the dock widget and popup.
600            pass
601
602        self.widget_registry = widget_registry
603        self.widgets_tool_box.setModel(widget_registry.model())
604        self.quick_category.setModel(widget_registry.model())
605
606        self.scheme_widget.setRegistry(widget_registry)
607
608    def set_quick_help_text(self, text):
609        self.canvas_tool_dock.help.setText(text)
610
611    def current_document(self):
612        return self.scheme_widget
613
614    def on_tool_box_widget_activated(self, action):
615        """A widget action in the widget toolbox has been activated.
616        """
617        widget_desc = action.data().toPyObject()
618        if widget_desc:
619            scheme_widget = self.current_document()
620            if scheme_widget:
621                scheme_widget.createNewNode(widget_desc)
622
623    def on_tool_box_widget_hovered(self, action):
624        """Mouse is over a widget in the widget toolbox
625        """
626        widget_desc = action.data().toPyObject()
627        title = ""
628        help_text = ""
629        if widget_desc:
630            title = widget_desc.name
631            description = widget_desc.help
632            if not help_text:
633                description = widget_desc.description
634
635            template = "<h3>{title}</h3>" + \
636                       "<p>{description}</p>" + \
637                       "<a href=''>more...</a>"
638            help_text = template.format(title=title, description=description)
639            # TODO: 'More...' link
640        self.set_quick_help_text(help_text)
641
642    def on_quick_category_action(self, action):
643        """The quick category menu action triggered.
644        """
645        category = action.text()
646        for i in range(self.widgets_tool_box.count()):
647            cat_act = self.widgets_tool_box.tabAction(i)
648            if cat_act.text() == category:
649                if not cat_act.isChecked():
650                    # Trigger the action to expand the tool grid contained
651                    # within.
652                    cat_act.trigger()
653
654            else:
655                if cat_act.isChecked():
656                    # Trigger the action to hide the tool grid contained
657                    # within.
658                    cat_act.trigger()
659
660        self.dock_widget.expand()
661
662    def set_scheme_margins_enabled(self, enabled):
663        """Enable/disable the margins around the scheme document.
664        """
665        if self.__scheme_margins_enabled != enabled:
666            self.__scheme_margins_enabled = enabled
667            self.__update_scheme_margins()
668
669    def scheme_margins_enabled(self):
670        return self.__scheme_margins_enabled
671
672    scheme_margins_enabled = Property(bool,
673                                      fget=scheme_margins_enabled,
674                                      fset=set_scheme_margins_enabled)
675
676    def __update_scheme_margins(self):
677        """Update the margins around the scheme document.
678        """
679        enabled = self.__scheme_margins_enabled
680        self.__dummy_top_toolbar.setVisible(enabled)
681        self.__dummy_bottom_toolbar.setVisible(enabled)
682        central = self.centralWidget()
683
684        margin = 20 if enabled else 0
685
686        if self.dockWidgetArea(self.dock_widget) == Qt.LeftDockWidgetArea:
687            margins = (margin / 2, 0, margin, 0)
688        else:
689            margins = (margin, 0, margin / 2, 0)
690
691        central.layout().setContentsMargins(*margins)
692
693    #################
694    # Action handlers
695    #################
696    def new_scheme(self):
697        """New scheme. Return QDialog.Rejected if the user canceled
698        the operation and QDialog.Accepted otherwise.
699
700        """
701        document = self.current_document()
702        if document.isModified():
703            # Ask for save changes
704            if self.ask_save_changes() == QDialog.Rejected:
705                return QDialog.Rejected
706
707        new_scheme = widgetsscheme.WidgetsScheme()
708
709        settings = QSettings()
710        show = settings.value("schemeinfo/show-at-new-scheme", True).toBool()
711
712        if show:
713            status = self.show_scheme_properties_for(
714                new_scheme, self.tr("New Scheme")
715            )
716
717            if status == QDialog.Rejected:
718                return QDialog.Rejected
719
720        scheme_doc_widget = self.current_document()
721        scheme_doc_widget.setScheme(new_scheme)
722
723        return QDialog.Accepted
724
725    def open_scheme(self):
726        """Open a new scheme. Return QDialog.Rejected if the user canceled
727        the operation and QDialog.Accepted otherwise.
728
729        """
730        document = self.current_document()
731        if document.isModified():
732            if self.ask_save_changes() == QDialog.Rejected:
733                return QDialog.Rejected
734
735        if self.last_scheme_dir is None:
736            # Get user 'Documents' folder
737            start_dir = QDesktopServices.storageLocation(
738                            QDesktopServices.DocumentsLocation)
739        else:
740            start_dir = self.last_scheme_dir
741
742        # TODO: Use a dialog instance and use 'addSidebarUrls' to
743        # set one or more extra sidebar locations where Schemes are stored.
744        # Also use setHistory
745        filename = QFileDialog.getOpenFileName(
746            self, self.tr("Open Orange Scheme File"),
747            start_dir, self.tr("Orange Scheme (*.ows)"),
748        )
749
750        if filename:
751            self.load_scheme(filename)
752            return QDialog.Accepted
753        else:
754            return QDialog.Rejected
755
756    def load_scheme(self, filename):
757        """Load a scheme from a file (`filename`) into the current
758        document updates the recent scheme list and the loaded scheme path
759        property.
760
761        """
762        filename = unicode(filename)
763        dirname = os.path.dirname(filename)
764
765        self.last_scheme_dir = dirname
766
767        new_scheme = self.new_scheme_from(filename)
768
769        scheme_doc_widget = self.current_document()
770        scheme_doc_widget.setScheme(new_scheme)
771
772        self.add_recent_scheme(new_scheme)
773
774    def new_scheme_from(self, filename):
775        """Create and return a new :class:`widgetsscheme.WidgetsScheme`
776        from a saved `filename`.
777
778        """
779        new_scheme = widgetsscheme.WidgetsScheme()
780        try:
781            new_scheme.load_from(open(filename, "rb"))
782        except Exception:
783            message_critical(
784                 self.tr("Could not load Orange Scheme file"),
785                 title=self.tr("Error"),
786                 informative_text=self.tr("An unexpected error occurred"),
787                 exc_info=True,
788                 parent=self)
789            return None
790
791        return new_scheme
792
793    def reload_last(self):
794        """Reload last opened scheme. Return QDialog.Rejected if the
795        user canceled the operation and QDialog.Accepted otherwise.
796
797        """
798        document = self.current_document()
799        if document.isModified():
800            if self.ask_save_changes() == QDialog.Rejected:
801                return QDialog.Rejected
802
803        # TODO: Search for a temp backup scheme with per process
804        # locking.
805        if self.recent_schemes:
806            self.load_scheme(self.recent_schemes[0][1])
807
808        return QDialog.Accepted
809
810    def ask_save_changes(self):
811        """Ask the user to save the changes to the current scheme.
812        Return QDialog.Accepted if the scheme was successfully saved
813        or the user selected to discard the changes. Otherwise return
814        QDialog.Rejected.
815
816        """
817        document = self.current_document()
818
819        selected = message_question(
820            self.tr("Do you want to save the changes you made to scheme %r?") \
821                    % document.scheme().title,
822            self.tr("Save Changes?"),
823            self.tr("If you do not save your changes will be lost"),
824            buttons=QMessageBox.Save | QMessageBox.Cancel | \
825                    QMessageBox.Discard,
826            default_button=QMessageBox.Save,
827            parent=self)
828
829        if selected == QMessageBox.Save:
830            return self.save_scheme()
831        elif selected == QMessageBox.Discard:
832            return QDialog.Accepted
833        elif selected == QMessageBox.Cancel:
834            return QDialog.Rejected
835
836    def save_scheme(self):
837        """Save the current scheme. If the scheme does not have an associated
838        path then prompt the user to select a scheme file. Return
839        QDialog.Accepted if the scheme was successfully saved and
840        QDialog.Rejected if the user canceled the file selection.
841
842        """
843        document = self.current_document()
844        curr_scheme = document.scheme()
845
846        if curr_scheme.path:
847            curr_scheme.save_to(open(curr_scheme.path, "wb"))
848            document.setModified(False)
849            return QDialog.Accepted
850        else:
851            return self.save_scheme_as()
852
853    def save_scheme_as(self):
854        """Save the current scheme by asking the user for a filename.
855        Return QFileDialog.Accepted if the scheme was saved successfully
856        and QFileDialog.Rejected if not.
857
858        """
859        document = self.current_document()
860        curr_scheme = document.scheme()
861
862        if curr_scheme.path:
863            start_dir = curr_scheme.path
864        else:
865            if self.last_scheme_dir is not None:
866                start_dir = self.last_scheme_dir
867            else:
868                start_dir = QDesktopServices.storageLocation(
869                    QDesktopServices.DocumentsLocation
870                )
871
872            title = curr_scheme.title or "untitled"
873            start_dir = os.path.join(unicode(start_dir), title + ".ows")
874
875        filename = QFileDialog.getSaveFileName(
876            self, self.tr("Save Orange Scheme File"),
877            start_dir, self.tr("Orange Scheme (*.ows)")
878        )
879
880        if filename:
881            filename = unicode(filename)
882            dirname, basename = os.path.split(filename)
883            self.last_scheme_dir = dirname
884
885            try:
886                curr_scheme.save_to(open(filename, "wb"))
887            except Exception:
888                log.error("Error saving %r to %r", curr_scheme, filename,
889                          exc_info=True)
890                # Also show a message box
891                # TODO: should handle permission errors with a
892                # specialized messages.
893                message_critical(
894                     self.tr("An error occurred while trying to save the %r "
895                             "scheme to %r" % \
896                             (curr_scheme.title, basename)),
897                     title=self.tr("Error saving %r") % basename,
898                     exc_info=True,
899                     parent=self)
900                return QFileDialog.Rejected
901
902            curr_scheme.path = filename
903            if not curr_scheme.title:
904                curr_scheme.title = os.path.splitext(basename)[0]
905
906            self.add_recent_scheme(curr_scheme)
907            document.setModified(False)
908            return QFileDialog.Accepted
909        else:
910            return QFileDialog.Rejected
911
912    def get_started(self, *args):
913        """Show getting started video
914        """
915        url = QUrl(LINKS["start-using"])
916        QDesktopServices.openUrl(url)
917
918    def tutorial(self, *args):
919        """Show tutorial.
920        """
921        url = QUrl(LINKS["tutorial"])
922        QDesktopServices.openUrl(url)
923
924    def documentation(self, *args):
925        """Show reference documentation.
926        """
927        url = QUrl(LINKS["tutorial"])
928        QDesktopServices.openUrl(url)
929
930    def recent_scheme(self, *args):
931        """Browse recent schemes. Return QDialog.Rejected if the user
932        canceled the operation and QDialog.Accepted otherwise.
933
934        """
935        items = [previewmodel.PreviewItem(name=title, path=path)
936                 for title, path in self.recent_schemes]
937        model = previewmodel.PreviewModel(items=items)
938
939        dialog = previewdialog.PreviewDialog(self)
940        title = self.tr("Recent Schemes")
941        dialog.setWindowTitle(title)
942        template = ('<h3 style="font-size: 26px">\n'
943                    #'<img height="26" src="canvas_icons:Recent.svg">\n'
944                    '{0}\n'
945                    '</h3>')
946        dialog.setHeading(template.format(title))
947        dialog.setModel(model)
948
949        model.delayedScanUpdate()
950
951        status = dialog.exec_()
952
953        if status == QDialog.Accepted:
954            doc = self.current_document()
955            if doc.isModified():
956                if self.ask_save_changes() == QDialog.Rejected:
957                    return QDialog.Rejected
958
959            index = dialog.currentIndex()
960            selected = model.item(index)
961
962            self.load_scheme(unicode(selected.path()))
963
964        return status
965
966    def tutorial_scheme(self, *args):
967        """Browse a collection of tutorial schemes. Returns QDialog.Rejected
968        if the user canceled the dialog else loads the selected scheme into
969        the canvas and returns QDialog.Accepted.
970
971        """
972        tutors = tutorials.tutorials()
973        items = [previewmodel.PreviewItem(path=t.abspath()) for t in tutors]
974        model = previewmodel.PreviewModel(items=items)
975        dialog = previewdialog.PreviewDialog(self)
976        title = self.tr("Tutorials")
977        dialog.setWindowTitle(title)
978        template = ('<h3 style="font-size: 26px">\n'
979                    #'<img height="26" src="canvas_icons:Tutorials.svg">\n'
980                    '{0}\n'
981                    '</h3>')
982
983        dialog.setHeading(template.format(title))
984        dialog.setModel(model)
985
986        model.delayedScanUpdate()
987        status = dialog.exec_()
988
989        if status == QDialog.Accepted:
990            doc = self.current_document()
991            if doc.isModified():
992                if self.ask_save_changes() == QDialog.Rejected:
993                    return QDialog.Rejected
994
995            index = dialog.currentIndex()
996            selected = model.item(index)
997
998            new_scheme = self.new_scheme_from(unicode(selected.path()))
999            # Clear the 'path' property (set by scheme.load_from), so
1000            # ctrl-s does not override the saved tutorial file in case the
1001            # tutorial file is writable.
1002            new_scheme.path = ""
1003            self.current_document().setScheme(new_scheme)
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, scheme):
1196        """Add `scheme` to the list of recent schemes.
1197        """
1198        if not scheme.path:
1199            return
1200
1201        title = scheme.title
1202        path = scheme.path
1203
1204        if title is None:
1205            title = os.path.basename(path)
1206            title, _ = os.path.splitext(title)
1207
1208        filename = os.path.abspath(os.path.realpath(path))
1209        filename = os.path.normpath(filename)
1210
1211        actions_by_filename = {}
1212        for action in self.recent_scheme_action_group.actions():
1213            path = unicode(action.data().toString())
1214            actions_by_filename[path] = action
1215
1216        if (title, filename) in self.recent_schemes:
1217            # Remove the title/filename (so it can be reinserted)
1218            recent_index = self.recent_schemes.index((title, filename))
1219            self.recent_schemes.pop(recent_index)
1220
1221        if filename in actions_by_filename:
1222            action = actions_by_filename[filename]
1223            self.recent_menu.removeAction(action)
1224        else:
1225            action = QAction(title, self, toolTip=filename)
1226            action.setData(filename)
1227
1228        self.recent_schemes.insert(0, (title, filename))
1229
1230        recent_actions = self.recent_menu.actions()
1231        begin_index = index(recent_actions, self.recent_menu_begin)
1232        action_before = recent_actions[begin_index + 1]
1233
1234        self.recent_menu.insertAction(action_before, action)
1235        self.recent_scheme_action_group.addAction(action)
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.