use std::collections::HashMap;
use annatto::{ReadFrom, importer::graphml::GraphMLImporter};
use anyhow::Context;
use egui::{Button, Color32, Id, InnerResponse, RichText, TextEdit, Widget};
use egui_file_dialog::FileDialog;
use facet::Facet;
use facet_reflect::peek_enum_variants;
use serde::{Deserialize, Serialize};
use crate::app::{messages::Notifier, util};
pub struct ImportConfigWidget<'a> {
widget_id: Id,
file_dialog: &'a mut FileDialog,
notifier: &'a Notifier,
}
#[derive(Serialize, Deserialize, Clone)]
pub struct ImportConfigWidgetState {
input_path: String,
current_importer: annatto::ReadFrom,
config_by_name: HashMap<String, String>,
overwrite_corpus_name: bool,
corpus_name: String,
additional_config: String,
config_error_message: Option<String>,
}
impl Default for ImportConfigWidgetState {
fn default() -> Self {
let graphml_config = GraphMLImporter::default();
Self {
input_path: String::default(),
current_importer: annatto::ReadFrom::GraphML(graphml_config),
overwrite_corpus_name: false,
corpus_name: String::default(),
additional_config: String::default(),
config_by_name: HashMap::new(),
config_error_message: None,
}
}
}
#[derive(Default)]
pub struct ImportConfigWidgetOutput {
pub import_step: Option<annatto::ImporterStep>,
pub overwrite_corpus_name: Option<String>,
}
impl<'a> ImportConfigWidget<'a> {
pub fn new<I: Into<Id>>(
widget_id: I,
notifier: &'a Notifier,
file_dialog: &'a mut FileDialog,
) -> Self {
Self {
widget_id: widget_id.into(),
notifier,
file_dialog,
}
}
pub fn load_state(&self, ctx: &egui::Context) -> ImportConfigWidgetState {
ctx.data_mut(|d| d.get_persisted(self.widget_id))
.unwrap_or_default()
}
pub fn store_state(&self, ctx: &egui::Context, state: ImportConfigWidgetState) {
ctx.data_mut(|d| d.insert_persisted(self.widget_id, state));
}
pub fn show(self, ui: &mut egui::Ui) -> InnerResponse<ImportConfigWidgetOutput> {
let mut state = self.load_state(ui.ctx());
let response = ui.vertical(|ui| {
let mut output = ImportConfigWidgetOutput::default();
let current_importer_name = state.current_importer.name().unwrap_or_default();
let cb_response = egui::ComboBox::from_label("Format")
.selected_text(¤t_importer_name)
.show_ui(ui, |ui| {
let mut result = None;
for variant in peek_enum_variants(ReadFrom::SHAPE).unwrap_or_default() {
if variant.data.fields.len() == 1
&& variant.data.fields[0].shape().is_default()
{
let module_name = variant.name.to_lowercase();
let config = state
.config_by_name
.entry(module_name.clone())
.or_insert_with(|| {
format!(
"format=\"{module_name}\"\npath=\"{}\"\n\n[config]\n",
&state.input_path.escape_default().to_string()
)
});
if let Some(deserialized_module) = self.notifier.ok_or_report(
toml::from_str::<ReadFrom>(config).with_context(|| {
format!("Creating module {module_name} failed")
}),
) && ui
.selectable_value(
&mut state.current_importer,
deserialized_module,
module_name,
)
.clicked()
{
result = Some(config.clone());
}
}
}
result
});
if let Some(Some(new_string)) = cb_response.inner
&& let Some(new_string) = self
.notifier
.ok_or_report(util::strip_module_header(&new_string))
{
state.additional_config = new_string;
}
ui.horizontal(|ui| {
TextEdit::singleline(&mut state.input_path)
.hint_text("Input path")
.show(ui);
if ui
.button("...")
.on_hover_text("Select the input path")
.clicked()
{
match state.current_importer {
ReadFrom::GraphML(_) => self.file_dialog.pick_file(),
_ => self.file_dialog.pick_directory(),
}
}
if let Some(new_path) = self.file_dialog.take_picked() {
state.input_path = new_path.to_string_lossy().to_string();
if let Some(file_stem) = new_path.file_stem() {
state.corpus_name = file_stem.to_string_lossy().to_string();
}
}
});
ui.hyperlink_to(
format!("Documentation for \"{current_importer_name}\""),
format!("https://github.com/korpling/annatto/blob/main/docs/importers/{current_importer_name}.md")
);
ui.label("Configuration");
let txt_config = TextEdit::multiline(&mut state.additional_config)
.code_editor()
.desired_width(ui.available_width())
.show(ui);
if txt_config.response.changed() {
state.config_error_message = match update_config_from_string(&mut state) {
Ok(_) => None,
Err(err) => Some(err.to_string()),
};
}
if let Some(err) = &state.config_error_message {
ui.label(RichText::new(err.to_string()).color(Color32::DARK_RED));
}
ui.horizontal(|ui| {
ui.checkbox(&mut state.overwrite_corpus_name, "Overwrite corpus name");
ui.add_enabled(
state.overwrite_corpus_name,
TextEdit::singleline(&mut state.corpus_name),
);
});
if ui
.add_enabled(
state.config_error_message.is_none() && !state.input_path.is_empty(),
Button::new("Start import"),
)
.clicked()
{
self.notifier
.ok_or_report(update_config_from_string(&mut state));
output.import_step = Some(annatto::ImporterStep::new(
state.current_importer.clone(),
state.input_path.clone(),
));
if state.overwrite_corpus_name {
output.overwrite_corpus_name = Some(state.corpus_name.clone());
}
}
output
});
self.store_state(ui.ctx(), state);
response
}
}
fn update_config_from_string(state: &mut ImportConfigWidgetState) -> anyhow::Result<()> {
let current_importer_name = state.current_importer.name()?;
let full_string = format!(
"format=\"{}\"\npath=\"{}\"\n\n[config]\n{}",
current_importer_name,
&state.input_path.escape_default().to_string(),
&state.additional_config
);
state.current_importer = toml::from_str(&full_string)?;
state
.config_by_name
.insert(current_importer_name, full_string);
Ok(())
}
impl<'a> Widget for ImportConfigWidget<'a> {
fn ui(self, ui: &mut egui::Ui) -> egui::Response {
self.show(ui).response
}
}