diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 4d789a0ad..8ac52cc89 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -892,6 +892,10 @@ list( APPEND SOURCES tracks/ui/ZoomHandle.cpp tracks/ui/ZoomHandle.h + # ui helpers + ui/AccessibleLinksFormatter.h + ui/AccessibleLinksFormatter.cpp + # Widgets widgets/AButton.cpp diff --git a/src/ShuttleGui.cpp b/src/ShuttleGui.cpp index 206b483b2..1d43798e7 100644 --- a/src/ShuttleGui.cpp +++ b/src/ShuttleGui.cpp @@ -113,6 +113,8 @@ for registering for changes. #include #include #include +#include + #include "../include/audacity/ComponentInterface.h" #include "widgets/ReadOnlyText.h" #include "widgets/wxPanelWrapper.h" @@ -191,6 +193,11 @@ void ShuttleGuiBase::ResetId() } +int ShuttleGuiBase::GetBorder() const noexcept +{ + return miBorder; +} + /// Used to modify an already placed FlexGridSizer to make a column stretchy. void ShuttleGuiBase::SetStretchyCol( int i ) { @@ -289,13 +296,13 @@ void ShuttleGuiBase::AddTitle(const TranslatableString &Prompt, int wrapWidth) /// Very generic 'Add' function. We can add anything we like. /// Useful for unique controls -wxWindow * ShuttleGuiBase::AddWindow(wxWindow * pWindow) +wxWindow* ShuttleGuiBase::AddWindow(wxWindow* pWindow, int PositionFlags) { if( mShuttleMode != eIsCreating ) return pWindow; mpWind = pWindow; SetProportions( 0 ); - UpdateSizersCore(false, wxALIGN_CENTRE | wxALL); + UpdateSizersCore(false, PositionFlags | wxALL); return pWindow; } @@ -1200,6 +1207,25 @@ void ShuttleGuiBase::EndVerticalLay() PopSizer(); } +void ShuttleGuiBase::StartWrapLay(int PositionFlags, int iProp) +{ + if (mShuttleMode != eIsCreating) + return; + + miSizerProp = iProp; + mpSubSizer = std::make_unique(wxHORIZONTAL, 0); + + UpdateSizersCore(false, PositionFlags | wxALL); +} + +void ShuttleGuiBase::EndWrapLay() +{ + if (mShuttleMode != eIsCreating) + return; + + PopSizer(); +} + void ShuttleGuiBase::StartMultiColumn(int nCols, int PositionFlags) { if( mShuttleMode != eIsCreating ) diff --git a/src/ShuttleGui.h b/src/ShuttleGui.h index e176c11c7..49da8fe4e 100644 --- a/src/ShuttleGui.h +++ b/src/ShuttleGui.h @@ -255,7 +255,7 @@ public: void AddPrompt(const TranslatableString &Prompt, int wrapWidth = 0); void AddUnits(const TranslatableString &Prompt, int wrapWidth = 0); void AddTitle(const TranslatableString &Prompt, int wrapWidth = 0); - wxWindow * AddWindow(wxWindow * pWindow); + wxWindow * AddWindow(wxWindow* pWindow, int PositionFlags = wxALIGN_CENTRE); wxSlider * AddSlider( const TranslatableString &Prompt, int pos, int Max, int Min = 0); wxSlider * AddVSlider(const TranslatableString &Prompt, int pos, int Max); @@ -347,10 +347,14 @@ public: // and create the appropriate widget. void StartHorizontalLay(int PositionFlags=wxALIGN_CENTRE, int iProp=1); void EndHorizontalLay(); + void StartVerticalLay(int iProp=1); void StartVerticalLay(int PositionFlags, int iProp); - void EndVerticalLay(); + + void StartWrapLay(int PositionFlags=wxEXPAND, int iProp = 0); + void EndWrapLay(); + wxScrolledWindow * StartScroller(int iStyle=0); void EndScroller(); wxPanel * StartPanel(int iStyle=0); @@ -482,6 +486,7 @@ public: const int min); //-- End of variants. void SetBorder( int Border ) {miBorder = Border;}; + int GetBorder() const noexcept; void SetSizerProportion( int iProp ) {miSizerProp = iProp;}; void SetStretchyCol( int i ); void SetStretchyRow( int i ); diff --git a/src/ui/AccessibleLinksFormatter.cpp b/src/ui/AccessibleLinksFormatter.cpp new file mode 100644 index 000000000..d4e52135f --- /dev/null +++ b/src/ui/AccessibleLinksFormatter.cpp @@ -0,0 +1,185 @@ +/*!******************************************************************** + + Audacity: A Digital Audio Editor + + @file AccessibleLinksFormatter.h + @brief Define a helper class to format text with link in a way, accessible to VI users. + + Dmitry Vedenko + **********************************************************************/ + +#include "AccessibleLinksFormatter.h" + +#include "ShuttleGui.h" + +#include +#include + +#include + + +namespace +{ +size_t OffsetPosition(size_t position, size_t length) +{ + if (position == wxString::npos) + return wxString::npos; + + return position + length; +} +} + +AccessibleLinksFormatter::AccessibleLinksFormatter(TranslatableString message) + : mMessage(std::move(message)) +{ +} + +AccessibleLinksFormatter& AccessibleLinksFormatter::FormatLink( + wxString placeholder, TranslatableString value, std::string targetURL) +{ + mFormatArguments.push_back({ + std::move(placeholder), + std::move(value), + {}, + std::move(targetURL) + }); + + return *this; +} + +AccessibleLinksFormatter& AccessibleLinksFormatter::FormatLink( + wxString placeholder, TranslatableString value, + LinkClickedHandler handler) +{ + mFormatArguments.push_back({ + std::move(placeholder), + std::move(value), + std::move(handler), + {} + }); + + return *this; +} + +void AccessibleLinksFormatter::Populate(ShuttleGui& S) const +{ + // Just add the text, if there are no links to format + if (mFormatArguments.empty()) + { + S.AddFixedText(mMessage); + return; + } + + wxString translated = mMessage.Translation(); + + std::vector processedArguments = + ProcessArguments(translated); + + if (processedArguments.empty()) + { + S.AddFixedText(mMessage); + return; + } + + const int borderSize = S.GetBorder(); + + S.Prop(0).StartInvisiblePanel(); + S.StartWrapLay(); + { + size_t currentPosition = 0; + + S.SetBorder(0); + + if (borderSize > 0) + S.AddSpace(borderSize); + + for (const ProcessedArgument& processedArgument : processedArguments) + { + const FormatArgument* argument = processedArgument.Argument; + + // Add everything between currentPosition and PlaceholderPosition + + if (currentPosition != processedArgument.PlaceholderPosition) + { + const size_t substrLength = + processedArgument.PlaceholderPosition - currentPosition; + + S.Prop(0).AddFixedText( + Verbatim(translated.substr(currentPosition, substrLength))); + } + + // Add hyperlink + + wxHyperlinkCtrl* hyperlink = safenew wxHyperlinkCtrl( + S.GetParent(), wxID_ANY, argument->Value.Translation(), + argument->TargetURL); + + if (argument->Handler) + { + hyperlink->Bind( + wxEVT_HYPERLINK, [handler = argument->Handler](wxHyperlinkEvent& evt) { + handler(); + }); + } + + S.AddWindow(hyperlink, wxALIGN_TOP | wxALIGN_LEFT); + + // Update the currentPostion to the first symbol after the Placeholder + + currentPosition = OffsetPosition( + processedArgument.PlaceholderPosition, + argument->Placeholder.Length()); + + if (currentPosition >= translated.Length()) + break; + } + + if (currentPosition < translated.Length()) + S.AddFixedText(Verbatim(translated.substr(currentPosition))); + } + S.EndWrapLay(); + S.EndInvisiblePanel(); + + S.SetBorder(borderSize); +} + +std::vector +AccessibleLinksFormatter::ProcessArguments(wxString translatedMessage) const +{ + std::vector result; + result.reserve(mFormatArguments.size()); + // Arguments with the same placeholder are processed left-to-right. + // Lets track the last known position of the placeholder + std::unordered_map knownPlaceholderPosition; + + for (const FormatArgument& argument : mFormatArguments) + { + auto it = knownPlaceholderPosition.find(argument.Placeholder); + + const size_t startingPosition = + it != knownPlaceholderPosition.end() ? + OffsetPosition(it->second, argument.Placeholder.length()) : + 0; + + const size_t placeholderPosition = + startingPosition == wxString::npos ? + wxString::npos : + translatedMessage.find(argument.Placeholder, startingPosition); + + knownPlaceholderPosition[argument.Placeholder] = placeholderPosition; + + if (placeholderPosition != wxString::npos) + { + result.emplace_back( + ProcessedArgument { &argument, placeholderPosition }); + } + } + + std::sort( + result.begin(), result.end(), + [](const ProcessedArgument& lhs, const ProcessedArgument& rhs) { + return lhs.PlaceholderPosition < rhs.PlaceholderPosition; + }); + + return result; +} diff --git a/src/ui/AccessibleLinksFormatter.h b/src/ui/AccessibleLinksFormatter.h new file mode 100644 index 000000000..d96ee08ee --- /dev/null +++ b/src/ui/AccessibleLinksFormatter.h @@ -0,0 +1,79 @@ +/*!******************************************************************** + + Audacity: A Digital Audio Editor + + @file AccessibleLinksFormatter.h + @brief Define a helper class to format text with link in a way, accessible to VI users. + + Dmitry Vedenko + **********************************************************************/ + +#pragma once + +#include +#include + +#include "TranslatableString.h" + +class ShuttleGui; + +/*! @brief A class that allows translatable text to have accessible links in it in a way +* that is friendly to translators. +* +* This class allows to replace arbitrary placeholders (like %s, %url, {} or anyting of the choice) +* with links, that are accessible from the keyboard. +* +* In case there are multiple placeholders with the same name - they will be replaced in order +* they appear in the message. +*/ +class AccessibleLinksFormatter final +{ +public: + //! Handler to be called, when the Link is activated + using LinkClickedHandler = std::function; + + /*! @brief Create AccessibleLinksFormatter using a TranslatableString. + * + * TranslatableString may have the formatting options attached. + * TranslatableString copy will be stored, so formatting options that are appended + * after AccessibleLinksFormatter is created won't have any effect on the + * AccessibleLinksFormatter instance. + */ + explicit AccessibleLinksFormatter(TranslatableString message); + + //! Replace placeholder with a link, that will open URL in default browser. + AccessibleLinksFormatter& FormatLink( + wxString placeholder, TranslatableString value, std::string targetURL); + + //! Replace placeholder with a link, that will call a callback provided. + AccessibleLinksFormatter& FormatLink( + wxString placeholder, TranslatableString value, + LinkClickedHandler handler); + + //! Generate the UI. + void Populate(ShuttleGui& S) const; +private: + struct FormatArgument final + { + wxString Placeholder; + TranslatableString Value; + + LinkClickedHandler Handler; + std::string TargetURL; + }; + + struct ProcessedArgument final + { + const FormatArgument* Argument { nullptr }; + size_t PlaceholderPosition { wxString::npos }; + }; + + /* Find the positions of the placeholders and sort + * arguments according to the positions. + */ + std::vector + ProcessArguments(wxString translatedMessage) const; + + TranslatableString mMessage; + std::vector mFormatArguments; +};