Files
kniip/src/Kniip/App.xaml.cs
T
2026-11-09 20:23:46 +01:00

632 lines
21 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Windows;
using System.Windows.Media.Imaging;
using System.Windows.Threading;
using Kniip.Capture;
using Kniip.Editor;
using Kniip.History;
using Kniip.Infrastructure;
using Kniip.Interop;
using Kniip.Notifications;
using Kniip.Recording;
using Kniip.Settings;
using Kniip.Update;
using Microsoft.Win32;
using Screen = System.Windows.Forms.Screen;
namespace Kniip;
public partial class App : Application
{
private SingleInstance? _singleInstance;
private TrayIcon? _tray;
private SettingsWindow? _settingsWindow;
private HistoryWindow? _historyWindow;
private readonly List<RegionCaptureWindow> _snipWindows = new();
private List<SnappedWindow> _snipSnapshot = new();
private CountdownWindow? _snipCountdown;
private uint? _ignoredClipboardSequence;
public static App Instance => (App)Current;
public AppSettings Settings { get; private set; } = new();
internal CaptureCoordinator Capture { get; private set; } = null!;
internal RecordingController Recorder { get; private set; } = null!;
/// <summary>Toetsenbordhaak voor alle sneltoetsen (knipsels en screencasts).</summary>
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)
{
base.OnStartup(e);
DispatcherUnhandledException += OnUnhandledException;
string[] args = e.Args;
if (HandleMaintenanceArguments(args))
{
Shutdown();
return;
}
_singleInstance = new SingleInstance();
if (!_singleInstance.TryAcquire())
{
SingleInstance.SendToPrimary(args);
_singleInstance.Dispose();
_singleInstance = null;
Shutdown();
return;
}
System.Windows.Forms.Application.EnableVisualStyles();
try
{
ShellIntegration.MigrateLegacyRegistration();
}
catch (Exception)
{
// Een mislukte opruimactie in het register mag het starten niet blokkeren.
}
Settings = SettingsStore.Load();
ThemeManager.Apply(Settings.ColorMode);
Capture = new CaptureCoordinator(Dispatcher);
Capture.ApplySettings(Settings);
Capture.CaptureReceived += OnCaptureReceived;
RecordingSession.CleanupTempFolder();
Recorder = new RecordingController(Settings, Dispatcher);
Recorder.StageChanged += OnRecordingStageChanged;
Recorder.RecordingFinished += OpenScreencast;
Recorder.Notify += (title, text) => _tray?.ShowBalloon(title, text);
_tray = new TrayIcon();
_tray.OpenFromClipboardRequested += OpenFromClipboard;
_tray.OpenFileRequested += OpenFileWithDialog;
_tray.SnipRequested += BeginRegionSnip;
_tray.WindowSnipRequested += BeginWindowSnip;
_tray.SetCaptureDelay(Settings.CaptureDelaySeconds);
_tray.CaptureDelayChanged += seconds =>
{
Settings.CaptureDelaySeconds = seconds;
SettingsStore.Save(Settings);
};
_tray.RecordRequested += () =>
{
if (Recorder.Stage == RecordingStage.Idle)
{
Recorder.Toggle(ScreencastFormat.Mp4);
}
else
{
Recorder.TrayAction();
}
};
_tray.GifRequested += () => Recorder.Toggle(ScreencastFormat.Gif);
_tray.SettingsRequested += ShowSettings;
_tray.HistoryRequested += ShowHistory;
_tray.FoldersProvider = TrayFolders;
_tray.ExitRequested += ExitApplication;
_tray.PauseChanged += _ => UpdateCapturePaused();
_tray.ApplyTheme(ThemeManager.IsDark);
ThemeManager.Changed += () => _tray?.ApplyTheme(ThemeManager.IsDark);
HotkeyHook = new GlobalHotkeyHook(Dispatcher);
ApplyHotkeys();
Updates.Apply(Settings.CheckForUpdates);
_singleInstance.StartServer(received => Dispatcher.BeginInvoke(() => HandleArguments(received, false)));
HandleArguments(args, true);
}
private static bool HandleMaintenanceArguments(string[] args)
{
if (args.Contains("--register-openwith", StringComparer.OrdinalIgnoreCase))
{
ShellIntegration.RegisterOpenWith();
return true;
}
if (args.Contains("--unregister", StringComparer.OrdinalIgnoreCase))
{
ShellIntegration.UnregisterOpenWith();
ShellIntegration.SetAutostart(false);
return true;
}
return false;
}
private void HandleArguments(string[] args, bool isFirstStart)
{
string[] files = args.Where(a => !a.StartsWith("--", StringComparison.Ordinal)).ToArray();
foreach (string file in files)
{
OpenFile(file);
}
if (args.Contains("--updated", StringComparer.OrdinalIgnoreCase))
{
_tray?.ShowBalloon("kniip is bijgewerkt", $"Je gebruikt nu versie {UpdateChecker.CurrentVersion}.");
return;
}
if (files.Length > 0 || args.Contains("--tray", StringComparer.OrdinalIgnoreCase))
{
return;
}
if (isFirstStart)
{
string? hotkey = HotkeyDisplay(Settings.SnipHotkey);
_tray?.ShowBalloon($"{AppIdentity.DisplayName} is actief", hotkey == null
? "Maak een knipsel met Win + Shift + S. kniip biedt het daarna aan om te bewerken."
: $"Druk op {hotkey} en sleep een rechthoek om te knippen en te bewerken.");
}
OpenBlankEditor();
}
private void OnCaptureReceived(BitmapSource image)
{
switch (Settings.Action)
{
case CaptureAction.OpenEditor:
OpenEditor(image);
break;
case CaptureAction.Notify:
SnipToast.ShowFor(image, () => OpenEditor(image));
break;
}
}
/// <param name="windowName">Programmanaam van het geknipte venster, voor %V in het naamsjabloon.</param>
public void OpenEditor(BitmapSource image, string? windowName = null)
{
var editor = new EditorWindow(image, Settings, windowName: windowName);
editor.Show();
BringToFront(editor);
}
/// <summary>Nieuw rechthoekknipsel na het sluiten van de editor.</summary>
public void StartNewSnip()
{
// Even wachten zodat het gesloten editorvenster niet op het nieuwe knipsel staat.
var timer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(300) };
timer.Tick += (_, _) =>
{
timer.Stop();
BeginSnip(pickWindow: false);
};
timer.Start();
}
/// <summary>Bevriest alle schermen (na de ingestelde vertraging) en laat een rechthoek slepen; het resultaat opent in de editor.</summary>
public void BeginRegionSnip() => BeginDelayedSnip(pickWindow: false);
/// <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)
{
if (_snipCountdown != null)
{
return;
}
int seconds = Settings.CaptureDelaySeconds;
if (seconds <= 0 || _snipWindows.Count > 0)
{
BeginSnip(pickWindow);
return;
}
var countdown = new CountdownWindow(seconds);
countdown.Elapsed += () =>
{
EndCountdown();
// Even wachten tot het aftelvenster echt van het scherm is.
var timer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(150) };
timer.Tick += (_, _) =>
{
timer.Stop();
BeginSnip(pickWindow);
};
timer.Start();
};
countdown.Cancelled += EndCountdown;
_snipCountdown = countdown;
ApplyHotkeys();
countdown.Start();
}
private void EndCountdown()
{
_snipCountdown = null;
ApplyHotkeys();
}
private void BeginSnip(bool pickWindow)
{
if (_snipWindows.Count > 0 || _snipCountdown != null || Recorder.Stage == RecordingStage.Selecting)
{
return;
}
NativeMethods.GetCursorPos(out NativeMethods.POINT cursor);
var cursorPoint = new System.Drawing.Point(cursor.X, cursor.Y);
// Eerst alle schermen vastleggen, dan pas vensters tonen, anders staat de overlay in het beeld.
_snipSnapshot = PhysicalWindow.Snapshot(includeOwn: true);
List<System.Drawing.Rectangle>? windows = pickWindow ? _snipSnapshot.Select(w => w.Bounds).ToList() : null;
var frozen = Screen.AllScreens.Select(screen => (screen, ScreenGrabber.Capture(screen.Bounds))).ToList();
foreach ((Screen screen, BitmapSource image) in frozen)
{
var window = new RegionCaptureWindow(screen, image, windows, Settings.ShowMagnifier);
window.Finished += OnRegionSnipFinished;
_snipWindows.Add(window);
window.Show();
}
RegionCaptureWindow? active = _snipWindows.FirstOrDefault(w => w.ContainsCursor(cursorPoint)) ?? _snipWindows.FirstOrDefault();
active?.Activate();
active?.Focus();
// Na een sneltoets in een ander programma krijgt de overlay vaak geen toetsenbordfocus;
// Esc en Enter lopen daarom via de haak.
if (HotkeyHook is { IsActive: true } hook)
{
hook.Capture = OnRegionSnipKey;
}
}
private void OnRegionSnipKey(HotkeyGesture pressed)
{
NativeMethods.GetCursorPos(out NativeMethods.POINT cursor);
var cursorPoint = new System.Drawing.Point(cursor.X, cursor.Y);
RegionCaptureWindow? target = _snipWindows.FirstOrDefault(w => w.ContainsCursor(cursorPoint)) ?? _snipWindows.FirstOrDefault();
if (target == null || pressed.Modifiers != System.Windows.Input.ModifierKeys.None)
{
return;
}
if (pressed.Key == System.Windows.Input.Key.Escape)
{
target.Cancel();
}
else if (pressed.Key == System.Windows.Input.Key.Enter)
{
target.SelectWholeScreen();
}
}
private void OnRegionSnipFinished(BitmapSource? image)
{
if (HotkeyHook != null)
{
HotkeyHook.Capture = null;
}
System.Drawing.Rectangle? area = _snipWindows.Select(w => w.SelectedArea).FirstOrDefault(a => a != null);
foreach (RegionCaptureWindow window in _snipWindows)
{
window.Finished -= OnRegionSnipFinished;
window.Close();
}
_snipWindows.Clear();
List<SnappedWindow> snapshot = _snipSnapshot;
_snipSnapshot = new();
if (image != null)
{
string? windowName = area is { } a ? PhysicalWindow.ProgramNameAt(snapshot, a) : null;
OpenEditor(image, windowName);
}
}
private string? HotkeyDisplay(string? hotkey) =>
HotkeyHook is { IsActive: true } && HotkeyGesture.TryParse(hotkey, out HotkeyGesture gesture)
? gesture.DisplayText
: null;
private void ApplyHotkeys()
{
if (HotkeyHook != null)
{
var bindings = new Dictionary<HotkeyGesture, Action>();
void Bind(string? hotkey, Action action)
{
if (HotkeyGesture.TryParse(hotkey, out HotkeyGesture gesture))
{
bindings.TryAdd(gesture, action);
}
}
Bind(Settings.SnipHotkey, BeginRegionSnip);
Bind(Settings.WindowHotkey, BeginWindowSnip);
Bind(Settings.RecordHotkey, () => Recorder.Toggle(ScreencastFormat.Mp4));
Bind(Settings.GifHotkey, () => Recorder.Toggle(ScreencastFormat.Gif));
Bind(Settings.ClipboardHotkey, OpenFromClipboard);
if (_snipCountdown is { } countdown)
{
// Alleen tijdens het aftellen; daarna moet Esc gewoon naar het actieve programma.
bindings[new HotkeyGesture(System.Windows.Input.ModifierKeys.None, System.Windows.Input.Key.Escape)] = countdown.Cancel;
}
HotkeyHook.Bindings = bindings;
}
_tray?.SetHotkeys(HotkeyDisplay(Settings.SnipHotkey), HotkeyDisplay(Settings.WindowHotkey),
HotkeyDisplay(Settings.RecordHotkey), HotkeyDisplay(Settings.GifHotkey), HotkeyDisplay(Settings.ClipboardHotkey));
}
/// <summary>Start een screencast: eerst het gebied kiezen, daarna de knoppenbalk.</summary>
public void StartRecording()
{
if (Recorder.Stage == RecordingStage.Idle)
{
Recorder.Toggle();
}
}
public void StopRecording()
{
if (Recorder.Stage is RecordingStage.Recording or RecordingStage.Paused)
{
Recorder.Toggle();
}
}
private void OnRecordingStageChanged(RecordingStage stage)
{
string? text = stage switch
{
RecordingStage.Idle => null,
RecordingStage.Recording or RecordingStage.Paused => "Opname stoppen",
RecordingStage.Finishing => "Opname afronden...",
_ => "Opname annuleren"
};
_tray?.SetRecordItem(text, stage is RecordingStage.Recording or RecordingStage.Paused);
UpdateCapturePaused();
}
/// <summary>Tijdens een screencast geen knipsels oppikken.</summary>
private void UpdateCapturePaused() =>
Capture.Paused = (_tray?.IsPaused ?? false) || Recorder.Stage != RecordingStage.Idle;
private void OpenScreencast(string path, ScreencastFormat? format, string? windowName)
{
var window = new ScreencastWindow(path, Settings, format, windowName);
window.Show();
BringToFront(window);
}
/// <summary>Het knipsel op het klembord is weggegooid; pas na een nieuwe kopie weer openen.</summary>
public void IgnoreCurrentClipboard() => _ignoredClipboardSequence = NativeMethods.GetClipboardSequenceNumber();
public void OpenFromClipboard()
{
BitmapSource? image = _ignoredClipboardSequence == NativeMethods.GetClipboardSequenceNumber()
? null
: ClipboardImageReader.TryRead(includeFiles: true);
if (image == null)
{
OpenBlankEditor();
return;
}
OpenEditor(image);
}
/// <summary>Editor met een leeg wit canvas, voor als er (nog) geen knipsel is.</summary>
public void OpenBlankEditor(Window? placeLike = null)
{
const int width = 1280, height = 720;
byte[] pixels = Enumerable.Repeat((byte)255, width * height * 4).ToArray();
BitmapSource blank = BitmapSource.Create(width, height, 96, 96, System.Windows.Media.PixelFormats.Bgra32, null, pixels, width * 4);
var editor = new EditorWindow(blank, Settings, isBlank: true);
if (placeLike != null)
{
Rect bounds = placeLike.WindowState == WindowState.Normal
? new Rect(placeLike.Left, placeLike.Top, placeLike.Width, placeLike.Height)
: placeLike.RestoreBounds;
editor.WindowStartupLocation = WindowStartupLocation.Manual;
editor.Left = bounds.Left;
editor.Top = bounds.Top;
editor.Width = bounds.Width;
editor.Height = bounds.Height;
editor.WindowState = placeLike.WindowState == WindowState.Maximized ? WindowState.Maximized : WindowState.Normal;
}
editor.Show();
BringToFront(editor);
}
public void OpenFile(string path)
{
try
{
OpenEditor(PixelUtil.LoadFile(path));
}
catch (Exception ex)
{
MessageBox.Show($"Kan '{Path.GetFileName(path)}' niet openen.\n\n{ex.Message}", "kniip",
MessageBoxButton.OK, MessageBoxImage.Warning);
}
}
private void OpenFileWithDialog()
{
if (PickImageFile(null) is { } path)
{
OpenFile(path);
}
}
public string? PickImageFile(Window? owner)
{
var dialog = new OpenFileDialog
{
Title = "Afbeelding openen in kniip",
Filter = "Afbeeldingen|*.png;*.jpg;*.jpeg;*.bmp;*.gif|Alle bestanden|*.*",
InitialDirectory = CaptureCoordinator.ScreenshotsFolder ?? Environment.GetFolderPath(Environment.SpecialFolder.MyPictures)
};
bool? ok = owner == null ? dialog.ShowDialog() : dialog.ShowDialog(owner);
return ok == true ? dialog.FileName : null;
}
public void ShowSettings()
{
if (_settingsWindow == null)
{
_settingsWindow = new SettingsWindow(Settings);
_settingsWindow.Closed += (_, _) => _settingsWindow = null;
_settingsWindow.Show();
}
BringToFront(_settingsWindow);
}
public void ShowHistory(bool images)
{
if (_historyWindow == null)
{
_historyWindow = new HistoryWindow(images);
_historyWindow.Closed += (_, _) => _historyWindow = null;
_historyWindow.Show();
}
else
{
_historyWindow.ShowImages(images);
}
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));
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")))
};
}
public void ShowBalloon(string title, string text) => _tray?.ShowBalloon(title, text);
public void SaveSettings()
{
SettingsStore.Save(Settings);
ThemeManager.Apply(Settings.ColorMode);
Capture.ApplySettings(Settings);
ApplyHotkeys();
Updates.Apply(Settings.CheckForUpdates);
}
private static void BringToFront(Window window)
{
if (window.WindowState == WindowState.Minimized)
{
window.WindowState = WindowState.Normal;
}
window.Topmost = true;
window.Activate();
window.Topmost = false;
window.Focus();
}
private void ExitApplication()
{
if (TryCloseAllWindows())
{
Shutdown();
}
}
/// <summary>Sluit alle vensters; false als een screencast loopt of een editor open blijft.</summary>
private bool TryCloseAllWindows()
{
if (Recorder.Stage != RecordingStage.Idle)
{
MessageBox.Show("Er loopt nog een screencast. Stop of annuleer die eerst.", "kniip",
MessageBoxButton.OK, MessageBoxImage.Information);
return false;
}
foreach (Window window in Windows.Cast<Window>().ToList())
{
window.Close();
}
return !Windows.OfType<EditorWindow>().Any();
}
/// <summary>Start de gedownloade installer en sluit kniip; de installer start kniip daarna opnieuw.</summary>
public void InstallUpdate(string installerPath)
{
if (!TryCloseAllWindows())
{
return;
}
try
{
UpdateInstaller.Start(installerPath);
}
catch (Exception ex)
{
MessageBox.Show($"De installer kon niet worden gestart.\n\n{ex.Message}", "kniip",
MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
Shutdown();
}
protected override void OnExit(ExitEventArgs e)
{
if (_singleInstance != null)
{
SettingsStore.Save(Settings);
}
Capture?.Dispose();
HotkeyHook?.Dispose();
_tray?.Dispose();
_singleInstance?.Dispose();
base.OnExit(e);
}
private void OnUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e)
{
try
{
Directory.CreateDirectory(SettingsStore.Folder);
File.AppendAllText(Path.Combine(SettingsStore.Folder, "error.log"), $"{DateTime.Now:O}\n{e.Exception}\n\n");
}
catch (Exception)
{
// Loggen is best effort.
}
MessageBox.Show($"Er ging iets mis:\n\n{e.Exception.Message}", "kniip", MessageBoxButton.OK, MessageBoxImage.Error);
e.Handled = true;
}
}