ScanSlicer/scanslicer/ui_builders_shared.py

236 lines
8.2 KiB
Python
Raw Normal View History

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,
)
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"
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)
class ScrollbarWheelOnlyScrollArea(QScrollArea):
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)
class ScanSlicerUiSharedMixin:
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
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,
)
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
def _add_full_width_widget(self, target_layout, row_index, widget):
target_layout.addWidget(widget, row_index, 0, 1, 2)
return row_index + 1
def _add_spacer_row(self, target_layout, row_index):
self._add_vertical_spacer(target_layout, row_index)
return row_index + 1
def _add_label_row(self, target_layout, row_index, text):
return self._add_full_width_widget(target_layout, row_index, QLabel(text))
def _create_panel_button(self, text, callback, tooltip=None):
button = QPushButton(text)
if tooltip:
button.setToolTip(tooltip)
button.clicked.connect(callback)
return button
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)
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)
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",
]