source: orange/Orange/OrangeCanvas/application/canvasmain.py @ 11173:0bcd5f586f7a

Revision 11173:0bcd5f586f7a, 49.5 KB checked in by Ales Erjavec <ales.erjavec@…>, 18 months ago (diff)

Use white icons on black toolbar background.

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