From 071b61d82de59b8635ae879b6d61bc7293d85bec Mon Sep 17 00:00:00 2001 From: Ralf Zerres Date: Fri, 19 Jun 2020 17:30:44 +0200 Subject: [PATCH] frontend: policylists: component to manage the policy code lists * policylist_view: GUI the user will interact with (orbtk Widget) will act on policy lists, that combines the policy data elements. * policydata_state: rust code with helper methods Signed-off-by: Ralf Zerres --- frontend/src/policylist_state.rs | 248 +++++++++++++++++++++++++++ frontend/src/policylist_view.rs | 279 +++++++++++++++++++++++++++++++ 2 files changed, 527 insertions(+) create mode 100644 frontend/src/policylist_state.rs create mode 100644 frontend/src/policylist_view.rs diff --git a/frontend/src/policylist_state.rs b/frontend/src/policylist_state.rs new file mode 100644 index 0000000..9d344ef --- /dev/null +++ b/frontend/src/policylist_state.rs @@ -0,0 +1,248 @@ +use orbtk::prelude::*; + +use crate::{ + base_state::BaseState, + data::{PolicyData, PolicyList}, + keys::*, +}; + +/// Actions that can execute on the task view. +#[derive(Debug, Clone, Copy)] +pub enum Action { + InputTextChanged(Entity), + NewEntry(Entity), + RemoveFocus(Entity), + RemoveEntry(usize), + OpenPolicyList(Entity), + SetEntry(Entity), + TextChanged(Entity, usize), +} + +/// Handles the requests of the `OverviewView`. +#[derive(Default, AsAny)] +pub struct PolicyListState { + action:Option, + add_button: Entity, + items_widget: Entity, + policylist_view: Entity, + last_focused: Option, + text_box: Entity, +} + +impl BaseState for PolicyListState {} + +impl PolicyListState { + /// Sets a new action. + pub fn action(&mut self, action: Action) { + self.action = action.into(); + } + + /// If policy data element 'name' is empty, disable the add button + /// otherwise otherwise enabled it. + fn adjust_add_button_enabled(&self, text_box: Entity, ctx: &mut Context) { + /* Old syntax + if ctx.get_widget(text_box).get::("name").is_empty() { + ctx.get_widget(self.add_button).set("enabled", false); + } else { + ctx.get_widget(self.add_button).set("enabled", true); + } + ctx.get_widget(self.add_button).update_theme_by_state(true); + */ + + if TextBox::get(ctx.get_widget(text_box)).text().is_empty() { + Button::get(ctx.child("add_button")).set_enabled(false); + } else { + Button::get(ctx.child("add_button")).set_enabled(true); + } + // new syntax missing + ctx.get_widget(self.add_button).update_theme_by_state(true); + } + + /// update number of available policy data entries. + fn adjust_count(&self, ctx: &mut Context) { + if let Some(index) = ctx.widget().clone::>("list_index") { + if let Some(policy_data) = ctx + .widget() + .clone::("policy_list") + .get(index) + { + ctx.widget().set("policy_list_count", policy_data.len()); + } + } + } + + // Creates a new policy list. + fn new_entry(&self, text: String, registry: &mut Registry, ctx: &mut Context) { + ctx.widget() + .get_mut::(PROP_POLICY_LIST) + .push(PolicyList::new(text)); + self.adjust_count(ctx); + self.save(registry, ctx); + } + + // opens a policy list. + fn open_policy_list(&self, index: usize, ctx: &mut Context) { + ctx.get_widget(self.text_box) + .set("name", String::from("")); + ctx.get_widget(self.policy_list) + .set("list_index", Some(index)); + self.navigate(self.policy_list, ctx); + } + + // removes a policy list. + fn remove_entry(&self, index: usize, registry: &mut Registry, ctx: &mut Context) { + ctx.widget() + .get_mut::(PROP_POLICY_LIST) + .remove(index); + self.adjust_count(ctx); + self.save(registry, ctx); + } + + /// Change status of given text box to edit mode. + fn set_entry(&self, text_box: Entity, ctx: &mut Context) { + if *ctx.get_widget(text_box).get::("focused") { + ctx.get_widget(text_box).set("enabled", false); + ctx.push_event_by_window(FocusEvent::RemoveFocus(text_box)); + return; + } + + if let Some(old_focused_element) = ctx.window().get::("global").focused_widget { + ctx.push_event_by_window(FocusEvent::RemoveFocus(old_focused_element)); + } + + ctx.get_widget(text_box).set("enabled", true); + + // select all + ctx.get_widget(text_box) + .get_mut::("text_selection") + .start_index = 0; + ctx.get_widget(text_box) + .get_mut::("text_selection") + .length = ctx.get_widget(text_box).get::("name").len(); + ctx.push_event_by_window(FocusEvent::RequestFocus(text_box)); + } + + /// Toggle the invalid element of the given policy data entry + fn toggle_invalid( + &self, + entry: Entity, + index: usize, + registry: &mut Registry, + ctx: &mut Context, + ) { + let invalid: bool = *ctx.get_widget(entry).get("invalid"); + + if let Some(idx) = ctx.widget().clone::>("list_index") { + if let Some(policy_list) = ctx + .widget() + .get_mut::("policy_list") + .get_mut(idx) + { + if let Some(policy_list) = policy_list.get_mut(index) { + policy_list.selected = selected; + } + } + } + self.save(registry, ctx); + } + + fn update_entry( + &self, + text_box: Entity, + index: usize, + registry: &mut Registry, + ctx: &mut Context, + ) { + let text: String16 = ctx.get_widget(text_box).clone("text"); + + if let Some(idx) = ctx.widget().clone::>("list_index") { + if let Some(policy_list) = ctx + .widget() + .get_mut::("policy_list") + .get_mut(idx) + { + if let Some(task) = policy_list.get_mut(index) { + task.text = text.to_string(); + } + } + } + self.save(registry, ctx); + } +} + +impl State for PolicyListState { + fn init(&mut self, registry: &mut Registry, ctx: &mut Context) { + self.text_box = ctx + .entity_of_child(ID_POLICY_LIST_TEXT_BOX) + .expect("PolicyListState.init: Child 'Text box' not found."); + self.add_button = ctx + .entity_of_child(ID_POLICY_LIST_ADD_BUTTON) + .expect("PolicyListState.init: Child 'Add button' not found."); + self.items_widget = ctx + .entity_of_child(ID_POLICY_LIST_ITEMS_WIDGET) + .expect("PolicyListState.init: Child 'Items widget' not found."); + self.policy_list = (*ctx.widget().get::("policy_list_view")).into(); + + if let Ok(tasks) = registry + .get::("settings") + .load::(PROP_POLICY_LIST) + { + ctx.widget().set(PROP_POLICY_LIST, tasks); + } + + self.adjust_count(ctx); + } + + fn update(&mut self, registry: &mut Registry, ctx: &mut Context) { + // clear focus on focus moved + if self.last_focused != ctx.window().get::("global").focused_widget { + if let Some(last_focused) = self.last_focused { + ctx.get_widget(last_focused).set("focused", false); + ctx.get_widget(last_focused) + .set("visibility", Visibility::Collapsed); + } + } + + if let Some(action) = self.action { + match action { + Action::InputTextChanged(text_box) => { + self.adjust_add_button_enabled(text_box, ctx); + } + Action::NewEntry(entity) => { + if let Some(text) = self.fetch_text(ctx, entity) { + self.new_entry(text, registry, ctx); + } + } + Action::RemoveEntry(index) => { + self.remove_entry(index, registry, ctx); + } + Action::TextChanged(entity, index) => { + let text: String16 = ctx.get_widget(entity).clone("text"); + + if let Some(header) = ctx + .widget() + .get_mut::("policy_list") + .get_mut(index) + { + header.name = text.to_string(); + } + + self.save(registry, ctx); + } + Action::SetEntry(text_box) => { + self.last_focused = Some(text_box); + self.set_entry(text_box, ctx); + } + Action::RemoveFocus(text_box) => { + self.last_focused = None; + ctx.push_event_by_window(FocusEvent::RemoveFocus(text_box)); + } + Action::OpenPolicyList(index) => { + self.open_policy_list(index, ctx); + } + } + } + + self.action = None; + } +} diff --git a/frontend/src/policylist_view.rs b/frontend/src/policylist_view.rs new file mode 100644 index 0000000..9f9f829 --- /dev/null +++ b/frontend/src/policylist_view.rs @@ -0,0 +1,279 @@ +use orbtk::prelude::*; + +use crate::{ + data::PolicyList, + keys::*, + policylist_state::{Action, PolicyListState}, +}; + +type ListIndex = Option; + +widget!( + /// Starter page that offers the dialog to enter an identifier of a policy. + /// This identifier is checked agains a map of valid policy codes. + PolicyListView { + policy_list: PolicyList, + policy_list_count: u32 + } +); + +impl Template for PolicyListView { + fn template(self, id: Entity, ctx: &mut BuildContext) -> Self { + // all items of our policy lists + let items_widget = ItemsWidget::new() + .id(ID_POLICY_LIST_ITEMS_WIDGET) + .v_align("start") + .items_builder(move |ctx, index| { + let mut name = "".to_string(); + let mut selected = false; + + if let Some(policy_list) = ctx + .get_widget(id) + .get::(PROP_POLICY_LIST) + .get(index) + { + name = policy_list.name.clone(); + } + + // plus button: open new policy + let helper_button = Button::new() + .min_height(48.0) + .class(CLASS_ITEM_BUTTON) + .attach(Grid::column(0)) + .attach(Grid::row(0)) + .attach(Grid::column_span(1)) + .on_click(move |ctx, _| { + ctx.get_mut::(id) + .action(Action::OpenPolicyList(index)); + true + }) + .build(ctx); + + let text_block = TextBlock::new() + .foreground(helper_button) + .margin((14.0, 0.0, 0.0, 0.0)) + .v_align("center") + .attach(Grid::column(0)) + .text(name) + .element("text-box") + .build(ctx); + + let text_box = TextBox::new() + .margin((8.0, 0.0, 0.0, 0.0)) + .visibility("collapsed") + .v_align("center") + .water_mark("Insert policy name...") + .attach(Grid::column(0)) + .text(text_block) + .on_changed(move |ctx, entity| { + ctx.get_mut::(id) + .action(Action::TextChanged(entity, index)); + }) + .on_activate(move |ctx, entity| { + ctx.get_mut::(id) + .action(Action::RemoveFocus(entity)); + }) + .build(ctx); + + Grid::new() + .height(48.0) + .columns( + Columns::new() + .add("*") + .add(8.0) + .add(32.0) + .add(4.0) + .add(32.0) + .add(8.0) + .build(), + ) + .child(helper_button) + .child(text_box) + .child(text_block) + .child( + ToggleButton::new() + .selected(("focused", text_box)) + .class(CLASS_ICON_ONLY) + .attach(Grid::column(2)) + .min_size(32.0, 32.0) + .v_align("center") + .build(ctx), + ) + .child( + Button::new() + // .selected(("focused", text_box)) + .class(CLASS_ICON_ONLY) + .attach(Grid::column(2)) + .min_size(32.0, 32.0) + .v_align("center") + // todo use remove from icons + // .icon(material_font_icons::DELETE_FONT_ICON) + .icon("") + .on_mouse_down(|_, _| true) + .on_click(move |ctx, _| { + ctx.get_mut::(id) + .action(Action::SetEntry(text_box)); + true + }) + .build(ctx), + ) + .child( + Button::new() + .class("icon_only") + .attach(Grid::column(4)) + .min_size(32.0, 32.0) + .v_align("center") + // todo use Tray icon for action remove + // .icon(material_font_icons::DELETE_FONT_ICON) + .icon("") + .on_mouse_down(|_, _| true) + .on_click(move |ctx, _| { + ctx.get_mut::(id) + .action(Action::RemoveEntry(index)); + true + }) + .build(ctx), + ) + .build(ctx) + }) + .policy_list_count(PROP_POLICY_LIST_COUNT, id) + .build(ctx); + + // create new policy list element + let policy_list_text_box = TextBox::new() + .id(ID_POLICY_LIST_TEXT_BOX) + .attach(Grid::row(4)) + .v_align("center") + .margin((4.0, 0.0, 0.0, 2.0)) + .lost_focus_on_activation(false) + .on_activate(move |ctx, entity| { + ctx.get_mut::(id) + .action(Action::NewEntry(entity)); + }) + .on_changed(move |ctx, entity| { + ctx.get_mut::(id) + .action(Action::InputTextChanged(entity)); + }) + .build(ctx); + + let scroll_viewer = ScrollViewer::new() + .scroll_viewer_mode(("disabled", "auto")) + .child(items_widget) + .build(ctx); + + self.name("Policy Lists") + .policy_list(PolicyList::default()) + .policy_list_count(0) + .child( + Grid::new() + .rows( + Rows::new() + .add(52.0) + .add(1.0) + .add("*") + .add(1.0) + .add(40.0) + .build(), + ) + .columns( + Columns::new() + .add("*") + .add(4.0) + .add(36.0) + .build(), + ) + // Content + .child( + Container::new() + .attach(Grid::row(2)) + .attach(Grid::column(0)) + .attach(Grid::column_span(3)) + .child(scroll_viewer) + .child( + ScrollIndicator::new() + .padding((0.0, 4.0, 0.0, 0.0)) + .content_id(items_widget.0) + .scroll_offset(scroll_viewer) + .build(ctx), + ) + .build(ctx), + ) + // Top Bar + .child( + Container::new() + .class(CLASS_TOP_BAR) + .attach(Grid::row(0)) + .attach(Grid::column(0)) + .attach(Grid::column_span(3)) + .child( + Grid::new() + .child( + TextBlock::new() + .class(CLASS_HEADER) + .v_align("center") + .h_align("center") + .text("Lists with policy data collections") + .build(ctx), + ) + .build(ctx), + ) + .build(ctx), + ) + .child( + Container::new() + .class("separator") + .attach(Grid::row(1)) + .attach(Grid::column_span(3)) + .build(ctx), + ) + .child( + Container::new() + .class("separator") + .attach(Grid::row(3)) + .attach(Grid::column_span(3)) + .build(ctx), + ) + // Bottom bar + .child( + Container::new() + .class(CLASS_BOTTOM_BAR) + .attach(Grid::row(4)) + .attach(Grid::column(0)) + .attach(Grid::column_span(3)) + .build(ctx), + ) + .child( + // workaround, todo fix scroll viewer mouse behavior in OrbTk + Button::new() + .attach(Grid::row(4)) + .attach(Grid::column(0)) + .attach(Grid::column_span(3)) + .on_mouse_down(|_, _| true) + .on_mouse_up(|_, _| true) + .on_click(|_, _| true) + .class(CLASS_TRANSPARENT) + .build(ctx), + ) + .child(policy_list_text_box) + .child( + Button::new() + .id(ID_POLICY_LIST_ADD_BUTTON) + .class(CLASS_ICON_ONLY) + .attach(Grid::row(4)) + .attach(Grid::column(2)) + .margin((0.0, 0.0, 4.0, 0.0)) + .enabled(false) + .min_size(32.0, 32.0) + .v_align("center") + .icon(material_font_icons::ADD_FONT_ICON) + .on_click(move |ctx, _| { + ctx.get_mut::(id) + .action(Action::NewEntry(policy_list_text_box)); + true + }) + .build(ctx), + ) + .build(ctx), + ) + } +}