A press that only brings the window to the front draws nothing
From the development repository at 62e0967. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
2de843143d
commit
51e66db9a1
5 files changed
+78
-10
No files matched your search
+1
-1
@@ -1223,7 +1223,7 @@ async function enterSession(invitationText, name, notebook) {
|
|||||||
session.start();
|
session.start();
|
||||||
// For looking at the session from the browser's console during development.
|
// For looking at the session from the browser's console during development.
|
||||||
Object.assign(window, { kokkoja: {
|
Object.assign(window, { kokkoja: {
|
||||||
session, renderer, writer, loadImage, textEditor,
|
session, renderer, writer, loadImage, textEditor, logText,
|
||||||
notebook: { listNotebooks, listKeptPages, readPage, readCurrentPage, notebooksInBrowser, discardNotebook, discardPage },
|
notebook: { listNotebooks, listKeptPages, readPage, readCurrentPage, notebooksInBrowser, discardNotebook, discardPage },
|
||||||
} });
|
} });
|
||||||
}
|
}
|
||||||
|
|||||||
+32
-1
@@ -16,6 +16,31 @@ const ERASER_BUTTON = 32;
|
|||||||
*/
|
*/
|
||||||
const NOMINAL_PRESSURE = 0.4;
|
const NOMINAL_PRESSURE = 0.4;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A press this soon after the window gained focus is taken to be the
|
||||||
|
* click that brought the window to the front, and draws nothing (Andy,
|
||||||
|
* 2026-10-11): clicking the page to return from the terminal, with the
|
||||||
|
* Pen chosen, left a stray dot. Measured on Hollis (Firefox, Linux):
|
||||||
|
* the window's focus event fires about 4 ms before such a press, the
|
||||||
|
* document already reporting focus, so neither document.hasFocus() nor
|
||||||
|
* the pointer's movement (a pen hovers either way) can tell it from an
|
||||||
|
* ordinary press; only the time since the focus can. A quarter second
|
||||||
|
* is far longer than that press and far shorter than any deliberate
|
||||||
|
* stroke made as a separate act after focusing the window.
|
||||||
|
*/
|
||||||
|
export const FOCUS_SETTLE_MS = 250;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The end-to-end tests turn the focusing-click rule off with
|
||||||
|
* `?no_focus_settle` in the address: a browser driven by WebDriver gains
|
||||||
|
* window focus on the first pointer action after every load or tab
|
||||||
|
* switch, so it cannot tell such a switch from a click that raised the
|
||||||
|
* window, and the rule would refuse legitimate test strokes. The rule
|
||||||
|
* itself is tested with a dispatched focus event in one tab. A real
|
||||||
|
* user never sets this, and it disables nothing but this nicety.
|
||||||
|
*/
|
||||||
|
const focusSettleOn = !new URLSearchParams(location.search).has('no_focus_settle');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {object} PenTarget
|
* @typedef {object} PenTarget
|
||||||
* @property {(clientX: number, clientY: number) => [number, number]} toPage
|
* @property {(clientX: number, clientY: number) => [number, number]} toPage
|
||||||
@@ -100,6 +125,8 @@ export function attachPenInput(surface, target) {
|
|||||||
let pressY = 0;
|
let pressY = 0;
|
||||||
/** @type {PointerEvent | null} The eraser's press, not yet erased with. */
|
/** @type {PointerEvent | null} The eraser's press, not yet erased with. */
|
||||||
let eraseWaiting = null;
|
let eraseWaiting = null;
|
||||||
|
/** When the window last gained focus, for the focusing-click rule. */
|
||||||
|
let lastFocusAt = -Infinity;
|
||||||
|
|
||||||
/** The eraser's press erases now, if it was waiting. */
|
/** The eraser's press erases now, if it was waiting. */
|
||||||
function eraseFromPress() {
|
function eraseFromPress() {
|
||||||
@@ -188,6 +215,10 @@ export function attachPenInput(surface, target) {
|
|||||||
if (activePointer !== null) return refuse(`pointer ${activePointer} still active`);
|
if (activePointer !== null) return refuse(`pointer ${activePointer} still active`);
|
||||||
if (event.target !== surface) return refuse('not on the page');
|
if (event.target !== surface) return refuse('not on the page');
|
||||||
if (event.pointerType === 'pen') penSeen = true;
|
if (event.pointerType === 'pen') penSeen = true;
|
||||||
|
// A press that only brought the window to the front draws nothing.
|
||||||
|
if (focusSettleOn && performance.now() - lastFocusAt < FOCUS_SETTLE_MS) {
|
||||||
|
return refuse('it brought the window to the front');
|
||||||
|
}
|
||||||
// Once a pen has been used, a touch is taken to be the resting hand.
|
// Once a pen has been used, a touch is taken to be the resting hand.
|
||||||
if (event.pointerType === 'touch' && penSeen) return refuse('a touch after a pen: the resting hand');
|
if (event.pointerType === 'touch' && penSeen) return refuse('a touch after a pen: the resting hand');
|
||||||
if (event.pointerType === 'mouse' && event.button !== 0) return refuse('not the main button');
|
if (event.pointerType === 'mouse' && event.button !== 0) return refuse('not the main button');
|
||||||
@@ -325,7 +356,7 @@ export function attachPenInput(surface, target) {
|
|||||||
// learned on his machine before the rule is written (a window focus
|
// learned on his machine before the rule is written (a window focus
|
||||||
// is faked by the test harness on every tab switch, so it cannot be
|
// is faked by the test harness on every tab switch, so it cannot be
|
||||||
// learned here). The press trace already carries "in front"/"behind".
|
// learned here). The press trace already carries "in front"/"behind".
|
||||||
window.addEventListener('focus', () => trace('window: focus'));
|
window.addEventListener('focus', () => { lastFocusAt = performance.now(); trace('window: focus'); });
|
||||||
window.addEventListener('blur', () => trace('window: blur'));
|
window.addEventListener('blur', () => trace('window: blur'));
|
||||||
document.addEventListener('visibilitychange', () => trace(`visibility: ${document.visibilityState}`));
|
document.addEventListener('visibilitychange', () => trace(`visibility: ${document.visibilityState}`));
|
||||||
|
|
||||||
|
|||||||
+3
-1
@@ -81,7 +81,9 @@ from the palette joins the row and stays there in this browser. White
|
|||||||
is for painting over. The four lines to the right of the colours are
|
is for painting over. The four lines to the right of the colours are
|
||||||
the line widths: 0.4, 0.7, 1.2 and 2.4 mm at ordinary pressure.
|
the line widths: 0.4, 0.7, 1.2 and 2.4 mm at ordinary pressure.
|
||||||
|
|
||||||
If a pen is in use, a hand resting on a touch screen is ignored.
|
If a pen is in use, a hand resting on a touch screen is ignored. A
|
||||||
|
click on the page that only brings the browser window to the front, when
|
||||||
|
you come back to it from another window, makes no mark.
|
||||||
|
|
||||||
### Text
|
### Text
|
||||||
|
|
||||||
|
|||||||
@@ -283,7 +283,7 @@ the zoom step of the wheel and the zoom per 100 pixels of a pen drag
|
|||||||
(`renderer.js`), the dead zone of that drag (`pen_input.js`), the margin
|
(`renderer.js`), the dead zone of that drag (`pen_input.js`), the margin
|
||||||
by which the page grows (`page_model.js`), the tooltip delay (1 s,
|
by which the page grows (`page_model.js`), the tooltip delay (1 s,
|
||||||
`DELAY_MS` in `tooltip.js`) and how long after one tooltip the next
|
`DELAY_MS` in `tooltip.js`) and how long after one tooltip the next
|
||||||
button's comes at once (1 s, `WARM_MS` there), the hold that moves an image (600 ms, with 6 pixels of
|
button's comes at once (1 s, `WARM_MS` there), the time within which a press after the window comes to the front is taken for the click that raised it and draws nothing (250 ms, `FOCUS_SETTLE_MS` in `pen_input.js`), the hold that moves an image (600 ms, with 6 pixels of
|
||||||
slack, `pen_input.js`), the reach of an image's corner (24 pixels,
|
slack, `pen_input.js`), the reach of an image's corner (24 pixels,
|
||||||
`renderer.js`), the smallest an image can be scaled to (1 cm,
|
`renderer.js`), the smallest an image can be scaled to (1 cm,
|
||||||
`page_model.js`), how often a dragged image's place is sent (40 ms,
|
`page_model.js`), how often a dragged image's place is sent (40 ms,
|
||||||
|
|||||||
@@ -570,12 +570,17 @@ class EndToEndTest(unittest.TestCase):
|
|||||||
cls.stop()
|
cls.stop()
|
||||||
raise
|
raise
|
||||||
|
|
||||||
def address(self, name, join=None):
|
def address(self, name, join=None, focus_settle=False):
|
||||||
"""The Kokkoja address of a participant's own local program, with
|
"""The Kokkoja address of a participant's own local program, with
|
||||||
its token, and an invitation to join if given."""
|
its token, and an invitation to join if given. By default it
|
||||||
|
carries `?no_focus_settle`, which turns off the focusing-click
|
||||||
|
rule: a driven browser gains focus on the first draw after a load
|
||||||
|
or tab switch, which the rule would take for a focusing click
|
||||||
|
(see test_a_focusing_click_draws_nothing, which keeps it on)."""
|
||||||
base, token = self.programs[name]
|
base, token = self.programs[name]
|
||||||
|
query = "" if focus_settle else "?no_focus_settle"
|
||||||
joining = "" if join is None else f"&join={urllib.parse.quote(join, safe='')}"
|
joining = "" if join is None else f"&join={urllib.parse.quote(join, safe='')}"
|
||||||
return f"{base}#token={token}{joining}"
|
return f"{base}{query}#token={token}{joining}"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def start(cls, command):
|
def start(cls, command):
|
||||||
@@ -708,7 +713,7 @@ class EndToEndTest(unittest.TestCase):
|
|||||||
# filled in, says where the notebooks are, and offers the installed
|
# filled in, says where the notebooks are, and offers the installed
|
||||||
# program to anyone who has it.
|
# program to anyone who has it.
|
||||||
guest_tab = firefox.new_tab()
|
guest_tab = firefox.new_tab()
|
||||||
firefox.go(link)
|
firefox.go(link.replace("#", "?no_focus_settle#", 1))
|
||||||
self.wait_until_true(lambda: firefox.run("return document.getElementById('invitation_entry').value;"),
|
self.wait_until_true(lambda: firefox.run("return document.getElementById('invitation_entry').value;"),
|
||||||
lambda v: v and v.startswith("ws://"), "the invitation in the start screen")
|
lambda v: v and v.startswith("ws://"), "the invitation in the start screen")
|
||||||
self.assertEqual(firefox.run("return document.getElementById('invitation_entry').value;"),
|
self.assertEqual(firefox.run("return document.getElementById('invitation_entry').value;"),
|
||||||
@@ -1063,6 +1068,33 @@ class EndToEndTest(unittest.TestCase):
|
|||||||
firefox.switch_to(host_tab)
|
firefox.switch_to(host_tab)
|
||||||
firefox.go("about:blank")
|
firefox.go("about:blank")
|
||||||
|
|
||||||
|
def test_a_focusing_click_draws_nothing(self):
|
||||||
|
"""Andy, 2026-10-11: clicking the page to bring the browser to the
|
||||||
|
front from the terminal, with the Pen chosen, left a stray dot. A
|
||||||
|
press within 250 ms of the window gaining focus draws nothing.
|
||||||
|
(The window focus is dispatched here: the test harness raises the
|
||||||
|
real window on the first draw after a tab switch, so a two-tab
|
||||||
|
test cannot feign the terminal case; this does it in one tab.)"""
|
||||||
|
firefox = self.firefox
|
||||||
|
firefox.go(self.address("Late", focus_settle=True))
|
||||||
|
self.enter_name("Focuser", "start_session")
|
||||||
|
self.wait_until(lambda s: s["page"]["number"] == 1, "the page")
|
||||||
|
strokes = lambda: len(self.state()["layers"].get("Focuser", []))
|
||||||
|
# The window gains focus, then a press right after: nothing drawn.
|
||||||
|
firefox.run("window.dispatchEvent(new Event('focus'));")
|
||||||
|
firefox.drag("#page", 200, 300, 400, 320)
|
||||||
|
time.sleep(0.4)
|
||||||
|
self.assertEqual(strokes(), 0)
|
||||||
|
self.assertIn("refused: it brought the window to the front",
|
||||||
|
firefox.run("return window.kokkoja.logText();"))
|
||||||
|
# Once the window has been focused a while, a stroke draws.
|
||||||
|
firefox.run("window.dispatchEvent(new Event('focus'));")
|
||||||
|
time.sleep(0.4)
|
||||||
|
firefox.drag("#page", 200, 400, 400, 420)
|
||||||
|
self.wait_until(lambda s: len(s["layers"].get("Focuser", [])) == 1,
|
||||||
|
"a stroke once the window had been focused a while")
|
||||||
|
firefox.go("about:blank")
|
||||||
|
|
||||||
def test_undo(self):
|
def test_undo(self):
|
||||||
"""Andy, 2026-10-11: one's latest gesture is taken back, then the
|
"""Andy, 2026-10-11: one's latest gesture is taken back, then the
|
||||||
one before, by the Undo button, Ctrl+Z, or two quick taps of the
|
one before, by the Undo button, Ctrl+Z, or two quick taps of the
|
||||||
@@ -2024,8 +2056,11 @@ class EndToEndTest(unittest.TestCase):
|
|||||||
self.assertEqual(len(self.kept_pages("Teacher notebook")), 2)
|
self.assertEqual(len(self.kept_pages("Teacher notebook")), 2)
|
||||||
|
|
||||||
# A new session in the same notebook: the page the last session
|
# A new session in the same notebook: the page the last session
|
||||||
# left there is kept, without anyone having asked.
|
# left there is kept, without anyone having asked. (The param
|
||||||
firefox.go(self.programs["Teacher"][0])
|
# that turns off the focusing-click rule, as address() adds, so
|
||||||
|
# that drawing after this navigation is not taken for a focusing
|
||||||
|
# click by the driven browser.)
|
||||||
|
firefox.go(self.programs["Teacher"][0] + "?no_focus_settle")
|
||||||
firefox.run("location.reload();")
|
firefox.run("location.reload();")
|
||||||
time.sleep(0.5)
|
time.sleep(0.5)
|
||||||
self.enter_name("Teacher", "start_session")
|
self.enter_name("Teacher", "start_session")
|
||||||
|
|||||||
Reference in new issue
Block a user