diff --git a/main.py b/main.py index c40fc5d..4125d44 100644 --- a/main.py +++ b/main.py @@ -1,12 +1,6 @@ import sys import os 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, QPushButton, QLabel, QStackedWidget) from PyQt6.QtCore import Qt, QSize @@ -14,6 +8,7 @@ from PyQt6.QtGui import QIcon, QFont from pages.status_page import StatusPage from pages.control_page import ControlPage +from pages.setting_page import SettingPage from utils.aio_print_api import AIOPrrintSystemAPI def load_config(): @@ -49,14 +44,11 @@ class MainWindow(QWidget): # 添加测试页面 self.page_status = StatusPage(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_control) - for page in [self.page_settings]: - page.setAlignment(Qt.AlignmentFlag.AlignCenter) - page.setStyleSheet("color: white; font-size: 48px; font-weight: bold;") - self.stacked_widget.addWidget(page) + self.stacked_widget.addWidget(self.page_settings) # 底部按钮区 bottom_layout = QHBoxLayout() diff --git a/pages/setting_page.py b/pages/setting_page.py new file mode 100644 index 0000000..5533f37 --- /dev/null +++ b/pages/setting_page.py @@ -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', '')} {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) \ No newline at end of file diff --git a/utils/aio_print_api.py b/utils/aio_print_api.py index b429699..9af8be6 100644 --- a/utils/aio_print_api.py +++ b/utils/aio_print_api.py @@ -19,13 +19,59 @@ class AIOPrrintSystemAPI: pass def get_status(self): - 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": {}} + + test_data = { + 'job': { + 'job': { + 'estimatedPrintTime': 1234, + 'filament': {'length': 765, 'volume': 24356}, + '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): return self._post_action("pause_print", action="pause") diff --git a/utils/floating_keyboard.py b/utils/floating_keyboard.py new file mode 100644 index 0000000..ac3f93d --- /dev/null +++ b/utils/floating_keyboard.py @@ -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()) diff --git a/utils/wifi_manager.py b/utils/wifi_manager.py index b1c7ade..074516d 100644 --- a/utils/wifi_manager.py +++ b/utils/wifi_manager.py @@ -89,6 +89,12 @@ class WifiManager: self._run_wpa_cli("save_config") 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): """删除某个已保存的网络""" self._run_wpa_cli("remove_network", str(network_id))