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();
|
||||
// For looking at the session from the browser's console during development.
|
||||
Object.assign(window, { kokkoja: {
|
||||
session, renderer, writer, loadImage, textEditor,
|
||||
session, renderer, writer, loadImage, textEditor, logText,
|
||||
notebook: { listNotebooks, listKeptPages, readPage, readCurrentPage, notebooksInBrowser, discardNotebook, discardPage },
|
||||
} });
|
||||
}
|
||||
|
||||
+32
-1
@@ -16,6 +16,31 @@ const ERASER_BUTTON = 32;
|
||||
*/
|
||||
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
|
||||
* @property {(clientX: number, clientY: number) => [number, number]} toPage
|
||||
@@ -100,6 +125,8 @@ export function attachPenInput(surface, target) {
|
||||
let pressY = 0;
|
||||
/** @type {PointerEvent | null} The eraser's press, not yet erased with. */
|
||||
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. */
|
||||
function eraseFromPress() {
|
||||
@@ -188,6 +215,10 @@ export function attachPenInput(surface, target) {
|
||||
if (activePointer !== null) return refuse(`pointer ${activePointer} still active`);
|
||||
if (event.target !== surface) return refuse('not on the page');
|
||||
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.
|
||||
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');
|
||||
@@ -325,7 +356,7 @@ export function attachPenInput(surface, target) {
|
||||
// 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
|
||||
// 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'));
|
||||
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
|
||||
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
|
||||
|
||||
|
||||
@@ -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
|
||||
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
|
||||
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,
|
||||
`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,
|
||||
|
||||
@@ -570,12 +570,17 @@ class EndToEndTest(unittest.TestCase):
|
||||
cls.stop()
|
||||
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
|
||||
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]
|
||||
query = "" if focus_settle else "?no_focus_settle"
|
||||
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
|
||||
def start(cls, command):
|
||||
@@ -708,7 +713,7 @@ class EndToEndTest(unittest.TestCase):
|
||||
# filled in, says where the notebooks are, and offers the installed
|
||||
# program to anyone who has it.
|
||||
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;"),
|
||||
lambda v: v and v.startswith("ws://"), "the invitation in the start screen")
|
||||
self.assertEqual(firefox.run("return document.getElementById('invitation_entry').value;"),
|
||||
@@ -1063,6 +1068,33 @@ class EndToEndTest(unittest.TestCase):
|
||||
firefox.switch_to(host_tab)
|
||||
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):
|
||||
"""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
|
||||
@@ -2024,8 +2056,11 @@ class EndToEndTest(unittest.TestCase):
|
||||
self.assertEqual(len(self.kept_pages("Teacher notebook")), 2)
|
||||
|
||||
# A new session in the same notebook: the page the last session
|
||||
# left there is kept, without anyone having asked.
|
||||
firefox.go(self.programs["Teacher"][0])
|
||||
# left there is kept, without anyone having asked. (The param
|
||||
# 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();")
|
||||
time.sleep(0.5)
|
||||
self.enter_name("Teacher", "start_session")
|
||||
|
||||
Reference in new issue
Block a user