#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Copyright © 2018 Michael J. Hayford
""" Support for fully featured QT windows for plotting/matplotlib
.. Created on Wed Nov 7 15:04:19 2018
.. codeauthor: Michael J. Hayford
"""
from pathlib import Path
from PySide6 import QtCore
from PySide6.QtCore import Qt
from PySide6 import QtGui
from PySide6.QtWidgets import (QHBoxLayout, QVBoxLayout, QWidget, QLineEdit,
QRadioButton, QGroupBox, QSizePolicy, QCheckBox,
QListWidget, QListWidgetItem, QToolBar, QMenu)
from matplotlib.backends.backend_qt5agg \
import (NavigationToolbar2QT as NavigationToolbar)
from matplotlib.backends.backend_qt5agg \
import FigureCanvasQTAgg as FigureCanvas
from rayoptics.gui.appmanager import ModelInfo
from rayoptics.mpl.axisarrayfigure import Fit
from rayoptics.mpl.styledfigure import StyledFigure
from opticalglass import glassmap as gm
from opticalglass import glassmapviewer as gmv
[docs]
class PlotCanvas(FigureCanvas):
def __init__(self, parent, figure, accept_drops=True, drop_action=None):
FigureCanvas.__init__(self, figure)
self.setParent(parent)
self.setAcceptDrops(True)
self.drop_action = drop_action if drop_action else NullDropAction()
# Next 2 lines are needed so that key press events are correctly
# passed with mouse events
# https://github.com/matplotlib/matplotlib/issues/707/
self.setFocusPolicy(Qt.FocusPolicy.ClickFocus)
self.setFocus()
FigureCanvas.setSizePolicy(self,
QSizePolicy.Expanding,
QSizePolicy.Expanding)
FigureCanvas.updateGeometry(self)
self.figure.plot()
[docs]
def dragEnterEvent(self, event):
if event.mimeData().hasFormat("text/plain"):
self.drop_action.dragEnterEvent(self, event)
event.acceptProposedAction()
[docs]
def dragMoveEvent(self, event):
if event.mimeData().hasFormat("text/plain"):
self.drop_action.dragMoveEvent(self, event)
event.acceptProposedAction()
[docs]
def dragLeaveEvent(self, event):
self.drop_action.dragLeaveEvent(self, event)
[docs]
def dropEvent(self, event):
if event.mimeData().hasText():
if self.drop_action.dropEvent(self, event):
event.acceptProposedAction()
else:
event.ignore()
[docs]
class NullDropAction():
[docs]
def dragEnterEvent(self, view, event):
pass
[docs]
def dragMoveEvent(self, view, event):
pass
[docs]
def dragLeaveEvent(self, view, event):
pass
[docs]
def dropEvent(self, view, event):
return False
[docs]
class CommandItem(QListWidgetItem):
def __init__(self, parent, txt, cntxt):
super().__init__(parent)
self.setData(Qt.ItemDataRole.DisplayRole, txt)
self.setData(Qt.ItemDataRole.EditRole, cntxt)
[docs]
def data(self, role):
if role == Qt.ItemDataRole.DisplayRole:
return self.txt
elif role == Qt.ItemDataRole.EditRole:
return self.cntxt
else:
return None
[docs]
def setData(self, role, value):
if role == Qt.ItemDataRole.DisplayRole:
self.txt = value
return True
elif role == Qt.ItemDataRole.EditRole:
self.cntxt = value
return True
else:
return False
[docs]
def create_command_panel(fig, commands):
command_panel = QListWidget()
for c in commands:
cmd_txt, cntxt = c
cntxt[2]['figure'] = fig
btn = CommandItem(command_panel, cmd_txt, cntxt)
command_panel.itemClicked.connect(on_command_clicked)
width = command_panel.size()
hint = command_panel.sizeHint()
frame_width = command_panel.frameWidth() + 2
column_width = command_panel.sizeHintForColumn(0) + 2*frame_width
command_panel.setMinimumWidth(column_width)
command_panel.setMaximumWidth(column_width)
command_panel.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Minimum)
return command_panel
[docs]
def on_command_clicked(item):
cntxt = item.data(Qt.ItemDataRole.EditRole)
fct, args, kwargs = cntxt
fct(*args, **kwargs)
[docs]
def create_plot_view(app, figure, title, view_width, view_ht, commands=None,
add_panel_fcts=None, add_nav_toolbar=False,
drop_action=None, context_menu=None):
""" create a window hosting a (mpl) figure """
def create_light_or_dark_callback(figure):
def l_or_d(is_dark):
figure.sync_light_or_dark(is_dark)
return l_or_d
# construct the top level widget
widget = QWidget()
top_layout = QHBoxLayout(widget)
# set the layout on the widget
widget.setLayout(top_layout)
widget.figure = figure
if commands:
command_panel = create_command_panel(figure, commands)
top_layout.addWidget(command_panel)
# construct the top level layout
plot_layout = QVBoxLayout()
top_layout.addLayout(plot_layout)
pc = PlotCanvas(app, figure, drop_action=drop_action)
if add_panel_fcts is not None:
panel_layout = QHBoxLayout()
plot_layout.addLayout(panel_layout)
for p in add_panel_fcts:
panel = p(app, pc)
panel_layout.addWidget(panel)
panel_layout.addStretch(50)
if context_menu is not None:
handle_context_menu = create_handle_context_menu(app, pc, context_menu)
pc.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
pc.customContextMenuRequested.connect(handle_context_menu)
mi = ModelInfo(app.app_manager.model, update_figure_view, (figure,))
sub_window = app.add_subwindow(widget, mi)
sub_window.sync_light_or_dark = create_light_or_dark_callback(figure)
lens_title = app.app_manager.model.name()
sub_window.setWindowTitle(title + ': ' + lens_title)
orig_x, orig_y = app.initial_window_offset()
sub_window.setGeometry(orig_x, orig_y, view_width, view_ht)
plot_layout.addWidget(pc)
if add_nav_toolbar:
plot_layout.addWidget(NavigationToolbar(pc, sub_window))
sub_window.show()
[docs]
def create_glass_map_view(app, glass_db):
def create_light_or_dark_callback(fig):
def l_or_d(is_dark):
fig.sync_light_or_dark(is_dark)
return l_or_d
title = 'Glass Map Viewer'
width = 1100
height = 650
# glass_db = gm.GlassMapDB(['Schott', 'Hoya', 'Ohara'])
pdt = "Refractive Index"
# hotwire GlassMapFigure to inherit from StyledFigure
gm.GlassMapFigure.__bases__ = (StyledFigure,)
figure = gm.GlassMapFigure(glass_db, plot_display_type=pdt,
# width=5, height=4,
)
widget, pick_model = gmv.init_UI(app, figure)
widget.figure = figure
def refresh_gui(**kwargs):
pick_model.fill_table(figure.pick_list)
figure.refresh_gui = refresh_gui
mi = ModelInfo(app.app_manager.model, update_figure_view, (figure,))
sub_window = app.add_subwindow(widget, mi)
sub_window.sync_light_or_dark = create_light_or_dark_callback(figure)
sub_window.setWindowTitle(title)
orig_x, orig_y = app.initial_window_offset()
sub_window.setGeometry(orig_x, orig_y, width, height)
sub_window.show()
[docs]
def on_plot_scale_toggled(cntxt, scale_type):
plotFigure, scale_wdgt = cntxt
plotFigure.scale_type = scale_type
if scale_type == Fit.User_Scale:
scale_wdgt.setReadOnly(False)
scale_wdgt.setText('{:.7g}'.format(plotFigure.user_scale_value))
else:
scale_wdgt.setReadOnly(True)
plotFigure.plot()
[docs]
def on_plot_scale_changed(cntxt):
plotFigure, scale_wdgt = cntxt
try:
val = float(scale_wdgt.text())
plotFigure.user_scale_value = val
scale_wdgt.setText('{:.7g}'.format(val))
except ValueError:
return ''
plotFigure.plot()
[docs]
def create_plot_scale_panel(app, pc):
groupBox = QGroupBox("Plot Scale", app)
user_scale_wdgt = QLineEdit()
user_scale_wdgt.setReadOnly(True)
pf = pc.figure
cntxt = pf, user_scale_wdgt
user_scale_wdgt.editingFinished.connect(lambda:
on_plot_scale_changed(cntxt))
fit_btn = QRadioButton("Fit")
fit_btn.setChecked(pf.scale_type == Fit.All)
fit_btn.toggled.connect(lambda:
on_plot_scale_toggled(cntxt, Fit.All))
user_scale_btn = QRadioButton("User Scale")
user_scale_btn.setChecked(pf.scale_type == Fit.User_Scale)
user_scale_btn.toggled.connect(lambda: on_plot_scale_toggled(
cntxt, Fit.User_Scale))
box = QHBoxLayout()
box.addWidget(fit_btn)
box.addWidget(user_scale_btn)
box.addWidget(user_scale_wdgt)
groupBox.setLayout(box)
return groupBox
[docs]
def create_multi_plot_scale_panel(app, pc):
groupBox = QGroupBox("Plot Scale", app)
user_scale_wdgt = QLineEdit()
user_scale_wdgt.setReadOnly(True)
pf = pc.figure
cntxt = pf, user_scale_wdgt
user_scale_wdgt.editingFinished.connect(lambda:
on_plot_scale_changed(cntxt))
fit_all_btn = QRadioButton("Fit All")
fit_all_btn.setChecked(pf.scale_type == Fit.All)
fit_all_btn.toggled.connect(lambda:
on_plot_scale_toggled(cntxt, Fit.All))
fit_all_same_btn = QRadioButton("Fit All Same")
fit_all_same_btn.setChecked(pf.scale_type == Fit.All_Same)
fit_all_same_btn.toggled.connect(lambda: on_plot_scale_toggled(
cntxt, Fit.All_Same))
user_scale_btn = QRadioButton("User Scale")
user_scale_btn.setChecked(pf.scale_type == Fit.User_Scale)
user_scale_btn.toggled.connect(lambda: on_plot_scale_toggled(
cntxt, Fit.User_Scale))
box = QHBoxLayout()
box.addWidget(fit_all_btn)
box.addWidget(fit_all_same_btn)
box.addWidget(user_scale_btn)
box.addWidget(user_scale_wdgt)
groupBox.setLayout(box)
return groupBox
[docs]
def get_icon(fig, icon_filepath, icon_size=48):
pm = QtGui.QPixmap(str(icon_filepath)).scaled(icon_size, icon_size)
if hasattr(pm, 'setDevicePixelRatio'):
try: # older mpl < 3.5.0
pm.setDevicePixelRatio(fig.canvas._dpi_ratio)
except AttributeError:
pm.setDevicePixelRatio(fig.canvas.device_pixel_ratio)
return QtGui.QIcon(pm)
[docs]
def create_draw_rays_groupbox(app, pc):
tb = QToolBar()
fig = pc.figure
def attr_check(fig, attr, state):
state = Qt.CheckState(state)
checked = state == Qt.CheckState.Checked
# cur_value = getattr(fig, attr, None)
setattr(fig, attr, checked)
fig.refresh()
parax_checkBox = QCheckBox("¶xial rays")
parax_checkBox.setChecked(fig.do_paraxial_layout)
parax_checkBox.stateChanged.connect(
lambda checked: attr_check(fig, 'do_paraxial_layout', checked))
beam_checkBox = QCheckBox("&beams")
beam_checkBox.setChecked(fig.do_draw_beams)
beam_checkBox.stateChanged.connect(
lambda checked: attr_check(fig, 'do_draw_beams', checked))
edge_checkBox = QCheckBox("&edge rays")
edge_checkBox.setChecked(fig.do_draw_edge_rays)
edge_checkBox.stateChanged.connect(
lambda checked: attr_check(fig, 'do_draw_edge_rays', checked))
fan_checkBox = QCheckBox("&ray fans")
fan_checkBox.setChecked(fig.do_draw_ray_fans)
fan_checkBox.stateChanged.connect(
lambda checked: attr_check(fig, 'do_draw_ray_fans', checked))
clip_rays_checkBox = QCheckBox("&clip rays")
clip_rays_checkBox.setChecked(fig.clip_rays)
clip_rays_checkBox.stateChanged.connect(
lambda checked: attr_check(fig, 'clip_rays', checked))
tb.addWidget(parax_checkBox)
tb.addWidget(beam_checkBox)
tb.addWidget(edge_checkBox)
tb.addWidget(fan_checkBox)
tb.addWidget(clip_rays_checkBox)
return tb
[docs]
def create_diagram_controls_groupbox(app, pc):
def on_barrel_constraint_toggled(cntxt, state):
fig, barrel_wdgt = cntxt
diagram = fig.diagram
state = Qt.CheckState(state)
checked = state == Qt.CheckState.Checked
if checked:
diagram.do_barrel_constraint = True
barrel_wdgt.setReadOnly(False)
barrel_wdgt.setText('{:.7g}'.format(diagram.barrel_constraint_radius))
else:
diagram.do_barrel_constraint = False
barrel_wdgt.setReadOnly(True)
fig.refresh()
def on_barrel_constraint_changed(cntxt):
fig, barrel_wdgt = cntxt
try:
val = float(barrel_wdgt.text())
fig.diagram.barrel_constraint_radius = val
barrel_wdgt.setText('{:.7g}'.format(val))
except ValueError:
return ''
fig.refresh()
groupBox = QGroupBox("Controls", app)
def attr_check(fig, attr, state):
state = Qt.CheckState(state)
checked = state == Qt.CheckState.Checked
# cur_value = getattr(fig, attr, None)
setattr(fig, attr, checked)
# print('attr_check: {}={}'.format(attr, checked))
fig.refresh()
barrel_value_wdgt = QLineEdit()
barrel_value_wdgt.setReadOnly(True)
fig = pc.figure
cntxt = fig, barrel_value_wdgt
barrel_value_wdgt.editingFinished.connect(lambda:
on_barrel_constraint_changed(
cntxt))
slide_checkBox = QCheckBox("&slide")
slide_checkBox.setChecked(fig.enable_slide)
slide_checkBox.stateChanged.connect(
lambda checked: attr_check(fig, 'enable_slide', checked))
barrel_checkBox = QCheckBox("&barrel constraint")
barrel_checkBox.setChecked(fig.diagram.do_barrel_constraint)
barrel_checkBox.stateChanged.connect(
lambda checked: on_barrel_constraint_toggled(cntxt, checked))
vbox = QVBoxLayout()
vbox.addWidget(slide_checkBox)
vbox.addWidget(barrel_checkBox)
vbox.addWidget(barrel_value_wdgt)
groupBox.setLayout(vbox)
return groupBox
[docs]
def create_diagram_edge_actions_groupbox(app, pc):
def on_bend_or_gap_toggled(diagram, radio_btn_id):
diagram.bend_or_gap = radio_btn_id
fig = pc.figure
diagram = fig.diagram
groupBox = QGroupBox("Edge Actions", app)
groupBox.setMaximumWidth(190)
bending_btn = QRadioButton("Bend")
bending_btn.setChecked(diagram.bend_or_gap == 'bend')
bending_btn.toggled.connect(lambda:
on_bend_or_gap_toggled(diagram, 'bend'))
gap_btn = QRadioButton("Gap")
gap_btn.setChecked(diagram.bend_or_gap == 'gap')
gap_btn.toggled.connect(lambda: on_bend_or_gap_toggled(diagram, 'gap'))
vbox = QVBoxLayout()
vbox.addWidget(bending_btn)
vbox.addWidget(gap_btn)
groupBox.setLayout(vbox)
return groupBox
[docs]
def create_diagram_layers_groupbox(app, pc):
def on_active_diagram_toggled(fig, layer_key):
fig.diagram.set_active_layer(layer_key)
fig.refresh(build='rebuild')
fig = pc.figure
diagram = fig.diagram
groupBox = QGroupBox("Layers", app)
groupBox.setMaximumWidth(190)
ifcs_btn = QRadioButton("interfaces")
ifcs_btn.setChecked(diagram.active_layer == 'ifcs')
ifcs_btn.toggled.connect(lambda:
on_active_diagram_toggled(fig, 'ifcs'))
ele_btn = QRadioButton("elements")
ele_btn.setChecked(diagram.active_layer == 'eles')
ele_btn.toggled.connect(lambda: on_active_diagram_toggled(fig, 'eles'))
asm_btn = QRadioButton("assembly")
asm_btn.setChecked(diagram.active_layer == 'asm')
asm_btn.toggled.connect(lambda: on_active_diagram_toggled(fig, 'asm'))
sys_btn = QRadioButton("system")
sys_btn.setChecked(diagram.active_layer == 'sys')
sys_btn.toggled.connect(lambda: on_active_diagram_toggled(fig, 'sys'))
vbox = QVBoxLayout()
vbox.addWidget(ifcs_btn)
vbox.addWidget(ele_btn)
vbox.addWidget(asm_btn)
vbox.addWidget(sys_btn)
groupBox.setLayout(vbox)
return groupBox
[docs]
def create_handle_context_menu(app, pc, menu_actions):
""" Create a (Qt) context menu for a (mpl) figure.
Args:
- app: the Qt main app
- pc: the Qt-specific backend for mpl, CanvasFigure
- menu_actions: list of potential context menu actions
each menu action is a tuple consisting of:
- action_desc:str the menu item text
- action_handle:str the key in the actions dict for the action
- action: the action class to be instantiated if picked
The fig.selected_shape is what the actions are built against. The action
should raise a TypeError if an incompatible shape is supplied.
"""
fig = pc.figure
def do_action(fig, shape, handle, action, info):
shape.actions[handle] = action
fig.selected_shape = (shape, handle), info
def handle_context_menu(point):
# show menu about the row
menu = QMenu(app)
selected_shape = fig.selected_shape
if selected_shape is None:
selected_artist = (fig.artist_infos[0]
if len(fig.artist_infos) > 0 else None)
if selected_artist is not None:
selected_shape, info = (selected_artist.artist.shape,
selected_artist.info)
else:
selected_shape, info = fig.selected_shape
if selected_shape is not None:
for ma in menu_actions:
action_desc, action_handle, action = ma
try:
action_obj = action(selected_shape[0])
except TypeError as e:
pass
else:
menu.addAction(action_desc,
lambda: do_action(fig, selected_shape[0],
action_handle, action_obj,
info))
menu.popup(pc.mapToGlobal(point))
return handle_context_menu