This commit is contained in:
2026-08-31 13:45:15 -04:00
parent ebad6e6097
commit abe3ac97b5
4 changed files with 208 additions and 173 deletions
+208 -65
View File
@@ -1,4 +1,4 @@
use std::collections::{HashMap, HashSet}; use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
use std::ops::Sub; use std::ops::Sub;
use crate::navigator::Navigator; use crate::navigator::Navigator;
use crate::rdf::term_helper::{TermHelper, TermHelperMut}; use crate::rdf::term_helper::{TermHelper, TermHelperMut};
@@ -12,7 +12,7 @@ use iced::keyboard::{Event, key};
use iced::widget::button::Style; use iced::widget::button::Style;
use iced::widget::{button, center, column, combo_box, container, grid, mouse_area, opaque, operation, pick_list, row, scrollable, space, stack, table, text, text_input, toggler}; use iced::widget::{button, center, column, combo_box, container, grid, mouse_area, opaque, operation, pick_list, row, scrollable, space, stack, table, text, text_input, toggler};
use iced::window::Settings; use iced::window::Settings;
use iced::{Background, Color, Element, Length, Subscription, Task, color, keyboard, window}; use iced::{color, keyboard, window, Background, Color, Element, Font, Length, Subscription, Task, font};
use iced::advanced::text::Wrapping; use iced::advanced::text::Wrapping;
use iced::widget::grid::Sizing; use iced::widget::grid::Sizing;
use ldp::middleware::BasicAuthMiddleware; use ldp::middleware::BasicAuthMiddleware;
@@ -21,7 +21,7 @@ use ldp::reqwest::{Client, Url};
use ldp::reqwest_middleware::{ClientBuilder, ClientWithMiddleware}; use ldp::reqwest_middleware::{ClientBuilder, ClientWithMiddleware};
use ldp::{RdfSource, RdfSourceUpdateResponse, ResourceRequestBuilder, SerializationOptions}; use ldp::{RdfSource, RdfSourceUpdateResponse, ResourceRequestBuilder, SerializationOptions};
use oxigraph::io::RdfFormat; use oxigraph::io::RdfFormat;
use oxigraph::model::{BaseDirection, Graph, NamedNode, NamedOrBlankNode, NamedOrBlankNodeRef, Quad, Term, TermRef, Triple, TripleRef}; use oxigraph::model::{BaseDirection, Graph, NamedNode, NamedNodeRef, NamedOrBlankNode, NamedOrBlankNodeRef, Quad, Term, TermRef, Triple, TripleRef};
use tracing::{debug, error, trace}; use tracing::{debug, error, trace};
use gl_graph::class::Class; use gl_graph::class::Class;
use gl_graph::ontology::{ConnectedOntology, OntologyBuilder, ReadOnlyEntity, ResourceDescription}; use gl_graph::ontology::{ConnectedOntology, OntologyBuilder, ReadOnlyEntity, ResourceDescription};
@@ -77,6 +77,7 @@ pub(crate) enum Message {
RunInference, RunInference,
SetInferredTriples(Graph), SetInferredTriples(Graph),
ShowInferencesWindow, ShowInferencesWindow,
ShowReadOnlyWindow,
Event(Event), Event(Event),
} }
@@ -110,6 +111,7 @@ pub(crate) struct Publisher {
resource_descriptions: HashMap<NamedNode, ResourceDescription>, resource_descriptions: HashMap<NamedNode, ResourceDescription>,
window_id: window::Id, window_id: window::Id,
inference_window_id: Option<window::Id>, inference_window_id: Option<window::Id>,
read_only_window_id: Option<window::Id>,
url_input: String, url_input: String,
url_input_valid: bool, url_input_valid: bool,
navigator: Navigator<Url>, navigator: Navigator<Url>,
@@ -167,6 +169,7 @@ impl Publisher {
resource_descriptions: HashMap::new(), resource_descriptions: HashMap::new(),
window_id: id, window_id: id,
inference_window_id: None, inference_window_id: None,
read_only_window_id: None,
url_input: starting_url.to_string(), url_input: starting_url.to_string(),
url_input_valid: true, url_input_valid: true,
navigator, navigator,
@@ -237,8 +240,10 @@ impl Publisher {
Message::WindowClosed(id) => { Message::WindowClosed(id) => {
if self.window_id == id { if self.window_id == id {
task = iced::exit(); task = iced::exit();
} else { } else if let Some(search_state) = &self.search_state && search_state.window_id == id {
self.search_state = None; self.search_state = None;
} else if let Some(infrence_window_id) = self.inference_window_id && infrence_window_id == id {
self.inference_window_id = None;
} }
} }
Message::URLInputChanged(value) => { Message::URLInputChanged(value) => {
@@ -633,6 +638,15 @@ impl Publisher {
task = window_task.map(|_| Message::None); task = window_task.map(|_| Message::None);
} }
} }
Message::ShowReadOnlyWindow => {
if let Some(id) = self.read_only_window_id {
task = window::gain_focus(id);
} else {
let (id, window_task) = window::open(Settings::default());
self.read_only_window_id = Some(id);
task = window_task.map(|_| Message::None);
}
}
Message::Event(Event::KeyPressed { Message::Event(Event::KeyPressed {
key: keyboard::Key::Named(key::Named::Tab), key: keyboard::Key::Named(key::Named::Tab),
modifiers, modifiers,
@@ -654,6 +668,8 @@ impl Publisher {
"Graph of Liberty Publisher - Inferences".to_string() "Graph of Liberty Publisher - Inferences".to_string()
} else if let Some(search_state) = &self.search_state && window == search_state.window_id { } else if let Some(search_state) = &self.search_state && window == search_state.window_id {
"Graph of Liberty Publisher - Search".to_string() "Graph of Liberty Publisher - Search".to_string()
} else if let Some(read_only_window_id) = &self.read_only_window_id {
"Graph of Liberty Publisher - Read Only Triples".to_string()
} else { } else {
"Graph of Liberty Publisher".to_string() "Graph of Liberty Publisher".to_string()
} }
@@ -666,6 +682,186 @@ impl Publisher {
]) ])
} }
fn view_inferred_class_table<'a>(&'a self, triples: impl IntoIterator<Item = TripleRef<'a>>) -> Element<'a, Message> {
let bold = |t| text(t).font(Font { weight: font::Weight::Bold, ..Font::default() });
let subject_column = table::column(bold("Subject"), |(subject, _): (NamedNodeRef, _)| {
text(self.curie_helper
.abbreviate(Some(self.document.origin()), subject.as_str())
.unwrap_or_else(|| subject.as_str().to_string()))
});
let class_column = table::column(bold("Classes"), |(_, classes): (_, BTreeSet<NamedNodeRef>)| {
let textual_classes = classes.iter()
.map(|class| {
let curie = self.curie_helper
.abbreviate(Some(self.document.origin()), class.as_str())
.unwrap_or_else(|| class.as_str().to_string());
let label = self.resource_descriptions
.get(&class.into_owned())
.and_then(|description| description.label.clone());
if let Some(label) = label {
format!("{curie} ({label})")
} else {
curie
}
}).collect::<Vec<_>>()
.join("\n");
text(textual_classes)
});
let mut inferred_classes: BTreeMap<NamedNodeRef, BTreeSet<NamedNodeRef>> = BTreeMap::new();
for triple in triples {
if let NamedOrBlankNodeRef::NamedNode(subject) = triple.subject &&
let TermRef::NamedNode(class) = triple.object {
inferred_classes.entry(subject)
.and_modify(|classes| { classes.insert(class); })
.or_insert(BTreeSet::from_iter([class]));
}
}
table([subject_column, class_column], inferred_classes).into()
}
fn view_triple_table<'a>(&'a self, triples: impl IntoIterator<Item = TripleRef<'a>>) -> Element<'a, Message> {
let bold = |t| text(t).font(Font { weight: font::Weight::Bold, ..Font::default() });
let subject_column = table::column(bold("Subject"), |triple: TripleRef| {
let curie = if let NamedOrBlankNodeRef::NamedNode(subject) = triple.subject {
Some(self.curie_helper
.abbreviate(Some(self.document.origin()), subject.as_str())
.unwrap_or_else(|| subject.as_str().to_string()))
} else {
None
};
curie.map(text)
});
let predicate_column = table::column(bold("Predicate"), |triple: TripleRef| {
let curie = self.curie_helper
.abbreviate(None, triple.predicate.as_str())
.unwrap_or_else(|| triple.predicate.as_str().to_string());
let label = self.resource_descriptions
.get(&triple.predicate.into_owned())
.and_then(|description| description.label.clone());
if let Some(label) = label {
text(format!("{curie} ({label})"))
} else {
text(curie)
}
});
let object_column = table::column(bold("Object"), |triple: TripleRef| {
match triple.object {
TermRef::NamedNode(node) => {
let curie = self.curie_helper
.abbreviate(Some(self.document.origin()), node.as_str())
.unwrap_or_else(|| node.as_str().to_string());
let label = self.resource_descriptions
.get(&node.into_owned())
.and_then(|description| description.label.clone());
if let Some(label) = label {
text(format!("{curie} ({label})"))
} else {
text(curie)
}
}
_ => text(triple.object.to_string()),
}
});
table([subject_column, predicate_column, object_column], triples).into()
}
fn view_search_window<'a>(&'a self, search_state: &'a SearchState) -> Element<'a, Message> {
let search_input = text_input("Query", &search_state.query)
.on_input(Message::QueryUpdated)
.id("query");
let type_selector = pick_list(
search_state.entity_class_selection.as_ref(),
Class::ALL,
ToString::to_string,
).on_select(Message::UpdateQueryClass);
let mut columns = vec![table::column(text("CURIE"), |document: &HashMap<gl_search::Field, gl_search::OwnedValue>| {
let iri = document.get(&Schema::iri_field())
.and_then(|value| value.as_str())
.unwrap_or_default();
let abbreviated_iri = document.get(&Schema::curie_field())
.and_then(|value| value.as_str())
.unwrap_or_default();
button(text(abbreviated_iri).wrapping(Wrapping::Word))
.on_press(Message::SearchResultClicked(NamedNode::new_unchecked(iri)))
.style(button::text)
})];
let fields = search_state.entity_class_selection
.as_ref()
.map(|class| {
class.fields(language::ENGLISH_TAG.clone())
}).unwrap_or_default();
for (field, label) in fields {
columns.push(
table::column(text(label), move |document: &HashMap<gl_search::Field, gl_search::OwnedValue>| {
let iri = document.get(&Schema::iri_field())
.and_then(|value| value.as_str())
.unwrap_or_default();
let value = document
.get(&field)
.and_then(|value| value.as_str())
.unwrap_or_default();
button(text(value).wrapping(Wrapping::Word))
.on_press(Message::SearchResultClicked(NamedNode::new_unchecked(iri)))
.style(button::text)
})
.width(Length::Fixed(256.0)),
);
}
let results_table = if columns.is_empty() {
None
} else {
Some(scrollable(table(columns, &search_state.results)))
};
column![row![search_input, type_selector], results_table].into()
}
fn view_inference_window(&self) -> Element<'_, Message> {
let classes = self.inferred_triples.triples_for_predicate(vocab::rdf::TYPE);
column![
self.view_inferred_class_table(classes),
scrollable(self.view_triple_table(&self.inferred_triples)),
].into()
}
fn view_read_only_window(&self) -> Element<'_, Message> {
let read_only_triples = self.document
.dataset()
.iter_both()
.filter_map(|(_, quad, state)| if state.read_only { Some(quad) } else { None })
.map(|quad| TripleRef::from(quad.as_ref()));
column![
scrollable(self.view_triple_table(read_only_triples)),
].into()
}
fn view_row<'a>( fn view_row<'a>(
&'a self, &'a self,
key: QuadKey, key: QuadKey,
@@ -835,66 +1031,6 @@ impl Publisher {
.into() .into()
} }
fn view_search_window<'a>(&'a self, search_state: &'a SearchState) -> Element<'a, Message> {
let search_input = text_input("Query", &search_state.query)
.on_input(Message::QueryUpdated)
.id("query");
let type_selector = pick_list(
search_state.entity_class_selection.as_ref(),
Class::ALL,
ToString::to_string,
).on_select(Message::UpdateQueryClass);
let mut columns = vec![table::column(text("CURIE"), |document: &HashMap<gl_search::Field, gl_search::OwnedValue>| {
let iri = document.get(&Schema::iri_field())
.and_then(|value| value.as_str())
.unwrap_or_default();
let abbreviated_iri = document.get(&Schema::curie_field())
.and_then(|value| value.as_str())
.unwrap_or_default();
button(text(abbreviated_iri).wrapping(Wrapping::Word))
.on_press(Message::SearchResultClicked(NamedNode::new_unchecked(iri)))
.style(button::text)
})];
let fields = search_state.entity_class_selection
.as_ref()
.map(|class| {
class.fields(language::ENGLISH_TAG.clone())
}).unwrap_or_default();
for (field, label) in fields {
columns.push(
table::column(text(label), move |document: &HashMap<gl_search::Field, gl_search::OwnedValue>| {
let iri = document.get(&Schema::iri_field())
.and_then(|value| value.as_str())
.unwrap_or_default();
let value = document
.get(&field)
.and_then(|value| value.as_str())
.unwrap_or_default();
button(text(value).wrapping(Wrapping::Word))
.on_press(Message::SearchResultClicked(NamedNode::new_unchecked(iri)))
.style(button::text)
})
.width(Length::Fixed(256.0)),
);
}
let results_table = if columns.is_empty() {
None
} else {
Some(scrollable(table(columns, &search_state.results)))
};
column![row![search_input, type_selector], results_table].into()
}
pub(crate) fn view(&self, window: window::Id) -> Element<'_, Message> { pub(crate) fn view(&self, window: window::Id) -> Element<'_, Message> {
if let Some(search_state) = &self.search_state && if let Some(search_state) = &self.search_state &&
search_state.window_id == window { search_state.window_id == window {
@@ -902,10 +1038,15 @@ impl Publisher {
} }
if let Some(inference_window_id) = self.inference_window_id && window == inference_window_id { if let Some(inference_window_id) = self.inference_window_id && window == inference_window_id {
return crate::window::inferences::view_inference_window(&self.resource_descriptions, self.document.origin(), &self.curie_helper, &self.inferred_triples); return self.view_inference_window();
}
if let Some(read_only_window_id) = self.read_only_window_id && window == read_only_window_id {
return self.view_read_only_window();
} }
let show_inferences_button = button("\u{1f9e0}").on_press(Message::ShowInferencesWindow); let show_inferences_button = button("\u{1f9e0}").on_press(Message::ShowInferencesWindow);
let show_read_only_button = button("\u{1f6ab}").on_press(Message::ShowReadOnlyWindow);
let back_button = button("\u{1f870}").on_press(Message::NavigateBack); let back_button = button("\u{1f870}").on_press(Message::NavigateBack);
let forward_button = button("\u{1f872}").on_press(Message::NavigateForward); let forward_button = button("\u{1f872}").on_press(Message::NavigateForward);
let new_document_button = button("New Document").on_press(Message::ShowNewDocumentButtons); let new_document_button = button("New Document").on_press(Message::ShowNewDocumentButtons);
@@ -923,6 +1064,7 @@ impl Publisher {
rows = self.document rows = self.document
.dataset() .dataset()
.iter_both() .iter_both()
.filter(|(_, _, state)| !state.read_only)
.map(|(key, quad, state)| self.view_row(key, quad, state)) .map(|(key, quad, state)| self.view_row(key, quad, state))
.collect(); .collect();
@@ -955,6 +1097,7 @@ impl Publisher {
let content = navigation_area(column![ let content = navigation_area(column![
row![ row![
show_inferences_button, show_inferences_button,
show_read_only_button,
new_document_button, new_document_button,
add_row_button, add_row_button,
back_button, back_button,
-1
View File
@@ -6,7 +6,6 @@ mod rdf;
mod theme; mod theme;
mod widget; mod widget;
mod tasks; mod tasks;
pub mod window;
use std::collections::HashMap; use std::collections::HashMap;
use crate::app::Publisher; use crate::app::Publisher;
-106
View File
@@ -1,106 +0,0 @@
use std::collections::{BTreeMap, BTreeSet, HashMap};
use iced::{font, Element, Font};
use iced::widget::{column, scrollable, span, table, text};
use oxigraph::model::{Graph, NamedNode, NamedNodeRef, NamedOrBlankNodeRef, TermRef, TripleRef};
use url::Url;
use gl_graph::{vocab, CurieHelper};
use gl_graph::ontology::ResourceDescription;
use crate::app::Message;
pub(crate) fn view_inference_window<'a>(resource_descriptions: &HashMap<NamedNode, ResourceDescription>, origin: &Url, curie_helper: &CurieHelper, inferences: &'a Graph) -> Element<'a, Message> {
let bold = |t| text(t).font(Font { weight: font::Weight::Bold, ..Font::default() });
let inferred_class_subject_column = table::column(bold("Subject"), |(subject, _): (NamedNodeRef, _)| {
text(curie_helper
.abbreviate(Some(origin), subject.as_str())
.unwrap_or_else(|| subject.as_str().to_string()))
});
let inferred_class_class_column = table::column(bold("Classes"), |(_, classes): (_, BTreeSet<NamedNodeRef>)| {
let textual_classes = classes.iter()
.map(|class| {
let curie = curie_helper
.abbreviate(Some(origin), class.as_str())
.unwrap_or_else(|| class.as_str().to_string());
let label = resource_descriptions
.get(&class.into_owned())
.and_then(|description| description.label.clone());
if let Some(label) = label {
format!("{curie} ({label})")
} else {
curie
}
}).collect::<Vec<_>>()
.join("\n");
text(textual_classes)
});
let mut inferred_classes: BTreeMap<NamedNodeRef, BTreeSet<NamedNodeRef>> = BTreeMap::new();
for triple in inferences.triples_for_predicate(vocab::rdf::TYPE) {
if let NamedOrBlankNodeRef::NamedNode(subject) = triple.subject && let TermRef::NamedNode(class) = triple.object {
inferred_classes.entry(subject)
.and_modify(|classes| { classes.insert(class); })
.or_insert(BTreeSet::from_iter([class]));
}
}
let inferred_class_table = table([inferred_class_subject_column, inferred_class_class_column], inferred_classes);
let subject_column = table::column(bold("Subject"), |triple: TripleRef| {
let curie = if let NamedOrBlankNodeRef::NamedNode(subject) = triple.subject {
Some(curie_helper
.abbreviate(Some(origin), subject.as_str())
.unwrap_or_else(|| subject.as_str().to_string()))
} else {
None
};
curie.map(text)
});
let predicate_column = table::column(bold("Predicate"), |triple: TripleRef| {
let curie = curie_helper
.abbreviate(None, triple.predicate.as_str())
.unwrap_or_else(|| triple.predicate.as_str().to_string());
let label = resource_descriptions
.get(&triple.predicate.into_owned())
.and_then(|description| description.label.clone());
if let Some(label) = label {
text(format!("{curie} ({label})"))
} else {
text(curie)
}
});
let object_column = table::column(bold("Object"), |triple: TripleRef| {
match triple.object {
TermRef::NamedNode(node) => {
let curie = curie_helper
.abbreviate(Some(origin), node.as_str())
.unwrap_or_else(|| node.as_str().to_string());
let label = resource_descriptions
.get(&node.into_owned())
.and_then(|description| description.label.clone());
if let Some(label) = label {
text(format!("{curie} ({label})"))
} else {
text(curie)
}
}
_ => text(triple.object.to_string()),
}
});
let all_inferences_table = table([subject_column, predicate_column, object_column], inferences);
column![
inferred_class_table,
scrollable(all_inferences_table),
].into()
}
-1
View File
@@ -1 +0,0 @@
pub(crate) mod inferences;