1 parent
5bdc851fa0
commit
67c98c7c0a
19 files changed
+447
-58
No files matched your search
@@ -2,6 +2,20 @@
|
||||
|
||||
Alle noemenswaardige wijzigingen in kniip staan in dit bestand. De nieuwste versie staat bovenaan.
|
||||
|
||||
## 1.1.4 - 2026-11-09
|
||||
|
||||
### Nieuw
|
||||
|
||||
- **Lijnen en pijlen buigen.** Selecteer een lijn of pijl. Midden op elk stuk staat dan een klein rondje. Sleep dat rondje en het wordt een buigpunt. De lijn loopt daarna als vloeiende kromme door alle punten. Zo kun je zoveel buigpunten toevoegen als je wilt.
|
||||
- Dubbelklik op een buigpunt om het weer te verwijderen.
|
||||
- Bij een pijl draait de punt mee met de richting van het laatste stuk.
|
||||
- Met Shift springen begin- en eindpunt in stappen van 45°, gemeten vanaf het buigpunt ernaast.
|
||||
- Toevoegen, verschuiven en verwijderen van buigpunten kun je ongedaan maken.
|
||||
|
||||
### Gewijzigd
|
||||
|
||||
- **Testversie naast kniip.** `build.ps1` maakt nu ook `KniipTest-<versie>.exe`. Die testversie heeft eigen instellingen en registervermeldingen, zodat hij naast de geïnstalleerde kniip kan draaien zonder die te raken. De testversie werkt zichzelf niet bij. Met `-SkipTest` sla je hem over.
|
||||
|
||||
## 1.1.3 - 2026-11-09
|
||||
|
||||
### Nieuw
|
||||
|
||||
@@ -24,7 +24,7 @@ kniip is een annotatie-editor die naast het Windows Knipprogramma werkt. Maak ee
|
||||
|
||||
| Versie | Installer |
|
||||
| ------ | --------- |
|
||||
| 1.1.3 | [Kniip-Setup-1.1.3.exe](https://git.hibbit.net/rogier/kniip/releases/download/1.1.3/Kniip-Setup-1.1.3.exe) |
|
||||
| 1.1.4 | [Kniip-Setup-1.1.4.exe](https://git.hibbit.net/rogier/kniip/releases/download/1.1.4/Kniip-Setup-1.1.4.exe) |
|
||||
|
||||
Oudere versies vind je bij [alle releases](https://git.hibbit.net/rogier/kniip/releases).
|
||||
|
||||
@@ -70,7 +70,7 @@ Vereisten:
|
||||
Bouwen:
|
||||
|
||||
```powershell
|
||||
.\build.ps1 # versie 1.1.3
|
||||
.\build.ps1 # versie 1.1.4
|
||||
.\build.ps1 -Version 1.2.0 # andere versie
|
||||
```
|
||||
|
||||
@@ -79,6 +79,20 @@ Het script levert op:
|
||||
- `artifacts\publish\Kniip.exe`: zelfstandige exe, draait zonder installatie
|
||||
- `artifacts\Kniip-<versie>.exe`: dezelfde exe, met het versienummer in de naam
|
||||
- `artifacts\Kniip-Setup-<versie>.exe`: de installer (alleen als Inno Setup 6 gevonden is)
|
||||
- `artifacts\KniipTest-<versie>.exe`: testversie (overslaan met `-SkipTest`)
|
||||
|
||||
### Testversie
|
||||
|
||||
De testversie draait naast de geïnstalleerde kniip en raakt die niet aan. Hij gebruikt overal `KniipTest` in plaats van `Kniip`: de autostartregel, de 'Openen met'-registratie (`KniipTest.Image`), de instellingenmap `%APPDATA%\KniipTest` en de single-instance-mutex. Bij de eerste start kopieert hij de instellingen van de gewone kniip. Zichzelf bijwerken doet hij niet, want de installer zou de gewone kniip overschrijven.
|
||||
|
||||
Opruimen hoeft alleen als je in de testversie autostart of 'Openen met' hebt aangezet:
|
||||
|
||||
```powershell
|
||||
.\artifacts\KniipTest-<versie>.exe --unregister
|
||||
Remove-Item "$env:APPDATA\KniipTest" -Recurse # optioneel: instellingen van de testversie
|
||||
```
|
||||
|
||||
Draaien beide versies tegelijk, dan reageren ze allebei op dezelfde sneltoetsen en knipsels. Sluit de gewone kniip tijdens het testen, of kies in de testversie andere sneltoetsen.
|
||||
|
||||
## Een nieuwe versie publiceren
|
||||
|
||||
|
||||
@@ -1,19 +1,27 @@
|
||||
# Publiceert kniip als zelfstandige single-file exe (geen .NET-installatie nodig op de doel-pc)
|
||||
# Publiceert kniip als zelfstandige single-file exe (geen .NET-installatie nodig op de doel-pc)
|
||||
# en bouwt de installer als Inno Setup 6 aanwezig is.
|
||||
# ScreenRecorderLib (screencasts) is een C++/CLI-assembly die niet uit de bundel geladen kan worden;
|
||||
# IncludeAllContentForSelfExtract pakt daarom bij de eerste start alles uit naar %TEMP%\.net.
|
||||
#
|
||||
# .\build.ps1 -> artifacts\publish\Kniip.exe (+ installer indien mogelijk)
|
||||
#
|
||||
# Elke build levert ook artifacts\KniipTest-<versie>.exe: een testversie met eigen register-, instellingen-
|
||||
# en mutexnamen (KniipTest), die naast de geïnstalleerde kniip draait zonder diens registratie te raken.
|
||||
#
|
||||
# .\build.ps1 -> artifacts\publish\Kniip.exe (+ installer indien mogelijk) + testexe
|
||||
# .\build.ps1 -Version 1.2.0
|
||||
# .\build.ps1 -SkipTest -> zonder testexe
|
||||
|
||||
param(
|
||||
[string]$Version = "1.1.3",
|
||||
[string]$Runtime = "win-x64"
|
||||
[string]$Version = "1.1.4",
|
||||
[string]$Runtime = "win-x64",
|
||||
[switch]$SkipTest
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$root = $PSScriptRoot
|
||||
$project = Join-Path $root "src\Kniip\Kniip.csproj"
|
||||
$publishDir = Join-Path $root "artifacts\publish"
|
||||
$testPublishDir = Join-Path $root "artifacts\publish-test"
|
||||
|
||||
# Gebruik een SDK in het gebruikersprofiel als er geen systeembrede SDK is.
|
||||
$localDotnet = Join-Path $env:LOCALAPPDATA "Microsoft\dotnet"
|
||||
@@ -22,18 +30,23 @@ if (-not (dotnet --list-sdks 2>$null) -and (Test-Path (Join-Path $localDotnet "d
|
||||
$env:PATH = "$localDotnet;$env:PATH"
|
||||
}
|
||||
|
||||
if (Test-Path $publishDir) { Remove-Item $publishDir -Recurse -Force }
|
||||
function Publish-Kniip([string]$outputDir, [bool]$test) {
|
||||
if (Test-Path $outputDir) { Remove-Item $outputDir -Recurse -Force }
|
||||
|
||||
dotnet publish (Join-Path $root "src\Kniip\Kniip.csproj") `
|
||||
-c Release -r $Runtime --self-contained true `
|
||||
-p:PublishSingleFile=true `
|
||||
-p:IncludeNativeLibrariesForSelfExtract=true `
|
||||
-p:IncludeAllContentForSelfExtract=true `
|
||||
-p:EnableCompressionInSingleFile=true `
|
||||
-p:DebugType=none `
|
||||
-p:Version=$Version `
|
||||
-o $publishDir
|
||||
if ($LASTEXITCODE -ne 0) { throw "dotnet publish is mislukt." }
|
||||
dotnet publish $project `
|
||||
-c Release -r $Runtime --self-contained true `
|
||||
-p:PublishSingleFile=true `
|
||||
-p:IncludeNativeLibrariesForSelfExtract=true `
|
||||
-p:IncludeAllContentForSelfExtract=true `
|
||||
-p:EnableCompressionInSingleFile=true `
|
||||
-p:DebugType=none `
|
||||
-p:Version=$Version `
|
||||
-p:KniipTest=$($test.ToString().ToLowerInvariant()) `
|
||||
-o $outputDir
|
||||
if ($LASTEXITCODE -ne 0) { throw "dotnet publish is mislukt." }
|
||||
}
|
||||
|
||||
Publish-Kniip $publishDir $false
|
||||
|
||||
Write-Host "Gepubliceerd: $publishDir\Kniip.exe" -ForegroundColor Green
|
||||
|
||||
@@ -55,3 +68,10 @@ if ($iscc) {
|
||||
} else {
|
||||
Write-Host "Inno Setup 6 niet gevonden; installer overgeslagen (winget install JRSoftware.InnoSetup)." -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
if (-not $SkipTest) {
|
||||
Publish-Kniip $testPublishDir $true
|
||||
$testExe = Join-Path $root "artifacts\KniipTest-$Version.exe"
|
||||
Copy-Item (Join-Path $testPublishDir "Kniip.exe") $testExe -Force
|
||||
Write-Host "Testversie: $testExe" -ForegroundColor Green
|
||||
}
|
||||
+1
-1
@@ -5,7 +5,7 @@
|
||||
#define AppName "kniip"
|
||||
#define AppExe "Kniip.exe"
|
||||
#ifndef AppVersion
|
||||
#define AppVersion "1.1.3"
|
||||
#define AppVersion "1.1.4"
|
||||
#endif
|
||||
#ifndef PublishDir
|
||||
#define PublishDir "..\artifacts\publish"
|
||||
|
||||
@@ -293,6 +293,92 @@
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!-- Contextmenu's: icoon, tekst, sneltoets en eventueel een submenu -->
|
||||
<Style TargetType="ContextMenu">
|
||||
<Setter Property="FontFamily" Value="Segoe UI" />
|
||||
<Setter Property="FontSize" Value="13" />
|
||||
<Setter Property="Foreground" Value="{StaticResource InkBrush}" />
|
||||
<Setter Property="HasDropShadow" Value="False" />
|
||||
<Setter Property="Grid.IsSharedSizeScope" Value="True" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="ContextMenu">
|
||||
<Border Background="{StaticResource ToolbarBrush}" BorderBrush="#D6D6D6" BorderThickness="1" CornerRadius="8"
|
||||
Padding="4" Margin="0,0,10,10" MinWidth="220">
|
||||
<Border.Effect>
|
||||
<DropShadowEffect BlurRadius="12" ShadowDepth="2" Opacity="0.18" />
|
||||
</Border.Effect>
|
||||
<ItemsPresenter KeyboardNavigation.DirectionalNavigation="Cycle" />
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="{x:Static MenuItem.SeparatorStyleKey}" TargetType="Separator">
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Separator">
|
||||
<Border Height="1" Margin="6,4" Background="{StaticResource DividerBrush}" />
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style TargetType="MenuItem">
|
||||
<Setter Property="Foreground" Value="{StaticResource InkBrush}" />
|
||||
<Setter Property="FocusVisualStyle" Value="{x:Null}" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="MenuItem">
|
||||
<Border x:Name="Bd" Background="Transparent" CornerRadius="4" Padding="8,6" SnapsToDevicePixels="True">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="28" />
|
||||
<ColumnDefinition Width="*" SharedSizeGroup="MenuHeader" />
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="MenuGesture" />
|
||||
<ColumnDefinition Width="16" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<ContentPresenter x:Name="IconHost" ContentSource="Icon" VerticalAlignment="Center" HorizontalAlignment="Left" />
|
||||
<TextBlock x:Name="Check" Text="" FontFamily="{StaticResource IconFont}" FontSize="12"
|
||||
VerticalAlignment="Center" HorizontalAlignment="Left" Visibility="Collapsed" />
|
||||
<ContentPresenter Grid.Column="1" ContentSource="Header" RecognizesAccessKey="True" VerticalAlignment="Center" />
|
||||
<TextBlock Grid.Column="2" Text="{TemplateBinding InputGestureText}" Margin="24,0,0,0"
|
||||
Foreground="{StaticResource SubtleTextBrush}" VerticalAlignment="Center" HorizontalAlignment="Right" />
|
||||
<TextBlock x:Name="Arrow" Grid.Column="3" Text="" FontFamily="{StaticResource IconFont}" FontSize="10"
|
||||
VerticalAlignment="Center" HorizontalAlignment="Right" Visibility="Collapsed" />
|
||||
<Popup x:Name="PART_Popup" Placement="Right" HorizontalOffset="6" VerticalOffset="-6" AllowsTransparency="True"
|
||||
Focusable="False" PopupAnimation="Fade" IsOpen="{Binding IsSubmenuOpen, RelativeSource={RelativeSource TemplatedParent}}">
|
||||
<Border Background="{StaticResource ToolbarBrush}" BorderBrush="#D6D6D6" BorderThickness="1" CornerRadius="8"
|
||||
Padding="4" Margin="0,0,10,10" MinWidth="180" Grid.IsSharedSizeScope="True">
|
||||
<Border.Effect>
|
||||
<DropShadowEffect BlurRadius="12" ShadowDepth="2" Opacity="0.18" />
|
||||
</Border.Effect>
|
||||
<ItemsPresenter KeyboardNavigation.DirectionalNavigation="Cycle" />
|
||||
</Border>
|
||||
</Popup>
|
||||
</Grid>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsChecked" Value="True">
|
||||
<Setter TargetName="Check" Property="Visibility" Value="Visible" />
|
||||
<Setter TargetName="IconHost" Property="Visibility" Value="Collapsed" />
|
||||
</Trigger>
|
||||
<Trigger Property="HasItems" Value="True">
|
||||
<Setter TargetName="Arrow" Property="Visibility" Value="Visible" />
|
||||
</Trigger>
|
||||
<Trigger Property="IsHighlighted" Value="True">
|
||||
<Setter TargetName="Bd" Property="Background" Value="{StaticResource HoverBrush}" />
|
||||
</Trigger>
|
||||
<Trigger Property="IsEnabled" Value="False">
|
||||
<Setter Property="Opacity" Value="0.4" />
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="PropCombo" TargetType="ComboBox">
|
||||
<Setter Property="MinWidth" Value="80" />
|
||||
<Setter Property="Height" Value="28" />
|
||||
|
||||
@@ -150,7 +150,7 @@ public partial class App : Application
|
||||
if (isFirstStart)
|
||||
{
|
||||
string? hotkey = HotkeyDisplay(Settings.SnipHotkey);
|
||||
_tray?.ShowBalloon("kniip is actief", hotkey == null
|
||||
_tray?.ShowBalloon($"{AppIdentity.DisplayName} is actief", hotkey == null
|
||||
? "Maak een knipsel met Win + Shift + S. kniip biedt het daarna aan om te bewerken."
|
||||
: $"Druk op {hotkey} en sleep een rechthoek om te knippen en te bewerken.");
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ namespace Kniip.Editor;
|
||||
public sealed class AnnotationSurface : FrameworkElement
|
||||
{
|
||||
private static readonly Brush AccentBrush = RenderHelpers.Freeze(new SolidColorBrush(Palette.Accent));
|
||||
private static readonly Brush GhostHandleBrush = RenderHelpers.Freeze(new SolidColorBrush(Color.FromArgb(170, 255, 255, 255)));
|
||||
private static readonly Brush CropShadeBrush = RenderHelpers.Freeze(new SolidColorBrush(Color.FromArgb(120, 0, 0, 0)));
|
||||
|
||||
public EditorDocument? Document { get; set; }
|
||||
@@ -74,8 +75,16 @@ public sealed class AnnotationSurface : FrameworkElement
|
||||
|
||||
double size = HandleSize;
|
||||
var pen = new Pen(AccentBrush, 1.2 / ViewScale);
|
||||
foreach (Point h in handles)
|
||||
for (int i = 0; i < handles.Count; i++)
|
||||
{
|
||||
Point h = handles[i];
|
||||
if (item.IsGhostHandle(i))
|
||||
{
|
||||
double radius = size * 0.35;
|
||||
dc.DrawEllipse(GhostHandleBrush, pen, h, radius, radius);
|
||||
continue;
|
||||
}
|
||||
|
||||
dc.DrawRoundedRectangle(Brushes.White, pen, new Rect(h.X - size / 2, h.Y - size / 2, size, size),
|
||||
1.5 / ViewScale, 1.5 / ViewScale);
|
||||
}
|
||||
@@ -90,14 +99,20 @@ public sealed class AnnotationSurface : FrameworkElement
|
||||
{
|
||||
IReadOnlyList<Point> handles = item.GetHandles();
|
||||
double tolerance = HandleSize / 2 + 2 / ViewScale;
|
||||
int ghost = -1;
|
||||
for (int i = handles.Count - 1; i >= 0; i--)
|
||||
{
|
||||
if (System.Math.Abs(handles[i].X - p.X) <= tolerance && System.Math.Abs(handles[i].Y - p.Y) <= tolerance)
|
||||
{
|
||||
return i;
|
||||
if (!item.IsGhostHandle(i))
|
||||
{
|
||||
return i;
|
||||
}
|
||||
|
||||
ghost = i;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
return ghost;
|
||||
}
|
||||
}
|
||||
@@ -33,6 +33,9 @@ public abstract class Annotation
|
||||
|
||||
public virtual IReadOnlyList<Point> GetHandles() => Array.Empty<Point>();
|
||||
|
||||
/// <summary>Een greep die pas iets wordt als je hem sleept, zoals het middenpunt om een lijn te buigen.</summary>
|
||||
public virtual bool IsGhostHandle(int index) => false;
|
||||
|
||||
public virtual void BeginHandleDrag(int index)
|
||||
{
|
||||
}
|
||||
@@ -41,6 +44,8 @@ public abstract class Annotation
|
||||
{
|
||||
}
|
||||
|
||||
public virtual bool RemoveHandle(int index) => false;
|
||||
|
||||
/// <summary>Te klein om te bewaren na het tekenen (bijv. een losse klik met het rechthoek-gereedschap).</summary>
|
||||
public virtual bool IsDegenerate => false;
|
||||
|
||||
|
||||
@@ -5,8 +5,16 @@ using System.Windows.Media;
|
||||
|
||||
namespace Kniip.Editor.Annotations;
|
||||
|
||||
/// <summary>
|
||||
/// Lijn of pijl. Met buigpunten wordt het een vloeiende kromme die door alle punten loopt.
|
||||
/// </summary>
|
||||
public sealed class LineAnnotation : Annotation
|
||||
{
|
||||
private const int SamplesPerSegment = 16;
|
||||
|
||||
private int _dragBend = -1;
|
||||
private int _pendingGhost = -1;
|
||||
|
||||
public LineAnnotation(bool isArrow)
|
||||
{
|
||||
IsArrow = isArrow;
|
||||
@@ -18,27 +26,124 @@ public sealed class LineAnnotation : Annotation
|
||||
|
||||
public Point End { get; set; }
|
||||
|
||||
/// <summary>Tussenpunten waar de kromme doorheen loopt, in volgorde van begin naar eind.</summary>
|
||||
public List<Point> Bends { get; set; } = new();
|
||||
|
||||
public override ToolKind Kind => IsArrow ? ToolKind.Arrow : ToolKind.Line;
|
||||
|
||||
public override Rect Bounds
|
||||
{
|
||||
get
|
||||
{
|
||||
var r = new Rect(Start, End);
|
||||
List<Point> points = Flatten();
|
||||
var r = new Rect(points[0], points[0]);
|
||||
foreach (Point p in points)
|
||||
{
|
||||
r.Union(p);
|
||||
}
|
||||
|
||||
double pad = Style.Thickness / 2 + (IsArrow ? HeadLength / 2 : 0);
|
||||
r.Inflate(pad, pad);
|
||||
return r;
|
||||
}
|
||||
}
|
||||
|
||||
public override bool IsDegenerate => (End - Start).Length < 4;
|
||||
public override bool IsDegenerate => Bends.Count == 0 && (End - Start).Length < 4;
|
||||
|
||||
private double HeadLength => Math.Max(12, Style.Thickness * 4);
|
||||
|
||||
private readonly record struct Segment(Point A, Point C1, Point C2, Point B)
|
||||
{
|
||||
public Point At(double t)
|
||||
{
|
||||
double u = 1 - t;
|
||||
double a = u * u * u, b = 3 * u * u * t, c = 3 * u * t * t, d = t * t * t;
|
||||
return new Point(
|
||||
a * A.X + b * C1.X + c * C2.X + d * B.X,
|
||||
a * A.Y + b * C1.Y + c * C2.Y + d * B.Y);
|
||||
}
|
||||
}
|
||||
|
||||
private List<Point> ThroughPoints()
|
||||
{
|
||||
var points = new List<Point>(Bends.Count + 2) { Start };
|
||||
points.AddRange(Bends);
|
||||
points.Add(End);
|
||||
return points;
|
||||
}
|
||||
|
||||
/// <summary>Catmull-Rom door alle punten, omgezet naar kubische Bezier-segmenten.</summary>
|
||||
private List<Segment> Segments()
|
||||
{
|
||||
List<Point> p = ThroughPoints();
|
||||
int last = p.Count - 1;
|
||||
var segments = new List<Segment>(last);
|
||||
for (int i = 0; i < last; i++)
|
||||
{
|
||||
Point prev = p[Math.Max(i - 1, 0)];
|
||||
Point next = p[Math.Min(i + 2, last)];
|
||||
segments.Add(new Segment(p[i], p[i] + (p[i + 1] - prev) / 6, p[i + 1] - (next - p[i]) / 6, p[i + 1]));
|
||||
}
|
||||
|
||||
return segments;
|
||||
}
|
||||
|
||||
private List<Point> Flatten()
|
||||
{
|
||||
if (Bends.Count == 0)
|
||||
{
|
||||
return new List<Point> { Start, End };
|
||||
}
|
||||
|
||||
var points = new List<Point> { Start };
|
||||
foreach (Segment s in Segments())
|
||||
{
|
||||
for (int k = 1; k <= SamplesPerSegment; k++)
|
||||
{
|
||||
points.Add(s.At(k / (double)SamplesPerSegment));
|
||||
}
|
||||
}
|
||||
|
||||
return points;
|
||||
}
|
||||
|
||||
private static double PathLength(List<Point> points)
|
||||
{
|
||||
double length = 0;
|
||||
for (int i = 1; i < points.Count; i++)
|
||||
{
|
||||
length += (points[i] - points[i - 1]).Length;
|
||||
}
|
||||
|
||||
return length;
|
||||
}
|
||||
|
||||
private Geometry BuildGeometry(List<Segment> segments)
|
||||
{
|
||||
var geometry = new StreamGeometry();
|
||||
using (StreamGeometryContext ctx = geometry.Open())
|
||||
{
|
||||
ctx.BeginFigure(segments[0].A, false, false);
|
||||
foreach (Segment s in segments)
|
||||
{
|
||||
if (Bends.Count == 0)
|
||||
{
|
||||
ctx.LineTo(s.B, true, true);
|
||||
}
|
||||
else
|
||||
{
|
||||
ctx.BezierTo(s.C1, s.C2, s.B, true, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
geometry.Freeze();
|
||||
return geometry;
|
||||
}
|
||||
|
||||
public override void Render(DrawingContext dc, EditorDocument document)
|
||||
{
|
||||
Vector v = End - Start;
|
||||
double length = v.Length;
|
||||
double length = PathLength(Flatten());
|
||||
if (length < 1)
|
||||
{
|
||||
return;
|
||||
@@ -50,18 +155,26 @@ public sealed class LineAnnotation : Annotation
|
||||
return;
|
||||
}
|
||||
|
||||
var geometry = new GeometryGroup { FillRule = FillRule.Nonzero };
|
||||
Brush? headFill = null;
|
||||
List<Segment> segments = Segments();
|
||||
|
||||
if (IsArrow)
|
||||
{
|
||||
Segment lastSegment = segments[^1];
|
||||
Vector v = lastSegment.B - lastSegment.C2;
|
||||
if (v.Length < 1e-6)
|
||||
{
|
||||
v = lastSegment.B - lastSegment.A;
|
||||
}
|
||||
|
||||
v.Normalize();
|
||||
double headLength = Math.Min(HeadLength, length);
|
||||
double halfWidth = headLength * 0.45;
|
||||
Point basePoint = End - v * headLength;
|
||||
var normal = new Vector(-v.Y, v.X);
|
||||
|
||||
geometry.Children.Add(new LineGeometry(Start, basePoint + v * Math.Min(2, headLength / 4)));
|
||||
Point lineEnd = basePoint + v * Math.Min(2, headLength / 4);
|
||||
Vector shift = lineEnd - End;
|
||||
segments[^1] = lastSegment with { C2 = lastSegment.C2 + shift, B = lineEnd };
|
||||
|
||||
var head = new StreamGeometry();
|
||||
using (StreamGeometryContext ctx = head.Open())
|
||||
@@ -72,39 +185,105 @@ public sealed class LineAnnotation : Annotation
|
||||
}
|
||||
|
||||
head.Freeze();
|
||||
headFill = RenderHelpers.BrushOrNull(Style.Stroke);
|
||||
Brush? headFill = RenderHelpers.BrushOrNull(Style.Stroke);
|
||||
|
||||
// Lijn en punt apart tekenen zodat de punt gevuld en niet gestippeld wordt.
|
||||
RenderHelpers.DrawWithShadow(dc, geometry, null, pen, Style.Shadow);
|
||||
RenderHelpers.DrawWithShadow(dc, BuildGeometry(segments), null, pen, Style.Shadow);
|
||||
var headPen = RenderHelpers.CreatePen(Style.Stroke, Math.Max(1, Style.Thickness / 3), LineDash.Solid);
|
||||
RenderHelpers.DrawWithShadow(dc, head, headFill, headPen, Style.Shadow);
|
||||
return;
|
||||
}
|
||||
|
||||
geometry.Children.Add(new LineGeometry(Start, End));
|
||||
RenderHelpers.DrawWithShadow(dc, geometry, null, pen, Style.Shadow);
|
||||
RenderHelpers.DrawWithShadow(dc, BuildGeometry(segments), null, pen, Style.Shadow);
|
||||
}
|
||||
|
||||
public override bool HitTest(Point point, double tolerance) =>
|
||||
RenderHelpers.DistanceToSegment(point, Start, End) <= tolerance + Style.Thickness / 2 + (IsArrow ? 4 : 0);
|
||||
public override bool HitTest(Point point, double tolerance)
|
||||
{
|
||||
double limit = tolerance + Style.Thickness / 2 + (IsArrow ? 4 : 0);
|
||||
List<Point> points = Flatten();
|
||||
for (int i = 1; i < points.Count; i++)
|
||||
{
|
||||
if (RenderHelpers.DistanceToSegment(point, points[i - 1], points[i]) <= limit)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public override void Move(Vector delta)
|
||||
{
|
||||
Start += delta;
|
||||
End += delta;
|
||||
for (int i = 0; i < Bends.Count; i++)
|
||||
{
|
||||
Bends[i] += delta;
|
||||
}
|
||||
}
|
||||
|
||||
public override IReadOnlyList<Point> GetHandles() => new[] { Start, End };
|
||||
/// <summary>Volgorde: begin, eind, buigpunten, en daarna één spook-greep midden op elk segment.</summary>
|
||||
public override IReadOnlyList<Point> GetHandles()
|
||||
{
|
||||
var handles = new List<Point> { Start, End };
|
||||
handles.AddRange(Bends);
|
||||
foreach (Segment s in Segments())
|
||||
{
|
||||
handles.Add(s.At(0.5));
|
||||
}
|
||||
|
||||
return handles;
|
||||
}
|
||||
|
||||
public override bool IsGhostHandle(int index) => index >= 2 + Bends.Count;
|
||||
|
||||
public override void BeginHandleDrag(int index)
|
||||
{
|
||||
_pendingGhost = IsGhostHandle(index) ? index - 2 - Bends.Count : -1;
|
||||
_dragBend = index >= 2 && _pendingGhost < 0 ? index - 2 : -1;
|
||||
}
|
||||
|
||||
public override void MoveHandle(int index, Point point, bool constrain)
|
||||
{
|
||||
if (index == 0)
|
||||
// Het buigpunt pas bij de eerste beweging invoegen, zodat een losse klik de lijn niet verandert.
|
||||
if (_pendingGhost >= 0)
|
||||
{
|
||||
Start = constrain ? RenderHelpers.SnapAngle(End, point) : point;
|
||||
Bends.Insert(_pendingGhost, point);
|
||||
_dragBend = _pendingGhost;
|
||||
_pendingGhost = -1;
|
||||
return;
|
||||
}
|
||||
else
|
||||
|
||||
if (_dragBend >= 0)
|
||||
{
|
||||
End = constrain ? RenderHelpers.SnapAngle(Start, point) : point;
|
||||
Bends[_dragBend] = point;
|
||||
}
|
||||
else if (index == 0)
|
||||
{
|
||||
Start = constrain ? RenderHelpers.SnapAngle(Bends.Count > 0 ? Bends[0] : End, point) : point;
|
||||
}
|
||||
else if (index == 1)
|
||||
{
|
||||
End = constrain ? RenderHelpers.SnapAngle(Bends.Count > 0 ? Bends[^1] : Start, point) : point;
|
||||
}
|
||||
}
|
||||
|
||||
public override bool RemoveHandle(int index)
|
||||
{
|
||||
int bend = index - 2;
|
||||
if (bend < 0 || bend >= Bends.Count)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Bends.RemoveAt(bend);
|
||||
return true;
|
||||
}
|
||||
|
||||
public override Annotation Clone()
|
||||
{
|
||||
var clone = (LineAnnotation)base.Clone();
|
||||
clone.Bends = new List<Point>(Bends);
|
||||
return clone;
|
||||
}
|
||||
}
|
||||
@@ -452,6 +452,14 @@ public partial class EditorWindow : Window
|
||||
if (_selected != null)
|
||||
{
|
||||
int handle = Surface.HitHandle(_selected, p);
|
||||
if (handle >= 0 && e.ClickCount == 2 && _selected.RemoveHandle(handle))
|
||||
{
|
||||
Commit(_pending);
|
||||
_pending = null;
|
||||
Surface.InvalidateVisual();
|
||||
return;
|
||||
}
|
||||
|
||||
if (handle >= 0)
|
||||
{
|
||||
_handleIndex = handle;
|
||||
@@ -1877,8 +1885,8 @@ public partial class EditorWindow : Window
|
||||
ToolKind.Select => "Klik om te selecteren, sleep om te verplaatsen. Dubbelklik op tekst om te bewerken. Pijltoetsen verschuiven.",
|
||||
ToolKind.Rectangle => "Sleep om een rechthoek te tekenen. Shift = vierkant.",
|
||||
ToolKind.Ellipse => "Sleep om een ellips te tekenen. Shift = cirkel.",
|
||||
ToolKind.Line => "Sleep om een lijn te tekenen. Shift = hoeken van 45°.",
|
||||
ToolKind.Arrow => "Sleep om een pijl te tekenen. Shift = hoeken van 45°.",
|
||||
ToolKind.Line => "Sleep om een lijn te tekenen. Shift = hoeken van 45°. Sleep een middenpunt om te buigen; dubbelklik op een buigpunt om het te verwijderen.",
|
||||
ToolKind.Arrow => "Sleep om een pijl te tekenen. Shift = hoeken van 45°. Sleep een middenpunt om te buigen; dubbelklik op een buigpunt om het te verwijderen.",
|
||||
ToolKind.Text => "Klik om een tekstkader te plaatsen, of klik op bestaande tekst om die te bewerken.",
|
||||
ToolKind.Step => "Klik om een genummerde stap te plaatsen. De nummering loopt automatisch door.",
|
||||
ToolKind.Cursor => "Klik om een cursor te plaatsen; de punt komt precies op de klikplek.",
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
namespace Kniip.Infrastructure;
|
||||
|
||||
/// <summary>
|
||||
/// Namen waaronder kniip zich in Windows inschrijft. De testbuild (<c>-p:KniipTest=true</c>) gebruikt eigen
|
||||
/// namen voor register, instellingen, mutex en tijdelijke bestanden, zodat hij naast de geïnstalleerde
|
||||
/// kniip kan draaien en die nooit overschrijft.
|
||||
/// </summary>
|
||||
internal static class AppIdentity
|
||||
{
|
||||
#if KNIIP_TEST
|
||||
public static bool IsTest => true;
|
||||
public const string Key = "KniipTest";
|
||||
public const string DisplayName = "kniip test";
|
||||
#else
|
||||
public static bool IsTest => false;
|
||||
public const string Key = "Kniip";
|
||||
public const string DisplayName = "kniip";
|
||||
#endif
|
||||
}
|
||||
@@ -9,10 +9,10 @@ namespace Kniip.Infrastructure;
|
||||
/// </summary>
|
||||
internal static class ShellIntegration
|
||||
{
|
||||
private const string ProgId = "Kniip.Image";
|
||||
private const string AppKeyName = "Kniip.exe";
|
||||
private const string ProgId = AppIdentity.Key + ".Image";
|
||||
private const string AppKeyName = AppIdentity.Key + ".exe";
|
||||
private const string RunKeyPath = @"Software\Microsoft\Windows\CurrentVersion\Run";
|
||||
private const string RunValueName = "Kniip";
|
||||
private const string RunValueName = AppIdentity.Key;
|
||||
|
||||
public static readonly string[] ImageExtensions = { ".png", ".jpg", ".jpeg", ".bmp", ".gif" };
|
||||
|
||||
@@ -32,8 +32,8 @@ internal static class ShellIntegration
|
||||
{
|
||||
using (var prog = Registry.CurrentUser.CreateSubKey($@"Software\Classes\{ProgId}"))
|
||||
{
|
||||
prog.SetValue(null, "kniip afbeelding");
|
||||
prog.SetValue("FriendlyTypeName", "kniip afbeelding");
|
||||
prog.SetValue(null, $"{AppIdentity.DisplayName} afbeelding");
|
||||
prog.SetValue("FriendlyTypeName", $"{AppIdentity.DisplayName} afbeelding");
|
||||
}
|
||||
using (var icon = Registry.CurrentUser.CreateSubKey($@"Software\Classes\{ProgId}\DefaultIcon"))
|
||||
{
|
||||
@@ -46,7 +46,7 @@ internal static class ShellIntegration
|
||||
|
||||
using (var app = Registry.CurrentUser.CreateSubKey($@"Software\Classes\Applications\{AppKeyName}"))
|
||||
{
|
||||
app.SetValue("FriendlyAppName", "kniip editor");
|
||||
app.SetValue("FriendlyAppName", $"{AppIdentity.DisplayName} editor");
|
||||
}
|
||||
using (var appCmd = Registry.CurrentUser.CreateSubKey($@"Software\Classes\Applications\{AppKeyName}\shell\open\command"))
|
||||
{
|
||||
@@ -89,6 +89,11 @@ internal static class ShellIntegration
|
||||
/// </summary>
|
||||
public static void MigrateLegacyRegistration()
|
||||
{
|
||||
if (AppIdentity.IsTest)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const string legacyProgId = "KnipPlus.Image";
|
||||
const string legacyAppKeyName = "KnipPlus.exe";
|
||||
const string legacyRunValueName = "KnipPlus";
|
||||
|
||||
@@ -13,7 +13,7 @@ namespace Kniip.Infrastructure;
|
||||
/// </summary>
|
||||
internal sealed class SingleInstance : IDisposable
|
||||
{
|
||||
private static readonly string BaseName = "Kniip-" + Environment.UserName;
|
||||
private static readonly string BaseName = AppIdentity.Key + "-" + Environment.UserName;
|
||||
private static readonly string PipeName = BaseName + "-pipe";
|
||||
|
||||
private readonly CancellationTokenSource _cts = new();
|
||||
|
||||
@@ -56,7 +56,7 @@ internal sealed class TrayIcon : IDisposable
|
||||
_icon = new NotifyIcon
|
||||
{
|
||||
Icon = LoadIcon(),
|
||||
Text = "kniip - annotaties voor het Knipprogramma",
|
||||
Text = IdleText,
|
||||
ContextMenuStrip = menu,
|
||||
Visible = true
|
||||
};
|
||||
@@ -86,9 +86,13 @@ internal sealed class TrayIcon : IDisposable
|
||||
{
|
||||
_recordItem.Text = text ?? RecordText;
|
||||
_gifItem.Visible = text == null;
|
||||
_icon.Text = recording ? "kniip - opname loopt" : "kniip - annotaties voor het Knipprogramma";
|
||||
_icon.Text = recording ? $"{AppIdentity.DisplayName} - opname loopt" : IdleText;
|
||||
}
|
||||
|
||||
private static string IdleText => AppIdentity.IsTest
|
||||
? $"{AppIdentity.DisplayName} {Kniip.Update.UpdateChecker.CurrentVersion}"
|
||||
: "kniip - annotaties voor het Knipprogramma";
|
||||
|
||||
public void ShowBalloon(string title, string text) =>
|
||||
_icon.ShowBalloonTip(4000, title, text, ToolTipIcon.Info);
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||
<Product>kniip</Product>
|
||||
<Description>Annotatie-editor naast het Windows Knipprogramma</Description>
|
||||
<Version>1.1.3</Version>
|
||||
<Version>1.1.4</Version>
|
||||
<SatelliteResourceLanguages>nl;en</SatelliteResourceLanguages>
|
||||
<!-- WPF regelt DPI via app.manifest; de WinForms-analyzer is hier niet van toepassing. -->
|
||||
<NoWarn>$(NoWarn);WFAC010</NoWarn>
|
||||
@@ -26,6 +26,12 @@
|
||||
<AllowedReferenceRelatedFileExtensions>.none</AllowedReferenceRelatedFileExtensions>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Testbuild (build.ps1, -p:KniipTest=true): eigen register- en instellingennamen, zie AppIdentity. -->
|
||||
<PropertyGroup Condition="'$(KniipTest)' == 'true'">
|
||||
<DefineConstants>$(DefineConstants);KNIIP_TEST</DefineConstants>
|
||||
<Product>kniip test</Product>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Resource Include="AppIcon.ico" />
|
||||
<Resource Include="Assets\Emoji\emoji.json;Assets\Emoji\twemoji-64.png" />
|
||||
|
||||
@@ -15,7 +15,7 @@ internal sealed record RecordingOptions(bool SystemAudio, bool Microphone, bool
|
||||
/// <summary>Eén opname naar een tijdelijk MP4-bestand. Gebeurtenissen komen binnen op de UI-thread.</summary>
|
||||
internal sealed class RecordingSession : IDisposable
|
||||
{
|
||||
public static string TempFolder { get; } = Path.Combine(Path.GetTempPath(), "Kniip");
|
||||
public static string TempFolder { get; } = Path.Combine(Path.GetTempPath(), Infrastructure.AppIdentity.Key);
|
||||
|
||||
private readonly Dispatcher _dispatcher;
|
||||
private readonly RecordingRegion _region;
|
||||
|
||||
@@ -7,19 +7,24 @@ using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Windows.Media;
|
||||
using Kniip.Infrastructure;
|
||||
|
||||
namespace Kniip.Settings;
|
||||
|
||||
public static class SettingsStore
|
||||
{
|
||||
public static string Folder { get; } =
|
||||
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Kniip");
|
||||
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), AppIdentity.Key);
|
||||
|
||||
private static string FilePath => Path.Combine(Folder, "settings.json");
|
||||
|
||||
/// <summary>Instellingen van voor de hernoeming van Knip+ naar kniip.</summary>
|
||||
/// <summary>
|
||||
/// Startpunt als er nog geen instellingen zijn: voor kniip die van voor de hernoeming van Knip+,
|
||||
/// voor de testbuild een kopie van die van de gewone kniip.
|
||||
/// </summary>
|
||||
private static string LegacyFilePath =>
|
||||
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "KnipPlus", "settings.json");
|
||||
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
||||
AppIdentity.IsTest ? "Kniip" : "KnipPlus", "settings.json");
|
||||
|
||||
private static readonly JsonSerializerOptions Options = new()
|
||||
{
|
||||
|
||||
@@ -45,6 +45,7 @@ public partial class SettingsWindow : Window
|
||||
{
|
||||
InitializeComponent();
|
||||
_settings = settings;
|
||||
Title = $"{AppIdentity.DisplayName} instellingen";
|
||||
|
||||
FolderText.Text = CaptureCoordinator.ScreenshotsFolder ?? "Map Schermafbeeldingen niet gevonden.";
|
||||
AutostartBox.IsChecked = ShellIntegration.IsAutostartEnabled();
|
||||
@@ -557,6 +558,14 @@ public partial class SettingsWindow : Window
|
||||
return;
|
||||
}
|
||||
|
||||
if (AppIdentity.IsTest)
|
||||
{
|
||||
UpdateStatus.Text = $"Versie {newer[0].Version} is beschikbaar. Deze testversie werkt zichzelf niet bij; " +
|
||||
"de installer zou de gewone kniip overschrijven.";
|
||||
ShowChangelog(newer, null);
|
||||
return;
|
||||
}
|
||||
|
||||
_update = newer.FirstOrDefault(r => r.InstallerUrl != null);
|
||||
if (_update != null)
|
||||
{
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$knownNames = @("Kniip", "KnipPlus")
|
||||
$exePattern = '(?i)(^|[\\/"])(Kniip|KnipPlus)\.exe(\b|")'
|
||||
$knownNames = @("Kniip", "KniipTest", "KnipPlus")
|
||||
$exePattern = '(?i)(^|[\\/"])(Kniip(Test)?(-[\d.]+)?|KnipPlus)\.exe(\b|")'
|
||||
|
||||
function Test-KniipCommand([string]$text) {
|
||||
return [bool]($text -match $exePattern)
|
||||
|
||||
Reference in new issue
Block a user