Skip to content

Commit ce7270b

Browse files
committed
Fix the bloody ">" regression again. Aquamoto pops up right justified.
1 parent 479fb97 commit ce7270b

9 files changed

Lines changed: 115 additions & 31 deletions

‎deps/src/75_aquamoto.cpp‎

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -355,12 +355,25 @@ class AquamotoWindow {
355355
// handle's re-show), and it runs before the file's grids are built.
356356
sceneSetAquaWindow(scene, true);
357357
AquamotoWindow *w = registry().value(scene, nullptr);
358-
if (!w || !w->win) {
358+
const bool fresh = !w || !w->win;
359+
if (fresh) {
359360
w = new AquamotoWindow(parent, scene);
360361
if (!w->win) { delete w; return; }
361362
registry().insert(scene, w);
362363
}
363364
w->win->show();
365+
// A NEW Aquamoto window pops up against the RIGHT side of its iGMT window, tops level, kept on
366+
// that window's screen. A re-shown one stays wherever the user put it.
367+
if (fresh && scene && scene->win) {
368+
const QRect vf = scene->win->frameGeometry();
369+
const QRect af = w->win->frameGeometry();
370+
QScreen *scr = scene->win->screen();
371+
const QRect avail = scr ? scr->availableGeometry() : QRect(vf.right() + 1, vf.top(), af.width(), af.height());
372+
int x = vf.right() + 1, y = vf.top();
373+
if (x + af.width() > avail.right() + 1) x = avail.right() + 1 - af.width();
374+
if (y + af.height() > avail.bottom() + 1) y = avail.bottom() + 1 - af.height();
375+
w->win->move(std::max(x, avail.left()), std::max(y, avail.top()));
376+
}
364377
w->win->raise();
365378
w->win->activateWindow();
366379
// Compile the slice path while the user is still choosing a file / reaching for the slider.
@@ -629,10 +642,16 @@ class AquamotoWindow {
629642
rightBtn->setAutoRepeat(true);
630643
row->insertWidget(idx, leftBtn);
631644
row->insertWidget(idx + 2, rightBtn); // sliceSlider shifted to idx+1 by the insert above
645+
// ONE LAYER PER DRAWN SLICE. A repeat that lands while a slice is still drawing (busy_) or
646+
// waiting to be caught up (sliceDirty_) is dropped: moving the slider then made the catch-up
647+
// jump straight to wherever the repeats had got to, skipping every layer in between on any
648+
// tank slower to draw than the repeat interval. The hold steps at the speed slices draw.
632649
QObject::connect(leftBtn, &QToolButton::clicked, sliceSlider, [this, leftBtn]() {
650+
if (busy_ || sliceDirty_) return;
633651
if (sliceSlider && arrowStillHeld(leftBtn)) sliceSlider->setValue(sliceSlider->value() - 1);
634652
});
635653
QObject::connect(rightBtn, &QToolButton::clicked, sliceSlider, [this, rightBtn]() {
654+
if (busy_ || sliceDirty_) return;
636655
if (sliceSlider && arrowStillHeld(rightBtn)) sliceSlider->setValue(sliceSlider->value() + 1);
637656
});
638657
}
@@ -2437,6 +2456,15 @@ class AquamotoWindow {
24372456
// button makes, after the slice is up. It runs INSIDE the transport's disabled window: it is
24382457
// the long half of the step, and a < / > auto-repeating through it is the stall.
24392458
if (ok && renderedImageCheck && renderedImageCheck->isChecked()) runCombinedImage(/*perSlice=*/true);
2459+
// THE SLICE REACHES THE SCREEN BEFORE THE NEXT ONE STARTS. Render() only draws into the view's
2460+
// offscreen buffer and SCHEDULES the paint that puts it on the monitor; the catch-up below can
2461+
// start the next slice before that paint is ever delivered, and then the screen skips layers
2462+
// (measured on a 765x476 tank: slices 2..15 all drawn, only 2,4,5,7,8,10,15 painted). Paint now.
2463+
if (ok && scene_ && sceneAlive(scene_) && scene_->widget) scene_->widget->repaint();
2464+
// …and so do the slider and the layer number box: their repaints are queued the same way and
2465+
// were starved the same way, so during a hold neither moved on screen.
2466+
if (sliceSlider) sliceSlider->repaint();
2467+
if (sliceSpin) sliceSpin->repaint();
24402468
transportEnable(true);
24412469
// Requests that came in while this one was running: ONE catch-up redraw, on the next turn of
24422470
// the loop (never a recursive call), at whatever slice the slider ended up on.

‎deps/src/90_c_api.cpp‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10897,6 +10897,40 @@ GMTVTK_API int gmtvtk_aqua_arrow_press_test(void *scene, int dir, int down) {
1089710897
// i.e. how many pictures actually reached the screen. `reset` != 0 zeroes it (installing the observer
1089810898
// on first use). A slice that is read and pushed but never rendered is invisible to every Julia-side
1089910899
// count; this is the count of what the user sees. -1 no widget/render window.
10900+
// test hook: what the 3-D view really PAINTS while Aquamoto's < / > is held. Every Paint event the
10901+
// view widget receives is logged with the slice slider's value at that moment, so "each layer was
10902+
// shown in turn" is read from the frames themselves, not from the host's state. mode 1 = start (and
10903+
// clear) the log; mode 0 = copy up to `cap` logged slider values into `out`, return how many.
10904+
struct PaintLogFilter : QObject {
10905+
QScrollBar *slider = nullptr;
10906+
std::vector<int> log;
10907+
bool eventFilter(QObject *o, QEvent *e) override {
10908+
if (e->type() == QEvent::Paint || e->type() == QEvent::UpdateRequest)
10909+
log.push_back(slider ? slider->value() : -1);
10910+
return QObject::eventFilter(o, e);
10911+
}
10912+
};
10913+
GMTVTK_API int gmtvtk_paint_log_test(void *scene, int mode, int *out, int cap) {
10914+
static std::map<void *, PaintLogFilter *> filters;
10915+
Scene *s = (Scene*)scene;
10916+
if (!s || !s->widget) return -1;
10917+
PaintLogFilter *&f = filters[s->widget];
10918+
if (!f) {
10919+
f = new PaintLogFilter;
10920+
s->widget->installEventFilter(f);
10921+
}
10922+
if (mode == 1) {
10923+
f->log.clear();
10924+
f->slider = nullptr;
10925+
for (QWidget *tl : QApplication::topLevelWidgets())
10926+
if (QScrollBar *sb = tl->findChild<QScrollBar *>("sliceSlider")) { f->slider = sb; break; }
10927+
return 0;
10928+
}
10929+
const int n = (int)std::min<size_t>(f->log.size(), (size_t)cap);
10930+
for (int i = 0; i < n; ++i) out[i] = f->log[(size_t)i];
10931+
return n;
10932+
}
10933+
1090010934
GMTVTK_API int gmtvtk_render_count_test(void *scene, int reset) {
1090110935
static std::map<void *, int> counts;
1090210936
static std::set<void *> hooked;

‎src/console.jl‎

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -193,12 +193,18 @@ end
193193
# the REPL does, with the REPL's own call, before its first command.
194194
const _CONSOLE_IUTILS = Ref(false)
195195

196+
# A call a TOOL makes through this door (every Aquamoto slice, every dialog request) is spelled
197+
# `InteractiveGMT.<function>(...)`. Those run exactly as they always did; the REPL-only extras below
198+
# (InteractiveUtils, `ans`) are for what the user TYPES -- a slice call must not overwrite `ans`.
199+
_console_typed(code::String)::Bool = !startswith(lstrip(code), "InteractiveGMT.")
200+
196201
function _console_eval_run(scene::Ptr{Cvoid}, cmd::Cstring, buf::Ptr{UInt8}, cap::Cint)::Cint
197-
if !_CONSOLE_IUTILS[]
202+
code = unsafe_string(cmd)
203+
typed = _console_typed(code)
204+
if typed && !_CONSOLE_IUTILS[]
198205
try Base.load_InteractiveUtils(Main) catch end
199206
_CONSOLE_IUTILS[] = true
200207
end
201-
code = unsafe_string(cmd)
202208
fig = get(_FIGREG, scene, nothing)
203209
fig !== nothing && Core.eval(Main, :(fig = $fig)) # console's `fig` = this window
204210
# Capture the command's stdout through a real pipe (redirect_stdout rejects an IOBuffer);
@@ -219,7 +225,7 @@ function _console_eval_run(scene::Ptr{Cvoid}, cmd::Cstring, buf::Ptr{UInt8}, cap
219225
# `ans` = the last result, as the REPL keeps it (another REPL-only thing a session with no
220226
# REPL never has). Only on success: a failed command leaves the previous `ans` alone, as
221227
# there. QuoteNode so a Symbol or Expr result is stored, not evaluated.
222-
Core.eval(Main, Expr(:(=), :ans, QuoteNode(val)))
228+
typed && Core.eval(Main, Expr(:(=), :ans, QuoteNode(val)))
223229
catch e
224230
err = e
225231
finally

‎src/craftapps.jl‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,10 @@ end
117117
function _craft_release(a::CraftApp)::Tuple{String,String,Int,String}
118118
suf = _craft_asset_suffix()
119119
isempty(suf) && error("$(a.title) has no download for this system ($(Sys.KERNEL) $(Sys.ARCH)).")
120-
json = _github_json("https://api.github.com/repos/$(a.repo)/releases/latest") # gmtplugin.jl
120+
io = IOBuffer()
121+
GMT.Downloads.download("https://api.github.com/repos/$(a.repo)/releases/latest", io;
122+
headers = ["Accept" => "application/vnd.github+json"], timeout = 20)
123+
json = String(take!(io))
121124
m = match(r"\"tag_name\"\s*:\s*\"([^\"]+)\"", json)
122125
m === nothing && error("GitHub did not report a release of $(a.title) (github.com/$(a.repo)).")
123126
tag = String(m.captures[1])

‎src/gmtplugin.jl‎

Lines changed: 11 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -125,39 +125,31 @@ function _plugin_local_time(dir::String)::Union{Nothing,GMT.Dates.DateTime}
125125
return tryparse(GMT.Dates.DateTime, strip(read(f, String)), GMT.Dates.dateformat"yyyy-mm-ddTHH:MM:SS")
126126
end
127127

128-
# A GitHub API reply (release JSON). The start-up update checks (plugins here, VectorCraft/PhotoCraft
129-
# in craftapps.jl) fetch it on a background task, and Julia 1.10's Downloads CRASHES the process at
130-
# exit (ACCESS VIOLATION in curl_multi_cleanup) when a transfer is still in flight then. So every
131-
# fetch takes _BG_DL_STOP, and _bg_check's atexit hook fires it and waits for the checks to end
132-
# before Downloads' own cleanup runs.
133-
const _BG_DL_STOP = Base.Event()
128+
# The start-up update checks (plugins here, VectorCraft/PhotoCraft in craftapps.jl) download on a
129+
# background task, and Julia 1.10's Downloads CRASHES the process at exit (ACCESS VIOLATION in
130+
# curl_multi_cleanup) when a transfer is still in flight then. So the atexit hook WAITS for a check
131+
# still running (each download has a 20 s timeout) before Downloads' own cleanup runs. Nothing else:
132+
# while the session runs, a check is the same plain download it always was -- an `interrupt` Event
133+
# on it made Aquamoto's < > hold freeze on one layer and jump ten (2026-10-26).
134134
const _BG_DL_TASKS = Task[]
135135

136-
function _github_json(url::String)::String
137-
io = IOBuffer()
138-
r = GMT.Downloads.request(url; output = io, headers = ["Accept" => "application/vnd.github+json"],
139-
timeout = 20, interrupt = _BG_DL_STOP)
140-
200 <= r.status < 300 || error("$url: HTTP $(r.status)")
141-
return String(take!(io))
142-
end
143-
144-
# Run a start-up update check `f` in the background, stopped at exit (see _github_json).
145136
function _bg_check(f::Function)
146137
isempty(_BG_DL_TASKS) && atexit(_bg_stop_checks)
147138
push!(_BG_DL_TASKS, @async f())
148139
return nothing
149140
end
150141

151142
function _bg_stop_checks()
152-
notify(_BG_DL_STOP)
153-
foreach(t -> timedwait(() -> istaskdone(t), 5.0), _BG_DL_TASKS)
143+
foreach(t -> timedwait(() -> istaskdone(t), 25.0), _BG_DL_TASKS)
154144
return nothing
155145
end
156146

157147
# The release asset's updated_at (UTC), from GitHub's API; nothing when it cannot be had.
158148
function _plugin_remote_time(p::GmtPlugin, asset::String)::Union{Nothing,GMT.Dates.DateTime}
159-
json = _github_json("https://api.github.com/repos/$(p.repo)/releases/tags/$(_plugin_tag(p))")
160-
return _plugin_asset_time(json, asset)
149+
io = IOBuffer()
150+
GMT.Downloads.download("https://api.github.com/repos/$(p.repo)/releases/tags/$(_plugin_tag(p))", io;
151+
headers = ["Accept" => "application/vnd.github+json"], timeout = 20)
152+
return _plugin_asset_time(String(take!(io)), asset)
161153
end
162154

163155
# The updated_at of asset `asset` in a GitHub release JSON: the first one after its "name".

‎test/aquamoto_fixture.jl‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,9 @@ elseif Sys.isapple()
250250
aqf_cg_post(UInt32(5)) # kCGEventMouseMoved
251251
end
252252
aqf_os_button(down) = aqf_cg_post(UInt32(down ? 1 : 2)) # kCGEventLeftMouseDown / Up
253-
aqf_cf_key(name) = unsafe_load(cglobal((name, AQF_CG), Ptr{Cvoid}))
253+
# the key's name is a runtime value, so dlsym it: cglobal((name, lib)) with a local name is a
254+
# lowering error on Julia >= 1.14, for the whole file, on every system
255+
aqf_cf_key(name) = unsafe_load(Ptr{Ptr{Cvoid}}(Base.Libc.Libdl.dlsym(Base.Libc.Libdl.dlopen(AQF_CG), name)))
254256
function aqf_cf_string(s::Ptr{Cvoid})
255257
s == C_NULL && return ""
256258
buf = zeros(UInt8, 512)

‎test/libgmtvtk_test.jl‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ const _TEST_SYMBOLS = (
7373
:gmtvtk_aqua_arrow_press_test, # press / release an arrow, no pumping (the app's pump drives it)
7474
:gmtvtk_aqua_slider_value_test, # the slice slider's value, no pumping
7575
:gmtvtk_render_count_test, # renders that reached the screen since the last reset
76+
:gmtvtk_paint_log_test, # slider value at every paint of the 3-D view
7677
:gmtvtk_axes_sets_test, # EVERY axes set: name|shown|onscreen|zlock|z0|z1 per line
7778
:gmtvtk_image_tex_hash_test, # hash + dims of an image extra's texture bytes
7879
:gmtvtk_aqua_arrow_screen_test, # screen pos of an arrow, for REAL OS mouse input

‎test/test-aquamoto-transport-gui.jl‎

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,18 @@
2828
renders(h, reset = false) = Int(ccall(GmtvtkTest._test_fn(:gmtvtk_render_count_test), Cint,
2929
(Ptr{Cvoid}, Cint), h, Cint(reset)))
3030

31+
# The slider's value at every paint of the 3-D view since the last start: what reached the screen.
32+
paintlog(h, start) = start ? (ccall(GmtvtkTest._test_fn(:gmtvtk_paint_log_test), Cint,
33+
(Ptr{Cvoid}, Cint, Ptr{Cint}, Cint), h, 1, C_NULL, 0); Int[]) :
34+
(buf = zeros(Cint, 100_000);
35+
n = ccall(GmtvtkTest._test_fn(:gmtvtk_paint_log_test), Cint,
36+
(Ptr{Cvoid}, Cint, Ptr{Cint}, Cint), h, 0, buf, Cint(length(buf)));
37+
Int.(buf[1:max(n, 0)]))
38+
3139
# Hold for `secs` by SLEEPING (the Timer pump runs), then release and let it settle `settle` s.
3240
function hold(h, st, dir, secs; settle = 3.0)
3341
renders(h, true)
42+
paintlog(h, true)
3443
v0 = aqf_realpress(h, dir, true)
3544
@assert v0 >= 0 "arrow press hook failed: $v0"
3645
drawn = Int[]; gap = 0.0; tl = time(); t0 = tl
@@ -45,22 +54,30 @@
4554
sleep(0.01); t = time(); gap = max(gap, t - tl); tl = t
4655
push!(marks, slider(h))
4756
end
48-
(; v0, vrel, ndrawn = length(unique(drawn)), nr, gap,
57+
pl = paintlog(h, false)
58+
painted = [pl[i] for i in eachindex(pl) if i == 1 || pl[i] != pl[i-1]] # each layer once, in order
59+
(; v0, vrel, ndrawn = length(unique(drawn)), nr, gap, painted,
4960
vmid = marks[max(1, length(marks) ÷ 2)], vend = slider(h), cur = st.cur + 1)
5061
end
5162

5263
aqf_on_screen() do # the real mouse needs the window on the screen
5364
mktempdir() do dir
65+
# The small tank, one of a real model's size (765x476), and a big one whose slice takes longer than
66+
# the arrow's repeat interval -- where the repeats used to run the slider ahead of the drawing.
67+
for (nx, ny) in ((128, 96), (765, 476), (2000, 1500))
5468
# 40 steps: enough that a working hold cannot run out of slices inside the hold window.
55-
nc = aqf_make_tsunami_nc(joinpath(dir, "tsu_transport.nc"); nt = 40)
69+
nc = aqf_make_tsunami_nc(joinpath(dir, "tsu_transport_$nx.nc"); nx, ny, nt = 40)
5670
f = iview()
5771
try
5872
IG._on_drop(f.h, nc); aqf_pump(80)
5973
st = IG._AQUA[f.h]
6074
for dir in (+1, -1)
6175
r = hold(f.h, st, dir, 2.0)
62-
@info "hold dir=$dir" r
63-
isdefined(Main, :_log) && Main._log(" hold dir=", dir, ": ", r)
76+
@info "hold $(nx)x$ny dir=$dir" r
77+
isdefined(Main, :_log) && Main._log(" hold ", nx, "x", ny, " dir=", dir, ": ", r)
78+
# SMOOTH: every layer the slider passed was PAINTED, one after the other, in order. A
79+
# hold that freezes on one layer and then jumps ten skips the ones in between.
80+
@test filter(!=(r.v0), r.painted) == collect(r.v0 + dir:dir:r.vrel)
6481
# Moves in the asked direction, several DRAWN slices (a healthy 2 s hold draws 10+).
6582
@test dir * (r.vrel - r.v0) > 3
6683
@test r.ndrawn > 3
@@ -79,6 +96,7 @@
7996
end
8097
end
8198
end
99+
end
82100
end
83101

84102
# THE SAME HOLD WITH "Sat img" ON, ON A REAL-SIZE TANK. With the satellite drape on, every slice also

‎test/test-gmtplugin-unit.jl‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,11 +102,11 @@ end
102102
# The start-up update checks fetch GitHub's release JSON on a background task. Julia 1.10's
103103
# Downloads crashed the process at exit (ACCESS VIOLATION in curl_multi_cleanup) whenever that
104104
# transfer was still in flight, e.g. a short test run, or a session closed right after start.
105-
# _bg_check stops it at exit. In a CHILD, exiting a moment after the check starts.
105+
# _bg_check waits for it at exit. In a CHILD, exiting a moment after the check starts.
106106
script = """
107107
using InteractiveGMT
108108
IG = InteractiveGMT
109-
IG._bg_check(() -> try IG._github_json("https://api.github.com/repos/GenericMappingTools/GMT.jl/releases/latest") catch end)
109+
IG._bg_check(() -> try IG.GMT.Downloads.download("https://api.github.com/repos/GenericMappingTools/GMT.jl/releases/latest", IOBuffer(); timeout = 20) catch end)
110110
sleep(0.05)
111111
print(stderr, "EXITING")
112112
"""

0 commit comments

Comments
 (0)