590 lines
21 KiB
Python
590 lines
21 KiB
Python
from PyQt6.QtWidgets import (
|
||
QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel,
|
||
QFrame, QGridLayout, QSizePolicy, QLineEdit, QApplication,
|
||
)
|
||
from PyQt6.QtCore import Qt, QTimer, QPointF, QRectF, QEvent
|
||
from PyQt6.QtGui import QFont, QPainter, QColor, QBrush, QPen, QDoubleValidator
|
||
from utils.config_parse import ConfigParse
|
||
from utils.floating_keyboard import FloatingKeyboard
|
||
|
||
MOVE_STEP = 10 # 每次点击移动 mm (保留备用)
|
||
|
||
MOVE_DETECT_MS = 300
|
||
|
||
|
||
class JoystickWidget(QWidget):
|
||
"""圆形 XY 摇杆 — 拖动小圆控制方向,松开回中,越远越快"""
|
||
|
||
def __init__(self, parent=None, max_speed=3000):
|
||
super().__init__(parent)
|
||
self.setFixedSize(320, 320)
|
||
self.setMouseTracking(True)
|
||
|
||
self._max_speed = max_speed # 最大进给速度 mm/min
|
||
self._radius_outer = 140 # 外圈半径
|
||
self._radius_inner = 30 # 内圈小圆半径
|
||
self._deadzone = 10 # 死区像素
|
||
|
||
# 当前小圆偏移(相对中心,像素)
|
||
self._dx = 0.0
|
||
self._dy = 0.0
|
||
self._pressed = False
|
||
|
||
# 定时发送 GCode
|
||
self._move_timer = QTimer(self)
|
||
self._move_timer.setInterval(MOVE_DETECT_MS)
|
||
self._move_timer.timeout.connect(self._emit_move)
|
||
|
||
# 回调(上层设置)
|
||
self.on_move = None # callable(dx_norm, dy_norm, feedrate)
|
||
|
||
def _emit_move(self):
|
||
"""定时器触发:计算速度方向并回调"""
|
||
dist = (self._dx ** 2 + self._dy ** 2) ** 0.5
|
||
if dist < self._deadzone:
|
||
return
|
||
ratio = min(dist / (self._radius_outer - self._radius_inner), 1.0)
|
||
# 速度映射:30% ~ 100% * max_speed
|
||
# speed = int((0.3 + ratio * 0.7) * self._max_speed)
|
||
# ratio = min(dist / max_dist, 1.0)
|
||
speed = ratio * self._max_speed / (1000/MOVE_DETECT_MS)
|
||
if dist > 0:
|
||
nx = self._dx / dist
|
||
ny = self._dy / dist
|
||
if self.on_move:
|
||
self.on_move(nx, ny, speed)
|
||
|
||
def paintEvent(self, event):
|
||
painter = QPainter(self)
|
||
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
|
||
cx, cy = self.width() / 2, self.height() / 2
|
||
|
||
# 外圈背景
|
||
painter.setBrush(QBrush(QColor("#2a2a2a")))
|
||
painter.setPen(QPen(QColor("#666666"), 3))
|
||
painter.drawEllipse(QPointF(cx, cy), self._radius_outer, self._radius_outer)
|
||
|
||
# 十字准线(浅)
|
||
painter.setPen(QPen(QColor("#555555"), 1))
|
||
painter.drawLine(int(cx - self._radius_outer), int(cy),
|
||
int(cx + self._radius_outer), int(cy))
|
||
painter.drawLine(int(cx), int(cy - self._radius_outer),
|
||
int(cx), int(cy + self._radius_outer))
|
||
|
||
# 小圆(可拖动)
|
||
knob_x = cx + self._dx
|
||
knob_y = cy + self._dy
|
||
painter.setBrush(QBrush(QColor("#4a9fc8" if self._pressed else "#6fb8dd")))
|
||
painter.setPen(QPen(QColor("#2f6f91"), 2))
|
||
painter.drawEllipse(QPointF(knob_x, knob_y), self._radius_inner, self._radius_inner)
|
||
|
||
# 中心小点标记
|
||
painter.setBrush(QBrush(QColor("#888888")))
|
||
painter.setPen(Qt.PenStyle.NoPen)
|
||
painter.drawEllipse(QPointF(cx, cy), 4, 4)
|
||
|
||
def mousePressEvent(self, event):
|
||
if event.button() == Qt.MouseButton.LeftButton:
|
||
self._pressed = True
|
||
self._update_knob(event.position())
|
||
self._move_timer.start()
|
||
|
||
def mouseMoveEvent(self, event):
|
||
if self._pressed:
|
||
self._update_knob(event.position())
|
||
|
||
def mouseReleaseEvent(self, event):
|
||
if event.button() == Qt.MouseButton.LeftButton:
|
||
self._pressed = False
|
||
self._move_timer.stop()
|
||
self._dx = 0.0
|
||
self._dy = 0.0
|
||
self.update()
|
||
|
||
def _update_knob(self, pos):
|
||
cx, cy = self.width() / 2, self.height() / 2
|
||
dx = pos.x() - cx
|
||
dy = pos.y() - cy
|
||
max_r = self._radius_outer - self._radius_inner
|
||
dist = (dx ** 2 + dy ** 2) ** 0.5
|
||
if dist > max_r:
|
||
dx = dx / dist * max_r
|
||
dy = dy / dist * max_r
|
||
self._dx = dx
|
||
self._dy = dy
|
||
self.update()
|
||
|
||
|
||
class ZFaderWidget(QWidget):
|
||
"""Z 轴拨杆 — 上下拖动控制 Z 速度,松开回中,越远越快"""
|
||
|
||
def __init__(self, parent=None, max_speed=200):
|
||
super().__init__(parent)
|
||
self.setFixedSize(120, 320)
|
||
self.setMouseTracking(True)
|
||
|
||
self._max_speed = max_speed
|
||
self._track_h = 260 # 滑道高度
|
||
self._knob_h = 40 # 滑块高度
|
||
self._knob_w = 80 # 滑块宽度
|
||
self._deadzone = 8
|
||
|
||
self._dy = 0.0 # 相对中心的偏移
|
||
self._pressed = False
|
||
|
||
self._move_timer = QTimer(self)
|
||
self._move_timer.setInterval(MOVE_DETECT_MS)
|
||
self._move_timer.timeout.connect(self._emit_move)
|
||
|
||
self.on_move = None # callable(direction, feedrate) direction: +1 or -1
|
||
|
||
def _emit_move(self):
|
||
dist = abs(self._dy)
|
||
if dist < self._deadzone:
|
||
return
|
||
max_dist = (self._track_h - self._knob_h) / 2
|
||
ratio = min(dist / max_dist, 1.0)
|
||
# speed = int((0.3 + ratio * 0.7) * self._max_speed / 10)
|
||
speed = ratio * self._max_speed / (1000/MOVE_DETECT_MS)
|
||
direction = 1 if self._dy < 0 else -1 # dy<0 → 向上
|
||
if self.on_move:
|
||
self.on_move(direction, speed)
|
||
|
||
def paintEvent(self, event):
|
||
painter = QPainter(self)
|
||
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
|
||
cx = self.width() / 2
|
||
|
||
# 滑道
|
||
rect = QRectF(cx - 12, (self.height() - self._track_h) / 2, 24, self._track_h)
|
||
painter.setBrush(QBrush(QColor("#2a2a2a")))
|
||
painter.setPen(QPen(QColor("#666666"), 2))
|
||
painter.drawRoundedRect(rect, 12, 12)
|
||
|
||
# 中心刻度线
|
||
mid_y = self.height() / 2
|
||
painter.setPen(QPen(QColor("#777777"), 2))
|
||
painter.drawLine(int(cx - 20), int(mid_y), int(cx + 20), int(mid_y))
|
||
|
||
# 滑块
|
||
knob_center_y = mid_y + self._dy
|
||
knob_y = knob_center_y - self._knob_h / 2
|
||
knob_rect = QRectF(cx - self._knob_w / 2, knob_y, self._knob_w, self._knob_h)
|
||
|
||
color = "#4a9fc8" if self._pressed else "#6fb8dd"
|
||
painter.setBrush(QBrush(QColor(color)))
|
||
painter.setPen(QPen(QColor("#2f6f91"), 2))
|
||
painter.drawRoundedRect(knob_rect, 10, 10)
|
||
|
||
# Z 标签
|
||
painter.setPen(QPen(QColor("#a0a0a0"), 1))
|
||
font = QFont("sans-serif", 14, QFont.Weight.Bold)
|
||
painter.setFont(font)
|
||
painter.drawText(knob_rect, Qt.AlignmentFlag.AlignCenter, "Z")
|
||
|
||
def mousePressEvent(self, event):
|
||
if event.button() == Qt.MouseButton.LeftButton:
|
||
self._pressed = True
|
||
self._update_knob(event.position().y())
|
||
self._move_timer.start()
|
||
|
||
def mouseMoveEvent(self, event):
|
||
if self._pressed:
|
||
self._update_knob(event.position().y())
|
||
|
||
def mouseReleaseEvent(self, event):
|
||
if event.button() == Qt.MouseButton.LeftButton:
|
||
self._pressed = False
|
||
self._move_timer.stop()
|
||
self._dy = 0.0
|
||
self.update()
|
||
|
||
def _update_knob(self, py):
|
||
mid_y = self.height() / 2
|
||
max_d = (self._track_h - self._knob_h) / 2
|
||
dy = py - mid_y
|
||
dy = max(-max_d, min(max_d, dy))
|
||
self._dy = dy
|
||
self.update()
|
||
|
||
|
||
class ControlPage(QWidget):
|
||
def __init__(self, api_client, parent=None):
|
||
super().__init__(parent)
|
||
self.api_client = api_client
|
||
self.config_parser = ConfigParse()
|
||
self.config_parser.config_changed.connect(self._on_config_changed)
|
||
self._load_limits()
|
||
self._load_speeds()
|
||
|
||
# 当前坐标(本地跟踪)
|
||
self.pos_x = 0.0
|
||
self.pos_y = 0.0
|
||
self.pos_z = 0.0
|
||
self._is_printing = False
|
||
self._homed = False # 是否已轴回零
|
||
self._motor_on = True # 电机是否已使能(默认True,关电机后置False)
|
||
|
||
# 归零后的停留位置
|
||
hp = self.config_parser.home_positions or {}
|
||
self._home_x = hp.get("x", 0.0)
|
||
self._home_y = hp.get("y", 0.0)
|
||
self._home_z = hp.get("z", 0.0)
|
||
|
||
# 悬浮键盘
|
||
self._keyboard = FloatingKeyboard()
|
||
self._keyboard_attached = False
|
||
|
||
self.init_ui()
|
||
self._sync_inputs()
|
||
self._apply_state()
|
||
self._start_status_poll()
|
||
|
||
def _load_speeds(self):
|
||
ms = self.config_parser.move_max_speed or {}
|
||
self.move_speed_xy = ms.get("xy", 3000)
|
||
self.move_speed_z = ms.get("z", 200)
|
||
|
||
def _start_status_poll(self):
|
||
self._status_timer = QTimer(self)
|
||
self._status_timer.timeout.connect(self._poll_status)
|
||
self._status_timer.start(2000)
|
||
|
||
def _poll_status(self):
|
||
"""定时轮询打印机状态,更新按钮启用状态和坐标"""
|
||
try:
|
||
data = self.api_client.get_status()
|
||
flags = data.get("status", {}).get("state", {}).get("flags", {})
|
||
printing = flags.get("printing", False)
|
||
paused = flags.get("paused", False)
|
||
self._is_printing = printing or paused
|
||
except Exception:
|
||
self._is_printing = False
|
||
|
||
self._update_buttons()
|
||
|
||
def _load_limits(self):
|
||
ma = self.config_parser.move_axis_area or {}
|
||
self.x_min = ma.get("x_min", 0)
|
||
self.x_max = ma.get("x_max", 300)
|
||
self.y_min = ma.get("y_min", 0)
|
||
self.y_max = ma.get("y_max", 300)
|
||
self.z_min = ma.get("z_min", 0)
|
||
self.z_max = ma.get("z_max", 300)
|
||
|
||
def _on_config_changed(self, config_instance):
|
||
self._load_limits()
|
||
self._load_speeds()
|
||
|
||
# ── 状态管理 ──────────────────────────────────────────
|
||
|
||
def _is_control_enabled(self):
|
||
"""摇杆/拨杆/坐标输入是否可用:已回零 + 电机已使能 + 非打印中"""
|
||
return self._homed and self._motor_on and not self._is_printing
|
||
|
||
def _apply_state(self):
|
||
"""根据回零/电机状态同步所有控件"""
|
||
enabled = self._is_control_enabled()
|
||
self.joystick.setEnabled(enabled)
|
||
self.z_fader.setEnabled(enabled)
|
||
for inp in (self.input_x, self.input_y, self.input_z):
|
||
inp.setEnabled(enabled)
|
||
if not enabled:
|
||
inp.setText("---")
|
||
if enabled:
|
||
self._sync_inputs()
|
||
|
||
def _sync_inputs(self):
|
||
"""将 pos 值同步到输入框"""
|
||
if self._is_control_enabled():
|
||
self.input_x.setText(f"{self.pos_x:.3f}")
|
||
self.input_y.setText(f"{self.pos_y:.3f}")
|
||
self.input_z.setText(f"{self.pos_z:.3f}")
|
||
|
||
def _on_coord_changed(self):
|
||
"""输入框手动输入触发运动"""
|
||
if not self._is_control_enabled():
|
||
return
|
||
try:
|
||
tx = float(self.input_x.text())
|
||
ty = float(self.input_y.text())
|
||
tz = float(self.input_z.text())
|
||
except ValueError:
|
||
return
|
||
tx = max(self.x_min, min(self.x_max, tx))
|
||
ty = max(self.y_min, min(self.y_max, ty))
|
||
tz = max(self.z_min, min(self.z_max, tz))
|
||
gcode = f"G1 X{tx:.3f} Y{ty:.3f} Z{tz:.3f} F3000"
|
||
self.api_client.send_gcode(gcode)
|
||
self.pos_x, self.pos_y, self.pos_z = tx, ty, tz
|
||
self._sync_inputs()
|
||
|
||
def _update_buttons(self):
|
||
printing = self._is_printing
|
||
self.btn_pause.setEnabled(printing)
|
||
self.btn_stop.setEnabled(printing)
|
||
idle = not printing
|
||
self.btn_home.setEnabled(idle)
|
||
self.btn_level.setEnabled(idle)
|
||
self.btn_motor_off.setEnabled(idle)
|
||
self._apply_state()
|
||
|
||
# ── GCode 发送 ──────────────────────────────────────────
|
||
|
||
def _send_move(self, x=None, y=None, z=None, feedrate=None):
|
||
if not self._is_control_enabled():
|
||
return
|
||
target_x = self.pos_x if x is None else x
|
||
target_y = self.pos_y if y is None else y
|
||
target_z = self.pos_z if z is None else z
|
||
target_x = max(self.x_min, min(self.x_max, target_x))
|
||
target_y = max(self.y_min, min(self.y_max, target_y))
|
||
target_z = max(self.z_min, min(self.z_max, target_z))
|
||
f = feedrate or 3000
|
||
gcode = f"G1 X{target_x:.3f} Y{target_y:.3f} Z{target_z:.3f} F{f}"
|
||
self.api_client.send_gcode(gcode)
|
||
self.pos_x, self.pos_y, self.pos_z = target_x, target_y, target_z
|
||
self._sync_inputs()
|
||
|
||
# ── 摇杆/拨杆回调 ──
|
||
|
||
def _on_joystick_move(self, nx, ny, speed):
|
||
if not self._is_control_enabled():
|
||
return
|
||
step = speed * MOVE_DETECT_MS / 1000
|
||
dx = nx * step
|
||
dy = ny * step
|
||
tx = max(self.x_min, min(self.x_max, self.pos_x + dx))
|
||
ty = max(self.y_min, min(self.y_max, self.pos_y + dy))
|
||
gcode = f"G1 X{tx:.1f} Y{ty:.1f} F{speed}"
|
||
print(f"Moving: X{tx:.1f} Y{ty:.1f} F{speed}")
|
||
self.api_client.send_gcode(gcode)
|
||
self.pos_x, self.pos_y = tx, ty
|
||
self._sync_inputs()
|
||
|
||
def _on_fader_move(self, direction, speed):
|
||
if not self._is_control_enabled():
|
||
return
|
||
step = speed * MOVE_DETECT_MS / 1000
|
||
tz = self.pos_z + direction * step
|
||
tz = max(self.z_min, min(self.z_max, tz))
|
||
gcode = f"G1 Z{tz:.3f} F{speed}"
|
||
print(f"Moving: Z{tz:.3f} F{speed}")
|
||
self.api_client.send_gcode(gcode)
|
||
self.pos_z = tz
|
||
self._sync_inputs()
|
||
|
||
# ── 指令按钮 ──
|
||
|
||
def _cmd_pause(self):
|
||
self.api_client.pause_print()
|
||
|
||
def _cmd_stop(self):
|
||
self.api_client.stop_print()
|
||
|
||
def _cmd_home(self):
|
||
self.api_client.home_axes(["x", "y", "z"])
|
||
self.pos_x = self._home_x
|
||
self.pos_y = self._home_y
|
||
self.pos_z = self._home_z
|
||
self._homed = True
|
||
self._motor_on = True
|
||
self._sync_inputs()
|
||
self._apply_state()
|
||
|
||
def _cmd_level(self):
|
||
self.api_client.auto_leveling()
|
||
|
||
def _cmd_motor_off(self):
|
||
self.api_client.off_motors()
|
||
self._motor_on = False
|
||
self._apply_state()
|
||
|
||
# ── 悬浮键盘 ─────────────────────────────────────────────
|
||
|
||
def _attach_keyboard(self, widget):
|
||
"""将悬浮键盘绑定到指定输入框并显示"""
|
||
self._keyboard.attach(widget)
|
||
self._keyboard.show_below(widget)
|
||
self._keyboard_attached = True
|
||
|
||
def _dismiss_keyboard(self):
|
||
"""关闭悬浮键盘"""
|
||
self._keyboard.hide()
|
||
self._keyboard.detach()
|
||
self._keyboard_attached = False
|
||
|
||
def _on_input_focus_in(self, widget):
|
||
"""输入框获得焦点时的处理"""
|
||
if self._keyboard_attached:
|
||
self._keyboard.attach(widget)
|
||
self._keyboard.show_below(widget)
|
||
else:
|
||
self._attach_keyboard(widget)
|
||
|
||
def eventFilter(self, obj, event):
|
||
if event.type() == QEvent.Type.FocusIn:
|
||
if obj in (self.input_x, self.input_y, self.input_z):
|
||
self._on_input_focus_in(obj)
|
||
elif self._keyboard_attached and not isinstance(obj, QLineEdit):
|
||
self._dismiss_keyboard()
|
||
elif event.type() == QEvent.Type.FocusOut:
|
||
if obj in (self.input_x, self.input_y, self.input_z):
|
||
QTimer.singleShot(100, self._check_dismiss_keyboard)
|
||
return super().eventFilter(obj, event)
|
||
|
||
def _check_dismiss_keyboard(self):
|
||
"""检查当前焦点是否还在坐标输入框上,不在则关闭键盘"""
|
||
w = self.focusWidget()
|
||
if w not in (self.input_x, self.input_y, self.input_z):
|
||
self._dismiss_keyboard()
|
||
|
||
# ── UI 构建 ──────────────────────────────────────────────
|
||
|
||
def init_ui(self):
|
||
self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
|
||
|
||
main_layout = QHBoxLayout(self)
|
||
main_layout.setContentsMargins(14, 14, 14, 14)
|
||
main_layout.setSpacing(14)
|
||
|
||
# =========================== 左栏 ===========================
|
||
left_panel = QFrame()
|
||
left_panel.setStyleSheet("background-color: #3f3f3f; border-radius: 10px;")
|
||
left_panel.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
|
||
left_layout = QVBoxLayout(left_panel)
|
||
left_layout.setContentsMargins(16, 14, 16, 14)
|
||
left_layout.setSpacing(8)
|
||
|
||
left_title = QLabel("控制")
|
||
left_title.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||
left_title.setStyleSheet("color: #efefef; font-size: 22px; font-weight: 700;")
|
||
left_layout.addWidget(left_title)
|
||
|
||
btn_style = """
|
||
QPushButton {
|
||
min-height: 50px;
|
||
font-size: 18px;
|
||
font-weight: 600;
|
||
color: #f8f8f8;
|
||
background-color: #555555;
|
||
border: 2px solid #888888;
|
||
border-radius: 8px;
|
||
padding: 4px 12px;
|
||
}
|
||
QPushButton:hover {
|
||
background-color: #636363;
|
||
border-color: #aaaaaa;
|
||
}
|
||
QPushButton:pressed {
|
||
background-color: #3d3d3d;
|
||
border-color: #5a9fcf;
|
||
}
|
||
QPushButton:disabled {
|
||
color: #606060;
|
||
background-color: #3a3a3a;
|
||
border-color: #505050;
|
||
}
|
||
"""
|
||
|
||
grid_btns = QGridLayout()
|
||
grid_btns.setSpacing(8)
|
||
|
||
self.btn_pause = QPushButton("⏸ 暂停打印")
|
||
self.btn_pause.setStyleSheet(btn_style)
|
||
self.btn_pause.clicked.connect(self._cmd_pause)
|
||
grid_btns.addWidget(self.btn_pause, 0, 0)
|
||
|
||
self.btn_stop = QPushButton("⏹ 停止打印")
|
||
self.btn_stop.setStyleSheet(btn_style)
|
||
self.btn_stop.clicked.connect(self._cmd_stop)
|
||
grid_btns.addWidget(self.btn_stop, 0, 1)
|
||
|
||
self.btn_home = QPushButton("⌂ 轴回零")
|
||
self.btn_home.setStyleSheet(btn_style)
|
||
self.btn_home.clicked.connect(self._cmd_home)
|
||
grid_btns.addWidget(self.btn_home, 1, 0)
|
||
|
||
self.btn_level = QPushButton("◉ 自动调平")
|
||
self.btn_level.setStyleSheet(btn_style)
|
||
self.btn_level.clicked.connect(self._cmd_level)
|
||
grid_btns.addWidget(self.btn_level, 1, 1)
|
||
|
||
self.btn_motor_off = QPushButton("⏻ 关电机")
|
||
self.btn_motor_off.setStyleSheet(btn_style)
|
||
self.btn_motor_off.clicked.connect(self._cmd_motor_off)
|
||
grid_btns.addWidget(self.btn_motor_off, 2, 0, 1, 2)
|
||
|
||
left_layout.addLayout(grid_btns)
|
||
main_layout.addWidget(left_panel, 2)
|
||
|
||
# =========================== 右栏 ===========================
|
||
right_panel = QFrame()
|
||
right_panel.setStyleSheet("background-color: #3f3f3f; border-radius: 10px;")
|
||
right_panel.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
|
||
right_layout = QVBoxLayout(right_panel)
|
||
right_layout.setContentsMargins(16, 14, 16, 14)
|
||
right_layout.setSpacing(10)
|
||
|
||
# ── 坐标输入 ──
|
||
coord_row = QHBoxLayout()
|
||
coord_row.setSpacing(12)
|
||
coord_row.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||
|
||
input_style = """
|
||
QLineEdit {
|
||
min-width: 80px;
|
||
max-width: 120px;
|
||
min-height: 44px;
|
||
font-size: 24px;
|
||
font-weight: 700;
|
||
color: #e0e0e0;
|
||
background-color: #2a2a2a;
|
||
border: 2px solid #646464;
|
||
border-radius: 8px;
|
||
padding: 4px 10px;
|
||
text-align: center;
|
||
}
|
||
QLineEdit:focus {
|
||
border-color: #5a9fcf;
|
||
}
|
||
QLineEdit:disabled {
|
||
color: #606060;
|
||
background-color: #222222;
|
||
border-color: #444444;
|
||
}
|
||
"""
|
||
|
||
for axis, val in [("X", 0.0), ("Y", 0.0), ("Z", 0.0)]:
|
||
lbl = QLabel(f"{axis}:")
|
||
lbl.setStyleSheet("color: #d0d0d0; font-size: 24px; font-weight: 700;")
|
||
coord_row.addWidget(lbl)
|
||
|
||
inp = QLineEdit(f"{val:.1f}")
|
||
inp.setStyleSheet(input_style)
|
||
inp.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||
inp.setValidator(QDoubleValidator(-9999, 9999, 1))
|
||
inp.returnPressed.connect(self._on_coord_changed)
|
||
inp.installEventFilter(self)
|
||
setattr(self, f"input_{axis.lower()}", inp)
|
||
coord_row.addWidget(inp)
|
||
|
||
right_layout.addLayout(coord_row)
|
||
|
||
# ── 操作器 ──
|
||
manip_row = QHBoxLayout()
|
||
manip_row.setSpacing(14)
|
||
manip_row.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||
|
||
self.joystick = JoystickWidget(max_speed=self.move_speed_xy)
|
||
self.joystick.on_move = self._on_joystick_move
|
||
manip_row.addWidget(self.joystick)
|
||
|
||
self.z_fader = ZFaderWidget(max_speed=self.move_speed_z)
|
||
self.z_fader.on_move = self._on_fader_move
|
||
manip_row.addWidget(self.z_fader)
|
||
|
||
right_layout.addLayout(manip_row)
|
||
|
||
main_layout.addWidget(right_panel, 3)
|
||
self._update_buttons() |