1616
1717package com .techsenger .tabshell .core .dialog ;
1818
19+ import com .techsenger .annotations .Unmodifiable ;
1920import com .techsenger .tabshell .core .popup .AbstractPopupFxView ;
2021import com .techsenger .tabshell .material .button .ResultButton ;
2122import com .techsenger .tabshell .material .button .ResultButtonName ;
2728import com .techsenger .toolkit .fx .RegionResizer ;
2829import com .techsenger .toolkit .fx .Spacer ;
2930import com .techsenger .toolkit .fx .pulse .LayoutPhase ;
30- import com .techsenger .toolkit .fx .pulse .LayoutPulseListener ;
3131import com .techsenger .toolkit .fx .utils .ButtonUtils ;
3232import com .techsenger .toolkit .fx .value .ValueUtils ;
33+ import java .util .Arrays ;
3334import java .util .HashMap ;
3435import java .util .List ;
3536import java .util .Map ;
3637import java .util .stream .Stream ;
38+ import javafx .application .Platform ;
3739import javafx .beans .binding .Bindings ;
3840import javafx .beans .property .BooleanProperty ;
3941import javafx .beans .property .DoubleProperty ;
4446import javafx .geometry .Orientation ;
4547import javafx .geometry .Pos ;
4648import javafx .scene .Cursor ;
47- import javafx .scene .Node ;
4849import javafx .scene .control .Button ;
4950import javafx .scene .control .Label ;
5051import javafx .scene .input .MouseEvent ;
@@ -116,8 +117,6 @@ public void remove() {
116117
117118 private final BooleanProperty resizable = new SimpleBooleanProperty ();
118119
119- private final BooleanProperty buttonWidthEqual = new SimpleBooleanProperty (false );
120-
121120 private final DoubleProperty minWidth = new SimpleDoubleProperty ();
122121
123122 private final DoubleProperty minHeight = new SimpleDoubleProperty ();
@@ -128,14 +127,6 @@ public void remove() {
128127
129128 private final Map <ResultButtonName , Button > buttonsByName = new HashMap <>();
130129
131- private boolean buttonWidthListenerAdded = false ;
132-
133- private final LayoutPulseListener buttonWidthListener = () -> {
134- makeResultButtonsEqual ();
135- buttonWidthListenerAdded = false ;
136- return false ;
137- };
138-
139130 /**
140131 * While dragging we need the difference. So, we keep in this variable previous value.
141132 */
@@ -219,11 +210,6 @@ public void setIcon(Icon<?> icon) {
219210 }
220211 }
221212
222- @ Override
223- public void setButtonWidthEqual (boolean value ) {
224- this .buttonWidthEqual .set (value );
225- }
226-
227213 @ Override
228214 public Composer getComposer () {
229215 return (Composer ) super .getComposer ();
@@ -274,10 +260,6 @@ public boolean isResizable() {
274260 return resizable .get ();
275261 }
276262
277- public boolean isButtonWidthEqual () {
278- return buttonWidthEqual .get ();
279- }
280-
281263 protected BooleanProperty activeProperty () {
282264 return resizable ;
283265 }
@@ -286,10 +268,6 @@ protected BooleanProperty resizableProperty() {
286268 return resizable ;
287269 }
288270
289- protected BooleanProperty buttonWidthEqualProperty () {
290- return buttonWidthEqual ;
291- }
292-
293271 protected BooleanProperty outOfBoundsAllowedProperty () {
294272 return outOfBoundsAllowed ;
295273 }
@@ -358,11 +336,6 @@ protected void bind() {
358336 @ Override
359337 protected void addListeners () {
360338 super .addListeners ();
361- ValueUtils .callAndAddListener (this .buttonWidthEqual , (ov , oldV , newV ) -> {
362- if (newV ) {
363- updateButtonsEqual ();
364- }
365- });
366339 ValueUtils .callAndAddListener (this .active , (ov , oldV , newV ) -> {
367340 dialogBox .pseudoClassStateChanged (INACTIVE_PSEUDO_CLASS , !newV );
368341 });
@@ -382,27 +355,34 @@ protected void addHandlers() {
382355 }
383356
384357 /**
385- * Makes all buttons in left and right boxes equal .
358+ * Makes the specified buttons equal in width .
386359 */
387- protected void makeResultButtonsEqual () {
388- var buttons = getResultButtons ();
389- ButtonUtils .makeEqualWidthBySize (buttons , true );
360+ protected void makeEqualWidth (Button ... buttons ) {
361+ makeEqualWidth (Arrays .asList (buttons ));
390362 }
391363
392364 /**
393- * Returns added buttons from the left and right boxes.
394- *
395- * @return list of all added buttons or empty collection
365+ * Makes the specified buttons equal in width.
396366 */
397- protected List <ResultButton > getResultButtons () {
398- Stream <Node > allChildren = Stream .concat (
399- leftButtonBox .getChildren ().stream (),
400- rightButtonBox .getChildren ().stream ()
401- );
402- return allChildren
403- .filter (ResultButton .class ::isInstance )
404- .map (ResultButton .class ::cast )
405- .toList ();
367+ protected void makeEqualWidth (List <Button > buttons ) {
368+ // To avoid flickering, both button boxes are hidden for one pulse:
369+ // [Before Pulse N] hide buttons, register POST-layout listener
370+ // [Pulse N | Layout] calculate sizes of invisible buttons
371+ // [Pulse N | POST] equalize button widths, schedule setVisible(true) via runLater
372+ // [Pulse N | Render] nothing visible, nothing rendered
373+ // [Between N and N+1] runLater executes → buttons marked visible + dirty
374+ // [Pulse N+1 | Layout] recalculate layout with correct minWidth
375+ // [Pulse N+1 | Render] buttons appear on screen with correct size
376+ rightButtonBox .setVisible (false );
377+ leftButtonBox .setVisible (false );
378+ getPulseListenerManager ().addListener (LayoutPhase .POST , () -> {
379+ ButtonUtils .makeEqualWidthBySize (buttons , true );
380+ Platform .runLater (() -> {
381+ rightButtonBox .setVisible (true );
382+ leftButtonBox .setVisible (true );
383+ });
384+ return false ;
385+ });
406386 }
407387
408388 /**
@@ -433,6 +413,43 @@ protected void unregisterButtons(ResultButton... buttons) {
433413 }
434414 }
435415
416+ /**
417+ * Returns an unmodifiable list of buttons located in the left button container.
418+ *
419+ * @param resultButtonsOnly if {@code true}, only buttons of type {@link ResultButton}
420+ * are returned; otherwise all {@link Button} instances are included
421+ * @return unmodifiable list of buttons from the left {@code HBox}
422+ */
423+ protected @ Unmodifiable List <Button > getLeftButtons (boolean resultButtonsOnly ) {
424+ return getButtons (leftButtonBox , resultButtonsOnly );
425+ }
426+
427+ /**
428+ * Returns an unmodifiable list of buttons located in the right button container.
429+ *
430+ * @param resultButtonsOnly if {@code true}, only buttons of type {@link ResultButton}
431+ * are returned; otherwise all {@link Button} instances are included
432+ * @return unmodifiable list of buttons from the right {@code HBox}
433+ */
434+ protected @ Unmodifiable List <Button > getRightButtons (boolean resultButtonsOnly ) {
435+ return getButtons (rightButtonBox , resultButtonsOnly );
436+ }
437+
438+ /**
439+ * Returns an unmodifiable combined list of buttons from both left and right button containers.
440+ *
441+ * <p>If {@code resultButtonsOnly} is {@code true}, only {@link ResultButton} instances are included.</p>
442+ *
443+ * @param resultButtonsOnly filter flag for selecting only result buttons
444+ * @return unmodifiable list of all matching buttons from both sides
445+ */
446+ protected @ Unmodifiable List <Button > getButtons (boolean resultButtonsOnly ) {
447+ return Stream .concat (
448+ getLeftButtons (resultButtonsOnly ).stream (),
449+ getRightButtons (resultButtonsOnly ).stream ())
450+ .toList ();
451+ }
452+
436453 /**
437454 * Returns a button box that can contain both result and additional buttons. In other words, it is safe to
438455 * add custom buttons to this box.
@@ -510,7 +527,6 @@ private void addButtons(HBox box, ResultButtonName... names) {
510527 box .getChildren ().add (button );
511528 }
512529 }
513- updateButtonsEqual ();
514530 updateTrap ();
515531 }
516532
@@ -520,11 +536,25 @@ private void removeButtons(HBox box) {
520536 );
521537 }
522538
523- private List <ResultButtonName > getButtons (HBox box ) {
524- return box .getChildren ().stream ()
525- .filter (ResultButton .class ::isInstance )
526- .map (ResultButton .class ::cast )
527- .map (b -> b .getName ())
539+ /**
540+ * Extracts buttons from the given {@link HBox} container with optional filtering by type.
541+ *
542+ * <p>If {@code resultButtonsOnly} is {@code true}, only instances of {@link ResultButton}
543+ * are included; otherwise all {@link Button} instances are returned.</p>
544+ *
545+ * @param buttonBox the container holding button nodes
546+ * @param resultButtonsOnly whether to include only {@link ResultButton} instances
547+ * @return unmodifiable list of buttons contained in the specified box
548+ */
549+ private @ Unmodifiable List <Button > getButtons (HBox buttonBox , boolean resultButtonsOnly ) {
550+ Class <? extends Button > filterClass = Button .class ;
551+ if (resultButtonsOnly ) {
552+ filterClass = ResultButton .class ;
553+ }
554+
555+ return buttonBox .getChildren ().stream ()
556+ .filter (filterClass ::isInstance )
557+ .map (Button .class ::cast )
528558 .toList ();
529559 }
530560
@@ -533,11 +563,4 @@ private void updateTrap() {
533563 this .focusTrap .update ();
534564 }
535565 }
536-
537- private void updateButtonsEqual () {
538- if (!this .buttonWidthListenerAdded ) {
539- getPulseListenerManager ().addListener (LayoutPhase .POST , buttonWidthListener );
540- buttonWidthListenerAdded = true ;
541- }
542- }
543566}
0 commit comments