1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
// Copyright 2018 The xi-editor Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//! File open/save dialogs, Windows implementation.
//!
//! For more information about how windows handles file dialogs, see
//! documentation for [_FILEOPENDIALOGOPTIONS] and [SetFileTypes].
//!
//! [_FILEOPENDIALOGOPTIONS]: https://docs.microsoft.com/en-us/windows/desktop/api/shobjidl_core/ne-shobjidl_core-_fileopendialogoptions
//! [SetFileTypes]: https://docs.microsoft.com/en-ca/windows/win32/api/shobjidl_core/nf-shobjidl_core-ifiledialog-setfiletypes

#![allow(non_upper_case_globals)]

use winapi::shared::minwindef::*;
use winapi::shared::ntdef::LPWSTR;
use winapi::shared::windef::*;
use winapi::shared::wtypesbase::*;
use winapi::um::combaseapi::*;
use winapi::um::shobjidl::*;
use winapi::um::shobjidl_core::*;
use winapi::um::shtypes::COMDLG_FILTERSPEC;
use winapi::Interface;
use wio::com::ComPtr;

use crate::dialog::{FileDialogOptions, FileDialogType, FileSpec};
use crate::util::{as_result, FromWide, ToWide};
use crate::Error;
use std::ffi::OsString;
use std::ptr::null_mut;

// TODO: remove these when they get added to winapi
DEFINE_GUID! {CLSID_FileOpenDialog,
0xDC1C_5A9C, 0xE88A, 0x4DDE, 0xA5, 0xA1, 0x60, 0xF8, 0x2A, 0x20, 0xAE, 0xF7}
DEFINE_GUID! {CLSID_FileSaveDialog,
0xC0B4_E2F3, 0xBA21, 0x4773, 0x8D, 0xBA, 0x33, 0x5E, 0xC9, 0x46, 0xEB, 0x8B}

/// For each item in `spec`, returns a pair of utf16 strings representing the name
/// and the filter spec.
///
/// As an example: given the name `"Markdown Document"`, and the extensions
/// `&["md", "mdown", "markdown"]`, this will return
/// `("Markdown Document (*.md; *.mdown; *.markdown)", "*.md;*.mdown;*.markdown")`.
///
/// The first of these is displayed to the user, and the second is used to match the path.
unsafe fn make_wstrs(spec: &FileSpec) -> (Vec<u16>, Vec<u16>) {
    let exts = spec
        .extensions
        .iter()
        .map(normalize_extension)
        .collect::<Vec<_>>();
    let name = format!("{} ({})", spec.name, exts.as_slice().join("; ")).to_wide();
    let extensions = exts.as_slice().join(";").to_wide();
    (name, extensions)
}

/// add preceding *., trimming preceding *. that might've been included by the user.
fn normalize_extension(ext: &&str) -> String {
    format!("*.{}", ext.trim_start_matches('*').trim_start_matches('.'))
}

pub(crate) unsafe fn get_file_dialog_path(
    hwnd_owner: HWND,
    ty: FileDialogType,
    options: FileDialogOptions,
) -> Result<OsString, Error> {
    let mut pfd: *mut IFileDialog = null_mut();
    let (class, id) = match ty {
        FileDialogType::Open => (&CLSID_FileOpenDialog, IFileOpenDialog::uuidof()),
        FileDialogType::Save => (&CLSID_FileSaveDialog, IFileSaveDialog::uuidof()),
    };
    as_result(CoCreateInstance(
        class,
        null_mut(),
        CLSCTX_INPROC_SERVER,
        &id,
        &mut pfd as *mut *mut IFileDialog as *mut LPVOID,
    ))?;
    let file_dialog = ComPtr::from_raw(pfd);

    // set options
    let mut flags: DWORD = 0;
    if options.show_hidden {
        flags |= FOS_FORCESHOWHIDDEN;
    }

    // - allowed filetypes

    // this is a vec of vec<u16>, e.g wide strings. this memory needs to be live
    // until `Show` returns.
    let spec = options.allowed_types.as_ref().map(|allowed_types| {
        allowed_types
            .iter()
            .map(|t| make_wstrs(t))
            .collect::<Vec<_>>()
    });

    // this is a vector of structs whose members point into the vector above.
    // this has to outlive that vector.
    let raw_spec = spec.as_ref().map(|buf_pairs| {
        buf_pairs
            .iter()
            .map(|(name, ext)| COMDLG_FILTERSPEC {
                pszName: name.as_ptr(),
                pszSpec: ext.as_ptr(),
            })
            .collect::<Vec<_>>()
    });

    if let Some(spec) = &raw_spec {
        as_result(file_dialog.SetFileTypes(spec.len() as u32, spec.as_ptr()))?;
        as_result(file_dialog.SetFileTypeIndex(1))?;
    }

    as_result(file_dialog.SetOptions(flags))?;

    // show the dialog
    as_result(file_dialog.Show(hwnd_owner))?;
    let mut result_ptr: *mut IShellItem = null_mut();
    as_result(file_dialog.GetResult(&mut result_ptr))?;
    let shell_item = ComPtr::from_raw(result_ptr);
    let mut display_name: LPWSTR = null_mut();
    as_result(shell_item.GetDisplayName(SIGDN_FILESYSPATH, &mut display_name))?;
    let filename = display_name.to_os_string();
    CoTaskMemFree(display_name as LPVOID);

    Ok(filename)
}