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
6 changes: 4 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ After editing `.po` files, always run `compile-translations.ps1` to regenerate t
- `Convert` — converts an `ImageItem` to a target format with optional resize, writes to disk
- `GeneratePreview` — produces a `Bitmap` for the preview panel
- `GenerateOutputPath`, `GetConflictRenamePath` — output path logic with conflict resolution
- `GetSupportedFormats` — all SIC! format keys in canonical order; `GetEnabledFormats(enabledKeys)` filters them to the user-selected subset (issue #47), preserving order and never returning an empty list
- Note: The class name conflicts with `System.Drawing.ImageConverter`; files that use it must import `using ImageConverter = Oire.Sic.Services.ImageConverter;`

**`src/Sic/Services/UnsupportedImageException.cs`** — Domain exception thrown when content downloads/loads fine but isn't a decodable image (e.g. a link to an HTML page). `LoadFromUrl` translates Magick.NET's decode failure into this so the UI can show a friendly "not an image" message without referencing Magick.NET — keeps the image-library dependency inside the Services layer.
Expand All @@ -65,13 +66,13 @@ After editing `.po` files, always run `compile-translations.ps1` to regenerate t

**`src/Sic/SettingsDialog.cs` + `SettingsDialog.Designer.cs`** — Settings form. A `TabControl` (filling the form, OK/Cancel beneath) with two tabs, each its own flat `TableLayoutPanel`:
- **General** — language dropdown, confirm-exit checkbox, "check for updates on startup" checkbox, background update-frequency dropdown.
- **Images** — output folder (textbox + browse + reset) and a "detect data in clipboard" checkbox (issue #36). Has room for future image/conversion options (e.g. selective targets).
- **Images** — output folder (textbox + browse + reset), a "detect data in clipboard" checkbox (issue #36), and a target-formats checklist that selects which formats appear in the main window's dropdown (issue #47). Built as individual `CheckBox` controls stacked in a `TableLayoutPanel` (`formatsPanel`, filled at runtime in `PopulateFormats()`) inside a `GroupBox` (`formatsGroupBox`) — *not* a `CheckedListBox`, which doesn't reliably announce toggle state changes to screen readers. The group box caption (rather than a separate label) supplies the accessible group name; each checkbox is its own tab stop. OK requires at least one format ticked; when all are ticked it saves `EnabledFormats` as empty so formats added in future versions show automatically.

Tab page `Text` doesn't honor `&` mnemonics — navigate tabs with Ctrl+Tab / Ctrl+PgUp/Dn. Exposes `UpdatePeriodicCheckChanged` so `MainWindow` can re-arm the background update loop live (without a restart) when the frequency changes.

### Utilities (`src/Sic/Utils/`)

- `Config.cs` — Static config manager using SharpConfig. Reads/writes `%APPDATA%/Oire/Sic/Sic.cfg` with a `[General]` section (Language, OutputFolder, LastInputFolder, ConfirmExitWithQueue, CheckForUpdatesOnStartup, UpdateCheckInterval, DetectClipboardData). Accepts `isGui` parameter to route errors to MessageBox (GUI) or stderr (CLI).
- `Config.cs` — Static config manager using SharpConfig. Reads/writes `%APPDATA%/Oire/Sic/Sic.cfg` with a `[General]` section (Language, OutputFolder, LastInputFolder, ConfirmExitWithQueue, CheckForUpdatesOnStartup, UpdateCheckInterval, DetectClipboardData, EnabledFormats). `EnabledFormats` is a comma-separated list of SIC! format keys shown in the target dropdown (empty = all); parse it via `GetEnabledFormatKeys()` and filter with `ImageConverter.GetEnabledFormats(...)`. Accepts `isGui` parameter to route errors to MessageBox (GUI) or stderr (CLI).
- `FileHelper.cs` — Cloud placeholder detection (OneDrive/SharePoint recall attributes) and image file enumeration with glob patterns.
- `UrlHelper.cs` — Single source of truth for link validation: `IsValidHttpUrl(text, out url)` trims input and checks for an absolute http(s) URL. Used by the "Add by link" dialog, Ctrl+V paste, and clipboard auto-detection so all three validate links identically.
- `Localization.cs` — Wraps GetText.NET with convenience methods: `_()`, `_n()`, `_p()`, `_pn()` for translations. Loads `.mo` files from the `locale/` folder relative to the executable. Falls back through language parents to `en-US`.
Expand Down Expand Up @@ -139,6 +140,7 @@ SIC! is an accessible image format converter primarily aimed at blind and low-co
- Check for updates on startup (default: enabled) — a single silent check shortly after launch
- Background update-check frequency (`UpdateCheckInterval`: Daily / EveryThreeDays / Weekly / Monthly / Never; default Daily)
- Detect data in clipboard (`DetectClipboardData`, default: off) — when on, SIC! offers (via a Yes/No prompt) to add usable clipboard content (raw image, image files, or an image link) when the window opens or regains focus. Deduplicated by the Win32 clipboard sequence number so the same payload is offered at most once.
- Target formats to show (`EnabledFormats`, default: empty = all) — comma-separated list of format keys to display in the target-format dropdown, letting users hide formats they never convert to (issue #47). Empty means every supported format; at least one must stay selected.

## Auto-Updates (NetSparkleUpdater)

Expand Down
19 changes: 16 additions & 3 deletions src/Sic/MainWindow.cs
Original file line number Diff line number Diff line change
Expand Up @@ -151,13 +151,23 @@ private void SetupEventHandlers() {
}

private void PopulateFormatComboBox() {
foreach (var format in ImageConverter.GetSupportedFormats()) {
// Preserve the current pick across a refresh (e.g. after Settings narrows the list),
// falling back to the first format when the previous one is no longer shown.
var previous = formatComboBox.SelectedItem as string;

formatComboBox.BeginUpdate();
formatComboBox.Items.Clear();
foreach (var format in ImageConverter.GetEnabledFormats(Config.General.GetEnabledFormatKeys())) {
formatComboBox.Items.Add(format);
}
formatComboBox.EndUpdate();

if (formatComboBox.Items.Count > 0) {
formatComboBox.SelectedIndex = 0;
if (formatComboBox.Items.Count == 0) {
return;
}

var index = previous != null ? formatComboBox.Items.IndexOf(previous) : -1;
formatComboBox.SelectedIndex = index >= 0 ? index : 0;
}

private void UpdateMenuState() {
Expand Down Expand Up @@ -300,6 +310,9 @@ private void SettingsMenuItem_Click(object? sender, EventArgs e) {
if (Config.General.Language != previousLanguage) {
ApplyLocalization();
}

// The Images tab can change which target formats are offered — rebuild the dropdown.
PopulateFormatComboBox();
}

private void ApplyLocalization() {
Expand Down
19 changes: 19 additions & 0 deletions src/Sic/Services/ImageConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,25 @@ public static class ImageConverter {

public static IReadOnlyList<string> GetSupportedFormats() => FormatMap.Keys.ToList();

/// <summary>
/// The supported formats filtered down to those whose keys appear in <paramref name="enabledKeys"/>,
/// preserving the canonical display order. If the filter is <c>null</c>, empty, or matches no known
/// format, every supported format is returned — the dropdown is never left empty (issue #47).
/// </summary>
public static IReadOnlyList<string> GetEnabledFormats(IEnumerable<string>? enabledKeys) {
if (enabledKeys is null) {
return GetSupportedFormats();
}

var set = new HashSet<string>(enabledKeys, StringComparer.OrdinalIgnoreCase);
if (set.Count == 0) {
return GetSupportedFormats();
}

var filtered = FormatMap.Keys.Where(set.Contains).ToList();
return filtered.Count > 0 ? filtered : GetSupportedFormats();
}

/// <summary>
/// Maps an arbitrary format name (a SIC! format key like "JPG", or a Magick.NET
/// format name like "Jpeg"/"Bmp3") to one of the canonical SIC! format keys.
Expand Down
38 changes: 35 additions & 3 deletions src/Sic/SettingsDialog.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

48 changes: 48 additions & 0 deletions src/Sic/SettingsDialog.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@
using Serilog;
using static Oire.Sic.Utils.Localization;
using App = Oire.Sic.Utils.Constants.App;
using ImageConverter = Oire.Sic.Services.ImageConverter;

namespace Oire.Sic;

public partial class SettingsDialog: Form {
private readonly Dictionary<string, string> _languageMap = new();
private readonly List<CheckBox> _formatCheckBoxes = new();

/// <summary>Set when the background update-frequency preference changed; MainWindow starts,
/// stops, or re-times the background update loop to match without waiting for a restart. The
Expand Down Expand Up @@ -63,6 +65,7 @@ private void LoadSettings() {
checkUpdatesOnStartupCheckBox.Checked = Config.General.CheckForUpdatesOnStartup;
detectClipboardCheckBox.Checked = Config.General.DetectClipboardData;
SelectUpdateInterval(Config.General.UpdateCheckInterval);
PopulateFormats();

// "System" always first — uses the OS language
var systemDisplayName = _("System");
Expand Down Expand Up @@ -135,6 +138,33 @@ private void SelectUpdateInterval(UpdateCheckInterval interval) {
private UpdateCheckInterval SelectedUpdateInterval() =>
(updateIntervalComboBox.SelectedItem as UpdateIntervalOption)?.Interval ?? UpdateCheckInterval.Daily;

/// <summary>
/// Builds one real <see cref="CheckBox"/> per supported target format, ticking the ones currently
/// shown in the main window's dropdown (issue #47). An empty
/// <see cref="Config.SectionGeneral.EnabledFormats"/> means "all formats", so on first use every box
/// starts checked. Individual checkboxes are used instead of a <see cref="CheckedListBox"/> because
/// that control doesn't reliably announce toggle state changes to screen readers.
/// </summary>
private void PopulateFormats() {
var enabled = new HashSet<string>(Config.General.GetEnabledFormatKeys(), StringComparer.OrdinalIgnoreCase);
foreach (var format in ImageConverter.GetSupportedFormats()) {
var checkBox = new CheckBox {
Text = format,
Checked = enabled.Count == 0 || enabled.Contains(format),
AutoSize = true,
UseMnemonic = false, // format names like "JPG" carry no accelerator
Anchor = AnchorStyles.Left,
Margin = new Padding(3, 2, 3, 2),
};
_formatCheckBoxes.Add(checkBox);
formatsPanel.Controls.Add(checkBox);
}
}

/// <summary>The format keys the user has ticked, in their canonical display order.</summary>
private List<string> SelectedFormats() =>
_formatCheckBoxes.Where(cb => cb.Checked).Select(cb => cb.Text).ToList();

private void BrowseButton_Click(object? sender, EventArgs e) {
using var dialog = new FolderBrowserDialog {
Description = _("Select output folder for converted images"),
Expand Down Expand Up @@ -173,13 +203,31 @@ private void OkButton_Click(object? sender, EventArgs e) {
return;
}

var selectedFormats = SelectedFormats();
if (selectedFormats.Count == 0) {
Log.Warning("Settings: No target formats selected");
MessageBox.Show(
_("Please show at least one target format."),
_("No Formats Selected"),
MessageBoxButtons.OK, MessageBoxIcon.Warning);
tabControl.SelectedTab = imagesTab;
_formatCheckBoxes.FirstOrDefault()?.Focus();
DialogResult = DialogResult.None;
return;
}

var oldUpdateInterval = Config.General.UpdateCheckInterval;

Config.General.OutputFolder = folder;
Config.General.ConfirmExitWithQueue = confirmExitCheckBox.Checked;
Config.General.CheckForUpdatesOnStartup = checkUpdatesOnStartupCheckBox.Checked;
Config.General.DetectClipboardData = detectClipboardCheckBox.Checked;
Config.General.UpdateCheckInterval = SelectedUpdateInterval();

// Store empty when every format is ticked, so any format added in a future version is
// shown automatically instead of being silently filtered out by a stale saved list.
var allSelected = selectedFormats.Count == _formatCheckBoxes.Count;
Config.General.EnabledFormats = allSelected ? "" : string.Join(",", selectedFormats);
var selectedDisplay = languageComboBox.SelectedItem as string;
Config.General.Language = selectedDisplay != null && _languageMap.TryGetValue(selectedDisplay, out var code)
? code
Expand Down
12 changes: 12 additions & 0 deletions src/Sic/Utils/Config.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,18 @@ public class SectionGeneral {
/// re-focusing never re-prompts for the same data. Opt-in; off by default.</summary>
public bool DetectClipboardData { get; set; }

/// <summary>Comma-separated list of SIC! format keys (e.g. <c>"JPG,PNG,WEBP"</c>) to show
/// in the target-format dropdown, letting users hide formats they never convert to
/// (issue #47). An empty value means "show every supported format" — the default — so a
/// fresh or upgraded config naturally exposes all formats. Parse it via
/// <see cref="GetEnabledFormatKeys"/>.</summary>
public string EnabledFormats { get; set; } = "";

/// <summary>The configured <see cref="EnabledFormats"/> split into individual format keys,
/// trimmed and with blanks dropped. Empty when no restriction is set (all formats shown).</summary>
public IEnumerable<string> GetEnabledFormatKeys() =>
EnabledFormats.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);

/// <summary>How often the app checks for updates in the background while it runs.
/// <see cref="UpdateCheckInterval.Never"/> disables the background loop. Independent of
/// <see cref="CheckForUpdatesOnStartup"/>: either, both, or neither may be active.</summary>
Expand Down
Loading