Files
UnrealEngineUWP/Engine/Source/Runtime/TimeManagement/Public/FrameNumberNumericInterface.h
matt johnson b726c51b72 TimeManagement: enable customization of the subframe indicator when stringifying time values
By default, FFrameNumberInterface uses an asterisk suffix to indicate that a stringified time
value represents a subframe time between whole number frames. This change allows
customizing that suffix for clients that can provide more useful information about the subframe
value. This will be used by skeletal animation tracks that have subframe timecode metadata.

#jira UE-141224
#rb max.chen
#preflight 620ff5cfcc18880e8bb998e4

[CL 19060365 by matt johnson in ue5-main branch]
2022-02-18 19:41:32 -05:00

234 lines
11 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "FrameNumberDisplayFormat.h"
#include "Widgets/Input/NumericTypeInterface.h"
#include "Misc/FrameRate.h"
#include "FrameNumberTimeEvaluator.h"
#include "Misc/Timecode.h"
#include "Misc/ExpressionParserTypes.h"
#include "Math/BasicMathExpressionEvaluator.h"
#include "Internationalization/FastDecimalFormat.h"
/**
* A large portion of the Sequencer UI is built around using SpinBox and NumericBox because the framerate
* used to be stored in (float) seconds. This creates a convenient UI as it allows the users to type in
* numbers (for frames or seconds), or to drag the mouse to change the time. When Sequencer was converted
* to using integer based frames and frame rates (expressed as numerator/denominator) the spinbox-based
* UI became an issue as SpinBox and NumericBox internally convert to a double to perform various calculations.
* This is an issue if your Spinbox type (ie: FQualifiedFrameTime) is not implicitly convertible to double,
* which the new Sequencer types are not (due to accidental type coercion when using outside the UI).
*
* To solve this, the Spinboxes will still use double as their type, but double now means frame number and not
* time. A double can store the entire range of int32 (which backs FFrameNumber) without precision loss, and
* we can execute callbacks to find out what framerate the sequence is running at. This allows us to display
* in Timecode, Time or Frames and convert back and forth to double for the UI, and from double into the backing
* FFrameNumber for the sequence.
*/
struct FFrameNumberInterface : public INumericTypeInterface<double>
{
FFrameNumberInterface(const TAttribute<EFrameNumberDisplayFormats>& InDisplayFormatAttr, const TAttribute<uint8>& InOnGetZeroPadFrameNumber, const TAttribute<FFrameRate>& InTickResolutionAttr, const TAttribute<FFrameRate>& InDisplayRateAttr)
: DisplayFormatAttr(InDisplayFormatAttr)
, TickResolutionAttr(InTickResolutionAttr)
, DisplayRateAttr(InDisplayRateAttr)
, ZeroPadFramesAttr(InOnGetZeroPadFrameNumber)
, SubframeIndicator(TEXT("*"))
{
check(InDisplayFormatAttr.IsSet());
check(InTickResolutionAttr.IsSet());
check(InDisplayRateAttr.IsSet());
}
/** Gets the minimum and maximum fractional digits. */
virtual int32 GetMinFractionalDigits() const override
{
return 0;
}
virtual int32 GetMaxFractionalDigits() const override
{
return 0;
}
/** Sets the minimum and maximum fractional digits - A minimum greater than 0 will always have that many trailing zeros */
virtual void SetMinFractionalDigits(const TAttribute<TOptional<int32>>& NewValue) override {}
virtual void SetMaxFractionalDigits(const TAttribute<TOptional<int32>>& NewValue) override {}
/** Check whether the typed character is valid */
virtual bool IsCharacterValid(TCHAR InChar) const override
{
auto IsValidLocalizedCharacter = [InChar]() -> bool
{
const FDecimalNumberFormattingRules& NumberFormattingRules = ExpressionParser::GetLocalizedNumberFormattingRules();
return InChar == NumberFormattingRules.GroupingSeparatorCharacter
|| InChar == NumberFormattingRules.DecimalSeparatorCharacter
|| Algo::Find(NumberFormattingRules.DigitCharacters, InChar) != 0;
};
static const FString ValidChars = TEXT("1234567890()-+=\\/.,*^%%hrmsf[]:; ");
return InChar != 0 && (ValidChars.GetCharArray().Contains(InChar) || IsValidLocalizedCharacter());
}
virtual FString ToString(const double& Value) const override
{
FFrameRate SourceFrameRate = TickResolutionAttr.Get();
FFrameRate DestinationFrameRate = DisplayRateAttr.Get();
EFrameNumberDisplayFormats Format = DisplayFormatAttr.Get();
// If they want Drop Frame Timecode format but we're in an unsupported frame rate, we'll override it and say they want non drop frame.
bool bIsValidRateForDropFrame = FTimecode::IsDropFormatTimecodeSupported(DestinationFrameRate);
if (Format == EFrameNumberDisplayFormats::DropFrameTimecode && !bIsValidRateForDropFrame)
{
Format = EFrameNumberDisplayFormats::NonDropFrameTimecode;
}
switch (Format)
{
case EFrameNumberDisplayFormats::Frames:
{
// Convert from sequence resolution into display rate frames.
FFrameTime DisplayTime = FFrameRate::TransformTime(FFrameTime::FromDecimal(Value), SourceFrameRate, DestinationFrameRate);
FString SubframeSuffix = FMath::IsNearlyZero(DisplayTime.GetSubFrame()) ? TEXT("") : SubframeIndicator;
return FString::Printf(TEXT("%0*d%s"), ZeroPadFramesAttr.Get(), DisplayTime.GetFrame().Value, *SubframeSuffix);
}
case EFrameNumberDisplayFormats::Seconds:
{
double TimeInSeconds = SourceFrameRate.AsSeconds(FFrameTime::FromDecimal(Value));
static const FNumberFormattingOptions NumberFormattingOptions = FNumberFormattingOptions()
.SetUseGrouping(false)
.SetMinimumFractionalDigits(2)
.SetMaximumFractionalDigits(ZeroPadFramesAttr.Get());
return FastDecimalFormat::NumberToString(TimeInSeconds, ExpressionParser::GetLocalizedNumberFormattingRules(), NumberFormattingOptions);
}
case EFrameNumberDisplayFormats::NonDropFrameTimecode:
case EFrameNumberDisplayFormats::DropFrameTimecode:
{
FFrameTime InternalFrameNumber = FFrameTime::FromDecimal(Value);
FFrameTime DisplayTime = FFrameRate::TransformTime(InternalFrameNumber, SourceFrameRate, DestinationFrameRate);
FString SubframeSuffix = FMath::IsNearlyZero(DisplayTime.GetSubFrame()) ? TEXT("") : SubframeIndicator;
bool bIsDropTimecode = Format == EFrameNumberDisplayFormats::DropFrameTimecode;
FTimecode AsNonDropTimecode = FTimecode::FromFrameNumber(DisplayTime.FloorToFrame(), DestinationFrameRate, bIsDropTimecode);
return FString::Printf(TEXT("[%s%s]"), *AsNonDropTimecode.ToString(false), *SubframeSuffix);
}
default:
return FString(TEXT("Unsupported Format"));
}
}
virtual TOptional<double> FromString(const FString& InString, const double& InExistingValue) override
{
FFrameRate SourceFrameRate = DisplayRateAttr.Get();
FFrameRate DestinationFrameRate = TickResolutionAttr.Get();
EFrameNumberDisplayFormats FallbackFormat = DisplayFormatAttr.Get();
// We allow input in any format (time, frames or timecode) and we just convert it into into the internal sequence resolution.
// The user's input can be ambiguous though (does "5" mean 5 frames or 5 seconds?) so when we check each possible result we
// also check to see if they explicitly specified that format, or if the evaluator just happens to be able to parse that.
// All of these will convert into the frame resolution from the user's input before returning.
FFrameNumberTimeEvaluator Eval;
bool bWasTimecodeText;
TValueOrError<FFrameTime, FExpressionError> TimecodeResult = Eval.EvaluateTimecode(*InString, SourceFrameRate, DestinationFrameRate, /*Out*/bWasTimecodeText);
bool bWasFrameText;
TValueOrError<FFrameTime, FExpressionError> FrameResult = Eval.EvaluateFrame(*InString, SourceFrameRate, DestinationFrameRate, /*Out*/ bWasFrameText);
bool bWasTimeText;
TValueOrError<FFrameTime, FExpressionError> TimeResult = Eval.EvaluateTime(*InString, DestinationFrameRate, /*Out*/ bWasTimeText);
// All three formats support ambiguous conversion where the user can enter "5" and wants it in the logical unit based on the current
// display format. This means 5 -> 5f, 5 ->5s, and 5 -> 5 frames (in timecode). We also support specifically specifying in a different
// format than your current display, ie if your display is in frames and you enter 1s, you get 1s in frames or 30 (at 30fps play rate).
if (!bWasTimecodeText && !bWasFrameText && !bWasTimeText)
{
// They've entered an ambiguous number, so we'll check the display format and see if we did successfully parse as that type. If it
// was able to parse the input for the given display format, we return that.
if (TimecodeResult.IsValid() && (FallbackFormat == EFrameNumberDisplayFormats::DropFrameTimecode || FallbackFormat == EFrameNumberDisplayFormats::NonDropFrameTimecode))
{
return TOptional<double>(TimecodeResult.GetValue().GetFrame().Value);
}
else if (TimeResult.IsValid() && FallbackFormat == EFrameNumberDisplayFormats::Seconds)
{
return TOptional<double>(TimeResult.GetValue().GetFrame().Value);
}
else if (FrameResult.IsValid() && FallbackFormat == EFrameNumberDisplayFormats::Frames)
{
return TOptional<double>(FrameResult.GetValue().GetFrame().Value);
}
static FBasicMathExpressionEvaluator Parser;
// If not parsed, try the math expression evaluator
if (FallbackFormat == EFrameNumberDisplayFormats::Seconds)
{
double TimeInSeconds = DestinationFrameRate.AsSeconds(FFrameTime::FromDecimal(InExistingValue));
TValueOrError<double, FExpressionError> Result = Parser.Evaluate(*InString, TimeInSeconds);
if (Result.IsValid())
{
FFrameTime ResultTime = DestinationFrameRate.AsFrameTime(Result.GetValue());
return TOptional<double>(ResultTime.GetFrame().Value);
}
}
else if (FallbackFormat == EFrameNumberDisplayFormats::Frames)
{
FFrameTime ExistingTime = FFrameRate::TransformTime(FFrameTime::FromDecimal(InExistingValue), DestinationFrameRate, SourceFrameRate);
TValueOrError<double, FExpressionError> Result = Parser.Evaluate(*InString, (double)(ExistingTime.GetFrame().Value));
if (Result.IsValid())
{
FFrameTime ResultTime = FFrameRate::TransformTime(FFrameTime::FromDecimal(Result.GetValue()), SourceFrameRate, DestinationFrameRate);
return TOptional<double>(ResultTime.GetFrame().Value);
}
}
// Whatever they entered wasn't understood by any of our parsers, so it was probably malformed or had letters, etc.
return TOptional<double>();
}
// If we've gotten here then they did explicitly specify a timecode so we return that.
if (bWasTimecodeText)
{
return TOptional<double>(TimecodeResult.GetValue().GetFrame().Value);
}
else if (bWasTimeText)
{
return TOptional<double>(TimeResult.GetValue().GetFrame().Value);
}
else if (bWasFrameText)
{
return TOptional<double>(FrameResult.GetValue().GetFrame().Value);
}
// We're not sure what they typed in
return TOptional<double>();
}
/** Gets the string suffix used when a stringified value represents a time between whole frames. */
FString GetSubframeIndicator() const
{
return SubframeIndicator;
}
/** Sets the string suffix to use when a stringified value represents a time between whole frames. */
void SetSubframeIndicator(const FString& InSubframeIndicator)
{
SubframeIndicator = InSubframeIndicator;
}
private:
TAttribute<EFrameNumberDisplayFormats> DisplayFormatAttr;
TAttribute<FFrameRate> TickResolutionAttr;
TAttribute<FFrameRate> DisplayRateAttr;
TAttribute<uint8> ZeroPadFramesAttr;
FString SubframeIndicator;
};