ScanSlicer/scanslicer/ui_builders_shared.py

250 lines
9.2 KiB
Python

import base64
from PySide6.QtCore import QPoint, QRect, Qt
from PySide6.QtGui import QFont, QFontInfo, QIcon, QPainter, QPixmap
from PySide6.QtSvg import QSvgRenderer
from PySide6.QtWidgets import (
QComboBox,
QGridLayout,
QGroupBox,
QHBoxLayout,
QLabel,
QPushButton,
QScrollArea,
QSizePolicy,
QSlider,
QSpacerItem,
QVBoxLayout,
QWidget,
)
# _icon_color_from_palette: function definition for ScanSlicer behavior.
def _icon_color_from_palette(palette):
if palette is None:
return "#f2f2f2"
button_text_color = palette.buttonText().color()
if button_text_color.isValid():
return button_text_color.name()
window_color = palette.window().color()
luma = (
(0.2126 * window_color.redF())
+ (0.7152 * window_color.greenF())
+ (0.0722 * window_color.blueF())
)
return "#1f1f1f" if luma >= 0.5 else "#f2f2f2"
# icon_from_svg_data_url: function definition for ScanSlicer behavior.
def icon_from_svg_data_url(data_url, size=24, color_hex=None, palette=None):
prefix = "data:image/svg+xml;base64,"
if not data_url.startswith(prefix):
return None
try:
svg_bytes = base64.b64decode(data_url[len(prefix):], validate=True)
except Exception:
return None
if color_hex is None:
color_hex = _icon_color_from_palette(palette)
# Most toolbar SVGs use "currentColor"; replace it with the resolved color.
if color_hex:
try:
svg_text = svg_bytes.decode("utf-8")
svg_text = svg_text.replace("currentColor", color_hex)
svg_text = svg_text.replace("currentcolor", color_hex)
svg_bytes = svg_text.encode("utf-8")
except UnicodeDecodeError:
pass
renderer = QSvgRenderer(svg_bytes)
if not renderer.isValid():
return None
pixmap = QPixmap(size, size)
pixmap.fill(Qt.GlobalColor.transparent)
painter = QPainter(pixmap)
renderer.render(painter)
painter.end()
return QIcon(pixmap)
# ScrollbarWheelOnlyScrollArea: class definition for ScanSlicer behavior.
class ScrollbarWheelOnlyScrollArea(QScrollArea):
# wheelEvent: function definition for ScanSlicer behavior.
def wheelEvent(self, event):
scrollbar = self.verticalScrollBar()
if scrollbar is None or not scrollbar.isVisible():
event.ignore()
return
global_pos = event.globalPosition().toPoint()
scrollbar_rect = QRect(scrollbar.mapToGlobal(QPoint(0, 0)), scrollbar.size())
if not scrollbar_rect.contains(global_pos):
event.ignore()
return
super().wheelEvent(event)
# ScanSlicerUiSharedMixin: class definition for ScanSlicer behavior.
class ScanSlicerUiSharedMixin:
# _build_section_title: function definition for ScanSlicer behavior.
def _build_section_title(self, text):
label = QLabel(text)
title_font = QFont(label.font())
title_font.setPixelSize(self.SECTION_TITLE_FONT_PX)
title_font.setBold(True)
label.setFont(title_font)
label.setStyleSheet(
f"padding-top: {self.SECTION_TITLE_VPAD}px; "
f"padding-bottom: {self.SECTION_TITLE_VPAD}px;"
)
return label
# _add_vertical_spacer: function definition for ScanSlicer behavior.
def _add_vertical_spacer(self, target_layout, row_index):
target_layout.addItem(
QSpacerItem(
0,
self.VERTICAL_SPACER_HEIGHT,
QSizePolicy.Policy.Minimum,
QSizePolicy.Policy.Fixed,
),
row_index,
0,
1,
2,
)
# _new_group_layout: function definition for ScanSlicer behavior.
def _new_group_layout(self):
group = QGroupBox()
group_layout = QGridLayout(group)
group_layout.setContentsMargins(
self.PANEL_CONTENT_MARGIN,
self.PANEL_CONTENT_MARGIN,
self.PANEL_CONTENT_MARGIN,
self.PANEL_CONTENT_MARGIN,
)
return group, group_layout
# _add_full_width_widget: function definition for ScanSlicer behavior.
def _add_full_width_widget(self, target_layout, row_index, widget):
target_layout.addWidget(widget, row_index, 0, 1, 2)
return row_index + 1
# _add_spacer_row: function definition for ScanSlicer behavior.
def _add_spacer_row(self, target_layout, row_index):
self._add_vertical_spacer(target_layout, row_index)
return row_index + 1
# _add_label_row: function definition for ScanSlicer behavior.
def _add_label_row(self, target_layout, row_index, text):
return self._add_full_width_widget(target_layout, row_index, QLabel(text))
# _create_panel_button: function definition for ScanSlicer behavior.
def _create_panel_button(self, text, callback, tooltip=None):
button = QPushButton(text)
if tooltip:
button.setToolTip(tooltip)
button.clicked.connect(callback)
return button
# _build_parameter_block: function definition for ScanSlicer behavior.
def _build_parameter_block(self, parent, name, parameter):
param_block = QWidget(parent)
param_block_layout = QVBoxLayout(param_block)
param_block_layout.setContentsMargins(0, 0, 0, self.VERTICAL_SPACER_HEIGHT)
param_block_layout.setSpacing(3)
param_label = QLabel(parameter.label)
param_label.setWordWrap(True)
param_block_layout.addWidget(param_label)
if parameter.label_secondary:
param_secondary_label = QLabel(parameter.label_secondary)
param_secondary_label.setWordWrap(True)
secondary_font = QFont(param_secondary_label.font())
secondary_info = QFontInfo(secondary_font)
if secondary_info.pixelSize() > 0:
secondary_font.setPixelSize(max(1, secondary_info.pixelSize() - 2))
elif secondary_info.pointSizeF() > 0:
secondary_font.setPointSizeF(max(1.0, secondary_info.pointSizeF() - 1.0))
else:
secondary_font.setPointSize(9)
param_secondary_label.setFont(secondary_font)
param_secondary_label.setStyleSheet("color: gray;")
param_block_layout.addWidget(param_secondary_label)
if name in self.CHOICE_PARAM_OPTIONS:
control = QComboBox(param_block)
for option_label, option_value in self.CHOICE_PARAM_OPTIONS[name]:
control.addItem(option_label, option_value)
choice_index = control.findData(parameter.get())
if choice_index >= 0:
control.setCurrentIndex(choice_index)
# on_param_choice_changed: function definition for ScanSlicer behavior.
def on_param_choice_changed(index, p=parameter, combo=control, param_name=name):
value = combo.itemData(index)
p.set(value)
if param_name in ("bw_method", "detect_pipeline", "morph_op"):
self.update_parameter_visibility()
if not self.updating_param_controls:
self.set_preset_dropdown_custom()
control.currentIndexChanged.connect(on_param_choice_changed)
param_block_layout.addWidget(control)
else:
slider_row = QWidget(param_block)
slider_row_layout = QHBoxLayout(slider_row)
slider_row_layout.setContentsMargins(0, 0, 0, 0)
slider_row_layout.setSpacing(6)
control = QSlider(Qt.Orientation.Horizontal)
control.setRange(parameter.min, parameter.max)
control.setSingleStep(max(1, parameter.step))
control.setPageStep(max(1, parameter.step))
control.setTickInterval(max(1, parameter.step))
control.setValue(parameter.get())
value_label = QLabel(str(parameter.get()))
value_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
value_label.setMinimumWidth(40)
# on_param_slider_changed: function definition for ScanSlicer behavior.
def on_param_slider_changed(value, p=parameter, slider=control, label=value_label, param_name=name):
step = max(1, p.step)
snapped = p.min + round((value - p.min) / step) * step
snapped = min(p.max, max(p.min, snapped))
if snapped != value:
slider.blockSignals(True)
slider.setValue(snapped)
slider.blockSignals(False)
value = snapped
p.set(value)
label.setText(str(p.get()))
if param_name in ("bw_method", "detect_pipeline", "morph_op"):
self.update_parameter_visibility()
if not self.updating_param_controls:
self.set_preset_dropdown_custom()
control.valueChanged.connect(on_param_slider_changed)
slider_row_layout.addWidget(control, 1)
slider_row_layout.addWidget(value_label, 0)
param_block_layout.addWidget(slider_row)
self.param_controls[name] = control
self.param_blocks[name] = param_block
return param_block
__all__ = [
"ScanSlicerUiSharedMixin",
"ScrollbarWheelOnlyScrollArea",
"icon_from_svg_data_url",
]