Transform Macros to different Window setups

Hi,
I've got a huge Macro Library with lots of macros that use the absolute mouse postion.
Now I have to work in. another setup with different monitor resolutions.
Is there any way to 'transform' my macros to the new setup?
Cheers,
Simon

There is (generally) no automated way to edit all your macros automatically. You will probably have to edit each macro manually.

Even if you are using “absolute” location values, those values don’t need to be constants, they can be expressions. How would an automated process handle expressions? It couldn’t.

In the future you can write your code in such a way that changing monitor resolution doesn’t break your macros. Do you want learn how to do that?

1 Like

Thanks Airy,
Yeah,I derinitely would like to know that!

Two separate problems there, perhaps:

  1. Mapping points from the old resolution to corresponding points in the new resolution.
  2. Replacing numeric values in the HorizontalPositionExpression and VerticalPositionExpression of mouse actions with variable names like local_MouseX and local_MouseY

The mapping is just vector arithmetic – within the scope of a simple calculation if you supply:

  • the old screen origin and dimensions
  • the new screen origin and dimensions
  • the old XY point

The replacement, in selected mouse action definitions, of key-value pairs like:

<key>HorizontalPositionExpression</key>
<string>450</string>

<key>VerticalPositionExpression</key>
<string>500</string>

with corresponding pairs which use variable names, like:

<key>HorizontalPositionExpression</key>
<string>local_MouseX</string>

<key>VerticalPositionExpression</key>
<string>local_MouseY</string>

Could be done with a Keyboard Maestro macro which uses an Execute JavaScript for Automation action.

1 Like

Very interesting.Thanks for this.
But for my understanding: This macro would more or less ‚translate‘ the old absolute position coordinates into vector-based variables?

Well, you could draft a macro, which, if you selected an action like:

would paste a transformed copy just after it – perhaps something like:

which aims for screen frame independence by using X and Y positions expressed as proportions of screen width and screen height respectively.

The macro would need an initial parameter specifying the XYWH (origin X, origin Y, width, height) frame size of the screen for which the absolute values were originally intended.

If your original screen had the frame (as reported by the %Screen%0% token, for example)

0,0,1728,1117

then the macro might be set up in something like this form:

Copy of absolute mouse action using proportions of width and height.kmmacros (9.7 KB)


Expand disclosure triangle to view JS source
return (() => {
    "use strict";

    const main = () => {
        const
            selection = Application("Keyboard Maestro").selection(),
            lrXYWH = xywhValuesLR(kmvar.local_OriginalXYWH);

        return either(
            alert("Relative version of absolute mouse positioning")
        )(
            xml => xml
        )(
            bindLR(
                0 < selection.length
                    ? Right(selection[0])
                    : Left("Nothing selected in Keyboard Maestro")
            )(
                maybeAction => bindLR(lrXYWH)(
                    xywh => bindLR(
                        "action" === maybeAction.properties().pcls
                            ? Right(maybeAction.xml())
                            : Left("First selected item in KM is not an action.")
                    )(
                        xml => bindLR(
                            jsoFromPlistStringLR(xml)
                        )(
                            action => "MouseMoveAndClick" === action.MacroActionType
                                ? "Absolute" === action.Relative
                                    ? bindLR(
                                        relativeVersionLR(xywh)(action)
                                    )(
                                        plistFromJSOLR
                                    )
                                    : Left("Only applies to absolute mouse actions.")
                                : Left("Only applies to mouse actions.")
                        )
                    )
                )
            )
        );
    };


    // xywhValuesLR :: String -> Either String (Int, Int, Int, Int)
    const xywhValuesLR = frameString => {
        const
            xywh = frameString.split(",").flatMap(s => {
                const maybeN = Number(s);

                return isNaN(maybeN)
                    ? []
                    : [maybeN]
            });

        return 4 === xywh.length
            ? Right(xywh)
            : Left("XYWH frame should be 4 comma-separated integers.")
    };


    // "VerticalPositionExpression": "SCREEN(0, height) * 0.75",
    // "HorizontalPositionExpression": "SCREEN(0, width) * 0.25",

    // relativeVersionLR  (Int, Int, Int, Int) -> Dict -> Either String Dict
    const relativeVersionLR = ([x, y, w, h]) =>
        action => {
            const
                xExpr = Number(action.HorizontalPositionExpression),
                yExpr = Number(action.VerticalPositionExpression);

            return bindLR(
                isNaN(xExpr)
                    ? Left(
                        [
                            "Expected simple numeric x-coordinate expression,",
                            `saw: "${action.HorizontalPositionExpression}"`
                        ]
                            .join("\n")
                    )
                    : Right((xExpr - x) / w)
            )(
                rx => fmapLR(ry => ({
                    ...action,
                    ["HorizontalPositionExpression"]: `SCREEN(0, width) * ${rx}`,
                    ["VerticalPositionExpression"]: `SCREEN(0, height) * ${ry}`

                }))(
                    isNaN(yExpr)
                        ? Left(
                            [
                                "Expected simple numeric y-coordinate expression,",
                                `saw: "${action.VerticalPositionExpression}"`
                            ]
                                .join("\n")
                        )
                        : Right((yExpr - y) / h)
                )
            )
        }


    // ----------------------- JXA -----------------------

    // alert :: String => String -> IO String
    const alert = title =>
        s => {
            const sa = Object.assign(
                Application("System Events"), {
                includeStandardAdditions: true
            });

            return (
                sa.activate(),
                sa.displayDialog(s, {
                    withTitle: title,
                    buttons: ["OK"],
                    defaultButton: "OK"
                }),
                s
            );
        };

    // jsoFromPlistStringLR :: XML String -> Either String Dict
    const jsoFromPlistStringLR = xml => {
        // Either an explanatory message, or a
        // JS dictionary parsed from the plist XML
        const
            e = $(),
            nsDict = $.NSPropertyListSerialization
                .propertyListWithDataOptionsFormatError(
                    $(xml).dataUsingEncoding(
                        $.NSUTF8StringEncoding
                    ),
                    0, 0, e
                );

        return nsDict.isNil()
            ? Left(
                ObjC.unwrap(
                    e.localizedDescription
                )
            )
            : Right(ObjC.deepUnwrap(nsDict));
    };

    // plistFromJSOLR :: JS Object -> Either String XML
    const plistFromJSOLR = jso => {
        const
            e = $(),
            nsXML = $.NSString.alloc.initWithDataEncoding(
                $.NSPropertyListSerialization
                    .dataWithPropertyListFormatOptionsError(
                        $(jso),
                        $.NSPropertyListXMLFormat_v1_0, 0,
                        e
                    ),
                $.NSUTF8StringEncoding
            );

        return nsXML.isNil()
            ? Left(e.localizedDescription)
            : Right(
                ObjC.unwrap(nsXML)
            );
    };

    // --------------------- GENERIC ---------------------

    // Left :: a -> Either a b
    const Left = x => ({
        type: "Either",
        Left: x
    });


    // Right :: b -> Either a b
    const Right = x => ({
        type: "Either",
        Right: x
    });


    // adjust :: (a -> a) -> Key ->
    // Dict Key a -> Dict Key a
    const adjust = f =>
        // The orginal dictionary, unmodified, if k is
        // not an existing key.
        // Otherwise, a new copy in which the existing
        // value of k is updated by application of f.
        k => dict => k in dict
            ? {
                ...dict,
                [k]: f(dict[k])
            }
            : dict;


    // bindLR (>>=) :: Either a ->
    // (a -> Either b) -> Either b
    const bindLR = lr =>
        // Bind operator for the Either option type.
        // If lr has a Left value then lr unchanged,
        // otherwise the function mf applied to the
        // Right value in lr.
        mf => "Left" in lr
            ? lr
            : mf(lr.Right);


    // either :: (a -> c) -> (b -> c) -> Either a b -> c
    const either = fl =>
        // Application of the function fl to the
        // contents of any Left value in e, or
        // the application of fr to its Right value.
        fr => e => "Left" in e
            ? fl(e.Left)
            : fr(e.Right);


    // fmapLR (<$>) :: (b -> c) -> Either a b -> Either a c
    const fmapLR = f =>
        // Either f mapped into the contents of any Right
        // value in e, or e unchanged if is a Left value.
        e => "Left" in e
            ? e
            : Right(f(e.Right));


    // MAIN ---
    return main();
})();

Okay. Just use screen percentages instead of absolute coordinates. ComplexPoint is showing you one way to do that. Although I would have implemented it differently, using a subroutine. Like this:

If you use this macro to move the mouse on the screen, then it will always move to the same "position" on the screen even if you replace the screen or change the screen's resolution.

However keep in mind that multi-monitor Macs might complicate things a bit. I'm assuming you have only one display at all times.

Using subroutines can be really beneficial at times. Even for some of the simplest KM actions, I often create a subroutine for it, so if I want to change the behaviour of the action, it's easy. For example, there's a KM action called "Speak Text", and I always put that action into a subroutine. By doing that I can change some options of the action, like the Voice of the spoken text, without having to manually edit hundreds of "Speak Text" actions. A side benefit of doing this is that by wisely choosing my subroutine names I can more reliably using the KM Editor's Search feature, because I get FAR FEWER "false positive search results", since my subroutines generally have more unique names.

And another approach would be a macro which, when you select an absolute mouse action like:

and run the macro, pastes a transformed copy which contains (rather than a fully calculated proportion in the form of a floating point number with many digits) simply the formula based on the origins and dimensions of the orginal screen frame:

As in this draft:

Copy of Absolute Mouse Action Using Calculation Relative to Screen Dimensions.kmmacros (10.0 KB)

and as @Airy suggests, it you could also adjust the macro to eplace all the explicit numbers with a variable name (followed by an array index in the range [1] to [4], corresponding to the four values of a screen frame array) and paste not only the transformed action, but also a preceding Set Variable to Calculation action, binding a name to a comma-separated frame array of four integers in the pattern originX, originY, width, height.

And another approach would be a macro which, when you select an absolute mouse action like:

and run the macro, will paste a transformed copy which contains (rather than a fully calculated proportion in the form of a floating point number with many digits) simply the formula based on the origins and dimensions of the orginal screen frame:

As in this draft:

Copy of Absolute Mouse Action Using Calculation Relative to Screen Dimensions.kmmacros (10 KB)


Expand disclosure triangle to view JS source
return (() => {
    "use strict";

    const main = () => {
        const
            selection = Application("Keyboard Maestro").selection(),
            lrXYWH = xywhValuesLR(kmvar.local_OriginalXYWH);

        return either(
            alert("Relative version of absolute mouse positioning")
        )(
            xml => xml
        )(
            bindLR(
                0 < selection.length
                    ? Right(selection[0])
                    : Left("Nothing selected in Keyboard Maestro")
            )(
                maybeAction => bindLR(lrXYWH)(
                    xywh => bindLR(
                        "action" === maybeAction.properties().pcls
                            ? Right(maybeAction.xml())
                            : Left("First selected item in KM is not an action.")
                    )(
                        xml => bindLR(
                            jsoFromPlistStringLR(xml)
                        )(
                            action => "MouseMoveAndClick" === action.MacroActionType
                                ? "Absolute" === action.Relative
                                    ? bindLR(
                                        relativeVersionLR(xywh)(action)
                                    )(
                                        plistFromJSOLR
                                    )
                                    : Left("Only applies to absolute mouse actions.")
                                : Left("Only applies to mouse actions.")
                        )
                    )
                )
            )
        );
    };


    // xywhValuesLR :: String -> Either String (Int, Int, Int, Int)
    const xywhValuesLR = frameString => {
        const
            xywh = frameString.split(",").flatMap(s => {
                const maybeN = Number(s);

                return isNaN(maybeN)
                    ? []
                    : [maybeN]
            });

        return 4 === xywh.length
            ? Right(xywh)
            : Left("XYWH frame should be 4 comma-separated integers.")
    };


    // "VerticalPositionExpression": "SCREEN(0, height) * 0.75",
    // "HorizontalPositionExpression": "SCREEN(0, width) * 0.25",

    // relativeVersionLR  (Int, Int, Int, Int) -> Dict -> Either String Dict
    const relativeVersionLR = ([ox, oy, w, h]) =>
        action => {
            const
                nx = Number(action.HorizontalPositionExpression),
                ny = Number(action.VerticalPositionExpression);

            return bindLR(
                isNaN(nx)
                    ? Left(
                        [
                            "Expected simple numeric x-coordinate expression,",
                            `saw: "${action.HorizontalPositionExpression}"`
                        ]
                            .join("\n")
                    )
                    : Right([ox, w, nx])
            )(
                ([xOrigin, width, x]) => fmapLR(([yOrigin, height, y]) => {
                    const
                        xCalc = `((${x} - ${xOrigin}) / ${width})`,
                        yCalc = `((${y} - ${yOrigin}) / ${height})`;

                    return ({
                        ...action,
                        ["ActionName"]: "Move Mouse to Position Relative to Screen Width and Height",
                        ["HorizontalPositionExpression"]: `SCREEN(0, width) * ${xCalc} + SCREEN(0, left)`,
                        ["VerticalPositionExpression"]: `SCREEN(0, height) * ${yCalc} + SCREEN(0, top)`

                    });
                })(
                    isNaN(ny)
                        ? Left(
                            [
                                "Expected simple numeric y-coordinate expression,",
                                `saw: "${action.VerticalPositionExpression}"`
                            ]
                                .join("\n")
                        )
                        : Right([oy, h, ny])
                )
            )
        }


    // ----------------------- JXA -----------------------

    // alert :: String => String -> IO String
    const alert = title =>
        s => {
            const sa = Object.assign(
                Application("System Events"), {
                includeStandardAdditions: true
            });

            return (
                sa.activate(),
                sa.displayDialog(s, {
                    withTitle: title,
                    buttons: ["OK"],
                    defaultButton: "OK"
                }),
                s
            );
        };

    // jsoFromPlistStringLR :: XML String -> Either String Dict
    const jsoFromPlistStringLR = xml => {
        // Either an explanatory message, or a
        // JS dictionary parsed from the plist XML
        const
            e = $(),
            nsDict = $.NSPropertyListSerialization
                .propertyListWithDataOptionsFormatError(
                    $(xml).dataUsingEncoding(
                        $.NSUTF8StringEncoding
                    ),
                    0, 0, e
                );

        return nsDict.isNil()
            ? Left(
                ObjC.unwrap(
                    e.localizedDescription
                )
            )
            : Right(ObjC.deepUnwrap(nsDict));
    };

    // plistFromJSOLR :: JS Object -> Either String XML
    const plistFromJSOLR = jso => {
        const
            e = $(),
            nsXML = $.NSString.alloc.initWithDataEncoding(
                $.NSPropertyListSerialization
                    .dataWithPropertyListFormatOptionsError(
                        $(jso),
                        $.NSPropertyListXMLFormat_v1_0, 0,
                        e
                    ),
                $.NSUTF8StringEncoding
            );

        return nsXML.isNil()
            ? Left(e.localizedDescription)
            : Right(
                ObjC.unwrap(nsXML)
            );
    };

    // --------------------- GENERIC ---------------------

    // Left :: a -> Either a b
    const Left = x => ({
        type: "Either",
        Left: x
    });


    // Right :: b -> Either a b
    const Right = x => ({
        type: "Either",
        Right: x
    });


    // adjust :: (a -> a) -> Key ->
    // Dict Key a -> Dict Key a
    const adjust = f =>
        // The orginal dictionary, unmodified, if k is
        // not an existing key.
        // Otherwise, a new copy in which the existing
        // value of k is updated by application of f.
        k => dict => k in dict
            ? {
                ...dict,
                [k]: f(dict[k])
            }
            : dict;


    // bindLR (>>=) :: Either a ->
    // (a -> Either b) -> Either b
    const bindLR = lr =>
        // Bind operator for the Either option type.
        // If lr has a Left value then lr unchanged,
        // otherwise the function mf applied to the
        // Right value in lr.
        mf => "Left" in lr
            ? lr
            : mf(lr.Right);


    // either :: (a -> c) -> (b -> c) -> Either a b -> c
    const either = fl =>
        // Application of the function fl to the
        // contents of any Left value in e, or
        // the application of fr to its Right value.
        fr => e => "Left" in e
            ? fl(e.Left)
            : fr(e.Right);


    // fmapLR (<$>) :: (b -> c) -> Either a b -> Either a c
    const fmapLR = f =>
        // Either f mapped into the contents of any Right
        // value in e, or e unchanged if is a Left value.
        e => "Left" in e
            ? e
            : Right(f(e.Right));


    // MAIN ---
    return main();
})();

Thank you gentlemen!
Very inspiring.Currently I'm in the middle of a hectic project but will try that and keep you posted!
Unfortunately I predominantly use two screens but I wonder whether this is really an issue when the two screens are equal in the vertical dimensions since I think KM is interpreting the horizontal dimensions as 'one screen' or am I mistaken?

The main point about a multi-screen setup is that pixel coordinates on at least one screen will not have their (0,0) origin at the top left of that screen.

That’s why translation from absolute to relative involves the whole (originX, originY, width, height) frame of each screen, and not just a (width, height) pair.

I see.
But when I define the origin of the second screen is 'resolution of the first screen + 1' it should work?

Look at the formulae in the macro I posted.

There are no simple constraints on the relative positions of main and additional screens.

They all share the same grid, with a single (0,0) origin, but the top-left pixel of additional screens can be anywhere around the main screen.

See: System Settings... > Displays > Arrange...

1 Like

Sorry for the late reply.

Your macro works like a charm.

Thank you