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 Some kind of plot?!
// TODO Some popup dialogs?
// TODO Something scrolling
// TODO Some popup dialogs with form entry, even some scrolling
// TODO Loading screen with timer?
struct App {

View File

@ -41,6 +41,18 @@ impl UserInput {
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 {
self.reserve_key(key, action);

View File

@ -1,5 +1,6 @@
use crate::layout::Widget;
use crate::widgets::{Checkbox, PopupMenu};
use crate::text;
use crate::widgets::{Checkbox, PopupMenu, TextBox};
use crate::{
Button, Color, Drawable, EventCtx, Filler, GeomBatch, GfxCtx, Histogram, HorizontalAlignment,
JustDraw, MultiKey, Plot, RewriteColor, ScreenDims, ScreenPt, ScreenRectangle, Slider, Text,
@ -30,6 +31,7 @@ enum WidgetType {
Draw(JustDraw),
Btn(Button),
Checkbox(Checkbox),
TextBox(TextBox),
Slider(String),
Menu(String),
Filler(String),
@ -289,6 +291,11 @@ impl ManagedWidget {
.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 {
ManagedWidget::new(WidgetType::DurationPlot(plot))
}
@ -329,6 +336,9 @@ impl ManagedWidget {
WidgetType::Checkbox(ref mut checkbox) => {
checkbox.event(ctx);
}
WidgetType::TextBox(ref mut textbox) => {
textbox.event(ctx);
}
WidgetType::Slider(ref name) => {
sliders.get_mut(name).unwrap().event(ctx);
}
@ -364,6 +374,7 @@ impl ManagedWidget {
WidgetType::Draw(ref j) => j.draw(g),
WidgetType::Btn(ref btn) => btn.draw(g),
WidgetType::Checkbox(ref checkbox) => checkbox.draw(g),
WidgetType::TextBox(ref textbox) => textbox.draw(g),
WidgetType::Slider(ref name) => {
if name != "horiz scrollbar" && name != "vert scrollbar" {
sliders[name].draw(g);
@ -397,6 +408,7 @@ impl ManagedWidget {
WidgetType::Draw(ref widget) => widget,
WidgetType::Btn(ref widget) => widget,
WidgetType::Checkbox(ref widget) => widget,
WidgetType::TextBox(ref widget) => widget,
WidgetType::Slider(ref name) => &sliders[name],
WidgetType::Menu(ref name) => &menus[name],
WidgetType::Filler(ref name) => &fillers[name],
@ -505,6 +517,9 @@ impl ManagedWidget {
WidgetType::Checkbox(ref mut widget) => {
widget.set_pos(top_left);
}
WidgetType::TextBox(ref mut widget) => {
widget.set_pos(top_left);
}
WidgetType::Slider(ref name) => {
sliders.get_mut(name).unwrap().set_pos(top_left);
}
@ -566,6 +581,7 @@ impl ManagedWidget {
| WidgetType::Menu(_)
| WidgetType::Filler(_)
| WidgetType::Checkbox(_)
| WidgetType::TextBox(_)
| WidgetType::DurationPlot(_)
| WidgetType::UsizePlot(_) => {}
WidgetType::Histogram(_) => {}
@ -597,7 +613,9 @@ impl ManagedWidget {
fn find(&self, name: &str) -> Option<&ManagedWidget> {
let found = match self.widget {
// 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::Slider(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> {
let found = match self.widget {
// 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::Slider(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 {
let f = &self.fillers[name];
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;
// 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)]
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::screenshot::{screenshot_current, screenshot_everything};
pub use self::slider::{ItemSlider, Slider, WarpingItemSlider};
pub(crate) use self::text_box::TextBox;
pub use self::warper::Warper;
pub use self::wizard::{Choice, Wizard, WrappedWizard};

View File

@ -1,12 +1,9 @@
use crate::layout::Widget;
use crate::{
text, Event, EventCtx, GfxCtx, InputResult, Key, Line, ScreenDims, ScreenPt, Text, UserInput,
};
use crate::{text, EventCtx, GfxCtx, Key, Line, ScreenDims, ScreenPt, Text};
// TODO right now, only a single line
pub struct TextBox {
prompt: String,
// TODO A rope would be cool.
line: String,
cursor_x: usize,
@ -17,25 +14,65 @@ pub struct TextBox {
}
impl TextBox {
pub fn new(ctx: &EventCtx, prompt: &str, prefilled: Option<String>) -> TextBox {
let line = prefilled.unwrap_or_else(String::new);
let mut tb = TextBox {
prompt: prompt.to_string(),
cursor_x: line.len(),
line,
pub fn new(ctx: &EventCtx, max_chars: usize, prefilled: String) -> TextBox {
TextBox {
cursor_x: prefilled.len(),
line: prefilled,
shift_pressed: false,
top_left: ScreenPt::new(0.0, 0.0),
dims: ScreenDims::new(0.0, 0.0),
};
// TODO Assume the dims never exceed the prompt width?
tb.dims = tb.get_text().dims(&ctx.prerender.assets);
tb
dims: ScreenDims::new(
(max_chars as f64) * text::MAX_CHAR_WIDTH,
ctx.default_line_height(),
),
}
}
pub fn get_text(&self) -> Text {
let mut txt = Text::from(Line(&self.prompt).roboto_bold()).with_bg();
txt.add(Line(&self.line[0..self.cursor_x]));
pub fn event(&mut self, ctx: &mut EventCtx) {
if let Some(key) = ctx.input.any_key_pressed() {
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() {
// TODO This "cursor" looks awful!
txt.append_all(vec![
@ -48,45 +85,6 @@ impl TextBox {
}
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 {

View File

@ -1,15 +1,14 @@
use crate::widgets::text_box::TextBox;
use crate::widgets::PopupMenu;
use crate::{
hotkey, layout, Button, Color, Composite, EventCtx, GfxCtx, HorizontalAlignment, InputResult,
Key, Line, ManagedWidget, MultiKey, Outcome, Text, VerticalAlignment,
hotkey, Button, Color, Composite, EventCtx, GfxCtx, HorizontalAlignment, InputResult, Key,
Line, ManagedWidget, MultiKey, Outcome, Text, VerticalAlignment,
};
use abstutil::Cloneable;
use std::collections::VecDeque;
pub struct Wizard {
alive: bool,
tb: Option<TextBox>,
tb_comp: Option<Composite>,
menu_comp: Option<Composite>,
ack: Option<Composite>,
@ -21,7 +20,7 @@ impl Wizard {
pub fn new() -> Wizard {
Wizard {
alive: true,
tb: None,
tb_comp: None,
menu_comp: None,
ack: None,
confirmed_state: Vec::new(),
@ -32,8 +31,8 @@ impl Wizard {
if let Some(ref comp) = self.menu_comp {
comp.draw(g);
}
if let Some(ref tb) = self.tb {
tb.draw(g);
if let Some(ref comp) = self.tb_comp {
comp.draw(g);
}
if let Some(ref s) = self.ack {
s.draw(g);
@ -82,23 +81,59 @@ impl Wizard {
return None;
}
if self.tb.is_none() {
self.tb = Some(TextBox::new(ctx, query, prefilled));
}
layout::stack_vertically(
layout::ContainerOrientation::Centered,
if self.tb_comp.is_none() {
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,
vec![self.tb.as_mut().unwrap()],
))
.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),
);
match self.tb.as_mut().unwrap().event(&mut ctx.input) {
InputResult::StillActive => None,
InputResult::Canceled => {
self.alive = false;
None
}
InputResult::Done(line, _) => {
self.tb = None;
assert!(self.alive);
// Otherwise, we try to use one event for two inputs potentially
if ctx.input.has_been_consumed() {
return None;
}
match self.tb_comp.as_mut().unwrap().event(ctx) {
Some(Outcome::Clicked(x)) => match x.as_ref() {
"quit" => {
self.alive = false;
self.tb_comp = None;
return None;
}
"done" => {
let line = self.tb_comp.take().unwrap().text_box("input");
if let Some(result) = parser(line.clone()) {
Some(result)
} else {
@ -106,6 +141,9 @@ impl Wizard {
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.
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.ack.is_none());
self.wizard.confirmed_state.clear();

View File

@ -177,8 +177,13 @@ impl State for TrafficSignalEditor {
);
}
"Edit metadata" => {
// 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" => {
if orig_signal.observation_md.is_none() {
return Transition::Push(msg("Error", vec!["Edit the metadata first"]));
@ -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> {
WizardState::new(Box::new(move |wiz, ctx, app| {
let mut wizard = wiz.wrap(ctx);