diff --git a/source/qml_v2/AdaptiveCardQmlEngine/AdaptiveCardContext.cpp b/source/qml_v2/AdaptiveCardQmlEngine/AdaptiveCardContext.cpp index 2d3a2e9678..e9e696d3d3 100644 --- a/source/qml_v2/AdaptiveCardQmlEngine/AdaptiveCardContext.cpp +++ b/source/qml_v2/AdaptiveCardQmlEngine/AdaptiveCardContext.cpp @@ -98,5 +98,54 @@ namespace AdaptiveCardQmlEngine m_lang = lang; } + void AdaptiveCardContext::addHeightEstimate(const int height) + { + m_HeightEstimate += height; + } + + void AdaptiveCardContext::setHeightEstimate(const int height) + { + m_HeightEstimate = height; + } + + const int AdaptiveCardContext::getHeightEstimate() + { + return m_HeightEstimate; + } + + const int AdaptiveCardQmlEngine::AdaptiveCardContext::getEstimatedTextHeight(const std::string text) + { + auto mCardConfig = this->getCardConfig()->getCardConfig(); + + int height = 0; + + height += ((((int(text.size()) * mCardConfig.averageCharWidth) / mCardConfig.cardWidth) + 1) * mCardConfig.averageCharHeight); + height += (int(std::count(text.begin(), text.end(), '\n')) * mCardConfig.averageCharHeight); + height += mCardConfig.averageSpacing; + + return height; + } + + const std::vector& AdaptiveCardContext::GetWarnings() + { + return m_warnings; + } + + void AdaptiveCardContext::AddWarning(const AdaptiveWarning& warning) + { + m_warnings.push_back(warning); + } + + + void AdaptiveCardQmlEngine::AdaptiveCardContext::addToRequiredInputElementsIdList(const std::string& elementId) + { + m_RequiredInputElementsIdList.push_back(elementId); + } + + const std::vector& AdaptiveCardQmlEngine::AdaptiveCardContext::getRequiredInputElementsIdList() + { + return m_RequiredInputElementsIdList; + } + } // namespace AdaptiveCardQmlEngine diff --git a/source/qml_v2/AdaptiveCardQmlEngine/AdaptiveCardContext.h b/source/qml_v2/AdaptiveCardQmlEngine/AdaptiveCardContext.h index 8b28e6f0d5..ba3c0882eb 100644 --- a/source/qml_v2/AdaptiveCardQmlEngine/AdaptiveCardContext.h +++ b/source/qml_v2/AdaptiveCardQmlEngine/AdaptiveCardContext.h @@ -7,6 +7,7 @@ #include "AdaptiveCardLightThemeConfig.h" #include "AdaptiveRenderArgs.h" +#include "AdaptiveWarning.h" #include "HostConfig.h" #include "Formatter.h" @@ -42,6 +43,19 @@ namespace AdaptiveCardQmlEngine std::string getLang(); void setLang(const std::string& lang); + void addHeightEstimate(const int height); + void setHeightEstimate(const int height); + const int getHeightEstimate(); + + const int getEstimatedTextHeight(const std::string text); + + const std::vector& GetWarnings(); + void AddWarning(const AdaptiveWarning& warning); + + void addToRequiredInputElementsIdList(const std::string& elementId); + const std::vector& getRequiredInputElementsIdList(); + + private: AdaptiveCardContext(); ~AdaptiveCardContext(); @@ -54,6 +68,12 @@ namespace AdaptiveCardQmlEngine std::shared_ptr mHostConfig; std::shared_ptr mCardConfig; AdaptiveCardEnums::AdaptiveCardTheme mAdaptiveCardTheme; - std::string m_lang; + std::string m_lang; + std::vector m_RequiredInputElementsIdList; + + + std::vector m_warnings; + + int m_HeightEstimate{0}; }; } diff --git a/source/qml_v2/AdaptiveCardQmlEngine/AdaptiveCardQmlTypes.h b/source/qml_v2/AdaptiveCardQmlEngine/AdaptiveCardQmlTypes.h index 168a931b58..998f5bc7c0 100644 --- a/source/qml_v2/AdaptiveCardQmlEngine/AdaptiveCardQmlTypes.h +++ b/source/qml_v2/AdaptiveCardQmlEngine/AdaptiveCardQmlTypes.h @@ -23,5 +23,11 @@ namespace AdaptiveCardQmlEngine qmlRegisterType(QUrl("qrc:qml/CardConstants.qml"), "AdaptiveCardQmlEngine", 1, 0, "CardConstants"); qmlRegisterType(QUrl("qrc:qml/TextBlockRender.qml"), "AdaptiveCardQmlEngine", 1, 0, "TextBlockRender"); + qmlRegisterType(QUrl("qrc:qml/ImageRender.qml"), "AdaptiveCardQmlEngine", 1, 0, "ImageRender"); + qmlRegisterType(QUrl("qrc:qml/RichTextBlockRender.qml"), "AdaptiveCardQmlEngine", 1, 0, "RichTextBlockRender"); + qmlRegisterType(QUrl("qrc:qml/NumberInputRender.qml"), "AdaptiveCardQmlEngine", 1, 0, "NumberInputRender"); + qmlRegisterType(QUrl("qrc:qml/InputFieldClearIcon.qml"), "AdaptiveCardQmlEngine", 1, 0, "InputFieldClearIcon"); + qmlRegisterType(QUrl("qrc:qml/InputLabel.qml"), "AdaptiveCardQmlEngine", 1, 0, "InputLabel"); + qmlRegisterType(QUrl("qrc:qml/InputErrorMessage.qml"), "AdaptiveCardQmlEngine", 1, 0, "InputErrorMessage"); } } // namespace RendererQml diff --git a/source/qml_v2/AdaptiveCardQmlEngine/CMakeLists.txt b/source/qml_v2/AdaptiveCardQmlEngine/CMakeLists.txt index 82fe7bada8..f4e093f5c9 100644 --- a/source/qml_v2/AdaptiveCardQmlEngine/CMakeLists.txt +++ b/source/qml_v2/AdaptiveCardQmlEngine/CMakeLists.txt @@ -57,6 +57,9 @@ file(GLOB_RECURSE SOURCES "TextBlockModel.cpp" "ImageModel.cpp" "RichTextBlockModel.cpp" + "NumberInputModel.cpp" + "AdaptiveWarning.cpp" + "AdaptiveCardQmlTypes.h" "AdaptiveCardUtils.cpp" @@ -74,10 +77,11 @@ file(GLOB_RECURSE SOURCES "AdaptiveCardModel.h" "CollectionItemModel.h" - "TextBlockModel.h" - "ImageModel.h" - "TextBlockModel.h" - "RichTextBlockModel.h" + "TextBlockModel.h" + "ImageModel.h" + "RichTextBlockModel.h" + "NumberInputModel.h" + "AdaptiveWarning.h" ) # Setup Library diff --git a/source/qml_v2/AdaptiveCardQmlEngine/models/AdaptiveWarning.cpp b/source/qml_v2/AdaptiveCardQmlEngine/models/AdaptiveWarning.cpp new file mode 100644 index 0000000000..b6b91c1dd3 --- /dev/null +++ b/source/qml_v2/AdaptiveCardQmlEngine/models/AdaptiveWarning.cpp @@ -0,0 +1,24 @@ +#include "pch.h" + +#include "AdaptiveWarning.h" + +namespace AdaptiveCardQmlEngine +{ + + AdaptiveWarning::AdaptiveWarning(Code code, const std::string& message) : + m_code(code), m_message(message) + { + + } + + Code AdaptiveWarning::GetStatusCode() const + { + return m_code; + } + + const std::string& AdaptiveWarning::GetReason() const + { + return m_message; + } + +} diff --git a/source/qml_v2/AdaptiveCardQmlEngine/models/AdaptiveWarning.h b/source/qml_v2/AdaptiveCardQmlEngine/models/AdaptiveWarning.h new file mode 100644 index 0000000000..599c27deca --- /dev/null +++ b/source/qml_v2/AdaptiveCardQmlEngine/models/AdaptiveWarning.h @@ -0,0 +1,25 @@ +#pragma once + +#include "pch.h" + +namespace AdaptiveCardQmlEngine +{ + + enum class Code + { + RenderException = 1 + }; + + class AdaptiveWarning + { + public: + AdaptiveWarning(Code code, const std::string& message); + Code GetStatusCode() const; + const std::string& GetReason() const; + + private: + const Code m_code; + const std::string m_message; + }; + +} diff --git a/source/qml_v2/AdaptiveCardQmlEngine/models/CollectionItemModel.cpp b/source/qml_v2/AdaptiveCardQmlEngine/models/CollectionItemModel.cpp index f570d92bc0..a8469443a1 100644 --- a/source/qml_v2/AdaptiveCardQmlEngine/models/CollectionItemModel.cpp +++ b/source/qml_v2/AdaptiveCardQmlEngine/models/CollectionItemModel.cpp @@ -2,6 +2,7 @@ #include "TextBlockModel.h" #include "ImageModel.h" #include "RichTextBlockModel.h" +#include "NumberInputModel.h" #include "AdaptiveCardEnums.h" CollectionItemModel::CollectionItemModel(std::vector> elements, QObject* parent) @@ -43,6 +44,8 @@ QHash CollectionItemModel::roleNames() const cardListModel[TextBlockRole] = "textBlockRole"; cardListModel[ImageRole] = "imageRole"; cardListModel[RichTextBlockRole] = "richTextBlockRole"; + cardListModel[NumberInputRole] = "numberInputRole"; + cardListModel[FillHeightRole] = "fillHeightRole"; return cardListModel; @@ -64,6 +67,9 @@ void CollectionItemModel::populateRowData(std::shared_ptr(element), rowContent); break; + case AdaptiveCards::CardElementType::NumberInput: + populateNumberInputModel(std::dynamic_pointer_cast(element), rowContent); + break; default: break; } @@ -86,3 +92,8 @@ void CollectionItemModel::populateRichTextBlockModel(std::shared_ptr numberInput, RowContent& rowContent) +{ + rowContent[CollectionModelRole::DelegateType] = QVariant::fromValue(AdaptiveCardEnums::CardElementType::NumberInput); + rowContent[CollectionModelRole::NumberInputRole] = QVariant::fromValue(new NumberInputModel(numberInput, nullptr)); +} diff --git a/source/qml_v2/AdaptiveCardQmlEngine/models/CollectionItemModel.h b/source/qml_v2/AdaptiveCardQmlEngine/models/CollectionItemModel.h index eb71dfaa2c..354ee96ace 100644 --- a/source/qml_v2/AdaptiveCardQmlEngine/models/CollectionItemModel.h +++ b/source/qml_v2/AdaptiveCardQmlEngine/models/CollectionItemModel.h @@ -2,19 +2,18 @@ #include #include - #include #include -#include "Enums.h" - -class TextBlockModel; -class ImageModel; #include "RichTextBlock.h" - +#include #include "Enums.h" + class TextBlockModel; +class ImageModel; class RichTextBlockModel; +class NumberInputModel; + class CollectionItemModel : public QAbstractListModel { @@ -26,6 +25,7 @@ class CollectionItemModel : public QAbstractListModel TextBlockRole, ImageRole, RichTextBlockRole, + NumberInputRole, FillHeightRole }; @@ -48,4 +48,5 @@ class CollectionItemModel : public QAbstractListModel void populateTextBlockModel(std::shared_ptr textBlock, RowContent& rowContent); void populateImageModel(std::shared_ptr image, RowContent& rowContent); void populateRichTextBlockModel(std::shared_ptr rightTextBlock, RowContent& rowContent); + void populateNumberInputModel(std::shared_ptr numberInput, RowContent& rowContent); }; diff --git a/source/qml_v2/AdaptiveCardQmlEngine/models/ImageModel.cpp b/source/qml_v2/AdaptiveCardQmlEngine/models/ImageModel.cpp index 8ce23b772c..6bd2cc4a30 100644 --- a/source/qml_v2/AdaptiveCardQmlEngine/models/ImageModel.cpp +++ b/source/qml_v2/AdaptiveCardQmlEngine/models/ImageModel.cpp @@ -6,6 +6,7 @@ ImageModel::ImageModel(std::shared_ptr image, QObject* parent) + : QObject(parent),mImage(image) { setImageLayoutProperties(); diff --git a/source/qml_v2/AdaptiveCardQmlEngine/models/ImageModel.h b/source/qml_v2/AdaptiveCardQmlEngine/models/ImageModel.h index f51a0a29f7..3d2d0d0377 100644 --- a/source/qml_v2/AdaptiveCardQmlEngine/models/ImageModel.h +++ b/source/qml_v2/AdaptiveCardQmlEngine/models/ImageModel.h @@ -19,6 +19,7 @@ class ImageModel : public QObject Q_PROPERTY(QString anchorCenter MEMBER mAnchorCenter CONSTANT); Q_PROPERTY(QString anchorRight MEMBER mAnchorRight CONSTANT); Q_PROPERTY(QString anchorLeft MEMBER mAnchorLeft CONSTANT); + Q_PROPERTY(QString actionType MEMBER mActionType CONSTANT); Q_PROPERTY(QString submitData MEMBER mSubmitJSON CONSTANT); Q_PROPERTY(QString openUrl MEMBER mOpenUrl CONSTANT); @@ -39,6 +40,7 @@ class ImageModel : public QObject private: QString GetImagePath(const std::string url); + void setImageLayoutProperties(); void setImageVisualProperties(); void setImageActionProperties(); diff --git a/source/qml_v2/AdaptiveCardQmlEngine/models/NumberInputModel.cpp b/source/qml_v2/AdaptiveCardQmlEngine/models/NumberInputModel.cpp new file mode 100644 index 0000000000..2de7b5a0b4 --- /dev/null +++ b/source/qml_v2/AdaptiveCardQmlEngine/models/NumberInputModel.cpp @@ -0,0 +1,76 @@ +#include "NumberInputModel.h" +#include "SharedAdaptiveCard.h" +#include +#include "Utils.h" +#include "MarkDownParser.h" + +NumberInputModel::NumberInputModel(std::shared_ptr numberInput, QObject* parent) : + QObject(parent), mInput(numberInput) + +{ + initialize(); + createInputLabel(); + createErrorMessage(); +} + +void NumberInputModel::initialize() +{ + const auto hostConfig = AdaptiveCardQmlEngine::AdaptiveCardContext::getInstance().getHostConfig(); + + mVisible = mInput->GetIsVisible(); + mPlaceholder = QString::fromStdString(AdaptiveCardQmlEngine::Utils::getBackQuoteEscapedString(mInput->GetPlaceholder())); + + if (mInput->GetValue().has_value()) + { + mValue = mInput->GetValue().value(); + mDefaultValue = "true"; + } + mMinValue = mInput->GetMin().value_or(-DBL_MAX); + mMaxValue = mInput->GetMax().value_or(DBL_MAX); + + if (mInput->GetIsRequired() || mInput->GetMin().has_value() || mInput->GetMax().has_value()) + { + AdaptiveCardQmlEngine::AdaptiveCardContext::getInstance().addToRequiredInputElementsIdList(mInput->GetId()); + mIsRequired = mInput->GetIsRequired(); + mValidationRequired = mInput->GetIsRequired(); + } +} + +void NumberInputModel::createInputLabel() +{ + if (!mInput->GetLabel().empty()) + { + AdaptiveCardQmlEngine::AdaptiveCardContext::getInstance().addHeightEstimate( + AdaptiveCardQmlEngine::AdaptiveCardContext::getInstance().getEstimatedTextHeight(mInput->GetLabel())); + mEscapedLabelString = + QString::fromStdString(AdaptiveCardQmlEngine::Utils::getBackQuoteEscapedString(mInput->GetLabel())); + } + else + { + if (mInput->GetIsRequired()) + { + AdaptiveCardQmlEngine::AdaptiveCardContext::getInstance().AddWarning(AdaptiveCardQmlEngine::AdaptiveWarning( + AdaptiveCardQmlEngine::Code::RenderException, "isRequired is not supported without labels")); + } + } +} + +void NumberInputModel::createErrorMessage() +{ + if (!mInput->GetErrorMessage().empty()) + { + // mNumberInputQmlElement->Property("_mEscapedErrorString",RendererQml::Formatter() << "String.raw`"<< RendererQml::Utils::getBackQuoteEscapedString(mInput->GetErrorMessage()) << "`"); + mEscapedErrorString = QString::fromStdString(AdaptiveCardQmlEngine::Utils::getBackQuoteEscapedString(mInput->GetErrorMessage())); + } + else + { + if (mInput->GetIsRequired()) + { + AdaptiveCardQmlEngine::AdaptiveCardContext::getInstance().AddWarning(AdaptiveCardQmlEngine::AdaptiveWarning(AdaptiveCardQmlEngine::Code::RenderException, "isRequired is not supported without error message")); + } + } +} + +NumberInputModel::~NumberInputModel() +{ +} diff --git a/source/qml_v2/AdaptiveCardQmlEngine/models/NumberInputModel.h b/source/qml_v2/AdaptiveCardQmlEngine/models/NumberInputModel.h new file mode 100644 index 0000000000..c792c2dc4b --- /dev/null +++ b/source/qml_v2/AdaptiveCardQmlEngine/models/NumberInputModel.h @@ -0,0 +1,56 @@ +#pragma once +#include "AdaptiveCardContext.h" +#include "NumberInput.h" +#include "OpenUrlAction.h" +#include "ToggleVisibilityAction.h" +#include "SubmitAction.h" + +#include +#include +#include +#include + +class NumberInputModel : public QObject +{ + Q_OBJECT + + Q_PROPERTY(QString placeHolder MEMBER mPlaceholder CONSTANT); + Q_PROPERTY(QString escapedLabelString MEMBER mEscapedLabelString CONSTANT); + Q_PROPERTY(QString defaultValue MEMBER mDefaultValue CONSTANT); + Q_PROPERTY(QString escapedErrorString MEMBER mEscapedErrorString CONSTANT); + + Q_PROPERTY(double minValue MEMBER mMinValue CONSTANT); + Q_PROPERTY(double maxValue MEMBER mMaxValue CONSTANT); + + Q_PROPERTY(bool visible MEMBER mVisible CONSTANT); + Q_PROPERTY(bool isRequired MEMBER mIsRequired CONSTANT); + Q_PROPERTY(bool validationRequired MEMBER mValidationRequired CONSTANT); + + Q_PROPERTY(int value MEMBER mValue CONSTANT); + +public: + explicit NumberInputModel(std::shared_ptr numberInput, QObject* parent = nullptr); + ~NumberInputModel(); + +private: + void initialize(); + void createInputLabel(); + void createErrorMessage(); + +private: + const std::shared_ptr& mInput; + + QString mPlaceholder; + QString mEscapedLabelString; + QString mDefaultValue; + QString mEscapedErrorString; + + double mMinValue; + double mMaxValue; + + bool mVisible; + bool mIsRequired; + bool mValidationRequired; + + int mValue; +}; diff --git a/source/qml_v2/AdaptiveCardQmlEngine/qml/CardConstants.qml b/source/qml_v2/AdaptiveCardQmlEngine/qml/CardConstants.qml index 720a6bd9e4..661ac845c7 100644 --- a/source/qml_v2/AdaptiveCardQmlEngine/qml/CardConstants.qml +++ b/source/qml_v2/AdaptiveCardQmlEngine/qml/CardConstants.qml @@ -1,6 +1,6 @@ -import "AdaptiveCardUtils.js" as AdaptiveCardUtils +import "JSUtils/AdaptiveCardUtils.js" as AdaptiveCardUtils import QtQuick 2.15 -pragma Singleton +//pragma Singleton QtObject { property bool isDarkTheme: false diff --git a/source/qml_v2/AdaptiveCardQmlEngine/qml/CollectionItemDelegate.qml b/source/qml_v2/AdaptiveCardQmlEngine/qml/CollectionItemDelegate.qml index 99c2165994..6afe92a24a 100644 --- a/source/qml_v2/AdaptiveCardQmlEngine/qml/CollectionItemDelegate.qml +++ b/source/qml_v2/AdaptiveCardQmlEngine/qml/CollectionItemDelegate.qml @@ -7,12 +7,15 @@ Loader { property var parentCardItem - Component.onCompleted :{ + Component.onCompleted :{ if (model.delegateType == AdaptiveCardEnums.CardElementType.TextBlock) source = "TextBlockRender.qml" - else if (model.delegateType == AdaptiveCardEnums.CardElementType.Image) - source = "ImageRender.qml" else if (model.delegateType == AdaptiveCardEnums.CardElementType.RichTextBlock) source = "RichTextBlockRender.qml"; - } + else if (model.delegateType == AdaptiveCardEnums.CardElementType.Image) + source = "ImageRender.qml" + else if (model.delegateType == AdaptiveCardEnums.CardElementType.NumberInput) + source = "NumberInputRender.qml"; + } + } diff --git a/source/qml_v2/AdaptiveCardQmlEngine/qml/InputErrorMessage.qml b/source/qml_v2/AdaptiveCardQmlEngine/qml/InputErrorMessage.qml new file mode 100644 index 0000000000..c60b42406f --- /dev/null +++ b/source/qml_v2/AdaptiveCardQmlEngine/qml/InputErrorMessage.qml @@ -0,0 +1,48 @@ +import QtQuick 2.15 +import QtQuick.Layouts 1.3 +import QtGraphicalEffects 1.15 +import QtQuick.Controls 2.15 +import AdaptiveCardQmlEngine 1.0 +import "JSUtils/AdaptiveCardUtils.js" as AdaptiveCardUtils + +Rectangle { + id: errorMessage + + width: parent.width + height: errorMessageLabel.implicitHeight + color: 'transparent' + + Button { + id: errorIcon + + width: cardConst.inputFieldConstants.errorIconWidth + anchors.left: parent.left + anchors.leftMargin: cardConst.inputFieldConstants.errorIconLeftMargin + anchors.topMargin: cardConst.inputFieldConstants.errorIconTopMargin + horizontalPadding: 0 + verticalPadding: 0 + icon.width: cardConst.inputFieldConstants.errorIconWidth + icon.height: cardConst.inputFieldConstants.errorIconHeight + icon.color: cardConst.toggleButtonConstants.errorMessageColor + anchors.top: parent.top + icon.source: cardConst.errorIcon + enabled: false + + background: Rectangle { + color: 'transparent' + } + } + + Label { + id: errorMessageLabel + + wrapMode: Text.Wrap + font.pixelSize: cardConst.inputFieldConstants.labelPixelSize + Accessible.ignored: true + color: cardConst.toggleButtonConstants.errorMessageColor + anchors.left: errorIcon.right + anchors.leftMargin: cardConst.inputFieldConstants.errorIconLeftMargin + anchors.right: parent.right + text: numberinputModel.escapedErrorString + } +} diff --git a/source/qml_v2/AdaptiveCardQmlEngine/qml/InputFieldClearIcon.qml b/source/qml_v2/AdaptiveCardQmlEngine/qml/InputFieldClearIcon.qml new file mode 100644 index 0000000000..6eebe2e0d7 --- /dev/null +++ b/source/qml_v2/AdaptiveCardQmlEngine/qml/InputFieldClearIcon.qml @@ -0,0 +1,31 @@ +import QtQuick 2.15 +import QtQuick.Layouts 1.3 +import QtGraphicalEffects 1.15 +import QtQuick.Controls 2.15 +import AdaptiveCardQmlEngine 1.0 +import "JSUtils/AdaptiveCardUtils.js" as AdaptiveCardUtils + +Button { + id: inputFieldClearIcon + + width: cardConst.inputFieldConstants.clearIconSize + horizontalPadding: 0 + verticalPadding: 0 + icon.width: cardConst.inputFieldConstants.clearIconSize + icon.height: cardConst.inputFieldConstants.clearIconSize + icon.color: cardConst.inputFieldConstants.clearIconColorNormal + icon.source: cardConst.clearIconImage + Accessible.name: qsTr("Clear Input") + Accessible.role: Accessible.Button + + background: Rectangle { + color: 'transparent' + radius: cardConst.inputFieldConstants.borderRadius + + WCustomFocusItem { + isRectangle: true + visible: inputFieldClearIcon.activeFocus + designatedParent: parent + } + } +} diff --git a/source/qml_v2/AdaptiveCardQmlEngine/qml/InputLabel.qml b/source/qml_v2/AdaptiveCardQmlEngine/qml/InputLabel.qml new file mode 100644 index 0000000000..889647692b --- /dev/null +++ b/source/qml_v2/AdaptiveCardQmlEngine/qml/InputLabel.qml @@ -0,0 +1,16 @@ +import QtQuick 2.15 +import QtQuick.Layouts 1.3 +import QtGraphicalEffects 1.15 +import QtQuick.Controls 2.15 +import AdaptiveCardQmlEngine 1.0 +import "JSUtils/AdaptiveCardUtils.js" as AdaptiveCardUtils +Label { + id: inputLabel + + wrapMode: Text.Wrap + width: parent.width + color: cardConst.toggleButtonConstants.textColor + font.pixelSize: cardConst.inputFieldConstants.labelPixelSize + Accessible.ignored: true + text: numberinputModel.isRequired ? AdaptiveCardUtils.escapeHtml( numberinputModel.escapedLabelString) + " " + "*" : AdaptiveCardUtils.escapeHtml( numberinputModel.escapedLabelString) +} diff --git a/source/qml_v2/AdaptiveCardQmlEngine/qml/NumberInputRender.qml b/source/qml_v2/AdaptiveCardQmlEngine/qml/NumberInputRender.qml new file mode 100644 index 0000000000..b5f3b6cce2 --- /dev/null +++ b/source/qml_v2/AdaptiveCardQmlEngine/qml/NumberInputRender.qml @@ -0,0 +1,280 @@ +import QtQuick 2.15 +import QtQuick.Layouts 1.3 +import QtGraphicalEffects 1.15 +import QtQuick.Controls 2.15 +import AdaptiveCardQmlEngine 1.0 +import "JSUtils/AdaptiveCardUtils.js" as AdaptiveCardUtils +Column { + id: numberInput + property var numberinputModel:model.numberInputRole + property bool showErrorMessage: false + property int spinBoxMinVal : Math.max(-2147483648, numberinputModel.minValue) + property int spinBoxMaxVal : Math.min(2147483647, numberinputModel.maxValue) + CardConstants{ + id:cardConst + + } + + function validate() { + if (numberInputTextField.text.length !== 0 && Number(numberInputTextField.text) >= numberinputModel.minValue && Number(numberInputTextField.text) <= numberinputModel.maxValue) { + showErrorMessage = false; + return false; + } else { + return true; + } + } + + function getAccessibleName() { + let accessibleName = ''; + if (showErrorMessage) + accessibleName += 'Error. ' + numberinputModel.escapedErrorString + '. '; + + if (numberinputModel.escapedLabelString) + accessibleName += numberinputModel.escapedLabelString + '. '; + + if (numberInputTextField.text !== '') + accessibleName += (numberInputTextField.text); + else + accessibleName += numberinputModel.placeHolder; + accessibleName += qsTr(", Type the number"); + return accessibleName; + } + width: parent.width + spacing: cardConst.inputFieldConstants.columnSpacing + onShowErrorMessageChanged: { + numberInputRectangle.colorChange(false); + } + onActiveFocusChanged: { + if (activeFocus) + numberInputTextField.forceActiveFocus(); + + } + visible: numberinputModel.visible + InputLabel { + id: numberInputLabel + visible:numberinputModel.escapedLabelString + } + + Row { + id: numberInputRow + + width: parent.width + height: cardConst.inputFieldConstants.height + + Rectangle { + id: numberInputRectangle + + function colorChange(isPressed) { + if (isPressed && !showErrorMessage) + color = inputFieldConstants.backgroundColorOnPressed; + else + color = numberInputTextField.activeFocus ? cardConst.inputFieldConstants.backgroundColorOnPressed : numberInputTextField.hovered ? cardConst.inputFieldConstants.backgroundColorOnHovered : cardConst.inputFieldConstants.backgroundColorNormal; + } + + border.width: cardConst.inputFieldConstants.borderWidth + border.color: showErrorMessage ? cardConst.inputFieldConstants.borderColorOnError : cardConst.inputFieldConstants.borderColorNormal + radius: cardConst.inputFieldConstants.borderRadius + height: parent.height + color: numberInputSpinBox.Opressed ? cardConst.inputFieldConstants.backgroundColorOnPressed : numberInputSpinBox.hovered ? cardConst.inputFieldConstants.backgroundColorOnHovered : cardConst.inputFieldConstants.backgroundColorNormal + width: parent.width - numberInputArrowRectangle.width + WCustomFocusItem { + isRectangle: true + visible: numberInputTextField.activeFocus + } + + SpinBox { + id: numberInputSpinBox + + + function changeValue(keyPressed) { + if ((keyPressed === Qt.Key_Up || keyPressed === Qt.Key_Down) && numberInputTextField.text.length === 0) { + value = (from > 0) ? from : 0; + } else if (keyPressed === Qt.Key_Up) { + numberInputSpinBox.value = Number(numberInputTextField.text); + numberInputSpinBox.increase(); + } else if (keyPressed === Qt.Key_Down) { + numberInputSpinBox.value = Number(numberInputTextField.text); + numberInputSpinBox.decrease(); + } + numberInputTextField.text = numberInputSpinBox.value; + } + + width: parent.width - numberInputClearIcon.width - cardConst.inputFieldConstants.clearIconHorizontalPadding + padding: 0 + editable: true + stepSize: 1 + to: spinBoxMaxVal + from: spinBoxMinVal + Keys.onPressed: { + if (event.key === Qt.Key_Up || event.key === Qt.Key_Down) { + numberInputSpinBox.changeValue(event.key); + event.accepted = true; + } + } + Accessible.ignored: true + Component.onCompleted: { + if (numberinputModel.defaultValue) + numberInputSpinBox.value = numberinputModel.value; + + } + + contentItem: TextField { + id: numberInputTextField + + font.pixelSize: cardConst.inputFieldConstants.pixelSize + anchors.left: parent.left + anchors.right: parent.right + selectByMouse: true + selectedTextColor: 'white' + readOnly: !numberInputSpinBox.editable + validator: numberInputSpinBox.validator + inputMethodHints: Qt.ImhFormattedNumbersOnly + onPressed: { + numberInputRectangle.colorChange(true); + event.accepted = true; + } + onReleased: { + numberInputRectangle.colorChange(false); + forceActiveFocus(); + event.accepted = true; + } + onHoveredChanged: numberInputRectangle.colorChange(false) + onActiveFocusChanged: { + numberInputRectangle.colorChange(false); + Accessible.name = getAccessibleName(); + } + + leftPadding: cardConst.inputFieldConstants.textHorizontalPadding + rightPadding: cardConst.inputFieldConstants.textHorizontalPadding + topPadding: cardConst.inputFieldConstants.textVerticalPadding + bottomPadding: cardConst.inputFieldConstants.textVerticalPadding + placeholderText: numberinputModel.placeHolder + Accessible.role: Accessible.EditableText + color: cardConst.inputFieldConstants.textColor + placeholderTextColor: cardConst.inputFieldConstants.placeHolderColor + onTextChanged: { + validate(); + } + Component.onCompleted: { + if (numberinputModel.defaultValue) + numberInputTextField.text = numberInputSpinBox.value; + } + + background: Rectangle { + id: numberInputTextFieldBg + + color: 'transparent' + } + + } + + background: Rectangle { + id: numberSpinBoxBg + + color: 'transparent' + } + + up.indicator: Rectangle { + color: 'transparent' + z: -1 + } + + down.indicator: Rectangle { + color: 'transparent' + z: -1 + } + + validator: DoubleValidator { + } + } + + InputFieldClearIcon { + id: numberInputClearIcon + + Keys.onReturnPressed: onClicked() + visible: numberInputTextField.length !== 0 + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: cardConst.inputFieldConstants.clearIconHorizontalPadding + onClicked: { + nextItemInFocusChain().forceActiveFocus(); + numberInputSpinBox.value = numberInputSpinBox.from; + numberInputTextField.clear(); + } + } + } + + Rectangle { + id: numberInputArrowRectangle + + property string accessibilityPrefix: '' + + width: cardConst.inputNumberConstants.upDownButtonWidth + radius: cardConst.inputFieldConstants.borderRadius + height: parent.height + border.color: cardConst.inputFieldConstants.borderColorNormal + activeFocusOnTab: true + color:(numberInputArrowIcon.pressed || activeFocus) ? cardConst.inputFieldConstants.backgroundColorOnPressed : numberInputArrowIcon.hovered ? cardConst.inputFieldConstants.backgroundColorOnHovered : cardConst.inputFieldConstants.backgroundColorNormal + + onActiveFocusChanged: { + if (activeFocus) + accessibilityPrefix = qsTr("Use up arrow to increase the value and down arrow to decrease the value") + (numberInputTextField.text ? ", Current number is " : ""); + + } + Accessible.name: accessibilityPrefix + numberInputTextField.displayText + Accessible.role: Accessible.NoRole + + Button { + id: numberInputArrowIcon + + width: parent.width + anchors.right: parent.right + horizontalPadding: cardConst.inputFieldConstants.iconPadding + verticalPadding: cardConst.inputFieldConstants.iconPadding + icon.width: cardConst.numberInputConstants.upDownIconSize + icon.height: cardConst.numberInputConstants.upDownIconSize + focusPolicy: Qt.NoFocus + icon.color: cardConst.inputNumberConstants.upDownIconColor + height: parent.height + icon.source: cardConst.numberInputUpDownArrowImage + + background: Rectangle { + color: 'transparent' + } + } + + MouseArea { + id: numberInputSpinBoxUpIndicatorArea + + width: parent.width + height: parent.height / 2 + anchors.top: parent.top + onReleased: { + numberInputSpinBox.changeValue(Qt.Key_Up); + numberInputArrowRectangle.forceActiveFocus(); + } + } + + MouseArea { + id: numberInputSpinBoxDownIndicatorArea + + width: parent.width + height: parent.height / 2 + anchors.top: numberInputSpinBoxUpIndicatorArea.bottom + onReleased: { + numberInputSpinBox.changeValue(Qt.Key_Down); + numberInputArrowRectangle.forceActiveFocus(); + } + } + + WCustomFocusItem { + isRectangle: true + } + } + } + + InputErrorMessage { + id: numberInputErrorMessage + visible: showErrorMessage + } +} \ No newline at end of file diff --git a/source/qml_v2/AdaptiveCardQmlEngine/resourceEngine.qrc b/source/qml_v2/AdaptiveCardQmlEngine/resourceEngine.qrc index c23bc8b2b0..4cfeb92e46 100644 --- a/source/qml_v2/AdaptiveCardQmlEngine/resourceEngine.qrc +++ b/source/qml_v2/AdaptiveCardQmlEngine/resourceEngine.qrc @@ -16,5 +16,9 @@ images/3.jpg images/4.jpg qml/RichTextBlockRender.qml + qml/NumberInputRender.qml + qml/InputLabel.qml + qml/InputFieldClearIcon.qml + qml/InputErrorMessage.qml diff --git a/source/qml_v2/AdaptiveCardQmlEngine/utils/AdaptiveCardEnums.h b/source/qml_v2/AdaptiveCardQmlEngine/utils/AdaptiveCardEnums.h index 89e4a43806..688c102a88 100644 --- a/source/qml_v2/AdaptiveCardQmlEngine/utils/AdaptiveCardEnums.h +++ b/source/qml_v2/AdaptiveCardQmlEngine/utils/AdaptiveCardEnums.h @@ -10,6 +10,7 @@ namespace AdaptiveCardEnums TextBlock, Image, RichTextBlock, + NumberInput, Container, Column, ColumnSet, diff --git a/source/qml_v2/AdaptiveCardQmlEngine/utils/Utils.cpp b/source/qml_v2/AdaptiveCardQmlEngine/utils/Utils.cpp index 4400f3373c..f393cf20a7 100644 --- a/source/qml_v2/AdaptiveCardQmlEngine/utils/Utils.cpp +++ b/source/qml_v2/AdaptiveCardQmlEngine/utils/Utils.cpp @@ -208,6 +208,23 @@ namespace AdaptiveCardQmlEngine return true; } + std::string AdaptiveCardQmlEngine::Utils::getBackQuoteEscapedString(std::string str) + { + std::string rawString = ""; + for (int i = 0; i < str.size(); i++) + { + if (str[i] == '`') + { + rawString += "${'`'}"; + } + else + { + rawString += str[i]; + } + } + return rawString; + } + } // namespace AdaptiveCardQmlEngine diff --git a/source/qml_v2/AdaptiveCardQmlEngine/utils/Utils.h b/source/qml_v2/AdaptiveCardQmlEngine/utils/Utils.h index d9a3ecd835..32c72315a1 100644 --- a/source/qml_v2/AdaptiveCardQmlEngine/utils/Utils.h +++ b/source/qml_v2/AdaptiveCardQmlEngine/utils/Utils.h @@ -17,6 +17,7 @@ namespace AdaptiveCardQmlEngine static const std::string formatHtmlUrl(std::string& text, const std::string& linkColor, const std::string& textDecoration); static std::vector splitString(const std::string& string, char delimiter); static std::string& replace(std::string& str, char what, char with); + static std::string getBackQuoteEscapedString(std::string str); template static bool IsInstanceOfSmart(U u); diff --git a/source/qml_v2/AdaptiveCardVisualizer/JSONSamples/numberInput.json b/source/qml_v2/AdaptiveCardVisualizer/JSONSamples/numberInput.json new file mode 100644 index 0000000000..0286aca842 --- /dev/null +++ b/source/qml_v2/AdaptiveCardVisualizer/JSONSamples/numberInput.json @@ -0,0 +1,46 @@ +{ + "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + "type": "AdaptiveCard", + "version": "1.0", + "body": [ + { + "type": "TextBlock", + "text": "Default text input" + }, + { + "type": "Input.Text", + "id": "defaultInputId", + "placeholder": "enter comment", + "maxLength": 500 + }, + { + "type": "TextBlock", + "text": "Multiline text input" + }, + { + "type": "Input.Text", + "id": "multilineInputId", + "placeholder": "enter comment", + "maxLength": 50, + "isMultiline": true + }, + { + "type": "TextBlock", + "text": "Input Number" + }, + { + "type": "Input.Number", + "id": "number", + "placeholder": "Enter a number", + "min": 1, + "max": 10, + "value": 3 + } + ], + "actions": [ + { + "type": "Action.Submit", + "title": "OK" + } + ] +} diff --git a/source/qml_v2/AdaptiveCardVisualizer/SampleCardListModel.cpp b/source/qml_v2/AdaptiveCardVisualizer/SampleCardListModel.cpp index 392880625b..0b5d2a7645 100644 --- a/source/qml_v2/AdaptiveCardVisualizer/SampleCardListModel.cpp +++ b/source/qml_v2/AdaptiveCardVisualizer/SampleCardListModel.cpp @@ -56,8 +56,8 @@ void SampleCardListModel::populateCardDisplayNames() mCardDisplayNames.insert("textBlock.json", "TextBlock"); mCardDisplayNames.insert("richTextBlock.json", "Rich text"); mCardDisplayNames.insert("inputText.json", "Input text"); + mCardDisplayNames.insert("numberInput.json", "Input number"); mCardDisplayNames.insert("actionInline.json", "Action Inline"); - mCardDisplayNames.insert("inputNumber.json", "Input number"); mCardDisplayNames.insert("dateInput.json", "Input date"); mCardDisplayNames.insert("checkBoxInput.json", "Input Toggle"); mCardDisplayNames.insert("choiceSetInput.json", "Input ChoiceSet");