feat: add compact instance actions
This commit is contained in:
+68
-12
@@ -10,6 +10,7 @@ import (
|
||||
"gioui.org/unit"
|
||||
"gioui.org/widget"
|
||||
"gioui.org/widget/material"
|
||||
"golang.org/x/exp/shiny/materialdesign/icons"
|
||||
)
|
||||
|
||||
type page uint8
|
||||
@@ -21,11 +22,16 @@ const (
|
||||
)
|
||||
|
||||
const (
|
||||
instanceNameColumnWeight = 25
|
||||
instanceNameColumnWeight = 24
|
||||
instanceBrowserColumnWeight = 12
|
||||
instanceDirectoryColumnWeight = 39
|
||||
instanceStatusColumnWeight = 14
|
||||
instanceStartColumnWeight = 10
|
||||
instanceDirectoryColumnWeight = 38
|
||||
instanceStatusColumnWeight = 12
|
||||
instanceActionColumnWeight = 14
|
||||
)
|
||||
|
||||
var (
|
||||
instanceStartIcon = mustIcon(icons.AVPlayArrow)
|
||||
instanceDeleteIcon = mustIcon(icons.ActionDelete)
|
||||
)
|
||||
|
||||
type PathField string
|
||||
@@ -123,6 +129,7 @@ type Shell struct {
|
||||
list widget.List
|
||||
rows []InstanceRow
|
||||
startClicks map[string]*widget.Clickable
|
||||
deleteClicks map[string]*widget.Clickable
|
||||
nextInstance uint64
|
||||
instanceFeedback string
|
||||
onSave func(SettingsState)
|
||||
@@ -146,7 +153,7 @@ func NewShell(theme *material.Theme) *Shell {
|
||||
PathEdgeExecutable: {},
|
||||
PathDefaultUserData: {},
|
||||
PathLogDirectory: {},
|
||||
}, startClicks: make(map[string]*widget.Clickable), nextInstance: 4, searchResults: make(chan pathSearchResult, 8), directoryResults: make(chan directoryPickResult, 1)}
|
||||
}, startClicks: make(map[string]*widget.Clickable), deleteClicks: make(map[string]*widget.Clickable), nextInstance: 4, searchResults: make(chan pathSearchResult, 8), 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`)
|
||||
@@ -222,6 +229,11 @@ func (s *Shell) Layout(gtx layout.Context) layout.Dimensions {
|
||||
s.requestStart(row.ID)
|
||||
}
|
||||
}
|
||||
for _, row := range append([]InstanceRow(nil), s.rows...) {
|
||||
for s.deleteClickFor(row.ID).Clicked(gtx) {
|
||||
s.deleteInstance(row.ID)
|
||||
}
|
||||
}
|
||||
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
|
||||
layout.Rigid(s.sidebar),
|
||||
layout.Flexed(1, func(gtx layout.Context) layout.Dimensions {
|
||||
@@ -279,7 +291,7 @@ func (s *Shell) instances(gtx layout.Context) layout.Dimensions {
|
||||
layout.Flexed(instanceBrowserColumnWeight, material.Body2(s.theme, row.Browser).Layout),
|
||||
layout.Flexed(instanceDirectoryColumnWeight, pathCell(s.theme, row.UserDataDir)),
|
||||
layout.Flexed(instanceStatusColumnWeight, statusLabel(s.theme, row.Status)),
|
||||
layout.Flexed(instanceStartColumnWeight, s.instanceStartButton(s.startClickFor(row.ID))),
|
||||
layout.Flexed(instanceActionColumnWeight, s.instanceActionButtons(row)),
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -296,8 +308,8 @@ func (s *Shell) instanceHeader(gtx layout.Context) layout.Dimensions {
|
||||
layout.Flexed(instanceBrowserColumnWeight, material.Caption(s.theme, "浏览器类型").Layout),
|
||||
layout.Flexed(instanceDirectoryColumnWeight, material.Caption(s.theme, "用户数据目录").Layout),
|
||||
layout.Flexed(instanceStatusColumnWeight, material.Caption(s.theme, "状态").Layout),
|
||||
layout.Flexed(instanceStartColumnWeight, func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.E.Layout(gtx, material.Caption(s.theme, "启动").Layout)
|
||||
layout.Flexed(instanceActionColumnWeight, func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.E.Layout(gtx, material.Caption(s.theme, "操作").Layout)
|
||||
}),
|
||||
)
|
||||
})
|
||||
@@ -309,16 +321,29 @@ func pathCell(theme *material.Theme, path string) layout.Widget {
|
||||
return style.Layout
|
||||
}
|
||||
|
||||
func (s *Shell) instanceStartButton(click *widget.Clickable) layout.Widget {
|
||||
func (s *Shell) instanceActionButtons(row InstanceRow) layout.Widget {
|
||||
return func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.E.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
style := material.Button(s.theme, click, "启动")
|
||||
style.Inset = layout.Inset{Top: unit.Dp(6), Right: unit.Dp(10), Bottom: unit.Dp(6), Left: unit.Dp(10)}
|
||||
return style.Layout(gtx)
|
||||
return layout.Flex{Alignment: layout.Middle}.Layout(gtx,
|
||||
layout.Rigid(s.instanceIconButton(s.startClickFor(row.ID), instanceStartIcon, "启动 "+row.Name, s.theme.Palette.ContrastBg)),
|
||||
layout.Rigid(layout.Spacer{Width: unit.Dp(4)}.Layout),
|
||||
layout.Rigid(s.instanceIconButton(s.deleteClickFor(row.ID), instanceDeleteIcon, "删除 "+row.Name, color.NRGBA{R: 188, G: 51, B: 51, A: 255})),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Shell) instanceIconButton(click *widget.Clickable, icon *widget.Icon, description string, iconColor color.NRGBA) layout.Widget {
|
||||
return func(gtx layout.Context) layout.Dimensions {
|
||||
style := material.IconButton(s.theme, click, icon, description)
|
||||
style.Size = unit.Dp(18)
|
||||
style.Inset = layout.UniformInset(unit.Dp(7))
|
||||
style.Background = color.NRGBA{}
|
||||
style.Color = iconColor
|
||||
return style.Layout(gtx)
|
||||
}
|
||||
}
|
||||
|
||||
func statusLabel(theme *material.Theme, status string) layout.Widget {
|
||||
style := material.Label(theme, unit.Sp(13), status)
|
||||
style.Color = map[string]color.NRGBA{
|
||||
@@ -574,6 +599,15 @@ func (s *Shell) startClickFor(id string) *widget.Clickable {
|
||||
return click
|
||||
}
|
||||
|
||||
func (s *Shell) deleteClickFor(id string) *widget.Clickable {
|
||||
if click := s.deleteClicks[id]; click != nil {
|
||||
return click
|
||||
}
|
||||
click := new(widget.Clickable)
|
||||
s.deleteClicks[id] = click
|
||||
return click
|
||||
}
|
||||
|
||||
func (s *Shell) requestStart(id string) {
|
||||
for _, row := range s.rows {
|
||||
if row.ID != id {
|
||||
@@ -585,6 +619,28 @@ func (s *Shell) requestStart(id string) {
|
||||
s.instanceFeedback = "找不到要启动的实例。"
|
||||
}
|
||||
|
||||
func (s *Shell) deleteInstance(id string) {
|
||||
for i, row := range s.rows {
|
||||
if row.ID != id {
|
||||
continue
|
||||
}
|
||||
s.rows = append(s.rows[:i], s.rows[i+1:]...)
|
||||
delete(s.startClicks, id)
|
||||
delete(s.deleteClicks, id)
|
||||
s.instanceFeedback = fmt.Sprintf("已删除实例“%s”;其 User Data Dir 未被删除。", row.Name)
|
||||
return
|
||||
}
|
||||
s.instanceFeedback = "找不到要删除的实例。"
|
||||
}
|
||||
|
||||
func mustIcon(data []byte) *widget.Icon {
|
||||
icon, err := widget.NewIcon(data)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return icon
|
||||
}
|
||||
|
||||
func (s *Shell) chooseInstanceDirectory() {
|
||||
if s.directoryPick.running {
|
||||
s.formFeedback = "目录选择器已打开,请在系统窗口中选择或取消。"
|
||||
|
||||
@@ -21,7 +21,7 @@ func TestShellStartsWithSemanticInstanceStatuses(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestShellInstanceRowsContainRequiredColumnsAndIndependentStartControls(t *testing.T) {
|
||||
func TestShellInstanceRowsContainRequiredColumnsAndIndependentActionControls(t *testing.T) {
|
||||
shell := NewShell(material.NewTheme())
|
||||
if len(shell.rows) < 2 {
|
||||
t.Fatal("expected fixture rows")
|
||||
@@ -36,18 +36,49 @@ func TestShellInstanceRowsContainRequiredColumnsAndIndependentStartControls(t *t
|
||||
if first == second {
|
||||
t.Fatal("rows share a start button state")
|
||||
}
|
||||
if shell.deleteClickFor(shell.rows[0].ID) == shell.deleteClickFor(shell.rows[1].ID) {
|
||||
t.Fatal("rows share a delete button state")
|
||||
}
|
||||
}
|
||||
|
||||
func TestInstanceListColumnWeightsPrioritizeDirectoryAndCompactAction(t *testing.T) {
|
||||
got := instanceNameColumnWeight + instanceBrowserColumnWeight + instanceDirectoryColumnWeight + instanceStatusColumnWeight + instanceStartColumnWeight
|
||||
got := instanceNameColumnWeight + instanceBrowserColumnWeight + instanceDirectoryColumnWeight + instanceStatusColumnWeight + instanceActionColumnWeight
|
||||
if got != 100 {
|
||||
t.Fatalf("instance column weights = %d, want 100", got)
|
||||
}
|
||||
if instanceDirectoryColumnWeight <= instanceNameColumnWeight {
|
||||
t.Fatal("user data directory must have the widest instance-list column")
|
||||
}
|
||||
if instanceStartColumnWeight >= instanceStatusColumnWeight {
|
||||
t.Fatal("start action column must remain compact")
|
||||
if instanceActionColumnWeight <= instanceStatusColumnWeight {
|
||||
t.Fatal("actions column must fit both compact icon commands")
|
||||
}
|
||||
}
|
||||
|
||||
func TestShellDeletesOnlyTheRequestedInstance(t *testing.T) {
|
||||
shell := NewShell(material.NewTheme())
|
||||
target := shell.rows[1]
|
||||
remaining := shell.rows[2].ID
|
||||
shell.startClickFor(target.ID)
|
||||
shell.deleteClickFor(target.ID)
|
||||
|
||||
shell.deleteInstance(target.ID)
|
||||
|
||||
if len(shell.rows) != 3 {
|
||||
t.Fatalf("instance count = %d, want 3", len(shell.rows))
|
||||
}
|
||||
for _, row := range shell.rows {
|
||||
if row.ID == target.ID {
|
||||
t.Fatalf("deleted instance %q remains in the list", target.ID)
|
||||
}
|
||||
}
|
||||
if _, exists := shell.startClicks[target.ID]; exists {
|
||||
t.Fatal("deleted instance start control was not released")
|
||||
}
|
||||
if _, exists := shell.deleteClicks[target.ID]; exists {
|
||||
t.Fatal("deleted instance delete control was not released")
|
||||
}
|
||||
if shell.rows[1].ID != remaining {
|
||||
t.Fatalf("remaining instance id = %q, want %q", shell.rows[1].ID, remaining)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user