1983 lines
64 KiB
Zig
1983 lines
64 KiB
Zig
const std = @import("std");
|
|
const build_config = @import("../../../build_config.zig");
|
|
const assert = std.debug.assert;
|
|
const adw = @import("adw");
|
|
const gdk = @import("gdk");
|
|
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 configpkg = @import("../../../config.zig");
|
|
const TitlebarStyle = configpkg.Config.GtkTitlebarStyle;
|
|
const input = @import("../../../input.zig");
|
|
const CoreSurface = @import("../../../Surface.zig");
|
|
const ext = @import("../ext.zig");
|
|
const gtk_version = @import("../gtk_version.zig");
|
|
const adw_version = @import("../adw_version.zig");
|
|
const gresource = @import("../build/gresource.zig");
|
|
const winprotopkg = @import("../winproto.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 SplitTree = @import("split_tree.zig").SplitTree;
|
|
const Surface = @import("surface.zig").Surface;
|
|
const Tab = @import("tab.zig").Tab;
|
|
const DebugWarning = @import("debug_warning.zig").DebugWarning;
|
|
const CommandPalette = @import("command_palette.zig").CommandPalette;
|
|
const InspectorWindow = @import("inspector_window.zig").InspectorWindow;
|
|
const WeakRef = @import("../weak_ref.zig").WeakRef;
|
|
|
|
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,
|
|
.{
|
|
.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,
|
|
.{
|
|
.accessor = C.privateObjFieldAccessor("config"),
|
|
},
|
|
);
|
|
};
|
|
|
|
pub const debug = struct {
|
|
pub const name = "debug";
|
|
const impl = gobject.ext.defineProperty(
|
|
name,
|
|
Self,
|
|
bool,
|
|
.{
|
|
.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 @"titlebar-style" = struct {
|
|
pub const name = "titlebar-style";
|
|
const impl = gobject.ext.defineProperty(
|
|
name,
|
|
Self,
|
|
TitlebarStyle,
|
|
.{
|
|
.default = .native,
|
|
.accessor = gobject.ext.typedAccessor(
|
|
Self,
|
|
TitlebarStyle,
|
|
.{
|
|
.getter = Self.getTitlebarStyle,
|
|
},
|
|
),
|
|
},
|
|
);
|
|
};
|
|
|
|
pub const @"headerbar-visible" = struct {
|
|
pub const name = "headerbar-visible";
|
|
const impl = gobject.ext.defineProperty(
|
|
name,
|
|
Self,
|
|
bool,
|
|
.{
|
|
.default = true,
|
|
.accessor = gobject.ext.typedAccessor(Self, bool, .{
|
|
.getter = Self.getHeaderbarVisible,
|
|
}),
|
|
},
|
|
);
|
|
};
|
|
|
|
pub const @"quick-terminal" = struct {
|
|
pub const name = "quick-terminal";
|
|
const impl = gobject.ext.defineProperty(
|
|
name,
|
|
Self,
|
|
bool,
|
|
.{
|
|
.default = true,
|
|
.accessor = gobject.ext.privateFieldAccessor(
|
|
Self,
|
|
Private,
|
|
&Private.offset,
|
|
"quick_terminal",
|
|
),
|
|
},
|
|
);
|
|
};
|
|
|
|
pub const @"tabs-autohide" = struct {
|
|
pub const name = "tabs-autohide";
|
|
const impl = gobject.ext.defineProperty(
|
|
name,
|
|
Self,
|
|
bool,
|
|
.{
|
|
.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,
|
|
.{
|
|
.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,
|
|
.{
|
|
.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,
|
|
.{
|
|
.default = .raised,
|
|
.accessor = gobject.ext.typedAccessor(
|
|
Self,
|
|
adw.ToolbarStyle,
|
|
.{
|
|
.getter = Self.getToolbarStyle,
|
|
},
|
|
),
|
|
},
|
|
);
|
|
};
|
|
};
|
|
|
|
const Private = struct {
|
|
/// Whether this window is a quick terminal. If it is then it
|
|
/// behaves slightly differently under certain scenarios.
|
|
quick_terminal: bool = false,
|
|
|
|
/// The window decoration override. If this is not set then we'll
|
|
/// inherit whatever the config has. This allows overriding the
|
|
/// config on a per-window basis.
|
|
window_decoration: ?configpkg.WindowDecoration = null,
|
|
|
|
/// Binding group for our active tab.
|
|
tab_bindings: *gobject.BindingGroup,
|
|
|
|
/// The configuration that this surface is using.
|
|
config: ?*Config = null,
|
|
|
|
/// State and logic for windowing protocol for a window.
|
|
winproto: winprotopkg.Window,
|
|
|
|
/// Kind of hacky to have this but this lets us know if we've
|
|
/// initialized any single surface yet. We need this because we
|
|
/// gate default size on this so that we don't resize the window
|
|
/// after surfaces already exist.
|
|
///
|
|
/// I think long term we can probably get rid of this by implementing
|
|
/// a property or method that gets us all the surfaces in all the
|
|
/// tabs and checking if we have zero or one that isn't initialized.
|
|
///
|
|
/// For now, this logic is more similar to our legacy GTK side.
|
|
surface_init: bool = false,
|
|
|
|
/// See tabOverviewOpen for why we have this.
|
|
tab_overview_focus_timer: ?c_uint = null,
|
|
|
|
/// A weak reference to a command palette.
|
|
command_palette: WeakRef(CommandPalette) = .empty,
|
|
|
|
// Template bindings
|
|
tab_overview: *adw.TabOverview,
|
|
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) *Self {
|
|
return gobject.ext.newInstance(Self, .{
|
|
.application = app,
|
|
});
|
|
}
|
|
|
|
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();
|
|
}
|
|
|
|
// We initialize our windowing protocol to none because we can't
|
|
// actually initialize this until we get realized.
|
|
priv.winproto = .none;
|
|
|
|
// 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();
|
|
|
|
// Start states based on config.
|
|
if (priv.config) |config_obj| {
|
|
const config = config_obj.get();
|
|
if (config.maximize) self.as(gtk.Window).maximize();
|
|
if (config.fullscreen) self.as(gtk.Window).fullscreen();
|
|
|
|
// If we have an explicit title set, we set that immediately
|
|
// so that any applications inspecting the window states see
|
|
// an immediate title set when the window appears, rather than
|
|
// waiting possibly a few event loop ticks for it to sync from
|
|
// the surface.
|
|
if (config.title) |v| self.as(gtk.Window).setTitle(v);
|
|
}
|
|
|
|
// We always sync our appearance at the end because loading our
|
|
// config and such can affect our bindings which are 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 s_variant_type = glib.ext.VariantType.newFor([:0]const u8);
|
|
defer s_variant_type.free();
|
|
|
|
const actions = [_]ext.actions.Action(Self){
|
|
.init("about", actionAbout, null),
|
|
.init("close", actionClose, null),
|
|
.init("close-tab", actionCloseTab, s_variant_type),
|
|
.init("new-tab", actionNewTab, null),
|
|
.init("new-window", actionNewWindow, null),
|
|
.init("ring-bell", actionRingBell, null),
|
|
.init("split-right", actionSplitRight, null),
|
|
.init("split-left", actionSplitLeft, null),
|
|
.init("split-up", actionSplitUp, null),
|
|
.init("split-down", actionSplitDown, null),
|
|
.init("copy", actionCopy, null),
|
|
.init("paste", actionPaste, null),
|
|
.init("reset", actionReset, null),
|
|
.init("clear", actionClear, null),
|
|
// TODO: accept the surface that toggled the command palette
|
|
.init("toggle-command-palette", actionToggleCommandPalette, null),
|
|
.init("toggle-inspector", actionToggleInspector, null),
|
|
};
|
|
|
|
ext.actions.add(Self, self, &actions);
|
|
}
|
|
|
|
/// Winproto backend for this window.
|
|
pub fn winproto(self: *Self) *winprotopkg.Window {
|
|
return &self.private().winproto;
|
|
}
|
|
|
|
/// Create a new tab with the given parent. The tab will be inserted
|
|
/// at the position dictated by the `window-new-tab-position` config.
|
|
/// The new tab will be selected.
|
|
pub fn newTab(self: *Self, parent_: ?*CoreSurface) void {
|
|
_ = self.newTabPage(parent_);
|
|
}
|
|
|
|
fn newTabPage(self: *Self, parent_: ?*CoreSurface) *adw.TabPage {
|
|
const priv = self.private();
|
|
const tab_view = priv.tab_view;
|
|
|
|
// Create our new tab object
|
|
const tab = gobject.ext.newInstance(Tab, .{
|
|
.config = priv.config,
|
|
});
|
|
if (parent_) |p| tab.setParent(p);
|
|
|
|
// Get the position that we should insert the new tab at.
|
|
const config = if (priv.config) |v| v.get() else {
|
|
// If we don't have a config we just append it at the end.
|
|
// This should never happen.
|
|
return tab_view.append(tab.as(gtk.Widget));
|
|
};
|
|
const position = switch (config.@"window-new-tab-position") {
|
|
.current => current: {
|
|
const selected = tab_view.getSelectedPage() orelse
|
|
break :current tab_view.getNPages();
|
|
const current = tab_view.getPagePosition(selected);
|
|
break :current current + 1;
|
|
},
|
|
|
|
.end => tab_view.getNPages(),
|
|
};
|
|
|
|
// Add the page and select it
|
|
const page = tab_view.insert(tab.as(gtk.Widget), position);
|
|
tab_view.setSelectedPage(page);
|
|
|
|
// Create some property bindings
|
|
_ = tab.as(gobject.Object).bindProperty(
|
|
"title",
|
|
page.as(gobject.Object),
|
|
"title",
|
|
.{ .sync_create = true },
|
|
);
|
|
_ = tab.as(gobject.Object).bindProperty(
|
|
"tooltip",
|
|
page.as(gobject.Object),
|
|
"tooltip",
|
|
.{ .sync_create = true },
|
|
);
|
|
|
|
// Bind signals
|
|
const split_tree = tab.getSplitTree();
|
|
_ = SplitTree.signals.changed.connect(
|
|
split_tree,
|
|
*Self,
|
|
tabSplitTreeChanged,
|
|
self,
|
|
.{},
|
|
);
|
|
|
|
// Run an initial notification for the surface tree so we can setup
|
|
// initial state.
|
|
tabSplitTreeChanged(
|
|
split_tree,
|
|
null,
|
|
split_tree.getTree(),
|
|
self,
|
|
);
|
|
|
|
return page;
|
|
}
|
|
|
|
pub const SelectTab = union(enum) {
|
|
previous,
|
|
next,
|
|
last,
|
|
n: usize,
|
|
};
|
|
|
|
/// Select the tab as requested. Returns true if the tab selection
|
|
/// changed.
|
|
pub fn selectTab(self: *Self, n: SelectTab) bool {
|
|
const priv = self.private();
|
|
const tab_view = priv.tab_view;
|
|
|
|
// Get our current tab numeric position
|
|
const selected = tab_view.getSelectedPage() orelse return false;
|
|
const current = tab_view.getPagePosition(selected);
|
|
|
|
// Get our total
|
|
const total = tab_view.getNPages();
|
|
|
|
const goto: c_int = switch (n) {
|
|
.previous => if (current > 0)
|
|
current - 1
|
|
else
|
|
total - 1,
|
|
|
|
.next => if (current < total - 1)
|
|
current + 1
|
|
else
|
|
0,
|
|
|
|
.last => total - 1,
|
|
|
|
.n => |v| n: {
|
|
// 1-indexed
|
|
if (v == 0) return false;
|
|
|
|
const n_int = std.math.cast(
|
|
c_int,
|
|
v,
|
|
) orelse return false;
|
|
break :n @min(n_int - 1, total - 1);
|
|
},
|
|
};
|
|
assert(goto >= 0);
|
|
assert(goto < total);
|
|
|
|
// If our target is the same as our current then we do nothing.
|
|
if (goto == current) return false;
|
|
|
|
// Add the page and select it
|
|
const page = tab_view.getNthPage(goto);
|
|
tab_view.setSelectedPage(page);
|
|
|
|
return true;
|
|
}
|
|
|
|
/// Move the tab containing the given surface by the given amount.
|
|
/// Returns if this affected any tab positioning.
|
|
pub fn moveTab(
|
|
self: *Self,
|
|
surface: *Surface,
|
|
amount: isize,
|
|
) bool {
|
|
const priv = self.private();
|
|
const tab_view = priv.tab_view;
|
|
|
|
// If we have one tab we never move.
|
|
const total = tab_view.getNPages();
|
|
if (total == 1) return false;
|
|
|
|
// Get the tab that contains the given surface.
|
|
const tab = ext.getAncestor(
|
|
Tab,
|
|
surface.as(gtk.Widget),
|
|
) orelse return false;
|
|
|
|
// Get the page position that contains the tab.
|
|
const page = tab_view.getPage(tab.as(gtk.Widget));
|
|
const pos = tab_view.getPagePosition(page);
|
|
|
|
// Move it
|
|
const desired_pos: c_int = desired: {
|
|
const initial: c_int = @intCast(pos + amount);
|
|
const max = total - 1;
|
|
break :desired if (initial < 0)
|
|
max + initial + 1
|
|
else if (initial > max)
|
|
initial - max - 1
|
|
else
|
|
initial;
|
|
};
|
|
assert(desired_pos >= 0);
|
|
assert(desired_pos < total);
|
|
|
|
return tab_view.reorderPage(page, desired_pos) != 0;
|
|
}
|
|
|
|
pub fn toggleTabOverview(self: *Self) void {
|
|
const priv = self.private();
|
|
const tab_overview = priv.tab_overview;
|
|
const is_open = tab_overview.getOpen() != 0;
|
|
tab_overview.setOpen(@intFromBool(!is_open));
|
|
}
|
|
|
|
/// Toggle the visible property.
|
|
pub fn toggleVisibility(self: *Self) void {
|
|
const widget = self.as(gtk.Widget);
|
|
widget.setVisible(@intFromBool(widget.isVisible() == 0));
|
|
}
|
|
|
|
/// 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 {
|
|
const priv = self.private();
|
|
const widget = self.as(gtk.Widget);
|
|
|
|
// Toggle style classes based on whether we're using CSDs or SSDs.
|
|
//
|
|
// These classes are defined in the gtk.Window documentation:
|
|
// https://docs.gtk.org/gtk4/class.Window.html#css-nodes.
|
|
{
|
|
// Reset all style classes first
|
|
inline for (&.{
|
|
"ssd",
|
|
"csd",
|
|
"solid-csd",
|
|
"no-border-radius",
|
|
}) |class|
|
|
widget.removeCssClass(class);
|
|
|
|
const csd_enabled = priv.winproto.clientSideDecorationEnabled();
|
|
self.as(gtk.Window).setDecorated(@intFromBool(csd_enabled));
|
|
|
|
if (csd_enabled) {
|
|
const display = widget.getDisplay();
|
|
|
|
// We do the exact same check GTK is doing internally and toggle
|
|
// either the `csd` or `solid-csd` style, based on whether the user's
|
|
// window manager is deemed _non-compositing_.
|
|
//
|
|
// In practice this only impacts users of traditional X11 window
|
|
// managers (e.g. i3, dwm, awesomewm, etc.) and not X11 desktop
|
|
// environments or Wayland compositors/DEs.
|
|
if (display.isRgba() != 0 and display.isComposited() != 0) {
|
|
widget.addCssClass("csd");
|
|
} else {
|
|
widget.addCssClass("solid-csd");
|
|
}
|
|
} else {
|
|
widget.addCssClass("ssd");
|
|
// Fix any artifacting that may occur in window corners.
|
|
widget.addCssClass("no-border-radius");
|
|
}
|
|
}
|
|
|
|
// Trigger all our dynamic properties that depend on the config.
|
|
inline for (&.{
|
|
"headerbar-visible",
|
|
"tabs-autohide",
|
|
"tabs-visible",
|
|
"tabs-wide",
|
|
"toolbar-style",
|
|
"titlebar-style",
|
|
}) |key| {
|
|
self.as(gobject.Object).notifyByPspec(
|
|
@field(properties, key).impl.param_spec,
|
|
);
|
|
}
|
|
|
|
// Remainder uses the config
|
|
const config = if (priv.config) |v| v.get() else return;
|
|
|
|
// Only add a solid background if we're opaque.
|
|
self.toggleCssClass(
|
|
"background",
|
|
config.@"background-opacity" >= 1,
|
|
);
|
|
|
|
// Apply class to color headerbar if window-theme is set to `ghostty` and
|
|
// GTK version is before 4.16. The conditional is because above 4.16
|
|
// we use GTK CSS color variables.
|
|
self.toggleCssClass(
|
|
"window-theme-ghostty",
|
|
!gtk_version.atLeast(4, 16, 0) and
|
|
config.@"window-theme" == .ghostty,
|
|
);
|
|
|
|
// 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)),
|
|
}
|
|
|
|
// Do our window-protocol specific appearance sync.
|
|
priv.winproto.syncAppearance() catch |err| {
|
|
log.warn("failed to sync winproto appearance error={}", .{err});
|
|
};
|
|
}
|
|
|
|
/// Sync the state of any actions on this window.
|
|
fn syncActions(self: *Self) void {
|
|
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));
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
fn connectSurfaceHandlers(
|
|
self: *Self,
|
|
tree: *const Surface.Tree,
|
|
) void {
|
|
const priv = self.private();
|
|
var it = tree.iterator();
|
|
while (it.next()) |entry| {
|
|
const surface = entry.view;
|
|
_ = Surface.signals.@"present-request".connect(
|
|
surface,
|
|
*Self,
|
|
surfacePresentRequest,
|
|
self,
|
|
.{},
|
|
);
|
|
_ = Surface.signals.@"clipboard-write".connect(
|
|
surface,
|
|
*Self,
|
|
surfaceClipboardWrite,
|
|
self,
|
|
.{},
|
|
);
|
|
_ = Surface.signals.menu.connect(
|
|
surface,
|
|
*Self,
|
|
surfaceMenu,
|
|
self,
|
|
.{},
|
|
);
|
|
_ = Surface.signals.@"toggle-fullscreen".connect(
|
|
surface,
|
|
*Self,
|
|
surfaceToggleFullscreen,
|
|
self,
|
|
.{},
|
|
);
|
|
_ = Surface.signals.@"toggle-maximize".connect(
|
|
surface,
|
|
*Self,
|
|
surfaceToggleMaximize,
|
|
self,
|
|
.{},
|
|
);
|
|
|
|
// If we've never had a surface initialize yet, then we register
|
|
// this signal. Its theoretically possible to launch multiple surfaces
|
|
// before init so we could register this on multiple and that is not
|
|
// a problem because we'll check the flag again in each handler.
|
|
if (!priv.surface_init) {
|
|
_ = Surface.signals.init.connect(
|
|
surface,
|
|
*Self,
|
|
surfaceInit,
|
|
self,
|
|
.{},
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Disconnect all the surface handlers for the given tree. This should
|
|
/// be called whenever a tree is no longer present in the window, e.g.
|
|
/// when a tab is detached or the tree changes.
|
|
fn disconnectSurfaceHandlers(
|
|
self: *Self,
|
|
tree: *const Surface.Tree,
|
|
) void {
|
|
var it = tree.iterator();
|
|
while (it.next()) |entry| {
|
|
const surface = entry.view;
|
|
_ = gobject.signalHandlersDisconnectMatched(
|
|
surface.as(gobject.Object),
|
|
.{ .data = true },
|
|
0,
|
|
0,
|
|
null,
|
|
null,
|
|
self,
|
|
);
|
|
}
|
|
}
|
|
|
|
//---------------------------------------------------------------
|
|
// Properties
|
|
|
|
/// Whether this terminal is a quick terminal or not.
|
|
pub fn isQuickTerminal(self: *Self) bool {
|
|
return self.private().quick_terminal;
|
|
}
|
|
|
|
/// 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();
|
|
}
|
|
|
|
/// Returns the configuration for this window. The reference count
|
|
/// is not increased.
|
|
pub fn getConfig(self: *Self) ?*Config {
|
|
return self.private().config;
|
|
}
|
|
|
|
/// Get the current window decoration value for this window.
|
|
pub fn getWindowDecoration(self: *Self) configpkg.WindowDecoration {
|
|
const priv = self.private();
|
|
if (priv.window_decoration) |v| return v;
|
|
if (priv.config) |v| return v.get().@"window-decoration";
|
|
return .auto;
|
|
}
|
|
|
|
/// Toggle the window decorations for this window.
|
|
pub fn toggleWindowDecorations(self: *Self) void {
|
|
const priv = self.private();
|
|
|
|
if (priv.window_decoration) |_| {
|
|
// Unset any previously set window decoration settings
|
|
self.setWindowDecoration(null);
|
|
return;
|
|
}
|
|
|
|
const config = if (priv.config) |v| v.get() else return;
|
|
self.setWindowDecoration(switch (config.@"window-decoration") {
|
|
// Use auto when the decoration is initially none
|
|
.none => .auto,
|
|
|
|
// Anything non-none to none
|
|
.auto, .client, .server => .none,
|
|
});
|
|
}
|
|
|
|
/// Set the window decoration override for this window. If this is null,
|
|
/// then we'll revert back to the configuration's default.
|
|
fn setWindowDecoration(
|
|
self: *Self,
|
|
new_: ?configpkg.WindowDecoration,
|
|
) void {
|
|
const priv = self.private();
|
|
priv.window_decoration = new_;
|
|
self.syncAppearance();
|
|
}
|
|
|
|
/// 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);
|
|
}
|
|
|
|
/// Returns true if this window needs confirmation before quitting.
|
|
fn getNeedsConfirmQuit(self: *Self) bool {
|
|
const priv = self.private();
|
|
const n = priv.tab_view.getNPages();
|
|
assert(n >= 0);
|
|
|
|
for (0..@intCast(n)) |i| {
|
|
const page = priv.tab_view.getNthPage(@intCast(i));
|
|
const child = page.getChild();
|
|
const tab = gobject.ext.cast(Tab, child) orelse {
|
|
log.warn("unexpected non-Tab child in tab view", .{});
|
|
continue;
|
|
};
|
|
if (tab.getNeedsConfirmQuit()) return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
fn isFullscreen(self: *Window) bool {
|
|
return self.as(gtk.Window).isFullscreen() != 0;
|
|
}
|
|
|
|
fn isMaximized(self: *Window) bool {
|
|
return self.as(gtk.Window).isMaximized() != 0;
|
|
}
|
|
|
|
fn getHeaderbarVisible(self: *Self) bool {
|
|
const priv = self.private();
|
|
|
|
// Never display the header bar when CSDs are disabled.
|
|
const csd_enabled = priv.winproto.clientSideDecorationEnabled();
|
|
if (!csd_enabled) return false;
|
|
|
|
// Never display the header bar as a quick terminal.
|
|
if (priv.quick_terminal) return false;
|
|
|
|
// If we're fullscreen we never show the header bar.
|
|
if (self.isFullscreen()) 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.isMaximized() and config.@"gtk-titlebar-hide-when-maximized") {
|
|
return false;
|
|
}
|
|
|
|
return switch (config.@"gtk-titlebar-style") {
|
|
// If the titlebar style is tabs never show the titlebar.
|
|
.tabs => false,
|
|
|
|
// If the titlebar style is native show the titlebar if configured
|
|
// to do so.
|
|
.native => config.@"gtk-titlebar",
|
|
};
|
|
}
|
|
|
|
fn getTabsAutohide(self: *Self) bool {
|
|
const priv = self.private();
|
|
const config = if (priv.config) |v| v.get() else return true;
|
|
|
|
return switch (config.@"gtk-titlebar-style") {
|
|
// If the titlebar style is tabs we cannot autohide.
|
|
.tabs => false,
|
|
|
|
.native => 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;
|
|
|
|
switch (config.@"gtk-titlebar-style") {
|
|
.tabs => {
|
|
// *Conditionally* disable the tab bar when maximized, the titlebar
|
|
// style is tabs, and gtk-titlebar-hide-when-maximized is set.
|
|
if (self.isMaximized() and config.@"gtk-titlebar-hide-when-maximized") return false;
|
|
|
|
// If the titlebar style is tabs the tab bar must always be visible.
|
|
return true;
|
|
},
|
|
.native => {
|
|
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 getTitlebarStyle(self: *Self) TitlebarStyle {
|
|
const priv = self.private();
|
|
const config = if (priv.config) |v| v.get() else return .native;
|
|
return config.@"gtk-titlebar-style";
|
|
}
|
|
|
|
fn propConfig(
|
|
_: *adw.ApplicationWindow,
|
|
_: *gobject.ParamSpec,
|
|
self: *Self,
|
|
) callconv(.c) void {
|
|
const priv = self.private();
|
|
if (priv.config) |config_obj| {
|
|
const config = config_obj.get();
|
|
if (config.@"app-notifications".@"config-reload") {
|
|
self.addToast(i18n._("Reloaded the configuration"));
|
|
}
|
|
}
|
|
|
|
self.syncAppearance();
|
|
}
|
|
|
|
fn propGdkSurfaceHeight(
|
|
_: *gdk.Surface,
|
|
_: *gobject.ParamSpec,
|
|
self: *Self,
|
|
) callconv(.c) void {
|
|
// X11 needs to fix blurring on resize, but winproto implementations
|
|
// could do anything.
|
|
self.private().winproto.resizeEvent() catch |err| {
|
|
log.warn(
|
|
"winproto resize event failed error={}",
|
|
.{err},
|
|
);
|
|
};
|
|
}
|
|
|
|
fn propIsActive(
|
|
_: *gtk.Window,
|
|
_: *gobject.ParamSpec,
|
|
self: *Self,
|
|
) callconv(.c) void {
|
|
// Don't change urgency if we're not the active window.
|
|
if (self.as(gtk.Window).isActive() == 0) return;
|
|
|
|
self.winproto().setUrgent(false) catch |err| {
|
|
log.warn(
|
|
"winproto failed to reset urgency={}",
|
|
.{err},
|
|
);
|
|
};
|
|
}
|
|
|
|
fn propGdkSurfaceWidth(
|
|
_: *gdk.Surface,
|
|
_: *gobject.ParamSpec,
|
|
self: *Self,
|
|
) callconv(.c) void {
|
|
// X11 needs to fix blurring on resize, but winproto implementations
|
|
// could do anything.
|
|
self.private().winproto.resizeEvent() catch |err| {
|
|
log.warn(
|
|
"winproto resize event failed error={}",
|
|
.{err},
|
|
);
|
|
};
|
|
}
|
|
|
|
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;
|
|
|
|
self.syncActions();
|
|
}
|
|
|
|
fn propQuickTerminal(
|
|
_: *adw.ApplicationWindow,
|
|
_: *gobject.ParamSpec,
|
|
self: *Self,
|
|
) callconv(.c) void {
|
|
const priv = self.private();
|
|
if (priv.surface_init) {
|
|
log.warn("quick terminal property can't be changed after surfaces have been initialized", .{});
|
|
return;
|
|
}
|
|
|
|
if (priv.quick_terminal) {
|
|
// Initialize the quick terminal at the app-layer
|
|
Application.default().winproto().initQuickTerminal(self) catch |err| {
|
|
log.warn("failed to initialize quick terminal error={}", .{err});
|
|
return;
|
|
};
|
|
}
|
|
}
|
|
|
|
fn propScaleFactor(
|
|
_: *adw.ApplicationWindow,
|
|
_: *gobject.ParamSpec,
|
|
self: *Self,
|
|
) callconv(.c) void {
|
|
// On some platforms (namely X11) we need to refresh our appearance when
|
|
// the scale factor changes. In theory this could be more fine-grained as
|
|
// a full refresh could be expensive, but a) this *should* be rare, and
|
|
// b) quite noticeable visual bugs would occur if this is not present.
|
|
self.private().winproto.syncAppearance() catch |err| {
|
|
log.warn(
|
|
"failed to sync appearance after scale factor has been updated={}",
|
|
.{err},
|
|
);
|
|
return;
|
|
};
|
|
}
|
|
|
|
fn closureTitlebarStyleIsTab(
|
|
_: *Self,
|
|
value: TitlebarStyle,
|
|
) callconv(.c) c_int {
|
|
return @intFromBool(switch (value) {
|
|
.native => false,
|
|
.tabs => true,
|
|
});
|
|
}
|
|
|
|
fn closureSubtitle(
|
|
_: *Self,
|
|
config_: ?*Config,
|
|
pwd_: ?[*:0]const u8,
|
|
) callconv(.c) ?[*:0]const u8 {
|
|
const config = if (config_) |v| v.get() else return null;
|
|
return switch (config.@"window-subtitle") {
|
|
.false => null,
|
|
.@"working-directory" => pwd: {
|
|
const pwd = pwd_ orelse return null;
|
|
break :pwd glib.ext.dupeZ(u8, std.mem.span(pwd));
|
|
},
|
|
};
|
|
}
|
|
|
|
//---------------------------------------------------------------
|
|
// Virtual methods
|
|
|
|
fn dispose(self: *Self) callconv(.c) void {
|
|
const priv = self.private();
|
|
|
|
priv.command_palette.set(null);
|
|
|
|
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();
|
|
priv.winproto.deinit(Application.default().allocator());
|
|
|
|
gobject.Object.virtual_methods.finalize.call(
|
|
Class.parent,
|
|
self.as(Parent),
|
|
);
|
|
}
|
|
|
|
//---------------------------------------------------------------
|
|
// Signal handlers
|
|
|
|
fn windowRealize(_: *gtk.Widget, self: *Window) callconv(.c) void {
|
|
const app = Application.default();
|
|
|
|
// Initialize our window protocol logic
|
|
if (winprotopkg.Window.init(
|
|
app.allocator(),
|
|
app.winproto(),
|
|
self,
|
|
)) |wp| {
|
|
self.private().winproto = wp;
|
|
} else |err| {
|
|
log.warn("failed to initialize window protocol error={}", .{err});
|
|
return;
|
|
}
|
|
|
|
// We need to setup resize notifications on our surface,
|
|
// which is only available after the window had been realized.
|
|
if (self.as(gtk.Native).getSurface()) |gdk_surface| {
|
|
_ = gobject.Object.signals.notify.connect(
|
|
gdk_surface,
|
|
*Self,
|
|
propGdkSurfaceWidth,
|
|
self,
|
|
.{ .detail = "width" },
|
|
);
|
|
_ = gobject.Object.signals.notify.connect(
|
|
gdk_surface,
|
|
*Self,
|
|
propGdkSurfaceHeight,
|
|
self,
|
|
.{ .detail = "height" },
|
|
);
|
|
}
|
|
|
|
// When we are realized we always setup our appearance since this
|
|
// calls some winproto functions.
|
|
self.syncAppearance();
|
|
}
|
|
|
|
fn btnNewTab(_: *adw.SplitButton, self: *Self) callconv(.c) void {
|
|
self.performBindingAction(.new_tab);
|
|
}
|
|
|
|
fn tabOverviewCreateTab(
|
|
_: *adw.TabOverview,
|
|
self: *Self,
|
|
) callconv(.c) *adw.TabPage {
|
|
return self.newTabPage(if (self.getActiveSurface()) |v| v.core() else null);
|
|
}
|
|
|
|
fn tabOverviewOpen(
|
|
tab_overview: *adw.TabOverview,
|
|
_: *gobject.ParamSpec,
|
|
self: *Self,
|
|
) callconv(.c) void {
|
|
// We only care about when the tab overview is closed.
|
|
if (tab_overview.getOpen() != 0) return;
|
|
|
|
// On tab overview close, focus is sometimes lost. This is an
|
|
// upstream issue in libadwaita[1]. When this is resolved we
|
|
// can put a runtime version check here to avoid this workaround.
|
|
//
|
|
// Our workaround is to start a timer after 500ms to refocus
|
|
// the currently selected tab. We choose 500ms because the adw
|
|
// animation is 400ms.
|
|
//
|
|
// [1]: https://gitlab.gnome.org/GNOME/libadwaita/-/issues/670
|
|
|
|
// If we have an old timer remove it
|
|
const priv = self.private();
|
|
if (priv.tab_overview_focus_timer) |timer| {
|
|
_ = glib.Source.remove(timer);
|
|
}
|
|
|
|
// Restart our timer
|
|
priv.tab_overview_focus_timer = glib.timeoutAdd(
|
|
500,
|
|
tabOverviewFocusTimer,
|
|
self,
|
|
);
|
|
}
|
|
|
|
fn tabOverviewFocusTimer(
|
|
ud: ?*anyopaque,
|
|
) callconv(.c) c_int {
|
|
const self: *Self = @ptrCast(@alignCast(ud orelse return 0));
|
|
|
|
// Always note our timer is removed
|
|
self.private().tab_overview_focus_timer = null;
|
|
|
|
// Get our currently active surface which should respect the newly
|
|
// selected tab. Grab focus.
|
|
const surface = self.getActiveSurface() orelse return 0;
|
|
surface.grabFocus();
|
|
|
|
// Remove the timer
|
|
return 0;
|
|
}
|
|
|
|
fn windowCloseRequest(
|
|
_: *gtk.Window,
|
|
self: *Self,
|
|
) callconv(.c) c_int {
|
|
if (self.getNeedsConfirmQuit()) {
|
|
// Show a confirmation dialog
|
|
const dialog: *CloseConfirmationDialog = .new(.window);
|
|
_ = 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 = ext.getAncestor(
|
|
adw.TabView,
|
|
page.getChild().as(gtk.Widget),
|
|
) orelse {
|
|
log.warn("close confirmation called for non-existent page", .{});
|
|
return;
|
|
};
|
|
tab_view.closePageFinish(page, @intFromBool(true));
|
|
}
|
|
|
|
fn closeConfirmationCancelTab(
|
|
_: *CloseConfirmationDialog,
|
|
page: *adw.TabPage,
|
|
) callconv(.c) void {
|
|
const tab_view = ext.getAncestor(
|
|
adw.TabView,
|
|
page.getChild().as(gtk.Widget),
|
|
) orelse {
|
|
log.warn("close confirmation called for non-existent page", .{});
|
|
return;
|
|
};
|
|
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));
|
|
|
|
// If the tab was previously marked as needing attention
|
|
// (e.g. due to a bell character), we now unmark that
|
|
page.setNeedsAttention(@intFromBool(false));
|
|
}
|
|
|
|
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.
|
|
if (tab.getSurfaceTree()) |tree| {
|
|
self.connectSurfaceHandlers(tree);
|
|
}
|
|
}
|
|
|
|
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 the tree handlers
|
|
if (tab.getSurfaceTree()) |tree| {
|
|
self.disconnectSurfaceHandlers(tree);
|
|
}
|
|
}
|
|
|
|
fn tabViewCreateWindow(
|
|
_: *adw.TabView,
|
|
_: *Self,
|
|
) callconv(.c) *adw.TabView {
|
|
// Create a new window without creating a new tab.
|
|
const win = gobject.ext.newInstance(
|
|
Self,
|
|
.{
|
|
.application = Application.default(),
|
|
},
|
|
);
|
|
|
|
// We have to show it otherwise it'll just be hidden.
|
|
gtk.Window.present(win.as(gtk.Window));
|
|
|
|
// Get our tab view
|
|
return win.private().tab_view;
|
|
}
|
|
|
|
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 surfaceMenu(
|
|
_: *Surface,
|
|
self: *Self,
|
|
) callconv(.c) void {
|
|
self.syncActions();
|
|
}
|
|
|
|
fn surfacePresentRequest(
|
|
surface: *Surface,
|
|
self: *Self,
|
|
) callconv(.c) void {
|
|
// Verify that this surface is actually in this window.
|
|
{
|
|
const surface_window = ext.getAncestor(
|
|
Self,
|
|
surface.as(gtk.Widget),
|
|
) orelse {
|
|
log.warn(
|
|
"present request called for non-existent surface",
|
|
.{},
|
|
);
|
|
return;
|
|
};
|
|
if (surface_window != self) {
|
|
log.warn(
|
|
"present request called for surface in different window",
|
|
.{},
|
|
);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Get the tab for this surface.
|
|
const tab = ext.getAncestor(
|
|
Tab,
|
|
surface.as(gtk.Widget),
|
|
) orelse {
|
|
log.warn("present request surface not found", .{});
|
|
return;
|
|
};
|
|
|
|
// Get the page that contains this tab
|
|
const priv = self.private();
|
|
const tab_view = priv.tab_view;
|
|
const page = tab_view.getPage(tab.as(gtk.Widget));
|
|
tab_view.setSelectedPage(page);
|
|
|
|
// Grab focus
|
|
surface.grabFocus();
|
|
}
|
|
|
|
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 surfaceInit(
|
|
surface: *Surface,
|
|
self: *Self,
|
|
) callconv(.c) void {
|
|
const priv = self.private();
|
|
|
|
// Make sure we init only once
|
|
if (priv.surface_init) return;
|
|
priv.surface_init = true;
|
|
|
|
// Setup our default and minimum size.
|
|
if (surface.getDefaultSize()) |size| {
|
|
self.as(gtk.Window).setDefaultSize(
|
|
@intCast(size.width),
|
|
@intCast(size.height),
|
|
);
|
|
}
|
|
if (surface.getMinSize()) |size| {
|
|
self.as(gtk.Widget).setSizeRequest(
|
|
@intCast(size.width),
|
|
@intCast(size.height),
|
|
);
|
|
}
|
|
}
|
|
|
|
fn tabSplitTreeChanged(
|
|
_: *SplitTree,
|
|
old_tree: ?*const Surface.Tree,
|
|
new_tree: ?*const Surface.Tree,
|
|
self: *Self,
|
|
) callconv(.c) void {
|
|
if (old_tree) |tree| {
|
|
self.disconnectSurfaceHandlers(tree);
|
|
}
|
|
|
|
if (new_tree) |tree| {
|
|
self.connectSurfaceHandlers(tree);
|
|
}
|
|
}
|
|
|
|
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 actionCloseTab(
|
|
_: *gio.SimpleAction,
|
|
param_: ?*glib.Variant,
|
|
self: *Window,
|
|
) callconv(.c) void {
|
|
const param = param_ orelse {
|
|
log.warn("win.close-tab called without a parameter", .{});
|
|
return;
|
|
};
|
|
|
|
var str: ?[*:0]const u8 = null;
|
|
param.get("&s", &str);
|
|
|
|
const mode = std.meta.stringToEnum(
|
|
input.Binding.Action.CloseTabMode,
|
|
std.mem.span(
|
|
str orelse {
|
|
log.warn("invalid mode provided to win.close-tab", .{});
|
|
return;
|
|
},
|
|
),
|
|
) orelse {
|
|
log.warn("invalid mode provided to win.close-tab: {s}", .{str.?});
|
|
return;
|
|
};
|
|
|
|
self.performBindingAction(.{ .close_tab = mode });
|
|
}
|
|
|
|
fn actionNewWindow(
|
|
_: *gio.SimpleAction,
|
|
_: ?*glib.Variant,
|
|
self: *Window,
|
|
) callconv(.c) void {
|
|
self.performBindingAction(.new_window);
|
|
}
|
|
|
|
fn actionNewTab(
|
|
_: *gio.SimpleAction,
|
|
_: ?*glib.Variant,
|
|
self: *Window,
|
|
) callconv(.c) void {
|
|
self.performBindingAction(.new_tab);
|
|
}
|
|
|
|
fn actionSplitRight(
|
|
_: *gio.SimpleAction,
|
|
_: ?*glib.Variant,
|
|
self: *Window,
|
|
) callconv(.c) void {
|
|
self.performBindingAction(.{ .new_split = .right });
|
|
}
|
|
|
|
fn actionSplitLeft(
|
|
_: *gio.SimpleAction,
|
|
_: ?*glib.Variant,
|
|
self: *Window,
|
|
) callconv(.c) void {
|
|
self.performBindingAction(.{ .new_split = .left });
|
|
}
|
|
|
|
fn actionSplitUp(
|
|
_: *gio.SimpleAction,
|
|
_: ?*glib.Variant,
|
|
self: *Window,
|
|
) callconv(.c) void {
|
|
self.performBindingAction(.{ .new_split = .up });
|
|
}
|
|
|
|
fn actionSplitDown(
|
|
_: *gio.SimpleAction,
|
|
_: ?*glib.Variant,
|
|
self: *Window,
|
|
) callconv(.c) void {
|
|
self.performBindingAction(.{ .new_split = .down });
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
fn actionRingBell(
|
|
_: *gio.SimpleAction,
|
|
_: ?*glib.Variant,
|
|
self: *Window,
|
|
) callconv(.c) void {
|
|
const priv = self.private();
|
|
const config = if (priv.config) |v| v.get() else return;
|
|
|
|
if (config.@"bell-features".system) system: {
|
|
const native = self.as(gtk.Native).getSurface() orelse {
|
|
log.warn("unable to get native surface from window", .{});
|
|
break :system;
|
|
};
|
|
native.beep();
|
|
}
|
|
|
|
if (config.@"bell-features".attention) attention: {
|
|
// Dont set urgency if the window is already active.
|
|
if (self.as(gtk.Window).isActive() != 0) break :attention;
|
|
|
|
// Request user attention
|
|
self.winproto().setUrgent(true) catch |err| {
|
|
log.warn("winproto failed to set urgency={}", .{err});
|
|
};
|
|
}
|
|
}
|
|
|
|
/// Toggle the command palette.
|
|
///
|
|
/// TODO: accept the surface that toggled the command palette as a parameter
|
|
fn toggleCommandPalette(self: *Window) void {
|
|
const priv = self.private();
|
|
|
|
// Get a reference to a command palette. First check the weak reference
|
|
// that we save to see if we already have one stored. If we don't then
|
|
// create a new one.
|
|
const command_palette = priv.command_palette.get() orelse command_palette: {
|
|
// Create a fresh command palette.
|
|
const command_palette = CommandPalette.new();
|
|
|
|
// Synchronize our config to the command palette's config.
|
|
_ = gobject.Object.bindProperty(
|
|
self.as(gobject.Object),
|
|
"config",
|
|
command_palette.as(gobject.Object),
|
|
"config",
|
|
.{ .sync_create = true },
|
|
);
|
|
|
|
// Listen to the activate signal to know if the user selected an option in
|
|
// the command palette.
|
|
_ = CommandPalette.signals.trigger.connect(
|
|
command_palette,
|
|
*Window,
|
|
signalCommandPaletteTrigger,
|
|
self,
|
|
.{},
|
|
);
|
|
|
|
// Save a weak reference to the command palette. We use a weak reference to avoid
|
|
// reference counting cycles that might cause problems later.
|
|
priv.command_palette.set(command_palette);
|
|
|
|
break :command_palette command_palette;
|
|
};
|
|
defer command_palette.unref();
|
|
|
|
// Tell the command palette to toggle itself. If the dialog gets
|
|
// presented (instead of hidden) it will be modal over our window.
|
|
command_palette.toggle(self);
|
|
}
|
|
|
|
// React to a signal from a command palette asking an action to be performed.
|
|
fn signalCommandPaletteTrigger(_: *CommandPalette, action: *const input.Binding.Action, self: *Self) callconv(.c) void {
|
|
// If the activation actually has an action, perform it.
|
|
self.performBindingAction(action.*);
|
|
}
|
|
|
|
/// React to a GTK action requesting that the command palette be toggled.
|
|
fn actionToggleCommandPalette(
|
|
_: *gio.SimpleAction,
|
|
_: ?*glib.Variant,
|
|
self: *Window,
|
|
) callconv(.c) void {
|
|
// TODO: accept the surface that toggled the command palette as a
|
|
// parameter
|
|
self.toggleCommandPalette();
|
|
}
|
|
|
|
/// Toggle the Ghostty inspector for the active surface.
|
|
fn toggleInspector(self: *Self) void {
|
|
const surface = self.getActiveSurface() orelse return;
|
|
_ = surface.controlInspector(.toggle);
|
|
}
|
|
|
|
/// React to a GTK action requesting that the Ghostty inspector be toggled.
|
|
fn actionToggleInspector(
|
|
_: *gio.SimpleAction,
|
|
_: ?*glib.Variant,
|
|
self: *Window,
|
|
) callconv(.c) void {
|
|
// TODO: accept the surface that toggled the command palette as a
|
|
// parameter
|
|
self.toggleInspector();
|
|
}
|
|
|
|
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);
|
|
gobject.ext.ensureType(SplitTree);
|
|
gobject.ext.ensureType(Surface);
|
|
gobject.ext.ensureType(Tab);
|
|
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.config.impl,
|
|
properties.debug.impl,
|
|
properties.@"headerbar-visible".impl,
|
|
properties.@"quick-terminal".impl,
|
|
properties.@"tabs-autohide".impl,
|
|
properties.@"tabs-visible".impl,
|
|
properties.@"tabs-wide".impl,
|
|
properties.@"toolbar-style".impl,
|
|
properties.@"titlebar-style".impl,
|
|
});
|
|
|
|
// Bindings
|
|
class.bindTemplateChildPrivate("tab_overview", .{});
|
|
class.bindTemplateChildPrivate("tab_bar", .{});
|
|
class.bindTemplateChildPrivate("tab_view", .{});
|
|
class.bindTemplateChildPrivate("toolbar", .{});
|
|
class.bindTemplateChildPrivate("toast_overlay", .{});
|
|
|
|
// Template Callbacks
|
|
class.bindTemplateCallback("realize", &windowRealize);
|
|
class.bindTemplateCallback("new_tab", &btnNewTab);
|
|
class.bindTemplateCallback("overview_create_tab", &tabOverviewCreateTab);
|
|
class.bindTemplateCallback("overview_notify_open", &tabOverviewOpen);
|
|
class.bindTemplateCallback("close_request", &windowCloseRequest);
|
|
class.bindTemplateCallback("close_page", &tabViewClosePage);
|
|
class.bindTemplateCallback("page_attached", &tabViewPageAttached);
|
|
class.bindTemplateCallback("page_detached", &tabViewPageDetached);
|
|
class.bindTemplateCallback("tab_create_window", &tabViewCreateWindow);
|
|
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_is_active", &propIsActive);
|
|
class.bindTemplateCallback("notify_maximized", &propMaximized);
|
|
class.bindTemplateCallback("notify_menu_active", &propMenuActive);
|
|
class.bindTemplateCallback("notify_quick_terminal", &propQuickTerminal);
|
|
class.bindTemplateCallback("notify_scale_factor", &propScaleFactor);
|
|
class.bindTemplateCallback("titlebar_style_is_tabs", &closureTitlebarStyleIsTab);
|
|
class.bindTemplateCallback("computed_subtitle", &closureSubtitle);
|
|
|
|
// 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;
|
|
};
|
|
};
|