diff --git a/Directory.Build.props b/Directory.Build.props index 1199a98..aea84e8 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -5,7 +5,7 @@ scripts/build-installer.ps1 both read it from here, so releasing is a reviewed change to this line rather than an edit in a pipeline variable group. --> - 1.1.5 + 1.2.0 latest enable enable diff --git a/README.md b/README.md index 687f405..7980293 100644 --- a/README.md +++ b/README.md @@ -11,9 +11,10 @@ How the project is developed and shipped is documented separately: - Low-latency, pressure-aware WPF wet ink, including rear-eraser detection on any pen that reports it - A normal cursor for physical mouse input, and a pen-hover indicator that shows what a tap would do: the laser with its halo and speed trail, a dashed square around what the eraser would clear, and a high-contrast dot for everything else. All of them disappear on contact +- Optional mouse drawing (default when Windows reports neither a pen tablet nor a touchscreen): the left button uses the current tool, Ctrl and the left button move and resize a container, and Eraser and Pan appear on the toolbar. A mouse reports no pressure, so ink is drawn at an even width and Calligraphy is the one tool that still varies, because its width comes from speed. Nothing about the pen changes when it is on - Touch panning and two-finger pinch zoom - Optional finger drawing (default when no pen is detected): one finger uses the current tool, two fingers still pan and pinch-zoom, and Eraser and Pan appear on the toolbar -- A notice at startup when Windows reports neither a pen tablet nor a touchscreen: the application still opens, but there is nothing to draw with, and [discussion 78](https://github.com/sql-bi/SQLBI-Whiteboard/discussions/78) collects votes for mouse-only drawing. Dismissable from the notice itself or from Preferences, since the tablet list Windows reports can miss a pen that has never been in range +- A notice at startup when Windows reports neither a pen tablet nor a touchscreen, saying which pointing device the session is drawing with and what a pen would add. Dismissable from the notice itself or from Preferences, since the tablet list Windows reports can miss a pen that has never been in range - Basic palm rejection: touch navigation is suspended when the pen makes contact - Mouse-wheel zoom and middle-button or temporary Space-key panning - Whole-stroke erasing @@ -30,7 +31,7 @@ How the project is developed and shipped is documented separately: - Markdown `.wimport` recipes that build image and text containers from headings - An intentionally small floating toolbar - A File / Edit / View / Help tab strip. Click a tab for a one-row command strip over the canvas -- Preferences for the startup monitor, full-screen start, finger drawing, the pen button, snippet format order, laser trail timing and weight, toolbar position and layout, and (except Store installs) a daily new-version check +- Preferences for the startup monitor, full-screen start, finger drawing, mouse drawing, the pen button, snippet format order, laser trail timing and weight, toolbar position and layout, and (except Store installs) a daily new-version check - About, with version and channel ## Build and run @@ -179,8 +180,9 @@ Use **Copy settings** after finding a useful combination so the exact values can | Pen hover | Show the small red pointer dot and hide the arrow | | Pen contact | Hide both the pointer dot and arrow | | Physical mouse movement | Show the normal arrow | -| Left mouse | Temporarily select/move/resize a container; return to the previous drawing tool on release | -| Double-click container | Center and fit the image, text, or LiveView to the canvas | +| Left mouse | With Mouse drawing off, temporarily select/move/resize a container and return to the previous drawing tool on release. With it on, the current tool: ink, erase, select, pan, or the laser | +| Ctrl + left mouse | Select/move/resize a container and return to the previous drawing tool — what the left button does on its own when Mouse drawing is off | +| Double-click container | Center and fit the image, text, or LiveView to the canvas. With Mouse drawing on and an ink or eraser tool selected, hold Ctrl: two plain clicks are two strokes | | Double-click empty canvas | Center and fit all board content, or reset an empty board | | Pen eraser | Erase complete strokes. The upper side button erases too: Windows reports it the same way as a pen turned round | | Pen barrel | Hold the barrel button for the action assigned in Preferences: Laser (default) or Straight line. Laser returns to the previous tool on release | @@ -203,7 +205,7 @@ Use **Copy settings** after finding a useful combination so the exact values can | Delete | Delete the selected container and its linked strokes | | Alt+L | Laser pointer | | File / Edit / View / Help | Tab strip. Click a tab for a one-row command strip over the canvas. Click the canvas to hide it | -| Help > Preferences | Searchable settings: startup monitor, full screen, no-pen warning, finger drawing, pen button, snippet format order, laser trail, toolbar, update checks | +| Help > Preferences | Searchable settings: startup monitor, full screen, no-pen warning, finger drawing, mouse drawing, pen button, snippet format order, laser trail, toolbar, update checks | | View > Bring to front / Send to back | Reorder the selected image, text, or LiveView (and its linked strokes) | | Help > About | Version, channel, license, the product site, and a download link when a newer release is known | | View > LiveView | Capture, freeze, disconnect, or reconnect a window or display | @@ -212,6 +214,8 @@ Use **Copy settings** after finding a useful combination so the exact values can With the mouse, selection is automatic: click a container to move it, or drag the circular bottom-right handle to resize it while preserving its aspect ratio. Double-click a container to center it and fit it to the canvas. Releasing the mouse returns to the previously selected drawing tool. +**Help → Preferences → Mouse drawing** changes what the left button means, and it defaults to on when Windows reports neither a pen tablet nor a touchscreen. With it on the left button uses the selected tool, the tool stays selected rather than being handed back, and Eraser and Pan join the toolbar as they do for finger drawing. Everything above then moves to Ctrl: Ctrl and the left button select, move, and resize a container and return to the previous drawing tool, and Ctrl with a double-click centers and fits one. Two plain clicks with an ink tool are two strokes, which is why framing moves out of the way. Shift still constrains a stroke to horizontal or vertical, and Alt+L is still the laser. A mouse reports no pressure, so ink is drawn at an even width; Calligraphy still varies, because its width comes from speed. The pen path is untouched, so Mouse drawing can be left on beside a pen. + Imported images, LiveViews, and text objects act as containers. A completed stroke is linked when it touches exactly one container, including crossing its edge; a stroke touching multiple containers remains independent. Moving or resizing a container transforms its linked strokes with it. **View → Bring to front** and **View → Send to back** reorder the selected container and those linked strokes. Deleting a container also deletes all of its linked strokes. Undo/redo treats each complete container operation as one action. Paste plain text to create a selected text container in display mode. **Help → Preferences** has Snippet format order: paste tries those languages from top to bottom and uses the first that accepts the text. Plain text always accepts, so leaving it first keeps every paste as plain text. Recognized extensions (`.dax`, `.sql`, `.txt`) keep their language; other dropped text files use the same order. Choose **Plain text**, **DAX**, or **SQL Server** from the title-bar chip afterward. Press **F6** to format DAX or SQL on the selected container without entering edit. Press **F2** to edit the body; the same list is in the title bar while editing. In F2, **F6** formats in place; **Ctrl+Enter** commits that edit (including the format) and returns to display. **Escape** restores the previous text, language, and dimensions. Text reflows while its edit-mode resize grip changes the width, and the height grows automatically when necessary. Double-click still centers and fits the container. Syntax highlighting applies in both edit and display modes. A language-aware title identifies a defined DAX or SQL object when possible. SQL Server mode targets SQL Server 2025 T-SQL, preserves `GO` batch separators, and leaves invalid scripts unchanged. In display mode, resizing preserves the aspect ratio and scales the complete text visual without reflowing it. @@ -275,3 +279,20 @@ Test these on the target device before tuning stroke algorithms: 5. Draw near all display edges and across the Windows display-scaling boundary, if multiple monitors use different scaling. Wacom driver settings can remap the barrel and eraser controls, so validate both Windows Ink mode and the intended application profile. + +## Mouse drawing validation + +Mouse drawing cannot be covered by the smoke tests, which are UI-free. Walk this after +touching any mouse handler: + +1. On a machine with no pen and no touchscreen, confirm the default turns it on and the + startup notice describes it. +2. Draw with Pen, Highlighter, and Calligraphy. Only Calligraphy should vary its width. +3. Click without moving, and confirm a dot is drawn rather than nothing. +4. Press and release Shift mid-stroke and confirm the constraint starts and ends there. +5. Erase, and confirm the dashed square matches what is removed. +6. Ctrl-drag a container, and confirm the drawing tool comes back on release. +7. Two quick clicks with the Pen, and confirm the board does not reframe. +8. Select the Eraser, pan with the right button, and confirm the Eraser is still selected. +9. On a pen machine with Mouse drawing **On**, draw with the pen and confirm nothing about + it changed. This is the regression that matters. diff --git a/TODO.md b/TODO.md index c40869d..307ad8d 100644 --- a/TODO.md +++ b/TODO.md @@ -16,14 +16,17 @@ The delivery chain works end to end: a merge to `main` builds, signs, and publis pre-release to GitHub Releases, and one approval promotes that same build to a release. reads its download links from the release manifest deployed beside it and needs no edit per release. The current product version is `VersionPrefix` in `Directory.Build.props` -(1.1.5). Identity version for the Store package is `VersionPrefix.0` (`1.1.5.0`). +(1.2.0). Identity version for the Store package is `VersionPrefix.0` (`1.2.0.0`). Declaring that number is decision 20 in [docs/decisions.md](docs/decisions.md). What 1.0 was waiting on shipped during 0.9.x: Preferences, `.wimport`, Explorer and VS Code previews, the public documentation site, and Finger drawing (default when no pen is detected). -No numbered work remains. The video teaser is recorded and served from the landing page +No numbered work remains. 1.2.0 answered +[discussion 78](https://github.com/sql-bi/SQLBI-Whiteboard/discussions/78) with Mouse +drawing — decision 23, with the alternatives kept in +[docs/mouse-mode.md](docs/mouse-mode.md). The video teaser is recorded and served from the landing page itself as `site/teaser-av1.mp4` / `site/teaser-h264.mp4` — the Vimeo-embed plan was reversed, see decision 19 in [docs/decisions.md](docs/decisions.md); the production script and staging assets are in `docs/teaser/`. The release manifests and the Store diff --git a/docs/decisions.md b/docs/decisions.md index 11efbdf..02c4ff6 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -422,6 +422,61 @@ was several hundred lines and never converged. --- +## 23. A mouse gets the tools, not the gestures + +**Implemented** in 1.2.0. The proposal it came from, with the alternatives that were +weighed and rejected, is [mouse-mode.md](mouse-mode.md). + +The application was built so that no input device imitates another: the pen inks, touch +navigates, and the mouse moves things. That was not a gap. It is why the pen path is as +direct as it is, and it stays the rule for the pen. + +What it cost was people who downloaded a whiteboard onto a laptop with no pen and no +touchscreen and found the toolbar did nothing. 1.1 conceded the point at startup and +collected votes in +[discussion 78](https://github.com/sql-bi/SQLBI-Whiteboard/discussions/78). So there is now +a **Mouse drawing** setting, on by default when Windows reports neither a stylus nor a +touchscreen, under which the left button does what the selected tool does. + +One sentence governs it, and any later change to it: **a mouse gets the tools, not the +gestures.** Everything on the toolbar becomes reachable with a mouse. Nothing that exists +because of what a hand and a pen can do — pressure, hover, the reverse end, the barrel +button, palm rejection, two fingers — is simulated with modifiers and timers. Where a +gesture has no honest mouse equivalent the mouse does without it, and the documentation +says so. That is what stops mouse support becoming a tax on every future input feature. + +Four consequences are worth recording, because each was a choice with a live alternative: + +- **`Ctrl` is the old mouse.** Letting the left button draw takes away the one genuinely + good thing about mouse input — moving an image without leaving the Pen — so it is handed + straight back on a modifier rather than lost. Giving it to the right button instead was + rejected: right-drag pan is the only pan that needs no keyboard. +- **Framing moves behind `Ctrl` too, but only where it has to.** Double-click is tested + before the tool branch, so two quick dabs with the Pen would otherwise reframe the board. + With Select or Pan active, and whenever Mouse drawing is off, a plain double-click still + frames. +- **Pressure is the constant the straight-line constraint already uses.** Deriving it from + speed was rejected as a default: the width would vary for a reason the hand cannot feel, + and a wobble nobody asked for reads as a bug. Calligraphy is unaffected, because its width + comes from speed rather than from pressure, and the highlighter already ignores pressure. +- **The tool becomes sticky.** Nothing is handed back after a mouse gesture that was not a + `Ctrl` borrow — including a right-button pan, which would otherwise take someone who chose + the Eraser and quietly leave them holding a pen. + +The reason this was a few hundred lines rather than a subsystem is decision 22. Because pen +ink is collected from raw points rather than from the InkCanvas, `AppendInkPoint` takes a +screen point and a pressure and has no idea what device it is serving; the mouse calls it, +and gets the straight-line constraint and the calligraphy dynamics without a second +implementation. The erase, pan and container paths already existed on the mouse handlers — +`PointerAction.Erase` was written and unreachable. + +Not one line of the pen path changed, and that was the condition for building it at all. +Every mouse handler already returned early on a non-null `StylusDevice`, so pen-promoted +mouse events never enter the mouse path. Mouse drawing can therefore be left on beside a +pen, which is why **On** is offered and not only the automatic default. + +--- + ## Open questions - arm64 is not built; add it if Surface devices matter for a pen application. diff --git a/docs/mouse-mode.md b/docs/mouse-mode.md new file mode 100644 index 0000000..6cca8ca --- /dev/null +++ b/docs/mouse-mode.md @@ -0,0 +1,377 @@ +# Mouse mode + +**Accepted and implemented in 1.2.0, with every recommendation below taken.** The settled +form is decision 23 in [decisions.md](decisions.md); this document is kept because it holds +the alternatives that were weighed and rejected, which the decision only summarizes. Where +the two disagree, the decision is current. + +It states what the mouse did before the change, every place where letting it draw +contradicted a decision already taken, what was proposed instead, and what the choice costs. +The "today" throughout is 1.1.5. + +Background: [README.md](../README.md) for what the application does, +[decisions.md](decisions.md) for the choices behind it — decision 22 in particular, which +explains why pen ink is read from the pen rather than from the InkCanvas. + +## Why this is being asked now + +The application was built on a constraint: each input device gets the behaviour it is good +at, and none of them is asked to imitate another. The pen inks, with pressure, a rear +eraser and a barrel button. Touch navigates, and draws only when there is no pen. The mouse +navigates and moves things, and does not draw. That constraint is not a gap; it is the +reason the pen path is as direct as it is. + +What happened after release is that people downloaded a whiteboard onto a laptop with no +pen and no touchscreen, and found that the toolbar did nothing. Version 1.1 says so out +loud at startup — `NoDigitizerWindow` — and points at +[discussion 78](https://github.com/sql-bi/SQLBI-Whiteboard/discussions/78) to collect +votes. That notice is already an admission: the application opens, and there is nothing +to draw with. + +So the question is not whether a mouse is a good drawing instrument. It is not, and no +amount of code changes that. The question is whether the application should fail silently +for someone who has no pen, or give them something honest and limited. + +## The organizing rule + +If this ships, one sentence should govern every later decision about it, so that mouse +support does not become a tax on every future input feature: + +> **A mouse gets the tools, not the gestures.** + +Everything on the toolbar becomes reachable with a mouse. Nothing that exists because of +what a hand and a pen can do — pressure, hover, the rear end, the barrel button, palm +rejection, two fingers — is simulated for the mouse with modifier keys and timers. Where a +gesture has no honest mouse equivalent, the mouse does without it and the documentation +says so. + +## What the mouse does today + +Read off `MainWindow.InkSurface_PreviewMouseDown`, `_PreviewMouseMove`, `_PreviewMouseUp`, +`_LostMouseCapture` and `CompleteMouseAction`. Every mouse handler returns immediately when +`e.StylusDevice is not null`, which is how pen- and touch-promoted mouse events are kept out +of this path. + +| Gesture | Behaviour | Mechanism | +| --- | --- | --- | +| Left drag | Borrows `Select` for one gesture: click a container to move it, drag the bottom-right handle to resize it. On release the tool reverts to `_lastDrawingTool` | `SetActiveTool(BoardTool.Select)` then `BeginContainerGesture`; `PointerAction.Container` | +| Left double-click | Center and fit the container under the pointer, or frame the whole board on empty canvas | `e.ClickCount >= 2` → `FrameContentAt` | +| Left drag, Laser active | Draws the laser trail at a fixed pressure of 0.5 | `PointerAction.Laser`, `MouseLaserPressure` | +| Middle drag | Pan | `PointerAction.Pan` | +| Right drag | Pan; returns to the drawing tool on release | `PointerAction.Pan` | +| Right-click on the palette | Hide or show the palette | `ToolPalette_PreviewMouseRightButtonDown` | +| Wheel | Zoom at the pointer; Shift+wheel zooms more slowly | `ZoomAtMouseWheel` | +| Movement | Restores the normal arrow and hides the pen's hover dot. Over a container with `Select` active, the cursor becomes a move or resize cursor | `UpdateSelectHover`, `SelectCursorAt` | + +Two facts from that table matter more than the rest. + +**The left button has one meaning, and it is not the active tool.** Left-drag is always +select-and-move, whatever tool is chosen, and the tool is handed back on release. That is +possible only because the mouse never draws. It is the single most convenient thing about +mouse input today — you can move an image without leaving the Pen — and it is exactly what +mouse drawing has to take away. + +**The erase path is already written and currently unreachable.** `InkSurface_PreviewMouseMove`, +`CompleteMouseAction` and `InkSurface_LostMouseCapture` all handle `PointerAction.Erase`, +but no mouse-down ever assigns it. Mouse erasing is a branch in one method away from +working. That is not an accident to be tidied up; it is a measure of how much of this +feature already exists. + +## What the mouse cannot report + +These are physical, not oversights, and they define the ceiling. + +- **No pressure.** `InkPoint` carries a pressure per point, `BoardSurface` renders width + from it, and `CalligraphyDynamics.AdjustPressure` shapes it by speed. A mouse reports a + button state. +- **No hover.** A pen in the air has a position and no contact, which is what + `UpdateHoverPointerDot`, the laser's comet, and the dashed eraser square all attach to. + For a mouse, button-up movement *is* the hover state, and there is no separate "about to + touch". +- **No inverted end.** `e.StylusDevice.Inverted` is how the rear eraser and the upper side + button are recognized (see TODO.md). A mouse has nothing equivalent. +- **No barrel button.** Laser and Straight line are assigned to it in Preferences. Both + already have keyboard homes — `Alt+L` and `Shift` — so this costs nothing. +- **A lower, coalesced event rate.** `AppendPenInk` reads bursts of packets through + `e.GetStylusPoints`. WPF coalesces mouse moves to roughly the frame rate. A fast mouse + stroke will have visibly fewer points than a fast pen stroke and will look more angular. + +## The conflicts, and what to do about each + +### 1. The left button stops meaning "select" + +**The conflict.** `InkSurface_PreviewMouseDown` hardwires left to `SetActiveTool(BoardTool.Select)`; +`CompleteMouseAction` and `InkSurface_LostMouseCapture` hand the tool back with +`SetActiveTool(_lastDrawingTool)`. If the left button draws, both have to go, and moving an +image stops being free. + +**Proposed.** With mouse mode on, the left button does what the active tool does — ink, +erase, select, pan, laser — exactly as `InkSurface_PreviewStylusDown` already branches for +the pen. The tool becomes sticky: no automatic revert. + +**And the convenience is kept, on a modifier.** `Ctrl` + left restores today's behaviour +precisely: borrow `Select` for one gesture, then return to the previous drawing tool. `Ctrl` +is free here — there is no multi-select to collide with — and it gives the mouse-mode user +one thing to learn rather than a lost capability. With mouse mode off, plain left is +unchanged, so nobody with a pen notices anything. + +**Considered and rejected:** giving select to the right button. Right-drag pan is +documented, liked, and the only pan that needs no keyboard. Trading it for a modifier is a +worse deal. + +### 2. Double-click to frame fires while drawing + +**The conflict.** `e.ClickCount >= 2` is tested *before* the tool branch. Two quick dabs +with the Pen would center and fit the board. This is the conflict most likely to be found +by a user rather than by us. + +**Proposed.** With mouse mode on and an ink or eraser tool active, plain double-click is +just two strokes, and framing moves to `Ctrl` + double-click — consistent with rule 1, +where `Ctrl` means "behave like the old mouse". With `Select` or `Pan` active, or with +mouse mode off, plain double-click still frames. Nothing changes for the pen, where +double-click framing lives on `Select` anyway. + +### 3. Ink has no pressure + +**The conflict.** Every stroke needs a pressure per point. + +**Proposed.** A constant `MousePressure = 0.5f` — the same neutral value +`StraightLinePressure` already uses, chosen because WPF draws it at exactly the configured +thickness with no taper at either end. Then: + +- **Highlighter** loses nothing: `InkDrawingAttributes` already sets `IgnorePressure` for it. +- **Calligraphy** loses almost nothing: its width comes from *speed*, through + `AdjustPressure(pressure, _penInkSpeed)`, and speed is something a mouse has. A + calligraphic mouse stroke will still thin as it accelerates, which is the whole point of + the nib. +- **Pen** loses its taper. A mouse line is a uniform line. + +**Considered:** deriving pen pressure from speed as well, so a mouse stroke tapers. Rejected +as a default because the width would vary for a reason the hand cannot feel or control, and +a wobbling line that the user did not ask for reads as a bug. Worth revisiting as a +Preference if anyone asks. **Open question 4** below. + +### 4. Nothing shows what a click will do + +**The conflict.** The pen's hover indicators — the high-contrast dot, the laser halo and +comet, the dashed eraser square — exist because a pen has an "about to touch" state. +`InkSurface_PreviewMouseMove` deliberately calls `HidePointerDot()` and restores +`Cursors.Arrow` for a physical mouse. + +**Proposed, per tool:** + +- **Ink tools:** replace the arrow with the same small high-contrast dot the pen hover uses. + The arrow's hotspot is its tip, so it is not inaccurate, but its body covers the canvas + down and to the right of where the ink will land. This is arguable; the arrow is + defensible. **Open question 5.** +- **Eraser:** show the dashed `EraserHint` square, always, not only on hover. `EraserScreenRadius` + is 12 px at zoom 1 and has nothing to do with the shape of an arrow, so without it + nothing on screen says what a click will remove. +- **Laser:** no change. The pen's hover comet exists so the room can follow a pointer it + cannot otherwise see; a mouse arrow is already on the projector. Today's behaviour — + arrow while hovering, trail while dragging — is correct as it stands. +- **Select and Pan:** unchanged. `SelectCursorAt` already gives move and resize cursors. + +### 5. Eraser and Pan are not on the toolbar + +**The conflict.** `FingerToolsRow` holds exactly those two buttons and `ApplyFingerMode` +shows it only when finger drawing is effective — because with a pen, erasing is the rear +end and panning is touch or Space. A mouse has neither. + +**Proposed.** Show the same row when *either* finger drawing or mouse drawing is on, and +rename it to say so. No new toolbar chrome: the palette is deliberately small, and the two +buttons that mouse mode needs are the two that already exist for the same reason. + +`ApplyFingerMode` also forces the tool back to `_lastDrawingTool` when Eraser or Pan is +active and the row disappears. That guard has to consider both modes, or turning finger +drawing off would strand a mouse user on a tool whose button just vanished. + +### 6. The startup notice becomes wrong + +**The conflict.** `NoDigitizerWindow` says, in as many words, that there is no mouse +drawing yet, and links a vote for it. `SettingsCatalog`'s `WarnWhenNoDigitizer` description +repeats the claim. So do the site FAQ, guide and shortcut pages. + +**Proposed.** Keep the notice and change its job. It stops being an apology and becomes an +orientation: mouse drawing is on, the left button draws with the selected tool, `Ctrl` moves +things, and here is what a pen or touchscreen would add. Keep the "do not show this again" +box — it is still the way out of a false positive from the tablet list. + +Removing the notice entirely was considered. Against it: a whiteboard that silently behaves +differently on two machines is worse than one that says which mode it is in, and the list of +what a mouse cannot do is genuinely useful to someone deciding whether to buy a tablet. + +### 7. Detection is unreliable, so this cannot be automatic only + +**The conflict.** `NoDigitizerWindow.HasDrawingDevice()` and `HasStylusDigitizer()` both read +`Tablet.TabletDevices`. The codebase already warns twice — in `AppSettings` and in the +`FingerMode` help text — that this is a list of digitizers rather than an answer about what +is plugged in: a pen that has never been brought into range can be missing from it, and a +Surface reports a stylus whether or not one is in the room. + +**Proposed.** A setting, mirroring `FingerMode` exactly: + +``` +MouseMode.WhenNoDigitizer // default: on when Windows reports neither stylus nor touch +MouseMode.Off +MouseMode.On +``` + +Three reasons it is a setting and not pure detection: the probe is unreliable in both +directions; someone recording a demo may want mouse mode on a pen machine; and someone +whose Surface reports a stylus they do not own needs a way in. + +Note the deliberate asymmetry with finger drawing. On a touchscreen with no pen, +`FingerMode.WhenNoPen` turns finger drawing on and `MouseMode.WhenNoDigitizer` leaves mouse +drawing off — because there is already something to draw with. Mouse mode is the last +resort, not the second choice. + +### 8. Pen and mouse together + +**Not a conflict, and worth stating so.** With `MouseMode.On` on a machine that has a pen, +nothing about the pen changes. Every mouse handler already returns early on a non-null +`StylusDevice`, so pen-promoted mouse events never enter the mouse path, and `AppendPenInk` +never sees a mouse. `_lastContactWasPen`, palm rejection, the barrel-button recovery in +`AccumulateMaskedTipInk` and the whole of decision 22 are untouched. The two paths run +beside each other and never meet. + +This is the load-bearing fact of the whole proposal: mouse mode is an addition, not a +redesign. If it required one change to the pen path, it should be rejected. + +## The proposed behaviour, in full + +With **Mouse drawing** on: + +| Gesture | Behaviour | +| --- | --- | +| Left drag | The active tool. Pen, Highlighter and Calligraphy ink; Eraser erases whole strokes; Select moves and resizes; Pan pans; Laser draws the trail | +| Shift + left drag | Constrain the stroke to horizontal or vertical, at uniform width. Works already: the constraint is read per point from `Keyboard.Modifiers` | +| Ctrl + left drag | Select, move or resize one container, then return to the previous drawing tool — today's plain left-drag | +| Ctrl + left double-click | Center and fit the container, or frame the board | +| Left double-click | With Select or Pan: frames, as today. With an ink or eraser tool: two strokes | +| Middle drag, right drag, Space | Pan. Unchanged | +| Wheel | Zoom at the pointer. Unchanged | +| Right-click on the palette | Hide or show the palette. Unchanged | +| `Alt+L` | Laser. Unchanged | +| Toolbar | Eraser and Pan buttons appear, as they do for finger drawing | +| Cursor | A dot for ink tools, the dashed square for the eraser, today's cursors for Select and Pan | + +Everything else — undo and redo, containers, text, LiveView, import, paste, save, full +screen, the tab strip — is keyboard and menu work that already has no opinion about the +pointing device. + +## What it costs + +Said plainly, because the documentation will have to say it too: + +- **A mouse line has no taper.** Only Calligraphy still varies its width. +- **Handwriting with a mouse is bad.** This makes the application usable without a pen. It + does not make it good without one, and marketing should not imply otherwise. +- **Strokes are more angular** at speed, because of the event rate. Smoothing would change + the ink model for every device and is deliberately out of scope — **open question 8**. +- **The rear eraser, palm rejection, pressure and hover have no mouse equivalent** and are + not simulated. +- **A permanent tax on future input work**: every new input feature now has a third column + to fill in. The organizing rule above is what keeps that column mostly reading "n/a". + +## Implementation sketch + +Included so the cost can be judged, not as a plan of record. + +**`src/SQLBI.Whiteboard.Core/Settings/AppSettings.cs`** — a `MouseMode` enum and property, +a clause in `Normalize`, and `AppSettingsSerializer.CurrentVersion` from 12 to 13. + +**`src/SQLBI.Whiteboard/SettingsCatalog.cs`** — one `SettingDescriptor` in the Input +category, `EnumChoice`, three choices; and a rewrite of the `WarnWhenNoDigitizer` +description, which currently states that a mouse cannot draw. + +**`src/SQLBI.Whiteboard/MainWindow.xaml.cs`** — the whole of the behaviour: + +- `IsMouseModeEffective`, beside `IsFingerModeEffective`. +- `PointerAction.Ink` added to the enum; `PointerAction.Erase` becomes reachable from the + mouse for the first time. +- `InkSurface_PreviewMouseDown` branches on `EffectiveTool` when mouse mode is on, mirroring + `InkSurface_PreviewStylusDown`, with the `Ctrl` path preserving today's behaviour. +- `InkSurface_PreviewMouseMove` gains an `Ink` case that feeds `AppendPenInkPoint` — which + takes a screen point and a pressure and is already device-agnostic, so it should lose the + `Pen` in its name — and sets `SceneSurface.PendingStroke`. +- `InkSurface_PreviewMouseUp`, `CompleteMouseAction` and `InkSurface_LostMouseCapture` gain + an `Ink` case calling `EndPenInk`, and stop reverting the tool when mouse mode is on and + the gesture was not a `Ctrl` borrow. +- `ApplyFingerMode` becomes the shared "which extra tools are on the toolbar" routine. +- Cursor and eraser-hint handling for the mouse, per conflict 4. + +**`src/SQLBI.Whiteboard/MainWindow.xaml`** — rename `FingerToolsRow`. + +**`src/SQLBI.Whiteboard/NoDigitizerWindow.xaml`** — rewritten per conflict 6. + +**Documentation** — the README feature list and Controls table; `site/guide.html`, +`site/shortcuts.html`, `site/faq.html`; a new entry in `docs/decisions.md`. + +No new subsystem, no new project, no change to persistence or to `Core` beyond one setting. +The reason it is this small is decision 22: because pen ink is collected from raw points +rather than from the InkCanvas, the ink pipeline from `AppendPenInkPoint` through +`CommitInkPoints` to `BoardSurface.PendingStroke` already has no idea what device it is +serving. + +## Testing + +`SQLBI.Whiteboard.Core.SmokeTests` is package-free and UI-free, so it can cover the +device-agnostic parts and nothing else: settings round-tripping with the new enum, the +version-12-to-13 migration, and the pressure constant if the policy moves into `Core`. + +Everything else is manual, and the checklist should sit beside the Wacom Cintiq list in the +README: + +1. On a machine with no pen and no touchscreen, confirm the default turns the mode on. +2. Draw with each of Pen, Highlighter and Calligraphy; confirm width behaviour matches + conflict 3. +3. Shift mid-stroke, pressed and released, and confirm the constraint starts and ends there. +4. Erase; confirm the dashed square matches what is removed. +5. `Ctrl` + drag a container, and confirm the tool comes back. +6. Two quick dabs with the Pen, and confirm the board does not reframe. +7. On a pen machine with `MouseMode.On`, draw with the pen and confirm nothing about it + changed — this is the regression that matters. + +## Open questions + +Numbered so they can be answered one at a time. + +1. **Does the left button change meaning?** If not, there is no feature. Recommend yes. +2. **`Ctrl` + left for select-and-move, or make Select a tool you must pick from the + toolbar?** Recommend `Ctrl`: it keeps the one genuinely good thing about mouse input + today. +3. **Double-click framing behind `Ctrl` for ink tools?** Recommend yes; the alternative is a + board that reframes itself while someone is drawing. +4. **Constant pressure, or speed-derived pressure for the Pen?** Recommend constant, and + revisit only if someone asks. +5. **A dot cursor for ink tools, or keep the arrow?** Genuinely arguable. Recommend the dot. +6. **Default `WhenNoDigitizer`, or `Off` with an opt-in?** Recommend `WhenNoDigitizer`: the + person this is for is the one who downloaded the application, found nothing worked, and + will not go looking in Preferences. +7. **Does the no-digitizer notice stay?** Recommend yes, rewritten. +8. **Smoothing for mouse strokes?** Recommend not in a first version. It changes ink for + every device and deserves its own decision. +9. **Is the mouse allowed into the marketing?** Recommend no more than a line in the FAQ and + the shortcut page. The application stays a pen application that no longer fails silently + on a mouse. + +## Recommendation + +**Build it, as a fallback, and say what it is.** + +The case against is real and should be recorded: the founding constraint was that no device +imitates another, a mouse whiteboard is a worse whiteboard, and supporting one invites +requests from people the product was not built for. That last cost is permanent, and it is +the one to watch. + +The case for is stronger on the specifics. The cost of building it is unusually low — a few +hundred lines, one setting, no new subsystem, and not one line changed in the pen path — +because the ink pipeline was already made device-agnostic for an unrelated reason. The +alternative is not "keep the constraint pure"; it is "keep shipping a startup dialog that +apologizes". And the constraint survives intact if the organizing rule is written into +decisions.md alongside the feature: a mouse gets the tools, not the gestures. + +If any of the load-bearing pieces fails on inspection — if the pen path has to change, or if +the left button cannot be given up — that is the signal to stop, and the honest answer to +discussion 78 becomes no. diff --git a/site/faq.html b/site/faq.html index 2e08811..d28d662 100644 --- a/site/faq.html +++ b/site/faq.html @@ -57,7 +57,7 @@

Windows 11 or Windows 10?

Built for Windows 11. It runs on Windows 10 version 2004 or later, 64-bit. That is the same floor as Windows Graphics Capture, which LiveView uses.

Is a pen required?

-

For ink, yes, unless Finger drawing is on. On a computer where Windows reports neither a pen tablet nor a touchscreen, a notice at startup says so and points to discussion 78, which collects votes for mouse-only drawing; the application still opens. A mouse and touch pan, zoom, and move containers. A keyboard runs the rest. The rear eraser and palm rejection still need a pen. New installs default Finger drawing to When no pen is detected, which looks at the digitizer list Windows reports, not whether a pen is in the room. On makes one finger use the current tool; two fingers still pan and pinch-zoom, and Eraser and Pan appear on the toolbar. The shortcut list is grouped by pen, mouse, touch, and keys.

+

No, though it is what the application is built for. Mouse drawing makes the left button use the selected tool, and Finger drawing makes one finger do the same; Preferences control when each turns on. The shortcut list is grouped by pen, mouse, touch, and keys.

Mac, iPad, or a browser?

No. It is a Windows application. Boards are files you can keep next to the material they document; they do not open in a browser.

diff --git a/site/guide.html b/site/guide.html index b1fcb3b..22f31b6 100644 --- a/site/guide.html +++ b/site/guide.html @@ -660,14 +660,14 @@

Ink and tools

Pan - Appears with Finger drawing on, so one finger can still move the board. + Appears with Finger drawing or Mouse drawing on, so a finger or the mouse can still move the board.
-

Palm rejection suspends touch while the pen is down, so a resting hand does not pan the board. Without a pen, turn on Finger drawing to reach Eraser and Pan on the palette. The toolbar stays small on purpose; Preferences control whether it sits in a corner or follows a layout you prefer, and right-clicking it hides it altogether.

+

Palm rejection suspends touch while the pen is down, so a resting hand does not pan the board. Without a pen, Finger drawing or Mouse drawing brings Eraser and Pan to the palette. The toolbar stays small on purpose; Preferences control whether it sits in a corner or follows a layout you prefer, and right-clicking it hides it altogether.

Navigation

-

The canvas is unbounded. Finger drawing changes what a single finger does and nothing else.

+

The canvas is unbounded. Finger drawing changes what a single finger does; Mouse drawing moves double-click framing onto Ctrl. Nothing else changes.

@@ -685,8 +685,11 @@

Navigation

The window can sit in a narrow strip beside another app. F11 fills the monitor and hides the title and tabs. Ctrl+F11 hides the same chrome and keeps the window where it is. Escape leaves either unless a text container is being edited.

+

Drawing with a mouse

+

Preferences → Mouse drawing turns the left button into the selected tool, and defaults to on where Windows reports neither a pen tablet nor a touchscreen. The tool then stays selected instead of being handed back, and Ctrl with the left button is how you move and resize a container. A mouse reports no pressure, so ink is drawn at an even width; Calligraphy still varies, because its width comes from speed. Nothing about the pen changes, so this can be left on beside one.

+

Containers

-

Images, LiveViews, and text blocks are containers. With a mouse, click to move, drag the circular handle to resize while keeping aspect ratio. Releasing the mouse returns to the drawing tool you had. Imported images accept PNG, JPEG, BMP, GIF, and SVG, including clipboard paste and Explorer drag-and-drop. An SVG is kept as its markup and redrawn at every size, so enlarging one costs it nothing.

+

Images, LiveViews, and text blocks are containers. With a mouse, click to move, drag the circular handle to resize while keeping aspect ratio — hold Ctrl too with Mouse drawing on. Releasing the mouse returns to the drawing tool you had. Imported images accept PNG, JPEG, BMP, GIF, and SVG, including clipboard paste and Explorer drag-and-drop. An SVG is kept as its markup and redrawn at every size, so enlarging one costs it nothing.

View → Bring to front and View → Send to back reorder the selected container and its linked strokes. Strokes cannot be reordered on their own.

Text, SQL, and DAX

diff --git a/site/shortcuts.html b/site/shortcuts.html index ca899ae..6d954da 100644 --- a/site/shortcuts.html +++ b/site/shortcuts.html @@ -90,7 +90,9 @@

MovementA physical mouse shows the normal arrow.
-
Left buttonSelect, move, or resize a container. Release returns to the previous drawing tool.
+
Left buttonSelect, move, or resize a container. Release returns to the previous drawing tool. With Mouse drawing on, uses the current tool instead.
+
Ctrl+Left buttonWith Mouse drawing on, select, move, or resize a container. Double-click with Ctrl frames one.
+
Mouse drawingNew installs default to When there is no pen or touchscreen in Preferences. On shows Eraser and Pan on the toolbar.
Resize handleDrag the circular bottom-right handle. Aspect ratio is preserved.
WheelZoom at the pointer. Hold Shift for a slower zoom.
Middle buttonPan.
diff --git a/src/SQLBI.Whiteboard.Core/Settings/AppSettings.cs b/src/SQLBI.Whiteboard.Core/Settings/AppSettings.cs index c1f70c0..6d70972 100644 --- a/src/SQLBI.Whiteboard.Core/Settings/AppSettings.cs +++ b/src/SQLBI.Whiteboard.Core/Settings/AppSettings.cs @@ -68,6 +68,37 @@ public enum FingerMode WhenNoPen = 2, } +/// +/// Whether the mouse draws. It is a setting rather than pure detection because +/// the digitizer list is unreliable in both directions, and because a mouse +/// gets the tools, not the gestures: turning this on changes what the left +/// button means, and that is the kind of change a person should be able to +/// refuse. +/// +public enum MouseMode +{ + /// + /// The left button borrows Select for one gesture and hands the tool back. + /// The mouse pans, zooms, and moves containers, and does not draw. + /// + Off = 0, + + /// + /// The left button does what the active tool does. The pen path is + /// untouched, so this is safe to turn on beside a pen. + /// + On = 1, + + /// + /// Default for a new setup: treat as On only when Windows reports neither a + /// stylus nor a touchscreen. Deliberately stricter than + /// - a touchscreen with no pen already + /// has something to draw with, so mouse drawing is the last resort rather + /// than the second choice. + /// + WhenNoDigitizer = 2, +} + public sealed class AppSettings { public int Version { get; set; } = AppSettingsSerializer.CurrentVersion; @@ -84,6 +115,8 @@ public sealed class AppSettings public FingerMode FingerMode { get; set; } = FingerMode.WhenNoPen; + public MouseMode MouseMode { get; set; } = MouseMode.WhenNoDigitizer; + public List SnippetFormatOrder { get; set; } = [.. TextLanguageIds.All]; public InkToolSettings Pen { get; set; } = InkToolSettings.From(InkPalettes.DefaultPen); @@ -120,7 +153,7 @@ public sealed class AppSettings public static class AppSettingsSerializer { - public const int CurrentVersion = 12; + public const int CurrentVersion = 13; private static readonly JsonSerializerOptions JsonOptions = new() { @@ -198,6 +231,11 @@ private static AppSettings Normalize(AppSettings? settings) settings.FingerMode = FingerMode.WhenNoPen; } + if (!Enum.IsDefined(settings.MouseMode)) + { + settings.MouseMode = MouseMode.WhenNoDigitizer; + } + if (settings.StartupMonitor == StartupMonitorKind.Named) { if (string.IsNullOrWhiteSpace(settings.StartupMonitorName)) diff --git a/src/SQLBI.Whiteboard/MainWindow.xaml b/src/SQLBI.Whiteboard/MainWindow.xaml index 4dda8e8..afd43c9 100644 --- a/src/SQLBI.Whiteboard/MainWindow.xaml +++ b/src/SQLBI.Whiteboard/MainWindow.xaml @@ -72,6 +72,7 @@ StylusLeave="InkSurface_StylusLeave" PreviewMouseDown="InkSurface_PreviewMouseDown" PreviewMouseMove="InkSurface_PreviewMouseMove" + MouseLeave="InkSurface_MouseLeave" PreviewMouseUp="InkSurface_PreviewMouseUp" PreviewMouseWheel="InkSurface_PreviewMouseWheel" LostMouseCapture="InkSurface_LostMouseCapture" @@ -464,7 +465,7 @@ - diff --git a/src/SQLBI.Whiteboard/MainWindow.xaml.cs b/src/SQLBI.Whiteboard/MainWindow.xaml.cs index 89e2f93..37a7deb 100644 --- a/src/SQLBI.Whiteboard/MainWindow.xaml.cs +++ b/src/SQLBI.Whiteboard/MainWindow.xaml.cs @@ -46,6 +46,7 @@ private enum PointerAction Pan, Container, Laser, + Ink, } private readonly Camera2D _camera = new(); @@ -80,6 +81,7 @@ private enum PointerAction private PenStyle _penStyle = InkPalettes.DefaultPen; private PointerAction _stylusAction; private PointerAction _mouseAction; + private bool _mouseToolBorrowed; private PointD _lastPanPoint; private bool _penInContact; private bool _touchNavigationLocked; @@ -175,7 +177,7 @@ public MainWindow(string? initialBoardPath) ApplyLaserSettings(); ApplyToolbarPlacement(); ApplyCalligraphyAccess(); - ApplyFingerMode(); + ApplyPointerModes(); ApplyDrawingAttributes(); SetActiveTool(BoardTool.Pen); InkSurface.Focus(); @@ -204,8 +206,9 @@ private async void MainWindow_Loaded(object sender, RoutedEventArgs e) } } - // A whiteboard with nothing to draw on it is worth saying out loud, rather - // than leaving someone to work out why the pen tools do nothing. + // Which pointing device this session is drawing with is worth saying out + // loud, rather than leaving someone to work out from the toolbar that the + // left button has taken on a job it does not have on a pen machine. private void WarnWhenNothingToDrawWith() { if (!_settings.WarnWhenNoDigitizer || NoDigitizerWindow.HasDrawingDevice()) @@ -213,7 +216,7 @@ private void WarnWhenNothingToDrawWith() return; } - var notice = new NoDigitizerWindow { Owner = this }; + var notice = new NoDigitizerWindow(IsMouseModeEffective) { Owner = this }; notice.ShowDialog(); if (!notice.DoNotShowAgain) { @@ -325,7 +328,7 @@ private void AppendPenInk(StylusEventArgs e) } pressed = true; - AppendPenInkPoint(new PointD(point.X, point.Y), point.PressureFactor); + AppendInkPoint(new PointD(point.X, point.Y), point.PressureFactor); } if (pressed) @@ -349,7 +352,11 @@ private void AppendPenInk(StylusEventArgs e) EndPenInk(); } - private void AppendPenInkPoint(PointD screen, float pressure) + // Shared by the pen and the mouse: it takes a screen point and a pressure, + // and has no opinion about where either came from. The straight-line + // constraint and the calligraphy dynamics live here, which is why the mouse + // gets both without a second implementation. + private void AppendInkPoint(PointD screen, float pressure) { var constrained = StraightLineConstraintActive; if (!constrained) @@ -1140,6 +1147,59 @@ private static bool IsBarrelButton(StylusDevice device, StylusButton button) private const float MouseLaserPressure = 0.5f; + // A mouse reports a button, not a pressure. This is the neutral value the + // straight-line constraint already draws at: exactly the configured + // thickness, with no taper at either end. Calligraphy still varies its + // width, because that comes from speed rather than from this number. + private const float MousePressure = 0.5f; + + /// + /// Whether the left button does what the active tool does. A mouse gets the + /// tools, not the gestures: nothing here simulates pressure, hover, the + /// reverse end or the barrel button, and the pen path is untouched. + /// + private bool IsMouseModeEffective => + _settings.MouseMode switch + { + MouseMode.On => true, + MouseMode.WhenNoDigitizer => !NoDigitizerWindow.HasDrawingDevice(), + _ => false, + }; + + private void BeginMouseInk(PointD screen) + { + EndPenInk(); + AppendInkPoint(screen, MousePressure); + } + + private void UpdateMouseInk(PointD screen) + { + AppendInkPoint(screen, MousePressure); + SceneSurface.PendingStroke = _penInk; + SceneSurface.PendingStrokeStyle = _penStyle; + SceneSurface.InvalidateVisual(); + } + + // A click that never moves is a dot, and a dot still has to be a stroke with + // some length: points in the same place enclose nothing to render. A pen tap + // does not have this problem - it reports a burst of packets, and no hand is + // that still. The nudge is a hundredth of a screen pixel at the current + // zoom, so what appears is the nib and nothing wider. + private void EndMouseInk() + { + if (_penInk.Count > 0 && _penInk.TrueForAll(point => point.Position == _penInk[0].Position)) + { + var nudge = 0.01 / Math.Max(_camera.Zoom, 0.0001); + var last = _penInk[^1]; + _penInk.Add(last with + { + Position = new PointD(last.Position.X + nudge, last.Position.Y + nudge), + }); + } + + EndPenInk(); + } + private void InkSurface_PreviewMouseDown(object sender, MouseButtonEventArgs e) { if (e.StylusDevice is not null) @@ -1159,15 +1219,33 @@ private void InkSurface_PreviewMouseDown(object sender, MouseButtonEventArgs e) CommitTextEdit(); InkSurface.Focus(); var screen = ToPointD(e.GetPosition(InkSurface)); + var mouseDraws = IsMouseModeEffective; + + // Ctrl is the old mouse. Letting the left button draw takes away the one + // genuinely good thing about mouse input - moving an image without + // leaving the Pen - so it is handed straight back on a modifier rather + // than lost. With mouse drawing off, every left gesture is this one. + var borrowSelect = !mouseDraws || Keyboard.Modifiers.HasFlag(ModifierKeys.Control); + if (e.ChangedButton is MouseButton.Middle or MouseButton.Right) { _lastPanPoint = screen; _mouseAction = PointerAction.Pan; + + // A sticky tool is not given back afterwards: reverting would take + // someone who chose the Eraser and panned with the right button and + // quietly leave them holding a pen. + _mouseToolBorrowed = !mouseDraws; Mouse.Capture(InkSurface); e.Handled = true; } - else if (e.ChangedButton == MouseButton.Left && e.ClickCount >= 2) + else if (e.ChangedButton == MouseButton.Left && + e.ClickCount >= 2 && + (borrowSelect || EffectiveTool is BoardTool.Select or BoardTool.Pan)) { + // Two quick dabs with an ink tool are two strokes, not a request to + // reframe the board, so framing moves behind Ctrl exactly where the + // left button has something else to do. FrameContentAt(screen); e.Handled = true; } @@ -1175,14 +1253,22 @@ private void InkSurface_PreviewMouseDown(object sender, MouseButtonEventArgs e) { BeginLaserContact(e.GetPosition(LaserTrail), MouseLaserPressure); _mouseAction = PointerAction.Laser; + _mouseToolBorrowed = false; Mouse.Capture(InkSurface); e.Handled = true; } - else if (e.ChangedButton == MouseButton.Left) + else if (e.ChangedButton == MouseButton.Left && borrowSelect) { SetActiveTool(BoardTool.Select); BeginContainerGesture(screen); _mouseAction = PointerAction.Container; + _mouseToolBorrowed = true; + Mouse.Capture(InkSurface); + e.Handled = true; + } + else if (e.ChangedButton == MouseButton.Left) + { + BeginMouseAction(screen); Mouse.Capture(InkSurface); e.Handled = true; } @@ -1193,6 +1279,38 @@ private void InkSurface_PreviewMouseDown(object sender, MouseButtonEventArgs e) } } + // The left button under mouse drawing, branching on the tool exactly as + // InkSurface_PreviewStylusDown does for the pen. The tool is sticky here: + // there is nothing to hand it back to. + private void BeginMouseAction(PointD screen) + { + _mouseToolBorrowed = false; + if (EffectiveTool != BoardTool.Select) + { + ClearSelection(); + } + + switch (EffectiveTool) + { + case BoardTool.Eraser: + BeginErase(screen); + _mouseAction = PointerAction.Erase; + break; + case BoardTool.Pan: + _lastPanPoint = screen; + _mouseAction = PointerAction.Pan; + break; + case BoardTool.Select: + BeginContainerGesture(screen); + _mouseAction = PointerAction.Container; + break; + default: + BeginMouseInk(screen); + _mouseAction = PointerAction.Ink; + break; + } + } + private void ToolPalette_PreviewMouseRightButtonDown( object sender, MouseButtonEventArgs e) @@ -1227,13 +1345,17 @@ private void InkSurface_PreviewMouseMove(object sender, MouseEventArgs e) return; } - HidePointerDot(); var screen = ToPointD(e.GetPosition(InkSurface)); + var mouseDraws = IsMouseModeEffective; if (EffectiveTool == BoardTool.Laser) { + HidePointerDot(); + // The laser hides the cursor for pen input. A physical mouse still // gets the arrow, matching pen, highlighter, and calligraphy. Hide - // it only while the mouse is drawing a trail. + // it only while the mouse is drawing a trail. The pen's hover comet + // is not borrowed here: it exists so the room can follow a pointer + // it cannot otherwise see, and a mouse arrow is already on screen. if (_mouseAction == PointerAction.Laser) { InkSurface.Cursor = Cursors.None; @@ -1246,11 +1368,29 @@ private void InkSurface_PreviewMouseMove(object sender, MouseEventArgs e) } else if (EffectiveTool == BoardTool.Select && _mouseAction == PointerAction.None) { + HidePointerDot(); UpdateSelectHover(screen); InkSurface.Cursor = SelectCursorAt(screen); } + else if (mouseDraws && EffectiveTool == BoardTool.Eraser) + { + // What a click would erase is a patch of board rather than a point, + // and the patch has nothing to do with the shape of an arrow. + PointerDot.Visibility = Visibility.Collapsed; + ShowEraserHint(e.GetPosition(RootGrid)); + InkSurface.Cursor = Cursors.None; + } + else if (mouseDraws && IsInkTool) + { + // The arrow's hotspot is its tip, so it is not inaccurate - but its + // body covers the canvas the ink is about to land on. + HideEraserHint(); + ShowPointerDot(e.GetPosition(RootGrid)); + InkSurface.Cursor = Cursors.None; + } else { + HidePointerDot(); InkSurface.Cursor = Cursors.Arrow; } @@ -1268,6 +1408,20 @@ private void InkSurface_PreviewMouseMove(object sender, MouseEventArgs e) case PointerAction.Laser: UpdateLaser(e.GetPosition(LaserTrail), leaveTrail: true, MouseLaserPressure); break; + case PointerAction.Ink: + UpdateMouseInk(screen); + break; + } + } + + // Nothing else clears the mouse's own pointer dot or eraser patch: the + // hover watchdog is the pen's, and a mouse that has moved onto the toolbar + // simply stops reporting. + private void InkSurface_MouseLeave(object sender, MouseEventArgs e) + { + if (e.StylusDevice is null && _mouseAction == PointerAction.None) + { + HidePointerDot(); } } @@ -1309,20 +1463,35 @@ private void InkSurface_LostMouseCapture(object sender, MouseEventArgs e) StopLaserSampling(); LaserTrail.Lift(); } + else if (_mouseAction == PointerAction.Ink) + { + EndMouseInk(); + } _mouseAction = PointerAction.None; - if (hadMouseAction && EffectiveTool != BoardTool.Laser) + if (!hadMouseAction) + { + // CompleteMouseAction releases the capture itself and is still + // mid-way through deciding what to do with the tool. Nothing here + // may touch that decision. + return; + } + + if (_mouseToolBorrowed && EffectiveTool != BoardTool.Laser) { SetActiveTool(_lastDrawingTool); } - else if (hadMouseAction) + else if (EffectiveTool == BoardTool.Laser) { InkSurface.Cursor = Cursors.Arrow; } + + _mouseToolBorrowed = false; } private void CompleteMouseAction(PointD screen) { + var borrowed = _mouseToolBorrowed; switch (_mouseAction) { case PointerAction.Erase: @@ -1340,6 +1509,10 @@ private void CompleteMouseAction(PointD screen) StopLaserSampling(); LaserTrail.Lift(); break; + case PointerAction.Ink: + UpdateMouseInk(screen); + EndMouseInk(); + break; } _mouseAction = PointerAction.None; @@ -1348,14 +1521,16 @@ private void CompleteMouseAction(PointD screen) Mouse.Capture(null); } - if (EffectiveTool != BoardTool.Laser) + if (borrowed && EffectiveTool != BoardTool.Laser) { SetActiveTool(_lastDrawingTool); } - else + else if (EffectiveTool == BoardTool.Laser) { InkSurface.Cursor = Cursors.Arrow; } + + _mouseToolBorrowed = false; } private void InkSurface_PreviewMouseWheel(object sender, MouseWheelEventArgs e) @@ -1417,21 +1592,27 @@ private static bool HasStylusDigitizer() return false; } - private void ApplyFingerMode() + // Finger drawing and mouse drawing are separate settings that need the same + // two toolbar buttons, for the same reason: erasing is the pen's reverse end + // and panning is touch or Space, and a device with neither has nowhere else + // to reach them. + private void ApplyPointerModes() { - var enabled = IsFingerModeEffective; - if (!enabled) + var fingerInk = IsFingerModeEffective; + if (!fingerInk) { CancelFingerTool(); } - InkSurface.SetAllowTouchInk(enabled); - if (FingerToolsRow is not null) + InkSurface.SetAllowTouchInk(fingerInk); + + var extraTools = fingerInk || IsMouseModeEffective; + if (ExtraToolsRow is not null) { - FingerToolsRow.Visibility = enabled ? Visibility.Visible : Visibility.Collapsed; + ExtraToolsRow.Visibility = extraTools ? Visibility.Visible : Visibility.Collapsed; } - if (!enabled && _activeTool is BoardTool.Eraser or BoardTool.Pan) + if (!extraTools && _activeTool is BoardTool.Eraser or BoardTool.Pan) { SetActiveTool(_lastDrawingTool); } @@ -3115,7 +3296,7 @@ private void ApplyPreferences() ApplyToolbarPlacement(); ApplyCalligraphyAccess(); ApplyLaserSettings(); - ApplyFingerMode(); + ApplyPointerModes(); if (!_settings.CheckForUpdates) { SessionBar.HideUpdateNotice(); @@ -4934,6 +5115,7 @@ private void Window_Deactivated(object? sender, EventArgs e) _stylusAction = PointerAction.None; _mouseAction = PointerAction.None; + _mouseToolBorrowed = false; _penInContact = false; _syntheticLaserContact = false; _barrelButton = null; diff --git a/src/SQLBI.Whiteboard/NoDigitizerWindow.xaml b/src/SQLBI.Whiteboard/NoDigitizerWindow.xaml index 85aee60..0140b95 100644 --- a/src/SQLBI.Whiteboard/NoDigitizerWindow.xaml +++ b/src/SQLBI.Whiteboard/NoDigitizerWindow.xaml @@ -21,25 +21,20 @@ FontSize="13" Foreground="#FF374151" TextWrapping="Wrap" - Text="SQLBI Whiteboard is built for drawing with a pen or a finger. Windows reports no pen tablet and no touchscreen on this computer, so there is nothing here to draw with." /> - + - + - - - - - - + TextWrapping="Wrap" + Text="A mouse reports no pressure, so a line is drawn at an even width — Calligraphy is the one tool that still varies, because its width comes from speed. A pen would add pressure, the rear eraser, palm rejection, and the hover indicator that shows what a tap would do." /> /// Shown once at startup when Windows reports neither a pen tablet nor a -/// touchscreen. The application still opens - everything that does not need ink -/// works with a mouse and the keyboard - so this says what is missing rather -/// than refusing to start. +/// touchscreen. The application still opens, and with Mouse drawing on it can +/// be drawn on, so this says which mode the session is in and what a pen would +/// add rather than apologizing for the machine. /// public partial class NoDigitizerWindow : Window { - public NoDigitizerWindow() => InitializeComponent(); + public NoDigitizerWindow(bool mouseDrawingEnabled) + { + InitializeComponent(); + ModeText.Text = mouseDrawingEnabled + ? "Mouse drawing is on, so the left button uses the tool selected on the toolbar. Hold Ctrl and drag to move or resize a container, and Ctrl with a double-click to fit one to the canvas." + : "Mouse drawing is turned off, so a mouse pans, zooms, and moves containers, but does not draw. Turn it on under Help > Preferences > Input."; + LimitsText.Visibility = mouseDrawingEnabled ? Visibility.Visible : Visibility.Collapsed; + } /// /// Whether the notice was dismissed for good. Also the way out of a false @@ -23,10 +28,10 @@ public partial class NoDigitizerWindow : Window public bool DoNotShowAgain => DoNotShowAgainBox.IsChecked == true; /// - /// Whether Windows reports anything to draw with. The same tablet list the - /// Finger drawing setting reads, which is a list of digitizers rather than - /// an answer about what is plugged in right now: a pen that has never been - /// brought into range can be absent from it. + /// Whether Windows reports anything to draw with by hand. The same tablet + /// list the Finger drawing setting reads, which is a list of digitizers + /// rather than an answer about what is plugged in right now: a pen that has + /// never been brought into range can be absent from it. /// public static bool HasDrawingDevice() { @@ -50,14 +55,5 @@ private void Window_PreviewKeyDown(object sender, KeyEventArgs e) } } - private void DiscussionLink_RequestNavigate(object sender, RequestNavigateEventArgs e) - { - Process.Start(new ProcessStartInfo(e.Uri.AbsoluteUri) - { - UseShellExecute = true, - }); - e.Handled = true; - } - private void ContinueButton_Click(object sender, RoutedEventArgs e) => Close(); } diff --git a/src/SQLBI.Whiteboard/PreferencesWindow.xaml.cs b/src/SQLBI.Whiteboard/PreferencesWindow.xaml.cs index ebaf79f..8ac9424 100644 --- a/src/SQLBI.Whiteboard/PreferencesWindow.xaml.cs +++ b/src/SQLBI.Whiteboard/PreferencesWindow.xaml.cs @@ -890,6 +890,7 @@ private string CurrentEnumId(SettingDescriptor setting) => SettingsCatalog.Ids.ToolbarPlacement => _settings.ToolbarPlacement.ToString(), SettingsCatalog.Ids.ToolbarLayout => _settings.CalligraphyAccess.ToString(), SettingsCatalog.Ids.FingerMode => _settings.FingerMode.ToString(), + SettingsCatalog.Ids.MouseMode => _settings.MouseMode.ToString(), SettingsCatalog.Ids.PenButton => _settings.PenButtons.Barrel.ToString(), _ => string.Empty, }; @@ -970,6 +971,10 @@ when Enum.TryParse(id, out var access): when Enum.TryParse(id, out var fingerMode): _settings.FingerMode = fingerMode; break; + case SettingsCatalog.Ids.MouseMode + when Enum.TryParse(id, out var mouseMode): + _settings.MouseMode = mouseMode; + break; case SettingsCatalog.Ids.PenButton when Enum.TryParse(id, out var penButton): _settings.PenButtons.Barrel = penButton; diff --git a/src/SQLBI.Whiteboard/SettingsCatalog.cs b/src/SQLBI.Whiteboard/SettingsCatalog.cs index ad4a80a..642e546 100644 --- a/src/SQLBI.Whiteboard/SettingsCatalog.cs +++ b/src/SQLBI.Whiteboard/SettingsCatalog.cs @@ -84,6 +84,7 @@ public static class Ids public const string ToolbarLayout = "toolbar.layout"; public const string WarnWhenNoDigitizer = "startup.noDigitizerNotice"; public const string FingerMode = "input.fingerMode"; + public const string MouseMode = "input.mouseMode"; public const string PenButton = "input.penButton"; public const string SnippetFormatOrder = "input.snippetFormatOrder"; public const string CheckForUpdates = "updates.check"; @@ -123,7 +124,7 @@ public static class Ids Id = Ids.WarnWhenNoDigitizer, Category = Startup, Title = "Warn when there is nothing to draw with", - Description = "Say so at startup when Windows reports neither a pen tablet nor a touchscreen. The application opens either way; a mouse pans, zooms, and moves containers, but cannot draw. What Windows reports is a list of digitizers rather than what is plugged in, so a pen that has never been brought into range can be missing from it.", + Description = "Say so at startup when Windows reports neither a pen tablet nor a touchscreen, and describe what Mouse drawing does and does not give you in place of a pen. What Windows reports is a list of digitizers rather than what is plugged in, so a pen that has never been brought into range can be missing from it.", Keywords = ["pen", "touch", "touchscreen", "digitizer", "tablet", "mouse", "warning", "notice", "startup"], Editor = SettingEditorKind.BooleanSwitch, }, @@ -143,6 +144,21 @@ public static class Ids ], }, new() + { + Id = Ids.MouseMode, + Category = Input, + Title = "Mouse drawing", + Description = "New installs default to When there is no pen or touchscreen. Off keeps the left button for moving containers, which is what it has always done. On makes the left button use the current tool, and puts Eraser and Pan on the toolbar; Ctrl and the left button then move and resize a container, and Ctrl with a double-click frames one. A mouse reports no pressure, so only Calligraphy still varies its width, and nothing about the pen changes when this is on.", + Keywords = ["mouse", "draw", "drawing", "pointer", "no pen", "digitizer", "left button", "ctrl"], + Editor = SettingEditorKind.EnumChoice, + Choices = + [ + new() { Id = nameof(Core.Settings.MouseMode.WhenNoDigitizer), Title = "When there is no pen or touchscreen" }, + new() { Id = nameof(Core.Settings.MouseMode.Off), Title = "Off" }, + new() { Id = nameof(Core.Settings.MouseMode.On), Title = "On" }, + ], + }, + new() { Id = Ids.PenButton, Category = Input, diff --git a/tests/SQLBI.Whiteboard.Core.SmokeTests/Program.cs b/tests/SQLBI.Whiteboard.Core.SmokeTests/Program.cs index c0528a2..df458d7 100644 --- a/tests/SQLBI.Whiteboard.Core.SmokeTests/Program.cs +++ b/tests/SQLBI.Whiteboard.Core.SmokeTests/Program.cs @@ -856,6 +856,33 @@ defaultSettings.StartupMonitorName is null && Assert( fingerModeRoundTrip.FingerMode == FingerMode.WhenNoPen, "Settings JSON should round-trip when-no-pen finger drawing."); +Assert( + defaultSettings.MouseMode == MouseMode.WhenNoDigitizer, + "Missing settings should default mouse drawing to when there is no digitizer."); +Assert( + AppSettingsSerializer.Parse("{ \"mouseMode\": \"Sideways\" }").MouseMode == + MouseMode.WhenNoDigitizer, + "Unknown mouse-mode values should fall back to when-no-digitizer."); +Assert( + AppSettingsSerializer.Parse("{ \"mouseMode\": \"Off\" }").MouseMode == MouseMode.Off && + AppSettingsSerializer.Parse("{ \"mouseMode\": \"On\" }").MouseMode == MouseMode.On, + "Saved mouse drawing Off and On should still load."); +var mouseModeRoundTrip = AppSettingsSerializer.Parse( + AppSettingsSerializer.Format(new AppSettings { MouseMode = MouseMode.On })); +Assert( + mouseModeRoundTrip.MouseMode == MouseMode.On, + "Settings JSON should round-trip mouse drawing."); +// Settings written before mouse drawing existed carry no mouseMode at all, and +// have to arrive as the new default rather than as Off - the whole point is the +// person who found nothing worked. +var settingsFromVersion12 = AppSettingsSerializer.Parse( + "{ \"version\": 12, \"fingerMode\": \"Off\", \"toolbarPlacement\": \"BottomLeft\" }"); +Assert( + settingsFromVersion12.MouseMode == MouseMode.WhenNoDigitizer && + settingsFromVersion12.FingerMode == FingerMode.Off && + settingsFromVersion12.ToolbarPlacement == ToolbarPlacement.BottomLeft && + settingsFromVersion12.Version == AppSettingsSerializer.CurrentVersion, + "Settings saved before mouse drawing existed should upgrade and keep their own choices."); Assert( defaultSettings.SnippetFormatOrder is ["plain", "dax", "sqlserver"], "Missing settings should keep Plain text first so paste stays plain text.");