Added pasting and sending features
This commit is contained in:
@@ -16,7 +16,9 @@
|
||||
Deactivated="PickerWindow_OnDeactivated"
|
||||
KeyDown="PickerWindow_OnKeyDown"
|
||||
Drop="PickerWindow_OnDrop"
|
||||
DragOver="PickerWindow_OnDragOver">
|
||||
DragOver="PickerWindow_OnDragOver"
|
||||
ContextMenuOpening="PickerWindow_OnContextMenuOpening"
|
||||
ContextMenuClosing="PickerWindow_OnContextMenuClosing">
|
||||
<Window.Resources>
|
||||
<Style x:Key="FlatButtonStyle" TargetType="Button">
|
||||
<Setter Property="Background" Value="#2B2D31"/>
|
||||
@@ -46,12 +48,20 @@
|
||||
<Border Background="#F0202225" CornerRadius="10" BorderBrush="#3A3B3E" BorderThickness="1">
|
||||
<Grid Margin="12">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="16"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Grid Grid.Row="0" Margin="0,0,0,10">
|
||||
<Border Grid.Row="0" Background="Transparent" Cursor="SizeAll"
|
||||
MouseLeftButtonDown="DragHandle_OnMouseLeftButtonDown"
|
||||
ToolTip="Drag to move">
|
||||
<Border Width="36" Height="4" CornerRadius="2" Background="#4A4C52"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
|
||||
<Grid Grid.Row="1" Margin="0,4,0,10">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
@@ -69,7 +79,7 @@
|
||||
ToolTip="Add memes"/>
|
||||
</Grid>
|
||||
|
||||
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto">
|
||||
<ScrollViewer Grid.Row="2" VerticalScrollBarVisibility="Auto">
|
||||
<Grid>
|
||||
<TextBlock Text="No memes yet. Click + or drag files in here."
|
||||
Foreground="#7B7D85" FontSize="13"
|
||||
@@ -99,7 +109,15 @@
|
||||
Style="{StaticResource FlatButtonStyle}"
|
||||
Command="{Binding DataContext.SelectItemCommand, ElementName=RootWindow}"
|
||||
CommandParameter="{Binding}"
|
||||
Tag="{Binding DataContext, ElementName=RootWindow}"
|
||||
ToolTip="{Binding DisplayName}">
|
||||
<Button.ContextMenu>
|
||||
<ContextMenu>
|
||||
<MenuItem Header="Remove"
|
||||
Command="{Binding PlacementTarget.Tag.RemoveItemCommand, RelativeSource={RelativeSource AncestorType=ContextMenu}}"
|
||||
CommandParameter="{Binding PlacementTarget.DataContext, RelativeSource={RelativeSource AncestorType=ContextMenu}}"/>
|
||||
</ContextMenu>
|
||||
</Button.ContextMenu>
|
||||
<Image Source="{Binding Thumbnail}" Stretch="Uniform" Width="80" Height="80"/>
|
||||
</Button>
|
||||
</DataTemplate>
|
||||
@@ -108,7 +126,7 @@
|
||||
</Grid>
|
||||
</ScrollViewer>
|
||||
|
||||
<TextBlock Grid.Row="2" Text="{Binding StatusMessage}" Foreground="#9A9CA3" FontSize="11" Margin="2,8,0,0"/>
|
||||
<TextBlock Grid.Row="3" Text="{Binding StatusMessage}" Foreground="#9A9CA3" FontSize="11" Margin="2,8,0,0"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Window>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
using System.ComponentModel;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using EbbesMemeClipboard.Native;
|
||||
using EbbesMemeClipboard.Services;
|
||||
using EbbesMemeClipboard.ViewModels;
|
||||
|
||||
namespace EbbesMemeClipboard.Views;
|
||||
@@ -9,17 +11,22 @@ namespace EbbesMemeClipboard.Views;
|
||||
public partial class PickerWindow : Window
|
||||
{
|
||||
private readonly PickerViewModel _viewModel;
|
||||
private readonly IAutoPasteService _autoPaste;
|
||||
private bool _isContextMenuOpen;
|
||||
|
||||
public PickerWindow(PickerViewModel viewModel)
|
||||
public PickerWindow(PickerViewModel viewModel, IAutoPasteService autoPaste)
|
||||
{
|
||||
InitializeComponent();
|
||||
_viewModel = viewModel;
|
||||
_autoPaste = autoPaste;
|
||||
DataContext = _viewModel;
|
||||
_viewModel.RequestClose += (_, _) => Hide();
|
||||
_viewModel.PropertyChanged += ViewModel_OnPropertyChanged;
|
||||
}
|
||||
|
||||
public void ShowNearCursor()
|
||||
{
|
||||
_autoPaste.CaptureForegroundWindow();
|
||||
PositionNearCursor();
|
||||
_viewModel.OnShown();
|
||||
Show();
|
||||
@@ -40,20 +47,19 @@ public partial class PickerWindow : Window
|
||||
}
|
||||
|
||||
/// <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.
|
||||
/// 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. Resolves the DPI scale of whichever monitor the cursor is actually on (not just
|
||||
/// whichever monitor this window last rendered on), so the popup stays fully on-screen on
|
||||
/// mixed-DPI multi-monitor setups too.
|
||||
/// </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 scale = MonitorInterop.GetDpiScaleForPoint(cursor);
|
||||
|
||||
double left = cursor.X / scale;
|
||||
double top = cursor.Y / scale;
|
||||
@@ -66,7 +72,36 @@ public partial class PickerWindow : Window
|
||||
Top = Math.Clamp(top, minTop, Math.Max(minTop, maxTop));
|
||||
}
|
||||
|
||||
private void PickerWindow_OnDeactivated(object? sender, EventArgs e) => Hide();
|
||||
private void DragHandle_OnMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (e.LeftButton == MouseButtonState.Pressed)
|
||||
{
|
||||
DragMove();
|
||||
}
|
||||
}
|
||||
|
||||
private void ViewModel_OnPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
// Once the "add memes" dialog closes, hand keyboard focus back to the picker so the
|
||||
// user can keep searching/clicking without having to click into it again.
|
||||
if (e.PropertyName == nameof(PickerViewModel.IsDialogOpen) && !_viewModel.IsDialogOpen && IsVisible)
|
||||
{
|
||||
Activate();
|
||||
}
|
||||
}
|
||||
|
||||
private void PickerWindow_OnDeactivated(object? sender, EventArgs e)
|
||||
{
|
||||
// Don't auto-hide while our own file-picker dialog or a tile's right-click context menu
|
||||
// is showing - both take keyboard focus away from the window in a way that looks like
|
||||
// "user clicked away" but isn't, and hiding here would close the menu/dialog too.
|
||||
if (_viewModel.IsDialogOpen || _isContextMenuOpen) return;
|
||||
Hide();
|
||||
}
|
||||
|
||||
private void PickerWindow_OnContextMenuOpening(object sender, ContextMenuEventArgs e) => _isContextMenuOpen = true;
|
||||
|
||||
private void PickerWindow_OnContextMenuClosing(object sender, ContextMenuEventArgs e) => _isContextMenuOpen = false;
|
||||
|
||||
private void PickerWindow_OnKeyDown(object sender, KeyEventArgs e)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
<Window x:Class="EbbesMemeClipboard.Views.SettingsWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
Title="Settings - Ebbe's Meme Clipboard"
|
||||
Width="380" Height="380"
|
||||
Background="#F0202225"
|
||||
WindowStartupLocation="CenterScreen"
|
||||
ResizeMode="NoResize"
|
||||
SizeToContent="Height">
|
||||
<StackPanel Margin="20">
|
||||
<TextBlock Text="Global Hotkey" Foreground="White" FontSize="14" FontWeight="Bold" Margin="0,0,0,8"/>
|
||||
<Border Background="#2B2D31" CornerRadius="6">
|
||||
<TextBox x:Name="HotkeyBox"
|
||||
Text="{Binding HotkeyDisplay, Mode=OneWay}"
|
||||
IsReadOnly="True" Focusable="True"
|
||||
Background="Transparent" Foreground="White" BorderThickness="0"
|
||||
Padding="10,8" FontSize="13"
|
||||
CaretBrush="Transparent"
|
||||
PreviewKeyDown="HotkeyBox_OnPreviewKeyDown"/>
|
||||
</Border>
|
||||
<TextBlock Text="Click the box above, then press a new key combination." TextWrapping="Wrap"
|
||||
Foreground="#7B7D85" FontSize="11" Margin="0,4,0,0"/>
|
||||
<TextBlock Text="{Binding HotkeyError}" TextWrapping="Wrap"
|
||||
Foreground="#E06060" FontSize="11" Margin="0,4,0,0"/>
|
||||
|
||||
<TextBlock Text="Insert Mode" Foreground="White" FontSize="14" FontWeight="Bold" Margin="0,24,0,8"/>
|
||||
<RadioButton GroupName="InsertMode" Content="Copy to clipboard" Foreground="White"
|
||||
IsChecked="{Binding IsCopyOnly}" Margin="0,0,0,8"/>
|
||||
<RadioButton GroupName="InsertMode" Content="Paste into active window" Foreground="White"
|
||||
IsChecked="{Binding IsPasteIntoActiveWindow}" Margin="0,0,0,8"/>
|
||||
<RadioButton GroupName="InsertMode" Content="Paste and send instantly" Foreground="White"
|
||||
IsChecked="{Binding IsPasteAndSend}"/>
|
||||
<TextBlock Text="Presses Enter right after pasting to submit it - use with care in chat apps."
|
||||
Foreground="#7B7D85" FontSize="10" TextWrapping="Wrap" Margin="20,4,0,0"/>
|
||||
</StackPanel>
|
||||
</Window>
|
||||
@@ -0,0 +1,55 @@
|
||||
using System.ComponentModel;
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
using EbbesMemeClipboard.ViewModels;
|
||||
|
||||
namespace EbbesMemeClipboard.Views;
|
||||
|
||||
public partial class SettingsWindow : Window
|
||||
{
|
||||
private readonly SettingsViewModel _viewModel;
|
||||
|
||||
public SettingsWindow(SettingsViewModel viewModel)
|
||||
{
|
||||
InitializeComponent();
|
||||
_viewModel = viewModel;
|
||||
DataContext = _viewModel;
|
||||
}
|
||||
|
||||
private void HotkeyBox_OnPreviewKeyDown(object sender, KeyEventArgs e)
|
||||
{
|
||||
e.Handled = true;
|
||||
|
||||
var key = e.Key == Key.System ? e.SystemKey : e.Key;
|
||||
if (key is Key.LeftCtrl or Key.RightCtrl or Key.LeftAlt or Key.RightAlt
|
||||
or Key.LeftShift or Key.RightShift or Key.LWin or Key.RWin or Key.System)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var modifiers = Keyboard.Modifiers;
|
||||
// Keyboard.Modifiers only tracks Ctrl/Alt/Shift - the Windows key isn't a "modifier" as
|
||||
// far as WPF's own key-state tracking is concerned, so it has to be checked separately.
|
||||
if (Keyboard.IsKeyDown(Key.LWin) || Keyboard.IsKeyDown(Key.RWin))
|
||||
{
|
||||
modifiers |= ModifierKeys.Windows;
|
||||
}
|
||||
|
||||
if (modifiers == ModifierKeys.None)
|
||||
{
|
||||
// A bare, unmodified key (e.g. plain "M") would fire on every keystroke system-wide.
|
||||
return;
|
||||
}
|
||||
|
||||
_viewModel.TrySetHotkey(modifiers, key);
|
||||
}
|
||||
|
||||
protected override void OnClosing(CancelEventArgs e)
|
||||
{
|
||||
// Reused DI singleton for the app's whole lifetime - closing (title-bar X) should just
|
||||
// hide it, not tear it down.
|
||||
e.Cancel = true;
|
||||
Hide();
|
||||
base.OnClosing(e);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user