Log compact header button bounds

This commit is contained in:
Joe Julian
2026-04-10 15:28:33 -07:00
parent 0e9fd478e5
commit 5838588fc5
7 changed files with 190 additions and 12 deletions
+14
View File
@@ -377,6 +377,7 @@ type ui struct {
settingsLifecycleAdvanced widget.Bool
settingsHistory widget.Bool
settingsDenseLayout widget.Bool
settingsDebugHeaderBounds widget.Bool
entryClicks []widget.Clickable
apiTokenClicks []widget.Clickable
apiPolicyRemoves []widget.Clickable
@@ -477,6 +478,7 @@ type ui struct {
autofillNoticePreference autofillNoticeMode
autofillFirstFillApprovalMode autofillFirstFillApprovalMode
accessibilityPrefs accessibilityPreferences
debugLogHeaderBounds bool
settingsDraft settingsDraft
recentVaults []string
recentRemotes []recentRemoteRecord
@@ -502,6 +504,8 @@ type ui struct {
lastLifecycleAction string
pendingLifecycleOpenIntent lifecycleOpenIntent
requestMasterPassFocus bool
lastHeaderBoundsLog string
frameInsetPx int
invalidate func()
}
@@ -1136,6 +1140,16 @@ func (u *ui) securityDialogContent(gtx layout.Context) layout.Dimensions {
check := material.CheckBox(u.theme, &u.settingsHistory, "Keep entry history collapsed")
return check.Layout(gtx)
},
func(gtx layout.Context) layout.Dimensions {
check := material.CheckBox(u.theme, &u.settingsDebugHeaderBounds, "Log compact header button bounds")
return check.Layout(gtx)
},
layout.Spacer{Height: unit.Dp(4)}.Layout,
func(gtx layout.Context) layout.Dimensions {
lbl := material.Label(u.theme, unit.Sp(12), "Write compact Android header button screen coordinates to the app log so emulator taps can read exact bounds from logcat.")
lbl.Color = mutedColor
return lbl.Layout(gtx)
},
layout.Spacer{Height: unit.Dp(14)}.Layout,
func(gtx layout.Context) layout.Dimensions {
lbl := material.Label(u.theme, unit.Sp(16), "Vault Security")
+1
View File
@@ -603,6 +603,7 @@ func (u *ui) layout(gtx layout.Context) layout.Dimensions {
u.handleGroupClicks(gtx)
u.handleInputUpdates(gtx)
u.updateViewportLayoutMode(gtx)
u.frameInsetPx = gtx.Dp(unit.Dp(16))
inset := layout.UniformInset(unit.Dp(16))
return layout.Stack{}.Layout(gtx,
layout.Expanded(func(gtx layout.Context) layout.Dimensions {
+40 -2
View File
@@ -1,6 +1,7 @@
package appui
import (
"fmt"
"image"
"gioui.org/layout"
@@ -39,11 +40,14 @@ func (u *ui) headerActions(gtx layout.Context) layout.Dimensions {
return layout.Dimensions{}
}
spacing := gtx.Dp(unit.Dp(8))
metrics := headerlayout.ActionMetrics{Spacing: spacing}
metrics := headerlayout.ActionMetrics{Spacing: spacing, SyncInnerSpacing: gtx.Dp(unit.Dp(3))}
if !u.usesCompactViewport() {
metrics.SyncInnerSpacing = gtx.Dp(unit.Dp(4))
}
actionCluster := func(gtx layout.Context) layout.Dimensions {
return layout.Flex{Spacing: layout.SpaceStart}.Layout(gtx,
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
metrics.SyncDims = u.syncButtonGroup(gtx)
metrics.SyncDims, metrics.SyncPrimaryDims, metrics.SyncToggleDims = u.syncButtonGroupWithMetrics(gtx)
return metrics.SyncDims
}),
layout.Rigid(layout.Spacer{Width: unit.Dp(8)}.Layout),
@@ -71,6 +75,9 @@ func (u *ui) headerActions(gtx layout.Context) layout.Dimensions {
rowCall.Add(gtx.Ops)
return metrics.RowDims
})
if u.usesCompactViewport() {
u.maybeLogHeaderBounds(newHeaderButtonBounds(image.Pt(u.frameInsetPx, u.frameInsetPx), metrics.Bounds()))
}
if u.usesCompactViewport() {
if u.syncMenuOpen {
@@ -94,6 +101,37 @@ func (u *ui) headerActions(gtx layout.Context) layout.Dimensions {
return rowDims
}
type headerButtonBounds struct {
SyncPrimary image.Rectangle
SyncToggle image.Rectangle
Lock image.Rectangle
MainMenu image.Rectangle
}
func newHeaderButtonBounds(origin image.Point, bounds headerlayout.ActionBounds) headerButtonBounds {
return headerButtonBounds{
SyncPrimary: bounds.SyncPrimary.Add(origin),
SyncToggle: bounds.SyncToggle.Add(origin),
Lock: bounds.Lock.Add(origin),
MainMenu: bounds.MainMenu.Add(origin),
}
}
func (b headerButtonBounds) logLine(mode string) string {
return fmt.Sprintf(
"keepassgo header-bounds mode=%s sync=%s sync_toggle=%s lock=%s menu=%s",
mode,
formatHeaderRect(b.SyncPrimary),
formatHeaderRect(b.SyncToggle),
formatHeaderRect(b.Lock),
formatHeaderRect(b.MainMenu),
)
}
func formatHeaderRect(rect image.Rectangle) string {
return fmt.Sprintf("%d,%d-%d,%d", rect.Min.X, rect.Min.Y, rect.Max.X, rect.Max.Y)
}
func (u *ui) topRightActionOrder() []string {
if u.isVaultLocked() {
return nil
+34 -6
View File
@@ -64,12 +64,15 @@ func (s DropdownSurface) Draw(gtx layout.Context, anchor DropdownAnchor, menu la
}
type ActionMetrics struct {
RowOriginX int
Spacing int
RowDims layout.Dimensions
SyncDims layout.Dimensions
LockDims layout.Dimensions
MainDims layout.Dimensions
RowOriginX int
Spacing int
SyncInnerSpacing int
RowDims layout.Dimensions
SyncDims layout.Dimensions
SyncPrimaryDims layout.Dimensions
SyncToggleDims layout.Dimensions
LockDims layout.Dimensions
MainDims layout.Dimensions
}
func (m ActionMetrics) SyncAnchor() DropdownAnchor {
@@ -86,3 +89,28 @@ func (m ActionMetrics) MainAnchor() DropdownAnchor {
TriggerBottomY: m.RowDims.Size.Y,
}
}
type ActionBounds struct {
SyncPrimary image.Rectangle
SyncToggle image.Rectangle
Lock image.Rectangle
MainMenu image.Rectangle
}
func (m ActionMetrics) Bounds() ActionBounds {
top := 0
syncLeft := m.RowOriginX
syncPrimary := image.Rect(syncLeft, top, syncLeft+m.SyncPrimaryDims.Size.X, top+m.SyncPrimaryDims.Size.Y)
syncToggleLeft := syncPrimary.Max.X + m.SyncInnerSpacing
syncToggle := image.Rect(syncToggleLeft, top, syncToggleLeft+m.SyncToggleDims.Size.X, top+m.SyncToggleDims.Size.Y)
lockLeft := syncLeft + m.SyncDims.Size.X + m.Spacing
lock := image.Rect(lockLeft, top, lockLeft+m.LockDims.Size.X, top+m.LockDims.Size.Y)
mainLeft := lock.Max.X + m.Spacing
mainMenu := image.Rect(mainLeft, top, mainLeft+m.MainDims.Size.X, top+m.MainDims.Size.Y)
return ActionBounds{
SyncPrimary: syncPrimary,
SyncToggle: syncToggle,
Lock: lock,
MainMenu: mainMenu,
}
}
+13 -3
View File
@@ -15,19 +15,29 @@ import (
)
func (u *ui) syncButtonGroup(gtx layout.Context) layout.Dimensions {
group, _, _ := u.syncButtonGroupWithMetrics(gtx)
return group
}
func (u *ui) syncButtonGroupWithMetrics(gtx layout.Context) (layout.Dimensions, layout.Dimensions, layout.Dimensions) {
spacing := unit.Dp(4)
if u.usesCompactViewport() {
spacing = unit.Dp(3)
}
return layout.Flex{Alignment: layout.Middle}.Layout(gtx,
var primaryDims layout.Dimensions
var toggleDims layout.Dimensions
groupDims := layout.Flex{Alignment: layout.Middle}.Layout(gtx,
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return syncPrimaryButton(gtx, u.theme, &u.synchronizeVault, "Sync", u.usesCompactViewport())
primaryDims = syncPrimaryButton(gtx, u.theme, &u.synchronizeVault, "Sync", u.usesCompactViewport())
return primaryDims
}),
layout.Rigid(layout.Spacer{Width: spacing}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return u.syncMenuToggle(gtx)
toggleDims = u.syncMenuToggle(gtx)
return toggleDims
}),
)
return groupDims, primaryDims, toggleDims
}
func (u *ui) syncMenuToggle(gtx layout.Context) layout.Dimensions {
+55
View File
@@ -5460,6 +5460,28 @@ func TestUISyncDefaultsPersistInSettings(t *testing.T) {
}
}
func TestUIDebugHeaderBoundsPersistInSettings(t *testing.T) {
t.Parallel()
configPath := filepath.Join(t.TempDir(), "settings.json")
first := newUIWithSession("phone", &session.Manager{}, statePaths{
SettingsPath: configPath,
})
first.debugLogHeaderBounds = true
first.saveSettings()
second := newUIWithSession("phone", &session.Manager{}, statePaths{
SettingsPath: configPath,
})
second.debugLogHeaderBounds = false
second.loadSettings()
if !second.debugLogHeaderBounds {
t.Fatal("debugLogHeaderBounds = false, want true after reload")
}
}
func TestUILoadSettingsFallsBackToLegacySyncDefaultsInUIPreferences(t *testing.T) {
t.Parallel()
@@ -5552,6 +5574,39 @@ func TestUISaveSecuritySettingsPersistsSyncDefaults(t *testing.T) {
}
}
func TestUISaveSecuritySettingsPersistsDebugHeaderBounds(t *testing.T) {
t.Parallel()
manager := &session.Manager{}
dir := t.TempDir()
u := newUIWithSession("phone", manager, statePaths{
DefaultSaveAsPath: filepath.Join(dir, "vault.kdbx"),
SettingsPath: filepath.Join(dir, "settings.json"),
UIPreferencesPath: filepath.Join(dir, "ui-prefs.json"),
})
u.masterPassword.SetText("correct horse battery staple")
if err := u.createVaultAction(); err != nil {
t.Fatalf("createVaultAction() error = %v", err)
}
u.securityCipher.SetText(vault.CipherAES256)
u.securityKDF.SetText(vault.KDFAES)
u.loadSettingsDraft()
u.settingsDebugHeaderBounds.Value = true
if err := u.saveSecuritySettingsAction(); err != nil {
t.Fatalf("saveSecuritySettingsAction() error = %v", err)
}
reloaded := newUIWithSession("phone", &session.Manager{}, statePaths{
SettingsPath: u.settingsPath,
})
reloaded.loadSettings()
if !reloaded.debugLogHeaderBounds {
t.Fatal("reloaded debugLogHeaderBounds = false, want true")
}
}
func TestUIAccessibilityPreferencesPersist(t *testing.T) {
t.Parallel()
+33 -1
View File
@@ -5,6 +5,7 @@ import (
"fmt"
"image"
"image/color"
"log"
"os"
"path/filepath"
"strings"
@@ -35,7 +36,8 @@ const (
type accessibilityPreferences = settingsmodel.AccessibilityPreferences
type settingsFile struct {
Sync syncSettings `json:"sync,omitempty"`
Sync syncSettings `json:"sync,omitempty"`
Debug debugSettings `json:"debug,omitempty"`
}
type syncSettings struct {
@@ -43,6 +45,10 @@ type syncSettings struct {
DirectionDefault string `json:"directionDefault,omitempty"`
}
type debugSettings struct {
LogHeaderBounds bool `json:"logHeaderBounds,omitempty"`
}
type syncSettingsDraft struct {
SourceDefault syncSourceMode
DirectionDefault syncDirection
@@ -51,6 +57,7 @@ type syncSettingsDraft struct {
type settingsDraft struct {
Accessibility accessibilityPreferences
Sync syncSettingsDraft
Debug debugSettings
}
type legacySyncPreferences struct {
@@ -191,7 +198,11 @@ func (u *ui) loadSettingsDraft() {
SourceDefault: u.syncDefaultSourceMode,
DirectionDefault: u.syncDefaultDirection,
},
Debug: debugSettings{
LogHeaderBounds: u.debugLogHeaderBounds,
},
}
u.settingsDebugHeaderBounds.Value = u.settingsDraft.Debug.LogHeaderBounds
}
func (u *ui) saveSecuritySettingsAction() error {
@@ -213,9 +224,14 @@ func (u *ui) applySecuritySettingsLive() error {
if u.settingsDraft.Accessibility.DisplayDensity == displayDensityForDenseLayout(u.denseLayout) {
u.settingsDraft.Accessibility.DisplayDensity = displayDensityForDenseLayout(u.settingsDenseLayout.Value)
}
u.settingsDraft.Debug.LogHeaderBounds = u.settingsDebugHeaderBounds.Value
u.settingsDenseLayout.Value = u.settingsDraft.Accessibility.DisplayDensity == displayDensityDense
u.syncDefaultSourceMode = sanitizeSyncSourceMode(u.settingsDraft.Sync.SourceDefault)
u.syncDefaultDirection = sanitizeSyncDirection(u.settingsDraft.Sync.DirectionDefault)
u.debugLogHeaderBounds = u.settingsDraft.Debug.LogHeaderBounds
if !u.debugLogHeaderBounds {
u.lastHeaderBoundsLog = ""
}
u.applySettingsFormToPreferences()
u.applyAccessibilityPreferences(u.settingsDraft.Accessibility)
u.saveSettings()
@@ -234,6 +250,7 @@ func (u *ui) loadSettings() {
if json.Unmarshal(content, &settings) == nil {
u.syncDefaultSourceMode = sanitizeSyncSourceMode(syncSourceMode(settings.Sync.SourceDefault))
u.syncDefaultDirection = sanitizeSyncDirection(syncDirection(settings.Sync.DirectionDefault))
u.debugLogHeaderBounds = settings.Debug.LogHeaderBounds
return
}
}
@@ -270,6 +287,9 @@ func (u *ui) saveSettings() {
SourceDefault: string(u.syncDefaultSourceMode),
DirectionDefault: string(u.syncDefaultDirection),
},
Debug: debugSettings{
LogHeaderBounds: u.debugLogHeaderBounds,
},
}, "", " ")
if err != nil {
return
@@ -277,6 +297,18 @@ func (u *ui) saveSettings() {
_ = os.WriteFile(u.settingsPath, content, 0o600)
}
func (u *ui) maybeLogHeaderBounds(bounds headerButtonBounds) {
if !u.debugLogHeaderBounds {
return
}
line := bounds.logLine(u.mode)
if line == u.lastHeaderBoundsLog {
return
}
log.Print(line)
u.lastHeaderBoundsLog = line
}
func (u *ui) showStatusMessage(message string) {
u.state.StatusMessage = message
if u.accessibilityPrefs.ReducedMotion {