source: orange/Orange/OrangeCanvas/application/canvasmain.py @ 11220:661725c19c2a

Revision 11220:661725c19c2a, 46.7 KB checked in by Ales Erjavec <ales.erjavec@…>, 17 months ago (diff)

Delete the dialogs after use.

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        index = dialog.currentIndex()
956
957        dialog.deleteLater()
958
959        if status == QDialog.Accepted:
960            doc = self.current_document()
961            if doc.isModified():
962                if self.ask_save_changes() == QDialog.Rejected:
963                    return QDialog.Rejected
964
965            selected = model.item(index)
966
967            self.load_scheme(unicode(selected.path()))
968
969        return status
970
971    def tutorial_scheme(self, *args):
972        """Browse a collection of tutorial schemes. Returns QDialog.Rejected
973        if the user canceled the dialog else loads the selected scheme into
974        the canvas and returns QDialog.Accepted.
975
976        """
977        tutors = tutorials.tutorials()
978        items = [previewmodel.PreviewItem(path=t.abspath()) for t in tutors]
979        model = previewmodel.PreviewModel(items=items)
980        dialog = previewdialog.PreviewDialog(self)
981        title = self.tr("Tutorials")
982        dialog.setWindowTitle(title)
983        template = ('<h3 style="font-size: 26px">\n'
984                    #'<img height="26" src="canvas_icons:Tutorials.svg">\n'
985                    '{0}\n'
986                    '</h3>')
987
988        dialog.setHeading(template.format(title))
989        dialog.setModel(model)
990
991        model.delayedScanUpdate()
992        status = dialog.exec_()
993        index = dialog.currentIndex()
994
995        dialog.deleteLater()
996
997        if status == QDialog.Accepted:
998            doc = self.current_document()
999            if doc.isModified():
1000                if self.ask_save_changes() == QDialog.Rejected:
1001                    return QDialog.Rejected
1002
1003            selected = model.item(index)
1004
1005            new_scheme = self.new_scheme_from(unicode(selected.path()))
1006            document = self.current_document()
1007            document.setScheme(new_scheme)
1008
1009        return status
1010
1011    def welcome_dialog(self):
1012        """Show a modal welcome dialog for Orange Canvas.
1013        """
1014
1015        dialog = welcomedialog.WelcomeDialog(self)
1016        dialog.setWindowTitle(self.tr("Welcome to Orange Data Mining"))
1017
1018        def new_scheme():
1019            if self.new_scheme() == QDialog.Accepted:
1020                dialog.accept()
1021
1022        def open_scheme():
1023            if self.open_scheme() == QDialog.Accepted:
1024                dialog.accept()
1025
1026        def open_recent():
1027            if self.recent_scheme() == QDialog.Accepted:
1028                dialog.accept()
1029
1030        def tutorial():
1031            if self.tutorial_scheme() == QDialog.Accepted:
1032                dialog.accept()
1033
1034        new_action = \
1035            QAction(self.tr("New"), dialog,
1036                    toolTip=self.tr("Open a new scheme."),
1037                    triggered=new_scheme,
1038                    shortcut=QKeySequence.New,
1039                    icon=canvas_icons("New.svg")
1040                    )
1041
1042        open_action = \
1043            QAction(self.tr("Open"), dialog,
1044                    objectName="welcome-action-open",
1045                    toolTip=self.tr("Open a scheme."),
1046                    triggered=open_scheme,
1047                    shortcut=QKeySequence.Open,
1048                    icon=canvas_icons("Open.svg")
1049                    )
1050
1051        recent_action = \
1052            QAction(self.tr("Recent"), dialog,
1053                    objectName="welcome-recent-action",
1054                    toolTip=self.tr("Browse and open a recent scheme."),
1055                    triggered=open_recent,
1056                    shortcut=QKeySequence(Qt.ControlModifier | \
1057                                          (Qt.ShiftModifier | Qt.Key_R)),
1058                    icon=canvas_icons("Recent.svg")
1059                    )
1060
1061        tutorials_action = \
1062            QAction(self.tr("Tutorial"), dialog,
1063                    objectName="welcome-tutorial-action",
1064                    toolTip=self.tr("Browse tutorial schemes."),
1065                    triggered=tutorial,
1066                    icon=canvas_icons("Tutorials.svg")
1067                    )
1068
1069        top_row = [self.get_started_action, tutorials_action,
1070                   self.documentation_action]
1071
1072        self.new_action.triggered.connect(dialog.accept)
1073        bottom_row = [new_action, open_action, recent_action]
1074
1075        dialog.addRow(top_row, background="light-grass")
1076        dialog.addRow(bottom_row, background="light-orange")
1077
1078        settings = QSettings()
1079
1080        dialog.setShowAtStartup(
1081            settings.value("welcomedialog/show-at-startup", True).toBool()
1082        )
1083
1084        status = dialog.exec_()
1085
1086        settings.setValue("welcomedialog/show-at-startup",
1087                          dialog.showAtStartup())
1088
1089        dialog.deleteLater()
1090
1091        return status
1092
1093    def show_scheme_properties(self):
1094        """Show current scheme properties.
1095        """
1096        current_doc = self.current_document()
1097        scheme = current_doc.scheme()
1098        return self.show_scheme_properties_for(scheme)
1099
1100    def show_scheme_properties_for(self, scheme, window_title=None):
1101        """Show scheme properties for `scheme` with `window_title (if None
1102        a default 'Scheme Info' title will be used.
1103
1104        """
1105        settings = QSettings()
1106        value_key = "schemeinfo/show-at-new-scheme"
1107
1108        dialog = SchemeInfoDialog(self)
1109
1110        if window_title is None:
1111            window_title = self.tr("Scheme Info")
1112
1113        dialog.setWindowTitle(window_title)
1114        dialog.setFixedSize(725, 450)
1115
1116        dialog.setDontShowAtNewScheme(
1117            not settings.value(value_key, True).toBool()
1118        )
1119
1120        dialog.setScheme(scheme)
1121
1122        status = dialog.exec_()
1123        if status == QDialog.Accepted:
1124            # Store the check state.
1125            settings.setValue(value_key, not dialog.dontShowAtNewScheme())
1126
1127        dialog.deleteLater()
1128
1129        return status
1130
1131    def set_signal_freeze(self, freeze):
1132        scheme = self.current_document().scheme()
1133        if freeze:
1134            scheme.signal_manager.freeze().push()
1135        else:
1136            scheme.signal_manager.freeze().pop()
1137
1138    def remove_selected(self):
1139        """Remove current scheme selection.
1140        """
1141        self.current_document().removeSelected()
1142
1143    def quit(self):
1144        """Quit the application.
1145        """
1146        self.close()
1147
1148    def select_all(self):
1149        self.current_document().selectAll()
1150
1151    def open_widget(self):
1152        """Open/raise selected widget's GUI.
1153        """
1154        self.current_document().openSelected()
1155
1156    def rename_widget(self):
1157        """Rename the current focused widget.
1158        """
1159        doc = self.current_document()
1160        nodes = doc.selectedNodes()
1161        if len(nodes) == 1:
1162            doc.editNodeTitle(nodes[0])
1163
1164    def widget_help(self):
1165        """Open widget help page.
1166        """
1167        doc = self.current_document()
1168        nodes = doc.selectedNodes()
1169        help_url = None
1170        if len(nodes) == 1:
1171            node = nodes[0]
1172            desc = node.description
1173            if desc.help:
1174                help_url = desc.help
1175
1176        if help_url is not None:
1177            QDesktopServices.openUrl(QUrl(help_url))
1178        else:
1179            message_information(
1180                self.tr("Sorry there is documentation available for "
1181                        "this widget."),
1182                parent=self)
1183
1184    def open_canvas_settings(self):
1185        """Open canvas settings/preferences dialog
1186        """
1187        pass
1188
1189    def show_output_view(self):
1190        """Show a window with application output.
1191        """
1192        self.output_dock.show()
1193
1194    def output_view(self):
1195        """Return the output text widget.
1196        """
1197        return self.output_dock.widget()
1198
1199    def open_about(self):
1200        """Open the about dialog.
1201        """
1202        dlg = AboutDialog(self)
1203        dlg.setAttribute(Qt.WA_DeleteOnClose)
1204        dlg.exec_()
1205
1206    def add_recent_scheme(self, title, path):
1207        """Add an entry (`title`, `path`) to the list of recent schemes.
1208        """
1209        if not path:
1210            # No associated persistent path so we can't do anything.
1211            return
1212
1213        if title is None:
1214            title = os.path.basename(path)
1215            title, _ = os.path.splitext(title)
1216
1217        filename = os.path.abspath(os.path.realpath(path))
1218        filename = os.path.normpath(filename)
1219
1220        actions_by_filename = {}
1221        for action in self.recent_scheme_action_group.actions():
1222            path = unicode(action.data().toString())
1223            actions_by_filename[path] = action
1224
1225        if filename in actions_by_filename:
1226            # Remove the title/filename (so it can be reinserted)
1227            recent_index = index(self.recent_schemes, filename,
1228                                 key=operator.itemgetter(1))
1229            self.recent_schemes.pop(recent_index)
1230
1231            action = actions_by_filename[filename]
1232            self.recent_menu.removeAction(action)
1233            action.setText(title or self.tr("untitled"))
1234        else:
1235            action = QAction(title or self.tr("untitled"), self,
1236                             toolTip=filename)
1237            action.setData(filename)
1238
1239        # Find the separator action in the menu (after 'Browse Recent')
1240        recent_actions = self.recent_menu.actions()
1241        begin_index = index(recent_actions, self.recent_menu_begin)
1242        action_before = recent_actions[begin_index + 1]
1243
1244        self.recent_menu.insertAction(action_before, action)
1245        self.recent_scheme_action_group.addAction(action)
1246        self.recent_schemes.insert(0, (title, filename))
1247
1248        config.save_recent_scheme_list(self.recent_schemes)
1249
1250    def clear_recent_schemes(self):
1251        """Clear list of recent schemes
1252        """
1253        actions = list(self.recent_menu.actions())
1254
1255        # Exclude permanent actions (Browse Recent, separators, Clear List)
1256        actions_to_remove = [action for action in actions \
1257                             if unicode(action.data().toString())]
1258
1259        for action in actions_to_remove:
1260            self.recent_menu.removeAction(action)
1261
1262        self.recent_schemes = []
1263        config.save_recent_scheme_list([])
1264
1265    def _on_recent_scheme_action(self, action):
1266        """A recent scheme action was triggered by the user
1267        """
1268        document = self.current_document()
1269        if document.isModified():
1270            if self.ask_save_changes() == QDialog.Rejected:
1271                return
1272
1273        filename = unicode(action.data().toString())
1274        self.load_scheme(filename)
1275
1276    def _on_dock_location_changed(self, location):
1277        """Location of the dock_widget has changed, fix the margins
1278        if necessary.
1279
1280        """
1281        self.__update_scheme_margins()
1282
1283    def createPopupMenu(self):
1284        # Override the default context menu popup (we don't want the user to
1285        # be able to hide the tool dock widget).
1286        return None
1287
1288    def closeEvent(self, event):
1289        """Close the main window.
1290        """
1291        document = self.current_document()
1292        if document.isModified():
1293            if self.ask_save_changes() == QDialog.Rejected:
1294                # Reject the event
1295                event.ignore()
1296                return
1297
1298        # Set an empty scheme to clear the document
1299        document.setScheme(widgetsscheme.WidgetsScheme())
1300        document.deleteLater()
1301
1302        config.save_config()
1303
1304        geometry = self.saveGeometry()
1305        state = self.saveState(version=self.SETTINGS_VERSION)
1306        settings = QSettings()
1307        settings.beginGroup("canvasmainwindow")
1308        settings.setValue("geometry", geometry)
1309        settings.setValue("state", state)
1310        settings.setValue("canvasdock/expanded",
1311                          self.dock_widget.expanded())
1312        settings.setValue("scheme_margins_enabled",
1313                          self.scheme_margins_enabled)
1314
1315        settings.setValue("last_scheme_dir", self.last_scheme_dir)
1316        settings.endGroup()
1317
1318        event.accept()
1319
1320        # Close any windows left.
1321        application = QApplication.instance()
1322        QTimer.singleShot(0, application.closeAllWindows)
1323
1324    def showEvent(self, event):
1325        settings = QSettings()
1326        geom_data = settings.value("canvasmainwindow/geometry")
1327        if geom_data.isValid():
1328            self.restoreGeometry(geom_data.toByteArray())
1329
1330        return QMainWindow.showEvent(self, event)
1331
1332    # Mac OS X
1333    if sys.platform == "darwin":
1334        def toggleMaximized(self):
1335            """Toggle normal/maximized window state.
1336            """
1337            if self.isMinimized():
1338                # Do nothing if window is minimized
1339                return
1340
1341            if self.isMaximized():
1342                self.showNormal()
1343            else:
1344                self.showMaximized()
1345
1346        def changeEvent(self, event):
1347            if event.type() == QEvent.WindowStateChange:
1348                # Enable/disable window menu based on minimized state
1349                self.window_menu.setEnabled(not self.isMinimized())
1350            QMainWindow.changeEvent(self, event)
1351
1352    def tr(self, sourceText, disambiguation=None, n=-1):
1353        """Translate the string.
1354        """
1355        return unicode(QMainWindow.tr(self, sourceText, disambiguation, n))
1356
1357
1358def identity(item):
1359    return item
1360
1361
1362def index(sequence, *what, **kwargs):
1363    """index(sequence, what, [key=None, [predicate=None]])
1364    Return index of `what` in `sequence`.
1365    """
1366    what = what[0]
1367    key = kwargs.get("key", identity)
1368    predicate = kwargs.get("predicate", operator.eq)
1369    for i, item in enumerate(sequence):
1370        item_key = key(item)
1371        if predicate(what, item_key):
1372            return i
1373    raise ValueError("%r not in sequence" % what)
Note: See TracBrowser for help on using the repository browser.