mirror of
https://github.com/zen-browser/desktop.git
synced 2026-01-28 23:56:11 +00:00
Merge pull request #7761 from mbergo/fix-tab-switching-crash
Fix tab switching crash when creating new tabs (Issue #7716)
This commit is contained in:
@@ -175,25 +175,136 @@ var gZenUIManager = {
|
||||
_clearTimeout: null,
|
||||
_lastTab: null,
|
||||
|
||||
// Track tab switching state to prevent race conditions
|
||||
_tabSwitchState: {
|
||||
inProgress: false,
|
||||
lastSwitchTime: 0,
|
||||
debounceTime: 100, // ms to wait between tab switches
|
||||
queue: [],
|
||||
processingQueue: false,
|
||||
},
|
||||
|
||||
// Queue tab switch operations to prevent race conditions
|
||||
async _queueTabOperation(operation) {
|
||||
// Add operation to queue
|
||||
this._tabSwitchState.queue.push(operation);
|
||||
|
||||
// If already processing queue, just return
|
||||
if (this._tabSwitchState.processingQueue) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Start processing queue
|
||||
this._tabSwitchState.processingQueue = true;
|
||||
|
||||
try {
|
||||
while (this._tabSwitchState.queue.length > 0) {
|
||||
// Get next operation
|
||||
const nextOp = this._tabSwitchState.queue.shift();
|
||||
|
||||
// Check if we need to wait for debounce
|
||||
const now = Date.now();
|
||||
const timeSinceLastSwitch = now - this._tabSwitchState.lastSwitchTime;
|
||||
|
||||
if (timeSinceLastSwitch < this._tabSwitchState.debounceTime) {
|
||||
await new Promise((resolve) => setTimeout(resolve, this._tabSwitchState.debounceTime - timeSinceLastSwitch));
|
||||
}
|
||||
|
||||
// Execute operation
|
||||
this._tabSwitchState.inProgress = true;
|
||||
await nextOp();
|
||||
this._tabSwitchState.inProgress = false;
|
||||
this._tabSwitchState.lastSwitchTime = Date.now();
|
||||
}
|
||||
} finally {
|
||||
this._tabSwitchState.processingQueue = false;
|
||||
}
|
||||
},
|
||||
|
||||
// Check if browser elements are in a valid state for tab operations
|
||||
_validateBrowserState() {
|
||||
// Check if browser window is still open
|
||||
if (window.closed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if gBrowser is available
|
||||
if (!gBrowser || !gBrowser.tabContainer) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if URL bar is available
|
||||
if (!gURLBar) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
handleNewTab(werePassedURL, searchClipboard, where) {
|
||||
// Validate browser state first
|
||||
if (!this._validateBrowserState()) {
|
||||
console.warn('Browser state invalid for new tab operation');
|
||||
return false;
|
||||
}
|
||||
|
||||
const shouldOpenURLBar = gZenVerticalTabsManager._canReplaceNewTab && !werePassedURL && !searchClipboard && where === 'tab';
|
||||
if (shouldOpenURLBar) {
|
||||
|
||||
if (!shouldOpenURLBar) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Queue the tab operation to prevent race conditions
|
||||
this._queueTabOperation(async () => {
|
||||
// Clear any existing timeout
|
||||
if (this._clearTimeout) {
|
||||
clearTimeout(this._clearTimeout);
|
||||
this._clearTimeout = null;
|
||||
}
|
||||
|
||||
// Store the current tab
|
||||
this._lastTab = gBrowser.selectedTab;
|
||||
this._lastTab._visuallySelected = false;
|
||||
this._prevUrlbarLabel = gURLBar._untrimmedValue;
|
||||
if (!this._lastTab) {
|
||||
console.warn('No selected tab found when creating new tab');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Set visual state with proper validation
|
||||
if (this._lastTab && !this._lastTab.closing) {
|
||||
this._lastTab._visuallySelected = false;
|
||||
}
|
||||
|
||||
// Store URL bar state
|
||||
this._prevUrlbarLabel = gURLBar._untrimmedValue || '';
|
||||
|
||||
// Set up URL bar for new tab
|
||||
gURLBar._zenHandleUrlbarClose = this.handleUrlbarClose.bind(this);
|
||||
gURLBar.setAttribute('zen-newtab', true);
|
||||
|
||||
// Update newtab buttons
|
||||
for (const button of this.newtabButtons) {
|
||||
button.setAttribute('in-urlbar', true);
|
||||
}
|
||||
document.getElementById('Browser:OpenLocation').doCommand();
|
||||
gURLBar.search(this._lastSearch);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
// Open location command
|
||||
try {
|
||||
// Wait for a small delay to ensure DOM is ready
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
|
||||
document.getElementById('Browser:OpenLocation').doCommand();
|
||||
|
||||
// Wait for URL bar to be ready
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
|
||||
gURLBar.search(this._lastSearch || '');
|
||||
} catch (e) {
|
||||
console.error('Error opening location in new tab:', e);
|
||||
this.handleUrlbarClose(false);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
clearUrlbarData() {
|
||||
@@ -202,30 +313,67 @@ var gZenUIManager = {
|
||||
},
|
||||
|
||||
handleUrlbarClose(onSwitch) {
|
||||
gURLBar._zenHandleUrlbarClose = null;
|
||||
gURLBar.removeAttribute('zen-newtab');
|
||||
this._lastTab._visuallySelected = true;
|
||||
this._lastTab = null;
|
||||
for (const button of this.newtabButtons) {
|
||||
button.removeAttribute('in-urlbar');
|
||||
// Validate browser state first
|
||||
if (!this._validateBrowserState()) {
|
||||
console.warn('Browser state invalid for URL bar close operation');
|
||||
return;
|
||||
}
|
||||
if (onSwitch) {
|
||||
this.clearUrlbarData();
|
||||
} else {
|
||||
this._lastSearch = gURLBar._untrimmedValue;
|
||||
this._clearTimeout = setTimeout(() => {
|
||||
this.clearUrlbarData();
|
||||
}, this.urlbarWaitToClear);
|
||||
}
|
||||
gURLBar.setURI(this._prevUrlbarLabel, onSwitch, false, false, !onSwitch);
|
||||
gURLBar.handleRevert();
|
||||
if (gURLBar.focused) {
|
||||
gURLBar.view.close({ elementPicked: onSwitch });
|
||||
gURLBar.updateTextOverflow();
|
||||
if (gBrowser.selectedTab.linkedBrowser && onSwitch) {
|
||||
gURLBar.getBrowserState(gBrowser.selectedTab.linkedBrowser).urlbarFocused = false;
|
||||
|
||||
// Queue the operation to prevent race conditions
|
||||
this._queueTabOperation(async () => {
|
||||
// Reset URL bar state
|
||||
if (gURLBar._zenHandleUrlbarClose) {
|
||||
gURLBar._zenHandleUrlbarClose = null;
|
||||
}
|
||||
}
|
||||
gURLBar.removeAttribute('zen-newtab');
|
||||
|
||||
// Safely restore tab visual state with proper validation
|
||||
if (this._lastTab && !this._lastTab.closing && this._lastTab.ownerGlobal && !this._lastTab.ownerGlobal.closed) {
|
||||
this._lastTab._visuallySelected = true;
|
||||
this._lastTab = null;
|
||||
}
|
||||
|
||||
// Reset newtab buttons
|
||||
for (const button of this.newtabButtons) {
|
||||
button.removeAttribute('in-urlbar');
|
||||
}
|
||||
|
||||
// Handle search data
|
||||
if (onSwitch) {
|
||||
this.clearUrlbarData();
|
||||
} else {
|
||||
this._lastSearch = gURLBar._untrimmedValue || '';
|
||||
|
||||
if (this._clearTimeout) {
|
||||
clearTimeout(this._clearTimeout);
|
||||
}
|
||||
|
||||
this._clearTimeout = setTimeout(() => {
|
||||
this.clearUrlbarData();
|
||||
}, this.urlbarWaitToClear);
|
||||
}
|
||||
|
||||
// Safely restore URL bar state with proper validation
|
||||
if (this._prevUrlbarLabel) {
|
||||
gURLBar.setURI(this._prevUrlbarLabel, onSwitch, false, false, !onSwitch);
|
||||
}
|
||||
|
||||
gURLBar.handleRevert();
|
||||
|
||||
if (gURLBar.focused) {
|
||||
gURLBar.view.close({ elementPicked: onSwitch });
|
||||
gURLBar.updateTextOverflow();
|
||||
|
||||
// Ensure tab and browser are valid before updating state
|
||||
const selectedTab = gBrowser.selectedTab;
|
||||
if (selectedTab && selectedTab.linkedBrowser && !selectedTab.closing && onSwitch) {
|
||||
const browserState = gURLBar.getBrowserState(selectedTab.linkedBrowser);
|
||||
if (browserState) {
|
||||
browserState.urlbarFocused = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
urlbarTrim(aURL) {
|
||||
|
||||
@@ -50,6 +50,16 @@ var ZenWorkspaces = new (class extends ZenMultiWindowFeature {
|
||||
}
|
||||
|
||||
async init() {
|
||||
// Initialize tab selection state
|
||||
this._tabSelectionState = {
|
||||
inProgress: false,
|
||||
lastSelectionTime: 0,
|
||||
debounceTime: 100, // ms to wait between tab selections
|
||||
};
|
||||
|
||||
// Initialize workspace change mutex
|
||||
this._workspaceChangeInProgress = false;
|
||||
|
||||
if (!this.shouldHaveWorkspaces) {
|
||||
this._resolveInitialized();
|
||||
document.getElementById('zen-current-workspace-indicator-container').setAttribute('hidden', 'true');
|
||||
@@ -108,28 +118,130 @@ var ZenWorkspaces = new (class extends ZenMultiWindowFeature {
|
||||
);
|
||||
}
|
||||
|
||||
selectEmptyTab(newTabTarget = null, selectURLBar = true) {
|
||||
if (this._emptyTab && gZenVerticalTabsManager._canReplaceNewTab) {
|
||||
if (gBrowser.selectedTab !== this._emptyTab && selectURLBar) {
|
||||
window.addEventListener(
|
||||
'TabSelect',
|
||||
() => {
|
||||
setTimeout(() => {
|
||||
gURLBar.select();
|
||||
}, 0);
|
||||
},
|
||||
{ once: true }
|
||||
);
|
||||
// Validate browser state before tab operations
|
||||
_validateBrowserState() {
|
||||
// Check if browser window is still open
|
||||
if (window.closed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if gBrowser is available
|
||||
if (!gBrowser || !gBrowser.tabContainer) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if URL bar is available
|
||||
if (!gURLBar) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Safely select a tab with debouncing to prevent race conditions
|
||||
async _safelySelectTab(tab) {
|
||||
if (!tab || tab.closing || !tab.ownerGlobal || tab.ownerGlobal.closed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if we need to debounce
|
||||
const now = Date.now();
|
||||
const timeSinceLastSelection = now - this._tabSelectionState.lastSelectionTime;
|
||||
|
||||
if (timeSinceLastSelection < this._tabSelectionState.debounceTime) {
|
||||
await new Promise((resolve) => setTimeout(resolve, this._tabSelectionState.debounceTime - timeSinceLastSelection));
|
||||
}
|
||||
|
||||
// Mark selection as in progress
|
||||
this._tabSelectionState.inProgress = true;
|
||||
|
||||
try {
|
||||
gBrowser.selectedTab = tab;
|
||||
this._tabSelectionState.lastSelectionTime = Date.now();
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.error('Error selecting tab:', e);
|
||||
return false;
|
||||
} finally {
|
||||
this._tabSelectionState.inProgress = false;
|
||||
}
|
||||
}
|
||||
|
||||
async selectEmptyTab(newTabTarget = null, selectURLBar = true) {
|
||||
// Validate browser state first
|
||||
if (!this._validateBrowserState()) {
|
||||
console.warn('Browser state invalid for empty tab selection');
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
// Check if we have a valid empty tab and can replace new tab
|
||||
if (
|
||||
this._emptyTab &&
|
||||
!this._emptyTab.closing &&
|
||||
this._emptyTab.ownerGlobal &&
|
||||
!this._emptyTab.ownerGlobal.closed &&
|
||||
gZenVerticalTabsManager._canReplaceNewTab
|
||||
) {
|
||||
// Only set up URL bar selection if we're switching to a different tab
|
||||
if (gBrowser.selectedTab !== this._emptyTab && selectURLBar) {
|
||||
// Use a Promise-based approach for better sequencing
|
||||
const urlBarSelectionPromise = new Promise((resolve) => {
|
||||
const tabSelectListener = () => {
|
||||
// Remove the event listener first to prevent any chance of multiple executions
|
||||
window.removeEventListener('TabSelect', tabSelectListener);
|
||||
|
||||
// Use requestAnimationFrame to ensure DOM is updated
|
||||
requestAnimationFrame(() => {
|
||||
// Then use setTimeout to ensure browser has time to process tab switch
|
||||
setTimeout(() => {
|
||||
if (gURLBar) {
|
||||
try {
|
||||
gURLBar.select();
|
||||
} catch (e) {
|
||||
console.warn('Error selecting URL bar:', e);
|
||||
}
|
||||
}
|
||||
resolve();
|
||||
}, 50);
|
||||
});
|
||||
};
|
||||
|
||||
window.addEventListener('TabSelect', tabSelectListener, { once: true });
|
||||
});
|
||||
}
|
||||
|
||||
// Safely switch to the empty tab using our debounced method
|
||||
const success = await this._safelySelectTab(this._emptyTab);
|
||||
if (!success) {
|
||||
throw new Error('Failed to select empty tab');
|
||||
}
|
||||
|
||||
return this._emptyTab;
|
||||
}
|
||||
gBrowser.selectedTab = this._emptyTab;
|
||||
return this._emptyTab;
|
||||
|
||||
// Fall back to creating a new tab
|
||||
const newTabUrl = newTabTarget || Services.prefs.getStringPref('browser.startup.homepage');
|
||||
let tab = gZenUIManager.openAndChangeToTab(newTabUrl);
|
||||
|
||||
// Set workspace ID if available
|
||||
if (window.uuid) {
|
||||
tab.setAttribute('zen-workspace-id', this.activeWorkspace);
|
||||
}
|
||||
return tab;
|
||||
} catch (e) {
|
||||
console.error('Error in selectEmptyTab:', e);
|
||||
|
||||
// Create a fallback tab as a last resort, with proper validation
|
||||
try {
|
||||
if (this._validateBrowserState()) {
|
||||
return gBrowser.addTrustedTab('about:blank');
|
||||
}
|
||||
} catch (fallbackError) {
|
||||
console.error('Critical error creating fallback tab:', fallbackError);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
const newTabUrl = newTabTarget || Services.prefs.getStringPref('browser.startup.homepage');
|
||||
let tab = gZenUIManager.openAndChangeToTab(newTabUrl);
|
||||
if (window.uuid) {
|
||||
tab.setAttribute('zen-workspace-id', this.activeWorkspace);
|
||||
}
|
||||
return tab;
|
||||
}
|
||||
|
||||
async delayedStartup() {
|
||||
@@ -2560,10 +2672,45 @@ var ZenWorkspaces = new (class extends ZenMultiWindowFeature {
|
||||
}
|
||||
|
||||
async switchTabIfNeeded(tab) {
|
||||
if (!tab.hasAttribute('zen-essential') && tab.getAttribute('zen-workspace-id') !== this.activeWorkspace) {
|
||||
await this.changeWorkspace({ uuid: tab.getAttribute('zen-workspace-id') });
|
||||
// Validate browser state first
|
||||
if (!this._validateBrowserState()) {
|
||||
console.warn('Browser state invalid for tab switching');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!tab) {
|
||||
console.warn('switchTabIfNeeded called with null tab');
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate tab state
|
||||
if (tab.closing || !tab.ownerGlobal || tab.ownerGlobal.closed || !tab.linkedBrowser) {
|
||||
console.warn('Tab is no longer valid, cannot select it');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Check if we need to change workspace
|
||||
if (!tab.hasAttribute('zen-essential') && tab.getAttribute('zen-workspace-id') !== this.activeWorkspace) {
|
||||
// Use a mutex-like approach to prevent concurrent workspace changes
|
||||
if (this._workspaceChangeInProgress) {
|
||||
console.warn('Workspace change already in progress, deferring tab switch');
|
||||
return;
|
||||
}
|
||||
|
||||
this._workspaceChangeInProgress = true;
|
||||
try {
|
||||
await this.changeWorkspace({ uuid: tab.getAttribute('zen-workspace-id') });
|
||||
} finally {
|
||||
this._workspaceChangeInProgress = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Safely switch to the tab using our debounced method
|
||||
await this._safelySelectTab(tab);
|
||||
} catch (e) {
|
||||
console.error('Error in switchTabIfNeeded:', e);
|
||||
}
|
||||
gBrowser.selectedTab = tab;
|
||||
}
|
||||
|
||||
getDefaultContainer() {
|
||||
|
||||
Reference in New Issue
Block a user