3 Commits
Author SHA1 Message Date
etwenandClaude Opus 4.8 c49ccd2cb2 merge: v0.7.2 — the whole UI now scales correctly at 125% / 150% display scaling
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MKF7C75bhA4ArQhyMpnsxU
2026-07-16 13:46:42 +08:00
etwenandClaude Opus 4.8 3e9e375d49 docs: add v0.7.2 release notes (and bump version to 0.7.2)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MKF7C75bhA4ArQhyMpnsxU
2026-07-16 13:46:29 +08:00
etwenandClaude Opus 4.8 46cb7a5662 fix(ui): Scale every hardcoded pixel size to the actual display DPI
At Windows display scaling above 100% (125%/150%, common on laptops), fonts
grew with the scale but hardcoded pixel sizes did not: the Log/Script/Stop
buttons on each session were truncated, the AI Chat toolbar button lost its
bottom edge at 150%, and absolute-coordinate dialogs (Quick Connect / New
Connection) could clip or overlap their fields.

Root cause: MainForm's AutoScaleMode.Dpi scales only the controls that exist
when the form initializes. Almost all of this app's UI is created at runtime
(SessionPage on open, views built in constructors after InitializeComponent),
so nothing was ever scaled — verified at 125%: the 250px sidebar stayed 250px.

Fix, in order of preference:
- New Theme.Dpi(Control, px) extension (px * DeviceDpi/96, same approach as
  the existing ActivityRail.ItemSize) applied to every hardcoded dimension:
  sidebar, toolbars, tab strips, Settings inputs and grids, PDU status grid,
  AI chat pane, Ctrl+F search bar, DarkScrollBar width.
- Buttons whose text changes at runtime (Log <-> Logging, Log All <->
  Logging All) now size from measured text width (SessionPage.BarButtonWidth)
  or plain AutoSize, so they can never truncate at any scale.
- Multi-line instruction labels switch to AutoSize instead of fixed frames.
- Absolute-coordinate dialogs get DarkDialog.ApplyDpiScale(): one recursive
  Control.Scale(DeviceDpi/96) at the end of each constructor.
- MainForm moves to AutoScaleMode.None with ClientSize/MinimumSize scaled
  manually — we own scaling explicitly, so a future .NET change to auto-scale
  semantics cannot double-scale us.

At 100% the conversion is the identity, so existing setups are unchanged.
Known limitation: changing the system scale while running needs an app
restart (no WM_DPICHANGED re-layout).

Also rides along in AboutView: the v0.7.2 changelog entry.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MKF7C75bhA4ArQhyMpnsxU
2026-07-16 13:46:29 +08:00
17 changed files with 183 additions and 100 deletions
+2
View File
@@ -10,6 +10,8 @@ ETTerms 是一個 **C# .NET 8 WinForms** 的原生 Windows 終端機工作台,
**進度:** Phase 15 ✅、Phase 6 ✅(TTL 引擎 + Group 同步,SSH 待驗收)、Phase 7 ✅(Settings/About)、Phase 8 ✅(PDU + Shell/ConPTY + SFTP + Settings 擴充)、Phase 9 ✅(Serial MCP server**GUI 持有 COM portMCP 經本機 named pipe 橋接**,AI 收發的資料即時以 `[AI]` 標色顯示在 GUI)、**Phase 10 ✅(v0.6.0)內建 AI Assistant**(見下)。打包待指示。 **進度:** Phase 15 ✅、Phase 6 ✅(TTL 引擎 + Group 同步,SSH 待驗收)、Phase 7 ✅(Settings/About)、Phase 8 ✅(PDU + Shell/ConPTY + SFTP + Settings 擴充)、Phase 9 ✅(Serial MCP server**GUI 持有 COM portMCP 經本機 named pipe 橋接**,AI 收發的資料即時以 `[AI]` 標色顯示在 GUI)、**Phase 10 ✅(v0.6.0)內建 AI Assistant**(見下)。打包待指示。
**v0.7.2** **高 DPI125%/150%)全面修正** —— 字型以點數指定會隨 DPI 放大、寫死像素不會,導致 user 機器(筆電常見 125%/150%)上 SessionPage 的 Log/Script/Stop 被截字、150% 時工具列 AI Chat 被裁、Quick Connect 對話框疊字。**根因**`MainForm``AutoScaleMode.Dpi` 只在 Form 初始化當下對「已存在」的控制項縮放一次,本專案 UI 幾乎全是執行期才 new 的(SessionPage 開分頁才建),完全吃不到(實測 125% 下 sidebar 250px 原封不動)。**作法**(1) `Theme.cs``Dpi()` 擴充(`px × DeviceDpi/96`,同 `ActivityRail.ItemSize` 的既有做法),全 app ~70 處裸像素尺寸(sidebar/工具列/tab 列/Settings 輸入框/表格/Ctrl+F 搜尋列/DarkScrollBar 寬)全部包上;(2) 會換字的按鈕(⏺ Log↔Logging、⏺ Log All↔Logging All)改 `TextRenderer.MeasureText` 量最長字串取寬(`SessionPage.BarButtonWidth`)/AutoSize(3) 多行說明 Label 改 `AutoSize = true` 不再固定框;(4) 絕對座標對話框(`Dialogs/`)在 `DarkDialog` 基底加 `ApplyDpiScale()`(建構子最後 `Control.Scale(DeviceDpi/96f)` 一次縮放全樹);(5) `MainForm``AutoScaleMode.None` + `ClientSize/MinimumSize` 手動 `Dpi()`(明確自己管,避免未來 .NET 行為改變造成雙重縮放)。⚠️ **新增 UI 的鐵則:任何寫死像素尺寸都要 `this.Dpi(n)`100% 看起來正常不代表對(`Dpi()` 在 96 DPI 是恆等)**。已知限制:執行中改系統縮放需重啟。通用規範已沉淀到 `create-architecture` skill 的「WinApp DPI Scaling Pattern」。
**v0.7.1** 新增 TTL 指令 **`sendlnretry '文字' '確認關鍵字' [最多送出次數]`**`TTLInterpreter.SendLnRetry` + `WaitConfirm`)——送出後等確認關鍵字,沒等到就**重送**;次數省略 = 無限重送,命中 `result=1`、用完次數 `result=0` 且**繼續執行**(不中止腳本)。**動機**:裝置開機、console 剛被 shell 接手的瞬間會丟棄輸入(tty 重開 / termios flush),`sendln` 送出的整行無聲消失(裝置不回顯、不執行),後面的 `wait` 就永遠卡住;這是機率性的,`pause` 只能調機率、無法根治(實測 45 輪 power-cycle 中 3 輪中招,且**中招時間點與成功時完全重疊**)。**為何需要新指令而非用 `wait` 重試**:單字串 `wait` 逾時是 throw 中止腳本(刻意設計,見下方「注意事項」),TeraTerm 的 `wait``if result = 0 then goto retry` 重送寫法在 ETTerms 寫不出來。**語意**:每次送出前清空 `_recv`(確保比對到的是這次送出的回應);命中只消費到**第一次**出現處,後續輸出留給接下來的 `wait`;刻意**不套 `SettleAndConsume` 的 settle**(關鍵字出現本身就證明裝置收到了);每次等待逾時 `timeout`/`mtimeout` 有設就用設定值、**沒設預設 3 秒**(此處 0=無限等於永不重送,故不沿用 `wait` 的 0=無限)。⚠️ 確認關鍵字要挑「命令真的有跑」的輸出,**不要挑指令回顯**——tty echo 由核心產生,不保證命令被 shell 讀走。文件:[docs/ttl-script-reference.md](docs/ttl-script-reference.md#送出並確認sendlnretry)。 **v0.7.1** 新增 TTL 指令 **`sendlnretry '文字' '確認關鍵字' [最多送出次數]`**`TTLInterpreter.SendLnRetry` + `WaitConfirm`)——送出後等確認關鍵字,沒等到就**重送**;次數省略 = 無限重送,命中 `result=1`、用完次數 `result=0` 且**繼續執行**(不中止腳本)。**動機**:裝置開機、console 剛被 shell 接手的瞬間會丟棄輸入(tty 重開 / termios flush),`sendln` 送出的整行無聲消失(裝置不回顯、不執行),後面的 `wait` 就永遠卡住;這是機率性的,`pause` 只能調機率、無法根治(實測 45 輪 power-cycle 中 3 輪中招,且**中招時間點與成功時完全重疊**)。**為何需要新指令而非用 `wait` 重試**:單字串 `wait` 逾時是 throw 中止腳本(刻意設計,見下方「注意事項」),TeraTerm 的 `wait``if result = 0 then goto retry` 重送寫法在 ETTerms 寫不出來。**語意**:每次送出前清空 `_recv`(確保比對到的是這次送出的回應);命中只消費到**第一次**出現處,後續輸出留給接下來的 `wait`;刻意**不套 `SettleAndConsume` 的 settle**(關鍵字出現本身就證明裝置收到了);每次等待逾時 `timeout`/`mtimeout` 有設就用設定值、**沒設預設 3 秒**(此處 0=無限等於永不重送,故不沿用 `wait` 的 0=無限)。⚠️ 確認關鍵字要挑「命令真的有跑」的輸出,**不要挑指令回顯**——tty echo 由核心產生,不保證命令被 shell 讀走。文件:[docs/ttl-script-reference.md](docs/ttl-script-reference.md#送出並確認sendlnretry)。
**v0.7.0** **AI Chat 改用 WebView2 渲染**(真氣泡 + Markdown + thinking 動畫泡)。訊息區從 RichTextBox 換成 `WebView2`user 右泡 / AI 左泡、AI 回覆走 **Markdig** markdown→HTML(程式碼區塊/表格/清單)、送出後顯示會動的 thinking「…」泡(`AgentHost.Status "thinking"``showThinking()`,收到 `AssistantText``hideThinking()` 再加 AI 泡)。HTML 模板全內嵌於 `Ai/ChatHtml.cs``NavigateToString`,無外部依賴),C# 經 `ExecuteScriptAsync` 呼叫 JS 函式;WebView2 async 初始化,就緒前的呼叫先入 `_pending` 佇列、`NavigationCompleted` 後 flush。使用者資料夾 `%LocalAppData%\ETTerms\WebView2`(避開 Program Files 唯讀)。底部控制列(輸入框/模型下拉/Send)仍 WinForms。新增 NuGet `Microsoft.Web.WebView2` + `Markdig`;依賴 WebView2 RuntimeWin11 內建,缺時 hint 顯示錯誤)。⚠️ **publish 要確認 WebView2 native`runtimes/win-x64/native/WebView2Loader.dll`)有進產物**。**工具呼叫上限可設定**`AppSettings.AiMaxToolRounds`,Settings → AI Assistant,`AgentHost` 建構子傳入):**0 = 無上限**(自動化長跑;每輪燒 token,聊天視窗 Send 鈕在執行中變 **Stop**,經 `CancellationToken` 中止),預設 30。 **v0.7.0** **AI Chat 改用 WebView2 渲染**(真氣泡 + Markdown + thinking 動畫泡)。訊息區從 RichTextBox 換成 `WebView2`user 右泡 / AI 左泡、AI 回覆走 **Markdig** markdown→HTML(程式碼區塊/表格/清單)、送出後顯示會動的 thinking「…」泡(`AgentHost.Status "thinking"``showThinking()`,收到 `AssistantText``hideThinking()` 再加 AI 泡)。HTML 模板全內嵌於 `Ai/ChatHtml.cs``NavigateToString`,無外部依賴),C# 經 `ExecuteScriptAsync` 呼叫 JS 函式;WebView2 async 初始化,就緒前的呼叫先入 `_pending` 佇列、`NavigationCompleted` 後 flush。使用者資料夾 `%LocalAppData%\ETTerms\WebView2`(避開 Program Files 唯讀)。底部控制列(輸入框/模型下拉/Send)仍 WinForms。新增 NuGet `Microsoft.Web.WebView2` + `Markdig`;依賴 WebView2 RuntimeWin11 內建,缺時 hint 顯示錯誤)。⚠️ **publish 要確認 WebView2 native`runtimes/win-x64/native/WebView2Loader.dll`)有進產物**。**工具呼叫上限可設定**`AppSettings.AiMaxToolRounds`,Settings → AI Assistant,`AgentHost` 建構子傳入):**0 = 無上限**(自動化長跑;每輪燒 token,聊天視窗 Send 鈕在執行中變 **Stop**,經 `CancellationToken` 中止),預設 30。
+39
View File
@@ -0,0 +1,39 @@
# v0.7.2 — The whole UI now scales correctly at 125% / 150% display scaling
## 🐛 Bug fixes
**Buttons and panels no longer get clipped at Windows display scaling above 100%**
* Fixed: on machines set to 125% or 150% scaling (most laptops), text grew with the scale but
many controls didn't — the **Log / Script / Stop** buttons on each session tab were truncated
or hidden, the **✨ AI Chat** toolbar button lost its bottom edge at 150%, and dialogs like
**Quick Connect** could clip or overlap their fields.
* Every fixed pixel size in the app now converts to your actual display scale, the same way
text always did: the sidebar, toolbars, tab strips, all Settings inputs and tables, the PDU
status grid, the AI chat pane, dialogs, the Ctrl+F search bar — even the scrollbar width.
* Buttons that change their text (like **⏺ Log → ⏺ Logging**) now size themselves from the
actual rendered text width, so they can never truncate again at any scale.
* At 100% scaling nothing changes — the conversion is exact there, so existing setups look
identical to before.
**Known limitation**
* If you change the Windows display scaling while ETTerms is running, restart the app to
re-layout at the new scale.
## 📦 Downloads
Two builds are produced:
| Build | Needs .NET 8 Desktop Runtime? | Notes |
|-------|-------------------------------|-------|
| **Standard** (`ETTerms_v0.7.2`) | ✅ Yes | Smaller; for machines that already have the runtime |
| **Portable** (`ETTerms_v0.7.2_portable`) | ❌ No | Runtime bundled — unzip and run, no install / admin needed |
Run `ETTerms v0.7.2.exe`.
## 🔗 Links
* TTL reference:
[docs/ttl-script-reference.md](https://github.com/ETWen/ETTerms/blob/main/docs/ttl-script-reference.md)
* Architecture:
[ARCHITECTURE.md](https://github.com/ETWen/ETTerms/blob/main/ARCHITECTURE.md)
**Full changelog:** [v0.7.1...v0.7.2](https://github.com/ETWen/ETTerms/compare/v0.7.1...v0.7.2)
+13 -5
View File
@@ -20,7 +20,7 @@ public sealed class AboutView : UserControl
// ── Main layout: left fixed + right scrollable ── // ── Main layout: left fixed + right scrollable ──
var left = new FlowLayoutPanel var left = new FlowLayoutPanel
{ {
Dock = DockStyle.Left, Width = 380, FlowDirection = FlowDirection.TopDown, Dock = DockStyle.Left, Width = this.Dpi(380), FlowDirection = FlowDirection.TopDown,
WrapContents = false, AutoScroll = true, Padding = new Padding(20), WrapContents = false, AutoScroll = true, Padding = new Padding(20),
BackColor = Theme.WorkspaceBack BackColor = Theme.WorkspaceBack
}; };
@@ -105,10 +105,10 @@ public sealed class AboutView : UserControl
{ {
FlowDirection = FlowDirection.TopDown, WrapContents = false, FlowDirection = FlowDirection.TopDown, WrapContents = false,
AutoSize = true, AutoSizeMode = AutoSizeMode.GrowAndShrink, AutoSize = true, AutoSizeMode = AutoSizeMode.GrowAndShrink,
MinimumSize = new Size(width, height),
Margin = new Padding(0, 0, 0, 12), Margin = new Padding(0, 0, 0, 12),
BackColor = Theme.TabBack, Padding = new Padding(12) BackColor = Theme.TabBack, Padding = new Padding(12)
}; };
card.MinimumSize = new Size(card.Dpi(width), card.Dpi(height));
build(card); build(card);
return card; return card;
} }
@@ -118,9 +118,10 @@ public sealed class AboutView : UserControl
{ {
var card = new Panel var card = new Panel
{ {
Width = 340, Height = 300, Margin = new Padding(0, 0, 0, 12), Margin = new Padding(0, 0, 0, 12),
BackColor = Theme.TabBack, Padding = new Padding(12) BackColor = Theme.TabBack, Padding = new Padding(12)
}; };
card.Size = new Size(card.Dpi(340), card.Dpi(300));
var flow = new FlowLayoutPanel var flow = new FlowLayoutPanel
{ {
Dock = DockStyle.Fill, FlowDirection = FlowDirection.TopDown, Dock = DockStyle.Fill, FlowDirection = FlowDirection.TopDown,
@@ -133,7 +134,7 @@ public sealed class AboutView : UserControl
{ {
flow.Controls.Add(new PictureBox flow.Controls.Add(new PictureBox
{ {
Width = 312, Height = 268, Margin = new Padding(0, 4, 0, 4), Width = card.Dpi(312), Height = card.Dpi(268), Margin = new Padding(0, 4, 0, 4),
SizeMode = PictureBoxSizeMode.Zoom, SizeMode = PictureBoxSizeMode.Zoom,
BackColor = Theme.TabBack, Image = iconImg.ToBitmap() BackColor = Theme.TabBack, Image = iconImg.ToBitmap()
}); });
@@ -164,7 +165,7 @@ public sealed class AboutView : UserControl
}; };
row.Controls.Add(new Label row.Controls.Add(new Label
{ {
Text = label, AutoSize = true, MinimumSize = new Size(70, 0), Text = label, AutoSize = true, MinimumSize = new Size(row.Dpi(70), 0),
ForeColor = Theme.TextDim, Font = Theme.UiFont, ForeColor = Theme.TextDim, Font = Theme.UiFont,
TextAlign = ContentAlignment.MiddleLeft, Margin = new Padding(0, 0, 8, 0) TextAlign = ContentAlignment.MiddleLeft, Margin = new Padding(0, 0, 8, 0)
}); });
@@ -183,6 +184,13 @@ public sealed class AboutView : UserControl
private static readonly ChangelogEntry[] Changelog = private static readonly ChangelogEntry[] Changelog =
[ [
new("0.7.2", new DateOnly(2026, 7, 16), "The whole UI now scales correctly at 125% / 150% display scaling",
[
"Fixed: at Windows display scaling above 100%, text grew with the scale but many buttons and panels didn't — the Log / Script / Stop buttons on each session got clipped, the AI Chat toolbar button lost its bottom edge at 150%, and dialogs like Quick Connect could overlap their fields.",
"Every fixed pixel size in the app — sidebars, toolbars, tab strips, settings inputs, tables, dialogs, the Ctrl+F search bar, even the scrollbar width — now converts to your actual display scale, the same way a button's text does.",
"Buttons that show changing text (like ⏺ Log / ⏺ Logging) now size themselves from the actual rendered text width, so they can never truncate again at any scale.",
"Note: if you change the Windows scaling while ETTerms is running, restart the app to re-layout at the new scale.",
]),
new("0.7.1", new DateOnly(2026, 7, 16), "Scripts no longer hang when a booting device swallows a command", new("0.7.1", new DateOnly(2026, 7, 16), "Scripts no longer hang when a booting device swallows a command",
[ [
"Fixed: a device that is still booting can silently throw away what you send it, the moment its shell takes over the console — the line never echoes and never runs, so the wait after it sat there forever and your test rig was dead until someone noticed. Adding a pause before the send only made it rarer: in one overnight 45-cycle power-cycle run, 3 cycles still hung this way.", "Fixed: a device that is still booting can silently throw away what you send it, the moment its shell takes over the console — the line never echoes and never runs, so the wait after it sat there forever and your test rig was dead until someone noticed. Adding a pause before the send only made it rarer: in one overnight 45-cycle power-cycle run, 3 cycles still hung this way.",
+6 -6
View File
@@ -48,7 +48,7 @@ public sealed class AiChatView : UserControl
_web = new WebView2 { Dock = DockStyle.Fill }; _web = new WebView2 { Dock = DockStyle.Fill };
// ── 底部控制列:輸入框(Fill) + 右下角欄(模型下拉在上、Send 在下) ── // ── 底部控制列:輸入框(Fill) + 右下角欄(模型下拉在上、Send 在下) ──
var bottom = new Panel { Dock = DockStyle.Bottom, Height = 96, BackColor = Theme.RailBack, Padding = new Padding(10, 8, 10, 8) }; var bottom = new Panel { Dock = DockStyle.Bottom, Height = this.Dpi(96), BackColor = Theme.RailBack, Padding = new Padding(10, 8, 10, 8) };
_input = new TextBox _input = new TextBox
{ {
@@ -60,9 +60,9 @@ public sealed class AiChatView : UserControl
if (e.KeyCode == Keys.Enter && !e.Shift && !_running) { e.Handled = e.SuppressKeyPress = true; OnSend(); } if (e.KeyCode == Keys.Enter && !e.Shift && !_running) { e.Handled = e.SuppressKeyPress = true; OnSend(); }
}; };
var rightCol = new Panel { Dock = DockStyle.Right, Width = 178, BackColor = Theme.RailBack, Padding = new Padding(8, 0, 0, 0) }; var rightCol = new Panel { Dock = DockStyle.Right, Width = this.Dpi(178), BackColor = Theme.RailBack, Padding = new Padding(8, 0, 0, 0) };
var modelWrap = new Panel { Dock = DockStyle.Top, Height = 24, BackColor = Theme.RailBack }; var modelWrap = new Panel { Dock = DockStyle.Top, Height = this.Dpi(26), BackColor = Theme.RailBack };
_modelBox = new ComboBox _modelBox = new ComboBox
{ {
Dock = DockStyle.Fill, DropDownStyle = ComboBoxStyle.DropDownList, Dock = DockStyle.Fill, DropDownStyle = ComboBoxStyle.DropDownList,
@@ -78,7 +78,7 @@ public sealed class AiChatView : UserControl
}; };
var refreshBtn = new Button var refreshBtn = new Button
{ {
Text = "↻", Dock = DockStyle.Right, Width = 24, FlatStyle = FlatStyle.Flat, Text = "↻", Dock = DockStyle.Right, Width = this.Dpi(24), FlatStyle = FlatStyle.Flat,
ForeColor = Theme.TextDim, BackColor = Theme.TabBack, Font = Theme.UiFont, Cursor = Cursors.Hand ForeColor = Theme.TextDim, BackColor = Theme.TabBack, Font = Theme.UiFont, Cursor = Cursors.Hand
}; };
refreshBtn.FlatAppearance.BorderColor = Theme.Border; refreshBtn.FlatAppearance.BorderColor = Theme.Border;
@@ -86,7 +86,7 @@ public sealed class AiChatView : UserControl
modelWrap.Controls.Add(_modelBox); // Fill modelWrap.Controls.Add(_modelBox); // Fill
modelWrap.Controls.Add(refreshBtn); // Right modelWrap.Controls.Add(refreshBtn); // Right
var gap = new Panel { Dock = DockStyle.Top, Height = 6, BackColor = Theme.RailBack }; var gap = new Panel { Dock = DockStyle.Top, Height = this.Dpi(6), BackColor = Theme.RailBack };
_send = new Button _send = new Button
{ {
@@ -105,7 +105,7 @@ public sealed class AiChatView : UserControl
_hint = new Label _hint = new Label
{ {
Dock = DockStyle.Top, Height = 40, BackColor = Color.FromArgb(60, 50, 30), ForeColor = Theme.Text, Dock = DockStyle.Top, Height = this.Dpi(40), BackColor = Color.FromArgb(60, 50, 30), ForeColor = Theme.Text,
Font = Theme.UiFont, TextAlign = ContentAlignment.MiddleCenter, Visible = false, Font = Theme.UiFont, TextAlign = ContentAlignment.MiddleCenter, Visible = false,
Text = "尚未設定 AI Provider — 到 Settings → AI Assistant 填入 Base URL 與 API Key。" Text = "尚未設定 AI Provider — 到 Settings → AI Assistant 填入 Base URL 與 API Key。"
}; };
+20 -20
View File
@@ -44,7 +44,7 @@ public sealed class ConnectionSidebar : UserControl
public ConnectionSidebar() public ConnectionSidebar()
{ {
Width = 250; Width = this.Dpi(250);
Dock = DockStyle.Left; Dock = DockStyle.Left;
BackColor = Theme.SidebarBack; BackColor = Theme.SidebarBack;
@@ -52,7 +52,7 @@ public sealed class ConnectionSidebar : UserControl
// 高度 40 與右側 Workspace 工具列對齊,讓「CONNECTIONS / 連線清單」與右側分頁列同一條基準線 // 高度 40 與右側 Workspace 工具列對齊,讓「CONNECTIONS / 連線清單」與右側分頁列同一條基準線
var tabBar = new FlowLayoutPanel var tabBar = new FlowLayoutPanel
{ {
Dock = DockStyle.Top, Height = 40, BackColor = Theme.RailBack, Dock = DockStyle.Top, Height = this.Dpi(40), BackColor = Theme.RailBack,
Padding = new Padding(4, 7, 4, 0), WrapContents = false Padding = new Padding(4, 7, 4, 0), WrapContents = false
}; };
@@ -65,7 +65,7 @@ public sealed class ConnectionSidebar : UserControl
var b = new Button var b = new Button
{ {
Text = text, AutoSize = true, AutoSizeMode = AutoSizeMode.GrowAndShrink, Text = text, AutoSize = true, AutoSizeMode = AutoSizeMode.GrowAndShrink,
MinimumSize = new Size(70, 24), Padding = new Padding(8, 2, 8, 2), MinimumSize = new Size(this.Dpi(70), this.Dpi(24)), Padding = new Padding(8, 2, 8, 2),
FlatStyle = FlatStyle.Flat, FlatStyle = FlatStyle.Flat,
ForeColor = Theme.Text, BackColor = Theme.TabBack, Font = Theme.UiFont, ForeColor = Theme.Text, BackColor = Theme.TabBack, Font = Theme.UiFont,
Margin = new Padding(0, 0, 4, 0), Cursor = Cursors.Hand Margin = new Padding(0, 0, 4, 0), Cursor = Cursors.Hand
@@ -90,7 +90,7 @@ public sealed class ConnectionSidebar : UserControl
tabBar.Controls.Add(sftpBtn); tabBar.Controls.Add(sftpBtn);
// ── Sessions panel content ── // ── Sessions panel content ──
var header = new Panel { Dock = DockStyle.Top, Height = 34, BackColor = Theme.SidebarBack }; var header = new Panel { Dock = DockStyle.Top, Height = this.Dpi(34), BackColor = Theme.SidebarBack };
var title = new Label var title = new Label
{ {
Text = "CONNECTIONS", Text = "CONNECTIONS",
@@ -108,7 +108,7 @@ public sealed class ConnectionSidebar : UserControl
header.Controls.Add(btnNewFolder); header.Controls.Add(btnNewFolder);
header.Controls.Add(btnNewConn); header.Controls.Add(btnNewConn);
var searchHost = new Panel { Dock = DockStyle.Top, Height = 32, BackColor = Theme.SidebarBack, Padding = new Padding(8, 2, 8, 4) }; var searchHost = new Panel { Dock = DockStyle.Top, Height = this.Dpi(32), BackColor = Theme.SidebarBack, Padding = new Padding(8, 2, 8, 4) };
_search = new TextBox _search = new TextBox
{ {
Dock = DockStyle.Fill, Dock = DockStyle.Fill,
@@ -125,7 +125,7 @@ public sealed class ConnectionSidebar : UserControl
{ {
Text = "▷ Quick Connect", Text = "▷ Quick Connect",
Dock = DockStyle.Top, Dock = DockStyle.Top,
Height = 34, Height = this.Dpi(34),
FlatStyle = FlatStyle.Flat, FlatStyle = FlatStyle.Flat,
ForeColor = Color.White, ForeColor = Color.White,
BackColor = Theme.Accent, BackColor = Theme.Accent,
@@ -136,22 +136,22 @@ public sealed class ConnectionSidebar : UserControl
quick.FlatAppearance.BorderSize = 0; quick.FlatAppearance.BorderSize = 0;
quick.FlatAppearance.MouseOverBackColor = Theme.AccentDim; quick.FlatAppearance.MouseOverBackColor = Theme.AccentDim;
quick.Click += (_, _) => QuickConnect(); quick.Click += (_, _) => QuickConnect();
var quickHost = new Panel { Dock = DockStyle.Top, Height = 42, BackColor = Theme.SidebarBack, Padding = new Padding(8, 4, 8, 4) }; var quickHost = new Panel { Dock = DockStyle.Top, Height = this.Dpi(42), BackColor = Theme.SidebarBack, Padding = new Padding(8, 4, 8, 4) };
quickHost.Controls.Add(quick); quickHost.Controls.Add(quick);
var shellBtn = new Button var shellBtn = new Button
{ {
Text = "🖥 Local Shell", Dock = DockStyle.Top, Height = 30, Text = "🖥 Local Shell", Dock = DockStyle.Top, Height = this.Dpi(30),
FlatStyle = FlatStyle.Flat, ForeColor = Theme.Text, BackColor = Theme.TabBack, FlatStyle = FlatStyle.Flat, ForeColor = Theme.Text, BackColor = Theme.TabBack,
Font = Theme.UiFont, Cursor = Cursors.Hand Font = Theme.UiFont, Cursor = Cursors.Hand
}; };
shellBtn.FlatAppearance.BorderColor = Theme.Border; shellBtn.FlatAppearance.BorderColor = Theme.Border;
shellBtn.FlatAppearance.MouseOverBackColor = Theme.Hover; shellBtn.FlatAppearance.MouseOverBackColor = Theme.Hover;
shellBtn.Click += (_, _) => OpenLocalShell(); shellBtn.Click += (_, _) => OpenLocalShell();
var shellHost = new Panel { Dock = DockStyle.Top, Height = 36, BackColor = Theme.SidebarBack, Padding = new Padding(8, 2, 8, 4) }; var shellHost = new Panel { Dock = DockStyle.Top, Height = this.Dpi(36), BackColor = Theme.SidebarBack, Padding = new Padding(8, 2, 8, 4) };
shellHost.Controls.Add(shellBtn); shellHost.Controls.Add(shellBtn);
var toolbar = new Panel { Dock = DockStyle.Top, Height = 28, BackColor = Theme.SidebarBack }; var toolbar = new Panel { Dock = DockStyle.Top, Height = this.Dpi(28), BackColor = Theme.SidebarBack };
var btnExpand = IconButton("⊞", "Expand All", (_, _) => SetAllExpanded(true)); var btnExpand = IconButton("⊞", "Expand All", (_, _) => SetAllExpanded(true));
var btnCollapse = IconButton("⊟", "Collapse All", (_, _) => SetAllExpanded(false)); var btnCollapse = IconButton("⊟", "Collapse All", (_, _) => SetAllExpanded(false));
btnExpand.Dock = DockStyle.Right; btnExpand.Dock = DockStyle.Right;
@@ -171,8 +171,8 @@ public sealed class ConnectionSidebar : UserControl
ShowRootLines = true, ShowRootLines = true,
ShowPlusMinus = true, ShowPlusMinus = true,
FullRowSelect = true, FullRowSelect = true,
ItemHeight = 26, ItemHeight = this.Dpi(26),
Indent = 18, Indent = this.Dpi(18),
AllowDrop = true AllowDrop = true
}; };
_tree.NodeMouseDoubleClick += OnNodeDoubleClick; _tree.NodeMouseDoubleClick += OnNodeDoubleClick;
@@ -213,15 +213,15 @@ public sealed class ConnectionSidebar : UserControl
// Connection row // Connection row
var connRow = new FlowLayoutPanel var connRow = new FlowLayoutPanel
{ {
Dock = DockStyle.Top, Height = 30, BackColor = Theme.SidebarBack, Dock = DockStyle.Top, Height = this.Dpi(30), BackColor = Theme.SidebarBack,
Padding = new Padding(4, 4, 4, 0), WrapContents = false Padding = new Padding(4, 4, 4, 0), WrapContents = false
}; };
var sshLabel = new Label { Text = "SSH:", AutoSize = true, ForeColor = Theme.TextDim, Font = Theme.UiFont, Margin = new Padding(0, 4, 4, 0) }; var sshLabel = new Label { Text = "SSH:", AutoSize = true, ForeColor = Theme.TextDim, Font = Theme.UiFont, Margin = new Padding(0, 4, 4, 0) };
var sshCombo = new ComboBox { Width = 120, DropDownStyle = ComboBoxStyle.DropDownList, BackColor = Theme.TabBack, ForeColor = Theme.Text, Font = Theme.UiFont }; var sshCombo = new ComboBox { Width = this.Dpi(120), DropDownStyle = ComboBoxStyle.DropDownList, BackColor = Theme.TabBack, ForeColor = Theme.Text, Font = Theme.UiFont };
var connectBtn = new Button var connectBtn = new Button
{ {
Text = "Connect", AutoSize = true, AutoSizeMode = AutoSizeMode.GrowAndShrink, Text = "Connect", AutoSize = true, AutoSizeMode = AutoSizeMode.GrowAndShrink,
MinimumSize = new Size(64, 24), Padding = new Padding(8, 2, 8, 2), MinimumSize = new Size(this.Dpi(64), this.Dpi(24)), Padding = new Padding(8, 2, 8, 2),
FlatStyle = FlatStyle.Flat, FlatStyle = FlatStyle.Flat,
ForeColor = Theme.Text, BackColor = Theme.TabBack, Font = Theme.UiFont, Cursor = Cursors.Hand, Margin = new Padding(4, 0, 0, 0) ForeColor = Theme.Text, BackColor = Theme.TabBack, Font = Theme.UiFont, Cursor = Cursors.Hand, Margin = new Padding(4, 0, 0, 0)
}; };
@@ -233,7 +233,7 @@ public sealed class ConnectionSidebar : UserControl
// Path bar // Path bar
_sftpPath = new TextBox _sftpPath = new TextBox
{ {
Dock = DockStyle.Top, Height = 24, Text = "/", Dock = DockStyle.Top, Height = this.Dpi(24), Text = "/",
BackColor = Theme.TabBack, ForeColor = Theme.Accent, Font = Theme.UiFont, BorderStyle = BorderStyle.FixedSingle BackColor = Theme.TabBack, ForeColor = Theme.Accent, Font = Theme.UiFont, BorderStyle = BorderStyle.FixedSingle
}; };
_sftpPath.KeyDown += (_, e) => { if (e.KeyCode == Keys.Enter && _sftp?.IsConnected == true) SftpNavigate(_sftpPath.Text); }; _sftpPath.KeyDown += (_, e) => { if (e.KeyCode == Keys.Enter && _sftp?.IsConnected == true) SftpNavigate(_sftpPath.Text); };
@@ -245,8 +245,8 @@ public sealed class ConnectionSidebar : UserControl
BackColor = Theme.SidebarBack, ForeColor = Theme.Text, Font = Theme.UiFont, BackColor = Theme.SidebarBack, ForeColor = Theme.Text, Font = Theme.UiFont,
BorderStyle = BorderStyle.None, HeaderStyle = ColumnHeaderStyle.Nonclickable BorderStyle = BorderStyle.None, HeaderStyle = ColumnHeaderStyle.Nonclickable
}; };
_sftpList.Columns.Add("Name", 150); _sftpList.Columns.Add("Name", this.Dpi(150));
_sftpList.Columns.Add("Size", 60, HorizontalAlignment.Right); _sftpList.Columns.Add("Size", this.Dpi(60), HorizontalAlignment.Right);
_sftpList.DoubleClick += (_, _) => _sftpList.DoubleClick += (_, _) =>
{ {
if (_sftpList.SelectedItems.Count == 0 || _sftp?.IsConnected != true) return; if (_sftpList.SelectedItems.Count == 0 || _sftp?.IsConnected != true) return;
@@ -651,8 +651,8 @@ public sealed class ConnectionSidebar : UserControl
var b = new Button var b = new Button
{ {
Text = glyph, Text = glyph,
Width = 34, Width = this.Dpi(34),
Height = 34, Height = this.Dpi(34),
FlatStyle = FlatStyle.Flat, FlatStyle = FlatStyle.Flat,
ForeColor = Theme.TextDim, ForeColor = Theme.TextDim,
BackColor = Theme.SidebarBack, BackColor = Theme.SidebarBack,
@@ -59,6 +59,7 @@ public sealed class ConnectionEditDialog : DarkDialog
_type.SelectedIndex = (existing?.Type ?? ConnectionType.Ssh) == ConnectionType.Ssh ? 0 : 1; _type.SelectedIndex = (existing?.Type ?? ConnectionType.Ssh) == ConnectionType.Ssh ? 0 : 1;
ToggleType(); ToggleType();
ApplyDpiScale();
} }
private void ToggleType() private void ToggleType()
+12
View File
@@ -12,6 +12,7 @@ public abstract class DarkDialog : Form
BackColor = Theme.SidebarBack; BackColor = Theme.SidebarBack;
ForeColor = Theme.Text; ForeColor = Theme.Text;
Font = Theme.UiFont; Font = Theme.UiFont;
AutoScaleMode = AutoScaleMode.None; // 縮放由 ApplyDpiScale() 統一處理,避免雙重縮放
FormBorderStyle = FormBorderStyle.FixedDialog; FormBorderStyle = FormBorderStyle.FixedDialog;
StartPosition = FormStartPosition.CenterParent; StartPosition = FormStartPosition.CenterParent;
MaximizeBox = false; MaximizeBox = false;
@@ -19,6 +20,17 @@ public abstract class DarkDialog : Form
ShowInTaskbar = false; ShowInTaskbar = false;
} }
/// <summary>
/// 依目前 DPI 一次縮放整個對話框(含所有子控制項的絕對座標),
/// 於衍生類別建構子「最後」呼叫。對話框以 96-DPI 座標排版,
/// 字型是點數會自己隨 DPI 放大,座標不縮放在 125%/150% 就會裁切、疊字。
/// </summary>
protected void ApplyDpiScale()
{
float f = DeviceDpi / 96f;
if (Math.Abs(f - 1f) > 0.01f) Scale(new SizeF(f, f));
}
protected override void OnHandleCreated(EventArgs e) protected override void OnHandleCreated(EventArgs e)
{ {
base.OnHandleCreated(e); base.OnHandleCreated(e);
@@ -30,6 +30,7 @@ public sealed class TextPromptDialog : DarkDialog
Controls.AddRange(new Control[] { lbl, _input, ok, cancel }); Controls.AddRange(new Control[] { lbl, _input, ok, cancel });
AcceptButton = ok; AcceptButton = ok;
CancelButton = cancel; CancelButton = cancel;
ApplyDpiScale();
} }
/// <summary>顯示對話框;回傳輸入值,取消或空白回傳 null。</summary> /// <summary>顯示對話框;回傳輸入值,取消或空白回傳 null。</summary>
+7 -6
View File
@@ -18,12 +18,13 @@ partial class MainForm
{ {
SuspendLayout(); SuspendLayout();
// 用 Dpi 自動縮放:尺寸縮放比例 = 螢幕 DPI 比例,與點數字型的實際渲染比例一致, // DPI 縮放由我們自己管(Theme.Dpi() + AutoSize),不靠 Form 的自動縮放:
// 避免「字放大了、容器沒放大」造成的高 DPI 文字/按鈕被裁切。 // AutoScaleMode.Dpi 只在 Form 初始化當下對「已存在」的控制項做一次,
AutoScaleDimensions = new SizeF(96F, 96F); // 執行期才建立的 SessionPage / 各 view 完全吃不到(實測 125%/150% 皆未縮放),
AutoScaleMode = AutoScaleMode.Dpi; // 留著反而有「哪天開始生效 → 與 Dpi() 雙重縮放」的風險。
ClientSize = new Size(1100, 700); AutoScaleMode = AutoScaleMode.None;
MinimumSize = new Size(720, 480); ClientSize = new Size(this.Dpi(1100), this.Dpi(700));
MinimumSize = new Size(this.Dpi(720), this.Dpi(480));
BackColor = Theme.WorkspaceBack; BackColor = Theme.WorkspaceBack;
ForeColor = Theme.Text; ForeColor = Theme.Text;
Font = Theme.UiFont; Font = Theme.UiFont;
+33 -32
View File
@@ -16,7 +16,7 @@ public sealed class SettingsView : UserControl
// Use a custom tab strip + panel swapping instead of TabControl to avoid white borders // Use a custom tab strip + panel swapping instead of TabControl to avoid white borders
var tabBar = new FlowLayoutPanel var tabBar = new FlowLayoutPanel
{ {
Dock = DockStyle.Top, Height = 32, BackColor = Theme.RailBack, Dock = DockStyle.Top, Height = this.Dpi(32), BackColor = Theme.RailBack,
Padding = new Padding(4, 4, 4, 0), WrapContents = false Padding = new Padding(4, 4, 4, 0), WrapContents = false
}; };
@@ -34,7 +34,7 @@ public sealed class SettingsView : UserControl
var b = new Button var b = new Button
{ {
Text = text, AutoSize = true, AutoSizeMode = AutoSizeMode.GrowAndShrink, Text = text, AutoSize = true, AutoSizeMode = AutoSizeMode.GrowAndShrink,
MinimumSize = new Size(70, 26), Padding = new Padding(10, 2, 10, 2), MinimumSize = new Size(this.Dpi(70), this.Dpi(26)), Padding = new Padding(10, 2, 10, 2),
FlatStyle = FlatStyle.Flat, FlatStyle = FlatStyle.Flat,
ForeColor = Theme.Text, BackColor = Theme.TabBack, Font = Theme.UiFont, ForeColor = Theme.Text, BackColor = Theme.TabBack, Font = Theme.UiFont,
Margin = new Padding(0, 0, 4, 0), Cursor = Cursors.Hand Margin = new Padding(0, 0, 4, 0), Cursor = Cursors.Hand
@@ -76,18 +76,18 @@ public sealed class SettingsView : UserControl
WrapContents = false, BackColor = Theme.WorkspaceBack, AutoScroll = true WrapContents = false, BackColor = Theme.WorkspaceBack, AutoScroll = true
}; };
var font = new ComboBox { Width = 200, DropDownStyle = ComboBoxStyle.DropDown, BackColor = Theme.TabBack, ForeColor = Theme.Text, FlatStyle = FlatStyle.Flat }; var font = new ComboBox { Width = this.Dpi(200), DropDownStyle = ComboBoxStyle.DropDown, BackColor = Theme.TabBack, ForeColor = Theme.Text, FlatStyle = FlatStyle.Flat };
font.Items.AddRange(new object[] { "Cascadia Mono", "Consolas", "Courier New", "Lucida Console", "JetBrains Mono" }); font.Items.AddRange(new object[] { "Cascadia Mono", "Consolas", "Courier New", "Lucida Console", "JetBrains Mono" });
font.Text = s.FontFamily; font.Text = s.FontFamily;
var fontSize = new NumericUpDown { Width = 80, Minimum = 8, Maximum = 24, DecimalPlaces = 1, Increment = 0.5m, Value = (decimal)s.FontSize, BackColor = Theme.TabBack, ForeColor = Theme.Text, BorderStyle = BorderStyle.FixedSingle }; var fontSize = new NumericUpDown { Width = this.Dpi(80), Minimum = 8, Maximum = 24, DecimalPlaces = 1, Increment = 0.5m, Value = (decimal)s.FontSize, BackColor = Theme.TabBack, ForeColor = Theme.Text, BorderStyle = BorderStyle.FixedSingle };
var scrollback = new NumericUpDown { Width = 100, Minimum = 500, Maximum = 50000, Increment = 500, Value = s.ScrollbackLines, BackColor = Theme.TabBack, ForeColor = Theme.Text, BorderStyle = BorderStyle.FixedSingle }; var scrollback = new NumericUpDown { Width = this.Dpi(100), Minimum = 500, Maximum = 50000, Increment = 500, Value = s.ScrollbackLines, BackColor = Theme.TabBack, ForeColor = Theme.Text, BorderStyle = BorderStyle.FixedSingle };
var scheme = new ComboBox { Width = 150, DropDownStyle = ComboBoxStyle.DropDownList, BackColor = Theme.TabBack, ForeColor = Theme.Text, FlatStyle = FlatStyle.Flat }; var scheme = new ComboBox { Width = this.Dpi(150), DropDownStyle = ComboBoxStyle.DropDownList, BackColor = Theme.TabBack, ForeColor = Theme.Text, FlatStyle = FlatStyle.Flat };
scheme.Items.AddRange(new object[] { "Dark", "Solarized Dark", "Monokai" }); scheme.Items.AddRange(new object[] { "Dark", "Solarized Dark", "Monokai" });
scheme.Text = s.ColorScheme; scheme.Text = s.ColorScheme;
var newline = new ComboBox { Width = 120, DropDownStyle = ComboBoxStyle.DropDownList, BackColor = Theme.TabBack, ForeColor = Theme.Text, FlatStyle = FlatStyle.Flat }; var newline = new ComboBox { Width = this.Dpi(120), DropDownStyle = ComboBoxStyle.DropDownList, BackColor = Theme.TabBack, ForeColor = Theme.Text, FlatStyle = FlatStyle.Flat };
newline.Items.AddRange(new object[] { "\\r\\n", "\\r", "\\n" }); newline.Items.AddRange(new object[] { "\\r\\n", "\\r", "\\n" });
newline.Text = s.DefaultNewLine; newline.Text = s.DefaultNewLine;
@@ -109,17 +109,17 @@ public sealed class SettingsView : UserControl
// Shell settings // Shell settings
flow.Controls.Add(new Label { Text = "Shell Settings", AutoSize = true, ForeColor = Theme.Accent, Font = Theme.UiFontBold, Margin = new Padding(0, 0, 0, 4) }); flow.Controls.Add(new Label { Text = "Shell Settings", AutoSize = true, ForeColor = Theme.Accent, Font = Theme.UiFontBold, Margin = new Padding(0, 0, 0, 4) });
var shellType = new ComboBox { Width = 150, DropDownStyle = ComboBoxStyle.DropDownList, BackColor = Theme.TabBack, ForeColor = Theme.Text, FlatStyle = FlatStyle.Flat }; var shellType = new ComboBox { Width = this.Dpi(150), DropDownStyle = ComboBoxStyle.DropDownList, BackColor = Theme.TabBack, ForeColor = Theme.Text, FlatStyle = FlatStyle.Flat };
shellType.Items.AddRange(new object[] { "PowerShell", "Bash", "Cmd" }); shellType.Items.AddRange(new object[] { "PowerShell", "Bash", "Cmd" });
shellType.Text = s.ShellType; shellType.Text = s.ShellType;
flow.Controls.Add(MakeRow("Terminal Shell", shellType)); flow.Controls.Add(MakeRow("Terminal Shell", shellType));
// 用一個帶邊框的容器包住「輸入框 + 瀏覽鈕」,讓兩者看起來像同一個欄位 // 用一個帶邊框的容器包住「輸入框 + 瀏覽鈕」,讓兩者看起來像同一個欄位
var dirPanel = new Panel { Width = InputWidth, Height = 24, BackColor = Theme.TabBack, BorderStyle = BorderStyle.FixedSingle }; var dirPanel = new Panel { Width = this.Dpi(InputWidth), Height = this.Dpi(24), BackColor = Theme.TabBack, BorderStyle = BorderStyle.FixedSingle };
var shellDir = new TextBox { BackColor = Theme.TabBack, ForeColor = Theme.Text, Font = Theme.UiFont, Text = s.ShellStartupDir, BorderStyle = BorderStyle.None }; var shellDir = new TextBox { BackColor = Theme.TabBack, ForeColor = Theme.Text, Font = Theme.UiFont, Text = s.ShellStartupDir, BorderStyle = BorderStyle.None };
var browseBtn = new Button var browseBtn = new Button
{ {
Text = "…", Width = 26, FlatStyle = FlatStyle.Flat, Text = "…", Width = this.Dpi(26), FlatStyle = FlatStyle.Flat,
ForeColor = Theme.TextDim, BackColor = Theme.TabBack, Font = Theme.UiFont, Cursor = Cursors.Hand ForeColor = Theme.TextDim, BackColor = Theme.TabBack, Font = Theme.UiFont, Cursor = Cursors.Hand
}; };
// 無邊框、暗色文字、與輸入框同底色 → 不再突兀 // 無邊框、暗色文字、與輸入框同底色 → 不再突兀
@@ -143,7 +143,7 @@ public sealed class SettingsView : UserControl
flow.Controls.Add(MakeSpacer(12)); flow.Controls.Add(MakeSpacer(12));
// ── Live Preview ── // ── Live Preview ──
var preview = new Panel { Width = 460, Height = 100, BackColor = Color.FromArgb(20, 20, 24), Margin = new Padding(0, 0, 0, 8) }; var preview = new Panel { Width = this.Dpi(460), Height = this.Dpi(100), BackColor = Color.FromArgb(20, 20, 24), Margin = new Padding(0, 0, 0, 8) };
var previewLabel = new Label var previewLabel = new Label
{ {
Dock = DockStyle.Fill, BackColor = Color.FromArgb(20, 20, 24), ForeColor = Color.FromArgb(200, 200, 200), Dock = DockStyle.Fill, BackColor = Color.FromArgb(20, 20, 24), ForeColor = Color.FromArgb(200, 200, 200),
@@ -204,7 +204,7 @@ public sealed class SettingsView : UserControl
{ {
Text = "Keywords below are highlighted in red in every terminal (case-insensitive).\n" + Text = "Keywords below are highlighted in red in every terminal (case-insensitive).\n" +
"When a keyword appears in a background tab, that tab's dot turns red until you open it.", "When a keyword appears in a background tab, that tab's dot turns red until you open it.",
AutoSize = false, Width = 520, Height = 34, AutoSize = true,
ForeColor = Theme.TextDim, Font = Theme.UiFont, Margin = new Padding(0, 0, 0, 8) ForeColor = Theme.TextDim, Font = Theme.UiFont, Margin = new Padding(0, 0, 0, 8)
}); });
@@ -218,7 +218,7 @@ public sealed class SettingsView : UserControl
// 規則清單:每列 = 啟用勾選 + 關鍵字(可直接編輯) // 規則清單:每列 = 啟用勾選 + 關鍵字(可直接編輯)
var grid = new DataGridView var grid = new DataGridView
{ {
Width = 420, Height = 240, Width = this.Dpi(420), Height = this.Dpi(240),
BackgroundColor = Theme.WorkspaceBack, ForeColor = Theme.Text, GridColor = Theme.Border, BackgroundColor = Theme.WorkspaceBack, ForeColor = Theme.Text, GridColor = Theme.Border,
BorderStyle = BorderStyle.None, CellBorderStyle = DataGridViewCellBorderStyle.SingleHorizontal, BorderStyle = BorderStyle.None, CellBorderStyle = DataGridViewCellBorderStyle.SingleHorizontal,
DefaultCellStyle = { BackColor = Theme.TabBack, ForeColor = Theme.Text, SelectionBackColor = Theme.Hover, SelectionForeColor = Theme.Text }, DefaultCellStyle = { BackColor = Theme.TabBack, ForeColor = Theme.Text, SelectionBackColor = Theme.Hover, SelectionForeColor = Theme.Text },
@@ -227,9 +227,9 @@ public sealed class SettingsView : UserControl
EnableHeadersVisualStyles = false, RowHeadersVisible = false, EnableHeadersVisualStyles = false, RowHeadersVisible = false,
AllowUserToAddRows = false, AllowUserToDeleteRows = false, AllowUserToAddRows = false, AllowUserToDeleteRows = false,
AllowUserToResizeRows = false, SelectionMode = DataGridViewSelectionMode.FullRowSelect, AllowUserToResizeRows = false, SelectionMode = DataGridViewSelectionMode.FullRowSelect,
Font = Theme.UiFont, RowTemplate = { Height = 24 }, Margin = new Padding(0, 0, 0, 8) Font = Theme.UiFont, RowTemplate = { Height = this.Dpi(24) }, Margin = new Padding(0, 0, 0, 8)
}; };
grid.Columns.Add(new DataGridViewCheckBoxColumn { Name = "On", HeaderText = "On", Width = 44 }); grid.Columns.Add(new DataGridViewCheckBoxColumn { Name = "On", HeaderText = "On", Width = this.Dpi(44) });
grid.Columns.Add(new DataGridViewTextBoxColumn grid.Columns.Add(new DataGridViewTextBoxColumn
{ {
Name = "Keyword", HeaderText = "Keyword", Name = "Keyword", HeaderText = "Keyword",
@@ -239,7 +239,7 @@ public sealed class SettingsView : UserControl
flow.Controls.Add(grid); flow.Controls.Add(grid);
// 新增 / 移除 // 新增 / 移除
var newKw = new TextBox { Width = 220, BackColor = Theme.TabBack, ForeColor = Theme.Text, Font = Theme.UiFont, BorderStyle = BorderStyle.FixedSingle }; var newKw = new TextBox { Width = this.Dpi(220), BackColor = Theme.TabBack, ForeColor = Theme.Text, Font = Theme.UiFont, BorderStyle = BorderStyle.FixedSingle };
var addBtn = MakeButton("Add", Theme.Accent); var addBtn = MakeButton("Add", Theme.Accent);
var removeBtn = MakeButton("Remove Selected", Color.FromArgb(210, 120, 120)); var removeBtn = MakeButton("Remove Selected", Color.FromArgb(210, 120, 120));
@@ -315,33 +315,33 @@ public sealed class SettingsView : UserControl
"Leave blank to keep the AI Assistant disabled. Pick the model from the dropdown inside the\n" + "Leave blank to keep the AI Assistant disabled. Pick the model from the dropdown inside the\n" +
"AI Chat pane (it lists what your endpoint offers). The model must support function calling.\n" + "AI Chat pane (it lists what your endpoint offers). The model must support function calling.\n" +
"The API key is stored in Windows Credential Manager, never in settings.json or the app.", "The API key is stored in Windows Credential Manager, never in settings.json or the app.",
AutoSize = false, Width = 620, Height = 68, AutoSize = true,
ForeColor = Theme.TextDim, Font = Theme.UiFont, Margin = new Padding(0, 0, 0, 10) ForeColor = Theme.TextDim, Font = Theme.UiFont, Margin = new Padding(0, 0, 0, 10)
}); });
var baseUrl = new TextBox var baseUrl = new TextBox
{ {
Width = 380, BackColor = Theme.TabBack, ForeColor = Theme.Text, Font = Theme.UiFont, Width = this.Dpi(380), BackColor = Theme.TabBack, ForeColor = Theme.Text, Font = Theme.UiFont,
BorderStyle = BorderStyle.FixedSingle, Text = s.AiBaseUrl, BorderStyle = BorderStyle.FixedSingle, Text = s.AiBaseUrl,
PlaceholderText = "http://localhost:11434/v1" PlaceholderText = "http://localhost:11434/v1"
}; };
var apiKey = new TextBox var apiKey = new TextBox
{ {
Width = 380, BackColor = Theme.TabBack, ForeColor = Theme.Text, Font = Theme.UiFont, Width = this.Dpi(380), BackColor = Theme.TabBack, ForeColor = Theme.Text, Font = Theme.UiFont,
BorderStyle = BorderStyle.FixedSingle, UseSystemPasswordChar = true, BorderStyle = BorderStyle.FixedSingle, UseSystemPasswordChar = true,
Text = CredentialVault.Get("ETTerms/AiApiKey") ?? "", Text = CredentialVault.Get("ETTerms/AiApiKey") ?? "",
PlaceholderText = "(stored in Credential Manager)" PlaceholderText = "(stored in Credential Manager)"
}; };
var sysPrompt = new TextBox var sysPrompt = new TextBox
{ {
Width = 560, Height = 70, Multiline = true, BackColor = Theme.TabBack, ForeColor = Theme.Text, Width = this.Dpi(560), Height = this.Dpi(70), Multiline = true, BackColor = Theme.TabBack, ForeColor = Theme.Text,
Font = Theme.UiFont, BorderStyle = BorderStyle.FixedSingle, Text = s.AiSystemPrompt, Font = Theme.UiFont, BorderStyle = BorderStyle.FixedSingle, Text = s.AiSystemPrompt,
PlaceholderText = "(optional) override the assistant persona / system prompt" PlaceholderText = "(optional) override the assistant persona / system prompt"
}; };
var maxRounds = new NumericUpDown var maxRounds = new NumericUpDown
{ {
Width = 100, Minimum = 0, Maximum = 100000, Increment = 10, Value = s.AiMaxToolRounds, Width = this.Dpi(100), Minimum = 0, Maximum = 100000, Increment = 10, Value = s.AiMaxToolRounds,
BackColor = Theme.TabBack, ForeColor = Theme.Text, BorderStyle = BorderStyle.FixedSingle BackColor = Theme.TabBack, ForeColor = Theme.Text, BorderStyle = BorderStyle.FixedSingle
}; };
@@ -352,7 +352,7 @@ public sealed class SettingsView : UserControl
{ {
Text = "How many tool calls the assistant may chain per message before it stops (a runaway-loop guard).\n" + Text = "How many tool calls the assistant may chain per message before it stops (a runaway-loop guard).\n" +
"0 = unlimited — for long automation runs. Every round costs tokens; press Stop in the chat to abort.", "0 = unlimited — for long automation runs. Every round costs tokens; press Stop in the chat to abort.",
AutoSize = false, Width = 620, Height = 34, AutoSize = true,
ForeColor = Theme.TextDim, Font = Theme.UiFont, Margin = new Padding(0, 0, 0, 6) ForeColor = Theme.TextDim, Font = Theme.UiFont, Margin = new Padding(0, 0, 0, 6)
}); });
flow.Controls.Add(MakeSpacer(4)); flow.Controls.Add(MakeSpacer(4));
@@ -368,7 +368,7 @@ public sealed class SettingsView : UserControl
{ {
Text = "Tools the assistant can call: serial send/read (via the GUI's open Serial session, shown as [AI]),\n" + Text = "Tools the assistant can call: serial send/read (via the GUI's open Serial session, shown as [AI]),\n" +
"and PDU control over SNMP. Turning an outlet off / power-cycling always asks you to confirm.", "and PDU control over SNMP. Turning an outlet off / power-cycling always asks you to confirm.",
AutoSize = false, Width = 620, Height = 36, AutoSize = true,
ForeColor = Theme.TextDim, Font = Theme.UiFont, Margin = new Padding(0, 0, 0, 8) ForeColor = Theme.TextDim, Font = Theme.UiFont, Margin = new Padding(0, 0, 0, 8)
}); });
@@ -416,7 +416,7 @@ public sealed class SettingsView : UserControl
"user-level config. Serial: ETTerms owns the COM port, the AI drives it through a\n" + "user-level config. Serial: ETTerms owns the COM port, the AI drives it through a\n" +
"local named pipe (open a Serial session first). PDU: the AI controls outlets\n" + "local named pipe (open a Serial session first). PDU: the AI controls outlets\n" +
"directly over SNMP — no GUI session required.", "directly over SNMP — no GUI session required.",
AutoSize = false, Width = 600, Height = 64, AutoSize = true,
ForeColor = Theme.TextDim, Font = Theme.UiFont, Margin = new Padding(0, 0, 0, 8) ForeColor = Theme.TextDim, Font = Theme.UiFont, Margin = new Padding(0, 0, 0, 8)
}); });
@@ -426,7 +426,7 @@ public sealed class SettingsView : UserControl
flow.Controls.Add(new Label flow.Controls.Add(new Label
{ {
Text = $"{name}: {exe}", Text = $"{name}: {exe}",
AutoSize = false, Width = 600, Height = 20, AutoSize = true,
ForeColor = exists ? Theme.SerialColor : Color.FromArgb(210, 150, 120), ForeColor = exists ? Theme.SerialColor : Color.FromArgb(210, 150, 120),
Font = Theme.UiFont, Margin = new Padding(0, 0, 0, 2) Font = Theme.UiFont, Margin = new Padding(0, 0, 0, 2)
}); });
@@ -436,7 +436,7 @@ public sealed class SettingsView : UserControl
flow.Controls.Add(new Label flow.Controls.Add(new Label
{ {
Text = "⚠ Some servers not built yet — publish the app (or build the MCP projects). Setup still writes the expected paths.", Text = "⚠ Some servers not built yet — publish the app (or build the MCP projects). Setup still writes the expected paths.",
AutoSize = false, Width = 600, Height = 20, AutoSize = true,
ForeColor = Color.FromArgb(210, 150, 120), Font = Theme.UiFont, Margin = new Padding(0, 0, 0, 4) ForeColor = Color.FromArgb(210, 150, 120), Font = Theme.UiFont, Margin = new Padding(0, 0, 0, 4)
}); });
} }
@@ -455,7 +455,7 @@ public sealed class SettingsView : UserControl
{ {
var card = new Panel var card = new Panel
{ {
Width = 600, Height = 196, BackColor = Theme.TabBack, Width = this.Dpi(600), Height = this.Dpi(196), BackColor = Theme.TabBack,
Padding = new Padding(14), Margin = new Padding(0, 0, 0, 4) Padding = new Padding(14), Margin = new Padding(0, 0, 0, 4)
}; };
var col = new FlowLayoutPanel var col = new FlowLayoutPanel
@@ -473,7 +473,7 @@ public sealed class SettingsView : UserControl
var pathLbl = new Label var pathLbl = new Label
{ {
Text = $"Config: {McpRegistrar.ConfigPath(target)}", Text = $"Config: {McpRegistrar.ConfigPath(target)}",
AutoSize = false, Width = 560, Height = 18, AutoSize = true,
ForeColor = Theme.TextDim, Font = Theme.UiFont, Margin = new Padding(0, 0, 0, 6) ForeColor = Theme.TextDim, Font = Theme.UiFont, Margin = new Padding(0, 0, 0, 6)
}; };
@@ -495,7 +495,7 @@ public sealed class SettingsView : UserControl
}; };
var verifyBox = new TextBox var verifyBox = new TextBox
{ {
Multiline = true, ReadOnly = true, Width = 560, Height = 56, Multiline = true, ReadOnly = true, Width = this.Dpi(560), Height = this.Dpi(56),
BackColor = Color.FromArgb(20, 20, 24), ForeColor = Color.FromArgb(200, 200, 200), BackColor = Color.FromArgb(20, 20, 24), ForeColor = Color.FromArgb(200, 200, 200),
BorderStyle = BorderStyle.FixedSingle, Font = new Font("Cascadia Mono", 9f), BorderStyle = BorderStyle.FixedSingle, Font = new Font("Cascadia Mono", 9f),
Text = McpRegistrar.VerifyHint(target) Text = McpRegistrar.VerifyHint(target)
@@ -571,11 +571,11 @@ public sealed class SettingsView : UserControl
}; };
var lbl = new Label var lbl = new Label
{ {
Text = label, AutoSize = true, MinimumSize = new Size(LabelWidth, 0), Text = label, AutoSize = true, MinimumSize = new Size(row.Dpi(LabelWidth), 0),
ForeColor = Theme.Text, Font = Theme.UiFont, ForeColor = Theme.Text, Font = Theme.UiFont,
TextAlign = ContentAlignment.MiddleLeft, Margin = new Padding(0, 6, 8, 0) TextAlign = ContentAlignment.MiddleLeft, Margin = new Padding(0, 6, 8, 0)
}; };
if (control.Width <= 0) control.Width = InputWidth; if (control.Width <= 0) control.Width = row.Dpi(InputWidth);
row.Controls.Add(lbl); row.Controls.Add(lbl);
row.Controls.Add(control); row.Controls.Add(control);
return row; return row;
@@ -586,11 +586,12 @@ public sealed class SettingsView : UserControl
var b = new Button var b = new Button
{ {
Text = text, AutoSize = true, AutoSizeMode = AutoSizeMode.GrowAndShrink, Text = text, AutoSize = true, AutoSizeMode = AutoSizeMode.GrowAndShrink,
MinimumSize = new Size(90, 28), Padding = new Padding(10, 2, 10, 2), Padding = new Padding(10, 2, 10, 2),
FlatStyle = FlatStyle.Flat, FlatStyle = FlatStyle.Flat,
ForeColor = Theme.Text, BackColor = Theme.TabBack, Font = Theme.UiFont, ForeColor = Theme.Text, BackColor = Theme.TabBack, Font = Theme.UiFont,
Cursor = Cursors.Hand, Margin = new Padding(8, 0, 0, 0) Cursor = Cursors.Hand, Margin = new Padding(8, 0, 0, 0)
}; };
b.MinimumSize = new Size(b.Dpi(90), b.Dpi(28));
b.FlatAppearance.BorderColor = borderColor; b.FlatAppearance.BorderColor = borderColor;
b.FlatAppearance.MouseOverBackColor = Theme.Hover; b.FlatAppearance.MouseOverBackColor = Theme.Hover;
return b; return b;
+8 -7
View File
@@ -19,7 +19,7 @@ public sealed class StatusView : UserControl
var tabBar = new FlowLayoutPanel var tabBar = new FlowLayoutPanel
{ {
Dock = DockStyle.Top, Height = 32, BackColor = Theme.RailBack, Dock = DockStyle.Top, Height = this.Dpi(32), BackColor = Theme.RailBack,
Padding = new Padding(4, 4, 4, 0), WrapContents = false Padding = new Padding(4, 4, 4, 0), WrapContents = false
}; };
@@ -37,7 +37,7 @@ public sealed class StatusView : UserControl
var b = new Button var b = new Button
{ {
Text = text, AutoSize = true, AutoSizeMode = AutoSizeMode.GrowAndShrink, Text = text, AutoSize = true, AutoSizeMode = AutoSizeMode.GrowAndShrink,
MinimumSize = new Size(70, 26), Padding = new Padding(10, 2, 10, 2), MinimumSize = new Size(this.Dpi(70), this.Dpi(26)), Padding = new Padding(10, 2, 10, 2),
FlatStyle = FlatStyle.Flat, FlatStyle = FlatStyle.Flat,
ForeColor = Theme.Text, BackColor = Theme.TabBack, Font = Theme.UiFont, ForeColor = Theme.Text, BackColor = Theme.TabBack, Font = Theme.UiFont,
Margin = new Padding(0, 0, 4, 0), Cursor = Cursors.Hand Margin = new Padding(0, 0, 4, 0), Cursor = Cursors.Hand
@@ -75,11 +75,11 @@ public sealed class StatusView : UserControl
}; };
// Connection row // Connection row
var ipBox = new TextBox { Width = 160, Text = "192.168.1.21", BackColor = Theme.TabBack, ForeColor = Theme.Text, Font = Theme.UiFont }; var ipBox = new TextBox { Width = this.Dpi(160), Text = "192.168.1.21", BackColor = Theme.TabBack, ForeColor = Theme.Text, Font = Theme.UiFont };
var connectBtn = MakeButton("Connect", Theme.SerialColor); var connectBtn = MakeButton("Connect", Theme.SerialColor);
var statusLabel = new Label { AutoSize = true, Text = "Disconnected", ForeColor = Theme.TextDim, Font = Theme.UiFont, Margin = new Padding(8, 8, 0, 0) }; var statusLabel = new Label { AutoSize = true, Text = "Disconnected", ForeColor = Theme.TextDim, Font = Theme.UiFont, Margin = new Padding(8, 8, 0, 0) };
var connRow = new FlowLayoutPanel { FlowDirection = FlowDirection.LeftToRight, Width = 500, Height = 36, WrapContents = false, Margin = new Padding(0, 0, 0, 8) }; var connRow = new FlowLayoutPanel { FlowDirection = FlowDirection.LeftToRight, AutoSize = true, WrapContents = false, Margin = new Padding(0, 0, 0, 8) };
connRow.Controls.Add(new Label { Text = "PDU IP:", AutoSize = true, ForeColor = Theme.Text, Font = Theme.UiFont, Margin = new Padding(0, 6, 8, 0) }); connRow.Controls.Add(new Label { Text = "PDU IP:", AutoSize = true, ForeColor = Theme.Text, Font = Theme.UiFont, Margin = new Padding(0, 6, 8, 0) });
connRow.Controls.Add(ipBox); connRow.Controls.Add(ipBox);
connRow.Controls.Add(connectBtn); connRow.Controls.Add(connectBtn);
@@ -89,7 +89,7 @@ public sealed class StatusView : UserControl
// Port status grid // Port status grid
var grid = new DataGridView var grid = new DataGridView
{ {
Width = 480, Height = 310, AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill, Width = this.Dpi(480), Height = this.Dpi(310), AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill,
BackgroundColor = Theme.WorkspaceBack, ForeColor = Theme.Text, GridColor = Theme.Border, BackgroundColor = Theme.WorkspaceBack, ForeColor = Theme.Text, GridColor = Theme.Border,
BorderStyle = BorderStyle.None, CellBorderStyle = DataGridViewCellBorderStyle.SingleHorizontal, BorderStyle = BorderStyle.None, CellBorderStyle = DataGridViewCellBorderStyle.SingleHorizontal,
DefaultCellStyle = { BackColor = Theme.TabBack, ForeColor = Theme.Text, SelectionBackColor = Theme.Hover, SelectionForeColor = Theme.Text }, DefaultCellStyle = { BackColor = Theme.TabBack, ForeColor = Theme.Text, SelectionBackColor = Theme.Hover, SelectionForeColor = Theme.Text },
@@ -99,7 +99,7 @@ public sealed class StatusView : UserControl
AllowUserToAddRows = false, AllowUserToDeleteRows = false, ReadOnly = true, AllowUserToAddRows = false, AllowUserToDeleteRows = false, ReadOnly = true,
AllowUserToResizeRows = false, SelectionMode = DataGridViewSelectionMode.FullRowSelect, AllowUserToResizeRows = false, SelectionMode = DataGridViewSelectionMode.FullRowSelect,
ScrollBars = ScrollBars.None, Font = Theme.UiFont, ScrollBars = ScrollBars.None, Font = Theme.UiFont,
RowTemplate = { Height = 24 }, RowTemplate = { Height = this.Dpi(24) },
Margin = new Padding(0, 8, 0, 8) Margin = new Padding(0, 8, 0, 8)
}; };
grid.Columns.Add("Port", "Port"); grid.Columns.Add("Port", "Port");
@@ -274,11 +274,12 @@ public sealed class StatusView : UserControl
var b = new Button var b = new Button
{ {
Text = text, AutoSize = true, AutoSizeMode = AutoSizeMode.GrowAndShrink, Text = text, AutoSize = true, AutoSizeMode = AutoSizeMode.GrowAndShrink,
MinimumSize = new Size(90, 28), Padding = new Padding(10, 2, 10, 2), Padding = new Padding(10, 2, 10, 2),
FlatStyle = FlatStyle.Flat, FlatStyle = FlatStyle.Flat,
ForeColor = Theme.Text, BackColor = Theme.TabBack, Font = Theme.UiFont, ForeColor = Theme.Text, BackColor = Theme.TabBack, Font = Theme.UiFont,
Cursor = Cursors.Hand, Margin = new Padding(8, 0, 0, 0) Cursor = Cursors.Hand, Margin = new Padding(8, 0, 0, 0)
}; };
b.MinimumSize = new Size(b.Dpi(90), b.Dpi(28));
b.FlatAppearance.BorderColor = borderColor; b.FlatAppearance.BorderColor = borderColor;
b.FlatAppearance.MouseOverBackColor = Theme.Hover; b.FlatAppearance.MouseOverBackColor = Theme.Hover;
return b; return b;
+9
View File
@@ -1,4 +1,5 @@
using System.Drawing; using System.Drawing;
using System.Windows.Forms;
namespace ETTerms.App; namespace ETTerms.App;
@@ -27,4 +28,12 @@ public static class Theme
public static readonly Font UiFont = new("Segoe UI", 9.5f); public static readonly Font UiFont = new("Segoe UI", 9.5f);
public static readonly Font UiFontBold = new("Segoe UI", 9.5f, FontStyle.Bold); public static readonly Font UiFontBold = new("Segoe UI", 9.5f, FontStyle.Bold);
/// <summary>
/// 96-DPI 設計像素 → 控制項目前 DPI 的實際像素。
/// 執行期建立的控制項不會被 Form 的自動縮放處理(那只發生在 Form 初始化當下),
/// 字型以點數指定會自己隨 DPI 放大,因此所有寫死的像素尺寸都必須經過這裡換算,
/// 否則 125%/150% 時就是「字大了、框沒大」的裁切(作法同 ActivityRail.ItemSize)。
/// </summary>
public static int Dpi(this Control c, int px) => (int)Math.Round(px * (c.DeviceDpi / 96.0));
} }
+15 -13
View File
@@ -44,9 +44,10 @@ public sealed class WorkspaceView : UserControl
private bool _dragging; // 是否已進入拖曳 private bool _dragging; // 是否已進入拖曳
private const int DragThreshold = 5; private const int DragThreshold = 5;
private const int StripH = 30; // Tab 列尺寸:owner-draw 不會被自動縮放,需依 DPI 換算(同 ActivityRail.ItemSize
private const int TabW = 180; private int StripH => this.Dpi(30);
private const int CloseSz = 14; private int TabW => this.Dpi(180);
private int CloseSz => this.Dpi(14);
private static readonly (string label, int r, int c)[] Presets = private static readonly (string label, int r, int c)[] Presets =
{ ("1×1", 1, 1), ("1×2", 1, 2), ("2×1", 2, 1), ("2×2", 2, 2), ("2×3", 2, 3), ("3×3", 3, 3) }; { ("1×1", 1, 1), ("1×2", 1, 2), ("2×1", 2, 1), ("2×2", 2, 2), ("2×3", 2, 3), ("3×3", 3, 3) };
@@ -58,7 +59,7 @@ public sealed class WorkspaceView : UserControl
// 頂部工具列容器:左側為 Layout/Run 群組(Fill),右側為 Log AllDock Right // 頂部工具列容器:左側為 Layout/Run 群組(Fill),右側為 Log AllDock Right
// 高度 40 與左側 ConnectionSidebar 的 tab bar 對齊,且容得下 AutoSize 按鈕(不被裁切) // 高度 40 與左側 ConnectionSidebar 的 tab bar 對齊,且容得下 AutoSize 按鈕(不被裁切)
var topBar = new Panel { Dock = DockStyle.Top, Height = 40, BackColor = Theme.RailBack }; var topBar = new Panel { Dock = DockStyle.Top, Height = this.Dpi(40), BackColor = Theme.RailBack };
_toolbar = new FlowLayoutPanel _toolbar = new FlowLayoutPanel
{ {
@@ -268,7 +269,7 @@ public sealed class WorkspaceView : UserControl
: $"{icon} {s.Title} [{glabel}]"; : $"{icon} {s.Title} [{glabel}]";
var lbl = new Label var lbl = new Label
{ {
Dock = DockStyle.Bottom, Height = 22, Dock = DockStyle.Bottom, Height = this.Dpi(22),
Text = labelText, Text = labelText,
ForeColor = s == _active ? Theme.Text : Theme.TextDim, ForeColor = s == _active ? Theme.Text : Theme.TextDim,
BackColor = Theme.RailBack, Font = Theme.UiFont, BackColor = Theme.RailBack, Font = Theme.UiFont,
@@ -293,7 +294,7 @@ public sealed class WorkspaceView : UserControl
foreach (var s in _sessions) foreach (var s in _sessions)
{ {
s.TabBounds = new Rectangle(x, 0, TabW, StripH); s.TabBounds = new Rectangle(x, 0, TabW, StripH);
s.CloseRect = new Rectangle(s.TabBounds.Right - CloseSz - 6, (StripH - CloseSz) / 2, CloseSz, CloseSz); s.CloseRect = new Rectangle(s.TabBounds.Right - CloseSz - this.Dpi(6), (StripH - CloseSz) / 2, CloseSz, CloseSz);
x += TabW; x += TabW;
} }
} }
@@ -400,9 +401,11 @@ public sealed class WorkspaceView : UserControl
// 警示中的背景分頁:型別圓點改紅色,切過去看時清除。AI 分頁用 accent 紫。 // 警示中的背景分頁:型別圓點改紅色,切過去看時清除。AI 分頁用 accent 紫。
Color dotColor = s.Alert ? Color.FromArgb(235, 85, 85) Color dotColor = s.Alert ? Color.FromArgb(235, 85, 85)
: s.IsAi ? Theme.Accent : s.IsSsh ? Theme.SshColor : Theme.SerialColor; : s.IsAi ? Theme.Accent : s.IsSsh ? Theme.SshColor : Theme.SerialColor;
int dotSz = this.Dpi(8);
using (var dot = new SolidBrush(dotColor)) using (var dot = new SolidBrush(dotColor))
g.FillEllipse(dot, s.TabBounds.Left + 9, StripH / 2 - 4, 8, 8); g.FillEllipse(dot, s.TabBounds.Left + this.Dpi(9), (StripH - dotSz) / 2, dotSz, dotSz);
var tr = new Rectangle(s.TabBounds.Left + 22, s.TabBounds.Top, s.TabBounds.Width - 22 - CloseSz - 10, StripH); int textL = this.Dpi(22);
var tr = new Rectangle(s.TabBounds.Left + textL, s.TabBounds.Top, s.TabBounds.Width - textL - CloseSz - this.Dpi(10), StripH);
TextRenderer.DrawText(g, s.Title, Theme.UiFont, tr, active ? Theme.Text : Theme.TextDim, TextRenderer.DrawText(g, s.Title, Theme.UiFont, tr, active ? Theme.Text : Theme.TextDim,
TextFormatFlags.Left | TextFormatFlags.VerticalCenter | TextFormatFlags.EndEllipsis); TextFormatFlags.Left | TextFormatFlags.VerticalCenter | TextFormatFlags.EndEllipsis);
TextRenderer.DrawText(g, "✕", Theme.UiFont, s.CloseRect, hover ? Theme.Text : Theme.TextDim, TextRenderer.DrawText(g, "✕", Theme.UiFont, s.CloseRect, hover ? Theme.Text : Theme.TextDim,
@@ -416,7 +419,7 @@ public sealed class WorkspaceView : UserControl
var b = new Button var b = new Button
{ {
Text = label, AutoSize = true, AutoSizeMode = AutoSizeMode.GrowAndShrink, Text = label, AutoSize = true, AutoSizeMode = AutoSizeMode.GrowAndShrink,
MinimumSize = new Size(40, 26), Padding = new Padding(8, 2, 8, 2), MinimumSize = new Size(this.Dpi(40), this.Dpi(26)), Padding = new Padding(8, 2, 8, 2),
FlatStyle = FlatStyle.Flat, FlatStyle = FlatStyle.Flat,
ForeColor = Theme.Text, BackColor = Theme.TabBack, Font = Theme.UiFont, ForeColor = Theme.Text, BackColor = Theme.TabBack, Font = Theme.UiFont,
Margin = new Padding(3, 0, 3, 0), Cursor = Cursors.Hand Margin = new Padding(3, 0, 3, 0), Cursor = Cursors.Hand
@@ -427,12 +430,12 @@ public sealed class WorkspaceView : UserControl
return b; return b;
} }
private static Button MakeActionButton(string text, int minWidth, int leftMargin, EventHandler onClick) private Button MakeActionButton(string text, int minWidth, int leftMargin, EventHandler onClick)
{ {
var b = new Button var b = new Button
{ {
Text = text, AutoSize = true, AutoSizeMode = AutoSizeMode.GrowAndShrink, Text = text, AutoSize = true, AutoSizeMode = AutoSizeMode.GrowAndShrink,
MinimumSize = new Size(minWidth, 26), Padding = new Padding(10, 2, 10, 2), MinimumSize = new Size(this.Dpi(minWidth), this.Dpi(26)), Padding = new Padding(10, 2, 10, 2),
FlatStyle = FlatStyle.Flat, FlatStyle = FlatStyle.Flat,
ForeColor = Theme.Text, BackColor = Theme.TabBack, Font = Theme.UiFont, ForeColor = Theme.Text, BackColor = Theme.TabBack, Font = Theme.UiFont,
Margin = new Padding(leftMargin, 0, 3, 0), Cursor = Cursors.Hand Margin = new Padding(leftMargin, 0, 3, 0), Cursor = Cursors.Hand
@@ -474,8 +477,7 @@ public sealed class WorkspaceView : UserControl
private void SetLogAllActive(bool on) private void SetLogAllActive(bool on)
{ {
_logAll.Text = on ? "⏺ Logging All" : "⏺ Log All"; _logAll.Text = on ? "⏺ Logging All" : "⏺ Log All"; // AutoSize 按鈕,寬度隨字自動調
_logAll.Width = on ? 110 : 96;
_logAll.ForeColor = on ? Theme.SerialColor : Theme.Text; _logAll.ForeColor = on ? Theme.SerialColor : Theme.Text;
} }
+1 -1
View File
@@ -10,7 +10,7 @@
<AssemblyName>ETTerms</AssemblyName> <AssemblyName>ETTerms</AssemblyName>
<!-- 版本資訊 --> <!-- 版本資訊 -->
<Version>0.7.1</Version> <Version>0.7.2</Version>
<Product>ETTerms</Product> <Product>ETTerms</Product>
<Company>ETTerms Project</Company> <Company>ETTerms Project</Company>
+8 -4
View File
@@ -73,7 +73,7 @@ public sealed class SessionPage : UserControl
Dock = DockStyle.Fill; Dock = DockStyle.Fill;
// ── 頂部腳本列 ── // ── 頂部腳本列 ──
var bar = new Panel { Dock = DockStyle.Top, Height = 24, BackColor = Theme.RailBack }; var bar = new Panel { Dock = DockStyle.Top, Height = this.Dpi(24), BackColor = Theme.RailBack };
_status = new Label _status = new Label
{ {
Dock = DockStyle.Fill, Text = "Idle", Dock = DockStyle.Fill, Text = "Idle",
@@ -84,7 +84,7 @@ public sealed class SessionPage : UserControl
_stop = MakeBarButton("■ Stop", (_, _) => _runner.Cancel()); _stop = MakeBarButton("■ Stop", (_, _) => _runner.Cancel());
_stop.Enabled = false; _stop.Enabled = false;
_log = MakeBarButton("⏺ Log", OnToggleLog); _log = MakeBarButton("⏺ Log", OnToggleLog);
_log.Width = 92; // 容納 "⏺ Logging" 不被截字 _log.Width = BarButtonWidth("⏺ Logging"); // 取切換後較長的字,不被截字
bar.Controls.Add(_status); // Fill 先加 bar.Controls.Add(_status); // Fill 先加
bar.Controls.Add(_log); // Right(最左:Log bar.Controls.Add(_log); // Right(最左:Log
bar.Controls.Add(_run); // Right(中:Script bar.Controls.Add(_run); // Right(中:Script
@@ -185,11 +185,15 @@ public sealed class SessionPage : UserControl
else action(); else action();
} }
private static Button MakeBarButton(string text, EventHandler onClick) /// <summary>依實際字寬(隨 DPI 放大)算按鈕寬度,固定像素在 125%/150% 會截字。</summary>
private int BarButtonWidth(string text) =>
TextRenderer.MeasureText(text, Theme.UiFont).Width + this.Dpi(18);
private Button MakeBarButton(string text, EventHandler onClick)
{ {
var b = new Button var b = new Button
{ {
Text = text, Dock = DockStyle.Right, Width = 72, FlatStyle = FlatStyle.Flat, Text = text, Dock = DockStyle.Right, Width = BarButtonWidth(text), FlatStyle = FlatStyle.Flat,
ForeColor = Theme.Text, BackColor = Theme.TabBack, Font = Theme.UiFont, Cursor = Cursors.Hand ForeColor = Theme.Text, BackColor = Theme.TabBack, Font = Theme.UiFont, Cursor = Cursors.Hand
}; };
b.FlatAppearance.BorderColor = Theme.Border; b.FlatAppearance.BorderColor = Theme.Border;
+2 -1
View File
@@ -1,6 +1,7 @@
using System.Drawing; using System.Drawing;
using System.Drawing.Drawing2D; using System.Drawing.Drawing2D;
using System.Windows.Forms; using System.Windows.Forms;
using ETTerms.App;
namespace ETTerms.Terminal; namespace ETTerms.Terminal;
@@ -44,7 +45,7 @@ public sealed class DarkScrollBar : Control
// 導致 TerminalView 收不到鍵盤輸入(打字 / Enter 全失效),需重開 session 才恢復。 // 導致 TerminalView 收不到鍵盤輸入(打字 / Enter 全失效),需重開 session 才恢復。
SetStyle(ControlStyles.Selectable, false); SetStyle(ControlStyles.Selectable, false);
TabStop = false; TabStop = false;
Width = 12; Width = this.Dpi(12);
BackColor = TrackColor; BackColor = TrackColor;
} }
+6 -5
View File
@@ -3,6 +3,7 @@ using System.Runtime.InteropServices;
using System.Text; using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Windows.Forms; using System.Windows.Forms;
using ETTerms.App;
using ETTerms.Infrastructure; using ETTerms.Infrastructure;
namespace ETTerms.Terminal; namespace ETTerms.Terminal;
@@ -440,7 +441,7 @@ public sealed class TerminalView : UserControl
if (_searchPanel != null) return; if (_searchPanel != null) return;
var back = Color.FromArgb(32, 32, 38); var back = Color.FromArgb(32, 32, 38);
_searchPanel = new Panel { Size = new Size(268, 30), BackColor = back, Visible = false }; _searchPanel = new Panel { Size = new Size(this.Dpi(268), this.Dpi(30)), BackColor = back, Visible = false };
_searchPanel.Paint += (_, pe) => _searchPanel.Paint += (_, pe) =>
{ {
using var pen = new Pen(Color.FromArgb(58, 58, 66)); using var pen = new Pen(Color.FromArgb(58, 58, 66));
@@ -449,7 +450,7 @@ public sealed class TerminalView : UserControl
_searchBox = new TextBox _searchBox = new TextBox
{ {
Bounds = new Rectangle(6, 5, 130, 20), BorderStyle = BorderStyle.None, Bounds = new Rectangle(this.Dpi(6), this.Dpi(5), this.Dpi(130), this.Dpi(20)), BorderStyle = BorderStyle.None,
BackColor = back, ForeColor = Color.FromArgb(222, 222, 226), Font = new Font("Segoe UI", 9.5f) BackColor = back, ForeColor = Color.FromArgb(222, 222, 226), Font = new Font("Segoe UI", 9.5f)
}; };
_searchBox.TextChanged += (_, _) => RunSearch(); _searchBox.TextChanged += (_, _) => RunSearch();
@@ -463,7 +464,7 @@ public sealed class TerminalView : UserControl
_searchCount = new Label _searchCount = new Label
{ {
Bounds = new Rectangle(138, 7, 56, 16), Text = "", Bounds = new Rectangle(this.Dpi(138), this.Dpi(7), this.Dpi(56), this.Dpi(16)), Text = "",
ForeColor = Color.FromArgb(150, 150, 158), BackColor = back, ForeColor = Color.FromArgb(150, 150, 158), BackColor = back,
Font = new Font("Segoe UI", 8.5f), TextAlign = ContentAlignment.MiddleRight Font = new Font("Segoe UI", 8.5f), TextAlign = ContentAlignment.MiddleRight
}; };
@@ -472,7 +473,7 @@ public sealed class TerminalView : UserControl
{ {
var b = new Button var b = new Button
{ {
Bounds = new Rectangle(x, 4, 22, 22), Text = text, FlatStyle = FlatStyle.Flat, Bounds = new Rectangle(this.Dpi(x), this.Dpi(4), this.Dpi(22), this.Dpi(22)), Text = text, FlatStyle = FlatStyle.Flat,
ForeColor = Color.FromArgb(180, 180, 188), BackColor = back, ForeColor = Color.FromArgb(180, 180, 188), BackColor = back,
Font = new Font("Segoe UI", 8.5f), TabStop = false, Cursor = Cursors.Hand Font = new Font("Segoe UI", 8.5f), TabStop = false, Cursor = Cursors.Hand
}; };
@@ -493,7 +494,7 @@ public sealed class TerminalView : UserControl
private void PositionSearchPanel() private void PositionSearchPanel()
{ {
if (_searchPanel == null) return; if (_searchPanel == null) return;
_searchPanel.Location = new Point(Math.Max(0, ContentWidth - _searchPanel.Width - 8), 6); _searchPanel.Location = new Point(Math.Max(0, ContentWidth - _searchPanel.Width - this.Dpi(8)), this.Dpi(6));
} }
/// <summary>重掃整個 bufferscrollback + 畫面)建立命中清單,並跳到最靠近底部的命中。</summary> /// <summary>重掃整個 bufferscrollback + 畫面)建立命中清單,並跳到最靠近底部的命中。</summary>