Release Vulpes OS 2.7-preview.22
This commit is contained in:
parent
200d1fb235
commit
a138f97877
294 changed files with 515786 additions and 187 deletions
294
host/actors/KeypadChild.sys.mjs
Normal file
294
host/actors/KeypadChild.sys.mjs
Normal file
|
|
@ -0,0 +1,294 @@
|
|||
import {nextKeypadTarget} from 'resource://vulpes/services/keypad-navigation.mjs';
|
||||
import {KeypadText} from 'resource://vulpes/services/keypad-text.mjs';
|
||||
import {InputEditor} from 'resource://vulpes/services/input-editor.mjs';
|
||||
|
||||
export class KeypadChild extends JSWindowActorChild {
|
||||
receiveMessage(message) {
|
||||
if (message.name === 'Vulpes:FocusBrowserToolbar') {
|
||||
if (this.document.location.hostname !== 'system.localhost') return;
|
||||
const bar = [...this.document.querySelectorAll('.chrome .urlbar')].find(element => {
|
||||
if (!element.checkVisibility({checkOpacity:true,checkVisibilityCSS:true})) return false;
|
||||
const rect = element.getBoundingClientRect();
|
||||
return rect.width > 0 && element.contains(this.document.elementFromPoint(
|
||||
rect.x + rect.width / 2, rect.y + rect.height / 2));
|
||||
});
|
||||
if (bar) this.select(bar);
|
||||
return;
|
||||
}
|
||||
if (message.name !== 'Vulpes:Navigate') return;
|
||||
const {key, code = '', keyCode = 0, repeat = false} = message.data;
|
||||
if (typeof key !== 'string' || key.length > 32) return;
|
||||
const document = this.document, win = this.contentWindow;
|
||||
if (key === 'F2') {
|
||||
if (!repeat) this.sendAsyncMessage('Vulpes:KeypadNotifications');
|
||||
return;
|
||||
}
|
||||
const exposed = element => {
|
||||
if (!element.checkVisibility({checkOpacity:true,checkVisibilityCSS:true})) return false;
|
||||
const rect = element.getBoundingClientRect();
|
||||
const x = rect.x + rect.width / 2, y = rect.y + rect.height / 2;
|
||||
return x >= 0 && x < win.innerWidth && y >= 0 && y < win.innerHeight &&
|
||||
element.contains(document.elementFromPoint(x, y));
|
||||
};
|
||||
this.composer ||= new KeypadText();
|
||||
if (key==='#' && !repeat) {
|
||||
this.hashHoldHandled=false;
|
||||
this.hashStartMode=this.composer.mode;
|
||||
this.hashStartReturnMode=this.composer.returnMode;
|
||||
}
|
||||
const target = document.activeElement;
|
||||
const editing = !target?.readOnly && (target?.isContentEditable || target?.localName === 'textarea' ||
|
||||
(target?.localName === 'input' && ['text','search','email','url','tel','password','number'].includes(target.type)));
|
||||
const modal = [...document.querySelectorAll('[aria-modal="true"],[data-type="confirm"],[data-type="action"]')]
|
||||
.reverse().find(exposed);
|
||||
if (!editing || modal) { this.composer.commit(); this.hint?.remove(); }
|
||||
if (modal && !this.modalAnchor && target?.localName === 'select') this.modalAnchor = target;
|
||||
if (!modal) this.modalAnchor = null;
|
||||
const browserControls = target?.closest?.('.chrome .controls');
|
||||
const inBrowserToolbar = browserControls && document.location.hostname === 'system.localhost';
|
||||
// Keep toolbar keys in Gaia; Down returns to the browser's content frame.
|
||||
if (!editing && !modal && (!inBrowserToolbar || key === 'ArrowDown')) {
|
||||
for (const frame of document.querySelectorAll('iframe')) {
|
||||
if (!frame.checkVisibility({checkOpacity:true,checkVisibilityCSS:true})) continue;
|
||||
const rect = frame.getBoundingClientRect();
|
||||
if (rect.width <= 0 || rect.height <= 0) continue;
|
||||
if (document.elementFromPoint(rect.x + rect.width / 2, rect.y + rect.height / 2) !== frame) continue;
|
||||
if (frame.browsingContext) {
|
||||
if (inBrowserToolbar) frame.focus();
|
||||
const browserContent = message.data.browserContent ||
|
||||
document.location.hostname === 'system.localhost' &&
|
||||
!!frame.closest('.appWindow')?.querySelector('.chrome .urlbar');
|
||||
this.sendAsyncMessage('Vulpes:KeypadRoute', {
|
||||
contextId:frame.browsingContext.id, key, code, keyCode, repeat, browserContent,
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
// The Nokia's red key maps to F4; keep Gaia's own call controls.
|
||||
if (key === 'F4' || key === 'EndCall') {
|
||||
if (repeat) return;
|
||||
this.composer.commit(); this.hint?.remove();
|
||||
const hangup = [...document.querySelectorAll('#callbar-hang-up,#incoming-reject')].find(exposed);
|
||||
if (hangup && !hangup.disabled) hangup.click();
|
||||
else this.sendAsyncMessage('Vulpes:KeypadHome');
|
||||
return;
|
||||
}
|
||||
// Gaia's dial pad is touch-driven, including its own number model.
|
||||
if (!modal && document.location.hostname === 'communications.localhost' && !editing) {
|
||||
const digit = /^[0-9*#]$/.test(key) ? key : key === 'Backspace' ? 'delete' : null;
|
||||
const button = digit && document.querySelector('[role="key"][data-value="'+digit+'"]');
|
||||
if (button && exposed(button)) { this.activate(button); return; }
|
||||
}
|
||||
if (!modal && key === 'F1') {
|
||||
const button = [...document.querySelectorAll('#callbar-answer,#incoming-answer,#keypad-callbar-call-action')].find(exposed);
|
||||
if (button && !button.disabled) { button.click(); return; }
|
||||
}
|
||||
if (editing && !modal && !['Escape', 'BrowserBack', 'GoBack', 'ContextMenu', 'ArrowUp', 'ArrowDown'].includes(key)) {
|
||||
if (key==='#' && repeat && !this.hashHoldHandled &&
|
||||
Services.env.get('VULPES_DEVICE')==='beatles' && !['tel','number'].includes(target.type)) {
|
||||
this.hashHoldHandled=true;
|
||||
this.composer.setT9(this.hashStartMode!=='t9',
|
||||
this.hashStartMode==='t9' ? this.hashStartReturnMode : this.hashStartMode);
|
||||
if (this.composer.mode==='t9') this.loadT9Dictionary();
|
||||
this.showMode(target);
|
||||
return;
|
||||
}
|
||||
if (!repeat || !/^[0-9*#]$/.test(key)) this.editKey(target, key);
|
||||
return;
|
||||
}
|
||||
if (['Escape', 'BrowserBack', 'GoBack', 'Backspace'].includes(key)) {
|
||||
this.composer.commit(); this.hint?.remove();
|
||||
const addressCancel = document.querySelector('#rocketbar.active #rocketbar-cancel');
|
||||
if (addressCancel && exposed(addressCancel)) { addressCancel.click(); return; }
|
||||
const cancel = modal?.querySelector('[data-action="cancel"],button.cancel,button[data-l10n-id="cancel"]');
|
||||
if (cancel) { cancel.click(); this.modalAnchor?.focus({preventScroll:true}); return; }
|
||||
const header = [...document.querySelectorAll('gaia-header[action="back"],gaia-header[action="close"]')]
|
||||
.find(exposed);
|
||||
const back = header?.shadowRoot?.querySelector('.action-button') || header?.querySelector('.action-button') ||
|
||||
[...document.querySelectorAll('[data-action="back"],button.back,[role="button"].back')]
|
||||
.find(exposed);
|
||||
if (back) back.click();
|
||||
else this.sendAsyncMessage('Vulpes:KeypadHome');
|
||||
return;
|
||||
}
|
||||
if (key === 'ContextMenu') {
|
||||
this.sendAsyncMessage('Vulpes:KeypadCards');
|
||||
return;
|
||||
}
|
||||
if (!modal && target?.localName === 'select' && ['Enter','Accept','Select'].includes(key)) {
|
||||
target.click();
|
||||
return;
|
||||
}
|
||||
if (!modal && target?.type === 'range' && ['ArrowLeft','ArrowRight'].includes(key)) {
|
||||
key === 'ArrowLeft' ? target.stepDown() : target.stepUp();
|
||||
target.dispatchEvent(new win.Event('input', {bubbles:true}));
|
||||
target.dispatchEvent(new win.Event('change', {bubbles:true}));
|
||||
return;
|
||||
}
|
||||
this.composer.commit(); this.hint?.remove();
|
||||
const arrows = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'];
|
||||
if (!arrows.includes(key) && !['Enter', 'Accept', 'Select'].includes(key)) {
|
||||
return;
|
||||
}
|
||||
const root = modal || (inBrowserToolbar ? browserControls : document);
|
||||
const candidates = [...root.querySelectorAll('a[href],button,input,select,textarea,[contenteditable="true"],[role="button"],[role="key"],[role="tab"],[tabindex]:not([tabindex="-1"]),[data-vulpes-keypad-focus],gaia-app-icon,gaia-pin-card,gaia-switch,gaia-checkbox')];
|
||||
if (inBrowserToolbar) candidates.push(...browserControls.querySelectorAll('.urlbar'));
|
||||
for (const header of root.querySelectorAll('gaia-header'))
|
||||
candidates.push(...(header.shadowRoot?.querySelectorAll('button') || []));
|
||||
const elements = [...new Set(candidates)].filter(element => {
|
||||
const rect = element.getBoundingClientRect();
|
||||
return !element.disabled && !element.hasAttribute('disabled') && !element.readOnly && element.getAttribute('aria-disabled') !== 'true' &&
|
||||
element.checkVisibility({checkOpacity:true,checkVisibilityCSS:true}) &&
|
||||
rect.width > 0 && rect.height > 0 && rect.right > 0 && rect.left < win.innerWidth;
|
||||
});
|
||||
if (!elements.length) {
|
||||
if (key === 'ArrowUp' && message.data.browserContent && win.scrollY <= 0)
|
||||
this.sendAsyncMessage('Vulpes:KeypadToolbar');
|
||||
else win.scrollBy({top:key === 'ArrowUp' ? -80 : key === 'ArrowDown' ? 80 : 0});
|
||||
return;
|
||||
}
|
||||
const focused = elements.includes(document.activeElement) ? document.activeElement : null;
|
||||
const compactSelection = document.documentElement.dataset.vulpesLayout === 'compact' ?
|
||||
document.querySelector('[data-vulpes-compact-selected]') : null;
|
||||
const current = elements.indexOf(focused || compactSelection || (this.selected?.isConnected && this.selected));
|
||||
const rects = elements.map(element => {
|
||||
const rect = element.getBoundingClientRect();
|
||||
return {x:rect.x + rect.width / 2, y:rect.y + rect.height / 2};
|
||||
});
|
||||
if (!arrows.includes(key)) {
|
||||
this.activate(elements[current < 0 ? 0 : current]);
|
||||
if (modal && !modal.isConnected) this.modalAnchor?.focus({preventScroll:true});
|
||||
return;
|
||||
}
|
||||
const index = nextKeypadTarget(rects, current, key);
|
||||
if (key === 'ArrowUp' && message.data.browserContent && win.scrollY <= 0 && (current < 0 || index < 0)) {
|
||||
this.sendAsyncMessage('Vulpes:KeypadToolbar');
|
||||
return;
|
||||
}
|
||||
if (index < 0) {
|
||||
win.scrollBy({top:key === 'ArrowUp' ? -80 : key === 'ArrowDown' ? 80 : 0});
|
||||
return;
|
||||
}
|
||||
this.select(elements[index]);
|
||||
}
|
||||
select(element) {
|
||||
const document = this.document;
|
||||
if (!this.style) {
|
||||
this.style = document.createElement('style');
|
||||
this.style.textContent = '[data-vulpes-keypad-focus]{outline:2px solid #00aaff !important;outline-offset:-2px !important;}';
|
||||
document.documentElement.append(this.style);
|
||||
}
|
||||
this.composer?.commit(); this.hint?.remove();
|
||||
this.selected?.removeAttribute('data-vulpes-keypad-focus');
|
||||
this.selected = element;
|
||||
this.selected.setAttribute('data-vulpes-keypad-focus', '');
|
||||
if (!this.selected.hasAttribute('tabindex')) this.selected.setAttribute('tabindex', '-1');
|
||||
this.selected.focus({preventScroll:true});
|
||||
this.selected.scrollIntoView({block:'nearest',inline:'nearest'});
|
||||
}
|
||||
activate(element) {
|
||||
if (element.disabled || element.hasAttribute('disabled') || element.getAttribute('aria-disabled') === 'true') return;
|
||||
if (['gaia-switch', 'gaia-checkbox'].includes(element.localName)) {
|
||||
// The component owns toggling and change events inside its shadow tree.
|
||||
// Calling the host's native click from this actor bypasses that handler.
|
||||
element.shadowRoot?.querySelector(element.localName === 'gaia-switch' ? '#switch' : '#checkbox')?.click();
|
||||
return;
|
||||
}
|
||||
if (element.getAttribute('role') === 'key' && element.hasAttribute('data-value')) {
|
||||
for (const type of ['touchstart','touchend'])
|
||||
element.dispatchEvent(new this.contentWindow.Event(type,{bubbles:true,cancelable:true}));
|
||||
} else element.click();
|
||||
}
|
||||
editKey(element, key) {
|
||||
const editor = new InputEditor(this.contentWindow);
|
||||
editor.focus(element);
|
||||
if (element.classList.contains('recipient') && ['Enter','Backspace'].includes(key)) {
|
||||
const keyCode = key === 'Enter' ? 13 : 8;
|
||||
// Gaia's recipients consume keypress, but a synthetic KeyboardEvent also
|
||||
// enters unsupported native key-binding IPC in the standalone host.
|
||||
const event = new this.contentWindow.Event('keypress', {bubbles:true,cancelable:true});
|
||||
for (const [name,value] of Object.entries({key,keyCode,which:keyCode,
|
||||
DOM_VK_BACK_SPACE:8,DOM_VK_ENTER:14,DOM_VK_TAB:9}))
|
||||
Object.defineProperty(event.wrappedJSObject,name,{value});
|
||||
if (!element.dispatchEvent(event)) { this.composer.commit(); return; }
|
||||
}
|
||||
const state = editor.state();
|
||||
const numeric = ['tel','number'].includes(state.inputType) ||
|
||||
['tel','numeric','decimal'].includes(state.inputMode);
|
||||
if (/^[0-9*#]$/.test(key)) {
|
||||
if (this.composer.mode==='t9' && this.t9Loading && /^[2-9]$/.test(key)) {
|
||||
this.t9Queue ||= [];
|
||||
if (this.t9Queue.length<16) this.t9Queue.push(key);
|
||||
return;
|
||||
}
|
||||
const operation = this.composer.press(key, {field:element,
|
||||
text:element.isContentEditable ? element.textContent : element.value,
|
||||
start:state.selectionStart, end:state.selectionEnd, numeric});
|
||||
if (operation?.text !== undefined) {
|
||||
if (operation.replace) editor.edit({operation:'replace',offset:-operation.replace,
|
||||
length:operation.replace,text:operation.text});
|
||||
else editor.edit({operation:'key',key:operation.text});
|
||||
}
|
||||
if (operation?.mode === 't9') this.loadT9Dictionary();
|
||||
if (!numeric) this.showMode(element);
|
||||
} else {
|
||||
this.composer.commit();
|
||||
if (key === 'ArrowLeft' || key === 'ArrowRight') {
|
||||
const start = key === 'ArrowLeft' ? Math.max(0, state.selectionStart - 1) :
|
||||
Math.min(state.text.length, state.selectionEnd + 1);
|
||||
editor.edit({operation:'selection', start, length:0});
|
||||
} else if (key.length === 1 || ['Backspace', 'Enter'].includes(key)) {
|
||||
editor.edit({operation:'key', key});
|
||||
}
|
||||
}
|
||||
}
|
||||
showMode(element) {
|
||||
if (!this.hint) {
|
||||
this.hint = this.document.createElement('span');
|
||||
this.hint.id = 'vulpes-keypad-mode';
|
||||
this.hint.style.cssText = 'position:fixed;right:4px;bottom:4px;z-index:2147483647;pointer-events:none;padding:2px 5px;border-radius:3px;background:#102337;color:white;font:12px sans-serif';
|
||||
this.contentWindow.addEventListener('blur', () => {
|
||||
this.composer.commit(); this.hint.remove();
|
||||
}, true);
|
||||
}
|
||||
if (this.composer.mode==='t9') {
|
||||
const french=(this.document.documentElement.lang || this.contentWindow.navigator.language || '').toLowerCase().startsWith('fr');
|
||||
this.hint.textContent=(french ? 'T9 · maintien # · * ' : 'T9 · hold # · * ')+
|
||||
(this.t9Loading ? '…' : this.composer.suggestion || '');
|
||||
} else this.hint.textContent=this.composer.mode+' · #';
|
||||
this.document.documentElement.append(this.hint);
|
||||
}
|
||||
loadT9Dictionary() {
|
||||
const win=this.contentWindow;
|
||||
const locale=(this.document.documentElement.lang || win.navigator.language || 'en').toLowerCase();
|
||||
if (this.t9Loading || this.composer.dictionary.size && this.t9Locale===locale) return;
|
||||
this.t9Loading=true;
|
||||
this.composer.dictionary=new Map();
|
||||
const choices=[locale,locale.split('-')[0]];
|
||||
if (locale==='en-us') choices.unshift('en_us');
|
||||
if (locale==='en-gb') choices.unshift('en_gb');
|
||||
if (locale==='pt-br') choices.unshift('pt_br');
|
||||
if (locale==='pt-pt') choices.unshift('pt_pt');
|
||||
const root=Services.env.get('VULPES_WORKSPACE_ROOT');
|
||||
if (!root) { this.t9Loading=false; return; }
|
||||
this.t9Field=this.document.activeElement;
|
||||
const {IOUtils}=ChromeUtils.importESModule('resource://gre/modules/IOUtils.sys.mjs');
|
||||
(async()=>{
|
||||
for (const language of choices) {
|
||||
if (!/^(?:[a-z]{2,3}(?:_[a-z]{2}|-[a-z]{2}|-latn|-cyrl)?)$/.test(language)) continue;
|
||||
try {
|
||||
const bytes=await IOUtils.read(root+'/tundra/devices/beatles/t9/'+language+'.tsv');
|
||||
this.composer.setDictionary(new TextDecoder().decode(bytes));
|
||||
if (this.composer.dictionary.size) { this.t9Locale=locale; break; }
|
||||
} catch (_) {}
|
||||
}
|
||||
this.t9Loading=false;
|
||||
if (this.hint?.isConnected) this.showMode(this.document.activeElement);
|
||||
if (this.document.activeElement===this.t9Field) {
|
||||
for (const key of this.t9Queue || []) this.editKey(this.t9Field,key);
|
||||
}
|
||||
this.t9Queue=[];
|
||||
})();
|
||||
}
|
||||
}
|
||||
44
host/actors/KeypadParent.sys.mjs
Normal file
44
host/actors/KeypadParent.sys.mjs
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import {Runtime} from 'resource://vulpes/host/Runtime.sys.mjs';
|
||||
import {Views} from 'resource://vulpes/host/Views.sys.mjs';
|
||||
|
||||
export class KeypadParent extends JSWindowActorParent {
|
||||
receiveMessage(message) {
|
||||
if (Services.env.get('VULPES_KEYPAD') !== '1') return;
|
||||
if (message.name === 'Vulpes:KeypadNotifications') {
|
||||
Runtime.broadcast('keypad-notifications', {}, 'system');
|
||||
return;
|
||||
}
|
||||
if (message.name === 'Vulpes:KeypadToolbar') {
|
||||
const shell = this.browsingContext.top.embedderElement?.ownerDocument;
|
||||
const system = shell?.getElementById('system')?.browsingContext;
|
||||
const belongsToHost = this.browsingContext.top === system ||
|
||||
[...Views.entries.values()].some(entry => entry.browser.browsingContext === this.browsingContext.top);
|
||||
if (belongsToHost) {
|
||||
system?.currentWindowGlobal?.getActor('Keypad')
|
||||
.sendAsyncMessage('Vulpes:FocusBrowserToolbar', {});
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (message.name === 'Vulpes:KeypadRoute') {
|
||||
const child = BrowsingContext.get(message.data.contextId);
|
||||
let ancestor = child?.parent;
|
||||
while (ancestor && ancestor !== this.browsingContext) ancestor = ancestor.parent;
|
||||
if (ancestor !== this.browsingContext) return;
|
||||
const view = Views.entries.get(child.id);
|
||||
const destination = view && view.browser.style.visibility === 'visible' ? view.browser.browsingContext : child;
|
||||
const page = destination.currentWindowGlobal;
|
||||
// Gecko error pages can autofocus a retry button far below the viewport.
|
||||
// Up must still offer an immediate way to edit the failed address.
|
||||
if (message.data.key === 'ArrowUp' &&
|
||||
/^about:(neterror|certerror|httpsonlyerror)\?/.test(page?.documentURI.spec || '')) {
|
||||
this.receiveMessage({name:'Vulpes:KeypadToolbar'});
|
||||
return;
|
||||
}
|
||||
page?.getActor('Keypad').sendAsyncMessage('Vulpes:Navigate', message.data);
|
||||
return;
|
||||
}
|
||||
if (!['Vulpes:KeypadHome', 'Vulpes:KeypadCards'].includes(message.name)) return;
|
||||
Views.hideAll();
|
||||
Runtime.broadcast(message.name === 'Vulpes:KeypadCards' ? 'holdhome' : 'home', {}, 'system');
|
||||
}
|
||||
}
|
||||
|
|
@ -3,9 +3,13 @@ import { Views } from 'resource://vulpes/host/Views.sys.mjs';
|
|||
import { packageURL } from 'resource://vulpes/services/packages.mjs';
|
||||
export class VulpesMarketplaceParent extends JSWindowActorParent {
|
||||
receiveMessage(message) {
|
||||
if(message.name!=='Marketplace:Download' || this.manager.documentPrincipal.originNoSuffix!=='https://vulpes-os.org')return;
|
||||
if(message.name!=='Marketplace:Download')return;
|
||||
const page=this.manager.documentURI;
|
||||
if(!((this.manager.documentPrincipal.originNoSuffix==='https://vulpes-os.org' && page.pathQueryRef.startsWith('/marketplace/')) ||
|
||||
(this.manager.documentPrincipal.originNoSuffix==='https://ffapps.danielherr.software' && page.pathQueryRef.startsWith('/app/'))))return;
|
||||
const view=[...Views.entries.values()].find(v=>v.browser.browsingContext.top.id===this.browsingContext.top.id);
|
||||
if(!view || !Runtime.identity(view.actor))return;
|
||||
if(view.browser.style.visibility!=='visible')return;
|
||||
Runtime.marketplaceDownload=packageURL(message.data.url);
|
||||
const app=Runtime.apps.find(a=>a.id==='marketplace');
|
||||
Runtime.broadcast('launch',{manifestURL:app.manifestURL,origin:app.origin,url:app.origin+'/index.html',timestamp:Date.now()},'system');
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
import {InputEditor} from 'resource://vulpes/services/input-editor.mjs';
|
||||
import {frameUnobscured} from 'resource://vulpes/services/view-visibility.mjs';
|
||||
import {requestMediaPermission} from 'resource://vulpes/services/media-permission.mjs';
|
||||
export class VulpesChild extends JSWindowActorChild {
|
||||
handleEvent(event) {
|
||||
if(['focusin','pointerdown','click'].includes(event.type)) {
|
||||
|
|
@ -41,20 +43,7 @@ export class VulpesChild extends JSWindowActorChild {
|
|||
params.contextId = native.browsingContext.id;
|
||||
if (params.operation === 'geometry') {
|
||||
const rect = native.getBoundingClientRect();
|
||||
const samples = [
|
||||
[0.5, 0.5],
|
||||
[0.05, 0.05],
|
||||
[0.95, 0.05],
|
||||
[0.05, 0.95],
|
||||
[0.95, 0.95],
|
||||
];
|
||||
const unobscured = samples.every(
|
||||
([x, y]) =>
|
||||
this.document.elementFromPoint(
|
||||
rect.left + rect.width * x,
|
||||
rect.top + rect.height * y,
|
||||
) === native,
|
||||
);
|
||||
const unobscured = frameUnobscured(this.document, native, rect);
|
||||
params.geometry = {
|
||||
left: rect.left + win.mozInnerScreenX,
|
||||
top: rect.top + win.mozInnerScreenY,
|
||||
|
|
@ -84,6 +73,11 @@ export class VulpesChild extends JSWindowActorChild {
|
|||
inputState() { return this.editor.state(); }
|
||||
edit(params) { return this.editor.edit(params); }
|
||||
receiveMessage(message) {
|
||||
if (message.name === 'Vulpes:MediaPermission') {
|
||||
if (this.document.location.hostname !== 'system.localhost' || this.browsingContext.parent)
|
||||
throw new Error('Invalid permission UI');
|
||||
return requestMediaPermission(this.contentWindow, message.data, value => Cu.cloneInto(value, this.contentWindow));
|
||||
}
|
||||
if(message.name==='Vulpes:Edit') return this.edit(message.data);
|
||||
if (message.name === 'Vulpes:ViewState') {
|
||||
const frame = Array.from(this.document.querySelectorAll('iframe')).find(
|
||||
|
|
|
|||
29
host/actors/WebInputChild.sys.mjs
Normal file
29
host/actors/WebInputChild.sys.mjs
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
import {InputEditor} from 'resource://vulpes/services/input-editor.mjs';
|
||||
|
||||
// Web pages may edit their focused field, but receive no Gaia service bridge.
|
||||
export class WebInputChild extends JSWindowActorChild {
|
||||
handleEvent(event) {
|
||||
if (!Services.cpmm.sharedData.get('vulpes:native-input') || this.editor?.editing) return;
|
||||
const contexts = Services.cpmm.sharedData.get('vulpes:host-contexts') || [];
|
||||
if (!contexts.includes(this.browsingContext.top.id)) return;
|
||||
// Standalone web views have no Firefox tabbrowser to focus their embedder.
|
||||
// Keep Gecko's picker focus checks intact by activating only a touched view.
|
||||
if (event.isTrusted && ['pointerdown','mousedown','click'].includes(event.type))
|
||||
this.sendAsyncMessage('Vulpes:WebActivate', null);
|
||||
let element = event.composedPath()[0];
|
||||
if (element?.isContentEditable) element = element.closest('[contenteditable=true]') || element;
|
||||
const editable = element?.isContentEditable || element?.localName === 'textarea' ||
|
||||
(element?.localName === 'input' && ['text','search','email','url','tel','password','number'].includes(element.type));
|
||||
if (!editable || element.disabled || element.readOnly) {
|
||||
if (event.type === 'click') this.sendAsyncMessage('Vulpes:InputFocus', null);
|
||||
return;
|
||||
}
|
||||
this.editor ||= new InputEditor(this.contentWindow);
|
||||
this.editor.focus(element);
|
||||
this.sendAsyncMessage('Vulpes:InputFocus', this.editor.state());
|
||||
}
|
||||
receiveMessage(message) {
|
||||
if (message.name !== 'Vulpes:Edit' || !this.editor) throw Error('INPUT_CONTEXT_EXPIRED');
|
||||
return this.editor.edit(message.data);
|
||||
}
|
||||
}
|
||||
22
host/actors/WebInputParent.sys.mjs
Normal file
22
host/actors/WebInputParent.sys.mjs
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import {Views} from 'resource://vulpes/host/Views.sys.mjs';
|
||||
import {Runtime} from 'resource://vulpes/host/Runtime.sys.mjs';
|
||||
|
||||
export class WebInputParent extends JSWindowActorParent {
|
||||
receiveMessage(message) {
|
||||
if (message.name === 'Vulpes:WebActivate') {
|
||||
if (Runtime.identity(this) || !Runtime.inputIdentity(this)) return;
|
||||
for (const entry of Views.entries.values()) {
|
||||
if (entry.browser.browsingContext === this.browsingContext.top &&
|
||||
entry.browser.ownerDocument.hasFocus()) {
|
||||
entry.browser.focus();
|
||||
break;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (message.name !== 'Vulpes:InputFocus') throw Error('METHOD_NOT_SUPPORTED');
|
||||
if (Runtime.identity(this)) return; // Bundled apps already use VulpesChild.
|
||||
Runtime.input.changed(this, message.data);
|
||||
}
|
||||
didDestroy() { Runtime.input.cleanup(this); }
|
||||
}
|
||||
Loading…
Reference in a new issue