first commit
This commit is contained in:
commit
ac5da6939f
36
.gitignore
vendored
Normal file
36
.gitignore
vendored
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
# Build output
|
||||||
|
bin/
|
||||||
|
obj/
|
||||||
|
publish/
|
||||||
|
artifacts/
|
||||||
|
TestResults/
|
||||||
|
|
||||||
|
# Visual Studio / MSBuild
|
||||||
|
.vs/
|
||||||
|
*.suo
|
||||||
|
*.user
|
||||||
|
*.userosscache
|
||||||
|
*.sln.docstates
|
||||||
|
*.csproj.user
|
||||||
|
*.tmp
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Rider / Resharper
|
||||||
|
.idea/
|
||||||
|
*.sln.iml
|
||||||
|
_ReSharper*/
|
||||||
|
*.DotSettings.user
|
||||||
|
|
||||||
|
# VS Code
|
||||||
|
.vscode/
|
||||||
|
|
||||||
|
# Cursor
|
||||||
|
.cursor/
|
||||||
|
|
||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
|
||||||
|
# OS files
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
364
AGENTS.md
Normal file
364
AGENTS.md
Normal file
@ -0,0 +1,364 @@
|
|||||||
|
# AmebaPro3 Control Panel Development Agent Specification (agents.md)
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
A Windows desktop control panel for Realtek AmebaPro3 SoC bring-up, debugging, and automation.
|
||||||
|
|
||||||
|
Target stack:
|
||||||
|
|
||||||
|
* C#
|
||||||
|
* .NET 8
|
||||||
|
* WPF (preferred over WinForms)
|
||||||
|
* Windows 11-like modern styling using native WPF only
|
||||||
|
* Phase 1: UI-only skeleton (baseline, no real hardware access)
|
||||||
|
|
||||||
|
Goals:
|
||||||
|
|
||||||
|
* Modern, clean, engineering-oriented UI
|
||||||
|
* Stable build and runtime (no third-party UI dependencies)
|
||||||
|
* Layout and component structure that supports future services (UART, J-Link, flashing)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## UI Framework & Styling (STRICT)
|
||||||
|
|
||||||
|
### Mandatory
|
||||||
|
|
||||||
|
* Framework: WPF
|
||||||
|
* Target: .NET 8 (e.g. net8.0-windows)
|
||||||
|
* No third-party UI libraries (no theme packages)
|
||||||
|
* Style target: Windows 11-like (cards, rounded corners, subtle borders, simple states)
|
||||||
|
* Styling technique: XAML ResourceDictionaries + Styles + ControlTemplates
|
||||||
|
|
||||||
|
### Forbidden
|
||||||
|
|
||||||
|
* x WPF-UI / lepoco, MahApps, MaterialDesign, FluentWPF, HandyControl, etc.
|
||||||
|
* x UWP / WinUI 3
|
||||||
|
* x Windows Store dependency
|
||||||
|
* x Runtime hardware access in Phase 1
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Architecture Principles
|
||||||
|
|
||||||
|
* UI-first development
|
||||||
|
* MVVM-friendly
|
||||||
|
* Clear separation between:
|
||||||
|
* UI (Views + ViewModels)
|
||||||
|
* Device services (UART/J-Link services)
|
||||||
|
* Debug services (future)
|
||||||
|
* Every device/session has independent console and command history
|
||||||
|
* Console/log UI must stay responsive under heavy output
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Solution Structure
|
||||||
|
|
||||||
|
* `App.xaml`
|
||||||
|
* Loads theme resources (Light by default)
|
||||||
|
* `Themes/Colors.Light.xaml` / `Themes/Colors.Dark.xaml`
|
||||||
|
* `Themes/Controls.xaml` (Button/TextBox/TabControl/List styles)
|
||||||
|
* `Views/MainWindow.xaml`
|
||||||
|
* `Views/Pages/MainPage.xaml`
|
||||||
|
* `Views/Pages/JLinkPage.xaml`
|
||||||
|
* `Views/Controls/ConsolePanel.xaml` (reusable)
|
||||||
|
* `ViewModels/*`
|
||||||
|
* `Services/Uart/*`
|
||||||
|
* `Scripts/uart_bridge.py`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Styling Contract (native WPF)
|
||||||
|
|
||||||
|
* Font: Segoe UI Variable (fallback: Segoe UI)
|
||||||
|
* Buttons: height 36, radius 10, consistent padding
|
||||||
|
* Inputs: radius 10, clear focus border
|
||||||
|
* Cards: radius 12, subtle border, optional very light shadow
|
||||||
|
* Accent color used only for primary actions / selected tab / selected nav
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Main Application Layout
|
||||||
|
|
||||||
|
### MainWindow.xaml
|
||||||
|
|
||||||
|
Purpose: Host top navigation + page content.
|
||||||
|
|
||||||
|
Root Layout:
|
||||||
|
|
||||||
|
* `Grid`
|
||||||
|
* `Row 0` = `Auto` (TopBar)
|
||||||
|
* `Row 1` = `*` (PageContent)
|
||||||
|
* `Row 2` = `Auto` (StatusBar)
|
||||||
|
|
||||||
|
Row 0: TopBar (Auto, ~56px)
|
||||||
|
|
||||||
|
* Left: App title `TextBlock` ("AmebaPro3 Control Panel")
|
||||||
|
* Right: `ListBox` navigation for page switching
|
||||||
|
* Tab 1: Panel (MainPage)
|
||||||
|
* Tab 2: Debugger (JLinkPage)
|
||||||
|
* Optional theme toggle or window buttons
|
||||||
|
|
||||||
|
Row 1: PageContent
|
||||||
|
|
||||||
|
* `ContentControl` bound to `CurrentPageViewModel`
|
||||||
|
|
||||||
|
Row 2: StatusBar (Auto, ~28px)
|
||||||
|
|
||||||
|
* Left: Selected device + connection status text
|
||||||
|
* Right: `Last RX time`, `RX bytes`, `TX bytes`
|
||||||
|
|
||||||
|
Naming (important):
|
||||||
|
|
||||||
|
* Main container: `MainRootGrid`
|
||||||
|
* Page host: `MainContentHost`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Pages
|
||||||
|
|
||||||
|
### 1) MainPage.xaml (Panel)
|
||||||
|
|
||||||
|
Purpose: Combine Flash Image, Arduino control, and AmebaPro3 UART in a split layout.
|
||||||
|
|
||||||
|
Root Layout:
|
||||||
|
|
||||||
|
* `Grid` with two columns
|
||||||
|
* Column 0 (Left, wider): `*` (AmebaPro3 UART)
|
||||||
|
* Column 1 (Right, narrower): ~480 (Flash + Arduino)
|
||||||
|
* Column gap: 12
|
||||||
|
|
||||||
|
Column 0: AmebaPro3 UART (largest area)
|
||||||
|
|
||||||
|
* A single `ConsolePanel` instance configured for AmebaPro3
|
||||||
|
* Header controls inside ConsolePanel:
|
||||||
|
* COM port dropdown
|
||||||
|
* Baud rate input (default 1500000)
|
||||||
|
* Connect / Disconnect button
|
||||||
|
* Optional TeraTerm launch button
|
||||||
|
|
||||||
|
Column 1: Right stack
|
||||||
|
|
||||||
|
* `Grid` with rows and a splitter:
|
||||||
|
* Row 0 = Auto (Flash Card)
|
||||||
|
* Row 1 = 8 (Spacing)
|
||||||
|
* Row 2 = * (Arduino Card)
|
||||||
|
|
||||||
|
#### Flash Image Card (Row 0, Auto)
|
||||||
|
|
||||||
|
Card container: `Border` (CardStyle)
|
||||||
|
Contents:
|
||||||
|
|
||||||
|
* Section title: `TextBlock` "Flash Image"
|
||||||
|
* `Grid` with rows:
|
||||||
|
* Bootloader file picker (`TextBox` readonly + `Browse` button)
|
||||||
|
* Application file picker (`TextBox` readonly + `Browse` button)
|
||||||
|
* `Flash Image` primary button (full width)
|
||||||
|
|
||||||
|
Control names:
|
||||||
|
|
||||||
|
* `BootloaderPathTextBox`, `BrowseBootloaderButton`
|
||||||
|
* `AppPathTextBox`, `BrowseAppButton`
|
||||||
|
* `FlashImageButton`
|
||||||
|
|
||||||
|
#### Arduino Controller Card (Row 2, *)
|
||||||
|
|
||||||
|
Card container: `Border` (CardStyle)
|
||||||
|
Contents (top to bottom):
|
||||||
|
|
||||||
|
1. Connection row:
|
||||||
|
* COM dropdown + Baud textbox (default 115200) + Connect button
|
||||||
|
2. Mode buttons (uniform size, aligned, share whole row width):
|
||||||
|
* Reset, Download Mode, Normal Mode, Test Mode
|
||||||
|
3. Test mode selector:
|
||||||
|
* `ComboBox` "Test 1" to "Test 7"
|
||||||
|
|
||||||
|
Control names:
|
||||||
|
|
||||||
|
* `ArduinoComComboBox`, `ArduinoBaudTextBox`, `ArduinoConnectButton`
|
||||||
|
* `ArduinoResetButton`, `ArduinoDownloadButton`, `ArduinoNormalButton`, `ArduinoTestModeButton`
|
||||||
|
* `ArduinoTestSelectComboBox`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2) JLinkPage.xaml (Debugger)
|
||||||
|
|
||||||
|
Purpose: Multi-AP debugging for AmebaPro3.
|
||||||
|
|
||||||
|
Root Layout:
|
||||||
|
|
||||||
|
* `Grid` with columns:
|
||||||
|
* Column 0 = `*` (Main AP console workspace)
|
||||||
|
* Column 1 = `280` (AP selector pane)
|
||||||
|
* Column gap: 12
|
||||||
|
|
||||||
|
Top area (inside Column 0, Row 0 Auto):
|
||||||
|
|
||||||
|
* J-Link selection row:
|
||||||
|
* `ComboBox` for J-Link serial number
|
||||||
|
* `Refresh` button (optional)
|
||||||
|
* `Connect` button (Phase 1 dummy)
|
||||||
|
|
||||||
|
Workspace area (Column 0, Row 1 *):
|
||||||
|
|
||||||
|
* `ContentControl` showing selected AP view
|
||||||
|
* Each AP view contains one `ConsolePanel` configured for that AP
|
||||||
|
|
||||||
|
Right AP selector pane (Column 1):
|
||||||
|
|
||||||
|
* Card container + `ListBox` (navigation style)
|
||||||
|
* Items:
|
||||||
|
* Cortex M33 - Processor NP
|
||||||
|
* Cortex M33 - Processor MP
|
||||||
|
* Cortex M23 - Processor FP
|
||||||
|
* Cortex CA32 - Processor AP
|
||||||
|
|
||||||
|
AP selector control name:
|
||||||
|
|
||||||
|
* `ApSelectorListBox`
|
||||||
|
|
||||||
|
AP sessions:
|
||||||
|
|
||||||
|
* Each AP has independent ConsolePanel state (log + history + command input)
|
||||||
|
|
||||||
|
Debugger command buttons (inside ConsolePanel `CommandButtons` slot):
|
||||||
|
|
||||||
|
* Halt, Continue, Step In, Step Over, Step Out
|
||||||
|
* File picker that lets user load multiple commands from file
|
||||||
|
* Command logic should support all JLink basic commands
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
|
||||||
|
* When AP connected, Phase 2+ should not halt CPU by default
|
||||||
|
* Designed to integrate Segger J-Link + GDB later
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Reusable UI Components
|
||||||
|
|
||||||
|
### ConsolePanel.xaml (Core component)
|
||||||
|
|
||||||
|
Must be reusable across:
|
||||||
|
|
||||||
|
* AmebaPro3 UART
|
||||||
|
* Arduino UART (optional)
|
||||||
|
* J-Link AP sessions
|
||||||
|
|
||||||
|
#### ConsolePanel Layout (codex-ready)
|
||||||
|
|
||||||
|
Root: `Grid`
|
||||||
|
|
||||||
|
* Row 0 = Auto (Header, ~48px)
|
||||||
|
* Row 1 = * (Log viewer)
|
||||||
|
* Row 2 = Auto (GridSplitter)
|
||||||
|
* Row 3 = * (History list, min ~140px)
|
||||||
|
* Row 4 = Auto (CommandButtons slot)
|
||||||
|
* Row 5 = Auto (Actions row)
|
||||||
|
* Row 6 = Auto (Input row, ~44px)
|
||||||
|
|
||||||
|
Row 0: Header
|
||||||
|
|
||||||
|
* Left: `Title` + status text
|
||||||
|
* Right: header content slot (page-specific controls)
|
||||||
|
|
||||||
|
Row 1: Log viewer
|
||||||
|
|
||||||
|
* Must support horizontal + vertical scrolling
|
||||||
|
* Read-only
|
||||||
|
* Monospace option
|
||||||
|
* Performance: use a virtualization-friendly approach
|
||||||
|
* Can scroll up, down, left, right
|
||||||
|
* `ListBox`/`ItemsControl` virtualized lines (one line per item)
|
||||||
|
|
||||||
|
Row 3: Command history
|
||||||
|
|
||||||
|
* `ListBox` (virtualized)
|
||||||
|
* Behaviors:
|
||||||
|
* Latest command at bottom
|
||||||
|
* No duplicate command entries
|
||||||
|
* Up/Down navigates history
|
||||||
|
* Single click selects and fills command input
|
||||||
|
* Double click sends command
|
||||||
|
|
||||||
|
Row 5: Actions row
|
||||||
|
|
||||||
|
* Clear, Save, Load Commands
|
||||||
|
|
||||||
|
Row 6: Input row
|
||||||
|
|
||||||
|
* `Grid` with columns:
|
||||||
|
* Column 0 = * command input `TextBox`
|
||||||
|
* Column 1 = 8 spacing
|
||||||
|
* Column 2 = Auto send `Button`
|
||||||
|
* Press Enter triggers Send
|
||||||
|
* After send: clear input
|
||||||
|
|
||||||
|
Optional slot: `CommandButtons` area (for debugger step/halt/continue)
|
||||||
|
|
||||||
|
* Placed between history and actions row
|
||||||
|
|
||||||
|
#### ConsolePanel Public Bindings (ViewModel properties)
|
||||||
|
|
||||||
|
* `string Title`
|
||||||
|
* `bool IsConnected`
|
||||||
|
* `ObservableCollection<string> LogLines`
|
||||||
|
* `ObservableCollection<string> History`
|
||||||
|
* `string CurrentCommand`
|
||||||
|
* `ICommand SendCommand`
|
||||||
|
* `ICommand ClearLog`
|
||||||
|
* `ICommand SaveLog`
|
||||||
|
* `ICommand LoadCommandFileCommand`
|
||||||
|
* `UIElement CommandButtonsContent` (optional)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase Roadmap
|
||||||
|
|
||||||
|
### Phase 1 (Baseline)
|
||||||
|
|
||||||
|
* UI-only
|
||||||
|
* All pages render correctly
|
||||||
|
* Dummy content / placeholder logs
|
||||||
|
* No hardware access
|
||||||
|
|
||||||
|
### Phase 2 (In progress)
|
||||||
|
|
||||||
|
* UART service (AmebaPro3) using Python + pyserial bridge (`Scripts/uart_bridge.py`)
|
||||||
|
* Optional TeraTerm launch integration
|
||||||
|
* Implement history behaviors
|
||||||
|
|
||||||
|
### Phase 3
|
||||||
|
|
||||||
|
* J-Link session management
|
||||||
|
* Multi-AP GDB integration
|
||||||
|
* Session lifecycle control
|
||||||
|
|
||||||
|
### Phase 4
|
||||||
|
|
||||||
|
* Flash pipeline
|
||||||
|
* C++ integration (Prefer)
|
||||||
|
* Python tool integration
|
||||||
|
* Automation & scripting
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Coding Rules for Agents
|
||||||
|
|
||||||
|
* Keep UI logic independent
|
||||||
|
* No blocking calls on UI thread
|
||||||
|
* One function = one job
|
||||||
|
* Prefer clarity over cleverness
|
||||||
|
* Engineering tool mindset
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
This project is a professional SoC control panel focused on:
|
||||||
|
|
||||||
|
* Clarity
|
||||||
|
* Stability (no UI library dependencies)
|
||||||
|
* Expandability
|
||||||
|
* Multi-device / multi-session workflows
|
||||||
|
|
||||||
|
UI correctness and structure come before any hardware logic.
|
||||||
15
AmebaPro3_ControlPanel.csproj
Normal file
15
AmebaPro3_ControlPanel.csproj
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>WinExe</OutputType>
|
||||||
|
<TargetFramework>net8.0-windows</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<UseWPF>true</UseWPF>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<None Include="Scripts\uart_bridge.py" CopyToOutputDirectory="PreserveNewest" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
3
AmebaPro3_ControlPanel.slnx
Normal file
3
AmebaPro3_ControlPanel.slnx
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<Solution>
|
||||||
|
<Project Path="AmebaPro3_ControlPanel.csproj" />
|
||||||
|
</Solution>
|
||||||
51
App.xaml
Normal file
51
App.xaml
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
<Application x:Class="AmebaPro3_ControlPanel.App"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:pages="clr-namespace:AmebaPro3_ControlPanel.Views.Pages"
|
||||||
|
xmlns:controls="clr-namespace:AmebaPro3_ControlPanel.Views.Controls"
|
||||||
|
xmlns:vm="clr-namespace:AmebaPro3_ControlPanel.ViewModels"
|
||||||
|
StartupUri="Views/MainWindow.xaml">
|
||||||
|
<Application.Resources>
|
||||||
|
<ResourceDictionary>
|
||||||
|
<ResourceDictionary.MergedDictionaries>
|
||||||
|
<ResourceDictionary Source="Themes/Colors.Light.xaml"/>
|
||||||
|
<ResourceDictionary Source="Themes/Controls.xaml"/>
|
||||||
|
</ResourceDictionary.MergedDictionaries>
|
||||||
|
|
||||||
|
<DataTemplate DataType="{x:Type vm:MainPageViewModel}">
|
||||||
|
<pages:MainPage/>
|
||||||
|
</DataTemplate>
|
||||||
|
<DataTemplate DataType="{x:Type vm:JLinkPageViewModel}">
|
||||||
|
<pages:JLinkPage/>
|
||||||
|
</DataTemplate>
|
||||||
|
<DataTemplate DataType="{x:Type vm:JLinkApSessionViewModel}">
|
||||||
|
<controls:ConsolePanel>
|
||||||
|
<controls:ConsolePanel.CommandButtonsContent>
|
||||||
|
<UniformGrid Columns="5" HorizontalAlignment="Stretch">
|
||||||
|
<Button Content="Halt"
|
||||||
|
Command="{Binding HaltCommand}"
|
||||||
|
Style="{StaticResource BaseButtonStyle}"
|
||||||
|
Margin="0,0,4,0"/>
|
||||||
|
<Button Content="Continue"
|
||||||
|
Command="{Binding ContinueCommand}"
|
||||||
|
Style="{StaticResource BaseButtonStyle}"
|
||||||
|
Margin="4,0,4,0"/>
|
||||||
|
<Button Content="Step In"
|
||||||
|
Command="{Binding StepInCommand}"
|
||||||
|
Style="{StaticResource BaseButtonStyle}"
|
||||||
|
Margin="4,0,4,0"/>
|
||||||
|
<Button Content="Step Over"
|
||||||
|
Command="{Binding StepOverCommand}"
|
||||||
|
Style="{StaticResource BaseButtonStyle}"
|
||||||
|
Margin="4,0,4,0"/>
|
||||||
|
<Button Content="Step Out"
|
||||||
|
Command="{Binding StepOutCommand}"
|
||||||
|
Style="{StaticResource BaseButtonStyle}"
|
||||||
|
Margin="4,0,0,0"/>
|
||||||
|
</UniformGrid>
|
||||||
|
</controls:ConsolePanel.CommandButtonsContent>
|
||||||
|
</controls:ConsolePanel>
|
||||||
|
</DataTemplate>
|
||||||
|
</ResourceDictionary>
|
||||||
|
</Application.Resources>
|
||||||
|
</Application>
|
||||||
13
App.xaml.cs
Normal file
13
App.xaml.cs
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
using System.Configuration;
|
||||||
|
using System.Data;
|
||||||
|
using System.Windows;
|
||||||
|
|
||||||
|
namespace AmebaPro3_ControlPanel;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Interaction logic for App.xaml
|
||||||
|
/// </summary>
|
||||||
|
public partial class App : Application
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
10
AssemblyInfo.cs
Normal file
10
AssemblyInfo.cs
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
using System.Windows;
|
||||||
|
|
||||||
|
[assembly:ThemeInfo(
|
||||||
|
ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
|
||||||
|
//(used if a resource is not found in the page,
|
||||||
|
// or application resource dictionaries)
|
||||||
|
ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
|
||||||
|
//(used if a resource is not found in the page,
|
||||||
|
// app, or any theme specific resource dictionaries)
|
||||||
|
)]
|
||||||
27
Scripts/build_portable.sh
Normal file
27
Scripts/build_portable.sh
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
root_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
project_path="$root_dir/AmebaPro3_ControlPanel.csproj"
|
||||||
|
|
||||||
|
runtime="${1:-win-x64}"
|
||||||
|
configuration="${2:-Release}"
|
||||||
|
publish_dir="${PUBLISH_DIR:-$root_dir/publish/portable-$runtime}"
|
||||||
|
|
||||||
|
echo "Publishing portable build"
|
||||||
|
echo "Project: $project_path"
|
||||||
|
echo "Runtime: $runtime"
|
||||||
|
echo "Config : $configuration"
|
||||||
|
echo "Output : $publish_dir"
|
||||||
|
|
||||||
|
dotnet publish "$project_path" \
|
||||||
|
-c "$configuration" \
|
||||||
|
-r "$runtime" \
|
||||||
|
--self-contained true \
|
||||||
|
-p:EnableWindowsTargeting=true \
|
||||||
|
-o "$publish_dir" \
|
||||||
|
-p:PublishSingleFile=true \
|
||||||
|
-p:IncludeNativeLibrariesForSelfExtract=true \
|
||||||
|
-p:EnableCompressionInSingleFile=true
|
||||||
|
|
||||||
|
echo "Done. Output in: $publish_dir"
|
||||||
91
Scripts/uart_bridge.py
Normal file
91
Scripts/uart_bridge.py
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
import argparse
|
||||||
|
import sys
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
|
||||||
|
|
||||||
|
def _load_serial():
|
||||||
|
try:
|
||||||
|
import serial # type: ignore
|
||||||
|
except ImportError:
|
||||||
|
sys.stderr.write("pyserial is not installed. Run: pip install pyserial\n")
|
||||||
|
sys.stderr.flush()
|
||||||
|
sys.exit(2)
|
||||||
|
return serial
|
||||||
|
|
||||||
|
|
||||||
|
def _reader_loop(ser, stop_event):
|
||||||
|
buffer = bytearray()
|
||||||
|
while not stop_event.is_set():
|
||||||
|
try:
|
||||||
|
data = ser.read(ser.in_waiting or 1)
|
||||||
|
except Exception as exc:
|
||||||
|
sys.stderr.write(f"serial read error: {exc}\n")
|
||||||
|
sys.stderr.flush()
|
||||||
|
break
|
||||||
|
|
||||||
|
if data:
|
||||||
|
buffer.extend(data)
|
||||||
|
while b"\n" in buffer:
|
||||||
|
line, _, rest = buffer.partition(b"\n")
|
||||||
|
buffer = bytearray(rest)
|
||||||
|
text = line.decode("utf-8", errors="replace").rstrip("\r")
|
||||||
|
sys.stdout.write(text + "\n")
|
||||||
|
sys.stdout.flush()
|
||||||
|
else:
|
||||||
|
time.sleep(0.01)
|
||||||
|
|
||||||
|
if buffer:
|
||||||
|
text = buffer.decode("utf-8", errors="replace").rstrip("\r")
|
||||||
|
sys.stdout.write(text + "\n")
|
||||||
|
sys.stdout.flush()
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description="UART bridge using pyserial.")
|
||||||
|
parser.add_argument("--port", required=True, help="COM port name, e.g. COM3")
|
||||||
|
parser.add_argument("--baud", type=int, required=True, help="Baud rate")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
serial = _load_serial()
|
||||||
|
|
||||||
|
try:
|
||||||
|
ser = serial.Serial(args.port, args.baud, timeout=0.1, write_timeout=1)
|
||||||
|
except Exception as exc:
|
||||||
|
sys.stderr.write(f"failed to open {args.port} @ {args.baud}: {exc}\n")
|
||||||
|
sys.stderr.flush()
|
||||||
|
return 1
|
||||||
|
|
||||||
|
sys.stdout.write(f"[UART] OPEN {args.port} @ {args.baud}\n")
|
||||||
|
sys.stdout.flush()
|
||||||
|
|
||||||
|
stop_event = threading.Event()
|
||||||
|
reader = threading.Thread(target=_reader_loop, args=(ser, stop_event), daemon=True)
|
||||||
|
reader.start()
|
||||||
|
|
||||||
|
try:
|
||||||
|
for line in sys.stdin:
|
||||||
|
command = line.rstrip("\n")
|
||||||
|
if command == "__exit__":
|
||||||
|
break
|
||||||
|
if not command:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
ser.write(command.encode("utf-8") + b"\r\n")
|
||||||
|
ser.flush()
|
||||||
|
except Exception as exc:
|
||||||
|
sys.stderr.write(f"serial write error: {exc}\n")
|
||||||
|
sys.stderr.flush()
|
||||||
|
break
|
||||||
|
finally:
|
||||||
|
stop_event.set()
|
||||||
|
try:
|
||||||
|
ser.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
34
Services/Uart/PortDiscovery.cs
Normal file
34
Services/Uart/PortDiscovery.cs
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using Microsoft.Win32;
|
||||||
|
|
||||||
|
namespace AmebaPro3_ControlPanel.Services.Uart;
|
||||||
|
|
||||||
|
public static class PortDiscovery
|
||||||
|
{
|
||||||
|
public static string[] GetPorts()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var key = Registry.LocalMachine.OpenSubKey(@"HARDWARE\\DEVICEMAP\\SERIALCOMM");
|
||||||
|
if (key == null)
|
||||||
|
{
|
||||||
|
return Array.Empty<string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
var ports = key.GetValueNames()
|
||||||
|
.Select(name => key.GetValue(name))
|
||||||
|
.OfType<string>()
|
||||||
|
.Where(value => !string.IsNullOrWhiteSpace(value))
|
||||||
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||||
|
.OrderBy(value => value, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
return ports;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return Array.Empty<string>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
238
Services/Uart/PythonUartSession.cs
Normal file
238
Services/Uart/PythonUartSession.cs
Normal file
@ -0,0 +1,238 @@
|
|||||||
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace AmebaPro3_ControlPanel.Services.Uart;
|
||||||
|
|
||||||
|
public sealed class PythonUartSession : IDisposable
|
||||||
|
{
|
||||||
|
private readonly SynchronizationContext? _syncContext;
|
||||||
|
private readonly string _scriptPath;
|
||||||
|
private readonly SemaphoreSlim _stdinLock = new(1, 1);
|
||||||
|
private Process? _process;
|
||||||
|
private CancellationTokenSource? _cts;
|
||||||
|
|
||||||
|
public PythonUartSession(string scriptPath, SynchronizationContext? syncContext)
|
||||||
|
{
|
||||||
|
_scriptPath = scriptPath;
|
||||||
|
_syncContext = syncContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
public event Action<string>? LineReceived;
|
||||||
|
public event Action<string>? ErrorReceived;
|
||||||
|
public event Action? Exited;
|
||||||
|
|
||||||
|
public bool IsRunning => _process is { HasExited: false };
|
||||||
|
|
||||||
|
public async Task<bool> StartAsync(string port, int baudRate, string? pythonExecutable = null)
|
||||||
|
{
|
||||||
|
if (IsRunning)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(_scriptPath) || !File.Exists(_scriptPath))
|
||||||
|
{
|
||||||
|
PostError($"UART bridge script not found: {_scriptPath}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var candidates = pythonExecutable is null ? GetPythonCandidates() : new[] { pythonExecutable };
|
||||||
|
string? lastError = null;
|
||||||
|
|
||||||
|
foreach (var candidate in candidates)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(candidate))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var args = $"\"{_scriptPath}\" --port {port} --baud {baudRate}";
|
||||||
|
var startInfo = new ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = candidate,
|
||||||
|
Arguments = args,
|
||||||
|
RedirectStandardInput = true,
|
||||||
|
RedirectStandardOutput = true,
|
||||||
|
RedirectStandardError = true,
|
||||||
|
UseShellExecute = false,
|
||||||
|
CreateNoWindow = true
|
||||||
|
};
|
||||||
|
|
||||||
|
var process = new Process
|
||||||
|
{
|
||||||
|
StartInfo = startInfo,
|
||||||
|
EnableRaisingEvents = true
|
||||||
|
};
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!process.Start())
|
||||||
|
{
|
||||||
|
lastError = $"Failed to start {candidate}.";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
lastError = $"Failed to start {candidate}: {ex.Message}";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
_process = process;
|
||||||
|
_process.Exited += (_, _) => PostExit();
|
||||||
|
_cts = new CancellationTokenSource();
|
||||||
|
|
||||||
|
_ = Task.Run(() => ReadStreamAsync(_process.StandardOutput, PostLine, _cts.Token));
|
||||||
|
_ = Task.Run(() => ReadStreamAsync(_process.StandardError, PostError, _cts.Token));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
PostError(lastError ?? "Python not found. Install Python 3 and pyserial.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SendAsync(string command)
|
||||||
|
{
|
||||||
|
if (!IsRunning || _process == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _stdinLock.WaitAsync().ConfigureAwait(false);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (_process.HasExited)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _process.StandardInput.WriteLineAsync(command).ConfigureAwait(false);
|
||||||
|
await _process.StandardInput.FlushAsync().ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
PostError($"UART send failed: {ex.Message}");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_stdinLock.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task StopAsync()
|
||||||
|
{
|
||||||
|
if (_process == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!_process.HasExited)
|
||||||
|
{
|
||||||
|
await SendAsync("__exit__").ConfigureAwait(false);
|
||||||
|
if (!_process.WaitForExit(1000))
|
||||||
|
{
|
||||||
|
_process.Kill(entireProcessTree: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
CleanupProcess();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ReadStreamAsync(StreamReader reader, Action<string> handler, CancellationToken token)
|
||||||
|
{
|
||||||
|
while (!token.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
string? line;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
line = await reader.ReadLineAsync().ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (line == null)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(line))
|
||||||
|
{
|
||||||
|
handler(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Post(Action<string>? handler, string line)
|
||||||
|
{
|
||||||
|
if (handler == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_syncContext != null)
|
||||||
|
{
|
||||||
|
_syncContext.Post(_ => handler(line), null);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
handler(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void PostLine(string line) => Post(LineReceived, line);
|
||||||
|
|
||||||
|
private void PostError(string line) => Post(ErrorReceived, line);
|
||||||
|
|
||||||
|
private void PostExit()
|
||||||
|
{
|
||||||
|
if (_syncContext != null)
|
||||||
|
{
|
||||||
|
_syncContext.Post(_ => Exited?.Invoke(), null);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Exited?.Invoke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CleanupProcess()
|
||||||
|
{
|
||||||
|
_cts?.Cancel();
|
||||||
|
_cts?.Dispose();
|
||||||
|
_cts = null;
|
||||||
|
|
||||||
|
_process?.Dispose();
|
||||||
|
_process = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string?[] GetPythonCandidates()
|
||||||
|
{
|
||||||
|
var explicitPath = Environment.GetEnvironmentVariable("AMEBA_PYTHON");
|
||||||
|
if (!string.IsNullOrWhiteSpace(explicitPath))
|
||||||
|
{
|
||||||
|
return new[] { explicitPath, "python", "py" };
|
||||||
|
}
|
||||||
|
|
||||||
|
return new[] { "python", "py" };
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_stdinLock.Dispose();
|
||||||
|
CleanupProcess();
|
||||||
|
}
|
||||||
|
}
|
||||||
80
Services/Uart/TeraTermLauncher.cs
Normal file
80
Services/Uart/TeraTermLauncher.cs
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
|
||||||
|
namespace AmebaPro3_ControlPanel.Services.Uart;
|
||||||
|
|
||||||
|
public static class TeraTermLauncher
|
||||||
|
{
|
||||||
|
public static bool TryLaunch(string? port, int baudRate, out string message)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(port))
|
||||||
|
{
|
||||||
|
message = "Select a COM port first.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var exePath = ResolveTeraTermPath();
|
||||||
|
if (string.IsNullOrWhiteSpace(exePath))
|
||||||
|
{
|
||||||
|
message = "Tera Term not found. Set TERATERM_PATH or install it.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var args = $"/C={port} /BAUD={baudRate}";
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Process.Start(new ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = exePath,
|
||||||
|
Arguments = args,
|
||||||
|
UseShellExecute = true
|
||||||
|
});
|
||||||
|
message = "Tera Term launched.";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
message = $"Failed to start Tera Term: {ex.Message}";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? ResolveTeraTermPath()
|
||||||
|
{
|
||||||
|
var envPath = Environment.GetEnvironmentVariable("TERATERM_PATH");
|
||||||
|
if (!string.IsNullOrWhiteSpace(envPath))
|
||||||
|
{
|
||||||
|
var candidate = Directory.Exists(envPath)
|
||||||
|
? Path.Combine(envPath, "ttermpro.exe")
|
||||||
|
: envPath;
|
||||||
|
|
||||||
|
if (File.Exists(candidate))
|
||||||
|
{
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var programFiles = new[]
|
||||||
|
{
|
||||||
|
Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles),
|
||||||
|
Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86)
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var root in programFiles)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(root))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var candidate = Path.Combine(root, "teraterm", "ttermpro.exe");
|
||||||
|
if (File.Exists(candidate))
|
||||||
|
{
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "ttermpro.exe";
|
||||||
|
}
|
||||||
|
}
|
||||||
43
Themes/Colors.Dark.xaml
Normal file
43
Themes/Colors.Dark.xaml
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||||
|
<!-- Dark theme color palette -->
|
||||||
|
<Color x:Key="AccentColor">#FF7BA7FF</Color>
|
||||||
|
<Color x:Key="AccentMutedColor">#FF9CBFFF</Color>
|
||||||
|
<Color x:Key="SurfaceColor">#FF0F172A</Color>
|
||||||
|
<Color x:Key="SurfaceAltColor">#FF1B2437</Color>
|
||||||
|
<Color x:Key="CardColor">#FF1F2B3D</Color>
|
||||||
|
<Color x:Key="BorderColor">#33475B</Color>
|
||||||
|
<Color x:Key="SubtleBorderColor">#222C3C</Color>
|
||||||
|
<Color x:Key="InputBackgroundColor">#FF1A2332</Color>
|
||||||
|
<Color x:Key="TextPrimaryColor">#FFE5EAF3</Color>
|
||||||
|
<Color x:Key="TextSecondaryColor">#FFC3CCDA</Color>
|
||||||
|
<Color x:Key="TextMutedColor">#FF9CA7BA</Color>
|
||||||
|
<Color x:Key="SuccessColor">#FF3AD88F</Color>
|
||||||
|
<Color x:Key="WarningColor">#FFEFB95C</Color>
|
||||||
|
<Color x:Key="DangerColor">#FFFF7A7A</Color>
|
||||||
|
|
||||||
|
<SolidColorBrush x:Key="AccentBrush" Color="{StaticResource AccentColor}"/>
|
||||||
|
<SolidColorBrush x:Key="AccentBrushMuted" Color="{StaticResource AccentMutedColor}"/>
|
||||||
|
<SolidColorBrush x:Key="SurfaceBrush" Color="{StaticResource SurfaceColor}"/>
|
||||||
|
<SolidColorBrush x:Key="SurfaceAltBrush" Color="{StaticResource SurfaceAltColor}"/>
|
||||||
|
<SolidColorBrush x:Key="CardBrush" Color="{StaticResource CardColor}"/>
|
||||||
|
<SolidColorBrush x:Key="BorderBrush" Color="{StaticResource BorderColor}"/>
|
||||||
|
<SolidColorBrush x:Key="SubtleBorderBrush" Color="{StaticResource SubtleBorderColor}"/>
|
||||||
|
<SolidColorBrush x:Key="InputBackgroundBrush" Color="{StaticResource InputBackgroundColor}"/>
|
||||||
|
<SolidColorBrush x:Key="InputBorderBrush" Color="{StaticResource BorderColor}"/>
|
||||||
|
<SolidColorBrush x:Key="InputBorderActiveBrush" Color="{StaticResource AccentColor}"/>
|
||||||
|
<SolidColorBrush x:Key="TextBrushPrimary" Color="{StaticResource TextPrimaryColor}"/>
|
||||||
|
<SolidColorBrush x:Key="TextBrushSecondary" Color="{StaticResource TextSecondaryColor}"/>
|
||||||
|
<SolidColorBrush x:Key="TextBrushMuted" Color="{StaticResource TextMutedColor}"/>
|
||||||
|
<SolidColorBrush x:Key="SuccessBrush" Color="{StaticResource SuccessColor}"/>
|
||||||
|
<SolidColorBrush x:Key="WarningBrush" Color="{StaticResource WarningColor}"/>
|
||||||
|
<SolidColorBrush x:Key="DangerBrush" Color="{StaticResource DangerColor}"/>
|
||||||
|
<SolidColorBrush x:Key="ShadowBrush" Color="#33000000"/>
|
||||||
|
|
||||||
|
<CornerRadius x:Key="ControlCornerRadius">10</CornerRadius>
|
||||||
|
<CornerRadius x:Key="CardCornerRadius">12</CornerRadius>
|
||||||
|
<Thickness x:Key="ControlPadding">12,8</Thickness>
|
||||||
|
<Thickness x:Key="CardPadding">16</Thickness>
|
||||||
|
|
||||||
|
<FontFamily x:Key="AppFontFamily">Segoe UI Variable, Segoe UI</FontFamily>
|
||||||
|
</ResourceDictionary>
|
||||||
43
Themes/Colors.Light.xaml
Normal file
43
Themes/Colors.Light.xaml
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||||
|
<!-- Light theme color palette -->
|
||||||
|
<Color x:Key="AccentColor">#FF4F6BED</Color>
|
||||||
|
<Color x:Key="AccentMutedColor">#FF8096F4</Color>
|
||||||
|
<Color x:Key="SurfaceColor">#FFF5F7FB</Color>
|
||||||
|
<Color x:Key="SurfaceAltColor">#FFFFFFFF</Color>
|
||||||
|
<Color x:Key="CardColor">#FFFFFFFF</Color>
|
||||||
|
<Color x:Key="BorderColor">#22000000</Color>
|
||||||
|
<Color x:Key="SubtleBorderColor">#11000000</Color>
|
||||||
|
<Color x:Key="InputBackgroundColor">#FFF7FAFF</Color>
|
||||||
|
<Color x:Key="TextPrimaryColor">#FF101828</Color>
|
||||||
|
<Color x:Key="TextSecondaryColor">#FF4B5565</Color>
|
||||||
|
<Color x:Key="TextMutedColor">#FF98A1B2</Color>
|
||||||
|
<Color x:Key="SuccessColor">#FF2BB673</Color>
|
||||||
|
<Color x:Key="WarningColor">#FFE0A500</Color>
|
||||||
|
<Color x:Key="DangerColor">#FFD14343</Color>
|
||||||
|
|
||||||
|
<SolidColorBrush x:Key="AccentBrush" Color="{StaticResource AccentColor}"/>
|
||||||
|
<SolidColorBrush x:Key="AccentBrushMuted" Color="{StaticResource AccentMutedColor}"/>
|
||||||
|
<SolidColorBrush x:Key="SurfaceBrush" Color="{StaticResource SurfaceColor}"/>
|
||||||
|
<SolidColorBrush x:Key="SurfaceAltBrush" Color="{StaticResource SurfaceAltColor}"/>
|
||||||
|
<SolidColorBrush x:Key="CardBrush" Color="{StaticResource CardColor}"/>
|
||||||
|
<SolidColorBrush x:Key="BorderBrush" Color="{StaticResource BorderColor}"/>
|
||||||
|
<SolidColorBrush x:Key="SubtleBorderBrush" Color="{StaticResource SubtleBorderColor}"/>
|
||||||
|
<SolidColorBrush x:Key="InputBackgroundBrush" Color="{StaticResource InputBackgroundColor}"/>
|
||||||
|
<SolidColorBrush x:Key="InputBorderBrush" Color="{StaticResource BorderColor}"/>
|
||||||
|
<SolidColorBrush x:Key="InputBorderActiveBrush" Color="{StaticResource AccentColor}"/>
|
||||||
|
<SolidColorBrush x:Key="TextBrushPrimary" Color="{StaticResource TextPrimaryColor}"/>
|
||||||
|
<SolidColorBrush x:Key="TextBrushSecondary" Color="{StaticResource TextSecondaryColor}"/>
|
||||||
|
<SolidColorBrush x:Key="TextBrushMuted" Color="{StaticResource TextMutedColor}"/>
|
||||||
|
<SolidColorBrush x:Key="SuccessBrush" Color="{StaticResource SuccessColor}"/>
|
||||||
|
<SolidColorBrush x:Key="WarningBrush" Color="{StaticResource WarningColor}"/>
|
||||||
|
<SolidColorBrush x:Key="DangerBrush" Color="{StaticResource DangerColor}"/>
|
||||||
|
<SolidColorBrush x:Key="ShadowBrush" Color="#19000000"/>
|
||||||
|
|
||||||
|
<CornerRadius x:Key="ControlCornerRadius">10</CornerRadius>
|
||||||
|
<CornerRadius x:Key="CardCornerRadius">12</CornerRadius>
|
||||||
|
<Thickness x:Key="ControlPadding">12,8</Thickness>
|
||||||
|
<Thickness x:Key="CardPadding">16</Thickness>
|
||||||
|
|
||||||
|
<FontFamily x:Key="AppFontFamily">Segoe UI Variable, Segoe UI</FontFamily>
|
||||||
|
</ResourceDictionary>
|
||||||
511
Themes/Controls.xaml
Normal file
511
Themes/Controls.xaml
Normal file
@ -0,0 +1,511 @@
|
|||||||
|
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||||
|
<!-- Typography -->
|
||||||
|
<Style TargetType="TextBlock">
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource TextBrushPrimary}"/>
|
||||||
|
<Setter Property="FontFamily" Value="{StaticResource AppFontFamily}"/>
|
||||||
|
<Setter Property="TextWrapping" Value="Wrap"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- Window -->
|
||||||
|
<Style TargetType="Window">
|
||||||
|
<Setter Property="Background" Value="{StaticResource SurfaceBrush}"/>
|
||||||
|
<Setter Property="FontFamily" Value="{StaticResource AppFontFamily}"/>
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource TextBrushPrimary}"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- Card container -->
|
||||||
|
<Style x:Key="CardStyle" TargetType="Border">
|
||||||
|
<Setter Property="Background" Value="{StaticResource CardBrush}"/>
|
||||||
|
<Setter Property="CornerRadius" Value="{StaticResource CardCornerRadius}"/>
|
||||||
|
<Setter Property="BorderBrush" Value="{StaticResource SubtleBorderBrush}"/>
|
||||||
|
<Setter Property="BorderThickness" Value="1"/>
|
||||||
|
<Setter Property="Padding" Value="{StaticResource CardPadding}"/>
|
||||||
|
<Setter Property="SnapsToDevicePixels" Value="True"/>
|
||||||
|
<Setter Property="Effect">
|
||||||
|
<Setter.Value>
|
||||||
|
<DropShadowEffect Color="#22000000"
|
||||||
|
BlurRadius="8"
|
||||||
|
ShadowDepth="1"
|
||||||
|
Opacity="0.35"/>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- Buttons -->
|
||||||
|
<Style x:Key="BaseButtonStyle" TargetType="Button">
|
||||||
|
<Setter Property="Height" Value="40"/>
|
||||||
|
<Setter Property="Padding" Value="14,10"/>
|
||||||
|
<Setter Property="Margin" Value="0"/>
|
||||||
|
<Setter Property="Background" Value="{StaticResource SurfaceAltBrush}"/>
|
||||||
|
<Setter Property="BorderBrush" Value="{StaticResource BorderBrush}"/>
|
||||||
|
<Setter Property="BorderThickness" Value="1"/>
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource TextBrushPrimary}"/>
|
||||||
|
<Setter Property="FontFamily" Value="{StaticResource AppFontFamily}"/>
|
||||||
|
<Setter Property="Cursor" Value="Hand"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="Button">
|
||||||
|
<Border x:Name="Root"
|
||||||
|
Background="{TemplateBinding Background}"
|
||||||
|
BorderBrush="{TemplateBinding BorderBrush}"
|
||||||
|
BorderThickness="{TemplateBinding BorderThickness}"
|
||||||
|
CornerRadius="{StaticResource ControlCornerRadius}">
|
||||||
|
<ContentPresenter HorizontalAlignment="Center"
|
||||||
|
VerticalAlignment="Center"/>
|
||||||
|
</Border>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
|
<Setter TargetName="Root" Property="Background" Value="{StaticResource SurfaceBrush}"/>
|
||||||
|
<Setter Property="BorderBrush" TargetName="Root" Value="{StaticResource AccentBrushMuted}"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsPressed" Value="True">
|
||||||
|
<Setter TargetName="Root" Property="Background" Value="{StaticResource AccentBrushMuted}"/>
|
||||||
|
<Setter TargetName="Root" Property="BorderBrush" Value="{StaticResource AccentBrush}"/>
|
||||||
|
<Setter Property="Foreground" Value="White"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsEnabled" Value="False">
|
||||||
|
<Setter TargetName="Root" Property="Opacity" Value="0.6"/>
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style x:Key="PrimaryButtonStyle" TargetType="Button" BasedOn="{StaticResource BaseButtonStyle}">
|
||||||
|
<Setter Property="Background" Value="{StaticResource AccentBrush}"/>
|
||||||
|
<Setter Property="BorderBrush" Value="{StaticResource AccentBrush}"/>
|
||||||
|
<Setter Property="Foreground" Value="White"/>
|
||||||
|
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="Button">
|
||||||
|
<Border x:Name="Root"
|
||||||
|
Background="{TemplateBinding Background}"
|
||||||
|
BorderBrush="{TemplateBinding BorderBrush}"
|
||||||
|
BorderThickness="{TemplateBinding BorderThickness}"
|
||||||
|
CornerRadius="{StaticResource ControlCornerRadius}">
|
||||||
|
<ContentPresenter HorizontalAlignment="Center"
|
||||||
|
VerticalAlignment="Center"/>
|
||||||
|
</Border>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
|
<Setter TargetName="Root" Property="Background" Value="{StaticResource AccentBrushMuted}"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsPressed" Value="True">
|
||||||
|
<Setter TargetName="Root" Property="Background" Value="{StaticResource AccentBrush}"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsEnabled" Value="False">
|
||||||
|
<Setter TargetName="Root" Property="Opacity" Value="0.6"/>
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style x:Key="GhostButtonStyle" TargetType="Button" BasedOn="{StaticResource BaseButtonStyle}">
|
||||||
|
<Setter Property="Background" Value="Transparent"/>
|
||||||
|
<Setter Property="BorderBrush" Value="{StaticResource BorderBrush}"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- TextBox -->
|
||||||
|
<Style TargetType="TextBox">
|
||||||
|
<Setter Property="Height" Value="40"/>
|
||||||
|
<Setter Property="Padding" Value="12,8"/>
|
||||||
|
<Setter Property="Background" Value="{StaticResource InputBackgroundBrush}"/>
|
||||||
|
<Setter Property="BorderBrush" Value="{StaticResource InputBorderBrush}"/>
|
||||||
|
<Setter Property="BorderThickness" Value="1"/>
|
||||||
|
<Setter Property="FontFamily" Value="{StaticResource AppFontFamily}"/>
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource TextBrushPrimary}"/>
|
||||||
|
<Setter Property="VerticalContentAlignment" Value="Center"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="TextBox">
|
||||||
|
<Border x:Name="Root"
|
||||||
|
Background="{TemplateBinding Background}"
|
||||||
|
BorderBrush="{TemplateBinding BorderBrush}"
|
||||||
|
BorderThickness="{TemplateBinding BorderThickness}"
|
||||||
|
CornerRadius="{StaticResource ControlCornerRadius}">
|
||||||
|
<ScrollViewer x:Name="PART_ContentHost" Margin="0"/>
|
||||||
|
</Border>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsKeyboardFocused" Value="True">
|
||||||
|
<Setter TargetName="Root" Property="BorderBrush" Value="{StaticResource AccentBrush}"/>
|
||||||
|
<Setter TargetName="Root" Property="BorderThickness" Value="2"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsEnabled" Value="False">
|
||||||
|
<Setter TargetName="Root" Property="Opacity" Value="0.6"/>
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- ComboBox -->
|
||||||
|
<Style TargetType="ComboBox">
|
||||||
|
<Setter Property="Height" Value="40"/>
|
||||||
|
<Setter Property="Padding" Value="12,6"/>
|
||||||
|
<Setter Property="Background" Value="{StaticResource InputBackgroundBrush}"/>
|
||||||
|
<Setter Property="BorderBrush" Value="{StaticResource InputBorderBrush}"/>
|
||||||
|
<Setter Property="BorderThickness" Value="1"/>
|
||||||
|
<Setter Property="FontFamily" Value="{StaticResource AppFontFamily}"/>
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource TextBrushPrimary}"/>
|
||||||
|
<Setter Property="Cursor" Value="Hand"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="ComboBox">
|
||||||
|
<Grid>
|
||||||
|
<Border x:Name="Root"
|
||||||
|
Background="{TemplateBinding Background}"
|
||||||
|
BorderBrush="{TemplateBinding BorderBrush}"
|
||||||
|
BorderThickness="{TemplateBinding BorderThickness}"
|
||||||
|
CornerRadius="{StaticResource ControlCornerRadius}"
|
||||||
|
SnapsToDevicePixels="True"
|
||||||
|
Cursor="Hand">
|
||||||
|
<Grid>
|
||||||
|
<ToggleButton x:Name="MainToggleButton"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
VerticalAlignment="Stretch"
|
||||||
|
Background="Transparent"
|
||||||
|
BorderThickness="0"
|
||||||
|
Focusable="False"
|
||||||
|
Cursor="Hand"
|
||||||
|
IsChecked="{Binding IsDropDownOpen, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}">
|
||||||
|
<ToggleButton.Template>
|
||||||
|
<ControlTemplate TargetType="ToggleButton">
|
||||||
|
<Border Background="Transparent" BorderThickness="0" Cursor="Hand"/>
|
||||||
|
</ControlTemplate>
|
||||||
|
</ToggleButton.Template>
|
||||||
|
</ToggleButton>
|
||||||
|
<DockPanel IsHitTestVisible="False">
|
||||||
|
<ContentPresenter x:Name="ContentSite"
|
||||||
|
Margin="12,0,8,0"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
HorizontalAlignment="Left"
|
||||||
|
Content="{TemplateBinding SelectionBoxItem}"
|
||||||
|
ContentTemplate="{TemplateBinding SelectionBoxItemTemplate}"/>
|
||||||
|
<ToggleButton DockPanel.Dock="Right"
|
||||||
|
Width="28"
|
||||||
|
Focusable="False"
|
||||||
|
HorizontalAlignment="Right"
|
||||||
|
Template="{DynamicResource ComboBoxToggleButton}"
|
||||||
|
IsChecked="{Binding IsDropDownOpen, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"/>
|
||||||
|
</DockPanel>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
<Popup x:Name="PART_Popup"
|
||||||
|
Placement="Bottom"
|
||||||
|
AllowsTransparency="True"
|
||||||
|
Focusable="True"
|
||||||
|
IsHitTestVisible="True"
|
||||||
|
IsOpen="{TemplateBinding IsDropDownOpen}"
|
||||||
|
PopupAnimation="Fade">
|
||||||
|
<Grid MaxHeight="320" Margin="0,4,0,0" IsHitTestVisible="True">
|
||||||
|
<Border Background="{StaticResource CardBrush}"
|
||||||
|
BorderBrush="{StaticResource SubtleBorderBrush}"
|
||||||
|
BorderThickness="1"
|
||||||
|
CornerRadius="12"
|
||||||
|
Width="{Binding ActualWidth, RelativeSource={RelativeSource TemplatedParent}}"
|
||||||
|
Padding="2"
|
||||||
|
IsHitTestVisible="True">
|
||||||
|
<Border.Effect>
|
||||||
|
<DropShadowEffect Color="#40000000"
|
||||||
|
BlurRadius="16"
|
||||||
|
ShadowDepth="4"
|
||||||
|
Opacity="0.4"
|
||||||
|
Direction="270"/>
|
||||||
|
</Border.Effect>
|
||||||
|
<ScrollViewer Margin="0"
|
||||||
|
SnapsToDevicePixels="True"
|
||||||
|
VerticalScrollBarVisibility="Auto"
|
||||||
|
IsHitTestVisible="True"
|
||||||
|
CanContentScroll="False">
|
||||||
|
<ItemsPresenter/>
|
||||||
|
</ScrollViewer>
|
||||||
|
</Border>
|
||||||
|
</Grid>
|
||||||
|
</Popup>
|
||||||
|
</Grid>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
|
<Setter TargetName="Root" Property="BorderBrush" Value="{StaticResource AccentBrushMuted}"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsKeyboardFocused" Value="True">
|
||||||
|
<Setter TargetName="Root" Property="BorderBrush" Value="{StaticResource AccentBrush}"/>
|
||||||
|
<Setter TargetName="Root" Property="BorderThickness" Value="2"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsEnabled" Value="False">
|
||||||
|
<Setter TargetName="Root" Property="Opacity" Value="0.6"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsDropDownOpen" Value="True">
|
||||||
|
<Setter TargetName="Root" Property="BorderBrush" Value="{StaticResource AccentBrush}"/>
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
<Setter Property="ItemContainerStyle">
|
||||||
|
<Setter.Value>
|
||||||
|
<Style TargetType="ComboBoxItem">
|
||||||
|
<Setter Property="Padding" Value="14,10"/>
|
||||||
|
<Setter Property="Background" Value="Transparent"/>
|
||||||
|
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
|
||||||
|
<Setter Property="VerticalContentAlignment" Value="Center"/>
|
||||||
|
<Setter Property="Margin" Value="2,1"/>
|
||||||
|
<Setter Property="IsHitTestVisible" Value="True"/>
|
||||||
|
<Setter Property="Cursor" Value="Hand"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="ComboBoxItem">
|
||||||
|
<Border x:Name="Root"
|
||||||
|
Padding="{TemplateBinding Padding}"
|
||||||
|
Background="{TemplateBinding Background}"
|
||||||
|
CornerRadius="8"
|
||||||
|
SnapsToDevicePixels="True"
|
||||||
|
IsHitTestVisible="True"
|
||||||
|
Cursor="Hand">
|
||||||
|
<ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
|
||||||
|
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
|
||||||
|
</Border>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
|
<Setter TargetName="Root" Property="Background" Value="{StaticResource SurfaceAltBrush}"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsSelected" Value="True">
|
||||||
|
<Setter TargetName="Root" Property="Background" Value="{StaticResource AccentBrush}"/>
|
||||||
|
<Setter Property="Foreground" Value="White"/>
|
||||||
|
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsEnabled" Value="False">
|
||||||
|
<Setter TargetName="Root" Property="Opacity" Value="0.5"/>
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- Toggle button used inside ComboBox -->
|
||||||
|
<ControlTemplate x:Key="ComboBoxToggleButton" TargetType="ToggleButton">
|
||||||
|
<Border x:Name="Border"
|
||||||
|
Background="Transparent"
|
||||||
|
Padding="8,4,16,4"
|
||||||
|
BorderBrush="{TemplateBinding BorderBrush}"
|
||||||
|
BorderThickness="0"
|
||||||
|
CornerRadius="{StaticResource ControlCornerRadius}">
|
||||||
|
<Path x:Name="Arrow"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Data="M 0 0 L 5 5 L 10 0 Z"
|
||||||
|
Fill="{StaticResource TextBrushSecondary}"
|
||||||
|
Stretch="Uniform"
|
||||||
|
Width="10"
|
||||||
|
Height="5"/>
|
||||||
|
</Border>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
|
<Setter TargetName="Arrow" Property="Fill" Value="{StaticResource AccentBrush}"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsPressed" Value="True">
|
||||||
|
<Setter TargetName="Arrow" Property="Fill" Value="{StaticResource AccentBrush}"/>
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
|
||||||
|
<!-- Navigation tab control -->
|
||||||
|
<Style x:Key="TopNavTabControlStyle" TargetType="TabControl">
|
||||||
|
<Setter Property="Background" Value="Transparent"/>
|
||||||
|
<Setter Property="BorderThickness" Value="0"/>
|
||||||
|
<Setter Property="Padding" Value="0"/>
|
||||||
|
<Setter Property="HorizontalAlignment" Value="Left"/>
|
||||||
|
<Setter Property="HorizontalContentAlignment" Value="Left"/>
|
||||||
|
<Setter Property="UseLayoutRounding" Value="True"/>
|
||||||
|
<Setter Property="Margin" Value="0"/>
|
||||||
|
<Setter Property="SnapsToDevicePixels" Value="True"/>
|
||||||
|
<Setter Property="ClipToBounds" Value="False"/>
|
||||||
|
<Setter Property="Width" Value="Auto"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="TabControl">
|
||||||
|
<Grid ClipToBounds="False" HorizontalAlignment="Left">
|
||||||
|
<TabPanel x:Name="HeaderPanel"
|
||||||
|
Panel.ZIndex="1"
|
||||||
|
IsItemsHost="True"
|
||||||
|
Margin="8,4,0,4"
|
||||||
|
HorizontalAlignment="Left"
|
||||||
|
ClipToBounds="False"
|
||||||
|
Background="Transparent"
|
||||||
|
UseLayoutRounding="False"/>
|
||||||
|
</Grid>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style x:Key="TopNavTabItemStyle" TargetType="TabItem">
|
||||||
|
<Setter Property="Height" Value="36"/>
|
||||||
|
<Setter Property="MinWidth" Value="130"/>
|
||||||
|
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||||
|
<Setter Property="Padding" Value="18,8"/>
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource TextBrushSecondary}"/>
|
||||||
|
<Setter Property="Margin" Value="0,0,24,0"/>
|
||||||
|
<Setter Property="SnapsToDevicePixels" Value="True"/>
|
||||||
|
<Setter Property="HorizontalContentAlignment" Value="Center"/>
|
||||||
|
<Setter Property="VerticalContentAlignment" Value="Center"/>
|
||||||
|
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
|
||||||
|
<Setter Property="UseLayoutRounding" Value="True"/>
|
||||||
|
<Setter Property="Panel.ZIndex" Value="0"/>
|
||||||
|
<Setter Property="ClipToBounds" Value="False"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="TabItem">
|
||||||
|
<Grid ClipToBounds="False">
|
||||||
|
<Border x:Name="Root"
|
||||||
|
Background="Transparent"
|
||||||
|
BorderBrush="{StaticResource SubtleBorderBrush}"
|
||||||
|
BorderThickness="1"
|
||||||
|
CornerRadius="0"
|
||||||
|
Padding="{TemplateBinding Padding}"
|
||||||
|
ClipToBounds="False">
|
||||||
|
|
||||||
|
<ContentPresenter ContentSource="Header"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
VerticalAlignment="Center"/>
|
||||||
|
</Border>
|
||||||
|
</Grid>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
|
<Setter TargetName="Root" Property="BorderBrush" Value="{StaticResource AccentBrushMuted}"/>
|
||||||
|
<Setter TargetName="Root" Property="Background" Value="{StaticResource SurfaceAltBrush}"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsSelected" Value="True">
|
||||||
|
<Setter TargetName="Root" Property="Background" Value="{StaticResource AccentBrush}"/>
|
||||||
|
<Setter TargetName="Root" Property="BorderBrush" Value="{StaticResource AccentBrush}"/>
|
||||||
|
<Setter Property="Foreground" Value="White"/>
|
||||||
|
<Setter Property="Panel.ZIndex" Value="10"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsEnabled" Value="False">
|
||||||
|
<Setter TargetName="Root" Property="Opacity" Value="0.6"/>
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- ListBox -->
|
||||||
|
<Style TargetType="ListBox">
|
||||||
|
<Setter Property="BorderThickness" Value="0"/>
|
||||||
|
<Setter Property="Background" Value="Transparent"/>
|
||||||
|
<Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Auto"/>
|
||||||
|
<Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Auto"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- Navigation ListBox style -->
|
||||||
|
<Style x:Key="TopNavListBoxStyle" TargetType="ListBox">
|
||||||
|
<Setter Property="BorderThickness" Value="0"/>
|
||||||
|
<Setter Property="Background" Value="Transparent"/>
|
||||||
|
<Setter Property="BorderBrush" Value="Transparent"/>
|
||||||
|
<Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Disabled"/>
|
||||||
|
<Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Disabled"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- Navigation ListBoxItem style -->
|
||||||
|
<Style x:Key="TopNavListBoxItemStyle" TargetType="ListBoxItem">
|
||||||
|
<Setter Property="Height" Value="36"/>
|
||||||
|
<Setter Property="MinWidth" Value="130"/>
|
||||||
|
<Setter Property="Margin" Value="0,0,12,0"/>
|
||||||
|
<Setter Property="Padding" Value="0"/>
|
||||||
|
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
|
||||||
|
<Setter Property="VerticalContentAlignment" Value="Stretch"/>
|
||||||
|
<Setter Property="Background" Value="Transparent"/>
|
||||||
|
<Setter Property="BorderThickness" Value="1"/>
|
||||||
|
<Setter Property="BorderBrush" Value="{StaticResource SubtleBorderBrush}"/>
|
||||||
|
<Setter Property="SnapsToDevicePixels" Value="True"/>
|
||||||
|
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
|
||||||
|
<Setter Property="ClipToBounds" Value="False"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="ListBoxItem">
|
||||||
|
<Border x:Name="Root"
|
||||||
|
Background="{TemplateBinding Background}"
|
||||||
|
BorderBrush="{TemplateBinding BorderBrush}"
|
||||||
|
BorderThickness="{TemplateBinding BorderThickness}"
|
||||||
|
CornerRadius="18"
|
||||||
|
Padding="0"
|
||||||
|
ClipToBounds="False">
|
||||||
|
<ContentPresenter HorizontalAlignment="Center"
|
||||||
|
VerticalAlignment="Center"/>
|
||||||
|
</Border>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
|
<Setter TargetName="Root" Property="BorderBrush" Value="{StaticResource AccentBrushMuted}"/>
|
||||||
|
<Setter TargetName="Root" Property="Background" Value="{StaticResource SurfaceAltBrush}"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsSelected" Value="True">
|
||||||
|
<Setter TargetName="Root" Property="Background" Value="{StaticResource AccentBrush}"/>
|
||||||
|
<Setter TargetName="Root" Property="BorderBrush" Value="{StaticResource AccentBrush}"/>
|
||||||
|
<Setter Property="Foreground" Value="White"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsEnabled" Value="False">
|
||||||
|
<Setter TargetName="Root" Property="Opacity" Value="0.6"/>
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style x:Key="SelectorListBoxItemStyle" TargetType="ListBoxItem">
|
||||||
|
<Setter Property="Height" Value="44"/>
|
||||||
|
<Setter Property="Padding" Value="12,6"/>
|
||||||
|
<Setter Property="Margin" Value="0,4,0,0"/>
|
||||||
|
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
|
||||||
|
<Setter Property="Background" Value="{StaticResource SurfaceAltBrush}"/>
|
||||||
|
<Setter Property="BorderThickness" Value="1"/>
|
||||||
|
<Setter Property="BorderBrush" Value="{StaticResource SubtleBorderBrush}"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="ListBoxItem">
|
||||||
|
<Border x:Name="Root"
|
||||||
|
Background="{TemplateBinding Background}"
|
||||||
|
BorderBrush="{TemplateBinding BorderBrush}"
|
||||||
|
BorderThickness="{TemplateBinding BorderThickness}"
|
||||||
|
CornerRadius="{StaticResource ControlCornerRadius}"
|
||||||
|
Padding="{TemplateBinding Padding}">
|
||||||
|
<ContentPresenter VerticalAlignment="Center"/>
|
||||||
|
</Border>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
|
<Setter TargetName="Root" Property="BorderBrush" Value="{StaticResource AccentBrushMuted}"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsSelected" Value="True">
|
||||||
|
<Setter TargetName="Root" Property="Background" Value="{StaticResource AccentBrush}"/>
|
||||||
|
<Setter Property="Foreground" Value="White"/>
|
||||||
|
<Setter TargetName="Root" Property="BorderBrush" Value="{StaticResource AccentBrush}"/>
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- Labels -->
|
||||||
|
<Style x:Key="SectionHeaderTextStyle" TargetType="TextBlock">
|
||||||
|
<Setter Property="FontSize" Value="16"/>
|
||||||
|
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource TextBrushPrimary}"/>
|
||||||
|
<Setter Property="Margin" Value="0,0,0,12"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style x:Key="CaptionTextStyle" TargetType="TextBlock">
|
||||||
|
<Setter Property="FontSize" Value="12"/>
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource TextBrushSecondary}"/>
|
||||||
|
</Style>
|
||||||
|
</ResourceDictionary>
|
||||||
24
Utilities/RelayCommand.cs
Normal file
24
Utilities/RelayCommand.cs
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
using System;
|
||||||
|
using System.Windows.Input;
|
||||||
|
|
||||||
|
namespace AmebaPro3_ControlPanel.Utilities;
|
||||||
|
|
||||||
|
public class RelayCommand : ICommand
|
||||||
|
{
|
||||||
|
private readonly Action<object?> _execute;
|
||||||
|
private readonly Func<object?, bool>? _canExecute;
|
||||||
|
|
||||||
|
public RelayCommand(Action<object?> execute, Func<object?, bool>? canExecute = null)
|
||||||
|
{
|
||||||
|
_execute = execute ?? throw new ArgumentNullException(nameof(execute));
|
||||||
|
_canExecute = canExecute;
|
||||||
|
}
|
||||||
|
|
||||||
|
public event EventHandler? CanExecuteChanged;
|
||||||
|
|
||||||
|
public bool CanExecute(object? parameter) => _canExecute?.Invoke(parameter) ?? true;
|
||||||
|
|
||||||
|
public void Execute(object? parameter) => _execute(parameter);
|
||||||
|
|
||||||
|
public void RaiseCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty);
|
||||||
|
}
|
||||||
189
ViewModels/ConsolePanelViewModel.cs
Normal file
189
ViewModels/ConsolePanelViewModel.cs
Normal file
@ -0,0 +1,189 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Windows.Input;
|
||||||
|
using AmebaPro3_ControlPanel.Utilities;
|
||||||
|
using Microsoft.Win32;
|
||||||
|
|
||||||
|
namespace AmebaPro3_ControlPanel.ViewModels;
|
||||||
|
|
||||||
|
public class ConsolePanelViewModel : ViewModelBase
|
||||||
|
{
|
||||||
|
private string _title;
|
||||||
|
private bool _isConnected;
|
||||||
|
private string _currentCommand = string.Empty;
|
||||||
|
|
||||||
|
public ConsolePanelViewModel(string title)
|
||||||
|
{
|
||||||
|
_title = title;
|
||||||
|
SendCommand = new RelayCommand(_ => SendCurrentCommand(true));
|
||||||
|
ClearLog = new RelayCommand(_ => LogLines.Clear());
|
||||||
|
SaveLog = new RelayCommand(_ => SaveLogToFile());
|
||||||
|
LoadCommandFileCommand = new RelayCommand(_ => LoadCommandsFromFile());
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Title
|
||||||
|
{
|
||||||
|
get => _title;
|
||||||
|
set => SetProperty(ref _title, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsConnected
|
||||||
|
{
|
||||||
|
get => _isConnected;
|
||||||
|
set => SetProperty(ref _isConnected, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ObservableCollection<string> LogLines { get; } = new();
|
||||||
|
|
||||||
|
public ObservableCollection<string> History { get; } = new();
|
||||||
|
|
||||||
|
public string CurrentCommand
|
||||||
|
{
|
||||||
|
get => _currentCommand;
|
||||||
|
set => SetProperty(ref _currentCommand, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ICommand SendCommand { get; }
|
||||||
|
|
||||||
|
public ICommand ClearLog { get; }
|
||||||
|
|
||||||
|
public ICommand SaveLog { get; }
|
||||||
|
|
||||||
|
public ICommand LoadCommandFileCommand { get; }
|
||||||
|
|
||||||
|
protected virtual void SendCurrentCommand(bool addToHistory)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(CurrentCommand))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var command = CurrentCommand.Trim();
|
||||||
|
LogLines.Add($"> {command}");
|
||||||
|
if (addToHistory)
|
||||||
|
{
|
||||||
|
AddHistoryEntry(command);
|
||||||
|
}
|
||||||
|
CurrentCommand = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SendFromHistory(string command)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(command))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
CurrentCommand = command;
|
||||||
|
SendCurrentCommand(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual void SaveLogToFile()
|
||||||
|
{
|
||||||
|
var dialog = new SaveFileDialog
|
||||||
|
{
|
||||||
|
Filter = "Text Files (*.txt)|*.txt|Log Files (*.log)|*.log|All Files (*.*)|*.*",
|
||||||
|
FileName = $"{Title}_Log.txt"
|
||||||
|
};
|
||||||
|
|
||||||
|
if (dialog.ShowDialog() == true)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
File.WriteAllLines(dialog.FileName, LogLines);
|
||||||
|
LogLines.Add($"Log saved to {dialog.FileName}");
|
||||||
|
}
|
||||||
|
catch (IOException ex)
|
||||||
|
{
|
||||||
|
LogLines.Add($"Failed to save log: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual void LoadCommandsFromFile()
|
||||||
|
{
|
||||||
|
var dialog = new OpenFileDialog
|
||||||
|
{
|
||||||
|
Filter = "Text Files (*.txt)|*.txt|All Files (*.*)|*.*",
|
||||||
|
Multiselect = false
|
||||||
|
};
|
||||||
|
|
||||||
|
if (dialog.ShowDialog() == true)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var commands = File.ReadAllLines(dialog.FileName)
|
||||||
|
.Where(line => !string.IsNullOrWhiteSpace(line))
|
||||||
|
.Select(line => line.Trim())
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
foreach (var command in commands)
|
||||||
|
{
|
||||||
|
AddHistoryEntry(command);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (commands.Count > 0)
|
||||||
|
{
|
||||||
|
LogLines.Add($"Loaded {commands.Count} command(s) from file.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (IOException ex)
|
||||||
|
{
|
||||||
|
LogLines.Add($"Failed to load commands: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SeedSampleLog(IEnumerable<string> lines)
|
||||||
|
{
|
||||||
|
foreach (var line in lines)
|
||||||
|
{
|
||||||
|
LogLines.Add(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SeedSampleHistory(IEnumerable<string> commands)
|
||||||
|
{
|
||||||
|
foreach (var command in commands)
|
||||||
|
{
|
||||||
|
AddHistoryEntry(command);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DeleteHistoryItems(IEnumerable<string> itemsToDelete)
|
||||||
|
{
|
||||||
|
foreach (var item in itemsToDelete)
|
||||||
|
{
|
||||||
|
History.Remove(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DeleteLogLines(IEnumerable<string> linesToDelete)
|
||||||
|
{
|
||||||
|
foreach (var line in linesToDelete)
|
||||||
|
{
|
||||||
|
LogLines.Remove(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void AddHistoryEntry(string command)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(command))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var trimmed = command.Trim();
|
||||||
|
for (var i = History.Count - 1; i >= 0; i--)
|
||||||
|
{
|
||||||
|
if (History[i] == trimmed)
|
||||||
|
{
|
||||||
|
History.RemoveAt(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
History.Add(trimmed);
|
||||||
|
}
|
||||||
|
}
|
||||||
37
ViewModels/JLinkApSessionViewModel.cs
Normal file
37
ViewModels/JLinkApSessionViewModel.cs
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
using System.Windows.Input;
|
||||||
|
using AmebaPro3_ControlPanel.Utilities;
|
||||||
|
|
||||||
|
namespace AmebaPro3_ControlPanel.ViewModels;
|
||||||
|
|
||||||
|
public class JLinkApSessionViewModel : ConsolePanelViewModel
|
||||||
|
{
|
||||||
|
public JLinkApSessionViewModel(string title) : base(title)
|
||||||
|
{
|
||||||
|
HaltCommand = new RelayCommand(_ => AppendDebuggerMessage("Halt requested (UI placeholder)."));
|
||||||
|
ContinueCommand = new RelayCommand(_ => AppendDebuggerMessage("Continue requested (UI placeholder)."));
|
||||||
|
StepInCommand = new RelayCommand(_ => AppendDebuggerMessage("Step In (UI placeholder)."));
|
||||||
|
StepOverCommand = new RelayCommand(_ => AppendDebuggerMessage("Step Over (UI placeholder)."));
|
||||||
|
StepOutCommand = new RelayCommand(_ => AppendDebuggerMessage("Step Out (UI placeholder)."));
|
||||||
|
}
|
||||||
|
|
||||||
|
public ICommand HaltCommand { get; }
|
||||||
|
|
||||||
|
public ICommand ContinueCommand { get; }
|
||||||
|
|
||||||
|
public ICommand StepInCommand { get; }
|
||||||
|
|
||||||
|
public ICommand StepOverCommand { get; }
|
||||||
|
|
||||||
|
public ICommand StepOutCommand { get; }
|
||||||
|
|
||||||
|
public void SetSessionConnected(bool connected)
|
||||||
|
{
|
||||||
|
IsConnected = connected;
|
||||||
|
AppendDebuggerMessage(connected ? "Session ready (dummy)." : "Session disconnected.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AppendDebuggerMessage(string message)
|
||||||
|
{
|
||||||
|
LogLines.Add(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
71
ViewModels/JLinkPageViewModel.cs
Normal file
71
ViewModels/JLinkPageViewModel.cs
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Windows.Input;
|
||||||
|
using AmebaPro3_ControlPanel.Utilities;
|
||||||
|
|
||||||
|
namespace AmebaPro3_ControlPanel.ViewModels;
|
||||||
|
|
||||||
|
public class JLinkPageViewModel : PageViewModelBase
|
||||||
|
{
|
||||||
|
private string? _selectedSerialNumber;
|
||||||
|
private JLinkApSessionViewModel? _selectedApSession;
|
||||||
|
|
||||||
|
public JLinkPageViewModel()
|
||||||
|
{
|
||||||
|
Title = "Debugger";
|
||||||
|
|
||||||
|
AvailableSerialNumbers = new ObservableCollection<string> { "123456789", "987654321", "555555555" };
|
||||||
|
SelectedSerialNumber = AvailableSerialNumbers.FirstOrDefault();
|
||||||
|
|
||||||
|
ApSessions = new ObservableCollection<JLinkApSessionViewModel>
|
||||||
|
{
|
||||||
|
new("Cortex M33 - Processor NP"),
|
||||||
|
new("Cortex M33 - Processor MP"),
|
||||||
|
new("Cortex M23 - Processor FP"),
|
||||||
|
new("Cortex CA32 - Processor AP")
|
||||||
|
};
|
||||||
|
|
||||||
|
SelectedApSession = ApSessions.FirstOrDefault();
|
||||||
|
|
||||||
|
RefreshSerialsCommand = new RelayCommand(_ => RefreshSerials());
|
||||||
|
ConnectJLinkCommand = new RelayCommand(_ => ConnectJLink());
|
||||||
|
}
|
||||||
|
|
||||||
|
public ObservableCollection<string> AvailableSerialNumbers { get; }
|
||||||
|
|
||||||
|
public string? SelectedSerialNumber
|
||||||
|
{
|
||||||
|
get => _selectedSerialNumber;
|
||||||
|
set => SetProperty(ref _selectedSerialNumber, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ObservableCollection<JLinkApSessionViewModel> ApSessions { get; }
|
||||||
|
|
||||||
|
public JLinkApSessionViewModel? SelectedApSession
|
||||||
|
{
|
||||||
|
get => _selectedApSession;
|
||||||
|
set => SetProperty(ref _selectedApSession, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ICommand RefreshSerialsCommand { get; }
|
||||||
|
|
||||||
|
public ICommand ConnectJLinkCommand { get; }
|
||||||
|
|
||||||
|
private void RefreshSerials()
|
||||||
|
{
|
||||||
|
AvailableSerialNumbers.Clear();
|
||||||
|
AvailableSerialNumbers.Add("123456789");
|
||||||
|
AvailableSerialNumbers.Add("987654321");
|
||||||
|
AvailableSerialNumbers.Add("555555555");
|
||||||
|
SelectedSerialNumber ??= AvailableSerialNumbers.FirstOrDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ConnectJLink()
|
||||||
|
{
|
||||||
|
foreach (var session in ApSessions)
|
||||||
|
{
|
||||||
|
session.SetSessionConnected(true);
|
||||||
|
session.LogLines.Add($"Attached to {session.Title} via J-Link {(SelectedSerialNumber ?? "N/A")} (UI placeholder).");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
171
ViewModels/MainPageViewModel.cs
Normal file
171
ViewModels/MainPageViewModel.cs
Normal file
@ -0,0 +1,171 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Windows.Input;
|
||||||
|
using AmebaPro3_ControlPanel.Services.Uart;
|
||||||
|
using AmebaPro3_ControlPanel.Utilities;
|
||||||
|
using Microsoft.Win32;
|
||||||
|
|
||||||
|
namespace AmebaPro3_ControlPanel.ViewModels;
|
||||||
|
|
||||||
|
public class MainPageViewModel : PageViewModelBase
|
||||||
|
{
|
||||||
|
private string _bootloaderPath = string.Empty;
|
||||||
|
private string _applicationPath = string.Empty;
|
||||||
|
private bool _isArduinoConnected;
|
||||||
|
private string? _selectedArduinoPort;
|
||||||
|
private int _arduinoBaudRate;
|
||||||
|
private string? _selectedArduinoTest;
|
||||||
|
|
||||||
|
public MainPageViewModel()
|
||||||
|
{
|
||||||
|
Title = "Panel";
|
||||||
|
|
||||||
|
AmebaConsole = new UartConsoleViewModel("AmebaPro3 UART", 1_500_000);
|
||||||
|
var amebaPorts = PortDiscovery.GetPorts();
|
||||||
|
AmebaConsole.SetPorts(amebaPorts);
|
||||||
|
if (amebaPorts.Length == 0)
|
||||||
|
{
|
||||||
|
AmebaConsole.LogLines.Add("[UART] No COM ports detected.");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
AmebaConsole.LogLines.Add("[UART] Ready. Select a COM port and connect.");
|
||||||
|
}
|
||||||
|
|
||||||
|
ArduinoTests = new ObservableCollection<string>
|
||||||
|
{
|
||||||
|
"Test 1",
|
||||||
|
"Test 2",
|
||||||
|
"Test 3",
|
||||||
|
"Test 4",
|
||||||
|
"Test 5",
|
||||||
|
"Test 6",
|
||||||
|
"Test 7"
|
||||||
|
};
|
||||||
|
|
||||||
|
ArduinoPorts = new ObservableCollection<string> { "COM7", "COM8", "COM9" };
|
||||||
|
SelectedArduinoPort = ArduinoPorts.FirstOrDefault();
|
||||||
|
_arduinoBaudRate = 115200;
|
||||||
|
SelectedArduinoTest = ArduinoTests.FirstOrDefault();
|
||||||
|
|
||||||
|
BrowseBootloaderCommand = new RelayCommand(_ => PickFile(path => BootloaderPath = path));
|
||||||
|
BrowseAppCommand = new RelayCommand(_ => PickFile(path => ApplicationPath = path));
|
||||||
|
FlashImageCommand = new RelayCommand(_ => FlashImage(), _ => !string.IsNullOrWhiteSpace(BootloaderPath) && !string.IsNullOrWhiteSpace(ApplicationPath));
|
||||||
|
|
||||||
|
ArduinoConnectCommand = new RelayCommand(_ => ToggleArduinoConnection(), _ => !string.IsNullOrWhiteSpace(SelectedArduinoPort));
|
||||||
|
ArduinoResetCommand = new RelayCommand(_ => LogArduinoAction("Reset"));
|
||||||
|
ArduinoDownloadCommand = new RelayCommand(_ => LogArduinoAction("Download Mode"));
|
||||||
|
ArduinoNormalCommand = new RelayCommand(_ => LogArduinoAction("Normal Mode"));
|
||||||
|
ArduinoTestModeCommand = new RelayCommand(_ => LogArduinoAction($"Test Mode ({SelectedArduinoTest ?? "Not selected"})"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public UartConsoleViewModel AmebaConsole { get; }
|
||||||
|
|
||||||
|
public ObservableCollection<string> ArduinoPorts { get; }
|
||||||
|
|
||||||
|
public ObservableCollection<string> ArduinoTests { get; }
|
||||||
|
|
||||||
|
public string BootloaderPath
|
||||||
|
{
|
||||||
|
get => _bootloaderPath;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (SetProperty(ref _bootloaderPath, value))
|
||||||
|
{
|
||||||
|
(FlashImageCommand as RelayCommand)?.RaiseCanExecuteChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string ApplicationPath
|
||||||
|
{
|
||||||
|
get => _applicationPath;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (SetProperty(ref _applicationPath, value))
|
||||||
|
{
|
||||||
|
(FlashImageCommand as RelayCommand)?.RaiseCanExecuteChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsArduinoConnected
|
||||||
|
{
|
||||||
|
get => _isArduinoConnected;
|
||||||
|
private set => SetProperty(ref _isArduinoConnected, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string? SelectedArduinoPort
|
||||||
|
{
|
||||||
|
get => _selectedArduinoPort;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (SetProperty(ref _selectedArduinoPort, value))
|
||||||
|
{
|
||||||
|
(ArduinoConnectCommand as RelayCommand)?.RaiseCanExecuteChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int ArduinoBaudRate
|
||||||
|
{
|
||||||
|
get => _arduinoBaudRate;
|
||||||
|
set => SetProperty(ref _arduinoBaudRate, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string? SelectedArduinoTest
|
||||||
|
{
|
||||||
|
get => _selectedArduinoTest;
|
||||||
|
set => SetProperty(ref _selectedArduinoTest, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ICommand BrowseBootloaderCommand { get; }
|
||||||
|
|
||||||
|
public ICommand BrowseAppCommand { get; }
|
||||||
|
|
||||||
|
public ICommand FlashImageCommand { get; }
|
||||||
|
|
||||||
|
public ICommand ArduinoConnectCommand { get; }
|
||||||
|
|
||||||
|
public ICommand ArduinoResetCommand { get; }
|
||||||
|
|
||||||
|
public ICommand ArduinoDownloadCommand { get; }
|
||||||
|
|
||||||
|
public ICommand ArduinoNormalCommand { get; }
|
||||||
|
|
||||||
|
public ICommand ArduinoTestModeCommand { get; }
|
||||||
|
|
||||||
|
private void PickFile(Action<string> setPath)
|
||||||
|
{
|
||||||
|
var dialog = new OpenFileDialog
|
||||||
|
{
|
||||||
|
Filter = "Binary Files (*.bin)|*.bin|All Files (*.*)|*.*",
|
||||||
|
Multiselect = false
|
||||||
|
};
|
||||||
|
|
||||||
|
if (dialog.ShowDialog() == true)
|
||||||
|
{
|
||||||
|
setPath(dialog.FileName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void FlashImage()
|
||||||
|
{
|
||||||
|
AmebaConsole.LogLines.Add($"[FLASH] Bootloader: {Path.GetFileName(BootloaderPath)}, App: {Path.GetFileName(ApplicationPath)}");
|
||||||
|
AmebaConsole.LogLines.Add("[FLASH] (UI-only) Flash operation queued.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ToggleArduinoConnection()
|
||||||
|
{
|
||||||
|
IsArduinoConnected = !IsArduinoConnected;
|
||||||
|
var state = IsArduinoConnected ? "connected" : "disconnected";
|
||||||
|
AmebaConsole.LogLines.Add($"[ARDUINO] {state} on {SelectedArduinoPort} @ {ArduinoBaudRate} baud (UI placeholder).");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LogArduinoAction(string action)
|
||||||
|
{
|
||||||
|
AmebaConsole.LogLines.Add($"[ARDUINO] {action} triggered.");
|
||||||
|
}
|
||||||
|
}
|
||||||
63
ViewModels/MainWindowViewModel.cs
Normal file
63
ViewModels/MainWindowViewModel.cs
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace AmebaPro3_ControlPanel.ViewModels;
|
||||||
|
|
||||||
|
public class MainWindowViewModel : ViewModelBase
|
||||||
|
{
|
||||||
|
private PageViewModelBase? _currentPage;
|
||||||
|
private string _selectedDevice = "AmebaPro3 (virtual)";
|
||||||
|
private string _connectionStatus = "Disconnected";
|
||||||
|
private string _lastRxTime = "N/A";
|
||||||
|
private long _rxBytes;
|
||||||
|
private long _txBytes;
|
||||||
|
|
||||||
|
public MainWindowViewModel()
|
||||||
|
{
|
||||||
|
Pages = new ObservableCollection<PageViewModelBase>
|
||||||
|
{
|
||||||
|
new MainPageViewModel(),
|
||||||
|
new JLinkPageViewModel()
|
||||||
|
};
|
||||||
|
|
||||||
|
CurrentPage = Pages.FirstOrDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ObservableCollection<PageViewModelBase> Pages { get; }
|
||||||
|
|
||||||
|
public PageViewModelBase? CurrentPage
|
||||||
|
{
|
||||||
|
get => _currentPage;
|
||||||
|
set => SetProperty(ref _currentPage, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string SelectedDevice
|
||||||
|
{
|
||||||
|
get => _selectedDevice;
|
||||||
|
set => SetProperty(ref _selectedDevice, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string ConnectionStatus
|
||||||
|
{
|
||||||
|
get => _connectionStatus;
|
||||||
|
set => SetProperty(ref _connectionStatus, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string LastRxTime
|
||||||
|
{
|
||||||
|
get => _lastRxTime;
|
||||||
|
set => SetProperty(ref _lastRxTime, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public long RxBytes
|
||||||
|
{
|
||||||
|
get => _rxBytes;
|
||||||
|
set => SetProperty(ref _rxBytes, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public long TxBytes
|
||||||
|
{
|
||||||
|
get => _txBytes;
|
||||||
|
set => SetProperty(ref _txBytes, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
12
ViewModels/PageViewModelBase.cs
Normal file
12
ViewModels/PageViewModelBase.cs
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
namespace AmebaPro3_ControlPanel.ViewModels;
|
||||||
|
|
||||||
|
public abstract class PageViewModelBase : ViewModelBase
|
||||||
|
{
|
||||||
|
private string _title = string.Empty;
|
||||||
|
|
||||||
|
public string Title
|
||||||
|
{
|
||||||
|
get => _title;
|
||||||
|
protected set => SetProperty(ref _title, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
167
ViewModels/UartConsoleViewModel.cs
Normal file
167
ViewModels/UartConsoleViewModel.cs
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Windows.Input;
|
||||||
|
using AmebaPro3_ControlPanel.Services.Uart;
|
||||||
|
using AmebaPro3_ControlPanel.Utilities;
|
||||||
|
|
||||||
|
namespace AmebaPro3_ControlPanel.ViewModels;
|
||||||
|
|
||||||
|
public class UartConsoleViewModel : ConsolePanelViewModel
|
||||||
|
{
|
||||||
|
private string? _selectedPort;
|
||||||
|
private int _baudRate;
|
||||||
|
private readonly PythonUartSession _uartSession;
|
||||||
|
private bool _isConnecting;
|
||||||
|
|
||||||
|
public UartConsoleViewModel(string title, int defaultBaudRate) : base(title)
|
||||||
|
{
|
||||||
|
_baudRate = defaultBaudRate;
|
||||||
|
_uartSession = new PythonUartSession(GetUartBridgePath(), SynchronizationContext.Current);
|
||||||
|
_uartSession.LineReceived += line => LogLines.Add(line);
|
||||||
|
_uartSession.ErrorReceived += line => LogLines.Add($"[UART ERROR] {line}");
|
||||||
|
_uartSession.Exited += OnSessionExited;
|
||||||
|
|
||||||
|
ConnectCommand = new RelayCommand(_ => ConnectAsync(), _ => !IsConnected && !_isConnecting && !string.IsNullOrWhiteSpace(SelectedPort));
|
||||||
|
DisconnectCommand = new RelayCommand(_ => Disconnect(), _ => IsConnected);
|
||||||
|
OpenTeraTermCommand = new RelayCommand(_ => OpenTeraTerm(), _ => !string.IsNullOrWhiteSpace(SelectedPort));
|
||||||
|
}
|
||||||
|
|
||||||
|
public ObservableCollection<string> AvailablePorts { get; } = new();
|
||||||
|
|
||||||
|
public string? SelectedPort
|
||||||
|
{
|
||||||
|
get => _selectedPort;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (SetProperty(ref _selectedPort, value))
|
||||||
|
{
|
||||||
|
(ConnectCommand as RelayCommand)?.RaiseCanExecuteChanged();
|
||||||
|
(OpenTeraTermCommand as RelayCommand)?.RaiseCanExecuteChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int BaudRate
|
||||||
|
{
|
||||||
|
get => _baudRate;
|
||||||
|
set => SetProperty(ref _baudRate, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ICommand ConnectCommand { get; }
|
||||||
|
|
||||||
|
public ICommand DisconnectCommand { get; }
|
||||||
|
|
||||||
|
public ICommand OpenTeraTermCommand { get; }
|
||||||
|
|
||||||
|
public void SetPorts(IEnumerable<string> ports)
|
||||||
|
{
|
||||||
|
AvailablePorts.Clear();
|
||||||
|
foreach (var port in ports)
|
||||||
|
{
|
||||||
|
AvailablePorts.Add(port);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(SelectedPort))
|
||||||
|
{
|
||||||
|
SelectedPort = AvailablePorts.FirstOrDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void SendCurrentCommand(bool addToHistory)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(CurrentCommand))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var command = CurrentCommand.Trim();
|
||||||
|
base.SendCurrentCommand(addToHistory);
|
||||||
|
|
||||||
|
if (IsConnected)
|
||||||
|
{
|
||||||
|
_ = _uartSession.SendAsync(command);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
LogLines.Add("[UART] Not connected.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void ConnectAsync()
|
||||||
|
{
|
||||||
|
if (_isConnecting || string.IsNullOrWhiteSpace(SelectedPort))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_isConnecting = true;
|
||||||
|
(ConnectCommand as RelayCommand)?.RaiseCanExecuteChanged();
|
||||||
|
LogLines.Add($"[UART] Connecting to {SelectedPort} @ {BaudRate}...");
|
||||||
|
|
||||||
|
var started = await _uartSession.StartAsync(SelectedPort, BaudRate);
|
||||||
|
if (started)
|
||||||
|
{
|
||||||
|
IsConnected = true;
|
||||||
|
LogLines.Add($"[UART] Connected to {SelectedPort} @ {BaudRate}.");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
LogLines.Add("[UART] Failed to start session. Ensure Python + pyserial are installed.");
|
||||||
|
}
|
||||||
|
|
||||||
|
_isConnecting = false;
|
||||||
|
(DisconnectCommand as RelayCommand)?.RaiseCanExecuteChanged();
|
||||||
|
(ConnectCommand as RelayCommand)?.RaiseCanExecuteChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Disconnect()
|
||||||
|
{
|
||||||
|
_ = DisconnectAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async System.Threading.Tasks.Task DisconnectAsync()
|
||||||
|
{
|
||||||
|
IsConnected = false;
|
||||||
|
_isConnecting = false;
|
||||||
|
(DisconnectCommand as RelayCommand)?.RaiseCanExecuteChanged();
|
||||||
|
(ConnectCommand as RelayCommand)?.RaiseCanExecuteChanged();
|
||||||
|
|
||||||
|
await _uartSession.StopAsync();
|
||||||
|
LogLines.Add("[UART] Disconnected.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnSessionExited()
|
||||||
|
{
|
||||||
|
if (!IsConnected)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
IsConnected = false;
|
||||||
|
_isConnecting = false;
|
||||||
|
LogLines.Add("[UART] Session closed.");
|
||||||
|
(DisconnectCommand as RelayCommand)?.RaiseCanExecuteChanged();
|
||||||
|
(ConnectCommand as RelayCommand)?.RaiseCanExecuteChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OpenTeraTerm()
|
||||||
|
{
|
||||||
|
if (TeraTermLauncher.TryLaunch(SelectedPort, BaudRate, out var message))
|
||||||
|
{
|
||||||
|
LogLines.Add($"[UART] {message}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
LogLines.Add($"[UART] {message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetUartBridgePath()
|
||||||
|
{
|
||||||
|
return Path.Combine(AppContext.BaseDirectory, "Scripts", "uart_bridge.py");
|
||||||
|
}
|
||||||
|
}
|
||||||
27
ViewModels/ViewModelBase.cs
Normal file
27
ViewModels/ViewModelBase.cs
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
|
namespace AmebaPro3_ControlPanel.ViewModels;
|
||||||
|
|
||||||
|
public abstract class ViewModelBase : INotifyPropertyChanged
|
||||||
|
{
|
||||||
|
public event PropertyChangedEventHandler? PropertyChanged;
|
||||||
|
|
||||||
|
protected void RaisePropertyChanged([CallerMemberName] string? propertyName = null)
|
||||||
|
{
|
||||||
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected bool SetProperty<T>(ref T storage, T value, [CallerMemberName] string? propertyName = null)
|
||||||
|
{
|
||||||
|
if (EqualityComparer<T>.Default.Equals(storage, value))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
storage = value;
|
||||||
|
RaisePropertyChanged(propertyName);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
216
Views/Controls/ConsolePanel.xaml
Normal file
216
Views/Controls/ConsolePanel.xaml
Normal file
@ -0,0 +1,216 @@
|
|||||||
|
<UserControl x:Class="AmebaPro3_ControlPanel.Views.Controls.ConsolePanel"
|
||||||
|
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"
|
||||||
|
mc:Ignorable="d"
|
||||||
|
d:DesignHeight="450"
|
||||||
|
d:DesignWidth="800">
|
||||||
|
<Border Style="{StaticResource CardStyle}">
|
||||||
|
<Grid>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/> <!-- Header with custom controls -->
|
||||||
|
<RowDefinition Height="*"/> <!-- Log -->
|
||||||
|
<RowDefinition Height="Auto"/> <!-- GridSplitter -->
|
||||||
|
<RowDefinition Height="*" MinHeight="140"/> <!-- History -->
|
||||||
|
<RowDefinition Height="Auto"/> <!-- Command buttons slot -->
|
||||||
|
<RowDefinition Height="Auto"/> <!-- Actions row -->
|
||||||
|
<RowDefinition Height="Auto"/> <!-- Input -->
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<!-- Header -->
|
||||||
|
<Grid Grid.Row="0" Margin="0,0,0,12">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||||
|
<Ellipse Width="10"
|
||||||
|
Height="10"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Margin="0,0,10,0">
|
||||||
|
<Ellipse.Style>
|
||||||
|
<Style TargetType="Ellipse">
|
||||||
|
<Setter Property="Fill" Value="{StaticResource DangerBrush}"/>
|
||||||
|
<Style.Triggers>
|
||||||
|
<DataTrigger Binding="{Binding IsConnected}" Value="True">
|
||||||
|
<Setter Property="Fill" Value="{StaticResource SuccessBrush}"/>
|
||||||
|
</DataTrigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
</Ellipse.Style>
|
||||||
|
</Ellipse>
|
||||||
|
<StackPanel>
|
||||||
|
<TextBlock Text="{Binding Title}"
|
||||||
|
FontSize="16"
|
||||||
|
FontWeight="SemiBold"/>
|
||||||
|
<TextBlock FontSize="12">
|
||||||
|
<TextBlock.Style>
|
||||||
|
<Style TargetType="TextBlock" BasedOn="{StaticResource CaptionTextStyle}">
|
||||||
|
<Setter Property="Text" Value="Disconnected"/>
|
||||||
|
<Style.Triggers>
|
||||||
|
<DataTrigger Binding="{Binding IsConnected}" Value="True">
|
||||||
|
<Setter Property="Text" Value="Connected"/>
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource SuccessBrush}"/>
|
||||||
|
</DataTrigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
</TextBlock.Style>
|
||||||
|
</TextBlock>
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<ContentPresenter Grid.Column="1"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Content="{Binding RelativeSource={RelativeSource AncestorType=UserControl}, Path=HeaderContent}">
|
||||||
|
<ContentPresenter.Style>
|
||||||
|
<Style TargetType="ContentPresenter">
|
||||||
|
<Setter Property="Visibility" Value="Visible"/>
|
||||||
|
<Style.Triggers>
|
||||||
|
<DataTrigger Binding="{Binding Content, RelativeSource={RelativeSource Self}}" Value="{x:Null}">
|
||||||
|
<Setter Property="Visibility" Value="Collapsed"/>
|
||||||
|
</DataTrigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
</ContentPresenter.Style>
|
||||||
|
</ContentPresenter>
|
||||||
|
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<!-- Log viewer -->
|
||||||
|
<Border Grid.Row="1"
|
||||||
|
Background="{StaticResource SurfaceAltBrush}"
|
||||||
|
BorderBrush="{StaticResource SubtleBorderBrush}"
|
||||||
|
BorderThickness="1"
|
||||||
|
CornerRadius="10">
|
||||||
|
<ListBox ItemsSource="{Binding LogLines}"
|
||||||
|
x:Name="LogListBox"
|
||||||
|
SelectionMode="Extended"
|
||||||
|
PreviewMouseWheel="LogListBox_OnPreviewMouseWheel"
|
||||||
|
PreviewMouseDown="LogListBox_OnPreviewMouseDown"
|
||||||
|
PreviewKeyDown="LogListBox_OnPreviewKeyDown"
|
||||||
|
KeyDown="LogListBox_OnKeyDown"
|
||||||
|
VirtualizingPanel.IsVirtualizing="True"
|
||||||
|
VirtualizingPanel.VirtualizationMode="Recycling"
|
||||||
|
ScrollViewer.HorizontalScrollBarVisibility="Auto"
|
||||||
|
ScrollViewer.VerticalScrollBarVisibility="Auto"
|
||||||
|
ScrollViewer.CanContentScroll="True">
|
||||||
|
<ListBox.ItemsPanel>
|
||||||
|
<ItemsPanelTemplate>
|
||||||
|
<VirtualizingStackPanel/>
|
||||||
|
</ItemsPanelTemplate>
|
||||||
|
</ListBox.ItemsPanel>
|
||||||
|
<ListBox.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<TextBlock Text="{Binding}"
|
||||||
|
FontFamily="Cascadia Code, Consolas, Segoe UI Mono"
|
||||||
|
FontSize="12"
|
||||||
|
TextWrapping="NoWrap"/>
|
||||||
|
</DataTemplate>
|
||||||
|
</ListBox.ItemTemplate>
|
||||||
|
</ListBox>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- GridSplitter for resizing history -->
|
||||||
|
<GridSplitter Grid.Row="2"
|
||||||
|
Height="4"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
VerticalAlignment="Top"
|
||||||
|
Background="{StaticResource SubtleBorderBrush}"
|
||||||
|
ResizeDirection="Rows"
|
||||||
|
ResizeBehavior="PreviousAndNext"
|
||||||
|
Margin="0,10,0,0"
|
||||||
|
ShowsPreview="True"/>
|
||||||
|
|
||||||
|
<!-- Command history -->
|
||||||
|
<Border Grid.Row="3"
|
||||||
|
Margin="0,14,0,0"
|
||||||
|
Background="{StaticResource SurfaceAltBrush}"
|
||||||
|
BorderBrush="{StaticResource SubtleBorderBrush}"
|
||||||
|
BorderThickness="1"
|
||||||
|
CornerRadius="10">
|
||||||
|
<ListBox x:Name="HistoryListBox"
|
||||||
|
ItemsSource="{Binding History}"
|
||||||
|
SelectionMode="Extended"
|
||||||
|
SelectionChanged="HistoryListBox_OnSelectionChanged"
|
||||||
|
MouseDoubleClick="HistoryListBox_OnMouseDoubleClick"
|
||||||
|
PreviewMouseWheel="HistoryListBox_OnPreviewMouseWheel"
|
||||||
|
PreviewMouseDown="HistoryListBox_OnPreviewMouseDown"
|
||||||
|
PreviewKeyDown="HistoryListBox_OnPreviewKeyDown"
|
||||||
|
KeyDown="HistoryListBox_OnKeyDown"
|
||||||
|
VirtualizingPanel.IsVirtualizing="True"
|
||||||
|
VirtualizingPanel.VirtualizationMode="Recycling"
|
||||||
|
ScrollViewer.VerticalScrollBarVisibility="Auto"
|
||||||
|
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
|
||||||
|
ScrollViewer.CanContentScroll="True">
|
||||||
|
<ListBox.ItemsPanel>
|
||||||
|
<ItemsPanelTemplate>
|
||||||
|
<VirtualizingStackPanel/>
|
||||||
|
</ItemsPanelTemplate>
|
||||||
|
</ListBox.ItemsPanel>
|
||||||
|
<ListBox.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<TextBlock Text="{Binding}" FontSize="12"/>
|
||||||
|
</DataTemplate>
|
||||||
|
</ListBox.ItemTemplate>
|
||||||
|
</ListBox>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- Optional command buttons slot -->
|
||||||
|
<ContentPresenter Grid.Row="4"
|
||||||
|
Margin="0,10,0,0"
|
||||||
|
Content="{Binding RelativeSource={RelativeSource AncestorType=UserControl}, Path=CommandButtonsContent}">
|
||||||
|
<ContentPresenter.Style>
|
||||||
|
<Style TargetType="ContentPresenter">
|
||||||
|
<Setter Property="Visibility" Value="Visible"/>
|
||||||
|
<Style.Triggers>
|
||||||
|
<DataTrigger Binding="{Binding Content, RelativeSource={RelativeSource Self}}" Value="{x:Null}">
|
||||||
|
<Setter Property="Visibility" Value="Collapsed"/>
|
||||||
|
</DataTrigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
</ContentPresenter.Style>
|
||||||
|
</ContentPresenter>
|
||||||
|
|
||||||
|
<!-- Actions row -->
|
||||||
|
<StackPanel Grid.Row="5"
|
||||||
|
Orientation="Horizontal"
|
||||||
|
HorizontalAlignment="Right"
|
||||||
|
Margin="0,10,0,0">
|
||||||
|
<Button Content="Clear"
|
||||||
|
Command="{Binding ClearLog}"
|
||||||
|
Style="{StaticResource GhostButtonStyle}"
|
||||||
|
Width="120"
|
||||||
|
Margin="0,0,8,0"/>
|
||||||
|
<Button Content="Save"
|
||||||
|
Command="{Binding SaveLog}"
|
||||||
|
Style="{StaticResource GhostButtonStyle}"
|
||||||
|
Width="120"
|
||||||
|
Margin="0,0,8,0"/>
|
||||||
|
<Button Content="Load Commands"
|
||||||
|
Command="{Binding LoadCommandFileCommand}"
|
||||||
|
Style="{StaticResource GhostButtonStyle}"
|
||||||
|
Width="140"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- Input row -->
|
||||||
|
<Grid Grid.Row="6" Margin="0,10,0,0">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
<ColumnDefinition Width="8"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<TextBox x:Name="CommandInput"
|
||||||
|
Grid.Column="0"
|
||||||
|
Text="{Binding CurrentCommand, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
|
||||||
|
KeyDown="CommandInput_OnKeyDown"/>
|
||||||
|
<Button Grid.Column="2"
|
||||||
|
Content="Send"
|
||||||
|
Command="{Binding SendCommand}"
|
||||||
|
Style="{StaticResource PrimaryButtonStyle}"
|
||||||
|
MinWidth="96"/>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
</UserControl>
|
||||||
482
Views/Controls/ConsolePanel.xaml.cs
Normal file
482
Views/Controls/ConsolePanel.xaml.cs
Normal file
@ -0,0 +1,482 @@
|
|||||||
|
using System.Collections.Specialized;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
using System.Windows.Controls.Primitives;
|
||||||
|
using System.Windows.Input;
|
||||||
|
using System.Windows.Media;
|
||||||
|
using AmebaPro3_ControlPanel.ViewModels;
|
||||||
|
|
||||||
|
namespace AmebaPro3_ControlPanel.Views.Controls;
|
||||||
|
|
||||||
|
public partial class ConsolePanel : UserControl
|
||||||
|
{
|
||||||
|
public static readonly DependencyProperty HeaderContentProperty =
|
||||||
|
DependencyProperty.Register(nameof(HeaderContent), typeof(UIElement), typeof(ConsolePanel), new PropertyMetadata(null));
|
||||||
|
|
||||||
|
public static readonly DependencyProperty CommandButtonsContentProperty =
|
||||||
|
DependencyProperty.Register(nameof(CommandButtonsContent), typeof(UIElement), typeof(ConsolePanel), new PropertyMetadata(null));
|
||||||
|
|
||||||
|
public UIElement? HeaderContent
|
||||||
|
{
|
||||||
|
get => (UIElement?)GetValue(HeaderContentProperty);
|
||||||
|
set => SetValue(HeaderContentProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public UIElement? CommandButtonsContent
|
||||||
|
{
|
||||||
|
get => (UIElement?)GetValue(CommandButtonsContentProperty);
|
||||||
|
set => SetValue(CommandButtonsContentProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool _isAutoScrolling = true;
|
||||||
|
private ScrollViewer? _logScrollViewer;
|
||||||
|
private bool _isProgrammaticScroll = false;
|
||||||
|
private bool _userScrollIntent = false;
|
||||||
|
private bool _isHistoryAutoScrolling = true;
|
||||||
|
private ScrollViewer? _historyScrollViewer;
|
||||||
|
private bool _isHistoryProgrammaticScroll = false;
|
||||||
|
private bool _historyUserScrollIntent = false;
|
||||||
|
|
||||||
|
public ConsolePanel()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
Loaded += ConsolePanel_Loaded;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ConsolePanel_Loaded(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (DataContext is ConsolePanelViewModel vm)
|
||||||
|
{
|
||||||
|
vm.LogLines.CollectionChanged += LogLines_CollectionChanged;
|
||||||
|
vm.History.CollectionChanged += History_CollectionChanged;
|
||||||
|
|
||||||
|
// Scroll to bottom if there are already items
|
||||||
|
if (vm.LogLines.Count > 0)
|
||||||
|
{
|
||||||
|
Dispatcher.BeginInvoke(new System.Action(() =>
|
||||||
|
{
|
||||||
|
if (_logScrollViewer != null)
|
||||||
|
{
|
||||||
|
_isProgrammaticScroll = true;
|
||||||
|
_logScrollViewer.ScrollToEnd();
|
||||||
|
_isProgrammaticScroll = false;
|
||||||
|
}
|
||||||
|
else if (LogListBox.Items.Count > 0)
|
||||||
|
{
|
||||||
|
LogListBox.ScrollIntoView(LogListBox.Items[LogListBox.Items.Count - 1]);
|
||||||
|
}
|
||||||
|
}), System.Windows.Threading.DispatcherPriority.Loaded);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (vm.History.Count > 0)
|
||||||
|
{
|
||||||
|
Dispatcher.BeginInvoke(new System.Action(() =>
|
||||||
|
{
|
||||||
|
if (_historyScrollViewer != null)
|
||||||
|
{
|
||||||
|
_isHistoryProgrammaticScroll = true;
|
||||||
|
_historyScrollViewer.ScrollToEnd();
|
||||||
|
_isHistoryProgrammaticScroll = false;
|
||||||
|
}
|
||||||
|
}), System.Windows.Threading.DispatcherPriority.Loaded);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the ScrollViewer in the LogListBox
|
||||||
|
_logScrollViewer = FindScrollViewer(LogListBox);
|
||||||
|
if (_logScrollViewer != null)
|
||||||
|
{
|
||||||
|
_logScrollViewer.ScrollChanged += LogScrollViewer_ScrollChanged;
|
||||||
|
// Initialize auto-scroll state
|
||||||
|
_isAutoScrolling = IsAtBottom(_logScrollViewer);
|
||||||
|
}
|
||||||
|
|
||||||
|
_historyScrollViewer = FindScrollViewer(HistoryListBox);
|
||||||
|
if (_historyScrollViewer != null)
|
||||||
|
{
|
||||||
|
_historyScrollViewer.ScrollChanged += HistoryScrollViewer_ScrollChanged;
|
||||||
|
_isHistoryAutoScrolling = IsAtBottom(_historyScrollViewer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LogLines_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.Action == NotifyCollectionChangedAction.Add)
|
||||||
|
{
|
||||||
|
// Ensure ScrollViewer is found (might not be available immediately)
|
||||||
|
if (_logScrollViewer == null)
|
||||||
|
{
|
||||||
|
_logScrollViewer = FindScrollViewer(LogListBox);
|
||||||
|
if (_logScrollViewer != null)
|
||||||
|
{
|
||||||
|
_logScrollViewer.ScrollChanged += LogScrollViewer_ScrollChanged;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we should auto-scroll (verify current position before scrolling)
|
||||||
|
bool shouldAutoScroll = _isAutoScrolling && !_userScrollIntent;
|
||||||
|
if (_logScrollViewer != null && !_userScrollIntent)
|
||||||
|
{
|
||||||
|
shouldAutoScroll = IsAtBottom(_logScrollViewer);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldAutoScroll)
|
||||||
|
{
|
||||||
|
// Scroll to the bottom after UI updates
|
||||||
|
Dispatcher.BeginInvoke(new System.Action(() =>
|
||||||
|
{
|
||||||
|
if (_logScrollViewer != null)
|
||||||
|
{
|
||||||
|
_isProgrammaticScroll = true;
|
||||||
|
_logScrollViewer.ScrollToEnd();
|
||||||
|
_isProgrammaticScroll = false;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Fallback: try to find ScrollViewer again and scroll
|
||||||
|
_logScrollViewer = FindScrollViewer(LogListBox);
|
||||||
|
if (_logScrollViewer != null)
|
||||||
|
{
|
||||||
|
_logScrollViewer.ScrollChanged += LogScrollViewer_ScrollChanged;
|
||||||
|
_isProgrammaticScroll = true;
|
||||||
|
_logScrollViewer.ScrollToEnd();
|
||||||
|
_isProgrammaticScroll = false;
|
||||||
|
}
|
||||||
|
else if (LogListBox.Items.Count > 0)
|
||||||
|
{
|
||||||
|
// Last resort: use ScrollIntoView
|
||||||
|
LogListBox.ScrollIntoView(LogListBox.Items[LogListBox.Items.Count - 1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}), System.Windows.Threading.DispatcherPriority.Loaded);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LogScrollViewer_ScrollChanged(object sender, ScrollChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if (_logScrollViewer == null) return;
|
||||||
|
|
||||||
|
// Ignore scroll changes caused by programmatic scrolling
|
||||||
|
if (_isProgrammaticScroll)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.VerticalChange != 0)
|
||||||
|
{
|
||||||
|
_userScrollIntent = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_userScrollIntent)
|
||||||
|
{
|
||||||
|
_isAutoScrolling = IsAtBottom(_logScrollViewer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void History_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.Action == NotifyCollectionChangedAction.Add)
|
||||||
|
{
|
||||||
|
if (_historyScrollViewer == null)
|
||||||
|
{
|
||||||
|
_historyScrollViewer = FindScrollViewer(HistoryListBox);
|
||||||
|
if (_historyScrollViewer != null)
|
||||||
|
{
|
||||||
|
_historyScrollViewer.ScrollChanged += HistoryScrollViewer_ScrollChanged;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool shouldAutoScroll = _isHistoryAutoScrolling && !_historyUserScrollIntent;
|
||||||
|
if (_historyScrollViewer != null && !_historyUserScrollIntent)
|
||||||
|
{
|
||||||
|
shouldAutoScroll = IsAtBottom(_historyScrollViewer);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldAutoScroll)
|
||||||
|
{
|
||||||
|
Dispatcher.BeginInvoke(new System.Action(() =>
|
||||||
|
{
|
||||||
|
if (_historyScrollViewer != null)
|
||||||
|
{
|
||||||
|
_isHistoryProgrammaticScroll = true;
|
||||||
|
_historyScrollViewer.ScrollToEnd();
|
||||||
|
_isHistoryProgrammaticScroll = false;
|
||||||
|
}
|
||||||
|
}), System.Windows.Threading.DispatcherPriority.Loaded);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HistoryScrollViewer_ScrollChanged(object sender, ScrollChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if (_historyScrollViewer == null) return;
|
||||||
|
|
||||||
|
if (_isHistoryProgrammaticScroll)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.VerticalChange != 0)
|
||||||
|
{
|
||||||
|
_historyUserScrollIntent = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_historyUserScrollIntent)
|
||||||
|
{
|
||||||
|
_isHistoryAutoScrolling = IsAtBottom(_historyScrollViewer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsAtBottom(ScrollViewer scrollViewer)
|
||||||
|
{
|
||||||
|
return scrollViewer.ExtentHeight <= scrollViewer.ViewportHeight
|
||||||
|
|| (scrollViewer.VerticalOffset + scrollViewer.ViewportHeight >= scrollViewer.ExtentHeight - 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static ScrollViewer? FindScrollViewer(DependencyObject parent)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(parent); i++)
|
||||||
|
{
|
||||||
|
var child = VisualTreeHelper.GetChild(parent, i);
|
||||||
|
if (child is ScrollViewer scrollViewer)
|
||||||
|
{
|
||||||
|
return scrollViewer;
|
||||||
|
}
|
||||||
|
var result = FindScrollViewer(child);
|
||||||
|
if (result != null)
|
||||||
|
{
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LogListBox_OnPreviewMouseWheel(object sender, MouseWheelEventArgs e)
|
||||||
|
{
|
||||||
|
MarkUserScrollIntent();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LogListBox_OnPreviewMouseDown(object sender, MouseButtonEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.ChangedButton == MouseButton.Left || e.ChangedButton == MouseButton.Middle || e.ChangedButton == MouseButton.Right)
|
||||||
|
{
|
||||||
|
MarkUserScrollIntent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LogListBox_OnPreviewKeyDown(object sender, KeyEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.Key is Key.Up or Key.Down or Key.PageUp or Key.PageDown or Key.Home or Key.End)
|
||||||
|
{
|
||||||
|
MarkUserScrollIntent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void MarkUserScrollIntent()
|
||||||
|
{
|
||||||
|
_userScrollIntent = true;
|
||||||
|
|
||||||
|
if (_logScrollViewer == null)
|
||||||
|
{
|
||||||
|
_logScrollViewer = FindScrollViewer(LogListBox);
|
||||||
|
if (_logScrollViewer != null)
|
||||||
|
{
|
||||||
|
_logScrollViewer.ScrollChanged += LogScrollViewer_ScrollChanged;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Dispatcher.BeginInvoke(new System.Action(() =>
|
||||||
|
{
|
||||||
|
if (_logScrollViewer == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_userScrollIntent)
|
||||||
|
{
|
||||||
|
_userScrollIntent = false;
|
||||||
|
_isAutoScrolling = IsAtBottom(_logScrollViewer);
|
||||||
|
}
|
||||||
|
}), System.Windows.Threading.DispatcherPriority.Background);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HistoryListBox_OnPreviewMouseWheel(object sender, MouseWheelEventArgs e)
|
||||||
|
{
|
||||||
|
MarkHistoryUserScrollIntent();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HistoryListBox_OnPreviewMouseDown(object sender, MouseButtonEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.ChangedButton == MouseButton.Left || e.ChangedButton == MouseButton.Middle || e.ChangedButton == MouseButton.Right)
|
||||||
|
{
|
||||||
|
MarkHistoryUserScrollIntent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HistoryListBox_OnPreviewKeyDown(object sender, KeyEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.Key is Key.Up or Key.Down or Key.PageUp or Key.PageDown or Key.Home or Key.End)
|
||||||
|
{
|
||||||
|
MarkHistoryUserScrollIntent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void MarkHistoryUserScrollIntent()
|
||||||
|
{
|
||||||
|
_historyUserScrollIntent = true;
|
||||||
|
|
||||||
|
if (_historyScrollViewer == null)
|
||||||
|
{
|
||||||
|
_historyScrollViewer = FindScrollViewer(HistoryListBox);
|
||||||
|
if (_historyScrollViewer != null)
|
||||||
|
{
|
||||||
|
_historyScrollViewer.ScrollChanged += HistoryScrollViewer_ScrollChanged;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Dispatcher.BeginInvoke(new System.Action(() =>
|
||||||
|
{
|
||||||
|
if (_historyScrollViewer == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_historyUserScrollIntent)
|
||||||
|
{
|
||||||
|
_historyUserScrollIntent = false;
|
||||||
|
_isHistoryAutoScrolling = IsAtBottom(_historyScrollViewer);
|
||||||
|
}
|
||||||
|
}), System.Windows.Threading.DispatcherPriority.Background);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HistoryListBox_OnSelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if (DataContext is ConsolePanelViewModel vm)
|
||||||
|
{
|
||||||
|
// Only update CurrentCommand if single item is selected
|
||||||
|
if (HistoryListBox.SelectedItems.Count == 1 && HistoryListBox.SelectedItem is string selected)
|
||||||
|
{
|
||||||
|
vm.CurrentCommand = selected;
|
||||||
|
}
|
||||||
|
else if (HistoryListBox.SelectedItems.Count == 0)
|
||||||
|
{
|
||||||
|
vm.CurrentCommand = string.Empty;
|
||||||
|
}
|
||||||
|
// For multi-select, don't update CurrentCommand
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HistoryListBox_OnMouseDoubleClick(object sender, MouseButtonEventArgs e)
|
||||||
|
{
|
||||||
|
if (DataContext is ConsolePanelViewModel vm && HistoryListBox.SelectedItem is string selected)
|
||||||
|
{
|
||||||
|
vm.SendFromHistory(selected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CommandInput_OnKeyDown(object sender, KeyEventArgs e)
|
||||||
|
{
|
||||||
|
if (DataContext is not ConsolePanelViewModel vm)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.Key == Key.Enter)
|
||||||
|
{
|
||||||
|
if (vm.SendCommand.CanExecute(null))
|
||||||
|
{
|
||||||
|
vm.SendCommand.Execute(null);
|
||||||
|
HistoryListBox.SelectedIndex = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
e.Handled = true;
|
||||||
|
}
|
||||||
|
else if (e.Key == Key.Up)
|
||||||
|
{
|
||||||
|
if (HistoryListBox.Items.Count == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (HistoryListBox.SelectedIndex < 0)
|
||||||
|
{
|
||||||
|
HistoryListBox.SelectedIndex = HistoryListBox.Items.Count - 1;
|
||||||
|
}
|
||||||
|
else if (HistoryListBox.SelectedIndex > 0)
|
||||||
|
{
|
||||||
|
HistoryListBox.SelectedIndex -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
e.Handled = true;
|
||||||
|
}
|
||||||
|
else if (e.Key == Key.Down)
|
||||||
|
{
|
||||||
|
if (HistoryListBox.Items.Count == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (HistoryListBox.SelectedIndex >= 0 && HistoryListBox.SelectedIndex < HistoryListBox.Items.Count - 1)
|
||||||
|
{
|
||||||
|
HistoryListBox.SelectedIndex += 1;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
HistoryListBox.SelectedIndex = -1;
|
||||||
|
vm.CurrentCommand = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
e.Handled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HistoryListBox_OnKeyDown(object sender, KeyEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.Key == Key.Delete && DataContext is ConsolePanelViewModel vm)
|
||||||
|
{
|
||||||
|
var selectedItems = HistoryListBox.SelectedItems.Cast<string>().ToList();
|
||||||
|
if (selectedItems.Count > 0)
|
||||||
|
{
|
||||||
|
vm.DeleteHistoryItems(selectedItems);
|
||||||
|
// Clear selection after deletion
|
||||||
|
HistoryListBox.SelectedIndex = -1;
|
||||||
|
e.Handled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LogListBox_OnKeyDown(object sender, KeyEventArgs e)
|
||||||
|
{
|
||||||
|
if (DataContext is not ConsolePanelViewModel vm)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.Key == Key.Delete)
|
||||||
|
{
|
||||||
|
var selectedItems = LogListBox.SelectedItems.Cast<string>().ToList();
|
||||||
|
if (selectedItems.Count > 0)
|
||||||
|
{
|
||||||
|
vm.DeleteLogLines(selectedItems);
|
||||||
|
// Clear selection after deletion
|
||||||
|
LogListBox.SelectedIndex = -1;
|
||||||
|
e.Handled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (e.Key == Key.C && (Keyboard.Modifiers & ModifierKeys.Control) == ModifierKeys.Control)
|
||||||
|
{
|
||||||
|
// Copy selected items to clipboard
|
||||||
|
var selectedItems = LogListBox.SelectedItems.Cast<string>().ToList();
|
||||||
|
if (selectedItems.Count > 0)
|
||||||
|
{
|
||||||
|
var clipboardText = string.Join("\r\n", selectedItems);
|
||||||
|
Clipboard.SetText(clipboardText);
|
||||||
|
e.Handled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
137
Views/MainWindow.xaml
Normal file
137
Views/MainWindow.xaml
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
<Window x:Class="AmebaPro3_ControlPanel.Views.MainWindow"
|
||||||
|
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"
|
||||||
|
mc:Ignorable="d"
|
||||||
|
Title="AmebaPro3 Control Panel"
|
||||||
|
Height="850"
|
||||||
|
Width="1400"
|
||||||
|
WindowStartupLocation="CenterScreen"
|
||||||
|
ClipToBounds="False">
|
||||||
|
<Grid x:Name="MainRootGrid" ClipToBounds="False">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="*"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<!-- Top bar -->
|
||||||
|
<Border x:Name="TopBarBorder" Grid.Row="0"
|
||||||
|
Background="{StaticResource SurfaceAltBrush}"
|
||||||
|
BorderBrush="{StaticResource SubtleBorderBrush}"
|
||||||
|
BorderThickness="0,0,0,1"
|
||||||
|
Padding="20,12,20,12"
|
||||||
|
ClipToBounds="False">
|
||||||
|
<Grid x:Name="TopBarGrid" ClipToBounds="False">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<StackPanel x:Name="TopBarStackPanel" Orientation="Vertical"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Margin="0,0,0,0"
|
||||||
|
ClipToBounds="False">
|
||||||
|
<TextBlock Text="AmebaPro3 Control Panel"
|
||||||
|
FontSize="20"
|
||||||
|
FontWeight="Bold"/>
|
||||||
|
<TextBlock Text="Powered by yiekheng, v2.0.0"
|
||||||
|
Style="{StaticResource CaptionTextStyle}"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<ListBox x:Name="TopNavListBox" Grid.Column="1"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Margin="24,0,0,0"
|
||||||
|
ItemsSource="{Binding Pages}"
|
||||||
|
SelectedItem="{Binding CurrentPage}"
|
||||||
|
Style="{StaticResource TopNavListBoxStyle}"
|
||||||
|
ItemContainerStyle="{StaticResource TopNavListBoxItemStyle}">
|
||||||
|
<ListBox.ItemsPanel>
|
||||||
|
<ItemsPanelTemplate>
|
||||||
|
<StackPanel Orientation="Horizontal"/>
|
||||||
|
</ItemsPanelTemplate>
|
||||||
|
</ListBox.ItemsPanel>
|
||||||
|
<ListBox.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<TextBlock Text="{Binding Title}"
|
||||||
|
Padding="18,8"
|
||||||
|
FontWeight="SemiBold"/>
|
||||||
|
</DataTemplate>
|
||||||
|
</ListBox.ItemTemplate>
|
||||||
|
</ListBox>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- Page content -->
|
||||||
|
<Border Grid.Row="1"
|
||||||
|
Margin="20,16"
|
||||||
|
Padding="0"
|
||||||
|
Background="Transparent">
|
||||||
|
<ContentControl x:Name="MainContentHost"
|
||||||
|
Content="{Binding CurrentPage}"/>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- Status bar -->
|
||||||
|
<Border Grid.Row="2"
|
||||||
|
Background="{StaticResource SurfaceAltBrush}"
|
||||||
|
BorderBrush="{StaticResource SubtleBorderBrush}"
|
||||||
|
BorderThickness="1,1,1,0"
|
||||||
|
Padding="20,8">
|
||||||
|
<Grid>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||||
|
<TextBlock Text="{Binding SelectedDevice}"
|
||||||
|
FontWeight="SemiBold"/>
|
||||||
|
<TextBlock Text=" • "
|
||||||
|
Margin="6,0"
|
||||||
|
Foreground="{StaticResource TextBrushMuted}"/>
|
||||||
|
<TextBlock Text="{Binding ConnectionStatus}"
|
||||||
|
Foreground="{StaticResource TextBrushSecondary}"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<StackPanel Grid.Column="1"
|
||||||
|
Orientation="Horizontal"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Margin="16,0">
|
||||||
|
<TextBlock Text="Last RX:"
|
||||||
|
Style="{StaticResource CaptionTextStyle}"/>
|
||||||
|
<TextBlock Text="{Binding LastRxTime}"
|
||||||
|
Margin="6,0,0,0"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<StackPanel Grid.Column="2"
|
||||||
|
Orientation="Horizontal"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Margin="16,0">
|
||||||
|
<TextBlock Text="RX:"
|
||||||
|
Style="{StaticResource CaptionTextStyle}"/>
|
||||||
|
<TextBlock Text="{Binding RxBytes}"
|
||||||
|
Margin="6,0,0,0"/>
|
||||||
|
<TextBlock Text="bytes"
|
||||||
|
Margin="4,0,0,0"
|
||||||
|
Style="{StaticResource CaptionTextStyle}"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<StackPanel Grid.Column="3"
|
||||||
|
Orientation="Horizontal"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Margin="16,0">
|
||||||
|
<TextBlock Text="TX:"
|
||||||
|
Style="{StaticResource CaptionTextStyle}"/>
|
||||||
|
<TextBlock Text="{Binding TxBytes}"
|
||||||
|
Margin="6,0,0,0"/>
|
||||||
|
<TextBlock Text="bytes"
|
||||||
|
Margin="4,0,0,0"
|
||||||
|
Style="{StaticResource CaptionTextStyle}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
</Grid>
|
||||||
|
</Window>
|
||||||
13
Views/MainWindow.xaml.cs
Normal file
13
Views/MainWindow.xaml.cs
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
using System.Windows;
|
||||||
|
using AmebaPro3_ControlPanel.ViewModels;
|
||||||
|
|
||||||
|
namespace AmebaPro3_ControlPanel.Views;
|
||||||
|
|
||||||
|
public partial class MainWindow : Window
|
||||||
|
{
|
||||||
|
public MainWindow()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
DataContext = new MainWindowViewModel();
|
||||||
|
}
|
||||||
|
}
|
||||||
70
Views/Pages/JLinkPage.xaml
Normal file
70
Views/Pages/JLinkPage.xaml
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
<UserControl x:Class="AmebaPro3_ControlPanel.Views.Pages.JLinkPage"
|
||||||
|
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"
|
||||||
|
mc:Ignorable="d"
|
||||||
|
d:DesignHeight="720"
|
||||||
|
d:DesignWidth="1280">
|
||||||
|
<Grid>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
<ColumnDefinition Width="280"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<!-- Left workspace -->
|
||||||
|
<Grid>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="*"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<StackPanel Grid.Row="0"
|
||||||
|
Orientation="Horizontal"
|
||||||
|
Margin="0,0,0,12">
|
||||||
|
<ComboBox Width="220"
|
||||||
|
ItemsSource="{Binding AvailableSerialNumbers}"
|
||||||
|
SelectedItem="{Binding SelectedSerialNumber}"
|
||||||
|
Margin="0,0,8,0"/>
|
||||||
|
<Button Content="Refresh"
|
||||||
|
Command="{Binding RefreshSerialsCommand}"
|
||||||
|
Style="{StaticResource GhostButtonStyle}"
|
||||||
|
Width="120"
|
||||||
|
Margin="0,0,8,0"/>
|
||||||
|
<Button Content="Connect"
|
||||||
|
Command="{Binding ConnectJLinkCommand}"
|
||||||
|
Style="{StaticResource PrimaryButtonStyle}"
|
||||||
|
Width="140"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<Border Grid.Row="1"
|
||||||
|
Background="Transparent"
|
||||||
|
BorderBrush="{StaticResource SubtleBorderBrush}"
|
||||||
|
BorderThickness="1"
|
||||||
|
CornerRadius="10">
|
||||||
|
<ContentControl Content="{Binding SelectedApSession}"/>
|
||||||
|
</Border>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<!-- AP selector -->
|
||||||
|
<Border Grid.Column="1"
|
||||||
|
Margin="12,0,0,0"
|
||||||
|
Style="{StaticResource CardStyle}">
|
||||||
|
<StackPanel>
|
||||||
|
<TextBlock Text="AP Targets"
|
||||||
|
Style="{StaticResource SectionHeaderTextStyle}"/>
|
||||||
|
<ListBox x:Name="ApSelectorListBox"
|
||||||
|
ItemsSource="{Binding ApSessions}"
|
||||||
|
SelectedItem="{Binding SelectedApSession}"
|
||||||
|
ItemContainerStyle="{StaticResource SelectorListBoxItemStyle}">
|
||||||
|
<ListBox.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<TextBlock Text="{Binding Title}"
|
||||||
|
FontWeight="SemiBold"/>
|
||||||
|
</DataTemplate>
|
||||||
|
</ListBox.ItemTemplate>
|
||||||
|
</ListBox>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
</Grid>
|
||||||
|
</UserControl>
|
||||||
11
Views/Pages/JLinkPage.xaml.cs
Normal file
11
Views/Pages/JLinkPage.xaml.cs
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
using System.Windows.Controls;
|
||||||
|
|
||||||
|
namespace AmebaPro3_ControlPanel.Views.Pages;
|
||||||
|
|
||||||
|
public partial class JLinkPage : UserControl
|
||||||
|
{
|
||||||
|
public JLinkPage()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
}
|
||||||
184
Views/Pages/MainPage.xaml
Normal file
184
Views/Pages/MainPage.xaml
Normal file
@ -0,0 +1,184 @@
|
|||||||
|
<UserControl x:Class="AmebaPro3_ControlPanel.Views.Pages.MainPage"
|
||||||
|
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:controls="clr-namespace:AmebaPro3_ControlPanel.Views.Controls"
|
||||||
|
mc:Ignorable="d"
|
||||||
|
d:DesignHeight="720"
|
||||||
|
d:DesignWidth="1280">
|
||||||
|
<Grid>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
<ColumnDefinition Width="480"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<!-- AmebaPro3 UART console -->
|
||||||
|
<controls:ConsolePanel DataContext="{Binding AmebaConsole}">
|
||||||
|
<controls:ConsolePanel.HeaderContent>
|
||||||
|
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||||
|
<ComboBox Width="160"
|
||||||
|
ItemsSource="{Binding AvailablePorts}"
|
||||||
|
SelectedItem="{Binding SelectedPort}"
|
||||||
|
Margin="0,0,8,0"/>
|
||||||
|
<TextBox Width="150"
|
||||||
|
Text="{Binding BaudRate}"
|
||||||
|
Margin="0,0,8,0"/>
|
||||||
|
<Button Content="Connect"
|
||||||
|
Command="{Binding ConnectCommand}"
|
||||||
|
Width="140"
|
||||||
|
Margin="0,0,8,0">
|
||||||
|
<Button.Style>
|
||||||
|
<Style TargetType="Button" BasedOn="{StaticResource BaseButtonStyle}">
|
||||||
|
<Setter Property="Content" Value="Connect"/>
|
||||||
|
<Setter Property="Command" Value="{Binding ConnectCommand}"/>
|
||||||
|
<Style.Triggers>
|
||||||
|
<DataTrigger Binding="{Binding IsConnected}" Value="True">
|
||||||
|
<Setter Property="Content" Value="Disconnect"/>
|
||||||
|
<Setter Property="Command" Value="{Binding DisconnectCommand}"/>
|
||||||
|
</DataTrigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
</Button.Style>
|
||||||
|
</Button>
|
||||||
|
<Button Content="TeraTerm"
|
||||||
|
Command="{Binding OpenTeraTermCommand}"
|
||||||
|
Width="120"
|
||||||
|
Style="{StaticResource BaseButtonStyle}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</controls:ConsolePanel.HeaderContent>
|
||||||
|
</controls:ConsolePanel>
|
||||||
|
|
||||||
|
<!-- Right stack -->
|
||||||
|
<Grid Grid.Column="1" Margin="12,0,0,0">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="8"/>
|
||||||
|
<RowDefinition Height="*"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<!-- Flash Image Card -->
|
||||||
|
<Border Style="{StaticResource CardStyle}">
|
||||||
|
<Grid>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<TextBlock Text="Flash Image"
|
||||||
|
Style="{StaticResource SectionHeaderTextStyle}"/>
|
||||||
|
|
||||||
|
<Grid Grid.Row="1" Margin="0,8,0,0">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<TextBox x:Name="BootloaderPathTextBox"
|
||||||
|
Grid.Column="0"
|
||||||
|
IsReadOnly="True"
|
||||||
|
Text="{Binding BootloaderPath}"
|
||||||
|
Margin="0,0,8,0"/>
|
||||||
|
<Button x:Name="BrowseBootloaderButton"
|
||||||
|
Grid.Column="1"
|
||||||
|
Content="Browse"
|
||||||
|
Width="110"
|
||||||
|
Command="{Binding BrowseBootloaderCommand}"
|
||||||
|
Style="{StaticResource BaseButtonStyle}"/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid Grid.Row="2" Margin="0,8,0,0">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<TextBox x:Name="AppPathTextBox"
|
||||||
|
Grid.Column="0"
|
||||||
|
IsReadOnly="True"
|
||||||
|
Text="{Binding ApplicationPath}"
|
||||||
|
Margin="0,0,8,0"/>
|
||||||
|
<Button x:Name="BrowseAppButton"
|
||||||
|
Grid.Column="1"
|
||||||
|
Content="Browse"
|
||||||
|
Width="110"
|
||||||
|
Command="{Binding BrowseAppCommand}"
|
||||||
|
Style="{StaticResource BaseButtonStyle}"/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Button x:Name="FlashImageButton"
|
||||||
|
Grid.Row="3"
|
||||||
|
Content="Flash Image"
|
||||||
|
Command="{Binding FlashImageCommand}"
|
||||||
|
Style="{StaticResource PrimaryButtonStyle}"
|
||||||
|
Margin="0,12,0,0"/>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- Arduino Controller Card -->
|
||||||
|
<Border Grid.Row="2" Style="{StaticResource CardStyle}">
|
||||||
|
<StackPanel>
|
||||||
|
<TextBlock Text="Arduino Controller"
|
||||||
|
Style="{StaticResource SectionHeaderTextStyle}"/>
|
||||||
|
|
||||||
|
<StackPanel Orientation="Horizontal" Margin="0,0,0,8">
|
||||||
|
<ComboBox x:Name="ArduinoComComboBox"
|
||||||
|
Width="160"
|
||||||
|
ItemsSource="{Binding ArduinoPorts}"
|
||||||
|
SelectedItem="{Binding SelectedArduinoPort}"
|
||||||
|
Margin="0,0,8,0"/>
|
||||||
|
<TextBox x:Name="ArduinoBaudTextBox"
|
||||||
|
Width="140"
|
||||||
|
Text="{Binding ArduinoBaudRate}"
|
||||||
|
Margin="0,0,8,0"/>
|
||||||
|
<Button x:Name="ArduinoConnectButton"
|
||||||
|
Width="130"
|
||||||
|
Content="Connect"
|
||||||
|
Command="{Binding ArduinoConnectCommand}">
|
||||||
|
<Button.Style>
|
||||||
|
<Style TargetType="Button" BasedOn="{StaticResource BaseButtonStyle}">
|
||||||
|
<Setter Property="Content" Value="Connect"/>
|
||||||
|
<Style.Triggers>
|
||||||
|
<DataTrigger Binding="{Binding IsArduinoConnected}" Value="True">
|
||||||
|
<Setter Property="Content" Value="Disconnect"/>
|
||||||
|
</DataTrigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
</Button.Style>
|
||||||
|
</Button>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<UniformGrid Columns="4" Margin="0,0,0,8">
|
||||||
|
<Button x:Name="ArduinoResetButton"
|
||||||
|
Content="Reset"
|
||||||
|
Command="{Binding ArduinoResetCommand}"
|
||||||
|
Style="{StaticResource BaseButtonStyle}"
|
||||||
|
Margin="0,0,8,0"/>
|
||||||
|
<Button x:Name="ArduinoDownloadButton"
|
||||||
|
Content="Download Mode"
|
||||||
|
Command="{Binding ArduinoDownloadCommand}"
|
||||||
|
Style="{StaticResource BaseButtonStyle}"
|
||||||
|
Margin="0,0,8,0"/>
|
||||||
|
<Button x:Name="ArduinoNormalButton"
|
||||||
|
Content="Normal Mode"
|
||||||
|
Command="{Binding ArduinoNormalCommand}"
|
||||||
|
Style="{StaticResource BaseButtonStyle}"
|
||||||
|
Margin="0,0,8,0"/>
|
||||||
|
<Button x:Name="ArduinoTestModeButton"
|
||||||
|
Content="Test Mode"
|
||||||
|
Command="{Binding ArduinoTestModeCommand}"
|
||||||
|
Style="{StaticResource BaseButtonStyle}"/>
|
||||||
|
</UniformGrid>
|
||||||
|
|
||||||
|
<StackPanel Orientation="Vertical">
|
||||||
|
<TextBlock Text="Test mode"
|
||||||
|
Style="{StaticResource CaptionTextStyle}"/>
|
||||||
|
<ComboBox x:Name="ArduinoTestSelectComboBox"
|
||||||
|
ItemsSource="{Binding ArduinoTests}"
|
||||||
|
SelectedItem="{Binding SelectedArduinoTest}"
|
||||||
|
Margin="0,4,0,0"/>
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</UserControl>
|
||||||
11
Views/Pages/MainPage.xaml.cs
Normal file
11
Views/Pages/MainPage.xaml.cs
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
using System.Windows.Controls;
|
||||||
|
|
||||||
|
namespace AmebaPro3_ControlPanel.Views.Pages;
|
||||||
|
|
||||||
|
public partial class MainPage : UserControl
|
||||||
|
{
|
||||||
|
public MainPage()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user