Skip to content

Commit 6a8b465

Browse files
author
Shengjie Xu
committed
[GUI] Initial code for imagepack widget
1 parent 505989e commit 6a8b465

File tree

5 files changed

+366
-13
lines changed

5 files changed

+366
-13
lines changed

src/preppipe/util/imagepack.py

+7-2
Original file line numberDiff line numberDiff line change
@@ -2975,15 +2975,20 @@ def get_heading_for_type(cls) -> str:
29752975
zh_cn="背景模板",
29762976
zh_hk="背景模板",
29772977
)
2978-
_tr_heading_background = TR_docs.tr("heading_background",
2978+
tr_heading_background = TR_docs.tr("heading_background",
29792979
en="Background",
29802980
zh_cn="背景",
29812981
zh_hk="背景",
29822982
)
2983+
tr_heading_charactersprite = TR_docs.tr("heading_charactersprite",
2984+
en="Character Sprite",
2985+
zh_cn="立绘",
2986+
zh_hk="立繪",
2987+
)
29832988

29842989
heading_list = [
29852990
_tr_heading_background_template,
2986-
_tr_heading_background,
2991+
tr_heading_background,
29872992
]
29882993

29892994
@classmethod
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
from PySide6.QtCore import Qt, QTimer, QPoint, QEvent
2+
from PySide6.QtGui import QPixmap, QMouseEvent, QWheelEvent, QContextMenuEvent, QTransform, QPainter
3+
from PySide6.QtWidgets import (
4+
QApplication,
5+
QGraphicsView,
6+
QGraphicsScene,
7+
QGraphicsPixmapItem,
8+
QSlider,
9+
QMenu,
10+
QFileDialog,
11+
QWidget,
12+
QVBoxLayout,
13+
)
14+
from preppipe.language import *
15+
16+
class ImageViewerWidget(QGraphicsView):
17+
TR_gui_imageviewerwidget = TranslationDomain("gui_imageviewerwidget")
18+
_tr_export_as_png = TR_gui_imageviewerwidget.tr("export_as_png",
19+
en="Export as PNG",
20+
zh_cn="导出为 PNG",
21+
zh_hk="匯出為 PNG",
22+
)
23+
def __init__(self, parent: QWidget = None, context_menu_callback=None):
24+
super().__init__(parent)
25+
# Set smooth rendering
26+
self.setRenderHints(QPainter.Antialiasing | QPainter.SmoothPixmapTransform)
27+
self.setMouseTracking(True) # for slider show/hide on mouse moves
28+
29+
# Set up the scene and pixmap item.
30+
self._scene = QGraphicsScene(self)
31+
self.setScene(self._scene)
32+
self._pixmap_item = QGraphicsPixmapItem()
33+
self._pixmap_item.setTransformationMode(Qt.SmoothTransformation)
34+
self._scene.addItem(self._pixmap_item)
35+
self._pixmap = None # currently displayed QPixmap
36+
37+
# Zoom factor (1.0 corresponds to 100%).
38+
self._current_scale = 1.0
39+
40+
# Transformation anchor for smooth zooming.
41+
self.setTransformationAnchor(QGraphicsView.AnchorUnderMouse)
42+
self.setDragMode(QGraphicsView.NoDrag) # we implement our own panning
43+
44+
# For panning: record last mouse position when left button is pressed.
45+
self._last_mouse_pos = None
46+
47+
# Create a horizontal slider for zoom control.
48+
self.slider = QSlider(Qt.Horizontal, self)
49+
self.slider.setMinimum(2)
50+
self.slider.setMaximum(200)
51+
self.slider.setValue(100)
52+
self.slider.valueChanged.connect(self.on_slider_value_changed)
53+
self.slider.hide() # initially hidden
54+
55+
# Timer to hide the slider after 1 second of inactivity.
56+
self._slider_timer = QTimer(self)
57+
self._slider_timer.setInterval(1000)
58+
self._slider_timer.setSingleShot(True)
59+
self._slider_timer.timeout.connect(self.slider.hide)
60+
61+
# Callback for additional context menu actions.
62+
self.context_menu_callback = context_menu_callback
63+
64+
def resizeEvent(self, event):
65+
# Position the slider at the bottom with a margin.
66+
margin = 10
67+
slider_height = 20
68+
self.slider.setGeometry(
69+
margin,
70+
self.height() - slider_height - margin,
71+
self.width() - 2 * margin,
72+
slider_height,
73+
)
74+
super().resizeEvent(event)
75+
76+
def on_slider_value_changed(self, value: int):
77+
# Convert slider percentage to scale factor.
78+
scale = value / 100.0
79+
self.set_zoom(scale)
80+
81+
def set_zoom(self, scale: float):
82+
"""Update zoom factor while preserving the current view center."""
83+
# Remember the current center in scene coordinates.
84+
center = self.mapToScene(self.viewport().rect().center())
85+
self._current_scale = scale
86+
# Set the new transformation.
87+
self.setTransform(QTransform().scale(scale, scale))
88+
# Restore the center.
89+
self.centerOn(center)
90+
91+
def wheelEvent(self, event: QWheelEvent):
92+
# Show the slider
93+
self.slider.show()
94+
self._slider_timer.start() # restart timer on every move
95+
# Zoom in/out with the mouse wheel.
96+
delta = event.angleDelta().y()
97+
factor = 1.0 + delta / 240.0 # Adjust zoom sensitivity as needed.
98+
new_scale = self._current_scale * factor
99+
# Clamp scale to slider's min/max.
100+
min_scale = self.slider.minimum() / 100.0
101+
max_scale = self.slider.maximum() / 100.0
102+
new_scale = max(min_scale, min(new_scale, max_scale))
103+
self._current_scale = new_scale
104+
105+
# Update the slider without triggering its signal.
106+
self.slider.blockSignals(True)
107+
self.slider.setValue(int(new_scale * 100))
108+
self.slider.blockSignals(False)
109+
110+
self.set_zoom(new_scale)
111+
112+
def mousePressEvent(self, event: QMouseEvent):
113+
if event.button() == Qt.LeftButton:
114+
# Begin panning.
115+
self._last_mouse_pos = event.pos()
116+
event.accept()
117+
else:
118+
super().mousePressEvent(event)
119+
120+
def mouseMoveEvent(self, event: QMouseEvent):
121+
if event.buttons() & Qt.LeftButton and self._last_mouse_pos is not None:
122+
# Calculate movement delta.
123+
delta = event.pos() - self._last_mouse_pos
124+
self._last_mouse_pos = event.pos()
125+
# Adjust the scrollbars to pan.
126+
self.horizontalScrollBar().setValue(self.horizontalScrollBar().value() - delta.x())
127+
self.verticalScrollBar().setValue(self.verticalScrollBar().value() - delta.y())
128+
event.accept()
129+
else:
130+
super().mouseMoveEvent(event)
131+
132+
def mouseReleaseEvent(self, event: QMouseEvent):
133+
if event.button() == Qt.LeftButton:
134+
self._last_mouse_pos = None
135+
super().mouseReleaseEvent(event)
136+
137+
def setImage(self, pixmap: QPixmap):
138+
"""
139+
Set a new image.
140+
If the new image has the same dimensions as the current one,
141+
the view position (pan/zoom) is preserved.
142+
Otherwise, the view is reset.
143+
"""
144+
if self._pixmap is not None:
145+
if self._pixmap.size() == pixmap.size():
146+
# Same dimensions: update the pixmap without resetting view.
147+
self._pixmap = pixmap
148+
self._pixmap_item.setPixmap(pixmap)
149+
return
150+
# New dimensions or first image: update and reset view.
151+
self._pixmap = pixmap
152+
self._pixmap_item.setPixmap(pixmap)
153+
self._scene.setSceneRect(pixmap.rect())
154+
155+
# Reset zoom factor to 100%.
156+
self._current_scale = 1.0
157+
self.slider.blockSignals(True)
158+
self.slider.setValue(100)
159+
self.slider.blockSignals(False)
160+
self.setTransform(QTransform().scale(1.0, 1.0))
161+
self.centerOn(self._pixmap_item)
162+
163+
def contextMenuEvent(self, event: QContextMenuEvent):
164+
"""Open a context menu on right-click."""
165+
menu = QMenu(self)
166+
export_action = menu.addAction(self._tr_export_as_png.get())
167+
# Allow outside code to add custom actions.
168+
if self.context_menu_callback:
169+
self.context_menu_callback(menu)
170+
# Execute the menu.
171+
action = menu.exec(event.globalPos())
172+
if action == export_action:
173+
self.export_as_png()
174+
175+
def export_as_png(self):
176+
"""Open a file dialog to export the current image as a PNG file."""
177+
file_path, _ = QFileDialog.getSaveFileName(self, "Save Image", "", "PNG Files (*.png)")
178+
if file_path:
179+
if not file_path.lower().endswith(".png"):
180+
file_path += ".png"
181+
# Save the original QPixmap.
182+
self._pixmap.save(file_path, "PNG")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<ui version="4.0">
3+
<class>ImagePackWidget</class>
4+
<widget class="QWidget" name="ImagePackWidget">
5+
<property name="geometry">
6+
<rect>
7+
<x>0</x>
8+
<y>0</y>
9+
<width>887</width>
10+
<height>533</height>
11+
</rect>
12+
</property>
13+
<property name="windowTitle">
14+
<string>Form</string>
15+
</property>
16+
<layout class="QVBoxLayout" name="verticalLayout_3">
17+
<item>
18+
<widget class="QSplitter" name="splitter">
19+
<property name="orientation">
20+
<enum>Qt::Orientation::Horizontal</enum>
21+
</property>
22+
<widget class="QFrame" name="viewerFrame">
23+
<property name="sizePolicy">
24+
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
25+
<horstretch>0</horstretch>
26+
<verstretch>0</verstretch>
27+
</sizepolicy>
28+
</property>
29+
<property name="frameShape">
30+
<enum>QFrame::Shape::StyledPanel</enum>
31+
</property>
32+
<property name="frameShadow">
33+
<enum>QFrame::Shadow::Raised</enum>
34+
</property>
35+
<layout class="QVBoxLayout" name="viewerLayout">
36+
<property name="leftMargin">
37+
<number>0</number>
38+
</property>
39+
<property name="topMargin">
40+
<number>0</number>
41+
</property>
42+
<property name="rightMargin">
43+
<number>0</number>
44+
</property>
45+
<property name="bottomMargin">
46+
<number>0</number>
47+
</property>
48+
</layout>
49+
</widget>
50+
<widget class="QGroupBox" name="controlAreaGroupBox">
51+
<property name="flat">
52+
<bool>true</bool>
53+
</property>
54+
<layout class="QVBoxLayout" name="verticalLayout">
55+
<item>
56+
<widget class="QGroupBox" name="sourceGroupBox">
57+
<property name="title">
58+
<string>图片选择</string>
59+
</property>
60+
</widget>
61+
</item>
62+
<item>
63+
<widget class="QGroupBox" name="forkParamGroupBox">
64+
<property name="title">
65+
<string>选区参数</string>
66+
</property>
67+
</widget>
68+
</item>
69+
<item>
70+
<spacer name="verticalSpacer">
71+
<property name="orientation">
72+
<enum>Qt::Orientation::Vertical</enum>
73+
</property>
74+
<property name="sizeHint" stdset="0">
75+
<size>
76+
<width>20</width>
77+
<height>40</height>
78+
</size>
79+
</property>
80+
</spacer>
81+
</item>
82+
<item>
83+
<widget class="QLabel" name="infoLabel">
84+
<property name="text">
85+
<string>说明信息</string>
86+
</property>
87+
<property name="wordWrap">
88+
<bool>true</bool>
89+
</property>
90+
</widget>
91+
</item>
92+
</layout>
93+
</widget>
94+
</widget>
95+
</item>
96+
</layout>
97+
</widget>
98+
<resources/>
99+
<connections/>
100+
</ui>

src/preppipe_gui_pyside6/navigatorwidget.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ class ToolNode:
2424
NAVIGATION_LIST : typing.ClassVar[list] = [
2525
SettingWidget,
2626
MainInputWidget,
27-
ImagePackBackgroundTool,
27+
(ImagePackTool, {"category_kind": ImagePackDescriptor.ImagePackType.BACKGROUND}),
28+
(ImagePackTool, {"category_kind": ImagePackDescriptor.ImagePackType.CHARACTER}),
2829
]
2930

3031
def __init__(self, /, info: ToolWidgetInfo | None, parent: ToolNode | None = None):

0 commit comments

Comments
 (0)