use crate::app::ActionRequest;
use crate::common::{CrudOperationRequested, ToOpenTimelineType, delete_from_id_crud, save_crud};
use crate::components::{DatesGui, EntityOrTimeline, NameGui, TagsGui};
use crate::config::SharedConfig;
use crate::consts::DEFAULT_WINDOW_SIZES;
use crate::shortcuts::global_shortcuts;
use crate::windows::{Deleted, DeletedStatus};
use crate::{
impl_is_valid_method_for_iterable, impl_valid_asynchronous_macro_never_called,
impl_valid_synchronous_macro_never_called, spawn_transaction_no_commit_send_result,
};
use eframe::egui::{
self, CentralPanel, Context, Response, ScrollArea, Spinner, Ui, Vec2, ViewportId,
};
use log::info;
use open_timeline_core::{Entity, HasIdAndName, OpenTimelineId};
use open_timeline_crud::{CrudError, FetchById};
use open_timeline_gui_core::{
BreakOutWindow, CreateOrEdit, DisplayStatus, Draw, GuiStatus, Reload, Shortcut, Valid,
ValidityAsynchronous, window_has_focus,
};
use std::sync::Arc;
use std::time::Instant;
use tokio::sync::mpsc::error::TryRecvError;
use tokio::sync::mpsc::{Receiver, UnboundedSender};
#[derive(Debug)]
pub struct EntityEditGui {
database_entry: Option<Entity>,
entity_id: Option<OpenTimelineId>,
name: NameGui,
dates: DatesGui,
tags: TagsGui,
deleted_status: DeletedStatus,
requested_reload: bool,
create_or_edit: CreateOrEdit,
status: Status,
crud_op_requested: Option<CrudOperationRequested>,
rx_create_update: Option<Receiver<Result<Entity, CrudError>>>,
rx_delete: Option<Receiver<Result<(), CrudError>>>,
rx_reload: Option<Receiver<Result<Entity, CrudError>>>,
tx_crud_operation_executed: UnboundedSender<()>,
tx_action_request: UnboundedSender<ActionRequest>,
wants_to_be_closed: bool,
shared_config: SharedConfig,
can_be_saved: bool,
differs_from_database: Option<bool>,
}
#[derive(Debug)]
enum Status {
WaitingForReload,
WaitingForInitialLoad,
NewWindowForCreation,
NewWindowForEditing,
CreateError(CrudError),
UpdateError(CrudError),
DeleteError(CrudError),
Created,
Updated,
Deleted,
Invalid(String),
HasBeenDeletedElseWhere,
}
impl DisplayStatus for Status {
fn status_display(&self, ui: &mut Ui) -> Response {
let str = match &self {
Self::WaitingForReload => String::from("Waiting for entity to reload"),
Self::WaitingForInitialLoad => String::from("Waiting for entity to load"),
Self::NewWindowForCreation => String::from("Ready to create an entity"),
Self::NewWindowForEditing => String::from("Ready to edit an entity"),
Self::CreateError(error) => {
format!("Error when trying to create entity: {error}")
}
Self::UpdateError(error) => {
format!("Error when trying to update entity: {error}")
}
Self::DeleteError(error) => {
format!("Error when trying to delete entity: {error}")
}
Self::Created => String::from("Entity successfully created"),
Self::Updated => String::from("Entity successfully updated"),
Self::Deleted => String::from("Entity successfully deleted"),
Self::Invalid(error) => format!("Entity is invalid: {error}"),
Self::HasBeenDeletedElseWhere => String::from("Entity was deleted elsewhere"),
};
ui.add(egui::Label::new(str).truncate())
}
}
impl EntityEditGui {
pub fn create_or_edit(&self) -> CreateOrEdit {
self.create_or_edit.clone()
}
pub fn entity_id(&self) -> Option<OpenTimelineId> {
self.entity_id
}
pub fn new_window_for_creating_entity(
shared_config: SharedConfig,
tx_action_request: UnboundedSender<ActionRequest>,
tx_crud_operation_executed: UnboundedSender<()>,
) -> Self {
EntityEditGui {
database_entry: None,
entity_id: None,
name: NameGui::new(Arc::clone(&shared_config), EntityOrTimeline::Entity),
dates: DatesGui::new(),
tags: TagsGui::new(),
deleted_status: DeletedStatus::NotDeleted,
requested_reload: false,
create_or_edit: CreateOrEdit::Create,
status: Status::NewWindowForCreation,
crud_op_requested: None,
rx_create_update: None,
rx_delete: None,
rx_reload: None,
tx_crud_operation_executed,
tx_action_request,
wants_to_be_closed: false,
shared_config,
can_be_saved: false,
differs_from_database: Some(false),
}
}
pub fn new_window_for_editing_entity(
shared_config: SharedConfig,
tx_action_request: UnboundedSender<ActionRequest>,
tx_crud_operation_executed: UnboundedSender<()>,
entity_id: OpenTimelineId,
) -> Self {
let mut entity_edit_gui = EntityEditGui {
database_entry: None,
entity_id: Some(entity_id),
name: NameGui::new(Arc::clone(&shared_config), EntityOrTimeline::Entity),
dates: DatesGui::new(),
tags: TagsGui::new(),
deleted_status: DeletedStatus::NotDeleted,
requested_reload: false,
create_or_edit: CreateOrEdit::Edit,
status: Status::NewWindowForEditing,
crud_op_requested: None,
rx_create_update: None,
rx_delete: None,
rx_reload: None,
tx_crud_operation_executed,
tx_action_request,
wants_to_be_closed: false,
shared_config,
can_be_saved: false,
differs_from_database: Some(false),
};
entity_edit_gui.request_reload();
entity_edit_gui
}
fn set_from_entity(&mut self, entity: Entity) {
self.database_entry = Some(entity.clone());
self.entity_id = entity.id();
self.name = NameGui::from_name(
Arc::clone(&self.shared_config),
EntityOrTimeline::Entity,
entity.name().clone(),
);
self.dates = (entity.start(), entity.end()).into();
self.tags = entity.tags().to_owned().into();
self.deleted_status = DeletedStatus::NotDeleted;
self.create_or_edit = CreateOrEdit::Edit;
self.crud_op_requested = None;
self.rx_create_update = None;
self.rx_delete = None;
self.rx_reload = None;
}
fn reset(&mut self) {
match &self.database_entry {
Some(entity) => self.set_from_entity(entity.clone()),
None => panic!("ERROR: shouldn't ever get here"),
}
}
fn differs_from_database_entry(&mut self) -> Option<bool> {
if self.create_or_edit() == CreateOrEdit::Create {
return None;
}
let stored_value = self.differs_from_database;
let differs = if self.validity() == ValidityAsynchronous::Valid {
let current_entity = self.to_opentimeline_type();
match self.database_entry.as_ref() {
Some(entity_in_db) => Some(current_entity != *entity_in_db),
None => panic!("Shouldn't get here"),
}
} else {
None
};
if stored_value != differs {
debug!(
"Entity (current name input value = {}) differs from database: {stored_value:?} -> {differs:?}",
self.name.name
);
self.differs_from_database = differs;
}
self.differs_from_database
}
fn draw_status(&mut self, ui: &mut Ui) {
if self.rx_create_update.is_some() || self.rx_delete.is_some() {
ui.add(Spinner::new());
}
GuiStatus::display(ui, &self.status);
}
fn draw_toolbar(&mut self, ui: &mut Ui) {
ui.horizontal(|ui| match self.create_or_edit {
CreateOrEdit::Create => {
if self.can_be_saved() {
if open_timeline_gui_core::Button::create(ui).clicked() {
self.request_create_or_update();
}
} else {
ui.label("Input valid information for a new entity");
}
}
CreateOrEdit::Edit => {
if open_timeline_gui_core::Button::delete(ui).clicked() {
self.request_delete();
}
if self.differs_from_database_entry() != Some(false)
&& open_timeline_gui_core::Button::reset(ui).clicked()
{
self.reset();
}
if self.can_be_saved() {
if open_timeline_gui_core::Button::update(ui).clicked() {
debug!("Entity save button clicked");
self.request_create_or_update();
}
}
}
});
}
fn can_be_saved(&mut self) -> bool {
let stored_value = self.can_be_saved;
let can_be_saved = self.differs_from_database_entry() != Some(false)
&& self.validity() == ValidityAsynchronous::Valid;
if stored_value != can_be_saved {
debug!(
"Entity (current name input value = {}) can be saved: {stored_value} -> {can_be_saved}",
self.name.name
);
self.can_be_saved = can_be_saved;
}
self.can_be_saved
}
fn request_create_or_update(&mut self) {
if self.can_be_saved() {
let (tx, rx) = tokio::sync::mpsc::channel(1);
self.rx_create_update = Some(rx);
self.crud_op_requested = Some(CrudOperationRequested::CreateOrUpdate);
let entity = self.to_opentimeline_type();
let edit_or_create = self.create_or_edit.clone();
let shared_config = Arc::clone(&self.shared_config);
tokio::spawn(
async move { save_crud(shared_config, &edit_or_create, entity, tx).await },
);
}
}
fn request_delete(&mut self) {
let (tx, rx) = tokio::sync::mpsc::channel(1);
self.rx_delete = Some(rx);
self.crud_op_requested = Some(CrudOperationRequested::Delete);
let entity_id = self.entity_id.unwrap();
let shared_config = Arc::clone(&self.shared_config);
tokio::spawn(
async move { delete_from_id_crud::<Entity>(shared_config, entity_id, tx).await },
);
}
fn receive_any_crud_status_updates(&mut self) {
if let Some(rx) = self.rx_create_update.as_mut() {
match rx.try_recv() {
Ok(result) => {
debug!("recv crud update");
match result {
Ok(entity) => {
info!("Entity updated sucessfully");
self.set_from_entity(entity);
self.status = match self.create_or_edit {
CreateOrEdit::Create => Status::Created,
CreateOrEdit::Edit => Status::Updated,
};
let _ = self.tx_crud_operation_executed.send(());
}
Err(error) => {
self.rx_create_update = None;
self.crud_op_requested = None;
self.status = match self.create_or_edit {
CreateOrEdit::Create => Status::CreateError(error),
CreateOrEdit::Edit => Status::UpdateError(error),
};
}
}
}
Err(TryRecvError::Empty) => (),
Err(TryRecvError::Disconnected) => (),
}
}
if let Some(rx) = self.rx_delete.as_mut() {
match rx.try_recv() {
Ok(result) => match result {
Ok(()) => {
self.rx_delete = None;
self.crud_op_requested = None;
self.status = Status::Deleted;
self.set_deleted_status(DeletedStatus::Deleted(Instant::now()));
let _ = self.tx_crud_operation_executed.send(());
}
Err(error) => {
self.rx_delete = None;
self.crud_op_requested = None;
self.status = Status::DeleteError(error);
}
},
Err(TryRecvError::Empty) => (),
Err(TryRecvError::Disconnected) => (),
}
}
}
}
impl ToOpenTimelineType<Entity> for EntityEditGui {
fn to_opentimeline_type(&self) -> Entity {
let id = self.entity_id;
let name = self.name.to_opentimeline_type();
let (start, end) = self.dates.to_opentimeline_type();
let tags = self.tags.to_opentimeline_type();
Entity::from(id, name, start, end, tags).unwrap()
}
}
impl Reload for EntityEditGui {
fn request_reload(&mut self) {
if self.has_been_deleted() {
return;
}
match self.entity_id {
Some(entity_id) => {
self.requested_reload = true;
let (tx, rx) = tokio::sync::mpsc::channel(1);
self.rx_reload = Some(rx);
let shared_config = Arc::clone(&self.shared_config);
spawn_transaction_no_commit_send_result!(
shared_config,
bounded,
tx,
|transaction| async move { Entity::fetch_by_id(transaction, &entity_id).await }
);
}
None => self.set_deleted_status(DeletedStatus::Deleted(Instant::now())),
}
}
fn check_reload_response(&mut self) {
if let Some(rx) = self.rx_reload.as_mut() {
match rx.try_recv() {
Ok(result) => {
debug!("reload response receved");
self.rx_reload = None;
self.requested_reload = false;
match result {
Ok(entity) => self.set_from_entity(entity),
Err(CrudError::IdNotInDb) => {
self.set_deleted_status(DeletedStatus::Deleted(Instant::now()))
}
Err(_) => todo!(),
}
}
Err(TryRecvError::Empty) => (),
Err(TryRecvError::Disconnected) => (),
}
}
}
}
impl Deleted for EntityEditGui {
fn set_deleted_status(&mut self, deleted_status: DeletedStatus) {
self.deleted_status = deleted_status;
}
fn deleted_status(&self) -> DeletedStatus {
self.deleted_status
}
}
impl_valid_synchronous_macro_never_called!(EntityEditGui);
impl_valid_asynchronous_macro_never_called!(EntityEditGui);
impl Valid for EntityEditGui {
fn validity(&self) -> ValidityAsynchronous {
impl_is_valid_method_for_iterable!([
self.name.validity(),
self.dates.validity(),
self.tags.validity(),
])
}
fn update_validity(&mut self) {
panic!()
}
}
impl BreakOutWindow for EntityEditGui {
fn draw(&mut self, ctx: &Context) {
if window_has_focus(ctx) {
if self.can_be_saved() && Shortcut::save(ctx) {
self.request_create_or_update();
}
if Shortcut::close_window(ctx) {
self.wants_to_be_closed = true;
}
}
global_shortcuts(ctx, &mut self.tx_action_request);
self.check_reload_response();
self.receive_any_crud_status_updates();
match self.validity() {
ValidityAsynchronous::Invalid(error) => self.status = Status::Invalid(error),
ValidityAsynchronous::Valid => (),
ValidityAsynchronous::Waiting => (),
}
CentralPanel::default().show(ctx, |ui| {
if self.requested_reload {
ui.spinner();
return;
}
open_timeline_gui_core::Label::heading(ui, "Entity");
ui.separator();
if self.has_been_deleted() {
self.draw_deleted_message(ctx, ui);
if let DeletedStatus::Deleted(deleted_at) = self.deleted_status() {
let elapsed_secs = deleted_at.elapsed().as_secs() as i32;
let remaining_seconds = 5 - elapsed_secs;
if remaining_seconds < 1 {
self.wants_to_be_closed = true;
}
}
return;
}
self.draw_status(ui);
ui.separator();
self.draw_toolbar(ui);
ui.separator();
self.name.draw(ctx, ui);
ui.separator();
self.dates.draw(ctx, ui);
ui.separator();
ScrollArea::vertical().show(ui, |ui| {
self.tags.draw(ctx, ui);
});
});
}
fn default_size(&self) -> Vec2 {
Vec2::new(
DEFAULT_WINDOW_SIZES.entity_edit.width,
DEFAULT_WINDOW_SIZES.entity_edit.height,
)
}
fn viewport_id(&mut self) -> ViewportId {
ViewportId(eframe::egui::Id::from(match self.create_or_edit() {
CreateOrEdit::Create => format!("entity_create_{}", OpenTimelineId::new()),
CreateOrEdit::Edit => {
format!("entity_edit_{}", self.entity_id().unwrap())
}
}))
}
fn title(&mut self) -> String {
match self.create_or_edit() {
CreateOrEdit::Create => {
format!("Create Entity • {}", self.name.name)
}
CreateOrEdit::Edit => {
format!("Edit Entity • {}", self.name.name)
}
}
}
fn wants_to_be_closed(&mut self) -> bool {
self.wants_to_be_closed
}
}