︎ If I copied content like this:
︎ I want to get/paste text like this: (only visible rows)
︎ not to get/paste text like this: (include all rows)
︎ If I copied content like this:
︎ I want to get/paste text like this: (only visible rows)
︎ not to get/paste text like this: (include all rows)
As a script, perhaps:
(() => {
"use strict";
ObjC.import("AppKit");
// BIKE :: Copy visible rows in selection only
// (Skipping hidden descendants)
// Rob Trew @2023
const main = () => {
const doc = Application("Bike").documents.at(0);
return doc.exists() ? (() => {
const
selectedRows = doc.rows.where({
selected: true,
visible: true
}),
report = [
[
"bike format",
"com.hogbaysoftware.bike.xml"
],
[
"OPML format",
"org.opml.opml"
],
[
"plain text format",
"public.utf8-plain-text"
]
]
.map(
([formatName, uti], i) => (
setClipOfTextType(0 === i)(uti)(
doc.export({
from: selectedRows,
as: formatName,
all: false
})
),
formatName
)
)
.join(", ");
return `Copied visible selection as ${report}`;
})() : "No document open in Bike.";
};
// ----------------------- JXA -----------------------
// setClipOfTextType :: String -> String -> IO String
const setClipOfTextType = withClearing =>
utiOrBundleID =>
txt => {
const pb = $.NSPasteboard.generalPasteboard;
return (
withClearing && pb.clearContents,
pb.setStringForType(
$(txt),
utiOrBundleID
),
txt
);
};
// MAIN ---
return main();
})();
To test in Script Editor, set language selector at top left to JavaScript
, rather than AppleScript
.
I notice that RTF is not directly supported in the scripting interface at the moment for that purpose, but if needed, we could expand the script above to derive RTF from the HTML.
FWIW, draft of a version which adds RTF pasteboard content to the existing Bike, OPML, and plain text:
(() => {
"use strict";
ObjC.import("AppKit");
// BIKE :: Copy visible rows in selection only
// (Skipping hidden descendants)
// Rob Trew @2023
// 0.02
// Added RTF pasteboard content to the existing
// bike, opml, plain.
const main = () => {
const doc = Application("Bike").documents.at(0);
return doc.exists() ? (() => {
const
selectedRows = doc.rows.where({
selected: true,
visible: true
}),
visibleContent = formatName =>
visibleRowsInFormat(doc)(
selectedRows
)(formatName),
report = [
[
"bike format",
"com.hogbaysoftware.bike.xml"
],
[
"OPML format",
"org.opml.opml"
],
[
"plain text format",
"public.utf8-plain-text"
],
[
"rich text format",
"public.rtf"
]
].map(
([formatName, uti], i) => (
setClipOfTextType(0 === i)(uti)(
"public.rtf" !== uti
? visibleContent(formatName)
: rtfFromHTML(
visibleContent(
"bike format"
)
).Right || ""
),
formatName.slice(0, -7)
)
).join(", ");
return `Copied visible selection as ${report}`;
})() : "No document open in Bike.";
};
// visibleRowsInFormat :: Bike Doc -> Bike Rows ->
// String -> String
const visibleRowsInFormat = doc =>
rows => formatName => doc.export({
from: rows,
as: formatName,
all: false
});
// ----------------------- JXA -----------------------
// rtfFromHTML :: String -> Either String String
const rtfFromHTML = strHTML => {
const
as = $.NSAttributedString.alloc
.initWithHTMLDocumentAttributes(
$(strHTML)
.dataUsingEncoding($.NSUTF8StringEncoding),
0
);
return bindLR(
"function" !== typeof as
.dataFromRangeDocumentAttributesError ? (
Left(
"String could not be parsed as HTML"
)
) : Right(as)
)(
// Function bound if Right value obtained above:
htmlAS => {
const
error = $(),
rtfData = htmlAS
.dataFromRangeDocumentAttributesError({
"location": 0,
"length": htmlAS.length
}, {
DocumentType: "NSRTF"
},
error
);
return Boolean(
ObjC.unwrap(rtfData) && !error.code
) ? Right(
ObjC.unwrap($.NSString.alloc
.initWithDataEncoding(
rtfData,
$.NSUTF8StringEncoding
))
) : Left(ObjC.unwrap(
error.localizedDescription
));
}
);
};
// setClipOfTextType :: String -> String -> IO String
const setClipOfTextType = withClearing =>
utiOrBundleID =>
txt => {
const pb = $.NSPasteboard.generalPasteboard;
return (
withClearing && pb.clearContents,
pb.setStringForType(
$(txt),
utiOrBundleID
),
txt
);
};
// --------------------- 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
});
// bindLR (>>=) :: Either a ->
// (a -> Either b) -> Either b
const bindLR = m =>
mf => m.Left ? (
m
) : mf(m.Right);
// MAIN ---
return main();
})();
( Only tested on macOS 11.7.4, Bike 1.9 – 112 )
A script is the only way now. I could add a “Copy Visible” command, but I worry it would rarely be found/used. Do any other outliner apps provide a command for copying only visible items?
I noticed that WorkFlowy and OmniOutliner both copy only the visible nodes by default, without a specific “copy visible” command.
Let make sure we are talking about the same thing. For me it seems like WorkFlowy is copying the folded rows. For example:
Testing OmniOutliner in viewer mode does seem to copy only visible rows. I can see how that might be wanted sometimes, but it seems a weird default for me at the moment. If I have a row fully selected and it contains child rows it seems logical to me that they are copied.
OmniOutliner also copies all hidden descendants with Row selection.
(It also has a text selection mode which only copies text selected in the modal text editing field for a given row)
It’s certainly a confusing feature decision all round… for me it seems like OmniOutliner is only copying visible rows
I think I see what is happening in OmniOutliner …
It seems important to me that the default would be to retain folded lines …
(In an editor of outline structures, one wants to be able to select and move a folded part without fearing that its contents might be lost )
Is that the same as what I’m seeing ?
In the earlier screenshot, I selected the the folded Alpha (clicking the left of the disclosure triangle for a row selection), and pasted further, getting both parent and descendants …
Anyway, not much turns on it
Looking at TaskPaper I added “Copy Displayed” option. I guess I will take that same approach unless someone has a better idea.
The latest Bike preview release adds Copy > Copy Displayed to handle this case: