Skip to content

Commit ec90970

Browse files
committed
feat: Add custom window positioning for the music bar
- NoraBar/MainWindow.xaml - Set background to transparent to allow dragging. - Add MouseLeftButtonDown event handler for dragging the window. - NoraBar/MainWindow.xaml.cs - Implement MainWindow_MouseLeftButtonDown to enable DragMove in edit mode. - Handle LocationChanged event to save the new window position to SettingsService. - NoraBar/Services/LocalizationService.cs - Add localized strings for window position settings. - NoraBar/Services/SettingsService.cs - Add properties for CustomWindowX, CustomWindowY, HasCustomPosition, and IsPositionEditMode. - Add SaveWindowPosition and ResetWindowPosition methods. - NoraBar/ViewModels/MainViewModel.cs - Add IsPositionEditMode and HasCustomPosition properties. - Implement ResetPositionCommand. - Add UI text properties for the new settings card. - NoraBar/Views/SettingsWindow.xaml - Add a new settings card for "Window Position" with Reset and Change Position buttons. - NoraBar/Views/SettingsWindow.xaml.cs - Ensure IsPositionEditMode is turned off when the settings window is closed.
1 parent b99d6c1 commit ec90970

7 files changed

Lines changed: 310 additions & 11 deletions

File tree

NoraBar/MainWindow.xaml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,10 @@
2323
Height="2"
2424
Background="#01FFFFFF"
2525
MouseEnter="HudBorder_MouseEnter"
26-
MouseLeave="HudBorder_MouseLeave">
26+
MouseLeave="HudBorder_MouseLeave"
27+
MouseLeftButtonDown="HudBorder_MouseLeftButtonDown"
28+
MouseMove="HudBorder_MouseMove"
29+
MouseLeftButtonUp="HudBorder_MouseLeftButtonUp">
2730
<Border.RenderTransform>
2831
<TranslateTransform x:Name="HudTransform" Y="0"/>
2932
</Border.RenderTransform>

NoraBar/MainWindow.xaml.cs

Lines changed: 132 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,7 @@ public MainWindow()
3737
_viewModel = new MainViewModel();
3838
this.DataContext = _viewModel;
3939

40-
// Position window at the top center of the screen
41-
this.Left = (SystemParameters.PrimaryScreenWidth - this.Width) / 2;
42-
this.Top = 0;
40+
ApplyWindowPosition();
4341

4442
// Initialize visual state to Idle
4543
_viewModel.CurrentState = IslandState.Idle;
@@ -64,6 +62,82 @@ protected override void OnContentRendered(EventArgs e)
6462
}
6563
}
6664

65+
private void ApplyWindowPosition()
66+
{
67+
if (_viewModel.HasCustomPosition)
68+
{
69+
this.Left = _viewModel.WindowLeft;
70+
var deviceTopLeft = DipsToDevice(new Point(_viewModel.WindowLeft, _viewModel.WindowTop));
71+
var deviceSize = DipsToDevice(new Point(this.Width, this.Height));
72+
var rect = new System.Drawing.Rectangle(
73+
(int)deviceTopLeft.X,
74+
(int)deviceTopLeft.Y,
75+
(int)deviceSize.X,
76+
(int)deviceSize.Y);
77+
var screen = System.Windows.Forms.Screen.FromRectangle(rect);
78+
this.Top = GetScreenBoundsInDips(screen).Top;
79+
}
80+
else
81+
{
82+
var screen = System.Windows.Forms.Screen.PrimaryScreen;
83+
if (screen != null)
84+
{
85+
var bounds = GetScreenBoundsInDips(screen);
86+
this.Left = bounds.Left + (bounds.Width - this.Width) / 2;
87+
this.Top = bounds.Top;
88+
}
89+
}
90+
}
91+
92+
private Point DipsToDevice(Point point)
93+
{
94+
var dpi = VisualTreeHelper.GetDpi(this);
95+
return new Point(point.X * dpi.DpiScaleX, point.Y * dpi.DpiScaleY);
96+
}
97+
98+
private Point DeviceToDips(Point point)
99+
{
100+
var dpi = VisualTreeHelper.GetDpi(this);
101+
return new Point(point.X / dpi.DpiScaleX, point.Y / dpi.DpiScaleY);
102+
}
103+
104+
private Rect GetScreenBoundsInDips(System.Windows.Forms.Screen screen)
105+
{
106+
var topLeft = DeviceToDips(new Point(screen.Bounds.Left, screen.Bounds.Top));
107+
var bottomRight = DeviceToDips(new Point(screen.Bounds.Right, screen.Bounds.Bottom));
108+
return new Rect(topLeft, bottomRight);
109+
}
110+
111+
private System.Windows.Forms.Screen GetScreenFromDips(Point point)
112+
{
113+
var devicePoint = DipsToDevice(point);
114+
return System.Windows.Forms.Screen.FromPoint(new System.Drawing.Point((int)devicePoint.X, (int)devicePoint.Y));
115+
}
116+
117+
private Point GetMouseScreenPositionInDips(MouseEventArgs e)
118+
{
119+
return DeviceToDips(PointToScreen(e.GetPosition(this)));
120+
}
121+
122+
private void UpdateEditModeVisuals()
123+
{
124+
if (_viewModel.IsPositionEditMode)
125+
{
126+
HudBorder.Background = new SolidColorBrush(Color.FromArgb(0x40, 0x00, 0xFF, 0x00));
127+
HudBorder.Cursor = Cursors.SizeAll;
128+
_viewModel.CurrentState = IslandState.Music;
129+
}
130+
else
131+
{
132+
HudBorder.Background = new SolidColorBrush(Color.FromArgb(0x01, 0xFF, 0xFF, 0xFF));
133+
HudBorder.Cursor = Cursors.Arrow;
134+
if (!HudBorder.IsMouseOver)
135+
{
136+
_viewModel.CurrentState = IslandState.Idle;
137+
}
138+
}
139+
}
140+
67141
private void InitializeSystemTray()
68142
{
69143
_notifyIcon = new NotifyIcon();
@@ -137,6 +211,14 @@ private void ViewModel_PropertyChanged(object? sender, PropertyChangedEventArgs
137211
{
138212
Dispatcher.Invoke(UpdateLocalizedShellText);
139213
}
214+
else if (e.PropertyName == nameof(MainViewModel.HasCustomPosition))
215+
{
216+
Dispatcher.Invoke(ApplyWindowPosition);
217+
}
218+
else if (e.PropertyName == nameof(MainViewModel.IsPositionEditMode))
219+
{
220+
Dispatcher.Invoke(UpdateEditModeVisuals);
221+
}
140222
}
141223

142224
private void UpdateLocalizedShellText()
@@ -243,16 +325,61 @@ private void AnimateSize(double targetWidth, double targetHeight, bool isClosing
243325

244326
private void HudBorder_MouseEnter(object sender, MouseEventArgs e)
245327
{
246-
if (_isClosingApp) return;
328+
if (_isClosingApp || _viewModel.IsPositionEditMode) return;
247329
_viewModel.CurrentState = IslandState.Music;
248330
}
249331

250332
private void HudBorder_MouseLeave(object sender, MouseEventArgs e)
251333
{
252-
if (_isClosingApp) return;
334+
if (_isClosingApp || _viewModel.IsPositionEditMode) return;
253335
_viewModel.CurrentState = IslandState.Idle;
254336
}
255337

338+
private bool _isDragging = false;
339+
private double _dragStartLeft;
340+
private double _dragStartMouseX;
341+
342+
private void HudBorder_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
343+
{
344+
if (_viewModel.IsPositionEditMode && e.LeftButton == MouseButtonState.Pressed)
345+
{
346+
_isDragging = true;
347+
_dragStartLeft = this.Left;
348+
_dragStartMouseX = GetMouseScreenPositionInDips(e).X;
349+
HudBorder.CaptureMouse();
350+
}
351+
}
352+
353+
private void HudBorder_MouseMove(object sender, MouseEventArgs e)
354+
{
355+
if (_isDragging && _viewModel.IsPositionEditMode)
356+
{
357+
var currentMousePosition = GetMouseScreenPositionInDips(e);
358+
var currentMouseX = currentMousePosition.X;
359+
var diffX = currentMouseX - _dragStartMouseX;
360+
this.Left = _dragStartLeft + diffX;
361+
362+
var screen = GetScreenFromDips(currentMousePosition);
363+
this.Top = GetScreenBoundsInDips(screen).Top;
364+
}
365+
}
366+
367+
private void HudBorder_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
368+
{
369+
if (_isDragging)
370+
{
371+
_isDragging = false;
372+
HudBorder.ReleaseMouseCapture();
373+
374+
var screen = GetScreenFromDips(GetMouseScreenPositionInDips(e));
375+
this.Top = GetScreenBoundsInDips(screen).Top;
376+
377+
_viewModel.WindowLeft = this.Left;
378+
_viewModel.WindowTop = this.Top;
379+
_viewModel.HasCustomPosition = true;
380+
}
381+
}
382+
256383
private void OpenSettings_Click(object sender, RoutedEventArgs e)
257384
{
258385
OpenSettings();

NoraBar/Services/LocalizationService.cs

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,12 @@ public enum LocalizationKey
4949
ThirdPartyTab,
5050
LoadingLyrics,
5151
LyricsNotFound,
52-
LyricsNetworkError
52+
LyricsNetworkError,
53+
WindowPosition,
54+
WindowPositionDescription,
55+
ChangePosition,
56+
ResetPosition,
57+
FinishPositionEdit
5358
}
5459

5560
public static class LocalizationService
@@ -103,7 +108,12 @@ public static class LocalizationService
103108
[LocalizationKey.ThirdPartyTab] = "サードパーティ",
104109
[LocalizationKey.LoadingLyrics] = "歌詞を読み込んでいます...",
105110
[LocalizationKey.LyricsNotFound] = "歌詞が見つかりませんでした",
106-
[LocalizationKey.LyricsNetworkError] = "ネットワークエラーが発生しました"
111+
[LocalizationKey.LyricsNetworkError] = "ネットワークエラーが発生しました",
112+
[LocalizationKey.WindowPosition] = "表示位置",
113+
[LocalizationKey.WindowPositionDescription] = "HUDの表示位置をカスタマイズします。",
114+
[LocalizationKey.ChangePosition] = "表示位置を変更する",
115+
[LocalizationKey.ResetPosition] = "デフォルトに戻す",
116+
[LocalizationKey.FinishPositionEdit] = "位置を確定する"
107117
},
108118
[AppLanguage.English] = new Dictionary<LocalizationKey, string>
109119
{
@@ -151,7 +161,12 @@ public static class LocalizationService
151161
[LocalizationKey.ThirdPartyTab] = "Third-party",
152162
[LocalizationKey.LoadingLyrics] = "Loading lyrics...",
153163
[LocalizationKey.LyricsNotFound] = "Lyrics not found",
154-
[LocalizationKey.LyricsNetworkError] = "Network error occurred"
164+
[LocalizationKey.LyricsNetworkError] = "Network error occurred",
165+
[LocalizationKey.WindowPosition] = "Window Position",
166+
[LocalizationKey.WindowPositionDescription] = "Customize the HUD display position.",
167+
[LocalizationKey.ChangePosition] = "Change Position",
168+
[LocalizationKey.ResetPosition] = "Reset to Default",
169+
[LocalizationKey.FinishPositionEdit] = "Finish Edit"
155170
}
156171
};
157172

NoraBar/Services/SettingsService.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ public class UserSettings
1111
public bool ShowProgressBar { get; set; } = true;
1212
public AppLanguage Language { get; set; } = AppLanguage.Japanese;
1313
public bool ShowLyrics { get; set; } = true;
14+
public bool HasCustomPosition { get; set; } = false;
15+
public double WindowLeft { get; set; } = 0;
16+
public double WindowTop { get; set; } = 0;
1417
}
1518

1619
public static class SettingsService

NoraBar/ViewModels/MainViewModel.cs

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,52 @@ public AppLanguage SelectedLanguage
8383
}
8484
}
8585

86+
private bool _hasCustomPosition;
87+
public bool HasCustomPosition
88+
{
89+
get => _hasCustomPosition;
90+
set
91+
{
92+
if (SetProperty(ref _hasCustomPosition, value))
93+
{
94+
SaveSettings();
95+
}
96+
}
97+
}
98+
99+
private double _windowLeft;
100+
public double WindowLeft
101+
{
102+
get => _windowLeft;
103+
set
104+
{
105+
if (SetProperty(ref _windowLeft, value))
106+
{
107+
SaveSettings();
108+
}
109+
}
110+
}
111+
112+
private double _windowTop;
113+
public double WindowTop
114+
{
115+
get => _windowTop;
116+
set
117+
{
118+
if (SetProperty(ref _windowTop, value))
119+
{
120+
SaveSettings();
121+
}
122+
}
123+
}
124+
125+
private bool _isPositionEditMode;
126+
public bool IsPositionEditMode
127+
{
128+
get => _isPositionEditMode;
129+
set => SetProperty(ref _isPositionEditMode, value);
130+
}
131+
86132
public IReadOnlyList<LanguageOption> AvailableLanguages { get; } =
87133
[
88134
new LanguageOption(AppLanguage.Japanese, LocalizationService.GetText(AppLanguage.Japanese, LocalizationKey.Japanese)),
@@ -160,6 +206,12 @@ public string CurrentPage
160206
public string DownloadText => T(LocalizationKey.Download);
161207
public string ThirdPartyTabTitle => T(LocalizationKey.ThirdPartyTab);
162208

209+
public string WindowPositionText => T(LocalizationKey.WindowPosition);
210+
public string WindowPositionDescriptionText => T(LocalizationKey.WindowPositionDescription);
211+
public string ChangePositionText => T(LocalizationKey.ChangePosition);
212+
public string ResetPositionText => T(LocalizationKey.ResetPosition);
213+
public string FinishPositionEditText => T(LocalizationKey.FinishPositionEdit);
214+
163215
private bool _isLicenseDialogOpen;
164216
public bool IsLicenseDialogOpen
165217
{
@@ -224,6 +276,7 @@ public string LatestReleaseUrl
224276
public ICommand SelectNoraBarLicenseCommand { get; }
225277
public ICommand SelectThirdPartyLicenseCommand { get; }
226278
public ICommand CloseUpdateDialogCommand { get; }
279+
public ICommand ResetPositionCommand { get; }
227280

228281
public MusicViewModel Music { get; } = new MusicViewModel();
229282

@@ -237,6 +290,9 @@ public MainViewModel()
237290
_showProgressBar = settings.ShowProgressBar;
238291
_showLyrics = settings.ShowLyrics;
239292
_selectedLanguage = settings.Language;
293+
_hasCustomPosition = settings.HasCustomPosition;
294+
_windowLeft = settings.WindowLeft;
295+
_windowTop = settings.WindowTop;
240296

241297
SetVariantCommand = new RelayCommand(ExecuteSetVariant);
242298
SetStateCommand = new RelayCommand(ExecuteSetState);
@@ -271,6 +327,11 @@ public MainViewModel()
271327
SelectNoraBarLicenseCommand = new RelayCommand(_ => IsNoraBarLicenseTab = true);
272328
SelectThirdPartyLicenseCommand = new RelayCommand(_ => IsNoraBarLicenseTab = false);
273329
CloseUpdateDialogCommand = new RelayCommand(_ => IsUpdateDialogOpen = false);
330+
ResetPositionCommand = new RelayCommand(_ =>
331+
{
332+
HasCustomPosition = false;
333+
IsPositionEditMode = false;
334+
});
274335
}
275336

276337
private void SaveSettings()
@@ -280,7 +341,10 @@ private void SaveSettings()
280341
Variant = CurrentVariant,
281342
ShowProgressBar = ShowProgressBar,
282343
ShowLyrics = ShowLyrics,
283-
Language = SelectedLanguage
344+
Language = SelectedLanguage,
345+
HasCustomPosition = HasCustomPosition,
346+
WindowLeft = WindowLeft,
347+
WindowTop = WindowTop
284348
});
285349
}
286350

@@ -449,6 +513,11 @@ private void RefreshLocalizedText()
449513
OnPropertyChanged(nameof(DownloadText));
450514
OnPropertyChanged(nameof(ThirdPartyTabTitle));
451515
OnPropertyChanged(nameof(CurrentLicenseText));
516+
OnPropertyChanged(nameof(WindowPositionText));
517+
OnPropertyChanged(nameof(WindowPositionDescriptionText));
518+
OnPropertyChanged(nameof(ChangePositionText));
519+
OnPropertyChanged(nameof(ResetPositionText));
520+
OnPropertyChanged(nameof(FinishPositionEditText));
452521
}
453522
}
454523
}

0 commit comments

Comments
 (0)