mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-20 23:54:26 +08:00
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:
6
.trae/specs/independent-settings-window/checklist.md
Normal file
6
.trae/specs/independent-settings-window/checklist.md
Normal file
@@ -0,0 +1,6 @@
|
||||
- [x] 从桌面、托盘、IPC、组件库进入设置时,都会落到同一个设置窗口
|
||||
- [x] 设置已打开时再次触发设置入口,只会聚焦已有窗口,不会切换成关闭
|
||||
- [x] 设置窗口始终拥有独立任务栏图标,不受“桌面主窗口在任务栏显示图标”开关影响
|
||||
- [x] 点击“回到 Windows”后,只隐藏或最小化桌面主窗口,设置窗口保持可见
|
||||
- [x] 启用滑入滑出动画后,只有主窗口参与动画,设置窗口不参与
|
||||
- [x] 点击设置窗口关闭按钮后会真实关闭;再次打开时创建新的居中窗口
|
||||
78
.trae/specs/independent-settings-window/spec.md
Normal file
78
.trae/specs/independent-settings-window/spec.md
Normal 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** 新窗口按参考屏幕居中显示
|
||||
25
.trae/specs/independent-settings-window/tasks.md
Normal file
25
.trae/specs/independent-settings-window/tasks.md
Normal 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 相关的测试
|
||||
@@ -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 行为
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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; } = [];
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user