초기 커밋: SheetMe 서식생성기 (P0~P5 완료 상태)
레거시 서식생성기(VB.NET WinForms) 대체용 C#/.NET 10 WPF 디자이너. 기준선: 실DB 활성 디자인 1,271건 왕복 의미론 diff 0 / 예외 0, 단위 테스트 49/49. 이 커밋에 함께 포함된 자격증명 분리: - appsettings.json 을 __HOST__/__PASSWORD__ 플레이스홀더로 전환 - 실접속 정보는 appsettings.Development.json 으로 분리(.gitignore 제외, csproj Debug 조건부 복사라 Release 산출물에 실리지 않음) - ConfigLoader 를 환경변수 > Development > appsettings 순 레이어링으로 변경, 미치환 플레이스홀더는 '미설정'으로 간주 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,338 @@
|
||||
using System.Globalization;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Shapes;
|
||||
|
||||
namespace SheetMe.Designer.Views;
|
||||
|
||||
/// <summary>
|
||||
/// Figma 식 색상 피커([200]SheetMe ColorPickerWindow 이식) — 채도/명도(SV) 사각형 + 색상(Hue) 슬라이더
|
||||
/// + HEX·RGB 입력 + 프리셋/최근색. 정적 <see cref="Pick"/> 호출: 확정 시 "#RRGGBB" 반환, 취소 시 null.
|
||||
/// 셸 색은 앱 테마 토큰(B.*) — SV/Hue/스와치는 색상 원본 그대로.
|
||||
/// </summary>
|
||||
public sealed class ColorPickerWindow : Window
|
||||
{
|
||||
#region Member Fields
|
||||
private static readonly List<string> recent = new();
|
||||
|
||||
private double hue360; // H 0..360
|
||||
private double sat; // S 0..1
|
||||
private double val; // V 0..1
|
||||
private bool syncing; // 텍스트박스 ↔ 상태 갱신 재진입 방지
|
||||
|
||||
private readonly Canvas svArea = new() { Width = 248, Height = 168, ClipToBounds = true, Cursor = Cursors.Cross };
|
||||
private readonly Rectangle svHue = new() { Width = 248, Height = 168 };
|
||||
private readonly Ellipse svThumb = new() { Width = 14, Height = 14, Stroke = Brushes.White, StrokeThickness = 2, IsHitTestVisible = false };
|
||||
private readonly Canvas hueArea = new() { Width = 248, Height = 16, ClipToBounds = true, Cursor = Cursors.Cross };
|
||||
private readonly Border hueThumb = new() { Width = 6, Height = 20, BorderBrush = Brushes.White, BorderThickness = new Thickness(2), CornerRadius = new CornerRadius(2), IsHitTestVisible = false };
|
||||
private readonly Border preview = new() { Width = 40, Height = 40, CornerRadius = new CornerRadius(6), BorderThickness = new Thickness(1) };
|
||||
private readonly TextBox hexBox = new() { Width = 92, VerticalContentAlignment = VerticalAlignment.Center };
|
||||
private readonly TextBox rBox = new() { Width = 46, VerticalContentAlignment = VerticalAlignment.Center };
|
||||
private readonly TextBox gBox = new() { Width = 46, VerticalContentAlignment = VerticalAlignment.Center };
|
||||
private readonly TextBox bBox = new() { Width = 46, VerticalContentAlignment = VerticalAlignment.Center };
|
||||
#endregion
|
||||
|
||||
#region Properties
|
||||
/// <summary>확정된 "#RRGGBB"(취소 시 null)</summary>
|
||||
public string? Result { get; private set; }
|
||||
#endregion
|
||||
|
||||
#region Methods
|
||||
/// <summary>색상 선택 대화상자 — 확정 시 "#RRGGBB", 취소 시 null. initialHex 로 초기색 지정.</summary>
|
||||
public static string? Pick(Window? owner, string? initialHex)
|
||||
{
|
||||
var window = new ColorPickerWindow(initialHex);
|
||||
if (owner is not null)
|
||||
{
|
||||
window.Owner = owner;
|
||||
}
|
||||
return window.ShowDialog() == true ? window.Result : null;
|
||||
}
|
||||
|
||||
private ColorPickerWindow(string? initialHex)
|
||||
{
|
||||
Title = "색상 선택";
|
||||
Width = 300;
|
||||
SizeToContent = SizeToContent.Height;
|
||||
WindowStartupLocation = WindowStartupLocation.CenterOwner;
|
||||
ResizeMode = ResizeMode.NoResize;
|
||||
ShowInTaskbar = false;
|
||||
FontSize = 12.5;
|
||||
|
||||
var line = FindBrush("B.Line");
|
||||
var muted = FindBrush("B.Muted");
|
||||
preview.BorderBrush = line;
|
||||
|
||||
var initial = ParseHex(initialHex) ?? Color.FromRgb(0x2F, 0x6D, 0xF0);
|
||||
(hue360, sat, val) = RgbToHsv(initial.R, initial.G, initial.B);
|
||||
|
||||
var root = new StackPanel { Margin = new Thickness(14) };
|
||||
|
||||
// ── SV 사각형(흰색→hue 가로 + 투명→검정 세로) ──
|
||||
svArea.Children.Add(svHue);
|
||||
svArea.Children.Add(new Rectangle
|
||||
{
|
||||
Width = 248, Height = 168,
|
||||
Fill = new LinearGradientBrush(Color.FromArgb(255, 255, 255, 255), Color.FromArgb(0, 255, 255, 255), new Point(0, 0), new Point(1, 0)),
|
||||
});
|
||||
svArea.Children.Add(new Rectangle
|
||||
{
|
||||
Width = 248, Height = 168,
|
||||
Fill = new LinearGradientBrush(Color.FromArgb(0, 0, 0, 0), Color.FromArgb(255, 0, 0, 0), new Point(0, 0), new Point(0, 1)),
|
||||
});
|
||||
svArea.Children.Add(svThumb);
|
||||
svArea.MouseLeftButtonDown += (_, e) => { svArea.CaptureMouse(); UpdateSvFrom(e.GetPosition(svArea)); };
|
||||
svArea.MouseMove += (_, e) => { if (e.LeftButton == MouseButtonState.Pressed && svArea.IsMouseCaptured) UpdateSvFrom(e.GetPosition(svArea)); };
|
||||
svArea.MouseLeftButtonUp += (_, _) => svArea.ReleaseMouseCapture();
|
||||
root.Children.Add(new Border { Child = svArea, CornerRadius = new CornerRadius(6), ClipToBounds = true, Margin = new Thickness(0, 0, 0, 10) });
|
||||
|
||||
// ── Hue 슬라이더(무지개) ──
|
||||
hueArea.Children.Add(new Rectangle
|
||||
{
|
||||
Width = 248, Height = 16,
|
||||
Fill = new LinearGradientBrush(new GradientStopCollection
|
||||
{
|
||||
new(Color.FromRgb(255, 0, 0), 0), new(Color.FromRgb(255, 255, 0), 1 / 6.0), new(Color.FromRgb(0, 255, 0), 2 / 6.0),
|
||||
new(Color.FromRgb(0, 255, 255), 3 / 6.0), new(Color.FromRgb(0, 0, 255), 4 / 6.0), new(Color.FromRgb(255, 0, 255), 5 / 6.0), new(Color.FromRgb(255, 0, 0), 1),
|
||||
}, new Point(0, 0), new Point(1, 0)),
|
||||
RadiusX = 8, RadiusY = 8,
|
||||
});
|
||||
hueArea.Children.Add(hueThumb);
|
||||
Canvas.SetTop(hueThumb, -2);
|
||||
hueArea.MouseLeftButtonDown += (_, e) => { hueArea.CaptureMouse(); UpdateHueFrom(e.GetPosition(hueArea)); };
|
||||
hueArea.MouseMove += (_, e) => { if (e.LeftButton == MouseButtonState.Pressed && hueArea.IsMouseCaptured) UpdateHueFrom(e.GetPosition(hueArea)); };
|
||||
hueArea.MouseLeftButtonUp += (_, _) => hueArea.ReleaseMouseCapture();
|
||||
root.Children.Add(new Border { Child = hueArea, Margin = new Thickness(0, 0, 0, 12) });
|
||||
|
||||
// ── 미리보기 + HEX / RGB ──
|
||||
var inputs = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(0, 0, 0, 12) };
|
||||
inputs.Children.Add(preview);
|
||||
var fields = new StackPanel { Margin = new Thickness(12, 0, 0, 0), VerticalAlignment = VerticalAlignment.Center };
|
||||
var hexRow = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(0, 0, 0, 6) };
|
||||
hexRow.Children.Add(new TextBlock { Text = "HEX", Width = 30, VerticalAlignment = VerticalAlignment.Center, Foreground = muted });
|
||||
hexRow.Children.Add(hexBox);
|
||||
fields.Children.Add(hexRow);
|
||||
var rgbRow = new StackPanel { Orientation = Orientation.Horizontal };
|
||||
rgbRow.Children.Add(new TextBlock { Text = "RGB", Width = 30, VerticalAlignment = VerticalAlignment.Center, Foreground = muted });
|
||||
rgbRow.Children.Add(rBox);
|
||||
rgbRow.Children.Add(new Border { Width = 4 });
|
||||
rgbRow.Children.Add(gBox);
|
||||
rgbRow.Children.Add(new Border { Width = 4 });
|
||||
rgbRow.Children.Add(bBox);
|
||||
fields.Children.Add(rgbRow);
|
||||
inputs.Children.Add(fields);
|
||||
root.Children.Add(inputs);
|
||||
|
||||
hexBox.LostKeyboardFocus += (_, _) => CommitHex();
|
||||
hexBox.KeyDown += (_, e) => { if (e.Key == Key.Enter) CommitHex(); };
|
||||
foreach (var box in new[] { rBox, gBox, bBox })
|
||||
{
|
||||
box.LostKeyboardFocus += (_, _) => CommitRgb();
|
||||
box.KeyDown += (_, e) => { if (e.Key == Key.Enter) CommitRgb(); };
|
||||
}
|
||||
|
||||
// ── 프리셋 + 최근색 ──
|
||||
root.Children.Add(SwatchGrid("프리셋", new[]
|
||||
{
|
||||
"#000000", "#374151", "#6B727B", "#C7CDD4", "#FFFFFF", "#C0392B", "#E67E22", "#F1C40F",
|
||||
"#1F9D55", "#16A085", "#2F6DF0", "#2980B9", "#8E44AD", "#EC4899", "#7F1D1D", "#FDE68A",
|
||||
}));
|
||||
if (recent.Count > 0)
|
||||
{
|
||||
root.Children.Add(SwatchGrid("최근 사용", recent.ToArray()));
|
||||
}
|
||||
|
||||
// ── 버튼 ──
|
||||
var buttons = new StackPanel { Orientation = Orientation.Horizontal, HorizontalAlignment = HorizontalAlignment.Right, Margin = new Thickness(0, 12, 0, 0) };
|
||||
var ok = new Button { Content = "적용", MinWidth = 64, Margin = new Thickness(0, 0, 8, 0), IsDefault = true };
|
||||
try { ok.Style = (Style)FindResource("Primary"); } catch { /* 테마 미로드 시 기본 */ }
|
||||
ok.Click += (_, _) => { Result = CurrentHex(); AddRecent(Result); DialogResult = true; };
|
||||
var cancel = new Button { Content = "취소", MinWidth = 64, IsCancel = true };
|
||||
cancel.Click += (_, _) => DialogResult = false;
|
||||
buttons.Children.Add(ok);
|
||||
buttons.Children.Add(cancel);
|
||||
root.Children.Add(buttons);
|
||||
|
||||
Content = root;
|
||||
RefreshAll();
|
||||
}
|
||||
|
||||
private FrameworkElement SwatchGrid(string title, string[] hexes)
|
||||
{
|
||||
var section = new StackPanel { Margin = new Thickness(0, 2, 0, 0) };
|
||||
section.Children.Add(new TextBlock { Text = title, FontSize = 11, Foreground = FindBrush("B.Muted"), Margin = new Thickness(0, 0, 0, 4) });
|
||||
var wrap = new WrapPanel();
|
||||
foreach (var hex in hexes)
|
||||
{
|
||||
var swatch = new Button
|
||||
{
|
||||
Width = 22, Height = 22, Margin = new Thickness(0, 0, 4, 4), Padding = new Thickness(0),
|
||||
ToolTip = hex, BorderBrush = FindBrush("B.Line2"),
|
||||
Background = new SolidColorBrush(ParseHex(hex) ?? Colors.White),
|
||||
};
|
||||
var captured = hex;
|
||||
swatch.Click += (_, _) =>
|
||||
{
|
||||
if (ParseHex(captured) is not { } color)
|
||||
{
|
||||
return;
|
||||
}
|
||||
(hue360, sat, val) = RgbToHsv(color.R, color.G, color.B);
|
||||
RefreshAll();
|
||||
};
|
||||
wrap.Children.Add(swatch);
|
||||
}
|
||||
section.Children.Add(wrap);
|
||||
return section;
|
||||
}
|
||||
|
||||
private static Brush FindBrush(string key)
|
||||
=> Application.Current.TryFindResource(key) as Brush ?? Brushes.Gray;
|
||||
|
||||
// ── 상호작용 → 상태 ──
|
||||
private void UpdateSvFrom(Point point)
|
||||
{
|
||||
sat = Math.Clamp(point.X / svArea.Width, 0, 1);
|
||||
val = 1 - Math.Clamp(point.Y / svArea.Height, 0, 1);
|
||||
RefreshAll();
|
||||
}
|
||||
|
||||
private void UpdateHueFrom(Point point)
|
||||
{
|
||||
hue360 = Math.Clamp(point.X / hueArea.Width, 0, 1) * 360;
|
||||
RefreshAll();
|
||||
}
|
||||
|
||||
private void CommitHex()
|
||||
{
|
||||
if (syncing)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (ParseHex(hexBox.Text) is not { } color)
|
||||
{
|
||||
RefreshAll();
|
||||
return;
|
||||
}
|
||||
(hue360, sat, val) = RgbToHsv(color.R, color.G, color.B);
|
||||
RefreshAll();
|
||||
}
|
||||
|
||||
private void CommitRgb()
|
||||
{
|
||||
if (syncing)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (byte.TryParse(rBox.Text, out var r) && byte.TryParse(gBox.Text, out var g) && byte.TryParse(bBox.Text, out var b))
|
||||
{
|
||||
(hue360, sat, val) = RgbToHsv(r, g, b);
|
||||
}
|
||||
RefreshAll();
|
||||
}
|
||||
|
||||
// ── 상태 → 화면 ──
|
||||
private void RefreshAll()
|
||||
{
|
||||
var (hueR, hueG, hueB) = HsvToRgb(hue360, 1, 1);
|
||||
svHue.Fill = new SolidColorBrush(Color.FromRgb(hueR, hueG, hueB));
|
||||
Canvas.SetLeft(svThumb, sat * svArea.Width - 7);
|
||||
Canvas.SetTop(svThumb, (1 - val) * svArea.Height - 7);
|
||||
var (r, g, b) = HsvToRgb(hue360, sat, val);
|
||||
svThumb.Fill = new SolidColorBrush(Color.FromRgb(r, g, b));
|
||||
Canvas.SetLeft(hueThumb, hue360 / 360 * hueArea.Width - 3);
|
||||
|
||||
preview.Background = new SolidColorBrush(Color.FromRgb(r, g, b));
|
||||
|
||||
syncing = true;
|
||||
hexBox.Text = CurrentHex();
|
||||
rBox.Text = r.ToString();
|
||||
gBox.Text = g.ToString();
|
||||
bBox.Text = b.ToString();
|
||||
syncing = false;
|
||||
}
|
||||
|
||||
private string CurrentHex()
|
||||
{
|
||||
var (r, g, b) = HsvToRgb(hue360, sat, val);
|
||||
return $"#{r:X2}{g:X2}{b:X2}";
|
||||
}
|
||||
|
||||
private static void AddRecent(string? hex)
|
||||
{
|
||||
if (string.IsNullOrEmpty(hex))
|
||||
{
|
||||
return;
|
||||
}
|
||||
recent.Remove(hex);
|
||||
recent.Insert(0, hex);
|
||||
while (recent.Count > 16)
|
||||
{
|
||||
recent.RemoveAt(recent.Count - 1);
|
||||
}
|
||||
}
|
||||
|
||||
// ── 색 변환 ──
|
||||
private static Color? ParseHex(string? text)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
var s = text.Trim().TrimStart('#');
|
||||
if (s.Length == 3)
|
||||
{
|
||||
s = $"{s[0]}{s[0]}{s[1]}{s[1]}{s[2]}{s[2]}";
|
||||
}
|
||||
if (s.Length != 6)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return int.TryParse(s, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var value)
|
||||
? Color.FromRgb((byte)((value >> 16) & 0xFF), (byte)((value >> 8) & 0xFF), (byte)(value & 0xFF))
|
||||
: null;
|
||||
}
|
||||
|
||||
private static (double H, double S, double V) RgbToHsv(byte r, byte g, byte b)
|
||||
{
|
||||
double rd = r / 255.0, gd = g / 255.0, bd = b / 255.0;
|
||||
double max = Math.Max(rd, Math.Max(gd, bd)), min = Math.Min(rd, Math.Min(gd, bd));
|
||||
double d = max - min, h = 0;
|
||||
if (d != 0)
|
||||
{
|
||||
if (max == rd)
|
||||
{
|
||||
h = 60 * (((gd - bd) / d) % 6);
|
||||
}
|
||||
else if (max == gd)
|
||||
{
|
||||
h = 60 * (((bd - rd) / d) + 2);
|
||||
}
|
||||
else
|
||||
{
|
||||
h = 60 * (((rd - gd) / d) + 4);
|
||||
}
|
||||
}
|
||||
if (h < 0)
|
||||
{
|
||||
h += 360;
|
||||
}
|
||||
return (h, max == 0 ? 0 : d / max, max);
|
||||
}
|
||||
|
||||
private static (byte R, byte G, byte B) HsvToRgb(double h, double s, double v)
|
||||
{
|
||||
double c = v * s, x = c * (1 - Math.Abs((h / 60 % 2) - 1)), m = v - c;
|
||||
double r = 0, g = 0, b = 0;
|
||||
if (h < 60) { r = c; g = x; }
|
||||
else if (h < 120) { r = x; g = c; }
|
||||
else if (h < 180) { g = c; b = x; }
|
||||
else if (h < 240) { g = x; b = c; }
|
||||
else if (h < 300) { r = x; b = c; }
|
||||
else { r = c; b = x; }
|
||||
return ((byte)Math.Round((r + m) * 255), (byte)Math.Round((g + m) * 255), (byte)Math.Round((b + m) * 255));
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
<UserControl x:Class="SheetMe.Designer.Views.DesignerCanvasView"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:b="http://schemas.microsoft.com/xaml/behaviors"
|
||||
xmlns:vm="clr-namespace:SheetMe.Designer.ViewModels"
|
||||
xmlns:v="clr-namespace:SheetMe.Designer.Views"
|
||||
xmlns:bh="clr-namespace:SheetMe.Designer.Behaviors"
|
||||
mc:Ignorable="d"
|
||||
d:DataContext="{d:DesignInstance vm:DesignerViewModel}"
|
||||
Focusable="True" FocusVisualStyle="{x:Null}"
|
||||
PreviewKeyDown="OnCanvasPreviewKeyDown" PreviewKeyUp="OnCanvasPreviewKeyUp">
|
||||
|
||||
<!-- 키보드 단축키(텍스트 입력 보호 포함) -->
|
||||
<b:Interaction.Behaviors>
|
||||
<bh:CanvasKeyboardBehavior/>
|
||||
</b:Interaction.Behaviors>
|
||||
|
||||
<!-- 디자인 캔버스: 스크롤 + 줌(LayoutTransform) 월드. 입력은 World 1곳에서 수신 -->
|
||||
<ScrollViewer x:Name="Scroll"
|
||||
HorizontalScrollBarVisibility="Auto"
|
||||
VerticalScrollBarVisibility="Auto"
|
||||
Background="{DynamicResource B.CanvasBg}"
|
||||
Focusable="False"
|
||||
PreviewMouseWheel="OnPreviewMouseWheel"
|
||||
PreviewMouseLeftButtonDown="OnPanMouseDown"
|
||||
PreviewMouseMove="OnPanMouseMove"
|
||||
PreviewMouseLeftButtonUp="OnPanMouseUp"
|
||||
LostMouseCapture="OnPanLostCapture">
|
||||
<Border Padding="24">
|
||||
<Grid x:Name="World"
|
||||
Background="Transparent"
|
||||
HorizontalAlignment="Left" VerticalAlignment="Top"
|
||||
Width="{Binding WorldWidth}" Height="{Binding WorldHeight}">
|
||||
<Grid.LayoutTransform>
|
||||
<ScaleTransform ScaleX="{Binding Zoom}" ScaleY="{Binding Zoom}"/>
|
||||
</Grid.LayoutTransform>
|
||||
|
||||
<b:Interaction.Behaviors>
|
||||
<bh:CanvasMouseBehavior/>
|
||||
<bh:CanvasDropBehavior/>
|
||||
</b:Interaction.Behaviors>
|
||||
|
||||
<!-- 층1: 페이지 스택 -->
|
||||
<ItemsControl ItemsSource="{Binding Pages}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<Canvas/>
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemContainerStyle>
|
||||
<Style TargetType="ContentPresenter">
|
||||
<Setter Property="Canvas.Left" Value="0"/>
|
||||
<Setter Property="Canvas.Top" Value="{Binding OffsetY}"/>
|
||||
</Style>
|
||||
</ItemsControl.ItemContainerStyle>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<v:PageView/>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
|
||||
<!-- 층2: 선택/마퀴/가이드 오버레이 (표시 전용) -->
|
||||
<v:SelectionOverlayView DataContext="{Binding Overlay}"/>
|
||||
|
||||
<!-- 층3: 인라인 텍스트 에디터 (더블클릭 시 코드비하인드가 배치) -->
|
||||
<Canvas x:Name="EditorLayer"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
</ScrollViewer>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,294 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using SheetMe.Designer.ViewModels;
|
||||
using SheetMe.Designer.ViewModels.Controls;
|
||||
|
||||
namespace SheetMe.Designer.Views;
|
||||
|
||||
/// <summary>
|
||||
/// 디자인 캔버스 뷰 — 코드비하인드 허용 범위: ①줌 휠 처리(커서 중심 스크롤 보정) ②캔버스 포커스 ③인라인 텍스트 에디터.
|
||||
/// 문서 변경 로직은 두지 않는다(커밋은 DesignerViewModel.CommitInlineText 경유).
|
||||
/// </summary>
|
||||
public partial class DesignerCanvasView : UserControl
|
||||
{
|
||||
#region Member Fields
|
||||
private DesignerViewModel? subscribedDesigner;
|
||||
private TextBox? inlineEditor;
|
||||
private ControlViewModel? editingTarget;
|
||||
|
||||
// 손(팬) 도구 — 플로팅 바 토글 또는 Space 누르는 동안
|
||||
private bool isPanning;
|
||||
private bool spacePanHeld;
|
||||
private Point panStartPoint;
|
||||
private double panStartHorizontal;
|
||||
private double panStartVertical;
|
||||
#endregion
|
||||
|
||||
#region Constructors
|
||||
public DesignerCanvasView()
|
||||
{
|
||||
InitializeComponent();
|
||||
DataContextChanged += OnDataContextChanged;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Methods - 인라인 에디터
|
||||
private void OnDataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (subscribedDesigner is not null)
|
||||
{
|
||||
subscribedDesigner.InlineEditRequested -= ShowInlineEditor;
|
||||
}
|
||||
CloseInlineEditor(commit: false);
|
||||
subscribedDesigner = e.NewValue as DesignerViewModel;
|
||||
if (subscribedDesigner is not null)
|
||||
{
|
||||
subscribedDesigner.InlineEditRequested += ShowInlineEditor;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>대상 컨트롤 위에 편집기 표시 — 폰트/정렬 일치, 줌은 World LayoutTransform 이 처리</summary>
|
||||
private void ShowInlineEditor(ControlViewModel target)
|
||||
{
|
||||
if (subscribedDesigner is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
CloseInlineEditor(commit: true);
|
||||
|
||||
// 데이터소스(MDataTable)는 인라인 대신 전용 쿼리 편집기 창
|
||||
if (target is DataTableViewModel dataTable)
|
||||
{
|
||||
var queryDialog = new QueryEditorWindow(dataTable.Id, dataTable.Model.Props.GetText("Query") ?? string.Empty)
|
||||
{
|
||||
Owner = Window.GetWindow(this),
|
||||
};
|
||||
if (queryDialog.ShowDialog() == true)
|
||||
{
|
||||
subscribedDesigner.CommitPropertyText(dataTable, "Query", queryDialog.QueryText);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var bounds = subscribedDesigner.WorldBoundsOf(target);
|
||||
var multiline = target is LabelViewModel
|
||||
|| (target is TextBoxViewModel textBox && textBox.Multiline);
|
||||
|
||||
var editor = new TextBox
|
||||
{
|
||||
// 종이 위 텍스트와 픽셀 일치가 목적 — 앱 테마의 암시 TextBox 템플릿(입력칩 라운드/MinHeight) 차단
|
||||
Style = new Style(typeof(TextBox)),
|
||||
Text = target.Text,
|
||||
FontFamily = target.FontFamily,
|
||||
FontSize = target.FontSize,
|
||||
FontWeight = target.FontWeight,
|
||||
FontStyle = target.FontStyle,
|
||||
Foreground = target.Foreground,
|
||||
Background = Brushes.White,
|
||||
BorderBrush = new SolidColorBrush(Color.FromRgb(0x1E, 0x7B, 0xE8)),
|
||||
BorderThickness = new Thickness(1),
|
||||
Padding = new Thickness(1, 0, 1, 0),
|
||||
MinWidth = Math.Max(40, bounds.Width),
|
||||
MinHeight = bounds.Height,
|
||||
MaxWidth = 640,
|
||||
AcceptsReturn = multiline,
|
||||
TextWrapping = multiline ? TextWrapping.Wrap : TextWrapping.NoWrap,
|
||||
VerticalContentAlignment = multiline ? VerticalAlignment.Top : VerticalAlignment.Center,
|
||||
ToolTip = multiline ? "Enter 확정 · Shift+Enter 줄바꿈 · Esc 취소" : "Enter 확정 · Esc 취소",
|
||||
};
|
||||
Canvas.SetLeft(editor, bounds.X);
|
||||
Canvas.SetTop(editor, bounds.Y);
|
||||
|
||||
editor.PreviewKeyDown += OnEditorKeyDown;
|
||||
editor.LostKeyboardFocus += OnEditorLostFocus;
|
||||
|
||||
editingTarget = target;
|
||||
inlineEditor = editor;
|
||||
EditorLayer.Children.Add(editor);
|
||||
|
||||
// 레이아웃 이후 포커스 + 전체 선택
|
||||
Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Input, () =>
|
||||
{
|
||||
editor.Focus();
|
||||
editor.SelectAll();
|
||||
});
|
||||
}
|
||||
|
||||
private void OnEditorKeyDown(object sender, KeyEventArgs e)
|
||||
{
|
||||
if (inlineEditor is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (e.Key == Key.Escape)
|
||||
{
|
||||
e.Handled = true;
|
||||
CloseInlineEditor(commit: false);
|
||||
Focus();
|
||||
}
|
||||
else if (e.Key == Key.Enter && !Keyboard.Modifiers.HasFlag(ModifierKeys.Shift))
|
||||
{
|
||||
// Enter=확정, Shift+Enter=줄바꿈(여러 줄 편집기)
|
||||
e.Handled = true;
|
||||
CloseInlineEditor(commit: true);
|
||||
Focus();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnEditorLostFocus(object sender, KeyboardFocusChangedEventArgs e)
|
||||
=> CloseInlineEditor(commit: true);
|
||||
|
||||
private void CloseInlineEditor(bool commit)
|
||||
{
|
||||
if (inlineEditor is null || editingTarget is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var editor = inlineEditor;
|
||||
var target = editingTarget;
|
||||
inlineEditor = null;
|
||||
editingTarget = null;
|
||||
|
||||
editor.PreviewKeyDown -= OnEditorKeyDown;
|
||||
editor.LostKeyboardFocus -= OnEditorLostFocus;
|
||||
EditorLayer.Children.Remove(editor);
|
||||
|
||||
if (commit)
|
||||
{
|
||||
subscribedDesigner?.CommitInlineText(target, editor.Text);
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Methods - 손(팬) 도구
|
||||
/// <summary>손 도구 활성 여부 — 플로팅 바 토글([200] 관행) 또는 Space 누르는 동안</summary>
|
||||
private bool IsHandToolActive =>
|
||||
spacePanHeld
|
||||
|| (Application.Current.MainWindow?.DataContext as MainViewModel)?.IsHandTool == true;
|
||||
|
||||
/// <summary>팬 시작 — Preview 단계 선점으로 하위 선택/이동 로직(World 버블링) 차단</summary>
|
||||
private void OnPanMouseDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (!IsHandToolActive)
|
||||
{
|
||||
return;
|
||||
}
|
||||
isPanning = true;
|
||||
panStartPoint = e.GetPosition(Scroll);
|
||||
panStartHorizontal = Scroll.HorizontalOffset;
|
||||
panStartVertical = Scroll.VerticalOffset;
|
||||
Scroll.CaptureMouse();
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
private void OnPanMouseMove(object sender, MouseEventArgs e)
|
||||
{
|
||||
// 손 모드 커서 — 하위(World) 히트테스트 커서보다 우선하도록 ForceCursor
|
||||
if (IsHandToolActive)
|
||||
{
|
||||
Scroll.Cursor = Cursors.Hand;
|
||||
Scroll.ForceCursor = true;
|
||||
}
|
||||
else if (Scroll.ForceCursor)
|
||||
{
|
||||
Scroll.ForceCursor = false;
|
||||
Scroll.Cursor = null;
|
||||
}
|
||||
|
||||
if (isPanning)
|
||||
{
|
||||
var position = e.GetPosition(Scroll);
|
||||
Scroll.ScrollToHorizontalOffset(panStartHorizontal - (position.X - panStartPoint.X));
|
||||
Scroll.ScrollToVerticalOffset(panStartVertical - (position.Y - panStartPoint.Y));
|
||||
e.Handled = true;
|
||||
}
|
||||
else if (IsHandToolActive)
|
||||
{
|
||||
e.Handled = true; // 손 모드 유휴 이동도 선점 — World 커서 갱신 억제
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPanMouseUp(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (!isPanning)
|
||||
{
|
||||
return;
|
||||
}
|
||||
isPanning = false;
|
||||
Scroll.ReleaseMouseCapture();
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
private void OnPanLostCapture(object sender, MouseEventArgs e) => isPanning = false;
|
||||
|
||||
/// <summary>Space 누르는 동안 임시 손 도구 — 텍스트 입력 중에는 개입하지 않음</summary>
|
||||
private void OnCanvasPreviewKeyDown(object sender, KeyEventArgs e)
|
||||
{
|
||||
if (e.Key == Key.Space && !e.IsRepeat && e.OriginalSource is not TextBox)
|
||||
{
|
||||
spacePanHeld = true;
|
||||
Scroll.Cursor = Cursors.Hand;
|
||||
Scroll.ForceCursor = true;
|
||||
e.Handled = true; // ScrollViewer 의 Space 페이지 스크롤 기본 동작 차단
|
||||
}
|
||||
}
|
||||
|
||||
private void OnCanvasPreviewKeyUp(object sender, KeyEventArgs e)
|
||||
{
|
||||
if (e.Key == Key.Space && spacePanHeld)
|
||||
{
|
||||
spacePanHeld = false;
|
||||
if (!IsHandToolActive)
|
||||
{
|
||||
Scroll.ForceCursor = false;
|
||||
Scroll.Cursor = null;
|
||||
}
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Methods - 줌
|
||||
/// <summary>Ctrl+휠 줌 — 커서 논리 위치를 유지하도록 스크롤 오프셋 보정</summary>
|
||||
private void OnPreviewMouseWheel(object sender, MouseWheelEventArgs e)
|
||||
{
|
||||
if (Keyboard.Modifiers != ModifierKeys.Control)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (DataContext is not DesignerViewModel designer)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
e.Handled = true;
|
||||
|
||||
var oldZoom = designer.Zoom;
|
||||
if (e.Delta > 0)
|
||||
{
|
||||
designer.ZoomIn();
|
||||
}
|
||||
else
|
||||
{
|
||||
designer.ZoomOut();
|
||||
}
|
||||
|
||||
var newZoom = designer.Zoom;
|
||||
if (Math.Abs(newZoom - oldZoom) < 0.0001)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// 커서가 가리키는 논리 지점이 화면상 같은 곳에 남도록 오프셋 보정 (레이아웃 갱신 후)
|
||||
var mouse = e.GetPosition(Scroll);
|
||||
var factor = newZoom / oldZoom;
|
||||
Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Background, () =>
|
||||
{
|
||||
Scroll.ScrollToHorizontalOffset((Scroll.HorizontalOffset + mouse.X) * factor - mouse.X);
|
||||
Scroll.ScrollToVerticalOffset((Scroll.VerticalOffset + mouse.Y) * factor - mouse.Y);
|
||||
});
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<Window x:Class="SheetMe.Designer.Views.FontManagerDialogView"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
Title="폰트 일괄 변경" Width="620" Height="560" MinWidth="520" MinHeight="420"
|
||||
WindowStartupLocation="CenterOwner" ShowInTaskbar="False">
|
||||
<DockPanel Margin="12">
|
||||
<TextBlock DockPanel.Dock="Top" Foreground="{DynamicResource B.Muted}" TextWrapping="Wrap" Margin="0,0,0,8"
|
||||
Text="문서 전체의 (타입·글꼴·크기) 조합입니다. 변경할 조합을 선택(다중 가능)하고 새 글꼴을 지정하세요."/>
|
||||
|
||||
<!-- 새 글꼴 지정 + 적용 -->
|
||||
<Border DockPanel.Dock="Bottom" Background="{DynamicResource B.Chip}" CornerRadius="4" Padding="10" Margin="0,10,0,0">
|
||||
<StackPanel>
|
||||
<DockPanel>
|
||||
<TextBlock Text="새 글꼴" Width="60" VerticalAlignment="Center"/>
|
||||
<ComboBox x:Name="FamilyBox" IsEditable="True" Width="180" HorizontalAlignment="Left"/>
|
||||
<TextBlock Text="크기(pt)" Margin="14,0,6,0" VerticalAlignment="Center"/>
|
||||
<TextBox x:Name="SizeBox" Width="50" Text="9" VerticalContentAlignment="Center"/>
|
||||
<CheckBox x:Name="BoldBox" Content="굵게" Margin="14,0,0,0" VerticalAlignment="Center"/>
|
||||
<CheckBox x:Name="ItalicBox" Content="기울임" Margin="10,0,0,0" VerticalAlignment="Center"/>
|
||||
<CheckBox x:Name="UnderlineBox" Content="밑줄" Margin="10,0,0,0" VerticalAlignment="Center"/>
|
||||
</DockPanel>
|
||||
<DockPanel Margin="0,10,0,0">
|
||||
<Button DockPanel.Dock="Right" Content="닫기" Padding="16,4" Margin="8,0,0,0" IsCancel="True"/>
|
||||
<Button DockPanel.Dock="Right" Content="전체에 적용" Padding="14,4" Margin="8,0,0,0" Click="OnApplyAll"/>
|
||||
<Button DockPanel.Dock="Right" Content="선택 조합에 적용" Padding="14,4" Click="OnApplySelected"/>
|
||||
<TextBlock x:Name="ResultText" VerticalAlignment="Center" Foreground="{DynamicResource B.Success}"/>
|
||||
</DockPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- 집계 목록 -->
|
||||
<ListBox x:Name="GroupList" SelectionMode="Extended" FontSize="13"
|
||||
ScrollViewer.HorizontalScrollBarVisibility="Disabled">
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<TextBlock Text="{Binding Display}" Margin="2"/>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
</DockPanel>
|
||||
</Window>
|
||||
@@ -0,0 +1,79 @@
|
||||
using System.Globalization;
|
||||
using System.Windows;
|
||||
using System.Windows.Media;
|
||||
using SheetMe.Core.Serialization;
|
||||
using SheetMe.Designer.ViewModels;
|
||||
|
||||
namespace SheetMe.Designer.Views;
|
||||
|
||||
/// <summary>
|
||||
/// 폰트 일괄 변경 대화상자 — 레거시 frmFontManager 이식.
|
||||
/// 문서 전체 (타입·글꼴·크기·스타일) 집계 → 선택 조합 또는 전체에 새 글꼴 일괄 적용(Undo 1스텝).
|
||||
/// </summary>
|
||||
public partial class FontManagerDialogView : Window
|
||||
{
|
||||
#region Member Fields
|
||||
private readonly DesignerViewModel designer;
|
||||
#endregion
|
||||
|
||||
#region Constructors
|
||||
public FontManagerDialogView(DesignerViewModel designer)
|
||||
{
|
||||
this.designer = designer;
|
||||
InitializeComponent();
|
||||
FamilyBox.ItemsSource = Fonts.SystemFontFamilies
|
||||
.Select(f => f.FamilyNames.Values.FirstOrDefault() ?? f.Source)
|
||||
.Distinct()
|
||||
.OrderBy(name => name, StringComparer.CurrentCulture)
|
||||
.ToList();
|
||||
FamilyBox.Text = "굴림";
|
||||
Reload();
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Methods
|
||||
private void Reload()
|
||||
{
|
||||
GroupList.ItemsSource = designer.CollectFontUsage();
|
||||
}
|
||||
|
||||
private void OnApplySelected(object sender, RoutedEventArgs e)
|
||||
=> Apply(GroupList.SelectedItems.Cast<FontUsageGroup>().ToList());
|
||||
|
||||
private void OnApplyAll(object sender, RoutedEventArgs e)
|
||||
=> Apply(GroupList.Items.Cast<FontUsageGroup>().ToList());
|
||||
|
||||
private void Apply(List<FontUsageGroup> targets)
|
||||
{
|
||||
if (targets.Count == 0)
|
||||
{
|
||||
MessageBox.Show("변경할 조합을 선택하세요.", "폰트 일괄 변경");
|
||||
return;
|
||||
}
|
||||
var family = FamilyBox.Text.Trim();
|
||||
if (family.Length == 0)
|
||||
{
|
||||
MessageBox.Show("글꼴명을 입력하세요.", "폰트 일괄 변경");
|
||||
return;
|
||||
}
|
||||
if (!double.TryParse(SizeBox.Text.Trim(), NumberStyles.Number, CultureInfo.InvariantCulture, out var sizePt)
|
||||
|| sizePt <= 0 || sizePt > 200)
|
||||
{
|
||||
MessageBox.Show("크기(pt)를 올바르게 입력하세요.", "폰트 일괄 변경");
|
||||
return;
|
||||
}
|
||||
|
||||
var newFont = new LegacyFont
|
||||
{
|
||||
Family = family,
|
||||
SizePt = sizePt,
|
||||
Bold = BoldBox.IsChecked == true,
|
||||
Italic = ItalicBox.IsChecked == true,
|
||||
Underline = UnderlineBox.IsChecked == true,
|
||||
};
|
||||
var changed = designer.ApplyFontBulk(targets, newFont);
|
||||
ResultText.Text = $"{changed}개 컨트롤 변경됨";
|
||||
Reload();
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
<UserControl x:Class="SheetMe.Designer.Views.InspectorView"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:ins="clr-namespace:SheetMe.Designer.ViewModels.Inspector"
|
||||
mc:Ignorable="d"
|
||||
d:DataContext="{d:DesignInstance ins:InspectorViewModel}">
|
||||
|
||||
<UserControl.Resources>
|
||||
<!-- 행 VM 타입별 암시적 템플릿 -->
|
||||
<Style x:Key="RowLabel" TargetType="TextBlock">
|
||||
<Setter Property="Width" Value="86"/>
|
||||
<Setter Property="VerticalAlignment" Value="Center"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource B.Muted}"/>
|
||||
<Setter Property="FontSize" Value="12"/>
|
||||
</Style>
|
||||
<Style x:Key="RowEditor" TargetType="Control">
|
||||
<Setter Property="FontSize" Value="12"/>
|
||||
</Style>
|
||||
|
||||
<DataTemplate DataType="{x:Type ins:SectionRowViewModel}">
|
||||
<TextBlock Text="{Binding Label}" FontWeight="Bold" Foreground="{DynamicResource B.Muted}"
|
||||
Margin="0,10,0,4" FontSize="12"/>
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate DataType="{x:Type ins:TextRowViewModel}">
|
||||
<DockPanel Margin="0,2">
|
||||
<TextBlock Text="{Binding Label}" Style="{StaticResource RowLabel}"/>
|
||||
<TextBox Text="{Binding ValueText, UpdateSourceTrigger=LostFocus}" FontSize="12"/>
|
||||
</DockPanel>
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate DataType="{x:Type ins:MultilineTextRowViewModel}">
|
||||
<DockPanel Margin="0,2">
|
||||
<TextBlock Text="{Binding Label}" Style="{StaticResource RowLabel}" VerticalAlignment="Top" Margin="0,4,0,0"/>
|
||||
<TextBox Text="{Binding ValueText, UpdateSourceTrigger=LostFocus}" FontSize="12"
|
||||
AcceptsReturn="True" TextWrapping="Wrap" MinHeight="48" MaxHeight="120"
|
||||
VerticalScrollBarVisibility="Auto"/>
|
||||
</DockPanel>
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate DataType="{x:Type ins:NumberRowViewModel}">
|
||||
<DockPanel Margin="0,2">
|
||||
<TextBlock Text="{Binding Label}" Style="{StaticResource RowLabel}"/>
|
||||
<TextBox Text="{Binding ValueText, UpdateSourceTrigger=LostFocus}" FontSize="12"/>
|
||||
</DockPanel>
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate DataType="{x:Type ins:ToggleRowViewModel}">
|
||||
<DockPanel Margin="0,2">
|
||||
<TextBlock Text="{Binding Label}" Style="{StaticResource RowLabel}"/>
|
||||
<CheckBox IsChecked="{Binding IsOn}" VerticalAlignment="Center"/>
|
||||
</DockPanel>
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate DataType="{x:Type ins:ChoiceRowViewModel}">
|
||||
<DockPanel Margin="0,2">
|
||||
<TextBlock Text="{Binding Label}" Style="{StaticResource RowLabel}"/>
|
||||
<ComboBox ItemsSource="{Binding Choices}" SelectedItem="{Binding ValueText}" FontSize="12"/>
|
||||
</DockPanel>
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate DataType="{x:Type ins:TagPickerRowViewModel}">
|
||||
<DockPanel Margin="0,2">
|
||||
<TextBlock Text="{Binding Label}" Style="{StaticResource RowLabel}"/>
|
||||
<Button DockPanel.Dock="Right" Content="…" Width="24" Margin="4,0,0,0"
|
||||
Command="{Binding BrowseCommand}" ToolTip="목록에서 선택"/>
|
||||
<TextBox Text="{Binding ValueText, UpdateSourceTrigger=LostFocus}" FontSize="12"
|
||||
ToolTip="직접 입력 또는 … 버튼으로 선택"/>
|
||||
</DockPanel>
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate DataType="{x:Type ins:ReadOnlyRowViewModel}">
|
||||
<DockPanel Margin="0,2">
|
||||
<TextBlock Text="{Binding Label}" Style="{StaticResource RowLabel}"/>
|
||||
<TextBlock Text="{Binding ValueText}" FontSize="11" Foreground="{DynamicResource B.Muted}"
|
||||
VerticalAlignment="Center" TextTrimming="CharacterEllipsis"/>
|
||||
</DockPanel>
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate DataType="{x:Type ins:ToggleAdvancedRowViewModel}">
|
||||
<Button Content="{Binding ButtonText}" Command="{Binding ToggleCommand}"
|
||||
Margin="0,6,0,2" Padding="6,3" HorizontalAlignment="Stretch"
|
||||
Background="{DynamicResource B.Chip}" BorderBrush="{DynamicResource B.Line2}" FontSize="11" Foreground="{DynamicResource B.Muted}"/>
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate DataType="{x:Type ins:AddPropertyRowViewModel}">
|
||||
<DockPanel Margin="0,6,0,2">
|
||||
<Button DockPanel.Dock="Right" Content="추가" Padding="8,2" Margin="4,0,0,0"
|
||||
Command="{Binding AddCommand}"
|
||||
ToolTip="레거시 컨트롤의 속성 이름을 정확히 입력하세요 (예: EnterTabYon, AutoHeight)"/>
|
||||
<TextBox Text="{Binding KeyText, UpdateSourceTrigger=PropertyChanged}" FontSize="12"
|
||||
ToolTip="새 속성 키 입력"/>
|
||||
</DockPanel>
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate DataType="{x:Type ins:QueryRowViewModel}">
|
||||
<DockPanel Margin="0,2">
|
||||
<TextBlock Text="{Binding Label}" Style="{StaticResource RowLabel}"/>
|
||||
<Button DockPanel.Dock="Right" Content="편집…" Padding="6,1" Margin="4,0,0,0"
|
||||
Command="{Binding EditCommand}" ToolTip="쿼리 편집기 열기 (데이터소스 더블클릭과 동일)"/>
|
||||
<TextBlock Text="{Binding Summary}" FontSize="11" Foreground="{DynamicResource B.Muted}"
|
||||
VerticalAlignment="Center" TextTrimming="CharacterEllipsis"
|
||||
ToolTip="{Binding ValueText}"/>
|
||||
</DockPanel>
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate DataType="{x:Type ins:ColorRowViewModel}">
|
||||
<DockPanel Margin="0,2">
|
||||
<TextBlock Text="{Binding Label}" Style="{StaticResource RowLabel}"/>
|
||||
<!-- 스와치 = 피커 버튼(클릭 → 색상 선택 대화상자), 직접 입력도 병행 -->
|
||||
<Button DockPanel.Dock="Right" Width="26" Height="22" Margin="4,0,0,0" Padding="0"
|
||||
Command="{Binding PickCommand}" Cursor="Hand"
|
||||
ToolTip="클릭하여 색상 선택">
|
||||
<Button.Template>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border x:Name="bd" Background="{Binding Preview}" BorderBrush="{DynamicResource B.Line2}"
|
||||
BorderThickness="1" CornerRadius="4" SnapsToDevicePixels="True"/>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="bd" Property="BorderBrush" Value="{DynamicResource B.Accent}"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Button.Template>
|
||||
</Button>
|
||||
<TextBox Text="{Binding ValueText, UpdateSourceTrigger=LostFocus}" FontSize="12"
|
||||
ToolTip="R, G, B 또는 색 이름 (예: 224, 224, 224 / White) — 오른쪽 스와치 클릭 시 피커"/>
|
||||
</DockPanel>
|
||||
</DataTemplate>
|
||||
</UserControl.Resources>
|
||||
|
||||
<DockPanel>
|
||||
<TextBlock DockPanel.Dock="Top" Text="{Binding Summary}" FontWeight="Bold"
|
||||
Margin="10,10,10,2" Foreground="{DynamicResource B.Muted}" TextTrimming="CharacterEllipsis"/>
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||
<ItemsControl ItemsSource="{Binding Rows}" Margin="10,0,10,10"/>
|
||||
</ScrollViewer>
|
||||
</DockPanel>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,12 @@
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace SheetMe.Designer.Views;
|
||||
|
||||
/// <summary>속성 인스펙터 뷰 — 행 VM 암시적 템플릿 렌더.</summary>
|
||||
public partial class InspectorView : UserControl
|
||||
{
|
||||
public InspectorView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,540 @@
|
||||
<Window x:Class="SheetMe.Designer.Views.MainView"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:shell="clr-namespace:System.Windows.Shell;assembly=PresentationFramework"
|
||||
xmlns:vm="clr-namespace:SheetMe.Designer.ViewModels"
|
||||
xmlns:v="clr-namespace:SheetMe.Designer.Views"
|
||||
xmlns:b="http://schemas.microsoft.com/xaml/behaviors"
|
||||
xmlns:bh="clr-namespace:SheetMe.Designer.Behaviors"
|
||||
xmlns:ctl="clr-namespace:SheetMe.Designer.Controls"
|
||||
mc:Ignorable="d"
|
||||
d:DataContext="{d:DesignInstance vm:MainViewModel}"
|
||||
Title="{Binding Title}"
|
||||
Icon="pack://application:,,,/SheetMe.Designer;component/Assets/sheetme.ico"
|
||||
Width="1440" Height="920" MinWidth="940" MinHeight="640"
|
||||
WindowStyle="None" WindowStartupLocation="CenterScreen"
|
||||
Background="{DynamicResource B.AppBg}" FontFamily="Malgun Gothic" FontSize="13"
|
||||
Loaded="OnWindowLoaded" StateChanged="OnWindowStateChanged">
|
||||
|
||||
<!-- GlassFrameThickness 0,0,0,1: DWM 창 그림자 활성화([200]SheetMe 크롬과 동일) -->
|
||||
<shell:WindowChrome.WindowChrome>
|
||||
<shell:WindowChrome CaptionHeight="40" ResizeBorderThickness="6" GlassFrameThickness="0,0,0,1"
|
||||
CornerRadius="0" UseAeroCaptionButtons="False"/>
|
||||
</shell:WindowChrome.WindowChrome>
|
||||
|
||||
<Window.DataContext>
|
||||
<vm:MainViewModel/>
|
||||
</Window.DataContext>
|
||||
|
||||
<Window.Resources>
|
||||
<ctl:TypeToIconConverter x:Key="TypeToIcon"/>
|
||||
<ctl:TypeToCategoryConverter x:Key="TypeToCategory"/>
|
||||
<ctl:IconNameToVisualConverter x:Key="IconName"/>
|
||||
<ctl:InverseBoolConverter x:Key="InverseBool"/>
|
||||
|
||||
<!-- 팔레트(도구 상자/플라이아웃 공용) — 카테고리 그룹 뷰 -->
|
||||
<CollectionViewSource x:Key="PaletteGrouped" Source="{Binding PaletteItems}">
|
||||
<CollectionViewSource.GroupDescriptions>
|
||||
<PropertyGroupDescription PropertyName="Type" Converter="{StaticResource TypeToCategory}"/>
|
||||
</CollectionViewSource.GroupDescriptions>
|
||||
</CollectionViewSource>
|
||||
|
||||
<!-- 도구 상자 타일 카드([200] 컴포넌트 카드 룩) — 선택 하이라이트 없음 -->
|
||||
<Style x:Key="PaletteTile" TargetType="ListBoxItem">
|
||||
<Setter Property="Cursor" Value="Hand"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="ListBoxItem">
|
||||
<Border x:Name="bd" Background="{DynamicResource B.Surface}" BorderBrush="{DynamicResource B.Line}"
|
||||
BorderThickness="1" CornerRadius="5" Margin="3" SnapsToDevicePixels="True">
|
||||
<ContentPresenter/>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="bd" Property="Background" Value="{DynamicResource B.Hover}"/>
|
||||
<Setter TargetName="bd" Property="BorderBrush" Value="{DynamicResource B.Line2}"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!-- 팔레트 카테고리 그룹 헤더 -->
|
||||
<Style x:Key="PaletteGroupHeader" TargetType="TextBlock">
|
||||
<Setter Property="FontSize" Value="10.5"/>
|
||||
<Setter Property="FontWeight" Value="Bold"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource B.Muted}"/>
|
||||
<Setter Property="Margin" Value="5,9,0,4"/>
|
||||
</Style>
|
||||
|
||||
<!-- 플라이아웃/줌 팝업 행 버튼 — 왼정렬 hover 행 -->
|
||||
<Style x:Key="FlyoutRow" TargetType="Button">
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource B.Ink}"/>
|
||||
<Setter Property="Cursor" Value="Hand"/>
|
||||
<Setter Property="Padding" Value="12,7"/>
|
||||
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border x:Name="bd" Background="{TemplateBinding Background}" CornerRadius="6"
|
||||
Padding="{TemplateBinding Padding}">
|
||||
<ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
|
||||
VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="bd" Property="Background" Value="{DynamicResource B.Hover}"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
</Window.Resources>
|
||||
|
||||
<DockPanel>
|
||||
<!-- ===== 커스텀 타이틀바: 로고 + 메뉴 | 중앙 제목 | 창 제어 ===== -->
|
||||
<Border DockPanel.Dock="Top" Background="{DynamicResource B.Titlebar}" Height="40"
|
||||
BorderBrush="{DynamicResource B.Line}" BorderThickness="0,0,0,1">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- 좌: 로고 + 메뉴 -->
|
||||
<StackPanel Grid.Column="0" Orientation="Horizontal" VerticalAlignment="Center" Margin="10,0,0,0"
|
||||
shell:WindowChrome.IsHitTestVisibleInChrome="True">
|
||||
<Image Source="pack://application:,,,/SheetMe.Designer;component/Assets/sheetme-logo.png"
|
||||
Width="20" Height="20" VerticalAlignment="Center" RenderOptions.BitmapScalingMode="HighQuality"/>
|
||||
<Menu VerticalAlignment="Center" Margin="8,0,0,0">
|
||||
<MenuItem Header="파일(_F)">
|
||||
<MenuItem Header="새 서식(_N)" Command="{Binding NewFileCommand}" InputGestureText="Ctrl+N"/>
|
||||
<MenuItem Header="열기(_O)..." Command="{Binding OpenFileCommand}" InputGestureText="Ctrl+O"/>
|
||||
<Separator/>
|
||||
<MenuItem Header="저장(_S)" Command="{Binding SaveFileCommand}" InputGestureText="Ctrl+S"/>
|
||||
<MenuItem Header="다른 이름으로 저장(_A)..." Command="{Binding SaveAsFileCommand}"/>
|
||||
<Separator/>
|
||||
<MenuItem Header="DB에서 열기(_D)..." Command="{Binding OpenFromDbCommand}"/>
|
||||
<MenuItem Header="DB에 저장(_B)..." Command="{Binding SaveToDbCommand}"/>
|
||||
<Separator/>
|
||||
<MenuItem Header="JSON 가져오기(_I)..." Command="{Binding ImportJsonCommand}"/>
|
||||
<MenuItem Header="JSON 내보내기(_E)..." Command="{Binding ExportJsonCommand}"/>
|
||||
<Separator/>
|
||||
<MenuItem Header="미리보기(_V)..." Command="{Binding PreviewCommand}"/>
|
||||
<MenuItem Header="인쇄(_P)..." Command="{Binding PrintCommand}" InputGestureText="Ctrl+P"/>
|
||||
<Separator/>
|
||||
<MenuItem Header="종료(_X)" Command="{Binding ExitCommand}"/>
|
||||
</MenuItem>
|
||||
<MenuItem Header="편집(_E)">
|
||||
<MenuItem Header="실행 취소(_U)" Command="{Binding CurrentDesigner.UndoCommand}" InputGestureText="Ctrl+Z"/>
|
||||
<MenuItem Header="다시 실행(_R)" Command="{Binding CurrentDesigner.RedoCommand}" InputGestureText="Ctrl+Y"/>
|
||||
<Separator/>
|
||||
<MenuItem Header="잘라내기(_T)" Command="{Binding CurrentDesigner.CutCommand}" InputGestureText="Ctrl+X"/>
|
||||
<MenuItem Header="복사(_C)" Command="{Binding CurrentDesigner.CopyCommand}" InputGestureText="Ctrl+C"/>
|
||||
<MenuItem Header="붙여넣기(_P)" Command="{Binding CurrentDesigner.PasteCommand}" InputGestureText="Ctrl+V"/>
|
||||
<MenuItem Header="복제(_D)" Command="{Binding CurrentDesigner.DuplicateCommand}" InputGestureText="Ctrl+D"/>
|
||||
<MenuItem Header="삭제(_L)" Command="{Binding CurrentDesigner.DeleteCommand}" InputGestureText="Del"/>
|
||||
<Separator/>
|
||||
<MenuItem Header="전체 선택(_A)" Command="{Binding CurrentDesigner.SelectAllCommand}" InputGestureText="Ctrl+A"/>
|
||||
</MenuItem>
|
||||
<MenuItem Header="배치(_A)">
|
||||
<MenuItem Header="맨 앞으로(_F)" Command="{Binding CurrentDesigner.BringToFrontCommand}"/>
|
||||
<MenuItem Header="맨 뒤로(_B)" Command="{Binding CurrentDesigner.SendToBackCommand}"/>
|
||||
<Separator/>
|
||||
<MenuItem Header="그룹(_G)" Command="{Binding CurrentDesigner.GroupCommand}" InputGestureText="Ctrl+G"/>
|
||||
<MenuItem Header="그룹 해제(_U)" Command="{Binding CurrentDesigner.UngroupCommand}" InputGestureText="Ctrl+Shift+G"/>
|
||||
<Separator/>
|
||||
<MenuItem Header="왼쪽 맞춤" Command="{Binding CurrentDesigner.AlignCommand}" CommandParameter="Left"/>
|
||||
<MenuItem Header="오른쪽 맞춤" Command="{Binding CurrentDesigner.AlignCommand}" CommandParameter="Right"/>
|
||||
<MenuItem Header="위 맞춤" Command="{Binding CurrentDesigner.AlignCommand}" CommandParameter="Top"/>
|
||||
<MenuItem Header="아래 맞춤" Command="{Binding CurrentDesigner.AlignCommand}" CommandParameter="Bottom"/>
|
||||
<MenuItem Header="가로 가운데" Command="{Binding CurrentDesigner.AlignCommand}" CommandParameter="CenterH"/>
|
||||
<MenuItem Header="세로 가운데" Command="{Binding CurrentDesigner.AlignCommand}" CommandParameter="CenterV"/>
|
||||
<Separator/>
|
||||
<MenuItem Header="같은 크기로">
|
||||
<MenuItem Header="너비/높이 모두(_B)" Command="{Binding CurrentDesigner.SizeToControlCommand}" CommandParameter="Both"/>
|
||||
<MenuItem Header="너비(_W)" Command="{Binding CurrentDesigner.SizeToControlCommand}" CommandParameter="Width"/>
|
||||
<MenuItem Header="높이(_H)" Command="{Binding CurrentDesigner.SizeToControlCommand}" CommandParameter="Height"/>
|
||||
</MenuItem>
|
||||
<MenuItem Header="가로 간격">
|
||||
<MenuItem Header="균등(_E)" Command="{Binding CurrentDesigner.SpacingCommand}" CommandParameter="HorizEqual"/>
|
||||
<MenuItem Header="넓게(_I)" Command="{Binding CurrentDesigner.SpacingCommand}" CommandParameter="HorizIncrease"/>
|
||||
<MenuItem Header="좁게(_D)" Command="{Binding CurrentDesigner.SpacingCommand}" CommandParameter="HorizDecrease"/>
|
||||
<MenuItem Header="붙이기(_C)" Command="{Binding CurrentDesigner.SpacingCommand}" CommandParameter="HorizConcat"/>
|
||||
</MenuItem>
|
||||
<MenuItem Header="세로 간격">
|
||||
<MenuItem Header="균등(_E)" Command="{Binding CurrentDesigner.SpacingCommand}" CommandParameter="VertEqual"/>
|
||||
<MenuItem Header="넓게(_I)" Command="{Binding CurrentDesigner.SpacingCommand}" CommandParameter="VertIncrease"/>
|
||||
<MenuItem Header="좁게(_D)" Command="{Binding CurrentDesigner.SpacingCommand}" CommandParameter="VertDecrease"/>
|
||||
<MenuItem Header="붙이기(_C)" Command="{Binding CurrentDesigner.SpacingCommand}" CommandParameter="VertConcat"/>
|
||||
</MenuItem>
|
||||
<Separator/>
|
||||
<MenuItem Header="페이지 가로 가운데(_H)" Command="{Binding CurrentDesigner.CenterInPageCommand}" CommandParameter="H"/>
|
||||
<MenuItem Header="페이지 세로 가운데(_V)" Command="{Binding CurrentDesigner.CenterInPageCommand}" CommandParameter="V"/>
|
||||
<Separator/>
|
||||
<MenuItem Header="{Binding CurrentDesigner.TabOrderMenuHeader, FallbackValue=탭순서 편집 시작}"
|
||||
Command="{Binding CurrentDesigner.TabOrderCommand}"
|
||||
IsChecked="{Binding CurrentDesigner.IsTabOrderMode, Mode=OneWay}"
|
||||
ToolTip="입력 컨트롤을 원하는 입력 순서대로 클릭한 뒤 완료 — Esc 취소"/>
|
||||
</MenuItem>
|
||||
<MenuItem Header="보기(_V)">
|
||||
<MenuItem Header="확대(_I)" Command="{Binding ZoomInCommand}" InputGestureText="Ctrl+휠↑"/>
|
||||
<MenuItem Header="축소(_O)" Command="{Binding ZoomOutCommand}" InputGestureText="Ctrl+휠↓"/>
|
||||
<MenuItem Header="100%(_R)" Command="{Binding ZoomResetCommand}"/>
|
||||
<Separator/>
|
||||
<MenuItem x:Name="ThemeMenuItem" Header="다크 테마(_D)" IsCheckable="True" Click="OnThemeToggleClick"/>
|
||||
</MenuItem>
|
||||
<MenuItem Header="도구(_T)">
|
||||
<MenuItem Header="서식 수정이력(_H)..." Command="{Binding SheetHistoryCommand}"/>
|
||||
<Separator/>
|
||||
<MenuItem Header="상용구 관리(_W)..." Command="{Binding RecordWordCommand}"/>
|
||||
<MenuItem Header="폰트 일괄 변경(_F)..." Command="{Binding FontManagerCommand}"/>
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</StackPanel>
|
||||
|
||||
<!-- 중앙: 창 제목 -->
|
||||
<TextBlock Grid.Column="1" Text="{Binding Title}" Foreground="{DynamicResource B.Muted}"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center" FontSize="12"/>
|
||||
|
||||
<!-- 우: 창 제어 -->
|
||||
<StackPanel Grid.Column="2" Orientation="Horizontal" shell:WindowChrome.IsHitTestVisibleInChrome="True">
|
||||
<Button Style="{StaticResource CaptionBtn}" Click="OnMinimizeClick" ToolTip="최소화">
|
||||
<TextBlock Text="" FontFamily="Segoe MDL2 Assets" FontSize="10"/>
|
||||
</Button>
|
||||
<Button Style="{StaticResource CaptionBtn}" Click="OnMaxRestoreClick" ToolTip="최대화/복원">
|
||||
<TextBlock x:Name="MaxGlyph" Text="" FontFamily="Segoe MDL2 Assets" FontSize="10"/>
|
||||
</Button>
|
||||
<Button Style="{StaticResource CloseBtn}" Click="OnCloseWinClick" ToolTip="닫기">
|
||||
<TextBlock Text="" FontFamily="Segoe MDL2 Assets" FontSize="10"/>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- ===== 하단 상태바 (줌은 플로팅 바로 이동) ===== -->
|
||||
<Border DockPanel.Dock="Bottom" Background="{DynamicResource B.Titlebar}"
|
||||
BorderBrush="{DynamicResource B.Line}" BorderThickness="0,1,0,0" Padding="12,6">
|
||||
<TextBlock Text="{Binding StatusText}" Foreground="{DynamicResource B.Muted}" FontSize="12"
|
||||
TextTrimming="CharacterEllipsis"/>
|
||||
</Border>
|
||||
|
||||
<!-- ===== 본문 3열: 좌 탭 패널 | 문서 탭+캔버스+플로팅 바 | 속성 ([200] 레이아웃) ===== -->
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="232" MinWidth="180" MaxWidth="480"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="280" MinWidth="220" MaxWidth="560"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- 좌: 알약 탭 — 서식 목록 / 레이어 / 도구 상자 -->
|
||||
<Border Grid.Column="0" Background="{DynamicResource B.Panel}">
|
||||
<TabControl x:Name="LeftTabs" SelectedIndex="{Binding SelectedLeftTabIndex, Mode=TwoWay}">
|
||||
|
||||
<!-- 서식 목록 -->
|
||||
<TabItem>
|
||||
<TabItem.Header><TextBlock Text="서식 목록" Style="{StaticResource TabHeaderText}"/></TabItem.Header>
|
||||
<DockPanel>
|
||||
<DockPanel DockPanel.Dock="Top" Margin="8,8,8,4">
|
||||
<Button DockPanel.Dock="Right" Content="검색" Margin="6,0,0,0"
|
||||
Command="{Binding SearchSheetsCommand}"/>
|
||||
<TextBox Text="{Binding SheetSearchKeyword, UpdateSourceTrigger=PropertyChanged}"
|
||||
ToolTip="서식명/코드 검색 (Enter)">
|
||||
<TextBox.InputBindings>
|
||||
<KeyBinding Key="Enter" Command="{Binding SearchSheetsCommand}"/>
|
||||
</TextBox.InputBindings>
|
||||
</TextBox>
|
||||
</DockPanel>
|
||||
<Grid>
|
||||
<ListBox x:Name="SheetListBox" ItemsSource="{Binding SheetList}"
|
||||
Margin="4,0,4,4"
|
||||
VirtualizingPanel.IsVirtualizing="True"
|
||||
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
|
||||
MouseDoubleClick="OnSheetListDoubleClick"
|
||||
ToolTip="더블클릭으로 열기 — 여러 서식을 탭으로 동시에 편집할 수 있습니다">
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<StackPanel Orientation="Horizontal" Margin="1">
|
||||
<TextBlock Text="{Binding ShtCod}" FontSize="11" Foreground="{DynamicResource B.Muted}"
|
||||
Width="52" VerticalAlignment="Center"/>
|
||||
<TextBlock Text="{Binding Name}" FontSize="12" TextTrimming="CharacterEllipsis">
|
||||
<TextBlock.Style>
|
||||
<Style TargetType="TextBlock" BasedOn="{StaticResource {x:Type TextBlock}}">
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding HasDesign}" Value="False">
|
||||
<Setter Property="Foreground" Value="{DynamicResource B.Muted}"/>
|
||||
<Setter Property="Opacity" Value="0.6"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</TextBlock.Style>
|
||||
</TextBlock>
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
<TextBlock Text="불러오는 중..." HorizontalAlignment="Center" VerticalAlignment="Center"
|
||||
Foreground="{DynamicResource B.Muted}"
|
||||
Visibility="{Binding IsSheetListLoading, Converter={StaticResource BoolToVisibility}}"/>
|
||||
</Grid>
|
||||
</DockPanel>
|
||||
</TabItem>
|
||||
|
||||
<!-- 레이어 (+ 페이지) -->
|
||||
<TabItem>
|
||||
<TabItem.Header><TextBlock Text="레이어" Style="{StaticResource TabHeaderText}"/></TabItem.Header>
|
||||
<DockPanel>
|
||||
<!-- 페이지 섹션 -->
|
||||
<DockPanel DockPanel.Dock="Top" Margin="0,4,0,0">
|
||||
<TextBlock DockPanel.Dock="Top" Text="페이지" FontWeight="Bold" FontSize="11"
|
||||
Margin="10,4,10,4" Foreground="{DynamicResource B.Muted}"/>
|
||||
<StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal" Margin="10,2,10,6">
|
||||
<Button Content="추가" Command="{Binding CurrentDesigner.AddPageCommand}"/>
|
||||
<Button Content="삭제" Margin="6,0,0,0" Command="{Binding CurrentDesigner.RemovePageCommand}"/>
|
||||
</StackPanel>
|
||||
<ListBox ItemsSource="{Binding CurrentDesigner.Pages}"
|
||||
SelectedItem="{Binding CurrentDesigner.SelectedPage}"
|
||||
MaxHeight="110" Margin="4,0">
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<StackPanel Orientation="Horizontal" Margin="2,1">
|
||||
<TextBlock Text="{Binding Index, StringFormat=페이지 {0}}" FontSize="12"/>
|
||||
<TextBlock Text="{Binding SizeText, StringFormat= ({0})}" FontSize="11" Foreground="{DynamicResource B.Muted}"/>
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
</DockPanel>
|
||||
<Border DockPanel.Dock="Top" Height="1" Background="{DynamicResource B.Line}" Margin="8,2"/>
|
||||
<!-- 레이어 목록 (활성 페이지, 그리기 순서) -->
|
||||
<ListBox ItemsSource="{Binding CurrentDesigner.SelectedPage.Controls}"
|
||||
SelectedItem="{Binding CurrentDesigner.SelectedLayerItem}"
|
||||
Margin="4,2,4,6"
|
||||
ScrollViewer.HorizontalScrollBarVisibility="Disabled">
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<DockPanel Margin="0,1">
|
||||
<CheckBox DockPanel.Dock="Right" IsChecked="{Binding IsLockedFlag}"
|
||||
ToolTip="잠금" Margin="4,0,0,0"/>
|
||||
<CheckBox DockPanel.Dock="Right" IsChecked="{Binding IsHiddenFlag}"
|
||||
ToolTip="숨김" Margin="4,0,0,0"/>
|
||||
<TextBlock Text="{Binding Id}" FontSize="12" TextTrimming="CharacterEllipsis"/>
|
||||
</DockPanel>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
</DockPanel>
|
||||
</TabItem>
|
||||
|
||||
<!-- 도구 상자 ([200] 타일 카드 룩 — 드래그/더블클릭 배치) -->
|
||||
<TabItem>
|
||||
<TabItem.Header><TextBlock Text="도구 상자" Style="{StaticResource TabHeaderText}"/></TabItem.Header>
|
||||
<ListBox ItemsSource="{Binding Source={StaticResource PaletteGrouped}}"
|
||||
ItemContainerStyle="{StaticResource PaletteTile}"
|
||||
Margin="6,4,6,6"
|
||||
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
|
||||
ToolTip="더블클릭 또는 드래그로 캔버스에 배치">
|
||||
<b:Interaction.Behaviors>
|
||||
<bh:PaletteDragBehavior/>
|
||||
</b:Interaction.Behaviors>
|
||||
<!-- 그룹 세로 나열(GroupStyle.Panel 기본) + 그룹 내 타일 2열(ItemsPanel — 그룹 내부 항목이 상속) -->
|
||||
<ListBox.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<UniformGrid Columns="2"/>
|
||||
</ItemsPanelTemplate>
|
||||
</ListBox.ItemsPanel>
|
||||
<ListBox.GroupStyle>
|
||||
<GroupStyle>
|
||||
<GroupStyle.HeaderTemplate>
|
||||
<DataTemplate>
|
||||
<TextBlock Text="{Binding Name}" Style="{StaticResource PaletteGroupHeader}"/>
|
||||
</DataTemplate>
|
||||
</GroupStyle.HeaderTemplate>
|
||||
</GroupStyle>
|
||||
</ListBox.GroupStyle>
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<StackPanel HorizontalAlignment="Center" Margin="2,9,2,8" Background="Transparent">
|
||||
<ContentControl Content="{Binding Type, Converter={StaticResource TypeToIcon}}"
|
||||
HorizontalAlignment="Center" Margin="0,0,0,5" IsTabStop="False" Focusable="False"/>
|
||||
<TextBlock Text="{Binding DisplayName}" FontSize="11" TextAlignment="Center"
|
||||
HorizontalAlignment="Center" TextWrapping="Wrap"/>
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
</TabItem>
|
||||
</TabControl>
|
||||
</Border>
|
||||
|
||||
<GridSplitter Grid.Column="1" Style="{StaticResource ColSplitter}" ToolTip="좌측 패널 너비 조절"/>
|
||||
|
||||
<!-- 중앙: 문서 탭(크롬식) + 캔버스 + 플로팅 팔레트 바 -->
|
||||
<Grid Grid.Column="2" Background="{DynamicResource B.CanvasBg}">
|
||||
<TabControl Style="{StaticResource DocTabs}"
|
||||
ItemsSource="{Binding OpenDesigners}"
|
||||
SelectedItem="{Binding CurrentDesigner}"
|
||||
ItemContainerStyle="{StaticResource DocTabItem}">
|
||||
<TabControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<ContentControl Content="{Binding Source=file-text, Converter={StaticResource IconName}, ConverterParameter=12}"
|
||||
Margin="0,0,6,0" VerticalAlignment="Center" IsTabStop="False" Focusable="False"/>
|
||||
<TextBlock Text="{Binding DisplayName}" MaxWidth="170" Style="{StaticResource TabHeaderText}"
|
||||
TextTrimming="CharacterEllipsis" VerticalAlignment="Center"/>
|
||||
<Button Content="✕" FontSize="9" Margin="7,0,0,0"
|
||||
Style="{StaticResource Subtle}" Padding="3,0" Cursor="Hand" ToolTip="문서 닫기"
|
||||
Command="{Binding DataContext.CloseDocumentCommand,
|
||||
RelativeSource={RelativeSource AncestorType=Window}}"
|
||||
CommandParameter="{Binding}"/>
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</TabControl.ItemTemplate>
|
||||
<TabControl.ContentTemplate>
|
||||
<DataTemplate>
|
||||
<v:DesignerCanvasView/>
|
||||
</DataTemplate>
|
||||
</TabControl.ContentTemplate>
|
||||
</TabControl>
|
||||
|
||||
<!-- 플로팅 팔레트 바 (Figma 식 — 컨트롤 추가 플라이아웃 | 줌) -->
|
||||
<Border Background="{DynamicResource B.Surface}" BorderBrush="{DynamicResource B.Line}" BorderThickness="1"
|
||||
CornerRadius="12" Padding="7,5" HorizontalAlignment="Center" VerticalAlignment="Bottom"
|
||||
Margin="0,0,0,20">
|
||||
<Border.Effect>
|
||||
<DropShadowEffect BlurRadius="20" ShadowDepth="3" Opacity="0.35" Color="Black"/>
|
||||
</Border.Effect>
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<ToggleButton Width="38" Height="34" Padding="0" Focusable="False"
|
||||
ToolTip="선택 도구 — 클릭으로 선택, 드래그로 이동"
|
||||
IsChecked="{Binding IsHandTool, Converter={StaticResource InverseBool}, Mode=OneWay}"
|
||||
Click="OnSelectToolClick"
|
||||
Content="{Binding Source=mouse-pointer-click, Converter={StaticResource IconName}, ConverterParameter=18}"/>
|
||||
<ToggleButton Width="38" Height="34" Padding="0" Focusable="False"
|
||||
ToolTip="손 도구 — 드래그로 화면 이동 (Space 누르는 동안 임시)"
|
||||
IsChecked="{Binding IsHandTool, Mode=OneWay}"
|
||||
Click="OnHandToolClick"
|
||||
Content="{Binding Source=hand, Converter={StaticResource IconName}, ConverterParameter=18}"/>
|
||||
<Border Width="1" Height="22" Background="{DynamicResource B.Line}" Margin="5,0" VerticalAlignment="Center"/>
|
||||
<Button x:Name="AddControlBtn" Style="{StaticResource Subtle}" Width="38" Height="34" Padding="0"
|
||||
ToolTip="컨트롤 추가 — 활성 페이지 중앙에 배치" Click="OnAddControlFlyoutClick"
|
||||
Content="{Binding Source=layout-grid, Converter={StaticResource IconName}, ConverterParameter=18}"/>
|
||||
<Border Width="1" Height="22" Background="{DynamicResource B.Line}" Margin="5,0" VerticalAlignment="Center"/>
|
||||
<Button Style="{StaticResource Subtle}" Width="30" Height="34" Padding="0" ToolTip="축소 (Ctrl+휠↓)"
|
||||
Command="{Binding ZoomOutCommand}"
|
||||
Content="{Binding Source=zoom-out, Converter={StaticResource IconName}, ConverterParameter=16}"/>
|
||||
<Button x:Name="ZoomBtn" Style="{StaticResource Subtle}" Height="34" Padding="8,0,6,0" ToolTip="줌"
|
||||
Click="OnZoomFlyoutClick">
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<TextBlock Text="{Binding CurrentDesigner.ZoomPercentText, FallbackValue=100%}"
|
||||
FontSize="12" MinWidth="38" TextAlignment="Center" VerticalAlignment="Center"/>
|
||||
<ContentControl Content="{Binding Source=chevron-down, Converter={StaticResource IconName}, ConverterParameter=11}"
|
||||
Margin="3,1,0,0" VerticalAlignment="Center" IsTabStop="False" Focusable="False"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<Button Style="{StaticResource Subtle}" Width="30" Height="34" Padding="0" ToolTip="확대 (Ctrl+휠↑)"
|
||||
Command="{Binding ZoomInCommand}"
|
||||
Content="{Binding Source=zoom-in, Converter={StaticResource IconName}, ConverterParameter=16}"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- 컨트롤 추가 플라이아웃 (카테고리별 타일) -->
|
||||
<Popup x:Name="AddControlPopup" PlacementTarget="{Binding ElementName=AddControlBtn}" Placement="Top"
|
||||
StaysOpen="False" AllowsTransparency="True" PopupAnimation="Fade" VerticalOffset="-8">
|
||||
<Border Margin="18" Background="{DynamicResource B.Panel}" BorderBrush="{DynamicResource B.Line}"
|
||||
BorderThickness="1" CornerRadius="10" MinWidth="300">
|
||||
<Border.Effect>
|
||||
<DropShadowEffect BlurRadius="16" ShadowDepth="3" Opacity="0.4" Color="Black"/>
|
||||
</Border.Effect>
|
||||
<ScrollViewer MaxHeight="440" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled">
|
||||
<ItemsControl ItemsSource="{Binding Source={StaticResource PaletteGrouped}}" Margin="10,9,10,10">
|
||||
<!-- 그룹 세로 나열 + 그룹 내 타일 4열([200] 플라이아웃 치수) -->
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<UniformGrid Columns="4"/>
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.GroupStyle>
|
||||
<GroupStyle>
|
||||
<GroupStyle.HeaderTemplate>
|
||||
<DataTemplate>
|
||||
<TextBlock Text="{Binding Name}" FontSize="10.5" FontWeight="Bold"
|
||||
Foreground="{DynamicResource B.Muted}" Margin="2,9,0,6"/>
|
||||
</DataTemplate>
|
||||
</GroupStyle.HeaderTemplate>
|
||||
</GroupStyle>
|
||||
</ItemsControl.GroupStyle>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Button Width="66" Margin="3" Padding="4,9,4,8" Click="OnFlyoutTileClick"
|
||||
Background="{DynamicResource B.Surface}" BorderBrush="{DynamicResource B.Line}"
|
||||
ToolTip="{Binding DisplayName}">
|
||||
<StackPanel HorizontalAlignment="Center">
|
||||
<ContentControl Content="{Binding Type, Converter={StaticResource TypeToIcon}}"
|
||||
HorizontalAlignment="Center" Margin="0,0,0,5" IsTabStop="False" Focusable="False"/>
|
||||
<TextBlock Text="{Binding DisplayName}" FontSize="11" TextAlignment="Center"
|
||||
HorizontalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</ScrollViewer>
|
||||
</Border>
|
||||
</Popup>
|
||||
|
||||
<!-- 줌 플라이아웃 -->
|
||||
<Popup x:Name="ZoomPopup" PlacementTarget="{Binding ElementName=ZoomBtn}" Placement="Top"
|
||||
StaysOpen="False" AllowsTransparency="True" PopupAnimation="Fade" VerticalOffset="-8">
|
||||
<Border Margin="18" Background="{DynamicResource B.Panel}" BorderBrush="{DynamicResource B.Line}"
|
||||
BorderThickness="1" CornerRadius="10" MinWidth="190">
|
||||
<Border.Effect>
|
||||
<DropShadowEffect BlurRadius="16" ShadowDepth="3" Opacity="0.4" Color="Black"/>
|
||||
</Border.Effect>
|
||||
<StackPanel Margin="5">
|
||||
<Button Style="{StaticResource FlyoutRow}" Command="{Binding ZoomInCommand}" Click="OnZoomRowClick">
|
||||
<DockPanel LastChildFill="True">
|
||||
<TextBlock DockPanel.Dock="Right" Text="Ctrl+휠↑" Foreground="{DynamicResource B.Muted}"
|
||||
FontSize="11.5" Margin="24,0,0,0" VerticalAlignment="Center"/>
|
||||
<TextBlock Text="확대" FontSize="12.5" VerticalAlignment="Center"/>
|
||||
</DockPanel>
|
||||
</Button>
|
||||
<Button Style="{StaticResource FlyoutRow}" Command="{Binding ZoomOutCommand}" Click="OnZoomRowClick">
|
||||
<DockPanel LastChildFill="True">
|
||||
<TextBlock DockPanel.Dock="Right" Text="Ctrl+휠↓" Foreground="{DynamicResource B.Muted}"
|
||||
FontSize="11.5" Margin="24,0,0,0" VerticalAlignment="Center"/>
|
||||
<TextBlock Text="축소" FontSize="12.5" VerticalAlignment="Center"/>
|
||||
</DockPanel>
|
||||
</Button>
|
||||
<Border Height="1" Background="{DynamicResource B.Line}" Margin="8,5"/>
|
||||
<Button Style="{StaticResource FlyoutRow}" Command="{Binding ZoomResetCommand}" Click="OnZoomRowClick">
|
||||
<TextBlock Text="100%" FontSize="12.5" VerticalAlignment="Center"/>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Popup>
|
||||
</Grid>
|
||||
|
||||
<GridSplitter Grid.Column="3" Style="{StaticResource ColSplitter}" ToolTip="속성 패널 너비 조절"/>
|
||||
|
||||
<!-- 우: 속성 인스펙터 -->
|
||||
<Border Grid.Column="4" Background="{DynamicResource B.Panel}">
|
||||
<DockPanel>
|
||||
<Border DockPanel.Dock="Top" Background="{DynamicResource B.PanelHeader}" Padding="11,8"
|
||||
BorderBrush="{DynamicResource B.Line}" BorderThickness="0,0,0,1">
|
||||
<TextBlock Text="속성" FontWeight="Bold" Foreground="{DynamicResource B.Muted}"/>
|
||||
</Border>
|
||||
<v:InspectorView DataContext="{Binding CurrentDesigner.Inspector}"/>
|
||||
</DockPanel>
|
||||
</Border>
|
||||
</Grid>
|
||||
</DockPanel>
|
||||
</Window>
|
||||
@@ -0,0 +1,110 @@
|
||||
using System.Windows;
|
||||
using SheetMe.Data.Stores;
|
||||
using SheetMe.Designer.Services;
|
||||
using SheetMe.Designer.ViewModels;
|
||||
|
||||
namespace SheetMe.Designer.Views;
|
||||
|
||||
/// <summary>
|
||||
/// 메인 셸 윈도우 — 커스텀 타이틀바(로고+메뉴) | 좌(서식 목록/도구상자/페이지/레이어) | 문서 탭 | 인스펙터.
|
||||
/// [200]SheetMe 크롬(WindowChrome CaptionHeight 40) 이식 — 창 제어/테마 토글은 뷰 전용 관심사라 코드비하인드 처리.
|
||||
/// </summary>
|
||||
public partial class MainView : Window
|
||||
{
|
||||
public MainView()
|
||||
{
|
||||
InitializeComponent();
|
||||
// StaysOpen=False 팝업은 바깥 클릭(버튼 포함)으로 먼저 닫힌다 — 토글 버튼 재클릭이 곧바로 다시 열지 않게 닫힌 시각 기록
|
||||
AddControlPopup.Closed += (_, _) => addControlPopupClosedAt = Environment.TickCount;
|
||||
ZoomPopup.Closed += (_, _) => zoomPopupClosedAt = Environment.TickCount;
|
||||
}
|
||||
|
||||
/// <summary>기동 — 테마 메뉴 체크 동기화 + 뷰모델 Loaded 커맨드 실행(구 CustomWindow.LoadedCommand 대체)</summary>
|
||||
private void OnWindowLoaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
ThemeMenuItem.IsChecked = !ThemeManager.IsLight;
|
||||
if (DataContext is MainViewModel viewModel && viewModel.LoadedCommand?.CanExecute(null) == true)
|
||||
{
|
||||
viewModel.LoadedCommand.Execute(null);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>서식 목록 더블클릭 → 탭으로 열기(뷰모델 위임 — ListBox 더블클릭은 InputBinding 미지원)</summary>
|
||||
private void OnSheetListDoubleClick(object sender, System.Windows.Input.MouseButtonEventArgs e)
|
||||
{
|
||||
if (DataContext is MainViewModel viewModel)
|
||||
{
|
||||
viewModel.OpenSheetFromList(SheetListBox.SelectedItem as SheetSummary);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnThemeToggleClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
ThemeManager.Toggle();
|
||||
ThemeMenuItem.IsChecked = !ThemeManager.IsLight;
|
||||
}
|
||||
|
||||
/// <summary>플로팅 바 — 컨트롤 추가 플라이아웃 토글(닫힘 직후 재클릭 가드)</summary>
|
||||
private void OnAddControlFlyoutClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (Environment.TickCount - addControlPopupClosedAt > 150)
|
||||
{
|
||||
AddControlPopup.IsOpen = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>플로팅 바 — 줌 플라이아웃 토글</summary>
|
||||
private void OnZoomFlyoutClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (Environment.TickCount - zoomPopupClosedAt > 150)
|
||||
{
|
||||
ZoomPopup.IsOpen = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>플라이아웃 타일 클릭 — 활성 페이지 중앙에 컨트롤 배치(팔레트 더블클릭과 동일 경로)</summary>
|
||||
private void OnFlyoutTileClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
AddControlPopup.IsOpen = false;
|
||||
if ((sender as FrameworkElement)?.DataContext is SheetMe.Core.Catalog.ControlDescriptor item
|
||||
&& DataContext is MainViewModel viewModel)
|
||||
{
|
||||
viewModel.CurrentDesigner?.AddPaletteItemAtCenter(item.Type);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>줌 플라이아웃 행 클릭 — 커맨드 실행 후 팝업 닫기</summary>
|
||||
private void OnZoomRowClick(object sender, RoutedEventArgs e) => ZoomPopup.IsOpen = false;
|
||||
|
||||
/// <summary>플로팅 바 — 선택 도구(기본)</summary>
|
||||
private void OnSelectToolClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (DataContext is MainViewModel viewModel)
|
||||
{
|
||||
viewModel.IsHandTool = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>플로팅 바 — 손(팬) 도구</summary>
|
||||
private void OnHandToolClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (DataContext is MainViewModel viewModel)
|
||||
{
|
||||
viewModel.IsHandTool = true;
|
||||
}
|
||||
}
|
||||
|
||||
private int addControlPopupClosedAt;
|
||||
private int zoomPopupClosedAt;
|
||||
|
||||
private void OnMinimizeClick(object sender, RoutedEventArgs e) => WindowState = WindowState.Minimized;
|
||||
|
||||
private void OnMaxRestoreClick(object sender, RoutedEventArgs e) =>
|
||||
WindowState = WindowState == WindowState.Maximized ? WindowState.Normal : WindowState.Maximized;
|
||||
|
||||
private void OnCloseWinClick(object sender, RoutedEventArgs e) => Close();
|
||||
|
||||
/// <summary>최대화/복원 글리프 전환(Segoe MDL2: E922=최대화, E923=복원)</summary>
|
||||
private void OnWindowStateChanged(object sender, EventArgs e) =>
|
||||
MaxGlyph.Text = WindowState == WindowState.Maximized ? "" : "";
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<UserControl x:Class="SheetMe.Designer.Views.PageView"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:vm="clr-namespace:SheetMe.Designer.ViewModels"
|
||||
mc:Ignorable="d"
|
||||
d:DataContext="{d:DesignInstance vm:PageViewModel}">
|
||||
|
||||
<!-- 종이 위 렌더는 레거시 충실 유지 — 앱 다크 테마의 암시 TextBlock 스타일(B.Ink 밝은 글자)이
|
||||
페이지 콘텐츠에 스미지 않게 여기서 기본(검정) 암시 스타일로 차단 -->
|
||||
<UserControl.Resources>
|
||||
<Style TargetType="TextBlock">
|
||||
<Setter Property="Foreground" Value="Black"/>
|
||||
</Style>
|
||||
</UserControl.Resources>
|
||||
|
||||
<!-- 용지 1장: 그림자 + 종이 + 컨트롤 층. 텍스트 선명도는 Ideal 포맷팅 -->
|
||||
<Grid Width="{Binding WidthDip}" Height="{Binding HeightDip}"
|
||||
TextOptions.TextFormattingMode="Ideal">
|
||||
|
||||
<!-- 종이 그림자 (Effect 대신 오프셋 사각형 — 성능) -->
|
||||
<Border Margin="3,3,-3,-3" Background="#22000000"/>
|
||||
|
||||
<!-- 종이 -->
|
||||
<Border Background="{Binding PaperBrush}" BorderBrush="#D8DCE2" BorderThickness="1"/>
|
||||
|
||||
<!-- 컨트롤 층 (그리기 순서 = 컬렉션 순서, 마지막이 최상위) -->
|
||||
<ItemsControl ItemsSource="{Binding Controls}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<Canvas/>
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemContainerStyle>
|
||||
<Style TargetType="ContentPresenter">
|
||||
<Setter Property="Canvas.Left" Value="{Binding X}"/>
|
||||
<Setter Property="Canvas.Top" Value="{Binding Y}"/>
|
||||
<Setter Property="Width" Value="{Binding Width}"/>
|
||||
<Setter Property="Height" Value="{Binding Height}"/>
|
||||
<Setter Property="Opacity" Value="{Binding DesignOpacity}"/>
|
||||
</Style>
|
||||
</ItemsControl.ItemContainerStyle>
|
||||
</ItemsControl>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,12 @@
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace SheetMe.Designer.Views;
|
||||
|
||||
/// <summary>용지 1장 렌더 뷰 — DataContext 는 PageViewModel.</summary>
|
||||
public partial class PageView : UserControl
|
||||
{
|
||||
public PageView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<Window x:Class="SheetMe.Designer.Views.PreviewWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
Title="미리보기" Width="900" Height="1000"
|
||||
WindowStartupLocation="CenterOwner" Background="{DynamicResource B.CanvasBg}">
|
||||
<DockPanel>
|
||||
<ToolBarTray DockPanel.Dock="Top">
|
||||
<ToolBar>
|
||||
<Button Content="인쇄..." Padding="10,3" Click="OnPrint"/>
|
||||
<Separator/>
|
||||
<Button Content="-" Padding="8,3" Click="OnZoomOut"/>
|
||||
<TextBlock x:Name="ZoomText" Text="100%" VerticalAlignment="Center" Margin="6,0" MinWidth="42" TextAlignment="Center"/>
|
||||
<Button Content="+" Padding="8,3" Click="OnZoomIn"/>
|
||||
</ToolBar>
|
||||
</ToolBarTray>
|
||||
|
||||
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto">
|
||||
<StackPanel x:Name="PagesHost" Margin="24" HorizontalAlignment="Center">
|
||||
<StackPanel.LayoutTransform>
|
||||
<ScaleTransform x:Name="ZoomTransform" ScaleX="1" ScaleY="1"/>
|
||||
</StackPanel.LayoutTransform>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</DockPanel>
|
||||
</Window>
|
||||
@@ -0,0 +1,73 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Documents;
|
||||
using SheetMe.Designer.Services;
|
||||
using SheetMe.Designer.ViewModels;
|
||||
|
||||
namespace SheetMe.Designer.Views;
|
||||
|
||||
/// <summary>미리보기 창 — 편집 크롬 없는 페이지 렌더(인쇄와 동일 비주얼) + 줌/인쇄.</summary>
|
||||
public partial class PreviewWindow : Window
|
||||
{
|
||||
#region Member Fields
|
||||
private readonly DesignerViewModel designer;
|
||||
private double zoom = 1.0;
|
||||
#endregion
|
||||
|
||||
#region Constructors
|
||||
public PreviewWindow(DesignerViewModel designer)
|
||||
{
|
||||
this.designer = designer;
|
||||
InitializeComponent();
|
||||
BuildPages();
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Methods
|
||||
private void BuildPages()
|
||||
{
|
||||
PagesHost.Children.Clear();
|
||||
foreach (var page in designer.Pages)
|
||||
{
|
||||
var frame = new Border
|
||||
{
|
||||
Background = page.PaperBrush,
|
||||
BorderBrush = System.Windows.Media.Brushes.LightGray,
|
||||
BorderThickness = new Thickness(1),
|
||||
Margin = new Thickness(0, 0, 0, 20),
|
||||
Child = PrintService.BuildPageVisual(page),
|
||||
Width = page.WidthDip,
|
||||
Height = page.HeightDip,
|
||||
};
|
||||
PagesHost.Children.Add(frame);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPrint(object sender, RoutedEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
PrintService.Print(designer, designer.Document.Title.Length > 0
|
||||
? designer.Document.Title
|
||||
: designer.Document.FormId);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show($"인쇄 중 오류가 발생했습니다.\n\n{ex.Message}", "오류",
|
||||
MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnZoomIn(object sender, RoutedEventArgs e) => ApplyZoom(zoom * 1.15);
|
||||
|
||||
private void OnZoomOut(object sender, RoutedEventArgs e) => ApplyZoom(zoom / 1.15);
|
||||
|
||||
private void ApplyZoom(double value)
|
||||
{
|
||||
zoom = Math.Clamp(value, 0.25, 3.0);
|
||||
ZoomTransform.ScaleX = zoom;
|
||||
ZoomTransform.ScaleY = zoom;
|
||||
ZoomText.Text = $"{zoom * 100:0}%";
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<Window x:Class="SheetMe.Designer.Views.QueryEditorWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
Title="쿼리 편집" Width="860" Height="560" MinWidth="640" MinHeight="400"
|
||||
WindowStartupLocation="CenterOwner" ShowInTaskbar="False">
|
||||
<DockPanel Margin="12">
|
||||
<TextBlock DockPanel.Dock="Top" Foreground="{DynamicResource B.Muted}" TextWrapping="Wrap" Margin="0,0,0,8"
|
||||
Text="작성 시점에 <<변수>> 가 실제 값으로 치환되어 실행됩니다. 우측 변수를 더블클릭하면 커서 위치에 삽입됩니다."/>
|
||||
|
||||
<StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,10,0,0">
|
||||
<TextBlock x:Name="LengthText" VerticalAlignment="Center" Margin="0,0,12,0" Foreground="{DynamicResource B.Muted}"/>
|
||||
<Button Content="확인" Padding="20,5" Click="OnConfirm"/>
|
||||
<Button Content="취소" Padding="20,5" Margin="8,0,0,0" IsCancel="True"/>
|
||||
</StackPanel>
|
||||
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="8"/>
|
||||
<ColumnDefinition Width="230"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- SQL 편집 영역 -->
|
||||
<TextBox x:Name="SqlBox" Grid.Column="0"
|
||||
FontFamily="Consolas, D2Coding, 굴림체" FontSize="13"
|
||||
AcceptsReturn="True" AcceptsTab="True"
|
||||
TextWrapping="NoWrap"
|
||||
HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto"
|
||||
TextChanged="OnSqlChanged"/>
|
||||
|
||||
<GridSplitter Grid.Column="1" HorizontalAlignment="Stretch" Background="{DynamicResource B.Line}"/>
|
||||
|
||||
<!-- 치환 변수 목록 -->
|
||||
<DockPanel Grid.Column="2">
|
||||
<TextBlock DockPanel.Dock="Top" Text="치환 변수 (더블클릭 삽입)" FontWeight="Bold"
|
||||
Foreground="{DynamicResource B.Muted}" Margin="4,0,0,6"/>
|
||||
<ListBox x:Name="VariableList" FontSize="12" MouseDoubleClick="OnInsertVariable"
|
||||
FontFamily="Consolas, D2Coding, 굴림체"/>
|
||||
</DockPanel>
|
||||
</Grid>
|
||||
</DockPanel>
|
||||
</Window>
|
||||
@@ -0,0 +1,82 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace SheetMe.Designer.Views;
|
||||
|
||||
/// <summary>
|
||||
/// 데이터소스(MDataTable) 쿼리 전용 편집기 — 큰 SQL 편집 영역 + 치환 변수 삽입.
|
||||
/// 치환 규칙 원본: [014]EMRLoader bzDesignSheetLoader.ConvertQuery — <<PatientInfo/SheetInfo/WorkInfo.속성>> 리플렉션 치환.
|
||||
/// 변수 목록 출처: [021]SheetLoadOperatingInfo 의 bzPatientInfo/bzSheetInfo/bzWorkInfo 공개 속성(스칼라만 큐레이션, 2026-07-16).
|
||||
/// </summary>
|
||||
public partial class QueryEditorWindow : Window
|
||||
{
|
||||
#region Member Fields
|
||||
/// <summary>치환 변수 목록 — 작성 시점 실제 값으로 치환됨</summary>
|
||||
private static readonly string[] Variables =
|
||||
{
|
||||
"<<PatientInfo.ChtNum>>",
|
||||
"<<PatientInfo.ComNum>>",
|
||||
"<<PatientInfo.PatTyp>>",
|
||||
"<<PatientInfo.OdrNum>>",
|
||||
"<<PatientInfo.OdrSeq>>",
|
||||
"<<SheetInfo.ShtCod>>",
|
||||
"<<SheetInfo.ShtNam>>",
|
||||
"<<SheetInfo.EmrKey>>",
|
||||
"<<SheetInfo.EmrGbn>>",
|
||||
"<<SheetInfo.AdpDtm>>",
|
||||
"<<SheetInfo.PatTyp>>",
|
||||
"<<SheetInfo.OdrNum>>",
|
||||
"<<SheetInfo.OdrSeq>>",
|
||||
"<<SheetInfo.TemKey>>",
|
||||
"<<WorkInfo.WrkUid>>",
|
||||
"<<WorkInfo.WrkNam>>",
|
||||
"<<WorkInfo.WrkDte>>",
|
||||
"<<WorkInfo.WrkDtm>>",
|
||||
"<<WorkInfo.AdpDep>>",
|
||||
"<<WorkInfo.AdpDtm>>",
|
||||
};
|
||||
#endregion
|
||||
|
||||
#region Properties
|
||||
/// <summary>편집 결과 SQL — 확인 시 채워짐</summary>
|
||||
public string QueryText { get; private set; } = string.Empty;
|
||||
#endregion
|
||||
|
||||
#region Constructors
|
||||
public QueryEditorWindow(string ownerLabel, string initialQuery)
|
||||
{
|
||||
InitializeComponent();
|
||||
Title = $"쿼리 편집 — {ownerLabel}";
|
||||
SqlBox.Text = initialQuery;
|
||||
VariableList.ItemsSource = Variables;
|
||||
Loaded += (_, _) =>
|
||||
{
|
||||
SqlBox.Focus();
|
||||
SqlBox.CaretIndex = SqlBox.Text.Length;
|
||||
};
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Methods
|
||||
private void OnSqlChanged(object sender, TextChangedEventArgs e)
|
||||
=> LengthText.Text = $"{SqlBox.Text.Length:N0}자";
|
||||
|
||||
private void OnInsertVariable(object sender, System.Windows.Input.MouseButtonEventArgs e)
|
||||
{
|
||||
if (VariableList.SelectedItem is not string variable)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var caret = SqlBox.CaretIndex;
|
||||
SqlBox.Text = SqlBox.Text.Insert(caret, variable);
|
||||
SqlBox.CaretIndex = caret + variable.Length;
|
||||
SqlBox.Focus();
|
||||
}
|
||||
|
||||
private void OnConfirm(object sender, RoutedEventArgs e)
|
||||
{
|
||||
QueryText = SqlBox.Text;
|
||||
DialogResult = true;
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<Window x:Class="SheetMe.Designer.Views.RecordWordDialogView"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
Title="상용구 관리" Width="560" Height="520" MinWidth="480" MinHeight="380"
|
||||
WindowStartupLocation="CenterOwner" ShowInTaskbar="False">
|
||||
<DockPanel Margin="12">
|
||||
<TextBlock x:Name="HeaderText" DockPanel.Dock="Top" Foreground="{DynamicResource B.Muted}"
|
||||
TextWrapping="Wrap" Margin="0,0,0,8"/>
|
||||
|
||||
<!-- 입력/버튼 -->
|
||||
<DockPanel DockPanel.Dock="Bottom" Margin="0,8,0,0">
|
||||
<Button DockPanel.Dock="Right" Content="닫기" Padding="16,4" Margin="8,0,0,0" IsCancel="True"/>
|
||||
<Button DockPanel.Dock="Right" Content="추가" Padding="16,4" Click="OnAdd" IsDefault="True"/>
|
||||
<TextBox x:Name="NewWordBox" Padding="4,3" Margin="0,0,8,0"
|
||||
VerticalContentAlignment="Center"
|
||||
ToolTip="새 상용구 입력 후 Enter 또는 추가"/>
|
||||
</DockPanel>
|
||||
|
||||
<!-- 목록 + 우측 조작 -->
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<ListBox x:Name="WordList" Grid.Column="0" FontSize="13"
|
||||
MouseDoubleClick="OnEdit"
|
||||
ScrollViewer.HorizontalScrollBarVisibility="Disabled">
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<TextBlock Text="{Binding Value}" TextWrapping="Wrap" Margin="2"/>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
|
||||
<StackPanel Grid.Column="1" Margin="8,0,0,0">
|
||||
<Button Content="수정" Padding="10,3" Click="OnEdit"/>
|
||||
<Button Content="삭제" Padding="10,3" Margin="0,6,0,0" Click="OnRemove"/>
|
||||
<Separator Margin="0,10"/>
|
||||
<Button Content="위로 ▲" Padding="10,3" Click="OnMoveUp"/>
|
||||
<Button Content="아래로 ▼" Padding="10,3" Margin="0,6,0,0" Click="OnMoveDown"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</DockPanel>
|
||||
</Window>
|
||||
@@ -0,0 +1,154 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Windows;
|
||||
using SheetMe.Data.Stores;
|
||||
|
||||
namespace SheetMe.Designer.Views;
|
||||
|
||||
/// <summary>
|
||||
/// 서식별 상용구 관리 대화상자 — E_SHTWRDMST 1단계 문구 추가/수정/삭제/순서.
|
||||
/// 레거시 런타임 상용구 팝업(MRecordWord)이 그대로 읽어가는 데이터를 관리한다.
|
||||
/// </summary>
|
||||
public partial class RecordWordDialogView : Window
|
||||
{
|
||||
#region Member Fields
|
||||
private readonly RecordWordStore store;
|
||||
private readonly string shtCod;
|
||||
private readonly ObservableCollection<RecordWordInfo> words = new();
|
||||
#endregion
|
||||
|
||||
#region Constructors
|
||||
public RecordWordDialogView(RecordWordStore store, string shtCod, string sheetTitle)
|
||||
{
|
||||
this.store = store;
|
||||
this.shtCod = shtCod;
|
||||
InitializeComponent();
|
||||
HeaderText.Text = $"[{shtCod}] {sheetTitle} — 작성 화면의 상용구 팝업에 표시되는 문구입니다. " +
|
||||
"(텍스트박스의 '상용구 사용'이 켜져 있어야 합니다)";
|
||||
WordList.ItemsSource = words;
|
||||
Loaded += (_, _) => Reload();
|
||||
NewWordBox.Loaded += (_, _) => NewWordBox.Focus();
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Methods
|
||||
private void Reload()
|
||||
{
|
||||
try
|
||||
{
|
||||
words.Clear();
|
||||
foreach (var word in store.List(shtCod))
|
||||
{
|
||||
words.Add(word);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show($"상용구 조회 중 오류가 발생했습니다.\n\n{ex.Message}", "오류",
|
||||
MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnAdd(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var value = NewWordBox.Text.Trim();
|
||||
if (value.Length == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
try
|
||||
{
|
||||
store.Add(shtCod, value, Environment.UserName);
|
||||
NewWordBox.Clear();
|
||||
NewWordBox.Focus();
|
||||
Reload();
|
||||
WordList.SelectedIndex = words.Count - 1;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show($"추가 중 오류가 발생했습니다.\n\n{ex.Message}", "오류",
|
||||
MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnEdit(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (WordList.SelectedItem is not RecordWordInfo selected)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var editor = new QueryEditorWindow($"상용구 수정", selected.Value) { Owner = this };
|
||||
// QueryEditorWindow 재사용(큰 편집 영역) — 변수 삽입은 무시해도 무해
|
||||
if (editor.ShowDialog() != true)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var newValue = editor.QueryText.Trim();
|
||||
if (newValue.Length == 0 || newValue == selected.Value)
|
||||
{
|
||||
return;
|
||||
}
|
||||
try
|
||||
{
|
||||
store.Update(selected.Key, newValue, Environment.UserName);
|
||||
Reload();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show($"수정 중 오류가 발생했습니다.\n\n{ex.Message}", "오류",
|
||||
MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnRemove(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (WordList.SelectedItem is not RecordWordInfo selected)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (MessageBox.Show($"삭제할까요?\n\n{Truncate(selected.Value)}", "상용구 삭제",
|
||||
MessageBoxButton.YesNo, MessageBoxImage.Question) != MessageBoxResult.Yes)
|
||||
{
|
||||
return;
|
||||
}
|
||||
try
|
||||
{
|
||||
store.Remove(selected.Key);
|
||||
Reload();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show($"삭제 중 오류가 발생했습니다.\n\n{ex.Message}", "오류",
|
||||
MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnMoveUp(object sender, RoutedEventArgs e) => Move(-1);
|
||||
|
||||
private void OnMoveDown(object sender, RoutedEventArgs e) => Move(+1);
|
||||
|
||||
private void Move(int delta)
|
||||
{
|
||||
var index = WordList.SelectedIndex;
|
||||
var target = index + delta;
|
||||
if (index < 0 || target < 0 || target >= words.Count)
|
||||
{
|
||||
return;
|
||||
}
|
||||
try
|
||||
{
|
||||
words.Move(index, target);
|
||||
store.Reorder(words.Select(w => w.Key).ToList(), Environment.UserName);
|
||||
WordList.SelectedIndex = target;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show($"순서 변경 중 오류가 발생했습니다.\n\n{ex.Message}", "오류",
|
||||
MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
Reload();
|
||||
}
|
||||
}
|
||||
|
||||
private static string Truncate(string value)
|
||||
=> value.Length > 80 ? value[..80] + "…" : value;
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<Window x:Class="SheetMe.Designer.Views.RegisterSheetDialogView"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
Title="신규 서식 등록" Width="420" SizeToContent="Height"
|
||||
WindowStartupLocation="CenterOwner" ShowInTaskbar="False" ResizeMode="NoResize">
|
||||
<StackPanel Margin="16">
|
||||
<TextBlock Text="E_ShtMst 에 등록되지 않은 서식입니다. 신규 등록 후 저장합니다."
|
||||
TextWrapping="Wrap" Foreground="{DynamicResource B.Muted}" Margin="0,0,0,12"/>
|
||||
|
||||
<DockPanel Margin="0,3">
|
||||
<TextBlock Text="서식 코드" Width="80" VerticalAlignment="Center"/>
|
||||
<TextBox x:Name="CodeBox" MaxLength="10" Padding="4,3"/>
|
||||
</DockPanel>
|
||||
<DockPanel Margin="0,3">
|
||||
<TextBlock Text="서식 명칭" Width="80" VerticalAlignment="Center"/>
|
||||
<TextBox x:Name="NameBox" MaxLength="100" Padding="4,3"/>
|
||||
</DockPanel>
|
||||
<DockPanel Margin="0,3">
|
||||
<TextBlock Text="분류 코드" Width="80" VerticalAlignment="Center"/>
|
||||
<TextBox x:Name="ClassBox" MaxLength="5" Padding="4,3"
|
||||
ToolTip="선택 입력 — 예: A(초진), J(진단서류) 등 병원 분류 체계"/>
|
||||
</DockPanel>
|
||||
|
||||
<TextBlock Text="기본값: 디자인 서식(ShtTyp='D'), 사용 'Y', 서식생성기 사용 'Y' — 상세 속성은 기록지정보 마스터에서 설정"
|
||||
TextWrapping="Wrap" Foreground="{DynamicResource B.Muted}" FontSize="11" Margin="0,10,0,0"/>
|
||||
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,14,0,0">
|
||||
<Button Content="등록 후 저장" Padding="14,5" Click="OnConfirm" IsDefault="True"/>
|
||||
<Button Content="취소" Padding="14,5" Margin="8,0,0,0" IsCancel="True"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Window>
|
||||
@@ -0,0 +1,39 @@
|
||||
using System.Windows;
|
||||
|
||||
namespace SheetMe.Designer.Views;
|
||||
|
||||
/// <summary>신규 서식(E_ShtMst) 최소 등록 대화상자 — 코드/명칭/분류 입력.</summary>
|
||||
public partial class RegisterSheetDialogView : Window
|
||||
{
|
||||
#region Properties
|
||||
/// <summary>서식 코드</summary>
|
||||
public string SheetCode => CodeBox.Text.Trim();
|
||||
|
||||
/// <summary>서식 명칭</summary>
|
||||
public string SheetName => NameBox.Text.Trim();
|
||||
|
||||
/// <summary>분류 코드(선택)</summary>
|
||||
public string? ClassCode => ClassBox.Text.Trim().Length == 0 ? null : ClassBox.Text.Trim();
|
||||
#endregion
|
||||
|
||||
#region Constructors
|
||||
public RegisterSheetDialogView(string initialCode, string initialName)
|
||||
{
|
||||
InitializeComponent();
|
||||
CodeBox.Text = initialCode;
|
||||
NameBox.Text = initialName;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Methods
|
||||
private void OnConfirm(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (SheetCode.Length == 0 || SheetName.Length == 0)
|
||||
{
|
||||
MessageBox.Show("서식 코드와 명칭을 입력하세요.", "신규 서식 등록");
|
||||
return;
|
||||
}
|
||||
DialogResult = true;
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
<UserControl x:Class="SheetMe.Designer.Views.SelectionOverlayView"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:vm="clr-namespace:SheetMe.Designer.ViewModels"
|
||||
mc:Ignorable="d"
|
||||
d:DataContext="{d:DesignInstance vm:SelectionOverlayViewModel}"
|
||||
IsHitTestVisible="False">
|
||||
|
||||
<!-- 선택/마퀴/가이드 표시 전용 오버레이 — 입력은 월드 파이프라인이 처리(기하 히트테스트) -->
|
||||
<Canvas>
|
||||
|
||||
<!-- 정렬 가이드선 -->
|
||||
<ItemsControl ItemsSource="{Binding Guides}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<Canvas/>
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Line Stroke="#FF4FA3" StrokeThickness="1" StrokeDashArray="4 3">
|
||||
<Line.Style>
|
||||
<Style TargetType="Line">
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding IsVertical}" Value="True">
|
||||
<Setter Property="X1" Value="{Binding Position}"/>
|
||||
<Setter Property="X2" Value="{Binding Position}"/>
|
||||
<Setter Property="Y1" Value="-100000"/>
|
||||
<Setter Property="Y2" Value="100000"/>
|
||||
</DataTrigger>
|
||||
<DataTrigger Binding="{Binding IsVertical}" Value="False">
|
||||
<Setter Property="Y1" Value="{Binding Position}"/>
|
||||
<Setter Property="Y2" Value="{Binding Position}"/>
|
||||
<Setter Property="X1" Value="-100000"/>
|
||||
<Setter Property="X2" Value="100000"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Line.Style>
|
||||
</Line>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
|
||||
<!-- 선택 박스 -->
|
||||
<Rectangle Canvas.Left="{Binding SelX}" Canvas.Top="{Binding SelY}"
|
||||
Width="{Binding SelW}" Height="{Binding SelH}"
|
||||
Stroke="#1E7BE8" StrokeThickness="1"
|
||||
Visibility="{Binding HasSelection, Converter={StaticResource BoolToVisibility}}"/>
|
||||
|
||||
<!-- 리사이즈 핸들 8개 -->
|
||||
<ItemsControl ItemsSource="{Binding Handles}"
|
||||
Visibility="{Binding ShowHandles, Converter={StaticResource BoolToVisibility}}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<Canvas/>
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemContainerStyle>
|
||||
<Style TargetType="ContentPresenter">
|
||||
<Setter Property="Canvas.Left" Value="{Binding X}"/>
|
||||
<Setter Property="Canvas.Top" Value="{Binding Y}"/>
|
||||
</Style>
|
||||
</ItemsControl.ItemContainerStyle>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Rectangle Width="8" Height="8" Fill="White" Stroke="#1E7BE8" StrokeThickness="1"/>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
|
||||
<!-- 마퀴(러버밴드) -->
|
||||
<Rectangle Canvas.Left="{Binding MarX}" Canvas.Top="{Binding MarY}"
|
||||
Width="{Binding MarW}" Height="{Binding MarH}"
|
||||
Stroke="#1E7BE8" StrokeThickness="1" StrokeDashArray="3 2" Fill="#181E7BE8"
|
||||
Visibility="{Binding HasMarquee, Converter={StaticResource BoolToVisibility}}"/>
|
||||
|
||||
<!-- 탭순서 배지 (편집 모드 전용) -->
|
||||
<ItemsControl ItemsSource="{Binding TabBadges}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<Canvas/>
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemContainerStyle>
|
||||
<Style TargetType="ContentPresenter">
|
||||
<Setter Property="Canvas.Left" Value="{Binding X}"/>
|
||||
<Setter Property="Canvas.Top" Value="{Binding Y}"/>
|
||||
</Style>
|
||||
</ItemsControl.ItemContainerStyle>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Border MinWidth="18" Height="18" CornerRadius="9" Padding="4,0"
|
||||
BorderThickness="1.5" BorderBrush="White">
|
||||
<Border.Style>
|
||||
<Style TargetType="Border">
|
||||
<Setter Property="Background" Value="#9AA6B4"/>
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding IsAssigned}" Value="True">
|
||||
<Setter Property="Background" Value="#1E7BE8"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Border.Style>
|
||||
<TextBlock Text="{Binding Text}" Foreground="White" FontSize="10" FontWeight="Bold"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</Canvas>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,12 @@
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace SheetMe.Designer.Views;
|
||||
|
||||
/// <summary>선택/마퀴/가이드 오버레이 — 표시 전용(입력 처리 없음).</summary>
|
||||
public partial class SelectionOverlayView : UserControl
|
||||
{
|
||||
public SelectionOverlayView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<Window x:Class="SheetMe.Designer.Views.SheetHistoryDialogView"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
Title="서식 수정이력" Width="560" Height="520" MinWidth="480" MinHeight="380"
|
||||
WindowStartupLocation="CenterOwner" ShowInTaskbar="False">
|
||||
<DockPanel Margin="12">
|
||||
<TextBlock x:Name="HeaderText" DockPanel.Dock="Top" Foreground="{DynamicResource B.Muted}"
|
||||
TextWrapping="Wrap" Margin="0,0,0,8"/>
|
||||
|
||||
<StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,10,0,0">
|
||||
<TextBlock x:Name="CountText" VerticalAlignment="Center" Margin="0,0,12,0" Foreground="{DynamicResource B.Muted}"/>
|
||||
<Button Content="열람(새 탭)" Padding="16,4" Click="OnOpen" IsDefault="True"
|
||||
ToolTip="선택한 버전을 새 탭으로 엽니다 — 열람 후 'DB에 저장'하면 그 내용이 새 활성 버전이 됩니다(복원)"/>
|
||||
<Button Content="닫기" Padding="16,4" Margin="8,0,0,0" IsCancel="True"/>
|
||||
</StackPanel>
|
||||
|
||||
<ListView x:Name="VersionList" MouseDoubleClick="OnOpen"
|
||||
VirtualizingPanel.IsVirtualizing="True">
|
||||
<ListView.View>
|
||||
<GridView>
|
||||
<GridViewColumn Header="버전(SdgKey)" Width="110" DisplayMemberBinding="{Binding SdgKey}"/>
|
||||
<GridViewColumn Header="수정일시" Width="150" DisplayMemberBinding="{Binding UpdDtmText}"/>
|
||||
<GridViewColumn Header="수정자" Width="110" DisplayMemberBinding="{Binding UpdUid}"/>
|
||||
<GridViewColumn Header="상태" Width="80">
|
||||
<GridViewColumn.CellTemplate>
|
||||
<DataTemplate>
|
||||
<TextBlock Text="{Binding StatusText}">
|
||||
<TextBlock.Style>
|
||||
<Style TargetType="TextBlock">
|
||||
<Setter Property="Foreground" Value="{DynamicResource B.Muted}"/>
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding StatusText}" Value="활성">
|
||||
<Setter Property="Foreground" Value="{DynamicResource B.Success}"/>
|
||||
<Setter Property="FontWeight" Value="Bold"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</TextBlock.Style>
|
||||
</TextBlock>
|
||||
</DataTemplate>
|
||||
</GridViewColumn.CellTemplate>
|
||||
</GridViewColumn>
|
||||
</GridView>
|
||||
</ListView.View>
|
||||
</ListView>
|
||||
</DockPanel>
|
||||
</Window>
|
||||
@@ -0,0 +1,77 @@
|
||||
using System.Windows;
|
||||
using SheetMe.Data.Stores;
|
||||
|
||||
namespace SheetMe.Designer.Views;
|
||||
|
||||
/// <summary>이력 목록 표시용 행</summary>
|
||||
public sealed class VersionRow
|
||||
{
|
||||
/// <summary>디자인 버전 키</summary>
|
||||
public decimal SdgKey { get; init; }
|
||||
|
||||
/// <summary>수정일시 표시(yyyy-MM-dd HH:mm)</summary>
|
||||
public string UpdDtmText { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>수정자</summary>
|
||||
public string UpdUid { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>상태 표시(활성/이력)</summary>
|
||||
public string StatusText { get; init; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 서식 수정이력 대화상자 — E_SdgMst 버전 목록(레거시 UcSheetHistory 이식).
|
||||
/// 선택 버전을 새 탭으로 열람하고, 열람본을 'DB에 저장'하면 새 활성 버전이 되어 복원 흐름이 완성된다.
|
||||
/// </summary>
|
||||
public partial class SheetHistoryDialogView : Window
|
||||
{
|
||||
#region Properties
|
||||
/// <summary>선택된 버전 키 — 열람 확정 시 채워짐</summary>
|
||||
public decimal? SelectedSdgKey { get; private set; }
|
||||
#endregion
|
||||
|
||||
#region Constructors
|
||||
public SheetHistoryDialogView(string shtCod, string sheetTitle, List<DesignVersionInfo> versions)
|
||||
{
|
||||
InitializeComponent();
|
||||
HeaderText.Text = $"[{shtCod}] {sheetTitle} — 저장할 때마다 이전 버전이 이력으로 보존됩니다. " +
|
||||
"이력 버전을 열람한 뒤 'DB에 저장'하면 해당 내용이 새 활성 버전이 됩니다(복원).";
|
||||
VersionList.ItemsSource = versions.Select(v => new VersionRow
|
||||
{
|
||||
SdgKey = v.SdgKey,
|
||||
UpdDtmText = FormatDtm(v.UpdDtm),
|
||||
UpdUid = v.UpdUid,
|
||||
StatusText = v.Deleted ? "이력" : "활성",
|
||||
}).ToList();
|
||||
CountText.Text = $"{versions.Count}개 버전";
|
||||
if (VersionList.Items.Count > 0)
|
||||
{
|
||||
VersionList.SelectedIndex = 0;
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Methods
|
||||
private void OnOpen(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (VersionList.SelectedItem is not VersionRow row)
|
||||
{
|
||||
return;
|
||||
}
|
||||
SelectedSdgKey = row.SdgKey;
|
||||
DialogResult = true;
|
||||
}
|
||||
|
||||
/// <summary>yyyyMMddHHmm(12) / yyyyMMddHHmmss(14) → 사람이 읽는 형식</summary>
|
||||
private static string FormatDtm(string dtm)
|
||||
{
|
||||
if (dtm.Length >= 12
|
||||
&& int.TryParse(dtm[..4], out _))
|
||||
{
|
||||
var time = $"{dtm[8..10]}:{dtm[10..12]}";
|
||||
return $"{dtm[..4]}-{dtm[4..6]}-{dtm[6..8]} {time}";
|
||||
}
|
||||
return dtm;
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<Window x:Class="SheetMe.Designer.Views.SheetOpenDialogView"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
Title="DB에서 서식 열기" Width="560" Height="520"
|
||||
WindowStartupLocation="CenterOwner" ShowInTaskbar="False">
|
||||
<DockPanel Margin="12">
|
||||
<DockPanel DockPanel.Dock="Top">
|
||||
<Button DockPanel.Dock="Right" Content="검색" Padding="14,4" Margin="6,0,0,0" Click="OnSearch" IsDefault="True"/>
|
||||
<TextBox x:Name="SearchBox" Padding="4,3" VerticalContentAlignment="Center"/>
|
||||
</DockPanel>
|
||||
|
||||
<StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,10,0,0">
|
||||
<TextBlock x:Name="CountText" VerticalAlignment="Center" Margin="0,0,12,0" Foreground="{DynamicResource B.Muted}"/>
|
||||
<Button Content="열기" Padding="18,5" Click="OnOpen"/>
|
||||
<Button Content="취소" Padding="18,5" Margin="8,0,0,0" IsCancel="True"/>
|
||||
</StackPanel>
|
||||
|
||||
<ListView x:Name="SheetList" Margin="0,10,0,0" MouseDoubleClick="OnOpen">
|
||||
<ListView.View>
|
||||
<GridView>
|
||||
<GridViewColumn Header="서식코드" Width="110" DisplayMemberBinding="{Binding ShtCod}"/>
|
||||
<GridViewColumn Header="서식명" Width="310" DisplayMemberBinding="{Binding Name}"/>
|
||||
<GridViewColumn Header="디자인" Width="70">
|
||||
<GridViewColumn.CellTemplate>
|
||||
<DataTemplate>
|
||||
<TextBlock Text="있음" Foreground="{DynamicResource B.Success}"
|
||||
Visibility="{Binding HasDesign, Converter={StaticResource BoolToVisibility}}"/>
|
||||
</DataTemplate>
|
||||
</GridViewColumn.CellTemplate>
|
||||
</GridViewColumn>
|
||||
</GridView>
|
||||
</ListView.View>
|
||||
</ListView>
|
||||
</DockPanel>
|
||||
</Window>
|
||||
@@ -0,0 +1,64 @@
|
||||
using System.Windows;
|
||||
using SheetMe.Data.Stores;
|
||||
|
||||
namespace SheetMe.Designer.Views;
|
||||
|
||||
/// <summary>
|
||||
/// DB 서식 열기 대화상자 — 검색/목록/선택.
|
||||
/// 검색 실행 콜백을 주입받는 얇은 대화상자(모달 결과 = SelectedSheet).
|
||||
/// </summary>
|
||||
public partial class SheetOpenDialogView : Window
|
||||
{
|
||||
#region Member Fields
|
||||
private readonly Func<string?, List<SheetSummary>> search;
|
||||
#endregion
|
||||
|
||||
#region Properties
|
||||
/// <summary>선택된 서식 — 확인 시 채워짐</summary>
|
||||
public SheetSummary? SelectedSheet { get; private set; }
|
||||
#endregion
|
||||
|
||||
#region Constructors
|
||||
public SheetOpenDialogView(Func<string?, List<SheetSummary>> search)
|
||||
{
|
||||
this.search = search;
|
||||
InitializeComponent();
|
||||
Loaded += (_, _) => RunSearch();
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Methods
|
||||
private void OnSearch(object sender, RoutedEventArgs e) => RunSearch();
|
||||
|
||||
private void RunSearch()
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = search(SearchBox.Text);
|
||||
SheetList.ItemsSource = result;
|
||||
CountText.Text = $"{result.Count}건";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show($"서식 목록 조회 중 오류가 발생했습니다.\n\n{ex.Message}", "오류",
|
||||
MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnOpen(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (SheetList.SelectedItem is not SheetSummary selected)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (!selected.HasDesign)
|
||||
{
|
||||
MessageBox.Show("선택한 서식에는 저장된 디자인이 없습니다.", "열기",
|
||||
MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
return;
|
||||
}
|
||||
SelectedSheet = selected;
|
||||
DialogResult = true;
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<Window x:Class="SheetMe.Designer.Views.TagPickerDialogView"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
Title="태그 선택" Width="480" Height="560"
|
||||
WindowStartupLocation="CenterOwner" ShowInTaskbar="False">
|
||||
<DockPanel Margin="12">
|
||||
<TextBlock x:Name="TitleText" DockPanel.Dock="Top" Foreground="{DynamicResource B.Muted}"
|
||||
TextWrapping="Wrap" Margin="0,0,0,8"/>
|
||||
|
||||
<TextBox x:Name="SearchBox" DockPanel.Dock="Top" Padding="4,3"
|
||||
TextChanged="OnSearchChanged" VerticalContentAlignment="Center"/>
|
||||
|
||||
<StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,10,0,0">
|
||||
<TextBlock x:Name="CountText" VerticalAlignment="Center" Margin="0,0,12,0" Foreground="{DynamicResource B.Muted}"/>
|
||||
<Button Content="값 지우기" Padding="12,4" Click="OnClear"/>
|
||||
<Button Content="선택" Padding="18,4" Margin="8,0,0,0" Click="OnPick" IsDefault="True"/>
|
||||
<Button Content="취소" Padding="18,4" Margin="8,0,0,0" IsCancel="True"/>
|
||||
</StackPanel>
|
||||
|
||||
<ListBox x:Name="TagList" Margin="0,8,0,0" MouseDoubleClick="OnPick"
|
||||
VirtualizingPanel.IsVirtualizing="True" FontSize="13"/>
|
||||
</DockPanel>
|
||||
</Window>
|
||||
@@ -0,0 +1,71 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace SheetMe.Designer.Views;
|
||||
|
||||
/// <summary>
|
||||
/// 태그 선택 대화상자 — 검색(부분 일치, 공백 구분 다중 토큰 AND) + 목록 선택.
|
||||
/// '값 지우기'는 빈 값으로 확정(속성 제거).
|
||||
/// </summary>
|
||||
public partial class TagPickerDialogView : Window
|
||||
{
|
||||
#region Member Fields
|
||||
private readonly IReadOnlyList<string> allTags;
|
||||
#endregion
|
||||
|
||||
#region Properties
|
||||
/// <summary>선택된 태그 — '값 지우기'면 빈 문자열</summary>
|
||||
public string? SelectedTag { get; private set; }
|
||||
#endregion
|
||||
|
||||
#region Constructors
|
||||
public TagPickerDialogView(string title, IReadOnlyList<string> tags, string? currentValue)
|
||||
{
|
||||
allTags = tags;
|
||||
InitializeComponent();
|
||||
TitleText.Text = title;
|
||||
ApplyFilter(string.Empty);
|
||||
if (!string.IsNullOrEmpty(currentValue))
|
||||
{
|
||||
TagList.SelectedItem = tags.FirstOrDefault(t => t == currentValue);
|
||||
TagList.ScrollIntoView(TagList.SelectedItem);
|
||||
}
|
||||
Loaded += (_, _) => SearchBox.Focus();
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Methods
|
||||
private void OnSearchChanged(object sender, TextChangedEventArgs e)
|
||||
=> ApplyFilter(SearchBox.Text);
|
||||
|
||||
private void ApplyFilter(string keyword)
|
||||
{
|
||||
var tokens = keyword.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
var filtered = tokens.Length == 0
|
||||
? allTags
|
||||
: allTags.Where(t => tokens.All(k => t.Contains(k, StringComparison.OrdinalIgnoreCase))).ToList();
|
||||
TagList.ItemsSource = filtered;
|
||||
CountText.Text = $"{filtered.Count}/{allTags.Count}건";
|
||||
if (filtered.Count > 0)
|
||||
{
|
||||
TagList.SelectedIndex = 0;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (TagList.SelectedItem is not string tag)
|
||||
{
|
||||
return;
|
||||
}
|
||||
SelectedTag = tag;
|
||||
DialogResult = true;
|
||||
}
|
||||
|
||||
private void OnClear(object sender, RoutedEventArgs e)
|
||||
{
|
||||
SelectedTag = string.Empty;
|
||||
DialogResult = true;
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
Reference in New Issue
Block a user