Feature request for a keyboard shortcut to fully expand the sidebar

Off topic but storing taskpaper files in Dropbox is not a great experience due to this ‘meta data’ issue. Document gets completely removed from the Recent Documents list, the only way I have to keep track of my documents is using the macOS colour labels, which do not get removed by the Dropbox application. So I can’t really say what is going on because I read the colour labels are tiny bits of meta data. I’m on High Sierra but this behavior dates back to Yosemite.

In principle iCloud should do better with these extended attributes than Dropbox.

I’m not an iCloud user myself, and the picture does seem to be mixed (some xattrs apparently do get dropped),

https://mjtsai.com/blog/2018/01/08/icloud-drive-can-strip-metadata-from-your-documents/

but Apple’s marketers seem to feel that the goal is to retain them.

PS

While on this topic, it occurs to me that any editing of a .taskpaper file in a 3rd party app on iOS might also shed xattrs.

@complexpoint — when I use xattr in the Terminal, I get:

com.apple.lastuseddate#PS
 com.apple.quarantine
 com.dropbox.attributes
 com.taskpaper.editor.defaultRestorableState.jim
 com.taskpaper.outline.metadata

I am not sure what to do with the above. At the moment, I don’t have another Mac to test with, so I am not sure if I can test if the metadata is synchronized or not.

Here is your reminder @jessegrosjean :slight_smile:

I’m not seeing that at all. My TaskPaper files are all listed in the recent documents menu. Weird.

So I edit something on my iPhone using Editorial and it syncs back to my Mac and Taskpaper has ‘forgotten’ the document, not only closes it but also missing from the Recent Documents. I do have the System Preference set properly as explained on these forums, I don’t know how macOS handles application state.

Hi Jim, those extended attribute keys, shown by xattr, turn out to store the folding state of the paragraphs themselves, but not, it seems, of the sidebar. (You can see the value attached to each key by adding the -l switch to the xattr command line (see man attr)

It appears that the the sidebar state is preserved by the app, as Jesse suggested, through a UI preservation API.

Jesse will know, but perhaps this is the process:

https://developer.apple.com/documentation/uikit/view_controllers/preserving_your_app_s_ui_across_launches/about_the_ui_preservation_process?language=objc

My impression is that this is a form of application-state memory, rather than document-state memory.
I wonder if the unexpectedly closed sidebar project trees arise when you re-open an earlier document (and the app) after a session with a different document, with which the sidebar tree had not been opened ?

Ok, I’ve looked into this a bit more.

First this info is stored in TaskPaper’s system preferences plist file under the key NSOutlineView Items Sidebar. Associated with that key is a list of item identifiers. Some are hard coded group names such as “projects”, “searches”. And some are content hashes of particular outline items in your outline that are expanded.

So it’s not stored in apple’s user interface preservation system, or in my xattr system… stored in TaskPaper’s user defaults.

This means a few things. First there’s a single user defaults key, so the sidebar expanded state is only stored for the last document that changes it. Since some keys “projects”, etc are shared across all documents it means all documents should open with the same group state. But since content hashes are not shared across documents it means that heading expansion state will only be restored for the last document that wrote to that key.

It also means that if you edit the outline branch of an expanded item then that will change the content hash for that item. And unless the state is reserved to the defaults then that item that was edited won’t be restored as expanded on the next launch.

Unfortunately I don’t have a lot of control over when those values are written to user defaults… right now I’m just setting the NSOutlineView’s autosavename and the system does the rest. But there doesn’t seem to be a good way to invalidate the default. From what I can tell it will only save new defaults when you explicitly expand/collapse an item.

I’m not sure what the best end solution is… I tried a few quick hacks to solve but they all had other tradeoffs. I think earlier you said an “Expand All in Sidebar” menu it would help you out… does that still seem useful? I could add that pretty easily.

In the new system that I’m working on with WriteRoom 4 I’m making the sidebar scriptable, so eventually you’ll have more control, but as always that’s taking more time then expected!

1 Like

I’d be delighted if I had that as a workaround solution.

I also just noticed that if you hold down “Option” when expanding items in the sidebar it will do an “Expand All”. So for example while holding down “Option” click the “Projects” group “Hide/Show” button and it will expand all in that group.

2 Likes

If I had a native keyboard shortcut that executed that, I’d be happy!

In the meanwhile, this JXA script assumes installation of a current version of Keyboard Maestro - you can use it on its own (as long as KM and TaskPaper 3 are both installed), but you can also, of course, paste it into a KM Execute a JavaScript for Automation action, and use that to assign a keystroke to it.

(() => {

    // Click-Toggle Show Project in Sidebar of TaskPaper 3
    // DEPENDENCIES:
    //  Assumes installation of Keyboard Maestro 8, and TaskPaper 3
    // USE: This is a JavaScript for Automation script, which can be
    // tested in Script Editor (with the language tab set to JavaScript)
    // From Script Menu, or as a Keyboard Maestro
    // 'Execute a JavaScript for Automation' action

    // Rob Trew (c) 2018
    // MIT License
    // Ver 0.5 2018-01-11

    // 0.5 In case of wider sidebars set clicked horizontal to X + (W-10)
    // 0.4 (Finally posted the correct version :-)
    // 0.3 (Simplified (>>=) bindMay chain)
    // 0.2 Added the Opt to Opt-Click

    // main :: JS Function -> FilePath -> a

    'use strict';

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

    // Just :: a -> Just a
    const Just = x => ({
        type: 'Maybe',
        Nothing: false,
        Just: x
    });

    // Nothing :: () -> Nothing
    const Nothing = (optionalMsg) => ({
        type: 'Maybe',
        Nothing: true,
        msg: optionalMsg
    });

    // append (++) :: [a] -> [a] -> [a]
    const append = (xs, ys) => xs.concat(ys);

    // bindMay (>>=) :: Maybe a -> (a -> Maybe b) -> Maybe b
    const bindMay = (mb, mf) =>
        mb.Nothing ? mb : mf(mb.Just);

    // isMaybe :: a -> Bool
    const isMaybe = x =>
        x.type === 'Maybe';

    // map :: (a -> b) -> [a] -> [b]
    const map = (f, xs) => xs.map(f);

    // readFile :: FilePath -> IO String
    const readFile = strPath => {
        var error = $(),
            str = ObjC.unwrap(
                $.NSString.stringWithContentsOfFileEncodingError(
                    $(strPath)
                    .stringByStandardizingPath,
                    $.NSUTF8StringEncoding,
                    error
                )
            );
        return Boolean(error.code) ? (
            ObjC.unwrap(error.localizedDescription)
        ) : str;
    };

    // takeBaseName :: FilePath -> String
    const takeBaseName = strPath =>
        strPath !== '' ? (
            strPath[strPath.length - 1] !== '/' ? (
                strPath.split('/')
                .slice(-1)[0].split('.')[0]
            ) : ''
        ) : '';

    // takeExtension :: FilePath -> String
    const takeExtension = strPath => {
        const
            xs = strPath.split('.'),
            lng = xs.length;
        return lng > 1 ? (
            '.' + xs[lng - 1]
        ) : '';
    };

    // File name template -> temporary path
    // (Random digit sequence inserted between template base and extension)
    // tempFilePath :: String -> IO FilePath
    const tempFilePath = template =>
        ObjC.unwrap($.NSTemporaryDirectory()) +
        takeBaseName(template) + Math.random()
        .toString()
        .substring(3) + takeExtension(template);

    // MAIN ---------------------------------------------------

    // jsoDoScript :: Object (Dict | Array) -> IO ()
    const jsoDoScript = jso => {
        const strPath = tempFilePath('tmp.plist');
        return (
            Application('Keyboard Maestro Engine')
            .doScript((
                $(Array.isArray(jso) ? jso : [jso])
                .writeToFileAtomically(
                    $(strPath)
                    .stringByStandardizingPath,
                    true
                ),
                readFile(strPath)
            )),
            true
        );
    };

    const
        se = Application('System Events'),
        procs = se.applicationProcesses.where({
            name: 'TaskPaper'
        }),
        mbResult =
        bindMay(
            bindMay(
                bindMay(
                    procs.length > 0 ? (
                        Just(procs.at(0))
                    ) : Nothing('TaskPaper not running.'),
                    proc => {
                        const ws = proc.windows;
                        return ws.length > 0 ? (
                            Just(ws.at(0)
                                .splitterGroups.at(0)
                                .scrollAreas)
                        ) : Nothing('No TaskPaper windows open.')
                    }
                ),
                areas => areas.length > 1 ? (
                    Just(areas.at(0)
                        .outlines.at(0)
                        .rows.at(1)
                        .uiElements.at(0))
                ) : Nothing('Sidebar not open.')
            ),
            cell => {
                const [
                    x, y, w, h
                ] = append(cell.position(), cell.size());
                const [
                    sx, sy
                ] = map(
                    n => n.toString(), [x + (w - 10), y + (h / 2)]
                );
                return (
                    // Effect
                    jsoDoScript([{
                            "ReopenWindows": false,
                            "AllWindows": true,
                            "MacroActionType": "ActivateApplication",
                            "AlreadyActivatedActionType": "Normal",
                            "TimeOutAbortsMacro": true,
                            "Application": {
                                "NewFile": "/Applications/TaskPaper.app",
                                "BundleIdentifier": "com.hogbaysoftware.TaskPaper3.direct",
                                "Name": "TaskPaper"
                            }
                        }, {
                            "MacroActionType": "PauseUntil",
                            "TimeOutAbortsMacro": true,
                            "Conditions": {
                                "ConditionList": [{
                                    "ApplicationConditionType": "Active",
                                    "ConditionType": "Application",
                                    "Application": {
                                        "NewFile": "/Applications/TaskPaper.app",
                                        "BundleIdentifier": "com.hogbaysoftware.TaskPaper3.direct",
                                        "Name": "TaskPaper"
                                    }
                                }],
                                "ConditionListMatch": "Any"
                            }
                        },
                        {
                            "UseFormat": false,
                            "MacroActionType": "SetVariableToCalculation",
                            "Variable": "instanceMouseX",
                            "Text": "MOUSEX()"
                        },
                        {
                            "UseFormat": false,
                            "MacroActionType": "SetVariableToCalculation",
                            "Variable": "instanceMouseY",
                            "Text": "MOUSEY()"
                        },
                        {
                            "ClickCount": 0,
                            "DragVerticalPosition": "0",
                            "MacroActionType": "MouseMoveAndClick",
                            "Modifiers": 0,
                            "Button": 0,
                            "Fuzz": 15,
                            "Action": "Move",
                            "DisplayMatches": false,
                            "DragHorizontalPosition": "0",
                            "RestoreMouseLocation": false,
                            "VerticalPositionExpression": sy,
                            "RelativeCorner": "TopLeft",
                            "HorizontalPositionExpression": sx,
                            "Relative": "Absolute",
                            "MouseDrag": "None"
                        },
                        {
                            "Time": ".1",
                            "MacroActionType": "Pause",
                            "TimeOutAbortsMacro": true
                        },
                        {
                            "ClickCount": 1,
                            "DragVerticalPosition": "0",
                            "MacroActionType": "MouseMoveAndClick",
                            "Modifiers": 2048,
                            "Button": 0,
                            "Fuzz": 15,
                            "Action": "MoveAndClick",
                            "DisplayMatches": false,
                            "DragHorizontalPosition": "0",
                            "RestoreMouseLocation": false,
                            "VerticalPositionExpression": sy,
                            "RelativeCorner": "TopLeft",
                            "HorizontalPositionExpression": sx,
                            "Relative": "Absolute",
                            "MouseDrag": "None"
                        },
                        {
                            "ClickCount": 0,
                            "DragVerticalPosition": "0",
                            "MacroActionType": "MouseMoveAndClick",
                            "Modifiers": 0,
                            "Button": 0,
                            "Fuzz": 15,
                            "Action": "Move",
                            "DisplayMatches": false,
                            "DragHorizontalPosition": "0",
                            "RestoreMouseLocation": false,
                            "VerticalPositionExpression": "instanceMouseY",
                            "RelativeCorner": "TopLeft",
                            "HorizontalPositionExpression": "instanceMouseX",
                            "Relative": "Absolute",
                            "MouseDrag": "None"
                        }
                    ]),
                    Just([sx, sy])
                )
            }
        );
    return mbResult.Nothing ? (
        mbResult.msg
    ) : ('Clicked TaskPaper sidebar at: ' + mbResult.Just);
})();

(Updated to 0.2 above – added the ⌥ as in Opt-Click )

Hi Rob,

Doesn’t work for me. I am guessing this is because I have made my sidebar wider.

Which number should I change to make the click work on my wider sidebar?

Is there an easy way to measure what the number should be?

Example:

Not your fault – I posted a version with dependencies - correcting to 0.4 now

And in 0.5 I’ve changed the clicked X to (X + (W-10)) which should be insensitive to sidebar width, I think,

Thanks! Version 0.5 works on my wide sidebar!

1 Like

Posted in a Keyboard Maestro macro (which additionally opens the sidebar if it finds it closed) at:

1 Like

(Incidentally, there may be a redundant pause action in there:

 {
    "Time": "0.1",
    "MacroActionType": "Pause",
     "TimeOutAbortsMacro": true
}

I find that in practice I can I either remove it or set the Time string to “0” on my system.

 {
    "Time": "0",
    "MacroActionType": "Pause",
     "TimeOutAbortsMacro": true
}

Removed it and it works fine on my MacBook Pro (15-inch, 2016).

1 Like

@Jim @complexpoint In latest preview release I’ve added “View > Show Sidebar And Expand Completely”:

2 Likes

@jessegrosjean — works well here! The only thing I would add is a native keyboard shortcut.

Interesting discussion! Thanks for all the juicy details.