1
0
Fork 1
mirror of https://github.com/zen-browser/desktop.git synced 2025-07-07 16:05:31 +02:00
zen-desktop/src/browser/components/preferences/zen-settings.js

1184 lines
39 KiB
JavaScript

// 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/.
var gZenMarketplaceManager = {
async init() {
const checkForUpdates = document.getElementById('zenThemeMarketplaceCheckForUpdates');
const header = document.getElementById('zenMarketplaceHeader');
if (!checkForUpdates || !header) {
return; // We haven't entered the settings page yet.
}
if (this.__hasInitializedEvents) {
return;
}
if (!window.gZenMods) {
window.gZenMods = ZenMultiWindowFeature.currentBrowser.gZenMods;
}
header.appendChild(this._initDisableAll());
this._initImportExport();
this.__hasInitializedEvents = true;
await this._buildModsList();
Services.prefs.addObserver(gZenMods.updatePref, this);
const checkForUpdateClick = (event) => {
if (event.target === checkForUpdates) {
event.preventDefault();
this._checkForThemeUpdates(event);
}
};
checkForUpdates.addEventListener('click', checkForUpdateClick);
document.addEventListener('ZenModsMarketplace:CheckForUpdatesFinished', (event) => {
checkForUpdates.disabled = false;
const updates = event.detail.updates;
const success = document.getElementById('zenThemeMarketplaceUpdatesSuccess');
const error = document.getElementById('zenThemeMarketplaceUpdatesFailure');
if (updates) {
success.hidden = false;
error.hidden = true;
} else {
success.hidden = true;
error.hidden = false;
}
});
window.addEventListener('unload', () => {
Services.prefs.removeObserver(gZenMods.updatePref, this);
this.__hasInitializedEvents = false;
document.removeEventListener('ZenModsMarketplace:CheckForUpdatesFinished', this);
document.removeEventListener('ZenCheckForModUpdates', this);
checkForUpdates.removeEventListener('click', checkForUpdateClick);
this.modsList.innerHTML = '';
this._doNotRebuildModsList = false;
});
},
_initImportExport() {
const importButton = document.getElementById('zenThemeMarketplaceImport');
const exportButton = document.getElementById('zenThemeMarketplaceExport');
if (importButton) {
importButton.addEventListener('click', this._importThemes.bind(this));
}
if (exportButton) {
exportButton.addEventListener('click', this._exportThemes.bind(this));
}
},
_initDisableAll() {
const areModsDisabled = Services.prefs.getBoolPref('zen.themes.disable-all', false);
const browser = ZenMultiWindowFeature.currentBrowser;
const mozToggle = document.createElement('moz-toggle');
mozToggle.className =
'zenThemeMarketplaceItemPreferenceToggle zenThemeMarketplaceDisableAllToggle';
mozToggle.pressed = !areModsDisabled;
browser.document.l10n.setAttributes(
mozToggle,
`zen-theme-disable-all-${!areModsDisabled ? 'enabled' : 'disabled'}`
);
mozToggle.addEventListener('toggle', async (event) => {
const { pressed = false } = event.target || {};
this.modsList.style.display = pressed ? '' : 'none';
Services.prefs.setBoolPref('zen.themes.disable-all', !pressed);
browser.document.l10n.setAttributes(
mozToggle,
`zen-theme-disable-all-${pressed ? 'enabled' : 'disabled'}`
);
});
if (areModsDisabled) {
this.modsList.style.display = 'none';
}
return mozToggle;
},
async observe() {
await this._buildModsList();
},
_checkForThemeUpdates(event) {
// Send a message to the child to check for theme updates.
event.target.disabled = true;
// send an event that will be listened by the child process.
document.dispatchEvent(new CustomEvent('ZenCheckForModUpdates'));
},
get modsList() {
if (!this._modsList) {
this._modsList = document.getElementById('zenThemeMarketplaceList');
}
return this._modsList;
},
_triggerBuildUpdateWithoutRebuild() {
this._doNotRebuildModsList = true;
gZenMods.triggerModsUpdate();
},
async removeMod(modId) {
await gZenMods.removeMod(modId);
gZenMods.triggerModsUpdate();
},
async disableMod(modId) {
await gZenMods.disableMod(modId);
this._triggerBuildUpdateWithoutRebuild();
},
async enableMod(modId) {
await gZenMods.enableMod(modId);
this._triggerBuildUpdateWithoutRebuild();
},
async _importThemes() {
const errorBox = document.getElementById('zenThemeMarketplaceImportFailure');
const successBox = document.getElementById('zenThemeMarketplaceImportSuccess');
successBox.hidden = true;
errorBox.hidden = true;
const input = document.createElement('input');
input.type = 'file';
input.accept = '.json';
input.style.display = 'none';
input.setAttribute('moz-accept', '.json');
input.setAttribute('accept', '.json');
let timeout;
const filePromise = new Promise((resolve) => {
input.addEventListener('change', (event) => {
if (timeout) {
clearTimeout(timeout);
}
const file = event.target.files[0];
resolve(file);
});
timeout = setTimeout(() => {
console.warn('[ZenSettings:ZenMods]: Import timeout reached, aborting.');
resolve(null);
}, 60000);
});
input.addEventListener('cancel', () => {
console.warn('[ZenSettings:ZenMods]: Import cancelled by user.');
clearTimeout(timeout);
});
input.click();
try {
const file = await filePromise;
if (!file) {
return;
}
const content = await file.text();
const mods = JSON.parse(content);
for (const mod of Object.values(mods)) {
mod.modId = mod.id;
await window.ZenInstallMod(mod);
}
} catch (error) {
console.error('[ZenSettings:ZenMods]: Error while importing mods:', error);
errorBox.hidden = false;
}
if (input) {
input.remove();
}
},
async _exportThemes() {
const errorBox = document.getElementById('zenThemeMarketplaceExportFailure');
const successBox = document.getElementById('zenThemeMarketplaceExportSuccess');
successBox.hidden = true;
errorBox.hidden = true;
let temporalAnchor, temporalUrl;
try {
const mods = await gZenMods.getMods();
const modsJson = JSON.stringify(mods, null, 2);
const blob = new Blob([modsJson], { type: 'application/json' });
temporalUrl = URL.createObjectURL(blob);
// Creating a link to download the JSON file
temporalAnchor = document.createElement('a');
temporalAnchor.href = temporalUrl;
temporalAnchor.download = 'zen-mods-export.json';
document.body.appendChild(temporalAnchor);
temporalAnchor.click();
temporalAnchor.remove();
successBox.hidden = false;
} catch (error) {
console.error('[ZenSettings:ZenMods]: Error while exporting mods:', error);
errorBox.hidden = false;
}
if (temporalAnchor) {
temporalAnchor.remove();
}
if (temporalUrl) {
URL.revokeObjectURL(temporalUrl);
}
},
async _buildModsList() {
if (!this.modsList) {
return;
}
if (this._doNotRebuildModsList) {
this._doNotRebuildModsList = false;
return;
}
const mods = await gZenMods.getMods();
const browser = ZenMultiWindowFeature.currentBrowser;
const modList = document.createElement('div');
for (const mod of Object.values(mods).sort((a, b) => a.name.localeCompare(b.name))) {
const sanitizedName = gZenMods.sanitizeModName(mod.name);
const isModEnabled = mod.enabled === undefined || mod.enabled;
const fragment = window.MozXULElement.parseXULToFragment(`
<vbox class="zenThemeMarketplaceItem">
<vbox class="zenThemeMarketplaceItemContent">
<hbox flex="1" id="zenThemeMarketplaceItemContentHeader">
<label><h3 class="zenThemeMarketplaceItemTitle"></h3></label>
</hbox>
<description class="description-deemphasized zenThemeMarketplaceItemDescription"></description>
</vbox>
<hbox class="zenThemeMarketplaceItemActions">
${mod.preferences ? `<button id="zenThemeMarketplaceItemConfigureButton-${sanitizedName}" class="zenThemeMarketplaceItemConfigureButton" hidden="true"></button>` : ''}
${mod.homepage ? `<button id="zenThemeMarketplaceItemHomePageLink-${sanitizedName}" class="zenThemeMarketplaceItemHomepageButton" zen-mod-id="${mod.id}"></button>` : ''}
<button class="zenThemeMarketplaceItemUninstallButton" data-l10n-id="zen-theme-marketplace-remove-button" zen-mod-id="${mod.id}"></button>
</hbox>
</vbox>
`);
const modName = `${mod.name} (v${mod.version ?? '1.0.0'})`;
const base = fragment.querySelector('.zenThemeMarketplaceItem');
const baseHeader = fragment.querySelector('#zenThemeMarketplaceItemContentHeader');
const dialog = document.createElement('dialog');
const mainDialogDiv = document.createElement('div');
const headerDiv = document.createElement('div');
const headerTitle = document.createElement('h3');
const closeButton = document.createElement('button');
const contentDiv = document.createElement('div');
const mozToggle = document.createElement('moz-toggle');
mainDialogDiv.className = 'zenThemeMarketplaceItemPreferenceDialog';
headerDiv.className = 'zenThemeMarketplaceItemPreferenceDialogTopBar';
headerTitle.textContent = modName;
browser.document.l10n.setAttributes(headerTitle, 'zen-theme-marketplace-theme-header-title', {
name: sanitizedName,
});
headerTitle.className = 'zenThemeMarketplaceItemTitle';
closeButton.id = `${sanitizedName}-modal-close`;
browser.document.l10n.setAttributes(closeButton, 'zen-theme-marketplace-close-modal');
contentDiv.id = `${sanitizedName}-preferences-content`;
contentDiv.className = 'zenThemeMarketplaceItemPreferenceDialogContent';
mozToggle.className = 'zenThemeMarketplaceItemPreferenceToggle';
mozToggle.pressed = isModEnabled;
browser.document.l10n.setAttributes(
mozToggle,
`zen-theme-marketplace-toggle-${isModEnabled ? 'enabled' : 'disabled'}-button`
);
baseHeader.appendChild(mozToggle);
headerDiv.appendChild(headerTitle);
headerDiv.appendChild(closeButton);
mainDialogDiv.appendChild(headerDiv);
mainDialogDiv.appendChild(contentDiv);
dialog.appendChild(mainDialogDiv);
base.appendChild(dialog);
closeButton.addEventListener('click', () => {
dialog.close();
});
mozToggle.addEventListener('toggle', async (event) => {
const modId = event.target
.closest('.zenThemeMarketplaceItem')
.querySelector('.zenThemeMarketplaceItemUninstallButton')
.getAttribute('zen-mod-id');
event.target.setAttribute('disabled', true);
if (!event.target.hasAttribute('pressed')) {
await this.disableMod(modId);
browser.document.l10n.setAttributes(
mozToggle,
'zen-theme-marketplace-toggle-disabled-button'
);
if (mod.preferences) {
document
.getElementById(`zenThemeMarketplaceItemConfigureButton-${sanitizedName}`)
.setAttribute('hidden', true);
}
} else {
await this.enableMod(modId);
browser.document.l10n.setAttributes(
mozToggle,
'zen-theme-marketplace-toggle-enabled-button'
);
if (mod.preferences) {
document
.getElementById(`zenThemeMarketplaceItemConfigureButton-${sanitizedName}`)
.removeAttribute('hidden');
}
}
setTimeout(() => {
// We use a timeout to make sure the theme list has been updated before re-enabling the button.
event.target.removeAttribute('disabled');
}, 400);
});
fragment.querySelector('.zenThemeMarketplaceItemTitle').textContent = modName;
fragment.querySelector('.zenThemeMarketplaceItemDescription').textContent = mod.description;
fragment
.querySelector('.zenThemeMarketplaceItemUninstallButton')
.addEventListener('click', async (event) => {
const [msg] = await document.l10n.formatValues([
{ id: 'zen-theme-marketplace-remove-confirmation' },
]);
if (!confirm(msg)) {
return;
}
await this.removeMod(event.target.getAttribute('zen-mod-id'));
});
if (mod.homepage) {
const homepageButton = fragment.querySelector('.zenThemeMarketplaceItemHomepageButton');
homepageButton.addEventListener('click', () => {
// open the homepage url in a new tab
const url = mod.homepage;
window.open(url, '_blank');
});
}
if (mod.preferences) {
fragment
.querySelector('.zenThemeMarketplaceItemConfigureButton')
.addEventListener('click', () => {
dialog.showModal();
});
if (isModEnabled) {
fragment
.querySelector('.zenThemeMarketplaceItemConfigureButton')
.removeAttribute('hidden');
}
}
const preferences = await gZenMods.getModPreferences(mod);
if (preferences.length > 0) {
const preferencesWrapper = document.createXULElement('vbox');
preferencesWrapper.setAttribute('flex', '1');
for (const entry of preferences) {
const { property, label, type, placeholder, defaultValue } = entry;
switch (type) {
case 'dropdown': {
const { options } = entry;
const container = document.createXULElement('hbox');
container.classList.add('zenThemeMarketplaceItemPreference');
container.setAttribute('align', 'center');
container.setAttribute('role', 'group');
const menulist = document.createXULElement('menulist');
const menupopup = document.createXULElement('menupopup');
menulist.setAttribute('sizetopopup', 'none');
menulist.setAttribute('id', property + '-popup-menulist');
const savedValue = Services.prefs.getStringPref(property, defaultValue ?? 'none');
menulist.setAttribute('value', savedValue);
menulist.setAttribute('tooltiptext', property);
const defaultItem = document.createXULElement('menuitem');
defaultItem.setAttribute('value', 'none');
if (placeholder) {
defaultItem.setAttribute('label', placeholder || '-');
} else {
browser.document.l10n.setAttributes(
defaultItem,
'zen-theme-marketplace-dropdown-default-label'
);
}
menupopup.appendChild(defaultItem);
for (const option of options) {
const { label, value } = option;
const valueType = typeof value;
if (!['string', 'number'].includes(valueType)) {
console.log(
`[ZenSettings:ZenMods]: Warning, invalid data type received (${valueType}), skipping.`
);
continue;
}
const menuitem = document.createXULElement('menuitem');
menuitem.setAttribute('value', value.toString());
menuitem.setAttribute('label', label);
menupopup.appendChild(menuitem);
}
menulist.appendChild(menupopup);
menulist.addEventListener('command', () => {
const value = menulist.selectedItem.value;
let element = browser.document.getElementById(sanitizedName);
if (!element) {
element = browser.document.createElement('div');
element.style.display = 'none';
element.setAttribute('id', sanitizedName);
browser.document.body.appendChild(element);
}
element.setAttribute(property?.replaceAll(/\./g, '-'), value);
Services.prefs.setStringPref(property, value === 'none' ? '' : value);
this._triggerBuildUpdateWithoutRebuild();
});
const nameLabel = document.createXULElement('label');
nameLabel.setAttribute('flex', '1');
nameLabel.setAttribute('class', 'zenThemeMarketplaceItemPreferenceLabel');
nameLabel.setAttribute('value', label);
nameLabel.setAttribute('tooltiptext', property);
container.appendChild(nameLabel);
container.appendChild(menulist);
container.setAttribute('aria-labelledby', label);
preferencesWrapper.appendChild(container);
break;
}
case 'checkbox': {
const checkbox = window.MozXULElement.parseXULToFragment(`
<hbox class="zenThemeMarketplaceItemPreference">
<checkbox class="zenThemeMarketplaceItemPreferenceCheckbox"></checkbox>
</hbox>
`);
const checkboxElement = checkbox.querySelector(
'.zenThemeMarketplaceItemPreferenceCheckbox'
);
checkboxElement.setAttribute('label', label);
checkboxElement.setAttribute('tooltiptext', property);
checkboxElement.setAttribute('zen-pref', property);
// Checkbox only works with "true" and "false" values, it's not like HTML checkboxes.
if (Services.prefs.getBoolPref(property, defaultValue ?? false)) {
checkboxElement.setAttribute('checked', 'true');
}
checkboxElement.addEventListener('click', (event) => {
const target = event.target.closest('.zenThemeMarketplaceItemPreferenceCheckbox');
const key = target.getAttribute('zen-pref');
const checked = target.hasAttribute('checked');
if (!checked) {
target.removeAttribute('checked');
} else {
target.setAttribute('checked', 'true');
}
Services.prefs.setBoolPref(key, !checked);
});
preferencesWrapper.appendChild(checkbox);
break;
}
case 'string': {
const container = document.createXULElement('hbox');
container.classList.add('zenThemeMarketplaceItemPreference');
container.setAttribute('align', 'center');
container.setAttribute('role', 'group');
const savedValue = Services.prefs.getStringPref(property, defaultValue ?? '');
const sanitizedProperty = property?.replaceAll(/\./g, '-');
const input = document.createElement('input');
input.setAttribute('flex', '1');
input.setAttribute('type', 'text');
input.id = `${sanitizedProperty}-input`;
input.value = savedValue;
if (placeholder) {
input.setAttribute('placeholder', placeholder || '-');
} else {
browser.document.l10n.setAttributes(
input,
'zen-theme-marketplace-input-default-placeholder'
);
}
input.addEventListener(
'change',
gZenMods.debounce((event) => {
const value = event.target.value;
Services.prefs.setStringPref(property, value);
this._triggerBuildUpdateWithoutRebuild();
if (value === '') {
browser.document
.querySelector(':root')
.style.removeProperty(`--${sanitizedProperty}`);
} else {
browser.document
.querySelector(':root')
.style.setProperty(`--${sanitizedProperty}`, value);
}
}, 500)
);
const nameLabel = document.createXULElement('label');
nameLabel.setAttribute('flex', '1');
nameLabel.setAttribute('class', 'zenThemeMarketplaceItemPreferenceLabel');
nameLabel.setAttribute('value', label);
nameLabel.setAttribute('tooltiptext', property);
container.appendChild(nameLabel);
container.appendChild(input);
container.setAttribute('aria-labelledby', label);
preferencesWrapper.appendChild(container);
break;
}
default:
console.log(
`[ZenSettings:ZenMods]: Warning, unknown preference type received (${type}), skipping.`
);
continue;
}
}
contentDiv.appendChild(preferencesWrapper);
}
modList.appendChild(fragment);
}
this.modsList.replaceChildren(...modList.children);
modList.remove();
},
};
const kZenExtendedSidebar = 'zen.view.sidebar-expanded';
const kZenSingleToolbar = 'zen.view.use-single-toolbar';
var gZenLooksAndFeel = {
init() {
if (this.__hasInitialized) return;
this.__hasInitialized = true;
gZenMarketplaceManager.init();
for (const pref of [kZenExtendedSidebar, kZenSingleToolbar]) {
Services.prefs.addObserver(pref, this);
}
window.addEventListener('unload', () => {
for (const pref of [kZenExtendedSidebar, kZenSingleToolbar]) {
Services.prefs.removeObserver(pref, this);
}
});
this.setCompactModeStyle();
this.applySidebarLayout();
},
observe(subject, topic, data) {
this.applySidebarLayout();
},
applySidebarLayout() {
const isSingleToolbar = Services.prefs.getBoolPref(kZenSingleToolbar);
const isExtendedSidebar = Services.prefs.getBoolPref(kZenExtendedSidebar);
for (const layout of document.getElementById('zenLayoutList').children) {
layout.classList.remove('selected');
if (layout.getAttribute('layout') == 'single' && isSingleToolbar) {
layout.classList.add('selected');
} else if (
layout.getAttribute('layout') == 'multiple' &&
!isSingleToolbar &&
isExtendedSidebar
) {
layout.classList.add('selected');
} else if (layout.getAttribute('layout') == 'collapsed' && !isExtendedSidebar) {
layout.classList.add('selected');
}
}
if (this.__hasInitializedLayout) return;
this.__hasInitializedLayout = true;
for (const layout of document.getElementById('zenLayoutList').children) {
layout.addEventListener('click', () => {
if (layout.hasAttribute('disabled')) {
return;
}
for (const el of document.getElementById('zenLayoutList').children) {
el.classList.remove('selected');
}
layout.classList.add('selected');
Services.prefs.setBoolPref(
kZenExtendedSidebar,
layout.getAttribute('layout') != 'collapsed'
);
Services.prefs.setBoolPref(kZenSingleToolbar, layout.getAttribute('layout') == 'single');
});
}
},
setCompactModeStyle() {
const chooser = document.getElementById('zen-compact-mode-styles-form');
const radios = [...chooser.querySelectorAll('input')];
let value = '';
if (
Services.prefs.getBoolPref('zen.view.compact.hide-tabbar', false) &&
Services.prefs.getBoolPref('zen.view.compact.hide-toolbar', false)
) {
value = 'both';
} else {
value = Services.prefs.getBoolPref('zen.view.compact.hide-tabbar') ? 'left' : 'top';
}
chooser.querySelector(`[value='${value}']`).checked = true;
for (let radio of radios) {
radio.addEventListener('change', (e) => {
let value = e.target.value;
switch (value) {
case 'left':
Services.prefs.setBoolPref('zen.view.compact.hide-tabbar', true);
Services.prefs.setBoolPref('zen.view.compact.hide-toolbar', false);
break;
case 'top':
Services.prefs.setBoolPref('zen.view.compact.hide-tabbar', false);
Services.prefs.setBoolPref('zen.view.compact.hide-toolbar', true);
break;
default:
Services.prefs.setBoolPref('zen.view.compact.hide-tabbar', true);
Services.prefs.setBoolPref('zen.view.compact.hide-toolbar', true);
break;
}
});
}
},
};
var gZenWorkspacesSettings = {
init() {
var tabsUnloaderPrefListener = {
async observe(subject, topic, data) {
let buttonIndex = await confirmRestartPrompt(true, 1, true, true);
if (buttonIndex == CONFIRM_RESTART_PROMPT_RESTART_NOW) {
Services.startup.quit(Ci.nsIAppStartup.eAttemptQuit | Ci.nsIAppStartup.eRestart);
}
},
};
Services.prefs.addObserver('zen.tab-unloader.enabled', tabsUnloaderPrefListener);
Services.prefs.addObserver('zen.glance.enabled', tabsUnloaderPrefListener); // We can use the same listener for both prefs
Services.prefs.addObserver('zen.workspaces.separate-essentials', tabsUnloaderPrefListener);
Services.prefs.addObserver('zen.glance.activation-method', tabsUnloaderPrefListener);
window.addEventListener('unload', () => {
Services.prefs.removeObserver('zen.tab-unloader.enabled', tabsUnloaderPrefListener);
Services.prefs.removeObserver('zen.glance.enabled', tabsUnloaderPrefListener);
Services.prefs.removeObserver('zen.glance.activation-method', tabsUnloaderPrefListener);
Services.prefs.removeObserver('zen.workspaces.separate-essentials', tabsUnloaderPrefListener);
});
},
};
const ZEN_CKS_CLASS_BASE = 'zenCKSOption';
const ZEN_CKS_INPUT_FIELD_CLASS = `${ZEN_CKS_CLASS_BASE}-input`;
const ZEN_CKS_LABEL_CLASS = `${ZEN_CKS_CLASS_BASE}-label`;
const ZEN_CKS_WRAPPER_ID = `${ZEN_CKS_CLASS_BASE}-wrapper`;
const ZEN_CKS_GROUP_PREFIX = `${ZEN_CKS_CLASS_BASE}-group`;
const KEYBIND_ATTRIBUTE_KEY = 'key';
var zenMissingKeyboardShortcutL10n = {
key_quickRestart: 'zen-key-quick-restart',
key_delete: 'zen-key-delete',
goBackKb: 'zen-key-go-back',
goForwardKb: 'zen-key-go-forward',
key_enterFullScreen: 'zen-key-enter-full-screen',
key_exitFullScreen: 'zen-key-exit-full-screen',
key_aboutProcesses: 'zen-key-about-processes',
key_stop: 'zen-key-stop',
key_sanitize: 'zen-key-sanitize',
key_wrCaptureCmd: 'zen-key-wr-capture-cmd',
key_wrToggleCaptureSequenceCmd: 'zen-key-wr-toggle-capture-sequence-cmd',
key_undoCloseWindow: 'zen-key-undo-close-window',
key_selectTab1: 'zen-key-select-tab-1',
key_selectTab2: 'zen-key-select-tab-2',
key_selectTab3: 'zen-key-select-tab-3',
key_selectTab4: 'zen-key-select-tab-4',
key_selectTab5: 'zen-key-select-tab-5',
key_selectTab6: 'zen-key-select-tab-6',
key_selectTab7: 'zen-key-select-tab-7',
key_selectTab8: 'zen-key-select-tab-8',
key_selectLastTab: 'zen-key-select-tab-last',
key_showAllTabs: 'zen-key-show-all-tabs',
key_gotoHistory: 'zen-key-goto-history',
goHome: 'zen-key-go-home',
key_redo: 'zen-key-redo',
key_inspectorMac: 'zen-key-inspector-mac',
// Devtools
key_toggleToolbox: 'zen-devtools-toggle-shortcut',
key_browserToolbox: 'zen-devtools-toggle-browser-toolbox-shortcut',
key_browserConsole: 'zen-devtools-toggle-browser-console-shortcut',
key_responsiveDesignMode: 'zen-devtools-toggle-responsive-design-mode-shortcut',
key_inspector: 'zen-devtools-toggle-inspector-shortcut',
key_webconsole: 'zen-devtools-toggle-web-console-shortcut',
key_jsdebugger: 'zen-devtools-toggle-js-debugger-shortcut',
key_netmonitor: 'zen-devtools-toggle-net-monitor-shortcut',
key_styleeditor: 'zen-devtools-toggle-style-editor-shortcut',
key_performance: 'zen-devtools-toggle-performance-shortcut',
key_storage: 'zen-devtools-toggle-storage-shortcut',
key_dom: 'zen-devtools-toggle-dom-shortcut',
key_accessibility: 'zen-devtools-toggle-accessibility-shortcut',
};
var zenIgnoreKeyboardShortcutL10n = [
'zen-full-zoom-reduce-shortcut-alt-b',
'zen-full-zoom-reduce-shortcut-alt-a',
];
var gZenCKSSettings = {
async init() {
await this._initializeCKS();
if (this.__hasInitialized) return;
this.__hasInitialized = true;
this._currentActionID = null;
this._initializeEvents();
window.addEventListener('unload', () => {
this.__hasInitialized = false;
document.getElementById(ZEN_CKS_WRAPPER_ID).innerHTML = '';
});
},
_initializeEvents() {
const resetAllListener = this.resetAllShortcuts.bind(this);
const handleKeyDown = this._handleKeyDown.bind(this);
window.addEventListener('keydown', handleKeyDown);
const button = document.getElementById('zenCKSResetButton');
button.addEventListener('click', resetAllListener);
window.addEventListener('unload', () => {
window.removeEventListener('keydown', handleKeyDown);
button.removeEventListener('click', resetAllListener);
});
},
async resetAllShortcuts() {
let buttonIndex = await confirmRestartPrompt(true, 1, true, false);
if (buttonIndex == CONFIRM_RESTART_PROMPT_RESTART_NOW) {
await gZenKeyboardShortcutsManager.resetAllShortcuts();
Services.startup.quit(Ci.nsIAppStartup.eAttemptQuit | Ci.nsIAppStartup.eRestart);
}
},
async _initializeCKS() {
let wrapper = document.getElementById(ZEN_CKS_WRAPPER_ID);
wrapper.innerHTML = '';
let shortcuts = await gZenKeyboardShortcutsManager.getModifiableShortcuts();
if (!shortcuts) {
throw Error('No shortcuts defined!');
}
// Generate section per each group
for (let group of VALID_SHORTCUT_GROUPS) {
let groupClass = `${ZEN_CKS_GROUP_PREFIX}-${group}`;
if (!wrapper.querySelector(`[data-group="${groupClass}"]`)) {
let groupElem = document.createElement('h2');
groupElem.setAttribute('data-group', groupClass);
document.l10n.setAttributes(groupElem, groupClass);
wrapper.appendChild(groupElem);
}
}
for (let shortcut of shortcuts) {
const keyID = shortcut.getID();
const action = shortcut.getAction();
const l10nID = shortcut.getL10NID();
const group = shortcut.getGroup();
const keyInString = shortcut.toUserString();
const labelValue = zenMissingKeyboardShortcutL10n[keyID] ?? l10nID;
if (zenIgnoreKeyboardShortcutL10n.includes(labelValue)) {
continue;
}
let fragment = window.MozXULElement.parseXULToFragment(`
<hbox class="${ZEN_CKS_CLASS_BASE}">
<label class="${ZEN_CKS_LABEL_CLASS}" for="${ZEN_CKS_CLASS_BASE}-${keyID}"></label>
<vbox flex="1">
<html:input readonly="1" class="${ZEN_CKS_INPUT_FIELD_CLASS}" id="${ZEN_CKS_INPUT_FIELD_CLASS}-${keyID}" />
</vbox>
</hbox>
`);
const label = fragment.querySelector(`.${ZEN_CKS_LABEL_CLASS}`);
if (!labelValue) {
label.textContent = action; // Just in case
} else {
document.l10n.setAttributes(label, labelValue);
}
let input = fragment.querySelector(`.${ZEN_CKS_INPUT_FIELD_CLASS}`);
if (keyInString && !shortcut.isEmpty()) {
input.value = keyInString;
} else {
this._resetShortcut(input);
}
input.setAttribute(KEYBIND_ATTRIBUTE_KEY, keyID);
input.setAttribute('data-group', group);
input.setAttribute('data-id', keyID);
input.addEventListener('focus', (event) => {
const value = event.target.getAttribute(KEYBIND_ATTRIBUTE_KEY);
this._currentActionID = event.target.getAttribute('data-id');
event.target.classList.add(`${ZEN_CKS_INPUT_FIELD_CLASS}-editing`);
this._hasSafed = true;
});
input.addEventListener('editDone', (event) => {
const target = event.target;
target.classList.add(`${ZEN_CKS_INPUT_FIELD_CLASS}-editing`);
});
input.addEventListener('blur', (event) => {
const target = event.target;
target.classList.remove(`${ZEN_CKS_INPUT_FIELD_CLASS}-editing`);
if (!this._hasSafed) {
target.classList.add(`${ZEN_CKS_INPUT_FIELD_CLASS}-unsafed`);
if (!target.nextElementSibling) {
target.after(
window.MozXULElement.parseXULToFragment(`
<label class="${ZEN_CKS_CLASS_BASE}-unsafed" data-l10n-id="zen-key-unsaved"></label>
`)
);
target.value = 'Not set';
}
} else {
target.classList.remove(`${ZEN_CKS_INPUT_FIELD_CLASS}-unsafed`);
const sibling = target.nextElementSibling;
if (sibling && sibling.classList.contains(`${ZEN_CKS_CLASS_BASE}-unsafed`)) {
sibling.remove();
}
}
if (target.classList.contains(`${ZEN_CKS_INPUT_FIELD_CLASS}-not-set`)) {
target.label = 'Not set';
}
});
const groupElem = wrapper.querySelector(`[data-group="${ZEN_CKS_GROUP_PREFIX}-${group}"]`);
groupElem.after(fragment);
}
},
async _resetShortcut(input) {
input.value = 'Not set';
input.classList.remove(`${ZEN_CKS_INPUT_FIELD_CLASS}-invalid`);
input.classList.remove(`${ZEN_CKS_INPUT_FIELD_CLASS}-editing`);
input.classList.add(`${ZEN_CKS_INPUT_FIELD_CLASS}-not-set`);
if (this._currentActionID) {
this._editDone();
await gZenKeyboardShortcutsManager.setShortcut(this._currentActionID, null, null);
}
},
_editDone(shortcut, modifiers) {
// Check if we have a valid key
if (!shortcut || !modifiers) {
return;
}
gZenKeyboardShortcutsManager.setShortcut(this._currentActionID, shortcut, modifiers);
this._currentActionID = null;
},
//TODO Check for duplicates
async _handleKeyDown(event) {
if (!this._currentActionID || document.hidden) {
return;
}
event.preventDefault();
let input = document.querySelector(
`.${ZEN_CKS_INPUT_FIELD_CLASS}[${KEYBIND_ATTRIBUTE_KEY}="${this._currentActionID}"]`
);
const modifiers = new nsKeyShortcutModifiers(
event.ctrlKey,
event.altKey,
event.shiftKey,
event.metaKey,
false
);
const modifiersActive = modifiers.areAnyActive();
input.classList.remove(`${ZEN_CKS_INPUT_FIELD_CLASS}-not-set`);
// First, try to read the *physical* key via event.code.
// If event.code is like "KeyS", "KeyA", ..., strip off "Key" → "S".
// Otherwise, fall back to event.key (e.g. "F5", "Enter", etc.).
let shortcut;
if (event.code && event.code.startsWith('Key')) shortcut = event.code.slice(3);
else shortcut = event.key;
shortcut = shortcut.replace(/Ctrl|Control|Shift|Alt|Option|Cmd|Meta/, ''); // Remove all modifiers
if (shortcut == 'Tab' && !modifiersActive) {
input.classList.remove(`${ZEN_CKS_INPUT_FIELD_CLASS}-not-set`);
input.classList.remove(`${ZEN_CKS_INPUT_FIELD_CLASS}-editing`);
this._latestValidKey = null;
return;
} else if (shortcut == 'Escape' && !modifiersActive) {
const hasConflicts = gZenKeyboardShortcutsManager.checkForConflicts(
this._latestValidKey ? this._latestValidKey : shortcut,
this._latestModifier ? this._latestModifier : modifiers,
this._currentActionID
);
if (!this._latestValidKey && !this._latestModifier) {
} else if (!this._latestValidKey || hasConflicts) {
if (!input.classList.contains(`${ZEN_CKS_INPUT_FIELD_CLASS}-invalid`)) {
input.classList.add(`${ZEN_CKS_INPUT_FIELD_CLASS}-invalid`);
}
input.classList.remove(`${ZEN_CKS_INPUT_FIELD_CLASS}-unsafed`);
if (hasConflicts && !input.nextElementSibling) {
input.after(
window.MozXULElement.parseXULToFragment(`
<label class="${ZEN_CKS_CLASS_BASE}-conflict" data-l10n-id="zen-key-conflict"></label>
`)
);
}
} else {
input.classList.remove(`${ZEN_CKS_INPUT_FIELD_CLASS}-editing`);
this._editDone(this._latestValidKey, this._latestModifier);
if (this.name == 'Not set') {
input.classList.add(`${ZEN_CKS_INPUT_FIELD_CLASS}-not-set`);
}
this._latestValidKey = null;
this._latestModifier = null;
input.classList.remove(`${ZEN_CKS_INPUT_FIELD_CLASS}-invalid`);
input.classList.add(`${ZEN_CKS_INPUT_FIELD_CLASS}-valid`);
setTimeout(() => {
input.classList.remove(`${ZEN_CKS_INPUT_FIELD_CLASS}-valid`);
}, 1000);
const sibling = input.nextElementSibling;
if (sibling && sibling.classList.contains(`${ZEN_CKS_CLASS_BASE}-conflict`)) {
sibling.remove();
}
}
this._hasSafed = true;
input.blur();
this._currentActionID = null;
return;
} else if (shortcut == 'Backspace' && !modifiersActive) {
this._resetShortcut(input);
this._latestValidKey = null;
this._latestModifier = null;
this._hasSafed = true;
const sibling = input.nextElementSibling;
if (sibling && sibling.classList.contains(`${ZEN_CKS_CLASS_BASE}-conflict`)) {
sibling.remove();
}
return;
}
this._latestModifier = modifiers;
this._hasSafed = false;
input.classList.remove(`${ZEN_CKS_INPUT_FIELD_CLASS}-invalid`);
input.classList.remove(`${ZEN_CKS_INPUT_FIELD_CLASS}-not-set`);
input.value = modifiers.toUserString() + shortcut;
this._latestValidKey = shortcut;
},
};
Preferences.addAll([
{
id: 'zen.view.compact.hide-toolbar',
type: 'bool',
default: false,
},
{
id: 'zen.view.compact.toolbar-flash-popup',
type: 'bool',
default: true,
},
{
id: 'zen.workspaces.hide-default-container-indicator',
type: 'bool',
default: true,
},
{
id: 'zen.tab-unloader.enabled',
type: 'bool',
default: true,
},
{
id: 'zen.tab-unloader.timeout-minutes',
type: 'int',
default: 10,
},
{
id: 'zen.pinned-tab-manager.restore-pinned-tabs-to-pinned-url',
type: 'bool',
default: true,
},
{
id: 'zen.pinned-tab-manager.close-shortcut-behavior',
type: 'string',
default: 'switch',
},
{
id: 'zen.workspaces.force-container-workspace',
type: 'bool',
default: true,
},
{
id: 'zen.workspaces.open-new-tab-if-last-unpinned-tab-is-closed',
type: 'bool',
default: true,
},
{
id: 'zen.tabs.show-newtab-under',
type: 'bool',
default: false,
},
{
id: 'zen.glance.activation-method',
type: 'string',
default: 'ctrl',
},
{
id: 'zen.glance.enabled',
type: 'bool',
default: true,
},
{
id: 'zen.view.compact.color-toolbar',
type: 'bool',
default: true,
},
{
id: 'zen.urlbar.behavior',
type: 'string',
default: 'float',
},
{
id: 'zen.view.compact.color-sidebar',
type: 'bool',
default: true,
},
{
id: 'zen.workspaces.separate-essentials',
type: 'bool',
default: false,
},
{
id: 'zen.tabs.show-newtab-vertical',
type: 'bool',
default: true,
},
{
id: 'zen.view.show-newtab-button-border-top',
type: 'bool',
default: false,
},
{
id: 'zen.view.show-newtab-button-top',
type: 'bool',
default: true,
},
{
id: 'media.videocontrols.picture-in-picture.enabled',
type: 'bool',
default: true,
},
{
id: 'zen.workspaces.continue-where-left-off',
type: 'bool',
default: false,
},
{
id: 'zen.mods.auto-update',
type: 'bool',
default: true,
},
]);