11package org .flossware .curses .api ;
22
33import org .flossware .curses .events .MouseEvent ;
4+ import java .util .AbstractList ;
45import java .util .ArrayList ;
6+ import java .util .Collection ;
57import java .util .List ;
8+ import java .util .ListIterator ;
69import java .util .SequencedCollection ;
710
811import static org .flossware .curses .api .Constants .*;
912
1013public class Container extends Component {
11- protected final List <Component > children = new ArrayList <>() ;
14+ protected final List <Component > children ;
1215 protected LayoutManager layoutManager ;
1316 private boolean layoutValid = false ;
1417 private int lastLayoutWidth = NO_INDEX ;
@@ -17,6 +20,13 @@ public class Container extends Component {
1720 // Snapshot cache to reduce GC pressure (Issue #71)
1821 private List <Component > cachedSnapshot ;
1922 private int lastSnapshotSize = -1 ;
23+ private long modificationCount = 0 ;
24+ private long cachedSnapshotModCount = -1 ;
25+
26+ public Container () {
27+ // Wrap children list with mutation tracking to detect external modifications (Issue #207)
28+ this .children = new MutationTrackingList (new ArrayList <>(), () -> modificationCount ++);
29+ }
2030
2131 public SequencedCollection <Component > getChildren () {
2232 return children ;
@@ -28,7 +38,8 @@ public void add(Component child) {
2838 children .add (child );
2939 child .setParent (this );
3040 layoutValid = false ; // Invalidate layout
31- invalidateSnapshot (); // Invalidate cached snapshot
41+ cachedSnapshot = null ; // Invalidate cached snapshot
42+ modificationCount ++; // Also increment for external mutation detection
3243 } finally {
3344 renderLock .unlock ();
3445 }
@@ -41,16 +52,22 @@ public void remove(Component child) {
4152 children .remove (child );
4253 child .setParent (null );
4354 layoutValid = false ; // Invalidate layout
44- invalidateSnapshot (); // Invalidate cached snapshot
55+ cachedSnapshot = null ; // Invalidate cached snapshot
56+ modificationCount ++; // Also increment for external mutation detection
4557 } finally {
4658 renderLock .unlock ();
4759 }
4860 repaint ();
4961 }
5062
5163 public void setLayout (LayoutManager layout ) {
52- this .layoutManager = layout ;
53- layoutValid = false ; // Invalidate layout
64+ renderLock .lock ();
65+ try {
66+ this .layoutManager = layout ;
67+ layoutValid = false ; // Invalidate layout
68+ } finally {
69+ renderLock .unlock ();
70+ }
5471 }
5572
5673 public void doLayout () {
@@ -77,27 +94,54 @@ public void invalidateLayout() {
7794 }
7895
7996 /**
80- * Invalidates the cached children snapshot when the children list is modified.
81- * This is called internally when add/remove operations occur.
97+ * Detects external mutations to the children list by checking modification count.
98+ * This handles cases where external code mutates children via getChildren()
99+ * bypassing the add/remove methods (e.g., Dialog.show() calls getChildren().addLast()).
100+ *
101+ * Returns true if children list has been modified since last snapshot was cached.
82102 */
83- private void invalidateSnapshot () {
84- lastSnapshotSize = -1 ;
85- cachedSnapshot = null ;
103+ private boolean detectExternalMutation () {
104+ return modificationCount != cachedSnapshotModCount ;
105+ }
106+
107+ /**
108+ * Returns a snapshot of the current children list.
109+ * This method is optimized to reuse the cached snapshot when the children list
110+ * hasn't changed, reducing GC pressure. Uses modification count to detect
111+ * external mutations via getChildren() that would bypass size-only checking.
112+ * Must be called by subclasses that need to iterate children while performing
113+ * temporary mutations (e.g., ScrollPane).
114+ *
115+ * @return a snapshot of the children list that is safe to iterate
116+ */
117+ protected List <Component > getChildrenSnapshot () {
118+ renderLock .lock ();
119+ try {
120+ if (cachedSnapshot == null || detectExternalMutation ()) {
121+ // Children list changed (size change or external mutation), create new snapshot
122+ cachedSnapshot = new ArrayList <>(children );
123+ cachedSnapshotModCount = modificationCount ;
124+ lastSnapshotSize = children .size ();
125+ }
126+ return cachedSnapshot ;
127+ } finally {
128+ renderLock .unlock ();
129+ }
86130 }
87131
88132 @ Override
89133 public void paint (char [][] buffer ) {
90134 // Use cached snapshot to avoid ArrayList allocation on every frame (Issue #71)
91- // Only allocate when children list has actually changed in size.
135+ // Detects both size changes and external mutations via modification counter (Issue #207)
92136 // This optimization reduces GC pressure at high frame rates when children list is stable.
93137 List <Component > snapshot ;
94138 renderLock .lock ();
95139 try {
96- int currentSize = children .size ();
97- if (cachedSnapshot == null || lastSnapshotSize != currentSize ) {
98- // Children list changed, create new snapshot
140+ if (cachedSnapshot == null || detectExternalMutation ()) {
141+ // Children list changed (size change or external mutation), create new snapshot
99142 cachedSnapshot = new ArrayList <>(children );
100- lastSnapshotSize = currentSize ;
143+ cachedSnapshotModCount = modificationCount ;
144+ lastSnapshotSize = children .size ();
101145 }
102146 snapshot = cachedSnapshot ;
103147 } finally {
@@ -166,4 +210,131 @@ protected void drawBorder(char[][] buffer) {
166210 buffer [y + height - 1 ][x + width - 1 ] = '+' ;
167211 }
168212 }
213+
214+ /**
215+ * Wrapper list that tracks mutations to detect external modifications via getChildren() (Issue #207).
216+ * Intercepts all modification methods and invokes a callback to increment modification counter.
217+ * Implements both List and SequencedCollection for full compatibility.
218+ */
219+ private static class MutationTrackingList extends AbstractList <Component > implements SequencedCollection <Component > {
220+ private final List <Component > delegate ;
221+ private final Runnable onMutation ;
222+
223+ MutationTrackingList (List <Component > delegate , Runnable onMutation ) {
224+ this .delegate = delegate ;
225+ this .onMutation = onMutation ;
226+ }
227+
228+ @ Override
229+ public int size () {
230+ return delegate .size ();
231+ }
232+
233+ @ Override
234+ public Component get (int index ) {
235+ return delegate .get (index );
236+ }
237+
238+ @ Override
239+ public Component set (int index , Component element ) {
240+ onMutation .run ();
241+ return delegate .set (index , element );
242+ }
243+
244+ @ Override
245+ public void add (int index , Component element ) {
246+ onMutation .run ();
247+ delegate .add (index , element );
248+ }
249+
250+ @ Override
251+ public Component remove (int index ) {
252+ onMutation .run ();
253+ return delegate .remove (index );
254+ }
255+
256+ @ Override
257+ public boolean addAll (Collection <? extends Component > c ) {
258+ if (!c .isEmpty ()) {
259+ onMutation .run ();
260+ }
261+ return delegate .addAll (c );
262+ }
263+
264+ @ Override
265+ public boolean addAll (int index , Collection <? extends Component > c ) {
266+ if (!c .isEmpty ()) {
267+ onMutation .run ();
268+ }
269+ return delegate .addAll (index , c );
270+ }
271+
272+ @ Override
273+ public boolean removeAll (Collection <?> c ) {
274+ int sizeBefore = delegate .size ();
275+ boolean changed = delegate .removeAll (c );
276+ if (changed ) {
277+ onMutation .run ();
278+ }
279+ return changed ;
280+ }
281+
282+ @ Override
283+ public boolean retainAll (Collection <?> c ) {
284+ int sizeBefore = delegate .size ();
285+ boolean changed = delegate .retainAll (c );
286+ if (changed ) {
287+ onMutation .run ();
288+ }
289+ return changed ;
290+ }
291+
292+ @ Override
293+ public void clear () {
294+ if (!delegate .isEmpty ()) {
295+ onMutation .run ();
296+ }
297+ delegate .clear ();
298+ }
299+
300+ // SequencedCollection methods
301+ @ Override
302+ public Component getFirst () {
303+ return delegate .getFirst ();
304+ }
305+
306+ @ Override
307+ public Component getLast () {
308+ return delegate .getLast ();
309+ }
310+
311+ @ Override
312+ public void addFirst (Component e ) {
313+ onMutation .run ();
314+ delegate .add (0 , e );
315+ }
316+
317+ @ Override
318+ public void addLast (Component e ) {
319+ onMutation .run ();
320+ delegate .add (e );
321+ }
322+
323+ @ Override
324+ public Component removeFirst () {
325+ onMutation .run ();
326+ return delegate .removeFirst ();
327+ }
328+
329+ @ Override
330+ public Component removeLast () {
331+ onMutation .run ();
332+ return delegate .removeLast ();
333+ }
334+
335+ @ Override
336+ public List <Component > reversed () {
337+ return delegate .reversed ();
338+ }
339+ }
169340}
0 commit comments