715 lines
28 KiB
C#
715 lines
28 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Collections.ObjectModel;
|
|
using System.ComponentModel;
|
|
using System.Diagnostics;
|
|
using System.Linq;
|
|
using System.Security.Principal;
|
|
using System.ServiceProcess;
|
|
using System.Threading.Tasks;
|
|
using System.Windows;
|
|
using System.Windows.Controls;
|
|
using System.Windows.Controls.Primitives;
|
|
using System.Windows.Input;
|
|
using System.Windows.Interop;
|
|
using System.Windows.Media;
|
|
using System.Windows.Threading;
|
|
using Forms = System.Windows.Forms;
|
|
|
|
namespace JJServiceManager
|
|
{
|
|
/// <summary>초기화면</summary>
|
|
public sealed class MainWindow : Window
|
|
{
|
|
private static readonly TimeSpan OpTimeout = TimeSpan.FromSeconds(60);
|
|
|
|
private readonly AppSettings _settings;
|
|
private readonly ObservableCollection<ServiceRow> _rows = new ObservableCollection<ServiceRow>();
|
|
private readonly LogPanel _log;
|
|
private readonly LogWindowManager _logWindows;
|
|
private readonly DispatcherTimer _statusTimer;
|
|
|
|
private readonly ItemsControl _list;
|
|
private readonly CheckBox _chkAll;
|
|
private readonly TextBlock _txtCount, _txtEmpty, _txtLogHidden;
|
|
private readonly ContentControl _ccLog;
|
|
private readonly FrameworkElement _pnlShutdown;
|
|
|
|
private Forms.NotifyIcon _tray;
|
|
private HotkeyManager _hotkey;
|
|
private bool _exiting;
|
|
private bool _refreshing;
|
|
private bool _trayTipShown;
|
|
private bool _modalOpen;
|
|
|
|
public MainWindow(AppSettings settings)
|
|
{
|
|
_settings = settings;
|
|
|
|
Title = "JJServiceManager - 서비스 관리";
|
|
FontFamily = new FontFamily("Malgun Gothic");
|
|
FontSize = 13;
|
|
Background = Brushes2.Make("#F5F7F8");
|
|
MinWidth = 760;
|
|
MinHeight = 520;
|
|
AppIcon.Apply(this);
|
|
RestoreWindowBounds(settings);
|
|
|
|
var root = XamlLoader.LoadElement("MainWindow.xaml");
|
|
Content = root;
|
|
|
|
_list = root.Find<ItemsControl>("icServices");
|
|
_chkAll = root.Find<CheckBox>("chkAll");
|
|
_txtCount = root.Find<TextBlock>("txtCount");
|
|
_txtEmpty = root.Find<TextBlock>("txtEmpty");
|
|
_txtLogHidden = root.Find<TextBlock>("txtLogHidden");
|
|
_ccLog = root.Find<ContentControl>("ccLog");
|
|
_pnlShutdown = root.Find<FrameworkElement>("pnlShutdown");
|
|
|
|
_log = new LogPanel { ShowTimestamp = true };
|
|
_ccLog.Content = _log;
|
|
|
|
_logWindows = new LogWindowManager(() => _settings, SaveSettings);
|
|
|
|
foreach (var e in settings.Services) _rows.Add(new ServiceRow(e));
|
|
_list.ItemsSource = _rows;
|
|
_rows.CollectionChanged += (s, e) => UpdateCounts();
|
|
|
|
// 상단 버튼
|
|
root.Find<Button>("btnAdd").Click += (s, e) => AddServices();
|
|
root.Find<Button>("btnDelete").Click += (s, e) => DeleteChecked();
|
|
root.Find<Button>("btnStartChecked").Click += async (s, e) => await RunChecked(true);
|
|
root.Find<Button>("btnStopChecked").Click += async (s, e) => await RunChecked(false);
|
|
root.Find<Button>("btnShutdown").Click += (s, e) => Shutdown(false);
|
|
root.Find<Button>("btnRestart").Click += (s, e) => Shutdown(true);
|
|
root.Find<Button>("btnSettings").Click += (s, e) => OpenSettings();
|
|
root.Find<Button>("btnExit").Click += (s, e) => ExitApp();
|
|
_chkAll.Click += (s, e) =>
|
|
{
|
|
bool v = _chkAll.IsChecked == true;
|
|
foreach (var r in _rows) r.IsChecked = v;
|
|
};
|
|
|
|
// 서비스 줄의 Start / Stop / Status / Log 버튼
|
|
_list.AddHandler(ButtonBase.ClickEvent, new RoutedEventHandler(OnRowButton));
|
|
// 우클릭: 로그 경로 지정 메뉴
|
|
_list.AddHandler(UIElement.PreviewMouseRightButtonUpEvent, new MouseButtonEventHandler(OnRowRightClick), true);
|
|
|
|
PreviewKeyDown += OnPreviewKeyDown;
|
|
|
|
// 상태 자동 갱신 (2초)
|
|
_statusTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(2) };
|
|
_statusTimer.Tick += async (s, e) => await RefreshStatuses(false);
|
|
|
|
// 창 핸들을 미리 만들어 트레이 시작 상태에서도 단축키가 동작하게 함
|
|
new WindowInteropHelper(this).EnsureHandle();
|
|
_hotkey = new HotkeyManager(new WindowInteropHelper(this).Handle);
|
|
_hotkey.Pressed += ToggleVisibility;
|
|
|
|
CreateTray();
|
|
ApplySettings(startup: true);
|
|
UpdateCounts();
|
|
|
|
Log("프로그램 시작" + (IsAdmin() ? " (관리자 권한)" : ""), LogLevel.Notice);
|
|
if (!IsAdmin()) Log("관리자 권한이 아닙니다. 서비스 Start/Stop이 실패할 수 있습니다.", LogLevel.Warning);
|
|
if (_rows.Count == 0) Log("[추가] 버튼을 눌러 관리할 서비스를 등록하세요.");
|
|
|
|
_ = RefreshStatuses(true);
|
|
_statusTimer.Start();
|
|
|
|
// 컴퓨터 시작시 기동이 켜져 있으면 실행 파일 위치가 바뀌었을 경우를 대비해 다시 등록
|
|
if (_settings.RunAtStartup) Task.Run(() => StartupManager.Enable());
|
|
}
|
|
|
|
// ───────────────────────── 로그 ─────────────────────────
|
|
|
|
private void Log(string text, LogLevel level = LogLevel.Info)
|
|
{
|
|
if (!Dispatcher.CheckAccess())
|
|
{
|
|
Dispatcher.BeginInvoke(new Action(() => Log(text, level)));
|
|
return;
|
|
}
|
|
OperationLogFile.Append(DateTime.Now, text);
|
|
if (_settings.ShowOperationLog) _log.Add(text, level);
|
|
}
|
|
|
|
// ───────────────────────── 서비스 목록 ─────────────────────────
|
|
|
|
private void UpdateCounts()
|
|
{
|
|
int running = _rows.Count(r => r.Status == ServiceControllerStatus.Running);
|
|
_txtCount.Text = _rows.Count == 0 ? "" : $"{_rows.Count}개 · 실행 중 {running}";
|
|
_txtEmpty.Visibility = _rows.Count == 0 ? Visibility.Visible : Visibility.Collapsed;
|
|
}
|
|
|
|
private void SyncEntriesFromRows()
|
|
{
|
|
_settings.Services = _rows.Select(r => r.Entry).ToList();
|
|
SaveSettings();
|
|
_logWindows.ServicesChanged();
|
|
}
|
|
|
|
private void AddServices()
|
|
{
|
|
if (_modalOpen) return;
|
|
var picker = new ServicePickerWindow(_rows.Select(r => r.ServiceName)) { Owner = this };
|
|
bool? ok;
|
|
_modalOpen = true;
|
|
try { ok = picker.ShowDialog(); }
|
|
finally { _modalOpen = false; }
|
|
if (ok != true || picker.Result == null) return;
|
|
|
|
var chosen = picker.Result;
|
|
var chosenNames = new HashSet<string>(chosen.Select(c => c.ServiceName), StringComparer.OrdinalIgnoreCase);
|
|
|
|
// 체크 해제된 기존 항목은 목록에서 뺌
|
|
var removed = _rows.Where(r => !chosenNames.Contains(r.ServiceName)).ToList();
|
|
foreach (var r in removed)
|
|
{
|
|
_rows.Remove(r);
|
|
_logWindows.Close(r.ServiceName);
|
|
}
|
|
|
|
// 새로 체크된 항목 추가 (기존 순서 유지, 새 항목은 뒤에)
|
|
var existing = new HashSet<string>(_rows.Select(r => r.ServiceName), StringComparer.OrdinalIgnoreCase);
|
|
var added = new List<string>();
|
|
foreach (var c in chosen)
|
|
{
|
|
if (existing.Contains(c.ServiceName)) continue;
|
|
var row = new ServiceRow(new ServiceEntry { ServiceName = c.ServiceName, DisplayName = c.DisplayName });
|
|
_rows.Add(row);
|
|
added.Add(c.DisplayName);
|
|
}
|
|
|
|
if (added.Count > 0) Log($"서비스 추가: {string.Join(", ", added)}", LogLevel.Success);
|
|
if (removed.Count > 0) Log($"목록에서 제외: {string.Join(", ", removed.Select(r => r.DisplayName))}");
|
|
if (added.Count == 0 && removed.Count == 0) Log("변경 사항 없음");
|
|
|
|
SyncEntriesFromRows();
|
|
_ = RefreshStatuses(true);
|
|
}
|
|
|
|
private void DeleteChecked()
|
|
{
|
|
var targets = _rows.Where(r => r.IsChecked).ToList();
|
|
if (targets.Count == 0)
|
|
{
|
|
Dialogs.Info(this, "삭제할 서비스를 체크하세요.");
|
|
return;
|
|
}
|
|
var names = string.Join("\n", targets.Select(t => " · " + t.DisplayName));
|
|
if (!Dialogs.Confirm(this, $"아래 {targets.Count}개 서비스를 목록에서 삭제할까요?\n\n{names}\n\n(Windows 서비스 자체는 그대로 유지됩니다)", "삭제"))
|
|
return;
|
|
|
|
foreach (var t in targets)
|
|
{
|
|
_rows.Remove(t);
|
|
_logWindows.Close(t.ServiceName);
|
|
}
|
|
_chkAll.IsChecked = false;
|
|
Log($"목록에서 삭제: {string.Join(", ", targets.Select(t => t.DisplayName))}");
|
|
SyncEntriesFromRows();
|
|
}
|
|
|
|
// ───────────────────────── Start / Stop / Status / Log ─────────────────────────
|
|
|
|
private async void OnRowButton(object sender, RoutedEventArgs e)
|
|
{
|
|
if (e.OriginalSource is not Button b || b.DataContext is not ServiceRow row) return;
|
|
switch (b.Tag as string)
|
|
{
|
|
case "start": await StartService(row); break;
|
|
case "stop": await StopService(row); break;
|
|
case "status": await ShowStatus(row); break;
|
|
case "log": OpenLog(row, b); break;
|
|
}
|
|
}
|
|
|
|
private async Task StartService(ServiceRow row)
|
|
{
|
|
if (row.IsBusy) return;
|
|
row.IsBusy = true;
|
|
Log($"[{row.DisplayName}] 시작 요청");
|
|
var sw = Stopwatch.StartNew();
|
|
try
|
|
{
|
|
await Task.Run(() => ServiceOps.Start(row.ServiceName, OpTimeout));
|
|
Log($"[{row.DisplayName}] 시작 완료 ({sw.Elapsed.TotalSeconds:0.0}초)", LogLevel.Success);
|
|
}
|
|
catch (InfoException ex) { Log($"[{row.DisplayName}] {ex.Message}", LogLevel.Warning); }
|
|
catch (Exception ex) { Log($"[{row.DisplayName}] 시작 실패: {ServiceOps.Describe(ex)}", LogLevel.Error); }
|
|
finally
|
|
{
|
|
row.IsBusy = false;
|
|
await RefreshStatuses(true);
|
|
}
|
|
}
|
|
|
|
private async Task StopService(ServiceRow row)
|
|
{
|
|
if (row.IsBusy) return;
|
|
row.IsBusy = true;
|
|
Log($"[{row.DisplayName}] 중지 요청");
|
|
var sw = Stopwatch.StartNew();
|
|
try
|
|
{
|
|
await Task.Run(() => ServiceOps.Stop(row.ServiceName, OpTimeout));
|
|
Log($"[{row.DisplayName}] 중지 완료 ({sw.Elapsed.TotalSeconds:0.0}초)", LogLevel.Success);
|
|
}
|
|
catch (InfoException ex) { Log($"[{row.DisplayName}] {ex.Message}", LogLevel.Warning); }
|
|
catch (Exception ex) { Log($"[{row.DisplayName}] 중지 실패: {ServiceOps.Describe(ex)}", LogLevel.Error); }
|
|
finally
|
|
{
|
|
row.IsBusy = false;
|
|
await RefreshStatuses(true);
|
|
}
|
|
}
|
|
|
|
private async Task RunChecked(bool start)
|
|
{
|
|
var targets = _rows.Where(r => r.IsChecked).ToList();
|
|
if (targets.Count == 0)
|
|
{
|
|
Dialogs.Info(this, $"{(start ? "시작" : "중지")}할 서비스를 체크하세요.");
|
|
return;
|
|
}
|
|
Log($"선택한 {targets.Count}개 서비스 {(start ? "시작" : "중지")}", LogLevel.Notice);
|
|
foreach (var r in targets)
|
|
{
|
|
if (start) await StartService(r);
|
|
else await StopService(r);
|
|
}
|
|
}
|
|
|
|
private async Task ShowStatus(ServiceRow row)
|
|
{
|
|
try
|
|
{
|
|
var lines = await Task.Run(() => ServiceOps.GetDetails(row.ServiceName));
|
|
Log($"[{row.DisplayName}] 상태 정보", LogLevel.Notice);
|
|
foreach (var l in lines) Log(" " + l);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Log($"[{row.DisplayName}] 상태 조회 실패: {ServiceOps.Describe(ex)}", LogLevel.Error);
|
|
}
|
|
await RefreshStatuses(true);
|
|
}
|
|
|
|
private void OpenLog(ServiceRow row, FrameworkElement anchor)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(row.Entry.LogPath))
|
|
{
|
|
// 경로가 없으면 파일/폴더 중 선택
|
|
ShowLogMenu(row, anchor, includeView: false);
|
|
return;
|
|
}
|
|
_logWindows.Open(row.Entry);
|
|
}
|
|
|
|
private void OnRowRightClick(object sender, MouseButtonEventArgs e)
|
|
{
|
|
var fe = e.OriginalSource as FrameworkElement;
|
|
while (fe != null && fe.DataContext is not ServiceRow) fe = VisualTreeHelper.GetParent(fe) as FrameworkElement;
|
|
if (fe?.DataContext is ServiceRow row)
|
|
{
|
|
ShowLogMenu(row, e.OriginalSource as FrameworkElement ?? fe, includeView: true);
|
|
e.Handled = true;
|
|
}
|
|
}
|
|
|
|
private void ShowLogMenu(ServiceRow row, FrameworkElement anchor, bool includeView)
|
|
{
|
|
var menu = new ContextMenu { PlacementTarget = anchor, Placement = PlacementMode.MousePoint };
|
|
|
|
if (includeView && !string.IsNullOrWhiteSpace(row.Entry.LogPath))
|
|
{
|
|
var view = new MenuItem { Header = "로그 보기", FontWeight = FontWeights.Bold };
|
|
view.Click += (s, e) => _logWindows.Open(row.Entry);
|
|
menu.Items.Add(view);
|
|
menu.Items.Add(new Separator());
|
|
}
|
|
|
|
var file = new MenuItem { Header = "로그 파일 지정…" };
|
|
file.Click += (s, e) => PickLogFile(row);
|
|
menu.Items.Add(file);
|
|
|
|
var folder = new MenuItem { Header = "로그 폴더 지정… (가장 최근 파일 자동 표시)" };
|
|
folder.Click += (s, e) => PickLogFolder(row);
|
|
menu.Items.Add(folder);
|
|
|
|
if (!string.IsNullOrWhiteSpace(row.Entry.LogPath))
|
|
{
|
|
menu.Items.Add(new Separator());
|
|
var cur = new MenuItem { Header = (row.Entry.LogIsFolder ? "현재 폴더: " : "현재 파일: ") + row.Entry.LogPath, IsEnabled = false };
|
|
menu.Items.Add(cur);
|
|
var clear = new MenuItem { Header = "로그 경로 해제" };
|
|
clear.Click += (s, e) =>
|
|
{
|
|
row.Entry.LogPath = "";
|
|
row.Entry.LogIsFolder = false;
|
|
row.LogPathChanged();
|
|
Log($"[{row.DisplayName}] 로그 경로 해제");
|
|
SyncEntriesFromRows();
|
|
_logWindows.EntryPathChanged(row.Entry);
|
|
};
|
|
menu.Items.Add(clear);
|
|
}
|
|
|
|
menu.IsOpen = true;
|
|
}
|
|
|
|
private void PickLogFile(ServiceRow row)
|
|
{
|
|
var dlg = new Microsoft.Win32.OpenFileDialog
|
|
{
|
|
Title = $"{row.DisplayName} - 로그 파일 선택",
|
|
Filter = "로그 파일 (*.log;*.txt)|*.log;*.txt|모든 파일 (*.*)|*.*",
|
|
CheckFileExists = true
|
|
};
|
|
if (!string.IsNullOrWhiteSpace(row.Entry.LogPath))
|
|
{
|
|
try
|
|
{
|
|
dlg.InitialDirectory = row.Entry.LogIsFolder
|
|
? row.Entry.LogPath
|
|
: System.IO.Path.GetDirectoryName(row.Entry.LogPath);
|
|
}
|
|
catch { }
|
|
}
|
|
if (dlg.ShowDialog(this) != true) return;
|
|
|
|
row.Entry.LogPath = dlg.FileName;
|
|
row.Entry.LogIsFolder = false;
|
|
row.LogPathChanged();
|
|
Log($"[{row.DisplayName}] 로그 파일 지정: {dlg.FileName}");
|
|
SyncEntriesFromRows();
|
|
_logWindows.EntryPathChanged(row.Entry);
|
|
_logWindows.Open(row.Entry);
|
|
}
|
|
|
|
private void PickLogFolder(ServiceRow row)
|
|
{
|
|
var dlg = new Microsoft.Win32.OpenFolderDialog
|
|
{
|
|
Title = $"{row.DisplayName} - 로그 폴더 선택 (가장 최근 파일을 자동으로 표시)"
|
|
};
|
|
if (!string.IsNullOrWhiteSpace(row.Entry.LogPath))
|
|
{
|
|
try
|
|
{
|
|
dlg.InitialDirectory = row.Entry.LogIsFolder
|
|
? row.Entry.LogPath
|
|
: System.IO.Path.GetDirectoryName(row.Entry.LogPath);
|
|
}
|
|
catch { }
|
|
}
|
|
if (dlg.ShowDialog(this) != true) return;
|
|
|
|
row.Entry.LogPath = dlg.FolderName;
|
|
row.Entry.LogIsFolder = true;
|
|
row.LogPathChanged();
|
|
Log($"[{row.DisplayName}] 로그 폴더 지정: {dlg.FolderName}");
|
|
SyncEntriesFromRows();
|
|
_logWindows.EntryPathChanged(row.Entry);
|
|
_logWindows.Open(row.Entry);
|
|
}
|
|
|
|
// ───────────────────────── 상태 자동 갱신 ─────────────────────────
|
|
|
|
private async Task RefreshStatuses(bool force)
|
|
{
|
|
if (_refreshing && !force) return;
|
|
_refreshing = true;
|
|
try
|
|
{
|
|
var names = _rows.Select(r => r.ServiceName).ToList();
|
|
var result = await Task.Run(() => names.ToDictionary(n => n, n => ServiceOps.GetStatus(n), StringComparer.OrdinalIgnoreCase));
|
|
|
|
foreach (var r in _rows)
|
|
{
|
|
if (!result.TryGetValue(r.ServiceName, out var st)) continue;
|
|
if (r.StatusKnown && r.Status != st && !r.IsBusy)
|
|
{
|
|
Log($"[{r.DisplayName}] 상태 변경: {ServiceOps.StatusText(r.Status)} → {ServiceOps.StatusText(st)}",
|
|
st == ServiceControllerStatus.Running ? LogLevel.Success
|
|
: st == ServiceControllerStatus.Stopped ? LogLevel.Warning : LogLevel.Info);
|
|
}
|
|
r.Status = st;
|
|
}
|
|
UpdateCounts();
|
|
}
|
|
catch
|
|
{
|
|
// 다음 주기에 다시 시도
|
|
}
|
|
finally
|
|
{
|
|
_refreshing = false;
|
|
}
|
|
}
|
|
|
|
// ───────────────────────── Windows 종료 ─────────────────────────
|
|
|
|
private void Shutdown(bool restart)
|
|
{
|
|
var what = restart ? "재시작" : "강제 종료";
|
|
if (_modalOpen) return;
|
|
bool confirmed;
|
|
_modalOpen = true;
|
|
try
|
|
{
|
|
confirmed = CodeConfirmWindow.Ask(this, "Windows " + what,
|
|
$"Windows를 지금 {what}합니다.\n저장하지 않은 작업은 모두 사라집니다.",
|
|
restart ? "Windows 재시작" : "Windows 강제종료");
|
|
}
|
|
finally { _modalOpen = false; }
|
|
if (!confirmed)
|
|
{
|
|
Log($"Windows {what} 취소");
|
|
return;
|
|
}
|
|
Log($"Windows {what} 실행", LogLevel.Warning);
|
|
try
|
|
{
|
|
if (restart) PowerOps.ForceRestart(); else PowerOps.ForceShutdown();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Log($"Windows {what} 실패: {ex.Message}", LogLevel.Error);
|
|
}
|
|
}
|
|
|
|
// ───────────────────────── 설정 ─────────────────────────
|
|
|
|
private void OpenSettings()
|
|
{
|
|
ShowFromTray();
|
|
if (_modalOpen) return;
|
|
bool wasStartup = _settings.RunAtStartup;
|
|
var dlg = new SettingsWindow(_settings) { Owner = this };
|
|
|
|
// 설정 창에서 단축키를 누를 때 전역 단축키가 먼저 받지 않도록 잠시 해제
|
|
_hotkey.Unregister();
|
|
bool? dialogOk;
|
|
_modalOpen = true;
|
|
try { dialogOk = dlg.ShowDialog(); }
|
|
finally { _modalOpen = false; }
|
|
if (dialogOk != true)
|
|
{
|
|
ApplyHotkey(quiet: true);
|
|
return;
|
|
}
|
|
|
|
SaveSettings();
|
|
Log("설정 저장");
|
|
|
|
if (wasStartup != _settings.RunAtStartup || (_settings.RunAtStartup && !StartupManager.IsEnabled()))
|
|
{
|
|
var (ok, msg) = _settings.RunAtStartup ? StartupManager.Enable() : StartupManager.Disable();
|
|
if (ok) Log(_settings.RunAtStartup ? "컴퓨터 시작시 기동: 켬 (작업 스케줄러 등록)" : "컴퓨터 시작시 기동: 끔", LogLevel.Success);
|
|
else Log("컴퓨터 시작시 기동 설정 실패: " + msg, LogLevel.Error);
|
|
}
|
|
ApplySettings(startup: false);
|
|
}
|
|
|
|
private void ApplySettings(bool startup)
|
|
{
|
|
// Windows 종료 버튼 표시
|
|
_pnlShutdown.Visibility = _settings.ShowShutdownButtons ? Visibility.Visible : Visibility.Collapsed;
|
|
|
|
// 작업 로그 표시
|
|
_ccLog.Visibility = _settings.ShowOperationLog ? Visibility.Visible : Visibility.Collapsed;
|
|
_txtLogHidden.Visibility = _settings.ShowOperationLog ? Visibility.Collapsed : Visibility.Visible;
|
|
|
|
ApplyHotkey(quiet: startup);
|
|
}
|
|
|
|
private void ApplyHotkey(bool quiet)
|
|
{
|
|
var mods = (ModifierKeys)_settings.HotkeyModifiers;
|
|
var key = (Key)_settings.HotkeyKey;
|
|
if (_settings.HotkeyEnabled)
|
|
{
|
|
if (_hotkey.Register(mods, key))
|
|
{
|
|
if (!quiet) Log($"단축키 등록: {HotkeyManager.Format(mods, key)}");
|
|
}
|
|
else
|
|
{
|
|
var prev = _hotkey.Active;
|
|
Log($"단축키 {HotkeyManager.Format(mods, key)} 등록 실패 (다른 프로그램이 사용 중일 수 있습니다)"
|
|
+ (prev.HasValue ? $" → 이전 단축키 {HotkeyManager.Format(prev.Value.mods, prev.Value.key)} 유지" : ""),
|
|
LogLevel.Warning);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
_hotkey.Unregister();
|
|
}
|
|
|
|
if (_tray != null)
|
|
_tray.Text = _settings.HotkeyEnabled
|
|
? Trim63($"JJServiceManager ({HotkeyManager.Format(mods, key)})")
|
|
: "JJServiceManager";
|
|
}
|
|
|
|
private static string Trim63(string s) => s.Length > 63 ? s.Substring(0, 63) : s;
|
|
|
|
private void SaveSettings()
|
|
{
|
|
if (WindowState == WindowState.Normal && IsLoaded)
|
|
{
|
|
_settings.WindowLeft = Left;
|
|
_settings.WindowTop = Top;
|
|
_settings.WindowWidth = Width;
|
|
_settings.WindowHeight = Height;
|
|
}
|
|
SettingsStore.Save(_settings);
|
|
}
|
|
|
|
private void RestoreWindowBounds(AppSettings s)
|
|
{
|
|
Width = s.WindowWidth > 300 ? s.WindowWidth : 1000;
|
|
Height = s.WindowHeight > 300 ? s.WindowHeight : 720;
|
|
bool valid = !double.IsNaN(s.WindowLeft) && !double.IsNaN(s.WindowTop)
|
|
&& s.WindowLeft >= SystemParameters.VirtualScreenLeft - 50
|
|
&& s.WindowTop >= SystemParameters.VirtualScreenTop - 50
|
|
&& s.WindowLeft < SystemParameters.VirtualScreenLeft + SystemParameters.VirtualScreenWidth - 100
|
|
&& s.WindowTop < SystemParameters.VirtualScreenTop + SystemParameters.VirtualScreenHeight - 100;
|
|
if (valid)
|
|
{
|
|
WindowStartupLocation = WindowStartupLocation.Manual;
|
|
Left = s.WindowLeft;
|
|
Top = s.WindowTop;
|
|
}
|
|
else
|
|
{
|
|
WindowStartupLocation = WindowStartupLocation.CenterScreen;
|
|
}
|
|
}
|
|
|
|
// ───────────────────────── 트레이 / Esc / 종료 ─────────────────────────
|
|
|
|
private void CreateTray()
|
|
{
|
|
var menu = new Forms.ContextMenuStrip();
|
|
menu.Items.Add("열기", null, (s, e) => Dispatcher.Invoke(ShowFromTray));
|
|
menu.Items.Add("설정", null, (s, e) => { if (!_modalOpen) Dispatcher.Invoke(OpenSettings); else Dispatcher.Invoke(ShowFromTray); });
|
|
menu.Items.Add(new Forms.ToolStripSeparator());
|
|
menu.Items.Add("완전 종료", null, (s, e) => Dispatcher.Invoke(ExitApp));
|
|
|
|
_tray = new Forms.NotifyIcon
|
|
{
|
|
Icon = AppIcon.Icon,
|
|
Text = "JJServiceManager",
|
|
ContextMenuStrip = menu,
|
|
Visible = true
|
|
};
|
|
_tray.MouseClick += (s, e) =>
|
|
{
|
|
if (e.Button == Forms.MouseButtons.Left) Dispatcher.Invoke(ShowFromTray);
|
|
};
|
|
}
|
|
|
|
public void ShowFromTray()
|
|
{
|
|
if (!IsVisible) Show();
|
|
if (WindowState == WindowState.Minimized) WindowState = WindowState.Normal;
|
|
Activate();
|
|
Topmost = true; // 다른 창 뒤에 숨지 않도록 잠깐 맨 앞으로
|
|
Topmost = false;
|
|
Focus();
|
|
}
|
|
|
|
private void HideToTray()
|
|
{
|
|
SaveSettings();
|
|
Hide();
|
|
if (!_trayTipShown && _tray != null)
|
|
{
|
|
_trayTipShown = true;
|
|
var hk = _settings.HotkeyEnabled ? $" 단축키 {HotkeyManager.Format((ModifierKeys)_settings.HotkeyModifiers, (Key)_settings.HotkeyKey)}로 다시 열 수 있습니다." : "";
|
|
_tray.ShowBalloonTip(2500, "JJServiceManager", "트레이에서 계속 실행 중입니다." + hk, Forms.ToolTipIcon.Info);
|
|
}
|
|
}
|
|
|
|
private void ToggleVisibility()
|
|
{
|
|
if (IsVisible && IsActive && WindowState != WindowState.Minimized) HideToTray();
|
|
else ShowFromTray();
|
|
}
|
|
|
|
private void OnPreviewKeyDown(object sender, KeyEventArgs e)
|
|
{
|
|
if (e.Key != Key.Escape) return;
|
|
e.Handled = true;
|
|
if (_settings.EscAction == EscAction.Exit) ExitApp();
|
|
else HideToTray();
|
|
}
|
|
|
|
protected override void OnClosing(CancelEventArgs e)
|
|
{
|
|
if (!_exiting)
|
|
{
|
|
// 창의 X 버튼도 Esc 설정을 따름
|
|
if (_settings.EscAction == EscAction.Tray)
|
|
{
|
|
e.Cancel = true;
|
|
HideToTray();
|
|
return;
|
|
}
|
|
_exiting = true;
|
|
Cleanup();
|
|
}
|
|
base.OnClosing(e);
|
|
}
|
|
|
|
protected override void OnClosed(EventArgs e)
|
|
{
|
|
base.OnClosed(e);
|
|
Application.Current.Shutdown();
|
|
}
|
|
|
|
/// <summary>Windows 로그오프/종료 시 정리</summary>
|
|
public void OnSessionEnding()
|
|
{
|
|
if (_exiting) return;
|
|
_exiting = true;
|
|
OperationLogFile.Append(DateTime.Now, "Windows 세션 종료");
|
|
Cleanup();
|
|
}
|
|
|
|
public void ExitApp()
|
|
{
|
|
if (_exiting) return;
|
|
_exiting = true;
|
|
Cleanup();
|
|
Close();
|
|
Application.Current.Shutdown();
|
|
}
|
|
|
|
private void Cleanup()
|
|
{
|
|
OperationLogFile.Append(DateTime.Now, "프로그램 종료");
|
|
_statusTimer.Stop();
|
|
SaveSettings();
|
|
_logWindows.CloseAll();
|
|
_hotkey?.Dispose();
|
|
if (_tray != null)
|
|
{
|
|
_tray.Visible = false;
|
|
_tray.Dispose();
|
|
_tray = null;
|
|
}
|
|
}
|
|
|
|
private static bool IsAdmin()
|
|
{
|
|
try { return new WindowsPrincipal(WindowsIdentity.GetCurrent()).IsInRole(WindowsBuiltInRole.Administrator); }
|
|
catch { return false; }
|
|
}
|
|
}
|
|
}
|