优化wifi设置页面,添加悬浮键盘

This commit is contained in:
2026-05-10 01:39:16 +08:00
parent 65c342219d
commit 649677f564
5 changed files with 1380 additions and 18 deletions

14
main.py
View File

@@ -1,12 +1,6 @@
import sys import sys
import os import os
import json import json
# Fix QtWebEngine initialization by importing it before QApplication is created
import os
os.environ["QTWEBENGINE_CHROMIUM_FLAGS"] = "--no-sandbox --disable-gpu --disable-gpu-compositing --disable-dev-shm-usage"
from PyQt6.QtWebEngineWidgets import QWebEngineView
from PyQt6.QtWidgets import (QApplication, QWidget, QVBoxLayout, QHBoxLayout, from PyQt6.QtWidgets import (QApplication, QWidget, QVBoxLayout, QHBoxLayout,
QPushButton, QLabel, QStackedWidget) QPushButton, QLabel, QStackedWidget)
from PyQt6.QtCore import Qt, QSize from PyQt6.QtCore import Qt, QSize
@@ -14,6 +8,7 @@ from PyQt6.QtGui import QIcon, QFont
from pages.status_page import StatusPage from pages.status_page import StatusPage
from pages.control_page import ControlPage from pages.control_page import ControlPage
from pages.setting_page import SettingPage
from utils.aio_print_api import AIOPrrintSystemAPI from utils.aio_print_api import AIOPrrintSystemAPI
def load_config(): def load_config():
@@ -49,14 +44,11 @@ class MainWindow(QWidget):
# 添加测试页面 # 添加测试页面
self.page_status = StatusPage(self.api_client) self.page_status = StatusPage(self.api_client)
self.page_control = ControlPage(self.api_client) self.page_control = ControlPage(self.api_client)
self.page_settings = QLabel("系统设置") self.page_settings = SettingPage(self.api_client)
self.stacked_widget.addWidget(self.page_status) self.stacked_widget.addWidget(self.page_status)
self.stacked_widget.addWidget(self.page_control) self.stacked_widget.addWidget(self.page_control)
for page in [self.page_settings]: self.stacked_widget.addWidget(self.page_settings)
page.setAlignment(Qt.AlignmentFlag.AlignCenter)
page.setStyleSheet("color: white; font-size: 48px; font-weight: bold;")
self.stacked_widget.addWidget(page)
# 底部按钮区 # 底部按钮区
bottom_layout = QHBoxLayout() bottom_layout = QHBoxLayout()

895
pages/setting_page.py Normal file
View File

@@ -0,0 +1,895 @@
from PyQt6.QtWidgets import (
QWidget,
QVBoxLayout,
QLabel,
QHBoxLayout,
QListWidget,
QStackedWidget,
QPushButton,
QLineEdit,
QMessageBox,
QFormLayout,
QComboBox,
QListWidgetItem,
QFrame,
QScrollArea,
QAbstractItemView,
)
from PyQt6.QtCore import Qt, QEvent, QObject, QThread, QTimer, pyqtSignal
from PyQt6.QtGui import QMouseEvent
import codecs
from utils.wifi_manager import WifiManager
from utils.floating_keyboard import FloatingKeyboard
class DragScrollArea(QScrollArea):
"""支持鼠标/触屏点击拖拽滑动的滚动区域"""
def __init__(self, parent=None):
super().__init__(parent)
self._press_pos = None
self._start_value = 0
self._dragging = False
def viewportEvent(self, event):
etype = event.type()
if etype == QEvent.Type.MouseButtonPress:
if event.button() == Qt.MouseButton.LeftButton:
self._press_pos = event.position().toPoint()
self._start_value = self.verticalScrollBar().value()
self._dragging = False
elif etype == QEvent.Type.MouseMove:
if self._press_pos and event.buttons() & Qt.MouseButton.LeftButton:
pos = event.position().toPoint()
delta = pos - self._press_pos
# 移动超过15px阈值才触发拖拽避免误触
if self._dragging or delta.manhattanLength() > 15:
self._dragging = True
new_val = self._start_value - delta.y()
# 限制在有效范围内
self.verticalScrollBar().setValue(
max(0, min(new_val, self.verticalScrollBar().maximum()))
)
return True # 拖拽中拦截事件,不传递给子控件
elif etype == QEvent.Type.MouseButtonRelease:
if event.button() == Qt.MouseButton.LeftButton and self._press_pos is not None:
was_dragging = self._dragging
self._press_pos = None
self._dragging = False
if was_dragging:
return True # 拖拽结束,拦截释放事件,避免触发子控件点击
return super().viewportEvent(event)
class DraggableListWidget(QListWidget):
"""支持鼠标/触屏点击拖拽滑动的列表控件"""
def __init__(self, parent=None):
super().__init__(parent)
self.setVerticalScrollMode(QAbstractItemView.ScrollMode.ScrollPerPixel)
self._press_pos = None
self._start_value = 0
self._dragging = False
def viewportEvent(self, event):
etype = event.type()
if etype == QEvent.Type.MouseButtonPress:
if event.button() == Qt.MouseButton.LeftButton:
self._press_pos = event.position().toPoint()
self._start_value = self.verticalScrollBar().value()
self._dragging = False
elif etype == QEvent.Type.MouseMove:
if self._press_pos and event.buttons() & Qt.MouseButton.LeftButton:
pos = event.position().toPoint()
delta = pos - self._press_pos
# 移动超过15px阈值才触发拖拽避免误触
if self._dragging or delta.manhattanLength() > 15:
self._dragging = True
new_val = self._start_value - int(delta.y())
# 限制在有效范围内
self.verticalScrollBar().setValue(
max(0, min(new_val, self.verticalScrollBar().maximum()))
)
return True # 拖拽中拦截事件,不传递给子控件
elif etype == QEvent.Type.MouseButtonRelease:
if event.button() == Qt.MouseButton.LeftButton and self._press_pos is not None:
was_dragging = self._dragging
self._press_pos = None
self._dragging = False
if was_dragging:
return True # 拖拽结束,拦截释放事件,避免触发子控件点击
return super().viewportEvent(event)
class WifiScanWorker(QObject):
"""在后台线程中执行WiFi扫描避免阻塞UI"""
scan_finished = pyqtSignal(list)
scan_error = pyqtSignal(str)
def __init__(self, wifi_manager):
super().__init__()
self.wifi_manager = wifi_manager
def run(self):
try:
networks = self.wifi_manager.scan_networks()
self.scan_finished.emit(networks)
except Exception as e:
self.scan_error.emit(str(e))
class SettingPage(QWidget):
def __init__(self, api_client, parent=None):
super().__init__(parent)
self.api_client = api_client
self.wifi_manager = WifiManager()
self._saved_networks_cache = [] # 缓存上次的已保存网络列表
# 让页面自身能接收焦点,用于点击扫描按钮后转移焦点防止跳动
self.setFocusPolicy(Qt.FocusPolicy.ClickFocus)
# 创建悬浮键盘
self._keyboard = FloatingKeyboard()
self._keyboard_attached = False
self.init_ui()
self._start_status_timer()
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):
"""输入框获得焦点时的处理"""
self._attach_keyboard(widget)
def eventFilter(self, obj, event):
if event.type() == QEvent.Type.FocusIn:
if obj in (self.ssid_input, self.identity_input, self.password_input):
self._attach_keyboard(obj)
# 如果不是输入框获得焦点且键盘正显示,则关闭键盘
elif self._keyboard_attached and not isinstance(obj, (QLineEdit, QComboBox, QPushButton)):
self._dismiss_keyboard()
elif event.type() == QEvent.Type.FocusOut:
# 延迟检查:如果焦点真的离开了所有输入框,就关闭键盘
if obj in (self.ssid_input, self.identity_input, self.password_input):
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.ssid_input, self.identity_input, self.password_input, self.auth_combo):
self._dismiss_keyboard()
@staticmethod
def _styled_message(icon, parent, title, text, buttons=QMessageBox.StandardButton.Ok):
"""显示与整体风格一致的暗色主题消息框"""
msg = QMessageBox(parent)
msg.setIcon(icon)
msg.setWindowTitle(title)
msg.setText(text)
msg.setStandardButtons(buttons)
msg.setStyleSheet("""
QMessageBox {
background-color: #3f3f3f;
color: #f2f2f2;
border: 2px solid #646464;
border-radius: 12px;
padding: 20px;
}
QMessageBox QLabel {
color: #f2f2f2;
font-size: 20px;
padding: 10px;
}
QMessageBox QPushButton {
min-width: 130px;
min-height: 50px;
font-size: 20px;
font-weight: 600;
color: #f8f8f8;
background-color: #555555;
border: 2px solid #888888;
border-radius: 10px;
padding: 8px 24px;
}
QMessageBox QPushButton:hover {
background-color: #636363;
border-color: #aaaaaa;
}
QMessageBox QPushButton:pressed {
background-color: #3d3d3d;
border-color: #5a9fcf;
}
""")
return msg.exec()
def init_ui(self):
layout = QHBoxLayout(self)
layout.setContentsMargins(14, 14, 14, 14)
layout.setSpacing(14)
left_panel = QFrame()
left_panel.setStyleSheet("background-color: #3f3f3f; border-radius: 10px;")
left_layout = QVBoxLayout(left_panel)
left_layout.setContentsMargins(10, 10, 10, 10)
title = QLabel("系统设置")
title.setAlignment(Qt.AlignmentFlag.AlignCenter)
title.setStyleSheet("color: #efefef; font-size: 24px; font-weight: 600;")
left_layout.addWidget(title)
# 左侧设置项列表(启用平滑/按像素滚动,增大触摸目标)
self.item_list = QListWidget()
self.item_list.setVerticalScrollMode(QAbstractItemView.ScrollMode.ScrollPerPixel)
self.item_list.setSpacing(6)
self.item_list.setUniformItemSizes(False)
self.item_list.setStyleSheet(
"""
QListWidget {
background-color: #4a4a4a;
color: #f2f2f2;
border: 1px solid #646464;
border-radius: 8px;
font-size: 22px;
outline: none;
}
QListWidget::item {
height: 56px;
padding-left: 10px;
border-radius: 6px;
}
QListWidget::item:selected {
background-color: #2f6f91;
color: white;
}
"""
)
self.item_list.addItem("WiFi设置")
self.item_list.addItem("其它设置(待定)")
self.item_list.currentRowChanged.connect(self.display_setting)
left_layout.addWidget(self.item_list)
layout.addWidget(left_panel, 1)
# 右侧设置参数区域(放入可滚动区域,便于触屏上下滑动)
self.settings_stack = QStackedWidget()
self.settings_stack.setStyleSheet("background-color: #444444; border-radius: 10px;")
self.init_wifi_settings()
self.init_todo_settings()
right_scroll = DragScrollArea()
right_scroll.setWidgetResizable(True)
right_scroll.setFrameShape(QFrame.Shape.NoFrame)
# 将堆叠面板放入 scroll area
right_scroll.setWidget(self.settings_stack)
right_scroll.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
right_scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
layout.addWidget(right_scroll, 3)
self.item_list.setCurrentRow(0)
def init_wifi_settings(self):
wifi_widget = QWidget()
wifi_widget.setStyleSheet(
"""
/* ------ 通用标签 ------ */
QLabel {
color: #e8e8e8;
}
/* ------ 大按钮:清晰边框、足够高度 ------ */
QPushButton {
min-height: 50px;
font-size: 20px;
font-weight: 600;
color: #f8f8f8;
background-color: #555555;
border: 2px solid #888888;
border-radius: 10px;
padding: 10px 24px;
}
QPushButton:hover {
background-color: #636363;
border-color: #aaaaaa;
}
QPushButton:pressed {
background-color: #3d3d3d;
border-color: #5a9fcf;
}
QPushButton:disabled {
color: #808080;
background-color: #404040;
border-color: #555555;
}
/* ------ 输入框 ------ */
QLineEdit {
min-height: 44px;
font-size: 18px;
color: #f2f2f2;
background-color: #3a3a3a;
border: 2px solid #707070;
border-radius: 8px;
padding: 4px 12px;
}
QLineEdit:focus {
border-color: #5a9fcf;
}
/* ------ 下拉框 ------ */
QComboBox {
min-height: 44px;
font-size: 18px;
color: #f2f2f2;
background-color: #3a3a3a;
border: 2px solid #707070;
border-radius: 8px;
padding: 4px 12px;
}
QComboBox:hover {
border-color: #aaaaaa;
}
QComboBox QAbstractItemView {
background-color: #3a3a3a;
color: #f2f2f2;
selection-background-color: #2f6f91;
border: 1px solid #707070;
font-size: 18px;
}
/* ------ 列表控件 ------ */
QListWidget {
min-height: 300px;
max-width: 500%;
background-color: #3a3a3a;
color: #f2f2f2;
border: 2px solid #707070;
border-radius: 8px;
font-size: 18px;
outline: none;
}
QListWidget::item {
min-height: 40px;
padding: 4px 10px;
border-radius: 4px;
}
QListWidget::item:selected {
background-color: #2f6f91;
color: #ffffff;
}
QListWidget::item:selected QLabel {
color: #ffffff;
}
QListWidget::item:hover {
background-color: #505050;
}
"""
)
wifi_layout = QVBoxLayout(wifi_widget)
wifi_layout.setContentsMargins(16, 16, 16, 16)
wifi_layout.setSpacing(14)
wifi_title = QLabel("WiFi设置")
wifi_title.setStyleSheet("color: #f2f2f2; font-size: 26px; font-weight: 700;")
wifi_layout.addWidget(wifi_title)
self.current_status_label = QLabel("当前连接:未知")
self.current_status_label.setStyleSheet("color: #cccccc; font-size: 19px; font-weight: 500;")
wifi_layout.addWidget(self.current_status_label)
# 显示保存的WiFi列表
saved_title = QLabel("已保存网络")
saved_title.setStyleSheet("color: #dcdcdc; font-size: 22px; font-weight: 600;")
wifi_layout.addWidget(saved_title)
self.saved_wifi_list = DraggableListWidget()
wifi_layout.addWidget(self.saved_wifi_list)
saved_buttons_layout = QHBoxLayout()
saved_buttons_layout.setSpacing(12)
connect_saved_button = QPushButton("连接到此网络")
connect_saved_button.clicked.connect(self.connect_to_saved_wifi)
saved_buttons_layout.addWidget(connect_saved_button)
remove_saved_button = QPushButton("删除选中")
remove_saved_button.clicked.connect(self.remove_selected_saved_wifi)
saved_buttons_layout.addWidget(remove_saved_button)
wifi_layout.addLayout(saved_buttons_layout)
nearby_title = QLabel("附近网络")
nearby_title.setStyleSheet("color: #dcdcdc; font-size: 22px; font-weight: 600;")
wifi_layout.addWidget(nearby_title)
self.nearby_wifi_list = DraggableListWidget()
self.nearby_wifi_list.itemClicked.connect(self.fill_ssid_from_scan)
wifi_layout.addWidget(self.nearby_wifi_list)
self.scan_button = QPushButton("扫描网络")
self.scan_button.clicked.connect(self.scan_nearby_wifi)
wifi_layout.addWidget(self.scan_button)
# 连接新WiFi
connect_title = QLabel("连接新网络")
connect_title.setStyleSheet("color: #dcdcdc; font-size: 22px; font-weight: 600;")
wifi_layout.addWidget(connect_title)
form = QFormLayout()
form.setLabelAlignment(Qt.AlignmentFlag.AlignRight)
form.setFormAlignment(Qt.AlignmentFlag.AlignLeft)
form.setHorizontalSpacing(16)
form.setVerticalSpacing(12)
# 表单标签使用浅色
ssid_label = QLabel("SSID")
ssid_label.setStyleSheet("color: #d0d0d0; font-size: 19px; font-weight: 500;")
self.ssid_input = QLineEdit()
self.ssid_input.setStyleSheet("max-width: 400%;")
self.ssid_input.setPlaceholderText("输入WiFi名称")
self.ssid_input.installEventFilter(self)
self.ssid_input.textChanged.connect(self._on_ssid_text_changed)
form.addRow(ssid_label, self.ssid_input)
auth_label = QLabel("认证方式")
auth_label.setStyleSheet("color: #d0d0d0; font-size: 19px; font-weight: 500;")
self.auth_combo = QComboBox()
self.auth_combo.setStyleSheet(
"""
QComboBox {
max-width: 400%;
font-size: 22px;
min-height: 44px;
}
"""
)
# 直接设置下拉列表视图的样式,避免被父级样式覆盖
self.auth_combo.view().setStyleSheet(
"""
QListView {
font-size: 24px;
min-height: 50px;
padding: 6px 12px;
}
QListView::item {
min-height: 50px;
padding: 8px 12px;
}
"""
)
self.auth_combo.addItem("开放网络(无密码)", "open")
self.auth_combo.addItem("WPA/WPA2-PSK", "psk")
self.auth_combo.addItem("WPA-EAP (PEAP/MSCHAPv2)", "eap")
self.auth_combo.currentIndexChanged.connect(self.update_auth_fields)
form.addRow(auth_label, self.auth_combo)
identity_label = QLabel("身份")
identity_label.setStyleSheet("color: #d0d0d0; font-size: 19px; font-weight: 500;")
self.identity_label = identity_label
self.identity_input = QLineEdit()
self.identity_input.setStyleSheet("max-width: 400%;")
self.identity_input.setPlaceholderText("企业网络用户名/身份")
self.identity_input.installEventFilter(self)
form.addRow(identity_label, self.identity_input)
password_label = QLabel("密码")
password_label.setStyleSheet("color: #d0d0d0; font-size: 19px; font-weight: 500;")
self.password_label = password_label
self.password_input = QLineEdit()
self.password_input.setStyleSheet("max-width: 400%;")
self.password_input.setPlaceholderText("输入WiFi密码")
self.password_input.installEventFilter(self)
self.password_input.setEchoMode(QLineEdit.EchoMode.Password)
form.addRow(password_label, self.password_input)
wifi_layout.addLayout(form)
# 连接按钮使用醒目的强调色
connect_button = QPushButton("连接")
connect_button.setStyleSheet(
"""
QPushButton {
min-height: 52px;
font-size: 22px;
font-weight: 700;
color: #ffffff;
background-color: #2f6f91;
border: 2px solid #4a9fc8;
border-radius: 10px;
padding: 10px 24px;
}
QPushButton:hover {
background-color: #3a85b3;
border-color: #6fb8dd;
}
QPushButton:pressed {
background-color: #1e4d66;
border-color: #2f6f91;
}
"""
)
connect_button.clicked.connect(self.connect_to_wifi)
wifi_layout.addWidget(connect_button)
wifi_layout.addStretch()
self.settings_stack.addWidget(wifi_widget)
self.update_auth_fields()
self.refresh_saved_wifi()
self.refresh_current_status()
def init_todo_settings(self):
todo_widget = QWidget()
todo_widget.setStyleSheet(
"""
QLabel {
color: #e4e4e4;
}
QPushButton {
min-height: 50px;
font-size: 20px;
font-weight: 600;
color: #f8f8f8;
background-color: #555555;
border: 2px solid #888888;
border-radius: 10px;
padding: 10px 24px;
}
QPushButton:hover {
background-color: #636363;
border-color: #aaaaaa;
}
"""
)
todo_layout = QVBoxLayout(todo_widget)
todo_layout.setContentsMargins(20, 20, 20, 20)
label = QLabel("其它配置项待定")
label.setAlignment(Qt.AlignmentFlag.AlignCenter)
label.setStyleSheet("color: #cecece; font-size: 26px; font-weight: 600;")
todo_layout.addWidget(label)
hint = QLabel("更多设置项将在后续版本中添加")
hint.setAlignment(Qt.AlignmentFlag.AlignCenter)
hint.setStyleSheet("color: #909090; font-size: 18px; margin-top: 12px;")
todo_layout.addWidget(hint)
todo_layout.addStretch()
self.settings_stack.addWidget(todo_widget)
def refresh_saved_wifi(self):
try:
saved_networks = self.wifi_manager.list_saved_networks()
except Exception as e:
self._styled_message(QMessageBox.Icon.Critical, self, "错误", f"无法加载保存的WiFi: {str(e)}")
return
# 比较与上次缓存的网络列表是否有变化
cache_key = [(n.get("network_id"), n.get("ssid"), n.get("flags")) for n in saved_networks]
if cache_key == self._saved_networks_cache:
return # 无变化,不清空列表
self._saved_networks_cache = cache_key
# 列表有变化时才真正刷新
current_item = self.saved_wifi_list.currentItem()
current_net_id = current_item.data(Qt.ItemDataRole.UserRole).get("network_id") if current_item else None
self.saved_wifi_list.clear()
for network in saved_networks:
item_text = f"[{network.get('network_id', '-')}] {network.get('ssid', '<hidden>')} {network.get('flags', '')}"
item = QListWidgetItem(item_text)
item.setData(Qt.ItemDataRole.UserRole, network)
self.saved_wifi_list.addItem(item)
# 恢复选中
if current_net_id is not None and network.get("network_id") == current_net_id:
self.saved_wifi_list.setCurrentItem(item)
def scan_nearby_wifi(self):
"""在后台线程中扫描WiFi防止界面卡死"""
self.nearby_wifi_list.clear()
# 把焦点交给页面自身,防止跳到输入框导致画面跳动
self.setFocus()
self.scan_button.setEnabled(False)
self.scan_button.setText("扫描中……")
self._scan_thread = QThread()
self._scan_worker = WifiScanWorker(self.wifi_manager)
self._scan_worker.moveToThread(self._scan_thread)
self._scan_thread.started.connect(self._scan_worker.run)
self._scan_worker.scan_finished.connect(self._on_scan_finished)
self._scan_worker.scan_error.connect(self._on_scan_error)
# 清理线程资源
self._scan_worker.scan_finished.connect(self._scan_thread.quit)
self._scan_worker.scan_error.connect(self._scan_thread.quit)
self._scan_worker.scan_finished.connect(self._scan_worker.deleteLater)
self._scan_worker.scan_error.connect(self._scan_worker.deleteLater)
self._scan_thread.finished.connect(self._scan_thread.deleteLater)
self._scan_thread.start()
def _on_scan_finished(self, networks):
"""扫描完成后的UI更新主线程中执行"""
self.scan_button.setEnabled(True)
self.scan_button.setText("扫描网络")
processed = self._deduplicate_networks(networks)
if not processed:
self._styled_message(QMessageBox.Icon.Information, self, "提示", "未扫描到可用网络")
return
for network in processed:
ssid = network.get("ssid", "")
if not ssid:
continue
decoded_ssid = self._decode_ssid(ssid)
signal = network.get("signal_level", "")
# 自定义列表项SSID靠左信号强度靠右
item_widget = QWidget()
item_widget.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
# 为了比较好看的列表项目,下面的不要删
item_widget.setStyleSheet("""
QWidget {
min-height: 40px;
padding: 4px 10px;
border-radius: 4px;
}
QWidget:selected {
background-color: #2f6f91;
color: #ffffff;
}
QWidget:selected QLabel {
color: #ffffff;
}
QWidget:hover {
background-color: #505050;
}
""")
item_layout = QHBoxLayout(item_widget)
item_layout.setContentsMargins(8, 2, 12, 2)
ssid_label = QLabel(decoded_ssid)
ssid_label.setStyleSheet("background: transparent; color: #f2f2f2; font-size: 18px;")
signal_label = QLabel(f"{signal} dBm" if signal else "")
signal_label.setStyleSheet("background: transparent; color: #aaaaaa; font-size: 16px;")
item_layout.addWidget(ssid_label)
item_layout.addStretch()
item_layout.addWidget(signal_label)
item = QListWidgetItem()
item.setData(Qt.ItemDataRole.UserRole, network)
item.setSizeHint(item_widget.sizeHint())
self.nearby_wifi_list.addItem(item)
self.nearby_wifi_list.setItemWidget(item, item_widget)
def _on_scan_error(self, error_msg):
"""扫描出错后的UI恢复主线程中执行"""
self.scan_button.setEnabled(True)
self.scan_button.setText("扫描网络")
self._styled_message(QMessageBox.Icon.Critical, self, "错误", f"扫描网络失败: {error_msg}")
def _on_ssid_text_changed(self, text):
"""SSID输入框文本变化时如果清空则重置为开放网络"""
if not text.strip():
self.auth_combo.blockSignals(True)
self.auth_combo.setCurrentIndex(0)
self.auth_combo.blockSignals(False)
self.update_auth_fields()
@staticmethod
def _decode_ssid(ssid):
"""尝试解码非英文WiFi名称处理 \\xe9\\x83\\xbd 等转义序列)"""
if not ssid:
return ssid
# 尝试 unicode_escape 解码(处理 \x 转义序列,返回 bytes
try:
decoded = ssid.encode('latin-1').decode('unicode-escape').encode('latin-1').decode()
if decoded != ssid:
return decoded
except Exception:
pass
# 尝试 latin-1 → UTF-8 双重转换
try:
decoded = bytes(ssid, encoding='utf-8').decode('utf-8')
if decoded != ssid:
return decoded
except Exception:
pass
return ssid
@staticmethod
def _deduplicate_networks(networks):
"""去重同名网络每个SSID只保留信号最强的一个"""
best = {}
for net in networks:
ssid = net.get("ssid", "")
if not ssid:
continue
raw = net.get("signal_level", -100)
try:
signal = int(raw)
except (ValueError, TypeError):
signal = -100
if ssid not in best or signal > best[ssid].get("_signal_int", -100):
net["_signal_int"] = signal
best[ssid] = net
return list(best.values())
@staticmethod
def _detect_auth_mode(network):
"""根据 wpa_supplicant 返回的标准 flags 判断认证方式"""
flags = network.get("flags", "").strip()
# 无加密标记 → 开放网络
if not flags or flags in ("", "[ESS]", "[NONE]", "NONE"):
return "open"
# 检查是否含企业级认证标记
eap_keywords = (
"WPA2-EAP", "WPA-EAP", "WPA3-EAP",
"EAP", "SUITE-B",
"802.1X", "IEEE8021X", "ENTERPRISE",
"FT/EAP",
)
if any(kw in flags.upper() for kw in eap_keywords):
return "eap"
# 检查是否含 PSK 类标记(个人级加密)
psk_keywords = (
"PSK", "SAE", "WPA2", "WPA3", "WPA",
"CCMP", "TKIP",
)
if any(kw in flags.upper() for kw in psk_keywords):
return "psk"
# 兜底:有标记但无法识别,默认 psk
return "psk"
def fill_ssid_from_scan(self, item):
network = item.data(Qt.ItemDataRole.UserRole) or {}
ssid = network.get("ssid", "")
if ssid:
decoded = self._decode_ssid(ssid)
self.ssid_input.setText(decoded)
# 根据网络标志自动选择认证方式
auth_mode = self._detect_auth_mode(network)
index = self.auth_combo.findData(auth_mode)
if index >= 0:
self.auth_combo.blockSignals(True)
self.auth_combo.setCurrentIndex(index)
self.auth_combo.blockSignals(False)
self.update_auth_fields()
def update_auth_fields(self):
auth_mode = self.auth_combo.currentData()
if auth_mode == "open":
self.identity_label.setVisible(False)
self.identity_input.setVisible(False)
self.password_label.setVisible(False)
self.password_input.setVisible(False)
elif auth_mode == "psk":
self.identity_label.setVisible(False)
self.identity_input.setVisible(False)
self.password_label.setVisible(True)
self.password_input.setVisible(True)
else:
self.identity_label.setVisible(True)
self.identity_input.setVisible(True)
self.password_label.setVisible(True)
self.password_input.setVisible(True)
def refresh_current_status(self):
try:
status = self.wifi_manager.get_current_status()
ssid = status.get("ssid", "未连接")
ip_addr = status.get("ip_address", "-")
state = status.get("wpa_state", "UNKNOWN")
self.current_status_label.setText(f"当前连接:{ssid} | IP: {ip_addr} | 状态: {state}")
except Exception:
self.current_status_label.setText("当前连接:未知")
def _start_status_timer(self):
"""启动定时器,每秒刷新已保存网络列表和当前连接状态"""
self._status_timer = QTimer(self)
self._status_timer.timeout.connect(self._on_status_timer_tick)
self._status_timer.start(1000)
def _on_status_timer_tick(self):
self.refresh_saved_wifi()
self.refresh_current_status()
def connect_to_saved_wifi(self):
"""连接已保存列表中选中的网络"""
item = self.saved_wifi_list.currentItem()
if item is None:
self._styled_message(QMessageBox.Icon.Warning, self, "提示", "请先选择一个已保存网络")
return
network = item.data(Qt.ItemDataRole.UserRole) or {}
network_id = network.get("network_id")
ssid = network.get("ssid", "")
if network_id is None:
self._styled_message(QMessageBox.Icon.Warning, self, "提示", "选中网络无效")
return
try:
ok = self.wifi_manager.connect_network_id(network_id)
if not ok:
self._styled_message(QMessageBox.Icon.Critical, self, "错误", "连接请求下发失败")
return
self._styled_message(QMessageBox.Icon.Information, self, "成功", f"已发起连接: {ssid}")
except Exception as e:
self._styled_message(QMessageBox.Icon.Critical, self, "错误", f"连接失败: {str(e)}")
def remove_selected_saved_wifi(self):
item = self.saved_wifi_list.currentItem()
if item is None:
self._styled_message(QMessageBox.Icon.Warning, self, "提示", "请先选择一个已保存网络")
return
network = item.data(Qt.ItemDataRole.UserRole) or {}
network_id = network.get("network_id")
ssid = network.get("ssid", "")
if network_id is None:
self._styled_message(QMessageBox.Icon.Warning, self, "提示", "选中网络无效,无法删除")
return
try:
self.wifi_manager.remove_network(network_id)
self._styled_message(QMessageBox.Icon.Information, self, "成功", f"已删除网络: {ssid}")
self.refresh_saved_wifi()
self.refresh_current_status()
except Exception as e:
self._styled_message(QMessageBox.Icon.Critical, self, "错误", f"删除失败: {str(e)}")
def connect_to_wifi(self):
ssid = self.ssid_input.text().strip()
password = self.password_input.text()
identity = self.identity_input.text().strip()
auth_mode = self.auth_combo.currentData()
if not ssid:
self._styled_message(QMessageBox.Icon.Warning, self, "警告", "WiFi名称不能为空")
return
if auth_mode == "psk" and not password:
self._styled_message(QMessageBox.Icon.Warning, self, "警告", "WPA/WPA2 认证需要密码")
return
if auth_mode == "eap" and (not identity or not password):
self._styled_message(QMessageBox.Icon.Warning, self, "警告", "WPA-EAP 认证需要身份和密码")
return
try:
if auth_mode == "open":
ok = self.wifi_manager.connect_wifi(ssid, None)
elif auth_mode == "psk":
ok = self.wifi_manager.connect_wifi(ssid, password)
else:
ok = self.wifi_manager.connect_eap(ssid, identity, password)
if not ok:
self._styled_message(QMessageBox.Icon.Critical, self, "错误", "连接请求下发失败,请检查系统日志")
return
self._styled_message(QMessageBox.Icon.Information, self, "成功", f"已发起连接: {ssid}")
self.refresh_saved_wifi()
self.refresh_current_status()
except Exception as e:
self._styled_message(QMessageBox.Icon.Critical, self, "错误", f"连接WiFi失败: {str(e)}")
def display_setting(self, index):
if index < 0:
return
self.settings_stack.setCurrentIndex(index)

View File

@@ -19,13 +19,59 @@ class AIOPrrintSystemAPI:
pass pass
def get_status(self): def get_status(self):
url = f"{self.api_url}/status"
try: test_data = {
r = requests.get(url, headers=self.headers, timeout=5) 'job': {
r.raise_for_status() 'job': {
return r.json() 'estimatedPrintTime': 1234,
except: 'filament': {'length': 765, 'volume': 24356},
return {"status": {}, "job": {}} 'file': {'display_name': 'Test File','date': None, 'name': '20260414135441_42bff5215c6148b8b5f4d8c4f15d5ddc.gcode', 'origin': 'local', 'path': None, 'size': 1468987},
'lastPrintTime': None,
'user': None
},
'progress': {
'completion': 74.8,
'filepos': 1234,
'printTime': 1235,
'printTimeLeft': 6353,
'printTimeLeftOrigin': 5366
},
'state': 'Operational'
},
'status': {
'sd': {'ready': False},
'state': {
'error': '',
'flags': {
'cancelling': False,
'closedOrError': False,
'error': False,
'finishing': False,
'operational': True,
'paused': False,
'pausing': False,
'printing': False,
'ready': True,
'resuming': False,
'sdReady': False
},
'text': 'Operational test'
},
'temperature': {
'bed': {'actual': 85, 'offset': 0, 'target': 56},
'tool0': {'actual': 0.0, 'offset': 0, 'target': 340}
}
}
}
return test_data
# url = f"{self.api_url}/status"
# try:
# r = requests.get(url, headers=self.headers, timeout=5)
# r.raise_for_status()
# return r.json()
# except:
# return {"status": {}, "job": {}}
def pause_print(self): def pause_print(self):
return self._post_action("pause_print", action="pause") return self._post_action("pause_print", action="pause")

423
utils/floating_keyboard.py Normal file
View File

@@ -0,0 +1,423 @@
"""
悬浮虚拟键盘
- 美式 QWERTY 键盘排列(字母 + 数字 + 常用符号)
- Shift / Caps Lock / 退格 / 空格 / 回车 / 关闭
- 可拖拽悬浮,自动跟随焦点输入框
- 暗色主题,大按钮适合触屏
"""
from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QPushButton,
QApplication, QLineEdit, QTextEdit,
)
from PyQt6.QtCore import Qt, QEvent, QPoint, pyqtSignal
from PyQt6.QtGui import QMouseEvent, QTextCursor
# ─── 键盘布局定义 ──────────────────────────────────────────────
KEY_ROWS = {
"normal": [
["q", "w", "e", "r", "t", "y", "u", "i", "o", "p"],
["a", "s", "d", "f", "g", "h", "j", "k", "l"],
["z", "x", "c", "v", "b", "n", "m"],
],
"shift": [
["Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P"],
["A", "S", "D", "F", "G", "H", "J", "K", "L"],
["Z", "X", "C", "V", "B", "N", "M"],
],
}
NUMBER_ROW = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"]
# 符号键:按 Shift 或符号切换时与数字行互换
SYMBOL_ROW = ["!", "@", "#", "$", "%", "^", "&", "*", "(", ")"]
# 右侧额外符号(放在字母区行末或单独行)
EXTRA_SYMBOLS = [
["-", "=", "[", "]", "\\", ";", "'", ",", ".", "/"],
["_", "+", "{", "}", "|", ":", "\"", "<", ">", "?"],
]
KEY_STYLE = """
QPushButton {
min-width: 52px;
min-height: 52px;
font-size: 22px;
font-weight: 600;
color: #f2f2f2;
background-color: #4a4a4a;
border: 2px solid #646464;
border-radius: 8px;
padding: 4px 4px;
}
QPushButton:hover {
background-color: #5a5a5a;
border-color: #888888;
}
QPushButton:pressed {
background-color: #2f6f91;
border-color: #5a9fcf;
}
"""
CTRL_KEY_STYLE = """
QPushButton {
min-width: 72px;
min-height: 52px;
font-size: 18px;
font-weight: 600;
color: #f2f2f2;
background-color: #555555;
border: 2px solid #707070;
border-radius: 8px;
padding: 4px 8px;
}
QPushButton:hover {
background-color: #666666;
border-color: #909090;
}
QPushButton:pressed {
background-color: #2f6f91;
border-color: #5a9fcf;
}
"""
SPACE_STYLE = """
QPushButton {
min-width: 280px;
min-height: 52px;
font-size: 18px;
color: transparent;
background-color: #4a4a4a;
border: 2px solid #646464;
border-radius: 8px;
}
QPushButton:hover {
background-color: #5a5a5a;
border-color: #888888;
}
QPushButton:pressed {
background-color: #2f6f91;
border-color: #5a9fcf;
}
"""
ACTIVE_CTRL_STYLE = """
QPushButton {
min-width: 72px;
min-height: 52px;
font-size: 18px;
font-weight: 600;
color: #ffffff;
background-color: #2f6f91;
border: 2px solid #5a9fcf;
border-radius: 8px;
padding: 4px 8px;
}
QPushButton:hover {
background-color: #3a85b3;
border-color: #6fb8dd;
}
"""
class FloatingKeyboard(QWidget):
"""可悬浮拖拽的虚拟键盘,自动跟随焦点输入框"""
key_pressed = pyqtSignal(str)
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowFlags(
Qt.WindowType.FramelessWindowHint
| Qt.WindowType.WindowStaysOnTopHint
| Qt.WindowType.Tool
)
self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
self.setStyleSheet("background: transparent;")
self._drag_pos = None
self._shift_on = False
self._caps_on = False
self._symbol_mode = False
self._target_widget = None
self.init_ui()
# ── UI 构建 ──────────────────────────────────────────────
def init_ui(self):
outer = QWidget()
outer.setStyleSheet(
"background-color: #333333; border: 2px solid #555555; border-radius: 12px;"
)
layout = QVBoxLayout(outer)
layout.setContentsMargins(8, 8, 8, 8)
layout.setSpacing(4)
# ── 顶部拖拽条 ──
title_bar = QWidget()
title_bar.setFixedHeight(28)
title_bar.setStyleSheet("background: transparent;")
title_bar.mousePressEvent = self._title_mouse_press
title_bar.mouseMoveEvent = self._title_mouse_move
title_layout = QHBoxLayout(title_bar)
title_layout.setContentsMargins(8, 0, 8, 0)
drag_label = QPushButton("≡ 键盘")
drag_label.setStyleSheet(
"""
QPushButton {
background: transparent;
color: #aaaaaa;
font-size: 16px;
border: none;
text-align: left;
}
"""
)
title_layout.addWidget(drag_label)
title_layout.addStretch()
close_btn = QPushButton("")
close_btn.setFixedSize(28, 28)
close_btn.setStyleSheet(
"""
QPushButton {
background: transparent;
color: #aaaaaa;
font-size: 18px;
border: none;
border-radius: 14px;
}
QPushButton:hover {
background-color: #555555;
color: #ffffff;
}
"""
)
close_btn.clicked.connect(self.hide)
title_layout.addWidget(close_btn)
layout.addWidget(title_bar)
# ── 数字/符号行 ──
self.num_layout = QHBoxLayout()
self.num_layout.setSpacing(4)
self.num_buttons = []
for ch in NUMBER_ROW:
btn = self._make_key(ch)
self.num_buttons.append(btn)
self.num_layout.addWidget(btn)
layout.addLayout(self.num_layout)
# ── 字母行 ──
self._letter_buttons = [] # flat list for shift toggle
for row_keys in KEY_ROWS["normal"]:
row_layout = QHBoxLayout()
row_layout.setSpacing(4)
row_layout.addStretch()
for ch in row_keys:
btn = self._make_key(ch)
self._letter_buttons.append(btn)
row_layout.addWidget(btn)
row_layout.addStretch()
layout.addLayout(row_layout)
# ── 额外符号行(- = [ ] 等) ──
self.extra_layout = QHBoxLayout()
self.extra_layout.setSpacing(4)
self.extra_layout.addStretch()
self.extra_buttons = []
for ch in EXTRA_SYMBOLS[0]:
btn = self._make_key(ch)
self.extra_buttons.append(btn)
self.extra_layout.addWidget(btn)
self.extra_layout.addStretch()
layout.addLayout(self.extra_layout)
# ── 功能键行 ──
ctrl_layout = QHBoxLayout()
ctrl_layout.setSpacing(4)
self.shift_btn = QPushButton("")
self.shift_btn.setStyleSheet(CTRL_KEY_STYLE)
self.shift_btn.clicked.connect(self._toggle_shift)
ctrl_layout.addWidget(self.shift_btn)
self.caps_btn = QPushButton("A/a")
self.caps_btn.setStyleSheet(CTRL_KEY_STYLE)
self.caps_btn.clicked.connect(self._toggle_caps)
ctrl_layout.addWidget(self.caps_btn)
self.sym_btn = QPushButton("?123")
self.sym_btn.setStyleSheet(CTRL_KEY_STYLE)
self.sym_btn.clicked.connect(self._toggle_symbol)
ctrl_layout.addWidget(self.sym_btn)
backspace_btn = QPushButton("")
backspace_btn.setStyleSheet(CTRL_KEY_STYLE)
backspace_btn.clicked.connect(lambda: self._send_key("\b"))
ctrl_layout.addWidget(backspace_btn)
ctrl_layout.addStretch()
enter_btn = QPushButton("↵ 回车")
enter_btn.setStyleSheet(CTRL_KEY_STYLE)
enter_btn.clicked.connect(lambda: self._send_key("\n"))
ctrl_layout.addWidget(enter_btn)
layout.addLayout(ctrl_layout)
# ── 空格行 ──
space_layout = QHBoxLayout()
space_layout.setSpacing(4)
space_layout.addStretch()
self.space_btn = QPushButton(" ") # full-width space as placeholder
self.space_btn.setStyleSheet(SPACE_STYLE)
self.space_btn.clicked.connect(lambda: self._send_key(" "))
space_layout.addWidget(self.space_btn)
space_layout.addStretch()
layout.addLayout(space_layout)
outer_layout = QVBoxLayout(self)
outer_layout.setContentsMargins(0, 0, 0, 0)
outer_layout.addWidget(outer)
self.setFixedWidth(680)
# ── 按键工厂 ──
def _make_key(self, text):
btn = QPushButton(text)
btn.setStyleSheet(KEY_STYLE)
btn.clicked.connect(lambda checked, t=text: self._send_key(t))
return btn
# ── 按键发送 ──
def _send_key(self, text):
"""发送按键到目标控件"""
if text == "\b":
self._backspace()
return
if self._symbol_mode:
self._toggle_symbol()
widget = self._get_target()
if widget is None:
return
if isinstance(widget, QLineEdit):
cursor = widget.cursorPosition()
current = widget.text()
new_text = current[:cursor] + text + current[cursor:]
widget.setText(new_text)
widget.setCursorPosition(cursor + len(text))
elif isinstance(widget, QTextEdit):
tc = widget.textCursor()
tc.insertText(text)
widget.setTextCursor(tc)
def _backspace(self):
widget = self._get_target()
if widget is None:
return
if isinstance(widget, QLineEdit):
cursor = widget.cursorPosition()
current = widget.text()
if cursor > 0:
new_text = current[:cursor - 1] + current[cursor:]
widget.setText(new_text)
widget.setCursorPosition(cursor - 1)
elif isinstance(widget, QTextEdit):
tc = widget.textCursor()
if not tc.hasSelection():
pos = tc.position()
if pos > 0:
tc.setPosition(pos - 1)
tc.movePosition(QTextCursor.MoveOperation.Right, QTextCursor.MoveMode.KeepAnchor)
tc.removeSelectedText()
def _get_target(self):
"""获取当前有效的目标输入控件"""
if self._target_widget and self._target_widget.hasFocus():
return self._target_widget
w = QApplication.focusWidget()
if isinstance(w, (QLineEdit, QTextEdit)):
return w
return None
# ── Shift / Caps / Symbol 切换 ──
def _toggle_shift(self):
self._shift_on = not self._shift_on
self._apply_shift_caps()
self.shift_btn.setStyleSheet(ACTIVE_CTRL_STYLE if self._shift_on else CTRL_KEY_STYLE)
def _toggle_caps(self):
self._caps_on = not self._caps_on
self._apply_shift_caps()
self.caps_btn.setStyleSheet(ACTIVE_CTRL_STYLE if self._caps_on else CTRL_KEY_STYLE)
def _toggle_symbol(self):
self._symbol_mode = not self._symbol_mode
if self._symbol_mode:
for btn, ch in zip(self.num_buttons, SYMBOL_ROW):
btn.setText(ch)
for btn, ch in zip(self.extra_buttons, EXTRA_SYMBOLS[1]):
btn.setText(ch)
self.sym_btn.setStyleSheet(ACTIVE_CTRL_STYLE)
self.sym_btn.setText("ABC")
else:
for btn, ch in zip(self.num_buttons, NUMBER_ROW):
btn.setText(ch)
for btn, ch in zip(self.extra_buttons, EXTRA_SYMBOLS[0]):
btn.setText(ch)
self.sym_btn.setStyleSheet(CTRL_KEY_STYLE)
self.sym_btn.setText("?123")
def _apply_shift_caps(self):
use_shift = self._shift_on != self._caps_on # XOR
rows = KEY_ROWS["shift"] if use_shift else KEY_ROWS["normal"]
flat = [ch for row in rows for ch in row]
for btn, ch in zip(self._letter_buttons, flat):
btn.setText(ch)
# ── 窗口拖拽 ──
def _title_mouse_press(self, event: QMouseEvent):
if event.button() == Qt.MouseButton.LeftButton:
self._drag_pos = event.globalPosition().toPoint() - self.frameGeometry().topLeft()
def _title_mouse_move(self, event: QMouseEvent):
if event.buttons() & Qt.MouseButton.LeftButton and self._drag_pos is not None:
self.move(event.globalPosition().toPoint() - self._drag_pos)
# ── 目标绑定 ──
def attach(self, widget):
"""绑定到一个输入控件,键盘输入将发送到此控件"""
self._target_widget = widget
def detach(self):
self._target_widget = None
def show_at(self, x, y):
"""在屏幕坐标 (x, y) 处显示"""
self.move(x, y)
self.show()
self.raise_()
def show_below(self, widget):
"""显示在指定控件下方"""
pos = widget.mapToGlobal(QPoint(0, widget.height() + 4))
screen = QApplication.primaryScreen().availableGeometry()
if pos.x() + self.width() > screen.width():
pos.setX(screen.width() - self.width() - 8)
self.show_at(pos.x(), pos.y())

View File

@@ -89,6 +89,12 @@ class WifiManager:
self._run_wpa_cli("save_config") self._run_wpa_cli("save_config")
return True return True
def connect_network_id(self, network_id):
"""通过 network_id 连接已保存的网络"""
ret1 = self._run_wpa_cli("select_network", str(network_id))
self._run_wpa_cli("save_config")
return bool(ret1)
def remove_network(self, network_id): def remove_network(self, network_id):
"""删除某个已保存的网络""" """删除某个已保存的网络"""
self._run_wpa_cli("remove_network", str(network_id)) self._run_wpa_cli("remove_network", str(network_id))