// ==UserScript==
// @name 嘉立创EDA专业版增强脚本
// @namespace http://tampermonkey.net/
// @version 1.4
// @description 对PC端嘉立创EDA专业版进行触摸适配,以及显示FPS等功能增强
// @author github@xiaowine
// @match https://pro.lceda.cn/editor?*
// @require https://cdn.jsdelivr.net/gh/hammerjs/hammer.js@ff687ea0daa3c806b9accd2ecb1a46165ea3c00a/hammer.min.js
// @grant GM.getValue
// @grant GM.setValue
// @grant GM.registerMenuCommand
// @run-at document-end
// @license GPL
// ==/UserScript==
// 配置管理类
class ConfigManager {
static DEFAULT_CONFIG = {
enablePan: true,
enablePinch: true,
enablePress: false,
showFPS: false, // 显示FPS
};
constructor () {
this.config = null;
this.listeners = new Set();
this.menuCommands = new Map(); // 存储菜单命令的引用
this.menuItems = [
{ key: 'enablePan', text: '启用触摸拖动' },
{ key: 'enablePinch', text: '启用触摸缩放' },
{ key: 'enablePress', text: '启用触摸长按' },
{ key: 'showFPS', text: '显示FPS' }
];
}
// 加载配置
async load () {
try {
const savedConfig = await GM.getValue('touchConfig', null);
this.config = savedConfig ? JSON.parse(savedConfig) : ConfigManager.DEFAULT_CONFIG;
} catch (error) {
console.error('加载配置失败:', error);
this.config = ConfigManager.DEFAULT_CONFIG;
}
return this.config;
}
// 保存配置
async save () {
await GM.setValue('touchConfig', JSON.stringify(this.config));
this.notifyListeners();
}
// 切换配置项
async toggleSetting (key) {
if (this._updating) return; // 防止重复触发
this.config[key] = !this.config[key];
await this.save();
await this.updateMenu(key);
}
// 获取配置
getConfig () {
return this.config;
}
// 注册(不可用)配置变更监听器
addChangeListener (listener) {
this.listeners.add(listener);
}
// 移除配置变更监听器
removeChangeListener (listener) {
this.listeners.delete(listener);
}
// 通知所有监听器
notifyListeners () {
this.listeners.forEach(listener => listener(this.config));
}
// 初始化菜单
async initMenu () {
// 确保先清理旧菜单
await this.unregisterAllMenus();
for (const { key, text } of this.menuItems) {
const command = await GM.registerMenuCommand(
`${this.config[key] ? '✅' : '❌'} ${text}`,
() => this.toggleSetting(key)
);
this.menuCommands.set(key, command);
}
}
// 更新菜单项显示
async updateMenu (key) {
// 防止重复更新
if (this._updating) return;
this._updating = true;
try {
const item = this.menuItems.find(item => item.key === key);
if (item) {
// 先注销所有旧的菜单命令
await this.unregisterAllMenus();
// 重新注册(不可用)所有菜单命令
await this.initMenu();
}
} finally {
this._updating = false;
}
}
// 注销所有菜单命令
async unregisterAllMenus () {
for (const command of this.menuCommands.values()) {
if (command) {
await GM.unregisterMenuCommand(command);
}
}
this.menuCommands.clear();
}
}
// TouchEventHandler类修改
class TouchEventHandler {
constructor (targetElement, configManager) {
this.targetElement = targetElement;
this.hammer = null;
this.scaleThreshold = 0.1;
this.scrollSensitivity = 0.1;
this.lastScale = 1;
this.configManager = configManager;
// 监听配置变更
this.configManager.addChangeListener(() => {
this.destroy();
this.init();
});
}
init () {
if (!this.targetElement) {
console.log("未找到目标元素");
return false;
}
console.log("目标元素已获取,初始化Hammer.js:", this.targetElement);
// 修改Hammer初始化方式
this.hammer = new Hammer.Manager(this.targetElement, {
touchAction: 'none',
inputClass: Hammer.TouchInput
});
// 添加识别器
this.hammer.add(new Hammer.Pan({ direction: Hammer.DIRECTION_ALL }));
this.hammer.add(new Hammer.Pinch());
this.hammer.add(new Hammer.Press({ time: 500 }));
this.addEventListeners();
return true;
}
getSVGCenter () {
const rect = this.targetElement.getBoundingClientRect();
return {
x: rect.left + rect.width / 2,
y: rect.top + rect.height / 2,
};
}
addEventListeners () {
console.log("添加Hammer.js事件监听");
const config = this.configManager.getConfig();
// 添加调试监听
this.hammer.on('hammer.input', (ev) => {
console.log('Hammer input:', ev.type, ev);
});
this.hammer.on('panstart', (ev) => {
console.log('Pan start triggered:', ev);
const mouseDownEvent = new MouseEvent("mousedown", {
bubbles: true,
cancelable: true,
button: 1,
clientX: ev.center.x,
clientY: ev.center.y
});
this.targetElement.dispatchEvent(mouseDownEvent);
});
this.hammer.on('panmove', (ev) => {
console.log('Pan move:', { x: ev.center.x, y: ev.center.y });
const mouseMoveEvent = new MouseEvent("mousemove", {
bubbles: true,
cancelable: true,
button: 1,
clientX: ev.center.x,
clientY: ev.center.y,
});
this.targetElement.dispatchEvent(mouseMoveEvent);
});
this.hammer.on('pinch', (ev) => {
console.log('Pinch:', { scale: ev.scale, lastScale: this.lastScale });
const scaleDiff = ev.scale - this.lastScale;
if (Math.abs(scaleDiff) > this.scaleThreshold) {
const center = this.getSVGCenter();
console.log('Pinch threshold reached:', { scaleDiff, center });
const scrollEvent = new WheelEvent("wheel", {
bubbles: true,
cancelable: true,
deltaY: scaleDiff > 0 ? -10 : 10,
clientX: center.x,
clientY: center.y,
});
this.targetElement.dispatchEvent(scrollEvent);
this.lastScale = ev.scale;
}
});
this.hammer.on('press', (ev) => {
const rightClickEvent = new MouseEvent("contextmenu", {
bubbles: true,
cancelable: true,
button: 2,
clientX: ev.center.x,
clientY: ev.center.y,
});
this.targetElement.parentNode.dispatchEvent(rightClickEvent);
});
}
destroy () {
if (this.hammer) {
console.log("正在移除Hammer.js事件监听");
this.hammer.destroy();
this.hammer = null;
}
console.log("Hammer.js事件监听已移除");
}
}
// UI增强处理类
class UIEnhancer {
constructor (configManager) {
console.log('UIEnhancer: Initializing...');
this.configManager = configManager;
this.fpsCounter = {
element: null,
frameCount: 0,
previousTimestamp: 0,
animationFrameHandle: null,
currentFps: 0,
interval: 1000,
originalText: '' // 保存原始用户名
};
this.configManager.addChangeListener(() => {
console.log('UIEnhancer: Config changed, updating UI');
this.updateUI();
});
}
init () {
console.log('UIEnhancer: Starting initialization');
this.findElements();
this.updateUI();
}
findElements () {
console.log('UIEnhancer: Finding username element');
// 查找用户名span元素
const usernameSpan = document.querySelector('#loginUsername span');
if (usernameSpan) {
console.log('UIEnhancer: Username element found');
this.fpsCounter.element = usernameSpan;
this.fpsCounter.originalText = usernameSpan.textContent;
} else {
console.log('UIEnhancer: Username element not found');
}
}
updateFps () {
if (!this.configManager.getConfig().showFPS) return;
const currentTimestamp = Date.now();
this.fpsCounter.frameCount++;
if (currentTimestamp > this.fpsCounter.interval + this.fpsCounter.previousTimestamp) {
this.fpsCounter.currentFps = Math.round(
(this.fpsCounter.frameCount * 1000) / (currentTimestamp - this.fpsCounter.previousTimestamp)
);
this.fpsCounter.frameCount = 0;
this.fpsCounter.previousTimestamp = currentTimestamp;
if (this.fpsCounter.element) {
console.log('UIEnhancer: FPS updated to', this.fpsCounter.currentFps);
this.fpsCounter.element.textContent = `FPS: ${this.fpsCounter.currentFps}`;
}
}
this.fpsCounter.animationFrameHandle = requestAnimationFrame(() => this.updateFps());
}
startFPSCounter () {
console.log('UIEnhancer: Starting FPS counter');
if (!this.fpsCounter.animationFrameHandle && this.fpsCounter.element) {
this.fpsCounter.frameCount = 0;
this.fpsCounter.previousTimestamp = Date.now();
this.updateFps();
}
}
stopFPSCounter () {
console.log('UIEnhancer: Stopping FPS counter');
if (this.fpsCounter.animationFrameHandle) {
cancelAnimationFrame(this.fpsCounter.animationFrameHandle);
this.fpsCounter.animationFrameHandle = null;
this.fpsCounter.currentFps = 0;
if (this.fpsCounter.element) {
this.fpsCounter.element.textContent = this.fpsCounter.originalText;
}
}
}
destroy () {
console.log('UIEnhancer: Destroying');
this.stopFPSCounter();
}
// 更新UI显示
updateUI () {
console.log('UIEnhancer: Updating UI');
const config = this.configManager.getConfig();
if (config.showFPS) {
this.startFPSCounter();
} else {
this.stopFPSCounter();
}
}
}
// 主程序入口
(async function () {
"use strict";
if (window.isScriptLoaded) {
return;
}
window.isScriptLoaded = true;
// 初始化配置管理器
const configManager = new ConfigManager();
await configManager.load();
await configManager.initMenu();
// 初始化UI增强
const uiEnhancer = new UIEnhancer(configManager);
uiEnhancer.init();
document.documentElement.style.touchAction = "none";
let touchHandler = null;
let lastUrl = "";
/**
* 移除现有的事件监听器
*/
function removeEventListeners () {
if (touchHandler) {
touchHandler.destroy();
touchHandler = null;
return true;
}
return false;
}
/**
* 初始化触摸事件处理
* @param {Element} element - 要监听触摸事件的目标元素
*/
function initTouchHandler (element) {
// 先移除现有的事件监听
removeEventListeners();
if (element) {
console.log("正在初始化触摸处理器,目标元素:", element);
touchHandler = new TouchEventHandler(element, configManager);
const success = touchHandler.init();
if (success) {
// 添加触摸调试信息
// element.addEventListener('touchstart', (e) => console.log('Native touchstart:', e), false);
console.log("触摸事件处理已初始化");
return true;
}
}
return false;
}
/**
* 解析 URL 中的 tab 参数并匹配 div 和 iframe
*/
function parseTabParams () {
const currentUrl = window.location.href;
// 如果 URL 没变,则不执行解析
if (currentUrl === lastUrl) {
console.log("URL 未变化,跳过执行");
return;
}
// 更新上次的 URL 记录
lastUrl = currentUrl;
console.log("检测到 URL 变化:", currentUrl);
// 解析 tab 参数
const hash = window.location.hash;
const tabMatch = hash.match(/tab=([^&#]*)/);
if (tabMatch) {
const tabList = tabMatch[1].split("|");
console.log("Tab 参数解析为列表:", tabList);
// 过滤出以 "*" 开头的项,并去除 "*"
const starredTabs = tabList
.filter((tab) => tab.startsWith("*"))
.map((tab) => tab.substring(1));
if (starredTabs.length > 0) {
console.log('以 "*" 开头的项:', starredTabs);
// 调用单独的函数解析 div 和 iframe
const rootElement = parseDivAndIframe(starredTabs);
if (rootElement) {
console.log("解析成功,初始化触摸事件处理");
initTouchHandler(rootElement);
} else {
console.log("未找到符合条件的元素,无法初始化触摸事件");
}
}
} else {
console.log("未找到 tab 参数");
}
}
/**
* 在 #tabbar_bodies 下查找 div 元素,并匹配 uuid
* 如果找到符合条件的 div,则继续查找其内部的 iframe,获取 #root 或 #canvas
* @param {Array} starredTabs - 以 "*" 开头的 tab 列表(去除了 "*")
* @returns {Element|boolean} 如果找到匹配的元素返回该元素,未找到返回 false
*/
function parseDivAndIframe (starredTabs) {
// 获取 #tabbar_bodies 下的所有 div
const divs = document.querySelectorAll("#tabbar_bodies div");
if (divs.length === 0) {
console.log("未找到任何 div");
return false;
}
// 遍历 div 并匹配 uuid
for (let div of divs) {
const uuid = div.getAttribute("uuid"); // 获取 uuid 属性
if (uuid && starredTabs.includes(uuid)) {
console.log("匹配的 div:", div);
// 查找 div 内的 iframe
const iframe = div.querySelector("iframe");
if (iframe) {
console.log("找到匹配的 iframe:", iframe);
// 尝试访问 iframe 的内容
try {
const iframeDoc =
iframe.contentDocument || iframe.contentWindow.document;
// 查找 #root 或 #canvas
let rootElement = iframeDoc.querySelector("#root");
if (!rootElement) {
rootElement = iframeDoc.querySelector("#canvas");
}
if (rootElement) {
console.log("找到目标元素:", rootElement);
return rootElement;
} else {
console.log("未找到 #root 或 #canvas");
}
} catch (error) {
console.error("无法访问 iframe 内容:", error);
}
} else {
console.log("匹配的 div 内未找到 iframe");
}
}
}
return false; // 如果没有找到匹配的 div 和 iframe
}
window.addEventListener("popstate", parseTabParams);
})();