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
|
/// Emitted when the focus wants to be brought to the top and
|
||||||
/// focused.
|
/// focused.
|
||||||
pub const @"present-request" = struct {
|
pub const @"present-request" = struct {
|
||||||
|
|
@ -462,6 +474,7 @@ pub const Surface = extern struct {
|
||||||
|
|
||||||
// Template binds
|
// Template binds
|
||||||
child_exited_overlay: *ChildExited,
|
child_exited_overlay: *ChildExited,
|
||||||
|
context_menu: *gtk.PopoverMenu,
|
||||||
drop_target: *gtk.DropTarget,
|
drop_target: *gtk.DropTarget,
|
||||||
progress_bar_overlay: *gtk.ProgressBar,
|
progress_bar_overlay: *gtk.ProgressBar,
|
||||||
|
|
||||||
|
|
@ -1473,6 +1486,16 @@ pub const Surface = extern struct {
|
||||||
self.close(.{ .surface = false });
|
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(
|
fn dtDrop(
|
||||||
_: *gtk.DropTarget,
|
_: *gtk.DropTarget,
|
||||||
value: *gobject.Value,
|
value: *gobject.Value,
|
||||||
|
|
@ -1647,9 +1670,9 @@ pub const Surface = extern struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Report the event
|
// Report the event
|
||||||
|
const button = translateMouseButton(gesture.as(gtk.GestureSingle).getCurrentButton());
|
||||||
const consumed = if (priv.core_surface) |surface| consumed: {
|
const consumed = if (priv.core_surface) |surface| consumed: {
|
||||||
const gtk_mods = event.getModifierState();
|
const gtk_mods = event.getModifierState();
|
||||||
const button = translateMouseButton(gesture.as(gtk.GestureSingle).getCurrentButton());
|
|
||||||
const mods = gtk_key.translateMods(gtk_mods);
|
const mods = gtk_key.translateMods(gtk_mods);
|
||||||
break :consumed surface.mouseButtonCallback(
|
break :consumed surface.mouseButtonCallback(
|
||||||
.press,
|
.press,
|
||||||
|
|
@ -1661,10 +1684,28 @@ pub const Surface = extern struct {
|
||||||
};
|
};
|
||||||
} else false;
|
} else false;
|
||||||
|
|
||||||
// TODO: context menu
|
// If a right click isn't consumed, mouseButtonCallback selects the hovered
|
||||||
_ = consumed;
|
// word and returns false. We can use this to handle the context menu
|
||||||
_ = x;
|
// opening under normal scenarios.
|
||||||
_ = y;
|
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(
|
fn gcMouseUp(
|
||||||
|
|
@ -2259,6 +2300,7 @@ pub const Surface = extern struct {
|
||||||
class.bindTemplateChildPrivate("url_left", .{});
|
class.bindTemplateChildPrivate("url_left", .{});
|
||||||
class.bindTemplateChildPrivate("url_right", .{});
|
class.bindTemplateChildPrivate("url_right", .{});
|
||||||
class.bindTemplateChildPrivate("child_exited_overlay", .{});
|
class.bindTemplateChildPrivate("child_exited_overlay", .{});
|
||||||
|
class.bindTemplateChildPrivate("context_menu", .{});
|
||||||
class.bindTemplateChildPrivate("progress_bar_overlay", .{});
|
class.bindTemplateChildPrivate("progress_bar_overlay", .{});
|
||||||
class.bindTemplateChildPrivate("resize_overlay", .{});
|
class.bindTemplateChildPrivate("resize_overlay", .{});
|
||||||
class.bindTemplateChildPrivate("drop_target", .{});
|
class.bindTemplateChildPrivate("drop_target", .{});
|
||||||
|
|
@ -2288,6 +2330,7 @@ pub const Surface = extern struct {
|
||||||
class.bindTemplateCallback("url_mouse_enter", &ecUrlMouseEnter);
|
class.bindTemplateCallback("url_mouse_enter", &ecUrlMouseEnter);
|
||||||
class.bindTemplateCallback("url_mouse_leave", &ecUrlMouseLeave);
|
class.bindTemplateCallback("url_mouse_leave", &ecUrlMouseLeave);
|
||||||
class.bindTemplateCallback("child_exited_close", &childExitedClose);
|
class.bindTemplateCallback("child_exited_close", &childExitedClose);
|
||||||
|
class.bindTemplateCallback("context_menu_closed", &contextMenuClosed);
|
||||||
class.bindTemplateCallback("notify_config", &propConfig);
|
class.bindTemplateCallback("notify_config", &propConfig);
|
||||||
class.bindTemplateCallback("notify_mouse_hover_url", &propMouseHoverUrl);
|
class.bindTemplateCallback("notify_mouse_hover_url", &propMouseHoverUrl);
|
||||||
class.bindTemplateCallback("notify_mouse_hidden", &propMouseHidden);
|
class.bindTemplateCallback("notify_mouse_hidden", &propMouseHidden);
|
||||||
|
|
@ -2315,6 +2358,7 @@ pub const Surface = extern struct {
|
||||||
signals.@"clipboard-read".impl.register(.{});
|
signals.@"clipboard-read".impl.register(.{});
|
||||||
signals.@"clipboard-write".impl.register(.{});
|
signals.@"clipboard-write".impl.register(.{});
|
||||||
signals.init.impl.register(.{});
|
signals.init.impl.register(.{});
|
||||||
|
signals.menu.impl.register(.{});
|
||||||
signals.@"present-request".impl.register(.{});
|
signals.@"present-request".impl.register(.{});
|
||||||
signals.@"toggle-fullscreen".impl.register(.{});
|
signals.@"toggle-fullscreen".impl.register(.{});
|
||||||
signals.@"toggle-maximize".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 {
|
fn toggleCssClass(self: *Self, class: [:0]const u8, value: bool) void {
|
||||||
const widget = self.as(gtk.Widget);
|
const widget = self.as(gtk.Widget);
|
||||||
if (value)
|
if (value)
|
||||||
|
|
@ -845,23 +866,7 @@ pub const Window = extern struct {
|
||||||
const active = button.getActive() != 0;
|
const active = button.getActive() != 0;
|
||||||
if (!active) return;
|
if (!active) return;
|
||||||
|
|
||||||
const has_selection = selection: {
|
self.syncActions();
|
||||||
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 propQuickTerminal(
|
fn propQuickTerminal(
|
||||||
|
|
@ -1210,6 +1215,13 @@ pub const Window = extern struct {
|
||||||
self,
|
self,
|
||||||
.{},
|
.{},
|
||||||
);
|
);
|
||||||
|
_ = Surface.signals.menu.connect(
|
||||||
|
surface,
|
||||||
|
*Self,
|
||||||
|
surfaceMenu,
|
||||||
|
self,
|
||||||
|
.{},
|
||||||
|
);
|
||||||
_ = Surface.signals.@"toggle-fullscreen".connect(
|
_ = Surface.signals.@"toggle-fullscreen".connect(
|
||||||
surface,
|
surface,
|
||||||
*Self,
|
*Self,
|
||||||
|
|
@ -1355,6 +1367,13 @@ pub const Window = extern struct {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn surfaceMenu(
|
||||||
|
_: *Surface,
|
||||||
|
self: *Self,
|
||||||
|
) callconv(.c) void {
|
||||||
|
self.syncActions();
|
||||||
|
}
|
||||||
|
|
||||||
fn surfacePresentRequest(
|
fn surfacePresentRequest(
|
||||||
surface: *Surface,
|
surface: *Surface,
|
||||||
self: *Self,
|
self: *Self,
|
||||||
|
|
|
||||||
|
|
@ -15,19 +15,32 @@ template $GhosttySurface: Adw.Bin {
|
||||||
focusable: false;
|
focusable: false;
|
||||||
focus-on-click: false;
|
focus-on-click: false;
|
||||||
|
|
||||||
GLArea gl_area {
|
child: Box {
|
||||||
realize => $gl_realize();
|
|
||||||
unrealize => $gl_unrealize();
|
|
||||||
render => $gl_render();
|
|
||||||
resize => $gl_resize();
|
|
||||||
hexpand: true;
|
hexpand: true;
|
||||||
vexpand: true;
|
vexpand: true;
|
||||||
focusable: true;
|
|
||||||
focus-on-click: true;
|
GLArea gl_area {
|
||||||
has-stencil-buffer: false;
|
realize => $gl_realize();
|
||||||
has-depth-buffer: false;
|
unrealize => $gl_unrealize();
|
||||||
use-es: false;
|
render => $gl_render();
|
||||||
}
|
resize => $gl_resize();
|
||||||
|
hexpand: true;
|
||||||
|
vexpand: true;
|
||||||
|
focusable: true;
|
||||||
|
focus-on-click: true;
|
||||||
|
has-stencil-buffer: false;
|
||||||
|
has-depth-buffer: false;
|
||||||
|
use-es: false;
|
||||||
|
}
|
||||||
|
|
||||||
|
PopoverMenu context_menu {
|
||||||
|
closed => $context_menu_closed();
|
||||||
|
menu-model: context_menu_model;
|
||||||
|
flags: nested;
|
||||||
|
halign: start;
|
||||||
|
has-arrow: false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
[overlay]
|
[overlay]
|
||||||
ProgressBar progress_bar_overlay {
|
ProgressBar progress_bar_overlay {
|
||||||
|
|
@ -122,3 +135,104 @@ IMMulticontext im_context {
|
||||||
preedit-end => $im_preedit_end();
|
preedit-end => $im_preedit_end();
|
||||||
commit => $im_commit();
|
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;
|
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"
|
/// Refresh the menu. Right now that means enabling/disabling the "Copy"
|
||||||
/// menu item based on whether there is an active selection or not, but
|
/// menu item based on whether there is an active selection or not, but
|
||||||
/// that may change in the future.
|
/// that may change in the future.
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue