GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.

Orange.canvas.application.CanvasMainWindow   F
last analyzed

Complexity

Total Complexity 198

Size/Duplication

Total Lines 1774
Duplicated Lines 0 %
Metric Value
dl 0
loc 1774
rs 0.6316
wmc 198

67 Methods

Rating   Name   Duplication   Size   Complexity  
A output_view() 0 4 1
B ask_save_changes() 0 25 4
A rename_widget() 0 7 2
B setup_ui() 0 170 2
A open_scheme_file() 0 14 4
A _on_tool_dock_expanded() 0 6 2
A open_canvas_settings() 0 9 2
A get_started() 0 5 1
A __update_scheme_margins() 0 16 3
A _on_recent_scheme_action() 0 10 3
A tutorial() 0 5 2
A sizeHint() 0 6 1
B __init__() 0 28 2
B recent_scheme() 0 39 5
A show_help() 0 19 3
B setup_actions() 0 209 2
A open_addons() 0 5 1
A show_scheme_properties_for() 0 23 3
B tutorial_scheme() 0 39 6
A toggleMaximized() 0 11 3
A set_widget_registry() 0 23 3
A createPopupMenu() 0 4 1
A reload_last() 0 16 4
A open_widget() 0 4 1
C save_scheme_to() 0 74 7
F event() 0 41 11
A document_title() 0 4 1
A remove_selected() 0 4 1
B reset_widget_settings() 0 25 4
B closeEvent() 0 46 3
A set_quick_help_text() 0 2 1
A set_signal_freeze() 0 7 2
F __update_from_settings() 0 104 9
A current_document() 0 2 1
A show_report_view() 0 4 1
B setup_menu() 0 97 3
A select_all() 0 2 1
B new_scheme_from() 0 33 3
F welcome_dialog() 0 81 9
A set_document_title() 0 13 3
A documentation() 0 5 1
A tr() 0 4 1
A set_scheme_margins_enabled() 0 6 2
A open_and_freeze_scheme() 0 17 4
B check_can_save() 0 34 6
A scheme_margins_enabled() 0 2 1
B new_scheme() 0 28 2
A showEvent() 0 18 4
A show_output_view() 0 4 1
A set_new_scheme() 0 20 2
A save_scheme() 0 20 4
A open_about() 0 6 1
B add_recent_scheme() 0 49 6
A quit() 0 11 2
B show_scheme_properties() 0 24 2
B on_quick_category_action() 0 24 6
A load_scheme() 0 18 2
A set_tool_dock_expanded() 0 5 1
A scheme_properties_dialog() 0 16 1
B save_scheme_as() 0 42 6
A open_recent() 0 3 2
A clear_recent_schemes() 0 15 4
A _on_dock_location_changed() 0 6 1
A on_tool_box_widget_activated() 0 8 3
B restore() 0 40 3
A changeEvent() 0 9 3
A open_scheme() 0 3 2

How to fix   Complexity   

Complex Class

Complex classes like Orange.canvas.application.CanvasMainWindow often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
"""
2
Orange Canvas Main Window
3
4
"""
5
import os
6
import sys
7
import logging
8
import operator
9
from functools import partial
10
from io import BytesIO
11
12
import pkg_resources
13
14
from PyQt4.QtGui import (
0 ignored issues
show
Configuration introduced by
The import PyQt4.QtGui could not be resolved.

This can be caused by one of the following:

1. Missing Dependencies

This error could indicate a configuration issue of Pylint. Make sure that your libraries are available by adding the necessary commands.

# .scrutinizer.yml
before_commands:
    - sudo pip install abc # Python2
    - sudo pip3 install abc # Python3
Tip: We are currently not using virtualenv to run pylint, when installing your modules make sure to use the command for the correct version.

2. Missing __init__.py files

This error could also result from missing __init__.py files in your module folders. Make sure that you place one file in each sub-folder.

Loading history...
15
    QMainWindow, QWidget, QAction, QActionGroup, QMenu, QMenuBar, QDialog,
16
    QFileDialog, QMessageBox, QVBoxLayout, QSizePolicy, QColor, QKeySequence,
17
    QIcon, QToolBar, QToolButton, QDockWidget, QDesktopServices, QApplication,
18
)
19
20
from PyQt4.QtCore import (
0 ignored issues
show
Configuration introduced by
The import PyQt4.QtCore could not be resolved.

This can be caused by one of the following:

1. Missing Dependencies

This error could indicate a configuration issue of Pylint. Make sure that your libraries are available by adding the necessary commands.

# .scrutinizer.yml
before_commands:
    - sudo pip install abc # Python2
    - sudo pip3 install abc # Python3
Tip: We are currently not using virtualenv to run pylint, when installing your modules make sure to use the command for the correct version.

2. Missing __init__.py files

This error could also result from missing __init__.py files in your module folders. Make sure that you place one file in each sub-folder.

Loading history...
21
    Qt, QEvent, QSize, QUrl, QTimer, QFile, QByteArray
22
)
23
24
from PyQt4.QtNetwork import QNetworkDiskCache
0 ignored issues
show
Configuration introduced by
The import PyQt4.QtNetwork could not be resolved.

This can be caused by one of the following:

1. Missing Dependencies

This error could indicate a configuration issue of Pylint. Make sure that your libraries are available by adding the necessary commands.

# .scrutinizer.yml
before_commands:
    - sudo pip install abc # Python2
    - sudo pip3 install abc # Python3
Tip: We are currently not using virtualenv to run pylint, when installing your modules make sure to use the command for the correct version.

2. Missing __init__.py files

This error could also result from missing __init__.py files in your module folders. Make sure that you place one file in each sub-folder.

Loading history...
25
26
from PyQt4.QtWebKit import QWebView
0 ignored issues
show
Configuration introduced by
The import PyQt4.QtWebKit could not be resolved.

This can be caused by one of the following:

1. Missing Dependencies

This error could indicate a configuration issue of Pylint. Make sure that your libraries are available by adding the necessary commands.

# .scrutinizer.yml
before_commands:
    - sudo pip install abc # Python2
    - sudo pip3 install abc # Python3
Tip: We are currently not using virtualenv to run pylint, when installing your modules make sure to use the command for the correct version.

2. Missing __init__.py files

This error could also result from missing __init__.py files in your module folders. Make sure that you place one file in each sub-folder.

Loading history...
27
28
from PyQt4.QtCore import pyqtProperty as Property
0 ignored issues
show
Configuration introduced by
The import PyQt4.QtCore could not be resolved.

This can be caused by one of the following:

1. Missing Dependencies

This error could indicate a configuration issue of Pylint. Make sure that your libraries are available by adding the necessary commands.

# .scrutinizer.yml
before_commands:
    - sudo pip install abc # Python2
    - sudo pip3 install abc # Python3
Tip: We are currently not using virtualenv to run pylint, when installing your modules make sure to use the command for the correct version.

2. Missing __init__.py files

This error could also result from missing __init__.py files in your module folders. Make sure that you place one file in each sub-folder.

Loading history...
29
30
# Compatibility with PyQt < v4.8.3
31
from ..utils.qtcompat import QSettings
32
33
from ..gui.dropshadow import DropShadowFrame
34
from ..gui.dock import CollapsibleDockWidget
35
from ..gui.quickhelp import QuickHelpTipEvent
36
from ..gui.utils import message_critical, message_question, \
37
                        message_warning, message_information
38
39
from ..help import HelpManager
40
41
from .canvastooldock import CanvasToolDock, QuickCategoryToolbar, \
42
                            CategoryPopupMenu, popup_position_from_source
43
from .aboutdialog import AboutDialog
44
from .schemeinfo import SchemeInfoDialog
45
from .outputview import OutputView
46
from .settings import UserSettingsDialog
47
48
from ..document.schemeedit import SchemeEditWidget
49
50
from ..scheme import widgetsscheme
51
from ..scheme.readwrite import scheme_load, sniff_version
52
53
from . import welcomedialog
54
from ..preview import previewdialog, previewmodel
55
56
from .. import config
57
58
from . import tutorials
59
60
log = logging.getLogger(__name__)
61
62
# TODO: Orange Version in the base link
0 ignored issues
show
Coding Style introduced by
TODO and FIXME comments should generally be avoided.
Loading history...
63
64
BASE_LINK = "http://orange.biolab.si/"
65
66
LINKS = \
67
    {"start-using": BASE_LINK + "start-using/",
68
     "tutorial": BASE_LINK + "tutorial/",
69
     "reference": BASE_LINK + "doc/"
70
     }
71
72
73
def style_icons(widget, standard_pixmap):
74
    """Return the Qt standard pixmap icon.
75
    """
76
    return QIcon(widget.style().standardPixmap(standard_pixmap))
77
78
79
def canvas_icons(name):
80
    """Return the named canvas icon.
81
    """
82
    icon_file = QFile("canvas_icons:" + name)
83
    if icon_file.exists():
84
        return QIcon("canvas_icons:" + name)
85
    else:
86
        return QIcon(pkg_resources.resource_filename(
87
                      config.__name__,
88
                      os.path.join("icons", name))
89
                     )
90
91
92
class FakeToolBar(QToolBar):
93
    """A Toolbar with no contents (used to reserve top and bottom margins
94
    on the main window).
95
96
    """
97
    def __init__(self, *args, **kwargs):
98
        QToolBar.__init__(self, *args, **kwargs)
99
        self.setFloatable(False)
100
        self.setMovable(False)
101
102
        # Don't show the tool bar action in the main window's
103
        # context menu.
104
        self.toggleViewAction().setVisible(False)
105
106
    def paintEvent(self, event):
107
        # Do nothing.
108
        pass
109
110
111
class DockableWindow(QDockWidget):
112
    def __init__(self, *args, **kwargs):
113
        QDockWidget.__init__(self, *args, **kwargs)
114
115
        # Fist show after floating
116
        self.__firstShow = True
117
        # Flags to use while floating
118
        self.__windowFlags = Qt.Window
119
        self.setWindowFlags(self.__windowFlags)
120
        self.topLevelChanged.connect(self.__on_topLevelChanged)
121
        self.visibilityChanged.connect(self.__on_visbilityChanged)
122
123
        self.__closeAction = QAction(self.tr("Close"), self,
124
                                     shortcut=QKeySequence.Close,
125
                                     triggered=self.close,
126
                                     enabled=self.isFloating())
127
        self.topLevelChanged.connect(self.__closeAction.setEnabled)
128
        self.addAction(self.__closeAction)
129
130
    def setFloatingWindowFlags(self, flags):
131
        """
132
        Set `windowFlags` to use while the widget is floating (undocked).
133
        """
134
        if self.__windowFlags != flags:
135
            self.__windowFlags = flags
136
            if self.isFloating():
137
                self.__fixWindowFlags()
138
139
    def floatingWindowFlags(self):
140
        """
141
        Return the `windowFlags` used when the widget is floating.
142
        """
143
        return self.__windowFlags
144
145
    def __fixWindowFlags(self):
146
        if self.isFloating():
147
            update_window_flags(self, self.__windowFlags)
148
149
    def __on_topLevelChanged(self, floating):
150
        if floating:
151
            self.__firstShow = True
152
            self.__fixWindowFlags()
153
154
    def __on_visbilityChanged(self, visible):
155
        if visible and self.isFloating() and self.__firstShow:
156
            self.__firstShow = False
157
            self.__fixWindowFlags()
158
159
160
def update_window_flags(widget, flags):
161
    currflags = widget.windowFlags()
162
    if int(flags) != int(currflags):
163
        hidden = widget.isHidden()
164
        widget.setWindowFlags(flags)
165
        # setting the flags hides the widget
166
        if not hidden:
167
            widget.show()
168
169
170
class CanvasMainWindow(QMainWindow):
171
    SETTINGS_VERSION = 2
172
173
    def __init__(self, *args):
174
        QMainWindow.__init__(self, *args)
175
176
        self.__scheme_margins_enabled = True
177
        self.__document_title = "untitled"
178
        self.__first_show = True
179
180
        self.widget_registry = None
181
182
        self.last_scheme_dir = QDesktopServices.StandardLocation(
183
            QDesktopServices.DocumentsLocation
184
        )
185
        try:
186
            self.recent_schemes = config.recent_schemes()
187
        except Exception:
0 ignored issues
show
Best Practice introduced by
Catching very general exceptions such as Exception is usually not recommended.

Generally, you would want to handle very specific errors in the exception handler. This ensure that you do not hide other types of errors which should be fixed.

So, unless you specifically plan to handle any error, consider adding a more specific exception.

Loading history...
188
            log.error("Failed to load recent scheme list.", exc_info=True)
189
            self.recent_schemes = []
190
191
        self.num_recent_schemes = 15
192
193
        self.open_in_external_browser = False
194
        self.help = HelpManager(self)
195
196
        self.setup_actions()
197
        self.setup_ui()
198
        self.setup_menu()
199
200
        self.restore()
201
202
    def setup_ui(self):
203
        """Setup main canvas ui
204
        """
205
206
        log.info("Setting up Canvas main window.")
207
208
        # Two dummy tool bars to reserve space
209
        self.__dummy_top_toolbar = FakeToolBar(
210
                            objectName="__dummy_top_toolbar")
211
        self.__dummy_bottom_toolbar = FakeToolBar(
212
                            objectName="__dummy_bottom_toolbar")
213
214
        self.__dummy_top_toolbar.setFixedHeight(20)
215
        self.__dummy_bottom_toolbar.setFixedHeight(20)
216
217
        self.addToolBar(Qt.TopToolBarArea, self.__dummy_top_toolbar)
218
        self.addToolBar(Qt.BottomToolBarArea, self.__dummy_bottom_toolbar)
219
220
        self.setCorner(Qt.BottomLeftCorner, Qt.LeftDockWidgetArea)
221
        self.setCorner(Qt.BottomRightCorner, Qt.RightDockWidgetArea)
222
223
        self.setDockOptions(QMainWindow.AnimatedDocks)
224
        # Create an empty initial scheme inside a container with fixed
225
        # margins.
226
        w = QWidget()
227
        w.setLayout(QVBoxLayout())
228
        w.layout().setContentsMargins(20, 0, 10, 0)
229
230
        self.scheme_widget = SchemeEditWidget()
231
        self.scheme_widget.setScheme(widgetsscheme.WidgetsScheme(parent=self))
232
233
        w.layout().addWidget(self.scheme_widget)
234
235
        self.setCentralWidget(w)
236
237
        # Drop shadow around the scheme document
238
        frame = DropShadowFrame(radius=15)
239
        frame.setColor(QColor(0, 0, 0, 100))
240
        frame.setWidget(self.scheme_widget)
241
242
        # Main window title and title icon.
243
        self.set_document_title(self.scheme_widget.scheme().title)
244
        self.scheme_widget.titleChanged.connect(self.set_document_title)
245
        self.scheme_widget.modificationChanged.connect(self.setWindowModified)
246
247
        # QMainWindow's Dock widget
248
        self.dock_widget = CollapsibleDockWidget(objectName="main-area-dock")
249
        self.dock_widget.setFeatures(QDockWidget.DockWidgetMovable | \
250
                                     QDockWidget.DockWidgetClosable)
251
252
        self.dock_widget.setAllowedAreas(Qt.LeftDockWidgetArea | \
253
                                         Qt.RightDockWidgetArea)
254
255
        # Main canvas tool dock (with widget toolbox, common actions.
256
        # This is the widget that is shown when the dock is expanded.
257
        canvas_tool_dock = CanvasToolDock(objectName="canvas-tool-dock")
258
        canvas_tool_dock.setSizePolicy(QSizePolicy.Fixed,
259
                                       QSizePolicy.MinimumExpanding)
260
261
        # Bottom tool bar
262
        self.canvas_toolbar = canvas_tool_dock.toolbar
263
        self.canvas_toolbar.setIconSize(QSize(25, 25))
264
        self.canvas_toolbar.setFixedHeight(28)
265
        self.canvas_toolbar.layout().setSpacing(1)
266
267
        # Widgets tool box
268
        self.widgets_tool_box = canvas_tool_dock.toolbox
269
        self.widgets_tool_box.setObjectName("canvas-toolbox")
270
        self.widgets_tool_box.setTabButtonHeight(30)
271
        self.widgets_tool_box.setTabIconSize(QSize(26, 26))
272
        self.widgets_tool_box.setButtonSize(QSize(64, 84))
273
        self.widgets_tool_box.setIconSize(QSize(48, 48))
274
275
        self.widgets_tool_box.triggered.connect(
276
            self.on_tool_box_widget_activated
277
        )
278
279
        self.dock_help = canvas_tool_dock.help
280
        self.dock_help.setMaximumHeight(150)
281
        self.dock_help.document().setDefaultStyleSheet("h3, a {color: orange;}")
282
283
        self.dock_help_action = canvas_tool_dock.toogleQuickHelpAction()
284
        self.dock_help_action.setText(self.tr("Show Help"))
285
        self.dock_help_action.setIcon(canvas_icons("Info.svg"))
286
287
        self.canvas_tool_dock = canvas_tool_dock
288
289
        # Dock contents when collapsed (a quick category tool bar, ...)
290
        dock2 = QWidget(objectName="canvas-quick-dock")
291
        dock2.setLayout(QVBoxLayout())
292
        dock2.layout().setContentsMargins(0, 0, 0, 0)
293
        dock2.layout().setSpacing(0)
294
        dock2.layout().setSizeConstraint(QVBoxLayout.SetFixedSize)
295
296
        self.quick_category = QuickCategoryToolbar()
297
        self.quick_category.setButtonSize(QSize(38, 30))
298
        self.quick_category.actionTriggered.connect(
299
            self.on_quick_category_action
300
        )
301
302
        tool_actions = self.current_document().toolbarActions()
303
304
        (self.canvas_zoom_action, self.canvas_align_to_grid_action,
305
         self.canvas_text_action, self.canvas_arrow_action,) = tool_actions
306
307
        self.canvas_zoom_action.setIcon(canvas_icons("Search.svg"))
308
        self.canvas_align_to_grid_action.setIcon(canvas_icons("Grid.svg"))
309
        self.canvas_text_action.setIcon(canvas_icons("Text Size.svg"))
310
        self.canvas_arrow_action.setIcon(canvas_icons("Arrow.svg"))
311
312
        dock_actions = [self.show_properties_action] + \
313
                       tool_actions + \
314
                       [self.freeze_action,
315
                        self.dock_help_action]
316
317
        # Tool bar in the collapsed dock state (has the same actions as
318
        # the tool bar in the CanvasToolDock
319
        actions_toolbar = QToolBar(orientation=Qt.Vertical)
320
        actions_toolbar.setFixedWidth(38)
321
        actions_toolbar.layout().setSpacing(0)
322
323
        actions_toolbar.setToolButtonStyle(Qt.ToolButtonIconOnly)
324
325
        for action in dock_actions:
326
            self.canvas_toolbar.addAction(action)
327
            button = self.canvas_toolbar.widgetForAction(action)
328
            button.setPopupMode(QToolButton.DelayedPopup)
329
330
            actions_toolbar.addAction(action)
331
            button = actions_toolbar.widgetForAction(action)
332
            button.setFixedSize(38, 30)
333
            button.setPopupMode(QToolButton.DelayedPopup)
334
335
        dock2.layout().addWidget(self.quick_category)
336
        dock2.layout().addWidget(actions_toolbar)
337
338
        self.dock_widget.setAnimationEnabled(False)
339
        self.dock_widget.setExpandedWidget(self.canvas_tool_dock)
340
        self.dock_widget.setCollapsedWidget(dock2)
341
        self.dock_widget.setExpanded(True)
342
        self.dock_widget.expandedChanged.connect(self._on_tool_dock_expanded)
343
344
        self.addDockWidget(Qt.LeftDockWidgetArea, self.dock_widget)
345
        self.dock_widget.dockLocationChanged.connect(
346
            self._on_dock_location_changed
347
        )
348
349
        self.output_dock = DockableWindow(self.tr("Output"), self,
350
                                          objectName="output-dock")
351
        self.output_dock.setAllowedAreas(Qt.BottomDockWidgetArea)
352
        output_view = OutputView()
353
        # Set widget before calling addDockWidget, otherwise the dock
354
        # does not resize properly on first undock
355
        self.output_dock.setWidget(output_view)
356
        self.output_dock.hide()
357
358
        self.help_dock = DockableWindow(self.tr("Help"), self,
359
                                        objectName="help-dock")
360
        self.help_dock.setAllowedAreas(Qt.NoDockWidgetArea)
361
        self.help_view = QWebView()
362
        manager = self.help_view.page().networkAccessManager()
363
        cache = QNetworkDiskCache()
364
        cache.setCacheDirectory(
365
            os.path.join(config.cache_dir(), "help", "help-view-cache")
366
        )
367
        manager.setCache(cache)
368
        self.help_dock.setWidget(self.help_view)
369
        self.help_dock.hide()
370
371
        self.setMinimumSize(600, 500)
372
373
    def setup_actions(self):
374
        """Initialize main window actions.
375
        """
376
377
        self.new_action = \
378
            QAction(self.tr("New"), self,
379
                    objectName="action-new",
380
                    toolTip=self.tr("Open a new workflow."),
381
                    triggered=self.new_scheme,
382
                    shortcut=QKeySequence.New,
383
                    icon=canvas_icons("New.svg")
384
                    )
385
386
        self.open_action = \
387
            QAction(self.tr("Open"), self,
388
                    objectName="action-open",
389
                    toolTip=self.tr("Open a workflow."),
390
                    triggered=self.open_scheme,
391
                    shortcut=QKeySequence.Open,
392
                    icon=canvas_icons("Open.svg")
393
                    )
394
395
        self.open_and_freeze_action = \
396
            QAction(self.tr("Open and Freeze"), self,
397
                    objectName="action-open-and-freeze",
398
                    toolTip=self.tr("Open a new workflow and freeze signal "
399
                                    "propagation."),
400
                    triggered=self.open_and_freeze_scheme
401
                    )
402
403
        self.save_action = \
404
            QAction(self.tr("Save"), self,
405
                    objectName="action-save",
406
                    toolTip=self.tr("Save current workflow."),
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 workflow 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 'Get Started' introduction."),
439
                    triggered=self.get_started,
440
                    icon=canvas_icons("Get Started.svg")
441
                    )
442
443
        self.tutorials_action = \
444
            QAction(self.tr("Tutorials"), self,
445
                    objectName="tutorial-action",
446
                    toolTip=self.tr("Browse tutorials."),
447
                    triggered=self.tutorial_scheme,
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 workflow."),
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 Workflow"), self,
485
                    objectName="reload-last-action",
486
                    toolTip=self.tr("Reload last open workflow."),
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("Workflow Info"), self,
500
                    objectName="show-properties-action",
501
                    toolTip=self.tr("Show workflow 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.canvas_addons_action = \
517
            QAction(self.tr("&Add-ons..."), self,
518
                    objectName="canvas-addons-action",
519
                    toolTip=self.tr("Manage add-ons."),
520
                    triggered=self.open_addons,
521
                    )
522
523
        self.show_output_action = \
524
            QAction(self.tr("Show Output View"), self,
525
                    toolTip=self.tr("Show application output."),
526
                    triggered=self.show_output_view,
527
                    )
528
529
        self.show_report_action = \
530
            QAction(self.tr("Show Report View"), self,
531
                    triggered=self.show_report_view
532
                    )
533
534
        if sys.platform == "darwin":
535
            # Actions for native Mac OSX look and feel.
536
            self.minimize_action = \
537
                QAction(self.tr("Minimize"), self,
538
                        triggered=self.showMinimized,
539
                        shortcut=QKeySequence(Qt.ControlModifier | Qt.Key_M)
540
                        )
541
542
            self.zoom_action = \
543
                QAction(self.tr("Zoom"), self,
544
                        objectName="application-zoom",
545
                        triggered=self.toggleMaximized,
546
                        )
547
548
        self.freeze_action = \
549
            QAction(self.tr("Freeze"), self,
550
                    objectName="signal-freeze-action",
551
                    checkable=True,
552
                    toolTip=self.tr("Freeze signal propagation."),
553
                    triggered=self.set_signal_freeze,
554
                    icon=canvas_icons("Pause.svg")
555
                    )
556
557
        self.toggle_tool_dock_expand = \
558
            QAction(self.tr("Expand Tool Dock"), self,
559
                    objectName="toggle-tool-dock-expand",
560
                    checkable=True,
561
                    shortcut=QKeySequence(Qt.ControlModifier |
562
                                          (Qt.ShiftModifier | Qt.Key_D)),
563
                    triggered=self.set_tool_dock_expanded)
564
        self.toggle_tool_dock_expand.setChecked(True)
565
566
        # Gets assigned in setup_ui (the action is defined in CanvasToolDock)
567
        # TODO: This is bad (should be moved here).
0 ignored issues
show
Coding Style introduced by
TODO and FIXME comments should generally be avoided.
Loading history...
568
        self.dock_help_action = None
569
570
        self.toogle_margins_action = \
571
            QAction(self.tr("Show Workflow Margins"), self,
572
                    checkable=True,
573
                    toolTip=self.tr("Show margins around the workflow view."),
574
                    )
575
        self.toogle_margins_action.setChecked(True)
576
        self.toogle_margins_action.toggled.connect(
577
            self.set_scheme_margins_enabled)
578
579
        self.reset_widget_settings_action = \
580
            QAction(self.tr("Reset widget settings..."), self,
581
                    triggered=self.reset_widget_settings)
582
583
    def setup_menu(self):
584
        menu_bar = QMenuBar()
585
586
        # File menu
587
        file_menu = QMenu(self.tr("&File"), menu_bar)
588
        file_menu.addAction(self.new_action)
589
        file_menu.addAction(self.open_action)
590
        file_menu.addAction(self.open_and_freeze_action)
591
        file_menu.addAction(self.reload_last_action)
592
593
        # File -> Open Recent submenu
594
        self.recent_menu = QMenu(self.tr("Open Recent"), file_menu)
595
        file_menu.addMenu(self.recent_menu)
596
        file_menu.addSeparator()
597
        file_menu.addAction(self.save_action)
598
        file_menu.addAction(self.save_as_action)
599
        file_menu.addSeparator()
600
        file_menu.addAction(self.show_properties_action)
601
        file_menu.addAction(self.quit_action)
602
603
        self.recent_menu.addAction(self.recent_action)
604
605
        # Store the reference to separator for inserting recent
606
        # schemes into the menu in `add_recent_scheme`.
607
        self.recent_menu_begin = self.recent_menu.addSeparator()
608
609
        # Add recent items.
610
        for title, filename in self.recent_schemes:
611
            action = QAction(title or self.tr("untitled"), self,
612
                             toolTip=filename)
613
614
            action.setData(filename)
615
            self.recent_menu.addAction(action)
616
            self.recent_scheme_action_group.addAction(action)
617
618
        self.recent_menu.addSeparator()
619
        self.recent_menu.addAction(self.clear_recent_action)
620
        menu_bar.addMenu(file_menu)
621
622
        editor_menus = self.scheme_widget.menuBarActions()
623
624
        # WARNING: Hard coded order, should lookup the action text
625
        # and determine the proper order
626
        self.edit_menu = editor_menus[0].menu()
627
        self.widget_menu = editor_menus[1].menu()
628
629
        # Edit menu
630
        menu_bar.addMenu(self.edit_menu)
631
632
        # View menu
633
        self.view_menu = QMenu(self.tr("&View"), self)
634
        self.toolbox_menu = QMenu(self.tr("Widget Toolbox Style"),
635
                                  self.view_menu)
636
        self.toolbox_menu_group = \
637
            QActionGroup(self, objectName="toolbox-menu-group")
638
639
        self.view_menu.addAction(self.toggle_tool_dock_expand)
640
641
        self.view_menu.addSeparator()
642
        self.view_menu.addAction(self.toogle_margins_action)
643
        menu_bar.addMenu(self.view_menu)
644
645
        # Options menu
646
        self.options_menu = QMenu(self.tr("&Options"), self)
647
        self.options_menu.addAction(self.show_output_action)
648
        self.options_menu.addAction(self.show_report_action)
649
#        self.options_menu.addAction("Add-ons")
650
#        self.options_menu.addAction("Developers")
651
#        self.options_menu.addAction("Run Discovery")
652
#        self.options_menu.addAction("Show Canvas Log")
653
#        self.options_menu.addAction("Attach Python Console")
654
        self.options_menu.addSeparator()
655
        self.options_menu.addAction(self.canvas_settings_action)
656
        self.options_menu.addAction(self.reset_widget_settings_action)
657
        self.options_menu.addAction(self.canvas_addons_action)
658
659
        # Widget menu
660
        menu_bar.addMenu(self.widget_menu)
661
662
        if sys.platform == "darwin":
663
            # Mac OS X native look and feel.
664
            self.window_menu = QMenu(self.tr("Window"), self)
665
            self.window_menu.addAction(self.minimize_action)
666
            self.window_menu.addAction(self.zoom_action)
667
            menu_bar.addMenu(self.window_menu)
668
669
        menu_bar.addMenu(self.options_menu)
670
671
        # Help menu.
672
        self.help_menu = QMenu(self.tr("&Help"), self)
673
        self.help_menu.addAction(self.about_action)
674
        self.help_menu.addAction(self.welcome_action)
675
        self.help_menu.addAction(self.tutorials_action)
676
        self.help_menu.addAction(self.documentation_action)
677
        menu_bar.addMenu(self.help_menu)
678
679
        self.setMenuBar(menu_bar)
680
681
    def restore(self):
682
        """Restore the main window state from saved settings.
683
        """
684
        QSettings.setDefaultFormat(QSettings.IniFormat)
685
        settings = QSettings()
686
        settings.beginGroup("mainwindow")
687
688
        self.dock_widget.setExpanded(
689
            settings.value("canvasdock/expanded", True, type=bool)
690
        )
691
692
        floatable = settings.value("toolbox-dock-floatable", False, type=bool)
693
        if floatable:
694
            self.dock_widget.setFeatures(self.dock_widget.features() | \
695
                                         QDockWidget.DockWidgetFloatable)
696
697
        self.widgets_tool_box.setExclusive(
698
            settings.value("toolbox-dock-exclusive", True, type=bool)
699
        )
700
701
        self.toogle_margins_action.setChecked(
702
            settings.value("scheme-margins-enabled", False, type=bool)
703
        )
704
705
        default_dir = QDesktopServices.storageLocation(
706
            QDesktopServices.DocumentsLocation
707
        )
708
709
        self.last_scheme_dir = settings.value("last-scheme-dir", default_dir,
710
                                              type=str)
711
712
        if not os.path.exists(self.last_scheme_dir):
713
            # if directory no longer exists reset the saved location.
714
            self.last_scheme_dir = default_dir
715
716
        self.canvas_tool_dock.setQuickHelpVisible(
717
            settings.value("quick-help/visible", True, type=bool)
718
        )
719
720
        self.__update_from_settings()
721
722
    def set_document_title(self, title):
723
        """Set the document title (and the main window title). If `title`
724
        is an empty string a default 'untitled' placeholder will be used.
725
726
        """
727
        if self.__document_title != title:
728
            self.__document_title = title
729
730
            if not title:
731
                # TODO: should the default name be platform specific
0 ignored issues
show
Coding Style introduced by
TODO and FIXME comments should generally be avoided.
Loading history...
732
                title = self.tr("untitled")
733
734
            self.setWindowTitle(title + "[*]")
735
736
    def document_title(self):
737
        """Return the document title.
738
        """
739
        return self.__document_title
740
741
    def set_widget_registry(self, widget_registry):
742
        """Set widget registry.
743
        """
744
        if self.widget_registry is not None:
745
            # Clear the dock widget and popup.
746
            pass
747
748
        self.widget_registry = widget_registry
749
        self.widgets_tool_box.setModel(widget_registry.model())
750
        self.quick_category.setModel(widget_registry.model())
751
752
        self.scheme_widget.setRegistry(widget_registry)
753
754
        self.help.set_registry(widget_registry)
755
756
        # Restore possibly saved widget toolbox tab states
757
        settings = QSettings()
758
759
        state = settings.value("mainwindow/widgettoolbox/state",
760
                                defaultValue=QByteArray(),
761
                                type=QByteArray)
762
        if state:
763
            self.widgets_tool_box.restoreState(state)
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()
775
        if widget_desc:
776
            scheme_widget = self.current_document()
777
            if scheme_widget:
778
                scheme_widget.createNewNode(widget_desc)
779
780
    def on_quick_category_action(self, action):
781
        """The quick category menu action triggered.
782
        """
783
        category = action.text()
784
        if self.use_popover:
785
            # Show a popup menu with the widgets in the category
786
            popup = CategoryPopupMenu(self.quick_category)
787
            reg = self.widget_registry.model()
788
            i = index(self.widget_registry.categories(), category,
789
                      predicate=lambda name, cat: cat.name == name)
790
            if i != -1:
791
                popup.setCategoryItem(reg.item(i))
792
                button = self.quick_category.buttonForAction(action)
793
                pos = popup_position_from_source(popup, button)
794
                action = popup.exec_(pos)
795
                if action is not None:
796
                    self.on_tool_box_widget_activated(action)
797
798
        else:
799
            for i in range(self.widgets_tool_box.count()):
800
                cat_act = self.widgets_tool_box.tabAction(i)
801
                cat_act.setChecked(cat_act.text() == category)
802
803
            self.dock_widget.expand()
804
805
    def set_scheme_margins_enabled(self, enabled):
806
        """Enable/disable the margins around the scheme document.
807
        """
808
        if self.__scheme_margins_enabled != enabled:
809
            self.__scheme_margins_enabled = enabled
810
            self.__update_scheme_margins()
811
812
    def scheme_margins_enabled(self):
813
        return self.__scheme_margins_enabled
814
815
    scheme_margins_enabled = Property(bool,
816
                                      fget=scheme_margins_enabled,
817
                                      fset=set_scheme_margins_enabled)
818
819
    def __update_scheme_margins(self):
820
        """Update the margins around the scheme document.
821
        """
822
        enabled = self.__scheme_margins_enabled
823
        self.__dummy_top_toolbar.setVisible(enabled)
824
        self.__dummy_bottom_toolbar.setVisible(enabled)
825
        central = self.centralWidget()
826
827
        margin = 20 if enabled else 0
828
829
        if self.dockWidgetArea(self.dock_widget) == Qt.LeftDockWidgetArea:
830
            margins = (margin / 2, 0, margin, 0)
831
        else:
832
            margins = (margin, 0, margin / 2, 0)
833
834
        central.layout().setContentsMargins(*margins)
835
836
    #################
837
    # Action handlers
838
    #################
839
    def new_scheme(self):
840
        """New scheme. Return QDialog.Rejected if the user canceled
841
        the operation and QDialog.Accepted otherwise.
842
843
        """
844
        document = self.current_document()
845
        if document.isModifiedStrict():
846
            # Ask for save changes
847
            if self.ask_save_changes() == QDialog.Rejected:
848
                return QDialog.Rejected
849
850
        new_scheme = widgetsscheme.WidgetsScheme(parent=self)
851
852
        settings = QSettings()
853
        show = settings.value("schemeinfo/show-at-new-scheme", True,
854
                              type=bool)
855
856
        if show:
857
            status = self.show_scheme_properties_for(
858
                new_scheme, self.tr("New Workflow")
859
            )
860
861
            if status == QDialog.Rejected:
862
                return QDialog.Rejected
863
864
        self.set_new_scheme(new_scheme)
865
866
        return QDialog.Accepted
867
868
    def open_scheme(self):
869
        """Open a new scheme. Return QDialog.Rejected if the user canceled
870
        the operation and QDialog.Accepted otherwise.
871
872
        """
873
        document = self.current_document()
874
        if document.isModifiedStrict():
875
            if self.ask_save_changes() == QDialog.Rejected:
876
                return QDialog.Rejected
877
878
        if self.last_scheme_dir is None:
879
            # Get user 'Documents' folder
880
            start_dir = QDesktopServices.storageLocation(
881
                            QDesktopServices.DocumentsLocation)
882
        else:
883
            start_dir = self.last_scheme_dir
884
885
        # TODO: Use a dialog instance and use 'addSidebarUrls' to
0 ignored issues
show
Coding Style introduced by
TODO and FIXME comments should generally be avoided.
Loading history...
886
        # set one or more extra sidebar locations where Schemes are stored.
887
        # Also use setHistory
888
        filename = QFileDialog.getOpenFileName(
889
            self, self.tr("Open Orange Workflow File"),
890
            start_dir, self.tr("Orange Workflow (*.ows)"),
891
        )
892
893
        if filename:
894
            self.load_scheme(filename)
895
            return QDialog.Accepted
896
        else:
897
            return QDialog.Rejected
898
899
    def open_and_freeze_scheme(self):
900
        """
901
        Open a new scheme and freeze signal propagation. Return
902
        QDialog.Rejected if the user canceled the operation and
903
        QDialog.Accepted otherwise.
904
905
        """
906
        frozen = self.freeze_action.isChecked()
907
        if not frozen:
908
            self.freeze_action.trigger()
909
910
        state = self.open_scheme()
911
        if state == QDialog.Rejected:
912
            # If the action was rejected restore the original frozen state
913
            if not frozen:
914
                self.freeze_action.trigger()
915
        return state
916
917
    def open_scheme_file(self, filename):
918
        """
919
        Open and load a scheme file.
920
        """
921
        if isinstance(filename, QUrl):
922
            filename = filename.toLocalFile()
923
924
        document = self.current_document()
925
        if document.isModifiedStrict():
926
            if self.ask_save_changes() == QDialog.Rejected:
927
                return QDialog.Rejected
928
929
        self.load_scheme(filename)
930
        return QDialog.Accepted
931
932
    def load_scheme(self, filename):
933
        """Load a scheme from a file (`filename`) into the current
934
        document updates the recent scheme list and the loaded scheme path
935
        property.
936
937
        """
938
        dirname = os.path.dirname(filename)
939
940
        self.last_scheme_dir = dirname
941
942
        new_scheme = self.new_scheme_from(filename)
943
        if new_scheme is not None:
944
            self.set_new_scheme(new_scheme)
945
946
            scheme_doc_widget = self.current_document()
947
            scheme_doc_widget.setPath(filename)
948
949
            self.add_recent_scheme(new_scheme.title, filename)
950
951
    def new_scheme_from(self, filename):
952
        """Create and return a new :class:`widgetsscheme.WidgetsScheme`
953
        from a saved `filename`. Return `None` if an error occurs.
954
955
        """
956
        new_scheme = widgetsscheme.WidgetsScheme(
957
            parent=self, env={"basedir": os.path.dirname(filename)})
958
        errors = []
959
        try:
960
            scheme_load(new_scheme, open(filename, "rb"),
961
                        error_handler=errors.append)
962
963
        except Exception:
0 ignored issues
show
Best Practice introduced by
Catching very general exceptions such as Exception is usually not recommended.

Generally, you would want to handle very specific errors in the exception handler. This ensure that you do not hide other types of errors which should be fixed.

So, unless you specifically plan to handle any error, consider adding a more specific exception.

Loading history...
964
            message_critical(
965
                 self.tr("Could not load an Orange Workflow file"),
966
                 title=self.tr("Error"),
967
                 informative_text=self.tr("An unexpected error occurred "
968
                                          "while loading '%s'.") % filename,
969
                 exc_info=True,
970
                 parent=self)
971
            return None
972
        if errors:
973
            message_warning(
974
                self.tr("Errors occurred while loading the workflow."),
975
                title=self.tr("Problem"),
976
                informative_text=self.tr(
977
                     "There were problems loading some "
978
                     "of the widgets/links in the "
979
                     "workflow."
980
                ),
981
                details="\n".join(map(repr, errors))
982
            )
983
        return new_scheme
984
985
    def reload_last(self):
986
        """Reload last opened scheme. Return QDialog.Rejected if the
987
        user canceled the operation and QDialog.Accepted otherwise.
988
989
        """
990
        document = self.current_document()
991
        if document.isModifiedStrict():
992
            if self.ask_save_changes() == QDialog.Rejected:
993
                return QDialog.Rejected
994
995
        # TODO: Search for a temp backup scheme with per process
0 ignored issues
show
Coding Style introduced by
TODO and FIXME comments should generally be avoided.
Loading history...
996
        # locking.
997
        if self.recent_schemes:
998
            self.load_scheme(self.recent_schemes[0][1])
999
1000
        return QDialog.Accepted
1001
1002
    def set_new_scheme(self, new_scheme):
1003
        """
1004
        Set new_scheme as the current shown scheme. The old scheme
1005
        will be deleted.
1006
1007
        """
1008
        scheme_doc = self.current_document()
1009
        old_scheme = scheme_doc.scheme()
1010
1011
        manager = new_scheme.signal_manager
1012
        if self.freeze_action.isChecked():
1013
            manager.pause()
1014
1015
        scheme_doc.setScheme(new_scheme)
1016
1017
        # Send a close event to the Scheme, it is responsible for
1018
        # closing/clearing all resources (widgets).
1019
        QApplication.sendEvent(old_scheme, QEvent(QEvent.Close))
1020
1021
        old_scheme.deleteLater()
1022
1023
    def ask_save_changes(self):
1024
        """Ask the user to save the changes to the current scheme.
1025
        Return QDialog.Accepted if the scheme was successfully saved
1026
        or the user selected to discard the changes. Otherwise return
1027
        QDialog.Rejected.
1028
1029
        """
1030
        document = self.current_document()
1031
        title = document.scheme().title or "untitled"
1032
        selected = message_question(
1033
            self.tr('Do you want to save the changes you made to workflow "%s"?')
1034
                    % title,
1035
            self.tr("Save Changes?"),
1036
            self.tr("Your changes will be lost if you do not save them."),
1037
            buttons=QMessageBox.Save | QMessageBox.Cancel | \
1038
                    QMessageBox.Discard,
1039
            default_button=QMessageBox.Save,
1040
            parent=self)
1041
1042
        if selected == QMessageBox.Save:
1043
            return self.save_scheme()
1044
        elif selected == QMessageBox.Discard:
1045
            return QDialog.Accepted
1046
        elif selected == QMessageBox.Cancel:
1047
            return QDialog.Rejected
1048
1049
    def check_can_save(self, document, path):
0 ignored issues
show
Unused Code introduced by
The argument document seems to be unused.
Loading history...
1050
        """
1051
        Check if saving the document to `path` would prevent it from
1052
        being read by the version 1.0 of scheme parser. Return ``True``
1053
        if the existing scheme is version 1.0 else show a message box and
1054
        return ``False``
1055
1056
        .. note::
1057
            In case of an error (opening, parsing), this method will return
1058
            ``True``, so the
1059
1060
        """
1061
        if path and os.path.exists(path):
1062
            try:
1063
                version = sniff_version(open(path, "rb"))
1064
            except (IOError, OSError):
1065
                log.error("Error opening '%s'", path, exc_info=True)
1066
                # The client should fail attempting to write.
1067
                return True
1068
            except Exception:
0 ignored issues
show
Best Practice introduced by
Catching very general exceptions such as Exception is usually not recommended.

Generally, you would want to handle very specific errors in the exception handler. This ensure that you do not hide other types of errors which should be fixed.

So, unless you specifically plan to handle any error, consider adding a more specific exception.

Loading history...
1069
                log.error("Error sniffing workflow version in '%s'", path,
1070
                          exc_info=True)
1071
                # Malformed .ows file, ...
1072
                return True
1073
1074
            if version == "1.0":
1075
                # TODO: Ask for overwrite confirmation instead
0 ignored issues
show
Coding Style introduced by
TODO and FIXME comments should generally be avoided.
Loading history...
1076
                message_information(
1077
                    self.tr("Can not overwrite a version 1.0 ows file. "
1078
                            "Please save your work to a new file"),
1079
                    title="Info",
1080
                    parent=self)
1081
                return False
1082
        return True
1083
1084
    def save_scheme(self):
1085
        """Save the current scheme. If the scheme does not have an associated
1086
        path then prompt the user to select a scheme file. Return
1087
        QDialog.Accepted if the scheme was successfully saved and
1088
        QDialog.Rejected if the user canceled the file selection.
1089
1090
        """
1091
        document = self.current_document()
1092
        curr_scheme = document.scheme()
1093
        path = document.path()
1094
1095
        if path and self.check_can_save(document, path):
1096
            if self.save_scheme_to(curr_scheme, path):
1097
                document.setModified(False)
1098
                self.add_recent_scheme(curr_scheme.title, document.path())
1099
                return QDialog.Accepted
1100
            else:
1101
                return QDialog.Rejected
1102
        else:
1103
            return self.save_scheme_as()
1104
1105
    def save_scheme_as(self):
1106
        """
1107
        Save the current scheme by asking the user for a filename. Return
1108
        `QFileDialog.Accepted` if the scheme was saved successfully and
1109
        `QFileDialog.Rejected` if not.
1110
1111
        """
1112
        document = self.current_document()
1113
        curr_scheme = document.scheme()
1114
        title = curr_scheme.title or "untitled"
1115
1116
        if document.path():
1117
            start_dir = document.path()
1118
        else:
1119
            if self.last_scheme_dir is not None:
1120
                start_dir = self.last_scheme_dir
1121
            else:
1122
                start_dir = QDesktopServices.storageLocation(
1123
                    QDesktopServices.DocumentsLocation
1124
                )
1125
1126
            start_dir = os.path.join(str(start_dir), title + ".ows")
1127
1128
        filename = QFileDialog.getSaveFileName(
1129
            self, self.tr("Save Orange Workflow File"),
1130
            start_dir, self.tr("Orange Workflow (*.ows)")
1131
        )
1132
1133
        if filename:
1134
            if not self.check_can_save(document, filename):
1135
                return QDialog.Rejected
1136
1137
            self.last_scheme_dir = os.path.dirname(filename)
1138
1139
            if self.save_scheme_to(curr_scheme, filename):
1140
                document.setPath(filename)
1141
                document.setModified(False)
1142
                self.add_recent_scheme(curr_scheme.title, document.path())
1143
1144
                return QFileDialog.Accepted
1145
1146
        return QFileDialog.Rejected
1147
1148
    def save_scheme_to(self, scheme, filename):
1149
        """
1150
        Save a Scheme instance `scheme` to `filename`. On success return
1151
        `True`, else show a message to the user explaining the error and
1152
        return `False`.
1153
1154
        """
1155
        dirname, basename = os.path.split(filename)
1156
        self.last_scheme_dir = dirname
1157
        title = scheme.title or "untitled"
1158
1159
        # First write the scheme to a buffer so we don't truncate an
1160
        # existing scheme file if `scheme.save_to` raises an error.
1161
        buffer = BytesIO()
1162
        try:
1163
            scheme.save_to(buffer, pretty=True, pickle_fallback=True)
1164
        except Exception:
0 ignored issues
show
Best Practice introduced by
Catching very general exceptions such as Exception is usually not recommended.

Generally, you would want to handle very specific errors in the exception handler. This ensure that you do not hide other types of errors which should be fixed.

So, unless you specifically plan to handle any error, consider adding a more specific exception.

Loading history...
1165
            log.error("Error saving %r to %r", scheme, filename, exc_info=True)
1166
            message_critical(
1167
                self.tr('An error occurred while trying to save workflow '
1168
                        '"%s" to "%s"') % (title, basename),
1169
                title=self.tr("Error saving %s") % basename,
1170
                exc_info=True,
1171
                parent=self
1172
            )
1173
            return False
1174
1175
        try:
1176
            with open(filename, "wb") as f:
1177
                f.write(buffer.getvalue())
1178
            scheme.set_runtime_env("basedir", os.path.dirname(filename))
1179
            return True
1180
        except (IOError, OSError) as ex:
1181
            log.error("%s saving '%s'", type(ex).__name__, filename,
1182
                      exc_info=True)
1183
            if ex.errno == 2:
1184
                # user might enter a string containing a path separator
1185
                message_warning(
1186
                    self.tr('Workflow "%s" could not be saved. The path does '
1187
                            'not exist') % title,
1188
                    title="",
1189
                    informative_text=self.tr("Choose another location."),
1190
                    parent=self
1191
                )
1192
            elif ex.errno == 13:
1193
                message_warning(
1194
                    self.tr('Workflow "%s" could not be saved. You do not '
1195
                            'have write permissions.') % title,
1196
                    title="",
1197
                    informative_text=self.tr(
1198
                        "Change the file system permissions or choose "
1199
                        "another location."),
1200
                    parent=self
1201
                )
1202
            else:
1203
                message_warning(
1204
                    self.tr('Workflow "%s" could not be saved.') % title,
1205
                    title="",
1206
                    informative_text=ex.strerror,
1207
                    exc_info=True,
1208
                    parent=self
1209
                )
1210
            return False
1211
1212
        except Exception:
0 ignored issues
show
Best Practice introduced by
Catching very general exceptions such as Exception is usually not recommended.

Generally, you would want to handle very specific errors in the exception handler. This ensure that you do not hide other types of errors which should be fixed.

So, unless you specifically plan to handle any error, consider adding a more specific exception.

Loading history...
1213
            log.error("Error saving %r to %r", scheme, filename, exc_info=True)
1214
            message_critical(
1215
                self.tr('An error occurred while trying to save workflow '
1216
                        '"%s" to "%s"') % (title, basename),
1217
                title=self.tr("Error saving %s") % basename,
1218
                exc_info=True,
1219
                parent=self
1220
            )
1221
            return False
1222
1223
    def get_started(self, *args):
0 ignored issues
show
Unused Code introduced by
The argument args seems to be unused.
Loading history...
Coding Style introduced by
This method could be written as a function/class method.

If a method does not access any attributes of the class, it could also be implemented as a function or static method. This can help improve readability. For example

class Foo:
    def some_method(self, x, y):
        return x + y;

could be written as

class Foo:
    @classmethod
    def some_method(cls, x, y):
        return x + y;
Loading history...
1224
        """Show getting started video
1225
        """
1226
        url = QUrl(LINKS["start-using"])
1227
        QDesktopServices.openUrl(url)
1228
1229
    def tutorial(self, *args):
0 ignored issues
show
Unused Code introduced by
The argument args seems to be unused.
Loading history...
Coding Style introduced by
This method could be written as a function/class method.

If a method does not access any attributes of the class, it could also be implemented as a function or static method. This can help improve readability. For example

class Foo:
    def some_method(self, x, y):
        return x + y;

could be written as

class Foo:
    @classmethod
    def some_method(cls, x, y):
        return x + y;
Loading history...
1230
        """Show tutorial.
1231
        """
1232
        url = QUrl(LINKS["tutorial"])
1233
        QDesktopServices.openUrl(url)
1234
1235
    def documentation(self, *args):
0 ignored issues
show
Unused Code introduced by
The argument args seems to be unused.
Loading history...
Coding Style introduced by
This method could be written as a function/class method.

If a method does not access any attributes of the class, it could also be implemented as a function or static method. This can help improve readability. For example

class Foo:
    def some_method(self, x, y):
        return x + y;

could be written as

class Foo:
    @classmethod
    def some_method(cls, x, y):
        return x + y;
Loading history...
1236
        """Show reference documentation.
1237
        """
1238
        url = QUrl(LINKS["tutorial"])
1239
        QDesktopServices.openUrl(url)
1240
1241
    def recent_scheme(self, *args):
0 ignored issues
show
Unused Code introduced by
The argument args seems to be unused.
Loading history...
1242
        """Browse recent schemes. Return QDialog.Rejected if the user
1243
        canceled the operation and QDialog.Accepted otherwise.
1244
1245
        """
1246
        items = [previewmodel.PreviewItem(name=title, path=path)
1247
                 for title, path in self.recent_schemes]
1248
        model = previewmodel.PreviewModel(items=items)
1249
1250
        dialog = previewdialog.PreviewDialog(self)
1251
        title = self.tr("Recent Workflows")
1252
        dialog.setWindowTitle(title)
1253
        template = ('<h3 style="font-size: 26px">\n'
1254
                    #'<img height="26" src="canvas_icons:Recent.svg">\n'
1255
                    '{0}\n'
1256
                    '</h3>')
1257
        dialog.setHeading(template.format(title))
1258
        dialog.setModel(model)
1259
1260
        model.delayedScanUpdate()
1261
1262
        status = dialog.exec_()
1263
1264
        index = dialog.currentIndex()
0 ignored issues
show
Comprehensibility Bug introduced by
index is re-defining a name which is already available in the outer-scope (previously defined on line 1958).

It is generally a bad practice to shadow variables from the outer-scope. In most cases, this is done unintentionally and might lead to unexpected behavior:

param = 5

class Foo:
    def __init__(self, param):   # "param" would be flagged here
        self.param = param
Loading history...
1265
1266
        dialog.deleteLater()
1267
        model.deleteLater()
1268
1269
        if status == QDialog.Accepted:
1270
            doc = self.current_document()
1271
            if doc.isModifiedStrict():
1272
                if self.ask_save_changes() == QDialog.Rejected:
1273
                    return QDialog.Rejected
1274
1275
            selected = model.item(index)
1276
1277
            self.load_scheme(str(selected.path()))
1278
1279
        return status
1280
1281
    def tutorial_scheme(self, *args):
0 ignored issues
show
Unused Code introduced by
The argument args seems to be unused.
Loading history...
1282
        """Browse a collection of tutorial schemes. Returns QDialog.Rejected
1283
        if the user canceled the dialog else loads the selected scheme into
1284
        the canvas and returns QDialog.Accepted.
1285
1286
        """
1287
        tutors = tutorials.tutorials()
1288
        items = [previewmodel.PreviewItem(path=t.abspath()) for t in tutors]
1289
        model = previewmodel.PreviewModel(items=items)
1290
        dialog = previewdialog.PreviewDialog(self)
1291
        title = self.tr("Tutorials")
1292
        dialog.setWindowTitle(title)
1293
        template = ('<h3 style="font-size: 26px">\n'
1294
                    #'<img height="26" src="canvas_icons:Tutorials.svg">\n'
1295
                    '{0}\n'
1296
                    '</h3>')
1297
1298
        dialog.setHeading(template.format(title))
1299
        dialog.setModel(model)
1300
1301
        model.delayedScanUpdate()
1302
        status = dialog.exec_()
1303
        index = dialog.currentIndex()
0 ignored issues
show
Comprehensibility Bug introduced by
index is re-defining a name which is already available in the outer-scope (previously defined on line 1958).

It is generally a bad practice to shadow variables from the outer-scope. In most cases, this is done unintentionally and might lead to unexpected behavior:

param = 5

class Foo:
    def __init__(self, param):   # "param" would be flagged here
        self.param = param
Loading history...
1304
1305
        dialog.deleteLater()
1306
1307
        if status == QDialog.Accepted:
1308
            doc = self.current_document()
1309
            if doc.isModifiedStrict():
1310
                if self.ask_save_changes() == QDialog.Rejected:
1311
                    return QDialog.Rejected
1312
1313
            selected = model.item(index)
1314
1315
            new_scheme = self.new_scheme_from(str(selected.path()))
1316
            if new_scheme is not None:
1317
                self.set_new_scheme(new_scheme)
1318
1319
        return status
1320
1321
    def welcome_dialog(self):
1322
        """Show a modal welcome dialog for Orange Canvas.
1323
        """
1324
1325
        dialog = welcomedialog.WelcomeDialog(self)
1326
        dialog.setWindowTitle(self.tr("Welcome to Orange Data Mining"))
1327
1328
        def new_scheme():
1329
            if self.new_scheme() == QDialog.Accepted:
1330
                dialog.accept()
1331
1332
        def open_scheme():
1333
            if self.open_scheme() == QDialog.Accepted:
1334
                dialog.accept()
1335
1336
        def open_recent():
1337
            if self.recent_scheme() == QDialog.Accepted:
1338
                dialog.accept()
1339
1340
        def tutorial():
1341
            if self.tutorial_scheme() == QDialog.Accepted:
1342
                dialog.accept()
1343
1344
        new_action = \
1345
            QAction(self.tr("New"), dialog,
1346
                    toolTip=self.tr("Open a new workflow."),
1347
                    triggered=new_scheme,
1348
                    shortcut=QKeySequence.New,
1349
                    icon=canvas_icons("New.svg")
1350
                    )
1351
1352
        open_action = \
1353
            QAction(self.tr("Open"), dialog,
1354
                    objectName="welcome-action-open",
1355
                    toolTip=self.tr("Open a workflow."),
1356
                    triggered=open_scheme,
1357
                    shortcut=QKeySequence.Open,
1358
                    icon=canvas_icons("Open.svg")
1359
                    )
1360
1361
        recent_action = \
1362
            QAction(self.tr("Recent"), dialog,
1363
                    objectName="welcome-recent-action",
1364
                    toolTip=self.tr("Browse and open a recent workflow."),
1365
                    triggered=open_recent,
1366
                    shortcut=QKeySequence(Qt.ControlModifier | \
1367
                                          (Qt.ShiftModifier | Qt.Key_R)),
1368
                    icon=canvas_icons("Recent.svg")
1369
                    )
1370
1371
        tutorials_action = \
1372
            QAction(self.tr("Tutorial"), dialog,
1373
                    objectName="welcome-tutorial-action",
1374
                    toolTip=self.tr("Browse tutorial workflows."),
1375
                    triggered=tutorial,
1376
                    icon=canvas_icons("Tutorials.svg")
1377
                    )
1378
1379
        bottom_row = [self.get_started_action, tutorials_action,
1380
                      self.documentation_action]
1381
1382
        self.new_action.triggered.connect(dialog.accept)
1383
        top_row = [new_action, open_action, recent_action]
1384
1385
        dialog.addRow(top_row, background="light-grass")
1386
        dialog.addRow(bottom_row, background="light-orange")
1387
1388
        settings = QSettings()
1389
1390
        dialog.setShowAtStartup(
1391
            settings.value("startup/show-welcome-screen", True, type=bool)
1392
        )
1393
1394
        status = dialog.exec_()
1395
1396
        settings.setValue("startup/show-welcome-screen",
1397
                          dialog.showAtStartup())
1398
1399
        dialog.deleteLater()
1400
1401
        return status
1402
1403
    def scheme_properties_dialog(self):
1404
        """Return an empty `SchemeInfo` dialog instance.
1405
        """
1406
        settings = QSettings()
1407
        value_key = "schemeinfo/show-at-new-scheme"
1408
1409
        dialog = SchemeInfoDialog(self)
1410
1411
        dialog.setWindowTitle(self.tr("Workflow Info"))
1412
        dialog.setFixedSize(725, 450)
1413
1414
        dialog.setShowAtNewScheme(
1415
            settings.value(value_key, True, type=bool)
1416
        )
1417
1418
        return dialog
1419
1420
    def show_scheme_properties(self):
1421
        """Show current scheme properties.
1422
        """
1423
        settings = QSettings()
1424
        value_key = "schemeinfo/show-at-new-scheme"
1425
1426
        current_doc = self.current_document()
1427
        scheme = current_doc.scheme()
1428
        dlg = self.scheme_properties_dialog()
1429
        dlg.setAutoCommit(False)
1430
        dlg.setScheme(scheme)
1431
        status = dlg.exec_()
1432
1433
        if status == QDialog.Accepted:
1434
            editor = dlg.editor
1435
            stack = current_doc.undoStack()
1436
            stack.beginMacro(self.tr("Change Info"))
1437
            current_doc.setTitle(editor.title())
1438
            current_doc.setDescription(editor.description())
1439
            stack.endMacro()
1440
1441
            # Store the check state.
1442
            settings.setValue(value_key, dlg.showAtNewScheme())
1443
        return status
1444
1445
    def show_scheme_properties_for(self, scheme, window_title=None):
1446
        """Show scheme properties for `scheme` with `window_title (if None
1447
        a default 'Scheme Info' title will be used.
1448
1449
        """
1450
        settings = QSettings()
1451
        value_key = "schemeinfo/show-at-new-scheme"
1452
1453
        dialog = self.scheme_properties_dialog()
1454
1455
        if window_title is not None:
1456
            dialog.setWindowTitle(window_title)
1457
1458
        dialog.setScheme(scheme)
1459
1460
        status = dialog.exec_()
1461
        if status == QDialog.Accepted:
1462
            # Store the check state.
1463
            settings.setValue(value_key, dialog.showAtNewScheme())
1464
1465
        dialog.deleteLater()
1466
1467
        return status
1468
1469
    def set_signal_freeze(self, freeze):
1470
        scheme = self.current_document().scheme()
1471
        manager = scheme.signal_manager
1472
        if freeze:
1473
            manager.pause()
1474
        else:
1475
            manager.resume()
1476
1477
    def remove_selected(self):
1478
        """Remove current scheme selection.
1479
        """
1480
        self.current_document().removeSelected()
1481
1482
    def quit(self):
1483
        """Quit the application.
1484
        """
1485
        if QApplication.activePopupWidget():
1486
            # On OSX the actions in the global menu bar are triggered
1487
            # even if an popup widget is running it's own event loop
1488
            # (in exec_)
1489
            log.debug("Ignoring a quit shortcut during an active "
1490
                      "popup dialog.")
1491
        else:
1492
            self.close()
1493
1494
    def select_all(self):
1495
        self.current_document().selectAll()
1496
1497
    def open_widget(self):
1498
        """Open/raise selected widget's GUI.
1499
        """
1500
        self.current_document().openSelected()
1501
1502
    def rename_widget(self):
1503
        """Rename the current focused widget.
1504
        """
1505
        doc = self.current_document()
1506
        nodes = doc.selectedNodes()
1507
        if len(nodes) == 1:
1508
            doc.editNodeTitle(nodes[0])
1509
1510
    def open_canvas_settings(self):
1511
        """Open canvas settings/preferences dialog
1512
        """
1513
        dlg = UserSettingsDialog(self)
1514
        dlg.setWindowTitle(self.tr("Preferences"))
1515
        dlg.show()
1516
        status = dlg.exec_()
1517
        if status == 0:
1518
            self.__update_from_settings()
1519
1520
    def open_addons(self):
1521
        from .addons import AddonManagerDialog
1522
        dlg = AddonManagerDialog(self, windowTitle=self.tr("Add-ons"))
1523
        dlg.setAttribute(Qt.WA_DeleteOnClose)
1524
        return dlg.exec_()
1525
1526
    def reset_widget_settings(self):
1527
        res = message_question(
1528
            "Clear all widget settings on next restart",
1529
            title="Clear settings",
1530
            informative_text=(
1531
                "A restart of the application is necessary " +
1532
                "for the changes to take effect"),
1533
            buttons=QMessageBox.Ok | QMessageBox.Cancel,
1534
            default_button=QMessageBox.Ok,
1535
            parent=self
1536
        )
1537
        if res == QMessageBox.Ok:
1538
            # Touch a finely crafted file inside the settings directory.
1539
            # The existence of this file is checked by the canvas main
1540
            # function and is deleted there.
1541
            fname = os.path.join(config.widget_settings_dir(),
1542
                                 "DELETE_ON_START")
1543
            os.makedirs(config.widget_settings_dir(), exist_ok=True)
1544
            with open(fname, "a"):
1545
                pass
1546
1547
            if not self.close():
1548
                message_information(
1549
                    "Settings will still be reset at next application start",
1550
                    parent=self)
1551
1552
    def show_output_view(self):
1553
        """Show a window with application output.
1554
        """
1555
        self.output_dock.show()
1556
1557
    def show_report_view(self):
1558
        doc = self.current_document()
1559
        scheme = doc.scheme()
1560
        scheme.show_report_view()
1561
1562
    def output_view(self):
1563
        """Return the output text widget.
1564
        """
1565
        return self.output_dock.widget()
1566
1567
    def open_about(self):
1568
        """Open the about dialog.
1569
        """
1570
        dlg = AboutDialog(self)
1571
        dlg.setAttribute(Qt.WA_DeleteOnClose)
1572
        dlg.exec_()
1573
1574
    def add_recent_scheme(self, title, path):
1575
        """Add an entry (`title`, `path`) to the list of recent schemes.
1576
        """
1577
        if not path:
1578
            # No associated persistent path so we can't do anything.
1579
            return
1580
1581
        if not title:
1582
            title = os.path.basename(path)
1583
1584
        filename = os.path.abspath(os.path.realpath(path))
1585
        filename = os.path.normpath(filename)
1586
1587
        actions_by_filename = {}
1588
        for action in self.recent_scheme_action_group.actions():
1589
            path = str(action.data())
1590
            actions_by_filename[path] = action
1591
1592
        if filename in actions_by_filename:
1593
            # Remove the title/filename (so it can be reinserted)
1594
            recent_index = index(self.recent_schemes, filename,
1595
                                 key=operator.itemgetter(1))
1596
            self.recent_schemes.pop(recent_index)
1597
1598
            action = actions_by_filename[filename]
1599
            self.recent_menu.removeAction(action)
1600
            self.recent_scheme_action_group.removeAction(action)
1601
            action.setText(title or self.tr("untitled"))
1602
        else:
1603
            action = QAction(title or self.tr("untitled"), self,
1604
                             toolTip=filename)
1605
            action.setData(filename)
1606
1607
        # Find the separator action in the menu (after 'Browse Recent')
1608
        recent_actions = self.recent_menu.actions()
1609
        begin_index = index(recent_actions, self.recent_menu_begin)
1610
        action_before = recent_actions[begin_index + 1]
1611
1612
        self.recent_menu.insertAction(action_before, action)
1613
        self.recent_scheme_action_group.addAction(action)
1614
        self.recent_schemes.insert(0, (title, filename))
1615
1616
        if len(self.recent_schemes) > max(self.num_recent_schemes, 1):
1617
            title, filename = self.recent_schemes.pop(-1)
1618
            action = actions_by_filename[filename]
1619
            self.recent_menu.removeAction(action)
1620
            self.recent_scheme_action_group.removeAction(action)
1621
1622
        config.save_recent_scheme_list(self.recent_schemes)
1623
1624
    def clear_recent_schemes(self):
1625
        """Clear list of recent schemes
1626
        """
1627
        actions = list(self.recent_menu.actions())
1628
1629
        # Exclude permanent actions (Browse Recent, separators, Clear List)
1630
        actions_to_remove = [action for action in actions \
1631
                             if str(action.data())]
1632
1633
        for action in actions_to_remove:
1634
            self.recent_menu.removeAction(action)
1635
            self.recent_scheme_action_group.removeAction(action)
1636
1637
        self.recent_schemes = []
1638
        config.save_recent_scheme_list([])
1639
1640
    def _on_recent_scheme_action(self, action):
1641
        """A recent scheme action was triggered by the user
1642
        """
1643
        document = self.current_document()
1644
        if document.isModifiedStrict():
1645
            if self.ask_save_changes() == QDialog.Rejected:
1646
                return
1647
1648
        filename = str(action.data())
1649
        self.load_scheme(filename)
1650
1651
    def _on_dock_location_changed(self, location):
0 ignored issues
show
Unused Code introduced by
The argument location seems to be unused.
Loading history...
1652
        """Location of the dock_widget has changed, fix the margins
1653
        if necessary.
1654
1655
        """
1656
        self.__update_scheme_margins()
1657
1658
    def set_tool_dock_expanded(self, expanded):
1659
        """
1660
        Set the dock widget expanded state.
1661
        """
1662
        self.dock_widget.setExpanded(expanded)
1663
1664
    def _on_tool_dock_expanded(self, expanded):
1665
        """
1666
        'dock_widget' widget was expanded/collapsed.
1667
        """
1668
        if expanded != self.toggle_tool_dock_expand.isChecked():
1669
            self.toggle_tool_dock_expand.setChecked(expanded)
1670
1671
    def createPopupMenu(self):
0 ignored issues
show
Coding Style introduced by
This method could be written as a function/class method.

If a method does not access any attributes of the class, it could also be implemented as a function or static method. This can help improve readability. For example

class Foo:
    def some_method(self, x, y):
        return x + y;

could be written as

class Foo:
    @classmethod
    def some_method(cls, x, y):
        return x + y;
Loading history...
1672
        # Override the default context menu popup (we don't want the user to
1673
        # be able to hide the tool dock widget).
1674
        return None
1675
1676
    def closeEvent(self, event):
1677
        """Close the main window.
1678
        """
1679
        document = self.current_document()
1680
        if document.isModifiedStrict():
1681
            if self.ask_save_changes() == QDialog.Rejected:
1682
                # Reject the event
1683
                event.ignore()
1684
                return
1685
1686
        old_scheme = document.scheme()
1687
1688
        # Set an empty scheme to clear the document
1689
        document.setScheme(widgetsscheme.WidgetsScheme())
1690
1691
        QApplication.sendEvent(old_scheme, QEvent(QEvent.Close))
1692
1693
        old_scheme.deleteLater()
1694
1695
        config.save_config()
1696
1697
        geometry = self.saveGeometry()
1698
        state = self.saveState(version=self.SETTINGS_VERSION)
1699
        settings = QSettings()
1700
        settings.beginGroup("mainwindow")
1701
        settings.setValue("geometry", geometry)
1702
        settings.setValue("state", state)
1703
        settings.setValue("canvasdock/expanded",
1704
                          self.dock_widget.expanded())
1705
        settings.setValue("scheme-margins-enabled",
1706
                          self.scheme_margins_enabled)
1707
1708
        settings.setValue("last-scheme-dir", self.last_scheme_dir)
1709
        settings.setValue("widgettoolbox/state",
1710
                          self.widgets_tool_box.saveState())
1711
1712
        settings.setValue("quick-help/visible",
1713
                          self.canvas_tool_dock.quickHelpVisible())
1714
1715
        settings.endGroup()
1716
1717
        event.accept()
1718
1719
        # Close any windows left.
1720
        application = QApplication.instance()
1721
        QTimer.singleShot(0, application.closeAllWindows)
1722
1723
    def showEvent(self, event):
1724
        if self.__first_show:
1725
            settings = QSettings()
1726
            settings.beginGroup("mainwindow")
1727
1728
            # Restore geometry and dock/toolbar state
1729
            state = settings.value("state", QByteArray(), type=QByteArray)
1730
            if state:
1731
                self.restoreState(state, version=self.SETTINGS_VERSION)
1732
1733
            geom_data = settings.value("geometry", QByteArray(),
1734
                                       type=QByteArray)
1735
            if geom_data:
1736
                self.restoreGeometry(geom_data)
1737
1738
            self.__first_show = False
1739
1740
        return QMainWindow.showEvent(self, event)
1741
1742
    def event(self, event):
1743
        if event.type() == QEvent.StatusTip and \
1744
                isinstance(event, QuickHelpTipEvent):
1745
            # Using singleShot to update the text browser.
1746
            # If updating directly the application experiences strange random
1747
            # segfaults (in ~StatusTipEvent in QTextLayout or event just normal
1748
            # event loop), but only when the contents are larger then the
1749
            # QTextBrowser's viewport.
1750
            if event.priority() == QuickHelpTipEvent.Normal:
1751
                QTimer.singleShot(0, partial(self.dock_help.showHelp,
1752
                                             event.html()))
1753
            elif event.priority() == QuickHelpTipEvent.Temporary:
1754
                QTimer.singleShot(0, partial(self.dock_help.showHelp,
1755
                                             event.html(), event.timeout()))
1756
            elif event.priority() == QuickHelpTipEvent.Permanent:
1757
                QTimer.singleShot(0, partial(self.dock_help.showPermanentHelp,
1758
                                             event.html()))
1759
1760
            return True
1761
1762
        elif event.type() == QEvent.WhatsThisClicked:
1763
            ref = event.href()
1764
            url = QUrl(ref)
1765
1766
            if url.scheme() == "help" and url.authority() == "search":
1767
                try:
1768
                    url = self.help.search(url)
1769
                except KeyError:
1770
                    url = None
1771
                    log.info("No help topic found for %r", url)
1772
1773
            if url:
1774
                self.show_help(url)
1775
            else:
1776
                message_information(
1777
                    self.tr("There is no documentation for this widget yet."),
1778
                    parent=self)
1779
1780
            return True
1781
1782
        return QMainWindow.event(self, event)
1783
1784
    def show_help(self, url):
1785
        """
1786
        Show `url` in a help window.
1787
        """
1788
        log.info("Setting help to url: %r", url)
1789
        if self.open_in_external_browser:
1790
            url = QUrl(url)
1791
            if not QDesktopServices.openUrl(url):
1792
                # Try fixing some common problems.
1793
                url = QUrl.fromUserInput(url.toString())
1794
                # 'fromUserInput' includes possible fragment into the path
1795
                # (which prevents it to open local files) so we reparse it
1796
                # again.
1797
                url = QUrl(url.toString())
1798
                QDesktopServices.openUrl(url)
1799
        else:
1800
            self.help_view.load(QUrl(url))
1801
            self.help_dock.show()
1802
            self.help_dock.raise_()
1803
1804
    # Mac OS X
1805
    if sys.platform == "darwin":
1806
        def toggleMaximized(self):
1807
            """Toggle normal/maximized window state.
1808
            """
1809
            if self.isMinimized():
1810
                # Do nothing if window is minimized
1811
                return
1812
1813
            if self.isMaximized():
1814
                self.showNormal()
1815
            else:
1816
                self.showMaximized()
1817
1818
        def changeEvent(self, event):
1819
            if event.type() == QEvent.WindowStateChange:
1820
                # Can get 'Qt.WindowNoState' before the widget is fully
1821
                # initialized
1822
                if hasattr(self, "window_state"):
1823
                    # Enable/disable window menu based on minimized state
1824
                    self.window_menu.setEnabled(not self.isMinimized())
1825
1826
            QMainWindow.changeEvent(self, event)
1827
1828
    def sizeHint(self):
1829
        """
1830
        Reimplemented from QMainWindow.sizeHint
1831
        """
1832
        hint = QMainWindow.sizeHint(self)
1833
        return hint.expandedTo(QSize(1024, 720))
1834
1835
    def tr(self, sourceText, disambiguation=None, n=-1):
1836
        """Translate the string.
1837
        """
1838
        return str(QMainWindow.tr(self, sourceText, disambiguation, n))
1839
1840
    def __update_from_settings(self):
1841
        settings = QSettings()
1842
        settings.beginGroup("mainwindow")
1843
        toolbox_floatable = settings.value("toolbox-dock-floatable",
1844
                                           defaultValue=False,
1845
                                           type=bool)
1846
1847
        features = self.dock_widget.features()
1848
        features = updated_flags(features, QDockWidget.DockWidgetFloatable,
1849
                                 toolbox_floatable)
1850
        self.dock_widget.setFeatures(features)
1851
1852
        toolbox_exclusive = settings.value("toolbox-dock-exclusive",
1853
                                           defaultValue=True,
1854
                                           type=bool)
1855
        self.widgets_tool_box.setExclusive(toolbox_exclusive)
1856
1857
        self.num_recent_schemes = settings.value("num-recent-schemes",
1858
                                                 defaultValue=15,
1859
                                                 type=int)
1860
1861
        settings.endGroup()
1862
        settings.beginGroup("quickmenu")
1863
1864
        triggers = 0
1865
        dbl_click = settings.value("trigger-on-double-click",
1866
                                   defaultValue=True,
1867
                                   type=bool)
1868
        if dbl_click:
1869
            triggers |= SchemeEditWidget.DoubleClicked
1870
1871
        right_click = settings.value("trigger-on-right-click",
1872
                                    defaultValue=True,
1873
                                    type=bool)
1874
        if right_click:
1875
            triggers |= SchemeEditWidget.RightClicked
1876
1877
        space_press = settings.value("trigger-on-space-key",
1878
                                     defaultValue=True,
1879
                                     type=bool)
1880
        if space_press:
1881
            triggers |= SchemeEditWidget.SpaceKey
1882
1883
        any_press = settings.value("trigger-on-any-key",
1884
                                   defaultValue=False,
1885
                                   type=bool)
1886
        if any_press:
1887
            triggers |= SchemeEditWidget.AnyKey
1888
1889
        self.scheme_widget.setQuickMenuTriggers(triggers)
1890
1891
        settings.endGroup()
1892
        settings.beginGroup("schemeedit")
1893
        show_channel_names = settings.value("show-channel-names",
1894
                                            defaultValue=True,
1895
                                            type=bool)
1896
        self.scheme_widget.setChannelNamesVisible(show_channel_names)
1897
1898
        node_animations = settings.value("enable-node-animations",
1899
                                         defaultValue=False,
1900
                                         type=bool)
1901
        self.scheme_widget.setNodeAnimationEnabled(node_animations)
1902
        settings.endGroup()
1903
1904
        settings.beginGroup("output")
1905
        stay_on_top = settings.value("stay-on-top", defaultValue=True,
1906
                                     type=bool)
1907
        if stay_on_top:
1908
            self.output_dock.setFloatingWindowFlags(Qt.Tool)
1909
        else:
1910
            self.output_dock.setFloatingWindowFlags(Qt.Window)
1911
1912
        dockable = settings.value("dockable", defaultValue=True,
1913
                                  type=bool)
1914
        if dockable:
1915
            self.output_dock.setAllowedAreas(Qt.BottomDockWidgetArea)
1916
        else:
1917
            self.output_dock.setAllowedAreas(Qt.NoDockWidgetArea)
1918
1919
        settings.endGroup()
1920
1921
        settings.beginGroup("help")
1922
        stay_on_top = settings.value("stay-on-top", defaultValue=True,
1923
                                     type=bool)
1924
        if stay_on_top:
1925
            self.help_dock.setFloatingWindowFlags(Qt.Tool)
1926
        else:
1927
            self.help_dock.setFloatingWindowFlags(Qt.Window)
1928
1929
        dockable = settings.value("dockable", defaultValue=False,
1930
                                  type=bool)
1931
        if dockable:
1932
            self.help_dock.setAllowedAreas(Qt.LeftDockWidgetArea | \
1933
                                           Qt.RightDockWidgetArea)
1934
        else:
1935
            self.help_dock.setAllowedAreas(Qt.NoDockWidgetArea)
1936
1937
        self.open_in_external_browser = \
1938
            settings.value("open-in-external-browser", defaultValue=False,
1939
                           type=bool)
1940
1941
        self.use_popover = \
0 ignored issues
show
Coding Style introduced by
The attribute use_popover was defined outside __init__.

It is generally a good practice to initialize all attributes to default values in the __init__ method:

class Foo:
    def __init__(self, x=None):
        self.x = x
Loading history...
1942
            settings.value("toolbox-dock-use-popover-menu", defaultValue=True,
1943
                           type=bool)
1944
1945
1946
def updated_flags(flags, mask, state):
1947
    if state:
1948
        flags |= mask
1949
    else:
1950
        flags &= ~mask
1951
    return flags
1952
1953
1954
def identity(item):
1955
    return item
1956
1957
1958
def index(sequence, *what, **kwargs):
1959
    """index(sequence, what, [key=None, [predicate=None]])
1960
1961
    Return index of `what` in `sequence`.
1962
1963
    """
1964
    what = what[0]
1965
    key = kwargs.get("key", identity)
1966
    predicate = kwargs.get("predicate", operator.eq)
1967
    for i, item in enumerate(sequence):
1968
        item_key = key(item)
1969
        if predicate(what, item_key):
1970
            return i
1971
    raise ValueError("%r not in sequence" % what)
1972