Pages stay painted: frames are composed; padlock shows the certificate; review fixes
Three pieces of work that share files, plus what two adversarial reviews found in them (Codex until its credits ran out, then the Cavekit inspector). THE PAGE STAYS PAINTED (owner: "pages only load when observed ... the entire page should stay rendered at once unless it's very long, so that navigation and zooming is smooth") - The painted region uses the whole shared segment: 4096 rows in portrait, up from a 2048 cap that stood on a worry about a GPU texture limit. Measured: a 4096-row region wraps and blits. JIHAD_REGION_ROWS_CAP is the way back. - Rendering that region whole for every frame does not survive its size: 4096 rows of a real page measured 300-1400 ms, with the daemon deaf to input meanwhile. Frames are now COMPOSED: rows the newest frame already holds are copied from its buffer, rows it lacks are rendered (at most 768 a frame, so a region grows over a few frames), the visible band is drawn as before, and at z~1 the band of the frame copied from is redrawn from the document where it is no longer this frame's band (fixed headers, the caret). Same page: 110-180 ms a frame. - What a copy cannot know is whether the page changed off screen, so after a frame that reacted to a change the off-screen rows are swept, a few hundred at a time, by maintenance frames once the page is loaded and still, with a pause between sweeps. Rows that are owed a sweep do not count as covering a pan. A large change at the same scroll repaints the band only. - Zoomed in, the region is as wide as the content, up to two viewports, so a sideways pan has something to show. The adapter's rotation guard now compares bufferWidth (the viewport) rather than renderedWidth (the region); the header layout is unchanged. - The coverage margin scales with the region, so a short or wide region is not judged uncovered at the position it was just painted for. - <state>/dump-request writes one frame to /tmp for inspection (refused when /tmp is short of room: forgotten dumps once filled that 40 MB RAM disk). CORRECTION to3218caad: its message says a scripted scroll over a whole-page frame "logs no uncovered rows". The adapter call the test used (pageScaleAndScroll) is an empty stub; nothing scrolled, and the claim proved nothing. What is measured now: on a colour-coded 16000-row page with a fixed header, after jumps by its anchor links, every row of the dumped frames holds what the page has there, the header is only at the top of the visible band, and no row is stale. A finger pan cannot be scripted on this platform (script-made pointer events do not reach the plugin, and the adapter's flick entry needs a drag in progress), so how a real fling looks is still the owner's to judge. THE PADLOCK (owner: "add clicking on the lock to view certificate information") - pageCertificate on the daemon's own Luna service, answered to this variant's own card only, in the reply itself: the loaded page's certificate (from the docshell's security UI), the connection's protocol and cipher, and whether the certificate was believed (overridden / untrusted / mismatch / badTime) and whether the page counts as secure (broken). - Enyo: the existing padlock is a button; it opens the certificate view, which gains fingerprint, connection and trust rows. Mochi and Mojo had no padlock: Mochi gets one inside the address field, Mojo one drawn as the field's background with the tap taken on its left edge. No padlock over a failed load. - The adapter's pageFocused was an empty stub. Thumbnails, printing and now the padlock all rely on a card saying "I have the focus" before a Luna request; with two cards open the daemon answered for whichever connected last. It now sends the adapter's existing asyncCmdPageFocused, and the daemon clears the focus when the focused card gives it up. REVIEW FIXES TO THE LONG-PRESS MENU (657282c4) - Page commands are queued and run from the guarded pump, tied to the document they were asked of; copy answers the adapter's callback with the real result. - Cut is two steps: the card copies, and only once the text is on the clipboard asks for the deletion, which the engine refuses unless the selection is still exactly what it copied, whole, in a field that accepts edits. The card is told how it ended. - State is per page (one component serves every card); answers about a copy or a cut are acted on only by the card that asked. - A page that handles its own contextmenu event gets no second menu. Read-only fields get the page's menu. Passwords are never copied or cut. - Clip files have random names and plain-ASCII contents; a selection far too big to copy is measured and read with a bound instead of being built whole. - Paste is bounded and sent in pieces: one IPC message holds 16 KB and a longer string was dropped whole, silently. - A TLS failure that comes with no certificate is a failed connection, not the "certificate is invalid - Trust Always?" dialog it used to raise. On the device (all three variants on one build from the installer): Enyo and Mochi pass the scripted long-press flows, the padlock, the failed-load case and the focus hand-over; Mojo shows its padlock on an https page. Mojo's menu and padlock tap have no test hook and need a finger, as does any real pan. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LcrVQFRMC2ANGDfVaRG9Ed
This commit is contained in:
1 parent
657282c431
commit
debd19341d
30 files changed
+1848
-326
No files matched your search
@@ -88,6 +88,11 @@ input.jihad-address {
|
||||
cursor: pointer;
|
||||
}
|
||||
.jihad-inline-btn .jihad-nav-img { width: 20px; height: 20px; margin: -10px 0 0 -10px; }
|
||||
/* The padlock: the same inline control at the field's LEFT edge, there only on a TLS page;
|
||||
the text makes room for it the way it does for the reload glyph on the right. */
|
||||
.jihad-lock-btn { right: auto; left: 6px; }
|
||||
.jihad-secure .jihad-address input,
|
||||
.jihad-secure input.jihad-address { padding-left: 40px; }
|
||||
.jihad-address-box { position: relative; }
|
||||
|
||||
/* Scale the base64 SVG glyphs to fill the Mochi icon box cleanly. */
|
||||
@@ -151,6 +156,10 @@ input.jihad-address {
|
||||
font-size: 16px;
|
||||
line-height: 1.4;
|
||||
margin-bottom: 14px;
|
||||
/* A message may be several lines (a certificate's details) and may hold one long word
|
||||
(a fingerprint). */
|
||||
white-space: pre-line;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
.jihad-dialog-inputbox { margin-bottom: 12px; }
|
||||
.jihad-dialog-input { width: 100%; -webkit-box-sizing: border-box; box-sizing: border-box; }
|
||||
|
||||
@@ -43,7 +43,7 @@ enyo.JihadIcons = {
|
||||
|
||||
// Dark reload/stop for INSIDE the light URL bar (the white nav icons would be
|
||||
// invisible there).
|
||||
enyo.JihadInlineIcons = { reload: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADgAAAA4CAYAAACohjseAAADxElEQVR4nO2ay0tVQRzHP9PLpKxVEZkakQaRpUUaQpBB0GMRFEGL2kQQIf0JUYlEraJWUcuWtewdIWb0pNIWQWGBi6S6YtENobJ+LeZcmnvuufe8Zm7d8gsDc65n5vf9nJk5Zx4qEeFf1pQ/bcC1JgH/dimlFpf6e0UDKqXqgdul7qlYQA+uF1hS6r6KBIwKBzDNkYEqoAPoBJqBZcACYDaggCzwDhgCngL9wB0R+R4xxD4iwAEgItYSsAY4B3wCJGYaA84DrRHiHDXLlrzXEthq4EYCqGLpGtBiAzBVF1VKVQMngC5gapHbhoAXwDAwjh4W87y0AqgLKLMZ2KSUOg0cFpHxxCZTtNoy4DnBLdAH7AcWRqynC3hQpK5BYGlZuyjQDowGmLkMrEnx0DYQ3NVHgbayAHpwWZ+B98B2G+PZi7GHwhdVFmh3Cuh1J3/L3Y/SFRNA1gOPfbEyQKMTQKA6YMzdBGbZhjNizvUeoBlzAOhxAXg6oOWcwRlxa4BHvtj3rAKiv3MTvjFnvVuWiL/ENyZ/2gb0v9m2lwvO8LA74O2aHhA9/cr7FJQbzvDSGxcwymrigO/6SIQyrtQdu0TIE6siv+/3WWyNeuA1RbpdnJSmBTvQr+qcLsR+ggGKs55LqzDATt/11bQBywkH4QvelUZ+SERGLMTcClxMWUcHMN3LZ0rdGAbYZORfpHGUk4ictVFPVIV10flGftilEVcKA6wx8lmXRlypInfV4igM0Gy1OS6NuFIY4AcjX+/SiCuFAb4y8stdGnGlMMBBI79UKbXQpRkXCgPs9V1vc2XElcIA76Mn2zntdWfFjUoCishX8qdV65VSa91asqso30H/1OqYCyOuFAooIk+B68ZPW5RSO9xZsivlLT5L36RUC3qPMjc5z6BPgd66s5YXvxa9GZzTBHBGohy3xViBnyJ/Jf0QmF2GfZhZwBNf7J7I5WMEmonedDUD3XIJiZ7s+zeaBoAZ1gG9gI3o7ulvyVoHcHUUbviOAA2x6kkQuI3Cw5cMsNMi3C4Kz0A+k+DkKqmBtoCWFPTbti1JnUa9twLqHQFWJ6ozhZnGgDGZS/3o/dRFEepZBBzEd95gpGfA4qQ+I30mikkpNRM4Dhyi+P7OG34fYX/xfpsL1KI3tRqKlJsATgLdIvItsUlLY2YVcAXfoUjC9BO4BDRb8WajEgO0FT21G0sA9g44YwvMShctJqXUDGAdsBHdDZvQ/whUA/wAPnoP4SV6hvQAuCsiP6x7cQH4N+m/31WreE0CVromAStdvwC9z4Jb+d27LgAAAABJRU5ErkJggg==", stop: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAACXklEQVR4nO2VsW4TQRCG/53ZQ0LCgJJHyBtgWfJd5nQOPIPLFEiJhWiQeAHnKlpEgxIXUJOWJuVhUxoRpFyZio6CDiXO7Q4Fd9LhOMnFoUHKL22zmp3v/rmZXeBWt/rXGg6H1O/3uUEoATDlWlpUBzeJK3V9aAUQkUREBgCQJImdj6v2wjCMkiT5GEVR69rQEmZE5Ekcxz97vZ7Gcbw1D63BOnEc/9jY2FAR+dbtdldUdWF5z30xAOR5bgB4AGvM/LAoihkzj0TEZFk2qkBZlhVhGHaCIDjw3j/w3sMY87XVap3u7OwYADqf+0Lbw+GQ0jT1URRtBUEw8t6fEVHgnBtMJpNR5ayCWWvJOTcaj8eDWu7mwKpkWZYVIrLNzHs16CYRHarqJ1W9X4f1+33e39/3i2BXAueh1to95xy89x7ALyK6Z61FURSNYI2Ac9BNY8xbVb0LgIjIqeqb8Xj8sgkMOD8/l0pVvxhjTvGnoWYA4L0/BIDj42O6CtYIWLkLw7DDzJ9VdcUYY4nojvfeWGvfi8hgOp2etdvt4EbAOiwIggNVbVlroaqvVfUpM5NzrmDmXRHZnk6nZ4suh7quHIvLWl9EBsy8u2BkFo7EhcASpiLymIg+lDCut/7R0RHneT6rRsY5VxCR9d4/Z+ZRr9fzaZr6RiUtbxoFsEZEKyVsr976eZ7P2u12MJlMRs65ARFZYwwAvDo5OVlN01Qvq+BClwCwvr7+QkTe1fb+SlL9syiKniVJ8r3b7T6qn7+u6ocufOuq5J1OZ7UWu7QIzea1gizlbFndyNmt/g/9Bn89SYCL2k6MAAAAAElFTkSuQmCC" };
|
||||
enyo.JihadInlineIcons = { lock: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADgAAAA4CAYAAACohjseAAADbElEQVR42u2aO2sUURTHf/OIjRoTkohiEwSNL0Q/RRqRQGo7sRNrtTIWNjaCWFiJfgBtBMXCRhBFrCIIoiJIQBKjJhKT7M6MxZ6Lh2FM5nXv7MIcGHaZnXvv+Z/X/Z87C6200kqT4jW4VuJi0cABqFA+49Rvvlo/GTQPegIgSgHaI99/A52UoWMbQD1L4Iyie4EZYBo4DkzI/Z/Ae+AZ8BD4oowQ93NO+/IZAleBbwJ2q+sXcBPY7ShtKoPbD7xQADrAOrAp3olT98xz78TLfQnSl9DcB8yLwptAN4cHuwroAnAsZbC+2GoCYAh4qcBF6vs9YBY4ARwFzgC3gRV5JhKvJsAnYEQZrXEx4XQ5A9xr4NQWYw8CTxRI48m7/RKqxsLjwHLKE2+BnaroBOIVs/+Fap7HKlzNHFP9EKpGyQuqoMTAhsqloRzjJ4AlGWu8eCP1TKPh+UiUWxfl7hdQzjwzJ2M35PNNA3QyMzwD4IPKvwQ4q2haHiN5wGmVi2Z/HKsKso74HlaKGEAfRck8rMRQtK/AquiUyLzj/QAwrSyKknkFImFEFaXawtK3nJtFx9Sujy2AiaMxuStY3R2Jl7q2G+9tM08jAAPZmDsZv3XVxr2dmJ4xax5DGsKybVRYIbS7isWkrTwCjIoRohxzxaoSaxkTbtpJ9ZlWG16j0CxwBZhU5NjIak7v8Z+OH7UXLgC3hJ9ab4hNdZzJ0QbVfV20TcA91RrNK94YZygTl7yy5ulImC/J5m+NvplJR4EfSgHbntNrHCm6vZXdB8vmQVJhvys11q/ozSISqTCPHK3prJlMJHfX6Z2JOuvUXQCMxfp3pEs/DFyrGOrYLjLLOYuMYTTPM+Z7kHomT5GZclVkihLop8Kadsg1JOcw1kh2nWQ7j9dPiqdM996Vewx6iOozz3Oqik4LnYtyzFEpRF0ANM+sCWcF+Jw6fxnYHNSypoy0gaMXoC4B+jka3IEGWJWqDQRA54e4oUNQw8ArKRqTrgzs8tw/AA659qBfIZfK8tLY5ZphyZDzG8j5UuuGJRZZAb5LTsUyR2Ixf43H/9D7U4NV/mr6uPMNHDrN2T50SofZJXpvhKItDo2qnsVEwCJwnX/vLryiIVA2dBJgF3DAskEXhfs6l2AQ1vJqKAK22YlzetdKK620klv+ApEHo1gvv6f2AAAAAElFTkSuQmCC", reload: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADgAAAA4CAYAAACohjseAAADxElEQVR4nO2ay0tVQRzHP9PLpKxVEZkakQaRpUUaQpBB0GMRFEGL2kQQIf0JUYlEraJWUcuWtewdIWb0pNIWQWGBi6S6YtENobJ+LeZcmnvuufe8Zm7d8gsDc65n5vf9nJk5Zx4qEeFf1pQ/bcC1JgH/dimlFpf6e0UDKqXqgdul7qlYQA+uF1hS6r6KBIwKBzDNkYEqoAPoBJqBZcACYDaggCzwDhgCngL9wB0R+R4xxD4iwAEgItYSsAY4B3wCJGYaA84DrRHiHDXLlrzXEthq4EYCqGLpGtBiAzBVF1VKVQMngC5gapHbhoAXwDAwjh4W87y0AqgLKLMZ2KSUOg0cFpHxxCZTtNoy4DnBLdAH7AcWRqynC3hQpK5BYGlZuyjQDowGmLkMrEnx0DYQ3NVHgbayAHpwWZ+B98B2G+PZi7GHwhdVFmh3Cuh1J3/L3Y/SFRNA1gOPfbEyQKMTQKA6YMzdBGbZhjNizvUeoBlzAOhxAXg6oOWcwRlxa4BHvtj3rAKiv3MTvjFnvVuWiL/ENyZ/2gb0v9m2lwvO8LA74O2aHhA9/cr7FJQbzvDSGxcwymrigO/6SIQyrtQdu0TIE6siv+/3WWyNeuA1RbpdnJSmBTvQr+qcLsR+ggGKs55LqzDATt/11bQBywkH4QvelUZ+SERGLMTcClxMWUcHMN3LZ0rdGAbYZORfpHGUk4ictVFPVIV10flGftilEVcKA6wx8lmXRlypInfV4igM0Gy1OS6NuFIY4AcjX+/SiCuFAb4y8stdGnGlMMBBI79UKbXQpRkXCgPs9V1vc2XElcIA76Mn2zntdWfFjUoCishX8qdV65VSa91asqso30H/1OqYCyOuFAooIk+B68ZPW5RSO9xZsivlLT5L36RUC3qPMjc5z6BPgd66s5YXvxa9GZzTBHBGohy3xViBnyJ/Jf0QmF2GfZhZwBNf7J7I5WMEmonedDUD3XIJiZ7s+zeaBoAZ1gG9gI3o7ulvyVoHcHUUbviOAA2x6kkQuI3Cw5cMsNMi3C4Kz0A+k+DkKqmBtoCWFPTbti1JnUa9twLqHQFWJ6ozhZnGgDGZS/3o/dRFEepZBBzEd95gpGfA4qQ+I30mikkpNRM4Dhyi+P7OG34fYX/xfpsL1KI3tRqKlJsATgLdIvItsUlLY2YVcAXfoUjC9BO4BDRb8WajEgO0FT21G0sA9g44YwvMShctJqXUDGAdsBHdDZvQ/whUA/wAPnoP4SV6hvQAuCsiP6x7cQH4N+m/31WreE0CVromAStdvwC9z4Jb+d27LgAAAABJRU5ErkJggg==", stop: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAACXklEQVR4nO2VsW4TQRCG/53ZQ0LCgJJHyBtgWfJd5nQOPIPLFEiJhWiQeAHnKlpEgxIXUJOWJuVhUxoRpFyZio6CDiXO7Q4Fd9LhOMnFoUHKL22zmp3v/rmZXeBWt/rXGg6H1O/3uUEoATDlWlpUBzeJK3V9aAUQkUREBgCQJImdj6v2wjCMkiT5GEVR69rQEmZE5Ekcxz97vZ7Gcbw1D63BOnEc/9jY2FAR+dbtdldUdWF5z30xAOR5bgB4AGvM/LAoihkzj0TEZFk2qkBZlhVhGHaCIDjw3j/w3sMY87XVap3u7OwYADqf+0Lbw+GQ0jT1URRtBUEw8t6fEVHgnBtMJpNR5ayCWWvJOTcaj8eDWu7mwKpkWZYVIrLNzHs16CYRHarqJ1W9X4f1+33e39/3i2BXAueh1to95xy89x7ALyK6Z61FURSNYI2Ac9BNY8xbVb0LgIjIqeqb8Xj8sgkMOD8/l0pVvxhjTvGnoWYA4L0/BIDj42O6CtYIWLkLw7DDzJ9VdcUYY4nojvfeWGvfi8hgOp2etdvt4EbAOiwIggNVbVlroaqvVfUpM5NzrmDmXRHZnk6nZ4suh7quHIvLWl9EBsy8u2BkFo7EhcASpiLymIg+lDCut/7R0RHneT6rRsY5VxCR9d4/Z+ZRr9fzaZr6RiUtbxoFsEZEKyVsr976eZ7P2u12MJlMRs65ARFZYwwAvDo5OVlN01Qvq+BClwCwvr7+QkTe1fb+SlL9syiKniVJ8r3b7T6qn7+u6ocufOuq5J1OZ7UWu7QIzea1gizlbFndyNmt/g/9Bn89SYCL2k6MAAAAAElFTkSuQmCC" };
|
||||
|
||||
enyo.kind({
|
||||
name: "JihadBrowser",
|
||||
@@ -67,6 +67,9 @@ enyo.kind({
|
||||
{name: "home", classes: "jihad-nav-btn", allowHtml: true, content: "<img class=jihad-nav-img src='" + enyo.JihadIcons.home + "'>", ontap: "goHome"},
|
||||
// Address field with the reload/stop control INSIDE it (Enyo-parity).
|
||||
{kind: "mochi.InputDecorator", name: "addressBox", classes: "jihad-address-box", fit: true, components: [
|
||||
// The padlock: shown on a page that came over TLS; a tap says whose
|
||||
// certificate it was served with (showPageCertificate).
|
||||
{name: "lock", classes: "jihad-inline-btn jihad-lock-btn", showing: false, allowHtml: true, content: "<img class=jihad-nav-img src='" + enyo.JihadInlineIcons.lock + "'>", ontap: "showPageCertificate"},
|
||||
{kind: "mochi.Input", name: "address", classes: "jihad-address", placeholder: "Search or type a URL", onchange: "addressEntered", onkeydown: "addressKeydown", attributes: {autocapitalize: "off", autocorrect: "off", spellcheck: "false", type: "url"}},
|
||||
{name: "reloadStop", classes: "jihad-inline-btn", allowHtml: true, content: "<img class=jihad-nav-img src='" + enyo.JihadInlineIcons.reload + "'>", ontap: "reloadOrStop"}
|
||||
]},
|
||||
@@ -81,6 +84,7 @@ enyo.kind({
|
||||
onLoadStarted: "loadStarted",
|
||||
onLoadProgress: "loadProgress",
|
||||
onLoadStopped: "loadStopped",
|
||||
onLoadFailed: "loadFailed",
|
||||
onPageInfoChanged: "pageInfoChanged",
|
||||
// Engine-created page (link with target / window.open) -> its own card.
|
||||
onNewPage: "newPageRequested",
|
||||
@@ -421,6 +425,10 @@ enyo.kind({
|
||||
// and where the copied text was put. Neither is a sentence to show.
|
||||
if (resp.category === "page-hold") { this.pageHold(resp.text); return; }
|
||||
if (resp.category === "page-copied") { this.pageCopied(resp.text); return; }
|
||||
// What the engine says about a copy or a cut is broadcast to every card;
|
||||
// it is shown by the one that asked.
|
||||
if (resp.category === "page-copy-empty" && !this.takeCopyAsked()) { return; }
|
||||
if ((resp.category === "page-cut" || resp.category === "page-cut-refused") && !this.takeCutAsked()) { return; }
|
||||
enyo.jihad.toast(resp.category, resp.text);
|
||||
}));
|
||||
},
|
||||
@@ -524,7 +532,14 @@ enyo.kind({
|
||||
|
||||
// --- WebView (JihadWebView) callbacks -> chrome state -------------------
|
||||
hideStartPage: function() { if (this.$.startPage && this.$.startPage.hide) this.$.startPage.hide(); },
|
||||
//* What is on screen is now the engine's error page, not the site in the address bar.
|
||||
loadFailed: function() {
|
||||
this._loadFailed = true;
|
||||
this.updateLock();
|
||||
},
|
||||
loadStarted: function() {
|
||||
this._loadFailed = false;
|
||||
this.updateLock();
|
||||
this.hideStartPage();
|
||||
this._lastProgress = 0;
|
||||
this.$.progress.setProgress(0);
|
||||
@@ -567,6 +582,7 @@ enyo.kind({
|
||||
if (typeof inEvent.title === "string" && inEvent.title) {
|
||||
this._title = inEvent.title;
|
||||
}
|
||||
this.updateLock();
|
||||
if (typeof inEvent.canGoBack === "boolean") {
|
||||
this._canGoBack = inEvent.canGoBack;
|
||||
this.$.back.addRemoveClass("disabled", !inEvent.canGoBack);
|
||||
@@ -594,6 +610,43 @@ enyo.kind({
|
||||
" overlap=" + Math.round((ir.right - pad) - br.left));
|
||||
} catch (e) { enyo.log("[JIHAD-URLGEOM] " + e); }
|
||||
},
|
||||
//* The padlock is there when the page on screen has an https address and its load did
|
||||
//* not fail (a failed load shows the engine's error page under that address).
|
||||
updateLock: function() {
|
||||
var secure = !this._loadFailed && /^https:\/\//i.test(this.url || "");
|
||||
this.$.lock.setShowing(secure);
|
||||
this.$.addressBox.addRemoveClass("jihad-secure", secure);
|
||||
},
|
||||
//* The padlock was tapped: ask this browser's own daemon whose certificate the page was
|
||||
//* served with (it answers this variant's own card only). markActive first — the
|
||||
//* question goes over Luna, which cannot say which card is asking.
|
||||
showPageCertificate: function() {
|
||||
var self = this;
|
||||
this.$.view.markActive();
|
||||
enyo.jihad.own("pageCertificate", {}, function(r) {
|
||||
var text = (r && r.returnValue && r.secure) ? self.$.dialogs.certText(r) : "";
|
||||
var c = r && r.connection, bits = [];
|
||||
if (!text) {
|
||||
self.openDialog("Certificate", "No certificate information is available for this page.");
|
||||
return;
|
||||
}
|
||||
if (c) {
|
||||
if (c.protocol) { bits.push(String(c.protocol)); }
|
||||
if (c.cipher) { bits.push(String(c.cipher)); }
|
||||
if (c.keyLength) { bits.push(c.keyLength + "-bit keys"); }
|
||||
}
|
||||
if (bits.length) { text += "\nConnection: " + bits.join(", "); }
|
||||
// Whether the certificate was believed, and whether the page counts as secure.
|
||||
if (r.overridden || r.untrusted || r.mismatch || r.badTime) {
|
||||
text += "\n" + (r.mismatch ? "This certificate was issued for a different site." :
|
||||
r.badTime ? "This certificate is not valid at this time." :
|
||||
"This certificate could not be verified.") + (r.overridden ? " You chose to trust it." : "");
|
||||
}
|
||||
if (r.broken) { text += "\nThis page is not fully secure: part of it, or the connection itself, is not protected."; }
|
||||
self.openDialog("Certificate", text);
|
||||
});
|
||||
return true;
|
||||
},
|
||||
//* Swap the reload/stop affordance during a load.
|
||||
setStopVisible: function(loading) {
|
||||
this._loading = loading;
|
||||
@@ -740,11 +793,15 @@ enyo.kind({
|
||||
return true;
|
||||
},
|
||||
//* The engine's answer: {editable, selected}. What a desktop browser offers on a
|
||||
//* right-click — on a text field the editing commands, elsewhere the page's.
|
||||
//* right-click — on a text field the editing commands, elsewhere the page's. The answer
|
||||
//* names no card, so it is taken only by the one that is on screen and passed a hold on
|
||||
//* within the last four seconds — which, a finger being in one place, is one card. (The
|
||||
//* answer normally takes 0.1-0.5 s; once, on a card started seconds earlier, it was not
|
||||
//* acted on at 2.5 s.)
|
||||
pageHold: function(text) {
|
||||
var at = this._pageHold, s = null, items = [];
|
||||
this._pageHold = 0;
|
||||
if (!at || Date.now() - at > 3000) { return; }
|
||||
if (!at || Date.now() - at > 4000 || (window.PalmSystem && window.PalmSystem.isActivated === false)) { return; }
|
||||
try { s = enyo.json.parse(text); } catch (e) { s = null; }
|
||||
s = s || {};
|
||||
if (s.editable) {
|
||||
@@ -767,33 +824,73 @@ enyo.kind({
|
||||
this.openSheet(this._title || "Page", this.url || "", items, {});
|
||||
},
|
||||
pageSelectAll: function() { this.$.view.callBrowserAdapter("selectAll"); },
|
||||
//* Copy and Cut ask the engine for the selected text; it comes back by way of pageCopied.
|
||||
pageCopy: function() { this.$.view.callBrowserAdapter("copy"); },
|
||||
pageCut: function() { this.$.view.callBrowserAdapter("cut"); },
|
||||
//* Copy asks the engine for the selected text; it comes back by way of pageCopied. Cut is
|
||||
//* a copy, and then — only once the text is on the clipboard — the adapter's cut, which
|
||||
//* tells the engine to delete what it handed over. What the engine says about either is
|
||||
//* broadcast to every card, so the card remembers that it asked.
|
||||
pageCopy: function() { this._copyAsked = {at: Date.now(), cut: false}; this.$.view.callBrowserAdapter("copy"); },
|
||||
pageCut: function() { this._copyAsked = {at: Date.now(), cut: true}; this.$.view.callBrowserAdapter("copy"); },
|
||||
pageReload: function() { this.$.view.callBrowserAdapter("reloadPage"); },
|
||||
pageFind: function() { this.$.findBar.show(); },
|
||||
//* The copy this card asked for within the last five seconds, once.
|
||||
takeCopyAsked: function() {
|
||||
var a = this._copyAsked;
|
||||
this._copyAsked = null;
|
||||
return (a && Date.now() - a.at < 5000) ? a : null;
|
||||
},
|
||||
//* How a cut this card asked for ended ("Cut.", or why not): its to show, nobody else's.
|
||||
takeCutAsked: function() {
|
||||
var at = this._cutAsked;
|
||||
this._cutAsked = 0;
|
||||
return !!(at && Date.now() - at < 5000);
|
||||
},
|
||||
//* The engine has written the selected text where this card may load it: a one-call
|
||||
//* script in the app's own clip/ directory (a card can load a local file from nowhere
|
||||
//* else, and the clipboard is the card's to write). `name` is that file's name.
|
||||
pageCopied: function(name) {
|
||||
if (!/^copy-[0-9-]+\.js$/.test(String(name))) { return; }
|
||||
var asked = this.takeCopyAsked(), view = this.$.view, self = this, done = false;
|
||||
if (!asked || !/^copy-[0-9a-f-]+\.js$/.test(String(name))) { return; }
|
||||
var s = document.createElement("script");
|
||||
window.jihadClip = function(text) {
|
||||
var failed = function() {
|
||||
if (done) { return; }
|
||||
done = true;
|
||||
window.jihadClip = null;
|
||||
enyo.jihad.toast("info", enyo.jihad.setClipboard(String(text)) ? "Copied." : "The text could not be copied.");
|
||||
enyo.jihad.toast("info", "The text could not be copied.");
|
||||
};
|
||||
s.onload = s.onerror = function() {
|
||||
window.jihadClip = function(text) {
|
||||
if (done) { return; }
|
||||
done = true;
|
||||
window.jihadClip = null;
|
||||
if (!enyo.jihad.setClipboard(String(text))) { enyo.jihad.toast("info", "The text could not be copied."); return; }
|
||||
// True from this moment whatever the cut does; "Cut." replaces it if the engine
|
||||
// confirms the deletion (the "page-cut" notification).
|
||||
enyo.jihad.toast("info", "Copied.");
|
||||
if (asked.cut) { self._cutAsked = Date.now(); view.callBrowserAdapter("cut"); }
|
||||
};
|
||||
s.onload = function() {
|
||||
if (s.parentNode) { s.parentNode.removeChild(s); }
|
||||
failed(); // it loaded and never called: not a clip script
|
||||
};
|
||||
s.onerror = function() {
|
||||
if (s.parentNode) { s.parentNode.removeChild(s); }
|
||||
failed();
|
||||
};
|
||||
s.src = "clip/" + name;
|
||||
document.body.appendChild(s);
|
||||
},
|
||||
//* Paste: the clipboard is read here (only a card can) and typed into the focused field.
|
||||
//* Another app decides what is on the clipboard, so its size is bounded here; and one
|
||||
//* message to the engine holds 16 KB, so the text goes in pieces (enyo.jihad.pasteChunks).
|
||||
pagePaste: function() {
|
||||
var view = this.$.view;
|
||||
enyo.jihad.getClipboard(function(text) {
|
||||
if (text) { view.callBrowserAdapter("insertStringAtCursor", [String(text)]); }
|
||||
else { enyo.jihad.toast("info", "The clipboard is empty."); }
|
||||
var parts, i;
|
||||
if (!text) { enyo.jihad.toast("info", "The clipboard is empty."); }
|
||||
else if (text.length > 262144) { enyo.jihad.toast("info", "The clipboard holds too much text to paste here."); }
|
||||
else {
|
||||
parts = enyo.jihad.pasteChunks(String(text));
|
||||
for (i = 0; i < parts.length; i++) { view.callBrowserAdapter("insertStringAtCursor", [parts[i]]); }
|
||||
}
|
||||
});
|
||||
},
|
||||
holdOpenInNewCard: function(e) { this.openCard({url: e.linkUrl}); },
|
||||
|
||||
@@ -265,6 +265,7 @@ enyo.jihad = enyo.jihad || {};
|
||||
if (window.PalmSystem && window.PalmSystem.paste) { window.PalmSystem.paste(); }
|
||||
else { document.execCommand("paste"); }
|
||||
} catch (e) {}
|
||||
// Whatever happened above, the textarea goes and the callback is called exactly once.
|
||||
function look() {
|
||||
var text = ta ? ta.value : "";
|
||||
if (!text && ta && ++tries < 15) { setTimeout(look, 40); return; }
|
||||
@@ -275,6 +276,28 @@ enyo.jihad = enyo.jihad || {};
|
||||
setTimeout(look, 20);
|
||||
};
|
||||
|
||||
//* `inText` as the pieces a paste is sent in. One message to the engine holds 16 KB and a
|
||||
//* longer string is dropped whole (the IPC's own limit), so each piece is at most 12000
|
||||
//* bytes of UTF-8, and no piece ends inside a surrogate pair.
|
||||
scope.pasteChunks = function(inText) {
|
||||
var out = [], start = 0, bytes = 0, i, c, n;
|
||||
for (i = 0; i < inText.length; i++) {
|
||||
c = inText.charCodeAt(i);
|
||||
n = c < 0x80 ? 1 : (c < 0x800 ? 2 : 3);
|
||||
// A low surrogate stays with the high one before it.
|
||||
if (bytes + n > 12000 && !(c >= 0xDC00 && c <= 0xDFFF)) {
|
||||
out.push(inText.substring(start, i));
|
||||
start = i;
|
||||
bytes = 0;
|
||||
}
|
||||
bytes += n;
|
||||
}
|
||||
if (start < inText.length) {
|
||||
out.push(inText.substring(start));
|
||||
}
|
||||
return out;
|
||||
};
|
||||
|
||||
// --- default search engine -------------------------------------------------
|
||||
// These three, by id. WHICH one is chosen on the settings page (about:preferences, Browser
|
||||
// pane) and cached by JihadChromePrefs; this is only the map from id to search url. The
|
||||
|
||||
@@ -93,7 +93,10 @@ enyo.kind({
|
||||
//* The user held on anything else. The hold is left to the engine, which answers
|
||||
//* with a "page-hold" notification saying what it found; the shell opens the page
|
||||
//* menu on that. This event only tells the shell an answer is on its way.
|
||||
onPageHold: ""
|
||||
onPageHold: "",
|
||||
//* A load failed: {code, url, message}. What the view shows now is the engine's
|
||||
//* error page under the address that failed.
|
||||
onLoadFailed: ""
|
||||
},
|
||||
|
||||
// --- lifecycle ----------------------------------------------------------
|
||||
@@ -331,6 +334,13 @@ enyo.kind({
|
||||
canGoForward: canGoForward
|
||||
});
|
||||
},
|
||||
// (adapter callback) a load failed (adapter msgFailedLoad -> "failedLoad": domain, code,
|
||||
// failing url, description). 1000 is a cancelled load, which is not a failure.
|
||||
failedLoad: function(domain, code, url, message) {
|
||||
if (code !== 1000) {
|
||||
this.doLoadFailed({code: code, url: url, message: message});
|
||||
}
|
||||
},
|
||||
// (adapter callback) the engine created a page for a link with a target /
|
||||
// window.open (adapter msgCreatePage -> "createPage", one string arg: the new
|
||||
// page identifier). BasicWebView.createPage -> doNewPage; the shell opens a
|
||||
|
||||
@@ -19,8 +19,8 @@
|
||||
// callBrowserAdapter() below is the ONLY app-facing adapter surface, and the set of
|
||||
// method-name literals routed through it is a strict SUBSET of the Enyo-1.0 app's
|
||||
// set in ../../../app/source/Browser.js —
|
||||
// Mojo : {copy, cut, findInPage, goBack, goForward, insertStringAtCursor, reloadPage,
|
||||
// selectAll, stopLoad}
|
||||
// Mojo : {copy, cut, findInPage, goBack, goForward, insertStringAtCursor, pageFocused,
|
||||
// reloadPage, selectAll, stopLoad}
|
||||
// Enyo : those and more (dialogs, images, printing, privacy switches)
|
||||
// — no additions, no renames. (copy, cut, selectAll and insertStringAtCursor arrived in both
|
||||
// shells on 2026-10-05 with the long-press menu; all four were already adapter methods.) This variant uses no palm://com.palm.browserServer/*
|
||||
@@ -281,6 +281,7 @@ MainAssistant.prototype.bindHandlers = function () {
|
||||
this.handleCreatePage = this.handleCreatePage.bind(this);
|
||||
this.handleMimeUnsupported = this.handleMimeUnsupported.bind(this);
|
||||
this.handleServerDisconnect = this.handleServerDisconnect.bind(this);
|
||||
this.handleAddressTap = this.handleAddressTap.bind(this);
|
||||
this.handleAddressSubmit = this.handleAddressSubmit.bind(this);
|
||||
this.handleRetry = this.handleRetry.bind(this);
|
||||
};
|
||||
@@ -311,6 +312,7 @@ MainAssistant.prototype.listen = function () {
|
||||
for (var i = 0; i < events.length; i++) {
|
||||
Mojo.Event.listen(view, events[i][0], events[i][1]);
|
||||
}
|
||||
Mojo.Event.listen(this.controller.get(this.ADDRESS_ID), Mojo.Event.tap, this.handleAddressTap, true);
|
||||
Mojo.Event.listen(this.controller.get(this.ADDRESS_ID), Mojo.Event.propertyChange,
|
||||
this.handleAddressSubmit);
|
||||
Mojo.Event.listen(this.controller.get(this.RETRY_ID), Mojo.Event.tap, this.handleRetry);
|
||||
@@ -393,9 +395,102 @@ MainAssistant.prototype.handleNotification = function (response) {
|
||||
this.handlePageCopied(response.text);
|
||||
return;
|
||||
}
|
||||
// What the engine says about a copy or a cut is broadcast to every card; it is shown by
|
||||
// the one that asked.
|
||||
if (response.category === "page-copy-empty" && !this.takeCopyAsked()) {
|
||||
return;
|
||||
}
|
||||
if ((response.category === "page-cut" || response.category === "page-cut-refused") && !this.takeCutAsked()) {
|
||||
return;
|
||||
}
|
||||
JihadToast.show(response.category, response.text);
|
||||
};
|
||||
|
||||
// --- the padlock ---------------------------------------------------------------------------
|
||||
//* On a page that came over TLS the address field carries a padlock at its left edge (drawn
|
||||
//* as the field's background: the field is a framework widget and redraws its own children).
|
||||
MainAssistant.prototype.updateLock = function () {
|
||||
var field = this.controller.get(this.ADDRESS_ID);
|
||||
this.secure = !this.loadFailed && /^https:\/\//i.test(this.url || "");
|
||||
if (field) {
|
||||
if (this.secure) {
|
||||
field.addClassName("jihad-secure");
|
||||
} else {
|
||||
field.removeClassName("jihad-secure");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
//* A tap on the padlock (the left 40 px of a field that shows one) says whose certificate the
|
||||
//* page was served with, instead of starting an edit. Asked of this browser's own daemon,
|
||||
//* which answers this variant's own card only; pageFocused first, because the question goes
|
||||
//* over Luna and cannot say which card is asking.
|
||||
MainAssistant.prototype.handleAddressTap = function (event) {
|
||||
var field = this.controller.get(this.ADDRESS_ID), down = event && event.down, rect;
|
||||
if (!this.secure || !field || !down || down.pageX === undefined) {
|
||||
return;
|
||||
}
|
||||
rect = field.getBoundingClientRect();
|
||||
if (down.pageX - rect.left > 40) {
|
||||
return;
|
||||
}
|
||||
event.stop();
|
||||
this.callBrowserAdapter("pageFocused", [true]);
|
||||
this.controller.serviceRequest("palm://net.riverstonerelay.jihadBrowserMojo", {
|
||||
method: "pageCertificate",
|
||||
parameters: {},
|
||||
onSuccess: this.showPageCertificate.bind(this),
|
||||
onFailure: this.showPageCertificate.bind(this)
|
||||
});
|
||||
};
|
||||
|
||||
MainAssistant.prototype.showPageCertificate = function (r) {
|
||||
var lines = [], c = r && r.connection, bits = [];
|
||||
function party(label, p) {
|
||||
var out = [], keys = ["commonname", "organization", "unit"], i;
|
||||
if (!p) {
|
||||
return;
|
||||
}
|
||||
for (i = 0; i < keys.length; i++) {
|
||||
if (p[keys[i]]) {
|
||||
out.push(String(p[keys[i]]));
|
||||
}
|
||||
}
|
||||
if (out.length) {
|
||||
lines.push(label + ": " + out.join(", "));
|
||||
}
|
||||
}
|
||||
if (r && r.returnValue && r.secure) {
|
||||
party($L("Issued to"), r.subject);
|
||||
party($L("Issued by"), r.issuer);
|
||||
if (r.startdate) { lines.push($L("Valid from") + ": " + r.startdate); }
|
||||
if (r.expiredate) { lines.push($L("Valid until") + ": " + r.expiredate); }
|
||||
if (r.serialNumber) { lines.push($L("Serial number") + ": " + r.serialNumber); }
|
||||
if (r.sha256) { lines.push($L("SHA-256 fingerprint") + ": " + r.sha256); }
|
||||
if (c) {
|
||||
if (c.protocol) { bits.push(String(c.protocol)); }
|
||||
if (c.cipher) { bits.push(String(c.cipher)); }
|
||||
if (c.keyLength) { bits.push(c.keyLength + $L("-bit keys")); }
|
||||
}
|
||||
if (bits.length) { lines.push($L("Connection") + ": " + bits.join(", ")); }
|
||||
// Whether the certificate was believed, and whether the page counts as secure.
|
||||
if (r.overridden || r.untrusted || r.mismatch || r.badTime) {
|
||||
lines.push((r.mismatch ? $L("This certificate was issued for a different site.") :
|
||||
r.badTime ? $L("This certificate is not valid at this time.") :
|
||||
$L("This certificate could not be verified.")) + (r.overridden ? " " + $L("You chose to trust it.") : ""));
|
||||
}
|
||||
if (r.broken) { lines.push($L("This page is not fully secure: part of it, or the connection itself, is not protected.")); }
|
||||
}
|
||||
this.controller.showAlertDialog({
|
||||
title: $L("Certificate"),
|
||||
// The dialog takes text, not markup: the certificate's own strings are shown as they are.
|
||||
message: lines.length ? lines.join("\n") : $L("No certificate information is available for this page."),
|
||||
allowHTMLMessage: false,
|
||||
choices: [{label: $L("OK"), value: "ok"}],
|
||||
onChoose: function () {}
|
||||
});
|
||||
};
|
||||
|
||||
// --- long-press menu -----------------------------------------------------------------------
|
||||
//* A finger held on the page. Mojo.Widget.WebView claims no hold, so every one goes to the
|
||||
//* engine, which gives the page its contextmenu event, selects the word under the finger (or
|
||||
@@ -443,11 +538,16 @@ MainAssistant.prototype.handlePageMenuChoice = function (command) {
|
||||
this.callBrowserAdapter("selectAll");
|
||||
break;
|
||||
case "page-copy":
|
||||
// The text comes back by way of handlePageCopied.
|
||||
// The text comes back by way of handlePageCopied. What the engine says about a copy
|
||||
// is broadcast to every card, so the card remembers that it asked.
|
||||
this.copyAsked = {at: Date.now(), cut: false};
|
||||
this.callBrowserAdapter("copy");
|
||||
break;
|
||||
case "page-cut":
|
||||
this.callBrowserAdapter("cut");
|
||||
// A copy, and then — only once the text is on the clipboard — the adapter's cut,
|
||||
// which tells the engine to delete what it handed over (handlePageCopied).
|
||||
this.copyAsked = {at: Date.now(), cut: true};
|
||||
this.callBrowserAdapter("copy");
|
||||
break;
|
||||
case "page-paste":
|
||||
this.pasteIntoPage();
|
||||
@@ -468,30 +568,96 @@ MainAssistant.prototype.handlePageMenuChoice = function (command) {
|
||||
}
|
||||
};
|
||||
|
||||
//* The copy this card asked for within the last five seconds, once.
|
||||
MainAssistant.prototype.takeCopyAsked = function () {
|
||||
var a = this.copyAsked;
|
||||
this.copyAsked = null;
|
||||
return (a && Date.now() - a.at < 5000) ? a : null;
|
||||
};
|
||||
|
||||
//* How a cut this card asked for ended ("Cut.", or why not): its to show, nobody else's.
|
||||
MainAssistant.prototype.takeCutAsked = function () {
|
||||
var at = this.cutAsked;
|
||||
this.cutAsked = 0;
|
||||
return !!(at && Date.now() - at < 5000);
|
||||
};
|
||||
|
||||
//* The engine has written the selected text where this card may load it: a one-call script in
|
||||
//* the app's own clip/ directory (a card can load a local file from nowhere else, and the
|
||||
//* clipboard is the card's to write). `name` is that file's name.
|
||||
MainAssistant.prototype.handlePageCopied = function (name) {
|
||||
var win = this.controller.window, doc = this.controller.document, sc = this.controller.stageController;
|
||||
var script;
|
||||
if (!/^copy-[0-9-]+\.js$/.test(String(name))) {
|
||||
var script, asked = this.takeCopyAsked(), self = this, done = false;
|
||||
if (!asked || !/^copy-[0-9a-f-]+\.js$/.test(String(name))) {
|
||||
return;
|
||||
}
|
||||
function failed() {
|
||||
if (done) {
|
||||
return;
|
||||
}
|
||||
done = true;
|
||||
win.jihadClip = null;
|
||||
JihadToast.show("info", $L("The text could not be copied."));
|
||||
}
|
||||
script = doc.createElement("script");
|
||||
win.jihadClip = function (text) {
|
||||
if (done) {
|
||||
return;
|
||||
}
|
||||
done = true;
|
||||
win.jihadClip = null;
|
||||
sc.setClipboard(String(text), false);
|
||||
try {
|
||||
sc.setClipboard(String(text), false);
|
||||
} catch (e) {
|
||||
JihadToast.show("info", $L("The text could not be copied."));
|
||||
return;
|
||||
}
|
||||
// True from this moment whatever the cut does; "Cut." replaces it if the engine
|
||||
// confirms the deletion (the "page-cut" notification).
|
||||
JihadToast.show("info", $L("Copied."));
|
||||
if (asked.cut) {
|
||||
self.cutAsked = Date.now();
|
||||
self.callBrowserAdapter("cut");
|
||||
}
|
||||
};
|
||||
script.onload = script.onerror = function () {
|
||||
script.onload = function () {
|
||||
if (script.parentNode) {
|
||||
script.parentNode.removeChild(script);
|
||||
}
|
||||
failed(); // it loaded and never called: not a clip script
|
||||
};
|
||||
script.onerror = function () {
|
||||
if (script.parentNode) {
|
||||
script.parentNode.removeChild(script);
|
||||
}
|
||||
failed();
|
||||
};
|
||||
script.src = "clip/" + name;
|
||||
doc.body.appendChild(script);
|
||||
};
|
||||
|
||||
//* `text` as the pieces a paste is sent in. One message to the engine holds 16 KB and a longer
|
||||
//* string is dropped whole (the IPC's own limit), so each piece is at most 12000 bytes of
|
||||
//* UTF-8, and no piece ends inside a surrogate pair.
|
||||
MainAssistant.prototype.pasteChunks = function (inText) {
|
||||
var out = [], start = 0, bytes = 0, i, c, n;
|
||||
for (i = 0; i < inText.length; i++) {
|
||||
c = inText.charCodeAt(i);
|
||||
n = c < 0x80 ? 1 : (c < 0x800 ? 2 : 3);
|
||||
// A low surrogate stays with the high one before it.
|
||||
if (bytes + n > 12000 && !(c >= 0xDC00 && c <= 0xDFFF)) {
|
||||
out.push(inText.substring(start, i));
|
||||
start = i;
|
||||
bytes = 0;
|
||||
}
|
||||
bytes += n;
|
||||
}
|
||||
if (start < inText.length) {
|
||||
out.push(inText.substring(start));
|
||||
}
|
||||
return out;
|
||||
};
|
||||
|
||||
//* Paste: the clipboard is read here (only a card can) — the system's paste into a throwaway
|
||||
//* textarea — and typed into the page's focused field. The paste is not synchronous and
|
||||
//* takes anything from a few milliseconds to more than 60 (measured in the Mochi shell, same
|
||||
@@ -499,7 +665,7 @@ MainAssistant.prototype.handlePageCopied = function (name) {
|
||||
//* called empty.
|
||||
MainAssistant.prototype.pasteIntoPage = function () {
|
||||
var doc = this.controller.document, win = this.controller.window, self = this, tries = 0;
|
||||
var ta = doc.createElement("textarea");
|
||||
var ta = doc.createElement("textarea"), parts, i;
|
||||
ta.style.position = "absolute";
|
||||
ta.style.left = "-2000px";
|
||||
ta.style.top = "0";
|
||||
@@ -515,6 +681,7 @@ MainAssistant.prototype.pasteIntoPage = function () {
|
||||
} catch (e) {
|
||||
Mojo.Log.error("[Jihad] paste: %s", e);
|
||||
}
|
||||
// Another app decides what is on the clipboard, so its size is bounded here.
|
||||
function look() {
|
||||
var text = ta.value;
|
||||
if (!text && ++tries < 15) {
|
||||
@@ -525,10 +692,15 @@ MainAssistant.prototype.pasteIntoPage = function () {
|
||||
if (ta.parentNode) {
|
||||
ta.parentNode.removeChild(ta);
|
||||
}
|
||||
if (text) {
|
||||
self.callBrowserAdapter("insertStringAtCursor", [String(text)]);
|
||||
} else {
|
||||
if (!text) {
|
||||
JihadToast.show("info", $L("The clipboard is empty."));
|
||||
} else if (text.length > 262144) {
|
||||
JihadToast.show("info", $L("The clipboard holds too much text to paste here."));
|
||||
} else {
|
||||
parts = self.pasteChunks(String(text));
|
||||
for (i = 0; i < parts.length; i++) {
|
||||
self.callBrowserAdapter("insertStringAtCursor", [parts[i]]);
|
||||
}
|
||||
}
|
||||
}
|
||||
win.setTimeout(look, 20);
|
||||
@@ -560,6 +732,7 @@ MainAssistant.prototype.cleanup = function () {
|
||||
for (var i = 0; i < events.length; i++) {
|
||||
Mojo.Event.stopListening(view, events[i][0], events[i][1]);
|
||||
}
|
||||
Mojo.Event.stopListening(this.controller.get(this.ADDRESS_ID), Mojo.Event.tap, this.handleAddressTap, true);
|
||||
Mojo.Event.stopListening(this.controller.get(this.ADDRESS_ID), Mojo.Event.propertyChange,
|
||||
this.handleAddressSubmit);
|
||||
Mojo.Event.stopListening(this.controller.get(this.RETRY_ID), Mojo.Event.tap, this.handleRetry);
|
||||
@@ -731,6 +904,8 @@ MainAssistant.prototype.handleCreatePage = function (event) {
|
||||
// --- load state -------------------------------------------------------------
|
||||
|
||||
MainAssistant.prototype.handleLoadStarted = function () {
|
||||
this.loadFailed = false;
|
||||
this.updateLock();
|
||||
this.hideError();
|
||||
this.setLoading(true);
|
||||
this.setProgress(0);
|
||||
@@ -760,6 +935,9 @@ MainAssistant.prototype.handleLoadFailed = function (event) {
|
||||
return;
|
||||
}
|
||||
this.failedUrl = (event && event.failingURL) || this.url;
|
||||
// What is on screen is an error now, not the site in the address field: no padlock.
|
||||
this.loadFailed = true;
|
||||
this.updateLock();
|
||||
this.showError(code, event && event.message);
|
||||
};
|
||||
|
||||
@@ -802,6 +980,7 @@ MainAssistant.prototype.handleTitleUrlChanged = function (event) {
|
||||
return;
|
||||
}
|
||||
this.url = event.url || this.url;
|
||||
this.updateLock();
|
||||
this.title = event.title || this.title;
|
||||
this.canGoBack = !!event.canGoBack;
|
||||
this.canGoForward = !!event.canGoForward;
|
||||
@@ -833,6 +1012,7 @@ MainAssistant.prototype.handleUrlChanged = function (event) {
|
||||
return;
|
||||
}
|
||||
this.url = event.url || this.url;
|
||||
this.updateLock();
|
||||
this.canGoBack = !!event.canGoBack;
|
||||
this.canGoForward = !!event.canGoForward;
|
||||
this.syncChrome();
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1000 B |
@@ -55,6 +55,17 @@
|
||||
|
||||
/* The TextField widget builds its own inner markup at render time; keep it inside
|
||||
the row whatever padding it brings. */
|
||||
/* A page that came over TLS: a padlock at the field's left edge, drawn as the field's own
|
||||
background (the field is a framework widget and redraws its children), with the text moved
|
||||
clear of it. The field's width does not change, so the toolbar's does not either. */
|
||||
.jihad-address.jihad-secure {
|
||||
background-image: url(../images/secure-lock.png);
|
||||
background-repeat: no-repeat;
|
||||
background-position: 8px center;
|
||||
-webkit-background-size: 22px 22px; background-size: 22px 22px;
|
||||
padding-left: 40px;
|
||||
}
|
||||
|
||||
.jihad-address * {
|
||||
-webkit-box-sizing: border-box; box-sizing: border-box;
|
||||
max-width: 100%;
|
||||
|
||||
@@ -27,7 +27,8 @@ enyo.kind({
|
||||
searchPreferences: {},
|
||||
defaultSearch: "",
|
||||
progress: 0,
|
||||
loading: false
|
||||
loading: false,
|
||||
loadFailed: false
|
||||
},
|
||||
events: {
|
||||
onBack: "",
|
||||
@@ -40,7 +41,8 @@ enyo.kind({
|
||||
onAddToLauncher: "",
|
||||
onShareLink: "",
|
||||
onOpenBookmarks: "",
|
||||
onNewCard: ""
|
||||
onNewCard: "",
|
||||
onSecureInfo: ""
|
||||
},
|
||||
components: [
|
||||
{kind: "Control", showing: false, name: "title", className: "page-title enyo-text-ellipsis", content: "Untitled"},
|
||||
@@ -51,7 +53,7 @@ enyo.kind({
|
||||
// Preferences (enyo.jihadChrome.loadHome(), default
|
||||
// https://start.duckduckgo.com/). Never disabled: home is always reachable.
|
||||
{kind: "ToolButton", name: "home", className: "actionbar-tool-button", icon: "images/chrome/menu-icon-home.png", onclick: "doHome"},
|
||||
{kind: "ToolButton", name: "search", className: "actionbar-tool-button", flex: 1, kind: "URLSearch", onLoad: "doLoad", onStopLoad: "doStopLoad", onRefresh: "doRefresh"},
|
||||
{kind: "ToolButton", name: "search", className: "actionbar-tool-button", flex: 1, kind: "URLSearch", onLoad: "doLoad", onStopLoad: "doStopLoad", onRefresh: "doRefresh", onSecureInfo: "doSecureInfo"},
|
||||
{kind: "ToolButton", name: "share", className: "actionbar-tool-button", icon: "images/chrome/menu-icon-share.png", onclick: "showSharePopup"},
|
||||
{kind: "ToolButton", className: "actionbar-tool-button", icon: "images/chrome/menu-icon-newcard.png", onclick: "doNewCard"},
|
||||
{kind: "ToolButton", className: "actionbar-tool-button", icon: "images/chrome/menu-icon-bookmark.png", onclick: "doOpenBookmarks"},
|
||||
@@ -83,6 +85,9 @@ enyo.kind({
|
||||
this.canShareChanged();
|
||||
this.loadingChanged();
|
||||
},
|
||||
loadFailedChanged: function() {
|
||||
this.$.search.setLoadFailed(this.loadFailed);
|
||||
},
|
||||
urlChanged: function() {
|
||||
this.$.search.setUrl(this.url);
|
||||
},
|
||||
|
||||
@@ -18,19 +18,24 @@ enyo.kind({
|
||||
align: "center",
|
||||
published: {
|
||||
url: "",
|
||||
loading: false
|
||||
loading: false,
|
||||
//* The load of `url` failed: what is on screen is an error, not that site, and it
|
||||
//* gets no padlock whatever the address says.
|
||||
loadFailed: false
|
||||
},
|
||||
events: {
|
||||
onBlur: "",
|
||||
onInputChange: "",
|
||||
onGo: "",
|
||||
onStop: "",
|
||||
onRefresh: ""
|
||||
onRefresh: "",
|
||||
//* The padlock was tapped: whose certificate is this page's?
|
||||
onSecureInfo: ""
|
||||
},
|
||||
chrome: [
|
||||
|
||||
{kind: "InputBox", layoutKind: "HFlexLayout", flex: 1, className: "enyo-tool-input", focusClassName: "enyo-tool-input-focus", spacingClassName: "enyo-tool-input-spacing", components: [
|
||||
{name: "secureLock", kind: enyo.CustomButton, showing: false, className: "secure-lock"},
|
||||
{name: "secureLock", kind: enyo.CustomButton, showing: false, className: "secure-lock", onclick: "doSecureInfo"},
|
||||
|
||||
{name: "userinput", kind: "Input", flex: 1, styled: false, inputType: "url", width: "100%", spellcheck: false, autocorrect: false, autoCapitalize: "lowercase", autoWordComplete: false, hint: $L("Enter URL or search terms"), selectAllOnFocus: true,
|
||||
onfocus: "selectInput",
|
||||
@@ -76,12 +81,15 @@ enyo.kind({
|
||||
loadingChanged: function() {
|
||||
this.changeButtons();
|
||||
},
|
||||
loadFailedChanged: function() {
|
||||
this.changeButtons();
|
||||
},
|
||||
changeButtons: function() {
|
||||
this.showLeftButton("");
|
||||
if (this.hasFocus() && this.$.userinput.getValue().length >= 0) {
|
||||
this.showRightButton("clearButton");
|
||||
} else {
|
||||
if (this.url.toLowerCase().substring(0, 8) === "https://") {
|
||||
if (!this.loadFailed && this.url.toLowerCase().substring(0, 8) === "https://") {
|
||||
this.showLeftButton("secureLock");
|
||||
}
|
||||
if (this.loading) {
|
||||
|
||||
+97
-13
@@ -49,8 +49,13 @@ enyo.kind({
|
||||
onAddToLauncher: "doAddToLauncher",
|
||||
onShareLink: "doShareLink",
|
||||
onOpenBookmarks: "doOpenBookmarks",
|
||||
onNewCard: "openNewCard"
|
||||
onNewCard: "openNewCard",
|
||||
onSecureInfo: "showPageCertificate"
|
||||
},
|
||||
// The padlock: this browser's own daemon says whose certificate the page was served
|
||||
// with (its pageCertificate method answers this variant's own card only).
|
||||
{name: "certService", kind: enyo.PalmService, service: "palm://net.riverstonerelay.jihadBrowser/",
|
||||
method: "pageCertificate", onSuccess: "gotPageCertificate", onFailure: "gotPageCertificate"},
|
||||
{name: "view", kind: "WebView", flex: 1, height: "100%",
|
||||
onMousehold: "openContextMenu",
|
||||
onPageTitleChanged: "pageTitleChanged",
|
||||
@@ -323,6 +328,20 @@ enyo.kind({
|
||||
this.$.sslCertDialog.validateComponents();
|
||||
this.$.sslCertDialog.openAtCenter();
|
||||
},
|
||||
//* The address bar's padlock was tapped. pageFocused first: the question goes over Luna,
|
||||
//* which cannot say which card is asking, and the daemon answers for the focused one.
|
||||
showPageCertificate: function() {
|
||||
this.$.view.callBrowserAdapter("pageFocused", [true]);
|
||||
this.$.certService.call({});
|
||||
},
|
||||
gotPageCertificate: function(inSender, inResponse) {
|
||||
if (inResponse && inResponse.returnValue && inResponse.secure && inResponse.subject) {
|
||||
this.$.sslCertDialog.showCertificate(inResponse);
|
||||
this.$.sslCertDialog.openAtCenter();
|
||||
} else {
|
||||
enyo.jihadToast.show("info", $L("No certificate information is available for this page."));
|
||||
}
|
||||
},
|
||||
closeSSLCertificate: function(inSender) {
|
||||
this.$.sslCertDialog.close();
|
||||
},
|
||||
@@ -360,11 +379,15 @@ enyo.kind({
|
||||
// hear that report, so only the card that is waiting for one acts on it.
|
||||
this._pageHold = {left: inEvent.pageX, top: inEvent.pageY, at: Date.now()};
|
||||
},
|
||||
//* The engine's answer to a long-press this card passed on: {editable, selected}.
|
||||
//* The engine's answer to a long-press this card passed on: {editable, selected}. The
|
||||
//* answer is broadcast to every card of this browser and names no card, so it is taken
|
||||
//* only by the one that is on screen and passed a hold on within the last four seconds —
|
||||
//* which, a finger being in one place, is one card. (The answer normally takes 0.1-0.5 s;
|
||||
//* once, on a card started seconds earlier, it was not acted on at 2.5 s.)
|
||||
pageHold: function(inText) {
|
||||
var h = this._pageHold, s = null;
|
||||
this._pageHold = null;
|
||||
if (!h || Date.now() - h.at > 3000) {
|
||||
if (!h || Date.now() - h.at > 4000 || (window.PalmSystem && PalmSystem.isActivated === false)) {
|
||||
return;
|
||||
}
|
||||
try { s = enyo.json.parse(inText); } catch (e) { s = null; }
|
||||
@@ -377,40 +400,95 @@ enyo.kind({
|
||||
pageSelectAllClick: function() {
|
||||
this.$.view.callBrowserAdapter("selectAll");
|
||||
},
|
||||
//* Copy and Cut ask the engine for the selected text; it comes back by way of pageCopied.
|
||||
//* Copy asks the engine for the selected text; it comes back by way of pageCopied. What
|
||||
//* the engine says about a copy is broadcast too, so the card remembers that it asked.
|
||||
pageCopyClick: function() {
|
||||
this._copyAsked = {at: Date.now(), cut: false};
|
||||
this.$.view.callBrowserAdapter("copy");
|
||||
},
|
||||
//* Cut is a copy, and then — only once the text is on the clipboard — the adapter's cut,
|
||||
//* which tells the engine to delete what it handed over (see pageCopied).
|
||||
pageCutClick: function() {
|
||||
this.$.view.callBrowserAdapter("cut");
|
||||
this._copyAsked = {at: Date.now(), cut: true};
|
||||
this.$.view.callBrowserAdapter("copy");
|
||||
},
|
||||
//* The copy this card asked for within the last five seconds, once.
|
||||
takeCopyAsked: function() {
|
||||
var a = this._copyAsked;
|
||||
this._copyAsked = null;
|
||||
return (a && Date.now() - a.at < 5000) ? a : null;
|
||||
},
|
||||
//* "Nothing is selected." and the like are for the card that asked.
|
||||
pageCopyEmpty: function() {
|
||||
return !!this.takeCopyAsked();
|
||||
},
|
||||
//* So is how a cut ended ("Cut.", or why not).
|
||||
pageCutResult: function() {
|
||||
var at = this._cutAsked;
|
||||
this._cutAsked = 0;
|
||||
return !!(at && Date.now() - at < 5000);
|
||||
},
|
||||
//* The engine has written the selected text where this card may load it: a one-call
|
||||
//* script in the app's own clip/ directory (a card can load a local file from nowhere
|
||||
//* else, and the clipboard is the card's to write). `inName` is that file's name.
|
||||
pageCopied: function(inName) {
|
||||
if (!/^copy-[0-9-]+\.js$/.test(String(inName))) {
|
||||
var asked = this.takeCopyAsked(), view = this.$.view, self = this, done = false;
|
||||
if (!asked || !/^copy-[0-9a-f-]+\.js$/.test(String(inName))) {
|
||||
return;
|
||||
}
|
||||
var s = document.createElement("script");
|
||||
window.jihadClip = function(inText) {
|
||||
var failed = function() {
|
||||
if (done) { return; }
|
||||
done = true;
|
||||
window.jihadClip = null;
|
||||
enyo.dom.setClipboard(String(inText));
|
||||
enyo.jihadToast.show("info", $L("Copied."));
|
||||
enyo.jihadToast.show("info", $L("The text could not be copied."));
|
||||
};
|
||||
s.onload = s.onerror = function() {
|
||||
window.jihadClip = function(inText) {
|
||||
if (done) { return; }
|
||||
done = true;
|
||||
window.jihadClip = null;
|
||||
if (!enyo.jihadSetClipboard(String(inText))) {
|
||||
enyo.jihadToast.show("info", $L("The text could not be copied."));
|
||||
return;
|
||||
}
|
||||
// True from this moment whatever the cut does; "Cut." replaces it if the
|
||||
// engine confirms the deletion (BrowserApp.gotNotification, "page-cut").
|
||||
enyo.jihadToast.show("info", $L("Copied."));
|
||||
if (asked.cut) {
|
||||
self._cutAsked = Date.now();
|
||||
view.callBrowserAdapter("cut");
|
||||
}
|
||||
};
|
||||
s.onload = function() {
|
||||
if (s.parentNode) { s.parentNode.removeChild(s); }
|
||||
failed(); // it loaded and never called: not a clip script
|
||||
};
|
||||
s.onerror = function() {
|
||||
if (s.parentNode) { s.parentNode.removeChild(s); }
|
||||
failed();
|
||||
};
|
||||
s.src = "clip/" + inName;
|
||||
document.body.appendChild(s);
|
||||
},
|
||||
//* Paste: the clipboard is read here (only a card can) and typed into the focused field.
|
||||
//* Another app decides what is on the clipboard, so its size is bounded here: more than
|
||||
//* a quarter of a million characters is not something to push through a text field.
|
||||
//* One message to the engine holds 16 KB, and a longer string is dropped
|
||||
//* whole, so the text goes in pieces of at most 12000 bytes of UTF-8, never cut inside a
|
||||
//* surrogate pair.
|
||||
pagePasteClick: function() {
|
||||
var view = this.$.view;
|
||||
enyo.jihadGetClipboard(function(inText) {
|
||||
if (inText) {
|
||||
view.callBrowserAdapter("insertStringAtCursor", [String(inText)]);
|
||||
} else {
|
||||
var parts, i;
|
||||
if (!inText) {
|
||||
enyo.jihadToast.show("info", $L("The clipboard is empty."));
|
||||
} else if (inText.length > 262144) {
|
||||
enyo.jihadToast.show("info", $L("The clipboard holds too much text to paste here."));
|
||||
} else {
|
||||
parts = enyo.jihadPasteChunks(String(inText));
|
||||
for (i = 0; i < parts.length; i++) {
|
||||
view.callBrowserAdapter("insertStringAtCursor", [parts[i]]);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
@@ -541,6 +619,7 @@ enyo.kind({
|
||||
this._timeoutHandle = null;
|
||||
}
|
||||
this.isErrorLoadFailed = false;
|
||||
this.$.actionbar.setLoadFailed(false);
|
||||
this.$.actionbar.setLoading(true);
|
||||
},
|
||||
loadProgress: function(inSender, inProgress) {
|
||||
@@ -565,6 +644,11 @@ enyo.kind({
|
||||
this._timeoutHandle = null;
|
||||
},
|
||||
browserError: function(inSender, inErrorCode, inMsg) {
|
||||
// What is on screen now is an error, not the site in the address bar: no padlock.
|
||||
// (A cancelled load is not an error and leaves the page that was there.)
|
||||
if (inErrorCode !== this.WebKitErrors.ERR_WK_FLOADER_CANCELLED) {
|
||||
this.$.actionbar.setLoadFailed(true);
|
||||
}
|
||||
|
||||
switch(inErrorCode){
|
||||
case this.WebKitErrors.ERR_SYS_FILE_DOESNT_EXIST:
|
||||
|
||||
@@ -186,6 +186,10 @@ enyo.kind({
|
||||
// the copied text was put. Neither is a sentence to show.
|
||||
if (inResponse.category === "page-hold") { this.$.browser.pageHold(inResponse.text); return; }
|
||||
if (inResponse.category === "page-copied") { this.$.browser.pageCopied(inResponse.text); return; }
|
||||
// What the engine says about a copy or a cut is broadcast to every card; it is
|
||||
// shown by the one that asked.
|
||||
if (inResponse.category === "page-copy-empty" && !this.$.browser.pageCopyEmpty()) { return; }
|
||||
if ((inResponse.category === "page-cut" || inResponse.category === "page-cut-refused") && !this.$.browser.pageCutResult()) { return; }
|
||||
enyo.jihadToast.show(inResponse.category, inResponse.text);
|
||||
},
|
||||
//* A failed subscription is not worth a dialog: it costs the user their status toasts and
|
||||
|
||||
@@ -78,6 +78,17 @@ enyo.kind({
|
||||
// ]},
|
||||
{kind:"RowGroup", caption:$L("Public Key Algorithm"), components:[
|
||||
{name:"publicKeyAlgorithm"}
|
||||
]},
|
||||
// What the daemon adds for a page that is loaded (the address bar's padlock): the
|
||||
// certificate's fingerprint, and the connection the page came over.
|
||||
{kind:"RowGroup", name:"fingerprintGroup", caption:$L("SHA-256 Fingerprint"), showing: false, components:[
|
||||
{name:"fingerprint", style: "word-wrap: break-word; font-size: 14px;"}
|
||||
]},
|
||||
{kind:"RowGroup", name:"connectionGroup", caption:$L("Connection"), showing: false, components:[
|
||||
{name:"connection"}
|
||||
]},
|
||||
{kind:"RowGroup", name:"trustGroup", caption:$L("Trust"), showing: false, components:[
|
||||
{name:"trust"}
|
||||
]}
|
||||
// {kind:"RowGroup", caption:$L("Public Key"), components:[
|
||||
// {name:"publicKey", kind: "RichText", richContent: false}
|
||||
@@ -114,19 +125,14 @@ enyo.kind({
|
||||
this.$.commonName.setContent(enyo.string.escapeHtml(alternate_name));
|
||||
}
|
||||
|
||||
if(this.cert.displayIssuerCommonName) {
|
||||
this.$.displayIssuerCommonName.setContent(enyo.string.escapeHtml(this.cert.displayIssuerCommonName));
|
||||
}
|
||||
// (Set whether or not present: the view is reused, and a certificate lacking a field
|
||||
// must not show the previous certificate's.)
|
||||
this.$.displayIssuerCommonName.setContent(enyo.string.escapeHtml(this.cert.displayIssuerCommonName || ""));
|
||||
|
||||
// The dates arrive as text. Formatted when they parse; shown as given when they do not
|
||||
// (the daemon sends the engine's own local-time string, which this WebKit may not parse).
|
||||
if(this.cert.startdate) {
|
||||
this.$.startDate.setContent(this.formatCertDate(this.cert.startdate));
|
||||
}
|
||||
|
||||
if(this.cert.expiredate) {
|
||||
this.$.expirationDate.setContent(this.formatCertDate(this.cert.expiredate));
|
||||
}
|
||||
this.$.startDate.setContent(this.cert.startdate ? this.formatCertDate(this.cert.startdate) : "");
|
||||
this.$.expirationDate.setContent(this.cert.expiredate ? this.formatCertDate(this.cert.expiredate) : "");
|
||||
|
||||
if(this.cert.subject) {
|
||||
|
||||
@@ -170,8 +176,33 @@ enyo.kind({
|
||||
this.$.issuerCommonName.setContent(enyo.string.escapeHtml(this.cert.issuer.commonname));
|
||||
}
|
||||
|
||||
this.$.serialNumber.setContent(enyo.string.escapeHtml(this.cert.serialNumber));
|
||||
this.$.version.setContent(enyo.string.escapeHtml(this.cert.version));
|
||||
this.$.serialNumber.setContent(enyo.string.escapeHtml(this.cert.serialNumber !== undefined ? String(this.cert.serialNumber) : ""));
|
||||
this.$.version.setContent(enyo.string.escapeHtml(this.cert.version !== undefined ? String(this.cert.version) : ""));
|
||||
// A view is reused: whatever the last certificate set here must not outlive it.
|
||||
this.$.signatureAlgorithm.setContent("");
|
||||
this.$.publicKeyAlgorithm.setContent("");
|
||||
|
||||
this.$.fingerprintGroup.setShowing(this.cert.sha256 ? true : false);
|
||||
this.$.fingerprint.setContent(enyo.string.escapeHtml(this.cert.sha256 ? String(this.cert.sha256) : ""));
|
||||
var conn = this.cert.connection, bits = [];
|
||||
if (conn) {
|
||||
if (conn.protocol) { bits.push(String(conn.protocol)); }
|
||||
if (conn.cipher) { bits.push(String(conn.cipher)); }
|
||||
if (conn.keyLength) { bits.push(conn.keyLength + $L("-bit keys")); }
|
||||
}
|
||||
this.$.connectionGroup.setShowing(bits.length > 0);
|
||||
this.$.connection.setContent(enyo.string.escapeHtml(bits.join(", ")));
|
||||
// Whether the certificate was believed, and whether the page counts as secure.
|
||||
var warn = [];
|
||||
if (this.cert.overridden || this.cert.untrusted || this.cert.mismatch || this.cert.badTime) {
|
||||
warn.push(this.cert.mismatch ? $L("This certificate was issued for a different site.") :
|
||||
this.cert.badTime ? $L("This certificate is not valid at this time.") :
|
||||
$L("This certificate could not be verified."));
|
||||
if (this.cert.overridden) { warn.push($L("You chose to trust it.")); }
|
||||
}
|
||||
if (this.cert.broken) { warn.push($L("This page is not fully secure: part of it, or the connection itself, is not protected.")); }
|
||||
this.$.trustGroup.setShowing(warn.length > 0);
|
||||
this.$.trust.setContent(enyo.string.escapeHtml(warn.join(" ")));
|
||||
|
||||
if(this.cert.signature) {
|
||||
if(this.cert.signature.algorithm) {
|
||||
@@ -250,6 +281,13 @@ enyo.kind({
|
||||
}
|
||||
}
|
||||
},
|
||||
//* Show a certificate the caller already has — the loaded page's own, which the address
|
||||
//* bar's padlock asks the daemon for. (certFile, above, is the other way in: the
|
||||
//* certificate a failed load is asking about.)
|
||||
showCertificate: function(inCert) {
|
||||
this.validateComponents();
|
||||
this.$.certDetail.setCert(inCert);
|
||||
},
|
||||
handleCertificate: function(inSender, inResponse) {
|
||||
// this.warn("cert details: ", enyo.json.stringify(inResponse));
|
||||
if(inResponse.returnValue) {
|
||||
|
||||
@@ -19,14 +19,16 @@ enyo.kind({
|
||||
url: "",
|
||||
searchPreferences: [],
|
||||
defaultSearch: "",
|
||||
loading: false
|
||||
loading: false,
|
||||
loadFailed: false
|
||||
},
|
||||
maxSearchResults: 32,
|
||||
className: "addressbar",
|
||||
events: {
|
||||
onLoad: "",
|
||||
onStopLoad: "",
|
||||
onRefresh: ""
|
||||
onRefresh: "",
|
||||
onSecureInfo: ""
|
||||
},
|
||||
components: [
|
||||
{name: "bookmarksService", kind: "DbService", method: "search", dbKind: "net.riverstonerelay.jihad-browser.bookmarks:1", onSuccess: "gotBookmarksData", onFailure: "finishShowSearchResults"},
|
||||
@@ -35,7 +37,8 @@ enyo.kind({
|
||||
onInputChange: "startSearch",
|
||||
onGo: "go",
|
||||
onStop: "doStopLoad",
|
||||
onRefresh: "doRefresh"
|
||||
onRefresh: "doRefresh",
|
||||
onSecureInfo: "doSecureInfo"
|
||||
},
|
||||
{name: "searchPopup", kind: "Menu", lazy: false, modal: false, className: "addressbar-popup", components: [
|
||||
{name: "providersList", className: "addressbar-providerslist", kind: "VirtualRepeater", onSetupRow: "providersListGetItem", components: [
|
||||
@@ -79,6 +82,9 @@ enyo.kind({
|
||||
cacheBoxSize: function() {
|
||||
this._boxSize = enyo.fetchControlSize(this);
|
||||
},
|
||||
loadFailedChanged: function() {
|
||||
this.$.address.setLoadFailed(this.loadFailed);
|
||||
},
|
||||
urlChanged: function() {
|
||||
this.$.address.setUrl(this.url);
|
||||
},
|
||||
|
||||
+69
-16
@@ -22,33 +22,86 @@ enyo.setClipboard = function(inText) {
|
||||
document.body.removeChild(n);
|
||||
}
|
||||
|
||||
//* Put inText on the clipboard and say whether that worked (enyo.setClipboard above does the
|
||||
//* same and does not say).
|
||||
enyo.jihadSetClipboard = function(inText) {
|
||||
var n = document.createElement("textarea"), ok = false;
|
||||
n.style.cssText = "position: absolute; height: 0px; width: 0px;";
|
||||
n.value = inText;
|
||||
document.body.appendChild(n);
|
||||
try {
|
||||
n.select();
|
||||
ok = document.execCommand("cut") !== false;
|
||||
} catch (e) {
|
||||
ok = false;
|
||||
}
|
||||
document.body.removeChild(n);
|
||||
return ok;
|
||||
};
|
||||
|
||||
//* Read the clipboard and hand its text to inCallback: a throwaway textarea and the system's
|
||||
//* paste into it. enyo.dom.getClipboard does the same and reads the textarea one tick later,
|
||||
//* but the paste takes anything from a few milliseconds to more than 60 (measured in the
|
||||
//* Mochi shell, same platform call), so this watches the textarea for up to 600 ms before
|
||||
//* calling the clipboard empty.
|
||||
//* calling the clipboard empty. Whatever happens, the textarea goes, the keyboard is given
|
||||
//* back and the callback is called exactly once.
|
||||
enyo.jihadGetClipboard = function(inCallback) {
|
||||
var n = document.createElement("textarea"), tries = 0;
|
||||
n.style.cssText = "position: absolute; height: 0px; width: 0px;";
|
||||
n.value = "";
|
||||
document.body.appendChild(n);
|
||||
enyo.keyboard.suspend();
|
||||
n.select();
|
||||
if (window.PalmSystem && PalmSystem.paste) {
|
||||
PalmSystem.paste();
|
||||
} else {
|
||||
document.execCommand("paste");
|
||||
var n = document.createElement("textarea"), tries = 0, done = false;
|
||||
function finish(inText) {
|
||||
if (done) {
|
||||
return;
|
||||
}
|
||||
done = true;
|
||||
try { n.blur(); } catch (e) {}
|
||||
try { enyo.keyboard.resume(); } catch (e) {}
|
||||
if (n.parentNode) {
|
||||
n.parentNode.removeChild(n);
|
||||
}
|
||||
inCallback(inText);
|
||||
}
|
||||
function look() {
|
||||
if (!n.value && ++tries < 15) {
|
||||
setTimeout(look, 40);
|
||||
return;
|
||||
}
|
||||
var text = n.value;
|
||||
n.blur();
|
||||
enyo.keyboard.resume();
|
||||
document.body.removeChild(n);
|
||||
inCallback(text);
|
||||
finish(n.value);
|
||||
}
|
||||
try {
|
||||
n.style.cssText = "position: absolute; height: 0px; width: 0px;";
|
||||
n.value = "";
|
||||
document.body.appendChild(n);
|
||||
enyo.keyboard.suspend();
|
||||
n.select();
|
||||
if (window.PalmSystem && PalmSystem.paste) {
|
||||
PalmSystem.paste();
|
||||
} else {
|
||||
document.execCommand("paste");
|
||||
}
|
||||
} catch (e) {
|
||||
finish("");
|
||||
return;
|
||||
}
|
||||
setTimeout(look, 20);
|
||||
};
|
||||
|
||||
//* inText as the pieces a paste is sent in. One message to the engine holds 16 KB and a
|
||||
//* longer string is dropped whole (the IPC's own limit), so each piece is at most 12000
|
||||
//* bytes of UTF-8, and no piece ends inside a surrogate pair.
|
||||
enyo.jihadPasteChunks = function(inText) {
|
||||
var out = [], start = 0, bytes = 0, i, c, n;
|
||||
for (i = 0; i < inText.length; i++) {
|
||||
c = inText.charCodeAt(i);
|
||||
n = c < 0x80 ? 1 : (c < 0x800 ? 2 : 3);
|
||||
// A low surrogate stays with the high one before it.
|
||||
if (bytes + n > 12000 && !(c >= 0xDC00 && c <= 0xDFFF)) {
|
||||
out.push(inText.substring(start, i));
|
||||
start = i;
|
||||
bytes = 0;
|
||||
}
|
||||
bytes += n;
|
||||
}
|
||||
if (start < inText.length) {
|
||||
out.push(inText.substring(start));
|
||||
}
|
||||
return out;
|
||||
};
|
||||
@@ -1168,7 +1168,11 @@ void BrowserAdapter::handlePaint(NpPalmDrawEvent* event)
|
||||
double cz = (gi->contentZoom > 0.0) ? gi->contentZoom : 1.0;
|
||||
double iv = (mZoomLevel > 0.0) ? (cz / mZoomLevel) : 1.0;
|
||||
bool nearUnity = iv > 0.98 && iv < 1.02;
|
||||
if (nearUnity && mWindow.width > 0 && gi->renderedWidth != (int)mWindow.width)
|
||||
// bufferWidth, not renderedWidth: since 2026-10-05 the painted region may be
|
||||
// WIDER than the viewport when zoomed in (sideways pan headroom), and what
|
||||
// this guard asks is which viewport the frame was rendered for. The two are
|
||||
// equal in every frame from a daemon that predates that.
|
||||
if (nearUnity && mWindow.width > 0 && gi->bufferWidth != (int)mWindow.width)
|
||||
return;
|
||||
}
|
||||
PGSurface* surf = PGSurface::wrap((uint32_t)gi->renderedWidth, (uint32_t)gi->renderedHeight,
|
||||
@@ -1275,7 +1279,8 @@ void BrowserAdapter::handlePaint(NpPalmDrawEvent* event)
|
||||
double cz = (info->contentZoom > 0.0) ? info->contentZoom : 1.0;
|
||||
double inv = (mZoomLevel > 0.0) ? (cz / mZoomLevel) : 1.0;
|
||||
bool nearUnity = inv > 0.98 && inv < 1.02;
|
||||
if (nearUnity && mWindow.width > 0 && info->renderedWidth != (int)mWindow.width)
|
||||
// bufferWidth is the viewport the frame was rendered for (see the PGContext path).
|
||||
if (nearUnity && mWindow.width > 0 && info->bufferWidth != (int)mWindow.width)
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2300,6 +2305,21 @@ const char* BrowserAdapter::js_addUrlRedirect(AdapterBase *adapterBase, const NP
|
||||
|
||||
const char* BrowserAdapter::js_pageFocused(AdapterBase *adapter, const NPVariant *args, uint32_t argCount, NPVariant *result)
|
||||
{
|
||||
// Jihad: this was an empty stub, and three things were built on the belief that it was not.
|
||||
// A request a card makes over Luna (a bookmark thumbnail, printing, the padlock's
|
||||
// certificate) cannot say which card it comes from, so the card calls pageFocused(true)
|
||||
// first and the daemon answers for the page that said so last. With the stub, "last" was
|
||||
// whichever card last CONNECTED or was given the plugin focus — with two cards open, not
|
||||
// necessarily the one asking, and the padlock would then have shown another page's
|
||||
// certificate (review 2026-10-05). The message is the adapter's own existing
|
||||
// asyncCmdPageFocused; nothing is added to the contract.
|
||||
BrowserAdapter *a = GetAndInitAdapter(adapter);
|
||||
if (!a->mBrowserServerConnected)
|
||||
return NULL;
|
||||
bool focused = true;
|
||||
if (argCount >= 1 && IsBooleanVariant(args[0]))
|
||||
focused = VariantToBoolean(args[0]);
|
||||
a->asyncCmdPageFocused(focused);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@ void ProxySink::msgLocationChanged(const char* uri, bool b, bool f) { mSrv->msgL
|
||||
void ProxySink::msgTitleChanged(const char* t) { mSrv->msgTitleChanged(mProxy, t); }
|
||||
void ProxySink::msgTitleAndUrlChanged(const char* title, const char* uri, bool b, bool f) { mSrv->msgTitleAndUrlChanged(mProxy, title, uri, b, f); }
|
||||
void ProxySink::msgEditorFocused(bool focused, int fieldType, int fieldActions) { mSrv->msgEditorFocused(mProxy, focused, fieldType, fieldActions); }
|
||||
void ProxySink::msgCopySuccessResponse(int queryNum, bool success) { mSrv->msgCopySuccessResponse(mProxy, queryNum, success); }
|
||||
void ProxySink::msgContentsSizeChanged(int32_t w, int32_t h) { mSrv->msgContentsSizeChanged(mProxy, w, h); }
|
||||
void ProxySink::msgScrolledTo(int32_t x, int32_t y) { mSrv->msgScrolledTo(mProxy, x, y); }
|
||||
void ProxySink::msgMetaViewportSet(double is, double mn, double mx, int32_t w, int32_t h, bool us) { mSrv->msgMetaViewportSet(mProxy, is, mn, mx, w, h, us); }
|
||||
@@ -730,7 +731,17 @@ void JihadBrowserServer::asyncCmdPageFocused(YapProxy* proxy, bool focused)
|
||||
{
|
||||
// Remembered as "the card the user is in": a request that arrives over Luna (a bookmark
|
||||
// thumbnail) carries no page, and a card says which page it means by sending this first.
|
||||
if (focused && pageFor(proxy)) mFocusProxy = proxy;
|
||||
// One line when the answer changes: "which card do Luna requests mean" is otherwise
|
||||
// invisible, and getting it wrong shows one page's thumbnail or certificate in another.
|
||||
if (focused && pageFor(proxy)) {
|
||||
if (mFocusProxy != proxy) printf("[jihad-bs] focus: Luna requests are now answered for the card that just said it has the focus\n");
|
||||
mFocusProxy = proxy;
|
||||
} else if (!focused && proxy == mFocusProxy && mFocusProxy) {
|
||||
// The focused card says it no longer is. Until a card claims the focus, requests go to
|
||||
// the newest connection (the fallback every such request already has).
|
||||
printf("[jihad-bs] focus: the focused card gave it up\n");
|
||||
mFocusProxy = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
bool JihadBrowserServer::printActivePage(const std::string& requestJson) {
|
||||
@@ -742,6 +753,12 @@ bool JihadBrowserServer::printActivePage(const std::string& requestJson) {
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool JihadBrowserServer::certificateForActivePage(std::string* json) {
|
||||
jihad::BrowserPageGoanna* p = pageFor(mFocusProxy);
|
||||
if (!p) p = pageFor(mLastProxy);
|
||||
return p && p->pageCertificate(json);
|
||||
}
|
||||
|
||||
// A thumbnail of the page the user is in, written to `path` at that page's next paint. The
|
||||
// card sends pageFocused(true) immediately before asking, so this is its own page even with
|
||||
// several cards open; with no such card it falls back to the newest connection.
|
||||
@@ -955,21 +972,20 @@ void JihadBrowserServer::asyncCmdGetElementInfoAtPoint(YapProxy* proxy, int32_t
|
||||
}
|
||||
|
||||
// The long-press menu's page actions. The card raises the menu and sends these; the bundled
|
||||
// component components/jihadPageCommands.js carries them out on the page.
|
||||
// component components/jihadPageCommands.js carries them out on the page — from the pump
|
||||
// tick, not from here (BrowserPageGoanna::pageCommand queues them).
|
||||
void JihadBrowserServer::asyncCmdSelectAll(YapProxy* proxy)
|
||||
{
|
||||
if (auto* p = pageFor(proxy)) p->pageCommand("selectAll");
|
||||
if (auto* p = pageFor(proxy)) { p->pageCommand("selectAll"); if (mWakeHook) mWakeHook(); }
|
||||
}
|
||||
|
||||
void JihadBrowserServer::asyncCmdCopy(YapProxy* proxy, int32_t queryNum)
|
||||
{
|
||||
// "Copy" here means: hand the selected text to the card, which owns the clipboard (it lives
|
||||
// in LunaSysMgr; a daemon has no way to write it). The reply below only releases the
|
||||
// adapter's callback record for this query — whether anything was selected is told to the
|
||||
// card separately, with the text.
|
||||
auto* p = pageFor(proxy);
|
||||
if (p) p->pageCommand("copy");
|
||||
msgCopySuccessResponse(proxy, queryNum, p != nullptr);
|
||||
// in LunaSysMgr; a daemon has no way to write it). The adapter's callback for this query is
|
||||
// answered when the command has run, with whether any text was handed over.
|
||||
if (auto* p = pageFor(proxy)) { p->pageCommand("copy", queryNum); if (mWakeHook) mWakeHook(); }
|
||||
else msgCopySuccessResponse(proxy, queryNum, false);
|
||||
}
|
||||
|
||||
void JihadBrowserServer::asyncCmdPaste(YapProxy* proxy)
|
||||
@@ -981,7 +997,10 @@ void JihadBrowserServer::asyncCmdPaste(YapProxy* proxy)
|
||||
|
||||
void JihadBrowserServer::asyncCmdCut(YapProxy* proxy)
|
||||
{
|
||||
if (auto* p = pageFor(proxy)) p->pageCommand("cut");
|
||||
// The SECOND half of a cut: delete the text the last copy handed to the card. A card asks
|
||||
// for it only once that text is on the clipboard; the component refuses unless the
|
||||
// selection is still exactly what it copied.
|
||||
if (auto* p = pageFor(proxy)) { p->pageCommand("cut"); if (mWakeHook) mWakeHook(); }
|
||||
}
|
||||
|
||||
void JihadBrowserServer::asyncCmdSetMouseMode(YapProxy* proxy, int32_t mode)
|
||||
|
||||
@@ -33,6 +33,7 @@ public:
|
||||
void msgTitleChanged(const char* t) override;
|
||||
void msgTitleAndUrlChanged(const char* title, const char* uri, bool b, bool f) override;
|
||||
void msgEditorFocused(bool focused, int fieldType, int fieldActions) override;
|
||||
void msgCopySuccessResponse(int queryNum, bool success) override;
|
||||
void msgContentsSizeChanged(int32_t w, int32_t h) override;
|
||||
void msgScrolledTo(int32_t x, int32_t y) override;
|
||||
void msgMetaViewportSet(double is, double mn, double mx, int32_t w, int32_t h, bool us) override;
|
||||
@@ -96,6 +97,8 @@ public:
|
||||
bool thumbnailForActivePage(const std::string& path);
|
||||
// Hand the page the user is in to the printing component with the card's request (JSON).
|
||||
bool printActivePage(const std::string& requestJson);
|
||||
// The certificate of the page the user is in, as JSON. False if it is not a TLS page.
|
||||
bool certificateForActivePage(std::string* json);
|
||||
// Whether the last tick had anything to do: a page that is busy (BrowserPageGoanna::isBusy) or
|
||||
// a command from a card since the tick before. main() uses it to choose the tick period.
|
||||
bool tickWasBusy() const { return mTickBusy; }
|
||||
|
||||
@@ -401,6 +401,29 @@ bool onPrintPage(LSHandle* sh, LSMessage* msg, void*) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// pageCertificate — what the padlock in the address bar shows: whose certificate the page
|
||||
// the user is in was served with, who issued it, its dates, and the connection's protocol
|
||||
// and cipher. Answered in the reply itself — which, unlike the notification channel, goes
|
||||
// to the caller alone — and only to this variant's own card: which site a user is on is
|
||||
// not something for any app on the device to ask. The card sends pageFocused(true) first,
|
||||
// so the daemon knows which of its cards is asking.
|
||||
// <- {"returnValue":true,"secure":true,"subject":{…},"issuer":{…},"startdate":…,
|
||||
// "expiredate":…,"serialNumber":…,"sha256":…,"connection":{…},
|
||||
// "broken":false,"overridden":false,"untrusted":false,"mismatch":false,"badTime":false}
|
||||
// <- {"returnValue":true,"secure":false} (not a TLS page)
|
||||
jihad::LunaCertificateFn gCertificateFn = nullptr;
|
||||
bool onPageCertificate(LSHandle* sh, LSMessage* msg, void*) {
|
||||
const bool allowed = callerIsOwnCard(msg);
|
||||
std::string out;
|
||||
const bool secure = allowed && gCertificateFn && gCertificateFn(&out) && !out.empty();
|
||||
printf("[jihad-bs] luna: pageCertificate (%s)\n",
|
||||
!allowed ? "REFUSED: caller is not this variant's card" : secure ? "sent" : "not a TLS page");
|
||||
if (!allowed) { reply(sh, msg, false); return true; }
|
||||
if (!secure) out = "{\"returnValue\":true,\"secure\":false}";
|
||||
replyJson(sh, msg, out);
|
||||
return true;
|
||||
}
|
||||
|
||||
// notifications — the CARD's non-blocking message channel (cavekit-gre-widgets.md R5).
|
||||
//
|
||||
// The card calls this ONCE at startup with {"subscribe":true} and leaves the call open; the
|
||||
@@ -504,6 +527,7 @@ LSMethod gMethodsPublic[] = {
|
||||
{ "findPrinters", onFindPrinters, 0 },
|
||||
{ "getFlash", onGetFlash, 0 },
|
||||
{ "notifications", onNotifications, 0 },
|
||||
{ "pageCertificate", onPageCertificate, 0 },
|
||||
{ "printPage", onPrintPage, 0 },
|
||||
{ "saveThumbnail", onSaveThumbnail, 0 },
|
||||
{ 0, 0, 0 },
|
||||
@@ -641,7 +665,7 @@ static bool startOnce(const std::string& serviceName, GMainLoop* loop, bool verb
|
||||
if (gApi.SubscriptionAdd && gApi.SubscriptionReply) SetNotificationSink(&gNotifier);
|
||||
printf("[jihad-bs] luna: serving palm://%s/ — public{clearCache,clearCookies,notifications,"
|
||||
"getFlash,enableFlash,disableFlash,saveThumbnail,deleteThumbnail,clearThumbnails,"
|
||||
"findPrinters,printPage} "
|
||||
"findPrinters,printPage,pageCertificate} "
|
||||
"private{clearCache,clearCookies,getChromePrefs,getFlash,notifications} (toast channel %s)\n",
|
||||
serviceName.c_str(),
|
||||
(gApi.SubscriptionAdd && gApi.SubscriptionReply) ? "ON" : "OFF");
|
||||
@@ -733,6 +757,7 @@ gboolean retryRegister(gpointer) {
|
||||
void LunaServiceSetOwnAppId(const std::string& appId) { gOwnAppId = appId; }
|
||||
void LunaServiceSetThumbnailFn(LunaThumbnailFn fn) { gThumbnailFn = fn; }
|
||||
void LunaServiceSetPrintFn(LunaPrintFn fn) { gPrintFn = fn; }
|
||||
void LunaServiceSetCertificateFn(LunaCertificateFn fn) { gCertificateFn = fn; }
|
||||
|
||||
bool LunaServiceStart(const std::string& serviceName, GMainLoop* loop) {
|
||||
if (startOnce(serviceName, loop, /*verbose*/true)) return true;
|
||||
|
||||
@@ -65,6 +65,11 @@ void LunaServiceSetThumbnailFn(LunaThumbnailFn fn);
|
||||
typedef bool (*LunaPrintFn)(const std::string& requestJson);
|
||||
void LunaServiceSetPrintFn(LunaPrintFn fn);
|
||||
|
||||
// Who knows the active page's certificate: fills `json` (one object, returnValue included)
|
||||
// and says whether the page came over TLS. Set by the server before LunaServiceStart.
|
||||
typedef bool (*LunaCertificateFn)(std::string* json);
|
||||
void LunaServiceSetCertificateFn(LunaCertificateFn fn);
|
||||
|
||||
// Best-effort unregister. Safe if Start() failed or was never called.
|
||||
void LunaServiceStop();
|
||||
|
||||
|
||||
@@ -347,6 +347,9 @@ int main(int argc, char** argv) {
|
||||
jihad::LunaServiceSetPrintFn([](const std::string& requestJson) -> bool {
|
||||
return sThumbServer && sThumbServer->printActivePage(requestJson);
|
||||
});
|
||||
jihad::LunaServiceSetCertificateFn([](std::string* json) -> bool {
|
||||
return sThumbServer && sThumbServer->certificateForActivePage(json);
|
||||
});
|
||||
jihad::LunaServiceStart(jihad::LunaServiceNameFor(name), server.mainLoop());
|
||||
|
||||
printf("[jihad-bs] engine up; serving YAP '%s' (waiting for BrowserAdapter)\n", name);
|
||||
|
||||
@@ -33,6 +33,13 @@ struct BrowserOffscreenInfo
|
||||
// is baked into these dimensions. For eg: renderedX = renderedZoom * pageX
|
||||
// renderedWidth == bufferWidth;
|
||||
// renderedHeight == renderedHeight (<= bufferHeight)
|
||||
//
|
||||
// Jihad Browser, 2026-10-05 (layout unchanged, one meaning narrowed): bufferWidth is the
|
||||
// width of the VIEWPORT the frame was rendered for, and renderedWidth the width of the
|
||||
// region actually painted, which is also the pixel rows' stride. They are equal except
|
||||
// when the page is zoomed in, where the region may be up to two viewports wide so that
|
||||
// a sideways pan has something to show. The adapter's rotation guard compares
|
||||
// bufferWidth with its window; everything that reads pixels uses renderedWidth.
|
||||
int renderedX;
|
||||
int renderedY;
|
||||
int renderedWidth;
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
#include <sys/shm.h>
|
||||
#include <sys/stat.h> // chmod (popup menu data file 0644), mkfifo (dialog reply pipe)
|
||||
#include <fcntl.h> // open() the dialog reply FIFO
|
||||
#include <sys/statvfs.h> // room on /tmp before an on-request frame dump
|
||||
#include <poll.h> // wait for the card's answer with a deadline
|
||||
#include <unistd.h>
|
||||
#include <pthread.h> // Jihad audio-scenario poll thread
|
||||
@@ -593,6 +594,7 @@ void BrowserPageGoanna::setWindowSize(uint32_t width, uint32_t height) {
|
||||
// Same reasoning as the navigation reset: a resize replaces the whole frame, and both
|
||||
// buffers still hold the old one.
|
||||
mBufFull[0].valid = mBufFull[1].valid = mBufFull[2].valid = false;
|
||||
mLatestSlot = -1; mGrowOwed = false; mSweepLeft = 0; mSweepRow = 0;
|
||||
mDamageValid = false; mDamageForceFull = true;
|
||||
if (!mPage) return;
|
||||
// The BrowserAdapter owns the shared framebuffer and its size (Codex P1/P2);
|
||||
@@ -719,6 +721,7 @@ void BrowserPageGoanna::thaw(int key1, int key2, int size) {
|
||||
// plugin animating, mDamageValid is true on essentially every tick, so that is the
|
||||
// guaranteed first frame after thaw, not a rare one.
|
||||
mBufFull[0].valid = mBufFull[1].valid = mBufFull[2].valid = false;
|
||||
mLatestSlot = -1; mGrowOwed = false; mSweepLeft = 0; mSweepRow = 0;
|
||||
mDamageValid = false; mDamageForceFull = true;
|
||||
mFrozen = false;
|
||||
mNeedsPaint = true;
|
||||
@@ -759,6 +762,7 @@ void BrowserPageGoanna::resetScrollForNavigation() {
|
||||
// those identical while replacing all the pixels — so geometry alone cannot catch it and
|
||||
// the record has to be dropped explicitly.
|
||||
mBufFull[0].valid = mBufFull[1].valid = mBufFull[2].valid = false;
|
||||
mLatestSlot = -1; mGrowOwed = false; mSweepLeft = 0; mSweepRow = 0;
|
||||
mDamageValid = false; mDamageForceFull = true;
|
||||
// The old page's plugins are gone; their rects must not outlive them. The adapter never
|
||||
// clears mFlashRects itself, so leaving them behind makes the NEW page carry invisible
|
||||
@@ -771,11 +775,54 @@ void BrowserPageGoanna::resetScrollForNavigation() {
|
||||
// re-emit at load-done even if the size repeats
|
||||
mLastScrollX = 0; mLastScrollY = 0; // emitScrollIfChanged dedup cache
|
||||
mLastScrollMs = 0; // disarm the adapter-driven suppression window
|
||||
mPaintedX = 0; mPaintedLo = 0; mPaintedHi = 0; // no stale coverage claim (setScrollPosition)
|
||||
mPaintedX = 0; mPaintedW = 0; mPaintedLo = 0; mPaintedHi = 0; // no stale coverage claim (setScrollPosition)
|
||||
mPrevPaintScrollY = 0; // overscan direction bias
|
||||
mSink.msgScrolledTo(0, 0); // the adapter-side half of the reset
|
||||
}
|
||||
|
||||
// Is the adapter's viewport at (x,y) — zoomed px, its own scroll — inside the region the last
|
||||
// paint covered, with room to spare? Repaint only when it nears the region's edge or leaves
|
||||
// it: panning inside the buffer is exactly what the headroom is for.
|
||||
bool BrowserPageGoanna::panCovered(int x, int y) {
|
||||
if (!mPage) return false;
|
||||
double z = (mZoom >= 0.05 && mZoom <= 20.0) ? mZoom : 1.0;
|
||||
long h2 = (long)mPage->Height(), w2 = (long)mPage->Width();
|
||||
long slack = h2 / 2; // trigger EARLY: the repaint takes real time on this CPU and the
|
||||
// fling keeps consuming rows while it renders ("content doesn't
|
||||
// stay consistently visible" — h/4 triggered too late)
|
||||
// ...but never more than the region can give. The paint puts as little as a sixth of its
|
||||
// spare rows behind the pan, and a region that is two viewports WIDE has half the rows:
|
||||
// with a fixed half-viewport margin the position just painted counted as uncovered, and
|
||||
// every pan message asked for another paint. An eighth of the spare rows always fits.
|
||||
{
|
||||
const long spare = (mPaintedHi - mPaintedLo) - h2;
|
||||
if (spare > 0 && spare / 8 < slack) slack = spare / 8;
|
||||
if (spare <= 0) slack = 0;
|
||||
}
|
||||
double Zc = (z >= 0.99 && z <= 1.01) ? 1.0 : z; // the zoom the paint path would use
|
||||
long contentRows = (mLastContentH > 0) ? (long)((double)mLastContentH * Zc) : 0;
|
||||
long loBound = (mPaintedLo <= 0) ? 0 : mPaintedLo + slack; // page top: no slack
|
||||
long hiBound = (contentRows > 0 && mPaintedHi >= contentRows)
|
||||
? mPaintedHi : mPaintedHi - slack; // page end: no slack
|
||||
// Columns. A region exactly one viewport wide (every unzoomed frame) is covered only at the
|
||||
// x it was painted for, as before. A wider one (zoomed in) is covered anywhere inside it,
|
||||
// less a quarter viewport of warning at any edge that is not the page's own.
|
||||
bool xCovered;
|
||||
if (mPaintedW <= w2) {
|
||||
xCovered = (x == mPaintedX);
|
||||
} else {
|
||||
long contentCols = (mLastContentW > 0) ? (long)((double)mLastContentW * Zc) : 0;
|
||||
long xs = w2 / 4;
|
||||
long loX = (mPaintedX <= 0) ? 0 : (long)mPaintedX + xs;
|
||||
long hiX = (long)mPaintedX + mPaintedW;
|
||||
if (!(contentCols > 0 && hiX >= contentCols)) hiX -= xs;
|
||||
xCovered = (long)x >= loX && (long)x + w2 <= hiX;
|
||||
}
|
||||
return mPaintedHi > mPaintedLo && xCovered &&
|
||||
(mPaintedZoom > Zc - 0.001 && mPaintedZoom < Zc + 0.001) &&
|
||||
(long)y >= loBound && (long)y + h2 <= hiBound;
|
||||
}
|
||||
|
||||
void BrowserPageGoanna::setScrollPosition(int x, int y) {
|
||||
if (!mPage) return;
|
||||
mLastScrollMs = jihadNowMs(); // a pan is in flight (see maybePaint)
|
||||
@@ -822,18 +869,11 @@ void BrowserPageGoanna::setScrollPosition(int x, int y) {
|
||||
// h/4) or leaves it — panning inside the buffer is exactly what the headroom is for.
|
||||
{
|
||||
long h2 = (long)mPage->Height();
|
||||
long slack = h2 / 2; // trigger EARLY: the repaint takes real time on this CPU and the
|
||||
// fling keeps consuming rows while it renders ("content doesn't
|
||||
// stay consistently visible" — h/4 triggered too late)
|
||||
double Zc = (z >= 0.99 && z <= 1.01) ? 1.0 : z; // the zoom the paint path would use
|
||||
long contentRows = (mLastContentH > 0) ? (long)((double)mLastContentH * Zc) : 0;
|
||||
long loBound = (mPaintedLo <= 0) ? 0 : mPaintedLo + slack; // page top: no slack
|
||||
long hiBound = (contentRows > 0 && mPaintedHi >= contentRows)
|
||||
? mPaintedHi : mPaintedHi - slack; // page end: no slack
|
||||
bool covered = mPaintedHi > mPaintedLo &&
|
||||
x == mPaintedX &&
|
||||
(mPaintedZoom > Zc - 0.001 && mPaintedZoom < Zc + 0.001) &&
|
||||
(long)y >= loBound && (long)y + h2 <= hiBound;
|
||||
// Covered by rows that may predate a change to the page (a sweep is owed) is not
|
||||
// covered: the viewport is repainted where it now is, so what is on screen is the page
|
||||
// as it stands and not as it was. The sweep itself waits for the pan to end.
|
||||
bool covered = panCovered(x, y) && mSweepLeft <= 0;
|
||||
if (!covered) mNeedsPaint = true; // covered: keep whatever was already pending
|
||||
// T-133 INSTRUMENT: the white band on a VKB toggle, counted in ROWS instead of inferred.
|
||||
// This is the only point in the daemon holding the adapter's pan and the painted band in one
|
||||
@@ -919,7 +959,10 @@ void BrowserPageGoanna::setZoomAndScroll(double zoom, int x, int y) {
|
||||
mPage->SetRenderPan(0.0, 0.0);
|
||||
mPage->ScrollTo((int)(x / z), (int)(y / z));
|
||||
}
|
||||
mNeedsPaint = true;
|
||||
// The card re-sends the zoom with every pan while zoomed. When the zoom is the one the
|
||||
// last frame was painted at and the viewport is still inside that frame, there is nothing
|
||||
// to paint — which is what lets a pan across a zoomed page run on the frame it has.
|
||||
if (!panCovered(x, y) || mSweepLeft > 0) mNeedsPaint = true; // (stale rows: see setScrollPosition)
|
||||
// NB: do NOT emitGeometry() here. A zoom-only command does not change the page's
|
||||
// intrinsic CSS content size, but re-reporting it fed the adapter's fit-zoom, which sent
|
||||
// a new zoom, which re-reported size... a cross-process oscillation (mZoom flapped
|
||||
@@ -1000,6 +1043,10 @@ void BrowserPageGoanna::openUrl(const char* url) {
|
||||
// generation bump stops an in-flight drain from continuing into the superseded page.
|
||||
mPendingMouse.clear(); ++mNavGen;
|
||||
mPendingEditActions.clear(); // queued Tab/Enter for the old page are stale after a navigation
|
||||
// So are queued page commands — but a copy the adapter is waiting on still gets its answer.
|
||||
for (const PendingPageCmd& c : mPendingPageCmds)
|
||||
if (c.queryNum >= 0) mSink.msgCopySuccessResponse(c.queryNum, false);
|
||||
mPendingPageCmds.clear();
|
||||
// Any navigation lowers the VKB (covers link tap, JS location.href re-drive, form submit,
|
||||
// typed URL, back/forward) — otherwise the keyboard stays up over the new page (F-005).
|
||||
if (mPage->ClearEditorFocus()) mSink.msgEditorFocused(false, 0, 0);
|
||||
@@ -1593,8 +1640,19 @@ void BrowserPageGoanna::popupMenuSelect(const char* identifier, int selectedIdx)
|
||||
mNeedsPaint = true;
|
||||
}
|
||||
|
||||
void BrowserPageGoanna::pageCommand(const char* cmd, const std::string& extraJson) {
|
||||
if (!mPage || !cmd) return;
|
||||
void BrowserPageGoanna::pageCommand(const char* cmd, int queryNum) {
|
||||
if (!cmd) return;
|
||||
// A queue a card cannot grow without bound: sixteen is a great many menu taps per tick.
|
||||
if (!mPage || mPendingPageCmds.size() >= 16) {
|
||||
if (queryNum >= 0) mSink.msgCopySuccessResponse(queryNum, false);
|
||||
return;
|
||||
}
|
||||
PendingPageCmd c; c.cmd = cmd; c.queryNum = queryNum; c.navGen = mNavGen;
|
||||
mPendingPageCmds.push_back(c);
|
||||
}
|
||||
|
||||
bool BrowserPageGoanna::runPageCommand(const char* cmd, const std::string& extraJson) {
|
||||
if (!mPage || !cmd) return false;
|
||||
// Copied text reaches the card as a file in the card's own application directory (the only
|
||||
// place a card may load a local file from — see the certificate details above for the same
|
||||
// constraint); the component is told where that is.
|
||||
@@ -1604,9 +1662,13 @@ void BrowserPageGoanna::pageCommand(const char* cmd, const std::string& extraJso
|
||||
std::string json = std::string("{\"cmd\":\"") + cmd + "\",\"dir\":\"" + dir + "\"";
|
||||
if (!extraJson.empty()) { json += ","; json += extraJson; }
|
||||
json += "}";
|
||||
// The component reports a hand-over through the notification door, synchronously, so the
|
||||
// count moving across this call is the result.
|
||||
const unsigned before = jihad::PageCopiedCount();
|
||||
if (!mPage->NotifyChrome("jihad-page-command", json.c_str()))
|
||||
fprintf(stderr, "[jihad-bs] page command %s: no page window to act on\n", cmd);
|
||||
mNeedsPaint = true;
|
||||
return jihad::PageCopiedCount() != before;
|
||||
}
|
||||
|
||||
void BrowserPageGoanna::insertStringAtCursor(const char* text) {
|
||||
@@ -2396,6 +2458,10 @@ void BrowserPageGoanna::pump(int msBudget, bool drainOnly) {
|
||||
const char* t = (e.type == PM_DOWN) ? "mousedown"
|
||||
: (e.type == PM_UP) ? "mouseup"
|
||||
: (e.type == PM_MOVE) ? "mousemove" : "contextmenu";
|
||||
// The component listens for what the page does with its contextmenu event: a page
|
||||
// that claims it (preventDefault) has a menu of its own and gets no second one.
|
||||
if (e.type == PM_CONTEXTMENU) runPageCommand("holdArm");
|
||||
if (!mPage || mNavGen != gen) break;
|
||||
mPage->MouseEvent(t, e.x, e.y, e.detail);
|
||||
if (e.type == PM_CONTEXTMENU) {
|
||||
// The long-press, after the page has had its contextmenu event. The card raises the
|
||||
@@ -2413,12 +2479,12 @@ void BrowserPageGoanna::pump(int msBudget, bool drainOnly) {
|
||||
if (mPage->TakeEditorFocus(&efoc, &eft, &efa)) mSink.msgEditorFocused(efoc, eft, efa);
|
||||
if (!mPage || mNavGen != gen) break;
|
||||
}
|
||||
char extra[128];
|
||||
snprintf(extra, sizeof extra, "\"x\":%d,\"y\":%d,\"editable\":%s,\"focusedNow\":%s",
|
||||
e.x, e.y, editable ? "true" : "false", (editable && !focused) ? "true" : "false");
|
||||
char extra[96];
|
||||
snprintf(extra, sizeof extra, "\"x\":%d,\"y\":%d,\"editable\":%s", e.x, e.y,
|
||||
editable ? "true" : "false");
|
||||
fprintf(stderr, "[jihad-bs] hold at %d,%d editable=%d wasFocused=%d\n", e.x, e.y,
|
||||
(int)editable, (int)focused);
|
||||
pageCommand("hold", extra);
|
||||
runPageCommand("hold", extra);
|
||||
}
|
||||
}
|
||||
mNeedsPaint = true;
|
||||
@@ -2427,6 +2493,21 @@ void BrowserPageGoanna::pump(int msBudget, bool drainOnly) {
|
||||
// that may move focus or submit a form (navigate), which is unsafe in the keyDown YAP callback
|
||||
// (Codex F-219). A form submit navigates via the engine and is completed by the TakeLinkClicked
|
||||
// re-drive below, exactly like any content-initiated navigation.
|
||||
// The long-press menu's page commands (selectAll / copy / cut), in the same guard and for
|
||||
// the same reason: the component may run page script. A navigation in the middle ends the
|
||||
// batch; a copy the adapter is waiting on is answered either way.
|
||||
if (!mPendingPageCmds.empty()) {
|
||||
std::vector<PendingPageCmd> cmds;
|
||||
cmds.swap(mPendingPageCmds);
|
||||
for (const PendingPageCmd& c : cmds) {
|
||||
// Asked of one document, run on that document or not at all: every navigation — back,
|
||||
// forward and reload included — moves mNavGen, and a Copy or a Cut that was queued
|
||||
// before it must not land on the page that replaced it.
|
||||
bool ok = false;
|
||||
if (mPage && mNavGen == c.navGen) ok = runPageCommand(c.cmd.c_str());
|
||||
if (c.queryNum >= 0) mSink.msgCopySuccessResponse(c.queryNum, ok);
|
||||
}
|
||||
}
|
||||
if (!mPendingEditActions.empty()) {
|
||||
// Process every queued Tab/Enter in press order (a swap-and-clear so a handler that queues more
|
||||
// doesn't reenter this loop). Stop early once an editable is gone (a submit navigated away).
|
||||
@@ -2714,6 +2795,40 @@ void BrowserPageGoanna::maybePaint() {
|
||||
// exists precisely because the pan is about to run out of painted rows — holding it
|
||||
// until the fling ended guaranteed the white gap it was trying to avoid (every scroll
|
||||
// message reset the gate, so a continuous fling starved paints entirely).
|
||||
// MAINTENANCE FRAMES — asked for here, when nothing else wants a paint:
|
||||
// grow the region is smaller than it should be (it is built a step per frame);
|
||||
// sweep its off-screen rows may predate a change to the page; redraw the next few.
|
||||
// Growing waits only for the last frame to have been taken; sweeping waits for the page
|
||||
// to be loaded and still, and never runs during a pan — that is when the daemon's time
|
||||
// is worth most. A maintenance frame that could not run (the adapter still holds the
|
||||
// buffer) is simply asked for again on a later tick: mMaintenance still set means that.
|
||||
// A frame dump was asked for (<state>/dump-request, see the dump in paintToSharedBuffer):
|
||||
// paint one, so the dump is of the frame as it stands and not of whenever the page next
|
||||
// changes. Looked for twice a second, not every tick.
|
||||
if (!mNeedsPaint) {
|
||||
static int64_t sDumpLookMs = 0;
|
||||
const int64_t now = jihadNowMs();
|
||||
if (now - sDumpLookMs >= 500) {
|
||||
sDumpLookMs = now;
|
||||
static const std::string req = jihad::RuntimeStateDir().empty()
|
||||
? std::string() : jihad::RuntimeStateDir() + "/dump-request";
|
||||
if (!req.empty() && access(req.c_str(), F_OK) == 0) mNeedsPaint = true;
|
||||
}
|
||||
}
|
||||
if (!mNeedsPaint && (mGrowOwed || mSweepLeft > 0)) {
|
||||
const int64_t now = jihadNowMs();
|
||||
int kind = 0;
|
||||
if (mGrowOwed && now - mLastPaintDoneMs >= 60) kind = 1;
|
||||
else if (mSweepLeft > 0 && mLoadWasDone && now >= mSweepNotBeforeMs &&
|
||||
now - mLastScrollMs >= 500 && now - mLastPaintDoneMs >= 250) kind = 2;
|
||||
if (kind) {
|
||||
mMaintenance = kind;
|
||||
mNeedsPaint = true;
|
||||
paintToSharedBuffer();
|
||||
if (mMaintenance) { mMaintenance = 0; mNeedsPaint = false; }
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (mNeedsPaint) paintToSharedBuffer(); // only when there is a new frame
|
||||
}
|
||||
|
||||
@@ -2791,8 +2906,11 @@ bool BrowserPageGoanna::isBusy() const {
|
||||
// for as long as it is frozen — which must not hold the fast tick for a card nobody can see.
|
||||
if (mFrozen) return false;
|
||||
if (mNeedsPaint || mDirtyPending || mGeometryDirty || mBlankRetryAtMs) return true;
|
||||
// Maintenance frames (maybePaint). A sweep that is only waiting out its pause does not
|
||||
// need the fast tick; any tick after the pause finds it.
|
||||
if (mGrowOwed || (mSweepLeft > 0 && mLoadWasDone && jihadNowMs() >= mSweepNotBeforeMs)) return true;
|
||||
if (mLoadStartMs != 0 && !mLoadWasDone) return true; // a load is running
|
||||
if (!mPendingMouse.empty() || !mPendingEditActions.empty()) return true;
|
||||
if (!mPendingMouse.empty() || !mPendingEditActions.empty() || !mPendingPageCmds.empty()) return true;
|
||||
if (!mEmittedFlashRects.empty()) return true; // plugin: the pull needs the grid
|
||||
const int64_t now = jihadNowMs();
|
||||
// Stay fast for a moment after the last paint or scroll: animation and panning arrive as a
|
||||
@@ -2888,6 +3006,8 @@ void BrowserPageGoanna::paintToSharedBuffer() {
|
||||
int w = mPage->Width(), h = mPage->Height();
|
||||
const size_t hdr = sizeof(BrowserOffscreenInfo);
|
||||
if (w <= 0 || h <= 0 || segSize < hdr + (size_t)w * h * 4) { return; }
|
||||
// From here the paint runs. maybePaint reads mMaintenance == 0 as "it ran".
|
||||
const int maintenanceKind = mMaintenance; mMaintenance = 0;
|
||||
BrowserOffscreenInfo* oi = (BrowserOffscreenInfo*)buf;
|
||||
unsigned char* pixels = buf + hdr;
|
||||
|
||||
@@ -2907,13 +3027,15 @@ void BrowserPageGoanna::paintToSharedBuffer() {
|
||||
// renderedWidth/Height (baseY = srcTop - renderedY, FILL outside) and holds only on a
|
||||
// WIDTH mismatch, so a taller-than-window buffer is safe on both composite paths.
|
||||
long maxRows = (long)((segSize - hdr) / ((size_t)w * 4));
|
||||
// SGX540/Piranha cap (review 2026-08-02 F1): the adapter's PRIMARY composite path wraps
|
||||
// the WHOLE painted region as one PGSurface and bitblts it through the GPU compositor.
|
||||
// Piranha is closed source and the SGX540's max texture dimension is 2048 — a portrait
|
||||
// region of 2355 rows would be the only configuration to cross it, and it is exactly the
|
||||
// configuration the desktop round-trip cannot exercise. Cap until a device run proves a
|
||||
// taller PGSurface blits.
|
||||
if (maxRows > 2048) maxRows = 2048;
|
||||
// The whole segment is the budget. Until 2026-10-05 this was capped at 2048 rows, on the
|
||||
// worry (review 2026-08-02 F1) that the adapter's PGSurface wrap of the region might be a
|
||||
// GPU texture and the SGX540's limit is 2048. It is not a texture: JIHAD_REGION_ROWS_CAP
|
||||
// unset, a 4096-row portrait region wraps and blits on the device. The variable stays as
|
||||
// the way back (a number of rows) if some configuration ever disagrees.
|
||||
{
|
||||
static const long kRowsCap = getenv("JIHAD_REGION_ROWS_CAP") ? atol(getenv("JIHAD_REGION_ROWS_CAP")) : 0;
|
||||
if (kRowsCap >= (long)h && maxRows > kRowsCap) maxRows = kRowsCap;
|
||||
}
|
||||
|
||||
// Where the viewport-relative band actually is, in ZOOMED px. At z~1 the engine scroll
|
||||
// is authoritative for Y (ScrollTo is an async javascript: — it can lag the adapter's
|
||||
@@ -2979,9 +3101,13 @@ void BrowserPageGoanna::paintToSharedBuffer() {
|
||||
if (slot >= 0 && mBufFull[slot].valid && mBufFull[slot].dmgValid &&
|
||||
mBufFull[slot].w == w && mBufFull[slot].h == h &&
|
||||
mBufFull[slot].zeff == Zeff &&
|
||||
mBufFull[slot].stripGen == mStripGen && // its off-screen rows are the newest there are
|
||||
mBufFull[slot].scrollX == mAdapterScrollX &&
|
||||
mBufFull[slot].scrollY == mAdapterScrollY) {
|
||||
const BufFrame& bf = mBufFull[slot];
|
||||
// The buffer's rows are pw px wide (the region's width: w, or more when zoomed in) and
|
||||
// the viewport starts bandCol px into each.
|
||||
const long pw = bf.paintedW > 0 ? (long)bf.paintedW : (long)w;
|
||||
// The damage box arrives in VIEWPORT device px; the buffer's rows are the painted
|
||||
// band, whose first visible row is bandRow. Clamp into the visible band and bail to
|
||||
// the full path on anything degenerate rather than guessing. Use THIS buffer's own
|
||||
@@ -2995,15 +3121,49 @@ void BrowserPageGoanna::paintToSharedBuffer() {
|
||||
if (dTop < bf.bandRow) { dH -= (bf.bandRow - dTop); dTop = bf.bandRow; }
|
||||
if (dLeft + dW > w) dW = w - dLeft;
|
||||
if (dTop + dH > (long)bf.bandRow + h) dH = (long)bf.bandRow + h - dTop;
|
||||
// A damage box that covers essentially the whole viewport saves nothing and adds a
|
||||
// code path; take the ordinary paint for it.
|
||||
// A damage box that covers essentially the whole viewport saves nothing over painting
|
||||
// the viewport...
|
||||
bool worthIt = dW > 0 && dH > 0 && (double)(dW * dH) < 0.6 * (double)(w * h);
|
||||
if (worthIt) {
|
||||
// ...but the region is usually far more than the viewport (the whole page, or every
|
||||
// row the segment holds), and re-rendering all of it because the SCREEN changed is
|
||||
// what made a page that animates, or one still loading, cost 100-250 ms a frame.
|
||||
// BAND-ONLY: redraw the visible rows, exactly as the full path draws them, and keep
|
||||
// the rest of this buffer's frame. Sound for the reason the small box is: the buffer
|
||||
// holds a complete frame of this geometry at this scroll. What it gives up is the
|
||||
// freshness of the rows nobody is looking at, which the idle sweep (mSweepLeft) pays
|
||||
// back once the page is still.
|
||||
const bool bandOnly = !worthIt && dW > 0 && dH > 0 && mThumbPath.empty() &&
|
||||
bf.paintedH > h && bf.bandRow + (long)h <= (long)bf.paintedH &&
|
||||
bf.bandCol + (long)w <= pw;
|
||||
if (worthIt || bandOnly) {
|
||||
int64_t pt0 = jihadNowMs();
|
||||
bool ok = mPage->RenderRegion(pixels + ((size_t)dTop * w + dLeft) * 4, w * 4,
|
||||
(int)dW, (int)dH,
|
||||
(double)(bf.paintedX + dLeft) / Zeff,
|
||||
(double)(bf.paintedTop + dTop) / Zeff, Zeff);
|
||||
bool ok = false;
|
||||
if (worthIt) {
|
||||
ok = mPage->RenderRegion(pixels + ((size_t)dTop * pw + bf.bandCol + dLeft) * 4, (int)pw * 4,
|
||||
(int)dW, (int)dH,
|
||||
(double)(bf.paintedX + bf.bandCol + dLeft) / Zeff,
|
||||
(double)(bf.paintedTop + dTop) / Zeff, Zeff);
|
||||
} else {
|
||||
if (!zoomed) {
|
||||
// Viewport-relative, so position:fixed content and the caret are where the user
|
||||
// sees them. A blank read is a mid-reflow white: not published; the full path
|
||||
// below repaints the buffer and applies its own blank rules.
|
||||
ok = mPage->ReadPixels(pixels + (size_t)bf.bandRow * w * 4, (size_t)w * h * 4) > 0;
|
||||
} else {
|
||||
ok = mPage->RenderRegion(pixels + ((size_t)bf.bandRow * pw + bf.bandCol) * 4, (int)pw * 4,
|
||||
w, h, (double)(bf.paintedX + bf.bandCol) / Zeff,
|
||||
(double)(bf.paintedTop + bf.bandRow) / Zeff, Zeff);
|
||||
}
|
||||
if (ok) {
|
||||
mPage->CompositePopups(pixels, (int)pw * 4, (int)pw, bf.paintedH,
|
||||
(double)bf.paintedX / Zeff, (double)bf.paintedTop / Zeff, Zeff);
|
||||
dLeft = 0; dTop = bf.bandRow; dW = w; dH = h;
|
||||
// The off-screen rows now predate a change this big: sweep them when idle.
|
||||
mSweepLeft = bf.paintedH;
|
||||
// Enough changed to have changed the page's size too (see the full path).
|
||||
mGeometryDirty = true;
|
||||
}
|
||||
}
|
||||
if (ok) {
|
||||
// Re-stamp the SAME geometry the buffer was last fully painted with. The pixels
|
||||
// outside the damage box are that frame's and are still current, which is the
|
||||
@@ -3011,8 +3171,8 @@ void BrowserPageGoanna::paintToSharedBuffer() {
|
||||
oi->bufferWidth = w; oi->bufferHeight = bf.paintedH;
|
||||
oi->contentZoom = Zeff;
|
||||
oi->renderedX = bf.paintedX; oi->renderedY = bf.paintedTop;
|
||||
oi->renderedWidth = w; oi->renderedHeight = bf.paintedH;
|
||||
mPaintedX = bf.paintedX; mPaintedLo = bf.paintedTop;
|
||||
oi->renderedWidth = (int)pw; oi->renderedHeight = bf.paintedH;
|
||||
mPaintedX = bf.paintedX; mPaintedW = (int)pw; mPaintedLo = bf.paintedTop;
|
||||
mPaintedHi = (long)bf.paintedTop + bf.paintedH; mPaintedZoom = Zeff;
|
||||
mLastPaintDoneMs = jihadNowMs();
|
||||
gJihadPaintDone++;
|
||||
@@ -3027,11 +3187,12 @@ void BrowserPageGoanna::paintToSharedBuffer() {
|
||||
int64_t nowP = jihadNowMs();
|
||||
if (nowP - sLastP >= 2000) {
|
||||
sLastP = nowP;
|
||||
fprintf(stderr, "[jihad-bs] partial paint %ldx%ld at %ld,%ld in %dms\n",
|
||||
dW, dH, dLeft, dTop, (int)(nowP - pt0));
|
||||
fprintf(stderr, "[jihad-bs] %s paint %ldx%ld at %ld,%ld in %dms\n",
|
||||
bandOnly ? "band-only" : "partial", dW, dH, dLeft, dTop, (int)(nowP - pt0));
|
||||
fflush(stderr);
|
||||
}
|
||||
}
|
||||
mLatestSlot = slot;
|
||||
mSink.msgPainted(mActiveKey);
|
||||
{
|
||||
int ps = slotForKey(mActiveKey);
|
||||
@@ -3044,6 +3205,15 @@ void BrowserPageGoanna::paintToSharedBuffer() {
|
||||
}
|
||||
}
|
||||
}
|
||||
// Anything known to have changed on screen since the last frame? A maintenance paint
|
||||
// (maybePaint: growing the region, or sweeping its off-screen rows) is one that was asked
|
||||
// for when nothing had.
|
||||
const bool hadDamage = mDamageValid || mDamageForceFull;
|
||||
const int maintKind = hadDamage ? 0 : maintenanceKind;
|
||||
// A change the engine could put a small box round (a caret, a spinner) is not a reason to
|
||||
// think the rest of the page moved; anything bigger, or of unknown extent, is.
|
||||
const bool smallDamage = mDamageValid && !mDamageForceFull &&
|
||||
(double)mDamageW * (double)mDamageH < 0.6 * (double)w * (double)h;
|
||||
mDamageValid = false; mDamageForceFull = false;
|
||||
|
||||
long bandX = mAdapterScrollX, bandY = mAdapterScrollY;
|
||||
@@ -3063,6 +3233,33 @@ void BrowserPageGoanna::paintToSharedBuffer() {
|
||||
// (When zoomed, bandX is the clamped pan — the render x — which is exact.)
|
||||
if (!zoomed) bandX = mAdapterScrollX;
|
||||
|
||||
// Columns [loX, loX+regW). One viewport wide unless ZOOMED IN on content wider than the
|
||||
// screen — the one case where there is anything to the left or right. There the region
|
||||
// was one viewport wide too, so every sideways pan exposed an undrawn column and waited
|
||||
// for a repaint (owner, 2026-10-05: "if I scroll … left, or right, the page is grey").
|
||||
// Now it is the content's width, up to two viewports, centred on the view; the rows it can
|
||||
// have shrink in proportion, since the segment is what it is. The adapter is told the
|
||||
// viewport width in bufferWidth and the region's in renderedWidth (its rotation guard
|
||||
// compares the former; an adapter older than 2026-10-05 compares the latter and holds the
|
||||
// frame, so the two ship together).
|
||||
long regW = w, loX = bandX;
|
||||
if (zoomed) {
|
||||
const long contentCols = (mLastContentW > 0) ? (long)((double)mLastContentW * Zeff) : 0;
|
||||
if (contentCols > (long)w) {
|
||||
regW = contentCols < 2L * w ? contentCols : 2L * w;
|
||||
const long adX = (long)mAdapterScrollX;
|
||||
loX = bandX - (regW - (long)w) / 2;
|
||||
if (adX < loX) loX = adX; // the adapter's own view…
|
||||
if (adX + (long)w > loX + regW) loX = adX + (long)w - regW; // …stays inside
|
||||
if (loX + regW > contentCols) loX = contentCols - regW;
|
||||
if (loX < 0) loX = 0;
|
||||
if (bandX < loX || bandX + (long)w > loX + regW) { regW = w; loX = bandX; }
|
||||
const long rowsFor = (long)((segSize - hdr) / ((size_t)regW * 4));
|
||||
if (rowsFor < (long)h) { regW = w; loX = bandX; } // no room: one viewport wide
|
||||
else if (rowsFor < maxRows) maxRows = rowsFor;
|
||||
}
|
||||
}
|
||||
|
||||
// Painted rows [lo, hi): cover the band AND the adapter's pan target, plus overscan
|
||||
// (half a viewport above, one below — scrolling down dominates), bounded by the segment
|
||||
// and the content height (mLastContentH is css px; unknown 0 = don't bound).
|
||||
@@ -3083,8 +3280,9 @@ void BrowserPageGoanna::paintToSharedBuffer() {
|
||||
// - a longer page gets the full budget around the viewport, most of it ahead of the pan.
|
||||
long slackRows = maxRows - (long)h; if (slackRows < 0) slackRows = 0;
|
||||
long ovAbove = slackRows / 3, ovBelow = slackRows - slackRows / 3;
|
||||
if (adY > mPrevPaintScrollY + 8) { ovAbove = slackRows / 6; ovBelow = slackRows - ovAbove; }
|
||||
else if (adY < mPrevPaintScrollY - 8) { ovBelow = slackRows / 6; ovAbove = slackRows - ovBelow; }
|
||||
const int panDir = (adY > mPrevPaintScrollY + 8) ? 1 : (adY < mPrevPaintScrollY - 8) ? -1 : 0;
|
||||
if (panDir > 0) { ovAbove = slackRows / 6; ovBelow = slackRows - ovAbove; }
|
||||
else if (panDir < 0) { ovBelow = slackRows / 6; ovAbove = slackRows - ovBelow; }
|
||||
mPrevPaintScrollY = adY;
|
||||
long lo = (bandY < adY ? bandY : adY) - ovAbove;
|
||||
long hi = (bandY > adY ? bandY : adY) + h + ovBelow;
|
||||
@@ -3111,6 +3309,80 @@ void BrowserPageGoanna::paintToSharedBuffer() {
|
||||
if (contentRows > 0 && hi > contentRows) hi = contentRows;
|
||||
if (hi < lo + h) hi = lo + h;
|
||||
}
|
||||
// ── BUILD ON THE FRAME ALREADY PAINTED (2026-10-05) ─────────────────────────────────────
|
||||
//
|
||||
// [lo, hi) above is the region this page SHOULD have: all of it, or as many rows as the
|
||||
// segment holds. Rendering that whole region for every frame is what the paint used to
|
||||
// do, and it does not survive the region getting big: 4096 rows of a real page measured
|
||||
// 300-1400 ms on this hardware, with the daemon deaf to input for the duration, to redraw
|
||||
// rows that had not changed. So a frame is now COMPOSED:
|
||||
//
|
||||
// - rows the most recent frame already holds (same document, zoom, width and columns)
|
||||
// are COPIED from its buffer — a memcpy of a few MB, tens of ms at worst;
|
||||
// - rows it does not hold are rendered, at most kGrowRows of them per frame, so the
|
||||
// region reaches its full size over a few frames instead of in one long one
|
||||
// (mGrowOwed asks maybePaint for the next step);
|
||||
// - the visible band is drawn as it always was (viewport-relative at z~1);
|
||||
// - at z~1 the band of the frame copied FROM carries viewport-relative content (a fixed
|
||||
// header, the caret) that is wrong anywhere but under the viewport, so where that band
|
||||
// is no longer this one's it is redrawn from the document.
|
||||
//
|
||||
// What a copy cannot know is whether the page changed somewhere nobody was looking — the
|
||||
// engine reports damage for the viewport only. So after any frame that reacted to a change
|
||||
// the off-screen rows are owed a SWEEP (mSweepLeft): redrawn kSweepRows at a time, by
|
||||
// maintenance frames, once the page has been still for a moment.
|
||||
const long kInitialRows = (long)h * 5 / 2; // with nothing to build on: what a first frame is worth
|
||||
const long kGrowRows = 768;
|
||||
const long kSweepRows = mSweepStep; // adapts to what a step costs on this page
|
||||
const int xs = slotForKey(mActiveKey);
|
||||
const uint64_t docId = mPage->CurrentDocId();
|
||||
const BufFrame* src = nullptr; const unsigned char* srcPix = nullptr;
|
||||
if (bandKnown && docId != 0 && mLatestSlot >= 0 && mLatestSlot != xs) {
|
||||
const BufFrame& y = mBufFull[mLatestSlot];
|
||||
const int ykey = mLatestSlot == 0 ? mKey1 : (mLatestSlot == 1 ? mKey2 : mKey3);
|
||||
if (ykey && y.valid && !y.overlay && y.docId == docId && y.w == w && y.h == h &&
|
||||
y.zeff == Zeff && y.paintedW == (int)regW && y.paintedX == (int)loX && y.paintedH > 0) {
|
||||
if (unsigned char* yb = attachShm(ykey)) { src = &y; srcPix = yb + hdr; }
|
||||
}
|
||||
}
|
||||
bool growOwed = false;
|
||||
{
|
||||
const long mustLo = (bandY < adY ? bandY : adY); // the band and the adapter's
|
||||
const long mustHi = (bandY > adY ? bandY : adY) + h; // own viewport: never optional
|
||||
long cLo = lo, cHi = hi;
|
||||
if (src) {
|
||||
const long sLo = src->paintedTop, sHi = sLo + src->paintedH;
|
||||
const long needAbove = sLo > mustLo ? sLo - mustLo : 0;
|
||||
const long needBelow = mustHi > sHi ? mustHi - sHi : 0;
|
||||
// The viewport has left what that frame holds by more than a couple of steps (a jump
|
||||
// to an anchor, a long fling): bridging the gap would mean rendering all of it.
|
||||
if (needAbove + needBelow > 2 * kGrowRows || sHi <= lo || sLo >= hi) src = nullptr;
|
||||
}
|
||||
if (src) {
|
||||
const long sLo = src->paintedTop, sHi = sLo + src->paintedH;
|
||||
long newAbove = sLo > lo ? sLo - lo : 0, newBelow = hi > sHi ? hi - sHi : 0;
|
||||
long allowAbove, allowBelow;
|
||||
if (panDir < 0) {
|
||||
allowAbove = newAbove < kGrowRows ? newAbove : kGrowRows;
|
||||
allowBelow = newBelow < kGrowRows - allowAbove ? newBelow : kGrowRows - allowAbove;
|
||||
} else {
|
||||
allowBelow = newBelow < kGrowRows ? newBelow : kGrowRows;
|
||||
allowAbove = newAbove < kGrowRows - allowBelow ? newAbove : kGrowRows - allowBelow;
|
||||
}
|
||||
if (sLo - allowAbove > cLo) cLo = sLo - allowAbove;
|
||||
if (sHi + allowBelow < cHi) cHi = sHi + allowBelow;
|
||||
} else if (hi - lo > kInitialRows) {
|
||||
cLo = mustLo - (kInitialRows - (mustHi - mustLo)) / 3;
|
||||
if (cLo < lo) cLo = lo;
|
||||
cHi = cLo + kInitialRows;
|
||||
if (cHi > hi) { cHi = hi; cLo = cHi - kInitialRows; if (cLo < lo) cLo = lo; }
|
||||
}
|
||||
if (mustLo < cLo) cLo = mustLo < lo ? lo : mustLo;
|
||||
if (mustHi > cHi) cHi = mustHi > hi ? hi : mustHi;
|
||||
if (cHi < cLo + h) { cHi = cLo + h; if (cHi > hi) { cHi = hi; cLo = cHi - h; } }
|
||||
growOwed = (cLo > lo || cHi < hi);
|
||||
lo = cLo; hi = cHi;
|
||||
}
|
||||
int tallH = (int)(hi - lo);
|
||||
if (!bandKnown) tallH = h; // F9: unknown band → viewport-exact fallback
|
||||
|
||||
@@ -3118,65 +3390,119 @@ void BrowserPageGoanna::paintToSharedBuffer() {
|
||||
// NO fling-mode fast path (tried 2026-08-02, removed the same day): skipping the band
|
||||
// overlay during flings painted position:fixed content at its DOCUMENT position in
|
||||
// those frames while full frames painted it at the viewport — two models alternating,
|
||||
// so a fixed banner "flew around" mid-scroll (user report). A full region+band paint
|
||||
// measured 66 ms on device, cheap enough to be every frame's model: fixed content bakes
|
||||
// at the band position each frame, swims with the pan between paints, snaps per paint —
|
||||
// the classic webOS-era behavior.
|
||||
// so a fixed banner "flew around" mid-scroll (user report). Fixed content bakes at the
|
||||
// band position each frame, swims with the pan between paints, snaps per paint — the
|
||||
// classic webOS-era behavior.
|
||||
long nb = -1;
|
||||
int paintedX = (int)bandX, paintedTop = (int)bandY, paintedH = h;
|
||||
int paintedW = w; // the buffer's row stride in px
|
||||
long bandCol = 0; // where the viewport starts in each row
|
||||
bool region = false;
|
||||
bool fresh = false; // every row of the region was rendered now
|
||||
bool stripsChanged = false; // rows outside the band differ from the source's
|
||||
long rowsRendered = 0, rowsCopied = 0;
|
||||
long bandRow = bandY - lo;
|
||||
if (bandRow < 0) bandRow = 0;
|
||||
if (bandRow > (long)tallH - h) bandRow = (long)tallH - h;
|
||||
if (tallH > h) {
|
||||
// Absolute document rows straight into the shared buffer (not bounded by the widget
|
||||
// DrawTarget). docX/docY are css px = zoomed px / Zeff.
|
||||
if (!zoomed) {
|
||||
// Render ONLY the strips doc-relative — the band rows come from the viewport-relative
|
||||
// overlay below, so rendering them here too was pure duplicate work (~30% of the
|
||||
// frame: 2048-row frame = 2990 rows rendered, now 2048 — "trim the paint cost",
|
||||
// device 2026-08-02, measured 185 ms before this change at the region cap).
|
||||
bool okAbove = true, okBelow = true;
|
||||
if (bandRow > 0) {
|
||||
okAbove = mPage->RenderRegion(pixels, w * 4, w, (int)bandRow,
|
||||
(double)bandX / Zeff, (double)lo / Zeff, Zeff);
|
||||
}
|
||||
long belowRows = (long)tallH - h - bandRow;
|
||||
if (belowRows > 0) {
|
||||
okBelow = mPage->RenderRegion(pixels + (size_t)(bandRow + h) * w * 4, w * 4,
|
||||
w, (int)belowRows, (double)bandX / Zeff,
|
||||
(double)(lo + bandRow + h) / Zeff, Zeff);
|
||||
}
|
||||
region = okAbove && okBelow;
|
||||
} else {
|
||||
// Zoomed: single-pass region (there is no separate band pass at zoom).
|
||||
region = mPage->RenderRegion(pixels, w * 4, w, tallH,
|
||||
(double)bandX / Zeff, (double)lo / Zeff, Zeff);
|
||||
if (tallH > h || regW > (long)w) {
|
||||
const size_t stride = (size_t)regW * 4;
|
||||
const double dX = (double)loX / Zeff; // css px; loX == bandX unless zoomed and wide
|
||||
const long bandLo = bandRow, bandHi = bandRow + h;
|
||||
// Region rows [r0, r1), straight from the document (docY css px = zoomed px / Zeff).
|
||||
auto rows = [&](long r0, long r1) -> bool {
|
||||
if (r0 < 0) r0 = 0;
|
||||
if (r1 > (long)tallH) r1 = tallH;
|
||||
if (r1 <= r0) return true;
|
||||
rowsRendered += r1 - r0;
|
||||
return mPage->RenderRegion(pixels + (size_t)r0 * stride, (int)stride, (int)regW,
|
||||
(int)(r1 - r0), dX, (double)(lo + r0) / Zeff, Zeff);
|
||||
};
|
||||
// The same, less the visible band: at z~1 those rows come from the viewport-relative
|
||||
// overlay below, and rendering them here too is pure duplicate work.
|
||||
auto strips = [&](long r0, long r1) -> bool {
|
||||
if (zoomed) return rows(r0, r1);
|
||||
return rows(r0, r1 < bandLo ? r1 : bandLo) && rows(r0 > bandHi ? r0 : bandHi, r1);
|
||||
};
|
||||
bool ok = true;
|
||||
long a = 0, b = 0; // document rows [a, b) the source can give
|
||||
if (src) {
|
||||
const long sLo = src->paintedTop, sHi = sLo + src->paintedH;
|
||||
a = lo > sLo ? lo : sLo; b = hi < sHi ? hi : sHi;
|
||||
if (b <= a) src = nullptr;
|
||||
}
|
||||
if (!src) {
|
||||
fresh = true;
|
||||
ok = strips(0, tallH);
|
||||
} else {
|
||||
const long sLo = src->paintedTop;
|
||||
memcpy(pixels + (size_t)(a - lo) * stride, srcPix + (size_t)(a - sLo) * stride,
|
||||
(size_t)(b - a) * stride);
|
||||
rowsCopied = b - a;
|
||||
const long before = rowsRendered;
|
||||
ok = strips(0, a - lo) && strips(b - lo, tallH); // rows it did not have
|
||||
if (ok && !zoomed) {
|
||||
// Its band, where that is not this frame's band (see the header comment).
|
||||
const long ob0 = (sLo + src->bandRow) - lo, ob1 = ob0 + h;
|
||||
if (ob0 != bandLo) {
|
||||
long c0 = ob0 > a - lo ? ob0 : a - lo, c1 = ob1 < b - lo ? ob1 : b - lo;
|
||||
if (c1 > c0) ok = strips(c0, c1);
|
||||
}
|
||||
}
|
||||
if (ok && maintKind == 2) {
|
||||
// One step of the sweep.
|
||||
long s0 = mSweepRow; if (s0 >= (long)tallH || s0 < 0) s0 = 0;
|
||||
long s1 = s0 + kSweepRows; if (s1 > (long)tallH) s1 = tallH;
|
||||
ok = strips(s0, s1);
|
||||
mSweepRow = (s1 >= (long)tallH) ? 0 : s1;
|
||||
mSweepLeft -= (s1 - s0);
|
||||
if (mSweepLeft < 0) mSweepLeft = 0;
|
||||
}
|
||||
stripsChanged = (rowsRendered != before);
|
||||
}
|
||||
region = ok;
|
||||
if (region) {
|
||||
paintedTop = (int)lo; paintedH = tallH;
|
||||
paintedX = (int)loX; paintedW = (int)regW; bandCol = bandX - loX;
|
||||
}
|
||||
}
|
||||
// The band a maintenance frame copied is still the screen: nothing has changed on it.
|
||||
const bool bandSame = region && src && !fresh && maintKind != 0 &&
|
||||
(long)src->paintedTop + src->bandRow == lo + bandRow &&
|
||||
(long)src->paintedX + src->bandCol == loX + bandCol &&
|
||||
src->paintedTop <= lo + bandRow &&
|
||||
(long)src->paintedTop + src->paintedH >= lo + bandRow + h;
|
||||
if (region && zoomed && !fresh && !bandSame) {
|
||||
// Zoomed: the copied rows under the viewport may predate what is on screen now.
|
||||
rowsRendered += h;
|
||||
if (!mPage->RenderRegion(pixels + ((size_t)bandRow * paintedW + bandCol) * 4, paintedW * 4,
|
||||
w, h, (double)bandX / Zeff, (double)(lo + bandRow) / Zeff, Zeff)) {
|
||||
region = false; paintedX = (int)bandX; paintedW = w; bandCol = 0;
|
||||
}
|
||||
if (region) { paintedTop = (int)lo; paintedH = tallH; }
|
||||
}
|
||||
if (region && !zoomed) {
|
||||
// Overlay the viewport-relative band over the visible rows so position:fixed/sticky
|
||||
// and the caret stay correct on screen; the doc-relative strips around it only show
|
||||
// transiently mid-pan. ReadPixels renders v2 (viewport-relative at z~1) + readback,
|
||||
// forces alpha and returns the nonblank count — same "did it render" signal as before.
|
||||
nb = mPage->ReadPixels(pixels + (size_t)bandRow * w * 4, (size_t)w * h * 4);
|
||||
if (bandSame) nb = 1;
|
||||
else nb = mPage->ReadPixels(pixels + (size_t)bandRow * w * 4, (size_t)w * h * 4);
|
||||
} else if (region) {
|
||||
// Zoomed: the region render IS the frame (pure visual viewport, same semantics as the
|
||||
// old single-pass zoomed paint; fixed content already doc-positioned when zoomed).
|
||||
// Count nonblank over the visible band rows only — same signal, 1/tallH the cost.
|
||||
// Zoomed: the region render IS the frame (pure visual viewport; fixed content already
|
||||
// doc-positioned when zoomed). Count nonblank over the visible band only.
|
||||
nb = 0;
|
||||
for (long y = bandRow; y < bandRow + h; ++y) {
|
||||
unsigned char* row = pixels + (size_t)y * w * 4;
|
||||
for (long y = bandRow; y < bandRow + h && !bandSame; ++y) {
|
||||
unsigned char* row = pixels + ((size_t)y * paintedW + bandCol) * 4;
|
||||
for (int x = 0; x < w; ++x) {
|
||||
unsigned char* p = row + (size_t)x * 4;
|
||||
if (!(p[0] > 240 && p[1] > 240 && p[2] > 240)) ++nb;
|
||||
}
|
||||
}
|
||||
if (bandSame) nb = 1;
|
||||
} else {
|
||||
// Region render unavailable (old libxul: weak symbol absent / render failed / no room
|
||||
// for overscan): the previous viewport-exact paint, stamped at the band's true position.
|
||||
paintedTop = (int)bandY; paintedH = h;
|
||||
fresh = true; growOwed = false;
|
||||
nb = mPage->ReadPixels(pixels, segSize - hdr);
|
||||
}
|
||||
|
||||
@@ -3185,10 +3511,12 @@ void BrowserPageGoanna::paintToSharedBuffer() {
|
||||
// painted, at the same origin/zoom. Costs one popup-manager query when none is open,
|
||||
// which is the normal case. Must come AFTER the band overlay: that overlay rewrites
|
||||
// the visible rows and would erase a popup drawn before it.
|
||||
int popupsDrawn = 0;
|
||||
if (nb >= 0) {
|
||||
int popups = mPage->CompositePopups(pixels, w * 4, w, paintedH,
|
||||
int popups = mPage->CompositePopups(pixels, paintedW * 4, paintedW, paintedH,
|
||||
(double)paintedX / Zeff, (double)paintedTop / Zeff,
|
||||
Zeff);
|
||||
popupsDrawn = popups;
|
||||
if (popups > 0) {
|
||||
fprintf(stderr, "[jihad-bs] composited %d popup(s) over the frame\n", popups);
|
||||
// A popup is content: an otherwise-blank frame carrying one must not be
|
||||
@@ -3197,12 +3525,13 @@ void BrowserPageGoanna::paintToSharedBuffer() {
|
||||
}
|
||||
}
|
||||
|
||||
oi->bufferWidth = w; oi->bufferHeight = paintedH;
|
||||
oi->bufferWidth = w; oi->bufferHeight = paintedH; // bufferWidth: the VIEWPORT's width
|
||||
oi->contentZoom = Zeff;
|
||||
oi->renderedX = paintedX; oi->renderedY = paintedTop;
|
||||
oi->renderedWidth = w; oi->renderedHeight = paintedH;
|
||||
oi->renderedWidth = paintedW; oi->renderedHeight = paintedH;
|
||||
// Record the painted region for the coverage-aware repaint skip (setScrollPosition).
|
||||
mPaintedX = paintedX; mPaintedLo = paintedTop; mPaintedHi = (long)paintedTop + paintedH;
|
||||
mPaintedX = paintedX; mPaintedW = paintedW;
|
||||
mPaintedLo = paintedTop; mPaintedHi = (long)paintedTop + paintedH;
|
||||
mPaintedZoom = Zeff;
|
||||
// NB: what this buffer now holds is recorded LOWER DOWN, past the blank-over-good
|
||||
// suppression — see the comment there. Recording it here marked a frame that is about to
|
||||
@@ -3213,9 +3542,11 @@ void BrowserPageGoanna::paintToSharedBuffer() {
|
||||
gJihadPaintDone++;
|
||||
JihadRecordFrameGap(mLastPaintDoneMs);
|
||||
if (nb >= 0) {
|
||||
fprintf(stderr, "[jihad-bs] painted shmid=0x%x bytes=%ld (%dx%d top=%d band=%ld) mZoom=%.4f contentZoom=%.4f ms=%ld\n",
|
||||
(unsigned)mActiveKey, nb, w, paintedH, paintedTop, bandY, mZoom, oi->contentZoom,
|
||||
(long)(jihadNowMs() - paintT0));
|
||||
fprintf(stderr, "[jihad-bs] painted shmid=0x%x bytes=%ld (%dx%d top=%d band=%ld left=%d) mZoom=%.4f contentZoom=%.4f ms=%ld"
|
||||
" rows new=%ld copied=%ld%s\n",
|
||||
(unsigned)mActiveKey, nb, paintedW, paintedH, paintedTop, bandY, paintedX, mZoom,
|
||||
oi->contentZoom, (long)(jihadNowMs() - paintT0), rowsRendered, rowsCopied,
|
||||
maintKind == 1 ? " (grow)" : maintKind == 2 ? " (sweep)" : "");
|
||||
// Debug: dump each NON-EMPTY painted frame to a PPM so we can see exactly what
|
||||
// the engine rendered (text vs blank) independent of the adapter's blit. The
|
||||
// first paint after connect is empty (nb==0, blank buffer); guarding on nb>0
|
||||
@@ -3233,22 +3564,66 @@ void BrowserPageGoanna::paintToSharedBuffer() {
|
||||
const long tr = region ? bandRow : 0;
|
||||
long rows = (long)paintedH - tr; if (rows > h) rows = h;
|
||||
const std::string out = mThumbPath; mThumbPath.clear();
|
||||
const bool ok = rows > 0 && writeThumbnail(pixels + (size_t)tr * w * 4, w, (int)rows, out);
|
||||
bool ok = false;
|
||||
if (rows > 0 && paintedW == w) {
|
||||
ok = writeThumbnail(pixels + (size_t)tr * w * 4, w, (int)rows, out);
|
||||
} else if (rows > 0) {
|
||||
// A region wider than the viewport: the thumbnail writer reads w-wide rows, so give
|
||||
// it the visible columns on their own.
|
||||
std::vector<unsigned char> band((size_t)w * rows * 4);
|
||||
for (long y = 0; y < rows; ++y)
|
||||
memcpy(&band[(size_t)y * w * 4], pixels + ((size_t)(tr + y) * paintedW + bandCol) * 4, (size_t)w * 4);
|
||||
ok = writeThumbnail(band.data(), w, (int)rows, out);
|
||||
}
|
||||
fprintf(stderr, "[jihad-bs] thumbnail %s -> %s\n", ok ? "written" : "FAILED", out.c_str());
|
||||
}
|
||||
static const std::string dumpPath = jihad::RuntimeResolvePath(getenv("JIHAD_DUMP"), "frame.ppm");
|
||||
if (!dumpPath.empty() && nb > 0) {
|
||||
FILE* f = fopen(dumpPath.c_str(), "wb");
|
||||
// ...or ONE frame, on request: create <state>/dump-request and the next non-empty frame
|
||||
// is written to /tmp (RAM — a 4096-row frame is 9 MB and /var is a 62 MB partition) and
|
||||
// the request removed. This is how a composed frame's OFF-SCREEN rows are looked at: no
|
||||
// screenshot shows them, and the environment variable means editing a generated job on
|
||||
// the root filesystem. The request lives in the state directory, which only root writes.
|
||||
static const std::string dumpReq = jihad::RuntimeStateDir().empty()
|
||||
? std::string() : jihad::RuntimeStateDir() + "/dump-request";
|
||||
bool dumpOnce = nb > 0 && !dumpReq.empty() && access(dumpReq.c_str(), F_OK) == 0;
|
||||
if (dumpOnce) {
|
||||
// /tmp is a 40 MB RAM disk the whole system uses (it was filled once by dumps left
|
||||
// lying there, and nothing on the device could write a temporary file until they
|
||||
// were removed). Replace the previous dump, and refuse unless what is free afterwards
|
||||
// is twice what this one needs.
|
||||
unlink(dumpReq.c_str());
|
||||
unlink("/tmp/jihad-frame.ppm");
|
||||
struct statvfs vfs;
|
||||
const unsigned long long need = (unsigned long long)paintedW * paintedH * 3 + 32;
|
||||
if (statvfs("/tmp", &vfs) != 0 || (unsigned long long)vfs.f_bavail * vfs.f_frsize < 2 * need) {
|
||||
fprintf(stderr, "[jihad-bs] frame dump refused: /tmp has too little room for %llu bytes\n", need);
|
||||
dumpOnce = false;
|
||||
}
|
||||
}
|
||||
const std::string outPath = dumpOnce ? std::string("/tmp/jihad-frame.ppm") : dumpPath;
|
||||
if (!outPath.empty() && nb > 0) {
|
||||
int dfd = open(outPath.c_str(), O_WRONLY | O_CREAT | O_TRUNC | O_NOFOLLOW, 0644);
|
||||
FILE* f = dfd >= 0 ? fdopen(dfd, "wb") : nullptr;
|
||||
if (!f && dfd >= 0) close(dfd);
|
||||
if (f) {
|
||||
fprintf(f, "P6\n%d %d\n255\n", w, paintedH); // buffer is BGRA -> write RGB
|
||||
for (long i = 0, px = (long)w * paintedH; i < px; i++) {
|
||||
unsigned char* p = pixels + i * 4; fputc(p[2], f); fputc(p[1], f); fputc(p[0], f);
|
||||
fprintf(f, "P6\n%d %d\n255\n", paintedW, paintedH); // buffer is BGRA -> write RGB
|
||||
std::vector<unsigned char> line((size_t)paintedW * 3);
|
||||
for (long y = 0; y < (long)paintedH; ++y) {
|
||||
const unsigned char* p = pixels + (size_t)y * paintedW * 4;
|
||||
for (int x = 0; x < paintedW; ++x) {
|
||||
line[(size_t)x * 3] = p[(size_t)x * 4 + 2];
|
||||
line[(size_t)x * 3 + 1] = p[(size_t)x * 4 + 1];
|
||||
line[(size_t)x * 3 + 2] = p[(size_t)x * 4];
|
||||
}
|
||||
fwrite(line.data(), 1, line.size(), f);
|
||||
}
|
||||
fclose(f);
|
||||
// Pin 0644 rather than inherit the daemon's umask (T-057 (d): root-owned
|
||||
// 0755 dirs / 0644 files — never a world-writable artifact).
|
||||
chmod(dumpPath.c_str(), 0644);
|
||||
fprintf(stderr, "[jihad-bs] dumped frame -> %s\n", dumpPath.c_str());
|
||||
chmod(outPath.c_str(), 0644);
|
||||
fprintf(stderr, "[jihad-bs] dumped frame -> %s (%dx%d top=%d left=%d bandRow=%ld bandCol=%ld)\n",
|
||||
outPath.c_str(), paintedW, paintedH, paintedTop, paintedX, region ? bandRow : 0L,
|
||||
region ? bandCol : 0L);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3268,8 +3643,8 @@ void BrowserPageGoanna::paintToSharedBuffer() {
|
||||
// sample the strips (every 8th row, every 4th px) and publish if anything is there.
|
||||
for (long y = 0; y < (long)paintedH && nb == 0; y += 8) {
|
||||
if (y >= bandRow && y < bandRow + h) continue; // the band was already counted
|
||||
unsigned char* row = pixels + (size_t)y * w * 4;
|
||||
for (int x = 0; x < w; x += 4) {
|
||||
unsigned char* row = pixels + (size_t)y * paintedW * 4;
|
||||
for (int x = 0; x < paintedW; x += 4) {
|
||||
unsigned char* p = row + (size_t)x * 4;
|
||||
if (!(p[0] > 240 && p[1] > 240 && p[2] > 240)) { nb = 1; break; }
|
||||
}
|
||||
@@ -3330,6 +3705,32 @@ void BrowserPageGoanna::paintToSharedBuffer() {
|
||||
mBufFull[fs].scrollX = mAdapterScrollX; mBufFull[fs].scrollY = mAdapterScrollY;
|
||||
mBufFull[fs].paintedX = paintedX; mBufFull[fs].paintedTop = paintedTop;
|
||||
mBufFull[fs].paintedH = paintedH; mBufFull[fs].bandRow = region ? bandRow : 0;
|
||||
mBufFull[fs].paintedW = paintedW; mBufFull[fs].bandCol = region ? bandCol : 0;
|
||||
mBufFull[fs].docId = docId;
|
||||
// A popup drawn over this frame is in its pixels, band and strips alike, and a later
|
||||
// frame must not copy it: such a frame is nobody's source.
|
||||
mBufFull[fs].overlay = popupsDrawn > 0;
|
||||
// Off-screen rows identical to the source's keep its generation, so three buffers
|
||||
// that were copied one from another all count as current (the fast path's test).
|
||||
mBufFull[fs].stripGen = (src && !fresh && !stripsChanged) ? src->stripGen : ++mStripGen;
|
||||
mLatestSlot = (nb > 0) ? fs : -1;
|
||||
// Another step is owed only if this frame can be built on — otherwise each "step"
|
||||
// would be a first frame again, for ever.
|
||||
mGrowOwed = growOwed && nb > 0 && docId != 0 && !mBufFull[fs].overlay &&
|
||||
!(maintKind == 1 && rowsCopied == 0);
|
||||
if (fresh) { mSweepLeft = 0; mSweepRow = 0; } // nothing here is old
|
||||
// Owed a sweep: a frame that reacted to a change of unknown or large extent. Not one
|
||||
// that was painted only because the viewport moved — nothing changed for that.
|
||||
else if (maintKind == 0 && hadDamage && !smallDamage) mSweepLeft = paintedH;
|
||||
if (maintKind == 2) {
|
||||
// Keep a sweep step short: the daemon hears nothing while it renders. And once a
|
||||
// sweep has gone all the way round, leave the page alone for a while — one that
|
||||
// changes every few seconds must not be swept continuously.
|
||||
const long took = (long)(jihadNowMs() - paintT0);
|
||||
if (took > 200 && mSweepStep > 128) mSweepStep /= 2;
|
||||
else if (took < 70 && mSweepStep < 1024) mSweepStep *= 2;
|
||||
if (mSweepLeft <= 0) mSweepNotBeforeMs = jihadNowMs() + 8000;
|
||||
}
|
||||
// A full paint repainted the whole band: this buffer is completely current, so it has no
|
||||
// pending per-buffer damage. (dmgValid is cleared even when valid=false — a blank buffer
|
||||
// has nothing to catch up either.)
|
||||
|
||||
@@ -106,6 +106,11 @@ public:
|
||||
virtual void msgEditorFocused(bool focused, int fieldType, int fieldActions) {
|
||||
(void)focused; (void)fieldType; (void)fieldActions;
|
||||
}
|
||||
// The answer to the adapter's copy(queryNum) (YAP 0x2032): whether selected text was
|
||||
// handed to the card. Sent once the queued command has actually run.
|
||||
virtual void msgCopySuccessResponse(int queryNum, bool success) {
|
||||
(void)queryNum; (void)success;
|
||||
}
|
||||
// A navigation resolved to a non-displayable resource (download / MIME handoff).
|
||||
// Drives BrowserServerBase::msgMimeHandoffUrl (YAP 0x2015) -> adapter
|
||||
// mimeHandoffUrl -> BasicWebView.doFileLoad -> app handleResource, which routes
|
||||
@@ -230,6 +235,8 @@ public:
|
||||
void requestThumbnail(const std::string& path);
|
||||
// Hand this page to a bundled JS component (see GoannaRenderPage::NotifyChrome).
|
||||
bool notifyChrome(const char* topic, const std::string& data) { return mPage && mPage->NotifyChrome(topic, data.c_str()); }
|
||||
// The loaded page's certificate and connection, as JSON for the card (the padlock).
|
||||
bool pageCertificate(std::string* json) { return mPage && mPage->GetPageCertInfoJson(json); }
|
||||
// YAP: hitTest — resolve what sits at CONTENT (x,y) into the isis HitTest.schema JSON.
|
||||
// The adapter GATES the long-press on this round-trip; see GoannaRenderPage::HitTestAt.
|
||||
void hitTest(int x, int y, std::string* json);
|
||||
@@ -252,8 +259,11 @@ public:
|
||||
void insertStringAtCursor(const char* text); // YAP: insertStringAtCursor
|
||||
// YAP: selectAll / copy / cut — the long-press menu's page actions. Done by the bundled
|
||||
// component components/jihadPageCommands.js (selection and editing are a few lines of
|
||||
// script there and pages of C++ here); this hands it the page and the command.
|
||||
void pageCommand(const char* cmd, const std::string& extraJson = std::string());
|
||||
// script there and pages of C++ here). QUEUED, like every other input command: two of
|
||||
// them can run page script (an `input` event, a selection listener), which must not
|
||||
// happen inside the socket callback. queryNum >= 0 is a copy whose result the adapter is
|
||||
// waiting for.
|
||||
void pageCommand(const char* cmd, int queryNum = -1);
|
||||
void dragStart(int x, int y); // YAP: dragStart
|
||||
void dragProcess(int deltaX, int deltaY); // YAP: dragProcess (scroll)
|
||||
void dragEnd(int x, int y); // YAP: dragEnd
|
||||
@@ -375,6 +385,10 @@ private:
|
||||
// could tear the page down under us (Codex F-219). A QUEUE, not a single slot, so several presses
|
||||
// between pump ticks aren't dropped (Codex F-241); see the PEA_* constants in the .cpp.
|
||||
std::vector<int> mPendingEditActions;
|
||||
struct PendingPageCmd { std::string cmd; int queryNum; unsigned navGen; };
|
||||
std::vector<PendingPageCmd> mPendingPageCmds; // drained in the guarded pump
|
||||
// Hand one command to the component NOW (pump only). True if it handed text to the card.
|
||||
bool runPageCommand(const char* cmd, const std::string& extraJson = std::string());
|
||||
// Queued raw mouse events (F-9). mouseEvent()/holdAt() are YAP socket callbacks with no
|
||||
// page-lifetime protection, exactly like clickAt: a mousedown/mouseup/contextmenu runs page JS
|
||||
// (onmousedown -> location/document.open) that can tear the document down mid-callback, after
|
||||
@@ -428,6 +442,18 @@ private:
|
||||
// Last painted region (zoomed px + its zoom) — coverage-aware repaint: a scroll whose
|
||||
// viewport is still well inside this region needs NO repaint (the adapter pans locally).
|
||||
int mPaintedX = 0;
|
||||
int mPaintedW = 0; // width of the painted region (>= the viewport's)
|
||||
// Composed frames (paintToSharedBuffer): the slot of the newest published frame, the
|
||||
// newest generation of off-screen rows, and what maintenance is owed — the region still
|
||||
// has rows to grow by, or mSweepLeft of its off-screen rows may predate a change.
|
||||
int mLatestSlot = -1;
|
||||
uint32_t mStripGen = 0;
|
||||
bool mGrowOwed = false;
|
||||
long mSweepLeft = 0, mSweepRow = 0;
|
||||
long mSweepStep = 512; // rows per sweep frame (128..1024, by measured cost)
|
||||
int64_t mSweepNotBeforeMs = 0; // pause after a completed sweep
|
||||
int mMaintenance = 0; // 1 grow, 2 sweep: the paint being asked for is one
|
||||
bool panCovered(int x, int y); // is the adapter's viewport inside the painted region?
|
||||
long mPaintedLo = 0, mPaintedHi = 0;
|
||||
double mPaintedZoom = 1.0;
|
||||
int64_t mLastPaintDoneMs = 0; // pan-cadence refresh (fixed-content drift bound)
|
||||
@@ -464,7 +490,15 @@ private:
|
||||
double zeff = 1.0;
|
||||
int scrollX = 0, scrollY = 0;
|
||||
int paintedX = 0, paintedTop = 0, paintedH = 0;
|
||||
int paintedW = 0; // the region's width = the buffer's row stride in px (>= w)
|
||||
long bandRow = 0; // first visible row of the band inside the buffer
|
||||
long bandCol = 0; // first visible column (0 unless the region is wider than w)
|
||||
// For composing the next frame from this one (paintToSharedBuffer, "build on the frame
|
||||
// already painted"): which document the rows are of, which generation of off-screen
|
||||
// rows they are, and whether a popup was drawn over them.
|
||||
uint64_t docId = 0;
|
||||
uint32_t stripGen = 0;
|
||||
bool overlay = false;
|
||||
// Damage accumulated since THIS buffer was last painted, in viewport device px. A
|
||||
// damage-only repaint must cover everything the buffer missed, not just the newest frame:
|
||||
// buffers alternate, so a buffer painted N frames ago skipped every intermediate frame's
|
||||
|
||||
@@ -300,6 +300,9 @@ static NotificationSink* gNotify = nullptr;
|
||||
|
||||
void SetNotificationSink(NotificationSink* sink) { gNotify = sink; }
|
||||
|
||||
static unsigned gPageCopied = 0;
|
||||
unsigned PageCopiedCount() { return gPageCopied; }
|
||||
|
||||
void PostNotification(const char* category, const char* text) {
|
||||
const char* cat = (category && *category) ? category : "info";
|
||||
const char* msg = text ? text : "";
|
||||
@@ -309,8 +312,9 @@ void PostNotification(const char* category, const char* text) {
|
||||
// "no channel" here is how a broken Luna registration announces itself. It is a log
|
||||
// write, not a dialog — nothing blocks and the caller carries on either way.
|
||||
fprintf(stderr, "[jihad-bs] notify (no channel): %s: %s\n", cat, msg);
|
||||
return;
|
||||
return; // nothing was handed to any card: PageCopiedCount stays
|
||||
}
|
||||
if (strcmp(cat, "page-copied") == 0) ++gPageCopied;
|
||||
gNotify->OnNotification(cat, msg);
|
||||
}
|
||||
|
||||
|
||||
@@ -76,6 +76,12 @@ void SetNotificationSink(NotificationSink* sink);
|
||||
// null or empty becomes "info". `text` is one short UTF-8 line meant to be read at a glance.
|
||||
void PostNotification(const char* category, const char* text);
|
||||
|
||||
// How many "page-copied" notifications have gone through PostNotification. The long-press
|
||||
// menu's Copy is carried out by a script component whose only way to say "text was handed to
|
||||
// the card" is that notification, raised synchronously; the daemon reads this before and
|
||||
// after to learn the result.
|
||||
unsigned PageCopiedCount();
|
||||
|
||||
// Install the override of "@mozilla.org/prompter;1". Call once after the engine
|
||||
// is initialised (XRE_InitEmbedding2). Idempotent; returns false on failure.
|
||||
//
|
||||
|
||||
@@ -79,6 +79,7 @@
|
||||
#include "inIDOMUtils.h" // getChildrenForNode(anon): the NAC walk nsIDOMDocumentXBL cannot do
|
||||
#include "inIDOMUtils.h" // getChildrenForNode(anon): the NAC walk nsIDOMDocumentXBL cannot do
|
||||
#include "nsISSLStatusProvider.h" // pull the server cert off a failed TLS channel
|
||||
#include "nsISecureBrowserUI.h" // the loaded page's certificate (the address-bar padlock)
|
||||
#include "nsISSLStatus.h"
|
||||
#include "nsIDOMNodeList.h" // JihadTypingSelfTest: enumerate <input> elements
|
||||
#include "nsIDOMWindow.h" // content window -> document (GetTitle)
|
||||
@@ -426,6 +427,20 @@ NS_IMETHODIMP PageChrome::OnStateChange(nsIWebProgress* aWebProgress, nsIRequest
|
||||
fprintf(stderr, "[jihad-bs] ssl: cert error on %s:%d secInfo=%d prov=%d status=%d cert=%d\n",
|
||||
mCertHost.get(), mCertPort, (int)!!secInfo, (int)!!prov, (int)!!st,
|
||||
(int)!!mCertCert);
|
||||
// NO CERTIFICATE, NO CERTIFICATE QUESTION. The security module also reports
|
||||
// failures that have nothing to do with a certificate — a handshake that broke
|
||||
// (SSL_ERROR_RX_MALFORMED_SERVER_HELLO, measured 2026-10-05 on a first
|
||||
// connection that succeeded on the next try), a protocol the server will not
|
||||
// speak. For those there is no certificate to show or to trust, and the dialog
|
||||
// ("The security certificate … sent is invalid", with Trust Always) was both
|
||||
// untrue and unanswerable: accepting had nothing to accept. They are failed
|
||||
// loads, and are reported as one.
|
||||
if (!mCertCert) {
|
||||
mCertError = false;
|
||||
mCertHost.Truncate();
|
||||
fprintf(stderr, "[jihad-bs] ssl: no certificate came with that failure — "
|
||||
"reporting a failed connection, not a certificate question\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2245,9 +2260,11 @@ bool NotifyChromeGlobal(const char* topic, const char* data) {
|
||||
return NS_SUCCEEDED(obs->NotifyObservers(nullptr, topic, NS_ConvertUTF8toUTF16(data ? data : "").get()));
|
||||
}
|
||||
|
||||
bool GoannaRenderPage::GetCertInfoJson(std::string* json) {
|
||||
if (!json || !mChrome || !mChrome->mCertCert) return false;
|
||||
nsIX509Cert* c = mChrome->mCertCert;
|
||||
// A certificate as the fields a card shows — the shape com.palm.certificatemanager answers
|
||||
// with (subject / issuer / dates / serial), which is what both shells' certificate views
|
||||
// were written against. `extra`, when not empty, is further top-level members.
|
||||
static bool jihadCertJson(nsIX509Cert* c, const std::string& extra, std::string* json) {
|
||||
if (!c || !json) return false;
|
||||
nsAutoString s;
|
||||
std::string subject, issuer, top;
|
||||
bool f = true;
|
||||
@@ -2269,10 +2286,68 @@ bool GoannaRenderPage::GetCertInfoJson(std::string* json) {
|
||||
s.Truncate(); c->GetSha256Fingerprint(s); jihadJsonField(top, "sha256", s, &f);
|
||||
*json = "{\"returnValue\":true,\"subject\":{" + subject + "},\"issuer\":{" + issuer + "}";
|
||||
if (!top.empty()) { *json += ","; *json += top; }
|
||||
if (!extra.empty()) { *json += ","; *json += extra; }
|
||||
*json += "}";
|
||||
return true;
|
||||
}
|
||||
|
||||
bool GoannaRenderPage::GetCertInfoJson(std::string* json) {
|
||||
if (!json || !mChrome || !mChrome->mCertCert) return false;
|
||||
return jihadCertJson(mChrome->mCertCert, std::string(), json);
|
||||
}
|
||||
|
||||
// The certificate of the page that is LOADED (the padlock in the address bar), as opposed
|
||||
// to the one a failed load is asking about (above). It comes from the docshell's security
|
||||
// UI, which follows the document through back/forward and bfcache; the connection's
|
||||
// protocol and cipher ride along, and whether the page pulled in anything insecure.
|
||||
bool GoannaRenderPage::GetPageCertInfoJson(std::string* json) {
|
||||
if (!json || !mChrome || !mChrome->mBrowser) return false;
|
||||
nsCOMPtr<nsIDocShell> ds = GetDocShell(mChrome->mBrowser);
|
||||
if (!ds) return false;
|
||||
nsCOMPtr<nsISecureBrowserUI> ui;
|
||||
ds->GetSecurityUI(getter_AddRefs(ui));
|
||||
nsCOMPtr<nsISSLStatusProvider> prov = do_QueryInterface(ui);
|
||||
nsCOMPtr<nsISSLStatus> st;
|
||||
if (prov) prov->GetSSLStatus(getter_AddRefs(st));
|
||||
nsCOMPtr<nsIX509Cert> cert;
|
||||
if (st) st->GetServerCert(getter_AddRefs(cert));
|
||||
if (!cert) return false;
|
||||
uint32_t state = 0;
|
||||
ui->GetState(&state);
|
||||
nsAutoCString cipher; st->GetCipherName(cipher);
|
||||
uint32_t bits = 0; st->GetSecretKeyLength(&bits);
|
||||
uint16_t ver = 0; st->GetProtocolVersion(&ver);
|
||||
const char* proto = ver == nsISSLStatus::TLS_VERSION_1_3 ? "TLS 1.3"
|
||||
: ver == nsISSLStatus::TLS_VERSION_1_2 ? "TLS 1.2"
|
||||
: ver == nsISSLStatus::TLS_VERSION_1_1 ? "TLS 1.1"
|
||||
: ver == nsISSLStatus::TLS_VERSION_1 ? "TLS 1.0"
|
||||
: ver == nsISSLStatus::SSL_VERSION_3 ? "SSL 3" : "";
|
||||
std::string extra = "\"secure\":true";
|
||||
// What a certificate SAYS is half of it; whether it was believed is the other half.
|
||||
// broken the engine does not count the page as secure although it came over TLS:
|
||||
// it loaded something that did not, or the connection itself is weak
|
||||
// (STATE_IS_BROKEN covers both and does not say which);
|
||||
// overridden the certificate did not verify and the user chose to accept it — it gets
|
||||
// a padlock like any other, and this is the only thing that tells them apart;
|
||||
// untrusted / mismatch / badTime why it did not verify.
|
||||
bool untrusted = false, mismatch = false, badTime = false;
|
||||
st->GetIsUntrusted(&untrusted);
|
||||
st->GetIsDomainMismatch(&mismatch);
|
||||
st->GetIsNotValidAtThisTime(&badTime);
|
||||
const bool overridden = (state & nsIWebProgressListener::STATE_CERT_USER_OVERRIDDEN) != 0;
|
||||
extra += (state & nsIWebProgressListener::STATE_IS_BROKEN) ? ",\"broken\":true" : ",\"broken\":false";
|
||||
extra += overridden ? ",\"overridden\":true" : ",\"overridden\":false";
|
||||
extra += untrusted ? ",\"untrusted\":true" : ",\"untrusted\":false";
|
||||
extra += mismatch ? ",\"mismatch\":true" : ",\"mismatch\":false";
|
||||
extra += badTime ? ",\"badTime\":true" : ",\"badTime\":false";
|
||||
std::string conn; bool f = true;
|
||||
jihadJsonField(conn, "protocol", NS_ConvertASCIItoUTF16(proto), &f);
|
||||
jihadJsonField(conn, "cipher", NS_ConvertUTF8toUTF16(cipher), &f);
|
||||
char kb[40]; snprintf(kb, sizeof kb, "%s\"keyLength\":%u", conn.empty() ? "" : ",", (unsigned)bits);
|
||||
extra += ",\"connection\":{" + conn + kb + "}";
|
||||
return jihadCertJson(cert, extra, json);
|
||||
}
|
||||
|
||||
bool GoannaRenderPage::GetCertDer(std::string* der) {
|
||||
if (!mChrome || !mChrome->mCertCert) return false;
|
||||
uint32_t len = 0;
|
||||
@@ -3735,6 +3810,14 @@ bool GoannaRenderPage::EditableAt(int x, int y, bool* isFocused) {
|
||||
for (int depth = 0; node && depth < 64; ++depth) {
|
||||
nsCOMPtr<nsIDOMElement> e = do_QueryInterface(node);
|
||||
if (e && edIsEditable(e)) {
|
||||
// A control that refuses edits is not a place to paste or cut: its text is the
|
||||
// page's, and gets the page's menu (Copy, Select All).
|
||||
bool ro = false, dis = false;
|
||||
nsCOMPtr<nsIDOMHTMLInputElement> in = do_QueryInterface(e);
|
||||
nsCOMPtr<nsIDOMHTMLTextAreaElement> ta = do_QueryInterface(e);
|
||||
if (in) { in->GetReadOnly(&ro); in->GetDisabled(&dis); }
|
||||
else if (ta) { ta->GetReadOnly(&ro); ta->GetDisabled(&dis); }
|
||||
if (ro || dis) return false;
|
||||
if (isFocused) *isFocused = (mChrome->mFocusedEditable == e);
|
||||
return true;
|
||||
}
|
||||
@@ -3945,6 +4028,14 @@ bool GoannaRenderPage::RenderRegion(unsigned char* dst, int stride, int w, int h
|
||||
return jihad_offscreen_render_region(mWidget, ds, zoom, docX, docY, dst, stride, w, h);
|
||||
}
|
||||
|
||||
uint64_t GoannaRenderPage::CurrentDocId() {
|
||||
if (!mChrome || !mChrome->mBrowser) return 0;
|
||||
nsCOMPtr<nsIDOMWindowUtils> u = GetWindowUtils(mChrome->mBrowser);
|
||||
uint64_t id = 0;
|
||||
if (u) u->GetCurrentInnerWindowID(&id);
|
||||
return id;
|
||||
}
|
||||
|
||||
long GoannaRenderPage::ReadPixels(unsigned char* dst, size_t dstBytes) {
|
||||
if (mOffscreen) {
|
||||
if (!mWidget) return -1;
|
||||
|
||||
@@ -323,6 +323,9 @@ public:
|
||||
// width*height*4 bytes. dst must be at least that large. Returns the number
|
||||
// of non-near-white pixels (a cheap "did it render" signal), or -1 on error.
|
||||
long ReadPixels(unsigned char* dst, size_t dstBytes);
|
||||
// Identifies the document now in the window (its inner window id; 0 if unknown). Two
|
||||
// frames with the same id are of the same document, so rows of one can stand in the other.
|
||||
uint64_t CurrentDocId();
|
||||
|
||||
// Render an ABSOLUTE document region (document-relative, engine scroll ignored)
|
||||
// directly into dst: w x h DEVICE px at zoom Z, covering the CSS rect
|
||||
@@ -399,6 +402,9 @@ public:
|
||||
// names as the platform certificate manager's reply (subject / issuer / startdate /
|
||||
// expiredate / serialNumber), because that is what the Enyo certificate view reads.
|
||||
bool GetCertInfoJson(std::string* json);
|
||||
// The certificate of the page now loaded, plus the connection it came over, in the same
|
||||
// shape. False when the page did not come over TLS (or there is no page).
|
||||
bool GetPageCertInfoJson(std::string* json);
|
||||
// What is actually wrong with that certificate, for choosing the card's
|
||||
// message. False when the last load had no cert error.
|
||||
bool GetCertProblem(jihad::certstore::Problem* out);
|
||||
|
||||
@@ -4,20 +4,31 @@
|
||||
*
|
||||
* Jihad Browser — the page's half of the long-press menu: Select All, Copy, Cut.
|
||||
*
|
||||
* The card shows the menu; these three act on the PAGE, so they are done here. Requests come
|
||||
* from the daemon as an observer notification (it gets them from the card over the frozen
|
||||
* adapter commands selectAll / copy / cut, which were no-op stubs until 2026-10-05):
|
||||
* The card shows the menu; these act on the PAGE, so they are done here. Requests come from
|
||||
* the daemon as an observer notification. It gets them from the card over the frozen adapter
|
||||
* commands selectAll / copy / cut (no-op stubs until 2026-10-05), queues them, and raises
|
||||
* them from its guarded pump tick — never from the socket callback, because two of them can
|
||||
* run page script (an `input` event, a selection listener):
|
||||
*
|
||||
* "jihad-page-command" subject = the content window, data = {"cmd": …, "dir": …}
|
||||
* hold a long-press landed at viewport x,y (`editable`: on a text field;
|
||||
* `focusedNow`: which the press itself has just focused).
|
||||
* On text it selects the word under the finger — unless the press is
|
||||
* inside what is already selected, which is then kept, as a right-click
|
||||
* keeps it. Answers with the notification "page-hold",
|
||||
* {"editable":…,"selected":…}; the card builds its menu from that.
|
||||
* "jihad-page-command" subject = the page's top content window,
|
||||
* data = {"cmd": …, "dir": …, …}
|
||||
*
|
||||
* holdArm a long-press is about to be given to the page as a contextmenu event.
|
||||
* hold it has been (viewport x,y; `editable`: on a text field). If the page
|
||||
* claimed the event (preventDefault — it has a menu of its own) nothing
|
||||
* more happens. Otherwise: on text, the word under the finger is
|
||||
* selected — unless the press is inside what is already selected, which
|
||||
* is then kept, as a right-click keeps it; in a field, the caret goes to
|
||||
* the finger on the same rule. Answers with the notification
|
||||
* "page-hold", {"editable":…,"selected":…}; the card builds its menu
|
||||
* from that.
|
||||
* selectAll select everything in what the last press was on: the field, or the page
|
||||
* copy hand the text selected there to the card
|
||||
* cut the same, then delete it (in a field; elsewhere it is a copy)
|
||||
* cut delete the text the last copy handed over — the SECOND half of Cut,
|
||||
* which the card asks for only once that text is on the clipboard. It
|
||||
* is refused unless the selection is still exactly what was copied, in
|
||||
* full, in a field that accepts edits. Either way the card is told:
|
||||
* "page-cut", or "page-cut-refused" with the reason.
|
||||
*
|
||||
* HOW THE TEXT REACHES THE CARD. The system clipboard belongs to LunaSysMgr, and only a card
|
||||
* can write it. The text cannot ride the hit-test reply (the platform's HitTest.schema
|
||||
@@ -26,13 +37,22 @@
|
||||
* directory — the one place a card may load a local file from — as a one-call script, and
|
||||
* the card is told its NAME over the notification channel:
|
||||
*
|
||||
* <appdir>/clip/copy-<n>.js if(window.jihadClip){window.jihadClip("…");}
|
||||
* notification category "page-copied", text "copy-<n>.js"
|
||||
* <appdir>/clip/copy-<24 random hex>.js if(window.jihadClip){window.jihadClip("…");}
|
||||
* notification category "page-copied", text = that name
|
||||
*
|
||||
* The card loads it with a script tag, puts the text on the clipboard and says "Copied.".
|
||||
* The file is removed fifteen seconds later (and the directory emptied at start-up); the
|
||||
* clipboard itself is readable by every app on the device, so this exposes nothing the copy
|
||||
* was not already going to.
|
||||
* The name is random so that a page cannot find the file by guessing (a local file:// page
|
||||
* may load another local file as a script); the text inside is plain ASCII with everything
|
||||
* else escaped, so no character set or stray surrogate can break the script.
|
||||
*
|
||||
* The card loads it with a script tag and puts the text on the clipboard. The file is removed
|
||||
* fifteen seconds later, and anything left by a daemon that died first is removed when this
|
||||
* component starts; the clipboard itself is readable by every app on the device, so this
|
||||
* exposes nothing the copy was not already going to. A password field's contents are never
|
||||
* copied or cut at all, as in any desktop browser.
|
||||
*
|
||||
* STATE IS PER PAGE. One daemon serves every card of a variant, and this component is one
|
||||
* object for all of them: what was pressed and what was copied are remembered per top window,
|
||||
* so a command from one card can never act on another card's page.
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
@@ -59,6 +79,30 @@ function notify(category, text) {
|
||||
} catch (e) { log("notify failed: " + e); }
|
||||
}
|
||||
|
||||
// --- per-page state ---------------------------------------------------------------------
|
||||
//* One entry per top window: {top, win, editable, copied, truncated, arm}. `top` and `win`
|
||||
//* are weak references, so a closed page costs nothing and is dropped on the next look.
|
||||
let gPages = [];
|
||||
|
||||
function pageState(top, create) {
|
||||
let found = null, live = [];
|
||||
for (let i = 0; i < gPages.length; i++) {
|
||||
let t = null;
|
||||
try { t = gPages[i].top.get(); } catch (e) { t = null; }
|
||||
if (!t) { continue; }
|
||||
live.push(gPages[i]);
|
||||
if (t === top) { found = gPages[i]; }
|
||||
}
|
||||
gPages = live;
|
||||
if (!found && create) {
|
||||
found = {top: Cu.getWeakReference(top), win: null, editable: false,
|
||||
copied: null, truncated: false, arm: null};
|
||||
gPages.push(found);
|
||||
if (gPages.length > 16) { gPages.shift(); }
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
//* The window that has the focus inside `win` (frames followed), and its focused element.
|
||||
function focusOf(win) {
|
||||
let w = win, el = null;
|
||||
@@ -78,39 +122,106 @@ function isTextField(el) {
|
||||
return n === "textarea" || n === "input";
|
||||
}
|
||||
|
||||
function selectedText(f) {
|
||||
// A field keeps its own selection; types without one (checkbox, number in some builds)
|
||||
// throw on selectionStart, and those have nothing to copy.
|
||||
if (isTextField(f.el)) {
|
||||
try {
|
||||
let a = f.el.selectionStart, b = f.el.selectionEnd;
|
||||
if (typeof a === "number" && typeof b === "number" && b > a) {
|
||||
return String(f.el.value).substring(a, b);
|
||||
}
|
||||
} catch (e) {}
|
||||
return "";
|
||||
}
|
||||
try { return String(f.win.getSelection() || ""); } catch (e) { return ""; }
|
||||
function isPassword(el) {
|
||||
try { return !!el && String(el.type).toLowerCase() === "password"; } catch (e) { return false; }
|
||||
}
|
||||
|
||||
//* What the last long-press was on: {win: weak reference to its window, editable}. The menu's
|
||||
//* commands act on THAT — Select All after a press on the page's text selects the page even
|
||||
//* while a text field has the focus, and Copy copies the page's selection, not the field's.
|
||||
let gHold = null;
|
||||
function fieldSelection(el) {
|
||||
// A field keeps its own selection; types without one (checkbox, number in some builds)
|
||||
// throw on selectionStart, and those have nothing to copy.
|
||||
try {
|
||||
let a = el.selectionStart, b = el.selectionEnd;
|
||||
if (typeof a === "number" && typeof b === "number" && b > a) { return {a: a, b: b}; }
|
||||
} catch (e) {}
|
||||
return null;
|
||||
}
|
||||
|
||||
function plainSelection(w) {
|
||||
//* Walk the text a selection covers: fn(node, from, to) for each run of characters, in
|
||||
//* order, until fn returns false or `maxNodes` text nodes have been looked at. Returns false
|
||||
//* if it was cut short. No string is built here (a plain-text page of many megabytes is ONE
|
||||
//* node), and only the selection is walked: from where each range starts to where it ends,
|
||||
//* not the whole subtree they share.
|
||||
function walkSelection(w, sel, maxNodes, fn) {
|
||||
let visited = 0;
|
||||
for (let i = 0; i < sel.rangeCount; i++) {
|
||||
let r = sel.getRangeAt(i), root = r.commonAncestorContainer;
|
||||
if (root.nodeType === 3) {
|
||||
if (r.endOffset > r.startOffset && fn(root, r.startOffset, r.endOffset) === false) { return false; }
|
||||
continue;
|
||||
}
|
||||
let walker = w.document.createTreeWalker(root, 4 /* SHOW_TEXT */, null, false);
|
||||
// Begin AT the range's start: the text node itself, or the first one inside or after
|
||||
// the element the range starts in.
|
||||
let n = null;
|
||||
if (r.startContainer.nodeType === 3) {
|
||||
n = r.startContainer;
|
||||
walker.currentNode = n;
|
||||
} else {
|
||||
let at = r.startContainer.childNodes[r.startOffset] || null;
|
||||
if (at) {
|
||||
walker.currentNode = at;
|
||||
n = (at.nodeType === 3) ? at : walker.nextNode();
|
||||
} else {
|
||||
// The range starts after the last child: the next text node past this subtree.
|
||||
let last = r.startContainer;
|
||||
while (last.lastChild) { last = last.lastChild; }
|
||||
walker.currentNode = last;
|
||||
n = walker.nextNode();
|
||||
}
|
||||
}
|
||||
for (; n; n = walker.nextNode()) {
|
||||
// Past the range's end: this range is done (not cut short).
|
||||
let rel = 0;
|
||||
try { rel = r.comparePoint(n, 0); } catch (e) { rel = 1; }
|
||||
if (rel > 0) { break; }
|
||||
if (++visited > maxNodes) { return false; }
|
||||
let a = (n === r.startContainer) ? r.startOffset : 0;
|
||||
let b = (n === r.endContainer) ? r.endOffset : n.length;
|
||||
if (b > a && fn(n, a, b) === false) { return false; }
|
||||
if (n === r.endContainer) { break; }
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
//* The selected text of a page's own content, at most `max` characters, and whether that is
|
||||
//* all of it. A selection is read whole, with the line breaks the layout gives it — unless
|
||||
//* it is far more than could be copied anyway, where building that string would stop the
|
||||
//* daemon for seconds (or exhaust it) only to throw most of it away. Its size is measured
|
||||
//* first, without building anything; an oversized one is read by walking its text and
|
||||
//* stopping at the bound.
|
||||
function docSelection(w, max) {
|
||||
let sel = null;
|
||||
try { sel = w.getSelection(); } catch (e) { sel = null; }
|
||||
if (!sel || sel.isCollapsed) { return {text: "", whole: true}; }
|
||||
let size = 0;
|
||||
let measured = walkSelection(w, sel, 400000, function(n, a, b) { size += b - a; return size <= 2 * max; });
|
||||
if (measured) {
|
||||
let s = String(sel);
|
||||
return s.length > max ? {text: s.substr(0, max), whole: false} : {text: s, whole: true};
|
||||
}
|
||||
let out = "";
|
||||
let whole = walkSelection(w, sel, 400000, function(n, a, b) {
|
||||
out += n.data.substring(a, b > a + (max - out.length) ? a + (max - out.length) : b);
|
||||
return out.length < max;
|
||||
});
|
||||
return {text: out, whole: whole};
|
||||
}
|
||||
|
||||
function hasDocSelection(w) {
|
||||
try {
|
||||
let sel = w.getSelection();
|
||||
return (sel && !sel.isCollapsed) ? String(sel) : "";
|
||||
} catch (e) { return ""; }
|
||||
return !!(sel && !sel.isCollapsed);
|
||||
} catch (e) { return false; }
|
||||
}
|
||||
|
||||
//* True when one of the selection's boxes is under (or within a fingertip's slop of) x,y.
|
||||
//* Only ever asked of a selection a press has just made — one word.
|
||||
function selectionIsAt(sel, x, y) {
|
||||
const SLOP = 12;
|
||||
for (let i = 0; i < sel.rangeCount; i++) {
|
||||
let rects = sel.getRangeAt(i).getClientRects();
|
||||
for (let k = 0; k < rects.length; k++) {
|
||||
for (let k = 0; k < rects.length && k < 64; k++) {
|
||||
let r = rects[k];
|
||||
if (x >= r.left - SLOP && x <= r.right + SLOP && y >= r.top - SLOP && y <= r.bottom + SLOP) {
|
||||
return true;
|
||||
@@ -121,18 +232,21 @@ function selectionIsAt(sel, x, y) {
|
||||
}
|
||||
|
||||
//* The long-press on plain content. Returns true when something is selected afterwards.
|
||||
function holdSelect(win, x, y) {
|
||||
let w = win;
|
||||
function holdSelect(st, top, x, y) {
|
||||
let w = top;
|
||||
for (let depth = 0; depth < 8; depth++) {
|
||||
let el = w.document.elementFromPoint(x, y), inner = null;
|
||||
try { inner = el && el.contentWindow; } catch (e) { inner = null; }
|
||||
if (!inner || !inner.document) { break; }
|
||||
let r = el.getBoundingClientRect();
|
||||
x -= r.left + el.clientLeft;
|
||||
y -= r.top + el.clientTop;
|
||||
// Into the frame's own coordinates: past its border AND its padding.
|
||||
let r = el.getBoundingClientRect(), cs = null;
|
||||
try { cs = w.getComputedStyle(el, null); } catch (e) { cs = null; }
|
||||
x -= r.left + el.clientLeft + (cs ? (parseFloat(cs.paddingLeft) || 0) : 0);
|
||||
y -= r.top + el.clientTop + (cs ? (parseFloat(cs.paddingTop) || 0) : 0);
|
||||
w = inner;
|
||||
}
|
||||
gHold = {win: Cu.getWeakReference(w), editable: false};
|
||||
st.win = Cu.getWeakReference(w);
|
||||
st.editable = false;
|
||||
let pos = null, sel = null;
|
||||
try { pos = w.document.caretPositionFromPoint(x, y); } catch (e) { pos = null; }
|
||||
try {
|
||||
@@ -152,43 +266,79 @@ function holdSelect(win, x, y) {
|
||||
} catch (e) { log("hold: selectAtPoint threw: " + e); }
|
||||
try {
|
||||
sel = w.getSelection();
|
||||
// A press on the last half-letter of a word puts the caret at the word's END, and the
|
||||
// engine then takes that word and the next one ("bravo charlie", measured). One press
|
||||
// is one word: when two came back, keep the one the caret is in or at the end of.
|
||||
if (/\S\s+\S/.test(plainSelection(w)) && pos && pos.offsetNode && pos.offsetNode.nodeType === 3) {
|
||||
let d = pos.offsetNode.data, a = pos.offset, b = pos.offset;
|
||||
while (a > 0 && /\S/.test(d.charAt(a - 1))) { a--; }
|
||||
while (b < d.length && /\S/.test(d.charAt(b))) { b++; }
|
||||
if (b > a) {
|
||||
let r = w.document.createRange();
|
||||
r.setStart(pos.offsetNode, a);
|
||||
r.setEnd(pos.offsetNode, b);
|
||||
sel.removeAllRanges();
|
||||
sel.addRange(r);
|
||||
if (sel && !sel.isCollapsed) {
|
||||
// What the engine has just selected is a word or two; reading it is cheap.
|
||||
let got = String(sel);
|
||||
// A press on the last half-letter of a word puts the caret at the word's END, and
|
||||
// the engine then takes that word and the next one ("bravo charlie", measured). One
|
||||
// press is one word: when two came back, keep the one the caret is in or at the end of.
|
||||
if (/\S\s+\S/.test(got) && pos && pos.offsetNode && pos.offsetNode.nodeType === 3) {
|
||||
let d = pos.offsetNode.data, a = pos.offset, b = pos.offset;
|
||||
while (a > 0 && /\S/.test(d.charAt(a - 1))) { a--; }
|
||||
while (b < d.length && /\S/.test(d.charAt(b))) { b++; }
|
||||
if (b > a) {
|
||||
let r = w.document.createRange();
|
||||
r.setStart(pos.offsetNode, a);
|
||||
r.setEnd(pos.offsetNode, b);
|
||||
sel.removeAllRanges();
|
||||
sel.addRange(r);
|
||||
got = d.substring(a, b);
|
||||
}
|
||||
}
|
||||
// A press on empty space selects the nearest word, wherever that is. Nothing was
|
||||
// pressed, so nothing is selected.
|
||||
if (!/\S/.test(got) || !selectionIsAt(sel, x, y)) { sel.removeAllRanges(); }
|
||||
}
|
||||
// A press on empty space selects the nearest word, wherever that is. Nothing was
|
||||
// pressed, so nothing is selected.
|
||||
if (sel && !sel.isCollapsed && !selectionIsAt(sel, x, y)) { sel.removeAllRanges(); }
|
||||
} catch (e) { log("hold: narrowing threw: " + e); }
|
||||
return /\S/.test(plainSelection(w));
|
||||
return hasDocSelection(w);
|
||||
}
|
||||
|
||||
//* The long-press in a text field: the caret goes where the finger is, as a click puts it —
|
||||
//* unless the press is inside what is selected there, which is then kept.
|
||||
function holdInField(st, f, x, y) {
|
||||
st.win = Cu.getWeakReference(f.win);
|
||||
st.editable = true;
|
||||
if (isTextField(f.el)) {
|
||||
try {
|
||||
let pos = f.win.document.caretPositionFromPoint(x, y);
|
||||
if (pos && pos.offsetNode === f.el) {
|
||||
let s = fieldSelection(f.el);
|
||||
if (!s || pos.offset < s.a || pos.offset > s.b) { f.el.setSelectionRange(pos.offset, pos.offset); }
|
||||
}
|
||||
} catch (e) {}
|
||||
return !isPassword(f.el) && !!fieldSelection(f.el); // nothing in a password is copyable
|
||||
}
|
||||
return hasDocSelection(f.win); // a contentEditable region
|
||||
}
|
||||
|
||||
//* What a command acts on: {field: focusOf() result} for a text field or editable region,
|
||||
//* or {doc: window} for a page's own text.
|
||||
function target(win) {
|
||||
let f = focusOf(win);
|
||||
if (gHold) {
|
||||
function target(st, top) {
|
||||
let f = focusOf(top);
|
||||
if (st && st.win) {
|
||||
let hw = null;
|
||||
try { hw = gHold.win.get(); } catch (e) { hw = null; }
|
||||
try { hw = st.win.get(); } catch (e) { hw = null; }
|
||||
try {
|
||||
if (hw && !hw.closed && hw.document) { return gHold.editable ? {field: f} : {doc: hw}; }
|
||||
if (hw && !hw.closed && hw.document && hw.top === top) {
|
||||
return st.editable ? {field: f} : {doc: hw};
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
// No press to go by (a command from somewhere other than the long-press menu).
|
||||
return isTextField(f.el) ? {field: f} : {doc: f.win};
|
||||
}
|
||||
|
||||
function selectedOf(t, max) {
|
||||
if (t.doc) { return docSelection(t.doc, max); }
|
||||
if (isTextField(t.field.el)) {
|
||||
let s = isPassword(t.field.el) ? null : fieldSelection(t.field.el);
|
||||
if (!s) { return {text: "", whole: true}; }
|
||||
let n = s.b - s.a;
|
||||
return {text: String(t.field.el.value).substr(s.a, n > max ? max : n), whole: n <= max};
|
||||
}
|
||||
return docSelection(t.field.win, max);
|
||||
}
|
||||
|
||||
function selectAll(t) {
|
||||
if (t.doc) {
|
||||
let d = t.doc.document;
|
||||
@@ -203,19 +353,18 @@ function selectAll(t) {
|
||||
|
||||
function deleteSelection(f) {
|
||||
if (isTextField(f.el)) {
|
||||
try {
|
||||
let a = f.el.selectionStart, b = f.el.selectionEnd;
|
||||
if (typeof a === "number" && b > a) {
|
||||
f.el.setRangeText("", a, b, "end");
|
||||
// What typing would have told the page.
|
||||
let ev = f.el.ownerDocument.createEvent("Event");
|
||||
ev.initEvent("input", true, false);
|
||||
f.el.dispatchEvent(ev);
|
||||
}
|
||||
return;
|
||||
} catch (e) {}
|
||||
if (f.el.readOnly || f.el.disabled) { return false; }
|
||||
let s = fieldSelection(f.el);
|
||||
if (!s) { return false; }
|
||||
f.el.setRangeText("", s.a, s.b, "end");
|
||||
// What typing would have told the page.
|
||||
let ev = f.el.ownerDocument.createEvent("Event");
|
||||
ev.initEvent("input", true, false);
|
||||
f.el.dispatchEvent(ev);
|
||||
return true;
|
||||
}
|
||||
docShellOf(f.win).doCommand("cmd_delete");
|
||||
return true;
|
||||
}
|
||||
|
||||
function docShellOf(win) {
|
||||
@@ -224,23 +373,54 @@ function docShellOf(win) {
|
||||
.QueryInterface(Ci.nsIDocShell);
|
||||
}
|
||||
|
||||
// --- the clip directory -------------------------------------------------------------------
|
||||
function randomName() {
|
||||
let hex = "";
|
||||
try {
|
||||
let bytes = Cc["@mozilla.org/security/random-generator;1"]
|
||||
.createInstance(Ci.nsIRandomGenerator).generateRandomBytes(12);
|
||||
for (let i = 0; i < bytes.length; i++) { hex += ("0" + (bytes[i] & 0xff).toString(16)).slice(-2); }
|
||||
} catch (e) { hex = ""; }
|
||||
if (hex.length !== 24) {
|
||||
hex = "";
|
||||
while (hex.length < 24) { hex += Math.floor(Math.random() * 16).toString(16); }
|
||||
}
|
||||
return "copy-" + hex + ".js";
|
||||
}
|
||||
|
||||
//* `text` as a JavaScript string literal in plain ASCII: JSON's own escapes, and \uXXXX for
|
||||
//* every other character — the two line separators a script cannot hold bare, anything a
|
||||
//* character set could mangle, and half surrogate pairs a page is free to put in its text.
|
||||
function asciiLiteral(text) {
|
||||
return JSON.stringify(text).replace(/[\u0080-\uffff]/g, function(c) {
|
||||
return "\\u" + ("0000" + c.charCodeAt(0).toString(16)).slice(-4);
|
||||
});
|
||||
}
|
||||
|
||||
function writeClip(dir, text) {
|
||||
let d = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
|
||||
d.initWithPath(dir);
|
||||
if (!d.exists()) { d.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755); }
|
||||
let name = "copy-" + Date.now() + "-" + (++gSeq) + ".js";
|
||||
let name = randomName();
|
||||
let f = d.clone();
|
||||
f.append(name);
|
||||
// JSON is a JavaScript string literal except for two line separators, which end a line in
|
||||
// a script and must be escaped there.
|
||||
let lit = JSON.stringify(text).replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
|
||||
let body = "if(window.jihadClip){window.jihadClip(" + lit + ");}\n";
|
||||
let body = "if(window.jihadClip){window.jihadClip(" + asciiLiteral(text) + ");}\n";
|
||||
let out = Cc["@mozilla.org/network/file-output-stream;1"].createInstance(Ci.nsIFileOutputStream);
|
||||
out.init(f, 0x02 | 0x08 | 0x20, 0o644, 0); // write | create | truncate
|
||||
let conv = Cc["@mozilla.org/intl/converter-output-stream;1"].createInstance(Ci.nsIConverterOutputStream);
|
||||
conv.init(out, "UTF-8", 0, 0);
|
||||
conv.writeString(body);
|
||||
conv.close();
|
||||
let ok = false;
|
||||
try {
|
||||
// ASCII by construction, so the bytes are the characters.
|
||||
let left = body;
|
||||
while (left.length) {
|
||||
let n = out.write(left, left.length);
|
||||
if (n <= 0) { throw new Error("short write"); }
|
||||
left = left.substr(n);
|
||||
}
|
||||
ok = true;
|
||||
} finally {
|
||||
try { out.close(); } catch (e) {}
|
||||
if (!ok) { try { f.remove(false); } catch (e) {} } // never leave half a script
|
||||
}
|
||||
// Not kept: the card reads it within a second.
|
||||
let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
|
||||
timer.initWithCallback({notify: function() {
|
||||
@@ -252,24 +432,53 @@ function writeClip(dir, text) {
|
||||
return name;
|
||||
}
|
||||
|
||||
//* Empty the clip directory: whatever is in it was written by a daemon that is gone.
|
||||
function sweep(dir) {
|
||||
//* Empty the clip directory: whatever is in it was written by a daemon that is gone (its
|
||||
//* fifteen-second timers went with it). The directory sits beside the profile, in the app's
|
||||
//* own directory; done once, when this component is created at daemon start.
|
||||
function sweepClipDir() {
|
||||
try {
|
||||
let d = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
|
||||
d.initWithPath(dir);
|
||||
let d = Services.dirsvc.get("ProfD", Ci.nsIFile).parent;
|
||||
d.append("clip");
|
||||
if (!d.exists() || !d.isDirectory()) { return; }
|
||||
let it = d.directoryEntries;
|
||||
while (it.hasMoreElements()) {
|
||||
let f = it.getNext().QueryInterface(Ci.nsIFile);
|
||||
if (/^copy-[0-9-]+\.js$/.test(f.leafName)) { try { f.remove(false); } catch (e) {} }
|
||||
if (/^copy-[0-9a-f-]+\.js$/.test(f.leafName)) { try { f.remove(false); } catch (e) {} }
|
||||
}
|
||||
} catch (e) {}
|
||||
} catch (e) { log("clip sweep: " + e); }
|
||||
}
|
||||
|
||||
let gSwept = false;
|
||||
// --- the page's own contextmenu handling ---------------------------------------------------
|
||||
//* Before the daemon dispatches contextmenu: listen for it in the system group, which runs
|
||||
//* after the page's own listeners, and note whether the page claimed it.
|
||||
function arm(st, top) {
|
||||
disarm(st);
|
||||
let a = {prevented: false, targets: [], handler: null};
|
||||
a.handler = function(ev) { if (ev.defaultPrevented) { a.prevented = true; } };
|
||||
let add = function(t) {
|
||||
if (!t) { return; }
|
||||
try { Services.els.addSystemEventListener(t, "contextmenu", a.handler, true); a.targets.push(t); } catch (e) {}
|
||||
};
|
||||
add(top);
|
||||
// Events in a frame do not pass through the top window; they do pass the docshell's
|
||||
// chrome event handler, where this embedding has one.
|
||||
try { add(docShellOf(top).chromeEventHandler); } catch (e) {}
|
||||
st.arm = a;
|
||||
}
|
||||
|
||||
function disarm(st) {
|
||||
let a = st && st.arm;
|
||||
if (!a) { return false; }
|
||||
st.arm = null;
|
||||
for (let i = 0; i < a.targets.length; i++) {
|
||||
try { Services.els.removeSystemEventListener(a.targets[i], "contextmenu", a.handler, true); } catch (e) {}
|
||||
}
|
||||
return a.prevented;
|
||||
}
|
||||
|
||||
function JihadPageCommands() {
|
||||
Services.obs.addObserver(this, "jihad-page-command", false);
|
||||
sweepClipDir();
|
||||
}
|
||||
|
||||
JihadPageCommands.prototype = {
|
||||
@@ -283,44 +492,62 @@ JihadPageCommands.prototype = {
|
||||
let win = aSubject;
|
||||
if (!win || !win.document) { return; }
|
||||
try {
|
||||
if (req.dir && !gSwept) { gSwept = true; sweep(req.dir); }
|
||||
if (req.cmd === "hold") {
|
||||
let x = Number(req.x) || 0, y = Number(req.y) || 0, selected = false;
|
||||
if (req.editable) {
|
||||
let f = focusOf(win);
|
||||
gHold = {win: Cu.getWeakReference(f.win), editable: true};
|
||||
// A field the press has just focused: the caret goes where the finger is, as a
|
||||
// click puts it (focusing alone leaves it at the start, and Paste would land there).
|
||||
if (req.focusedNow && isTextField(f.el)) {
|
||||
try {
|
||||
let pos = f.win.document.caretPositionFromPoint(x, y);
|
||||
if (pos && pos.offsetNode === f.el) { f.el.setSelectionRange(pos.offset, pos.offset); }
|
||||
} catch (e) {}
|
||||
}
|
||||
selected = !!selectedText(f);
|
||||
} else {
|
||||
selected = holdSelect(win, x, y);
|
||||
if (req.cmd === "holdArm") {
|
||||
arm(pageState(win, true), win);
|
||||
} else if (req.cmd === "hold") {
|
||||
let st = pageState(win, true);
|
||||
if (disarm(st)) {
|
||||
// The page has a context menu of its own; a second one over it helps nobody.
|
||||
log("hold: the page handled the contextmenu event itself, no menu");
|
||||
return;
|
||||
}
|
||||
notify("page-hold", JSON.stringify({editable: !!req.editable, selected: selected}));
|
||||
log("hold: editable=" + !!req.editable + " selected=" + selected);
|
||||
let x = Number(req.x) || 0, y = Number(req.y) || 0;
|
||||
// The editing commands act on the FOCUSED field. If the page kept the press from
|
||||
// focusing the field it landed on (a custom editor that cancels mousedown), that is
|
||||
// some other field: offer the page's menu rather than edit the wrong one.
|
||||
let f = req.editable ? focusOf(win) : null, editable = false;
|
||||
if (f && f.el && f.win === win) {
|
||||
let at = null;
|
||||
try { at = win.document.elementFromPoint(x, y); } catch (e) { at = null; }
|
||||
editable = !!at && (at === f.el || f.el.contains(at));
|
||||
}
|
||||
let selected = editable ? holdInField(st, f, x, y) : holdSelect(st, win, x, y);
|
||||
notify("page-hold", JSON.stringify({editable: editable, selected: selected}));
|
||||
log("hold: editable=" + editable + " selected=" + selected);
|
||||
} else if (req.cmd === "selectAll") {
|
||||
selectAll(target(win));
|
||||
} else if (req.cmd === "copy" || req.cmd === "cut") {
|
||||
let t = target(win);
|
||||
let text = t.doc ? plainSelection(t.doc) : selectedText(t.field);
|
||||
if (!text) { notify("page-copy-empty", "Nothing is selected."); return; }
|
||||
if (text.length > MAX_COPY) { text = text.substr(0, MAX_COPY); }
|
||||
selectAll(target(pageState(win, false), win));
|
||||
} else if (req.cmd === "copy") {
|
||||
let st = pageState(win, true);
|
||||
let got = selectedOf(target(st, win), MAX_COPY);
|
||||
st.copied = null; st.truncated = false;
|
||||
if (!got.text) { notify("page-copy-empty", "Nothing is selected."); return; }
|
||||
if (!req.dir) { notify("page-copy-empty", "Copying is not available."); return; }
|
||||
notify("page-copied", writeClip(req.dir, text));
|
||||
log(req.cmd + ": " + text.length + " characters handed to the card");
|
||||
// Cut on a page's own text is a copy: there is nothing there to delete.
|
||||
if (req.cmd === "cut" && t.field) { deleteSelection(t.field); }
|
||||
let name = writeClip(req.dir, got.text);
|
||||
st.copied = got.text; st.truncated = !got.whole;
|
||||
notify("page-copied", name);
|
||||
log("copy: " + got.text.length + " characters handed to the card" + (got.whole ? "" : " (the selection is longer)"));
|
||||
} else if (req.cmd === "cut") {
|
||||
// Delete what the card has just put on the clipboard — and only that.
|
||||
let st = pageState(win, false);
|
||||
let t = target(st, win);
|
||||
let was = st ? st.copied : null, cut = st ? st.truncated : false;
|
||||
if (st) { st.copied = null; st.truncated = false; }
|
||||
if (!t.field || was === null) { // a page's own text: Cut is a copy
|
||||
notify("page-cut-refused", "Copied. This text cannot be removed from the page.");
|
||||
return;
|
||||
}
|
||||
if (cut) { notify("page-cut-refused", "Too much text to cut at once. It was copied in part and left in place."); return; }
|
||||
if (selectedOf(t, MAX_COPY + 1).text !== was) {
|
||||
notify("page-cut-refused", "Copied. The selection changed, so nothing was removed.");
|
||||
return;
|
||||
}
|
||||
if (deleteSelection(t.field)) { notify("page-cut", "Cut."); }
|
||||
else { notify("page-cut-refused", "Copied. This field does not accept changes."); }
|
||||
}
|
||||
} catch (e) {
|
||||
log(String(req.cmd) + " threw: " + e);
|
||||
if (req.cmd === "copy" || req.cmd === "cut") {
|
||||
notify("page-copy-empty", "The text could not be copied.");
|
||||
}
|
||||
if (req.cmd === "copy") { notify("page-copy-empty", "The text could not be copied."); }
|
||||
if (req.cmd === "cut") { notify("page-cut-refused", "Copied. The text could not be removed."); }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -59,11 +59,78 @@
|
||||
bb.call("palm://com.palm.db/put", enyo.json.stringify({objects: [{_kind: "net.riverstonerelay.jihad-browser.bookmarks:1", title: "jihad test bookmark", url: "http://example.invalid/jihad-test", date: (new Date()).getTime()}]}));
|
||||
window.__jihadTestBridges.push(bb);
|
||||
log(name, "test bookmark added");
|
||||
} else if (st.charAt(0) === "z") { // zZOOM,X,Y: zoom and scroll, as a pinch or a pan ends up
|
||||
var zs = st.slice(1).split(",");
|
||||
view.callBrowserAdapter("pageScaleAndScroll", [parseFloat(zs[0]), parseInt(zs[1], 10), parseInt(zs[2], 10)]);
|
||||
log(name, "zoom/scroll " + st.slice(1));
|
||||
wait = 1200;
|
||||
} else if (st.charAt(0) === "f") { // fVX,VY: a flick, as the adapter's scroller gets it when a
|
||||
// finger lets go — the real kinetic scroll, at the adapter's own frame rate. (The
|
||||
// adapter's pageScaleAndScroll, which an earlier "z" step called, is an empty stub:
|
||||
// that step never moved anything, and what it was thought to prove it did not.)
|
||||
var fv = st.slice(1).split(",");
|
||||
view.callBrowserAdapter("handleFlick", [parseInt(fv[0], 10), parseInt(fv[1], 10)]);
|
||||
log(name, "flick " + st.slice(1));
|
||||
wait = 2500;
|
||||
} else if (st.charAt(0) === "d") { // dX,Y,DX,DY[,VX,VY]: a drag from view X,Y by DX,DY as DOM
|
||||
// mouse events on the plugin node (what one finger becomes), in eight moves; with
|
||||
// VX,VY the release is a flick of that velocity. Whether the platform passes
|
||||
// script-made mouse events on to a plugin is itself something this step finds out.
|
||||
var dp = st.slice(1).split(","), dnode = view.hasNode(), drect = dnode.getBoundingClientRect();
|
||||
var dx0 = drect.left + parseInt(dp[0], 10), dy0 = drect.top + parseInt(dp[1], 10);
|
||||
var ddx = parseInt(dp[2], 10), ddy = parseInt(dp[3], 10), dn = 0;
|
||||
var dfire = function(type, x, y) {
|
||||
var e = document.createEvent("MouseEvents");
|
||||
e.initMouseEvent(type, true, true, window, 1, x, y, x, y, false, false, false, false, 0, null);
|
||||
dnode.dispatchEvent(e);
|
||||
};
|
||||
dfire("mousedown", dx0, dy0);
|
||||
(function dtick() {
|
||||
dn++;
|
||||
dfire("mousemove", dx0 + ddx * dn / 8, dy0 + ddy * dn / 8);
|
||||
if (dn < 8) { setTimeout(dtick, 30); return; }
|
||||
if (dp.length >= 6) { view.callBrowserAdapter("handleFlick", [parseInt(dp[4], 10), parseInt(dp[5], 10)]); }
|
||||
dfire("mouseup", dx0 + ddx, dy0 + ddy);
|
||||
})();
|
||||
log(name, "drag " + st.slice(1));
|
||||
wait = 2500;
|
||||
} else if (st.charAt(0) === "g") { // gSCALE,X,Y: a pinch to SCALE about view X,Y, in eight
|
||||
// steps through the adapter's own gesture calls. The scale and rotation must be
|
||||
// non-integers: the adapter refuses an integer where it expects a double.
|
||||
var gp = st.slice(1).split(","), gTarget = parseFloat(gp[0]), gx = parseInt(gp[1], 10), gy = parseInt(gp[2], 10), gn = 0;
|
||||
view.callBrowserAdapter("gestureStart", [gx, gy, 1.0001, 0.0001, gx, gy]);
|
||||
(function gtick() {
|
||||
gn++;
|
||||
var sc = 1 + (gTarget - 1) * gn / 8 + 0.00001;
|
||||
if (gn < 8) { view.callBrowserAdapter("gestureChange", [gx, gy, sc, 0.0001, gx, gy]); setTimeout(gtick, 60); }
|
||||
else { view.callBrowserAdapter("gestureEnd", [gx, gy, sc, 0.0001, gx, gy]); }
|
||||
})();
|
||||
log(name, "pinch " + st.slice(1));
|
||||
wait = 3500;
|
||||
} else if (st === "A") { // tap the padlock of the OTHER card of this app, from here.
|
||||
// Two cards are open; this one connected last, so the daemon takes it for the
|
||||
// focused one. The other card's own handler is called in the other card's
|
||||
// window (one process, so its objects are reachable; its own timers do not run
|
||||
// while it is in the background, which is why it cannot do this itself). The
|
||||
// certificate it gets must be ITS page's, not this card's.
|
||||
var wins = enyo.windows.getWindows(), other = null;
|
||||
for (var wk in wins) {
|
||||
try { if (wins[wk] !== window && wins[wk].enyo.$.browserApp) { other = wins[wk]; } } catch (e) {}
|
||||
}
|
||||
log(name, "other card: " + (other ? String(other.enyo.$.browserApp.$.browser.getUrl()).slice(0, 60) : "NOT FOUND") + " ; this card: " + String(app.$.browser.getUrl()).slice(0, 60));
|
||||
if (other) {
|
||||
other.enyo.$.browserApp.$.browser.showPageCertificate();
|
||||
later(6000, function() {
|
||||
var dlg = other.enyo.$.browserApp.$.browser.$.sslCertDialog, cd = dlg && dlg.$.certDetail;
|
||||
log(name, "the other card's certificate dialog: open=" + Boolean(dlg && dlg.isOpen) + " name=[" + (cd && cd.$.commonName ? cd.$.commonName.getContent() : "?") + "]");
|
||||
});
|
||||
}
|
||||
} else if (st === "F") { // this card tells the daemon it does NOT have the focus
|
||||
// (the adapter's pageFocused(false)), so that the next thing to claim it — the
|
||||
// padlock — can be seen claiming it in the daemon's log.
|
||||
view.callBrowserAdapter("pageFocused", [false]);
|
||||
log(name, "said pageFocused(false)");
|
||||
} else if (st === "a") { // answer OK to the engine dialog that is open (an alert)
|
||||
app.$.browser.viewCall("acceptDialog", []);
|
||||
log(name, "answered the engine's dialog");
|
||||
} else if (st === "x") { // close this card (a second card that only needed to connect)
|
||||
log(name, "closing this card");
|
||||
window.close();
|
||||
} else if (st === "?") { // report the state now
|
||||
state("now", function() {});
|
||||
} else if (st.charAt(0) === "h") { // hX,Y: a long-press at view X,Y, from the point the
|
||||
@@ -83,11 +150,27 @@
|
||||
app.$.browser.contextItemClick(cm, it.value, cm.tapInfo, cm.tapPosition); // what a tap on the row does
|
||||
cm.close();
|
||||
}
|
||||
} else if (st === "c") { // what is on the clipboard now
|
||||
enyo.jihadGetClipboard(function(t) { log(name, "clipboard=[" + String(t).slice(0, 160) + "] length=" + String(t).length); });
|
||||
} else if (st.charAt(0) === "c") { // c=TEXT: is TEXT what is on the clipboard now? Only the
|
||||
// answer and the length are logged — the clipboard is the user's, the log is kept.
|
||||
(function(want) {
|
||||
enyo.jihadGetClipboard(function(t) { log(name, "clipboard is the expected text: " + (String(t) === want) + " (length " + String(t).length + ", expected " + want.length + ")"); });
|
||||
})(st.slice(2));
|
||||
} else if (st.charAt(0) === "k") { // kTEXT: put TEXT on the clipboard (to have something to paste)
|
||||
enyo.dom.setClipboard(st.slice(1));
|
||||
log(name, "clipboard set");
|
||||
} else if (st === "l") { // the padlock: is it showing, and what does a click on it bring up?
|
||||
var lk = app.$.browser.$.actionbar.$.search.$.address.$.secureLock, ln = lk && lk.hasNode();
|
||||
log(name, "padlock showing=" + Boolean(lk && lk.showing));
|
||||
if (ln) {
|
||||
var le = document.createEvent("MouseEvents");
|
||||
le.initMouseEvent("click", true, true, window, 1, 0, 0, 0, 0, false, false, false, false, 0, null);
|
||||
ln.dispatchEvent(le); // through the framework's own click dispatch
|
||||
}
|
||||
later(6000, function() {
|
||||
var dlg = app.$.browser.$.sslCertDialog, cd = dlg && dlg.$.certDetail, f = function(n) { return (cd && cd.$[n]) ? cd.$[n].getContent() : "?"; };
|
||||
log(name, "certificate dialog open=" + Boolean(dlg && dlg.isOpen) + " name=[" + f("commonName") + "] issuer=[" + f("displayIssuerCommonName") +
|
||||
"] from=[" + f("startDate") + "] to=[" + f("expirationDate") + "] connection=[" + f("connection") + "] fingerprint chars=" + String(f("fingerprint")).length);
|
||||
});
|
||||
} else if (st.charAt(0) === "s") {
|
||||
// The framework's own drop-down (enyo.WebView's PopupList): choose and close,
|
||||
// which is what a tap on one of its rows does.
|
||||
|
||||
@@ -28,6 +28,16 @@
|
||||
// click opened; wN waits N seconds; b adds a test bookmark; ? reports.
|
||||
// Reports the cache, the toast and the sizes of the bookmark and history lists at the end.
|
||||
settings: function(app, name) {
|
||||
// When the engine's answer to a hold reaches this card, and in what state: the one
|
||||
// place a "the menu did not open" can be told apart (late, not asked, or not on screen).
|
||||
if (!app.__holdProbe) {
|
||||
app.__holdProbe = app.pageHold;
|
||||
app.pageHold = function(t) {
|
||||
log(name, "hold answer arrives: asked " + (this._pageHold ? (Date.now() - this._pageHold) + " ms ago" : "never") +
|
||||
", card activated=" + (window.PalmSystem ? window.PalmSystem.isActivated : "n/a") + ", answer=" + t);
|
||||
return app.__holdProbe.apply(this, arguments);
|
||||
};
|
||||
}
|
||||
var spec = String(name).indexOf(":") > 0 ? String(name).slice(String(name).indexOf(":") + 1) : "";
|
||||
var steps = spec ? spec.split(";") : [];
|
||||
function count(kind, cb) { enyo.jihad.dbFind(kind, null, "_rev", true, function(r) { cb((r || []).length); }); }
|
||||
@@ -50,11 +60,52 @@
|
||||
} else if (st === "b") { // a bookmark of the test's own, to have one to clear
|
||||
enyo.jihad.dbPut([{_kind: enyo.jihad.kinds.bookmarks, title: "jihad test bookmark", url: "http://example.invalid/jihad-test", date: (new Date()).getTime()}]);
|
||||
log(name, "test bookmark added");
|
||||
} else if (st.charAt(0) === "z") { // zZOOM,X,Y: zoom and scroll, as a pinch or a pan ends up
|
||||
var zs = st.slice(1).split(",");
|
||||
app.$.view.callBrowserAdapter("pageScaleAndScroll", [parseFloat(zs[0]), parseInt(zs[1], 10), parseInt(zs[2], 10)]);
|
||||
log(name, "zoom/scroll " + st.slice(1));
|
||||
wait = 1200;
|
||||
} else if (st.charAt(0) === "f") { // fVX,VY: a flick, as the adapter's scroller gets it when a
|
||||
// finger lets go — the real kinetic scroll, at the adapter's own frame rate. (The
|
||||
// adapter's pageScaleAndScroll, which an earlier "z" step called, is an empty stub:
|
||||
// that step never moved anything, and what it was thought to prove it did not.)
|
||||
var fv = st.slice(1).split(",");
|
||||
app.$.view.callBrowserAdapter("handleFlick", [parseInt(fv[0], 10), parseInt(fv[1], 10)]);
|
||||
log(name, "flick " + st.slice(1));
|
||||
wait = 2500;
|
||||
} else if (st.charAt(0) === "d") { // dX,Y,DX,DY[,VX,VY]: a drag from view X,Y by DX,DY as DOM
|
||||
// mouse events on the plugin node (what one finger becomes), in eight moves; with
|
||||
// VX,VY the release is a flick of that velocity. Whether the platform passes
|
||||
// script-made mouse events on to a plugin is itself something this step finds out.
|
||||
var dp = st.slice(1).split(","), dnode = app.$.view.hasNode(), drect = dnode.getBoundingClientRect();
|
||||
var dx0 = drect.left + parseInt(dp[0], 10), dy0 = drect.top + parseInt(dp[1], 10);
|
||||
var ddx = parseInt(dp[2], 10), ddy = parseInt(dp[3], 10), dn = 0;
|
||||
var dfire = function(type, x, y) {
|
||||
var e = document.createEvent("MouseEvents");
|
||||
e.initMouseEvent(type, true, true, window, 1, x, y, x, y, false, false, false, false, 0, null);
|
||||
dnode.dispatchEvent(e);
|
||||
};
|
||||
dfire("mousedown", dx0, dy0);
|
||||
(function dtick() {
|
||||
dn++;
|
||||
dfire("mousemove", dx0 + ddx * dn / 8, dy0 + ddy * dn / 8);
|
||||
if (dn < 8) { setTimeout(dtick, 30); return; }
|
||||
if (dp.length >= 6) { app.$.view.callBrowserAdapter("handleFlick", [parseInt(dp[4], 10), parseInt(dp[5], 10)]); }
|
||||
dfire("mouseup", dx0 + ddx, dy0 + ddy);
|
||||
})();
|
||||
log(name, "drag " + st.slice(1));
|
||||
wait = 2500;
|
||||
} else if (st.charAt(0) === "g") { // gSCALE,X,Y: a pinch to SCALE about view X,Y, in eight
|
||||
// steps through the adapter's own gesture calls. The scale and rotation must be
|
||||
// non-integers: the adapter refuses an integer where it expects a double.
|
||||
var gp = st.slice(1).split(","), gTarget = parseFloat(gp[0]), gx = parseInt(gp[1], 10), gy = parseInt(gp[2], 10), gn = 0;
|
||||
app.$.view.callBrowserAdapter("gestureStart", [gx, gy, 1.0001, 0.0001, gx, gy]);
|
||||
(function gtick() {
|
||||
gn++;
|
||||
var sc = 1 + (gTarget - 1) * gn / 8 + 0.00001;
|
||||
if (gn < 8) { app.$.view.callBrowserAdapter("gestureChange", [gx, gy, sc, 0.0001, gx, gy]); setTimeout(gtick, 60); }
|
||||
else { app.$.view.callBrowserAdapter("gestureEnd", [gx, gy, sc, 0.0001, gx, gy]); }
|
||||
})();
|
||||
log(name, "pinch " + st.slice(1));
|
||||
wait = 3500;
|
||||
} else if (st === "x") { // close this card (a second card that only needed to connect)
|
||||
log(name, "closing this card");
|
||||
window.close();
|
||||
} else if (st === "?") { // report the state now
|
||||
state("now", function() {});
|
||||
} else if (st.charAt(0) === "h") { // hX,Y: a long-press at view X,Y, from the point the
|
||||
@@ -72,11 +123,20 @@
|
||||
log(name, "sheet pick [" + its[parseInt(st.slice(1), 10)].getContent() + "]");
|
||||
app.sheetPick(its[parseInt(st.slice(1), 10)]); // what a tap on the row does
|
||||
}
|
||||
} else if (st === "l") { // the padlock: is it showing, and what does its tap bring up?
|
||||
log(name, "padlock showing=" + app.$.lock.getShowing() + " field has room for it=" + app.$.addressBox.hasClass("jihad-secure"));
|
||||
app.showPageCertificate(); // the padlock's own ontap handler
|
||||
later(2500, function() {
|
||||
log(name, "dialog showing=" + app.$.dialog.getShowing() + " title=[" + app.$.dialogTitle.getContent() + "] text=[" + String(app.$.dialogMessage.getContent()).replace(/\n/g, " | ").slice(0, 420) + "]");
|
||||
});
|
||||
} else if (st === "p") { // the menu's Paste, without the menu
|
||||
log(name, "paste (direct)");
|
||||
app.pagePaste();
|
||||
} else if (st === "c") { // what is on the clipboard now
|
||||
enyo.jihad.getClipboard(function(t) { log(name, "clipboard=[" + String(t).slice(0, 160) + "] length=" + String(t).length); });
|
||||
} else if (st.charAt(0) === "c") { // c=TEXT: is TEXT what is on the clipboard now? Only the
|
||||
// answer and the length are logged — the clipboard is the user's, the log is kept.
|
||||
(function(want) {
|
||||
enyo.jihad.getClipboard(function(t) { log(name, "clipboard is the expected text: " + (String(t) === want) + " (length " + String(t).length + ", expected " + want.length + ")"); });
|
||||
})(st.slice(2));
|
||||
} else if (st.charAt(0) === "k") { // kTEXT: put TEXT on the clipboard (to have something to paste)
|
||||
log(name, "clipboard set=" + enyo.jihad.setClipboard(st.slice(1)));
|
||||
} else if (st.charAt(0) === "s") {
|
||||
|
||||
Reference in new issue
Block a user