enter signal metadata via a form with multiple textboxes at once. needs

work.
This commit is contained in:
Dustin Carlino 2020-03-04 15:33:28 -08:00
parent 1ad434f3d6
commit 0fd4de749d
8 changed files with 307 additions and 93 deletions

View File

@ -6,8 +6,7 @@ use geom::{Angle, Duration, Polygon, Pt2D};
// TODO Add text to the logo (showing zoom) // TODO Add text to the logo (showing zoom)
// TODO Some kind of plot?! // TODO Some kind of plot?!
// TODO Some popup dialogs? // TODO Some popup dialogs with form entry, even some scrolling
// TODO Something scrolling
// TODO Loading screen with timer? // TODO Loading screen with timer?
struct App { struct App {

View File

@ -41,6 +41,18 @@ impl UserInput {
false false
} }
pub fn any_key_pressed(&mut self) -> Option<Key> {
if self.event_consumed {
return None;
}
if let Event::KeyPress(key) = self.event {
self.consume_event();
return Some(key);
}
None
}
pub fn unimportant_key_pressed(&mut self, key: Key, action: &str) -> bool { pub fn unimportant_key_pressed(&mut self, key: Key, action: &str) -> bool {
self.reserve_key(key, action); self.reserve_key(key, action);

View File

@ -1,5 +1,6 @@
use crate::layout::Widget; use crate::layout::Widget;
use crate::widgets::{Checkbox, PopupMenu}; use crate::text;
use crate::widgets::{Checkbox, PopupMenu, TextBox};
use crate::{ use crate::{
Button, Color, Drawable, EventCtx, Filler, GeomBatch, GfxCtx, Histogram, HorizontalAlignment, Button, Color, Drawable, EventCtx, Filler, GeomBatch, GfxCtx, Histogram, HorizontalAlignment,
JustDraw, MultiKey, Plot, RewriteColor, ScreenDims, ScreenPt, ScreenRectangle, Slider, Text, JustDraw, MultiKey, Plot, RewriteColor, ScreenDims, ScreenPt, ScreenRectangle, Slider, Text,
@ -30,6 +31,7 @@ enum WidgetType {
Draw(JustDraw), Draw(JustDraw),
Btn(Button), Btn(Button),
Checkbox(Checkbox), Checkbox(Checkbox),
TextBox(TextBox),
Slider(String), Slider(String),
Menu(String), Menu(String),
Filler(String), Filler(String),
@ -289,6 +291,11 @@ impl ManagedWidget {
.named(label) .named(label)
} }
pub fn text_entry(ctx: &EventCtx, prefilled: String) -> ManagedWidget {
// TODO Hardcoded style, max chars
ManagedWidget::new(WidgetType::TextBox(TextBox::new(ctx, 50, prefilled))).bg(text::BG_COLOR)
}
pub(crate) fn duration_plot(plot: Plot<Duration>) -> ManagedWidget { pub(crate) fn duration_plot(plot: Plot<Duration>) -> ManagedWidget {
ManagedWidget::new(WidgetType::DurationPlot(plot)) ManagedWidget::new(WidgetType::DurationPlot(plot))
} }
@ -329,6 +336,9 @@ impl ManagedWidget {
WidgetType::Checkbox(ref mut checkbox) => { WidgetType::Checkbox(ref mut checkbox) => {
checkbox.event(ctx); checkbox.event(ctx);
} }
WidgetType::TextBox(ref mut textbox) => {
textbox.event(ctx);
}
WidgetType::Slider(ref name) => { WidgetType::Slider(ref name) => {
sliders.get_mut(name).unwrap().event(ctx); sliders.get_mut(name).unwrap().event(ctx);
} }
@ -364,6 +374,7 @@ impl ManagedWidget {
WidgetType::Draw(ref j) => j.draw(g), WidgetType::Draw(ref j) => j.draw(g),
WidgetType::Btn(ref btn) => btn.draw(g), WidgetType::Btn(ref btn) => btn.draw(g),
WidgetType::Checkbox(ref checkbox) => checkbox.draw(g), WidgetType::Checkbox(ref checkbox) => checkbox.draw(g),
WidgetType::TextBox(ref textbox) => textbox.draw(g),
WidgetType::Slider(ref name) => { WidgetType::Slider(ref name) => {
if name != "horiz scrollbar" && name != "vert scrollbar" { if name != "horiz scrollbar" && name != "vert scrollbar" {
sliders[name].draw(g); sliders[name].draw(g);
@ -397,6 +408,7 @@ impl ManagedWidget {
WidgetType::Draw(ref widget) => widget, WidgetType::Draw(ref widget) => widget,
WidgetType::Btn(ref widget) => widget, WidgetType::Btn(ref widget) => widget,
WidgetType::Checkbox(ref widget) => widget, WidgetType::Checkbox(ref widget) => widget,
WidgetType::TextBox(ref widget) => widget,
WidgetType::Slider(ref name) => &sliders[name], WidgetType::Slider(ref name) => &sliders[name],
WidgetType::Menu(ref name) => &menus[name], WidgetType::Menu(ref name) => &menus[name],
WidgetType::Filler(ref name) => &fillers[name], WidgetType::Filler(ref name) => &fillers[name],
@ -505,6 +517,9 @@ impl ManagedWidget {
WidgetType::Checkbox(ref mut widget) => { WidgetType::Checkbox(ref mut widget) => {
widget.set_pos(top_left); widget.set_pos(top_left);
} }
WidgetType::TextBox(ref mut widget) => {
widget.set_pos(top_left);
}
WidgetType::Slider(ref name) => { WidgetType::Slider(ref name) => {
sliders.get_mut(name).unwrap().set_pos(top_left); sliders.get_mut(name).unwrap().set_pos(top_left);
} }
@ -566,6 +581,7 @@ impl ManagedWidget {
| WidgetType::Menu(_) | WidgetType::Menu(_)
| WidgetType::Filler(_) | WidgetType::Filler(_)
| WidgetType::Checkbox(_) | WidgetType::Checkbox(_)
| WidgetType::TextBox(_)
| WidgetType::DurationPlot(_) | WidgetType::DurationPlot(_)
| WidgetType::UsizePlot(_) => {} | WidgetType::UsizePlot(_) => {}
WidgetType::Histogram(_) => {} WidgetType::Histogram(_) => {}
@ -597,7 +613,9 @@ impl ManagedWidget {
fn find(&self, name: &str) -> Option<&ManagedWidget> { fn find(&self, name: &str) -> Option<&ManagedWidget> {
let found = match self.widget { let found = match self.widget {
// TODO Consolidate and just do this // TODO Consolidate and just do this
WidgetType::Draw(_) | WidgetType::Checkbox(_) => self.id == Some(name.to_string()), WidgetType::Draw(_) | WidgetType::Checkbox(_) | WidgetType::TextBox(_) => {
self.id == Some(name.to_string())
}
WidgetType::Btn(ref btn) => btn.action == name, WidgetType::Btn(ref btn) => btn.action == name,
WidgetType::Slider(ref n) => n == name, WidgetType::Slider(ref n) => n == name,
WidgetType::Menu(ref n) => n == name, WidgetType::Menu(ref n) => n == name,
@ -623,7 +641,9 @@ impl ManagedWidget {
fn find_mut(&mut self, name: &str) -> Option<&mut ManagedWidget> { fn find_mut(&mut self, name: &str) -> Option<&mut ManagedWidget> {
let found = match self.widget { let found = match self.widget {
// TODO Consolidate and just do this // TODO Consolidate and just do this
WidgetType::Draw(_) | WidgetType::Checkbox(_) => self.id == Some(name.to_string()), WidgetType::Draw(_) | WidgetType::Checkbox(_) | WidgetType::TextBox(_) => {
self.id == Some(name.to_string())
}
WidgetType::Btn(ref btn) => btn.action == name, WidgetType::Btn(ref btn) => btn.action == name,
WidgetType::Slider(ref n) => n == name, WidgetType::Slider(ref n) => n == name,
WidgetType::Menu(ref n) => n == name, WidgetType::Menu(ref n) => n == name,
@ -925,6 +945,13 @@ impl Composite {
} }
} }
pub fn text_box(&self, name: &str) -> String {
match self.find(name).widget {
WidgetType::TextBox(ref textbox) => textbox.get_entry(),
_ => panic!("{} isn't a textbox", name),
}
}
pub fn filler_rect(&self, name: &str) -> ScreenRectangle { pub fn filler_rect(&self, name: &str) -> ScreenRectangle {
let f = &self.fillers[name]; let f = &self.fillers[name];
ScreenRectangle::top_left(f.top_left, f.dims) ScreenRectangle::top_left(f.top_left, f.dims)

View File

@ -14,7 +14,7 @@ pub const INACTIVE_CHOICE_COLOR: Color = Color::grey(0.4);
pub const SCALE_LINE_HEIGHT: f64 = 1.2; pub const SCALE_LINE_HEIGHT: f64 = 1.2;
// TODO Don't do this! // TODO Don't do this!
const MAX_CHAR_WIDTH: f64 = 25.0; pub const MAX_CHAR_WIDTH: f64 = 25.0;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Font { pub enum Font {

View File

@ -24,5 +24,6 @@ pub use self::plot::{Plot, PlotOptions, Series};
pub(crate) use self::popup_menu::PopupMenu; pub(crate) use self::popup_menu::PopupMenu;
pub(crate) use self::screenshot::{screenshot_current, screenshot_everything}; pub(crate) use self::screenshot::{screenshot_current, screenshot_everything};
pub use self::slider::{ItemSlider, Slider, WarpingItemSlider}; pub use self::slider::{ItemSlider, Slider, WarpingItemSlider};
pub(crate) use self::text_box::TextBox;
pub use self::warper::Warper; pub use self::warper::Warper;
pub use self::wizard::{Choice, Wizard, WrappedWizard}; pub use self::wizard::{Choice, Wizard, WrappedWizard};

View File

@ -1,12 +1,9 @@
use crate::layout::Widget; use crate::layout::Widget;
use crate::{ use crate::{text, EventCtx, GfxCtx, Key, Line, ScreenDims, ScreenPt, Text};
text, Event, EventCtx, GfxCtx, InputResult, Key, Line, ScreenDims, ScreenPt, Text, UserInput,
};
// TODO right now, only a single line // TODO right now, only a single line
pub struct TextBox { pub struct TextBox {
prompt: String,
// TODO A rope would be cool. // TODO A rope would be cool.
line: String, line: String,
cursor_x: usize, cursor_x: usize,
@ -17,25 +14,65 @@ pub struct TextBox {
} }
impl TextBox { impl TextBox {
pub fn new(ctx: &EventCtx, prompt: &str, prefilled: Option<String>) -> TextBox { pub fn new(ctx: &EventCtx, max_chars: usize, prefilled: String) -> TextBox {
let line = prefilled.unwrap_or_else(String::new); TextBox {
let mut tb = TextBox { cursor_x: prefilled.len(),
prompt: prompt.to_string(), line: prefilled,
cursor_x: line.len(),
line,
shift_pressed: false, shift_pressed: false,
top_left: ScreenPt::new(0.0, 0.0), top_left: ScreenPt::new(0.0, 0.0),
dims: ScreenDims::new(0.0, 0.0), dims: ScreenDims::new(
}; (max_chars as f64) * text::MAX_CHAR_WIDTH,
// TODO Assume the dims never exceed the prompt width? ctx.default_line_height(),
tb.dims = tb.get_text().dims(&ctx.prerender.assets); ),
tb }
} }
pub fn get_text(&self) -> Text { pub fn event(&mut self, ctx: &mut EventCtx) {
let mut txt = Text::from(Line(&self.prompt).roboto_bold()).with_bg(); if let Some(key) = ctx.input.any_key_pressed() {
txt.add(Line(&self.line[0..self.cursor_x])); match key {
Key::LeftShift => {
self.shift_pressed = true;
}
Key::LeftArrow => {
if self.cursor_x > 0 {
self.cursor_x -= 1;
}
}
Key::RightArrow => {
self.cursor_x = (self.cursor_x + 1).min(self.line.len());
}
Key::Backspace => {
if self.cursor_x > 0 {
self.line.remove(self.cursor_x - 1);
self.cursor_x -= 1;
}
}
_ => {
if let Some(c) = key.to_char(self.shift_pressed) {
self.line.insert(self.cursor_x, c);
self.cursor_x += 1;
} else {
ctx.input.unconsume_event();
}
}
};
}
if ctx.input.key_released(Key::LeftShift) {
self.shift_pressed = false;
}
}
pub fn draw(&self, g: &mut GfxCtx) {
g.draw_blocking_text_at_screenspace_topleft(self.calculate_text(), self.top_left);
}
pub fn get_entry(&self) -> String {
self.line.clone()
}
fn calculate_text(&self) -> Text {
let mut txt = Text::from(Line(&self.line[0..self.cursor_x]));
if self.cursor_x < self.line.len() { if self.cursor_x < self.line.len() {
// TODO This "cursor" looks awful! // TODO This "cursor" looks awful!
txt.append_all(vec![ txt.append_all(vec![
@ -48,45 +85,6 @@ impl TextBox {
} }
txt txt
} }
pub fn event(&mut self, input: &mut UserInput) -> InputResult<()> {
let maybe_ev = input.use_event_directly();
if maybe_ev.is_none() {
return InputResult::StillActive;
}
let ev = maybe_ev.unwrap();
if ev == Event::KeyPress(Key::Escape) {
return InputResult::Canceled;
} else if ev == Event::KeyPress(Key::Enter) {
return InputResult::Done(self.line.clone(), ());
} else if ev == Event::KeyPress(Key::LeftShift) {
self.shift_pressed = true;
} else if ev == Event::KeyRelease(Key::LeftShift) {
self.shift_pressed = false;
} else if ev == Event::KeyPress(Key::LeftArrow) {
if self.cursor_x > 0 {
self.cursor_x -= 1;
}
} else if ev == Event::KeyPress(Key::RightArrow) {
self.cursor_x = (self.cursor_x + 1).min(self.line.len());
} else if ev == Event::KeyPress(Key::Backspace) {
if self.cursor_x > 0 {
self.line.remove(self.cursor_x - 1);
self.cursor_x -= 1;
}
} else if let Event::KeyPress(key) = ev {
if let Some(c) = key.to_char(self.shift_pressed) {
self.line.insert(self.cursor_x, c);
self.cursor_x += 1;
}
};
InputResult::StillActive
}
pub fn draw(&self, g: &mut GfxCtx) {
g.draw_blocking_text_at_screenspace_topleft(self.get_text(), self.top_left);
}
} }
impl Widget for TextBox { impl Widget for TextBox {

View File

@ -1,15 +1,14 @@
use crate::widgets::text_box::TextBox;
use crate::widgets::PopupMenu; use crate::widgets::PopupMenu;
use crate::{ use crate::{
hotkey, layout, Button, Color, Composite, EventCtx, GfxCtx, HorizontalAlignment, InputResult, hotkey, Button, Color, Composite, EventCtx, GfxCtx, HorizontalAlignment, InputResult, Key,
Key, Line, ManagedWidget, MultiKey, Outcome, Text, VerticalAlignment, Line, ManagedWidget, MultiKey, Outcome, Text, VerticalAlignment,
}; };
use abstutil::Cloneable; use abstutil::Cloneable;
use std::collections::VecDeque; use std::collections::VecDeque;
pub struct Wizard { pub struct Wizard {
alive: bool, alive: bool,
tb: Option<TextBox>, tb_comp: Option<Composite>,
menu_comp: Option<Composite>, menu_comp: Option<Composite>,
ack: Option<Composite>, ack: Option<Composite>,
@ -21,7 +20,7 @@ impl Wizard {
pub fn new() -> Wizard { pub fn new() -> Wizard {
Wizard { Wizard {
alive: true, alive: true,
tb: None, tb_comp: None,
menu_comp: None, menu_comp: None,
ack: None, ack: None,
confirmed_state: Vec::new(), confirmed_state: Vec::new(),
@ -32,8 +31,8 @@ impl Wizard {
if let Some(ref comp) = self.menu_comp { if let Some(ref comp) = self.menu_comp {
comp.draw(g); comp.draw(g);
} }
if let Some(ref tb) = self.tb { if let Some(ref comp) = self.tb_comp {
tb.draw(g); comp.draw(g);
} }
if let Some(ref s) = self.ack { if let Some(ref s) = self.ack {
s.draw(g); s.draw(g);
@ -82,30 +81,69 @@ impl Wizard {
return None; return None;
} }
if self.tb.is_none() { if self.tb_comp.is_none() {
self.tb = Some(TextBox::new(ctx, query, prefilled)); self.tb_comp = Some(
Composite::new(
ManagedWidget::col(vec![
ManagedWidget::row(vec![
ManagedWidget::draw_text(ctx, Text::from(Line(query).roboto_bold())),
// TODO nice text button
ManagedWidget::btn(Button::text_bg(
Text::from(Line("X").fg(Color::BLACK)),
Color::WHITE,
Color::ORANGE,
hotkey(Key::Escape),
"quit",
ctx,
))
.margin(5)
.align_right(),
]),
ManagedWidget::text_entry(ctx, prefilled.unwrap_or_else(String::new))
.named("input"),
ManagedWidget::btn(Button::text_bg(
Text::from(Line("Done").fg(Color::BLACK)),
Color::WHITE,
Color::ORANGE,
hotkey(Key::Enter),
"done",
ctx,
)),
])
.bg(Color::grey(0.4))
.outline(5.0, Color::WHITE)
.padding(5),
)
.build(ctx),
);
} }
layout::stack_vertically(
layout::ContainerOrientation::Centered,
ctx,
vec![self.tb.as_mut().unwrap()],
);
match self.tb.as_mut().unwrap().event(&mut ctx.input) { assert!(self.alive);
InputResult::StillActive => None,
InputResult::Canceled => { // Otherwise, we try to use one event for two inputs potentially
self.alive = false; if ctx.input.has_been_consumed() {
None return None;
} }
InputResult::Done(line, _) => {
self.tb = None; match self.tb_comp.as_mut().unwrap().event(ctx) {
if let Some(result) = parser(line.clone()) { Some(Outcome::Clicked(x)) => match x.as_ref() {
Some(result) "quit" => {
} else { self.alive = false;
println!("Invalid input {}", line); self.tb_comp = None;
None return None;
} }
} "done" => {
let line = self.tb_comp.take().unwrap().text_box("input");
if let Some(result) = parser(line.clone()) {
Some(result)
} else {
println!("Invalid input {}", line);
None
}
}
_ => unreachable!(),
},
None => None,
} }
} }
} }
@ -399,7 +437,7 @@ impl<'a, 'b> WrappedWizard<'a, 'b> {
// If the control flow through a wizard block needs to change, might need to call this. // If the control flow through a wizard block needs to change, might need to call this.
pub fn reset(&mut self) { pub fn reset(&mut self) {
assert!(self.wizard.tb.is_none()); assert!(self.wizard.tb_comp.is_none());
assert!(self.wizard.menu_comp.is_none()); assert!(self.wizard.menu_comp.is_none());
assert!(self.wizard.ack.is_none()); assert!(self.wizard.ack.is_none());
self.wizard.confirmed_state.clear(); self.wizard.confirmed_state.clear();

View File

@ -177,7 +177,12 @@ impl State for TrafficSignalEditor {
); );
} }
"Edit metadata" => { "Edit metadata" => {
return Transition::Push(edit_md(self.i)); // TODO Not sure which one I prefer usability or code wise...
if true {
return Transition::Push(Box::new(EditMetadata::new(ctx, app, self.i)));
} else {
return Transition::Push(edit_md(self.i));
}
} }
"Export" => { "Export" => {
if orig_signal.observation_md.is_none() { if orig_signal.observation_md.is_none() {
@ -879,6 +884,140 @@ impl State for PreviewTrafficSignal {
} }
} }
struct EditMetadata {
composite: Composite,
}
impl EditMetadata {
fn new(ctx: &mut EventCtx, app: &App, i: IntersectionID) -> EditMetadata {
let default = traffic_signals::Metadata {
author: "Anonymous".to_string(),
datetime: "MM/DD/YYYY HH:MM:SS".to_string(),
notes: "no notes".to_string(),
};
let prev_observed = app
.primary
.map
.get_traffic_signal(i)
.observation_md
.clone()
.unwrap_or_else(|| default.clone());
// TODO Always ask this?
let prev_audited = app
.primary
.map
.get_traffic_signal(i)
.audit_md
.clone()
.unwrap_or_else(|| default.clone());
EditMetadata {
composite: Composite::new(
ManagedWidget::col(vec![
ManagedWidget::row(vec![
ManagedWidget::draw_text(
ctx,
Text::from(Line("Metadata about the traffic signal").roboto_bold()),
),
WrappedComposite::text_button(ctx, "X", hotkey(Key::Escape)).align_right(),
]),
ManagedWidget::draw_text(ctx, Text::from(Line("The mapper").roboto_bold())),
ManagedWidget::draw_text(
ctx,
Text::from(Line(
"Who mapped this signal? (Feel free to remain anonymous.)",
)),
),
ManagedWidget::text_entry(ctx, prev_observed.author).named("observed author"),
ManagedWidget::draw_text(
ctx,
Text::from(Line("When was this signal mapped? TODO format")),
),
ManagedWidget::text_entry(ctx, prev_observed.datetime)
.named("observed datetime"),
ManagedWidget::draw_text(
ctx,
Text::from(Line("Any other observations about the signal?")),
),
ManagedWidget::text_entry(ctx, prev_observed.notes).named("observed notes"),
ManagedWidget::draw_text(
ctx,
Text::from(
Line("The last person to audit the mapped signal").roboto_bold(),
),
),
ManagedWidget::draw_text(
ctx,
Text::from(Line(
"Who audited this signal? (Feel free to remain anonymous.)",
)),
),
ManagedWidget::text_entry(ctx, prev_audited.author).named("audited author"),
ManagedWidget::draw_text(
ctx,
Text::from(Line("When was this signal audited? TODO format")),
),
ManagedWidget::text_entry(ctx, prev_audited.datetime).named("audited datetime"),
ManagedWidget::draw_text(
ctx,
Text::from(Line("Any other notes about auditing the signal?")),
),
ManagedWidget::text_entry(ctx, prev_audited.notes).named("audited notes"),
WrappedComposite::text_bg_button(ctx, "Done", hotkey(Key::Enter))
.centered_horiz(),
])
.bg(colors::PANEL_BG),
)
.build(ctx),
}
}
}
impl State for EditMetadata {
fn event(&mut self, ctx: &mut EventCtx, _: &mut App) -> Transition {
match self.composite.event(ctx) {
Some(Outcome::Clicked(x)) => match x.as_ref() {
"X" => {
return Transition::Pop;
}
"Done" => {
// This feels like overkill...
let observed = traffic_signals::Metadata {
author: self.composite.text_box("observed author"),
datetime: self.composite.text_box("observed datetime"),
notes: self.composite.text_box("observed notes"),
};
let audited = traffic_signals::Metadata {
author: self.composite.text_box("audited author"),
datetime: self.composite.text_box("audited datetime"),
notes: self.composite.text_box("audited notes"),
};
return Transition::PopWithData(Box::new(move |state, app, ctx| {
let editor = state.downcast_mut::<TrafficSignalEditor>().unwrap();
let orig_signal = app.primary.map.get_traffic_signal(editor.i);
let mut new_signal = orig_signal.clone();
new_signal.observation_md = Some(observed);
new_signal.audit_md = Some(audited);
editor.command_stack.push(orig_signal.clone());
editor.redo_stack.clear();
editor.top_panel = make_top_panel(ctx, app, true, false);
change_traffic_signal(new_signal, app, ctx);
}));
}
_ => unreachable!(),
},
None => {}
}
Transition::Keep
}
fn draw(&self, g: &mut GfxCtx, _: &App) {
self.composite.draw(g);
}
}
fn edit_md(i: IntersectionID) -> Box<dyn State> { fn edit_md(i: IntersectionID) -> Box<dyn State> {
WizardState::new(Box::new(move |wiz, ctx, app| { WizardState::new(Box::new(move |wiz, ctx, app| {
let mut wizard = wiz.wrap(ctx); let mut wizard = wiz.wrap(ctx);