Make settings window independent and taskbar-aware

Convert the settings window into an independent top-level window with its own taskbar icon and open-or-focus semantics. Removed Owner/anchor/toggle semantics from SettingsWindowService and added ScreenReferenceWindow for centering; settings windows now ShowInTaskbar = true and are truly destroyed on close. Added SettingsWindowPlacementHelper and tests for placement/centering. Main window now respects an AppSettingsSnapshot.ShowInTaskbar flag (new setting exposed in GeneralSettings UI) and slide/visibility animations and "back to Windows" behavior no longer affect the independent settings window. Updated various callers to use OpenIndependentSettingsModule, adjusted window transitions/X offsets, and added/updated spec files documenting the feature and animation boundary.
This commit is contained in:
lincube
2026-04-22 20:46:43 +08:00
parent aa7c118d13
commit e20462ac2b
15 changed files with 367 additions and 180 deletions

View File

@@ -0,0 +1,6 @@
- [x] 从桌面、托盘、IPC、组件库进入设置时都会落到同一个设置窗口
- [x] 设置已打开时再次触发设置入口,只会聚焦已有窗口,不会切换成关闭
- [x] 设置窗口始终拥有独立任务栏图标,不受“桌面主窗口在任务栏显示图标”开关影响
- [x] 点击“回到 Windows”后只隐藏或最小化桌面主窗口设置窗口保持可见
- [x] 启用滑入滑出动画后,只有主窗口参与动画,设置窗口不参与
- [x] 点击设置窗口关闭按钮后会真实关闭;再次打开时创建新的居中窗口

View File

@@ -0,0 +1,78 @@
# 独立设置窗口 Spec
## Why
- 当前设置窗口仍然带有桌面壳的 owner / anchor 语义,点击“回到 Windows”或触发桌面动画时容易被一起隐藏或重新定位。
- 产品新增了“在任务栏显示图标”和“启用滑入滑出动画”设置,需要明确边界:它们只影响桌面主窗口,不影响设置窗口。
- 桌面底栏、托盘菜单、IPC、组件库等入口应当始终打开同一个独立设置窗口而不是切换成附属浮窗或开关行为。
## What Changes
- 将设置窗口改为独立顶层窗口,始终使用自己的任务栏按钮和图标。
- `SettingsWindowService.Open` 改为幂等的 open-or-focus重复打开只聚焦已有窗口并在提供目标页时切换到对应页面。
- 移除 `Owner`、锚点定位和 `Toggle` 语义;首次打开按参考屏幕居中,关闭为真实关闭。
- 桌面壳的“回到 Windows”、最小化到托盘/任务栏、滑入滑出动画,只影响 `MainWindow`,不会影响设置窗口。
- 统一桌面、托盘、IPC、组件库等设置入口全部走 `OpenIndependentSettingsModule`
- 设置页文案明确“在任务栏显示图标”只控制桌面主窗口;设置窗口始终保留独立任务栏图标。
## Impact
- Affected code:
- `LanMountainDesktop/Services/Settings/SettingsWindowService.cs`
- `LanMountainDesktop/App.axaml.cs`
- `LanMountainDesktop/Views/MainWindow.axaml.cs`
- `LanMountainDesktop/Views/MainWindow.ComponentSystem.cs`
- `LanMountainDesktop/Views/FusedDesktopComponentLibraryControl.axaml.cs`
- `LanMountainDesktop/Views/SettingsPages/GeneralSettingsPage.axaml`
- Affected behavior:
- 设置窗口生命周期
- 设置入口一致性
- 任务栏图标与桌面壳显示边界
---
## ADDED Requirements
### Requirement: 设置窗口为独立顶层窗口
系统 SHALL 将设置窗口作为独立顶层窗口显示,而不是作为桌面主窗口的附属子窗。
#### Scenario: 设置窗口拥有独立任务栏图标
- **WHEN** 用户打开设置窗口
- **THEN** 设置窗口使用独立顶层窗口方式显示
- **AND THEN** 设置窗口在任务栏中保留自己的独立按钮和图标
- **AND THEN** “在任务栏显示图标”开关不会影响设置窗口的任务栏按钮
### Requirement: 设置入口统一为 open-or-focus
系统 SHALL 让所有设置入口打开或聚焦同一个设置窗口实例。
#### Scenario: 已打开时重复触发设置入口
- **WHEN** 设置窗口已经打开,用户再次从桌面、托盘或 IPC 触发打开设置
- **THEN** 系统只聚焦现有设置窗口
- **AND THEN** 如果请求包含目标页,则导航到目标页
- **AND THEN** 不会把已打开的设置窗口当作开关关闭
### Requirement: 设置窗口不参与桌面壳可见性切换
系统 SHALL 让桌面壳的隐藏、最小化和进出场动画只作用于主窗口。
#### Scenario: 回到 Windows 时设置窗口保持可见
- **WHEN** 主窗口执行“回到 Windows”并隐藏到托盘或最小化到任务栏
- **THEN** 设置窗口保持当前可见状态
- **AND THEN** 设置窗口不会跟随主窗口一起隐藏、最小化或重定位
#### Scenario: 桌面滑入滑出动画不作用于设置窗口
- **WHEN** 启用了滑入滑出动画并触发主窗口退场或入场
- **THEN** 只有主窗口参与动画
- **AND THEN** 设置窗口不会消失,也不会跟随主窗口做进出场动画
### Requirement: 关闭设置窗口时真实销毁实例
系统 SHALL 在用户关闭设置窗口时真实关闭该窗口实例。
#### Scenario: 关闭后再次打开
- **WHEN** 用户点击设置窗口右上角关闭按钮
- **THEN** 当前设置窗口实例被关闭并销毁
- **AND THEN** 下次再次打开设置时创建新的设置窗口实例
- **AND THEN** 新窗口按参考屏幕居中显示

View File

@@ -0,0 +1,25 @@
# Tasks
- [x] Task 1: 简化设置窗口打开契约
- [x]`SettingsWindowOpenRequest` 从 owner / anchor 语义改为目标页 + 参考屏幕语义
- [x] 移除 `ISettingsWindowService.Toggle`
- [x] Task 2: 重做设置窗口服务行为
- [x] 设置窗口始终使用 `Show()` 打开
- [x] 设置窗口始终 `ShowInTaskbar = true`
- [x] 已打开时只聚焦并在需要时切页
- [x] 关闭后销毁实例,下次打开重新创建并居中
- [x] Task 3: 统一设置入口并解耦桌面壳
- [x] 桌面底栏设置按钮改为 open-or-focus
- [x] 组件库入口改为复用 `OpenIndependentSettingsModule`
- [x] 移除 `MainWindow` 上的设置窗口锚点逻辑
- [x] Task 4: 明确产品边界
- [x] 调整“在任务栏显示图标”文案,限定为桌面主窗口
- [x] 新增独立设置窗口 feature spec
- [x] 在窗口过渡动画 spec 中补充“设置窗口不参与动画”
- [x] Task 5: 验证
- [x] 运行 `dotnet build LanMountainDesktop.slnx -c Debug`
- [x] 运行与新 helper 相关的测试

View File

@@ -113,6 +113,15 @@
- **AND THEN** 过渡时长使用 `FluttermotionToken.Duration.Page`320ms`FluttermotionToken.Duration.Intro`400ms
- **AND THEN** 缓动函数使用 `0.05,0.75,0.10,1.00`DecelerateBezier
### Requirement: 设置窗口不参与桌面壳过渡动画
系统 SHALL 将桌面壳进出场动画限制在主窗口范围内,不影响独立设置窗口。
#### Scenario: 设置窗口在桌面动画期间保持独立
- **WHEN** 主窗口执行滑入、滑出、最小化或恢复动画
- **THEN** 设置窗口不参与该动画
- **AND THEN** 设置窗口不会跟随主窗口一起隐藏、最小化或重定位
## MODIFIED Requirements
### Requirement: OnMinimizeClick 行为

View File

@@ -0,0 +1,50 @@
using Avalonia;
using LanMountainDesktop.Services.Settings;
using Xunit;
namespace LanMountainDesktop.Tests;
public sealed class SettingsWindowPlacementHelperTests
{
[Fact]
public void ResolveWorkingArea_PrefersReferenceScreen()
{
var referenceArea = new PixelRect(1920, 0, 2560, 1440);
var primaryArea = new PixelRect(0, 0, 1920, 1080);
var result = SettingsWindowPlacementHelper.ResolveWorkingArea(
referenceArea,
primaryArea,
fallbackWindowWidth: 1120,
fallbackWindowHeight: 760);
Assert.Equal(referenceArea, result);
}
[Fact]
public void ResolveWorkingArea_FallsBackToPrimaryScreenWhenReferenceIsMissing()
{
var primaryArea = new PixelRect(0, 0, 1920, 1080);
var result = SettingsWindowPlacementHelper.ResolveWorkingArea(
referenceWorkingArea: null,
primaryWorkingArea: primaryArea,
fallbackWindowWidth: 1120,
fallbackWindowHeight: 760);
Assert.Equal(primaryArea, result);
}
[Fact]
public void CalculateCenteredPosition_ReturnsCenteredPointInsideWorkingArea()
{
var workingArea = new PixelRect(1920, 40, 2560, 1400);
var result = SettingsWindowPlacementHelper.CalculateCenteredPosition(
workingArea,
windowWidth: 1120,
windowHeight: 760);
Assert.Equal(new PixelPoint(2640, 360), result);
}
}

View File

@@ -117,8 +117,8 @@ public partial class App : Application
$"Opening settings window. Source='{source}'; PageTag='{pageTag ?? "<default>"}'.");
_settingsWindowService?.Open(new SettingsWindowOpenRequest(
Source: source,
Owner: _mainWindow is { IsVisible: true } ? _mainWindow : null,
PageId: pageTag));
PageId: pageTag,
ScreenReferenceWindow: _mainWindow is { IsVisible: true } ? _mainWindow : null));
}
public App()
@@ -738,7 +738,7 @@ public partial class App : Application
var mainWindow = GetOrCreateMainWindow(desktop, source);
mainWindow.PrepareEnterAnimation();
mainWindow.ShowInTaskbar = true;
mainWindow.ShowInTaskbar = ShouldShowMainWindowInTaskbar();
if (!mainWindow.IsVisible)
{
@@ -1106,7 +1106,7 @@ public partial class App : Application
var mainWindow = new MainWindow
{
DataContext = new MainWindowViewModel(),
ShowInTaskbar = true
ShowInTaskbar = ShouldShowMainWindowInTaskbar()
};
_mainWindowOpened = false;
@@ -1296,6 +1296,11 @@ public partial class App : Application
}
}
private bool ShouldShowMainWindowInTaskbar()
{
return _settingsFacade.Settings.LoadSnapshot<AppSettingsSnapshot>(SettingsScope.App).ShowInTaskbar;
}
private void SetDesktopShellState(DesktopShellState state, string source)
{
if (_desktopShellState == state)

View File

@@ -154,6 +154,8 @@ public sealed class AppSettingsSnapshot
public bool EnableSlideTransition { get; set; } = false;
public bool ShowInTaskbar { get; set; } = false;
public bool EnableFusedDesktop { get; set; } = false;
public List<string> DisabledPluginIds { get; set; } = [];

View File

@@ -14,28 +14,10 @@ using LanMountainDesktop.Views;
namespace LanMountainDesktop.Services.Settings;
public enum SettingsWindowAnchorTarget
{
DesktopDockTrailingEdge = 0
}
public enum SettingsWindowFallbackMode
{
None = 0,
ScreenBottomRight = 1
}
public readonly record struct SettingsWindowOpenRequest(
string Source,
Window? Owner = null,
string? PageId = null,
SettingsWindowAnchorTarget AnchorTarget = SettingsWindowAnchorTarget.DesktopDockTrailingEdge,
SettingsWindowFallbackMode FallbackMode = SettingsWindowFallbackMode.ScreenBottomRight);
public interface ISettingsWindowAnchorProvider
{
bool TryGetSettingsWindowAnchorBounds(out PixelRect anchorBounds);
}
Window? ScreenReferenceWindow = null);
public interface ISettingsWindowService
{
@@ -46,8 +28,6 @@ public interface ISettingsWindowService
void Open(SettingsWindowOpenRequest request);
void Close();
void Toggle(SettingsWindowOpenRequest request);
}
internal sealed class SettingsWindowService : ISettingsWindowService
@@ -92,27 +72,25 @@ internal sealed class SettingsWindowService : ISettingsWindowService
var appearanceSnapshot = _appearanceThemeService.GetCurrent();
_window.ApplyChromeMode(appearanceSnapshot.UseSystemChrome);
ApplyTheme(_window);
_window.ReloadPages(request.PageId);
PositionWindow(_window, request);
var targetPageId = request.PageId ?? _window.ViewModel.CurrentPageId;
_window.ReloadPages(targetPageId);
if (!_window.IsVisible)
{
if (request.Owner is not null && request.Owner.IsVisible)
{
_window.Show(request.Owner);
}
else
{
CenterWindow(_window, request);
_window.Show();
}
NotifyStateChanged();
PositionWindowLater(_window, request);
CenterWindowLater(_window, request);
return;
}
if (_window.WindowState == WindowState.Minimized)
{
_window.WindowState = WindowState.Normal;
}
_window.Activate();
PositionWindowLater(_window, request);
}
public void Close()
@@ -120,17 +98,6 @@ internal sealed class SettingsWindowService : ISettingsWindowService
_window?.Close();
}
public void Toggle(SettingsWindowOpenRequest request)
{
if (IsOpen)
{
Close();
return;
}
Open(request);
}
private SettingsWindow CreateWindow()
{
var regionState = _settingsFacade.Region.Get();
@@ -147,7 +114,7 @@ internal sealed class SettingsWindowService : ISettingsWindowService
_hostApplicationLifecycle,
useSystemChrome);
ApplyTheme(window);
window.ShowInTaskbar = false;
window.ShowInTaskbar = true;
window.Closed += (_, _) =>
{
_window = null;
@@ -156,106 +123,87 @@ internal sealed class SettingsWindowService : ISettingsWindowService
return window;
}
private void PositionWindowLater(SettingsWindow window, SettingsWindowOpenRequest request)
private void CenterWindowLater(SettingsWindow window, SettingsWindowOpenRequest request)
{
Dispatcher.UIThread.Post(
() =>
{
if (!window.IsVisible)
if (!ReferenceEquals(_window, window) || !window.IsVisible)
{
return;
}
PositionWindow(window, request);
CenterWindow(window, request);
},
DispatcherPriority.Background);
}
private static void PositionWindow(SettingsWindow window, SettingsWindowOpenRequest request)
private static void CenterWindow(SettingsWindow window, SettingsWindowOpenRequest request)
{
if (request.AnchorTarget == SettingsWindowAnchorTarget.DesktopDockTrailingEdge &&
request.Owner is ISettingsWindowAnchorProvider anchorProvider &&
anchorProvider.TryGetSettingsWindowAnchorBounds(out var anchorBounds))
{
PositionWindowAboveAnchor(window, anchorBounds, request);
return;
var referenceWorkingArea =
request.ScreenReferenceWindow is { IsVisible: true } screenReferenceWindow &&
screenReferenceWindow.Screens?.ScreenFromWindow(screenReferenceWindow) is { } referenceScreen
? referenceScreen.WorkingArea
: (PixelRect?)null;
var width = ResolveWindowWidth(window, request.ScreenReferenceWindow);
var height = ResolveWindowHeight(window, request.ScreenReferenceWindow);
var workingArea = SettingsWindowPlacementHelper.ResolveWorkingArea(
referenceWorkingArea,
window.Screens?.Primary?.WorkingArea,
width,
height);
window.Position = SettingsWindowPlacementHelper.CalculateCenteredPosition(workingArea, width, height);
}
if (request.FallbackMode == SettingsWindowFallbackMode.ScreenBottomRight)
private static int ResolveWindowWidth(Window window, Window? referenceWindow)
{
PositionWindowNearScreenBottomRight(window, request);
}
}
private static void PositionWindowAboveAnchor(Window window, PixelRect anchorBounds, SettingsWindowOpenRequest request)
{
var workingArea = GetWorkingArea(window, request);
if (anchorBounds.Width <= 0 || anchorBounds.Height <= 0 ||
anchorBounds.Right < workingArea.X || anchorBounds.Y > workingArea.Bottom)
{
PositionWindowNearScreenBottomRight(window, request);
return;
}
var scale = window.RenderScaling > 0 ? window.RenderScaling : 1d;
var width = ResolveWindowWidth(window, scale);
var height = ResolveWindowHeight(window, scale);
var inset = (int)Math.Round(24 * scale);
var gap = (int)Math.Round(16 * scale);
var x = anchorBounds.Right - width - inset;
var y = anchorBounds.Y - height - gap;
x = Math.Clamp(x, workingArea.X + inset, Math.Max(workingArea.X + inset, workingArea.Right - width - inset));
y = Math.Clamp(y, workingArea.Y + inset, Math.Max(workingArea.Y + inset, workingArea.Bottom - height - inset));
window.Position = new PixelPoint(x, y);
}
private static void PositionWindowNearScreenBottomRight(Window window, SettingsWindowOpenRequest request)
{
var workingArea = GetWorkingArea(window, request);
var scale = window.RenderScaling > 0 ? window.RenderScaling : 1d;
var width = ResolveWindowWidth(window, scale);
var height = ResolveWindowHeight(window, scale);
var inset = (int)Math.Round(24 * scale);
var x = Math.Max(workingArea.X + inset, workingArea.Right - width - inset);
var y = Math.Max(workingArea.Y + inset, workingArea.Bottom - height - inset);
window.Position = new PixelPoint(x, y);
}
private static PixelRect GetWorkingArea(Window window, SettingsWindowOpenRequest request)
{
if (request.Owner is not null && request.Owner.Screens?.ScreenFromWindow(request.Owner) is { } ownerScreen)
{
return ownerScreen.WorkingArea;
}
if (window.Screens?.ScreenFromWindow(window) is { } windowScreen)
{
return windowScreen.WorkingArea;
}
return window.Screens?.Primary?.WorkingArea
?? new PixelRect(
0,
0,
Math.Max(1280, ResolveWindowWidth(window, 1d) + 96),
Math.Max(720, ResolveWindowHeight(window, 1d) + 96));
}
private static int ResolveWindowWidth(Window window, double scale)
{
var widthDip = window.Bounds.Width > 1 ? window.Bounds.Width : Math.Max(window.Width, window.MinWidth);
var widthDip = ResolveWindowDimensionDip(window.Bounds.Width, window.Width, window.MinWidth, 1120d);
var scale = ResolveWindowScale(window, referenceWindow);
return Math.Max(320, (int)Math.Round(widthDip * scale));
}
private static int ResolveWindowHeight(Window window, double scale)
private static int ResolveWindowHeight(Window window, Window? referenceWindow)
{
var heightDip = window.Bounds.Height > 1 ? window.Bounds.Height : Math.Max(window.Height, window.MinHeight);
var heightDip = ResolveWindowDimensionDip(window.Bounds.Height, window.Height, window.MinHeight, 760d);
var scale = ResolveWindowScale(window, referenceWindow);
return Math.Max(240, (int)Math.Round(heightDip * scale));
}
private static double ResolveWindowScale(Window window, Window? referenceWindow)
{
if (referenceWindow is not null && referenceWindow.RenderScaling > 0)
{
return referenceWindow.RenderScaling;
}
if (window.RenderScaling > 0)
{
return window.RenderScaling;
}
return 1d;
}
private static double ResolveWindowDimensionDip(double boundsDip, double configuredDip, double minimumDip, double fallbackDip)
{
if (boundsDip > 1)
{
return boundsDip;
}
if (!double.IsNaN(configuredDip) && configuredDip > 1)
{
return configuredDip;
}
if (!double.IsNaN(minimumDip) && minimumDip > 1)
{
return minimumDip;
}
return fallbackDip;
}
private void NotifyStateChanged()
{
StateChanged?.Invoke(this, EventArgs.Empty);
@@ -363,3 +311,38 @@ internal sealed class SettingsWindowService : ISettingsWindowService
}, DispatcherPriority.Background);
}
}
internal static class SettingsWindowPlacementHelper
{
internal static PixelRect ResolveWorkingArea(
PixelRect? referenceWorkingArea,
PixelRect? primaryWorkingArea,
int fallbackWindowWidth,
int fallbackWindowHeight)
{
if (referenceWorkingArea is { } referenceArea)
{
return referenceArea;
}
if (primaryWorkingArea is { } primaryArea)
{
return primaryArea;
}
return new PixelRect(
0,
0,
Math.Max(1280, fallbackWindowWidth + 96),
Math.Max(720, fallbackWindowHeight + 96));
}
internal static PixelPoint CalculateCenteredPosition(PixelRect workingArea, int windowWidth, int windowHeight)
{
var horizontalOffset = Math.Max(0, (workingArea.Width - windowWidth) / 2);
var verticalOffset = Math.Max(0, (workingArea.Height - windowHeight) / 2);
return new PixelPoint(
workingArea.X + horizontalOffset,
workingArea.Y + verticalOffset);
}
}

View File

@@ -202,6 +202,7 @@ public sealed partial class GeneralSettingsPageViewModel : ViewModelBase, IDispo
string.Equals(option.Value, normalizedRenderMode, StringComparison.OrdinalIgnoreCase))
?? RenderModes[0];
EnableSlideTransition = appSnapshot.EnableSlideTransition;
ShowInTaskbar = appSnapshot.ShowInTaskbar;
_isInitializing = false;
RefreshPreview();
@@ -238,6 +239,11 @@ public sealed partial class GeneralSettingsPageViewModel : ViewModelBase, IDispo
{
EnableSlideTransition = _settingsFacade.Settings.LoadSnapshot<AppSettingsSnapshot>(SettingsScope.App).EnableSlideTransition;
}
if (changedKeys.Contains(nameof(AppSettingsSnapshot.ShowInTaskbar)))
{
ShowInTaskbar = _settingsFacade.Settings.LoadSnapshot<AppSettingsSnapshot>(SettingsScope.App).ShowInTaskbar;
}
}
public event Action? RestartRequested;
@@ -260,6 +266,9 @@ public sealed partial class GeneralSettingsPageViewModel : ViewModelBase, IDispo
[ObservableProperty]
private bool _enableSlideTransition;
[ObservableProperty]
private bool _showInTaskbar;
public bool IsSlideTransitionAvailable => System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Windows);
[ObservableProperty]
@@ -367,6 +376,12 @@ public sealed partial class GeneralSettingsPageViewModel : ViewModelBase, IDispo
SaveField(nameof(AppSettingsSnapshot.EnableSlideTransition), value);
}
partial void OnShowInTaskbarChanged(bool value)
{
if (_isInitializing) return;
SaveField(nameof(AppSettingsSnapshot.ShowInTaskbar), value);
}
private void SaveField<T>(string key, T value)
{
var snapshot = _settingsFacade.Settings.LoadSnapshot<AppSettingsSnapshot>(SettingsScope.App);

View File

@@ -256,18 +256,14 @@ public partial class FusedDesktopComponentLibraryControl : UserControl
private void OnFindMoreComponentsClick(object? sender, RoutedEventArgs e)
{
// 打开设置窗口并导航到插件目录页面
if (Application.Current is App app && app.SettingsWindowService is { } settingsWindowService)
if (Application.Current is App app)
{
var mainWindow = (Application.Current?.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime)?.MainWindow as MainWindow;
var request = new SettingsWindowOpenRequest(
Source: "FusedDesktopComponentLibrary",
Owner: mainWindow,
PageId: "plugin-catalog");
settingsWindowService.Open(request);
app.OpenIndependentSettingsModule("FusedDesktopComponentLibrary", "plugin-catalog");
}
// 关闭所在窗口
var window = this.FindAncestorOfType<Window>();
window?.Close();
var componentLibraryWindow = this.FindAncestorOfType<Window>();
componentLibraryWindow?.Close();
}
}

View File

@@ -19,7 +19,6 @@ using LanMountainDesktop.DesktopEditing;
using LanMountainDesktop.Host.Abstractions;
using LanMountainDesktop.Models;
using LanMountainDesktop.Services;
using LanMountainDesktop.Services.Settings;
using LanMountainDesktop.Settings.Core;
using LanMountainDesktop.Theme;
using LanMountainDesktop.Views.Components;
@@ -282,16 +281,7 @@ public partial class MainWindow
CloseComponentLibraryWindow(reopenSettings: false);
}
var app = Application.Current as App;
if (app?.SettingsWindowService is { } settingsWindowService)
{
settingsWindowService.Toggle(new SettingsWindowOpenRequest(
Source: "MainWindowTaskbar",
Owner: this));
return;
}
app?.OpenIndependentSettingsModule("MainWindowTaskbar");
(Application.Current as App)?.OpenIndependentSettingsModule("MainWindowTaskbar");
}
private void OnPowerMenuEnterClick(object? sender, RoutedEventArgs e)
@@ -2861,34 +2851,6 @@ public partial class MainWindow
CloseDetachedComponentLibraryWindow();
}
public bool TryGetSettingsWindowAnchorBounds(out PixelRect anchorBounds)
{
anchorBounds = default;
if (!IsVisible || BottomTaskbarContainer is null)
{
return false;
}
var origin = BottomTaskbarContainer.TranslatePoint(new Point(0, 0), this);
if (origin is null)
{
return false;
}
var scale = RenderScaling > 0 ? RenderScaling : 1d;
var width = (int)Math.Round(BottomTaskbarContainer.Bounds.Width * scale);
var height = (int)Math.Round(BottomTaskbarContainer.Bounds.Height * scale);
if (width <= 0 || height <= 0)
{
return false;
}
var x = Position.X + (int)Math.Round(origin.Value.X * scale);
var y = Position.Y + (int)Math.Round(origin.Value.Y * scale);
anchorBounds = new PixelRect(x, y, width, height);
return true;
}
private void CollapseComponentLibraryPanel()
{
// Animate component library panel collapsing downward

View File

@@ -79,6 +79,7 @@ public partial class MainWindow
string.Equals(key, nameof(AppSettingsSnapshot.UpdateDownloadSource), StringComparison.OrdinalIgnoreCase) ||
string.Equals(key, nameof(AppSettingsSnapshot.UpdateDownloadThreads), StringComparison.OrdinalIgnoreCase) ||
string.Equals(key, nameof(AppSettingsSnapshot.EnableThreeFingerSwipe), StringComparison.OrdinalIgnoreCase) ||
string.Equals(key, nameof(AppSettingsSnapshot.ShowInTaskbar), StringComparison.OrdinalIgnoreCase) ||
string.Equals(key, nameof(AppSettingsSnapshot.EnableSlideTransition), StringComparison.OrdinalIgnoreCase)))
{
return;
@@ -688,6 +689,10 @@ public partial class MainWindow
StatusBarShadowEnabled = _statusBarShadowEnabled,
StatusBarShadowColor = _statusBarShadowColor,
StatusBarShadowOpacity = _statusBarShadowOpacity,
EnableThreeFingerSwipe = existingSnapshot.EnableThreeFingerSwipe,
EnableSlideTransition = existingSnapshot.EnableSlideTransition,
ShowInTaskbar = existingSnapshot.ShowInTaskbar,
EnableFusedDesktop = existingSnapshot.EnableFusedDesktop,
DisabledPluginIds = existingSnapshot.DisabledPluginIds,
StudyFrameMs = existingSnapshot.StudyFrameMs,
StudyScoreThresholdDbfs = existingSnapshot.StudyScoreThresholdDbfs,

View File

@@ -20,6 +20,7 @@
UseLayoutRounding="True"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
Background="Transparent"
TransparencyLevelHint="Transparent"
Title="LanMountainDesktop">
<Design.DataContext>
@@ -99,12 +100,17 @@
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<Grid.RenderTransform>
<TranslateTransform />
<TranslateTransform>
<TranslateTransform.Transitions>
<Transitions>
<DoubleTransition Property="X" Duration="{StaticResource FluttermotionToken.Duration.Intro}" Easing="0.05,0.75,0.10,1.00" />
</Transitions>
</TranslateTransform.Transitions>
</TranslateTransform>
</Grid.RenderTransform>
<Grid.Transitions>
<Transitions>
<DoubleTransition Property="Opacity" Duration="{StaticResource FluttermotionToken.Duration.Page}" Easing="0.05,0.75,0.10,1.00" />
<DoubleTransition Property="TranslateTransform.X" Duration="{StaticResource FluttermotionToken.Duration.Intro}" Easing="0.05,0.75,0.10,1.00" />
</Transitions>
</Grid.Transitions>

View File

@@ -29,7 +29,7 @@ using LanMountainDesktop.Views.Components;
namespace LanMountainDesktop.Views;
public partial class MainWindow : Window, ISettingsWindowAnchorProvider
public partial class MainWindow : Window
{
private enum WallpaperMediaType
{
@@ -450,6 +450,8 @@ public partial class MainWindow : Window, ISettingsWindowAnchorProvider
MinShortSideCells,
MaxShortSideCells);
ShowInTaskbar = snapshot.ShowInTaskbar;
_gridSpacingPreset = _gridSettingsService.NormalizeSpacingPreset(snapshot.GridSpacingPreset);
_desktopEdgeInsetPercent = Math.Clamp(snapshot.DesktopEdgeInsetPercent, MinEdgeInsetPercent, MaxEdgeInsetPercent);
@@ -884,7 +886,19 @@ public partial class MainWindow : Window, ISettingsWindowAnchorProvider
return;
}
var snapshot = _settingsService.LoadSnapshot<AppSettingsSnapshot>(SettingsScope.App);
if (snapshot.ShowInTaskbar)
{
WindowState = WindowState.Minimized;
}
else if (Application.Current is App app)
{
app.HideMainWindowToTray(this, "MinimizeAction");
}
else
{
WindowState = WindowState.Minimized;
}
slideTransform.X = 0;
DesktopPage.Opacity = 1;
@@ -906,7 +920,8 @@ public partial class MainWindow : Window, ISettingsWindowAnchorProvider
if (useSlide)
{
slideTransform.X = Bounds.Width > 0 ? Bounds.Width : 1920;
var screenWidth = Screens.ScreenFromVisual(this)?.Bounds.Width ?? 3840;
slideTransform.X = Bounds.Width > 0 ? Bounds.Width : screenWidth;
}
DesktopPage.Transitions = savedTransitions;
@@ -941,7 +956,27 @@ public partial class MainWindow : Window, ISettingsWindowAnchorProvider
return;
}
if (WindowState is WindowState.Minimized or WindowState.FullScreen)
var newState = (WindowState)e.NewValue!;
var oldState = (WindowState)e.OldValue!;
if (oldState == WindowState.Minimized && newState != WindowState.Minimized)
{
PrepareEnterAnimation();
if (newState != WindowState.FullScreen)
{
WindowState = WindowState.FullScreen;
}
Dispatcher.UIThread.Post(() =>
{
PlayEnterAnimation();
}, DispatcherPriority.Background);
return;
}
if (newState is WindowState.Minimized or WindowState.FullScreen)
{
return;
}

View File

@@ -117,6 +117,16 @@
</ui:SettingsExpander.Footer>
</ui:SettingsExpander>
<ui:SettingsExpander Header="桌面主窗口在任务栏显示图标"
Description="仅控制桌面主窗口在系统任务栏中的图标显示;不会影响设置窗口,设置窗口打开时始终保留独立任务栏图标">
<ui:SettingsExpander.IconSource>
<fi:SymbolIconSource Symbol="Window" />
</ui:SettingsExpander.IconSource>
<ui:SettingsExpander.Footer>
<ToggleSwitch IsChecked="{Binding ShowInTaskbar}" />
</ui:SettingsExpander.Footer>
</ui:SettingsExpander>
</StackPanel>
</ScrollViewer>
</UserControl>