1 parent
43a2c4f57b
commit
6d19b91963
30 files changed
+1694
-78
No files matched your search
+94
-7
@@ -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()
|
||||
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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="" 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="" 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="" 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="" 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="" 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.
Wat je in kniip opslaat of uploadt, komt hier te staan." />
|
||||
</Grid>
|
||||
</DockPanel>
|
||||
</Window>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user