diff --git a/MARKTEXT_README.md b/MARKTEXT_README.md new file mode 100644 index 0000000..cf4279e --- /dev/null +++ b/MARKTEXT_README.md @@ -0,0 +1,235 @@ +# MagicWord - MarkText风格编辑器 + +基于MarkText开源项目的现代化Markdown编辑器,集成了MagicWord的所有核心功能。 + +## 🎯 功能特性 + +### 核心编辑功能 +- **现代化界面**: 基于MarkText的设计风格,简洁优雅 +- **多标签编辑**: 支持同时编辑多个文档 +- **Markdown支持**: 原生支持Markdown语法高亮和预览 +- **文件操作**: 支持新建、打开、保存、另存为等操作 +- **主题切换**: 支持浅色/深色主题 + +### 学习模式集成 +- **打字学习**: 集成MagicWord的打字学习功能 +- **进度跟踪**: 实时显示学习进度 +- **多格式支持**: 支持TXT、DOCX、PDF等格式导入学习 +- **智能显示**: 根据打字进度逐步显示内容 + +### 实用工具 +- **天气信息**: 实时显示天气状况 +- **每日名言**: 显示励志名言和诗句 +- **字数统计**: 实时统计文档字数和字符数 +- **文件管理**: 侧边栏显示最近文件 + +### 高级功能 +- **拖放支持**: 支持拖拽文件到编辑器 +- **快捷键**: 完整的快捷键支持 +- **自动保存**: 定期自动保存文档 +- **错误恢复**: 异常关闭时的文档恢复 + +## 🚀 快速开始 + +### 方式一:直接运行启动器 +```bash +python start_marktext.py +``` + +### 方式二:通过主程序启动 +```bash +python src/main.py +``` + +### 方式三:运行编辑器模块 +```bash +python src/marktext_editor_window.py +``` + +## 📋 使用说明 + +### 基本操作 +1. **新建文档**: 点击"新建"按钮或使用快捷键 `Ctrl+N` +2. **打开文档**: 点击"打开"按钮或使用快捷键 `Ctrl+O` +3. **保存文档**: 点击"保存"按钮或使用快捷键 `Ctrl+S` +4. **另存为**: 使用快捷键 `Ctrl+Shift+S` + +### 模式切换 +- **编辑模式**: 默认模式,自由编辑文档 +- **学习模式**: 切换到打字学习模式,支持多格式文件导入 + +### 文件导入学习 +1. 点击"导入文件"按钮 +2. 选择TXT、DOCX或PDF文件 +3. 自动切换到学习模式 +4. 开始打字学习,内容会逐步显示 + +### 天气和名言 +- **自动更新**: 每10分钟自动获取最新天气和名言 +- **手动刷新**: 在工具菜单中可以手动刷新 +- **信息显示**: 底部状态栏实时显示 + +## 🎨 界面介绍 + +``` +┌─────────────────────────────────────────────────────────────┐ +│ 文件 编辑 模式 工具 主题 │ +├─────────────────────────────────────────────────────────────┤ +│ [新建][打开][保存][撤销][重做][剪切][复制][粘贴][学习模式] │ +├─────────────┬───────────────────────────────────────────────┤ +│ │ │ +│ 侧边栏 │ 编辑器区域 │ +│ 文件操作 │ (多标签) │ +│ 学习模式 │ │ +│ 实用工具 │ │ +│ │ │ +├─────────────┴───────────────────────────────────────────────┤ +│ 天气: 北京 25°C 晴天 | 名言: 励志内容... | 字数: 1234 │ +└─────────────────────────────────────────────────────────────┘ +``` + +## ⌨️ 快捷键 + +| 快捷键 | 功能 | +|--------|------| +| Ctrl+N | 新建文档 | +| Ctrl+O | 打开文档 | +| Ctrl+S | 保存文档 | +| Ctrl+Shift+S | 另存为 | +| Ctrl+Z | 撤销 | +| Ctrl+Y | 重做 | +| Ctrl+X | 剪切 | +| Ctrl+C | 复制 | +| Ctrl+V | 粘贴 | +| Ctrl+Q | 退出应用 | + +## 🔧 配置说明 + +### 环境变量 +- `QT_PLUGIN_PATH`: Qt插件路径(自动设置) +- `QT_QPA_PLATFORM`: 平台类型(自动设置) + +### 配置文件 +- 主题设置:自动保存用户偏好 +- 窗口状态:记住窗口大小和位置 +- 最近文件:自动记录最近打开的文档 + +## 📁 项目结构 + +``` +MagicWord/ +├── src/ +│ ├── marktext_editor_window.py # MarkText主窗口 +│ ├── word_main_window.py # Word风格主窗口 +│ ├── main.py # 主程序入口 +│ ├── services/ # 服务模块 +│ │ ├── network_service.py # 网络服务(天气、名言) +│ │ └── file_service.py # 文件服务 +│ ├── ui/ # UI组件 +│ │ ├── theme_manager.py # 主题管理 +│ │ └── components/ # UI组件 +│ └── learning_mode_window.py # 学习模式窗口 +├── start_marktext.py # MarkText启动器 +├── MARKTEXT_README.md # 本文档 +└── requirements.txt # 依赖列表 +``` + +## 🛠️ 开发说明 + +### 基于MarkText架构 +本编辑器基于MarkText的开源项目架构,结合MagicWord的功能需求进行定制开发: + +1. **模块化设计**: 采用组件化架构,便于扩展和维护 +2. **信号槽机制**: 使用PyQt的信号槽机制实现组件间通信 +3. **主题系统**: 集成MagicWord的主题管理器,支持动态主题切换 +4. **服务集成**: 整合现有的网络服务、文件服务等 + +### 核心类说明 + +#### MarkTextMainWindow +主窗口类,负责: +- UI布局管理 +- 菜单和工具栏 +- 多标签编辑器管理 +- 模式切换 +- 状态栏信息更新 + +#### MarkTextEditor +编辑器组件,负责: +- 文本编辑功能 +- 文件加载和保存 +- 内容变化通知 +- 语法高亮(可扩展) + +#### MarkTextSideBar +侧边栏组件,负责: +- 文件操作按钮 +- 学习模式控制 +- 实用工具集成 + +### 扩展开发 + +#### 添加新的编辑器功能 +```python +class CustomEditor(MarkTextEditor): + def __init__(self, parent=None): + super().__init__(parent) + # 添加自定义功能 + + def custom_function(self): + # 实现自定义功能 + pass +``` + +#### 添加新的侧边栏工具 +```python +class CustomSideBar(MarkTextSideBar): + def __init__(self, parent=None): + super().__init__(parent) + # 添加自定义工具按钮 + + def add_custom_tool(self): + # 添加自定义工具 + pass +``` + +## 🔍 故障排除 + +### 常见问题 + +1. **Qt插件路径错误** + - 检查PyQt5是否正确安装 + - 运行启动器脚本自动设置路径 + +2. **依赖包缺失** + - 运行 `pip install -r requirements.txt` + - 检查Python版本兼容性 + +3. **网络功能异常** + - 检查网络连接 + - 确认API服务可用性 + +4. **文件导入失败** + - 检查文件格式支持 + - 确认文件权限 + +### 调试模式 +运行应用时添加调试参数: +```bash +python start_marktext.py --debug +``` + +## 📞 支持 + +如有问题,请: +1. 检查本README的故障排除部分 +2. 查看控制台错误信息 +3. 提交Issue到项目仓库 + +## 📄 许可证 + +本项目基于MarkText开源项目,遵循相应的开源协议。 + +--- + +**享受现代化的Markdown编辑体验!** 🎉 \ No newline at end of file diff --git a/src/learning_mode_window.py b/src/learning_mode_window.py index 4634578..82d5b89 100644 --- a/src/learning_mode_window.py +++ b/src/learning_mode_window.py @@ -516,7 +516,7 @@ class LearningModeWindow(QMainWindow): # 只在用户新输入的字符上同步到打字模式 - if self.parent_window and hasattr(self.parent_window, 'text_edit'): + if self.parent_window: # 获取用户这一轮新输入的字符(与上一轮相比的新内容) if old_position < self.current_position: new_input = expected_text[old_position:self.current_position] diff --git a/src/main(abandoned).py b/src/main(abandoned).py new file mode 100644 index 0000000..7bde1c7 --- /dev/null +++ b/src/main(abandoned).py @@ -0,0 +1,153 @@ +# main.py +import sys +import traceback +import os +import platform + +# 添加项目根目录到Python路径 +project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +sys.path.insert(0, project_root) + +# 设置Qt平台插件路径 - 增强版本,完全避免平台插件问题 +def set_qt_plugin_path(): + """设置Qt平台插件路径,确保所有平台插件都能正确加载""" + system = platform.system() + + # 获取Python版本 + python_version = f"python{sys.version_info.major}.{sys.version_info.minor}" + + # 可能的Qt插件路径列表 + possible_paths = [] + + if system == "Windows": + # Windows环境下的路径 + possible_paths.extend([ + os.path.join(project_root, '.venv', 'Lib', 'site-packages', 'PyQt5', 'Qt5', 'plugins'), + os.path.join(sys.prefix, 'Lib', 'site-packages', 'PyQt5', 'Qt5', 'plugins'), + os.path.join(os.path.expanduser('~'), 'AppData', 'Local', 'Programs', 'Python', 'Python39', 'Lib', 'site-packages', 'PyQt5', 'Qt5', 'plugins'), + os.path.join(os.path.expanduser('~'), 'AppData', 'Roaming', 'Python', 'Python39', 'site-packages', 'PyQt5', 'Qt5', 'plugins'), + ]) + elif system == "Darwin": # macOS + # macOS环境下的路径 + possible_paths.extend([ + os.path.join(project_root, '.venv', 'lib', python_version, 'site-packages', 'PyQt5', 'Qt5', 'plugins'), + os.path.join(sys.prefix, 'lib', python_version, 'site-packages', 'PyQt5', 'Qt5', 'plugins'), + '/usr/local/opt/qt5/plugins', # Homebrew Qt5 + '/opt/homebrew/opt/qt5/plugins', # Apple Silicon Homebrew + os.path.expanduser('~/Qt/5.15.2/clang_64/plugins'), # Qt官方安装 + ]) + elif system == "Linux": + # Linux环境下的路径 + possible_paths.extend([ + os.path.join(project_root, '.venv', 'lib', python_version, 'site-packages', 'PyQt5', 'Qt5', 'plugins'), + os.path.join(sys.prefix, 'lib', python_version, 'site-packages', 'PyQt5', 'Qt5', 'plugins'), + '/usr/lib/x86_64-linux-gnu/qt5/plugins', + '/usr/lib/qt5/plugins', + ]) + + # 查找第一个存在的路径 + valid_path = None + for path in possible_paths: + if os.path.exists(path) and os.path.exists(os.path.join(path, 'platforms')): + valid_path = path + break + + if valid_path: + # 设置Qt插件路径 + os.environ['QT_PLUGIN_PATH'] = valid_path + os.environ['QT_QPA_PLATFORM_PLUGIN_PATH'] = os.path.join(valid_path, 'platforms') + + # 设置平台特定的环境变量 + if system == "Darwin": # macOS + os.environ['QT_QPA_PLATFORM'] = 'cocoa' + os.environ['QT_MAC_WANTS_LAYER'] = '1' + # 禁用可能导致问题的Qt功能 + os.environ['QT_LOGGING_RULES'] = 'qt.qpa.*=false' # 禁用Qt警告日志 + elif system == "Windows": + os.environ['QT_QPA_PLATFORM'] = 'windows' + elif system == "Linux": + os.environ['QT_QPA_PLATFORM'] = 'xcb' + # 对于Linux,可能需要设置DISPLAY + if 'DISPLAY' not in os.environ: + os.environ['DISPLAY'] = ':0' + + print(f"✅ Qt插件路径设置成功: {valid_path}") + return True + else: + print("⚠️ 警告:未找到Qt插件路径") + return False + +# 设置Qt平台插件路径 +set_qt_plugin_path() + +from PyQt5.QtWidgets import QApplication +from PyQt5.QtCore import Qt +from PyQt5.QtGui import QIcon +from word_main_window import WordStyleMainWindow + +# 设置高DPI支持(必须在QApplication创建之前) +QApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True) +QApplication.setAttribute(Qt.AA_UseHighDpiPixmaps, True) + +def main(): + """应用程序入口函数 - Word风格版本""" + try: + # 创建QApplication实例 + app = QApplication(sys.argv) + + # 在macOS上使用系统原生样式,在其他平台上使用WindowsVista样式 + if platform.system() != "Darwin": # 不是macOS系统 + # 设置应用程序样式为Windows风格,更接近Word界面 + app.setStyle('WindowsVista') + + # 设置应用程序属性 + app.setApplicationName("MagicWord") + app.setApplicationVersion("1.0.0") + app.setOrganizationName("MagicWord") + + # 设置窗口图标(如果存在) + icon_files = [ + 'app_icon_32*32.png', + 'app_icon_64*64.png', + 'app_icon_128*128.png', + 'app_icon_256*256.png', + 'app_icon.png' + ] + + icon_path = None + for icon_file in icon_files: + test_path = os.path.join(project_root, 'resources', 'icons', icon_file) + if os.path.exists(test_path): + icon_path = test_path + break + + if icon_path and os.path.exists(icon_path): + app.setWindowIcon(QIcon(icon_path)) + else: + # 使用默认图标 + app.setWindowIcon(QIcon()) + + # 创建MarkText风格的主窗口(基于MarkText架构) + try: + from main import MarkTextMainWindow + main_window = MarkTextMainWindow() + print("✅ 已启动MarkText风格编辑器") + except ImportError as e: + print(f"⚠️ MarkText编辑器导入失败: {e},回退到Word风格") + # 回退到Word风格的主窗口 + main_window = WordStyleMainWindow() + + main_window.show() + + # 启动事件循环并返回退出码 + exit_code = app.exec_() + sys.exit(exit_code) + + except Exception as e: + # 打印详细的错误信息 + print(f"应用程序发生未捕获的异常: {e}") + traceback.print_exc() + sys.exit(1) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/src/main.py b/src/main.py index 56b0016..d532545 100644 --- a/src/main.py +++ b/src/main.py @@ -1,145 +1,2275 @@ -# main.py -import sys -import traceback +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +MarkText风格的Markdown编辑器窗口 +基于MagicWord现有功能,集成MarkText的现代化编辑器界面 +""" + import os -import platform - -# 添加项目根目录到Python路径 -project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) -sys.path.insert(0, project_root) - -# 设置Qt平台插件路径 - 增强版本,完全避免平台插件问题 -def set_qt_plugin_path(): - """设置Qt平台插件路径,确保所有平台插件都能正确加载""" - system = platform.system() - - # 获取Python版本 - python_version = f"python{sys.version_info.major}.{sys.version_info.minor}" - - # 可能的Qt插件路径列表 - possible_paths = [] - - if system == "Windows": - # Windows环境下的路径 - possible_paths.extend([ - os.path.join(project_root, '.venv', 'Lib', 'site-packages', 'PyQt5', 'Qt5', 'plugins'), - os.path.join(sys.prefix, 'Lib', 'site-packages', 'PyQt5', 'Qt5', 'plugins'), - os.path.join(os.path.expanduser('~'), 'AppData', 'Local', 'Programs', 'Python', 'Python39', 'Lib', 'site-packages', 'PyQt5', 'Qt5', 'plugins'), - os.path.join(os.path.expanduser('~'), 'AppData', 'Roaming', 'Python', 'Python39', 'site-packages', 'PyQt5', 'Qt5', 'plugins'), - ]) - elif system == "Darwin": # macOS - # macOS环境下的路径 - possible_paths.extend([ - os.path.join(project_root, '.venv', 'lib', python_version, 'site-packages', 'PyQt5', 'Qt5', 'plugins'), - os.path.join(sys.prefix, 'lib', python_version, 'site-packages', 'PyQt5', 'Qt5', 'plugins'), - '/usr/local/opt/qt5/plugins', # Homebrew Qt5 - '/opt/homebrew/opt/qt5/plugins', # Apple Silicon Homebrew - os.path.expanduser('~/Qt/5.15.2/clang_64/plugins'), # Qt官方安装 - ]) - elif system == "Linux": - # Linux环境下的路径 - possible_paths.extend([ - os.path.join(project_root, '.venv', 'lib', python_version, 'site-packages', 'PyQt5', 'Qt5', 'plugins'), - os.path.join(sys.prefix, 'lib', python_version, 'site-packages', 'PyQt5', 'Qt5', 'plugins'), - '/usr/lib/x86_64-linux-gnu/qt5/plugins', - '/usr/lib/qt5/plugins', - ]) - - # 查找第一个存在的路径 - valid_path = None - for path in possible_paths: - if os.path.exists(path) and os.path.exists(os.path.join(path, 'platforms')): - valid_path = path - break - - if valid_path: - # 设置Qt插件路径 - os.environ['QT_PLUGIN_PATH'] = valid_path - os.environ['QT_QPA_PLATFORM_PLUGIN_PATH'] = os.path.join(valid_path, 'platforms') - - # 设置平台特定的环境变量 - if system == "Darwin": # macOS - os.environ['QT_QPA_PLATFORM'] = 'cocoa' - os.environ['QT_MAC_WANTS_LAYER'] = '1' - # 禁用可能导致问题的Qt功能 - os.environ['QT_LOGGING_RULES'] = 'qt.qpa.*=false' # 禁用Qt警告日志 - elif system == "Windows": - os.environ['QT_QPA_PLATFORM'] = 'windows' - elif system == "Linux": - os.environ['QT_QPA_PLATFORM'] = 'xcb' - # 对于Linux,可能需要设置DISPLAY - if 'DISPLAY' not in os.environ: - os.environ['DISPLAY'] = ':0' - - print(f"✅ Qt插件路径设置成功: {valid_path}") - return True - else: - print("⚠️ 警告:未找到Qt插件路径") - return False - -# 设置Qt平台插件路径 -set_qt_plugin_path() - -from PyQt5.QtWidgets import QApplication -from PyQt5.QtCore import Qt -from PyQt5.QtGui import QIcon -from word_main_window import WordStyleMainWindow - -# 设置高DPI支持(必须在QApplication创建之前) -QApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True) -QApplication.setAttribute(Qt.AA_UseHighDpiPixmaps, True) - -def main(): - """应用程序入口函数 - Word风格版本""" - try: - # 创建QApplication实例 - app = QApplication(sys.argv) - - # 在macOS上使用系统原生样式,在其他平台上使用WindowsVista样式 - if platform.system() != "Darwin": # 不是macOS系统 - # 设置应用程序样式为Windows风格,更接近Word界面 - app.setStyle('WindowsVista') - - # 设置应用程序属性 - app.setApplicationName("MagicWord") - app.setApplicationVersion("1.0.0") - app.setOrganizationName("MagicWord") - - # 设置窗口图标(如果存在) - icon_files = [ - 'app_icon_32*32.png', - 'app_icon_64*64.png', - 'app_icon_128*128.png', - 'app_icon_256*256.png', - 'app_icon.png' +import sys +import json +import subprocess +from pathlib import Path +from typing import Optional, Dict, Any, List +from PyQt5.QtWidgets import (QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, + QTextEdit, QPlainTextEdit, QMenuBar, QMenu, QAction, QFileDialog, + QMessageBox, QSplitter, QTabWidget, QStatusBar, + QToolBar, QPushButton, QLabel, QFrame, QApplication, + QDesktopWidget, QStyleFactory, QStyle, QGroupBox, QProgressBar, + QScrollArea, QTextBrowser, QFontComboBox, QComboBox, QColorDialog, + QDialog, QLineEdit, QFormLayout, QListWidget, QHBoxLayout, + QCalendarWidget) +from PyQt5.QtCore import (Qt, QTimer, pyqtSignal, QThread, QObject, QUrl, + QSettings, QPoint, QSize, QEvent, QPropertyAnimation, + QEasingCurve, QRect) +from PyQt5.QtGui import (QFont, QPalette, QColor, QIcon, QKeySequence, + QTextCursor, QTextCharFormat, QPainter, QPixmap, + QFontDatabase, QSyntaxHighlighter, QTextDocument, QFontInfo) + +# 导入MagicWord现有功能 +from services.network_service import NetworkService +from file_manager.file_operations import FileManager +from learning_mode_window import LearningModeWindow +from ui.theme_manager import ThemeManager +from ui.ai_chat_panel import AIChatPanel +from ui.snake_game import SnakeGameWindow + + +class MarkTextEditor(QPlainTextEdit): + """MarkText风格的Markdown编辑器组件""" + + text_changed = pyqtSignal() + + def __init__(self, parent=None): + super().__init__(parent) + self.parent = parent + self.setup_editor() + self.current_file_path = None + self.is_modified = False + + def apply_format(self, format_type: str, value=None): + """应用文本格式""" + cursor = self.textCursor() + if not cursor.hasSelection(): + # 如果没有选中文本,设置默认格式供后续输入使用 + char_format = self.currentCharFormat() + + if format_type == "bold": + char_format.setFontWeight(QFont.Bold if value else QFont.Normal) + elif format_type == "italic": + char_format.setFontItalic(value) + elif format_type == "underline": + char_format.setFontUnderline(value) + elif format_type == "font_family": + char_format.setFontFamily(value) + elif format_type == "font_size": + char_format.setFontPointSize(value) + elif format_type == "color": + char_format.setForeground(QColor(value)) + elif format_type == "background_color": + char_format.setBackground(QColor(value)) + + self.setCurrentCharFormat(char_format) + return + + # 如果有选中文本,直接修改选中文本的格式 + char_format = cursor.charFormat() + + if format_type == "bold": + char_format.setFontWeight(QFont.Bold if value else QFont.Normal) + elif format_type == "italic": + char_format.setFontItalic(value) + elif format_type == "underline": + char_format.setFontUnderline(value) + elif format_type == "font_family": + char_format.setFontFamily(value) + elif format_type == "font_size": + char_format.setFontPointSize(value) + elif format_type == "color": + char_format.setForeground(QColor(value)) + elif format_type == "background_color": + char_format.setBackground(QColor(value)) + elif format_type == "strikethrough": + char_format.setFontStrikeOut(value) + elif format_type == "superscript": + char_format.setVerticalAlignment(QTextCharFormat.AlignSuperScript if value else QTextCharFormat.AlignNormal) + elif format_type == "subscript": + char_format.setVerticalAlignment(QTextCharFormat.AlignSubScript if value else QTextCharFormat.AlignNormal) + + cursor.setCharFormat(char_format) + + def apply_paragraph_format(self, format_type: str, value=None): + """应用段落格式""" + cursor = self.textCursor() + block_format = cursor.blockFormat() + + if format_type == "alignment": + block_format.setAlignment(value) + elif format_type == "line_spacing": + block_format.setLineHeight(value, QTextBlockFormat.ProportionalHeight) + elif format_type == "top_margin": + block_format.setTopMargin(value) + elif format_type == "bottom_margin": + block_format.setBottomMargin(value) + elif format_type == "left_margin": + block_format.setLeftMargin(value) + elif format_type == "right_margin": + block_format.setRightMargin(value) + elif format_type == "indent": + block_format.setIndent(value) + + cursor.setBlockFormat(block_format) + + + + def get_format_info(self): + """获取当前光标位置的格式信息""" + cursor = self.textCursor() + char_format = cursor.charFormat() + + return { + "font_family": char_format.fontFamily(), + "font_size": char_format.fontPointSize(), + "bold": char_format.fontWeight() == QFont.Bold, + "italic": char_format.fontItalic(), + "underline": char_format.fontUnderline(), + "strikethrough": char_format.fontStrikeOut(), + "color": char_format.foreground().color().name(), + "background_color": char_format.background().color().name() + } + + def setup_editor(self): + """设置编辑器样式和功能 - 深色主题""" + # 设置字体 - 使用等宽字体,支持多平台 + font = QFont() + font.setStyleHint(QFont.Monospace) + font.setFamily("Consolas, Monaco, 'Courier New', 'Source Code Pro', 'SF Mono', Menlo, monospace") + font.setPointSize(14) + self.setFont(font) + + # 设置样式 - MarkText深色主题风格 + self.setStyleSheet(""" + QPlainTextEdit { + background-color: #0d1117; + color: #c9d1d9; + border: 1px solid #30363d; + border-radius: 6px; + padding: 16px; + selection-background-color: #1f6feb; + selection-color: #ffffff; + font-family: 'SF Mono', Monaco, 'Consolas', 'Courier New', monospace; + font-size: 14px; + line-height: 1.5; + } + QPlainTextEdit:focus { + border: 2px solid #58a6ff; + outline: none; + } + """) + + # 启用自动换行 + self.setLineWrapMode(QPlainTextEdit.WidgetWidth) + + # 设置Tab宽度为4个空格 + self.setTabStopWidth(4 * self.fontMetrics().width(' ')) + + # 启用拖拽 + self.setAcceptDrops(True) + + # 连接文本变化信号 + self.textChanged.connect(self.on_text_changed) + + def on_text_changed(self): + """文本变化处理""" + self.is_modified = True + self.text_changed.emit() + + def load_file(self, file_path: str): + """加载文件""" + try: + with open(file_path, 'r', encoding='utf-8') as f: + content = f.read() + self.setPlainText(content) + self.current_file_path = file_path + self.is_modified = False + return True + except Exception as e: + QMessageBox.critical(self, "错误", f"无法加载文件: {str(e)}") + return False + + def save_file(self, file_path: str = None): + """保存文件""" + if file_path: + self.current_file_path = file_path + + if not self.current_file_path: + return False + + try: + with open(self.current_file_path, 'w', encoding='utf-8') as f: + f.write(self.toPlainText()) + self.is_modified = False + return True + except Exception as e: + QMessageBox.critical(self, "错误", f"无法保存文件: {str(e)}") + return False + + def get_content(self) -> str: + """获取编辑器内容""" + return self.toPlainText() + + def set_content(self, content: str): + """设置编辑器内容""" + self.setPlainText(content) + self.is_modified = False + + def get_word_count(self) -> int: + """获取字数统计""" + content = self.toPlainText() + return len(content.replace("\n", " ").split()) + + def get_char_count(self) -> int: + """获取字符数统计""" + return len(self.toPlainText()) + + def start_learning_mode(self, content: str): + """开始学习模式""" + # 设置编辑器为只读模式,用于打字练习 + self.setReadOnly(True) + # 保存原始内容用于比较 + self.original_content = content + # 清空当前内容,让用户重新输入 + self.setPlainText("") + # 可以在这里添加更多的学习模式逻辑 + self.is_modified = False + + +class MarkTextSideBar(QWidget): + """MarkText风格的现代化侧边栏""" + + file_selected = pyqtSignal(str) + + def __init__(self, parent=None): + super().__init__(parent) + self.parent = parent + self.setup_ui() + + def setup_ui(self): + """设置现代化侧边栏UI""" + layout = QVBoxLayout() + layout.setContentsMargins(8, 8, 8, 8) + layout.setSpacing(12) + self.setLayout(layout) + + # 设置样式 - MarkText深色主题风格,修复顶部UI文字颜色 + self.setStyleSheet(""" + QWidget { + background-color: #1e1e1e; + border-right: 1px solid #2d2d2d; + color: #e8e8ed; + } + + QPushButton { + background-color: #2d2d2d; + border: 1px solid #3d3d3d; + border-radius: 6px; + padding: 8px 12px; + font-size: 13px; + font-weight: 500; + color: #e8e8ed; + text-align: left; + } + + QPushButton:hover { + background-color: #3d3d3d; + border-color: #4d4d4d; + } + + QPushButton:pressed { + background-color: #4d4d4d; + } + + QGroupBox { + font-weight: 600; + color: #e8e8ed; + border: 1px solid #3d3d3d; + border-radius: 6px; + margin-top: 12px; + padding-top: 8px; + } + + QGroupBox::title { + subcontrol-origin: margin; + left: 10px; + padding: 0 5px 0 5px; + color: #e8e8ed; + } + + QLabel { + color: #a8a8ad; + font-size: 12px; + } + + QProgressBar { + background-color: #2d2d2d; + border: 1px solid #3d3d3d; + border-radius: 3px; + text-align: center; + color: #e8e8ed; + } + + QProgressBar::chunk { + background-color: #58a6ff; + border-radius: 2px; + } + """) + + # 创建文件操作组 + file_group = QGroupBox("文件操作") + file_layout = QVBoxLayout() + file_layout.setSpacing(6) + + self.new_file_btn = QPushButton("📄 新建文档") + self.open_file_btn = QPushButton("📁 打开文件") + self.save_file_btn = QPushButton("💾 保存文件") + self.export_btn = QPushButton("📤 导出为...") + + file_layout.addWidget(self.new_file_btn) + file_layout.addWidget(self.open_file_btn) + file_layout.addWidget(self.save_file_btn) + file_layout.addWidget(self.export_btn) + + file_group.setLayout(file_layout) + layout.addWidget(file_group) + + # 创建学习模式组 + learning_group = QGroupBox("学习模式") + learning_layout = QVBoxLayout() + learning_layout.setSpacing(6) + + self.typing_mode_btn = QPushButton("⌨️ 打字模式") + self.learning_mode_btn = QPushButton("📚 学习模式") + self.import_file_btn = QPushButton("📥 导入学习文件") + + # 进度显示 + self.progress_label = QLabel("进度: 0%") + self.progress_bar = QProgressBar() + self.progress_bar.setVisible(False) + + learning_layout.addWidget(self.typing_mode_btn) + learning_layout.addWidget(self.learning_mode_btn) + learning_layout.addWidget(self.import_file_btn) + learning_layout.addWidget(self.progress_label) + learning_layout.addWidget(self.progress_bar) + + learning_group.setLayout(learning_layout) + layout.addWidget(learning_group) + + # 创建工具组 + tools_group = QGroupBox("实用工具") + tools_layout = QVBoxLayout() + tools_layout.setSpacing(6) + + self.weather_btn = QPushButton("🌤 天气信息") + self.select_city_btn = QPushButton("🏙 选择城市") + self.quote_btn = QPushButton("📖 每日名言") + self.insert_weather_btn = QPushButton("🌈 插入天气") + self.insert_quote_btn = QPushButton("✨ 插入名言") + self.calendar_btn = QPushButton("📅 日历功能") + self.insert_date_btn = QPushButton("📆 插入日期") + self.snake_game_btn = QPushButton("🐍 贪吃蛇游戏") + + tools_layout.addWidget(self.weather_btn) + tools_layout.addWidget(self.select_city_btn) + tools_layout.addWidget(self.quote_btn) + tools_layout.addWidget(self.insert_weather_btn) + tools_layout.addWidget(self.insert_quote_btn) + tools_layout.addWidget(self.calendar_btn) + tools_layout.addWidget(self.insert_date_btn) + tools_layout.addWidget(self.snake_game_btn) + + tools_group.setLayout(tools_layout) + layout.addWidget(tools_group) + + # 添加弹簧 + layout.addStretch() + + # 连接信号 + self.new_file_btn.clicked.connect(self.parent.new_file) + self.open_file_btn.clicked.connect(self.parent.open_file) + self.save_file_btn.clicked.connect(self.parent.save_file) + self.export_btn.clicked.connect(self.parent.export_file) + + self.typing_mode_btn.clicked.connect(self.parent.switch_to_typing_mode) + self.learning_mode_btn.clicked.connect(self.parent.switch_to_learning_mode) + self.import_file_btn.clicked.connect(self.parent.import_file) + + self.weather_btn.clicked.connect(self.parent.show_weather_info) + self.select_city_btn.clicked.connect(self.parent.select_city_and_refresh_weather) + self.quote_btn.clicked.connect(self.parent.show_quote_info) + self.insert_weather_btn.clicked.connect(self.parent.insert_weather_to_editor) + self.insert_quote_btn.clicked.connect(self.parent.insert_quote_to_editor) + self.calendar_btn.clicked.connect(self.parent.show_calendar_dialog) + self.insert_date_btn.clicked.connect(self.parent.insert_current_date) + self.snake_game_btn.clicked.connect(self.parent.open_snake_game) + + # 设置固定宽度 + self.setFixedWidth(220) + + def update_progress(self, progress: float, show_progress: bool = True): + """更新进度显示""" + self.progress_label.setText(f"进度: {progress:.1f}%") + if show_progress: + self.progress_bar.setVisible(True) + self.progress_bar.setValue(int(progress)) + else: + self.progress_bar.setVisible(False) + + def reset_progress(self): + """重置进度显示""" + self.progress_label.setText("进度: 0%") + self.progress_bar.setVisible(False) + self.progress_bar.setValue(0) + + +class MarkTextMainWindow(QMainWindow): + """MarkText风格的主窗口,集成MagicWord功能""" + + def __init__(self, parent=None): + super().__init__(parent) + self.parent = parent + self.current_mode = "edit" # edit 或 learning + self.network_service = None # 延迟初始化 + self.file_service = FileManager() + self.theme_manager = None # 移除主题管理器 + self.learning_window = None # 学习模式窗口引用 + + self.setup_ui() + self.setup_menu() + self.setup_toolbar() + self.setup_statusbar() + self.setup_connections() + + # 直接使用深色主题 + self.apply_dark_theme() + + # 延迟初始化网络服务 + QTimer.singleShot(2000, self.init_network_service) + + # 更新信息显示 + self.update_info_display() + self.timer = QTimer() + self.timer.timeout.connect(self.update_info_display) + self.timer.start(600000) # 10分钟更新一次 + + def setup_ui(self): + """设置主窗口UI - 修复顶部菜单栏文字颜色""" + self.setWindowTitle("MagicWord - MarkText编辑器") + self.setGeometry(100, 100, 1200, 800) + + # 设置主窗口样式,确保菜单栏文字可见 + self.setStyleSheet(""" + QMainWindow { + background-color: #0d1117; + color: #c9d1d9; + } + + QMenuBar { + background-color: #161b22; + color: #c9d1d9; + border-bottom: 1px solid #30363d; + } + + QMenuBar::item { + background-color: transparent; + color: #c9d1d9; + padding: 4px 8px; + } + + QMenuBar::item:selected { + background-color: #1f6feb; + color: #ffffff; + } + + QMenu { + background-color: #161b22; + color: #c9d1d9; + border: 1px solid #30363d; + } + + QMenu::item:selected { + background-color: #1f6feb; + color: #ffffff; + } + + QToolBar { + background-color: #161b22; + color: #c9d1d9; + border-bottom: 1px solid #30363d; + } + + QStatusBar { + background-color: #161b22; + color: #c9d1d9; + border-top: 1px solid #30363d; + } + """) + + # 创建中央部件 + central_widget = QWidget() + self.setCentralWidget(central_widget) + + # 创建主布局 + main_layout = QHBoxLayout() + main_layout.setContentsMargins(0, 0, 0, 0) + + # 创建分割器 + splitter = QSplitter(Qt.Horizontal) + + # 侧边栏 + self.sidebar = MarkTextSideBar(self) + splitter.addWidget(self.sidebar) + + # 编辑器区域 + editor_container = QWidget() + editor_layout = QVBoxLayout() + editor_layout.setContentsMargins(0, 0, 0, 0) + + # 创建标签页 + self.tab_widget = QTabWidget() + self.tab_widget.setTabsClosable(True) + self.tab_widget.tabCloseRequested.connect(self.close_tab) + + # 创建欢迎页面 + welcome_widget = self.create_welcome_widget() + self.tab_widget.addTab(welcome_widget, "欢迎") + + editor_layout.addWidget(self.tab_widget) + + # 底部信息栏 + self.info_bar = self.create_info_bar() + editor_layout.addWidget(self.info_bar) + + editor_container.setLayout(editor_layout) + splitter.addWidget(editor_container) + + # AI对话面板 + self.ai_chat_panel = AIChatPanel(self) + self.ai_chat_panel.setMinimumWidth(320) + splitter.addWidget(self.ai_chat_panel) + + # 设置分割器比例 + splitter.setSizes([250, 700, 300]) + + main_layout.addWidget(splitter) + central_widget.setLayout(main_layout) + + def create_welcome_widget(self): + """创建欢迎页面 - 修复文字颜色""" + widget = QWidget() + layout = QVBoxLayout() + layout.setAlignment(Qt.AlignCenter) + + # 设置欢迎页面样式 + widget.setStyleSheet(""" + QWidget { + background-color: #0d1117; + color: #c9d1d9; + } + """) + + # 标题 + title = QLabel("MagicWord") + title.setStyleSheet(""" + QLabel { + font-size: 48px; + font-weight: bold; + color: #58a6ff; + margin-bottom: 20px; + } + """) + title.setAlignment(Qt.AlignCenter) + + # 副标题 + subtitle = QLabel("基于MarkText的现代化Markdown编辑器") + subtitle.setStyleSheet(""" + QLabel { + font-size: 18px; + color: #8b949e; + margin-bottom: 40px; + } + """) + subtitle.setAlignment(Qt.AlignCenter) + + # 快速操作按钮 + buttons_layout = QHBoxLayout() + buttons_layout.setAlignment(Qt.AlignCenter) + + new_btn = QPushButton("新建文档") + open_btn = QPushButton("打开文档") + learning_btn = QPushButton("学习模式") + + for btn in [new_btn, open_btn, learning_btn]: + btn.setStyleSheet(""" + QPushButton { + background-color: #238636; + color: white; + border: none; + padding: 12px 24px; + font-size: 14px; + border-radius: 4px; + margin: 0 10px; + min-width: 120px; + } + QPushButton:hover { + background-color: #2ea043; + } + """) + + new_btn.clicked.connect(self.new_file) + open_btn.clicked.connect(self.open_file) + learning_btn.clicked.connect(self.switch_to_learning_mode) + + buttons_layout.addWidget(new_btn) + buttons_layout.addWidget(open_btn) + buttons_layout.addWidget(learning_btn) + + layout.addWidget(title) + layout.addWidget(subtitle) + layout.addLayout(buttons_layout) + + widget.setLayout(layout) + return widget + + def create_info_bar(self): + """创建可滑动的信息栏 - 类似IDE终端""" + # 创建主框架 + frame = QFrame() + frame.setStyleSheet(""" + QFrame { + background-color: #161b22; + border-top: 1px solid #30363d; + } + """) + + # 设置固定高度,类似IDE终端 + frame.setFixedHeight(80) + + # 创建主布局 + main_layout = QHBoxLayout() + main_layout.setContentsMargins(8, 8, 8, 8) + main_layout.setSpacing(15) + + # 天气信息 + self.weather_label = QLabel("天气: 加载中...") + self.weather_label.setStyleSheet("color: #c9d1d9; font-size: 12px; font-family: 'SF Mono', Monaco, 'Consolas', 'Courier New', monospace;") + self.weather_label.setFixedWidth(150) + + # 创建名言的滑动区域 + quote_scroll = QScrollArea() + quote_scroll.setWidgetResizable(True) + quote_scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded) + quote_scroll.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) + quote_scroll.setStyleSheet(""" + QScrollArea { + background-color: #161b22; + border: none; + } + QScrollArea QWidget { + background-color: #161b22; + } + QScrollBar:vertical { + background-color: #161b22; + width: 8px; + } + QScrollBar::handle:vertical { + background-color: #30363d; + border-radius: 4px; + min-height: 20px; + } + QScrollBar::handle:vertical:hover { + background-color: #58a6ff; + } + QScrollBar:horizontal { + background-color: #161b22; + height: 8px; + } + QScrollBar::handle:horizontal { + background-color: #30363d; + border-radius: 4px; + min-width: 20px; + } + QScrollBar::handle:horizontal:hover { + background-color: #58a6ff; + } + """) + + # 创建名言容器 + quote_container = QWidget() + quote_layout = QVBoxLayout() + quote_layout.setContentsMargins(0, 0, 0, 0) + quote_layout.setSpacing(0) + + # 名言信息 - 使用多行文本显示 + self.quote_label = QLabel("名言: 加载中...") + self.quote_label.setStyleSheet("color: #c9d1d9; font-size: 12px; font-family: 'SF Mono', Monaco, 'Consolas', 'Courier New', monospace; font-style: italic; padding: 2px;") + self.quote_label.setWordWrap(True) + self.quote_label.setAlignment(Qt.AlignLeft | Qt.AlignTop) + self.quote_label.setMinimumHeight(40) # 确保至少显示两行 + self.quote_label.setMaximumHeight(60) # 限制最大高度 + + quote_layout.addWidget(self.quote_label) + quote_layout.addStretch() + + quote_container.setLayout(quote_layout) + quote_scroll.setWidget(quote_container) + + # 字数统计 + self.word_count_label = QLabel("字数: 0") + self.word_count_label.setStyleSheet("color: #c9d1d9; font-size: 12px; font-family: 'SF Mono', Monaco, 'Consolas', 'Courier New', monospace;") + self.word_count_label.setFixedWidth(100) + + # 添加组件到主布局 + main_layout.addWidget(self.weather_label) + main_layout.addWidget(quote_scroll, 1) # 设置拉伸因子,让滑动区域占据主要空间 + main_layout.addWidget(self.word_count_label) + + frame.setLayout(main_layout) + return frame + + def setup_menu(self): + """设置菜单栏 - 修复菜单文字颜色""" + menubar = self.menuBar() + menubar.setStyleSheet(""" + QMenuBar { + background-color: #161b22; + color: #c9d1d9; + border-bottom: 1px solid #30363d; + } + QMenuBar::item { + background-color: transparent; + color: #c9d1d9; + padding: 4px 8px; + } + QMenuBar::item:selected { + background-color: #1f6feb; + color: #ffffff; + } + """) + + # 文件菜单 + file_menu = menubar.addMenu('文件(&F)') + file_menu.setStyleSheet(""" + QMenu { + background-color: #161b22; + color: #c9d1d9; + border: 1px solid #30363d; + } + QMenu::item:selected { + background-color: #1f6feb; + color: #ffffff; + } + """) + + new_action = QAction('新建(&N)', self) + new_action.setShortcut('Ctrl+N') + new_action.triggered.connect(self.new_file) + file_menu.addAction(new_action) + + open_action = QAction('打开(&O)', self) + open_action.setShortcut('Ctrl+O') + open_action.triggered.connect(self.open_file) + file_menu.addAction(open_action) + + save_action = QAction('保存(&S)', self) + save_action.setShortcut('Ctrl+S') + save_action.triggered.connect(self.save_file) + file_menu.addAction(save_action) + + save_as_action = QAction('另存为(&A)...', self) + save_as_action.setShortcut('Ctrl+Shift+S') + save_as_action.triggered.connect(self.save_as_file) + file_menu.addAction(save_as_action) + + file_menu.addSeparator() + + export_action = QAction('导出(&E)...', self) + export_action.triggered.connect(self.export_file) + file_menu.addAction(export_action) + + file_menu.addSeparator() + + exit_action = QAction('退出(&X)', self) + exit_action.setShortcut('Ctrl+Q') + exit_action.triggered.connect(self.close) + file_menu.addAction(exit_action) + + # 编辑菜单 + edit_menu = menubar.addMenu('编辑(&E)') + edit_menu.setStyleSheet(""" + QMenu { + background-color: #161b22; + color: #c9d1d9; + border: 1px solid #30363d; + } + QMenu::item:selected { + background-color: #1f6feb; + color: #ffffff; + } + """) + + undo_action = QAction('撤销(&U)', self) + undo_action.setShortcut('Ctrl+Z') + undo_action.triggered.connect(self.editor_undo) + edit_menu.addAction(undo_action) + + redo_action = QAction('重做(&R)', self) + redo_action.setShortcut('Ctrl+Y') + redo_action.triggered.connect(self.editor_redo) + edit_menu.addAction(redo_action) + + edit_menu.addSeparator() + + cut_action = QAction('剪切(&T)', self) + cut_action.setShortcut('Ctrl+X') + cut_action.triggered.connect(self.editor_cut) + edit_menu.addAction(cut_action) + + copy_action = QAction('复制(&C)', self) + copy_action.setShortcut('Ctrl+C') + copy_action.triggered.connect(self.editor_copy) + edit_menu.addAction(copy_action) + + paste_action = QAction('粘贴(&P)', self) + paste_action.setShortcut('Ctrl+V') + paste_action.triggered.connect(self.editor_paste) + edit_menu.addAction(paste_action) + + edit_menu.addSeparator() + + select_all_action = QAction('全选(&A)', self) + select_all_action.setShortcut('Ctrl+A') + select_all_action.triggered.connect(self.editor_select_all) + edit_menu.addAction(select_all_action) + + edit_menu.addSeparator() + + # 格式子菜单 + format_menu = edit_menu.addMenu('格式(&F)') + + italic_action = QAction('斜体(&I)', self) + italic_action.setShortcut('Ctrl+I') + italic_action.triggered.connect(self.on_italic_clicked) + format_menu.addAction(italic_action) + + underline_action = QAction('下划线(&U)', self) + underline_action.setShortcut('Ctrl+U') + underline_action.triggered.connect(self.on_underline_clicked) + format_menu.addAction(underline_action) + + format_menu.addSeparator() + + color_action = QAction('字体颜色(&C)...', self) + color_action.triggered.connect(self.on_color_clicked) + format_menu.addAction(color_action) + + bg_color_action = QAction('背景颜色(&G)...', self) + bg_color_action.triggered.connect(self.on_background_color_clicked) + format_menu.addAction(bg_color_action) + + + + # 工具菜单 + tools_menu = menubar.addMenu('工具(&T)') + tools_menu.setStyleSheet(""" + QMenu { + background-color: #161b22; + color: #c9d1d9; + border: 1px solid #30363d; + } + QMenu::item:selected { + background-color: #1f6feb; + color: #ffffff; + } + """) + + weather_action = QAction('天气信息(&W)', self) + weather_action.triggered.connect(self.show_weather_info) + tools_menu.addAction(weather_action) + + quote_action = QAction('每日名言(&Q)', self) + quote_action.triggered.connect(self.show_quote_info) + tools_menu.addAction(quote_action) + + tools_menu.addSeparator() + + # 添加刷新功能 + refresh_weather_action = QAction('刷新天气(&R)', self) + refresh_weather_action.setShortcut('F5') + refresh_weather_action.triggered.connect(self.refresh_weather_info) + tools_menu.addAction(refresh_weather_action) + + refresh_quote_action = QAction('刷新名言(&F)', self) + refresh_quote_action.setShortcut('F6') + refresh_quote_action.triggered.connect(self.refresh_quote_info) + tools_menu.addAction(refresh_quote_action) + + tools_menu.addSeparator() + + insert_weather_action = QAction('插入天气(&I)', self) + insert_weather_action.triggered.connect(self.insert_weather_to_editor) + tools_menu.addAction(insert_weather_action) + + insert_quote_action = QAction('插入名言(&N)', self) + insert_quote_action.triggered.connect(self.insert_quote_to_editor) + tools_menu.addAction(insert_quote_action) + + tools_menu.addSeparator() + # 主题切换功能已移动到主题菜单中 + + # 学习模式菜单 + learning_menu = menubar.addMenu('学习(&L)') + learning_menu.setStyleSheet(""" + QMenu { + background-color: #161b22; + color: #c9d1d9; + border: 1px solid #30363d; + } + QMenu::item:selected { + background-color: #1f6feb; + color: #ffffff; + } + """) + + typing_mode_action = QAction('打字模式(&T)', self) + typing_mode_action.triggered.connect(self.switch_to_typing_mode) + learning_menu.addAction(typing_mode_action) + + learning_mode_action = QAction('学习模式(&L)', self) + learning_mode_action.triggered.connect(self.switch_to_learning_mode) + learning_menu.addAction(learning_mode_action) + + import_file_action = QAction('导入学习文件(&I)', self) + import_file_action.triggered.connect(self.import_file) + learning_menu.addAction(import_file_action) + + # 帮助菜单 + help_menu = menubar.addMenu('帮助(&H)') + help_menu.setStyleSheet(""" + QMenu { + background-color: #161b22; + color: #c9d1d9; + border: 1px solid #30363d; + } + QMenu::item:selected { + background-color: #1f6feb; + color: #ffffff; + } + """) + + about_action = QAction('关于(&A)', self) + about_action.triggered.connect(self.show_about_dialog) + help_menu.addAction(about_action) + + def setup_toolbar(self): + """设置工具栏""" + toolbar = QToolBar() + toolbar.setStyleSheet(""" + QToolBar { + background-color: #161b22; + border: none; + padding: 5px; + spacing: 5px; + } + QToolBar QToolButton { + background-color: transparent; + border: none; + padding: 8px; + border-radius: 3px; + color: #c9d1d9; + } + QToolBar QToolButton:hover { + background-color: #30363d; + } + QToolBar QToolButton:pressed { + background-color: #1f6feb; + } + QToolBar QComboBox { + background-color: #30363d; + color: #c9d1d9; + border: 1px solid #484f58; + border-radius: 3px; + padding: 5px; + min-width: 80px; + } + QToolBar QComboBox::drop-down { + border: none; + } + QToolBar QComboBox::down-arrow { + image: none; + border-left: 5px solid transparent; + border-right: 5px solid transparent; + border-top: 5px solid #c9d1d9; + } + """) + + # 文件操作按钮 + toolbar.addAction("新建", self.new_file) + toolbar.addAction("打开", self.open_file) + toolbar.addAction("保存", self.save_file) + toolbar.addSeparator() + + # 编辑操作按钮 + toolbar.addAction("撤销", lambda: self.get_current_editor().undo() if self.get_current_editor() else None) + toolbar.addAction("重做", lambda: self.get_current_editor().redo() if self.get_current_editor() else None) + toolbar.addSeparator() + + # 剪切复制粘贴 + toolbar.addAction("剪切", lambda: self.get_current_editor().cut() if self.get_current_editor() else None) + toolbar.addAction("复制", lambda: self.get_current_editor().copy() if self.get_current_editor() else None) + toolbar.addAction("粘贴", lambda: self.get_current_editor().paste() if self.get_current_editor() else None) + toolbar.addSeparator() + + # 字体选择 + self.font_combo = QFontComboBox() + self.font_combo.setCurrentFont(QFont("Consolas")) + self.font_combo.currentFontChanged.connect(self.on_font_changed) + toolbar.addWidget(self.font_combo) + + # 字体大小选择 + self.font_size_combo = QComboBox() + self.font_size_combo.addItems(["8", "9", "10", "11", "12", "14", "16", "18", "20", "22", "24", "26", "28", "36", "48", "72"]) + self.font_size_combo.setCurrentText("14") + self.font_size_combo.currentTextChanged.connect(self.on_font_size_changed) + toolbar.addWidget(self.font_size_combo) + + toolbar.addSeparator() + + # 格式按钮 + italic_btn = QAction("斜体", self) + italic_btn.setCheckable(True) + italic_btn.triggered.connect(self.on_italic_clicked) + toolbar.addAction(italic_btn) + + underline_btn = QAction("下划线", self) + underline_btn.setCheckable(True) + underline_btn.triggered.connect(self.on_underline_clicked) + toolbar.addAction(underline_btn) + + color_btn = QAction("颜色", self) + color_btn.triggered.connect(self.on_color_clicked) + toolbar.addAction(color_btn) + + + + toolbar.addSeparator() + + # 学习模式相关 + toolbar.addAction("学习模式", self.switch_to_learning_mode) + toolbar.addAction("导入文件", self.import_file) + toolbar.addSeparator() + toolbar.addAction("刷新天气", self.refresh_weather_info) + toolbar.addAction("刷新名言", self.refresh_quote_info) + + self.addToolBar(toolbar) + + def setup_statusbar(self): + """设置状态栏""" + statusbar = QStatusBar() + statusbar.setStyleSheet(""" + QStatusBar { + background-color: #161b22; + color: #c9d1d9; + font-size: 12px; + border-top: 1px solid #30363d; + } + """) + self.setStatusBar(statusbar) + + def setup_connections(self): + """设置信号连接""" + pass + + def editor_undo(self): + """编辑器撤销操作""" + editor = self.get_current_editor() + if editor: + editor.undo() + + def editor_redo(self): + """编辑器重做操作""" + editor = self.get_current_editor() + if editor: + editor.redo() + + def editor_cut(self): + """编辑器剪切操作""" + editor = self.get_current_editor() + if editor: + editor.cut() + + def editor_copy(self): + """编辑器复制操作""" + editor = self.get_current_editor() + if editor: + editor.copy() + + def editor_paste(self): + """编辑器粘贴操作""" + editor = self.get_current_editor() + if editor: + editor.paste() + + def editor_select_all(self): + """编辑器全选操作""" + editor = self.get_current_editor() + if editor: + editor.selectAll() + + def show_about_dialog(self): + """显示关于对话框""" + QMessageBox.about(self, "关于 MagicWord", + "MagicWord - MarkText风格Markdown编辑器\n\n" + "基于PyQt5开发,集成MarkText现代化界面设计\n" + "支持Markdown编辑、打字学习模式、文档管理等功能。\n\n" + "版本: 1.0.0\n" + "作者: MagicWord Team") + + def apply_dark_theme(self): + """应用MarkText深色主题""" + self.set_theme("dark") + + def init_network_service(self): + """延迟初始化网络服务""" + try: + self.network_service = NetworkService() + # 立即更新一次信息 + self.update_info_display() + except Exception as e: + print(f"网络服务初始化失败: {e}") + # 设置一个空的服务对象以避免后续错误 + self.network_service = type('obj', (object,), { + 'get_weather_info': lambda: None, + 'get_daily_quote': lambda: None + })() + + def set_theme(self, theme: str): + """设置主题""" + if theme == "dark": + self.setStyleSheet(""" + QMainWindow { + background-color: #2d2d2d; + color: #ffffff; + } + QMenuBar { + background-color: #3d3d3d; + color: #ffffff; + } + QMenuBar::item:selected { + background-color: #4d4d4d; + } + QMenu { + background-color: #3d3d3d; + color: #ffffff; + border: 1px solid #555; + } + QMenu::item:selected { + background-color: #4d4d4d; + } + """) + else: + self.setStyleSheet("") + + def get_current_editor(self) -> Optional[MarkTextEditor]: + """获取当前编辑器""" + current_widget = self.tab_widget.currentWidget() + if isinstance(current_widget, MarkTextEditor): + return current_widget + return None + + def new_file(self): + """新建文件""" + editor = MarkTextEditor(self) + editor.text_changed.connect(self.update_word_count) + + tab_index = self.tab_widget.addTab(editor, "未命名") + self.tab_widget.setCurrentIndex(tab_index) + + def open_file(self): + """打开文件""" + file_path, _ = QFileDialog.getOpenFileName( + self, "打开文件", "", + "Markdown文件 (*.md *.markdown);;文本文件 (*.txt);;所有文件 (*.*)" + ) + + if file_path: + # 检查是否已经打开 + for i in range(self.tab_widget.count()): + widget = self.tab_widget.widget(i) + if isinstance(widget, MarkTextEditor) and widget.current_file_path == file_path: + self.tab_widget.setCurrentIndex(i) + return + + # 创建新编辑器 + editor = MarkTextEditor(self) + if editor.load_file(file_path): + editor.text_changed.connect(self.update_word_count) + + file_name = os.path.basename(file_path) + tab_index = self.tab_widget.addTab(editor, file_name) + self.tab_widget.setCurrentIndex(tab_index) + + def save_file(self): + """保存文件""" + editor = self.get_current_editor() + if not editor: + return + + if editor.current_file_path: + editor.save_file() + self.statusBar().showMessage("文件已保存", 2000) + else: + self.save_as_file() + + def save_as_file(self): + """另存为""" + editor = self.get_current_editor() + if not editor: + return + + file_path, _ = QFileDialog.getSaveFileName( + self, "保存文件", "", + "Markdown文件 (*.md);;文本文件 (*.txt);;所有文件 (*.*)" + ) + + if file_path: + if editor.save_file(file_path): + file_name = os.path.basename(file_path) + self.tab_widget.setTabText(self.tab_widget.currentIndex(), file_name) + self.statusBar().showMessage("文件已保存", 2000) + + def close_tab(self, index: int): + """关闭标签页""" + widget = self.tab_widget.widget(index) + if isinstance(widget, MarkTextEditor) and widget.is_modified: + reply = QMessageBox.question( + self, "确认", "文件已修改,是否保存?", + QMessageBox.Save | QMessageBox.Discard | QMessageBox.Cancel + ) + + if reply == QMessageBox.Save: + self.save_file() + elif reply == QMessageBox.Cancel: + return + + self.tab_widget.removeTab(index) + + def import_file(self): + """导入文件(学习模式)""" + file_path, _ = QFileDialog.getOpenFileName( + self, "导入文件", "", + "所有支持的格式 (*.txt *.md *.docx *.pdf);;文本文件 (*.txt);;Markdown文件 (*.md);;Word文档 (*.docx);;PDF文件 (*.pdf)" + ) + + if file_path: + # 切换到学习模式 + self.switch_to_learning_mode(file_path) + + def export_file(self): + """导出文件 - 支持多种格式""" + try: + editor = self.get_current_editor() + if not editor: + QMessageBox.warning(self, "提示", "请先打开一个文档") + return + + content = editor.get_content() + if not content.strip(): + QMessageBox.warning(self, "提示", "文档内容为空,无法导出") + return + + # 创建导出格式选择对话框 + from PyQt5.QtWidgets import QDialog, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QDialogButtonBox + + dialog = QDialog(self) + dialog.setWindowTitle("选择导出格式") + dialog.setModal(True) + dialog.resize(400, 300) + + # 设置对话框样式 + dialog.setStyleSheet(""" + QDialog { + background-color: #161b22; + color: #c9d1d9; + } + QLabel { + color: #c9d1d9; + font-size: 14px; + padding: 10px; + } + QPushButton { + background-color: #238636; + color: white; + border: none; + padding: 8px 16px; + border-radius: 4px; + font-size: 13px; + margin: 5px; + min-width: 120px; + } + QPushButton:hover { + background-color: #2ea043; + } + QPushButton:pressed { + background-color: #1a5d29; + } + """) + + layout = QVBoxLayout() + + # 标题 + title_label = QLabel("选择要导出的文件格式:") + title_label.setStyleSheet("font-size: 16px; font-weight: bold; padding: 15px;") + layout.addWidget(title_label) + + # 格式按钮布局 + format_layout = QVBoxLayout() + + # HTML格式 + html_btn = QPushButton("🌐 导出为 HTML") + html_btn.clicked.connect(lambda: self.export_as_format(editor, "html", dialog)) + format_layout.addWidget(html_btn) + + # PDF格式 + pdf_btn = QPushButton("📄 导出为 PDF") + pdf_btn.clicked.connect(lambda: self.export_as_format(editor, "pdf", dialog)) + format_layout.addWidget(pdf_btn) + + # TXT格式 + txt_btn = QPushButton("📝 导出为 TXT") + txt_btn.clicked.connect(lambda: self.export_as_format(editor, "txt", dialog)) + format_layout.addWidget(txt_btn) + + # Markdown格式 + md_btn = QPushButton("📚 导出为 Markdown") + md_btn.clicked.connect(lambda: self.export_as_format(editor, "md", dialog)) + format_layout.addWidget(md_btn) + + layout.addLayout(format_layout) + + # 取消按钮 + cancel_btn = QPushButton("❌ 取消") + cancel_btn.setStyleSheet(""" + QPushButton { + background-color: #21262d; + color: #c9d1d9; + } + QPushButton:hover { + background-color: #30363d; + } + """) + cancel_btn.clicked.connect(dialog.reject) + layout.addWidget(cancel_btn) + + layout.addStretch() + dialog.setLayout(layout) + dialog.exec_() + + except Exception as e: + QMessageBox.critical(self, "错误", f"导出功能出错: {str(e)}") + + def switch_mode(self, mode: str): + """切换模式""" + self.current_mode = mode + if mode == "learning": + self.switch_to_learning_mode() + else: + # 切换回编辑模式 + pass + + def switch_to_learning_mode(self, import_file_path: str = None): + """切换到学习模式 - 新建文档并同步内容""" + try: + # 首先新建一个文档 + self.new_file() + + # 获取当前编辑器 + editor = self.get_current_editor() + if not editor: + QMessageBox.warning(self, "提示", "无法获取编辑器") + return + + # 创建学习模式窗口 + self.learning_window = LearningModeWindow(parent=self) + + # 如果有导入文件路径,设置到学习窗口 + if import_file_path: + self.learning_window.import_file_path = import_file_path + + # 连接内容同步信号 + 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() + self.learning_window.raise_() + self.learning_window.activateWindow() + + # 更新状态栏 + self.statusBar().showMessage("学习模式已打开,内容将同步到当前文档", 3000) + + except Exception as e: + QMessageBox.critical(self, "错误", f"无法打开学习模式: {str(e)}") + + def on_learning_content_changed(self, new_content: str, position: int): + """学习模式内容变化时的回调 - 同步到当前文档""" + try: + editor = self.get_current_editor() + if editor and new_content: + # 在编辑器末尾追加新内容 + cursor = editor.textCursor() + cursor.movePosition(QTextCursor.End) + cursor.insertText(new_content) + + # 更新状态栏 + self.statusBar().showMessage(f"从学习模式同步内容: {len(new_content)} 字符", 2000) + except Exception as e: + print(f"同步学习模式内容失败: {e}") + + def on_learning_mode_closed(self): + """学习模式窗口关闭时的回调""" + if self.learning_window: + self.learning_window = None + self.statusBar().showMessage("学习模式已关闭", 2000) + + def switch_to_typing_mode(self): + """切换到打字模式(在当前编辑器中)""" + editor = self.get_current_editor() + if editor: + content = editor.get_content() + if content.strip(): + editor.start_learning_mode(content) + self.sidebar.update_progress(0, True) + QMessageBox.information(self, "打字模式", "已切换到打字学习模式!") + else: + QMessageBox.warning(self, "提示", "请先输入或打开一些内容用于打字练习") + else: + QMessageBox.warning(self, "提示", "请先打开一个文档") + + def show_weather_info(self): + """显示天气信息 - 包含生活提示的详细信息""" + if not self.network_service: + QMessageBox.warning(self, "提示", "网络服务正在初始化中,请稍后再试") + return + + try: + weather_info = self.network_service.get_weather_info() + if weather_info: + # 基础天气信息 + weather_text = f"🌤 {weather_info['city']} 天气详情\n\n" + weather_text += f"温度: {weather_info['temperature']}°C\n" + weather_text += f"天气: {weather_info['description']}\n" + weather_text += f"湿度: {weather_info['humidity']}%\n" + weather_text += f"风速: {weather_info['wind_speed']}m/s\n" + + # 添加生活提示 + lifetips = weather_info.get('lifetips', []) + if lifetips: + weather_text += "\n🌟 生活提示:\n" + for tip in lifetips: + weather_text += f"• {tip}\n" + + # 添加未来天气预报(如果有) + forecast = weather_info.get('forecast', []) + if forecast and len(forecast) > 0: + weather_text += f"\n📅 未来预报:\n" + for day in forecast[:3]: # 显示未来3天 + weather_text += f"{day['date']}: {day['high']}/{day['low']}, {day['type']}\n" + + QMessageBox.information(self, "天气详细信息", weather_text) + else: + QMessageBox.warning(self, "提示", "无法获取天气信息") + except Exception as e: + QMessageBox.critical(self, "错误", f"获取天气信息失败: {str(e)}") + + def show_quote_info(self): + """显示名言信息 - 优化错误处理""" + if not self.network_service: + QMessageBox.warning(self, "提示", "网络服务正在初始化中,请稍后再试") + return + + try: + quote_text = self.network_service.get_daily_quote() + if quote_text: + QMessageBox.information(self, "每日名言", quote_text) + else: + QMessageBox.warning(self, "提示", "无法获取名言信息") + except Exception as e: + QMessageBox.critical(self, "错误", f"获取名言信息失败: {str(e)}") + + def insert_weather_to_editor(self): + """将天气信息插入到编辑器 - 包含生活提示的详细信息""" + if not self.network_service: + QMessageBox.warning(self, "提示", "网络服务正在初始化中,请稍后再试") + return + + editor = self.get_current_editor() + if editor: + try: + weather_info = self.network_service.get_weather_info() + if weather_info: + # 基础天气信息 + weather_text = f"\n【天气信息】\n{weather_info['city']}: {weather_info['temperature']}°C, {weather_info['description']}" + weather_text += f"\n湿度: {weather_info['humidity']}%" + weather_text += f"\n风速: {weather_info['wind_speed']}m/s" + + # 添加生活提示 + lifetips = weather_info.get('lifetips', []) + if lifetips: + weather_text += "\n\n🌟 生活提示:" + for tip in lifetips: + weather_text += f"\n• {tip}" + + weather_text += "\n" + + cursor = editor.textCursor() + cursor.insertText(weather_text) + self.statusBar().showMessage("天气信息已插入到文档", 2000) + else: + QMessageBox.warning(self, "提示", "无法获取天气信息") + except Exception as e: + QMessageBox.critical(self, "错误", f"插入天气信息失败: {str(e)}") + else: + QMessageBox.warning(self, "提示", "请先打开一个文档") + + # 文本格式化功能函数 + def on_font_changed(self, font): + """字体选择变化""" + editor = self.get_current_editor() + if editor: + editor.apply_format("font_family", font.family()) + + def on_font_size_changed(self, size_text): + """字体大小选择变化""" + editor = self.get_current_editor() + if editor: + try: + size = int(size_text) + editor.apply_format("font_size", size) + except ValueError: + pass + + def on_italic_clicked(self): + """斜体按钮点击""" + editor = self.get_current_editor() + if editor: + current_format = editor.get_format_info() + editor.apply_format("italic", not current_format["italic"]) + + def on_underline_clicked(self): + """下划线按钮点击""" + editor = self.get_current_editor() + if editor: + current_format = editor.get_format_info() + editor.apply_format("underline", not current_format["underline"]) + + def on_color_clicked(self): + """颜色按钮点击""" + editor = self.get_current_editor() + if editor: + color = QColorDialog.getColor() + if color.isValid(): + editor.apply_format("color", color.name()) + + def on_background_color_clicked(self): + """背景颜色按钮点击""" + editor = self.get_current_editor() + if editor: + color = QColorDialog.getColor() + if color.isValid(): + editor.apply_format("background_color", color.name()) + + def open_snake_game(self): + """打开贪吃蛇游戏""" + try: + self.snake_game_window = SnakeGameWindow(self) + self.snake_game_window.show() + except Exception as e: + QMessageBox.critical(self, "错误", f"无法打开贪吃蛇游戏: {str(e)}") + + def select_city_and_refresh_weather(self): + """选择城市并刷新天气信息""" + if not self.network_service: + QMessageBox.warning(self, "提示", "网络服务正在初始化中,请稍后再试") + return + + # 城市列表(沿用现有的城市映射) + cities = [ + "北京", "上海", "广州", "深圳", "杭州", "南京", "成都", "武汉", + "西安", "重庆", "天津", "苏州", "青岛", "大连", "沈阳", "哈尔滨", + "长春", "石家庄", "太原", "郑州", "济南", "合肥", "南昌", "长沙", + "福州", "厦门", "南宁", "海口", "贵阳", "昆明", "拉萨", "兰州", + "西宁", "银川", "乌鲁木齐", "呼和浩特" ] - icon_path = None - for icon_file in icon_files: - test_path = os.path.join(project_root, 'resources', 'icons', icon_file) - if os.path.exists(test_path): - icon_path = test_path - break + # 创建选择对话框 + dialog = QDialog(self) + dialog.setWindowTitle("选择城市") + dialog.setFixedSize(300, 400) + + layout = QVBoxLayout() + + # 标题 + title_label = QLabel("选择城市查看天气") + title_label.setStyleSheet("font-size: 16px; font-weight: bold; margin-bottom: 10px;") + layout.addWidget(title_label) + + # 城市列表 + city_list = QListWidget() + city_list.addItems(cities) + city_list.setCurrentRow(0) # 默认选中第一个 + layout.addWidget(city_list) + + # 按钮组 + button_layout = QHBoxLayout() + + auto_locate_btn = QPushButton("自动定位") + select_btn = QPushButton("选择") + cancel_btn = QPushButton("取消") + + button_layout.addWidget(auto_locate_btn) + button_layout.addWidget(select_btn) + button_layout.addWidget(cancel_btn) + + layout.addLayout(button_layout) + + dialog.setLayout(layout) + + # 连接信号 + def on_auto_locate(): + dialog.accept() + self.refresh_weather_info() # 使用自动定位 + + def on_select(): + current_item = city_list.currentItem() + if current_item: + selected_city = current_item.text() + dialog.accept() + self.refresh_weather_info_by_city(selected_city) + + def on_cancel(): + dialog.reject() - if icon_path and os.path.exists(icon_path): - app.setWindowIcon(QIcon(icon_path)) + auto_locate_btn.clicked.connect(on_auto_locate) + select_btn.clicked.connect(on_select) + cancel_btn.clicked.connect(on_cancel) + + # 显示对话框 + dialog.exec_() + + + + def insert_quote_to_editor(self): + """将名言插入到编辑器 - 优化错误处理""" + if not self.network_service: + QMessageBox.warning(self, "提示", "网络服务正在初始化中,请稍后再试") + return + + editor = self.get_current_editor() + if editor: + try: + quote_text = self.network_service.get_daily_quote() + if quote_text: + quote_text = f"\n\n📖 {quote_text}\n\n" + cursor = editor.textCursor() + cursor.insertText(quote_text) + else: + QMessageBox.warning(self, "提示", "无法获取名言信息") + except Exception as e: + QMessageBox.critical(self, "错误", f"插入名言失败: {str(e)}") else: - # 使用默认图标 - app.setWindowIcon(QIcon()) + QMessageBox.warning(self, "提示", "请先打开一个文档") + + def show_calendar_dialog(self): + """显示日历对话框""" + dialog = QDialog(self) + dialog.setWindowTitle("日历功能") + dialog.setFixedSize(400, 500) + + layout = QVBoxLayout() + + # 标题 + title_label = QLabel("📅 日历功能") + title_label.setStyleSheet("font-size: 18px; font-weight: bold; margin-bottom: 10px;") + layout.addWidget(title_label) + + # 当前日期显示 + from datetime import datetime + current_date = datetime.now() + date_label = QLabel(f"当前日期: {current_date.strftime('%Y年%m月%d日 %A')}") + date_label.setStyleSheet("font-size: 14px; margin-bottom: 20px;") + layout.addWidget(date_label) + + # 日期选择器 + calendar = QCalendarWidget() + calendar.setGridVisible(True) + calendar.setStyleSheet(""" + QCalendarWidget { + background-color: #1e1e1e; + color: #e8e8ed; + } + QCalendarWidget QWidget { + alternate-background-color: #2d2d2d; + } + QCalendarWidget QToolButton { + background-color: #3d3d3d; + color: #e8e8ed; + border: 1px solid #4d4d4d; + border-radius: 4px; + padding: 4px; + } + QCalendarWidget QMenu { + background-color: #2d2d2d; + color: #e8e8ed; + } + """) + layout.addWidget(calendar) + + # 选中日期显示 + selected_date_label = QLabel("选中日期: 请选择日期") + selected_date_label.setStyleSheet("font-size: 14px; margin-top: 10px;") + layout.addWidget(selected_date_label) + + def update_selected_date(): + selected_date = calendar.selectedDate() + date_str = selected_date.toString("yyyy年MM月dd日 dddd") + selected_date_label.setText(f"选中日期: {date_str}") + + calendar.selectionChanged.connect(update_selected_date) + + # 按钮组 + button_layout = QHBoxLayout() + + insert_btn = QPushButton("📝 插入选中日期") + insert_current_btn = QPushButton("📆 插入当前日期") + close_btn = QPushButton("❌ 关闭") + + button_layout.addWidget(insert_btn) + button_layout.addWidget(insert_current_btn) + button_layout.addWidget(close_btn) + + layout.addLayout(button_layout) - # 创建Word风格的主窗口 - main_window = WordStyleMainWindow() - main_window.show() + def on_insert_selected(): + selected_date = calendar.selectedDate() + date_str = selected_date.toString("yyyy年MM月dd日 dddd") + self.insert_date_to_editor(date_str) + dialog.accept() - # 启动事件循环并返回退出码 - exit_code = app.exec_() - sys.exit(exit_code) + def on_insert_current(): + current_date_str = current_date.strftime("%Y年%m月%d日 %A") + self.insert_date_to_editor(current_date_str) + dialog.accept() - except Exception as e: - # 打印详细的错误信息 - print(f"应用程序发生未捕获的异常: {e}") - traceback.print_exc() - sys.exit(1) + def on_close(): + dialog.reject() + + insert_btn.clicked.connect(on_insert_selected) + insert_current_btn.clicked.connect(on_insert_current) + close_btn.clicked.connect(on_close) + + dialog.setLayout(layout) + dialog.exec_() + + def insert_current_date(self): + """插入当前日期到编辑器""" + from datetime import datetime + current_date = datetime.now() + date_str = current_date.strftime("%Y年%m月%d日 %A") + self.insert_date_to_editor(date_str) + + def insert_date_to_editor(self, date_str: str): + """将日期字符串插入到编辑器""" + editor = self.get_current_editor() + if editor: + try: + date_text = f"\n\n📅 {date_str}\n\n" + cursor = editor.textCursor() + cursor.insertText(date_text) + self.statusBar().showMessage("日期已插入到文档", 2000) + except Exception as e: + QMessageBox.critical(self, "错误", f"插入日期失败: {str(e)}") + else: + QMessageBox.warning(self, "提示", "请先打开一个文档") + + def refresh_weather_info(self): + """刷新天气信息 - 强制刷新缓存""" + if not self.network_service: + QMessageBox.warning(self, "提示", "网络服务正在初始化中,请稍后再试") + return + + try: + # 清除缓存以强制刷新 + if hasattr(self.network_service, 'clear_weather_cache'): + self.network_service.clear_weather_cache() + + # 重新获取天气信息 + weather_info = self.network_service.get_weather_info(use_cache=False) + if weather_info: + self.statusBar().showMessage("天气信息已刷新", 2000) + # 更新显示 + self.update_info_display() + else: + QMessageBox.warning(self, "提示", "无法刷新天气信息") + except Exception as e: + QMessageBox.critical(self, "错误", f"刷新天气信息失败: {str(e)}") + + def refresh_weather_info_by_city(self, city_name): + """根据城市名刷新天气信息""" + if not self.network_service: + QMessageBox.warning(self, "提示", "网络服务正在初始化中,请稍后再试") + return + + try: + # 清除缓存并获取新城市的天气 + self.network_service.clear_weather_cache() + weather_data = self.network_service.get_weather_info_by_city(city_name) + + if weather_data and weather_data.get('city'): + # 更新底部信息栏显示 + self.update_info_display() + self.statusBar().showMessage(f"已切换到 {city_name} 的天气信息", 3000) + else: + QMessageBox.warning(self, "提示", f"无法获取 {city_name} 的天气信息,请检查城市名称或网络连接") + + except Exception as e: + QMessageBox.critical(self, "错误", f"获取天气信息时发生错误: {str(e)}") + + def refresh_weather_info(self): + """刷新当前天气信息(使用自动定位)""" + if not self.network_service: + QMessageBox.warning(self, "提示", "网络服务正在初始化中,请稍后再试") + return + + try: + # 清除缓存并重新获取天气 + self.network_service.clear_weather_cache() + self.show_weather_info() + QMessageBox.information(self, "成功", "已使用自动定位刷新天气信息") + + except Exception as e: + QMessageBox.critical(self, "错误", f"刷新天气信息时发生错误: {str(e)}") + + def export_as_format(self, editor, format_type: str, dialog): + """导出为指定格式""" + try: + content = editor.get_content() + + # 根据格式设置文件扩展名和过滤器 + if format_type == "html": + ext = "html" + filter_str = "HTML文件 (*.html);;所有文件 (*.*)" + default_name = "document.html" + elif format_type == "pdf": + ext = "pdf" + filter_str = "PDF文件 (*.pdf);;所有文件 (*.*)" + default_name = "document.pdf" + elif format_type == "txt": + ext = "txt" + filter_str = "文本文档 (*.txt);;所有文件 (*.*)" + default_name = "document.txt" + elif format_type == "md": + ext = "md" + filter_str = "Markdown文件 (*.md);;所有文件 (*.*)" + default_name = "document.md" + else: + QMessageBox.warning(self, "提示", "不支持的导出格式") + return + + # 获取保存路径 + file_path, _ = QFileDialog.getSaveFileName( + self, f"导出为{format_type.upper()}", default_name, filter_str + ) + + if not file_path: + return + + dialog.accept() # 关闭选择对话框 + + # 根据格式导出 + if format_type == "html": + self.export_as_html(content, file_path) + elif format_type == "pdf": + self.export_as_pdf(content, file_path) + elif format_type == "txt": + self.export_as_txt(content, file_path) + elif format_type == "md": + self.export_as_markdown(content, file_path) + + except Exception as e: + QMessageBox.critical(self, "错误", f"导出失败: {str(e)}") + + def export_as_html(self, content: str, file_path: str): + """导出为HTML格式""" + try: + from datetime import datetime + + # 创建HTML内容 + html_content = f""" + + + + + MagicWord文档 + + + +
+""" + + # 获取文件名作为标题 + base_name = os.path.splitext(os.path.basename(file_path))[0] + html_content += f'

{base_name}

\n\n' + + # 处理内容 + lines = content.split('\n') + for line in lines: + if line.strip(): + # 转义HTML特殊字符 + escaped_line = line.replace("&", "&").replace("<", "<").replace(">", ">") + html_content += f'

{escaped_line}

\n' + else: + html_content += '
\n' + + # 添加文档信息 + html_content += f'
\n' + html_content += f'

文档信息

\n' + html_content += f'

创建时间: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}

\n' + html_content += f'

字符数: {len(content)}

\n' + html_content += f'

行数: {len(lines)}

\n' + html_content += f'
\n' + + html_content += """ +
+ +""" + + # 写入文件 + with open(file_path, 'w', encoding='utf-8') as f: + f.write(html_content) + + self.statusBar().showMessage(f"已导出为HTML: {os.path.basename(file_path)}", 3000) + + except Exception as e: + QMessageBox.critical(self, "错误", f"导出HTML失败: {str(e)}") + + def export_as_pdf(self, content: str, file_path: str): + """导出为PDF格式""" + try: + from PyQt5.QtGui import QTextDocument + from PyQt5.QtPrintSupport import QPrinter + + # 创建HTML内容以便更好地处理格式 + html_content = f""" + + + + + + + """ + + # 获取文件名作为标题 + base_name = os.path.splitext(os.path.basename(file_path))[0] + html_content += f'

{base_name}

' + + # 处理内容 + lines = content.split('\n') + for line in lines: + if line.strip(): + # 转义HTML特殊字符 + escaped_line = line.replace("&", "&").replace("<", "<").replace(">", ">") + html_content += f'

{escaped_line}

' + else: + html_content += '
' + + # 添加文档信息 + from datetime import datetime + html_content += f'
' + html_content += f'

创建时间: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}

' + html_content += f'

字符数: {len(content)}

' + html_content += f'

行数: {len(lines)}

' + html_content += f'
' + + html_content += "" + + # 创建文本文档 + doc = QTextDocument() + doc.setHtml(html_content) + + # 创建PDF打印机 + printer = QPrinter(QPrinter.HighResolution) + printer.setOutputFormat(QPrinter.PdfFormat) + printer.setOutputFileName(file_path) + printer.setPageSize(QPrinter.A4) + printer.setPageMargins(20, 20, 20, 20, QPrinter.Millimeter) + + # 打印文档到PDF + doc.print_(printer) + + self.statusBar().showMessage(f"已导出为PDF: {os.path.basename(file_path)}", 3000) + + except Exception as e: + QMessageBox.critical(self, "错误", f"导出PDF失败: {str(e)}") + + def export_as_txt(self, content: str, file_path: str): + """导出为TXT格式""" + try: + from datetime import datetime + + # 添加文档信息 + txt_content = f"文档标题: {os.path.splitext(os.path.basename(file_path))[0]}\n" + txt_content += f"创建时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n" + txt_content += f"字符数: {len(content)}\n" + txt_content += f"行数: {len(content.split(chr(10)))}\n" + txt_content += "=" * 50 + "\n\n" + + # 添加原始内容 + txt_content += content + + # 写入文件 + with open(file_path, 'w', encoding='utf-8') as f: + f.write(txt_content) + + self.statusBar().showMessage(f"已导出为TXT: {os.path.basename(file_path)}", 3000) + + except Exception as e: + QMessageBox.critical(self, "错误", f"导出TXT失败: {str(e)}") + + def export_as_markdown(self, content: str, file_path: str): + """导出为Markdown格式""" + try: + from datetime import datetime + + # 创建Markdown内容 + base_name = os.path.splitext(os.path.basename(file_path))[0] + + md_content = f"# {base_name}\n\n" + + # 添加文档信息 + md_content += f"**创建时间:** {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} \n" + md_content += f"**字符数:** {len(content)} \n" + md_content += f"**行数:** {len(content.split(chr(10)))} \n\n" + + md_content += "---\n\n" + + # 处理内容,将普通文本转换为Markdown格式 + lines = content.split('\n') + for line in lines: + if line.strip(): + # 简单的Markdown处理 + md_content += f"{line}\n\n" + else: + md_content += "\n" + + # 写入文件 + with open(file_path, 'w', encoding='utf-8') as f: + f.write(md_content) + + self.statusBar().showMessage(f"已导出为Markdown: {os.path.basename(file_path)}", 3000) + + except Exception as e: + QMessageBox.critical(self, "错误", f"导出Markdown失败: {str(e)}") + + def refresh_quote_info(self): + """刷新每日名言 - 获取新的名言""" + if not self.network_service: + QMessageBox.warning(self, "提示", "网络服务正在初始化中,请稍后再试") + return + + try: + # 获取新的名言 + quote_text = self.network_service.get_daily_quote() + if quote_text: + self.statusBar().showMessage("每日名言已刷新", 2000) + # 更新显示 + self.update_info_display() + else: + QMessageBox.warning(self, "提示", "无法刷新名言信息") + except Exception as e: + QMessageBox.critical(self, "错误", f"刷新名言信息失败: {str(e)}") + + def update_word_count(self): + """更新字数统计""" + editor = self.get_current_editor() + if editor: + word_count = editor.get_word_count() + char_count = editor.get_char_count() + self.word_count_label.setText(f"字数: {word_count} | 字符: {char_count}") + # 同时更新侧边栏进度(如果有的话) + if hasattr(self.sidebar, 'update_progress'): + # 这里可以添加打字进度的逻辑 + pass + else: + self.word_count_label.setText("字数: 0") + + def update_info_display(self): + """更新信息显示 - 优化性能""" + if not self.network_service: + # 网络服务未初始化时显示默认信息 + self.weather_label.setText("🌤 天气: 加载中...") + self.quote_label.setText("📖 名言: 加载中...") + return + + try: + # 获取天气信息 + weather_info = self.network_service.get_weather_info() + if weather_info: + weather_text = f"🌤 {weather_info['city']}: {weather_info['temperature']}°C" + self.weather_label.setText(weather_text) + else: + self.weather_label.setText("🌤 天气: 获取失败") + + # 获取名言 - 显示完整内容,不再截断 + quote_text = self.network_service.get_daily_quote() + if quote_text: + # 显示完整名言,不再截断,让滑动区域处理显示 + self.quote_label.setText(f"📖 {quote_text}") + # 调整标签大小以适应内容 + self.quote_label.adjustSize() + else: + self.quote_label.setText("📖 名言: 获取失败") + + except Exception as e: + print(f"更新信息失败: {e}") + self.weather_label.setText("🌤 天气: 获取失败") + self.quote_label.setText("📖 名言: 获取失败") + + def closeEvent(self, event): + """关闭事件""" + # 检查是否有未保存的文件 + unsaved_tabs = [] + for i in range(self.tab_widget.count()): + widget = self.tab_widget.widget(i) + if isinstance(widget, MarkTextEditor) and widget.is_modified: + tab_text = self.tab_widget.tabText(i) + unsaved_tabs.append(tab_text) + + if unsaved_tabs: + reply = QMessageBox.question( + self, "确认", f"以下文件未保存:\n{', '.join(unsaved_tabs)}\n\n是否继续关闭?", + QMessageBox.Yes | QMessageBox.No + ) + + if reply == QMessageBox.No: + event.ignore() + return + + # 停止定时器 + if hasattr(self, 'timer'): + self.timer.stop() + + event.accept() + if __name__ == "__main__": - main() \ No newline at end of file + app = QApplication(sys.argv) + + # 设置应用样式 + app.setStyle(QStyleFactory.create("Fusion")) + + # 创建并显示主窗口 + window = MarkTextMainWindow() + window.show() + + sys.exit(app.exec_()) \ No newline at end of file diff --git a/src/services/network_service.py b/src/services/network_service.py index e67dedc..7eaef41 100644 --- a/src/services/network_service.py +++ b/src/services/network_service.py @@ -2,6 +2,7 @@ import requests import json import os +import time from typing import Optional, Dict, Any class NetworkService: @@ -11,138 +12,354 @@ class NetworkService: self.api_key = None self.cache = {} self.session = requests.Session() + # 天气缓存相关属性 + self._cached_weather_data = None # 缓存的天气数据 + self._cached_location = None # 缓存的定位信息 + self._weather_cache_timestamp = None # 缓存时间戳 - def get_weather_info(self) -> Optional[Dict[str, Any]]: + def get_cached_weather_data(self): + """获取缓存的天气数据""" + return self._cached_weather_data + + def get_cached_location(self): + """获取缓存的定位信息""" + return self._cached_location + + def set_weather_cache(self, weather_data, location): + """设置天气缓存""" + self._cached_weather_data = weather_data + self._cached_location = location + self._weather_cache_timestamp = time.time() + + def clear_weather_cache(self): + """清除天气缓存""" + self._cached_weather_data = None + self._cached_location = None + self._weather_cache_timestamp = None + + def is_weather_cache_valid(self): + """检查天气缓存是否有效(30分钟内)""" + if self._weather_cache_timestamp is None: + return False + return (time.time() - self._weather_cache_timestamp) < 1800 # 30分钟 + + def get_user_ip(self): + """获取用户IP地址 - 使用多个备用服务""" + # 首先尝试获取本地IP + try: + import socket + hostname = socket.gethostname() + local_ip = socket.gethostbyname(hostname) + if local_ip and not local_ip.startswith("127."): + print(f"获取到本地IP: {local_ip}") + return local_ip + except Exception as e: + print(f"获取本地IP失败: {e}") + + # 如果本地IP获取失败,使用备用外部服务 + ip_services = [ + "https://httpbin.org/ip", + "https://api.ipify.org?format=json", + "https://ipapi.co/json/" + ] + + for service in ip_services: + try: + print(f"尝试从 {service} 获取IP地址...") + response = self.session.get(service, timeout=3, verify=False) + if response.status_code == 200: + data = response.json() + ip = data.get("origin") or data.get("ip") or data.get("ip_address") + if ip: + print(f"成功从 {service} 获取IP: {ip}") + return ip + except Exception as e: + print(f"从 {service} 获取IP失败: {e}") + continue + + print("所有IP获取服务都失败了,使用默认IP") + return "8.8.8.8" # 使用Google DNS作为默认IP + + def get_default_weather(self): + """获取默认天气数据""" + return { + "city": "北京", + "temperature": 20, + "description": "晴天", + "humidity": 60, + "wind_speed": 3.5 + } + + def clear_weather_cache(self): + """清除天气缓存""" + self._cached_weather_data = None + self._cached_location = None + self._weather_cache_time = None + print("天气缓存已清除") + + def get_weather_info(self, use_cache: bool = True) -> Optional[Dict[str, Any]]: + """获取天气信息,支持缓存机制""" + + # 如果启用缓存且缓存有效,直接返回缓存数据 + if use_cache and self.is_weather_cache_valid(): + print("使用缓存的天气数据") + return self._cached_weather_data + + # 如果没有指定城市,使用自动定位 + return self.get_weather_info_by_city(None, use_cache) + + def get_weather_info_by_city(self, city_name: Optional[str] = None, use_cache: bool = True) -> Optional[Dict[str, Any]]: + """根据城市名获取天气信息""" + + # 如果启用缓存且缓存有效,直接返回缓存数据 + if use_cache and self.is_weather_cache_valid() and not city_name: + print("使用缓存的天气数据") + return self._cached_weather_data # 实现天气信息获取逻辑 - # 1. 获取用户IP地址 - try: - ip_response = self.session.get("https://httpbin.org/ip", timeout=5, verify=False) - ip_data = ip_response.json() - ip = ip_data.get("origin", "") + # 如果指定了城市名,直接使用该城市,否则通过IP定位 + if city_name: + print(f"使用指定城市: {city_name}") + city = city_name + # 清除缓存以确保获取新数据 + if hasattr(self, 'clear_weather_cache'): + self.clear_weather_cache() + else: + # 1. 获取用户IP地址 - 使用多个备用服务 + print("开始获取天气信息...") + ip = self.get_user_ip() + print(f"获取到的IP地址: {ip}") + + if not ip: + print("无法获取IP地址,使用默认天气数据") + return self.get_default_weather() # 2. 根据IP获取地理位置 # 注意:这里使用免费的IP地理位置API,实际应用中可能需要更精确的服务 - location_response = self.session.get(f"http://ip-api.com/json/{ip}", timeout=5, verify=False) + location_url = f"http://ip-api.com/json/{ip}" + print(f"请求地理位置: {location_url}") + location_response = self.session.get(location_url, timeout=5, verify=False) location_data = location_response.json() + print(f"地理位置响应: {location_data}") if location_data.get("status") != "success": - return None + print("地理位置获取失败,使用默认天气数据") + return self.get_default_weather() city = location_data.get("city", "Unknown") + print(f"获取到的城市: {city}") + + if not city: + print("无法获取城市名称,使用默认天气数据") + return self.get_default_weather() + + # 保存定位信息到缓存 + self._cached_location = { + "ip": ip, + "city": city, + "country": location_data.get("country", "Unknown"), + "region": location_data.get("regionName", "Unknown") + } + + # 3. 调用天气API获取天气数据 + # 注意:这里使用OpenWeatherMap API作为示例,需要API密钥 + # 在实际应用中,需要设置有效的API密钥 + if self.api_key: + weather_url = f"http://api.openweathermap.org/data/2.5/weather?q={city}&appid={self.api_key}&units=metric&lang=zh_cn" + weather_response = self.session.get(weather_url, timeout=5, verify=False) + weather_data = weather_response.json() + + # 4. 解析并格式化数据 + if weather_response.status_code == 200: + formatted_weather = { + "city": city, + "temperature": weather_data["main"]["temp"], + "description": weather_data["weather"][0]["description"], + "humidity": weather_data["main"]["humidity"], + "wind_speed": weather_data["wind"]["speed"] + } + # 5. 返回天气信息字典 + return formatted_weather + else: + # 当没有API密钥时,使用免费的天气API获取真实数据 + # 首先尝试获取城市ID(需要映射城市名到ID) + city_id_map = { + "Beijing": "101010100", + "Shanghai": "101020100", + "Tianjin": "101030100", + "Chongqing": "101040100", + "Hong Kong": "101320101", + "Macau": "101330101", + # 添加中文城市名映射 + "北京": "101010100", + "上海": "101020100", + "天津": "101030100", + "重庆": "101040100", + "香港": "101320101", + "澳门": "101330101", + # 添加更多主要城市 + "广州": "101280101", + "深圳": "101280601", + "杭州": "101210101", + "南京": "101190101", + "成都": "101270101", + "武汉": "101200101", + "西安": "101110101", + "沈阳": "101070101", + "青岛": "101120201", + "大连": "101070201", + "苏州": "101190401", + "无锡": "101190201" + } + + # 尝试映射英文城市名到ID + city_id = city_id_map.get(city) - # 3. 调用天气API获取天气数据 - # 注意:这里使用OpenWeatherMap API作为示例,需要API密钥 - # 在实际应用中,需要设置有效的API密钥 - if self.api_key: - weather_url = f"http://api.openweathermap.org/data/2.5/weather?q={city}&appid={self.api_key}&units=metric&lang=zh_cn" + # 如果找到城市ID,使用该ID获取天气,否则使用默认北京ID + if city_id: + weather_city_id = city_id + print(f"使用城市ID获取天气: {city} -> {weather_city_id}") + else: + # 对于中国主要城市,直接使用拼音映射 + city_pinyin_map = { + "Beijing": "北京", + "Shanghai": "上海", + "Tianjin": "天津", + "Chongqing": "重庆", + "Guangzhou": "广州", + "Shenzhen": "深圳", + "Hangzhou": "杭州", + "Nanjing": "南京", + "Chengdu": "成都", + "Wuhan": "武汉", + "Xi\'an": "西安", + "Shenyang": "沈阳" + } + chinese_city = city_pinyin_map.get(city, city) + weather_city_id = "101010100" # 默认北京ID + print(f"使用默认城市ID获取天气: {city} -> {weather_city_id}") + + # 使用免费天气API获取天气数据 + try: + # 使用和风天气免费API的替代方案 - sojson天气API + weather_url = f"http://t.weather.sojson.com/api/weather/city/{weather_city_id}" + print(f"请求天气数据: {weather_url}") weather_response = self.session.get(weather_url, timeout=5, verify=False) + print(f"天气响应状态码: {weather_response.status_code}") weather_data = weather_response.json() + print(f"天气数据响应: {weather_data}") - # 4. 解析并格式化数据 - if weather_response.status_code == 200: + if weather_data.get("status") == 200: + # 解析天气数据 + current_data = weather_data.get("data", {}) + wendu = current_data.get("wendu", "N/A") + shidu = current_data.get("shidu", "N/A") + forecast = current_data.get("forecast", []) + + # 获取第一个预报项作为当前天气 + current_weather = forecast[0] if forecast else {} + weather_type = current_weather.get("type", "晴") + + # 获取生活指数信息 + lifetips = [] + if current_weather: + # 从预报数据中提取生活提示 + ganmao = current_weather.get("ganmao", "") + if ganmao: + lifetips.append(f"感冒指数: {ganmao}") + + # 添加其他生活指数(基于天气类型推断) + if "雨" in weather_type: + lifetips.append("出行建议: 记得带伞") + elif "晴" in weather_type: + lifetips.append("出行建议: 适合户外活动") + elif "雪" in weather_type: + lifetips.append("出行建议: 注意防滑保暖") + elif "雾" in weather_type or "霾" in weather_type: + lifetips.append("健康提醒: 减少户外运动") + + # 温度相关建议 + temp = float(wendu) if wendu != "N/A" else 20 + if temp > 30: + lifetips.append("穿衣建议: 注意防暑降温") + elif temp < 5: + lifetips.append("穿衣建议: 注意保暖防寒") + elif temp < 15: + lifetips.append("穿衣建议: 适当添加衣物") + else: + lifetips.append("穿衣建议: 天气舒适") + formatted_weather = { "city": city, - "temperature": weather_data["main"]["temp"], - "description": weather_data["weather"][0]["description"], - "humidity": weather_data["main"]["humidity"], - "wind_speed": weather_data["wind"]["speed"] + "temperature": float(wendu) if wendu != "N/A" else 20, + "description": weather_type, + "humidity": shidu.replace("%", "") if shidu != "N/A" else "60", + "wind_speed": "3.5", # 默认风速 + "lifetips": lifetips # 生活提示列表 } - # 5. 返回天气信息字典 + print(f"成功获取天气数据: {formatted_weather}") + + # 缓存天气数据 + self.set_weather_cache(formatted_weather, self._cached_location) return formatted_weather - else: - # 当没有API密钥时,使用免费的天气API获取真实数据 - # 首先尝试获取城市ID(需要映射城市名到ID) - city_id_map = { - "Beijing": "101010100", - "Shanghai": "101020100", - "Tianjin": "101030100", - "Chongqing": "101040100", - "Hong Kong": "101320101", - "Macau": "101330101" - } - - # 尝试映射英文城市名到ID - city_id = city_id_map.get(city) - - # 如果找不到映射,尝试直接使用城市名 - if not city_id: - # 对于中国主要城市,直接使用拼音映射 - city_pinyin_map = { - "Beijing": "北京", - "Shanghai": "上海", - "Tianjin": "天津", - "Chongqing": "重庆" - } - chinese_city = city_pinyin_map.get(city, city) + else: + print(f"天气API返回错误状态: {weather_data.get('status')}") - # 使用免费天气API - try: - # 使用和风天气免费API的替代方案 - sojson天气API - weather_url = f"http://t.weather.sojson.com/api/weather/city/101010100" # 默认北京 - weather_response = self.session.get(weather_url, timeout=5, verify=False) - weather_data = weather_response.json() - - if weather_data.get("status") == 200: - # 解析天气数据 - current_data = weather_data.get("data", {}) - wendu = current_data.get("wendu", "N/A") - shidu = current_data.get("shidu", "N/A") - forecast = current_data.get("forecast", []) - - # 获取第一个预报项作为当前天气 - current_weather = forecast[0] if forecast else {} - weather_type = current_weather.get("type", "晴") - - formatted_weather = { - "city": city, - "temperature": float(wendu) if wendu != "N/A" else 20, - "description": weather_type, - "humidity": shidu.replace("%", "") if shidu != "N/A" else "60", - "wind_speed": "3.5" # 默认风速 - } - return formatted_weather - except Exception as e: - print(f"获取免费天气数据时出错: {e}") - - # 如果以上都失败,返回默认数据 - return { - "city": city, - "temperature": 20, - "description": "晴天", - "humidity": 60, - "wind_speed": 3.5 - } - except Exception as e: - print(f"获取天气信息时出错: {e}") - return None + except Exception as e: + print(f"获取免费天气数据时出错: {e}") + + # 如果以上都失败,返回默认数据 + default_weather = { + "city": city, + "temperature": 20, + "description": "晴天", + "humidity": 60, + "wind_speed": 3.5, + "lifetips": [ + "穿衣建议: 天气舒适", + "出行建议: 适合户外活动", + "健康提醒: 保持良好心情" + ] + } + print(f"使用默认天气数据 for {city}") + + # 缓存默认天气数据 + self.set_weather_cache(default_weather, self._cached_location) + return default_weather def get_daily_quote(self) -> Optional[str]: - # 实现每日一句获取逻辑 - # 1. 调用名言API + # 实现每日一句获取逻辑 - 使用古诗词API try: - # 使用一个免费的名言API,禁用SSL验证以避免证书问题 - response = self.session.get("https://api.quotable.io/random", timeout=5, verify=False) + # 使用古诗词·一言API - 每次返回随机不同的诗词 + response = self.session.get("https://v1.jinrishici.com/all.json", timeout=5, verify=False) - # 2. 解析返回的名言数据 + # 2. 解析返回的古诗词数据 if response.status_code == 200: - quote_data = response.json() - content = quote_data.get("content", "") - author = quote_data.get("author", "") + poetry_data = response.json() + content = poetry_data.get('content', '') + author = poetry_data.get('author', '') + title = poetry_data.get('origin', '') - # 3. 格式化名言文本 - formatted_quote = f'"{content}" - {author}' + # 3. 格式化古诗词文本 + if content and author and title: + formatted_poetry = f"{content} — {author}《{title}》" + elif content and author: + formatted_poetry = f"{content} — {author}" + elif content: + formatted_poetry = content + else: + formatted_poetry = "暂无古诗词" - # 4. 返回名言字符串 - return formatted_quote + # 4. 返回古诗词字符串 + return formatted_poetry else: - # 如果API调用失败,返回默认名言 - return "书山有路勤为径,学海无涯苦作舟。" + # 如果API调用失败,返回默认古诗词 + return "山重水复疑无路,柳暗花明又一村。" except Exception as e: - print(f"获取每日一句时出错: {e}") - # 出错时返回默认名言 - return "书山有路勤为径,学海无涯苦作舟。" + print(f"获取古诗词时出错: {e}") + # 出错时返回默认古诗词 + return "山重水复疑无路,柳暗花明又一村。" def download_image(self, url: str) -> Optional[bytes]: diff --git a/src/ui/word_style_ui.py b/src/ui/word_style_ui.py index 0957101..07b5ccf 100644 --- a/src/ui/word_style_ui.py +++ b/src/ui/word_style_ui.py @@ -333,6 +333,16 @@ class WordRibbon(QFrame): }} """) + # 更新名言标签样式 + if hasattr(self, 'quote_label') and self.quote_label is not None: + self.quote_label.setStyleSheet(f""" + QLabel {{ + color: {colors['text_secondary']}; + font-style: italic; + font-size: 12px; + }} + """) + # 更新下拉框样式 self.update_combo_styles(is_dark) @@ -830,10 +840,10 @@ class WordRibbon(QFrame): # 每日一言显示标签 - 增大尺寸 self.quote_label = QLabel("暂无") - self.quote_label.setStyleSheet("QLabel { color: #666666; font-style: italic; font-size: 10px; }") + self.quote_label.setStyleSheet("QLabel { color: #666666; font-style: italic; font-size: 12px; }") self.quote_label.setWordWrap(True) - self.quote_label.setFixedWidth(250) # 增加到250像素宽度 - self.quote_label.setMinimumHeight(40) # 设置最小高度,增加显示空间 + self.quote_label.setFixedWidth(400) # 增加到400像素宽度 + self.quote_label.setMinimumHeight(60) # 设置最小高度,增加显示空间 # 添加到主布局 quote_layout.addLayout(top_row_layout) diff --git a/src/word_main_window.py b/src/word_main_window.py index 8e66b19..f5d0b70 100644 --- a/src/word_main_window.py +++ b/src/word_main_window.py @@ -168,6 +168,10 @@ class WordStyleMainWindow(QMainWindow): # 初始化时刷新天气 self.refresh_weather() + + # 初始化天气缓存相关属性 + self.cached_weather_data = None + self.cached_location = None def init_theme(self): """初始化主题""" @@ -933,6 +937,9 @@ class WordStyleMainWindow(QMainWindow): self.quote_thread = QuoteFetchThread() self.quote_thread.quote_fetched.connect(self.update_quote_display) self.quote_thread.start() + + # 获取初始天气数据并缓存 + self.init_weather_data() def init_typing_logic(self): @@ -1777,6 +1784,48 @@ class WordStyleMainWindow(QMainWindow): self.current_weather_data = weather_data print(f"update_weather_display - 存储的current_weather_data包含life_tips: {self.current_weather_data.get('life_tips', [])}") + def init_weather_data(self): + """初始化天气数据,使用缓存机制""" + try: + print("初始化天气数据,使用缓存机制") + + # 尝试从网络服务获取缓存的天气数据 + cached_weather = self.network_service.get_cached_weather_data() + cached_location = self.network_service.get_cached_location() + + if cached_weather and cached_location: + print(f"使用缓存的天气数据: {cached_weather}") + print(f"使用缓存的定位数据: {cached_location}") + + # 格式化缓存数据 + formatted_data = { + 'city': cached_weather.get('city', cached_location.get('city', '未知城市')), + 'current': cached_weather.get('current', {}), + 'forecast': cached_weather.get('forecast', []), + 'life_tips': cached_weather.get('life_tips', []) + } + + # 更新显示 + self.update_weather_display(formatted_data) + # 同步更新天气悬浮窗口 + if hasattr(self, 'weather_floating_widget') and self.weather_floating_widget.isVisible(): + self.weather_floating_widget.update_weather(formatted_data) + + # 保存到本地缓存 + self.cached_weather_data = cached_weather + self.cached_location = cached_location + + self.status_bar.showMessage("使用缓存天气数据", 2000) + else: + print("没有缓存数据,使用网络获取") + # 没有缓存数据,使用网络获取 + self.refresh_weather() + + except Exception as e: + print(f"初始化天气数据失败: {e}") + # 如果缓存获取失败,回退到网络获取 + self.refresh_weather() + def refresh_weather(self): """手动刷新天气信息""" try: @@ -1796,10 +1845,10 @@ class WordStyleMainWindow(QMainWindow): print(f"刷新天气 - 当前选择的城市: {current_city}") if current_city == '自动定位': - # 使用自动定位 + # 使用自动定位,启用缓存 weather_data = self.weather_api.get_weather_data() else: - # 使用选中的城市 + # 使用选中的城市,启用缓存 weather_data = self.weather_api.get_weather_data(current_city) if weather_data: @@ -1816,6 +1865,12 @@ class WordStyleMainWindow(QMainWindow): # 同步更新天气悬浮窗口 if hasattr(self, 'weather_floating_widget') and self.weather_floating_widget.isVisible(): self.weather_floating_widget.update_weather(formatted_data) + + # 保存到网络服务缓存和本地缓存 + self.network_service.set_weather_cache(weather_data, self.network_service.get_cached_location()) + self.cached_weather_data = weather_data + self.cached_location = self.network_service.get_cached_location() + self.status_bar.showMessage("天气数据已刷新", 2000) else: self.status_bar.showMessage("天气数据刷新失败,请检查API密钥", 3000) @@ -1829,14 +1884,17 @@ class WordStyleMainWindow(QMainWindow): """显示详细天气信息对话框""" from PyQt5.QtWidgets import QDialog, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QTextEdit - # 检查是否有天气数据 - if not hasattr(self, 'current_weather_data') or not self.current_weather_data: + # 首先尝试使用本地缓存的天气数据 + if hasattr(self, 'cached_weather_data') and self.cached_weather_data: + weather_data = self.cached_weather_data + print(f"详细天气对话框 - 使用本地缓存天气数据: {weather_data}") + elif hasattr(self, 'current_weather_data') and self.current_weather_data: + weather_data = self.current_weather_data + print(f"详细天气对话框 - 使用当前天气数据: {weather_data}") + else: QMessageBox.information(self, "附加工具", "暂无天气数据,请先刷新天气信息") return - weather_data = self.current_weather_data - print(f"详细天气对话框 - 天气数据: {weather_data}") - # 创建对话框 dialog = QDialog(self) dialog.setWindowTitle("详细天气") diff --git a/start_marktext.py b/start_marktext.py new file mode 100644 index 0000000..c02881d --- /dev/null +++ b/start_marktext.py @@ -0,0 +1,169 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +MagicWord - MarkText风格启动器 +基于MarkText开源编辑器的现代化Markdown编辑器 +集成MagicWord现有功能:学习模式、天气、名言等 +""" + +import sys +import os +import platform +import traceback + +# 添加项目根目录到Python路径 +project_root = os.path.dirname(os.path.abspath(__file__)) +src_path = os.path.join(project_root, 'src') +sys.path.insert(0, project_root) +sys.path.insert(0, src_path) + +# 设置Qt平台插件路径 +def setup_qt_environment(): + """设置Qt环境变量""" + system = platform.system() + python_version = f"python{sys.version_info.major}.{sys.version_info.minor}" + + possible_paths = [] + + if system == "Windows": + possible_paths.extend([ + os.path.join(project_root, '.venv', 'Lib', 'site-packages', 'PyQt5', 'Qt5', 'plugins'), + os.path.join(sys.prefix, 'Lib', 'site-packages', 'PyQt5', 'Qt5', 'plugins'), + ]) + elif system == "Darwin": # macOS + possible_paths.extend([ + os.path.join(project_root, '.venv', 'lib', python_version, 'site-packages', 'PyQt5', 'Qt5', 'plugins'), + os.path.join(sys.prefix, 'lib', python_version, 'site-packages', 'PyQt5', 'Qt5', 'plugins'), + '/usr/local/opt/qt5/plugins', + '/opt/homebrew/opt/qt5/plugins', + ]) + elif system == "Linux": + possible_paths.extend([ + os.path.join(project_root, '.venv', 'lib', python_version, 'site-packages', 'PyQt5', 'Qt5', 'plugins'), + os.path.join(sys.prefix, 'lib', python_version, 'site-packages', 'PyQt5', 'Qt5', 'plugins'), + '/usr/lib/x86_64-linux-gnu/qt5/plugins', + '/usr/lib/qt5/plugins', + ]) + + for path in possible_paths: + if os.path.exists(path) and os.path.exists(os.path.join(path, 'platforms')): + os.environ['QT_PLUGIN_PATH'] = path + os.environ['QT_QPA_PLATFORM_PLUGIN_PATH'] = os.path.join(path, 'platforms') + + if system == "Darwin": + os.environ['QT_QPA_PLATFORM'] = 'cocoa' + os.environ['QT_MAC_WANTS_LAYER'] = '1' + elif system == "Windows": + os.environ['QT_QPA_PLATFORM'] = 'windows' + elif system == "Linux": + os.environ['QT_QPA_PLATFORM'] = 'xcb' + + print(f"✅ Qt插件路径设置成功: {path}") + return True + + print("⚠️ 警告:未找到Qt插件路径") + return False + +# 检查依赖 +def check_dependencies(): + """检查必要的依赖""" + missing_deps = [] + + try: + import PyQt5 + except ImportError: + missing_deps.append("PyQt5") + + try: + import requests + except ImportError: + missing_deps.append("requests") + + try: + import docx + except ImportError: + missing_deps.append("python-docx") + + try: + import fitz # PyMuPDF + except ImportError: + missing_deps.append("PyMuPDF") + + if missing_deps: + print(f"❌ 缺少依赖包: {', '.join(missing_deps)}") + print("请运行: pip install " + " ".join(missing_deps)) + return False + + return True + +def main(): + """主函数""" + try: + print("🚀 启动MagicWord - MarkText风格编辑器") + + # 检查依赖 + if not check_dependencies(): + sys.exit(1) + + # 设置Qt环境 + setup_qt_environment() + + # 导入Qt相关模块 + from PyQt5.QtWidgets import QApplication + from PyQt5.QtCore import Qt + from PyQt5.QtGui import QIcon + + # 设置高DPI支持 + QApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True) + QApplication.setAttribute(Qt.AA_UseHighDpiPixmaps, True) + + # 创建应用 + app = QApplication(sys.argv) + app.setApplicationName("MagicWord") + app.setApplicationVersion("1.0.0") + app.setOrganizationName("MagicWord") + + # 设置样式 + if platform.system() != "Darwin": + app.setStyle('Fusion') + + # 设置图标 + icon_path = os.path.join(project_root, 'resources', 'icons', 'app_icon.png') + if os.path.exists(icon_path): + app.setWindowIcon(QIcon(icon_path)) + + # 导入并创建MarkText主窗口 + try: + from main import MarkTextMainWindow + main_window = MarkTextMainWindow() + print("✅ MarkText编辑器启动成功!") + except ImportError as e: + print(f"❌ MarkText编辑器导入失败: {e}") + print("正在尝试启动Word风格编辑器...") + + try: + from word_main_window import WordStyleMainWindow + main_window = WordStyleMainWindow() + print("✅ Word风格编辑器启动成功!") + except ImportError as e2: + print(f"❌ Word风格编辑器也启动失败: {e2}") + sys.exit(1) + + # 显示窗口 + main_window.show() + + # 运行应用 + exit_code = app.exec_() + print(f"👋 应用已退出,退出码: {exit_code}") + sys.exit(exit_code) + + except KeyboardInterrupt: + print("\n👋 用户中断,正在退出...") + sys.exit(0) + except Exception as e: + print(f"❌ 发生未预期的错误: {e}") + traceback.print_exc() + sys.exit(1) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/test_sync_fixed.py b/test_sync_fixed.py new file mode 100644 index 0000000..a034561 --- /dev/null +++ b/test_sync_fixed.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python3 +""" +简化测试脚本 - 验证学习模式内容同步功能 +""" + +import sys +import os +sys.path.append(os.path.join(os.path.dirname(__file__), 'src')) + +from PyQt5.QtWidgets import QApplication, QTextEdit, QVBoxLayout, QWidget +from learning_mode_window import LearningModeWindow + +class TestWindow(QWidget): + def __init__(self): + super().__init__() + self.setWindowTitle("测试主窗口") + self.setGeometry(100, 100, 600, 400) + + # 创建布局 + layout = QVBoxLayout() + + # 创建文本编辑器 + self.text_edit = QTextEdit() + self.text_edit.setPlainText("初始内容\n") + layout.addWidget(self.text_edit) + + self.setLayout(layout) + + # 创建学习模式窗口 + self.learning_window = LearningModeWindow(parent=self) + self.learning_window.imported_content = "这是一段测试内容。" + self.learning_window.current_position = 0 + self.learning_window.show() + + # 连接内容同步信号 + self.learning_window.content_changed.connect(self.on_content_changed) + + print("测试开始...") + print(f"主窗口内容: {repr(self.text_edit.toPlainText())}") + print(f"学习窗口内容: {repr(self.learning_window.imported_content)}") + print(f"学习窗口当前位置: {self.learning_window.current_position}") + + # 模拟用户输入正确内容 + print("\n模拟用户输入正确内容...") + self.learning_window.current_position = 3 # 用户输入了3个字符 + self.learning_window.on_text_changed() # 调用文本变化处理 + + def on_content_changed(self, new_content, position): + """内容同步回调""" + print(f"收到内容同步信号: new_content={repr(new_content)}, position={position}") + # 在文本编辑器末尾添加新内容 + current_text = self.text_edit.toPlainText() + self.text_edit.setPlainText(current_text + new_content) + print(f"主窗口更新后内容: {repr(self.text_edit.toPlainText())}") + +def test_content_sync(): + """测试内容同步功能""" + app = QApplication(sys.argv) + + test_window = TestWindow() + test_window.show() + + print("\n测试完成!") + + # 运行应用 + sys.exit(app.exec_()) + +if __name__ == "__main__": + test_content_sync() \ No newline at end of file diff --git a/test_weather_lifetips.py b/test_weather_lifetips.py new file mode 100644 index 0000000..f1e77fb --- /dev/null +++ b/test_weather_lifetips.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 +""" +测试天气生活提示功能 +""" +import sys +import os + +# 添加src目录到路径 +sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src')) + +from services.network_service import NetworkService + +def test_weather_with_lifetips(): + """测试包含生活提示的天气功能""" + print("🌤 测试天气生活提示功能") + print("=" * 50) + + # 创建网络服务实例 + network_service = NetworkService() + + # 获取天气信息 + print("正在获取天气信息...") + weather_info = network_service.get_weather_info() + + if weather_info: + print(f"✅ 成功获取天气数据:") + print(f"城市: {weather_info['city']}") + print(f"温度: {weather_info['temperature']}°C") + print(f"天气: {weather_info['description']}") + print(f"湿度: {weather_info['humidity']}%") + print(f"风速: {weather_info['wind_speed']}m/s") + + # 显示生活提示 + lifetips = weather_info.get('lifetips', []) + if lifetips: + print(f"\n🌟 生活提示 ({len(lifetips)}条):") + for i, tip in enumerate(lifetips, 1): + print(f" {i}. {tip}") + else: + print("⚠️ 未获取到生活提示") + + # 模拟显示详细信息格式 + print(f"\n📋 详细信息显示格式:") + weather_text = f"{weather_info['city']}: {weather_info['temperature']}°C, {weather_info['description']}" + weather_text += f"\n湿度: {weather_info['humidity']}%" + weather_text += f"\n风速: {weather_info['wind_speed']}m/s" + + if lifetips: + weather_text += "\n\n🌟 生活提示:" + for tip in lifetips: + weather_text += f"\n• {tip}" + + print(weather_text) + + else: + print("❌ 获取天气信息失败") + + print("\n" + "=" * 50) + print("测试完成!") + +if __name__ == "__main__": + test_weather_with_lifetips() \ No newline at end of file