Skip to content

Commit 27671b5

Browse files
timholyclaude
andcommitted
Migrate tests, README, and docstrings to the new field-access API
Converts every call site that indexes the `imshow` return value by string to the new struct field syntax (e.g. `result["gui"]["window"]` → `result.window`, `result["roi"]["zoomregion"]` → `result.zoomregion`, `result["roi"]["image roi"]` → `result.image_roi`). Updates the README's tutorial and script-usage examples, the `imshow`/`imshow_gui`/ `scalebar` docstrings, and adds a NEWS.md entry describing the change and the deprecation shim. The shim itself is untouched — the legacy string-key API continues to work with a `Base.depwarn`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 5f3f67e commit 27671b5

11 files changed

Lines changed: 113 additions & 85 deletions

File tree

NEWS.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,26 @@
11
# New in 0.13
22

3+
- `imshow` now returns an `ImageDisplay` struct rather than a nested
4+
`Dict{String,Any}`. The nested GUI and ROI groupings are exposed as
5+
the structs `ImageViewGUI` (returned by `imshow_gui`) and `ImageROI`
6+
(returned by the low-level `imshow(::Canvas, ...)`). Field access
7+
is flattened on `ImageDisplay`, so where you previously wrote
8+
`result["gui"]["window"]`, `result["roi"]["zoomregion"]`, or
9+
`result["roi"]["image roi"]`, you can now write `result.window`,
10+
`result.zoomregion`, `result.image_roi`. Call `propertynames(result)`
11+
to see every accessible field.
12+
13+
This change fixes a long-standing usability problem: the default
14+
`show` for the old `Dict` recursively printed the underlying image
15+
array, which could hang the REPL for several seconds on a large
16+
image when the trailing semicolon was forgotten. The new types have
17+
compact `show` methods that print only summary information.
18+
19+
The string-key API (`result["gui"]["window"]` etc.) still works and
20+
routes through a deprecation shim; each call emits a
21+
`Base.depwarn`. The shim will be removed in the next breaking
22+
release. Downstream packages can dispatch on `ImageView.ImageDisplay`,
23+
`ImageView.ImageViewGUI`, or `ImageView.ImageROI` directly.
324
- Julia 1.10 is now required
425
- MultiChannelColors, FileIO support now in extensions
526

Project.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name = "ImageView"
22
uuid = "86fae568-95e7-573e-a6b2-d8a6b900c9ef"
33
author = ["Tim Holy <tim.holy@gmail.com", "Jared Wahlstrand <jwahlstrand@gmail.com>"]
4-
version = "0.13.1"
4+
version = "0.13.2"
55

66
[deps]
77
AxisArrays = "39de3d68-74b9-583c-8d2d-e117c070f3a9"

README.md

Lines changed: 40 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -112,47 +112,53 @@ You can place multiple images in the same window using `imshow_gui`:
112112
```
113113
using ImageView, TestImages, Gtk4
114114
gui = imshow_gui((300, 300), (2, 1)) # 2 columns, 1 row of images (each initially 300×300)
115-
canvases = gui["canvas"]
115+
canvases = gui.canvas
116116
imshow(canvases[1,1], testimage("lighthouse"))
117117
imshow(canvases[1,2], testimage("mandrill"))
118-
show(gui["window"])
118+
show(gui.window)
119119
```
120120

121121
![canvasgrid snapshot](readme_images/canvasgrid.jpg)
122122

123-
`gui["window"]` returns the window; `gui["canvas"]` either returns a single canvas
123+
`gui.window` returns the window; `gui.canvas` either returns a single canvas
124124
(if there is just one), or an array if you've specified a grid of canvases.
125125

126126
`Gtk4.show(win)` is sometimes needed when using the lower-level utilities of this
127127
package. Generally you should call it after you've finished assembling the entire window,
128128
so as to avoid redraws with each subsequent change.
129129

130-
### The dictionary and region-of-interest manipulations
130+
### The display handle and region-of-interest manipulations
131131

132-
`imshow` returns a dictionary containing a wealth of information about
133-
the state of the viewer. Perhaps most interesting is the `"roi"`
134-
entry, which itself is another dictionary containing information about
135-
the current selected region or interest. These are
136-
[Observables signals](https://juliagizmos.github.io/Observables.jl/), and consequently you can even manipulate the
137-
state of the GUI by `push!`ing new values to these signals.
132+
`imshow` returns an `ImageDisplay`, a struct holding the GTK widgets and
133+
Observables that drive the viewer. The most interesting subgroup is the
134+
region-of-interest (accessible as `disp.roi` or directly via hoisted fields
135+
like `disp.zoomregion`, `disp.image_roi`, `disp.slicedata`). The signals are
136+
[Observables](https://juliagizmos.github.io/Observables.jl/), and you can
137+
manipulate the state of the GUI by `push!`ing (or assigning) new values to
138+
them.
138139

139140
For example, using the `"mri"` image above, you can select the 5th slice with
140141
```julia
141-
guidict = imshow(img)
142-
guidict["roi"]["slicedata"].signals[1][] = 5
142+
disp = imshow(img)
143+
disp.slicedata.signals[1][] = 5
143144
```
144145

145146
`SliceData` objects contain information about which axes are displayed
146147
and the current slice indices of those axes perpendicular to the view
147-
plane. Likewise, `"image roi"` contains the actual image data
148+
plane. Likewise, `disp.image_roi` contains the actual image data
148149
currently being shown in the window (including all zoom/slice
149150
settings).
150151

151-
The `"zoom"`- and `"pan"`-related signals originate from
152+
The `zoom_*` and `pan_*` signals originate from
152153
[GtkObservables](https://juliagizmos.github.io/GtkObservables.jl/stable/),
153154
and users should see the documentation for that package for more
154155
information.
155156

157+
`propertynames(disp)` lists every accessible field. For backwards
158+
compatibility, the older string-key syntax (`disp["gui"]["window"]`,
159+
`disp["roi"]["zoomregion"]`, etc.) still works but emits a deprecation
160+
warning; it will be removed in the next breaking release.
161+
156162
### Coupling two or more images together
157163

158164
`imshow` allows you to pass many more arguments; please use `?imshow`
@@ -171,9 +177,9 @@ mriseg[mri .> 0.5] .= colorant"red"
171177
Now we display the images:
172178

173179
```julia
174-
guidata = imshow(mri, axes=(1,2))
175-
zr = guidata["roi"]["zoomregion"]
176-
slicedata = guidata["roi"]["slicedata"]
180+
disp = imshow(mri, axes=(1,2))
181+
zr = disp.zoomregion
182+
slicedata = disp.slicedata
177183
imshow(mriseg, nothing, zr, slicedata)
178184
```
179185

@@ -187,8 +193,8 @@ Alternatively, you can place both displays in a single window:
187193
```julia
188194
zr, slicedata = roi(mri, (1,2))
189195
gd = imshow_gui((200, 200), (2, 1); slicedata=slicedata)
190-
imshow(gd["frame"][1,1], gd["canvas"][1,1], mri, nothing, zr, slicedata)
191-
imshow(gd["frame"][1,2], gd["canvas"][1,2], mriseg, nothing, zr, slicedata)
196+
imshow(gd.frame[1,1], gd.canvas[1,1], mri, nothing, zr, slicedata)
197+
imshow(gd.frame[1,2], gd.canvas[1,2], mriseg, nothing, zr, slicedata)
192198
```
193199

194200
You should see something like this:
@@ -208,8 +214,8 @@ and "floating" annotations are best for things like scalebars.
208214

209215
Here's an example of adding a 30-pixel scale bar to an image:
210216
```julia
211-
guidict = imshow(img)
212-
scalebar(guidict, 30; x = 0.1, y = 0.05)
217+
disp = imshow(img)
218+
scalebar(disp, 30; x = 0.1, y = 0.05)
213219
```
214220

215221
`x` and `y` describe the center of the scale bar in normalized
@@ -226,14 +232,14 @@ using Images, ImageView
226232
z = ones(10,50);
227233
y = 8; x = 2;
228234
z[y,x] = 0
229-
guidict = imshow(z)
230-
idx = annotate!(guidict, AnnotationText(x, y, "x", color=RGB(0,0,1), fontsize=3))
231-
idx2 = annotate!(guidict, AnnotationPoint(x+10, y, shape='.', size=4, color=RGB(1,0,0)))
232-
idx3 = annotate!(guidict, AnnotationPoint(x+20, y-6, shape='.', size=1, color=RGB(1,0,0), linecolor=RGB(0,0,0), scale=true))
233-
idx4 = annotate!(guidict, AnnotationLine(x+10, y, x+20, y-6, linewidth=2, color=RGB(0,1,0)))
234-
idx5 = annotate!(guidict, AnnotationBox(x+10, y, x+20, y-6, linewidth=2, color=RGB(0,0,1)))
235+
disp = imshow(z)
236+
idx = annotate!(disp, AnnotationText(x, y, "x", color=RGB(0,0,1), fontsize=3))
237+
idx2 = annotate!(disp, AnnotationPoint(x+10, y, shape='.', size=4, color=RGB(1,0,0)))
238+
idx3 = annotate!(disp, AnnotationPoint(x+20, y-6, shape='.', size=1, color=RGB(1,0,0), linecolor=RGB(0,0,0), scale=true))
239+
idx4 = annotate!(disp, AnnotationLine(x+10, y, x+20, y-6, linewidth=2, color=RGB(0,1,0)))
240+
idx5 = annotate!(disp, AnnotationBox(x+10, y, x+20, y-6, linewidth=2, color=RGB(0,0,1)))
235241
# This shows that you can remove annotations, too:
236-
delete!(guidict, idx)
242+
delete!(disp, idx)
237243
```
238244

239245
This is what this looks like before `delete!`ing the first annotation:
@@ -247,7 +253,7 @@ by calling `annotations()`:
247253
using ImageView, Images, Gtk4
248254
# Create the window and a 2x2 grid of canvases, each 300x300 pixels in size
249255
gui = imshow_gui((300, 300), (2, 2))
250-
canvases = gui["canvas"]
256+
canvases = gui.canvas
251257
# Create some single-color images (just for testing purposes)
252258
makeimage(color) = fill(color, 100, 100)
253259
imgs = makeimage.([colorant"red" colorant"green";
@@ -256,10 +262,10 @@ imgs = makeimage.([colorant"red" colorant"green";
256262
anns = [annotations() annotations();
257263
annotations() annotations()]
258264
# Draw the images on the canvases. Note that we supply the annotation object for each.
259-
roidict = [imshow(canvases[1,1], imgs[1,1], anns[1,1]) imshow(canvases[1,2], imgs[1,2], anns[1,2]);
260-
imshow(canvases[2,1], imgs[2,1], anns[2,1]) imshow(canvases[2,2], imgs[2,2], anns[2,2])]
265+
rois = [imshow(canvases[1,1], imgs[1,1], anns[1,1]) imshow(canvases[1,2], imgs[1,2], anns[1,2]);
266+
imshow(canvases[2,1], imgs[2,1], anns[2,1]) imshow(canvases[2,2], imgs[2,2], anns[2,2])]
261267
# Now we'll add an annotation to the lower-right image
262-
annotate!(anns[2,2], canvases[2,2], roidict[2,2], AnnotationBox(5, 5, 30, 80, linewidth=3, color=RGB(1,1,0)))
268+
annotate!(anns[2,2], canvases[2,2], rois[2,2], AnnotationBox(5, 5, 30, 80, linewidth=3, color=RGB(1,1,0)))
263269
```
264270

265271
![grid annotations](readme_images/grid_annotations.png)
@@ -352,7 +358,7 @@ If you call Julia from a script file, the GLib main loop (which is responsible f
352358
using Images, ImageView, TestImages, Gtk4
353359
354360
img = testimage("mandrill")
355-
guidict = imshow(img);
361+
disp = imshow(img);
356362
357363
#If we are not in a REPL
358364
if (!isinteractive())
@@ -361,7 +367,7 @@ if (!isinteractive())
361367
c = Condition()
362368
363369
# Get the window
364-
win = guidict["gui"]["window"]
370+
win = disp.window
365371
366372
# Start the GLib main loop
367373
@async Gtk4.GLib.glib_main()

src/ImageView.jl

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -284,13 +284,14 @@ function copy_with_restrict!(cnvs, img::AbstractMatrix)
284284
end
285285

286286
"""
287-
imshow(img; axes=(1,2), name="ImageView") -> guidict
288-
imshow(img, clim; kwargs...) -> guidict
289-
imshow(img, clim, zoomregion, slicedata, annotations; kwargs...) -> guidict
290-
291-
Display the image `img` in a new window titled with `name`, returning
292-
a dictionary `guidict` containing any Observables signals or GtkObservables
293-
widgets. If the image is 3 or 4 dimensional, GUI controls will be
287+
imshow(img; axes=(1,2), name="ImageView") -> disp::ImageDisplay
288+
imshow(img, clim; kwargs...) -> disp::ImageDisplay
289+
imshow(img, clim, zoomregion, slicedata, annotations; kwargs...) -> disp::ImageDisplay
290+
291+
Display the image `img` in a new window titled with `name`, returning an
292+
[`ImageDisplay`](@ref) `disp` whose fields expose the Observables and
293+
GtkObservables widgets that drive the GUI (see `propertynames(disp)`).
294+
If the image is 3 or 4 dimensional, GUI controls will be
294295
added for slicing along "extra" axes. By default the two-dimensional
295296
slice containing axes 1 and 2 are shown, but that can be changed by
296297
passing a different setting for `axes`.
@@ -455,7 +456,7 @@ function fullscreen_cb(aptr::Ptr, par, win)
455456
end
456457

457458
"""
458-
guidict = imshow_gui(canvassize, gridsize=(1,1); name="ImageView", aspect=:auto, slicedata=SliceData{false}())
459+
gui = imshow_gui(canvassize, gridsize=(1,1); name="ImageView", aspect=:auto, slicedata=SliceData{false}()) -> gui::ImageViewGUI
459460
460461
Create an image-viewer GUI. By default creates a single canvas, but
461462
with custom `gridsize = (nx, ny)` you can create a grid of canvases.
@@ -560,9 +561,9 @@ Compat.@constprop :none function frame_canvas(aspect)
560561
end
561562

562563
"""
563-
imshow(canvas, imgsig::Observable) -> guidict
564-
imshow(canvas, imgsig::Observable, zr::Observable{ZoomRegion}) -> guidict
565-
imshow(frame::Frame, canvas, imgsig::Observable, zr::Observable{ZoomRegion}) -> guidict
564+
imshow(canvas, imgsig::Observable) -> roi::ImageROI
565+
imshow(canvas, imgsig::Observable, zr::Observable{ZoomRegion}) -> roi::ImageROI
566+
imshow(frame::Frame, canvas, imgsig::Observable, zr::Observable{ZoomRegion}) -> roi::ImageROI
566567
567568
Display `imgsig` (a `Observable` of an image) in `canvas`, setting up
568569
panning and zooming. Optionally include a `frame` for preserving
@@ -576,8 +577,8 @@ using ImageView, TestImages, Gtk4
576577
mri = testimage("mri");
577578
# Create a canvas `c`. There are other approaches, like stealing one from a previous call
578579
# to `imshow`, or using GtkObservables directly.
579-
guidict = imshow_gui((300, 300))
580-
c = guidict["canvas"];
580+
gui = imshow_gui((300, 300))
581+
c = gui.canvas;
581582
# To see anything you have to call `showall` on the window (once)
582583
# Create the image Observable
583584
imgsig = Observable(mri[:,:,1]);

src/annotations.jl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -259,10 +259,10 @@ normalized_lengths(imsl::Observable, width, height) =
259259
normalized_lengths(imsl[], width, height)
260260

261261
"""
262-
scalebar(guidict::Dict, length; x = 0.8, y = 0.1, color = RGB(1,1,1))
262+
scalebar(disp::ImageDisplay, length; x = 0.8, y = 0.1, color = RGB(1,1,1))
263263
264264
Add a scale bar annotation to the image display controlled by
265-
`guidict` (returned by [`imshow`](@ref)). If the
265+
`disp` (returned by [`imshow`](@ref)). If the
266266
[`pixelspacing`](@ref) of the image is set using Unitful quantities,
267267
`length` should also be expressed in physical units.
268268

test/annotations.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ scalebar(guidict, 30; x = 0.1, y = 0.05)
2323

2424
# Grid of images (issue #202)
2525
gd = imshow_gui((300, 300), (2, 2))
26-
canvases = gd["canvas"]
26+
canvases = gd.canvas
2727
anns = [annotations() annotations();
2828
annotations() annotations()]
2929
makeimage(color) = fill(color, 100, 100)

test/contrast.jl

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,9 @@ end
5656
Observable(CLim(RGB(0f0,0f0,0f0), RGB(1f0,1f0,1f0))))) == 3
5757

5858
# setup_contrast_popup! registers the contrast_gui action on the canvas
59-
# (gridsize (1,1) default → gd["canvas"] is a single Canvas, not a matrix)
59+
# (gridsize (1,1) default → gd.canvas is a single Canvas, not a matrix)
6060
gd = imshow_gui((50, 50))
61-
canvas = gd["canvas"]
61+
canvas = gd.canvas
6262
clim2 = Observable(CLim(0.0f0, 1.0f0))
6363
n_preserved = length(canvas.preserved)
6464
ret = setup_contrast_popup!(canvas, clim2)
@@ -68,12 +68,12 @@ end
6868

6969
# with img kwarg: uses histsignals; action still registered
7070
gd2 = imshow_gui((50, 50))
71-
canvas2 = gd2["canvas"]
71+
canvas2 = gd2.canvas
7272
clim3 = Observable(CLim(0.0f0, 1.0f0))
7373
img = Observable(rand(Float32, 10, 10))
7474
setup_contrast_popup!(canvas2, clim3; img=img)
7575
@test "contrast_gui" in keys(canvas2.action_group)
7676

77-
Gtk4.destroy(gd["window"])
78-
Gtk4.destroy(gd2["window"])
77+
Gtk4.destroy(gd.window)
78+
Gtk4.destroy(gd2.window)
7979
end

test/kwargs.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,5 +22,5 @@ end
2222
@testset "window size with kwargs" begin
2323
gd = imshow([1 0; 0 1], canvassize=(500,500))
2424
sleep(2.0)
25-
get(ENV, "CI", nothing) === nothing && @test all(>=(500), size(gd["gui"]["window"]))
25+
get(ENV, "CI", nothing) === nothing && @test all(>=(500), size(gd.window))
2626
end

0 commit comments

Comments
 (0)