from enum import auto
import sys
from PyQt5 import Qt
from PyQt5 import QtCore
from PyQt5 import QtWidgets
from ipywebrtc.webrtc import VideoStream
# You may need to uncomment these lines on some systems:
#import vtk.qt
#vtk.qt.QVTKRWIBase = "QGLWidget"
from vtk.qt.QVTKRenderWindowInteractor import QVTKRenderWindowInteractor
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT as NavigationToolbar
from matplotlib.figure import Figure
import matplotlib.pyplot as plt
import matplotlib as mpl
from iblviewer.application import Viewer
got_ibllib = True
try:
from iblviewer.mouse_brain import MouseBrainViewer
except ModuleNotFoundError:
got_ibllib = False
from vedo import Plotter
import numpy as np
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg
from matplotlib.figure import Figure
from threading import *
[docs]class MplCanvas(FigureCanvasQTAgg): #or simply FigureCanvas?
"""
Matplotlib statistics
"""
def __init__(self, parent=None, width=5, height=4, dpi=100, background_color='white'):
fig = Figure(figsize=(width, height), dpi=dpi, facecolor=background_color)
self.axes = fig.add_subplot(111)
super(MplCanvas, self).__init__(fig)
[docs]class ViewerWindow(Qt.QMainWindow):
"""
This is the main window container that holds the UI, the 3D viewer and statistics
"""
def __init__(self, parent=None):
"""
Constructor
"""
Qt.QMainWindow.__init__(self, parent)
self.frame = Qt.QFrame()
self.ui_layout = Qt.QVBoxLayout()
self.main_layout = Qt.QHBoxLayout()
self.frame.setLayout(self.main_layout)
self.setCentralWidget(self.frame)
self.left = 10
self.top = 10
self.title = 'International Brain Laboratory Viewer'
self.width = 1920
self.height = 1280
self.viewer = None
self.viewer_function = None
self.statistics_function = None
self.dark_mode = False
self.kwargs = {}
self.auto_complete_data_changed = False
self.regions_search_names = None
self.setWindowTitle(self.title)
self.setGeometry(self.left, self.top, self.width, self.height)
# Overwrite some plot properties to integrate it within Qt
self.vtkWidget = QVTKRenderWindowInteractor(self.frame)
self.plot = Plotter(qtWidget=self.vtkWidget)
self.viewer_initialized = False
self.neuroscience_context = False
self.statistics_visible = True
[docs] def initialize(self, viewer=None, callable=None, stats_callable=None, dark_mode=True, **kwargs):
"""
Initialize the viewer app
:param viewer: Either iblviewer.application.Viewer or
iblviewer.mouse_brain.MouseBrainViewer
:param callable: A function that you pass to this method. You must write
that function with a required parameter (the viewer). This allows
you to perform actions with the viewer before content is shown in QT UI.
It would also be possible to add a python console in QT in order to run
Python code live like in Jupyter notebooks but this isn't implemented yet.
:param stats_callable: A function that will be executed every time the plot is updated
for instance when a new object or sub selecton is made
:param dark_mode: Whether the app is in dark mode or not
"""
self.viewer_function = callable
self.statistics_function = stats_callable
self.dark_mode = dark_mode
if isinstance(kwargs, dict):
self.kwargs = kwargs
if got_ibllib:
if isinstance(viewer, MouseBrainViewer) or isinstance(viewer, Viewer):
self.viewer = viewer
if self.viewer is None:
self.viewer = MouseBrainViewer()
self.neuroscience_context = isinstance(self.viewer, MouseBrainViewer)
else:
self.neuroscience_context = False
self.initialize_ui()
# It's important to start the viewer in another thread so that the QT UI
# doesn't freeze when interacting with it.
# In case you need to go further with this, look at QThread with a good summary here:
# https://realpython.com/python-pyqt-qthread/#using-qthread-to-prevent-freezing-guis
thread = Thread(target=self._initialize_viewer)
thread.start()
def _initialize_viewer(self):
"""
Initialize the viewer
"""
#if viewer.plot is not None:
#viewer.plot.close()
if 'embed_ui' in self.kwargs:
self.kwargs['embed_ui'] = False
#if 'offscreen' in self.kwargs:
#self.kwargs['offscreen'] = True
if self.viewer is None:
if got_ibllib:
self.viewer = MouseBrainViewer()
else:
self.viewer = Viewer()
self.viewer.initialize(plot=self.plot, dark_mode=self.dark_mode, **self.kwargs)
if self.viewer_function is not None:
try:
# Allows users to add data to be visualized
self.viewer_function(self.viewer)
except Exception as e:
print(e)
# Assign functions (to mimick event callbacks)
# Have a look at Qt signals if you want to do something more advanced
# Basic example : https://stackoverflow.com/questions/28793440/pyqt5-focusin-out-events
self.viewer.selection_changed = self.onSelectionChanged
self.viewer.sub_selection_changed = self.onSelectionChanged
self.viewer.objects_changed = self.onObjectsChanged
self.viewer_initialized = True
self.show_viewer()
[docs] def initialize_ui(self):
"""
Initialize the QT UI
"""
self.background_color = 'white'
mpl.rcParams['axes.spines.left'] = True
mpl.rcParams['axes.spines.right'] = False
mpl.rcParams['axes.spines.top'] = False
mpl.rcParams['axes.spines.bottom'] = True
self.set_dark_mode(self.dark_mode)
self.main_layout.addLayout(self.ui_layout, 1)
self.menu_tabs = Qt.QTabWidget()
self.camera_menu = Qt.QWidget()
self.tools_menu = Qt.QWidget()
self.object_menu = Qt.QWidget()
#self.data_menu = Qt.QWidget()
self.export_menu = Qt.QWidget()
self.menu_tabs.addTab(self.camera_menu, 'Camera')
self.menu_tabs.addTab(self.tools_menu, 'Tools')
self.menu_tabs.addTab(self.object_menu, 'Object')
#self.menu_tabs.addTab(self.data_menu, 'Data')
self.menu_tabs.addTab(self.export_menu, 'Export')
#camera_group = QtWidgets.QGroupBox('Camera presets')
self.camera_menu.layout = Qt.QVBoxLayout()
hbox = Qt.QHBoxLayout()
self.add_button('Left', self.onLeftCameraPreset, hbox)
self.add_button('Right', self.onRightCameraPreset, hbox)
self.camera_menu.layout.addLayout(hbox)
hbox = Qt.QHBoxLayout()
self.add_button('Dorsal', self.onDorsalCameraPreset, hbox)
self.add_button('Ventral', self.onVentralCameraPreset, hbox)
self.camera_menu.layout.addLayout(hbox)
hbox = Qt.QHBoxLayout()
self.add_button('Anterior', self.onAnteriorCameraPreset, hbox)
self.add_button('Posterior', self.onPosteriorCameraPreset, hbox)
self.camera_menu.layout.addLayout(hbox)
hbox = Qt.QHBoxLayout()
self.add_checkbox('Autofocus', self.onToggleAutofocus, hbox, set_checked=True)
self.add_checkbox('Orthographic', self.onToggleOrthoCamera, hbox)
hbox.addStretch(1)
self.camera_menu.layout.addLayout(hbox)
hbox = Qt.QHBoxLayout()
self.add_button('View selected', self.onViewSelection, hbox)
self.camera_menu.layout.addLayout(hbox)
self.camera_menu.layout.addStretch(1)
self.camera_menu.setLayout(self.camera_menu.layout)
#self.ui_layout.addWidget(camera_group)
scene_group = QtWidgets.QGroupBox('Scene')
vbox = Qt.QVBoxLayout()
self.selection_combo = self.add_combo('Select an object', [], self.onChangeSelection, vbox)
scene_group.setLayout(vbox)
view_group = QtWidgets.QGroupBox('View options')
vbox = Qt.QVBoxLayout()
hbox = Qt.QHBoxLayout()
self.add_checkbox('Outline', self.onToggleOutline, hbox, set_checked=True)
self.add_checkbox('Marker', self.onToggleMarker, hbox, set_checked=True)
self.add_checkbox('Color bar', self.onToggleColorBar, hbox, set_checked=True)
vbox.addLayout(hbox)
hbox = Qt.QHBoxLayout()
self.add_checkbox('Information text', self.onToggleInfoText, hbox, set_checked=True)
self.add_checkbox('Dark background', self.onToggleDarkBackground, hbox, set_checked=True)
vbox.addLayout(hbox)
hbox = Qt.QHBoxLayout()
self.add_checkbox('Fast volumes', self.onToggleVolumesLOD, hbox)
vbox.addLayout(hbox)
view_group.setLayout(vbox)
self.ui_layout.addWidget(scene_group)
self.ui_layout.addWidget(view_group)
if self.statistics_function is not None:
stats_group = QtWidgets.QGroupBox('Statistics')
hbox = Qt.QHBoxLayout()
self.add_checkbox('Statistics panel', self.onToggleStatistics, hbox, auto_render=False)
stats_group.setLayout(hbox)
self.ui_layout.addWidget(stats_group)
#object_group = QtWidgets.QGroupBox('Object settings')
self.object_menu.layout = Qt.QVBoxLayout()
self.opacity_slider = self.add_slider('Opacity', 1.0, 0.0, 1.0, 0.05, self.onOpacityChange, self.object_menu.layout)
self.slices_opacity_slider = self.add_slider('Slices opacity', 1.0, 0.0, 1.0, 0.05,
self.onSliceOpacityChange, self.object_menu.layout)
self.hollow_checkbox = self.add_checkbox('Hollow regions', self.onToggleHollow, self.object_menu.layout)
self.time_series_slider = self.add_slider('Time series', 1, 0, 10, 1, self.onTimeSeriesChange, self.object_menu.layout)
self.add_button('Remove object', self.onRemoveObject, self.object_menu.layout)
self.object_menu.layout.addStretch(1)
self.object_menu.setLayout(self.object_menu.layout)
#self.ui_layout.addWidget(object_group)
#self.object_menu.layout.addWidget(object_group)
self.tools_menu.layout = Qt.QVBoxLayout()
self.new_probe_button = self.add_button('Add new probe', self.onNewProbe, self.tools_menu.layout)
self.edit_probe_button = self.add_button('Edit probe', self.onEditProbe, self.tools_menu.layout)
self.slicer_button = self.add_button('Cutter/Slicer mode', self.onSlicerToggle, self.tools_menu.layout, toggle=True)
if self.neuroscience_context:
self.search_input = self.add_input('Search an atlas region', None, self.tools_menu.layout, autocomplete=True)
#self.isosurface_checkbox = self.add_checkbox('Show regions surface', None, vbox, set_checked=True)
self.search_button = self.add_button('Get region', self.onSearch, self.tools_menu.layout)
self.clipping_combo = self.add_combo('Select a clipping object', [], None, self.tools_menu.layout, auto_render=False)
self.clip_to_bounds = self.add_button('Clip to object bounds', self.onClipToBounds, self.tools_menu.layout)
self.tools_menu.layout.addStretch(1)
self.tools_menu.setLayout(self.tools_menu.layout)
#group = QtWidgets.QGroupBox('Data settings')
#self.data_menu.layout = Qt.QVBoxLayout()
#self.selection_label = QtWidgets.QLabel()
#self.data_menu.layout.addWidget(self.selection_label)
#self.data_menu.layout.addStretch(1)
#self.data_menu.setLayout(self.data_menu.layout)
#self.ui_layout.addWidget(data_group)
#video_group = QtWidgets.QGroupBox('Video export')
self.export_menu.layout = Qt.QVBoxLayout()
video_group = QtWidgets.QGroupBox('Export video presets')
vbox = Qt.QVBoxLayout()
self.duration_slider = self.add_slider('Duration', 8, 0, 60, 1, None, vbox)
self.start_angle_slider = self.add_slider('Start angle', 0, 0, 360, 1, None, vbox)
self.end_angle_slider = self.add_slider('End angle', 360, 0, 360, 1, None, vbox)
self.end_angle_slider.setTracking(True)
self.end_angle_slider.setTickPosition(QtWidgets.QSlider.TicksAbove)
self.export_button = self.add_button('Export video...', self.onExportVideo, vbox)
video_group.setLayout(vbox)
#self.video_menu.layout.addStretch(1)
self.export_menu.layout.addWidget(video_group)
image_group = QtWidgets.QGroupBox('Export image presets')
vbox = Qt.QVBoxLayout()
self.magnification_scale = self.add_slider('Magnification scale', 2, 1, 10, 1, None, vbox)
self.export_button = self.add_button('Export image...', self.onExportImage, vbox)
image_group.setLayout(vbox)
self.export_menu.layout.addStretch(1)
self.export_menu.layout.addWidget(image_group)
self.export_menu.setLayout(self.export_menu.layout)
#self.ui_layout.addWidget(video_group)
# Once we're done with preparing the whole menu with tabs, add that to the UI
self.ui_layout.addWidget(self.menu_tabs)
self.ui_layout.addStretch(1)
splitter = QtWidgets.QSplitter(QtCore.Qt.Vertical)
splitter.addWidget(self.vtkWidget)
self.statistics = MplCanvas(self, 5, 4, 100, self.background_color)
self.statistics.setStyleSheet("background-color:transparent;")
self.update_statistics()
# Create toolbar, passing statistics as first parament, parent (self, the MainWindow) as second.
#toolbar = NavigationToolbar(self.statistics, self)
#layout = Qt.QVBoxLayout()
#layout.addWidget(toolbar)
#layout.addWidget(self.statistics)
#self.main_vbox.addLayout(layout)
self.statistics_widget = Qt.QWidget()
self.statistics_layout = Qt.QHBoxLayout(self.statistics_widget)
self.statistics_layout.addWidget(self.statistics)
# Don't show stats initially
self.onToggleStatistics()
splitter.addWidget(self.statistics_widget)
#splitter.setStretchFactor(3, 1)
self.main_layout.addWidget(splitter, 4)
self.main_splitter = splitter
#splitter.setSizes([125, 150])
self.vtkWidget.update()
self.show()
[docs] def onViewerInitialized(self):
if self.neuroscience_context:
regions_data = self.viewer.get_region_names()
self.search_input.completer_model.setStringList(regions_data)
[docs] def show_viewer(self):
"""
Show the viewer (when it's initialized)
"""
self.viewer.show()
self.vtkWidget.update()
self.onObjectsChanged()
self.onViewerInitialized()
self.update_ui()
[docs] def set_light_mode(self):
"""
Set light mode to viewer and stats
"""
self.set_dark_mode(False)
[docs] def set_dark_mode(self, on=True):
"""
Set dark mode to viewer and stats
:param on: Whether dark mode is on
"""
if on:
self.background_color = '#2d2d2d'
plt.style.use('dark_background')
mpl.rcParams['axes.facecolor'] = self.background_color
else:
self.background_color = '#dddddd'
plt.style.use('default')
mpl.rcParams['axes.facecolor'] = self.background_color
if self.viewer is not None:
self.viewer.set_dark_mode(on)
[docs] def update_ui(self):
"""
Update the QT UI
"""
got_selection = self.viewer.model.got_selection()
is_volume = self.viewer.is_volume()
is_probe = self.viewer.is_probe()
self.new_probe_button.setEnabled(got_selection and is_volume)
self.edit_probe_button.setEnabled(got_selection and is_probe)
#self.isosurface_checkbox.setEnabled(got_selection and is_volume)
self.hollow_checkbox.setEnabled(got_selection and is_volume)
self.clipping_combo.setEnabled(got_selection)
self.clip_to_bounds.setEnabled(got_selection)
self.slicer_button.setEnabled(got_selection)
self.slicer_button.setChecked(self.viewer.box_widget is not None)
if got_selection:
'''
text, data = self.viewer.get_selection_info(line_length=32)
if text is not None:
self.selection_label.setText(text+'\n')
else:
self.selection_label.setText('')
'''
index = self.selection_combo.findText(self.viewer.model.selection.name)
if index != -1:
self.selection_combo.setCurrentIndex(index)
else:
self.selection_combo.setCurrentIndex(0)
#self.selection_label.setText('')
opacity = self.viewer.get_selection_opacity()
self.opacity_slider.setEnabled(opacity is not None)
self.opacity_slider.label.setEnabled(opacity is not None)
'''
if opacity is None:
self.opacity_slider.label.hide()
else:
self.opacity_slider.show()
self.opacity_slider.label.show()
'''
if opacity is not None:
self.opacity_slider.setValue(int(opacity / self.opacity_slider.step))
slices_opacity = self.viewer.get_selection_slices_opacity()
self.slices_opacity_slider.setEnabled(slices_opacity is not None)
self.slices_opacity_slider.label.setEnabled(slices_opacity is not None)
if slices_opacity is not None:
self.slices_opacity_slider.setValue(int(slices_opacity / self.slices_opacity_slider.step))
current_value, max_value = self.viewer.get_time_series_info()
self.time_series_slider.setEnabled(current_value is not None)
if isinstance(current_value, int):
self.time_series_slider.setValue(current_value)
if isinstance(max_value, int):
self.time_series_slider.setRange(0, max_value)
[docs] def update_statistics(self):
"""
Update statistics
"""
if not self.statistics_visible:
return
if self.statistics_function is not None:
try:
plot = self.statistics_function(self.statistics, self.viewer)
if plot is not None:
self.statistics = plot
except Exception as e:
e_type = type(e).__name__
msg = 'Your custom statistics function failed with error type ' + e_type
if 'TypeError' in e_type:
msg += '\nMake sure your functions accepts two arguments (the statistics plot and the 3d viewer).'
print(msg)
print(e)
else:
self.statistics.axes.clear()
self.statistics.axes.plot(np.arange(20), np.random.random(20)/2)
#self.statistics.setStyleSheet("background-color:#eeeeee;")
self.statistics.draw()
[docs] def onObjectsChanged(self):
"""
Event triggered when the dictionary of 3D objects has been updated
"""
names = self.viewer.get_object_names()
current_id = None
for n_id in range(len(names)):
if names[n_id] == self.selection_combo.currentText():
current_id = n_id
names = ['None'] + names
self.selection_combo.clear()
self.selection_combo.addItems(names)
self.clipping_combo.clear()
self.clipping_combo.addItems(names)
if current_id is not None:
self.selection_combo.setCurrentIndex(current_id)
[docs] def onChangeSelection(self, value):
"""
Event triggered by QT to change the viewer's selection
"""
index = self.selection_combo.findText(value)
if index != -1:
self.selection_combo.setCurrentIndex(index)
self.viewer.select(value, allow_none=True)
[docs] def onSelectionChanged(self):
"""
Event triggered by the viewer when a new valid selection is made
"""
self.update_ui()
self.update_statistics()
[docs] def onSearch(self):
search_term = self.search_input.text()
if search_term == '':
return
result = self.viewer.find_region(search_term)
if len(result) < 1:
return
# TODO: improve this and give user the choice (left or right hemisphere)
region_id = result[0]
#if self.isosurface_checkbox.isChecked():
self.viewer.isosurface(region_id, split_meshes=False)
[docs] def onOpacityChange(self, value):
self.viewer.update_opacity(value=value)
[docs] def onSliceOpacityChange(self, value):
self.viewer.update_slices_opacity(value=value)
[docs] def onTimeSeriesChange(self, value):
self.viewer.set_time_series(value)
[docs] @Qt.pyqtSlot()
def onToggleStatistics(self):
if self.statistics_visible:
self.statistics_visible = False
self.statistics_widget.hide()
else:
self.statistics_visible = True
# Using show on the maptlotlib widget will segfault with an error
# in mpl backend wrt to QT. So it's better to start with a new plot
self.statistics_layout.removeWidget(self.statistics)
self.statistics = MplCanvas(self, 5, 4, 100, self.background_color)
self.statistics.setStyleSheet("background-color:transparent;")
self.update_statistics()
self.statistics_layout.addWidget(self.statistics)
self.statistics_widget.show()
[docs] @Qt.pyqtSlot()
def onToggleMarker(self):
self.viewer.toggle_marker()
[docs] @Qt.pyqtSlot()
def onToggleOutline(self):
self.viewer.toggle_outline()
[docs] @Qt.pyqtSlot()
def onToggleColorBar(self):
self.viewer.toggle_color_bar()
[docs] @Qt.pyqtSlot()
def onToggleInfoText(self):
self.viewer.toggle_info_text()
[docs] @Qt.pyqtSlot()
def onToggleDarkBackground(self):
self.viewer.toggle_dark_mode()
[docs] @Qt.pyqtSlot()
def onToggleVolumesLOD(self):
self.viewer.toggle_volumetric_lod()
[docs] @Qt.pyqtSlot()
def onToggleMarkerType(self):
self.viewer.toggle_marker_type()
[docs] @Qt.pyqtSlot()
def onToggleHollow(self):
self.viewer.toggle_hollow_mode()
[docs] @Qt.pyqtSlot()
def onRemoveObject(self):
self.viewer.remove_object()
self.onObjectsChanged()
[docs] @Qt.pyqtSlot()
def onExportImage(self):
options = QtWidgets.QFileDialog.Options()
options |= QtWidgets.QFileDialog.DontUseNativeDialog
file_path, _ = QtWidgets.QFileDialog.getSaveFileName(self, 'QFileDialog.getSaveFileName()',
'iblviewer.jpg', 'All Files (*);;Images (*.png *.jpg);',
options=options)
if file_path:
self.viewer.render(file_path)
Qt.QMessageBox.about(self, 'Image rendering complete', f'File was saved under {file_path}')
[docs] @Qt.pyqtSlot()
def onExportVideo(self):
options = QtWidgets.QFileDialog.Options()
options |= QtWidgets.QFileDialog.DontUseNativeDialog
file_path, _ = QtWidgets.QFileDialog.getSaveFileName(self, 'QFileDialog.getSaveFileName()',
'iblviewer.mp4', 'All Files (*);;Videos (*.mp4);',
options=options)
if file_path:
start_angle = self.start_angle_slider.value()
end_angle = self.end_angle_slider.value()
duration = self.duration_slider.value()
self.viewer.export_turntable_video(file_path, start_angle, end_angle, duration)
Qt.QMessageBox.about(self, 'Video rendering complete', f'File was saved under {file_path}')
[docs] @Qt.pyqtSlot()
def onClipToBounds(self):
bounds_obj = self.viewer.objects.get(self.clipping_combo.currentText())
self.viewer.clip_to_bounds(bounds_obj)
[docs] @Qt.pyqtSlot()
def onIsosurface(self):
self.viewer.isosurface(split_meshes=True)
[docs] @Qt.pyqtSlot()
def onSlicerToggle(self):
self.viewer.toggle_box_widget()
[docs] @Qt.pyqtSlot()
def onNewProbe(self):
self.viewer.add_probe()
[docs] @Qt.pyqtSlot()
def onEditProbe(self):
self.viewer.edit_probe()
[docs] @Qt.pyqtSlot()
def onToggleOrthoCamera(self):
self.viewer.toggle_orthographic_view()
[docs] @Qt.pyqtSlot()
def onViewSelection(self):
self.viewer.view_selected()
[docs] @Qt.pyqtSlot()
def onToggleAutofocus(self):
self.viewer.toggle_autofocus()
[docs] @Qt.pyqtSlot()
def onLeftCameraPreset(self):
self.viewer.set_left_view()
[docs] @Qt.pyqtSlot()
def onRightCameraPreset(self):
self.viewer.set_right_view()
[docs] @Qt.pyqtSlot()
def onDorsalCameraPreset(self):
self.viewer.set_dorsal_view()
[docs] @Qt.pyqtSlot()
def onVentralCameraPreset(self):
self.viewer.set_ventral_view()
[docs] @Qt.pyqtSlot()
def onAnteriorCameraPreset(self):
self.viewer.set_anterior_view()
[docs] @Qt.pyqtSlot()
def onPosteriorCameraPreset(self):
self.viewer.set_posterior_view()
[docs] @Qt.pyqtSlot()
def onClose(self):
#Disable the interactor before closing to prevent it
#from trying to act on already deleted items
self.vtkWidget.close()
# Below are utility functions
[docs] def add_checkbox(self, label, click_function, layout=None, tooltip=None,
auto_render=True, set_checked=False):
"""
Add a new checkbox to a layout
"""
checkbox = QtWidgets.QCheckBox(label, self)
checkbox.move(20, 0)
if isinstance(tooltip, str):
checkbox.setToolTip(tooltip)
def change_handler(value):
if click_function is not None:
click_function()
if auto_render:
self.viewer.render()
checkbox.setChecked(set_checked)
checkbox.stateChanged.connect(change_handler)
if layout is not None:
layout.addWidget(checkbox)
return checkbox
[docs] def add_combo(self, text, values=None, change_function=None, layout=None, auto_render=True):
"""
Add a new combobox with a label to a layout
"""
label = QtWidgets.QLabel(self)
label.setText(text)
combo = QtWidgets.QComboBox(self)
combo.label = label
combo.last_value = None
if values is not None:
combo.addItems(values)
def update_combo(value):
#label.setText(f'{value}')
if change_function is not None:
change_function(value)
if auto_render:
self.viewer.render()
combo.last_value = value
if change_function is not None:
combo.activated[str].connect(update_combo)
#combo.currentIndexChanged['QString'].connect(update_combo)
#combo.valueChanged.connect(update_combo)
if layout is not None:
layout.addWidget(label)
layout.addWidget(combo)
return combo
[docs] def add_slider(self, text, value, min_value=0, max_value=10, step=1, change_function=None,
layout=None, ticks=None, label_precision=2, auto_render=True):
"""
Add a new slider with a label to a layout
"""
label = QtWidgets.QLabel(self)
label.setText(text)
#label.setPixmap(QPixmap('mute.png'))
slider = QtWidgets.QSlider(QtCore.Qt.Horizontal, self)
#slider.setGeometry(50,50, 200, 50)
#slider.move(0, 30)
# Sliders in Qt can only accept integers (!)
# so we have to work around that
if max_value <= 1.0 and step < 1.0:
min_value /= step
max_value /= step
value /= step
# Custom dynamic properties
slider.label = label
slider.step = step
slider.last_value = None
slider.setMinimum(int(min_value))
slider.setMaximum(int(max_value))
slider.setValue(int(value))
slider.setMinimumWidth(200)
real_value = value
if slider.step < 1.0:
real_value = value * step
if isinstance(real_value, float):
label.setText(f'{text}: {real_value:.{label_precision}}')
else:
label.setText(f'{text}: {real_value}')
if ticks is None:
ticks = {'interval':2, 'position':'below'}
#slider.setFocusPolicy(Qt.StrongFocus)
slider.setTickPosition(QtWidgets.QSlider.TicksBelow)
if ticks.get('interval') is not None:
slider.setTickInterval(ticks.get('interval'))
if step is not None:
slider.setSingleStep(max(1, step))
def update_slider_label(value):
real_value = value
if slider.step < 1.0:
real_value = value * step
if isinstance(real_value, float):
label.setText(f'{text}: {real_value:.{label_precision}}')
else:
label.setText(f'{text}: {real_value}')
if change_function is not None:
change_function(real_value)
if auto_render:
self.viewer.render()
slider.last_value = real_value
slider.valueChanged.connect(update_slider_label)
if layout is not None:
layout.addWidget(label)
layout.addWidget(slider)
return slider
[docs]class ViewerApp(Qt.QApplication):
"""
This is the main entry point to start a Qt application.
"""
def __init__(self):
super().__init__(sys.argv)
self.window = ViewerWindow()
self.aboutToQuit.connect(self.window.onClose)
[docs] def initialize(self, viewer=None, callable=None, stats_callable=None, dark_mode=True, **kwargs):
if dark_mode:
# Other dark-style Qt stylesheets exist but few get it right
from darktheme.widget_template import DarkPalette
self.setStyle('Fusion')
self.setPalette(DarkPalette())
# Handling disabled states with the custom stylesheet below
self.setStyleSheet("QToolTip { color: #ffffff; background-color: grey; border: 1px solid white; }"
"QCheckBox:disabled {color:#999999;}"
"QSlider::sub-page:disabled {background:#999999;}"
"QRadioButton:disabled {color:#999999;}"
"QWidget:disabled {color:#999999;}")
Qt.QApplication.setOverrideCursor(QtCore.Qt.WaitCursor)
self.window.initialize(viewer, callable, stats_callable, dark_mode, **kwargs)
Qt.QApplication.restoreOverrideCursor()
self.exec_()
[docs]def main():
app = ViewerApp(dark_mode=False)
if __name__ == "__main__":
main()