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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@ broke. The heading is parsed by `scripts/release-notes.ps1`, so keep its shape;
under it is ordinary Markdown, and the renderer handles paragraphs, lists, links, `code`
and **bold**.

## 1.5.1 - 14 September 2026

### Imported containers lay out at the spacing you choose
**Preferences → Import** sets the **Horizontal spacing** between containers in a row and the
**Vertical spacing** between rows, from 0 to 500 px at 100% zoom. Both start at 32 px, the
gap an import has always used, and a change applies to the next file you import rather than
to containers already on the board.

## 1.5.0 - 11 September 2026

### The board you had open comes back
Expand Down
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
scripts/build-installer.ps1 both read it from here, so releasing is a reviewed change
to this line rather than an edit in a pipeline variable group.
-->
<VersionPrefix>1.5.0</VersionPrefix>
<VersionPrefix>1.5.1</VersionPrefix>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
Expand Down
6 changes: 6 additions & 0 deletions docs/wimport.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,12 @@ Whiteboard measures each container, then packs them **left to right**. A themati
starts a new row even if the current row is not full. Without a break, a row wraps when the
next item would exceed about 2400 world units.

In Preferences → Import, **Horizontal spacing** sets the gap between side-by-side
containers, and **Vertical spacing** sets the gap below the tallest container when a new
row starts, whether from a break or automatic wrapping. Both default to **32 px** and range
from 0 to 500 px at 100% zoom. Changes apply to future imports, not containers already on
the board.

On drop, the group’s **top-left** is the pointer. On Open or File → Import, the group’s
top-left is the top-left of the visible view.

Expand Down
1 change: 1 addition & 0 deletions site/wimport.html
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ <h3>Languages shipped today</h3>

<h2>Layout</h2>
<p>Whiteboard measures each container, then packs them left to right. A thematic break starts a new row even if the current row is not full. Without a break, a row wraps when the next item would exceed about 2400 world units.</p>
<p>In <span class="ui">Preferences → Import</span>, set <span class="ui">Horizontal spacing</span> between containers and <span class="ui">Vertical spacing</span> between rows. Both default to 32 px and range from 0 to 500 px at 100% zoom. Changes apply to future imports only.</p>
<p>On drop, the group’s top-left is the pointer. On <span class="ui">Open</span> or toolbar <span class="ui">Import</span>, the group’s top-left is the top-left of the visible view. Do not put coordinates in the file. There is no <code>pos:</code>, no YAML front matter, and no HTML comment layout.</p>

<h2>How the file is opened</h2>
Expand Down
8 changes: 5 additions & 3 deletions src/SQLBI.Whiteboard.Core/Import/ImportLayout.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ public static class ImportLayout

public static IReadOnlyList<RectD> Place(
IReadOnlyList<(double Width, double Height, bool StartNewRow)> items,
PointD originTopLeft)
PointD originTopLeft,
double horizontalSpacing = Gap,
double verticalSpacing = Gap)
{
ArgumentNullException.ThrowIfNull(items);
var placed = new RectD[items.Count];
Expand All @@ -30,13 +32,13 @@ public static IReadOnlyList<RectD> Place(
if (wrap)
{
x = originTopLeft.X;
y += rowHeight + Gap;
y += rowHeight + verticalSpacing;
rowHeight = 0;
rowOccupied = false;
}

placed[index] = new RectD(x, y, width, height);
x += width + Gap;
x += width + horizontalSpacing;
rowHeight = Math.Max(rowHeight, height);
rowOccupied = true;
}
Expand Down
5 changes: 4 additions & 1 deletion src/SQLBI.Whiteboard.Core/Settings/AppSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,8 @@ public sealed class AppSettings

public ExportSettings Export { get; set; } = new();

public ImportSettings Import { get; set; } = new();

/// <summary>
/// Whether to say at startup that Windows reports nothing to draw with. The
/// tablet list this reads is a list of digitizers rather than an answer
Expand Down Expand Up @@ -180,7 +182,7 @@ public sealed class AppSettings

public static class AppSettingsSerializer
{
public const int CurrentVersion = 17;
public const int CurrentVersion = 18;

/// <summary>
/// The version that moved plain text to the end of the default snippet
Expand Down Expand Up @@ -293,6 +295,7 @@ private static AppSettings Normalize(AppSettings? settings)
settings.Laser = LaserSettings.Normalize(settings.Laser);
settings.PenButtons = PenButtonSettings.Normalize(settings.PenButtons);
settings.Export = ExportSettings.Normalize(settings.Export);
settings.Import = ImportSettings.Normalize(settings.Import);
if (settings.Version < VersionWithPlainTextLast &&
TextLanguageIds.IsLegacyDefaultOrder(settings.SnippetFormatOrder))
{
Expand Down
26 changes: 26 additions & 0 deletions src/SQLBI.Whiteboard.Core/Settings/ImportSettings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using SQLBI.Whiteboard.Core.Import;

namespace SQLBI.Whiteboard.Core.Settings;

public sealed class ImportSettings
{
public const double DefaultSpacing = ImportLayout.Gap;
public const double MinimumSpacing = 0;
public const double MaximumSpacing = 500;

// Board pixels at 100% zoom, independent of the view used to import the file.
public double HorizontalSpacing { get; set; } = DefaultSpacing;

public double VerticalSpacing { get; set; } = DefaultSpacing;

public static ImportSettings Normalize(ImportSettings? settings)
{
var result = settings ?? new ImportSettings();
result.HorizontalSpacing = Clamp(result.HorizontalSpacing);
result.VerticalSpacing = Clamp(result.VerticalSpacing);
return result;
}

private static double Clamp(double value) =>
double.IsFinite(value) ? Math.Clamp(value, MinimumSpacing, MaximumSpacing) : DefaultSpacing;
}
6 changes: 5 additions & 1 deletion src/SQLBI.Whiteboard/MainWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5402,7 +5402,11 @@ private void ApplyImport(ImportDocument imported, PointD originTopLeft, bool rec
return;
}

var rects = ImportLayout.Place(sizes, originTopLeft);
var rects = ImportLayout.Place(
sizes,
originTopLeft,
_settings.Import.HorizontalSpacing,
_settings.Import.VerticalSpacing);
var objects = new List<BoardObject>(decoded.Count);
var assets = new List<BoardAsset>();
for (var index = 0; index < decoded.Count; index++)
Expand Down
48 changes: 32 additions & 16 deletions src/SQLBI.Whiteboard/PreferencesWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -265,9 +265,9 @@ private Border CreateRow(SettingDescriptor setting, string? query)
var value = new TextBlock
{
Style = (Style)FindResource("SettingsValueLabel"),
Text = FormatSeconds(slider.Value),
Text = FormatRangeValue(setting, slider.Value),
};
slider.ValueChanged += (_, _) => value.Text = FormatSeconds(slider.Value);
slider.ValueChanged += (_, _) => value.Text = FormatRangeValue(setting, slider.Value);
Grid.SetColumn(value, 1);
Grid.SetRow(slider, 1);
Grid.SetColumnSpan(slider, 2);
Expand Down Expand Up @@ -908,20 +908,26 @@ private ToggleButton CreateSwitch(SettingDescriptor setting)

private Slider CreateSlider(SettingDescriptor setting)
{
var value = setting.Id == SettingsCatalog.Ids.LaserFadeSeconds
? _settings.Laser.FadeSeconds
: _settings.Laser.HoldSeconds;
var value = setting.Id switch
{
SettingsCatalog.Ids.LaserFadeSeconds => _settings.Laser.FadeSeconds,
SettingsCatalog.Ids.LaserHoldSeconds => _settings.Laser.HoldSeconds,
SettingsCatalog.Ids.ImportHorizontalSpacing => _settings.Import.HorizontalSpacing,
SettingsCatalog.Ids.ImportVerticalSpacing => _settings.Import.VerticalSpacing,
_ => throw new ArgumentException("Unknown range setting.", nameof(setting)),
};
var slider = new Slider
{
Style = (Style)FindResource("SettingsSlider"),
Minimum = setting.Minimum,
Maximum = setting.Maximum,
SmallChange = setting.Id == SettingsCatalog.Ids.LaserFadeSeconds ? 0.05 : 0.25,
LargeChange = setting.Id == SettingsCatalog.Ids.LaserFadeSeconds ? 0.5 : 1,
TickFrequency = setting.Id == SettingsCatalog.Ids.LaserFadeSeconds ? 0.05 : 0.25,
SmallChange = setting.SmallChange,
LargeChange = setting.LargeChange,
TickFrequency = setting.SmallChange,
IsSnapToTickEnabled = true,
Value = value,
};
AutomationProperties.SetName(slider, setting.Title);
slider.ValueChanged += (_, _) => SetRange(setting, slider.Value);
return slider;
}
Expand Down Expand Up @@ -1109,16 +1115,26 @@ private void SetRange(SettingDescriptor setting, double value)
return;
}

if (setting.Id == SettingsCatalog.Ids.LaserFadeSeconds)
{
_settings.Laser.FadeSeconds = value;
}
else
switch (setting.Id)
{
_settings.Laser.HoldSeconds = value;
case SettingsCatalog.Ids.LaserFadeSeconds:
_settings.Laser.FadeSeconds = value;
break;
case SettingsCatalog.Ids.LaserHoldSeconds:
_settings.Laser.HoldSeconds = value;
break;
case SettingsCatalog.Ids.ImportHorizontalSpacing:
_settings.Import.HorizontalSpacing = value;
break;
case SettingsCatalog.Ids.ImportVerticalSpacing:
_settings.Import.VerticalSpacing = value;
break;
default:
return;
}

_settings.Laser = LaserSettings.Normalize(_settings.Laser);
_settings.Import = ImportSettings.Normalize(_settings.Import);
NotifyApplied();
}

Expand Down Expand Up @@ -1195,11 +1211,11 @@ private static int DistinctCategoryCount(IReadOnlyList<SettingDescriptor> settin
return seen.Count;
}

private static string FormatSeconds(double value)
private static string FormatRangeValue(SettingDescriptor setting, double value)
{
var rounded = Math.Round(value, 2, MidpointRounding.AwayFromZero);
var text = rounded.ToString(rounded == Math.Truncate(rounded) ? "0" : "0.##", CultureInfo.CurrentCulture);
return $"{text} s";
return $"{text} {setting.Unit}";
}

private sealed class MonitorChoiceItem
Expand Down
43 changes: 42 additions & 1 deletion src/SQLBI.Whiteboard/SettingsCatalog.cs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,12 @@ internal sealed class SettingDescriptor

public double Maximum { get; init; }

public double SmallChange { get; init; } = 1;

public double LargeChange { get; init; } = 10;

public string Unit { get; init; } = string.Empty;

public bool HideInStore { get; init; }
}

Expand All @@ -96,6 +102,8 @@ public static class Ids
{
public const string StartupMonitor = "startup.monitor";
public const string StartFullScreen = "startup.fullscreen";
public const string ImportHorizontalSpacing = "import.horizontalSpacing";
public const string ImportVerticalSpacing = "import.verticalSpacing";
public const string LaserHoldSeconds = "laser.holdSeconds";
public const string LaserFadeSeconds = "laser.fadeSeconds";
public const string LaserHoldMode = "laser.holdMode";
Expand Down Expand Up @@ -127,12 +135,13 @@ public static class EraserButton

public const string Startup = "Startup";
public const string Input = "Input";
public const string Import = "Import";
public const string Laser = "Laser pointer";
public const string Toolbar = "Toolbar";
public const string Updates = "Updates";

public static IReadOnlyList<string> Categories { get; } =
[Startup, Input, Laser, Toolbar, Updates];
[Startup, Input, Import, Laser, Toolbar, Updates];

public static IReadOnlyList<SettingDescriptor> All { get; } =
[
Expand Down Expand Up @@ -244,6 +253,32 @@ public static class EraserButton
Editor = SettingEditorKind.OrderedList,
},
new()
{
Id = Ids.ImportHorizontalSpacing,
Category = Import,
Title = "Horizontal spacing",
Summary = "Space between imported containers in a row",
Description = "Pixels at 100% zoom. Applies to the next .wimport file, without rearranging existing containers. The default is 32 px.",
Keywords = ["import", "wimport", "horizontal", "spacing", "gap", "pixels", "layout"],
Editor = SettingEditorKind.DoubleRange,
Minimum = ImportSettings.MinimumSpacing,
Maximum = ImportSettings.MaximumSpacing,
Unit = "px",
},
new()
{
Id = Ids.ImportVerticalSpacing,
Category = Import,
Title = "Vertical spacing",
Summary = "Space between imported rows",
Description = "Pixels at 100% zoom, measured below the tallest item in the previous row. Applies to explicit line breaks and automatic wrapping in the next .wimport file. The default is 32 px.",
Keywords = ["import", "wimport", "vertical", "spacing", "gap", "pixels", "row", "line", "layout"],
Editor = SettingEditorKind.DoubleRange,
Minimum = ImportSettings.MinimumSpacing,
Maximum = ImportSettings.MaximumSpacing,
Unit = "px",
},
new()
{
Id = Ids.LaserHoldSeconds,
Category = Laser,
Expand All @@ -253,6 +288,9 @@ public static class EraserButton
Editor = SettingEditorKind.DoubleRange,
Minimum = LaserSettings.MinimumHoldSeconds,
Maximum = LaserSettings.MaximumHoldSeconds,
SmallChange = 0.25,
LargeChange = 1,
Unit = "s",
},
new()
{
Expand All @@ -264,6 +302,9 @@ public static class EraserButton
Editor = SettingEditorKind.DoubleRange,
Minimum = LaserSettings.MinimumFadeSeconds,
Maximum = LaserSettings.MaximumFadeSeconds,
SmallChange = 0.05,
LargeChange = 0.5,
Unit = "s",
},
new()
{
Expand Down
Loading