2 Commits
Author SHA1 Message Date
rogier 534262a4c7 2.1.0 2026-11-11 17:01:01 +01:00
rogier 561ea57d3e 2.0.0 2026-11-10 23:54:21 +01:00
23 changed files with 1079 additions and 97 deletions

No files matched your search

@@ -0,0 +1,78 @@
---
name: Printknop in editor
overview: Een icoonknop Printen links van het tandwiel in de editorwerkbalk (plus Ctrl+P) die het knipsel met annotaties via het standaard Windows-printvenster afdrukt, passend op de pagina.
todos:
- id: xaml-button
content: Icoonknop Printen (ItemId Print, FlatButton) links van het tandwiel in EditorWindow.xaml
status: completed
- id: toolbar-lists
content: Print invoegen voor Settings in DefaultSplitOrder
status: completed
- id: print-method
content: Print() + Print_Click + Ctrl+P in EditorWindow.xaml.cs (PrintDialog, passend op pagina)
status: completed
- id: docs
content: CHANGELOG-sectie 2.1.0 en README (Delen, build-voorbeeld) bijwerken
status: completed
- id: version
content: Versie naar 2.1.0 in Kniip.csproj, installer/Kniip.iss en build.ps1
status: completed
isProject: false
---
# Printknop in de editor
## Aanpak
Het knipsel wordt al plat gemaakt voor Opslaan en Kopiëren via `DocumentRenderer.Export(_doc)`. Printen gebruikt dezelfde afbeelding en stuurt die naar de WPF-`PrintDialog` (`System.Windows.Controls`, geen extra package nodig).
## Wijzigingen
**[src/Kniip/Editor/EditorWindow.xaml](src/Kniip/Editor/EditorWindow.xaml)**
- Nieuwe icoonknop direct links van het tandwiel: tussen `Sep5` en `SettingsButton` (regel ~495), in dezelfde stijl als Instellingen:
```xml
<Button local:ToolbarPanel.ItemId="Print" local:ToolbarPanel.Title="Printen"
Style="{StaticResource FlatButton}" ToolTip="Printen (Ctrl+P)" Click="Print_Click">
<TextBlock Text="&#xE749;" Style="{StaticResource IconText}" />
</Button>
```
**[src/Kniip/Editor/EditorWindow.xaml.cs](src/Kniip/Editor/EditorWindow.xaml.cs)**
- In `DefaultSplitOrder` (regel 50-55) `"Print"` invoegen tussen `"SplitSpacer2"` en `"Settings"`, zodat de knop in de gesplitste indeling rechtsboven links van het tandwiel staat. Niet toevoegen aan `SplitBottomIds`.
- Geen label, dus niets toevoegen aan `UpdateActionLabels`.
- `Print_Click` naast `Save_Click`, en `case Key.P: Print();` in de Ctrl-sneltoetsen (regel ~2916). Ctrl+P is nog vrij.
- Nieuwe methode `Print()`:
- `CommitTextEdit()`, daarna `DocumentRenderer.Export(_doc)`
- `new PrintDialog()`; als de gebruiker annuleert, gebeurt er niets
- Afbeelding in een `DrawingVisual` tekenen, gecentreerd in het afdrukbare gebied (`PrintableAreaWidth/Height`), met behouden verhouding en alleen verkleind als het knipsel niet past (kleine knipsels worden niet uitvergroot)
- `dialog.PrintVisual(visual, "kniip - knipsel")`, daarna `ShowTemporaryHint("Naar de printer gestuurd.")`
- Fouten tonen via `MessageBox` op dezelfde manier als `WriteTo` ("Printen is mislukt: ...")
Bestaande gebruikers krijgen de knop automatisch te zien: `MergeToolbarOrder` voegt nieuwe ID's in na hun standaardvoorganger (`Sep5` bij één werkbalk, `SplitSpacer2` in de gesplitste indeling), dus direct links van het tandwiel. De knop is te verslepen en te verbergen via het bestaande werkbalkmenu.
**[CHANGELOG.md](CHANGELOG.md)**
- Nieuwe sectie bovenaan, in de stijl van 2.0.0:
```markdown
## 2.1.0 - 2026-11-11
### Nieuw
- **Printen.**\
Druk een knipsel met je aantekeningen af. De knop met het printericoon staat links van het tandwiel, of druk op Ctrl+P. Je kiest de printer en de afdrukstand in het printvenster van Windows. Het knipsel komt in het midden van de pagina; is het te groot, dan wordt het verkleind.
```
**Versienummer naar 2.1.0**
- [src/Kniip/Kniip.csproj](src/Kniip/Kniip.csproj) regel 21: `<Version>2.1.0</Version>`
- [installer/Kniip.iss](installer/Kniip.iss) regel 8: `#define AppVersion "2.1.0"`
- [build.ps1](build.ps1) regel 15: `[string]$Version = "2.1.0",`
**[README.md](README.md)**
- Bij "Delen" (regel 28) printen toevoegen: "Opslaan, kopiëren naar het klembord, printen of uploaden ...".
- Regel 83: `.\build.ps1 # versie 2.1.0`.
- De downloadtabel (regel 37) blijft op 2.0.0 tot de release van 2.1.0 echt op git.hibbit.net staat (stap 4 van "Een nieuwe versie publiceren").
## Niet in scope
- Geen eigen afdrukvoorbeeld of pagina-instellingen; afdrukstand en printer kies je in het Windows-printvenster.
- Geen printknop in het screencastvenster.
+32
View File
@@ -2,6 +2,38 @@
Alle noemenswaardige wijzigingen in kniip staan in dit bestand. De nieuwste versie staat bovenaan.
## 2.1.0 - 2026-11-11
### Nieuw
- **Printen.**\
Druk een knipsel met je aantekeningen af. De knop met het printericoon staat links van het tandwiel, of druk op Ctrl+P. Je kiest de printer en de afdrukstand in het printvenster van Windows. Het knipsel komt in het midden van de pagina; is het te groot, dan wordt het verkleind.
- **Mappen openen.**\
Links van de printknop staat een knop met een mapicoon. Klik erop en kies **Open Screenshot map** of **Open Screencast map**. Je krijgt dezelfde mappen als in het systeemvakmenu: de opslagmap uit de instellingen, anders de map waarin je het laatst hebt opgeslagen.
- Staan de linialen aan, dan zit er ook een mapknop in het hoekje waar de linialen samenkomen. Die opent hetzelfde menu, met **Screenshotmap openen** en **Screencastmap openen**. Rechtsklik op het hoekje toont nog steeds het menu om hulplijnen te wissen of de linialen te verbergen.
- Is er nog geen screencastmap, dan maakt kniip die aan (`Video's\kniip`). Daardoor kun je **Screencasts** in het systeemvakmenu nu ook kiezen voordat je je eerste screencast hebt opgeslagen.
- **Meer opslagopties onder de Opslaan-knop.**\
Rechts in de Opslaan-knop staat een klein pijltje. Klik erop voor een menu met de andere manier van opslaan en **Opslaan als...**. Staat de standaardknop in de instellingen op **Opslaan en kopiëren**, dan kies je hier **Opslaan** zonder te kopiëren; staat hij op **Opslaan**, dan kies je hier **Opslaan en kopiëren**. **Opslaan als...** vraagt altijd waar het bestand moet komen. Het menu staat ook in het screencastvenster, met het gekozen formaat erbij, zoals **Opslaan als MP4**.
## 2.0.0 - 2026-11-10
### Nieuw
- **OCR (tekst herkennen).**\
Lees tekst van het scherm en kopieer hem. Naast de knoppen voor een schermafdruk en een screencast staat nu de knop **OCR**, met hetzelfde icoon als in het Knipprogramma. Klik erop, sleep een rechthoek, en de tekst daarin opent in een apart venster. Daar pas je hem aan en kopieer je alles, of alleen wat je selecteert. De editor gaat tijdens het kiezen even opzij en komt daarna terug.
- Druk in de editor op Ctrl+Shift+T om de tekst van het geopende knipsel te lezen, met je aantekeningen erbij.
- Je vindt OCR ook in het menu van het icoon in het systeemvak, onder **OCR (tekst herkennen)...**. Geef het in de instellingen onder **Sneltoetsen** een eigen sneltoets.
- kniip gebruikt de tekstherkenning van Windows, in de talen van je Windows-account. Vindt Windows geen taal voor tekstherkenning, dan staat in het venster hoe je er een toevoegt.
- **Verloop in elke richting.**\
De richting van een verloop als achtergrond kies je nu met een draaiknop in plaats van uit vier vaste richtingen. Klik op de knop naast de kleuren en sleep de knop rond de cirkel. Je ziet het verloop meteen meedraaien.
- Hij klikt vast op de acht hoofdrichtingen. Houd Shift ingedrukt voor stappen van 15°, of Alt om niet vast te klikken. Je kunt ook de hoek in graden typen, of scrollen boven de knop.
- **Vanuit het midden** staat nu als knop onder de cirkel.
- Elke nieuwe richting is één stap in **Ongedaan maken**, ook als je lang sleept.
### Gewijzigd
- Het blokjespatroon achter transparante delen is lichter en daardoor rustiger om naar te kijken.
## 1.1.9 - 2026-11-10
### Gewijzigd
+3 -3
View File
@@ -25,7 +25,7 @@ kniip is een annotatie-editor die naast het Windows Knipprogramma werkt. Maak ee
- **Knippen tot op de pixel.** Rechthoek of venster, met een vergrootglas voor precieze hoeken en een instelbare vertraging om eerst een menu of tooltip te openen.
- **Aantekenen.** Pijlen en lijnen die je kunt buigen, tekst, vormen, genummerde stappen, markeerstift, emoji en vervaging.
- **Screencasts.** Neem een deel van je scherm op en exporteer als video of GIF.
- **Delen.** Opslaan, kopiëren naar het klembord of uploaden en de link meteen delen.
- **Delen.** Opslaan, kopiëren naar het klembord, printen of uploaden en de link meteen delen.
- **Geschiedenis.** Vind eerdere knipsels en uploads terug, met miniaturen en zoeken.
- **Jouw werkbalk.** Kies een of twee werkbalken, versleep knoppen en verberg wat je niet gebruikt.
- **Altijd bij.** kniip meldt zelf wanneer er een nieuwe versie is en werkt zichzelf bij met een klik.
@@ -34,7 +34,7 @@ kniip is een annotatie-editor die naast het Windows Knipprogramma werkt. Maak ee
| Versie | Installer |
| ------ | --------- |
| 1.1.9 | [Kniip-Setup-1.1.9.exe](https://git.hibbit.net/rogier/kniip/releases/download/1.1.9/Kniip-Setup-1.1.9.exe) |
| 2.0.0 | [Kniip-Setup-2.0.0.exe](https://git.hibbit.net/rogier/kniip/releases/download/2.0.0/Kniip-Setup-2.0.0.exe) |
Oudere versies vind je bij [alle releases](https://git.hibbit.net/rogier/kniip/releases).
@@ -80,7 +80,7 @@ Vereisten:
Bouwen:
```powershell
.\build.ps1 # versie 1.1.9
.\build.ps1 # versie 2.1.0
.\build.ps1 -Version 1.2.0 # andere versie
```
+1 -1
View File
@@ -12,7 +12,7 @@
# .\build.ps1 -SkipTest -> zonder testexe
param(
[string]$Version = "1.1.9",
[string]$Version = "2.1.0",
[string]$Runtime = "win-x64",
[switch]$SkipTest
)
+1 -1
View File
@@ -5,7 +5,7 @@
#define AppName "kniip"
#define AppExe "Kniip.exe"
#ifndef AppVersion
#define AppVersion "1.1.9"
#define AppVersion "2.1.0"
#endif
#ifndef PublishDir
#define PublishDir "..\artifacts\publish"
+72 -17
View File
@@ -21,6 +21,9 @@ namespace Kniip;
public partial class App : Application
{
/// <summary>Waar een rechthoekknipsel heen gaat: naar de editor of naar tekstherkenning.</summary>
private enum SnipTarget { Editor, Ocr }
private SingleInstance? _singleInstance;
private TrayIcon? _tray;
private SettingsWindow? _settingsWindow;
@@ -28,6 +31,8 @@ public partial class App : Application
private readonly List<RegionCaptureWindow> _snipWindows = new();
private List<SnappedWindow> _snipSnapshot = new();
private CountdownWindow? _snipCountdown;
private SnipTarget _snipTarget;
private Action? _snipFinished;
private uint? _ignoredClipboardSequence;
public static App Instance => (App)Current;
@@ -94,6 +99,7 @@ public partial class App : Application
_tray.OpenFileRequested += OpenFileWithDialog;
_tray.SnipRequested += BeginRegionSnip;
_tray.WindowSnipRequested += BeginWindowSnip;
_tray.TextRecognitionRequested += () => BeginTextSnip();
_tray.SetCaptureDelay(Settings.CaptureDelaySeconds);
_tray.CaptureDelayChanged += seconds =>
{
@@ -205,6 +211,11 @@ public partial class App : Application
timer.Tick += (_, _) =>
{
timer.Stop();
if (_snipCountdown == null && _snipWindows.Count == 0)
{
_snipTarget = SnipTarget.Editor;
}
BeginSnip(pickWindow: false);
};
timer.Start();
@@ -216,8 +227,34 @@ public partial class App : Application
/// <summary>Bevriest alle schermen (na de ingestelde vertraging) en laat een venster aanklikken; het resultaat opent in de editor.</summary>
public void BeginWindowSnip() => BeginDelayedSnip(pickWindow: true);
private void BeginDelayedSnip(bool pickWindow)
/// <summary>Als een rechthoekknipsel, maar de herkende tekst opent in een apart venster in plaats van de editor.</summary>
/// <param name="finished">Na het kiezen of annuleren, vóórdat het tekstvenster opent; bijvoorbeeld om de editor terug te zetten.</param>
public void BeginTextSnip(Action? finished = null)
{
if (_snipCountdown != null || _snipWindows.Count > 0 || Recorder.Stage == RecordingStage.Selecting)
{
finished?.Invoke();
return;
}
_snipFinished = finished;
BeginDelayedSnip(pickWindow: false, SnipTarget.Ocr);
}
private void RunSnipFinished()
{
Action? finished = _snipFinished;
_snipFinished = null;
finished?.Invoke();
}
private void BeginDelayedSnip(bool pickWindow, SnipTarget target = SnipTarget.Editor)
{
if (_snipCountdown == null && _snipWindows.Count == 0)
{
_snipTarget = target;
}
if (_snipCountdown != null)
{
return;
@@ -243,7 +280,11 @@ public partial class App : Application
};
timer.Start();
};
countdown.Cancelled += EndCountdown;
countdown.Cancelled += () =>
{
EndCountdown();
RunSnipFinished();
};
_snipCountdown = countdown;
ApplyHotkeys();
countdown.Start();
@@ -326,7 +367,14 @@ public partial class App : Application
_snipWindows.Clear();
List<SnappedWindow> snapshot = _snipSnapshot;
_snipSnapshot = new();
if (image != null)
SnipTarget target = _snipTarget;
_snipTarget = SnipTarget.Editor;
RunSnipFinished();
if (image != null && target == SnipTarget.Ocr)
{
BringToFront(OcrResultWindow.Open(image));
}
else if (image != null)
{
string? windowName = area is { } a ? PhysicalWindow.ProgramNameAt(snapshot, a) : null;
OpenEditor(image, windowName);
@@ -356,6 +404,7 @@ public partial class App : Application
Bind(Settings.RecordHotkey, () => Recorder.Toggle(ScreencastFormat.Mp4));
Bind(Settings.GifHotkey, () => Recorder.Toggle(ScreencastFormat.Gif));
Bind(Settings.ClipboardHotkey, OpenFromClipboard);
Bind(Settings.OcrHotkey, () => BeginTextSnip());
if (_snipCountdown is { } countdown)
{
// Alleen tijdens het aftellen; daarna moet Esc gewoon naar het actieve programma.
@@ -366,7 +415,8 @@ public partial class App : Application
}
_tray?.SetHotkeys(HotkeyDisplay(Settings.SnipHotkey), HotkeyDisplay(Settings.WindowHotkey),
HotkeyDisplay(Settings.RecordHotkey), HotkeyDisplay(Settings.GifHotkey), HotkeyDisplay(Settings.ClipboardHotkey));
HotkeyDisplay(Settings.RecordHotkey), HotkeyDisplay(Settings.GifHotkey), HotkeyDisplay(Settings.ClipboardHotkey),
HotkeyDisplay(Settings.OcrHotkey));
}
/// <summary>Start een screencast: eerst het gebied kiezen, daarna de knoppenbalk.</summary>
@@ -512,20 +562,25 @@ public partial class App : Application
BringToFront(_historyWindow);
}
private IReadOnlyList<(string Label, string? Path)> TrayFolders()
{
static string? Existing(params string?[] folders) =>
folders.FirstOrDefault(folder => folder is { Length: > 0 } && Directory.Exists(folder));
/// <summary>Map met knipsels: de opslagmap, anders de laatst gebruikte map, anders Afbeeldingen.</summary>
public string SnipFolder =>
ExistingFolder(Settings.SnipSaveFolder, Settings.LastSaveFolder)
?? Environment.GetFolderPath(Environment.SpecialFolder.MyPictures);
return new (string, string?)[]
{
("Schermafbeeldingen", Existing(CaptureCoordinator.ScreenshotsFolder)),
("Knipsels", Existing(Settings.SnipSaveFolder, Settings.LastSaveFolder,
Environment.GetFolderPath(Environment.SpecialFolder.MyPictures))),
("Screencasts", Existing(Settings.VideoSaveFolder, Settings.LastVideoFolder,
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyVideos), "kniip")))
};
}
/// <summary>Map met screencasts: de opslagmap, anders de laatst gebruikte map, anders Video's\kniip (bestaat mogelijk nog niet).</summary>
public string VideoFolder =>
ExistingFolder(Settings.VideoSaveFolder, Settings.LastVideoFolder)
?? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyVideos), "kniip");
private static string? ExistingFolder(params string?[] folders) =>
folders.FirstOrDefault(folder => folder is { Length: > 0 } && Directory.Exists(folder));
private IReadOnlyList<(string Label, string? Path)> TrayFolders() => new (string, string?)[]
{
("Schermafbeeldingen", ExistingFolder(CaptureCoordinator.ScreenshotsFolder)),
("Knipsels", SnipFolder),
("Screencasts", VideoFolder)
};
public void ShowBalloon(string title, string text) => _tray?.ShowBalloon(title, text);
+1 -1
View File
@@ -15,7 +15,7 @@ public sealed class AnnotationSurface : FrameworkElement
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 CheckerLight = RenderHelpers.Freeze(new SolidColorBrush(Colors.White));
private static readonly Brush CheckerDark = RenderHelpers.Freeze(new SolidColorBrush(Color.FromRgb(0xD9, 0xD9, 0xD9)));
private static readonly Brush CheckerDark = RenderHelpers.Freeze(new SolidColorBrush(Color.FromRgb(0xEF, 0xEF, 0xEF)));
public static readonly Color GuideColor = Color.FromRgb(0, 180, 255);
private static readonly Brush GuideBrush = RenderHelpers.Freeze(new SolidColorBrush(GuideColor));
+17 -8
View File
@@ -7,21 +7,23 @@ namespace Kniip.Editor;
public enum BackgroundKind { None, Color, Gradient, Image }
public enum GradientDirection { Vertical, Horizontal, Diagonal, Radial }
public enum GradientDirection { Linear, Radial }
/// <summary>
/// Wat er onder het knipsel ligt; alleen zichtbaar waar het knipsel transparant is.
/// De kleuren en afbeelding blijven bewaard bij het wisselen van soort.
/// </summary>
/// <param name="Angle">Richting van een lineair verloop in graden, met de klok mee vanaf boven: 90 = naar rechts, 180 = naar onder.</param>
public sealed record CanvasBackground(
BackgroundKind Kind,
Color Color,
Color Color2,
GradientDirection Direction,
double Angle,
BitmapSource? Image)
{
public static readonly CanvasBackground Default = new(
BackgroundKind.None, Colors.White, Color.FromRgb(0x00, 0x78, 0xD4), GradientDirection.Vertical, null);
BackgroundKind.None, Colors.White, Color.FromRgb(0x00, 0x78, 0xD4), GradientDirection.Linear, 180, null);
public bool IsVisible => Kind switch
{
@@ -51,13 +53,20 @@ public sealed record CanvasBackground(
}
}
private Brush CreateGradient() => Direction switch
private Brush CreateGradient()
{
GradientDirection.Horizontal => new LinearGradientBrush(Color, Color2, new Point(0, 0.5), new Point(1, 0.5)),
GradientDirection.Diagonal => new LinearGradientBrush(Color, Color2, new Point(0, 0), new Point(1, 1)),
GradientDirection.Radial => new RadialGradientBrush(Color, Color2),
_ => new LinearGradientBrush(Color, Color2, new Point(0.5, 0), new Point(0.5, 1))
};
if (Direction == GradientDirection.Radial)
{
return new RadialGradientBrush(Color, Color2);
}
// In de eenheidsruimte van het vlak, zodat 45° van hoek naar hoek loopt; de halve lengte reikt tot in de hoeken.
double radians = Angle * Math.PI / 180;
var direction = new Vector(Math.Sin(radians), -Math.Cos(radians));
double half = (Math.Abs(direction.X) + Math.Abs(direction.Y)) / 2;
var center = new Point(0.5, 0.5);
return new LinearGradientBrush(Color, Color2, center - direction * half, center + direction * half);
}
/// <summary>Afbeelding zo schalen dat ze het hele vlak vult; wat uitsteekt valt weg.</summary>
private static Rect Cover(double imageWidth, double imageHeight, Rect area)
+1 -1
View File
@@ -7,7 +7,7 @@
<DrawingBrush.Drawing>
<DrawingGroup>
<GeometryDrawing Brush="White" Geometry="M0,0 H2 V2 H0 Z" />
<GeometryDrawing Brush="#D4D4D4" Geometry="M0,0 H1 V1 H0 Z M1,1 H2 V2 H1 Z" />
<GeometryDrawing Brush="#E4E4E4" Geometry="M0,0 H1 V1 H0 Z M1,1 H2 V2 H1 Z" />
</DrawingGroup>
</DrawingBrush.Drawing>
</DrawingBrush>
+30 -32
View File
@@ -288,7 +288,7 @@
<DrawingBrush.Drawing>
<DrawingGroup>
<GeometryDrawing Brush="White" Geometry="M0,0 H2 V2 H0 Z" />
<GeometryDrawing Brush="#C8C8C8" Geometry="M0,0 H1 V1 H0 Z M1,1 H2 V2 H1 Z" />
<GeometryDrawing Brush="#E4E4E4" Geometry="M0,0 H1 V1 H0 Z M1,1 H2 V2 H1 Z" />
</DrawingGroup>
</DrawingBrush.Drawing>
</DrawingBrush>
@@ -326,32 +326,6 @@
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
<!-- Richting van het verloop -->
<DataTemplate x:Key="GradientDirectionTemplate">
<StackPanel Orientation="Horizontal">
<Path x:Name="Icon" Data="M9,2.5 V15.5 M5,11.5 L9,15.5 L13,11.5" Stroke="{DynamicResource InkBrush}" StrokeThickness="1.4"
StrokeLineJoin="Round" StrokeStartLineCap="Round" StrokeEndLineCap="Round" Width="18" Height="18" VerticalAlignment="Center" />
<TextBlock x:Name="Label" Text="Van boven naar onder" Margin="8,0,0,0" VerticalAlignment="Center" FontSize="12" />
</StackPanel>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding}" Value="{x:Static local:GradientDirection.Horizontal}">
<Setter TargetName="Icon" Property="Data" Value="M2.5,9 H15.5 M11.5,5 L15.5,9 L11.5,13" />
<Setter TargetName="Label" Property="Text" Value="Van links naar rechts" />
</DataTrigger>
<DataTrigger Binding="{Binding}" Value="{x:Static local:GradientDirection.Diagonal}">
<Setter TargetName="Icon" Property="Data" Value="M3.5,3.5 L14.5,14.5 M8.5,14.5 H14.5 V8.5" />
<Setter TargetName="Label" Property="Text" Value="Diagonaal" />
</DataTrigger>
<DataTrigger Binding="{Binding}" Value="{x:Static local:GradientDirection.Radial}">
<Setter TargetName="Icon" Property="Data" Value="M9,3 A6,6 0 1 1 8.99,3 Z M9,7.5 A1.5,1.5 0 1 1 8.99,7.5 Z" />
<Setter TargetName="Label" Property="Text" Value="Vanuit het midden" />
</DataTrigger>
<DataTrigger Binding="{Binding RelativeSource={RelativeSource AncestorType=ComboBoxItem}}" Value="{x:Null}">
<Setter TargetName="Label" Property="Visibility" Value="Collapsed" />
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</Window.Resources>
<DockPanel>
<!-- Statusbalk -->
@@ -466,6 +440,15 @@
<TextBlock x:Name="ScreencastLabel" Text="Screencast" FontSize="12" VerticalAlignment="Center" Margin="6,0,0,0" />
</StackPanel>
</Button>
<Button x:Name="OcrButton" local:ToolbarPanel.ItemId="OcrSnip" local:ToolbarPanel.Title="OCR"
Style="{StaticResource ProminentButton}" Click="Ocr_Click"
ToolTip="OCR: sleep een rechthoek op het scherm en kopieer de tekst daarin. Ctrl+Shift+T leest de tekst van dit knipsel.">
<StackPanel Orientation="Horizontal">
<Path Style="{StaticResource IconPath}" StrokeThickness="1.2"
Data="M3,7 V5.5 A2.5,2.5 0 0 1 5.5,3 H7 M13,3 H14.5 A2.5,2.5 0 0 1 17,5.5 V7 M17,13 V14.5 A2.5,2.5 0 0 1 14.5,17 H13 M7,17 H5.5 A2.5,2.5 0 0 1 3,14.5 V13 M7,7.5 H13 M7,10 H13 M7,12.5 H10.5" />
<TextBlock x:Name="OcrLabel" Text="OCR" FontSize="12" VerticalAlignment="Center" Margin="6,0,0,0" />
</StackPanel>
</Button>
<!-- Wat tussen de twee spacers staat, komt in het midden van de werkbalk -->
<local:ToolbarSpacer local:ToolbarPanel.ItemId="Spacer2" />
@@ -510,6 +493,14 @@
<TextBlock Text="&#xE8A3;" Style="{StaticResource IconText}" />
</Button>
<local:ToolbarSeparator local:ToolbarPanel.ItemId="Sep5" />
<Button x:Name="FoldersButton" local:ToolbarPanel.ItemId="Folders" local:ToolbarPanel.Title="Mappen openen"
Style="{StaticResource FlatButton}" ToolTip="Screenshot- of screencastmap openen" Click="Folders_Click">
<TextBlock Text="&#xE8B7;" Style="{StaticResource IconText}" />
</Button>
<Button local:ToolbarPanel.ItemId="Print" local:ToolbarPanel.Title="Printen"
Style="{StaticResource FlatButton}" ToolTip="Printen (Ctrl+P)" Click="Print_Click">
<TextBlock Text="&#xE749;" Style="{StaticResource IconText}" />
</Button>
<Button x:Name="SettingsButton" local:ToolbarPanel.ItemId="Settings" local:ToolbarPanel.Title="Instellingen"
Style="{StaticResource FlatButton}" ToolTip="Instellingen" Click="Settings_Click">
<Grid>
@@ -520,10 +511,13 @@
</Grid>
</Button>
<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; het pijltje toont meer opties" Click="Save_Click">
<StackPanel Orientation="Horizontal">
<TextBlock Text="&#xE74E;" Style="{StaticResource IconText}" FontSize="14" />
<TextBlock x:Name="SaveLabel" Text="Opslaan" Margin="6,0,0,0" />
<TextBlock x:Name="SaveMenuChevron" Text="&#xE70D;" Style="{StaticResource IconText}" FontSize="9"
Background="Transparent" Padding="4,2,0,2" Margin="2,0,0,0"
ToolTip="Meer opslagopties" PreviewMouseLeftButtonDown="SaveMenu_MouseDown" />
</StackPanel>
</Button>
<Button x:Name="UploadButton" local:ToolbarPanel.ItemId="Upload" local:ToolbarPanel.Title="Uploaden"
@@ -724,8 +718,7 @@
ToolTip="Achtergrond onder de transparante delen" SelectionChanged="BackgroundKind_Changed" />
<local:ColorPickerButton x:Name="BackgroundColorPicker" ToolTip="Kleur" />
<local:ColorPickerButton x:Name="BackgroundColor2Picker" ToolTip="Tweede kleur van het verloop" />
<ComboBox x:Name="GradientDirectionCombo" Style="{StaticResource MiniCombo}" ItemTemplate="{StaticResource GradientDirectionTemplate}"
ToolTip="Richting van het verloop" SelectionChanged="GradientDirection_Changed" />
<local:GradientAngleDial x:Name="GradientDial" Margin="2,0" />
<Button x:Name="BackgroundImageButton" Style="{StaticResource FlatButton}" Padding="6,4" MinWidth="0" MinHeight="30" Margin="2,0"
BorderBrush="{DynamicResource ControlBorderBrush}" Background="{DynamicResource ControlBrush}"
ToolTip="Afbeelding kiezen; die vult het hele canvas" Click="BackgroundImage_Click">
@@ -766,8 +759,13 @@
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Border x:Name="RulerCorner" Width="20" Height="20" Background="{DynamicResource ToolbarBrush}"
BorderBrush="{DynamicResource DividerBrush}" BorderThickness="0,0,1,1" ToolTip="Rechtsklik om de hulplijnen te wissen"
MouseRightButtonUp="RulerCorner_MouseRightButtonUp" />
BorderBrush="{DynamicResource DividerBrush}" BorderThickness="0,0,1,1"
MouseRightButtonUp="RulerCorner_MouseRightButtonUp">
<Button x:Name="RulerFolderButton" Style="{StaticResource FlatButton}" MinWidth="0" MinHeight="0" Margin="0"
Padding="0" BorderThickness="0" ToolTip="Map openen (rechtsklik: hulplijnen wissen)" Click="RulerFolder_Click">
<TextBlock Text="&#xE8B7;" Style="{StaticResource IconText}" FontSize="12" />
</Button>
</Border>
<local:RulerControl x:Name="HRuler" Grid.Column="1" Height="20" Orientation="Horizontal"
Background="{DynamicResource ToolbarBrush}" ImageBackground="{DynamicResource CardBrush}"
Foreground="{DynamicResource SubtleTextBrush}" BorderBrush="{DynamicResource DividerBrush}"
+164 -24
View File
@@ -46,28 +46,25 @@ public partial class EditorWindow : Window
private static readonly int[] CloudBubblesValues = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 12, 15, 20 };
private static readonly BackgroundKind[] BackgroundKindValues =
{ BackgroundKind.None, BackgroundKind.Color, BackgroundKind.Gradient, BackgroundKind.Image };
private static readonly GradientDirection[] GradientDirectionValues =
{ GradientDirection.Vertical, GradientDirection.Horizontal, GradientDirection.Diagonal, GradientDirection.Radial };
/// <summary>Standaardvolgorde in de gesplitste indeling: eerst de bovenste werkbalk, dan de onderste.</summary>
private static readonly string[] DefaultSplitOrder =
{
"SplitSpacer1", "Select", "Sep1", "Rectangle", "Ellipse", "Shape", "Line", "Arrow", "Text", "Step", "Cursor", "Emoji", "Highlight", "Blur",
"Sep2", "Crop", "Sep3", "Undo", "Redo", "Delete", "Sep4", "Clear", "SplitSpacer2", "Settings",
"New", "Screencast", "Spacer", "Save", "Upload", "Copy", "Sep5", "Spacer2", "Rulers", "ZoomOut", "Zoom", "ZoomIn"
"Sep2", "Crop", "Sep3", "Undo", "Redo", "Delete", "Sep4", "Clear", "SplitSpacer2", "Folders", "Print", "Settings",
"New", "Screencast", "OcrSnip", "Spacer", "Save", "Upload", "Copy", "Sep5", "Spacer2", "Rulers", "ZoomOut", "Zoom", "ZoomIn"
};
/// <summary>Items die in de gesplitste indeling op de onderste werkbalk staan.</summary>
private static readonly HashSet<string> SplitBottomIds = new()
{
"New", "Screencast", "Spacer", "Save", "Upload", "Copy", "Sep5", "Spacer2", "Rulers", "ZoomOut", "Zoom", "ZoomIn"
"New", "Screencast", "OcrSnip", "Spacer", "Save", "Upload", "Copy", "Sep5", "Spacer2", "Rulers", "ZoomOut", "Zoom", "ZoomIn"
};
/// <summary>Items die alleen in de gesplitste indeling op een werkbalk staan.</summary>
private static readonly HashSet<string> SplitOnlyIds = new() { "SplitSpacer1", "SplitSpacer2" };
/// <summary>Items die in de drie-split-indeling in de zijbalk staan in plaats van de onderste werkbalk.</summary>
private static readonly HashSet<string> CaptureBarIds = new() { "New", "Screencast" };
private static readonly HashSet<string> CaptureBarIds = new() { "New", "Screencast", "OcrSnip" };
private readonly EditorDocument _doc;
private readonly UndoStack _undo = new();
@@ -76,6 +73,8 @@ public partial class EditorWindow : Window
private readonly List<string> _defaultToolbarOrder = new();
private readonly List<string> _defaultSplitOrder = new();
private readonly Dictionary<string, UIElement> _toolbarItems = new();
private CanvasBackground? _backgroundBeforePreview;
private bool _recognizing;
private EditorLayout _appliedLayout;
private ToolKind _tool;
@@ -141,7 +140,6 @@ public partial class EditorWindow : Window
CloudBubblesCombo.ItemsSource = CloudBubblesValues;
ShapeKindCombo.ItemsSource = ShapeKindValues;
BackgroundKindCombo.ItemsSource = BackgroundKindValues;
GradientDirectionCombo.ItemsSource = GradientDirectionValues;
UpdateShapeToolIcon();
StrokePicker.ColorChanged += c => ChangeStyle("stroke", s => s.Stroke = c);
@@ -149,6 +147,10 @@ public partial class EditorWindow : Window
ForegroundPicker.ColorChanged += c => ChangeStyle("foreground", s => s.Foreground = c);
BackgroundColorPicker.ColorChanged += c => ChangeBackground(b => b with { Color = c });
BackgroundColor2Picker.ColorChanged += c => ChangeBackground(b => b with { Color2 = c });
GradientDial.AngleChanging += PreviewGradientAngle;
GradientDial.AngleCommitted += angle => CommitGradient(b => b with { Direction = GradientDirection.Linear, Angle = angle });
GradientDial.RadialChanged += radial =>
CommitGradient(b => b with { Direction = radial ? GradientDirection.Radial : GradientDirection.Linear });
EmojiPicker.Recent = settings.RecentEmoji;
EmojiPicker.EmojiPicked += emoji => ChangeStyle("emoji", s => s.Emoji = emoji);
EmojiPicker.SkinToneChanged += tone => ChangeStyle("skintone", s => s.SkinTone = tone);
@@ -460,7 +462,7 @@ public partial class EditorWindow : Window
// Labels alleen tonen als alles op één regel past; staat de werkbalk opzij of zweeft hij, dan altijd alleen iconen.
private void UpdateActionLabels(Border bar, ToolbarPanel panel)
{
List<TextBlock> labels = new[] { NewLabel, ClearLabel, ScreencastLabel, SaveLabel, UploadLabel, CopyLabel }
List<TextBlock> labels = new[] { NewLabel, ClearLabel, ScreencastLabel, SaveLabel, UploadLabel, OcrLabel, CopyLabel }
.Where(label => PanelOf(label) == panel).ToList();
if (labels.Count == 0)
{
@@ -1569,6 +1571,19 @@ public partial class EditorWindow : Window
e.Handled = true;
}
private void RulerFolder_Click(object sender, RoutedEventArgs e)
{
var menu = new ContextMenu { PlacementTarget = RulerFolderButton, Placement = PlacementMode.Bottom };
menu.Items.Add(MenuItem("Screenshotmap openen", "\uE91B", "", () => Kniip.History.HistoryActions.OpenFolder(ScreenshotFolder)));
menu.Items.Add(MenuItem("Screencastmap openen", "\uE714", "", () => Kniip.History.HistoryActions.OpenFolder(App.Instance.VideoFolder)));
menu.IsOpen = true;
}
private string ScreenshotFolder =>
new[] { _settings.SnipSaveFolder, _settings.LastSaveFolder, Kniip.Capture.CaptureCoordinator.ScreenshotsFolder }
.FirstOrDefault(folder => folder is { Length: > 0 } && System.IO.Directory.Exists(folder))
?? Environment.GetFolderPath(Environment.SpecialFolder.MyPictures);
private bool SnapEnabled => _settings.ShowRulers && !Keyboard.Modifiers.HasFlag(ModifierKeys.Alt);
/// <summary>Kleinste verschuiving waarmee een van de waarden op een hulplijn of op de rand of het midden van de afbeelding valt.</summary>
@@ -2094,7 +2109,7 @@ public partial class EditorWindow : Window
BackgroundKind kind = background.Kind;
SetVisible(BackgroundColorPicker, kind is BackgroundKind.Color or BackgroundKind.Gradient);
SetVisible(BackgroundColor2Picker, kind == BackgroundKind.Gradient);
SetVisible(GradientDirectionCombo, kind == BackgroundKind.Gradient);
SetVisible(GradientDial, kind == BackgroundKind.Gradient);
SetVisible(BackgroundImageButton, kind == BackgroundKind.Image);
BackgroundColorPicker.ToolTip = kind == BackgroundKind.Gradient ? "Eerste kleur van het verloop" : "Kleur";
BackgroundImageThumb.Source = background.Image;
@@ -2106,7 +2121,8 @@ public partial class EditorWindow : Window
try
{
BackgroundKindCombo.SelectedItem = kind;
GradientDirectionCombo.SelectedItem = background.Direction;
GradientDial.Angle = background.Angle;
GradientDial.IsRadial = background.Direction == GradientDirection.Radial;
BackgroundColorPicker.SelectedColor = background.Color;
BackgroundColor2Picker.SelectedColor = background.Color2;
}
@@ -2158,12 +2174,26 @@ public partial class EditorWindow : Window
ChangeBackground(b => b with { Kind = kind });
}
private void GradientDirection_Changed(object sender, SelectionChangedEventArgs e)
/// <summary>Live voorbeeld tijdens het slepen van de draaiknop; <see cref="CommitGradient"/> maakt er één stap van.</summary>
private void PreviewGradientAngle(double angle)
{
if (GradientDirectionCombo.SelectedItem is GradientDirection direction)
_backgroundBeforePreview ??= _doc.Background;
_doc.Background = _doc.Background with { Direction = GradientDirection.Linear, Angle = angle };
Surface.InvalidateVisual();
}
private void CommitGradient(Func<CanvasBackground, CanvasBackground> change)
{
if (_backgroundBeforePreview is { } before)
{
ChangeBackground(b => b with { Direction = direction });
_backgroundBeforePreview = null;
CanvasBackground updated = change(_doc.Background);
_doc.Background = before;
ChangeBackground(_ => updated);
return;
}
ChangeBackground(change);
}
private void BackgroundImage_Click(object sender, RoutedEventArgs e)
@@ -2444,8 +2474,8 @@ public partial class EditorWindow : Window
SaveLabel.Text = label;
ToolbarPanel.SetTitle(SaveButton, label);
SaveButton.ToolTip = copy
? "Opslaan en naar het klembord kopiëren (Ctrl+S); Shift+klik of Ctrl+Shift+S = opslaan als"
: "Opslaan (Ctrl+S); Shift+klik of Ctrl+Shift+S = opslaan als";
? "Opslaan en naar het klembord kopiëren (Ctrl+S); Shift+klik of Ctrl+Shift+S = opslaan als; het pijltje toont meer opties"
: "Opslaan (Ctrl+S); Shift+klik of Ctrl+Shift+S = opslaan als; het pijltje toont meer opties";
UpdateToolbarLayout();
}
@@ -2527,13 +2557,53 @@ public partial class EditorWindow : Window
}
}
/// <summary>Tekst van het geopende knipsel herkennen (Ctrl+Shift+T).</summary>
private async void RecognizeText()
{
if (_recognizing)
{
return;
}
CommitTextEdit();
BitmapSource image = DocumentRenderer.Export(_doc);
_recognizing = true;
ShowTemporaryHint("Tekst herkennen...");
try
{
await OcrResultWindow.Open(image, this).Completion;
}
finally
{
_recognizing = false;
}
}
/// <summary>Editor wegzetten en een rechthoek op het scherm laten kiezen; daarna komt de editor terug met het tekstvenster erboven.</summary>
private void Ocr_Click(object sender, RoutedEventArgs e)
{
CommitTextEdit();
CancelDrag();
WindowState previous = WindowState == WindowState.Minimized ? WindowState.Normal : WindowState;
WindowState = WindowState.Minimized;
var timer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(300) };
timer.Tick += (_, _) =>
{
timer.Stop();
App.Instance.BeginTextSnip(() => WindowState = previous);
};
timer.Start();
}
private static string DefaultName => $"Knipsel {DateTime.Now:yyyy-MM-dd HHmmss}";
private bool Save()
private bool Save() => Save(CopiesOnSave);
private bool Save(bool copy)
{
if (_settings.SnipSaveFolder is not { Length: > 0 } target)
{
return SaveAs();
return SaveAs(copy);
}
CommitTextEdit();
@@ -2546,13 +2616,15 @@ public partial class EditorWindow : Window
{
MessageBox.Show(this, $"De opslagmap uit de instellingen is niet bruikbaar:\n{target}\n\n{ex.Message}\n\nKies zelf waar je het knipsel opslaat.",
"kniip", MessageBoxButton.OK, MessageBoxImage.Warning);
return SaveAs();
return SaveAs(copy);
}
return WriteTo(path);
return WriteTo(path, copy);
}
private bool SaveAs()
private bool SaveAs() => SaveAs(CopiesOnSave);
private bool SaveAs(bool copy)
{
CommitTextEdit();
string folder = _settings.LastSaveFolder is { } last && System.IO.Directory.Exists(last)
@@ -2572,7 +2644,7 @@ public partial class EditorWindow : Window
return false;
}
if (!WriteTo(dialog.FileName))
if (!WriteTo(dialog.FileName, copy))
{
return false;
}
@@ -2581,7 +2653,7 @@ public partial class EditorWindow : Window
return true;
}
private bool WriteTo(string path)
private bool WriteTo(string path, bool copy)
{
try
{
@@ -2592,7 +2664,7 @@ public partial class EditorWindow : Window
_dirty = false;
_exported = true;
string name = System.IO.Path.GetFileName(path);
if (CopiesOnSave)
if (copy)
{
ShowTemporaryHint(ClipboardWriter.SetImage(image, path)
? $"Opgeslagen en gekopieerd: {name}."
@@ -2626,6 +2698,68 @@ public partial class EditorWindow : Window
}
}
private void SaveMenu_MouseDown(object sender, MouseButtonEventArgs e)
{
e.Handled = true;
if (!SaveButton.IsEnabled)
{
return;
}
bool copy = CopiesOnSave;
string ellipsis = string.IsNullOrEmpty(_settings.SnipSaveFolder) ? "..." : string.Empty;
var menu = new ContextMenu { PlacementTarget = SaveButton, Placement = PlacementMode.Bottom };
menu.Items.Add(copy
? MenuItem("Opslaan" + ellipsis, "\uE74E", "", () => Save(false))
: MenuItem("Opslaan en kopiëren" + ellipsis, "\uE74E", "", () => Save(true)));
menu.Items.Add(MenuItem("Opslaan als...", "\uE792", "Ctrl+Shift+S", () => SaveAs()));
menu.IsOpen = true;
}
private void Folders_Click(object sender, RoutedEventArgs e)
{
var menu = new ContextMenu { PlacementTarget = FoldersButton, Placement = PlacementMode.Bottom };
menu.Items.Add(MenuItem("Open Screenshot map", "\uE91B", "", () => Kniip.History.HistoryActions.OpenFolder(App.Instance.SnipFolder)));
menu.Items.Add(MenuItem("Open Screencast map", "\uE714", "", () => Kniip.History.HistoryActions.OpenFolder(App.Instance.VideoFolder)));
menu.IsOpen = true;
}
private void Print_Click(object sender, RoutedEventArgs e) => Print();
private void Print()
{
CommitTextEdit();
var dialog = new PrintDialog();
if (dialog.ShowDialog() != true)
{
return;
}
try
{
BitmapSource image = DocumentRenderer.Export(_doc);
double pageWidth = dialog.PrintableAreaWidth;
double pageHeight = dialog.PrintableAreaHeight;
double scale = Math.Min(1, Math.Min(pageWidth / image.Width, pageHeight / image.Height));
double width = image.Width * scale;
double height = image.Height * scale;
var visual = new DrawingVisual();
RenderOptions.SetBitmapScalingMode(visual, BitmapScalingMode.HighQuality);
using (DrawingContext dc = visual.RenderOpen())
{
dc.DrawImage(image, new Rect((pageWidth - width) / 2, (pageHeight - height) / 2, width, height));
}
dialog.PrintVisual(visual, "kniip - knipsel");
ShowTemporaryHint("Naar de printer gestuurd.");
}
catch (Exception ex)
{
MessageBox.Show(this, $"Printen is mislukt:\n\n{ex.Message}", "kniip", MessageBoxButton.OK, MessageBoxImage.Warning);
}
}
private void Undo_Click(object sender, RoutedEventArgs e) => Undo();
private void Redo_Click(object sender, RoutedEventArgs e) => Redo();
@@ -2861,9 +2995,15 @@ public partial class EditorWindow : Window
case Key.S:
Save();
break;
case Key.P:
Print();
break;
case Key.U when CanUpload:
Upload();
break;
case Key.T when shift:
RecognizeText();
break;
case Key.N:
DiscardAndStartNewSnip();
break;
+64
View File
@@ -0,0 +1,64 @@
<UserControl x:Class="Kniip.Editor.GradientAngleDial"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
VerticalAlignment="Center">
<Grid>
<ToggleButton x:Name="Toggle" Style="{StaticResource FlatButton}" Padding="6,4" MinWidth="0" MinHeight="30"
BorderBrush="{DynamicResource ControlBorderBrush}" Background="{DynamicResource ControlBrush}"
MouseWheel="Dial_MouseWheel">
<StackPanel Orientation="Horizontal">
<Grid Width="20" Height="20">
<Ellipse Stroke="{DynamicResource InkBrush}" StrokeThickness="1.2" Margin="1" />
<Path x:Name="SmallArrow" Data="M10,15 V5 M7,8 L10,5 L13,8" Stroke="{DynamicResource InkBrush}" StrokeThickness="1.4"
StrokeLineJoin="Round" StrokeStartLineCap="Round" StrokeEndLineCap="Round" RenderTransformOrigin="0.5,0.5">
<Path.RenderTransform>
<RotateTransform x:Name="SmallRotate" />
</Path.RenderTransform>
</Path>
<Ellipse x:Name="SmallRadial" Width="5" Height="5" Fill="{DynamicResource InkBrush}" Visibility="Collapsed" />
</Grid>
<TextBlock Text="&#xE70D;" Style="{StaticResource IconText}" FontSize="9" Margin="5,0,1,0" Foreground="{DynamicResource SubtleTextBrush}" />
</StackPanel>
</ToggleButton>
<Popup x:Name="PopupPanel" PlacementTarget="{Binding ElementName=Toggle}" Placement="Bottom" StaysOpen="False"
AllowsTransparency="True" PopupAnimation="Fade"
IsOpen="{Binding IsChecked, ElementName=Toggle, Mode=TwoWay}">
<Border Background="{DynamicResource CardBrush}" BorderBrush="{DynamicResource ControlStrongBorderBrush}" BorderThickness="1" CornerRadius="6" Padding="10" Margin="0,4,10,10">
<Border.Effect>
<DropShadowEffect BlurRadius="10" ShadowDepth="2" Opacity="0.18" />
</Border.Effect>
<StackPanel>
<!-- Sleep de knop rond; klik ergens op de cirkel om de richting meteen te kiezen -->
<Canvas x:Name="DialCanvas" Width="140" Height="140" Background="Transparent" Cursor="Hand"
MouseLeftButtonDown="Dial_MouseDown" MouseMove="Dial_MouseMove" MouseLeftButtonUp="Dial_MouseUp"
LostMouseCapture="Dial_LostMouseCapture" MouseWheel="Dial_MouseWheel">
<Ellipse x:Name="Track" Canvas.Left="20" Canvas.Top="20" Width="100" Height="100"
Stroke="{DynamicResource ControlStrongBorderBrush}" StrokeThickness="2" />
<Canvas x:Name="Ticks" IsHitTestVisible="False" />
<Line x:Name="Needle" X1="70" Y1="70" Stroke="{DynamicResource AccentBrush}" StrokeThickness="2"
StrokeStartLineCap="Round" StrokeEndLineCap="Round" IsHitTestVisible="False" />
<Ellipse Canvas.Left="66" Canvas.Top="66" Width="8" Height="8" Fill="{DynamicResource AccentBrush}" IsHitTestVisible="False" />
<Ellipse x:Name="Knob" Width="16" Height="16" Fill="White" Stroke="{DynamicResource AccentBrush}" StrokeThickness="2"
IsHitTestVisible="False" />
</Canvas>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,8,0,0">
<TextBox x:Name="AngleBox" Width="48" VerticalContentAlignment="Center" HorizontalContentAlignment="Right" FontSize="12"
Padding="2,3" KeyDown="AngleBox_KeyDown" LostKeyboardFocus="AngleBox_LostKeyboardFocus"
ToolTip="Hoek in graden: 0 = naar boven, 90 = naar rechts, 180 = naar onder" />
<TextBlock Text="°" FontSize="12" VerticalAlignment="Center" Margin="3,0,0,0" Foreground="{DynamicResource InkBrush}" />
</StackPanel>
<ToggleButton x:Name="RadialToggle" Style="{StaticResource ToolToggle}" Margin="0,8,0,0" HorizontalAlignment="Stretch"
Click="Radial_Click" ToolTip="Verloop vanuit het midden naar buiten">
<StackPanel Orientation="Horizontal">
<Path Data="M9,3 A6,6 0 1 1 8.99,3 Z M9,7.5 A1.5,1.5 0 1 1 8.99,7.5 Z" Stroke="{DynamicResource InkBrush}" StrokeThickness="1.4"
Width="18" Height="18" VerticalAlignment="Center" />
<TextBlock Text="Vanuit het midden" FontSize="12" VerticalAlignment="Center" Margin="6,0,0,0" />
</StackPanel>
</ToggleButton>
<TextBlock Text="Shift = stappen van 15°, Alt = niet vastklikken" FontSize="11" Margin="0,6,0,0" HorizontalAlignment="Center"
Foreground="{DynamicResource SubtleTextBrush}" />
</StackPanel>
</Border>
</Popup>
</Grid>
</UserControl>
+250
View File
@@ -0,0 +1,250 @@
using System;
using System.Globalization;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Shapes;
namespace Kniip.Editor;
/// <summary>
/// Richting van een verloop: een cirkel met een knop die je rondsleept, plus een hoekveld en "vanuit het midden".
/// Hoeken in graden met de klok mee vanaf boven, zoals <see cref="CanvasBackground.Angle"/>.
/// </summary>
public partial class GradientAngleDial : UserControl
{
private const double Center = 70;
private const double Radius = 50;
private double _angle = 180;
private bool _isRadial;
private bool _dragging;
/// <summary>Tijdens het slepen, voor een live voorbeeld; nog geen stap in Ongedaan maken.</summary>
public event Action<double>? AngleChanging;
/// <summary>Definitieve hoek: bij loslaten, scrollen of typen. Kiest ook een lineair verloop.</summary>
public event Action<double>? AngleCommitted;
/// <summary>"Vanuit het midden" aan- of uitgezet.</summary>
public event Action<bool>? RadialChanged;
public GradientAngleDial()
{
InitializeComponent();
BuildTicks();
UpdateVisuals();
}
public double Angle
{
get => _angle;
set
{
_angle = Normalize(value);
UpdateVisuals();
}
}
public bool IsRadial
{
get => _isRadial;
set
{
_isRadial = value;
UpdateVisuals();
}
}
private static double Normalize(double angle)
{
angle %= 360;
if (angle < 0)
{
angle += 360;
}
return Math.Round(angle, 1) >= 360 ? 0 : Math.Round(angle, 1);
}
private static Vector Direction(double angle)
{
double radians = angle * Math.PI / 180;
return new Vector(Math.Sin(radians), -Math.Cos(radians));
}
private void BuildTicks()
{
for (int i = 0; i < 8; i++)
{
Vector d = Direction(i * 45);
double inner = Radius + 4, outer = Radius + (i % 2 == 0 ? 11 : 8);
Ticks.Children.Add(new Line
{
X1 = Center + d.X * inner,
Y1 = Center + d.Y * inner,
X2 = Center + d.X * outer,
Y2 = Center + d.Y * outer,
StrokeThickness = 1.5,
StrokeStartLineCap = PenLineCap.Round,
StrokeEndLineCap = PenLineCap.Round
});
((Line)Ticks.Children[i]).SetResourceReference(Shape.StrokeProperty, "SubtleTextBrush");
}
}
private void UpdateVisuals()
{
Vector d = Direction(_angle);
Point knob = new Point(Center, Center) + d * Radius;
Canvas.SetLeft(Knob, knob.X - Knob.Width / 2);
Canvas.SetTop(Knob, knob.Y - Knob.Height / 2);
Needle.X2 = knob.X;
Needle.Y2 = knob.Y;
DialCanvas.Opacity = _isRadial ? 0.45 : 1;
SmallRotate.Angle = _angle;
SmallArrow.Visibility = _isRadial ? Visibility.Collapsed : Visibility.Visible;
SmallRadial.Visibility = _isRadial ? Visibility.Visible : Visibility.Collapsed;
RadialToggle.IsChecked = _isRadial;
if (!AngleBox.IsKeyboardFocusWithin)
{
AngleBox.Text = _angle.ToString("0.#", CultureInfo.CurrentCulture);
}
Toggle.ToolTip = _isRadial
? "Richting van het verloop: vanuit het midden"
: $"Richting van het verloop: {_angle.ToString("0.#", CultureInfo.CurrentCulture)}° (scroll om te draaien)";
}
private double AngleAt(Point p)
{
Vector v = p - new Point(Center, Center);
if (v.Length < 1)
{
return _angle;
}
double angle = Normalize(Math.Atan2(v.X, -v.Y) * 180 / Math.PI);
ModifierKeys modifiers = Keyboard.Modifiers;
if (modifiers.HasFlag(ModifierKeys.Alt))
{
return Normalize(Math.Round(angle));
}
if (modifiers.HasFlag(ModifierKeys.Shift))
{
return Normalize(Math.Round(angle / 15) * 15);
}
double nearest = Math.Round(angle / 45) * 45;
return Normalize(Math.Abs(angle - nearest) <= 6 ? nearest : Math.Round(angle));
}
private void Dial_MouseDown(object sender, MouseButtonEventArgs e)
{
double angle = AngleAt(e.GetPosition(DialCanvas));
_dragging = DialCanvas.CaptureMouse();
if (_dragging)
{
Preview(angle);
}
else
{
Commit(angle);
}
e.Handled = true;
}
private void Dial_MouseMove(object sender, MouseEventArgs e)
{
if (_dragging)
{
Preview(AngleAt(e.GetPosition(DialCanvas)));
}
}
private void Dial_MouseUp(object sender, MouseButtonEventArgs e)
{
if (_dragging)
{
DialCanvas.ReleaseMouseCapture();
}
}
private void Dial_LostMouseCapture(object sender, MouseEventArgs e)
{
if (!_dragging)
{
return;
}
_dragging = false;
Commit(_angle);
}
private void Dial_MouseWheel(object sender, MouseWheelEventArgs e)
{
double step = Keyboard.Modifiers.HasFlag(ModifierKeys.Alt) ? 1 : 15;
double angle = e.Delta > 0 ? _angle + step : _angle - step;
Commit(step == 15 ? Math.Round(angle / 15) * 15 : angle);
e.Handled = true;
}
private void Preview(double angle)
{
_isRadial = false;
Angle = angle;
AngleChanging?.Invoke(_angle);
}
private void Commit(double angle)
{
_isRadial = false;
Angle = angle;
AngleCommitted?.Invoke(_angle);
}
private void AngleBox_KeyDown(object sender, KeyEventArgs e)
{
if (e.Key == Key.Enter)
{
ApplyTypedAngle();
AngleBox.SelectAll();
e.Handled = true;
}
else if (e.Key == Key.Escape)
{
AngleBox.Text = _angle.ToString("0.#", CultureInfo.CurrentCulture);
Toggle.IsChecked = false;
e.Handled = true;
}
}
private void AngleBox_LostKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e) => ApplyTypedAngle();
private void ApplyTypedAngle()
{
string text = AngleBox.Text.Trim().TrimEnd('°');
if (double.TryParse(text, NumberStyles.Float, CultureInfo.CurrentCulture, out double angle)
|| double.TryParse(text, NumberStyles.Float, CultureInfo.InvariantCulture, out angle))
{
if (Normalize(angle) != _angle || _isRadial)
{
Commit(angle);
return;
}
}
AngleBox.Text = _angle.ToString("0.#", CultureInfo.CurrentCulture);
}
private void Radial_Click(object sender, RoutedEventArgs e)
{
_isRadial = RadialToggle.IsChecked == true;
UpdateVisuals();
RadialChanged?.Invoke(_isRadial);
}
}
+31
View File
@@ -0,0 +1,31 @@
<Window x:Class="Kniip.Editor.OcrResultWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="kniip tekst herkennen" Width="560" Height="420" MinWidth="360" MinHeight="240"
WindowStartupLocation="CenterScreen" Background="{DynamicResource WindowBrush}" Foreground="{DynamicResource InkBrush}"
FontFamily="Segoe UI" FontSize="13" Icon="pack://application:,,,/AppIcon.ico">
<DockPanel Margin="14">
<DockPanel DockPanel.Dock="Bottom" Margin="0,12,0,0" LastChildFill="False">
<TextBlock x:Name="StatusText" DockPanel.Dock="Left" VerticalAlignment="Center" FontSize="12"
Foreground="{DynamicResource SubtleTextBrush}" TextTrimming="CharacterEllipsis" />
<Button DockPanel.Dock="Right" Style="{StaticResource SecondaryButton}" Content="Sluiten" IsCancel="True" Click="Close_Click" />
<Button x:Name="CopyButton" DockPanel.Dock="Right" Style="{StaticResource AccentButton}" ToolTip="Alles kopiëren, of alleen de geselecteerde tekst"
Click="Copy_Click">
<StackPanel Orientation="Horizontal">
<TextBlock Text="&#xE8C8;" Style="{StaticResource IconText}" FontSize="14" Foreground="White" />
<TextBlock x:Name="CopyLabel" Text="Kopiëren" Margin="6,0,0,0" />
</StackPanel>
</Button>
</DockPanel>
<Grid>
<TextBox x:Name="ResultBox" AcceptsReturn="True" AcceptsTab="True" TextWrapping="Wrap" Padding="6"
VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled"
FontFamily="Segoe UI" FontSize="13" SelectionChanged="ResultBox_SelectionChanged" />
<StackPanel x:Name="EmptyPanel" HorizontalAlignment="Center" VerticalAlignment="Center" Visibility="Collapsed" IsHitTestVisible="False">
<TextBlock Text="&#xE8D2;" Style="{StaticResource IconText}" FontSize="28" Foreground="{DynamicResource SubtleTextBrush}" />
<TextBlock x:Name="EmptyText" Text="Geen tekst gevonden in deze afbeelding." Margin="0,8,0,0" TextWrapping="Wrap"
TextAlignment="Center" MaxWidth="380" Foreground="{DynamicResource SubtleTextBrush}" />
</StackPanel>
</Grid>
</DockPanel>
</Window>
+110
View File
@@ -0,0 +1,110 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Media.Imaging;
namespace Kniip.Editor;
/// <summary>Toont de herkende tekst van een afbeelding, om aan te passen en te kopiëren.</summary>
public partial class OcrResultWindow : Window
{
private OcrResultWindow()
{
InitializeComponent();
Infrastructure.ThemeManager.Attach(this);
}
/// <summary>Klaar zodra de tekst herkend is of het herkennen is mislukt.</summary>
public Task Completion { get; private set; } = Task.CompletedTask;
/// <summary>Opent het venster meteen en vult het zodra de tekst herkend is.</summary>
public static OcrResultWindow Open(BitmapSource image, Window? owner = null)
{
var window = new OcrResultWindow();
if (owner is { IsVisible: true })
{
window.Owner = owner;
window.WindowStartupLocation = WindowStartupLocation.CenterOwner;
}
window.SetBusy(true);
window.Show();
window.Activate();
window.Completion = window.RecognizeAsync(image);
return window;
}
private async Task RecognizeAsync(BitmapSource image)
{
try
{
ShowText(await OcrService.RecognizeAsync(image));
}
catch (Exception ex)
{
ShowError(ex is InvalidOperationException ? ex.Message : $"Tekst herkennen is mislukt.\n\n{ex.Message}");
}
finally
{
SetBusy(false);
}
}
private void SetBusy(bool busy)
{
ResultBox.IsReadOnly = busy;
CopyButton.IsEnabled = !busy && ResultBox.Text.Length > 0;
if (busy)
{
StatusText.Text = "Tekst herkennen...";
}
}
private void ShowText(string text)
{
ResultBox.Text = text;
bool empty = text.Trim().Length == 0;
EmptyPanel.Visibility = empty ? Visibility.Visible : Visibility.Collapsed;
int lines = text.Split('\n').Count(line => line.Trim().Length > 0);
StatusText.Text = empty ? string.Empty
: $"{lines} {(lines == 1 ? "regel" : "regels")} herkend. Je kunt de tekst hier aanpassen.";
if (!empty)
{
ResultBox.Focus();
ResultBox.SelectAll();
}
}
private void ShowError(string message)
{
ResultBox.Text = string.Empty;
ResultBox.IsEnabled = false;
EmptyText.Text = message;
EmptyPanel.Visibility = Visibility.Visible;
StatusText.Text = string.Empty;
}
private void ResultBox_SelectionChanged(object sender, RoutedEventArgs e)
{
if (!IsLoaded)
{
return;
}
CopyLabel.Text = ResultBox.SelectionLength > 0 && ResultBox.SelectionLength < ResultBox.Text.Length
? "Selectie kopiëren"
: "Kopiëren";
CopyButton.IsEnabled = !ResultBox.IsReadOnly && ResultBox.Text.Length > 0;
}
private void Copy_Click(object sender, RoutedEventArgs e)
{
string text = ResultBox.SelectionLength > 0 ? ResultBox.SelectedText : ResultBox.Text;
StatusText.Text = ClipboardWriter.SetText(text)
? "Gekopieerd naar het klembord."
: "Kopiëren mislukt: het klembord is bezet. Probeer het opnieuw.";
}
private void Close_Click(object sender, RoutedEventArgs e) => Close();
}
+162
View File
@@ -0,0 +1,162 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using Windows.Globalization;
using Windows.Media.Ocr;
using BitmapAlphaMode = Windows.Graphics.Imaging.BitmapAlphaMode;
using BitmapPixelFormat = Windows.Graphics.Imaging.BitmapPixelFormat;
using SoftwareBitmap = Windows.Graphics.Imaging.SoftwareBitmap;
using WinBitmapDecoder = Windows.Graphics.Imaging.BitmapDecoder;
namespace Kniip.Editor;
/// <summary>Tekst herkennen met de OCR die in Windows zit; de taal volgt de talen van het gebruikersprofiel.</summary>
internal static class OcrService
{
/// <summary>Kleinere afbeeldingen worden vergroot: kleine schermtekst mist de OCR anders vaak.</summary>
private const int UpscaleBelowPixels = 2_000_000;
/// <summary>Herkende regels, gescheiden door regeleinden; leeg als er geen tekst is gevonden.</summary>
/// <exception cref="InvalidOperationException">Er is geen taal voor tekstherkenning geïnstalleerd.</exception>
public static async Task<string> RecognizeAsync(BitmapSource image)
{
OcrEngine engine = CreateEngine();
using SoftwareBitmap bitmap = await ToSoftwareBitmapAsync(Prepare(image));
OcrResult result = await engine.RecognizeAsync(bitmap);
return Layout(result);
}
/// <summary>
/// Regels opnieuw opbouwen uit de plek van elk woord. Windows geeft de tekst per blok terug; bij afwisselende
/// kleuren of opmaak valt één regel dan in stukken die ver uit elkaar in het resultaat staan.
/// </summary>
private static string Layout(OcrResult result)
{
var words = result.Lines
.SelectMany(line => line.Words)
.Select(word => (word.Text, Box: new Rect(word.BoundingRect.X, word.BoundingRect.Y, word.BoundingRect.Width, word.BoundingRect.Height)))
.Where(word => word.Text.Length > 0 && word.Box.Height > 0)
.OrderBy(word => word.Box.Top + word.Box.Height / 2)
.ToList();
var rows = new List<Row>();
foreach ((string text, Rect box) in words)
{
double center = box.Top + box.Height / 2;
Row? row = rows.LastOrDefault(r => center >= r.Top && center <= r.Bottom
&& Math.Abs(r.Center - center) <= Math.Min(r.Height, box.Height) * 0.6);
if (row == null)
{
row = new Row();
rows.Add(row);
}
row.Add(text, box);
}
rows.Sort((a, b) => a.Center.CompareTo(b.Center));
double lineHeight = rows.Count == 0 ? 0 : rows.Select(r => r.Height).OrderBy(h => h).ElementAt(rows.Count / 2);
var output = new StringBuilder();
for (int i = 0; i < rows.Count; i++)
{
if (i > 0)
{
output.AppendLine();
// Een duidelijk grotere witruimte dan een gewone regelafstand: een lege regel tussen alinea's.
if (rows[i].Top - rows[i - 1].Bottom > lineHeight * 0.9)
{
output.AppendLine();
}
}
output.Append(rows[i].Text);
}
return output.ToString();
}
private sealed class Row
{
private readonly List<(string Text, Rect Box)> _words = new();
public double Top { get; private set; } = double.MaxValue;
public double Bottom { get; private set; } = double.MinValue;
public double Height => Bottom - Top;
public double Center => (Top + Bottom) / 2;
public void Add(string text, Rect box)
{
_words.Add((text, box));
Top = Math.Min(Top, box.Top);
Bottom = Math.Max(Bottom, box.Bottom);
}
public string Text => string.Join(" ", _words.OrderBy(w => w.Box.Left).Select(w => w.Text));
}
private static OcrEngine CreateEngine()
{
if (OcrEngine.TryCreateFromUserProfileLanguages() is { } engine)
{
return engine;
}
var english = new Language("en-US");
if (OcrEngine.IsLanguageSupported(english) && OcrEngine.TryCreateFromLanguage(english) is { } fallback)
{
return fallback;
}
throw new InvalidOperationException(
"Windows heeft geen taal voor tekstherkenning. Voeg er een toe via Instellingen > Tijd en taal > Taal en regio: " +
"kies een taal en zet bij de taalopties Optische tekenherkenning aan.");
}
/// <summary>Op wit leggen (transparant leest de OCR als zwart) en zo nodig vergroten of verkleinen.</summary>
private static BitmapSource Prepare(BitmapSource image)
{
int width = image.PixelWidth, height = image.PixelHeight;
long pixels = (long)width * height;
double scale = pixels < UpscaleBelowPixels / 4 ? 3 : pixels < UpscaleBelowPixels ? 2 : 1;
double max = OcrEngine.MaxImageDimension;
scale = Math.Min(scale, Math.Min(max / width, max / height));
int w = Math.Max(1, (int)Math.Round(width * scale));
int h = Math.Max(1, (int)Math.Round(height * scale));
var visual = new DrawingVisual();
using (DrawingContext dc = visual.RenderOpen())
{
var area = new Rect(0, 0, w, h);
dc.DrawRectangle(Brushes.White, null, area);
dc.DrawImage(image, area);
}
RenderOptions.SetBitmapScalingMode(visual, BitmapScalingMode.HighQuality);
var target = new RenderTargetBitmap(w, h, 96, 96, PixelFormats.Pbgra32);
target.Render(visual);
target.Freeze();
return target;
}
private static async Task<SoftwareBitmap> ToSoftwareBitmapAsync(BitmapSource image)
{
var encoder = new PngBitmapEncoder();
encoder.Frames.Add(BitmapFrame.Create(image));
using var png = new MemoryStream();
encoder.Save(png);
png.Position = 0;
using var stream = png.AsRandomAccessStream();
WinBitmapDecoder decoder = await WinBitmapDecoder.CreateAsync(stream);
return await decoder.GetSoftwareBitmapAsync(BitmapPixelFormat.Bgra8, BitmapAlphaMode.Premultiplied);
}
}
+9 -1
View File
@@ -17,6 +17,7 @@ internal sealed class TrayIcon : IDisposable
private readonly ToolStripMenuItem _pauseItem;
private readonly ToolStripMenuItem _snipItem;
private readonly ToolStripMenuItem _windowItem;
private readonly ToolStripMenuItem _ocrItem;
private readonly ToolStripMenuItem _recordItem;
private readonly ToolStripMenuItem _gifItem;
private readonly ToolStripMenuItem _foldersItem;
@@ -35,6 +36,7 @@ internal sealed class TrayIcon : IDisposable
public event Action? OpenFromClipboardRequested;
public event Action? SnipRequested;
public event Action? WindowSnipRequested;
public event Action? TextRecognitionRequested;
public event Action? RecordRequested;
public event Action? GifRequested;
public event Action? OpenFileRequested;
@@ -62,6 +64,11 @@ internal sealed class TrayIcon : IDisposable
menu.Items.Add(_snipItem);
_windowItem = new ToolStripMenuItem("Venster knippen", null, (_, _) => WindowSnipRequested?.Invoke());
menu.Items.Add(_windowItem);
_ocrItem = new ToolStripMenuItem("OCR (tekst herkennen)...", null, (_, _) => TextRecognitionRequested?.Invoke())
{
ToolTipText = "Sleep een rechthoek; de tekst daarin opent in een apart venster om te kopiëren"
};
menu.Items.Add(_ocrItem);
_delayItem = BuildDelayItem();
menu.Items.Add(_delayItem);
_recordItem = new ToolStripMenuItem(RecordText, null, (_, _) => RecordRequested?.Invoke());
@@ -108,9 +115,10 @@ internal sealed class TrayIcon : IDisposable
public bool IsPaused => _pauseItem.Checked;
/// <summary>Toont de sneltoetsen naast de menu-items; null = geen sneltoets.</summary>
public void SetHotkeys(string? snip, string? window, string? record, string? gif, string? clipboard)
public void SetHotkeys(string? snip, string? window, string? record, string? gif, string? clipboard, string? ocr)
{
_clipboardItem.ShortcutKeyDisplayString = clipboard;
_ocrItem.ShortcutKeyDisplayString = ocr;
_snipItem.ShortcutKeyDisplayString = snip;
_windowItem.ShortcutKeyDisplayString = window;
_recordItem.ShortcutKeyDisplayString = record;
+1 -1
View File
@@ -18,7 +18,7 @@
<ApplicationManifest>app.manifest</ApplicationManifest>
<Product>kniip</Product>
<Description>Annotatie-editor naast het Windows Knipprogramma</Description>
<Version>1.1.9</Version>
<Version>2.1.0</Version>
<SatelliteResourceLanguages>nl;en</SatelliteResourceLanguages>
<!-- WPF regelt DPI via app.manifest; de WinForms-analyzer is hier niet van toepassing. -->
<NoWarn>$(NoWarn);WFAC010</NoWarn>
@@ -111,6 +111,9 @@
<StackPanel Orientation="Horizontal">
<TextBlock Text="&#xE74E;" Style="{StaticResource IconText}" FontSize="14" Margin="0,0,6,0" />
<TextBlock x:Name="SaveText" Text="Opslaan als MP4..." VerticalAlignment="Center" />
<TextBlock Text="&#xE70D;" Style="{StaticResource IconText}" FontSize="9"
Background="Transparent" Padding="4,2,0,2" Margin="6,0,0,0"
ToolTip="Meer opslagopties" PreviewMouseLeftButtonDown="SaveMenu_MouseDown" />
</StackPanel>
</Button>
</StackPanel>
+34 -6
View File
@@ -8,6 +8,7 @@ using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Input;
using System.Windows.Threading;
using Kniip.Editor;
@@ -346,13 +347,13 @@ public partial class ScreencastWindow : Window
Mp4Toggle.IsChecked = format == ScreencastFormat.Mp4;
GifToggle.IsChecked = format == ScreencastFormat.Gif;
GifOptions.Visibility = format == ScreencastFormat.Gif ? Visibility.Visible : Visibility.Collapsed;
bool copy = _settings.SaveButton == SaveButtonChoice.SaveAndCopy;
bool copy = CopiesOnSave;
string ellipsis = string.IsNullOrEmpty(_settings.VideoSaveFolder) ? "..." : string.Empty;
string action = copy ? "Opslaan en kopiëren als " : "Opslaan als ";
SaveText.Text = action + (format == ScreencastFormat.Gif ? "GIF" : "MP4") + ellipsis;
SaveButton.ToolTip = copy
? "Slaat op en kopieert het bestand naar het klembord. Shift+klik = altijd vragen waar je opslaat."
: "Shift+klik = altijd vragen waar je opslaat";
? "Slaat op en kopieert het bestand naar het klembord. Shift+klik = altijd vragen waar je opslaat; het pijltje toont meer opties."
: "Shift+klik = altijd vragen waar je opslaat; het pijltje toont meer opties";
UploadButton.Visibility = _settings.Uploader is { IsConfigured: true } ? Visibility.Visible : Visibility.Collapsed;
UpdateInfo();
}
@@ -395,7 +396,36 @@ public partial class ScreencastWindow : Window
private static string DefaultFolder =>
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyVideos), "kniip");
private async void Save_Click(object sender, RoutedEventArgs e)
private bool CopiesOnSave => _settings.SaveButton == SaveButtonChoice.SaveAndCopy;
private async void Save_Click(object sender, RoutedEventArgs e) =>
await SaveAsync(Keyboard.Modifiers.HasFlag(ModifierKeys.Shift), CopiesOnSave);
private void SaveMenu_MouseDown(object sender, MouseButtonEventArgs e)
{
e.Handled = true;
if (IsBusy || !SaveButton.IsEnabled)
{
return;
}
bool copy = CopiesOnSave;
string format = Format == ScreencastFormat.Gif ? "GIF" : "MP4";
string ellipsis = string.IsNullOrEmpty(_settings.VideoSaveFolder) ? "..." : string.Empty;
var menu = new ContextMenu { PlacementTarget = SaveButton, Placement = PlacementMode.Bottom };
menu.Items.Add(SaveMenuItem((copy ? "Opslaan als " : "Opslaan en kopiëren als ") + format + ellipsis, false, !copy));
menu.Items.Add(SaveMenuItem("Opslaan als...", true, copy));
menu.IsOpen = true;
}
private MenuItem SaveMenuItem(string header, bool ask, bool copy)
{
var item = new MenuItem { Header = header };
item.Click += async (_, _) => await SaveAsync(ask, copy);
return item;
}
private async Task SaveAsync(bool ask, bool copy)
{
if (IsBusy)
{
@@ -403,7 +433,6 @@ public partial class ScreencastWindow : Window
}
SetPlaying(false);
bool ask = Keyboard.Modifiers.HasFlag(ModifierKeys.Shift);
string? target = ask ? null : FixedTarget();
if (target is null)
{
@@ -421,7 +450,6 @@ public partial class ScreencastWindow : Window
_kept = true;
RememberChoices();
AddToHistory(target, null, target);
bool copy = _settings.SaveButton == SaveButtonChoice.SaveAndCopy;
bool copied = !copy || ClipboardWriter.SetFile(target);
App.Instance.ShowBalloon(copy && copied ? "Screencast opgeslagen en gekopieerd" : "Screencast opgeslagen", target);
if (!copied)
+3
View File
@@ -88,6 +88,9 @@ public sealed class AppSettings
/// <summary>Sneltoets om de afbeelding op het klembord in de editor te openen; standaard geen.</summary>
public string ClipboardHotkey { get; set; } = "";
/// <summary>Sneltoets om een rechthoek te kiezen en de tekst erin te herkennen; standaard geen.</summary>
public string OcrHotkey { get; set; } = "";
public bool RecordSystemAudio { get; set; } = true;
public bool RecordMicrophone { get; set; }
public bool RecordShowCursor { get; set; } = true;
+8 -1
View File
@@ -131,13 +131,20 @@
<Button Tag="Gif" Style="{StaticResource FlatButton}" Content="Geen" Click="ClearHotkey_Click" Margin="6,0,0,0"
ToolTip="Geen sneltoets gebruiken" />
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,0,0,6">
<StackPanel Orientation="Horizontal" Margin="0,0,0,4">
<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>
<StackPanel Orientation="Horizontal" Margin="0,0,0,6">
<TextBlock Text="OCR (tekst herkennen)" VerticalAlignment="Center" Width="150" />
<Button x:Name="OcrHotkeyButton" Tag="Ocr" Style="{StaticResource SecondaryButton}" Width="170" Margin="0"
Click="HotkeyButton_Click" ToolTip="Klik en druk daarna de gewenste toetscombinatie in; sleep daarna een rechthoek om de tekst erin te lezen" />
<Button Tag="Ocr" 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 Text="Gebied kiezen" Style="{StaticResource CardTitle}" Margin="0,12,0,8" />
<CheckBox x:Name="MagnifierBox" Content="Vergrootglas tonen bij het kiezen van een gebied (knipsel en screencast)" />
@@ -27,6 +27,7 @@ public partial class SettingsWindow : Window
["Record"] = "screencast (MP4)",
["Gif"] = "screencast (GIF)",
["Clipboard"] = "bewerken vanaf klembord",
["Ocr"] = "OCR",
};
private readonly AppSettings _settings;
@@ -66,6 +67,7 @@ public partial class SettingsWindow : Window
["Record"] = RecordHotkeyButton,
["Gif"] = GifHotkeyButton,
["Clipboard"] = ClipboardHotkeyButton,
["Ocr"] = OcrHotkeyButton,
};
HotkeyHint.Text = App.Instance.HotkeyHook?.IsActive == true
@@ -127,6 +129,7 @@ public partial class SettingsWindow : Window
_hotkeys["Record"] = settings.RecordHotkey;
_hotkeys["Gif"] = settings.GifHotkey;
_hotkeys["Clipboard"] = settings.ClipboardHotkey;
_hotkeys["Ocr"] = settings.OcrHotkey;
foreach (string id in _hotkeys.Keys)
{
ShowHotkey(id);
@@ -477,6 +480,7 @@ public partial class SettingsWindow : Window
_settings.RecordHotkey = _hotkeys["Record"];
_settings.GifHotkey = _hotkeys["Gif"];
_settings.ClipboardHotkey = _hotkeys["Clipboard"];
_settings.OcrHotkey = _hotkeys["Ocr"];
_settings.RecordSystemAudio = SystemAudioBox.IsChecked == true;
_settings.RecordMicrophone = MicrophoneBox.IsChecked == true;
_settings.RecordShowCursor = ShowCursorBox.IsChecked == true;