From 0018e4e9dbacdced0e54e24d270bdbe5f791cf47 Mon Sep 17 00:00:00 2001 From: Lucky Resistor Date: Sun, 6 Jun 2021 13:29:22 +0200 Subject: [PATCH] Adding a framework to generate code for fonts. - Work in progress to add a framework generating bitmap fonts. --- ApplicationController.cpp | 67 +++++++ ApplicationController.hpp | 57 ++++++ BitmapConverter.cpp | 29 +++ BitmapConverter.hpp | 45 +++++ BitmapPanel.cpp | 77 +++++++- BitmapPanel.hpp | 32 +++- BitmapPreview.cpp | 31 +++- BitmapPreview.hpp | 10 + Converter.cpp | 10 +- Converter.hpp | 25 ++- ConverterFramebuf.cpp | 2 +- ConverterFramebuf.hpp | 4 +- ConverterFramebufMono.cpp | 109 +---------- ConverterFramebufMono.hpp | 40 +--- ConverterFramebufMonoHLSB.cpp | 4 +- ConverterFramebufMonoHMSB.cpp | 4 +- ConverterFramebufMonoVLSB.cpp | 4 +- FontConverter.cpp | 149 +++++++++++++++ FontConverter.hpp | 58 ++++++ FontConverterFramebufMono.cpp | 52 ++++++ FontConverterFramebufMono.hpp | 39 ++++ MainWindow.cpp | 334 +++++++++++++++++++++++++++++----- MainWindow.hpp | 60 +++++- MicropythonBitmapTool.pro | 22 ++- MonoTools.cpp | 134 ++++++++++++++ MonoTools.hpp | 83 +++++++++ ParameterDefinition.cpp | 81 +++++++++ ParameterDefinition.hpp | 73 ++++++++ ParameterEntry.cpp | 65 +++++++ ParameterEntry.hpp | 74 ++++++++ ParameterFactory.cpp | 40 ++++ ParameterFactory.hpp | 37 ++++ ParameterType.hpp | 28 +++ ParameterWidget.cpp | 173 ++++++++++++++++++ ParameterWidget.hpp | 114 ++++++++++++ data/AppLogo.png | Bin 8789 -> 4928 bytes data/AppLogo@2x.png | Bin 19068 -> 9714 bytes data/AppLogo@3x.png | Bin 30997 -> 14774 bytes data/application.css | 10 +- main.cpp | 10 +- 40 files changed, 1962 insertions(+), 224 deletions(-) create mode 100644 ApplicationController.cpp create mode 100644 ApplicationController.hpp create mode 100644 BitmapConverter.cpp create mode 100644 BitmapConverter.hpp create mode 100644 FontConverter.cpp create mode 100644 FontConverter.hpp create mode 100644 FontConverterFramebufMono.cpp create mode 100644 FontConverterFramebufMono.hpp create mode 100644 MonoTools.cpp create mode 100644 MonoTools.hpp create mode 100644 ParameterDefinition.cpp create mode 100644 ParameterDefinition.hpp create mode 100644 ParameterEntry.cpp create mode 100644 ParameterEntry.hpp create mode 100644 ParameterFactory.cpp create mode 100644 ParameterFactory.hpp create mode 100644 ParameterType.hpp create mode 100644 ParameterWidget.cpp create mode 100644 ParameterWidget.hpp diff --git a/ApplicationController.cpp b/ApplicationController.cpp new file mode 100644 index 0000000..efd2c7b --- /dev/null +++ b/ApplicationController.cpp @@ -0,0 +1,67 @@ +// +// (c)2021 by Lucky Resistor. https://luckyresistor.me/ +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// 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. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// +#include "ApplicationController.hpp" + + +ApplicationController *ApplicationController::_instance = nullptr; + + +ApplicationController::ApplicationController(QObject *parent) + : QObject(parent) +{ + _instance = this; + + // Start as soon the main loop is running. + QMetaObject::invokeMethod(this, &ApplicationController::start, Qt::QueuedConnection); +} + + +QFont ApplicationController::monospaceFont() const +{ + QFont monospaceFont; +#ifdef Q_OS_WIN32 + monospaceFont = QFont("Consolas", 12); +#else +#ifdef Q_OS_MAC + monospaceFont = QFont("Menlo", 12); +#else + monospaceFont = QFont("Lucida Console", 12); +#endif +#endif + return monospaceFont; +} + + +void ApplicationController::start() +{ + _mainWindow = new MainWindow(); + _mainWindow->show(); +} + + +ApplicationController *ApplicationController::instance() +{ + return _instance; +} + + +ApplicationController* gApp() +{ + return ApplicationController::instance(); +} + + diff --git a/ApplicationController.hpp b/ApplicationController.hpp new file mode 100644 index 0000000..be7700e --- /dev/null +++ b/ApplicationController.hpp @@ -0,0 +1,57 @@ +// +// (c)2021 by Lucky Resistor. https://luckyresistor.me/ +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// 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. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// +#pragma once + + +#include "MainWindow.hpp" + +#include + + +/// The application controller +/// +class ApplicationController : public QObject +{ + Q_OBJECT + +public: + /// ctor + /// + explicit ApplicationController(QObject *parent = nullptr); + +public: + /// Get the default monospaced font. + /// + QFont monospaceFont() const; + +public: + /// Get the global instance. + /// + static ApplicationController* instance(); + +private: + /// Start the application. + /// + Q_SLOT void start(); + +private: + static ApplicationController *_instance; ///< The controller instance. + MainWindow *_mainWindow; ///< The main window. +}; + + +ApplicationController* gApp(); diff --git a/BitmapConverter.cpp b/BitmapConverter.cpp new file mode 100644 index 0000000..f15f3c5 --- /dev/null +++ b/BitmapConverter.cpp @@ -0,0 +1,29 @@ +// +// (c)2021 by Lucky Resistor. https://luckyresistor.me/ +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// 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. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// +#include "BitmapConverter.hpp" + + +BitmapConverter::BitmapConverter(const QString &displayName) + : Converter(displayName) +{ +} + + +Converter::Mode BitmapConverter::mode() const +{ + return Mode::Bitmap; +} diff --git a/BitmapConverter.hpp b/BitmapConverter.hpp new file mode 100644 index 0000000..757e712 --- /dev/null +++ b/BitmapConverter.hpp @@ -0,0 +1,45 @@ +// +// (c)2021 by Lucky Resistor. https://luckyresistor.me/ +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// 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. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// +#pragma once + + +#include "Converter.hpp" + + +/// The base class for all bitmap converters. +/// +class BitmapConverter : public Converter +{ +public: + /// ctor + /// + BitmapConverter(const QString &displayName); + +public: // implement Converter + Mode mode() const override; + +public: + /// Generate the code from the given image. + /// + /// @param image The image to convert. + /// @param parameter A map with parameters passed to this converter. + /// @return The generated code. + /// + virtual QString generateCode(const QImage &image, const QVariantMap ¶meter) const = 0; +}; + + diff --git a/BitmapPanel.cpp b/BitmapPanel.cpp index e6db3ab..f71e71a 100644 --- a/BitmapPanel.cpp +++ b/BitmapPanel.cpp @@ -17,16 +17,19 @@ #include "BitmapPanel.hpp" +#include "ApplicationController.hpp" #include "Converter.hpp" #include "BitmapPreview.hpp" -#include -#include -#include -#include +#include +#include +#include +#include +#include -BitmapPanel::BitmapPanel(QWidget *parent) : QWidget(parent) +BitmapPanel::BitmapPanel(QWidget *parent) + : QWidget(parent), _converter(nullptr) { initializeUi(); } @@ -35,16 +38,49 @@ BitmapPanel::BitmapPanel(QWidget *parent) : QWidget(parent) void BitmapPanel::setImage(const QImage &image) { _bitmapPreview->setImage(image); + _bitmapPreview->setFixedSize(_bitmapPreview->sizeHint()); } void BitmapPanel::setConverter(const Converter *converter) { - _bitmapPreview->setConverter(converter); + if (_converter != converter) { + _converter = converter; + _bitmapPreview->setConverter(converter); + _bitmapPreview->setFixedSize(_bitmapPreview->sizeHint()); + _characterSelector->setVisible(_converter->mode() == Converter::Mode::Font); + } +} + + +void BitmapPanel::setCharacters(const QString &characters) +{ + if (_characters != characters) { + _characters = characters; + _characterSelector->clear(); + for (int i = 0; i < characters.size(); ++i) { + const auto c = _characters.at(i); + _characterSelector->addItem(QString("%1: %2 0x%3").arg(i, 3, 10, QChar('0')) + .arg(QString(c)).arg(c.unicode(), 4, 16, QChar('0')), c); + } + _characterSelector->setCurrentIndex(0); + } +} + + +void BitmapPanel::setParameter(const QVariantMap ¶meter) +{ + _bitmapPreview->setParameter(parameter); _bitmapPreview->setFixedSize(_bitmapPreview->sizeHint()); } +QChar BitmapPanel::selectedCharacter() const +{ + return _characterSelector->currentData().toChar(); +} + + void BitmapPanel::initializeUi() { setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); @@ -72,6 +108,35 @@ void BitmapPanel::initializeUi() auto previewSettingsLayout = new QHBoxLayout(previewSettingsPanel); previewSettingsLayout->setContentsMargins(0, 0, 0, 0); previewSettingsLayout->setSpacing(4); + + _fontConversionTools = new QFrame(); + auto fontConversionLayout = new QHBoxLayout(_fontConversionTools); + fontConversionLayout->setContentsMargins(0, 0, 0, 0); + fontConversionLayout->setSpacing(2); + previewSettingsLayout->addWidget(_fontConversionTools); + + auto previousCharButton = new QPushButton("<"); + fontConversionLayout->addWidget(previousCharButton); + _characterSelector = new QComboBox(); + _characterSelector->setFont(gApp()->monospaceFont()); + fontConversionLayout->addWidget(_characterSelector); + auto nextCharButton = new QPushButton(">"); + fontConversionLayout->addWidget(nextCharButton); + connect(_characterSelector, &QComboBox::currentIndexChanged, [=]{ + Q_EMIT selectedCharacterChanged(_characterSelector->currentData().toChar()); + }); + connect(previousCharButton, &QPushButton::clicked, [=]{ + auto currentIndex = _characterSelector->currentIndex(); + if (currentIndex > 0) { + _characterSelector->setCurrentIndex(currentIndex - 1); + } + }); + connect(nextCharButton, &QPushButton::clicked, [=]{ + auto currentIndex = _characterSelector->currentIndex(); + if (currentIndex < (_characterSelector->count()-1)) { + _characterSelector->setCurrentIndex(currentIndex + 1); + } + }); previewSettingsLayout->addStretch(); previewSettingsLayout->addWidget(new QLabel(tr("Overlay Mode:"))); _overlaySelector = new QComboBox(); diff --git a/BitmapPanel.hpp b/BitmapPanel.hpp index d51e3fb..f30a7f0 100644 --- a/BitmapPanel.hpp +++ b/BitmapPanel.hpp @@ -23,13 +23,18 @@ class BitmapPreview; class Converter; class QComboBox; +class QFrame; +/// A panel with the scrollable bitmap preview. +/// class BitmapPanel : public QWidget { Q_OBJECT public: + /// Create the panel. + /// explicit BitmapPanel(QWidget *parent = nullptr); public: @@ -41,11 +46,34 @@ public: /// void setConverter(const Converter *converter); -private: - void initializeUi(); + /// Set the current character set. + /// + void setCharacters(const QString &characters); + + /// Set the current parameter. + /// + void setParameter(const QVariantMap ¶meter); + + /// Get the current selected character + /// + QChar selectedCharacter() const; private: + /// Initialize all UI elements. + /// + void initializeUi(); + +Q_SIGNALS: + /// Emitted if the selected character changes. + /// + void selectedCharacterChanged(QChar c); + +private: + const Converter *_converter; ///< The current converter. + QString _characters; ///< The list of characters to convert. BitmapPreview *_bitmapPreview; ///< The bitmap preview; + QFrame *_fontConversionTools; ///< The area with tools for font conversion. + QComboBox *_characterSelector; ///< The selector for a single character. QComboBox *_overlaySelector; ///< The selector for the overlays. }; diff --git a/BitmapPreview.cpp b/BitmapPreview.cpp index 6080adb..e8c88ba 100644 --- a/BitmapPreview.cpp +++ b/BitmapPreview.cpp @@ -48,9 +48,7 @@ void BitmapPreview::setImage(const QImage &image) if (_image != image) { _image = image; _pixmap = QPixmap::fromImage(image); - if (_converter != nullptr) { - setGeneratedSize(_converter->generatedSize(_image.size())); - } + updateGeneratedSize(); recalculate(); update(); } @@ -80,9 +78,18 @@ void BitmapPreview::setConverter(const Converter *converter) { if (_converter != converter) { _converter = converter; - if (_converter != nullptr) { - setGeneratedSize(_converter->generatedSize(_image.size())); - } + updateGeneratedSize(); + update(); + } +} + + +void BitmapPreview::setParameter(const QVariantMap ¶meter) +{ + if (_parameter != parameter) { + _parameter = parameter; + updateGeneratedSize(); + recalculate(); update(); } } @@ -118,7 +125,7 @@ void BitmapPreview::paintEvent(QPaintEvent *pe) p.setOpacity(1.0); p.translate(x, y); OverlayPainter op(&p, pe->rect(), _displayFactor, _pixmap.size(), _generatedSize); - _converter->paintOverlay(op, _overlayMode, _image); + _converter->paintOverlay(op, _overlayMode, _image, _parameter); } } else { p.setPen(Qt::white); @@ -134,6 +141,16 @@ void BitmapPreview::resizeEvent(QResizeEvent *e) } +void BitmapPreview::updateGeneratedSize() +{ + if (_converter != nullptr && _converter->mode() == Converter::Mode::Bitmap) { + setGeneratedSize(_converter->generatedSize(_image.size(), _parameter)); + } else { + setGeneratedSize(_image.size()); + } +} + + void BitmapPreview::recalculate() { if (_pixmap.isNull()) { diff --git a/BitmapPreview.hpp b/BitmapPreview.hpp index c8b39d3..8b32032 100644 --- a/BitmapPreview.hpp +++ b/BitmapPreview.hpp @@ -22,6 +22,7 @@ #include #include #include +#include class Converter; @@ -49,6 +50,10 @@ public: /// void setConverter(const Converter *converter); + /// Set the current parameter. + /// + void setParameter(const QVariantMap ¶meter); + public: QSize sizeHint() const override; QSize minimumSizeHint() const override; @@ -58,6 +63,10 @@ protected: // QWidget interface void resizeEvent(QResizeEvent *event) override; private: + /// update the generated size + /// + void updateGeneratedSize(); + /// Recalculate all sizes /// void recalculate(); @@ -74,5 +83,6 @@ private: QSize _minimumSize; ///< The preferred size of this widget. OverlayMode _overlayMode; ///< The overlay mode. const Converter *_converter; ///< The current converter. + QVariantMap _parameter; ///< The current set of parameter. }; diff --git a/Converter.cpp b/Converter.cpp index 48db2f8..f15f825 100644 --- a/Converter.cpp +++ b/Converter.cpp @@ -29,12 +29,18 @@ QString Converter::displayName() const } -QSize Converter::generatedSize(const QSize &imageSize) const +QSize Converter::generatedSize(const QSize &imageSize, const QVariantMap&) const { return imageSize; } +ParameterDefinitionPtr Converter::createParameterDefinition() const +{ + return ParameterDefinition::create(); +} + + LegendDataPtr Converter::legendData(OverlayMode) const { auto ld = LegendData::create(); @@ -43,7 +49,7 @@ LegendDataPtr Converter::legendData(OverlayMode) const } -void Converter::paintOverlay(OverlayPainter &op, OverlayMode, const QImage&) const +void Converter::paintOverlay(OverlayPainter &op, OverlayMode, const QImage&, const QVariantMap&) const { op.drawPixelOutline(op.imageRect(), colorBitmapSizeOriginal, 1, 2); if (op.generatedSize().isValid() && !op.generatedSize().isEmpty()) { diff --git a/Converter.hpp b/Converter.hpp index c0cd353..fe0a188 100644 --- a/Converter.hpp +++ b/Converter.hpp @@ -20,6 +20,7 @@ #include "LegendData.hpp" #include "OverlayPainter.hpp" #include "OverlayMode.hpp" +#include "ParameterDefinition.hpp" #include #include @@ -30,10 +31,17 @@ /// class Converter { +public: + enum class Mode { + Bitmap, + Font + }; + public: /// Create a new converter. /// /// @param displayName The name displayed in the UI for this coonverter. + /// @param mode The converter mode. /// Converter(const QString &displayName); @@ -46,21 +54,21 @@ public: /// QString displayName() const; + /// Get the mode for this converter. + /// + virtual Mode mode() const = 0; + public: /// Return the size of the generated bitmap data. /// /// @param imageSize The size of the image. /// @return The size of the generated bitmap data. /// - virtual QSize generatedSize(const QSize &imageSize) const; + virtual QSize generatedSize(const QSize &imageSize, const QVariantMap ¶meter) const; - /// Generate the code from the given image. + /// Create the parameter definition for the converter. /// - /// @param image The image to convert. - /// @param parameter A map with parameters passed to this converter. - /// @return The generated code. - /// - virtual QString generateCode(const QImage &image, const QVariantMap ¶meter) const = 0; + virtual ParameterDefinitionPtr createParameterDefinition() const; /// Return a legend for the bitmap preview. /// @@ -71,8 +79,9 @@ public: /// @param p The painter. /// @param mode The overlay mode (never `None`). /// @param image The image. + /// @param parameter The current set of parameter. /// - virtual void paintOverlay(OverlayPainter &p, OverlayMode mode, const QImage &image) const; + virtual void paintOverlay(OverlayPainter &p, OverlayMode mode, const QImage &image, const QVariantMap ¶meter) const; protected: /// The color for the image frame. diff --git a/ConverterFramebuf.cpp b/ConverterFramebuf.cpp index 058e900..173744d 100644 --- a/ConverterFramebuf.cpp +++ b/ConverterFramebuf.cpp @@ -21,7 +21,7 @@ ConverterFramebuf::ConverterFramebuf(const QString &displayName) - : Converter(displayName) + : BitmapConverter(displayName) { } diff --git a/ConverterFramebuf.hpp b/ConverterFramebuf.hpp index 8a261fc..58d1e7a 100644 --- a/ConverterFramebuf.hpp +++ b/ConverterFramebuf.hpp @@ -17,12 +17,12 @@ #pragma once -#include "Converter.hpp" +#include "BitmapConverter.hpp" /// Base class of framebuf converters. /// -class ConverterFramebuf : public Converter +class ConverterFramebuf : public BitmapConverter { public: ConverterFramebuf(const QString &displayName); diff --git a/ConverterFramebufMono.cpp b/ConverterFramebufMono.cpp index faa0496..ab6cc7b 100644 --- a/ConverterFramebufMono.cpp +++ b/ConverterFramebufMono.cpp @@ -25,124 +25,29 @@ ConverterFramebufMono::ConverterFramebufMono( int unitSize) : ConverterFramebuf(displayName), - _unitOrientation(unitOrientation), - _bitDirection(bitDirection), - _unitSize(unitSize) + MonoTools(unitOrientation, bitDirection, unitSize) { } -QSize ConverterFramebufMono::generatedSize(const QSize &imageSize) const +QSize ConverterFramebufMono::generatedSize(const QSize &imageSize, const QVariantMap ¶meter) const { - if (_unitOrientation == UnitOrientation::Vertical) { - if ((imageSize.height() % _unitSize) != 0) { - return QSize(imageSize.width(), ((imageSize.height()/_unitSize)+1)*_unitSize); - } else { - return imageSize; - } - } else { - if ((imageSize.width() % _unitSize) != 0) { - return QSize(((imageSize.width()/_unitSize)+1)*_unitSize, imageSize.height()); - } else { - return imageSize; - } - } + return monoGeneratedSize(imageSize, parameter); } LegendDataPtr ConverterFramebufMono::legendData(OverlayMode mode) const { auto data = ConverterFramebuf::legendData(mode); - if (mode == OverlayMode::BitAssigments) { - data->addEntry(colorBitAssignment1, colorBitAssignment2, QObject::tr("Bit Assignment")); - } else { - data->addEntry(colorPixelInterpretation, QObject::tr("Pixel Interpretation")); - } + addMonoLegendData(data, mode); return data; } -void ConverterFramebufMono::paintOverlay(OverlayPainter &p, OverlayMode mode, const QImage &image) const +void ConverterFramebufMono::paintOverlay(OverlayPainter &p, OverlayMode mode, const QImage &image, const QVariantMap ¶meter) const { - ConverterFramebuf::paintOverlay(p, mode, image); - if (mode == OverlayMode::BitAssigments) { - const auto bitL = (_bitDirection == BitDirection::LSB ? "0" : QString::number(_unitSize-1)); - const auto bitH = (_bitDirection == BitDirection::LSB ? QString::number(_unitSize-1) : "0"); - bool colorFlag = false; - if (_unitOrientation == UnitOrientation::Vertical) { - for (int y = 0; y < p.imageSize().height(); y += _unitSize) { - for (int x = 0; x < p.imageSize().width(); ++x) { - QRect rect(x, y, 1, _unitSize); - if (p.arePixelUpdated(rect)) { - const auto color = colorFlag ? colorBitAssignment1 : colorBitAssignment2; - p.drawPixelOutline(rect, color, 1); - p.drawPixelText(QRect(x, y, 1, 1), color, bitL); - p.drawPixelText(QRect(x, y+_unitSize-1, 1, 1), color, bitH); - } - colorFlag = !colorFlag; - } - if (p.imageSize().width() % 2 == 0) { - colorFlag = !colorFlag; - } - } - } else { - for (int y = 0; y < p.imageSize().height(); ++y) { - for (int x = 0; x < p.imageSize().width(); x += _unitSize) { - QRect rect(x, y, _unitSize, 1); - if (p.arePixelUpdated(rect)) { - const auto color = colorFlag ? colorBitAssignment1 : colorBitAssignment2; - p.drawPixelOutline(rect, color, 1); - p.drawPixelText(QRect(x, y, 1, 1), color, bitH); - p.drawPixelText(QRect(x+_unitSize-1, y, 1, 1), color, bitL); - } - colorFlag = !colorFlag; - } - if ((p.imageSize().width()/_unitSize) % 2 == 0) { - colorFlag = !colorFlag; - } - } - } - } else if (mode == OverlayMode::PixelInterpretation) { - for (int y = 0; y < p.generatedSize().height(); ++y) { - for (int x = 0; x < p.generatedSize().width(); ++x) { - auto text = (getPixel(x, y, image) ? "1" : "0"); - const QRect rect(x, y, 1, 1); - if (p.arePixelUpdated(rect)) { - p.drawPixelText(rect, colorPixelInterpretation, text); - } - } - } - } -} - - -bool ConverterFramebufMono::getPixel(int x, int y, const QImage &image) -{ - if (x < 0 || y < 0 || x >= image.width() || y >= image.height()) { - return false; - } - auto color = image.pixelColor(x, y); - float h, s, l, a; - color.getHslF(&h, &s, &l, &a); - if (a < 0.5) { - return false; - } - return l < 0.5; -} - - -uint32_t ConverterFramebufMono::readUnit(int x, int y, int dx, int dy, int count, const QImage &image) -{ - uint32_t result = 0; - for (int i = 0; i < count; ++i) { - result <<= 1; - if (getPixel(x, y, image)) { - result |= 0b1; - } - x += dx; - y += dy; - } - return result; + ConverterFramebuf::paintOverlay(p, mode, image, parameter); + monoPaintOverlay(p, mode, image, parameter); } diff --git a/ConverterFramebufMono.hpp b/ConverterFramebufMono.hpp index 54bd17b..442bde2 100644 --- a/ConverterFramebufMono.hpp +++ b/ConverterFramebufMono.hpp @@ -17,24 +17,14 @@ #pragma once +#include "MonoTools.hpp" #include "ConverterFramebuf.hpp" /// The base class of all mono converters /// -class ConverterFramebufMono : public ConverterFramebuf +class ConverterFramebufMono : public ConverterFramebuf, public MonoTools { -protected: - enum class UnitOrientation { - Horizontal, - Vertical - }; - - enum class BitDirection { - LSB, - MSB - }; - public: /// Create a new mono converter. /// @@ -49,30 +39,8 @@ public: int unitSize); public: // Converter interface - QSize generatedSize(const QSize &imageSize) const override; + QSize generatedSize(const QSize &imageSize, const QVariantMap ¶meter) const override; LegendDataPtr legendData(OverlayMode mode) const override; - void paintOverlay(OverlayPainter &p, OverlayMode mode, const QImage &image) const override; - -protected: - /// Interpret a single pixel. - /// - /// Returns `false` if the pixel is out of bounds. - /// - static bool getPixel(int x, int y, const QImage &image); - - /// Read a single unit. - /// - static uint32_t readUnit(int x, int y, int dx, int dy, int count, const QImage &image); - - /// The colors for the bit assignments - /// - static constexpr QColor colorBitAssignment1 = QColor(240, 40, 40); - static constexpr QColor colorBitAssignment2 = QColor(240, 80, 80); - static constexpr QColor colorPixelInterpretation = QColor(240, 240, 40); - -protected: - UnitOrientation _unitOrientation; ///< The unit orientatioon. - BitDirection _bitDirection; ///< The bit direction. - int _unitSize; ///< The number of bits per unit. + void paintOverlay(OverlayPainter &p, OverlayMode mode, const QImage &image, const QVariantMap ¶meter) const override; }; diff --git a/ConverterFramebufMonoHLSB.cpp b/ConverterFramebufMonoHLSB.cpp index 779e881..814af7c 100644 --- a/ConverterFramebufMonoHLSB.cpp +++ b/ConverterFramebufMonoHLSB.cpp @@ -26,7 +26,7 @@ ConverterFramebufMonoHLSB::ConverterFramebufMonoHLSB() } -QString ConverterFramebufMonoHLSB::generateCode(const QImage &image, const QVariantMap&) const +QString ConverterFramebufMonoHLSB::generateCode(const QImage &image, const QVariantMap ¶meter) const { QByteArray data; for (int y = 0; y < image.height(); ++y) { @@ -34,5 +34,5 @@ QString ConverterFramebufMonoHLSB::generateCode(const QImage &image, const QVari data.append(static_cast(readUnit(x, y, 1, 0, 8, image))); } } - return createCode(data, generatedSize(image.size()), "MONO_HLSB"); + return createCode(data, generatedSize(image.size(), parameter), "MONO_HLSB"); } diff --git a/ConverterFramebufMonoHMSB.cpp b/ConverterFramebufMonoHMSB.cpp index fed9891..da7c7c8 100644 --- a/ConverterFramebufMonoHMSB.cpp +++ b/ConverterFramebufMonoHMSB.cpp @@ -23,7 +23,7 @@ ConverterFramebufMonoHMSB::ConverterFramebufMonoHMSB() } -QString ConverterFramebufMonoHMSB::generateCode(const QImage &image, const QVariantMap&) const +QString ConverterFramebufMonoHMSB::generateCode(const QImage &image, const QVariantMap ¶meter) const { QByteArray data; for (int y = 0; y < image.height(); ++y) { @@ -31,6 +31,6 @@ QString ConverterFramebufMonoHMSB::generateCode(const QImage &image, const QVari data.append(static_cast(readUnit(x+7, y, -1, 0, 8, image))); } } - return createCode(data, generatedSize(image.size()), "MONO_HMSB"); + return createCode(data, generatedSize(image.size(), parameter), "MONO_HMSB"); } diff --git a/ConverterFramebufMonoVLSB.cpp b/ConverterFramebufMonoVLSB.cpp index 868f6c1..2c2443c 100644 --- a/ConverterFramebufMonoVLSB.cpp +++ b/ConverterFramebufMonoVLSB.cpp @@ -23,7 +23,7 @@ ConverterFramebufMonoVLSB::ConverterFramebufMonoVLSB() } -QString ConverterFramebufMonoVLSB::generateCode(const QImage &image, const QVariantMap&) const +QString ConverterFramebufMonoVLSB::generateCode(const QImage &image, const QVariantMap ¶meter) const { QByteArray data; for (int y = 0; y < image.height(); y += 8) { @@ -31,6 +31,6 @@ QString ConverterFramebufMonoVLSB::generateCode(const QImage &image, const QVari data.append(static_cast(readUnit(x, y+7, 0, -1, 8, image))); } } - return createCode(data, generatedSize(image.size()), "MONO_VLSB"); + return createCode(data, generatedSize(image.size(), parameter), "MONO_VLSB"); } diff --git a/FontConverter.cpp b/FontConverter.cpp new file mode 100644 index 0000000..5ee49bf --- /dev/null +++ b/FontConverter.cpp @@ -0,0 +1,149 @@ +// +// (c)2021 by Lucky Resistor. https://luckyresistor.me/ +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// 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. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// +#include "FontConverter.hpp" + + +FontConverter::FontConverter(const QString &displayName) + : Converter(displayName) +{ +} + + +Converter::Mode FontConverter::mode() const +{ + return Mode::Font; +} + + +ParameterDefinitionPtr FontConverter::createParameterDefinition() const +{ + auto pd = Converter::createParameterDefinition(); + pd->addIntegerSize("maximumSize", QObject::tr("Maximum Size"), + QSize(12, 16), QSize(8, 8), QSize(256, 256)); + pd->addIntegerPosition("charOffset", QObject::tr("Char Offset"), + QPoint(0, 0), QPoint(-256, -256), QPoint(256, 256)); + pd->addCheckbox("convertMono", QObject::tr("Convert to Mono"), false); + pd->addInteger("convertMonoThreshold", QObject::tr("Conversion Threshold"), 128, 0, 255); + pd->addCheckbox("preferBitmapFont", QObject::tr("Prefer Bitmap Font"), false); + pd->addCheckbox("noAntialiasFont", QObject::tr("No Antialias Font"), false); + pd->addCheckbox("trimLeft", QObject::tr("Trim Left Side"), true); + pd->addCheckbox("trimRight", QObject::tr("Trim Right Side"), true); + pd->addCheckbox("invertFont", QObject::tr("Invert the Font"), false); + return pd; +} + + +QImage FontConverter::generateImage(const QFont &font, QChar c, const QVariantMap ¶meter) const +{ + auto maximumSize = parameter["maximumSize"].toSize(); + if (!maximumSize.isValid() || maximumSize.isNull()) { + maximumSize = QSize(8, 8); + } + const auto charOffset = parameter["charOffset"].toPoint(); + QImage result(maximumSize, QImage::Format_ARGB32_Premultiplied); + result.setDevicePixelRatio(1.0); + QPainter p; + p.begin(&result); + p.fillRect(result.rect(), Qt::white); + p.setRenderHint(QPainter::TextAntialiasing, false); + auto fontForDrawing = font; + int fontStyleStrategy = 0; + if (parameter["preferBitmapFont"].toBool()) { + fontStyleStrategy |= QFont::PreferBitmap; + } + if (parameter["noAntialiasFont"].toBool()) { + fontStyleStrategy |= QFont::NoAntialias; + } + fontStyleStrategy |= QFont::NoSubpixelAntialias; + fontForDrawing.setStyleStrategy(static_cast(fontStyleStrategy)); + QFontMetrics fontMetrics(fontForDrawing); + auto charBoundingRect = fontMetrics.boundingRect(c); + // Make sure the first pixel is in the bitmap, draw baseline at ascent. + QPoint targetPoint(-charBoundingRect.left(), fontMetrics.ascent()); + targetPoint += charOffset; + p.setFont(fontForDrawing); + p.drawText(targetPoint, QString(c)); + p.end(); + // Postprocess the font. + const auto threshold = static_cast(parameter["convertMonoThreshold"].toInt()); + if (parameter["convertMono"].toBool()) { + for (int x = 0; x < result.width(); ++x) { + for (int y = 0; y < result.height(); ++y) { + const auto pixel = result.pixel(x, y); + if ((pixel & 0xff) <= threshold) { + result.setPixel(x, y, qRgb(0, 0, 0)); + } else { + result.setPixel(x, y, qRgb(255, 255, 255)); + } + } + } + } + // Trim the font. + const auto trimLeft = parameter["trimLeft"].toBool(); + const auto trimRight = parameter["trimRight"].toBool(); + if (trimLeft || trimRight) { + auto resultRect = result.rect(); + int leftTrim = 0; + int rightTrim = 0; + if (trimLeft) { + for (int x = 0; x < result.width(); ++x) { + bool hasPixelSet = false; + for (int y = 0; y < result.height(); ++y) { + const auto pixel = result.pixel(x, y); + if ((pixel & 0xff) <= threshold) { + hasPixelSet = true; + break; + } + } + if (hasPixelSet) { + break; + } + leftTrim += 1; + } + } + if (trimRight) { + for (int x = result.width()-1; x >= 0; --x) { + bool hasPixelSet = false; + for (int y = 0; y < result.width(); ++y) { + const auto pixel = result.pixel(x, y); + if ((pixel & 0xff) <= threshold) { + hasPixelSet = true; + break; + } + } + if (hasPixelSet) { + break; + } + rightTrim += 1; + } + } + if ((resultRect.width() - leftTrim - rightTrim) <= 0) { + // The font image is empty, use one pixel line. + resultRect.setWidth(1); + } else { + resultRect = resultRect.marginsRemoved(QMargins(leftTrim, 0, rightTrim, 0)); + } + if (resultRect != result.rect()) { + result = result.copy(resultRect); + } + } + // Invert the font. + if (parameter["invertFont"].toBool()) { + result.invertPixels(); + } + return result; +} diff --git a/FontConverter.hpp b/FontConverter.hpp new file mode 100644 index 0000000..cf34d99 --- /dev/null +++ b/FontConverter.hpp @@ -0,0 +1,58 @@ +// +// (c)2021 by Lucky Resistor. https://luckyresistor.me/ +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// 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. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// +#pragma once + + +#include "Converter.hpp" + + +/// The base class for all font converters. +/// +class FontConverter : public Converter +{ +public: + /// ctor + /// + FontConverter(const QString &displayName); + +public: // implement Converter + Mode mode() const override; + ParameterDefinitionPtr createParameterDefinition() const override; + +public: + /// Create a bitmap for a given character. + /// + /// This method should return the given character in exact the size and format it will + /// be converted as bitmap. + /// + /// @param font The font for the character. + /// @param c The character to convert. + /// @param parameter The current map of parameters. + /// + virtual QImage generateImage(const QFont &font, QChar c, const QVariantMap ¶meter) const; + + /// Generate the code from the given font. + /// + /// The string with all characters to convert is passed as parameter 'characters'. + /// + /// @param font The font to convert. + /// @param parameter A map with parameters passed to this converter. + /// @return The generated code. + /// + virtual QString generateCode(const QFont &font, const QVariantMap ¶meter) const = 0; +}; + diff --git a/FontConverterFramebufMono.cpp b/FontConverterFramebufMono.cpp new file mode 100644 index 0000000..c36232c --- /dev/null +++ b/FontConverterFramebufMono.cpp @@ -0,0 +1,52 @@ +// +// (c)2021 by Lucky Resistor. https://luckyresistor.me/ +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// 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. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// +#include "FontConverterFramebufMono.hpp" + + +FontConverterFramebufMono::FontConverterFramebufMono() +: + FontConverter("MicroPython Font Mono VLSB"), + MonoTools(UnitOrientation::Vertical, BitDirection::LSB, 8) +{ +} + + +QSize FontConverterFramebufMono::generatedSize(const QSize &imageSize, const QVariantMap ¶meter) const +{ + return monoGeneratedSize(imageSize, parameter); +} + + +QString FontConverterFramebufMono::generateCode(const QFont&, const QVariantMap&) const +{ + return "The generated format is not decided yet."; +} + + +LegendDataPtr FontConverterFramebufMono::legendData(OverlayMode mode) const +{ + auto data = FontConverter::legendData(mode); + addMonoLegendData(data, mode); + return data; +} + + +void FontConverterFramebufMono::paintOverlay(OverlayPainter &p, OverlayMode mode, const QImage &image, const QVariantMap ¶meter) const +{ + FontConverter::paintOverlay(p, mode, image, parameter); + monoPaintOverlay(p, mode, image, parameter); +} diff --git a/FontConverterFramebufMono.hpp b/FontConverterFramebufMono.hpp new file mode 100644 index 0000000..aaca602 --- /dev/null +++ b/FontConverterFramebufMono.hpp @@ -0,0 +1,39 @@ +// +// (c)2021 by Lucky Resistor. https://luckyresistor.me/ +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// 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. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// +#pragma once + + +#include "FontConverter.hpp" +#include "MonoTools.hpp" + + +/// A simple converter to create mono bitmap fonts. +/// +class FontConverterFramebufMono : public FontConverter, public MonoTools +{ +public: + /// ctor + /// + FontConverterFramebufMono(); + +public: // implement FontConverter + QSize generatedSize(const QSize &imageSize, const QVariantMap ¶meter) const override; + QString generateCode(const QFont &font, const QVariantMap ¶meter) const override; + LegendDataPtr legendData(OverlayMode mode) const override; + void paintOverlay(OverlayPainter &p, OverlayMode mode, const QImage &image, const QVariantMap ¶meter) const override; +}; + diff --git a/MainWindow.cpp b/MainWindow.cpp index 522e40c..5597f8a 100644 --- a/MainWindow.cpp +++ b/MainWindow.cpp @@ -17,10 +17,12 @@ #include "MainWindow.hpp" +#include "ApplicationController.hpp" #include "BitmapPanel.hpp" #include "ConverterFramebufMonoVLSB.hpp" #include "ConverterFramebufMonoHLSB.hpp" #include "ConverterFramebufMonoHMSB.hpp" +#include "FontConverterFramebufMono.hpp" #include #include @@ -37,6 +39,11 @@ #include #include #include +#include +#include +#include +#include +#include #include @@ -47,6 +54,9 @@ MainWindow::MainWindow(QWidget *parent) initializeUi(); initializeMenu(); loadSettings(); + onFormatChanged(); + onFontChanged(); + onCharactersChanged(); } @@ -60,13 +70,16 @@ void MainWindow::initializeConverterList() _converterList.append(new ConverterFramebufMonoVLSB()); _converterList.append(new ConverterFramebufMonoHMSB()); _converterList.append(new ConverterFramebufMonoHLSB()); + _converterList.append(new FontConverterFramebufMono()); } void MainWindow::initializeUi() { + const int cFixedWidth = 400; + setMinimumSize(800, 600); - setWindowTitle(tr("Micropython Bitmap Tool - V%1 - Lucky Resistor").arg(qApp->applicationVersion())); + setWindowTitle(tr("MicroPython Bitmap Tool - V%1 - Lucky Resistor").arg(qApp->applicationVersion())); auto centralWidget = new QWidget(); centralWidget->setObjectName("CentralWidget"); @@ -83,15 +96,16 @@ void MainWindow::initializeUi() settingsLayout->setSpacing(4); auto logo = new QLabel(); - logo->setFixedSize(300, 250); + logo->setFixedSize(cFixedWidth, 100); logo->setPixmap(QPixmap(":/images/AppLogo.png")); + logo->setAlignment(Qt::AlignCenter); settingsLayout->addWidget(logo); auto versionLabel = new QLabel(tr("Version %1").arg(qApp->applicationVersion())); versionLabel->setAlignment(Qt::AlignCenter); settingsLayout->addWidget(versionLabel); settingsLayout->addSpacing(16); - settingsLayout->addWidget(new QLabel(tr("Generated Format:"))); + settingsLayout->addWidget(new QLabel(tr("Generated Format:"))); _formatSelector = new QComboBox(); for (auto converter : _converterList) { _formatSelector->addItem(converter->displayName()); @@ -99,20 +113,96 @@ void MainWindow::initializeUi() _formatSelector->setCurrentIndex(0); settingsLayout->addWidget(_formatSelector); - settingsLayout->addStretch(); + settingsLayout->addWidget(new QLabel(tr("Parameters:"))); - settingsLayout->addWidget(new QLabel(tr("Loaded Bitmap Info:"))); + auto parameterScroll = new QScrollArea(); + parameterScroll->setObjectName("ParameterScrollArea"); + _parameterFrame = new QFrame(); + _parameterFrame->setObjectName("ParameterFrame"); + _parameterFrame->setFixedWidth(cFixedWidth); + _parameterLayout = new QFormLayout(_parameterFrame); + _parameterLayout->setContentsMargins(8, 8, 8, 8); + parameterScroll->setWidget(_parameterFrame); + parameterScroll->setWidgetResizable(true); + parameterScroll->setFixedWidth(cFixedWidth); + parameterScroll->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Expanding); + //parameterScroll->setBackgroundRole(QPalette::Window); + settingsLayout->addWidget(parameterScroll); + + _bitmapConverterFrame = new QFrame(); + auto bitmapConverterLayout = new QVBoxLayout(_bitmapConverterFrame); + bitmapConverterLayout->setContentsMargins(0, 0, 0, 0); + bitmapConverterLayout->setSpacing(4); + settingsLayout->addWidget(_bitmapConverterFrame); + + bitmapConverterLayout->addWidget(new QLabel(tr("Loaded Bitmap Info:"))); _bitmapInfo = new QLabel(tr("No Bitmap Loaded")); _bitmapInfo->setObjectName("BitmapInfo"); - _bitmapInfo->setMinimumHeight(200); + _bitmapInfo->setMinimumHeight(100); _bitmapInfo->setAlignment(Qt::AlignTop|Qt::AlignLeft); - settingsLayout->addWidget(_bitmapInfo); + bitmapConverterLayout->addWidget(_bitmapInfo); auto loadButton = new QPushButton(); loadButton->setText(tr("Load Bitmap")); - settingsLayout->addWidget(loadButton); + bitmapConverterLayout->addWidget(loadButton); connect(loadButton, &QPushButton::clicked, this, &MainWindow::onLoadBitmap); + _fontConverterFrame = new QFrame(); + auto fontConverterLayout = new QVBoxLayout(_fontConverterFrame); + fontConverterLayout->setContentsMargins(0, 0, 0, 0); + fontConverterLayout->setSpacing(4); + settingsLayout->addWidget(_fontConverterFrame); + + fontConverterLayout->addWidget(new QLabel(tr("Selected Font Info:"))); + _fontInfo = new QLabel(tr("No Font Seleccted")); + _fontInfo->setObjectName("FontInfo"); + _fontInfo->setMinimumHeight(100); + _fontInfo->setAlignment(Qt::AlignTop|Qt::AlignLeft); + fontConverterLayout->addWidget(_fontInfo); + + fontConverterLayout->addWidget(new QLabel(tr("Selected Font:"))); + _fontSelector = new QFontComboBox(); + fontConverterLayout->addWidget(_fontSelector); + auto fontDetailsLayout = new QHBoxLayout(); + fontDetailsLayout->setContentsMargins(0, 0, 0, 0); + fontDetailsLayout->setSpacing(4); + fontConverterLayout->addLayout(fontDetailsLayout); + _fontWeightSelector = new QComboBox(); + _fontWeightSelector->addItem(tr("Thin"), static_cast(QFont::Thin)); + _fontWeightSelector->addItem(tr("Extra Light"), static_cast(QFont::ExtraLight)); + _fontWeightSelector->addItem(tr("Light"), static_cast(QFont::Light)); + _fontWeightSelector->addItem(tr("Normal"), static_cast(QFont::Normal)); + _fontWeightSelector->addItem(tr("Medium"), static_cast(QFont::Medium)); + _fontWeightSelector->addItem(tr("Demi Bold"), static_cast(QFont::DemiBold)); + _fontWeightSelector->addItem(tr("Bold"), static_cast(QFont::Bold)); + _fontWeightSelector->addItem(tr("Extra Bold"), static_cast(QFont::ExtraBold)); + _fontWeightSelector->addItem(tr("Black"), static_cast(QFont::Black)); + _fontWeightSelector->setCurrentIndex(3); + _fontSizeSelector = new QSpinBox(); + _fontSizeSelector->setRange(4, 200); + _fontSizeSelector->setValue(12); + _fontHinting = new QComboBox(); + _fontHinting->addItem(tr("Default"), static_cast(QFont::PreferDefaultHinting)); + _fontHinting->addItem(tr("None"), static_cast(QFont::PreferNoHinting)); + _fontHinting->addItem(tr("Vertical"), static_cast(QFont::PreferVerticalHinting)); + _fontHinting->addItem(tr("Full"), static_cast(QFont::PreferFullHinting)); + _fontHinting->setCurrentIndex(0); + fontDetailsLayout->addWidget(new QLabel(tr("Size:"))); + fontDetailsLayout->addWidget(_fontSizeSelector); + fontDetailsLayout->addWidget(new QLabel(tr("Weight:"))); + fontDetailsLayout->addWidget(_fontWeightSelector); + fontDetailsLayout->addWidget(new QLabel(tr("Hinting:"))); + fontDetailsLayout->addWidget(_fontHinting); + fontConverterLayout->addWidget(new QLabel(tr("Characters to convert:"))); + _fontCharacters = new QPlainTextEdit(); + _fontCharacters->setFixedHeight(100); + _fontCharacters->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Minimum); + _fontCharacters->setWordWrapMode(QTextOption::WrapAnywhere); + _fontCharacters->setPlainText("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.,:;!?()+-=\"'<>"); + _fontCharacters->setReadOnly(true); + fontConverterLayout->addWidget(_fontCharacters); + connect(_fontCharacters, &QPlainTextEdit::textChanged, this, &MainWindow::onCharactersChanged); + auto previewSplittter = new QSplitter(Qt::Vertical); previewSplittter->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); previewSplittter->setChildrenCollapsible(false); @@ -133,15 +223,9 @@ void MainWindow::initializeUi() _codePreview->setWordWrapMode(QTextOption::NoWrap); codeLayout->addWidget(_codePreview); -#ifdef Q_OS_WIN32 - _codePreview->setFont(QFont("Consolas", 12)); -#else -#ifdef Q_OS_MAC - _codePreview->setFont(QFont("Menlo", 12)); -#else - _codePreview->setFont(QFont("Lucida Console", 12)); -#endif -#endif + const auto monospaceFont = gApp()->monospaceFont(); + _codePreview->setFont(monospaceFont); + _fontCharacters->setFont(monospaceFont); auto codeActions = new QFrame(); auto codeActionsLayout = new QHBoxLayout(codeActions); @@ -161,11 +245,12 @@ void MainWindow::initializeUi() previewSplittter->setStretchFactor(0, 4); previewSplittter->setStretchFactor(1, 1); - connect(_formatSelector, &QComboBox::currentIndexChanged, [=]{ - _bitmapPanel->setConverter(selectedConverter()); - updateBitmapInfo(); - updateCode(); - }); + connect(_formatSelector, &QComboBox::currentIndexChanged, this, &MainWindow::onFormatChanged); + connect(_fontSelector, &QFontComboBox::currentFontChanged, this, &MainWindow::onFontChanged); + connect(_fontSizeSelector, &QSpinBox::valueChanged, this, &MainWindow::onFontChanged); + connect(_fontWeightSelector, &QComboBox::currentIndexChanged, this, &MainWindow::onFontChanged); + connect(_fontHinting, &QComboBox::currentIndexChanged, this, &MainWindow::onFontChanged); + connect(_bitmapPanel, &BitmapPanel::selectedCharacterChanged, this, &MainWindow::onSelectedCharacterChanged); } @@ -229,6 +314,18 @@ void MainWindow::loadSettings() restoreGeometry(settings.value("mainWindow.geometry").toByteArray()); restoreState(settings.value("mainWindow.state").toByteArray()); _formatSelector->setCurrentIndex(settings.value("format.index").toInt(0)); + _currentFont = QFont(settings.value("font.family", "Arial").toString()); + _currentFont.setPixelSize(settings.value("font.size", 12).toInt()); + _currentFont.setWeight(static_cast(settings.value("font.weight", 500).toInt())); + _fontSelector->setCurrentFont(_currentFont); + _fontSizeSelector->setValue(_currentFont.pixelSize()); + const int index = _fontWeightSelector->findData(static_cast(_currentFont.weight())); + if (index >= 0) { + _fontWeightSelector->setCurrentIndex(index); + } else { + _fontWeightSelector->setCurrentIndex(3); // normal + } + _fontHinting->setCurrentIndex(settings.value("font.hinting", 0).toInt()); } @@ -238,6 +335,10 @@ void MainWindow::saveSettings() settings.setValue("mainWindow.geometry", saveGeometry()); settings.setValue("mainWindow.state", saveState()); settings.setValue("format.index", _formatSelector->currentIndex()); + settings.setValue("font.family", _currentFont.family()); + settings.setValue("font.size", _currentFont.pixelSize()); + settings.setValue("font.weight", static_cast(_currentFont.weight())); + settings.setValue("font.hinting", _fontHinting->currentIndex()); } @@ -251,42 +352,120 @@ Converter *MainWindow::selectedConverter() const } -void MainWindow::updateBitmapInfo() +void MainWindow::updateInfoBox() { QString text; QTextStream ts(&text); - ts << "

"; - ts << "Bitmap Size: " << _currentImage.width() << "x" << _currentImage.height() << "
"; - ts << "Bit Depth: " << _currentImage.depth() << "
"; - ts << "Color Format: "; - switch (_currentImage.pixelFormat().colorModel()) { - case QPixelFormat::RGB: ts << "RGB"; break; - case QPixelFormat::BGR: ts << "BGR"; break; - case QPixelFormat::Indexed: ts << "Indexed"; break; - case QPixelFormat::Grayscale: ts << "Grayscale"; break; - case QPixelFormat::CMYK: ts << "CMYK"; break; - case QPixelFormat::HSL: ts << "HSL"; break; - case QPixelFormat::HSV: ts << "HSV"; break; - case QPixelFormat::YUV: ts << "YUV"; break; - case QPixelFormat::Alpha: ts << "Alpha"; break; + if (selectedConverter()->mode() == Converter::Mode::Bitmap) { + ts << "

"; + ts << "Bitmap Size: " << _currentImage.width() << "x" << _currentImage.height() << "
"; + ts << "Bit Depth: " << _currentImage.depth() << "
"; + ts << "Color Format: "; + switch (_currentImage.pixelFormat().colorModel()) { + case QPixelFormat::RGB: ts << "RGB"; break; + case QPixelFormat::BGR: ts << "BGR"; break; + case QPixelFormat::Indexed: ts << "Indexed"; break; + case QPixelFormat::Grayscale: ts << "Grayscale"; break; + case QPixelFormat::CMYK: ts << "CMYK"; break; + case QPixelFormat::HSL: ts << "HSL"; break; + case QPixelFormat::HSV: ts << "HSV"; break; + case QPixelFormat::YUV: ts << "YUV"; break; + case QPixelFormat::Alpha: ts << "Alpha"; break; + } + ts << "
"; + const auto gs = selectedConverter()->generatedSize(_currentImage.size(), createParameterMap()); + ts << "Generated Size: " << gs.width() << "x" << gs.height() << "

"; + _bitmapInfo->setText(text); + } else { + ts << "

"; + ts << "Family: " << _currentFont.family() << "
"; + ts << "Weight: "; + switch (_currentFont.weight()) { + case QFont::Thin: ts << tr("Thin"); break; + case QFont::ExtraLight: ts << tr("Extra Light"); break; + case QFont::Light: ts << tr("Light"); break; + case QFont::Normal: ts << tr("Normal"); break; + case QFont::Medium: ts << tr("Medium"); break; + case QFont::DemiBold: ts << tr("Demi Bold"); break; + case QFont::Bold: ts << tr("Bold"); break; + case QFont::ExtraBold: ts << tr("Extra Bold"); break; + case QFont::Black: ts << tr("Black"); break; + } + ts << "
"; + ts << "Pixel Size: " << _currentFont.pixelSize() << "
"; + ts << "

"; + _fontInfo->setText(text); } - ts << "
"; - const auto gs = selectedConverter()->generatedSize(_currentImage.size()); - ts << "Generated Size: " << gs.width() << "x" << gs.height() << "

"; - _bitmapInfo->setText(text); } void MainWindow::updateCode() { QString code; - if (!_currentImage.isNull()) { - code = selectedConverter()->generateCode(_currentImage, QVariantMap()); + if (selectedConverter()->mode() == Converter::Mode::Bitmap) { + auto bitmapConverter = static_cast(selectedConverter()); + if (!_currentImage.isNull()) { + code = bitmapConverter->generateCode(_currentImage, QVariantMap()); + } + } else { + auto fontConverter = static_cast(selectedConverter()); + code = fontConverter->generateCode(_currentFont, createParameterMap()); } _codePreview->setPlainText(code); } +void MainWindow::updateParameters() +{ + auto converter = selectedConverter(); + if (converter == nullptr) { + return; + } + + // Replace the current parameter definition. + _displayedParameters = converter->createParameterDefinition(); + // Remove the previous UI + while (_parameterLayout->count() > 0) { + auto item = _parameterLayout->takeAt(0); + delete item; + } + for (auto w : _parameterFrame->findChildren()) { + w->deleteLater(); + } + // Build the new parameter UI. + if (_displayedParameters->parameterList().isEmpty()) { + auto label = new QLabel(); + label->setText(tr("No Parameters")); + _parameterLayout->addWidget(label); + } else { + for (const auto ¶meter : _displayedParameters->parameterList()) { + auto pw = parameterFactory.createWidget(parameter); + pw->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); + pw->setObjectName(parameter->identifier()); + _parameterLayout->addRow(parameter->label() + ":", pw); + connect(pw, &ParameterWidget::valueChanged, this, &MainWindow::onParameterChanged); + } + } + auto stretchItem = new QWidget(); + stretchItem->setFixedWidth(10); + stretchItem->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Expanding); + _parameterLayout->addRow(stretchItem); +} + + +QVariantMap MainWindow::createParameterMap() const +{ + QVariantMap result; + result["characters"] = _fontCharacters->toPlainText(); + result["font"] = _currentFont; + result["char"] = _bitmapPanel->selectedCharacter(); + for (auto pw : _parameterFrame->findChildren()) { + result[pw->objectName()] = pw->value(); + } + return result; +} + + void MainWindow::onLoadBitmap() { QSettings settings; @@ -320,11 +499,78 @@ void MainWindow::onLoadBitmap() _currentImage = newImage; _bitmapPanel->setImage(newImage); _bitmapPanel->setConverter(selectedConverter()); - updateBitmapInfo(); + updateInfoBox(); updateCode(); } +void MainWindow::onFormatChanged() +{ + const auto converter = selectedConverter(); + _bitmapPanel->setConverter(converter); + if (converter->mode() == Converter::Mode::Bitmap) { + _bitmapConverterFrame->setVisible(true); + _fontConverterFrame->setVisible(false); + } else { + _bitmapConverterFrame->setVisible(false); + _fontConverterFrame->setVisible(true); + } + updateParameters(); + updateInfoBox(); + updateCode(); + _bitmapPanel->setParameter(createParameterMap()); + onSelectedCharacterChanged(_bitmapPanel->selectedCharacter()); +} + + +void MainWindow::onFontChanged() +{ + _currentFont = _fontSelector->currentFont(); + _currentFont.setWeight(static_cast(_fontWeightSelector->currentData().toInt())); + _currentFont.setPixelSize(_fontSizeSelector->value()); + const auto hintingPreferences = static_cast(_fontHinting->currentData().toInt()); + _currentFont.setHintingPreference(hintingPreferences); + updateInfoBox(); + updateCode(); + _bitmapPanel->setParameter(createParameterMap()); + onSelectedCharacterChanged(_bitmapPanel->selectedCharacter()); +} + + +void MainWindow::onCharactersChanged() +{ + auto characters = _fontCharacters->toPlainText(); + _bitmapPanel->setCharacters(characters); + _bitmapPanel->setConverter(selectedConverter()); + _bitmapPanel->setParameter(createParameterMap()); + onSelectedCharacterChanged(_bitmapPanel->selectedCharacter()); +} + + +void MainWindow::onSelectedCharacterChanged(QChar c) +{ + auto converter = selectedConverter(); + if (converter == nullptr) { + return; + } + if (converter->mode() == Converter::Mode::Font) { + auto fontConverter = static_cast(converter); + auto newImage = fontConverter->generateImage(_currentFont, c, createParameterMap()); + _currentImage = newImage; + _bitmapPanel->setImage(newImage); + _bitmapPanel->setConverter(selectedConverter()); + _bitmapPanel->setParameter(createParameterMap()); + } +} + + +void MainWindow::onParameterChanged() +{ + _bitmapPanel->setParameter(createParameterMap()); + onSelectedCharacterChanged(_bitmapPanel->selectedCharacter()); +} + + void MainWindow::closeEvent(QCloseEvent *event) { saveSettings(); diff --git a/MainWindow.hpp b/MainWindow.hpp index 4107239..e9d7b84 100644 --- a/MainWindow.hpp +++ b/MainWindow.hpp @@ -17,6 +17,10 @@ #pragma once +#include "ParameterDefinition.hpp" +#include "ParameterFactory.hpp" + +#include #include @@ -25,6 +29,11 @@ class QPlainTextEdit; class QComboBox; class QLabel; class BitmapPanel; +class QFrame; +class QFontComboBox; +class QLineEdit; +class QSpinBox; +class QFormLayout; /// The main widnow @@ -64,26 +73,73 @@ private: /// Update the bitmap info. /// - void updateBitmapInfo(); + void updateInfoBox(); /// Update the code. /// void updateCode(); + /// Update the parameter frame for the current converter. + /// + void updateParameters(); + + /// Get the current parameters + /// + QVariantMap createParameterMap() const; + private Q_SLOTS: /// Load a bitmap. /// void onLoadBitmap(); + /// After a new converter is selected. + /// + void onFormatChanged(); + + /// After a new font is selected. + /// + void onFontChanged(); + + /// After the character list has changed + /// + void onCharactersChanged(); + + /// If the selected character changed + /// + void onSelectedCharacterChanged(QChar c); + + /// After a parameter has changed. + /// + void onParameterChanged(); + protected: // Implement QWidget void closeEvent(QCloseEvent *event) override; private: + ParameterFactory parameterFactory; ///< The parameter factory. + QComboBox *_formatSelector; ///< The format selector. QPlainTextEdit *_codePreview; ///< The generated code. BitmapPanel *_bitmapPanel; ///< The bitmap panel. - QImage _currentImage; ///< The current loaded image + + QFrame *_parameterFrame; ///< The frame with the parameters. + QFormLayout *_parameterLayout; ///< The parameter layout. + ParameterDefinitionPtr _displayedParameters; ///< The currently displayed parameters. + + QImage _currentImage; ///< The current loaded image for bitmap converters. + QFont _currentFont; ///< The currently selected font for font converters. + + QFrame *_bitmapConverterFrame; ///< The frame for bitmap converters. QLabel *_bitmapInfo; ///< The label with the image info. + + QFrame *_fontConverterFrame; ///< The frame for font converters. + QLabel *_fontInfo; ///< The label with the font info. + QFontComboBox *_fontSelector; ///< The combo box to select a new font. + QComboBox *_fontWeightSelector; ///< The selector for the font weight. + QSpinBox *_fontSizeSelector; ///< The selector for the used font size. + QComboBox *_fontHinting; ///< The hinting settings for the font. + QPlainTextEdit *_fontCharacters; ///< The characters of the font to be converted. + QList _converterList; ///< A list of available converters. }; diff --git a/MicropythonBitmapTool.pro b/MicropythonBitmapTool.pro index 29fb710..9d04c53 100644 --- a/MicropythonBitmapTool.pro +++ b/MicropythonBitmapTool.pro @@ -3,6 +3,7 @@ QT += core gui greaterThan(QT_MAJOR_VERSION, 4): QT += widgets CONFIG += c++17 +CONFIG += sdk_no_version_check TARGET = "MicroPythonBitmapTool" @@ -11,6 +12,8 @@ TARGET = "MicroPythonBitmapTool" #DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0 SOURCES += \ + ApplicationController.cpp \ + BitmapConverter.cpp \ BitmapPanel.cpp \ BitmapPreview.cpp \ Converter.cpp \ @@ -19,13 +22,22 @@ SOURCES += \ ConverterFramebufMonoHLSB.cpp \ ConverterFramebufMonoHMSB.cpp \ ConverterFramebufMonoVLSB.cpp \ + FontConverter.cpp \ + FontConverterFramebufMono.cpp \ LegendData.cpp \ LegendEntry.cpp \ + MonoTools.cpp \ OverlayPainter.cpp \ + ParameterDefinition.cpp \ + ParameterEntry.cpp \ + ParameterFactory.cpp \ + ParameterWidget.cpp \ main.cpp \ MainWindow.cpp HEADERS += \ + ApplicationController.hpp \ + BitmapConverter.hpp \ BitmapPanel.hpp \ BitmapPreview.hpp \ Converter.hpp \ @@ -34,11 +46,19 @@ HEADERS += \ ConverterFramebufMonoHLSB.hpp \ ConverterFramebufMonoHMSB.hpp \ ConverterFramebufMonoVLSB.hpp \ + FontConverter.hpp \ + FontConverterFramebufMono.hpp \ LegendData.hpp \ LegendEntry.hpp \ MainWindow.hpp \ + MonoTools.hpp \ OverlayMode.hpp \ - OverlayPainter.hpp + OverlayPainter.hpp \ + ParameterDefinition.hpp \ + ParameterEntry.hpp \ + ParameterFactory.hpp \ + ParameterType.hpp \ + ParameterWidget.hpp RESOURCES += \ data/data.qrc diff --git a/MonoTools.cpp b/MonoTools.cpp new file mode 100644 index 0000000..f48dbee --- /dev/null +++ b/MonoTools.cpp @@ -0,0 +1,134 @@ +// +// (c)2021 by Lucky Resistor. https://luckyresistor.me/ +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// 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. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// +#include "MonoTools.hpp" + + +MonoTools::MonoTools(MonoTools::UnitOrientation unitOrientation, MonoTools::BitDirection bitDirection, int unitSize) + : _unitOrientation(unitOrientation), _bitDirection(bitDirection), _unitSize(unitSize) +{ +} + + +void MonoTools::addMonoLegendData(LegendDataPtr data, OverlayMode mode) const +{ + if (mode == OverlayMode::BitAssigments) { + data->addEntry(colorBitAssignment1, colorBitAssignment2, QObject::tr("Bit Assignment")); + } else { + data->addEntry(colorPixelInterpretation, QObject::tr("Pixel Interpretation")); + } +} + + +QSize MonoTools::monoGeneratedSize(const QSize &imageSize, const QVariantMap&) const +{ + if (_unitOrientation == UnitOrientation::Vertical) { + if ((imageSize.height() % _unitSize) != 0) { + return QSize(imageSize.width(), ((imageSize.height()/_unitSize)+1)*_unitSize); + } else { + return imageSize; + } + } else { + if ((imageSize.width() % _unitSize) != 0) { + return QSize(((imageSize.width()/_unitSize)+1)*_unitSize, imageSize.height()); + } else { + return imageSize; + } + } +} + + +void MonoTools::monoPaintOverlay(OverlayPainter &p, OverlayMode mode, const QImage &image, const QVariantMap&) const +{ + if (mode == OverlayMode::BitAssigments) { + const auto bitL = (_bitDirection == BitDirection::LSB ? "0" : QString::number(_unitSize-1)); + const auto bitH = (_bitDirection == BitDirection::LSB ? QString::number(_unitSize-1) : "0"); + bool colorFlag = false; + if (_unitOrientation == UnitOrientation::Vertical) { + for (int y = 0; y < p.imageSize().height(); y += _unitSize) { + for (int x = 0; x < p.imageSize().width(); ++x) { + QRect rect(x, y, 1, _unitSize); + if (p.arePixelUpdated(rect)) { + const auto color = colorFlag ? colorBitAssignment1 : colorBitAssignment2; + p.drawPixelOutline(rect, color, 1); + p.drawPixelText(QRect(x, y, 1, 1), color, bitL); + p.drawPixelText(QRect(x, y+_unitSize-1, 1, 1), color, bitH); + } + colorFlag = !colorFlag; + } + if (p.imageSize().width() % 2 == 0) { + colorFlag = !colorFlag; + } + } + } else { + for (int y = 0; y < p.imageSize().height(); ++y) { + for (int x = 0; x < p.imageSize().width(); x += _unitSize) { + QRect rect(x, y, _unitSize, 1); + if (p.arePixelUpdated(rect)) { + const auto color = colorFlag ? colorBitAssignment1 : colorBitAssignment2; + p.drawPixelOutline(rect, color, 1); + p.drawPixelText(QRect(x, y, 1, 1), color, bitH); + p.drawPixelText(QRect(x+_unitSize-1, y, 1, 1), color, bitL); + } + colorFlag = !colorFlag; + } + if ((p.imageSize().width()/_unitSize) % 2 == 0) { + colorFlag = !colorFlag; + } + } + } + } else if (mode == OverlayMode::PixelInterpretation) { + for (int y = 0; y < p.generatedSize().height(); ++y) { + for (int x = 0; x < p.generatedSize().width(); ++x) { + auto text = (getPixel(x, y, image) ? "1" : "0"); + const QRect rect(x, y, 1, 1); + if (p.arePixelUpdated(rect)) { + p.drawPixelText(rect, colorPixelInterpretation, text); + } + } + } + } +} + + +bool MonoTools::getPixel(int x, int y, const QImage &image) +{ + if (x < 0 || y < 0 || x >= image.width() || y >= image.height()) { + return false; + } + auto color = image.pixelColor(x, y); + float h, s, l, a; + color.getHslF(&h, &s, &l, &a); + if (a < 0.5) { + return false; + } + return l < 0.5; +} + + +uint32_t MonoTools::readUnit(int x, int y, int dx, int dy, int count, const QImage &image) +{ + uint32_t result = 0; + for (int i = 0; i < count; ++i) { + result <<= 1; + if (getPixel(x, y, image)) { + result |= 0b1; + } + x += dx; + y += dy; + } + return result; +} diff --git a/MonoTools.hpp b/MonoTools.hpp new file mode 100644 index 0000000..f31b8cd --- /dev/null +++ b/MonoTools.hpp @@ -0,0 +1,83 @@ +// +// (c)2021 by Lucky Resistor. https://luckyresistor.me/ +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// 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. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// +#pragma once + + +#include "LegendData.hpp" +#include "OverlayPainter.hpp" +#include "OverlayMode.hpp" +#include "ParameterDefinition.hpp" + +#include +#include + + +class MonoTools +{ +public: + enum class UnitOrientation { + Horizontal, + Vertical + }; + + enum class BitDirection { + LSB, + MSB + }; + +public: + /// ctor + /// + MonoTools(UnitOrientation unitOrientation, + BitDirection bitDirection, + int unitSize); + + /// Legend data for mono conversion + /// + void addMonoLegendData(LegendDataPtr data, OverlayMode mode) const; + + /// The generated size for mono conversion. + /// + QSize monoGeneratedSize(const QSize &imageSize, const QVariantMap ¶meter) const; + + /// Paint overlay for mono data. + /// + void monoPaintOverlay(OverlayPainter &p, OverlayMode mode, const QImage &image, const QVariantMap ¶meter) const; + + /// Interpret a single pixel. + /// + /// Returns `false` if the pixel is out of bounds. + /// + static bool getPixel(int x, int y, const QImage &image); + + /// Read a single unit. + /// + static uint32_t readUnit(int x, int y, int dx, int dy, int count, const QImage &image); + +public: + /// The colors for the bit assignments + /// + static constexpr QColor colorBitAssignment1 = QColor(240, 40, 40); + static constexpr QColor colorBitAssignment2 = QColor(240, 80, 80); + static constexpr QColor colorPixelInterpretation = QColor(240, 240, 40); + +protected: + UnitOrientation _unitOrientation; ///< The unit orientatioon. + BitDirection _bitDirection; ///< The bit direction. + int _unitSize; ///< The number of bits per unit. +}; + diff --git a/ParameterDefinition.cpp b/ParameterDefinition.cpp new file mode 100644 index 0000000..2d05662 --- /dev/null +++ b/ParameterDefinition.cpp @@ -0,0 +1,81 @@ +// +// (c)2021 by Lucky Resistor. https://luckyresistor.me/ +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// 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. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// +#include "ParameterDefinition.hpp" + + +ParameterDefinitionPtr ParameterDefinition::create() +{ + return ParameterDefinitionPtr(new ParameterDefinition()); +} + + +void ParameterDefinition::addCheckbox( + const QString &identifier, + const QString &label, + bool defaultValue) +{ + QVariantMap settings; + settings["default"] = defaultValue; + _parameterList.append(ParameterEntry::create( + ParameterType::Checkbox, identifier, label, settings)); +} + + +void ParameterDefinition::addInteger( + const QString &identifier, + const QString &label, + int defaultValue, + int minimum, + int maximum) +{ + QVariantMap settings; + settings["default"] = defaultValue; + settings["minimum"] = minimum; + settings["maximum"] = maximum; + _parameterList.append(ParameterEntry::create(ParameterType::IntegerValue, identifier, label, settings)); +} + + +void ParameterDefinition::addIntegerSize(const QString &identifier, const QString &label, QSize defaultValue, QSize minimum, QSize maximum) +{ + QVariantMap settings; + settings["default"] = defaultValue; + settings["minimum"] = minimum; + settings["maximum"] = maximum; + _parameterList.append(ParameterEntry::create(ParameterType::IntegerSize, identifier, label, settings)); +} + + +void ParameterDefinition::addIntegerPosition(const QString &identifier, const QString &label, QPoint defaultValue, QPoint minimum, QPoint maximum) +{ + QVariantMap settings; + settings["default"] = defaultValue; + settings["minimum"] = minimum; + settings["maximum"] = maximum; + _parameterList.append(ParameterEntry::create(ParameterType::IntegerPosition, identifier, label, settings)); +} + + +const ParameterDefinition::ParameterList &ParameterDefinition::parameterList() const +{ + return _parameterList; +} + + +ParameterDefinition::ParameterDefinition() +{ +} diff --git a/ParameterDefinition.hpp b/ParameterDefinition.hpp new file mode 100644 index 0000000..5e61b8a --- /dev/null +++ b/ParameterDefinition.hpp @@ -0,0 +1,73 @@ +// +// (c)2021 by Lucky Resistor. https://luckyresistor.me/ +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// 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. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// +#pragma once + + +#include "ParameterEntry.hpp" + +#include +#include +#include + + +class ParameterDefinition; +using ParameterDefinitionPtr = QSharedPointer; +using ParameterDefinitionPtrConst = QSharedPointer; + + +/// The definition of parameters, returned by a converter. +/// +class ParameterDefinition +{ +public: + /// A parameter list. + /// + using ParameterList = QList; + +public: + /// Create a new parameter definition. + /// + static ParameterDefinitionPtr create(); + +public: + /// Add a checkbox parameter. + /// + void addCheckbox(const QString &identifier, const QString &label, bool defaultValue); + + /// Add an integer parameter. + /// + void addInteger(const QString &identifier, const QString &label, int defaultValue, int minimum, int maximum); + + /// Add an integer size parameter. + /// + void addIntegerSize(const QString &identifier, const QString &label, QSize defaultValue, QSize minimum, QSize maximum); + + /// Add an integer position parameter. + /// + void addIntegerPosition(const QString &identifier, const QString &label, QPoint defaultValue, QPoint minimum, QPoint maximum); + + /// Get the parameter list. + /// + const ParameterList& parameterList() const; + +private: + ParameterDefinition(); + +private: + ParameterList _parameterList; ///< The list with all parameters. +}; + diff --git a/ParameterEntry.cpp b/ParameterEntry.cpp new file mode 100644 index 0000000..12c776b --- /dev/null +++ b/ParameterEntry.cpp @@ -0,0 +1,65 @@ +// +// (c)2021 by Lucky Resistor. https://luckyresistor.me/ +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// 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. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// +#include "ParameterEntry.hpp" + + +ParameterEntryPtr ParameterEntry::create( + ParameterType type, + const QString &identifier, + const QString &label, + const QVariantMap &settings) +{ + return ParameterEntryPtr(new ParameterEntry(type, identifier, label, settings)); +} + + +ParameterType ParameterEntry::type() const +{ + return _type; +} + + +QString ParameterEntry::identifier() const +{ + return _identifier; +} + + +QString ParameterEntry::label() const +{ + return _label; +} + + +QVariantMap ParameterEntry::settings() const +{ + return _settings; +} + + +ParameterEntry::ParameterEntry( + ParameterType type, + const QString &identifier, + const QString &label, + const QVariantMap &settings) +: + _type(type), + _identifier(identifier), + _label(label), + _settings(settings) +{ +} diff --git a/ParameterEntry.hpp b/ParameterEntry.hpp new file mode 100644 index 0000000..c68bc7c --- /dev/null +++ b/ParameterEntry.hpp @@ -0,0 +1,74 @@ +// +// (c)2021 by Lucky Resistor. https://luckyresistor.me/ +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// 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. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// +#pragma once + + +#include "ParameterType.hpp" + +#include +#include +#include + + +class ParameterEntry; +using ParameterEntryPtr = QSharedPointer; + + +/// One entry in the parameter definition. +/// +class ParameterEntry +{ +public: + static ParameterEntryPtr create( + ParameterType type, + const QString &identifier, + const QString &label, + const QVariantMap &settings); + +public: + /// The parameter type. + /// + ParameterType type() const; + + /// The identifier of the parameter. + /// + QString identifier() const; + + /// The label for the parameter. + /// + QString label() const; + + /// Settings for the parameter. + /// + QVariantMap settings() const; + +private: + /// Create a new raw paremeter entry. + /// + ParameterEntry( + ParameterType type, + const QString &identifier, + const QString &label, + const QVariantMap &settings); + +private: + ParameterType _type; + QString _identifier; + QString _label; + QVariantMap _settings; +}; + diff --git a/ParameterFactory.cpp b/ParameterFactory.cpp new file mode 100644 index 0000000..3c18a51 --- /dev/null +++ b/ParameterFactory.cpp @@ -0,0 +1,40 @@ +// +// (c)2021 by Lucky Resistor. https://luckyresistor.me/ +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// 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. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// +#include "ParameterFactory.hpp" + + +ParameterFactory::ParameterFactory() +{ +} + + +ParameterWidget *ParameterFactory::createWidget(const ParameterEntryPtr ¶meter) const +{ + switch (parameter->type()) { + case ParameterType::Checkbox: + return new CheckBoxParameter(parameter); + case ParameterType::IntegerValue: + return new IntegerParameter(parameter); + case ParameterType::IntegerSize: + return new IntegerSizeParameter(parameter); + case ParameterType::IntegerPosition: + return new IntegerPositionParameter(parameter); + default: + break; + } + return nullptr; +} diff --git a/ParameterFactory.hpp b/ParameterFactory.hpp new file mode 100644 index 0000000..ef767ea --- /dev/null +++ b/ParameterFactory.hpp @@ -0,0 +1,37 @@ +// +// (c)2021 by Lucky Resistor. https://luckyresistor.me/ +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// 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. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// +#pragma once + + +#include "ParameterWidget.hpp" + +#include + + +/// A factory to handle the generic parameters. +/// +class ParameterFactory +{ +public: + ParameterFactory(); + +public: + /// Create the widget for to edit a parameter. + /// + ParameterWidget *createWidget(const ParameterEntryPtr ¶meter) const; +}; + diff --git a/ParameterType.hpp b/ParameterType.hpp new file mode 100644 index 0000000..8ee36ff --- /dev/null +++ b/ParameterType.hpp @@ -0,0 +1,28 @@ +// +// (c)2021 by Lucky Resistor. https://luckyresistor.me/ +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// 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. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// +#pragma once + + +/// The parameter type. +/// +enum class ParameterType { + Checkbox, ///< A boolean parameter + IntegerValue, ///< An integer value. + IntegerSize, ///< A size value + IntegerPosition, ///< A position value. +}; + diff --git a/ParameterWidget.cpp b/ParameterWidget.cpp new file mode 100644 index 0000000..c42536b --- /dev/null +++ b/ParameterWidget.cpp @@ -0,0 +1,173 @@ +// +// (c)2021 by Lucky Resistor. https://luckyresistor.me/ +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// 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. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// +#include "ParameterWidget.hpp" + + +#include +#include +#include +#include + + +ParameterWidget::ParameterWidget(const ParameterEntryPtr ¶meter, QWidget *parent) + : QWidget(parent), _currentValue(parameter->settings()["default"]) +{ +} + + +CheckBoxParameter::CheckBoxParameter(const ParameterEntryPtr ¶meter, QWidget *parent) + : ParameterWidget(parameter, parent) +{ + auto mainLayout = new QHBoxLayout(this); + mainLayout->setContentsMargins(0, 0, 0, 0); + mainLayout->setSpacing(2); + _value = new QCheckBox(); + mainLayout->addWidget(_value); + _value->setChecked(_currentValue.toBool()); + connect(_value, &QCheckBox::toggled, this, &CheckBoxParameter::valueChanged); +} + + +QVariant CheckBoxParameter::value() const +{ + return static_cast(_value->isChecked()); +} + + +void CheckBoxParameter::setValue(const QVariant &value) +{ + if (value != _currentValue) { + _currentValue = value; + _value->setChecked(value.toBool()); + } +} + + +IntegerParameter::IntegerParameter(const ParameterEntryPtr ¶meter, QWidget *parent) + : ParameterWidget(parameter, parent) +{ + auto mainLayout = new QHBoxLayout(this); + mainLayout->setContentsMargins(0, 0, 0, 0); + mainLayout->setSpacing(2); + _value = new QSpinBox(); + _value->setRange( + parameter->settings()["minimum"].toInt(), + parameter->settings()["maximum"].toInt() + ); + mainLayout->addWidget(_value); + _value->setValue(_currentValue.toInt()); + connect(_value, &QSpinBox::valueChanged, this, &CheckBoxParameter::valueChanged); +} + + +QVariant IntegerParameter::value() const +{ + return _value->value(); +} + + +void IntegerParameter::setValue(const QVariant &value) +{ + if (value != _currentValue) { + _value->setValue(value.toInt()); + } +} + + +IntegerSizeParameter::IntegerSizeParameter(const ParameterEntryPtr ¶meter, QWidget *parent) + : ParameterWidget(parameter, parent) +{ + auto mainLayout = new QHBoxLayout(this); + mainLayout->setContentsMargins(0, 0, 0, 0); + mainLayout->setSpacing(2); + mainLayout->addWidget(new QLabel(tr("W:"))); + _a = new QSpinBox(); + _a->setRange( + parameter->settings()["minimum"].toSize().width(), + parameter->settings()["maximum"].toSize().width() + ); + mainLayout->addWidget(_a); + _a->setValue(_currentValue.toSize().width()); + connect(_a, &QSpinBox::valueChanged, this, &CheckBoxParameter::valueChanged); + mainLayout->addWidget(new QLabel(tr("H:"))); + _b = new QSpinBox(); + _b->setRange( + parameter->settings()["minimum"].toSize().height(), + parameter->settings()["maximum"].toSize().height() + ); + mainLayout->addWidget(_b); + _b->setValue(_currentValue.toSize().height()); + connect(_b, &QSpinBox::valueChanged, this, &CheckBoxParameter::valueChanged); +} + + +QVariant IntegerSizeParameter::value() const +{ + return QSize(_a->value(), _b->value()); +} + + +void IntegerSizeParameter::setValue(const QVariant &value) +{ + if (_currentValue != value) { + _a->setValue(value.toSize().width()); + _b->setValue(value.toSize().height()); + } +} + + +IntegerPositionParameter::IntegerPositionParameter(const ParameterEntryPtr ¶meter, QWidget *parent) + : ParameterWidget(parameter, parent) +{ + auto mainLayout = new QHBoxLayout(this); + mainLayout->setContentsMargins(0, 0, 0, 0); + mainLayout->setSpacing(2); + mainLayout->addWidget(new QLabel(tr("X:"))); + _a = new QSpinBox(); + _a->setRange( + parameter->settings()["minimum"].toPoint().x(), + parameter->settings()["maximum"].toPoint().x() + ); + mainLayout->addWidget(_a); + _a->setValue(_currentValue.toPoint().x()); + connect(_a, &QSpinBox::valueChanged, this, &CheckBoxParameter::valueChanged); + mainLayout->addWidget(new QLabel(tr("Y:"))); + _b = new QSpinBox(); + _b->setRange( + parameter->settings()["minimum"].toPoint().y(), + parameter->settings()["maximum"].toPoint().y() + ); + mainLayout->addWidget(_b); + _b->setValue(_currentValue.toPoint().y()); + connect(_b, &QSpinBox::valueChanged, this, &CheckBoxParameter::valueChanged); +} + + +QVariant IntegerPositionParameter::value() const +{ + return QPoint(_a->value(), _b->value()); +} + + +void IntegerPositionParameter::setValue(const QVariant &value) +{ + if (_currentValue != value) { + _a->setValue(value.toPoint().x()); + _b->setValue(value.toPoint().y()); + } +} + diff --git a/ParameterWidget.hpp b/ParameterWidget.hpp new file mode 100644 index 0000000..99d72f2 --- /dev/null +++ b/ParameterWidget.hpp @@ -0,0 +1,114 @@ +// +// (c)2021 by Lucky Resistor. https://luckyresistor.me/ +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// 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. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// +#pragma once + + +#include "ParameterEntry.hpp" + +#include + + +class QCheckBox; +class QSpinBox; + + +/// The base class of all parameter widgets. +/// +class ParameterWidget : public QWidget +{ + Q_OBJECT + +public: + /// Create the widget. + /// + ParameterWidget(const ParameterEntryPtr ¶meter, QWidget *parent = nullptr); + + /// Get the current value for the widget. + /// + virtual QVariant value() const = 0; + + /// Set the value for this widget. + /// + virtual void setValue(const QVariant &value) = 0; + +signals: + /// Emitted if the value for this parameter changes. + /// + void valueChanged(); + +protected: + QVariant _currentValue; ///< Buffer to limit signals. +}; + + +class CheckBoxParameter : public ParameterWidget +{ + Q_OBJECT + +public: + CheckBoxParameter(const ParameterEntryPtr ¶meter, QWidget *parent = nullptr); + QVariant value() const override; + void setValue(const QVariant &value) override; + +private: + QCheckBox *_value; +}; + + +class IntegerParameter : public ParameterWidget +{ + Q_OBJECT + +public: + IntegerParameter(const ParameterEntryPtr ¶meter, QWidget *parent = nullptr); + QVariant value() const override; + void setValue(const QVariant &value) override; + +private: + QSpinBox *_value; +}; + + +class IntegerSizeParameter : public ParameterWidget +{ + Q_OBJECT + +public: + IntegerSizeParameter(const ParameterEntryPtr ¶meter, QWidget *parent = nullptr); + QVariant value() const override; + void setValue(const QVariant &value) override; + +private: + QSpinBox *_a; + QSpinBox *_b; +}; + + +class IntegerPositionParameter : public ParameterWidget +{ + Q_OBJECT + +public: + IntegerPositionParameter(const ParameterEntryPtr ¶meter, QWidget *parent = nullptr); + QVariant value() const override; + void setValue(const QVariant &value) override; + +private: + QSpinBox *_a; + QSpinBox *_b; +}; + diff --git a/data/AppLogo.png b/data/AppLogo.png index 159a917292d48ffe5a71639809269c09686390f4..c099c94f7951fba197375379de095a4b5e1fbc88 100644 GIT binary patch literal 4928 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 HWF7u5)4_%B literal 8789 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

7IlKx0bd!01 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 JtaD0e0s#1?U+4e; diff --git a/data/AppLogo@3x.png b/data/AppLogo@3x.png index 3c1a363a8e09a327a20caa13d1862166d8abd3ad..bf245a2ff361ab9a83aaaf9cb1c6819f0e1d55b0 100644 GIT binary patch literal 14774 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 literal 30997 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�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 diff --git a/data/application.css b/data/application.css index 53ee096..6ff17f5 100644 --- a/data/application.css +++ b/data/application.css @@ -14,8 +14,16 @@ color: #f8f8f8; } -#BitmapInfo { +#BitmapInfo, #FontInfo { border: 1px solid rgba(0,0,0,0.25); padding: 8px; margin: 0px 0px 16px 0px; } + +#ParameterScrollArea { + border: none; +} + +#ParameterFrame { + background-color: #616875; +} diff --git a/main.cpp b/main.cpp index 3cc1424..7b68f09 100644 --- a/main.cpp +++ b/main.cpp @@ -14,16 +14,19 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . // -#include "MainWindow.hpp" + + +#include "ApplicationController.hpp" #include #include + int main(int argc, char *argv[]) { QApplication a(argc, argv); a.setApplicationName("MicroPython Bitmap Tool"); - a.setApplicationVersion("1.0.1"); + a.setApplicationVersion("1.2.0"); a.setApplicationDisplayName("MicroPython Bitmap Tool"); a.setOrganizationDomain("luckyresistor.me"); a.setOrganizationName("Lucky Resistoor"); @@ -32,7 +35,6 @@ int main(int argc, char *argv[]) a.setStyleSheet(QString::fromUtf8(styleSheet.readAll())); styleSheet.close(); - MainWindow w; - w.show(); + ApplicationController app; return a.exec(); }