diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0292000e..b0aa5740 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -54,6 +54,8 @@ jobs: - uses: julia-actions/julia-runtest@v1 with: prefix: ${{ matrix.prefix }} + env: + JULIA_NUM_THREADS: 2 - uses: julia-actions/julia-uploadcodecov@v0.1 continue-on-error: true - uses: julia-actions/julia-uploadcoveralls@v0.1 diff --git a/example/gl-area.jl b/example/gl-area.jl index 75e229cc..3215ab05 100644 --- a/example/gl-area.jl +++ b/example/gl-area.jl @@ -44,6 +44,6 @@ showall(win) # https://stackoverflow.com/a/33571506/1500988 signal_connect(win, :destroy) do widget - Gtk.gtk_quit() + Gtk.gtk_main_quit() end Gtk.gtk_main() diff --git a/src/GLib/GLib.jl b/src/GLib/GLib.jl index b70a9cb3..6a957f0c 100644 --- a/src/GLib/GLib.jl +++ b/src/GLib/GLib.jl @@ -41,8 +41,6 @@ cfunction_(@nospecialize(f), r, a::Tuple) = cfunction_(f, r, Tuple{a...}) end end -const gtk_eventloop_f = Ref{Function}() - # local function, handles Symbol and makes UTF8-strings easier const AbstractStringLike = Union{AbstractString, Symbol} bytestring(s) = String(s) @@ -53,6 +51,8 @@ bytestring(s::Ptr{UInt8}) = unsafe_string(s) g_malloc(s::Integer) = ccall((:g_malloc, libglib), Ptr{Nothing}, (Csize_t,), s) g_free(p::Ptr) = ccall((:g_free, libglib), Nothing, (Ptr{Nothing},), p) +main_depth() = ccall((:g_main_depth, libglib), Cint, ()) + ccall((:g_type_init, libgobject), Nothing, ()) include("MutableTypes.jl") diff --git a/src/GLib/signals.jl b/src/GLib/signals.jl index a917048a..075a7679 100644 --- a/src/GLib/signals.jl +++ b/src/GLib/signals.jl @@ -381,7 +381,6 @@ end @deprecate g_timeout_add(interval, cb, user_data) g_timeout_add(() -> cb(user_data), interval) function g_idle_add(cb::Function) - gtk_eventloop_f[](true) callback = @cfunction(_g_callback, Cint, (Ref{Function},)) ref, deref = gc_ref_closure(cb) return ccall((:g_idle_add_full , libglib),Cint, diff --git a/src/Gtk.jl b/src/Gtk.jl index c0340da9..dc7f39b9 100644 --- a/src/Gtk.jl +++ b/src/Gtk.jl @@ -159,50 +159,63 @@ function __init__() C_NULL, C_NULL, "Julia Gtk Bindings", C_NULL, C_NULL, error_check) end - # if g_main_depth > 0, a glib main-loop is already running. - # unfortunately this call does not reliably reflect the state after the - # loop has been stopped or restarted, so only use it once at the start - gtk_main_running[] = ccall((:g_main_depth, GLib.libglib), Cint, ()) > 0 - - # Given GLib provides `g_idle_add` to specify what happens during idle, this allows - # that call to also start the eventloop - GLib.gtk_eventloop_f[] = enable_eventloop - auto_idle[] = get(ENV, "GTK_AUTO_IDLE", "true") == "true" - - # by default, defer starting the event loop until either `show`, `showall`, or `g_idle_add` is called - enable_eventloop(!auto_idle[]) + # by default, defer starting the event loop until widgets are "realized" + if auto_idle[] + # Start-stopping the event loop once makes the auto-stop process + # more stable. Reason unknown + enable_eventloop(true) + enable_eventloop(false) + else + enable_eventloop(true) + end end const auto_idle = Ref{Bool}(true) # control default via ENV["GTK_AUTO_IDLE"] -const gtk_main_running = Ref{Bool}(false) -const quit_task = Ref{Task}() const enable_eventloop_lock = Base.ReentrantLock() +const eventloop_instructed_to_stop = Ref{Bool}(false) + """ Gtk.enable_eventloop(b::Bool = true) Set whether Gtk's event loop is running. """ -function enable_eventloop(b::Bool = true; wait_stopped::Bool = false) +function enable_eventloop(b::Bool = true; wait = true) lock(enable_eventloop_lock) do # handle widgets that are being shown/destroyed from different threads - isassigned(quit_task) && wait(quit_task[]) # prevents starting while the async is still stopping if b if !is_eventloop_running() + eventloop_instructed_to_stop[] = false global gtk_main_task = schedule(Task(gtk_main)) - gtk_main_running[] = true + if !is_eventloop_running() && wait + t = Timer(5) # TODO: replace with Base.timedwait when 1.3 is dropped + while isopen(t) && !is_eventloop_running() + sleep(0.1) + end + isopen(t) || @debug "enable_eventloop: timed-out waiting for eventloop to start" + end end else if is_eventloop_running() - # @async and short sleep is needer on MacOS at least, otherwise - # the window doesn't always finish closing before the eventloop stops. - quit_task[] = @async begin - sleep(0.2) - gtk_quit() - gtk_main_running[] = false + recursive_quit_main() + eventloop_instructed_to_stop[] = true + if is_eventloop_running() && wait + t = Timer(5) # TODO: replace with Base.timedwait when 1.3 is dropped + while isopen(t) && is_eventloop_running() + sleep(0.1) + end + isopen(t) || @debug "enable_eventloop: timed-out waiting for eventloop to stop" end - wait_stopped && wait(quit_task[]) end end + return is_eventloop_running() + end +end + +# based on https://stackoverflow.com/a/44292631 +function recursive_quit_main() + gtk_main_quit() + if GLib.main_depth() > 1 + @idle_add recursive_quit_main() end end @@ -214,8 +227,8 @@ pausing. Respects whether Gtk.jl is configured to allow auto-stopping of the eventloop, unless `force = true`. """ function pause_eventloop(f; force = false) - was_running = is_eventloop_running() - (force || auto_idle[]) && enable_eventloop(false, wait_stopped = true) + was_running = eventloop_instructed_to_stop[] ? false : is_eventloop_running() + (force || auto_idle[]) && enable_eventloop(false) try f() finally @@ -228,7 +241,8 @@ end Check whether Gtk's event loop is running. """ -is_eventloop_running() = gtk_main_running[] +is_eventloop_running() = GLib.main_depth() > 0 + const ser_version = Serialization.ser_version let cachedir = joinpath(splitdir(@__FILE__)[1], "..", "gen") diff --git a/src/base.jl b/src/base.jl index 4bc5d603..2d7fbb2c 100644 --- a/src/base.jl +++ b/src/base.jl @@ -32,14 +32,14 @@ const shown_widgets = WeakKeyDict() function handle_auto_idle(w::GtkWidget) if auto_idle[] signal_connect(w, :realize) do w - enable_eventloop(true) + enable_eventloop(true, wait = false) # can't wait in a callback, unfortunately shown_widgets[w] = nothing - signal_connect(w, :destroy, #= after =# true) do w + signal_connect(w, :destroy, #= after =# false) do w delete!(shown_widgets, w) - isempty(shown_widgets) && enable_eventloop(false) + isempty(shown_widgets) && enable_eventloop(false, wait = false) end end - @static Sys.iswindows() && yield() # issue #610 + yield() # issue #610 end end function show(w::GtkWidget) diff --git a/src/events.jl b/src/events.jl index da9bd0e0..7a897f7e 100644 --- a/src/events.jl +++ b/src/events.jl @@ -2,10 +2,12 @@ gtk_main() = GLib.g_sigatom() do ccall((:gtk_main, libgtk), Nothing, ()) end -function gtk_quit() +function gtk_main_quit() ccall((:gtk_main_quit, libgtk), Nothing, ()) end +const gtk_quit = gtk_main_quit # deprecated + add_events(widget::GtkWidget, mask::Integer) = ccall((:gtk_widget_add_events, libgtk), Nothing, (Ptr{GObject}, GEnum), widget, mask) # widget[:event] = function(ptr, obj) diff --git a/test/eventloop.jl b/test/eventloop.jl new file mode 100644 index 00000000..47ed23bc --- /dev/null +++ b/test/eventloop.jl @@ -0,0 +1,105 @@ +@testset "eventloop" begin + # make sure all shown widgets have been destroyed, otherwise the eventloop + # won't stop automatically + @test length(Gtk.shown_widgets) == 0 + + @testset "control" begin + before = Gtk.auto_idle[] + + @testset "basics" begin + Gtk.auto_idle[] = true + Gtk.enable_eventloop(false) + @test !Gtk.is_eventloop_running() + Gtk.enable_eventloop(true) + @test Gtk.is_eventloop_running() + Gtk.enable_eventloop(false) + @test !Gtk.is_eventloop_running() + end + + @testset "pause_eventloop" begin + + @testset "pauses then restarts" begin + Gtk.enable_eventloop(true) + @test Gtk.is_eventloop_running() + Gtk.pause_eventloop() do + @test !Gtk.is_eventloop_running() + end + @test Gtk.is_eventloop_running() + end + + @testset "doesn't restart a stopping eventloop" begin + Gtk.enable_eventloop(false) + c = GtkCanvas() + win = GtkWindow(c) + showall(win) + sleep(1) + @test Gtk.is_eventloop_running() + destroy(win) + # the eventloop is likely still stopping here + Gtk.pause_eventloop() do + @test !Gtk.is_eventloop_running() + end + @test !Gtk.is_eventloop_running() + end + + @testset "observes auto_idle = false" begin + Gtk.auto_idle[] = false + Gtk.enable_eventloop(true) + Gtk.pause_eventloop() do + @test Gtk.is_eventloop_running() + end + @test Gtk.is_eventloop_running() + end + + @testset "observes force = true" begin + Gtk.auto_idle[] = false + Gtk.enable_eventloop(true) + Gtk.pause_eventloop(force = true) do + @test !Gtk.is_eventloop_running() + end + @test Gtk.is_eventloop_running() + end + + # Note: Test disabled because this isn't true. The event loop takes some time to stop. + # TODO: Figure out how to wait in the handle_auto_idle callbacks + + # @testset "eventloop is stopped immediately after a destroy(win) completes" begin + # c = GtkCanvas() + # win = GtkWindow(c) + # showall(win) + # @test Gtk.is_eventloop_running() + # destroy(win) + # @test !Gtk.is_eventloop_running() + # end + end + + Gtk.auto_idle[] = before + end + + @testset "Multithreading" begin + @testset "no blocking when eventloop is paused" begin + Gtk.auto_idle[] = true + Threads.nthreads() < 1 && @warn "Threads.nthreads() == 1. Multithread blocking tests are not effective" + + function multifoo() + Threads.@threads for _ in 1:Threads.nthreads() + sleep(0.1) + end + end + + Gtk.enable_eventloop(false) + win = Gtk.Window("Multithread test", 400, 300) + showall(win) + @test Gtk.is_eventloop_running() + for i in 1:10 + Gtk.pause_eventloop() do + @test !Gtk.is_eventloop_running() + t = @elapsed multifoo() # should take slightly more than 0.1 seconds + @test t < 4.5 # given the Glib uv_prepare timeout is 5000 ms + end + end + @test Gtk.is_eventloop_running() + destroy(win) + end + end +end \ No newline at end of file diff --git a/test/glib.jl b/test/glib.jl index 39cd240a..248dbeeb 100644 --- a/test/glib.jl +++ b/test/glib.jl @@ -24,6 +24,8 @@ repr = Base.print_to_string(wrap) #should display properties x = Ref{Int}(1) +Gtk.enable_eventloop(true) + function g_timeout_add_cb() x[] = 2 false @@ -82,6 +84,8 @@ g_timeout_add(()->g_timeout_add_cb(x), 1) sleep(0.5) @test x[] == 2 +Gtk.enable_eventloop(false) + end # TODO diff --git a/test/glist.jl b/test/glist.jl index f2881a16..50db5edc 100644 --- a/test/glist.jl +++ b/test/glist.jl @@ -7,10 +7,10 @@ using Test @testset "pointers" begin -w = Window("Window", 400, 300) +w1 = Window("Window", 400, 300) nb = Notebook() -w = push!(Window("Notebook"),nb) -l = ccall((:gtk_container_get_children,Gtk.libgtk),Ptr{Gtk._GList{Gtk.GtkWidget}},(Ptr{Gtk.GObject},),w) +w2 = push!(Window("Notebook"),nb) +l = ccall((:gtk_container_get_children,Gtk.libgtk),Ptr{Gtk._GList{Gtk.GtkWidget}},(Ptr{Gtk.GObject},),w2) @test eltype(l)==Gtk.GtkWidget @@ -22,6 +22,9 @@ for item in l @test item==nb end +destroy(w1) +destroy(w2) + end @testset "string" begin diff --git a/test/gui.jl b/test/gui.jl index 786aa038..10834b15 100755 --- a/test/gui.jl +++ b/test/gui.jl @@ -582,6 +582,7 @@ end @test mtrx.xx == 300 @test mtrx.yy == 280 @test mtrx.xy == mtrx.yx == mtrx.x0 == mtrx.y0 == 0 + destroy(win) end @testset "Menus" begin diff --git a/test/misc.jl b/test/misc.jl index 347fed78..b307ffa5 100644 --- a/test/misc.jl +++ b/test/misc.jl @@ -38,30 +38,4 @@ destroy(win) @test isa(Gtk.GdkEventKey(), Gtk.GdkEventKey) -@testset "Eventloop control" begin - before = Gtk.auto_idle[] - - Gtk.enable_eventloop(true) - @test Gtk.is_eventloop_running() - - Gtk.auto_idle[] = true - Gtk.pause_eventloop() do - @test !Gtk.is_eventloop_running() - end - @test Gtk.is_eventloop_running() - - Gtk.auto_idle[] = false - Gtk.pause_eventloop() do - @test Gtk.is_eventloop_running() - end - @test Gtk.is_eventloop_running() - - Gtk.pause_eventloop(force = true) do - @test !Gtk.is_eventloop_running() - end - @test Gtk.is_eventloop_running() - - Gtk.auto_idle[] = before -end - end diff --git a/test/runtests.jl b/test/runtests.jl index 5316cce8..6d128b3d 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -7,5 +7,6 @@ include("gui.jl") include("list.jl") include("misc.jl") include("text.jl") +include("eventloop.jl") end diff --git a/test/tree.jl b/test/tree.jl index 074ccde0..9613e58e 100644 --- a/test/tree.jl +++ b/test/tree.jl @@ -76,4 +76,6 @@ select!(selection, iter) @test length(selection) == 1 # this crashes -# iters = Gtk.selected_rows(selection) \ No newline at end of file +# iters = Gtk.selected_rows(selection) + +destroy(window)