diff --git a/scripts/distrobox/run.sh b/scripts/distrobox/run.sh index 1bc3a6f..4c778fd 100755 --- a/scripts/distrobox/run.sh +++ b/scripts/distrobox/run.sh @@ -15,13 +15,18 @@ cd -- "$repo_root" # Avoid host/container DMABUF incompatibilities on mismatched GPU stacks. An # explicit GTK renderer choice still takes precedence. export GSK_RENDERER="${GSK_RENDERER:-cairo}" -# gtk4-layer-shell must load before libwayland-client or its Wayland shim is -# skipped and the overlay sits as an unpositioned normal window. -layer_shell_lib="$(pkg-config --variable=libdir gtk4-layer-shell-0)/libgtk4-layer-shell.so.0" -if [[ -f "$layer_shell_lib" ]]; then - export LD_PRELOAD="${layer_shell_lib}${LD_PRELOAD:+:$LD_PRELOAD}" -fi +# gtk4-layer-shell must precede libwayland-client in the link line. Do NOT +# LD_PRELOAD it here: the shim breaks the main GtkApplication window on +# Wayland when forced onto the whole process. export CGO_LDFLAGS="-Wl,--no-as-needed -lgtk4-layer-shell ${CGO_LDFLAGS:-}" + +desktop_bin=build/captioneer-desktop +if [[ -x "$desktop_bin" ]]; then + printf "captioneer: launching %s\\n" "$desktop_bin" >&2 + exec "./$desktop_bin" "$@" +fi + +printf "captioneer: no build/captioneer-desktop yet; compiling with go run (first launch is slow)\\n" >&2 exec go run -tags gtk ./src/cmd/captioneer-desktop "$@"' exec distrobox enter --no-workdir --name "$container_name" -- \ diff --git a/src/config/config.go b/src/config/config.go index 4b7f10b..182a8d7 100644 --- a/src/config/config.go +++ b/src/config/config.go @@ -76,6 +76,10 @@ type OverlaySettings struct { Monitor string FinalTimeout time.Duration ClickThrough bool + // PositionX/PositionY are monitor-local logical pixels. -1 means automatic + // placement (horizontally centered, BottomMargin above the bottom edge). + PositionX int + PositionY int } func DefaultOverlaySettings() OverlaySettings { @@ -89,6 +93,8 @@ func DefaultOverlaySettings() OverlaySettings { Monitor: "auto", FinalTimeout: MinimumOverlayFinalLifetime, ClickThrough: true, + PositionX: -1, + PositionY: -1, } } @@ -202,6 +208,12 @@ func (s OverlaySettings) Validate() error { if s.Monitor == "" { return errors.New("--overlay-monitor cannot be empty") } + if s.PositionX < -1 { + return errors.New("overlay position x cannot be less than -1") + } + if s.PositionY < -1 { + return errors.New("overlay position y cannot be less than -1") + } return nil } diff --git a/src/config/store.go b/src/config/store.go index 912b504..046d766 100644 --- a/src/config/store.go +++ b/src/config/store.go @@ -44,6 +44,8 @@ type diskConfig struct { Monitor string `json:"monitor"` FinalTimeout string `json:"final_timeout"` ClickThrough bool `json:"click_through"` + PositionX *int `json:"position_x,omitempty"` + PositionY *int `json:"position_y,omitempty"` } `json:"overlay"` TranscriptLog struct { Enabled bool `json:"enabled"` @@ -163,6 +165,10 @@ func diskFromConfig(config AppConfig) diskConfig { disk.Overlay.Monitor = config.Overlay.Monitor disk.Overlay.FinalTimeout = config.Overlay.FinalTimeout.String() disk.Overlay.ClickThrough = config.Overlay.ClickThrough + positionX := config.Overlay.PositionX + positionY := config.Overlay.PositionY + disk.Overlay.PositionX = &positionX + disk.Overlay.PositionY = &positionY disk.TranscriptLog.Enabled = config.TranscriptLog.Enabled disk.TranscriptLog.Directory = config.TranscriptLog.Directory return disk @@ -185,6 +191,13 @@ func (disk diskConfig) toConfig() (AppConfig, error) { if err != nil { return AppConfig{}, err } + positionX, positionY := -1, -1 + if disk.Overlay.PositionX != nil { + positionX = *disk.Overlay.PositionX + } + if disk.Overlay.PositionY != nil { + positionY = *disk.Overlay.PositionY + } return AppConfig{ Version: disk.Version, GUI: GUISettings{ @@ -218,6 +231,8 @@ func (disk diskConfig) toConfig() (AppConfig, error) { Monitor: disk.Overlay.Monitor, FinalTimeout: finalTimeout, ClickThrough: disk.Overlay.ClickThrough, + PositionX: positionX, + PositionY: positionY, }, TranscriptLog: TranscriptLogSettings{ Enabled: disk.TranscriptLog.Enabled, diff --git a/src/gui/app.go b/src/gui/app.go index c170ebb..527ab99 100644 --- a/src/gui/app.go +++ b/src/gui/app.go @@ -143,13 +143,16 @@ func (a *Application) activate() { a.buildWindow() a.installActions() a.subscribeModels() + overlay.SetPositionSaver(a.saveOverlayPosition) if a.launch.Warning != nil { a.log.Add(diagnostics.Warning, "configuration", fmt.Sprintf("%v; using built-in defaults", a.launch.Warning)) } + // Show the main window before creating the overlay so a Layer Shell / + // display failure cannot leave the user with a silent no-GUI launch. + a.window.Present() if err := a.configureOutputs(a.config, true); err != nil { a.log.Add(diagnostics.Error, "outputs", err.Error()) } - a.window.Present() a.applyAlwaysOnTop(a.config.GUI.MainWindowAlwaysOnTop) if a.config.GUI.StartCaptions { a.startCaptions() @@ -395,6 +398,18 @@ func (a *Application) restartCaptions() { }() } +func (a *Application) saveOverlayPosition(x, y int) { + a.config.Overlay.PositionX = x + a.config.Overlay.PositionY = y + a.applied.Overlay.PositionX = x + a.applied.Overlay.PositionY = y + if err := config.Save(a.launch.Path, a.config); err != nil { + a.log.Add(diagnostics.Error, "configuration", fmt.Sprintf("Save overlay position: %v", err)) + return + } + a.log.Add(diagnostics.Info, "overlay", fmt.Sprintf("Saved caption position (%d, %d)", x, y)) +} + func (a *Application) configureOutputs(next config.AppConfig, initial bool) error { var outputErr error a.history.SetEnabled(next.Outputs.History) diff --git a/src/gui/settings.go b/src/gui/settings.go index 3905e76..3fff5bd 100644 --- a/src/gui/settings.go +++ b/src/gui/settings.go @@ -170,7 +170,7 @@ func (a *Application) overlaySettingsPage() *gtk.ScrolledWindow { box.Append(settingRow("Maximum width", "Maximum overlay width in pixels.", a.settings.overlayMaxWidth)) box.Append(settingRow("Monitor", "Use auto or the configured monitor connector/name.", a.settings.overlayMonitor)) box.Append(settingRow("Final caption lifetime", "Zero keeps final text visible; otherwise use 3s or longer.", a.settings.overlayFinalTimeout)) - box.Append(settingRow("Click through overlay", "Allow pointer input to pass through the caption surface.", a.settings.overlayClickThrough)) + box.Append(settingRow("Click through overlay", "Pass pointer input through empty areas. Hold Ctrl and drag the caption bubble to reposition it; the spot is remembered.", a.settings.overlayClickThrough)) return settingsScroller(box) } @@ -272,6 +272,8 @@ func (a *Application) readSettings() (config.AppConfig, error) { Monitor: strings.TrimSpace(a.settings.overlayMonitor.Text()), FinalTimeout: finalTimeout, ClickThrough: a.settings.overlayClickThrough.Active(), + PositionX: a.config.Overlay.PositionX, + PositionY: a.config.Overlay.PositionY, }, TranscriptLog: config.TranscriptLogSettings{ Enabled: a.settings.transcriptEnabled.Active(), diff --git a/src/output/overlay/overlay_gtk.go b/src/output/overlay/overlay_gtk.go index df46c55..bed0a26 100644 --- a/src/output/overlay/overlay_gtk.go +++ b/src/output/overlay/overlay_gtk.go @@ -33,6 +33,16 @@ typedef struct { gboolean final_visible; gboolean preview_visible; gboolean x11; + gboolean layer_shell; + gboolean click_through; + gboolean dragging; + int bottom_margin; + int position_x; + int position_y; + double drag_start_px; + double drag_start_py; + int drag_origin_x; + int drag_origin_y; } CaptioneerOverlay; typedef struct { @@ -41,6 +51,9 @@ typedef struct { char *preview_text; } CaptioneerUpdate; +// Implemented on the Go side via //export so drag placement can be persisted. +extern void captioneerOverlayPositionChanged(int x, int y); + static void captioneer_append_warning(GString *warnings, const char *message) { if (warnings->len > 0) { g_string_append(warnings, " "); @@ -126,15 +139,198 @@ static GdkMonitor *captioneer_select_monitor( return monitor; } -static void captioneer_set_input_passthrough(CaptioneerOverlay *overlay, GString *warnings) { - if (!gdk_display_supports_input_shapes(gtk_widget_get_display(GTK_WIDGET(overlay->window)))) { - captioneer_append_warning(warnings, "This display backend does not support pointer click-through."); +static GdkMonitor *captioneer_overlay_monitor(CaptioneerOverlay *overlay) { + if (overlay->monitor != NULL) { + return overlay->monitor; + } + GdkDisplay *display = gtk_widget_get_display(GTK_WIDGET(overlay->window)); + GdkSurface *surface = gtk_native_get_surface(GTK_NATIVE(overlay->window)); + if (surface != NULL) { + GdkMonitor *monitor = gdk_display_get_monitor_at_surface(display, surface); + if (monitor != NULL) { + return monitor; + } + } + return captioneer_monitor_by_index(display, 0); +} + +static void captioneer_update_input_region(CaptioneerOverlay *overlay) { + GdkSurface *surface = gtk_native_get_surface(GTK_NATIVE(overlay->window)); + if (surface == NULL) { + return; + } + if (!overlay->click_through || overlay->dragging) { + gdk_surface_set_input_region(surface, NULL); + return; + } + // Limit hit-testing to the caption bubble so the transparent full-width + // chrome stays click-through. CTRL+drag still works because clicks that + // land on the bubble include modifier state. + int bubble_width = gtk_widget_get_width(overlay->bubble); + int bubble_height = gtk_widget_get_height(overlay->bubble); + if (bubble_width <= 0 || bubble_height <= 0) { + cairo_region_t *empty = cairo_region_create(); + gdk_surface_set_input_region(surface, empty); + cairo_region_destroy(empty); + return; + } + double native_x = 0, native_y = 0; + if (!gtk_widget_translate_coordinates(overlay->bubble, GTK_WIDGET(overlay->window), 0, 0, &native_x, &native_y)) { + native_x = 0; + native_y = 0; + } + cairo_rectangle_int_t rect = { + .x = (int)native_x, + .y = (int)native_y, + .width = bubble_width, + .height = bubble_height, + }; + cairo_region_t *region = cairo_region_create_rectangle(&rect); + gdk_surface_set_input_region(surface, region); + cairo_region_destroy(region); +} + +static void captioneer_apply_placement(CaptioneerOverlay *overlay) { + GdkSurface *surface = gtk_native_get_surface(GTK_NATIVE(overlay->window)); + if (surface == NULL) { + return; + } + GdkMonitor *monitor = captioneer_overlay_monitor(overlay); + if (monitor == NULL) { + return; + } + GdkRectangle geometry; + gdk_monitor_get_geometry(monitor, &geometry); + int width = gdk_surface_get_width(surface); + int height = gdk_surface_get_height(surface); + if (width <= 1 || height <= 1) { + return; + } + + int x = overlay->position_x; + int y = overlay->position_y; + if (x < 0) { + x = (geometry.width - width) / 2; + } + if (y < 0) { + y = geometry.height - height - overlay->bottom_margin; + } + if (x < 0) { + x = 0; + } + if (y < 0) { + y = 0; + } + if (x + width > geometry.width) { + x = MAX(0, geometry.width - width); + } + if (y + height > geometry.height) { + y = MAX(0, geometry.height - height); + } + + if (overlay->layer_shell) { + gtk_layer_set_anchor(overlay->window, GTK_LAYER_SHELL_EDGE_LEFT, TRUE); + gtk_layer_set_anchor(overlay->window, GTK_LAYER_SHELL_EDGE_TOP, TRUE); + gtk_layer_set_anchor(overlay->window, GTK_LAYER_SHELL_EDGE_RIGHT, FALSE); + gtk_layer_set_anchor(overlay->window, GTK_LAYER_SHELL_EDGE_BOTTOM, FALSE); + gtk_layer_set_margin(overlay->window, GTK_LAYER_SHELL_EDGE_LEFT, x); + gtk_layer_set_margin(overlay->window, GTK_LAYER_SHELL_EDGE_TOP, y); + gtk_layer_set_margin(overlay->window, GTK_LAYER_SHELL_EDGE_RIGHT, 0); + gtk_layer_set_margin(overlay->window, GTK_LAYER_SHELL_EDGE_BOTTOM, 0); + } else if (overlay->x11) { + Display *display = GDK_SURFACE_XDISPLAY(surface); + Window xid = GDK_SURFACE_XID(surface); + int root_x = geometry.x + x; + int root_y = geometry.y + y; + XSizeHints *size_hints = XAllocSizeHints(); + if (size_hints != NULL) { + size_hints->flags = USPosition | PPosition; + size_hints->x = root_x; + size_hints->y = root_y; + XSetWMNormalHints(display, xid, size_hints); + XFree(size_hints); + } + XMoveWindow(display, xid, root_x, root_y); + XFlush(display); + } + captioneer_update_input_region(overlay); +} + +static gboolean captioneer_reposition_x11(gpointer data) { + captioneer_apply_placement(data); + return G_SOURCE_REMOVE; +} + +static void captioneer_surface_size_changed(GObject *object, GParamSpec *spec, gpointer data) { + (void)object; + (void)spec; + g_idle_add(captioneer_reposition_x11, data); +} + +static gboolean captioneer_ctrl_held(GdkEvent *event) { + return (gdk_event_get_modifier_state(event) & GDK_CONTROL_MASK) != 0; +} + +static void captioneer_drag_begin(GtkGestureDrag *gesture, double x, double y, gpointer data) { + (void)x; + (void)y; + CaptioneerOverlay *overlay = data; + GdkEvent *event = gtk_event_controller_get_current_event(GTK_EVENT_CONTROLLER(gesture)); + if (event == NULL || !captioneer_ctrl_held(event)) { + gtk_gesture_set_state(GTK_GESTURE(gesture), GTK_EVENT_SEQUENCE_DENIED); return; } GdkSurface *surface = gtk_native_get_surface(GTK_NATIVE(overlay->window)); - cairo_region_t *empty = cairo_region_create(); - gdk_surface_set_input_region(surface, empty); - cairo_region_destroy(empty); + if (surface == NULL) { + gtk_gesture_set_state(GTK_GESTURE(gesture), GTK_EVENT_SEQUENCE_DENIED); + return; + } + overlay->dragging = TRUE; + captioneer_update_input_region(overlay); + gtk_gesture_set_state(GTK_GESTURE(gesture), GTK_EVENT_SEQUENCE_CLAIMED); + + GdkMonitor *monitor = captioneer_overlay_monitor(overlay); + GdkRectangle geometry = {0}; + if (monitor != NULL) { + gdk_monitor_get_geometry(monitor, &geometry); + } + int width = gdk_surface_get_width(surface); + int height = gdk_surface_get_height(surface); + int origin_x = overlay->position_x; + int origin_y = overlay->position_y; + if (origin_x < 0) { + origin_x = (geometry.width - width) / 2; + } + if (origin_y < 0) { + origin_y = geometry.height - height - overlay->bottom_margin; + } + overlay->drag_origin_x = origin_x; + overlay->drag_origin_y = origin_y; + gtk_gesture_drag_get_start_point(gesture, &overlay->drag_start_px, &overlay->drag_start_py); +} + +static void captioneer_drag_update(GtkGestureDrag *gesture, double offset_x, double offset_y, gpointer data) { + CaptioneerOverlay *overlay = data; + if (!overlay->dragging) { + return; + } + (void)gesture; + overlay->position_x = overlay->drag_origin_x + (int)offset_x; + overlay->position_y = overlay->drag_origin_y + (int)offset_y; + captioneer_apply_placement(overlay); +} + +static void captioneer_drag_end(GtkGestureDrag *gesture, double offset_x, double offset_y, gpointer data) { + (void)gesture; + CaptioneerOverlay *overlay = data; + if (!overlay->dragging) { + return; + } + overlay->position_x = overlay->drag_origin_x + (int)offset_x; + overlay->position_y = overlay->drag_origin_y + (int)offset_y; + overlay->dragging = FALSE; + captioneer_apply_placement(overlay); + captioneerOverlayPositionChanged(overlay->position_x, overlay->position_y); } static void captioneer_apply_x11_hints(CaptioneerOverlay *overlay) { @@ -177,51 +373,11 @@ static void captioneer_apply_x11_hints(CaptioneerOverlay *overlay) { XFlush(display); } -static gboolean captioneer_reposition_x11(gpointer data) { - CaptioneerOverlay *overlay = data; - if (!overlay->x11 || overlay->monitor == NULL) { - return G_SOURCE_REMOVE; - } - GdkSurface *surface = gtk_native_get_surface(GTK_NATIVE(overlay->window)); - if (surface == NULL) { - return G_SOURCE_REMOVE; - } - GdkRectangle geometry; - gdk_monitor_get_geometry(overlay->monitor, &geometry); - int width = gdk_surface_get_width(surface); - int height = gdk_surface_get_height(surface); - if (width <= 1 || height <= 1) { - return G_SOURCE_REMOVE; - } - int bottom_margin = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(overlay->window), "bottom-margin")); - int x = geometry.x + (geometry.width - width) / 2; - int y = geometry.y + geometry.height - height - bottom_margin; - Display *display = GDK_SURFACE_XDISPLAY(surface); - Window xid = GDK_SURFACE_XID(surface); - XSizeHints *size_hints = XAllocSizeHints(); - if (size_hints != NULL) { - size_hints->flags = USPosition | PPosition; - size_hints->x = x; - size_hints->y = y; - XSetWMNormalHints(display, xid, size_hints); - XFree(size_hints); - } - XMoveWindow(display, xid, x, y); - XFlush(display); - return G_SOURCE_REMOVE; -} - -static void captioneer_surface_size_changed(GObject *object, GParamSpec *spec, gpointer data) { - (void)object; - (void)spec; - g_idle_add(captioneer_reposition_x11, data); -} - static void captioneer_update_window_visibility(CaptioneerOverlay *overlay) { gboolean visible = overlay->final_visible || overlay->preview_visible; gtk_widget_set_visible(overlay->bubble, visible); gtk_widget_set_visible(GTK_WIDGET(overlay->window), visible); - if (visible && overlay->x11) { + if (visible) { g_idle_add(captioneer_reposition_x11, overlay); } } @@ -236,9 +392,7 @@ static gboolean captioneer_apply_update(gpointer data) { overlay->preview_visible = update->preview_text[0] != '\0'; if (overlay->preview_visible) { - char *preview = g_strdup_printf("… %s", update->preview_text); - gtk_label_set_text(GTK_LABEL(overlay->preview_label), preview); - g_free(preview); + gtk_label_set_text(GTK_LABEL(overlay->preview_label), update->preview_text); } else { gtk_label_set_text(GTK_LABEL(overlay->preview_label), ""); } @@ -287,6 +441,8 @@ static CaptioneerOverlay *captioneer_overlay_new( int max_width, const char *monitor_selection, int click_through, + int position_x, + int position_y, char **warning_message, char **error_message ) { @@ -315,6 +471,10 @@ static CaptioneerOverlay *captioneer_overlay_new( CaptioneerOverlay *overlay = g_new0(CaptioneerOverlay, 1); overlay->loop = g_main_loop_new(NULL, FALSE); overlay->x11 = x11; + overlay->click_through = click_through != 0; + overlay->bottom_margin = bottom_margin; + overlay->position_x = position_x; + overlay->position_y = position_y; char *monitor_error = NULL; if (wayland && g_strcmp0(monitor_selection, "auto") == 0) { @@ -336,20 +496,17 @@ static CaptioneerOverlay *captioneer_overlay_new( gtk_window_set_resizable(overlay->window, FALSE); gtk_widget_set_focusable(GTK_WIDGET(overlay->window), FALSE); gtk_widget_add_css_class(GTK_WIDGET(overlay->window), "captioneer-overlay"); - g_object_set_data(G_OBJECT(overlay->window), "bottom-margin", GINT_TO_POINTER(bottom_margin)); g_signal_connect(overlay->window, "close-request", G_CALLBACK(captioneer_window_close), overlay); if (wayland && gtk_layer_is_supported()) { + overlay->layer_shell = TRUE; gtk_layer_init_for_window(overlay->window); gtk_layer_set_namespace(overlay->window, "captioneer"); gtk_layer_set_layer(overlay->window, GTK_LAYER_SHELL_LAYER_OVERLAY); - // Stretch across the bottom edge so the compositor pins the surface - // there (single-edge anchoring can leave it floating mid-screen on - // Plasma). The bubble itself stays content-width and centered. + // Pin with LEFT+TOP margins so placement is exact. Stretching LEFT+RIGHT + // made Plasma size the surface oddly and parked captions mid-screen. gtk_layer_set_anchor(overlay->window, GTK_LAYER_SHELL_EDGE_LEFT, TRUE); - gtk_layer_set_anchor(overlay->window, GTK_LAYER_SHELL_EDGE_RIGHT, TRUE); - gtk_layer_set_anchor(overlay->window, GTK_LAYER_SHELL_EDGE_BOTTOM, TRUE); - gtk_layer_set_margin(overlay->window, GTK_LAYER_SHELL_EDGE_BOTTOM, bottom_margin); + gtk_layer_set_anchor(overlay->window, GTK_LAYER_SHELL_EDGE_TOP, TRUE); gtk_layer_set_exclusive_zone(overlay->window, 0); gtk_layer_set_keyboard_mode(overlay->window, GTK_LAYER_SHELL_KEYBOARD_MODE_NONE); if (overlay->monitor != NULL) { @@ -384,7 +541,7 @@ static CaptioneerOverlay *captioneer_overlay_new( overlay->bubble = box; gtk_widget_add_css_class(box, "captioneer-bubble"); gtk_widget_set_halign(box, GTK_ALIGN_CENTER); - gtk_widget_set_valign(box, GTK_ALIGN_END); + gtk_widget_set_valign(box, GTK_ALIGN_START); gtk_widget_set_hexpand(box, FALSE); gtk_widget_set_vexpand(box, FALSE); gtk_widget_set_size_request(box, effective_width, -1); @@ -417,6 +574,14 @@ static CaptioneerOverlay *captioneer_overlay_new( gtk_widget_add_css_class(overlay->preview_label, "captioneer-preview"); gtk_window_set_child(overlay->window, box); + GtkGesture *drag = gtk_gesture_drag_new(); + gtk_gesture_single_set_button(GTK_GESTURE_SINGLE(drag), GDK_BUTTON_PRIMARY); + gtk_event_controller_set_propagation_phase(GTK_EVENT_CONTROLLER(drag), GTK_PHASE_CAPTURE); + g_signal_connect(drag, "drag-begin", G_CALLBACK(captioneer_drag_begin), overlay); + g_signal_connect(drag, "drag-update", G_CALLBACK(captioneer_drag_update), overlay); + g_signal_connect(drag, "drag-end", G_CALLBACK(captioneer_drag_end), overlay); + gtk_widget_add_controller(overlay->bubble, GTK_EVENT_CONTROLLER(drag)); + char *font_family_rule = captioneer_font_family_rule(font_family); char *css = g_strdup_printf( "window.captioneer-overlay { background-color: transparent; box-shadow: none; }" @@ -433,15 +598,20 @@ static CaptioneerOverlay *captioneer_overlay_new( gtk_widget_realize(GTK_WIDGET(overlay->window)); GdkSurface *surface = gtk_native_get_surface(GTK_NATIVE(overlay->window)); - if (click_through) { - captioneer_set_input_passthrough(overlay, warnings); + if (overlay->click_through) { + if (!gdk_display_supports_input_shapes(display)) { + captioneer_append_warning(warnings, "This display backend does not support pointer click-through."); + } else { + captioneer_update_input_region(overlay); + } } if (x11) { captioneer_apply_x11_hints(overlay); - g_signal_connect(surface, "notify::width", G_CALLBACK(captioneer_surface_size_changed), overlay); - g_signal_connect(surface, "notify::height", G_CALLBACK(captioneer_surface_size_changed), overlay); } + g_signal_connect(surface, "notify::width", G_CALLBACK(captioneer_surface_size_changed), overlay); + g_signal_connect(surface, "notify::height", G_CALLBACK(captioneer_surface_size_changed), overlay); gtk_widget_set_visible(GTK_WIDGET(overlay->window), FALSE); + captioneer_apply_placement(overlay); if (warnings->len > 0) { *warning_message = g_string_free(warnings, FALSE); @@ -516,6 +686,28 @@ type Sink struct { closed bool } +var ( + positionSaverMu sync.Mutex + positionSaver func(x, y int) +) + +// SetPositionSaver registers a callback invoked when the user finishes a CTRL-drag. +func SetPositionSaver(save func(x, y int)) { + positionSaverMu.Lock() + positionSaver = save + positionSaverMu.Unlock() +} + +//export captioneerOverlayPositionChanged +func captioneerOverlayPositionChanged(x, y C.int) { + positionSaverMu.Lock() + save := positionSaver + positionSaverMu.Unlock() + if save != nil { + save(int(x), int(y)) + } +} + func New(settings config.OverlaySettings) (*Sink, string, error) { backend := C.CString(string(settings.Backend)) fontFamily := C.CString(settings.FontFamily) @@ -535,6 +727,8 @@ func New(settings config.OverlaySettings) (*Sink, string, error) { C.int(settings.MaxWidth), monitor, boolInt(settings.ClickThrough), + C.int(settings.PositionX), + C.int(settings.PositionY), &warningMessage, &errorMessage, )