Skip to content

Commit 9ea2241

Browse files
v3: finger touch panning/zoom + textbox auto-activates text tool
1 parent 448c65f commit 9ea2241

16 files changed

Lines changed: 766 additions & 91 deletions

Controls/PdfPageControl.xaml

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,18 @@
66
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
77
mc:Ignorable="d">
88

9-
<Grid x:Name="PageGrid" Background="White">
10-
<!-- PDF Background -->
11-
<Image x:Name="PdfImage" Stretch="Uniform" HorizontalAlignment="Center" VerticalAlignment="Top"
12-
RenderOptions.BitmapScalingMode="HighQuality"/>
9+
<Grid x:Name="RootGrid">
10+
<!-- Shadow casting Border (separate from content to maintain hardware accel for ink) -->
11+
<Border Background="White">
12+
<Border.Effect>
13+
<DropShadowEffect BlurRadius="12" ShadowDepth="2" Opacity="0.10" Color="Black" />
14+
</Border.Effect>
15+
</Border>
16+
17+
<Grid x:Name="PageGrid" Background="White">
18+
<!-- PDF Background -->
19+
<Image x:Name="PdfImage" Stretch="Uniform" HorizontalAlignment="Center" VerticalAlignment="Top"
20+
RenderOptions.BitmapScalingMode="HighQuality"/>
1321

1422
<!-- Ink Layer (WPF InkCanvas) -->
1523
<InkCanvas x:Name="InkCanvas" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Background="Transparent"
@@ -18,7 +26,10 @@
1826
Stylus.IsFlicksEnabled="False" />
1927

2028
<!-- Text/Widget Layer -->
21-
<Canvas x:Name="TextOverlayCanvas" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Background="Transparent"/>
29+
<Canvas x:Name="TextOverlayCanvas" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"/>
30+
31+
<!-- Persistent Text Highlights Layer -->
32+
<Canvas x:Name="HighlightsCanvas" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Background="Transparent" IsHitTestVisible="False"/>
2233

2334
<!-- PDF Text Selection Layer -->
2435
<Canvas x:Name="PdfTextSelectionCanvas" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
@@ -39,6 +50,7 @@
3950
Visibility="Collapsed"
4051
IsHitTestVisible="False"/>
4152
</Canvas>
53+
</Grid>
4254
</Grid>
4355
</UserControl>
4456

Controls/PdfPageControl.xaml.cs

Lines changed: 132 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -146,9 +146,16 @@ public BitmapSource PageSource
146146
/// </summary>
147147
public bool TiltEnabled { get; set; } = true;
148148

149+
// Custom stroke collection to prevent WPF's InkCanvas from clearing strokes
150+
// during visibility or EditingMode toggles.
151+
private readonly StrokeCollection _strokes = new StrokeCollection();
152+
149153
public PdfPageControl()
150154
{
151155
InitializeComponent();
156+
157+
// Assign stable stroke collection
158+
InkCanvas.Strokes = _strokes;
152159

153160
_drawingAttributes = new DrawingAttributes
154161
{
@@ -164,6 +171,8 @@ public PdfPageControl()
164171
};
165172

166173
InkCanvas.DefaultDrawingAttributes = _drawingAttributes;
174+
InkCanvas.UseCustomCursor = true;
175+
InkCanvas.Cursor = Cursors.None;
167176
InkCanvas.EditingMode = InkCanvasEditingMode.None;
168177
InkCanvas.EditingModeInverted = InkCanvasEditingMode.None; // Disable native inverted erasing so we can use custom logic
169178
InkCanvas.StrokeCollected += InkCanvas_StrokeCollected;
@@ -178,6 +187,25 @@ public PdfPageControl()
178187
Unloaded += PdfPageControl_Unloaded;
179188
}
180189

190+
/// <summary>
191+
/// Returns true if the given stylus device is a finger touch rather
192+
/// than a pen/stylus. Used to let finger events pass through to the
193+
/// WPF manipulation system for pan/zoom gestures.
194+
/// </summary>
195+
private static bool IsTouchFinger(StylusDevice device)
196+
{
197+
if (device == null) return false;
198+
var tablet = device.TabletDevice;
199+
if (tablet == null) return false;
200+
// Pen devices report as Stylus. Some pen-as-touch devices (e.g.
201+
// Huawei M-Pencil) report as Touch but have multiple stylus buttons.
202+
// Real fingers have TabletDeviceType.Touch with ≤1 button.
203+
if (tablet.Type == System.Windows.Input.TabletDeviceType.Stylus)
204+
return false;
205+
return tablet.Type == System.Windows.Input.TabletDeviceType.Touch
206+
&& device.StylusButtons.Count <= 1;
207+
}
208+
181209
/// <summary>
182210
/// Set the shared <see cref="WindowsPenService"/> so this control can
183211
/// probe stylus devices and read pressure/tilt capabilities.
@@ -291,8 +319,9 @@ private void PdfPageControl_RequestBringIntoView(object sender, RequestBringInto
291319

292320
private void InkCanvas_MouseEnter(object sender, MouseEventArgs e)
293321
{
294-
if (_currentMode == CustomInkInputProcessingMode.Erasing || _isStylusInverted)
322+
if (_currentMode == CustomInkInputProcessingMode.Erasing || _isStylusInverted || _currentMode == CustomInkInputProcessingMode.Inking)
295323
{
324+
UpdateBrushIndicatorStyle();
296325
EraserIndicator.Visibility = Visibility.Visible;
297326
Cursor = Cursors.None;
298327
UpdateEraserIndicatorPosition(e.GetPosition(PageGrid));
@@ -344,6 +373,12 @@ private void InkCanvas_StylusInAirMove(object sender, StylusEventArgs e)
344373

345374
if (inverted)
346375
{
376+
UpdateBrushIndicatorStyle();
377+
UpdateEraserIndicatorPosition(e.GetPosition(PageGrid));
378+
}
379+
else if (_currentMode == CustomInkInputProcessingMode.Inking)
380+
{
381+
UpdateBrushIndicatorStyle();
347382
UpdateEraserIndicatorPosition(e.GetPosition(PageGrid));
348383
}
349384
}
@@ -399,6 +434,11 @@ private void PdfTextSelectionCanvas_StylusDown(object sender, StylusDownEventArg
399434
if (!_isPdfTextSelectionEnabled)
400435
return;
401436

437+
// Let finger touches pass through to WPF manipulation (pan/zoom).
438+
// Only pen/stylus devices should trigger PDF text selection.
439+
if (IsTouchFinger(e.StylusDevice))
440+
return;
441+
402442
PdfTextSelectionCanvas.CaptureStylus();
403443
PdfTextSelectionPointerPressed?.Invoke(this, new PdfTextSelectionPointerEventArgs(e.GetPosition(PageGrid), MouseButtonState.Pressed));
404444
e.Handled = true;
@@ -409,6 +449,9 @@ private void PdfTextSelectionCanvas_StylusMove(object sender, StylusEventArgs e)
409449
if (!_isPdfTextSelectionEnabled)
410450
return;
411451

452+
if (IsTouchFinger(e.StylusDevice))
453+
return;
454+
412455
PdfTextSelectionPointerMoved?.Invoke(this, new PdfTextSelectionPointerEventArgs(e.GetPosition(PageGrid), MouseButtonState.Pressed));
413456
if (PdfTextSelectionCanvas.IsStylusCaptured)
414457
e.Handled = true;
@@ -419,6 +462,9 @@ private void PdfTextSelectionCanvas_StylusUp(object sender, StylusEventArgs e)
419462
if (!_isPdfTextSelectionEnabled)
420463
return;
421464

465+
if (IsTouchFinger(e.StylusDevice))
466+
return;
467+
422468
PdfTextSelectionPointerReleased?.Invoke(this, new PdfTextSelectionPointerEventArgs(e.GetPosition(PageGrid), MouseButtonState.Released));
423469
if (PdfTextSelectionCanvas.IsStylusCaptured)
424470
PdfTextSelectionCanvas.ReleaseStylusCapture();
@@ -527,6 +573,7 @@ private void InkCanvas_StylusDown(object sender, StylusDownEventArgs e)
527573
InkCanvas.EditingMode = InkCanvasEditingMode.None;
528574

529575
// Show eraser indicator at contact point
576+
UpdateBrushIndicatorStyle();
530577
EraserIndicator.Visibility = Visibility.Visible;
531578
InkCanvas.Cursor = Cursors.None;
532579
UpdateEraserIndicatorPosition(e.GetPosition(PageGrid));
@@ -575,24 +622,46 @@ private void InkCanvas_StylusUp(object sender, StylusEventArgs e)
575622

576623
private void InkCanvas_MouseMove(object sender, MouseEventArgs e)
577624
{
578-
if (_currentMode == CustomInkInputProcessingMode.Erasing)
625+
if (_currentMode == CustomInkInputProcessingMode.Erasing || _currentMode == CustomInkInputProcessingMode.Inking)
579626
{
627+
UpdateBrushIndicatorStyle();
580628
var point = e.GetPosition(PageGrid);
581629
UpdateEraserIndicatorPosition(point);
582630

583-
if (e.LeftButton == MouseButtonState.Pressed)
631+
if (_currentMode == CustomInkInputProcessingMode.Erasing && e.LeftButton == MouseButtonState.Pressed)
584632
{
585633
EraseStrokesAtPoints(new StylusPointCollection { new StylusPoint(e.GetPosition(InkCanvas).X, e.GetPosition(InkCanvas).Y) });
586634
}
587635
}
588636
}
589637

638+
private void UpdateBrushIndicatorStyle()
639+
{
640+
if (_currentMode == CustomInkInputProcessingMode.Erasing || _isStylusInverted)
641+
{
642+
EraserIndicator.Width = _eraserSize;
643+
EraserIndicator.Height = _eraserSize;
644+
EraserIndicator.Stroke = new SolidColorBrush(Color.FromArgb(255, 0, 120, 212));
645+
EraserIndicator.Fill = new SolidColorBrush(Color.FromArgb(16, 0, 120, 212));
646+
}
647+
else if (_currentMode == CustomInkInputProcessingMode.Inking)
648+
{
649+
double size = _drawingAttributes.Width;
650+
EraserIndicator.Width = Math.Max(4, size);
651+
EraserIndicator.Height = Math.Max(4, size);
652+
653+
Color c = _drawingAttributes.Color;
654+
EraserIndicator.Stroke = new SolidColorBrush(Color.FromArgb(200, c.R, c.G, c.B));
655+
EraserIndicator.Fill = _drawingAttributes.IsHighlighter ? new SolidColorBrush(Color.FromArgb(50, c.R, c.G, c.B)) : Brushes.Transparent;
656+
}
657+
}
658+
590659
private void UpdateEraserIndicatorPosition(Point point)
591660
{
592-
EraserIndicator.Width = _eraserSize;
593-
EraserIndicator.Height = _eraserSize;
594-
Canvas.SetLeft(EraserIndicator, point.X - _eraserSize / 2);
595-
Canvas.SetTop(EraserIndicator, point.Y - _eraserSize / 2);
661+
double w = EraserIndicator.Width;
662+
double h = EraserIndicator.Height;
663+
Canvas.SetLeft(EraserIndicator, point.X - w / 2);
664+
Canvas.SetTop(EraserIndicator, point.Y - h / 2);
596665
EraserIndicator.Visibility = Visibility.Visible;
597666
}
598667

@@ -717,7 +786,9 @@ private static void OnPageSourceChanged(DependencyObject d, DependencyPropertyCh
717786

718787
public void SetMode(bool isTextMode)
719788
{
720-
TextOverlayCanvas.IsHitTestVisible = isTextMode;
789+
// Always keep TextOverlayCanvas hit-testable so textboxes can be clicked in any tool mode.
790+
// Its background is transparent, so non-textbox clicks fall through to InkCanvas below.
791+
TextOverlayCanvas.IsHitTestVisible = true;
721792
}
722793

723794
public void SetPdfTextSelectionEnabled(bool enabled)
@@ -797,7 +868,7 @@ public void SetInputMode(CustomInkInputProcessingMode mode)
797868
if (!_isPdfTextSelectionEnabled)
798869
InkCanvas.IsHitTestVisible = true;
799870
InkCanvas.EditingMode = InkCanvasEditingMode.Ink;
800-
EraserIndicator.Visibility = Visibility.Collapsed;
871+
// Keep indicator visible since we're updating it on mouse/stylus move now
801872
InkCanvas.Cursor = Cursors.Cross;
802873
Cursor = Cursors.Arrow;
803874
break;
@@ -1488,6 +1559,58 @@ private void SelectionOverlayCanvas_StylusUp(object sender, StylusEventArgs e)
14881559
SelectionOverlayCanvas_MouseLeftButtonUpCore();
14891560
e.Handled = true;
14901561
}
1562+
1563+
private readonly List<HighlightAnnotation> _highlights = new();
1564+
1565+
public IReadOnlyList<HighlightAnnotation> GetHighlights() => _highlights;
1566+
1567+
public void AddHighlightAnnotation(IReadOnlyList<Rect> rects, Color color)
1568+
{
1569+
var highlight = new HighlightAnnotation
1570+
{
1571+
R = color.R,
1572+
G = color.G,
1573+
B = color.B,
1574+
A = 120 // Semi-transparent overlay
1575+
};
1576+
1577+
foreach (var r in rects)
1578+
{
1579+
highlight.Rects.Add(new double[] { r.X, r.Y, r.Width, r.Height });
1580+
}
1581+
1582+
_highlights.Add(highlight);
1583+
RenderHighlightVisual(highlight);
1584+
}
1585+
1586+
public void AddHighlight(HighlightAnnotation highlight)
1587+
{
1588+
_highlights.Add(highlight);
1589+
RenderHighlightVisual(highlight);
1590+
}
1591+
1592+
private void RenderHighlightVisual(HighlightAnnotation highlight)
1593+
{
1594+
var color = Color.FromArgb(highlight.A, highlight.R, highlight.G, highlight.B);
1595+
var brush = new SolidColorBrush(color);
1596+
1597+
foreach (var rectInfo in highlight.Rects)
1598+
{
1599+
if (rectInfo.Length >= 4)
1600+
{
1601+
var rect = new System.Windows.Shapes.Rectangle
1602+
{
1603+
Width = rectInfo[2],
1604+
Height = rectInfo[3],
1605+
Fill = brush,
1606+
IsHitTestVisible = false
1607+
};
1608+
Canvas.SetLeft(rect, rectInfo[0]);
1609+
Canvas.SetTop(rect, rectInfo[1]);
1610+
HighlightsCanvas.Children.Add(rect);
1611+
}
1612+
}
1613+
}
14911614
}
14921615
}
14931616

MainWindow.xaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,7 @@
210210
<Grid x:Name="TabContentArea" Grid.Row="2"/>
211211

212212
<Border x:Name="ToastBorder" Grid.Row="2" HorizontalAlignment="Center" VerticalAlignment="Bottom"
213-
Margin="0,0,0,28" Visibility="Collapsed" Opacity="0" IsHitTestVisible="False"
213+
Margin="0,0,0,80" Visibility="Collapsed" Opacity="0" IsHitTestVisible="False"
214214
CornerRadius="10" Padding="20,10">
215215
<Border.Background>
216216
<SolidColorBrush Color="#1A1A1A" Opacity="0.88"/>

Models/AnnotationModels.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ public class PageAnnotation
1212
{
1313
public List<StrokeAnnotation> Strokes { get; set; } = new();
1414
public List<TextAnnotation> Texts { get; set; } = new();
15+
public List<HighlightAnnotation> Highlights { get; set; } = new();
1516
}
1617

1718
public class StrokeAnnotation
@@ -35,4 +36,14 @@ public class TextAnnotation
3536
public byte B { get; set; }
3637
public double FontSize { get; set; } = 18;
3738
}
39+
40+
public class HighlightAnnotation
41+
{
42+
// Each array contains [X, Y, Width, Height]
43+
public List<double[]> Rects { get; set; } = new();
44+
public byte R { get; set; } = 255;
45+
public byte G { get; set; } = 255;
46+
public byte B { get; set; } = 0;
47+
public byte A { get; set; } = 128;
48+
}
3849
}

Models/AppSettings.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ namespace Caelum.Models
33
public sealed class AppSettings
44
{
55
public AppLanguage Language { get; set; } = AppLanguage.English;
6+
public bool EnablePressure { get; set; } = true;
67
}
78

89
public sealed class LanguageOption

Pages/EditorPage.xaml

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
Background="Transparent"
66
AllowDrop="True"
77
PreviewMouseDown="EditorPage_PreviewMouseDown"
8-
PreviewStylusDown="EditorPage_PreviewStylusDown">
8+
PreviewStylusDown="EditorPage_PreviewStylusDown"
9+
PreviewKeyDown="EditorPage_PreviewKeyDown">
910

1011
<Page.Resources>
1112
<!-- Toolbar button -->
@@ -84,7 +85,7 @@
8485

8586
<!-- PDF Pages Container (full area) -->
8687
<ScrollViewer x:Name="PdfScrollViewer" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto"
87-
Background="#FFB0B0B0"
88+
Background="#90E5E7EB"
8889
Style="{StaticResource SleekScrollViewer}"
8990
PanningMode="None"
9091
Stylus.IsFlicksEnabled="False"
@@ -146,10 +147,12 @@
146147
<!-- Highlighter -->
147148
<ToggleButton x:Name="HighlighterToolButton" Click="HighlighterToolButton_Click"
148149
Style="{StaticResource ToolbarToggleButtonStyle}"
149-
Width="42" Height="36" Margin="2,0">
150+
Width="42" Height="36" Margin="2,0" ToolTip="Freehand Highlighter">
150151
<TextBlock x:Name="HighlighterIcon" Text="&#xE7E6;" FontFamily="Segoe MDL2 Assets" FontSize="19" Foreground="#F59E0B"/>
151152
</ToggleButton>
152153

154+
155+
153156
<!-- Eraser -->
154157
<ToggleButton x:Name="EraserToolButton" Click="EraserToolButton_Click"
155158
Style="{StaticResource ToolbarToggleButtonStyle}"
@@ -173,8 +176,7 @@
173176
<TextBlock Text="A" FontSize="17" FontWeight="Bold" Foreground="#555"/>
174177
</ToggleButton>
175178

176-
<!-- Separator -->
177-
<Border Width="1" Background="#18000000" Margin="4,8"/>
179+
178180

179181
<!-- Save -->
180182
<Button x:Name="SavePdfButton" Click="SavePdf_Click"
@@ -186,6 +188,13 @@
186188
<!-- Separator -->
187189
<Border Width="1" Background="#18000000" Margin="4,8"/>
188190

191+
<!-- Page Number -->
192+
<TextBlock x:Name="PageNumberText" Text="0 / 0" FontSize="13" Foreground="#555"
193+
VerticalAlignment="Center" Margin="8,0,8,0" ToolTip="Current Page"/>
194+
195+
<!-- Separator -->
196+
<Border Width="1" Background="#18000000" Margin="4,8"/>
197+
189198
<!-- Zoom Out -->
190199
<Button x:Name="ZoomOutButton" Click="ZoomOutButton_Click"
191200
Style="{StaticResource ToolbarButtonStyle}"

0 commit comments

Comments
 (0)