diff --git a/Directory.Build.props b/Directory.Build.props
index aea84e8..849785e 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.2.0
+ 1.2.1latestenableenable
diff --git a/README.md b/README.md
index 7980293..e52309b 100644
--- a/README.md
+++ b/README.md
@@ -11,7 +11,7 @@ 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
+- 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
- 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, 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:
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.
+
+With Mouse drawing **Off**, the offer has its own list. Every one of these is about a key
+doing nothing, which is the part a later change is most likely to undo:
+
+1. Pick a tool from the toolbar with the mouse. The tool is selected, and the offer appears
+ after it rather than instead of it.
+2. Pick another tool. The offer does not appear again this session.
+3. Press Enter with nothing focused. Nothing happens and the dialog stays open.
+4. Tab to the checkbox and press Enter. Still nothing.
+5. Tab to a button and press Enter. That button, and only that button, acts.
+6. Press Escape. The dialog closes and Mouse drawing stays off.
+7. Choose **Enable mouse mode**. The left button draws immediately, and Eraser and Pan
+ appear on the toolbar.
+8. Tick **Don't show me this again**, choose **Cancel**, restart, and confirm the offer is
+ gone and **Help → Preferences → Input** can bring it back.
+9. Reach for the toolbar with the pen on a pen machine, and confirm no offer appears.
diff --git a/TODO.md b/TODO.md
index 307ad8d..0d8e590 100644
--- a/TODO.md
+++ b/TODO.md
@@ -16,7 +16,7 @@ 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.2.0). Identity version for the Store package is `VersionPrefix.0` (`1.2.0.0`).
+(1.2.1). Identity version for the Store package is `VersionPrefix.0` (`1.2.1.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
@@ -26,7 +26,8 @@ detected).
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
+[docs/mouse-mode.md](docs/mouse-mode.md) — and 1.2.1 made it discoverable from the
+toolbar, decision 24. 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 02c4ff6..3273d1f 100644
--- a/docs/decisions.md
+++ b/docs/decisions.md
@@ -477,6 +477,41 @@ pen, which is why **On** is offered and not only the automatic default.
---
+## 24. The mouse mode offer has no default button
+
+**Implemented** in 1.2.1.
+
+Decision 23 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 unambiguous and 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. That is when the offer appears.
+
+Three properties of it were chosen rather than inherited, and each is the kind a later
+change would quietly undo:
+
+- **Neither button is `IsDefault`, and Enter is swallowed.** This is a question about how
+ the application behaves, not a confirmation, and the two answers are not
+ interchangeable — one changes what the left button means. A default button would let
+ Enter answer it for someone who was typing, and whichever button we picked would be the
+ wrong one half the time. `Window_PreviewKeyDown` therefore marks every Enter handled
+ before it can reach a button, and invokes one only when the person has deliberately moved
+ focus to it. Escape closes, because dismissing is always safe. Nothing is focused when
+ the dialog opens, so the first Tab reaches the checkbox rather than a primed button.
+- **Once a session, and the checkbox is unchecked.** Prompting on every toolbar click would
+ be intolerable, and prompting once and never again would lose the person who was not
+ ready to decide. A single Cancel answers this session; the checkbox answers every one
+ after it, and `Help → Preferences → Input` is the way back from that.
+- **The offer is queued, not shown from the click handler.** It is dispatched at background
+ priority 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 — rather than intercepting the click and
+ leaving the person unsure whether their tool was chosen at all.
+
+**Enable mouse mode** sets `MouseMode.On` rather than `WhenNoDigitizer`, because the offer
+can only have appeared on a machine where the automatic default already decided not to.
+
+---
+
## Open questions
- arm64 is not built; add it if Surface devices matter for a pen application.
diff --git a/site/guide.html b/site/guide.html
index 22f31b6..a744dd2 100644
--- a/site/guide.html
+++ b/site/guide.html
@@ -686,7 +686,7 @@
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.
+
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. With it off, picking a tool from the toolbar with the mouse offers to turn it on, once a session.
Containers
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.
diff --git a/src/SQLBI.Whiteboard.Core/Settings/AppSettings.cs b/src/SQLBI.Whiteboard.Core/Settings/AppSettings.cs
index 6d70972..6fbe7ad 100644
--- a/src/SQLBI.Whiteboard.Core/Settings/AppSettings.cs
+++ b/src/SQLBI.Whiteboard.Core/Settings/AppSettings.cs
@@ -117,6 +117,15 @@ public sealed class AppSettings
public MouseMode MouseMode { get; set; } = MouseMode.WhenNoDigitizer;
+ ///
+ /// Whether to offer Mouse drawing the first time in a session that someone
+ /// picks a tool with the mouse while it is off. Reaching for the toolbar
+ /// with a mouse is the one moment the application can be sure the question
+ /// is worth asking, and the setting is what makes the offer refusable for
+ /// good.
+ ///
+ public bool SuggestMouseMode { get; set; } = true;
+
public List SnippetFormatOrder { get; set; } = [.. TextLanguageIds.All];
public InkToolSettings Pen { get; set; } = InkToolSettings.From(InkPalettes.DefaultPen);
@@ -153,7 +162,7 @@ public sealed class AppSettings
public static class AppSettingsSerializer
{
- public const int CurrentVersion = 13;
+ public const int CurrentVersion = 14;
private static readonly JsonSerializerOptions JsonOptions = new()
{
diff --git a/src/SQLBI.Whiteboard/MainWindow.xaml b/src/SQLBI.Whiteboard/MainWindow.xaml
index afd43c9..2c1b8ac 100644
--- a/src/SQLBI.Whiteboard/MainWindow.xaml
+++ b/src/SQLBI.Whiteboard/MainWindow.xaml
@@ -229,6 +229,7 @@
BorderBrush="{StaticResource ToolbarBorderBrush}"
BorderThickness="1"
Background="{StaticResource ToolbarBackgroundBrush}"
+ PreviewMouseLeftButtonDown="ToolPalette_PreviewMouseLeftButtonDown"
PreviewMouseRightButtonDown="ToolPalette_PreviewMouseRightButtonDown"
PreviewStylusDown="ToolPalette_PreviewStylusDown"
Panel.ZIndex="10">
diff --git a/src/SQLBI.Whiteboard/MainWindow.xaml.cs b/src/SQLBI.Whiteboard/MainWindow.xaml.cs
index 37a7deb..e2420f5 100644
--- a/src/SQLBI.Whiteboard/MainWindow.xaml.cs
+++ b/src/SQLBI.Whiteboard/MainWindow.xaml.cs
@@ -82,6 +82,7 @@ private enum PointerAction
private PointerAction _stylusAction;
private PointerAction _mouseAction;
private bool _mouseToolBorrowed;
+ private bool _mouseModeOffered;
private PointD _lastPanPoint;
private bool _penInContact;
private bool _touchNavigationLocked;
@@ -1311,6 +1312,52 @@ private void BeginMouseAction(PointD screen)
}
}
+ // Picking a tool from the toolbar with the mouse is the one moment the
+ // application can be sure the question is worth asking: a pen user reaches
+ // for the palette with the pen. The offer is queued rather than shown from
+ // here, so the click first does what it came to do - the tool is chosen,
+ // and the dialog then explains why it may not behave as expected.
+ private void ToolPalette_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
+ {
+ if (e.StylusDevice is not null ||
+ _mouseModeOffered ||
+ !_settings.SuggestMouseMode ||
+ IsMouseModeEffective)
+ {
+ return;
+ }
+
+ // Asked once a session however many tools are picked afterwards. The
+ // checkbox on the offer is what answers it for every session after
+ // this one.
+ _mouseModeOffered = true;
+ Dispatcher.BeginInvoke(DispatcherPriority.Background, new Action(OfferMouseMode));
+ }
+
+ private void OfferMouseMode()
+ {
+ var offer = new MouseModeOfferWindow { Owner = this };
+ offer.ShowDialog();
+ var changed = false;
+ if (offer.EnableRequested)
+ {
+ _settings.MouseMode = MouseMode.On;
+ ApplyPointerModes();
+ changed = true;
+ }
+
+ if (offer.DoNotShowAgain)
+ {
+ _settings.SuggestMouseMode = false;
+ changed = true;
+ }
+
+ if (changed)
+ {
+ PersistSettings();
+ }
+ }
+
private void ToolPalette_PreviewMouseRightButtonDown(
object sender,
MouseButtonEventArgs e)
diff --git a/src/SQLBI.Whiteboard/MouseModeOfferWindow.xaml b/src/SQLBI.Whiteboard/MouseModeOfferWindow.xaml
new file mode 100644
index 0000000..762cb1d
--- /dev/null
+++ b/src/SQLBI.Whiteboard/MouseModeOfferWindow.xaml
@@ -0,0 +1,59 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/SQLBI.Whiteboard/MouseModeOfferWindow.xaml.cs b/src/SQLBI.Whiteboard/MouseModeOfferWindow.xaml.cs
new file mode 100644
index 0000000..f808022
--- /dev/null
+++ b/src/SQLBI.Whiteboard/MouseModeOfferWindow.xaml.cs
@@ -0,0 +1,75 @@
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Input;
+
+namespace SQLBI.Whiteboard;
+
+///
+/// Offered the first time in a session that someone picks a tool from the
+/// toolbar with the mouse while Mouse drawing is off. Reaching for the toolbar
+/// with a mouse is the one moment the application can be sure the question is
+/// worth asking.
+///
+public partial class MouseModeOfferWindow : Window
+{
+ public MouseModeOfferWindow() => InitializeComponent();
+
+ ///
+ /// Whether the person asked for Mouse drawing to be turned on.
+ ///
+ public bool EnableRequested { get; private set; }
+
+ ///
+ /// Whether the offer was declined for good. Unchecked by default: a single
+ /// Cancel is an answer for this session, not for every one after it.
+ ///
+ public bool DoNotShowAgain => DoNotShowAgainBox.IsChecked == true;
+
+ // Nothing starts focused, so the first Tab reaches the checkbox rather than
+ // a button already primed to act. Without this the checkbox takes focus on
+ // open, and a stray Space would answer the wrong question.
+ private void Window_Loaded(object sender, RoutedEventArgs e) => Keyboard.Focus(this);
+
+ private void Window_PreviewKeyDown(object sender, KeyEventArgs e)
+ {
+ if (e.Key == Key.Escape)
+ {
+ Close();
+ e.Handled = true;
+ return;
+ }
+
+ if (e.Key != Key.Enter)
+ {
+ return;
+ }
+
+ // Enter is always handled here, and never reaches a button that WPF
+ // would otherwise treat as the default. It acts only on a button the
+ // person has deliberately moved focus to, so the answer is theirs.
+ e.Handled = true;
+ if (Keyboard.FocusedElement is not Button focused)
+ {
+ return;
+ }
+
+ if (ReferenceEquals(focused, EnableButton))
+ {
+ Enable();
+ }
+ else if (ReferenceEquals(focused, CancelButton))
+ {
+ Close();
+ }
+ }
+
+ private void EnableButton_Click(object sender, RoutedEventArgs e) => Enable();
+
+ private void CancelButton_Click(object sender, RoutedEventArgs e) => Close();
+
+ private void Enable()
+ {
+ EnableRequested = true;
+ Close();
+ }
+}
diff --git a/src/SQLBI.Whiteboard/PreferencesWindow.xaml.cs b/src/SQLBI.Whiteboard/PreferencesWindow.xaml.cs
index 8ac9424..a2631f0 100644
--- a/src/SQLBI.Whiteboard/PreferencesWindow.xaml.cs
+++ b/src/SQLBI.Whiteboard/PreferencesWindow.xaml.cs
@@ -728,6 +728,7 @@ private ToggleButton CreateSwitch(SettingDescriptor setting)
{
SettingsCatalog.Ids.StartFullScreen => _settings.StartFullScreen,
SettingsCatalog.Ids.WarnWhenNoDigitizer => _settings.WarnWhenNoDigitizer,
+ SettingsCatalog.Ids.SuggestMouseMode => _settings.SuggestMouseMode,
SettingsCatalog.Ids.CheckForUpdates => _settings.CheckForUpdates,
_ => false,
},
@@ -910,6 +911,10 @@ private void SetBoolean(SettingDescriptor setting, bool value)
{
_settings.WarnWhenNoDigitizer = value;
}
+ else if (setting.Id == SettingsCatalog.Ids.SuggestMouseMode)
+ {
+ _settings.SuggestMouseMode = value;
+ }
else if (setting.Id == SettingsCatalog.Ids.CheckForUpdates)
{
_settings.CheckForUpdates = value;
diff --git a/src/SQLBI.Whiteboard/SettingsCatalog.cs b/src/SQLBI.Whiteboard/SettingsCatalog.cs
index 642e546..918936a 100644
--- a/src/SQLBI.Whiteboard/SettingsCatalog.cs
+++ b/src/SQLBI.Whiteboard/SettingsCatalog.cs
@@ -85,6 +85,7 @@ public static class Ids
public const string WarnWhenNoDigitizer = "startup.noDigitizerNotice";
public const string FingerMode = "input.fingerMode";
public const string MouseMode = "input.mouseMode";
+ public const string SuggestMouseMode = "input.mouseModeOffer";
public const string PenButton = "input.penButton";
public const string SnippetFormatOrder = "input.snippetFormatOrder";
public const string CheckForUpdates = "updates.check";
@@ -159,6 +160,15 @@ public static class Ids
],
},
new()
+ {
+ Id = Ids.SuggestMouseMode,
+ Category = Input,
+ Title = "Offer mouse drawing when the mouse picks a tool",
+ Description = "With Mouse drawing off, choosing a tool from the toolbar with the mouse offers to turn it on. Asked once a session, and not again once the offer has been declined for good.",
+ Keywords = ["mouse", "offer", "prompt", "dialog", "toolbar", "suggest", "ask"],
+ Editor = SettingEditorKind.BooleanSwitch,
+ },
+ 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 df458d7..f2b66b1 100644
--- a/tests/SQLBI.Whiteboard.Core.SmokeTests/Program.cs
+++ b/tests/SQLBI.Whiteboard.Core.SmokeTests/Program.cs
@@ -883,6 +883,21 @@ defaultSettings.StartupMonitorName is null &&
settingsFromVersion12.ToolbarPlacement == ToolbarPlacement.BottomLeft &&
settingsFromVersion12.Version == AppSettingsSerializer.CurrentVersion,
"Settings saved before mouse drawing existed should upgrade and keep their own choices.");
+Assert(
+ defaultSettings.SuggestMouseMode,
+ "Missing settings should offer mouse drawing when the mouse picks a tool.");
+Assert(
+ settingsFromVersion12.SuggestMouseMode,
+ "Settings saved before the offer existed should get it, not lose it.");
+Assert(
+ AppSettingsSerializer.Parse("{ \"suggestMouseMode\": false }") is
+ { SuggestMouseMode: false },
+ "An offer declined for good should still be declined after a restart.");
+var offerRoundTrip = AppSettingsSerializer.Parse(
+ AppSettingsSerializer.Format(new AppSettings { SuggestMouseMode = false }));
+Assert(
+ !offerRoundTrip.SuggestMouseMode,
+ "Settings JSON should round-trip the mouse drawing offer.");
Assert(
defaultSettings.SnippetFormatOrder is ["plain", "dax", "sqlserver"],
"Missing settings should keep Plain text first so paste stays plain text.");