use core::fmt;
use std::path::{Path, PathBuf};
use crate::event::Event;
use crate::ui::Ui;
use crate::{ui_ref::UiRef, GemGuiError, JSMap, JSType, ui_data::UiData, JSMessageTx, ui::private::UserInterface};
use futures::Future;
#[derive(Clone)]
pub struct Menu {
items: Vec<JSType>,
}
impl Default for Menu {
fn default() -> Self {
Self::new()
}
}
pub (crate) static MENU_ELEMENT: &str = "app menu";
static MENU_EVENT: &str = "menu_event";
#[derive(serde::Deserialize, serde::Serialize, Debug, Default)]
struct MenuItems {
#[serde(rename = "type")]
_type: String,
#[serde(skip_serializing_if = "Option::is_none")]
action_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
sub_menu: Option<Vec<JSType>>,
}
impl Menu {
pub fn new() -> Menu {
Menu {items: Vec::new()}
}
pub fn add_separator(mut self) -> Menu{
let item = MenuItems {
_type: "separator".to_string(),
..Default::default()
};
let json = serde_json::to_value(item).unwrap();
self.items.push(json);
self
}
pub fn add_item(mut self, title: &str, action_id: &str) -> Menu {
let item = MenuItems {
_type: "action".to_string(),
title: Some(title.to_string()),
action_id: Some(action_id.to_string()),
..Default::default()
};
let json = serde_json::to_value(item).unwrap();
self.items.push(json);
self
}
pub fn add_sub_menu(mut self, title: &str, menu: Menu) -> Menu {
let item = MenuItems {
_type: "sub_menu".to_string(),
title: Some(title.to_string()),
sub_menu: Some(menu.items),
..Default::default()
};
let json = serde_json::to_value(item).unwrap();
self.items.push(json);
self
}
pub fn subscribe<CB>(ui: &UiRef, callback: CB)
where CB: FnMut(UiRef, &str) + Send + Clone + 'static {
let element_cb = move |ui: UiRef, event: Event| {
let mut callback = callback.clone();
let id = event.property_str("menu_id").expect("Invalid event");
callback(ui, id)
};
ui.element(MENU_ELEMENT).subscribe(MENU_EVENT, element_cb)
}
pub fn subscribe_async<CB, Fut>(ui: &UiRef, callback: CB)
where CB: FnOnce(UiRef, &str)-> Fut + Send + Clone + 'static,
Fut: Future<Output = ()> + Send + 'static {
let element_cb = |ui: UiRef, event: Event| async move {
let id = event.property_str("menu_id").expect("Invalid event");
callback(ui, id).await
};
ui.element(MENU_ELEMENT).subscribe_async(MENU_EVENT, element_cb)
}
#[allow(clippy::inherent_to_string)]
pub (crate) fn to_string(&self) -> String {
serde_json::to_string(&self.items).unwrap()
}
}
enum DialogType {
OpenFile,
OpenFiles,
OpenDir,
SaveFile,
}
impl fmt::Display for DialogType {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
DialogType::OpenFile => write!(f, "openFile"),
DialogType::OpenFiles => write!(f, "openFiles"),
DialogType::OpenDir => write!(f, "openDir"),
DialogType::SaveFile => write!(f, "saveFile"),
}
}
}
enum DialogValue {
FileName(String),
FileNames(Vec<String>),
}
fn make_filters(filters: &[(&str, std::vec::Vec<&str>)]) -> JSMap {
let mut ft = JSMap::new();
for (name, exts) in filters.iter() {
let mut ext_vec = Vec::new();
for ext in exts.iter() {
ext_vec.push(serde_json::json!(ext));
}
ft.insert(name.to_string(), serde_json::json!(ext_vec));
}
ft
}
pub async fn open_file(ui: &UiRef, dir: &Path, filters: &[(&str, std::vec::Vec<&str>)]) -> Result<PathBuf, GemGuiError> {
let ft = make_filters(filters);
let mut properties = JSMap::new();
properties.insert("dir".to_string(), JSType::from(dir.to_string_lossy()));
properties.insert("filters".to_string(), serde_json::json!(ft));
let file_name = dialog(ui, DialogType::OpenFile, properties).await?;
if let DialogValue::FileName(file_name) = file_name {
let path = Path::new(&file_name);
return Ok(path.to_path_buf());
}
GemGuiError::error("Invalid type".to_string())
}
pub async fn open_files(ui: &UiRef, dir: &Path, filters: &[(&str, std::vec::Vec<&str>)]) -> Result<Vec<PathBuf>, GemGuiError> {
let ft = make_filters(filters);
let mut properties = JSMap::new();
properties.insert("dir".to_string(), JSType::from(dir.to_string_lossy()));
properties.insert("filters".to_string(), serde_json::json!(ft));
let file_name = dialog(ui, DialogType::OpenFiles, properties).await?;
if let DialogValue::FileNames(file_names) = file_name {
let mut paths = Vec::new();
for fname in file_names.iter() {
let path = Path::new(&fname).to_path_buf();
paths.push(path);
}
return Ok(paths);
}
GemGuiError::error("Invalid type".to_string())
}
pub async fn open_dir(ui: &UiRef, dir: &Path) -> Result<PathBuf, GemGuiError> {
let mut properties = JSMap::new();
properties.insert("dir".to_string(), JSType::from(dir.to_string_lossy()));
let file_name = dialog(ui, DialogType::OpenDir, properties).await?;
if let DialogValue::FileName(file_name) = file_name {
let path = Path::new(&file_name);
return Ok(path.to_path_buf());
}
GemGuiError::error("Invalid type".to_string())
}
pub async fn save_file(ui: &UiRef, dir: &Path, filters: &[(&str, std::vec::Vec<&str>)]) -> Result<PathBuf, GemGuiError> {
let ft = make_filters(filters);
let mut properties = JSMap::new();
properties.insert("dir".to_string(), JSType::from(dir.to_string_lossy()));
properties.insert("filters".to_string(), serde_json::json!(ft));
let file_name = dialog(ui, DialogType::SaveFile, properties).await?;
if let DialogValue::FileName(file_name) = file_name {
let path = Path::new(&file_name);
return Ok(path.to_path_buf());
}
GemGuiError::error("Invalid type".to_string())
}
async fn dialog(ui: &UiRef, dialog_type: DialogType, dialog_params: JSMap) -> Result<DialogValue, GemGuiError> {
let (id, receiver) = UiData::new_query(ui.ui());
let extension_call = dialog_type.to_string();
let msg = JSMessageTx {
_type: "extension",
extension_id: Some(&id),
extension_call: Some(&extension_call),
extension_params: Some(&dialog_params),
..Default::default()
};
UiData::send(ui.ui(), msg);
let value = tokio::task::spawn_blocking(move || {
receiver.blocking_recv()
}).await.unwrap_or_else(|e| {panic!("Extension spawn blocking {e:#?}")});
match value {
Ok(value) => {
match dialog_type {
DialogType::OpenFiles => {
match crate::value_to_string_list(value) {
Some(v) => Ok(DialogValue::FileNames(v)),
None => GemGuiError::error("Bad value"),
}
},
_ => Ok(DialogValue::FileName(value.as_str().expect("Not a string").to_string()))
}
},
Err(e) => GemGuiError::error(format!("Extension error {e}"))
}
}