use std::{
collections::{BTreeSet, HashSet},
fmt::Debug,
sync::Arc,
};
use anyhow::Context;
use egui::{
Button, CollapsingHeader, Id, Key, Link, Modifiers, Response, RichText, ScrollArea, TextEdit,
Ui, Widget, mutex::RwLock,
};
use egui_extras::{Column, TableRow};
use egui_notify::Toast;
use egui_phosphor::regular as icon;
use graphannis::{
AnnotationGraph,
graph::{AnnoKey, Annotation, NodeID},
model::{AnnotationComponent, AnnotationComponentType::PartOf},
};
use graphannis_core::{
annostorage::ValueSearch,
graph::{ANNIS_NS, NODE_NAME, NODE_NAME_KEY, NODE_TYPE},
};
use serde::{Deserialize, Serialize};
use crate::app::{
MainView, Notifier, OPEN_LABEL, actions::GraphAction, job_executor::JobExecutor,
project::EditorStateUpdates, views::Editor,
};
#[cfg(test)]
mod tests;
#[derive(Debug, PartialEq, Clone, Default, Eq, PartialOrd, Ord)]
struct MetaEntry {
ns: String,
name: String,
value: String,
old_value: String,
}
#[derive(Clone, PartialEq, Default, Debug)]
struct Data {
parent_node_name: String,
node_annos: Vec<MetaEntry>,
new_entry: MetaEntry,
corpus_graph_nodes: HashSet<NodeID>,
toplevel_nodes: BTreeSet<NodeID>,
}
#[derive(Serialize, Deserialize)]
struct CorpusTreeItem {
node: NodeID,
level: usize,
}
#[derive(Clone)]
pub(crate) struct CorpusTree {
root_corpus_name: String,
selected_corpus_node: Option<NodeID>,
request_focus: Option<NodeID>,
node_name_filter: String,
data: Data,
pending_actions: Vec<GraphAction>,
graph: Arc<RwLock<AnnotationGraph>>,
jobs: JobExecutor,
notifier: Notifier,
}
impl Debug for CorpusTree {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("CorpusTree")
.field("selected_corpus_node", &self.selected_corpus_node)
.field("data", &self.data)
.finish()
}
}
impl CorpusTree {
pub fn create_from_graph(
root_corpus_name: String,
graph: Arc<RwLock<AnnotationGraph>>,
selected_corpus_node: Option<NodeID>,
jobs: JobExecutor,
notifier: Notifier,
) -> anyhow::Result<Self> {
{
let mut graph = graph.write();
let all_partof_components = graph.get_all_components(Some(PartOf), None);
graph.ensure_loaded_parallel(&all_partof_components)?;
}
let mut result = Self {
root_corpus_name,
request_focus: selected_corpus_node,
node_name_filter: String::new(),
selected_corpus_node,
data: Data::default(),
pending_actions: Vec::new(),
jobs,
notifier,
graph,
};
result.update_data_after_selection();
if result.request_focus.is_none() {
result.request_focus = result.data.toplevel_nodes.first().copied();
}
Ok(result)
}
fn show_structure(&mut self, ui: &mut Ui) {
let root_nodes = self.data.toplevel_nodes.clone();
ui.group(|ui| {
ui.horizontal(|ui| {
let id = ui.label("Filter:").id;
ui.add(TextEdit::singleline(&mut self.node_name_filter))
.labelled_by(id);
});
ScrollArea::vertical().show(ui, |ui| {
if root_nodes.len() > 1 {
CollapsingHeader::new("<root>")
.default_open(true)
.show(ui, |ui| {
for root_node in root_nodes.iter() {
let mut root_item = CorpusTreeItem {
level: 0,
node: *root_node,
};
root_item.ui(ui, self);
}
});
} else if let Some(root_node) = root_nodes.first() {
let mut root_item = CorpusTreeItem {
level: 0,
node: *root_node,
};
root_item.ui(ui, self);
}
});
});
}
fn add_corpus_tree_actions(
&mut self,
label: &Response,
can_add: bool,
is_selected: bool,
node_name: &str,
ui: &mut Ui,
) {
if can_add {
let add_button = Button::new(icon::PLUS_CIRCLE).ui(ui);
if add_button.clicked() {
self.pending_actions.push(GraphAction::AddSubCorpus {
parent_node: node_name.to_string(),
});
}
}
if node_name != self.root_corpus_name {
let delete_button = Button::new(icon::TRASH).ui(ui);
if delete_button.clicked() {
self.pending_actions.push(GraphAction::DeleteCorpusNode {
node_name: node_name.to_string(),
});
}
}
if is_selected {
let node_name = node_name.to_string();
let open = Link::new(OPEN_LABEL.as_str()).ui(ui);
if open.clicked()
|| label
.ctx
.input_mut(|i| i.consume_key(Modifiers::COMMAND, Key::Enter))
{
self.jobs.add_foreground_job(
OPEN_LABEL.as_str(),
|_| Ok(()),
move |_state, app| {
app.change_view(MainView::EditDocument { node_name });
},
);
}
}
}
fn show_meta_editor(&mut self, ui: &mut Ui) {
if self.selected_corpus_node.is_some() {
let text_style_body = egui::TextStyle::Body.resolve(ui.style());
let available_width = ui.available_width() - 40.0;
let namespace_name_width = available_width / 3.0;
let value_width = (available_width / 3.0) * 2.0;
egui_extras::TableBuilder::new(ui)
.columns(Column::exact(namespace_name_width / 2.0), 2)
.column(Column::exact(value_width))
.column(Column::auto())
.header(text_style_body.size + 2.0, |mut header| {
header.col(|ui| {
ui.label(RichText::new("Namespace").underline());
});
header.col(|ui| {
ui.label(RichText::new("Name").underline());
});
header.col(|ui| {
ui.label(RichText::new("Value").underline());
});
header.col(|_ui| {});
})
.body(|body| {
body.rows(
text_style_body.size + 10.0,
self.data.node_annos.len() + 1,
|mut row| {
if row.index() < self.data.node_annos.len() {
self.show_existing_metadata_entries(&mut row);
} else {
self.show_new_metadata_row(&mut row);
}
},
);
});
} else {
ui.label("Select a corpus/document node to edit it.");
}
}
fn show_existing_metadata_entries(&mut self, row: &mut TableRow<'_, '_>) {
let entry_idx = row.index();
row.col(|ui| {
let entry = &mut self.data.node_annos[entry_idx];
TextEdit::singleline(&mut entry.ns.as_str()).ui(ui);
});
row.col(|ui| {
let entry = &mut self.data.node_annos[entry_idx];
TextEdit::singleline(&mut entry.name.as_str()).ui(ui);
});
row.col(|ui| {
let entry = &mut self.data.node_annos[entry_idx];
let text_edit = TextEdit::singleline(&mut entry.value);
let text_edit = text_edit.ui(ui);
if text_edit.lost_focus() && entry.value != entry.old_value {
self.pending_actions.push(GraphAction::AnnoValueChanged {
node_name: self.data.parent_node_name.clone(),
anno: Annotation {
key: AnnoKey {
ns: entry.ns.as_str().into(),
name: entry.name.as_str().into(),
},
val: entry.value.as_str().into(),
},
});
}
});
row.col(|ui| {
let delete_button = Button::new(RichText::new(icon::TRASH))
.ui(ui)
.on_hover_text("Delete metadata entry");
let entry = &mut self.data.node_annos[entry_idx];
if delete_button.clicked() {
self.pending_actions.push(GraphAction::AnnoRemoved {
node_name: self.data.parent_node_name.clone(),
anno_key: AnnoKey {
name: entry.name.as_str().into(),
ns: entry.ns.as_str().into(),
},
});
};
});
}
fn show_new_metadata_row(&mut self, row: &mut TableRow<'_, '_>) {
row.col(|ui| {
TextEdit::singleline(&mut self.data.new_entry.ns)
.id(Id::from("new-metadata-entry-ns"))
.ui(ui);
});
row.col(|ui| {
TextEdit::singleline(&mut self.data.new_entry.name)
.id(Id::from("new-metadata-entry-name"))
.ui(ui);
});
row.col(|ui| {
let text_value = TextEdit::singleline(&mut self.data.new_entry.value)
.id(Id::from("new-metadata-entry-value"))
.ui(ui);
if text_value.lost_focus() && text_value.ctx.input(|re| re.key_pressed(Key::Enter)) {
self.add_new_metadata_entry();
}
});
row.col(|ui| {
let add_button = Button::new(icon::PLUS_CIRCLE)
.ui(ui)
.on_hover_text("Add new metadata entry");
if add_button.clicked() {
self.add_new_metadata_entry();
}
});
}
fn add_new_metadata_entry(&mut self) {
if self.data.new_entry.name.is_empty() {
self.notifier
.add_toast(Toast::error("Cannot add entry with empty name"));
} else if self
.data
.node_annos
.iter()
.any(|a| a.ns == self.data.new_entry.ns && a.name == self.data.new_entry.name)
{
self.notifier.add_toast(Toast::error(format!(
"Entry with namespace \"{}\" and name \"{}\" already exists.",
self.data.new_entry.ns, self.data.new_entry.name
)));
} else {
let new_entry = std::mem::take(&mut self.data.new_entry);
self.pending_actions.push(GraphAction::AnnoValueChanged {
node_name: self.data.parent_node_name.clone(),
anno: Annotation {
key: AnnoKey {
name: new_entry.name.as_str().into(),
ns: new_entry.ns.as_str().into(),
},
val: new_entry.value.as_str().into(),
},
});
}
}
fn failable_update_corpus_graph_info(&mut self) -> anyhow::Result<()> {
let graph = self.graph.read();
for part_of_component in graph.get_all_components(Some(PartOf), None) {
let partof = graph
.get_graphstorage(&part_of_component)
.context("Missing PartOf component")?;
let corpus_nodes = graph.get_node_annos().exact_anno_search(
Some(ANNIS_NS),
NODE_TYPE,
ValueSearch::Some("corpus"),
);
for corpus_node in corpus_nodes {
let corpus_node = corpus_node?.node;
self.data.corpus_graph_nodes.insert(corpus_node);
if !partof.has_outgoing_edges(corpus_node)? {
self.data.toplevel_nodes.insert(corpus_node);
}
}
}
Ok(())
}
fn update_data_after_selection(&mut self) {
self.data.corpus_graph_nodes.clear();
self.data.toplevel_nodes.clear();
let result = self.failable_update_corpus_graph_info();
self.notifier.ok_or_report(result);
if let Some(parent) = self.selected_corpus_node {
self.data.node_annos.clear();
let graph = self.graph.read();
let anno_keys = graph
.get_node_annos()
.get_all_keys_for_item(&parent, None, None);
let anno_keys = self
.notifier
.unwrap_or_default(anno_keys.context("Could not get annotation keys"));
for k in anno_keys {
let anno_value = graph
.get_node_annos()
.get_value_for_item(&parent, &k)
.map(|v| v.unwrap_or_default().to_string());
let anno_value = self
.notifier
.unwrap_or_default(anno_value.context("Could not get annotation value"));
self.data.node_annos.push(MetaEntry {
ns: k.ns.clone(),
name: k.name.clone(),
value: anno_value.clone(),
old_value: anno_value,
});
}
self.data.node_annos.sort();
let parent_node_name = graph
.get_node_annos()
.get_value_for_item(&parent, &NODE_NAME_KEY);
let parent_node_name = self
.notifier
.unwrap_or_default(parent_node_name.context("Could not get parent node name"))
.unwrap_or_default();
self.data.parent_node_name = parent_node_name.to_string();
}
}
fn select_corpus_node(&mut self, selection: Option<NodeID>) {
self.selected_corpus_node = selection;
self.update_data_after_selection();
}
fn get_corpus_tree_child_nodes(&self, parent: NodeID) -> anyhow::Result<BTreeSet<NodeID>> {
let mut result = BTreeSet::new();
let part_of_component = AnnotationComponent::new(PartOf, ANNIS_NS.into(), "".into());
let graph = self.graph.read();
let partof = graph
.get_graphstorage(&part_of_component)
.context("Missing PartOf component")?;
for child_node in partof.get_ingoing_edges(parent) {
let child_node = child_node?;
if self.data.corpus_graph_nodes.contains(&child_node) {
result.insert(child_node);
}
}
Ok(result)
}
}
impl Editor for CorpusTree {
fn show(&mut self, ui: &mut Ui) {
ui.heading(&self.root_corpus_name);
ui.columns_const(|[c1, c2]| {
c1.push_id("corpus_structure", |ui| {
self.show_structure(ui);
});
c2.push_id("meta_editor", |ui| self.show_meta_editor(ui));
});
}
fn has_pending_updates(&self) -> bool {
!self.pending_actions.is_empty()
}
fn take_pending_updates(&mut self) -> Vec<GraphAction> {
std::mem::take(&mut self.pending_actions)
}
fn editor_state_updates(&self, action: &GraphAction) -> anyhow::Result<EditorStateUpdates> {
let graph = self.graph.read();
let mut result = EditorStateUpdates::default();
match action {
GraphAction::AddSubCorpus { .. } => {
let next_node_id = graph
.get_node_annos()
.get_largest_item()
.unwrap_or_default()
.unwrap_or_default()
+ 1;
result.set_before(move |ct: &mut CorpusTree| {
ct.data.corpus_graph_nodes.insert(next_node_id);
});
}
GraphAction::DeleteCorpusNode { node_name } => {
let node_id = graph
.get_node_annos()
.get_node_id_from_name(node_name)?
.context("Missing node ID")?;
let mut nodes_to_remove_from_graph = BTreeSet::new();
nodes_to_remove_from_graph.insert(node_id);
let is_parent_node = &self.data.parent_node_name == node_name;
for c in graph.get_all_components(Some(PartOf), None) {
if let Some(gs) = graph.get_graphstorage_as_ref(&c) {
for other in
gs.find_connected_inverse(node_id, 1, std::ops::Bound::Unbounded)
{
let other = other?;
nodes_to_remove_from_graph.insert(other);
}
}
}
result.set_after(move |ct: &mut CorpusTree| {
for n in nodes_to_remove_from_graph {
ct.data.corpus_graph_nodes.remove(&n);
}
if is_parent_node {
ct.select_corpus_node(None);
}
});
}
GraphAction::AnnoValueChanged { anno, .. } => {
let ns = anno.key.ns.to_string();
let name = anno.key.name.to_string();
let new_value = anno.val.to_string();
result.set_before(move |ct: &mut CorpusTree| {
if ns == ANNIS_NS && name == NODE_NAME {
ct.data.parent_node_name = new_value.clone();
}
if let Some(entry) = ct
.data
.node_annos
.iter_mut()
.find(|e| e.ns == ns && e.name == name)
{
entry.value = new_value.clone();
entry.old_value = new_value.clone();
} else {
let new_entry = MetaEntry {
ns: ns.clone(),
name: name.clone(),
value: new_value.clone(),
old_value: new_value.clone(),
};
ct.data.node_annos.push(new_entry);
ct.data.node_annos.sort();
}
});
}
GraphAction::AnnoRemoved { anno_key, .. } => {
let ns = anno_key.ns.to_string();
let name = anno_key.name.to_string();
result.set_before(move |ct: &mut CorpusTree| {
ct.data
.node_annos
.retain(|entry| entry.ns != ns || entry.name != name);
});
}
_ => {}
}
Ok(result)
}
fn get_edited_node(&self) -> &str {
&self.root_corpus_name
}
fn get_selected_nodes(&self) -> BTreeSet<String> {
let mut result = BTreeSet::new();
if let Some(selected) = self.selected_corpus_node {
let graph = self.graph.read();
let node_name = graph
.get_node_annos()
.get_value_for_item(&selected, &NODE_NAME_KEY)
.context("Missing node name");
if let Some(Some(node_name)) = self.notifier.ok_or_report(node_name) {
result.insert(node_name.to_string());
}
}
result
}
fn any_mut(&mut self) -> &mut dyn std::any::Any {
self
}
}
impl CorpusTreeItem {
pub fn ui(&mut self, ui: &mut egui::Ui, ct: &mut CorpusTree) {
let child_nodes = ct.get_corpus_tree_child_nodes(self.node);
let child_nodes = ct
.notifier
.unwrap_or_default(child_nodes.context("Could not get child nodes"));
let (parent_node_name, has_doc_anno) = {
let graph = ct.graph.read();
let parent_node_name = match graph
.get_node_annos()
.get_value_for_item(&self.node, &NODE_NAME_KEY)
{
Ok(o) => o.map(|o| o.to_string()),
Err(e) => {
ct.notifier.report_error(e.into());
None
}
};
let doc_key = AnnoKey {
ns: ANNIS_NS.into(),
name: "doc".into(),
};
let has_doc_anno = graph
.get_node_annos()
.has_value_for_item(&self.node, &doc_key);
let has_doc_anno = ct
.notifier
.unwrap_or_default(has_doc_anno.context("Could not check annis:doc annotation."));
(parent_node_name, has_doc_anno)
};
if let Some(parent_node_name) = parent_node_name {
let id = ui.make_persistent_id(format!("corpus-header-{}", &parent_node_name));
if has_doc_anno
&& !ct.node_name_filter.is_empty()
&& !parent_node_name
.to_lowercase()
.contains(&ct.node_name_filter.to_lowercase())
{
return;
}
if child_nodes.is_empty() {
ui.horizontal_top(|ui| {
self.add_corpus_tree_node(ct, !has_doc_anno, &parent_node_name, ui);
});
} else {
let is_selected = ct.selected_corpus_node.is_some_and(|n| n == self.node);
let mut header = egui::collapsing_header::CollapsingState::load_with_default_open(
ui.ctx(),
id,
self.level == 0 || is_selected,
);
if !header.is_open() && is_selected {
header.set_open(true);
}
header
.show_header(ui, |ui| {
self.add_corpus_tree_node(ct, true, &parent_node_name, ui);
})
.body(|ui| {
for child_corpus in &child_nodes {
let mut child_item = CorpusTreeItem {
level: self.level + 1,
node: *child_corpus,
};
child_item.ui(ui, ct);
}
});
}
}
}
fn add_corpus_tree_node(
&mut self,
ct: &mut CorpusTree,
can_add: bool,
node_name: &str,
ui: &mut Ui,
) {
let is_selected = ct.selected_corpus_node.is_some_and(|n| n == self.node);
let label = ui.selectable_label(is_selected, node_name);
if !is_selected && label.gained_focus() {
ct.select_corpus_node(Some(self.node));
} else if label.clicked() {
label.request_focus();
if is_selected {
ct.select_corpus_node(None);
} else {
ct.select_corpus_node(Some(self.node));
}
} else if Some(self.node) == ct.request_focus {
label.request_focus();
ct.request_focus = None;
}
ct.add_corpus_tree_actions(&label, can_add, is_selected, node_name, ui);
}
}