/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict"; // This is loaded into chrome windows with the subscript loader. Wrap in // a block to prevent accidentally leaking globals onto `window`. { const lazy = {}; XPCOMUtils.defineLazyPreferenceGetter( lazy, "smartTabGroupsEnabled", "browser.tabs.groups.smart.enabled" ); const { TabStateFlusher } = ChromeUtils.importESModule( "resource:///modules/sessionstore/TabStateFlusher.sys.mjs" ); class MozTabbrowserTabGroupMenu extends MozXULElement { static COLORS = [ "blue", "purple", "cyan", "orange", "yellow", "pink", "green", "gray", "red", ]; static markup = ` `; static State = { // Standard create mode (No AI UI) CREATE_STANDARD_INITIAL: 0, // Create mode with AI able to suggest tabs CREATE_AI_INITIAL: 1, // No ungrouped tabs to suggest (hide AI interactions) CREATE_AI_INITIAL_SUGGESTIONS_DISABLED: 2, // Create mode with suggestions CREATE_AI_WITH_SUGGESTIONS: 3, // Create mode with no suggestions CREATE_AI_WITH_NO_SUGGESTIONS: 4, // Standard edit mode (No AI UI) EDIT_STANDARD_INITIAL: 5, // Edit mode with AI able to suggest tabs EDIT_AI_INITIAL: 6, // No ungrouped tabs to suggest EDIT_AI_INITIAL_SUGGESTIONS_DISABLED: 7, // Edit mode with suggestions EDIT_AI_WITH_SUGGESTIONS: 8, // Edit mode with no suggestions EDIT_AI_WITH_NO_SUGGESTIONS: 9, LOADING: 10, ERROR: 11, }; #activeGroup; #cancelButton; #createButton; #createMode; #keepNewlyCreatedGroup; #nameField; #panel; #swatches; #swatchesContainer; #suggestionState = MozTabbrowserTabGroupMenu.State.CREATE_STANDARD_INITIAL; constructor() { super(); } connectedCallback() { if (this._initialized) { return; } this.textContent = ""; this.appendChild(this.constructor.fragment); this.initializeAttributeInheritance(); this._initialized = true; this.#cancelButton = this.querySelector( "#tab-group-editor-button-cancel" ); this.#createButton = this.querySelector( "#tab-group-editor-button-create" ); this.#panel = this.querySelector("panel"); this.#nameField = this.querySelector("#tab-group-name"); this.#swatchesContainer = this.querySelector( ".tab-group-editor-swatches" ); this.#populateSwatches(); this.#cancelButton.addEventListener("click", () => { this.close(false); }); this.#createButton.addEventListener("click", () => { this.close(); }); this.#nameField.addEventListener("input", () => { if (this.activeGroup) { this.activeGroup.label = this.#nameField.value; } }); document .getElementById("tabGroupEditor_addNewTabInGroup") .addEventListener("command", () => { this.#handleNewTabInGroup(); }); document .getElementById("tabGroupEditor_moveGroupToNewWindow") .addEventListener("command", () => { gBrowser.replaceGroupWithWindow(this.activeGroup); }); document .getElementById("tabGroupEditor_ungroupTabs") .addEventListener("command", () => { this.#handleUngroup(); }); document .getElementById("tabGroupEditor_saveAndCloseGroup") .addEventListener("command", () => { this.#handleSaveAndClose(); }); document .getElementById("tabGroupEditor_deleteGroup") .addEventListener("command", () => { this.#handleDelete(); }); this.panel.addEventListener("popupshown", this); this.panel.addEventListener("popuphidden", this); this.panel.addEventListener("keypress", this); this.#swatchesContainer.addEventListener("change", this); } #populateSwatches() { this.#clearSwatches(); for (let colorCode of MozTabbrowserTabGroupMenu.COLORS) { let input = document.createElement("input"); input.id = `tab-group-editor-swatch-${colorCode}`; input.type = "radio"; input.name = "tab-group-color"; input.value = colorCode; let label = document.createElement("label"); label.classList.add("tab-group-editor-swatch"); label.setAttribute( "data-l10n-id", `tab-group-editor-color-selector2-${colorCode}` ); label.htmlFor = input.id; label.style.setProperty( "--tabgroup-swatch-color", `var(--tab-group-color-${colorCode})` ); label.style.setProperty( "--tabgroup-swatch-color-invert", `var(--tab-group-color-${colorCode}-invert)` ); this.#swatchesContainer.append(input, label); this.#swatches.push(input); } } #clearSwatches() { this.#swatchesContainer.innerHTML = ""; this.#swatches = []; } get createMode() { return this.#createMode; } set createMode(enableCreateMode) { this.#panel.classList.toggle( "tab-group-editor-mode-create", enableCreateMode ); this.#panel.setAttribute( "aria-labelledby", enableCreateMode ? "tab-group-editor-title-create" : "tab-group-editor-title-edit" ); this.#createMode = enableCreateMode; } get activeGroup() { return this.#activeGroup; } set activeGroup(group = null) { this.#activeGroup = group; this.#nameField.value = group ? group.label : ""; this.#swatches.forEach(node => { if (group && node.value == group.color) { node.checked = true; } else { node.checked = false; } }); } get nextUnusedColor() { let usedColors = []; gBrowser.getAllTabGroups().forEach(group => { usedColors.push(group.color); }); let color = MozTabbrowserTabGroupMenu.COLORS.find( colorCode => !usedColors.includes(colorCode) ); if (!color) { // if all colors are used, pick one randomly let randomIndex = Math.floor( Math.random() * MozTabbrowserTabGroupMenu.COLORS.length ); color = MozTabbrowserTabGroupMenu.COLORS[randomIndex]; } return color; } get panel() { return this.children[0]; } get #panelPosition() { if (gBrowser.tabContainer.verticalMode) { return SidebarController._positionStart ? "topleft topright" : "topright topleft"; } return "bottomleft topleft"; } openCreateModal(group) { this.activeGroup = group; this.createMode = true; this.suggestionState = MozTabbrowserTabGroupMenu.State.CREATE_STANDARD_INITIAL; if (lazy.smartTabGroupsEnabled) { //TODO: set appropriate state } this.#panel.openPopup(group.firstChild, { position: this.#panelPosition, }); } openEditModal(group) { this.activeGroup = group; this.createMode = false; this.suggestionState = MozTabbrowserTabGroupMenu.State.EDIT_STANDARD_INITIAL; if (lazy.smartTabGroupsEnabled) { //TODO: set appropriate state } this.#panel.openPopup(group.firstChild, { position: this.#panelPosition, }); document.getElementById("tabGroupEditor_moveGroupToNewWindow").disabled = gBrowser.openTabs.length == this.activeGroup?.tabs.length; this.#maybeDisableOrHideSaveButton(); } #maybeDisableOrHideSaveButton() { const saveAndCloseGroup = document.getElementById( "tabGroupEditor_saveAndCloseGroup" ); if (PrivateBrowsingUtils.isWindowPrivate(this.ownerGlobal)) { saveAndCloseGroup.hidden = true; return; } let flushes = []; this.activeGroup.tabs.forEach(tab => { flushes.push(TabStateFlusher.flush(tab.linkedBrowser)); }); Promise.allSettled(flushes).then(() => { saveAndCloseGroup.disabled = !SessionStore.shouldSaveTabGroup( this.activeGroup ); }); } close(keepNewlyCreatedGroup = true) { if (this.createMode) { this.#keepNewlyCreatedGroup = keepNewlyCreatedGroup; } this.#panel.hidePopup(); } on_popupshown() { if (this.createMode) { this.#keepNewlyCreatedGroup = true; } this.#nameField.focus(); } on_popuphidden() { if (this.createMode) { if (this.#keepNewlyCreatedGroup) { this.dispatchEvent( new CustomEvent("TabGroupCreateDone", { bubbles: true }) ); } else { this.activeGroup.ungroupTabs(); } } this.activeGroup = null; } on_keypress(event) { if (event.defaultPrevented) { // The event has already been consumed inside of the panel. return; } switch (event.keyCode) { case KeyEvent.DOM_VK_ESCAPE: this.close(false); break; case KeyEvent.DOM_VK_RETURN: this.close(); break; } } /** * change handler for color input */ on_change(aEvent) { if (aEvent.target.name != "tab-group-color") { return; } if (this.activeGroup) { this.activeGroup.color = aEvent.target.value; } } async #handleNewTabInGroup() { let lastTab = this.activeGroup?.tabs.at(-1); let onTabOpened = async aEvent => { this.activeGroup?.addTabs([aEvent.target]); this.close(); window.removeEventListener("TabOpen", onTabOpened); }; window.addEventListener("TabOpen", onTabOpened); gBrowser.addAdjacentNewTab(lastTab); } #handleUngroup() { this.activeGroup?.ungroupTabs(); } #handleSaveAndClose() { this.activeGroup.save(); this.activeGroup.dispatchEvent( new CustomEvent("TabGroupSaved", { bubbles: true }) ); gBrowser.removeTabGroup(this.activeGroup); } #handleDelete() { gBrowser.removeTabGroup(this.activeGroup); } /** * @param {number} newState - See MozTabbrowserTabGroupMenu.State */ set suggestionState(newState) { if (this.#suggestionState === newState) { return; } this.#suggestionState = newState; this.#renderSuggestionState(); } #resetCommonUI() { // TODO - has commun UI reset logic } #renderSuggestionState() { switch (this.#suggestionState) { // CREATE STANDARD INITIAL case MozTabbrowserTabGroupMenu.State.CREATE_STANDARD_INITIAL: this.#resetCommonUI(); //TODO break; //CREATE AI INITIAL case MozTabbrowserTabGroupMenu.State.CREATE_AI_INITIAL: this.#resetCommonUI(); //TODO break; // CREATE AI INITIAL SUGGESTIONS DISABLED case MozTabbrowserTabGroupMenu.State .CREATE_AI_INITIAL_SUGGESTIONS_DISABLED: this.#resetCommonUI(); //TODO break; // CREATE AI WITH SUGGESTIONS case MozTabbrowserTabGroupMenu.State.CREATE_AI_WITH_SUGGESTIONS: //TODO break; // CREATE AI WITH NO SUGGESTIONS case MozTabbrowserTabGroupMenu.State.CREATE_AI_WITH_NO_SUGGESTIONS: //TODO break; // EDIT STANDARD INITIAL case MozTabbrowserTabGroupMenu.State.EDIT_STANDARD_INITIAL: this.#resetCommonUI(); //TODO break; // EDIT AI INITIAL case MozTabbrowserTabGroupMenu.State.EDIT_AI_INITIAL: this.#resetCommonUI(); //TODO break; // EDIT AI INITIAL SUGGESTIONS DISABLED case MozTabbrowserTabGroupMenu.State .EDIT_AI_INITIAL_SUGGESTIONS_DISABLED: this.#resetCommonUI(); //TODO break; // EDIT AI WITH SUGGESTIONS case MozTabbrowserTabGroupMenu.State.EDIT_AI_WITH_SUGGESTIONS: //TODO break; // EDIT AI WITH NO SUGGESTIONS case MozTabbrowserTabGroupMenu.State.EDIT_AI_WITH_NO_SUGGESTIONS: //TODO break; // LOADING case MozTabbrowserTabGroupMenu.State.LOADING: //TODO break; // ERROR case MozTabbrowserTabGroupMenu.State.ERROR: //TODO break; } } } customElements.define("tabgroup-menu", MozTabbrowserTabGroupMenu); }