feat(overlay): add position customization for overlay placement
- Introduce PositionX and PositionY fields in OverlaySettings to allow manual positioning of the overlay. - Update validation to ensure PositionX and PositionY are not less than -1. - Modify diskConfig to include new position fields for JSON serialization. - Implement functionality to save overlay position during drag events in the GUI. - Enhance overlay placement logic to respect user-defined positions while ensuring proper display behavior.
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
+16
-1
@@ -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)
|
||||
|
||||
+3
-1
@@ -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(),
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user