feature<FolderDialog>: remove View.FolderBrowser and use windows shell32 API to select folders

This commit is contained in:
leo 2021-05-26 11:08:31 +08:00
parent 0419cf96fc
commit 6061f5a074
8 changed files with 169 additions and 213 deletions

View file

@ -0,0 +1,151 @@
using System;
using System.Runtime.InteropServices;
using System.Security;
namespace SourceGit.Views.Controls {
[SuppressUnmanagedCodeSecurity]
internal delegate Int32 BrowseCallbackProc(IntPtr hwnd, Int32 msg, IntPtr lParam, IntPtr lpData);
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
[SuppressUnmanagedCodeSecurity]
internal class BrowseInfo {
public IntPtr hwndOwner;
public IntPtr pidlRoot;
public IntPtr pszDisplayName;
public String lpszTitle;
public Int32 ulFlags;
public BrowseCallbackProc lpfn;
public IntPtr lParam;
public Int32 iImage;
}
/// <summary>
/// Win32 API封装user32.dll)
/// </summary>
[SuppressUnmanagedCodeSecurity]
internal static class User32 {
[DllImport("user32.dll", CharSet = CharSet.Auto)]
public static extern IntPtr SendMessage(HandleRef hWnd, Int32 msg, Int32 wParam, String lParam);
[DllImport("user32.dll", CharSet = CharSet.Auto)]
public static extern IntPtr SendMessage(HandleRef hWnd, Int32 msg, Int32 wParam, Int32 lParam);
}
/// <summary>
/// Win32 API封装ole32.dll)
/// </summary>
[SuppressUnmanagedCodeSecurity]
internal static class Ole32 {
[DllImport("ole32.dll", CharSet = CharSet.Auto, ExactSpelling = true, SetLastError = true)]
internal static extern void CoTaskMemFree(IntPtr pv);
}
/// <summary>
/// Win32 API封装shell32.dll)
/// </summary>
[SuppressUnmanagedCodeSecurity]
internal static class Shell32 {
[DllImport("shell32.dll")]
public static extern Int32 SHGetSpecialFolderLocation(IntPtr hwnd, Int32 csidl, ref IntPtr ppidl);
[DllImport("shell32.dll", CharSet = CharSet.Auto)]
public static extern Boolean SHGetPathFromIDList(IntPtr pidl, IntPtr pszPath);
[DllImport("shell32.dll", CharSet = CharSet.Auto)]
public static extern IntPtr SHBrowseForFolder([In] BrowseInfo lpbi);
}
/// <summary>
/// 调用WindowsAPI打开对话目录对话框
/// </summary>
public class FolderDialog : Microsoft.Win32.CommonDialog {
/// <summary>
/// 描述信息
/// </summary>
public string Description { get; set; }
/// <summary>
/// 选中的目录
/// </summary>
public string SelectedPath { get; private set; }
public FolderDialog(string descKey) {
Description = App.Text(descKey);
SelectedPath = string.Empty;
}
public override void Reset() {
Description = string.Empty;
SelectedPath = string.Empty;
}
protected override bool RunDialog(IntPtr hwndOwner) {
IntPtr ppidl = IntPtr.Zero;
Shell32.SHGetSpecialFolderLocation(hwndOwner, (Int32)Environment.SpecialFolder.Desktop, ref ppidl);
if (ppidl == IntPtr.Zero) {
Shell32.SHGetSpecialFolderLocation(hwndOwner, 0, ref ppidl);
if (ppidl == IntPtr.Zero) {
Models.Exception.Raise("Failed to open folder dialog!!!");
return false;
}
}
BrowseCallbackProc callback = new BrowseCallbackProc(BrowseCallbackHandler);
IntPtr displayName = Marshal.AllocHGlobal(260 * Marshal.SystemDefaultCharSize);
bool ok = false;
try {
var info = new BrowseInfo();
info.pidlRoot = ppidl;
info.hwndOwner = hwndOwner;
info.pszDisplayName = displayName;
info.lpszTitle = Description;
info.ulFlags = 0x0040;
info.lpfn = callback;
info.lParam = IntPtr.Zero;
info.iImage = 0;
IntPtr result = Shell32.SHBrowseForFolder(info);
if (result != IntPtr.Zero) {
IntPtr pathPtr = Marshal.AllocHGlobal(260 * Marshal.SystemDefaultCharSize);
Shell32.SHGetPathFromIDList(result, pathPtr);
if (pathPtr != IntPtr.Zero) {
SelectedPath = Marshal.PtrToStringAuto(pathPtr);
ok = true;
Marshal.FreeHGlobal(pathPtr);
}
Ole32.CoTaskMemFree(result);
}
} finally {
Ole32.CoTaskMemFree(ppidl);
if (displayName != IntPtr.Zero) Marshal.FreeHGlobal(displayName);
callback = null;
}
return ok;
}
private Int32 BrowseCallbackHandler(IntPtr hwnd, Int32 msg, IntPtr lParam, IntPtr lpData) {
switch (msg) {
case 1:
if (!string.IsNullOrEmpty(SelectedPath)) {
Int32 flag = Marshal.SystemDefaultCharSize == 1 ? 1126 : 1127;
User32.SendMessage(new HandleRef(null, hwnd), flag, 1, SelectedPath);
}
break;
case 2:
if (lParam != IntPtr.Zero) {
IntPtr pathPtr = Marshal.AllocHGlobal(260 * Marshal.SystemDefaultCharSize);
bool flag = Shell32.SHGetPathFromIDList(lParam, pathPtr);
Marshal.FreeHGlobal(pathPtr);
User32.SendMessage(new HandleRef(null, hwnd), 1125, 0, flag ? 1 : 0);
}
break;
}
return 0;
}
}
}

View file

@ -1,98 +0,0 @@
<Window x:Class="SourceGit.Views.FolderBrowser"
x:Name="me"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:controls="clr-namespace:SourceGit.Views.Controls"
mc:Ignorable="d"
UseLayoutRounding="True"
Title="{StaticResource Text.FolderDialog}"
WindowStartupLocation="CenterOwner"
ResizeMode="NoResize"
Height="400" Width="400">
<WindowChrome.WindowChrome>
<WindowChrome UseAeroCaptionButtons="False" CornerRadius="0" CaptionHeight="28" ResizeBorderThickness="1"/>
</WindowChrome.WindowChrome>
<controls:WindowBorder>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="28"/>
<RowDefinition Height="32"/>
<RowDefinition Height="*"/>
<RowDefinition Height="48"/>
</Grid.RowDefinitions>
<!-- Title Bar -->
<Grid Grid.Row="0" Background="{StaticResource Brush.TitleBar}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<!-- ICON -->
<Path Grid.Column="0" Width="16" Height="16" Margin="6,0" Data="{StaticResource Icon.Folder.Open}"/>
<!-- Title -->
<TextBlock Grid.Column="1" Text="{Binding ElementName=me, Path=Description}"/>
<!-- Close -->
<controls:IconButton
Grid.Column="3"
Click="Quit"
Width="28" Padding="8"
WindowChrome.IsHitTestVisibleInChrome="True"
Icon="{StaticResource Icon.Close}"
HoverBackground="Red"/>
</Grid>
<!-- Selected -->
<StackPanel Grid.Row="1" Orientation="Horizontal" Margin="8,4">
<TextBlock Text="{StaticResource Text.FolderDialog.Selected}"/>
<TextBlock x:Name="txtSelected" Text="NONE" Foreground="{StaticResource Brush.FG2}" Margin="8,0,0,0"/>
</StackPanel>
<!-- File System Tree -->
<Border
Grid.Row="2"
Margin="8,0"
Background="{StaticResource Brush.Contents}"
BorderBrush="{StaticResource Brush.Border0}"
BorderThickness="1">
<controls:Tree x:Name="tree" FontFamily="Consolas" ItemsSource="{Binding ElementName=me, Path=Nodes}" SelectionChanged="OnTreeSelectionChanged">
<controls:Tree.ItemContainerStyle>
<Style TargetType="{x:Type controls:TreeItem}" BasedOn="{StaticResource Style.TreeItem}">
<EventSetter Event="Expanded" Handler="OnTreeNodeExpanded"/>
</Style>
</controls:Tree.ItemContainerStyle>
<controls:Tree.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Children}">
<StackPanel Orientation="Horizontal" Height="24">
<Path x:Name="Icon" Width="14" Height="14" Fill="Goldenrod" Data="{StaticResource Icon.Folder.Fill}"/>
<TextBlock Text="{Binding Name}" Margin="6,0,0,0"/>
</StackPanel>
<HierarchicalDataTemplate.Triggers>
<DataTrigger Binding="{Binding RelativeSource={RelativeSource AncestorType={x:Type controls:TreeItem}}, Path=IsExpanded}" Value="True">
<Setter TargetName="Icon" Property="Data" Value="{StaticResource Icon.Folder.Open}"/>
</DataTrigger>
</HierarchicalDataTemplate.Triggers>
</HierarchicalDataTemplate>
</controls:Tree.ItemTemplate>
</controls:Tree>
</Border>
<!-- Options -->
<Border Grid.Row="3">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<Button x:Name="btnSure" IsEnabled="False" Width="100" Height="32" Content="{StaticResource Text.Sure}" Background="{StaticResource Brush.Accent1}" BorderBrush="{StaticResource Brush.FG1}"/>
<Button Click="Quit" Width="100" Height="32" Margin="8,0,0,0" Content="{StaticResource Text.Cancel}"/>
</StackPanel>
</Border>
</Grid>
</controls:WindowBorder>
</Window>

View file

@ -1,102 +0,0 @@
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.IO;
using System.Windows;
namespace SourceGit.Views {
/// <summary>
/// 目录选择对话框
/// </summary>
public partial class FolderBrowser : Window {
/// <summary>
/// 目录树节点.
/// </summary>
public class Node : INotifyPropertyChanged {
public string Name { get; set; }
public string Path { get; set; }
public ObservableCollection<Node> Children { get; set; }
public event PropertyChangedEventHandler PropertyChanged;
public Node(string name, string path) {
Name = name;
Path = path;
Children = new ObservableCollection<Node>();
}
public void CollectChildren() {
Children.Clear();
try {
var dir = new DirectoryInfo(Path);
var subs = dir.GetDirectories();
foreach (var sub in subs) {
if ((sub.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden) continue;
Children.Add(new Node(sub.Name, sub.FullName));
}
} catch { }
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Children"));
}
}
public string Description { get; set; }
public ObservableCollection<Node> Nodes { get; set; }
public FolderBrowser(string description, Action<string> onOK) {
Description = description;
Nodes = new ObservableCollection<Node>();
var drives = DriveInfo.GetDrives();
foreach (var d in drives) {
var node = new Node(d.Name, d.Name);
node.CollectChildren();
Nodes.Add(node);
}
InitializeComponent();
btnSure.Click += (o, e) => {
if (tree.Selected.Count == 0) return;
var node = tree.Selected[0] as Node;
onOK?.Invoke(node.Path);
Close();
};
}
public static void Open(Window owner, string description, Action<string> onOK) {
var dialog = new FolderBrowser(description, onOK);
if (owner == null) dialog.Owner = Application.Current.MainWindow;
else dialog.Owner = owner;
dialog.ShowDialog();
}
private void OnTreeNodeExpanded(object sender, RoutedEventArgs e) {
var item = sender as Controls.TreeItem;
if (item == null) return;
var node = item.DataContext as Node;
if (node == null) return;
foreach (var c in node.Children) c.CollectChildren();
e.Handled = true;
}
private void OnTreeSelectionChanged(object sender, RoutedEventArgs e) {
if (tree.Selected.Count == 0) {
btnSure.IsEnabled = false;
txtSelected.Text = "NONE";
} else {
btnSure.IsEnabled = true;
txtSelected.Text = (tree.Selected[0] as Node).Path;
}
}
private void Quit(object sender, RoutedEventArgs e) {
Close();
}
}
}

View file

@ -54,10 +54,11 @@ namespace SourceGit.Views.Popups {
} }
private void OnFolderSelectorClick(object sender, System.Windows.RoutedEventArgs e) { private void OnFolderSelectorClick(object sender, System.Windows.RoutedEventArgs e) {
FolderBrowser.Open(null, App.Text("Clone.Folder.Placeholder"), path => { var dialog = new Controls.FolderDialog("Clone.Folder.Placeholder");
Folder = path; if (dialog.ShowDialog() == true) {
Folder = dialog.SelectedPath;
txtFolder.GetBindingExpression(TextBox.TextProperty).UpdateTarget(); txtFolder.GetBindingExpression(TextBox.TextProperty).UpdateTarget();
}); }
} }
} }
} }

View file

@ -61,10 +61,11 @@ namespace SourceGit.Views {
} }
private void SelectGitCloneDir(object sender, RoutedEventArgs e) { private void SelectGitCloneDir(object sender, RoutedEventArgs e) {
FolderBrowser.Open(this, App.Text("Preference.Dialog.GitDir"), path => { var dialog = new Controls.FolderDialog("Preference.Dialog.GitDir");
Models.Preference.Instance.Git.DefaultCloneDir = path; if (dialog.ShowDialog() == true) {
Models.Preference.Instance.Git.DefaultCloneDir = dialog.SelectedPath;
txtGitCloneDir?.GetBindingExpression(TextBox.TextProperty).UpdateTarget(); txtGitCloneDir?.GetBindingExpression(TextBox.TextProperty).UpdateTarget();
}); }
} }
private void SelectMergeTool(object sender, RoutedEventArgs e) { private void SelectMergeTool(object sender, RoutedEventArgs e) {

View file

@ -342,9 +342,10 @@ namespace SourceGit.Views.Widgets {
var saveToPatch = new MenuItem(); var saveToPatch = new MenuItem();
saveToPatch.Header = App.Text("CommitCM.SaveAsPatch"); saveToPatch.Header = App.Text("CommitCM.SaveAsPatch");
saveToPatch.Click += (o, e) => { saveToPatch.Click += (o, e) => {
FolderBrowser.Open(null, "Save patch to ...", saveTo => { var dialog = new Controls.FolderDialog("SaveFileTo");
new Commands.FormatPatch(repo.Path, commit.SHA, saveTo).Exec(); if (dialog.ShowDialog() == true) {
}); new Commands.FormatPatch(repo.Path, commit.SHA, dialog.SelectedPath).Exec();
}
}; };
menu.Items.Add(saveToPatch); menu.Items.Add(saveToPatch);
menu.Items.Add(new Separator()); menu.Items.Add(new Separator());

View file

@ -289,10 +289,11 @@ namespace SourceGit.Views.Widgets {
saveAs.Header = App.Text("SaveAs"); saveAs.Header = App.Text("SaveAs");
saveAs.IsEnabled = node.Type == Models.ObjectType.Blob; saveAs.IsEnabled = node.Type == Models.ObjectType.Blob;
saveAs.Click += (obj, ev) => { saveAs.Click += (obj, ev) => {
FolderBrowser.Open(null, App.Text("SaveFileTo"), saveTo => { var dialog = new Controls.FolderDialog("SaveFileTo");
var full = Path.Combine(saveTo, Path.GetFileName(node.Path)); if (dialog.ShowDialog() == true) {
var full = Path.Combine(dialog.SelectedPath, Path.GetFileName(node.Path));
new Commands.SaveRevisionFile(repo, node.Path, sha, full).Exec(); new Commands.SaveRevisionFile(repo, node.Path, sha, full).Exec();
}); }
ev.Handled = true; ev.Handled = true;
}; };

View file

@ -47,7 +47,8 @@ namespace SourceGit.Views.Widgets {
#region FUNC_EVENTS #region FUNC_EVENTS
private void OnOpenClicked(object sender, RoutedEventArgs e) { private void OnOpenClicked(object sender, RoutedEventArgs e) {
FolderBrowser.Open(null, App.Text("Welcome.OpenOrInitDialog"), CheckAndOpen); var dialog = new Controls.FolderDialog("Welcome.OpenOrInitDialog");
if (dialog.ShowDialog() == true) CheckAndOpen(dialog.SelectedPath);
} }
private void OnCloneClicked(object sender, RoutedEventArgs e) { private void OnCloneClicked(object sender, RoutedEventArgs e) {