diff --git a/.gitignore b/.gitignore index 26c155113..00460fa2d 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ cmake-build-*/ /*.vscode radio/src/tests/googletest* radio/src/tests/gtest*.tar.gz +.editorconfig diff --git a/radio/src/bitmaps/480x272/default_theme/mask_radio_edit_theme.png b/radio/src/bitmaps/480x272/default_theme/mask_radio_edit_theme.png new file mode 100644 index 000000000..787a2b8d3 Binary files /dev/null and b/radio/src/bitmaps/480x272/default_theme/mask_radio_edit_theme.png differ diff --git a/radio/src/gui/colorlcd/CMakeLists.txt b/radio/src/gui/colorlcd/CMakeLists.txt index b1cd3261f..96fd3ed5e 100644 --- a/radio/src/gui/colorlcd/CMakeLists.txt +++ b/radio/src/gui/colorlcd/CMakeLists.txt @@ -20,6 +20,12 @@ set(GUI_SRC bitmaps.cpp theme.cpp theme_manager.cpp + color_editor_popup.cpp + color_editor.cpp + color_list.cpp + preview_window.cpp + file_carosell.cpp + listbox.cpp tabsgroup.cpp page.cpp fab_button.cpp @@ -56,6 +62,7 @@ set(GUI_SRC radio_hardware.cpp radio_diagkeys.cpp radio_diaganas.cpp + radio_theme.cpp radio_calibration.cpp view_about.cpp view_main.cpp diff --git a/radio/src/gui/colorlcd/color_editor.cpp b/radio/src/gui/colorlcd/color_editor.cpp new file mode 100644 index 000000000..70fa56ea3 --- /dev/null +++ b/radio/src/gui/colorlcd/color_editor.cpp @@ -0,0 +1,318 @@ +/* + * Copyright (C) EdgeTX + * + * Based on code named + * opentx - https://github.com/opentx/opentx + * th9x - http://code.google.com/p/th9x + * er9x - http://code.google.com/p/er9x + * gruvin9x - http://code.google.com/p/gruvin9x + * + * License GPLv2: http://www.gnu.org/licenses/gpl-2.0.html + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + */ +#include "color_editor.h" + +constexpr int BAR_MARGIN = 5; + +constexpr int BAR_TOP_MARGIN = 5; +constexpr int BAR_HEIGHT_OFFSET = BAR_TOP_MARGIN + 25; + +const char *RGBChars[MAX_BARS] = { "R", "G", "B" }; +const char *HSVChars[MAX_BARS] = { "H", "S", "V" }; + +// ColorTypes() +// A ColorType implements a three bar color editor. Currently we support HSV and RGB +enum HSV_BAR_TYPE +{ + HUE = 0, + SATURATION = 1, + BRIGHTNESS = 2 +}; + + +HSVColorType::HSVColorType(FormGroup *window, uint32_t color) : + ColorType(window->height() - BAR_HEIGHT_OFFSET) +{ + auto r = GET_RED(color), g = GET_GREEN(color), b = GET_BLUE(color); + float values[MAX_BARS]; + RGBtoHSV(r, g, b, values[0], values[1], values[2]); + values[1] *= MAX_SATURATION; // convert the proper base + values[2] *= MAX_BRIGHTNESS; + + auto spacePerBar = (window->width() / MAX_BARS); + int leftPos = 0; + + for (auto i = 0; i < MAX_BARS; i++) { + barInfo[i].leftPos = leftPos + (i != 0 ? BAR_MARGIN : 0); + barInfo[i].barWidth = spacePerBar - BAR_MARGIN - 5; + barInfo[i].maxValue = i == 0 ? MAX_HUE : MAX_BRIGHTNESS; + barInfo[i].invert = i != 0; + barInfo[i].sliding = false; + barInfo[i].value = values[i]; + + leftPos += spacePerBar; + } +} + +uint32_t HSVColorType::getRGB() +{ + return HSVtoRGB(barInfo[0].value, barInfo[1].value, barInfo[2].value); +} + +void HSVColorType::drawBarValue(BitmapBuffer *dc, int bar) +{ + dc->drawText(barInfo[bar].leftPos, BAR_TOP_MARGIN + screenHeight + 9, HSVChars[bar], COLOR_THEME_PRIMARY1 | FONT(XXS)); + dc->drawText(barInfo[bar].leftPos + 10, BAR_TOP_MARGIN + screenHeight + 3, std::to_string(barInfo[bar].value).c_str(), COLOR_THEME_PRIMARY1); +} + +void HSVColorType::drawBar(BitmapBuffer* dc, int bar, getRGBFromPos getRGB) +{ + int maxRange = screenHeight; + for (auto i = 0; i < maxRange; i++) { + auto rgb = getRGB(bar, i); + auto r = GET_RED(rgb); + auto g = GET_GREEN(rgb); + auto b = GET_BLUE(rgb); + + dc->drawSolidHorizontalLine(barInfo[bar].leftPos, i + BAR_TOP_MARGIN, barInfo[bar].barWidth, COLOR2FLAGS(RGB(r, g, b))); + } + + dc->drawSolidRect(barInfo[bar].leftPos, BAR_TOP_MARGIN, barInfo[bar].barWidth, screenHeight, 1, COLOR2FLAGS(BLACK)); + dc->drawFilledCircle(barInfo[bar].leftPos + (barInfo[bar].barWidth / 2), BAR_TOP_MARGIN + valueToScreen(bar, barInfo[bar].value), 5, COLOR2FLAGS(WHITE));\ + + drawBarValue(dc, bar); +} + +void HSVColorType::paint(BitmapBuffer *dc) +{ + drawBar(dc, HUE, [=] (int bar, int pos) { + int hue = screenToValue(0, pos); + auto rgb = HSVtoRGB(hue, barInfo[1].value, barInfo[2].value); + return rgb; + }); + drawBar(dc, SATURATION, [=] (int bar, int pos) { + int saturation = screenToValue(1, pos); + auto rgb = HSVtoRGB(barInfo[0].value, saturation, barInfo[2].value); + return rgb; + }); + drawBar(dc, BRIGHTNESS, [=] (int bar, int pos) { + int brightness = screenToValue(2, pos); + auto rgb = HSVtoRGB(barInfo[0].value, barInfo[1].value, brightness); + return rgb; + }); +} + +RGBColorType::RGBColorType(FormGroup *window, uint32_t color) : + ColorType(window->height() - BAR_HEIGHT_OFFSET) +{ + screenHeight = screenHeight; + auto r = GET_RED(color), g = GET_GREEN(color), b = GET_BLUE(color); + float values[MAX_BARS]; + values[0] = r; values[1] = g; values[2] = b; + + auto spacePerBar = (window->width() / MAX_BARS); + int leftPos = 0; + for (auto i = 0; i < MAX_BARS; i++) { + barInfo[i].leftPos = leftPos + (i != 0 ? BAR_MARGIN : 0); + barInfo[i].barWidth = spacePerBar - BAR_MARGIN - 5; + barInfo[i].maxValue = 255; + barInfo[i].sliding = false; + barInfo[i].value = values[i]; + barInfo[i].invert = true; + leftPos += spacePerBar; + } +} + +uint32_t RGBColorType::getRGB() +{ + return RGB(barInfo[0].value, barInfo[1].value, barInfo[2].value); +} + +void RGBColorType::drawBarValue(BitmapBuffer *dc, int bar) +{ + dc->drawText(barInfo[bar].leftPos, BAR_TOP_MARGIN + screenHeight + 9, RGBChars[bar], COLOR_THEME_PRIMARY1 | FONT(XXS)); + dc->drawText(barInfo[bar].leftPos + 10, BAR_TOP_MARGIN + screenHeight + 3, std::to_string(barInfo[bar].value).c_str(), COLOR_THEME_PRIMARY1); +} + +void RGBColorType::paint(BitmapBuffer *dc) +{ + for (int bar = 0; bar < MAX_BARS; bar++) { + for (uint32_t pos = 0; pos < screenHeight; pos++) { + uint32_t value = screenToValue(bar, pos); + uint32_t color; + if (bar == 0) + color = RGB(value, 0, 0); + else if (bar == 1) + color = RGB(0, value, 0); + else + color = RGB(0, 0, value); + + dc->drawSolidHorizontalLine(barInfo[bar].leftPos, pos + BAR_TOP_MARGIN, barInfo[bar].barWidth, + COLOR2FLAGS(color)); + dc->drawSolidRect(barInfo[bar].leftPos, BAR_TOP_MARGIN, barInfo[bar].barWidth, screenHeight, 1, + COLOR2FLAGS(BLACK)); + } + + dc->drawFilledCircle(barInfo[bar].leftPos + (barInfo[bar].barWidth / 2), BAR_TOP_MARGIN + valueToScreen(bar, barInfo[bar].value), 5, + COLOR2FLAGS(WHITE)); + + drawBarValue(dc, bar); + } +} + +///////////////////////////////////////////////////////////////////////// +////// ColorEditor Base class +///////////////////////////////////////////////////////////////////////// +ColorEditor::ColorEditor(FormGroup *window, const rect_t rect, uint32_t color, + std::function setValue) : + FormGroup(window, rect), + _setValue(std::move(setValue)), + _color(color) +{ + _colorType = new HSVColorType(this, color); + setFocusHandler([=](bool focus) { + _focused = focus; + }); +} + +void ColorEditor::setNextFocusBar() +{ + _focusBar = (_focusBar + 1) % MAX_BARS; + setFocus(); + invalidate(); +} + +void ColorEditor::setColorEditorType(COLOR_EDITOR_TYPE colorType) +{ + if (_colorType != nullptr) { + delete _colorType; + } + if (colorType == RGB_COLOR_EDITOR) + _colorType = new RGBColorType(this, _color); + else + _colorType = new HSVColorType(this, _color); + invalidate(); +} + +void ColorEditor::setRGB() +{ + _color = _colorType->getRGB(); + invalidate(); + if (_setValue != nullptr) + _setValue(_color); +} + +#if defined(HARDWARE_TOUCH) +bool ColorEditor::onTouchSlide(coord_t x, coord_t y, coord_t startX, coord_t startY, coord_t slideX, coord_t slideY) +{ + if (touchState.event == TE_SLIDE_END) { + onTouchEnd(0,0); + return true; + } + + for (auto i = 0; i < MAX_BARS; i++) { + if (_colorType->barInfo[i].sliding) { + y -= BAR_TOP_MARGIN; + auto value = _colorType->screenToValue(i, y); + if (value != _colorType->barInfo[i].value) { + slidingWindow = this; + _colorType->barInfo[i].value = value; + setRGB(); + } + } + } + + return true; +} + +bool ColorEditor::onTouchEnd(coord_t x, coord_t y) +{ + bool bSliding = false; + for (auto i = 0; i < MAX_BARS; i++) { + if (_colorType->barInfo[i].sliding) { + bSliding = true; + } + _colorType->barInfo[i].sliding = false; + } + + if (bSliding) { + invalidate(); + } + + return FormGroup::onTouchEnd(x,y); +} + +bool ColorEditor::onTouchStart(coord_t x, coord_t y) +{ + bool bFound = false; + for (auto i = 0; i < MAX_BARS; i++) { + if (y >= BAR_TOP_MARGIN && y < (coord_t)(BAR_TOP_MARGIN + _colorType->screenHeight) && + x >= _colorType->barInfo[i].leftPos && x < _colorType->barInfo[i].leftPos + _colorType->barInfo[i].barWidth) { + _colorType->barInfo[i].sliding = true; + y -= BAR_TOP_MARGIN; + int value = _colorType->screenToValue(i, y); + _colorType->barInfo[i].value = value; + bFound = true; + _focusBar = i; + setFocus(); + } else { + _colorType->barInfo[i].sliding = false; + } + } + if (bFound) { + setRGB(); + return true; + } else { + return FormGroup::onTouchStart(x,y); + } +} +#endif + +#if defined(HARDWARE_KEYS) +void ColorEditor::onEvent(event_t event) +{ + switch(event) + { + case EVT_ROTARY_RIGHT: + case EVT_ROTARY_LEFT: + { + int direction = event == EVT_ROTARY_RIGHT ? 1 : -1; + auto bar = &_colorType->barInfo[_focusBar]; + int newValue = (int) bar->value; + newValue += direction; + + newValue = min(newValue, (int)bar->maxValue); + newValue = max(newValue, 0); + bar->value = newValue; + setRGB(); + onKeyPress(); + } + break; + default: + FormGroup::onEvent(event); + break; + } +} +#endif + +void ColorEditor::drawFocusBox(BitmapBuffer *dc) +{ + auto bar = _colorType->barInfo[_focusBar]; + dc->drawSolidRect(bar.leftPos, BAR_TOP_MARGIN, bar.barWidth, _colorType->screenHeight, 2, COLOR_THEME_FOCUS); +} + +void ColorEditor::paint(BitmapBuffer *dc) +{ + dc->clear(COLOR_THEME_SECONDARY3); + _colorType->paint(dc); + drawFocusBox(dc); +} + diff --git a/radio/src/gui/colorlcd/color_editor.h b/radio/src/gui/colorlcd/color_editor.h new file mode 100644 index 000000000..1cbb526be --- /dev/null +++ b/radio/src/gui/colorlcd/color_editor.h @@ -0,0 +1,147 @@ +/* + * Copyright (C) EdgeTX + * + * Based on code named + * opentx - https://github.com/opentx/opentx + * th9x - http://code.google.com/p/th9x + * er9x - http://code.google.com/p/er9x + * gruvin9x - http://code.google.com/p/gruvin9x + * + * License GPLv2: http://www.gnu.org/licenses/gpl-2.0.html + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + */ +#pragma once +#include +#include +#include +#include "bitmapbuffer.h" +#include "libopenui.h" +#include "theme_manager.h" +#include "listbox.h" + +constexpr int MAX_BARS = 3; +typedef std::function getRGBFromPos; + +class Bar +{ + public: + bool sliding = false; + int leftPos = 0; + int barWidth = 0; + uint32_t maxValue = 0; + uint32_t value = 0; + bool invert = false; +}; + +class ColorType +{ + public: + ColorType(coord_t screenHeight) : + screenHeight(screenHeight) + { + } + virtual ~ColorType() + { + }; + + virtual int valueToScreen(int bar, int value) + { + int scaledValue = (((float)value / barInfo[bar].maxValue) * screenHeight); + if (barInfo[bar].invert) + scaledValue = screenHeight - scaledValue; + + return scaledValue; + } + virtual uint32_t screenToValue(int bar, int pos) + { + // range check + pos = min(pos, screenHeight); + pos = max(pos, 0); + + uint32_t scaledValue = (((float)pos / screenHeight) * barInfo[bar].maxValue); + if (barInfo[bar].invert) + scaledValue = barInfo[bar].maxValue - scaledValue; + return scaledValue; + } + + Bar barInfo[MAX_BARS]; + virtual void paint(BitmapBuffer *dc) {} + virtual uint32_t getRGB() {return 0;} + uint32_t screenHeight; +}; + +class HSVColorType : public ColorType +{ + public: + HSVColorType(FormGroup *window, uint32_t color); + uint32_t getRGB() override; + void paint(BitmapBuffer* dc) override; + + protected: + void drawBar(BitmapBuffer *dc, int bar, getRGBFromPos getRGB); + void drawBarValue(BitmapBuffer *dc, int bar); +}; + +class RGBColorType : public ColorType +{ + public: + RGBColorType(FormGroup *window, uint32_t color); + uint32_t getRGB() override; + void paint(BitmapBuffer* dc) override; + void drawBarValue(BitmapBuffer *dc, int bar); +}; + +enum COLOR_EDITOR_TYPE +{ + RGB_COLOR_EDITOR, + HSV_COLOR_EDITOR +}; + +// the ColorEditor() control is a group of other controls +class ColorEditor : public FormGroup +{ + public: + ColorEditor(FormGroup *window, const rect_t rect, uint32_t color, std::function setValue = nullptr); + +#if defined(DEBUG_WINDOWS) + std::string getName() const override + { + return "ColorEditor"; + } +#endif + + void setColorEditorType(COLOR_EDITOR_TYPE colorType); + +#if defined(HARDWARE_TOUCH) + bool onTouchSlide(coord_t x, coord_t y, coord_t startX, coord_t startY, coord_t slideX, coord_t slideY) override; + bool onTouchEnd(coord_t x, coord_t y) override; + bool onTouchStart(coord_t x, coord_t y) override; +#endif + +void setNextFocusBar(); + +#if defined(HARDWARE_KEYS) + void onEvent(event_t event) override; +#endif + + void paint(BitmapBuffer *dc) override; + + protected: + ColorType *_colorType = nullptr; + void drawColorBox(BitmapBuffer *dc); + void drawFocusBox(BitmapBuffer *dc); + void setRGB(); + std::function _setValue; + uint32_t _color; + int _focusBar = 0; + bool _focused = false; +}; + diff --git a/radio/src/gui/colorlcd/color_editor_popup.cpp b/radio/src/gui/colorlcd/color_editor_popup.cpp new file mode 100644 index 000000000..19064034e --- /dev/null +++ b/radio/src/gui/colorlcd/color_editor_popup.cpp @@ -0,0 +1,264 @@ +/* + * Copyright (C) EdgeTX + * + * Based on code named + * opentx - https://github.com/opentx/opentx + * th9x - http://code.google.com/p/th9x + * er9x - http://code.google.com/p/er9x + * gruvin9x - http://code.google.com/p/gruvin9x + * + * License GPLv2: http://www.gnu.org/licenses/gpl-2.0.html + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + */ +#include "color_editor_popup.h" + +#define SET_DIRTY() setDirty() + +//////////////////////////////////////////////////////////////////////////////////////////////// +///// ColorEditorContent +//////////////////////////////////////////////////////////////////////////////////////////////// +constexpr int GRID_TOP_MARGIN = 60; + +constexpr int HUE_BAR_TOP_MARGIN = 35; +constexpr int HUE_BAR_HEIGHT = 20; + +constexpr int COLOR_BOX_TOP = 130; +constexpr int COLOR_BOX_WIDTH = 90; +constexpr int COLOR_BOX_HEIGHT = 32; + +#if (LCD_W > LCD_H) +constexpr int COLOR_BOX_LEFT = 240; +constexpr int SCREEN_LEFT_MARGIN = 10; +constexpr rect_t contentRect = { 50, 50, 360 + 20, 170 }; +#else +constexpr int COLOR_BOX_LEFT = 210; +constexpr rect_t contentRect = { 5, 50, LCD_W - 10, 170 }; +constexpr int SCREEN_LEFT_MARGIN = 0; +#endif + +ColorEditorContent::ColorEditorContent(ModalWindow *window, const rect_t rect, uint32_t color, std::function setValue) : + ModalWindowContent(window, rect), + setValue(std::move(setValue)) +{ + r = GET_RED(color); g = GET_GREEN(color); b = GET_BLUE(color); + float h, s, v; + RGBtoHSV(r, g, b, h, s, v); + hue = (int) h; + this->s = 100 * s; + this->v = 100 * v; + + rect_t rText = {COLOR_BOX_LEFT, 60, 50, 20}; + rect_t rNumber = {COLOR_BOX_LEFT + 60, 60, 40, 20}; + + new StaticText(this, rText, STR_RED, 0, COLOR_THEME_PRIMARY1); + rText.y += 23; + rEdit = new NumberEdit(this, rNumber, 0, 255, [=] () { return r; }, [=] (int value) { r = value; }); + rNumber.y += 23; + new StaticText(this, rText, STR_GREEN, 0, COLOR_THEME_PRIMARY1); + rText.y += 23; + gEdit = new NumberEdit(this, rNumber, 0, 255, [=] () { return g; }, [=] (int value) { g = value; }); + rNumber.y += 23; + new StaticText(this, rText, STR_BLUE, 0, COLOR_THEME_PRIMARY1); + bEdit = new NumberEdit(this, rNumber, 0, 255, [=] () { return b; }, [=] (int value) { b = value; }); + + invalidate(); +} + +void ColorEditorContent::setRGB() +{ + int rgb = HSVtoRGB(hue, s, v); + r = GET_RED(rgb); + g = GET_GREEN(rgb); + b = GET_BLUE(rgb); + + rEdit->setValue(r); + gEdit->setValue(g); + bEdit->setValue(b); + invalidate(); + + if (setValue != nullptr) + setValue(rgb); +} + +#if defined(HARDWARE_TOUCH) +bool ColorEditorContent::onTouchSlide(coord_t x, coord_t y, coord_t startX, coord_t startY, coord_t slideX, coord_t slideY) +{ + if (touchState.event == TE_SLIDE_END) { + sliding = false; + colorPicking = false; + slidingWindow = nullptr; + invalidate(); + } else if (sliding) { + x -= SCREEN_LEFT_MARGIN; + x = max(x, 0); + x = min(x, MAX_HUE); + if (hue != x) { + hue = x; + slidingWindow = this; // KLK: (Hack IMHO) so we get the end slide message + setRGB(); + } + } else if (colorPicking) { + s = min((x - SCREEN_LEFT_MARGIN) / 2, MAX_SATURATION); + s = max(s, 0); + v = min(MAX_BRIGHTNESS - ((y - GRID_TOP_MARGIN)), MAX_BRIGHTNESS); + v = max(v, 0); + + setRGB(); + } + + return true; +} + +bool ColorEditorContent::onTouchEnd(coord_t x, coord_t y) +{ + sliding = false; + colorPicking = false; + invalidate(); + return true; +} + +bool ColorEditorContent::onTouchStart(coord_t x, coord_t y) +{ + if (! sliding && (y > HUE_BAR_TOP_MARGIN && y < HUE_BAR_TOP_MARGIN + HUE_BAR_HEIGHT)) + { + hue = x - SCREEN_LEFT_MARGIN; + hue = max(hue, 0); + hue = min(hue, MAX_HUE); + sliding = true; + + invalidate(); + } else if (x >= SCREEN_LEFT_MARGIN && x <= SCREEN_LEFT_MARGIN + (MAX_SATURATION * 2) && y >= GRID_TOP_MARGIN && y <= GRID_TOP_MARGIN + MAX_BRIGHTNESS) { + colorPicking = true; + v = min(MAX_BRIGHTNESS - (y - GRID_TOP_MARGIN), MAX_BRIGHTNESS); + s = min((x - SCREEN_LEFT_MARGIN) / 2, MAX_SATURATION); + + setRGB(); + } + return true; +} +#endif + +#if defined(HARDWARE_KEYS) +void ColorEditorContent::onEvent(event_t event) +{ + int oldSliderVal = hue; + if (event == EVT_ROTARY_LEFT) { + hue -= 2; + killAllEvents(); + } + else if (event == EVT_ROTARY_RIGHT) { + hue += 2; + killAllEvents(); + } + + hue = max(hue, 0); + hue = min(hue, MAX_HUE); + if (hue != oldSliderVal) { + invalidate(); + } +} +#endif + + +void ColorEditorContent::drawHueBar(BitmapBuffer *dc) +{ + for (int i = 0; i < MAX_HUE; i++) { + auto rgb = HSVtoRGB(i, MAX_SATURATION, MAX_BRIGHTNESS); + auto r = GET_RED(rgb); + auto g = GET_GREEN(rgb); + auto b = GET_BLUE(rgb); + dc->drawSolidVerticalLine(i + SCREEN_LEFT_MARGIN, HUE_BAR_TOP_MARGIN, HUE_BAR_HEIGHT, COLOR2FLAGS(RGB(r, g, b))); + } + + dc->drawFilledCircle(hue + SCREEN_LEFT_MARGIN, HUE_BAR_TOP_MARGIN + (HUE_BAR_HEIGHT / 2), 5, COLOR2FLAGS(WHITE)); + if (sliding) { + dc->drawText(hue + (SCREEN_LEFT_MARGIN / 2), 25, std::to_string(hue).c_str(), FONT(XXS) | COLOR2FLAGS(WHITE)); + } +} + +void ColorEditorContent::drawGrid(BitmapBuffer *dc) +{ + for (int s = 0; s <= MAX_SATURATION; s++) { + for (int v = MAX_BRIGHTNESS; v >= 0; v--) { + uint32_t rgbVal = HSVtoRGB(hue, s, v); + dc->drawPixel((s * 2) + SCREEN_LEFT_MARGIN , ((MAX_BRIGHTNESS - v)) + GRID_TOP_MARGIN, rgbVal); + dc->drawPixel((s * 2) + SCREEN_LEFT_MARGIN + 1, ((MAX_BRIGHTNESS - v)) + GRID_TOP_MARGIN, rgbVal); + } + } + + dc->drawFilledCircle((this->s * 2) + SCREEN_LEFT_MARGIN, (MAX_BRIGHTNESS - this->v) + GRID_TOP_MARGIN, 3, COLOR2FLAGS(WHITE)); +} + +void ColorEditorContent::drawColorBox(BitmapBuffer *dc) +{ + int32_t rgb = RGB(r,g,b); + + dc->drawSolidFilledRect(COLOR_BOX_LEFT, COLOR_BOX_TOP, COLOR_BOX_WIDTH, COLOR_BOX_HEIGHT, COLOR2FLAGS(rgb)); + dc->drawSolidRect(COLOR_BOX_LEFT, COLOR_BOX_TOP, COLOR_BOX_WIDTH, COLOR_BOX_HEIGHT, 1, COLOR2FLAGS(BLACK)); +} + +void ColorEditorContent::paint(BitmapBuffer *dc) +{ + ModalWindowContent::paint(dc); + drawHueBar(dc); + drawGrid(dc); + drawColorBox(dc); +} + + +//////////////////////////////////////////////////////////////////////////////////////////////// +///// ColorEditorPopup +//////////////////////////////////////////////////////////////////////////////////////////////// +ColorEditorPopup::ColorEditorPopup(Window *window, std::function getValue, std::function setValue) : + ModalWindow(window, true), + _getValue(getValue), + _setValue(setValue) +{ + color = _getValue(); + content = new ColorEditorContent(this, contentRect, color, [=] (uint32_t rgb) { + if (_setValue != nullptr) + _setValue(rgb); + }); + + content->setFocus(); + bringToTop(); + content->setTitle(STR_COLOR_PICKER); + killAllEvents(); +} + +#if defined(HARDWARE_TOUCH) +bool ColorEditorPopup::onTouchStart(coord_t x, coord_t y) +{ + content->onTouchStart(x - content->rect.x + content->scrollPositionX, y - content->rect.y + content->scrollPositionY); + return true; +} + +bool ColorEditorPopup::onTouchEnd(coord_t x, coord_t y) +{ + content->onTouchEnd(x - content->rect.x + content->scrollPositionX, y - content->rect.y + content->scrollPositionY); + if (!Window::onTouchEnd(x, y) && closeWhenClickOutside) { + onKeyPress(); + deleteLater(); + } + return true; +} +#endif + +void ColorEditorPopup::deleteLater(bool detach, bool trash) +{ + if (_deleted) + return; + + Layer::pop(this); + Window::deleteLater(detach, trash); +} + + diff --git a/radio/src/gui/colorlcd/color_editor_popup.h b/radio/src/gui/colorlcd/color_editor_popup.h new file mode 100644 index 000000000..ebfdfd015 --- /dev/null +++ b/radio/src/gui/colorlcd/color_editor_popup.h @@ -0,0 +1,98 @@ +/* + * Copyright (C) EdgeTX + * + * Based on code named + * opentx - https://github.com/opentx/opentx + * th9x - http://code.google.com/p/th9x + * er9x - http://code.google.com/p/er9x + * gruvin9x - http://code.google.com/p/gruvin9x + * + * License GPLv2: http://www.gnu.org/licenses/gpl-2.0.html + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + */ +#pragma once +#include +#include +#include +#include "bitmapbuffer.h" +#include "libopenui.h" +#include "theme_manager.h" +#include "listbox.h" + +// the content page of the ColorEditorPupup +class ColorEditorContent : public ModalWindowContent +{ + friend class ColorEditorPopup; + public: + ColorEditorContent(ModalWindow *window, const rect_t rect, uint32_t color, std::function setValue = nullptr); + +#if defined(DEBUG_WINDOWS) + std::string getName() const override + { + return "ColorEditorContent"; + } +#endif + +#if defined(HARDWARE_TOUCH) + bool onTouchSlide(coord_t x, coord_t y, coord_t startX, coord_t startY, coord_t slideX, coord_t slideY) override; + bool onTouchEnd(coord_t x, coord_t y) override; + bool onTouchStart(coord_t x, coord_t y) override; +#endif + +#if defined(HARDWARE_KEYS) + void onEvent(event_t event) override; +#endif + + void paint(BitmapBuffer *dc) override; + + protected: + bool sliding = false; + bool colorPicking = false; + int r, g, b; + int hue = 0, s, v; + NumberEdit *rEdit; + NumberEdit *gEdit; + NumberEdit *bEdit; + + void drawHueBar(BitmapBuffer *dc); + void drawGrid(BitmapBuffer *dc); + void drawColorBox(BitmapBuffer *dc); + void setRGB(); + std::function setValue; +}; + +// a color editor popup +class ColorEditorPopup : public ModalWindow +{ + public: + ColorEditorPopup(Window *window, std::function getValue, std::function setValue = nullptr); + + void deleteLater(bool detach = true, bool trash = true) override; + +#if defined(DEBUG_WINDOWS) + std::string getName() const override + { + return "ColorEditorPopup"; + } +#endif + +#if defined(HARDWARE_TOUCH) + bool onTouchStart(coord_t x, coord_t y) override; + bool onTouchEnd(coord_t x, coord_t y) override; +#endif + + protected: + uint32_t color; + ColorEditorContent *content; + std::function _getValue; + std::function _setValue; +}; + diff --git a/radio/src/gui/colorlcd/color_list.cpp b/radio/src/gui/colorlcd/color_list.cpp new file mode 100644 index 000000000..8cd26a6ad --- /dev/null +++ b/radio/src/gui/colorlcd/color_list.cpp @@ -0,0 +1,53 @@ +/* + * Copyright (C) EdgeTX + * + * Based on code named + * opentx - https://github.com/opentx/opentx + * th9x - http://code.google.com/p/th9x + * er9x - http://code.google.com/p/er9x + * gruvin9x - http://code.google.com/p/gruvin9x + * + * License GPLv2: http://www.gnu.org/licenses/gpl-2.0.html + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + */ +#include "color_list.h" +#include "color_editor.h" +#include "color_editor.h" + +ColorList::ColorList( + Window *parent, const rect_t &rect, std::vector colors, + WindowFlags windowFlag, LcdFlags lcdFlags) : + ListBase(parent, rect, getColorListNames(colors), nullptr, nullptr), + _colorList(colors), + _tp(ThemePersistance::instance()) +{ + setSelected(0); +} + +std::vector ColorList::getColorListNames(std::vector colors) +{ + std::vector names; + char **colorNames = ThemePersistance::getColorNames(); + for (auto color : colors) { + names.emplace_back(colorNames[color.colorNumber]); + } + + return names; +} + +void ColorList::drawLine(BitmapBuffer *dc, const rect_t &rect, uint32_t index, LcdFlags lcdFlags) +{ + ListBase::drawLine(dc, rect, index, lcdFlags); + auto fontHeight = getFontHeight(FONT(STD)); + dc->drawSolidFilledRect(rect.w - 22, rect.y, 16, fontHeight, + COLOR2FLAGS(_colorList[index].colorValue)); + dc->drawSolidRect(rect.w - 22, rect.y, 16, fontHeight, 1, COLOR2FLAGS(BLACK)); +} diff --git a/radio/src/gui/colorlcd/color_list.h b/radio/src/gui/colorlcd/color_list.h new file mode 100644 index 000000000..fdfa8daae --- /dev/null +++ b/radio/src/gui/colorlcd/color_list.h @@ -0,0 +1,57 @@ +/* + * Copyright (C) EdgeTX + * + * Based on code named + * opentx - https://github.com/opentx/opentx + * th9x - http://code.google.com/p/th9x + * er9x - http://code.google.com/p/er9x + * gruvin9x - http://code.google.com/p/gruvin9x + * + * License GPLv2: http://www.gnu.org/licenses/gpl-2.0.html + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + */ +#pragma once +#include +#include +#include +#include "bitmapbuffer.h" +#include "libopenui.h" +#include "theme_manager.h" +#include "listbox.h" + +// a list of color theme names with their color to the right +class ColorList : public ListBase +{ + public: + ColorList(Window *parent, const rect_t &rect, std::vector colors, + WindowFlags windowFlags = 0, LcdFlags lcdFlags = 0); + + inline ColorEntry getSelectedColor() + { + return _colorList[selected]; + } + + std::vector getColorListNames(std::vector colors); + + inline void setColorList(std::vector colorList) + { + this->_colorList = colorList; + ListBase::setNames(getColorListNames(colorList)); + invalidate(); + } + + void drawLine(BitmapBuffer *dc, const rect_t &rect, uint32_t index, LcdFlags lcdFlags) override; + + protected: + std::vector _colorList; + ThemePersistance *_tp; +}; + diff --git a/radio/src/gui/colorlcd/colors.cpp b/radio/src/gui/colorlcd/colors.cpp index ae518f5c5..ea9024340 100644 --- a/radio/src/gui/colorlcd/colors.cpp +++ b/radio/src/gui/colorlcd/colors.cpp @@ -20,6 +20,86 @@ */ #include "colors.h" +#include +#include uint16_t lcdColorTable[LCD_COLOR_COUNT]; +uint32_t HSVtoRGB(float H, float S, float V) +{ + if (H > 360 || H < 0 || S > 100 || S < 0 || V > 100 || V < 0) { + return 0; + } + + float s = S / 100; + float v = V / 100; + float C = s * v; + float X = C * (1 - abs(fmod(H / 60.0, 2) - 1)); + float m = v - C; + float r, g, b; + + if (H >= 0 && H < 60) { + r = C, g = X, b = 0; + } + else if (H >= 60 && H < 120) { + r = X, g = C, b = 0; + } + else if (H >= 120 && H < 180) { + r = 0, g = C, b = X; + } + else if (H >= 180 && H < 240) { + r = 0, g = X, b = C; + } + else if (H >= 240 && H < 300) { + r = X, g = 0, b = C; + } + else { + r = C, g = 0, b = X; + } + int R = (r + m) * 255; + int G = (g + m) * 255; + int B = (b + m) * 255; + + return RGB(R, G, B); +} + +void RGBtoHSV(uint8_t R, uint8_t G, uint8_t B, float& fH, float& fS, float& fV) +{ + float fR = (float)R / 255; + float fG = (float)G / 255; + float fB = (float)B / 255; + float fCMax = std::max(std::max(fR, fG), fB); + float fCMin = std::min(std::min(fR, fG), fB); + float fDelta = fCMax - fCMin; + + if (fDelta > 0) { + if (fCMax == fR) { + fH = 60 * (fmod(((fG - fB) / fDelta), 6)); + } + else if (fCMax == fG) { + fH = 60 * (((fB - fR) / fDelta) + 2); + } + else if (fCMax == fB) { + fH = 60 * (((fR - fG) / fDelta) + 4); + } + + if (fCMax > 0) { + fS = fDelta / fCMax; + } + else { + fS = 0; + } + + fV = fCMax; + } + else { + fH = 0; + fS = 0; + fV = fCMax; + } + + if (fH < 0) { + fH = 360 + fH; + } +} + diff --git a/radio/src/gui/colorlcd/colors.h b/radio/src/gui/colorlcd/colors.h index 99cc1e9a5..04896de2f 100644 --- a/radio/src/gui/colorlcd/colors.h +++ b/radio/src/gui/colorlcd/colors.h @@ -91,4 +91,11 @@ inline void lcdSetColor(uint16_t color) #define WINDOWS_INSPECT_BORDER_COLOR COLOR_THEME_PRIMARY3 #endif +constexpr int MAX_SATURATION = 100; +constexpr int MAX_HUE = 360; +constexpr int MAX_BRIGHTNESS = 100; + +extern uint32_t HSVtoRGB(float H, float S, float V); +extern void RGBtoHSV(uint8_t R, uint8_t G, uint8_t B, float& fH, float& fS, float& fV); + #endif // _COLORS_H_ diff --git a/radio/src/gui/colorlcd/file_carosell.cpp b/radio/src/gui/colorlcd/file_carosell.cpp new file mode 100644 index 000000000..6a9bf07da --- /dev/null +++ b/radio/src/gui/colorlcd/file_carosell.cpp @@ -0,0 +1,64 @@ +/* + * Copyright (C) EdgeTX + * + * Based on code named + * opentx - https://github.com/opentx/opentx + * th9x - http://code.google.com/p/th9x + * er9x - http://code.google.com/p/er9x + * gruvin9x - http://code.google.com/p/gruvin9x + * + * License GPLv2: http://www.gnu.org/licenses/gpl-2.0.html + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + */ +#include "file_carosell.h" + +extern inline tmr10ms_t getTicks() +{ + return g_tmr10ms; +} + +#define PAGE_INTERVAL ((1000 / 10) * 2) + + +FileCarosell::FileCarosell(Window *parent, const rect_t &rect, + std::vector fileNames, FormField *nextCtrl) : + FormGroup(parent, rect, NO_FOCUS | FORM_NO_BORDER), + _fileNames(fileNames), + fp(new FilePreview(this, {0, 0, rect.w, rect.h}, false)) +{ + timer = getTicks(); + setSelected(0); +} + +FileCarosell::~FileCarosell() +{ +} + + void FileCarosell::setFileNames(std::vector fileNames) + { + _fileNames = fileNames; + selected = -1; // so it passes the sameness comparison + setSelected(0); + timer = getTicks(); + } + + +void FileCarosell::checkEvents() +{ + FormGroup::checkEvents(); + + uint32_t newTicks = getTicks(); + if (newTicks - timer > PAGE_INTERVAL && _fileNames.size()) { + int newSelected = (selected + 1) % _fileNames.size(); + setSelected(newSelected); + timer = newTicks; + } +} diff --git a/radio/src/gui/colorlcd/file_carosell.h b/radio/src/gui/colorlcd/file_carosell.h new file mode 100644 index 000000000..e09dc62f7 --- /dev/null +++ b/radio/src/gui/colorlcd/file_carosell.h @@ -0,0 +1,52 @@ +/* + * Copyright (C) EdgeTX + * + * Based on code named + * opentx - https://github.com/opentx/opentx + * th9x - http://code.google.com/p/th9x + * er9x - http://code.google.com/p/er9x + * gruvin9x - http://code.google.com/p/gruvin9x + * + * License GPLv2: http://www.gnu.org/licenses/gpl-2.0.html + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + */ +#pragma once +#include "color_editor.h" +#include "file_preview.h" +#include "file_carosell.h" +#include "tabsgroup.h" + +class FileCarosell : public FormGroup +{ + public: + FileCarosell(Window *parent, const rect_t &rect, + std::vector fileNames, FormField *nextCtrl = nullptr); + ~FileCarosell(); + + inline void setSelected(int n) + { + if (n == selected) return; + if (n >= 0 && n < (int)_fileNames.size()) { + selected = n; + fp->setFile(_fileNames[selected].c_str()); + } else + fp->setFile(""); + } + + void setFileNames(std::vector fileNames); + void checkEvents() override; + + protected: + int selected = -1; + std::vector _fileNames; + FilePreview *fp; + uint32_t timer; +}; diff --git a/radio/src/gui/colorlcd/file_preview.h b/radio/src/gui/colorlcd/file_preview.h index 56188c1e3..ede22777c 100644 --- a/radio/src/gui/colorlcd/file_preview.h +++ b/radio/src/gui/colorlcd/file_preview.h @@ -25,10 +25,17 @@ class FilePreview : public Window { public: - FilePreview(Window *parent, const rect_t &rect) : - Window(parent, rect, NO_SCROLLBAR) + FilePreview(Window *parent, const rect_t &rect, bool drawCentered = true) : + Window(parent, rect, NO_SCROLLBAR), + _drawCentered(drawCentered) { } + + ~FilePreview() + { + if (bitmap != nullptr) + delete bitmap; + } #if defined(DEBUG_WINDOWS) std::string getName() const override @@ -39,7 +46,10 @@ class FilePreview : public Window void setFile(const char *filename) { - delete bitmap; + if (bitmap != nullptr) + delete bitmap; + bitmap = nullptr; + const char *ext = getFileExtension(filename); if (ext && isExtensionMatching(ext, BITMAPS_EXT)) { bitmap = BitmapBuffer::loadBitmap(filename); @@ -52,16 +62,18 @@ class FilePreview : public Window void paint(BitmapBuffer *dc) override { dc->setFormat(BMP_RGB565); - coord_t y = parent->getScrollPositionY() + 2; - coord_t h = MENU_BODY_HEIGHT - 4; - if (bitmap) { + coord_t y = _drawCentered ? parent->getScrollPositionY() + 2 : 2; + coord_t h = _drawCentered ? MENU_BODY_HEIGHT - 4 : rect.h; + if (bitmap != nullptr) { coord_t bitmapHeight = min(h, bitmap->height()); coord_t bitmapWidth = min(width(), bitmap->width()); - dc->drawScaledBitmap(bitmap, (width() - bitmapWidth) / 2, y + (h - bitmapHeight) / 2, bitmapWidth, - bitmapHeight); + dc->drawScaledBitmap(bitmap, (width() - bitmapWidth) / 2, + _drawCentered ? y + (h - bitmapHeight) / 2 : 0, + bitmapWidth, bitmapHeight); } } protected: BitmapBuffer *bitmap = nullptr; + bool _drawCentered = true; }; diff --git a/radio/src/gui/colorlcd/listbox.cpp b/radio/src/gui/colorlcd/listbox.cpp new file mode 100644 index 000000000..899a36ed2 --- /dev/null +++ b/radio/src/gui/colorlcd/listbox.cpp @@ -0,0 +1,208 @@ +/* + * Copyright (C) EdgeTX + * + * Based on code named + * opentx - https://github.com/opentx/opentx + * th9x - http://code.google.com/p/th9x + * er9x - http://code.google.com/p/er9x + * gruvin9x - http://code.google.com/p/gruvin9x + * + * License GPLv2: http://www.gnu.org/licenses/gpl-2.0.html + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + */ +#include "listbox.h" + +constexpr int LONG_PRESS_10MS = 40; +constexpr int ACTIVE_RADIUS = 5; + +extern inline tmr10ms_t getTicks() +{ + return g_tmr10ms; +} + +ListBase::ListBase(Window *parent, const rect_t &rect, std::vector names, + std::function getValue, + std::function setValue, + uint8_t lineHeight, + WindowFlags windowFlags, LcdFlags lcdFlags) : + FormField(parent, rect, windowFlags), + names(names), + _getValue(std::move(getValue)), + _setValue(std::move(setValue)), + lineHeight(lineHeight) +{ +#if defined(HARDWARE_TOUCH) + duration10ms = 0; +#endif + setInnerHeight(names.size() * lineHeight); + if (_getValue != nullptr) + setSelected(_getValue()); + else + setSelected(0); +} + +void ListBase::setSelected(int selected) +{ + if (selected >= 0 && selected < (int)names.size()) { + this->selected = selected; + setScrollPositionY(lineHeight * this->selected - lineHeight); + if (_setValue != nullptr) { + _setValue(this->selected); + } + invalidate(); + } +} + +void ListBase::drawLine(BitmapBuffer *dc, const rect_t &rect, uint32_t index, LcdFlags lcdFlags) +{ + std::string name = names[index]; + int x = rect.x; + + if ((uint32_t)activeIndex == index) { + LcdFlags circleColor = index == selected ? COLOR_THEME_PRIMARY2 : COLOR_THEME_PRIMARY1; + auto textWidth = getTextWidth(name.c_str(), name.length(), lcdFlags); + auto fontHeight = getFontHeight(FONT(STD)); + + dc->drawFilledCircle(rect.x + 4 + textWidth + ACTIVE_RADIUS, rect.y + fontHeight / 2, ACTIVE_RADIUS, circleColor); + } + + dc->drawText(x, rect.y, name.c_str(), lcdFlags); +} + +void ListBase::paint(BitmapBuffer *dc) +{ + dc->clear(COLOR_THEME_SECONDARY3); + uint32_t bgColor = 0; + bgColor = hasFocus() ? COLOR_THEME_FOCUS : COLOR_THEME_SECONDARY1; + + int curY = 0; + for (int n = 0; n < (int)names.size(); n++) { + if (n == selected) { + dc->drawSolidFilledRect(1, curY, rect.w - 2, lineHeight, bgColor); + } + + LcdFlags textColor = + n == selected ? COLOR_THEME_PRIMARY2 : COLOR_THEME_PRIMARY1; + + auto fontHeight = getFontHeight(FONT(STD)); + drawLine(dc, { 8, curY + (lineHeight - fontHeight) / 2, rect.w, lineHeight}, n, textColor); + + curY += lineHeight; + } + if (!(windowFlags & (FORM_NO_BORDER | FORM_FORWARD_FOCUS))) { + dc->drawSolidRect(0, getScrollPositionY(), rect.w, rect.h, 2, + COLOR_THEME_FOCUS); + } +} + +#if defined(HARDWARE_KEYS) + void ListBase::onEvent(event_t event) + { + int oldSelected = selected; + switch (event) { + case EVT_ROTARY_RIGHT: + oldSelected = (selected + 1) % names.size(); + setSelected(oldSelected); + onKeyPress(); + break; + case EVT_ROTARY_LEFT: + oldSelected--; + if (oldSelected < 0) oldSelected = names.size() - 1; + setSelected(oldSelected); + onKeyPress(); + break; + case EVT_KEY_LONG(KEY_ENTER): + if (longPressHandler) { + killEvents(event); + longPressHandler(event); + } + break; + case EVT_KEY_BREAK(KEY_ENTER): + if (pressHandler) { + killEvents(event); + pressHandler(event); + } + break; + + default: + FormField::onEvent(event); + } + } +#endif + +#if defined(HARDWARE_TOUCH) +bool ListBase::isLongPress() +{ + unsigned int curTimer = getTicks(); + return (slidingWindow == nullptr && duration10ms != 0 && curTimer - duration10ms > LONG_PRESS_10MS); +} + + +void ListBase::checkEvents(void) +{ + Window::checkEvents(); + + if (isLongPress()) { + if (longPressHandler) { + setSelected(yDown / lineHeight); + longPressHandler(0); + killAllEvents(); + duration10ms = 0; + return; + } + } +} + +bool ListBase::onTouchSlide(coord_t x, coord_t y, coord_t startX, coord_t startY, coord_t slideX, coord_t slideY) +{ + if (touchState.event == TE_SLIDE_END) { + duration10ms = 0; + } + + return FormField::onTouchSlide(x, y, startX, startY, slideX, slideY); +} + +bool ListBase::onTouchStart(coord_t x, coord_t y) +{ + if (duration10ms == 0) { + duration10ms = getTicks(); + } + + yDown = y; + + return true; // stop the processing and say that i handled it +} + +bool ListBase::onTouchEnd(coord_t x, coord_t y) +{ + if (!isEnabled()) return false; + if (slidingWindow) + return false; // if we slide then this is not a selection + + auto selected = yDown / lineHeight; + setSelected(selected); + + duration10ms = 0; + + if (!hasFocus()) { + setFocus(SET_FOCUS_DEFAULT); + } + + if (pressHandler != nullptr) { + pressHandler(0); + } + invalidate(); + + return true; +} +#endif + + diff --git a/radio/src/gui/colorlcd/listbox.h b/radio/src/gui/colorlcd/listbox.h new file mode 100644 index 000000000..e9102d9d4 --- /dev/null +++ b/radio/src/gui/colorlcd/listbox.h @@ -0,0 +1,135 @@ +/* + * Copyright (C) EdgeTX + * + * Based on code named + * opentx - https://github.com/opentx/opentx + * th9x - http://code.google.com/p/th9x + * er9x - http://code.google.com/p/er9x + * gruvin9x - http://code.google.com/p/gruvin9x + * + * License GPLv2: http://www.gnu.org/licenses/gpl-2.0.html + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + */ +#pragma once +#include +#include +#include +#include +#include "bitmapbuffer.h" +#include "libopenui.h" +#include "touch.h" + +// base class for lists of elements with names +class ListBase : public FormField +{ + public: + ListBase(Window *parent, const rect_t &rect, std::vector names, + std::function getValue = nullptr, + std::function setValue = nullptr, + uint8_t lineHeight = MENUS_LINE_HEIGHT, + WindowFlags windowFlags = 0, LcdFlags lcdFlags = 0); + + + // draw one line of the list. Default implementation just draws the names. Oher types of lists + // can draw anything they want. + virtual void drawLine(BitmapBuffer *dc, const rect_t &rect, uint32_t index, LcdFlags lcdFlags); + void paint (BitmapBuffer *dc) override; + + virtual void setSelected(int selected); + + void setLongPressHandler(std::function handler) + { + longPressHandler = std::move(handler); + } + + void setPressHandler(std::function handler) + { + pressHandler = std::move(handler); + } + + void setActiveIndex(int index) + { + if (index >= 0 && index < (int) names.size()) { + activeIndex = index; + } else { + activeIndex = -1; + } + + invalidate(); + } + + inline int getSelected() + { + return selected; + } + + inline void setLineHeight(uint8_t height) + { + lineHeight = height; + } + + inline void setNames(std::vector names) + { + this->names.assign(names.begin(), names.end()); + setInnerHeight(names.size() * lineHeight); + invalidate(); + } + +#if defined(DEBUG_WINDOWS) + std::string getName() const override + { + return "ListBox"; + } +#endif + +#if defined(HARDWARE_KEYS) + void onEvent(event_t event) override; +#endif + +#if defined(HARDWARE_TOUCH) + void checkEvents(void) override; + bool onTouchEnd(coord_t x, coord_t y) override; + bool onTouchStart(coord_t x, coord_t y) override; + bool onTouchSlide(coord_t x, coord_t y, coord_t startX, coord_t startY, coord_t slideX, coord_t slideY) override; + bool isLongPress(); +#endif + + protected: + std::function longPressHandler = nullptr; + std::function pressHandler = nullptr; + std::vector names; + std::function _getValue; + std::function _setValue; + int lineHeight; + int32_t selected = -1; + int32_t activeIndex = -1; + +#if defined(HARDWARE_TOUCH) + uint32_t duration10ms; + coord_t yDown; +#endif +}; + + +class ListBox : public ListBase +{ + public: + using ListBase::ListBase; + + inline void setTitle(std::string title) + { + this->title = title; + } + + protected: + std::string title; +}; + diff --git a/radio/src/gui/colorlcd/menu_radio.cpp b/radio/src/gui/colorlcd/menu_radio.cpp index 1e9984643..691f72b40 100644 --- a/radio/src/gui/colorlcd/menu_radio.cpp +++ b/radio/src/gui/colorlcd/menu_radio.cpp @@ -29,6 +29,7 @@ #include "radio_trainer.h" #include "radio_version.h" #include "radio_hardware.h" +#include "radio_theme.h" #include "myeeprom.h" RadioMenu::RadioMenu(): @@ -37,6 +38,7 @@ RadioMenu::RadioMenu(): addTab(new RadioToolsPage()); addTab(new RadioSdManagerPage()); addTab(new RadioSetupPage()); + addTab(new ThemeSetupPage()); addTab(new SpecialFunctionsPage(g_eeGeneral.customFn)); addTab(new RadioTrainerPage()); addTab(new RadioHardwarePage()); diff --git a/radio/src/gui/colorlcd/menus.h b/radio/src/gui/colorlcd/menus.h index 76051d70d..142aa5bf1 100644 --- a/radio/src/gui/colorlcd/menus.h +++ b/radio/src/gui/colorlcd/menus.h @@ -41,6 +41,7 @@ enum MenuIcons { ICON_RADIO_TRAINER, ICON_RADIO_HARDWARE, ICON_RADIO_CALIBRATION, + ICON_RADIO_EDIT_THEME, ICON_RADIO_VERSION, ICON_MODEL, ICON_MODEL_SETUP, diff --git a/radio/src/gui/colorlcd/page.h b/radio/src/gui/colorlcd/page.h index 4bbf0c6c0..6d786a3c7 100644 --- a/radio/src/gui/colorlcd/page.h +++ b/radio/src/gui/colorlcd/page.h @@ -43,6 +43,29 @@ class PageHeader: public FormGroup } #endif + void onEvent(event_t event) override + { + if (event == EVT_KEY_BREAK(KEY_PGUP)) { + onKeyPress(); + FormField *focus = dynamic_cast(getFocus()); + if (focus != nullptr && focus->getPreviousField()) { + focus->getPreviousField()->setFocus(SET_FOCUS_BACKWARD, focus); + } + } else if (event == EVT_KEY_BREAK(KEY_PGDN)) { + onKeyPress(); + FormField *focus = dynamic_cast(getFocus()); + if (focus != nullptr && focus->getNextField()) { + focus->getNextField()->setFocus(SET_FOCUS_FORWARD, focus); + } + } else if (event == EVT_KEY_FIRST(KEY_EXIT)) { + killEvents(event); + parent->deleteLater(); // destroy the Page + } + else { + FormGroup::onEvent(event); + } + } + void paint(BitmapBuffer * dc) override; uint8_t getIcon() const @@ -86,7 +109,7 @@ class Page: public Window bool onTouchSlide(coord_t x, coord_t y, coord_t startX, coord_t startY, coord_t slideX, coord_t slideY) override { - Window::onTouchSlide(x, y, startX, startY, slideX, slideY); + Window::onTouchSlide(x, y, startX, startY, 0/*slideX*/, slideY); return true; } #endif diff --git a/radio/src/gui/colorlcd/preview_window.cpp b/radio/src/gui/colorlcd/preview_window.cpp new file mode 100644 index 000000000..d7130b7e8 --- /dev/null +++ b/radio/src/gui/colorlcd/preview_window.cpp @@ -0,0 +1,23 @@ +/* + * Copyright (C) EdgeTX + * + * Based on code named + * opentx - https://github.com/opentx/opentx + * th9x - http://code.google.com/p/th9x + * er9x - http://code.google.com/p/er9x + * gruvin9x - http://code.google.com/p/gruvin9x + * + * License GPLv2: http://www.gnu.org/licenses/gpl-2.0.html + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + */ +#include "preview_window.h" + +ColorMaintainer colorMaintainer; diff --git a/radio/src/gui/colorlcd/preview_window.h b/radio/src/gui/colorlcd/preview_window.h new file mode 100644 index 000000000..6b172a4e0 --- /dev/null +++ b/radio/src/gui/colorlcd/preview_window.h @@ -0,0 +1,251 @@ +/* + * Copyright (C) EdgeTX + * + * Based on code named + * opentx - https://github.com/opentx/opentx + * th9x - http://code.google.com/p/th9x + * er9x - http://code.google.com/p/er9x + * gruvin9x - http://code.google.com/p/gruvin9x + * + * License GPLv2: http://www.gnu.org/licenses/gpl-2.0.html + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + */ + +#ifndef PREVIEW_WINDOW_INCLUDE +#define PREVIEW_WINDOW_INCLUDE +#include + +#include "libopenui.h" +#include "opentx.h" +#include "sliders.h" +#include "theme_manager.h" +#include "themes/480_bitmaps.h" + +extern inline tmr10ms_t getTicks() { return g_tmr10ms; } + +#define PREVIEW_WINDOW_REFRESH_INTERVAL ((1000 / 10) * 1) + +// class to hold a color list and apply / restore it while drawing standard +// controls +class ColorMaintainer +{ + public: + void setColorList(std::vector colorList) + { + this->colorList.assign(colorList.begin(), colorList.end()); + } + + void applyColorValues() + { + // save old values; + for (auto i = 0; i < COLOR_COUNT; i++) { + oldColorVals[i] = lcdColorTable[i]; + } + + for (auto color : colorList) { + lcdColorTable[color.colorNumber] = color.colorValue; + } + } + + void restoreColorValues() + { + for (auto i = 0; i < COLOR_COUNT; i++) { + lcdColorTable[i] = oldColorVals[i]; + } + } + + protected: + uint32_t oldColorVals[COLOR_COUNT]; + std::vector colorList; +}; + +extern ColorMaintainer colorMaintainer; + +// override of controls to draw them using a selected theme +class ThemedTextEdit : public TextEdit +{ + public: + using TextEdit::TextEdit; + + void paint(BitmapBuffer *dc) override + { + colorMaintainer.applyColorValues(); + TextEdit::paint(dc); + colorMaintainer.restoreColorValues(); + } +}; + +class ThemedStaticText : public StaticText +{ + public: + using StaticText::StaticText; + + void paint(BitmapBuffer *dc) override + { + colorMaintainer.applyColorValues(); + setTextFlags(COLOR_THEME_PRIMARY1); + StaticText::paint(dc); + colorMaintainer.restoreColorValues(); + } +}; + +class ThemedCheckBox : public CheckBox +{ + public: + ThemedCheckBox(Window *parent, rect_t rect, bool checked) : + CheckBox( + parent, rect, [=]() { return checked; }, [](uint8_t value) {}), + checked(checked) + { + } + + void paint(BitmapBuffer *dc) override + { + colorMaintainer.applyColorValues(); + CheckBox::paint(dc); + colorMaintainer.restoreColorValues(); + } + + protected: + bool checked; +}; + +class ThemedMainViewHorizontalTrim : public MainViewHorizontalTrim +{ + public: + using MainViewHorizontalTrim::MainViewHorizontalTrim; + void paint(BitmapBuffer *dc) override + { + colorMaintainer.applyColorValues(); + MainViewHorizontalTrim::paint(dc); + colorMaintainer.restoreColorValues(); + } +}; + +class ThemedMainViewHorizontalSlider : public MainViewHorizontalSlider +{ + public: + using MainViewHorizontalSlider::MainViewHorizontalSlider; + void paint(BitmapBuffer *dc) override + { + colorMaintainer.applyColorValues(); + MainViewHorizontalSlider::paint(dc); + colorMaintainer.restoreColorValues(); + } +}; + +// display controls using the appropriate theme. +class PreviewWindow : public FormGroup +{ + public: + PreviewWindow(Window *window, rect_t rect, + std::vector colorList) : + FormGroup(window, rect, NO_FOCUS), _colorList(colorList) + { + new ThemedStaticText(this, {5, 40, 100, LINE_HEIGHT}, "Checkbox", 0, + COLOR_THEME_PRIMARY1); + new ThemedCheckBox(this, {100 + 15, 40, 20, LINE_HEIGHT}, true); + new ThemedCheckBox(this, {140 + 15, 40, 20, LINE_HEIGHT}, false); + new ThemedMainViewHorizontalTrim(this, + {5, 65, HORIZONTAL_SLIDERS_WIDTH, 20}, 0); + new ThemedMainViewHorizontalSlider( + this, {5, 87, HORIZONTAL_SLIDERS_WIDTH, 20}, 0); + ticks = getTicks(); + } + + void setColorList(std::vector colorList) + { + _colorList = colorList; + } + + void checkEvents() override + { + Window::checkEvents(); + + tmr10ms_t newTicks = getTicks(); + if (newTicks - ticks > PREVIEW_WINDOW_REFRESH_INTERVAL) { + invalidate(); + ticks = newTicks; + } + } + + BitmapBuffer *getBitmap(const uint8_t *maskData, uint32_t bgColor, + uint32_t fgColor, int *width) + { + auto mask = BitmapBuffer::load8bitMask(maskData); + BitmapBuffer *newBm = + new BitmapBuffer(BMP_RGB565, mask->width(), mask->height()); + newBm->clear(bgColor); + newBm->drawMask(0, 0, mask, fgColor); + delete mask; + return newBm; + } + + void drawTime(BitmapBuffer *dc) + { + // time on top bar + struct gtm t; + char str[10]; + char str1[20]; + const char *const STR_MONTHS[] = TR_MONTHS; + + gettime(&t); + sprintf(str, "%d %s\n", t.tm_mday, STR_MONTHS[t.tm_mon]); + + getTimerString(str1, getValue(MIXSRC_TX_TIME)); + strcat(str, str1); + dc->drawText(rect.w - 40, 5, str, COLOR_THEME_PRIMARY2 | FONT(XS)); + } + + void paint(BitmapBuffer *dc) override + { + colorMaintainer.setColorList(_colorList); + colorMaintainer.applyColorValues(); + + // background + dc->clear(COLOR_THEME_SECONDARY3); + + // top bar background + dc->drawSolidFilledRect(0, 0, rect.w, TOPBAR_HEIGHT, + COLOR_THEME_SECONDARY1); + + int width; + int x = 5; + // topbar icons + auto bm = getBitmap(mask_menu_radio, COLOR_THEME_SECONDARY1, + COLOR_THEME_PRIMARY2, &width); + dc->drawBitmap(x, 5, bm); + x += MENU_HEADER_BUTTON_WIDTH + 2; + delete bm; + + bm = getBitmap(mask_radio_tools, COLOR_THEME_SECONDARY1, + COLOR_THEME_PRIMARY2, &width); + dc->drawBitmap(x, 5, bm); + x += MENU_HEADER_BUTTON_WIDTH + 2; + delete bm; + + bm = getBitmap(mask_radio_setup, COLOR_THEME_SECONDARY1, + COLOR_THEME_PRIMARY2, &width); + dc->drawBitmap(x, 5, bm); + delete bm; + + drawTime(dc); + + colorMaintainer.restoreColorValues(); + } + + protected: + std::vector _colorList; + CheckBox *checkBox; + tmr10ms_t ticks; +}; + +#endif \ No newline at end of file diff --git a/radio/src/gui/colorlcd/radio_theme.cpp b/radio/src/gui/colorlcd/radio_theme.cpp new file mode 100644 index 000000000..cd9138252 --- /dev/null +++ b/radio/src/gui/colorlcd/radio_theme.cpp @@ -0,0 +1,586 @@ +/* + * Copyright (C) EdgeTX + * + * Based on code named + * opentx - https://github.com/opentx/opentx + * th9x - http://code.google.com/p/th9x + * er9x - http://code.google.com/p/er9x + * gruvin9x - http://code.google.com/p/gruvin9x + * + * License GPLv2: http://www.gnu.org/licenses/gpl-2.0.html + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + */ +#include "radio_theme.h" +#include + +#include "bitmapbuffer.h" +#include "libopenui.h" +#include "opentx.h" +#include "file_preview.h" +#include "theme_manager.h" +#include "view_main.h" +#include "widget_settings.h" +#include "sliders.h" +#include "color_list.h" +#include "color_editor_popup.h" +#include "color_editor.h" +#include "listbox.h" +#include "preview_window.h" + +#if (LCD_W > LCD_H) + #define COLOR_PREVIEW_WIDTH 18 + #define COLOR_PREVIEW_HEIGHT (LCD_H - TOPBAR_HEIGHT - 30) + #define LEFT_LIST_WIDTH (LCD_W / 2) - COLOR_PREVIEW_WIDTH + #define LEFT_LIST_HEIGHT (LCD_H - TOPBAR_HEIGHT - 37) +#else + #define COLOR_PREVIEW_WIDTH 18 + #define COLOR_PREVIEW_HEIGHT (LCD_H - TOPBAR_HEIGHT - 30) + #define LEFT_LIST_WIDTH LCD_W + #define LEFT_LIST_HEIGHT (LCD_H / 2 - 38) +#endif + +#define MARGIN_WIDTH 5 + +#if (LCD_W > LCD_H) +constexpr int BUTTON_HEIGHT = 30; +constexpr int BUTTON_WIDTH = 75; +constexpr LcdFlags textFont = FONT(STD); +constexpr rect_t detailsDialogRect = {50, 50, 400, 170}; +constexpr int labelWidth = 150; + +constexpr int COLOR_BOX_LEFT = 3; +constexpr int COLOR_BOX_WIDTH = 45; +constexpr int COLOR_BOX_HEIGHT = 27; +#else +constexpr int BUTTON_HEIGHT = 25; +constexpr int BUTTON_WIDTH = 50; +constexpr LcdFlags textFont = FONT(XS); +constexpr rect_t detailsDialogRect = {5, 50, LCD_W - 10, 340}; +constexpr int labelWidth = 120; + +constexpr int COLOR_BOX_LEFT = 3; +constexpr int COLOR_BOX_WIDTH = 55; +constexpr int COLOR_BOX_HEIGHT = 32; + +constexpr int COLOR_BUTTON_WIDTH = COLOR_BOX_WIDTH; +constexpr int COLOR_BUTTON_HEIGHT = 20; +#endif + +#define COLOR_LIST_WIDTH ((LCD_W * 3)/10) +#define COLOR_LIST_HEIGHT (LCD_H - TOPBAR_HEIGHT) + + +class ThemeDetailsDialog: public Dialog +{ + public: + ThemeDetailsDialog(Window *parent, ThemeFile theme, std::function saveHandler = nullptr) : + Dialog(parent, STR_EDIT_THEME_DETAILS, detailsDialogRect), + theme(theme), + saveHandler(saveHandler) + { + FormGridLayout grid(detailsDialogRect.w); + grid.setLabelWidth(labelWidth); + grid.spacer(8); + + new StaticText(&content->form, grid.getLabelSlot(), STR_NAME, 0, COLOR_THEME_PRIMARY1); + new TextEdit(&content->form, grid.getFieldSlot(), this->theme.getName(), NAME_LENGTH); + grid.nextLine(); + new StaticText(&content->form, grid.getLabelSlot(), STR_AUTHOR, 0, COLOR_THEME_PRIMARY1); + new TextEdit(&content->form, grid.getFieldSlot(), this->theme.getAuthor(), AUTHOR_LENGTH); + grid.nextLine(); + new StaticText(&content->form, grid.getLineSlot(), STR_DESCRIPTION, 0, COLOR_THEME_PRIMARY1); + grid.nextLine(); + new TextEdit(&content->form, grid.getLineSlot(), this->theme.getInfo(), INFO_LENGTH); + grid.nextLine(); + + rect_t r = {detailsDialogRect.w - (BUTTON_WIDTH + 5), grid.getWindowHeight() + 5, BUTTON_WIDTH, BUTTON_HEIGHT }; + new TextButton(&content->form, r, STR_SAVE, [=] () { + if (saveHandler != nullptr) + saveHandler(this->theme); + deleteLater(); + return 0; + }, BUTTON_BACKGROUND | OPAQUE, textFont); + r.x -= (BUTTON_WIDTH + 5); + new TextButton(&content->form, r, STR_CANCEL, [=] () { + deleteLater(); + return 0; + }, BUTTON_BACKGROUND | OPAQUE, textFont); + } + + void onEvent(event_t event) override + { + if (event == EVT_KEY_BREAK(KEY_EXIT)) { + deleteLater(); + } + } + + protected: + ThemeFile theme; + std::function saveHandler = nullptr; +}; + +class ColorSquare : public FormField +{ + public: + ColorSquare(Window *window, const rect_t &rect, uint32_t color, WindowFlags windowFlags = 0) + : FormField(window, rect, windowFlags | NO_FOCUS), + _color(color) + { + } + + void paint (BitmapBuffer *dc) override + { + dc->drawSolidFilledRect(0, 0, rect.w, rect.h, COLOR2FLAGS(_color)); + dc->drawSolidRect(0, 0, rect.w, rect.h, 1, COLOR2FLAGS(BLACK)); + } + + void setColorToEdit(uint32_t colorEntry) + { + _color = colorEntry; + + invalidate(); + } + + protected: + uint32_t _color; +}; + +void setHexStr(StaticText* hexBox, uint32_t rgb) +{ + auto r = GET_RED(rgb), g = GET_GREEN(rgb), b = GET_BLUE(rgb); + char hexstr[80]; + sprintf(hexstr, "%02X%02X%02X\n", (uint16_t)r, (uint16_t)g, (uint16_t)b); + hexBox->setText(hexstr); +} + +class ColorEditPage : public Page +{ +public: + ColorEditPage(ThemeFile theme, LcdColorIndex indexOfColor, + std::function setValue = nullptr) : + Page(ICON_RADIO_EDIT_THEME), + _setValue(std::move(setValue)), + _indexOfColor(indexOfColor), + _theme(theme) + { + buildHead(&header); + buildBody(&body); + } + + void setActiveColorBar(int activeTab) + { + if (activeTab >= 0 && _activeTab <= (int)_tabs.size()) { + _activeTab = activeTab; + for (auto i = 0; i < (int) _tabs.size(); i++) + _tabs[i]->check(i == _activeTab); + _colorEditor->setColorEditorType(_activeTab == 1 ? HSV_COLOR_EDITOR : RGB_COLOR_EDITOR); + onKeyPress(); + } + } + +#if defined(HARDWARE_KEYS) + void onEvent(event_t event ) override + { + if (event == EVT_KEY_BREAK(KEY_TELEM)) { + _colorEditor->setNextFocusBar(); + } else if (event == EVT_KEY_BREAK(KEY_PGUP) || event == EVT_KEY_BREAK(KEY_PGDN)) { + int direction = event == EVT_KEY_BREAK(KEY_PGUP) ? -1 : 1; + int newActiveTab = _activeTab + direction; + if (newActiveTab < 0) + newActiveTab = 1; + if (newActiveTab >= (int) _tabs.size()) + newActiveTab = 0; + + setActiveColorBar(newActiveTab); + } else { + Page::onEvent(event); + } + } +#endif + +protected: + std::function _setValue; + LcdColorIndex _indexOfColor; + ThemeFile _theme; + TextButton *_cancelButton; + ColorEditor *_colorEditor; + PreviewWindow *_previewWindow; + std::vector _tabs; + int _activeTab = 0; + ColorSquare *_colorSquare; + StaticText *_hexBox; + + void buildBody(FormWindow* window) + { + rect_t r; + + // hexBox + r = { COLOR_BOX_LEFT + COLOR_BOX_WIDTH + 5, 4, 90, 30 }; + _hexBox = new StaticText(window, r, "", 0, COLOR_THEME_PRIMARY1 | FONT(L) | RIGHT); + setHexStr(_hexBox, _theme.getColorEntryByIndex(_indexOfColor)->colorValue); + + r = { COLOR_BOX_LEFT, COLOR_BOX_HEIGHT + 7, COLOR_LIST_WIDTH, window->height() - COLOR_BOX_HEIGHT - 7}; + _colorEditor = new ColorEditor(window, r, _theme.getColorEntryByIndex(_indexOfColor)->colorValue, + [=](uint32_t rgb) { + _theme.setColor(_indexOfColor, rgb); + if (_colorSquare != nullptr) { + _colorSquare->setColorToEdit(rgb); + } + if (_previewWindow != nullptr) { + _previewWindow->setColorList(_theme.getColorList()); + } + if (_hexBox != nullptr) { + setHexStr(_hexBox, rgb); + } + if (_setValue != nullptr) + _setValue(rgb); + }); + _colorEditor->setColorEditorType(HSV_COLOR_EDITOR); + _activeTab = 1; + + r = { COLOR_BOX_LEFT, 4, COLOR_BOX_WIDTH, COLOR_BOX_HEIGHT }; + _colorSquare = new ColorSquare(window, r, _theme.getColorEntryByIndex(_indexOfColor)->colorValue); + + r = LCD_W > LCD_H ? + rect_t { COLOR_LIST_WIDTH + MARGIN_WIDTH, 4, LCD_W - COLOR_LIST_WIDTH - MARGIN_WIDTH * 2, COLOR_LIST_HEIGHT } : + rect_t { 0, LEFT_LIST_HEIGHT + 4, LEFT_LIST_WIDTH, LEFT_LIST_HEIGHT - 4 }; + _previewWindow = new PreviewWindow(window, r, _theme.getColorList()); + } + + void buildHead(PageHeader* window) + { + LcdFlags flags = 0; + if (LCD_W < LCD_H) { + flags = FONT(XS); + } + + // page title + new StaticText(window, + { PAGE_TITLE_LEFT, PAGE_TITLE_TOP, LCD_W - PAGE_TITLE_LEFT, + PAGE_LINE_HEIGHT }, + "Edit Color", 0, COLOR_THEME_PRIMARY2 | flags); + new StaticText(window, + { PAGE_TITLE_LEFT, PAGE_TITLE_TOP + PAGE_LINE_HEIGHT, + LCD_W - PAGE_TITLE_LEFT, PAGE_LINE_HEIGHT }, + ThemePersistance::getColorNames()[(int)_indexOfColor], 0, COLOR_THEME_PRIMARY2 | flags); + + // page tabs + rect_t r = { LCD_W - (BUTTON_WIDTH + 5), 6, BUTTON_WIDTH, BUTTON_HEIGHT }; + _tabs.emplace_back( + new PageButton(window, r, "RGB", + [=] () { + setActiveColorBar(0); + }, + 0, COLOR_THEME_PRIMARY1)); + r.x -= (BUTTON_WIDTH + 5); + _tabs.emplace_back( + new PageButton(window, r, "HSV", + [=] () { + setActiveColorBar(1); + }, + 0, COLOR_THEME_PRIMARY1)); + _tabs[1]->check(true); + } +}; + +class ThemeEditPage : public Page +{ + public: + explicit ThemeEditPage(ThemeFile theme, std::function saveHandler = nullptr) : + Page(ICON_RADIO_EDIT_THEME), + _theme(theme), + page(this), + saveHandler(std::move(saveHandler)) + { + buildBody(&body); + buildHeader(&header); + } + +#if defined(HARDWARE_KEYS) + void onEvent(event_t event) override + { + if (event == EVT_KEY_BREAK(KEY_PGUP)) { + onKeyPress(); + FormField *focus = dynamic_cast(getFocus()); + if (focus != nullptr && focus->getPreviousField()) { + focus->getPreviousField()->setFocus(SET_FOCUS_BACKWARD, focus); + } + } else if (event == EVT_KEY_BREAK(KEY_PGDN)) { + onKeyPress(); + FormField *focus = dynamic_cast(getFocus()); + if (focus != nullptr && focus->getNextField()) { + focus->getNextField()->setFocus(SET_FOCUS_FORWARD, focus); + } + } else if (event == EVT_KEY_FIRST(KEY_EXIT)) { + deleteLater(); + } else { + Window::onEvent(event); + } + } +#endif + + void deleteLater(bool detach = true, bool trash = true) override + { + if (_dirty) { + if (confirmationDialog("Save Theme?", _theme.getName())) { + if (saveHandler != nullptr) { + saveHandler(_theme); + } + } + } + + Page::deleteLater(detach, trash); + } + + void editColorPage() + { + auto colorEntry = _cList->getSelectedColor(); + new ColorEditPage(_theme, colorEntry.colorNumber, + [=] (uint32_t color) { + _dirty = true; + _theme.setColor(colorEntry.colorNumber, color); + _cList->setColorList(_theme.getColorList()); + _previewWindow->setColorList(_theme.getColorList()); + }); + } + + void buildHeader(FormGroup *window) + { + LcdFlags flags = 0; + if (LCD_W < LCD_H) { + flags = FONT(XS); + } + + // page title + new StaticText(window, + {PAGE_TITLE_LEFT, PAGE_TITLE_TOP, LCD_W - PAGE_TITLE_LEFT, + PAGE_LINE_HEIGHT}, + STR_EDIT_THEME, 0, COLOR_THEME_PRIMARY2 | flags); + _themeName = new StaticText(window, + {PAGE_TITLE_LEFT, PAGE_TITLE_TOP + PAGE_LINE_HEIGHT, + LCD_W - PAGE_TITLE_LEFT, PAGE_LINE_HEIGHT}, + _theme.getName(), 0, COLOR_THEME_PRIMARY2 | flags); + + // save and cancel + rect_t r = {LCD_W - (BUTTON_WIDTH + 5), 6, BUTTON_WIDTH, BUTTON_HEIGHT }; + _detailButton = new TextButton(window, r, STR_DETAILS, [=] () { + new ThemeDetailsDialog(page, _theme, [=] (ThemeFile t) { + _theme.setAuthor(t.getAuthor()); + _theme.setInfo(t.getInfo()); + _theme.setName(t.getName()); + + // update the theme name + _themeName->setText(_theme.getName()); + _dirty = true; + }); + return 0; + }, BUTTON_BACKGROUND | OPAQUE, textFont); + + // setup the prev next controls so save and details are in the mix + _cList->setNextField(_detailButton); + _cList->setPreviousField(_detailButton); + _detailButton->setNextField(_cList); + _detailButton->setPreviousField(_cList); + } + + void buildBody(FormGroup *window) + { + rect_t r = { 0, 4, COLOR_LIST_WIDTH, COLOR_LIST_HEIGHT}; + _cList = new ColorList(window, r, _theme.getColorList()); + _cList->setLongPressHandler([=] (event_t event) { editColorPage(); }); + _cList->setPressHandler([=] (event_t event) { editColorPage(); }); + + if (LCD_W > LCD_H) { + r = { COLOR_LIST_WIDTH + MARGIN_WIDTH, 4, LCD_W - COLOR_LIST_WIDTH - MARGIN_WIDTH*2, COLOR_LIST_HEIGHT }; + } else { + r = { 0, LEFT_LIST_HEIGHT + 4, LEFT_LIST_WIDTH, LEFT_LIST_HEIGHT - 4}; + } + _previewWindow = new PreviewWindow(window, r, _theme.getColorList()); + } + + protected: + bool _dirty = false; + ThemeFile _theme; + Page *page; + std::function saveHandler = nullptr; + PreviewWindow *_previewWindow = nullptr; + ColorList *_cList = nullptr; + StaticText *_themeName = nullptr; + TextButton *_saveButton; + TextButton *_detailButton; +}; + +ThemeSetupPage::ThemeSetupPage() : PageTab(STR_THEME_EDITOR, ICON_RADIO_EDIT_THEME) {} +ThemeSetupPage::~ThemeSetupPage() +{ +} + +void ThemeSetupPage::displayThemeMenu(Window *window, ThemePersistance *tp) +{ + auto menu = new Menu(listBox,false); + + // you cant activate the active theme + if (listBox->getSelected() != tp->getThemeIndex()) { + menu->addLine(STR_ACTIVATE, [=]() { + tp->applyTheme(listBox->getSelected()); + tp->setDefaultTheme(listBox->getSelected()); + nameText->setTextFlags(COLOR_THEME_PRIMARY1); + authorText->setTextFlags(COLOR_THEME_PRIMARY1); + listBox->setActiveIndex(tp->getThemeIndex()); + }); + } + + // you cant edit the default theme + if (listBox->getSelected() != 0) { + menu->addLine(STR_EDIT, + [=] () { + auto theme = tp->getThemeByIndex(currentTheme); + if (theme == nullptr) return; + + new ThemeEditPage(*theme, + [=](ThemeFile &theme) { + auto curTheme = tp->getThemeByIndex(currentTheme); + if (curTheme != nullptr) { + curTheme->setName(theme.getName()); + curTheme->setAuthor(theme.getAuthor()); + curTheme->setInfo(theme.getInfo()); + + // update the colors that were edited + int n = 0; + for (auto color : theme.getColorList()) { + curTheme->setColorByIndex(n, color.colorValue); + n++; + } + + // save it to disk so it will come back the next time. + curTheme->serialize(); + + // if the active theme was edited then activate it. + if (listBox->getSelected() == tp->getThemeIndex()) + tp->applyTheme(listBox->getSelected()); + + // the list of theme names might have changed + listBox->setNames(tp->getNames()); + } + }); + }); + } + + menu->addLine(STR_DUPLICATE, [=] () { + ThemeFile newTheme; + + new ThemeDetailsDialog(window, newTheme, [=] (ThemeFile theme) { + if (strlen(theme.getName()) != 0) { + char path[NAME_LENGTH + 20]; + strncpy(path, theme.getName(), NAME_LENGTH + 19); + removeAllWhiteSpace(path); + strncat(path, ".yml", NAME_LENGTH + 19); + theme.setPath(path); + + // use the current themes color list to make the new theme + auto curTheme = tp->getCurrentTheme(); + for (auto color : curTheme->getColorList()) + theme.setColor(color.colorNumber, color.colorValue); + + theme.serialize(); + tp->refresh(); + listBox->setNames(tp->getNames()); + listBox->setSelected(currentTheme); + } + }); + }); + + // you cant delete the default theme or the currently active theme + if (listBox->getSelected() != 0 && listBox->getSelected() != tp->getThemeIndex()) { + menu->addLine(STR_DELETE, [=] () { + if (confirmationDialog("Delete Theme?", tp->getThemeByIndex(listBox->getSelected())->getName())) { + tp->deleteThemeByIndex(listBox->getSelected()); + listBox->setNames(tp->getNames()); + listBox->setSelected(currentTheme); + } + }); + } +} + +void ThemeSetupPage::setupListbox(FormWindow *window, rect_t r, ThemePersistance *tp) +{ + listBox = new ListBox( + window, r, tp->getNames(), + [=]() { + return currentTheme; + }, + [=](uint8_t value) { + if (themeColorPreview && authorText && nameText && fileCarosell) { + ThemeFile *theme = tp->getThemeByIndex(value); + themeColorPreview->setColorList(theme->getColorList()); + authorText->setText(theme->getAuthor()); + nameText->setText(theme->getName()); + fileCarosell->setFileNames(theme->getThemeImageFileNames()); + } + currentTheme = value; + }); + listBox->setActiveIndex(tp->getThemeIndex()); + listBox->setTitle(STR_THEME + std::string("s")); + listBox->setLongPressHandler([=] (event_t event) { displayThemeMenu(window, tp); }); + listBox->setPressHandler([=] (event_t event) { displayThemeMenu(window, tp); }); +} + +void ThemeSetupPage::build(FormWindow *window) +{ + auto tp = ThemePersistance::instance(); + auto theme = tp->getCurrentTheme(); + currentTheme = tp->getThemeIndex(); + + themeColorPreview = nullptr; + listBox = nullptr; + fileCarosell = nullptr; + nameText = nullptr; + authorText = nullptr; + + // create listbox and setup menus + rect_t r = { 2, 3, LEFT_LIST_WIDTH, LEFT_LIST_HEIGHT }; + setupListbox(window, r, tp); + + if (LCD_W > LCD_H) { + r.x = LEFT_LIST_WIDTH + MARGIN_WIDTH; + r.w = LCD_W - r.x; + } else { + r.y += LEFT_LIST_HEIGHT + 4; + r.x = 0; + r.w = LCD_W; + } + + // setup ThemeColorPreview() + auto colorList = theme != nullptr ? theme->getColorList() : std::vector(); + rect_t colorPreviewRect = {LEFT_LIST_WIDTH + 6, 0, COLOR_PREVIEW_WIDTH, COLOR_PREVIEW_HEIGHT}; + themeColorPreview = new ThemeColorPreview(window, colorPreviewRect, colorList); + + // setup FileCarosell() + r.h = 130; + r.w -= COLOR_PREVIEW_WIDTH; + r.x += COLOR_PREVIEW_WIDTH; + auto fileNames = theme != nullptr ? theme->getThemeImageFileNames() : std::vector(); + fileCarosell = new FileCarosell(window, r, fileNames, listBox); + + // author and name of theme on right side of screen + r.x += 7; + r.y += 130; + r.h = 20; + nameText = new StaticText(window, r, theme != nullptr ? theme->getName() : "", 0, + COLOR_THEME_PRIMARY1); + if (theme != nullptr && strlen(theme->getAuthor())) { + r.y += 20; + char author[256]; + strcpy(author, "By: "); + strcat(author, theme->getAuthor()); + authorText = new StaticText(window, r, author, 0, COLOR_THEME_PRIMARY1); + } +} diff --git a/radio/src/gui/colorlcd/radio_theme.h b/radio/src/gui/colorlcd/radio_theme.h new file mode 100644 index 000000000..08407097d --- /dev/null +++ b/radio/src/gui/colorlcd/radio_theme.h @@ -0,0 +1,140 @@ +/* + * Copyright (C) EdgeTX + * + * Based on code named + * opentx - https://github.com/opentx/opentx + * th9x - http://code.google.com/p/th9x + * er9x - http://code.google.com/p/er9x + * gruvin9x - http://code.google.com/p/gruvin9x + * + * License GPLv2: http://www.gnu.org/licenses/gpl-2.0.html + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + */ +#include "tabsgroup.h" +#include "color_editor.h" +#include "file_preview.h" +#include "file_carosell.h" + +constexpr int BOX_MARGIN = 2; +constexpr int MAX_BOX_WIDTH = 15; + +class PageButton : public Window +{ + public: + PageButton(FormGroup *parent, const rect_t &rect, std::string text, + std::function setTab = nullptr, + WindowFlags windowFlags = BUTTON_BACKGROUND | OPAQUE, + LcdFlags textFlags = 0) : + Window(parent, rect, windowFlags | NO_FOCUS, textFlags), + _setTab(std::move(setTab)), + _text(text) + { + } + +#if defined(HARDWARE_TOUCH) + bool onTouchEnd(coord_t x, coord_t y) override + { + if (_setTab != nullptr) { + _setTab(); + } + + return true; + } +#endif + + void paint(BitmapBuffer *dc) override + { + uint32_t textColor = _checked ? COLOR_THEME_PRIMARY2 : COLOR_THEME_PRIMARY1; + dc->clear(_checked ? COLOR_THEME_ACTIVE : COLOR_THEME_SECONDARY2); + + dc->drawText(rect.w / 2, 1 + (rect.h - getFontHeight(textFlags)) / 2, + _text.c_str(), CENTERED | textColor); + } + + void check(bool checked) + { + _checked = checked; + invalidate(); + } + + private: + std::function _setTab; + bool _checked = false; + std::string _text; +}; + +class ThemeColorPreview : public FormField +{ + public: + ThemeColorPreview(Window *parent, const rect_t &rect, std::vector colorList) : + FormField(parent, rect, NO_FOCUS), + colorList(colorList) + { + setBoxWidth(); + } + ~ThemeColorPreview() + { + } + + void setColorList(std::vector colorList) + { + this->colorList.assign(colorList.begin(), colorList.end()); + setBoxWidth(); + invalidate(); + } + + void paint(BitmapBuffer *dc) override + { + int totalNessessarySpace = colorList.size() * (boxWidth + 2); + int axis = rect.w > rect.h ? rect.w : rect.h; + axis = (axis - totalNessessarySpace) / 2; + for (auto color: colorList) { + if (rect.w > rect.h) { + dc->drawSolidFilledRect(axis, 0, boxWidth, boxWidth, COLOR2FLAGS(color.colorValue)); + dc->drawSolidRect(axis, 0, boxWidth, boxWidth, 1, COLOR2FLAGS(BLACK)); + } else { + dc->drawSolidFilledRect(0, axis, boxWidth, boxWidth, COLOR2FLAGS(color.colorValue)); + dc->drawSolidRect(0, axis, boxWidth, boxWidth, 1, COLOR2FLAGS(BLACK)); + } + axis += boxWidth + BOX_MARGIN; + } + } + + protected: + std::vector colorList; + int boxWidth = MAX_BOX_WIDTH; + void setBoxWidth() + { + auto winSize = rect.w > rect.h ? rect.w : rect.h; + boxWidth = winSize / (colorList.size() + BOX_MARGIN); + boxWidth = min(boxWidth, MAX_BOX_WIDTH); + } +}; + + +class ThemeSetupPage: public PageTab { + public: + ThemeSetupPage(); + ~ThemeSetupPage(); + + void build(FormWindow * window) override; + + protected: + Window *previewWindow = nullptr; + FileCarosell *fileCarosell = nullptr; + ThemeColorPreview *themeColorPreview = nullptr; + ListBox *listBox = nullptr; + StaticText *authorText = nullptr; + StaticText *nameText = nullptr; + int currentTheme = 0; + void setupListbox(FormWindow *window, rect_t r, ThemePersistance *tp); + void displayThemeMenu(Window *window, ThemePersistance *tp); +}; diff --git a/radio/src/gui/colorlcd/screen_setup.cpp b/radio/src/gui/colorlcd/screen_setup.cpp index 5d7fbfb57..7a0bb04b3 100644 --- a/radio/src/gui/colorlcd/screen_setup.cpp +++ b/radio/src/gui/colorlcd/screen_setup.cpp @@ -18,7 +18,6 @@ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. */ - #include #include "screen_setup.h" #include "opentx.h" @@ -30,7 +29,10 @@ #include "theme_manager.h" #include "file_preview.h" -#define SET_DIRTY() storageDirty(EE_MODEL) +#define SET_DIRTY() storageDirty(EE_MODEL) +#define BUTTON_HEIGHT 30 +#define BUTTON_WIDTH 75 + class LayoutChoice: public FormField { @@ -261,15 +263,113 @@ class SetupWidgetsPage: public FormWindow // } }; +class ThemeEditorPage : public Page +{ + public: + explicit ThemeEditorPage(Window *parent, ThemeFile *theme, WindowFlags flags = 0) : + Page(0), + theme(theme) + { + buildBody(&body); + buildHeader(&header); + } + + void buildHeader(FormGroup *window) + { + ThemePersistance *tp = ThemePersistance::instance(); + + new StaticText(window, + {PAGE_TITLE_LEFT, PAGE_TITLE_TOP, LCD_W - PAGE_TITLE_LEFT, + PAGE_LINE_HEIGHT}, + "Edit Theme", 0, COLOR_THEME_PRIMARY2); + new StaticText(window, + {PAGE_TITLE_LEFT, PAGE_TITLE_TOP + PAGE_LINE_HEIGHT, + LCD_W - PAGE_TITLE_LEFT, PAGE_LINE_HEIGHT}, + theme->getName(), 0, COLOR_THEME_PRIMARY2); + + rect_t r = {LCD_W - (BUTTON_WIDTH + 5), static_cast(PAGE_TITLE_TOP + ((header.getRect().h - BUTTON_HEIGHT) / 2)), BUTTON_WIDTH, BUTTON_HEIGHT }; + new TextButton(window, r, "Save", [=] () { + theme->serialize(); + tp->refresh(); + tp->applyTheme(tp->getThemeIndex()); + body.clear(); + buildBody(&body); + return 0; + }); + } + + void buildBody(FormGroup *window) + { + FormGridLayout grid; + grid.spacer(8); + + new StaticText(window, grid.getLabelSlot(), "Name", 0, COLOR_THEME_PRIMARY1); + new TextEdit(window, grid.getFieldSlot(), theme->getName(), NAME_LENGTH); + grid.nextLine(); + new StaticText(window, grid.getLabelSlot(), "Author", 0, COLOR_THEME_PRIMARY1); + new TextEdit(window, grid.getFieldSlot(), theme->getAuthor(), AUTHOR_LENGTH); + grid.nextLine(); + new StaticText(window, grid.getLineSlot(), "Description", 0, COLOR_THEME_PRIMARY1); + grid.nextLine(); + new TextEdit(window, grid.getLineSlot(), theme->getInfo(), INFO_LENGTH); + grid.nextLine(); + + grid.spacer(10); + new StaticText(window, grid.getLineSlot(), "Colors", 0, COLOR_THEME_PRIMARY1 | FONT(BOLD)); + grid.nextLine(); + grid.spacer(10); + + char **colorNames = ThemePersistance::getColorNames(); + for (int i = 0; i < COLOR_COUNT; i++) { + char * name = colorNames[i]; + new StaticText(window, grid.getLabelSlot(), name, 0, COLOR_THEME_PRIMARY1); + + new ColorEdit(window, grid.getFieldSlot(), + [=] () { + return theme->getColorByName(name); + }, + [=] (uint16_t value) { + theme->setColor((LcdColorIndex)i, value); + }); + + grid.nextLine(); + } + + grid.spacer(20); + window->setInnerHeight(grid.getWindowHeight()); + } + + protected: + ThemeFile *theme; +}; + ScreenUserInterfacePage::ScreenUserInterfacePage(ScreenMenu* menu): PageTab(STR_USER_INTERFACE, ICON_THEME_SETUP), menu(menu) { } + +class MyFormGridLayout : public FormGridLayout +{ + public: + MyFormGridLayout(int width, int top) : + FormGridLayout(width, top) + { + } + + rect_t getFieldSlotEx(uint8_t count = 1, uint8_t index = 0, uint8_t number = 1) const + { + coord_t width = (this->width - labelWidth - lineMarginRight - (count - 1) * PAGE_LINE_SPACING) / count; + coord_t left = labelWidth + (width + PAGE_LINE_SPACING) * index; + return {left, currentY, width *number, PAGE_LINE_HEIGHT}; + } +}; + + void ScreenUserInterfacePage::build(FormWindow * window) { - FormGridLayout grid(LCD_W, 10); + MyFormGridLayout grid(LCD_W, 10); // Top Bar new StaticText(window, grid.getLabelSlot(), STR_TOP_BAR, 0, COLOR_THEME_PRIMARY1); @@ -289,7 +389,7 @@ void ScreenUserInterfacePage::build(FormWindow * window) auto tp = ThemePersistance::instance(); tp->refresh(); std::vector names = tp->getNames(); - new Choice(window, grid.getFieldSlot(), names, 0, names.size() - 1, + new Choice(window, grid.getFieldSlotEx(), names, 0, names.size() - 1, [=] () { return tp->getThemeIndex(); }, @@ -298,12 +398,24 @@ void ScreenUserInterfacePage::build(FormWindow * window) tp->applyTheme(value); tp->setDefaultTheme(value); + window->clear(); + build(window); + }); + + std::function launchEditor = [=]() { + auto theme = tp->getCurrentTheme(); + auto te = new ThemeEditorPage(window, theme); + te->setCloseHandler([=] () { window->clear(); build(window); }); + + return 0; + }; + + // new TextButton(window, grid.getFieldSlot(3, 2), STR_EDIT, launchEditor, BUTTON_BACKGROUND | OPAQUE, COLOR_THEME_PRIMARY2); bool bNarrowScreen = LCD_W < LCD_H; - if (bNarrowScreen) grid.setLabelWidth(LCD_W); @@ -328,7 +440,7 @@ void ScreenUserInterfacePage::build(FormWindow * window) new StaticText(window, r, info, 0, COLOR_THEME_PRIMARY1); rect_t previewRect = bNarrowScreen ? - rect_t {0, r.x + r.h, LCD_W- 12, window->height()} : + rect_t {0, r.h, LCD_W- 12, window->height()} : rect_t {LCD_W / 2 + 6, 30, LCD_W / 2 - 12, window->height()}; auto preview = new FilePreview(window, previewRect); preview->setFile(themeImage.c_str()); @@ -410,8 +522,6 @@ ScreenSetupPage::ScreenSetupPage(ScreenMenu * menu, unsigned pageIndex, unsigned { } - - static void updateLayoutOptions(FormGroup* optionsWindow, unsigned customScreenIndex) { FormGridLayout grid; diff --git a/radio/src/gui/colorlcd/str_functions.cpp b/radio/src/gui/colorlcd/str_functions.cpp index 078127d42..06c619cdc 100644 --- a/radio/src/gui/colorlcd/str_functions.cpp +++ b/radio/src/gui/colorlcd/str_functions.cpp @@ -26,6 +26,17 @@ static char splitChars[] = { ' ', '-', '\t' }; +void removeAllWhiteSpace(char *str) +{ + int len = strlen(str); + int origLen = len; + while(len) { + if (iswspace(str[len])) + memmove(str + len, str + len + 1, origLen - len); + len--; + } +} + char *trim(char *str) { char *end; diff --git a/radio/src/gui/colorlcd/str_functions.h b/radio/src/gui/colorlcd/str_functions.h index b96afc1e9..3574104e4 100644 --- a/radio/src/gui/colorlcd/str_functions.h +++ b/radio/src/gui/colorlcd/str_functions.h @@ -25,5 +25,6 @@ #include extern char *trim(char *str); +extern void removeAllWhiteSpace(char *str); extern std::string trim_start(std::string str); extern std::string wrap(std::string str, uint32_t width); diff --git a/radio/src/gui/colorlcd/theme_manager.cpp b/radio/src/gui/colorlcd/theme_manager.cpp index 35388a5ab..c57f2d4b1 100644 --- a/radio/src/gui/colorlcd/theme_manager.cpp +++ b/radio/src/gui/colorlcd/theme_manager.cpp @@ -18,27 +18,34 @@ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. */ - #include "theme_manager.h" -#include "theme.h" - -#include -#include +#define MAX_FILES 20 ThemePersistance themePersistance; -#define COLOR_COUNT 12 -static const char *conversionArray[COLOR_COUNT] = { +static const char *colorNames[COLOR_COUNT] = { "DEFAULT", "PRIMARY1", "PRIMARY2", "PRIMARY3", "SECONDARY1", "SECONDARY2", "SECONDARY3", "FOCUS", - "EDIT", "ACTIVE", "WARNING", "DISABLED", + "EDIT", "ACTIVE", "WARNING", "DISABLED", "CUSTOM", }; +#define HEX_COLOR_VALUE_LEN 8 constexpr const char *RGBSTRING = "RGB("; - - constexpr const char* SELECTED_THEME_FILE = THEMES_PATH "/selectedtheme.txt"; +ThemeFile& ThemeFile::operator= (const ThemeFile& theme) +{ + if (this == &theme) + return *this; + + path = theme.path; + strncpy(name, theme.name, NAME_LENGTH); + strncpy(author, theme.author, AUTHOR_LENGTH); + strncpy(info, theme.info, INFO_LENGTH); + colorList.assign(theme.colorList.begin(), theme.colorList.end()); + return *this; +}; + std::string ThemeFile::getThemeImageFileName() { char fullPath[FF_MAX_LFN + 1]; @@ -53,7 +60,65 @@ std::string ThemeFile::getThemeImageFileName() return ""; } -void ThemeFile::scanFile() +std::vector ThemeFile::getThemeImageFileNames() +{ + std::vector fileNames; + + char fullPath[FF_MAX_LFN + 1]; + strncpy(fullPath, THEMES_PATH "/", FF_MAX_LFN); + auto found = path.rfind('.'); + FIL file; + if (found != std::string::npos) { + int n = 0; + while (n < MAX_FILES) { + auto baseFileName(fullPath + path.substr(0, found) + (n != 0 ? std::to_string(n) : "") + ".png"); + FRESULT result = f_open(&file, baseFileName.c_str(), FA_OPEN_EXISTING); + if (result == FR_OK) { + fileNames.emplace_back(baseFileName); + f_close(&file); + } else { + break; + } + + n++; + } + } + + return fileNames; +} + +void ThemeFile::serialize() +{ + char fullPath[FF_MAX_LFN + 1]; + strncpy(fullPath, THEMES_PATH "/", FF_MAX_LFN); + strncat(fullPath, path.c_str(), FF_MAX_LFN); + + FIL file; + FRESULT result = f_open(&file, fullPath, FA_CREATE_ALWAYS | FA_WRITE); + if (result == FR_OK) { + f_printf(&file, "---\n"); + f_printf(&file, "summary:\n"); + f_printf(&file, " name: %s\n", name); + f_printf(&file, " author: %s\n", author); + f_printf(&file, " info: %s\n", info); + f_printf(&file, "\n"); + f_printf(&file, "colors:\n"); + + for (auto colorEntry : colorList) { + auto r = GET_RED(colorEntry.colorValue); + auto g = GET_GREEN(colorEntry.colorValue); + auto b = GET_BLUE(colorEntry.colorValue); + + std::string colorName(colorNames[colorEntry.colorNumber]); + colorName += ":"; + + f_printf(&file, " %-11s 0x%02X%02X%02X\n", colorName.c_str(), r,g,b); + } + f_close(&file); + } +} + +void ThemeFile::deSerialize() { char line[256 + 1]; char fullPath[FF_MAX_LFN + 1]; @@ -62,11 +127,12 @@ void ThemeFile::scanFile() strncpy(fullPath, THEMES_PATH "/", FF_MAX_LFN); strncat(fullPath, path.c_str(), FF_MAX_LFN); + FIL file; FRESULT result = f_open(&file, fullPath, FA_OPEN_EXISTING | FA_READ); if (result != FR_OK) return; int lineNo = 1; - while (readNextLine(line, 256)) { + while (readNextLine(file, line, 256)) { int len = strlen(line); if (len == 0) continue; @@ -98,7 +164,7 @@ void ThemeFile::scanFile() char *plvalue; char *prvalue; - strncpy(lvalue, line, std::min((int)(ptr - line), 63)); + strncpy(lvalue, line, min((int)(ptr - line), 63)); lvalue[ptr - line] = '\0'; strncpy(rvalue, ptr + 1, 63); plvalue = trim(lvalue); @@ -117,12 +183,16 @@ void ThemeFile::scanFile() } break; case summary: { - if (strcmp(plvalue, "name") == 0) - name = prvalue; - else if (strcmp(plvalue, "author") == 0) - author = prvalue; - else if (strcmp(plvalue, "info") == 0) - info = prvalue; + if (strcmp(plvalue, "name") == 0) { + strncpy(name, prvalue, NAME_LENGTH); + name[NAME_LENGTH] = '\0'; + } else if (strcmp(plvalue, "author") == 0) { + strncpy(author, prvalue, AUTHOR_LENGTH); + author[AUTHOR_LENGTH] = '\0'; + } else if (strcmp(plvalue, "info") == 0) { + strncpy(info, prvalue, INFO_LENGTH); + info[INFO_LENGTH] = '\0'; + } } break; } } @@ -133,8 +203,6 @@ void ThemeFile::scanFile() f_close(&file); } -#define HEX_COLOR_VALUE_LEN 8 - bool ThemeFile::convertRGB(char *pColorRGB, uint32_t &color) { if (strlen(pColorRGB) < strlen(RGBSTRING)) return false; @@ -189,7 +257,7 @@ LcdColorIndex ThemeFile::findColorIndex(const char *name) { int i; for (i = 0; i < COLOR_COUNT; i++) { - if (strcmp(name, conversionArray[i]) == 0) break; + if (strcmp(name, colorNames[i]) == 0) break; } if (i >= COLOR_COUNT) return DEFAULT_COLOR_INDEX; @@ -197,12 +265,11 @@ LcdColorIndex ThemeFile::findColorIndex(const char *name) return (LcdColorIndex)i; } -bool ThemeFile::readNextLine(char *line, int maxlen) +bool ThemeFile::readNextLine(FIL &file, char *line, int maxlen) { if (f_gets(line, maxlen, &file) != NULL) { int curlen = strlen(line) - 1; - if (line[curlen] == - '\n') { // remove unwanted chars if file was edited using windows + if (line[curlen] == '\n') { // remove unwanted chars if file was edited using windows if (line[curlen - 1] == '\r') { line[curlen - 1] = 0; } else { @@ -216,10 +283,21 @@ bool ThemeFile::readNextLine(char *line, int maxlen) return false; } +void ThemeFile::setColorByIndex(int index, uint32_t color) +{ + if (index >= 0 && index < (int) colorList.size()) { + colorList[index].colorValue = color; + } +} + void ThemeFile::setColor(LcdColorIndex colorIndex, uint32_t color) { if (colorIndex >= DEFAULT_COLOR_INDEX && colorIndex < LCD_COLOR_COUNT) { - colorList.emplace_back(ColorEntry {colorIndex, color}); + auto colorEntry = std::find(colorList.begin(), colorList.end(), ColorEntry { colorIndex, 0}); + if (colorEntry != colorList.end()) + colorEntry->colorValue = color; + else + colorList.emplace_back(ColorEntry {colorIndex, color}); } } @@ -231,13 +309,20 @@ void ThemeFile::applyTheme() OpenTxTheme::instance()->update(false); } -// not sure why this is needed... -constexpr unsigned MAX_SD_FILE_LENGTH = 64; +// avoid leaking memory +void ThemePersistance::clearThemes() +{ + for (auto theme: themes) { + delete theme; + } + themes.clear(); +} + +#define MAX_SD_FILE_LENGTH 128 void ThemePersistance::scanForThemes() { - TRACE("in scanForThemes"); - themes.clear(); + clearThemes(); DIR dir; FILINFO fno; @@ -259,15 +344,16 @@ void ThemePersistance::scanForThemes() if (res != FR_OK || fno.fname[0] == 0) break; // Break on error or end of dir - if (strlen((const char *)fno.fname) > MAX_SD_FILE_LENGTH) continue; + if (strlen((const char *)fno.fname) > SD_SCREEN_FILE_LENGTH) continue; if (fno.fattrib & AM_DIR) continue; TRACE("scanForThemes: found file %s", fno.fname); std::string fname(fno.fname); - auto found = fname.find('.'); + auto found = fname.find("."); if (found != std::string::npos) { if (strcasecmp(fname.substr(found).c_str(), YAML_EXT) != 0) continue; } + else continue; themes.emplace_back(new ThemeFile(fno.fname)); } @@ -278,6 +364,8 @@ void ThemePersistance::scanForThemes() void ThemePersistance::loadDefaultTheme() { + refresh(); + FIL file; FRESULT status = f_open(&file, SELECTED_THEME_FILE, FA_READ); if (status != FR_OK) return; @@ -287,7 +375,6 @@ void ThemePersistance::loadDefaultTheme() status = f_read(&file, line, 256, &len); if (status == FR_OK) { - refresh(); line[len] = '\0'; @@ -304,6 +391,38 @@ void ThemePersistance::loadDefaultTheme() f_close(&file); } +char ** ThemePersistance::getColorNames() +{ + return (char **) colorNames; +} + +bool ThemePersistance::deleteThemeByIndex(int index) +{ + // greater than 0 is intentional here. cant delete default theme. + if (index > 0 && index < (int) themes.size()) { + ThemeFile* theme = themes[index]; + + char curFile[FF_MAX_LFN + 1]; + strncpy(curFile, THEMES_PATH "/", FF_MAX_LFN); + strncat(curFile, theme->getPath().c_str(), FF_MAX_LFN); + + char newFile[FF_MAX_LFN + 1]; + strncpy(newFile, curFile, FF_MAX_LFN); + strcat(newFile, ".deleted"); + + // for now we are just renaming the file so we don't find it + FRESULT status = f_rename(curFile, newFile); + refresh(); + + // make sure currentTheme stays in bounds + if (getThemeIndex() >= (int) themes.size()) + setThemeIndex(themes.size() - 1); + + return status == FR_OK; + } + return false; +} + void ThemePersistance::deleteDefaultTheme() { FIL file; @@ -314,12 +433,15 @@ void ThemePersistance::deleteDefaultTheme() void ThemePersistance::setDefaultTheme(int index) { FIL file; - auto theme = themes[index]; - FRESULT status = f_open(&file, SELECTED_THEME_FILE, FA_CREATE_ALWAYS | FA_WRITE); - if (status != FR_OK) return; + if (index >= 0 && index < (int) themes.size()) { + auto theme = themes[index]; + FRESULT status = f_open(&file, SELECTED_THEME_FILE, FA_CREATE_ALWAYS | FA_WRITE); + if (status != FR_OK) return; - f_printf(&file, theme->getPath().c_str()); - f_close(&file); + currentTheme = index; + f_printf(&file, theme->getPath().c_str()); + f_close(&file); + } } class DefaultEdgeTxTheme : public ThemeFile @@ -330,32 +452,37 @@ class DefaultEdgeTxTheme : public ThemeFile setName("EdgeTX Default"); setAuthor("EdgeTX Team"); setInfo("Default EdgeTX Color Scheme"); + + // initializze the default color table + colorList.emplace_back(ColorEntry { COLOR_THEME_PRIMARY1_INDEX, RGB(0, 0, 0) }); + colorList.emplace_back(ColorEntry { COLOR_THEME_PRIMARY2_INDEX, RGB(255, 255, 255) }); + colorList.emplace_back(ColorEntry { COLOR_THEME_PRIMARY3_INDEX, RGB(12, 63, 102) }); + colorList.emplace_back(ColorEntry { COLOR_THEME_SECONDARY1_INDEX, RGB(18, 94, 153) }); + colorList.emplace_back(ColorEntry { COLOR_THEME_SECONDARY2_INDEX, RGB(182, 224, 242) }); + colorList.emplace_back(ColorEntry { COLOR_THEME_SECONDARY3_INDEX, RGB(228, 238, 242) }); + colorList.emplace_back(ColorEntry { COLOR_THEME_FOCUS_INDEX, RGB(20, 161, 229) }); + colorList.emplace_back(ColorEntry { COLOR_THEME_EDIT_INDEX, RGB(0, 153, 9) }); + colorList.emplace_back(ColorEntry { COLOR_THEME_ACTIVE_INDEX, RGB(255, 222, 0) }); + colorList.emplace_back(ColorEntry { COLOR_THEME_WARNING_INDEX, RGB(224, 0, 0) }); + colorList.emplace_back(ColorEntry { COLOR_THEME_DISABLED_INDEX, RGB(140, 140, 140) }); } void applyTheme() override { - lcdColorTable[DEFAULT_COLOR_INDEX] = RGB(18, 94, 153); - - lcdColorTable[COLOR_THEME_PRIMARY1_INDEX] = RGB(0, 0, 0); - lcdColorTable[COLOR_THEME_PRIMARY2_INDEX] = RGB(255, 255, 255); - lcdColorTable[COLOR_THEME_PRIMARY3_INDEX] = RGB(12, 63, 102); - lcdColorTable[COLOR_THEME_SECONDARY1_INDEX] = RGB(18, 94, 153); - lcdColorTable[COLOR_THEME_SECONDARY2_INDEX] = RGB(182, 224, 242); - lcdColorTable[COLOR_THEME_SECONDARY3_INDEX] = RGB(228, 238, 242); - lcdColorTable[COLOR_THEME_FOCUS_INDEX] = RGB(20, 161, 229); - lcdColorTable[COLOR_THEME_EDIT_INDEX] = RGB(0, 153, 9); - lcdColorTable[COLOR_THEME_ACTIVE_INDEX] = RGB(255, 222, 0); - lcdColorTable[COLOR_THEME_WARNING_INDEX] = RGB(224, 0, 0); - lcdColorTable[COLOR_THEME_DISABLED_INDEX] = RGB(140, 140, 140); - lcdColorTable[CUSTOM_COLOR_INDEX] = RGB(170, 85, 0); - - OpenTxTheme::instance()->update(false); + ThemeFile::applyTheme(); } std::string getThemeImageFileName() override { return "/THEMES/EdgeTX.png"; } + + std::vector getThemeImageFileNames() override + { + std::vector fileNames; + fileNames.emplace_back("/THEMES/EdgeTX.png"); + return fileNames; + } }; void ThemePersistance::insertDefaultTheme() diff --git a/radio/src/gui/colorlcd/theme_manager.h b/radio/src/gui/colorlcd/theme_manager.h index eb61624e8..6d21b7ed1 100644 --- a/radio/src/gui/colorlcd/theme_manager.h +++ b/radio/src/gui/colorlcd/theme_manager.h @@ -18,66 +18,116 @@ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. */ - #pragma once +#include -#include -#include -#include -#include +#include -#include "debug.h" -#include "sdcard.h" #include "colors.h" +#include "debug.h" +#include "opentx.h" +#include "sdcard.h" #include "str_functions.h" -// sdReadir: used to be in "sdcard.h" (another GPL violation...) -#include "libopenui_file.h" - class ThemePersistance; extern ThemePersistance themePersistance; +#define COLOR_COUNT 13 + struct ColorEntry { LcdColorIndex colorNumber; uint32_t colorValue; + + bool operator== (const ColorEntry &a) { return this->colorNumber == a.colorNumber; } }; + +#define AUTHOR_LENGTH 50 +#define INFO_LENGTH 255 +#define NAME_LENGTH 26 class ThemeFile { public: + ThemeFile() { + name[0] = '\0'; + author[0] = '\0'; + info[0] = '\0'; + } + ThemeFile(std::string themePath) : path(themePath) { if (themePath.size()) { - scanFile(); + deSerialize(); } } + ThemeFile(const ThemeFile &theme) + { + path = theme.path; + strncpy(name, theme.name, NAME_LENGTH); + name[NAME_LENGTH] = '\0'; + strncpy(author, theme.author, AUTHOR_LENGTH); + author[AUTHOR_LENGTH] = '\0'; + strncpy(info, theme.info, INFO_LENGTH); + info[INFO_LENGTH] = '\0'; + colorList.assign(theme.colorList.begin(), theme.colorList.end()); + } + virtual ~ThemeFile() + { + } + + ThemeFile& operator= (const ThemeFile& theme); + + void serialize(); std::string getPath() { return path; } - std::string getName() { return name; } - std::string getAuthor() { return author; } - std::string getInfo() { return info; } - - void setName(std::string name) { this->name = name; } - void setAuthor(std::string author) { this->author = author; } - void setInfo(std::string info) { this->info = info; } + char *getName() { return name; } + char *getAuthor() { return author; } + char *getInfo() { return info; } - std::vector getColorList() { return colorList; } + ColorEntry *getColorEntryByIndex(LcdColorIndex colorNumber) { + int n = 0; + for (auto colorEntry : colorList) { + if (colorEntry.colorNumber == colorNumber) + return &colorList[n]; + n++; + } + + return nullptr; + } + + uint32_t getColorByName(std::string colorName) { + + auto colorIndex = findColorIndex(colorName.c_str()); + ColorEntry a = {colorIndex, 0}; + auto colorEntry = std::find(colorList.begin(), colorList.end(), a); + if (colorEntry != colorList.end()) + return colorEntry->colorValue; + + return 0; + } + + void setName(std::string name) { strncpy(this->name, name.c_str(), NAME_LENGTH); } + void setAuthor(std::string author) { strncpy(this->author, author.c_str(), AUTHOR_LENGTH); } + void setInfo(std::string info) { strncpy(this->info, info.c_str(), INFO_LENGTH); } + void setPath(std::string path) { this->path = path; } + + std::vector& getColorList() { return colorList; } void setColor(LcdColorIndex colorIndex, uint32_t color); + void setColorByIndex(int index, uint32_t color); virtual std::string getThemeImageFileName(); + virtual std::vector getThemeImageFileNames(); virtual void applyTheme(); protected: - FIL file; std::string path; - std::string name; - std::string author; - std::string info; + char name[NAME_LENGTH + 1]; + char author[AUTHOR_LENGTH + 1]; + char info[INFO_LENGTH + 1]; std::vector colorList; - enum ScanState { none, @@ -85,10 +135,10 @@ class ThemeFile colors }; - void scanFile(); + void deSerialize(); bool convertRGB(char *pColorRGB, uint32_t &color); LcdColorIndex findColorIndex(const char *name); - bool readNextLine(char * line, int maxlen); + bool readNextLine(FIL &file, char *line, int maxlen); }; @@ -106,6 +156,8 @@ class ThemePersistance void loadDefaultTheme(); void setDefaultTheme(int index); void deleteDefaultTheme(); + static char **getColorNames(); + bool deleteThemeByIndex(int index); std::vector getNames() { @@ -119,14 +171,28 @@ class ThemePersistance void applyTheme(int index) { - auto theme = themes[index]; + if (index >= 0 && index < (int) themes.size()) { + auto theme = themes[index]; theme->applyTheme(); + } } inline int getThemeIndex() {return currentTheme;} inline void setThemeIndex(int index) { currentTheme = index;} - inline ThemeFile* getCurrentTheme() { return themes[currentTheme]; } + inline ThemeFile* getCurrentTheme() + { + if (currentTheme < (int)themes.size()) + return themes[currentTheme]; + return nullptr; + } + + inline ThemeFile* getThemeByIndex(int index) + { + if (index < (int) themes.size()) + return themes[index]; + return nullptr; + } void refresh() { @@ -139,4 +205,5 @@ class ThemePersistance int currentTheme = 0; void scanForThemes(); void insertDefaultTheme(); + void clearThemes(); }; diff --git a/radio/src/gui/colorlcd/themes/480_bitmaps.h b/radio/src/gui/colorlcd/themes/480_bitmaps.h index 56af8588d..8a7ec52e2 100644 --- a/radio/src/gui/colorlcd/themes/480_bitmaps.h +++ b/radio/src/gui/colorlcd/themes/480_bitmaps.h @@ -160,6 +160,10 @@ const uint8_t mask_radio_setup[] = { const uint8_t mask_radio_tools[] = { #include "mask_radio_tools.lbm" }; +const uint8_t mask_radio_edit_theme[] = { +#include "mask_radio_edit_theme.lbm" +}; + const uint8_t mask_radio_trainer[] = { #include "mask_radio_trainer.lbm" }; diff --git a/radio/src/gui/colorlcd/themes/480_default.cpp b/radio/src/gui/colorlcd/themes/480_default.cpp index 77d3e8e2f..cf29f0c44 100644 --- a/radio/src/gui/colorlcd/themes/480_default.cpp +++ b/radio/src/gui/colorlcd/themes/480_default.cpp @@ -109,6 +109,7 @@ class Theme480: public OpenTxTheme loadMenuIcon(ICON_RADIO_TRAINER, mask_radio_trainer, reload); loadMenuIcon(ICON_RADIO_HARDWARE, mask_radio_hardware, reload); loadMenuIcon(ICON_RADIO_CALIBRATION, mask_radio_calibration, reload); + loadMenuIcon(ICON_RADIO_EDIT_THEME, mask_radio_edit_theme, reload); loadMenuIcon(ICON_RADIO_VERSION, mask_radio_version, reload); loadMenuIcon(ICON_MODEL, mask_menu_model, reload); loadMenuIcon(ICON_MODEL_SETUP, mask_model_setup, reload); diff --git a/radio/src/targets/horus/libopenui_config.h b/radio/src/targets/horus/libopenui_config.h index f99613872..5122df8f2 100644 --- a/radio/src/targets/horus/libopenui_config.h +++ b/radio/src/targets/horus/libopenui_config.h @@ -68,6 +68,8 @@ constexpr uint32_t MENU_HEADER_BACK_BUTTON_HEIGHT = MENU_HEADER_HEIGHT; constexpr coord_t PAGE_PADDING = 6; constexpr uint32_t PAGE_LINE_HEIGHT = 20; +constexpr coord_t LINE_HEIGHT = PAGE_LINE_HEIGHT; + constexpr uint32_t PAGE_LINE_SPACING = 2; constexpr uint32_t PAGE_INDENT_WIDTH = 10; constexpr uint32_t PAGE_LABEL_WIDTH = 240; diff --git a/radio/src/targets/nv14/libopenui_config.h b/radio/src/targets/nv14/libopenui_config.h index 31783a95f..c9e4ba7f2 100644 --- a/radio/src/targets/nv14/libopenui_config.h +++ b/radio/src/targets/nv14/libopenui_config.h @@ -88,6 +88,7 @@ constexpr uint32_t DATETIME_SEPARATOR_X = LCD_W - 53; constexpr uint32_t DATETIME_LINE1 = 7; constexpr uint32_t DATETIME_LINE2 = 22; constexpr uint32_t DATETIME_MIDDLE = (LCD_W + DATETIME_SEPARATOR_X + 1) / 2; +constexpr coord_t LINE_HEIGHT = PAGE_LINE_HEIGHT; constexpr uint32_t RSSI_X = LCD_W - 80; constexpr uint32_t AUDIO_X = LCD_W - 115; diff --git a/radio/src/translations.cpp b/radio/src/translations.cpp index d579051df..e6d64c86f 100644 --- a/radio/src/translations.cpp +++ b/radio/src/translations.cpp @@ -944,3 +944,18 @@ const char STR_RFPOWER_AFHDS2[] = TR_RFPOWER_AFHDS2; #endif const char STR_USE_THEME_COLOR[] = TR_USE_THEME_COLOR; + +const char STR_DUPLICATE[] = TR_DUPLICATE; +const char STR_ACTIVATE[] = TR_ACTIVATE; +const char STR_RED[] = TR_RED; +const char STR_GREEN[] = TR_GREEN; +const char STR_BLUE[] = TR_BLUE; +const char STR_COLOR_PICKER[] = TR_COLOR_PICKER; +const char STR_EDIT_THEME_DETAILS[] = TR_EDIT_THEME_DETAILS; +const char STR_AUTHOR[] = TR_AUTHOR; +const char STR_DESCRIPTION[] = TR_DESCRIPTION; +const char STR_SAVE[] = TR_SAVE; +const char STR_CANCEL[] = TR_CANCEL; +const char STR_EDIT_THEME[] = TR_EDIT_THEME; +const char STR_DETAILS[] = TR_DETAILS; +const char STR_THEME_EDITOR[] = TR_THEME_EDITOR; diff --git a/radio/src/translations.h b/radio/src/translations.h index 57e43b455..53a6ffa34 100644 --- a/radio/src/translations.h +++ b/radio/src/translations.h @@ -1140,6 +1140,21 @@ extern const char STR_RACING_MODE[]; extern const char STR_USE_THEME_COLOR[]; +extern const char STR_DUPLICATE[]; +extern const char STR_ACTIVATE[]; +extern const char STR_RED[]; +extern const char STR_BLUE[]; +extern const char STR_GREEN[]; +extern const char STR_COLOR_PICKER[]; +extern const char STR_EDIT_THEME_DETAILS[]; +extern const char STR_AUTHOR[]; +extern const char STR_DESCRIPTION[]; +extern const char STR_SAVE[]; +extern const char STR_CANCEL[]; +extern const char STR_EDIT_THEME[]; +extern const char STR_DETAILS[]; +extern const char STR_THEME_EDITOR[]; + #define CHR_HOUR TR_CHR_HOUR #define CHR_INPUT TR_CHR_INPUT diff --git a/radio/src/translations/untranslated.h b/radio/src/translations/untranslated.h index 567102751..e394427b3 100644 --- a/radio/src/translations/untranslated.h +++ b/radio/src/translations/untranslated.h @@ -329,3 +329,18 @@ #define CHAR_LUA STR_CHAR_LUA[0] #define TR_USE_THEME_COLOR "Use theme color" + +#define TR_DUPLICATE "Duplicate" +#define TR_ACTIVATE "Set Active" +#define TR_RED "Red" +#define TR_BLUE "Blue" +#define TR_GREEN "Green" +#define TR_COLOR_PICKER "Color Picker" +#define TR_EDIT_THEME_DETAILS "Edit Theme Details" +#define TR_AUTHOR "Author" +#define TR_DESCRIPTION "Description" +#define TR_SAVE "Save" +#define TR_CANCEL "Cancel" +#define TR_EDIT_THEME "EDIT THEME" +#define TR_DETAILS "Details" +#define TR_THEME_EDITOR "THEMES"