인스펙터 3탭 분할 — 빈 탭은 그리지 않는다
사용자 요청으로 [200] 식 「디자인 / 데이터 / 동작」 3탭을 넣었다. [분류] 속성 49종을 레거시 실사용 근거로 배정했다. 이름만 보고 찍으면 사용자가 속성 하나를 찾아 세 탭을 다 뒤지게 되므로, 각 속성이 레거시에서 실제로 어떻게 쓰이는지를 근거로 삼았다. · GetValue() 가 그 속성을 돌려주면 값이다 — CheckBox/RadioButton 의 Checked 는 IIf(Checked,'Y','') 이므로 초기값(데이터). · DateTimePicker.Format/CustomFormat 은 데이터처럼 보이지만 GetValue 가 Format 과 무관하게 항상 yyyyMMddHHmmss 를 돌려준다 — 그리기 문자열만 바꾸므로 디자인. 확정적. · Mask 는 WinForms 분류로는 Behavior 지만 실제로는 날짜·시간 입력 형식 제약이라 데이터. · Score 는 '점수'라는 이름과 달리 체크됐을 때만 값을 내는 데이터 항목이고 CalcBox.Formula 가 컨트롤명으로 합산한다. · 상용구 2종(RwdRsvWrdYon/RwdOrderAutYon)은 입력 보조처럼 보이지만 조작 훅이다 — 전자는 포커스 핸들러 부착 게이트, 후자는 상용구를 고른 결과로 OCS 처방을 전송하는 게이트. **동작**으로 정정했다 (초안에서는 데이터로 뒀다). 결과: 디자인 21 / 데이터 20 / 동작 8. [빈 탭은 그리지 않는다 — 이번 결정의 핵심] 분류를 마치고 세어 보니 **동작 속성을 가진 타입은 TextBox·Button 둘뿐이고 나머지 14타입(87.5%)에서 동작 탭이 통째로 빈다.** 데이터 탭도 6타입에서 빈다. 빈 탭을 남겨 두면 대부분의 컨트롤에서 '여긴 없습니다'만 보게 되고 사용자는 빈 탭을 계속 눌러보게 된다 — 탭이 없느니만 못하다. 그래서 개수 0인 탭은 알약을 아예 그리지 않고, 남는 탭이 하나뿐이면 알약 줄 자체를 숨긴다. 결과적으로 라벨·선·패널·그룹상자·날짜선택은 탭 없이 평평하게 보이고, TextBox 만 3개가 뜬다. 현재 탭이 새 선택에서 비면 디자인으로 되돌린다 — 디자인은 공통·글꼴이 있어 절대 비지 않는 안전한 착지점이다. [유지] 선택한 탭은 컨트롤을 바꿔도 유지된다. 데이터 탭에서 여러 컨트롤의 태그를 잇달아 손보는 작업이 흔한데 매번 디자인으로 돌아가면 계속 다시 눌러야 한다([200] 은 매번 리셋된다 — 따르지 않았다). [탈출구는 쪼개지 않는다] '전체 속성(고급)'의 레거시 원문 키는 분류 근거가 없고 무엇보다 탈출구다. 탭에 가두면 '고급에도 없네' 상황이 생기므로 모든 탭 하단에 그대로 둔다. [분류표 위치] Core 가 아니라 Designer 계층에 뒀다(PaletteIconCatalog 선례). 탭 구성은 화면 사정이지 서식 데이터의 성질이 아니다. 모르는 키는 편집기 종류로 추론해 사이트 커스텀 속성에 대비한다. 검증: 테스트 124/124, edit-smoke 실패 0, 왕복 1,271건 diff 0/예외 0, 종이 렌더 P062 픽셀 차이 0. dialog-shots 진단에 데이터·동작 탭 스냅샷을 추가해 세 탭 전부를 양 테마로 확인. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
abd7eb7d0e
commit
394807f8df
@@ -6,6 +6,7 @@ using SheetMe.Core.Models;
|
||||
using SheetMe.Data.Stores;
|
||||
using SheetMe.Designer.Services;
|
||||
using SheetMe.Designer.ViewModels;
|
||||
using SheetMe.Designer.ViewModels.Inspector;
|
||||
|
||||
namespace SheetMe.Designer.Diagnostics;
|
||||
|
||||
@@ -123,15 +124,19 @@ public static class DialogShots
|
||||
// 속성 패널 레이아웃 회귀를 눈으로 확인할 수 있는 유일한 자동 경로다.
|
||||
("09-inspector-label", () => HostInspector(designer, "Label1")),
|
||||
("10-inspector-textbox", () => HostInspector(designer, "TextBox1")),
|
||||
("11-inspector-data", () => HostInspector(designer, "TextBox1", InspectorTab.Data)),
|
||||
("12-inspector-behavior", () => HostInspector(designer, "TextBox1", InspectorTab.Behavior)),
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>인스펙터를 실제 패널 폭(300px)의 창에 담는다 — 컨트롤 하나를 선택한 상태로</summary>
|
||||
private static Window HostInspector(DesignerViewModel designer, string controlId)
|
||||
private static Window HostInspector(DesignerViewModel designer, string controlId,
|
||||
InspectorTab tab = InspectorTab.Design)
|
||||
{
|
||||
var page = designer.Pages[0];
|
||||
var target = page.Controls.FirstOrDefault(c => c.Id == controlId) ?? page.Controls[0];
|
||||
designer.Selection.SetSingle(target);
|
||||
designer.Inspector.SelectedTab = tab;
|
||||
designer.Inspector.Rebuild();
|
||||
|
||||
return new Window
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
using SheetMe.Core.Catalog;
|
||||
|
||||
namespace SheetMe.Designer.ViewModels.Inspector;
|
||||
|
||||
/// <summary>인스펙터 상단 탭</summary>
|
||||
public enum InspectorTab
|
||||
{
|
||||
/// <summary>화면에 어떻게 보이는가 — 글자·색·정렬·테두리·크기·표시 형식</summary>
|
||||
Design,
|
||||
|
||||
/// <summary>무슨 값이 채워지는가 — 자동 채움 원천, 조회, 초기값, 입력 형식·검증, 저장 대상</summary>
|
||||
Data,
|
||||
|
||||
/// <summary>조작하면 무슨 일이 일어나는가 — 액션 태그, 대상 컨트롤, 단축키</summary>
|
||||
Behavior,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 속성 → 인스펙터 탭 분류.
|
||||
///
|
||||
/// <b>Core 에 두지 않는다.</b> ControlRegistry 는 UI 무관하게 유지하고 표시 계층인 여기서만
|
||||
/// 매핑한다(PaletteIconCatalog 와 같은 선례). 탭 구성은 화면 사정이지 서식 데이터의 성질이 아니다.
|
||||
///
|
||||
/// 분류가 틀리면 탭이 없느니만 못하다 — 사용자가 속성 하나를 찾아 세 탭을 다 뒤지게 된다.
|
||||
/// 그래서 키를 하나씩 명시하고, 모르는 키는 <see cref="InspectorTab.Design"/> 이 아니라
|
||||
/// 편집기 종류로 추론한다(사이트가 추가한 커스텀 속성 대비).
|
||||
/// </summary>
|
||||
public static class InspectorTabCatalog
|
||||
{
|
||||
#region Member Fields
|
||||
private static readonly Dictionary<string, InspectorTab> Map = new(StringComparer.Ordinal)
|
||||
{
|
||||
// ── 디자인: 화면에 어떻게 보이는가 ──
|
||||
["Text"] = InspectorTab.Design, // 라벨·버튼·그룹상자 표시 문구
|
||||
["TextAlign"] = InspectorTab.Design,
|
||||
["ForeColor"] = InspectorTab.Design,
|
||||
["BackColor"] = InspectorTab.Design,
|
||||
["LineColor"] = InspectorTab.Design,
|
||||
["BorderStyle"] = InspectorTab.Design,
|
||||
["SizeMode"] = InspectorTab.Design, // 이미지 맞춤 방식
|
||||
["Format"] = InspectorTab.Design, // 날짜 표시 형식
|
||||
["CustomFormat"] = InspectorTab.Design,
|
||||
["Multiline"] = InspectorTab.Design, // 여러 줄로 '보이는가'
|
||||
|
||||
// ── 데이터: 무슨 값이 채워지는가 ──
|
||||
["DataInterfaceTag"] = InspectorTab.Data, // 자동 채움 원천
|
||||
["DataTableField"] = InspectorTab.Data, // 저장 대상 필드
|
||||
["Query"] = InspectorTab.Data,
|
||||
["ExcuteQuery"] = InspectorTab.Data,
|
||||
["InitialValue"] = InspectorTab.Data,
|
||||
["Mask"] = InspectorTab.Data, // 입력 형식
|
||||
["IsRequiredValue"] = InspectorTab.Data, // 검증
|
||||
["Items"] = InspectorTab.Data, // 고를 수 있는 값 목록
|
||||
["Checked"] = InspectorTab.Data, // 기본 체크 = 초기값
|
||||
["Score"] = InspectorTab.Data, // 설문 배점
|
||||
["Formula"] = InspectorTab.Data, // 다른 컨트롤의 Score 를 합산해 이 칸의 값을 만든다
|
||||
|
||||
// ── 동작: 조작하면 무슨 일이 일어나는가 ──
|
||||
["DataActionTag"] = InspectorTab.Behavior,
|
||||
["DataActionTagControl"] = InspectorTab.Behavior,
|
||||
["GetDataActionTagControl"] = InspectorTab.Behavior,
|
||||
["KeyboardShortcut"] = InspectorTab.Behavior,
|
||||
// 상용구 2종은 이름이 '입력 보조'처럼 보이지만 레거시에서는 조작 훅이다.
|
||||
// RwdRsvWrdYon 은 포커스(Enter) 핸들러 부착 게이트라 상용구 창이 이 칸에 연동되고,
|
||||
// RwdOrderAutYon 은 상용구를 고른 결과로 OCS 처방을 전송하는 게이트다.
|
||||
["RwdRsvWrdYon"] = InspectorTab.Behavior,
|
||||
["RwdOrderAutYon"] = InspectorTab.Behavior,
|
||||
};
|
||||
#endregion
|
||||
|
||||
#region Methods
|
||||
/// <summary>속성이 속한 탭</summary>
|
||||
public static InspectorTab TabOf(PropertyDef def)
|
||||
{
|
||||
if (Map.TryGetValue(def.Key, out var tab))
|
||||
{
|
||||
return tab;
|
||||
}
|
||||
// 목록에 없는 키(사이트 커스텀 등)는 편집기 종류로 추론한다 — 무조건 디자인으로 보내면
|
||||
// 성격이 다른 속성이 디자인 탭에 쌓인다.
|
||||
return def.Editor switch
|
||||
{
|
||||
PropEditorKind.DataActionTag => InspectorTab.Behavior,
|
||||
PropEditorKind.DataInterfaceTag or PropEditorKind.SqlQuery or PropEditorKind.Mask
|
||||
=> InspectorTab.Data,
|
||||
_ => InspectorTab.Design,
|
||||
};
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
@@ -21,6 +21,10 @@ public sealed class InspectorViewModel : ViewModelBase
|
||||
private readonly DesignerViewModel designer;
|
||||
private readonly List<(PropertyRowViewModel Row, RowBinding Binding)> boundsRows = new();
|
||||
private SectionRowViewModel? currentSection;
|
||||
private InspectorTab selectedTab = InspectorTab.Design;
|
||||
private int designCount;
|
||||
private int dataCount;
|
||||
private int behaviorCount;
|
||||
private bool showAdvanced;
|
||||
|
||||
/// <summary>공통/글꼴 섹션이 이미 다루는 키 — 고급 목록에서 제외</summary>
|
||||
@@ -37,6 +41,83 @@ public sealed class InspectorViewModel : ViewModelBase
|
||||
/// <summary>선택된 컨트롤이 있는지 — 인스펙터 빈 상태 판정</summary>
|
||||
public bool HasSelection => designer.Selection.Items.Count > 0;
|
||||
|
||||
/// <summary>
|
||||
/// 선택된 탭. 컨트롤을 바꿔도 유지된다 — 데이터 탭에서 여러 컨트롤의 태그를 잇달아 손보는
|
||||
/// 작업이 흔한데, 선택할 때마다 디자인 탭으로 되돌아가면 매번 다시 눌러야 한다.
|
||||
/// </summary>
|
||||
public InspectorTab SelectedTab
|
||||
{
|
||||
get => selectedTab;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref selectedTab, value))
|
||||
{
|
||||
OnPropertyChanged(nameof(IsDesignTab));
|
||||
OnPropertyChanged(nameof(IsDataTab));
|
||||
OnPropertyChanged(nameof(IsBehaviorTab));
|
||||
Rebuild();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>탭 알약 바인딩 — ToggleButton 은 bool 만 받는다</summary>
|
||||
public bool IsDesignTab
|
||||
{
|
||||
get => selectedTab == InspectorTab.Design;
|
||||
set { if (value) { SelectedTab = InspectorTab.Design; } else { OnPropertyChanged(nameof(IsDesignTab)); } }
|
||||
}
|
||||
|
||||
public bool IsDataTab
|
||||
{
|
||||
get => selectedTab == InspectorTab.Data;
|
||||
set { if (value) { SelectedTab = InspectorTab.Data; } else { OnPropertyChanged(nameof(IsDataTab)); } }
|
||||
}
|
||||
|
||||
public bool IsBehaviorTab
|
||||
{
|
||||
get => selectedTab == InspectorTab.Behavior;
|
||||
set { if (value) { SelectedTab = InspectorTab.Behavior; } else { OnPropertyChanged(nameof(IsBehaviorTab)); } }
|
||||
}
|
||||
|
||||
/// <summary>탭 배지 — 눌러보지 않아도 볼 게 있는지 알 수 있게</summary>
|
||||
public int DesignCount
|
||||
{
|
||||
get => designCount;
|
||||
private set => SetProperty(ref designCount, value);
|
||||
}
|
||||
|
||||
public int DataCount
|
||||
{
|
||||
get => dataCount;
|
||||
private set => SetProperty(ref dataCount, value);
|
||||
}
|
||||
|
||||
public int BehaviorCount
|
||||
{
|
||||
get => behaviorCount;
|
||||
private set => SetProperty(ref behaviorCount, value);
|
||||
}
|
||||
|
||||
/// <summary>데이터 탭을 그릴지 — 이 컨트롤에 데이터 속성이 있을 때만</summary>
|
||||
public bool ShowDataTab => DataCount > 0;
|
||||
|
||||
/// <summary>동작 탭을 그릴지 — 이 컨트롤에 동작 속성이 있을 때만</summary>
|
||||
public bool ShowBehaviorTab => BehaviorCount > 0;
|
||||
|
||||
/// <summary>탭이 하나뿐이면 알약 줄 자체가 잡음이다 — 둘 이상일 때만 띄운다</summary>
|
||||
public bool ShowTabStrip => HasSelection && (ShowDataTab || ShowBehaviorTab);
|
||||
|
||||
/// <summary>선택은 있는데 이 탭에 보여줄 게 없는 상태 — 빈 화면 대신 안내를 띄운다</summary>
|
||||
public bool IsTabEmpty => HasSelection && Rows.Count == 0;
|
||||
|
||||
/// <summary>빈 탭 안내 문구</summary>
|
||||
public string EmptyTabText => selectedTab switch
|
||||
{
|
||||
InspectorTab.Data => "이 컨트롤에는 데이터 속성이 없습니다",
|
||||
InspectorTab.Behavior => "이 컨트롤에는 동작 속성이 없습니다",
|
||||
_ => "표시할 속성이 없습니다",
|
||||
};
|
||||
|
||||
/// <summary>선택 요약 텍스트</summary>
|
||||
public string Summary
|
||||
=> designer.Selection.Items.Count switch
|
||||
@@ -84,10 +165,18 @@ public sealed class InspectorViewModel : ViewModelBase
|
||||
var items = designer.Selection.Items;
|
||||
if (items.Count == 0)
|
||||
{
|
||||
RefreshTabCounts(null);
|
||||
return;
|
||||
}
|
||||
|
||||
// 공통: 이름/위치/크기
|
||||
// 같은 타입만 타입 전용 섹션을 낸다 — 탭 개수 계산도 이 조건을 따라야 어긋나지 않는다
|
||||
var type = items[0].Type;
|
||||
var descriptor = items.All(i => i.Type == type) ? ControlRegistry.Find(type) : null;
|
||||
RefreshTabCounts(descriptor);
|
||||
|
||||
if (selectedTab == InspectorTab.Design)
|
||||
{
|
||||
// 공통: 이름/위치/크기 — 어느 컨트롤에나 있으므로 디자인 탭에 둔다
|
||||
AddSection("공통");
|
||||
if (items.Count == 1)
|
||||
{
|
||||
@@ -110,25 +199,70 @@ public sealed class InspectorViewModel : ViewModelBase
|
||||
AddFontRow("크기(pt)", f => f.SizePt.ToString("0.##", CultureInfo.InvariantCulture),
|
||||
(f, v) => f.SizePt = double.TryParse(v, NumberStyles.Number, CultureInfo.InvariantCulture, out var size) && size > 0 ? size : f.SizePt);
|
||||
AddFontStyleSegment();
|
||||
}
|
||||
|
||||
// 타입 전용 — 전체 선택이 같은 타입일 때만
|
||||
var type = items[0].Type;
|
||||
// 타입 전용 — 선택 탭에 해당하는 속성만
|
||||
var curatedKeys = new HashSet<string>(HandledKeys, StringComparer.Ordinal);
|
||||
if (items.All(i => i.Type == type) && ControlRegistry.Find(type) is { } descriptor && descriptor.Properties.Count > 0)
|
||||
if (descriptor is not null)
|
||||
{
|
||||
AddSection(descriptor.DisplayName);
|
||||
// 큐레이션 키는 탭과 무관하게 전부 모아야 한다 — 고급 목록에서 중복 노출을 막는 용도라
|
||||
// 탭으로 걸러 버리면 다른 탭 속성이 고급에 다시 나온다.
|
||||
foreach (var def in descriptor.Properties)
|
||||
{
|
||||
AddDefRow(def);
|
||||
curatedKeys.Add(def.Key);
|
||||
}
|
||||
|
||||
var tabDefs = descriptor.Properties.Where(d => InspectorTabCatalog.TabOf(d) == selectedTab).ToList();
|
||||
if (tabDefs.Count > 0)
|
||||
{
|
||||
AddSection(descriptor.DisplayName);
|
||||
foreach (var def in tabDefs)
|
||||
{
|
||||
AddDefRow(def);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 전체 속성(고급) — 단일 선택 시 PropBag 의 나머지 레거시 속성 전부(레거시 PropertyGrid 등가)
|
||||
// 전체 속성(고급) — 단일 선택 시 PropBag 의 나머지 레거시 속성 전부(레거시 PropertyGrid 등가).
|
||||
// 레거시 원문 키는 분류 근거가 없어 어느 탭에도 속하지 않는다 — 탈출구이므로 모든 탭 아래에 둔다.
|
||||
if (items.Count == 1)
|
||||
{
|
||||
BuildAdvancedRows(items[0], curatedKeys);
|
||||
}
|
||||
|
||||
OnPropertyChanged(nameof(IsTabEmpty));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 탭 배지·표시 여부 갱신.
|
||||
///
|
||||
/// <b>비어 있는 탭은 그리지 않는다.</b> 레거시 속성을 실사용 근거로 분류해 보면 동작 속성을
|
||||
/// 가진 타입은 TextBox·Button 둘뿐이고 나머지 14타입에서는 동작 탭이 통째로 빈다
|
||||
/// (데이터 탭도 6타입에서 빈다). 빈 탭을 남겨 두면 대부분의 컨트롤에서 "여긴 없습니다"만
|
||||
/// 보게 되고, 사용자는 속성 하나를 찾아 빈 탭을 계속 눌러보게 된다 — 탭이 없느니만 못하다.
|
||||
/// </summary>
|
||||
private void RefreshTabCounts(ControlDescriptor? descriptor)
|
||||
{
|
||||
var typed = descriptor?.Properties ?? (IReadOnlyList<PropertyDef>)Array.Empty<PropertyDef>();
|
||||
// 디자인 탭에는 공통 5 + 글꼴 6 이 항상 붙는다 → 선택이 있으면 절대 비지 않는다
|
||||
DesignCount = 11 + typed.Count(d => InspectorTabCatalog.TabOf(d) == InspectorTab.Design);
|
||||
DataCount = typed.Count(d => InspectorTabCatalog.TabOf(d) == InspectorTab.Data);
|
||||
BehaviorCount = typed.Count(d => InspectorTabCatalog.TabOf(d) == InspectorTab.Behavior);
|
||||
|
||||
// 현재 탭이 이 컨트롤에서 비면 내용 있는 탭으로 되돌린다 — 안 그러면 인스펙터가 통째로 빈다.
|
||||
// 디자인은 공통·글꼴이 있어 절대 비지 않으므로 항상 안전한 착지점이다.
|
||||
if ((selectedTab == InspectorTab.Data && DataCount == 0) ||
|
||||
(selectedTab == InspectorTab.Behavior && BehaviorCount == 0))
|
||||
{
|
||||
selectedTab = InspectorTab.Design;
|
||||
}
|
||||
|
||||
OnPropertyChanged(nameof(IsDesignTab));
|
||||
OnPropertyChanged(nameof(IsDataTab));
|
||||
OnPropertyChanged(nameof(IsBehaviorTab));
|
||||
OnPropertyChanged(nameof(ShowDataTab));
|
||||
OnPropertyChanged(nameof(ShowBehaviorTab));
|
||||
OnPropertyChanged(nameof(ShowTabStrip));
|
||||
}
|
||||
|
||||
/// <summary>고급 섹션 구성 — 접이식, raw 문자열 편집 + 속성 추가</summary>
|
||||
|
||||
@@ -10,6 +10,53 @@
|
||||
|
||||
<UserControl.Resources>
|
||||
<ctl:IconNameToVisualConverter x:Key="IconName"/>
|
||||
<BooleanToVisibilityConverter x:Key="BoolToVisibility"/>
|
||||
|
||||
<!-- 3탭 알약 — 좌측 패널 탭과 같은 결(선택만 채움) -->
|
||||
<Style x:Key="TabPill" TargetType="ToggleButton">
|
||||
<Setter Property="Cursor" Value="Hand"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource B.Muted}"/>
|
||||
<Setter Property="FontSize" Value="12"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="ToggleButton">
|
||||
<Border x:Name="bd" Background="Transparent" CornerRadius="6" Padding="0,5" Margin="1">
|
||||
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center">
|
||||
<ContentPresenter.Resources>
|
||||
<Style TargetType="TextBlock">
|
||||
<Setter Property="Foreground" Value="{Binding Foreground, RelativeSource={RelativeSource AncestorType=ToggleButton}}"/>
|
||||
</Style>
|
||||
</ContentPresenter.Resources>
|
||||
</ContentPresenter>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="bd" Property="Background" Value="{DynamicResource B.Hover}"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsChecked" Value="True">
|
||||
<Setter TargetName="bd" Property="Background" Value="{DynamicResource B.Surface}"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource B.Ink}"/>
|
||||
<Setter Property="FontWeight" Value="Bold"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!-- 탭 배지 — 0 이면 그 탭에 볼 게 없다는 뜻이라 흐리게 -->
|
||||
<Style x:Key="TabBadge" TargetType="TextBlock">
|
||||
<Setter Property="FontSize" Value="10.5"/>
|
||||
<Setter Property="FontWeight" Value="Normal"/>
|
||||
<Setter Property="Margin" Value="4,1,0,0"/>
|
||||
<Setter Property="VerticalAlignment" Value="Center"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource B.Muted}"/>
|
||||
<Style.Triggers>
|
||||
<Trigger Property="Text" Value="0">
|
||||
<Setter Property="Opacity" Value="0.45"/>
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
|
||||
<!--
|
||||
라벨 폭. 실측상 가장 긴 라벨이 11자("입력 파라미터 컨트롤", "선택지(줄바꿈 구분)")라
|
||||
@@ -348,6 +395,42 @@
|
||||
<DockPanel>
|
||||
<TextBlock DockPanel.Dock="Top" Text="{Binding Summary}" FontWeight="Bold"
|
||||
Margin="10,10,10,2" Foreground="{DynamicResource B.Muted}" TextTrimming="CharacterEllipsis"/>
|
||||
|
||||
<!--
|
||||
디자인 / 데이터 / 동작 3탭 알약([200] 인스펙터 구성).
|
||||
배지는 눌러보지 않아도 볼 게 있는지 알려준다 — 컨트롤에 따라 데이터·동작 탭이
|
||||
통째로 비는 경우가 흔해서(라벨·선 등), 배지가 없으면 빈 탭을 계속 눌러보게 된다.
|
||||
-->
|
||||
<Border DockPanel.Dock="Top" Margin="10,4,10,8" Padding="2" CornerRadius="8"
|
||||
Background="{DynamicResource B.Chip}"
|
||||
Visibility="{Binding ShowTabStrip, Converter={StaticResource BoolToVisibility}}">
|
||||
<UniformGrid Rows="1">
|
||||
<ToggleButton Style="{StaticResource TabPill}" IsChecked="{Binding IsDesignTab, Mode=TwoWay}"
|
||||
ToolTip="글자·색·정렬·크기 등 보이는 방식">
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
|
||||
<TextBlock Text="디자인"/>
|
||||
<TextBlock Text="{Binding DesignCount}" Style="{StaticResource TabBadge}"/>
|
||||
</StackPanel>
|
||||
</ToggleButton>
|
||||
<ToggleButton Style="{StaticResource TabPill}" IsChecked="{Binding IsDataTab, Mode=TwoWay}"
|
||||
Visibility="{Binding ShowDataTab, Converter={StaticResource BoolToVisibility}}"
|
||||
ToolTip="자동 채움·조회·초기값·입력 형식 등 값에 관한 것">
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
|
||||
<TextBlock Text="데이터"/>
|
||||
<TextBlock Text="{Binding DataCount}" Style="{StaticResource TabBadge}"/>
|
||||
</StackPanel>
|
||||
</ToggleButton>
|
||||
<ToggleButton Style="{StaticResource TabPill}" IsChecked="{Binding IsBehaviorTab, Mode=TwoWay}"
|
||||
Visibility="{Binding ShowBehaviorTab, Converter={StaticResource BoolToVisibility}}"
|
||||
ToolTip="액션 태그·대상 컨트롤 등 조작했을 때 벌어지는 일">
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
|
||||
<TextBlock Text="동작"/>
|
||||
<TextBlock Text="{Binding BehaviorCount}" Style="{StaticResource TabBadge}"/>
|
||||
</StackPanel>
|
||||
</ToggleButton>
|
||||
</UniformGrid>
|
||||
</Border>
|
||||
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||
<ItemsControl ItemsSource="{Binding Rows}" Margin="10,0,10,10">
|
||||
<!-- 접힌 섹션의 행은 컨테이너만 숨긴다 — 행 VM 을 재생성하지 않아 편집 상태가 유지된다 -->
|
||||
@@ -364,6 +447,22 @@
|
||||
</ScrollViewer>
|
||||
</DockPanel>
|
||||
|
||||
<!-- 선택은 있는데 이 탭이 빈 경우 — 빈 화면은 고장으로 읽힌다 -->
|
||||
<TextBlock Text="{Binding EmptyTabText}" FontSize="12" TextAlignment="Center" TextWrapping="Wrap"
|
||||
Foreground="{DynamicResource B.Muted}" HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center" Margin="24,0">
|
||||
<TextBlock.Style>
|
||||
<Style TargetType="TextBlock" BasedOn="{StaticResource {x:Type TextBlock}}">
|
||||
<Setter Property="Visibility" Value="Collapsed"/>
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding IsTabEmpty}" Value="True">
|
||||
<Setter Property="Visibility" Value="Visible"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</TextBlock.Style>
|
||||
</TextBlock>
|
||||
|
||||
<!-- 빈 상태 — 선택이 없으면 "선택 없음" 한 줄만 떠 있어 패널이 고장난 것처럼 보였다 -->
|
||||
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center" Margin="24,0">
|
||||
<StackPanel.Style>
|
||||
|
||||
Reference in New Issue
Block a user