Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 67 additions & 15 deletions se5/WordCensor/MainWindow.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,15 @@
x:Class="SubtitleEdit.Plugins.WordCensor.MainWindow"
mc:Ignorable="d"
Title="Word censor"
Width="720" Height="600"
MinWidth="500" MinHeight="380"
Width="780" Height="640"
MinWidth="560" MinHeight="420"
WindowStartupLocation="CenterScreen">
<DockPanel Margin="20">

<!-- Bottom: status + apply/cancel -->
<Grid DockPanel.Dock="Bottom"
ColumnDefinitions="*,Auto,Auto"
Margin="0,14,0,0">
Margin="0,16,0,0">
<TextBlock Grid.Column="0"
x:Name="SummaryLabel"
VerticalAlignment="Center"
Expand All @@ -32,26 +33,76 @@
IsDefault="True" />
</Grid>

<StackPanel Orientation="Horizontal" DockPanel.Dock="Top" Spacing="10" Margin="0,0,0,4">
<Button Content="Select all" Click="OnSelectAll" />
<Button Content="Select none" Click="OnSelectNone" />
<CheckBox x:Name="ColorRedCheck"
Content="Highlight censored words in red"
VerticalAlignment="Center"
Margin="12,0,0,0" />
</StackPanel>

<!-- Title -->
<TextBlock DockPanel.Dock="Top"
FontSize="18"
FontSize="20"
FontWeight="SemiBold"
Text="Word censor" />

<TextBlock DockPanel.Dock="Top"
x:Name="SubtitleLabel"
Margin="0,4,0,12"
Margin="0,4,0,0"
Opacity="0.7"
TextWrapping="Wrap" />

<!-- Mode picker card -->
<Border DockPanel.Dock="Top"
Margin="0,14,0,0"
Padding="16,14"
CornerRadius="6"
Background="#11808080"
BorderBrush="#22808080"
BorderThickness="1">
<StackPanel Spacing="10">
<TextBlock Text="How should offensive words be replaced?"
FontWeight="SemiBold" />

<StackPanel Orientation="Vertical" Spacing="6">
<RadioButton x:Name="GrawlixModeRadio"
GroupName="CensorMode"
Content="Grawlix — mask with #@!?$%&amp; characters"
IsChecked="True" />

<Grid ColumnDefinitions="Auto,8,*"
VerticalAlignment="Center">
<RadioButton Grid.Column="0"
x:Name="ReplacementModeRadio"
GroupName="CensorMode"
Content="Replace with text:" />
<TextBox Grid.Column="2"
x:Name="ReplacementBox"
Text="[censored]"
MinWidth="180"
HorizontalAlignment="Left"
Watermark="[censored]" />
</Grid>

<RadioButton x:Name="AlternativeModeRadio"
GroupName="CensorMode"
Content="Swap with a random alternative word from the list" />
</StackPanel>

<CheckBox x:Name="ColorRedCheck"
Content="Highlight censored words in red"
Margin="0,4,0,0" />
</StackPanel>
</Border>

<!-- Selection controls -->
<Grid DockPanel.Dock="Top"
ColumnDefinitions="Auto,Auto,*"
Margin="0,12,0,4">
<Button Grid.Column="0"
Content="Select all"
Click="OnSelectAll"
MinWidth="100" />
<Button Grid.Column="1"
Content="Select none"
Click="OnSelectNone"
MinWidth="100"
Margin="8,0,0,0" />
</Grid>

<TextBlock DockPanel.Dock="Top"
x:Name="NoChangesLabel"
FontSize="14"
Expand All @@ -64,7 +115,8 @@
<Border BorderBrush="#22808080"
BorderThickness="1"
CornerRadius="4"
Padding="0">
Padding="0"
Margin="0,4,0,0">
<ListBox x:Name="ChangesList"
Background="Transparent"
SelectionMode="Single">
Expand Down
107 changes: 90 additions & 17 deletions se5/WordCensor/MainWindow.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,15 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Globalization;
using System.Linq;
using System.Text.Json;

namespace SubtitleEdit.Plugins.WordCensor;

public partial class MainWindow : Window
{
private const string DefaultReplacement = "[censored]";

private readonly PluginRequest _request;
private readonly List<SrtBlock> _blocks;
private readonly WordCensorEngine _engine;
Expand All @@ -25,6 +26,10 @@ public partial class MainWindow : Window
private ListBox _changesList = null!;
private Button _applyButton = null!;
private CheckBox _colorRedCheck = null!;
private RadioButton _grawlixRadio = null!;
private RadioButton _replacementRadio = null!;
private RadioButton _alternativeRadio = null!;
private TextBox _replacementBox = null!;

public MainWindow() : this(new PluginRequest()) { }

Expand All @@ -36,16 +41,31 @@ public MainWindow(PluginRequest request)
_engine = new WordCensorEngine();
_blocks = SubRipParser.Parse(request.Subtitle.SubRip);

_colorRedCheck.IsChecked = LoadColorRedSetting(request.Settings);
_colorRedCheck.IsCheckedChanged += (_, _) => RebuildProposals();
var (mode, replacement, colorRed) = LoadSettings(request.Settings);
SetMode(mode);
_replacementBox.Text = replacement;
_colorRedCheck.IsChecked = colorRed;

_grawlixRadio.IsCheckedChanged += (_, _) => OnModeOrOptionChanged();
_replacementRadio.IsCheckedChanged += (_, _) => OnModeOrOptionChanged();
_alternativeRadio.IsCheckedChanged += (_, _) => OnModeOrOptionChanged();
_colorRedCheck.IsCheckedChanged += (_, _) => OnModeOrOptionChanged();
_replacementBox.TextChanged += (_, _) =>
{
// Only re-build when the textbox actually affects the preview.
if (CurrentMode() == CensorMode.Replacement)
{
OnModeOrOptionChanged();
}
};

BuildProposals();
_changesList.ItemsSource = _proposals;

var scope = request.SelectedIndices.Count > 0
? $"the {request.SelectedIndices.Count} selected line(s)"
: "all lines";
_subtitleLabel.Text = $"Replace offensive words with grawlix characters (#@!$%) in {scope}.";
_subtitleLabel.Text = $"Replace offensive words in {scope}. Pick a style below.";

UpdateUiForProposals();
}
Expand All @@ -59,6 +79,10 @@ private void InitializeComponent()
_changesList = this.FindControl<ListBox>("ChangesList")!;
_applyButton = this.FindControl<Button>("ApplyButton")!;
_colorRedCheck = this.FindControl<CheckBox>("ColorRedCheck")!;
_grawlixRadio = this.FindControl<RadioButton>("GrawlixModeRadio")!;
_replacementRadio = this.FindControl<RadioButton>("ReplacementModeRadio")!;
_alternativeRadio = this.FindControl<RadioButton>("AlternativeModeRadio")!;
_replacementBox = this.FindControl<TextBox>("ReplacementBox")!;
}

protected override void OnOpened(EventArgs e)
Expand All @@ -67,9 +91,30 @@ protected override void OnOpened(EventArgs e)
this.BringToForeground();
}

private CensorOptions BuildOptions() => new()
{
Mode = CurrentMode(),
Replacement = string.IsNullOrEmpty(_replacementBox.Text) ? DefaultReplacement : _replacementBox.Text,
ColorRed = _colorRedCheck.IsChecked == true,
};

private CensorMode CurrentMode()
{
if (_replacementRadio.IsChecked == true) return CensorMode.Replacement;
if (_alternativeRadio.IsChecked == true) return CensorMode.Alternative;
return CensorMode.Grawlix;
}

private void SetMode(CensorMode mode)
{
_grawlixRadio.IsChecked = mode == CensorMode.Grawlix;
_replacementRadio.IsChecked = mode == CensorMode.Replacement;
_alternativeRadio.IsChecked = mode == CensorMode.Alternative;
}

private void BuildProposals()
{
var colorRed = _colorRedCheck.IsChecked == true;
var options = BuildOptions();
var selected = new HashSet<int>(_request.SelectedIndices);
var applyToAll = selected.Count == 0;

Expand All @@ -86,7 +131,7 @@ private void BuildProposals()
continue;
}

if (_engine.TryCensor(_blocks[i].Text, colorRed, out var censored))
if (_engine.TryCensor(_blocks[i].Text, options, out var censored))
{
var proposal = new ChangeProposal(i, _blocks[i].Text, censored);
proposal.PropertyChanged += OnProposalChanged;
Expand All @@ -95,10 +140,10 @@ private void BuildProposals()
}
}

private void RebuildProposals()
private void OnModeOrOptionChanged()
{
// Re-censor existing proposals so the colour-toggle is reflected in the preview.
// The grawlix characters re-randomise, which is fine - the user is comparing approaches.
// Re-censor existing proposals so the new mode/colour/text is reflected in the preview.
// Grawlix re-randomises and Alternative re-rolls, which is fine the user is comparing approaches.
var previousInclude = _proposals.ToDictionary(p => p.LineIndex, p => p.Include);
BuildProposals();
foreach (var p in _proposals)
Expand Down Expand Up @@ -199,7 +244,7 @@ private void OnApply(object? sender, RoutedEventArgs e)
Format = "SubRip",
Native = SubRipParser.Serialize(_blocks),
},
Settings = BuildSettings(_colorRedCheck.IsChecked == true),
Settings = BuildSettings(BuildOptions()),
};
}
catch (Exception ex)
Expand All @@ -210,22 +255,50 @@ private void OnApply(object? sender, RoutedEventArgs e)
Close();
}

private static bool LoadColorRedSetting(JsonElement? settings)
private static (CensorMode mode, string replacement, bool colorRed) LoadSettings(JsonElement? settings)
{
var mode = CensorMode.Grawlix;
var replacement = DefaultReplacement;
var colorRed = false;

if (settings is null || settings.Value.ValueKind != JsonValueKind.Object)
{
return false;
return (mode, replacement, colorRed);
}

var root = settings.Value;

if (root.TryGetProperty("mode", out var modeProp) && modeProp.ValueKind == JsonValueKind.String &&
Enum.TryParse<CensorMode>(modeProp.GetString(), ignoreCase: true, out var parsed))
{
mode = parsed;
}
if (settings.Value.TryGetProperty("colorRed", out var value) && value.ValueKind == JsonValueKind.True)
if (root.TryGetProperty("replacement", out var rep) && rep.ValueKind == JsonValueKind.String)
{
return true;
var s = rep.GetString();
if (!string.IsNullOrEmpty(s))
{
replacement = s;
}
}
if (root.TryGetProperty("colorRed", out var cr) && cr.ValueKind == JsonValueKind.True)
{
colorRed = true;
}
return false;

return (mode, replacement, colorRed);
}

private static JsonElement BuildSettings(bool colorRed)
private static JsonElement BuildSettings(CensorOptions options)
{
using var doc = JsonDocument.Parse($"{{\"colorRed\":{colorRed.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()}}}");
var dto = new
{
mode = options.Mode.ToString(),
replacement = options.Replacement,
colorRed = options.ColorRed,
};
var json = JsonSerializer.Serialize(dto);
using var doc = JsonDocument.Parse(json);
return doc.RootElement.Clone();
}
}
Loading
Loading