diff --git a/Ink Canvas/Controls/Popups/BoardRoamingPopupContent.xaml b/Ink Canvas/Controls/Popups/BoardRoamingPopupContent.xaml
index 8e96fd8a9..a48ca19ea 100644
--- a/Ink Canvas/Controls/Popups/BoardRoamingPopupContent.xaml
+++ b/Ink Canvas/Controls/Popups/BoardRoamingPopupContent.xaml
@@ -5,6 +5,7 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:props="clr-namespace:Ink_Canvas.Properties"
xmlns:controls="clr-namespace:Ink_Canvas.Controls;assembly=InkCanvas.Controls"
+ xmlns:ui="http://schemas.inkore.net/lib/ui/wpf/modern"
mc:Ignorable="d"
d:DesignWidth="380" d:DesignHeight="300">
@@ -43,6 +44,36 @@
Foreground="{DynamicResource FloatingBarForegroundBrush}"
Opacity="0.72"
FontSize="11" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Ink Canvas/Controls/Popups/BoardRoamingPopupContent.xaml.cs b/Ink Canvas/Controls/Popups/BoardRoamingPopupContent.xaml.cs
index d0f1652c6..7b796f959 100644
--- a/Ink Canvas/Controls/Popups/BoardRoamingPopupContent.xaml.cs
+++ b/Ink Canvas/Controls/Popups/BoardRoamingPopupContent.xaml.cs
@@ -1,3 +1,4 @@
+using iNKORE.UI.WPF.Modern.Controls;
using System;
using System.Windows;
using System.Windows.Controls;
@@ -26,6 +27,8 @@ private enum DragInputDevice
public Image PreviewImageControl => PreviewImage;
public Button CloseButtonControl => Shell?.CloseButtonControl;
+ public ToggleSwitch TwoFingerZoomToggle => ToggleSwitchEnableTwoFingerZoom;
+ public ToggleSwitch TwoFingerRotationToggle => ToggleSwitchEnableTwoFingerRotation;
public BoardRoamingPopupContent()
{
diff --git a/Ink Canvas/MainWindow.xaml b/Ink Canvas/MainWindow.xaml
index 2fa18b60f..548c7bb45 100644
--- a/Ink Canvas/MainWindow.xaml
+++ b/Ink Canvas/MainWindow.xaml
@@ -339,9 +339,11 @@
+ 提醒用户当前处于批注模式而非鼠标模式。
+ Placement=RelativePoint 相对 inkCanvas,避免 DPI 缩放导致的位置偏差。 -->
+
+
+
+
+
+
+
0)
{
- EndBoardRoaming();
- inkCanvas.ReleaseStylusCapture();
- ViewboxFloatingBar.IsHitTestVisible = true;
- BlackboardUIGridForInkReplay.IsHitTestVisible = true;
+ EndBoardRoamingContact(e.StylusDevice.Id);
+ if (_boardRoamingContacts.Count == 0)
+ {
+ inkCanvas.ReleaseStylusCapture();
+ ViewboxFloatingBar.IsHitTestVisible = true;
+ BlackboardUIGridForInkReplay.IsHitTestVisible = true;
+ }
e.Handled = true;
return;
}
diff --git a/Ink Canvas/MainWindow_cs/MW_AnnotationDotHint.cs b/Ink Canvas/MainWindow_cs/MW_AnnotationDotHint.cs
index 310668c01..adada95d0 100644
--- a/Ink Canvas/MainWindow_cs/MW_AnnotationDotHint.cs
+++ b/Ink Canvas/MainWindow_cs/MW_AnnotationDotHint.cs
@@ -28,6 +28,10 @@ public partial class MainWindow
private readonly Queue _annotationDotPositions = new Queue();
/// 最近点击位置队列的最大容量。
private const int AnnotationDotMaxQueueSize = 10;
+ /// 最近一次「长笔迹」(真实书写)的提交时间;用于 30 秒未书写的空闲门控。
+ private DateTime? _lastLongStrokeTime;
+ /// 距离上次真实书写多久后,功能按「空闲」状态触发(秒)。
+ private const double AnnotationDotIdleSeconds = 30;
/// 提示自动隐藏计时器。
private DispatcherTimer _annotationDotHintTimer;
/// 提示是否正在显示。
@@ -50,16 +54,32 @@ internal void HandleAnnotationDotAfterStroke(Stroke stroke)
double maxDim = Math.Max(bounds.Width, bounds.Height);
double strokeThreshold = Settings.Canvas.AnnotationDotHintStrokeLengthThreshold;
- // 仅对极短墨迹(点击)进行追踪
- if (maxDim > strokeThreshold) return;
+ // 长笔迹 = 真实书写:重置 30 秒空闲计时器,并清空点击轨迹。
+ if (maxDim > strokeThreshold)
+ {
+ _lastLongStrokeTime = DateTime.Now;
+ _annotationDotPositions.Clear();
+ return;
+ }
var center = new Point(bounds.Left + bounds.Width / 2, bounds.Top + bounds.Height / 2);
if (double.IsNaN(center.X) || double.IsNaN(center.Y)) return;
- // 对单点 / 极短墨迹补画可见圆点(不影响原始墨迹管线)
+ // 对单点 / 极短墨迹补画可见圆点(按笔的实际粗细,不做加粗)
EnsureDotVisible(stroke, center);
- TrackAnnotationDotPosition(center);
+ if (IsAnnotationIdle())
+ {
+ // 30 秒未书写:每次点击都显示指示小圆点
+ ShowAnnotationDotIndicator(center);
+ // 短时内连续点击达到阈值 → 显示「批注中」提示
+ TrackAnnotationDotPosition(center, showHint: true);
+ }
+ else
+ {
+ // 30 秒内有书写:仅连续点击达到阈值时,在末次点击显示一次指示小圆点(不显示批注提示)
+ TrackAnnotationDotPosition(center, showHint: false);
+ }
}
catch (Exception ex)
{
@@ -90,8 +110,7 @@ private void EnsureDotVisible(Stroke originalStroke, Point center)
?? (inkCanvas.DefaultDrawingAttributes?.Clone()
?? new DrawingAttributes { Color = Colors.Black, Width = 2, Height = 2 });
- drawingAttrs.Width = Math.Max(drawingAttrs.Width, 3);
- drawingAttrs.Height = Math.Max(drawingAttrs.Height, 3);
+ // 按笔的实际粗细渲染,不做加粗处理。
// 构建一个由 8 个点组成的微小圆(半径 2px),确保视觉可见
var points = new StylusPointCollection();
@@ -122,11 +141,12 @@ private void EnsureDotVisible(Stroke originalStroke, Point center)
}
///
- /// 记录点击位置到追踪队列,并检查是否需要显示提示。
- /// 仅检查最近 N 个点(N = 点击次数阈值),而非队列全部点,
- /// 避免跨区域点击导致判定失败。
+ /// 记录点击位置到追踪队列,并在连续点击达到阈值时执行相应动作。
+ /// 为 true 时触发「批注中」提示(30 秒空闲后路径);
+ /// 为 false 时仅显示一次指示小圆点(30 秒内路径,不显示批注提示)。
+ /// 仅检查最近 N 个点(N = 点击次数阈值),而非队列全部点,避免跨区域误判。
///
- private void TrackAnnotationDotPosition(Point position)
+ private void TrackAnnotationDotPosition(Point position, bool showHint)
{
if (double.IsNaN(position.X) || double.IsNaN(position.Y)) return;
@@ -143,7 +163,16 @@ private void TrackAnnotationDotPosition(Point position)
// 而非队列中所有点,避免队列中混入旧区域点导致误判
if (IsRecentClusterWithinRadius(clickCount, clusterRadius))
{
- ShowAnnotationDotHint(position);
+ if (showHint)
+ {
+ ShowAnnotationDotHint(position);
+ }
+ else
+ {
+ // 30 秒内连续点击达到阈值:末次点击显示一次指示小圆点,但不显示「批注中」提示
+ ShowAnnotationDotIndicator(position);
+ }
+ _annotationDotPositions.Clear();
}
}
@@ -181,7 +210,9 @@ private bool IsRecentClusterWithinRadius(int count, double radius)
}
///
- /// 显示批注状态提示。使用屏幕坐标绝对定位,边缘点击时对齐锚点而非居中。
+ /// 显示批注状态提示。Popup 使用 Placement=RelativePoint 且 PlacementTarget=inkCanvas,
+ /// 偏移量直接使用画布(逻辑)坐标,避免 DPI 缩放下的屏幕坐标换算偏差。
+ /// 靠近画布边缘时对齐锚点而非居中。
///
private void ShowAnnotationDotHint(Point anchor)
{
@@ -193,51 +224,47 @@ private void ShowAnnotationDotHint(Point anchor)
var popup = AnnotationDotHintPopup;
if (popup == null) return;
- // 直接将画布坐标转为屏幕坐标(考虑 RenderTransform 等)
- var clickScreen = inkCanvas.PointToScreen(anchor);
-
- // 使用实际 Border 宽度,确保与 XAML 定义一致
- double hintWidth = (AnnotationDotHintBorder?.ActualWidth > 0) ? AnnotationDotHintBorder.ActualWidth : 380;
- double hintHeight = 60;
+ // 使用实际 Border 尺寸,与 XAML 定义一致;未布局时回退到 XAML 固定值
+ double hintWidth = (AnnotationDotHintBorder?.ActualWidth > 0) ? AnnotationDotHintBorder.ActualWidth : 295;
+ double hintHeight = (AnnotationDotHintBorder?.ActualHeight > 0) ? AnnotationDotHintBorder.ActualHeight : 60;
const double margin = 20;
- var workArea = SystemParameters.WorkArea;
- double screenW = workArea.Width;
- double screenH = workArea.Height;
+ double canvasW = inkCanvas.ActualWidth;
+ double canvasH = inkCanvas.ActualHeight;
double hintLeft, hintTop;
- // 水平:靠近左边缘时对齐左边缘,靠近右边缘时对齐右边缘
- if (clickScreen.X < workArea.Left + screenW / 2)
+ // 水平:靠近左半边时对齐左边缘,靠近右半边时对齐右边缘
+ if (anchor.X < canvasW / 2)
{
- // 左半屏:提示左边缘对齐锚点
- hintLeft = clickScreen.X;
+ // 左半边:提示左边缘对齐锚点
+ hintLeft = anchor.X;
}
else
{
- // 右半屏:提示右边缘对齐锚点
- hintLeft = clickScreen.X - hintWidth;
+ // 右半边:提示右边缘对齐锚点
+ hintLeft = anchor.X - hintWidth;
}
- // 垂直:上半屏放锚点下方,下半屏放锚点上方
- if (clickScreen.Y < workArea.Top + screenH / 2)
+ // 垂直:上半边放锚点下方,下半边放锚点上方
+ if (anchor.Y < canvasH / 2)
{
- hintTop = clickScreen.Y + 10;
+ hintTop = anchor.Y + 10;
}
else
{
- hintTop = clickScreen.Y - hintHeight - 10;
+ hintTop = anchor.Y - hintHeight - 10;
}
- // 钳制到屏幕工作区域内
- if (hintLeft < workArea.Left + margin)
- hintLeft = workArea.Left + margin;
- if (hintLeft + hintWidth > workArea.Right - margin)
- hintLeft = workArea.Right - hintWidth - margin;
- if (hintTop < workArea.Top + margin)
- hintTop = workArea.Top + margin;
- if (hintTop + hintHeight > workArea.Bottom - margin)
- hintTop = workArea.Bottom - hintHeight - margin;
+ // 钳制到画布可见区域内
+ if (hintLeft < margin)
+ hintLeft = margin;
+ if (hintLeft + hintWidth > canvasW - margin)
+ hintLeft = canvasW - hintWidth - margin;
+ if (hintTop < margin)
+ hintTop = margin;
+ if (hintTop + hintHeight > canvasH - margin)
+ hintTop = canvasH - hintHeight - margin;
popup.HorizontalOffset = hintLeft;
popup.VerticalOffset = hintTop;
@@ -263,6 +290,50 @@ private void ShowAnnotationDotHint(Point anchor)
_annotationDotHintTimer.Start();
}
+ ///
+ /// 当前是否满足「30 秒未在白板内书写」的空闲条件。
+ /// 从未书写过( 为空)时视为空闲。
+ ///
+ private bool IsAnnotationIdle()
+ {
+ return !_lastLongStrokeTime.HasValue
+ || (DateTime.Now - _lastLongStrokeTime.Value).TotalSeconds >= AnnotationDotIdleSeconds;
+ }
+
+ ///
+ /// 显示「处于批注状态」的指示小圆点:带圆角容器,渐显出现、短暂停留后渐隐消失。
+ /// Popup 使用 Placement=RelativePoint 且 PlacementTarget=inkCanvas,坐标直接使用画布(逻辑)坐标。
+ ///
+ private void ShowAnnotationDotIndicator(Point anchor)
+ {
+ var popup = AnnotationDotIndicatorPopup;
+ var border = AnnotationDotIndicatorBorder;
+ if (popup == null || border == null) return;
+
+ // 容器尺寸与 XAML 定义一致(28×28),居中对齐点击位置
+ const double size = 28;
+ popup.HorizontalOffset = anchor.X - size / 2;
+ popup.VerticalOffset = anchor.Y - size / 2;
+ popup.IsOpen = true;
+
+ // 渐显 → 短暂停留 → 渐隐
+ var anim = new DoubleAnimationUsingKeyFrames();
+ anim.KeyFrames.Add(new LinearDoubleKeyFrame(0, KeyTime.FromTimeSpan(TimeSpan.Zero)));
+ anim.KeyFrames.Add(new LinearDoubleKeyFrame(1, KeyTime.FromTimeSpan(TimeSpan.FromMilliseconds(150))));
+ anim.KeyFrames.Add(new LinearDoubleKeyFrame(1, KeyTime.FromTimeSpan(TimeSpan.FromMilliseconds(550))));
+ anim.KeyFrames.Add(new LinearDoubleKeyFrame(0, KeyTime.FromTimeSpan(TimeSpan.FromMilliseconds(750))));
+ anim.Completed += (s, e) =>
+ {
+ if (AnnotationDotIndicatorPopup != null)
+ AnnotationDotIndicatorPopup.IsOpen = false;
+ };
+
+ // 结束上一次未完成的动画,避免快速连点时叠加
+ border.BeginAnimation(UIElement.OpacityProperty, null);
+ border.Opacity = 0;
+ border.BeginAnimation(UIElement.OpacityProperty, anim);
+ }
+
private void HideAnnotationDotHint()
{
_annotationDotHintVisible = false;
diff --git a/Ink Canvas/MainWindow_cs/MW_BoardRoaming.cs b/Ink Canvas/MainWindow_cs/MW_BoardRoaming.cs
index 054ef17b4..618941900 100644
--- a/Ink Canvas/MainWindow_cs/MW_BoardRoaming.cs
+++ b/Ink Canvas/MainWindow_cs/MW_BoardRoaming.cs
@@ -25,6 +25,18 @@ public partial class MainWindow
private Rect _boardRoamingPreviewMovementBounds;
private bool _isUpdatingBoardRoamingPopup;
private bool _boardRoamingPopupEventsAttached;
+ private bool _isSyncingBoardRoamingToggles;
+ // 漫游触摸接触点(StylusDevice.Id -> 最近位置,按落下顺序排列)。
+ // 漫游模式下 Stylus 事件全部标记 Handled,WPF 不会把它们提升为 Touch/Manipulation,
+ // 双指手势必须直接由接触点计算(WispLogic.PromoteMainToOther:Stylus Handled 即跳过提升)。
+ private readonly List> _boardRoamingContacts = new List>();
+ private bool _isBoardRoamingTwoFingerGesture;
+ private bool _isBoardRoamingPopupDragActive;
+ // 单指拖动挂起:第一指落下后先不拖动,位移超过阈值才激活,
+ // 避免放置第二指过程中的手部漂移带动板书产生异常位移。
+ private bool _isBoardRoamingSingleFingerPending;
+ private Point _boardRoamingPendingStartPoint;
+ private const double BoardRoamingSingleFingerActivationThreshold = 10.0;
internal void ActivateBoardRoamingMode()
{
@@ -36,6 +48,7 @@ internal void ActivateBoardRoamingMode()
}
HideEdgeExpandHint();
+ ResetBoardRoamingGestureState();
ResetTouchStates();
CancelSingleFingerDragMode();
drawingShapeMode = 0;
@@ -92,10 +105,8 @@ private void MoveBoardRoaming(Point point)
var delta = point - _boardRoamingLastPoint;
if (delta.X == 0 && delta.Y == 0) return;
+ // 视口世界坐标由 TransformBoardRoamingContent 按逆矩阵统一维护
TranslateBoardRoamingContent(delta.X, delta.Y);
- _boardRoamingViewportWorldPosition = new Point(
- _boardRoamingViewportWorldPosition.X - delta.X,
- _boardRoamingViewportWorldPosition.Y - delta.Y);
_boardRoamingLastPoint = point;
RefreshBoardRoamingPopup(false);
@@ -111,6 +122,251 @@ private void EndBoardRoaming()
inkCanvas.Cursor = IsBoardRoamingMode ? Cursors.Hand : Cursors.Arrow;
}
+ ///
+ /// 强制结束漫游交互(单指拖动/双指手势),用于切换工具等场景。
+ ///
+ private void CancelBoardRoamingInteraction()
+ {
+ ResetBoardRoamingGestureState();
+ EndBoardRoaming();
+ }
+
+ private void ResetBoardRoamingGestureState()
+ {
+ _boardRoamingContacts.Clear();
+ _isBoardRoamingTwoFingerGesture = false;
+ _isBoardRoamingPopupDragActive = false;
+ _isBoardRoamingSingleFingerPending = false;
+ }
+
+ ///
+ /// 漫游模式下的接触点落下:第 1 指开始单指拖动,第 2 指切换为双指手势。
+ ///
+ private void BeginBoardRoamingContact(int contactId, Point point)
+ {
+ if (!IsBoardRoamingMode) return;
+ if (_boardRoamingContacts.FindIndex(c => c.Key == contactId) >= 0) return;
+
+ _boardRoamingContacts.Add(new KeyValuePair(contactId, point));
+
+ if (_boardRoamingContacts.Count == 1)
+ {
+ // 先挂起,等位移越过激活阈值再启动单指拖动(见 MoveBoardRoamingContact)。
+ _isBoardRoamingSingleFingerPending = true;
+ _boardRoamingPendingStartPoint = point;
+ }
+ else if (_boardRoamingContacts.Count == 2 && !_isBoardRoamingTwoFingerGesture)
+ {
+ BeginBoardRoamingTwoFingerGesture();
+ }
+ }
+
+ private void MoveBoardRoamingContact(int contactId, Point point)
+ {
+ var index = _boardRoamingContacts.FindIndex(c => c.Key == contactId);
+ if (index < 0) return;
+
+ if (!IsBoardRoamingMode)
+ {
+ ResetBoardRoamingGestureState();
+ EndBoardRoaming();
+ return;
+ }
+
+ var previousPoint = _boardRoamingContacts[index].Value;
+
+ if (_isBoardRoamingTwoFingerGesture && _boardRoamingContacts.Count >= 2)
+ {
+ var previousFirst = _boardRoamingContacts[0].Value;
+ var previousSecond = _boardRoamingContacts[1].Value;
+ _boardRoamingContacts[index] = new KeyValuePair(contactId, point);
+ ApplyBoardRoamingTwoFingerGesture(previousFirst, previousSecond,
+ _boardRoamingContacts[0].Value, _boardRoamingContacts[1].Value);
+ RefreshBoardRoamingPopup(false);
+ return;
+ }
+
+ _boardRoamingContacts[index] = new KeyValuePair(contactId, point);
+
+ if (_isBoardRoamingTwoFingerGesture)
+ {
+ // 双指手势进行中但仅剩一指:继续按该指平移(与批注态 Manipulation 一致)。
+ var delta = point - previousPoint;
+ if (delta.X != 0 || delta.Y != 0)
+ {
+ var matrix = Matrix.Identity;
+ matrix.Translate(delta.X, delta.Y);
+ TransformBoardRoamingContent(matrix, delta, 1);
+ RefreshBoardRoamingPopup(false);
+ }
+ return;
+ }
+
+ if (_isBoardRoamingSingleFingerPending)
+ {
+ var offset = point - _boardRoamingPendingStartPoint;
+ if (offset.Length < BoardRoamingSingleFingerActivationThreshold) return;
+
+ // 激活单指拖动:以当前位置为拖动起点,本次移动不产生位移(无跳变)。
+ _isBoardRoamingSingleFingerPending = false;
+ BeginBoardRoaming(point);
+ return;
+ }
+
+ MoveBoardRoaming(point);
+ }
+
+ ///
+ /// 漫游模式下的接触点抬起。全部抬起时提交手势历史。
+ ///
+ private void EndBoardRoamingContact(int contactId)
+ {
+ var index = _boardRoamingContacts.FindIndex(c => c.Key == contactId);
+ if (index >= 0) _boardRoamingContacts.RemoveAt(index);
+
+ if (_isBoardRoamingTwoFingerGesture)
+ {
+ if (_boardRoamingContacts.Count == 0)
+ EndBoardRoamingTwoFingerGesture();
+ return;
+ }
+
+ if (_boardRoamingContacts.Count == 0)
+ {
+ _isBoardRoamingSingleFingerPending = false;
+ EndBoardRoaming();
+ }
+ }
+
+ private void BeginBoardRoamingTwoFingerGesture()
+ {
+ // 取消单指挂起并提交可能存在的单指拖动阶段历史,双指手势单独成一步撤销记录。
+ _isBoardRoamingSingleFingerPending = false;
+ EndBoardRoaming();
+
+ _isBoardRoamingTwoFingerGesture = true;
+ _boardRoamingStrokeHistory = new Dictionary();
+ foreach (var stroke in inkCanvas.Strokes)
+ _boardRoamingStrokeHistory[stroke] = stroke.StylusPoints.Clone();
+ }
+
+ private void EndBoardRoamingTwoFingerGesture()
+ {
+ _isBoardRoamingTwoFingerGesture = false;
+ _isBoardRoamingPointerDown = false;
+ CommitBoardRoamingHistory();
+ CompletePluginCanvasViewportTransform();
+ inkCanvas.Cursor = IsBoardRoamingMode ? Cursors.Hand : Cursors.Arrow;
+ RefreshBoardRoamingPopup();
+ }
+
+ ///
+ /// 双指手势:以两指连线中点为锚点——内容跟随指心平移,按两指间距比例
+ /// 等比缩放(非变形,横纵向同比例),旋转按两指角度变化绕中点进行。
+ ///
+ private void ApplyBoardRoamingTwoFingerGesture(Point previousFirst, Point previousSecond, Point currentFirst, Point currentSecond)
+ {
+ bool enableZoom = Settings.Gesture.IsEnableTwoFingerZoomRoaming;
+ bool enableRotate = Settings.Gesture.IsEnableTwoFingerRotationRoaming;
+
+ var previousMidpoint = new Point(
+ (previousFirst.X + previousSecond.X) / 2,
+ (previousFirst.Y + previousSecond.Y) / 2);
+ var currentMidpoint = new Point(
+ (currentFirst.X + currentSecond.X) / 2,
+ (currentFirst.Y + currentSecond.Y) / 2);
+
+ var m = new Matrix();
+ double scale = 1;
+
+ if (enableZoom)
+ {
+ var previousDistance = GetDistance(previousFirst, previousSecond);
+ var currentDistance = GetDistance(currentFirst, currentSecond);
+ if (previousDistance > 0.001 && currentDistance > 0.001)
+ {
+ scale = currentDistance / previousDistance;
+ m.ScaleAt(scale, scale, previousMidpoint.X, previousMidpoint.Y);
+ }
+ }
+
+ if (enableRotate)
+ {
+ var previousAngle = Math.Atan2(previousSecond.Y - previousFirst.Y, previousSecond.X - previousFirst.X) * 180 / Math.PI;
+ var currentAngle = Math.Atan2(currentSecond.Y - currentFirst.Y, currentSecond.X - currentFirst.X) * 180 / Math.PI;
+ m.RotateAt(currentAngle - previousAngle, previousMidpoint.X, previousMidpoint.Y);
+ }
+
+ var trans = currentMidpoint - previousMidpoint;
+ m.Translate(trans.X, trans.Y);
+
+ TransformBoardRoamingContent(m, trans, scale);
+ }
+
+ ///
+ /// 对板书内容应用任意矩阵(平移/等比缩放/旋转),同步图片、圆圈标注、插件视口、
+ /// 视口世界坐标与视频展台预览。平移分量单独传给展台(展台不支持缩放旋转)。
+ ///
+ private void TransformBoardRoamingContent(Matrix matrix, Vector translationDelta, double scale)
+ {
+ var previousCommitType = _currentCommitType;
+ _currentCommitType = CommitReason.CodeInput;
+ try
+ {
+ foreach (var stroke in inkCanvas.Strokes)
+ {
+ stroke.Transform(matrix, false);
+ if (scale != 1)
+ {
+ try
+ {
+ stroke.DrawingAttributes.Width *= scale;
+ stroke.DrawingAttributes.Height *= scale;
+ }
+ catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); }
+ }
+ }
+
+ TransformCanvasImages(matrix);
+
+ foreach (var circle in circles)
+ {
+ circle.R = GetDistance(circle.Stroke.StylusPoints[0].ToPoint(),
+ circle.Stroke.StylusPoints[circle.Stroke.StylusPoints.Count / 2].ToPoint()) / 2;
+ circle.Centroid = new Point(
+ (circle.Stroke.StylusPoints[0].X +
+ circle.Stroke.StylusPoints[circle.Stroke.StylusPoints.Count / 2].X) / 2,
+ (circle.Stroke.StylusPoints[0].Y +
+ circle.Stroke.StylusPoints[circle.Stroke.StylusPoints.Count / 2].Y) / 2);
+ }
+
+ PublishPluginCanvasViewportTransform(matrix);
+
+ if (_isVideoPresenterSpecialMode)
+ {
+ _boothPreviewTranslateX += translationDelta.X;
+ _boothPreviewTranslateY += translationDelta.Y;
+ ApplyBoothPreviewTransform();
+ ResetRotationBaseline();
+ }
+
+ // 视口世界坐标跟随内容变换的逆矩阵(纯平移时等价于 pos -= delta,与既有行为一致)。
+ if (matrix.HasInverse)
+ {
+ var inverse = matrix;
+ inverse.Invert();
+ var origin = inverse.Transform(new Point(0, 0));
+ _boardRoamingViewportWorldPosition = new Point(
+ _boardRoamingViewportWorldPosition.X + origin.X,
+ _boardRoamingViewportWorldPosition.Y + origin.Y);
+ }
+ }
+ finally
+ {
+ _currentCommitType = previousCommitType;
+ }
+ }
+
private void CommitBoardRoamingHistory()
{
if (_boardRoamingStrokeHistory == null) return;
@@ -144,12 +400,29 @@ private void ShowBoardRoamingPopup()
if (BoardRoamingPopup == null || BoardRoamingPopupContent == null) return;
AttachBoardRoamingPopupEvents();
+ SyncBoardRoamingToggleStates();
BoardRoamingPopup.IsOpen = false;
RefreshBoardRoamingPopup();
AnimationsHelper.ShowPopupWithSlideAndFade(BoardRoamingPopup);
_popupManager?.BringToFront(BoardRoamingPopup);
}
+ private void SyncBoardRoamingToggleStates()
+ {
+ _isSyncingBoardRoamingToggles = true;
+ try
+ {
+ if (BoardRoamingPopupContent.TwoFingerZoomToggle != null)
+ BoardRoamingPopupContent.TwoFingerZoomToggle.IsOn = Settings.Gesture.IsEnableTwoFingerZoomRoaming;
+ if (BoardRoamingPopupContent.TwoFingerRotationToggle != null)
+ BoardRoamingPopupContent.TwoFingerRotationToggle.IsOn = Settings.Gesture.IsEnableTwoFingerRotationRoaming;
+ }
+ finally
+ {
+ _isSyncingBoardRoamingToggles = false;
+ }
+ }
+
private void AttachBoardRoamingPopupEvents()
{
if (_boardRoamingPopupEventsAttached || BoardRoamingPopupContent == null) return;
@@ -158,10 +431,50 @@ private void AttachBoardRoamingPopupEvents()
BoardRoamingPopupContent.ViewportDragStarted += BeginBoardRoamingPopupDrag;
BoardRoamingPopupContent.ViewportDragCompleted += EndBoardRoamingPopupDrag;
if (BoardRoamingPopupContent.CloseButtonControl != null)
- BoardRoamingPopupContent.CloseButtonControl.Click += (s, e) => BoardRoamingPopup.IsOpen = false;
+ {
+ // 按下阶段立即关闭,避免首次点击被拖拽/捕获等逻辑吞掉导致需要点两次。
+ BoardRoamingPopupContent.CloseButtonControl.PreviewMouseLeftButtonDown +=
+ BoardRoamingCloseButton_PreviewInputDown;
+ BoardRoamingPopupContent.CloseButtonControl.PreviewStylusDown +=
+ BoardRoamingCloseButton_PreviewStylusDown;
+ }
+ if (BoardRoamingPopupContent.TwoFingerZoomToggle != null)
+ BoardRoamingPopupContent.TwoFingerZoomToggle.Toggled += BoardRoamingTwoFingerZoom_Toggled;
+ if (BoardRoamingPopupContent.TwoFingerRotationToggle != null)
+ BoardRoamingPopupContent.TwoFingerRotationToggle.Toggled += BoardRoamingTwoFingerRotation_Toggled;
_boardRoamingPopupEventsAttached = true;
}
+ private void BoardRoamingCloseButton_PreviewInputDown(object sender, MouseButtonEventArgs e)
+ {
+ BoardRoamingPopup.IsOpen = false;
+ e.Handled = true;
+ }
+
+ private void BoardRoamingCloseButton_PreviewStylusDown(object sender, StylusDownEventArgs e)
+ {
+ BoardRoamingPopup.IsOpen = false;
+ e.Handled = true;
+ }
+
+ private void BoardRoamingTwoFingerZoom_Toggled(object sender, RoutedEventArgs e)
+ {
+ if (_isSyncingBoardRoamingToggles) return;
+ var toggle = sender as iNKORE.UI.WPF.Modern.Controls.ToggleSwitch;
+ if (toggle == null) return;
+ Settings.Gesture.IsEnableTwoFingerZoomRoaming = toggle.IsOn;
+ SaveSettingsToFile();
+ }
+
+ private void BoardRoamingTwoFingerRotation_Toggled(object sender, RoutedEventArgs e)
+ {
+ if (_isSyncingBoardRoamingToggles) return;
+ var toggle = sender as iNKORE.UI.WPF.Modern.Controls.ToggleSwitch;
+ if (toggle == null) return;
+ Settings.Gesture.IsEnableTwoFingerRotationRoaming = toggle.IsOn;
+ SaveSettingsToFile();
+ }
+
private void RefreshBoardRoamingPopup()
{
RefreshBoardRoamingPopup(true);
@@ -306,9 +619,9 @@ private void BoardRoamingPopupContent_ViewportPositionChanged(Point previewPosit
private void BeginBoardRoamingPopupDrag()
{
- if (_isBoardRoamingPointerDown) return;
+ if (_isBoardRoamingPopupDragActive || _isBoardRoamingPointerDown || _boardRoamingContacts.Count > 0) return;
- _isBoardRoamingPointerDown = true;
+ _isBoardRoamingPopupDragActive = true;
_boardRoamingStrokeHistory = new Dictionary();
foreach (var stroke in inkCanvas.Strokes)
_boardRoamingStrokeHistory[stroke] = stroke.StylusPoints.Clone();
@@ -316,9 +629,9 @@ private void BeginBoardRoamingPopupDrag()
private void EndBoardRoamingPopupDrag()
{
- if (!_isBoardRoamingPointerDown) return;
+ if (!_isBoardRoamingPopupDragActive) return;
- _isBoardRoamingPointerDown = false;
+ _isBoardRoamingPopupDragActive = false;
CommitBoardRoamingHistory();
CompletePluginCanvasViewportTransform();
RefreshBoardRoamingPopup();
@@ -328,28 +641,7 @@ private void TranslateBoardRoamingContent(double deltaX, double deltaY)
{
var matrix = Matrix.Identity;
matrix.Translate(deltaX, deltaY);
- var previousCommitType = _currentCommitType;
- _currentCommitType = CommitReason.CodeInput;
- try
- {
- foreach (var stroke in inkCanvas.Strokes)
- stroke.Transform(matrix, false);
- TransformCanvasImages(matrix);
- PublishPluginCanvasViewportTransform(matrix);
- // 视频展台特殊模式:漫游时预览画面与墨迹同步平移
- // (否则只有墨迹会动,展台背景不动)
- if (_isVideoPresenterSpecialMode)
- {
- _boothPreviewTranslateX += deltaX;
- _boothPreviewTranslateY += deltaY;
- ApplyBoothPreviewTransform();
- ResetRotationBaseline();
- }
- }
- finally
- {
- _currentCommitType = previousCommitType;
- }
+ TransformBoardRoamingContent(matrix, new Vector(deltaX, deltaY), 1);
}
private static bool AreStylusPointsEqual(StylusPointCollection first, StylusPointCollection second)
diff --git a/Ink Canvas/MainWindow_cs/MW_FloatingBarIcons.cs b/Ink Canvas/MainWindow_cs/MW_FloatingBarIcons.cs
index 54dbfbd7b..8c695dc11 100644
--- a/Ink Canvas/MainWindow_cs/MW_FloatingBarIcons.cs
+++ b/Ink Canvas/MainWindow_cs/MW_FloatingBarIcons.cs
@@ -3650,7 +3650,7 @@ internal void PenIcon_Click(object sender, MouseButtonEventArgs e)
{
if (TryBlockFrozenPageMutation("切换到画笔")) return;
- EndBoardRoaming();
+ CancelBoardRoamingInteraction();
if (lastBorderMouseDownObject is Panel panel)
panel.Background = new SolidColorBrush(Colors.Transparent);
diff --git a/Ink Canvas/MainWindow_cs/MW_TouchEvents.cs b/Ink Canvas/MainWindow_cs/MW_TouchEvents.cs
index c61bb80a7..50b8e33ba 100644
--- a/Ink Canvas/MainWindow_cs/MW_TouchEvents.cs
+++ b/Ink Canvas/MainWindow_cs/MW_TouchEvents.cs
@@ -2576,11 +2576,13 @@ private void Main_Grid_ManipulationDelta(object sender, ManipulationDeltaEventAr
bool isBoardMode = currentMode == 1;
bool enableTranslate = IsBoardRoamingMode || (isBoardMode ? Settings.Gesture.IsEnableTwoFingerTranslateBoard : Settings.Gesture.IsEnableTwoFingerTranslate);
- bool enableRotate = !IsBoardRoamingMode && (isBoardMode ? Settings.Gesture.IsEnableTwoFingerRotationBoard : Settings.Gesture.IsEnableTwoFingerRotation);
- bool enableZoom = !IsBoardRoamingMode && (isBoardMode ? Settings.Gesture.IsEnableTwoFingerZoomBoard : Settings.Gesture.IsEnableTwoFingerZoom);
- bool enableGestureTranslateOrRotate = IsBoardRoamingMode || (isBoardMode
- ? (Settings.Gesture.IsEnableTwoFingerTranslateBoard || Settings.Gesture.IsEnableTwoFingerRotationBoard)
- : (Settings.Gesture.IsEnableTwoFingerTranslate || Settings.Gesture.IsEnableTwoFingerRotation));
+ bool enableRotate = IsBoardRoamingMode
+ ? Settings.Gesture.IsEnableTwoFingerRotationRoaming
+ : (isBoardMode ? Settings.Gesture.IsEnableTwoFingerRotationBoard : Settings.Gesture.IsEnableTwoFingerRotation);
+ bool enableZoom = IsBoardRoamingMode
+ ? Settings.Gesture.IsEnableTwoFingerZoomRoaming
+ : (isBoardMode ? Settings.Gesture.IsEnableTwoFingerZoomBoard : Settings.Gesture.IsEnableTwoFingerZoom);
+ bool enableGestureTranslateOrRotate = enableTranslate || enableRotate;
if (enableTranslate)
m.Translate(trans.X, trans.Y); // 移动
diff --git a/Ink Canvas/Properties/FloatingBarStrings.en-US.resx b/Ink Canvas/Properties/FloatingBarStrings.en-US.resx
index b3b6d4d80..9c2eb07df 100644
--- a/Ink Canvas/Properties/FloatingBarStrings.en-US.resx
+++ b/Ink Canvas/Properties/FloatingBarStrings.en-US.resx
@@ -703,7 +703,7 @@
Annotation status dot hint
- Show a hint when clicking repeatedly in annotation mode to remind you're in annotation mode
+ In annotation mode, once nothing has been written for 30 seconds, each canvas click briefly flashes a status dot; reaching the click threshold within a short time also shows the "annotating" hint. Writing a long stroke restarts the timer.
Cluster radius
diff --git a/Ink Canvas/Properties/FloatingBarStrings.resx b/Ink Canvas/Properties/FloatingBarStrings.resx
index 772641abc..3925b82e8 100644
--- a/Ink Canvas/Properties/FloatingBarStrings.resx
+++ b/Ink Canvas/Properties/FloatingBarStrings.resx
@@ -703,7 +703,7 @@
批注状态点提示
- 批注模式下连续点击画布时,显示提示提醒当前处于批注模式
+ 批注模式下 30 秒未书写后,点击画布会短暂闪现状态小圆点;短时内连续点击达到次数阈值时,才显示「批注中」提示。书写长笔迹会重新计时。
连续点击范围大小
diff --git a/Ink Canvas/Properties/FloatingBarStrings.zh-ME.resx b/Ink Canvas/Properties/FloatingBarStrings.zh-ME.resx
index 2707613fb..839204f87 100644
--- a/Ink Canvas/Properties/FloatingBarStrings.zh-ME.resx
+++ b/Ink Canvas/Properties/FloatingBarStrings.zh-ME.resx
@@ -676,7 +676,7 @@
批注状态点提示
- 在批注中一直点画布,会弹出来提醒你现在是在批注中哦
+ 批注中 30 秒没写字时,点画布会闪出状态小圆点;短时间内连点到次数,才会跳出「正在批注」的提示。写长一点的笔迹会重新计时。
连续点击范围大小
diff --git a/Ink Canvas/Resources/Settings.cs b/Ink Canvas/Resources/Settings.cs
index 86f60ff7b..9acaa277c 100644
--- a/Ink Canvas/Resources/Settings.cs
+++ b/Ink Canvas/Resources/Settings.cs
@@ -618,6 +618,11 @@ public class Gesture
public bool IsEnableTwoFingerTranslateBoard { get; set; } = true;
[JsonProperty("isEnableTwoFingerRotationBoard")]
public bool IsEnableTwoFingerRotationBoard { get; set; }
+
+ [JsonProperty("isEnableTwoFingerZoomRoaming")]
+ public bool IsEnableTwoFingerZoomRoaming { get; set; } = true;
+ [JsonProperty("isEnableTwoFingerRotationRoaming")]
+ public bool IsEnableTwoFingerRotationRoaming { get; set; }
}
// 更新通道枚举
diff --git a/Ink Canvas/Windows/OobePresetWindow.xaml.cs b/Ink Canvas/Windows/OobePresetWindow.xaml.cs
index 017d5d5f6..15f866f98 100644
--- a/Ink Canvas/Windows/OobePresetWindow.xaml.cs
+++ b/Ink Canvas/Windows/OobePresetWindow.xaml.cs
@@ -238,7 +238,6 @@ public static void ApplyLite(Settings settings)
settings.Canvas.DisablePressure = false;
settings.Canvas.HideStrokeWhenSelecting = true;
settings.Canvas.EnablePalmEraser = false;
- settings.Canvas.IsEnableAnnotationDotHint = false;
// 墨迹纠正
settings.InkToShape.IsInkToShapeEnabled = false;