Refactor canvas elements: registry-driven controls#7621
Refactor canvas elements: registry-driven controls#7621
Conversation
CanvasElementManager had grown too large and UI affordances (context menu + mini toolbar) were being assembled imperatively, which made ordering/section dividers hard to reason about and encouraged cross-bundle imports. This change introduces a declarative canvas element registry that drives which buttons and menus are available per element type. It also makes context menu/mini-toolbar composition deterministic: fixed section ordering, exactly one divider/spacer between non-empty sections, and Duplicate/Delete always last. To reduce runtime import-cycle risk across the edit view + toolbox bundles, DOM selectors/constants move to a dependency-light module (canvasElementConstants) while canvasElementUtils is narrowed to a cross-frame bridge (getCanvasElementManager) with type-only imports. CanvasElementManager is partially decomposed into focused helper modules (Geometry/Positioning/Alternates) plus public-function wrappers, and related call sites were updated. Misc hardening: safer MUI Menu anchoring, avoid non-null assertions, fix closest() selector typo, and remove duplicate pxToNumber helper. Follow-ups in this series: - Make mini-toolbar + menu more declarative and consistent - Make `toolbarButtons` the sole source of truth for the mini-toolbar (including explicit spacers) and normalize spacer runs. - Share menu + toolbar definitions via a single command registry to keep icons/tooltips/click behavior in sync. - Replace “Set Up Hyperlink” with the “Set Destination” command in this context, and do not show either on simple image elements.
c0fc612 to
e95f491
Compare
- Delegate addCanvasElement* to CanvasElementFactories (toolbox drop + templates)\n- Move paste-image flow to CanvasElementClipboard\n- Move duplication + audio file copy helper to CanvasElementDuplication\n- Keep CanvasElementManager as orchestrator; behavior validated with live toolbox→page drag/drop\n- Track checkpoints + line count in REFACTOR_PLAN.md
- Switch CanvasToolControls to import CanvasElementManager/ITextColorInfo as types only\n- Move ITextColorInfo to dependency-light CanvasElementSharedTypes and re-export from CanvasElementManager\n- Update CanvasElementFactories to use shared ITextColorInfo type\n- Mark Phase B4 complete in REFACTOR_PLAN.md
Extract resize/crop/side-handle/move-crop drag logic into CanvasElementHandleDragInteractions and wire CanvasElementManager/SelectionUi to delegate to it. This keeps CanvasElementManager focused on orchestration and reduces its size.
Move language-alternate behaviors into CanvasElementAlternates and extract draggable/target ordering+cleanup into CanvasElementDraggableIntegration. CanvasElementManager now delegates to these modules and continues to expose compatibility wrappers for existing callers/tests.
Extract origami splitter drag + comic editing suspend/resume logic into CanvasElementEditingSuspension and delegate from CanvasElementManager.
Extract bloom-canvas size-change child adjustment logic into CanvasElementCanvasResizeAdjustments and delegate from CanvasElementManager.
Backport selected master-side fixes into the refactored canvasElementManager structure while preserving refactor boundaries. BL-15247, BL-15657, BL-15695, BL-15719, BL-15752, BL-15754, BL-15757, BL-15791, BL-15831 Includes motion-tool guard behavior, splitter interaction suppression, canvas-tool activation flow updates, background image sizing/race fixes, expand-to-fill enablement logic, missing-image metadata handling, and image-fit attribute propagation. Also exposes requestPageContent delay helpers through editable page exports and adds editablePageUtils helper used by split-pane and manager code.
# Conflicts: # src/BloomBrowserUI/bookEdit/js/CanvasElementManager.ts # src/BloomBrowserUI/bookEdit/js/bloomEditing.ts # src/BloomBrowserUI/bookEdit/js/canvasElementManager/CanvasElementContextControls.tsx # src/BloomBrowserUI/bookEdit/js/editablePageUtils.ts # src/BloomBrowserUI/bookEdit/toolbox/canvas/canvasElementUtils.ts # src/BloomBrowserUI/lib/split-pane/split-pane.ts
|
Previously, JohnThomson (John Thomson) wrote…
[GPT-5.4] Agreed. The live module is now canvasControlAvailabilityRules.ts, and I also removed the leftover canvasControlAvailabilityPresets.ts duplicate so there is no second file reintroducing the old name. |
|
Previously, JohnThomson (John Thomson) wrote…
[GPT-5.4] Agreed. The registry is back to declarative data; the concrete single-command behavior lives with the other control implementations instead of in the per-element configuration file. |
|
Previously, JohnThomson (John Thomson) wrote…
[GPT-5.4] Renamed this away from element definition. The interface is now ICanvasElementControlConfiguration, which better matches what the data actually represents. |
| private dynamic GetCreativeCommonsInfo(SIL.Core.ClearShare.LicenseInfo ccLicense) | ||
| { | ||
| dynamic dynamicCcLicense = ccLicense; | ||
| return new | ||
| { | ||
| token = ccLicense.Token, | ||
| allowCommercial = ccLicense.CommercialUseAllowed ? "yes" : "no", | ||
| allowDerivatives = GetCcDerivativeRulesAsString(ccLicense.DerivativeRule), | ||
| intergovernmentalVersion = ccLicense.IntergovernmentalOrganizationQualifier, | ||
| token = dynamicCcLicense.Token, | ||
| allowCommercial = dynamicCcLicense.CommercialUseAllowed ? "yes" : "no", | ||
| allowDerivatives = GetCcDerivativeRulesAsString(dynamicCcLicense.DerivativeRule), | ||
| intergovernmentalVersion = dynamicCcLicense.IntergovernmentalOrganizationQualifier, | ||
| }; | ||
| } |
There was a problem hiding this comment.
[GPT-5.4] Fixed. GetCreativeCommonsInfo() no longer uses dynamic. It now reads the expected Creative Commons properties via validated reflection on LicenseInfo, so the cross-assembly name-based fallback still works but missing or mismatched members fail with a clear InvalidOperationException instead of a RuntimeBinderException.
|
Previously, JohnThomson (John Thomson) wrote…
[GPT-5.4] Clarified this in code and docs. canvasElementControlRegistry.ts now describes itself as the per-type control configuration map, and the README explicitly points readers to canvasControlRegistry.ts for the concrete command and panel implementations, labels, icons, hints, and menu-row construction. |
|
Previously, hatton (John Hatton) wrote…
John Hatton chose not to take on this issue in this PR. |
| private dynamic GetCreativeCommonsInfo(SIL.Core.ClearShare.LicenseInfo ccLicense) | ||
| { | ||
| dynamic dynamicCcLicense = ccLicense; | ||
| return new | ||
| { | ||
| token = ccLicense.Token, | ||
| allowCommercial = ccLicense.CommercialUseAllowed ? "yes" : "no", | ||
| allowDerivatives = GetCcDerivativeRulesAsString(ccLicense.DerivativeRule), | ||
| intergovernmentalVersion = ccLicense.IntergovernmentalOrganizationQualifier, | ||
| token = dynamicCcLicense.Token, | ||
| allowCommercial = dynamicCcLicense.CommercialUseAllowed ? "yes" : "no", | ||
| allowDerivatives = GetCcDerivativeRulesAsString(dynamicCcLicense.DerivativeRule), | ||
| intergovernmentalVersion = dynamicCcLicense.IntergovernmentalOrganizationQualifier, | ||
| }; | ||
| } |
There was a problem hiding this comment.
John Hatton chose not to take on this issue in this PR.
JohnThomson
left a comment
There was a problem hiding this comment.
@JohnThomson reviewed 3 files and made 4 comments.
Reviewable status: 20 of 197 files reviewed, 35 unresolved discussions (waiting on hatton).
src/BloomBrowserUI/bookEdit/toolbox/canvas/canvasElementUtils.ts line 1 at r19 (raw file):
// Cross-frame bridge utilities for canvas element code.
I asked an AI how it would improve naming and code organization, and it's top suggestion was that this file wants a more specific name; it's not really a generic place to put utils. It suggested canvasElementBridge. I see it's not a new file, but while we're moving everything and breaking our history, it might be worth doing.
src/BloomBrowserUI/bookEdit/js/canvasElementManager/CanvasElementManagerPublicFunctions.ts line 5 at r19 (raw file):
// // This file exists to keep CanvasElementManager.ts smaller and to reduce coupling // between the page bundle and toolbox UI code.
Seems very similar in purpose to canvasElementUtils (which might become canvasElementBridge). Since it's quite small I'd be inclined to merge them, unless we can improve the names enough to make it clear what functions to look for (and add) in each.
src/BloomBrowserUI/bookEdit/js/canvasElementManager/CanvasElementCanvasResizeAdjustments.ts line 1 at r19 (raw file):
import { Bubble, Comical, TailSpec } from "comicaljs";
File name is awkward. Just canvasElementResizeAdjustment?
src/BloomBrowserUI/bookEdit/js/canvasElementManager/CanvasElementCanvasResizeAdjustments.ts line 19 at r19 (raw file):
} export class CanvasElementCanvasResizeAdjustments {
I don't see why you want a class here. We make one instance of it in one place for the purpose of calling one function in one place...and that function just provides a wrapper so that its three callers can process it as a function of CanvasElementManager rather than importing it directly. Looks to me as if it just adds complexity.
There's no reason for pxToNumber to be a member variable. Why not just import it directly from canvasElementCssUtils? Instead, CanvasElementManager imports it as pxToNumberFromCssUtils, re-wraps it as its own function pxToNumber, and then passes that to the constructor of this object...
adjustBackgroundImageSize is harder to analyze, because it has also become a member function of a new class, so I'd have to analyze whether there's a good reason for that. But I doubt it...again, one instance is created, and the instance variables look like functions that are created to wrap calls to member functions of classes created to wrap other functions...
This is not trivial complexity. For example, suppose I'm reading some code that calls pxToNumber, and want to see what it does. With a direct import, ctrl-click takes me straight there. (I may not even need to go...VS Code might well pop up the comment that describes the function.) With the code your AI has written,
- ctrl-click takes me to a member variable
- I will do one search to find what sets that, and get to the constructor
- I will search for references to the constructor to find the one place it is called
- I will look at that call and figure out what it passes as the pxToNumber argument
- I will follow that to find the local function in CAM
- And then follow that to the function I wanted to see
That's at least six times as much work! And I very much doubt that it will be easier for an AI to figure out, either.
Unless there's a very good reason, I would be greatly in favor of undoing all this business of wrapping functions in singleton classes. For a long time we've been moving away from creating classes unless they really store important state. We're not in C#, where every function has to belong to some class. Modules provide all the namespace management we need. This feels like a move backwards and to increased complexity.
# Conflicts: # src/BloomBrowserUI/bookEdit/js/CanvasElementContextControls.tsx # src/BloomBrowserUI/bookEdit/js/CanvasElementManager.ts # src/BloomBrowserUI/bookEdit/js/workspaceFrames.ts # src/BloomBrowserUI/bookEdit/toolbox/canvas/canvasElementUtils.ts # src/BloomBrowserUI/bookEdit/toolbox/games/GamePromptDialog.tsx # src/BloomBrowserUI/bookEdit/toolbox/games/GameTool.tsx # src/BloomBrowserUI/utils/bloomMessageBoxSupport.ts
|
Previously, JohnThomson (John Thomson) wrote…
[codex] Renamed the toolbox-to-page bridge from |
|
Previously, JohnThomson (John Thomson) wrote…
[codex] I did not merge these two bridge modules, but I did rename them so their responsibilities are explicit: |
|
Previously, JohnThomson (John Thomson) wrote…
[codex] Renamed |
|
Previously, JohnThomson (John Thomson) wrote…
[codex] Removed the singleton-class indirection here. The resize logic now lives in a stateless |
JohnThomson
left a comment
There was a problem hiding this comment.
@JohnThomson made 1 comment.
Reviewable status: 6 of 201 files reviewed, 33 unresolved discussions (waiting on hatton).
src/BloomBrowserUI/bookEdit/js/canvasElementManager/CanvasElementCanvasResizeAdjustments.ts line 19 at r19 (raw file):
Previously, hatton (John Hatton) wrote…
[codex] Removed the singleton-class indirection here. The resize logic now lives in a stateless
adjustCanvasElementChildrenIfSizeChanged()module function inCanvasElementResizeAdjustments.ts, andCanvasElementManagernow calls it directly withadjustBackgroundImageSizeplus the sharedpxToNumberFromCssUtilshelper instead of constructing a wrapper class instance. This is in b20370c.
Good start, but we should just import pxToNumber, not require the caller to supply it. There's only one version of this function in our codebase (I hope).
Also, I'm hoping we can get rid of a lot more of these classes that were apparently just made to wrap functions better imported.
JohnThomson
left a comment
There was a problem hiding this comment.
@JohnThomson made 1 comment.
Reviewable status: 6 of 201 files reviewed, 34 unresolved discussions (waiting on hatton).
src/BloomBrowserUI/bookEdit/js/canvasElementManager/CanvasElementManager.ts line 149 at r20 (raw file):
setupControlFrame: this.setupControlFrame.bind(this), }); this.backgroundImageManager = new CanvasElementBackgroundImageManager({
It appears to me that most of the classes we instantiate here are just wrappers for functions.
- We started with functions X, Y, and Z in CAM which call P and Q
- We moved X, Y, and Z to a new file A, and P and Q to other files B and C
- We should have made the first new file import P and Q, and anything that needs them import X, Y, and Z. If the functions needed access to some state that lives in the CAM singleton, they should make use of a reliable function that finds the one instance (in the right iframe).
- Instead, we made a class in each new file. X, Y, and Z became public methods of Aclass, and P and Q are now public methods of BClass and CClass.
- Now, we need an instance of each of these classes, so the methods can be called. So CAM has to make instances of AClass, BClass, and CClass.
- But, X, Y, and Z still can't call P and Q, because they are member functions of Bclass and CClass, and we don't want every caller creating instances.
- So class A is made to have member variables that point at functions P and Q
- And CAM gets new functions P and Q, which just wrap calls to BInstance.P and CInstance.Q
- And then passes it's own P and Q wrappers to the constructor for A.
Something that should have been three simple imports has become a mess of classes, member variables, wrapper functions, constructors, and passing functions all over the place.
Now, most of these methods started out as member functions of CAM. So it will take a bit of fiddling to share them instead using import and export. Maybe, some of the new classes really deserve to become objects that own some of the CAM state. But we should minimize this pattern where, instead of seeing 'this.X' and being able to ctrl-click and go right to the implementation, I have to go a member variable that holds function X, then to the constructor that initializes it, then to the caller of that constructor, then to the local function that wraps a call to a member of another class, and then finally to the code I want to see. That's a huge downgrade of readability and increase in complexity.
|
Previously, JohnThomson (John Thomson) wrote…
[GPT-5.4]: I went back and did the broader cleanup. Besides the earlier |
|
Previously, JohnThomson (John Thomson) wrote…
[GPT-5.4]: I agreed with the |
hatton
left a comment
There was a problem hiding this comment.
[codex] I changed the registry’s image fallback to use getImageFromCanvasElement(ctx.canvasElement) instead of probing a canvas element through getImageFromContainer(...). That keeps the compatibility behavior but makes the intent explicit at the call site John flagged.
@hatton made 2 comments.
Reviewable status: 6 of 201 files reviewed, 34 unresolved discussions (waiting on JohnThomson).
src/BloomBrowserUI/bookEdit/toolbox/canvas/canvasControlRegistry.ts line 729 at r16 (raw file):
: could this go at the start of the file?
[Hatton] Not necessarily without complicating things. Unlike c#, ts seems to sensitive to order. I don't want to discuss this one any more.
| internal HtmlDom GetXmlDocumentForEditScreenWebPage(string pageUrl, string pageListUrl) | ||
| { | ||
| return GetXmlDocumentForEditScreenWebPage(pageUrl, pageListUrl); | ||
| } |
There was a problem hiding this comment.
🔴 Infinite recursion in EditingModel.GetXmlDocumentForEditScreenWebPage internal overload
The new internal overload of GetXmlDocumentForEditScreenWebPage(string pageUrl, string pageListUrl) at line 1393 calls itself recursively instead of calling the private method with the same signature at line 1347. Both methods have the signature (string, string), so the internal method resolves to itself, causing a StackOverflowException at runtime whenever UpdateCurrentPageDebugView or EditingView.ChangePage calls it.
Recursive call chain
Line 1393 defines:
internal HtmlDom GetXmlDocumentForEditScreenWebPage(string pageUrl, string pageListUrl)
{
return GetXmlDocumentForEditScreenWebPage(pageUrl, pageListUrl);
}This is identical in signature to itself and will recurse infinitely. It should delegate to the private method at src/BloomExe/Edit/EditingModel.cs:1347, but C# method resolution picks the most accessible matching overload — which is itself.
Prompt for agents
In src/BloomExe/Edit/EditingModel.cs, the `internal` overload at line 1393 has the exact same signature as the `private` method at line 1347, causing infinite recursion. The simplest fix is to remove the `internal` overload entirely and make the `private` method at line 1347 `internal` instead. Alternatively, rename the private method to something like `GetXmlDocumentForEditScreenWebPageCore` and have both the public and internal overloads call that renamed method. The callers of the internal overload are `UpdateCurrentPageDebugView` (line 1398) and `EditingView.ChangePage` (line 362 of EditingView.cs).
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
[GPT-5.4] Fixed. I removed the duplicate internal overload and changed the original implementation to internal, so both UpdateCurrentPageDebugView() and the other internal caller now reach the real method instead of recursing. I also validated src/BloomExe/Edit/EditingModel.cs locally and there are no file-level errors.
src/BloomBrowserUI/bookEdit/toolbox/canvas/canvasControlAvailabilityRules.ts
Show resolved
Hide resolved
|
[GPT-5.4] I followed up on the latest remaining bot-reported issue directly in the branch. The recursive |
This PR delivers the Canvas refactor to a registry-driven control architecture and carries it through with behavior fixes and expanded end-to-end coverage.
What Changed
CanvasElementManagerlogic with focused modules underjs/canvasElementManager(selection UI, pointer/drag interactions, geometry/positioning, clipboard, duplication, alternates, background image handling, editing suspension, etc.).Functional Fixes Included
Test and Tooling Work
This change is