Closed asmagill closed 4 years ago
That all makes sense to me!
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.
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!
@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
@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!
@asmagill - I've only just started playing - but it's AMAZING!!
searchPath is exactly what I needed. Thank you!
@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?
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.
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!
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!
@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!!
@asmagill - Absolutely no pressure what-so-ever, but just wondering if you've done any further work on axuielement recently?
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.
@latenitefilms I've made some updates to axuielement in preparation for getting it ready for migration. In this update, the main changes are:
setTimeout
element("property")
and property
is not defined for the element, it returns nil
now instead of an error. I still check against the existing list for actions and when setting a new value for an attribute since performing these doesn't return anything so trying to perform a non-existent action or changing a non-existent property would otherwise go unnoticed.doXXX
is considered an action only when XXX
starts with an upper case letter. This changes means ("dog")
will now be considered a request for a value while ("doPress")
will be considered an action... the downside now is that ("dopress")
will also be considered a request for a value, but (a) I don't think this feature is commonly used yet, and (b) the module isn't in core yet :-)setXXX
also requires that the first letter after set
be uppercase.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...)
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.
@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
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
.
I can probably refactor buildTree
first... I used it in some debugging as well... syntax may change slightly, though. I'll keep you posted.
@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.
@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.
Awesome! Will check and respond to all of the above later today.
Sorry for the delayed reply. v7.6 seems to work fine here!
discussion re changes/additions now occurring at https://github.com/Hammerspoon/hammerspoon/pull/2373
@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:
elementSearch
- a primarily lua based method which can take a list of criteria (or no criteria) and returns an array of all axuielements, starting with the initial node (often, but not necessarily, a window or application) and traversing from that point down through the accessibility hierarchy. Accessibility Elements which match the criteria (or all elements, if no criteria is specified) are included in the array in the order they are discovered. Discovery occurs in a depth first manner.getAllChildElements
- an objective-c method which takes no criteria and returns all "child" accessibility elements starting with the initial node. Similar toelementSearch
when no criteria is specified.elementAtPosition
andsystemElementAtPosition
- these each return 1 (or 0) accessibility element, the topmost element (of the application forelementAtPosition
, or the topmost visible forsystemElementAtPosition
) whose boundaries encompass the point specified.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 withelementSearch
'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
(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?