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 _snipWindows = new(); private List _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!; /// Toetsenbordhaak voor alle sneltoetsen (knipsels en screencasts). internal GlobalHotkeyHook? HotkeyHook { get; private set; } /// Achtergrondcontrole op nieuwe versies; stuurt de blauwe stip bij Instellingen. 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; } } /// Programmanaam van het geknipte venster, voor %V in het naamsjabloon. public void OpenEditor(BitmapSource image, string? windowName = null) { var editor = new EditorWindow(image, Settings, windowName: windowName); editor.Show(); BringToFront(editor); } /// Nieuw rechthoekknipsel na het sluiten van de editor. 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(); } /// Bevriest alle schermen (na de ingestelde vertraging) en laat een rechthoek slepen; het resultaat opent in de editor. public void BeginRegionSnip() => BeginDelayedSnip(pickWindow: false); /// Bevriest alle schermen (na de ingestelde vertraging) en laat een venster aanklikken; het resultaat opent in de editor. 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? 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 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(); 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)); } /// Start een screencast: eerst het gebied kiezen, daarna de knoppenbalk. 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(); } /// Tijdens een screencast geen knipsels oppikken. 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); } /// Het knipsel op het klembord is weggegooid; pas na een nieuwe kopie weer openen. 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); } /// Editor met een leeg wit canvas, voor als er (nog) geen knipsel is. 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(); } } /// Sluit alle vensters; false als een screencast loopt of een editor open blijft. 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().ToList()) { window.Close(); } return !Windows.OfType().Any(); } /// Start de gedownloade installer en sluit kniip; de installer start kniip daarna opnieuw. 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; } }