# -*- coding: utf-8 -*-
# pylint: disable=invalid-name, too-many-instance-attributes, unused-argument, protected-access, line-too-long,
# pylint: disable=access-member-before-definition, too-many-locals, too-many-branches
"""
Event handlers for the main application window.
Most of those come straight from QuickNXS.
"""
import time
from typing import List
from matplotlib.backend_bases import MouseEvent
from qtpy import QtWidgets
from quicknxs.ui.mplwidget import MPLWidget
[docs]
def slow_down_events(fn):
"""Decorator to slow down UI events.
Seems to be necessary since PyQt5 seems to be overactive with the UI events.
"""
def function_wrapper(self, *args, **kws):
"""Wrap a function to slow it down."""
if self.last_event is not None and time.time() - self.last_event < 0.3:
return None
self.last_event = time.time()
return fn(self, *args, **kws)
return function_wrapper
[docs]
class PlotHandler(object):
"""Class to handle plotting events."""
_picked_line = None
control_down = False
last_event = None
refl = None
def __init__(self, main_window):
self.main_window = main_window
self.ui = main_window.ui
self.plot_manager = main_window.plot_manager
self.data_manager = main_window.data_manager
self.connect_plot_events()
[docs]
def connect_plot_events(self):
"""Connect matplotlib mouse events."""
for plot in [
self.ui.xy_pp,
self.ui.xy_mp,
self.ui.xy_pm,
self.ui.xy_mm,
self.ui.xtof_pp,
self.ui.xtof_mp,
self.ui.xtof_pm,
self.ui.xtof_mm,
self.ui.xy_overview,
self.ui.xtof_overview,
self.ui.x_project,
self.ui.y_project,
self.ui.refl,
self.ui.offspec_pp,
self.ui.offspec_mm,
self.ui.offspec_pm,
self.ui.offspec_mp,
]:
plot.canvas.mpl_connect("motion_notify_event", self.plot_mouse_event)
for plot in [
self.ui.xy_pp,
self.ui.xy_mp,
self.ui.xy_pm,
self.ui.xy_mm,
self.ui.xtof_pp,
self.ui.xtof_mp,
self.ui.xtof_pm,
self.ui.xtof_mm,
self.ui.xy_overview,
self.ui.xtof_overview,
self.ui.offspec_pp,
self.ui.offspec_mm,
self.ui.offspec_pm,
self.ui.offspec_mp,
]:
plot.canvas.mpl_connect("scroll_event", self.change_color_scale)
# self.ui.x_project.canvas.mpl_connect('motion_notify_event', self.plot_pick_x)
self.ui.x_project.canvas.mpl_connect("button_press_event", self.plot_pick_x)
self.ui.x_project.canvas.mpl_connect("button_release_event", self.plot_release)
# self.ui.y_project.canvas.mpl_connect('motion_notify_event', self.plot_pick_y)
self.ui.y_project.canvas.mpl_connect("button_press_event", self.plot_pick_y)
self.ui.y_project.canvas.mpl_connect("button_release_event", self.plot_release)
self.ui.xy_overview.canvas.mpl_connect("button_press_event", self.plot_pick_xy)
# self.ui.xy_overview.canvas.mpl_connect('motion_notify_event', self.plot_pick_xy)
self.ui.xy_overview.canvas.mpl_connect("button_release_event", self.plot_release)
self.ui.xtof_overview.canvas.mpl_connect("button_press_event", self.plot_pick_xtof)
# self.ui.xtof_overview.canvas.mpl_connect('motion_notify_event', self.plot_pick_xtof)
self.ui.xtof_overview.canvas.mpl_connect("button_release_event", self.plot_release)
# Status bar indicator
self.x_position_indicator = QtWidgets.QLabel(" x=%g" % 0.0)
self.x_position_indicator.setSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Preferred)
self.x_position_indicator.setMaximumWidth(100)
self.x_position_indicator.setMinimumWidth(100)
self.ui.statusbar.addPermanentWidget(self.x_position_indicator)
self.y_position_indicator = QtWidgets.QLabel(" y=%g" % 0.0)
self.y_position_indicator.setSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Preferred)
self.y_position_indicator.setMaximumWidth(100)
self.y_position_indicator.setMinimumWidth(100)
self.ui.statusbar.addPermanentWidget(self.y_position_indicator)
[docs]
def plot_mouse_event(self, event: MouseEvent):
"""Show the mouse position of any plot in the main window status bar.
The single plot status indicator is only visible for larger plot toolbars.
"""
if event.inaxes is None:
return
self.x_position_indicator.setText(" x=%g" % event.xdata)
self.y_position_indicator.setText(" y=%g" % event.ydata)
[docs]
@slow_down_events
def change_color_scale(self, event: MouseEvent):
"""Change the intensity limits of a map plot with the mouse wheel."""
# Scaling parameters
_scale = 1.5
_step = 0.42
canvas = None
for plot in [self.ui.xy_overview, self.ui.xtof_overview]:
if plot.canvas is event.canvas:
canvas = [plot]
if event.canvas in [self.ui.xy_pp.canvas, self.ui.xy_mp.canvas, self.ui.xy_pm.canvas, self.ui.xy_mm.canvas]:
canvas = [self.ui.xy_pp, self.ui.xy_mp, self.ui.xy_pm, self.ui.xy_mm]
if event.canvas in [
self.ui.xtof_pp.canvas,
self.ui.xtof_mp.canvas,
self.ui.xtof_pm.canvas,
self.ui.xtof_mm.canvas,
]:
canvas = [self.ui.xtof_pp, self.ui.xtof_mp, self.ui.xtof_pm, self.ui.xtof_mm]
if not (canvas and canvas[0].cplot):
return
clim = canvas[0].cplot.get_clim()
for canv in canvas:
if canv.cplot is None:
continue
if self.control_down:
canv.cplot.set_clim(min(clim[1] / _scale, clim[0] * 10 ** (_step * event.step)), clim[1])
else:
canv.cplot.set_clim(clim[0], max(clim[0] * _scale, clim[1] * 10 ** (_step * event.step)))
canv.draw()
[docs]
def plot_release(self, event):
"""Release the mouse button on a plot."""
self._picked_line = None
self.main_window.changeRegionValues()
[docs]
@slow_down_events
def plot_pick_x(self, event: MouseEvent):
"""Plot for x-projection has been clicked."""
if event.button is not None and event.xdata is not None:
self.main_window.auto_change_active = True
if event.button == 1:
xcen = self.ui.refXPos.value()
bgc = self.ui.bgCenter.value()
bgw = self.ui.bgWidth.value()
bgl = bgc - bgw / 2.0
bgr = bgc + bgw / 2.0
dists = [abs(event.xdata - item) for item in [xcen, bgl, bgr]]
min_dist = dists.index(min(dists))
pl = self._picked_line
if pl == "bgl" or (pl is None and min_dist == 1):
# left of right background bar and closer to left one
bgl = event.xdata
bgc = (bgr + bgl) / 2.0
bgw = bgr - bgl
self.ui.bgCenter.setValue(bgc)
self.ui.bgWidth.setValue(bgw)
self._picked_line = "bgl"
elif pl == "bgr" or (pl is None and min_dist == 2):
# left of right background bar or closer to right background than peak
bgr = event.xdata
bgc = (bgr + bgl) / 2.0
bgw = bgr - bgl
self.ui.bgCenter.setValue(bgc)
self.ui.bgWidth.setValue(bgw)
self._picked_line = "bgr"
else:
self.ui.refXPos.setValue(event.xdata)
self._picked_line = "xpos"
elif event.button == 3:
self.ui.refXWidth.setValue(abs(self.ui.refXPos.value() - event.xdata) * 2.0)
self.main_window.auto_change_active = False
self.change_region_values()
[docs]
def plot_pick_y(self, event: MouseEvent):
"""Plot for y-projection has been clicked."""
self.main_window.auto_change_active = True
if event.button == 1 and event.xdata is not None:
ypos = self.ui.refYPos.value()
yw = self.ui.refYWidth.value()
yl = ypos - yw / 2.0
yr = ypos + yw / 2.0
pl = self._picked_line
if pl == "yl" or (pl is None and abs(event.xdata - yl) < abs(event.xdata - yr)):
yl = event.xdata
self._picked_line = "yl"
else:
yr = event.xdata
self._picked_line = "yr"
ypos = (yr + yl) / 2.0
yw = yr - yl
self.ui.refYPos.setValue(ypos)
self.ui.refYWidth.setValue(yw)
self.main_window.auto_change_active = False
self.change_region_values()
[docs]
def plot_pick_xy(self, event):
"""Plot for xy-map has been clicked."""
self.main_window.auto_change_active = True
if event.button == 1 and event.xdata is not None:
self.ui.refXPos.setValue(event.xdata)
elif event.button == 3 and event.ydata is not None:
ypos = self.ui.refYPos.value()
yw = self.ui.refYWidth.value()
yl = ypos - yw / 2.0
yr = ypos + yw / 2.0
pl = self._picked_line
if pl == "yl" or (pl is None and abs(event.ydata - yl) < abs(event.ydata - yr)):
yl = event.ydata
self._picked_line = "yl"
else:
yr = event.ydata
self._picked_line = "yr"
ypos = (yr + yl) / 2.0
yw = yr - yl
self.ui.refYPos.setValue(ypos)
self.ui.refYWidth.setValue(yw)
self.main_window.auto_change_active = False
self.change_region_values()
[docs]
def plot_pick_xtof(self, event: MouseEvent):
"""Plot for xtof-map has been clicked."""
self.main_window.auto_change_active = True
if event.button == 1 and event.ydata is not None:
xcen = self.ui.refXPos.value()
bgc = self.ui.bgCenter.value()
bgw = self.ui.bgWidth.value()
bgl = bgc - bgw / 2.0
bgr = bgc + bgw / 2.0
dists = [abs(event.ydata - item) for item in [xcen, bgl, bgr]]
min_dist = dists.index(min(dists))
pl = self._picked_line
if pl == "bgl" or (pl is None and min_dist == 1):
# left of right background bar and closer to left one
bgl = event.ydata
bgc = (bgr + bgl) / 2.0
bgw = bgr - bgl
self.ui.bgCenter.setValue(bgc)
self.ui.bgWidth.setValue(bgw)
self._picked_line = "bgl"
elif pl == "bgr" or (pl is None and min_dist == 2):
# left of right background bar or closer to right background than peak
bgr = event.ydata
bgc = (bgr + bgl) / 2.0
bgw = bgr - bgl
self.ui.bgCenter.setValue(bgc)
self.ui.bgWidth.setValue(bgw)
self._picked_line = "bgr"
else:
self.ui.refXPos.setValue(event.ydata)
self._picked_line = "xpos"
elif event.button == 3 and event.ydata is not None:
xpos = self.ui.refXPos.value()
self.ui.refXWidth.setValue(abs(xpos - event.ydata) * 2.0)
self.main_window.auto_change_active = False
self.change_region_values()
[docs]
def change_region_values(self):
"""Called when the reflectivity extraction region has been changed.
Sets up a trigger to replot the reflectivity with a delay so
a subsequent change can occur without several replots.
"""
if self.plot_manager.proj_lines is None:
return
lines = self.plot_manager.proj_lines
x_peak = self.ui.refXPos.value()
x_width = self.ui.refXWidth.value()
y_pos = self.ui.refYPos.value()
y_width = self.ui.refYWidth.value()
bg_pos = self.ui.bgCenter.value()
bg_width = self.ui.bgWidth.value()
lines[0].set_xdata([x_peak - x_width / 2.0, x_peak - x_width / 2.0])
lines[1].set_xdata([x_peak, x_peak])
lines[2].set_xdata([x_peak + x_width / 2.0, x_peak + x_width / 2.0])
lines[3].set_xdata([bg_pos - bg_width / 2.0, bg_pos - bg_width / 2.0])
lines[4].set_xdata([bg_pos + bg_width / 2.0, bg_pos + bg_width / 2.0])
lines[5].set_xdata([y_pos - y_width / 2.0, y_pos - y_width / 2.0])
lines[6].set_xdata([y_pos + y_width / 2.0, y_pos + y_width / 2.0])
self.ui.x_project.draw()
self.ui.y_project.draw()
if not self.ui.tthPhi.isChecked():
self.plot_manager.xy_x1.set_xdata([x_peak - x_width / 2.0, x_peak - x_width / 2.0])
self.plot_manager.xy_x2.set_xdata([x_peak + x_width / 2.0, x_peak + x_width / 2.0])
self.plot_manager.xy_y1.set_ydata([y_pos - y_width / 2.0, y_pos - y_width / 2.0])
self.plot_manager.xy_y2.set_ydata([y_pos + y_width / 2.0, y_pos + y_width / 2.0])
self.ui.xy_overview.draw()
self.plot_manager.xtof_x1.set_ydata([x_peak - x_width / 2.0, x_peak - x_width / 2.0])
self.plot_manager.xtof_x2.set_ydata([x_peak + x_width / 2.0, x_peak + x_width / 2.0])
self.plot_manager.xtof_bck1.set_ydata([bg_pos - bg_width / 2.0, bg_pos - bg_width / 2.0])
self.plot_manager.xtof_bck2.set_ydata([bg_pos + bg_width / 2.0, bg_pos + bg_width / 2.0])
self.ui.xtof_overview.draw()
[docs]
def change_offspec_colorscale(self):
"""Modify color scale."""
plots: List[MPLWidget] = [self.ui.offspec_pp, self.ui.offspec_mm, self.ui.offspec_pm, self.ui.offspec_mp]
Imin = 10 ** self.ui.offspecImin.value()
Imax = 10 ** self.ui.offspecImax.value()
if Imin >= Imax:
return
data_set_keys = list(self.main_window.data_manager.data_sets.keys())
for i in range(len(data_set_keys)):
plot = plots[i]
if plot.cplot is not None:
for item in plot.canvas.ax.collections:
item.set_clim(Imin, Imax)
self.plot_manager.plot_offspec(recalc=False)
[docs]
def clip_offspec_colorscale(self):
"""Modify color scale."""
plots = [self.ui.offspec_pp, self.ui.offspec_mm, self.ui.offspec_pm, self.ui.offspec_mp]
Imin = 1e10
data_set_keys = list(self.main_window.data_manager.data_sets.keys())
for i in range(len(data_set_keys)):
plot = plots[i]
if plot.cplot is not None:
for item in plot.canvas.ax.collections:
I = item.get_array()
Imin = min(Imin, I[I > 0].min())
for i in range(len(data_set_keys)):
plot = plots[i]
if plot.cplot is not None:
for item in plot.canvas.ax.collections:
I = item.get_array()
I[I <= 0] = Imin
item.set_array(I)
plot.draw()