Copy As TSV User Data (for OmniGraffle 7)

Copies selected OmniGraffle 7 shapes as tab-delimited Custom Data records, for pasting into Excel etc.

(Based on the Name and Custom Data keys and values exposed in the OmniGraffle Object Inspector)

Copy from OmniGraffle 7 as Tab-Delimited Object User Data of Shapes.kmmacros (24 KB)

JavaScript source
(() => {
    'use strict';

    // Convert any OmniGraffle shapes in the clipboard
    // to tab-delimited (Name +) User Data.
    // TSV lines containing User Data values
    // (as listed in the OmniGraffle Object Inspector)
    // are preceded by a TSV header with field names.

    // Rob Trew 2020
    // Ver 0.01

    ObjC.import('AppKit');

    // main :: IO ()
    const main = () =>
        either(
            msg => msg
        )(
            tsv => (
                copyText(tsv),
                'Tab-delimited user data now in clipboard.'
            )
        )(
            bindLR(
                ogGraphicTypeJSONFromPBoardLR()
            )(
                json => bindLR(
                    jsonParseLR(json)
                )(
                    dct => Right(tsvFromShapes(
                        dct.GraphicsList.filter(
                            g => 'ShapedGraphic' === g.Class
                        )
                    ))
                )
            )
        );

    //----------- TSV FROM OMNIGRAFFLE CLIPBOARD -----------

    // ogGraphicTypeJSONFromPBoardLR :: IO () ->
    // Either String String
    const ogGraphicTypeJSONFromPBoardLR = () => {
        const
            ogType = 'com.omnigroup.OmniGraffle.GraphicType',
            pBoard = $.NSPasteboard.generalPasteboard;
        return ObjC.deepUnwrap(
            pBoard.pasteboardItems.js[0].types
        ).includes(ogType) ? (
            Right(JSON.stringify(
                ObjC.deepUnwrap(
                    pBoard.propertyListForType(
                        ogType
                    )
                ),
                null, 2
            ))
        ) : Left('No OmniGraffle content in clipboard.')
    };

    // tsvFromShapes :: [Dict] -> String
    const tsvFromShapes = shapeDicts => {
        const
            userDicts = shapeDicts.flatMap(x => {
                const dctUser = x.UserInfo;
                return Boolean(dctUser) ? (
                    [dctUser]
                ) : [];
            }),
            userKeys = allKeysFromRecords(
                userDicts
            ),
            emptyRecord = userKeys.map(x => '');
        return unlines([
            'Name\t' + userKeys.join('\t'),
            ...shapeDicts.flatMap(x => {
                const
                    maybeName = x.Name,
                    maybeUser = x.UserInfo;
                return (
                    (maybeName || maybeUser) ? [
                        [maybeName || ''].concat(
                            maybeUser ? (
                                userKeys.map(
                                    k => maybeUser[k] || ''
                                )
                            ) : emptyRecord
                        ).join('\t')
                    ] : []
                );
            }).sort()
        ]);
    };

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

    // copyText :: String -> IO String
    const copyText = s => {
        // String copied to general pasteboard.
        const pb = $.NSPasteboard.generalPasteboard;
        return (
            pb.clearContents,
            pb.setStringForType(
                $(s),
                $.NSPasteboardTypeString
            ),
            s
        );
    };

    //----------------- GENERIC FUNCTIONS ------------------

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

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

    // allKeysFromRecords :: Dicts -> [String]
    const allKeysFromRecords = dicts =>
        Object.keys(
            dicts.flatMap(Object.keys)
            .reduce(
                (a, k) => Object.assign(a, {
                    [k]: true
                }), {}
            )
        ).sort();

    // bindLR (>>=) :: Either a -> 
    // (a -> Either b) -> Either b
    const bindLR = m =>
        mf => undefined !== m.Left ? (
            m
        ) : mf(m.Right);

    // either :: (a -> c) -> (b -> c) -> Either a b -> c
    const either = fl =>
        fr => e => 'Either' === e.type ? (
            undefined !== e.Left ? (
                fl(e.Left)
            ) : fr(e.Right)
        ) : undefined;

    // jsonParseLR :: String -> Either String a
    const jsonParseLR = s => {
        try {
            return Right(JSON.parse(s));
        } catch (e) {
            return Left(
                `${e.message} (line:${e.line} col:${e.column})`
            );
        }
    };

    // unlines :: [String] -> String
    const unlines = xs =>
        // A single string formed by the intercalation
        // of a list of strings with the newline character.
        xs.join('\n');

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

1 Like

It seems the macro is no longer working! The value pairs are not extracted! Using Omnigraffle latest version 7.24.4 (v205.51.2). Any ideas or help?

I believe they've changed the structure of their clipboard,
and moved this kind of thing out of reach.

( com.omnigroup.OmniGraffle.GraphicType no longer has a string value )

I select an object in Omnigraffle and do "Edit >Copy As > Javascript" and this is what I get (example):

// Floating point values in this script may be rounded, resulting in minor visual differences from the original
var canvas = document.windows[0].selection.canvas;
var g1 = canvas.newLine();
g1.cornerRadius = 0;
g1.headType = "";
g1.actionURL = null;
g1.tail = null;
g1.shadowColor = null;
g1.flippedVertically = false;
g1.allowsConnections = true;
g1.tailType = "";
g1.strokeType = StrokeType.Single;
g1.userData = {"Metros": "3.15"};
g1.head = null;
g1.lineType = LineType.Straight;
g1.tailMagnet = 0;
g1.strokeJoin = LineJoin.Round;
g1.strokeThickness = 1;
g1.plasticHighlightAngle = null;
g1.strokeColor = Color.RGB(1.0, 0.577706039, 0.0);
g1.shadowFuzziness = 3;
g1.locked = false;
g1.rotation = 0;
g1.shadowVector = new Point(0.00, 2.00);
g1.alignsEdgesToGrid = true;
g1.hopType = HopType.None;
g1.automationAction = ;
g1.headMagnet = 0;
g1.tailScale = 1;
g1.name = "LedTW";
g1.flippedHorizontally = false;
g1.notes = "";
g1.strokeCap = LineCap.Round;
g1.plasticCurve = null;
g1.headScale = 1;
g1.points = [new Point(436.40, 287.20), new Point(446.00, 206.06)];
g1.strokePattern = StrokeDash.Solid;

The key pairs seem to be under : g1.userData

Any way to make the script work again?

No.

As you demonstrate, their Copy As > JavaScript delivers a disappointingly flat procedural chain.

(In earlier versions, their default (Edit > Copy) pasteboard included a structured, and thus usable, JSON representation of the data structures. No longer)