Initial first version.

This commit is contained in:
Ebbe Baß
2026-08-13 10:04:59 +02:00
parent df725b43c4
commit 940e80b630
22 changed files with 890 additions and 0 deletions
@@ -0,0 +1,114 @@
<Window x:Class="EbbesMemeClipboard.Views.PickerWindow"
x:Name="RootWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:EbbesMemeClipboard.ViewModels"
Title="Meme Clipboard"
Width="420" Height="520"
WindowStyle="None"
AllowsTransparency="True"
Background="Transparent"
ShowInTaskbar="False"
Topmost="True"
ResizeMode="NoResize"
WindowStartupLocation="Manual"
AllowDrop="True"
Deactivated="PickerWindow_OnDeactivated"
KeyDown="PickerWindow_OnKeyDown"
Drop="PickerWindow_OnDrop"
DragOver="PickerWindow_OnDragOver">
<Window.Resources>
<Style x:Key="FlatButtonStyle" TargetType="Button">
<Setter Property="Background" Value="#2B2D31"/>
<Setter Property="Foreground" Value="White"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Cursor" Value="Hand"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border x:Name="Bd" Background="{TemplateBinding Background}" CornerRadius="8" SnapsToDevicePixels="True">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" Margin="{TemplateBinding Padding}"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Bd" Property="Background" Value="#3A3D42"/>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter TargetName="Bd" Property="Background" Value="#454952"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Window.Resources>
<Border Background="#F0202225" CornerRadius="10" BorderBrush="#3A3B3E" BorderThickness="1">
<Grid Margin="12">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid Grid.Row="0" Margin="0,0,0,10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Border Grid.Column="0" Background="#2B2D31" CornerRadius="8">
<TextBox x:Name="SearchBox"
Text="{Binding SearchText, UpdateSourceTrigger=PropertyChanged}"
Background="Transparent" Foreground="White" BorderThickness="0"
Padding="10,7" FontSize="14"
CaretBrush="White"/>
</Border>
<Button Grid.Column="1" Content="+" Width="36" Height="36" Margin="8,0,0,0"
FontSize="18" Style="{StaticResource FlatButtonStyle}"
Command="{Binding ImportFilesCommand}"
ToolTip="Add memes"/>
</Grid>
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto">
<Grid>
<TextBlock Text="No memes yet. Click + or drag files in here."
Foreground="#7B7D85" FontSize="13"
HorizontalAlignment="Center" VerticalAlignment="Center"
TextWrapping="Wrap" TextAlignment="Center" Width="260">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Setter Property="Visibility" Value="Collapsed"/>
<Style.Triggers>
<DataTrigger Binding="{Binding Items.Count}" Value="0">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
<ItemsControl ItemsSource="{Binding Items}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel Orientation="Horizontal"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type vm:MemeTileViewModel}">
<Button Width="88" Height="88" Margin="4" Padding="0"
Style="{StaticResource FlatButtonStyle}"
Command="{Binding DataContext.SelectItemCommand, ElementName=RootWindow}"
CommandParameter="{Binding}"
ToolTip="{Binding DisplayName}">
<Image Source="{Binding Thumbnail}" Stretch="Uniform" Width="80" Height="80"/>
</Button>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
</ScrollViewer>
<TextBlock Grid.Row="2" Text="{Binding StatusMessage}" Foreground="#9A9CA3" FontSize="11" Margin="2,8,0,0"/>
</Grid>
</Border>
</Window>
@@ -0,0 +1,98 @@
using System.ComponentModel;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
using EbbesMemeClipboard.ViewModels;
namespace EbbesMemeClipboard.Views;
public partial class PickerWindow : Window
{
private readonly PickerViewModel _viewModel;
public PickerWindow(PickerViewModel viewModel)
{
InitializeComponent();
_viewModel = viewModel;
DataContext = _viewModel;
_viewModel.RequestClose += (_, _) => Hide();
}
public void ShowNearCursor()
{
PositionNearCursor();
_viewModel.OnShown();
Show();
Activate();
SearchBox.Focus();
}
public void ToggleVisibility()
{
if (IsVisible)
{
Hide();
}
else
{
ShowNearCursor();
}
}
/// <summary>
/// Positions the popup near the mouse cursor, clamped to the current monitor's working
/// area. The real Windows Emoji Panel can follow the text caret because it's a privileged
/// shell component; a third-party app can't replicate that, so cursor position is the
/// practical stand-in. Uses a single DPI scale factor for the conversion from WinForms'
/// physical pixels to WPF's device-independent units, which is exact for same-DPI
/// multi-monitor setups (the common case) and only approximate when the cursor is on a
/// secondary monitor running a different DPI scale than the one this window last rendered on.
/// </summary>
private void PositionNearCursor()
{
var cursor = System.Windows.Forms.Cursor.Position;
var workingArea = System.Windows.Forms.Screen.FromPoint(cursor).WorkingArea;
double scale = VisualTreeHelper.GetDpi(this).DpiScaleX;
double left = cursor.X / scale;
double top = cursor.Y / scale;
double maxLeft = (workingArea.Right / scale) - Width;
double maxTop = (workingArea.Bottom / scale) - Height;
double minLeft = workingArea.Left / scale;
double minTop = workingArea.Top / scale;
Left = Math.Clamp(left, minLeft, Math.Max(minLeft, maxLeft));
Top = Math.Clamp(top, minTop, Math.Max(minTop, maxTop));
}
private void PickerWindow_OnDeactivated(object? sender, EventArgs e) => Hide();
private void PickerWindow_OnKeyDown(object sender, KeyEventArgs e)
{
if (e.Key == Key.Escape) Hide();
}
private void PickerWindow_OnDragOver(object sender, DragEventArgs e)
{
e.Effects = e.Data.GetDataPresent(DataFormats.FileDrop) ? DragDropEffects.Copy : DragDropEffects.None;
e.Handled = true;
}
private async void PickerWindow_OnDrop(object sender, DragEventArgs e)
{
if (e.Data.GetData(DataFormats.FileDrop) is string[] paths)
{
await _viewModel.ImportPathsAsync(paths);
}
}
protected override void OnClosing(CancelEventArgs e)
{
// This window is reused for the app's whole lifetime - closing it (e.g. Alt+F4) should
// just hide it. Real shutdown only happens via the tray icon's Exit command.
e.Cancel = true;
Hide();
base.OnClosing(e);
}
}