mirror of
https://github.com/cookiengineer/audacity
synced 2025-06-15 15:49:36 +02:00
Adds a class that allows keyboard accessible links in the text.
ShuttleGUI::AddWindow has a new argument now: positionFlags. This argument does not change the default behavior. StartWrapLay/EndWrapLay are added, so wxWrapSizer can be used from the ShuttleGUI.
This commit is contained in:
parent
82802f7781
commit
73e9b4dcfe
@ -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
|
||||
|
@ -113,6 +113,8 @@ for registering for changes.
|
||||
#include <wx/spinctrl.h>
|
||||
#include <wx/stattext.h>
|
||||
#include <wx/bmpbuttn.h>
|
||||
#include <wx/wrapsizer.h>
|
||||
|
||||
#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<wxWrapSizer>(wxHORIZONTAL, 0);
|
||||
|
||||
UpdateSizersCore(false, PositionFlags | wxALL);
|
||||
}
|
||||
|
||||
void ShuttleGuiBase::EndWrapLay()
|
||||
{
|
||||
if (mShuttleMode != eIsCreating)
|
||||
return;
|
||||
|
||||
PopSizer();
|
||||
}
|
||||
|
||||
void ShuttleGuiBase::StartMultiColumn(int nCols, int PositionFlags)
|
||||
{
|
||||
if( mShuttleMode != eIsCreating )
|
||||
|
@ -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 );
|
||||
|
185
src/ui/AccessibleLinksFormatter.cpp
Normal file
185
src/ui/AccessibleLinksFormatter.cpp
Normal file
@ -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 <unordered_map>
|
||||
#include <algorithm>
|
||||
|
||||
#include <wx/hyperlink.h>
|
||||
|
||||
|
||||
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<ProcessedArgument> 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::ProcessedArgument>
|
||||
AccessibleLinksFormatter::ProcessArguments(wxString translatedMessage) const
|
||||
{
|
||||
std::vector<ProcessedArgument> 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<wxString, size_t> 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;
|
||||
}
|
79
src/ui/AccessibleLinksFormatter.h
Normal file
79
src/ui/AccessibleLinksFormatter.h
Normal file
@ -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 <functional>
|
||||
#include <vector>
|
||||
|
||||
#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<void()>;
|
||||
|
||||
/*! @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<ProcessedArgument>
|
||||
ProcessArguments(wxString translatedMessage) const;
|
||||
|
||||
TranslatableString mMessage;
|
||||
std::vector<FormatArgument> mFormatArguments;
|
||||
};
|
Loading…
x
Reference in New Issue
Block a user