Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions graphcode-windows/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,26 @@ the selected loop in the graph rather than opening its terminal. Native forms
keep their own text editing, Tab navigation, and acceptance/cancellation;
Enter in an already-open menu activates its highlighted item.

Help > **Keyboard Shortcuts** lists the remaining keyboard routes: sending a
loop, context-sensitive `Ctrl+E`, project or node identity navigation,
worktree-row selection, terminal focus, clone cancellation, terminal paste,
focused-toolbar navigation, and jump-palette navigation. The Loop, View, and
File menu items also show `Ctrl+Shift+G` for Show in Graph, `Ctrl+R` for
Reconnect, and `Ctrl+Shift+P` for Project Worktree Policy. `Ctrl+P` remains
unlisted because it duplicates the `Ctrl+J` jump-palette route;
`Ctrl+Shift+I` remains unlisted because it duplicates the `Ctrl+Shift+W`
Worktrees route. `Ctrl+Shift+C` is shown only for Clone Repository: the
terminal context also uses it to copy a selection, so Help does not present
the context-dependent collision as a second shortcut.

Canvas and pointer actions still have no visible gesture hints: clicking
selects/opens canvas items; dragging blank canvas pans, dragging a node moves
it, and dragging a connector to another node creates an edge; wheel and
touchscreen pinch zoom the canvas; right-click opens item-specific context
menus; and dragging a top-level sidebar loop reorders it. Closing this gap
requires hints in the owning `GraphCanvas.zig` and `Sidebar.zig` rendering
surfaces, both outside this change's scope.

Custom canvas, sidebar, header, and detail layout use 96-DPI logical units.
UI Automation receives physical client pixels: logical bounds, including the
fixed graph group, are scaled once at the reporting boundary. Terminal tab and
Expand Down
79 changes: 76 additions & 3 deletions graphcode-windows/src/MainWindow.zig
Original file line number Diff line number Diff line change
Expand Up @@ -521,6 +521,7 @@ pub fn installMenu(hwnd: c.HWND) !void {
const terminal = c.CreatePopupMenu() orelse return error.MenuCreationFailed;
const view = c.CreatePopupMenu() orelse return error.MenuCreationFailed;
const help = c.CreatePopupMenu() orelse return error.MenuCreationFailed;
const discovery = c.CreatePopupMenu() orelse return error.MenuCreationFailed;
const workspace = c.CreatePopupMenu() orelse return error.MenuCreationFailed;

append(add_folder, "Open Folder...\tCtrl+O", @intFromEnum(Command.open_folder));
Expand All @@ -537,7 +538,7 @@ pub fn installMenu(hwnd: c.HWND) !void {
append(file, "Worktrees...\tCtrl+Shift+W", @intFromEnum(Command.worktrees));
append(file, "Reclaim Selected Worktrees...", @intFromEnum(Command.reclaim_worktrees));
append(file, "Reveal Selected Worktree in Explorer\tCtrl+Shift+E", @intFromEnum(Command.reveal_worktree));
append(file, "Project Worktree Policy...", @intFromEnum(Command.edit_worktree_policy));
append(file, "Project Worktree Policy...\tCtrl+Shift+P", @intFromEnum(Command.edit_worktree_policy));
append(file, "Save Worktree Policy\tCtrl+Shift+S", @intFromEnum(Command.save_worktree_policy));
separator(file);
append(file, "Exit", @intFromEnum(Command.exit));
Expand All @@ -550,7 +551,7 @@ pub fn installMenu(hwnd: c.HWND) !void {
separator(loop);
append(loop, "New Loop...\tCtrl+N", @intFromEnum(Command.create_node));
append(loop, "Create Edge...", @intFromEnum(Command.create_edge));
append(loop, "Show in Graph", @intFromEnum(Command.show_graph));
append(loop, "Show in Graph\tCtrl+Shift+G", @intFromEnum(Command.show_graph));
append(loop, "Stop Loop\tCtrl+S", @intFromEnum(Command.stop_loop));

append(terminal, "New Tab\tCtrl+T", @intFromEnum(Command.new_tab));
Expand All @@ -575,13 +576,24 @@ pub fn installMenu(hwnd: c.HWND) !void {
append(view, "Zoom In\tCtrl+=", @intFromEnum(Command.zoom_in));
append(view, "Fit Canvas\tCtrl+9", @intFromEnum(Command.fit_canvas));
separator(view);
append(view, "Reconnect", @intFromEnum(Command.reconnect));
append(view, "Reconnect\tCtrl+R", @intFromEnum(Command.reconnect));
append(view, "Settings...\tCtrl+Shift+,", @intFromEnum(Command.product_settings));
append(view, "Advanced Connection Settings...\tCtrl+,", @intFromEnum(Command.settings));
append(help, "GraphCode Basics\tF1", @intFromEnum(Command.onboarding));
append(help, "Check for Updates...", @intFromEnum(Command.check_updates));
separator(help);
append(help, "About GraphCode", @intFromEnum(Command.about));
appendInfo(discovery, "Send selected loop\tCtrl+M");
appendInfo(discovery, "Rename selected loop / edit selected edge\tCtrl+E");
appendInfo(discovery, "Navigate by project or node\tCtrl+Up / Ctrl+Down");
appendInfo(discovery, "Select a worktree row\tUp / Down");
appendInfo(discovery, "Focus Terminal A\t1");
appendInfo(discovery, "Focus Terminal B\t2");
appendInfo(discovery, "Cancel clone\tCtrl+Shift+X");
appendInfo(discovery, "Paste terminal text\tCtrl+Shift+V");
appendInfo(discovery, "Focused toolbar: Tab / arrows / Home / End move; Enter / Space activate; Esc exits");
appendInfo(discovery, "Jump palette: Up / Down navigate; Enter opens the selected loop");
appendPopup(help, "Keyboard Shortcuts", discovery);

append(workspace, "New Workspace...", @intFromEnum(Command.workspace_new));
append(workspace, "Manage Workspaces...", @intFromEnum(Command.workspace_manage));
Expand Down Expand Up @@ -754,6 +766,10 @@ fn append(menu: c.HMENU, text: []const u8, id: usize) void {
appendEnabled(menu, text, id, true);
}

fn appendInfo(menu: c.HMENU, text: []const u8) void {
appendEnabled(menu, text, 0, false);
}

fn appendEnabled(menu: c.HMENU, text: []const u8, id: usize, enabled: bool) void {
const wide = toWideZ(std.heap.c_allocator, text) catch return;
defer std.heap.c_allocator.free(wide);
Expand Down Expand Up @@ -888,6 +904,63 @@ test "native menu exposes the parity command groups" {
try std.testing.expectEqual(@as(?Command, null), commandFromId(9999));
}

test "main and help menus expose missing shortcuts without canvas gesture claims" {
const hwnd = try hiddenWorkspaceTestWindow();
defer _ = c.DestroyWindow(hwnd);
try installMenu(hwnd);
const root = c.GetMenu(hwnd);
const file = c.GetSubMenu(root, 0);
const loop = c.GetSubMenu(root, 1);
const view = c.GetSubMenu(root, 4);
const help = c.GetSubMenu(root, 5);

const actual_labels = [_]struct { menu: c.HMENU, command: Command, expected: []const u8 }{
.{ .menu = file, .command = .edit_worktree_policy, .expected = "Project Worktree Policy...\tCtrl+Shift+P" },
.{ .menu = loop, .command = .show_graph, .expected = "Show in Graph\tCtrl+Shift+G" },
.{ .menu = view, .command = .reconnect, .expected = "Reconnect\tCtrl+R" },
};
for (actual_labels) |item| {
var label: [128]u16 = undefined;
const length = c.GetMenuStringW(item.menu, @intFromEnum(item.command), &label, label.len, c.MF_BYCOMMAND);
try std.testing.expect(length > 0 and length < label.len - 1);
const actual = try std.unicode.utf16LeToUtf8Alloc(std.testing.allocator, label[0..@intCast(length)]);
defer std.testing.allocator.free(actual);
try std.testing.expectEqualStrings(item.expected, actual);
}

const guide = c.GetSubMenu(help, 4);
try std.testing.expect(guide != null);
const expected = [_][]const u8{
"Send selected loop\tCtrl+M",
"Rename selected loop / edit selected edge\tCtrl+E",
"Navigate by project or node\tCtrl+Up / Ctrl+Down",
"Select a worktree row\tUp / Down",
"Focus Terminal A\t1",
"Focus Terminal B\t2",
"Cancel clone\tCtrl+Shift+X",
"Paste terminal text\tCtrl+Shift+V",
"Focused toolbar: Tab / arrows / Home / End move; Enter / Space activate; Esc exits",
"Jump palette: Up / Down navigate; Enter opens the selected loop",
};
try std.testing.expectEqual(@as(c_int, expected.len), c.GetMenuItemCount(guide));
var guide_title: [128]u16 = undefined;
const title_length = c.GetMenuStringW(help, 4, &guide_title, guide_title.len, c.MF_BYPOSITION);
try std.testing.expect(title_length > 0 and title_length < guide_title.len - 1);
const actual_title = try std.unicode.utf16LeToUtf8Alloc(std.testing.allocator, guide_title[0..@intCast(title_length)]);
defer std.testing.allocator.free(actual_title);
try std.testing.expectEqualStrings("Keyboard Shortcuts", actual_title);
for (expected, 0..) |expected_label, expected_index| {
const index: c.UINT = @intCast(expected_index);
var label: [128]u16 = undefined;
const length = c.GetMenuStringW(guide, index, &label, label.len, c.MF_BYPOSITION);
try std.testing.expect(length > 0 and length < label.len - 1);
const actual = try std.unicode.utf16LeToUtf8Alloc(std.testing.allocator, label[0..@intCast(length)]);
defer std.testing.allocator.free(actual);
try std.testing.expectEqualStrings(expected_label, actual);
try std.testing.expect(c.GetMenuState(guide, index, c.MF_BYPOSITION) & c.MF_GRAYED != 0);
}
}

const NativeMenuDispatchTest = struct {
const name = std.unicode.utf8ToUtf16LeStringLiteral("GraphCodeNativeF10DispatchTest");
var menu_commands: usize = 0;
Expand Down
Loading
Loading