source: orange/Orange/OrangeCanvas/application/canvasmain.py @ 11125:9e3faa1de572

Revision 11125:9e3faa1de572, 43.4 KB checked in by Ales Erjavec <ales.erjavec@…>, 18 months ago (diff)

Added main application window widget.

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