apprt/gtk-ng: surface context menu (#8144)
Port with changes: * Utilizes the Surface blueprint for defining the `PopoverMenu` * We can't attach it directly to the Overlay using blueprints because an overlay can only have a single child property and you can't see other children via Blueprint. To overcome this, use a `Box` * Utilizing a `menu` signal the window can listen to to refresh its action map instead of digging into ancestor hierarchy.pull/8146/head
commit
84cb4ce31a
|
|
@ -327,6 +327,18 @@ pub const Surface = extern struct {
|
|||
);
|
||||
};
|
||||
|
||||
/// Emitted just prior to the context menu appearing.
|
||||
pub const menu = struct {
|
||||
pub const name = "menu";
|
||||
pub const connect = impl.connect;
|
||||
const impl = gobject.ext.defineSignal(
|
||||
name,
|
||||
Self,
|
||||
&.{},
|
||||
void,
|
||||
);
|
||||
};
|
||||
|
||||
/// Emitted when the focus wants to be brought to the top and
|
||||
/// focused.
|
||||
pub const @"present-request" = struct {
|
||||
|
|
@ -462,6 +474,7 @@ pub const Surface = extern struct {
|
|||
|
||||
// Template binds
|
||||
child_exited_overlay: *ChildExited,
|
||||
context_menu: *gtk.PopoverMenu,
|
||||
drop_target: *gtk.DropTarget,
|
||||
progress_bar_overlay: *gtk.ProgressBar,
|
||||
|
||||
|
|
@ -1473,6 +1486,16 @@ pub const Surface = extern struct {
|
|||
self.close(.{ .surface = false });
|
||||
}
|
||||
|
||||
fn contextMenuClosed(
|
||||
_: *gtk.PopoverMenu,
|
||||
self: *Self,
|
||||
) callconv(.c) void {
|
||||
// When the context menu closes, it moves focus back to the tab
|
||||
// bar if there are tabs. That's not correct. We need to grab it
|
||||
// on the surface.
|
||||
self.grabFocus();
|
||||
}
|
||||
|
||||
fn dtDrop(
|
||||
_: *gtk.DropTarget,
|
||||
value: *gobject.Value,
|
||||
|
|
@ -1647,9 +1670,9 @@ pub const Surface = extern struct {
|
|||
}
|
||||
|
||||
// Report the event
|
||||
const button = translateMouseButton(gesture.as(gtk.GestureSingle).getCurrentButton());
|
||||
const consumed = if (priv.core_surface) |surface| consumed: {
|
||||
const gtk_mods = event.getModifierState();
|
||||
const button = translateMouseButton(gesture.as(gtk.GestureSingle).getCurrentButton());
|
||||
const mods = gtk_key.translateMods(gtk_mods);
|
||||
break :consumed surface.mouseButtonCallback(
|
||||
.press,
|
||||
|
|
@ -1661,10 +1684,28 @@ pub const Surface = extern struct {
|
|||
};
|
||||
} else false;
|
||||
|
||||
// TODO: context menu
|
||||
_ = consumed;
|
||||
_ = x;
|
||||
_ = y;
|
||||
// If a right click isn't consumed, mouseButtonCallback selects the hovered
|
||||
// word and returns false. We can use this to handle the context menu
|
||||
// opening under normal scenarios.
|
||||
if (!consumed and button == .right) {
|
||||
signals.menu.impl.emit(
|
||||
self,
|
||||
null,
|
||||
.{},
|
||||
null,
|
||||
);
|
||||
|
||||
const rect: gdk.Rectangle = .{
|
||||
.f_x = @intFromFloat(x),
|
||||
.f_y = @intFromFloat(y),
|
||||
.f_width = 1,
|
||||
.f_height = 1,
|
||||
};
|
||||
|
||||
const popover = priv.context_menu.as(gtk.Popover);
|
||||
popover.setPointingTo(&rect);
|
||||
popover.popup();
|
||||
}
|
||||
}
|
||||
|
||||
fn gcMouseUp(
|
||||
|
|
@ -2259,6 +2300,7 @@ pub const Surface = extern struct {
|
|||
class.bindTemplateChildPrivate("url_left", .{});
|
||||
class.bindTemplateChildPrivate("url_right", .{});
|
||||
class.bindTemplateChildPrivate("child_exited_overlay", .{});
|
||||
class.bindTemplateChildPrivate("context_menu", .{});
|
||||
class.bindTemplateChildPrivate("progress_bar_overlay", .{});
|
||||
class.bindTemplateChildPrivate("resize_overlay", .{});
|
||||
class.bindTemplateChildPrivate("drop_target", .{});
|
||||
|
|
@ -2288,6 +2330,7 @@ pub const Surface = extern struct {
|
|||
class.bindTemplateCallback("url_mouse_enter", &ecUrlMouseEnter);
|
||||
class.bindTemplateCallback("url_mouse_leave", &ecUrlMouseLeave);
|
||||
class.bindTemplateCallback("child_exited_close", &childExitedClose);
|
||||
class.bindTemplateCallback("context_menu_closed", &contextMenuClosed);
|
||||
class.bindTemplateCallback("notify_config", &propConfig);
|
||||
class.bindTemplateCallback("notify_mouse_hover_url", &propMouseHoverUrl);
|
||||
class.bindTemplateCallback("notify_mouse_hidden", &propMouseHidden);
|
||||
|
|
@ -2315,6 +2358,7 @@ pub const Surface = extern struct {
|
|||
signals.@"clipboard-read".impl.register(.{});
|
||||
signals.@"clipboard-write".impl.register(.{});
|
||||
signals.init.impl.register(.{});
|
||||
signals.menu.impl.register(.{});
|
||||
signals.@"present-request".impl.register(.{});
|
||||
signals.@"toggle-fullscreen".impl.register(.{});
|
||||
signals.@"toggle-maximize".impl.register(.{});
|
||||
|
|
|
|||
|
|
@ -590,6 +590,27 @@ pub const Window = extern struct {
|
|||
};
|
||||
}
|
||||
|
||||
/// 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)
|
||||
|
|
@ -845,23 +866,7 @@ pub const Window = extern struct {
|
|||
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));
|
||||
self.syncActions();
|
||||
}
|
||||
|
||||
fn propQuickTerminal(
|
||||
|
|
@ -1210,6 +1215,13 @@ pub const Window = extern struct {
|
|||
self,
|
||||
.{},
|
||||
);
|
||||
_ = Surface.signals.menu.connect(
|
||||
surface,
|
||||
*Self,
|
||||
surfaceMenu,
|
||||
self,
|
||||
.{},
|
||||
);
|
||||
_ = Surface.signals.@"toggle-fullscreen".connect(
|
||||
surface,
|
||||
*Self,
|
||||
|
|
@ -1355,6 +1367,13 @@ pub const Window = extern struct {
|
|||
}
|
||||
}
|
||||
|
||||
fn surfaceMenu(
|
||||
_: *Surface,
|
||||
self: *Self,
|
||||
) callconv(.c) void {
|
||||
self.syncActions();
|
||||
}
|
||||
|
||||
fn surfacePresentRequest(
|
||||
surface: *Surface,
|
||||
self: *Self,
|
||||
|
|
|
|||
|
|
@ -15,6 +15,10 @@ template $GhosttySurface: Adw.Bin {
|
|||
focusable: false;
|
||||
focus-on-click: false;
|
||||
|
||||
child: Box {
|
||||
hexpand: true;
|
||||
vexpand: true;
|
||||
|
||||
GLArea gl_area {
|
||||
realize => $gl_realize();
|
||||
unrealize => $gl_unrealize();
|
||||
|
|
@ -29,6 +33,15 @@ template $GhosttySurface: Adw.Bin {
|
|||
use-es: false;
|
||||
}
|
||||
|
||||
PopoverMenu context_menu {
|
||||
closed => $context_menu_closed();
|
||||
menu-model: context_menu_model;
|
||||
flags: nested;
|
||||
halign: start;
|
||||
has-arrow: false;
|
||||
}
|
||||
};
|
||||
|
||||
[overlay]
|
||||
ProgressBar progress_bar_overlay {
|
||||
styles [
|
||||
|
|
@ -122,3 +135,104 @@ IMMulticontext im_context {
|
|||
preedit-end => $im_preedit_end();
|
||||
commit => $im_commit();
|
||||
}
|
||||
|
||||
menu context_menu_model {
|
||||
section {
|
||||
item {
|
||||
label: _("Copy");
|
||||
action: "win.copy";
|
||||
}
|
||||
|
||||
item {
|
||||
label: _("Paste");
|
||||
action: "win.paste";
|
||||
}
|
||||
}
|
||||
|
||||
section {
|
||||
item {
|
||||
label: _("Clear");
|
||||
action: "win.clear";
|
||||
}
|
||||
|
||||
item {
|
||||
label: _("Reset");
|
||||
action: "win.reset";
|
||||
}
|
||||
}
|
||||
|
||||
section {
|
||||
submenu {
|
||||
label: _("Split");
|
||||
|
||||
item {
|
||||
label: _("Change Title…");
|
||||
action: "win.prompt-title";
|
||||
}
|
||||
|
||||
item {
|
||||
label: _("Split Up");
|
||||
action: "win.split-up";
|
||||
}
|
||||
|
||||
item {
|
||||
label: _("Split Down");
|
||||
action: "win.split-down";
|
||||
}
|
||||
|
||||
item {
|
||||
label: _("Split Left");
|
||||
action: "win.split-left";
|
||||
}
|
||||
|
||||
item {
|
||||
label: _("Split Right");
|
||||
action: "win.split-right";
|
||||
}
|
||||
}
|
||||
|
||||
submenu {
|
||||
label: _("Tab");
|
||||
|
||||
item {
|
||||
label: _("New Tab");
|
||||
action: "win.new-tab";
|
||||
}
|
||||
|
||||
item {
|
||||
label: _("Close Tab");
|
||||
action: "win.close-tab";
|
||||
}
|
||||
}
|
||||
|
||||
submenu {
|
||||
label: _("Window");
|
||||
|
||||
item {
|
||||
label: _("New Window");
|
||||
action: "win.new-window";
|
||||
}
|
||||
|
||||
item {
|
||||
label: _("Close Window");
|
||||
action: "win.close";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
section {
|
||||
submenu {
|
||||
label: _("Config");
|
||||
|
||||
item {
|
||||
label: _("Open Configuration");
|
||||
action: "app.open-config";
|
||||
}
|
||||
|
||||
item {
|
||||
label: _("Reload Configuration");
|
||||
action: "app.reload-config";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -82,10 +82,6 @@ pub fn Menu(
|
|||
return self.menu_widget.as(gtk.Widget).getVisible() != 0;
|
||||
}
|
||||
|
||||
pub fn setVisible(self: *const Self, visible: bool) void {
|
||||
self.menu_widget.as(gtk.Widget).setVisible(@intFromBool(visible));
|
||||
}
|
||||
|
||||
/// Refresh the menu. Right now that means enabling/disabling the "Copy"
|
||||
/// menu item based on whether there is an active selection or not, but
|
||||
/// that may change in the future.
|
||||
|
|
|
|||
Loading…
Reference in New Issue