From 47fbae7e0db84f9f4c2afe191de77728032428ef Mon Sep 17 00:00:00 2001 From: "Mr. M" Date: Tue, 22 Apr 2025 00:33:15 +0200 Subject: [PATCH] chore: Continued working on containerized essentials, b=(no-bug), c=tabs, workspaces --- src/zen/@types/zen.d.ts | 390 +++++++++++++++++++++++++++ src/zen/tabs/ZenPinnedTabManager.mjs | 2 +- src/zen/workspaces/ZenWorkspaces.mjs | 92 +++++-- 3 files changed, 456 insertions(+), 28 deletions(-) diff --git a/src/zen/@types/zen.d.ts b/src/zen/@types/zen.d.ts index deed4a90f..87967cc37 100644 --- a/src/zen/@types/zen.d.ts +++ b/src/zen/@types/zen.d.ts @@ -49,3 +49,393 @@ interface nsIXPCComponents extends nsISupports { readonly Constructor: (aClass: any, aIID: any, aFlags: any) => any; returnCode: any; } + +/** + * TS-TODO - Needs typing. + * + * This file contains type stubs for loading things from Gecko. All of these + * types should be used in the correct places eventually. + */ + +/** + * Namespace anything that has its types mocked out here. These definitions are + * only "good enough" to get the type checking to pass in this directory. + * Eventually some more structured solution should be found. This namespace is + * global and makes sure that all the definitions inside do not clash with + * naming. + */ +declare namespace MockedExports { + /** + * This interface teaches ChromeUtils.importESModule how to find modules. + */ + interface KnownModules { + Services: typeof import('Services'); + 'resource://gre/modules/AppConstants.sys.mjs': typeof import('resource://gre/modules/AppConstants.sys.mjs'); + 'resource:///modules/CustomizableUI.sys.mjs': typeof import('resource:///modules/CustomizableUI.sys.mjs'); + 'resource:///modules/CustomizableWidgets.sys.mjs': typeof import('resource:///modules/CustomizableWidgets.sys.mjs'); + } + + interface ChromeUtils { + /** + * This function reads the KnownModules and resolves which import to use. + * If you are getting the TS2345 error: + * + * Argument of type '"resource:///.../file.sys.mjs"' is not assignable to + * parameter of type + * + * Then add the file path to the KnownModules above. + */ + importESModule: (module: S) => KnownModules[S]; + defineESModuleGetters: (target: any, mappings: any) => void; + } + + interface MessageManager { + loadFrameScript(url: string, flag: boolean): void; + sendAsyncMessage: (event: string, data: any) => void; + addMessageListener: (event: string, listener: (event: any) => void) => void; + } + + // This is the thing in window.gBrowser, defined in + // https://searchfox.org/mozilla-central/source/browser/base/content/tabbrowser.js + interface Browser { + addWebTab: (url: string, options: any) => BrowserTab; + contentPrincipal: any; + selectedTab: BrowserTab; + selectedBrowser?: ChromeBrowser; + messageManager: MessageManager; + ownerDocument?: ChromeDocument; + tabs: BrowserTab[]; + } + + interface BrowserGroup { + readonly tabs: BrowserTab[]; + readonly group?: BrowserGroup; + } + + // This is a tab in a browser, defined in + // https://searchfox.org/mozilla-central/rev/6b8a3f804789fb865f42af54e9d2fef9dd3ec74d/browser/base/content/tabbrowser.js#2580 + interface BrowserTab extends XULElement { + linkedBrowser: ChromeBrowser; + readonly group?: BrowserGroup; + } + + interface BrowserWindow extends Window { + gBrowser: Browser; + focus(): void; + } + + // The thing created in https://searchfox.org/mozilla-central/rev/6b8a3f804789fb865f42af54e9d2fef9dd3ec74d/browser/base/content/tabbrowser.js#2088 + // This is linked to BrowserTab. + interface ChromeBrowser { + browsingContext?: BrowsingContext; + browserId: number; + } + + interface BrowsingContext { + /** + * A unique identifier for the browser element that is hosting this + * BrowsingContext tree. Every BrowsingContext in the element's tree will + * return the same ID in all processes and it will remain stable regardless of + * process changes. When a browser element's frameloader is switched to + * another browser element this ID will remain the same but hosted under the + * under the new browser element. + * We are using this identifier for getting the active tab ID and passing to + * the profiler back-end. See `getActiveBrowserID` for the usage. + */ + browserId: number; + } + + type GetPref = (prefName: string, defaultValue?: T) => T; + type SetPref = (prefName: string, value?: T) => T; + type nsIPrefBranch = { + clearUserPref: (prefName: string) => void; + getStringPref: GetPref; + setStringPref: SetPref; + getCharPref: GetPref; + setCharPref: SetPref; + getIntPref: GetPref; + setIntPref: SetPref; + getBoolPref: GetPref; + setBoolPref: SetPref; + addObserver: (aDomain: string, aObserver: PrefObserver, aHoldWeak?: boolean) => void; + removeObserver: (aDomain: string, aObserver: PrefObserver) => void; + }; + + type PrefObserverFunction = (aSubject: nsIPrefBranch, aTopic: 'nsPref:changed', aData: string) => unknown; + type PrefObserver = PrefObserverFunction | { observe: PrefObserverFunction }; + + interface nsIURI {} + + interface SharedLibrary { + start: number; + end: number; + offset: number; + name: string; + path: string; + debugName: string; + debugPath: string; + breakpadId: string; + arch: string; + } + + interface ProfileGenerationAdditionalInformation { + sharedLibraries: SharedLibrary[]; + } + + interface ProfileAndAdditionalInformation { + profile: ArrayBuffer; + additionalInformation?: ProfileGenerationAdditionalInformation; + } + + type Services = { + env: { + set: (name: string, value: string) => void; + get: (name: string) => string; + exists: (name: string) => boolean; + }; + prefs: nsIPrefBranch; + profiler: { + StartProfiler: ( + entryCount: number, + interval: number, + features: string[], + filters?: string[], + activeTabId?: number, + duration?: number + ) => void; + StopProfiler: () => void; + IsPaused: () => boolean; + Pause: () => void; + Resume: () => void; + IsSamplingPaused: () => boolean; + PauseSampling: () => void; + ResumeSampling: () => void; + GetFeatures: () => string[]; + getProfileDataAsync: (sinceTime?: number) => Promise; + getProfileDataAsArrayBuffer: (sinceTime?: number) => Promise; + getProfileDataAsGzippedArrayBuffer: (sinceTime?: number) => Promise; + IsActive: () => boolean; + sharedLibraries: SharedLibrary[]; + }; + platform: string; + obs: { + addObserver: (observer: object, type: string) => void; + removeObserver: (observer: object, type: string) => void; + }; + wm: { + getMostRecentWindow: (name: string) => BrowserWindow; + getMostRecentNonPBWindow: (name: string) => BrowserWindow; + }; + focus: { + activeWindow: BrowserWindow; + }; + io: { + newURI(url: string): nsIURI; + }; + scriptSecurityManager: any; + startup: { + quit: (optionsBitmask: number) => void; + eForceQuit: number; + eRestart: number; + }; + }; + + const EventEmitter: { + decorate: (target: object) => void; + }; + + const AppConstantsSYSMJS: { + AppConstants: { + platform: string; + }; + }; + + interface BrowsingContextStub {} + interface PrincipalStub {} + + interface WebChannelTarget { + browsingContext: BrowsingContextStub; + browser: Browser; + eventTarget: null; + principal: PrincipalStub; + } + + interface FaviconData { + uri: nsIURI; + dataLen: number; + data: number[]; + mimeType: string; + size: number; + } + + const PlaceUtilsSYSMJS: { + PlacesUtils: { + promiseFaviconData: (pageUrl: string | URL | nsIURI, preferredWidth?: number) => Promise; + // TS-TODO: Add the rest. + }; + }; + + // TS-TODO + const CustomizableUISYSMJS: any; + const CustomizableWidgetsSYSMJS: any; + const PanelMultiViewSYSMJS: any; + + const LoaderESM: { + require: (path: string) => any; + }; + + const Services: Services; + + // This class is needed by the Cc importing mechanism. e.g. + // Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker); + class nsIFilePicker {} + + interface FilePicker { + init: (browsingContext: BrowsingContext, title: string, mode: number) => void; + open: (callback: (rv: number) => unknown) => void; + // The following are enum values. + modeGetFolder: number; + returnOK: number; + file: { + path: string; + }; + } + + interface Cc { + '@mozilla.org/filepicker;1': { + createInstance(instance: nsIFilePicker): FilePicker; + }; + } + + interface Ci { + nsIFilePicker: nsIFilePicker; + } + + interface Cu { + exportFunction: (fn: Function, scope: object, options?: object) => void; + cloneInto: (value: any, scope: object, options?: object) => void; + isInAutomation: boolean; + } + + interface FluentLocalization { + /** + * This function sets the attributes data-l10n-id and possibly data-l10n-args + * on the element. + */ + setAttributes(target: Element, id?: string, args?: Record): void; + } +} + +interface PathUtilsInterface { + split: (path: string) => string[]; + isAbsolute: (path: string) => boolean; +} + +declare module 'Services' { + export = MockedExports.Services; +} + +declare module 'ChromeUtils' { + export = ChromeUtils; +} + +declare var ChromeUtils: MockedExports.ChromeUtils; + +declare var PathUtils: PathUtilsInterface; + +// These global objects can be used directly in JSM files only. +declare var Cu: MockedExports.Cu; +declare var Cc: MockedExports.Cc; +declare var Ci: MockedExports.Ci; +declare var Services: MockedExports.Services; + +/** + * This is a variant on the normal Document, as it contains chrome-specific properties. + */ +declare interface ChromeDocument extends Document { + /** + * Create a XUL element of a specific type. Right now this function + * only refines iframes, but more tags could be added. + */ + createXULElement: ((type: 'iframe') => XULIframeElement) & ((type: string) => XULElement); + + /** + * This is a fluent instance connected to this document. + */ + l10n: MockedExports.FluentLocalization; +} + +/** + * This is a variant on the HTMLElement, as it contains chrome-specific properties. + */ +declare interface ChromeHTMLElement extends HTMLElement { + ownerDocument: ChromeDocument; +} + +declare interface XULIframeElement extends XULElement { + contentWindow: Window; + src: string; +} + +// `declare interface Window` is TypeScript way to let us implicitely extend and +// augment the already existing Window interface defined in the TypeScript library. +// This makes it possible to define properties that exist in the window object +// while in a privileged context. We assume that all of the environments we run +// in this project will be pribileged, that's why we take this shortcut of +// globally extending the Window type. +// See the ChromeOnly attributes in https://searchfox.org/mozilla-central/rev/896042a1a71066254ceb5291f016ca3dbca21cb7/dom/webidl/Window.webidl#391 +// +// openWebLinkIn and openTrustedLinkIn aren't in all privileged windows, but +// they're also defined in the privileged environments we're dealing with in +// this project, so they're defined here for convenience. +declare interface Window { + browsingContext: MockedExports.BrowsingContext; + openWebLinkIn: ( + url: string, + where: 'current' | 'tab' | 'tabshifted' | 'window' | 'save', + options?: Partial<{ + // Not all possible options are present, please add more if/when needed. + userContextId: number; + forceNonPrivate: boolean; + relatedToCurrent: boolean; + resolveOnContentBrowserCreated: (contentBrowser: MockedExports.ChromeBrowser) => unknown; + }> + ) => void; + openTrustedLinkIn: ( + url: string, + where: 'current' | 'tab' | 'tabshifted' | 'window' | 'save', + options?: Partial<{ + // Not all possible options are present, please add more if/when needed. + userContextId: number; + forceNonPrivate: boolean; + relatedToCurrent: boolean; + resolveOnContentBrowserCreated: (contentBrowser: MockedExports.ChromeBrowser) => unknown; + }> + ) => void; +} + +declare class ChromeWorker extends Worker {} + +declare interface MenuListElement extends XULElement { + value: string; + disabled: boolean; +} + +declare interface XULCommandEvent extends Event { + target: XULElement; +} + +declare interface XULElementWithCommandHandler { + addEventListener: (type: 'command', handler: (event: XULCommandEvent) => void, isCapture?: boolean) => void; + removeEventListener: (type: 'command', handler: (event: XULCommandEvent) => void, isCapture?: boolean) => void; +} + +declare type nsIPrefBranch = MockedExports.nsIPrefBranch; + +// chrome context-only DOM isInstance method +// XXX: This hackishly extends Function because there is no way to extend DOM constructors. +// Callers should manually narrow the type when needed. +// See also https://github.com/microsoft/TypeScript-DOM-lib-generator/issues/222 +interface Function { + isInstance(obj: any): boolean; +} diff --git a/src/zen/tabs/ZenPinnedTabManager.mjs b/src/zen/tabs/ZenPinnedTabManager.mjs index c9a2dd7b1..d3ea00d73 100644 --- a/src/zen/tabs/ZenPinnedTabManager.mjs +++ b/src/zen/tabs/ZenPinnedTabManager.mjs @@ -800,7 +800,7 @@ if (tabsTarget === gBrowser.tabs.at(-1)) { newIndex++; } - gBrowser.moveTabTo(draggedTab, newIndex, { forceUngrouped: true }); + gBrowser.moveTabTo(draggedTab, { tabIndex: newIndex, forceUngrouped: true }); } } } diff --git a/src/zen/workspaces/ZenWorkspaces.mjs b/src/zen/workspaces/ZenWorkspaces.mjs index 3a314628c..e14bdd57a 100644 --- a/src/zen/workspaces/ZenWorkspaces.mjs +++ b/src/zen/workspaces/ZenWorkspaces.mjs @@ -255,7 +255,7 @@ var ZenWorkspaces = new (class extends ZenMultiWindowFeature { if (!this.containerSpecificEssentials) { container = 0; } - let essentialsContainer = document.querySelector(`.zen-essentials-container[container="${container}"]`); + let essentialsContainer = document.querySelector(`.zen-essentials-container[container="${container}"]:not([clone])`); if (!essentialsContainer) { essentialsContainer = document.createXULElement('vbox'); essentialsContainer.className = 'zen-essentials-container zen-workspace-tabs-section'; @@ -657,7 +657,7 @@ var ZenWorkspaces = new (class extends ZenMultiWindowFeature { if (this._initialTab) { this.moveTabToWorkspace(this._initialTab, this.activeWorkspace); gBrowser.selectedTab = this._initialTab; - gBrowser.moveTabTo(this._initialTab, 0, { forceUngrouped: true }); + gBrowser.moveTabTo(this._initialTab, { forceUngrouped: true, tabIndex: 0 }); this._initialTab._possiblyEmpty = false; this._initialTab = null; } @@ -1735,23 +1735,26 @@ var ZenWorkspaces = new (class extends ZenMultiWindowFeature { const workspaces = await this._workspaces(); const newWorkspaceIndex = workspaces.workspaces.findIndex((w) => w.uuid === newWorkspace.uuid); const clonedEssentials = []; - const essentialsContainerMap = {}; - if (shouldAnimate) { + if (shouldAnimate && this.containerSpecificEssentials) { for (const workspace of workspaces.workspaces) { const essentialsContainer = this.getEssentialsSection(workspace.containerTabId); + if (clonedEssentials[clonedEssentials.length - 1]?.contextId == workspace.containerTabId) { + clonedEssentials[clonedEssentials.length - 1].repeat++; + clonedEssentials[clonedEssentials.length - 1].workspaces.push(workspace); + continue; + } essentialsContainer.setAttribute('hidden', 'true'); const essentialsClone = essentialsContainer.cloneNode(true); essentialsClone.removeAttribute('hidden'); + essentialsClone.setAttribute('cloned', 'true'); clonedEssentials.push({ container: essentialsClone, - workspaceId: workspace.uuid, + workspaces: [workspace], contextId: workspace.containerTabId, originalContainer: essentialsContainer, repeat: 0, }); essentialsContainer.parentNode.appendChild(essentialsClone); - // +0 to convert null to 0 - essentialsContainerMap[workspace.containerTabId + 0] = essentialsContainer; } } for (const element of document.querySelectorAll('.zen-workspace-tabs-section')) { @@ -1778,26 +1781,6 @@ var ZenWorkspaces = new (class extends ZenMultiWindowFeature { } ) ); - if (element.parentNode.id === 'zen-current-workspace-indicator-container') { - // Get essential container clone for this workspace - const clonedEssential = clonedEssentials.find((cloned) => cloned.workspaceId === elementWorkspaceId); - if (clonedEssential && !clonedEssential.animating) { - clonedEssential.animating = true; // Avoid motion hanging due to animating the same element twice - animations.push( - gZenUIManager.motion.animate( - clonedEssential.container, - { - transform: existingTransform ? [existingTransform, newTransform] : newTransform, - }, - { - type: 'spring', - bounce: 0, - duration: kGlobalAnimationDuration, - } - ) - ); - } - } } if (offset === 0) { element.setAttribute('active', 'true'); @@ -1808,6 +1791,58 @@ var ZenWorkspaces = new (class extends ZenMultiWindowFeature { element.removeAttribute('active'); } } + if (this.containerSpecificEssentials) { + // Animate essentials + for (const cloned of clonedEssentials) { + const container = cloned.container; + const essentialsWorkspacess = cloned.workspaces; + const repeats = cloned.repeat; + const containerId = cloned.contextId; + // Animate like the workspaces above expect essentials are a bit more + // complicated because they are not based on workspaces but on containers + // So, if we have the following arangement: + // | [workspace1] [workspace2] [workspace3] [workspace4] + // | [container1] [container1] [container2] [container1] + // And if we are changing from workspace 1 to workspace 4, + // we should be doing the following: + // First container (repeat 2 times) will stay in place until + // we reach container 3, then animate to the left and container 2 + // also move to the left after that while container 1 in workspace 4 + // will slide in from the right + + // Get the index from first and last workspace + const firstWorkspaceIndex = workspaces.workspaces.findIndex((w) => w.uuid === essentialsWorkspacess[0].uuid); + const lastWorkspaceIndex = workspaces.workspaces.findIndex( + (w) => w.uuid === essentialsWorkspacess[essentialsWorkspacess.length - 1].uuid + ); + const isGoingLeft = newWorkspaceIndex > lastWorkspaceIndex; + const isGoingInsideSameContainer = essentialsWorkspacess.some((w) => w.uuid === newWorkspace.uuid); + if (isGoingInsideSameContainer) { + continue; // We dont want to animate if we are going inside the same container + } + const firstOffset = -(newWorkspaceIndex - firstWorkspaceIndex - (isGoingLeft ? repeats : -repeats)) * 100; + const lastOffset = -(newWorkspaceIndex - lastWorkspaceIndex - (isGoingLeft ? repeats : -repeats)) * 100; + const newTransform = `translateX(${firstOffset}%)`; + const existingTransform = `translateX(${lastOffset}%)`; + const stepsInBetween = Math.abs(lastWorkspaceIndex - firstWorkspaceIndex); + if (shouldAnimate) { + container.style.transform = newTransform; + animations.push( + gZenUIManager.motion.animate( + container, + { + transform: [existingTransform, new Array(stepsInBetween).fill(newTransform).join(',')], + }, + { + type: 'spring', + bounce: 0, + duration: kGlobalAnimationDuration, + } + ) + ); + } + } + } await Promise.all(animations); if (shouldAnimate) { for (const cloned of clonedEssentials) { @@ -2391,6 +2426,9 @@ var ZenWorkspaces = new (class extends ZenMultiWindowFeature { } const containers = [...essentialsContainer, ...pinnedContainers, ...normalContainers]; for (const container of containers) { + if (container.hasAttribute('cloned')) { + continue; + } for (const tab of container.children) { if (tab.tagName === 'tab') { tabs.push(tab);