diff --git a/src/learning_mode_window.py b/src/learning_mode_window.py index 1ec350b..49305f4 100644 --- a/src/learning_mode_window.py +++ b/src/learning_mode_window.py @@ -14,6 +14,10 @@ from src.file_parser import FileParser from src.ui.theme_manager import theme_manager class LearningModeWindow(QMainWindow): + # 定义内容变化信号 + content_changed = pyqtSignal(str, int) # 参数:内容,位置 + # 定义关闭信号 + closed = pyqtSignal() def __init__(self, parent=None, imported_content="", current_position=0): """ 学习模式窗口 @@ -39,6 +43,9 @@ class LearningModeWindow(QMainWindow): # 初始化打字逻辑 self.init_typing_logic() + # 初始化同步位置跟踪 + self.last_sync_position = current_position + # 如果有导入内容,初始化显示 if self.imported_content: self.initialize_with_imported_content() @@ -335,6 +342,7 @@ class LearningModeWindow(QMainWindow): 文本变化处理 - 根据导入的内容逐步显示 - 更新学习进度 + - 同步内容到打字模式 """ # 如果正在加载文件,跳过处理 if self.is_loading_file: @@ -382,6 +390,7 @@ class LearningModeWindow(QMainWindow): self.status_label.setText(f"输入错误!期望字符: '{result.get('expected', '')}'") else: # 输入正确,更新进度 + old_position = self.current_position self.current_position = len(current_text) progress = (self.current_position / len(self.imported_content)) * 100 @@ -389,6 +398,15 @@ class LearningModeWindow(QMainWindow): f"进度: {progress:.1f}% ({self.current_position}/{len(self.imported_content)} 字符)" ) + # 只在用户新输入的字符上同步到打字模式 + if self.parent_window and hasattr(self.parent_window, 'text_edit'): + # 获取用户这一轮新输入的字符(与上一轮相比的新内容) + if old_position < self.current_position: + new_input = expected_text[old_position:self.current_position] + if new_input: # 只有新输入内容时才同步 + # 只同步新输入的内容,不传递整个文本 + self.content_changed.emit(new_input, len(new_input)) + # 检查是否完成 if result.get('completed', False): self.status_label.setText("恭喜!学习完成!") @@ -417,6 +435,9 @@ class LearningModeWindow(QMainWindow): 窗口关闭事件 - 通知父窗口学习模式已关闭 """ + # 发射关闭信号 + self.closed.emit() + if self.parent_window and hasattr(self.parent_window, 'on_learning_mode_closed'): self.parent_window.on_learning_mode_closed() diff --git a/src/main_window.py b/src/main_window.py index 6c8afa6..0dc3bd0 100644 --- a/src/main_window.py +++ b/src/main_window.py @@ -1,482 +1,9 @@ -import sys -import os -from PyQt5.QtWidgets import (QApplication, QMainWindow, QTextEdit, QAction, - QFileDialog, QVBoxLayout, QWidget, QLabel, QStatusBar, QMessageBox) -from PyQt5.QtGui import QFont, QTextCharFormat, QColor, QTextCursor -from PyQt5.QtCore import Qt, QTimer, QThread, pyqtSignal - -# 添加项目根目录到Python路径 -sys.path.append(os.path.join(os.path.dirname(__file__), '..')) - -# 导入自定义UI组件 -from src.ui.components import CustomTitleBar, ProgressBarWidget, TextDisplayWidget, StatsDisplayWidget, QuoteDisplayWidget, WeatherDisplayWidget -from src.file_parser import FileParser -from src.typing_logic import TypingLogic -from src.services.network_service import NetworkService - -class WeatherFetchThread(QThread): - """天气信息获取线程""" - weather_fetched = pyqtSignal(object) # 天气信息获取成功信号 - error_occurred = pyqtSignal(str) # 错误发生信号 - - def __init__(self): - super().__init__() - self.network_service = NetworkService() - - def run(self): - try: - weather_info = self.network_service.get_weather_info() - if weather_info: - # 格式化天气信息 - formatted_info = ( - f"天气: {weather_info['city']} - " - f"{weather_info['description']} - " - f"温度: {weather_info['temperature']}°C - " - f"湿度: {weather_info['humidity']}% - " - f"风速: {weather_info['wind_speed']} m/s" - ) - self.weather_fetched.emit(formatted_info) - else: - self.error_occurred.emit("无法获取天气信息") - except Exception as e: - self.error_occurred.emit(f"获取天气信息时出错: {str(e)}") - -class MainWindow(QMainWindow): - def __init__(self): +def on_learning_mode_closed(self): """ - 初始化主窗口 - - 设置窗口标题为"隐私学习软件 - 仿Word" - - 设置窗口大小为800x600 - - 初始化学习内容存储变量 - - 初始化当前输入位置 - - 调用initUI()方法 + 学习模式窗口关闭回调 + - 清除学习窗口引用 + - 更新菜单状态 """ - super().__init__() - self.learning_content = "" - self.current_position = 0 - self.typing_logic = None - self.text_edit = None - self.status_bar = None - self.title_bar = None - self.progress_bar_widget = None - self.text_display_widget = None - self.initUI() - - def initUI(self): - """ - 创建和布局所有UI组件 - - 创建自定义标题栏 - - 创建文本显示组件 - - 调用createMenuBar()创建菜单 - - 创建状态栏并显示"就绪" - """ - # 设置窗口属性 - self.setWindowTitle("隐私学习软件 - 仿Word") - self.setGeometry(100, 100, 800, 600) - self.setWindowFlags(Qt.FramelessWindowHint) # 移除默认标题栏 - - # 创建中央widget - central_widget = QWidget() - self.setCentralWidget(central_widget) - - # 创建主布局 - main_layout = QVBoxLayout() - main_layout.setContentsMargins(0, 0, 0, 0) - main_layout.setSpacing(0) - central_widget.setLayout(main_layout) - - # 创建自定义标题栏 - self.title_bar = CustomTitleBar(self) - main_layout.addWidget(self.title_bar) - - # 创建统计信息显示组件(默认隐藏) - self.stats_display = StatsDisplayWidget(self) - self.stats_display.setVisible(False) # 默认隐藏 - main_layout.addWidget(self.stats_display) - - # 创建每日一言显示组件(默认隐藏) - self.quote_display = QuoteDisplayWidget(self) - self.quote_display.setVisible(False) # 默认隐藏 - main_layout.addWidget(self.quote_display) - - # 创建天气显示组件(默认隐藏) - self.weather_display = WeatherDisplayWidget(self) - self.weather_display.setVisible(False) # 默认隐藏 - main_layout.addWidget(self.weather_display) - - # 创建文本显示组件 - self.text_display_widget = TextDisplayWidget(self) - main_layout.addWidget(self.text_display_widget) - - # 连接文本显示组件的文本变化信号 - self.text_display_widget.text_display.textChanged.connect(self.onTextChanged) - - # 创建菜单栏 - self.createMenuBar() - - # 创建状态栏 - self.status_bar = self.statusBar() - self.status_bar.showMessage("就绪") - - def createTopFunctionArea(self, main_layout): - """ - 创建顶部功能区域 - - 显示准确率、WPM等统计信息 - - 显示每日一言功能 - """ - from PyQt5.QtWidgets import QWidget, QHBoxLayout, QLabel, QPushButton - from PyQt5.QtCore import Qt - - # 创建顶部功能区域widget - top_widget = QWidget() - top_widget.setStyleSheet(""" - QWidget { - background-color: #f0f0f0; - border-bottom: 1px solid #d0d0d0; - } - """) - - # 创建水平布局 - top_layout = QHBoxLayout() - top_layout.setContentsMargins(10, 5, 10, 5) - top_layout.setSpacing(15) - - # 创建统计信息标签 - self.wpm_label = QLabel("WPM: 0") - self.accuracy_label = QLabel("准确率: 0%") - self.quote_label = QLabel("每日一言: 暂无") - self.quote_label.setStyleSheet("QLabel { color: #666666; font-style: italic; }") - - # 设置标签样式 - label_style = "font-size: 12px; font-weight: normal; color: #333333;" - self.wpm_label.setStyleSheet(label_style) - self.accuracy_label.setStyleSheet(label_style) - - # 创建每日一言刷新按钮 - self.refresh_quote_button = QPushButton("刷新") - self.refresh_quote_button.setStyleSheet(""" - QPushButton { - background-color: #0078d7; - color: white; - border: none; - padding: 5px 10px; - border-radius: 3px; - font-size: 12px; - } - QPushButton:hover { - background-color: #005a9e; - } - """) - self.refresh_quote_button.clicked.connect(self.refresh_daily_quote) - - # 添加组件到布局 - top_layout.addWidget(self.wpm_label) - top_layout.addWidget(self.accuracy_label) - top_layout.addStretch() - top_layout.addWidget(self.quote_label) - top_layout.addWidget(self.refresh_quote_button) - - top_widget.setLayout(top_layout) - main_layout.addWidget(top_widget) - - def createMenuBar(self): - """ - 创建菜单栏和所有菜单项 - - 文件菜单:打开(Ctrl+O)、保存(Ctrl+S)、退出(Ctrl+Q) - - 视图菜单:显示统计信息、显示每日一言 - - 帮助菜单:关于 - - 为每个菜单项连接对应的槽函数 - """ - menu_bar = self.menuBar() - - # 文件菜单 - file_menu = menu_bar.addMenu('文件') - - # 打开动作 - open_action = QAction('打开', self) - open_action.setShortcut('Ctrl+O') - open_action.triggered.connect(self.openFile) - file_menu.addAction(open_action) - - # 保存动作 - save_action = QAction('保存', self) - save_action.setShortcut('Ctrl+S') - save_action.triggered.connect(self.saveFile) - file_menu.addAction(save_action) - - # 分隔线 - file_menu.addSeparator() - - # 退出动作 - exit_action = QAction('退出', self) - exit_action.setShortcut('Ctrl+Q') - exit_action.triggered.connect(self.close) - file_menu.addAction(exit_action) - - # 视图菜单 - view_menu = menu_bar.addMenu('视图') - - # 显示统计信息动作 - self.stats_action = QAction('显示统计信息', self) - self.stats_action.setCheckable(True) - self.stats_action.setChecked(True) - self.stats_action.triggered.connect(self.toggleStatsDisplay) - view_menu.addAction(self.stats_action) - - # 显示每日一言动作 - self.quote_action = QAction('显示每日一言', self) - self.quote_action.setCheckable(True) - self.quote_action.setChecked(True) - self.quote_action.triggered.connect(self.toggleQuoteDisplay) - view_menu.addAction(self.quote_action) - - # 显示天气信息动作 - self.weather_action = QAction('显示天气', self) - self.weather_action.setCheckable(True) - self.weather_action.setChecked(True) - self.weather_action.triggered.connect(self.toggleWeatherDisplay) - view_menu.addAction(self.weather_action) - - # 帮助菜单 - help_menu = menu_bar.addMenu('帮助') - - # 关于动作 - about_action = QAction('关于', self) - about_action.triggered.connect(self.showAbout) - help_menu.addAction(about_action) - - def toggleStatsDisplay(self, checked): - """ - 切换统计信息显示 - - checked: 是否显示统计信息 - """ - self.stats_display.setVisible(checked) - - def toggleQuoteDisplay(self, checked): - """ - 切换每日一言显示 - - checked: 是否显示每日一言 - """ - self.quote_display.setVisible(checked) - # 如果启用显示且quote为空,则刷新一次 - if checked and not self.quote_display.quote_label.text(): - self.refresh_daily_quote() - - def toggleWeatherDisplay(self, checked): - """切换天气信息显示""" - self.weather_display.setVisible(checked) - # 如果启用显示且天气信息为空,则刷新一次 - if checked and not self.weather_display.weather_label.text(): - self.refresh_weather_info() - - def openFile(self): - """ - 打开文件选择对话框并加载选中的文件 - - 显示文件选择对话框,过滤条件:*.txt, *.docx, *.pdf - - 如果用户选择了文件,调用FileParser.parse_file(file_path) - - 成功时:将内容存储但不直接显示,重置打字状态 - - 失败时:显示错误消息框 - """ - options = QFileDialog.Options() - file_path, _ = QFileDialog.getOpenFileName( - self, - "打开文件", - "", - "文本文件 (*.txt);;Word文档 (*.docx);;PDF文件 (*.pdf);;所有文件 (*)", - options=options - ) - - if file_path: - try: - # 解析文件内容 - content = FileParser.parse_file(file_path) - self.learning_content = content - - # 在文本显示组件中设置内容(初始为空,通过打字逐步显示) - if self.text_display_widget: - self.text_display_widget.set_text(content) # 设置文件内容 - - # 重置打字状态 - self.typing_logic = TypingLogic(content) - self.current_position = 0 - - # 更新状态栏 - self.status_bar.showMessage(f"已打开文件: {file_path},开始打字以显示内容") - except Exception as e: - # 显示错误消息框 - QMessageBox.critical(self, "错误", f"无法打开文件:\n{str(e)}") - - def saveFile(self): - """ - 保存当前内容到文件 - - 显示保存文件对话框 - - 将文本区域内容写入选定文件 - - 返回操作结果 - """ - options = QFileDialog.Options() - file_path, _ = QFileDialog.getSaveFileName( - self, - "保存文件", - "", - "文本文件 (*.txt);;所有文件 (*)", - options=options - ) - - if file_path: - try: - # 获取文本编辑区域的内容 - content = self.text_edit.toPlainText() - - # 写入文件 - with open(file_path, 'w', encoding='utf-8') as f: - f.write(content) - - # 更新状态栏 - self.status_bar.showMessage(f"文件已保存: {file_path}") - - return True - except Exception as e: - # 显示错误消息框 - QMessageBox.critical(self, "错误", f"无法保存文件:\n{str(e)}") - return False - - return False - - def showAbout(self): - """ - 显示关于对话框 - - 显示消息框,包含软件名称、版本、描述 - """ - QMessageBox.about( - self, - "关于", - "隐私学习软件 - 仿Word\n\n" - "版本: 1.0\n\n" - "这是一个用于隐私学习的打字练习软件,\n" - "可以加载文档并进行打字练习,\n" - "帮助提高打字速度和准确性。" - ) - - def refresh_daily_quote(self): - """ - 刷新每日一言 - - 从网络API获取名言 - - 更新显示 - """ - import requests - import json - from PyQt5.QtCore import Qt - from src.constants import QUOTE_API_URL - - try: - # 发送请求获取每日一言 - response = requests.get(QUOTE_API_URL, timeout=5) - if response.status_code == 200: - data = response.json() - quote_content = data.get('content', '暂无内容') - quote_author = data.get('author', '未知作者') - - # 更新显示 - self.quote_label.setText(f"每日一言: {quote_content} — {quote_author}") - - # 同时更新统计信息显示组件中的每日一言 - if hasattr(self, 'stats_display') and self.stats_display: - self.stats_display.update_quote(f"{quote_content} — {quote_author}") - else: - self.quote_label.setText("每日一言: 获取失败") - # 同时更新统计信息显示组件中的每日一言 - if hasattr(self, 'stats_display') and self.stats_display: - self.stats_display.update_quote("获取失败") - except Exception as e: - self.quote_label.setText("每日一言: 获取失败") - # 同时更新统计信息显示组件中的每日一言 - if hasattr(self, 'stats_display') and self.stats_display: - self.stats_display.update_quote("获取失败") - - def onTextChanged(self): - """ - 处理用户输入变化事件(打字练习) - - 获取文本显示组件中的文本 - - 使用TypingLogic.check_input检查输入 - - 根据结果更新文本显示组件 - - 更新统计数据展示 - """ - # 防止递归调用 - if getattr(self, '_processing_text_change', False): - return - - if not self.typing_logic: - return - - # 设置标志防止递归 - self._processing_text_change = True - - try: - # 获取当前输入文本 - current_text = self.text_display_widget.text_display.toPlainText() - - # 检查输入是否正确 - result = self.typing_logic.check_input(current_text) - is_correct = result["correct"] - expected_char = result["expected"] - - # 更新文本显示组件 - if self.text_display_widget: - # 显示用户输入反馈 - self.text_display_widget.show_user_input(current_text) - - # 不再高亮下一个字符,因为内容通过打字逐步显示 - - # 计算统计数据 - stats = self.typing_logic.get_statistics() - accuracy = stats['accuracy_rate'] * 100 # 转换为百分比 - # 可以根据需要添加更多统计数据的计算 - wpm = 0 # 暂时设置为0,后续可以实现WPM计算 - - # 更新状态栏 - self.status_bar.showMessage(f"WPM: {wpm:.1f} | 准确率: {accuracy:.1f}%") - - # 更新统计信息显示组件 - if hasattr(self, 'stats_display') and self.stats_display.isVisible(): - self.stats_display.update_stats(int(wpm), accuracy) - - # 更新每日一言显示组件(如果需要) - if hasattr(self, 'quote_display') and self.quote_display.isVisible() and not self.quote_display.quote_label.text(): - self.refresh_daily_quote() - - # 更新顶部功能区的统计数据(如果仍然存在) - if hasattr(self, 'wpm_label') and self.wpm_label: - self.wpm_label.setText(f"WPM: {wpm:.1f}") - if hasattr(self, 'accuracy_label') and self.accuracy_label: - self.accuracy_label.setText(f"准确率: {accuracy:.1f}%") - finally: - # 清除递归防止标志 - self._processing_text_change = False - - def refresh_daily_quote(self): - """刷新每日一言""" - # 创建并启动获取名言的线程 - self.quote_thread = QuoteFetchThread() - self.quote_thread.quote_fetched.connect(self.on_quote_fetched) - self.quote_thread.error_occurred.connect(self.on_quote_error) - self.quote_thread.start() - - def refresh_weather_info(self): - """刷新天气信息""" - # 创建并启动获取天气信息的线程 - self.weather_thread = WeatherFetchThread() - self.weather_thread.weather_fetched.connect(self.on_weather_fetched) - self.weather_thread.error_occurred.connect(self.on_weather_error) - self.weather_thread.start() - - def on_weather_fetched(self, weather_info): - """处理天气信息获取成功""" - # 更新天气显示组件 - if hasattr(self, 'weather_display') and self.weather_display: - self.weather_display.update_weather(weather_info) - - def on_weather_error(self, error_msg): - """处理天气信息获取错误""" - # 更新天气显示组件 - if hasattr(self, 'weather_display') and self.weather_display: - self.weather_display.update_weather(error_msg) \ No newline at end of file + self.learning_window = None + self.learning_mode_action.setChecked(False) + self.typing_mode_action.setChecked(True) \ No newline at end of file diff --git a/src/word_main_window.py b/src/word_main_window.py index 2a7f7da..9fcdd7c 100644 --- a/src/word_main_window.py +++ b/src/word_main_window.py @@ -83,6 +83,10 @@ class WordStyleMainWindow(QMainWindow): self.learning_text = "" # 学习模式下的文本内容 self.cursor_position = 0 # 光标位置 + # 学习模式窗口引用和同步标记 + self.learning_window = None # 学习模式窗口引用 + self.sync_from_learning = False # 从学习模式同步内容的标记 + # 统一文档内容管理 self.unified_document_content = "" # 统一文档内容 self.last_edit_mode = "typing" # 上次编辑模式 @@ -836,6 +840,10 @@ class WordStyleMainWindow(QMainWindow): # 如果正在加载文件,跳过处理 if self.is_loading_file: return + + # 检查是否是从学习模式同步内容,避免递归调用 + if hasattr(self, 'sync_from_learning') and self.sync_from_learning: + return # 根据当前视图模式处理 if self.view_mode == "learning": @@ -852,8 +860,9 @@ class WordStyleMainWindow(QMainWindow): self.handle_learning_mode_typing() elif self.view_mode == "typing": - # 打字模式:可以自由打字 - self.handle_typing_mode_typing() + # 打字模式:可以自由打字,不自动处理内容 + # 只在用户主动操作时处理,避免内容被覆盖 + pass # 标记文档为已修改 if not self.is_modified: @@ -1856,10 +1865,24 @@ class WordStyleMainWindow(QMainWindow): imported_content = self.imported_content if hasattr(self, 'learning_progress') and self.learning_progress > 0: current_position = self.learning_progress + else: + # 如果没有导入内容,检查当前打字模式的内容 + current_text = self.text_edit.toPlainText() + if current_text and current_text != "在此输入您的内容...": + # 将打字模式的内容作为学习模式的导入内容 + imported_content = current_text + current_position = 0 + self.imported_content = current_text # 创建学习模式窗口,直接传递导入内容 self.learning_window = LearningModeWindow(self, imported_content, current_position) + # 连接学习模式窗口的内容变化信号 + self.learning_window.content_changed.connect(self.on_learning_content_changed) + + # 连接学习模式窗口的关闭信号 + self.learning_window.closed.connect(self.on_learning_mode_closed) + # 显示学习模式窗口 self.learning_window.show() @@ -1890,8 +1913,46 @@ class WordStyleMainWindow(QMainWindow): self.learning_mode_action.setChecked(False) self.typing_mode_action.setChecked(True) self.view_mode = "typing" + + # 清除学习窗口引用 + self.learning_window = None + self.status_bar.showMessage("学习模式窗口已关闭", 2000) + def on_learning_content_changed(self, new_content, position): + """学习模式内容变化时的回调 - 只在末尾追加新内容""" + # 设置同步标记,防止递归调用 + self.sync_from_learning = True + + try: + # 只在末尾追加新输入的内容,不修改已有内容 + if new_content: + # 直接在末尾追加新内容 + cursor = self.text_edit.textCursor() + cursor.movePosition(QTextCursor.End) + cursor.insertText(new_content) + + # 更新导入内容(但不覆盖用户额外输入的内容) + if self.imported_content: + self.imported_content += new_content + else: + self.imported_content = new_content + + self.learning_progress = len(self.imported_content) if self.imported_content else 0 + + # 重置打字逻辑(不追踪进度) + if self.typing_logic: + self.typing_logic.imported_content = self.imported_content + self.typing_logic.current_index = self.learning_progress + self.typing_logic.typed_chars = self.learning_progress + + # 更新状态栏 + self.status_bar.showMessage(f"从学习模式同步新内容: {new_content}", 3000) + + finally: + # 重置同步标记 + self.sync_from_learning = False + def set_page_color(self, color): """设置页面颜色""" color_map = { diff --git a/test.docx b/test.docx new file mode 100644 index 0000000..2564c62 Binary files /dev/null and b/test.docx differ