fix: open native directory picker for new instances
This commit is contained in:
@@ -14,6 +14,7 @@ import (
|
|||||||
"chub/internal/domain"
|
"chub/internal/domain"
|
||||||
"chub/internal/platform/browser"
|
"chub/internal/platform/browser"
|
||||||
"chub/internal/platform/config"
|
"chub/internal/platform/config"
|
||||||
|
"chub/internal/platform/files"
|
||||||
"chub/internal/platform/logging"
|
"chub/internal/platform/logging"
|
||||||
"chub/internal/ui"
|
"chub/internal/ui"
|
||||||
"gioui.org/app"
|
"gioui.org/app"
|
||||||
@@ -102,6 +103,8 @@ func runWindow(logger *slog.Logger) {
|
|||||||
window.Option(app.Title("Chub"), app.Size(unit.Dp(1100), unit.Dp(720)))
|
window.Option(app.Title("Chub"), app.Size(unit.Dp(1100), unit.Dp(720)))
|
||||||
theme := material.NewTheme()
|
theme := material.NewTheme()
|
||||||
shell := ui.NewShell(theme)
|
shell := ui.NewShell(theme)
|
||||||
|
directoryPicker := files.NewDirectoryPicker()
|
||||||
|
shell.OnChooseDirectory(directoryPicker.ChooseDirectory, window.Invalidate)
|
||||||
discoverer := browser.NewDiscoverer()
|
discoverer := browser.NewDiscoverer()
|
||||||
shell.OnPathSearch(func(ctx context.Context, field ui.PathField, current string) (string, error) {
|
shell.OnPathSearch(func(ctx context.Context, field ui.PathField, current string) (string, error) {
|
||||||
switch field {
|
switch field {
|
||||||
|
|||||||
@@ -29,6 +29,7 @@
|
|||||||
| T-204 | UI 全状态验收与 Windows 打包 smoke | T-201,T-202,T-203 | DONE |
|
| T-204 | UI 全状态验收与 Windows 打包 smoke | T-201,T-202,T-203 | DONE |
|
||||||
| T-205 | 设置路径字段增加 Label、选择和搜索操作 | T-201 | DONE |
|
| T-205 | 设置路径字段增加 Label、选择和搜索操作 | T-201 | DONE |
|
||||||
| T-206 | 左侧导航、可取消路径搜索与新建实例表单语义 | T-205 | DONE |
|
| T-206 | 左侧导航、可取消路径搜索与新建实例表单语义 | T-205 | DONE |
|
||||||
|
| T-207 | 接入新建实例的 Windows 目录选择器 | T-206 | DONE |
|
||||||
|
|
||||||
## Backlog
|
## Backlog
|
||||||
|
|
||||||
|
|||||||
@@ -5,9 +5,9 @@
|
|||||||
- 日期:2026-07-22
|
- 日期:2026-07-22
|
||||||
- 阶段:Phase 2 产品外壳(T-201 至 T-206 已完成)
|
- 阶段:Phase 2 产品外壳(T-201 至 T-206 已完成)
|
||||||
- 代码:已建立 Go module `chub`、`cmd/chub` 入口、logging 测试基座、浏览器 domain/application 合约、Chrome/Edge 参数/发现模块、启动 registry、Windows 身份/占用检查、优雅关闭、Job Object、真实 Chrome/Edge smoke、JSON 配置存储、启动恢复、CLI JSON 合约、应用内事件总线、UI 状态测试和 Windows smoke 脚本,T-001 至 T-003、T-101 至 T-104、T-201 至 T-206 已完成
|
- 代码:已建立 Go module `chub`、`cmd/chub` 入口、logging 测试基座、浏览器 domain/application 合约、Chrome/Edge 参数/发现模块、启动 registry、Windows 身份/占用检查、优雅关闭、Job Object、真实 Chrome/Edge smoke、JSON 配置存储、启动恢复、CLI JSON 合约、应用内事件总线、UI 状态测试和 Windows smoke 脚本,T-001 至 T-003、T-101 至 T-104、T-201 至 T-206 已完成
|
||||||
- UI:Gio 双页 Shell 使用左侧“实例/设置”导航;四个设置路径有独立异步可取消搜索,切换页面后输入和任务状态保留;新建实例表单使用 Label、目录选择入口和 Chrome/Edge RadioButton;CLI `list/events` 已可用,`start/stop/restart` 等待 BrowserManager adapter
|
- UI:Gio 双页 Shell 使用左侧“实例/设置”导航;四个设置路径有独立异步可取消搜索,切换页面后输入和任务状态保留;新建实例表单使用 Label、Windows 原生目录选择器和 Chrome/Edge RadioButton;CLI `list/events` 已可用,`start/stop/restart` 等待 BrowserManager adapter
|
||||||
- 浏览器核心:设计参考来自 `D:\OPC\shop_helm\internal\platform\chrome`,尚未复制或接入本项目
|
- 浏览器核心:设计参考来自 `D:\OPC\shop_helm\internal\platform\chrome`,尚未复制或接入本项目
|
||||||
- blocker:无;下一步可进入 BrowserManager application adapter、原生文件选择器和真实 UI 操作接线。
|
- blocker:无;下一步可进入 BrowserManager application adapter,以及设置页路径选择器和真实 UI 操作接线。
|
||||||
|
|
||||||
## 当前目录
|
## 当前目录
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -35,4 +35,4 @@ owner: codex
|
|||||||
- 变更:Gio Shell 改为左侧导航并移除窗口内“Chub 浏览器管理”文字;路径搜索接入独立 task ID、取消令牌、结果通道和 `Window.Invalidate`;新建实例增加 Label、目录选择入口与 Chrome/Edge RadioButton;同步更新 HTML 原型。
|
- 变更:Gio Shell 改为左侧导航并移除窗口内“Chub 浏览器管理”文字;路径搜索接入独立 task ID、取消令牌、结果通道和 `Window.Invalidate`;新建实例增加 Label、目录选择入口与 Chrome/Edge RadioButton;同步更新 HTML 原型。
|
||||||
- 验证:`gofmt`、HTML script syntax check、`go test ./... -count=1`、`go vet ./...`、`scripts/smoke-windows.ps1` 均通过。
|
- 验证:`gofmt`、HTML script syntax check、`go test ./... -count=1`、`go vet ./...`、`scripts/smoke-windows.ps1` 均通过。
|
||||||
- 阻塞:无。
|
- 阻塞:无。
|
||||||
- 残余风险:原生 Windows 文件/目录选择器仍需后续 platform adapter;`go test -race` 在当前环境因 `CGO_ENABLED=0` 不可用。
|
- 残余风险:新建实例的原生目录选择器已由 T-207 接入;设置页路径选择器仍需同一 platform adapter 扩展;`go test -race` 在当前环境因 `CGO_ENABLED=0` 不可用。
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
---
|
||||||
|
id: T-207
|
||||||
|
title: 接入新建实例的 Windows 目录选择器
|
||||||
|
phase: 2
|
||||||
|
deps: [T-206]
|
||||||
|
status: DONE
|
||||||
|
created: 2026-07-22
|
||||||
|
owner: codex
|
||||||
|
---
|
||||||
|
|
||||||
|
## 需求与背景
|
||||||
|
|
||||||
|
新建实例的“选择路径”此前只是表单反馈,不会打开 Windows 文件夹选择器,用户无法通过按钮设置 User Data Dir。
|
||||||
|
|
||||||
|
## 方案与边界
|
||||||
|
|
||||||
|
- Windows 平台适配层使用隐藏的 STA PowerShell 进程打开 `FolderBrowserDialog`,避免在 Gio FrameEvent 中阻塞。
|
||||||
|
- Gio Shell 在 goroutine 中调用目录选择器,并通过结果通道和 `Window.Invalidate` 回到 UI;成功后回填 User Data Dir,取消或失败保留原输入。
|
||||||
|
- 同时限制重复打开:选择器已打开时不创建第二个对话框。
|
||||||
|
- 本任务只覆盖新建实例的 User Data Dir;设置页各路径选择按钮仍是后续范围。
|
||||||
|
|
||||||
|
## 验收要点
|
||||||
|
|
||||||
|
- 点击“选择路径”打开 Windows 文件夹选择器。
|
||||||
|
- 选择目录后回填输入框;取消时不清空输入。
|
||||||
|
- 平台命令和 Shell 回填逻辑有单元测试。
|
||||||
|
- `go test ./...`、`go vet ./...` 和 Windows 构建通过。
|
||||||
|
|
||||||
|
## 执行记录
|
||||||
|
|
||||||
|
- 状态:DONE
|
||||||
|
- 变更:新增 `internal/platform/files` Windows 目录选择器和 Gio 异步回填状态;更新 UI 测试与当前状态文档。
|
||||||
|
- 验证:`gofmt`、`go test ./...`、`go vet ./...`、Windows build/smoke 通过。
|
||||||
|
- 阻塞:无。
|
||||||
|
- 残余风险:当前 PowerShell adapter 依赖 Windows Forms 和 `powershell.exe` 可用;设置页的路径选择器尚未复用该 adapter。
|
||||||
+1
-1
@@ -23,7 +23,7 @@
|
|||||||
- 详情和新建配置使用右侧抽屉,不创建文档标签。
|
- 详情和新建配置使用右侧抽屉,不创建文档标签。
|
||||||
- 选择实例后,右侧详情同时承载当前状态、操作、配置摘要和最近活动;不要求用户在实例与活动页面之间切换。
|
- 选择实例后,右侧详情同时承载当前状态、操作、配置摘要和最近活动;不要求用户在实例与活动页面之间切换。
|
||||||
- 设置作为独立顶级页面,采用显式提交;取消只恢复本次未保存的表单修改。
|
- 设置作为独立顶级页面,采用显式提交;取消只恢复本次未保存的表单修改。
|
||||||
- 路径搜索不进行默认全盘扫描:浏览器搜索标准安装路径,目录搜索验证当前输入;原生选择器由后续 platform adapter 提供。
|
- 路径搜索不进行默认全盘扫描:浏览器搜索标准安装路径,目录搜索验证当前输入;新建实例的 User Data Dir 已接入 Windows 原生选择器。
|
||||||
- 外部实例显示占用证据,但不提供接管或强制关闭入口。
|
- 外部实例显示占用证据,但不提供接管或强制关闭入口。
|
||||||
- 原型中的“确认”只模拟事件反馈,不启动真实浏览器或改变文件。
|
- 原型中的“确认”只模拟事件反馈,不启动真实浏览器或改变文件。
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package files
|
||||||
|
|
||||||
|
import "context"
|
||||||
|
|
||||||
|
// DirectoryPicker opens a platform directory chooser. The implementation must
|
||||||
|
// block only in its own goroutine; UI callers receive the result asynchronously.
|
||||||
|
type DirectoryPicker interface {
|
||||||
|
ChooseDirectory(context.Context) (string, error)
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
//go:build !windows
|
||||||
|
|
||||||
|
package files
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ErrSelectionCanceled = context.Canceled
|
||||||
|
|
||||||
|
type unsupportedDirectoryPicker struct{}
|
||||||
|
|
||||||
|
func NewDirectoryPicker() DirectoryPicker { return unsupportedDirectoryPicker{} }
|
||||||
|
|
||||||
|
func (unsupportedDirectoryPicker) ChooseDirectory(context.Context) (string, error) {
|
||||||
|
return "", errors.New("directory picker is only supported on Windows")
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
//go:build windows
|
||||||
|
|
||||||
|
package files
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ErrSelectionCanceled = context.Canceled
|
||||||
|
|
||||||
|
type commandRunner interface {
|
||||||
|
Run(context.Context, string, ...string) ([]byte, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type execRunner struct{}
|
||||||
|
|
||||||
|
func (execRunner) Run(ctx context.Context, name string, args ...string) ([]byte, error) {
|
||||||
|
return exec.CommandContext(ctx, name, args...).Output()
|
||||||
|
}
|
||||||
|
|
||||||
|
type windowsDirectoryPicker struct{ runner commandRunner }
|
||||||
|
|
||||||
|
func NewDirectoryPicker() DirectoryPicker { return windowsDirectoryPicker{runner: execRunner{}} }
|
||||||
|
|
||||||
|
func newDirectoryPickerWithRunner(runner commandRunner) windowsDirectoryPicker {
|
||||||
|
return windowsDirectoryPicker{runner: runner}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p windowsDirectoryPicker) ChooseDirectory(ctx context.Context) (string, error) {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
output, err := p.runner.Run(ctx, "powershell.exe",
|
||||||
|
"-NoProfile", "-NonInteractive", "-STA", "-WindowStyle", "Hidden", "-Command", folderDialogScript)
|
||||||
|
if err != nil {
|
||||||
|
if ctxErr := ctx.Err(); ctxErr != nil {
|
||||||
|
return "", ctxErr
|
||||||
|
}
|
||||||
|
return "", fmt.Errorf("open directory picker: %w", err)
|
||||||
|
}
|
||||||
|
selected := strings.TrimSpace(string(output))
|
||||||
|
if selected == "" {
|
||||||
|
return "", ErrSelectionCanceled
|
||||||
|
}
|
||||||
|
if !filepath.IsAbs(selected) {
|
||||||
|
return "", fmt.Errorf("directory picker returned a non-absolute path")
|
||||||
|
}
|
||||||
|
return filepath.Clean(selected), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const folderDialogScript = `[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false)
|
||||||
|
Add-Type -AssemblyName System.Windows.Forms
|
||||||
|
$dialog = New-Object System.Windows.Forms.FolderBrowserDialog
|
||||||
|
$dialog.Description = '选择浏览器 User Data Dir'
|
||||||
|
$dialog.ShowNewFolderButton = $true
|
||||||
|
if ($dialog.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) {
|
||||||
|
[Console]::Out.Write($dialog.SelectedPath)
|
||||||
|
}`
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
//go:build windows
|
||||||
|
|
||||||
|
package files
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
type fakeRunner struct {
|
||||||
|
name string
|
||||||
|
args []string
|
||||||
|
out []byte
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeRunner) Run(_ context.Context, name string, args ...string) ([]byte, error) {
|
||||||
|
f.name = name
|
||||||
|
f.args = append([]string(nil), args...)
|
||||||
|
return f.out, f.err
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWindowsDirectoryPickerReturnsCleanAbsolutePath(t *testing.T) {
|
||||||
|
runner := &fakeRunner{out: []byte("C:\\profiles\\demo\\\r\n")}
|
||||||
|
picker := newDirectoryPickerWithRunner(runner)
|
||||||
|
got, err := picker.ChooseDirectory(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if got != `C:\profiles\demo` {
|
||||||
|
t.Fatalf("path = %q", got)
|
||||||
|
}
|
||||||
|
if runner.name != "powershell.exe" || !strings.Contains(strings.Join(runner.args, " "), "-STA") {
|
||||||
|
t.Fatalf("unexpected picker command: %s %v", runner.name, runner.args)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWindowsDirectoryPickerTreatsEmptySelectionAsCanceled(t *testing.T) {
|
||||||
|
picker := newDirectoryPickerWithRunner(&fakeRunner{})
|
||||||
|
if _, err := picker.ChooseDirectory(context.Background()); !errors.Is(err, ErrSelectionCanceled) {
|
||||||
|
t.Fatalf("error = %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
+93
-10
@@ -31,6 +31,8 @@ const (
|
|||||||
|
|
||||||
type PathSearcher func(context.Context, PathField, string) (string, error)
|
type PathSearcher func(context.Context, PathField, string) (string, error)
|
||||||
|
|
||||||
|
type DirectoryChooser func(context.Context) (string, error)
|
||||||
|
|
||||||
type pathSearchState struct {
|
type pathSearchState struct {
|
||||||
request uint64
|
request uint64
|
||||||
cancel context.CancelFunc
|
cancel context.CancelFunc
|
||||||
@@ -44,6 +46,18 @@ type pathSearchResult struct {
|
|||||||
err error
|
err error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type directoryPickState struct {
|
||||||
|
request uint64
|
||||||
|
cancel context.CancelFunc
|
||||||
|
running bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type directoryPickResult struct {
|
||||||
|
request uint64
|
||||||
|
path string
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
// InstanceRow is the read-only view model used by the first Gio shell.
|
// InstanceRow is the read-only view model used by the first Gio shell.
|
||||||
// Runtime data will replace these fixtures when the application service is wired.
|
// Runtime data will replace these fixtures when the application service is wired.
|
||||||
type InstanceRow struct {
|
type InstanceRow struct {
|
||||||
@@ -97,13 +111,16 @@ type Shell struct {
|
|||||||
pathFeedback string
|
pathFeedback string
|
||||||
formFeedback string
|
formFeedback string
|
||||||
|
|
||||||
list widget.List
|
list widget.List
|
||||||
rows []InstanceRow
|
rows []InstanceRow
|
||||||
onSave func(SettingsState)
|
onSave func(SettingsState)
|
||||||
pathSearcher PathSearcher
|
pathSearcher PathSearcher
|
||||||
invalidate func()
|
invalidate func()
|
||||||
searches map[PathField]*pathSearchState
|
searches map[PathField]*pathSearchState
|
||||||
searchResults chan pathSearchResult
|
searchResults chan pathSearchResult
|
||||||
|
directoryChooser DirectoryChooser
|
||||||
|
directoryPick directoryPickState
|
||||||
|
directoryResults chan directoryPickResult
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewShell(theme *material.Theme) *Shell {
|
func NewShell(theme *material.Theme) *Shell {
|
||||||
@@ -117,7 +134,7 @@ func NewShell(theme *material.Theme) *Shell {
|
|||||||
PathEdgeExecutable: {},
|
PathEdgeExecutable: {},
|
||||||
PathDefaultUserData: {},
|
PathDefaultUserData: {},
|
||||||
PathLogDirectory: {},
|
PathLogDirectory: {},
|
||||||
}, searchResults: make(chan pathSearchResult, 8)}
|
}, searchResults: make(chan pathSearchResult, 8), directoryResults: make(chan directoryPickResult, 1)}
|
||||||
s.chromePath.SetText(`C:\Program Files\Google\Chrome\Application\chrome.exe`)
|
s.chromePath.SetText(`C:\Program Files\Google\Chrome\Application\chrome.exe`)
|
||||||
s.edgePath.SetText(`C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe`)
|
s.edgePath.SetText(`C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe`)
|
||||||
s.dataDir.SetText(`C:\Users\Public\chub\profiles`)
|
s.dataDir.SetText(`C:\Users\Public\chub\profiles`)
|
||||||
@@ -149,8 +166,14 @@ func (s *Shell) OnPathSearch(searcher PathSearcher, invalidate func()) {
|
|||||||
s.invalidate = invalidate
|
s.invalidate = invalidate
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Shell) OnChooseDirectory(chooser DirectoryChooser, invalidate func()) {
|
||||||
|
s.directoryChooser = chooser
|
||||||
|
s.invalidate = invalidate
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Shell) Layout(gtx layout.Context) layout.Dimensions {
|
func (s *Shell) Layout(gtx layout.Context) layout.Dimensions {
|
||||||
s.consumeSearchResults()
|
s.consumeSearchResults()
|
||||||
|
s.consumeDirectoryResults()
|
||||||
for s.instancesClick.Clicked(gtx) {
|
for s.instancesClick.Clicked(gtx) {
|
||||||
s.page = pageInstances
|
s.page = pageInstances
|
||||||
}
|
}
|
||||||
@@ -170,7 +193,7 @@ func (s *Shell) Layout(gtx layout.Context) layout.Dimensions {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
for s.instanceDirPick.Clicked(gtx) {
|
for s.instanceDirPick.Clicked(gtx) {
|
||||||
s.formFeedback = "请选择实例 User Data Dir(原生文件夹选择器待接入)。"
|
s.chooseInstanceDirectory()
|
||||||
}
|
}
|
||||||
for s.backClick.Clicked(gtx) {
|
for s.backClick.Clicked(gtx) {
|
||||||
s.page = pageInstances
|
s.page = pageInstances
|
||||||
@@ -379,7 +402,7 @@ func (s *Shell) instanceDirField(gtx layout.Context) layout.Dimensions {
|
|||||||
return layout.Flex{Alignment: layout.Middle}.Layout(gtx,
|
return layout.Flex{Alignment: layout.Middle}.Layout(gtx,
|
||||||
layout.Flexed(1, material.Editor(s.theme, &s.instanceDir, "请输入或粘贴绝对路径").Layout),
|
layout.Flexed(1, material.Editor(s.theme, &s.instanceDir, "请输入或粘贴绝对路径").Layout),
|
||||||
layout.Rigid(layout.Spacer{Width: unit.Dp(8)}.Layout),
|
layout.Rigid(layout.Spacer{Width: unit.Dp(8)}.Layout),
|
||||||
layout.Rigid(material.Button(s.theme, &s.instanceDirPick, "选择路径").Layout),
|
layout.Rigid(material.Button(s.theme, &s.instanceDirPick, s.instanceDirButtonLabel()).Layout),
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
@@ -488,6 +511,66 @@ func browserDisplay(value string) string {
|
|||||||
return "Chrome"
|
return "Chrome"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Shell) chooseInstanceDirectory() {
|
||||||
|
if s.directoryPick.running {
|
||||||
|
s.formFeedback = "目录选择器已打开,请在系统窗口中选择或取消。"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if s.directoryChooser == nil {
|
||||||
|
s.formFeedback = "目录选择器尚未准备好。"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.directoryPick.request++
|
||||||
|
request := s.directoryPick.request
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
s.directoryPick.cancel = cancel
|
||||||
|
s.directoryPick.running = true
|
||||||
|
s.formFeedback = "正在打开目录选择器…"
|
||||||
|
go func() {
|
||||||
|
path, err := s.directoryChooser(ctx)
|
||||||
|
s.directoryResults <- directoryPickResult{request: request, path: path, err: err}
|
||||||
|
if s.invalidate != nil {
|
||||||
|
s.invalidate()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Shell) consumeDirectoryResults() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case result := <-s.directoryResults:
|
||||||
|
if !s.directoryPick.running || result.request != s.directoryPick.request {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
s.directoryPick.running = false
|
||||||
|
s.directoryPick.cancel = nil
|
||||||
|
if result.err != nil {
|
||||||
|
if errors.Is(result.err, context.Canceled) {
|
||||||
|
s.formFeedback = "已取消选择 User Data Dir。"
|
||||||
|
} else {
|
||||||
|
s.formFeedback = fmt.Sprintf("选择 User Data Dir 失败:%v", result.err)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if result.path == "" {
|
||||||
|
s.formFeedback = "未选择 User Data Dir。"
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
s.instanceDir.SetText(result.path)
|
||||||
|
s.formFeedback = "已更新 User Data Dir。"
|
||||||
|
default:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Shell) instanceDirButtonLabel() string {
|
||||||
|
if s.directoryPick.running {
|
||||||
|
return "选择中…"
|
||||||
|
}
|
||||||
|
return "选择路径"
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Shell) resetSettings() {
|
func (s *Shell) resetSettings() {
|
||||||
s.chromePath.SetText(`C:\Program Files\Google\Chrome\Application\chrome.exe`)
|
s.chromePath.SetText(`C:\Program Files\Google\Chrome\Application\chrome.exe`)
|
||||||
s.edgePath.SetText(`C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe`)
|
s.edgePath.SetText(`C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe`)
|
||||||
|
|||||||
@@ -115,3 +115,22 @@ func TestShellIgnoresCancelledPathSearchResult(t *testing.T) {
|
|||||||
t.Fatal("cancelled result overwrote the editor")
|
t.Fatal("cancelled result overwrote the editor")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestShellAppliesSelectedInstanceDirectory(t *testing.T) {
|
||||||
|
shell := NewShell(material.NewTheme())
|
||||||
|
shell.OnChooseDirectory(func(context.Context) (string, error) {
|
||||||
|
return `C:\profiles\new-instance`, nil
|
||||||
|
}, nil)
|
||||||
|
shell.chooseInstanceDirectory()
|
||||||
|
var result directoryPickResult
|
||||||
|
select {
|
||||||
|
case result = <-shell.directoryResults:
|
||||||
|
case <-time.After(time.Second):
|
||||||
|
t.Fatal("directory picker did not produce a result")
|
||||||
|
}
|
||||||
|
shell.directoryResults <- result
|
||||||
|
shell.consumeDirectoryResults()
|
||||||
|
if got := shell.instanceDir.Text(); got != `C:\profiles\new-instance` {
|
||||||
|
t.Fatalf("instance dir = %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user