Release Preview 20 shared desktop, Android and Sargo updates
Restore Gaia settings and media workflows, extend native network adapters, and align the APK payload. Generate French/English changelogs and website entries from the simplified JSON source. Validation: shared JS and Tundra unit tests; desktop workflows and persistence; signed APK payload parity; Sargo RAM and installed startup with boot readback. Android runtime qualification is recorded separately.
This commit is contained in:
parent
be2cdce823
commit
1311bed5e9
170 changed files with 19698 additions and 2495 deletions
|
|
@ -1,16 +1,20 @@
|
|||
import {InputEditor} from 'resource://vulpes/services/input-editor.mjs';
|
||||
export class VulpesChild extends JSWindowActorChild {
|
||||
handleEvent(event) {
|
||||
if(['focusin','pointerdown'].includes(event.type)) {
|
||||
if(this.editing) return;
|
||||
if(['focusin','pointerdown','click'].includes(event.type)) {
|
||||
if(this._editor?.editing) return;
|
||||
if(!Services.cpmm.sharedData.get('vulpes:native-input')) return;
|
||||
let element=event.target;
|
||||
if(element?.isContentEditable) element=element.closest('[contenteditable=true]') || element;
|
||||
const editable=element?.isContentEditable || (['input','textarea'].includes(element?.localName) && !['button','submit','range','checkbox','radio','date','time','datetime-local','file'].includes(element.type));
|
||||
if(!editable || element.disabled || element.readOnly) {
|
||||
if(event.type==='pointerdown') this.sendAsyncMessage('Vulpes:InputFocus',null);
|
||||
// Dismissing on pointerdown moves buttons before pointerup and loses
|
||||
// the click. Composer actions may also explicitly retain the editor.
|
||||
if(event.type==='click' && !element?.closest?.('[data-vulpes-keep-input]'))
|
||||
this.sendAsyncMessage('Vulpes:InputFocus',null);
|
||||
return;
|
||||
}
|
||||
this.inputElement=element;
|
||||
this.editor.focus(element);
|
||||
this.sendAsyncMessage('Vulpes:InputFocus',this.inputState());
|
||||
return;
|
||||
}
|
||||
|
|
@ -76,69 +80,9 @@ export class VulpesChild extends JSWindowActorChild {
|
|||
{ defineAs: 'vulpesFrameForContext' },
|
||||
);
|
||||
}
|
||||
inputState() {
|
||||
const e=this.inputElement;
|
||||
const text=e.isContentEditable ? e.textContent : e.value;
|
||||
const selection=this.contentWindow.getSelection();
|
||||
let start=e.selectionStart ?? text.length, end=e.selectionEnd ?? start;
|
||||
if(e.isContentEditable && selection.rangeCount && e.contains(selection.anchorNode)) {
|
||||
const range=this.document.createRange();range.selectNodeContents(e);
|
||||
range.setEnd(selection.anchorNode,selection.anchorOffset);start=range.toString().length;
|
||||
range.setEnd(selection.focusNode,selection.focusOffset);end=range.toString().length;
|
||||
[start,end]=[Math.min(start,end),Math.max(start,end)];
|
||||
}
|
||||
return {type:e.isContentEditable?'contenteditable':e.localName,inputType:e.type || (e.classList.contains('recipient')?'tel':'text'),
|
||||
inputMode:e.inputMode || (e.classList.contains('recipient')?'tel':''),
|
||||
text:e.type==='password'?'':text,selectionStart:start,selectionEnd:end};
|
||||
}
|
||||
edit(p) {
|
||||
const e=this.inputElement,win=this.contentWindow;
|
||||
if(!e?.isConnected || this.document.hidden) throw Error('INPUT_CONTEXT_EXPIRED');
|
||||
this.editing=true;
|
||||
try {e.focus();} finally {this.editing=false;}
|
||||
// A touch can focus a new contenteditable while Gecko retains the previous
|
||||
// field's selection. execCommand follows that selection, not activeElement.
|
||||
if(e.isContentEditable) {
|
||||
const selection=win.getSelection();
|
||||
if(!e.contains(selection.anchorNode) || !e.contains(selection.focusNode)) {
|
||||
const range=this.document.createRange();
|
||||
range.selectNodeContents(e);range.collapse(false);
|
||||
selection.removeAllRanges();selection.addRange(range);
|
||||
}
|
||||
}
|
||||
if(p.operation==='key') {
|
||||
let key=typeof p.key==='object'?p.key.key:p.key;
|
||||
if(key===8 || key==='Backspace') this.document.execCommand('delete');
|
||||
else if(key===13 || key==='Enter') {
|
||||
const options={key:'Enter',code:'Enter',keyCode:13,charCode:13,which:13,bubbles:true,cancelable:true};
|
||||
const accepted=e.dispatchEvent(new win.KeyboardEvent('keydown',options)) && e.dispatchEvent(new win.KeyboardEvent('keypress',options));
|
||||
if(accepted && (e.isContentEditable || e.localName==='textarea')) this.document.execCommand('insertLineBreak');
|
||||
e.dispatchEvent(new win.KeyboardEvent('keyup',options));
|
||||
} else {
|
||||
const text=typeof key==='string'?key:String.fromCodePoint(p.charCode || key);
|
||||
if(text.length>16) throw Error('INVALID_VALUE');
|
||||
this.document.execCommand('insertText',false,text);
|
||||
}
|
||||
} else if(['selection','replace'].includes(p.operation)) {
|
||||
const state=this.inputState();
|
||||
const start=p.operation==='selection'?p.start:state.selectionStart+(p.offset||0);
|
||||
const length=p.length||0;
|
||||
if(!Number.isInteger(start)||!Number.isInteger(length)||start<0||length<0||start+length>(e.value ?? e.textContent).length) throw Error('INVALID_VALUE');
|
||||
if(e.isContentEditable) {
|
||||
const walker=this.document.createTreeWalker(e,win.NodeFilter.SHOW_TEXT);
|
||||
const points=[];let offset=0,node;
|
||||
while((node=walker.nextNode())) {points.push({node,start:offset,end:offset+node.length});offset+=node.length;}
|
||||
const point=index=>{const part=points.find(p=>index<=p.end);return part?[part.node,index-part.start]:[e,0];};
|
||||
const range=this.document.createRange();range.setStart(...point(start));range.setEnd(...point(start+length));
|
||||
win.getSelection().removeAllRanges();win.getSelection().addRange(range);
|
||||
} else e.setSelectionRange(start,start+length);
|
||||
if(p.operation==='replace') {
|
||||
if(typeof p.text!=='string'||p.text.length>4096) throw Error('INVALID_VALUE');
|
||||
this.document.execCommand('insertText',false,p.text);
|
||||
}
|
||||
} else throw Error('METHOD_NOT_SUPPORTED');
|
||||
return this.inputState();
|
||||
}
|
||||
get editor() { return this._editor ||= new InputEditor(this.contentWindow); }
|
||||
inputState() { return this.editor.state(); }
|
||||
edit(params) { return this.editor.edit(params); }
|
||||
receiveMessage(message) {
|
||||
if(message.name==='Vulpes:Edit') return this.edit(message.data);
|
||||
if (message.name === 'Vulpes:ViewState') {
|
||||
|
|
@ -146,9 +90,16 @@ export class VulpesChild extends JSWindowActorChild {
|
|||
f => f.browsingContext?.id === message.data.contextId);
|
||||
if (!frame?.isConnected) return {visible:false};
|
||||
const win = this.contentWindow, rect = frame.getBoundingClientRect();
|
||||
// App chrome (the status bar and browser controls) legitimately covers
|
||||
// the corners of a mozbrowser iframe. Requiring every corner to hit the
|
||||
// iframe hid the external browser completely, leaving a white page for
|
||||
// sites such as YouTube. The centre plus one side sample is sufficient
|
||||
// to prove that the content viewport is visible.
|
||||
const samples = [[.5,.5],[.05,.5],[.95,.5],[.5,.05],[.5,.95]];
|
||||
const hits = samples.reduce((count,[x,y]) => count +
|
||||
(this.document.elementFromPoint(rect.left+rect.width*x,rect.top+rect.height*y)===frame ? 1 : 0), 0);
|
||||
const visible = !this.document.hidden && frame.checkVisibility({checkOpacity:true,checkVisibilityCSS:true}) &&
|
||||
[[.5,.5],[.05,.05],[.95,.05],[.05,.95],[.95,.95]].every(([x,y]) =>
|
||||
this.document.elementFromPoint(rect.left+rect.width*x,rect.top+rect.height*y)===frame);
|
||||
hits >= 2;
|
||||
let top = Math.max(0,rect.top);
|
||||
// Gaia's status bar belongs above all app content, including native web views.
|
||||
if (this.document.location.hostname === 'system.localhost') {
|
||||
|
|
|
|||
Loading…
Reference in a new issue