fix: enable browser executable selection
This commit is contained in:
+127
-11
@@ -65,6 +65,8 @@ type PathSearcher func(context.Context, PathField, string) (string, error)
|
||||
|
||||
type DirectoryChooser func(context.Context) (string, error)
|
||||
|
||||
type ExecutableChooser func(context.Context) (string, error)
|
||||
|
||||
// ProxyOption is a non-secret proxy item exposed to the UI. Server has already
|
||||
// been validated as an unauthenticated scheme://host:port endpoint.
|
||||
type ProxyOption struct {
|
||||
@@ -210,6 +212,20 @@ type directoryPickResult struct {
|
||||
err error
|
||||
}
|
||||
|
||||
type executablePickState struct {
|
||||
request uint64
|
||||
cancel context.CancelFunc
|
||||
running bool
|
||||
field PathField
|
||||
}
|
||||
|
||||
type executablePickResult struct {
|
||||
field PathField
|
||||
request uint64
|
||||
path string
|
||||
err error
|
||||
}
|
||||
|
||||
type directoryPickTarget uint8
|
||||
|
||||
const (
|
||||
@@ -312,6 +328,7 @@ type Shell struct {
|
||||
editDiscardDrop widget.Clickable
|
||||
editDiscardStay widget.Clickable
|
||||
pathFeedback string
|
||||
pathFieldFeedback map[PathField]string
|
||||
formFeedback string
|
||||
editFeedback string
|
||||
proxyServer widget.Editor
|
||||
@@ -396,6 +413,9 @@ type Shell struct {
|
||||
invalidate func()
|
||||
searches map[PathField]*pathSearchState
|
||||
searchResults chan pathSearchResult
|
||||
executableChooser ExecutableChooser
|
||||
executablePick executablePickState
|
||||
executableResults chan executablePickResult
|
||||
directoryChooser DirectoryChooser
|
||||
directoryPick directoryPickState
|
||||
directoryResults chan directoryPickResult
|
||||
@@ -412,7 +432,7 @@ func NewShell(theme *material.Theme) *Shell {
|
||||
PathEdgeExecutable: {},
|
||||
PathDefaultUserData: {},
|
||||
PathLogDirectory: {},
|
||||
}, rowClicks: make(map[string]*widget.Clickable), startClicks: make(map[string]*widget.Clickable), editClicks: make(map[string]*widget.Clickable), deleteClicks: make(map[string]*widget.Clickable), proxyPickerChoices: make(map[string]*widget.Clickable), startStates: make(map[string]*instanceStartState), stopStates: make(map[string]*instanceStopState), pendingManagedExit: make(map[managedExitKey]ManagedInstanceExit), nextInstance: 4, startResults: make(chan instanceStartResult, 8), stopResults: make(chan instanceStopResult, 8), refreshResults: make(chan instanceRefreshResult, 1), tabsResults: make(chan instanceTabsResult, 1), searchResults: make(chan pathSearchResult, 8), directoryResults: make(chan directoryPickResult, 1)}
|
||||
}, pathFieldFeedback: make(map[PathField]string), rowClicks: make(map[string]*widget.Clickable), startClicks: make(map[string]*widget.Clickable), editClicks: make(map[string]*widget.Clickable), deleteClicks: make(map[string]*widget.Clickable), proxyPickerChoices: make(map[string]*widget.Clickable), startStates: make(map[string]*instanceStartState), stopStates: make(map[string]*instanceStopState), pendingManagedExit: make(map[managedExitKey]ManagedInstanceExit), nextInstance: 4, startResults: make(chan instanceStartResult, 8), stopResults: make(chan instanceStopResult, 8), refreshResults: make(chan instanceRefreshResult, 1), tabsResults: make(chan instanceTabsResult, 1), searchResults: make(chan pathSearchResult, 8), executableResults: make(chan executablePickResult, 1), directoryResults: make(chan directoryPickResult, 1)}
|
||||
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`)
|
||||
@@ -543,6 +563,11 @@ func (s *Shell) OnPathSearch(searcher PathSearcher, invalidate func()) {
|
||||
s.invalidate = invalidate
|
||||
}
|
||||
|
||||
func (s *Shell) OnChooseExecutable(chooser ExecutableChooser, invalidate func()) {
|
||||
s.executableChooser = chooser
|
||||
s.invalidate = invalidate
|
||||
}
|
||||
|
||||
func (s *Shell) OnChooseDirectory(chooser DirectoryChooser, invalidate func()) {
|
||||
s.directoryChooser = chooser
|
||||
s.invalidate = invalidate
|
||||
@@ -550,6 +575,7 @@ func (s *Shell) OnChooseDirectory(chooser DirectoryChooser, invalidate func()) {
|
||||
|
||||
func (s *Shell) Layout(gtx layout.Context) layout.Dimensions {
|
||||
s.consumeSearchResults()
|
||||
s.consumeExecutableResults()
|
||||
s.consumeDirectoryResults()
|
||||
s.consumeStartResults()
|
||||
s.consumeStopResults()
|
||||
@@ -1847,13 +1873,13 @@ func statusLabel(theme *material.Theme, status string) layout.Widget {
|
||||
|
||||
func (s *Shell) settings(gtx layout.Context) layout.Dimensions {
|
||||
for s.chromePick.Clicked(gtx) {
|
||||
s.pathFeedback = "请选择 Chrome 可执行文件"
|
||||
s.chooseExecutable(PathChromeExecutable)
|
||||
}
|
||||
for s.chromeSearch.Clicked(gtx) {
|
||||
s.togglePathSearch(PathChromeExecutable)
|
||||
}
|
||||
for s.edgePick.Clicked(gtx) {
|
||||
s.pathFeedback = "请选择 Edge 可执行文件"
|
||||
s.chooseExecutable(PathEdgeExecutable)
|
||||
}
|
||||
for s.edgeSearch.Clicked(gtx) {
|
||||
s.togglePathSearch(PathEdgeExecutable)
|
||||
@@ -2044,7 +2070,7 @@ func (s *Shell) pathField(field PathField, label string, editor *widget.Editor,
|
||||
return s.settingsField(func(gtx layout.Context) layout.Dimensions {
|
||||
pathActions := func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Flex{Alignment: layout.Middle}.Layout(gtx,
|
||||
layout.Rigid(material.Button(s.theme, pick, "选择").Layout),
|
||||
layout.Rigid(material.Button(s.theme, pick, s.pathPickButtonLabel(field)).Layout),
|
||||
layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout),
|
||||
layout.Rigid(material.Button(s.theme, search, s.searchButtonLabel(field)).Layout),
|
||||
)
|
||||
@@ -2068,6 +2094,12 @@ func (s *Shell) pathField(field PathField, label string, editor *widget.Editor,
|
||||
)
|
||||
}))
|
||||
}
|
||||
if feedback := s.pathFieldFeedback[field]; feedback != "" {
|
||||
children = append(children,
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||
layout.Rigid(s.settingsFeedback(feedback)),
|
||||
)
|
||||
}
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx, children...)
|
||||
})(gtx)
|
||||
}
|
||||
@@ -2425,11 +2457,11 @@ func (s *Shell) togglePathSearch(field PathField) {
|
||||
state.cancel = nil
|
||||
state.running = false
|
||||
state.request++
|
||||
s.pathFeedback = fmt.Sprintf("已取消%s搜索。", pathFieldLabel(field))
|
||||
s.setPathFieldFeedback(field, fmt.Sprintf("已取消%s搜索。", pathFieldLabel(field)))
|
||||
return
|
||||
}
|
||||
if s.pathSearcher == nil {
|
||||
s.pathFeedback = fmt.Sprintf("%s搜索服务尚未准备好。", pathFieldLabel(field))
|
||||
s.setPathFieldFeedback(field, fmt.Sprintf("%s搜索服务尚未准备好。", pathFieldLabel(field)))
|
||||
return
|
||||
}
|
||||
state.request++
|
||||
@@ -2438,7 +2470,7 @@ func (s *Shell) togglePathSearch(field PathField) {
|
||||
state.cancel = cancel
|
||||
state.running = true
|
||||
current := s.pathEditor(field).Text()
|
||||
s.pathFeedback = fmt.Sprintf("正在搜索%s…", pathFieldLabel(field))
|
||||
s.setPathFieldFeedback(field, 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}
|
||||
@@ -2460,18 +2492,88 @@ func (s *Shell) consumeSearchResults() {
|
||||
state.cancel = nil
|
||||
if result.err != nil {
|
||||
if errors.Is(result.err, context.Canceled) {
|
||||
s.pathFeedback = fmt.Sprintf("已取消%s搜索。", pathFieldLabel(result.field))
|
||||
s.setPathFieldFeedback(result.field, fmt.Sprintf("已取消%s搜索。", pathFieldLabel(result.field)))
|
||||
} else {
|
||||
s.pathFeedback = fmt.Sprintf("搜索%s失败:%v", pathFieldLabel(result.field), result.err)
|
||||
s.setPathFieldFeedback(result.field, fmt.Sprintf("搜索%s失败,请重试或手动输入路径。", pathFieldLabel(result.field)))
|
||||
}
|
||||
continue
|
||||
}
|
||||
if result.path == "" {
|
||||
s.pathFeedback = fmt.Sprintf("未找到%s。", pathFieldLabel(result.field))
|
||||
s.setPathFieldFeedback(result.field, fmt.Sprintf("未找到%s。", pathFieldLabel(result.field)))
|
||||
continue
|
||||
}
|
||||
current := strings.TrimSpace(s.pathEditor(result.field).Text())
|
||||
s.pathEditor(result.field).SetText(result.path)
|
||||
if current != "" && strings.EqualFold(filepath.Clean(current), filepath.Clean(result.path)) {
|
||||
s.setPathFieldFeedback(result.field, fmt.Sprintf("已确认%s:当前路径可用。", pathFieldLabel(result.field)))
|
||||
} else {
|
||||
s.setPathFieldFeedback(result.field, fmt.Sprintf("已找到%s,已更新路径。", pathFieldLabel(result.field)))
|
||||
}
|
||||
default:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Shell) chooseExecutable(field PathField) {
|
||||
if field != PathChromeExecutable && field != PathEdgeExecutable {
|
||||
return
|
||||
}
|
||||
if s.executablePick.running {
|
||||
if s.executablePick.field == field {
|
||||
s.setPathFieldFeedback(field, fmt.Sprintf("正在打开%s选择器,请在系统窗口中选择或取消。", pathFieldLabel(field)))
|
||||
} else {
|
||||
s.setPathFieldFeedback(field, "另一个浏览器可执行文件选择器已打开,请先完成或取消。")
|
||||
}
|
||||
return
|
||||
}
|
||||
if s.executableChooser == nil {
|
||||
s.setPathFieldFeedback(field, fmt.Sprintf("%s选择器尚未准备好。", pathFieldLabel(field)))
|
||||
return
|
||||
}
|
||||
s.executablePick.request++
|
||||
request := s.executablePick.request
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
s.executablePick.cancel = cancel
|
||||
s.executablePick.running = true
|
||||
s.executablePick.field = field
|
||||
s.setPathFieldFeedback(field, fmt.Sprintf("正在打开%s选择器…", pathFieldLabel(field)))
|
||||
go func() {
|
||||
path, err := s.executableChooser(ctx)
|
||||
s.executableResults <- executablePickResult{field: field, request: request, path: path, err: err}
|
||||
if s.invalidate != nil {
|
||||
s.invalidate()
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (s *Shell) consumeExecutableResults() {
|
||||
for {
|
||||
select {
|
||||
case result := <-s.executableResults:
|
||||
if !s.executablePick.running || result.request != s.executablePick.request || result.field != s.executablePick.field {
|
||||
continue
|
||||
}
|
||||
cancel := s.executablePick.cancel
|
||||
s.executablePick.running = false
|
||||
s.executablePick.cancel = nil
|
||||
if cancel != nil {
|
||||
cancel()
|
||||
}
|
||||
if result.err != nil {
|
||||
if errors.Is(result.err, context.Canceled) {
|
||||
s.setPathFieldFeedback(result.field, fmt.Sprintf("已取消选择%s。", pathFieldLabel(result.field)))
|
||||
} else {
|
||||
s.setPathFieldFeedback(result.field, fmt.Sprintf("选择%s失败,请重试或手动输入路径。", pathFieldLabel(result.field)))
|
||||
}
|
||||
continue
|
||||
}
|
||||
if strings.TrimSpace(result.path) == "" {
|
||||
s.setPathFieldFeedback(result.field, fmt.Sprintf("未选择%s。", pathFieldLabel(result.field)))
|
||||
continue
|
||||
}
|
||||
s.pathEditor(result.field).SetText(result.path)
|
||||
s.pathFeedback = fmt.Sprintf("已找到%s。", pathFieldLabel(result.field))
|
||||
s.setPathFieldFeedback(result.field, fmt.Sprintf("已选择%s。", pathFieldLabel(result.field)))
|
||||
default:
|
||||
return
|
||||
}
|
||||
@@ -2485,6 +2587,20 @@ func (s *Shell) searchButtonLabel(field PathField) string {
|
||||
return "搜索"
|
||||
}
|
||||
|
||||
func (s *Shell) pathPickButtonLabel(field PathField) string {
|
||||
if s.executablePick.running && s.executablePick.field == field {
|
||||
return "选择中…"
|
||||
}
|
||||
return "选择"
|
||||
}
|
||||
|
||||
func (s *Shell) setPathFieldFeedback(field PathField, message string) {
|
||||
if s.pathFieldFeedback == nil {
|
||||
s.pathFieldFeedback = make(map[PathField]string)
|
||||
}
|
||||
s.pathFieldFeedback[field] = message
|
||||
}
|
||||
|
||||
func (s *Shell) pathEditor(field PathField) *widget.Editor {
|
||||
switch field {
|
||||
case PathChromeExecutable:
|
||||
|
||||
Reference in New Issue
Block a user