Skip to content

Commit e169783

Browse files
marcosqlbiclaude
andauthored
Offer mouse drawing when the mouse reaches for the toolbar (#82)
1.2.0 left Mouse drawing discoverable only in Preferences, which is the one place someone who does not know the feature exists will not look. The signal that they might want it is already in the application: **picking a tool from the toolbar with the mouse.** A pen user reaches for the palette with the pen, so a mouse arriving there is someone whose next stroke is going to disappoint them. ## The dialog `MouseModeOfferWindow` — "Draw with a mouse?", an unchecked **Don't show me this again**, and **Enable mouse mode** / **Cancel**. The offer is a question, not a confirmation, and the two answers are not interchangeable: one changes what the left button means. So: - **Neither button is `IsDefault` or `IsCancel`, and Enter is swallowed.** A focused non-default `Button` responding to Enter is version- and style-dependent, so rather than rely on it not firing, `Window_PreviewKeyDown` marks *every* Enter handled — it tunnels from the Window before any button's `KeyDown` — and invokes a button only when `Keyboard.FocusedElement` is one of ours. Enter with nothing focused, or with the checkbox focused, does nothing and leaves the dialog open. - **Escape closes**, because dismissing is always safe. - **Nothing is focused on open.** `Keyboard.Focus(this)` in `Loaded`, so the first Tab reaches the checkbox rather than a primed button — otherwise the checkbox takes focus and a stray Space answers the wrong question. ## Three judgment calls - **Once a session.** Prompting on every toolbar click would be intolerable; prompting once ever would lose someone not ready to decide. A Cancel answers this session, the checkbox answers every one after it. - **The offer is queued at background priority, not shown from the click handler**, so the click first does what it came to do. The tool is selected, and the dialog then explains why it may not behave as expected. - **A `SuggestMouseMode` setting** (settings version 13 → 14) with a Preferences entry under Input, because "don't show again" is otherwise a one-way door. `WarnWhenNoDigitizer` sets the precedent. **Enable mouse mode** sets `MouseMode.On` rather than `WhenNoDigitizer`, because the offer can only have appeared where the automatic default already decided not to. Decision 24 records all of it. Version is 1.2.1. ## Verification Build clean, Core smoke tests pass including the settings round-trip and the 13 → 14 upgrade. Those tests are UI-free, so the dialog's behaviour is not covered by them; the README gains a second validation list under Mouse drawing validation, and every item on it is about a key doing nothing — the part a later change is most likely to undo. A slow-shutdown regression was investigated against this branch and did not reproduce on the maintainer's machine. A clean launch-and-close measures 85–358 ms. No change was made for it. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
1 parent e9f1145 commit e169783

13 files changed

Lines changed: 279 additions & 6 deletions

File tree

Directory.Build.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
scripts/build-installer.ps1 both read it from here, so releasing is a reviewed change
66
to this line rather than an edit in a pipeline variable group.
77
-->
8-
<VersionPrefix>1.2.0</VersionPrefix>
8+
<VersionPrefix>1.2.1</VersionPrefix>
99
<LangVersion>latest</LangVersion>
1010
<Nullable>enable</Nullable>
1111
<ImplicitUsings>enable</ImplicitUsings>

README.md

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ How the project is developed and shipped is documented separately:
1111

1212
- Low-latency, pressure-aware WPF wet ink, including rear-eraser detection on any pen that reports it
1313
- 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
14-
- 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
14+
- 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. With it off, picking a tool from the toolbar with the mouse offers to turn it on, once a session
1515
- Touch panning and two-finger pinch zoom
1616
- 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
1717
- 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
@@ -296,3 +296,19 @@ touching any mouse handler:
296296
8. Select the Eraser, pan with the right button, and confirm the Eraser is still selected.
297297
9. On a pen machine with Mouse drawing **On**, draw with the pen and confirm nothing about
298298
it changed. This is the regression that matters.
299+
300+
With Mouse drawing **Off**, the offer has its own list. Every one of these is about a key
301+
doing nothing, which is the part a later change is most likely to undo:
302+
303+
1. Pick a tool from the toolbar with the mouse. The tool is selected, and the offer appears
304+
after it rather than instead of it.
305+
2. Pick another tool. The offer does not appear again this session.
306+
3. Press Enter with nothing focused. Nothing happens and the dialog stays open.
307+
4. Tab to the checkbox and press Enter. Still nothing.
308+
5. Tab to a button and press Enter. That button, and only that button, acts.
309+
6. Press Escape. The dialog closes and Mouse drawing stays off.
310+
7. Choose **Enable mouse mode**. The left button draws immediately, and Eraser and Pan
311+
appear on the toolbar.
312+
8. Tick **Don't show me this again**, choose **Cancel**, restart, and confirm the offer is
313+
gone and **Help → Preferences → Input** can bring it back.
314+
9. Reach for the toolbar with the pen on a pen machine, and confirm no offer appears.

TODO.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ The delivery chain works end to end: a merge to `main` builds, signs, and publis
1616
pre-release to GitHub Releases, and one approval promotes that same build to a release.
1717
<https://whiteboard.sqlbi.com> reads its download links from the release manifest
1818
deployed beside it and needs no edit per release. The current product version is `VersionPrefix` in `Directory.Build.props`
19-
(1.2.0). Identity version for the Store package is `VersionPrefix.0` (`1.2.0.0`).
19+
(1.2.1). Identity version for the Store package is `VersionPrefix.0` (`1.2.1.0`).
2020

2121
Declaring that number is decision 20 in [docs/decisions.md](docs/decisions.md). What 1.0
2222
was waiting on shipped during 0.9.x: Preferences, `.wimport`, Explorer and VS Code
@@ -26,7 +26,8 @@ detected).
2626
No numbered work remains. 1.2.0 answered
2727
[discussion 78](https://github.com/sql-bi/SQLBI-Whiteboard/discussions/78) with Mouse
2828
drawing — decision 23, with the alternatives kept in
29-
[docs/mouse-mode.md](docs/mouse-mode.md). The video teaser is recorded and served from the landing page
29+
[docs/mouse-mode.md](docs/mouse-mode.md) — and 1.2.1 made it discoverable from the
30+
toolbar, decision 24. The video teaser is recorded and served from the landing page
3031
itself as `site/teaser-av1.mp4` / `site/teaser-h264.mp4` — the Vimeo-embed plan was
3132
reversed, see decision 19 in [docs/decisions.md](docs/decisions.md); the production
3233
script and staging assets are in `docs/teaser/`. The release manifests and the Store

docs/decisions.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -477,6 +477,41 @@ pen, which is why **On** is offered and not only the automatic default.
477477

478478
---
479479

480+
## 24. The mouse mode offer has no default button
481+
482+
**Implemented** in 1.2.1.
483+
484+
Decision 23 left Mouse drawing discoverable only in Preferences, which is the one place
485+
someone who does not know the feature exists will not look. The signal that they might want
486+
it is unambiguous and already in the application: **picking a tool from the toolbar with the
487+
mouse.** A pen user reaches for the palette with the pen, so a mouse arriving there is
488+
someone whose next stroke is going to disappoint them. That is when the offer appears.
489+
490+
Three properties of it were chosen rather than inherited, and each is the kind a later
491+
change would quietly undo:
492+
493+
- **Neither button is `IsDefault`, and Enter is swallowed.** This is a question about how
494+
the application behaves, not a confirmation, and the two answers are not
495+
interchangeable — one changes what the left button means. A default button would let
496+
Enter answer it for someone who was typing, and whichever button we picked would be the
497+
wrong one half the time. `Window_PreviewKeyDown` therefore marks every Enter handled
498+
before it can reach a button, and invokes one only when the person has deliberately moved
499+
focus to it. Escape closes, because dismissing is always safe. Nothing is focused when
500+
the dialog opens, so the first Tab reaches the checkbox rather than a primed button.
501+
- **Once a session, and the checkbox is unchecked.** Prompting on every toolbar click would
502+
be intolerable, and prompting once and never again would lose the person who was not
503+
ready to decide. A single Cancel answers this session; the checkbox answers every one
504+
after it, and `Help → Preferences → Input` is the way back from that.
505+
- **The offer is queued, not shown from the click handler.** It is dispatched at background
506+
priority so the click first does what it came to do. The tool is selected, and the dialog
507+
then explains why it may not behave as expected — rather than intercepting the click and
508+
leaving the person unsure whether their tool was chosen at all.
509+
510+
**Enable mouse mode** sets `MouseMode.On` rather than `WhenNoDigitizer`, because the offer
511+
can only have appeared on a machine where the automatic default already decided not to.
512+
513+
---
514+
480515
## Open questions
481516

482517
- arm64 is not built; add it if Surface devices matter for a pen application.

site/guide.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -686,7 +686,7 @@ <h3>Navigation</h3>
686686
<p>The window can sit in a narrow strip beside another app. <kbd>F11</kbd> fills the monitor and hides the title and tabs. <kbd>Ctrl</kbd><span class="plus">+</span><kbd>F11</kbd> hides the same chrome and keeps the window where it is. <kbd>Escape</kbd> leaves either unless a text container is being edited.</p>
687687

688688
<h3>Drawing with a mouse</h3>
689-
<p><span class="ui">Preferences → Mouse drawing</span> 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 <kbd>Ctrl</kbd> 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.</p>
689+
<p><span class="ui">Preferences → Mouse drawing</span> 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 <kbd>Ctrl</kbd> 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. With it off, picking a tool from the toolbar with the mouse offers to turn it on, once a session.</p>
690690

691691
<h3>Containers</h3>
692692
<p>Images, LiveViews, and text blocks are containers. With a mouse, click to move, drag the circular handle to resize while keeping aspect ratio — hold <kbd>Ctrl</kbd> too with <span class="ui">Mouse drawing</span> 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.</p>

src/SQLBI.Whiteboard.Core/Settings/AppSettings.cs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,15 @@ public sealed class AppSettings
117117

118118
public MouseMode MouseMode { get; set; } = MouseMode.WhenNoDigitizer;
119119

120+
/// <summary>
121+
/// Whether to offer Mouse drawing the first time in a session that someone
122+
/// picks a tool with the mouse while it is off. Reaching for the toolbar
123+
/// with a mouse is the one moment the application can be sure the question
124+
/// is worth asking, and the setting is what makes the offer refusable for
125+
/// good.
126+
/// </summary>
127+
public bool SuggestMouseMode { get; set; } = true;
128+
120129
public List<string> SnippetFormatOrder { get; set; } = [.. TextLanguageIds.All];
121130

122131
public InkToolSettings Pen { get; set; } = InkToolSettings.From(InkPalettes.DefaultPen);
@@ -153,7 +162,7 @@ public sealed class AppSettings
153162

154163
public static class AppSettingsSerializer
155164
{
156-
public const int CurrentVersion = 13;
165+
public const int CurrentVersion = 14;
157166

158167
private static readonly JsonSerializerOptions JsonOptions = new()
159168
{

src/SQLBI.Whiteboard/MainWindow.xaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,7 @@
229229
BorderBrush="{StaticResource ToolbarBorderBrush}"
230230
BorderThickness="1"
231231
Background="{StaticResource ToolbarBackgroundBrush}"
232+
PreviewMouseLeftButtonDown="ToolPalette_PreviewMouseLeftButtonDown"
232233
PreviewMouseRightButtonDown="ToolPalette_PreviewMouseRightButtonDown"
233234
PreviewStylusDown="ToolPalette_PreviewStylusDown"
234235
Panel.ZIndex="10">

src/SQLBI.Whiteboard/MainWindow.xaml.cs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ private enum PointerAction
8282
private PointerAction _stylusAction;
8383
private PointerAction _mouseAction;
8484
private bool _mouseToolBorrowed;
85+
private bool _mouseModeOffered;
8586
private PointD _lastPanPoint;
8687
private bool _penInContact;
8788
private bool _touchNavigationLocked;
@@ -1311,6 +1312,52 @@ private void BeginMouseAction(PointD screen)
13111312
}
13121313
}
13131314

1315+
// Picking a tool from the toolbar with the mouse is the one moment the
1316+
// application can be sure the question is worth asking: a pen user reaches
1317+
// for the palette with the pen. The offer is queued rather than shown from
1318+
// here, so the click first does what it came to do - the tool is chosen,
1319+
// and the dialog then explains why it may not behave as expected.
1320+
private void ToolPalette_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
1321+
{
1322+
if (e.StylusDevice is not null ||
1323+
_mouseModeOffered ||
1324+
!_settings.SuggestMouseMode ||
1325+
IsMouseModeEffective)
1326+
{
1327+
return;
1328+
}
1329+
1330+
// Asked once a session however many tools are picked afterwards. The
1331+
// checkbox on the offer is what answers it for every session after
1332+
// this one.
1333+
_mouseModeOffered = true;
1334+
Dispatcher.BeginInvoke(DispatcherPriority.Background, new Action(OfferMouseMode));
1335+
}
1336+
1337+
private void OfferMouseMode()
1338+
{
1339+
var offer = new MouseModeOfferWindow { Owner = this };
1340+
offer.ShowDialog();
1341+
var changed = false;
1342+
if (offer.EnableRequested)
1343+
{
1344+
_settings.MouseMode = MouseMode.On;
1345+
ApplyPointerModes();
1346+
changed = true;
1347+
}
1348+
1349+
if (offer.DoNotShowAgain)
1350+
{
1351+
_settings.SuggestMouseMode = false;
1352+
changed = true;
1353+
}
1354+
1355+
if (changed)
1356+
{
1357+
PersistSettings();
1358+
}
1359+
}
1360+
13141361
private void ToolPalette_PreviewMouseRightButtonDown(
13151362
object sender,
13161363
MouseButtonEventArgs e)
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<Window x:Class="SQLBI.Whiteboard.MouseModeOfferWindow"
2+
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
3+
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
4+
Title="Draw with a mouse?"
5+
Width="480"
6+
SizeToContent="Height"
7+
ResizeMode="NoResize"
8+
WindowStartupLocation="CenterOwner"
9+
ShowInTaskbar="False"
10+
Focusable="True"
11+
FocusManager.IsFocusScope="True"
12+
Background="{DynamicResource ToolbarBackgroundBrush}"
13+
Loaded="Window_Loaded"
14+
PreviewKeyDown="Window_PreviewKeyDown">
15+
<StackPanel Margin="28,24">
16+
<TextBlock FontFamily="Segoe UI"
17+
FontSize="16"
18+
FontWeight="SemiBold"
19+
Foreground="#FF1F2937"
20+
TextWrapping="Wrap"
21+
Text="Draw with a mouse?" />
22+
<TextBlock Margin="0,12,0,0"
23+
FontFamily="Segoe UI"
24+
FontSize="13"
25+
Foreground="#FF374151"
26+
TextWrapping="Wrap"
27+
Text="SQLBI Whiteboard is designed for a pen and a touchscreen. If you would rather use a mouse, Mouse drawing can be turned on in Preferences." />
28+
<TextBlock Margin="0,10,0,0"
29+
FontFamily="Segoe UI"
30+
FontSize="13"
31+
Foreground="#FF374151"
32+
TextWrapping="Wrap"
33+
Text="The left button then uses the selected tool, and Ctrl with the left button moves and resizes a container. A mouse reports no pressure, so ink is drawn at an even width. Nothing about the pen changes." />
34+
<CheckBox x:Name="DoNotShowAgainBox"
35+
Margin="0,20,0,0"
36+
FontFamily="Segoe UI"
37+
FontSize="13"
38+
Foreground="#FF374151"
39+
Content="Don't show me this again" />
40+
<!-- Neither button is IsDefault or IsCancel. This is a question the
41+
person is meant to answer, so Enter must not answer it for them by
42+
landing on whichever button a dialog would normally favour. -->
43+
<StackPanel Margin="0,16,0,0"
44+
Orientation="Horizontal"
45+
HorizontalAlignment="Right">
46+
<Button x:Name="EnableButton"
47+
MinWidth="140"
48+
MinHeight="30"
49+
Margin="0,0,8,0"
50+
Content="Enable mouse mode"
51+
Click="EnableButton_Click" />
52+
<Button x:Name="CancelButton"
53+
MinWidth="96"
54+
MinHeight="30"
55+
Content="Cancel"
56+
Click="CancelButton_Click" />
57+
</StackPanel>
58+
</StackPanel>
59+
</Window>
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
using System.Windows;
2+
using System.Windows.Controls;
3+
using System.Windows.Input;
4+
5+
namespace SQLBI.Whiteboard;
6+
7+
/// <summary>
8+
/// Offered the first time in a session that someone picks a tool from the
9+
/// toolbar with the mouse while Mouse drawing is off. Reaching for the toolbar
10+
/// with a mouse is the one moment the application can be sure the question is
11+
/// worth asking.
12+
/// </summary>
13+
public partial class MouseModeOfferWindow : Window
14+
{
15+
public MouseModeOfferWindow() => InitializeComponent();
16+
17+
/// <summary>
18+
/// Whether the person asked for Mouse drawing to be turned on.
19+
/// </summary>
20+
public bool EnableRequested { get; private set; }
21+
22+
/// <summary>
23+
/// Whether the offer was declined for good. Unchecked by default: a single
24+
/// Cancel is an answer for this session, not for every one after it.
25+
/// </summary>
26+
public bool DoNotShowAgain => DoNotShowAgainBox.IsChecked == true;
27+
28+
// Nothing starts focused, so the first Tab reaches the checkbox rather than
29+
// a button already primed to act. Without this the checkbox takes focus on
30+
// open, and a stray Space would answer the wrong question.
31+
private void Window_Loaded(object sender, RoutedEventArgs e) => Keyboard.Focus(this);
32+
33+
private void Window_PreviewKeyDown(object sender, KeyEventArgs e)
34+
{
35+
if (e.Key == Key.Escape)
36+
{
37+
Close();
38+
e.Handled = true;
39+
return;
40+
}
41+
42+
if (e.Key != Key.Enter)
43+
{
44+
return;
45+
}
46+
47+
// Enter is always handled here, and never reaches a button that WPF
48+
// would otherwise treat as the default. It acts only on a button the
49+
// person has deliberately moved focus to, so the answer is theirs.
50+
e.Handled = true;
51+
if (Keyboard.FocusedElement is not Button focused)
52+
{
53+
return;
54+
}
55+
56+
if (ReferenceEquals(focused, EnableButton))
57+
{
58+
Enable();
59+
}
60+
else if (ReferenceEquals(focused, CancelButton))
61+
{
62+
Close();
63+
}
64+
}
65+
66+
private void EnableButton_Click(object sender, RoutedEventArgs e) => Enable();
67+
68+
private void CancelButton_Click(object sender, RoutedEventArgs e) => Close();
69+
70+
private void Enable()
71+
{
72+
EnableRequested = true;
73+
Close();
74+
}
75+
}

0 commit comments

Comments
 (0)