Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 19 additions & 5 deletions addon/js/background/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -221,11 +221,6 @@
})()

const actionApi = browser.browserAction || browser.action || null
if (actionApi && actionApi.onClicked) {
actionApi.onClicked.addListener(function () {
browser.runtime.openOptionsPage()
})
}

const notificationsApi = browser.notifications || null
if (notificationsApi && notificationsApi.onClicked) {
Expand Down Expand Up @@ -263,6 +258,24 @@
else shadowArms.set(key, { count: existing.count - 1, at: existing.at })
})

/**
* Hides page actions from every open tab.
* @returns {void}
*/
function hidePageActions() {
if (!browser.pageAction) return
browser.tabs
.query({})
.then((tabs) =>
Promise.all(
tabs
.filter((tab) => tab.id != null && pendingPicker.get(tab.id)?.mode !== 'pageAction')
.map((tab) => browser.pageAction.hide(tab.id))
)
)
.catch((e) => logger.debug('pageAction.hide failed', e))
}

browser.storage.onChanged.addListener((changes, area) => {
if (area !== 'local') return
let hasSiteChange = false
Expand All @@ -279,6 +292,7 @@
if (hasSiteChange) refreshWorkerPolyfillSites()
if (Object.keys(patch).length === 0) return
settings.set(patch)
if (patch.hidePageAction) hidePageActions()
})

browser.tabs.onRemoved.addListener((tabId) => {
Expand Down
115 changes: 97 additions & 18 deletions addon/js/background/messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
const http = webhid.import('http')
const logger = webhid.import('logger')
const isChromium = webhid.import('isChromium')
const globalSettingKey = webhid.import('globalSettingKey')
const decodeDeviceCollections = webhid.import('decodeDeviceCollections')
const { deviceCache, pendingPicker, permissionsPolicy, allowedCrossOrigin } =
webhid.import('bgState')
Expand Down Expand Up @@ -573,6 +574,31 @@
return false
}

/**
* Shows the page action for a tab that has used the WebHID API.
* @param {object} request
* @param {object} sender
* @param {function(*): void} sendResponse
* @returns {boolean}
*/
function handleShowPageAction(request, sender, sendResponse) {
const tabId = sender.tab != null ? sender.tab.id : undefined
if (isChromium || !browser.pageAction || tabId == null) {
sendResponse({})
return false
}
browser.storage.local
.get(globalSettingKey('hidePageAction'))
.then((values) => {
if (!values[globalSettingKey('hidePageAction')]) {
return browser.pageAction.show(tabId)
}
})
.catch((e) => logger.debug('pageAction.show failed', e))
.finally(() => sendResponse({}))
return true
}

/**
* @param {object} request
* @param {object} sender
Expand Down Expand Up @@ -714,6 +740,32 @@
.catch(() => {})
}

/**
* Restores the page action visibility and popup after a picker closes.
* @param {number} tabId
* @returns {void}
*/
function restorePageAction(tabId) {
if (isChromium || !browser.pageAction || tabId == null) return
const key = globalSettingKey('hidePageAction')
browser.storage.local
.get(key)
.then((values) => {
const visibility = values[key]
? browser.pageAction.hide(tabId)
: browser.pageAction.show(tabId)
return Promise.all([
visibility,
browser.pageAction.setIcon({ tabId, path: 'icons/gamepad.svg' }),
browser.pageAction.setPopup({
tabId,
popup: 'js/internal/pages/popup/index.html'
})
])
})
.catch((e) => logger.debug('restore pageAction failed', e))
}

/**
* Opens the picker as a pageAction popup, alerting the user via a
* notification when the requesting tab is not the active one.
Expand All @@ -722,17 +774,26 @@
* @param {string} origin
* @returns {void}
*/
function openPickerPageAction(req, tabId, origin) {
function openPickerPageAction(tabId, origin) {
if (isChromium) return
browser.pageAction.setIcon({
tabId,
path: 'icons/gamepad.alert.svg'
})
browser.pageAction.setPopup({
tabId,
popup: 'js/internal/pages/picker/index.html'
})
if (browser.pageAction.openPopup) browser.pageAction.openPopup().catch(() => {})
browser.pageAction
.show(tabId)
.then(() =>
Promise.all([
browser.pageAction.setIcon({
tabId,
path: 'icons/gamepad.alert.svg'
}),
browser.pageAction.setPopup({
tabId,
popup: 'js/internal/pages/picker/index.html'
})
])
)
.then(() => {
if (browser.pageAction.openPopup) return browser.pageAction.openPopup()
})
.catch((e) => logger.debug('openPickerPageAction failed', e))
browser.tabs
.query({ active: true, currentWindow: true })
.then((tabs) => {
Expand All @@ -749,6 +810,27 @@
.catch(() => {})
}

/**
* Cancels a page-action picker after the requesting page times out.
* @param {object} request
* @param {object} sender
* @param {function(*): void} sendResponse
* @returns {boolean}
*/
function handleCancelPicker(request, sender, sendResponse) {
const tabId = sender.tab != null ? sender.tab.id : undefined
const req = tabId != null ? pendingPicker.get(tabId) : null
if (req && req.requestId === request.requestId) {
pendingPicker.delete(tabId)
if (req.mode === 'pageAction') {
restorePageAction(tabId)
if (browser.notifications) browser.notifications.clear('webhid-picker').catch(() => {})
}
}
sendResponse({ ok: true })
return false
}

/**
* @param {object} request
* @param {object} sender
Expand All @@ -773,7 +855,7 @@
if (req.mode === 'window') {
openPickerWindow()
} else {
openPickerPageAction(req, tabId, request.origin)
openPickerPageAction(tabId, request.origin)
}
sendResponse({ ok: true })
return false
Expand Down Expand Up @@ -915,13 +997,8 @@
if (tabId == null && pendingPicker.size > 0) tabId = [...pendingPicker.keys()][0]
const req = tabId != null ? pendingPicker.get(tabId) : null
if (tabId != null) pendingPicker.delete(tabId)
const reqMode = req?.mode
if (reqMode === 'pageAction' && !isChromium) {
browser.pageAction.setIcon({ tabId, path: 'icons/gamepad.svg' })
browser.pageAction.setPopup({
tabId,
popup: 'js/internal/pages/popup/index.html'
})
if (req?.mode === 'pageAction' && !isChromium) {
restorePageAction(tabId)
if (browser.notifications) browser.notifications.clear('webhid-picker').catch(() => {})
}
if (request.windowId != null) browser.windows.remove(request.windowId).catch(() => {})
Expand Down Expand Up @@ -959,13 +1036,15 @@
unpairDevice: handleUnpairDevice,
getAllowedDevices: handleGetAllowedDevices,
deviceCountChanged: handleDeviceCountChanged,
showPageAction: handleShowPageAction,
getDeviceCache: handleGetDeviceCache,
getDeviceInfo: handleGetDeviceInfo,
fetchResource: handleFetchResource,
getCspInfo: handleGetCspInfo,
getFrameOrigins: handleGetFrameOrigins,
getWorkerBundle: handleGetWorkerBundle,
showPicker: handleShowPicker,
cancelPicker: handleCancelPicker,
getPendingPicker: handleGetPendingPicker,
getPolicy: handleGetPolicy,
armShadowSpawn: handleArmShadowSpawn,
Expand Down
36 changes: 36 additions & 0 deletions addon/js/content/isolated/bridge.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,32 @@
'pickerResult'
])

const PAGE_ACTION_API_ACTIONS = new Set([
'getPolicy',
'getPairedDevices',
'enumerate',
'requestDevice',
'open',
'close',
'sendReport',
'receiveFeatureReport',
'sendFeatureReport',
'unpairDevice'
])
let pageActionMarked = false

/**
* Marks the current tab as using WebHID so its page action becomes visible.
* @returns {void}
*/
function markPageActionUsed() {
if (pageActionMarked || settings.hidePageAction) return
pageActionMarked = true
sendBackgroundRequest({ action: 'showPageAction' }).catch(() => {
pageActionMarked = false
})
}

/** @type {Set<string>} */
const openDevices = new Set()
/** @type {Map<string, Map<string, string[]>>} deviceId -> origin -> LIFO
Expand Down Expand Up @@ -332,6 +358,9 @@
/** @type {import("./types.js").SettingsStore} */
const settings = createSettingsStore(webhid.import('GLOBAL_DEFAULTS'))
logger.bindSettings(settings)
settings.on('hidePageAction', (hidden) => {
if (!hidden) pageActionMarked = false
})
/** @type {Map<Window, MessagePort>} */
const pagePorts = new Map()
/** @type {Map<MessagePort, Window>} */
Expand Down Expand Up @@ -1182,6 +1211,11 @@
})
.catch((e) => logger.debug('showPicker send failed', e))
const pickerTimeout = setTimeout(() => {
browser.runtime.onMessage.removeListener(onPickerResult)
sendBackgroundRequest({
action: 'cancelPicker',
requestId: data.id
}).catch((e) => logger.debug('cancelPicker send failed', e))
replyToPage({
type: 'response',
id: data.id,
Expand Down Expand Up @@ -1400,6 +1434,7 @@
if (!data || data.id === undefined) return

logger.debug('req action=' + data.action + ' id=' + data.id)
if (PAGE_ACTION_API_ACTIONS.has(data.action)) markPageActionUsed()

const handler = REQUEST_HANDLERS[data.action]
if (handler) {
Expand Down Expand Up @@ -1561,6 +1596,7 @@
: msg.type === 'sendFeature'
? 'sendFeatureReport'
: 'receiveFeatureReport'
markPageActionUsed()
const payload = { deviceId, reportId: msg.reportId }
if (msg.type === 'send' || msg.type === 'sendFeature') payload.data = msg.data
const reqId = allocateDataReqId()
Expand Down
1 change: 1 addition & 0 deletions addon/js/content/isolated/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
* @property {string} dataPlane
* @property {number} logLevel
* @property {boolean} daemonAsNmHost
* @property {boolean} hidePageAction
* @property {string} devicePickerMode
* @property {boolean} workerPolyfillEnabled
* @property {boolean} allowActivationlessRequestDevice
Expand Down
1 change: 1 addition & 0 deletions addon/js/content/main/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
* @property {string} dataPlane
* @property {number} logLevel
* @property {boolean} daemonAsNmHost
* @property {boolean} hidePageAction
* @property {string} devicePickerMode
* @property {boolean} workerPolyfillEnabled
* @property {boolean} allowActivationlessRequestDevice
Expand Down
7 changes: 7 additions & 0 deletions addon/js/internal/pages/popup/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
const groupDevices = webhid.import('groupDevices')
const t = webhid.import('t')
const localizeHTML = webhid.import('localizeHTML')
const loadGlobalSettings = webhid.import('loadGlobalSettings')
const loadEffectiveSettings = webhid.import('loadEffectiveSettings')
const GLOBAL_DEFAULTS = webhid.import('GLOBAL_DEFAULTS')
const settingsUi = webhid.import('settingsUi')
Expand All @@ -18,6 +19,7 @@

localizeHTML(document)
initInfoPopovers(document)
const globalSettings = await loadGlobalSettings()

/** @type {object|undefined} */
let tab
Expand Down Expand Up @@ -235,6 +237,11 @@
const viewDevices = document.getElementById('view-devices')
const viewSettings = document.getElementById('view-settings')
const btnSettings = document.getElementById('btn-settings')
const startsInSettings = window.location.hash === '#settings' && !globalSettings.hidePageAction
viewDevices.hidden = startsInSettings
viewSettings.hidden = !startsInSettings
btnSettings.classList.toggle('active', startsInSettings)

btnSettings.addEventListener('click', () => {
const open = viewSettings.hidden
viewDevices.hidden = open
Expand Down
16 changes: 16 additions & 0 deletions addon/js/internal/pages/settings/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,22 @@ <h3 id="settingsDaemonAsNmHost" data-i18n="settingsDaemonAsNmHost">Daemon as NM
<span class="slider" role="presentation"></span>
</label>
</div>
<div class="setting">
<div class="setting-info">
<h3 id="settingsHidePageAction">Hide Page Action</h3>
<div class="info-popover">
<p>
Keep the page action hidden, even when a site uses the WebHID API. The browser action
opens the device view by default.
</p>
</div>
</div>
<label class="toggle">
<input type="checkbox" id="hidePageAction" aria-labelledby="settingsHidePageAction" />
<span class="slider" role="presentation"></span>
</label>
</div>

<div class="setting">
<div class="setting-info">
<h3 id="settingsDataPlane" data-i18n="settingsDataPlane">Data Plane</h3>
Expand Down
2 changes: 2 additions & 0 deletions addon/js/internal/pages/settings/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

for (const key of [
'daemonAsNmHost',
'hidePageAction',
'workerPolyfillEnabled',
'allowActivationlessRequestDevice'
]) {
Expand Down Expand Up @@ -48,6 +49,7 @@

for (const key of [
'daemonAsNmHost',
'hidePageAction',
'workerPolyfillEnabled',
'allowActivationlessRequestDevice'
]) {
Expand Down
Loading