Files
leonpan-pc/app/view/widgets/preview_box.py
2025-11-02 19:17:20 +08:00

884 lines
37 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# coding: utf-8
from loguru import logger
import os
from PyQt6.QtCore import Qt, QTimer, QUrl, QDir, QFileInfo
from PyQt6.QtGui import QPixmap
from PyQt6.QtWebEngineWidgets import QWebEngineView
from PyQt6.QtWebEngineCore import QWebEnginePage, QWebEngineContextMenuRequest
from qfluentwidgets import (
ImageLabel,
InfoBar,
InfoBarPosition,
IndeterminateProgressBar,
MessageBoxBase,
PushButton,
)
from app.core import (ImageLoaderThread, TextLoaderThread, UpdateFileContentThread)
class MonacoWebEnginePage(QWebEnginePage):
"""自定义WebEnginePage以处理右键菜单"""
def __init__(self, parent=None):
super().__init__(parent)
def contextMenuEvent(self, event):
"""自定义右键菜单事件,允许基本的编辑操作"""
request = event.request()
# 可以在这里根据需要自定义右键菜单的行为
# 例如,只允许复制、粘贴等特定操作
# 目前我们让Monaco Editor自己处理右键菜单
pass
from app.core.api import miaoStarsBasicApi
from app.view.components.empty_card import EmptyCard
# 图片预览类
def createThumbnail(pixmap, max_size=200):
"""创建快速缩略图"""
if pixmap.isNull():
return pixmap
# 使用快速缩放算法
return pixmap.scaled(
max_size,
max_size,
Qt.AspectRatioMode.KeepAspectRatio,
Qt.TransformationMode.FastTransformation,
)
class OptimizedPreviewBox(MessageBoxBase):
def __init__(self, parent=None, url=None, fileId=None):
super().__init__(parent=parent)
self.fileId = fileId # 保存文件ID
self.url = url # 暂存原始URL可能需要作为fallback
logger.info(f"初始化图片预览框文件ID: {self.fileId}, 原始URL: {self.url}")
self.widget.setMinimumSize(500, 500)
self.original_pixmap = None
self.current_scale = 1.0
# 加载状态显示
self.loadingCard = EmptyCard(self)
self.loadingCard.load()
self.viewLayout.addWidget(self.loadingCard, 0, Qt.AlignmentFlag.AlignCenter)
# 图片显示标签
self.previewLabel = ImageLabel(self)
self.previewLabel.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.previewLabel.setScaledContents(False) # 重要:禁用自动缩放
self.viewLayout.addWidget(self.previewLabel, 0, Qt.AlignmentFlag.AlignCenter)
# 延迟启动加载避免阻塞UI初始化
from PyQt6.QtCore import QTimer
QTimer.singleShot(100, self._initImageLoading)
def _ensure_full_url(self, url):
"""确保URL是完整的添加scheme和base URL如果缺失"""
if not url:
return url
# 检查URL是否已经包含scheme
if url.startswith(('http://', 'https://')):
return url
# 对于相对路径使用API的base URL构建完整URL
# 移除可能的前导斜杠,避免重复
path = url.lstrip('/')
# 从MiaoStarsBasicApi获取base URL但只使用到域名部分
base_url = miaoStarsBasicApi.basicApi.split('/api/v4')[0]
full_url = f"{base_url}/{path}"
logger.debug(f"将相对路径转换为完整URL: {url} -> {full_url}")
return full_url
def _initImageLoading(self):
"""初始化图片加载优先使用getFileUrl方法获取临时URL"""
if self.fileId:
logger.info(f"使用文件URI {self.fileId} 获取临时预览URL")
try:
# 使用getFileUrl方法获取临时URL
from app.core.api import miaoStarsBasicApi
response = miaoStarsBasicApi.getFileUrl(self.fileId, redirect=False)
# 详细记录响应内容
logger.debug(f"getFileUrl响应: {response}")
# 处理getFileUrl的返回值支持多种格式
if isinstance(response, str):
# 直接返回URL字符串的情况
preview_url = response.strip('` ')
logger.info(f"成功获取临时预览URL: {preview_url}")
self.url = preview_url
elif isinstance(response, dict):
# 检查是否有code字段表示错误
if response.get('code') == -1:
error_msg = response.get('msg', '获取临时URL失败')
logger.warning(f"获取临时URL失败: {error_msg}将使用原始URL作为fallback")
# 检查是否有urls字段Cloudreve API返回格式
elif 'urls' in response and isinstance(response['urls'], list) and len(response['urls']) > 0:
preview_url = response['urls'][0].get('url', '').strip('` ')
logger.info(f"成功获取临时预览URL: {preview_url}")
self.url = preview_url
# 检查是否有data字段Cloudreve V4 API标准格式
elif 'data' in response:
data = response['data']
if isinstance(data, dict):
# 如果data是字典检查是否包含urls数组
if 'urls' in data and isinstance(data['urls'], list) and len(data['urls']) > 0:
preview_url = data['urls'][0].get('url', '').strip('` ')
logger.info(f"成功从data中获取临时预览URL: {preview_url}")
self.url = preview_url
else:
# 尝试将data直接转为字符串作为fallback
preview_url = str(data).strip('` ')
logger.warning(f"data字段不包含urls数组使用原始data字符串: {preview_url}")
self.url = preview_url
else:
# data不是字典直接转为字符串
preview_url = str(data).strip('` ')
logger.info(f"成功获取临时预览URL: {preview_url}")
self.url = preview_url
else:
error_msg = response.get('msg', '获取临时URL失败')
logger.warning(f"获取临时URL失败: {error_msg}将使用原始URL作为fallback")
if self.url:
self.url = self._ensure_full_url(self.url)
logger.info(f"使用处理后的fallback URL: {self.url}")
else:
logger.error("没有可用的URL无法加载图片")
self.handleError("没有可用的图片URL")
return
except Exception as e:
logger.error(f"获取预览URL时发生异常: {str(e)}")
# 尝试使用备用方式处理
if self.url:
self.url = self._ensure_full_url(self.url)
logger.info(f"异常后使用处理后的fallback URL: {self.url}")
else:
logger.error("异常后没有可用的URL无法加载图片")
self.handleError(f"获取URL异常: {str(e)}")
return
else:
# 如果没有fileId尝试处理原始URL
if self.url:
self.url = self._ensure_full_url(self.url)
logger.info(f"使用处理后的原始URL: {self.url}")
else:
logger.error("没有提供文件ID或URL无法加载图片")
self.handleError("没有提供图片URL")
return
# 创建图片加载线程
self.imageLoaderThread = ImageLoaderThread(self.url)
self.imageLoaderThread.imageLoaded.connect(self.setPreviewImg)
self.imageLoaderThread.errorOccurred.connect(self.handleError)
self.imageLoaderThread.progressUpdated.connect(self.updateProgress)
# 开始加载
self.imageLoaderThread.start()
logger.info(f"开始加载图片: {self.url}")
# startLoading方法已被_initImageLoading替代
def updateProgress(self, progress):
"""更新加载进度"""
logger.debug(f"图片加载进度: {progress}%")
self.loadingCard.setText(f"正在加载图片... {progress}%")
def setPreviewImg(self, img: QPixmap):
"""设置预览图片"""
logger.info(f"图片加载成功,尺寸: {img.width()}x{img.height()}px")
self.loadingCard.hide()
self.original_pixmap = img
# 立即显示缩略图
thumbnail = createThumbnail(img)
self.previewLabel.setPixmap(thumbnail)
# 然后异步加载高质量版本
self.adjustImageSize()
def resizeEvent(self, event):
"""重写窗口大小改变事件"""
super().resizeEvent(event)
if self.original_pixmap and not self.original_pixmap.isNull():
# 使用定时器延迟调整,避免频繁调整
from PyQt6.QtCore import QTimer
QTimer.singleShot(50, self.adjustImageSize)
def adjustImageSize(self):
"""根据窗口大小动态调整图片尺寸"""
if not self.original_pixmap or self.original_pixmap.isNull():
return
# 获取可用显示区域大小
margin = 80
available_width = self.width() - margin * 2
available_height = self.height() - margin * 2
# 获取原始图片尺寸
original_width = self.original_pixmap.width()
original_height = self.original_pixmap.height()
# 计算缩放比例
width_ratio = available_width / original_width
height_ratio = available_height / original_height
scale_ratio = min(width_ratio, height_ratio, 1.0)
# 只在需要时重新缩放
if abs(scale_ratio - self.current_scale) > 0.05: # 变化超过5%才重新缩放
self.current_scale = scale_ratio
new_width = int(original_width * scale_ratio)
new_height = int(original_height * scale_ratio)
# 使用平滑缩放
scaled_pixmap = self.original_pixmap.scaled(
new_width,
new_height,
Qt.AspectRatioMode.KeepAspectRatio,
Qt.TransformationMode.SmoothTransformation,
)
self.previewLabel.setPixmap(scaled_pixmap)
def handleError(self, msg):
"""处理加载错误"""
logger.error(f"图片预览失败: {msg}")
self.loadingCard.error()
self.previewLabel.hide()
# 文本文档预览类
class PreviewTextBox(MessageBoxBase):
"""文本预览对话框"""
def __init__(self, parent=None, url=None, _id=None):
super().__init__(parent=parent)
# 处理URL确保它是完整的URL
self.url = self._ensure_full_url(url)
logger.info(f"初始化文本预览框URL: {self.url}, 文件ID: {_id}")
self.updateTxtThread = None
self.widget.setMinimumSize(600, 400)
self._id = _id
self.isChanged = False
# 设置编辑器HTML文件路径 - 修正为项目根目录的_internal文件夹
project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
self.editor_index = os.path.join(project_root, "_internal/editor_main/index.html")
logger.info(f"编辑器HTML路径: {self.editor_index}")
# 创建占位符标签替代WebEngineView
from PyQt6.QtWidgets import QLabel
self.placeholderLabel = QLabel('''<div style='text-align:center; padding:20px;'>
<h3>文本编辑将在外部浏览器中打开</h3>
<p>请在浏览器中完成编辑后,点击"保存并返回"按钮</p>
<p>应用将自动检测并更新内容</p>
</div>''')
self.placeholderLabel.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.placeholderLabel.setWordWrap(True)
self.viewLayout.addWidget(self.placeholderLabel)
# 初始化变量
self.editorContent = ""
self.tempFilePath = None
# 轮询定时器,用于检查浏览器是否保存了内容
self.pollingTimer = QTimer(self)
self.pollingTimer.setInterval(1000) # 每秒检查一次
self.pollingTimer.timeout.connect(self._checkBrowserContent)
# 加载状态显示
self.loadingCard = EmptyCard(self)
self.loadingCard.load()
self.viewLayout.addWidget(self.loadingCard, 0, Qt.AlignmentFlag.AlignCenter)
# 确保在创建线程前使用处理后的URL
self.textLoaderThread = TextLoaderThread(self.url, fileId=_id)
self.textLoaderThread.textLoaded.connect(self.setTextContent)
self.textLoaderThread.errorOccurred.connect(self.handleError)
self.textLoaderThread.progressUpdated.connect(self.updateProgress)
self.yesButton.hide()
# 创建保存按钮
self.saveButton = PushButton("保存修改", self)
# 创建进度条
self.saveProgressBar = IndeterminateProgressBar(self)
self.saveProgressBar.setFixedHeight(4)
self.saveProgressBar.hide()
# 添加按钮和进度条到布局
self.buttonLayout.insertWidget(
0, self.saveButton, 1, Qt.AlignmentFlag.AlignVCenter
)
self.buttonLayout.insertWidget(
1, self.saveProgressBar, 1, Qt.AlignmentFlag.AlignVCenter
)
self.saveButton.setEnabled(False)
self.saveButton.clicked.connect(self.saveText)
self.cancelButton.setText("返回")
# 延迟启动加载避免阻塞UI初始化
QTimer.singleShot(100, self.startLoading)
def saveText(self):
logger.info(f"保存文本文件修改文件ID: {self._id}")
# 显示进度条并禁用按钮
self.saveProgressBar.show()
self.saveButton.setEnabled(False)
# 直接使用editorContent变量因为我们已经切换到外部浏览器编辑模式
self._saveContent(self.editorContent)
def _successSave(self):
logger.info(f"文本文件保存成功文件ID: {self._id}")
InfoBar.success(
"成功",
"修改保存成功",
Qt.Orientation.Horizontal,
True,
1000,
InfoBarPosition.TOP_RIGHT,
self.window(),
)
# 隐藏进度条
self.saveProgressBar.hide()
QTimer.singleShot(700, self.accept)
def _errorSave(self, msg):
logger.error(f"文本文件保存失败文件ID: {self._id}, 错误: {msg}")
InfoBar.error(
"失败",
msg,
Qt.Orientation.Horizontal,
True,
1000,
InfoBarPosition.TOP_RIGHT,
self.window(),
)
# 隐藏进度条并重新启用按钮
self.saveProgressBar.hide()
self.saveButton.setEnabled(True)
def _ensure_full_url(self, url):
"""确保URL是完整的添加scheme和base URL如果缺失"""
if not url:
return url
# 检查URL是否已经包含scheme
if url.startswith(('http://', 'https://')):
return url
# 对于相对路径使用API的base URL构建完整URL
# 移除可能的前导斜杠,避免重复
path = url.lstrip('/')
# 从MiaoStarsBasicApi获取base URL但只使用到域名部分
base_url = miaoStarsBasicApi.basicApi.split('/api/v4')[0]
full_url = f"{base_url}/{path}"
logger.debug(f"将相对路径转换为完整URL: {url} -> {full_url}")
return full_url
def startLoading(self):
"""开始加载文本"""
# 如果有fileId尝试获取临时URL
if self._id:
logger.info(f"使用文件URI {self._id} 获取临时文本URL")
response = miaoStarsBasicApi.getFileUrl(self._id, redirect=False)
# 处理getFileUrl的返回值支持多种格式
preview_url = None
if isinstance(response, dict):
# 检查是否是Cloudreve V4 API格式 (data.urls)
if response.get('code') == 0 and 'data' in response:
data = response['data']
# 格式1: data.urls数组
if isinstance(data, dict) and 'urls' in data and isinstance(data['urls'], list) and len(data['urls']) > 0:
preview_url = data['urls'][0].get('url', '').strip('` ')
# 格式2: data直接包含URL
elif isinstance(data, str):
preview_url = data.strip('` ')
# 检查是否直接包含urls字段
elif 'urls' in response and isinstance(response['urls'], list) and len(response['urls']) > 0:
preview_url = response['urls'][0].get('url', '').strip('` ')
# 如果成功获取到URL
if preview_url:
self.url = preview_url
logger.info(f"成功获取临时文本URL: {self.url}")
else:
error_msg = response.get('msg', '获取临时URL失败')
logger.warning(f"获取临时URL失败: {error_msg}将使用处理后的原始URL")
# 更新线程的URL和fileId
self.textLoaderThread = TextLoaderThread(self.url, fileId=self._id)
self.textLoaderThread.textLoaded.connect(self.setTextContent)
self.textLoaderThread.errorOccurred.connect(self.handleError)
self.textLoaderThread.progressUpdated.connect(self.updateProgress)
logger.info(f"开始加载文本文件: {self.url}")
self.textLoaderThread.start()
def updateProgress(self, progress):
"""更新加载进度"""
logger.debug(f"文本加载进度: {progress}%")
self.loadingCard.setText(f"正在加载文本... {progress}%")
def setTextContent(self, content):
"""设置文本内容并在外部浏览器中打开"""
# 检查内容是否已保存,如果已保存则不允许再次编辑
if hasattr(self, 'isContentSaved') and self.isContentSaved:
logger.warning("内容已保存,不允许再次编辑")
self.placeholderLabel.setText('''<div style='text-align:center; padding:20px;'>
<h3>已经保存</h3>
<p>该内容已经保存,不再允许编辑</p>
</div>''')
return
logger.info(f"文本文件加载成功,原始内容长度: {len(content)}字符")
self.loadingCard.hide()
self.saveButton.setEnabled(True)
# 保存原始内容
self.editorContent = content
# 检测语言类型
language = self._detect_language(content)
# 创建临时文件用于保存内容和语言信息
import tempfile
import base64
# 创建临时文件
fd, self.tempFilePath = tempfile.mkstemp(suffix='.json', text=True)
os.close(fd)
# 对内容进行Base64编码
encoded_content = base64.b64encode(content.encode('utf-8')).decode('ascii')
# 保存到临时文件
import json
with open(self.tempFilePath, 'w', encoding='utf-8') as f:
json.dump({
'content': encoded_content,
'language': language,
'file_id': self._id,
'saved': False # 初始状态为未保存
}, f)
# 获取编辑器HTML文件的绝对路径
editor_abs_path = os.path.abspath(self.editor_index)
# 构建URL参数
params = {
'temp_file': self.tempFilePath,
'content': encoded_content,
'language': language
}
# 导入必要的模块来创建本地Web服务器
import http.server
import socketserver
import threading
import sys
# 创建本地Web服务器类
class SimpleHTTPRequestHandler(http.server.SimpleHTTPRequestHandler):
def log_message(self, format, *args):
# 禁用服务器日志输出
pass
# 获取_internal目录的路径
# 当前文件路径是app/view/widgets/preview_box.py需要找到项目根目录
current_file = os.path.abspath(__file__)
# 从当前文件路径向上找项目根目录跳过app目录
project_root = os.path.dirname(os.path.dirname(os.path.dirname(current_file)))
internal_dir = os.path.join(project_root, "_internal")
logger.info(f"项目根目录: {project_root}")
logger.info(f"_internal目录: {internal_dir}")
# 启动本地Web服务器
port = 36852
handler = SimpleHTTPRequestHandler
# 创建一个允许地址重用的TCPServer类
class ReuseTCPServer(socketserver.TCPServer):
allow_reuse_address = True
# 自定义HTTP请求处理器用于处理POST保存请求
class EditorHTTPRequestHandler(http.server.SimpleHTTPRequestHandler):
# 保存对PreviewTextBox实例的引用
preview_box_instance = None
def do_POST(self):
"""处理POST请求特别是保存请求"""
if self.path == '/save':
# 获取请求体长度
content_length = int(self.headers['Content-Length'])
# 读取请求体数据
post_data = self.rfile.read(content_length)
try:
# 解析JSON数据
import json
save_data = json.loads(post_data)
# 记录保存请求
logger.info("接收到来自浏览器的保存请求")
# 使用preview_box_instance处理保存的数据
if EditorHTTPRequestHandler.preview_box_instance:
EditorHTTPRequestHandler.preview_box_instance._processSavedContent(save_data)
# 返回成功响应
self.send_response(200)
self.send_header('Content-Type', 'application/json')
self.end_headers()
self.wfile.write(json.dumps({'status': 'success'}).encode('utf-8'))
except Exception as e:
logger.error(f"处理保存请求失败: {e}")
# 返回错误响应
self.send_response(500)
self.send_header('Content-Type', 'application/json')
self.end_headers()
self.wfile.write(json.dumps({'status': 'error', 'message': str(e)}).encode('utf-8'))
else:
# 对于其他POST请求返回404
self.send_response(404)
self.end_headers()
def log_message(self, format, *args):
"""禁用日志消息,保持安静"""
return
# 尝试使用多个备用端口,避免端口被占用
max_attempts = 5
attempts = 0
while attempts < max_attempts:
try:
# 设置处理器的实例引用
EditorHTTPRequestHandler.preview_box_instance = self
# 使用自定义处理器创建服务器
self.httpd = ReuseTCPServer(("127.0.0.1", port), EditorHTTPRequestHandler)
logger.info(f"Web服务器成功在端口 {port} 启动")
break
except OSError as e:
attempts += 1
logger.warning(f"端口 {port} 被占用,尝试使用备用端口")
port += 1 # 使用下一个端口
if attempts >= max_attempts:
logger.error(f"无法找到可用端口,最后一个错误: {e}")
raise
# 切换工作目录到_internal目录
if os.path.exists(internal_dir):
os.chdir(internal_dir)
logger.info(f"切换Web服务器工作目录到: {internal_dir}")
else:
logger.error(f"_internal目录不存在: {internal_dir}")
# 如果目录不存在,保持在当前目录运行服务器
# 在后台线程中运行服务器
self.server_thread = threading.Thread(target=self.httpd.serve_forever, daemon=True)
self.server_thread.start()
# 构建基于HTTP的URL
from urllib.parse import urlencode
from PyQt6.QtGui import QDesktopServices
# 根据目录是否存在来调整HTML路径
if os.path.exists(internal_dir):
# 如果在_internal目录下运行服务器使用相对路径
html_path = "editor_main/index.html"
else:
# 如果不在_internal目录下使用完整路径
html_path = "_internal/editor_main/index.html"
url = f'http://127.0.0.1:{port}/{html_path}?{urlencode(params)}'
# 使用系统默认浏览器打开URL
logger.info(f"在外部浏览器中打开编辑器(HTTP): {url}")
QDesktopServices.openUrl(QUrl(url))
# 显示保存提醒
self.placeholderLabel.setText('''<div style='text-align:center; padding:20px;'>
<h3>文本编辑已在外部浏览器中打开</h3>
<p><strong>重要提示:</strong>完成编辑后,请点击浏览器中的"保存并返回"按钮</p>
<p>应用正在自动检测...</p>
</div>''')
# 启动轮询检查
self.pollingTimer.start()
def _checkBrowserContent(self):
"""检查浏览器是否已保存内容"""
# 首先尝试从localStorage读取
try:
import json
import os
import base64
# 尝试获取localStorage数据
# 在Windows上localStorage通常存储在用户的AppData目录中
# 由于直接访问localStorage有困难我们使用一个更可靠的方法
# 1. 首先检查临时文件
if self.tempFilePath and os.path.exists(self.tempFilePath):
try:
with open(self.tempFilePath, 'r', encoding='utf-8') as f:
data = json.load(f)
if data.get('saved', False):
logger.info("检测到浏览器已保存内容(通过临时文件)")
self._processSavedContent(data)
return
except Exception as e:
logger.error(f"读取临时文件失败: {e}")
# 2. 作为备用方案,检查是否有特定的保存文件
# 这个文件可以由浏览器通过特定的方法创建
import tempfile
app_data_dir = os.path.join(tempfile.gettempdir(), 'LeonPan')
os.makedirs(app_data_dir, exist_ok=True)
save_file_path = os.path.join(app_data_dir, 'editor_content.json')
if os.path.exists(save_file_path):
try:
with open(save_file_path, 'r', encoding='utf-8') as f:
data = json.load(f)
if data.get('saved', False):
logger.info("检测到浏览器已保存内容(通过备用文件)")
self._processSavedContent(data)
# 删除备用文件
os.unlink(save_file_path)
except Exception as e:
logger.error(f"读取备用保存文件失败: {e}")
except Exception as e:
logger.error(f"检查浏览器内容时出错: {e}")
def _processSavedContent(self, data):
"""处理已保存的内容"""
# 停止轮询
self.pollingTimer.stop()
# 更新内容
import base64
encoded_content = data.get('content', '')
try:
self.editorContent = base64.b64decode(encoded_content).decode('utf-8')
except Exception as e:
logger.error(f"解码内容失败: {e}")
return
# 标记内容已保存
self.isContentSaved = True
# 更新占位符文本,显示已保存信息
self.placeholderLabel.setText('''<div style='text-align:center; padding:20px;'>
<h3>已经保存</h3>
<p>内容已从浏览器同步到应用并已保存</p>
<p>该内容将不再允许编辑</p>
</div>''')
# 禁用保存按钮
self.saveButton.setEnabled(False)
# 清理临时文件
if self.tempFilePath and os.path.exists(self.tempFilePath):
try:
os.unlink(self.tempFilePath)
self.tempFilePath = None
except Exception as e:
logger.error(f"删除临时文件失败: {e}")
def handleError(self, error_msg):
"""处理加载错误"""
logger.error(f"文本预览失败URL: {self.url}, 错误: {error_msg}")
self.loadingCard.error()
def _initMonacoEditor(self, content):
"""初始化Monaco Editor并加载内容"""
# 获取编辑器文件的绝对路径
editor_dir = QDir("_internal/editor")
editor_path = editor_dir.absoluteFilePath("min/vs/editor/editor.main.js")
editor_file = QFileInfo(editor_path)
base_url = QUrl.fromLocalFile(editor_file.absolutePath())
# 检测文本语言
language = self._detect_language(content)
logger.info(f"检测到文本语言类型: {language}")
# 构建HTML内容使用ES模块方式加载Monaco Editor
html_content = f'''
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
body, html {{
margin: 0;
padding: 0;
width: 100%;
height: 100%;
overflow: hidden;
font-family: 'Microsoft YaHei', Arial, sans-serif;
}}
#container {{
width: 100%;
height: 100%;
}}
</style>
<script src="vs/loader.js"></script>
<script>
let editor;
require.config({{ paths: {{ 'vs': './vs' }} }});
require(['vs/editor/editor.main'], function() {{
editor = monaco.editor.create(document.getElementById('container'), {{
value: `{content.replace('`', '\\`')}`,
}});
// 重新获取编辑器实例以确保正确初始化
setTimeout(() => {{
editor = monaco.editor.getModels()[0] ? monaco.editor.getModels()[0].getContainerInfo().domNode.monacoEditor : null;
}}, 100);'\\`'}}`,
language: '{language}',
theme: 'vs-dark',
automaticLayout: true,
minimap: {{ enabled: true }},
scrollBeyondLastLine: false,
fontSize: 14,
wordWrap: 'on',
lineNumbers: 'on',
readOnly: false,
fontFamily: 'Consolas, "Microsoft YaHei", monospace'
}});
// 添加内容变化监听器
editor.onDidChangeModelContent(function() {{
// 可以在这里添加内容变化的处理逻辑
}});
}});
// 暴露获取内容的方法给QWebEngineView调用
function getEditorContent() {{
return editor ? editor.getValue() : '';
}}
// 暴露设置内容的方法
function setEditorContent(content) {{
if (editor) {{
editor.setValue(content);
}}
}}
</script>
</head>
<body>
<div id="container"></div>
</body>
</html>
'''
# 由于我们已经切换到外部浏览器编辑模式这里不再需要加载HTML内容到textEdit
# self.textEdit.setHtml(html_content, base_url) - 已注释因为textEdit不再存在
def _detect_language(self, content):
"""检测文本语言类型"""
content_lower = content.lower()
if 'public class' in content_lower and ('import ' in content_lower or 'package ' in content_lower):
return 'java'
elif 'def ' in content_lower and ('import ' in content_lower or 'print(' in content_lower):
return 'python'
elif '<!doctype html>' in content_lower or '<html>' in content_lower:
return 'html'
elif '{' in content_lower and '}' in content_lower and ':' in content_lower:
return 'json'
elif '{' in content_lower and '}' in content_lower and ('function' in content_lower or 'const ' in content_lower):
return 'javascript'
elif '<?xml' in content_lower:
return 'xml'
elif '<?php' in content_lower:
return 'php'
elif 'class ' in content_lower and '{' in content_lower and '}' in content_lower and ';' in content_lower:
return 'cpp'
elif 'select ' in content_lower and 'from ' in content_lower:
return 'sql'
elif '#include' in content_lower:
return 'cpp'
elif '<script' in content_lower:
return 'html'
elif 'function' in content_lower and '{' in content_lower and '}' in content_lower:
return 'javascript'
elif 'namespace' in content_lower and '{' in content_lower and '}' in content_lower:
return 'cpp'
elif 'var ' in content_lower and '=' in content_lower:
return 'javascript'
elif 'using namespace' in content_lower:
return 'cpp'
elif 'struct ' in content_lower and '{' in content_lower:
return 'cpp'
elif '#' in content_lower and 'define' in content_lower:
return 'cpp'
else:
return 'text'
# JavaScript执行完成的处理已移除
def _saveContent(self, content):
"""保存编辑器内容并提交修改"""
logger.info(f"保存文本文件修改文件ID: {self._id}")
self.saveTextThread = UpdateFileContentThread(
self._id,
content,
)
self.saveTextThread.successUpdated.connect(self._successSave)
self.saveTextThread.errorUpdated.connect(self._errorSave)
self.saveTextThread.start()
def getTextContent(self):
"""获取编辑器内容"""
return self.editorContent
def resizeEvent(self, event):
"""重写窗口大小改变事件"""
super().resizeEvent(event)
def closeEvent(self, event):
"""关闭事件清理临时文件、定时器和Web服务器"""
# 停止轮询定时器
if hasattr(self, 'pollingTimer'):
self.pollingTimer.stop()
logger.info("轮询定时器已停止")
# 停止Web服务器
if hasattr(self, 'httpd'):
try:
logger.info("正在停止本地Web服务器...")
self.httpd.shutdown()
self.httpd.server_close()
logger.info("本地Web服务器已成功停止")
except Exception as e:
logger.error(f"停止Web服务器失败: {e}")
# 清理临时文件
if hasattr(self, 'tempFilePath') and os.path.exists(self.tempFilePath):
try:
os.unlink(self.tempFilePath)
logger.info(f"临时文件已清理: {self.tempFilePath}")
except Exception as e:
logger.error(f"清理临时文件失败: {e}")
# 调用父类方法并接受事件
super().closeEvent(event)
event.accept()