feat: add cancellable settings path search
This commit is contained in:
+240
-56
@@ -1,6 +1,9 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"image/color"
|
||||
|
||||
"gioui.org/layout"
|
||||
@@ -17,6 +20,30 @@ const (
|
||||
pageCreate
|
||||
)
|
||||
|
||||
type PathField string
|
||||
|
||||
const (
|
||||
PathChromeExecutable PathField = "chrome_executable"
|
||||
PathEdgeExecutable PathField = "edge_executable"
|
||||
PathDefaultUserData PathField = "default_user_data_dir"
|
||||
PathLogDirectory PathField = "log_directory"
|
||||
)
|
||||
|
||||
type PathSearcher func(context.Context, PathField, string) (string, error)
|
||||
|
||||
type pathSearchState struct {
|
||||
request uint64
|
||||
cancel context.CancelFunc
|
||||
running bool
|
||||
}
|
||||
|
||||
type pathSearchResult struct {
|
||||
field PathField
|
||||
request uint64
|
||||
path string
|
||||
err error
|
||||
}
|
||||
|
||||
// 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.
|
||||
type InstanceRow struct {
|
||||
@@ -40,24 +67,23 @@ type Shell struct {
|
||||
theme *material.Theme
|
||||
page page
|
||||
|
||||
instancesClick widget.Clickable
|
||||
settingsClick widget.Clickable
|
||||
newClick widget.Clickable
|
||||
startClick widget.Clickable
|
||||
createClick widget.Clickable
|
||||
backClick widget.Clickable
|
||||
chromeKind widget.Clickable
|
||||
edgeKind widget.Clickable
|
||||
chromePick widget.Clickable
|
||||
chromeSearch widget.Clickable
|
||||
edgePick widget.Clickable
|
||||
edgeSearch widget.Clickable
|
||||
dataPick widget.Clickable
|
||||
dataSearch widget.Clickable
|
||||
logPick widget.Clickable
|
||||
logSearch widget.Clickable
|
||||
saveClick widget.Clickable
|
||||
cancelClick widget.Clickable
|
||||
instancesClick widget.Clickable
|
||||
settingsClick widget.Clickable
|
||||
newClick widget.Clickable
|
||||
startClick widget.Clickable
|
||||
createClick widget.Clickable
|
||||
backClick widget.Clickable
|
||||
instanceDirPick widget.Clickable
|
||||
chromePick widget.Clickable
|
||||
chromeSearch widget.Clickable
|
||||
edgePick widget.Clickable
|
||||
edgeSearch widget.Clickable
|
||||
dataPick widget.Clickable
|
||||
dataSearch widget.Clickable
|
||||
logPick widget.Clickable
|
||||
logSearch widget.Clickable
|
||||
saveClick widget.Clickable
|
||||
cancelClick widget.Clickable
|
||||
|
||||
chromePath widget.Editor
|
||||
edgePath widget.Editor
|
||||
@@ -67,12 +93,17 @@ type Shell struct {
|
||||
instanceName widget.Editor
|
||||
instanceDir widget.Editor
|
||||
instanceURL widget.Editor
|
||||
newBrowser string
|
||||
browserKind widget.Enum
|
||||
pathFeedback string
|
||||
formFeedback string
|
||||
|
||||
list widget.List
|
||||
rows []InstanceRow
|
||||
onSave func(SettingsState)
|
||||
list widget.List
|
||||
rows []InstanceRow
|
||||
onSave func(SettingsState)
|
||||
pathSearcher PathSearcher
|
||||
invalidate func()
|
||||
searches map[PathField]*pathSearchState
|
||||
searchResults chan pathSearchResult
|
||||
}
|
||||
|
||||
func NewShell(theme *material.Theme) *Shell {
|
||||
@@ -81,13 +112,18 @@ func NewShell(theme *material.Theme) *Shell {
|
||||
{Name: "广告投放", Browser: "Edge", Profile: "Profile 2", Status: "启动中"},
|
||||
{Name: "素材采集", Browser: "Chrome", Profile: "Default", Status: "外部占用"},
|
||||
{Name: "备用环境", Browser: "Edge", Profile: "Profile 1", Status: "已退出"},
|
||||
}}
|
||||
}, searches: map[PathField]*pathSearchState{
|
||||
PathChromeExecutable: {},
|
||||
PathEdgeExecutable: {},
|
||||
PathDefaultUserData: {},
|
||||
PathLogDirectory: {},
|
||||
}, searchResults: make(chan pathSearchResult, 8)}
|
||||
s.chromePath.SetText(`C:\Program Files\Google\Chrome\Application\chrome.exe`)
|
||||
s.edgePath.SetText(`C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe`)
|
||||
s.dataDir.SetText(`C:\Users\Public\chub\profiles`)
|
||||
s.logDir.SetText(`C:\Users\Public\chub\logs`)
|
||||
s.closeOnExit.Value = true
|
||||
s.newBrowser = "Chrome"
|
||||
s.browserKind.Value = "chrome"
|
||||
return s
|
||||
}
|
||||
|
||||
@@ -108,7 +144,13 @@ func (s *Shell) SetSettings(value SettingsState) {
|
||||
|
||||
func (s *Shell) OnSave(fn func(SettingsState)) { s.onSave = fn }
|
||||
|
||||
func (s *Shell) OnPathSearch(searcher PathSearcher, invalidate func()) {
|
||||
s.pathSearcher = searcher
|
||||
s.invalidate = invalidate
|
||||
}
|
||||
|
||||
func (s *Shell) Layout(gtx layout.Context) layout.Dimensions {
|
||||
s.consumeSearchResults()
|
||||
for s.instancesClick.Clicked(gtx) {
|
||||
s.page = pageInstances
|
||||
}
|
||||
@@ -120,32 +162,48 @@ func (s *Shell) Layout(gtx layout.Context) layout.Dimensions {
|
||||
}
|
||||
for s.createClick.Clicked(gtx) {
|
||||
if name := s.instanceName.Text(); name != "" {
|
||||
s.rows = append(s.rows, InstanceRow{Name: name, Browser: s.newBrowser, Profile: "Default", Status: "已退出"})
|
||||
s.rows = append(s.rows, InstanceRow{Name: name, Browser: browserDisplay(s.browserKind.Value), Profile: "Default", Status: "已退出"})
|
||||
s.page = pageInstances
|
||||
s.formFeedback = ""
|
||||
} else {
|
||||
s.formFeedback = "请输入实例名称后再创建。"
|
||||
}
|
||||
}
|
||||
for s.instanceDirPick.Clicked(gtx) {
|
||||
s.formFeedback = "请选择实例 User Data Dir(原生文件夹选择器待接入)。"
|
||||
}
|
||||
for s.backClick.Clicked(gtx) {
|
||||
s.page = pageInstances
|
||||
}
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
layout.Rigid(s.header),
|
||||
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
|
||||
layout.Rigid(s.sidebar),
|
||||
layout.Flexed(1, func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Inset{Top: unit.Dp(20), Bottom: unit.Dp(20), Left: unit.Dp(28), Right: unit.Dp(28)}.Layout(gtx, s.content)
|
||||
return layout.Inset{Top: unit.Dp(24), Bottom: unit.Dp(24), Left: unit.Dp(28), Right: unit.Dp(28)}.Layout(gtx, s.content)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
func (s *Shell) header(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Inset{Top: unit.Dp(18), Bottom: unit.Dp(10), Left: unit.Dp(28), Right: unit.Dp(28)}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Flex{Alignment: layout.Middle}.Layout(gtx,
|
||||
layout.Flexed(1, material.H5(s.theme, "Chub 浏览器管理").Layout),
|
||||
layout.Rigid(material.Button(s.theme, &s.instancesClick, "实例").Layout),
|
||||
layout.Rigid(layout.Spacer{Width: unit.Dp(8)}.Layout),
|
||||
layout.Rigid(material.Button(s.theme, &s.settingsClick, "设置").Layout),
|
||||
func (s *Shell) sidebar(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Inset{Top: unit.Dp(24), Left: unit.Dp(16), Right: unit.Dp(16)}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
layout.Rigid(s.navButton(&s.instancesClick, "实例", s.page == pageInstances || s.page == pageCreate)),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
|
||||
layout.Rigid(s.navButton(&s.settingsClick, "设置", s.page == pageSettings)),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Shell) navButton(click *widget.Clickable, label string, selected bool) layout.Widget {
|
||||
return func(gtx layout.Context) layout.Dimensions {
|
||||
style := material.Button(s.theme, click, label)
|
||||
if selected {
|
||||
style.Background = color.NRGBA{R: 214, G: 228, B: 255, A: 255}
|
||||
style.Color = color.NRGBA{R: 0, G: 76, B: 153, A: 255}
|
||||
}
|
||||
return style.Layout(gtx)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Shell) content(gtx layout.Context) layout.Dimensions {
|
||||
if s.page == pageSettings {
|
||||
return s.settings(gtx)
|
||||
@@ -200,25 +258,25 @@ func (s *Shell) settings(gtx layout.Context) layout.Dimensions {
|
||||
s.pathFeedback = "请选择 Chrome 可执行文件"
|
||||
}
|
||||
for s.chromeSearch.Clicked(gtx) {
|
||||
s.pathFeedback = "正在搜索 Chrome 可执行文件…"
|
||||
s.togglePathSearch(PathChromeExecutable)
|
||||
}
|
||||
for s.edgePick.Clicked(gtx) {
|
||||
s.pathFeedback = "请选择 Edge 可执行文件"
|
||||
}
|
||||
for s.edgeSearch.Clicked(gtx) {
|
||||
s.pathFeedback = "正在搜索 Edge 可执行文件…"
|
||||
s.togglePathSearch(PathEdgeExecutable)
|
||||
}
|
||||
for s.dataPick.Clicked(gtx) {
|
||||
s.pathFeedback = "请选择默认 User Data Dir"
|
||||
}
|
||||
for s.dataSearch.Clicked(gtx) {
|
||||
s.pathFeedback = "正在搜索默认 User Data Dir…"
|
||||
s.togglePathSearch(PathDefaultUserData)
|
||||
}
|
||||
for s.logPick.Clicked(gtx) {
|
||||
s.pathFeedback = "请选择日志目录"
|
||||
}
|
||||
for s.logSearch.Clicked(gtx) {
|
||||
s.pathFeedback = "正在搜索日志目录…"
|
||||
s.togglePathSearch(PathLogDirectory)
|
||||
}
|
||||
for s.saveClick.Clicked(gtx) {
|
||||
if s.onSave != nil {
|
||||
@@ -232,13 +290,13 @@ func (s *Shell) settings(gtx layout.Context) layout.Dimensions {
|
||||
layout.Rigid(material.H4(s.theme, "设置").Layout),
|
||||
layout.Rigid(material.Body1(s.theme, "配置浏览器可执行文件、默认目录和本地运行行为").Layout),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(18)}.Layout),
|
||||
layout.Rigid(s.pathField("Chrome 可执行文件", &s.chromePath, &s.chromePick, &s.chromeSearch)),
|
||||
layout.Rigid(s.pathField(PathChromeExecutable, "Chrome 可执行文件", &s.chromePath, &s.chromePick, &s.chromeSearch)),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout),
|
||||
layout.Rigid(s.pathField("Edge 可执行文件", &s.edgePath, &s.edgePick, &s.edgeSearch)),
|
||||
layout.Rigid(s.pathField(PathEdgeExecutable, "Edge 可执行文件", &s.edgePath, &s.edgePick, &s.edgeSearch)),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout),
|
||||
layout.Rigid(s.pathField("默认 User Data Dir", &s.dataDir, &s.dataPick, &s.dataSearch)),
|
||||
layout.Rigid(s.pathField(PathDefaultUserData, "默认 User Data Dir", &s.dataDir, &s.dataPick, &s.dataSearch)),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout),
|
||||
layout.Rigid(s.pathField("日志目录", &s.logDir, &s.logPick, &s.logSearch)),
|
||||
layout.Rigid(s.pathField(PathLogDirectory, "日志目录", &s.logDir, &s.logPick, &s.logSearch)),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||
layout.Rigid(material.Caption(s.theme, s.pathFeedback).Layout),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout),
|
||||
@@ -254,7 +312,7 @@ func (s *Shell) settings(gtx layout.Context) layout.Dimensions {
|
||||
)
|
||||
}
|
||||
|
||||
func (s *Shell) pathField(label string, editor *widget.Editor, pick, search *widget.Clickable) layout.Widget {
|
||||
func (s *Shell) pathField(field PathField, label string, editor *widget.Editor, pick, search *widget.Clickable) layout.Widget {
|
||||
return func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Flex{Alignment: layout.Middle}.Layout(gtx,
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
@@ -264,35 +322,32 @@ func (s *Shell) pathField(label string, editor *widget.Editor, pick, search *wid
|
||||
layout.Rigid(layout.Spacer{Width: unit.Dp(8)}.Layout),
|
||||
layout.Rigid(material.Button(s.theme, pick, "选择").Layout),
|
||||
layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout),
|
||||
layout.Rigid(material.Button(s.theme, search, "搜索").Layout),
|
||||
layout.Rigid(material.Button(s.theme, search, s.searchButtonLabel(field)).Layout),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Shell) createInstance(gtx layout.Context) layout.Dimensions {
|
||||
for s.chromeKind.Clicked(gtx) {
|
||||
s.newBrowser = "Chrome"
|
||||
}
|
||||
for s.edgeKind.Clicked(gtx) {
|
||||
s.newBrowser = "Edge"
|
||||
}
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
layout.Rigid(material.H4(s.theme, "新建实例").Layout),
|
||||
layout.Rigid(material.Body1(s.theme, "为一个独立的 Chrome 或 Edge User Data Dir 创建托管实例").Layout),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(18)}.Layout),
|
||||
layout.Rigid(material.Editor(s.theme, &s.instanceName, "实例名称").Layout),
|
||||
layout.Rigid(s.formField("实例名称", "用于在实例列表中识别此浏览器环境", &s.instanceName)),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout),
|
||||
layout.Rigid(material.Editor(s.theme, &s.instanceDir, "User Data Dir(绝对路径)").Layout),
|
||||
layout.Rigid(s.instanceDirField),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout),
|
||||
layout.Rigid(material.Editor(s.theme, &s.instanceURL, "启动 URL(可选)").Layout),
|
||||
layout.Rigid(s.formField("启动 URL(可选)", "仅支持 http/https;留空时启动浏览器默认页", &s.instanceURL)),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(14)}.Layout),
|
||||
layout.Rigid(material.Body2(s.theme, "浏览器类型").Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Flex{Alignment: layout.Middle}.Layout(gtx,
|
||||
layout.Rigid(material.Button(s.theme, &s.chromeKind, "Chrome").Layout),
|
||||
layout.Rigid(material.RadioButton(s.theme, &s.browserKind, "chrome", "Chrome").Layout),
|
||||
layout.Rigid(layout.Spacer{Width: unit.Dp(8)}.Layout),
|
||||
layout.Rigid(material.Button(s.theme, &s.edgeKind, "Edge").Layout),
|
||||
layout.Rigid(material.RadioButton(s.theme, &s.browserKind, "edge", "Edge").Layout),
|
||||
)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||
layout.Rigid(material.Caption(s.theme, s.formFeedback).Layout),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(18)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Flex{Alignment: layout.Middle}.Layout(gtx,
|
||||
@@ -304,6 +359,135 @@ func (s *Shell) createInstance(gtx layout.Context) layout.Dimensions {
|
||||
)
|
||||
}
|
||||
|
||||
func (s *Shell) formField(label, help string, editor *widget.Editor) layout.Widget {
|
||||
return func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
layout.Rigid(material.Body2(s.theme, label).Layout),
|
||||
layout.Rigid(material.Caption(s.theme, help).Layout),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(4)}.Layout),
|
||||
layout.Rigid(material.Editor(s.theme, editor, "请输入").Layout),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Shell) instanceDirField(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
layout.Rigid(material.Body2(s.theme, "User Data Dir").Layout),
|
||||
layout.Rigid(material.Caption(s.theme, "浏览器独立数据目录;必须是绝对路径").Layout),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(4)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Flex{Alignment: layout.Middle}.Layout(gtx,
|
||||
layout.Flexed(1, material.Editor(s.theme, &s.instanceDir, "请输入或粘贴绝对路径").Layout),
|
||||
layout.Rigid(layout.Spacer{Width: unit.Dp(8)}.Layout),
|
||||
layout.Rigid(material.Button(s.theme, &s.instanceDirPick, "选择路径").Layout),
|
||||
)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
func (s *Shell) togglePathSearch(field PathField) {
|
||||
state := s.searches[field]
|
||||
if state == nil {
|
||||
return
|
||||
}
|
||||
if state.running {
|
||||
state.cancel()
|
||||
state.cancel = nil
|
||||
state.running = false
|
||||
state.request++
|
||||
s.pathFeedback = fmt.Sprintf("已取消%s搜索。", pathFieldLabel(field))
|
||||
return
|
||||
}
|
||||
if s.pathSearcher == nil {
|
||||
s.pathFeedback = fmt.Sprintf("%s搜索服务尚未准备好。", pathFieldLabel(field))
|
||||
return
|
||||
}
|
||||
state.request++
|
||||
request := state.request
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
state.cancel = cancel
|
||||
state.running = true
|
||||
current := s.pathEditor(field).Text()
|
||||
s.pathFeedback = fmt.Sprintf("正在搜索%s…", pathFieldLabel(field))
|
||||
go func() {
|
||||
path, err := s.pathSearcher(ctx, field, current)
|
||||
s.searchResults <- pathSearchResult{field: field, request: request, path: path, err: err}
|
||||
if s.invalidate != nil {
|
||||
s.invalidate()
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (s *Shell) consumeSearchResults() {
|
||||
for {
|
||||
select {
|
||||
case result := <-s.searchResults:
|
||||
state := s.searches[result.field]
|
||||
if state == nil || !state.running || result.request != state.request {
|
||||
continue
|
||||
}
|
||||
state.running = false
|
||||
state.cancel = nil
|
||||
if result.err != nil {
|
||||
if errors.Is(result.err, context.Canceled) {
|
||||
s.pathFeedback = fmt.Sprintf("已取消%s搜索。", pathFieldLabel(result.field))
|
||||
} else {
|
||||
s.pathFeedback = fmt.Sprintf("搜索%s失败:%v", pathFieldLabel(result.field), result.err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if result.path == "" {
|
||||
s.pathFeedback = fmt.Sprintf("未找到%s。", pathFieldLabel(result.field))
|
||||
continue
|
||||
}
|
||||
s.pathEditor(result.field).SetText(result.path)
|
||||
s.pathFeedback = fmt.Sprintf("已找到%s。", pathFieldLabel(result.field))
|
||||
default:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Shell) searchButtonLabel(field PathField) string {
|
||||
if state := s.searches[field]; state != nil && state.running {
|
||||
return "取消"
|
||||
}
|
||||
return "搜索"
|
||||
}
|
||||
|
||||
func (s *Shell) pathEditor(field PathField) *widget.Editor {
|
||||
switch field {
|
||||
case PathChromeExecutable:
|
||||
return &s.chromePath
|
||||
case PathEdgeExecutable:
|
||||
return &s.edgePath
|
||||
case PathDefaultUserData:
|
||||
return &s.dataDir
|
||||
default:
|
||||
return &s.logDir
|
||||
}
|
||||
}
|
||||
|
||||
func pathFieldLabel(field PathField) string {
|
||||
switch field {
|
||||
case PathChromeExecutable:
|
||||
return "Chrome 可执行文件"
|
||||
case PathEdgeExecutable:
|
||||
return "Edge 可执行文件"
|
||||
case PathDefaultUserData:
|
||||
return "默认 User Data Dir"
|
||||
default:
|
||||
return "日志目录"
|
||||
}
|
||||
}
|
||||
|
||||
func browserDisplay(value string) string {
|
||||
if value == "edge" {
|
||||
return "Edge"
|
||||
}
|
||||
return "Chrome"
|
||||
}
|
||||
|
||||
func (s *Shell) resetSettings() {
|
||||
s.chromePath.SetText(`C:\Program Files\Google\Chrome\Application\chrome.exe`)
|
||||
s.edgePath.SetText(`C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe`)
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gioui.org/widget/material"
|
||||
)
|
||||
@@ -19,6 +21,51 @@ func TestShellStartsWithSemanticInstanceStatuses(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestShellCancelsOnlyTheActivePathSearch(t *testing.T) {
|
||||
shell := NewShell(material.NewTheme())
|
||||
started := make(chan struct{}, 1)
|
||||
shell.OnPathSearch(func(ctx context.Context, field PathField, current string) (string, error) {
|
||||
started <- struct{}{}
|
||||
<-ctx.Done()
|
||||
return "", ctx.Err()
|
||||
}, nil)
|
||||
shell.togglePathSearch(PathChromeExecutable)
|
||||
if got := shell.searchButtonLabel(PathChromeExecutable); got != "取消" {
|
||||
t.Fatalf("chrome search label = %q, want 取消", got)
|
||||
}
|
||||
if got := shell.searchButtonLabel(PathEdgeExecutable); got != "搜索" {
|
||||
t.Fatalf("edge search label = %q, want 搜索", got)
|
||||
}
|
||||
select {
|
||||
case <-started:
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("search did not start")
|
||||
}
|
||||
shell.togglePathSearch(PathChromeExecutable)
|
||||
if got := shell.searchButtonLabel(PathChromeExecutable); got != "搜索" {
|
||||
t.Fatalf("chrome search label = %q after cancel, want 搜索", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestShellAppliesLatestPathSearchResult(t *testing.T) {
|
||||
shell := NewShell(material.NewTheme())
|
||||
shell.OnPathSearch(func(context.Context, PathField, string) (string, error) {
|
||||
return `C:\Browser\chrome.exe`, nil
|
||||
}, nil)
|
||||
shell.togglePathSearch(PathChromeExecutable)
|
||||
var result pathSearchResult
|
||||
select {
|
||||
case result = <-shell.searchResults:
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("search did not produce a result")
|
||||
}
|
||||
shell.searchResults <- result
|
||||
shell.consumeSearchResults()
|
||||
if got := shell.chromePath.Text(); got != `C:\Browser\chrome.exe` {
|
||||
t.Fatalf("chrome path = %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestShellSettingsCanBeRestored(t *testing.T) {
|
||||
shell := NewShell(material.NewTheme())
|
||||
shell.SetSettings(SettingsState{ChromePath: "chrome", EdgePath: "edge", DefaultDir: "profiles", LogDir: "logs", CloseOnExit: false})
|
||||
@@ -26,3 +73,45 @@ func TestShellSettingsCanBeRestored(t *testing.T) {
|
||||
t.Fatalf("settings were not restored")
|
||||
}
|
||||
}
|
||||
|
||||
func TestShellKeepsSettingsStateAcrossPageSwitch(t *testing.T) {
|
||||
shell := NewShell(material.NewTheme())
|
||||
shell.page = pageSettings
|
||||
shell.dataDir.SetText(`D:\profiles\current`)
|
||||
shell.pathFeedback = "正在搜索默认 User Data Dir…"
|
||||
shell.page = pageInstances
|
||||
shell.page = pageSettings
|
||||
if got := shell.dataDir.Text(); got != `D:\profiles\current` {
|
||||
t.Fatalf("data dir = %q", got)
|
||||
}
|
||||
if got := shell.pathFeedback; got != "正在搜索默认 User Data Dir…" {
|
||||
t.Fatalf("path feedback = %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestShellIgnoresCancelledPathSearchResult(t *testing.T) {
|
||||
shell := NewShell(material.NewTheme())
|
||||
started := make(chan struct{}, 1)
|
||||
shell.OnPathSearch(func(ctx context.Context, field PathField, current string) (string, error) {
|
||||
started <- struct{}{}
|
||||
<-ctx.Done()
|
||||
return `C:\stale\chrome.exe`, nil
|
||||
}, nil)
|
||||
shell.togglePathSearch(PathChromeExecutable)
|
||||
select {
|
||||
case <-started:
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("search did not start")
|
||||
}
|
||||
shell.togglePathSearch(PathChromeExecutable)
|
||||
select {
|
||||
case result := <-shell.searchResults:
|
||||
shell.searchResults <- result
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("cancelled search did not return")
|
||||
}
|
||||
shell.consumeSearchResults()
|
||||
if got := shell.chromePath.Text(); got == `C:\stale\chrome.exe` {
|
||||
t.Fatal("cancelled result overwrote the editor")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user