[Win32] Keep tool bar normal, hot and disabled image lists index-aligned - #3471
[Win32] Keep tool bar normal, hot and disabled image lists index-aligned#3471HeikoKlare wants to merge 5 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
This PR fixes a Win32-specific toolbar rendering defect where a ToolItem could show another item’s hot (hover) or disabled image due to the native image lists (normal/hot/disabled) becoming index-misaligned. It enforces a single authoritative slot index per item across all three image lists to prevent stale or cross-item icon lookups.
Changes:
ToolItem.updateImages: derive the slot index once from the normal image list and write hot/disabled images at that same index; additionally clear the hot slot when the normal image is cleared.ToolBar.updateOrientation: when rebuilding image lists, keep new hot/disabled lists aligned by writing at the normal list’s returned index (instead of independentadd(...)calls).ImageList: introduceputAt(index, image)and implementadd(image)on top of it.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/widgets/ToolItem.java | Keeps normal/hot/disabled image list indices aligned and clears hot slots when the normal image is cleared. |
| bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/widgets/ToolBar.java | Preserves index alignment across rebuilt image lists during orientation updates. |
| bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/internal/ImageList.java | Adds putAt(...) to support caller-chosen indices and refactors add(...) to use it. |
4438236 to
6941171
Compare
| * method does nothing.</li> | ||
| * </ul> | ||
| */ | ||
| public void putAt (int index, Image image) { |
There was a problem hiding this comment.
ImageList now contains both a public putAt(int index, Image image) and a public put(int index, Image image) method where putAt allows appending and put does not. So maybe putOrAppend would be a better name for this method?
There was a problem hiding this comment.
That a valid concern. I had originally named that method putOrAppend before I changed it, but I agree that given the already existing put, this is very misleading. Rather than renaming, I have now merged the two: putAt is gone and put now also accepts the index right after the last slot, appending there.
put previously ignored such an index silently, which is the surprising behaviour of the two. Map.put inserts when the key is absent, so store-or-append is closer to what the name suggests. add keeps its shape (find the first free slot, then put), which now describes exactly what it does.
The change is safe for existing callers: the sole caller outside ToolBar/ToolItem is TabFolder, whose index comes from indexOf and is therefore always inside the list, i.e., it cannot reach the new append path. Every other caller passes a button's iImage, likewise always in range.
I have also rebased this change/PR on a sequence of refactorings for properly encapsulating and aligning the three ImageLists of a ToolBar inside the ToolBar class and a specific three-list container. Only the last of the current commits compared to master covers the actual change of the PR.
I have also added a bunch of tests for preserving the enhanced ImageList behavior.
6941171 to
1b20bec
Compare
ToolItem manipulated ToolBar's three normal/hot/disabled ImageLists directly: reading them via ToolBar's getters, creating them inline when absent, and calling add()/put() on each of them itself. This spread the bookkeeping for a single item's images across both classes and made ToolItem responsible for details that are really ToolBar's to own, such as lazily creating the image lists sized to the first image added. With this change, ToolBar exposes addImage/putImage/clearImage instead, and ToolItem goes through these instead of touching ImageList directly. ToolBar's internal representation (three separate ImageList fields, and their existing setImageList/setHotImageList/setDisabledImageList synchronization methods) is otherwise unchanged. This is a behavior-preserving refactoring: the image lists are still created, filled and synchronized with the same values as before. The disabledImageList != null guard in ToolItem.updateImages is dropped rather than moved: in that branch the item already has an image index, so all three image lists necessarily exist, as they are only ever created together in addImage and cleared together in destroyItem and releaseWidget. Related to eclipse-platform#3466
ToolBar's setImageList/setHotImageList/setDisabledImageList each carried a near-identical copy of the logic to compare the current TB_GET*IMAGELIST handle against the ImageList's handle for the current zoom, and, if different, apply it via TB_SET*IMAGELIST while toggling setDropDownItems around it to avoid a Windows layout glitch. handleDPIChange and addImage each called all three setters in turn whenever any one image list might have changed. This change consolidates that duplicated logic into a single refreshImageLists method operating on the current field values, replacing the three getters and three setters. ToolBar's representation is still three separate ImageList fields. It prepares the ground for introducing a class that owns the three image lists as one unit. This is not a purely behavior-preserving refactoring: refreshImageLists toggles setDropDownItems at most once per refresh batch instead of once per list, and only when the tool bar's buttons are actually being added or recreated (addImage, handleDPIChange) rather than for updateOrientation. Both the toggling and the native updates are skipped entirely when the image lists already set on the tool bar are the current ones, which in particular avoids any native calls for tool bars without images. Related to eclipse-platform#3466
destroyItem (when the last button is removed) and releaseWidget each released the three image lists and cleared their native references with near-identical, duplicated code; updateOrientation released and swapped them inline as well. Consolidates all three into shared clearAndReleaseImageLists/releaseImageLists helpers built on top of refreshImageLists. All of them refresh with itemsChanged=false: in destroyItem and releaseWidget no tool bar buttons remain that the drop-down padding workaround would have to protect, and in updateOrientation the buttons are only re-pointed at their migrated images rather than added or recreated. updateOrientation released the old image lists before pointing the native tool bar at the new ones, leaving a window where the control could reference an already-disposed image list. It now assigns the fields to the freshly created lists upfront, migrating each button's images from the old lists (kept in local variables) into them, and only refreshes the native references and releases the old lists afterwards, matching clearAndReleaseImageLists. Related to eclipse-platform#3466
ToolBar's three image lists were still three separate fields that were created, filled, cleared, moved and released individually in addImage, putImage, destroyItem, releaseWidget and updateOrientation, even though they only ever change together and have to stay index-aligned. This change introduces ToolBarImageLists, which owns the three ImageLists as one unit and offers the operations on them as single calls. Synchronizing the image lists with the native control remains in ToolBar, which retrieves the handles to set from ToolBarImageLists. ToolBar's addImage/putImage/clearImage API (and therefore ToolItem, which only calls that API) is unaffected; this change is entirely internal to ToolBar. This is a behavior-preserving internal refactor; it does not itself change what is rendered. It lays the groundwork for fixing the image-list index-alignment problems tracked in eclipse-platform#3466
A ToolItem could render another item's hot (hover) icon without any multiple monitors or image disposal involved: setting an image, then a hot image, then clearing the image while adding a new item to the tool bar was enough. A tool bar keeps three native image lists (normal, hot, disabled) but each button stores a single image index that addresses all three at once, so the three lists must stay index-aligned. Two things broke that in ToolItem.updateImages, now routed through ToolBarImageLists: - Clearing the normal image while a hot image was still set freed the normal and disabled slots but kept the hot slot occupied. A later item reusing the freed normal slot then rendered the first item's stale hot icon on hover. - A first image was appended to each list with three independent add() calls that could return different indices once the lists' free-slot patterns diverged. Only the normal list's index was written to the button, so its slot could point at another item's hot or disabled image. With this change, we enforce one index per item across all three lists: free the hot slot too when the normal image is cleared, and derive the index once from the normal list and store the hot and disabled image at that same index instead of appending them independently. Storing an image at a given index required ImageList.put(...) to also support the index right after the last one, which previously was one of the out-of-range indices it ignored. Its storing behavior, and that it keeps lists aligned whose free slots differ, is covered by unit tests in ImageListTests. Contributes to eclipse-platform#3466 Co-authored-by: Claude <noreply@anthropic.com>
1b20bec to
63e1819
Compare
Important
This change is based on and should thus be merged after:
Problem
On Windows a
ToolItemcould render another item's hot (hover) or disabled icon — no multiple monitors and no image disposal required. Setting an image, then a hot image, then clearing the image while adding a new item to the tool bar is already enough to trigger it.A tool bar keeps three native image lists (normal, hot, disabled), but each button stores a single image index that addresses all three lists at once. The three lists therefore have to stay index-aligned. Two code paths in
ToolItem.updateImagesbroke that invariant:add()calls. Once the lists' free-slot patterns diverged (as caused by the first defect), those calls could return different indices. Only the normal list's index is written to the button, so its slot could end up pointing at another item's hot or disabled image.Contributes to #3466.
Fix
Enforce one index per item across all three lists:
add()calls returning matching indices).ToolBar.updateOrientationadopts the same discipline.This is implemented with a small
ImageList.putAt(index, image)that stores an image at a caller-chosen index (appending when the index equals the current size, otherwise replacing or, for anullimage, clearing the slot).add(image)is now implemented on top of it by first computing the target index.Reproduction
Run the snippet below and hover the mouse over the second (blue) button:
Before (wrong behavior):

After (corrected behavior):
