<!DOCTYPE html>
<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
  </head>
  <body>
    <p>Hi Andy / List,</p>
    <p>I've given this some thought first, without looking too much at
      the proposal.</p>
    <p>In my view, focus traversal should be implemented using events,
      and FX should provide standard handling of these events controlled
      with properties (potentially even CSS stylable for easy mass
      changing of the default navigation policy).<br>
      <br>
      ## KeyEvent and TraversalEvent separation<br>
    </p>
    <p>I think the cleanest implementation would be to implement a
      KeyEvent listener on Scene that takes any unused KeyEvents, checks
      if they're considered navigation keys, and converts these keys to
      a new type of event, the TraversalEvent. The TraversalEvent is
      then fired at the original target. The TraversalEvent is
      structured into Directional and Logical sub types, and has leaf
      types UP/DOWN/LEFT/RIGHT and NEXT/PREVIOUS.  Scene is the logical
      place to handle this as without a Scene there is no focus owner,
      and so there is no point in doing focus traversal.</p>
    <p>This separation of KeyEvents into TraversalEvents achieves the
      following:</p>
    <p>- User can decide to act on **any** key, even navigation keys,
      without the system interfering by consuming keys early,
      unexpectedly or even consuming these keys without doing anything
      (sometimes keys get consumed that don't actually change
      focus...).  The navigation keys have many possible dual purposes,
      and robbing the user of the opportunity to use them due to an
      overzealous component interpreting them as traversal keys is very
      annoying.  Dual purposes include for example cursor control in
      TextField/TextArea, Scrollbars, etc.  The user should have the
      same control here as these FX controls have.<br>
      <br>
      - Scene is interpreting the KeyEvents, and this interpretation is
      now controllable.  If I want a Toolbar (or the whole application)
      to react to WASD navigation keys, then installing a KeyEvent
      handler at Scene level or at any intermediate Parent level that
      converts WASD to UP/LEFT/DOWN/RIGHT Traversal events would affect
      this change easily.<br>
      <br>
      - The separation also allows to block Focus Traversal only,
      without blocking the actual Keys involved.  If I want to stop a
      Toolbar from reacting to LEFT/RIGHT, but I need those keys higher
      up in the hierarchy, then I'm screwed.  With the separation, the
      key events are unaffected, and I can block Toolbars from reacting
      specifically to traversal events only.<br>
    </p>
    <p>## Traversal Policy Properties on Parent<br>
      <br>
      I think FX should provide several policies out of the box, based
      on common navigation patterns.  The goal here is to have policies
      in place that cover all use cases in current FX provided
      controls.  This will provide a good base that will cover probably
      all realistic work loads that custom controls may have. The goal
      is not to support every esoteric form of navigation, instead an
      escape hatch will be provided in the form of disabling the
      standard navigation.</p>
    <p>In order to achieve this, I think Parent should get two new
      properties, which control how it will react to Directional and
      Logical navigation.  These will have default values that allow
      navigation to flow from Node to Node within a Parent and from
      Parent to its Parent when navigation options in a chosen direction
      are exhausted within a Parent.  Custom controls like Combo boxes,
      Toolbars, Button groups, etc, can change the default provided by a
      Parent (similar to how some controls change the mouse transparent
      flag default).</p>
    <p>These two properties should cover all realistic needs, and IMHO
      should be considered to be CSS stylable in the future to allow
      easy changing of default policies of controls (ie. want all
      Toolbars to react differently to navigation keys, then just style
      the appropriate property for all toolbars in one go).</p>
    <p>Parent will use these properties to install an event handler that
      reacts to TraversalEvents (not KeyEvents).  This handler can be
      fully disabled, or overridden (using setOnTraversalEvent).</p>
    <p>- logicalTraversalPolicy<br>
      - directionalTraversalPolicy</p>
    <p>The properties can be set with a value from a TraversalPolicy
      enum.  I would suggest the following options:</p>
    <p>- OPEN<br>
    </p>
    <p>This policy should be the default policy for all Parents.  It
      will act and consume a given TraversalEvent only when there is a
      suitable target within its hierarchy.  If there is no suitable
      target, or the target would remain unchanged, the event is NOT
      consumed and left to bubble up, allowing its parent(s) to act on
      it instead.<br>
    </p>
    <p>- CONFINED</p>
    <p>This policy consumes all TraversalEvents, regardless of whether
      there is something to navigate to or not.  This policy is suitable
      for controls that have some kind of substructure that we don't
      want to accidentally exit with either Directional or Logical
      navigation.  In most cases, you only want to set one of the
      properties to CONFINED as otherwise there would be no keyboard
      supported way to exit your control.  This is a suitable policy for
      say button groups, toolbars, comboboxes, etc.<br>
    </p>
    <p>- CYCLIC</p>
    <p>Similar to CONFINED but instead of stopping navigation at the
      controls logical boundaries, the navigation wraps around to the
      logical start.  For example, when were positioned on the right
      most button in a button group, pressing RIGHT again would navigate
      to the left most button.<br>
      <br>
      - IGNORED</p>
    <p>This is similar to the mouseTransparent property, and basically
      leaves the TraversalEvent to bubble up.  This policy allows you to
      completely disable directional and/or logical navigation for a
      control.  Useful if you want to install your own handler (the
      escape hatch) but still want to keep either the default
      directional or logical navigation.</p>
    <p>Possible other options for this enum could include a version that
      consumes all TraversalEvents (BLOCK) but I don't see a use for it
      at the moment.  There may also be variants of CONFINED and CYCLIC
      that make an exception for cases where there is only a single
      choice available.  A ButtonGroup for example may want to react
      differently depending on whether it has 0, 1 or more buttons. 
      Whether these should be enshrined with a custom enum value, or
      perhaps a flag, or just left up to a custom implementation is
      something we'd need to decide still.<br>
    </p>
    <p>## Use Cases<br>
    </p>
    1) User wants to change the behavior of a control from its default
    to something else (ie. a Control that is CYCLIC can be changed to
    OPEN or CONFINED)
    <p>Just call the setters with the appropriate preferred policy. 
      This could be done in CSS for maximum convenience to enable a
      global change of all similar controls.</p>
    <p>2) User wants to act on Traversal events that the standard policy
      leaves to bubble up</p>
    <p>Just install a Traversal event handler either on the control or
      on its parent (depending on their needs).  A potential action to
      an unused Traversal event could be to close a Dialog/Toast popup,
      or a custom behavior like selecting the first/last item or
      next/previous row (ie. if I press "RIGHT" and there is no further
      right item, a user could decide to have this select the first item
      again in the current Row or the first item in the **next** Row).<br>
    </p>
    <p>3) User wants to do crazy custom navigation</p>
    <p>Set both policies to IGNORED, then install your own event handler
      (or use the setOnTraversalHandler to completely override the
      handler).  Now react on the Traversal events, consuming them at
      will and changing focus to whatever control you desire.</p>
    <p>4) User wants to change what keys are considered navigation keys</p>
    <p>Install event handler on Scene (or any intermediate Parent) for
      KeyEvents, interpret WASD keys as UP/LEFT/DOWN/RIGHT and sent out
      a corresponding Traversal event</p>
    <p>5) User wants to use keys that are considered navigation keys for
      their own purposes</p>
    <p>Just install a KeyEvent handler as usual, without having to worry
      that Skins/Controls eat these events before you can get to them<br>
      <br>
      6) User wants to stop a control from reacting to traversal events,
      without filtering navigation keys completely<br>
    </p>
    <p>With the separation of unconsumed KeyEvents into TraversalEvents,
      a user can now block only the latter to achieve this goal without
      having to blanket block certain KeyEvents.<br>
    </p>
    <p>-----</p>
    <p>About the Proposal:</p>
    <p>I think the Goals are fine as stated, although I think we differ
      on what the Traversal events signify.</p>
    <p>I think CSS support should be considered a possible future goal. 
      The proposal should therefore take into account that we may want
      to offer this in the future.</p>
    <p>Motivation looks okay.</p>
    <p>> The focus traversal is provided by the FocusTraversal class
      which offers static methods for traversing focus in various
      directions, determined by the TraversalDirection enum.</p>
    <p>I think these methods don't need to be exposed with a good
      selection of standard TraversalPolicy options.  After all, there
      are only so many ways that you can do a sensible navigation action
      without confusing the user, and therefore I think these policy
      options will cover 99% of the use cases already.  For the left
      over 1% we could **consider** providing these focus traversal
      functions as a separate public API, but I would have them return
      the Node they would suggest, and leave the final decision to call
      requestFocus up to the caller.  Initially however I think there is
      already more than enough power for custom implementations to
      listen to Traversal events and do their own custom navigation.  If
      it is not similar to one of the standard navigation options, the
      traverseUp/Down functions won't be of much use then anyway.<br>
    </p>
    <p>About your typical example:</p>
    <p>    Node from = ...<br>
          switch (((KeyEvent)event).getCode()) {<br>
          case UP:<br>
              FocusTraversal.traverse(from, TraversalDirection.UP,
      TraversalMethod.KEY);<br>
              event.consume();<br>
              break;<br>
          case DOWN:<br>
              // or use the convenience method<br>
              FocusTraversal.traverseDown(from);<br>
              event.consume();<br>
              break;<br>
          }<br>
    </p>
    <p>I think this is not a good way to deal with events.</p>
    <p>1) The event is consumed regardless of the outcome of traverse. 
      What if focus did not change?  Should the event be consumed?</p>
    <p>2) This is consuming KeyEvents directly, robbing the user of the
      opportunity to act on keys considered "special" by FX.</p>
    <p>3) This code is not only consuming KeyEvents directly, but also
      deciding what keys are navigation keys.</p>
    <p>So I think this example code should be different. However, first
      I expect that in most cases, configuring a different traversal
      policy on your Parent subclass will already be sufficient in
      almost all cases (especially if we look at FX current controls and
      see if the suggested policies would cover those use cases).  So
      this code will almost never be needed.  However, in the event that
      you need something even more specific, you may consider handling
      Traversal events directly.  In which case the code should IMHO
      look something like this:</p>
    <p>    Node from = ...<br>
      <br>
          Node result = switch(traversalEvent.getEventType()) {<br>
            case TraversalEvent.UP -> FocusTraversals.findUp(from);<br>
            case TraversalEvent.DOWN ->
      FocusTraversals.findDown(from);<br>
            // etc<br>
         }<br>
      <br>
          if (result != null) {<br>
               result.requestFocus();<br>
               traversalEvent.consume();<br>
          }<br>
    </p>
    <p>Note that the above code leaves the final decision to call
      requestFocus up to the caller.  It also allows the caller to
      distinguish between the case where there is no suitable Node in
      the indicated direction and act accordingly.  </p>
    <p>This allows it to NOT consume the event if it prefers its Parent
      to handle it (if the control doesn't want CYCLIC or CONFINED style
      navigation).  It also allows it to further scrutinize the
      suggested Node, and if it decides it does not like it (due to some
      property or CSS style or whatever) it may follow up with another
      findXXX call or some other option to pick the Node it wants.  It
      also allows (in the case of no Node being found) to pick its own
      preferred Node in those cases.  In other words, it is just far
      more flexible.</p>
    <p>I'm not sure yet where to place these static helper methods (if
      we decide to expose them at all initially), or even if they should
      be static.  Given that its first parameter is always a Node, a
      non-static location for them could simply be on Node itself, in
      which case the calling convention would become "Node result  =
      from.findTraversableUp()" (suggested name only)<br>
    </p>
    <p>> Focus traversals generate a new type of event, encapsulated
      by the class TraversalEvent which extends javafx.event.Event,
      using the event type TraversalEvent.NODE_TRAVERSED.</p>
    <p>What is the point of this event?  If you want to know that focus
      changed, you can add a listener to Scene.focusOwnerProperty.  What
      does it mean if I filter this event?  What if I consume it?  I
      don't think this should be an event at all, unless implemented as
      I suggested above, where consuming/filtering/bubbling can be used
      to control how controls will react to navigation events.</p>
    <p>--John<br>
    </p>
    <p><br>
    </p>
    <p><br>
    </p>
    On 03/09/2024 21:33, Andy Goryachev wrote:<br>
    <blockquote type="cite"
cite="mid:BL3PR10MB6185A474EE760F9DAAF9B10AE5932@BL3PR10MB6185.namprd10.prod.outlook.com">
      <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
      <meta name="Generator"
        content="Microsoft Word 15 (filtered medium)">
      <style>@font-face
        {font-family:"Cambria Math";
        panose-1:2 4 5 3 5 4 6 3 2 4;}@font-face
        {font-family:"Yu Gothic";
        panose-1:2 11 4 0 0 0 0 0 0 0;}@font-face
        {font-family:Aptos;
        panose-1:2 11 0 4 2 2 2 2 2 4;}@font-face
        {font-family:"Iosevka Fixed SS16";
        panose-1:2 0 5 9 3 0 0 0 0 4;}@font-face
        {font-family:"Times New Roman \(Body CS\)";
        panose-1:2 11 6 4 2 2 2 2 2 4;}@font-face
        {font-family:"\@Yu Gothic";
        panose-1:2 11 4 0 0 0 0 0 0 0;}p.MsoNormal, li.MsoNormal, div.MsoNormal
        {margin:0in;
        font-size:12.0pt;
        font-family:"Aptos",sans-serif;
        mso-ligatures:standardcontextual;}a:link, span.MsoHyperlink
        {mso-style-priority:99;
        color:#467886;
        text-decoration:underline;}span.EmailStyle17
        {mso-style-type:personal-compose;
        font-family:"Iosevka Fixed SS16";
        color:windowtext;}span.outlook-search-highlight
        {mso-style-name:outlook-search-highlight;}p.p1, li.p1, div.p1
        {mso-style-name:p1;
        mso-margin-top-alt:auto;
        margin-right:0in;
        mso-margin-bottom-alt:auto;
        margin-left:0in;
        font-size:12.0pt;
        font-family:"Aptos",sans-serif;}.MsoChpDefault
        {mso-style-type:export-only;}div.WordSection1
        {page:WordSection1;}</style>
      <div class="WordSection1">
        <p class="MsoNormal"><span
style="font-size:11.0pt;font-family:"Iosevka Fixed SS16";color:#212121">Dear
            fellow developers:<o:p></o:p></span></p>
        <p class="MsoNormal"
style="caret-color: rgb(33, 33, 33);font-variant-caps: normal;orphans: auto;text-align:start;widows: auto;-webkit-text-stroke-width: 0px;word-spacing:0px">
          <span
style="font-size:11.0pt;font-family:"Iosevka Fixed SS16";color:#212121"> <o:p></o:p></span></p>
        <p class="MsoNormal"
style="caret-color: rgb(33, 33, 33);font-variant-caps: normal;orphans: auto;text-align:start;widows: auto;-webkit-text-stroke-width: 0px;word-spacing:0px">
          <span
style="font-size:11.0pt;font-family:"Iosevka Fixed SS16";color:#212121">I'd
            like to propose the public focus traversal API:<o:p></o:p></span></p>
        <p class="MsoNormal"
style="caret-color: rgb(33, 33, 33);font-variant-caps: normal;orphans: auto;text-align:start;widows: auto;-webkit-text-stroke-width: 0px;word-spacing:0px">
          <span
style="font-size:11.0pt;font-family:"Iosevka Fixed SS16";color:#212121"> <o:p></o:p></span></p>
        <p class="p1"
style="margin:0in;caret-color: rgb(33, 33, 33);font-variant-caps: normal;orphans: auto;text-align:start;widows: auto;-webkit-text-stroke-width: 0px;word-spacing:0px">
          <span
style="font-size:11.0pt;font-family:"Iosevka Fixed SS16";color:#212121"><a
href="https://github.com/andy-goryachev-oracle/Test/blob/main/doc/FocusTraversal/FocusTraversal.md"
title="https://github.com/andy-goryachev-oracle/Test/blob/main/doc/FocusTraversal/FocusTraversal.md"
              moz-do-not-send="true"><span style="color:#0078D7">https://github.com/andy-goryachev-oracle/Test/blob/main/doc/<span
                  class="outlook-search-highlight">Focus</span>Traversal/<span
                  class="outlook-search-highlight">Focus</span>Traversal.md</span></a><o:p></o:p></span></p>
        <p class="MsoNormal"
style="caret-color: rgb(33, 33, 33);font-variant-caps: normal;orphans: auto;text-align:start;widows: auto;-webkit-text-stroke-width: 0px;word-spacing:0px">
          <span
style="font-size:11.0pt;font-family:"Iosevka Fixed SS16";color:#212121"> <o:p></o:p></span></p>
        <p class="MsoNormal"
style="caret-color: rgb(33, 33, 33);font-variant-caps: normal;orphans: auto;text-align:start;widows: auto;-webkit-text-stroke-width: 0px;word-spacing:0px">
          <span
style="font-size:11.0pt;font-family:"Iosevka Fixed SS16";color:#212121">Draft
            PR:<o:p></o:p></span></p>
        <p class="MsoNormal"><span
style="font-size:11.0pt;font-family:"Iosevka Fixed SS16";color:#212121"><o:p> </o:p></span></p>
        <p class="MsoNormal"><span
style="font-size:11.0pt;font-family:"Iosevka Fixed SS16";color:#212121"><a
              href="https://github.com/openjdk/jfx/pull/1555"
              moz-do-not-send="true" class="moz-txt-link-freetext">https://github.com/openjdk/jfx/pull/1555</a></span><span
style="font-size:11.0pt;font-family:"Iosevka Fixed SS16""><o:p></o:p></span></p>
        <p class="MsoNormal"><span
style="font-size:11.0pt;font-family:"Iosevka Fixed SS16";color:#212121"><o:p> </o:p></span></p>
        <p class="MsoNormal"><span
style="font-size:11.0pt;font-family:"Iosevka Fixed SS16";color:#212121">Your
            comments and suggestions will be warmly accepted and
            appreciated.<o:p></o:p></span></p>
        <p class="MsoNormal"><span
style="font-size:11.0pt;font-family:"Iosevka Fixed SS16";color:#212121"> <o:p></o:p></span></p>
        <p class="MsoNormal"
style="caret-color: rgb(33, 33, 33);font-variant-caps: normal;orphans: auto;text-align:start;widows: auto;-webkit-text-stroke-width: 0px;word-spacing:0px">
          <span
style="font-size:11.0pt;font-family:"Iosevka Fixed SS16";color:#212121">Thank
            you<o:p></o:p></span></p>
        <p class="MsoNormal"
style="caret-color: rgb(33, 33, 33);font-variant-caps: normal;orphans: auto;text-align:start;widows: auto;-webkit-text-stroke-width: 0px;word-spacing:0px">
          <span
style="font-size:11.0pt;font-family:"Iosevka Fixed SS16";color:#212121"> <o:p></o:p></span></p>
        <p class="MsoNormal"
style="caret-color: rgb(33, 33, 33);font-variant-caps: normal;orphans: auto;text-align:start;widows: auto;-webkit-text-stroke-width: 0px;word-spacing:0px">
          <span
style="font-size:11.0pt;font-family:"Iosevka Fixed SS16";color:#212121">-andy<o:p></o:p></span></p>
        <p class="MsoNormal"><span
style="font-size:11.0pt;font-family:"Iosevka Fixed SS16""><o:p> </o:p></span></p>
      </div>
    </blockquote>
  </body>
</html>