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
12 changes: 12 additions & 0 deletions src/MandoCode.Desktop/Controls/ChatTabView.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,18 @@ public async Task InitializeAsync()
}
catch { }

// User-data host: serves the chat background image (see ThemeManager.SetChatBackground).
// Missing folder just means no background renders.
try
{
Directory.CreateDirectory(ThemeManager.UserDataFolder);
core.SetVirtualHostNameToFolderMapping(
"mandocode.userdata",
ThemeManager.UserDataFolder,
Microsoft.Web.WebView2.Core.CoreWebView2HostResourceAccessKind.Allow);
}
catch { }

core.NavigateToString(TranscriptHtmlBuilder.BaseDocument(ThemeManager.Current));
}
catch (Exception ex)
Expand Down
107 changes: 92 additions & 15 deletions src/MandoCode.Desktop/MainWindow.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -414,25 +414,28 @@
<StackPanel Spacing="4">
<TextBlock Text="Appearance" FontSize="22" FontWeight="SemiBold"
Foreground="{StaticResource MandoAccentBrush}"/>
<TextBlock Text="A property of the window, not per-agent. Themes restyle everything, transcripts included. Saved on this machine only; never touches the shared CLI config."
<TextBlock Text="App-wide, not per-agent. Saved on this machine only; never touches the shared CLI config."
Opacity="0.6" FontSize="12" TextWrapping="Wrap"/>
</StackPanel>

<ScrollViewer Grid.Row="1">
<StackPanel Spacing="14" MaxWidth="680" HorizontalAlignment="Left" Padding="0,8,0,8">
<Grid ColumnSpacing="12">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Slider x:Name="S_WindowOpacity" Header="Window opacity" Minimum="30" Maximum="100"
Value="100" MinWidth="320" ValueChanged="WindowOpacity_Changed"
ToolTipService.ToolTip="Makes the whole window translucent so you can see what's behind it. 100% = solid (default). Purely visual — never affects the model."/>
<TextBlock x:Name="S_WindowOpacityLabel" Grid.Column="1" Text="100%"
VerticalAlignment="Bottom" Margin="0,0,0,8" Opacity="0.7"
FontSize="13" MinWidth="44"/>
</Grid>
<ListView x:Name="ThemeList" SelectionMode="Single" SelectionChanged="ThemeList_SelectionChanged">
<StackPanel Spacing="8" MaxWidth="680" HorizontalAlignment="Left" Padding="0,8,0,8">
<!-- Win11-Settings style: one collapsible card per setting, with the
current value on the header so the collapsed page reads at a glance. -->
<Expander MinWidth="640"
HorizontalAlignment="Stretch" HorizontalContentAlignment="Stretch">
<Expander.Header>
<StackPanel Orientation="Horizontal" Spacing="10" Padding="0,14">
<FontIcon Glyph="&#xE790;" FontSize="16"/>
<TextBlock Text="Theme" FontWeight="SemiBold"/>
<TextBlock x:Name="ThemeHeaderValue" Opacity="0.6" FontSize="13"
VerticalAlignment="Center"/>
</StackPanel>
</Expander.Header>
<StackPanel Spacing="4">
<TextBlock Text="Themes restyle everything, transcripts included."
Opacity="0.6" FontSize="12" TextWrapping="Wrap"/>
<ListView x:Name="ThemeList" SelectionMode="Single" SelectionChanged="ThemeList_SelectionChanged">
<ListView.ItemTemplate>
<DataTemplate x:DataType="local:ThemeVm">
<Border Background="{x:Bind BgBrush}" BorderBrush="{x:Bind EdgeBrush}"
Expand All @@ -458,6 +461,80 @@
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</StackPanel>
</Expander>

<Expander MinWidth="640"
HorizontalAlignment="Stretch" HorizontalContentAlignment="Stretch">
<Expander.Header>
<StackPanel Orientation="Horizontal" Spacing="10" Padding="0,14">
<FontIcon Glyph="&#xE8B9;" FontSize="16"/>
<TextBlock Text="Chat background" FontWeight="SemiBold"/>
<TextBlock x:Name="BgFileLabel" Text="No image set" Opacity="0.6"
FontSize="13" VerticalAlignment="Center"/>
</StackPanel>
</Expander.Header>
<StackPanel Spacing="10">
<TextBlock Text="Shown behind the conversation in every agent tab. The slider fades the image only — text always stays fully readable."
Opacity="0.6" FontSize="12" TextWrapping="Wrap"/>

<!-- Live preview: theme background + the image at slider opacity,
with sample chat lines on top to judge readability. -->
<Border Height="120" CornerRadius="8"
BorderBrush="{StaticResource MandoBorderBrush}" BorderThickness="1"
Background="{StaticResource MandoBackgroundBrush}">
<Grid>
<Image x:Name="BgPreviewImage" Stretch="UniformToFill" Opacity="0.3"/>
<StackPanel Padding="14,0" Spacing="2" VerticalAlignment="Center">
<TextBlock Text="&gt; how does this look?"
Foreground="{StaticResource MandoGoldBrush}"
FontWeight="SemiBold" FontSize="13"/>
<TextBlock Text="MandoCode"
Foreground="{StaticResource MandoGreenBrush}"
FontWeight="Bold" FontSize="12"/>
<TextBlock Text="Like this — the image fades, the text never does."
Foreground="{StaticResource MandoTextBrush}" FontSize="13"/>
</StackPanel>
</Grid>
</Border>

<StackPanel Orientation="Horizontal" Spacing="8">
<Button x:Name="BgChooseButton" Content="Choose image…" Click="BgChoose_Click"/>
<Button x:Name="BgClearButton" Content="Remove" Click="BgClear_Click"/>
</StackPanel>
<Grid ColumnSpacing="12">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Slider x:Name="S_BgOpacity" Header="Image opacity" Minimum="5" Maximum="100"
Value="30" MinWidth="320" ValueChanged="BgOpacity_Changed"
ToolTipService.ToolTip="How strongly the image shows through. Lower it if text gets hard to read — the text itself never fades."/>
<TextBlock x:Name="S_BgOpacityLabel" Grid.Column="1" Text="30%"
VerticalAlignment="Bottom" Margin="0,0,0,8" Opacity="0.7"
FontSize="13" MinWidth="44"/>
</Grid>
</StackPanel>
</Expander>

<Expander MinWidth="640"
HorizontalAlignment="Stretch" HorizontalContentAlignment="Stretch">
<Expander.Header>
<StackPanel Orientation="Horizontal" Spacing="10" Padding="0,14">
<FontIcon Glyph="&#xE706;" FontSize="16"/>
<TextBlock Text="Window opacity" FontWeight="SemiBold"/>
<TextBlock x:Name="S_WindowOpacityLabel" Text="100%" Opacity="0.6"
FontSize="13" VerticalAlignment="Center"/>
</StackPanel>
</Expander.Header>
<StackPanel Spacing="4">
<TextBlock Text="Makes the whole window translucent so you can see what's behind it. 100% = solid (default). Purely visual — never affects the model."
Opacity="0.6" FontSize="12" TextWrapping="Wrap"/>
<Slider x:Name="S_WindowOpacity" Minimum="30" Maximum="100" Value="100"
MinWidth="320" HorizontalAlignment="Left"
ValueChanged="WindowOpacity_Changed"/>
</StackPanel>
</Expander>
</StackPanel>
</ScrollViewer>
</Grid>
Expand Down
79 changes: 75 additions & 4 deletions src/MandoCode.Desktop/MainWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Media.Animation;
using Microsoft.UI.Xaml.Media.Imaging;
using Microsoft.UI.Xaml.Shapes;
using Windows.ApplicationModel.DataTransfer;
using Windows.System;
Expand Down Expand Up @@ -152,9 +153,15 @@ public MainWindow()
ThemeManager.ThemeChanged += () => OnUi(ApplyThemeToAllTabs);
SettingsTabs.SelectedItem = Tab_Model; // the setup that matters most opens first
ThemeList.ItemsSource = UiTheme.All.Select(t => new ThemeVm { Theme = t }).ToList();
ThemeHeaderValue.Text = ThemeManager.Current.Name;
ModelCombo.Loaded += (_, _) => ApplyModelComboTarget();
S_WindowOpacity.Value = ThemeManager.WindowOpacity * 100;
S_WindowOpacityLabel.Text = $"{(int)S_WindowOpacity.Value}%";
ApplyWindowOpacity(ThemeManager.WindowOpacity);
S_BgOpacity.Value = ThemeManager.ChatBackgroundOpacity * 100;
S_BgOpacityLabel.Text = $"{(int)S_BgOpacity.Value}%";
UpdateBgControls();
_appearanceReady = true; // opacity handlers may persist from here on

_dispatcher = Microsoft.UI.Dispatching.DispatcherQueue.GetForCurrentThread();

Expand Down Expand Up @@ -836,17 +843,80 @@ private void SettingsTabs_SelectionChanged(SelectorBar sender, SelectorBarSelect
// "Make Default for New Agents" always applies.
}

/// <summary>False until the constructor has loaded persisted appearance settings into the
/// sliders. The sliders' XAML default Values fire ValueChanged during InitializeComponent —
/// BEFORE ThemeManager.Initialize reads ui-settings.json — and a Save() in that window
/// overwrites the file with defaults (that bug ate users' saved background image).</summary>
private bool _appearanceReady;

private void WindowOpacity_Changed(object sender, Microsoft.UI.Xaml.Controls.Primitives.RangeBaseValueChangedEventArgs e)
{
// The slider's XAML Value fires this during InitializeComponent, before the
// label (declared after it) exists — nothing to update yet, the constructor
// applies the persisted opacity right after the tree is built.
if (S_WindowOpacityLabel is null) return;
if (!_appearanceReady) return;
S_WindowOpacityLabel.Text = $"{(int)e.NewValue}%";
ThemeManager.SetWindowOpacity(e.NewValue / 100.0);
ApplyWindowOpacity(ThemeManager.WindowOpacity);
}

// ============================================================
// Chat background image (Appearance page)
// ============================================================

private async void BgChoose_Click(object sender, RoutedEventArgs e)
{
var picker = new Windows.Storage.Pickers.FileOpenPicker();
// Desktop apps must marry the picker to an HWND before use.
WinRT.Interop.InitializeWithWindow.Initialize(picker, WinRT.Interop.WindowNative.GetWindowHandle(this));
foreach (var ext in new[] { ".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp" })
picker.FileTypeFilter.Add(ext);

var file = await picker.PickSingleFileAsync();
if (file == null) return;

ThemeManager.SetChatBackground(file.Path);
UpdateBgControls();
ApplyThemeToAllTabs();
}

private void BgClear_Click(object sender, RoutedEventArgs e)
{
ThemeManager.SetChatBackground(null);
UpdateBgControls();
ApplyThemeToAllTabs();
}

private void BgOpacity_Changed(object sender, Microsoft.UI.Xaml.Controls.Primitives.RangeBaseValueChangedEventArgs e)
{
if (!_appearanceReady) return; // see _appearanceReady — a Save() here wipes settings
S_BgOpacityLabel.Text = $"{(int)e.NewValue}%";
BgPreviewImage.Opacity = e.NewValue / 100.0;
ThemeManager.SetChatBackgroundOpacity(e.NewValue / 100.0);
ApplyThemeToAllTabs(); // live preview while dragging — the script is tiny
}

private void UpdateBgControls()
{
var hasImage = ThemeManager.ChatBackgroundFile != null;
BgFileLabel.Text = hasImage ? "Image set ✓" : "No image set";
BgClearButton.IsEnabled = hasImage;
S_BgOpacity.IsEnabled = hasImage;
BgPreviewImage.Opacity = ThemeManager.ChatBackgroundOpacity;

// Decode from bytes, not from the file URI — a URI-sourced BitmapImage keeps the
// file open, and SetChatBackground must be able to overwrite it on the next pick.
BitmapImage? bmp = null;
if (hasImage)
{
try
{
using var ms = new MemoryStream(File.ReadAllBytes(ThemeManager.ChatBackgroundFile!));
bmp = new BitmapImage();
bmp.SetSource(ms.AsRandomAccessStream());
}
catch { bmp = null; /* unreadable image — preview just shows the theme colors */ }
}
BgPreviewImage.Source = bmp;
}

// WinUI has no Window.Opacity — whole-window translucency is a Win32 layered-window
// attribute on the HWND. At 100% the layered style is removed entirely so the
// compositor does no extra work for the default solid window.
Expand Down Expand Up @@ -880,6 +950,7 @@ private void ThemeList_SelectionChanged(object sender, SelectionChangedEventArgs
{
if (_loadingSettings || ThemeList.SelectedItem is not ThemeVm vm) return;
ThemeManager.Apply(vm.Theme, Root);
ThemeHeaderValue.Text = vm.Theme.Name;
SettingsStatus.Text = $"Theme set to {vm.Theme.Name}.";
}

Expand Down
Loading
Loading