[v2.0.0] Phase L3-7 다중 디스플레이 지원
LauncherWindow.Animations.cs:
- CenterOnScreen(): SystemParameters.WorkArea(주 모니터 고정) → 마우스 커서 위치 모니터 기반으로 변경
- GetCurrentMonitorWorkArea() 신규: Screen.FromPoint(Cursor.Position)으로 현재 모니터 탐색
· PresentationSource.TransformFromDevice로 물리 픽셀 → WPF DIP 정확 변환
· PresentationSource 미사용 가능 시 SystemParameters.WorkArea 폴백
- using AxCopilot.Services 추가
AppSettings.cs:
- LauncherSettings에 MonitorDockPositions 딕셔너리 추가
· key=모니터 디바이스 이름 (\.\DISPLAY1 등), value=[Left, Top]
· JSON 직렬화 지원
App.Settings.cs:
- ToggleDockBar(): 독 바 위치 변경 시 per-monitor 위치 저장 (GetMonitorDeviceNameAt)
- 독 바 복원 시 IsDockPositionOnAnyMonitor로 유효성 검사
· 연결 끊긴 모니터 위치면 중앙 하단(-1,-1)으로 자동 초기화
- GetMonitorDeviceNameAt(): WPF DIP → 물리 픽셀 변환 후 모니터 디바이스명 반환
- IsDockPositionOnAnyMonitor(): Screen.AllScreens 범위 검사
docs/LAUNCHER_ROADMAP.md: L3-7 ✅ 완료 표시
효과:
- 듀얼 모니터에서 핫키 → 마우스가 있는 모니터에 런처 표시
- 독 바 드래그 시 해당 모니터 이름으로 위치 기억
- 모니터 분리 후 재실행 시 독 바 위치 자동 복원
빌드: 경고 0, 오류 0
This commit is contained in:
@@ -101,7 +101,7 @@
|
||||
| ✅ L3-4 | **파라미터 퀵링크** | `jira {티켓번호}` → URL 템플릿 변수 치환 (사내 JIRA/Confluence 등) | 중간 | → Agent 18-4 |
|
||||
| ✅ L3-5 | **파일 태그 시스템** | 파일에 사용자 태그 부여, `tag` 프리픽스로 태그 기반 검색. `file_tags.json` 로컬 저장 | 중간 | — |
|
||||
| L3-6 | **오프라인 AI (로컬 SLM)** | ONNX Runtime + phi-3, 서버 없이 번역/요약 | 낮음 | → Agent 18-5 |
|
||||
| L3-7 | **다중 디스플레이** | 모니터별 런처/독 바 위치 기억 | 낮음 | — |
|
||||
| ✅ L3-7 | **다중 디스플레이** | 마우스 커서 위치 모니터에 런처 표시, 독 바 per-monitor 위치 저장·유효성 검증 | 낮음 | — |
|
||||
| L3-8 | **알림 센터 통합** | Windows 알림과 연동 | 낮음 | — |
|
||||
| L3-9 | **런처 미니 위젯** | 날씨/일정/할일을 런처 하단에 카드형으로 표시. 로컬 데이터 기반 | 낮음 | — |
|
||||
|
||||
|
||||
@@ -154,23 +154,87 @@ public partial class App
|
||||
var launcher = _settings?.Settings.Launcher;
|
||||
var dockItems = launcher?.DockBarItems ?? new() { "launcher", "clipboard", "capture", "agent", "clock", "cpu" };
|
||||
_dockBar.BuildFromSettings(dockItems);
|
||||
|
||||
// Phase L3-7: 독 바 위치 변경 시 모니터별로 저장
|
||||
_dockBar.OnPositionChanged = (left, top) =>
|
||||
{
|
||||
if (_settings != null)
|
||||
{
|
||||
_settings.Settings.Launcher.DockBarLeft = left;
|
||||
_settings.Settings.Launcher.DockBarTop = top;
|
||||
_settings.Save();
|
||||
}
|
||||
if (_settings == null) return;
|
||||
var lnch = _settings.Settings.Launcher;
|
||||
lnch.DockBarLeft = left;
|
||||
lnch.DockBarTop = top;
|
||||
|
||||
// 현재 독 바가 속한 모니터 디바이스 이름으로 위치 기억
|
||||
var deviceName = GetMonitorDeviceNameAt(_dockBar, left, top);
|
||||
if (deviceName != null)
|
||||
lnch.MonitorDockPositions[deviceName] = new[] { left, top };
|
||||
|
||||
_settings.Save();
|
||||
};
|
||||
|
||||
_dockBar.Show();
|
||||
|
||||
// Phase L3-7: 저장 위치 유효성 검사 — 연결이 끊긴 모니터면 중앙 하단으로 리셋
|
||||
var savedLeft = launcher?.DockBarLeft ?? -1;
|
||||
var savedTop = launcher?.DockBarTop ?? -1;
|
||||
if (!IsDockPositionOnAnyMonitor(savedLeft, savedTop))
|
||||
{
|
||||
LogService.Info("[MultiMonitor] 독 바 저장 위치가 연결된 모니터 밖 → 중앙 하단으로 초기화");
|
||||
savedLeft = -1;
|
||||
savedTop = -1;
|
||||
}
|
||||
|
||||
_dockBar.ApplySettings(
|
||||
launcher?.DockBarOpacity ?? 0.92,
|
||||
launcher?.DockBarLeft ?? -1,
|
||||
launcher?.DockBarTop ?? -1,
|
||||
savedLeft,
|
||||
savedTop,
|
||||
launcher?.DockBarRainbowGlow ?? false);
|
||||
}
|
||||
|
||||
// ─── Phase L3-7: 다중 모니터 헬퍼 ─────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// WPF 논리 좌표를 물리적 좌표로 변환하여 해당 모니터의 디바이스 이름을 반환합니다.
|
||||
/// </summary>
|
||||
private static string? GetMonitorDeviceNameAt(
|
||||
System.Windows.Media.Visual? visual, double wpfLeft, double wpfTop)
|
||||
{
|
||||
try
|
||||
{
|
||||
System.Drawing.Point physPt;
|
||||
var src = visual != null ? PresentationSource.FromVisual(visual) : null;
|
||||
if (src?.CompositionTarget != null)
|
||||
{
|
||||
var m = src.CompositionTarget.TransformToDevice; // logical→physical
|
||||
var pt = m.Transform(new System.Windows.Point(wpfLeft + 10, wpfTop + 10));
|
||||
physPt = new System.Drawing.Point((int)pt.X, (int)pt.Y);
|
||||
}
|
||||
else
|
||||
{
|
||||
physPt = new System.Drawing.Point((int)wpfLeft + 10, (int)wpfTop + 10);
|
||||
}
|
||||
return System.Windows.Forms.Screen.FromPoint(physPt).DeviceName;
|
||||
}
|
||||
catch { return null; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 저장된 독 바 위치(WPF 논리 좌표)가 현재 연결된 모니터 중 하나에 있는지 검사합니다.
|
||||
/// 모니터가 분리되거나 해상도가 변경되어 위치가 화면 밖에 있으면 false를 반환합니다.
|
||||
/// </summary>
|
||||
private static bool IsDockPositionOnAnyMonitor(double left, double top)
|
||||
{
|
||||
if (left < 0 || top < 0) return false; // -1 기본값은 항상 재계산 대상
|
||||
try
|
||||
{
|
||||
// 참고: WPF 논리 좌표와 물리적 픽셀의 차이는 DPI에 따라 다르나
|
||||
// "화면 밖인지" 판단에는 근사 비교로 충분합니다.
|
||||
return System.Windows.Forms.Screen.AllScreens
|
||||
.Any(s => left >= s.Bounds.Left && left < s.Bounds.Right
|
||||
&& top >= s.Bounds.Top && top < s.Bounds.Bottom);
|
||||
}
|
||||
catch { return true; } // 오류 시 보수적으로 true (위치 유지)
|
||||
}
|
||||
|
||||
/// <summary>독 바를 현재 설정으로 즉시 새로고침합니다.</summary>
|
||||
public void RefreshDockBar()
|
||||
{
|
||||
|
||||
@@ -247,6 +247,14 @@ public class LauncherSettings
|
||||
/// <summary>독 바 마지막 위치 Y. -1이면 하단.</summary>
|
||||
[JsonPropertyName("dockBarTop")]
|
||||
public double DockBarTop { get; set; } = -1;
|
||||
|
||||
/// <summary>
|
||||
/// Phase L3-7: 모니터별 독 바 위치.
|
||||
/// key = 모니터 디바이스 이름 (예: \\.\DISPLAY1), value = [WPF Left, WPF Top].
|
||||
/// 모니터가 재연결되면 해당 모니터의 저장 위치로 자동 복원됩니다.
|
||||
/// </summary>
|
||||
[JsonPropertyName("monitorDockPositions")]
|
||||
public Dictionary<string, double[]> MonitorDockPositions { get; set; } = new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Animation;
|
||||
using AxCopilot.Services;
|
||||
|
||||
namespace AxCopilot.Views;
|
||||
|
||||
@@ -107,23 +108,59 @@ public partial class LauncherWindow
|
||||
return a;
|
||||
}
|
||||
|
||||
// ─── 화면 배치 ────────────────────────────────────────────────────────────
|
||||
// ─── 화면 배치 (Phase L3-7: 다중 디스플레이 지원) ────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// 마우스 커서가 위치한 모니터 중앙에 런처를 배치합니다.
|
||||
/// 듀얼 모니터 환경에서도 핫키를 누른 순간 마우스가 있는 화면에 나타납니다.
|
||||
/// </summary>
|
||||
private void CenterOnScreen()
|
||||
{
|
||||
var screen = SystemParameters.WorkArea;
|
||||
var area = GetCurrentMonitorWorkArea();
|
||||
// ActualHeight/ActualWidth는 첫 Show() 전 레이아웃 패스 이전에 0일 수 있음 → 기본값으로 보호
|
||||
var w = ActualWidth > 0 ? ActualWidth : 640;
|
||||
var h = ActualHeight > 0 ? ActualHeight : 80;
|
||||
Left = (screen.Width - w) / 2 + screen.Left;
|
||||
Left = (area.Width - w) / 2 + area.Left;
|
||||
Top = _vm.WindowPosition switch
|
||||
{
|
||||
"center" => (screen.Height - h) / 2 + screen.Top,
|
||||
"bottom" => screen.Height * 0.75 + screen.Top,
|
||||
_ => screen.Height * 0.2 + screen.Top, // "center-top" (기본)
|
||||
"center" => (area.Height - h) / 2 + area.Top,
|
||||
"bottom" => area.Height * 0.75 + area.Top,
|
||||
_ => area.Height * 0.2 + area.Top, // "center-top" (기본)
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 마우스 커서가 있는 모니터의 작업 영역을 WPF 논리 좌표(DIP)로 반환합니다.
|
||||
/// PresentationSource를 통해 DPI 스케일을 정확하게 보정합니다.
|
||||
/// </summary>
|
||||
private System.Windows.Rect GetCurrentMonitorWorkArea()
|
||||
{
|
||||
try
|
||||
{
|
||||
// 마우스 커서 물리적 좌표 → 해당 모니터 탐색
|
||||
var cursorPos = System.Windows.Forms.Cursor.Position;
|
||||
var screen = System.Windows.Forms.Screen.FromPoint(cursorPos);
|
||||
var wa = screen.WorkingArea; // 물리적 픽셀 단위
|
||||
|
||||
// 물리적 픽셀 → WPF DIP 변환 (DPI 보정)
|
||||
var src = PresentationSource.FromVisual(this);
|
||||
if (src?.CompositionTarget != null)
|
||||
{
|
||||
var m = src.CompositionTarget.TransformFromDevice; // physical→logical
|
||||
var tl = m.Transform(new System.Windows.Point(wa.Left, wa.Top));
|
||||
var br = m.Transform(new System.Windows.Point(wa.Right, wa.Bottom));
|
||||
return new System.Windows.Rect(tl, br);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogService.Warn($"[MultiMonitor] 모니터 감지 실패, 기본 모니터 사용: {ex.Message}");
|
||||
}
|
||||
|
||||
// 폴백: WPF 기본 주 모니터
|
||||
return SystemParameters.WorkArea;
|
||||
}
|
||||
|
||||
// ─── 등장 애니메이션 ──────────────────────────────────────────────────────
|
||||
|
||||
private void AnimateIn()
|
||||
|
||||
Reference in New Issue
Block a user