asmagill / hs._asm.axuielement

Accessing Accessibility Objects with Hammerspoon
34 stars 2 forks source link

Thoughts on additions/changes #1

Closed asmagill closed 4 years ago

asmagill commented 8 years ago

@cmsj, @latenitefilms I'm moving this to its own repo because I think it deserves its own conversations

As of right now, this is just a copy of what is in my generic module repository, but I will replace the one there with this as a submodule once some progress has been made.

At any rate, let me sum up where I think this module is and where I think we'd like to see it go...

First, the primary methods for "finding" an axuielement are:

The first two work on the assumption that we want all of the children (which may match a criteria) without regard to their actual position in the hierarchy -- other than that they are "lower" then the starting node. This works adequately when we can start pretty far into the hierarchy already or we have a relatively small domain to search (see the examples for the Dock and the Speakable items list). They can both be very slow if we're at the top of the hierarchy (e.g. a window or application) and most likely return much more then we really need, necessitating further refinements with elementSearch on the previous result set. Even with elementSearch's search criteria, it's still traversing the entire hierarchy (for the topmost search) from the starting node, even if it only returns a subset of it.

What is needed is way to specify a query that limits the required search area as it gets more specific by specifying a path to follow. The path doesn't need to be complete, but where the path is specified, it must be followed: e.g. for the BBedit example I posted in the Hammerspoon issue 990, something like

first item with AXRole = "AXScrollBar" of item with AXRole = "AXWindow" and AXTitle = "init.lua.*"

(if you look closely at the example I posted, the scrollbar is actually in a child of an element of an unknown type, but we don't specify it here... once we've specified the BBEdit window with a title that starts with "init.lua", no other window of BBEdit will be examined.

@latenitefilm's example of get position of value indicator 1 of group 1 of scroll area 2 of splitter group 1 of group 7 of splitter group 1 of window "Final Cut Pro" (reworded to match whatever language ends up being easiest to parse) is a more directed example.

Starting from the end, each "of" specifies a pruning of paths that will never be examined, unlike the current methods available in the module.

Have I stated that reasonably?

latenitefilms commented 8 years ago

That all makes sense to me!

asmagill commented 8 years ago

Just to keep you updated, I'm working on a rewrite that I hope will be faster and easier to perform directed searches with.

It will probably be a few days before I have any update ready for testing, but will post here when it's ready.

latenitefilms commented 8 years ago

Amazing, thank you so much! HUGELY appreciated!

One thing, that would be REALLY incredible to have would be the ability to just return a UI element just based off a single search criteria. For example:

select UI element that has a description of 'persistent playhead'

This would be really useful for me, as in my case, I know that there's only one UI element that has this description, and it would solve a LOT of problems if your clever code could just quickly find this element, rather than me having to track down where exactly it lives (which, in this case is actually: 'description of value indicator 1 of group 1 of scroll area 2 of splitter group 1 of group 8 of splitter group 1 of window "Final Cut Pro" in AppleScript terms).

Thanks so much for all your hard work!

asmagill commented 8 years ago

@cmsj, @latenitefilms

Ok, the current version of this code now supports a few new methods: searchPath, buildTree, and path. You can can see how to use them by reviewing https://github.com/asmagill/hs._asm.axuielement/blob/master/Queries.md

Before adding this to core, it definitely needs:

It could probably use:

Let me know what you guys think, and I'd really like to see/hear if the new methods and tools help out with your project, @latenitefilms

latenitefilms commented 8 years ago

@asmagill - This is AMAZING!! Thank you so much!

The Queries documentation is awesome. Such a great way of explaining everything! Thank you!

I've just downloaded and will have a play over the next few days. Based on what you've written in the Queries documentation, I think this will be a HUGE help for what I'm doing. I should be able to now do what I'm currently doing with 100s of lines of code with on 10 or so lines, which is great. I'm sure it'll be faster too.

Watchers would definitely be helpful. I'd love it if I could trigger a function if a UI element suddenly appears in a specific application.

I'll keep you posted with how I go testing things.

Thanks again!

latenitefilms commented 8 years ago

@asmagill - I've only just started playing - but it's AMAZING!!

searchPath is exactly what I needed. Thank you!

latenitefilms commented 8 years ago

@asmagill - For what it's worth, and I definitely need to do some more testing to confirm, but I THINK searchPath may still be slower than the very messy and complicated code that I was previously using (which is basically just lots and lots of loops).

Your code is HEAPS nicer, cleaner and SO MUCH easier to code - but unless I'm missing something, I do think my messy code is actually faster. Here's an example:

Using your new functions:

function highlightFCPXBrowserPlayhead()

    sw = ax.windowElement(hs.application("Final Cut Pro"):mainWindow())

    persistentPlayhead = sw:searchPath({
        { role = "AXWindow", Title = "Final Cut Pro"},
        { role = "AXSplitGroup", AXRoleDescription = "split group" },
        { role = "AXGroup", },
        { role = "AXSplitGroup", Identifier = "_NS:11" },
        { role = "AXScrollArea", Description = "organizer" },
        { role = "AXGroup", Identifier = "_NS:9"},
        { role = "AXValueIndicator", Description = "persistent playhead" },
    }, 1)

    persistentPlayheadPosition = persistentPlayhead:attributeValue("AXPosition")
    persistentPlayheadSize = persistentPlayhead:attributeValue("AXSize")

    mouseHighlight(persistentPlayheadPosition["x"], persistentPlayheadPosition["y"], persistentPlayheadSize["w"], persistentPlayheadSize["h"])

end

Using your original functions:

function highlightFCPXBrowserPlayhead()

    --------------------------------------------------------------------------------
    -- Delete any pre-existing highlights:
    --------------------------------------------------------------------------------
    deleteAllHighlights()

    --------------------------------------------------------------------------------
    -- Filmstrip or List Mode?
    --------------------------------------------------------------------------------
    local fcpxBrowserMode = fcpxWhichBrowserMode()

    -- Error Checking:
    if (fcpxBrowserMode == "Failed") then
        displayErrorMessage("Unable to determine if Filmstrip or List Mode.")
        return
    end

    --------------------------------------------------------------------------------
    -- Get all FCPX UI Elements:
    --------------------------------------------------------------------------------
    fcpx = hs.application("Final Cut Pro")
    fcpxElements = ax.applicationElement(fcpx)[1]

    --------------------------------------------------------------------------------
    -- Which Split Group:
    --------------------------------------------------------------------------------
    local whichSplitGroup = nil
    for i=1, fcpxElements:attributeValueCount("AXChildren") do
        if whichSplitGroup == nil then
            if fcpxElements:attributeValue("AXChildren")[i]:attributeValue("AXRole") == "AXSplitGroup" then
                whichSplitGroup = i
            end
        end
    end
    if whichSplitGroup == nil then
        displayErrorMessage("Unable to locate Split Group.")
        return "Failed"
    end

    --------------------------------------------------------------------------------
    -- List Mode:
    --------------------------------------------------------------------------------
    if fcpxBrowserMode == "List" then

        --------------------------------------------------------------------------------
        -- Which Group contains the browser:
        --------------------------------------------------------------------------------
        local whichGroup = nil
        for i=1, fcpxElements[whichSplitGroup]:attributeValueCount("AXChildren") do
            if whichGroupGroup == nil then
                if fcpxElements[whichSplitGroup][i]:attributeValue("AXRole") == "AXGroup" then
                    --------------------------------------------------------------------------------
                    -- We now have ALL of the groups, and need to work out which group we actually want:
                    --------------------------------------------------------------------------------
                    for x=1, fcpxElements[whichSplitGroup][i]:attributeValueCount("AXChildren") do
                        if fcpxElements[whichSplitGroup][i][x]:attributeValue("AXRole") == "AXSplitGroup" then
                            --------------------------------------------------------------------------------
                            -- Which Split Group is it:
                            --------------------------------------------------------------------------------
                            for y=1, fcpxElements[whichSplitGroup][i][x]:attributeValueCount("AXChildren") do
                                if fcpxElements[whichSplitGroup][i][x][y]:attributeValue("AXRole") == "AXSplitGroup" then
                                    if fcpxElements[whichSplitGroup][i][x][y]:attributeValue("AXIdentifier") == "_NS:231" then
                                        whichGroup = i
                                        goto listGroupDone
                                    end
                                end
                            end
                        end
                    end
                end
            end
        end
        ::listGroupDone::
        if whichGroup == nil then
            displayErrorMessage("Unable to locate Group.")
            return "Failed"
        end

        --------------------------------------------------------------------------------
        -- Which Split Group Two:
        --------------------------------------------------------------------------------
        local whichSplitGroupTwo = nil
        for i=1, (fcpxElements[whichSplitGroup][whichGroup]:attributeValueCount("AXChildren")) do
            if whichSplitGroupTwo == nil then
                if fcpxElements[whichSplitGroup][whichGroup]:attributeValue("AXChildren")[i]:attributeValue("AXRole") == "AXSplitGroup" then
                    whichSplitGroupTwo = i
                    goto listSplitGroupTwo
                end
            end
        end
        ::listSplitGroupTwo::
        if whichSplitGroupTwo == nil then
            displayErrorMessage("Unable to locate Split Group Two.")
            return "Failed"
        end

        --------------------------------------------------------------------------------
        -- Which Split Group Three:
        --------------------------------------------------------------------------------
        local whichSplitGroupThree = nil
        for i=1, (fcpxElements[whichSplitGroup][whichGroup][whichSplitGroupTwo]:attributeValueCount("AXChildren")) do
            if whichSplitGroupThree == nil then
                if fcpxElements[whichSplitGroup][whichGroup][whichSplitGroupTwo]:attributeValue("AXChildren")[i]:attributeValue("AXRole") == "AXSplitGroup" then
                    whichSplitGroupThree = i
                    goto listSplitGroupThree
                end
            end
        end
        ::listSplitGroupThree::
        if whichSplitGroupThree == nil then
            displayErrorMessage("Unable to locate Split Group Three.")
            return "Failed"
        end

        --------------------------------------------------------------------------------
        -- Which Group Two:
        --------------------------------------------------------------------------------
        local whichGroupTwo = nil
        for i=1, (fcpxElements[whichSplitGroup][whichGroup][whichSplitGroupTwo][whichSplitGroupThree]:attributeValueCount("AXChildren")) do
            if fcpxElements[whichSplitGroup][whichGroup][whichSplitGroupTwo][whichSplitGroupThree]:attributeValue("AXChildren")[i]:attributeValue("AXRole") == "AXGroup" then
                whichGroupTwo = i
            end
        end
        if whichGroupTwo == nil then
            displayErrorMessage("Unable to locate Group Two.")
            return "Failed"
        end

        --------------------------------------------------------------------------------
        -- Which is Persistent Playhead?
        --------------------------------------------------------------------------------
        local whichPersistentPlayhead = (fcpxElements[whichSplitGroup][whichGroup][whichSplitGroupTwo][whichSplitGroupThree][whichGroupTwo]:attributeValueCount("AXChildren")) - 1

        --------------------------------------------------------------------------------
        -- Let's highlight it at long last!
        --------------------------------------------------------------------------------
        persistentPlayheadPosition = fcpxElements[whichSplitGroup][whichGroup][whichSplitGroupTwo][whichSplitGroupThree][whichGroupTwo][whichPersistentPlayhead]:attributeValue("AXPosition")
        persistentPlayheadSize = fcpxElements[whichSplitGroup][whichGroup][whichSplitGroupTwo][whichSplitGroupThree][whichGroupTwo][whichPersistentPlayhead]:attributeValue("AXSize")

        mouseHighlight(persistentPlayheadPosition["x"], persistentPlayheadPosition["y"], persistentPlayheadSize["w"], persistentPlayheadSize["h"])

    --------------------------------------------------------------------------------
    -- Filmstrip Mode:
    --------------------------------------------------------------------------------
    elseif fcpxBrowserMode == "Filmstrip" then

        --------------------------------------------------------------------------------
        -- Which Group contains the browser:
        --------------------------------------------------------------------------------
        local whichGroup = nil
        for i=1, fcpxElements[whichSplitGroup]:attributeValueCount("AXChildren") do
            if whichGroupGroup == nil then
                if fcpxElements[whichSplitGroup][i]:attributeValue("AXRole") == "AXGroup" then
                    --------------------------------------------------------------------------------
                    -- We now have ALL of the groups, and need to work out which group we actually want:
                    --------------------------------------------------------------------------------
                    for x=1, fcpxElements[whichSplitGroup][i]:attributeValueCount("AXChildren") do
                        if fcpxElements[whichSplitGroup][i][x]:attributeValue("AXRole") == "AXSplitGroup" then
                            --------------------------------------------------------------------------------
                            -- Which Split Group is it:
                            --------------------------------------------------------------------------------
                            for y=1, fcpxElements[whichSplitGroup][i][x]:attributeValueCount("AXChildren") do
                                if fcpxElements[whichSplitGroup][i][x][y]:attributeValue("AXRole") == "AXScrollArea" then
                                    if fcpxElements[whichSplitGroup][i][x][y]:attributeValue("AXIdentifier") == "_NS:40" then
                                        whichGroup = i
                                        goto filmstripGroupDone
                                    end
                                end
                            end
                        end
                    end
                end
            end
        end
        ::filmstripGroupDone::
        if whichGroup == nil then
            displayErrorMessage("Unable to locate Group.")
            return "Failed"
        end

        --------------------------------------------------------------------------------
        -- Which Split Group Two:
        --------------------------------------------------------------------------------
        local whichSplitGroupTwo = nil
        for i=1, (fcpxElements[whichSplitGroup][whichGroup]:attributeValueCount("AXChildren")) do
            if whichSplitGroupTwo == nil then
                if fcpxElements[whichSplitGroup][whichGroup]:attributeValue("AXChildren")[i]:attributeValue("AXRole") == "AXSplitGroup" then
                    whichSplitGroupTwo = i
                    goto filmstripSplitGroupTwoDone
                end
            end
        end
        ::filmstripSplitGroupTwoDone::
        if whichSplitGroupTwo == nil then
            displayErrorMessage("Unable to locate Split Group Two.")
            return "Failed"
        end

        --------------------------------------------------------------------------------
        -- Which Scroll Area:
        --------------------------------------------------------------------------------
        local whichScrollArea = nil
        for i=1, (fcpxElements[whichSplitGroup][whichGroup][whichSplitGroupTwo]:attributeValueCount("AXChildren")) do
            if fcpxElements[whichSplitGroup][whichGroup][whichSplitGroupTwo]:attributeValue("AXChildren")[i]:attributeValue("AXRole") == "AXScrollArea" then
                whichScrollArea = i
            end
        end
        if whichScrollArea == nil then
            displayErrorMessage("Unable to locate Scroll Area.")
            return "Failed"
        end

        --------------------------------------------------------------------------------
        -- Which Group Two:
        --------------------------------------------------------------------------------
        local whichGroupTwo = nil
        for i=1, (fcpxElements[whichSplitGroup][whichGroup][whichSplitGroupTwo][whichScrollArea]:attributeValueCount("AXChildren")) do
            if fcpxElements[whichSplitGroup][whichGroup][whichSplitGroupTwo][whichScrollArea]:attributeValue("AXChildren")[i]:attributeValue("AXRole") == "AXGroup" then
                whichGroupTwo = i
            end
        end
        if whichGroupTwo == nil then
            displayErrorMessage("Unable to locate Group Two.")
            return "Failed"
        end

        --------------------------------------------------------------------------------
        -- Which is Persistent Playhead?
        --------------------------------------------------------------------------------
        local whichPersistentPlayhead = (fcpxElements[whichSplitGroup][whichGroup][whichSplitGroupTwo][whichScrollArea][whichGroupTwo]:attributeValueCount("AXChildren")) - 1

        --------------------------------------------------------------------------------
        -- Let's highlight it at long last!
        --------------------------------------------------------------------------------
        persistentPlayheadPosition = fcpxElements[whichSplitGroup][whichGroup][whichSplitGroupTwo][whichScrollArea][whichGroupTwo][whichPersistentPlayhead]:attributeValue("AXPosition")
        persistentPlayheadSize = fcpxElements[whichSplitGroup][whichGroup][whichSplitGroupTwo][whichScrollArea][whichGroupTwo][whichPersistentPlayhead]:attributeValue("AXSize")

        mouseHighlight(persistentPlayheadPosition["x"], persistentPlayheadPosition["y"], persistentPlayheadSize["w"], persistentPlayheadSize["h"])
    end
end

Any suggestions?

asmagill commented 8 years ago

I'll take a closer look over the weekend... it's certainly possible that a properly designed specifically targeted solution will be faster... I'm trying to make this somewhat universal after all! The new approach allows much more refinement and targeting then the original blunt instrument, and a significant speed gain if you take the time to target your search, but there will always be ways to improve.

There are some portions of my new approach that I would like to eventually move into Objective-C for speed, but I wanted (a) something that worked, and (b) something that I could easily tweak until I found the optimal solution, so that meant staying in Lua as much as possible.

latenitefilms commented 8 years ago

Absolutely - and I'm HUGELY appreciative of all your help and support! What you've done is truly awesome, and VERY useful. It's already been REALLY helpful for me, as it allows me to test and prototype things REALLY quickly - and then once I get things working, then I can replace your simple cost with a horrible mess of if's and for's to get slightly faster speeds.

However, looking at my code though - what I'm doing is pretty basic. I'm just starting with the ax.applicationElement, then doing a series of for's and if's to check criteria. The way you can just feed searchPath a table is perfect - but I wonder if there would be benefit in having a fastSearchPath feature, that basically just does exactly what my series of for's and if's do - i.e. just search for very specific things. Your code is way too smart for me to fully understand, but it looks like you're "looking up" a lot of data (i.e. allAttributeValues()) - which I'm sure slows things down. I'm wondering if there's a way to only search for very specific things to try and speed things up.

For example - I'm thinking something like:

    persistentPlayhead = sw:fastSearchPath({
        { role = "AXWindow", AXTitle = "Final Cut Pro" }, -- There might be multiple AXWindow's at this level, so selected based on only the Window Title.
        { role = "AXSplitGroup", OneOfAKind }, -- There's only one Split Group at this level.
        { role = "AXGroup", Child = {AXSplitGroup, Child = { AXScrollArea, AXIdentifier = "_NS:40" }}}, -- There might be multiple AXGroup's at this level, so we work out which group to use based on only it's child (i.e. AXSplitGroup), and children of child (i.e. AXScrollArea) details (i.e. AXIdentifier value)
        { role = "AXSplitGroup", OneOfAKind }, -- There's only one Split Group at this level.
        { role = "AXScrollArea", OneOfAKind }, -- There's only one Scroll Area at this level.
        { role = "AXGroup", OneOfAKind }, -- There's only one Group at this level.
        { role = "AXValueIndicator", AXDescription = "persistent playhead" }, -- There might be multiple AXValueIndicator indicators at this level, so selected based on the AXDescription.
    }, 1)

Rather than calling allAttributeValues(), we would only get data on things that are specifically asked for. Because we're basically telling Hammerspoon almost exactly where something is - we also don't need to "search" multiple layers of UI elements - we should only search within the parameters we're given - otherwise we just return nil.

Anyway, just food for thought.

Thanks again for all your help!

latenitefilms commented 8 years ago

What could also be handy is something like...

 example = sw:fastSearchPath({
        { role = "AXGroup", WhichItemOfSameRole = First }, -- Pick first AXGroup at this level
        { role = "AXGroup", WhichItemOfSameRole = Last }, -- Pick last AXGroup at this level
        { role = "AXGroup", WhichItemOfSameRole = 3 }, -- Pick 3rd AXGroup at this level
        { ID = 4 }, -- Pick 4th UI Element at this level regardless of role
    }, 1)

Again... just food for thought. Feel free to completely ignore! Please don't feel obligated to do any of these suggestions!

latenitefilms commented 8 years ago

@asmagill - Apple has just released a new version of Final Cut Pro, which has broken ALL of my carefully crafted GUI Scripting done using your AMAZING hs._asm.axuielement script.

For speed reasons, as explained above, I was using a massive amounts of "for" loops to make everything happen - but now that I have to re-write anyway, I figure I should do it smarter this time, ideally using your searchPath feature.

I was just wondering if you've done any changes to the script since we last spoke? If not, I might see if I can somehow speed up searchPath by making it simpler, as exampled above.

Thanks for your constant help and support! HUGELY appreciated!!

latenitefilms commented 7 years ago

@asmagill - Absolutely no pressure what-so-ever, but just wondering if you've done any further work on axuielement recently?

asmagill commented 7 years ago

I needed a bit of a break from this, but I plan to get back to it soon. As you've been doing, keep posting suggestions/observations and I'll review them when I do, hopefully this weekend.

asmagill commented 4 years ago

@latenitefilms I've made some updates to axuielement in preparation for getting it ready for migration. In this update, the main changes are:

I plan to closely review elementSearch, matches, and getAllChildElements this weekend. I'm leaning towards rewriting them so that they take advantage of coroutines to keep Hammerspoon more responsive. It will likely make getAllChildElements take a little longer, but as we found out a couple of months back, the queries have to occur on the main thread, so even being written in Objective-C and using a callback upon completion, the actual loop blocks Hammerspoon noticeably when used for grabbing anything with a large number of children (starting the capture at the application element, for example). Using coroutines might take a little longer, but shouldn't lock things up as much.

I'll also be reviewing the issues here to see what else might be fixable relatively easily before (hopefully) setting up a pull request for core sometime next week (again, hopefully, but no promises yet! Then on to cfpreferences/userpreferences...)

latenitefilms commented 4 years ago

Awesome - I just updated CommandPost to use 0.7.5.2, so I'll let you know if we spot any issues.

FWIW, we're not using :matches(), elementSearch or getAllChildElements currently in CommandPost for all the original performance issues we discussed. However, the techniques we are currently using seem to be working pretty well.

asmagill commented 4 years ago

@latenitefilms I'm trying to pair down axuielement to its essentials so it can be added to core and we can start seeing what it can be used to make more responsive (e.g. hs.application:getMenuItems)

The following are the current methods in the module which can be particularly slow/problematic/hard-to-use and should either be replaced with more responsive versions themselves or culled entirely if they're not really as useful as once thought. Can you tell me if CommandPost is currently using any of these?

hs._asm.axuielement:getAllChildElements hs._asm.axuielement:matches hs._asm.axuielement:elementSearch hs._asm.axuielement:buildTree hs._asm.axuielement:matchesCriteria hs._asm.axuielement:searchPath hs._asm.axuielement:next

edited to reflect the ones you've already said you're not using

latenitefilms commented 4 years ago

We're using buildTree for some debugging code. Not a deal breaker if this disappeared.

Not currently using matchesCriteria, searchPath or next.

We doing most of our AX management using cp.ui.axutils.

asmagill commented 4 years ago

I can probably refactor buildTree first... I used it in some debugging as well... syntax may change slightly, though. I'll keep you posted.

asmagill commented 4 years ago

@latenitefilms

Ok, I'm about ready to upload a paired down version of axuielement that has all of the core functions (observer is untouched) but without the methods listed above, except for buildTree.

buildTree has been changed; the syntax is now hs._asm.axuielement:buildTree(callback, [depth], [withParents]) -- the callback is required and the method utilizes coroutines to keep Hammerspoon responsive while building up the tree structure.

It returns a table as an object that allows you to cancel and check if its still running... e.g.

ax = require("hs._asm.axuielement")
a = ax.applicationElement(hs.application("Safari"))
s = os.time()
t = a:buildTree(function(msg, results) r = results ; print(msg, os.time() - s) end, 10)

t:isRunning() -- will return true or false
t:cancel() -- will cancel the current run

The results parameter to the callback is the table you expect from the previous version; the msg parameter will be "completed" when buildTree is allowed to run to its normal end, either by finishing the build or by reaching the maximum depth specified. It will be " cancelled" if you issue the :cancel() method on the build object. Currently it will also trigger a cancel (with message " gc on buildTree object") if the build object (t in the above example) is collected, but I'm undecided on this...

Should the build object require you to capture it? If you don't capture it, then you can't cancel it, but as the a regular user of this method, which would you prefer? Requiring the capture of the buildObject to prevent collection, or allowing collection to occur but keeping the process running in the background, perhaps for a long time? (For "ballpark referencing", I currently have about 8 Safari windows open, and even limiting the depth to 10, it took a little over 2 minutes to complete -- of course this was from the application object itself, not a specific window or tab).

This is the model that I plan to use for the other search/filter methods that I re-add -- a callback, utilizing coroutines to keep Hamemrspoon responsive, with a returned object that allows you to cancel early, so your thoughts on __gc will probably be applied to them as well.


You don't have to read this, but if you care, here are my thoughts re __gc and cancelling/cleaning things up in general, not just here...

In general, I dislike things that require me to explicitly delete them; it defies the whole concept and benefit of garbage collection and means I have to worry about cleaning up after myself... yes, this means that I dislike the model used for hs.canvas (and previously hs.drawing) but kept it when we migrated to canvas because it was what was expected of the drawing replacement.

On the other hand, we get a lot of questions like "why did my timer stop working?" from people who probably don't come from a traditional programming background, and I can understand why someone might not want to keep a whole lot of extra variables (global or up-value captured local) around for the drawing elements that "I want to always be there"...

So, I understand. I dislike, but I understand.

Which is why I'm asking for your (and other CommandPost developers) input regarding the garbage collection of these builder/searcher/filter objects.

asmagill commented 4 years ago

@latenitefilms when you get a chance, if you could confirm the 7.6 build doesn't break anything of yours, I'd like to start prepping it for inclusion in core before messing with the other potential search/filter methods. What's present is sufficient to start replacing things like hs.application.getMenus with more responsive versions, so I'd like to get that in there as well as an example for others.

Do note that hs._asm.axuielement:buildTree has changed syntax slightly... it now requires a callback function, but I opted not to have it cancel itself on garbage collection (as described in my previous comment)... I'm still undecided long term, but for now it seems like the least change to backwards compatibility is probably best.

latenitefilms commented 4 years ago

Awesome! Will check and respond to all of the above later today.

latenitefilms commented 4 years ago

Sorry for the delayed reply. v7.6 seems to work fine here!

asmagill commented 4 years ago

discussion re changes/additions now occurring at https://github.com/Hammerspoon/hammerspoon/pull/2373