diff --git a/gulpfile.mjs b/gulpfile.mjs index 539e199cf..57c8c66d1 100644 --- a/gulpfile.mjs +++ b/gulpfile.mjs @@ -1148,6 +1148,7 @@ function buildComponents(defines, dir) { "web/images/messageBar_*.svg", "web/images/toolbarButton-{editorHighlight,menuArrow}.svg", "web/images/cursor-*.svg", + "web/images/secondaryToolbarButton-documentProperties.svg", ]; return ordered([ diff --git a/l10n/en-US/viewer.ftl b/l10n/en-US/viewer.ftl index fbff0d86a..3e4a3510c 100644 --- a/l10n/en-US/viewer.ftl +++ b/l10n/en-US/viewer.ftl @@ -503,3 +503,24 @@ pdfjs-editor-alt-text-settings-editor-title = Alt text editor pdfjs-editor-alt-text-settings-show-dialog-button-label = Show alt text editor right away when adding an image pdfjs-editor-alt-text-settings-show-dialog-description = Helps you make sure all your images have alt text. pdfjs-editor-alt-text-settings-close-button = Close + +## "Annotations removed" bar + +pdfjs-editor-undo-bar-message-highlight = Highlight removed +pdfjs-editor-undo-bar-message-freetext = Text removed +pdfjs-editor-undo-bar-message-ink = Drawing removed +pdfjs-editor-undo-bar-message-stamp = Image removed +# Variables: +# $count (Number) - the number of removed annotations. +pdfjs-editor-undo-bar-message-multiple = + { $count -> + [one] { $count } annotation removed + *[other] { $count } annotations removed + } + +pdfjs-editor-undo-bar-undo-button = + .title = Undo +pdfjs-editor-undo-bar-undo-button-label = Undo +pdfjs-editor-undo-bar-close-button = + .title = Close +pdfjs-editor-undo-bar-close-button-label = Close diff --git a/src/display/editor/draw.js b/src/display/editor/draw.js index 3906fc4ee..d5f1c4bb0 100644 --- a/src/display/editor/draw.js +++ b/src/display/editor/draw.js @@ -676,6 +676,7 @@ class DrawingEditor extends AnnotationEditor { signal, }); parent.toggleDrawing(); + uiManager._editorUndoBar?.hide(); if (this._currentDraw) { parent.drawLayer.updateProperties( diff --git a/src/display/editor/editor.js b/src/display/editor/editor.js index 026d22574..50aff8624 100644 --- a/src/display/editor/editor.js +++ b/src/display/editor/editor.js @@ -1142,6 +1142,8 @@ class AnnotationEditor { bindEvents(this, this.div, ["pointerdown"]); + this._uiManager._editorUndoBar?.hide(); + return this.div; } diff --git a/src/display/editor/tools.js b/src/display/editor/tools.js index d60832488..cb1d9e24b 100644 --- a/src/display/editor/tools.js +++ b/src/display/editor/tools.js @@ -620,6 +620,8 @@ class AnnotationEditorUIManager { #editorsToRescale = new Set(); + _editorUndoBar = null; + #enableHighlightFloatingButton = false; #enableUpdatedAddImage = false; @@ -829,7 +831,8 @@ class AnnotationEditorUIManager { enableHighlightFloatingButton, enableUpdatedAddImage, enableNewAltTextWhenAddingImage, - mlManager + mlManager, + editorUndoBar ) { const signal = (this._signal = this.#abortController.signal); this.#container = container; @@ -864,6 +867,7 @@ class AnnotationEditorUIManager { rotation: 0, }; this.isShiftKeyDown = false; + this._editorUndoBar = editorUndoBar || null; if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("TESTING")) { Object.defineProperty(this, "reset", { @@ -904,6 +908,7 @@ class AnnotationEditorUIManager { clearTimeout(this.#translationTimeoutId); this.#translationTimeoutId = null; } + this._editorUndoBar?.destroy(); } combinedSignal(ac) { @@ -1656,6 +1661,8 @@ class AnnotationEditorUIManager { this.setEditingState(false); this.#disableAll(); + this._editorUndoBar?.hide(); + this.#updateModeCapability.resolve(); return; } @@ -2038,6 +2045,7 @@ class AnnotationEditorUIManager { hasSomethingToRedo: true, isEmpty: this.#isEmpty(), }); + this._editorUndoBar?.hide(); } /** @@ -2099,6 +2107,10 @@ class AnnotationEditorUIManager { ? [drawingEditor] : [...this.#selectedEditors]; const cmd = () => { + this._editorUndoBar?.show( + undo, + editors.length === 1 ? editors[0].editorType : editors.length + ); for (const editor of editors) { editor.remove(); } diff --git a/web/app.js b/web/app.js index aa4bba755..e45c954b9 100644 --- a/web/app.js +++ b/web/app.js @@ -69,6 +69,7 @@ import { AltTextManager } from "web-alt_text_manager"; import { AnnotationEditorParams } from "web-annotation_editor_params"; import { CaretBrowsingMode } from "./caret_browsing.js"; import { DownloadManager } from "web-download_manager"; +import { EditorUndoBar } from "./editor_undo_bar.js"; import { OverlayManager } from "./overlay_manager.js"; import { PasswordPrompt } from "./password_prompt.js"; import { PDFAttachmentViewer } from "web-pdf_attachment_viewer"; @@ -192,6 +193,7 @@ const PDFViewerApplication = { _isCtrlKeyDown: false, _caretBrowsing: null, _isScrolling: false, + editorUndoBar: null, // Called once when the document is loaded. async initialize(appConfig) { @@ -461,6 +463,10 @@ const PDFViewerApplication = { : null; } + if (appConfig.editorUndoBar) { + this.editorUndoBar = new EditorUndoBar(appConfig.editorUndoBar, eventBus); + } + const enableHWA = AppOptions.get("enableHWA"); const pdfViewer = new PDFViewer({ container, @@ -470,6 +476,7 @@ const PDFViewerApplication = { linkService: pdfLinkService, downloadManager, altTextManager, + editorUndoBar: this.editorUndoBar, findController, scriptingManager: AppOptions.get("enableScripting") && pdfScriptingManager, @@ -2732,7 +2739,7 @@ function onTouchEnd(evt) { this._isPinching = false; } -function onClick(evt) { +function closeSecondaryToolbar(evt) { if (!this.secondaryToolbar?.isOpen) { return; } @@ -2749,6 +2756,20 @@ function onClick(evt) { } } +function closeEditorUndoBar(evt) { + if (!this.editorUndoBar?.isOpen) { + return; + } + if (this.appConfig.secondaryToolbar?.toolbar.contains(evt.target)) { + this.editorUndoBar.hide(); + } +} + +function onClick(evt) { + closeSecondaryToolbar.call(this, evt); + closeEditorUndoBar.call(this, evt); +} + function onKeyUp(evt) { // evt.ctrlKey is false hence we use evt.key. if (evt.key === "Control") { @@ -2759,6 +2780,20 @@ function onKeyUp(evt) { function onKeyDown(evt) { this._isCtrlKeyDown = evt.key === "Control"; + if ( + this.editorUndoBar?.isOpen && + evt.keyCode !== 9 && + evt.keyCode !== 16 && + !( + (evt.keyCode === 13 || evt.keyCode === 32) && + getActiveOrFocusedElement() === this.appConfig.editorUndoBar.undoButton + ) + ) { + // Hide undo bar on keypress except for Shift, Tab, Shift+Tab. + // Also avoid hiding if the undo button is triggered. + this.editorUndoBar.hide(); + } + if (this.overlayManager.active) { return; } diff --git a/web/editor_undo_bar.js b/web/editor_undo_bar.js new file mode 100644 index 000000000..aa064e384 --- /dev/null +++ b/web/editor_undo_bar.js @@ -0,0 +1,128 @@ +/* Copyright 2024 Mozilla Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { noContextMenu } from "pdfjs-lib"; + +class EditorUndoBar { + #closeButton = null; + + #container; + + #eventBus = null; + + #focusTimeout = null; + + #initController = null; + + isOpen = false; + + #message; + + #showController = null; + + #undoButton; + + static #l10nMessages = Object.freeze({ + highlight: "pdfjs-editor-undo-bar-message-highlight", + freetext: "pdfjs-editor-undo-bar-message-freetext", + stamp: "pdfjs-editor-undo-bar-message-stamp", + ink: "pdfjs-editor-undo-bar-message-ink", + _multiple: "pdfjs-editor-undo-bar-message-multiple", + }); + + constructor({ container, message, undoButton, closeButton }, eventBus) { + this.#container = container; + this.#message = message; + this.#undoButton = undoButton; + this.#closeButton = closeButton; + this.#eventBus = eventBus; + } + + destroy() { + this.#initController?.abort(); + this.#initController = null; + + this.hide(); + } + + show(undoAction, messageData) { + if (!this.#initController) { + this.#initController = new AbortController(); + const opts = { signal: this.#initController.signal }; + const boundHide = this.hide.bind(this); + + this.#container.addEventListener("contextmenu", noContextMenu, opts); + this.#closeButton.addEventListener("click", boundHide, opts); + this.#eventBus._on("beforeprint", boundHide, opts); + this.#eventBus._on("download", boundHide, opts); + } + + this.hide(); + + if (typeof messageData === "string") { + this.#message.setAttribute( + "data-l10n-id", + EditorUndoBar.#l10nMessages[messageData] + ); + } else { + this.#message.setAttribute( + "data-l10n-id", + EditorUndoBar.#l10nMessages._multiple + ); + this.#message.setAttribute( + "data-l10n-args", + JSON.stringify({ count: messageData }) + ); + } + this.isOpen = true; + this.#container.hidden = false; + + this.#showController = new AbortController(); + + this.#undoButton.addEventListener( + "click", + () => { + undoAction(); + this.hide(); + }, + { signal: this.#showController.signal } + ); + + // Without the setTimeout, VoiceOver will read out the document title + // instead of the popup label. + this.#focusTimeout = setTimeout(() => { + this.#container.focus(); + this.#focusTimeout = null; + }, 100); + } + + hide() { + if (!this.isOpen) { + return; + } + this.isOpen = false; + this.#container.hidden = true; + + this.#showController?.abort(); + this.#showController = null; + + if (this.#focusTimeout) { + clearTimeout(this.#focusTimeout); + this.#focusTimeout = null; + } + } +} + +export { EditorUndoBar }; diff --git a/web/message_bar.css b/web/message_bar.css index 2e908f478..4a948d2ed 100644 --- a/web/message_bar.css +++ b/web/message_bar.css @@ -125,3 +125,97 @@ } } } + +#editorUndoBar { + --text-primary-color: #15141a; + + --message-bar-icon: url(images/secondaryToolbarButton-documentProperties.svg); + --message-bar-icon-color: #0060df; + --message-bar-bg-color: #deeafc; + --message-bar-fg-color: var(--text-primary-color); + --message-bar-border-color: rgb(0 0 0 / 0.08); + + --undo-button-bg-color: rgb(21 20 26 / 0.07); + --undo-button-bg-color-hover: rgb(21 20 26 / 0.14); + --undo-button-bg-color-active: rgb(21 20 26 / 0.21); + + --undo-button-fg-color: var(--message-bar-fg-color); + --undo-button-fg-color-hover: var(--undo-button-fg-color); + --undo-button-fg-color-active: var(--undo-button-fg-color); + + --focus-ring-color: #0060df; + --focus-ring-outline: 2px solid var(--focus-ring-color); + + @media (prefers-color-scheme: dark) { + --text-primary-color: #fbfbfe; + + --message-bar-icon-color: #73a7f3; + --message-bar-bg-color: #003070; + --message-bar-border-color: rgb(255 255 255 / 0.08); + + --undo-button-bg-color: rgb(255 255 255 / 0.08); + --undo-button-bg-color-hover: rgb(255 255 255 / 0.14); + --undo-button-bg-color-active: rgb(255 255 255 / 0.21); + } + + @media screen and (forced-colors: active) { + --text-primary-color: CanvasText; + + --message-bar-icon-color: CanvasText; + --message-bar-bg-color: Canvas; + --message-bar-border-color: CanvasText; + + --undo-button-bg-color: ButtonText; + --undo-button-bg-color-hover: SelectedItem; + --undo-button-bg-color-active: SelectedItem; + + --undo-button-fg-color: ButtonFace; + --undo-button-fg-color-hover: SelectedItemText; + --undo-button-fg-color-active: SelectedItemText; + + --focus-ring-color: CanvasText; + } + + position: fixed; + top: 50px; + left: 50%; + transform: translateX(-50%); + z-index: 10; + + padding-block: 8px; + padding-inline: 16px 8px; + + font: menu; + font-size: 15px; + + cursor: default; + + button { + cursor: pointer; + } + + #editorUndoBarUndoButton { + border-radius: 4px; + font-weight: 590; + line-height: 19.5px; + color: var(--undo-button-fg-color); + border: none; + padding: 4px 16px; + margin-inline-start: 8px; + height: 32px; + + background-color: var(--undo-button-bg-color); + + &:hover { + background-color: var(--undo-button-bg-color-hover); + } + + &:active { + background-color: var(--undo-button-bg-color-active); + } + } + + > div { + align-items: center; + } +} diff --git a/web/pdf_viewer.js b/web/pdf_viewer.js index 6f05d56fe..e968db3f1 100644 --- a/web/pdf_viewer.js +++ b/web/pdf_viewer.js @@ -214,6 +214,8 @@ class PDFViewer { #containerTopLeft = null; + #editorUndoBar = null; + #enableHWA = false; #enableHighlightFloatingButton = false; @@ -281,6 +283,7 @@ class PDFViewer { this.downloadManager = options.downloadManager || null; this.findController = options.findController || null; this.#altTextManager = options.altTextManager || null; + this.#editorUndoBar = options.editorUndoBar || null; if (this.findController) { this.findController.onIsPageVisible = pageNumber => @@ -907,7 +910,8 @@ class PDFViewer { this.#enableHighlightFloatingButton, this.#enableUpdatedAddImage, this.#enableNewAltTextWhenAddingImage, - this.#mlManager + this.#mlManager, + this.#editorUndoBar ); eventBus.dispatch("annotationeditoruimanager", { source: this, diff --git a/web/viewer.html b/web/viewer.html index dc8020d32..bee574ad9 100644 --- a/web/viewer.html +++ b/web/viewer.html @@ -688,6 +688,20 @@ See https://github.com/adobe-type-tools/cmap-resources + +
diff --git a/web/viewer.js b/web/viewer.js index 30eefd022..4cab20308 100644 --- a/web/viewer.js +++ b/web/viewer.js @@ -223,6 +223,12 @@ function getViewerConfiguration() { editorHighlightShowAll: document.getElementById("editorHighlightShowAll"), }, printContainer: document.getElementById("printContainer"), + editorUndoBar: { + container: document.getElementById("editorUndoBar"), + message: document.getElementById("editorUndoBarMessage"), + undoButton: document.getElementById("editorUndoBarUndoButton"), + closeButton: document.getElementById("editorUndoBarCloseButton"), + }, }; }