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:
@@ -15,13 +15,18 @@ cd -- "$repo_root"
|
|||||||
# Avoid host/container DMABUF incompatibilities on mismatched GPU stacks. An
|
# Avoid host/container DMABUF incompatibilities on mismatched GPU stacks. An
|
||||||
# explicit GTK renderer choice still takes precedence.
|
# explicit GTK renderer choice still takes precedence.
|
||||||
export GSK_RENDERER="${GSK_RENDERER:-cairo}"
|
export GSK_RENDERER="${GSK_RENDERER:-cairo}"
|
||||||
# gtk4-layer-shell must load before libwayland-client or its Wayland shim is
|
# gtk4-layer-shell must precede libwayland-client in the link line. Do NOT
|
||||||
# skipped and the overlay sits as an unpositioned normal window.
|
# LD_PRELOAD it here: the shim breaks the main GtkApplication window on
|
||||||
layer_shell_lib="$(pkg-config --variable=libdir gtk4-layer-shell-0)/libgtk4-layer-shell.so.0"
|
# Wayland when forced onto the whole process.
|
||||||
if [[ -f "$layer_shell_lib" ]]; then
|
|
||||||
export LD_PRELOAD="${layer_shell_lib}${LD_PRELOAD:+:$LD_PRELOAD}"
|
|
||||||
fi
|
|
||||||
export CGO_LDFLAGS="-Wl,--no-as-needed -lgtk4-layer-shell ${CGO_LDFLAGS:-}"
|
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 go run -tags gtk ./src/cmd/captioneer-desktop "$@"'
|
||||||
|
|
||||||
exec distrobox enter --no-workdir --name "$container_name" -- \
|
exec distrobox enter --no-workdir --name "$container_name" -- \
|
||||||
|
|||||||
@@ -76,6 +76,10 @@ type OverlaySettings struct {
|
|||||||
Monitor string
|
Monitor string
|
||||||
FinalTimeout time.Duration
|
FinalTimeout time.Duration
|
||||||
ClickThrough bool
|
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 {
|
func DefaultOverlaySettings() OverlaySettings {
|
||||||
@@ -89,6 +93,8 @@ func DefaultOverlaySettings() OverlaySettings {
|
|||||||
Monitor: "auto",
|
Monitor: "auto",
|
||||||
FinalTimeout: MinimumOverlayFinalLifetime,
|
FinalTimeout: MinimumOverlayFinalLifetime,
|
||||||
ClickThrough: true,
|
ClickThrough: true,
|
||||||
|
PositionX: -1,
|
||||||
|
PositionY: -1,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -202,6 +208,12 @@ func (s OverlaySettings) Validate() error {
|
|||||||
if s.Monitor == "" {
|
if s.Monitor == "" {
|
||||||
return errors.New("--overlay-monitor cannot be empty")
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -44,6 +44,8 @@ type diskConfig struct {
|
|||||||
Monitor string `json:"monitor"`
|
Monitor string `json:"monitor"`
|
||||||
FinalTimeout string `json:"final_timeout"`
|
FinalTimeout string `json:"final_timeout"`
|
||||||
ClickThrough bool `json:"click_through"`
|
ClickThrough bool `json:"click_through"`
|
||||||
|
PositionX *int `json:"position_x,omitempty"`
|
||||||
|
PositionY *int `json:"position_y,omitempty"`
|
||||||
} `json:"overlay"`
|
} `json:"overlay"`
|
||||||
TranscriptLog struct {
|
TranscriptLog struct {
|
||||||
Enabled bool `json:"enabled"`
|
Enabled bool `json:"enabled"`
|
||||||
@@ -163,6 +165,10 @@ func diskFromConfig(config AppConfig) diskConfig {
|
|||||||
disk.Overlay.Monitor = config.Overlay.Monitor
|
disk.Overlay.Monitor = config.Overlay.Monitor
|
||||||
disk.Overlay.FinalTimeout = config.Overlay.FinalTimeout.String()
|
disk.Overlay.FinalTimeout = config.Overlay.FinalTimeout.String()
|
||||||
disk.Overlay.ClickThrough = config.Overlay.ClickThrough
|
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.Enabled = config.TranscriptLog.Enabled
|
||||||
disk.TranscriptLog.Directory = config.TranscriptLog.Directory
|
disk.TranscriptLog.Directory = config.TranscriptLog.Directory
|
||||||
return disk
|
return disk
|
||||||
@@ -185,6 +191,13 @@ func (disk diskConfig) toConfig() (AppConfig, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return AppConfig{}, err
|
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{
|
return AppConfig{
|
||||||
Version: disk.Version,
|
Version: disk.Version,
|
||||||
GUI: GUISettings{
|
GUI: GUISettings{
|
||||||
@@ -218,6 +231,8 @@ func (disk diskConfig) toConfig() (AppConfig, error) {
|
|||||||
Monitor: disk.Overlay.Monitor,
|
Monitor: disk.Overlay.Monitor,
|
||||||
FinalTimeout: finalTimeout,
|
FinalTimeout: finalTimeout,
|
||||||
ClickThrough: disk.Overlay.ClickThrough,
|
ClickThrough: disk.Overlay.ClickThrough,
|
||||||
|
PositionX: positionX,
|
||||||
|
PositionY: positionY,
|
||||||
},
|
},
|
||||||
TranscriptLog: TranscriptLogSettings{
|
TranscriptLog: TranscriptLogSettings{
|
||||||
Enabled: disk.TranscriptLog.Enabled,
|
Enabled: disk.TranscriptLog.Enabled,
|
||||||
|
|||||||
+16
-1
@@ -143,13 +143,16 @@ func (a *Application) activate() {
|
|||||||
a.buildWindow()
|
a.buildWindow()
|
||||||
a.installActions()
|
a.installActions()
|
||||||
a.subscribeModels()
|
a.subscribeModels()
|
||||||
|
overlay.SetPositionSaver(a.saveOverlayPosition)
|
||||||
if a.launch.Warning != nil {
|
if a.launch.Warning != nil {
|
||||||
a.log.Add(diagnostics.Warning, "configuration", fmt.Sprintf("%v; using built-in defaults", a.launch.Warning))
|
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 {
|
if err := a.configureOutputs(a.config, true); err != nil {
|
||||||
a.log.Add(diagnostics.Error, "outputs", err.Error())
|
a.log.Add(diagnostics.Error, "outputs", err.Error())
|
||||||
}
|
}
|
||||||
a.window.Present()
|
|
||||||
a.applyAlwaysOnTop(a.config.GUI.MainWindowAlwaysOnTop)
|
a.applyAlwaysOnTop(a.config.GUI.MainWindowAlwaysOnTop)
|
||||||
if a.config.GUI.StartCaptions {
|
if a.config.GUI.StartCaptions {
|
||||||
a.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 {
|
func (a *Application) configureOutputs(next config.AppConfig, initial bool) error {
|
||||||
var outputErr error
|
var outputErr error
|
||||||
a.history.SetEnabled(next.Outputs.History)
|
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("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("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("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)
|
return settingsScroller(box)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -272,6 +272,8 @@ func (a *Application) readSettings() (config.AppConfig, error) {
|
|||||||
Monitor: strings.TrimSpace(a.settings.overlayMonitor.Text()),
|
Monitor: strings.TrimSpace(a.settings.overlayMonitor.Text()),
|
||||||
FinalTimeout: finalTimeout,
|
FinalTimeout: finalTimeout,
|
||||||
ClickThrough: a.settings.overlayClickThrough.Active(),
|
ClickThrough: a.settings.overlayClickThrough.Active(),
|
||||||
|
PositionX: a.config.Overlay.PositionX,
|
||||||
|
PositionY: a.config.Overlay.PositionY,
|
||||||
},
|
},
|
||||||
TranscriptLog: config.TranscriptLogSettings{
|
TranscriptLog: config.TranscriptLogSettings{
|
||||||
Enabled: a.settings.transcriptEnabled.Active(),
|
Enabled: a.settings.transcriptEnabled.Active(),
|
||||||
|
|||||||
@@ -33,6 +33,16 @@ typedef struct {
|
|||||||
gboolean final_visible;
|
gboolean final_visible;
|
||||||
gboolean preview_visible;
|
gboolean preview_visible;
|
||||||
gboolean x11;
|
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;
|
} CaptioneerOverlay;
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
@@ -41,6 +51,9 @@ typedef struct {
|
|||||||
char *preview_text;
|
char *preview_text;
|
||||||
} CaptioneerUpdate;
|
} 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) {
|
static void captioneer_append_warning(GString *warnings, const char *message) {
|
||||||
if (warnings->len > 0) {
|
if (warnings->len > 0) {
|
||||||
g_string_append(warnings, " ");
|
g_string_append(warnings, " ");
|
||||||
@@ -126,15 +139,198 @@ static GdkMonitor *captioneer_select_monitor(
|
|||||||
return monitor;
|
return monitor;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void captioneer_set_input_passthrough(CaptioneerOverlay *overlay, GString *warnings) {
|
static GdkMonitor *captioneer_overlay_monitor(CaptioneerOverlay *overlay) {
|
||||||
if (!gdk_display_supports_input_shapes(gtk_widget_get_display(GTK_WIDGET(overlay->window)))) {
|
if (overlay->monitor != NULL) {
|
||||||
captioneer_append_warning(warnings, "This display backend does not support pointer click-through.");
|
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;
|
return;
|
||||||
}
|
}
|
||||||
GdkSurface *surface = gtk_native_get_surface(GTK_NATIVE(overlay->window));
|
GdkSurface *surface = gtk_native_get_surface(GTK_NATIVE(overlay->window));
|
||||||
cairo_region_t *empty = cairo_region_create();
|
if (surface == NULL) {
|
||||||
gdk_surface_set_input_region(surface, empty);
|
gtk_gesture_set_state(GTK_GESTURE(gesture), GTK_EVENT_SEQUENCE_DENIED);
|
||||||
cairo_region_destroy(empty);
|
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) {
|
static void captioneer_apply_x11_hints(CaptioneerOverlay *overlay) {
|
||||||
@@ -177,51 +373,11 @@ static void captioneer_apply_x11_hints(CaptioneerOverlay *overlay) {
|
|||||||
XFlush(display);
|
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) {
|
static void captioneer_update_window_visibility(CaptioneerOverlay *overlay) {
|
||||||
gboolean visible = overlay->final_visible || overlay->preview_visible;
|
gboolean visible = overlay->final_visible || overlay->preview_visible;
|
||||||
gtk_widget_set_visible(overlay->bubble, visible);
|
gtk_widget_set_visible(overlay->bubble, visible);
|
||||||
gtk_widget_set_visible(GTK_WIDGET(overlay->window), visible);
|
gtk_widget_set_visible(GTK_WIDGET(overlay->window), visible);
|
||||||
if (visible && overlay->x11) {
|
if (visible) {
|
||||||
g_idle_add(captioneer_reposition_x11, overlay);
|
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';
|
overlay->preview_visible = update->preview_text[0] != '\0';
|
||||||
if (overlay->preview_visible) {
|
if (overlay->preview_visible) {
|
||||||
char *preview = g_strdup_printf("… %s", update->preview_text);
|
gtk_label_set_text(GTK_LABEL(overlay->preview_label), update->preview_text);
|
||||||
gtk_label_set_text(GTK_LABEL(overlay->preview_label), preview);
|
|
||||||
g_free(preview);
|
|
||||||
} else {
|
} else {
|
||||||
gtk_label_set_text(GTK_LABEL(overlay->preview_label), "");
|
gtk_label_set_text(GTK_LABEL(overlay->preview_label), "");
|
||||||
}
|
}
|
||||||
@@ -287,6 +441,8 @@ static CaptioneerOverlay *captioneer_overlay_new(
|
|||||||
int max_width,
|
int max_width,
|
||||||
const char *monitor_selection,
|
const char *monitor_selection,
|
||||||
int click_through,
|
int click_through,
|
||||||
|
int position_x,
|
||||||
|
int position_y,
|
||||||
char **warning_message,
|
char **warning_message,
|
||||||
char **error_message
|
char **error_message
|
||||||
) {
|
) {
|
||||||
@@ -315,6 +471,10 @@ static CaptioneerOverlay *captioneer_overlay_new(
|
|||||||
CaptioneerOverlay *overlay = g_new0(CaptioneerOverlay, 1);
|
CaptioneerOverlay *overlay = g_new0(CaptioneerOverlay, 1);
|
||||||
overlay->loop = g_main_loop_new(NULL, FALSE);
|
overlay->loop = g_main_loop_new(NULL, FALSE);
|
||||||
overlay->x11 = x11;
|
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;
|
char *monitor_error = NULL;
|
||||||
if (wayland && g_strcmp0(monitor_selection, "auto") == 0) {
|
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_window_set_resizable(overlay->window, FALSE);
|
||||||
gtk_widget_set_focusable(GTK_WIDGET(overlay->window), FALSE);
|
gtk_widget_set_focusable(GTK_WIDGET(overlay->window), FALSE);
|
||||||
gtk_widget_add_css_class(GTK_WIDGET(overlay->window), "captioneer-overlay");
|
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);
|
g_signal_connect(overlay->window, "close-request", G_CALLBACK(captioneer_window_close), overlay);
|
||||||
|
|
||||||
if (wayland && gtk_layer_is_supported()) {
|
if (wayland && gtk_layer_is_supported()) {
|
||||||
|
overlay->layer_shell = TRUE;
|
||||||
gtk_layer_init_for_window(overlay->window);
|
gtk_layer_init_for_window(overlay->window);
|
||||||
gtk_layer_set_namespace(overlay->window, "captioneer");
|
gtk_layer_set_namespace(overlay->window, "captioneer");
|
||||||
gtk_layer_set_layer(overlay->window, GTK_LAYER_SHELL_LAYER_OVERLAY);
|
gtk_layer_set_layer(overlay->window, GTK_LAYER_SHELL_LAYER_OVERLAY);
|
||||||
// Stretch across the bottom edge so the compositor pins the surface
|
// Pin with LEFT+TOP margins so placement is exact. Stretching LEFT+RIGHT
|
||||||
// there (single-edge anchoring can leave it floating mid-screen on
|
// made Plasma size the surface oddly and parked captions mid-screen.
|
||||||
// Plasma). The bubble itself stays content-width and centered.
|
|
||||||
gtk_layer_set_anchor(overlay->window, GTK_LAYER_SHELL_EDGE_LEFT, TRUE);
|
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_TOP, 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_exclusive_zone(overlay->window, 0);
|
gtk_layer_set_exclusive_zone(overlay->window, 0);
|
||||||
gtk_layer_set_keyboard_mode(overlay->window, GTK_LAYER_SHELL_KEYBOARD_MODE_NONE);
|
gtk_layer_set_keyboard_mode(overlay->window, GTK_LAYER_SHELL_KEYBOARD_MODE_NONE);
|
||||||
if (overlay->monitor != NULL) {
|
if (overlay->monitor != NULL) {
|
||||||
@@ -384,7 +541,7 @@ static CaptioneerOverlay *captioneer_overlay_new(
|
|||||||
overlay->bubble = box;
|
overlay->bubble = box;
|
||||||
gtk_widget_add_css_class(box, "captioneer-bubble");
|
gtk_widget_add_css_class(box, "captioneer-bubble");
|
||||||
gtk_widget_set_halign(box, GTK_ALIGN_CENTER);
|
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_hexpand(box, FALSE);
|
||||||
gtk_widget_set_vexpand(box, FALSE);
|
gtk_widget_set_vexpand(box, FALSE);
|
||||||
gtk_widget_set_size_request(box, effective_width, -1);
|
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_widget_add_css_class(overlay->preview_label, "captioneer-preview");
|
||||||
gtk_window_set_child(overlay->window, box);
|
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 *font_family_rule = captioneer_font_family_rule(font_family);
|
||||||
char *css = g_strdup_printf(
|
char *css = g_strdup_printf(
|
||||||
"window.captioneer-overlay { background-color: transparent; box-shadow: none; }"
|
"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));
|
gtk_widget_realize(GTK_WIDGET(overlay->window));
|
||||||
GdkSurface *surface = gtk_native_get_surface(GTK_NATIVE(overlay->window));
|
GdkSurface *surface = gtk_native_get_surface(GTK_NATIVE(overlay->window));
|
||||||
if (click_through) {
|
if (overlay->click_through) {
|
||||||
captioneer_set_input_passthrough(overlay, warnings);
|
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) {
|
if (x11) {
|
||||||
captioneer_apply_x11_hints(overlay);
|
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);
|
gtk_widget_set_visible(GTK_WIDGET(overlay->window), FALSE);
|
||||||
|
captioneer_apply_placement(overlay);
|
||||||
|
|
||||||
if (warnings->len > 0) {
|
if (warnings->len > 0) {
|
||||||
*warning_message = g_string_free(warnings, FALSE);
|
*warning_message = g_string_free(warnings, FALSE);
|
||||||
@@ -516,6 +686,28 @@ type Sink struct {
|
|||||||
closed bool
|
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) {
|
func New(settings config.OverlaySettings) (*Sink, string, error) {
|
||||||
backend := C.CString(string(settings.Backend))
|
backend := C.CString(string(settings.Backend))
|
||||||
fontFamily := C.CString(settings.FontFamily)
|
fontFamily := C.CString(settings.FontFamily)
|
||||||
@@ -535,6 +727,8 @@ func New(settings config.OverlaySettings) (*Sink, string, error) {
|
|||||||
C.int(settings.MaxWidth),
|
C.int(settings.MaxWidth),
|
||||||
monitor,
|
monitor,
|
||||||
boolInt(settings.ClickThrough),
|
boolInt(settings.ClickThrough),
|
||||||
|
C.int(settings.PositionX),
|
||||||
|
C.int(settings.PositionY),
|
||||||
&warningMessage,
|
&warningMessage,
|
||||||
&errorMessage,
|
&errorMessage,
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user