From cbb80988c2567c11a9ce9432557d4b56307d8294 Mon Sep 17 00:00:00 2001 From: David Tejada Date: Fri, 27 Dec 2024 12:01:39 +0100 Subject: [PATCH] Implement commands to locate element by drawing a pattern on it (#443) * Implement mouse movement (wip) * Finish implementation of drawing and removing locate pattern on element --- .xo-config.json | 1 + src/background/commands/commandListeners.ts | 18 ++++- .../commands/handleIncomingCommand.ts | 4 +- src/content/actions/locatePattern.ts | 67 +++++++++++++++++++ src/content/dom/utils.ts | 5 +- src/content/messaging/messageListeners.ts | 13 ++++ src/typings/Action.ts | 5 ++ src/typings/ProtocolMap.ts | 5 ++ src/typings/TalonAction.ts | 2 +- 9 files changed, 113 insertions(+), 7 deletions(-) create mode 100644 src/content/actions/locatePattern.ts diff --git a/.xo-config.json b/.xo-config.json index d7fcbf12..5567c256 100644 --- a/.xo-config.json +++ b/.xo-config.json @@ -14,6 +14,7 @@ "rules": { "n/prefer-global/process": "off", "unicorn/prefer-top-level-await": "off", + "capitalized-comments": "off", "n/file-extension-in-import": "off", "import/extensions": [ 2, diff --git a/src/background/commands/commandListeners.ts b/src/background/commands/commandListeners.ts index 936cb413..6a05e12b 100644 --- a/src/background/commands/commandListeners.ts +++ b/src/background/commands/commandListeners.ts @@ -339,6 +339,18 @@ export function addCommandListeners() { } }); + onCommand("drawLocatePattern", async ({ target, colors }) => { + assertPrimitiveTarget(target); + await sendMessageToTargetFrames("drawLocatePattern", { + target, + colors, + }); + }); + + onCommand("removeLocatePattern", async () => { + await sendMessageToAllFrames("removeLocatePattern"); + }); + onCommand("copyElementTextContent", async ({ target }) => { const { results } = await sendMessageToTargetFrames( "getElementTextContent", @@ -383,7 +395,7 @@ export function addCommandListeners() { const message = `Command "focusAndDeleteContents" has been removed. Update rango-talon.`; await notify.error(message); - return { name: "printError", message }; + return { name: "throwError", message }; }); onCommand("focusElement", async ({ target }) => { @@ -413,7 +425,7 @@ export function addCommandListeners() { const message = `Command "insertToField" has been removed. Update rango-talon.`; await notify.error(message); - return { name: "printError", message }; + return { name: "throwError", message }; }); onCommand("openInBackgroundTab", async ({ target }) => { @@ -668,7 +680,7 @@ export function addCommandListeners() { const message = `Command "checkActiveElementIsEditable" has been removed. Update rango-talon.`; await notify.error(message); - return { name: "printError", message }; + return { name: "throwError", message }; }); onCommand("requestTimedOut", async () => { diff --git a/src/background/commands/handleIncomingCommand.ts b/src/background/commands/handleIncomingCommand.ts index 27643778..3304a913 100644 --- a/src/background/commands/handleIncomingCommand.ts +++ b/src/background/commands/handleIncomingCommand.ts @@ -22,12 +22,12 @@ export async function handleIncomingCommand() { // Rethrow the error with a more descriptive message. const message = "Unable to run command. This command can't run on browser system pages (settings, new tabs, extensions, or other privileged pages)."; - await writeResponse({ name: "printError", message }); + await writeResponse({ name: "throwError", message }); throw new UnreachableContentScriptError(message); } if (error instanceof Error) { - await writeResponse({ name: "printError", message: error.message }); + await writeResponse({ name: "throwError", message: error.message }); } throw error; diff --git a/src/content/actions/locatePattern.ts b/src/content/actions/locatePattern.ts new file mode 100644 index 00000000..6a6c75bd --- /dev/null +++ b/src/content/actions/locatePattern.ts @@ -0,0 +1,67 @@ +import { setStyleProperties } from "../dom/setStyleProperties"; +import { createElement } from "../dom/utils"; +import { type ElementWrapper } from "../wrappers/ElementWrapper"; + +const squareSize = 3; + +export function drawLocatePattern( + wrapper: ElementWrapper, + colors: [number, number, number, number] +) { + const rect = wrapper.element.getClientRects()[0]!; + const { top, left, width, height } = + rect.width > 0 && rect.height > 0 + ? rect + : wrapper.element.getBoundingClientRect(); + + const canvas: HTMLCanvasElement = + document.querySelector("#rangoCanvas") ?? + createElement("canvas", { + id: "rangoCanvas", + width: squareSize * 2, + height: squareSize * 2, + }); + + setStyleProperties(canvas, { + position: "fixed", + width: `${(squareSize * 2) / window.devicePixelRatio}px`, + height: `${(squareSize * 2) / window.devicePixelRatio}px`, + top: `${top + height / 2}px`, + left: `${left + width / 2}px`, + "z-index": "2147483647", + "pointer-events": "none", + "image-rendering": "pixelated", + }); + + const ctx = canvas.getContext("2d")!; + + draw4x4Pattern(ctx, colors); + + document.body.append(canvas); + + setTimeout(() => { + canvas.remove(); + }, 1000); +} + +export function removeLocatePattern() { + document.querySelector("#rangoCanvas")?.remove(); +} + +function draw4x4Pattern( + ctx: CanvasRenderingContext2D, + colors: [number, number, number, number] +) { + const hexColors = colors.map( + (n: number) => "#" + n.toString(16).padStart(6, "0") + ) as [string, string, string, string]; + + ctx.fillStyle = hexColors[0]; + ctx.fillRect(0, 0, squareSize, squareSize); + ctx.fillStyle = hexColors[1]; + ctx.fillRect(squareSize, 0, squareSize, squareSize); + ctx.fillStyle = hexColors[2]; + ctx.fillRect(0, squareSize, squareSize, squareSize); + ctx.fillStyle = hexColors[3]; + ctx.fillRect(squareSize, squareSize, squareSize, squareSize); +} diff --git a/src/content/dom/utils.ts b/src/content/dom/utils.ts index 4644a286..d2862560 100644 --- a/src/content/dom/utils.ts +++ b/src/content/dom/utils.ts @@ -67,5 +67,8 @@ export function createElement( tag: K, attributes: Partial ) { - return Object.assign(document.createElement(tag), attributes); + return Object.assign( + document.createElement(tag), + attributes + ) as HTMLElementTagNameMap[K]; } diff --git a/src/content/messaging/messageListeners.ts b/src/content/messaging/messageListeners.ts index b911984b..39d2165c 100644 --- a/src/content/messaging/messageListeners.ts +++ b/src/content/messaging/messageListeners.ts @@ -16,6 +16,10 @@ import { markHintsAsKeyboardReachable, restoreKeyboardReachableHints, } from "../actions/keyboardClicking"; +import { + drawLocatePattern, + removeLocatePattern, +} from "../actions/locatePattern"; import { matchElementByText } from "../actions/matchElementByText"; import { navigateToNextPage, @@ -132,6 +136,15 @@ export function addMessageListeners() { return clickElement(wrappers); }); + onMessage("drawLocatePattern", async ({ target, colors }) => { + const wrapper = await getFirstWrapper(target); + drawLocatePattern(wrapper, colors); + }); + + onMessage("removeLocatePattern", async () => { + removeLocatePattern(); + }); + onMessage("getElementTextContent", async ({ target }) => { const wrappers = await getTargetedWrappers(target); return getElementTextContent(wrappers); diff --git a/src/typings/Action.ts b/src/typings/Action.ts index 6213fdf7..ad0258df 100644 --- a/src/typings/Action.ts +++ b/src/typings/Action.ts @@ -76,6 +76,11 @@ export type ActionMap = { copyLink: { target: Target }; copyMarkdownLink: { target: Target }; directClickElement: { target: Target }; + drawLocatePattern: { + target: Target; + colors: [number, number, number, number]; + }; + removeLocatePattern: void; focusAndDeleteContents: void; focusElement: { target: Target }; focusFirstInput: void; diff --git a/src/typings/ProtocolMap.ts b/src/typings/ProtocolMap.ts index e1c2295f..5d36cfdf 100644 --- a/src/typings/ProtocolMap.ts +++ b/src/typings/ProtocolMap.ts @@ -79,6 +79,11 @@ export type ContentBoundMessageMap = { unhoverAll: () => void; setSelectionBefore: (data: { target: Target }) => boolean; setSelectionAfter: (data: { target: Target }) => boolean; + drawLocatePattern: (data: { + target: Target; + colors: [number, number, number, number]; + }) => void; + removeLocatePattern: () => void; // Scroll scroll: (data: { diff --git a/src/typings/TalonAction.ts b/src/typings/TalonAction.ts index 66e1183e..36a7b382 100644 --- a/src/typings/TalonAction.ts +++ b/src/typings/TalonAction.ts @@ -9,7 +9,7 @@ export type TalonAction = | TalonActionBase<"focusPageAndResend"> | TalonActionBase<"key", { key: string }> | TalonActionBase<"openInNewTab", { url: string }> - | TalonActionBase<"printError", { message: string }> + | TalonActionBase<"throwError", { message: string }> | TalonActionBase<"responseValue", { value: any }> | TalonActionBase<"sleep", { ms?: number }> | TalonActionBase<"typeTargetCharacters">;