Add emoji prefix.
This commit is contained in:
+161
-24
@@ -19,6 +19,7 @@ const OpenLayoutDialog = @import("OpenLayoutDialog.zig");
|
||||
const Pane = @import("Pane.zig");
|
||||
const SaveLayoutDialog = @import("SaveLayoutDialog.zig");
|
||||
const SettingsDialog = @import("SettingsDialog.zig");
|
||||
const TabSettingsDialog = @import("TabSettingsDialog.zig");
|
||||
const Terminal = @import("Terminal.zig");
|
||||
const View = @import("View.zig");
|
||||
const appearance = @import("appearance.zig");
|
||||
@@ -80,6 +81,14 @@ const Tab = struct {
|
||||
/// the sidebar without reading the title.
|
||||
icon: *gtk.Image,
|
||||
|
||||
/// An emoji the user picked, shown in the icon's place. Unlike `custom_name`
|
||||
/// this is not owned: the glyph points into `emoji.table`, which is static,
|
||||
/// so there is nothing here to copy and nothing to free.
|
||||
emoji: ?[:0]const u8 = null,
|
||||
|
||||
/// The widget that draws that emoji, sharing the icon's slot in the row.
|
||||
emoji_label: *gtk.Label,
|
||||
|
||||
/// Status dot, hidden unless the tab has something to report.
|
||||
dot: *gtk.Image,
|
||||
|
||||
@@ -87,6 +96,9 @@ const Tab = struct {
|
||||
/// Null means the label tracks the content, which is the default.
|
||||
custom_name: ?[]u8 = null,
|
||||
|
||||
/// The row's right-click menu, parented to this tab's row.
|
||||
menu_popover: *gtk.Popover,
|
||||
|
||||
/// Popover holding the rename entry, parented to this tab's row.
|
||||
rename_popover: *gtk.Popover,
|
||||
rename_entry: *gtk.Entry,
|
||||
@@ -312,7 +324,9 @@ fn newTabEmpty(self: *Window) !*Tab {
|
||||
.row = gtk.ListBoxRow.new(),
|
||||
.label = gtk.Label.new("shell"),
|
||||
.icon = gtk.Image.newFromIconName("utilities-terminal-symbolic"),
|
||||
.emoji_label = gtk.Label.new(null),
|
||||
.dot = gtk.Image.newFromIconName(Pane.status_icon),
|
||||
.menu_popover = gtk.Popover.new(),
|
||||
.rename_popover = gtk.Popover.new(),
|
||||
.rename_entry = gtk.Entry.new(),
|
||||
.name = undefined,
|
||||
@@ -325,7 +339,13 @@ fn newTabEmpty(self: *Window) !*Tab {
|
||||
const row_box = gtk.Box.new(.horizontal, 6);
|
||||
row_box.as(gtk.Widget).addCssClass("playpen-row");
|
||||
|
||||
// Both live in the row, and `refreshLabel` shows exactly one of them. The
|
||||
// emoji is given the icon's width so that a sidebar of mixed rows still
|
||||
// has its labels starting in one column.
|
||||
row_box.append(tab.icon.as(gtk.Widget));
|
||||
tab.emoji_label.as(gtk.Widget).addCssClass("playpen-tab-emoji");
|
||||
tab.emoji_label.as(gtk.Widget).setVisible(0);
|
||||
row_box.append(tab.emoji_label.as(gtk.Widget));
|
||||
|
||||
tab.label.setXalign(0);
|
||||
tab.label.setEllipsize(.end);
|
||||
@@ -345,6 +365,7 @@ fn newTabEmpty(self: *Window) !*Tab {
|
||||
row_box.append(close.as(gtk.Widget));
|
||||
|
||||
tab.row.setChild(row_box.as(gtk.Widget));
|
||||
self.buildRowMenu(tab, row_box);
|
||||
self.buildRename(tab, row_box);
|
||||
self.list.append(tab.row.as(gtk.Widget));
|
||||
|
||||
@@ -355,6 +376,122 @@ fn newTabEmpty(self: *Window) !*Tab {
|
||||
return tab;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// The row menu
|
||||
//
|
||||
// Right-clicking a row opens a menu rather than going straight to the rename
|
||||
// entry, which is where it used to land. Renaming was the only thing a row
|
||||
// could do, so it was reasonable for the gesture to *be* renaming; now that a
|
||||
// row also has settings behind it, a gesture that silently picks one of the two
|
||||
// would make the other one unreachable by the route people try first.
|
||||
//
|
||||
// It does not select the row it belongs to. Renaming a tab, or giving it an
|
||||
// emoji, is not a reason to go and look at it — often it is the opposite, since
|
||||
// the tab you are labelling is the one you are about to leave alone for a while.
|
||||
|
||||
/// Attach the row's context menu and the right-click that opens it.
|
||||
fn buildRowMenu(self: *Window, tab: *Tab, anchor: *gtk.Box) void {
|
||||
_ = self;
|
||||
|
||||
const box = gtk.Box.new(.vertical, 2);
|
||||
box.as(gtk.Widget).addCssClass("playpen-row-menu");
|
||||
box.append(menuItem("Rename", &onMenuRename, tab));
|
||||
box.append(menuItem("Settings…", &onMenuSettings, tab));
|
||||
|
||||
tab.menu_popover.setChild(box.as(gtk.Widget));
|
||||
tab.menu_popover.setHasArrow(0);
|
||||
tab.menu_popover.as(gtk.Widget).setParent(anchor.as(gtk.Widget));
|
||||
|
||||
const secondary = gtk.GestureClick.new();
|
||||
secondary.as(gtk.GestureSingle).setButton(3);
|
||||
_ = gtk.GestureClick.signals.pressed.connect(
|
||||
secondary,
|
||||
*Tab,
|
||||
&onRowSecondary,
|
||||
tab,
|
||||
.{},
|
||||
);
|
||||
anchor.as(gtk.Widget).addController(secondary.as(gtk.EventController));
|
||||
}
|
||||
|
||||
/// One line of the row menu, styled like the layout menu's rows so the two
|
||||
/// popovers read as the same kind of thing.
|
||||
fn menuItem(
|
||||
text: [:0]const u8,
|
||||
handler: *const fn (*gtk.Button, *Tab) callconv(.c) void,
|
||||
tab: *Tab,
|
||||
) *gtk.Widget {
|
||||
const button = gtk.Button.newWithLabel(text);
|
||||
button.as(gtk.Widget).addCssClass("flat");
|
||||
button.setHasFrame(0);
|
||||
if (button.getChild()) |child| child.setHalign(.start);
|
||||
_ = gtk.Button.signals.clicked.connect(button, *Tab, handler, tab, .{});
|
||||
return button.as(gtk.Widget);
|
||||
}
|
||||
|
||||
/// Open the menu where the pointer is, rather than centred on the row: with one
|
||||
/// popover per row anchored to the whole row, a fixed position would put the
|
||||
/// menu somewhere you weren't pointing.
|
||||
fn onRowSecondary(
|
||||
_: *gtk.GestureClick,
|
||||
_: c_int,
|
||||
x: f64,
|
||||
y: f64,
|
||||
tab: *Tab,
|
||||
) callconv(.c) void {
|
||||
const at: gdk.Rectangle = .{
|
||||
.f_x = @intFromFloat(x),
|
||||
.f_y = @intFromFloat(y),
|
||||
.f_width = 1,
|
||||
.f_height = 1,
|
||||
};
|
||||
tab.menu_popover.setPointingTo(&at);
|
||||
tab.menu_popover.popup();
|
||||
}
|
||||
|
||||
fn onMenuRename(_: *gtk.Button, tab: *Tab) callconv(.c) void {
|
||||
tab.menu_popover.popdown();
|
||||
tab.window.beginRename(tab);
|
||||
}
|
||||
|
||||
fn onMenuSettings(_: *gtk.Button, tab: *Tab) callconv(.c) void {
|
||||
tab.menu_popover.popdown();
|
||||
tab.window.openTabSettings(tab);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Per-tab settings
|
||||
|
||||
/// Open the settings for one tab.
|
||||
///
|
||||
/// The tab is handed over as the dialog's opaque context and resolved again on
|
||||
/// the way back, so the dialog never holds a pointer into anything it owns. What
|
||||
/// it does hold is the tab itself, which is why `closeTab` closes it.
|
||||
fn openTabSettings(self: *Window, tab: *Tab) void {
|
||||
var buf: [128]u8 = undefined;
|
||||
|
||||
TabSettingsDialog.present(
|
||||
self.alloc,
|
||||
self.window.as(gtk.Window),
|
||||
.{
|
||||
.tab_name = self.tabName(tab, &buf),
|
||||
.emoji = tab.emoji,
|
||||
},
|
||||
&onTabEmojiChanged,
|
||||
tab,
|
||||
) catch |err| {
|
||||
std.log.err("failed to open tab settings: {s}", .{@errorName(err)});
|
||||
};
|
||||
}
|
||||
|
||||
/// The picker chose a glyph, or cleared the choice. The glyph is static, so
|
||||
/// there is nothing to copy and nothing to release.
|
||||
fn onTabEmojiChanged(ctx: ?*anyopaque, glyph: ?[:0]const u8) void {
|
||||
const tab: *Tab = @ptrCast(@alignCast(ctx.?));
|
||||
tab.emoji = glyph;
|
||||
tab.window.refreshLabel(tab);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Renaming
|
||||
//
|
||||
@@ -393,19 +530,10 @@ fn buildRename(self: *Window, tab: *Tab, anchor: *gtk.Box) void {
|
||||
tab.rename_popover.as(gtk.Widget).addCssClass("playpen-rename-popover");
|
||||
tab.rename_popover.as(gtk.Widget).setParent(anchor.as(gtk.Widget));
|
||||
|
||||
// Right-click is the discoverable route; double-click matches how tab
|
||||
// strips elsewhere behave. Both land in the same place.
|
||||
const secondary = gtk.GestureClick.new();
|
||||
secondary.as(gtk.GestureSingle).setButton(3);
|
||||
_ = gtk.GestureClick.signals.pressed.connect(
|
||||
secondary,
|
||||
*Tab,
|
||||
&onRowSecondary,
|
||||
tab,
|
||||
.{},
|
||||
);
|
||||
anchor.as(gtk.Widget).addController(secondary.as(gtk.EventController));
|
||||
|
||||
// Double-click still goes straight here, without passing through the menu:
|
||||
// it matches how tab strips elsewhere behave, and it is the shortcut worth
|
||||
// keeping for the one thing you rename a tab far more often than you
|
||||
// configure it.
|
||||
const double = gtk.GestureClick.new();
|
||||
double.as(gtk.GestureSingle).setButton(1);
|
||||
_ = gtk.GestureClick.signals.pressed.connect(
|
||||
@@ -456,16 +584,6 @@ fn onRenameActivate(_: *gtk.Entry, tab: *Tab) callconv(.c) void {
|
||||
self.refreshLabel(tab);
|
||||
}
|
||||
|
||||
fn onRowSecondary(
|
||||
_: *gtk.GestureClick,
|
||||
_: c_int,
|
||||
_: f64,
|
||||
_: f64,
|
||||
tab: *Tab,
|
||||
) callconv(.c) void {
|
||||
tab.window.beginRename(tab);
|
||||
}
|
||||
|
||||
fn onRowDoubleClick(
|
||||
_: *gtk.GestureClick,
|
||||
n_press: c_int,
|
||||
@@ -731,8 +849,12 @@ fn closeTab(self: *Window, tab: *Tab) void {
|
||||
|
||||
self.stack.remove(tab.view.widget());
|
||||
|
||||
// The dialog holds this tab as an opaque pointer, so it has to go first.
|
||||
TabSettingsDialog.closeFor(tab);
|
||||
|
||||
// A popover attached with setParent is not an ordinary child, so it has
|
||||
// to be detached by hand; letting the row take it down warns instead.
|
||||
tab.menu_popover.as(gtk.Widget).unparent();
|
||||
tab.rename_popover.as(gtk.Widget).unparent();
|
||||
|
||||
self.list.remove(tab.row.as(gtk.Widget));
|
||||
@@ -838,7 +960,19 @@ fn refreshLabel(self: *Window, tab: *Tab) void {
|
||||
|
||||
tab.label.setText(buf[0..text.len :0]);
|
||||
tab.label.as(gtk.Widget).setTooltipText(buf[0..text.len :0]);
|
||||
tab.icon.setFromIconName(tab.view.iconName());
|
||||
|
||||
// An emoji replaces the icon rather than joining it. The row has one slot
|
||||
// for "what is this tab", and filling it twice would spend twice the width
|
||||
// saying it once — width the label is short of already.
|
||||
if (tab.emoji) |glyph| {
|
||||
tab.emoji_label.setText(glyph);
|
||||
tab.emoji_label.as(gtk.Widget).setVisible(1);
|
||||
tab.icon.as(gtk.Widget).setVisible(0);
|
||||
} else {
|
||||
tab.emoji_label.as(gtk.Widget).setVisible(0);
|
||||
tab.icon.as(gtk.Widget).setVisible(1);
|
||||
tab.icon.setFromIconName(tab.view.iconName());
|
||||
}
|
||||
|
||||
self.refreshStatus(tab);
|
||||
}
|
||||
@@ -890,6 +1024,9 @@ fn onDestroy(_: *adw.ApplicationWindow, self: *Window) callconv(.c) void {
|
||||
// Each terminal owns a session, which owns a PTY and its child process.
|
||||
// Dropping them here reaps the children rather than orphaning them.
|
||||
for (self.tabs.items) |tab| {
|
||||
// As in closeTab: the tab settings dialog is not a child of this
|
||||
// window, so nothing else takes it down before the tab it points at.
|
||||
TabSettingsDialog.closeFor(tab);
|
||||
tab.view.destroy();
|
||||
if (tab.custom_name) |name| self.alloc.free(name);
|
||||
self.alloc.destroy(tab);
|
||||
|
||||
Reference in New Issue
Block a user