diff --git a/src/GMT.jl b/src/GMT.jl index d17713ab6..34dac3b13 100644 --- a/src/GMT.jl +++ b/src/GMT.jl @@ -121,6 +121,7 @@ const isPSclosed = Ref{Bool}(false) # Modern mode will close the PS at the e const noGrdCopy = Ref{Bool}(false) # If true, grids are sent without transpose/copy const GMTCONF = Ref{Bool}(false) # Flag if gmtset was used and must be 'unused' const FMT = Ref{String}("png") # The default plot format +const USE_PSVIEW = Ref{Bool}(false) # showfig() displays with psview() when no fmt/figname. See use_psview() const BOX_STR = Ref{String}("") # Used in plotyy to know -R of first call const POSTMAN = Ref{Dict{String,String}}(Dict{String,String}()) # To pass messages to functions (start with get_dataset) const DEF_FIG_SIZE = "15c/10c" # Default fig size for plot like programs. Approx 16/11 @@ -165,7 +166,7 @@ export grdview, grdview!, grdvolume, greenspline, histogram, histogram!, image, image!, image_alpha!, image_cpt!, imshow, ind2rgb, isnodata, kml2gmt, logo, logo!, makecpt, mapscale, mapscale!, mask, mask!, mapproject, movie, nearneighbor, plot, plot!, plot3, plot3!, plot3d, plot3d!, plotyy, project, pscontour, pscontour!, - psconvert, ps2image, ps2raster, psbbox, psview, psbasemap, psbasemap!, psclip, psclip!, pscoast, pscoast!, psevents, pshistogram, pshistogram!, + psconvert, ps2image, ps2raster, psbbox, psview, use_psview, psbasemap, psbasemap!, psclip, psclip!, pscoast, pscoast!, psevents, pshistogram, pshistogram!, psimage, psimage!, pslegend, pslegend!, psmask, psmask!, psrose, psrose!, psscale, psscale!, pssolar, pssolar!, psternary, psternary!, pstext, pstext!, pswiggle, pswiggle!, psxy, psxy!, psxyz, psxyz!, regress, resetGMT, rose, rose!, sample1d, scatter, scatter!, scatter3, scatter3!, solar, solar!, analemma, enso, keeling, @@ -540,6 +541,7 @@ function __init__(test::Bool=false) haskey(ENV, "JULIA_GMT_IMGFORMAT") && (FMT[] = ENV["JULIA_GMT_IMGFORMAT"]) f = joinpath(GMTuserdir[1], "theme_jl.txt") (isfile(f)) && (theme(readline(f)); ThemeIsOn[] = false) # False because we don't want it reset in showfig() + USE_PSVIEW[] = isfile(joinpath(GMTuserdir[1], "psview_jl.txt")) # Saved by use_psview() user = (Sys.isunix() || Sys.isapple()) ? Libc.getpwuid(Libc.getuid(), true).username : Sys.iswindows() ? ENV["USERNAME"] : "" !isascii(user) && (user = string(hash(user), base=16)[1:8])# For non-ASCII user names TMPDIR_USR.username = replace(user, " " => "_") diff --git a/src/common_options.jl b/src/common_options.jl index fc04ecfb9..f800d87a2 100644 --- a/src/common_options.jl +++ b/src/common_options.jl @@ -3684,6 +3684,7 @@ function fname_out(d::Dict{Symbol,Any}, del::Bool=false) fname, EXT = splitext(val) EXT = (EXT == "") ? FMT[] : EXT[2:end] end + no_fmt_name = (fname == "" && !haskey(d, :fmt)) # Must be known before 'fmt' is deleted. Used by use_psview() if (EXT == FMT[] && haskey(d, :fmt)) EXT = string(d[:fmt])::String (del) && delete!(d, :fmt) @@ -3714,6 +3715,8 @@ function fname_out(d::Dict{Symbol,Any}, del::Bool=false) elseif (ext == "pdg") opt_T = " -Tf -Qp"; EXT = "pdf" else error("Unknown graphics file extension (.$EXT)") end + # Display it with psview() instead of psconvert + system viewer. Flag is consumed by showfig() + (del && USE_PSVIEW[] && no_fmt_name && opt_T != "" && !ret_ps && !IamModern[]) && (d[:_psview] = true) end (fname != "") && (fname *= "." * EXT) @@ -4252,6 +4255,31 @@ function update_cmds_history(cmd::Vector{String}) return cmd end +# --------------------------------------------------------------------------------------------------- +""" + use_psview([onoff::Bool]) -> Bool + +Make `showfig`, and so `viz`, `imshow` and every `show=true`, display the figure with `psview` instead of +converting it with `psconvert` and opening it in the system viewer. This only happens when neither `fmt` +nor `figname` (or its aliases `savefig`, `name`) is used. Those keep working as before. + +- `use_psview()`: toggle the current state. +- `use_psview(true)` or `use_psview(false)`: set it. + +The state is saved in the directory printed by `GMT.GMTuserdir[1]` so it is remembered in next sessions. +Returns the new state. +""" +function use_psview(onoff::Bool=!USE_PSVIEW[])::Bool + f = joinpath(GMTuserdir[1], "psview_jl.txt") + if (onoff) + !isdir(GMTuserdir[1]) && mkdir(GMTuserdir[1]) + write(f, "psview") + else + isfile(f) && rm(f) + end + USE_PSVIEW[] = onoff +end + # --------------------------------------------------------------------------------------------------- """ showfig([fmt="format", figname="figname[.fmt]"]) @@ -4274,6 +4302,9 @@ function showfig(d::Dict{Symbol, Any}, fname_ps::String, fname_ext::String, opt_ digests_legend_bag(d) # Plot the legend if requested isPluto = isdefined(Main, :PlutoRunner) && Main.PlutoRunner isa Module + # use_psview() mode: show the PS itself in psview(), no psconvert. Not in notebooks, VSCode or the docs. + do_psview = (pop!(d, :_psview, false)::Bool) && !isPluto && !isJupyter[] && !isFranklin[] && + !(isdefined(Main, :IJulia) && Main.IJulia.inited) && !(isdefined(Main, :VSCodeServer) && get(ENV, "DISPLAY_IN_VSC", "") != "no") if (fname == "" && (isdefined(Main, :IJulia) && Main.IJulia.inited) || isPluto) opt_T = " -Tg"; fname_ext = "png" # In Jupyter or Pluto, png only end @@ -4290,6 +4321,7 @@ function showfig(d::Dict{Symbol, Any}, fname_ps::String, fname_ext::String, opt_ # When we are in Jupyter or Pluto, the image may already be in a raster format by a previous call to 'gmt end' (isJupyter[] || isPluto) && (fname_ps = TMPDIR_USR.dir * "/" * "GMTjl_" * TMPDIR_USR.username * TMPDIR_USR.pid_suffix * "." * FMT[]; opt_T = "") + do_psview && (opt_T = "") # psview() shows the PS itself, no psconvert if (opt_T != "") (K) && close_PS_file(fname_ps) # Close the PS file first ((val = hlp_desnany_str(d, [:dpi, :DPI])) !== "") && (opt_T *= " -E" * val) @@ -4302,6 +4334,12 @@ function showfig(d::Dict{Symbol, Any}, fname_ps::String, fname_ext::String, opt_ out = (fname != "") ? mv(fname_ps, fname, force=true) : fname_ps end + if (do_psview && haskey(d, :show) && d[:show] != 0) + psview(out; dpi=(((val = hlp_desnany_str(d, [:dpi, :DPI])) !== "") ? parse(Int, val) : 300)) + d[:show] = 0 # Done. Skip the display below + reset_theme() + end + retPluto = false # To know if we are in Pluto if (haskey(d, :show) && d[:show] != 0) if ((isdefined(Main, :IJulia) && Main.IJulia.inited) || (isdefined(Main, :VSCodeServer) && get(ENV, "DISPLAY_IN_VSC", "") != "no")) diff --git a/src/extras/weather.jl b/src/extras/weather.jl index dc764cf94..e2b247930 100644 --- a/src/extras/weather.jl +++ b/src/extras/weather.jl @@ -253,7 +253,19 @@ function ecmwf(source::Symbol=:reanalysis; filename="", cb::Bool=false, dataset= end if (cb && params == "") - tcb = clipboard() + # On Windows another process (clipboard history, sync, ...) may hold the clipboard for a few ms after a write + function read_cb() + for k = 1:9 + try + return clipboard() + catch e + !isa(e, SystemError) && rethrow() + sleep(0.05) + end + end + return clipboard() # Last attempt, let it error + end + tcb = read_cb() !contains(tcb, "variable\"") && error("Clipboard does not contain a valid API request code") body, _dataset = parse_request(tcb) else diff --git a/src/ghost/ghost_funs.jl b/src/ghost/ghost_funs.jl index de8a0ecc2..a288fe4e4 100644 --- a/src/ghost/ghost_funs.jl +++ b/src/ghost/ghost_funs.jl @@ -415,6 +415,35 @@ isdefined(@__MODULE__, :_GS_SCROLL_Y) || const _GS_SCROLL_Y = Ref{Int32}(Int32(0 isdefined(@__MODULE__, :_GS_CLIENT_W) || const _GS_CLIENT_W = Ref{Int32}(Int32(0)) isdefined(@__MODULE__, :_GS_CLIENT_H) || const _GS_CLIENT_H = Ref{Int32}(Int32(0)) isdefined(@__MODULE__, :_GS_ZOOM) || const _GS_ZOOM = Ref{Float64}(1.0) +# The viewer's own image size and window. Not _GS_STATE's, which other GS calls reset while the window is open. +isdefined(@__MODULE__, :_GS_VIEW_W) || const _GS_VIEW_W = Ref{Int}(0) +isdefined(@__MODULE__, :_GS_VIEW_H) || const _GS_VIEW_H = Ref{Int}(0) +isdefined(@__MODULE__, :_GS_HWND) || const _GS_HWND = Ref{Ptr{Cvoid}}(C_NULL) + +# Close the viewer window, if one is open. Must run on the thread that created it. +function _gs_close_viewer() + hwnd = _GS_HWND[] + (hwnd != C_NULL && ccall((:IsWindow, "user32"), Cint, (Ptr{Cvoid},), hwnd) != 0) && + ccall((:DestroyWindow, "user32"), Cint, (Ptr{Cvoid},), hwnd) + _GS_HWND[] = C_NULL + nothing +end + +# Pump the viewer window's messages from a task, so that the REPL stays usable. Only this window's +# messages are taken (hwnd filter), others on this thread (e.g. a Qt app) are left to their owners. +function _gs_pump(hwnd::Ptr{Cvoid}) + msg = zeros(UInt8, 48) # MSG struct is 48 bytes on Windows 64-bit + while ccall((:IsWindow, "user32"), Cint, (Ptr{Cvoid},), hwnd) != 0 + while ccall((:PeekMessageW, "user32"), Cint, (Ptr{UInt8}, Ptr{Cvoid}, UInt32, UInt32, UInt32), + msg, hwnd, 0, 0, 0x0001) != 0 # PM_REMOVE + ccall((:TranslateMessage, "user32"), Bool, (Ptr{UInt8},), msg) + ccall((:DispatchMessageW, "user32"), Int64, (Ptr{UInt8},), msg) + end + sleep(0.02) + end + (_GS_HWND[] == hwnd) && (_GS_HWND[] = C_NULL; _GS_BGR[] = UInt8[]) # Free the image + nothing +end struct _SCROLLINFO # 28 bytes cbSize :: UInt32 @@ -428,8 +457,8 @@ end function _gs_resize_to_zoom(hwnd::Ptr{Cvoid}) zoom = _GS_ZOOM[] - new_cw = round(Int, _GS_STATE.width * zoom) - new_ch = round(Int, _GS_STATE.height * zoom) + new_cw = round(Int, _GS_VIEW_W[] * zoom) + new_ch = round(Int, _GS_VIEW_H[] * zoom) # Non-client area (title bar + borders + scrollbars) wrect = zeros(Int32, 4); crect = zeros(Int32, 4) GC.@preserve wrect crect begin @@ -460,7 +489,7 @@ end function _gs_update_scroll_ranges(hwnd::Ptr{Cvoid}) zoom = _GS_ZOOM[] - W = _GS_STATE.width; H = _GS_STATE.height + W = _GS_VIEW_W[]; H = _GS_VIEW_H[] zW = round(Int, W*zoom); zH = round(Int, H*zoom) cw = Int(_GS_CLIENT_W[]); ch = Int(_GS_CLIENT_H[]) sv = Ref(_SCROLLINFO(UInt32(28), UInt32(0x0003), Int32(0), Int32(max(0,zH-1)), UInt32(ch), Int32(0), Int32(0))) @@ -470,7 +499,7 @@ function _gs_update_scroll_ranges(hwnd::Ptr{Cvoid}) end function _gs_set_hscroll(hwnd::Ptr{Cvoid}, pos::Int) - zW = round(Int, _GS_STATE.width * _GS_ZOOM[]) + zW = round(Int, _GS_VIEW_W[] * _GS_ZOOM[]) pos = clamp(pos, 0, max(0, zW - Int(_GS_CLIENT_W[]))) _GS_SCROLL_X[] = Int32(pos) si = Ref(_SCROLLINFO(UInt32(28), UInt32(0x0004), Int32(0), Int32(0), UInt32(0), Int32(pos), Int32(0))) @@ -479,7 +508,7 @@ function _gs_set_hscroll(hwnd::Ptr{Cvoid}, pos::Int) end function _gs_set_vscroll(hwnd::Ptr{Cvoid}, pos::Int) - zH = round(Int, _GS_STATE.height * _GS_ZOOM[]) + zH = round(Int, _GS_VIEW_H[] * _GS_ZOOM[]) pos = clamp(pos, 0, max(0, zH - Int(_GS_CLIENT_H[]))) _GS_SCROLL_Y[] = Int32(pos) si = Ref(_SCROLLINFO(UInt32(28), UInt32(0x0004), Int32(0), Int32(0), UInt32(0), Int32(pos), Int32(0))) @@ -488,8 +517,8 @@ function _gs_set_vscroll(hwnd::Ptr{Cvoid}, pos::Int) end function _gs_paint_to_dc(hdc::Ptr{Cvoid}, xoff::Int=0, yoff::Int=0) - _GS_STATE.ready && !isempty(_GS_BGR[]) || return - W = _GS_STATE.width; H = _GS_STATE.height + !isempty(_GS_BGR[]) || return + W = _GS_VIEW_W[]; H = _GS_VIEW_H[] zoom = _GS_ZOOM[] zW = round(Int, W * zoom); zH = round(Int, H * zoom) bmi = _BITMAPINFOHEADER(40, Int32(W), Int32(-H), 1, 24, 0, 0, 0, 0, 0, 0) @@ -592,8 +621,7 @@ function _gs_wndproc(hwnd::Ptr{Cvoid}, msg::UInt32, wp::UInt64, lp::Int64)::Int6 ccall((:InvalidateRect, "user32"), Bool, (Ptr{Cvoid}, Ptr{Cvoid}, Bool), hwnd, C_NULL, false) return Int64(0) - elseif msg == 0x0002 # WM_DESTROY - ccall((:PostQuitMessage, "user32"), Cvoid, (Cint,), 0) + elseif msg == 0x0002 # WM_DESTROY. No PostQuitMessage: its WM_QUIT would end any other loop on this thread return Int64(0) elseif msg == 0x0100 && wp == 0x1B # WM_KEYDOWN + VK_ESCAPE @@ -609,7 +637,8 @@ end Display PostScript in a native Win32 window — equivalent to `gswin64c file.ps`. GS rasterizes via display device; pixels are painted with `SetDIBitsToDevice`. -Close the window or press Escape to exit. +Close the window or press Escape to exit. Returns at once, the window's messages are pumped by a task +so the REPL stays usable. A new call closes the previous window. `crop=true` crops to the bounding box (`psbbox`). """ psview(ps_data::GMTps; dpi::Int=300, crop::Bool=true) = psview(ps_data.postscript; dpi=dpi, crop=crop) @@ -626,6 +655,8 @@ function psview(ps_data::Union{String, AbstractVector{UInt8}}=PSname[]; dpi::Int _gs_run_ps_str(inst, ps_data, () -> "") end; display_cb=cb_ref) _GS_STATE.ready || error("Ghostscript produced no output.") + _gs_close_viewer() # One viewer at a time: the window state below is shared + _GS_VIEW_W[] = _GS_STATE.width; _GS_VIEW_H[] = _GS_STATE.height _GS_BGR[] = _gs_make_bgr_dib(_GS_STATE.width, _GS_STATE.height, _GS_STATE.raster, _GS_STATE.data) # ── Create Win32 window ─────────────────────────────────────────────────── @@ -683,14 +714,11 @@ function psview(ps_data::Union{String, AbstractVector{UInt8}}=PSname[]; dpi::Int C_NULL, C_NULL, hmod, C_NULL) hwnd == C_NULL && error("CreateWindowExW failed") ccall((:UpdateWindow, "user32"), Bool, (Ptr{Cvoid},), hwnd) - - # Message loop — blocks until window closed - msg = zeros(UInt8, 48) # MSG struct is 48 bytes on Windows 64-bit - while ccall((:GetMessageW, "user32"), Int32, (Ptr{UInt8}, Ptr{Cvoid}, UInt32, UInt32), msg, C_NULL, 0, 0) > 0 - ccall((:TranslateMessage, "user32"), Bool, (Ptr{UInt8},), msg) - ccall((:DispatchMessageW, "user32"), Int64, (Ptr{UInt8},), msg) - end end + + # Message loop in a task (same thread, as Win32 requires) — returns at once, the REPL stays usable + _GS_HWND[] = hwnd + errormonitor(@async _gs_pump(hwnd)) nothing end @@ -701,7 +729,7 @@ end # @static if Sys.iswindows() psview(ps_data; dpi=300, crop=true) Display PostScript using Ghostscript's native window (x11 on Linux, aqua on macOS). -Blocks until the viewer window is closed. +Returns at once, the window is kept alive by a task. A new call closes the previous window. `crop=true` crops to the bounding box (`psbbox`). """ function psview(ps_data::Union{String, AbstractVector{UInt8}}=PSname[]; dpi::Int=300, crop::Bool=true) @@ -709,16 +737,21 @@ function psview(ps_data::Union{String, AbstractVector{UInt8}}=PSname[]; dpi::Int crop_args, prologue = _gs_crop(ps_data, crop) #device = Sys.islinux() ? "x11" : "aqua" args = ["gs", "-dNOPAUSE", "-dQUIET", "-sDEVICE=display", "-r$(dpi)", crop_args...] - _gs_session(args, inst -> begin + # One viewer at a time: make the previous one end its GS session before starting a new one + my_id = (_GS_VIEW_ID[] += 1) + (_GS_VIEW_TASK[] !== nothing) && (try wait(_GS_VIEW_TASK[]) catch end) # A failed one was already reported + _GS_VIEW_TASK[] = errormonitor(@async _gs_session(args, inst -> begin (prologue != "") && _gs_run_ps_str(inst, prologue, () -> "") _gs_run_ps_str(inst, ps_data, () -> "") exit_code = Ref{Cint}(0) rc = 0 - while rc >= 0 + while rc >= 0 && _GS_VIEW_ID[] == my_id rc = gsapi_run_string_with_length(inst, "\n", Cuint(1), Cint(0), exit_code) sleep(0.05) end - end) + end)) nothing end +isdefined(@__MODULE__, :_GS_VIEW_ID) || const _GS_VIEW_ID = Ref{Int}(0) +isdefined(@__MODULE__, :_GS_VIEW_TASK) || const _GS_VIEW_TASK = Ref{Union{Task, Nothing}}(nothing) end # @static if !Sys.iswindows()