source: orange/Orange/OrangeCanvas/application/canvasmain.py @ 11214:e0f438d0ad39

Revision 11214:e0f438d0ad39, 46.6 KB checked in by Ales Erjavec <ales.erjavec@…>, 17 months ago (diff)

Fixed duplicates in the Recent Schemes list when a saves scheme title changes.

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            action.setData(filename)
510            self.recent_menu.addAction(action)
511            self.recent_scheme_action_group.addAction(action)
512
513        self.recent_menu.addSeparator()
514        self.recent_menu.addAction(self.clear_recent_action)
515        menu_bar.addMenu(file_menu)
516
517        editor_menus = self.scheme_widget.menuBarActions()
518
519        # WARNING: Hard coded order, should lookup the action text
520        # and determine the proper order
521        self.edit_menu = editor_menus[0].menu()
522        self.widget_menu = editor_menus[1].menu()
523
524        # Edit menu
525        menu_bar.addMenu(self.edit_menu)
526
527        # View menu
528        self.view_menu = QMenu(self.tr("&View"), self)
529        self.toolbox_menu = QMenu(self.tr("Widget Toolbox Style"),
530                                  self.view_menu)
531        self.toolbox_menu_group = \
532            QActionGroup(self, objectName="toolbox-menu-group")
533
534        a1 = self.toolbox_menu.addAction(self.tr("Tool Box"))
535        a2 = self.toolbox_menu.addAction(self.tr("Tool List"))
536        self.toolbox_menu_group.addAction(a1)
537        self.toolbox_menu_group.addAction(a2)
538
539        self.view_menu.addMenu(self.toolbox_menu)
540        self.view_menu.addSeparator()
541        self.view_menu.addAction(self.toogle_margins_action)
542        menu_bar.addMenu(self.view_menu)
543
544        # Options menu
545        self.options_menu = QMenu(self.tr("&Options"), self)
546        self.options_menu.addAction(self.show_output_action)
547#        self.options_menu.addAction("Add-ons")
548#        self.options_menu.addAction("Developers")
549#        self.options_menu.addAction("Run Discovery")
550#        self.options_menu.addAction("Show Canvas Log")
551#        self.options_menu.addAction("Attach Python Console")
552        self.options_menu.addSeparator()
553        self.options_menu.addAction(self.canvas_settings_action)
554
555        # Widget menu
556        menu_bar.addMenu(self.widget_menu)
557
558        if sys.platform == "darwin":
559            # Mac OS X native look and feel.
560            self.window_menu = QMenu(self.tr("Window"), self)
561            self.window_menu.addAction(self.minimize_action)
562            self.window_menu.addAction(self.zoom_action)
563            menu_bar.addMenu(self.window_menu)
564
565        menu_bar.addMenu(self.options_menu)
566
567        # Help menu.
568        self.help_menu = QMenu(self.tr("&Help"), self)
569        self.help_menu.addAction(self.about_action)
570        self.help_menu.addAction(self.welcome_action)
571        self.help_menu.addAction(self.tutorials_action)
572        self.help_menu.addAction(self.documentation_action)
573        menu_bar.addMenu(self.help_menu)
574
575        self.setMenuBar(menu_bar)
576
577    def set_document_title(self, title):
578        """Set the document title (and the main window title). If `title`
579        is an empty string a default 'untitled' placeholder will be used.
580
581        """
582        if self.__document_title != title:
583            self.__document_title = title
584
585            if not title:
586                # TODO: should the default name be platform specific
587                title = self.tr("untitled")
588
589            self.setWindowTitle(title + "[*]")
590
591    def document_title(self):
592        """Return the document title.
593        """
594        return self.__document_title
595
596    def set_widget_registry(self, widget_registry):
597        """Set widget registry.
598        """
599        if self.widget_registry is not None:
600            # Clear the dock widget and popup.
601            pass
602
603        self.widget_registry = widget_registry
604        self.widgets_tool_box.setModel(widget_registry.model())
605        self.quick_category.setModel(widget_registry.model())
606
607        self.scheme_widget.setRegistry(widget_registry)
608
609    def set_quick_help_text(self, text):
610        self.canvas_tool_dock.help.setText(text)
611
612    def current_document(self):
613        return self.scheme_widget
614
615    def on_tool_box_widget_activated(self, action):
616        """A widget action in the widget toolbox has been activated.
617        """
618        widget_desc = action.data().toPyObject()
619        if widget_desc:
620            scheme_widget = self.current_document()
621            if scheme_widget:
622                scheme_widget.createNewNode(widget_desc)
623
624    def on_tool_box_widget_hovered(self, action):
625        """Mouse is over a widget in the widget toolbox
626        """
627        widget_desc = action.data().toPyObject()
628        title = ""
629        help_text = ""
630        if widget_desc:
631            title = widget_desc.name
632            description = widget_desc.help
633            if not help_text:
634                description = widget_desc.description
635
636            template = "<h3>{title}</h3>" + \
637                       "<p>{description}</p>" + \
638                       "<a href=''>more...</a>"
639            help_text = template.format(title=title, description=description)
640            # TODO: 'More...' link
641        self.set_quick_help_text(help_text)
642
643    def on_quick_category_action(self, action):
644        """The quick category menu action triggered.
645        """
646        category = action.text()
647        for i in range(self.widgets_tool_box.count()):
648            cat_act = self.widgets_tool_box.tabAction(i)
649            if cat_act.text() == category:
650                if not cat_act.isChecked():
651                    # Trigger the action to expand the tool grid contained
652                    # within.
653                    cat_act.trigger()
654
655            else:
656                if cat_act.isChecked():
657                    # Trigger the action to hide the tool grid contained
658                    # within.
659                    cat_act.trigger()
660
661        self.dock_widget.expand()
662
663    def set_scheme_margins_enabled(self, enabled):
664        """Enable/disable the margins around the scheme document.
665        """
666        if self.__scheme_margins_enabled != enabled:
667            self.__scheme_margins_enabled = enabled
668            self.__update_scheme_margins()
669
670    def scheme_margins_enabled(self):
671        return self.__scheme_margins_enabled
672
673    scheme_margins_enabled = Property(bool,
674                                      fget=scheme_margins_enabled,
675                                      fset=set_scheme_margins_enabled)
676
677    def __update_scheme_margins(self):
678        """Update the margins around the scheme document.
679        """
680        enabled = self.__scheme_margins_enabled
681        self.__dummy_top_toolbar.setVisible(enabled)
682        self.__dummy_bottom_toolbar.setVisible(enabled)
683        central = self.centralWidget()
684
685        margin = 20 if enabled else 0
686
687        if self.dockWidgetArea(self.dock_widget) == Qt.LeftDockWidgetArea:
688            margins = (margin / 2, 0, margin, 0)
689        else:
690            margins = (margin, 0, margin / 2, 0)
691
692        central.layout().setContentsMargins(*margins)
693
694    #################
695    # Action handlers
696    #################
697    def new_scheme(self):
698        """New scheme. Return QDialog.Rejected if the user canceled
699        the operation and QDialog.Accepted otherwise.
700
701        """
702        document = self.current_document()
703        if document.isModified():
704            # Ask for save changes
705            if self.ask_save_changes() == QDialog.Rejected:
706                return QDialog.Rejected
707
708        new_scheme = widgetsscheme.WidgetsScheme()
709
710        settings = QSettings()
711        show = settings.value("schemeinfo/show-at-new-scheme", True).toBool()
712
713        if show:
714            status = self.show_scheme_properties_for(
715                new_scheme, self.tr("New Scheme")
716            )
717
718            if status == QDialog.Rejected:
719                return QDialog.Rejected
720
721        scheme_doc_widget = self.current_document()
722        scheme_doc_widget.setScheme(new_scheme)
723
724        return QDialog.Accepted
725
726    def open_scheme(self):
727        """Open a new scheme. Return QDialog.Rejected if the user canceled
728        the operation and QDialog.Accepted otherwise.
729
730        """
731        document = self.current_document()
732        if document.isModified():
733            if self.ask_save_changes() == QDialog.Rejected:
734                return QDialog.Rejected
735
736        if self.last_scheme_dir is None:
737            # Get user 'Documents' folder
738            start_dir = QDesktopServices.storageLocation(
739                            QDesktopServices.DocumentsLocation)
740        else:
741            start_dir = self.last_scheme_dir
742
743        # TODO: Use a dialog instance and use 'addSidebarUrls' to
744        # set one or more extra sidebar locations where Schemes are stored.
745        # Also use setHistory
746        filename = QFileDialog.getOpenFileName(
747            self, self.tr("Open Orange Scheme File"),
748            start_dir, self.tr("Orange Scheme (*.ows)"),
749        )
750
751        if filename:
752            self.load_scheme(filename)
753            return QDialog.Accepted
754        else:
755            return QDialog.Rejected
756
757    def load_scheme(self, filename):
758        """Load a scheme from a file (`filename`) into the current
759        document updates the recent scheme list and the loaded scheme path
760        property.
761
762        """
763        filename = unicode(filename)
764        dirname = os.path.dirname(filename)
765
766        self.last_scheme_dir = dirname
767
768        new_scheme = self.new_scheme_from(filename)
769
770        scheme_doc_widget = self.current_document()
771        scheme_doc_widget.setScheme(new_scheme)
772
773        self.add_recent_scheme(new_scheme)
774
775    def new_scheme_from(self, filename):
776        """Create and return a new :class:`widgetsscheme.WidgetsScheme`
777        from a saved `filename`.
778
779        """
780        new_scheme = widgetsscheme.WidgetsScheme()
781        try:
782            new_scheme.load_from(open(filename, "rb"))
783        except Exception:
784            message_critical(
785                 self.tr("Could not load Orange Scheme file"),
786                 title=self.tr("Error"),
787                 informative_text=self.tr("An unexpected error occurred"),
788                 exc_info=True,
789                 parent=self)
790            return None
791
792        return new_scheme
793
794    def reload_last(self):
795        """Reload last opened scheme. Return QDialog.Rejected if the
796        user canceled the operation and QDialog.Accepted otherwise.
797
798        """
799        document = self.current_document()
800        if document.isModified():
801            if self.ask_save_changes() == QDialog.Rejected:
802                return QDialog.Rejected
803
804        # TODO: Search for a temp backup scheme with per process
805        # locking.
806        if self.recent_schemes:
807            self.load_scheme(self.recent_schemes[0][1])
808
809        return QDialog.Accepted
810
811    def ask_save_changes(self):
812        """Ask the user to save the changes to the current scheme.
813        Return QDialog.Accepted if the scheme was successfully saved
814        or the user selected to discard the changes. Otherwise return
815        QDialog.Rejected.
816
817        """
818        document = self.current_document()
819
820        selected = message_question(
821            self.tr("Do you want to save the changes you made to scheme %r?") \
822                    % document.scheme().title,
823            self.tr("Save Changes?"),
824            self.tr("If you do not save your changes will be lost"),
825            buttons=QMessageBox.Save | QMessageBox.Cancel | \
826                    QMessageBox.Discard,
827            default_button=QMessageBox.Save,
828            parent=self)
829
830        if selected == QMessageBox.Save:
831            return self.save_scheme()
832        elif selected == QMessageBox.Discard:
833            return QDialog.Accepted
834        elif selected == QMessageBox.Cancel:
835            return QDialog.Rejected
836
837    def save_scheme(self):
838        """Save the current scheme. If the scheme does not have an associated
839        path then prompt the user to select a scheme file. Return
840        QDialog.Accepted if the scheme was successfully saved and
841        QDialog.Rejected if the user canceled the file selection.
842
843        """
844        document = self.current_document()
845        curr_scheme = document.scheme()
846
847        if curr_scheme.path:
848            curr_scheme.save_to(open(curr_scheme.path, "wb"))
849            document.setModified(False)
850            return QDialog.Accepted
851        else:
852            return self.save_scheme_as()
853
854    def save_scheme_as(self):
855        """Save the current scheme by asking the user for a filename.
856        Return QFileDialog.Accepted if the scheme was saved successfully
857        and QFileDialog.Rejected if not.
858
859        """
860        document = self.current_document()
861        curr_scheme = document.scheme()
862
863        if curr_scheme.path:
864            start_dir = curr_scheme.path
865        else:
866            if self.last_scheme_dir is not None:
867                start_dir = self.last_scheme_dir
868            else:
869                start_dir = QDesktopServices.storageLocation(
870                    QDesktopServices.DocumentsLocation
871                )
872
873            title = curr_scheme.title or "untitled"
874            start_dir = os.path.join(unicode(start_dir), title + ".ows")
875
876        filename = QFileDialog.getSaveFileName(
877            self, self.tr("Save Orange Scheme File"),
878            start_dir, self.tr("Orange Scheme (*.ows)")
879        )
880
881        if filename:
882            filename = unicode(filename)
883            dirname, basename = os.path.split(filename)
884            self.last_scheme_dir = dirname
885
886            try:
887                curr_scheme.save_to(open(filename, "wb"))
888            except Exception:
889                log.error("Error saving %r to %r", curr_scheme, filename,
890                          exc_info=True)
891                # Also show a message box
892                # TODO: should handle permission errors with a
893                # specialized messages.
894                message_critical(
895                     self.tr("An error occurred while trying to save the %r "
896                             "scheme to %r" % \
897                             (curr_scheme.title, basename)),
898                     title=self.tr("Error saving %r") % basename,
899                     exc_info=True,
900                     parent=self)
901                return QFileDialog.Rejected
902
903            curr_scheme.path = filename
904            if not curr_scheme.title:
905                curr_scheme.title = os.path.splitext(basename)[0]
906
907            self.add_recent_scheme(curr_scheme)
908            document.setModified(False)
909            return QFileDialog.Accepted
910        else:
911            return QFileDialog.Rejected
912
913    def get_started(self, *args):
914        """Show getting started video
915        """
916        url = QUrl(LINKS["start-using"])
917        QDesktopServices.openUrl(url)
918
919    def tutorial(self, *args):
920        """Show tutorial.
921        """
922        url = QUrl(LINKS["tutorial"])
923        QDesktopServices.openUrl(url)
924
925    def documentation(self, *args):
926        """Show reference documentation.
927        """
928        url = QUrl(LINKS["tutorial"])
929        QDesktopServices.openUrl(url)
930
931    def recent_scheme(self, *args):
932        """Browse recent schemes. Return QDialog.Rejected if the user
933        canceled the operation and QDialog.Accepted otherwise.
934
935        """
936        items = [previewmodel.PreviewItem(name=title, path=path)
937                 for title, path in self.recent_schemes]
938        model = previewmodel.PreviewModel(items=items)
939
940        dialog = previewdialog.PreviewDialog(self)
941        title = self.tr("Recent Schemes")
942        dialog.setWindowTitle(title)
943        template = ('<h3 style="font-size: 26px">\n'
944                    #'<img height="26" src="canvas_icons:Recent.svg">\n'
945                    '{0}\n'
946                    '</h3>')
947        dialog.setHeading(template.format(title))
948        dialog.setModel(model)
949
950        model.delayedScanUpdate()
951
952        status = dialog.exec_()
953
954        if status == QDialog.Accepted:
955            doc = self.current_document()
956            if doc.isModified():
957                if self.ask_save_changes() == QDialog.Rejected:
958                    return QDialog.Rejected
959
960            index = dialog.currentIndex()
961            selected = model.item(index)
962
963            self.load_scheme(unicode(selected.path()))
964
965        return status
966
967    def tutorial_scheme(self, *args):
968        """Browse a collection of tutorial schemes. Returns QDialog.Rejected
969        if the user canceled the dialog else loads the selected scheme into
970        the canvas and returns QDialog.Accepted.
971
972        """
973        tutors = tutorials.tutorials()
974        items = [previewmodel.PreviewItem(path=t.abspath()) for t in tutors]
975        model = previewmodel.PreviewModel(items=items)
976        dialog = previewdialog.PreviewDialog(self)
977        title = self.tr("Tutorials")
978        dialog.setWindowTitle(title)
979        template = ('<h3 style="font-size: 26px">\n'
980                    #'<img height="26" src="canvas_icons:Tutorials.svg">\n'
981                    '{0}\n'
982                    '</h3>')
983
984        dialog.setHeading(template.format(title))
985        dialog.setModel(model)
986
987        model.delayedScanUpdate()
988        status = dialog.exec_()
989
990        if status == QDialog.Accepted:
991            doc = self.current_document()
992            if doc.isModified():
993                if self.ask_save_changes() == QDialog.Rejected:
994                    return QDialog.Rejected
995
996            index = dialog.currentIndex()
997            selected = model.item(index)
998
999            new_scheme = self.new_scheme_from(unicode(selected.path()))
1000            # Clear the 'path' property (set by scheme.load_from), so
1001            # ctrl-s does not override the saved tutorial file in case the
1002            # tutorial file is writable.
1003            new_scheme.path = ""
1004            self.current_document().setScheme(new_scheme)
1005        return status
1006
1007    def welcome_dialog(self):
1008        """Show a modal welcome dialog for Orange Canvas.
1009        """
1010
1011        dialog = welcomedialog.WelcomeDialog(self)
1012        dialog.setWindowTitle(self.tr("Welcome to Orange Data Mining"))
1013
1014        def new_scheme():
1015            if self.new_scheme() == QDialog.Accepted:
1016                dialog.accept()
1017
1018        def open_scheme():
1019            if self.open_scheme() == QDialog.Accepted:
1020                dialog.accept()
1021
1022        def open_recent():
1023            if self.recent_scheme() == QDialog.Accepted:
1024                dialog.accept()
1025
1026        def tutorial():
1027            if self.tutorial_scheme() == QDialog.Accepted:
1028                dialog.accept()
1029
1030        new_action = \
1031            QAction(self.tr("New"), dialog,
1032                    toolTip=self.tr("Open a new scheme."),
1033                    triggered=new_scheme,
1034                    shortcut=QKeySequence.New,
1035                    icon=canvas_icons("New.svg")
1036                    )
1037
1038        open_action = \
1039            QAction(self.tr("Open"), dialog,
1040                    objectName="welcome-action-open",
1041                    toolTip=self.tr("Open a scheme."),
1042                    triggered=open_scheme,
1043                    shortcut=QKeySequence.Open,
1044                    icon=canvas_icons("Open.svg")
1045                    )
1046
1047        recent_action = \
1048            QAction(self.tr("Recent"), dialog,
1049                    objectName="welcome-recent-action",
1050                    toolTip=self.tr("Browse and open a recent scheme."),
1051                    triggered=open_recent,
1052                    shortcut=QKeySequence(Qt.ControlModifier | \
1053                                          (Qt.ShiftModifier | Qt.Key_R)),
1054                    icon=canvas_icons("Recent.svg")
1055                    )
1056
1057        tutorials_action = \
1058            QAction(self.tr("Tutorial"), dialog,
1059                    objectName="welcome-tutorial-action",
1060                    toolTip=self.tr("Browse tutorial schemes."),
1061                    triggered=tutorial,
1062                    icon=canvas_icons("Tutorials.svg")
1063                    )
1064
1065        top_row = [self.get_started_action, tutorials_action,
1066                   self.documentation_action]
1067
1068        self.new_action.triggered.connect(dialog.accept)
1069        bottom_row = [new_action, open_action, recent_action]
1070
1071        dialog.addRow(top_row, background="light-grass")
1072        dialog.addRow(bottom_row, background="light-orange")
1073
1074        settings = QSettings()
1075
1076        dialog.setShowAtStartup(
1077            settings.value("welcomedialog/show-at-startup", True).toBool()
1078        )
1079
1080        status = dialog.exec_()
1081
1082        settings.setValue("welcomedialog/show-at-startup",
1083                          dialog.showAtStartup())
1084        return status
1085
1086    def show_scheme_properties(self):
1087        """Show current scheme properties.
1088        """
1089        current_doc = self.current_document()
1090        scheme = current_doc.scheme()
1091        return self.show_scheme_properties_for(scheme)
1092
1093    def show_scheme_properties_for(self, scheme, window_title=None):
1094        """Show scheme properties for `scheme` with `window_title (if None
1095        a default 'Scheme Info' title will be used.
1096
1097        """
1098        settings = QSettings()
1099        value_key = "schemeinfo/show-at-new-scheme"
1100
1101        dialog = SchemeInfoDialog(self)
1102
1103        if window_title is None:
1104            window_title = self.tr("Scheme Info")
1105
1106        dialog.setWindowTitle(window_title)
1107        dialog.setFixedSize(725, 450)
1108
1109        dialog.setDontShowAtNewScheme(
1110            not settings.value(value_key, True).toBool()
1111        )
1112
1113        dialog.setScheme(scheme)
1114
1115        status = dialog.exec_()
1116        if status == QDialog.Accepted:
1117            # Store the check state.
1118            settings.setValue(value_key, not dialog.dontShowAtNewScheme())
1119
1120        return status
1121
1122    def set_signal_freeze(self, freeze):
1123        scheme = self.current_document().scheme()
1124        if freeze:
1125            scheme.signal_manager.freeze().push()
1126        else:
1127            scheme.signal_manager.freeze().pop()
1128
1129    def remove_selected(self):
1130        """Remove current scheme selection.
1131        """
1132        self.current_document().removeSelected()
1133
1134    def quit(self):
1135        """Quit the application.
1136        """
1137        self.close()
1138
1139    def select_all(self):
1140        self.current_document().selectAll()
1141
1142    def open_widget(self):
1143        """Open/raise selected widget's GUI.
1144        """
1145        self.current_document().openSelected()
1146
1147    def rename_widget(self):
1148        """Rename the current focused widget.
1149        """
1150        doc = self.current_document()
1151        nodes = doc.selectedNodes()
1152        if len(nodes) == 1:
1153            doc.editNodeTitle(nodes[0])
1154
1155    def widget_help(self):
1156        """Open widget help page.
1157        """
1158        doc = self.current_document()
1159        nodes = doc.selectedNodes()
1160        help_url = None
1161        if len(nodes) == 1:
1162            node = nodes[0]
1163            desc = node.description
1164            if desc.help:
1165                help_url = desc.help
1166
1167        if help_url is not None:
1168            QDesktopServices.openUrl(QUrl(help_url))
1169        else:
1170            message_information(
1171                self.tr("Sorry there is documentation available for "
1172                        "this widget."),
1173                parent=self)
1174
1175    def open_canvas_settings(self):
1176        """Open canvas settings/preferences dialog
1177        """
1178        pass
1179
1180    def show_output_view(self):
1181        """Show a window with application output.
1182        """
1183        self.output_dock.show()
1184
1185    def output_view(self):
1186        """Return the output text widget.
1187        """
1188        return self.output_dock.widget()
1189
1190    def open_about(self):
1191        """Open the about dialog.
1192        """
1193        dlg = AboutDialog(self)
1194        dlg.exec_()
1195
1196    def add_recent_scheme(self, scheme):
1197        """Add `scheme` to the list of recent schemes.
1198        """
1199        if not scheme.path:
1200            # Scheme does not have an associated persistent path so we
1201            # can't do anything.
1202            return
1203
1204        title = scheme.title
1205        path = scheme.path
1206
1207        if title is None:
1208            title = os.path.basename(path)
1209            title, _ = os.path.splitext(title)
1210
1211        filename = os.path.abspath(os.path.realpath(path))
1212        filename = os.path.normpath(filename)
1213
1214        actions_by_filename = {}
1215        for action in self.recent_scheme_action_group.actions():
1216            path = unicode(action.data().toString())
1217            actions_by_filename[path] = action
1218
1219        if filename in actions_by_filename:
1220            # Remove the title/filename (so it can be reinserted)
1221            recent_index = index(self.recent_schemes, filename,
1222                                 key=operator.itemgetter(1))
1223            self.recent_schemes.pop(recent_index)
1224
1225            action = actions_by_filename[filename]
1226            self.recent_menu.removeAction(action)
1227        else:
1228            action = QAction(title or self.tr("untitled"), self,
1229                             toolTip=filename)
1230            action.setData(filename)
1231
1232        self.recent_schemes.insert(0, (title, filename))
1233
1234        recent_actions = self.recent_menu.actions()
1235        begin_index = index(recent_actions, self.recent_menu_begin)
1236        action_before = recent_actions[begin_index + 1]
1237
1238        self.recent_menu.insertAction(action_before, action)
1239        self.recent_scheme_action_group.addAction(action)
1240
1241        config.save_recent_scheme_list(self.recent_schemes)
1242
1243    def clear_recent_schemes(self):
1244        """Clear list of recent schemes
1245        """
1246        actions = list(self.recent_menu.actions())
1247
1248        # Exclude permanent actions (Browse Recent, separators, Clear List)
1249        actions_to_remove = [action for action in actions \
1250                             if unicode(action.data().toString())]
1251
1252        for action in actions_to_remove:
1253            self.recent_menu.removeAction(action)
1254
1255        self.recent_schemes = []
1256        config.save_recent_scheme_list([])
1257
1258    def _on_recent_scheme_action(self, action):
1259        """A recent scheme action was triggered by the user
1260        """
1261        document = self.current_document()
1262        if document.isModified():
1263            if self.ask_save_changes() == QDialog.Rejected:
1264                return
1265
1266        filename = unicode(action.data().toString())
1267        self.load_scheme(filename)
1268
1269    def _on_dock_location_changed(self, location):
1270        """Location of the dock_widget has changed, fix the margins
1271        if necessary.
1272
1273        """
1274        self.__update_scheme_margins()
1275
1276    def createPopupMenu(self):
1277        # Override the default context menu popup (we don't want the user to
1278        # be able to hide the tool dock widget).
1279        return None
1280
1281    def closeEvent(self, event):
1282        """Close the main window.
1283        """
1284        document = self.current_document()
1285        if document.isModified():
1286            if self.ask_save_changes() == QDialog.Rejected:
1287                # Reject the event
1288                event.ignore()
1289                return
1290
1291        # Set an empty scheme to clear the document
1292        document.setScheme(widgetsscheme.WidgetsScheme())
1293        document.deleteLater()
1294
1295        config.save_config()
1296
1297        geometry = self.saveGeometry()
1298        state = self.saveState(version=self.SETTINGS_VERSION)
1299        settings = QSettings()
1300        settings.beginGroup("canvasmainwindow")
1301        settings.setValue("geometry", geometry)
1302        settings.setValue("state", state)
1303        settings.setValue("canvasdock/expanded",
1304                          self.dock_widget.expanded())
1305        settings.setValue("scheme_margins_enabled",
1306                          self.scheme_margins_enabled)
1307
1308        settings.setValue("last_scheme_dir", self.last_scheme_dir)
1309        settings.endGroup()
1310
1311        event.accept()
1312
1313        # Close any windows left.
1314        application = QApplication.instance()
1315        QTimer.singleShot(0, application.closeAllWindows)
1316
1317    def showEvent(self, event):
1318        settings = QSettings()
1319        geom_data = settings.value("canvasmainwindow/geometry")
1320        if geom_data.isValid():
1321            self.restoreGeometry(geom_data.toByteArray())
1322
1323        return QMainWindow.showEvent(self, event)
1324
1325    # Mac OS X
1326    if sys.platform == "darwin":
1327        def toggleMaximized(self):
1328            """Toggle normal/maximized window state.
1329            """
1330            if self.isMinimized():
1331                # Do nothing if window is minimized
1332                return
1333
1334            if self.isMaximized():
1335                self.showNormal()
1336            else:
1337                self.showMaximized()
1338
1339        def changeEvent(self, event):
1340            if event.type() == QEvent.WindowStateChange:
1341                # Enable/disable window menu based on minimized state
1342                self.window_menu.setEnabled(not self.isMinimized())
1343            QMainWindow.changeEvent(self, event)
1344
1345    def tr(self, sourceText, disambiguation=None, n=-1):
1346        """Translate the string.
1347        """
1348        return unicode(QMainWindow.tr(self, sourceText, disambiguation, n))
1349
1350
1351def identity(item):
1352    return item
1353
1354
1355def index(sequence, *what, **kwargs):
1356    """index(sequence, what, [key=None, [predicate=None]])
1357    Return index of `what` in `sequence`.
1358    """
1359    what = what[0]
1360    key = kwargs.get("key", identity)
1361    predicate = kwargs.get("predicate", operator.eq)
1362    for i, item in enumerate(sequence):
1363        item_key = key(item)
1364        if predicate(what, item_key):
1365            return i
1366    raise ValueError("%r not in sequence" % what)
Note: See TracBrowser for help on using the repository browser.