commit
d51d778f92
@ -0,0 +1,532 @@ |
|||||||
|
/**
|
||||||
|
* A toolkit for building character based user interfaces on small displays. |
||||||
|
*
|
||||||
|
* Copyright (c) 2012 Jason von Nieda <jason@vonnieda.org> |
||||||
|
* This work is licensed under the Creative Commons Attribution-ShareAlike 3.0
|
||||||
|
* Unported License. To view a copy of this license, visit
|
||||||
|
* http://creativecommons.org/licenses/by-sa/3.0/ or send a letter to Creative
|
||||||
|
* Commons, 444 Castro Street, Suite 900, Mountain View, California, 94041, USA. |
||||||
|
*/ |
||||||
|
|
||||||
|
#include <ScreenUi.h> |
||||||
|
#include <string.h> |
||||||
|
#include <stdlib.h> |
||||||
|
#include <stdio.h> |
||||||
|
|
||||||
|
#ifdef SCREENUI_DEBUG |
||||||
|
#include <WProgram.h> |
||||||
|
#endif |
||||||
|
|
||||||
|
// TODO: Change to PROGMEM
|
||||||
|
uint8_t charCheckmark[] = {0, // B00000
|
||||||
|
0, // B00000
|
||||||
|
1, // B00001
|
||||||
|
2, // B00010
|
||||||
|
20, // B10100
|
||||||
|
8, // B01000
|
||||||
|
0, // B00000
|
||||||
|
0}; // B00000
|
||||||
|
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// Screen
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
Screen::Screen(uint8_t width, uint8_t height) { |
||||||
|
setSize(width, height); |
||||||
|
cleared_ = false; |
||||||
|
focusHolder_ = NULL; |
||||||
|
focusHolderSelected_ = false; |
||||||
|
createCustomChar(7, charCheckmark); |
||||||
|
annoyingBugWorkedAround_ = false; |
||||||
|
} |
||||||
|
|
||||||
|
void Screen::update() { |
||||||
|
if (!cleared_) { |
||||||
|
clear(); |
||||||
|
cleared_ = true; |
||||||
|
} |
||||||
|
Container::update(this); |
||||||
|
int x, y; |
||||||
|
bool selected, cancelled; |
||||||
|
getInputDeltas(&x, &y, &selected, &cancelled); |
||||||
|
Component *oldFocusHolder = focusHolder_; |
||||||
|
if (x || y || selected || cancelled) { |
||||||
|
if (focusHolderSelected_) { |
||||||
|
focusHolderSelected_ = focusHolder_->handleInputEvent(x, y, selected, cancelled); |
||||||
|
} |
||||||
|
else { |
||||||
|
if (selected) { |
||||||
|
focusHolderSelected_ = focusHolder_->handleInputEvent(x, y, selected, cancelled); |
||||||
|
} |
||||||
|
else if (x || y) { |
||||||
|
// TODO: Make axis x or y configurable.
|
||||||
|
if (y > 0) { |
||||||
|
focusHolder_ = nextFocusHolder(focusHolder_, false); |
||||||
|
if (!focusHolder_) { |
||||||
|
focusHolder_ = nextFocusHolder(focusHolder_, false); |
||||||
|
} |
||||||
|
} |
||||||
|
else if (y < 0) { |
||||||
|
focusHolder_ = nextFocusHolder(focusHolder_, true); |
||||||
|
if (!focusHolder_) { |
||||||
|
focusHolder_ = nextFocusHolder(focusHolder_, true); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
if (focusHolder_ == NULL) { |
||||||
|
focusHolder_ = nextFocusHolder(focusHolder_, false); |
||||||
|
} |
||||||
|
if (oldFocusHolder != focusHolder_) { |
||||||
|
if (oldFocusHolder) { |
||||||
|
oldFocusHolder->repaint(); |
||||||
|
} |
||||||
|
focusHolder_->repaint(); |
||||||
|
} |
||||||
|
paint(this); |
||||||
|
moveCursor(cursorX_, cursorY_); |
||||||
|
|
||||||
|
// TODO: Bug I can't figure out. If we use the dirtyness system, the first paint
|
||||||
|
// fails to draw anything to the screen and then subsequent ones don't get called
|
||||||
|
// because they aren't dirty. The second paint works fine.
|
||||||
|
// Adding repaint() here allows us to paint during the second loop which gets
|
||||||
|
// everything into a state where it works great, but I can't figure out why.
|
||||||
|
// It doesn't seem to have to do with timing or the clear().
|
||||||
|
if (!annoyingBugWorkedAround_) { |
||||||
|
repaint(); |
||||||
|
annoyingBugWorkedAround_ = true; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// Container
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
Container::Container() { |
||||||
|
components_ = NULL; |
||||||
|
componentsLength_ = 0; |
||||||
|
componentCount_ = 0; |
||||||
|
} |
||||||
|
|
||||||
|
Container::~Container() { |
||||||
|
free(components_); |
||||||
|
} |
||||||
|
|
||||||
|
void Container::update(Screen *screen) { |
||||||
|
for (int i = 0; i < componentCount_; i++) { |
||||||
|
components_[i]->update(screen); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
void Container::paint(Screen *screen) { |
||||||
|
for (int i = 0; i < componentCount_; i++) { |
||||||
|
if (components_[i]->dirty()) { |
||||||
|
components_[i]->paint(screen); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
void Container::repaint() { |
||||||
|
for (int i = 0; i < componentCount_; i++) { |
||||||
|
components_[i]->repaint(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
void Container::add(Component *component, int8_t x, int8_t y) { |
||||||
|
if (!components_ || componentsLength_ <= componentCount_) { |
||||||
|
uint8_t newComponentsLength = (componentsLength_ * 2) + 1; |
||||||
|
Component** newComponents = (Component**) malloc(newComponentsLength * sizeof(Component*)); |
||||||
|
for (int i = 0; i < componentCount_; i++) { |
||||||
|
newComponents[i] = components_[i]; |
||||||
|
} |
||||||
|
free(components_); |
||||||
|
components_ = newComponents; |
||||||
|
componentsLength_ = newComponentsLength; |
||||||
|
} |
||||||
|
components_[componentCount_++] = component; |
||||||
|
component->setLocation(x, y); |
||||||
|
component->repaint(); |
||||||
|
} |
||||||
|
|
||||||
|
Component *Container::nextFocusHolder(Component *focusHolder, bool reverse) { |
||||||
|
bool focusHolderFound = false; |
||||||
|
return nextFocusHolder(focusHolder, reverse, &focusHolderFound); |
||||||
|
} |
||||||
|
|
||||||
|
Component *Container::nextFocusHolder(Component *focusHolder, bool reverse, bool *focusHolderFound) { |
||||||
|
for (int i = (reverse ? componentCount_ - 1 : 0); (reverse ? (i > 0) : (i < componentCount_)); (reverse ? i-- : i++)) { |
||||||
|
Component *c = components_[i]; |
||||||
|
if (c->isContainer()) { |
||||||
|
Component *next = ((Container*) c)->nextFocusHolder(focusHolder, reverse, focusHolderFound); |
||||||
|
if (next) { |
||||||
|
return next; |
||||||
|
} |
||||||
|
} |
||||||
|
else { |
||||||
|
if (c->acceptsFocus()) { |
||||||
|
if (!focusHolder || *focusHolderFound) { |
||||||
|
return c; |
||||||
|
} |
||||||
|
else if (c == focusHolder) { |
||||||
|
*focusHolderFound = true; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
return NULL; |
||||||
|
} |
||||||
|
|
||||||
|
bool Container::dirty() { |
||||||
|
for (int i = 0; i < componentCount_; i++) { |
||||||
|
if (components_[i]->dirty()) { |
||||||
|
return true; |
||||||
|
} |
||||||
|
} |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
bool Container::contains(Component *component) { |
||||||
|
for (int i = 0; i < componentCount_; i++) { |
||||||
|
Component *c = components_[i]; |
||||||
|
if (c == component) { |
||||||
|
return true; |
||||||
|
} |
||||||
|
else if (c->isContainer()) { |
||||||
|
if (((Container*) c)->contains(component)) { |
||||||
|
return true; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// Component
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
void Component::paint(Screen *screen) { |
||||||
|
dirty_ = false; |
||||||
|
} |
||||||
|
|
||||||
|
bool Component::dirty() { |
||||||
|
return dirty_; |
||||||
|
} |
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// Label
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
Label::Label(const char *text) { |
||||||
|
setSize(0, 1); |
||||||
|
setText(text); |
||||||
|
captured_ = false; |
||||||
|
dirtyWidth_ = 0; |
||||||
|
} |
||||||
|
|
||||||
|
void Label::paint(Screen *screen) { |
||||||
|
Component::paint(screen); |
||||||
|
|
||||||
|
// Label does not accept focus, but Button, Checkbox and List are all
|
||||||
|
// subclasses that want to share the same text drawing system, so we
|
||||||
|
// just account for it here.
|
||||||
|
if (acceptsFocus()) { |
||||||
|
if (screen->focusHolder() == this) { |
||||||
|
if (captured_) { |
||||||
|
screen->draw(x_, y_, ">"); |
||||||
|
screen->draw(x_ + width_ + 1, y_, "<"); |
||||||
|
} |
||||||
|
else { |
||||||
|
screen->draw(x_, y_, "<"); |
||||||
|
screen->draw(x_ + width_ + 1, y_, ">"); |
||||||
|
} |
||||||
|
} |
||||||
|
else { |
||||||
|
screen->draw(x_, y_, "["); |
||||||
|
screen->draw(x_ + width_ + 1, y_, "]"); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
screen->draw(x_ + (acceptsFocus() ? 1 : 0), y_, text_); |
||||||
|
if (dirtyWidth_) { |
||||||
|
for (int i = 0; i < dirtyWidth_ - width_; i++) { |
||||||
|
screen->draw(x_ + width_ + i + (acceptsFocus() ? 2 : 0), y_, " "); |
||||||
|
} |
||||||
|
dirtyWidth_ = 0; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
void Label::setText(const char *text) { |
||||||
|
text_ = (char*) text; |
||||||
|
uint8_t newWidth = strlen(text); |
||||||
|
if (newWidth < width_) { |
||||||
|
dirtyWidth_ = width_; |
||||||
|
} |
||||||
|
width_ = newWidth; |
||||||
|
repaint(); |
||||||
|
} |
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// Button
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
Button::Button(const char *text) : Label(text) { |
||||||
|
setText(text); |
||||||
|
pressed_ = false; |
||||||
|
} |
||||||
|
|
||||||
|
void Button::update(Screen *screen) { |
||||||
|
pressed_ = false; |
||||||
|
} |
||||||
|
|
||||||
|
bool Button::handleInputEvent(int x, int y, bool selected, bool cancelled) { |
||||||
|
pressed_ = selected; |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// Checkbox
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
Checkbox::Checkbox() : Label(" ") { |
||||||
|
checked_ = false; |
||||||
|
} |
||||||
|
|
||||||
|
bool Checkbox::handleInputEvent(int x, int y, bool selected, bool cancelled) { |
||||||
|
if (selected) { |
||||||
|
checked_ = !checked_; |
||||||
|
// Not James Bond. The 8th custom character location. By using a non-zero
|
||||||
|
// location we can still send it via a string, which means we can still
|
||||||
|
// be a Label instead of having a custom paint routine.
|
||||||
|
setText(checked_ ? "\007" : " "); |
||||||
|
repaint(); |
||||||
|
} |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// List
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
List::List(uint8_t maxItems) : Label(NULL) { |
||||||
|
items_ = (char **) malloc(maxItems * (sizeof(char*))); |
||||||
|
itemCount_ = 0; |
||||||
|
selectedIndex_ = 0; |
||||||
|
captured_ = false; |
||||||
|
} |
||||||
|
|
||||||
|
void List::addItem(const char *item) { |
||||||
|
items_[itemCount_++] = (char*) item; |
||||||
|
if (text_ == NULL) { |
||||||
|
setText(selectedItem()); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
void List::setSelectedIndex(uint8_t selectedIndex) { |
||||||
|
selectedIndex_ = selectedIndex; |
||||||
|
setText(selectedItem()); |
||||||
|
repaint(); |
||||||
|
} |
||||||
|
|
||||||
|
bool List::handleInputEvent(int x, int y, bool selected, bool cancelled) { |
||||||
|
if (captured_ && y) { |
||||||
|
if (y < 0) { |
||||||
|
setSelectedIndex(max(selectedIndex_ + y, 0)); |
||||||
|
} |
||||||
|
else { |
||||||
|
setSelectedIndex(min(selectedIndex_ + y, itemCount_ - 1)); |
||||||
|
} |
||||||
|
} |
||||||
|
if (selected) { |
||||||
|
captured_ = !captured_; |
||||||
|
repaint(); |
||||||
|
} |
||||||
|
return captured_; |
||||||
|
} |
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// Input
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
// TODO: trim incoming text and after each return, right justify the text
|
||||||
|
Input::Input(char *text) : Label((const char*) text) { |
||||||
|
position_ = 0; |
||||||
|
selecting_ = false; |
||||||
|
} |
||||||
|
|
||||||
|
void Input::setText(char *text) { |
||||||
|
Label::setText((const char *) text); |
||||||
|
position_ = 0; |
||||||
|
selecting_ = false; |
||||||
|
repaint(); |
||||||
|
} |
||||||
|
|
||||||
|
void Input::paint(Screen *screen) { |
||||||
|
Label::paint(screen); |
||||||
|
screen->setCursorVisible(captured_ && selecting_); |
||||||
|
screen->setBlink(captured_ && !selecting_); |
||||||
|
screen->setCursorLocation(x_ + position_ + 1, y_); |
||||||
|
} |
||||||
|
|
||||||
|
bool Input::handleInputEvent(int x, int y, bool selected, bool cancelled) { |
||||||
|
// If the input is captured and there has been a scroll event we're going to
|
||||||
|
// either change the position or change the selection.
|
||||||
|
if (captured_ && y) { |
||||||
|
// If we're changing the selection, scroll through the character set.
|
||||||
|
if (selecting_) { |
||||||
|
// TODO: replace this with a selectable character set that makes more
|
||||||
|
// sense
|
||||||
|
if (y < 0) { |
||||||
|
text_[position_] = max(text_[position_] + y, ' '); |
||||||
|
} |
||||||
|
else { |
||||||
|
text_[position_] = min(text_[position_] + y, '}'); |
||||||
|
} |
||||||
|
} |
||||||
|
// Otherwise we are changing the position. If the position is moving before
|
||||||
|
// or after the field we release the input.
|
||||||
|
else { |
||||||
|
position_ += y; |
||||||
|
if (position_ < 0 || position_ >= width_) { |
||||||
|
captured_ = false; |
||||||
|
} |
||||||
|
} |
||||||
|
repaint(); |
||||||
|
} |
||||||
|
// If there has been a click we will either capture the input,
|
||||||
|
// start selection or end selection.
|
||||||
|
if (selected) { |
||||||
|
// If input is captured we will start or end selection
|
||||||
|
// input.
|
||||||
|
if (captured_) { |
||||||
|
selecting_ = !selecting_; |
||||||
|
} |
||||||
|
// Capture the input
|
||||||
|
else { |
||||||
|
captured_ = true; |
||||||
|
position_ = 0; |
||||||
|
selecting_ = false; |
||||||
|
} |
||||||
|
repaint(); |
||||||
|
} |
||||||
|
return captured_; |
||||||
|
} |
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// ScrollContainer
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
ScrollContainer::ScrollContainer(Screen *screen, uint8_t width, uint8_t height) { |
||||||
|
setSize(width, height); |
||||||
|
screen_ = screen; |
||||||
|
clearLine = (char*) malloc(width + 1); |
||||||
|
memset(clearLine, ' ', width); |
||||||
|
clearLine[width] = NULL; |
||||||
|
firstUpdateCompleted_ = false; |
||||||
|
} |
||||||
|
|
||||||
|
ScrollContainer::~ScrollContainer() { |
||||||
|
free(clearLine); |
||||||
|
} |
||||||
|
|
||||||
|
void ScrollContainer::add(Component *component, int8_t x, int8_t y) { |
||||||
|
Container::add(component, x, y); |
||||||
|
if (firstUpdateCompleted_) { |
||||||
|
// TODO: if the first update has already completed we need to update
|
||||||
|
// incoming components locations as they are added
|
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
bool ScrollContainer::dirty() { |
||||||
|
if (Container::dirty()) { |
||||||
|
return true; |
||||||
|
} |
||||||
|
return scrollNeeded(); |
||||||
|
} |
||||||
|
|
||||||
|
void ScrollContainer::update(Screen *screen) { |
||||||
|
if (!firstUpdateCompleted_) { |
||||||
|
offsetChildren(0, y_); |
||||||
|
firstUpdateCompleted_ = true; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
void ScrollContainer::offsetChildren(int x, int y) { |
||||||
|
for (int i = 0; i < componentCount_; i++) { |
||||||
|
Component *c = components_[i]; |
||||||
|
/*
|
||||||
|
Serial.print("Moving "); |
||||||
|
Serial.print(c->description()); |
||||||
|
Serial.print(" from "); |
||||||
|
Serial.print(c->x(), DEC); |
||||||
|
Serial.print(", "); |
||||||
|
Serial.print(c->y(), DEC); |
||||||
|
Serial.print(" to "); |
||||||
|
Serial.print(c->x() + x, DEC); |
||||||
|
Serial.print(", "); |
||||||
|
Serial.println(c->y() + y, DEC); |
||||||
|
*/ |
||||||
|
c->setLocation(c->x() + x, c->y() + y); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
bool ScrollContainer::scrollNeeded() { |
||||||
|
// see if the focus holder has changed since the last check
|
||||||
|
Component *focusHolder = screen_->focusHolder(); |
||||||
|
if (lastFocusHolder_ != focusHolder) { |
||||||
|
// it has, so see if the new focus holder is one of ours
|
||||||
|
if (contains(focusHolder)) { |
||||||
|
// it is, so we need to be sure it's visible, which means it's
|
||||||
|
// y position plus height has to be within our window of visibility
|
||||||
|
// our window of visbility is our y_ + row_ to y_ + row_ + height_
|
||||||
|
uint8_t yStart = y_; |
||||||
|
uint8_t yEnd = y_ + height_ - 1; |
||||||
|
if (focusHolder->y() < yStart || focusHolder->y() > yEnd) { |
||||||
|
// it is not currently visible, so we are dirty
|
||||||
|
return true; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
void ScrollContainer::paint(Screen *screen) { |
||||||
|
Component::paint(screen); |
||||||
|
if (scrollNeeded()) { |
||||||
|
// we need to scroll the window
|
||||||
|
Component *focusHolder = screen_->focusHolder(); |
||||||
|
// clear the window
|
||||||
|
for (int i = 0; i < height_; i++) { |
||||||
|
screen->draw(x_, y_ + i, clearLine); |
||||||
|
} |
||||||
|
// set the new row_. if the new focus holder is below our currently
|
||||||
|
// visible area we want to increment the row the minimum amount to make it
|
||||||
|
// visible, and likewise if it is above, we want to decrement.
|
||||||
|
// TODO: These are calculated in scrollNeeded(), see if it would be better
|
||||||
|
// to reuse them somehow
|
||||||
|
uint8_t yStart = y_; |
||||||
|
uint8_t yEnd = y_ + height_ - 1; |
||||||
|
if (focusHolder->y() > yEnd) { |
||||||
|
// if the component is below our visible window, increment the row count
|
||||||
|
// by the difference between the bottom visible row and the y position
|
||||||
|
// of the component
|
||||||
|
offsetChildren(0, yEnd - focusHolder->y()); |
||||||
|
} |
||||||
|
else { |
||||||
|
offsetChildren(0, yStart - focusHolder->y()); |
||||||
|
} |
||||||
|
|
||||||
|
lastFocusHolder_ = screen->focusHolder(); |
||||||
|
|
||||||
|
// tell all the children they need to be repainted. we will only paint
|
||||||
|
// the ones that are now visible
|
||||||
|
repaint(); |
||||||
|
} |
||||||
|
for (int i = 0; i < componentCount_; i++) { |
||||||
|
Component *component = components_[i]; |
||||||
|
if (component->dirty() && (component->y() >= y_) && (component->y() < y_ + height_)) { |
||||||
|
component->paint(screen); |
||||||
|
} |
||||||
|
else { |
||||||
|
component->clearDirty(); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
@ -0,0 +1,294 @@ |
|||||||
|
/**
|
||||||
|
* A toolkit for building character based user interfaces on small displays. |
||||||
|
*
|
||||||
|
* Copyright (c) 2012 Jason von Nieda <jason@vonnieda.org> |
||||||
|
* This work is licensed under the Creative Commons Attribution-ShareAlike 3.0
|
||||||
|
* Unported License. To view a copy of this license, visit
|
||||||
|
* http://creativecommons.org/licenses/by-sa/3.0/ or send a letter to Creative
|
||||||
|
* Commons, 444 Castro Street, Suite 900, Mountain View, California, 94041, USA. |
||||||
|
*/ |
||||||
|
|
||||||
|
#ifndef __ScreenUi_h__ |
||||||
|
#define __ScreenUi_h__ |
||||||
|
|
||||||
|
#include <stdint.h> |
||||||
|
|
||||||
|
//#define SCREENUI_DEBUG 1
|
||||||
|
|
||||||
|
#define min(a,b) ((a)<(b)?(a):(b)) |
||||||
|
#define max(a,b) ((a)>(b)?(a):(b)) |
||||||
|
|
||||||
|
class Screen; |
||||||
|
|
||||||
|
class Component { |
||||||
|
public: |
||||||
|
Component() { x_ = y_ = width_ = height_ = 0; } |
||||||
|
// Set the location on screen for this component. x and y are zero based,
|
||||||
|
// absolute character positions.
|
||||||
|
virtual void setLocation(int8_t x, int8_t y) { x_ = x; y_ = y;} |
||||||
|
// Set the width and height this component. Most components will either
|
||||||
|
// require a width and height to be specified during creation or will
|
||||||
|
// adopt sane defaults based on their input data.
|
||||||
|
void setSize(uint8_t width, uint8_t height) { width_ = width; height_ = height; } |
||||||
|
int8_t x() { return x_; } |
||||||
|
int8_t y() { return y_; } |
||||||
|
uint8_t width() { return width_; } |
||||||
|
uint8_t height() { return height_; } |
||||||
|
// Returns true if the component is willing to accept focus from the focus
|
||||||
|
// subsystem. For a component to receive input events it must be willing
|
||||||
|
// to accept focus.
|
||||||
|
virtual bool acceptsFocus() { return false; } |
||||||
|
// The first step in the component update cycle. This is called by Screen
|
||||||
|
// during it's update cycle to allow each component to reset or set up
|
||||||
|
// any data that needs to be modified from the last update cycle.
|
||||||
|
virtual void update(Screen *screen) {} |
||||||
|
// Called if the component has focus and is selected. x and y are delta
|
||||||
|
// since the last event.
|
||||||
|
// Returns true if the component wishes to remain selected. Returns false
|
||||||
|
// to give up selection.
|
||||||
|
virtual bool handleInputEvent(int x, int y, bool selected, bool cancelled) { return false; } |
||||||
|
// The final step in the component update cycle. Called by Screen to allow
|
||||||
|
// the component to draw itself on screen. It shoud generally draw itself
|
||||||
|
// at it's location and should not overflow it's size.
|
||||||
|
// Currently components are responsible for drawing their own focus
|
||||||
|
// indicator. This may change in the future.
|
||||||
|
// Component::paint() sets dirty to false and should be called by every
|
||||||
|
// subclass's paint method.
|
||||||
|
virtual void paint(Screen *screen); |
||||||
|
// Returns true if the component is a container for other components. This
|
||||||
|
// is a shortcut so that we don't have to do RTTI when iterating over a
|
||||||
|
// list of components looking for containers.
|
||||||
|
virtual bool isContainer() { return false; } |
||||||
|
// Returns true if the Component is marked dirty and needs to be painted
|
||||||
|
// on the next update.
|
||||||
|
virtual bool dirty(); |
||||||
|
// Sets dirty to true for this Component, causing it to be painted during
|
||||||
|
// the next update.
|
||||||
|
// TODO refactor the two below into setDirty()
|
||||||
|
virtual void repaint() { dirty_ = true; } |
||||||
|
virtual void clearDirty() { dirty_ = false; } |
||||||
|
#ifdef SCREENUI_DEBUG |
||||||
|
virtual char *description() { return "Component"; } |
||||||
|
#endif |
||||||
|
protected: |
||||||
|
int8_t x_, y_; |
||||||
|
uint8_t width_, height_; |
||||||
|
bool dirty_; |
||||||
|
}; |
||||||
|
|
||||||
|
// A Component that contains other Components. Users should not generally
|
||||||
|
// create instances of this class.
|
||||||
|
class Container : public Component { |
||||||
|
public: |
||||||
|
Container(); |
||||||
|
~Container(); |
||||||
|
virtual void add(Component *component, int8_t x, int8_t y); |
||||||
|
virtual void update(Screen *screen); |
||||||
|
// Paints any dirty child components.
|
||||||
|
virtual void paint(Screen *screen); |
||||||
|
virtual bool isContainer() { return true; } |
||||||
|
// Returns true if any child components are dirty.
|
||||||
|
virtual bool dirty(); |
||||||
|
// Sets dirty to true for all child components, causing them to be repainted
|
||||||
|
// during the next update.
|
||||||
|
virtual void repaint(); |
||||||
|
#ifdef SCREENUI_DEBUG |
||||||
|
virtual char *description() { return "Container"; } |
||||||
|
#endif |
||||||
|
virtual bool contains(Component *component); |
||||||
|
protected: |
||||||
|
Component *nextFocusHolder(Component *focusHolder, bool reverse); |
||||||
|
Component *nextFocusHolder(Component *focusHolder, bool reverse, bool *focusHolderFound); |
||||||
|
|
||||||
|
Component **components_; |
||||||
|
uint8_t componentsLength_; |
||||||
|
uint8_t componentCount_; |
||||||
|
}; |
||||||
|
|
||||||
|
// The main entry point into the ScreenUi system. A Screen instance represents
|
||||||
|
// a full screen of data on the display, including modifiable Components and
|
||||||
|
// provides methods for input and output.
|
||||||
|
// The user should create instances of Screen to manage a user interface and
|
||||||
|
// add() Components to the screen.
|
||||||
|
// When a Screen is ready to be displayed, the user should call update() in
|
||||||
|
// a loop. After each call to update(), Components can be queried for their
|
||||||
|
// data.
|
||||||
|
class Screen : public Container { |
||||||
|
public: |
||||||
|
Screen(uint8_t width, uint8_t height); |
||||||
|
// Should be called regularly by the main program to update the Screen
|
||||||
|
// and process input. After each call to update(), each Component
|
||||||
|
// will have processed any input it received and will have updated it
|
||||||
|
// data.
|
||||||
|
virtual void update(); |
||||||
|
// Returns the current focus holder for the Screen. The focus holder is
|
||||||
|
// the component that input events will be sent to.
|
||||||
|
Component *focusHolder() { return focusHolder_; } |
||||||
|
// Sets the current focus holder. This can be used to set the default
|
||||||
|
// button on a screen before it is displayed, for instance.
|
||||||
|
void setFocusHolder(Component *focusHolder) { focusHolder_ = focusHolder; } |
||||||
|
void setCursorLocation(uint8_t x, uint8_t y) { cursorX_ = x; cursorY_ = y; } |
||||||
|
|
||||||
|
#ifdef SCREENUI_DEBUG |
||||||
|
virtual char *description() { return "Screen"; } |
||||||
|
#endif |
||||||
|
|
||||||
|
// The following methods must be overridden by the user to provide
|
||||||
|
// hardware support
|
||||||
|
|
||||||
|
// Get any changes in the input since the last call to update();
|
||||||
|
virtual void getInputDeltas(int *x, int *y, bool *selected, bool *cancelled); |
||||||
|
// Clear the screen
|
||||||
|
virtual void clear(); |
||||||
|
virtual void createCustomChar(uint8_t slot, uint8_t *data); |
||||||
|
virtual void draw(uint8_t x, uint8_t y, const char *text); |
||||||
|
virtual void draw(uint8_t x, uint8_t y, uint8_t customChar); |
||||||
|
virtual void setCursorVisible(bool visible); |
||||||
|
virtual void setBlink(bool blink); |
||||||
|
virtual void moveCursor(uint8_t x, uint8_t y); |
||||||
|
|
||||||
|
private: |
||||||
|
bool cleared_; |
||||||
|
Component *focusHolder_; |
||||||
|
bool focusHolderSelected_; |
||||||
|
bool annoyingBugWorkedAround_; |
||||||
|
uint8_t cursorX_, cursorY_; |
||||||
|
}; |
||||||
|
|
||||||
|
// A Component that displays static text at a specific position.
|
||||||
|
class Label : public Component { |
||||||
|
public: |
||||||
|
Label(const char *text); |
||||||
|
virtual void setText(const char *text); |
||||||
|
virtual void paint(Screen *screen); |
||||||
|
#ifdef SCREENUI_DEBUG |
||||||
|
virtual char *description() { return "Label"; } |
||||||
|
#endif |
||||||
|
protected: |
||||||
|
char* text_; |
||||||
|
bool captured_; |
||||||
|
uint8_t dirtyWidth_; |
||||||
|
}; |
||||||
|
|
||||||
|
// A Component that can receive focus and select events. If the Button has
|
||||||
|
// focus when the user presses the select button the Button's pressed() property
|
||||||
|
// is set indicating that the button was selected.
|
||||||
|
// Button is a subclass of Label and thus displays itself as text.
|
||||||
|
class Button : public Label { |
||||||
|
public: |
||||||
|
Button(const char *text); |
||||||
|
virtual bool acceptsFocus() { return true; } |
||||||
|
bool pressed() { return pressed_; } |
||||||
|
virtual void update(Screen *screen); |
||||||
|
virtual bool handleInputEvent(int x, int y, bool selected, bool cancelled); |
||||||
|
#ifdef SCREENUI_DEBUG |
||||||
|
virtual char *description() { return "Button"; } |
||||||
|
#endif |
||||||
|
private: |
||||||
|
bool pressed_; |
||||||
|
}; |
||||||
|
|
||||||
|
// A Component that displays either an on or off state. Clicking the component
|
||||||
|
// while it has focus causes it to change from on to off or vice-versa.
|
||||||
|
class Checkbox : public Label { |
||||||
|
public: |
||||||
|
Checkbox(); |
||||||
|
bool checked() { return checked_; } |
||||||
|
virtual bool acceptsFocus() { return true; } |
||||||
|
virtual bool handleInputEvent(int x, int y, bool selected, bool cancelled); |
||||||
|
#ifdef SCREENUI_DEBUG |
||||||
|
virtual char *description() { return "Checkbox"; } |
||||||
|
#endif |
||||||
|
private: |
||||||
|
bool checked_; |
||||||
|
}; |
||||||
|
|
||||||
|
// A Component that allows the user to scroll through several choices and
|
||||||
|
// select one. When the List is selected, future scroll events will cause it
|
||||||
|
// to scroll through it's selections. A select sets the current item
|
||||||
|
// or a cancel resets the list to it's previously selected item and
|
||||||
|
// releases control.
|
||||||
|
class List : public Label { |
||||||
|
public: |
||||||
|
List(uint8_t maxItems); |
||||||
|
void addItem(const char *item); |
||||||
|
const char *selectedItem() { return items_[selectedIndex_]; } |
||||||
|
uint8_t selectedIndex() { return selectedIndex_; } |
||||||
|
void setSelectedIndex(uint8_t selectedIndex); |
||||||
|
virtual bool acceptsFocus() { return true; } |
||||||
|
virtual bool handleInputEvent(int x, int y, bool selected, bool cancelled); |
||||||
|
#ifdef SCREENUI_DEBUG |
||||||
|
virtual char *description() { return "List"; } |
||||||
|
#endif |
||||||
|
private: |
||||||
|
char **items_; |
||||||
|
uint8_t itemCount_; |
||||||
|
uint8_t selectedIndex_; |
||||||
|
}; |
||||||
|
|
||||||
|
// allows text input. Each character can be clicked to scroll through the alphabet.
|
||||||
|
class Input : public Label { |
||||||
|
public: |
||||||
|
Input(char *text); |
||||||
|
virtual void setText(char *text); |
||||||
|
virtual bool acceptsFocus() { return true; } |
||||||
|
virtual void paint(Screen *screen); |
||||||
|
virtual bool handleInputEvent(int x, int y, bool selected, bool cancelled); |
||||||
|
#ifdef SCREENUI_DEBUG |
||||||
|
virtual char *description() { return "Input"; } |
||||||
|
#endif |
||||||
|
protected: |
||||||
|
int8_t position_; |
||||||
|
bool selecting_; |
||||||
|
}; |
||||||
|
|
||||||
|
// allows input of a floating point number.
|
||||||
|
class DecimalInput : public Input { |
||||||
|
}; |
||||||
|
|
||||||
|
// allows input of an integer number with a certain base.
|
||||||
|
class IntegerInput : public Input { |
||||||
|
}; |
||||||
|
|
||||||
|
// Component that allows the user to enter a time with up to three fields
|
||||||
|
// separated by semi-colons. e.g. 00, 00:00, 00:00:00
|
||||||
|
class TimeInput : public Input { |
||||||
|
}; |
||||||
|
|
||||||
|
// A Container that allows the user to scroll through any number of rows of
|
||||||
|
// Components. The ScrollContainer can be thought of as a Screen with the same
|
||||||
|
// width as the Container it is added to, and an unlimited height.
|
||||||
|
// Components that will be added to the ScrollContainer should have their
|
||||||
|
// location set relative to their position in the ScrollContainer, not
|
||||||
|
// the main Screen.
|
||||||
|
class ScrollContainer : public Container { |
||||||
|
public: |
||||||
|
// We require a reference to Screen because we need focus information
|
||||||
|
// during the dirty() check. This is a bit of a dirty hack, but it's
|
||||||
|
// better than adding this property to every other object or walking
|
||||||
|
// the tree to find it.
|
||||||
|
ScrollContainer(Screen *screen, uint8_t width, uint8_t height); |
||||||
|
~ScrollContainer(); |
||||||
|
virtual void update(Screen *screen); |
||||||
|
virtual void add(Component *component, int8_t x, int8_t y); |
||||||
|
virtual void paint(Screen *screen); |
||||||
|
virtual bool dirty(); |
||||||
|
#ifdef SCREENUI_DEBUG |
||||||
|
virtual char *description() { return "ScrollContainer"; } |
||||||
|
#endif |
||||||
|
private: |
||||||
|
bool scrollNeeded(); |
||||||
|
void offsetChildren(int x, int y); |
||||||
|
|
||||||
|
Component *lastFocusHolder_; |
||||||
|
Screen *screen_; |
||||||
|
char *clearLine; |
||||||
|
bool firstUpdateCompleted_; |
||||||
|
}; |
||||||
|
|
||||||
|
// A specialization of ScrollContainer that contains only Buttons and provides
|
||||||
|
// a simple API for managing the set of Buttons like a menu.
|
||||||
|
class Menu : public ScrollContainer { |
||||||
|
}; |
||||||
|
|
||||||
|
#endif |
Loading…
Reference in new issue