AX Agent 공통 팝업과 시각 상호작용 렌더를 분리해 메인 창 구조를 정리한다
Some checks failed
Release Gate / gate (push) Has been cancelled
Some checks failed
Release Gate / gate (push) Has been cancelled
- ChatWindow.VisualInteractionHelpers.cs를 추가해 메시지 액션 버튼, 선택 스타일, hover 애니메이션, 공통 체크 아이콘 생성을 메인 창 코드 밖으로 이동했다. - ChatWindow.PopupPresentation.cs를 추가해 공통 테마 팝업 컨테이너, 메뉴 아이템, 구분선, 최근 폴더 컨텍스트 메뉴 구성을 한 곳으로 모았다. - README와 DEVELOPMENT 문서에 2026-04-06 09:03 (KST) 기준 구조 분리 이력을 반영했다. - dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ 검증 결과 경고 0 / 오류 0을 확인했다.
This commit is contained in:
@@ -7,6 +7,10 @@ Windows 전용 시맨틱 런처 & 워크스페이스 매니저
|
|||||||
개발 참고: Claw Code 동등성 작업 추적 문서
|
개발 참고: Claw Code 동등성 작업 추적 문서
|
||||||
`docs/claw-code-parity-plan.md`
|
`docs/claw-code-parity-plan.md`
|
||||||
|
|
||||||
|
- 업데이트: 2026-04-06 09:03 (KST)
|
||||||
|
- AX Agent 공통 선택 팝업 조립 로직을 `ChatWindow.PopupPresentation.cs`로 분리했습니다. 테마 팝업 컨테이너, 공통 메뉴 아이템, 구분선, 최근 폴더 우클릭 컨텍스트 메뉴가 메인 창 코드 밖으로 이동해 footer/file-browser 쪽 팝업 품질 작업을 이어가기 쉬운 구조로 정리했습니다.
|
||||||
|
- `ChatWindow.xaml.cs`는 대화 상태와 런타임 orchestration 쪽에 더 집중하도록 정리했고, 공통 팝업 시각 언어를 한 곳에서 다듬을 수 있는 기반을 만들었습니다.
|
||||||
|
|
||||||
- 업데이트: 2026-04-06 08:55 (KST)
|
- 업데이트: 2026-04-06 08:55 (KST)
|
||||||
- AX Agent 파일 브라우저 렌더를 `ChatWindow.FileBrowserPresentation.cs`로 분리했습니다. 파일 탐색기 열기/닫기, 폴더 트리 구성, 파일 헤더/아이콘/크기 표시, 우클릭 메뉴, 디바운스 새로고침 흐름이 메인 창 코드 밖으로 이동했습니다.
|
- AX Agent 파일 브라우저 렌더를 `ChatWindow.FileBrowserPresentation.cs`로 분리했습니다. 파일 탐색기 열기/닫기, 폴더 트리 구성, 파일 헤더/아이콘/크기 표시, 우클릭 메뉴, 디바운스 새로고침 흐름이 메인 창 코드 밖으로 이동했습니다.
|
||||||
- `ChatWindow.xaml.cs`는 transcript·runtime orchestration 중심으로 더 정리됐고, claw-code 기준 사이드 surface 품질 작업을 이어가기 쉬운 구조로 맞췄습니다.
|
- `ChatWindow.xaml.cs`는 transcript·runtime orchestration 중심으로 더 정리됐고, claw-code 기준 사이드 surface 품질 작업을 이어가기 쉬운 구조로 맞췄습니다.
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
# AX Copilot - 媛쒕컻 臾몄꽌
|
# AX Copilot - 媛쒕컻 臾몄꽌
|
||||||
|
|
||||||
|
- Document update: 2026-04-06 09:03 (KST) - Split common themed popup construction out of `ChatWindow.xaml.cs` into `ChatWindow.PopupPresentation.cs`. Shared popup container creation, generic popup menu items/separators, and the recent-folder context menu now live in a dedicated partial instead of the main window orchestration file.
|
||||||
|
- Document update: 2026-04-06 09:03 (KST) - This keeps footer/file-browser popup styling on a single visual path and reduces direct popup composition inside the main chat window flow, making further `claw-code` style popup UX work easier to maintain.
|
||||||
|
|
||||||
- Document update: 2026-04-06 07:31 (KST) - Split permission presentation logic out of `ChatWindow.xaml.cs` into `ChatWindow.PermissionPresentation.cs`. Permission popup row construction, popup refresh, section expansion persistence, and permission banner/status styling now live in a dedicated partial instead of the main window orchestration file.
|
- Document update: 2026-04-06 07:31 (KST) - Split permission presentation logic out of `ChatWindow.xaml.cs` into `ChatWindow.PermissionPresentation.cs`. Permission popup row construction, popup refresh, section expansion persistence, and permission banner/status styling now live in a dedicated partial instead of the main window orchestration file.
|
||||||
- Document update: 2026-04-06 07:31 (KST) - Split context usage card/popup rendering into `ChatWindow.ContextUsagePresentation.cs`. The Cowork/Code context usage ring, tooltip popup copy, hover close behavior, and screen-coordinate hit testing are now isolated from the rest of the chat window flow.
|
- Document update: 2026-04-06 07:31 (KST) - Split context usage card/popup rendering into `ChatWindow.ContextUsagePresentation.cs`. The Cowork/Code context usage ring, tooltip popup copy, hover close behavior, and screen-coordinate hit testing are now isolated from the rest of the chat window flow.
|
||||||
- Document update: 2026-04-06 01:37 (KST) - Reworked AX Agent plan approval toward a more transcript-native flow. The inline decision card remains the primary approval path, while the `계획` affordance now opens the stored plan as a detail-only surface instead of acting like a required popup step.
|
- Document update: 2026-04-06 01:37 (KST) - Reworked AX Agent plan approval toward a more transcript-native flow. The inline decision card remains the primary approval path, while the `계획` affordance now opens the stored plan as a detail-only surface instead of acting like a required popup step.
|
||||||
|
|||||||
159
src/AxCopilot/Views/ChatWindow.PopupPresentation.cs
Normal file
159
src/AxCopilot/Views/ChatWindow.PopupPresentation.cs
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
using System;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
using System.Windows.Controls.Primitives;
|
||||||
|
using System.Windows.Input;
|
||||||
|
using System.Windows.Media;
|
||||||
|
using System.Windows.Threading;
|
||||||
|
|
||||||
|
namespace AxCopilot.Views;
|
||||||
|
|
||||||
|
public partial class ChatWindow
|
||||||
|
{
|
||||||
|
private Popup? _sharedContextPopup;
|
||||||
|
|
||||||
|
private (Popup Popup, StackPanel Panel) CreateThemedPopupMenu(
|
||||||
|
UIElement? placementTarget = null,
|
||||||
|
PlacementMode placement = PlacementMode.MousePoint,
|
||||||
|
double minWidth = 200)
|
||||||
|
{
|
||||||
|
_sharedContextPopup?.SetCurrentValue(Popup.IsOpenProperty, false);
|
||||||
|
|
||||||
|
var bg = TryFindResource("LauncherBackground") as Brush ?? Brushes.White;
|
||||||
|
var border = TryFindResource("BorderColor") as Brush ?? Brushes.Gray;
|
||||||
|
var panel = new StackPanel { Margin = new Thickness(2) };
|
||||||
|
var container = new Border
|
||||||
|
{
|
||||||
|
Background = bg,
|
||||||
|
BorderBrush = border,
|
||||||
|
BorderThickness = new Thickness(1),
|
||||||
|
CornerRadius = new CornerRadius(10),
|
||||||
|
Padding = new Thickness(6),
|
||||||
|
MinWidth = minWidth,
|
||||||
|
Child = panel,
|
||||||
|
Effect = new System.Windows.Media.Effects.DropShadowEffect
|
||||||
|
{
|
||||||
|
BlurRadius = 16,
|
||||||
|
ShadowDepth = 3,
|
||||||
|
Opacity = 0.18,
|
||||||
|
Color = Colors.Black,
|
||||||
|
Direction = 270,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
var popup = new Popup
|
||||||
|
{
|
||||||
|
Child = container,
|
||||||
|
StaysOpen = false,
|
||||||
|
AllowsTransparency = true,
|
||||||
|
PopupAnimation = PopupAnimation.Fade,
|
||||||
|
Placement = placement,
|
||||||
|
PlacementTarget = placementTarget,
|
||||||
|
};
|
||||||
|
|
||||||
|
_sharedContextPopup = popup;
|
||||||
|
return (popup, panel);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Border CreatePopupMenuItem(
|
||||||
|
Popup popup,
|
||||||
|
string icon,
|
||||||
|
string label,
|
||||||
|
Brush iconBrush,
|
||||||
|
Brush labelBrush,
|
||||||
|
Brush hoverBrush,
|
||||||
|
Action action)
|
||||||
|
{
|
||||||
|
var sp = new StackPanel { Orientation = Orientation.Horizontal };
|
||||||
|
sp.Children.Add(new TextBlock
|
||||||
|
{
|
||||||
|
Text = icon,
|
||||||
|
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||||
|
FontSize = 12.5,
|
||||||
|
Foreground = iconBrush,
|
||||||
|
VerticalAlignment = VerticalAlignment.Center,
|
||||||
|
Margin = new Thickness(0, 0, 9, 0),
|
||||||
|
});
|
||||||
|
sp.Children.Add(new TextBlock
|
||||||
|
{
|
||||||
|
Text = label,
|
||||||
|
FontSize = 12.5,
|
||||||
|
Foreground = labelBrush,
|
||||||
|
VerticalAlignment = VerticalAlignment.Center,
|
||||||
|
});
|
||||||
|
|
||||||
|
var item = new Border
|
||||||
|
{
|
||||||
|
Child = sp,
|
||||||
|
Background = Brushes.Transparent,
|
||||||
|
CornerRadius = new CornerRadius(8),
|
||||||
|
Cursor = Cursors.Hand,
|
||||||
|
Padding = new Thickness(10, 7, 12, 7),
|
||||||
|
Margin = new Thickness(0, 1, 0, 1),
|
||||||
|
};
|
||||||
|
item.MouseEnter += (s, _) => { if (s is Border b) b.Background = hoverBrush; };
|
||||||
|
item.MouseLeave += (s, _) => { if (s is Border b) b.Background = Brushes.Transparent; };
|
||||||
|
item.MouseLeftButtonUp += (_, _) =>
|
||||||
|
{
|
||||||
|
popup.SetCurrentValue(Popup.IsOpenProperty, false);
|
||||||
|
action();
|
||||||
|
};
|
||||||
|
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AddPopupMenuSeparator(Panel panel, Brush brush)
|
||||||
|
{
|
||||||
|
panel.Children.Add(new Border
|
||||||
|
{
|
||||||
|
Height = 1,
|
||||||
|
Margin = new Thickness(10, 4, 10, 4),
|
||||||
|
Background = brush,
|
||||||
|
Opacity = 0.35,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>최근 폴더 항목 우클릭 컨텍스트 메뉴를 표시합니다.</summary>
|
||||||
|
private void ShowRecentFolderContextMenu(string folderPath)
|
||||||
|
{
|
||||||
|
var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
|
||||||
|
var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
|
||||||
|
var borderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.Gray;
|
||||||
|
var hoverBg = TryFindResource("ItemHoverBackground") as Brush ?? Brushes.Transparent;
|
||||||
|
var warningBrush = new SolidColorBrush(Color.FromRgb(0xEF, 0x44, 0x44));
|
||||||
|
var (popup, panel) = CreateThemedPopupMenu();
|
||||||
|
|
||||||
|
panel.Children.Add(CreatePopupMenuItem(popup, "\uED25", "폴더 열기", secondaryText, primaryText, hoverBg, () =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = folderPath,
|
||||||
|
UseShellExecute = true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
panel.Children.Add(CreatePopupMenuItem(popup, "\uE8C8", "경로 복사", secondaryText, primaryText, hoverBg, () =>
|
||||||
|
{
|
||||||
|
try { Clipboard.SetText(folderPath); } catch { }
|
||||||
|
}));
|
||||||
|
|
||||||
|
AddPopupMenuSeparator(panel, borderBrush);
|
||||||
|
|
||||||
|
panel.Children.Add(CreatePopupMenuItem(popup, "\uE74D", "목록에서 삭제", warningBrush, warningBrush, hoverBg, () =>
|
||||||
|
{
|
||||||
|
_settings.Settings.Llm.RecentWorkFolders.RemoveAll(
|
||||||
|
p => p.Equals(folderPath, StringComparison.OrdinalIgnoreCase));
|
||||||
|
_settings.Save();
|
||||||
|
if (FolderMenuPopup.IsOpen)
|
||||||
|
ShowFolderMenu();
|
||||||
|
}));
|
||||||
|
|
||||||
|
Dispatcher.BeginInvoke(() => { popup.IsOpen = true; }, DispatcherPriority.Input);
|
||||||
|
}
|
||||||
|
}
|
||||||
259
src/AxCopilot/Views/ChatWindow.VisualInteractionHelpers.cs
Normal file
259
src/AxCopilot/Views/ChatWindow.VisualInteractionHelpers.cs
Normal file
@@ -0,0 +1,259 @@
|
|||||||
|
using System;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
using System.Windows.Input;
|
||||||
|
using System.Windows.Media;
|
||||||
|
using System.Windows.Media.Animation;
|
||||||
|
|
||||||
|
namespace AxCopilot.Views;
|
||||||
|
|
||||||
|
public partial class ChatWindow
|
||||||
|
{
|
||||||
|
/// <summary>마우스 오버 시 살짝 확대하는 호버 애니메이션. 독립적 공간이 있는 버튼에만 적용합니다.</summary>
|
||||||
|
private static void ApplyHoverScaleAnimation(FrameworkElement element, double hoverScale = 1.08)
|
||||||
|
{
|
||||||
|
void EnsureTransform()
|
||||||
|
{
|
||||||
|
element.RenderTransformOrigin = new Point(0.5, 0.5);
|
||||||
|
if (element.RenderTransform is not ScaleTransform || element.RenderTransform.IsFrozen)
|
||||||
|
element.RenderTransform = new ScaleTransform(1, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
element.Loaded += (_, _) => EnsureTransform();
|
||||||
|
|
||||||
|
element.MouseEnter += (_, _) =>
|
||||||
|
{
|
||||||
|
EnsureTransform();
|
||||||
|
var st = (ScaleTransform)element.RenderTransform;
|
||||||
|
var grow = new DoubleAnimation(hoverScale, TimeSpan.FromMilliseconds(150))
|
||||||
|
{
|
||||||
|
EasingFunction = new CubicEase { EasingMode = EasingMode.EaseOut }
|
||||||
|
};
|
||||||
|
st.BeginAnimation(ScaleTransform.ScaleXProperty, grow);
|
||||||
|
st.BeginAnimation(ScaleTransform.ScaleYProperty, grow);
|
||||||
|
};
|
||||||
|
|
||||||
|
element.MouseLeave += (_, _) =>
|
||||||
|
{
|
||||||
|
EnsureTransform();
|
||||||
|
var st = (ScaleTransform)element.RenderTransform;
|
||||||
|
var shrink = new DoubleAnimation(1.0, TimeSpan.FromMilliseconds(200))
|
||||||
|
{
|
||||||
|
EasingFunction = new CubicEase { EasingMode = EasingMode.EaseOut }
|
||||||
|
};
|
||||||
|
st.BeginAnimation(ScaleTransform.ScaleXProperty, shrink);
|
||||||
|
st.BeginAnimation(ScaleTransform.ScaleYProperty, shrink);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>마우스 오버 시 텍스트가 살짝 튀어오르는 바운스 애니메이션.</summary>
|
||||||
|
private static void ApplyHoverBounceAnimation(FrameworkElement element, double bounceY = -2.5)
|
||||||
|
{
|
||||||
|
void EnsureTransform()
|
||||||
|
{
|
||||||
|
if (element.RenderTransform is not TranslateTransform || element.RenderTransform.IsFrozen)
|
||||||
|
element.RenderTransform = new TranslateTransform(0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
element.Loaded += (_, _) => EnsureTransform();
|
||||||
|
|
||||||
|
element.MouseEnter += (_, _) =>
|
||||||
|
{
|
||||||
|
EnsureTransform();
|
||||||
|
var tt = (TranslateTransform)element.RenderTransform;
|
||||||
|
tt.BeginAnimation(
|
||||||
|
TranslateTransform.YProperty,
|
||||||
|
new DoubleAnimation(bounceY, TimeSpan.FromMilliseconds(200))
|
||||||
|
{
|
||||||
|
EasingFunction = new CubicEase { EasingMode = EasingMode.EaseOut }
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
element.MouseLeave += (_, _) =>
|
||||||
|
{
|
||||||
|
EnsureTransform();
|
||||||
|
var tt = (TranslateTransform)element.RenderTransform;
|
||||||
|
tt.BeginAnimation(
|
||||||
|
TranslateTransform.YProperty,
|
||||||
|
new DoubleAnimation(0, TimeSpan.FromMilliseconds(250))
|
||||||
|
{
|
||||||
|
EasingFunction = new ElasticEase
|
||||||
|
{
|
||||||
|
EasingMode = EasingMode.EaseOut,
|
||||||
|
Oscillations = 1,
|
||||||
|
Springiness = 10
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>심플한 V 체크 아이콘을 생성합니다.</summary>
|
||||||
|
private static FrameworkElement CreateSimpleCheck(Brush color, double size = 14)
|
||||||
|
{
|
||||||
|
return new System.Windows.Shapes.Path
|
||||||
|
{
|
||||||
|
Data = Geometry.Parse($"M {size * 0.15} {size * 0.5} L {size * 0.4} {size * 0.75} L {size * 0.85} {size * 0.28}"),
|
||||||
|
Stroke = color,
|
||||||
|
StrokeThickness = 2,
|
||||||
|
StrokeStartLineCap = PenLineCap.Round,
|
||||||
|
StrokeEndLineCap = PenLineCap.Round,
|
||||||
|
StrokeLineJoin = PenLineJoin.Round,
|
||||||
|
Width = size,
|
||||||
|
Height = size,
|
||||||
|
Margin = new Thickness(0, 0, 10, 0),
|
||||||
|
VerticalAlignment = VerticalAlignment.Center,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>팝업 메뉴 항목에 호버 배경색 + 미세 확대 효과를 적용합니다.</summary>
|
||||||
|
private static void ApplyMenuItemHover(Border item)
|
||||||
|
{
|
||||||
|
var originalBg = item.Background?.Clone() ?? Brushes.Transparent;
|
||||||
|
if (originalBg.CanFreeze)
|
||||||
|
originalBg.Freeze();
|
||||||
|
|
||||||
|
item.RenderTransformOrigin = new Point(0.5, 0.5);
|
||||||
|
item.RenderTransform = new ScaleTransform(1, 1);
|
||||||
|
|
||||||
|
item.MouseEnter += (s, _) =>
|
||||||
|
{
|
||||||
|
if (s is Border b)
|
||||||
|
{
|
||||||
|
if (originalBg is SolidColorBrush scb && scb.Color.A > 0x20)
|
||||||
|
b.Opacity = 0.85;
|
||||||
|
else
|
||||||
|
b.Background = new SolidColorBrush(Color.FromArgb(0x18, 0xFF, 0xFF, 0xFF));
|
||||||
|
}
|
||||||
|
|
||||||
|
var st = item.RenderTransform as ScaleTransform;
|
||||||
|
st?.BeginAnimation(ScaleTransform.ScaleXProperty, new DoubleAnimation(1.02, TimeSpan.FromMilliseconds(120)));
|
||||||
|
st?.BeginAnimation(ScaleTransform.ScaleYProperty, new DoubleAnimation(1.02, TimeSpan.FromMilliseconds(120)));
|
||||||
|
};
|
||||||
|
|
||||||
|
item.MouseLeave += (s, _) =>
|
||||||
|
{
|
||||||
|
if (s is Border b)
|
||||||
|
{
|
||||||
|
b.Opacity = 1.0;
|
||||||
|
b.Background = originalBg;
|
||||||
|
}
|
||||||
|
|
||||||
|
var st = item.RenderTransform as ScaleTransform;
|
||||||
|
st?.BeginAnimation(ScaleTransform.ScaleXProperty, new DoubleAnimation(1.0, TimeSpan.FromMilliseconds(150)));
|
||||||
|
st?.BeginAnimation(ScaleTransform.ScaleYProperty, new DoubleAnimation(1.0, TimeSpan.FromMilliseconds(150)));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private Button CreateActionButton(string symbol, string tooltip, Brush foreground, Action onClick)
|
||||||
|
{
|
||||||
|
var hoverBrush = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
|
||||||
|
var hoverBg = TryFindResource("ItemHoverBackground") as Brush
|
||||||
|
?? new SolidColorBrush(Color.FromArgb(0x10, 0xFF, 0xFF, 0xFF));
|
||||||
|
var borderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.Gray;
|
||||||
|
var icon = new TextBlock
|
||||||
|
{
|
||||||
|
Text = symbol,
|
||||||
|
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||||
|
FontSize = 10,
|
||||||
|
Foreground = foreground,
|
||||||
|
VerticalAlignment = VerticalAlignment.Center
|
||||||
|
};
|
||||||
|
|
||||||
|
var btn = new Button
|
||||||
|
{
|
||||||
|
Content = icon,
|
||||||
|
Background = Brushes.Transparent,
|
||||||
|
BorderBrush = borderBrush,
|
||||||
|
BorderThickness = new Thickness(1),
|
||||||
|
Cursor = Cursors.Hand,
|
||||||
|
Width = 24,
|
||||||
|
Height = 24,
|
||||||
|
Padding = new Thickness(0),
|
||||||
|
Margin = new Thickness(0, 0, 2, 0),
|
||||||
|
ToolTip = tooltip
|
||||||
|
};
|
||||||
|
|
||||||
|
btn.Template = BuildMinimalIconButtonTemplate();
|
||||||
|
btn.MouseEnter += (_, _) =>
|
||||||
|
{
|
||||||
|
icon.Foreground = hoverBrush;
|
||||||
|
btn.Background = hoverBg;
|
||||||
|
};
|
||||||
|
btn.MouseLeave += (_, _) =>
|
||||||
|
{
|
||||||
|
icon.Foreground = foreground;
|
||||||
|
btn.Background = Brushes.Transparent;
|
||||||
|
};
|
||||||
|
btn.Click += (_, _) => onClick();
|
||||||
|
return btn;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ShowMessageActionBar(StackPanel actionBar)
|
||||||
|
{
|
||||||
|
if (actionBar == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
actionBar.Opacity = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HideMessageActionBarIfNotSelected(StackPanel actionBar)
|
||||||
|
{
|
||||||
|
if (actionBar == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!ReferenceEquals(_selectedMessageActionBar, actionBar))
|
||||||
|
actionBar.Opacity = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SelectMessageActionBar(StackPanel actionBar, Border? messageBorder = null)
|
||||||
|
{
|
||||||
|
if (_selectedMessageActionBar != null && !ReferenceEquals(_selectedMessageActionBar, actionBar))
|
||||||
|
_selectedMessageActionBar.Opacity = 0;
|
||||||
|
|
||||||
|
if (_selectedMessageBorder != null && !ReferenceEquals(_selectedMessageBorder, messageBorder))
|
||||||
|
ApplyMessageSelectionStyle(_selectedMessageBorder, false);
|
||||||
|
|
||||||
|
_selectedMessageActionBar = actionBar;
|
||||||
|
_selectedMessageActionBar.Opacity = 1;
|
||||||
|
_selectedMessageBorder = messageBorder;
|
||||||
|
if (_selectedMessageBorder != null)
|
||||||
|
ApplyMessageSelectionStyle(_selectedMessageBorder, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplyMessageSelectionStyle(Border border, bool selected)
|
||||||
|
{
|
||||||
|
if (border == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var accent = TryFindResource("AccentColor") as Brush ?? Brushes.CornflowerBlue;
|
||||||
|
var defaultBorder = TryFindResource("BorderColor") as Brush ?? Brushes.Gray;
|
||||||
|
border.BorderBrush = selected ? accent : defaultBorder;
|
||||||
|
border.BorderThickness = selected ? new Thickness(1.5) : new Thickness(1);
|
||||||
|
border.Effect = selected
|
||||||
|
? new System.Windows.Media.Effects.DropShadowEffect
|
||||||
|
{
|
||||||
|
BlurRadius = 16,
|
||||||
|
ShadowDepth = 0,
|
||||||
|
Opacity = 0.10,
|
||||||
|
Color = Colors.Black,
|
||||||
|
}
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ControlTemplate BuildMinimalIconButtonTemplate()
|
||||||
|
{
|
||||||
|
var template = new ControlTemplate(typeof(Button));
|
||||||
|
var border = new FrameworkElementFactory(typeof(Border));
|
||||||
|
border.SetValue(Border.BackgroundProperty, new TemplateBindingExtension(Button.BackgroundProperty));
|
||||||
|
border.SetValue(Border.BorderBrushProperty, new TemplateBindingExtension(Button.BorderBrushProperty));
|
||||||
|
border.SetValue(Border.BorderThicknessProperty, new TemplateBindingExtension(Button.BorderThicknessProperty));
|
||||||
|
border.SetValue(Border.CornerRadiusProperty, new CornerRadius(8));
|
||||||
|
border.SetValue(Border.PaddingProperty, new TemplateBindingExtension(Button.PaddingProperty));
|
||||||
|
var presenter = new FrameworkElementFactory(typeof(ContentPresenter));
|
||||||
|
presenter.SetValue(ContentPresenter.HorizontalAlignmentProperty, HorizontalAlignment.Center);
|
||||||
|
presenter.SetValue(ContentPresenter.VerticalAlignmentProperty, VerticalAlignment.Center);
|
||||||
|
border.AppendChild(presenter);
|
||||||
|
template.VisualTree = border;
|
||||||
|
return template;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1608,152 +1608,6 @@ public partial class ChatWindow : Window
|
|||||||
SkillService.ActivateConditionalSkillsForPaths(_attachedFiles, cwd);
|
SkillService.ActivateConditionalSkillsForPaths(_attachedFiles, cwd);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Popup? _sharedContextPopup;
|
|
||||||
|
|
||||||
private (Popup Popup, StackPanel Panel) CreateThemedPopupMenu(
|
|
||||||
UIElement? placementTarget = null,
|
|
||||||
PlacementMode placement = PlacementMode.MousePoint,
|
|
||||||
double minWidth = 200)
|
|
||||||
{
|
|
||||||
_sharedContextPopup?.SetCurrentValue(Popup.IsOpenProperty, false);
|
|
||||||
|
|
||||||
var bg = TryFindResource("LauncherBackground") as Brush ?? Brushes.White;
|
|
||||||
var border = TryFindResource("BorderColor") as Brush ?? Brushes.Gray;
|
|
||||||
var panel = new StackPanel { Margin = new Thickness(2) };
|
|
||||||
var container = new Border
|
|
||||||
{
|
|
||||||
Background = bg,
|
|
||||||
BorderBrush = border,
|
|
||||||
BorderThickness = new Thickness(1),
|
|
||||||
CornerRadius = new CornerRadius(10),
|
|
||||||
Padding = new Thickness(6),
|
|
||||||
MinWidth = minWidth,
|
|
||||||
Child = panel,
|
|
||||||
Effect = new System.Windows.Media.Effects.DropShadowEffect
|
|
||||||
{
|
|
||||||
BlurRadius = 16,
|
|
||||||
ShadowDepth = 3,
|
|
||||||
Opacity = 0.18,
|
|
||||||
Color = Colors.Black,
|
|
||||||
Direction = 270,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
var popup = new Popup
|
|
||||||
{
|
|
||||||
Child = container,
|
|
||||||
StaysOpen = false,
|
|
||||||
AllowsTransparency = true,
|
|
||||||
PopupAnimation = PopupAnimation.Fade,
|
|
||||||
Placement = placement,
|
|
||||||
PlacementTarget = placementTarget,
|
|
||||||
};
|
|
||||||
|
|
||||||
_sharedContextPopup = popup;
|
|
||||||
return (popup, panel);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Border CreatePopupMenuItem(
|
|
||||||
Popup popup,
|
|
||||||
string icon,
|
|
||||||
string label,
|
|
||||||
Brush iconBrush,
|
|
||||||
Brush labelBrush,
|
|
||||||
Brush hoverBrush,
|
|
||||||
Action action)
|
|
||||||
{
|
|
||||||
var sp = new StackPanel { Orientation = Orientation.Horizontal };
|
|
||||||
sp.Children.Add(new TextBlock
|
|
||||||
{
|
|
||||||
Text = icon,
|
|
||||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
|
||||||
FontSize = 12.5,
|
|
||||||
Foreground = iconBrush,
|
|
||||||
VerticalAlignment = VerticalAlignment.Center,
|
|
||||||
Margin = new Thickness(0, 0, 9, 0),
|
|
||||||
});
|
|
||||||
sp.Children.Add(new TextBlock
|
|
||||||
{
|
|
||||||
Text = label,
|
|
||||||
FontSize = 12.5,
|
|
||||||
Foreground = labelBrush,
|
|
||||||
VerticalAlignment = VerticalAlignment.Center,
|
|
||||||
});
|
|
||||||
|
|
||||||
var item = new Border
|
|
||||||
{
|
|
||||||
Child = sp,
|
|
||||||
Background = Brushes.Transparent,
|
|
||||||
CornerRadius = new CornerRadius(8),
|
|
||||||
Cursor = Cursors.Hand,
|
|
||||||
Padding = new Thickness(10, 7, 12, 7),
|
|
||||||
Margin = new Thickness(0, 1, 0, 1),
|
|
||||||
};
|
|
||||||
item.MouseEnter += (s, _) => { if (s is Border b) b.Background = hoverBrush; };
|
|
||||||
item.MouseLeave += (s, _) => { if (s is Border b) b.Background = Brushes.Transparent; };
|
|
||||||
item.MouseLeftButtonUp += (_, _) =>
|
|
||||||
{
|
|
||||||
popup.SetCurrentValue(Popup.IsOpenProperty, false);
|
|
||||||
action();
|
|
||||||
};
|
|
||||||
|
|
||||||
return item;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void AddPopupMenuSeparator(Panel panel, Brush brush)
|
|
||||||
{
|
|
||||||
panel.Children.Add(new Border
|
|
||||||
{
|
|
||||||
Height = 1,
|
|
||||||
Margin = new Thickness(10, 4, 10, 4),
|
|
||||||
Background = brush,
|
|
||||||
Opacity = 0.35,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>최근 폴더 항목 우클릭 컨텍스트 메뉴를 표시합니다.</summary>
|
|
||||||
private void ShowRecentFolderContextMenu(string folderPath)
|
|
||||||
{
|
|
||||||
var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
|
|
||||||
var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
|
|
||||||
var borderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.Gray;
|
|
||||||
var hoverBg = TryFindResource("ItemHoverBackground") as Brush ?? Brushes.Transparent;
|
|
||||||
var warningBrush = new SolidColorBrush(Color.FromRgb(0xEF, 0x44, 0x44));
|
|
||||||
var (popup, panel) = CreateThemedPopupMenu();
|
|
||||||
|
|
||||||
panel.Children.Add(CreatePopupMenuItem(popup, "\uED25", "폴더 열기", secondaryText, primaryText, hoverBg, () =>
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo
|
|
||||||
{
|
|
||||||
FileName = folderPath,
|
|
||||||
UseShellExecute = true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
catch { }
|
|
||||||
}));
|
|
||||||
|
|
||||||
panel.Children.Add(CreatePopupMenuItem(popup, "\uE8C8", "경로 복사", secondaryText, primaryText, hoverBg, () =>
|
|
||||||
{
|
|
||||||
try { Clipboard.SetText(folderPath); } catch { }
|
|
||||||
}));
|
|
||||||
|
|
||||||
AddPopupMenuSeparator(panel, borderBrush);
|
|
||||||
|
|
||||||
panel.Children.Add(CreatePopupMenuItem(popup, "\uE74D", "목록에서 삭제", warningBrush, warningBrush, hoverBg, () =>
|
|
||||||
{
|
|
||||||
_settings.Settings.Llm.RecentWorkFolders.RemoveAll(
|
|
||||||
p => p.Equals(folderPath, StringComparison.OrdinalIgnoreCase));
|
|
||||||
_settings.Save();
|
|
||||||
// 메뉴 새로고침
|
|
||||||
if (FolderMenuPopup.IsOpen)
|
|
||||||
ShowFolderMenu();
|
|
||||||
}));
|
|
||||||
|
|
||||||
Dispatcher.BeginInvoke(() => { popup.IsOpen = true; }, DispatcherPriority.Input);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void BtnFolderClear_Click(object sender, RoutedEventArgs e)
|
private void BtnFolderClear_Click(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
FolderPathLabel.Text = "폴더를 선택하세요";
|
FolderPathLabel.Text = "폴더를 선택하세요";
|
||||||
@@ -4014,242 +3868,6 @@ public partial class ChatWindow : Window
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>마우스 오버 시 살짝 확대 + 복귀하는 호버 애니메이션을 적용합니다.</summary>
|
|
||||||
/// <summary>
|
|
||||||
/// 마우스 오버 시 살짝 확대하는 호버 애니메이션.
|
|
||||||
/// 주의: 인접 요소(탭 버튼, 가로 나열 메뉴 등)에는 사용 금지 — 확대 시 이웃 요소를 가립니다.
|
|
||||||
/// 독립적 공간이 있는 버튼에만 적용하세요.
|
|
||||||
/// </summary>
|
|
||||||
private static void ApplyHoverScaleAnimation(FrameworkElement element, double hoverScale = 1.08)
|
|
||||||
{
|
|
||||||
// Loaded 이벤트에서 실행해야 XAML Style의 봉인된 Transform을 안전하게 교체 가능
|
|
||||||
void EnsureTransform()
|
|
||||||
{
|
|
||||||
element.RenderTransformOrigin = new Point(0.5, 0.5);
|
|
||||||
// 봉인(frozen)된 Transform이면 새로 생성하여 교체
|
|
||||||
if (element.RenderTransform is not ScaleTransform || element.RenderTransform.IsFrozen)
|
|
||||||
element.RenderTransform = new ScaleTransform(1, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
element.Loaded += (_, _) => EnsureTransform();
|
|
||||||
|
|
||||||
element.MouseEnter += (_, _) =>
|
|
||||||
{
|
|
||||||
EnsureTransform();
|
|
||||||
var st = (ScaleTransform)element.RenderTransform;
|
|
||||||
var grow = new DoubleAnimation(hoverScale, TimeSpan.FromMilliseconds(150))
|
|
||||||
{ EasingFunction = new CubicEase { EasingMode = EasingMode.EaseOut } };
|
|
||||||
st.BeginAnimation(ScaleTransform.ScaleXProperty, grow);
|
|
||||||
st.BeginAnimation(ScaleTransform.ScaleYProperty, grow);
|
|
||||||
};
|
|
||||||
element.MouseLeave += (_, _) =>
|
|
||||||
{
|
|
||||||
EnsureTransform();
|
|
||||||
var st = (ScaleTransform)element.RenderTransform;
|
|
||||||
var shrink = new DoubleAnimation(1.0, TimeSpan.FromMilliseconds(200))
|
|
||||||
{ EasingFunction = new CubicEase { EasingMode = EasingMode.EaseOut } };
|
|
||||||
st.BeginAnimation(ScaleTransform.ScaleXProperty, shrink);
|
|
||||||
st.BeginAnimation(ScaleTransform.ScaleYProperty, shrink);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>마우스 오버 시 텍스트가 살짝 튀어오르는 바운스 애니메이션을 적용합니다.</summary>
|
|
||||||
/// <summary>
|
|
||||||
/// 마우스 오버 시 텍스트가 살짝 튀어오르는 바운스 애니메이션.
|
|
||||||
/// Scale과 달리 크기가 변하지 않아 인접 요소를 가리지 않습니다.
|
|
||||||
/// </summary>
|
|
||||||
private static void ApplyHoverBounceAnimation(FrameworkElement element, double bounceY = -2.5)
|
|
||||||
{
|
|
||||||
void EnsureTransform()
|
|
||||||
{
|
|
||||||
if (element.RenderTransform is not TranslateTransform || element.RenderTransform.IsFrozen)
|
|
||||||
element.RenderTransform = new TranslateTransform(0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
element.Loaded += (_, _) => EnsureTransform();
|
|
||||||
|
|
||||||
element.MouseEnter += (_, _) =>
|
|
||||||
{
|
|
||||||
EnsureTransform();
|
|
||||||
var tt = (TranslateTransform)element.RenderTransform;
|
|
||||||
tt.BeginAnimation(TranslateTransform.YProperty,
|
|
||||||
new DoubleAnimation(bounceY, TimeSpan.FromMilliseconds(200))
|
|
||||||
{ EasingFunction = new CubicEase { EasingMode = EasingMode.EaseOut } });
|
|
||||||
};
|
|
||||||
element.MouseLeave += (_, _) =>
|
|
||||||
{
|
|
||||||
EnsureTransform();
|
|
||||||
var tt = (TranslateTransform)element.RenderTransform;
|
|
||||||
tt.BeginAnimation(TranslateTransform.YProperty,
|
|
||||||
new DoubleAnimation(0, TimeSpan.FromMilliseconds(250))
|
|
||||||
{ EasingFunction = new ElasticEase { EasingMode = EasingMode.EaseOut, Oscillations = 1, Springiness = 10 } });
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>심플한 V 체크 아이콘을 생성합니다 (디자인 통일용).</summary>
|
|
||||||
private static FrameworkElement CreateSimpleCheck(Brush color, double size = 14)
|
|
||||||
{
|
|
||||||
return new System.Windows.Shapes.Path
|
|
||||||
{
|
|
||||||
Data = Geometry.Parse($"M {size * 0.15} {size * 0.5} L {size * 0.4} {size * 0.75} L {size * 0.85} {size * 0.28}"),
|
|
||||||
Stroke = color,
|
|
||||||
StrokeThickness = 2,
|
|
||||||
StrokeStartLineCap = PenLineCap.Round,
|
|
||||||
StrokeEndLineCap = PenLineCap.Round,
|
|
||||||
StrokeLineJoin = PenLineJoin.Round,
|
|
||||||
Width = size,
|
|
||||||
Height = size,
|
|
||||||
Margin = new Thickness(0, 0, 10, 0),
|
|
||||||
VerticalAlignment = VerticalAlignment.Center,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>팝업 메뉴 항목에 호버 배경색 + 미세 확대 효과를 적용합니다.</summary>
|
|
||||||
private static void ApplyMenuItemHover(Border item)
|
|
||||||
{
|
|
||||||
var originalBg = item.Background?.Clone() ?? Brushes.Transparent;
|
|
||||||
if (originalBg.CanFreeze) originalBg.Freeze();
|
|
||||||
item.RenderTransformOrigin = new Point(0.5, 0.5);
|
|
||||||
item.RenderTransform = new ScaleTransform(1, 1);
|
|
||||||
item.MouseEnter += (s, _) =>
|
|
||||||
{
|
|
||||||
if (s is Border b)
|
|
||||||
{
|
|
||||||
// 원래 배경이 투명이면 반투명 흰색, 아니면 밝기 변경
|
|
||||||
if (originalBg is SolidColorBrush scb && scb.Color.A > 0x20)
|
|
||||||
b.Opacity = 0.85;
|
|
||||||
else
|
|
||||||
b.Background = new SolidColorBrush(Color.FromArgb(0x18, 0xFF, 0xFF, 0xFF));
|
|
||||||
}
|
|
||||||
var st = item.RenderTransform as ScaleTransform;
|
|
||||||
st?.BeginAnimation(ScaleTransform.ScaleXProperty, new DoubleAnimation(1.02, TimeSpan.FromMilliseconds(120)));
|
|
||||||
st?.BeginAnimation(ScaleTransform.ScaleYProperty, new DoubleAnimation(1.02, TimeSpan.FromMilliseconds(120)));
|
|
||||||
};
|
|
||||||
item.MouseLeave += (s, _) =>
|
|
||||||
{
|
|
||||||
if (s is Border b)
|
|
||||||
{
|
|
||||||
b.Opacity = 1.0;
|
|
||||||
b.Background = originalBg;
|
|
||||||
}
|
|
||||||
var st = item.RenderTransform as ScaleTransform;
|
|
||||||
st?.BeginAnimation(ScaleTransform.ScaleXProperty, new DoubleAnimation(1.0, TimeSpan.FromMilliseconds(150)));
|
|
||||||
st?.BeginAnimation(ScaleTransform.ScaleYProperty, new DoubleAnimation(1.0, TimeSpan.FromMilliseconds(150)));
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private Button CreateActionButton(string symbol, string tooltip, Brush foreground, Action onClick)
|
|
||||||
{
|
|
||||||
var hoverBrush = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
|
|
||||||
var hoverBg = TryFindResource("ItemHoverBackground") as Brush
|
|
||||||
?? new SolidColorBrush(Color.FromArgb(0x10, 0xFF, 0xFF, 0xFF));
|
|
||||||
var borderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.Gray;
|
|
||||||
var icon = new TextBlock
|
|
||||||
{
|
|
||||||
Text = symbol,
|
|
||||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
|
||||||
FontSize = 10,
|
|
||||||
Foreground = foreground,
|
|
||||||
VerticalAlignment = VerticalAlignment.Center
|
|
||||||
};
|
|
||||||
var btn = new Button
|
|
||||||
{
|
|
||||||
Content = icon,
|
|
||||||
Background = Brushes.Transparent,
|
|
||||||
BorderBrush = borderBrush,
|
|
||||||
BorderThickness = new Thickness(1),
|
|
||||||
Cursor = Cursors.Hand,
|
|
||||||
Width = 24,
|
|
||||||
Height = 24,
|
|
||||||
Padding = new Thickness(0),
|
|
||||||
Margin = new Thickness(0, 0, 2, 0),
|
|
||||||
ToolTip = tooltip
|
|
||||||
};
|
|
||||||
btn.Template = BuildMinimalIconButtonTemplate();
|
|
||||||
btn.MouseEnter += (_, _) =>
|
|
||||||
{
|
|
||||||
icon.Foreground = hoverBrush;
|
|
||||||
btn.Background = hoverBg;
|
|
||||||
};
|
|
||||||
btn.MouseLeave += (_, _) =>
|
|
||||||
{
|
|
||||||
icon.Foreground = foreground;
|
|
||||||
btn.Background = Brushes.Transparent;
|
|
||||||
};
|
|
||||||
btn.Click += (_, _) => onClick();
|
|
||||||
return btn;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ShowMessageActionBar(StackPanel actionBar)
|
|
||||||
{
|
|
||||||
if (actionBar == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
actionBar.Opacity = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void HideMessageActionBarIfNotSelected(StackPanel actionBar)
|
|
||||||
{
|
|
||||||
if (actionBar == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (!ReferenceEquals(_selectedMessageActionBar, actionBar))
|
|
||||||
actionBar.Opacity = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void SelectMessageActionBar(StackPanel actionBar, Border? messageBorder = null)
|
|
||||||
{
|
|
||||||
if (_selectedMessageActionBar != null && !ReferenceEquals(_selectedMessageActionBar, actionBar))
|
|
||||||
_selectedMessageActionBar.Opacity = 0;
|
|
||||||
|
|
||||||
if (_selectedMessageBorder != null && !ReferenceEquals(_selectedMessageBorder, messageBorder))
|
|
||||||
ApplyMessageSelectionStyle(_selectedMessageBorder, false);
|
|
||||||
|
|
||||||
_selectedMessageActionBar = actionBar;
|
|
||||||
_selectedMessageActionBar.Opacity = 1;
|
|
||||||
_selectedMessageBorder = messageBorder;
|
|
||||||
if (_selectedMessageBorder != null)
|
|
||||||
ApplyMessageSelectionStyle(_selectedMessageBorder, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ApplyMessageSelectionStyle(Border border, bool selected)
|
|
||||||
{
|
|
||||||
if (border == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var accent = TryFindResource("AccentColor") as Brush ?? Brushes.CornflowerBlue;
|
|
||||||
var defaultBorder = TryFindResource("BorderColor") as Brush ?? Brushes.Gray;
|
|
||||||
border.BorderBrush = selected ? accent : defaultBorder;
|
|
||||||
border.BorderThickness = selected ? new Thickness(1.5) : new Thickness(1);
|
|
||||||
border.Effect = selected
|
|
||||||
? new System.Windows.Media.Effects.DropShadowEffect
|
|
||||||
{
|
|
||||||
BlurRadius = 16,
|
|
||||||
ShadowDepth = 0,
|
|
||||||
Opacity = 0.10,
|
|
||||||
Color = Colors.Black,
|
|
||||||
}
|
|
||||||
: null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static ControlTemplate BuildMinimalIconButtonTemplate()
|
|
||||||
{
|
|
||||||
var template = new ControlTemplate(typeof(Button));
|
|
||||||
var border = new FrameworkElementFactory(typeof(Border));
|
|
||||||
border.SetValue(Border.BackgroundProperty, new TemplateBindingExtension(Button.BackgroundProperty));
|
|
||||||
border.SetValue(Border.BorderBrushProperty, new TemplateBindingExtension(Button.BorderBrushProperty));
|
|
||||||
border.SetValue(Border.BorderThicknessProperty, new TemplateBindingExtension(Button.BorderThicknessProperty));
|
|
||||||
border.SetValue(Border.CornerRadiusProperty, new CornerRadius(8));
|
|
||||||
border.SetValue(Border.PaddingProperty, new TemplateBindingExtension(Button.PaddingProperty));
|
|
||||||
var presenter = new FrameworkElementFactory(typeof(ContentPresenter));
|
|
||||||
presenter.SetValue(ContentPresenter.HorizontalAlignmentProperty, HorizontalAlignment.Center);
|
|
||||||
presenter.SetValue(ContentPresenter.VerticalAlignmentProperty, VerticalAlignment.Center);
|
|
||||||
border.AppendChild(presenter);
|
|
||||||
template.VisualTree = border;
|
|
||||||
return template;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// ─── 스트리밍 커서 깜빡임 + AI 아이콘 펄스 ────────────────────────────
|
// ─── 스트리밍 커서 깜빡임 + AI 아이콘 펄스 ────────────────────────────
|
||||||
|
|
||||||
private void StopAiIconPulse()
|
private void StopAiIconPulse()
|
||||||
|
|||||||
Reference in New Issue
Block a user