6 Commits
Author SHA1 Message Date
rogierandCursor b780c6a116 1.1.4
Update-melding, rechtermuisknopmenu in de editor.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-11-09 17:00:58 +01:00
rogierandCursor 67c98c7c0a 1.1.4
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-11-09 16:36:28 +01:00
rogier 5bdc851fa0 1.1.3 2026-11-09 13:02:24 +01:00
rogierandCursor 9b9ac36261 1.1.2
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-11-09 00:38:41 +01:00
rogier ceec9ecdc0 Toon alleen de laatste versie. 2026-11-09 00:26:57 +01:00
rogierandCursor eb1fc40534 1.1.1
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-11-09 00:18:46 +01:00
28 changed files with 1114 additions and 122 deletions

No files matched your search

+47
View File
@@ -2,6 +2,53 @@
Alle noemenswaardige wijzigingen in kniip staan in dit bestand. De nieuwste versie staat bovenaan. Alle noemenswaardige wijzigingen in kniip staan in dit bestand. De nieuwste versie staat bovenaan.
## 1.1.4 - 2026-11-09
### Nieuw
- **Melding bij een nieuwe versie.** kniip kijkt bij het starten en daarna elke dag of er een nieuwere versie is. Is die er, dan staat er een blauwe stip op de knop **Instellingen** in de editor en op de tab **Update** in de instellingen. Wil je dat niet, zet dan in de tab **Update** het vinkje **Automatisch controleren op nieuwe versies** uit.
- **Uitgebreid rechtermuisknopmenu in de editor.** Klik met rechts op het canvas, ook op een leeg stuk, voor een menu met iconen en sneltoetsen. Wat op dat moment niet kan, staat grijs.
- **Ongedaan maken** en **Opnieuw**.
- **Verwijder**, **Verwijder alles** (Shift+Delete, de afbeelding blijft staan) en **Samenvoegen** (Ctrl+Shift+F). Samenvoegen maakt de annotaties deel van de afbeelding en kun je ongedaan maken.
- **Knip** (Ctrl+X), **Kopieer** (Ctrl+Shift+C), **Plak** (Ctrl+V) en **Dupliceren** (Ctrl+D). Plak via het menu zet de annotatie op de plek waar je rechts klikte. Kopiëren en plakken werkt ook tussen twee editorvensters.
- **Naar voorgrond** (Home), **Naar voren** (PageUp), **Naar achteren** (PageDown) en **Naar achtergrond** (End).
- **Werkbalken verbergen** (Ctrl+H). Via hetzelfde menu of Ctrl+H haal je ze terug. Bij een nieuwe start zijn ze er altijd weer.
- **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
- kniip zoekt nu standaard zelf op de achtergrond naar updates. Tot nu toe gebeurde dat alleen als je de tab **Update** opende of op **Opnieuw controleren** klikte. Een mislukte controle, bijvoorbeeld zonder internet, geeft geen foutmelding.
- **Ctrl+V** plakt nu eerst een gekopieerde annotatie. Heb je daarna iets anders gekopieerd, dan opent Ctrl+V zoals voorheen de afbeelding van het klembord.
- Het rechtermuisknopmenu van de werkbalk heeft dezelfde nieuwe stijl gekregen.
- **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
- **Sneltoets voor het klembord.** In de tab **Sneltoetsen** kun je een combinatie zetten op **Vanaf klembord**. Die opent de afbeelding op het klembord in de editor. Standaard is er geen sneltoets. De gekozen combinatie staat ook bij **Bewerken vanaf klembord** in het systeemvakmenu.
- **Afbeelding naar de editor slepen.** Sleep een afbeeldingsbestand, of een afbeelding uit een ander programma, naar het kniip-venster. kniip vraagt daarna of je die afbeelding wilt openen, zodat een lopende bewerking niet zomaar verdwijnt.
## 1.1.2 - 2026-11-09
### Opgelost
- **Link kopiëren na uploaden.** Na het uploaden kon kniip de link soms niet op het klembord zetten en meldde dan dat het klembord bezet was. Dat gebeurde vooral als de klembordgeschiedenis van Windows (Win+V) of een ander klembordprogramma actief is. kniip zet de link nu op een directere manier op het klembord en probeert het langer opnieuw. Dit geldt ook voor het uploaden van screencasts en de test-upload in de instellingen.
## 1.1.1 - 2026-11-09
### Gewijzigd
- **Instellingen zonder scrollbalk.** Het instellingenvenster is iets hoger geworden, zodat elke tab, ook **Opslaan** met de nieuwe naamvelden, in één keer past. De changelog in de tab **Update** heeft daardoor ook meer ruimte.
### Opgelost
- De downloadlinks in de handleiding (README) wezen naar een verkeerde tag en gaven een foutmelding.
## 1.1.0 - 2026-11-09 ## 1.1.0 - 2026-11-09
### Nieuw ### Nieuw
+21 -5
View File
@@ -24,9 +24,9 @@ kniip is een annotatie-editor die naast het Windows Knipprogramma werkt. Maak ee
| Versie | Installer | | Versie | Installer |
| ------ | --------- | | ------ | --------- |
| Nieuwste versie | [Alle releases](https://git.hibbit.net/rogier/kniip/releases) | | 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) |
| 1.1.0 | [Kniip-Setup-1.1.0.exe](https://git.hibbit.net/rogier/kniip/releases/download/v1.1.0/Kniip-Setup-1.1.0.exe) |
| 1.0.0 | [Kniip-Setup-1.0.0.exe](https://git.hibbit.net/rogier/kniip/releases/download/v1.0.0/Kniip-Setup-1.0.0.exe) | Oudere versies vind je bij [alle releases](https://git.hibbit.net/rogier/kniip/releases).
## Installeren ## Installeren
@@ -70,7 +70,7 @@ Vereisten:
Bouwen: Bouwen:
```powershell ```powershell
.\build.ps1 # versie 1.1.0 .\build.ps1 # versie 1.1.4
.\build.ps1 -Version 1.2.0 # andere versie .\build.ps1 -Version 1.2.0 # andere versie
``` ```
@@ -79,18 +79,34 @@ Het script levert op:
- `artifacts\publish\Kniip.exe`: zelfstandige exe, draait zonder installatie - `artifacts\publish\Kniip.exe`: zelfstandige exe, draait zonder installatie
- `artifacts\Kniip-<versie>.exe`: dezelfde exe, met het versienummer in de naam - `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\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 ## Een nieuwe versie publiceren
1. Bouw met `.\build.ps1 -Version <versie>`. 1. Bouw met `.\build.ps1 -Version <versie>`.
2. Maak op [git.hibbit.net](https://git.hibbit.net/rogier/kniip/releases) een release met tag `v<versie>` en voeg `artifacts\Kniip-Setup-<versie>.exe` toe als bijlage. 2. Maak op [git.hibbit.net](https://git.hibbit.net/rogier/kniip/releases) een release met tag `v<versie>` en voeg `artifacts\Kniip-Setup-<versie>.exe` toe als bijlage.
3. Schrijf de wijzigingen in markdown in de beschrijving van de release. kniip toont die als changelog onder **Instellingen > Update**. 3. Schrijf de wijzigingen in markdown in de beschrijving van de release. kniip toont die als changelog onder **Instellingen > Update**.
4. Voeg de nieuwe versie toe aan de tabel onder [Downloaden](#downloaden). 4. Vervang de versie en de link in de tabel onder [Downloaden](#downloaden) door de nieuwe versie.
### Updaten vanuit kniip ### Updaten vanuit kniip
Onder **Instellingen > Update** controleert kniip via de API van git.hibbit.net of er een nieuwere release is en toont de changelog van alle nieuwere versies. **Updaten** downloadt de installer, sluit kniip, installeert stil en start kniip opnieuw. De eerder gekozen integraties blijven behouden. Onder **Instellingen > Update** controleert kniip via de API van git.hibbit.net of er een nieuwere release is en toont de changelog van alle nieuwere versies. **Updaten** downloadt de installer, sluit kniip, installeert stil en start kniip opnieuw. De eerder gekozen integraties blijven behouden.
kniip controleert ook zelf bij het starten en daarna elke 24 uur. Is er een nieuwere versie, dan staat er een blauwe stip op de knop **Instellingen** in de editor en op de tab **Update**. Dit zet je uit met **Automatisch controleren op nieuwe versies** in die tab.
Voorwaarden: Voorwaarden:
- De repository `rogier/kniip` is publiek; kniip gebruikt geen token. - De repository `rogier/kniip` is publiek; kniip gebruikt geen token.
+35 -15
View File
@@ -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. # 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; # 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. # 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 -Version 1.2.0
# .\build.ps1 -SkipTest -> zonder testexe
param( param(
[string]$Version = "1.1.0", [string]$Version = "1.1.4",
[string]$Runtime = "win-x64" [string]$Runtime = "win-x64",
[switch]$SkipTest
) )
$ErrorActionPreference = "Stop" $ErrorActionPreference = "Stop"
$root = $PSScriptRoot $root = $PSScriptRoot
$project = Join-Path $root "src\Kniip\Kniip.csproj"
$publishDir = Join-Path $root "artifacts\publish" $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. # Gebruik een SDK in het gebruikersprofiel als er geen systeembrede SDK is.
$localDotnet = Join-Path $env:LOCALAPPDATA "Microsoft\dotnet" $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" $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") ` dotnet publish $project `
-c Release -r $Runtime --self-contained true ` -c Release -r $Runtime --self-contained true `
-p:PublishSingleFile=true ` -p:PublishSingleFile=true `
-p:IncludeNativeLibrariesForSelfExtract=true ` -p:IncludeNativeLibrariesForSelfExtract=true `
-p:IncludeAllContentForSelfExtract=true ` -p:IncludeAllContentForSelfExtract=true `
-p:EnableCompressionInSingleFile=true ` -p:EnableCompressionInSingleFile=true `
-p:DebugType=none ` -p:DebugType=none `
-p:Version=$Version ` -p:Version=$Version `
-o $publishDir -p:KniipTest=$($test.ToString().ToLowerInvariant()) `
if ($LASTEXITCODE -ne 0) { throw "dotnet publish is mislukt." } -o $outputDir
if ($LASTEXITCODE -ne 0) { throw "dotnet publish is mislukt." }
}
Publish-Kniip $publishDir $false
Write-Host "Gepubliceerd: $publishDir\Kniip.exe" -ForegroundColor Green Write-Host "Gepubliceerd: $publishDir\Kniip.exe" -ForegroundColor Green
@@ -55,3 +68,10 @@ if ($iscc) {
} else { } else {
Write-Host "Inno Setup 6 niet gevonden; installer overgeslagen (winget install JRSoftware.InnoSetup)." -ForegroundColor Yellow 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
View File
@@ -5,7 +5,7 @@
#define AppName "kniip" #define AppName "kniip"
#define AppExe "Kniip.exe" #define AppExe "Kniip.exe"
#ifndef AppVersion #ifndef AppVersion
#define AppVersion "1.1.0" #define AppVersion "1.1.4"
#endif #endif
#ifndef PublishDir #ifndef PublishDir
#define PublishDir "..\artifacts\publish" #define PublishDir "..\artifacts\publish"
+86
View File
@@ -293,6 +293,92 @@
</Setter> </Setter>
</Style> </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="&#xE73E;" 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="&#xE76C;" 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"> <Style x:Key="PropCombo" TargetType="ComboBox">
<Setter Property="MinWidth" Value="80" /> <Setter Property="MinWidth" Value="80" />
<Setter Property="Height" Value="28" /> <Setter Property="Height" Value="28" />
+8 -2
View File
@@ -38,6 +38,9 @@ public partial class App : Application
/// <summary>Toetsenbordhaak voor alle sneltoetsen (knipsels en screencasts).</summary> /// <summary>Toetsenbordhaak voor alle sneltoetsen (knipsels en screencasts).</summary>
internal GlobalHotkeyHook? HotkeyHook { get; private set; } internal GlobalHotkeyHook? HotkeyHook { get; private set; }
/// <summary>Achtergrondcontrole op nieuwe versies; stuurt de blauwe stip bij Instellingen.</summary>
public UpdateNotifier Updates { get; } = new();
protected override void OnStartup(StartupEventArgs e) protected override void OnStartup(StartupEventArgs e)
{ {
base.OnStartup(e); base.OnStartup(e);
@@ -105,6 +108,7 @@ public partial class App : Application
HotkeyHook = new GlobalHotkeyHook(Dispatcher); HotkeyHook = new GlobalHotkeyHook(Dispatcher);
ApplyHotkeys(); ApplyHotkeys();
Updates.Apply(Settings.CheckForUpdates);
_singleInstance.StartServer(received => Dispatcher.BeginInvoke(() => HandleArguments(received, false))); _singleInstance.StartServer(received => Dispatcher.BeginInvoke(() => HandleArguments(received, false)));
HandleArguments(args, true); HandleArguments(args, true);
@@ -150,7 +154,7 @@ public partial class App : Application
if (isFirstStart) if (isFirstStart)
{ {
string? hotkey = HotkeyDisplay(Settings.SnipHotkey); 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." ? "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."); : $"Druk op {hotkey} en sleep een rechthoek om te knippen en te bewerken.");
} }
@@ -298,11 +302,12 @@ public partial class App : Application
Bind(Settings.WindowHotkey, BeginWindowSnip); Bind(Settings.WindowHotkey, BeginWindowSnip);
Bind(Settings.RecordHotkey, () => Recorder.Toggle(ScreencastFormat.Mp4)); Bind(Settings.RecordHotkey, () => Recorder.Toggle(ScreencastFormat.Mp4));
Bind(Settings.GifHotkey, () => Recorder.Toggle(ScreencastFormat.Gif)); Bind(Settings.GifHotkey, () => Recorder.Toggle(ScreencastFormat.Gif));
Bind(Settings.ClipboardHotkey, OpenFromClipboard);
HotkeyHook.Bindings = bindings; HotkeyHook.Bindings = bindings;
} }
_tray?.SetHotkeys(HotkeyDisplay(Settings.SnipHotkey), HotkeyDisplay(Settings.WindowHotkey), _tray?.SetHotkeys(HotkeyDisplay(Settings.SnipHotkey), HotkeyDisplay(Settings.WindowHotkey),
HotkeyDisplay(Settings.RecordHotkey), HotkeyDisplay(Settings.GifHotkey)); HotkeyDisplay(Settings.RecordHotkey), HotkeyDisplay(Settings.GifHotkey), HotkeyDisplay(Settings.ClipboardHotkey));
} }
/// <summary>Start een screencast: eerst het gebied kiezen, daarna de knoppenbalk.</summary> /// <summary>Start een screencast: eerst het gebied kiezen, daarna de knoppenbalk.</summary>
@@ -439,6 +444,7 @@ public partial class App : Application
SettingsStore.Save(Settings); SettingsStore.Save(Settings);
Capture.ApplySettings(Settings); Capture.ApplySettings(Settings);
ApplyHotkeys(); ApplyHotkeys();
Updates.Apply(Settings.CheckForUpdates);
} }
private static void BringToFront(Window window) private static void BringToFront(Window window)
+63 -24
View File
@@ -41,33 +41,14 @@ internal static class ClipboardImageReader
return null; return null;
} }
if (includeFiles && TryReadFile(data) is { } fromFile) if (ReadData(data, includeFiles) is { } image)
{ {
return fromFile; return image;
} }
if (data.GetDataPresent("PNG") && data.GetData("PNG") is MemoryStream png) if (Clipboard.ContainsImage() && Clipboard.GetImage() is { } bitmap)
{ {
return PixelUtil.Decode(png); return OpaqueCopy(bitmap);
}
if (data.GetDataPresent(DataFormats.Dib) && data.GetData(DataFormats.Dib) is MemoryStream dib)
{
BitmapSource? fromDib = FromDib(dib.ToArray());
if (fromDib != null)
{
return fromDib;
}
}
if (Clipboard.ContainsImage())
{
BitmapSource? image = Clipboard.GetImage();
if (image != null)
{
var copy = PixelUtil.CreateBitmap(PixelUtil.ToOpaqueBgra(image), image.PixelWidth, image.PixelHeight);
return copy;
}
} }
} }
catch (Exception ex) when (ex is COMException or ExternalException or NotSupportedException or FileFormatException) catch (Exception ex) when (ex is COMException or ExternalException or NotSupportedException or FileFormatException)
@@ -78,6 +59,64 @@ internal static class ClipboardImageReader
return null; return null;
} }
/// <summary>Snelle controle tijdens het slepen, zonder te decoderen.</summary>
public static bool CanDrop(IDataObject data)
{
try
{
return data.GetDataPresent("PNG") || data.GetDataPresent(DataFormats.Dib) || data.GetDataPresent(DataFormats.Bitmap)
|| (data.GetData(DataFormats.FileDrop) is string[] files && Array.Exists(files, IsImageFile));
}
catch (Exception ex) when (ex is COMException or ExternalException)
{
return false;
}
}
/// <summary>Afbeelding uit iets dat in kniip is neergezet: een afbeeldingsbestand of afbeeldingsdata.</summary>
public static BitmapSource? FromDrop(IDataObject data)
{
try
{
if (ReadData(data, includeFiles: true) is { } image)
{
return image;
}
return data.GetData(DataFormats.Bitmap) is BitmapSource bitmap ? OpaqueCopy(bitmap) : null;
}
catch (Exception ex) when (ex is COMException or ExternalException or NotSupportedException or FileFormatException)
{
return null;
}
}
private static BitmapSource? ReadData(IDataObject data, bool includeFiles)
{
if (includeFiles && TryReadFile(data) is { } fromFile)
{
return fromFile;
}
if (data.GetDataPresent("PNG") && data.GetData("PNG") is MemoryStream png)
{
return PixelUtil.Decode(png);
}
if (data.GetDataPresent(DataFormats.Dib) && data.GetData(DataFormats.Dib) is MemoryStream dib)
{
return FromDib(dib.ToArray());
}
return null;
}
private static BitmapSource OpaqueCopy(BitmapSource image) =>
PixelUtil.CreateBitmap(PixelUtil.ToOpaqueBgra(image), image.PixelWidth, image.PixelHeight);
private static bool IsImageFile(string file) =>
Array.IndexOf(ImageExtensions, Path.GetExtension(file).ToLowerInvariant()) >= 0;
private static BitmapSource? TryReadFile(IDataObject data) private static BitmapSource? TryReadFile(IDataObject data)
{ {
if (!data.GetDataPresent(DataFormats.FileDrop) || data.GetData(DataFormats.FileDrop) is not string[] files) if (!data.GetDataPresent(DataFormats.FileDrop) || data.GetData(DataFormats.FileDrop) is not string[] files)
@@ -87,7 +126,7 @@ internal static class ClipboardImageReader
foreach (string file in files) foreach (string file in files)
{ {
if (!File.Exists(file) || Array.IndexOf(ImageExtensions, Path.GetExtension(file).ToLowerInvariant()) < 0) if (!File.Exists(file) || !IsImageFile(file))
{ {
continue; continue;
} }
+49
View File
@@ -0,0 +1,49 @@
using System;
using System.Runtime.InteropServices;
using System.Windows;
using Kniip.Editor.Annotations;
namespace Kniip.Editor;
/// <summary>
/// Gekopieerde annotatie, gedeeld tussen editorvensters. Op het Windows-klembord staat alleen een markering,
/// zodat iets wat daarna in een ander programma wordt gekopieerd bij plakken voorgaat.
/// </summary>
internal static class AnnotationClipboard
{
private const string MarkerFormat = "Kniip.Annotation";
private static Annotation? _item;
private static string? _token;
public static bool Set(Annotation item)
{
string token = Guid.NewGuid().ToString("N");
if (!ClipboardWriter.SetMarker(MarkerFormat, token))
{
return false;
}
_item = item.Clone();
_token = token;
return true;
}
/// <summary>Een kopie van de annotatie, of null als het klembord inmiddels iets anders bevat.</summary>
public static Annotation? Get()
{
if (_item == null)
{
return null;
}
try
{
return Clipboard.GetData(MarkerFormat) as string == _token ? _item.Clone() : null;
}
catch (ExternalException)
{
return null;
}
}
}
+18 -3
View File
@@ -12,6 +12,7 @@ namespace Kniip.Editor;
public sealed class AnnotationSurface : FrameworkElement public sealed class AnnotationSurface : FrameworkElement
{ {
private static readonly Brush AccentBrush = RenderHelpers.Freeze(new SolidColorBrush(Palette.Accent)); 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))); private static readonly Brush CropShadeBrush = RenderHelpers.Freeze(new SolidColorBrush(Color.FromArgb(120, 0, 0, 0)));
public EditorDocument? Document { get; set; } public EditorDocument? Document { get; set; }
@@ -74,8 +75,16 @@ public sealed class AnnotationSurface : FrameworkElement
double size = HandleSize; double size = HandleSize;
var pen = new Pen(AccentBrush, 1.2 / ViewScale); 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), dc.DrawRoundedRectangle(Brushes.White, pen, new Rect(h.X - size / 2, h.Y - size / 2, size, size),
1.5 / ViewScale, 1.5 / ViewScale); 1.5 / ViewScale, 1.5 / ViewScale);
} }
@@ -90,14 +99,20 @@ public sealed class AnnotationSurface : FrameworkElement
{ {
IReadOnlyList<Point> handles = item.GetHandles(); IReadOnlyList<Point> handles = item.GetHandles();
double tolerance = HandleSize / 2 + 2 / ViewScale; double tolerance = HandleSize / 2 + 2 / ViewScale;
int ghost = -1;
for (int i = handles.Count - 1; i >= 0; i--) 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) 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>(); 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) 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> /// <summary>Te klein om te bewaren na het tekenen (bijv. een losse klik met het rechthoek-gereedschap).</summary>
public virtual bool IsDegenerate => false; public virtual bool IsDegenerate => false;
+197 -18
View File
@@ -5,8 +5,16 @@ using System.Windows.Media;
namespace Kniip.Editor.Annotations; 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 public sealed class LineAnnotation : Annotation
{ {
private const int SamplesPerSegment = 16;
private int _dragBend = -1;
private int _pendingGhost = -1;
public LineAnnotation(bool isArrow) public LineAnnotation(bool isArrow)
{ {
IsArrow = isArrow; IsArrow = isArrow;
@@ -18,27 +26,124 @@ public sealed class LineAnnotation : Annotation
public Point End { get; set; } 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 ToolKind Kind => IsArrow ? ToolKind.Arrow : ToolKind.Line;
public override Rect Bounds public override Rect Bounds
{ {
get 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); double pad = Style.Thickness / 2 + (IsArrow ? HeadLength / 2 : 0);
r.Inflate(pad, pad); r.Inflate(pad, pad);
return r; 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 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) public override void Render(DrawingContext dc, EditorDocument document)
{ {
Vector v = End - Start; double length = PathLength(Flatten());
double length = v.Length;
if (length < 1) if (length < 1)
{ {
return; return;
@@ -50,18 +155,26 @@ public sealed class LineAnnotation : Annotation
return; return;
} }
var geometry = new GeometryGroup { FillRule = FillRule.Nonzero }; List<Segment> segments = Segments();
Brush? headFill = null;
if (IsArrow) if (IsArrow)
{ {
Segment lastSegment = segments[^1];
Vector v = lastSegment.B - lastSegment.C2;
if (v.Length < 1e-6)
{
v = lastSegment.B - lastSegment.A;
}
v.Normalize(); v.Normalize();
double headLength = Math.Min(HeadLength, length); double headLength = Math.Min(HeadLength, length);
double halfWidth = headLength * 0.45; double halfWidth = headLength * 0.45;
Point basePoint = End - v * headLength; Point basePoint = End - v * headLength;
var normal = new Vector(-v.Y, v.X); 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(); var head = new StreamGeometry();
using (StreamGeometryContext ctx = head.Open()) using (StreamGeometryContext ctx = head.Open())
@@ -72,39 +185,105 @@ public sealed class LineAnnotation : Annotation
} }
head.Freeze(); 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. // 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); var headPen = RenderHelpers.CreatePen(Style.Stroke, Math.Max(1, Style.Thickness / 3), LineDash.Solid);
RenderHelpers.DrawWithShadow(dc, head, headFill, headPen, Style.Shadow); RenderHelpers.DrawWithShadow(dc, head, headFill, headPen, Style.Shadow);
return; return;
} }
geometry.Children.Add(new LineGeometry(Start, End)); RenderHelpers.DrawWithShadow(dc, BuildGeometry(segments), null, pen, Style.Shadow);
RenderHelpers.DrawWithShadow(dc, geometry, null, pen, Style.Shadow);
} }
public override bool HitTest(Point point, double tolerance) => public override bool HitTest(Point point, double tolerance)
RenderHelpers.DistanceToSegment(point, Start, End) <= tolerance + Style.Thickness / 2 + (IsArrow ? 4 : 0); {
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) public override void Move(Vector delta)
{ {
Start += delta; Start += delta;
End += 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) 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;
}
} }
+7
View File
@@ -59,6 +59,13 @@ public sealed class EditorDocument
} }
} }
/// <summary>Vervangt het knipsel door de platgemaakte versie; de annotaties zitten daarna in de pixels.</summary>
public void Flatten(BitmapSource rendered)
{
SetPixels(PixelUtil.ToOpaqueBgra(rendered), rendered.PixelWidth, rendered.PixelHeight);
Items = new List<Annotation>();
}
public string GetStepLabel(StepAnnotation step) public string GetStepLabel(StepAnnotation step)
{ {
int ordinal = 0; int ordinal = 0;
+10 -4
View File
@@ -5,7 +5,8 @@
Title="kniip" Width="1180" Height="780" MinWidth="760" MinHeight="420" Title="kniip" Width="1180" Height="780" MinWidth="760" MinHeight="420"
WindowStartupLocation="CenterScreen" Background="{StaticResource WindowBrush}" WindowStartupLocation="CenterScreen" Background="{StaticResource WindowBrush}"
FontFamily="Segoe UI" FontSize="13" UseLayoutRounding="True" FontFamily="Segoe UI" FontSize="13" UseLayoutRounding="True"
Icon="pack://application:,,,/AppIcon.ico"> Icon="pack://application:,,,/AppIcon.ico"
AllowDrop="True" PreviewDragEnter="Window_PreviewDragOver" PreviewDragOver="Window_PreviewDragOver" PreviewDrop="Window_PreviewDrop">
<Window.Resources> <Window.Resources>
<local:CornerPreviewConverter x:Key="CornerPreview" /> <local:CornerPreviewConverter x:Key="CornerPreview" />
<local:CursorIconConverter x:Key="CursorIcon" /> <local:CursorIconConverter x:Key="CursorIcon" />
@@ -314,9 +315,14 @@
<TextBlock Text="&#xE8A3;" Style="{StaticResource IconText}" /> <TextBlock Text="&#xE8A3;" Style="{StaticResource IconText}" />
</Button> </Button>
<local:ToolbarSeparator local:ToolbarPanel.ItemId="Sep5" /> <local:ToolbarSeparator local:ToolbarPanel.ItemId="Sep5" />
<Button local:ToolbarPanel.ItemId="Settings" local:ToolbarPanel.Title="Instellingen" <Button x:Name="SettingsButton" local:ToolbarPanel.ItemId="Settings" local:ToolbarPanel.Title="Instellingen"
Style="{StaticResource FlatButton}" ToolTip="Instellingen" Click="Settings_Click"> Style="{StaticResource FlatButton}" ToolTip="Instellingen" Click="Settings_Click">
<TextBlock Text="&#xE713;" Style="{StaticResource IconText}" /> <Grid>
<TextBlock Text="&#xE713;" Style="{StaticResource IconText}" />
<Ellipse x:Name="SettingsBadge" Width="8" Height="8" Fill="{StaticResource AccentBrush}" Stroke="White"
StrokeThickness="1" HorizontalAlignment="Right" VerticalAlignment="Top" Margin="0,-2,-3,0"
IsHitTestVisible="False" Visibility="Collapsed" />
</Grid>
</Button> </Button>
<Button x:Name="SaveButton" local:ToolbarPanel.ItemId="Save" local:ToolbarPanel.Title="Opslaan" <Button x:Name="SaveButton" local:ToolbarPanel.ItemId="Save" local:ToolbarPanel.Title="Opslaan"
Style="{StaticResource SecondaryButton}" ToolTip="Opslaan (Ctrl+S); Shift+klik of Ctrl+Shift+S = opslaan als" Click="Save_Click"> Style="{StaticResource SecondaryButton}" ToolTip="Opslaan (Ctrl+S); Shift+klik of Ctrl+Shift+S = opslaan als" Click="Save_Click">
@@ -343,7 +349,7 @@
</Border> </Border>
<!-- Eigenschappen van het actieve gereedschap of het geselecteerde object --> <!-- Eigenschappen van het actieve gereedschap of het geselecteerde object -->
<Border DockPanel.Dock="Top" Background="White" BorderBrush="{StaticResource DividerBrush}" BorderThickness="0,0,0,1" <Border x:Name="PropsBar" DockPanel.Dock="Top" Background="White" BorderBrush="{StaticResource DividerBrush}" BorderThickness="0,0,0,1"
Padding="12,4" MinHeight="42"> Padding="12,4" MinHeight="42">
<WrapPanel x:Name="PropsPanel" VerticalAlignment="Center" HorizontalAlignment="Center"> <WrapPanel x:Name="PropsPanel" VerticalAlignment="Center" HorizontalAlignment="Center">
<StackPanel x:Name="StrokeGroup" Style="{StaticResource PropGroup}"> <StackPanel x:Name="StrokeGroup" Style="{StaticResource PropGroup}">
+235 -22
View File
@@ -22,6 +22,8 @@ public partial class EditorWindow : Window
{ {
private enum DragMode { None, Creating, Moving, Handle, Cropping } private enum DragMode { None, Creating, Moving, Handle, Cropping }
private enum ZOrder { Front, Forward, Backward, Back }
private static readonly double[] ThicknessValues = { 0, 1, 2, 3, 4, 6, 8, 12, 16, 20, 24, 32 }; private static readonly double[] ThicknessValues = { 0, 1, 2, 3, 4, 6, 8, 12, 16, 20, 24, 32 };
private static readonly double[] FontSizeValues = { 10, 12, 14, 16, 18, 20, 24, 28, 32, 40, 48, 64 }; private static readonly double[] FontSizeValues = { 10, 12, 14, 16, 18, 20, 24, 28, 32, 40, 48, 64 };
private static readonly double[] SizeValues = { 16, 20, 24, 28, 32, 40, 48, 56, 64, 80 }; private static readonly double[] SizeValues = { 16, 20, 24, 28, 32, 40, 48, 56, 64, 80 };
@@ -68,6 +70,7 @@ public partial class EditorWindow : Window
private bool _dirty; private bool _dirty;
private bool _discarding; private bool _discarding;
private bool _exported; private bool _exported;
private bool _toolbarsHidden;
private readonly string? _windowName; private readonly string? _windowName;
/// <param name="windowName">Programmanaam van het geknipte venster, voor %V in het naamsjabloon.</param> /// <param name="windowName">Programmanaam van het geknipte venster, voor %V in het naamsjabloon.</param>
@@ -117,6 +120,10 @@ public partial class EditorWindow : Window
DpiChanged += (_, _) => ApplyZoom(); DpiChanged += (_, _) => ApplyZoom();
PreviewKeyDown += OnPreviewKeyDown; PreviewKeyDown += OnPreviewKeyDown;
App.Instance.Updates.AvailabilityChanged += OnUpdateAvailabilityChanged;
Closed += (_, _) => App.Instance.Updates.AvailabilityChanged -= OnUpdateAvailabilityChanged;
ShowUpdateBadge();
InitializeToolbar(); InitializeToolbar();
ApplySaveButton(); ApplySaveButton();
_suppressStyleEvents = false; _suppressStyleEvents = false;
@@ -452,6 +459,14 @@ public partial class EditorWindow : Window
if (_selected != null) if (_selected != null)
{ {
int handle = Surface.HitHandle(_selected, p); 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) if (handle >= 0)
{ {
_handleIndex = handle; _handleIndex = handle;
@@ -723,26 +738,53 @@ public partial class EditorWindow : Window
private void Surface_MouseRightButtonUp(object sender, MouseButtonEventArgs e) private void Surface_MouseRightButtonUp(object sender, MouseButtonEventArgs e)
{ {
CommitTextEdit(); CommitTextEdit();
Annotation? hit = HitTest(e.GetPosition(Surface)); Point point = e.GetPosition(Surface);
if (hit == null) Annotation? hit = HitTest(point);
{
return;
}
SetSelected(hit); SetSelected(hit);
bool has = hit != null;
int index = hit != null ? _doc.Items.IndexOf(hit) : -1;
int last = _doc.Items.Count - 1;
var menu = new ContextMenu { PlacementTarget = Surface }; var menu = new ContextMenu { PlacementTarget = Surface };
menu.Items.Add(MenuItem("Naar voren", () => Reorder(hit, toFront: true))); menu.Items.Add(MenuItem("Ongedaan maken", "\uE7A7", "Ctrl+Z", Undo, _undo.CanUndo));
menu.Items.Add(MenuItem("Naar achteren", () => Reorder(hit, toFront: false))); menu.Items.Add(MenuItem("Opnieuw", "\uE7A6", "Ctrl+Y", Redo, _undo.CanRedo));
menu.Items.Add(MenuItem("Dupliceren", () => Duplicate(hit)));
menu.Items.Add(new Separator()); menu.Items.Add(new Separator());
menu.Items.Add(MenuItem("Verwijderen", DeleteSelected)); menu.Items.Add(MenuItem("Verwijder", "\uE74D", "Delete", DeleteSelected, has));
menu.Items.Add(MenuItem("Verwijder alles", "\uE894", "Shift+Delete", DeleteAll, last >= 0));
menu.Items.Add(MenuItem("Samenvoegen", "\uE81E", "Ctrl+Shift+F", Flatten, last >= 0));
menu.Items.Add(new Separator());
menu.Items.Add(MenuItem("Knip", "\uE8C6", "Ctrl+X", () => CutAnnotation(hit!), has));
menu.Items.Add(MenuItem("Kopieer", "\uE8C8", "Ctrl+Shift+C", () => CopyAnnotation(hit!), has));
menu.Items.Add(MenuItem("Plak", "\uE77F", "Ctrl+V", () => Paste(point), CanPaste()));
menu.Items.Add(MenuItem("Dupliceren", "\uE710", "Ctrl+D", () => Duplicate(hit!), has));
menu.Items.Add(new Separator());
menu.Items.Add(MenuItem("Naar voorgrond", "\uE898", "Home", () => Reorder(hit!, ZOrder.Front), has && index < last));
menu.Items.Add(MenuItem("Naar voren", "\uE74A", "PageUp", () => Reorder(hit!, ZOrder.Forward), has && index < last));
menu.Items.Add(MenuItem("Naar achteren", "\uE74B", "PageDown", () => Reorder(hit!, ZOrder.Backward), has && index > 0));
menu.Items.Add(MenuItem("Naar achtergrond", "\uE896", "End", () => Reorder(hit!, ZOrder.Back), has && index > 0));
menu.Items.Add(new Separator());
menu.Items.Add(_toolbarsHidden
? MenuItem("Werkbalken tonen", "\uE7B3", "Ctrl+H", ToggleToolbars)
: MenuItem("Werkbalken verbergen", "\uED1A", "Ctrl+H", ToggleToolbars));
menu.IsOpen = true; menu.IsOpen = true;
e.Handled = true; e.Handled = true;
} }
private static MenuItem MenuItem(string header, Action action) private static MenuItem MenuItem(string header, string glyph, string gesture, Action action, bool isEnabled = true)
{ {
var item = new MenuItem { Header = header }; var item = new MenuItem
{
Header = header,
InputGestureText = gesture,
IsEnabled = isEnabled,
Icon = new TextBlock
{
Text = glyph,
FontFamily = (FontFamily)Application.Current.FindResource("IconFont"),
FontSize = 14,
VerticalAlignment = VerticalAlignment.Center
}
};
item.Click += (_, _) => action(); item.Click += (_, _) => action();
return item; return item;
} }
@@ -781,20 +823,121 @@ public partial class EditorWindow : Window
SetSelected(null); SetSelected(null);
} }
private void Reorder(Annotation item, bool toFront) private void DeleteAll()
{ {
Commit(_doc.CaptureState()); if (_doc.Items.Count == 0)
_doc.Items.Remove(item);
if (toFront)
{ {
_doc.Items.Add(item); return;
}
Commit(_doc.CaptureState());
_doc.Items.Clear();
SetSelected(null);
}
private void Flatten()
{
CommitTextEdit();
if (_doc.Items.Count == 0)
{
return;
}
Commit(_doc.CaptureState());
_doc.Flatten(DocumentRenderer.Export(_doc));
SetSelected(null);
ShowTemporaryHint("Annotaties samengevoegd met de afbeelding.");
}
private void Reorder(Annotation item, ZOrder order)
{
int index = _doc.Items.IndexOf(item);
int last = _doc.Items.Count - 1;
int target = order switch
{
ZOrder.Front => last,
ZOrder.Forward => Math.Min(index + 1, last),
ZOrder.Backward => Math.Max(index - 1, 0),
_ => 0
};
if (index < 0 || target == index)
{
return;
}
Commit(_doc.CaptureState());
_doc.Items.RemoveAt(index);
_doc.Items.Insert(target, item);
Surface.InvalidateVisual();
}
private void CopyAnnotation(Annotation item)
{
if (!AnnotationClipboard.Set(item))
{
ShowTemporaryHint("Kopiëren mislukt: het klembord is bezet. Probeer het opnieuw.");
}
}
private void CutAnnotation(Annotation item)
{
if (AnnotationClipboard.Set(item))
{
SetSelected(item);
DeleteSelected();
} }
else else
{ {
_doc.Items.Insert(0, item); ShowTemporaryHint("Knippen mislukt: het klembord is bezet. Probeer het opnieuw.");
}
}
/// <param name="at">Middelpunt voor de geplakte annotatie; zonder punt een kleine verschuiving.</param>
private void Paste(Point? at = null)
{
if (AnnotationClipboard.Get() is not { } copy)
{
PasteImage();
return;
} }
Surface.InvalidateVisual(); Commit(_doc.CaptureState());
Rect bounds = copy.Bounds;
Vector offset = at is { } point
? point - new Point(bounds.X + bounds.Width / 2, bounds.Y + bounds.Height / 2)
: new Vector(12 / Surface.ViewScale, 12 / Surface.ViewScale);
copy.Move(offset);
_doc.Items.Add(copy);
SetSelected(copy);
}
private static bool CanPaste()
{
if (AnnotationClipboard.Get() != null)
{
return true;
}
try
{
return Clipboard.ContainsImage() || Clipboard.ContainsData("PNG") || Clipboard.ContainsFileDropList();
}
catch (System.Runtime.InteropServices.ExternalException)
{
return false;
}
}
private void ToggleToolbars()
{
_toolbarsHidden = !_toolbarsHidden;
Visibility visibility = _toolbarsHidden ? Visibility.Collapsed : Visibility.Visible;
Toolbar.Visibility = visibility;
PropsBar.Visibility = visibility;
if (_toolbarsHidden)
{
ShowTemporaryHint("Werkbalken verborgen. Ctrl+H of rechtsklik op het canvas om ze terug te halen.");
}
} }
private void Duplicate(Annotation item) private void Duplicate(Annotation item)
@@ -1539,6 +1682,15 @@ public partial class EditorWindow : Window
private void Settings_Click(object sender, RoutedEventArgs e) => App.Instance.ShowSettings(); private void Settings_Click(object sender, RoutedEventArgs e) => App.Instance.ShowSettings();
private void OnUpdateAvailabilityChanged(object? sender, EventArgs e) => ShowUpdateBadge();
private void ShowUpdateBadge()
{
bool available = App.Instance.Updates.IsUpdateAvailable;
SettingsBadge.Visibility = available ? Visibility.Visible : Visibility.Collapsed;
SettingsButton.ToolTip = available ? "Instellingen - nieuwe versie beschikbaar" : "Instellingen";
}
private void NewSnip_Click(object sender, RoutedEventArgs e) => DiscardAndStartNewSnip(); private void NewSnip_Click(object sender, RoutedEventArgs e) => DiscardAndStartNewSnip();
private void Clear_Click(object sender, RoutedEventArgs e) => DiscardAndStartBlank(); private void Clear_Click(object sender, RoutedEventArgs e) => DiscardAndStartBlank();
@@ -1595,6 +1747,36 @@ public partial class EditorWindow : Window
ReplaceWith(image, "Wil je doorgaan en de afbeelding van het klembord openen?"); ReplaceWith(image, "Wil je doorgaan en de afbeelding van het klembord openen?");
} }
private void Window_PreviewDragOver(object sender, DragEventArgs e)
{
if (!ClipboardImageReader.CanDrop(e.Data))
{
return;
}
e.Effects = DragDropEffects.Copy;
e.Handled = true;
}
private void Window_PreviewDrop(object sender, DragEventArgs e)
{
if (!ClipboardImageReader.CanDrop(e.Data))
{
return;
}
e.Handled = true;
BitmapSource? image = ClipboardImageReader.FromDrop(e.Data);
if (image == null)
{
ShowTemporaryHint("Deze afbeelding kon niet worden geopend.");
return;
}
// Pas na het slepen vragen: een dialoog tijdens Drop blokkeert Verkenner.
Dispatcher.BeginInvoke(() => ReplaceWith(image, "Wil je doorgaan en deze afbeelding openen?"));
}
private void OpenImageFile() private void OpenImageFile()
{ {
CommitTextEdit(); CommitTextEdit();
@@ -1698,10 +1880,26 @@ public partial class EditorWindow : Window
break; break;
case Key.Z: case Key.Z:
Undo(); Undo();
break;
case Key.C when shift:
if (_selected != null)
{
CopyAnnotation(_selected);
}
break; break;
case Key.C: case Key.C:
CopyToClipboard(); CopyToClipboard();
break; break;
case Key.X when _selected != null:
CutAnnotation(_selected);
break;
case Key.F when shift:
Flatten();
break;
case Key.H:
ToggleToolbars();
break;
case Key.S when shift: case Key.S when shift:
SaveAs(); SaveAs();
break; break;
@@ -1718,7 +1916,7 @@ public partial class EditorWindow : Window
DiscardAndStartBlank(); DiscardAndStartBlank();
break; break;
case Key.V: case Key.V:
PasteImage(); Paste();
break; break;
case Key.O: case Key.O:
OpenImageFile(); OpenImageFile();
@@ -1758,10 +1956,25 @@ public partial class EditorWindow : Window
double step = shift ? 10 : 1; double step = shift ? 10 : 1;
switch (key) switch (key)
{ {
case Key.Delete when shift:
DeleteAll();
break;
case Key.Delete: case Key.Delete:
case Key.Back: case Key.Back:
DeleteSelected(); DeleteSelected();
break; break;
case Key.Home when _selected != null:
Reorder(_selected, ZOrder.Front);
break;
case Key.PageUp when _selected != null:
Reorder(_selected, ZOrder.Forward);
break;
case Key.PageDown when _selected != null:
Reorder(_selected, ZOrder.Backward);
break;
case Key.End when _selected != null:
Reorder(_selected, ZOrder.Back);
break;
case Key.Escape: case Key.Escape:
if (_drag != DragMode.None) if (_drag != DragMode.None)
{ {
@@ -1847,8 +2060,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.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.Rectangle => "Sleep om een rechthoek te tekenen. Shift = vierkant.",
ToolKind.Ellipse => "Sleep om een ellips te tekenen. Shift = cirkel.", ToolKind.Ellipse => "Sleep om een ellips te tekenen. Shift = cirkel.",
ToolKind.Line => "Sleep om een lijn 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°.", 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.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.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.", ToolKind.Cursor => "Klik om een cursor te plaatsen; de punt komt precies op de klikplek.",
+92
View File
@@ -4,9 +4,11 @@ using System.IO;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Threading; using System.Threading;
using System.Windows; using System.Windows;
using System.Windows.Interop;
using System.Windows.Media; using System.Windows.Media;
using System.Windows.Media.Imaging; using System.Windows.Media.Imaging;
using Kniip.Capture; using Kniip.Capture;
using Kniip.Interop;
namespace Kniip.Editor; namespace Kniip.Editor;
@@ -68,13 +70,103 @@ internal static class ClipboardWriter
return TrySet(data); return TrySet(data);
} }
/// <summary>Alleen een eigen markering op het klembord, zonder afbeelding of tekst.</summary>
public static bool SetMarker(string format, string value)
{
var data = new DataObject();
data.SetData(format, value);
return TrySet(data);
}
/// <summary>
/// Tekst gaat rechtstreeks via Win32 op het klembord. De OLE-route van WPF moet het klembord
/// daarna nog leegspoelen, en dat mislukt zodra een klembordbeheerder het meteen opent.
/// </summary>
public static bool SetText(string text) public static bool SetText(string text)
{ {
if (TrySetUnicodeText(text))
{
return true;
}
var data = new DataObject(); var data = new DataObject();
data.SetText(text); data.SetText(text);
return TrySet(data); return TrySet(data);
} }
private static HwndSource? _owner;
private static IntPtr OwnerHandle
{
get
{
// Een venster als eigenaar is verplicht: zonder eigenaar weigert SetClipboardData na EmptyClipboard.
_owner ??= new HwndSource(new HwndSourceParameters("KniipClipboardWriter")
{
ParentWindow = NativeMethods.HWND_MESSAGE,
WindowStyle = 0
});
return _owner.Handle;
}
}
private static bool TrySetUnicodeText(string text)
{
IntPtr owner = OwnerHandle;
for (int attempt = 0; attempt < 20; attempt++)
{
if (NativeMethods.OpenClipboard(owner))
{
try
{
return NativeMethods.EmptyClipboard() && PutUnicodeText(text);
}
finally
{
NativeMethods.CloseClipboard();
}
}
Thread.Sleep(50);
}
return false;
}
private static bool PutUnicodeText(string text)
{
IntPtr handle = NativeMethods.GlobalAlloc(NativeMethods.GMEM_MOVEABLE, (UIntPtr)((text.Length + 1) * 2));
if (handle == IntPtr.Zero)
{
return false;
}
IntPtr target = NativeMethods.GlobalLock(handle);
if (target == IntPtr.Zero)
{
NativeMethods.GlobalFree(handle);
return false;
}
try
{
Marshal.Copy(text.ToCharArray(), 0, target, text.Length);
Marshal.WriteInt16(target, text.Length * 2, 0);
}
finally
{
NativeMethods.GlobalUnlock(handle);
}
if (NativeMethods.SetClipboardData(NativeMethods.CF_UNICODETEXT, handle) == IntPtr.Zero)
{
NativeMethods.GlobalFree(handle);
return false;
}
return true;
}
private static bool TrySet(DataObject data) private static bool TrySet(DataObject data)
{ {
for (int attempt = 0; attempt < 5; attempt++) for (int attempt = 0; attempt < 5; attempt++)
+19
View File
@@ -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
}
+11 -6
View File
@@ -9,10 +9,10 @@ namespace Kniip.Infrastructure;
/// </summary> /// </summary>
internal static class ShellIntegration internal static class ShellIntegration
{ {
private const string ProgId = "Kniip.Image"; private const string ProgId = AppIdentity.Key + ".Image";
private const string AppKeyName = "Kniip.exe"; private const string AppKeyName = AppIdentity.Key + ".exe";
private const string RunKeyPath = @"Software\Microsoft\Windows\CurrentVersion\Run"; 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" }; 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}")) using (var prog = Registry.CurrentUser.CreateSubKey($@"Software\Classes\{ProgId}"))
{ {
prog.SetValue(null, "kniip afbeelding"); prog.SetValue(null, $"{AppIdentity.DisplayName} afbeelding");
prog.SetValue("FriendlyTypeName", "kniip afbeelding"); prog.SetValue("FriendlyTypeName", $"{AppIdentity.DisplayName} afbeelding");
} }
using (var icon = Registry.CurrentUser.CreateSubKey($@"Software\Classes\{ProgId}\DefaultIcon")) 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}")) 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")) using (var appCmd = Registry.CurrentUser.CreateSubKey($@"Software\Classes\Applications\{AppKeyName}\shell\open\command"))
{ {
@@ -89,6 +89,11 @@ internal static class ShellIntegration
/// </summary> /// </summary>
public static void MigrateLegacyRegistration() public static void MigrateLegacyRegistration()
{ {
if (AppIdentity.IsTest)
{
return;
}
const string legacyProgId = "KnipPlus.Image"; const string legacyProgId = "KnipPlus.Image";
const string legacyAppKeyName = "KnipPlus.exe"; const string legacyAppKeyName = "KnipPlus.exe";
const string legacyRunValueName = "KnipPlus"; const string legacyRunValueName = "KnipPlus";
+1 -1
View File
@@ -13,7 +13,7 @@ namespace Kniip.Infrastructure;
/// </summary> /// </summary>
internal sealed class SingleInstance : IDisposable 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 static readonly string PipeName = BaseName + "-pipe";
private readonly CancellationTokenSource _cts = new(); private readonly CancellationTokenSource _cts = new();
+11 -5
View File
@@ -9,6 +9,7 @@ internal sealed class TrayIcon : IDisposable
private readonly NotifyIcon _icon; private readonly NotifyIcon _icon;
private const string RecordText = "Screencast opnemen (MP4)..."; private const string RecordText = "Screencast opnemen (MP4)...";
private readonly ToolStripMenuItem _clipboardItem;
private readonly ToolStripMenuItem _pauseItem; private readonly ToolStripMenuItem _pauseItem;
private readonly ToolStripMenuItem _snipItem; private readonly ToolStripMenuItem _snipItem;
private readonly ToolStripMenuItem _windowItem; private readonly ToolStripMenuItem _windowItem;
@@ -28,11 +29,11 @@ internal sealed class TrayIcon : IDisposable
public TrayIcon() public TrayIcon()
{ {
var menu = new ContextMenuStrip(); var menu = new ContextMenuStrip();
var openClipboard = new ToolStripMenuItem("Bewerken vanaf klembord", null, (_, _) => OpenFromClipboardRequested?.Invoke()) _clipboardItem = new ToolStripMenuItem("Bewerken vanaf klembord", null, (_, _) => OpenFromClipboardRequested?.Invoke())
{ {
Font = new Font(SystemFonts.MenuFont ?? Control.DefaultFont, FontStyle.Bold) Font = new Font(SystemFonts.MenuFont ?? Control.DefaultFont, FontStyle.Bold)
}; };
menu.Items.Add(openClipboard); menu.Items.Add(_clipboardItem);
menu.Items.Add("Afbeelding openen...", null, (_, _) => OpenFileRequested?.Invoke()); menu.Items.Add("Afbeelding openen...", null, (_, _) => OpenFileRequested?.Invoke());
menu.Items.Add(new ToolStripSeparator()); menu.Items.Add(new ToolStripSeparator());
_snipItem = new ToolStripMenuItem("Rechthoek knippen", null, (_, _) => SnipRequested?.Invoke()); _snipItem = new ToolStripMenuItem("Rechthoek knippen", null, (_, _) => SnipRequested?.Invoke());
@@ -55,7 +56,7 @@ internal sealed class TrayIcon : IDisposable
_icon = new NotifyIcon _icon = new NotifyIcon
{ {
Icon = LoadIcon(), Icon = LoadIcon(),
Text = "kniip - annotaties voor het Knipprogramma", Text = IdleText,
ContextMenuStrip = menu, ContextMenuStrip = menu,
Visible = true Visible = true
}; };
@@ -71,8 +72,9 @@ internal sealed class TrayIcon : IDisposable
public bool IsPaused => _pauseItem.Checked; public bool IsPaused => _pauseItem.Checked;
/// <summary>Toont de sneltoetsen naast de menu-items; null = geen sneltoets.</summary> /// <summary>Toont de sneltoetsen naast de menu-items; null = geen sneltoets.</summary>
public void SetHotkeys(string? snip, string? window, string? record, string? gif) public void SetHotkeys(string? snip, string? window, string? record, string? gif, string? clipboard)
{ {
_clipboardItem.ShortcutKeyDisplayString = clipboard;
_snipItem.ShortcutKeyDisplayString = snip; _snipItem.ShortcutKeyDisplayString = snip;
_windowItem.ShortcutKeyDisplayString = window; _windowItem.ShortcutKeyDisplayString = window;
_recordItem.ShortcutKeyDisplayString = record; _recordItem.ShortcutKeyDisplayString = record;
@@ -84,9 +86,13 @@ internal sealed class TrayIcon : IDisposable
{ {
_recordItem.Text = text ?? RecordText; _recordItem.Text = text ?? RecordText;
_gifItem.Visible = text == null; _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) => public void ShowBalloon(string title, string text) =>
_icon.ShowBalloonTip(4000, title, text, ToolTipIcon.Info); _icon.ShowBalloonTip(4000, title, text, ToolTipIcon.Info);
+31
View File
@@ -26,6 +26,37 @@ internal static class NativeMethods
[DllImport("user32.dll")] [DllImport("user32.dll")]
public static extern uint GetClipboardSequenceNumber(); public static extern uint GetClipboardSequenceNumber();
public const uint CF_UNICODETEXT = 13;
public const uint GMEM_MOVEABLE = 0x0002;
[DllImport("user32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool OpenClipboard(IntPtr hWndNewOwner);
[DllImport("user32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool CloseClipboard();
[DllImport("user32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool EmptyClipboard();
[DllImport("user32.dll", SetLastError = true)]
public static extern IntPtr SetClipboardData(uint format, IntPtr hMem);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern IntPtr GlobalAlloc(uint flags, UIntPtr bytes);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern IntPtr GlobalLock(IntPtr hMem);
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool GlobalUnlock(IntPtr hMem);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern IntPtr GlobalFree(IntPtr hMem);
[DllImport("user32.dll")] [DllImport("user32.dll")]
public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint processId); public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint processId);
+7 -1
View File
@@ -18,7 +18,7 @@
<ApplicationManifest>app.manifest</ApplicationManifest> <ApplicationManifest>app.manifest</ApplicationManifest>
<Product>kniip</Product> <Product>kniip</Product>
<Description>Annotatie-editor naast het Windows Knipprogramma</Description> <Description>Annotatie-editor naast het Windows Knipprogramma</Description>
<Version>1.1.0</Version> <Version>1.1.4</Version>
<SatelliteResourceLanguages>nl;en</SatelliteResourceLanguages> <SatelliteResourceLanguages>nl;en</SatelliteResourceLanguages>
<!-- WPF regelt DPI via app.manifest; de WinForms-analyzer is hier niet van toepassing. --> <!-- WPF regelt DPI via app.manifest; de WinForms-analyzer is hier niet van toepassing. -->
<NoWarn>$(NoWarn);WFAC010</NoWarn> <NoWarn>$(NoWarn);WFAC010</NoWarn>
@@ -26,6 +26,12 @@
<AllowedReferenceRelatedFileExtensions>.none</AllowedReferenceRelatedFileExtensions> <AllowedReferenceRelatedFileExtensions>.none</AllowedReferenceRelatedFileExtensions>
</PropertyGroup> </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> <ItemGroup>
<Resource Include="AppIcon.ico" /> <Resource Include="AppIcon.ico" />
<Resource Include="Assets\Emoji\emoji.json;Assets\Emoji\twemoji-64.png" /> <Resource Include="Assets\Emoji\emoji.json;Assets\Emoji\twemoji-64.png" />
+1 -1
View File
@@ -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> /// <summary>Eén opname naar een tijdelijk MP4-bestand. Gebeurtenissen komen binnen op de UI-thread.</summary>
internal sealed class RecordingSession : IDisposable 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 Dispatcher _dispatcher;
private readonly RecordingRegion _region; private readonly RecordingRegion _region;
+6
View File
@@ -63,6 +63,9 @@ public sealed class AppSettings
/// <summary>Sneltoets voor een screencast die als GIF wordt aangeboden.</summary> /// <summary>Sneltoets voor een screencast die als GIF wordt aangeboden.</summary>
public string GifHotkey { get; set; } = "Ctrl+Shift+G"; public string GifHotkey { get; set; } = "Ctrl+Shift+G";
/// <summary>Sneltoets om de afbeelding op het klembord in de editor te openen; standaard geen.</summary>
public string ClipboardHotkey { get; set; } = "";
public bool RecordSystemAudio { get; set; } = true; public bool RecordSystemAudio { get; set; } = true;
public bool RecordMicrophone { get; set; } public bool RecordMicrophone { get; set; }
public bool RecordShowCursor { get; set; } = true; public bool RecordShowCursor { get; set; } = true;
@@ -96,6 +99,9 @@ public sealed class AppSettings
/// <summary>Zipline-server waar Uploaden naartoe gaat; leeg betekent geen uploadknop.</summary> /// <summary>Zipline-server waar Uploaden naartoe gaat; leeg betekent geen uploadknop.</summary>
public UploaderConfig? Uploader { get; set; } public UploaderConfig? Uploader { get; set; }
/// <summary>Bij het starten en daarna dagelijks op de achtergrond kijken of er een nieuwere versie is.</summary>
public bool CheckForUpdates { get; set; } = true;
public StyleSet StyleFor(ToolKind tool) public StyleSet StyleFor(ToolKind tool)
{ {
if (!ToolStyles.TryGetValue(tool, out StyleSet? style)) if (!ToolStyles.TryGetValue(tool, out StyleSet? style))
+8 -3
View File
@@ -7,19 +7,24 @@ using System.Text;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using System.Windows.Media; using System.Windows.Media;
using Kniip.Infrastructure;
namespace Kniip.Settings; namespace Kniip.Settings;
public static class SettingsStore public static class SettingsStore
{ {
public static string Folder { get; } = 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"); 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 => 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() private static readonly JsonSerializerOptions Options = new()
{ {
+22 -9
View File
@@ -1,7 +1,7 @@
<Window x:Class="Kniip.Settings.SettingsWindow" <Window x:Class="Kniip.Settings.SettingsWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="kniip instellingen" Width="720" Height="540" ResizeMode="NoResize" Title="kniip instellingen" Width="720" Height="600" ResizeMode="NoResize"
WindowStartupLocation="CenterScreen" Background="{StaticResource WindowBrush}" WindowStartupLocation="CenterScreen" Background="{StaticResource WindowBrush}"
FontFamily="Segoe UI" FontSize="13" Icon="pack://application:,,,/AppIcon.ico"> FontFamily="Segoe UI" FontSize="13" Icon="pack://application:,,,/AppIcon.ico">
<Window.Resources> <Window.Resources>
@@ -70,11 +70,8 @@
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<TabPanel Grid.Column="0" IsItemsHost="True" VerticalAlignment="Top" Background="Transparent" /> <TabPanel Grid.Column="0" IsItemsHost="True" VerticalAlignment="Top" Background="Transparent" />
<Border Grid.Column="1" Background="White" BorderBrush="#E5E5E5" BorderThickness="1" <Border Grid.Column="1" Background="White" BorderBrush="#E5E5E5" BorderThickness="1"
CornerRadius="6" SnapsToDevicePixels="True"> CornerRadius="6" SnapsToDevicePixels="True" Padding="16,14">
<ScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled" <ContentPresenter ContentSource="SelectedContent" />
Padding="16,14">
<ContentPresenter ContentSource="SelectedContent" />
</ScrollViewer>
</Border> </Border>
</Grid> </Grid>
</ControlTemplate> </ControlTemplate>
@@ -130,13 +127,20 @@
<Button Tag="Record" Style="{StaticResource FlatButton}" Content="Geen" Click="ClearHotkey_Click" Margin="6,0,0,0" <Button Tag="Record" Style="{StaticResource FlatButton}" Content="Geen" Click="ClearHotkey_Click" Margin="6,0,0,0"
ToolTip="Geen sneltoets gebruiken" /> ToolTip="Geen sneltoets gebruiken" />
</StackPanel> </StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,0,0,6"> <StackPanel Orientation="Horizontal" Margin="0,0,0,4">
<TextBlock Text="Screencast (GIF)" VerticalAlignment="Center" Width="150" /> <TextBlock Text="Screencast (GIF)" VerticalAlignment="Center" Width="150" />
<Button x:Name="GifHotkeyButton" Tag="Gif" Style="{StaticResource SecondaryButton}" Width="170" Margin="0" <Button x:Name="GifHotkeyButton" Tag="Gif" Style="{StaticResource SecondaryButton}" Width="170" Margin="0"
Click="HotkeyButton_Click" ToolTip="Klik en druk daarna de gewenste toetscombinatie in" /> Click="HotkeyButton_Click" ToolTip="Klik en druk daarna de gewenste toetscombinatie in" />
<Button Tag="Gif" Style="{StaticResource FlatButton}" Content="Geen" Click="ClearHotkey_Click" Margin="6,0,0,0" <Button Tag="Gif" Style="{StaticResource FlatButton}" Content="Geen" Click="ClearHotkey_Click" Margin="6,0,0,0"
ToolTip="Geen sneltoets gebruiken" /> ToolTip="Geen sneltoets gebruiken" />
</StackPanel> </StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,0,0,6">
<TextBlock Text="Vanaf klembord" VerticalAlignment="Center" Width="150" />
<Button x:Name="ClipboardHotkeyButton" Tag="Clipboard" Style="{StaticResource SecondaryButton}" Width="170" Margin="0"
Click="HotkeyButton_Click" ToolTip="Klik en druk daarna de gewenste toetscombinatie in; opent de afbeelding op het klembord" />
<Button Tag="Clipboard" Style="{StaticResource FlatButton}" Content="Geen" Click="ClearHotkey_Click" Margin="6,0,0,0"
ToolTip="Geen sneltoets gebruiken" />
</StackPanel>
<TextBlock x:Name="HotkeyHint" Style="{StaticResource Hint}" Margin="0,0,0,4" /> <TextBlock x:Name="HotkeyHint" Style="{StaticResource Hint}" Margin="0,0,0,4" />
</StackPanel> </StackPanel>
</TabItem> </TabItem>
@@ -252,10 +256,19 @@
</StackPanel> </StackPanel>
</TabItem> </TabItem>
<TabItem x:Name="UpdateTab" Header="Update"> <TabItem x:Name="UpdateTab">
<TabItem.Header>
<StackPanel Orientation="Horizontal">
<TextBlock Text="Update" />
<Ellipse x:Name="UpdateTabBadge" Width="8" Height="8" Fill="{StaticResource AccentBrush}" Stroke="White"
StrokeThickness="1" Margin="6,0,0,0" VerticalAlignment="Center" Visibility="Collapsed"
ToolTip="Er is een nieuwe versie beschikbaar" />
</StackPanel>
</TabItem.Header>
<StackPanel> <StackPanel>
<TextBlock Text="Update" Style="{StaticResource CardTitle}" /> <TextBlock Text="Update" Style="{StaticResource CardTitle}" />
<TextBlock x:Name="CurrentVersionText" Margin="0,0,0,2" /> <TextBlock x:Name="CurrentVersionText" Margin="0,0,0,2" />
<CheckBox x:Name="AutoUpdateCheckBox" Content="Automatisch controleren op nieuwe versies (bij starten en dagelijks)" />
<TextBlock x:Name="UpdateStatus" Style="{StaticResource Hint}" Margin="0,0,0,8" /> <TextBlock x:Name="UpdateStatus" Style="{StaticResource Hint}" Margin="0,0,0,8" />
<StackPanel Orientation="Horizontal" Margin="0,0,0,8"> <StackPanel Orientation="Horizontal" Margin="0,0,0,8">
<Button x:Name="UpdateButton" Style="{StaticResource AccentButton}" Click="Update_Click" Margin="0,0,6,0" <Button x:Name="UpdateButton" Style="{StaticResource AccentButton}" Click="Update_Click" Margin="0,0,6,0"
@@ -267,7 +280,7 @@
</StackPanel> </StackPanel>
<ProgressBar x:Name="DownloadProgress" Height="4" Minimum="0" Maximum="1" Margin="0,0,0,8" <ProgressBar x:Name="DownloadProgress" Height="4" Minimum="0" Maximum="1" Margin="0,0,0,8"
Foreground="{StaticResource AccentBrush}" Visibility="Collapsed" /> Foreground="{StaticResource AccentBrush}" Visibility="Collapsed" />
<Border BorderBrush="{StaticResource DividerBrush}" BorderThickness="1" CornerRadius="4" Height="270"> <Border BorderBrush="{StaticResource DividerBrush}" BorderThickness="1" CornerRadius="4" Height="300">
<FlowDocumentScrollViewer x:Name="ChangelogViewer" VerticalScrollBarVisibility="Auto" <FlowDocumentScrollViewer x:Name="ChangelogViewer" VerticalScrollBarVisibility="Auto"
HorizontalScrollBarVisibility="Disabled" IsToolBarVisible="False" HorizontalScrollBarVisibility="Disabled" IsToolBarVisible="False"
Zoom="100" MinZoom="100" MaxZoom="100" Focusable="False" /> Zoom="100" MinZoom="100" MaxZoom="100" Focusable="False" />
+24
View File
@@ -26,6 +26,7 @@ public partial class SettingsWindow : Window
["Window"] = "venster knippen", ["Window"] = "venster knippen",
["Record"] = "screencast (MP4)", ["Record"] = "screencast (MP4)",
["Gif"] = "screencast (GIF)", ["Gif"] = "screencast (GIF)",
["Clipboard"] = "bewerken vanaf klembord",
}; };
private readonly AppSettings _settings; private readonly AppSettings _settings;
@@ -44,6 +45,7 @@ public partial class SettingsWindow : Window
{ {
InitializeComponent(); InitializeComponent();
_settings = settings; _settings = settings;
Title = $"{AppIdentity.DisplayName} instellingen";
FolderText.Text = CaptureCoordinator.ScreenshotsFolder ?? "Map Schermafbeeldingen niet gevonden."; FolderText.Text = CaptureCoordinator.ScreenshotsFolder ?? "Map Schermafbeeldingen niet gevonden.";
AutostartBox.IsChecked = ShellIntegration.IsAutostartEnabled(); AutostartBox.IsChecked = ShellIntegration.IsAutostartEnabled();
@@ -62,17 +64,21 @@ public partial class SettingsWindow : Window
["Window"] = WindowHotkeyButton, ["Window"] = WindowHotkeyButton,
["Record"] = RecordHotkeyButton, ["Record"] = RecordHotkeyButton,
["Gif"] = GifHotkeyButton, ["Gif"] = GifHotkeyButton,
["Clipboard"] = ClipboardHotkeyButton,
}; };
HotkeyHint.Text = App.Instance.HotkeyHook?.IsActive == true HotkeyHint.Text = App.Instance.HotkeyHook?.IsActive == true
? HotkeyHintText ? HotkeyHintText
: "De toetsenbordhaak kon niet worden gestart; de sneltoetsen werken nu niet."; : "De toetsenbordhaak kon niet worden gestart; de sneltoetsen werken nu niet.";
Deactivated += (_, _) => StopListening(); Deactivated += (_, _) => StopListening();
App.Instance.Updates.AvailabilityChanged += OnUpdateAvailabilityChanged;
Closed += (_, _) => Closed += (_, _) =>
{ {
StopListening(); StopListening();
_updateCts?.Cancel(); _updateCts?.Cancel();
App.Instance.Updates.AvailabilityChanged -= OnUpdateAvailabilityChanged;
}; };
ShowUpdateBadge();
CurrentVersionText.Text = $"Geïnstalleerde versie: {UpdateChecker.CurrentVersion}"; CurrentVersionText.Text = $"Geïnstalleerde versie: {UpdateChecker.CurrentVersion}";
LoadFrom(settings); LoadFrom(settings);
@@ -91,6 +97,7 @@ public partial class SettingsWindow : Window
ActionOpenRadio.IsChecked = settings.Action == CaptureAction.OpenEditor; ActionOpenRadio.IsChecked = settings.Action == CaptureAction.OpenEditor;
ActionNothingRadio.IsChecked = settings.Action == CaptureAction.Nothing; ActionNothingRadio.IsChecked = settings.Action == CaptureAction.Nothing;
StraightHighlightBox.IsChecked = settings.StraightHighlight; StraightHighlightBox.IsChecked = settings.StraightHighlight;
AutoUpdateCheckBox.IsChecked = settings.CheckForUpdates;
_snipFolder = settings.SnipSaveFolder; _snipFolder = settings.SnipSaveFolder;
_videoFolder = settings.VideoSaveFolder; _videoFolder = settings.VideoSaveFolder;
@@ -105,6 +112,7 @@ public partial class SettingsWindow : Window
_hotkeys["Window"] = settings.WindowHotkey; _hotkeys["Window"] = settings.WindowHotkey;
_hotkeys["Record"] = settings.RecordHotkey; _hotkeys["Record"] = settings.RecordHotkey;
_hotkeys["Gif"] = settings.GifHotkey; _hotkeys["Gif"] = settings.GifHotkey;
_hotkeys["Clipboard"] = settings.ClipboardHotkey;
foreach (string id in _hotkeys.Keys) foreach (string id in _hotkeys.Keys)
{ {
ShowHotkey(id); ShowHotkey(id);
@@ -433,11 +441,13 @@ public partial class SettingsWindow : Window
: ActionNothingRadio.IsChecked == true ? CaptureAction.Nothing : ActionNothingRadio.IsChecked == true ? CaptureAction.Nothing
: CaptureAction.Notify; : CaptureAction.Notify;
_settings.StraightHighlight = StraightHighlightBox.IsChecked == true; _settings.StraightHighlight = StraightHighlightBox.IsChecked == true;
_settings.CheckForUpdates = AutoUpdateCheckBox.IsChecked == true;
StopListening(); StopListening();
_settings.SnipHotkey = _hotkeys["Snip"]; _settings.SnipHotkey = _hotkeys["Snip"];
_settings.WindowHotkey = _hotkeys["Window"]; _settings.WindowHotkey = _hotkeys["Window"];
_settings.RecordHotkey = _hotkeys["Record"]; _settings.RecordHotkey = _hotkeys["Record"];
_settings.GifHotkey = _hotkeys["Gif"]; _settings.GifHotkey = _hotkeys["Gif"];
_settings.ClipboardHotkey = _hotkeys["Clipboard"];
_settings.RecordSystemAudio = SystemAudioBox.IsChecked == true; _settings.RecordSystemAudio = SystemAudioBox.IsChecked == true;
_settings.RecordMicrophone = MicrophoneBox.IsChecked == true; _settings.RecordMicrophone = MicrophoneBox.IsChecked == true;
_settings.RecordShowCursor = ShowCursorBox.IsChecked == true; _settings.RecordShowCursor = ShowCursorBox.IsChecked == true;
@@ -500,6 +510,11 @@ public partial class SettingsWindow : Window
private void CheckUpdate_Click(object sender, RoutedEventArgs e) => _ = CheckForUpdatesAsync(); private void CheckUpdate_Click(object sender, RoutedEventArgs e) => _ = CheckForUpdatesAsync();
private void OnUpdateAvailabilityChanged(object? sender, EventArgs e) => ShowUpdateBadge();
private void ShowUpdateBadge() =>
UpdateTabBadge.Visibility = App.Instance.Updates.IsUpdateAvailable ? Visibility.Visible : Visibility.Collapsed;
private void ReleasePage_Click(object sender, RoutedEventArgs e) => OpenUrl(UpdateChecker.ReleasesPage); private void ReleasePage_Click(object sender, RoutedEventArgs e) => OpenUrl(UpdateChecker.ReleasesPage);
private async System.Threading.Tasks.Task CheckForUpdatesAsync() private async System.Threading.Tasks.Task CheckForUpdatesAsync()
@@ -543,6 +558,7 @@ public partial class SettingsWindow : Window
{ {
Version current = UpdateChecker.CurrentVersion; Version current = UpdateChecker.CurrentVersion;
List<ReleaseInfo> newer = releases.Where(r => r.Version > current).ToList(); List<ReleaseInfo> newer = releases.Where(r => r.Version > current).ToList();
App.Instance.Updates.Report(newer.FirstOrDefault());
if (newer.Count == 0) if (newer.Count == 0)
{ {
UpdateStatus.Text = releases.Count == 0 UpdateStatus.Text = releases.Count == 0
@@ -553,6 +569,14 @@ public partial class SettingsWindow : Window
return; 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); _update = newer.FirstOrDefault(r => r.InstallerUrl != null);
if (_update != null) if (_update != null)
{ {
+97
View File
@@ -0,0 +1,97 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Threading;
namespace Kniip.Update;
/// <summary>Kijkt op de achtergrond of er een nieuwere versie is en meldt dat aan de vensters.</summary>
public sealed class UpdateNotifier
{
private static readonly TimeSpan FirstCheckDelay = TimeSpan.FromSeconds(10);
private static readonly TimeSpan CheckInterval = TimeSpan.FromHours(24);
private readonly DispatcherTimer _timer = new();
private bool _enabled;
private bool _checking;
public UpdateNotifier()
{
_timer.Tick += async (_, _) =>
{
_timer.Interval = CheckInterval;
await CheckAsync();
};
}
/// <summary>Nieuwste versie die nieuwer is dan deze exe; null als er niets nieuws is.</summary>
public ReleaseInfo? Available { get; private set; }
public bool IsUpdateAvailable => Available != null;
/// <summary>Wordt op de UI-thread afgevuurd als <see cref="Available"/> verandert.</summary>
public event EventHandler? AvailabilityChanged;
public void Apply(bool enabled)
{
if (enabled == _enabled)
{
return;
}
_enabled = enabled;
_timer.Stop();
if (enabled)
{
_timer.Interval = FirstCheckDelay;
_timer.Start();
}
else
{
Report(null);
}
}
/// <summary>Resultaat van een controle elders (de tab Update), zodat de melding gelijkloopt.</summary>
public void Report(ReleaseInfo? newest)
{
if (newest != null && newest.Version <= UpdateChecker.CurrentVersion)
{
newest = null;
}
bool changed = Available?.Version != newest?.Version;
Available = newest;
if (changed)
{
AvailabilityChanged?.Invoke(this, EventArgs.Empty);
}
}
private async Task CheckAsync()
{
if (_checking)
{
return;
}
_checking = true;
try
{
IReadOnlyList<ReleaseInfo> releases = await UpdateChecker.GetReleasesAsync();
if (_enabled)
{
Report(releases.FirstOrDefault(r => r.Version > UpdateChecker.CurrentVersion));
}
}
catch (UpdateException)
{
// Een mislukte achtergrondcontrole is geen melding waard; de volgende poging komt vanzelf.
}
finally
{
_checking = false;
}
}
}
+2 -2
View File
@@ -10,8 +10,8 @@
$ErrorActionPreference = "Stop" $ErrorActionPreference = "Stop"
$knownNames = @("Kniip", "KnipPlus") $knownNames = @("Kniip", "KniipTest", "KnipPlus")
$exePattern = '(?i)(^|[\\/"])(Kniip|KnipPlus)\.exe(\b|")' $exePattern = '(?i)(^|[\\/"])(Kniip(Test)?(-[\d.]+)?|KnipPlus)\.exe(\b|")'
function Test-KniipCommand([string]$text) { function Test-KniipCommand([string]$text) {
return [bool]($text -match $exePattern) return [bool]($text -match $exePattern)