From ac362eab3490dffd9c0e7c9fa5fefe0b001631a9 Mon Sep 17 00:00:00 2001 From: Darshan-upadhyay1110 Date: Tue, 21 Apr 2026 19:54:33 +0530 Subject: [PATCH] cool#15364 wsd, kit, browser: honour the Viewing/Editing toggle across the session MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Problem: switching to Viewing mode only updated the browser UI. The view stayed editable in core, so shape drags and arrow-key shape moves still went through, and per-comment Edit/Reply/Delete controls were only hidden at comment-render time so existing comments kept showing them until reloaded. Solution: when a WOPI-writable user toggles between Viewing and Editing, send a new setviewreadonly message to core so the view is marked read-only there too, and flip the client-side comment/redline flags so the UI reacts immediately. Build the comment menu once and show or hide it reactively on updatepermission, instead of tying its creation to the initial permission. Code pointers: - Permission.js: sends setviewreadonly and updates app.file flags (gated on wopi.UserCanWrite to avoid breaking comment-only PDFs). - ClientSession.cpp / ChildSession.cpp: forward and handle the new message; kit calls setViewReadOnly + setAllowChangeComments + setAllowManageRedlines for the current view. - CommentSection.ts: updateEditability() listens on updatepermission and toggles menuBarCell / edit / reply panels; cleaned up in onRemove. Signed-off-by: Darshan-upadhyay1110 Change-Id: I4f57a4efc08d163930cdf1716bf836dce3f620cb Reviewed-on: https://gerrit.collaboraoffice.com/c/online/+/1252 Reviewed-by: Szymon Kłos Tested-by: Szymon Kłos --- browser/src/canvas/sections/CommentSection.ts | 42 ++++++++++++++++++- browser/src/control/Permission.js | 27 ++++++++++++ kit/ChildSession.cpp | 28 +++++++++++++ wsd/ClientSession.cpp | 1 + 4 files changed, 97 insertions(+), 1 deletion(-) diff --git a/browser/src/canvas/sections/CommentSection.ts b/browser/src/canvas/sections/CommentSection.ts index 4604b92926386..121fcbe72a03b 100644 --- a/browser/src/canvas/sections/CommentSection.ts +++ b/browser/src/canvas/sections/CommentSection.ts @@ -123,6 +123,7 @@ export class Comment extends CanvasSectionObject { this.sectionProperties.acceptButton = null; this.sectionProperties.rejectButton = null; this.sectionProperties.menu = null; + this.sectionProperties.menuBarCell = null; this.sectionProperties.captionNode = null; this.sectionProperties.captionText = null; @@ -228,7 +229,11 @@ export class Comment extends CanvasSectionObject { this.createTrackChangeButtons(); } - if (this.sectionProperties.noMenu !== true && app.isCommentEditingAllowed()) { + // Always create the menu if allowed; its visibility is kept in sync with + // the current edit permission by updateEditability() so the Viewing/ + // Editing toggle can show or hide the Edit/Reply/Delete affordances + // without re-rendering the comment. + if (this.sectionProperties.noMenu !== true) { this.createMenu(); } @@ -273,6 +278,35 @@ export class Comment extends CanvasSectionObject { if (!(window).mode.isMobile()) document.getElementById('document-container').appendChild(this.sectionProperties.container); + + this.updateEditability(); + this.sectionProperties.onUpdatePermissionBound = this.updateEditability.bind(this); + app.events.on('updatepermission', this.sectionProperties.onUpdatePermissionBound); + } + + // Syncs visible edit affordances with the current comment-editing + // permission. The editable=true branch only re-shows the menubar + // cell: nodeModify/nodeReply remain hidden until the user explicitly + // triggers edit()/reply(), so we deliberately do NOT auto-reopen a + // previously open edit/reply pane when permission is restored. + private makeEditable (editable: boolean): void { + const props = this.sectionProperties; + if (props.menuBarCell?.style) + props.menuBarCell.style.display = editable ? '' : 'none'; + if (editable) return; + if (props.nodeModify?.style) props.nodeModify.style.display = 'none'; + if (props.nodeReply?.style) props.nodeReply.style.display = 'none'; + if (props.contentNode?.style) props.contentNode.style.display = ''; + props.container.classList.remove('modify-annotation-container'); + props.container.classList.remove('reply-annotation-container'); + this.cachedIsEdit = false; + } + + // Invoked at init and on every updatepermission event, so toggling + // between Viewing and Editing mode immediately hides or restores the + // per-comment controls. + private updateEditability (): void { + this.makeEditable(app.isCommentEditingAllowed()); } private createContainerAndWrapper (): void { @@ -331,6 +365,7 @@ export class Comment extends CanvasSectionObject { private createMenu (): void { var tdMenu = window.L.DomUtil.create('td', 'cool-annotation-menubar', this.sectionProperties.authorRow); + this.sectionProperties.menuBarCell = tdMenu; const edit = window.L.DomUtil.create('div', 'cool-annotation-menu-edit', tdMenu); edit.id = 'comment-annotation-menu-edit-' + this.sectionProperties.data.id; edit.tabIndex = 0; @@ -1703,6 +1738,11 @@ export class Comment extends CanvasSectionObject { public onRemove (): void { this.sectionProperties.commentContainerRemoved = true; + if (this.sectionProperties.onUpdatePermissionBound) { + app.events.off('updatepermission', this.sectionProperties.onUpdatePermissionBound); + this.sectionProperties.onUpdatePermissionBound = null; + } + if (this.sectionProperties.commentListSection.sectionProperties.selectedComment === this) this.sectionProperties.commentListSection.sectionProperties.selectedComment = null; diff --git a/browser/src/control/Permission.js b/browser/src/control/Permission.js index 52e7d7fe3d5d8..538035504c861 100644 --- a/browser/src/control/Permission.js +++ b/browser/src/control/Permission.js @@ -210,6 +210,22 @@ window.L.Map.include({ } }, + // Tell core whether this view is read-only, and flip the client-side + // comment/redline gates the UI checks via isCommentEditingAllowed()/ + // isRedlineManagementAllowed(). Only meaningful for users that actually + // have WOPI write permission: users without write permission already + // get read-only set up server-side at session start, and their + // app.file.editComment / allowManageRedlines flags already reflect the + // real doc state (e.g. comment-only PDFs) and must not be touched. + _applyViewReadOnly: function (readOnly) { + if (!this['wopi'] || !this['wopi'].UserCanWrite) + return; + if (app.socket) + app.socket.sendMessage('setviewreadonly value=' + readOnly); + app.file.editComment = !readOnly; + app.file.allowManageRedlines = !readOnly; + }, + _enterEditMode: function (perm) { this._permission = perm; @@ -220,6 +236,11 @@ window.L.Map.include({ if (app.map['stateChangeHandler'].getItemValue('EditDoc') === 'false') app.map.sendUnoCommand('.uno:EditDoc?Editable:bool=true'); + // Re-enable direct-canvas interactions (shape drag, arrow-key + // shape move) that the matching _enterReadOnlyMode branch + // disabled. + this._applyViewReadOnly(false); + app.events.fire('updatepermission', {perm : perm}); if (this._docLayer._docType === 'text') { @@ -241,6 +262,12 @@ window.L.Map.include({ this._docLayer._onUpdateCursor(); this._docLayer._clearSelections(); } + + // Block direct-canvas interactions (shape drag, arrow-key shape + // move) server-side and hide per-comment edit/redline controls + // in the UI. + this._applyViewReadOnly(true); + app.events.fire('updatepermission', {perm : perm}); this.fire('closemobilewizard'); this.fire('closealldialogs'); diff --git a/kit/ChildSession.cpp b/kit/ChildSession.cpp index d7577becb0c49..1f382dc1c6a35 100644 --- a/kit/ChildSession.cpp +++ b/kit/ChildSession.cpp @@ -578,6 +578,7 @@ bool ChildSession::_handleInput(const char *buffer, int length) tokens.equals(0, "formfieldevent") || tokens.equals(0, "traceeventrecording") || tokens.equals(0, "sallogoverride") || + tokens.equals(0, "setviewreadonly") || tokens.equals(0, "rendersearchresult") || tokens.equals(0, "contentcontrolevent") || tokens.equals(0, "a11ystate") || @@ -848,6 +849,33 @@ bool ChildSession::_handleInput(const char *buffer, int length) getLOKit()->setOption("sallogoverride", tokens[1].c_str()); } } + else if (tokens.equals(0, "setviewreadonly")) + { + // Propagate the browser-side Viewing/Editing toggle to core so it can + // block direct-canvas interactions (shape drag, arrow-key move) and + // gate comment/redline commands via the dispatch filter. + bool readOnly = false; + std::string value; + if (tokens.size() > 1 && getTokenString(tokens[1], "value", value)) + readOnly = (value == "true"); + + if (getLOKitDocument()) + { + getLOKitDocument()->setView(_viewId); + getLOKitDocument()->setViewReadOnly(_viewId, readOnly); + + // Browser only sends setviewreadonly when the user has WOPI + // write permission, so this path is the Viewing/Editing toggle + // on a fully editable doc. Block comments and redline management + // in Viewing mode too - comment-only docs (e.g. PDFs) are set + // up separately at session start and never reach this branch. + getLOKitDocument()->setAllowChangeComments(_viewId, !readOnly); + getLOKitDocument()->setAllowManageRedlines(_viewId, !readOnly); + + LOG_DBG("setviewreadonly: viewId=" << _viewId + << " readOnly=" << readOnly); + } + } else if (tokens.equals(0, "rendersearchresult")) { return renderSearchResult(buffer, length, tokens); diff --git a/wsd/ClientSession.cpp b/wsd/ClientSession.cpp index 9b69550aa9acc..f15c159278ca3 100644 --- a/wsd/ClientSession.cpp +++ b/wsd/ClientSession.cpp @@ -1234,6 +1234,7 @@ bool ClientSession::_handleInput(const char *buffer, int length) } else if (tokens.equals(0, "formfieldevent") || tokens.equals(0, "sallogoverride") || + tokens.equals(0, "setviewreadonly") || tokens.equals(0, "contentcontrolevent")) { return forwardToChild(firstLine, docBroker);