apprt/gtk-ng: audio bell

Co-authored-by: Jeffrey C. Ollie <jcollie@dmacc.edu>
pull/8221/head
Mitchell Hashimoto 2025-08-13 09:15:23 -07:00
parent d8a309c734
commit 6de98eda04
No known key found for this signature in database
GPG Key ID: 523D5DC389D273BC
3 changed files with 90 additions and 41 deletions

View File

@ -2043,7 +2043,7 @@ const Action = struct {
pub fn ringBell(target: apprt.Target) void {
switch (target) {
.app => {},
.surface => |v| v.rt_surface.surface.ringBell(),
.surface => |v| v.rt_surface.surface.setBellRinging(true),
}
}

View File

@ -270,24 +270,6 @@ pub const Surface = extern struct {
);
};
/// The bell is rung.
///
/// The surface view handles the audio bell feature but none of the
/// others so it is up to the embedding widget to react to this.
///
/// Bell ringing will also emit the tab.ring-bell and win.ring-bell
/// actions.
pub const bell = struct {
pub const name = "bell";
pub const connect = impl.connect;
const impl = gobject.ext.defineSignal(
name,
Self,
&.{},
void,
);
};
/// Emitted whenever the clipboard has been written.
pub const @"clipboard-write" = struct {
pub const name = "clipboard-write";
@ -541,27 +523,6 @@ pub const Surface = extern struct {
priv.gl_area.queueRender();
}
/// Ring the bell.
pub fn ringBell(self: *Self) void {
self.as(gobject.Object).freezeNotify();
defer self.as(gobject.Object).thawNotify();
// Enable our bell ringing state
self.setBellRinging(true);
// Emit our direct signal for anyone who cares
signals.bell.impl.emit(
self,
null,
.{},
null,
);
// Activate a window action if it exists
_ = self.as(gtk.Widget).activateAction("tab.ring-bell", null);
_ = self.as(gtk.Widget).activateAction("win.ring-bell", null);
}
pub fn toggleFullscreen(self: *Self) void {
signals.@"toggle-fullscreen".impl.emit(
self,
@ -1560,6 +1521,64 @@ pub const Surface = extern struct {
priv.gl_area.as(gtk.Widget).setCursorFromName(name.ptr);
}
fn propBellRinging(
self: *Self,
_: *gobject.ParamSpec,
_: ?*anyopaque,
) callconv(.c) void {
const priv = self.private();
if (!priv.bell_ringing) return;
// Activate actions if they exist
_ = self.as(gtk.Widget).activateAction("tab.ring-bell", null);
_ = self.as(gtk.Widget).activateAction("win.ring-bell", null);
// Do our sound
const config = if (priv.config) |c| c.get() else return;
if (config.@"bell-features".audio) audio: {
const config_path = config.@"bell-audio-path" orelse break :audio;
const path, const required = switch (config_path) {
.optional => |path| .{ path, false },
.required => |path| .{ path, true },
};
const volume = std.math.clamp(
config.@"bell-audio-volume",
0.0,
1.0,
);
assert(std.fs.path.isAbsolute(path));
const media_file = gtk.MediaFile.newForFilename(path);
// If the audio file is marked as required, we'll emit an error if
// there was a problem playing it. Otherwise there will be silence.
if (required) {
_ = gobject.Object.signals.notify.connect(
media_file,
?*anyopaque,
mediaFileError,
null,
.{ .detail = "error" },
);
}
// Watch for the "ended" signal so that we can clean up after
// ourselves.
_ = gobject.Object.signals.notify.connect(
media_file,
?*anyopaque,
mediaFileEnded,
null,
.{ .detail = "ended" },
);
const media_stream = media_file.as(gtk.MediaStream);
media_stream.setVolume(volume);
media_stream.play();
}
}
//---------------------------------------------------------------
// Signal Handlers
@ -2365,6 +2384,35 @@ pub const Surface = extern struct {
right.setVisible(0);
}
fn mediaFileError(
media_file: *gtk.MediaFile,
_: *gobject.ParamSpec,
_: ?*anyopaque,
) callconv(.c) void {
const path = path: {
const file = media_file.getFile() orelse break :path null;
break :path file.getPath();
};
defer if (path) |p| glib.free(p);
const media_stream = media_file.as(gtk.MediaStream);
const err = media_stream.getError() orelse return;
log.warn("error playing bell from {s}: {s} {d} {s}", .{
path orelse "<<unknown>>",
glib.quarkToString(err.f_domain),
err.f_code,
err.f_message orelse "",
});
}
fn mediaFileEnded(
media_file: *gtk.MediaFile,
_: *gobject.ParamSpec,
_: ?*anyopaque,
) callconv(.c) void {
media_file.unref();
}
const C = Common(Self, Private);
pub const as = C.as;
pub const ref = C.ref;
@ -2429,6 +2477,7 @@ pub const Surface = extern struct {
class.bindTemplateCallback("notify_mouse_hover_url", &propMouseHoverUrl);
class.bindTemplateCallback("notify_mouse_hidden", &propMouseHidden);
class.bindTemplateCallback("notify_mouse_shape", &propMouseShape);
class.bindTemplateCallback("notify_bell_ringing", &propBellRinging);
// Properties
gobject.ext.registerProperties(class, &.{
@ -2449,7 +2498,6 @@ pub const Surface = extern struct {
// Signals
signals.@"close-request".impl.register(.{});
signals.bell.impl.register(.{});
signals.@"clipboard-read".impl.register(.{});
signals.@"clipboard-write".impl.register(.{});
signals.init.impl.register(.{});

View File

@ -6,6 +6,7 @@ template $GhosttySurface: Adw.Bin {
"surface",
]
notify::bell-ringing => $notify_bell_ringing();
notify::config => $notify_config();
notify::mouse-hover-url => $notify_mouse_hover_url();
notify::mouse-hidden => $notify_mouse_hidden();