1048 lines
34 KiB
Zig
1048 lines
34 KiB
Zig
const std = @import("std");
|
|
const build_config = @import("../../../build_config.zig");
|
|
const assert = std.debug.assert;
|
|
const adw = @import("adw");
|
|
const gio = @import("gio");
|
|
const glib = @import("glib");
|
|
const gobject = @import("gobject");
|
|
const gtk = @import("gtk");
|
|
|
|
const i18n = @import("../../../os/main.zig").i18n;
|
|
const apprt = @import("../../../apprt.zig");
|
|
const input = @import("../../../input.zig");
|
|
const CoreSurface = @import("../../../Surface.zig");
|
|
const gtk_version = @import("../gtk_version.zig");
|
|
const adw_version = @import("../adw_version.zig");
|
|
const gresource = @import("../build/gresource.zig");
|
|
const Common = @import("../class.zig").Common;
|
|
const Config = @import("config.zig").Config;
|
|
const Application = @import("application.zig").Application;
|
|
const CloseConfirmationDialog = @import("close_confirmation_dialog.zig").CloseConfirmationDialog;
|
|
const Surface = @import("surface.zig").Surface;
|
|
const Tab = @import("tab.zig").Tab;
|
|
const DebugWarning = @import("debug_warning.zig").DebugWarning;
|
|
|
|
const log = std.log.scoped(.gtk_ghostty_window);
|
|
|
|
pub const Window = extern struct {
|
|
const Self = @This();
|
|
parent_instance: Parent,
|
|
pub const Parent = adw.ApplicationWindow;
|
|
pub const getGObjectType = gobject.ext.defineClass(Self, .{
|
|
.name = "GhosttyWindow",
|
|
.instanceInit = &init,
|
|
.classInit = &Class.init,
|
|
.parent_class = &Class.parent,
|
|
.private = .{ .Type = Private, .offset = &Private.offset },
|
|
});
|
|
|
|
pub const properties = struct {
|
|
/// The active surface is the focus that should be receiving all
|
|
/// surface-targeted actions. This is usually the focused surface,
|
|
/// but may also not be focused if the user has selected a non-surface
|
|
/// widget.
|
|
pub const @"active-surface" = struct {
|
|
pub const name = "active-surface";
|
|
const impl = gobject.ext.defineProperty(
|
|
name,
|
|
Self,
|
|
?*Surface,
|
|
.{
|
|
.nick = "Active Surface",
|
|
.blurb = "The currently active surface.",
|
|
.accessor = gobject.ext.typedAccessor(
|
|
Self,
|
|
?*Surface,
|
|
.{
|
|
.getter = Self.getActiveSurface,
|
|
},
|
|
),
|
|
},
|
|
);
|
|
};
|
|
|
|
pub const config = struct {
|
|
pub const name = "config";
|
|
const impl = gobject.ext.defineProperty(
|
|
name,
|
|
Self,
|
|
?*Config,
|
|
.{
|
|
.nick = "Config",
|
|
.blurb = "The configuration that this surface is using.",
|
|
.accessor = C.privateObjFieldAccessor("config"),
|
|
},
|
|
);
|
|
};
|
|
|
|
pub const debug = struct {
|
|
pub const name = "debug";
|
|
const impl = gobject.ext.defineProperty(
|
|
name,
|
|
Self,
|
|
bool,
|
|
.{
|
|
.nick = "Debug",
|
|
.blurb = "True if runtime safety checks are enabled.",
|
|
.default = build_config.is_debug,
|
|
.accessor = gobject.ext.typedAccessor(Self, bool, .{
|
|
.getter = struct {
|
|
pub fn getter(_: *Self) bool {
|
|
return build_config.is_debug;
|
|
}
|
|
}.getter,
|
|
}),
|
|
},
|
|
);
|
|
};
|
|
|
|
pub const @"headerbar-visible" = struct {
|
|
pub const name = "headerbar-visible";
|
|
const impl = gobject.ext.defineProperty(
|
|
name,
|
|
Self,
|
|
bool,
|
|
.{
|
|
.nick = "Headerbar Visible",
|
|
.blurb = "True if the headerbar is visible.",
|
|
.default = true,
|
|
.accessor = gobject.ext.typedAccessor(Self, bool, .{
|
|
.getter = Self.getHeaderbarVisible,
|
|
}),
|
|
},
|
|
);
|
|
};
|
|
|
|
pub const @"background-opaque" = struct {
|
|
pub const name = "background-opaque";
|
|
const impl = gobject.ext.defineProperty(
|
|
name,
|
|
Self,
|
|
bool,
|
|
.{
|
|
.nick = "Background Opaque",
|
|
.blurb = "True if the background should be opaque.",
|
|
.default = true,
|
|
.accessor = gobject.ext.typedAccessor(Self, bool, .{
|
|
.getter = Self.getBackgroundOpaque,
|
|
}),
|
|
},
|
|
);
|
|
};
|
|
|
|
pub const @"tabs-autohide" = struct {
|
|
pub const name = "tabs-autohide";
|
|
const impl = gobject.ext.defineProperty(
|
|
name,
|
|
Self,
|
|
bool,
|
|
.{
|
|
.nick = "Autohide Tab Bar",
|
|
.blurb = "If true, tab bar should autohide.",
|
|
.default = true,
|
|
.accessor = gobject.ext.typedAccessor(Self, bool, .{
|
|
.getter = Self.getTabsAutohide,
|
|
}),
|
|
},
|
|
);
|
|
};
|
|
|
|
pub const @"tabs-wide" = struct {
|
|
pub const name = "tabs-wide";
|
|
const impl = gobject.ext.defineProperty(
|
|
name,
|
|
Self,
|
|
bool,
|
|
.{
|
|
.nick = "Wide Tabs",
|
|
.blurb = "If true, tabs will be in the wide expanded style.",
|
|
.default = true,
|
|
.accessor = gobject.ext.typedAccessor(Self, bool, .{
|
|
.getter = Self.getTabsWide,
|
|
}),
|
|
},
|
|
);
|
|
};
|
|
|
|
pub const @"tabs-visible" = struct {
|
|
pub const name = "tabs-visible";
|
|
const impl = gobject.ext.defineProperty(
|
|
name,
|
|
Self,
|
|
bool,
|
|
.{
|
|
.nick = "Tab Bar Visibility",
|
|
.blurb = "If true, tab bar should be visible.",
|
|
.default = true,
|
|
.accessor = gobject.ext.typedAccessor(Self, bool, .{
|
|
.getter = Self.getTabsVisible,
|
|
}),
|
|
},
|
|
);
|
|
};
|
|
|
|
pub const @"toolbar-style" = struct {
|
|
pub const name = "toolbar-style";
|
|
const impl = gobject.ext.defineProperty(
|
|
name,
|
|
Self,
|
|
adw.ToolbarStyle,
|
|
.{
|
|
.nick = "Toolbar Style",
|
|
.blurb = "The style for the toolbar top/bottom bars.",
|
|
.default = .raised,
|
|
.accessor = gobject.ext.typedAccessor(
|
|
Self,
|
|
adw.ToolbarStyle,
|
|
.{
|
|
.getter = Self.getToolbarStyle,
|
|
},
|
|
),
|
|
},
|
|
);
|
|
};
|
|
};
|
|
|
|
const Private = struct {
|
|
/// Binding group for our active tab.
|
|
tab_bindings: *gobject.BindingGroup,
|
|
|
|
/// The configuration that this surface is using.
|
|
config: ?*Config = null,
|
|
|
|
// Template bindings
|
|
tab_bar: *adw.TabBar,
|
|
tab_view: *adw.TabView,
|
|
toolbar: *adw.ToolbarView,
|
|
toast_overlay: *adw.ToastOverlay,
|
|
|
|
pub var offset: c_int = 0;
|
|
};
|
|
|
|
pub fn new(app: *Application, parent_: ?*CoreSurface) *Self {
|
|
const self = gobject.ext.newInstance(Self, .{
|
|
.application = app,
|
|
});
|
|
|
|
// Create our initial tab. This will trigger the selected-page
|
|
// signal handler which will setup the remainder of the bindings
|
|
// for this to all work.
|
|
const priv = self.private();
|
|
const tab = gobject.ext.newInstance(Tab, .{
|
|
.config = priv.config,
|
|
});
|
|
if (parent_) |p| tab.setParent(p);
|
|
_ = priv.tab_view.append(tab.as(gtk.Widget));
|
|
|
|
return self;
|
|
}
|
|
|
|
fn init(self: *Self, _: *Class) callconv(.C) void {
|
|
gtk.Widget.initTemplate(self.as(gtk.Widget));
|
|
|
|
// If our configuration is null then we get the configuration
|
|
// from the application.
|
|
const priv = self.private();
|
|
if (priv.config == null) {
|
|
const app = Application.default();
|
|
priv.config = app.getConfig();
|
|
}
|
|
|
|
// Add our dev CSS class if we're in debug mode.
|
|
if (comptime build_config.is_debug) {
|
|
self.as(gtk.Widget).addCssClass("devel");
|
|
}
|
|
|
|
// Setup our tab binding group. This ensures certain properties
|
|
// are only synced from the currently active tab.
|
|
priv.tab_bindings = gobject.BindingGroup.new();
|
|
priv.tab_bindings.bind("title", self.as(gobject.Object), "title", .{});
|
|
|
|
// Set our window icon. We can't set this in the blueprint file
|
|
// because its dependent on the build config.
|
|
self.as(gtk.Window).setIconName(build_config.bundle_id);
|
|
|
|
// Initialize our actions
|
|
self.initActionMap();
|
|
|
|
// We always sync our appearance at the end because loading our
|
|
// config and such can affect our bindings which ar setup initially
|
|
// in initTemplate.
|
|
self.syncAppearance();
|
|
|
|
// We need to do this so that the title initializes properly,
|
|
// I think because its a dynamic getter.
|
|
self.as(gobject.Object).notifyByPspec(properties.@"active-surface".impl.param_spec);
|
|
}
|
|
|
|
/// Setup our action map.
|
|
fn initActionMap(self: *Self) void {
|
|
const actions = .{
|
|
.{ "about", actionAbout, null },
|
|
.{ "close", actionClose, null },
|
|
.{ "new-window", actionNewWindow, null },
|
|
.{ "copy", actionCopy, null },
|
|
.{ "paste", actionPaste, null },
|
|
.{ "reset", actionReset, null },
|
|
.{ "clear", actionClear, null },
|
|
};
|
|
|
|
const action_map = self.as(gio.ActionMap);
|
|
inline for (actions) |entry| {
|
|
const action = gio.SimpleAction.new(
|
|
entry[0],
|
|
entry[2],
|
|
);
|
|
defer action.unref();
|
|
_ = gio.SimpleAction.signals.activate.connect(
|
|
action,
|
|
*Self,
|
|
entry[1],
|
|
self,
|
|
.{},
|
|
);
|
|
action_map.addAction(action.as(gio.Action));
|
|
}
|
|
}
|
|
|
|
/// Updates various appearance properties. This should always be safe
|
|
/// to call multiple times. This should be called whenever a change
|
|
/// happens that might affect how the window appears (config change,
|
|
/// fullscreen, etc.).
|
|
fn syncAppearance(self: *Self) void {
|
|
// TODO: CSD/SSD
|
|
|
|
// Trigger all our dynamic properties that depend on the config.
|
|
inline for (&.{
|
|
"background-opaque",
|
|
"headerbar-visible",
|
|
"tabs-autohide",
|
|
"tabs-visible",
|
|
"tabs-wide",
|
|
"toolbar-style",
|
|
}) |key| {
|
|
self.as(gobject.Object).notifyByPspec(
|
|
@field(properties, key).impl.param_spec,
|
|
);
|
|
}
|
|
|
|
// Remainder uses the config
|
|
const priv = self.private();
|
|
const config = if (priv.config) |v| v.get() else return;
|
|
|
|
// Move the tab bar to the proper location.
|
|
priv.toolbar.remove(priv.tab_bar.as(gtk.Widget));
|
|
switch (config.@"gtk-tabs-location") {
|
|
.top => priv.toolbar.addTopBar(priv.tab_bar.as(gtk.Widget)),
|
|
.bottom => priv.toolbar.addBottomBar(priv.tab_bar.as(gtk.Widget)),
|
|
}
|
|
}
|
|
|
|
fn toggleCssClass(self: *Self, class: [:0]const u8, value: bool) void {
|
|
const widget = self.as(gtk.Widget);
|
|
if (value)
|
|
widget.addCssClass(class.ptr)
|
|
else
|
|
widget.removeCssClass(class.ptr);
|
|
}
|
|
|
|
/// Perform a binding action on the window's active surface.
|
|
fn performBindingAction(
|
|
self: *Self,
|
|
action: input.Binding.Action,
|
|
) void {
|
|
const surface = self.getActiveSurface() orelse return;
|
|
const core_surface = surface.core() orelse return;
|
|
_ = core_surface.performBindingAction(action) catch |err| {
|
|
log.warn("error performing binding action error={}", .{err});
|
|
return;
|
|
};
|
|
}
|
|
|
|
/// Queue a simple text-based toast. All text-based toasts share the
|
|
/// same timeout for consistency.
|
|
///
|
|
// This is not `pub` because we should be using signals emitted by
|
|
// other widgets to trigger our toasts. Other objects should not
|
|
// trigger toasts directly.
|
|
fn addToast(self: *Self, title: [*:0]const u8) void {
|
|
const toast = adw.Toast.new(title);
|
|
toast.setTimeout(3);
|
|
self.private().toast_overlay.addToast(toast);
|
|
}
|
|
|
|
//---------------------------------------------------------------
|
|
// Properties
|
|
|
|
/// Get the currently active surface. See the "active-surface" property.
|
|
/// This does not ref the value.
|
|
fn getActiveSurface(self: *Self) ?*Surface {
|
|
const tab = self.getSelectedTab() orelse return null;
|
|
return tab.getActiveSurface();
|
|
}
|
|
|
|
/// Get the currently selected tab as a Tab object.
|
|
fn getSelectedTab(self: *Self) ?*Tab {
|
|
const priv = self.private();
|
|
const page = priv.tab_view.getSelectedPage() orelse return null;
|
|
const child = page.getChild();
|
|
assert(gobject.ext.isA(child, Tab));
|
|
return gobject.ext.cast(Tab, child);
|
|
}
|
|
|
|
fn getHeaderbarVisible(self: *Self) bool {
|
|
// TODO: CSD/SSD
|
|
// TODO: QuickTerminal
|
|
|
|
// If we're fullscreen we never show the header bar.
|
|
if (self.as(gtk.Window).isFullscreen() != 0) return false;
|
|
|
|
// The remainder needs a config
|
|
const config_obj = self.private().config orelse return true;
|
|
const config = config_obj.get();
|
|
|
|
// *Conditionally* disable the header bar when maximized,
|
|
// and gtk-titlebar-hide-when-maximized is set
|
|
if (self.as(gtk.Window).isMaximized() != 0 and
|
|
config.@"gtk-titlebar-hide-when-maximized")
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return config.@"gtk-titlebar";
|
|
}
|
|
|
|
fn getBackgroundOpaque(self: *Self) bool {
|
|
const priv = self.private();
|
|
const config = (priv.config orelse return true).get();
|
|
return config.@"background-opacity" >= 1.0;
|
|
}
|
|
|
|
fn getTabsAutohide(self: *Self) bool {
|
|
const priv = self.private();
|
|
const config = if (priv.config) |v| v.get() else return true;
|
|
return switch (config.@"window-show-tab-bar") {
|
|
// Auto we always autohide... obviously.
|
|
.auto => true,
|
|
|
|
// Always we never autohide because we always show the tab bar.
|
|
.always => false,
|
|
|
|
// Never we autohide because it doesn't actually matter,
|
|
// since getTabsVisible will return false.
|
|
.never => true,
|
|
};
|
|
}
|
|
|
|
fn getTabsVisible(self: *Self) bool {
|
|
const priv = self.private();
|
|
const config = if (priv.config) |v| v.get() else return true;
|
|
return switch (config.@"window-show-tab-bar") {
|
|
.always, .auto => true,
|
|
.never => false,
|
|
};
|
|
}
|
|
|
|
fn getTabsWide(self: *Self) bool {
|
|
const priv = self.private();
|
|
const config = if (priv.config) |v| v.get() else return true;
|
|
return config.@"gtk-wide-tabs";
|
|
}
|
|
|
|
fn getToolbarStyle(self: *Self) adw.ToolbarStyle {
|
|
const priv = self.private();
|
|
const config = if (priv.config) |v| v.get() else return .raised;
|
|
return switch (config.@"gtk-toolbar-style") {
|
|
.flat => .flat,
|
|
.raised => .raised,
|
|
.@"raised-border" => .raised_border,
|
|
};
|
|
}
|
|
|
|
fn propConfig(
|
|
_: *adw.ApplicationWindow,
|
|
_: *gobject.ParamSpec,
|
|
self: *Self,
|
|
) callconv(.c) void {
|
|
self.addToast(i18n._("Reloaded the configuration"));
|
|
self.syncAppearance();
|
|
}
|
|
|
|
fn propFullscreened(
|
|
_: *adw.ApplicationWindow,
|
|
_: *gobject.ParamSpec,
|
|
self: *Self,
|
|
) callconv(.c) void {
|
|
self.syncAppearance();
|
|
}
|
|
|
|
fn propMaximized(
|
|
_: *adw.ApplicationWindow,
|
|
_: *gobject.ParamSpec,
|
|
self: *Self,
|
|
) callconv(.c) void {
|
|
self.syncAppearance();
|
|
}
|
|
|
|
fn propMenuActive(
|
|
button: *gtk.MenuButton,
|
|
_: *gobject.ParamSpec,
|
|
self: *Self,
|
|
) callconv(.c) void {
|
|
// Debian 12 is stuck on GTK 4.8
|
|
if (!gtk_version.atLeast(4, 10, 0)) return;
|
|
|
|
// We only care if we're activating. If we're activating then
|
|
// we need to check the validity of our menu items.
|
|
const active = button.getActive() != 0;
|
|
if (!active) return;
|
|
|
|
const has_selection = selection: {
|
|
const surface = self.getActiveSurface() orelse
|
|
break :selection false;
|
|
const core_surface = surface.core() orelse
|
|
break :selection false;
|
|
break :selection core_surface.hasSelection();
|
|
};
|
|
|
|
const action_map: *gio.ActionMap = gobject.ext.cast(
|
|
gio.ActionMap,
|
|
self,
|
|
) orelse return;
|
|
const action: *gio.SimpleAction = gobject.ext.cast(
|
|
gio.SimpleAction,
|
|
action_map.lookupAction("copy") orelse return,
|
|
) orelse return;
|
|
action.setEnabled(@intFromBool(has_selection));
|
|
}
|
|
|
|
/// Add or remove "background" CSS class depending on if the background
|
|
/// should be opaque.
|
|
fn propBackgroundOpaque(
|
|
_: *adw.ApplicationWindow,
|
|
_: *gobject.ParamSpec,
|
|
self: *Self,
|
|
) callconv(.c) void {
|
|
self.toggleCssClass("background", self.getBackgroundOpaque());
|
|
}
|
|
|
|
//---------------------------------------------------------------
|
|
// Virtual methods
|
|
|
|
fn dispose(self: *Self) callconv(.C) void {
|
|
const priv = self.private();
|
|
if (priv.config) |v| {
|
|
v.unref();
|
|
priv.config = null;
|
|
}
|
|
priv.tab_bindings.setSource(null);
|
|
|
|
gtk.Widget.disposeTemplate(
|
|
self.as(gtk.Widget),
|
|
getGObjectType(),
|
|
);
|
|
|
|
gobject.Object.virtual_methods.dispose.call(
|
|
Class.parent,
|
|
self.as(Parent),
|
|
);
|
|
}
|
|
|
|
fn finalize(self: *Self) callconv(.C) void {
|
|
const priv = self.private();
|
|
priv.tab_bindings.unref();
|
|
|
|
gobject.Object.virtual_methods.finalize.call(
|
|
Class.parent,
|
|
self.as(Parent),
|
|
);
|
|
}
|
|
|
|
//---------------------------------------------------------------
|
|
// Signal handlers
|
|
|
|
fn windowCloseRequest(
|
|
_: *gtk.Window,
|
|
self: *Self,
|
|
) callconv(.c) c_int {
|
|
// If our surface needs confirmation then we show confirmation.
|
|
// This will have to be expanded to a list when we have tabs
|
|
// or splits.
|
|
confirm: {
|
|
const surface = self.getActiveSurface() orelse break :confirm;
|
|
const core_surface = surface.core() orelse break :confirm;
|
|
if (!core_surface.needsConfirmQuit()) break :confirm;
|
|
|
|
// Show a confirmation dialog
|
|
const dialog: *CloseConfirmationDialog = .new(.app);
|
|
_ = CloseConfirmationDialog.signals.@"close-request".connect(
|
|
dialog,
|
|
*Self,
|
|
closeConfirmationClose,
|
|
self,
|
|
.{},
|
|
);
|
|
|
|
// Show it
|
|
dialog.present(self.as(gtk.Widget));
|
|
return @intFromBool(true);
|
|
}
|
|
|
|
self.as(gtk.Window).destroy();
|
|
return @intFromBool(false);
|
|
}
|
|
|
|
fn closeConfirmationClose(
|
|
_: *CloseConfirmationDialog,
|
|
self: *Self,
|
|
) callconv(.c) void {
|
|
self.as(gtk.Window).destroy();
|
|
}
|
|
|
|
fn closeConfirmationCloseTab(
|
|
_: *CloseConfirmationDialog,
|
|
page: *adw.TabPage,
|
|
) callconv(.c) void {
|
|
const tab_view_widget = page
|
|
.getChild()
|
|
.as(gtk.Widget)
|
|
.getAncestor(gobject.ext.typeFor(adw.TabView)) orelse {
|
|
log.warn("close confirmation called for non-existent page", .{});
|
|
return;
|
|
};
|
|
const tab_view = gobject.ext.cast(
|
|
adw.TabView,
|
|
tab_view_widget,
|
|
).?;
|
|
tab_view.closePageFinish(page, @intFromBool(true));
|
|
}
|
|
|
|
fn closeConfirmationCancelTab(
|
|
_: *CloseConfirmationDialog,
|
|
page: *adw.TabPage,
|
|
) callconv(.c) void {
|
|
const tab_view_widget = page
|
|
.getChild()
|
|
.as(gtk.Widget)
|
|
.getAncestor(gobject.ext.typeFor(adw.TabView)) orelse {
|
|
log.warn("close confirmation called for non-existent page", .{});
|
|
return;
|
|
};
|
|
const tab_view = gobject.ext.cast(
|
|
adw.TabView,
|
|
tab_view_widget,
|
|
).?;
|
|
tab_view.closePageFinish(page, @intFromBool(false));
|
|
}
|
|
|
|
fn tabViewClosePage(
|
|
_: *adw.TabView,
|
|
page: *adw.TabPage,
|
|
self: *Self,
|
|
) callconv(.c) c_int {
|
|
const priv = self.private();
|
|
const child = page.getChild();
|
|
const tab = gobject.ext.cast(Tab, child) orelse
|
|
return @intFromBool(false);
|
|
|
|
// If the tab says it doesn't need confirmation then we go ahead
|
|
// and close immediately.
|
|
if (!tab.getNeedsConfirmQuit()) {
|
|
priv.tab_view.closePageFinish(page, @intFromBool(true));
|
|
return @intFromBool(true);
|
|
}
|
|
|
|
// Show a confirmation dialog
|
|
const dialog: *CloseConfirmationDialog = .new(.tab);
|
|
_ = CloseConfirmationDialog.signals.@"close-request".connect(
|
|
dialog,
|
|
*adw.TabPage,
|
|
closeConfirmationCloseTab,
|
|
page,
|
|
.{},
|
|
);
|
|
_ = CloseConfirmationDialog.signals.cancel.connect(
|
|
dialog,
|
|
*adw.TabPage,
|
|
closeConfirmationCancelTab,
|
|
page,
|
|
.{},
|
|
);
|
|
|
|
// Show it
|
|
dialog.present(child);
|
|
return @intFromBool(true);
|
|
}
|
|
|
|
fn tabViewSelectedPage(
|
|
_: *adw.TabView,
|
|
_: *gobject.ParamSpec,
|
|
self: *Self,
|
|
) callconv(.c) void {
|
|
const priv = self.private();
|
|
|
|
// Always reset our binding source in case we have no pages.
|
|
priv.tab_bindings.setSource(null);
|
|
|
|
// Get our current page which MUST be a Tab object.
|
|
const page = priv.tab_view.getSelectedPage() orelse return;
|
|
const child = page.getChild();
|
|
assert(gobject.ext.isA(child, Tab));
|
|
|
|
// Setup our binding group. This ensures things like the title
|
|
// are synced from the active tab.
|
|
priv.tab_bindings.setSource(child.as(gobject.Object));
|
|
}
|
|
|
|
fn tabViewPageAttached(
|
|
_: *adw.TabView,
|
|
page: *adw.TabPage,
|
|
_: c_int,
|
|
self: *Self,
|
|
) callconv(.c) void {
|
|
// Get the attached page which must be a Tab object.
|
|
const child = page.getChild();
|
|
const tab = gobject.ext.cast(Tab, child) orelse return;
|
|
|
|
// Attach listeners for the tab.
|
|
_ = Tab.signals.@"close-request".connect(
|
|
tab,
|
|
*Self,
|
|
tabCloseRequest,
|
|
self,
|
|
.{},
|
|
);
|
|
|
|
// Attach listeners for the surface.
|
|
//
|
|
// Interesting behavior here that was previously undocumented but
|
|
// I'm going to make it explicit here: we accept all the signals here
|
|
// (like toggle-fullscreen) regardless of whether the surface or tab
|
|
// is focused. At the time of writing this we have no API that could
|
|
// really trigger these that way but its theoretically possible.
|
|
//
|
|
// What is DEFINITELY possible is something like OSC52 triggering
|
|
// a clipboard-write signal on an unfocused tab/surface. We definitely
|
|
// want to show the user a notification about that but our notification
|
|
// right now is a toast that doesn't make it clear WHO used the
|
|
// clipboard. We probably want to change that in the future.
|
|
//
|
|
// I'm not sure how desirable all the above is, and we probably
|
|
// should be thoughtful about future signals here. But all of this
|
|
// behavior is consistent with macOS and the previous GTK apprt,
|
|
// but that behavior was all implicit and not documented, so here
|
|
// I am.
|
|
//
|
|
// TODO: When we have a split tree we'll want to attach to that.
|
|
const surface = tab.getActiveSurface();
|
|
_ = Surface.signals.@"close-request".connect(
|
|
surface,
|
|
*Self,
|
|
surfaceCloseRequest,
|
|
self,
|
|
.{},
|
|
);
|
|
_ = Surface.signals.@"clipboard-write".connect(
|
|
surface,
|
|
*Self,
|
|
surfaceClipboardWrite,
|
|
self,
|
|
.{},
|
|
);
|
|
_ = Surface.signals.@"toggle-fullscreen".connect(
|
|
surface,
|
|
*Self,
|
|
surfaceToggleFullscreen,
|
|
self,
|
|
.{},
|
|
);
|
|
_ = Surface.signals.@"toggle-maximize".connect(
|
|
surface,
|
|
*Self,
|
|
surfaceToggleMaximize,
|
|
self,
|
|
.{},
|
|
);
|
|
}
|
|
|
|
fn tabViewPageDetached(
|
|
_: *adw.TabView,
|
|
page: *adw.TabPage,
|
|
_: c_int,
|
|
self: *Self,
|
|
) callconv(.c) void {
|
|
// We need to get the tab to disconnect the signals.
|
|
const child = page.getChild();
|
|
const tab = gobject.ext.cast(Tab, child) orelse return;
|
|
_ = gobject.signalHandlersDisconnectMatched(
|
|
tab.as(gobject.Object),
|
|
.{ .data = true },
|
|
0,
|
|
0,
|
|
null,
|
|
null,
|
|
self,
|
|
);
|
|
|
|
// Remove all the signals that have this window as the userdata.
|
|
const surface = tab.getActiveSurface();
|
|
_ = gobject.signalHandlersDisconnectMatched(
|
|
surface.as(gobject.Object),
|
|
.{ .data = true },
|
|
0,
|
|
0,
|
|
null,
|
|
null,
|
|
self,
|
|
);
|
|
}
|
|
|
|
fn tabCloseRequest(
|
|
tab: *Tab,
|
|
self: *Self,
|
|
) callconv(.c) void {
|
|
const priv = self.private();
|
|
const page = priv.tab_view.getPage(tab.as(gtk.Widget));
|
|
// TODO: connect close page handler to tab to check for confirmation
|
|
priv.tab_view.closePage(page);
|
|
}
|
|
|
|
fn tabViewNPages(
|
|
_: *adw.TabView,
|
|
_: *gobject.ParamSpec,
|
|
self: *Self,
|
|
) callconv(.c) void {
|
|
const priv = self.private();
|
|
if (priv.tab_view.getNPages() == 0) {
|
|
// If we have no pages left then we want to close window.
|
|
self.as(gtk.Window).close();
|
|
}
|
|
}
|
|
|
|
fn surfaceClipboardWrite(
|
|
_: *Surface,
|
|
clipboard_type: apprt.Clipboard,
|
|
text: [*:0]const u8,
|
|
self: *Self,
|
|
) callconv(.c) void {
|
|
// We only toast for the standard clipboard.
|
|
if (clipboard_type != .standard) return;
|
|
|
|
// We only toast if configured to
|
|
const priv = self.private();
|
|
const config_obj = priv.config orelse return;
|
|
const config = config_obj.get();
|
|
if (!config.@"app-notifications".@"clipboard-copy") {
|
|
return;
|
|
}
|
|
|
|
if (text[0] != 0)
|
|
self.addToast(i18n._("Copied to clipboard"))
|
|
else
|
|
self.addToast(i18n._("Cleared clipboard"));
|
|
}
|
|
|
|
fn surfaceCloseRequest(
|
|
_: *Surface,
|
|
scope: *const Surface.CloseScope,
|
|
self: *Self,
|
|
) callconv(.c) void {
|
|
switch (scope.*) {
|
|
// Handled directly by the tab. If the surface is the last
|
|
// surface then the tab will emit its own signal to request
|
|
// closing itself.
|
|
.surface => return,
|
|
|
|
// Also handled directly by the tab.
|
|
.tab => return,
|
|
|
|
// The only one we care about!
|
|
.window => self.as(gtk.Window).close(),
|
|
}
|
|
}
|
|
|
|
fn surfaceToggleFullscreen(
|
|
surface: *Surface,
|
|
self: *Self,
|
|
) callconv(.c) void {
|
|
_ = surface;
|
|
if (self.as(gtk.Window).isFullscreen() != 0) {
|
|
self.as(gtk.Window).unfullscreen();
|
|
} else {
|
|
self.as(gtk.Window).fullscreen();
|
|
}
|
|
|
|
// We react to the changes in the propFullscreen callback
|
|
}
|
|
|
|
fn surfaceToggleMaximize(
|
|
surface: *Surface,
|
|
self: *Self,
|
|
) callconv(.c) void {
|
|
_ = surface;
|
|
if (self.as(gtk.Window).isMaximized() != 0) {
|
|
self.as(gtk.Window).unmaximize();
|
|
} else {
|
|
self.as(gtk.Window).maximize();
|
|
}
|
|
|
|
// We react to the changes in the propMaximized callback
|
|
}
|
|
|
|
fn actionAbout(
|
|
_: *gio.SimpleAction,
|
|
_: ?*glib.Variant,
|
|
self: *Self,
|
|
) callconv(.c) void {
|
|
const name = "Ghostty";
|
|
const icon = "com.mitchellh.ghostty";
|
|
const website = "https://ghostty.org";
|
|
|
|
if (adw_version.supportsDialogs()) {
|
|
adw.showAboutDialog(
|
|
self.as(gtk.Widget),
|
|
"application-name",
|
|
name,
|
|
"developer-name",
|
|
i18n._("Ghostty Developers"),
|
|
"application-icon",
|
|
icon,
|
|
"version",
|
|
build_config.version_string.ptr,
|
|
"issue-url",
|
|
"https://github.com/ghostty-org/ghostty/issues",
|
|
"website",
|
|
website,
|
|
@as(?*anyopaque, null),
|
|
);
|
|
} else {
|
|
gtk.showAboutDialog(
|
|
self.as(gtk.Window),
|
|
"program-name",
|
|
name,
|
|
"logo-icon-name",
|
|
icon,
|
|
"title",
|
|
i18n._("About Ghostty"),
|
|
"version",
|
|
build_config.version_string.ptr,
|
|
"website",
|
|
website,
|
|
@as(?*anyopaque, null),
|
|
);
|
|
}
|
|
}
|
|
|
|
fn actionClose(
|
|
_: *gio.SimpleAction,
|
|
_: ?*glib.Variant,
|
|
self: *Self,
|
|
) callconv(.c) void {
|
|
self.as(gtk.Window).close();
|
|
}
|
|
|
|
fn actionNewWindow(
|
|
_: *gio.SimpleAction,
|
|
_: ?*glib.Variant,
|
|
self: *Window,
|
|
) callconv(.c) void {
|
|
self.performBindingAction(.new_window);
|
|
}
|
|
|
|
fn actionCopy(
|
|
_: *gio.SimpleAction,
|
|
_: ?*glib.Variant,
|
|
self: *Window,
|
|
) callconv(.c) void {
|
|
self.performBindingAction(.copy_to_clipboard);
|
|
}
|
|
|
|
fn actionPaste(
|
|
_: *gio.SimpleAction,
|
|
_: ?*glib.Variant,
|
|
self: *Window,
|
|
) callconv(.c) void {
|
|
self.performBindingAction(.paste_from_clipboard);
|
|
}
|
|
|
|
fn actionReset(
|
|
_: *gio.SimpleAction,
|
|
_: ?*glib.Variant,
|
|
self: *Window,
|
|
) callconv(.c) void {
|
|
self.performBindingAction(.reset);
|
|
}
|
|
|
|
fn actionClear(
|
|
_: *gio.SimpleAction,
|
|
_: ?*glib.Variant,
|
|
self: *Window,
|
|
) callconv(.c) void {
|
|
self.performBindingAction(.clear_screen);
|
|
}
|
|
|
|
const C = Common(Self, Private);
|
|
pub const as = C.as;
|
|
pub const ref = C.ref;
|
|
pub const unref = C.unref;
|
|
const private = C.private;
|
|
|
|
pub const Class = extern struct {
|
|
parent_class: Parent.Class,
|
|
var parent: *Parent.Class = undefined;
|
|
pub const Instance = Self;
|
|
|
|
fn init(class: *Class) callconv(.C) void {
|
|
gobject.ext.ensureType(DebugWarning);
|
|
gtk.Widget.Class.setTemplateFromResource(
|
|
class.as(gtk.Widget.Class),
|
|
comptime gresource.blueprint(.{
|
|
.major = 1,
|
|
.minor = 5,
|
|
.name = "window",
|
|
}),
|
|
);
|
|
|
|
// Properties
|
|
gobject.ext.registerProperties(class, &.{
|
|
properties.@"active-surface".impl,
|
|
properties.@"background-opaque".impl,
|
|
properties.config.impl,
|
|
properties.debug.impl,
|
|
properties.@"headerbar-visible".impl,
|
|
properties.@"tabs-autohide".impl,
|
|
properties.@"tabs-visible".impl,
|
|
properties.@"tabs-wide".impl,
|
|
properties.@"toolbar-style".impl,
|
|
});
|
|
|
|
// Bindings
|
|
class.bindTemplateChildPrivate("tab_bar", .{});
|
|
class.bindTemplateChildPrivate("tab_view", .{});
|
|
class.bindTemplateChildPrivate("toolbar", .{});
|
|
class.bindTemplateChildPrivate("toast_overlay", .{});
|
|
|
|
// Template Callbacks
|
|
class.bindTemplateCallback("close_request", &windowCloseRequest);
|
|
class.bindTemplateCallback("close_page", &tabViewClosePage);
|
|
class.bindTemplateCallback("page_attached", &tabViewPageAttached);
|
|
class.bindTemplateCallback("page_detached", &tabViewPageDetached);
|
|
class.bindTemplateCallback("notify_n_pages", &tabViewNPages);
|
|
class.bindTemplateCallback("notify_selected_page", &tabViewSelectedPage);
|
|
class.bindTemplateCallback("notify_config", &propConfig);
|
|
class.bindTemplateCallback("notify_fullscreened", &propFullscreened);
|
|
class.bindTemplateCallback("notify_maximized", &propMaximized);
|
|
class.bindTemplateCallback("notify_menu_active", &propMenuActive);
|
|
class.bindTemplateCallback("notify_background_opaque", &propBackgroundOpaque);
|
|
|
|
// Virtual methods
|
|
gobject.Object.virtual_methods.dispose.implement(class, &dispose);
|
|
gobject.Object.virtual_methods.finalize.implement(class, &finalize);
|
|
}
|
|
|
|
pub const as = C.Class.as;
|
|
pub const bindTemplateChildPrivate = C.Class.bindTemplateChildPrivate;
|
|
pub const bindTemplateCallback = C.Class.bindTemplateCallback;
|
|
};
|
|
};
|