Use new Inline controls to selectively style TextBlock text

This commit is contained in:
Michael Bucari-Tovo 2023-01-07 12:05:38 -07:00
parent fe804796ab
commit acb6d1b335
2 changed files with 90 additions and 119 deletions

View File

@ -8,11 +8,6 @@
Icon="/Assets/libation.ico" Icon="/Assets/libation.ico"
Title="EditTemplateDialog"> Title="EditTemplateDialog">
<Window.Resources>
<dialogs:BracketEscapeConverter x:Key="BracketEscapeConverter" />
</Window.Resources>
<Grid RowDefinitions="Auto,*,Auto"> <Grid RowDefinitions="Auto,*,Auto">
<Grid <Grid
Grid.Row="0" Grid.Row="0"
@ -27,37 +22,33 @@
<TextBox <TextBox
Grid.Column="0" Grid.Column="0"
Grid.Row="1" Grid.Row="1"
Text="{Binding workingTemplateText, Mode=TwoWay}" /> Text="{Binding UserTemplateText, Mode=TwoWay}" />
<Button <Button
Grid.Column="1" Grid.Column="1"
Grid.Row="1" Grid.Row="1"
Margin="10,0,0,0" Margin="10,0,0,0"
VerticalAlignment="Stretch" VerticalAlignment="Stretch"
Padding="20,3,20,3" VerticalContentAlignment="Center"
Content="Reset to Default" Content="Reset to Default"
Click="ResetButton_Click" /> Click="ResetButton_Click" />
</Grid> </Grid>
<Grid Grid.Row="1" ColumnDefinitions="Auto,*"> <Grid Grid.Row="1" ColumnDefinitions="Auto,*">
<Border <DataGrid
Grid.Row="0" Grid.Row="0"
Grid.Column="0" Grid.Column="0"
Margin="5" BorderBrush="{DynamicResource DataGridGridLinesBrush}"
BorderThickness="1" BorderThickness="1"
BorderBrush="{DynamicResource DataGridGridLinesBrush}">
<DataGrid
GridLinesVisibility="All" GridLinesVisibility="All"
AutoGenerateColumns="False" AutoGenerateColumns="False"
Items="{Binding ListItems}" > Items="{Binding ListItems}" >
<DataGrid.Columns> <DataGrid.Columns>
<DataGridTemplateColumn Width="Auto" Header="Tag"> <DataGridTemplateColumn Width="Auto" Header="Tag">
<DataGridTemplateColumn.CellTemplate> <DataGridTemplateColumn.CellTemplate>
<DataTemplate> <DataTemplate>
<TextPresenter Height="18" Margin="10,0,10,0" VerticalAlignment="Center" Text="{Binding TagName, Converter={StaticResource BracketEscapeConverter}}" /> <TextPresenter Height="18" Margin="10,0,10,0" VerticalAlignment="Center" Text="{Binding Item1}" />
</DataTemplate> </DataTemplate>
</DataGridTemplateColumn.CellTemplate> </DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn> </DataGridTemplateColumn>
@ -68,7 +59,7 @@
<TextPresenter <TextPresenter
Height="18" Height="18"
Margin="10,0,10,0" Margin="10,0,10,0"
VerticalAlignment="Center" Text="{Binding Description}" /> VerticalAlignment="Center" Text="{Binding Item2}" />
</DataTemplate> </DataTemplate>
</DataGridTemplateColumn.CellTemplate> </DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn> </DataGridTemplateColumn>
@ -76,13 +67,12 @@
</DataGrid.Columns> </DataGrid.Columns>
</DataGrid> </DataGrid>
</Border>
<Grid <Grid
Grid.Column="1" Grid.Column="1"
Margin="5" Margin="5,0,5,0"
RowDefinitions="Auto,*,80" HorizontalAlignment="Stretch"> RowDefinitions="Auto,*,Auto"
HorizontalAlignment="Stretch">
<TextBlock <TextBlock
Margin="5,5,5,10" Margin="5,5,5,10"
@ -94,10 +84,9 @@
BorderThickness="1" BorderThickness="1"
BorderBrush="{DynamicResource DataGridGridLinesBrush}"> BorderBrush="{DynamicResource DataGridGridLinesBrush}">
<WrapPanel <TextBlock
Grid.Row="1" TextWrapping="WrapWithOverflow"
Name="wrapPanel" Inlines="{Binding Inlines}" />
Orientation="Horizontal" />
</Border> </Border>
@ -105,10 +94,9 @@
Grid.Row="2" Grid.Row="2"
Margin="5" Margin="5"
Foreground="Firebrick" Foreground="Firebrick"
Text="{Binding WarningText}" /> Text="{Binding WarningText}"
IsVisible="{Binding WarningText, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
</Grid> </Grid>
</Grid> </Grid>
<Button <Button
Grid.Row="2" Grid.Row="2"

View File

@ -1,10 +1,7 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Data; using Avalonia.Data;
using Avalonia.Data.Converters; using Avalonia.Data.Converters;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Media.TextFormatting;
using Dinah.Core; using Dinah.Core;
using LibationFileManager; using LibationFileManager;
using System; using System;
@ -14,6 +11,9 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using ReactiveUI; using ReactiveUI;
using Avalonia.Controls.Documents;
using Avalonia.Collections;
using Avalonia.Controls;
namespace LibationAvalonia.Dialogs namespace LibationAvalonia.Dialogs
{ {
@ -22,14 +22,14 @@ namespace LibationAvalonia.Dialogs
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{ {
if (value is string str && str[0] != '<' && str[^1] != '>') if (value is string str && str[0] != '<' && str[^1] != '>')
return $"<{str}>"; return $"<{str}>".Replace("->", "-\x200C>").Replace("<-", "<\x200C-");
return new BindingNotification(new InvalidCastException(), BindingErrorType.Error); return new BindingNotification(new InvalidCastException(), BindingErrorType.Error);
} }
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{ {
if (value is string str && str[0] == '<' && str[^1] == '>') if (value is string str && str[0] == '<' && str[^1] == '>')
return str[1..^2]; return str[1..^2].Replace("-\x200C>", "->").Replace("<\x200C-", "<-");
return new BindingNotification(new InvalidCastException(), BindingErrorType.Error); return new BindingNotification(new InvalidCastException(), BindingErrorType.Error);
} }
} }
@ -42,26 +42,27 @@ namespace LibationAvalonia.Dialogs
public EditTemplateDialog() public EditTemplateDialog()
{ {
InitializeComponent(); AvaloniaXamlLoader.Load(this);
_viewModel = new(Configuration.Instance, this.Find<WrapPanel>(nameof(wrapPanel))); if (Design.IsDesignMode)
{
AudibleUtilities.AudibleApiStorage.EnsureAccountsSettingsFileExists();
_viewModel = new(Configuration.Instance, Templates.File);
_viewModel.resetTextBox(_viewModel.Template.DefaultTemplate);
Title = $"Edit {_viewModel.Template.Name}";
DataContext = _viewModel;
}
} }
public EditTemplateDialog(Templates template, string inputTemplateText) : this() public EditTemplateDialog(Templates template, string inputTemplateText) : this()
{ {
_viewModel.template = ArgumentValidator.EnsureNotNull(template, nameof(template)); ArgumentValidator.EnsureNotNull(template, nameof(template));
Title = $"Edit {_viewModel.template.Name}";
_viewModel.Description = _viewModel.template.Description; _viewModel = new EditTemplateViewModel(Configuration.Instance, template);
_viewModel.resetTextBox(inputTemplateText); _viewModel.resetTextBox(inputTemplateText);
Title = $"Edit {template.Name}";
_viewModel.ListItems = _viewModel.template.GetTemplateTags();
DataContext = _viewModel; DataContext = _viewModel;
} }
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
protected override async Task SaveAndCloseAsync() protected override async Task SaveAndCloseAsync()
{ {
if (!await _viewModel.Validate()) if (!await _viewModel.Validate())
@ -75,51 +76,57 @@ namespace LibationAvalonia.Dialogs
=> await SaveAndCloseAsync(); => await SaveAndCloseAsync();
public void ResetButton_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e) public void ResetButton_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
=> _viewModel.resetTextBox(_viewModel.template.DefaultTemplate); => _viewModel.resetTextBox(_viewModel.Template.DefaultTemplate);
private class EditTemplateViewModel : ViewModels.ViewModelBase private class EditTemplateViewModel : ViewModels.ViewModelBase
{ {
WrapPanel WrapPanel; private readonly Configuration config;
public Configuration config { get; } public InlineCollection Inlines { get; } = new();
public EditTemplateViewModel(Configuration configuration, WrapPanel panel) public Templates Template { get; }
public EditTemplateViewModel(Configuration configuration, Templates templates)
{ {
config = configuration; config = configuration;
WrapPanel = panel; Template = templates;
Description = templates.Description;
ListItems
= new AvaloniaList<Tuple<string, string>>(
Template
.GetTemplateTags()
.Select(
t => new Tuple<string, string>(
$"<{t.TagName.Replace("->", "-\x200C>").Replace("<-", "<\x200C-")}>",
t.Description)
)
);
} }
// hold the work-in-progress value. not guaranteed to be valid // hold the work-in-progress value. not guaranteed to be valid
private string _workingTemplateText; private string _userTemplateText;
public string workingTemplateText public string UserTemplateText
{ {
get => _workingTemplateText; get => _userTemplateText;
set set
{ {
_workingTemplateText = template.Sanitize(value); _userTemplateText = value;
templateTb_TextChanged(); templateTb_TextChanged();
} }
} }
public string workingTemplateText => Template.Sanitize(UserTemplateText);
private string _warningText; private string _warningText;
public string WarningText public string WarningText { get => _warningText; set => this.RaiseAndSetIfChanged(ref _warningText, value); }
{
get => _warningText;
set
{
this.RaiseAndSetIfChanged(ref _warningText, value);
}
}
public Templates template { get; set; } public string Description { get; }
public string Description { get; set; }
public IEnumerable<TemplateTags> ListItems { get; set; } public AvaloniaList<Tuple<string, string>> ListItems { get; set; }
public void resetTextBox(string value) => workingTemplateText = value; public void resetTextBox(string value) => UserTemplateText = value;
public async Task<bool> Validate() public async Task<bool> Validate()
{ {
if (template.IsValid(workingTemplateText)) if (Template.IsValid(workingTemplateText))
return true; return true;
var errors = template var errors = Template
.GetErrors(workingTemplateText) .GetErrors(workingTemplateText)
.Select(err => $"- {err}") .Select(err => $"- {err}")
.Aggregate((a, b) => $"{a}\r\n{b}"); .Aggregate((a, b) => $"{a}\r\n{b}");
@ -129,8 +136,8 @@ namespace LibationAvalonia.Dialogs
private void templateTb_TextChanged() private void templateTb_TextChanged()
{ {
var isChapterTitle = template == Templates.ChapterTitle; var isChapterTitle = Template == Templates.ChapterTitle;
var isFolder = template == Templates.Folder; var isFolder = Template == Templates.Folder;
var libraryBookDto = new LibraryBookDto var libraryBookDto = new LibraryBookDto
{ {
@ -142,7 +149,10 @@ namespace LibationAvalonia.Dialogs
Authors = new List<string> { "Arthur Conan Doyle", "Stephen Fry - introductions" }, Authors = new List<string> { "Arthur Conan Doyle", "Stephen Fry - introductions" },
Narrators = new List<string> { "Stephen Fry" }, Narrators = new List<string> { "Stephen Fry" },
SeriesName = "Sherlock Holmes", SeriesName = "Sherlock Holmes",
SeriesNumber = "1" SeriesNumber = "1",
BitRate = 128,
SampleRate = 44100,
Channels = 2
}; };
var chapterName = "A Flight for Life"; var chapterName = "A Flight for Life";
var chapterNumber = 4; var chapterNumber = 4;
@ -162,15 +172,15 @@ namespace LibationAvalonia.Dialogs
isFolder ? workingTemplateText : config.FolderTemplate); isFolder ? workingTemplateText : config.FolderTemplate);
var file var file
= template == Templates.ChapterFile = Template == Templates.ChapterFile
? Templates.ChapterFile.GetPortionFilename( ? Templates.ChapterFile.GetPortionFilename(
libraryBookDto, libraryBookDto,
workingTemplateText, workingTemplateText,
partFileProperties, partFileProperties,
"") "")
: Templates.File.GetPortionFilename( : Templates.File.GetPortionFilename(
libraryBookDto, libraryBookDto,
isFolder ? config.FileTemplate : workingTemplateText); isFolder ? config.FileTemplate : workingTemplateText);
var ext = config.DecryptToLossy ? "mp3" : "m4b"; var ext = config.DecryptToLossy ? "mp3" : "m4b";
var chapterTitle = Templates.ChapterTitle.GetPortionTitle(libraryBookDto, workingTemplateText, partFileProperties); var chapterTitle = Templates.ChapterTitle.GetPortionTitle(libraryBookDto, workingTemplateText, partFileProperties);
@ -186,63 +196,36 @@ namespace LibationAvalonia.Dialogs
string slashWrap(string val) => val.Replace(sing, $"{ZERO_WIDTH_SPACE}{sing}"); string slashWrap(string val) => val.Replace(sing, $"{ZERO_WIDTH_SPACE}{sing}");
WarningText WarningText
= !template.HasWarnings(workingTemplateText) = !Template.HasWarnings(workingTemplateText)
? "" ? ""
: "Warning:\r\n" + : "Warning:\r\n" +
template Template
.GetWarnings(workingTemplateText) .GetWarnings(workingTemplateText)
.Select(err => $"- {err}") .Select(err => $"- {err}")
.Aggregate((a, b) => $"{a}\r\n{b}"); .Aggregate((a, b) => $"{a}\r\n{b}");
var list = new List<TextCharacters>(); var bold = FontWeight.Bold;
var reg = FontWeight.Normal;
var bold = new Typeface(Typeface.Default.FontFamily, FontStyle.Normal, FontWeight.Bold); Inlines.Clear();
var normal = new Typeface(Typeface.Default.FontFamily, FontStyle.Normal, FontWeight.Normal);
var stringList = new List<(string, FontWeight)>();
if (isChapterTitle) if (isChapterTitle)
{ {
stringList.Add((chapterTitle, FontWeight.Bold)); Inlines.Add(new Run(chapterTitle) { FontWeight = bold });
} return;
else
{
stringList.Add((slashWrap(books), FontWeight.Normal));
stringList.Add((sing, FontWeight.Normal));
stringList.Add((slashWrap(folder), isFolder ? FontWeight.Bold : FontWeight.Normal));
stringList.Add((sing, FontWeight.Normal));
stringList.Add((file, !isFolder ? FontWeight.Bold : FontWeight.Normal));
stringList.Add(($".{ext}", FontWeight.Normal));
} }
WrapPanel.Children.Clear(); Inlines.Add(new Run(slashWrap(books)) { FontWeight = reg });
Inlines.Add(new Run(sing) { FontWeight = reg });
//Avalonia doesn't yet support anything like rich text, so add a new textblock for every word/style Inlines.Add(new Run(slashWrap(folder)) { FontWeight = isFolder ? bold : reg });
foreach (var item in stringList)
{
var wordsSplit = item.Item1.Split(' ');
for(int i = 0; i < wordsSplit.Length; i++) Inlines.Add(new Run(sing));
{
var tb = new TextBlock
{
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Bottom,
TextWrapping = TextWrapping.Wrap,
Text = wordsSplit[i] + (i == wordsSplit.Length - 1 ? "" : " "),
FontWeight = item.Item2
};
WrapPanel.Children.Add(tb); Inlines.Add(new Run(slashWrap(file)) { FontWeight = isFolder ? reg : bold });
}
}
Inlines.Add(new Run($".{ext}"));
} }
} }
} }
} }