rustydialogs 0.4.2

Provides a simple and cross-platform way to display native dialog boxes.
Documentation
use windows::core::{Interface, PCWSTR};
use windows::Win32::Foundation::RPC_E_CHANGED_MODE;
use windows::Win32::System::Com::{
	CoCreateInstance, CoInitializeEx, CoTaskMemFree, CoUninitialize, CLSCTX_INPROC_SERVER,
	COINIT_APARTMENTTHREADED, COINIT_DISABLE_OLE1DDE,
};
use windows::Win32::UI::Shell::{
	FileOpenDialog, FileSaveDialog, IFileDialog, IFileOpenDialog, IFileSaveDialog, IShellItem,
	FOS_ALLOWMULTISELECT, FOS_FILEMUSTEXIST, FOS_FORCEFILESYSTEM, FOS_OVERWRITEPROMPT,
	FOS_PATHMUSTEXIST, SHCreateItemFromParsingName, SIGDN_FILESYSPATH,
};
use windows::Win32::UI::Shell::Common::COMDLG_FILTERSPEC;

use super::*;

pub fn pick_file(p: &FileDialog<'_>) -> Option<PathBuf> {
	pick_files_impl(p, false).and_then(|paths| paths.into_iter().next())
}

pub fn pick_files(p: &FileDialog<'_>) -> Option<Vec<PathBuf>> {
	pick_files_impl(p, true)
}

pub fn save_file(p: &FileDialog<'_>) -> Option<PathBuf> {
	let _com = ComApartment::init()?;

	let dialog: IFileSaveDialog = unsafe {
		CoCreateInstance(&FileSaveDialog, None, CLSCTX_INPROC_SERVER).ok()?
	};
	let file_dialog: IFileDialog = dialog.cast().ok()?;

	configure_dialog(&file_dialog, p, false, true)?;

	if unsafe { file_dialog.Show(hwnd(p.owner)) }.is_err() {
		return None;
	}

	let item = unsafe { dialog.GetResult() }.ok()?;
	path_from_shell_item(&item)
}

fn pick_files_impl(p: &FileDialog<'_>, allow_multiple_selects: bool) -> Option<Vec<PathBuf>> {
	let _com = ComApartment::init()?;

	let dialog: IFileOpenDialog = unsafe {
		CoCreateInstance(&FileOpenDialog, None, CLSCTX_INPROC_SERVER).ok()?
	};
	let file_dialog: IFileDialog = dialog.cast().ok()?;

	configure_dialog(&file_dialog, p, allow_multiple_selects, false)?;

	if unsafe { file_dialog.Show(hwnd(p.owner)) }.is_err() {
		return None;
	}

	if allow_multiple_selects {
		let items = unsafe { dialog.GetResults() }.ok()?;
		let count = unsafe { items.GetCount() }.ok()?;
		let mut paths = Vec::with_capacity(count as usize);
		for index in 0..count {
			let item = unsafe { items.GetItemAt(index) }.ok()?;
			paths.push(path_from_shell_item(&item)?);
		}
		Some(paths)
	} else {
		let item = unsafe { dialog.GetResult() }.ok()?;
		Some(vec![path_from_shell_item(&item)?])
	}
}

fn configure_dialog(dialog: &IFileDialog, p: &FileDialog<'_>, allow_multiple_selects: bool, is_save: bool) -> Option<()> {
	let title = utf16cs(p.title);
	let mut filters = build_windows_filter(p.filter);

	if !p.title.is_empty() {
		unsafe { dialog.SetTitle(PCWSTR(title.as_ptr())) }.ok()?;
	}

	if let Some(filters) = filters.as_mut() {
		unsafe { dialog.SetFileTypes(&filters.specs) }.ok()?;
	}

	let mut options = unsafe { dialog.GetOptions() }.ok()?;
	options |= FOS_FORCEFILESYSTEM | FOS_PATHMUSTEXIST;
	if is_save {
		options |= FOS_OVERWRITEPROMPT;
	} else if allow_multiple_selects {
		options |= FOS_ALLOWMULTISELECT | FOS_FILEMUSTEXIST;
	} else {
		options |= FOS_FILEMUSTEXIST;
	}
	unsafe { dialog.SetOptions(options) }.ok()?;

	let initial_path = utils::abspath(p.path);
	if initial_path.is_dir() {
		let path = utf16cs(&initial_path.to_string_lossy());
		if let Some(folder) = shell_item_from_path(&path) {
			unsafe { dialog.SetDefaultFolder(&folder) }.ok()?;
		}
	} else {
		if let Some(parent) = initial_path.parent() {
			let parent_path = utf16cs(&parent.to_string_lossy());
			if let Some(folder) = shell_item_from_path(&parent_path) {
				unsafe { dialog.SetDefaultFolder(&folder) }.ok()?;
			}
		}
		if let Some(file_name) = initial_path.file_name().and_then(|name| name.to_str()) {
			let name = utf16cs(file_name);
			unsafe { dialog.SetFileName(PCWSTR(name.as_ptr())) }.ok()?;
		}
	}

	Some(())
}

fn path_from_shell_item(item: &IShellItem) -> Option<PathBuf> {
	let display_name = unsafe { item.GetDisplayName(SIGDN_FILESYSPATH) }.ok()?;
	let path = unsafe {
		let ptr = display_name.0;
		if ptr.is_null() {
			None
		} else {
			let mut length = 0usize;
			while *ptr.add(length) != 0 {
				length += 1;
			}
			Some(PathBuf::from(String::from_utf16_lossy(std::slice::from_raw_parts(ptr, length))))
		}
	};
	unsafe {
		CoTaskMemFree(Some(display_name.0 as _));
	}
	path
}

fn shell_item_from_path(path: &[u16]) -> Option<IShellItem> {
	unsafe { SHCreateItemFromParsingName(PCWSTR(path.as_ptr()), None).ok() }
}

struct DialogFilters {
	_names: Vec<Vec<u16>>,
	_specs_storage: Vec<Vec<u16>>,
	specs: Vec<COMDLG_FILTERSPEC>,
}

fn build_windows_filter(filter: Option<&[FileFilter<'_>]>) -> Option<DialogFilters> {
	let filter = filter?;
	if filter.is_empty() {
		return None;
	}

	let mut names = Vec::with_capacity(filter.len() + 1);
	let mut specs_storage = Vec::with_capacity(filter.len() + 1);

	for entry in filter {
		names.push(utf16cs(entry.desc));
		specs_storage.push(utf16cs(&utils::PrintJoin {
			parts: entry.patterns,
			separator: ";",
		}.to_string()));
	}

	names.push(utf16cs("All Files"));
	specs_storage.push(utf16cs("*.*"));

	let mut specs = Vec::with_capacity(names.len());
	for index in 0..names.len() {
		specs.push(COMDLG_FILTERSPEC {
			pszName: PCWSTR(names[index].as_ptr()),
			pszSpec: PCWSTR(specs_storage[index].as_ptr()),
		});
	}

	Some(DialogFilters {
		_names: names,
		_specs_storage: specs_storage,
		specs,
	})
}

struct ComApartment {
	should_uninitialize: bool,
}

impl ComApartment {
	fn init() -> Option<Self> {
		let result = unsafe {
			CoInitializeEx(
				None,
				COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE,
			)
		};

		if result.is_ok() {
			Some(Self {
				should_uninitialize: true,
			})
		} else if result == RPC_E_CHANGED_MODE {
			Some(Self {
				should_uninitialize: false,
			})
		} else {
			None
		}
	}
}

impl Drop for ComApartment {
	fn drop(&mut self) {
		if self.should_uninitialize {
			unsafe {
				CoUninitialize();
			}
		}
	}
}