source: orange/Orange/OrangeCanvas/application/canvasmain.py @ 11201:04262f6c3392

Revision 11201:04262f6c3392, 49.0 KB checked in by Ales Erjavec <ales.erjavec@…>, 17 months ago (diff)

Added annotation action menus for font size and arrow color.

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