Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Final Cut Pro Font actions broken #3212

Closed
latenitefilms opened this issue May 8, 2023 · 9 comments
Closed

Final Cut Pro Font actions broken #3212

latenitefilms opened this issue May 8, 2023 · 9 comments
Assignees
Labels
Milestone

Comments

@latenitefilms
Copy link
Contributor

latenitefilms commented May 8, 2023

Reported by Daniel Malferrari on Facebook.

Error Message Displayed: Failed to get Font Family UI: nil

image

The label and reset button seem to work fine - it's just the family and typeface that are failing:

image

Interestingly, the row does contain the right UI items, so it looks like it's failing on childFromRight(row, 2, PopUpButton.matches).

image

@latenitefilms latenitefilms added this to the 1.4.18 milestone May 8, 2023
@latenitefilms latenitefilms self-assigned this May 8, 2023
@latenitefilms
Copy link
Contributor Author

I think there's something funky happening with:

image

@randomeizer
Copy link
Contributor

Hmm, axutil.children looks pretty solid to me. A lot of other things would break if that was buggy.

A guess off the top of my head is that the order/structure of the missing fields has changed at some point? Sometimes PopupButtons get switched to MenuButtons by Apple between versions, or they add an extra UI element, or there is something that is only visible sometimes and not others.

@latenitefilms
Copy link
Contributor Author

@randomeizer - The interesting thing is that font row is returning the correct results that we expect:

image

What's failing is childFromRight(row, 2, PopUpButton.matches), which is bizarre because if we do row:children()[2] it works.

PopUpButton.matches also works as expected:

> cp.ui.PopUpButton.matches(cp.apple.finalcutpro.inspector.text:basic():font():children()[2])
true

...so it's something wrong with childFromRight(). However, childFromRight() is very simple:

--- cp.ui.axutils.childFromRight(element, index[, matcherFn]) -> axuielement
--- Function
--- Searches for the child element which is at number `index` when sorted right-to-left.
---
--- Parameters:
---  * element      - the axuielement or array of axuielements
---  * index        - the index number of the child to find.
---  * matcherFn    - an optional function which is passed each child and returns `true` if the child should be processed.
---
--- Returns:
---  * The child, or `nil` if the index is larger than the number of children.
function axutils.childFromRight(element, index, matcherFn)
    return axutils.childAtIndex(element, index, axutils.compare.rightToLeft, matcherFn)
end

...it just uses childAtIndex():

--- cp.ui.axutils.childAtIndex(element, index, compareFn[, matcherFn]) -> axuielement
--- Function
--- Searches for the child element which is at number `index` when sorted using the `compareFn`.
---
--- Parameters:
---  * element      - the axuielement or array of axuielements
---  * index        - the index number of the child to find.
---  * compareFn    - a function to compare the elements.
---  * matcherFn    - an optional function which is passed each child and returns `true` if the child should be processed.
---
--- Returns:
---  * The child, or `nil` if the index is larger than the number of children.
function axutils.childAtIndex(element, index, compareFn, matcherFn)
    if element and index > 0 then
        local children = axutils.children(element)
        if children then
            if matcherFn then
                children = axutils.childrenMatching(children, matcherFn)
            end
            if #children >= index then
                sort(children, compareFn)
                return children[index]
            end
        end
    end
    return nil
end

So I think the problem is in local children = axutils.children(element).

image

Maybe related to #3200 - i.e. self:app().preferences.prop("FFPlayerBackground") is no longer working, but self:app().preferences:prop("FFPlayerBackground") does?

Maybe some kind of change upstream in Hammerspoon?

@randomeizer
Copy link
Contributor

It could be some leftover old code from before we switched the hs.ax library to the official version?

@latenitefilms
Copy link
Contributor Author

Basically, the problem is that every cp.ui.Element object has a attributeValue property.

--- cp.ui.Element:attributeValue(id) -> anything, true | nil, false
--- Method
--- Attempts to retrieve the specified `AX` attribute value, if the `UI` is available.
---
--- Parameters:
---  * id - The `AX` attribute to retrieve.
---
--- Returns:
---  * The current value for the attribute, or `nil` if the `UI` is not available, followed by `true` if the `UI` is present and was called.
function Element:attributeValue(id)
    local ui = self:UI()
    if ui then
        return ui:attributeValue(id), true
    end
    return nil, false
end

This means that when we pass in our row, rather than it using row:children() (which has the correct rows), it's using row:attributeValue("AXChildren") - which means we get an empty table as the result.

--- cp.ui.axutils.children(element[, compareFn]) -> table
--- Function
--- Finds the children for the element. If it is an `hs.axuielement`, it will
--- attempt to get the `AXChildren` attribute. If it is a table with a `children` function,
--- that will get called. If no children exist, an empty table will be returned.
---
--- Parameters:
---  * element      - The element to retrieve the children of.
---  * compareFn    - Optional function to use to sort the order of the returned children.
---
--- Returns:
---  * a table of children
function axutils.children(element, compareFn)
    local children = element
    --------------------------------------------------------------------------------
    -- Try to get the children array directly, if present, to optimise the loop.
    --
    -- NOTE: There seems to be some weirdness with some elements coming from
    --       `axuielement` without the correct metatable.
    --------------------------------------------------------------------------------
    if element and element.attributeValue then
        --------------------------------------------------------------------------------
        -- It's an AXUIElement:
        --------------------------------------------------------------------------------
        children = element:attributeValue("AXChildren") or element
    elseif element and is.callable(element.children) then
        children = element:children()
    end

     if type(children) == "table" then
        if type(compareFn) == "function" then
            sort(children, compareFn)
        end
        return children
    end
    return {}
end

The tricky part is knowing at which point we broke things, as cp.ui.Element:attributeValue has been there for at least two years, and you would have thought that someone would have complained about things not working at some point.

@randomeizer
Copy link
Contributor

Hmm. Interesting...

A possible fix could be to flip the order of the check - check for children function first, and then check for attributeValue, on the assumption that children will be a more specialised implementation.

@randomeizer
Copy link
Contributor

randomeizer commented May 8, 2023

So, this implementation seems to work for me:

--- cp.ui.axutils.children(element[, compareFn]) -> table
--- Function
--- Finds the children for the element. If it is an `hs.axuielement`, it will
--- attempt to get the `AXChildren` attribute. If it is a table with a `children` function,
--- that will get called. If no children exist, an empty table will be returned.
---
--- Parameters:
---  * element      - The element to retrieve the children of.
---  * compareFn    - Optional function to use to sort the order of the returned children.
---
--- Returns:
---  * a table of children
function axutils.children(element, compareFn)
    local children = element

    if element and is.callable(element.children) then
        --------------------------------------------------------------------------------
        -- There is a `children` function, priorise that.
        --------------------------------------------------------------------------------
        children = element:children()
    elseif element and element.attributeValue then
        --------------------------------------------------------------------------------
        -- It's an AXUIElement:
        --------------------------------------------------------------------------------
        children = element:attributeValue("AXChildren") or element
    end

    if type(children) == "table" then
        if type(compareFn) == "function" then
            sort(children, compareFn)
        end
        return children
    end
    return {}
end

Searching through the codebase, very few elements implement a children function. It used to be that AXChildren was automatically created for AXUIelements, but that no longer happens. That basically leaves PropertyRow (our culprit here) and cp.apple.finalcutpro.timeline.Contents.

@randomeizer
Copy link
Contributor

In practical terms, it would have only broken for PropertyRow lookups like this one, which is probably why it hasn't really shown up too often - not too many values where we are using that filter to find specific children I guess.

@latenitefilms
Copy link
Contributor Author

@randomeizer - Would it be better for performance if we left cp.ui.axutils.children as-is, and modified cp.ui.PropertyRow instead?

For example, we could just add:

--- cp.ui.PropertyRow:attributeValue(id) -> anything, true | nil, false
--- Method
--- Attempts to retrieve the specified `AX` attribute value, if the `UI` is available.
---
--- Parameters:
---  * id - The `AX` attribute to retrieve.
---
--- Returns:
---  * The current value for the attribute, or `nil` if the `UI` is not available, followed by `true` if the `UI` is present and was called.
---
--- Notes:
---  * If the `id` is `AXChildren`, then we'll return a table of children for the Property Row.
function PropertyRow:attributeValue(id, ...)
    if id == "AXChildren" then
        return self:children()
    end

    local ui = self:UI()
    if ui then
        return ui:attributeValue(id), true
    end
    return nil, false
end

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants