from qtpy import QtCore, QtWidgets
import sys
from traceback import format_exception, format_exception_only
import logging
from .. import APP
[docs]class ExceptionLauncher(QtCore.QObject):
# Used to display exceptions in the status bar of PyrplWidgets
show_exception = QtCore.Signal(list) # use a signal to make
# sure no thread is messing up with gui
show_log = QtCore.Signal(list)
def __init__(self):
super(ExceptionLauncher, self).__init__()
[docs] def display_exception(self, etype, evalue, tb):
#self.etype = etype
#self.evalue = evalue
#self.tb = tb
self.show_exception.emit([etype, evalue, tb])
self.old_except_hook(etype, evalue, tb)
[docs] def display_log(self, record):
self.show_log.emit([record])
EL = ExceptionLauncher()
# Exceptions raised by the event loop should be displayed in the MainWindow status_bar.
# see http://stackoverflow.com/questions/40608610/exceptions-in-pyqt-event-loop-and-ipython
# when running in ipython, we have to monkeypatch sys.excepthook in the qevent loop.
[docs]def patch_excepthook():
EL.old_except_hook = sys.excepthook
sys.excepthook = EL.display_exception
TIMER = QtCore.QTimer()
TIMER.setSingleShot(True)
TIMER.setInterval(0)
TIMER.timeout.connect(patch_excepthook)
TIMER.start()
[docs]class LogHandler(QtCore.QObject, logging.Handler):
"""
A handler class which sends log strings to a wx object
"""
show_log = QtCore.Signal(list)
def __init__(self):
"""
Initialize the handler
"""
logging.Handler.__init__(self)
QtCore.QObject.__init__(self)
# set format of logged messages
self.setFormatter(logging.Formatter('%(levelname)s (%(name)s): %(message)s'))
[docs] def emit(self, record):
"""
Emit a record.
"""
try:
msg = self.format(record)
self.show_log.emit([msg])
#EL.display_log(record)
except (KeyboardInterrupt, SystemExit):
raise
except:
self.handleError(record)
class MyDockWidget(QtWidgets.QDockWidget):
"""
A DockWidget where the inner widget is only created when needed (To reduce load times).
"""
scrollable = True # use scroll bars?
def __init__(self, create_widget_func, name):
"""
create_widget_func is a function to create the widget.
"""
super(MyDockWidget, self).__init__(name)
self.setObjectName(name)
self.setFeatures(
QtWidgets.QDockWidget.DockWidgetFloatable |
QtWidgets.QDockWidget.DockWidgetMovable |
QtWidgets.QDockWidget.DockWidgetVerticalTitleBar|
QtWidgets.QDockWidget.DockWidgetClosable)
self.create_widget_func = create_widget_func
self.widget = None
def showEvent(self, event):
if self.widget is None:
self.widget = self.create_widget_func()
if self.scrollable:
self.scrollarea = QtWidgets.QScrollArea()
self.scrollarea.setWidget(self.widget)
self.scrollarea.setWidgetResizable(True)
self.setWidget(self.scrollarea)
else:
self.setWidget(self.widget)
super(MyDockWidget, self).showEvent(event)
def event(self, event):
event_type = event.type()
if event.type() == 176: # QEvent::NonClientAreaMouseButtonDblClick
if self.isFloating():
if self.isMaximized():
fn = lambda: self.showNormal()
else:
fn = lambda: self.showMaximized()
# strange bug: always goes back to normal
# self.showMaximized()
# dirty workaround: make a timer
self.timer = QtCore.QTimer()
self.timer.timeout.connect(fn)
self.timer.setSingleShot(True)
self.timer.setInterval(1.0)
self.timer.start()
event.accept()
return True
else:
#return super(MyDockWidget, self).event(event)
return QtWidgets.QDockWidget.event(self, event)
class PyrplWidget(QtWidgets.QMainWindow):
def __init__(self, pyrpl_instance):
self.parent = pyrpl_instance
self.logger = self.parent.logger
self.handler = LogHandler()
self.logger.addHandler(self.handler)
super(PyrplWidget, self).__init__()
self.setDockNestingEnabled(True) # allow dockwidget nesting
self.setAnimated(True) # animate docking of dock widgets
self.dock_widgets = {}
self.last_docked = None
self.menu_modules = self.menuBar().addMenu("Modules")
self.module_actions = []
for module in self.parent.software_modules:
self.add_dock_widget(module._create_widget, module.name)
# self.showMaximized() # maximized by default
self.centralwidget = QtWidgets.QFrame()
self.setCentralWidget(self.centralwidget)
self.centrallayout = QtWidgets.QVBoxLayout()
self.centrallayout.setAlignment(QtCore.Qt.AlignCenter)
self.centralwidget.setLayout(self.centrallayout)
self.centralbutton = QtWidgets.QPushButton('Click on "Modules" in the '
'upper left corner to load a '
'specific PyRPL module!')
self.centralbutton.clicked.connect(self.click_menu_modules)
self.centrallayout.addWidget(self.centralbutton)
self.set_window_position()
self.timer_save_pos = QtCore.QTimer()
self.timer_save_pos.setInterval(1000)
self.timer_save_pos.timeout.connect(self.save_window_position)
self.timer_save_pos.start()
self.timer_toolbar = QtCore.QTimer()
self.timer_toolbar.setInterval(1000)
self.timer_toolbar.setSingleShot(True)
self.timer_toolbar.timeout.connect(self.vanish_toolbar)
self.status_bar = self.statusBar()
EL.show_exception.connect(self.show_exception)
self.handler.show_log.connect(self.show_log)
self.setWindowTitle(self.parent.c.pyrpl.name)
self.timers = [self.timer_save_pos, self.timer_toolbar]
#self.set_background_color(self)
def click_menu_modules(self):
self.menu_modules.popup(self.mapToGlobal(QtCore.QPoint(10,10)))
def hide_centralbutton(self):
for dock_widget in self.dock_widgets.values():
if dock_widget.isVisible():
self.centralwidget.hide()
return
# only if no dockwidget is shown, show central button
self.centralwidget.show()
def show_exception(self, typ_val_tb):
"""
show exception in red in toolbar
"""
typ, val, tb = typ_val_tb
self.timer_toolbar.stop()
self.status_bar.showMessage(''.join(format_exception_only(typ, val)))
self.status_bar.setStyleSheet('color: white;background-color: red;')
self._next_toolbar_style = 'color: orange;'
self.status_bar.setToolTip(''.join(format_exception(typ, val, tb)))
self.timer_toolbar.start()
def show_log(self, records):
record = records[0]
self.timer_toolbar.stop()
self.status_bar.showMessage(record)
self.status_bar.setStyleSheet('color: white;background-color: green;')
self._next_toolbar_style = 'color: grey;'
self.timer_toolbar.start()
def vanish_toolbar(self):
"""
Toolbar becomes orange after (called 1s after exception occured)
"""
self.status_bar.setStyleSheet(self._next_toolbar_style)
def _clear(self):
for timer in self.timers:
timer.stop()
def add_dock_widget(self, create_widget, name):
dock_widget = MyDockWidget(create_widget,
name + ' (%s)' % self.parent.name)
self.dock_widgets[name] = dock_widget
self.addDockWidget(QtCore.Qt.TopDockWidgetArea,
dock_widget)
if self.last_docked is not None:
self.tabifyDockWidget(self.last_docked, dock_widget)
# put tabs on top
self.setTabPosition(dock_widget.allowedAreas(),
QtWidgets.QTabWidget.North)
self.last_docked = dock_widget
self.last_docked.hide() # by default no widget is created...
action = QtWidgets.QAction(name, self.menu_modules)
action.setCheckable(True)
self.module_actions.append(action)
self.menu_modules.addAction(action)
# make sure menu and widget are in sync
action.changed.connect(lambda: dock_widget.setVisible(action.isChecked()))
dock_widget.visibilityChanged.connect(lambda:action.setChecked(dock_widget.isVisible()))
dock_widget.visibilityChanged.connect(self.hide_centralbutton)
self.set_background_color(dock_widget)
def remove_dock_widget(self, name):
dock_widget = self.dock_widgets.pop(name)
# return later whether the widget was visible
wasvisible = dock_widget.isVisible()
# disconnect signals from widget
dock_widget.blockSignals(True) # avoid further signals
# remove action button from context menu
for action in self.module_actions:
buttontext = action.text()
if buttontext == name:
action.blockSignals(True) # avoid further signals
self.module_actions.remove(action)
self.menu_modules.removeAction(action)
action.deleteLater()
# remove dock widget
if self.last_docked == dock_widget:
self.last_docked = list(self.dock_widgets.values())[-1]
# not sure what this is supposed to mean, but dict keys/values
# are not indexable in python 3. Please, convert to list before!
self.removeDockWidget(dock_widget)
dock_widget.deleteLater()
# return whether the widget was visible
return wasvisible
def reload_dock_widget(self, name):
"""
This function destroys the old lockbox widget and loads a new one
"""
pyrpl = self.parent
module = getattr(pyrpl, name)
# save window position
self.timer_save_pos.stop()
self.save_window_position()
pyrpl.c._write_to_file() # make sure positions are written
# replace dock widget
self.remove_dock_widget(name)
self.add_dock_widget(module._create_widget, name)
# restore window position and widget visibility
self.set_window_position() # reset the same window position as before
self.timer_save_pos.start()
def save_window_position(self):
# Don't try to save position if window is closed (otherwise, random position is saved)
if self.isVisible():
# pre-serialize binary data as "latin1" string
act_state = (bytes(self.saveState())).decode("latin1")
if (not "dock_positions" in self.parent.c.pyrpl._keys()) or \
(self.parent.c.pyrpl["dock_positions"]!=act_state):
self.parent.c.pyrpl["dock_positions"] = act_state
act_window_pos = self.window_position
saved_window_pos = self.parent.c.pyrpl._get_or_create("window_position")._data
if saved_window_pos != act_window_pos:
self.parent.c.pyrpl.window_position = self.window_position
#else:
# self.logger.debug("Gui is not started. Cannot save position.\n")
def set_window_position(self):
if "dock_positions" in self.parent.c.pyrpl._keys():
try:
self.restoreState(
self.parent.c.pyrpl.dock_positions.encode("latin1"))
except:
self.logger.warning("Sorry, there was a problem with the "
"restoration of Dock positions. ")
try:
coords = self.parent.c.pyrpl["window_position"]._data
except KeyError:
coords = [0, 0, 800, 600]
try:
self.window_position = coords
if QtWidgets.QApplication.desktop().screenNumber(self)==-1:
# window doesn't fit inside screen
self.window_position = (0,0)
except Exception as e:
self.logger.warning("Gui is not started. Cannot set window position.\n"\
+ str(e))
@property
def window_position(self):
xy = self.pos()
x = xy.x()
y = xy.y()
dxdy = self.size()
dx = dxdy.width()
dy = dxdy.height()
return [x, y, dx, dy]
@window_position.setter
def window_position(self, coords):
self.move(coords[0], coords[1])
self.resize(coords[2], coords[3])
def set_background_color(self, widget):
try:
color = str(self.parent.c.pyrpl.background_color)
except KeyError:
return
else:
if color.strip() == "":
return
try: # hex values must receive a preceeding hashtag
int(color, 16)
except ValueError:
pass
else:
color = "#"+color
widget.setStyleSheet("background-color:%s"%color)