From 838b8ee4222c8995d3bd21632ac70be98477a191 Mon Sep 17 00:00:00 2001 From: Aloshi Date: Sat, 28 Sep 2013 17:35:38 -0500 Subject: [PATCH] DateTimeComponent. Can display dates, date + times, and an english description of a time relative to now ("2 secs ago", "1 day ago", etc.). Supports editing dates (including day-of-month validation). This took a lot longer than I thought. --- CMakeLists.txt | 2 + src/MetaData.cpp | 14 +- src/MetaData.h | 7 +- src/components/DateTimeComponent.cpp | 269 +++++++++++++++++++++++++++ src/components/DateTimeComponent.h | 47 +++++ 5 files changed, 335 insertions(+), 4 deletions(-) create mode 100644 src/components/DateTimeComponent.cpp create mode 100644 src/components/DateTimeComponent.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 89a1ea6d2..0d9d6b8c7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -160,6 +160,7 @@ set(ES_HEADERS ${CMAKE_CURRENT_SOURCE_DIR}/src/components/AsyncReqComponent.h ${CMAKE_CURRENT_SOURCE_DIR}/src/components/ButtonComponent.h ${CMAKE_CURRENT_SOURCE_DIR}/src/components/ComponentListComponent.h + ${CMAKE_CURRENT_SOURCE_DIR}/src/components/DateTimeComponent.h ${CMAKE_CURRENT_SOURCE_DIR}/src/components/ImageComponent.h ${CMAKE_CURRENT_SOURCE_DIR}/src/components/NinePatchComponent.h ${CMAKE_CURRENT_SOURCE_DIR}/src/components/RatingComponent.h @@ -214,6 +215,7 @@ set(ES_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/src/components/AsyncReqComponent.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/components/ButtonComponent.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/components/ComponentListComponent.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/components/DateTimeComponent.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/components/ImageComponent.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/components/NinePatchComponent.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/components/RatingComponent.cpp diff --git a/src/MetaData.cpp b/src/MetaData.cpp index be717e96d..5879888d2 100644 --- a/src/MetaData.cpp +++ b/src/MetaData.cpp @@ -4,6 +4,7 @@ #include "components/TextEditComponent.h" #include "components/RatingComponent.h" +#include "components/DateTimeComponent.h" MetaDataList::MetaDataList() { @@ -23,7 +24,7 @@ std::vector MetaDataList::getDefaultGameMDD() {"image", MD_IMAGE_PATH, "", false}, {"thumbnail", MD_IMAGE_PATH, "", false}, {"rating", MD_RATING, "0", false}, - {"releasedate", MD_TIME, "0", false}, + {"releasedate", MD_DATE, "0", false}, {"playcount", MD_INT, "0", true}, {"lastplayed", MD_TIME, "0", true} }; @@ -131,6 +132,17 @@ GuiComponent* MetaDataList::makeEditor(Window* window, MetaDataType as) comp->setSize(comp->getSize().x(), comp->getSize().y() * 3); return comp; } + case MD_DATE: + { + DateTimeComponent* comp = new DateTimeComponent(window); + return comp; + } + case MD_TIME: + { + DateTimeComponent* comp = new DateTimeComponent(window); + comp->setDisplayMode(DateTimeComponent::DISP_RELATIVE_TO_NOW); + return comp; + } default: { TextEditComponent* comp = new TextEditComponent(window); diff --git a/src/MetaData.h b/src/MetaData.h index 59b5da145..f83d2aeed 100644 --- a/src/MetaData.h +++ b/src/MetaData.h @@ -17,7 +17,8 @@ enum MetaDataType MD_MULTILINE_STRING, MD_IMAGE_PATH, MD_RATING, - MD_TIME + MD_DATE, + MD_TIME //used for lastplayed }; struct MetaDataDecl @@ -28,7 +29,7 @@ struct MetaDataDecl bool isStatistic; //if true, ignore scraper values for this metadata }; -boost::posix_time::ptime string_to_ptime(const std::string& str, const std::string& fmt); +boost::posix_time::ptime string_to_ptime(const std::string& str, const std::string& fmt = "%Y%m%dT%H%M%S%F%q"); class MetaDataList { @@ -41,7 +42,7 @@ public: MetaDataList(const std::vector& mdd); void set(const std::string& key, const std::string& value); - void setTime(const std::string& key, const boost::posix_time::ptime& time); + void setTime(const std::string& key, const boost::posix_time::ptime& time); //times are internally stored as ISO strings (e.g. boost::posix_time::to_iso_string(ptime)) const std::string& get(const std::string& key) const; int getInt(const std::string& key) const; diff --git a/src/components/DateTimeComponent.cpp b/src/components/DateTimeComponent.cpp new file mode 100644 index 000000000..cd6266f12 --- /dev/null +++ b/src/components/DateTimeComponent.cpp @@ -0,0 +1,269 @@ +#include "DateTimeComponent.h" +#include "../MetaData.h" +#include "../Renderer.h" +#include "../Window.h" +#include "../Log.h" + +DateTimeComponent::DateTimeComponent(Window* window) : GuiComponent(window), + mEditing(false), mEditIndex(0), mDisplayMode(DISP_DATE), mRelativeUpdateAccumulator(0) +{ + mSize << 64, (float)getFont()->getHeight(); + updateTextCache(); +} + +void DateTimeComponent::setDisplayMode(DisplayMode mode) +{ + mDisplayMode = mode; +} + +bool DateTimeComponent::input(InputConfig* config, Input input) +{ + if(input.value == 0) + return false; + + if(config->isMappedTo("a", input)) + { + if(mDisplayMode != DISP_RELATIVE_TO_NOW) //don't allow editing for relative times + mEditing = !mEditing; + + if(mEditing) + { + //started editing + mTimeBeforeEdit = mTime; + + //initialize to now if unset + if(mTime == boost::posix_time::not_a_date_time) + { + mTime = boost::posix_time::ptime(boost::gregorian::day_clock::local_day()); + updateTextCache(); + } + } + + return true; + } + + if(mEditing) + { + if(config->isMappedTo("b", input)) + { + mEditing = false; + mTime = mTimeBeforeEdit; + updateTextCache(); + } + + int incDir = 0; + if(config->isMappedTo("up", input)) + incDir = 1; + else if(config->isMappedTo("down", input)) + incDir = -1; + + if(incDir != 0) + { + tm new_tm = boost::posix_time::to_tm(mTime); + + if(mEditIndex == 0) + { + new_tm.tm_mon += incDir; + + if(new_tm.tm_mon > 11) + new_tm.tm_mon = 11; + else if(new_tm.tm_mon < 0) + new_tm.tm_mon = 0; + + }else if(mEditIndex == 1) + { + new_tm.tm_mday += incDir; + int days_in_month = mTime.date().end_of_month().day().as_number(); + if(new_tm.tm_mday > days_in_month) + new_tm.tm_mday = days_in_month; + else if(new_tm.tm_mday < 1) + new_tm.tm_mday = 1; + + }else if(mEditIndex == 2) + { + new_tm.tm_year += incDir; + if(new_tm.tm_year < 0) + new_tm.tm_year = 0; + } + + //validate day + int days_in_month = boost::gregorian::date(new_tm.tm_year + 1900, new_tm.tm_mon + 1, 1).end_of_month().day().as_number(); + if(new_tm.tm_mday > days_in_month) + new_tm.tm_mday = days_in_month; + + mTime = boost::posix_time::ptime_from_tm(new_tm); + + updateTextCache(); + return true; + } + + if(config->isMappedTo("right", input)) + { + mEditIndex++; + if(mEditIndex >= (int)mCursorBoxes.size()) + mEditIndex--; + return true; + } + + if(config->isMappedTo("left", input)) + { + mEditIndex--; + if(mEditIndex < 0) + mEditIndex++; + return true; + } + } + + return GuiComponent::input(config, input); +} + +void DateTimeComponent::update(int deltaTime) +{ + if(mDisplayMode == DISP_RELATIVE_TO_NOW) + { + mRelativeUpdateAccumulator += deltaTime; + if(mRelativeUpdateAccumulator > 1000) + { + mRelativeUpdateAccumulator = 0; + updateTextCache(); + } + } + + GuiComponent::update(deltaTime); +} + +void DateTimeComponent::render(const Eigen::Affine3f& parentTrans) +{ + Eigen::Affine3f trans = parentTrans * getTransform(); + Renderer::setMatrix(trans); + + if(mTextCache) + { + std::shared_ptr font = getFont(); + font->renderTextCache(mTextCache.get()); + + if(mEditing) + { + if(mEditIndex >= 0 && (unsigned int)mEditIndex < mCursorBoxes.size()) + { + Renderer::drawRect((int)mCursorBoxes[mEditIndex][0], (int)mCursorBoxes[mEditIndex][1], + (int)mCursorBoxes[mEditIndex][2], (int)mCursorBoxes[mEditIndex][3], 0x00000022); + } + } + } + + renderChildren(trans); +} + +void DateTimeComponent::setValue(const std::string& val) +{ + mTime = string_to_ptime(val); + updateTextCache(); +} + +std::string DateTimeComponent::getValue() const +{ + return boost::posix_time::to_iso_string(mTime); +} + +DateTimeComponent::DisplayMode DateTimeComponent::getCurrentDisplayMode() const +{ + /*if(mEditing) + { + if(mDisplayMode == DISP_RELATIVE_TO_NOW) + { + //TODO: if time component == 00:00:00, return DISP_DATE, else return DISP_DATE_TIME + return DISP_DATE; + } + }*/ + + return mDisplayMode; +} + +std::string DateTimeComponent::getDisplayString(DisplayMode mode) const +{ + std::string fmt; + switch(mode) + { + case DISP_DATE: + fmt = "%m/%d/%Y"; + break; + case DISP_DATE_TIME: + fmt = "%m/%d/%Y %H:%M:%S"; + break; + case DISP_RELATIVE_TO_NOW: + { + //relative time + using namespace boost::posix_time; + + if(mTime == not_a_date_time) + return "never"; + + ptime now = second_clock::universal_time(); + time_duration dur = now - mTime; + + if(dur < seconds(2)) + return "just now"; + if(dur < seconds(60)) + return std::to_string((long long)dur.seconds()) + " secs ago"; + if(dur < minutes(60)) + return std::to_string((long long)dur.minutes()) + " min" + (dur < minutes(2) ? "" : "s") + " ago"; + if(dur < hours(24)) + return std::to_string((long long)dur.hours()) + " hour" + (dur < hours(2) ? "" : "s") + " ago"; + + return std::to_string((long long)(ptime() + dur).date().day_count().as_number()); + } + break; + } + + if(mTime == boost::posix_time::not_a_date_time) + return "unknown"; + + boost::posix_time::time_facet* facet = new boost::posix_time::time_facet(); + facet->format(fmt.c_str()); + std::locale loc(std::locale::classic(), facet); + + std::stringstream ss; + ss.imbue(loc); + ss << mTime; + return ss.str(); +} + +std::shared_ptr DateTimeComponent::getFont() const +{ + return Font::get(*mWindow->getResourceManager(), Font::getDefaultPath(), FONT_SIZE_MEDIUM); +} + +void DateTimeComponent::updateTextCache() +{ + DisplayMode mode = getCurrentDisplayMode(); + const std::string dispString = getDisplayString(mode); + std::shared_ptr font = getFont(); + mTextCache = std::unique_ptr(font->buildTextCache(dispString, 0, 0, 0x000000FF)); + + //set up cursor positions + mCursorBoxes.clear(); + + if(dispString.empty() || mode == DISP_RELATIVE_TO_NOW) + return; + + //month + Eigen::Vector2f start(0, 0); + Eigen::Vector2f end = font->sizeText(dispString.substr(0, 2)); + Eigen::Vector2f diff = end - start; + mCursorBoxes.push_back(Eigen::Vector4f(start[0], start[1], diff[0], diff[1])); + + //day + start[0] = font->sizeText(dispString.substr(0, 3)).x(); + end = font->sizeText(dispString.substr(0, 5)); + diff = end - start; + mCursorBoxes.push_back(Eigen::Vector4f(start[0], start[1], diff[0], diff[1])); + + //year + start[0] = font->sizeText(dispString.substr(0, 6)).x(); + end = font->sizeText(dispString.substr(0, 10)); + diff = end - start; + mCursorBoxes.push_back(Eigen::Vector4f(start[0], start[1], diff[0], diff[1])); + + //if mode == DISP_DATE_TIME do times too but I don't wanna do the logic for editing times because no one will ever use it so screw it +} diff --git a/src/components/DateTimeComponent.h b/src/components/DateTimeComponent.h new file mode 100644 index 000000000..1f5359978 --- /dev/null +++ b/src/components/DateTimeComponent.h @@ -0,0 +1,47 @@ +#pragma once + +#include "../GuiComponent.h" +#include +#include "../Font.h" + +class DateTimeComponent : public GuiComponent +{ +public: + DateTimeComponent(Window* window); + + void setValue(const std::string& val) override; + std::string getValue() const override; + + bool input(InputConfig* config, Input input) override; + void update(int deltaTime) override; + void render(const Eigen::Affine3f& parentTrans) override; + + enum DisplayMode + { + DISP_DATE, + DISP_DATE_TIME, + DISP_RELATIVE_TO_NOW + }; + + void setDisplayMode(DisplayMode mode); + +private: + std::shared_ptr getFont() const; + + std::string getDisplayString(DisplayMode mode) const; + DisplayMode getCurrentDisplayMode() const; + + void updateTextCache(); + + boost::posix_time::ptime mTime; + boost::posix_time::ptime mTimeBeforeEdit; + + bool mEditing; + int mEditIndex; + DisplayMode mDisplayMode; + + int mRelativeUpdateAccumulator; + + std::unique_ptr mTextCache; + std::vector mCursorBoxes; +};