The dragging of Tinderbox macros from one document to another has been temporarily suspended in current builds after it was broken by Catalina.
Copy macros from front Tinderbox document.kmmacros.zip (12.9 KB)
Here is a Keyboard Maestro macro which, (at least on Mojave, I haven’t tested on Catalina), aims to let you:
- list the macros in the front Tinderbox 8 document,
- and select all or a subset for copying.
(in a clipboard format which then lets you paste them into another Tinderbox document)
Note that only saved macro edits are available for copying in this way – the script reads them from the most recent save of the front document’s .tbx file.
Once copied, you can paste them into another document by selecting it and using the usual ⌘V or Edit > Paste
(Not much will appear to happen, but you should then find the pasted macros in the Macros pane of the HTML inspector)
The JavaScript for Automation source is below, and can be used outside Keyboard Maestro, in any context from which JXA script can be run, for example in macOS Script Editor, with the language selector pull-down at top left set to JavaScript rather than AppleScript
JavaScript source
(() => {
'use strict';
// Copy Tinderbox 8 macros from latest save of front document.
// (For pasting to another document).
// Rob Trew 2020
// Ver 0.01
ObjC.import('AppKit');
// main :: IO ()
const main = () =>
either(alert('No macros copied'))(
xmlMacros => {
// XML for 'com.eastgate.tinderbox.scrap' clipboard:
// XML composed,
const
strMacrosXML = unlines([
'<?xml version="1.0" encoding="UTF-8" ?>',
'<tinderbox version="2" revision="12" >',
'<macros >',
xmlMacros[0].join(''),
'</macros>',
'</tinderbox>'
]);
return (
// XML copied as 'com.eastgate.tinderbox.scrap' pboard.
setClipOfTextType(
'com.eastgate.tinderbox.scrap'
)(strMacrosXML),
// Notification of copy,
Object.assign(Application.currentApplication(), {
includeStandardAdditions: true
}).displayNotification(
unwords(xmlMacros[1]), {
withTitle: str(xmlMacros[0].length) +
' Tinderbox macros copied',
subtitle: '(from ' + xmlMacros[2] + ')'
}
),
// and return of macro name list.
unlines(xmlMacros[1])
);
}
)(bindLR(tbxFrontDocLR())(
doc => bindLR(
null !== doc.file() ? (
// XML read from latest save of document file.
macroDictFromTBXxml(
readFile(doc.file().toString())
)
) : Left('Changes not saved: ' + doc.name())
)(dctMacros => {
const ks = Object.keys(dctMacros);
// User choice of macros to copy:
return 0 < ks.length ? (
bindLR(
showMenuLR(true)('Copy Tinderbox Macros')(ks)
)(names => Right([
names.map(k => dctMacros[k]),
names,
doc.name()
]))
) : Left('No macros found in ' + doc.name())
})
));
// ---------------------TINDERBOX----------------------
// macroDictFromTBXxml ::
// String -> Either String {key::String, macro::XML}
const macroDictFromTBXxml = strMacrosXML => {
// Either a message String, or
// a dictionary of the macros found
// in a Tinderbox (.tbx) XML document.
const
uw = ObjC.unwrap,
eXML = $(),
docXML = $.NSXMLDocument.alloc
.initWithXMLStringOptionsError(
strMacrosXML, $.NSXMLNodePreserveAll, eXML
);
return bindLR(
docXML.isNil() ? (
Left(uw(eXML.localizedDescription))
) : Right(docXML)
)(docXML => {
const
eXP = $(),
xs = docXML
.nodesForXPathError('//macro', eXP);
return xs.isNil() ? (
Left(uw(eXP.localizedDescription))
) : Right(
uw(xs).reduce(
(a, x) => Object.assign(a, {
[uw(
x.attributeForName('name')
.valueForKey('stringValue')
)]: uw(x.XMLString)
}), {}
)
);
})
};
// tbxFrontDocLR :: Tbx IO () -> Either String Tbx Doc
const tbxFrontDocLR = () => {
// Either the front document in Tinderbox 8, or an
// explanatory message if no documents are open,
// or Tinderbox 8 is not running.
const tbx = Application('Tinderbox 8');
return tbx.running() ? (() => {
const ds = tbx.documents;
return 0 < ds.length ? (
Right(ds.at(0))
) : Left('No documents open in Tinderbox');
})() : Left('Tinderbox 8 is not running.');
};
// ------------------------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
);
};
// setClipOfTextType :: String -> String -> IO Bool
const setClipOfTextType = utiOrBundleID =>
txt => {
const pb = $.NSPasteboard.generalPasteboard;
return (
pb.clearContents,
pb.setStringForType(
$(txt),
utiOrBundleID
)
);
};
// showMenuLR :: Bool -> String -> [String] ->
// Either String [String]
const showMenuLR = blnMult => title => xs =>
0 < xs.length ? (() => {
const sa = Object.assign(
Application('System Events'), {
includeStandardAdditions: true
});
sa.activate();
const v = sa.chooseFromList(xs, {
withTitle: title,
withPrompt: 'Select' + (
blnMult ? ' one or more of ' +
xs.length.toString() +
' to copy:' : ':'
),
defaultItems: xs,
okButtonName: 'Copy',
cancelButtonName: 'Cancel',
multipleSelectionsAllowed: blnMult,
emptySelectionAllowed: false
});
return Array.isArray(v) ? (
Right(v)
) : Left('User cancelled ' + title + ' menu.');
})() : Left(title + ': No items to choose from.');
// -----------------GENERIC FUNCTIONS------------------
// https://github.com/RobTrew/prelude-jxa
// Left :: a -> Either a b
const Left = x => ({
type: 'Either',
Left: x
});
// Right :: b -> Either a b
const Right = x => ({
type: 'Either',
Right: x
});
// 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;
// readFile :: FilePath -> IO String
const readFile = fp => {
const
e = $(),
ns = $.NSString.stringWithContentsOfFileEncodingError(
$(fp).stringByStandardizingPath,
$.NSUTF8StringEncoding,
e
);
return ObjC.unwrap(
ns.isNil() ? (
e.localizedDescription
) : ns
);
};
// showLog :: a -> IO ()
const showLog = (...args) =>
console.log(
args
.map(JSON.stringify)
.join(' -> ')
);
// str :: a -> String
const str = x =>
// Stringification of a JS value.
x.toString();
// unlines :: [String] -> String
const unlines = xs => xs.join('\n');
// unwords :: [String] -> String
const unwords = xs => xs.join(' ');
// MAIN ---
return main();
})();