Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
rogierandCursor committed 2026-11-09 17:32:50 +01:00
1 parent 43a2c4f57b
commit 6d19b91963
30 files changed
+1694 -78

No files matched your search

+94 -7
View File
@@ -7,6 +7,7 @@ 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;
@@ -23,8 +24,10 @@ 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;
@@ -90,6 +93,12 @@ public partial class App : Application
_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)
@@ -103,6 +112,8 @@ public partial class App : Application
};
_tray.GifRequested += () => Recorder.Toggle(ScreencastFormat.Gif);
_tray.SettingsRequested += ShowSettings;
_tray.HistoryRequested += ShowHistory;
_tray.FoldersProvider = TrayFolders;
_tray.ExitRequested += ExitApplication;
_tray.PauseChanged += _ => UpdateCapturePaused();
@@ -191,20 +202,59 @@ public partial class App : Application
timer.Tick += (_, _) =>
{
timer.Stop();
BeginRegionSnip();
BeginSnip(pickWindow: false);
};
timer.Start();
}
/// <summary>Bevriest alle schermen en laat een rechthoek slepen; het resultaat opent in de editor.</summary>
public void BeginRegionSnip() => BeginSnip(pickWindow: false);
/// <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 en laat een venster aanklikken; het resultaat opent in de editor.</summary>
public void BeginWindowSnip() => BeginSnip(pickWindow: true);
/// <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 || Recorder.Stage == RecordingStage.Selecting)
if (_snipWindows.Count > 0 || _snipCountdown != null || Recorder.Stage == RecordingStage.Selecting)
{
return;
}
@@ -218,7 +268,7 @@ public partial class App : Application
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);
var window = new RegionCaptureWindow(screen, image, windows, Settings.ShowMagnifier);
window.Finished += OnRegionSnipFinished;
_snipWindows.Add(window);
window.Show();
@@ -303,6 +353,12 @@ public partial class App : Application
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;
}
@@ -437,6 +493,37 @@ public partial class App : Application
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()
+21
View File
@@ -0,0 +1,21 @@
<Window x:Class="Kniip.Capture.CountdownWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="kniip" WindowStyle="None" AllowsTransparency="True" Background="Transparent"
Topmost="True" ShowInTaskbar="False" ShowActivated="False" ResizeMode="NoResize"
SizeToContent="WidthAndHeight" FontFamily="Segoe UI" UseLayoutRounding="True">
<Border Margin="10" Background="#FCFCFC" CornerRadius="8" BorderBrush="#DADADA" BorderThickness="1"
Cursor="Hand" MouseLeftButtonUp="Border_Click" ToolTip="Klik om te annuleren">
<Border.Effect>
<DropShadowEffect BlurRadius="14" ShadowDepth="2" Opacity="0.22" />
</Border.Effect>
<StackPanel Orientation="Horizontal" Margin="12,8,14,8">
<TextBlock x:Name="SecondsText" FontSize="28" FontWeight="SemiBold" Foreground="#0067C0"
MinWidth="34" TextAlignment="Center" VerticalAlignment="Center" />
<StackPanel Margin="10,0,0,0" VerticalAlignment="Center">
<TextBlock Text="Knipsel over" FontSize="12" FontWeight="SemiBold" />
<TextBlock Text="Esc om te annuleren" FontSize="11" Foreground="#5C5C5C" />
</StackPanel>
</StackPanel>
</Border>
</Window>
+116
View File
@@ -0,0 +1,116 @@
using System;
using System.Windows;
using System.Windows.Input;
using System.Windows.Threading;
using Kniip.Interop;
using Kniip.Recording;
using Rectangle = System.Drawing.Rectangle;
using Screen = System.Windows.Forms.Screen;
namespace Kniip.Capture;
/// <summary>Klein venster bij de muis dat aftelt tot een vertraagd knipsel; neemt nooit de focus over.</summary>
public partial class CountdownWindow : Window
{
private const int CursorOffset = 20;
private readonly DispatcherTimer _follow = new() { Interval = TimeSpan.FromMilliseconds(40) };
private readonly DateTime _end;
private bool _finished;
/// <summary>De tijd is om; het venster is dan al gesloten.</summary>
public event Action? Elapsed;
/// <summary>Geannuleerd met Esc of een klik; het venster is dan al gesloten.</summary>
public event Action? Cancelled;
public CountdownWindow(int seconds)
{
InitializeComponent();
Left = Top = -32000;
_end = DateTime.UtcNow.AddSeconds(seconds);
SecondsText.Text = seconds.ToString();
_follow.Tick += (_, _) => Tick();
SourceInitialized += (_, _) =>
{
NativeMethods.AddExtendedStyle(PhysicalWindow.Handle(this),
NativeMethods.WS_EX_TOOLWINDOW | NativeMethods.WS_EX_NOACTIVATE);
PhysicalWindow.ExcludeFromCapture(this);
};
ContentRendered += (_, _) => FollowCursor();
Closed += (_, _) =>
{
_follow.Stop();
if (!_finished)
{
_finished = true;
Cancelled?.Invoke();
}
};
}
public void Start()
{
Show();
FollowCursor();
_follow.Start();
}
public void Cancel() => Finish(Cancelled);
private void Tick()
{
TimeSpan left = _end - DateTime.UtcNow;
if (left <= TimeSpan.Zero)
{
Finish(Elapsed);
return;
}
SecondsText.Text = ((int)Math.Ceiling(left.TotalSeconds)).ToString();
FollowCursor();
}
private void FollowCursor()
{
if (!IsVisible)
{
return;
}
NativeMethods.GetCursorPos(out NativeMethods.POINT cursor);
Rectangle area = Screen.FromPoint(new System.Drawing.Point(cursor.X, cursor.Y)).WorkingArea;
Rectangle size = PhysicalWindow.GetBounds(this);
int x = cursor.X + CursorOffset, y = cursor.Y + CursorOffset;
if (x + size.Width > area.Right)
{
x = cursor.X - CursorOffset - size.Width;
}
if (y + size.Height > area.Bottom)
{
y = cursor.Y - CursorOffset - size.Height;
}
x = Math.Max(area.Left, x);
y = Math.Max(area.Top, y);
if (x != size.X || y != size.Y)
{
PhysicalWindow.Place(this, new Rectangle(x, y, size.Width, size.Height), resize: false);
}
}
private void Finish(Action? handler)
{
if (_finished)
{
return;
}
_finished = true;
Close();
handler?.Invoke();
}
private void Border_Click(object sender, MouseButtonEventArgs e) => Cancel();
}
+44
View File
@@ -0,0 +1,44 @@
<UserControl x:Class="Kniip.Capture.MagnifierControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
HorizontalAlignment="Left" VerticalAlignment="Top" IsHitTestVisible="False">
<StackPanel>
<Grid Width="120" Height="120">
<Grid.Effect>
<DropShadowEffect BlurRadius="10" ShadowDepth="2" Opacity="0.35" />
</Grid.Effect>
<Grid>
<Grid.Clip>
<EllipseGeometry Center="60,60" RadiusX="60" RadiusY="60" />
</Grid.Clip>
<Image x:Name="Zoom" Stretch="Fill" RenderOptions.BitmapScalingMode="NearestNeighbor" />
<Rectangle>
<Rectangle.Fill>
<DrawingBrush Viewport="0,0,8,8" ViewportUnits="Absolute" TileMode="Tile">
<DrawingBrush.Drawing>
<GeometryDrawing>
<GeometryDrawing.Pen>
<Pen Brush="#40000000" Thickness="0.5" />
</GeometryDrawing.Pen>
<GeometryDrawing.Geometry>
<RectangleGeometry Rect="0,0,8,8" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
</DrawingBrush.Drawing>
</DrawingBrush>
</Rectangle.Fill>
</Rectangle>
<Rectangle Height="8" VerticalAlignment="Top" Margin="0,56,0,0" Fill="{StaticResource AccentBrush}" Opacity="0.55" />
<Rectangle Width="8" HorizontalAlignment="Left" Margin="56,0,0,0" Fill="{StaticResource AccentBrush}" Opacity="0.55" />
<Rectangle Width="10" Height="10" HorizontalAlignment="Left" VerticalAlignment="Top" Margin="55,55,0,0"
Stroke="White" StrokeThickness="1" />
<Rectangle Width="8" Height="8" HorizontalAlignment="Left" VerticalAlignment="Top" Margin="56,56,0,0"
Stroke="#202020" StrokeThickness="1" />
</Grid>
<Ellipse Stroke="White" StrokeThickness="2" />
</Grid>
<Border HorizontalAlignment="Center" Margin="0,6,0,0" Background="#E6202020" CornerRadius="4" Padding="6,2">
<TextBlock x:Name="CoordinateText" Foreground="White" FontSize="12" />
</Border>
</StackPanel>
</UserControl>
@@ -0,0 +1,76 @@
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Imaging;
namespace Kniip.Capture;
/// <summary>Rond vergrootglas met kruisdraad en coördinaten dat naast de muisaanwijzer meeloopt.</summary>
public partial class MagnifierControl : UserControl
{
private const int Pixels = 15;
private const double CursorOffset = 20;
private readonly WriteableBitmap _zoom = new(Pixels, Pixels, 96, 96, PixelFormats.Bgra32, null);
private readonly byte[] _buffer = new byte[Pixels * Pixels * 4];
public MagnifierControl()
{
InitializeComponent();
Zoom.Source = _zoom;
Visibility = Visibility.Collapsed;
}
/// <param name="frozen">Vastgelegd beeld van het scherm.</param>
/// <param name="relative">Muispositie in pixels van <paramref name="frozen"/>.</param>
/// <param name="screen">Muispositie in fysieke bureaubladcoördinaten, voor de tekst.</param>
/// <param name="cursor">Muispositie in het venster (DIP).</param>
/// <param name="area">Grootte van het venster (DIP).</param>
public void Show(BitmapSource frozen, System.Drawing.Point relative, System.Drawing.Point screen, Point cursor, Size area)
{
CopyPixels(frozen, relative);
CoordinateText.Text = $"X: {screen.X} Y: {screen.Y}";
Visibility = Visibility.Visible;
Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
Size size = DesiredSize;
double left = cursor.X + CursorOffset;
if (left + size.Width > area.Width)
{
left = cursor.X - CursorOffset - size.Width;
}
double top = cursor.Y + CursorOffset;
if (top + size.Height > area.Height)
{
top = cursor.Y - CursorOffset - size.Height;
}
Margin = new Thickness(Math.Max(0, left), Math.Max(0, top), 0, 0);
}
public void Hide() => Visibility = Visibility.Collapsed;
private void CopyPixels(BitmapSource frozen, System.Drawing.Point center)
{
Array.Clear(_buffer);
for (int i = 3; i < _buffer.Length; i += 4)
{
_buffer[i] = 255;
}
int half = Pixels / 2;
int x0 = Math.Max(0, center.X - half);
int y0 = Math.Max(0, center.Y - half);
int x1 = Math.Min(frozen.PixelWidth, center.X + half + 1);
int y1 = Math.Min(frozen.PixelHeight, center.Y + half + 1);
if (x1 > x0 && y1 > y0)
{
int offset = ((y0 - (center.Y - half)) * Pixels + (x0 - (center.X - half))) * 4;
frozen.CopyPixels(new Int32Rect(x0, y0, x1 - x0, y1 - y0), _buffer, Pixels * 4, offset);
}
_zoom.WritePixels(new Int32Rect(0, 0, Pixels, Pixels), _buffer, Pixels * 4, 0);
}
}
@@ -1,6 +1,7 @@
<Window x:Class="Kniip.Capture.RegionCaptureWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Kniip.Capture"
Title="kniip rechthoek knippen" WindowStyle="None" ResizeMode="NoResize"
ShowInTaskbar="False" Topmost="True" Cursor="Cross" FontFamily="Segoe UI" FontSize="13"
Background="Black">
@@ -13,6 +14,7 @@
CornerRadius="4" Padding="6,2" Visibility="Collapsed" IsHitTestVisible="False">
<TextBlock x:Name="SizeText" Foreground="White" FontSize="12" />
</Border>
<local:MagnifierControl x:Name="Magnifier" />
<Border x:Name="Hint" HorizontalAlignment="Center" VerticalAlignment="Top" Margin="0,28,0,0" Background="#F2FFFFFF"
CornerRadius="6" Padding="16,9" IsHitTestVisible="False">
<Border.Effect>
+13 -2
View File
@@ -22,6 +22,7 @@ public partial class RegionCaptureWindow : Window
private readonly Screen _screen;
private readonly BitmapSource _frozen;
private readonly IReadOnlyList<Rectangle>? _windows;
private readonly bool _showMagnifier;
private Point? _dragStart;
private bool _dragging;
private Rectangle? _hover;
@@ -33,13 +34,16 @@ public partial class RegionCaptureWindow : Window
internal Rectangle? SelectedArea { get; private set; }
/// <param name="windows">Vensterrechthoeken van boven naar onder; gezet = venster aanklikken in plaats van slepen.</param>
internal RegionCaptureWindow(Screen screen, BitmapSource frozen, IReadOnlyList<Rectangle>? windows = null)
/// <param name="showMagnifier">Vergrootglas bij de muisaanwijzer tonen; niet bij venster aanklikken.</param>
internal RegionCaptureWindow(Screen screen, BitmapSource frozen, IReadOnlyList<Rectangle>? windows = null, bool showMagnifier = false)
{
InitializeComponent();
_screen = screen;
_frozen = frozen;
_windows = windows;
_showMagnifier = showMagnifier && windows == null;
Frozen.Source = frozen;
MouseLeave += (_, _) => Magnifier.Hide();
if (windows != null)
{
Cursor = Cursors.Hand;
@@ -78,12 +82,19 @@ public partial class RegionCaptureWindow : Window
return;
}
Point position = e.GetPosition(this);
if (_showMagnifier)
{
System.Drawing.Point physical = ToPhysical(position);
var relative = new System.Drawing.Point(physical.X - _screen.Bounds.X, physical.Y - _screen.Bounds.Y);
Magnifier.Show(_frozen, relative, physical, position, new Size(ActualWidth, ActualHeight));
}
if (_dragStart is not Point start || e.LeftButton != MouseButtonState.Pressed)
{
return;
}
Point position = e.GetPosition(this);
if (!_dragging && (position - start).Length >= DragThreshold)
{
_dragging = true;
+10
View File
@@ -188,6 +188,16 @@
</DockPanel>
</Border>
<!-- Onderste werkbalk in de gesplitste indeling; de code verplaatst de acties hierheen -->
<Border x:Name="ActionBar" DockPanel.Dock="Bottom" Background="{StaticResource ToolbarBrush}" BorderBrush="{StaticResource DividerBrush}"
BorderThickness="0,1,0,0" Padding="8,6" Visibility="Collapsed"
SizeChanged="Toolbar_SizeChanged" ContextMenuOpening="Toolbar_ContextMenuOpening">
<Border.ContextMenu>
<ContextMenu x:Name="ActionBarMenu" />
</Border.ContextMenu>
<local:ToolbarPanel x:Name="ActionItems" OrderChanged="ToolbarItems_OrderChanged" />
</Border>
<!-- Gereedschappen en acties; rechtsklik voor vergrendelen, positie en zichtbare knoppen -->
<Border x:Name="Toolbar" DockPanel.Dock="Top" Background="{StaticResource ToolbarBrush}" BorderBrush="{StaticResource DividerBrush}"
BorderThickness="0,0,0,1" Padding="8,6" SizeChanged="Toolbar_SizeChanged" ContextMenuOpening="Toolbar_ContextMenuOpening">
+179 -61
View File
@@ -35,11 +35,28 @@ public partial class EditorWindow : Window
private static readonly CursorKind[] CursorKindValues = { CursorKind.Arrow, CursorKind.Click, CursorKind.Hand };
private static readonly BlurMode[] BlurModeValues = { BlurMode.Pixelate, BlurMode.Blur };
/// <summary>Standaardvolgorde in de gesplitste indeling: eerst de bovenste werkbalk, dan de onderste.</summary>
private static readonly string[] DefaultSplitOrder =
{
"Select", "Sep1", "Rectangle", "Ellipse", "Line", "Arrow", "Text", "Step", "Cursor", "Emoji", "Highlight", "Blur",
"Sep2", "Crop", "Sep3", "Undo", "Redo", "Delete", "Sep4", "Clear",
"New", "Screencast", "Spacer", "ZoomOut", "Zoom", "ZoomIn", "Spacer2", "Settings", "Sep5", "Save", "Upload", "Copy"
};
/// <summary>Items die in de gesplitste indeling op de onderste werkbalk staan.</summary>
private static readonly HashSet<string> SplitBottomIds = new()
{
"New", "Screencast", "Spacer", "ZoomOut", "Zoom", "ZoomIn", "Spacer2", "Settings", "Sep5", "Save", "Upload", "Copy"
};
private readonly EditorDocument _doc;
private readonly UndoStack _undo = new();
private readonly AppSettings _settings;
private readonly DispatcherTimer _hintResetTimer = new() { Interval = TimeSpan.FromSeconds(3) };
private readonly List<string> _defaultToolbarOrder = new();
private readonly List<string> _defaultSplitOrder = new();
private readonly Dictionary<string, UIElement> _toolbarItems = new();
private EditorLayout _appliedLayout;
private ToolKind _tool;
private Annotation? _selected;
@@ -140,45 +157,71 @@ public partial class EditorWindow : Window
private void InitializeToolbar()
{
_defaultToolbarOrder.AddRange(ToolbarItems.Items.Select(ItemId));
ApplyToolbarOrder(_settings.ToolbarOrder);
foreach (UIElement item in ToolbarItems.Items)
{
_toolbarItems[ItemId(item)] = item;
_defaultToolbarOrder.Add(ItemId(item));
}
_defaultSplitOrder.AddRange(DefaultSplitOrder.Where(_toolbarItems.ContainsKey));
_defaultSplitOrder.AddRange(_defaultToolbarOrder.Except(_defaultSplitOrder));
ApplyToolbarLayout();
foreach (UIElement item in _toolbarItems.Values)
{
item.Visibility = _settings.ToolbarHidden.Contains(ItemId(item)) ? Visibility.Collapsed : Visibility.Visible;
}
ApplyUploadButton();
ApplyToolbarPosition();
ApplyToolbarLock();
}
private static string ItemId(UIElement item) => ToolbarPanel.GetItemId(item) ?? string.Empty;
private void ApplyToolbarOrder(IReadOnlyList<string> saved)
private bool IsSplitLayout => _settings.EditorLayout == EditorLayout.Split;
private IEnumerable<UIElement> AllToolbarItems => ToolbarItems.Items.Concat(ActionItems.Items);
/// <summary>Items volgens de opgeslagen volgorde van de huidige indeling over de bovenste en onderste werkbalk verdelen.</summary>
private void ApplyToolbarLayout()
{
Dictionary<string, UIElement> byId = ToolbarItems.Items.ToDictionary(ItemId);
List<string> order = saved.Where(byId.ContainsKey).Distinct().ToList();
bool split = IsSplitLayout;
_appliedLayout = _settings.EditorLayout;
List<string> order = split
? MergeToolbarOrder(_settings.SplitToolbarOrder, _defaultSplitOrder)
: MergeToolbarOrder(_settings.ToolbarOrder, _defaultToolbarOrder);
ToolbarItems.Children.Clear();
ActionItems.Children.Clear();
foreach (string id in order)
{
(split && SplitBottomIds.Contains(id) ? ActionItems : ToolbarItems).Children.Add(_toolbarItems[id]);
}
ToolbarItems.HorizontalAlignment = split ? HorizontalAlignment.Center : HorizontalAlignment.Stretch;
ActionBar.Visibility = split && !_toolbarsHidden ? Visibility.Visible : Visibility.Collapsed;
ApplyToolbarPosition();
}
private List<string> MergeToolbarOrder(IReadOnlyList<string> saved, IReadOnlyList<string> defaults)
{
List<string> order = saved.Where(_toolbarItems.ContainsKey).Distinct().ToList();
// Items die nog niet in de opgeslagen volgorde staan (zoals nieuwe knoppen) komen na hun standaardvoorganger.
for (int i = 0; i < _defaultToolbarOrder.Count; i++)
for (int i = 0; i < defaults.Count; i++)
{
string id = _defaultToolbarOrder[i];
string id = defaults[i];
if (!order.Contains(id))
{
order.Insert(i == 0 ? 0 : order.IndexOf(_defaultToolbarOrder[i - 1]) + 1, id);
order.Insert(i == 0 ? 0 : order.IndexOf(defaults[i - 1]) + 1, id);
}
}
ToolbarItems.Children.Clear();
foreach (string id in order)
{
ToolbarItems.Children.Add(byId[id]);
}
return order;
}
private void ApplyToolbarPosition()
{
Dock dock = _settings.ToolbarPosition;
Dock dock = IsSplitLayout ? Dock.Top : _settings.ToolbarPosition;
bool vertical = dock is Dock.Left or Dock.Right;
DockPanel.SetDock(Toolbar, dock);
ToolbarItems.Orientation = vertical ? Orientation.Vertical : Orientation.Horizontal;
@@ -196,8 +239,11 @@ public partial class EditorWindow : Window
private void ApplyToolbarLock()
{
bool locked = _settings.ToolbarLocked;
Brush background = locked ? (Brush)FindResource("ToolbarBrush") : new SolidColorBrush(Color.FromRgb(0xEA, 0xF2, 0xFB));
ToolbarItems.IsLocked = locked;
Toolbar.Background = locked ? (Brush)FindResource("ToolbarBrush") : new SolidColorBrush(Color.FromRgb(0xEA, 0xF2, 0xFB));
ActionItems.IsLocked = locked;
Toolbar.Background = background;
ActionBar.Background = background;
UpdateHint();
}
@@ -209,73 +255,136 @@ public partial class EditorWindow : Window
}
}
// Labels alleen tonen als alles op één regel past; staat de werkbalk opzij, dan altijd alleen iconen.
private void UpdateToolbarLayout()
{
if (ToolbarItems.Orientation == Orientation.Vertical)
UpdateActionLabels(Toolbar, ToolbarItems);
UpdateActionLabels(ActionBar, ActionItems);
}
// Labels alleen tonen als alles op één regel past; staat de werkbalk opzij, dan altijd alleen iconen.
private void UpdateActionLabels(Border bar, ToolbarPanel panel)
{
List<TextBlock> labels = new[] { NewLabel, ClearLabel, ScreencastLabel, SaveLabel, UploadLabel, CopyLabel }
.Where(label => PanelOf(label) == panel).ToList();
if (labels.Count == 0)
{
SetActionLabelsVisible(false);
return;
}
double available = Toolbar.ActualWidth - Toolbar.Padding.Left - Toolbar.Padding.Right
- Toolbar.BorderThickness.Left - Toolbar.BorderThickness.Right;
SetActionLabelsVisible(true);
ToolbarItems.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
SetActionLabelsVisible(ToolbarItems.SingleLineLength <= available);
if (panel.Orientation == Orientation.Vertical)
{
SetActionLabelsVisible(panel, labels, false);
return;
}
double available = bar.ActualWidth - bar.Padding.Left - bar.Padding.Right
- bar.BorderThickness.Left - bar.BorderThickness.Right;
SetActionLabelsVisible(panel, labels, true);
panel.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
SetActionLabelsVisible(panel, labels, panel.SingleLineLength <= available);
}
private void SetActionLabelsVisible(bool visible)
private static ToolbarPanel? PanelOf(DependencyObject element)
{
for (DependencyObject? d = element; d != null; d = VisualTreeHelper.GetParent(d))
{
if (d is ToolbarPanel panel)
{
return panel;
}
}
return null;
}
private static void SetActionLabelsVisible(ToolbarPanel panel, IEnumerable<TextBlock> labels, bool visible)
{
Visibility visibility = visible ? Visibility.Visible : Visibility.Collapsed;
foreach (TextBlock label in new[] { NewLabel, ClearLabel, ScreencastLabel, SaveLabel, UploadLabel, CopyLabel })
foreach (TextBlock label in labels)
{
label.Visibility = visibility;
// Anders geeft een directe Measure() van het paneel nog de gecachete knopbreedtes terug.
for (DependencyObject? d = label; d != null && d != ToolbarItems; d = VisualTreeHelper.GetParent(d))
for (DependencyObject? d = label; d != null && d != panel; d = VisualTreeHelper.GetParent(d))
{
(d as UIElement)?.InvalidateMeasure();
}
}
ToolbarItems.InvalidateMeasure();
panel.InvalidateMeasure();
}
private void ToolbarItems_OrderChanged(object? sender, EventArgs e)
{
_settings.ToolbarOrder = ToolbarItems.Items.Select(ItemId).ToList();
if (IsSplitLayout)
{
_settings.SplitToolbarOrder = AllToolbarItems.Select(ItemId).ToList();
}
else
{
_settings.ToolbarOrder = ToolbarItems.Items.Select(ItemId).ToList();
}
UpdateToolbarLayout();
}
private void SetEditorLayout(EditorLayout layout)
{
_settings.EditorLayout = layout;
ApplyToolbarLayout();
}
private void Toolbar_ContextMenuOpening(object sender, ContextMenuEventArgs e)
{
ToolbarMenu.Items.Clear();
var bar = (Border)sender;
ToolbarPanel panel = bar == ActionBar ? ActionItems : ToolbarItems;
ContextMenu menu = bar.ContextMenu;
menu.Items.Clear();
var lockItem = new MenuItem { Header = "Werkbalk vergrendelen", IsCheckable = true, IsChecked = _settings.ToolbarLocked };
lockItem.Click += (_, _) => SetToolbarLocked(lockItem.IsChecked);
ToolbarMenu.Items.Add(lockItem);
menu.Items.Add(lockItem);
var position = new MenuItem { Header = "Positie" };
foreach ((Dock dock, string label) in new[] { (Dock.Top, "Boven"), (Dock.Bottom, "Onder"), (Dock.Left, "Links"), (Dock.Right, "Rechts") })
var layout = new MenuItem { Header = "Indeling" };
foreach ((EditorLayout value, string label) in new[]
{
(EditorLayout.Single, "Eén werkbalk"),
(EditorLayout.Split, "Gesplitst: gereedschappen boven, opslaan onder (zoals ShareX)")
})
{
var item = new MenuItem { Header = label, IsCheckable = true, IsChecked = _settings.ToolbarPosition == dock };
item.Click += (_, _) =>
{
_settings.ToolbarPosition = dock;
ApplyToolbarPosition();
};
position.Items.Add(item);
var item = new MenuItem { Header = label, IsCheckable = true, IsChecked = _settings.EditorLayout == value };
item.Click += (_, _) => SetEditorLayout(value);
layout.Items.Add(item);
}
ToolbarMenu.Items.Add(position);
menu.Items.Add(layout);
var swap = new MenuItem { Header = "Groepen omwisselen" };
swap.Click += (_, _) => SwapToolbarGroups();
ToolbarMenu.Items.Add(swap);
if (!IsSplitLayout)
{
var position = new MenuItem { Header = "Positie" };
foreach ((Dock dock, string label) in new[] { (Dock.Top, "Boven"), (Dock.Bottom, "Onder"), (Dock.Left, "Links"), (Dock.Right, "Rechts") })
{
var item = new MenuItem { Header = label, IsCheckable = true, IsChecked = _settings.ToolbarPosition == dock };
item.Click += (_, _) =>
{
_settings.ToolbarPosition = dock;
ApplyToolbarPosition();
};
position.Items.Add(item);
}
menu.Items.Add(position);
}
if (panel.Items.Any(i => i is ToolbarSpacer))
{
var swap = new MenuItem { Header = "Groepen omwisselen" };
swap.Click += (_, _) => SwapToolbarGroups(panel);
menu.Items.Add(swap);
}
var buttons = new MenuItem { Header = "Knoppen tonen" };
foreach (UIElement element in ToolbarItems.Items)
foreach (UIElement element in AllToolbarItems)
{
if (ToolbarPanel.GetTitle(element) is not { } title || (element == UploadButton && !CanUpload))
{
@@ -287,12 +396,12 @@ public partial class EditorWindow : Window
buttons.Items.Add(item);
}
ToolbarMenu.Items.Add(buttons);
ToolbarMenu.Items.Add(new Separator());
menu.Items.Add(buttons);
menu.Items.Add(new Separator());
var reset = new MenuItem { Header = "Standaard herstellen" };
reset.Click += (_, _) => ResetToolbar();
ToolbarMenu.Items.Add(reset);
menu.Items.Add(reset);
}
private void SetToolbarLocked(bool locked)
@@ -315,49 +424,50 @@ public partial class EditorWindow : Window
}
/// <summary>De groepen tussen de flexibele ruimtes in omgekeerde volgorde zetten; een middengroep blijft in het midden.</summary>
private void SwapToolbarGroups()
private void SwapToolbarGroups(ToolbarPanel panel)
{
var groups = new List<List<string>> { new() };
var spacers = new List<string>();
foreach (UIElement item in ToolbarItems.Items)
var groups = new List<List<UIElement>> { new() };
var spacers = new List<UIElement>();
foreach (UIElement item in panel.Items)
{
if (item is ToolbarSpacer)
{
spacers.Add(ItemId(item));
groups.Add(new List<string>());
spacers.Add(item);
groups.Add(new List<UIElement>());
}
else
{
groups[^1].Add(ItemId(item));
groups[^1].Add(item);
}
}
groups.Reverse();
var order = new List<string>(groups[0]);
var order = new List<UIElement>(groups[0]);
for (int i = 0; i < spacers.Count; i++)
{
order.Add(spacers[i]);
order.AddRange(groups[i + 1]);
}
ApplyToolbarOrder(order);
panel.Children.Clear();
order.ForEach(item => panel.Children.Add(item));
ToolbarItems_OrderChanged(this, EventArgs.Empty);
}
private void ResetToolbar()
{
_settings.ToolbarOrder = new List<string>();
_settings.SplitToolbarOrder = new List<string>();
_settings.ToolbarHidden = new List<string>();
_settings.ToolbarPosition = Dock.Top;
_settings.ToolbarLocked = true;
ApplyToolbarOrder(_defaultToolbarOrder);
foreach (UIElement item in ToolbarItems.Items)
ApplyToolbarLayout();
foreach (UIElement item in _toolbarItems.Values)
{
item.Visibility = Visibility.Visible;
}
ApplyUploadButton();
ApplyToolbarPosition();
ApplyToolbarLock();
}
@@ -365,7 +475,7 @@ public partial class EditorWindow : Window
private void SelectTool(ToolKind tool)
{
foreach (RadioButton button in ToolbarItems.Items.OfType<RadioButton>())
foreach (RadioButton button in _toolbarItems.Values.OfType<RadioButton>())
{
if ((string)button.Tag == tool.ToString())
{
@@ -934,6 +1044,7 @@ public partial class EditorWindow : Window
Visibility visibility = _toolbarsHidden ? Visibility.Collapsed : Visibility.Visible;
Toolbar.Visibility = visibility;
PropsBar.Visibility = visibility;
ActionBar.Visibility = IsSplitLayout ? visibility : Visibility.Collapsed;
if (_toolbarsHidden)
{
ShowTemporaryHint("Werkbalken verborgen. Ctrl+H of rechtsklik op het canvas om ze terug te halen.");
@@ -1508,6 +1619,11 @@ public partial class EditorWindow : Window
protected override void OnActivated(EventArgs e)
{
base.OnActivated(e);
if (_appliedLayout != _settings.EditorLayout)
{
ApplyToolbarLayout();
}
ApplySaveButton();
ApplyUploadButton();
}
@@ -1541,6 +1657,7 @@ public partial class EditorWindow : Window
{
using System.IO.MemoryStream png = ImageExporter.EncodePng(image);
string url = await ZiplineUploader.UploadAsync(config, png, $"Knipsel {DateTime.Now:yyyy-MM-dd HHmmss}.png", "image/png");
Kniip.History.HistoryStore.Add(Kniip.History.HistoryKind.Snip, null, url, image);
_dirty = false;
_exported = true;
ShowTemporaryHint(ClipboardWriter.SetText(url)
@@ -1637,6 +1754,7 @@ public partial class EditorWindow : Window
App.Instance.Capture.IgnorePath(path);
BitmapSource image = DocumentRenderer.Export(_doc);
ImageExporter.Save(image, path);
Kniip.History.HistoryStore.Add(Kniip.History.HistoryKind.Snip, path, null, image);
_dirty = false;
_exported = true;
string name = System.IO.Path.GetFileName(path);
+102
View File
@@ -0,0 +1,102 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Windows;
using Kniip.Editor;
using Kniip.Infrastructure;
namespace Kniip.History;
/// <param name="Glyph">Teken uit Segoe Fluent Icons.</param>
public sealed record HistoryCommand(string Label, string Glyph, bool Enabled, Action Run);
/// <summary>Wat je met een item uit de geschiedenis kunt doen, gedeeld door het systeemvakmenu en het geschiedenisvenster.</summary>
public static class HistoryActions
{
/// <summary>De acties voor een item; null staat voor een scheidingslijn.</summary>
public static IReadOnlyList<HistoryCommand?> CommandsFor(HistoryEntry entry)
{
bool file = entry.FileExists;
bool url = entry.Url is { Length: > 0 };
bool image = file && entry.Kind == HistoryKind.Snip;
return new HistoryCommand?[]
{
new("Openen", "\uE8E5", file || url, () => Open(entry)),
new("Openen in de editor", "\uE70F", image, () => App.Instance.OpenFile(entry.FilePath!)),
new("Link openen", "\uE774", url, () => Launch(entry.Url!)),
null,
new("Link kopiëren", "\uE71B", url, () => Copy(entry.Url!)),
new("Afbeelding kopiëren", "\uE8C8", image, () => CopyImage(entry.FilePath!)),
new("Pad kopiëren", "\uE8C8", file, () => Copy(entry.FilePath!)),
new("Tonen in map", "\uE838", file, () => ShowInFolder(entry.FilePath!))
};
}
/// <summary>Bestand in het standaardprogramma, anders de link in de browser.</summary>
public static void Open(HistoryEntry entry)
{
if (entry.FileExists)
{
Launch(entry.FilePath!);
}
else if (entry.Url is { Length: > 0 } url)
{
Launch(url);
}
else
{
MessageBox.Show($"Het bestand bestaat niet meer:\n{entry.FilePath}", "kniip", MessageBoxButton.OK, MessageBoxImage.Information);
}
}
public static void OpenFolder(string folder)
{
Directory.CreateDirectory(folder);
Launch(folder);
}
private static void ShowInFolder(string path) =>
Run(() => Process.Start("explorer.exe", $"/select,\"{path}\""));
private static void Launch(string target) =>
Run(() => Process.Start(new ProcessStartInfo(target) { UseShellExecute = true }));
private static void Copy(string text)
{
if (!ClipboardWriter.SetText(text))
{
ClipboardBusy();
}
}
private static void CopyImage(string path)
{
try
{
if (!ClipboardWriter.SetImage(PixelUtil.LoadFile(path), path))
{
ClipboardBusy();
}
}
catch (Exception ex)
{
MessageBox.Show($"Kan '{Path.GetFileName(path)}' niet kopiëren.\n\n{ex.Message}", "kniip", MessageBoxButton.OK, MessageBoxImage.Warning);
}
}
private static void ClipboardBusy() =>
MessageBox.Show("Kopiëren is mislukt: het klembord is bezet. Probeer het opnieuw.", "kniip", MessageBoxButton.OK, MessageBoxImage.Warning);
private static void Run(Action action)
{
try
{
action();
}
catch (Exception ex)
{
MessageBox.Show($"Openen is mislukt.\n\n{ex.Message}", "kniip", MessageBoxButton.OK, MessageBoxImage.Warning);
}
}
}
+47
View File
@@ -0,0 +1,47 @@
using System;
using System.IO;
using System.Text.Json.Serialization;
namespace Kniip.History;
public enum HistoryKind
{
Snip,
Mp4,
Gif
}
public sealed class HistoryEntry
{
public string Id { get; set; } = Guid.NewGuid().ToString("N");
public DateTime Time { get; set; } = DateTime.Now;
public HistoryKind Kind { get; set; }
/// <summary>Opgeslagen bestand; leeg als het alleen is geüpload.</summary>
public string? FilePath { get; set; }
/// <summary>Link na het uploaden; leeg als het alleen is opgeslagen.</summary>
public string? Url { get; set; }
/// <summary>Bestandsnaam van de miniatuur in <see cref="HistoryStore.ThumbnailFolder"/>.</summary>
public string? Thumbnail { get; set; }
public int Width { get; set; }
public int Height { get; set; }
[JsonIgnore]
public string Name => FilePath is { Length: > 0 }
? Path.GetFileName(FilePath)
: Url is { Length: > 0 } ? Url[(Url.TrimEnd('/').LastIndexOf('/') + 1)..] : "";
[JsonIgnore]
public string KindText => Kind switch
{
HistoryKind.Mp4 => "Screencast (MP4)",
HistoryKind.Gif => "Screencast (GIF)",
_ => "Knipsel"
};
[JsonIgnore]
public bool FileExists => FilePath is { Length: > 0 } && File.Exists(FilePath);
}
+169
View File
@@ -0,0 +1,169 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using Kniip.Infrastructure;
using Kniip.Settings;
namespace Kniip.History;
/// <summary>Wat er in kniip is opgeslagen of geüpload, nieuwste eerst.</summary>
public static class HistoryStore
{
private const int MaxEntries = 500;
private const int ThumbnailSize = 240;
private static readonly JsonSerializerOptions Options = new()
{
Converters = { new JsonStringEnumConverter() }
};
private static List<HistoryEntry>? _entries;
private static string FilePath => Path.Combine(SettingsStore.Folder, "history.json");
public static string ThumbnailFolder => Path.Combine(SettingsStore.Folder, "history");
public static event Action? Changed;
public static IReadOnlyList<HistoryEntry> Entries => Load();
public static void Add(HistoryKind kind, string? filePath, string? url, BitmapSource? preview)
{
var entry = new HistoryEntry { Kind = kind, FilePath = filePath, Url = url };
if (preview != null)
{
entry.Width = preview.PixelWidth;
entry.Height = preview.PixelHeight;
entry.Thumbnail = SaveThumbnail(entry.Id, preview);
}
List<HistoryEntry> entries = Load();
entries.Insert(0, entry);
while (entries.Count > MaxEntries)
{
DeleteThumbnail(entries[^1]);
entries.RemoveAt(entries.Count - 1);
}
Save();
}
public static void Remove(HistoryEntry entry)
{
if (Load().Remove(entry))
{
DeleteThumbnail(entry);
Save();
}
}
public static void Clear()
{
foreach (HistoryEntry entry in Load())
{
DeleteThumbnail(entry);
}
Load().Clear();
Save();
}
public static string? ThumbnailPath(HistoryEntry entry) =>
entry.Thumbnail is { Length: > 0 } name ? Path.Combine(ThumbnailFolder, name) : null;
/// <summary>Eerste beeld van een afbeelding of GIF, of null als dat niet lukt.</summary>
public static BitmapSource? PreviewOf(string file)
{
try
{
return PixelUtil.LoadFile(file);
}
catch (Exception)
{
return null;
}
}
private static List<HistoryEntry> Load()
{
if (_entries != null)
{
return _entries;
}
try
{
if (File.Exists(FilePath))
{
_entries = JsonSerializer.Deserialize<List<HistoryEntry>>(File.ReadAllText(FilePath), Options);
}
}
catch (Exception)
{
// Beschadigd bestand: met een lege geschiedenis verder.
}
_entries ??= new List<HistoryEntry>();
_entries.RemoveAll(entry => entry == null!);
return _entries;
}
private static void Save()
{
try
{
Directory.CreateDirectory(SettingsStore.Folder);
File.WriteAllText(FilePath, JsonSerializer.Serialize(Load(), Options));
}
catch (Exception)
{
// De geschiedenis bijwerken mag opslaan of uploaden nooit laten mislukken.
}
Changed?.Invoke();
}
private static string? SaveThumbnail(string id, BitmapSource image)
{
try
{
double scale = Math.Min(1.0, (double)ThumbnailSize / Math.Max(image.PixelWidth, image.PixelHeight));
BitmapSource thumb = scale < 1 ? new TransformedBitmap(image, new ScaleTransform(scale, scale)) : image;
var encoder = new PngBitmapEncoder();
encoder.Frames.Add(BitmapFrame.Create(thumb));
Directory.CreateDirectory(ThumbnailFolder);
string name = id + ".png";
using var stream = new FileStream(Path.Combine(ThumbnailFolder, name), FileMode.Create, FileAccess.Write);
encoder.Save(stream);
return name;
}
catch (Exception)
{
return null;
}
}
private static void DeleteThumbnail(HistoryEntry entry)
{
if (ThumbnailPath(entry) is not { } path)
{
return;
}
try
{
File.Delete(path);
}
catch (Exception)
{
// Blijft staan; een losse miniatuur kan geen kwaad.
}
}
public static IEnumerable<HistoryEntry> Recent(int count) => Load().Take(count);
}
+135
View File
@@ -0,0 +1,135 @@
<Window x:Class="Kniip.History.HistoryWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="kniip geschiedenis" Width="980" Height="620" MinWidth="560" MinHeight="360"
WindowStartupLocation="CenterScreen" Background="{StaticResource WindowBrush}"
FontFamily="Segoe UI" FontSize="13" Icon="pack://application:,,,/AppIcon.ico">
<Window.Resources>
<Style x:Key="ViewToggle" TargetType="RadioButton" BasedOn="{StaticResource ToolToggle}">
<Setter Property="Padding" Value="10,4" />
</Style>
<Style x:Key="RowStyle" TargetType="ListViewItem">
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
<EventSetter Event="MouseDoubleClick" Handler="Item_DoubleClick" />
<Style.Triggers>
<DataTrigger Binding="{Binding IsAvailable}" Value="False">
<Setter Property="Opacity" Value="0.5" />
</DataTrigger>
</Style.Triggers>
</Style>
<Style x:Key="TileStyle" TargetType="ListBoxItem">
<Setter Property="Padding" Value="0" />
<Setter Property="Margin" Value="4" />
<EventSetter Event="MouseDoubleClick" Handler="Item_DoubleClick" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ListBoxItem">
<Border x:Name="Bd" Background="White" BorderBrush="#E0E0E0" BorderThickness="1" CornerRadius="6" Padding="6">
<ContentPresenter />
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Bd" Property="Background" Value="#F5F5F5" />
</Trigger>
<Trigger Property="IsSelected" Value="True">
<Setter TargetName="Bd" Property="Background" Value="#1A0067C0" />
<Setter TargetName="Bd" Property="BorderBrush" Value="{StaticResource AccentBrush}" />
</Trigger>
<DataTrigger Binding="{Binding IsAvailable}" Value="False">
<Setter Property="Opacity" Value="0.5" />
</DataTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<DataTemplate x:Key="SmallThumb">
<Grid Width="40" Height="28">
<Image Source="{Binding Thumbnail}" Stretch="Uniform" RenderOptions.BitmapScalingMode="HighQuality" />
<TextBlock Text="&#xE714;" Style="{StaticResource IconText}" Foreground="{StaticResource SubtleTextBrush}"
Visibility="{Binding NoThumbnailVisibility}" />
</Grid>
</DataTemplate>
</Window.Resources>
<DockPanel Margin="12">
<DockPanel DockPanel.Dock="Top" Margin="0,0,0,10" LastChildFill="False">
<RadioButton x:Name="ListToggle" Style="{StaticResource ViewToggle}" GroupName="View" Checked="View_Checked">
<StackPanel Orientation="Horizontal">
<TextBlock Text="&#xE8FD;" Style="{StaticResource IconText}" Margin="0,0,6,0" />
<TextBlock Text="Lijst" VerticalAlignment="Center" />
</StackPanel>
</RadioButton>
<RadioButton x:Name="GridToggle" Style="{StaticResource ViewToggle}" GroupName="View" Checked="View_Checked">
<StackPanel Orientation="Horizontal">
<TextBlock Text="&#xE91B;" Style="{StaticResource IconText}" Margin="0,0,6,0" />
<TextBlock Text="Miniaturen" VerticalAlignment="Center" />
</StackPanel>
</RadioButton>
<Grid DockPanel.Dock="Right" Width="260" VerticalAlignment="Center">
<TextBox x:Name="SearchBox" Height="30" Padding="26,0,6,0" VerticalContentAlignment="Center"
TextChanged="SearchBox_TextChanged" />
<TextBlock Text="&#xE721;" Style="{StaticResource IconText}" FontSize="13" HorizontalAlignment="Left" Margin="8,0,0,0"
Foreground="{StaticResource SubtleTextBrush}" IsHitTestVisible="False" />
<TextBlock x:Name="SearchHint" Text="Zoeken op naam of link" Margin="27,0,0,0" VerticalAlignment="Center"
Foreground="{StaticResource SubtleTextBrush}" IsHitTestVisible="False" />
</Grid>
</DockPanel>
<DockPanel DockPanel.Dock="Bottom" Margin="0,10,0,0" LastChildFill="False">
<TextBlock x:Name="CountText" VerticalAlignment="Center" Foreground="{StaticResource SubtleTextBrush}" />
<Button DockPanel.Dock="Right" Content="Sluiten" Style="{StaticResource SecondaryButton}" IsCancel="True" Click="Close_Click" />
<Button x:Name="ClearButton" DockPanel.Dock="Right" Content="Geschiedenis wissen..." Style="{StaticResource SecondaryButton}"
Click="Clear_Click" />
</DockPanel>
<Grid>
<ListView x:Name="ListItems" ItemContainerStyle="{StaticResource RowStyle}" SelectionMode="Single"
BorderBrush="#E0E0E0" ContextMenuOpening="Items_ContextMenuOpening">
<ListView.ContextMenu>
<ContextMenu />
</ListView.ContextMenu>
<ListView.View>
<GridView>
<GridViewColumn Width="56" CellTemplate="{StaticResource SmallThumb}" />
<GridViewColumn Header="Datum" Width="140" DisplayMemberBinding="{Binding TimeText}" />
<GridViewColumn Header="Soort" Width="120" DisplayMemberBinding="{Binding Entry.KindText}" />
<GridViewColumn Header="Naam" Width="200" DisplayMemberBinding="{Binding Entry.Name}" />
<GridViewColumn Header="Map" Width="200" DisplayMemberBinding="{Binding Folder}" />
<GridViewColumn Header="Link" Width="220" DisplayMemberBinding="{Binding Entry.Url}" />
</GridView>
</ListView.View>
</ListView>
<ListBox x:Name="GridItems" ItemContainerStyle="{StaticResource TileStyle}" SelectionMode="Single" Visibility="Collapsed"
Background="Transparent" BorderThickness="0" ScrollViewer.HorizontalScrollBarVisibility="Disabled"
ContextMenuOpening="Items_ContextMenuOpening">
<ListBox.ContextMenu>
<ContextMenu />
</ListBox.ContextMenu>
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Width="180" ToolTip="{Binding ToolTip}">
<Grid Height="120" Background="#F3F3F3">
<Image Source="{Binding Thumbnail}" Stretch="Uniform" RenderOptions.BitmapScalingMode="HighQuality" />
<TextBlock Text="&#xE714;" Style="{StaticResource IconText}" FontSize="32"
Foreground="{StaticResource SubtleTextBrush}" Visibility="{Binding NoThumbnailVisibility}" />
</Grid>
<TextBlock Text="{Binding Entry.Name}" Margin="0,6,0,0" TextTrimming="CharacterEllipsis" />
<TextBlock Text="{Binding TimeText}" FontSize="12" Foreground="{StaticResource SubtleTextBrush}" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<TextBlock x:Name="EmptyText" HorizontalAlignment="Center" VerticalAlignment="Center" TextAlignment="Center"
Foreground="{StaticResource SubtleTextBrush}" Visibility="Collapsed"
Text="Nog niets in de geschiedenis.&#x0a;Wat je in kniip opslaat of uploadt, komt hier te staan." />
</Grid>
</DockPanel>
</Window>
+173
View File
@@ -0,0 +1,173 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
namespace Kniip.History;
public partial class HistoryWindow : Window
{
private List<HistoryRow> _rows = new();
public HistoryWindow(bool images)
{
InitializeComponent();
Reload();
ShowImages(images);
HistoryStore.Changed += Reload;
Closed += (_, _) => HistoryStore.Changed -= Reload;
}
public void ShowImages(bool images)
{
(images ? GridToggle : ListToggle).IsChecked = true;
}
private void Reload()
{
_rows = HistoryStore.Entries.Select(entry => new HistoryRow(entry)).ToList();
ListItems.ItemsSource = _rows;
GridItems.ItemsSource = _rows;
ApplyFilter();
}
private void ApplyFilter()
{
string query = SearchBox.Text.Trim();
ICollectionView view = System.Windows.Data.CollectionViewSource.GetDefaultView(_rows);
view.Filter = query.Length == 0 ? null : item => ((HistoryRow)item).Matches(query);
int shown = view.Cast<object>().Count();
CountText.Text = query.Length == 0 ? $"{_rows.Count} items" : $"{shown} van {_rows.Count} items";
EmptyText.Visibility = _rows.Count == 0 ? Visibility.Visible : Visibility.Collapsed;
ClearButton.IsEnabled = _rows.Count > 0;
}
private void View_Checked(object sender, RoutedEventArgs e)
{
bool images = GridToggle.IsChecked == true;
GridItems.Visibility = images ? Visibility.Visible : Visibility.Collapsed;
ListItems.Visibility = images ? Visibility.Collapsed : Visibility.Visible;
Title = images ? "kniip afbeeldingsgeschiedenis" : "kniip geschiedenis";
}
private void SearchBox_TextChanged(object sender, TextChangedEventArgs e)
{
SearchHint.Visibility = SearchBox.Text.Length == 0 ? Visibility.Visible : Visibility.Collapsed;
ApplyFilter();
}
private void Item_DoubleClick(object sender, MouseButtonEventArgs e)
{
if (sender is FrameworkElement { DataContext: HistoryRow row })
{
HistoryActions.Open(row.Entry);
}
}
private void Items_ContextMenuOpening(object sender, ContextMenuEventArgs e)
{
var list = (Selector)sender;
if (e.OriginalSource is DependencyObject source
&& ItemsControl.ContainerFromElement(list, source) is FrameworkElement { DataContext: HistoryRow clicked })
{
list.SelectedItem = clicked;
}
if (list.SelectedItem is not HistoryRow row || list.ContextMenu is not { } menu)
{
e.Handled = true;
return;
}
menu.Items.Clear();
foreach (HistoryCommand? command in HistoryActions.CommandsFor(row.Entry))
{
menu.Items.Add(command == null ? new Separator() : MenuItemFor(command.Label, command.Glyph, command.Enabled, command.Run));
}
menu.Items.Add(new Separator());
menu.Items.Add(MenuItemFor("Uit geschiedenis verwijderen", "\uE74D", true, () => HistoryStore.Remove(row.Entry)));
}
private static MenuItem MenuItemFor(string label, string glyph, bool enabled, Action run)
{
var item = new MenuItem
{
Header = label,
IsEnabled = enabled,
Icon = new TextBlock { Text = glyph, Style = (Style)Application.Current.FindResource("IconText"), FontSize = 14 }
};
item.Click += (_, _) => run();
return item;
}
private void Clear_Click(object sender, RoutedEventArgs e)
{
if (MessageBox.Show(this, "De hele geschiedenis wissen? Je opgeslagen bestanden en uploads blijven gewoon bestaan.", "kniip",
MessageBoxButton.YesNo, MessageBoxImage.Question, MessageBoxResult.No) == MessageBoxResult.Yes)
{
HistoryStore.Clear();
}
}
private void Close_Click(object sender, RoutedEventArgs e) => Close();
private sealed class HistoryRow
{
public HistoryRow(HistoryEntry entry)
{
Entry = entry;
IsAvailable = entry.FileExists || entry.Url is { Length: > 0 };
Thumbnail = LoadThumbnail(entry);
}
public HistoryEntry Entry { get; }
public bool IsAvailable { get; }
public ImageSource? Thumbnail { get; }
public Visibility NoThumbnailVisibility => Thumbnail == null ? Visibility.Visible : Visibility.Collapsed;
public string TimeText => Entry.Time.ToString("dd-MM-yyyy HH:mm:ss");
public string? Folder => Entry.FilePath is { Length: > 0 } path ? Path.GetDirectoryName(path) : null;
public string ToolTip => string.Join("\n", new[]
{
Entry.KindText + (Entry.Width > 0 ? $", {Entry.Width} x {Entry.Height}" : ""),
Entry.FilePath,
Entry.Url
}.Where(line => !string.IsNullOrEmpty(line)));
public bool Matches(string query) =>
Entry.Name.Contains(query, StringComparison.OrdinalIgnoreCase)
|| (Entry.FilePath?.Contains(query, StringComparison.OrdinalIgnoreCase) ?? false)
|| (Entry.Url?.Contains(query, StringComparison.OrdinalIgnoreCase) ?? false);
private static ImageSource? LoadThumbnail(HistoryEntry entry)
{
if (HistoryStore.ThumbnailPath(entry) is not { } path || !File.Exists(path))
{
return null;
}
try
{
var image = new BitmapImage();
image.BeginInit();
image.CacheOption = BitmapCacheOption.OnLoad;
image.StreamSource = new MemoryStream(File.ReadAllBytes(path));
image.EndInit();
image.Freeze();
return image;
}
catch (Exception)
{
return null;
}
}
}
}
+202
View File
@@ -1,6 +1,10 @@
using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Windows.Forms;
using Kniip.History;
namespace Kniip.Infrastructure;
@@ -15,6 +19,16 @@ internal sealed class TrayIcon : IDisposable
private readonly ToolStripMenuItem _windowItem;
private readonly ToolStripMenuItem _recordItem;
private readonly ToolStripMenuItem _gifItem;
private readonly ToolStripMenuItem _foldersItem;
private readonly ToolStripMenuItem _recentItem;
private readonly ToolStripMenuItem _delayItem;
private TrackBar _delaySlider = null!;
private NumericUpDown _delayNumber = null!;
private int _delaySeconds;
private int _savedDelaySeconds;
/// <summary>Nieuwe vertraging in seconden, afgevuurd bij het sluiten van het submenu als de waarde is gewijzigd.</summary>
public event Action<int>? CaptureDelayChanged;
public event Action? OpenFromClipboardRequested;
public event Action? SnipRequested;
@@ -26,6 +40,12 @@ internal sealed class TrayIcon : IDisposable
public event Action? ExitRequested;
public event Action<bool>? PauseChanged;
/// <summary>Geschiedenisvenster openen; true = met miniaturen.</summary>
public event Action<bool>? HistoryRequested;
/// <summary>Mappen voor het submenu Map openen, opgevraagd bij het openen; een pad van null staat grijs.</summary>
public Func<IReadOnlyList<(string Label, string? Path)>>? FoldersProvider { get; set; }
public TrayIcon()
{
var menu = new ContextMenuStrip();
@@ -40,12 +60,26 @@ internal sealed class TrayIcon : IDisposable
menu.Items.Add(_snipItem);
_windowItem = new ToolStripMenuItem("Venster knippen", null, (_, _) => WindowSnipRequested?.Invoke());
menu.Items.Add(_windowItem);
_delayItem = BuildDelayItem();
menu.Items.Add(_delayItem);
_recordItem = new ToolStripMenuItem(RecordText, null, (_, _) => RecordRequested?.Invoke());
menu.Items.Add(_recordItem);
_gifItem = new ToolStripMenuItem("Screencast opnemen (GIF)...", null, (_, _) => GifRequested?.Invoke());
menu.Items.Add(_gifItem);
menu.Items.Add(new ToolStripSeparator());
_foldersItem = new ToolStripMenuItem("Map openen");
_foldersItem.DropDownItems.Add(new ToolStripMenuItem("..."));
_foldersItem.DropDownOpening += (_, _) => BuildFolders();
menu.Items.Add(_foldersItem);
menu.Items.Add("Geschiedenis...", null, (_, _) => HistoryRequested?.Invoke(false));
menu.Items.Add("Afbeeldingsgeschiedenis...", null, (_, _) => HistoryRequested?.Invoke(true));
_recentItem = new ToolStripMenuItem("Recente items");
_recentItem.DropDownItems.Add(new ToolStripMenuItem("..."));
_recentItem.DropDownOpening += (_, _) => BuildRecent();
menu.Items.Add(_recentItem);
menu.Items.Add(new ToolStripSeparator());
_pauseItem = new ToolStripMenuItem("Knipsels niet automatisch oppikken") { CheckOnClick = true };
_pauseItem.CheckedChanged += (_, _) => PauseChanged?.Invoke(_pauseItem.Checked);
menu.Items.Add(_pauseItem);
@@ -89,10 +123,178 @@ internal sealed class TrayIcon : IDisposable
_icon.Text = recording ? $"{AppIdentity.DisplayName} - opname loopt" : IdleText;
}
public void SetCaptureDelay(int seconds)
{
_savedDelaySeconds = seconds;
ShowCaptureDelay(seconds);
}
private ToolStripMenuItem BuildDelayItem()
{
var item = new ToolStripMenuItem();
_delaySlider = new TrackBar
{
Minimum = 0,
Maximum = Kniip.Settings.AppSettings.MaxCaptureDelaySeconds,
TickFrequency = 5,
LargeChange = 5,
SmallChange = 1,
AutoSize = false,
Width = 200,
Height = 32,
Location = new Point(4, 6)
};
_delayNumber = new NumericUpDown
{
Minimum = 0,
Maximum = Kniip.Settings.AppSettings.MaxCaptureDelaySeconds,
Width = 52,
Location = new Point(_delaySlider.Right + 6, 10),
TextAlign = HorizontalAlignment.Right
};
var unit = new Label
{
Text = "seconden",
AutoSize = true,
Location = new Point(_delayNumber.Right + 4, 13)
};
var panel = new Panel
{
Size = new Size(unit.Left + unit.PreferredWidth + 10, 44),
BackColor = ProfessionalColors.ToolStripDropDownBackground
};
_delaySlider.BackColor = panel.BackColor;
panel.Controls.AddRange(new Control[] { _delaySlider, _delayNumber, unit });
_delaySlider.ValueChanged += (_, _) => ShowCaptureDelay(_delaySlider.Value);
_delayNumber.ValueChanged += (_, _) => ShowCaptureDelay((int)_delayNumber.Value);
_delayNumber.KeyDown += (_, e) =>
{
if (e.KeyCode is Keys.Enter or Keys.Escape)
{
e.SuppressKeyPress = true;
_icon.ContextMenuStrip?.Close();
}
};
item.DropDownItems.Add(new ToolStripControlHost(panel) { AutoSize = false, Size = panel.Size, Margin = Padding.Empty });
item.DropDownClosed += (_, _) =>
{
ShowCaptureDelay((int)_delayNumber.Value);
if (_delaySeconds != _savedDelaySeconds)
{
_savedDelaySeconds = _delaySeconds;
CaptureDelayChanged?.Invoke(_delaySeconds);
}
};
item.Text = "Schermafdruk vertraging: 0s";
return item;
}
private void ShowCaptureDelay(int seconds)
{
_delaySeconds = seconds;
if (_delaySlider.Value != seconds)
{
_delaySlider.Value = seconds;
}
if ((int)_delayNumber.Value != seconds)
{
_delayNumber.Value = seconds;
}
_delayItem.Text = $"Schermafdruk vertraging: {seconds}s";
}
private static string IdleText => AppIdentity.IsTest
? $"{AppIdentity.DisplayName} {Kniip.Update.UpdateChecker.CurrentVersion}"
: "kniip - annotaties voor het Knipprogramma";
private void BuildFolders()
{
_foldersItem.DropDownItems.Clear();
foreach ((string label, string? path) in FoldersProvider?.Invoke() ?? Array.Empty<(string, string?)>())
{
var item = new ToolStripMenuItem(label) { Enabled = path != null, ToolTipText = path };
if (path != null)
{
item.Click += (_, _) => HistoryActions.OpenFolder(path);
}
_foldersItem.DropDownItems.Add(item);
}
}
private void BuildRecent()
{
foreach (ToolStripItem old in _recentItem.DropDownItems)
{
old.Image?.Dispose();
}
_recentItem.DropDownItems.Clear();
List<HistoryEntry> entries = HistoryStore.Recent(10).ToList();
if (entries.Count == 0)
{
_recentItem.DropDownItems.Add(new ToolStripMenuItem("Nog niets opgeslagen") { Enabled = false });
return;
}
foreach (HistoryEntry entry in entries)
{
var item = new ToolStripMenuItem($"[{entry.Time:HH:mm:ss}] {entry.Url ?? entry.Name}")
{
Image = LoadThumbnail(entry),
ToolTipText = entry.FilePath
};
item.Click += (_, _) =>
{
_icon.ContextMenuStrip?.Close();
HistoryActions.Open(entry);
};
foreach (HistoryCommand? command in HistoryActions.CommandsFor(entry))
{
if (command == null)
{
item.DropDownItems.Add(new ToolStripSeparator());
}
else
{
item.DropDownItems.Add(new ToolStripMenuItem(command.Label, null, (_, _) => command.Run()) { Enabled = command.Enabled });
}
}
_recentItem.DropDownItems.Add(item);
}
}
private static Image? LoadThumbnail(HistoryEntry entry)
{
if (HistoryStore.ThumbnailPath(entry) is not { } path || !File.Exists(path))
{
return null;
}
try
{
using var stream = new MemoryStream(File.ReadAllBytes(path));
using var image = Image.FromStream(stream);
Size size = SystemInformation.SmallIconSize;
double scale = Math.Min((double)size.Width / image.Width, (double)size.Height / image.Height);
var thumb = new Bitmap(size.Width, size.Height);
using Graphics g = Graphics.FromImage(thumb);
g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
int w = Math.Max(1, (int)(image.Width * scale)), h = Math.Max(1, (int)(image.Height * scale));
g.DrawImage(image, (size.Width - w) / 2, (size.Height - h) / 2, w, h);
return thumb;
}
catch (Exception)
{
return null;
}
}
public void ShowBalloon(string title, string text) =>
_icon.ShowBalloonTip(4000, title, text, ToolTipIcon.Info);
+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.4</Version>
<Version>1.1.5</Version>
<SatelliteResourceLanguages>nl;en</SatelliteResourceLanguages>
<!-- WPF regelt DPI via app.manifest; de WinForms-analyzer is hier niet van toepassing. -->
<NoWarn>$(NoWarn);WFAC010</NoWarn>
+6 -2
View File
@@ -132,9 +132,13 @@ internal sealed class RecordingController
NativeMethods.GetCursorPos(out NativeMethods.POINT cursor);
var cursorPoint = new System.Drawing.Point(cursor.X, cursor.Y);
foreach (Screen screen in Screen.AllScreens)
// Vastleggen voordat de overlays er staan, anders zit hun grijze laag in het vergrootglas.
var frozen = Screen.AllScreens
.Select(screen => (screen, image: _settings.ShowMagnifier ? Kniip.Capture.ScreenGrabber.Capture(screen.Bounds) : null))
.ToList();
foreach ((Screen screen, System.Windows.Media.Imaging.BitmapSource? image) in frozen)
{
var selector = new RegionSelectorWindow(screen, windows);
var selector = new RegionSelectorWindow(screen, windows, image);
selector.Finished += OnRegionChosen;
_selectors.Add(selector);
selector.Show();
@@ -1,6 +1,7 @@
<Window x:Class="Kniip.Recording.RegionSelectorWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:capture="clr-namespace:Kniip.Capture"
Title="kniip opnamegebied kiezen" WindowStyle="None" AllowsTransparency="True" ResizeMode="NoResize"
ShowInTaskbar="False" Topmost="True" Cursor="Cross" FontFamily="Segoe UI" FontSize="13"
Background="#01000000">
@@ -12,6 +13,7 @@
CornerRadius="4" Padding="6,2" Visibility="Collapsed" IsHitTestVisible="False">
<TextBlock x:Name="SizeText" Foreground="White" FontSize="12" />
</Border>
<capture:MagnifierControl x:Name="Magnifier" />
<Border x:Name="Hint" HorizontalAlignment="Center" VerticalAlignment="Top" Margin="0,28,0,0" Background="#F2FFFFFF"
CornerRadius="6" Padding="16,9" IsHitTestVisible="False">
<Border.Effect>
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using Rectangle = System.Drawing.Rectangle;
using Screen = System.Windows.Forms.Screen;
@@ -19,6 +20,7 @@ public partial class RegionSelectorWindow : Window
private readonly Screen _screen;
private readonly IReadOnlyList<Rectangle> _windows;
private readonly BitmapSource? _frozen;
private Point? _dragStart;
private bool _dragging;
private Rectangle? _hover;
@@ -26,15 +28,18 @@ public partial class RegionSelectorWindow : Window
/// <summary>Gekozen gebied, of null bij annuleren.</summary>
internal event Action<RecordingRegion?>? Finished;
internal RegionSelectorWindow(Screen screen, IReadOnlyList<Rectangle> windows)
/// <param name="frozen">Beeld van het scherm voor het vergrootglas; null betekent geen vergrootglas.</param>
internal RegionSelectorWindow(Screen screen, IReadOnlyList<Rectangle> windows, BitmapSource? frozen = null)
{
InitializeComponent();
_screen = screen;
_windows = windows;
_frozen = frozen;
SourceInitialized += (_, _) => PhysicalWindow.Place(this, screen.Bounds);
Loaded += (_, _) => ShowSelection(null);
MouseLeave += (_, _) =>
{
Magnifier.Hide();
if (!_dragging)
{
_hover = null;
@@ -49,6 +54,13 @@ public partial class RegionSelectorWindow : Window
{
base.OnMouseMove(e);
Point position = e.GetPosition(this);
if (_frozen != null)
{
System.Drawing.Point physical = ToPhysical(position);
var relative = new System.Drawing.Point(physical.X - _screen.Bounds.X, physical.Y - _screen.Bounds.Y);
Magnifier.Show(_frozen, relative, physical, position, new Size(ActualWidth, ActualHeight));
}
if (_dragStart is Point start && e.LeftButton == MouseButtonState.Pressed)
{
if (!_dragging && (position - start).Length >= DragThreshold)
@@ -419,6 +419,7 @@ 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);
@@ -524,6 +525,7 @@ public partial class ScreencastWindow : Window
_kept = true;
RememberChoices();
AddToHistory(null, url, target);
bool copied = ClipboardWriter.SetText(url);
SavedRun.Text = copied ? $"Geüpload; de link staat op het klembord: {url} " : $"Geüpload: {url} ";
OpenRun.Text = "Link openen";
@@ -553,6 +555,14 @@ public partial class ScreencastWindow : Window
}
}
/// <param name="exported">Het geëxporteerde bestand, voor de miniatuur van een GIF.</param>
private void AddToHistory(string? file, string? url, string exported)
{
bool gif = Format == ScreencastFormat.Gif;
Kniip.History.HistoryStore.Add(gif ? Kniip.History.HistoryKind.Gif : Kniip.History.HistoryKind.Mp4, file, url,
gif ? Kniip.History.HistoryStore.PreviewOf(exported) : null);
}
private void RememberChoices()
{
_settings.LastScreencastFormat = Format;
+30
View File
@@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using System.Windows.Controls;
using Kniip.Editor;
@@ -32,6 +33,15 @@ public enum SaveButtonChoice
SaveAndCopy
}
public enum EditorLayout
{
/// <summary>Eén werkbalk met gereedschappen en acties.</summary>
Single,
/// <summary>Gereedschappen boven, opslaan/kopiëren en zoom onder (zoals ShareX).</summary>
Split
}
public sealed class AppSettings
{
public bool WatchClipboard { get; set; } = true;
@@ -94,6 +104,12 @@ public sealed class AppSettings
/// <summary>Volgorde van de werkbalkitems (ItemId's); leeg betekent de standaardvolgorde.</summary>
public List<string> ToolbarOrder { get; set; } = new();
/// <summary>Indeling van de werkbalk(en) in de editor.</summary>
public EditorLayout EditorLayout { get; set; } = EditorLayout.Single;
/// <summary>Volgorde van de werkbalkitems in de gesplitste indeling (boven, dan onder); leeg betekent de standaardvolgorde.</summary>
public List<string> SplitToolbarOrder { get; set; } = new();
public List<string> ToolbarHidden { get; set; } = new();
/// <summary>Zipline-server waar Uploaden naartoe gaat; leeg betekent geen uploadknop.</summary>
@@ -102,6 +118,20 @@ public sealed class AppSettings
/// <summary>Bij het starten en daarna dagelijks op de achtergrond kijken of er een nieuwere versie is.</summary>
public bool CheckForUpdates { get; set; } = true;
/// <summary>Vergrootglas met kruisdraad en coördinaten tonen bij het kiezen van een gebied (knipsel en screencast).</summary>
public bool ShowMagnifier { get; set; } = true;
private int _captureDelaySeconds;
/// <summary>Seconden wachten voordat een rechthoek- of vensterknipsel het scherm bevriest (0-60); 0 is direct.</summary>
public int CaptureDelaySeconds
{
get => _captureDelaySeconds;
set => _captureDelaySeconds = Math.Clamp(value, 0, MaxCaptureDelaySeconds);
}
public const int MaxCaptureDelaySeconds = 60;
public StyleSet StyleFor(ToolKind tool)
{
if (!ToolStyles.TryGetValue(tool, out StyleSet? style))
+7
View File
@@ -142,6 +142,8 @@
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)" />
</StackPanel>
</TabItem>
@@ -253,6 +255,11 @@
<StackPanel>
<TextBlock Text="Editor" Style="{StaticResource CardTitle}" />
<CheckBox x:Name="StraightHighlightBox" Content="Markeerstift trekt standaard een rechte horizontale streep (Shift wisselt)" />
<TextBlock Text="Werkbalk" Style="{StaticResource CardTitle}" Margin="0,16,0,6" />
<RadioButton x:Name="LayoutSingleRadio" GroupName="EditorLayout" Content="Eén werkbalk met gereedschappen en acties" />
<RadioButton x:Name="LayoutSplitRadio" GroupName="EditorLayout" Content="Gesplitst zoals ShareX: gereedschappen boven, opslaan onder" />
<TextBlock Style="{StaticResource Hint}"
Text="Ook te wisselen via rechtsklik op de werkbalk in de editor. Daar kun je knoppen ook verslepen of verbergen." />
</StackPanel>
</TabItem>
@@ -97,7 +97,10 @@ public partial class SettingsWindow : Window
ActionOpenRadio.IsChecked = settings.Action == CaptureAction.OpenEditor;
ActionNothingRadio.IsChecked = settings.Action == CaptureAction.Nothing;
StraightHighlightBox.IsChecked = settings.StraightHighlight;
LayoutSplitRadio.IsChecked = settings.EditorLayout == EditorLayout.Split;
LayoutSingleRadio.IsChecked = settings.EditorLayout != EditorLayout.Split;
AutoUpdateCheckBox.IsChecked = settings.CheckForUpdates;
MagnifierBox.IsChecked = settings.ShowMagnifier;
_snipFolder = settings.SnipSaveFolder;
_videoFolder = settings.VideoSaveFolder;
@@ -441,7 +444,9 @@ public partial class SettingsWindow : Window
: ActionNothingRadio.IsChecked == true ? CaptureAction.Nothing
: CaptureAction.Notify;
_settings.StraightHighlight = StraightHighlightBox.IsChecked == true;
_settings.EditorLayout = LayoutSplitRadio.IsChecked == true ? EditorLayout.Split : EditorLayout.Single;
_settings.CheckForUpdates = AutoUpdateCheckBox.IsChecked == true;
_settings.ShowMagnifier = MagnifierBox.IsChecked == true;
StopListening();
_settings.SnipHotkey = _hotkeys["Snip"];
_settings.WindowHotkey = _hotkeys["Window"];
@@ -466,6 +471,7 @@ public partial class SettingsWindow : Window
_settings.ToolbarPosition = defaults.ToolbarPosition;
_settings.ToolbarLocked = defaults.ToolbarLocked;
_settings.ToolbarOrder = defaults.ToolbarOrder;
_settings.SplitToolbarOrder = defaults.SplitToolbarOrder;
_settings.ToolbarHidden = defaults.ToolbarHidden;
}