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;