Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions Ink Canvas/Controls/Popups/BoardRoamingPopupContent.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -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">
<Grid>
Expand Down Expand Up @@ -43,6 +44,36 @@
Foreground="{DynamicResource FloatingBarForegroundBrush}"
Opacity="0.72"
FontSize="11" />
<Grid Margin="0,10,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Label Grid.Column="0"
Content="{x:Static props:FloatingBarStrings.FloatingBar_Gesture_TwoFingerZoom}"
Foreground="{DynamicResource FloatingBarForegroundBrush}"
Padding="0"
VerticalAlignment="Center" />
<ui:ToggleSwitch Grid.Column="1"
x:Name="ToggleSwitchEnableTwoFingerZoom"
MinWidth="0" OnContent="" OffContent=""
VerticalAlignment="Center" />
</Grid>
<Grid Margin="0,6,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Label Grid.Column="0"
Content="{x:Static props:FloatingBarStrings.FloatingBar_Gesture_TwoFingerRotate}"
Foreground="{DynamicResource FloatingBarForegroundBrush}"
Padding="0"
VerticalAlignment="Center" />
<ui:ToggleSwitch Grid.Column="1"
x:Name="ToggleSwitchEnableTwoFingerRotation"
MinWidth="0" OnContent="" OffContent=""
VerticalAlignment="Center" />
</Grid>
</StackPanel>
</ContentControl>
</Grid>
Expand Down
3 changes: 3 additions & 0 deletions Ink Canvas/Controls/Popups/BoardRoamingPopupContent.xaml.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using iNKORE.UI.WPF.Modern.Controls;
using System;
using System.Windows;
using System.Windows.Controls;
Expand Down Expand Up @@ -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()
{
Expand Down
31 changes: 29 additions & 2 deletions Ink Canvas/MainWindow.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -339,9 +339,11 @@
</Popup>

<!-- 批注状态点提示:当用户在批注模式下反复点击同一区域时浮现,
提醒用户当前处于批注模式而非鼠标模式。 -->
提醒用户当前处于批注模式而非鼠标模式。
Placement=RelativePoint 相对 inkCanvas,避免 DPI 缩放导致的位置偏差。 -->
<Popup x:Name="AnnotationDotHintPopup"
Placement="Absolute"
Placement="RelativePoint"
PlacementTarget="{Binding ElementName=inkCanvas}"
AllowsTransparency="True"
StaysOpen="True"
IsOpen="False"
Expand Down Expand Up @@ -417,6 +419,31 @@
</Border>
</Popup>

<!-- 批注状态指示小圆点:带圆角容器的短促渐显/渐隐图标,指示当前处于批注状态。
Placement=RelativePoint 相对 inkCanvas,避免 DPI 缩放导致的位置偏差。 -->
<Popup x:Name="AnnotationDotIndicatorPopup"
Placement="RelativePoint"
PlacementTarget="{Binding ElementName=inkCanvas}"
AllowsTransparency="True"
StaysOpen="True"
IsOpen="False"
HorizontalOffset="0"
VerticalOffset="0">
<Border x:Name="AnnotationDotIndicatorBorder"
Width="28" Height="28"
CornerRadius="14"
Background="#B0303030"
BorderBrush="#40FFFFFF"
BorderThickness="1"
SnapsToDevicePixels="True"
Opacity="0">
<Ellipse Width="12" Height="12"
Fill="#FF2563EB"
HorizontalAlignment="Center"
VerticalAlignment="Center" />
</Border>
</Popup>

<!-- 快抽悬浮按钮 -->
<localControls:QuickDrawFloatingButtonControl x:Name="QuickDrawFloatingButton"
HorizontalAlignment="Right"
Expand Down
19 changes: 11 additions & 8 deletions Ink Canvas/MainWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2496,7 +2496,7 @@ private void inkCanvas_StylusDown(object sender, StylusDownEventArgs e)
inkCanvas.CaptureStylus();
ViewboxFloatingBar.IsHitTestVisible = false;
BlackboardUIGridForInkReplay.IsHitTestVisible = false;
BeginBoardRoaming(e.GetPosition(inkCanvas));
BeginBoardRoamingContact(e.StylusDevice.Id, e.GetPosition(inkCanvas));
e.Handled = true;
return;
}
Expand All @@ -2520,22 +2520,25 @@ private void inkCanvas_StylusMove(object sender, StylusEventArgs e)
e.Handled = true;
return;
}
if (!_isBoardRoamingPointerDown) return;
if (!_isBoardRoamingPointerDown && !_isBoardRoamingTwoFingerGesture && _boardRoamingContacts.Count == 0) return;

MoveBoardRoaming(e.GetPosition(inkCanvas));
MoveBoardRoamingContact(e.StylusDevice.Id, e.GetPosition(inkCanvas));
e.Handled = true;
}

// 手写笔抬起事件(用于橡皮擦自动切换)
private void inkCanvas_StylusUp(object sender, StylusEventArgs e)
{
EndSecAgentStrokeErase();
if (_isBoardRoamingPointerDown)
if (_isBoardRoamingPointerDown || _isBoardRoamingTwoFingerGesture || _boardRoamingContacts.Count > 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;
}
Expand Down
151 changes: 111 additions & 40 deletions Ink Canvas/MainWindow_cs/MW_AnnotationDotHint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ public partial class MainWindow
private readonly Queue<Point> _annotationDotPositions = new Queue<Point>();
/// <summary>最近点击位置队列的最大容量。</summary>
private const int AnnotationDotMaxQueueSize = 10;
/// <summary>最近一次「长笔迹」(真实书写)的提交时间;用于 30 秒未书写的空闲门控。</summary>
private DateTime? _lastLongStrokeTime;
/// <summary>距离上次真实书写多久后,功能按「空闲」状态触发(秒)。</summary>
private const double AnnotationDotIdleSeconds = 30;
/// <summary>提示自动隐藏计时器。</summary>
private DispatcherTimer _annotationDotHintTimer;
/// <summary>提示是否正在显示。</summary>
Expand All @@ -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())
Comment thread
PYLXU marked this conversation as resolved.
{
// 30 秒未书写:每次点击都显示指示小圆点
ShowAnnotationDotIndicator(center);
// 短时内连续点击达到阈值 → 显示「批注中」提示
TrackAnnotationDotPosition(center, showHint: true);
}
else
{
// 30 秒内有书写:仅连续点击达到阈值时,在末次点击显示一次指示小圆点(不显示批注提示)
TrackAnnotationDotPosition(center, showHint: false);
}
}
catch (Exception ex)
{
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -122,11 +141,12 @@ private void EnsureDotVisible(Stroke originalStroke, Point center)
}

/// <summary>
/// 记录点击位置到追踪队列,并检查是否需要显示提示。
/// 仅检查最近 N 个点(N = 点击次数阈值),而非队列全部点,
/// 避免跨区域点击导致判定失败。
/// 记录点击位置到追踪队列,并在连续点击达到阈值时执行相应动作。
/// <paramref name="showHint"/> 为 true 时触发「批注中」提示(30 秒空闲后路径);
/// 为 false 时仅显示一次指示小圆点(30 秒内路径,不显示批注提示)。
/// 仅检查最近 N 个点(N = 点击次数阈值),而非队列全部点,避免跨区域误判。
/// </summary>
private void TrackAnnotationDotPosition(Point position)
private void TrackAnnotationDotPosition(Point position, bool showHint)
{
if (double.IsNaN(position.X) || double.IsNaN(position.Y)) return;

Expand All @@ -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();
}
}

Expand Down Expand Up @@ -181,7 +210,9 @@ private bool IsRecentClusterWithinRadius(int count, double radius)
}

/// <summary>
/// 显示批注状态提示。使用屏幕坐标绝对定位,边缘点击时对齐锚点而非居中。
/// 显示批注状态提示。Popup 使用 Placement=RelativePoint 且 PlacementTarget=inkCanvas,
/// 偏移量直接使用画布(逻辑)坐标,避免 DPI 缩放下的屏幕坐标换算偏差。
/// 靠近画布边缘时对齐锚点而非居中。
/// </summary>
private void ShowAnnotationDotHint(Point anchor)
{
Expand All @@ -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;
Expand All @@ -263,6 +290,50 @@ private void ShowAnnotationDotHint(Point anchor)
_annotationDotHintTimer.Start();
}

/// <summary>
/// 当前是否满足「30 秒未在白板内书写」的空闲条件。
/// 从未书写过(<see cref="_lastLongStrokeTime"/> 为空)时视为空闲。
/// </summary>
private bool IsAnnotationIdle()
{
return !_lastLongStrokeTime.HasValue
|| (DateTime.Now - _lastLongStrokeTime.Value).TotalSeconds >= AnnotationDotIdleSeconds;
}

/// <summary>
/// 显示「处于批注状态」的指示小圆点:带圆角容器,渐显出现、短暂停留后渐隐消失。
/// Popup 使用 Placement=RelativePoint 且 PlacementTarget=inkCanvas,坐标直接使用画布(逻辑)坐标。
/// </summary>
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;
Expand Down
Loading