Skip to content

Commit

Permalink
Gtk4 port (#284)
Browse files Browse the repository at this point in the history
* begin port to gtk4

* try to get all tests passing

Ran into a new bug related to getting the screen size from the default Gdk display.
screen_size() works early in the tests but fails later on. It looks like the
GListModel of monitors is being unreferenced somewhere, whereas it should be left
alone if we are respecting the introspection annotations (which it looks like we are).

* restore some code that was removed during debugging of refcounting problem

* get tests passing again

* fix contrast gui

* try uncommenting player controls code

seeing a freeze in the tests that needs looking into

* bump Gtk4 compat

* bump GtkObservables compat

* set a minimum size for the canvas

When the canvas is put in a window such that it receives no allocated space, it is
never realized, and this causes everything to freeze. It would be good to solve
the underlying issue, but setting a small minimum size (10 by 10 pixels here) prevents
it from ever happening.

* add a delay to get last commmented out test working

* remove rounded corners in frames using CSS

* set `obey_child` to false in AspectFrame

* update README, tweak popup UI, small cleanups
  • Loading branch information
jwahlstrand authored Sep 17, 2023
1 parent f037ca6 commit b9c78e6
Show file tree
Hide file tree
Showing 11 changed files with 138 additions and 129 deletions.
6 changes: 3 additions & 3 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ AxisArrays = "39de3d68-74b9-583c-8d2d-e117c070f3a9"
Cairo = "159f3aea-2a34-519c-b102-8c37f9878175"
Compat = "34da2185-b29b-5c13-b0c7-acf172513d20"
Graphics = "a2bd30eb-e257-5431-a919-1863eab51364"
Gtk = "4c0ca9eb-093a-5379-98c5-f87ac0bbbf44"
Gtk4 = "9db2cae5-386f-4011-9d63-a5602296539b"
GtkObservables = "8710efd8-4ad6-11eb-33ea-2d5ceb25a41c"
ImageBase = "c817782e-172a-44cc-b673-b171935fbb9e"
ImageCore = "a09fc81d-aa75-5fe9-8630-4744c3626534"
Expand All @@ -23,8 +23,8 @@ AxisArrays = "0.3, 0.4"
Cairo = "0.6, 0.7, 0.8, 1"
Compat = "3, 4"
Graphics = "0.2, 0.3, 0.4, 1"
Gtk = "0.16, 0.17, 0.18, 1"
GtkObservables = "1.2.2"
GtkObservables = "2"
Gtk4 = "0.5"
ImageBase = "0.1"
ImageCore = "0.9, 0.10"
ImageMetadata = "0.9"
Expand Down
24 changes: 14 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,20 +110,20 @@ values into the text boxes.

You can place multiple images in the same window using `imshow_gui`:
```
using ImageView, TestImages, Gtk.ShortNames
using ImageView, TestImages, Gtk4
gui = imshow_gui((300, 300), (2, 1)) # 2 columns, 1 row of images (each initially 300×300)
canvases = gui["canvas"]
imshow(canvases[1,1], testimage("lighthouse"))
imshow(canvases[1,2], testimage("mandrill"))
Gtk.showall(gui["window"])
show(gui["window"])
```

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

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

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

Expand Down Expand Up @@ -189,7 +189,6 @@ zr, slicedata = roi(mri, (1,2))
gd = imshow_gui((200, 200), (1,2); slicedata=slicedata)
imshow(gd["frame"][1,1], gd["canvas"][1,1], mri, nothing, zr, slicedata)
imshow(gd["frame"][1,2], gd["canvas"][1,2], mriseg, nothing, zr, slicedata)
Gtk.showall(gd["window"])
```

You should see something like this:
Expand Down Expand Up @@ -245,7 +244,7 @@ If you have a grid of images, then each image needs its own set of annotations,
by calling `annotations()`:

```julia
using ImageView, Images, Gtk.ShortNames
using ImageView, Images, Gtk4
# Create the window and a 2x2 grid of canvases, each 300x300 pixels in size
gui = imshow_gui((300, 300), (2, 2))
canvases = gui["canvas"]
Expand All @@ -261,7 +260,6 @@ roidict = [imshow(canvases[1,1], imgs[1,1], anns[1,1]) imshow(canvases[1,2], img
imshow(canvases[2,1], imgs[2,1], anns[2,1]) imshow(canvases[2,2], imgs[2,2], anns[2,2])]
# Now we'll add an annotation to the lower-right image
annotate!(anns[2,2], canvases[2,2], roidict[2,2], AnnotationBox(5, 5, 30, 80, linewidth=3, color=RGB(1,1,0)))
Gtk.showall(gui["window"])
```

![grid annotations](readme_images/grid_annotations.png)
Expand Down Expand Up @@ -348,10 +346,10 @@ Properties:

### Calling imshow from a script file

If you call Julia from a script file, the julia process will terminate at the end of the program. This will cause any windows opened with `imshow()` to terminate, which is probably not what you intend. We want to make it only terminate the process when the image window is closed. Below is some example code to do this:
If you call Julia from a script file, the GLib main loop (which is responsible for handling events like mouse clicks, etc.) will not start automatically, and the julia process will terminate at the end of the program. This will cause any windows opened with `imshow()` to terminate, which is probably not what you intend. We want to start the main loop and then make it only terminate the process when the image window is closed. Below is some example code to do this:

```
using Images, ImageView, TestImages, Gtk.ShortNames
using Images, ImageView, TestImages, Gtk4
img = testimage("mandrill")
guidict = imshow(img);
Expand All @@ -364,9 +362,12 @@ if (!isinteractive())
# Get the window
win = guidict["gui"]["window"]
# Start the GLib main loop
@async Gtk4.GLib.glib_main()
# Notify the condition object when the window closes
signal_connect(win, :destroy) do widget
signal_connect(win, :close_request) do widget
notify(c)
end
Expand All @@ -382,4 +383,7 @@ manually close it with `CTRL + C`.

If you are opening more than one window you will need to create more
than one `Condition` object, if you wish to wait until the last one is
closed.
closed. See
(here)[https://juliagtk.github.io/Gtk4.jl/dev/howto/nonreplusage/] for
more information.

82 changes: 46 additions & 36 deletions src/ImageView.jl
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ using ImageCore, ImageBase, StatsBase
using ImageCore.MappedArrays
using MultiChannelColors
using RoundingIntegers
using Gtk.ShortNames, GtkObservables, Graphics, Cairo
using Gtk.GConstants.GtkAlign: GTK_ALIGN_START, GTK_ALIGN_END, GTK_ALIGN_FILL
using Gtk4, GtkObservables, Graphics, Cairo
using Gtk4: Align_START, Align_END, Align_FILL
using GtkObservables.Observables
using AxisArrays: AxisArrays, Axis, AxisArray, axisnames, axisvalues
using AxisArrays: AxisArrays, Axis, AxisArray, axisnames, axisvalues, axisdim
using ImageMetadata
using Compat # for @constprop :none

Expand Down Expand Up @@ -57,7 +57,7 @@ function closeall()
nothing
end

const window_wrefs = WeakKeyDict{Gtk.GtkWindowLeaf,Nothing}()
const window_wrefs = WeakKeyDict{Gtk4.GtkWindowLeaf,Nothing}()

"""
imshow()
Expand Down Expand Up @@ -94,12 +94,11 @@ using GtkObservables's tools:
# Example
```julia
using ImageView, GtkObservables, Gtk.ShortNames, TestImages
using ImageView, GtkObservables, Gtk4, TestImages
# Create a window with a canvas in it
win = Window()
win = GtkWindow()
c = canvas(UserUnit)
push!(win, c)
Gtk.showall(win)
# Load images
mri = testimage("mri")
# Display the image
Expand All @@ -118,7 +117,7 @@ function imshow!(canvas::GtkObservables.Canvas{UserUnit},
end
end

function imshow!(frame::Frame,
function imshow!(frame::Union{GtkFrame,GtkAspectFrame},
canvas::GtkObservables.Canvas{UserUnit},
imgsig::Observable,
zr::Observable{ZoomRegion{T}},
Expand All @@ -128,6 +127,7 @@ function imshow!(frame::Frame,
set_coordinates(cnvs, zr[])
set_aspect!(frame, image)
draw_annotations(cnvs, anns)
nothing
end
end

Expand Down Expand Up @@ -233,13 +233,12 @@ Compat.@constprop :none function imshow(@nospecialize(img::AbstractArray), clim,
wrap_signal(clim), zr, sd, anns)

win = guidict["window"]
Gtk.showall(win)
dct = Dict("gui"=>guidict, "clim"=>clim, "roi"=>roidict, "annotations"=>anns)
GtkObservables.gc_preserve(win, dct)
return dct
end

function imshow(frame::Gtk.GtkFrame, canvas::GtkObservables.Canvas,
function imshow(frame::Union{Gtk4.GtkFrame,Gtk4.GtkAspectFrame}, canvas::GtkObservables.Canvas,
@nospecialize(img::AbstractArray), clim::Union{Nothing,Observable{<:CLim}},
zr::Observable{ZoomRegion{T}}, sd::SliceData,
anns::Annotations=annotations()) where T
Expand All @@ -257,7 +256,7 @@ function imshow(frame::Gtk.GtkFrame, canvas::GtkObservables.Canvas,
imgc = prep_contrast(canvas, imgsig, clim)
GtkObservables.gc_preserve(frame, imgc)
# If there is an error in one of the functions being mapped elementwise, we often don't
# discover it until it triggers an error inside `Gtk.draw`. Check for problems here so
# discover it until it triggers an error inside `Gtk4.draw`. Check for problems here so
# such errors become easier to debug.
if !supported_eltype(imgc[])
!supported_eltype(imgsig[]) && error("got unsupported eltype $(eltype(imgsig[])) in creating slice")
Expand Down Expand Up @@ -292,13 +291,12 @@ Compat.@constprop :none function imshow(img,
roidict = imshow(guidict["frame"], guidict["canvas"], img, zr, sd, anns)

win = guidict["window"]
Gtk.showall(win)
dct = Dict("gui"=>guidict, "roi"=>roidict)
GtkObservables.gc_preserve(win, dct)
return dct
end

function imshow(frame::Gtk.GtkFrame, canvas::GtkObservables.Canvas,
function imshow(frame::Union{GtkFrame,GtkAspectFrame}, canvas::GtkObservables.Canvas,
img, zr::Observable{ZoomRegion{T}}, sd::SliceData,
anns::Annotations=annotations()) where T
@nospecialize
Expand Down Expand Up @@ -334,12 +332,12 @@ Compat.@constprop :none function imshow_gui(canvassize::Tuple{Int,Int},
name = "ImageView", aspect=:auto,
slicedata::SliceData=SliceData{false}())
winsize = canvas_size(screen_size(), map(*, canvassize, gridsize))
win = Window(name, winsize...)
win = GtkWindow(name, winsize...)
window_wrefs[win] = nothing
signal_connect(win, :destroy) do w
delete!(window_wrefs, win)
end
vbox = Box(:v)
vbox = GtkBox(:v)
push!(win, vbox)
if gridsize == (1,1)
frames, canvases = frame_canvas(aspect)
Expand All @@ -348,8 +346,8 @@ Compat.@constprop :none function imshow_gui(canvassize::Tuple{Int,Int},
g, frames, canvases = canvasgrid(gridsize, aspect)
end
push!(vbox, g)
status = Label("")
set_gtk_property!(status, :halign, Gtk.GConstants.GtkAlign.START)
status = GtkLabel("")
set_gtk_property!(status, :halign, Gtk4.Align_START)
push!(vbox, status)

guidict = Dict("window"=>win, "vbox"=>vbox, "frame"=>frames, "status"=>status,
Expand All @@ -359,7 +357,7 @@ Compat.@constprop :none function imshow_gui(canvassize::Tuple{Int,Int},
if !isempty(slicedata)
players = [player(slicedata.signals[i], axisvalues(slicedata.axs[i])[1]; id=i) for i = 1:length(slicedata)]
guidict["players"] = players
hbox = Box(:h)
hbox = GtkBox(:h)
for p in players
push!(hbox, frame(p))
end
Expand All @@ -384,7 +382,7 @@ GtkAspectRatioFrames that contain each canvas, and `canvases` is an
`ny`-by-`nx` array of canvases.
"""
Compat.@constprop :none function canvasgrid(gridsize::Tuple{Int,Int}, aspect=:auto)
g = Grid()
g = GtkGrid()
frames = Matrix{Any}(undef, gridsize)
canvases = Matrix{Any}(undef, gridsize)
for j = 1:gridsize[2], i = 1:gridsize[1]
Expand All @@ -397,11 +395,12 @@ Compat.@constprop :none function canvasgrid(gridsize::Tuple{Int,Int}, aspect=:au
end

Compat.@constprop :none function frame_canvas(aspect)
f = aspect==:none ? Frame() : AspectFrame("", 0.5, 0.5, 1)
set_gtk_property!(f, :expand, true)
set_gtk_property!(f, :shadow_type, Gtk.GConstants.GtkShadowType.NONE)
c = canvas(UserUnit)
push!(f, widget(c))
f = aspect==:none ? GtkFrame() : GtkAspectFrame(0.5, 0.5, 1, false)
Gtk4.G_.set_css_classes(f, ["squared"]) # remove rounded corners (see __init__)
set_gtk_property!(f, :hexpand, true)
set_gtk_property!(f, :vexpand, true)
c = canvas(UserUnit,10,10) # set minimum size of 10x10 pixels
f[] = widget(c)
f, c
end

Expand All @@ -418,14 +417,13 @@ Observable-view of a higher-dimensional object).
# Example
```julia
using ImageView, TestImages, Gtk
using ImageView, TestImages, Gtk4
mri = testimage("mri");
# Create a canvas `c`. There are other approaches, like stealing one from a previous call
# to `imshow`, or using GtkObservables directly.
guidict = imshow_gui((300, 300))
c = guidict["canvas"];
# To see anything you have to call `showall` on the window (once)
Gtk.showall(guidict["window"])
# Create the image Observable
imgsig = Observable(mri[:,:,1]);
# Show it
Expand All @@ -451,7 +449,7 @@ function imshow(canvas::GtkObservables.Canvas{UserUnit},
dct
end

function imshow(frame::Frame,
function imshow(frame::Union{GtkFrame,GtkAspectFrame},
canvas::GtkObservables.Canvas{UserUnit},
imgsig::Observable,
zr::Observable{ZoomRegion{T}},
Expand Down Expand Up @@ -673,18 +671,20 @@ nanz(x::FixedPoint) = x
nanz(x::Integer) = x

function create_contrast_popup(canvas, enabled, hists, clim)
popupmenu = Menu()
contrast = MenuItem("Contrast...")
push!(popupmenu, contrast)
Gtk.showall(popupmenu)
popupmenu = GtkPopover()
Gtk4.parent(popupmenu, widget(canvas))
contrast = GtkButton("Contrast...")
popupmenu[] = contrast
push!(canvas.preserved, on(canvas.mouse.buttonpress) do btn
if btn.button == 3 && btn.clicktype == BUTTON_PRESS
popup(popupmenu, btn.gtkevent)
x,y = GtkObservables.convertunits(DeviceUnit, canvas, btn.position.x, btn.position.y)
Gtk4.G_.set_pointing_to(popupmenu,Ref(Gtk4._GdkRectangle(round(Int32,x.val),round(Int32,y.val),1,1)))
popup(popupmenu)
end
end)
signal_connect(contrast, :activate) do widget
signal_connect(contrast, :clicked) do widget
enabled[] = true
contrast_gui(enabled, hists, clim)
@idle_add contrast_gui(enabled, hists, clim)
end
end

Expand All @@ -696,7 +696,7 @@ function map_image_roi(@nospecialize(img), zr::Observable{ZoomRegion{T}}, slices
end
map_image_roi(img::Observable, zr::Observable{ZoomRegion{T}}, slices...) where {T} = img

function set_aspect!(frame::AspectFrame, image)
function set_aspect!(frame::GtkAspectFrame, image)
ps = map(abs, pixelspacing(image))
sz = map(length, axes(image))
r = sz[2]*ps[2]/(sz[1]*ps[1])
Expand Down Expand Up @@ -727,7 +727,7 @@ and `screensize` are supplied in Gtk order (x, y).
When supplying a GtkWindow `win`, the canvas size is limited to 60% of
the total screen size.
"""
Compat.@constprop :none function canvas_size(win::Gtk.GtkWindowLeaf, requestedsize_xy; minsize=100)
Compat.@constprop :none function canvas_size(win::Gtk4.GtkWindowLeaf, requestedsize_xy; minsize=100)
ssz = screen_size(win)
canvas_size(map(x->0.6*x, ssz), requestedsize_xy; minsize=minsize)
end
Expand Down Expand Up @@ -781,6 +781,16 @@ include("link.jl")
include("contrast_gui.jl")
include("annotations.jl")

function __init__()
# by default, GtkFrame and GtkAspectFrame use rounded corners
# the way to override this is via custom CSS
css="""
.squared {border-radius: 0;}
"""
cssprov=GtkCssProvider(css)
push!(GdkDisplay(), cssprov, Gtk4.STYLE_PROVIDER_PRIORITY_APPLICATION)
end

using PrecompileTools
@compile_workload begin
for T in (N0f8, N0f16, Float32)
Expand Down
4 changes: 2 additions & 2 deletions src/annotations.jl
Original file line number Diff line number Diff line change
Expand Up @@ -291,7 +291,7 @@ annotation_isvalid(x, z, t) = true
function setvalid!(ann::FloatingAnnotation, z, t)
end

function Gtk.draw(c::Gtk.GtkCanvas, ann::AnchoredAnnotation)
function Gtk4.draw(c::Gtk4.GtkCanvas, ann::AnchoredAnnotation)
if ann.valid
ctx = getgc(c)
Graphics.save(ctx)
Expand All @@ -304,7 +304,7 @@ function Gtk.draw(c::Gtk.GtkCanvas, ann::AnchoredAnnotation)
end
end

function Gtk.draw(c::Gtk.GtkCanvas, ann::FloatingAnnotation{AnnotationScalebarFixed{T}}) where T
function Gtk4.draw(c::Gtk4.GtkCanvas, ann::FloatingAnnotation{AnnotationScalebarFixed{T}}) where T
ctx = getgc(c)
Graphics.save(ctx)
data = ann.data
Expand Down
Loading

0 comments on commit b9c78e6

Please sign in to comment.