Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/GMT.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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, " " => "_")
Expand Down
38 changes: 38 additions & 0 deletions src/common_options.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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]"])
Expand All @@ -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
Expand All @@ -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)
Expand All @@ -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"))
Expand Down
14 changes: 13 additions & 1 deletion src/extras/weather.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
75 changes: 54 additions & 21 deletions src/ghost/ghost_funs.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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)))
Expand All @@ -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)))
Expand All @@ -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)))
Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -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 ───────────────────────────────────────────────────
Expand Down Expand Up @@ -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

Expand All @@ -701,24 +729,29 @@ 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)
_gs_close_if_open(ps_data) && (ps_data = read(ps_data))
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()
Loading