Add host activity and DB browser, and keep dialogs, drag-drop, and idle maintenance responsive.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
78
src/Explorer.App/DatabaseWindow.xaml
Normal file
78
src/Explorer.App/DatabaseWindow.xaml
Normal file
@@ -0,0 +1,78 @@
|
||||
<Window x:Class="Explorer.App.DatabaseWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
Title="Database"
|
||||
Icon="pack://application:,,,/Assets/explorer-workbench.ico"
|
||||
Height="720" Width="1100"
|
||||
MinHeight="480" MinWidth="800"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
Background="{DynamicResource Bg}" Foreground="{DynamicResource Fg}">
|
||||
<DockPanel Margin="12">
|
||||
<DockPanel DockPanel.Dock="Bottom" Margin="0,10,0,0">
|
||||
<Button DockPanel.Dock="Right" Content="Close" MinWidth="88" Height="30" Click="OnClose" Margin="8,0,0,0"/>
|
||||
<TextBlock Text="{Binding Status}" VerticalAlignment="Center" Foreground="{DynamicResource FgMuted}"
|
||||
TextTrimming="CharacterEllipsis"/>
|
||||
</DockPanel>
|
||||
|
||||
<DockPanel DockPanel.Dock="Top" Margin="0,0,0,8">
|
||||
<Button DockPanel.Dock="Right" Content="Close DB" MinWidth="80" Height="28"
|
||||
Command="{Binding CloseDatabaseCommand}" Margin="6,0,0,0"/>
|
||||
<Button DockPanel.Dock="Right" Content="Open file…" MinWidth="90" Height="28"
|
||||
Click="OnOpenFile" Margin="6,0,0,0"/>
|
||||
<Button DockPanel.Dock="Right" Content="Open Workbench index" MinWidth="150" Height="28"
|
||||
Command="{Binding OpenWorkbenchIndexCommand}" Margin="6,0,0,0"/>
|
||||
<StackPanel>
|
||||
<TextBlock Text="{Binding PathLabel}" FontWeight="SemiBold" TextTrimming="CharacterEllipsis"/>
|
||||
<TextBlock Text="{Binding ModeLabel}" Foreground="{DynamicResource FgMuted}" FontSize="11"/>
|
||||
</StackPanel>
|
||||
</DockPanel>
|
||||
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="220"/>
|
||||
<ColumnDefinition Width="8"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<DockPanel>
|
||||
<DockPanel DockPanel.Dock="Top" Margin="0,0,0,6">
|
||||
<Button DockPanel.Dock="Right" Content="↻" Width="28" Height="26"
|
||||
Command="{Binding RefreshTablesCommand}" ToolTip="Refresh tables"/>
|
||||
<TextBlock Text="Tables" FontWeight="SemiBold" VerticalAlignment="Center"/>
|
||||
</DockPanel>
|
||||
<ListBox ItemsSource="{Binding Tables}" SelectedItem="{Binding SelectedTable}"/>
|
||||
</DockPanel>
|
||||
|
||||
<DockPanel Grid.Column="2">
|
||||
<DockPanel DockPanel.Dock="Top" Margin="0,0,0,8">
|
||||
<Button DockPanel.Dock="Right" Content="Run SQL" MinWidth="80" Height="28"
|
||||
Command="{Binding RunSqlCommand}" Margin="8,0,0,0"/>
|
||||
<TextBox Text="{Binding Sql, UpdateSourceTrigger=PropertyChanged}" AcceptsReturn="True"
|
||||
Height="56" TextWrapping="Wrap" VerticalScrollBarVisibility="Auto"
|
||||
FontFamily="Consolas"/>
|
||||
</DockPanel>
|
||||
<DockPanel DockPanel.Dock="Top" Margin="0,0,0,8">
|
||||
<TextBlock DockPanel.Dock="Left" Text="{Binding PageLabel}" VerticalAlignment="Center"
|
||||
Foreground="{DynamicResource FgMuted}" Margin="0,0,12,0"/>
|
||||
<Button DockPanel.Dock="Left" Content="Prev" MinWidth="60" Height="26"
|
||||
Command="{Binding PrevPageCommand}" Margin="0,0,6,0"/>
|
||||
<Button DockPanel.Dock="Left" Content="Next" MinWidth="60" Height="26"
|
||||
Command="{Binding NextPageCommand}" Margin="0,0,12,0"/>
|
||||
<Button DockPanel.Dock="Left" Content="Edit cell…" MinWidth="80" Height="26"
|
||||
Click="OnEditCell" IsEnabled="{Binding CanWrite}" Margin="0,0,6,0"/>
|
||||
<Button DockPanel.Dock="Left" Content="Insert row…" MinWidth="90" Height="26"
|
||||
Click="OnInsertRow" IsEnabled="{Binding CanWrite}" Margin="0,0,6,0"/>
|
||||
<Button DockPanel.Dock="Left" Content="Delete row" MinWidth="80" Height="26"
|
||||
Command="{Binding DeleteSelectedRowCommand}" IsEnabled="{Binding CanWrite}"/>
|
||||
</DockPanel>
|
||||
<ListView x:Name="GridViewHost"
|
||||
ItemsSource="{Binding Rows}"
|
||||
SelectedItem="{Binding SelectedRow}"
|
||||
MouseDoubleClick="OnRowDoubleClick">
|
||||
<ListView.View>
|
||||
<GridView x:Name="ResultGrid"/>
|
||||
</ListView.View>
|
||||
</ListView>
|
||||
</DockPanel>
|
||||
</Grid>
|
||||
</DockPanel>
|
||||
</Window>
|
||||
159
src/Explorer.App/DatabaseWindow.xaml.cs
Normal file
159
src/Explorer.App/DatabaseWindow.xaml.cs
Normal file
@@ -0,0 +1,159 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Input;
|
||||
using Explorer.Presentation.ViewModels;
|
||||
using Microsoft.Win32;
|
||||
|
||||
namespace Explorer.App;
|
||||
|
||||
public partial class DatabaseWindow : Window
|
||||
{
|
||||
public DatabaseWindow(DatabaseViewerViewModel vm)
|
||||
{
|
||||
InitializeComponent();
|
||||
DataContext = vm;
|
||||
ViewModel = vm;
|
||||
vm.GridChanged += (_, _) => RebuildColumns();
|
||||
ModelessWindowClose.EnableEscape(this);
|
||||
Closed += async (_, _) => await vm.DisposeAsync().ConfigureAwait(true);
|
||||
Loaded += async (_, _) => await vm.OpenIndexAsync(preferWrite: false).ConfigureAwait(true);
|
||||
}
|
||||
|
||||
public DatabaseViewerViewModel ViewModel { get; }
|
||||
|
||||
private void OnClose(object sender, RoutedEventArgs e) => Close();
|
||||
|
||||
private void RebuildColumns()
|
||||
{
|
||||
ResultGrid.Columns.Clear();
|
||||
for (var i = 0; i < ViewModel.Columns.Count; i++)
|
||||
{
|
||||
var index = i;
|
||||
var header = ViewModel.Columns[i];
|
||||
ResultGrid.Columns.Add(new GridViewColumn
|
||||
{
|
||||
Header = header,
|
||||
Width = header.Equals("_rowid_", StringComparison.OrdinalIgnoreCase) ? 70 : 140,
|
||||
DisplayMemberBinding = new Binding($"Cells[{index}]")
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void OnOpenFile(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var dlg = new OpenFileDialog
|
||||
{
|
||||
Title = "Open SQLite database",
|
||||
Filter = "SQLite databases (*.db;*.sqlite;*.sqlite3)|*.db;*.sqlite;*.sqlite3|All files (*.*)|*.*"
|
||||
};
|
||||
if (dlg.ShowDialog(this) == true)
|
||||
{
|
||||
var write = MessageBox.Show(
|
||||
this,
|
||||
"Open for writing? Choose No for read-only.",
|
||||
"Database",
|
||||
MessageBoxButton.YesNoCancel,
|
||||
MessageBoxImage.Question);
|
||||
if (write == MessageBoxResult.Cancel)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_ = ViewModel.OpenPathAsync(dlg.FileName, preferWrite: write == MessageBoxResult.Yes);
|
||||
}
|
||||
}
|
||||
|
||||
private async void OnEditCell(object sender, RoutedEventArgs e)
|
||||
=> await EditSelectedCellAsync().ConfigureAwait(true);
|
||||
|
||||
private async void OnRowDoubleClick(object sender, MouseButtonEventArgs e)
|
||||
=> await EditSelectedCellAsync().ConfigureAwait(true);
|
||||
|
||||
private async Task EditSelectedCellAsync()
|
||||
{
|
||||
if (!ViewModel.CanWrite || ViewModel.SelectedRow is null || string.IsNullOrWhiteSpace(ViewModel.SelectedTable))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var columns = ViewModel.EditableColumns();
|
||||
if (columns.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var column = Prompt("Column to edit", string.Join(", ", columns.Take(8)) + (columns.Count > 8 ? "…" : ""), columns[0]);
|
||||
if (column is null || !columns.Contains(column, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var index = ViewModel.Columns.ToList().FindIndex(c => c.Equals(column, StringComparison.OrdinalIgnoreCase));
|
||||
var current = index >= 0 && index < ViewModel.SelectedRow.Cells.Count
|
||||
? ViewModel.SelectedRow.Cells[index]
|
||||
: "";
|
||||
var next = Prompt("New value for " + column, "Leave empty for NULL.", current);
|
||||
if (next is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await ViewModel.UpdateSelectedCellAsync(column, string.IsNullOrEmpty(next) ? null : next)
|
||||
.ConfigureAwait(true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show(this, ex.Message, "Database", MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
}
|
||||
}
|
||||
|
||||
private async void OnInsertRow(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (!ViewModel.CanWrite || string.IsNullOrWhiteSpace(ViewModel.SelectedTable))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var columns = ViewModel.EditableColumns();
|
||||
if (columns.Count == 0)
|
||||
{
|
||||
MessageBox.Show(this, "Load a table first so columns are known.", "Database");
|
||||
return;
|
||||
}
|
||||
|
||||
var values = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var column in columns)
|
||||
{
|
||||
var value = Prompt("Value for " + column, "Cancel skips this column. Empty = NULL.", "");
|
||||
if (value is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
values[column] = string.IsNullOrEmpty(value) ? null : value;
|
||||
}
|
||||
|
||||
if (values.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await ViewModel.InsertRowAsync(values).ConfigureAwait(true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show(this, ex.Message, "Database", MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
}
|
||||
}
|
||||
|
||||
private string? Prompt(string title, string message, string initial)
|
||||
{
|
||||
var dlg = new PromptWindow(title, message, initial) { Owner = this };
|
||||
return dlg.ShowDialog() == true ? dlg.Value : null;
|
||||
}
|
||||
}
|
||||
@@ -9,9 +9,9 @@
|
||||
Background="{DynamicResource Bg}" Foreground="{DynamicResource Fg}">
|
||||
<DockPanel>
|
||||
<DockPanel DockPanel.Dock="Bottom" Margin="16,8,16,16">
|
||||
<Button DockPanel.Dock="Right" Content="Close" MinWidth="88" Height="32" IsCancel="True" Margin="8,0,0,0"/>
|
||||
<Button DockPanel.Dock="Right" Content="Close" MinWidth="88" Height="32" Click="OnClose" Margin="8,0,0,0"/>
|
||||
<Button DockPanel.Dock="Right" Content="Open file" MinWidth="88" Height="32" Click="OnOpenFile"
|
||||
x:Name="OpenFileButton" Margin="8,0,0,0"/>
|
||||
x:Name="OpenFileButton" IsEnabled="False" Margin="8,0,0,0"/>
|
||||
<Button DockPanel.Dock="Right" Content="Reload" MinWidth="88" Height="32" Click="OnReload" Margin="8,0,0,0"/>
|
||||
<TextBlock x:Name="SourceLabel" VerticalAlignment="Center" TextWrapping="Wrap"
|
||||
Foreground="{DynamicResource FgMuted}"/>
|
||||
|
||||
@@ -11,14 +11,19 @@ public partial class DocumentationWindow : Window
|
||||
{
|
||||
private FlowDocument? _document;
|
||||
private bool _suppressToc;
|
||||
private int _renderGeneration;
|
||||
|
||||
public DocumentationWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
Loaded += (_, _) => Render();
|
||||
ModelessWindowClose.EnableEscape(this);
|
||||
Loaded += async (_, _) => await RenderAsync().ConfigureAwait(true);
|
||||
}
|
||||
|
||||
private void OnReload(object sender, RoutedEventArgs e) => Render();
|
||||
private void OnClose(object sender, RoutedEventArgs e) => Close();
|
||||
|
||||
private async void OnReload(object sender, RoutedEventArgs e)
|
||||
=> await RenderAsync().ConfigureAwait(true);
|
||||
|
||||
private void OnOpenFile(object sender, RoutedEventArgs e)
|
||||
{
|
||||
@@ -44,10 +49,35 @@ public partial class DocumentationWindow : Window
|
||||
}
|
||||
}
|
||||
|
||||
private void Render()
|
||||
private async Task RenderAsync()
|
||||
{
|
||||
var loaded = DocumentationLoader.Load();
|
||||
var parsed = MarkdownParser.Parse(loaded.Markdown);
|
||||
var generation = Interlocked.Increment(ref _renderGeneration);
|
||||
SourceLabel.Text = "Loading…";
|
||||
LoadedDocumentation loaded;
|
||||
MarkdownDocument parsed;
|
||||
try
|
||||
{
|
||||
(loaded, parsed) = await Task.Run(() =>
|
||||
{
|
||||
var document = DocumentationLoader.Load();
|
||||
return (document, MarkdownParser.Parse(document.Markdown));
|
||||
}).ConfigureAwait(true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (generation == _renderGeneration)
|
||||
{
|
||||
SourceLabel.Text = "Could not load documentation: " + ex.Message;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (generation != _renderGeneration || !IsLoaded)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var brushes = new DocumentationBrushes(
|
||||
Brush("Fg"),
|
||||
Brush("FgMuted"),
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
Background="{DynamicResource Bg}" Foreground="{DynamicResource Fg}">
|
||||
<DockPanel Margin="16">
|
||||
<DockPanel DockPanel.Dock="Bottom" Margin="0,12,0,0">
|
||||
<Button DockPanel.Dock="Right" Content="Close" MinWidth="88" Height="32" IsCancel="True" Margin="8,0,0,0"/>
|
||||
<Button DockPanel.Dock="Right" Content="Close" MinWidth="88" Height="32" Click="OnClose" Margin="8,0,0,0"/>
|
||||
<Button DockPanel.Dock="Right" Content="Commit…" MinWidth="88" Height="32"
|
||||
Click="OnCommit" IsEnabled="{Binding CanCommit}" Margin="8,0,0,0"/>
|
||||
<Button DockPanel.Dock="Right" Content="Push" MinWidth="72" Height="32"
|
||||
|
||||
@@ -12,10 +12,13 @@ public partial class GitChangesWindow : Window
|
||||
InitializeComponent();
|
||||
DataContext = vm;
|
||||
ViewModel = vm;
|
||||
ModelessWindowClose.EnableEscape(this);
|
||||
}
|
||||
|
||||
public GitChangesViewModel ViewModel { get; }
|
||||
|
||||
private void OnClose(object sender, RoutedEventArgs e) => Close();
|
||||
|
||||
private async void OnRowDoubleClick(object sender, MouseButtonEventArgs e)
|
||||
=> await ShowDiffAsync().ConfigureAwait(true);
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
Background="{DynamicResource Bg}" Foreground="{DynamicResource Fg}">
|
||||
<DockPanel Margin="16">
|
||||
<Button DockPanel.Dock="Bottom" Content="Close" MinWidth="88" Height="32" HorizontalAlignment="Right"
|
||||
IsCancel="True" Margin="0,12,0,0"/>
|
||||
Click="OnClose" Margin="0,12,0,0"/>
|
||||
<TextBlock DockPanel.Dock="Top" Text="{Binding EmptyText}" Margin="0,0,0,8"
|
||||
Foreground="{DynamicResource FgMuted}" TextWrapping="Wrap"
|
||||
Visibility="{Binding ShowEmpty, Converter={StaticResource BoolVis}}"/>
|
||||
|
||||
@@ -10,5 +10,8 @@ public partial class GitDiffWindow : Window
|
||||
InitializeComponent();
|
||||
DataContext = vm;
|
||||
Title = vm.Title;
|
||||
ModelessWindowClose.EnableEscape(this);
|
||||
}
|
||||
|
||||
private void OnClose(object sender, RoutedEventArgs e) => Close();
|
||||
}
|
||||
|
||||
130
src/Explorer.App/HostActivityMonitorWindow.xaml
Normal file
130
src/Explorer.App/HostActivityMonitorWindow.xaml
Normal file
@@ -0,0 +1,130 @@
|
||||
<Window x:Class="Explorer.App.HostActivityMonitorWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:shell="clr-namespace:System.Windows.Shell;assembly=PresentationFramework"
|
||||
Title="Host activity"
|
||||
Icon="pack://application:,,,/Assets/explorer-workbench.ico"
|
||||
Height="640" Width="960"
|
||||
MinHeight="420" MinWidth="720"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
WindowStyle="None"
|
||||
ResizeMode="CanResize"
|
||||
Background="{DynamicResource Bg}"
|
||||
Foreground="{DynamicResource Fg}"
|
||||
UseLayoutRounding="True"
|
||||
SnapsToDevicePixels="True">
|
||||
<shell:WindowChrome.WindowChrome>
|
||||
<shell:WindowChrome CaptionHeight="40"
|
||||
ResizeBorderThickness="6"
|
||||
GlassFrameThickness="0"
|
||||
CornerRadius="0"
|
||||
UseAeroCaptionButtons="False"/>
|
||||
</shell:WindowChrome.WindowChrome>
|
||||
<DockPanel>
|
||||
<Border DockPanel.Dock="Top" Height="40" Background="{DynamicResource Panel}"
|
||||
BorderBrush="{DynamicResource Stroke}" BorderThickness="0,0,0,1"
|
||||
MouseLeftButtonDown="OnTitleBarMouseDown">
|
||||
<Grid>
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" Margin="12,0,0,0" IsHitTestVisible="False">
|
||||
<Image Width="20" Height="20" Margin="0,0,8,0" VerticalAlignment="Center"
|
||||
RenderOptions.BitmapScalingMode="HighQuality"
|
||||
Source="pack://application:,,,/Assets/explorer-workbench-20.png"/>
|
||||
<TextBlock Text="Host activity" FontWeight="SemiBold" VerticalAlignment="Center"
|
||||
Foreground="{DynamicResource Fg}"/>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
|
||||
<Button Style="{StaticResource CaptionButton}" Content="─" Click="OnMinimize" ToolTip="Minimize"/>
|
||||
<Button x:Name="MaxRestoreButton" Style="{StaticResource CaptionButton}" Content="☐"
|
||||
Click="OnMaxRestore" ToolTip="Maximize"/>
|
||||
<Button Style="{StaticResource CaptionCloseButton}" Content="✕" Click="OnClose" ToolTip="Close"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<DockPanel Margin="14">
|
||||
<DockPanel DockPanel.Dock="Bottom" Margin="0,10,0,0">
|
||||
<Button DockPanel.Dock="Right" Content="Close" MinWidth="88" Height="30" Click="OnClose" Margin="8,0,0,0"/>
|
||||
<Button DockPanel.Dock="Right" Content="Run maintenance now" MinWidth="140" Height="30"
|
||||
Command="{Binding RunMaintenanceNowCommand}" Margin="8,0,0,0"/>
|
||||
<Button DockPanel.Dock="Right" Content="Refresh" MinWidth="88" Height="30"
|
||||
Command="{Binding RefreshCommand}" Margin="8,0,0,0"/>
|
||||
<TextBlock VerticalAlignment="Center" Foreground="{DynamicResource FgMuted}">
|
||||
<Run Text="{Binding Status, Mode=OneWay}"/>
|
||||
<Run Text=" · "/>
|
||||
<Run Text="{Binding IsLive, Mode=OneWay, StringFormat=Live: {0}}"/>
|
||||
</TextBlock>
|
||||
</DockPanel>
|
||||
|
||||
<TextBlock DockPanel.Dock="Top" Text="{Binding Summary}" FontWeight="SemiBold" FontSize="15"
|
||||
TextWrapping="Wrap" Margin="0,0,0,10"/>
|
||||
|
||||
<UniformGrid DockPanel.Dock="Top" Rows="2" Columns="2" Margin="0,0,0,10">
|
||||
<Border BorderBrush="{DynamicResource Stroke}" BorderThickness="1" Padding="10" Margin="0,0,6,6">
|
||||
<StackPanel>
|
||||
<TextBlock Text="Maintenance" Foreground="{DynamicResource FgMuted}" FontSize="11"/>
|
||||
<TextBlock Text="{Binding MaintenanceText}" TextWrapping="Wrap" Margin="0,4,0,0"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
<Border BorderBrush="{DynamicResource Stroke}" BorderThickness="1" Padding="10" Margin="6,0,0,6">
|
||||
<StackPanel>
|
||||
<TextBlock Text="Indexing" Foreground="{DynamicResource FgMuted}" FontSize="11"/>
|
||||
<TextBlock Text="{Binding IndexingText}" TextWrapping="Wrap" Margin="0,4,0,0"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
<Border BorderBrush="{DynamicResource Stroke}" BorderThickness="1" Padding="10" Margin="0,6,6,0">
|
||||
<StackPanel>
|
||||
<TextBlock Text="Hashing" Foreground="{DynamicResource FgMuted}" FontSize="11"/>
|
||||
<TextBlock Text="{Binding HashText}" TextWrapping="Wrap" Margin="0,4,0,0"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
<Border BorderBrush="{DynamicResource Stroke}" BorderThickness="1" Padding="10" Margin="6,6,0,0">
|
||||
<StackPanel>
|
||||
<TextBlock Text="Transfers" Foreground="{DynamicResource FgMuted}" FontSize="11"/>
|
||||
<TextBlock Text="{Binding TransferText}" TextWrapping="Wrap" Margin="0,4,0,0"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</UniformGrid>
|
||||
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="280"/>
|
||||
<ColumnDefinition Width="10"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<DockPanel>
|
||||
<TextBlock DockPanel.Dock="Top" Text="Current indexing jobs" FontWeight="SemiBold" Margin="0,0,0,6"/>
|
||||
<ListView ItemsSource="{Binding Jobs}" BorderBrush="{DynamicResource Stroke}"
|
||||
ScrollViewer.VerticalScrollBarVisibility="Auto"
|
||||
ScrollViewer.HorizontalScrollBarVisibility="Disabled">
|
||||
<ListView.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<StackPanel Margin="0,2">
|
||||
<TextBlock Text="{Binding Title}" FontWeight="SemiBold"/>
|
||||
<TextBlock Text="{Binding Detail}" Foreground="{DynamicResource FgMuted}" TextTrimming="CharacterEllipsis"/>
|
||||
<TextBlock Text="{Binding Origin}" Foreground="{DynamicResource FgMuted}" FontSize="11"/>
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ListView.ItemTemplate>
|
||||
</ListView>
|
||||
</DockPanel>
|
||||
<DockPanel Grid.Column="2">
|
||||
<TextBlock DockPanel.Dock="Top" Text="{Binding LogCaption}" FontWeight="SemiBold" Margin="0,0,0,6"/>
|
||||
<ListView x:Name="EventList" ItemsSource="{Binding Events}" BorderBrush="{DynamicResource Stroke}"
|
||||
ScrollViewer.ScrollChanged="OnEventScrollChanged"
|
||||
ScrollViewer.VerticalScrollBarVisibility="Visible"
|
||||
ScrollViewer.HorizontalScrollBarVisibility="Auto"
|
||||
VirtualizingPanel.IsVirtualizing="True"
|
||||
VirtualizingPanel.VirtualizationMode="Recycling">
|
||||
<ListView.View>
|
||||
<GridView>
|
||||
<GridViewColumn Header="Time" Width="70" DisplayMemberBinding="{Binding Time}"/>
|
||||
<GridViewColumn Header="Area" Width="100" DisplayMemberBinding="{Binding Category}"/>
|
||||
<GridViewColumn Header="Detail" Width="440" DisplayMemberBinding="{Binding Message}"/>
|
||||
</GridView>
|
||||
</ListView.View>
|
||||
</ListView>
|
||||
</DockPanel>
|
||||
</Grid>
|
||||
</DockPanel>
|
||||
</DockPanel>
|
||||
</Window>
|
||||
139
src/Explorer.App/HostActivityMonitorWindow.xaml.cs
Normal file
139
src/Explorer.App/HostActivityMonitorWindow.xaml.cs
Normal file
@@ -0,0 +1,139 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Threading;
|
||||
using Explorer.Presentation.ViewModels;
|
||||
|
||||
namespace Explorer.App;
|
||||
|
||||
public partial class HostActivityMonitorWindow : Window
|
||||
{
|
||||
private bool _stickToEnd = true;
|
||||
private bool _scrollQueued;
|
||||
private ScrollViewer? _eventScroll;
|
||||
|
||||
public HostActivityMonitorWindow(HostActivityMonitorViewModel vm)
|
||||
{
|
||||
InitializeComponent();
|
||||
DataContext = vm;
|
||||
ViewModel = vm;
|
||||
ModelessWindowClose.EnableEscape(this);
|
||||
StateChanged += (_, _) => SyncMaxRestoreButton();
|
||||
vm.EventsReplaced += OnEventsReplaced;
|
||||
Closed += (_, _) =>
|
||||
{
|
||||
vm.EventsReplaced -= OnEventsReplaced;
|
||||
vm.Dispose();
|
||||
};
|
||||
Loaded += (_, _) =>
|
||||
{
|
||||
SyncMaxRestoreButton();
|
||||
_eventScroll = FindScrollViewer(EventList);
|
||||
vm.Start();
|
||||
};
|
||||
}
|
||||
|
||||
public HostActivityMonitorViewModel ViewModel { get; }
|
||||
|
||||
private void OnClose(object sender, RoutedEventArgs e) => Close();
|
||||
|
||||
private void OnMinimize(object sender, RoutedEventArgs e) => WindowState = WindowState.Minimized;
|
||||
|
||||
private void OnMaxRestore(object sender, RoutedEventArgs e) => ToggleMaximized();
|
||||
|
||||
private void OnTitleBarMouseDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (e.ChangedButton != MouseButton.Left)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.ClickCount == 2)
|
||||
{
|
||||
ToggleMaximized();
|
||||
return;
|
||||
}
|
||||
|
||||
DragMove();
|
||||
}
|
||||
|
||||
private void ToggleMaximized()
|
||||
{
|
||||
WindowState = WindowState == WindowState.Maximized ? WindowState.Normal : WindowState.Maximized;
|
||||
SyncMaxRestoreButton();
|
||||
}
|
||||
|
||||
private void SyncMaxRestoreButton()
|
||||
=> MaxRestoreButton.Content = WindowState == WindowState.Maximized ? "❐" : "☐";
|
||||
|
||||
private void OnEventScrollChanged(object sender, ScrollChangedEventArgs e)
|
||||
{
|
||||
if (e.OriginalSource is not ScrollViewer viewer)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_eventScroll = viewer;
|
||||
if (e.ExtentHeightChange != 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Only treat intentional user scroll as leaving the live tail.
|
||||
if (e.VerticalChange == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_stickToEnd = viewer.VerticalOffset >= viewer.ScrollableHeight - 8;
|
||||
}
|
||||
|
||||
private void OnEventsReplaced(object? sender, EventArgs e)
|
||||
{
|
||||
if (!_stickToEnd || _scrollQueued)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_scrollQueued = true;
|
||||
Dispatcher.BeginInvoke(() =>
|
||||
{
|
||||
_scrollQueued = false;
|
||||
if (!_stickToEnd)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_eventScroll ??= FindScrollViewer(EventList);
|
||||
if (_eventScroll is not null)
|
||||
{
|
||||
_eventScroll.ScrollToEnd();
|
||||
}
|
||||
else if (EventList.Items.Count > 0)
|
||||
{
|
||||
EventList.ScrollIntoView(EventList.Items[^1]);
|
||||
}
|
||||
}, DispatcherPriority.Background);
|
||||
}
|
||||
|
||||
private static ScrollViewer? FindScrollViewer(DependencyObject root)
|
||||
{
|
||||
if (root is ScrollViewer scroll)
|
||||
{
|
||||
return scroll;
|
||||
}
|
||||
|
||||
for (var i = 0; i < VisualTreeHelper.GetChildrenCount(root); i++)
|
||||
{
|
||||
var child = VisualTreeHelper.GetChild(root, i);
|
||||
var found = FindScrollViewer(child);
|
||||
if (found is not null)
|
||||
{
|
||||
return found;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -183,6 +183,9 @@
|
||||
<MenuItem Header="_Operation profiles…" Click="OnOperationProfiles"/>
|
||||
</MenuItem>
|
||||
<MenuItem Header="_Development">
|
||||
<MenuItem Header="_Host activity…" Click="OnHostActivity"/>
|
||||
<MenuItem Header="_Database…" Click="OnDatabase"/>
|
||||
<Separator/>
|
||||
<MenuItem Header="View _changes…" Click="OnGitChanges"
|
||||
IsEnabled="{Binding ShowGitActions}"/>
|
||||
<MenuItem Header="_Commit…" Click="OnGitCommit"
|
||||
@@ -986,66 +989,91 @@
|
||||
</Border>
|
||||
|
||||
<Border Grid.Column="2" Background="#99000000" Visibility="{Binding Duplicates.IsOpen, Converter={StaticResource BoolVis}}">
|
||||
<Border Background="{DynamicResource Panel}" BorderBrush="{DynamicResource Stroke}" BorderThickness="1" Margin="48" Padding="16">
|
||||
<DockPanel>
|
||||
<DockPanel DockPanel.Dock="Top" Margin="0,0,0,8">
|
||||
<Border Background="{DynamicResource Panel}" BorderBrush="{DynamicResource Stroke}" BorderThickness="1"
|
||||
Margin="48" Padding="16" VerticalAlignment="Stretch" HorizontalAlignment="Stretch">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
<DockPanel Margin="0,0,0,8">
|
||||
<Button DockPanel.Dock="Right" Content="Close" Command="{Binding Duplicates.CloseCommand}"/>
|
||||
<TextBlock Text="Duplicates" FontSize="18" FontWeight="SemiBold" Foreground="{DynamicResource Fg}"/>
|
||||
</DockPanel>
|
||||
<TextBlock DockPanel.Dock="Top" Text="{Binding Duplicates.Status}" Foreground="{DynamicResource Fg}" Margin="0,0,0,8" TextWrapping="Wrap"/>
|
||||
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal" Margin="0,0,0,8">
|
||||
<TextBlock Grid.Row="1" Text="{Binding Duplicates.Status}" Foreground="{DynamicResource Fg}" Margin="0,0,0,8" TextWrapping="Wrap"/>
|
||||
<StackPanel Grid.Row="2" Orientation="Horizontal" Margin="0,0,0,8">
|
||||
<CheckBox Content="Show intentional" IsChecked="{Binding Duplicates.ShowIntentional}" Margin="0,0,16,0"/>
|
||||
<CheckBox Content="Show hard links" IsChecked="{Binding Duplicates.ShowHardlinks}"/>
|
||||
</StackPanel>
|
||||
<ProgressBar DockPanel.Dock="Top" Margin="0,0,0,8" IsIndeterminate="True"
|
||||
<ProgressBar Grid.Row="3" Margin="0,0,0,8" IsIndeterminate="True"
|
||||
Visibility="{Binding Duplicates.IsBusy, Converter={StaticResource BoolVis}}"/>
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||
<ItemsControl ItemsSource="{Binding Duplicates.Groups}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Border BorderBrush="{DynamicResource Stroke}" BorderThickness="0,0,0,1" Padding="0,10">
|
||||
<DockPanel>
|
||||
<DockPanel DockPanel.Dock="Top" Margin="0,0,0,6">
|
||||
<StackPanel DockPanel.Dock="Right" Orientation="Horizontal">
|
||||
<Button Content="Mark as intentional" Margin="0,0,6,0"
|
||||
Command="{Binding DataContext.Duplicates.MarkIntentionalCommand, RelativeSource={RelativeSource AncestorType=Window}}"
|
||||
CommandParameter="{Binding}"
|
||||
Visibility="{Binding CanMarkIntentional, Converter={StaticResource BoolVis}}"/>
|
||||
<Button Content="Mark as accidental"
|
||||
Command="{Binding DataContext.Duplicates.MarkAccidentalCommand, RelativeSource={RelativeSource AncestorType=Window}}"
|
||||
CommandParameter="{Binding}"
|
||||
Visibility="{Binding CanMarkAccidental, Converter={StaticResource BoolVis}}"/>
|
||||
</StackPanel>
|
||||
<TextBlock Foreground="{DynamicResource Fg}" FontWeight="SemiBold">
|
||||
<Run Text="{Binding ClassLabel, Mode=OneWay}"/>
|
||||
<Run Text=" · "/>
|
||||
<Run Text="{Binding SizeLabel, Mode=OneWay}"/>
|
||||
<Run Text=" · "/>
|
||||
<Run Text="{Binding Summary, Mode=OneWay}"/>
|
||||
<Run Text=" · "/>
|
||||
<Run Text="{Binding WastedLabel, Mode=OneWay}"/>
|
||||
</TextBlock>
|
||||
</DockPanel>
|
||||
<ItemsControl ItemsSource="{Binding Files}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<DockPanel Margin="0,2">
|
||||
<Button DockPanel.Dock="Right" Content="Show in Explorer" Margin="8,0,0,0"
|
||||
Command="{Binding DataContext.Duplicates.RevealCommand, RelativeSource={RelativeSource AncestorType=Window}}"
|
||||
CommandParameter="{Binding}"/>
|
||||
<TextBlock Text="{Binding LocationLabel}" Foreground="{DynamicResource FgMuted}"
|
||||
TextTrimming="CharacterEllipsis" VerticalAlignment="Center"/>
|
||||
</DockPanel>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
<ListBox Grid.Row="4"
|
||||
ItemsSource="{Binding Duplicates.Groups}"
|
||||
Background="Transparent"
|
||||
BorderThickness="0"
|
||||
Padding="0"
|
||||
HorizontalContentAlignment="Stretch"
|
||||
ScrollViewer.CanContentScroll="True"
|
||||
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
|
||||
ScrollViewer.VerticalScrollBarVisibility="Visible"
|
||||
VirtualizingPanel.IsVirtualizing="True"
|
||||
VirtualizingPanel.VirtualizationMode="Recycling"
|
||||
VirtualizingPanel.ScrollUnit="Pixel">
|
||||
<ListBox.ItemContainerStyle>
|
||||
<Style TargetType="ListBoxItem">
|
||||
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
|
||||
<Setter Property="Padding" Value="0"/>
|
||||
<Setter Property="Margin" Value="0"/>
|
||||
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="ListBoxItem">
|
||||
<ContentPresenter HorizontalAlignment="Stretch"/>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
</ListBox.ItemContainerStyle>
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Border BorderBrush="{DynamicResource Stroke}" BorderThickness="0,0,0,1" Padding="0,10">
|
||||
<DockPanel>
|
||||
<DockPanel DockPanel.Dock="Top" Margin="0,0,0,6">
|
||||
<StackPanel DockPanel.Dock="Right" Orientation="Horizontal">
|
||||
<Button Content="Mark as intentional" Margin="0,0,6,0"
|
||||
Command="{Binding DataContext.Duplicates.MarkIntentionalCommand, RelativeSource={RelativeSource AncestorType=Window}}"
|
||||
CommandParameter="{Binding}"
|
||||
Visibility="{Binding CanMarkIntentional, Converter={StaticResource BoolVis}}"/>
|
||||
<Button Content="Mark as accidental"
|
||||
Command="{Binding DataContext.Duplicates.MarkAccidentalCommand, RelativeSource={RelativeSource AncestorType=Window}}"
|
||||
CommandParameter="{Binding}"
|
||||
Visibility="{Binding CanMarkAccidental, Converter={StaticResource BoolVis}}"/>
|
||||
</StackPanel>
|
||||
<TextBlock Text="{Binding Header, Mode=OneWay}" Foreground="{DynamicResource Fg}" FontWeight="SemiBold"
|
||||
TextTrimming="CharacterEllipsis"/>
|
||||
</DockPanel>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</ScrollViewer>
|
||||
</DockPanel>
|
||||
<ItemsControl ItemsSource="{Binding Files}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<DockPanel Margin="0,2">
|
||||
<Button DockPanel.Dock="Right" Content="Show in Explorer" Margin="8,0,0,0"
|
||||
Command="{Binding DataContext.Duplicates.RevealCommand, RelativeSource={RelativeSource AncestorType=Window}}"
|
||||
CommandParameter="{Binding}"/>
|
||||
<TextBlock Text="{Binding LocationLabel}" Foreground="{DynamicResource FgMuted}"
|
||||
TextTrimming="CharacterEllipsis" VerticalAlignment="Center"/>
|
||||
</DockPanel>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</DockPanel>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Border>
|
||||
</Grid>
|
||||
|
||||
@@ -18,11 +18,14 @@ public partial class MainWindow : Window
|
||||
{
|
||||
private DocumentationWindow? _docs;
|
||||
private GitChangesWindow? _gitChanges;
|
||||
private HostActivityMonitorWindow? _hostActivity;
|
||||
private DatabaseWindow? _database;
|
||||
private Point _dragStart;
|
||||
private bool _dragPending;
|
||||
private MouseButton _dragButton;
|
||||
private FolderItemViewModel? _dragItem;
|
||||
private ListView? _dragList;
|
||||
private bool _draggingFromHere;
|
||||
private FolderItemViewModel[] _dragSelection = [];
|
||||
private bool _dragFromMultiSelect;
|
||||
private bool _syncingSelection;
|
||||
@@ -661,7 +664,7 @@ public partial class MainWindow : Window
|
||||
}
|
||||
|
||||
_suppressItemContextMenu = false;
|
||||
_dragStart = e.GetPosition(null);
|
||||
_dragStart = e.GetPosition(this);
|
||||
_dragPending = true;
|
||||
_dragButton = e.ChangedButton;
|
||||
_dragList = sender as ListView;
|
||||
@@ -739,11 +742,12 @@ public partial class MainWindow : Window
|
||||
&& dest is not null
|
||||
&& NavigationTreeViewModel.PathsEqual(hover.FullPath, dest)
|
||||
&& !DragDropPolicy.IsInvalidTarget(files, dest)
|
||||
&& !IsRedundantLeftDrop(files, dest)
|
||||
? hover
|
||||
: null;
|
||||
SetListDropTarget(folderTarget);
|
||||
SetTreeDropTarget(null);
|
||||
if (dest is null || DragDropPolicy.IsInvalidTarget(files, dest))
|
||||
if (dest is null || DragDropPolicy.IsInvalidTarget(files, dest) || IsRedundantLeftDrop(files, dest))
|
||||
{
|
||||
e.Effects = DragDropEffects.None;
|
||||
e.Handled = true;
|
||||
@@ -786,6 +790,11 @@ public partial class MainWindow : Window
|
||||
return;
|
||||
}
|
||||
|
||||
if (IsRedundantLeftDrop(files, dest))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await ApplyDropAsync(files, dest, ResolveDropAction(e, files, dest)).ConfigureAwait(true);
|
||||
}
|
||||
|
||||
@@ -863,9 +872,8 @@ public partial class MainWindow : Window
|
||||
return;
|
||||
}
|
||||
|
||||
var pos = e.GetPosition(null);
|
||||
if (Math.Abs(pos.X - _dragStart.X) < SystemParameters.MinimumHorizontalDragDistance
|
||||
&& Math.Abs(pos.Y - _dragStart.Y) < SystemParameters.MinimumVerticalDragDistance)
|
||||
var pos = e.GetPosition(this);
|
||||
if (!DragMovedEnough(pos, DragDropPolicy.DragStartDistance))
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -887,10 +895,18 @@ public partial class MainWindow : Window
|
||||
_sourceRightDrag = _dragButton == MouseButton.Right;
|
||||
_suppressItemContextMenu = _sourceRightDrag;
|
||||
var data = new DataObject(DataFormats.FileDrop, paths.ToArray());
|
||||
DragDrop.DoDragDrop(this, data, DragDropEffects.Copy | DragDropEffects.Move | DragDropEffects.Link);
|
||||
_sourceRightDrag = false;
|
||||
_incomingRightDrag = false;
|
||||
ClearDropTargets();
|
||||
_draggingFromHere = true;
|
||||
try
|
||||
{
|
||||
DragDrop.DoDragDrop(this, data, DragDropEffects.Copy | DragDropEffects.Move | DragDropEffects.Link);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_draggingFromHere = false;
|
||||
_sourceRightDrag = false;
|
||||
_incomingRightDrag = false;
|
||||
ClearDropTargets();
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnQueryContinueDrag(QueryContinueDragEventArgs e)
|
||||
@@ -1017,16 +1033,58 @@ public partial class MainWindow : Window
|
||||
private string? ResolveDropDirectory(ListView list, DragEventArgs e)
|
||||
{
|
||||
var item = HitTestFolderItem(list, e.GetPosition(list));
|
||||
if (item is { IsDirectory: true })
|
||||
if (!_draggingFromHere)
|
||||
{
|
||||
if (item is { IsDirectory: true })
|
||||
{
|
||||
return item.FullPath;
|
||||
}
|
||||
|
||||
return CurrentListDirectory(list);
|
||||
}
|
||||
|
||||
var pos = e.GetPosition(this);
|
||||
if (item is { IsDirectory: true }
|
||||
&& DragMovedEnough(pos, FolderDropDistance(list)))
|
||||
{
|
||||
return item.FullPath;
|
||||
}
|
||||
|
||||
if (!ReferenceEquals(list, _dragList)
|
||||
&& !DragMovedEnough(pos, DragDropPolicy.DropCrossViewDistance))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return CurrentListDirectory(list);
|
||||
}
|
||||
|
||||
private string? CurrentListDirectory(ListView list)
|
||||
{
|
||||
var tab = Vm.ActiveTab;
|
||||
var path = list.ItemsSource == tab.Right.Items ? tab.Right.CurrentPath : tab.Left.CurrentPath;
|
||||
return LocationRoots.IsVirtual(path) ? null : path;
|
||||
}
|
||||
|
||||
private double FolderDropDistance(ListView list)
|
||||
=> ReferenceEquals(list, _dragList)
|
||||
? DragDropPolicy.DropIntoItemDistance
|
||||
: DragDropPolicy.DropCrossViewDistance;
|
||||
|
||||
private bool DragMovedEnough(Point current, double preferred)
|
||||
=> DragDropPolicy.ExceedsDistance(
|
||||
current.X - _dragStart.X,
|
||||
current.Y - _dragStart.Y,
|
||||
Math.Max(preferred, SystemDragMinimum));
|
||||
|
||||
private static double SystemDragMinimum
|
||||
=> Math.Max(SystemParameters.MinimumHorizontalDragDistance, SystemParameters.MinimumVerticalDragDistance);
|
||||
|
||||
private bool IsRedundantLeftDrop(IReadOnlyList<string> files, string dest)
|
||||
=> !_sourceRightDrag
|
||||
&& !_incomingRightDrag
|
||||
&& DragDropPolicy.AllAlreadyInDirectory(files, dest);
|
||||
|
||||
private static FolderItemViewModel? HitTestFolderItem(DependencyObject? origin, Point point)
|
||||
{
|
||||
if (origin is not Visual visual)
|
||||
@@ -1178,6 +1236,11 @@ public partial class MainWindow : Window
|
||||
return;
|
||||
}
|
||||
|
||||
if (IsRedundantLeftDrop(files, dest))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await ApplyDropAsync(files, dest, ResolveDropAction(e, files, dest)).ConfigureAwait(true);
|
||||
}
|
||||
|
||||
@@ -1848,6 +1911,48 @@ public partial class MainWindow : Window
|
||||
private void OnAbout(object sender, RoutedEventArgs e)
|
||||
=> new AboutWindow { Owner = this }.ShowDialog();
|
||||
|
||||
private void OnHostActivity(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (_hostActivity is { IsVisible: true })
|
||||
{
|
||||
_hostActivity.Activate();
|
||||
return;
|
||||
}
|
||||
|
||||
var vm = Vm.CreateHostActivityMonitorViewModel();
|
||||
if (vm is null)
|
||||
{
|
||||
MessageBox.Show(this, "Host activity is unavailable until the background host is connected.",
|
||||
"Explorer Workbench", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
return;
|
||||
}
|
||||
|
||||
_hostActivity = new HostActivityMonitorWindow(vm) { Owner = this };
|
||||
_hostActivity.Closed += (_, _) => _hostActivity = null;
|
||||
_hostActivity.Show();
|
||||
}
|
||||
|
||||
private void OnDatabase(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (_database is { IsVisible: true })
|
||||
{
|
||||
_database.Activate();
|
||||
return;
|
||||
}
|
||||
|
||||
var vm = Vm.CreateDatabaseViewerViewModel();
|
||||
if (vm is null)
|
||||
{
|
||||
MessageBox.Show(this, "Database tools are unavailable.",
|
||||
"Explorer Workbench", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
return;
|
||||
}
|
||||
|
||||
_database = new DatabaseWindow(vm) { Owner = this };
|
||||
_database.Closed += (_, _) => _database = null;
|
||||
_database.Show();
|
||||
}
|
||||
|
||||
private async void OnGitChanges(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (_gitChanges is { IsVisible: true })
|
||||
|
||||
23
src/Explorer.App/ModelessWindowClose.cs
Normal file
23
src/Explorer.App/ModelessWindowClose.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace Explorer.App;
|
||||
|
||||
/// <summary>
|
||||
/// Close helpers for windows opened with <see cref="Window.Show"/>.
|
||||
/// <see cref="Button.IsCancel"/> sets <see cref="Window.DialogResult"/>, which throws on modeless windows.
|
||||
/// </summary>
|
||||
internal static class ModelessWindowClose
|
||||
{
|
||||
public static void EnableEscape(Window window)
|
||||
=> window.PreviewKeyDown += (_, e) =>
|
||||
{
|
||||
if (e.Key != Key.Escape)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
window.Close();
|
||||
e.Handled = true;
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user