Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ENH] Remote reporting of unexpected errors #1558

Merged
merged 5 commits into from
Sep 23, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 27 additions & 53 deletions Orange/canvas/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import pickle
import shlex
import shutil
from unittest.mock import patch

import pkg_resources

Expand All @@ -23,11 +24,11 @@
from Orange.canvas.application.application import CanvasApplication
from Orange.canvas.application.canvasmain import CanvasMainWindow
from Orange.canvas.application.outputview import TextStream, ExceptHook
from Orange.canvas.application.errorreporting import ErrorReporting

from Orange.canvas.gui.splashscreen import SplashScreen
from Orange.canvas.config import cache_dir
from Orange.canvas import config
from Orange.canvas.utils.redirect import redirect_stdout, redirect_stderr
from Orange.canvas.utils.qtcompat import QSettings

from Orange.canvas.registry import qt
Expand All @@ -37,14 +38,6 @@
log = logging.getLogger(__name__)


def running_in_ipython():
try:
__IPYTHON__
return True
except NameError:
return False


# Allow termination with CTRL + C
import signal
signal.signal(signal.SIGINT, signal.SIG_DFL)
Expand Down Expand Up @@ -118,9 +111,6 @@ def main(argv=None):
parser.add_option("-l", "--log-level",
help="Logging level (0, 1, 2, 3, 4)",
type="int", default=1)
parser.add_option("--no-redirect",
action="store_true",
help="Do not redirect stdout/err to canvas output view.")
parser.add_option("--style",
help="QStyle to use",
type="str", default=None)
Expand Down Expand Up @@ -335,42 +325,6 @@ def show_message(message):
open_requests[-1])
canvas_window.load_scheme(open_requests[-1].toLocalFile())

stdout_redirect = \
settings.value("output/redirect-stdout", True, type=bool)

stderr_redirect = \
settings.value("output/redirect-stderr", True, type=bool)

# cmd line option overrides settings / no redirect is possible
# under ipython
if options.no_redirect or running_in_ipython():
stderr_redirect = stdout_redirect = False

output_view = canvas_window.output_view()

if stdout_redirect:
stdout = TextStream()
stdout.stream.connect(output_view.write)
if sys.stdout is not None:
# also connect to original fd
stdout.stream.connect(sys.stdout.write)
else:
stdout = sys.stdout

if stderr_redirect:
error_writer = output_view.formated(color=Qt.red)
stderr = TextStream()
stderr.stream.connect(error_writer.write)
if sys.stderr is not None:
# also connect to original fd
stderr.stream.connect(sys.stderr.write)
else:
stderr = sys.stderr

if stderr_redirect:
sys.excepthook = ExceptHook()
sys.excepthook.handledException.connect(output_view.parent().show)

# If run for the first time, open a browser tab with a survey
show_survey = settings.value("startup/show-survey", True, type=bool)
if show_survey:
Expand All @@ -395,12 +349,32 @@ def handle_response(result):
question.finished.connect(handle_response)
question.show()

with redirect_stdout(stdout), redirect_stderr(stderr):
log.info("Entering main event loop.")
try:
# Tee stdout and stderr into Output dock
log_view = canvas_window.log_view()

stdout = TextStream()
stdout.stream.connect(log_view.write)
if sys.stdout:
stdout.stream.connect(sys.stdout.write)
stdout.flushed.connect(sys.stdout.flush)

stderr = TextStream()
error_writer = log_view.formated(color=Qt.red)
stderr.stream.connect(error_writer.write)
if sys.stderr:
stderr.stream.connect(sys.stderr.write)
stderr.flushed.connect(sys.stderr.flush)

log.info("Entering main event loop.")
try:
with patch('sys.excepthook',
ExceptHook(stream=stderr, canvas=canvas_window,
handledException=ErrorReporting.handle_exception)),\
patch('sys.stderr', stderr),\
patch('sys.stdout', stdout):
status = app.exec_()
except BaseException:
log.error("Error in main event loop.", exc_info=True)
except BaseException:
log.error("Error in main event loop.", exc_info=True)

canvas_window.deleteLater()
app.processEvents()
Expand Down
177 changes: 54 additions & 123 deletions Orange/canvas/application/canvasmain.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
QMainWindow, QWidget, QAction, QActionGroup, QMenu, QMenuBar, QDialog,
QFileDialog, QMessageBox, QVBoxLayout, QSizePolicy, QColor, QKeySequence,
QIcon, QToolBar, QToolButton, QDockWidget, QDesktopServices, QApplication,
QShortcut
)

from PyQt4.QtCore import (
Expand Down Expand Up @@ -108,75 +109,38 @@ def paintEvent(self, event):
pass


class DockableWindow(QDockWidget):
def __init__(self, *args, **kwargs):
QDockWidget.__init__(self, *args, **kwargs)

# Fist show after floating
self.__firstShow = True
# Flags to use while floating
self.__windowFlags = Qt.Window
self.setWindowFlags(self.__windowFlags)
self.topLevelChanged.connect(self.__on_topLevelChanged)
self.visibilityChanged.connect(self.__on_visbilityChanged)

self.__closeAction = QAction(self.tr("Close"), self,
shortcut=QKeySequence.Close,
triggered=self.close,
enabled=self.isFloating())
self.topLevelChanged.connect(self.__closeAction.setEnabled)
self.addAction(self.__closeAction)

def setFloatingWindowFlags(self, flags):
"""
Set `windowFlags` to use while the widget is floating (undocked).
"""
if self.__windowFlags != flags:
self.__windowFlags = flags
if self.isFloating():
self.__fixWindowFlags()

def paintEvent(self, event):
# HACK: Preventing calls to QDockWidget.paintEvent() seems to fix the
#
# QPainter::begin: Paint device returned engine == 0, type: 3
# QPainter...
#
# wall-of-text error that filled the buffers of our consoles.
#
# Without understanding reasons or implications, but with
# no immediate apparent negative side effects:
pass

def floatingWindowFlags(self):
"""
Return the `windowFlags` used when the widget is floating.
"""
return self.__windowFlags
try:
QKeySequence.Cancel
except AttributeError: # < Qt 5.?
QKeySequence.Cancel = QKeySequence(Qt.Key_Escape)

def __fixWindowFlags(self):
if self.isFloating():
update_window_flags(self, self.__windowFlags)

def __on_topLevelChanged(self, floating):
if floating:
self.__firstShow = True
self.__fixWindowFlags()

def __on_visbilityChanged(self, visible):
if visible and self.isFloating() and self.__firstShow:
self.__firstShow = False
self.__fixWindowFlags()

class DockWidget(QDockWidget):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The old name signified that this is a windows that can be docked. The new one suggests that extending classes can only be used as dock components.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's correct. Docks that can be docked or undocked but they aren't windows in themselves.

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

def update_window_flags(widget, flags):
currflags = widget.windowFlags()
if int(flags) != int(currflags):
hidden = widget.isHidden()
widget.setWindowFlags(flags)
# setting the flags hides the widget
if not hidden:
widget.show()
settings_group = kwargs.get('objectName')
if settings_group:
settings = QSettings()
self.dockLocationChanged.connect(
lambda area: settings.setValue(settings_group + '/area', area))
self.visibilityChanged.connect(
lambda visible: settings.setValue(settings_group + '/is-visible', visible))
self.topLevelChanged.connect(
lambda floating: settings.setValue(settings_group + '/is-floating', floating))
self.setVisible(
settings.value(settings_group + '/is-visible', False, type=bool))
self.setFloating(
settings.value(settings_group + '/is-floating', False, type=bool))

def _close():
focused = QApplication.focusWidget()
if any(dock_widget is focused
for dock_widget in self.findChildren(type(focused))):
self.close()

for key in (QKeySequence.Close, QKeySequence.Cancel):
QShortcut(key, self, _close, _close)


class CanvasMainWindow(QMainWindow):
Expand Down Expand Up @@ -358,18 +322,18 @@ def setup_ui(self):
self._on_dock_location_changed
)

self.output_dock = DockableWindow(self.tr("Output"), self,
objectName="output-dock")
self.output_dock.setAllowedAreas(Qt.BottomDockWidgetArea)
output_view = OutputView()
self.log_dock = DockWidget(self.tr("Log"), self,
objectName="log-dock",
allowedAreas=Qt.BottomDockWidgetArea)
# Set widget before calling addDockWidget, otherwise the dock
# does not resize properly on first undock
self.output_dock.setWidget(output_view)
self.output_dock.hide()
self.log_dock.setWidget(OutputView())
self.addDockWidget(Qt.BottomDockWidgetArea, self.log_dock)

self.help_dock = DockableWindow(self.tr("Help"), self,
objectName="help-dock")
self.help_dock.setAllowedAreas(Qt.NoDockWidgetArea)
self.help_dock = DockWidget(self.tr("Help"), self,
objectName="help-dock",
allowedAreas=Qt.RightDockWidgetArea |
Qt.BottomDockWidgetArea)
self.help_view = QWebView()
manager = self.help_view.page().networkAccessManager()
cache = QNetworkDiskCache()
Expand All @@ -378,7 +342,9 @@ def setup_ui(self):
)
manager.setCache(cache)
self.help_dock.setWidget(self.help_view)
self.help_dock.hide()
self.addDockWidget(
QSettings().value('help-dock/area', Qt.RightDockWidgetArea, type=int),
self.help_dock)

self.setMinimumSize(600, 500)

Expand Down Expand Up @@ -538,14 +504,15 @@ def setup_actions(self):
triggered=self.open_addons,
)

self.show_output_action = \
QAction(self.tr("Show Output View"), self,
toolTip=self.tr("Show application output."),
triggered=self.show_output_view,
self.show_log_action = \
QAction(self.tr("&Log"), self,
toolTip=self.tr("Show application standard output."),
checkable=True,
triggered=lambda checked: self.log_dock.setVisible(checked),
)

self.show_report_action = \
QAction(self.tr("Show Report View"), self,
QAction(self.tr("&Report"), self,
triggered=self.show_report_view,
shortcut=QKeySequence(Qt.ShiftModifier | Qt.Key_R)
)
Expand Down Expand Up @@ -657,15 +624,15 @@ def setup_menu(self):
QActionGroup(self, objectName="toolbox-menu-group")

self.view_menu.addAction(self.toggle_tool_dock_expand)
self.view_menu.addAction(self.show_log_action)
self.view_menu.addAction(self.show_report_action)

self.view_menu.addSeparator()
self.view_menu.addAction(self.toogle_margins_action)
menu_bar.addMenu(self.view_menu)

# Options menu
self.options_menu = QMenu(self.tr("&Options"), self)
self.options_menu.addAction(self.show_output_action)
self.options_menu.addAction(self.show_report_action)
# self.options_menu.addAction("Add-ons")
# self.options_menu.addAction("Developers")
# self.options_menu.addAction("Run Discovery")
Expand Down Expand Up @@ -721,6 +688,8 @@ def restore(self):
self.toogle_margins_action.setChecked(
settings.value("scheme-margins-enabled", False, type=bool)
)
self.show_log_action.setChecked(
settings.value("output-dock/is-visible", False, type=bool))

default_dir = QDesktopServices.storageLocation(
QDesktopServices.DocumentsLocation
Expand Down Expand Up @@ -1630,20 +1599,15 @@ def reset_widget_settings(self):
"Settings will still be reset at next application start",
parent=self)

def show_output_view(self):
"""Show a window with application output.
"""
self.output_dock.show()

def show_report_view(self):
doc = self.current_document()
scheme = doc.scheme()
scheme.show_report_view()

def output_view(self):
def log_view(self):
"""Return the output text widget.
"""
return self.output_dock.widget()
return self.log_dock.widget()

def open_about(self):
"""Open the about dialog.
Expand Down Expand Up @@ -1979,39 +1943,6 @@ def __update_from_settings(self):
self.scheme_widget.setNodeAnimationEnabled(node_animations)
settings.endGroup()

settings.beginGroup("output")
stay_on_top = settings.value("stay-on-top", defaultValue=True,
type=bool)
if stay_on_top:
self.output_dock.setFloatingWindowFlags(Qt.Tool)
else:
self.output_dock.setFloatingWindowFlags(Qt.Window)

dockable = settings.value("dockable", defaultValue=True,
type=bool)
if dockable:
self.output_dock.setAllowedAreas(Qt.BottomDockWidgetArea)
else:
self.output_dock.setAllowedAreas(Qt.NoDockWidgetArea)

settings.endGroup()

settings.beginGroup("help")
stay_on_top = settings.value("stay-on-top", defaultValue=True,
type=bool)
if stay_on_top:
self.help_dock.setFloatingWindowFlags(Qt.Tool)
else:
self.help_dock.setFloatingWindowFlags(Qt.Window)

dockable = settings.value("dockable", defaultValue=False,
type=bool)
if dockable:
self.help_dock.setAllowedAreas(Qt.LeftDockWidgetArea | \
Qt.RightDockWidgetArea)
else:
self.help_dock.setAllowedAreas(Qt.NoDockWidgetArea)

self.open_in_external_browser = \
settings.value("open-in-external-browser", defaultValue=False,
type=bool)
Expand Down
Loading