Skip to content

Commit bf233ff

Browse files
committed
frontend/latex: default vs new layout, document command type interface to clarify what is what, precedence, etc.
1 parent 92f50c4 commit bf233ff

File tree

3 files changed

+201
-12
lines changed

3 files changed

+201
-12
lines changed

src/packages/frontend/frame-editors/frame-tree/commands/generic-commands.tsx

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1320,6 +1320,39 @@ addCommands({
13201320
defaultMessage: "Default",
13211321
}),
13221322
},
1323+
new_layout: {
1324+
icon: "layout",
1325+
group: "frame_types",
1326+
title: defineMessage({
1327+
id: "command.generic.new_layout.title",
1328+
defaultMessage:
1329+
"Switch to a simplified layout with LaTeX source editor and multi-purpose output panel",
1330+
}),
1331+
label: defineMessage({
1332+
id: "command.generic.new_layout.label",
1333+
defaultMessage: "New Layout",
1334+
}),
1335+
button: defineMessage({
1336+
id: "command.generic.new_layout.button",
1337+
defaultMessage: "New",
1338+
}),
1339+
isVisible: ({ props }) =>
1340+
typeof props.actions?._new_latex_frame_tree === "function",
1341+
onClick: ({ props }) => {
1342+
try {
1343+
// Check if this is a LaTeX editor and use its specific layout method
1344+
if (
1345+
props.actions._new_latex_frame_tree &&
1346+
props.actions.replace_frame_tree_with_custom
1347+
) {
1348+
const tree = props.actions._new_latex_frame_tree();
1349+
props.actions.replace_frame_tree_with_custom(tree);
1350+
}
1351+
} catch (error) {
1352+
console.error("Error in New Layout:", error);
1353+
}
1354+
},
1355+
},
13231356
button_bar: {
13241357
alwaysShow: true,
13251358
icon: "tool",

src/packages/frontend/frame-editors/frame-tree/commands/types.ts

Lines changed: 122 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,67 +6,178 @@
66
import type { ReactNode } from "react";
77

88
import { IconName, IconRotation } from "@cocalc/frontend/components/icon";
9+
import { StudentProjectFunctionality } from "@cocalc/util/db-schema/projects";
910
import { IntlMessage } from "@cocalc/frontend/i18n";
1011
import type { ManageCommands } from "./manage";
1112
import { MENUS } from "./menus";
1213

14+
// Specification for a single menu in the frame title bar.
1315
interface MenuSpec {
16+
// Display label for the menu. Can be translated text or special APPLICATION_MENU constant
1417
label: IntlMessage | string;
18+
19+
// Position for menu ordering. Lower numbers appear first
1520
pos: number;
21+
22+
// Array of group names that belong to this menu. Groups contain the actual commands.
1623
groups: string[];
1724
}
1825

26+
/**
27+
* Collection of all menu specifications. Menus are registered via addMenus()
28+
* in generic-menus.ts and define the top-level dropdown structure in frame title bars.
29+
* Examples: "file", "edit", "view", "go", "help", "app"
30+
*/
1931
export interface Menus {
2032
[name: string]: MenuSpec;
2133
}
2234

2335
export type Group = (typeof MENUS)[keyof typeof MENUS]["groups"][number];
2436

37+
/**
38+
* Function signature for command click handlers. Receives the ManageCommands
39+
* instance (providing access to frame context, props, etc.) and an optional
40+
* event object from the UI interaction.
41+
*/
2542
export type OnClick = (opts: ManageCommands & { event? }) => void;
2643

44+
/**
45+
* Configuration options for Ant Design Popconfirm modal dialogs.
46+
* Used to show confirmation prompts before executing potentially destructive commands.
47+
* All fields support internationalization via IntlMessage.
48+
*/
2749
interface PopconfirmOpts {
50+
// Main title text shown at the top of the confirmation dialog
2851
title?: string | IntlMessage | React.JSX.Element;
52+
// Detailed description explaining the action and its consequences
2953
description?: string | IntlMessage | React.JSX.Element;
54+
// Text for the confirmation button (defaults to "OK")
3055
okText?: string | IntlMessage;
56+
// Text for the cancel button (defaults to "Cancel")
3157
cancelText?: string | IntlMessage;
3258
}
3359

60+
/**
61+
* Defines a command that can appear in frame editor menus, toolbars, and buttons.
62+
* For example, split frame, zoom, save, etc.
63+
*
64+
* Commands are organized into menus via groups.
65+
* Each editor (LaTeX, code, etc.) can include specific commands in their EditorDescription.commands.
66+
*
67+
* The ManageCommands class handles visibility, rendering, and execution of commands
68+
* based on the current frame context, user permissions, and editor specifications.
69+
*/
3470
export interface Command {
35-
// group -- inside of a menu
71+
/**
72+
* The menu group this command belongs to.
73+
* Commands are organized into groups for logical menu structure.
74+
*/
3675
group: Group;
37-
name?: string; //not used
38-
// position, for sorting
76+
77+
/**
78+
* Used in menu construction helpers (e.g., addEditorMenus) to identify
79+
* commands within the menu system. Not used in the final Command objects
80+
* that get registered, which are identified by their key in the commands object.
81+
*/
82+
name?: string;
83+
84+
/**
85+
* Position within the group for sorting. Lower numbers appear first.
86+
* Used by ManageCommands.getAllCommandPositions() to determine display order
87+
* in menus and toolbars. Defaults to 1e6 (very high) if not specified.
88+
*/
3989
pos?: number;
90+
91+
// Tooltip text shown when hovering over the command
4092
title?: ReactNode | ((opts: ManageCommands) => ReactNode) | IntlMessage;
93+
94+
// Icon to display for this command.
4195
icon?: IconName | ReactNode | ((opts: ManageCommands) => ReactNode);
96+
97+
// Rotation angle for the icon in degrees. Only applies when icon is an IconName.
4298
iconRotate?: IconRotation;
99+
100+
// Label for button bar buttons. Separate from 'label' to allow different text in menus vs. compact button bar.
43101
button?: ReactNode | ((opts: ManageCommands) => ReactNode) | IntlMessage;
102+
103+
// unclear?
44104
//color?: string | ((opts: ManageCommands) => string);
105+
106+
// Primary label for the command in menus.
45107
label?: ReactNode | ((opts: ManageCommands) => ReactNode) | IntlMessage;
46-
// If onClick is NOT set, then editor_actions[name] must be defined
47-
// and be a function that takes the frame id as input.
108+
109+
/**
110+
* Click handler for the command. If not provided, the system falls back to
111+
* calling props.actions[name](props.id) where 'name' is the command key.
112+
* Receives ManageCommands context plus an optional 'event' parameter.
113+
*/
48114
onClick?: OnClick;
49-
// isVisible: if a function, determine visibility based on that.
50-
// if a string, use editor spec for given frame.
115+
116+
/**
117+
* Controls command visibility.
118+
* Used to conditionally show commands based on frame type, file type, etc.
119+
* Can be a string (references another command name in the editor spec) or a function
120+
* NOTE: Completely ignored if alwaysShow is true
121+
*/
51122
isVisible?: string | (({ props }) => boolean);
52-
disable?: string;
123+
124+
/**
125+
* Disables the command when the specified student project functionality
126+
* is disabled. References keys in studentProjectFunctionality object.
127+
* Used in educational contexts to restrict certain features.
128+
*/
129+
disable?: keyof StudentProjectFunctionality;
130+
131+
/**
132+
* Keyboard shortcut display. Shown in menu items on desktop (not mobile).
133+
* Should match actual keyboard shortcuts defined elsewhere in the app.
134+
*/
53135
keyboard?: ReactNode;
136+
137+
// Sub-commands that appear as a submenu.
54138
children?:
55139
| Partial<Command>[]
56140
| ((opts: ManageCommands) => Partial<Command>[]);
141+
142+
// if this returns true, the command should be disabled (grayed out) but still visible
57143
disabled?: (opts: ManageCommands) => boolean;
58-
// not used yet
144+
145+
// for interactive tours -- not used yet
59146
tour?: string;
147+
60148
// do modal popconfirm first -- takes options to antd
61149
// Popconfirm, or a function that returns Popconfirm options.
62150
// See frontend/app/popconfirm-modal.tsx for subtleties.
63151
popconfirm?:
64152
| PopconfirmOpts
65153
| ((opts: ManageCommands) => PopconfirmOpts | undefined);
66-
// if true, never show this on mobile
154+
155+
/**
156+
* If true, this command is never shown on mobile devices.
157+
* Used for commands that don't work well on touch interfaces or
158+
* are not needed in mobile contexts.
159+
*/
67160
neverVisibleOnMobile?: boolean;
68-
// if true, always show this (unless neverVisibleOnMobile set, obviously).
161+
162+
/**
163+
* If true, forces the command to always be visible, overriding all other
164+
* visibility checks including isVisible functions and editor spec requirements.
165+
* Used for essential commands like frame controls that should always be available.
166+
* Takes precedence over neverVisibleOnMobile.
167+
*/
69168
alwaysShow?: boolean;
169+
170+
/**
171+
* If true, keeps dropdown menus open after clicking this command.
172+
* Used for commands that don't navigate away or change context,
173+
* allowing multiple selections (e.g., zoom level changes).
174+
*/
70175
stayOpenOnClick?: boolean;
176+
177+
/**
178+
* Additional search terms for the command search functionality.
179+
* Used by the search_commands feature to find commands beyond their
180+
* title, label, and name. Improves discoverability.
181+
*/
71182
search?: string;
72183
}

src/packages/frontend/frame-editors/latex-editor/actions.ts

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/*
1+
/*
22
* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.
33
* License: MS-RSL – see LICENSE.md for details
44
*/
@@ -635,6 +635,51 @@ export class Actions extends BaseActions<LatexEditorState> {
635635
}
636636
}
637637

638+
_new_latex_frame_tree(): FrameTree {
639+
if (this.is_public) {
640+
return { type: "cm" };
641+
} else {
642+
return {
643+
type: "node",
644+
direction: "col",
645+
first: { type: "cm" },
646+
second: { type: "output" },
647+
pos: 0.5,
648+
};
649+
}
650+
}
651+
652+
// Method to replace the entire frame tree with a custom tree structure
653+
replace_frame_tree_with_custom(customTree: FrameTree): void {
654+
let local = this.store.get("local_view_state");
655+
656+
// Process the custom tree: assign IDs and ensure uniqueness
657+
let frame_tree = fromJS(customTree) as Map<string, any>;
658+
frame_tree = tree_ops.assign_ids(frame_tree);
659+
frame_tree = tree_ops.ensure_ids_are_unique(frame_tree);
660+
661+
// Set the frame tree to the custom tree
662+
local = local.set("frame_tree", frame_tree);
663+
664+
// Also make some id active, since existing active_id is no longer valid
665+
local = local.set("active_id", tree_ops.get_some_leaf_id(frame_tree));
666+
667+
// Update state, so visible to UI
668+
this.setState({ local_view_state: local });
669+
670+
// And save this new state to localStorage
671+
this.save_local_view_state();
672+
673+
// Emit new-frame events for all leaf nodes
674+
for (const id in this._get_leaf_ids()) {
675+
const leaf = this._get_frame_node(id);
676+
if (leaf != null) {
677+
const type = leaf.get("type");
678+
this.store.emit("new-frame", { id, type });
679+
}
680+
}
681+
}
682+
638683
check_for_fatal_error(): void {
639684
const build_logs: BuildLogs = this.store.get("build_logs");
640685
if (!build_logs) return;

0 commit comments

Comments
 (0)