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
// Copyright 2019 The Druid 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, GTK implementation.

use std::ffi::OsString;

use anyhow::anyhow;
use gtk::{FileChooserAction, FileChooserExt, FileFilter, NativeDialogExt, ResponseType, Window};

use crate::dialog::{FileDialogOptions, FileDialogType, FileSpec};
use crate::Error;

fn file_filter(fs: &FileSpec) -> FileFilter {
    let ret = FileFilter::new();
    ret.set_name(Some(fs.name));
    for ext in fs.extensions {
        ret.add_pattern(&format!("*.{}", ext));
    }
    ret
}

pub(crate) fn get_file_dialog_path(
    window: &Window,
    ty: FileDialogType,
    options: FileDialogOptions,
) -> Result<OsString, Error> {
    // TODO: support message localization

    let (title, action) = match (ty, options.select_directories) {
        (FileDialogType::Open, false) => ("Open File", FileChooserAction::Open),
        (FileDialogType::Open, true) => ("Open Folder", FileChooserAction::SelectFolder),
        (FileDialogType::Save, _) => ("Save File", FileChooserAction::Save),
    };
    let title = options.title.as_deref().unwrap_or(title);

    let mut dialog = gtk::FileChooserNativeBuilder::new()
        .transient_for(window)
        .title(title);
    if let Some(button_text) = &options.button_text {
        dialog = dialog.accept_label(button_text);
    }
    let dialog = dialog.build();

    dialog.set_action(action);

    dialog.set_show_hidden(options.show_hidden);

    if action != FileChooserAction::Save {
        dialog.set_select_multiple(options.multi_selection);
    }

    // Don't set the filters when showing the folder selection dialog,
    // because then folder traversing won't work.
    if action != FileChooserAction::SelectFolder {
        let mut found_default_filter = false;
        if let Some(file_types) = &options.allowed_types {
            for f in file_types {
                let filter = file_filter(f);
                dialog.add_filter(&filter);

                if let Some(default) = &options.default_type {
                    if default == f {
                        // Note that we're providing the same FileFilter object to
                        // add_filter and set_filter, because gtk checks them for
                        // identity, not structural equality.
                        dialog.set_filter(&filter);
                        found_default_filter = true;
                    }
                }
            }
        }

        if let Some(dt) = &options.default_type {
            if !found_default_filter {
                tracing::warn!("The default type {:?} is not present in allowed types.", dt);
            }
        }
    }

    if let Some(default_name) = &options.default_name {
        dialog.set_current_name(default_name);
    }

    let result = dialog.run();

    let result = match result {
        ResponseType::Accept => match dialog.get_filename() {
            Some(path) => Ok(path.into_os_string()),
            None => Err(anyhow!("No path received for filename")),
        },
        ResponseType::Cancel => Err(anyhow!("Dialog was deleted")),
        _ => {
            tracing::warn!("Unhandled dialog result: {:?}", result);
            Err(anyhow!("Unhandled dialog result"))
        }
    };

    // TODO properly handle errors into the Error type

    dialog.destroy();

    Ok(result?)
}