tarnish crashes when built using Toltec’s toolchain

Describe the bug
When built using Toltec’s qt:v1.1 image, tarnish crashes with the following output:
Creating DBusService instance
Looking for wireless devices...
  Checking lo...
    Not a wireless device
  Checking sit0...
    Not a wireless device
  Checking usb0...
    Not a wireless device
  Checking wlan0...
    Wireless device found!
Loading networks from settings...
Registering networks...
  Found network "Freebox_188"
  Found network "Freebox_188_5GHz"
Loaded networks.
Turning wifi on
Reconnecting to wifi
Wifi API false
Looking for batteries and chargers...
  Checking bq27441-0...
    Found Battery!
  Checking imx_usb_charger...
    Unknown type
Can't find charger information
terminate called without an active exception
Aborted (core dumped)

To Reproduce

Spin a Docker container with the Toltec qt image: docker container run -it --rm ghcr.io/toltec-dev/qt:v1.1
Download oxide v2.0-beta:

curl -L https://github.com/Eeems/oxide/archive/v2.0-beta.zip -o oxide.zip
apt-get -yqq update && apt-get -yqq install unzip
unzip oxide.zip

Build oxide using the linux-arm-gnueabihf-g++ spec file.

sed -i 's/linux-oe-g++/linux-arm-gnueabihf-g++/' applications/*/*.pro
make release

Transfer the result to the reMarkable and run tarnish.

Expected behavior
No crash.
Screenshots
—
Version Information:

Device: reMarkable 1
OS: https://github.com/orgs/toltec-dev/packages/container/package/qt v1.1
Version v2.0-beta

Additional context
When building in debug mode (by passing CONFIG+=debug to qmake), the crash disappears. When building in release with debug symbols mode (with CONFIG+=release CONFIG+=force_debug_info), the crash comes back, and we can investigate further with gdb.
The problem may be thread-related, as terminate called without an active exception is the message given by the C++ standard library when a thread object that was not joined or detached gets destructed. But it could be something else.
When tracing the crash with gdb, it happens somewhere in the constructor of AppsAPI, but not always at the same line depending on whether you have an existing .config/Eeems/tarnish.conf file, which could indicate a race condition somewhere.
I’m unfortunately not able to switch threads with Entware’s gdb package to further investigate.
The only outstanding difference that I can see between the reMarkable-provided toolchain and the Toltec qt image is that the former contains gcc v7.3.0 and the later v8.3.0.

Checking imx_usb_charger...
    Unknown type
Can't find charger information

Well that's interesting, I wasn't seeing that on my device. This could be related to the issue. Looking at my code, I don't expect it to fail if it fails to detect a charger, but I'd like to be sure. @matteodelabre what does cat /sys/class/power_supply/imx_usb_charger/type return for you?

The problem may be thread-related, as terminate called without an active exception is the message given by the C++ standard library when a thread object that was not joined or detached gets destructed. But it could be something else.
When tracing the crash with gdb, it happens somewhere in the constructor of AppsAPI, but not always at the same line depending on whether you have an existing .config/Eeems/tarnish.conf file, which could indicate a race condition somewhere.
I’m unfortunately not able to switch threads with Entware’s gdb package to further investigate.

I do have a couple places that read from the settings file, I might need to look into moving it to a single instance as it could be a timing issue between the threads.

Checking imx_usb_charger...
    Unknown type
Can't find charger information

Well that's interesting, I wasn't seeing that on my device. This could be related to the issue. Looking at my code, I don't expect it to fail if it fails to detect a charger, but I'd like to be sure. @matteodelabre what does cat /sys/class/power_supply/imx_usb_charger/type return for you?

It reports USB_CDP.

@matteodelabre Are you using the cable that came with the device?

Yes I am. I also tested with other cables and it does not seem to make a difference.

Interesting. It's reporting as USB for me, which is what I'm keying off of. I'll have to add USB_CDP as well.

@matteodelabre would you be able to test against the v2.1 branch to see if this behaviour is still exhibited?

Did a quick bisect and it seems like you fixed the issue with commit cabf05a97. Now it runs perfectly even when compiled with Toltec’s toolchain. Thanks!

@matteodelabre Perfect!


setup travis with php lint for 5.3-7.3

refs:

https://github.com/zf1s/zend-view/pull/3
https://github.com/zf1s/zend-loader/pull/2

cc @falkenhawk

to be replaced with https://github.com/zf1s/zf1/pull/4


[docs] Document I/O errors

Some I/O errors can be half expected - for example "out of space" or "file does not exists". They should be documented.

Could you please provide more info on where or what APIs have this errors that are not documented?


chore: 🤖 add types for blockchain fields

This PR adds a new type Blockchain and assigns it to the blockchain fields in
all API responses for better autocomplete support.
(PS: I work at Ankr too xD, DevRel)

This PR adds a new type Blockchain and assigns it to the blockchain fields in all API responses for better autocomplete support.
(PS: I work at Ankr too xD, DevRel)

hi! thanks for pr. We have these types autogenerated from our Go structs, therefore to add the type I have to modify Go code in our internal repo first

@Dhaiwat10 found a way to autogenerate it like this https://github.com/Ankr-network/ankr.js/commit/af7d75ba5a41a3eadff4280561599af866cc527b

@romanankr awesome! :)


Removed Wire due to their shady pricacy practices

What: This PR removes Wire and Wire logo from How section.
Why: PrivacyTools.io just delisted Wire after discovering some very shady practices and new language in their Privacy policy after they got sold to a US company.
More on this: https://blog.privacytools.io/delisting-wire/

Yikes.
Thanks for the PR Artem!

Yikes indeed. I mean, I understand it's business and all, but I hate seeing great products being sold and basically turn their whole philosophy around :( It makes me sad


Hostname validator does not accept .name IDNs

The Hostname class keeps an internal list of IDNs (here) and .name is not on it
Example: nødtvedt.name (nødtvedt.name) is a valid TLD (ref: http://whois.domaintools.com/xn--ndtvedt-q1a.name) but Hostname does not accept it.

We need to run the update script.

name shows up in the $validTlds  list (here), which IIRC is where the update script would put it.  The problem is that it is not identified as a TLD which supports IDNA (validIdns list).  I did a quick search and could not find a definitive list of which characters are supported by .name, which would be necessary to add it to the validIdns list.

I've run the update script at this point, and that's on master; can you test, please, @adamlundrigan ?

@weierophinney that didn't fix it.  The issue isn't that it's missing from the list of valid TLDs, it's that we don't have it listed in the other list in this class (validIdns) which identifies the domains supporting IDNA.
If we can take this page on OpenSRS as authoritative source on the subject, then .name supports the same range of non-ASCII characters as .com.  That page also puts .cc and .tv in the same "category", and those are also missing from the validIdns list in this class.

@adamlundrigan Would you be willing to create a pull request to add these, then, since you seem to know what needs to be done? :smile:

Certainly :+1:

@weierophinney IANA publishes a "Repository of IDN Practices" which lists all valid characters for each IDN.  I can update the bin/update_hostname_validator.php script to spider those lists and compile the correct character ranges for each IDN.
If a change to validIdns causes a domain which was previously accepted by the Hostname validator to no longer be accepted, is that considered a BC break?  IMO it is, so for v2.x releases we'd need to retain the existing validIdns list as-is and append the auto-generated character lists to those.  For v3 we can drop the manually-curated list.

@adamlundrigan That sounds like a great approach!

The script won't be pretty but it will work ;)
How do you think we should store the data?  I see two options:

Importer converts each "table" (eg: https://www.iana.org/domains/idn-tables/tables/academy_hebr_1.0.txt) into a PHP file which returns an array of accepted code point regexes (similar to how it's done now).  The Hostname validator is updated to include the appropriate tables for each domain.

Build a single PHP file and for each "table" create a PHP array variable containing the code points, then return from the file a map with the TLD as the key and the array of code point regexes as the value.  Many TLDs share the same tables (eg: it looks like every "Donuts Inc" TLD references the ".academy" table files) so we can strip out a lot of duplication by having each of those TLDs reference the same array variable, named according to the table filename.

I'm on the side of (2) since (1) will create a mess of individual PHP files and a ton of include statements inside the Hostname class.

@adamlundrigan I'd opt for 2 as well; fewer includes is better.
Mention me in a comment on the PR once it's ready, or flag me as a reviewer.

I contacted IANA to confirm we could bundle the tables (we can, with attribution) and if they had a machine-readable index of the tables (they don't but would consider adding it).  They also warned against using the tables the way we do:

I would caution you to consider the appropriateness of using the tables for the purpose you are pursuing. The IDN table repository is not 100% comprehensive, insofar as it is not mandatory for all TLDs to register their tables in the resource. Therefore, if you use it as a mechanism to whitelist domains that support IDNs, you will find it likely will miss some cases.

Pulling in the tables that are available is still a big improvement over what we have in place currently so I will proceed.

@weierophinney I've been working on this a bit and it's quite a rabbit hole, so I suggest that the 2.10 release goes ahead without this.

Issues encountered:

Not all IANA tables are in the same format

IANA tables are large (compiling all codepoints into an array and serializing results in a ~25MB JSON file).  With extra work the downloader could condense the individual codepoints into ranges to reduce a lot of the bulk.

Not all information is encoded into the tables in machine-readable format.  For example, character location constraints are included as natural language comments:

# Code points: U+002D (HYPHEN-MINUS)
# Reference: RFC 5891 (sec <IP_ADDRESS>)
# Rules: Label must neither start nor end with U+002D. Label must not have U+002D in both third and fourth position.

I've been working on this a bit and it's quite a rabbit hole, so I suggest that the 2.10 release goes ahead without this.

Okay, will do! I'll shift this to the following bugfix release, if that's okay with you.


Wider items in feeds

Seems we have narrow preview on feeds
#1612
spec
Fix it where items are narrow, i.e gallery, collections, artists profile (all tabs incl), collection detail
doc
Comparing on beta

Vs on legacy

@yangwao I'll check this

@yangwao I'll check this


2.x fix metadata map copy in InstanceInfo.Builder

NetflixOSS » eureka » eureka-pull-requests #454 SUCCESS
This pull request looks good


feature request: schedule retweets

it would be great to be able to schedule retweets (not old-style RT quote).
there's two ways i do that today via Buffer, and that is via a chrome extension adding an option on twitter.com, and via sending an email f.ex from my mobile twitter client.

yes, this is my primary use of buffer, only i use the firefox extension


fix: onOptionChange on TreeSelects and fix Switch Toggle

Description
FIxes onOptionChange for TreeSelect and Multiselect
Fixes #9078
Fixes #9598
Type of change

Please delete options that are not relevant.

Bug fix (non-breaking change which fixes an issue)

How Has This Been Tested?

Please describe the tests that you ran to verify your changes. Provide instructions, so we can reproduce.
Please also list any relevant details for your test configuration.

Test A
Test B

Checklist:

[ ] My code follows the style guidelines of this project
[ ] I have performed a self-review of my own code
[ ] I have commented my code, particularly in hard-to-understand areas
[ ] I have made corresponding changes to the documentation
[ ] My changes generate no new warnings
[ ] I have added tests that prove my fix is effective or that my feature works
[ ] New and existing unit tests pass locally with my changes

Test coverage results :test_tube:
:white_circle: Total coverage has not changed
// Code coverage diff between base branch:release and head branch: fix/OnOptionChange-called-twice-in-Tree-select

Status
File
% Stmts
% Branch
% Funcs
% Lines

:green_circle:
app/client/src/widgets/SwitchWidget/component/index.tsx
93.75 (0.42)
62.5 (0)
50 (0)
93.75 (0.42)

/ok-to-test sha=76432cf

/ok-to-test sha=82c373d

/ok-to-test sha=82c373d

@Tooluloope Tested this PR and both the issues #9078 and #9598 are working as expected


Unexpected error with schemas

My endpoint attr
/// Login a user
///
/// This route will return a new token for the user, if the user does not exist, it will return an error.
/// To revoke a previous tokens, use the revoke route `/api/auth/revoke`.
#[utoipa::path(
    context_path = "/api/auth",
    request_body = crate::schemas::auth::LoginSchema,
    responses(
        (status = 200, description = "Login successfully and return a new token", body = crate::schemas::user::UserSchema),
        (status = 400, description = "The username or password is incorrect", body = crate::schemas::errors::ErrorSchema),
    ),
    tag = "auth"
)]
#[post("/login")]
pub async fn login(
    db: web::Data<DatabaseConnection>,
    payload: web::Json<LoginSchema>,
) -> impl Responder {
    // ...
}

My openapi struct
#[derive(OpenApi)]
#[openapi(
    paths(
        crate::auth::login::login
    ),
    components (
        schemas (
            crate::schemas::auth::LoginSchema,
            crate::schemas::errors::ErrorSchema,
            crate::schemas::user::UserSchema,
        )
    ),
    tags(
        (name = "auth", description = "A tag for authentication routes")
    ),
    modifiers(&SecurityAddon)
)]
pub struct ApiDoc;

My schemas
/// The schema for login request
#[derive(Debug, Serialize, Deserialize, ToSchema, Clone)]
pub struct LoginSchema {
    /// The username of the user
    #[schema(example = "Awiteb")]
    pub username: String,
    /// The password of the user
    #[schema(example = "123456")]
    pub password: String,
}

/// The schema for response error
#[derive(Debug, Serialize, Deserialize, ToSchema, Clone)]
pub struct ErrorSchema {
    /// The status code of the error
    #[schema(example = "400")]
    pub status: u16,
    /// The error message
    #[schema(example = "The username or password is incorrect")]
    pub message: String,
}

/// The schema of the user response
#[derive(Debug, Serialize, Deserialize, ToSchema, Clone)]
pub struct UserSchema {
    /// The name of the user
    #[serde(rename = "username")]
    #[schema(example = "Awiteb")]
    pub name: String,
    /// The token of the user
    #[schema(
        example = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdXN0IjoidGVzdCJ9.rCOJrqm0MQvxTcNyyXJdqI7smV7VNvAJZ02LslGkL78"
    )]
    pub token: String,
    #[serde(skip)]
    #[schema(hidden)]
    code: u16,
}

The errors
Errors

Resolver error at paths./api/auth/login.post.requestBody.content.application/json.schema.$ref
Could not resolve reference: Could not resolve pointer: /components/schemas/crate.schemas.auth.LoginSchema does not exist in document
Resolver error at paths./api/auth/login.post.responses.200.content.application/json.schema.$ref
Could not resolve reference: Could not resolve pointer: /components/schemas/crate.schemas.user.UserSchema does not exist in document
Resolver error at paths./api/auth/login.post.responses.400.content.application/json.schema.$ref
Could not resolve reference: Could not resolve pointer: /components/schemas/crate.schemas.errors.ErrorSchema does not exist in document

The request_body and body accepts path format to allow distinction for different objects which have same name. In order to make this work at the moment there is 2 options.

 components (
       schemas (
           crate::schemas::auth::LoginSchema,
           crate::schemas::errors::ErrorSchema,
           crate::schemas::user::UserSchema,
       )
   ),

// ........... later in code
#[utoipa::path(
   context_path = "/api/auth",
   request_body = crate::schemas::auth::LoginSchema,
   responses(
       (status = 200, description = "Login successfully and return a new token", body = crate::schemas::user::UserSchema),
       (status = 400, description = "The username or password is incorrect", body = crate::schemas::errors::ErrorSchema),
   ),
   tag = "auth"
)]

Option, either define schemas with as` keyword e.g:

        schemas (
            crate::schemas::auth::LoginSchema as crate::schemas::user::LoginSchema,
            crate::schemas::errors::ErrorSchema as crate::schemas::errors::ErrorSchema,
            crate::schemas::user::UserSchema as crate::schemas::user::UserSchema ,
        )

Option, use direct names when defining response body or request_body e.g:

    request_body = LoginSchema,
    responses(
        (status = 200, description = "Login successfully and return a new token", body = UserSchema),

This is indeed not well documented behavior, and should most likely be revised a bit. Perhaps the whole as statement should be removed from the schemas and at least should be better documented indeed.

When using the utoipa::path macro, the request_body and body in responses have to be the name that appears in the openapi document. So since the three schemas in your error are LoginSchema, ErrorSchema, and UserSchema, you would reference them as such in the utoipa::path macro rather than the path that the rust crate uses.
/// Login a user
///
/// This route will return a new token for the user, if the user does not exist, it will return an error.
/// To revoke a previous tokens, use the revoke route `/api/auth/revoke`.
#[utoipa::path(
    context_path = "/api/auth",
    request_body = LoginSchema,
    responses(
        (status = 200, description = "Login successfully and return a new token", body = UserSchema),
        (status = 400, description = "The username or password is incorrect", body = ErrorSchema),
    ),
    tag = "auth"
)]
#[post("/login")]
pub async fn login(
    db: web::Data<DatabaseConnection>,
    payload: web::Json<LoginSchema>,
) -> impl Responder {
    // ...
}

Hope this helps. I am fairly new to utoipa myself so this is just my understanding of it.

There is a new functionality coming in new release 3.0.0 described here https://github.com/juhaku/utoipa/issues/435#issuecomment-1405448818. It is also documented better in the docs as well.


reconnection possible bug ?

hello guys
i was looking at https://github.com/supabase/realtime-js/blob/master/src/RealtimeSubscription.ts source code and i have this doubt.
here
https://github.com/supabase/realtime-js/blob/10228a3d7cb87897751952dfd2f39d0a09f4cdac/src/RealtimeSubscription.ts#L49
when the realtime subscription got an error its trigger the reconnection timer, but timer callback is asking here
https://github.com/supabase/realtime-js/blob/10228a3d7cb87897751952dfd2f39d0a09f4cdac/src/RealtimeSubscription.ts#L64
for this.socket.isConnected(), what if the socket is not connected ? then the realtime subscription it's left on a broken state ? ( like not receiving any more updates from server till subscription is restarted by client code ?)
much thanks in advance

what if the socket is not connected ?

@smarcet when the socket is not properly disconnected then it'll continuously try to reconnect, and once this is successful, then each subscription will then be able to rejoin. You can take a look at https://github.com/supabase/realtime-js/blob/702594effeeb94e106c7f9518b1366ff6b89a12a/src/RealtimeClient.ts#L388.


Add client functions for comment and attachment APIs

Adds the following functions:

Create a new comment on an Issue
Edit and delete comments
Get attachments on a comment
Edit and delete attachments
Upload a new attachment to a comment

One caveat is that because Attachment objects don't carry all the information needed to edit or delete them, they are edited and deleted via Comment.edit_attachment and Comment.delete_attachment instead.

Can we add a link to the API docs for each implementation in the client? :)

Can we add a link to the API docs for each implementation in the client? :)

Added them.


Fonts with scale are drawn incorrectly

Hello!
The following code (with the extra invalidateHierarchy call at TextraLabel.layout, from https://github.com/tommyettinger/textratypist/issues/13#issuecomment-1927853865):
package com.mygdx.game;

import com.badlogic.gdx.ApplicationAdapter;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.scenes.scene2d.Stage;
import com.badlogic.gdx.scenes.scene2d.ui.Skin;
import com.badlogic.gdx.scenes.scene2d.ui.Stack;
import com.badlogic.gdx.scenes.scene2d.ui.Table;
import com.badlogic.gdx.utils.Align;
import com.badlogic.gdx.utils.ScreenUtils;
import com.badlogic.gdx.utils.viewport.ScreenViewport;
import com.github.tommyettinger.textra.TextraLabel;

public class MyGdxGame extends ApplicationAdapter {

  private Stage stage;
  private Skin skin;

  @Override
  public void create () {
    stage = new Stage(new ScreenViewport());

    skin = new Skin(Gdx.files.internal("uiskin.json"));

    skin.getFont("default-font").getData().setScale(2);

    TextraLabel label = new TextraLabel("This is a test. This is a test. This is a test. This is a test. This is a test.", skin) {
      @Override
      public void layout() {
        float width = getWidth();
        if (style != null && style.background != null) {
          width = (width - (style.background.getLeftWidth() + style.background.getRightWidth()));
        }
        if (wrap && layout.getTargetWidth() != width) {
          if(width != 0)
            layout.setTargetWidth(width);
          font.regenerateLayout(layout);
          invalidateHierarchy(); // Uncomment this line to fix the issue.
        }
      }
    };
    label.setWrap(true);
    label.setAlignment(Align.left);

    Table table = new Table(skin);
    table.setBackground("default-pane");
    table.add(label).prefWidth(300).row();

    Table wrappingTable = new Table();
    wrappingTable.add(table).row();
    wrappingTable.add().growY();

    Stack stack = new Stack(wrappingTable);
    stack.setFillParent(true);

    stage.addActor(stack);
  }

  @Override
  public void render () {
    ScreenUtils.clear(0, 0, 0, 1);
    stage.act();
    stage.draw();
  }

  @Override
  public void dispose () {
    stage.dispose();
    skin.dispose();
  }
}

Will result in:

Without setting the font scale, the label is drawn correctly:

The example above is using libGDX 1.12.2-SNAPSHOT and textralabel 6406d6f093 snapshot.
This also happens to other fonts and also when setting a scale < 1. For example, without scaling:

With scale of 0.5f:

Let me know if you want me to share the second example resources or if you need more details.
Regards.

I'll look into this now. The whole mess with when invalidating is good, or expected, or bad, or forbidden, is a mystery to me. Not to mention how invalidate(), invalidateHierarchy(), pack(), layout(), etc. are different...

@lucas-kakele You seem to be totally right that invalidateHierarchy() in TextraLabel.layout really helps the line positions. That fix is in now; I may find some other way to make it work, but your fix seems to be the best at this point in time. Once TextraLabels looked more reasonable, I tracked the scale issue to a different area: new Font(BitmapFont) currently doesn't handle scaled BitmapFonts very well. You can pass an unscaled BitmapFont to the constructor now, then call Font.scale(float, float) or one of the similar methods; this works in the latest code. For scaled BitmapFonts to carry over their scaling to Font, I'll have to dig deeper to figure out what changes in BitmapFont when one of those gets scaled...

Thanks @tommyettinger!
Using an unscaled BitmapFont and then calling Font.scale worked for me.
Regards.

OK, almost a year later, but I think this is resolved sufficiently now, heh. I need to clean up old issues more frequently...


identifiers: error validation

closes https://github.com/inveniosoftware/invenio-app-rdm/issues/961

Resolve the conflict and we can merge.

Rebased


vLLM request parameters constraint_mode and constraint

Introduced generic vLLM request parameters constraint_mode and constraint to replace json_schema and regex.
The main reason for the change is to allow for adding an arbitrary number of grammars without having to add new parameters to the vLLM API every time. It also reduces the probability of a future collision with an original vLLM request parameter name.
It also reduces confusion due to naming, like cfg meaning both "configuration" and "Context-Free Grammar", which was confusing at least to me. (This is for the upcoming PR once we have the full Lark grammar support ready.)
Kept the old json_schema and regex parameters for backwards compatibility, but we should remove them soon.

We could also name these constraint_mode and constraint, then use "grammar" as a constraint_mode value for the full Lark grammar. What do you think about these names?

@rlouf Could you please review this? Thanks!

I find this naming less explicit than the current one. It may be confusing.

Fair enough. We'll see whether this will still be the case after adding the 5th logits processor.

We'll cross that bridge when we come to it 😁


⚠ Bofrost.es has degraded performance

In b22d0c1, Bofrost.es (https://www.bofrost.es) experienced degraded performance:

HTTP code: 200
Response time: 1635 ms

Resolved: Bofrost.es performance has improved in 307a8de after 7 minutes.


screen should be freezed when taking the screenshot

Imagine you want to take a screenshot of a live video that can't be paused. It would be nice to "pause" everything on the screen when you are taking the screen.

Yes, I was thinking about such feature, would also be helpful for making screenshots of popups that would go away when you click other window. Basically, it would capture whole screen on run and allow user to select any range.

@joeyave, feature implemented. You can add --freeze command line option to take a screenshot immediately after starting the tool. All other experience (selecting region/window remain unchanged).

Thanks, will check it out!


Run mypy check from osbuild ci container

Currently we are installing mypy via pip, but we should be running our tools from a container so we control when they are updated. This is in order to guarantee that the behaviour between is stable between runs.

This is fixed on main.


InteractiveHighlighting should know when its highlight is active

From https://github.com/phetsims/scenery-phet/issues/815, @jessegreenberg and I realized that a workaround added for a GroupSortInteraction was incomplete, and it would be best to be able to know when interactive highlights were enabled for a given Node. This is mostly helpful as we continue to encounter and prioritize mouse+keyboard interaction and the combination in between.
I'll take a first pass and see how it goes.

Looks like we have a function isInteractiveHighlightActivated that can be removed because we should be able to use our new Property fort his. I'll use the same naming.

Just starting the investigation, mostly just questions.

Subject: [PATCH] add a couple more pageload connector for testing, https://github.com/phetsims/phet-io-website/issues/130
---
Index: js/accessibility/voicing/InteractiveHighlighting.ts
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/js/accessibility/voicing/InteractiveHighlighting.ts b/js/accessibility/voicing/InteractiveHighlighting.ts
--- a/js/accessibility/voicing/InteractiveHighlighting.ts	(revision 992ac9fc8797f7a200a426424d77c1c9d2297d5d)
+++ b/js/accessibility/voicing/InteractiveHighlighting.ts	(date<PHONE_NUMBER>197)
@@ -13,6 +13,8 @@
 import { Highlight } from '../../overlays/HighlightOverlay.js';
 import TEmitter from '../../../../axon/js/TEmitter.js';
 import memoize from '../../../../phet-core/js/memoize.js';
+import TReadOnlyProperty from '../../../../axon/js/TReadOnlyProperty.js';
+import TinyProperty from '../../../../axon/js/TinyProperty.js';

 // constants
 // option keys for InteractiveHighlighting, each of these will have a setter and getter and values are applied with mutate()
@@ -61,7 +63,7 @@
     // If true, the highlight will be layerable in the scene graph instead of drawn
     // above everything in the HighlightOverlay. If true, you are responsible for adding the interactiveHighlight
     // in the location you want in the scene graph. The interactiveHighlight will become visible when
-    // this.interactiveHighlightActivated is true.
+    // this.isInteractiveHighlightActiveProperty is true.
     private _interactiveHighlightLayerable = false;

     // If true, the highlight will be displayed on activation input. If false, it will not and we can remove listeners
@@ -71,6 +73,9 @@
     // Emits an event when the interactive highlight changes for this Node
     public interactiveHighlightChangedEmitter: TEmitter = new TinyEmitter();

+    public readonly isInteractiveHighlightActiveProperty: TReadOnlyProperty<boolean>;
+    private readonly _isInteractiveHighlightActiveProperty = new TinyProperty( false );
+
     // When new instances of this Node are created, adds an entry to the map of Displays.
     private readonly _changedInstanceListener: ( instance: Instance, added: boolean ) => void;

@@ -112,6 +117,8 @@
         cancel: boundPointerCancel,
         interrupt: boundPointerCancel
       };
+
+      this.isInteractiveHighlightActiveProperty = this._isInteractiveHighlightActiveProperty;
     }

     /**
@@ -216,6 +223,8 @@
     /**
      * Returns true if this Node is "activated" by a pointer, indicating that a Pointer is over it
      * and this Node mixes InteractiveHighlighting so an interactive highlight should surround it.
+     * TODO: delete? Has no usages anyways. https://github.com/phetsims/scenery/issues/1602
+     * TODO: This algorithm didn't account for the locked pointer, so it could have been incorrect. https://github.com/phetsims/scenery/issues/1602
      */
     public isInteractiveHighlightActivated(): boolean {
       let activated = false;
@@ -284,6 +293,7 @@

           // only set focus if current Pointer focus is not defined (from a more descendant Node)
           if ( display.focusManager.pointerFocusProperty.value === null ) {
+            // TODO: I'm confused, why is this setting focus for the ancestor, https://github.com/phetsims/scenery/issues/1602
             display.focusManager.pointerFocusProperty.set( new Focus( display, rootToGroupNode ) );
           }
         }
@@ -364,6 +374,7 @@
       const displays = Object.values( this.displays );
       for ( let i = 0; i < displays.length; i++ ) {
         const display = displays[ i ];
+        // TODO: nested loop to make sure that other displays aren't showing this Node? Oh boy, https://github.com/phetsims/scenery/issues/1602
         display.focusManager.pointerFocusProperty.set( null );

         // An exit event may come from a Node along the trail becoming invisible or unpickable. In that case unlock
@@ -425,6 +436,7 @@
       const displays = Object.values( this.displays );
       for ( let i = 0; i < displays.length; i++ ) {
         const display = displays[ i ];
+        // TODO: why does this not have to check to see if this Node was the lockedPointer? https://github.com/phetsims/scenery/issues/1602
         display.focusManager.lockedPointerFocusProperty.value = null;

         // Unlink the listener that was watching for the lockedPointerFocusProperty to be cleared externally

I've gotten to a pretty good spot, and I'm ready to check in with @jessegreenberg. Basically we had discussed an algorithm that looked at each time InterativeHighlighting set the locked or pointer focusProperty of a display, and we can still do that if that is going to be the best path forward, but to start I added a new listener to those Properties when a display is added to our local map. It is working, but I haven't tested it thoroughly (and actually wanted to see if @jessegreenberg had some ideas about how to test edge cases).
I'll reach out on slack to see if we can review together.

Subject: [PATCH] fdasf
---
Index: center-and-variability/js/median/view/InteractiveCardNodeContainer.ts
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/center-and-variability/js/median/view/InteractiveCardNodeContainer.ts b/center-and-variability/js/median/view/InteractiveCardNodeContainer.ts
--- a/center-and-variability/js/median/view/InteractiveCardNodeContainer.ts	(revision a5f942a9842b7a9f7fd47e0f31ec6528d9e70e1f)
+++ b/center-and-variability/js/median/view/InteractiveCardNodeContainer.ts	(date<PHONE_NUMBER>815)
@@ -124,6 +124,10 @@
       sortingRangeProperty: sortingRangeProperty
     } );

+    this.isInteractiveHighlightActiveProperty.lazyLink( active => {
+      console.log( 'active', active );
+    } );
+

     this.cardMap.forEach( ( cardNode, cardModel ) => {
       // Update the position of all cards (via animation) whenever any card is dragged
Index: scenery/js/accessibility/voicing/InteractiveHighlighting.ts
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/scenery/js/accessibility/voicing/InteractiveHighlighting.ts b/scenery/js/accessibility/voicing/InteractiveHighlighting.ts
--- a/scenery/js/accessibility/voicing/InteractiveHighlighting.ts	(revision 5908cf546bfb26479822d6c24c612a7c73400954)
+++ b/scenery/js/accessibility/voicing/InteractiveHighlighting.ts	(date<PHONE_NUMBER>665)
@@ -13,6 +13,8 @@
 import { Highlight } from '../../overlays/HighlightOverlay.js';
 import TEmitter from '../../../../axon/js/TEmitter.js';
 import memoize from '../../../../phet-core/js/memoize.js';
+import TReadOnlyProperty from '../../../../axon/js/TReadOnlyProperty.js';
+import TinyProperty from '../../../../axon/js/TinyProperty.js';

 // constants
 // option keys for InteractiveHighlighting, each of these will have a setter and getter and values are applied with mutate()
@@ -61,7 +63,7 @@
     // If true, the highlight will be layerable in the scene graph instead of drawn
     // above everything in the HighlightOverlay. If true, you are responsible for adding the interactiveHighlight
     // in the location you want in the scene graph. The interactiveHighlight will become visible when
-    // this.interactiveHighlightActivated is true.
+    // this.isInteractiveHighlightActiveProperty is true.
     private _interactiveHighlightLayerable = false;

     // If true, the highlight will be displayed on activation input. If false, it will not and we can remove listeners
@@ -71,6 +73,9 @@
     // Emits an event when the interactive highlight changes for this Node
     public interactiveHighlightChangedEmitter: TEmitter = new TinyEmitter();

+    public readonly isInteractiveHighlightActiveProperty: TReadOnlyProperty<boolean>;
+    private readonly _isInteractiveHighlightActiveProperty = new TinyProperty( false );
+
     // When new instances of this Node are created, adds an entry to the map of Displays.
     private readonly _changedInstanceListener: ( instance: Instance, added: boolean ) => void;

@@ -86,6 +91,7 @@
     // Input listener that locks the HighlightOverlay so that there are no updates to the highlight
     // while the pointer is down over something that uses InteractiveHighlighting.
     private readonly _pointerListener: TInputListener;
+    private readonly _boundHandleHighlightActiveChange: () => void;

     public constructor( ...args: IntentionalAny[] ) {
       super( ...args );
@@ -103,6 +109,7 @@

       this._interactiveHighlightingEnabledListener = this._onInteractiveHighlightingEnabledChange.bind( this );
       this._boundPointerFocusClearedListener = this.handleLockedPointerFocusCleared.bind( this );
+      this._boundHandleHighlightActiveChange = this.handleHighlightActiveChange.bind( this );

       const boundPointerReleaseListener = this._onPointerRelease.bind( this );
       const boundPointerCancel = this._onPointerCancel.bind( this );
@@ -112,6 +119,8 @@
         cancel: boundPointerCancel,
         interrupt: boundPointerCancel
       };
+
+      this.isInteractiveHighlightActiveProperty = this._isInteractiveHighlightActiveProperty;
     }

     /**
@@ -216,14 +225,24 @@
     /**
      * Returns true if this Node is "activated" by a pointer, indicating that a Pointer is over it
      * and this Node mixes InteractiveHighlighting so an interactive highlight should surround it.
+     * If you come to desire this function, instead you should use isInteractiveHighlightActiveProperty.
      */
-    public isInteractiveHighlightActivated(): boolean {
+    private isInteractiveHighlightActivated(): boolean {
       let activated = false;

       const trailIds = Object.keys( this.displays );
       for ( let i = 0; i < trailIds.length; i++ ) {
-        const pointerFocus = this.displays[ trailIds[ i ] ].focusManager.pointerFocusProperty.value;
-        if ( pointerFocus && pointerFocus.trail.lastNode() === this ) {
+        const display = this.displays[ trailIds[ i ] ];
+        const pointerFocus = display.focusManager.pointerFocusProperty.value;
+        const lockedPointerFocus = display.focusManager.lockedPointerFocusProperty.value;
+        // TODO: Is this the right way to account for the locked pointer (locked takes precedent). https://github.com/phetsims/scenery/issues/1602
+        if ( lockedPointerFocus ) {
+          if ( lockedPointerFocus?.trail.lastNode() === this ) {
+            activated = true;
+            break;
+          }
+        }
+        else if ( pointerFocus?.trail.lastNode() === this ) {
           activated = true;
           break;
         }
@@ -250,12 +269,7 @@
       const trailIds = Object.keys( this.displays );
       for ( let i = 0; i < trailIds.length; i++ ) {
         const display = this.displays[ trailIds[ i ] ];
-
-        if ( display.focusManager.lockedPointerFocusProperty.hasListener( this._boundPointerFocusClearedListener ) ) {
-          display.focusManager.lockedPointerFocusProperty.unlink( this._boundPointerFocusClearedListener );
-        }
-
-        display.focusManager.pointerHighlightsVisibleProperty.unlink( this._interactiveHighlightingEnabledListener );
+        this.onDisplayRemoved( display, true );
         delete this.displays[ trailIds[ i ] ];
       }

@@ -284,6 +298,7 @@

           // only set focus if current Pointer focus is not defined (from a more descendant Node)
           if ( display.focusManager.pointerFocusProperty.value === null ) {
+            // TODO: I'm confused, why is this setting focus for the ancestor, https://github.com/phetsims/scenery/issues/1602
             display.focusManager.pointerFocusProperty.set( new Focus( display, rootToGroupNode ) );
           }
         }
@@ -364,6 +379,7 @@
       const displays = Object.values( this.displays );
       for ( let i = 0; i < displays.length; i++ ) {
         const display = displays[ i ];
+        // TODO: nested loop to make sure that other displays aren't showing this Node? Oh boy, https://github.com/phetsims/scenery/issues/1602
         display.focusManager.pointerFocusProperty.set( null );

         // An exit event may come from a Node along the trail becoming invisible or unpickable. In that case unlock
@@ -414,6 +430,47 @@
       }
     }

+    private onDisplayAdded( display: Display ): void {
+
+      [ display.focusManager.pointerFocusProperty,
+        display.focusManager.lockedPointerFocusProperty,
+        display.focusManager.pointerHighlightsVisibleProperty
+      ].forEach( property => {
+        // TODO: lazy link? https://github.com/phetsims/scenery/issues/1602
+        !property.hasListener( this._boundHandleHighlightActiveChange ) && property.link( this._boundHandleHighlightActiveChange );
+      } );
+
+      // Listener may already by on the display in cases of DAG, only add if this is the first instance of this Node
+      if ( !display.focusManager.pointerHighlightsVisibleProperty.hasListener( this._interactiveHighlightingEnabledListener ) ) {
+        display.focusManager.pointerHighlightsVisibleProperty.link( this._interactiveHighlightingEnabledListener );
+      }
+    }
+
+    private onDisplayRemoved( display: Display, unlink = true ): void {
+
+      [ display.focusManager.pointerFocusProperty,
+        display.focusManager.lockedPointerFocusProperty,
+        display.focusManager.pointerHighlightsVisibleProperty
+      ].forEach( property => {
+        property.hasListener( this._boundHandleHighlightActiveChange ) && property.unlink( this._boundHandleHighlightActiveChange );
+      } );
+
+      if ( unlink ) {
+
+        // Pointer focus was locked due to interaction with this listener, but unlocked because of other
+        // scenery-internal listeners. But the Property still has this listener so it needs to be removed now.
+        if ( display.focusManager.lockedPointerFocusProperty.hasListener( this._boundPointerFocusClearedListener ) ) {
+          display.focusManager.lockedPointerFocusProperty.unlink( this._boundPointerFocusClearedListener );
+        }
+
+        display.focusManager.pointerHighlightsVisibleProperty.unlink( this._interactiveHighlightingEnabledListener );
+      }
+    }
+
+    private handleHighlightActiveChange(): void {
+      this._isInteractiveHighlightActiveProperty.value = this.isInteractiveHighlightActivated();
+    }
+
     /**
      * When a Pointer goes up after going down on this Node, signal to the Displays that the pointerFocusProperty no
      * longer needs to be locked.
@@ -425,6 +482,7 @@
       const displays = Object.values( this.displays );
       for ( let i = 0; i < displays.length; i++ ) {
         const display = displays[ i ];
+        // TODO: why does this not have to check to see if this Node was the lockedPointer? https://github.com/phetsims/scenery/issues/1602
         display.focusManager.lockedPointerFocusProperty.value = null;

         // Unlink the listener that was watching for the lockedPointerFocusProperty to be cleared externally
@@ -537,29 +595,18 @@
       if ( added ) {
         this.displays[ instance.trail!.uniqueId ] = instance.display;

-        // Listener may already by on the display in cases of DAG, only add if this is the first instance of this Node
-        if ( !instance.display.focusManager.pointerHighlightsVisibleProperty.hasListener( this._interactiveHighlightingEnabledListener ) ) {
-          instance.display.focusManager.pointerHighlightsVisibleProperty.link( this._interactiveHighlightingEnabledListener );
-        }
+        this.onDisplayAdded( instance.display );
       }
       else {
         assert && assert( instance.node, 'should have a node' );
         const display = this.displays[ instance.trail!.uniqueId ];
+        // TODO: there was a truthy check on display, wat? https://github.com/phetsims/scenery/issues/1602

         // If the node was disposed, this display reference has already been cleaned up, but instances are updated
         // (disposed) on the next frame after the node was disposed. Only unlink if there are no more instances of
         // this node;
-        if ( display && instance.node!.instances.length === 0 ) {
-
-          // Pointer focus was locked due to interaction with this listener, but unlocked because of other
-          // scenery-internal listeners. But the Property still has this listener so it needs to be removed now.
-          if ( display.focusManager.lockedPointerFocusProperty.hasListener( this._boundPointerFocusClearedListener ) ) {
-            display.focusManager.lockedPointerFocusProperty.unlink( this._boundPointerFocusClearedListener );
-          }
-
-          display.focusManager.pointerHighlightsVisibleProperty.unlink( this._interactiveHighlightingEnabledListener );
-        }
-
+        const shouldUnlink = instance.node!.instances.length === 0;
+        this.onDisplayRemoved( display, shouldUnlink );
         delete this.displays[ instance.trail!.uniqueId ];
       }
     }

MK and I met to discuss. We are concerned by the listener count this change adds to FocusManager Properties. We want to try a different implementation where listeners on the pointerFocus Properties are only added when the highlight is active. We also identified additional work to be done in onInteractiveHighlightingEnabledChange. Here is a patch with a new TODO.

Subject: [PATCH] Indicate that items have been sorted, see https://github.com/phetsims/scenery-phet/issues/815
---
Index: center-and-variability/js/median/view/InteractiveCardNodeContainer.ts
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/center-and-variability/js/median/view/InteractiveCardNodeContainer.ts b/center-and-variability/js/median/view/InteractiveCardNodeContainer.ts
--- a/center-and-variability/js/median/view/InteractiveCardNodeContainer.ts	(revision da157047c73dd061136faca288794aa1e845e598)
+++ b/center-and-variability/js/median/view/InteractiveCardNodeContainer.ts	(date<PHONE_NUMBER>911)
@@ -124,6 +124,10 @@
       sortingRangeProperty: sortingRangeProperty
     } );

+    this.isInteractiveHighlightActiveProperty.lazyLink( active => {
+      console.log( 'active', active );
+    } );
+

     this.cardMap.forEach( ( cardNode, cardModel ) => {
       // Update the position of all cards (via animation) whenever any card is dragged
Index: scenery/js/accessibility/voicing/InteractiveHighlighting.ts
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/scenery/js/accessibility/voicing/InteractiveHighlighting.ts b/scenery/js/accessibility/voicing/InteractiveHighlighting.ts
--- a/scenery/js/accessibility/voicing/InteractiveHighlighting.ts	(revision 4fdc67f88e886673c2c5ce5a4dec4cf008489f0b)
+++ b/scenery/js/accessibility/voicing/InteractiveHighlighting.ts	(date<PHONE_NUMBER>337)
@@ -13,6 +13,8 @@
 import { Highlight } from '../../overlays/HighlightOverlay.js';
 import TEmitter from '../../../../axon/js/TEmitter.js';
 import memoize from '../../../../phet-core/js/memoize.js';
+import TReadOnlyProperty from '../../../../axon/js/TReadOnlyProperty.js';
+import TinyProperty from '../../../../axon/js/TinyProperty.js';

 // constants
 // option keys for InteractiveHighlighting, each of these will have a setter and getter and values are applied with mutate()
@@ -61,7 +63,7 @@
     // If true, the highlight will be layerable in the scene graph instead of drawn
     // above everything in the HighlightOverlay. If true, you are responsible for adding the interactiveHighlight
     // in the location you want in the scene graph. The interactiveHighlight will become visible when
-    // this.interactiveHighlightActivated is true.
+    // this.isInteractiveHighlightActiveProperty is true.
     private _interactiveHighlightLayerable = false;

     // If true, the highlight will be displayed on activation input. If false, it will not and we can remove listeners
@@ -71,6 +73,9 @@
     // Emits an event when the interactive highlight changes for this Node
     public interactiveHighlightChangedEmitter: TEmitter = new TinyEmitter();

+    public readonly isInteractiveHighlightActiveProperty: TReadOnlyProperty<boolean>;
+    private readonly _isInteractiveHighlightActiveProperty = new TinyProperty( false );
+
     // When new instances of this Node are created, adds an entry to the map of Displays.
     private readonly _changedInstanceListener: ( instance: Instance, added: boolean ) => void;

@@ -86,6 +91,7 @@
     // Input listener that locks the HighlightOverlay so that there are no updates to the highlight
     // while the pointer is down over something that uses InteractiveHighlighting.
     private readonly _pointerListener: TInputListener;
+    private readonly _boundHandleHighlightActiveChange: () => void;

     public constructor( ...args: IntentionalAny[] ) {
       super( ...args );
@@ -103,6 +109,7 @@

       this._interactiveHighlightingEnabledListener = this._onInteractiveHighlightingEnabledChange.bind( this );
       this._boundPointerFocusClearedListener = this.handleLockedPointerFocusCleared.bind( this );
+      this._boundHandleHighlightActiveChange = this.handleHighlightActiveChange.bind( this );

       const boundPointerReleaseListener = this._onPointerRelease.bind( this );
       const boundPointerCancel = this._onPointerCancel.bind( this );
@@ -112,6 +119,8 @@
         cancel: boundPointerCancel,
         interrupt: boundPointerCancel
       };
+
+      this.isInteractiveHighlightActiveProperty = this._isInteractiveHighlightActiveProperty;
     }

     /**
@@ -216,14 +225,24 @@
     /**
      * Returns true if this Node is "activated" by a pointer, indicating that a Pointer is over it
      * and this Node mixes InteractiveHighlighting so an interactive highlight should surround it.
+     * If you come to desire this function, instead you should use isInteractiveHighlightActiveProperty.
      */
-    public isInteractiveHighlightActivated(): boolean {
+    private isInteractiveHighlightActivated(): boolean {
       let activated = false;

       const trailIds = Object.keys( this.displays );
       for ( let i = 0; i < trailIds.length; i++ ) {
-        const pointerFocus = this.displays[ trailIds[ i ] ].focusManager.pointerFocusProperty.value;
-        if ( pointerFocus && pointerFocus.trail.lastNode() === this ) {
+        const display = this.displays[ trailIds[ i ] ];
+        const pointerFocus = display.focusManager.pointerFocusProperty.value;
+        const lockedPointerFocus = display.focusManager.lockedPointerFocusProperty.value;
+        // TODO: Is this the right way to account for the locked pointer (locked takes precedent). https://github.com/phetsims/scenery/issues/1602
+        if ( lockedPointerFocus ) {
+          if ( lockedPointerFocus?.trail.lastNode() === this ) {
+            activated = true;
+            break;
+          }
+        }
+        else if ( pointerFocus?.trail.lastNode() === this ) {
           activated = true;
           break;
         }
@@ -250,12 +269,7 @@
       const trailIds = Object.keys( this.displays );
       for ( let i = 0; i < trailIds.length; i++ ) {
         const display = this.displays[ trailIds[ i ] ];
-
-        if ( display.focusManager.lockedPointerFocusProperty.hasListener( this._boundPointerFocusClearedListener ) ) {
-          display.focusManager.lockedPointerFocusProperty.unlink( this._boundPointerFocusClearedListener );
-        }
-
-        display.focusManager.pointerHighlightsVisibleProperty.unlink( this._interactiveHighlightingEnabledListener );
+        this.onDisplayRemoved( display, true );
         delete this.displays[ trailIds[ i ] ];
       }

@@ -284,6 +298,7 @@

           // only set focus if current Pointer focus is not defined (from a more descendant Node)
           if ( display.focusManager.pointerFocusProperty.value === null ) {
+            // TODO: I'm confused, why is this setting focus for the ancestor, https://github.com/phetsims/scenery/issues/1602
             display.focusManager.pointerFocusProperty.set( new Focus( display, rootToGroupNode ) );
           }
         }
@@ -364,6 +379,7 @@
       const displays = Object.values( this.displays );
       for ( let i = 0; i < displays.length; i++ ) {
         const display = displays[ i ];
+        // TODO: nested loop to make sure that other displays aren't showing this Node? Oh boy, https://github.com/phetsims/scenery/issues/1602
         display.focusManager.pointerFocusProperty.set( null );

         // An exit event may come from a Node along the trail becoming invisible or unpickable. In that case unlock
@@ -414,6 +430,49 @@
       }
     }

+    private onDisplayAdded( display: Display ): void {
+
+      [ display.focusManager.pointerFocusProperty,
+        display.focusManager.lockedPointerFocusProperty,
+        display.focusManager.pointerHighlightsVisibleProperty
+      ].forEach( property => {
+        // TODO: lazy link? https://github.com/phetsims/scenery/issues/1602
+        !property.hasListener( this._boundHandleHighlightActiveChange ) && property.link( this._boundHandleHighlightActiveChange );
+      } );
+
+      // Listener may already by on the display in cases of DAG, only add if this is the first instance of this Node
+      if ( !display.focusManager.pointerHighlightsVisibleProperty.hasListener( this._interactiveHighlightingEnabledListener ) ) {
+        display.focusManager.pointerHighlightsVisibleProperty.link( this._interactiveHighlightingEnabledListener );
+      }
+    }
+
+    private onDisplayRemoved( display: Display, unlink = true ): void {
+
+      [ display.focusManager.pointerFocusProperty,
+        display.focusManager.lockedPointerFocusProperty,
+        display.focusManager.pointerHighlightsVisibleProperty
+      ].forEach( property => {
+        property.hasListener( this._boundHandleHighlightActiveChange ) && property.unlink( this._boundHandleHighlightActiveChange );
+      } );
+
+      if ( unlink ) {
+
+        // Pointer focus was locked due to interaction with this listener, but unlocked because of other
+        // scenery-internal listeners. But the Property still has this listener so it needs to be removed now.
+        if ( display.focusManager.lockedPointerFocusProperty.hasListener( this._boundPointerFocusClearedListener ) ) {
+          display.focusManager.lockedPointerFocusProperty.unlink( this._boundPointerFocusClearedListener );
+        }
+
+        display.focusManager.pointerHighlightsVisibleProperty.unlink( this._interactiveHighlightingEnabledListener );
+      }
+    }
+
+    private handleHighlightActiveChange(): void {
+
+      // The perofrmance of this is OK - what we
+      this._isInteractiveHighlightActiveProperty.value = this.isInteractiveHighlightActivated();
+    }
+
     /**
      * When a Pointer goes up after going down on this Node, signal to the Displays that the pointerFocusProperty no
      * longer needs to be locked.
@@ -425,6 +484,7 @@
       const displays = Object.values( this.displays );
       for ( let i = 0; i < displays.length; i++ ) {
         const display = displays[ i ];
+        // TODO: why does this not have to check to see if this Node was the lockedPointer? https://github.com/phetsims/scenery/issues/1602
         display.focusManager.lockedPointerFocusProperty.value = null;

         // Unlink the listener that was watching for the lockedPointerFocusProperty to be cleared externally
@@ -518,6 +578,9 @@
       // Only listen to the activation listener if the feature is enabled and highlighting is enabled for this Node.
       const enabled = featureEnabled && this._interactiveHighlightEnabled;

+      // TODO: We need to recalculate the active state too because the enabled state may become false while
+      // a highlight is active.
+
       const hasActivationListener = this.hasInputListener( this._activationListener );
       if ( enabled && !hasActivationListener ) {
         this.addInputListener( this._activationListener );
@@ -537,29 +600,18 @@
       if ( added ) {
         this.displays[ instance.trail!.uniqueId ] = instance.display;

-        // Listener may already by on the display in cases of DAG, only add if this is the first instance of this Node
-        if ( !instance.display.focusManager.pointerHighlightsVisibleProperty.hasListener( this._interactiveHighlightingEnabledListener ) ) {
-          instance.display.focusManager.pointerHighlightsVisibleProperty.link( this._interactiveHighlightingEnabledListener );
-        }
+        this.onDisplayAdded( instance.display );
       }
       else {
         assert && assert( instance.node, 'should have a node' );
         const display = this.displays[ instance.trail!.uniqueId ];
+        // TODO: there was a truthy check on display, wat? https://github.com/phetsims/scenery/issues/1602

         // If the node was disposed, this display reference has already been cleaned up, but instances are updated
         // (disposed) on the next frame after the node was disposed. Only unlink if there are no more instances of
         // this node;
-        if ( display && instance.node!.instances.length === 0 ) {
-
-          // Pointer focus was locked due to interaction with this listener, but unlocked because of other
-          // scenery-internal listeners. But the Property still has this listener so it needs to be removed now.
-          if ( display.focusManager.lockedPointerFocusProperty.hasListener( this._boundPointerFocusClearedListener ) ) {
-            display.focusManager.lockedPointerFocusProperty.unlink( this._boundPointerFocusClearedListener );
-          }
-
-          display.focusManager.pointerHighlightsVisibleProperty.unlink( this._interactiveHighlightingEnabledListener );
-        }
-
+        const shouldUnlink = instance.node!.instances.length === 0;
+        this.onDisplayRemoved( display, shouldUnlink );
         delete this.displays[ instance.trail!.uniqueId ];
       }
     }

Some other thoughts to come back to:

[ ] Confirm this handles the nulling out in other spots (like in the focusManager)
[ ] Decide how to handle testing of weird cases and edge cases (like with multiple displays?)

Here I have things adding listener on the right input, but not removing it from logic just yet. This is because we need to keep track of the listener PER DISPLAY, which is too bad, but alas. I want to put in a patch before adding that complexity in case I go off on the wrong path.

I wanted to use InteractiveHighlighting.displays, but that doesn't necessarily seem like a good fit. I need to know if this is a map that holds only unique displays.
https://github.com/phetsims/scenery/blob/992ac9fc8797f7a200a426424d77c1c9d2297d5d/js/accessibility/voicing/InteractiveHighlighting.ts#L51-L57
Since this is only called on instance changed, I though we may be able to depend on this being a unique list of displays, but I don't think so since you could use DAG internally in one display, and this would keep track of each instance pointing to that display. Thus I think we will need a new data structure that can store the bound pointer/lockedPointer focus Property listeners for each display, to make sure that there is no duplication.
If I'm correct in the above. I'd like to update this doc more specifically to recognize that the same display may be in the map more than once (per instance).

To start next time:

Subject: [PATCH] fdsafds
---
Index: center-and-variability/js/median/view/InteractiveCardNodeContainer.ts
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/center-and-variability/js/median/view/InteractiveCardNodeContainer.ts b/center-and-variability/js/median/view/InteractiveCardNodeContainer.ts
--- a/center-and-variability/js/median/view/InteractiveCardNodeContainer.ts	(revision da157047c73dd061136faca288794aa1e845e598)
+++ b/center-and-variability/js/median/view/InteractiveCardNodeContainer.ts	(date<PHONE_NUMBER>728)
@@ -124,6 +124,10 @@
       sortingRangeProperty: sortingRangeProperty
     } );

+    this.isInteractiveHighlightActiveProperty.lazyLink( active => {
+      console.log( 'active', active );
+    } );
+

     this.cardMap.forEach( ( cardNode, cardModel ) => {
       // Update the position of all cards (via animation) whenever any card is dragged
Index: scenery/js/accessibility/voicing/InteractiveHighlighting.ts
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/scenery/js/accessibility/voicing/InteractiveHighlighting.ts b/scenery/js/accessibility/voicing/InteractiveHighlighting.ts
--- a/scenery/js/accessibility/voicing/InteractiveHighlighting.ts	(revision 91c0358aa6c68ce4cb6cd2b5c1a6884b1169f726)
+++ b/scenery/js/accessibility/voicing/InteractiveHighlighting.ts	(date<PHONE_NUMBER>657)
@@ -13,6 +13,8 @@
 import { Highlight } from '../../overlays/HighlightOverlay.js';
 import TEmitter from '../../../../axon/js/TEmitter.js';
 import memoize from '../../../../phet-core/js/memoize.js';
+import TReadOnlyProperty from '../../../../axon/js/TReadOnlyProperty.js';
+import TinyProperty from '../../../../axon/js/TinyProperty.js';

 // constants
 // option keys for InteractiveHighlighting, each of these will have a setter and getter and values are applied with mutate()
@@ -22,6 +24,11 @@
   'interactiveHighlightEnabled'
 ];

+type DisplayListeners = {
+  pointerFocusListener: null | VoidFunction;
+  lockedPointerFocusListener: null | VoidFunction;
+};
+
 type SelfOptions = {
   interactiveHighlight?: Highlight;
   interactiveHighlightLayerable?: boolean;
@@ -54,6 +61,9 @@
     // (with updateDisplay) since Instances are added asynchronously.
     protected displays: Record<string, Display> = {};

+    // TODO: START HERE! https://github.com/phetsims/scenery/issues/1602
+    private boundDisplayListenersMap = new Map<Display, DisplayListeners>();
+
     // The highlight that will surround this Node when it is activated and a Pointer is currently over it. When
     // null, the focus highlight will be used (as defined in ParallelDOM.js).
     private _interactiveHighlight: Highlight = null;
@@ -61,7 +71,7 @@
     // If true, the highlight will be layerable in the scene graph instead of drawn
     // above everything in the HighlightOverlay. If true, you are responsible for adding the interactiveHighlight
     // in the location you want in the scene graph. The interactiveHighlight will become visible when
-    // this.interactiveHighlightActivated is true.
+    // this.isInteractiveHighlightActiveProperty is true.
     private _interactiveHighlightLayerable = false;

     // If true, the highlight will be displayed on activation input. If false, it will not and we can remove listeners
@@ -71,6 +81,9 @@
     // Emits an event when the interactive highlight changes for this Node
     public interactiveHighlightChangedEmitter: TEmitter = new TinyEmitter();

+    public readonly isInteractiveHighlightActiveProperty: TReadOnlyProperty<boolean>;
+    private readonly _isInteractiveHighlightActiveProperty = new TinyProperty( false );
+
     // When new instances of this Node are created, adds an entry to the map of Displays.
     private readonly _changedInstanceListener: ( instance: Instance, added: boolean ) => void;

@@ -86,6 +99,8 @@
     // Input listener that locks the HighlightOverlay so that there are no updates to the highlight
     // while the pointer is down over something that uses InteractiveHighlighting.
     private readonly _pointerListener: TInputListener;
+    // private readonly _boundHandleHighlightActiveChangePointerFocus: () => void;
+    // private readonly _boundHandleHighlightActiveChangeLockedPointerFocus: () => void;

     public constructor( ...args: IntentionalAny[] ) {
       super( ...args );
@@ -103,6 +118,8 @@

       this._interactiveHighlightingEnabledListener = this._onInteractiveHighlightingEnabledChange.bind( this );
       this._boundPointerFocusClearedListener = this.handleLockedPointerFocusCleared.bind( this );
+      // this._boundHandleHighlightActiveChangePointerFocus = this.handleHighlightActiveChangePointerFocus.bind( this );
+      // this._boundHandleHighlightActiveChangeLockedPointerFocus = this.handleHighlightActiveChangeLockedPointerFocus.bind( this );

       const boundPointerReleaseListener = this._onPointerRelease.bind( this );
       const boundPointerCancel = this._onPointerCancel.bind( this );
@@ -112,6 +129,8 @@
         cancel: boundPointerCancel,
         interrupt: boundPointerCancel
       };
+
+      this.isInteractiveHighlightActiveProperty = this._isInteractiveHighlightActiveProperty;
     }

     /**
@@ -216,23 +235,45 @@
     /**
      * Returns true if this Node is "activated" by a pointer, indicating that a Pointer is over it
      * and this Node mixes InteractiveHighlighting so an interactive highlight should surround it.
+     *
+     * This algorithm depends on the direct focus over the pointer, the "locked" focus (from an attached listener),
+     * and if pointer highlights are visible at all.
+     *
+     * If you come to desire this function, instead you should use isInteractiveHighlightActiveProperty.
+     *
      */
-    public isInteractiveHighlightActivated(): boolean {
+    private isInteractiveHighlightActivated(): boolean {
       let activated = false;

+      // TODO: We should grab a uniq list of these displays, not just necessarily all of them, https://github.com/phetsims/scenery/issues/1602
+      // TODO: factor out a function that runs code on each display only once? https://github.com/phetsims/scenery/issues/1602
       const trailIds = Object.keys( this.displays );
       for ( let i = 0; i < trailIds.length; i++ ) {
-        const pointerFocus = this.displays[ trailIds[ i ] ].focusManager.pointerFocusProperty.value;
-        if ( pointerFocus && pointerFocus.trail.lastNode() === this ) {
-          activated = true;
-          break;
+        const display = this.displays[ trailIds[ i ] ];
+
+        // console.log( 'this is a lot' );
+
+        // TODO: right? Only if highlights are visible for that display could there be an activation, https://github.com/phetsims/scenery/issues/1602
+        if ( display.focusManager.pointerHighlightsVisibleProperty.value ) {
+
+          const pointerFocus = display.focusManager.pointerFocusProperty.value;
+          const lockedPointerFocus = display.focusManager.lockedPointerFocusProperty.value;
+          // TODO: Is this the right way to account for the locked pointer (locked takes precedent). https://github.com/phetsims/scenery/issues/1602
+          if ( lockedPointerFocus ) {
+            if ( lockedPointerFocus?.trail.lastNode() === this ) {
+              activated = true;
+              break;
+            }
+          }
+          else if ( pointerFocus?.trail.lastNode() === this ) {
+            activated = true;
+            break;
+          }
         }
       }
       return activated;
     }

-    public get interactiveHighlightActivated(): boolean { return this.isInteractiveHighlightActivated(); }
-
     public override dispose(): void {
       this.changedInstanceEmitter.removeListener( this._changedInstanceListener );

@@ -250,12 +291,7 @@
       const trailIds = Object.keys( this.displays );
       for ( let i = 0; i < trailIds.length; i++ ) {
         const display = this.displays[ trailIds[ i ] ];
-
-        if ( display.focusManager.lockedPointerFocusProperty.hasListener( this._boundPointerFocusClearedListener ) ) {
-          display.focusManager.lockedPointerFocusProperty.unlink( this._boundPointerFocusClearedListener );
-        }
-
-        display.focusManager.pointerHighlightsVisibleProperty.unlink( this._interactiveHighlightingEnabledListener );
+        this.onDisplayRemoved( display );
         delete this.displays[ trailIds[ i ] ];
       }

@@ -284,6 +320,7 @@

           // only set focus if current Pointer focus is not defined (from a more descendant Node)
           if ( display.focusManager.pointerFocusProperty.value === null ) {
+            // TODO: I'm confused, why is this setting focus for the ancestor, https://github.com/phetsims/scenery/issues/1602
             display.focusManager.pointerFocusProperty.set( new Focus( display, rootToGroupNode ) );
           }
         }
@@ -313,6 +350,8 @@
           if ( display.focusManager.lockedPointerFocusProperty.value === null && event.pointer.attachedListener ) {
             lockPointer = this.attemptHighlightLock( newFocus, display.focusManager, event.pointer );
           }
+          // TODO: After lock is set too, right? https://github.com/phetsims/scenery/issues/1602
+          this.setUpActiveTracking( display );
         }
       }

@@ -346,6 +385,8 @@
             if ( display.focusManager.lockedPointerFocusProperty.value === null && event.pointer.attachedListener ) {
               lockPointer = this.attemptHighlightLock( newFocus, display.focusManager, event.pointer );
             }
+            // TODO: After lock is set too, right? https://github.com/phetsims/scenery/issues/1602
+            this.setUpActiveTracking( display );
           }
         }

@@ -364,6 +405,7 @@
       const displays = Object.values( this.displays );
       for ( let i = 0; i < displays.length; i++ ) {
         const display = displays[ i ];
+        // TODO: nested loop to make sure that other displays aren't showing this Node? Oh boy, https://github.com/phetsims/scenery/issues/1602
         display.focusManager.pointerFocusProperty.set( null );

         // An exit event may come from a Node along the trail becoming invisible or unpickable. In that case unlock
@@ -405,6 +447,8 @@
             // Set the lockedPointerFocusProperty with a copy of the Focus (as deep as possible) because we want
             // to keep a reference to the old Trail while pointerFocusProperty changes.
             lockPointer = this.attemptHighlightLock( focus, display.focusManager, event.pointer );
+
+            // TODO: MK doesn't actually think this would ever effect activation of this (since the locked is already being listened to). Do we need to call setUpActiveTracking? https://github.com/phetsims/scenery/issues/1602
           }
         }

@@ -414,6 +458,58 @@
       }
     }

+    private onDisplayAdded( display: Display ): void {
+
+      // Listener may already by on the display in cases of DAG, only add if this is the first instance of this Node
+      if ( !display.focusManager.pointerHighlightsVisibleProperty.hasListener( this._interactiveHighlightingEnabledListener ) ) {
+        display.focusManager.pointerHighlightsVisibleProperty.link( this._interactiveHighlightingEnabledListener );
+      }
+    }
+
+    // TODO: review factored out onDisplay() functions, https://github.com/phetsims/scenery/issues/1602
+    private onDisplayRemoved( display: Display ): void {
+
+      // Pointer focus was locked due to interaction with this listener, but unlocked because of other
+      // scenery-internal listeners. But the Property still has this listener so it needs to be removed now.
+      if ( display.focusManager.lockedPointerFocusProperty.hasListener( this._boundPointerFocusClearedListener ) ) {
+        display.focusManager.lockedPointerFocusProperty.unlink( this._boundPointerFocusClearedListener );
+      }
+
+      display.focusManager.pointerHighlightsVisibleProperty.unlink( this._interactiveHighlightingEnabledListener );
+    }
+
+
+    private setUpActiveTracking( display: Display ): void {
+      // TODO: lazy link? MK thinks not, but really only one needs to be a link, is this important? https://github.com/phetsims/scenery/issues/1602
+      // !display.focusManager.pointerFocusProperty.hasListener( this._boundHandleHighlightActiveChangePointerFocus ) &&
+      // TODO how to capture display inside the binding? https://github.com/phetsims/scenery/issues/1602
+      display.focusManager.pointerFocusProperty.link( () => {
+        this.handleHighlightActiveChangeForProperty( display.focusManager.pointerFocusProperty );
+      } );
+      // !display.focusManager.lockedPointerFocusProperty.hasListener( this._boundHandleHighlightActiveChangeLockedPointerFocus ) &&
+      display.focusManager.lockedPointerFocusProperty.link( () => {
+        this.handleHighlightActiveChangeForProperty( display.focusManager.lockedPointerFocusProperty );
+
+      } );
+    }
+
+    private handleHighlightActiveChangeForProperty( property: TReadOnlyProperty<Focus | null> ): void {
+
+      this.handleHighlightActiveChange();
+
+      if ( property.value?.trail.lastNode() !== this ) {
+        // unlink
+      }
+    }
+
+
+    private handleHighlightActiveChange(): void {
+
+      // The performance of this is OK - what we
+      this._isInteractiveHighlightActiveProperty.value = this.isInteractiveHighlightActivated();
+    }
+
+
     /**
      * When a Pointer goes up after going down on this Node, signal to the Displays that the pointerFocusProperty no
      * longer needs to be locked.
@@ -425,6 +521,7 @@
       const displays = Object.values( this.displays );
       for ( let i = 0; i < displays.length; i++ ) {
         const display = displays[ i ];
+        // TODO: why does this not have to check to see if this Node was the lockedPointer? https://github.com/phetsims/scenery/issues/1602
         display.focusManager.lockedPointerFocusProperty.value = null;

         // Unlink the listener that was watching for the lockedPointerFocusProperty to be cleared externally
@@ -525,6 +622,9 @@
       else if ( !enabled && hasActivationListener ) {
         this.removeInputListener( this._activationListener );
       }
+
+      // TODO: Review this, because we want to update active when things are hidden or shown. Pointer focus isn't the only thing (it also has to be visible). https://github.com/phetsims/scenery/issues/1602
+      this.handleHighlightActiveChange();
     }

     /**
@@ -537,29 +637,17 @@
       if ( added ) {
         this.displays[ instance.trail!.uniqueId ] = instance.display;

-        // Listener may already by on the display in cases of DAG, only add if this is the first instance of this Node
-        if ( !instance.display.focusManager.pointerHighlightsVisibleProperty.hasListener( this._interactiveHighlightingEnabledListener ) ) {
-          instance.display.focusManager.pointerHighlightsVisibleProperty.link( this._interactiveHighlightingEnabledListener );
-        }
+        this.onDisplayAdded( instance.display );
       }
       else {
         assert && assert( instance.node, 'should have a node' );
         const display = this.displays[ instance.trail!.uniqueId ];
+        // TODO: there was a truthy check on display, wat? https://github.com/phetsims/scenery/issues/1602

         // If the node was disposed, this display reference has already been cleaned up, but instances are updated
         // (disposed) on the next frame after the node was disposed. Only unlink if there are no more instances of
         // this node;
-        if ( display && instance.node!.instances.length === 0 ) {
-
-          // Pointer focus was locked due to interaction with this listener, but unlocked because of other
-          // scenery-internal listeners. But the Property still has this listener so it needs to be removed now.
-          if ( display.focusManager.lockedPointerFocusProperty.hasListener( this._boundPointerFocusClearedListener ) ) {
-            display.focusManager.lockedPointerFocusProperty.unlink( this._boundPointerFocusClearedListener );
-          }
-
-          display.focusManager.pointerHighlightsVisibleProperty.unlink( this._interactiveHighlightingEnabledListener );
-        }
-
+        instance.node!.instances.length === 0 && this.onDisplayRemoved( display );
         delete this.displays[ instance.trail!.uniqueId ];
       }
     }

Oh my goodness! Refreshed from the most recent weekend, I had a new thought. @jessegreenberg and I were both not excited at the added complexity of adding and removing listeners per display per activation per highlighting instance. Instead, I think that another path to investigate would be to manage this is FocusManager, making handleHighlightActiveChange public, and called on the old and new Nodes whenever pointer/lockedPointer focus changed for that Display. I'll give it a try and see if it seems cleaner. Pro is that it would mean just a single listener per Display Property, con is that it pulls the logic outside of InteractiveHighlighting. I'll report back.

I love this patch! It is so clean and so simple. It is hard coded to pointer focus though, and in FocusManager, so I do want to discuss a couple points with @jessegreenberg. Noting here that a console log in onPointerFocusChange shows that now this listener is only called up to 4 times on pointer change (if the locked and pointer focus changed at the same time).
There are a couple TODOs, and I'd like to clean this up. I'll reach out to @jessegreenberg to see if async or sync is best for this.

Subject: [PATCH] focusManager adds the listeners, https://github.com/phetsims/scenery/issues/1602
---
Index: scenery/js/accessibility/FocusManager.ts
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/scenery/js/accessibility/FocusManager.ts b/scenery/js/accessibility/FocusManager.ts
--- a/scenery/js/accessibility/FocusManager.ts	(revision 8e6497a2f5afaf004d20888d8ff0ffe69662dd8a)
+++ b/scenery/js/accessibility/FocusManager.ts	(date<PHONE_NUMBER>150)
@@ -34,6 +34,7 @@
 import NullableIO from '../../../tandem/js/types/NullableIO.js';
 import Utterance from '../../../utterance-queue/js/Utterance.js';
 import { Display, Focus, FocusDisplayedController, Node, PDOMInstance, PDOMUtils, ReadingBlockUtterance, scenery, voicingManager } from '../imports.js';
+import { InteractiveHighlightingNode } from './voicing/InteractiveHighlighting.js';

 type SpeakingListener = ( text: string, utterance: Utterance ) => void;

@@ -93,6 +94,7 @@
   private static globallyAttached = false;

   public constructor() {
+    // TODO: why aren't these disposed? https://github.com/phetsims/scenery/issues/1602
     this.pointerFocusProperty = new Property( null );
     this.readingBlockFocusProperty = new Property( null );
     this.lockedPointerFocusProperty = new Property( null );
@@ -160,7 +162,16 @@
     } );

     this.lockedPointerFocusDisplayedController = new FocusDisplayedController( this.lockedPointerFocusProperty );
+
+
+    [
+      this.pointerFocusProperty,
+      this.lockedPointerFocusProperty
+    ].forEach( property => {
+      property.link( this.onPointerFocusChange.bind( this ) );
+    } );
   }
+

   public dispose(): void {
     this.readingBlockFocusController.dispose();
@@ -248,6 +259,16 @@
     }
   }

+  // Listener to update the "active" highlight state for an interactiveHighlightingNode
+  private onPointerFocusChange( pointerFocus: Focus | null, oldFocus: Focus | null ): void {
+    // TODO: Do we need to handle any other nodes? Or just the last one? https://github.com/phetsims/scenery/issues/1602
+    const focusNode = pointerFocus?.trail.lastNode() as InteractiveHighlightingNode;
+    focusNode && focusNode.isInteractiveHighlighting && focusNode.handleHighlightActiveChange();
+    // TODO: Is a type case the best way to handle this? https://github.com/phetsims/scenery/issues/1602
+    const oldFocusNode = oldFocus?.trail.lastNode() as InteractiveHighlightingNode;
+    oldFocusNode && oldFocusNode.isInteractiveHighlighting && oldFocusNode.handleHighlightActiveChange();
+  }
+
   /**
    * Set the DOM focus. A DOM limitation is that there can only be one element with focus at a time so this must
    * be a static for the FocusManager.
Index: center-and-variability/js/median/view/InteractiveCardNodeContainer.ts
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/center-and-variability/js/median/view/InteractiveCardNodeContainer.ts b/center-and-variability/js/median/view/InteractiveCardNodeContainer.ts
--- a/center-and-variability/js/median/view/InteractiveCardNodeContainer.ts	(revision 446da132a23614998d3e02a77115d92023df5274)
+++ b/center-and-variability/js/median/view/InteractiveCardNodeContainer.ts	(date<PHONE_NUMBER>115)
@@ -124,6 +124,9 @@
       sortingRangeProperty: sortingRangeProperty
     } );

+    this.isInteractiveHighlightActiveProperty.lazyLink( active => {
+      console.log( 'active', active );
+    } );

     this.cardMap.forEach( ( cardNode, cardModel ) => {
       // Update the position of all cards (via animation) whenever any card is dragged
Index: scenery/js/accessibility/voicing/InteractiveHighlighting.ts
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/scenery/js/accessibility/voicing/InteractiveHighlighting.ts b/scenery/js/accessibility/voicing/InteractiveHighlighting.ts
--- a/scenery/js/accessibility/voicing/InteractiveHighlighting.ts	(revision 8e6497a2f5afaf004d20888d8ff0ffe69662dd8a)
+++ b/scenery/js/accessibility/voicing/InteractiveHighlighting.ts	(date<PHONE_NUMBER>632)
@@ -13,6 +13,8 @@
 import { Highlight } from '../../overlays/HighlightOverlay.js';
 import TEmitter from '../../../../axon/js/TEmitter.js';
 import memoize from '../../../../phet-core/js/memoize.js';
+import TReadOnlyProperty from '../../../../axon/js/TReadOnlyProperty.js';
+import TinyProperty from '../../../../axon/js/TinyProperty.js';

 // constants
 // option keys for InteractiveHighlighting, each of these will have a setter and getter and values are applied with mutate()
@@ -61,7 +63,7 @@
     // If true, the highlight will be layerable in the scene graph instead of drawn
     // above everything in the HighlightOverlay. If true, you are responsible for adding the interactiveHighlight
     // in the location you want in the scene graph. The interactiveHighlight will become visible when
-    // this.interactiveHighlightActivated is true.
+    // this.isInteractiveHighlightActiveProperty is true.
     private _interactiveHighlightLayerable = false;

     // If true, the highlight will be displayed on activation input. If false, it will not and we can remove listeners
@@ -71,6 +73,9 @@
     // Emits an event when the interactive highlight changes for this Node
     public interactiveHighlightChangedEmitter: TEmitter = new TinyEmitter();

+    public readonly isInteractiveHighlightActiveProperty: TReadOnlyProperty<boolean>;
+    private readonly _isInteractiveHighlightActiveProperty = new TinyProperty( false );
+
     // When new instances of this Node are created, adds an entry to the map of Displays.
     private readonly _changedInstanceListener: ( instance: Instance, added: boolean ) => void;

@@ -112,6 +117,8 @@
         cancel: boundPointerCancel,
         interrupt: boundPointerCancel
       };
+
+      this.isInteractiveHighlightActiveProperty = this._isInteractiveHighlightActiveProperty;
     }

     /**
@@ -216,23 +223,44 @@
     /**
      * Returns true if this Node is "activated" by a pointer, indicating that a Pointer is over it
      * and this Node mixes InteractiveHighlighting so an interactive highlight should surround it.
+     *
+     * This algorithm depends on the direct focus over the pointer, the "locked" focus (from an attached listener),
+     * and if pointer highlights are visible at all.
+     *
+     * If you come to desire this function, instead you should use isInteractiveHighlightActiveProperty.
+     *
      */
-    public isInteractiveHighlightActivated(): boolean {
+    private isInteractiveHighlightActivated(): boolean {
       let activated = false;

+      // TODO: We should grab a uniq list of these displays, not just necessarily all of them, https://github.com/phetsims/scenery/issues/1602
+      // TODO: factor out a function that runs code on each display only once? https://github.com/phetsims/scenery/issues/1602
       const trailIds = Object.keys( this.displays );
       for ( let i = 0; i < trailIds.length; i++ ) {
-        const pointerFocus = this.displays[ trailIds[ i ] ].focusManager.pointerFocusProperty.value;
-        if ( pointerFocus && pointerFocus.trail.lastNode() === this ) {
-          activated = true;
-          break;
+        const display = this.displays[ trailIds[ i ] ];
+
+        // TODO: right? Only if highlights are visible for that display could there be an activation, https://github.com/phetsims/scenery/issues/1602
+        // TODO: I believe I thought that this name (visible), meant that it was set to false when you start tabbing, but it is not that, it is more a main on/off switch for the feature. https://github.com/phetsims/scenery/issues/1602
+        if ( display.focusManager.pointerHighlightsVisibleProperty.value ) {
+
+          const pointerFocus = display.focusManager.pointerFocusProperty.value;
+          const lockedPointerFocus = display.focusManager.lockedPointerFocusProperty.value;
+          // TODO: Is this the right way to account for the locked pointer (locked takes precedent). https://github.com/phetsims/scenery/issues/1602
+          if ( lockedPointerFocus ) {
+            if ( lockedPointerFocus?.trail.lastNode() === this ) {
+              activated = true;
+              break;
+            }
+          }
+          else if ( pointerFocus?.trail.lastNode() === this ) {
+            activated = true;
+            break;
+          }
         }
       }
       return activated;
     }

-    public get interactiveHighlightActivated(): boolean { return this.isInteractiveHighlightActivated(); }
-
     public override dispose(): void {
       this.changedInstanceEmitter.removeListener( this._changedInstanceListener );

@@ -250,12 +278,7 @@
       const trailIds = Object.keys( this.displays );
       for ( let i = 0; i < trailIds.length; i++ ) {
         const display = this.displays[ trailIds[ i ] ];
-
-        if ( display.focusManager.lockedPointerFocusProperty.hasListener( this._boundPointerFocusClearedListener ) ) {
-          display.focusManager.lockedPointerFocusProperty.unlink( this._boundPointerFocusClearedListener );
-        }
-
-        display.focusManager.pointerHighlightsVisibleProperty.unlink( this._interactiveHighlightingEnabledListener );
+        this.onDisplayRemoved( display );
         delete this.displays[ trailIds[ i ] ];
       }

@@ -284,6 +307,7 @@

           // only set focus if current Pointer focus is not defined (from a more descendant Node)
           if ( display.focusManager.pointerFocusProperty.value === null ) {
+            // TODO: I'm confused, why is this setting focus for the ancestor, https://github.com/phetsims/scenery/issues/1602
             display.focusManager.pointerFocusProperty.set( new Focus( display, rootToGroupNode ) );
           }
         }
@@ -414,6 +438,33 @@
       }
     }

+    private onDisplayAdded( display: Display ): void {
+
+      // Listener may already by on the display in cases of DAG, only add if this is the first instance of this Node
+      if ( !display.focusManager.pointerHighlightsVisibleProperty.hasListener( this._interactiveHighlightingEnabledListener ) ) {
+        display.focusManager.pointerHighlightsVisibleProperty.link( this._interactiveHighlightingEnabledListener );
+      }
+    }
+
+    // TODO: review factored out onDisplay() functions, they seem good and same to me (helpful to have the name for maintainability) https://github.com/phetsims/scenery/issues/1602
+    private onDisplayRemoved( display: Display ): void {
+
+      // Pointer focus was locked due to interaction with this listener, but unlocked because of other
+      // scenery-internal listeners. But the Property still has this listener so it needs to be removed now.
+      if ( display.focusManager.lockedPointerFocusProperty.hasListener( this._boundPointerFocusClearedListener ) ) {
+        display.focusManager.lockedPointerFocusProperty.unlink( this._boundPointerFocusClearedListener );
+      }
+
+      display.focusManager.pointerHighlightsVisibleProperty.unlink( this._interactiveHighlightingEnabledListener );
+    }
+
+    public handleHighlightActiveChange(): void {
+
+      // The performance of this is OK - what we
+      this._isInteractiveHighlightActiveProperty.value = this.isInteractiveHighlightActivated();
+    }
+
+
     /**
      * When a Pointer goes up after going down on this Node, signal to the Displays that the pointerFocusProperty no
      * longer needs to be locked.
@@ -425,6 +476,7 @@
       const displays = Object.values( this.displays );
       for ( let i = 0; i < displays.length; i++ ) {
         const display = displays[ i ];
+        // TODO: why does this not have to check to see if this Node was the lockedPointer? https://github.com/phetsims/scenery/issues/1602
         display.focusManager.lockedPointerFocusProperty.value = null;

         // Unlink the listener that was watching for the lockedPointerFocusProperty to be cleared externally
@@ -525,6 +577,9 @@
       else if ( !enabled && hasActivationListener ) {
         this.removeInputListener( this._activationListener );
       }
+
+      // TODO: Review this, because we want to update active when things are hidden or shown. Pointer focus isn't the only thing (it also has to be visible). https://github.com/phetsims/scenery/issues/1602
+      this.handleHighlightActiveChange();
     }

     /**
@@ -537,29 +592,17 @@
       if ( added ) {
         this.displays[ instance.trail!.uniqueId ] = instance.display;

-        // Listener may already by on the display in cases of DAG, only add if this is the first instance of this Node
-        if ( !instance.display.focusManager.pointerHighlightsVisibleProperty.hasListener( this._interactiveHighlightingEnabledListener ) ) {
-          instance.display.focusManager.pointerHighlightsVisibleProperty.link( this._interactiveHighlightingEnabledListener );
-        }
+        this.onDisplayAdded( instance.display );
       }
       else {
         assert && assert( instance.node, 'should have a node' );
         const display = this.displays[ instance.trail!.uniqueId ];
+        // TODO: there was a truthy check on display, wat? https://github.com/phetsims/scenery/issues/1602

         // If the node was disposed, this display reference has already been cleaned up, but instances are updated
         // (disposed) on the next frame after the node was disposed. Only unlink if there are no more instances of
         // this node;
-        if ( display && instance.node!.instances.length === 0 ) {
-
-          // Pointer focus was locked due to interaction with this listener, but unlocked because of other
-          // scenery-internal listeners. But the Property still has this listener so it needs to be removed now.
-          if ( display.focusManager.lockedPointerFocusProperty.hasListener( this._boundPointerFocusClearedListener ) ) {
-            display.focusManager.lockedPointerFocusProperty.unlink( this._boundPointerFocusClearedListener );
-          }
-
-          display.focusManager.pointerHighlightsVisibleProperty.unlink( this._interactiveHighlightingEnabledListener );
-        }
-
+        instance.node!.instances.length === 0 && this.onDisplayRemoved( display );
         delete this.displays[ instance.trail!.uniqueId ];
       }
     }

I know I assigned this to you last week, but I found myself back here while working on https://github.com/phetsims/buoyancy/issues/76 and thought I'd take another look. I ended up getting confident enough for a commit. I would still like @jessegreenberg to review, but there are only two small TODOs left pointing to this issue.
Also, to test, I was using these two logs (CAV and Buoyancy) with interactive highlighting on.

Subject: [PATCH] add isInteractiveHighlightActiveProperty, https://github.com/phetsims/scenery/issues/1602
---
Index: center-and-variability/js/median/view/InteractiveCardNodeContainer.ts
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/center-and-variability/js/median/view/InteractiveCardNodeContainer.ts b/center-and-variability/js/median/view/InteractiveCardNodeContainer.ts
--- a/center-and-variability/js/median/view/InteractiveCardNodeContainer.ts	(revision e4ecf93986d47c3c4e9a893b1ff1b64f215d3e12)
+++ b/center-and-variability/js/median/view/InteractiveCardNodeContainer.ts	(date<PHONE_NUMBER>541)
@@ -124,6 +124,9 @@
       sortingRangeProperty: sortingRangeProperty
     } );

+    this.isInteractiveHighlightActiveProperty.lazyLink( active => {
+      console.log( 'active', active );
+    } );

     this.cardMap.forEach( ( cardNode, cardModel ) => {
       // Update the position of all cards (via animation) whenever any card is dragged
Index: density-buoyancy-common/js/common/view/MassView.ts
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/density-buoyancy-common/js/common/view/MassView.ts b/density-buoyancy-common/js/common/view/MassView.ts
--- a/density-buoyancy-common/js/common/view/MassView.ts	(revision 2af1ac1a0a2c09da012ce5c747eaf2c2e4abeab7)
+++ b/density-buoyancy-common/js/common/view/MassView.ts	(date<PHONE_NUMBER>825)
@@ -119,6 +119,10 @@
       focusable: true,
       visibleProperty: mass.visibleProperty
     } );
+
+    this.focusablePath.isInteractiveHighlightActiveProperty.link( active => {
+      console.log( mass.constructor.name, active );
+    } );
   }

   /**

For review:

Look at the commits, mostly around how we are working in FocusManager, and the implementation of isInteractiveHighlightActivated
Apply the patch and see how it behaves in Buoyancy and CAV. (just for logging)
Activation is not on if you are the group highlight for another child. Is that correct?
There are two TODOs that are me asking you questions.

Thanks!

Thank you for doing this! That patch is so much cleaner than what we were thinking before. Its working great when I test it and all looks correct. Here are review notes/responses to TODOs and questions.

In https://github.com/phetsims/scenery/commit/6fabda31795a5c117e5d041f96f54b6e3b37a8b0 an existence check for the Display was removed. Can you comment on why that is safe? Probably related, do you know why the type checker is convinced it is safe? I expected an error like "argument of type Display | null is not assignable to argument of type Display where you call onDisplayRemoved.
Re "Is a type cast the best way to handle this" TODO - I believe so unfortunately. I can't think of a better way and have had to do that elsewhere in InteractiveHighlighting/ReadingBlock. I removed the TODO.
Re "why aren't these disposed?" TODO - They totally should be! They are now. I also checked for other missing disposals but didn't see anything else.
There is an unfinished comment here that seems important, would you mind completing it? https://github.com/phetsims/scenery/blob/42fefc31fc2910edfd75a72c69e33e78b088c84a/js/accessibility/voicing/InteractiveHighlighting.ts#L263
"Activation is not on if you are the group highlight for another child. Is that correct?" I think that is right! I saw this when testing the cards in CaV. I think that isInteractiveHighlightActiveProperty should match Focus with basically only one active at a time.

Thanks again and back to you.

In 6fabda3 an existence check for the Display was removed. Can you comment on why that is safe? Probably related, do you know why the type checker is convinced it is safe? I expected an error like "argument of type Display | null is not assignable to argument of type Display " where you call onDisplayRemoved.

The type bugginess is because Instance is still in JavaScript. I but a bandaid on it above with the help of @samreid. Then I looked at the implementation and decided we can trust Display enough to add assertions. I'll go back if CT complains.  It's a good reminder that it would be nice if we had 100% TypeScript coverage.
Doc has been updated.

I'll check back to see about CT before closing.

All is looking good here! Closing


14 32 added errors for config 103 143

Added error to configuration for validation rule 103. Related to #14
Added error to configuration for validation rule 143. Related to #32

These two functions have now been added to #280 and #281


[#140222101]declaration overview: enable visibility in non-open framework statuses

Story https://www.pivotaltracker.com/story/show/140222101
In the story it was decided to cope with all complete declarations for non-open frameworks and ignore whether a supplier was onFramework or not. Suppliers with non-complete declarations for a framework will receive a 410 (see code for reasoning).
One consequence of this is that non-open frameworks end up with no introductory text or paragraph on this page.

One thing still TBD is where/how this page will get linked to from the framework dashboard in non-open states. Currently it just isn't (but navigating to the url manually will allow a user to see this page, which could still be useful to allow an admin to direct a user to this page if there's some dispute...)

It might be nice to have conditional breadcrumb text once the framework is in pending/standstill/live - it still says "Apply to ":

But the page it links to has the title changed to "Your  application", which could be the same for the breadcrumb here.

Other than the breadcrumbs thing I think this is good.  And we can probably live with them if we have to.

Yes I think we should do something about the breadcrumb... I'd rather do something more general than a little ad hoc fix here, but... scope creep...

Still need to fix example g7 declaration for this...

Couple of minor final comments but this looks good :+1: after a rebase, with or without suggested changes.


pre-req installer script

Hi Michael,
for those who create on-demand/clean VM for tkng lab cluster it might be quite compelling to have an installer script that installs the prerequisite components.
For this purpose I created a simple bash script that installs latest stable versions of docker-ce, kubectl, helm and make for Ubuntu 20.4 LTS AMD64.
https://gist.github.com/hellt/61242c680c78c3c813f20ecb9577a93e
Feel free to consider if its worth adding a link to it to the docs
ADD1: later I will update it to have extras installed as well (k9s, auto-completions, etc)

Thanks, this is very helpful. I've added a reference to it in adc48e12b3f1c9e85b5114a68fd9666ffc293f2e