extern crate libc;
extern crate winapi;
use crate::winapi::Interface;
use std::ffi::OsString;
use std::os::windows::ffi::OsStringExt;
use std::path::PathBuf;
use std::ptr::null_mut;
use std::slice;
use libc::wcslen;
use winapi::{
ctypes::c_void,
shared::{
minwindef::LPVOID,
ntdef::LPWSTR,
winerror::{HRESULT, SUCCEEDED}
},
um:: {
combaseapi::{CoCreateInstance, CoInitializeEx, CoTaskMemFree, CoUninitialize, CLSCTX_ALL},
objbase::{COINIT_APARTMENTTHREADED, COINIT_DISABLE_OLE1DDE},
shobjidl::{IFileDialog, IFileOpenDialog, IFileSaveDialog, IShellItemArray},
shobjidl_core::{CLSID_FileOpenDialog, CLSID_FileSaveDialog, IShellItem, SFGAOF, SHCreateItemFromParsingName, SIGDN_FILESYSPATH},
shtypes::COMDLG_FILTERSPEC,
}
};
pub use winapi::um::shobjidl::{
FOS_ALLNONSTORAGEITEMS, FOS_ALLOWMULTISELECT, FOS_CREATEPROMPT, FOS_DEFAULTNOMINIMODE,
FOS_DONTADDTORECENT, FOS_FILEMUSTEXIST, FOS_FORCEFILESYSTEM, FOS_FORCEPREVIEWPANEON,
FOS_FORCESHOWHIDDEN, FOS_HIDEMRUPLACES, FOS_HIDEPINNEDPLACES, FOS_NOCHANGEDIR,
FOS_NODEREFERENCELINKS, FOS_NOREADONLYRETURN, FOS_NOTESTFILECREATE, FOS_NOVALIDATE,
FOS_OVERWRITEPROMPT, FOS_PATHMUSTEXIST, FOS_PICKFOLDERS, FOS_SHAREAWARE, FOS_STRICTFILETYPES,
FOS_SUPPORTSTREAMABLEITEMS,
};
macro_rules! com {
($com_expr:expr, $method_name:expr ) => { com(|| unsafe { $com_expr }, $method_name) };
}
trait NullTermUTF16 {
fn as_null_term_utf16(&self) -> Vec<u16>;
}
impl NullTermUTF16 for str {
fn as_null_term_utf16(&self) -> Vec<u16> {
self.encode_utf16().chain(Some(0)).collect()
}
}
const SFGAO_FILESYSTEM: u32 = 0x4000_0000;
type FileExtensionFilterPair<'a> = (&'a str, &'a str);
#[derive(Debug)]
pub struct DialogParams<'a> {
pub default_extension: &'a str,
pub default_folder: &'a str,
pub file_name: &'a str,
pub file_name_label: &'a str,
pub file_type_index: u32,
pub file_types: Vec<(&'a str, &'a str)>,
pub folder: &'a str,
pub ok_button_label: &'a str,
pub options: u32,
pub save_as_item: &'a str,
pub title: &'a str
}
impl<'a> Default for DialogParams<'a> {
fn default() -> Self {
DialogParams {
default_extension: "",
default_folder: "",
file_name: "",
file_name_label: "",
file_type_index: 1,
file_types: vec![("All types (*.*)", "*.*")],
folder: "",
ok_button_label: "",
options: 0,
save_as_item: "",
title: "",
}
}
}
#[derive(Debug)]
pub struct OpenDialogResult {
pub selected_file_path: PathBuf,
pub selected_file_paths: Vec<PathBuf>,
pub selected_file_type_index: u32,
}
#[derive(Debug)]
pub struct SaveDialogResult {
pub selected_file_path: PathBuf,
pub selected_filter_index: u32,
}
#[derive(Debug)]
pub enum DialogError {
UserCancelled,
UnsupportedFilepath,
HResultFailed {
error_method: String,
hresult: i32
},
}
pub fn open_dialog(params: DialogParams) -> Result<OpenDialogResult, DialogError> {
com!(CoInitializeEx(
null_mut(),
COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE,
), "CoInitializeEx")?;
let mut file_open_dialog: *mut IFileOpenDialog = null_mut();
com!(CoCreateInstance(
&CLSID_FileOpenDialog,
null_mut(),
CLSCTX_ALL,
&IFileOpenDialog::uuidof(),
&mut file_open_dialog as *mut *mut IFileOpenDialog as *mut *mut c_void,
), "CoCreateInstance - IFileOpenDialog")?;
let file_open_dialog = unsafe { &*file_open_dialog };
configure_file_dialog(file_open_dialog, ¶ms)?;
show_dialog(file_open_dialog)?;
let mut shell_item_array: *mut IShellItemArray = null_mut();
com!(file_open_dialog.GetResults(&mut shell_item_array), "IFileOpenDialog::GetResults")?;
let shell_item_array = unsafe { &*shell_item_array };
let mut item_count: u32 = 0;
com!(shell_item_array.GetCount(&mut item_count), "IShellItemArray::GetCount")?;
let mut file_paths: Vec<PathBuf> = vec![];
for i in 0..item_count {
let mut shell_item: *mut IShellItem = null_mut();
com!(shell_item_array.GetItemAt(i, &mut shell_item), "IShellItemArray::GetItemAt")?;
let shell_item = unsafe { &*shell_item };
let mut attribs: SFGAOF = 0;
com!(shell_item.GetAttributes(SFGAO_FILESYSTEM, &mut attribs), "IShellItem::GetAttributes")?;
if attribs & SFGAO_FILESYSTEM == 0 {
continue;
}
let file_name = get_shell_item_display_name(&shell_item)?;
file_paths.push(PathBuf::from(file_name));
unsafe { shell_item.Release() };
}
let selected_filter_index = get_file_type_index(file_open_dialog)?;
unsafe {
CoUninitialize();
}
file_paths.get(0).cloned().map(|x| {
OpenDialogResult {
selected_file_path: x,
selected_file_paths: file_paths,
selected_file_type_index: selected_filter_index
}
}).ok_or(DialogError::UnsupportedFilepath)
}
pub fn save_dialog(params: DialogParams) -> Result<SaveDialogResult, DialogError> {
com!(CoInitializeEx(
null_mut(),
COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE,
)
, "CoInitializeEx")?;
let mut file_save_dialog: *mut IFileSaveDialog;
file_save_dialog = null_mut();
com!(CoCreateInstance(
&CLSID_FileSaveDialog,
null_mut(),
CLSCTX_ALL,
&IFileSaveDialog::uuidof(),
&mut file_save_dialog as *mut *mut IFileSaveDialog as *mut *mut c_void,
)
, "CoCreateInstance - FileSaveDialog")?;
let file_save_dialog = unsafe { &*file_save_dialog };
if params.save_as_item != "" {
let mut item: *mut IShellItem = null_mut();
let path = params.save_as_item.as_null_term_utf16();
com!(SHCreateItemFromParsingName(path.as_ptr(), null_mut(), &IShellItem::uuidof(), &mut item as *mut *mut IShellItem as *mut *mut c_void), "SHCreateItemFromParsingName")?;
com!(file_save_dialog.SetSaveAsItem(item), "IFileDialog::SetSaveAsItem")?;
unsafe {
let item = &*item;
item.Release();
}
}
configure_file_dialog(file_save_dialog, ¶ms)?;
show_dialog(file_save_dialog)?;
let mut shell_item: *mut IShellItem = null_mut();
com!(file_save_dialog.GetResult(&mut shell_item), "IFileDialog::GetResult")?;
let shell_item = unsafe { &*shell_item };
let file_name = get_shell_item_display_name(&shell_item)?;
unsafe { shell_item.Release() };
let selected_filter_index = get_file_type_index(file_save_dialog)?;
unsafe {
CoUninitialize();
}
let result = SaveDialogResult {
selected_filter_index,
selected_file_path: PathBuf::from(file_name),
};
Ok(result)
}
#[allow(overflowing_literals)]
#[allow(unused_comparisons)]
fn show_dialog(file_dialog: &IFileDialog) -> Result<(), DialogError> {
let result = com!(file_dialog.Show(null_mut()), "IModalWindow::Show");
match result {
Ok(_) => Ok(()),
Err(e) => match e {
DialogError::HResultFailed { hresult, .. } => {
if hresult == 0x8007_04C7 {
Err(DialogError::UserCancelled)
} else {
Err(e)
}
}
_ => Err(e),
},
}
}
fn configure_file_dialog(file_dialog: &IFileDialog, params: &DialogParams) -> Result<(), DialogError> {
if params.default_extension != "" {
let default_extension = params.default_extension.as_null_term_utf16();
com!(file_dialog.SetDefaultExtension(default_extension.as_ptr()), "IFileDialog::SetDefaultExtension")?;
}
if params.default_folder != "" {
let mut default_folder: *mut IShellItem = null_mut();
let path = params.default_folder.as_null_term_utf16();
com!(SHCreateItemFromParsingName(path.as_ptr(), null_mut(), &IShellItem::uuidof(), &mut default_folder as *mut *mut IShellItem as *mut *mut c_void), "SHCreateItemFromParsingName")?;
com!(file_dialog.SetDefaultFolder(default_folder), "IFileDialog::SetDefaultFolder")?;
unsafe {
let default_folder = &*default_folder;
default_folder.Release();
}
}
if params.folder != "" {
let mut folder: *mut IShellItem = null_mut();
let path = params.folder.as_null_term_utf16();
com!(SHCreateItemFromParsingName(path.as_ptr(), null_mut(), &IShellItem::uuidof(), &mut folder as *mut *mut IShellItem as *mut *mut c_void), "SHCreateItemFromParsingName")?;
com!(file_dialog.SetFolder(folder), "IFileDialog::SetFolder")?;
unsafe {
let folder = &*folder;
folder.Release();
}
}
if params.file_name != "" {
let initial_file_name = params.file_name.as_null_term_utf16();
com!(file_dialog.SetFileName(initial_file_name.as_ptr()), "IFileDialog::SetFileName")?;
}
if params.file_name_label != "" {
let file_name_label = params.file_name_label.as_null_term_utf16();
com!(file_dialog.SetFileNameLabel(file_name_label.as_ptr()), "IFileDialog::SetFileNameLabel")?;
}
if !params.file_types.is_empty() {
add_filters(file_dialog, ¶ms.file_types)?;
}
if !params.file_types.is_empty() && params.file_type_index > 0 {
com!(file_dialog.SetFileTypeIndex(params.file_type_index), "IFileDialog::SetFileTypeIndex")?;
}
if params.ok_button_label != "" {
let ok_buttom_label = params.ok_button_label.as_null_term_utf16();
com!(file_dialog.SetOkButtonLabel(ok_buttom_label.as_ptr()), "IFileDialog::SetOkButtonLabel")?;
}
if params.options > 0 {
let mut existing_options: u32 = 0;
com!(file_dialog.GetOptions(&mut existing_options), "IFileDialog::GetOptions")?;
com!(file_dialog.SetOptions(existing_options | params.options), "IFileDialog::SetOptions")?;
}
if params.title != "" {
let title = params.title.as_null_term_utf16();
com!(file_dialog.SetTitle(title.as_ptr()), "IFileDialog::SetTitle")?;
}
Ok(())
}
fn add_filters(dialog: &IFileDialog, filters: &[FileExtensionFilterPair]) -> Result<(), DialogError> {
let temp_filters = filters
.iter()
.map(|filter| {
let name = filter.0.as_null_term_utf16();
let pattern = filter.1.as_null_term_utf16();
(name, pattern)
})
.collect::<Vec<(Vec<u16>, Vec<u16>)>>();
let filter_specs = temp_filters
.iter()
.map(|x| COMDLG_FILTERSPEC {
pszName: x.0.as_ptr(),
pszSpec: x.1.as_ptr(),
})
.collect::<Vec<COMDLG_FILTERSPEC>>();
com!(dialog.SetFileTypes(filter_specs.len() as u32, filter_specs.as_ptr()), "IFileDialog::SetFileTypes")?;
Ok(())
}
fn get_file_type_index(file_dialog: &IFileDialog) -> Result<u32, DialogError> {
let mut selected_filter_index: u32 = 0;
com!(file_dialog.GetFileTypeIndex(&mut selected_filter_index), "IFileDialog::GetFileTypeIndex")?;
Ok(selected_filter_index)
}
fn get_shell_item_display_name(shell_item: &IShellItem) -> Result<OsString, DialogError> {
let mut display_name: LPWSTR = null_mut();
com!(shell_item.GetDisplayName(SIGDN_FILESYSPATH, &mut display_name), "IShellItem::GetDisplayName")?;
let slice = unsafe { slice::from_raw_parts(display_name, wcslen(display_name)) };
let result = OsString::from_wide(slice);
unsafe { CoTaskMemFree(display_name as LPVOID) };
Ok(result)
}
fn com<F>(mut f: F, method: &str) -> Result<(), DialogError>
where
F: FnMut() -> HRESULT,
{
let hresult = f();
if !SUCCEEDED(hresult) {
Err(DialogError::HResultFailed {
hresult,
error_method: method.to_string() })
} else {
Ok(())
}
}