Merge pull request #73 from bnema/feat/accent-picker
feat: add macOS-style accent picker for dead keys support
26 files · +2432 -39
M .mockery.yaml +3 -0
| @@ -23,6 +23,9 @@ packages: | |||
| 23 | 23 | HardwareSurveyor: {} | |
| 24 | 24 | ColorSchemeDetector: {} | |
| 25 | 25 | ColorSchemeResolver: {} | |
| 26 | + | TextInputTarget: {} | |
| 27 | + | FocusedInputProvider: {} | |
| 28 | + | AccentPickerUI: {} | |
| 26 | 29 | github.com/bnema/dumber/internal/domain/repository: | |
| 27 | 30 | config: | |
| 28 | 31 | dir: '{{.InterfaceDir}}/mocks' | |
| @@ -23,6 +23,9 @@ packages: | |||||
| 23 | HardwareSurveyor: {} | 23 | HardwareSurveyor: {} | ||
| 24 | ColorSchemeDetector: {} | 24 | ColorSchemeDetector: {} | ||
| 25 | ColorSchemeResolver: {} | 25 | ColorSchemeResolver: {} | ||
| 26 | + | TextInputTarget: {} | |||
| 27 | + | FocusedInputProvider: {} | |||
| 28 | + | AccentPickerUI: {} | |||
| 26 | github.com/bnema/dumber/internal/domain/repository: | 29 | github.com/bnema/dumber/internal/domain/repository: | ||
| 27 | config: | 30 | config: | ||
| 28 | dir: '{{.InterfaceDir}}/mocks' | 31 | dir: '{{.InterfaceDir}}/mocks' | ||
M CHANGELOG.md +3 -0
| @@ -4,6 +4,9 @@ All notable changes to this project will be documented in this file. | |||
| 4 | 4 | ||
| 5 | 5 | ## [Unreleased] | |
| 6 | 6 | ||
| 7 | + | ### Added | |
| 8 | + | - **macOS-style accent picker**: Long-press (400ms) on keys with accent variants (e, a, o, u, i, c, n, s, y, z) shows a popup with accented character options. Select with number keys 1-9, arrow keys + Enter, or click. Works in omnibox, find bar (Ctrl+F), and web page inputs. Implements dead keys/compose sequence alternative for users who can't use system-level input methods. | |
| 9 | + | ||
| 7 | 10 | ### Fixed | |
| 8 | 11 | - **Dark mode detection on internal pages**: Fixed `dumb://home` and other internal pages showing light theme despite system being in dark mode. Refactored color scheme detection to use a priority-based resolver with proper initialization ordering. The libadwaita detector (highest priority) is now correctly enabled only after `adw.Init()` completes, ensuring WebViews created during bootstrap get the correct theme preference. | |
| 9 | 12 | ||
| @@ -4,6 +4,9 @@ All notable changes to this project will be documented in this file. | |||||
| 4 | 4 | ||||
| 5 | ## [Unreleased] | 5 | ## [Unreleased] | ||
| 6 | 6 | ||||
| 7 | + | ### Added | |||
| 8 | + | - **macOS-style accent picker**: Long-press (400ms) on keys with accent variants (e, a, o, u, i, c, n, s, y, z) shows a popup with accented character options. Select with number keys 1-9, arrow keys + Enter, or click. Works in omnibox, find bar (Ctrl+F), and web page inputs. Implements dead keys/compose sequence alternative for users who can't use system-level input methods. | |||
| 9 | + | ||||
| 7 | ### Fixed | 10 | ### Fixed | ||
| 8 | - **Dark mode detection on internal pages**: Fixed `dumb://home` and other internal pages showing light theme despite system being in dark mode. Refactored color scheme detection to use a priority-based resolver with proper initialization ordering. The libadwaita detector (highest priority) is now correctly enabled only after `adw.Init()` completes, ensuring WebViews created during bootstrap get the correct theme preference. | 11 | - **Dark mode detection on internal pages**: Fixed `dumb://home` and other internal pages showing light theme despite system being in dark mode. Refactored color scheme detection to use a priority-based resolver with proper initialization ordering. The libadwaita detector (highest priority) is now correctly enabled only after `adw.Init()` completes, ensuring WebViews created during bootstrap get the correct theme preference. | ||
| 9 | 12 | ||||
M README.md +1 -0
| @@ -125,6 +125,7 @@ Some features are very alpha/experimental (notably consume-or-expel panes) and m | |||
| 125 | 125 | | **Space** | Toggle Favorite | Omnibox mode: favorite/unfavorite selected item | | |
| 126 | 126 | | **Ctrl+1-9, 0** | Quick Navigate | Jump to result by number (1-10) | | |
| 127 | 127 | | **↑/↓ Arrow** | Navigate Results | Browse suggestions/matches | | |
| 128 | + | | **Hold key 400ms** | Accent Picker | Shows accented variants in any text input (e.g., hold 'e' for è, é, ê, ë) | | |
| 128 | 129 | ||
| 129 | 130 | All zoom changes are automatically persisted per-domain and restored on next visit. | |
| 130 | 131 | ||
| @@ -125,6 +125,7 @@ Some features are very alpha/experimental (notably consume-or-expel panes) and m | |||||
| 125 | | **Space** | Toggle Favorite | Omnibox mode: favorite/unfavorite selected item | | 125 | | **Space** | Toggle Favorite | Omnibox mode: favorite/unfavorite selected item | | ||
| 126 | | **Ctrl+1-9, 0** | Quick Navigate | Jump to result by number (1-10) | | 126 | | **Ctrl+1-9, 0** | Quick Navigate | Jump to result by number (1-10) | | ||
| 127 | | **↑/↓ Arrow** | Navigate Results | Browse suggestions/matches | | 127 | | **↑/↓ Arrow** | Navigate Results | Browse suggestions/matches | | ||
| 128 | + | | **Hold key 400ms** | Accent Picker | Shows accented variants in any text input (e.g., hold 'e' for è, é, ê, ë) | | |||
| 128 | 129 | ||||
| 129 | All zoom changes are automatically persisted per-domain and restored on next visit. | 130 | All zoom changes are automatically persisted per-domain and restored on next visit. | ||
| 130 | 131 | ||||
A internal/application/port/accent_picker.go +17 -0
| @@ -0,0 +1,17 @@ | |||
| 1 | + | package port | |
| 2 | + | ||
| 3 | + | // AccentPickerUI defines the interface for displaying an accent selection overlay. | |
| 4 | + | // The picker shows a list of accented characters and allows the user to select one | |
| 5 | + | // via arrow keys, number keys (1-9), or Enter. | |
| 6 | + | type AccentPickerUI interface { | |
| 7 | + | // Show displays the accent picker with the given accent variants. | |
| 8 | + | // selectedCallback is called when the user selects an accent. | |
| 9 | + | // cancelCallback is called when the user cancels (Escape key). | |
| 10 | + | Show(accents []rune, selectedCallback func(accent rune), cancelCallback func()) | |
| 11 | + | ||
| 12 | + | // Hide hides the accent picker. | |
| 13 | + | Hide() | |
| 14 | + | ||
| 15 | + | // IsVisible returns true if the accent picker is currently visible. | |
| 16 | + | IsVisible() bool | |
| 17 | + | } | |
| @@ -0,0 +1,17 @@ | |||||
| 1 | + | package port | |||
| 2 | + | ||||
| 3 | + | // AccentPickerUI defines the interface for displaying an accent selection overlay. | |||
| 4 | + | // The picker shows a list of accented characters and allows the user to select one | |||
| 5 | + | // via arrow keys, number keys (1-9), or Enter. | |||
| 6 | + | type AccentPickerUI interface { | |||
| 7 | + | // Show displays the accent picker with the given accent variants. | |||
| 8 | + | // selectedCallback is called when the user selects an accent. | |||
| 9 | + | // cancelCallback is called when the user cancels (Escape key). | |||
| 10 | + | Show(accents []rune, selectedCallback func(accent rune), cancelCallback func()) | |||
| 11 | + | ||||
| 12 | + | // Hide hides the accent picker. | |||
| 13 | + | Hide() | |||
| 14 | + | ||||
| 15 | + | // IsVisible returns true if the accent picker is currently visible. | |||
| 16 | + | IsVisible() bool | |||
| 17 | + | } | |||
M internal/application/port/mocks/mocks.go +410 -0
| @@ -14,6 +14,162 @@ import ( | |||
| 14 | 14 | mock "github.com/stretchr/testify/mock" | |
| 15 | 15 | ) | |
| 16 | 16 | ||
| 17 | + | // NewMockAccentPickerUI creates a new instance of MockAccentPickerUI. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. | |
| 18 | + | // The first argument is typically a *testing.T value. | |
| 19 | + | func NewMockAccentPickerUI(t interface { | |
| 20 | + | mock.TestingT | |
| 21 | + | Cleanup(func()) | |
| 22 | + | }) *MockAccentPickerUI { | |
| 23 | + | mock := &MockAccentPickerUI{} | |
| 24 | + | mock.Mock.Test(t) | |
| 25 | + | ||
| 26 | + | t.Cleanup(func() { mock.AssertExpectations(t) }) | |
| 27 | + | ||
| 28 | + | return mock | |
| 29 | + | } | |
| 30 | + | ||
| 31 | + | // MockAccentPickerUI is an autogenerated mock type for the AccentPickerUI type | |
| 32 | + | type MockAccentPickerUI struct { | |
| 33 | + | mock.Mock | |
| 34 | + | } | |
| 35 | + | ||
| 36 | + | type MockAccentPickerUI_Expecter struct { | |
| 37 | + | mock *mock.Mock | |
| 38 | + | } | |
| 39 | + | ||
| 40 | + | func (_m *MockAccentPickerUI) EXPECT() *MockAccentPickerUI_Expecter { | |
| 41 | + | return &MockAccentPickerUI_Expecter{mock: &_m.Mock} | |
| 42 | + | } | |
| 43 | + | ||
| 44 | + | // Hide provides a mock function for the type MockAccentPickerUI | |
| 45 | + | func (_mock *MockAccentPickerUI) Hide() { | |
| 46 | + | _mock.Called() | |
| 47 | + | return | |
| 48 | + | } | |
| 49 | + | ||
| 50 | + | // MockAccentPickerUI_Hide_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Hide' | |
| 51 | + | type MockAccentPickerUI_Hide_Call struct { | |
| 52 | + | *mock.Call | |
| 53 | + | } | |
| 54 | + | ||
| 55 | + | // Hide is a helper method to define mock.On call | |
| 56 | + | func (_e *MockAccentPickerUI_Expecter) Hide() *MockAccentPickerUI_Hide_Call { | |
| 57 | + | return &MockAccentPickerUI_Hide_Call{Call: _e.mock.On("Hide")} | |
| 58 | + | } | |
| 59 | + | ||
| 60 | + | func (_c *MockAccentPickerUI_Hide_Call) Run(run func()) *MockAccentPickerUI_Hide_Call { | |
| 61 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 62 | + | run() | |
| 63 | + | }) | |
| 64 | + | return _c | |
| 65 | + | } | |
| 66 | + | ||
| 67 | + | func (_c *MockAccentPickerUI_Hide_Call) Return() *MockAccentPickerUI_Hide_Call { | |
| 68 | + | _c.Call.Return() | |
| 69 | + | return _c | |
| 70 | + | } | |
| 71 | + | ||
| 72 | + | func (_c *MockAccentPickerUI_Hide_Call) RunAndReturn(run func()) *MockAccentPickerUI_Hide_Call { | |
| 73 | + | _c.Run(run) | |
| 74 | + | return _c | |
| 75 | + | } | |
| 76 | + | ||
| 77 | + | // IsVisible provides a mock function for the type MockAccentPickerUI | |
| 78 | + | func (_mock *MockAccentPickerUI) IsVisible() bool { | |
| 79 | + | ret := _mock.Called() | |
| 80 | + | ||
| 81 | + | if len(ret) == 0 { | |
| 82 | + | panic("no return value specified for IsVisible") | |
| 83 | + | } | |
| 84 | + | ||
| 85 | + | var r0 bool | |
| 86 | + | if returnFunc, ok := ret.Get(0).(func() bool); ok { | |
| 87 | + | r0 = returnFunc() | |
| 88 | + | } else { | |
| 89 | + | r0 = ret.Get(0).(bool) | |
| 90 | + | } | |
| 91 | + | return r0 | |
| 92 | + | } | |
| 93 | + | ||
| 94 | + | // MockAccentPickerUI_IsVisible_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsVisible' | |
| 95 | + | type MockAccentPickerUI_IsVisible_Call struct { | |
| 96 | + | *mock.Call | |
| 97 | + | } | |
| 98 | + | ||
| 99 | + | // IsVisible is a helper method to define mock.On call | |
| 100 | + | func (_e *MockAccentPickerUI_Expecter) IsVisible() *MockAccentPickerUI_IsVisible_Call { | |
| 101 | + | return &MockAccentPickerUI_IsVisible_Call{Call: _e.mock.On("IsVisible")} | |
| 102 | + | } | |
| 103 | + | ||
| 104 | + | func (_c *MockAccentPickerUI_IsVisible_Call) Run(run func()) *MockAccentPickerUI_IsVisible_Call { | |
| 105 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 106 | + | run() | |
| 107 | + | }) | |
| 108 | + | return _c | |
| 109 | + | } | |
| 110 | + | ||
| 111 | + | func (_c *MockAccentPickerUI_IsVisible_Call) Return(b bool) *MockAccentPickerUI_IsVisible_Call { | |
| 112 | + | _c.Call.Return(b) | |
| 113 | + | return _c | |
| 114 | + | } | |
| 115 | + | ||
| 116 | + | func (_c *MockAccentPickerUI_IsVisible_Call) RunAndReturn(run func() bool) *MockAccentPickerUI_IsVisible_Call { | |
| 117 | + | _c.Call.Return(run) | |
| 118 | + | return _c | |
| 119 | + | } | |
| 120 | + | ||
| 121 | + | // Show provides a mock function for the type MockAccentPickerUI | |
| 122 | + | func (_mock *MockAccentPickerUI) Show(accents []rune, selectedCallback func(accent rune), cancelCallback func()) { | |
| 123 | + | _mock.Called(accents, selectedCallback, cancelCallback) | |
| 124 | + | return | |
| 125 | + | } | |
| 126 | + | ||
| 127 | + | // MockAccentPickerUI_Show_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Show' | |
| 128 | + | type MockAccentPickerUI_Show_Call struct { | |
| 129 | + | *mock.Call | |
| 130 | + | } | |
| 131 | + | ||
| 132 | + | // Show is a helper method to define mock.On call | |
| 133 | + | // - accents []rune | |
| 134 | + | // - selectedCallback func(accent rune) | |
| 135 | + | // - cancelCallback func() | |
| 136 | + | func (_e *MockAccentPickerUI_Expecter) Show(accents interface{}, selectedCallback interface{}, cancelCallback interface{}) *MockAccentPickerUI_Show_Call { | |
| 137 | + | return &MockAccentPickerUI_Show_Call{Call: _e.mock.On("Show", accents, selectedCallback, cancelCallback)} | |
| 138 | + | } | |
| 139 | + | ||
| 140 | + | func (_c *MockAccentPickerUI_Show_Call) Run(run func(accents []rune, selectedCallback func(accent rune), cancelCallback func())) *MockAccentPickerUI_Show_Call { | |
| 141 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 142 | + | var arg0 []rune | |
| 143 | + | if args[0] != nil { | |
| 144 | + | arg0 = args[0].([]rune) | |
| 145 | + | } | |
| 146 | + | var arg1 func(accent rune) | |
| 147 | + | if args[1] != nil { | |
| 148 | + | arg1 = args[1].(func(accent rune)) | |
| 149 | + | } | |
| 150 | + | var arg2 func() | |
| 151 | + | if args[2] != nil { | |
| 152 | + | arg2 = args[2].(func()) | |
| 153 | + | } | |
| 154 | + | run( | |
| 155 | + | arg0, | |
| 156 | + | arg1, | |
| 157 | + | arg2, | |
| 158 | + | ) | |
| 159 | + | }) | |
| 160 | + | return _c | |
| 161 | + | } | |
| 162 | + | ||
| 163 | + | func (_c *MockAccentPickerUI_Show_Call) Return() *MockAccentPickerUI_Show_Call { | |
| 164 | + | _c.Call.Return() | |
| 165 | + | return _c | |
| 166 | + | } | |
| 167 | + | ||
| 168 | + | func (_c *MockAccentPickerUI_Show_Call) RunAndReturn(run func(accents []rune, selectedCallback func(accent rune), cancelCallback func())) *MockAccentPickerUI_Show_Call { | |
| 169 | + | _c.Run(run) | |
| 170 | + | return _c | |
| 171 | + | } | |
| 172 | + | ||
| 17 | 173 | // NewMockColorSchemeDetector creates a new instance of MockColorSchemeDetector. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. | |
| 18 | 174 | // The first argument is typically a *testing.T value. | |
| 19 | 175 | func NewMockColorSchemeDetector(t interface { | |
| @@ -1438,6 +1594,260 @@ func (_c *MockSessionSpawner_SpawnWithSession_Call) RunAndReturn(run func(sessio | |||
| 1438 | 1594 | return _c | |
| 1439 | 1595 | } | |
| 1440 | 1596 | ||
| 1597 | + | // NewMockTextInputTarget creates a new instance of MockTextInputTarget. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. | |
| 1598 | + | // The first argument is typically a *testing.T value. | |
| 1599 | + | func NewMockTextInputTarget(t interface { | |
| 1600 | + | mock.TestingT | |
| 1601 | + | Cleanup(func()) | |
| 1602 | + | }) *MockTextInputTarget { | |
| 1603 | + | mock := &MockTextInputTarget{} | |
| 1604 | + | mock.Mock.Test(t) | |
| 1605 | + | ||
| 1606 | + | t.Cleanup(func() { mock.AssertExpectations(t) }) | |
| 1607 | + | ||
| 1608 | + | return mock | |
| 1609 | + | } | |
| 1610 | + | ||
| 1611 | + | // MockTextInputTarget is an autogenerated mock type for the TextInputTarget type | |
| 1612 | + | type MockTextInputTarget struct { | |
| 1613 | + | mock.Mock | |
| 1614 | + | } | |
| 1615 | + | ||
| 1616 | + | type MockTextInputTarget_Expecter struct { | |
| 1617 | + | mock *mock.Mock | |
| 1618 | + | } | |
| 1619 | + | ||
| 1620 | + | func (_m *MockTextInputTarget) EXPECT() *MockTextInputTarget_Expecter { | |
| 1621 | + | return &MockTextInputTarget_Expecter{mock: &_m.Mock} | |
| 1622 | + | } | |
| 1623 | + | ||
| 1624 | + | // DeleteBeforeCursor provides a mock function for the type MockTextInputTarget | |
| 1625 | + | func (_mock *MockTextInputTarget) DeleteBeforeCursor(ctx context.Context, n int) error { | |
| 1626 | + | ret := _mock.Called(ctx, n) | |
| 1627 | + | ||
| 1628 | + | if len(ret) == 0 { | |
| 1629 | + | panic("no return value specified for DeleteBeforeCursor") | |
| 1630 | + | } | |
| 1631 | + | ||
| 1632 | + | var r0 error | |
| 1633 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, int) error); ok { | |
| 1634 | + | r0 = returnFunc(ctx, n) | |
| 1635 | + | } else { | |
| 1636 | + | r0 = ret.Error(0) | |
| 1637 | + | } | |
| 1638 | + | return r0 | |
| 1639 | + | } | |
| 1640 | + | ||
| 1641 | + | // MockTextInputTarget_DeleteBeforeCursor_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeleteBeforeCursor' | |
| 1642 | + | type MockTextInputTarget_DeleteBeforeCursor_Call struct { | |
| 1643 | + | *mock.Call | |
| 1644 | + | } | |
| 1645 | + | ||
| 1646 | + | // DeleteBeforeCursor is a helper method to define mock.On call | |
| 1647 | + | // - ctx context.Context | |
| 1648 | + | // - n int | |
| 1649 | + | func (_e *MockTextInputTarget_Expecter) DeleteBeforeCursor(ctx interface{}, n interface{}) *MockTextInputTarget_DeleteBeforeCursor_Call { | |
| 1650 | + | return &MockTextInputTarget_DeleteBeforeCursor_Call{Call: _e.mock.On("DeleteBeforeCursor", ctx, n)} | |
| 1651 | + | } | |
| 1652 | + | ||
| 1653 | + | func (_c *MockTextInputTarget_DeleteBeforeCursor_Call) Run(run func(ctx context.Context, n int)) *MockTextInputTarget_DeleteBeforeCursor_Call { | |
| 1654 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 1655 | + | var arg0 context.Context | |
| 1656 | + | if args[0] != nil { | |
| 1657 | + | arg0 = args[0].(context.Context) | |
| 1658 | + | } | |
| 1659 | + | var arg1 int | |
| 1660 | + | if args[1] != nil { | |
| 1661 | + | arg1 = args[1].(int) | |
| 1662 | + | } | |
| 1663 | + | run( | |
| 1664 | + | arg0, | |
| 1665 | + | arg1, | |
| 1666 | + | ) | |
| 1667 | + | }) | |
| 1668 | + | return _c | |
| 1669 | + | } | |
| 1670 | + | ||
| 1671 | + | func (_c *MockTextInputTarget_DeleteBeforeCursor_Call) Return(err error) *MockTextInputTarget_DeleteBeforeCursor_Call { | |
| 1672 | + | _c.Call.Return(err) | |
| 1673 | + | return _c | |
| 1674 | + | } | |
| 1675 | + | ||
| 1676 | + | func (_c *MockTextInputTarget_DeleteBeforeCursor_Call) RunAndReturn(run func(ctx context.Context, n int) error) *MockTextInputTarget_DeleteBeforeCursor_Call { | |
| 1677 | + | _c.Call.Return(run) | |
| 1678 | + | return _c | |
| 1679 | + | } | |
| 1680 | + | ||
| 1681 | + | // InsertText provides a mock function for the type MockTextInputTarget | |
| 1682 | + | func (_mock *MockTextInputTarget) InsertText(ctx context.Context, text string) error { | |
| 1683 | + | ret := _mock.Called(ctx, text) | |
| 1684 | + | ||
| 1685 | + | if len(ret) == 0 { | |
| 1686 | + | panic("no return value specified for InsertText") | |
| 1687 | + | } | |
| 1688 | + | ||
| 1689 | + | var r0 error | |
| 1690 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, string) error); ok { | |
| 1691 | + | r0 = returnFunc(ctx, text) | |
| 1692 | + | } else { | |
| 1693 | + | r0 = ret.Error(0) | |
| 1694 | + | } | |
| 1695 | + | return r0 | |
| 1696 | + | } | |
| 1697 | + | ||
| 1698 | + | // MockTextInputTarget_InsertText_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'InsertText' | |
| 1699 | + | type MockTextInputTarget_InsertText_Call struct { | |
| 1700 | + | *mock.Call | |
| 1701 | + | } | |
| 1702 | + | ||
| 1703 | + | // InsertText is a helper method to define mock.On call | |
| 1704 | + | // - ctx context.Context | |
| 1705 | + | // - text string | |
| 1706 | + | func (_e *MockTextInputTarget_Expecter) InsertText(ctx interface{}, text interface{}) *MockTextInputTarget_InsertText_Call { | |
| 1707 | + | return &MockTextInputTarget_InsertText_Call{Call: _e.mock.On("InsertText", ctx, text)} | |
| 1708 | + | } | |
| 1709 | + | ||
| 1710 | + | func (_c *MockTextInputTarget_InsertText_Call) Run(run func(ctx context.Context, text string)) *MockTextInputTarget_InsertText_Call { | |
| 1711 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 1712 | + | var arg0 context.Context | |
| 1713 | + | if args[0] != nil { | |
| 1714 | + | arg0 = args[0].(context.Context) | |
| 1715 | + | } | |
| 1716 | + | var arg1 string | |
| 1717 | + | if args[1] != nil { | |
| 1718 | + | arg1 = args[1].(string) | |
| 1719 | + | } | |
| 1720 | + | run( | |
| 1721 | + | arg0, | |
| 1722 | + | arg1, | |
| 1723 | + | ) | |
| 1724 | + | }) | |
| 1725 | + | return _c | |
| 1726 | + | } | |
| 1727 | + | ||
| 1728 | + | func (_c *MockTextInputTarget_InsertText_Call) Return(err error) *MockTextInputTarget_InsertText_Call { | |
| 1729 | + | _c.Call.Return(err) | |
| 1730 | + | return _c | |
| 1731 | + | } | |
| 1732 | + | ||
| 1733 | + | func (_c *MockTextInputTarget_InsertText_Call) RunAndReturn(run func(ctx context.Context, text string) error) *MockTextInputTarget_InsertText_Call { | |
| 1734 | + | _c.Call.Return(run) | |
| 1735 | + | return _c | |
| 1736 | + | } | |
| 1737 | + | ||
| 1738 | + | // NewMockFocusedInputProvider creates a new instance of MockFocusedInputProvider. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. | |
| 1739 | + | // The first argument is typically a *testing.T value. | |
| 1740 | + | func NewMockFocusedInputProvider(t interface { | |
| 1741 | + | mock.TestingT | |
| 1742 | + | Cleanup(func()) | |
| 1743 | + | }) *MockFocusedInputProvider { | |
| 1744 | + | mock := &MockFocusedInputProvider{} | |
| 1745 | + | mock.Mock.Test(t) | |
| 1746 | + | ||
| 1747 | + | t.Cleanup(func() { mock.AssertExpectations(t) }) | |
| 1748 | + | ||
| 1749 | + | return mock | |
| 1750 | + | } | |
| 1751 | + | ||
| 1752 | + | // MockFocusedInputProvider is an autogenerated mock type for the FocusedInputProvider type | |
| 1753 | + | type MockFocusedInputProvider struct { | |
| 1754 | + | mock.Mock | |
| 1755 | + | } | |
| 1756 | + | ||
| 1757 | + | type MockFocusedInputProvider_Expecter struct { | |
| 1758 | + | mock *mock.Mock | |
| 1759 | + | } | |
| 1760 | + | ||
| 1761 | + | func (_m *MockFocusedInputProvider) EXPECT() *MockFocusedInputProvider_Expecter { | |
| 1762 | + | return &MockFocusedInputProvider_Expecter{mock: &_m.Mock} | |
| 1763 | + | } | |
| 1764 | + | ||
| 1765 | + | // GetFocusedInput provides a mock function for the type MockFocusedInputProvider | |
| 1766 | + | func (_mock *MockFocusedInputProvider) GetFocusedInput() port.TextInputTarget { | |
| 1767 | + | ret := _mock.Called() | |
| 1768 | + | ||
| 1769 | + | if len(ret) == 0 { | |
| 1770 | + | panic("no return value specified for GetFocusedInput") | |
| 1771 | + | } | |
| 1772 | + | ||
| 1773 | + | var r0 port.TextInputTarget | |
| 1774 | + | if returnFunc, ok := ret.Get(0).(func() port.TextInputTarget); ok { | |
| 1775 | + | r0 = returnFunc() | |
| 1776 | + | } else { | |
| 1777 | + | if ret.Get(0) != nil { | |
| 1778 | + | r0 = ret.Get(0).(port.TextInputTarget) | |
| 1779 | + | } | |
| 1780 | + | } | |
| 1781 | + | return r0 | |
| 1782 | + | } | |
| 1783 | + | ||
| 1784 | + | // MockFocusedInputProvider_GetFocusedInput_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetFocusedInput' | |
| 1785 | + | type MockFocusedInputProvider_GetFocusedInput_Call struct { | |
| 1786 | + | *mock.Call | |
| 1787 | + | } | |
| 1788 | + | ||
| 1789 | + | // GetFocusedInput is a helper method to define mock.On call | |
| 1790 | + | func (_e *MockFocusedInputProvider_Expecter) GetFocusedInput() *MockFocusedInputProvider_GetFocusedInput_Call { | |
| 1791 | + | return &MockFocusedInputProvider_GetFocusedInput_Call{Call: _e.mock.On("GetFocusedInput")} | |
| 1792 | + | } | |
| 1793 | + | ||
| 1794 | + | func (_c *MockFocusedInputProvider_GetFocusedInput_Call) Run(run func()) *MockFocusedInputProvider_GetFocusedInput_Call { | |
| 1795 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 1796 | + | run() | |
| 1797 | + | }) | |
| 1798 | + | return _c | |
| 1799 | + | } | |
| 1800 | + | ||
| 1801 | + | func (_c *MockFocusedInputProvider_GetFocusedInput_Call) Return(textInputTarget port.TextInputTarget) *MockFocusedInputProvider_GetFocusedInput_Call { | |
| 1802 | + | _c.Call.Return(textInputTarget) | |
| 1803 | + | return _c | |
| 1804 | + | } | |
| 1805 | + | ||
| 1806 | + | func (_c *MockFocusedInputProvider_GetFocusedInput_Call) RunAndReturn(run func() port.TextInputTarget) *MockFocusedInputProvider_GetFocusedInput_Call { | |
| 1807 | + | _c.Call.Return(run) | |
| 1808 | + | return _c | |
| 1809 | + | } | |
| 1810 | + | ||
| 1811 | + | // SetFocusedInput provides a mock function for the type MockFocusedInputProvider | |
| 1812 | + | func (_mock *MockFocusedInputProvider) SetFocusedInput(target port.TextInputTarget) { | |
| 1813 | + | _mock.Called(target) | |
| 1814 | + | return | |
| 1815 | + | } | |
| 1816 | + | ||
| 1817 | + | // MockFocusedInputProvider_SetFocusedInput_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetFocusedInput' | |
| 1818 | + | type MockFocusedInputProvider_SetFocusedInput_Call struct { | |
| 1819 | + | *mock.Call | |
| 1820 | + | } | |
| 1821 | + | ||
| 1822 | + | // SetFocusedInput is a helper method to define mock.On call | |
| 1823 | + | // - target port.TextInputTarget | |
| 1824 | + | func (_e *MockFocusedInputProvider_Expecter) SetFocusedInput(target interface{}) *MockFocusedInputProvider_SetFocusedInput_Call { | |
| 1825 | + | return &MockFocusedInputProvider_SetFocusedInput_Call{Call: _e.mock.On("SetFocusedInput", target)} | |
| 1826 | + | } | |
| 1827 | + | ||
| 1828 | + | func (_c *MockFocusedInputProvider_SetFocusedInput_Call) Run(run func(target port.TextInputTarget)) *MockFocusedInputProvider_SetFocusedInput_Call { | |
| 1829 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 1830 | + | var arg0 port.TextInputTarget | |
| 1831 | + | if args[0] != nil { | |
| 1832 | + | arg0 = args[0].(port.TextInputTarget) | |
| 1833 | + | } | |
| 1834 | + | run( | |
| 1835 | + | arg0, | |
| 1836 | + | ) | |
| 1837 | + | }) | |
| 1838 | + | return _c | |
| 1839 | + | } | |
| 1840 | + | ||
| 1841 | + | func (_c *MockFocusedInputProvider_SetFocusedInput_Call) Return() *MockFocusedInputProvider_SetFocusedInput_Call { | |
| 1842 | + | _c.Call.Return() | |
| 1843 | + | return _c | |
| 1844 | + | } | |
| 1845 | + | ||
| 1846 | + | func (_c *MockFocusedInputProvider_SetFocusedInput_Call) RunAndReturn(run func(target port.TextInputTarget)) *MockFocusedInputProvider_SetFocusedInput_Call { | |
| 1847 | + | _c.Run(run) | |
| 1848 | + | return _c | |
| 1849 | + | } | |
| 1850 | + | ||
| 1441 | 1851 | // NewMockMemoryPressureApplier creates a new instance of MockMemoryPressureApplier. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. | |
| 1442 | 1852 | // The first argument is typically a *testing.T value. | |
| 1443 | 1853 | func NewMockMemoryPressureApplier(t interface { | |
| @@ -14,6 +14,162 @@ import ( | |||||
| 14 | mock "github.com/stretchr/testify/mock" | 14 | mock "github.com/stretchr/testify/mock" | ||
| 15 | ) | 15 | ) | ||
| 16 | 16 | ||||
| 17 | + | // NewMockAccentPickerUI creates a new instance of MockAccentPickerUI. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. | |||
| 18 | + | // The first argument is typically a *testing.T value. | |||
| 19 | + | func NewMockAccentPickerUI(t interface { | |||
| 20 | + | mock.TestingT | |||
| 21 | + | Cleanup(func()) | |||
| 22 | + | }) *MockAccentPickerUI { | |||
| 23 | + | mock := &MockAccentPickerUI{} | |||
| 24 | + | mock.Mock.Test(t) | |||
| 25 | + | ||||
| 26 | + | t.Cleanup(func() { mock.AssertExpectations(t) }) | |||
| 27 | + | ||||
| 28 | + | return mock | |||
| 29 | + | } | |||
| 30 | + | ||||
| 31 | + | // MockAccentPickerUI is an autogenerated mock type for the AccentPickerUI type | |||
| 32 | + | type MockAccentPickerUI struct { | |||
| 33 | + | mock.Mock | |||
| 34 | + | } | |||
| 35 | + | ||||
| 36 | + | type MockAccentPickerUI_Expecter struct { | |||
| 37 | + | mock *mock.Mock | |||
| 38 | + | } | |||
| 39 | + | ||||
| 40 | + | func (_m *MockAccentPickerUI) EXPECT() *MockAccentPickerUI_Expecter { | |||
| 41 | + | return &MockAccentPickerUI_Expecter{mock: &_m.Mock} | |||
| 42 | + | } | |||
| 43 | + | ||||
| 44 | + | // Hide provides a mock function for the type MockAccentPickerUI | |||
| 45 | + | func (_mock *MockAccentPickerUI) Hide() { | |||
| 46 | + | _mock.Called() | |||
| 47 | + | return | |||
| 48 | + | } | |||
| 49 | + | ||||
| 50 | + | // MockAccentPickerUI_Hide_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Hide' | |||
| 51 | + | type MockAccentPickerUI_Hide_Call struct { | |||
| 52 | + | *mock.Call | |||
| 53 | + | } | |||
| 54 | + | ||||
| 55 | + | // Hide is a helper method to define mock.On call | |||
| 56 | + | func (_e *MockAccentPickerUI_Expecter) Hide() *MockAccentPickerUI_Hide_Call { | |||
| 57 | + | return &MockAccentPickerUI_Hide_Call{Call: _e.mock.On("Hide")} | |||
| 58 | + | } | |||
| 59 | + | ||||
| 60 | + | func (_c *MockAccentPickerUI_Hide_Call) Run(run func()) *MockAccentPickerUI_Hide_Call { | |||
| 61 | + | _c.Call.Run(func(args mock.Arguments) { | |||
| 62 | + | run() | |||
| 63 | + | }) | |||
| 64 | + | return _c | |||
| 65 | + | } | |||
| 66 | + | ||||
| 67 | + | func (_c *MockAccentPickerUI_Hide_Call) Return() *MockAccentPickerUI_Hide_Call { | |||
| 68 | + | _c.Call.Return() | |||
| 69 | + | return _c | |||
| 70 | + | } | |||
| 71 | + | ||||
| 72 | + | func (_c *MockAccentPickerUI_Hide_Call) RunAndReturn(run func()) *MockAccentPickerUI_Hide_Call { | |||
| 73 | + | _c.Run(run) | |||
| 74 | + | return _c | |||
| 75 | + | } | |||
| 76 | + | ||||
| 77 | + | // IsVisible provides a mock function for the type MockAccentPickerUI | |||
| 78 | + | func (_mock *MockAccentPickerUI) IsVisible() bool { | |||
| 79 | + | ret := _mock.Called() | |||
| 80 | + | ||||
| 81 | + | if len(ret) == 0 { | |||
| 82 | + | panic("no return value specified for IsVisible") | |||
| 83 | + | } | |||
| 84 | + | ||||
| 85 | + | var r0 bool | |||
| 86 | + | if returnFunc, ok := ret.Get(0).(func() bool); ok { | |||
| 87 | + | r0 = returnFunc() | |||
| 88 | + | } else { | |||
| 89 | + | r0 = ret.Get(0).(bool) | |||
| 90 | + | } | |||
| 91 | + | return r0 | |||
| 92 | + | } | |||
| 93 | + | ||||
| 94 | + | // MockAccentPickerUI_IsVisible_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsVisible' | |||
| 95 | + | type MockAccentPickerUI_IsVisible_Call struct { | |||
| 96 | + | *mock.Call | |||
| 97 | + | } | |||
| 98 | + | ||||
| 99 | + | // IsVisible is a helper method to define mock.On call | |||
| 100 | + | func (_e *MockAccentPickerUI_Expecter) IsVisible() *MockAccentPickerUI_IsVisible_Call { | |||
| 101 | + | return &MockAccentPickerUI_IsVisible_Call{Call: _e.mock.On("IsVisible")} | |||
| 102 | + | } | |||
| 103 | + | ||||
| 104 | + | func (_c *MockAccentPickerUI_IsVisible_Call) Run(run func()) *MockAccentPickerUI_IsVisible_Call { | |||
| 105 | + | _c.Call.Run(func(args mock.Arguments) { | |||
| 106 | + | run() | |||
| 107 | + | }) | |||
| 108 | + | return _c | |||
| 109 | + | } | |||
| 110 | + | ||||
| 111 | + | func (_c *MockAccentPickerUI_IsVisible_Call) Return(b bool) *MockAccentPickerUI_IsVisible_Call { | |||
| 112 | + | _c.Call.Return(b) | |||
| 113 | + | return _c | |||
| 114 | + | } | |||
| 115 | + | ||||
| 116 | + | func (_c *MockAccentPickerUI_IsVisible_Call) RunAndReturn(run func() bool) *MockAccentPickerUI_IsVisible_Call { | |||
| 117 | + | _c.Call.Return(run) | |||
| 118 | + | return _c | |||
| 119 | + | } | |||
| 120 | + | ||||
| 121 | + | // Show provides a mock function for the type MockAccentPickerUI | |||
| 122 | + | func (_mock *MockAccentPickerUI) Show(accents []rune, selectedCallback func(accent rune), cancelCallback func()) { | |||
| 123 | + | _mock.Called(accents, selectedCallback, cancelCallback) | |||
| 124 | + | return | |||
| 125 | + | } | |||
| 126 | + | ||||
| 127 | + | // MockAccentPickerUI_Show_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Show' | |||
| 128 | + | type MockAccentPickerUI_Show_Call struct { | |||
| 129 | + | *mock.Call | |||
| 130 | + | } | |||
| 131 | + | ||||
| 132 | + | // Show is a helper method to define mock.On call | |||
| 133 | + | // - accents []rune | |||
| 134 | + | // - selectedCallback func(accent rune) | |||
| 135 | + | // - cancelCallback func() | |||
| 136 | + | func (_e *MockAccentPickerUI_Expecter) Show(accents interface{}, selectedCallback interface{}, cancelCallback interface{}) *MockAccentPickerUI_Show_Call { | |||
| 137 | + | return &MockAccentPickerUI_Show_Call{Call: _e.mock.On("Show", accents, selectedCallback, cancelCallback)} | |||
| 138 | + | } | |||
| 139 | + | ||||
| 140 | + | func (_c *MockAccentPickerUI_Show_Call) Run(run func(accents []rune, selectedCallback func(accent rune), cancelCallback func())) *MockAccentPickerUI_Show_Call { | |||
| 141 | + | _c.Call.Run(func(args mock.Arguments) { | |||
| 142 | + | var arg0 []rune | |||
| 143 | + | if args[0] != nil { | |||
| 144 | + | arg0 = args[0].([]rune) | |||
| 145 | + | } | |||
| 146 | + | var arg1 func(accent rune) | |||
| 147 | + | if args[1] != nil { | |||
| 148 | + | arg1 = args[1].(func(accent rune)) | |||
| 149 | + | } | |||
| 150 | + | var arg2 func() | |||
| 151 | + | if args[2] != nil { | |||
| 152 | + | arg2 = args[2].(func()) | |||
| 153 | + | } | |||
| 154 | + | run( | |||
| 155 | + | arg0, | |||
| 156 | + | arg1, | |||
| 157 | + | arg2, | |||
| 158 | + | ) | |||
| 159 | + | }) | |||
| 160 | + | return _c | |||
| 161 | + | } | |||
| 162 | + | ||||
| 163 | + | func (_c *MockAccentPickerUI_Show_Call) Return() *MockAccentPickerUI_Show_Call { | |||
| 164 | + | _c.Call.Return() | |||
| 165 | + | return _c | |||
| 166 | + | } | |||
| 167 | + | ||||
| 168 | + | func (_c *MockAccentPickerUI_Show_Call) RunAndReturn(run func(accents []rune, selectedCallback func(accent rune), cancelCallback func())) *MockAccentPickerUI_Show_Call { | |||
| 169 | + | _c.Run(run) | |||
| 170 | + | return _c | |||
| 171 | + | } | |||
| 172 | + | ||||
| 17 | // NewMockColorSchemeDetector creates a new instance of MockColorSchemeDetector. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. | 173 | // NewMockColorSchemeDetector creates a new instance of MockColorSchemeDetector. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. | ||
| 18 | // The first argument is typically a *testing.T value. | 174 | // The first argument is typically a *testing.T value. | ||
| 19 | func NewMockColorSchemeDetector(t interface { | 175 | func NewMockColorSchemeDetector(t interface { | ||
| @@ -1438,6 +1594,260 @@ func (_c *MockSessionSpawner_SpawnWithSession_Call) RunAndReturn(run func(sessio | |||||
| 1438 | return _c | 1594 | return _c | ||
| 1439 | } | 1595 | } | ||
| 1440 | 1596 | ||||
| 1597 | + | // NewMockTextInputTarget creates a new instance of MockTextInputTarget. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. | |||
| 1598 | + | // The first argument is typically a *testing.T value. | |||
| 1599 | + | func NewMockTextInputTarget(t interface { | |||
| 1600 | + | mock.TestingT | |||
| 1601 | + | Cleanup(func()) | |||
| 1602 | + | }) *MockTextInputTarget { | |||
| 1603 | + | mock := &MockTextInputTarget{} | |||
| 1604 | + | mock.Mock.Test(t) | |||
| 1605 | + | ||||
| 1606 | + | t.Cleanup(func() { mock.AssertExpectations(t) }) | |||
| 1607 | + | ||||
| 1608 | + | return mock | |||
| 1609 | + | } | |||
| 1610 | + | ||||
| 1611 | + | // MockTextInputTarget is an autogenerated mock type for the TextInputTarget type | |||
| 1612 | + | type MockTextInputTarget struct { | |||
| 1613 | + | mock.Mock | |||
| 1614 | + | } | |||
| 1615 | + | ||||
| 1616 | + | type MockTextInputTarget_Expecter struct { | |||
| 1617 | + | mock *mock.Mock | |||
| 1618 | + | } | |||
| 1619 | + | ||||
| 1620 | + | func (_m *MockTextInputTarget) EXPECT() *MockTextInputTarget_Expecter { | |||
| 1621 | + | return &MockTextInputTarget_Expecter{mock: &_m.Mock} | |||
| 1622 | + | } | |||
| 1623 | + | ||||
| 1624 | + | // DeleteBeforeCursor provides a mock function for the type MockTextInputTarget | |||
| 1625 | + | func (_mock *MockTextInputTarget) DeleteBeforeCursor(ctx context.Context, n int) error { | |||
| 1626 | + | ret := _mock.Called(ctx, n) | |||
| 1627 | + | ||||
| 1628 | + | if len(ret) == 0 { | |||
| 1629 | + | panic("no return value specified for DeleteBeforeCursor") | |||
| 1630 | + | } | |||
| 1631 | + | ||||
| 1632 | + | var r0 error | |||
| 1633 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, int) error); ok { | |||
| 1634 | + | r0 = returnFunc(ctx, n) | |||
| 1635 | + | } else { | |||
| 1636 | + | r0 = ret.Error(0) | |||
| 1637 | + | } | |||
| 1638 | + | return r0 | |||
| 1639 | + | } | |||
| 1640 | + | ||||
| 1641 | + | // MockTextInputTarget_DeleteBeforeCursor_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeleteBeforeCursor' | |||
| 1642 | + | type MockTextInputTarget_DeleteBeforeCursor_Call struct { | |||
| 1643 | + | *mock.Call | |||
| 1644 | + | } | |||
| 1645 | + | ||||
| 1646 | + | // DeleteBeforeCursor is a helper method to define mock.On call | |||
| 1647 | + | // - ctx context.Context | |||
| 1648 | + | // - n int | |||
| 1649 | + | func (_e *MockTextInputTarget_Expecter) DeleteBeforeCursor(ctx interface{}, n interface{}) *MockTextInputTarget_DeleteBeforeCursor_Call { | |||
| 1650 | + | return &MockTextInputTarget_DeleteBeforeCursor_Call{Call: _e.mock.On("DeleteBeforeCursor", ctx, n)} | |||
| 1651 | + | } | |||
| 1652 | + | ||||
| 1653 | + | func (_c *MockTextInputTarget_DeleteBeforeCursor_Call) Run(run func(ctx context.Context, n int)) *MockTextInputTarget_DeleteBeforeCursor_Call { | |||
| 1654 | + | _c.Call.Run(func(args mock.Arguments) { | |||
| 1655 | + | var arg0 context.Context | |||
| 1656 | + | if args[0] != nil { | |||
| 1657 | + | arg0 = args[0].(context.Context) | |||
| 1658 | + | } | |||
| 1659 | + | var arg1 int | |||
| 1660 | + | if args[1] != nil { | |||
| 1661 | + | arg1 = args[1].(int) | |||
| 1662 | + | } | |||
| 1663 | + | run( | |||
| 1664 | + | arg0, | |||
| 1665 | + | arg1, | |||
| 1666 | + | ) | |||
| 1667 | + | }) | |||
| 1668 | + | return _c | |||
| 1669 | + | } | |||
| 1670 | + | ||||
| 1671 | + | func (_c *MockTextInputTarget_DeleteBeforeCursor_Call) Return(err error) *MockTextInputTarget_DeleteBeforeCursor_Call { | |||
| 1672 | + | _c.Call.Return(err) | |||
| 1673 | + | return _c | |||
| 1674 | + | } | |||
| 1675 | + | ||||
| 1676 | + | func (_c *MockTextInputTarget_DeleteBeforeCursor_Call) RunAndReturn(run func(ctx context.Context, n int) error) *MockTextInputTarget_DeleteBeforeCursor_Call { | |||
| 1677 | + | _c.Call.Return(run) | |||
| 1678 | + | return _c | |||
| 1679 | + | } | |||
| 1680 | + | ||||
| 1681 | + | // InsertText provides a mock function for the type MockTextInputTarget | |||
| 1682 | + | func (_mock *MockTextInputTarget) InsertText(ctx context.Context, text string) error { | |||
| 1683 | + | ret := _mock.Called(ctx, text) | |||
| 1684 | + | ||||
| 1685 | + | if len(ret) == 0 { | |||
| 1686 | + | panic("no return value specified for InsertText") | |||
| 1687 | + | } | |||
| 1688 | + | ||||
| 1689 | + | var r0 error | |||
| 1690 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, string) error); ok { | |||
| 1691 | + | r0 = returnFunc(ctx, text) | |||
| 1692 | + | } else { | |||
| 1693 | + | r0 = ret.Error(0) | |||
| 1694 | + | } | |||
| 1695 | + | return r0 | |||
| 1696 | + | } | |||
| 1697 | + | ||||
| 1698 | + | // MockTextInputTarget_InsertText_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'InsertText' | |||
| 1699 | + | type MockTextInputTarget_InsertText_Call struct { | |||
| 1700 | + | *mock.Call | |||
| 1701 | + | } | |||
| 1702 | + | ||||
| 1703 | + | // InsertText is a helper method to define mock.On call | |||
| 1704 | + | // - ctx context.Context | |||
| 1705 | + | // - text string | |||
| 1706 | + | func (_e *MockTextInputTarget_Expecter) InsertText(ctx interface{}, text interface{}) *MockTextInputTarget_InsertText_Call { | |||
| 1707 | + | return &MockTextInputTarget_InsertText_Call{Call: _e.mock.On("InsertText", ctx, text)} | |||
| 1708 | + | } | |||
| 1709 | + | ||||
| 1710 | + | func (_c *MockTextInputTarget_InsertText_Call) Run(run func(ctx context.Context, text string)) *MockTextInputTarget_InsertText_Call { | |||
| 1711 | + | _c.Call.Run(func(args mock.Arguments) { | |||
| 1712 | + | var arg0 context.Context | |||
| 1713 | + | if args[0] != nil { | |||
| 1714 | + | arg0 = args[0].(context.Context) | |||
| 1715 | + | } | |||
| 1716 | + | var arg1 string | |||
| 1717 | + | if args[1] != nil { | |||
| 1718 | + | arg1 = args[1].(string) | |||
| 1719 | + | } | |||
| 1720 | + | run( | |||
| 1721 | + | arg0, | |||
| 1722 | + | arg1, | |||
| 1723 | + | ) | |||
| 1724 | + | }) | |||
| 1725 | + | return _c | |||
| 1726 | + | } | |||
| 1727 | + | ||||
| 1728 | + | func (_c *MockTextInputTarget_InsertText_Call) Return(err error) *MockTextInputTarget_InsertText_Call { | |||
| 1729 | + | _c.Call.Return(err) | |||
| 1730 | + | return _c | |||
| 1731 | + | } | |||
| 1732 | + | ||||
| 1733 | + | func (_c *MockTextInputTarget_InsertText_Call) RunAndReturn(run func(ctx context.Context, text string) error) *MockTextInputTarget_InsertText_Call { | |||
| 1734 | + | _c.Call.Return(run) | |||
| 1735 | + | return _c | |||
| 1736 | + | } | |||
| 1737 | + | ||||
| 1738 | + | // NewMockFocusedInputProvider creates a new instance of MockFocusedInputProvider. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. | |||
| 1739 | + | // The first argument is typically a *testing.T value. | |||
| 1740 | + | func NewMockFocusedInputProvider(t interface { | |||
| 1741 | + | mock.TestingT | |||
| 1742 | + | Cleanup(func()) | |||
| 1743 | + | }) *MockFocusedInputProvider { | |||
| 1744 | + | mock := &MockFocusedInputProvider{} | |||
| 1745 | + | mock.Mock.Test(t) | |||
| 1746 | + | ||||
| 1747 | + | t.Cleanup(func() { mock.AssertExpectations(t) }) | |||
| 1748 | + | ||||
| 1749 | + | return mock | |||
| 1750 | + | } | |||
| 1751 | + | ||||
| 1752 | + | // MockFocusedInputProvider is an autogenerated mock type for the FocusedInputProvider type | |||
| 1753 | + | type MockFocusedInputProvider struct { | |||
| 1754 | + | mock.Mock | |||
| 1755 | + | } | |||
| 1756 | + | ||||
| 1757 | + | type MockFocusedInputProvider_Expecter struct { | |||
| 1758 | + | mock *mock.Mock | |||
| 1759 | + | } | |||
| 1760 | + | ||||
| 1761 | + | func (_m *MockFocusedInputProvider) EXPECT() *MockFocusedInputProvider_Expecter { | |||
| 1762 | + | return &MockFocusedInputProvider_Expecter{mock: &_m.Mock} | |||
| 1763 | + | } | |||
| 1764 | + | ||||
| 1765 | + | // GetFocusedInput provides a mock function for the type MockFocusedInputProvider | |||
| 1766 | + | func (_mock *MockFocusedInputProvider) GetFocusedInput() port.TextInputTarget { | |||
| 1767 | + | ret := _mock.Called() | |||
| 1768 | + | ||||
| 1769 | + | if len(ret) == 0 { | |||
| 1770 | + | panic("no return value specified for GetFocusedInput") | |||
| 1771 | + | } | |||
| 1772 | + | ||||
| 1773 | + | var r0 port.TextInputTarget | |||
| 1774 | + | if returnFunc, ok := ret.Get(0).(func() port.TextInputTarget); ok { | |||
| 1775 | + | r0 = returnFunc() | |||
| 1776 | + | } else { | |||
| 1777 | + | if ret.Get(0) != nil { | |||
| 1778 | + | r0 = ret.Get(0).(port.TextInputTarget) | |||
| 1779 | + | } | |||
| 1780 | + | } | |||
| 1781 | + | return r0 | |||
| 1782 | + | } | |||
| 1783 | + | ||||
| 1784 | + | // MockFocusedInputProvider_GetFocusedInput_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetFocusedInput' | |||
| 1785 | + | type MockFocusedInputProvider_GetFocusedInput_Call struct { | |||
| 1786 | + | *mock.Call | |||
| 1787 | + | } | |||
| 1788 | + | ||||
| 1789 | + | // GetFocusedInput is a helper method to define mock.On call | |||
| 1790 | + | func (_e *MockFocusedInputProvider_Expecter) GetFocusedInput() *MockFocusedInputProvider_GetFocusedInput_Call { | |||
| 1791 | + | return &MockFocusedInputProvider_GetFocusedInput_Call{Call: _e.mock.On("GetFocusedInput")} | |||
| 1792 | + | } | |||
| 1793 | + | ||||
| 1794 | + | func (_c *MockFocusedInputProvider_GetFocusedInput_Call) Run(run func()) *MockFocusedInputProvider_GetFocusedInput_Call { | |||
| 1795 | + | _c.Call.Run(func(args mock.Arguments) { | |||
| 1796 | + | run() | |||
| 1797 | + | }) | |||
| 1798 | + | return _c | |||
| 1799 | + | } | |||
| 1800 | + | ||||
| 1801 | + | func (_c *MockFocusedInputProvider_GetFocusedInput_Call) Return(textInputTarget port.TextInputTarget) *MockFocusedInputProvider_GetFocusedInput_Call { | |||
| 1802 | + | _c.Call.Return(textInputTarget) | |||
| 1803 | + | return _c | |||
| 1804 | + | } | |||
| 1805 | + | ||||
| 1806 | + | func (_c *MockFocusedInputProvider_GetFocusedInput_Call) RunAndReturn(run func() port.TextInputTarget) *MockFocusedInputProvider_GetFocusedInput_Call { | |||
| 1807 | + | _c.Call.Return(run) | |||
| 1808 | + | return _c | |||
| 1809 | + | } | |||
| 1810 | + | ||||
| 1811 | + | // SetFocusedInput provides a mock function for the type MockFocusedInputProvider | |||
| 1812 | + | func (_mock *MockFocusedInputProvider) SetFocusedInput(target port.TextInputTarget) { | |||
| 1813 | + | _mock.Called(target) | |||
| 1814 | + | return | |||
| 1815 | + | } | |||
| 1816 | + | ||||
| 1817 | + | // MockFocusedInputProvider_SetFocusedInput_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetFocusedInput' | |||
| 1818 | + | type MockFocusedInputProvider_SetFocusedInput_Call struct { | |||
| 1819 | + | *mock.Call | |||
| 1820 | + | } | |||
| 1821 | + | ||||
| 1822 | + | // SetFocusedInput is a helper method to define mock.On call | |||
| 1823 | + | // - target port.TextInputTarget | |||
| 1824 | + | func (_e *MockFocusedInputProvider_Expecter) SetFocusedInput(target interface{}) *MockFocusedInputProvider_SetFocusedInput_Call { | |||
| 1825 | + | return &MockFocusedInputProvider_SetFocusedInput_Call{Call: _e.mock.On("SetFocusedInput", target)} | |||
| 1826 | + | } | |||
| 1827 | + | ||||
| 1828 | + | func (_c *MockFocusedInputProvider_SetFocusedInput_Call) Run(run func(target port.TextInputTarget)) *MockFocusedInputProvider_SetFocusedInput_Call { | |||
| 1829 | + | _c.Call.Run(func(args mock.Arguments) { | |||
| 1830 | + | var arg0 port.TextInputTarget | |||
| 1831 | + | if args[0] != nil { | |||
| 1832 | + | arg0 = args[0].(port.TextInputTarget) | |||
| 1833 | + | } | |||
| 1834 | + | run( | |||
| 1835 | + | arg0, | |||
| 1836 | + | ) | |||
| 1837 | + | }) | |||
| 1838 | + | return _c | |||
| 1839 | + | } | |||
| 1840 | + | ||||
| 1841 | + | func (_c *MockFocusedInputProvider_SetFocusedInput_Call) Return() *MockFocusedInputProvider_SetFocusedInput_Call { | |||
| 1842 | + | _c.Call.Return() | |||
| 1843 | + | return _c | |||
| 1844 | + | } | |||
| 1845 | + | ||||
| 1846 | + | func (_c *MockFocusedInputProvider_SetFocusedInput_Call) RunAndReturn(run func(target port.TextInputTarget)) *MockFocusedInputProvider_SetFocusedInput_Call { | |||
| 1847 | + | _c.Run(run) | |||
| 1848 | + | return _c | |||
| 1849 | + | } | |||
| 1850 | + | ||||
| 1441 | // NewMockMemoryPressureApplier creates a new instance of MockMemoryPressureApplier. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. | 1851 | // NewMockMemoryPressureApplier creates a new instance of MockMemoryPressureApplier. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. | ||
| 1442 | // The first argument is typically a *testing.T value. | 1852 | // The first argument is typically a *testing.T value. | ||
| 1443 | func NewMockMemoryPressureApplier(t interface { | 1853 | func NewMockMemoryPressureApplier(t interface { | ||
A internal/application/port/text_input.go +25 -0
| @@ -0,0 +1,25 @@ | |||
| 1 | + | package port | |
| 2 | + | ||
| 3 | + | import "context" | |
| 4 | + | ||
| 5 | + | // TextInputTarget represents a text input field that can receive text insertions. | |
| 6 | + | // Implementations include GTK Entry widgets and WebView text fields. | |
| 7 | + | type TextInputTarget interface { | |
| 8 | + | // InsertText inserts text at the current cursor position. | |
| 9 | + | InsertText(ctx context.Context, text string) error | |
| 10 | + | ||
| 11 | + | // DeleteBeforeCursor deletes n characters before the cursor position. | |
| 12 | + | DeleteBeforeCursor(ctx context.Context, n int) error | |
| 13 | + | } | |
| 14 | + | ||
| 15 | + | // FocusedInputProvider tracks which text input target currently has focus. | |
| 16 | + | // This allows the accent picker to insert text into the correct input field. | |
| 17 | + | type FocusedInputProvider interface { | |
| 18 | + | // GetFocusedInput returns the currently focused text input target. | |
| 19 | + | // Returns nil if no text input has focus. | |
| 20 | + | GetFocusedInput() TextInputTarget | |
| 21 | + | ||
| 22 | + | // SetFocusedInput sets the currently focused text input target. | |
| 23 | + | // Pass nil to clear focus. | |
| 24 | + | SetFocusedInput(target TextInputTarget) | |
| 25 | + | } | |
| @@ -0,0 +1,25 @@ | |||||
| 1 | + | package port | |||
| 2 | + | ||||
| 3 | + | import "context" | |||
| 4 | + | ||||
| 5 | + | // TextInputTarget represents a text input field that can receive text insertions. | |||
| 6 | + | // Implementations include GTK Entry widgets and WebView text fields. | |||
| 7 | + | type TextInputTarget interface { | |||
| 8 | + | // InsertText inserts text at the current cursor position. | |||
| 9 | + | InsertText(ctx context.Context, text string) error | |||
| 10 | + | ||||
| 11 | + | // DeleteBeforeCursor deletes n characters before the cursor position. | |||
| 12 | + | DeleteBeforeCursor(ctx context.Context, n int) error | |||
| 13 | + | } | |||
| 14 | + | ||||
| 15 | + | // FocusedInputProvider tracks which text input target currently has focus. | |||
| 16 | + | // This allows the accent picker to insert text into the correct input field. | |||
| 17 | + | type FocusedInputProvider interface { | |||
| 18 | + | // GetFocusedInput returns the currently focused text input target. | |||
| 19 | + | // Returns nil if no text input has focus. | |||
| 20 | + | GetFocusedInput() TextInputTarget | |||
| 21 | + | ||||
| 22 | + | // SetFocusedInput sets the currently focused text input target. | |||
| 23 | + | // Pass nil to clear focus. | |||
| 24 | + | SetFocusedInput(target TextInputTarget) | |||
| 25 | + | } | |||
A internal/application/usecase/insert_accent.go +277 -0
| @@ -0,0 +1,277 @@ | |||
| 1 | + | // Package usecase contains application use cases that orchestrate domain logic. | |
| 2 | + | package usecase | |
| 3 | + | ||
| 4 | + | import ( | |
| 5 | + | "context" | |
| 6 | + | "sync" | |
| 7 | + | "time" | |
| 8 | + | ||
| 9 | + | "github.com/bnema/dumber/internal/application/port" | |
| 10 | + | "github.com/bnema/dumber/internal/domain/entity" | |
| 11 | + | "github.com/bnema/dumber/internal/logging" | |
| 12 | + | ) | |
| 13 | + | ||
| 14 | + | // LongPressDelay is the duration a key must be held to trigger the accent picker. | |
| 15 | + | const LongPressDelay = 400 * time.Millisecond | |
| 16 | + | ||
| 17 | + | // InsertAccentUseCase handles long-press detection and accent selection. | |
| 18 | + | // When a key with accent variants is held for 400ms, it shows an accent picker | |
| 19 | + | // and inserts the selected accent into the focused text input. | |
| 20 | + | type InsertAccentUseCase struct { | |
| 21 | + | focusProvider port.FocusedInputProvider | |
| 22 | + | accentPicker port.AccentPickerUI | |
| 23 | + | ||
| 24 | + | // Current long-press state | |
| 25 | + | pressedChar rune | |
| 26 | + | shiftHeld bool | |
| 27 | + | timer *time.Timer | |
| 28 | + | pickerVisible bool | |
| 29 | + | repeatCount int // Number of key repeats while timer is pending | |
| 30 | + | targetInput port.TextInputTarget // The input that was focused when long-press started | |
| 31 | + | ||
| 32 | + | // Callback to show picker on GTK main thread | |
| 33 | + | showPickerOnMainThread func(fn func()) | |
| 34 | + | ||
| 35 | + | mu sync.Mutex | |
| 36 | + | } | |
| 37 | + | ||
| 38 | + | // NewInsertAccentUseCase creates a new accent insertion use case. | |
| 39 | + | // showOnMainThread is a callback that runs a function on the GTK main thread | |
| 40 | + | // (typically using glib.IdleAdd). This is required because the timer fires | |
| 41 | + | // on a background goroutine. | |
| 42 | + | func NewInsertAccentUseCase( | |
| 43 | + | focusProvider port.FocusedInputProvider, | |
| 44 | + | accentPicker port.AccentPickerUI, | |
| 45 | + | showOnMainThread func(fn func()), | |
| 46 | + | ) *InsertAccentUseCase { | |
| 47 | + | return &InsertAccentUseCase{ | |
| 48 | + | focusProvider: focusProvider, | |
| 49 | + | accentPicker: accentPicker, | |
| 50 | + | showPickerOnMainThread: showOnMainThread, | |
| 51 | + | } | |
| 52 | + | } | |
| 53 | + | ||
| 54 | + | // OnKeyPressed handles a key press event. | |
| 55 | + | // Returns true if the use case is handling this key and the caller should suppress it. | |
| 56 | + | // Note: Due to WebKit's input handling, key suppression may not be fully effective. | |
| 57 | + | // When the picker shows, we delete any repeated characters that slipped through. | |
| 58 | + | func (uc *InsertAccentUseCase) OnKeyPressed(ctx context.Context, char rune, shiftHeld bool) bool { | |
| 59 | + | log := logging.FromContext(ctx) | |
| 60 | + | ||
| 61 | + | uc.mu.Lock() | |
| 62 | + | defer uc.mu.Unlock() | |
| 63 | + | ||
| 64 | + | // If picker is visible, block all keys (picker handles them) | |
| 65 | + | if uc.pickerVisible { | |
| 66 | + | return true | |
| 67 | + | } | |
| 68 | + | ||
| 69 | + | // If the same key is still pressed (key repeat), count it | |
| 70 | + | // We try to block it but also track repeats in case blocking doesn't work | |
| 71 | + | if uc.pressedChar == char && uc.timer != nil { | |
| 72 | + | uc.repeatCount++ | |
| 73 | + | log.Debug(). | |
| 74 | + | Str("char", string(char)). | |
| 75 | + | Int("repeat_count", uc.repeatCount). | |
| 76 | + | Msg("key repeat detected") | |
| 77 | + | return true // Try to block repeat keys | |
| 78 | + | } | |
| 79 | + | ||
| 80 | + | // Cancel any pending timer for a different key | |
| 81 | + | uc.cancelTimerLocked() | |
| 82 | + | ||
| 83 | + | // Check if this character has accents | |
| 84 | + | if !entity.HasAccents(char) { | |
| 85 | + | return false | |
| 86 | + | } | |
| 87 | + | ||
| 88 | + | // Capture the currently focused input - we'll insert into this target | |
| 89 | + | // even if focus changes when the picker appears | |
| 90 | + | uc.targetInput = uc.focusProvider.GetFocusedInput() | |
| 91 | + | if uc.targetInput == nil { | |
| 92 | + | log.Debug().Msg("no focused input, skipping accent detection") | |
| 93 | + | return false | |
| 94 | + | } | |
| 95 | + | ||
| 96 | + | // Start long-press detection | |
| 97 | + | uc.pressedChar = char | |
| 98 | + | uc.shiftHeld = shiftHeld | |
| 99 | + | uc.repeatCount = 0 // Reset repeat counter | |
| 100 | + | ||
| 101 | + | log.Debug(). | |
| 102 | + | Str("char", string(char)). | |
| 103 | + | Bool("shift", shiftHeld). | |
| 104 | + | Msg("starting long-press detection") | |
| 105 | + | ||
| 106 | + | // Use context.Background() since timer fires after delay and original ctx may be canceled | |
| 107 | + | uc.timer = time.AfterFunc(LongPressDelay, func() { | |
| 108 | + | uc.onLongPressTriggered(context.Background()) | |
| 109 | + | }) | |
| 110 | + | ||
| 111 | + | // Allow the first key press through (user might just want to type quickly) | |
| 112 | + | return false | |
| 113 | + | } | |
| 114 | + | ||
| 115 | + | // OnKeyReleased handles a key release event. | |
| 116 | + | // This cancels any pending long-press detection. | |
| 117 | + | func (uc *InsertAccentUseCase) OnKeyReleased(ctx context.Context, char rune) { | |
| 118 | + | log := logging.FromContext(ctx) | |
| 119 | + | ||
| 120 | + | uc.mu.Lock() | |
| 121 | + | defer uc.mu.Unlock() | |
| 122 | + | ||
| 123 | + | // Only cancel if it's the same key that was pressed | |
| 124 | + | if uc.pressedChar == char && uc.timer != nil { | |
| 125 | + | log.Debug(). | |
| 126 | + | Str("char", string(char)). | |
| 127 | + | Msg("key released before long-press threshold") | |
| 128 | + | uc.cancelTimerLocked() | |
| 129 | + | } | |
| 130 | + | } | |
| 131 | + | ||
| 132 | + | // Cancel cancels any pending long-press detection and hides the picker. | |
| 133 | + | func (uc *InsertAccentUseCase) Cancel(ctx context.Context) { | |
| 134 | + | log := logging.FromContext(ctx) | |
| 135 | + | ||
| 136 | + | uc.mu.Lock() | |
| 137 | + | defer uc.mu.Unlock() | |
| 138 | + | ||
| 139 | + | if uc.timer != nil || uc.pickerVisible { | |
| 140 | + | log.Debug().Msg("canceling accent picker") | |
| 141 | + | } | |
| 142 | + | ||
| 143 | + | uc.cancelTimerLocked() | |
| 144 | + | if uc.pickerVisible { | |
| 145 | + | uc.accentPicker.Hide() | |
| 146 | + | uc.pickerVisible = false | |
| 147 | + | } | |
| 148 | + | } | |
| 149 | + | ||
| 150 | + | // IsPickerVisible returns true if the accent picker is currently visible. | |
| 151 | + | func (uc *InsertAccentUseCase) IsPickerVisible() bool { | |
| 152 | + | uc.mu.Lock() | |
| 153 | + | defer uc.mu.Unlock() | |
| 154 | + | return uc.pickerVisible | |
| 155 | + | } | |
| 156 | + | ||
| 157 | + | // onLongPressTriggered is called when the long-press timer fires. | |
| 158 | + | // This runs on a background goroutine, so we use showPickerOnMainThread. | |
| 159 | + | func (uc *InsertAccentUseCase) onLongPressTriggered(ctx context.Context) { | |
| 160 | + | log := logging.FromContext(ctx) | |
| 161 | + | ||
| 162 | + | uc.mu.Lock() | |
| 163 | + | char := uc.pressedChar | |
| 164 | + | uppercase := uc.shiftHeld | |
| 165 | + | repeatCount := uc.repeatCount | |
| 166 | + | uc.timer = nil | |
| 167 | + | uc.mu.Unlock() | |
| 168 | + | ||
| 169 | + | accents := entity.GetAccents(char, uppercase) | |
| 170 | + | if len(accents) == 0 { | |
| 171 | + | return | |
| 172 | + | } | |
| 173 | + | ||
| 174 | + | log.Debug(). | |
| 175 | + | Str("char", string(char)). | |
| 176 | + | Bool("uppercase", uppercase). | |
| 177 | + | Int("accents", len(accents)). | |
| 178 | + | Int("repeat_count", repeatCount). | |
| 179 | + | Msg("long-press triggered, showing accent picker") | |
| 180 | + | ||
| 181 | + | // Show picker on main thread | |
| 182 | + | uc.showPickerOnMainThread(func() { | |
| 183 | + | uc.showPicker(ctx, accents, repeatCount) | |
| 184 | + | }) | |
| 185 | + | } | |
| 186 | + | ||
| 187 | + | // showPicker displays the accent picker UI. | |
| 188 | + | // Must be called on the GTK main thread. | |
| 189 | + | // repeatCount is the number of repeated characters that may have been typed | |
| 190 | + | // (despite our attempt to block them) and need to be deleted. | |
| 191 | + | func (uc *InsertAccentUseCase) showPicker(ctx context.Context, accents []rune, repeatCount int) { | |
| 192 | + | log := logging.FromContext(ctx) | |
| 193 | + | ||
| 194 | + | uc.mu.Lock() | |
| 195 | + | target := uc.targetInput // Use the captured target, not current focus | |
| 196 | + | uc.mu.Unlock() | |
| 197 | + | ||
| 198 | + | // Delete any repeated characters that slipped through | |
| 199 | + | // +1 for the initial character that was typed | |
| 200 | + | charsToDelete := repeatCount + 1 | |
| 201 | + | if charsToDelete > 0 && target != nil { | |
| 202 | + | log.Debug(). | |
| 203 | + | Int("chars_to_delete", charsToDelete). | |
| 204 | + | Msg("deleting repeated characters before showing picker") | |
| 205 | + | if err := target.DeleteBeforeCursor(ctx, charsToDelete); err != nil { | |
| 206 | + | log.Warn().Err(err).Msg("failed to delete repeated characters") | |
| 207 | + | } | |
| 208 | + | } | |
| 209 | + | ||
| 210 | + | uc.mu.Lock() | |
| 211 | + | uc.pickerVisible = true | |
| 212 | + | uc.mu.Unlock() | |
| 213 | + | ||
| 214 | + | uc.accentPicker.Show(accents, func(accent rune) { | |
| 215 | + | uc.onAccentSelected(ctx, accent) | |
| 216 | + | }, func() { | |
| 217 | + | uc.onPickerCanceled(ctx) | |
| 218 | + | }) | |
| 219 | + | ||
| 220 | + | log.Debug().Msg("accent picker shown") | |
| 221 | + | } | |
| 222 | + | ||
| 223 | + | // onAccentSelected is called when the user selects an accent. | |
| 224 | + | func (uc *InsertAccentUseCase) onAccentSelected(ctx context.Context, accent rune) { | |
| 225 | + | log := logging.FromContext(ctx) | |
| 226 | + | ||
| 227 | + | uc.mu.Lock() | |
| 228 | + | target := uc.targetInput // Use the captured target, not current focus | |
| 229 | + | uc.pickerVisible = false | |
| 230 | + | uc.pressedChar = 0 | |
| 231 | + | uc.repeatCount = 0 | |
| 232 | + | uc.targetInput = nil | |
| 233 | + | uc.mu.Unlock() | |
| 234 | + | ||
| 235 | + | uc.accentPicker.Hide() | |
| 236 | + | ||
| 237 | + | if target == nil { | |
| 238 | + | log.Warn().Msg("no target input for accent insertion") | |
| 239 | + | return | |
| 240 | + | } | |
| 241 | + | ||
| 242 | + | log.Debug(). | |
| 243 | + | Str("accent", string(accent)). | |
| 244 | + | Msg("inserting accent") | |
| 245 | + | ||
| 246 | + | if err := target.InsertText(ctx, string(accent)); err != nil { | |
| 247 | + | log.Error().Err(err).Msg("failed to insert accent") | |
| 248 | + | } | |
| 249 | + | } | |
| 250 | + | ||
| 251 | + | // onPickerCanceled is called when the user cancels the picker (Escape). | |
| 252 | + | func (uc *InsertAccentUseCase) onPickerCanceled(ctx context.Context) { | |
| 253 | + | log := logging.FromContext(ctx) | |
| 254 | + | ||
| 255 | + | uc.mu.Lock() | |
| 256 | + | uc.pickerVisible = false | |
| 257 | + | uc.pressedChar = 0 | |
| 258 | + | uc.repeatCount = 0 | |
| 259 | + | uc.targetInput = nil | |
| 260 | + | uc.mu.Unlock() | |
| 261 | + | ||
| 262 | + | uc.accentPicker.Hide() | |
| 263 | + | ||
| 264 | + | log.Debug().Msg("accent picker canceled") | |
| 265 | + | } | |
| 266 | + | ||
| 267 | + | // cancelTimerLocked cancels the long-press timer. | |
| 268 | + | // Must be called with uc.mu held. | |
| 269 | + | func (uc *InsertAccentUseCase) cancelTimerLocked() { | |
| 270 | + | if uc.timer != nil { | |
| 271 | + | uc.timer.Stop() | |
| 272 | + | uc.timer = nil | |
| 273 | + | } | |
| 274 | + | uc.pressedChar = 0 | |
| 275 | + | uc.repeatCount = 0 | |
| 276 | + | uc.targetInput = nil | |
| 277 | + | } | |
| @@ -0,0 +1,277 @@ | |||||
| 1 | + | // Package usecase contains application use cases that orchestrate domain logic. | |||
| 2 | + | package usecase | |||
| 3 | + | ||||
| 4 | + | import ( | |||
| 5 | + | "context" | |||
| 6 | + | "sync" | |||
| 7 | + | "time" | |||
| 8 | + | ||||
| 9 | + | "github.com/bnema/dumber/internal/application/port" | |||
| 10 | + | "github.com/bnema/dumber/internal/domain/entity" | |||
| 11 | + | "github.com/bnema/dumber/internal/logging" | |||
| 12 | + | ) | |||
| 13 | + | ||||
| 14 | + | // LongPressDelay is the duration a key must be held to trigger the accent picker. | |||
| 15 | + | const LongPressDelay = 400 * time.Millisecond | |||
| 16 | + | ||||
| 17 | + | // InsertAccentUseCase handles long-press detection and accent selection. | |||
| 18 | + | // When a key with accent variants is held for 400ms, it shows an accent picker | |||
| 19 | + | // and inserts the selected accent into the focused text input. | |||
| 20 | + | type InsertAccentUseCase struct { | |||
| 21 | + | focusProvider port.FocusedInputProvider | |||
| 22 | + | accentPicker port.AccentPickerUI | |||
| 23 | + | ||||
| 24 | + | // Current long-press state | |||
| 25 | + | pressedChar rune | |||
| 26 | + | shiftHeld bool | |||
| 27 | + | timer *time.Timer | |||
| 28 | + | pickerVisible bool | |||
| 29 | + | repeatCount int // Number of key repeats while timer is pending | |||
| 30 | + | targetInput port.TextInputTarget // The input that was focused when long-press started | |||
| 31 | + | ||||
| 32 | + | // Callback to show picker on GTK main thread | |||
| 33 | + | showPickerOnMainThread func(fn func()) | |||
| 34 | + | ||||
| 35 | + | mu sync.Mutex | |||
| 36 | + | } | |||
| 37 | + | ||||
| 38 | + | // NewInsertAccentUseCase creates a new accent insertion use case. | |||
| 39 | + | // showOnMainThread is a callback that runs a function on the GTK main thread | |||
| 40 | + | // (typically using glib.IdleAdd). This is required because the timer fires | |||
| 41 | + | // on a background goroutine. | |||
| 42 | + | func NewInsertAccentUseCase( | |||
| 43 | + | focusProvider port.FocusedInputProvider, | |||
| 44 | + | accentPicker port.AccentPickerUI, | |||
| 45 | + | showOnMainThread func(fn func()), | |||
| 46 | + | ) *InsertAccentUseCase { | |||
| 47 | + | return &InsertAccentUseCase{ | |||
| 48 | + | focusProvider: focusProvider, | |||
| 49 | + | accentPicker: accentPicker, | |||
| 50 | + | showPickerOnMainThread: showOnMainThread, | |||
| 51 | + | } | |||
| 52 | + | } | |||
| 53 | + | ||||
| 54 | + | // OnKeyPressed handles a key press event. | |||
| 55 | + | // Returns true if the use case is handling this key and the caller should suppress it. | |||
| 56 | + | // Note: Due to WebKit's input handling, key suppression may not be fully effective. | |||
| 57 | + | // When the picker shows, we delete any repeated characters that slipped through. | |||
| 58 | + | func (uc *InsertAccentUseCase) OnKeyPressed(ctx context.Context, char rune, shiftHeld bool) bool { | |||
| 59 | + | log := logging.FromContext(ctx) | |||
| 60 | + | ||||
| 61 | + | uc.mu.Lock() | |||
| 62 | + | defer uc.mu.Unlock() | |||
| 63 | + | ||||
| 64 | + | // If picker is visible, block all keys (picker handles them) | |||
| 65 | + | if uc.pickerVisible { | |||
| 66 | + | return true | |||
| 67 | + | } | |||
| 68 | + | ||||
| 69 | + | // If the same key is still pressed (key repeat), count it | |||
| 70 | + | // We try to block it but also track repeats in case blocking doesn't work | |||
| 71 | + | if uc.pressedChar == char && uc.timer != nil { | |||
| 72 | + | uc.repeatCount++ | |||
| 73 | + | log.Debug(). | |||
| 74 | + | Str("char", string(char)). | |||
| 75 | + | Int("repeat_count", uc.repeatCount). | |||
| 76 | + | Msg("key repeat detected") | |||
| 77 | + | return true // Try to block repeat keys | |||
| 78 | + | } | |||
| 79 | + | ||||
| 80 | + | // Cancel any pending timer for a different key | |||
| 81 | + | uc.cancelTimerLocked() | |||
| 82 | + | ||||
| 83 | + | // Check if this character has accents | |||
| 84 | + | if !entity.HasAccents(char) { | |||
| 85 | + | return false | |||
| 86 | + | } | |||
| 87 | + | ||||
| 88 | + | // Capture the currently focused input - we'll insert into this target | |||
| 89 | + | // even if focus changes when the picker appears | |||
| 90 | + | uc.targetInput = uc.focusProvider.GetFocusedInput() | |||
| 91 | + | if uc.targetInput == nil { | |||
| 92 | + | log.Debug().Msg("no focused input, skipping accent detection") | |||
| 93 | + | return false | |||
| 94 | + | } | |||
| 95 | + | ||||
| 96 | + | // Start long-press detection | |||
| 97 | + | uc.pressedChar = char | |||
| 98 | + | uc.shiftHeld = shiftHeld | |||
| 99 | + | uc.repeatCount = 0 // Reset repeat counter | |||
| 100 | + | ||||
| 101 | + | log.Debug(). | |||
| 102 | + | Str("char", string(char)). | |||
| 103 | + | Bool("shift", shiftHeld). | |||
| 104 | + | Msg("starting long-press detection") | |||
| 105 | + | ||||
| 106 | + | // Use context.Background() since timer fires after delay and original ctx may be canceled | |||
| 107 | + | uc.timer = time.AfterFunc(LongPressDelay, func() { | |||
| 108 | + | uc.onLongPressTriggered(context.Background()) | |||
| 109 | + | }) | |||
| 110 | + | ||||
| 111 | + | // Allow the first key press through (user might just want to type quickly) | |||
| 112 | + | return false | |||
| 113 | + | } | |||
| 114 | + | ||||
| 115 | + | // OnKeyReleased handles a key release event. | |||
| 116 | + | // This cancels any pending long-press detection. | |||
| 117 | + | func (uc *InsertAccentUseCase) OnKeyReleased(ctx context.Context, char rune) { | |||
| 118 | + | log := logging.FromContext(ctx) | |||
| 119 | + | ||||
| 120 | + | uc.mu.Lock() | |||
| 121 | + | defer uc.mu.Unlock() | |||
| 122 | + | ||||
| 123 | + | // Only cancel if it's the same key that was pressed | |||
| 124 | + | if uc.pressedChar == char && uc.timer != nil { | |||
| 125 | + | log.Debug(). | |||
| 126 | + | Str("char", string(char)). | |||
| 127 | + | Msg("key released before long-press threshold") | |||
| 128 | + | uc.cancelTimerLocked() | |||
| 129 | + | } | |||
| 130 | + | } | |||
| 131 | + | ||||
| 132 | + | // Cancel cancels any pending long-press detection and hides the picker. | |||
| 133 | + | func (uc *InsertAccentUseCase) Cancel(ctx context.Context) { | |||
| 134 | + | log := logging.FromContext(ctx) | |||
| 135 | + | ||||
| 136 | + | uc.mu.Lock() | |||
| 137 | + | defer uc.mu.Unlock() | |||
| 138 | + | ||||
| 139 | + | if uc.timer != nil || uc.pickerVisible { | |||
| 140 | + | log.Debug().Msg("canceling accent picker") | |||
| 141 | + | } | |||
| 142 | + | ||||
| 143 | + | uc.cancelTimerLocked() | |||
| 144 | + | if uc.pickerVisible { | |||
| 145 | + | uc.accentPicker.Hide() | |||
| 146 | + | uc.pickerVisible = false | |||
| 147 | + | } | |||
| 148 | + | } | |||
| 149 | + | ||||
| 150 | + | // IsPickerVisible returns true if the accent picker is currently visible. | |||
| 151 | + | func (uc *InsertAccentUseCase) IsPickerVisible() bool { | |||
| 152 | + | uc.mu.Lock() | |||
| 153 | + | defer uc.mu.Unlock() | |||
| 154 | + | return uc.pickerVisible | |||
| 155 | + | } | |||
| 156 | + | ||||
| 157 | + | // onLongPressTriggered is called when the long-press timer fires. | |||
| 158 | + | // This runs on a background goroutine, so we use showPickerOnMainThread. | |||
| 159 | + | func (uc *InsertAccentUseCase) onLongPressTriggered(ctx context.Context) { | |||
| 160 | + | log := logging.FromContext(ctx) | |||
| 161 | + | ||||
| 162 | + | uc.mu.Lock() | |||
| 163 | + | char := uc.pressedChar | |||
| 164 | + | uppercase := uc.shiftHeld | |||
| 165 | + | repeatCount := uc.repeatCount | |||
| 166 | + | uc.timer = nil | |||
| 167 | + | uc.mu.Unlock() | |||
| 168 | + | ||||
| 169 | + | accents := entity.GetAccents(char, uppercase) | |||
| 170 | + | if len(accents) == 0 { | |||
| 171 | + | return | |||
| 172 | + | } | |||
| 173 | + | ||||
| 174 | + | log.Debug(). | |||
| 175 | + | Str("char", string(char)). | |||
| 176 | + | Bool("uppercase", uppercase). | |||
| 177 | + | Int("accents", len(accents)). | |||
| 178 | + | Int("repeat_count", repeatCount). | |||
| 179 | + | Msg("long-press triggered, showing accent picker") | |||
| 180 | + | ||||
| 181 | + | // Show picker on main thread | |||
| 182 | + | uc.showPickerOnMainThread(func() { | |||
| 183 | + | uc.showPicker(ctx, accents, repeatCount) | |||
| 184 | + | }) | |||
| 185 | + | } | |||
| 186 | + | ||||
| 187 | + | // showPicker displays the accent picker UI. | |||
| 188 | + | // Must be called on the GTK main thread. | |||
| 189 | + | // repeatCount is the number of repeated characters that may have been typed | |||
| 190 | + | // (despite our attempt to block them) and need to be deleted. | |||
| 191 | + | func (uc *InsertAccentUseCase) showPicker(ctx context.Context, accents []rune, repeatCount int) { | |||
| 192 | + | log := logging.FromContext(ctx) | |||
| 193 | + | ||||
| 194 | + | uc.mu.Lock() | |||
| 195 | + | target := uc.targetInput // Use the captured target, not current focus | |||
| 196 | + | uc.mu.Unlock() | |||
| 197 | + | ||||
| 198 | + | // Delete any repeated characters that slipped through | |||
| 199 | + | // +1 for the initial character that was typed | |||
| 200 | + | charsToDelete := repeatCount + 1 | |||
| 201 | + | if charsToDelete > 0 && target != nil { | |||
| 202 | + | log.Debug(). | |||
| 203 | + | Int("chars_to_delete", charsToDelete). | |||
| 204 | + | Msg("deleting repeated characters before showing picker") | |||
| 205 | + | if err := target.DeleteBeforeCursor(ctx, charsToDelete); err != nil { | |||
| 206 | + | log.Warn().Err(err).Msg("failed to delete repeated characters") | |||
| 207 | + | } | |||
| 208 | + | } | |||
| 209 | + | ||||
| 210 | + | uc.mu.Lock() | |||
| 211 | + | uc.pickerVisible = true | |||
| 212 | + | uc.mu.Unlock() | |||
| 213 | + | ||||
| 214 | + | uc.accentPicker.Show(accents, func(accent rune) { | |||
| 215 | + | uc.onAccentSelected(ctx, accent) | |||
| 216 | + | }, func() { | |||
| 217 | + | uc.onPickerCanceled(ctx) | |||
| 218 | + | }) | |||
| 219 | + | ||||
| 220 | + | log.Debug().Msg("accent picker shown") | |||
| 221 | + | } | |||
| 222 | + | ||||
| 223 | + | // onAccentSelected is called when the user selects an accent. | |||
| 224 | + | func (uc *InsertAccentUseCase) onAccentSelected(ctx context.Context, accent rune) { | |||
| 225 | + | log := logging.FromContext(ctx) | |||
| 226 | + | ||||
| 227 | + | uc.mu.Lock() | |||
| 228 | + | target := uc.targetInput // Use the captured target, not current focus | |||
| 229 | + | uc.pickerVisible = false | |||
| 230 | + | uc.pressedChar = 0 | |||
| 231 | + | uc.repeatCount = 0 | |||
| 232 | + | uc.targetInput = nil | |||
| 233 | + | uc.mu.Unlock() | |||
| 234 | + | ||||
| 235 | + | uc.accentPicker.Hide() | |||
| 236 | + | ||||
| 237 | + | if target == nil { | |||
| 238 | + | log.Warn().Msg("no target input for accent insertion") | |||
| 239 | + | return | |||
| 240 | + | } | |||
| 241 | + | ||||
| 242 | + | log.Debug(). | |||
| 243 | + | Str("accent", string(accent)). | |||
| 244 | + | Msg("inserting accent") | |||
| 245 | + | ||||
| 246 | + | if err := target.InsertText(ctx, string(accent)); err != nil { | |||
| 247 | + | log.Error().Err(err).Msg("failed to insert accent") | |||
| 248 | + | } | |||
| 249 | + | } | |||
| 250 | + | ||||
| 251 | + | // onPickerCanceled is called when the user cancels the picker (Escape). | |||
| 252 | + | func (uc *InsertAccentUseCase) onPickerCanceled(ctx context.Context) { | |||
| 253 | + | log := logging.FromContext(ctx) | |||
| 254 | + | ||||
| 255 | + | uc.mu.Lock() | |||
| 256 | + | uc.pickerVisible = false | |||
| 257 | + | uc.pressedChar = 0 | |||
| 258 | + | uc.repeatCount = 0 | |||
| 259 | + | uc.targetInput = nil | |||
| 260 | + | uc.mu.Unlock() | |||
| 261 | + | ||||
| 262 | + | uc.accentPicker.Hide() | |||
| 263 | + | ||||
| 264 | + | log.Debug().Msg("accent picker canceled") | |||
| 265 | + | } | |||
| 266 | + | ||||
| 267 | + | // cancelTimerLocked cancels the long-press timer. | |||
| 268 | + | // Must be called with uc.mu held. | |||
| 269 | + | func (uc *InsertAccentUseCase) cancelTimerLocked() { | |||
| 270 | + | if uc.timer != nil { | |||
| 271 | + | uc.timer.Stop() | |||
| 272 | + | uc.timer = nil | |||
| 273 | + | } | |||
| 274 | + | uc.pressedChar = 0 | |||
| 275 | + | uc.repeatCount = 0 | |||
| 276 | + | uc.targetInput = nil | |||
| 277 | + | } | |||
A internal/application/usecase/insert_accent_test.go +219 -0
| @@ -0,0 +1,219 @@ | |||
| 1 | + | package usecase | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "context" | |
| 5 | + | "sync" | |
| 6 | + | "testing" | |
| 7 | + | "time" | |
| 8 | + | ||
| 9 | + | "github.com/bnema/dumber/internal/application/port/mocks" | |
| 10 | + | "github.com/stretchr/testify/assert" | |
| 11 | + | "github.com/stretchr/testify/mock" | |
| 12 | + | ) | |
| 13 | + | ||
| 14 | + | func TestInsertAccentUseCase_OnKeyPressed_NoAccents(t *testing.T) { | |
| 15 | + | ctx := context.Background() | |
| 16 | + | focusProvider := mocks.NewMockFocusedInputProvider(t) | |
| 17 | + | accentPicker := mocks.NewMockAccentPickerUI(t) | |
| 18 | + | ||
| 19 | + | uc := NewInsertAccentUseCase(focusProvider, accentPicker, func(fn func()) { fn() }) | |
| 20 | + | ||
| 21 | + | // Key 'x' has no accents - should return false and not start timer | |
| 22 | + | handled := uc.OnKeyPressed(ctx, 'x', false) | |
| 23 | + | assert.False(t, handled, "OnKeyPressed should return false for key without accents") | |
| 24 | + | assert.False(t, uc.IsPickerVisible(), "picker should not be visible") | |
| 25 | + | } | |
| 26 | + | ||
| 27 | + | func TestInsertAccentUseCase_OnKeyPressed_HasAccents(t *testing.T) { | |
| 28 | + | ctx := context.Background() | |
| 29 | + | focusProvider := mocks.NewMockFocusedInputProvider(t) | |
| 30 | + | accentPicker := mocks.NewMockAccentPickerUI(t) | |
| 31 | + | textInput := mocks.NewMockTextInputTarget(t) | |
| 32 | + | ||
| 33 | + | // GetFocusedInput is called when starting long-press to capture target | |
| 34 | + | focusProvider.EXPECT().GetFocusedInput().Return(textInput).Maybe() | |
| 35 | + | ||
| 36 | + | uc := NewInsertAccentUseCase(focusProvider, accentPicker, func(fn func()) { fn() }) | |
| 37 | + | ||
| 38 | + | // Key 'e' has accents - should return false initially (timer started) | |
| 39 | + | handled := uc.OnKeyPressed(ctx, 'e', false) | |
| 40 | + | assert.False(t, handled, "OnKeyPressed should return false while timer is pending") | |
| 41 | + | ||
| 42 | + | // Cancel to clean up timer | |
| 43 | + | uc.Cancel(ctx) | |
| 44 | + | } | |
| 45 | + | ||
| 46 | + | func TestInsertAccentUseCase_OnKeyReleased_CancelsTimer(t *testing.T) { | |
| 47 | + | ctx := context.Background() | |
| 48 | + | focusProvider := mocks.NewMockFocusedInputProvider(t) | |
| 49 | + | accentPicker := mocks.NewMockAccentPickerUI(t) | |
| 50 | + | textInput := mocks.NewMockTextInputTarget(t) | |
| 51 | + | ||
| 52 | + | // GetFocusedInput is called when starting long-press to capture target | |
| 53 | + | focusProvider.EXPECT().GetFocusedInput().Return(textInput).Maybe() | |
| 54 | + | ||
| 55 | + | timerFired := false | |
| 56 | + | uc := NewInsertAccentUseCase(focusProvider, accentPicker, func(fn func()) { | |
| 57 | + | timerFired = true | |
| 58 | + | fn() | |
| 59 | + | }) | |
| 60 | + | ||
| 61 | + | // Start long-press detection | |
| 62 | + | uc.OnKeyPressed(ctx, 'e', false) | |
| 63 | + | ||
| 64 | + | // Release before timer fires | |
| 65 | + | uc.OnKeyReleased(ctx, 'e') | |
| 66 | + | ||
| 67 | + | // Wait a bit longer than LongPressDelay | |
| 68 | + | time.Sleep(LongPressDelay + 50*time.Millisecond) | |
| 69 | + | ||
| 70 | + | assert.False(t, timerFired, "timer should have been canceled by key release") | |
| 71 | + | } | |
| 72 | + | ||
| 73 | + | func TestInsertAccentUseCase_LongPress_ShowsPicker(t *testing.T) { | |
| 74 | + | ctx := context.Background() | |
| 75 | + | focusProvider := mocks.NewMockFocusedInputProvider(t) | |
| 76 | + | accentPicker := mocks.NewMockAccentPickerUI(t) | |
| 77 | + | textInput := mocks.NewMockTextInputTarget(t) | |
| 78 | + | ||
| 79 | + | var wg sync.WaitGroup | |
| 80 | + | wg.Add(1) | |
| 81 | + | ||
| 82 | + | // GetFocusedInput is called when showing picker to delete repeated characters | |
| 83 | + | focusProvider.EXPECT().GetFocusedInput().Return(textInput).Maybe() | |
| 84 | + | // DeleteBeforeCursor will be called to delete the initial character + any repeats | |
| 85 | + | textInput.EXPECT().DeleteBeforeCursor(mock.Anything, mock.AnythingOfType("int")).Return(nil).Maybe() | |
| 86 | + | ||
| 87 | + | // Expect Show to be called with accents for 'e' | |
| 88 | + | accentPicker.EXPECT(). | |
| 89 | + | Show(mock.MatchedBy(func(accents []rune) bool { | |
| 90 | + | return len(accents) > 0 && accents[0] == 'è' | |
| 91 | + | }), mock.AnythingOfType("func(int32)"), mock.AnythingOfType("func()")). | |
| 92 | + | Run(func(_ []rune, _ func(rune), _ func()) { | |
| 93 | + | wg.Done() | |
| 94 | + | }). | |
| 95 | + | Once() | |
| 96 | + | ||
| 97 | + | // Hide will be called by Cancel | |
| 98 | + | accentPicker.EXPECT().Hide().Maybe() | |
| 99 | + | ||
| 100 | + | uc := NewInsertAccentUseCase(focusProvider, accentPicker, func(fn func()) { fn() }) | |
| 101 | + | ||
| 102 | + | // Start long-press detection | |
| 103 | + | uc.OnKeyPressed(ctx, 'e', false) | |
| 104 | + | ||
| 105 | + | // Wait for timer to fire | |
| 106 | + | wg.Wait() | |
| 107 | + | time.Sleep(50 * time.Millisecond) // Give time for state update | |
| 108 | + | ||
| 109 | + | assert.True(t, uc.IsPickerVisible(), "picker should be visible after long-press") | |
| 110 | + | ||
| 111 | + | // Clean up | |
| 112 | + | uc.Cancel(ctx) | |
| 113 | + | } | |
| 114 | + | ||
| 115 | + | func TestInsertAccentUseCase_Cancel(t *testing.T) { | |
| 116 | + | ctx := context.Background() | |
| 117 | + | focusProvider := mocks.NewMockFocusedInputProvider(t) | |
| 118 | + | accentPicker := mocks.NewMockAccentPickerUI(t) | |
| 119 | + | textInput := mocks.NewMockTextInputTarget(t) | |
| 120 | + | ||
| 121 | + | // GetFocusedInput is called when starting long-press to capture target | |
| 122 | + | focusProvider.EXPECT().GetFocusedInput().Return(textInput).Maybe() | |
| 123 | + | ||
| 124 | + | uc := NewInsertAccentUseCase(focusProvider, accentPicker, func(fn func()) { fn() }) | |
| 125 | + | ||
| 126 | + | // Start timer | |
| 127 | + | uc.OnKeyPressed(ctx, 'e', false) | |
| 128 | + | ||
| 129 | + | // Cancel should stop timer | |
| 130 | + | uc.Cancel(ctx) | |
| 131 | + | ||
| 132 | + | // Wait past the timer duration | |
| 133 | + | time.Sleep(LongPressDelay + 50*time.Millisecond) | |
| 134 | + | ||
| 135 | + | // Picker should never have been shown | |
| 136 | + | assert.False(t, uc.IsPickerVisible(), "picker should not be visible after cancel") | |
| 137 | + | } | |
| 138 | + | ||
| 139 | + | func TestInsertAccentUseCase_UppercaseAccents(t *testing.T) { | |
| 140 | + | ctx := context.Background() | |
| 141 | + | focusProvider := mocks.NewMockFocusedInputProvider(t) | |
| 142 | + | accentPicker := mocks.NewMockAccentPickerUI(t) | |
| 143 | + | textInput := mocks.NewMockTextInputTarget(t) | |
| 144 | + | ||
| 145 | + | var wg sync.WaitGroup | |
| 146 | + | wg.Add(1) | |
| 147 | + | ||
| 148 | + | // GetFocusedInput is called when showing picker to delete repeated characters | |
| 149 | + | focusProvider.EXPECT().GetFocusedInput().Return(textInput).Maybe() | |
| 150 | + | // DeleteBeforeCursor will be called to delete the initial character + any repeats | |
| 151 | + | textInput.EXPECT().DeleteBeforeCursor(mock.Anything, mock.AnythingOfType("int")).Return(nil).Maybe() | |
| 152 | + | ||
| 153 | + | // Expect Show to be called with uppercase accents when shift is held | |
| 154 | + | accentPicker.EXPECT(). | |
| 155 | + | Show(mock.MatchedBy(func(accents []rune) bool { | |
| 156 | + | return len(accents) > 0 && accents[0] == 'È' // Uppercase È | |
| 157 | + | }), mock.AnythingOfType("func(int32)"), mock.AnythingOfType("func()")). | |
| 158 | + | Run(func(_ []rune, _ func(rune), _ func()) { | |
| 159 | + | wg.Done() | |
| 160 | + | }). | |
| 161 | + | Once() | |
| 162 | + | ||
| 163 | + | // Hide will be called by Cancel | |
| 164 | + | accentPicker.EXPECT().Hide().Maybe() | |
| 165 | + | ||
| 166 | + | uc := NewInsertAccentUseCase(focusProvider, accentPicker, func(fn func()) { fn() }) | |
| 167 | + | ||
| 168 | + | // Start long-press with shift held | |
| 169 | + | uc.OnKeyPressed(ctx, 'e', true) | |
| 170 | + | ||
| 171 | + | // Wait for timer | |
| 172 | + | wg.Wait() | |
| 173 | + | ||
| 174 | + | // Clean up | |
| 175 | + | uc.Cancel(ctx) | |
| 176 | + | } | |
| 177 | + | ||
| 178 | + | func TestInsertAccentUseCase_PickerVisibleBlocksNewLongPress(t *testing.T) { | |
| 179 | + | ctx := context.Background() | |
| 180 | + | focusProvider := mocks.NewMockFocusedInputProvider(t) | |
| 181 | + | accentPicker := mocks.NewMockAccentPickerUI(t) | |
| 182 | + | textInput := mocks.NewMockTextInputTarget(t) | |
| 183 | + | ||
| 184 | + | var showCalled int | |
| 185 | + | var mu sync.Mutex | |
| 186 | + | ||
| 187 | + | // GetFocusedInput is called when showing picker to delete repeated characters | |
| 188 | + | focusProvider.EXPECT().GetFocusedInput().Return(textInput).Maybe() | |
| 189 | + | // DeleteBeforeCursor will be called to delete the initial character + any repeats | |
| 190 | + | textInput.EXPECT().DeleteBeforeCursor(mock.Anything, mock.AnythingOfType("int")).Return(nil).Maybe() | |
| 191 | + | ||
| 192 | + | accentPicker.EXPECT(). | |
| 193 | + | Show(mock.Anything, mock.Anything, mock.Anything). | |
| 194 | + | Run(func(_ []rune, _ func(rune), _ func()) { | |
| 195 | + | mu.Lock() | |
| 196 | + | showCalled++ | |
| 197 | + | mu.Unlock() | |
| 198 | + | }). | |
| 199 | + | Maybe() | |
| 200 | + | ||
| 201 | + | accentPicker.EXPECT().Hide().Maybe() | |
| 202 | + | ||
| 203 | + | uc := NewInsertAccentUseCase(focusProvider, accentPicker, func(fn func()) { fn() }) | |
| 204 | + | ||
| 205 | + | // Trigger first long-press | |
| 206 | + | uc.OnKeyPressed(ctx, 'e', false) | |
| 207 | + | time.Sleep(LongPressDelay + 50*time.Millisecond) | |
| 208 | + | ||
| 209 | + | // Try to start another long-press while picker is visible | |
| 210 | + | handled := uc.OnKeyPressed(ctx, 'a', false) | |
| 211 | + | assert.True(t, handled, "should return true when picker is visible") | |
| 212 | + | ||
| 213 | + | mu.Lock() | |
| 214 | + | count := showCalled | |
| 215 | + | mu.Unlock() | |
| 216 | + | assert.LessOrEqual(t, count, 1, "Show should only be called once") | |
| 217 | + | ||
| 218 | + | uc.Cancel(ctx) | |
| 219 | + | } | |
| @@ -0,0 +1,219 @@ | |||||
| 1 | + | package usecase | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | "sync" | |||
| 6 | + | "testing" | |||
| 7 | + | "time" | |||
| 8 | + | ||||
| 9 | + | "github.com/bnema/dumber/internal/application/port/mocks" | |||
| 10 | + | "github.com/stretchr/testify/assert" | |||
| 11 | + | "github.com/stretchr/testify/mock" | |||
| 12 | + | ) | |||
| 13 | + | ||||
| 14 | + | func TestInsertAccentUseCase_OnKeyPressed_NoAccents(t *testing.T) { | |||
| 15 | + | ctx := context.Background() | |||
| 16 | + | focusProvider := mocks.NewMockFocusedInputProvider(t) | |||
| 17 | + | accentPicker := mocks.NewMockAccentPickerUI(t) | |||
| 18 | + | ||||
| 19 | + | uc := NewInsertAccentUseCase(focusProvider, accentPicker, func(fn func()) { fn() }) | |||
| 20 | + | ||||
| 21 | + | // Key 'x' has no accents - should return false and not start timer | |||
| 22 | + | handled := uc.OnKeyPressed(ctx, 'x', false) | |||
| 23 | + | assert.False(t, handled, "OnKeyPressed should return false for key without accents") | |||
| 24 | + | assert.False(t, uc.IsPickerVisible(), "picker should not be visible") | |||
| 25 | + | } | |||
| 26 | + | ||||
| 27 | + | func TestInsertAccentUseCase_OnKeyPressed_HasAccents(t *testing.T) { | |||
| 28 | + | ctx := context.Background() | |||
| 29 | + | focusProvider := mocks.NewMockFocusedInputProvider(t) | |||
| 30 | + | accentPicker := mocks.NewMockAccentPickerUI(t) | |||
| 31 | + | textInput := mocks.NewMockTextInputTarget(t) | |||
| 32 | + | ||||
| 33 | + | // GetFocusedInput is called when starting long-press to capture target | |||
| 34 | + | focusProvider.EXPECT().GetFocusedInput().Return(textInput).Maybe() | |||
| 35 | + | ||||
| 36 | + | uc := NewInsertAccentUseCase(focusProvider, accentPicker, func(fn func()) { fn() }) | |||
| 37 | + | ||||
| 38 | + | // Key 'e' has accents - should return false initially (timer started) | |||
| 39 | + | handled := uc.OnKeyPressed(ctx, 'e', false) | |||
| 40 | + | assert.False(t, handled, "OnKeyPressed should return false while timer is pending") | |||
| 41 | + | ||||
| 42 | + | // Cancel to clean up timer | |||
| 43 | + | uc.Cancel(ctx) | |||
| 44 | + | } | |||
| 45 | + | ||||
| 46 | + | func TestInsertAccentUseCase_OnKeyReleased_CancelsTimer(t *testing.T) { | |||
| 47 | + | ctx := context.Background() | |||
| 48 | + | focusProvider := mocks.NewMockFocusedInputProvider(t) | |||
| 49 | + | accentPicker := mocks.NewMockAccentPickerUI(t) | |||
| 50 | + | textInput := mocks.NewMockTextInputTarget(t) | |||
| 51 | + | ||||
| 52 | + | // GetFocusedInput is called when starting long-press to capture target | |||
| 53 | + | focusProvider.EXPECT().GetFocusedInput().Return(textInput).Maybe() | |||
| 54 | + | ||||
| 55 | + | timerFired := false | |||
| 56 | + | uc := NewInsertAccentUseCase(focusProvider, accentPicker, func(fn func()) { | |||
| 57 | + | timerFired = true | |||
| 58 | + | fn() | |||
| 59 | + | }) | |||
| 60 | + | ||||
| 61 | + | // Start long-press detection | |||
| 62 | + | uc.OnKeyPressed(ctx, 'e', false) | |||
| 63 | + | ||||
| 64 | + | // Release before timer fires | |||
| 65 | + | uc.OnKeyReleased(ctx, 'e') | |||
| 66 | + | ||||
| 67 | + | // Wait a bit longer than LongPressDelay | |||
| 68 | + | time.Sleep(LongPressDelay + 50*time.Millisecond) | |||
| 69 | + | ||||
| 70 | + | assert.False(t, timerFired, "timer should have been canceled by key release") | |||
| 71 | + | } | |||
| 72 | + | ||||
| 73 | + | func TestInsertAccentUseCase_LongPress_ShowsPicker(t *testing.T) { | |||
| 74 | + | ctx := context.Background() | |||
| 75 | + | focusProvider := mocks.NewMockFocusedInputProvider(t) | |||
| 76 | + | accentPicker := mocks.NewMockAccentPickerUI(t) | |||
| 77 | + | textInput := mocks.NewMockTextInputTarget(t) | |||
| 78 | + | ||||
| 79 | + | var wg sync.WaitGroup | |||
| 80 | + | wg.Add(1) | |||
| 81 | + | ||||
| 82 | + | // GetFocusedInput is called when showing picker to delete repeated characters | |||
| 83 | + | focusProvider.EXPECT().GetFocusedInput().Return(textInput).Maybe() | |||
| 84 | + | // DeleteBeforeCursor will be called to delete the initial character + any repeats | |||
| 85 | + | textInput.EXPECT().DeleteBeforeCursor(mock.Anything, mock.AnythingOfType("int")).Return(nil).Maybe() | |||
| 86 | + | ||||
| 87 | + | // Expect Show to be called with accents for 'e' | |||
| 88 | + | accentPicker.EXPECT(). | |||
| 89 | + | Show(mock.MatchedBy(func(accents []rune) bool { | |||
| 90 | + | return len(accents) > 0 && accents[0] == 'è' | |||
| 91 | + | }), mock.AnythingOfType("func(int32)"), mock.AnythingOfType("func()")). | |||
| 92 | + | Run(func(_ []rune, _ func(rune), _ func()) { | |||
| 93 | + | wg.Done() | |||
| 94 | + | }). | |||
| 95 | + | Once() | |||
| 96 | + | ||||
| 97 | + | // Hide will be called by Cancel | |||
| 98 | + | accentPicker.EXPECT().Hide().Maybe() | |||
| 99 | + | ||||
| 100 | + | uc := NewInsertAccentUseCase(focusProvider, accentPicker, func(fn func()) { fn() }) | |||
| 101 | + | ||||
| 102 | + | // Start long-press detection | |||
| 103 | + | uc.OnKeyPressed(ctx, 'e', false) | |||
| 104 | + | ||||
| 105 | + | // Wait for timer to fire | |||
| 106 | + | wg.Wait() | |||
| 107 | + | time.Sleep(50 * time.Millisecond) // Give time for state update | |||
| 108 | + | ||||
| 109 | + | assert.True(t, uc.IsPickerVisible(), "picker should be visible after long-press") | |||
| 110 | + | ||||
| 111 | + | // Clean up | |||
| 112 | + | uc.Cancel(ctx) | |||
| 113 | + | } | |||
| 114 | + | ||||
| 115 | + | func TestInsertAccentUseCase_Cancel(t *testing.T) { | |||
| 116 | + | ctx := context.Background() | |||
| 117 | + | focusProvider := mocks.NewMockFocusedInputProvider(t) | |||
| 118 | + | accentPicker := mocks.NewMockAccentPickerUI(t) | |||
| 119 | + | textInput := mocks.NewMockTextInputTarget(t) | |||
| 120 | + | ||||
| 121 | + | // GetFocusedInput is called when starting long-press to capture target | |||
| 122 | + | focusProvider.EXPECT().GetFocusedInput().Return(textInput).Maybe() | |||
| 123 | + | ||||
| 124 | + | uc := NewInsertAccentUseCase(focusProvider, accentPicker, func(fn func()) { fn() }) | |||
| 125 | + | ||||
| 126 | + | // Start timer | |||
| 127 | + | uc.OnKeyPressed(ctx, 'e', false) | |||
| 128 | + | ||||
| 129 | + | // Cancel should stop timer | |||
| 130 | + | uc.Cancel(ctx) | |||
| 131 | + | ||||
| 132 | + | // Wait past the timer duration | |||
| 133 | + | time.Sleep(LongPressDelay + 50*time.Millisecond) | |||
| 134 | + | ||||
| 135 | + | // Picker should never have been shown | |||
| 136 | + | assert.False(t, uc.IsPickerVisible(), "picker should not be visible after cancel") | |||
| 137 | + | } | |||
| 138 | + | ||||
| 139 | + | func TestInsertAccentUseCase_UppercaseAccents(t *testing.T) { | |||
| 140 | + | ctx := context.Background() | |||
| 141 | + | focusProvider := mocks.NewMockFocusedInputProvider(t) | |||
| 142 | + | accentPicker := mocks.NewMockAccentPickerUI(t) | |||
| 143 | + | textInput := mocks.NewMockTextInputTarget(t) | |||
| 144 | + | ||||
| 145 | + | var wg sync.WaitGroup | |||
| 146 | + | wg.Add(1) | |||
| 147 | + | ||||
| 148 | + | // GetFocusedInput is called when showing picker to delete repeated characters | |||
| 149 | + | focusProvider.EXPECT().GetFocusedInput().Return(textInput).Maybe() | |||
| 150 | + | // DeleteBeforeCursor will be called to delete the initial character + any repeats | |||
| 151 | + | textInput.EXPECT().DeleteBeforeCursor(mock.Anything, mock.AnythingOfType("int")).Return(nil).Maybe() | |||
| 152 | + | ||||
| 153 | + | // Expect Show to be called with uppercase accents when shift is held | |||
| 154 | + | accentPicker.EXPECT(). | |||
| 155 | + | Show(mock.MatchedBy(func(accents []rune) bool { | |||
| 156 | + | return len(accents) > 0 && accents[0] == 'È' // Uppercase È | |||
| 157 | + | }), mock.AnythingOfType("func(int32)"), mock.AnythingOfType("func()")). | |||
| 158 | + | Run(func(_ []rune, _ func(rune), _ func()) { | |||
| 159 | + | wg.Done() | |||
| 160 | + | }). | |||
| 161 | + | Once() | |||
| 162 | + | ||||
| 163 | + | // Hide will be called by Cancel | |||
| 164 | + | accentPicker.EXPECT().Hide().Maybe() | |||
| 165 | + | ||||
| 166 | + | uc := NewInsertAccentUseCase(focusProvider, accentPicker, func(fn func()) { fn() }) | |||
| 167 | + | ||||
| 168 | + | // Start long-press with shift held | |||
| 169 | + | uc.OnKeyPressed(ctx, 'e', true) | |||
| 170 | + | ||||
| 171 | + | // Wait for timer | |||
| 172 | + | wg.Wait() | |||
| 173 | + | ||||
| 174 | + | // Clean up | |||
| 175 | + | uc.Cancel(ctx) | |||
| 176 | + | } | |||
| 177 | + | ||||
| 178 | + | func TestInsertAccentUseCase_PickerVisibleBlocksNewLongPress(t *testing.T) { | |||
| 179 | + | ctx := context.Background() | |||
| 180 | + | focusProvider := mocks.NewMockFocusedInputProvider(t) | |||
| 181 | + | accentPicker := mocks.NewMockAccentPickerUI(t) | |||
| 182 | + | textInput := mocks.NewMockTextInputTarget(t) | |||
| 183 | + | ||||
| 184 | + | var showCalled int | |||
| 185 | + | var mu sync.Mutex | |||
| 186 | + | ||||
| 187 | + | // GetFocusedInput is called when showing picker to delete repeated characters | |||
| 188 | + | focusProvider.EXPECT().GetFocusedInput().Return(textInput).Maybe() | |||
| 189 | + | // DeleteBeforeCursor will be called to delete the initial character + any repeats | |||
| 190 | + | textInput.EXPECT().DeleteBeforeCursor(mock.Anything, mock.AnythingOfType("int")).Return(nil).Maybe() | |||
| 191 | + | ||||
| 192 | + | accentPicker.EXPECT(). | |||
| 193 | + | Show(mock.Anything, mock.Anything, mock.Anything). | |||
| 194 | + | Run(func(_ []rune, _ func(rune), _ func()) { | |||
| 195 | + | mu.Lock() | |||
| 196 | + | showCalled++ | |||
| 197 | + | mu.Unlock() | |||
| 198 | + | }). | |||
| 199 | + | Maybe() | |||
| 200 | + | ||||
| 201 | + | accentPicker.EXPECT().Hide().Maybe() | |||
| 202 | + | ||||
| 203 | + | uc := NewInsertAccentUseCase(focusProvider, accentPicker, func(fn func()) { fn() }) | |||
| 204 | + | ||||
| 205 | + | // Trigger first long-press | |||
| 206 | + | uc.OnKeyPressed(ctx, 'e', false) | |||
| 207 | + | time.Sleep(LongPressDelay + 50*time.Millisecond) | |||
| 208 | + | ||||
| 209 | + | // Try to start another long-press while picker is visible | |||
| 210 | + | handled := uc.OnKeyPressed(ctx, 'a', false) | |||
| 211 | + | assert.True(t, handled, "should return true when picker is visible") | |||
| 212 | + | ||||
| 213 | + | mu.Lock() | |||
| 214 | + | count := showCalled | |||
| 215 | + | mu.Unlock() | |||
| 216 | + | assert.LessOrEqual(t, count, 1, "Show should only be called once") | |||
| 217 | + | ||||
| 218 | + | uc.Cancel(ctx) | |||
| 219 | + | } | |||
A internal/domain/entity/accent.go +46 -0
| @@ -0,0 +1,46 @@ | |||
| 1 | + | package entity | |
| 2 | + | ||
| 3 | + | import "unicode" | |
| 4 | + | ||
| 5 | + | // AccentMap maps base characters to their accented variants. | |
| 6 | + | // The order determines display order in the accent picker. | |
| 7 | + | var AccentMap = map[rune][]rune{ | |
| 8 | + | 'a': {'à', 'á', 'â', 'ä', 'ã', 'å', 'æ'}, | |
| 9 | + | 'c': {'ç', 'ć', 'č'}, | |
| 10 | + | 'e': {'è', 'é', 'ê', 'ë', 'ę', 'ė'}, | |
| 11 | + | 'i': {'ì', 'í', 'î', 'ï', 'į'}, | |
| 12 | + | 'n': {'ñ', 'ń'}, | |
| 13 | + | 'o': {'ò', 'ó', 'ô', 'ö', 'õ', 'ø', 'œ'}, | |
| 14 | + | 's': {'ß', 'ś', 'š'}, | |
| 15 | + | 'u': {'ù', 'ú', 'û', 'ü', 'ū'}, | |
| 16 | + | 'y': {'ÿ', 'ý'}, | |
| 17 | + | } | |
| 18 | + | ||
| 19 | + | // HasAccents returns true if the given character has accent variants. | |
| 20 | + | func HasAccents(char rune) bool { | |
| 21 | + | lower := unicode.ToLower(char) | |
| 22 | + | _, ok := AccentMap[lower] | |
| 23 | + | return ok | |
| 24 | + | } | |
| 25 | + | ||
| 26 | + | // GetAccents returns the accent variants for a character. | |
| 27 | + | // If uppercase is true, returns uppercase variants. | |
| 28 | + | // Returns nil if the character has no accent variants. | |
| 29 | + | func GetAccents(char rune, uppercase bool) []rune { | |
| 30 | + | lower := unicode.ToLower(char) | |
| 31 | + | accents, ok := AccentMap[lower] | |
| 32 | + | if !ok { | |
| 33 | + | return nil | |
| 34 | + | } | |
| 35 | + | ||
| 36 | + | if !uppercase { | |
| 37 | + | return accents | |
| 38 | + | } | |
| 39 | + | ||
| 40 | + | // Return uppercase versions | |
| 41 | + | result := make([]rune, len(accents)) | |
| 42 | + | for i, a := range accents { | |
| 43 | + | result[i] = unicode.ToUpper(a) | |
| 44 | + | } | |
| 45 | + | return result | |
| 46 | + | } | |
| @@ -0,0 +1,46 @@ | |||||
| 1 | + | package entity | |||
| 2 | + | ||||
| 3 | + | import "unicode" | |||
| 4 | + | ||||
| 5 | + | // AccentMap maps base characters to their accented variants. | |||
| 6 | + | // The order determines display order in the accent picker. | |||
| 7 | + | var AccentMap = map[rune][]rune{ | |||
| 8 | + | 'a': {'à', 'á', 'â', 'ä', 'ã', 'å', 'æ'}, | |||
| 9 | + | 'c': {'ç', 'ć', 'č'}, | |||
| 10 | + | 'e': {'è', 'é', 'ê', 'ë', 'ę', 'ė'}, | |||
| 11 | + | 'i': {'ì', 'í', 'î', 'ï', 'į'}, | |||
| 12 | + | 'n': {'ñ', 'ń'}, | |||
| 13 | + | 'o': {'ò', 'ó', 'ô', 'ö', 'õ', 'ø', 'œ'}, | |||
| 14 | + | 's': {'ß', 'ś', 'š'}, | |||
| 15 | + | 'u': {'ù', 'ú', 'û', 'ü', 'ū'}, | |||
| 16 | + | 'y': {'ÿ', 'ý'}, | |||
| 17 | + | } | |||
| 18 | + | ||||
| 19 | + | // HasAccents returns true if the given character has accent variants. | |||
| 20 | + | func HasAccents(char rune) bool { | |||
| 21 | + | lower := unicode.ToLower(char) | |||
| 22 | + | _, ok := AccentMap[lower] | |||
| 23 | + | return ok | |||
| 24 | + | } | |||
| 25 | + | ||||
| 26 | + | // GetAccents returns the accent variants for a character. | |||
| 27 | + | // If uppercase is true, returns uppercase variants. | |||
| 28 | + | // Returns nil if the character has no accent variants. | |||
| 29 | + | func GetAccents(char rune, uppercase bool) []rune { | |||
| 30 | + | lower := unicode.ToLower(char) | |||
| 31 | + | accents, ok := AccentMap[lower] | |||
| 32 | + | if !ok { | |||
| 33 | + | return nil | |||
| 34 | + | } | |||
| 35 | + | ||||
| 36 | + | if !uppercase { | |||
| 37 | + | return accents | |||
| 38 | + | } | |||
| 39 | + | ||||
| 40 | + | // Return uppercase versions | |||
| 41 | + | result := make([]rune, len(accents)) | |||
| 42 | + | for i, a := range accents { | |||
| 43 | + | result[i] = unicode.ToUpper(a) | |||
| 44 | + | } | |||
| 45 | + | return result | |||
| 46 | + | } | |||
A internal/domain/entity/accent_test.go +108 -0
| @@ -0,0 +1,108 @@ | |||
| 1 | + | package entity | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "testing" | |
| 5 | + | ) | |
| 6 | + | ||
| 7 | + | func TestHasAccents(t *testing.T) { | |
| 8 | + | tests := []struct { | |
| 9 | + | name string | |
| 10 | + | char rune | |
| 11 | + | want bool | |
| 12 | + | }{ | |
| 13 | + | {"lowercase e", 'e', true}, | |
| 14 | + | {"uppercase E", 'E', true}, | |
| 15 | + | {"lowercase a", 'a', true}, | |
| 16 | + | {"uppercase A", 'A', true}, | |
| 17 | + | {"lowercase c", 'c', true}, | |
| 18 | + | {"lowercase n", 'n', true}, | |
| 19 | + | {"lowercase x (no accents)", 'x', false}, | |
| 20 | + | {"uppercase X (no accents)", 'X', false}, | |
| 21 | + | {"digit 1 (no accents)", '1', false}, | |
| 22 | + | {"space (no accents)", ' ', false}, | |
| 23 | + | } | |
| 24 | + | ||
| 25 | + | for _, tt := range tests { | |
| 26 | + | t.Run(tt.name, func(t *testing.T) { | |
| 27 | + | got := HasAccents(tt.char) | |
| 28 | + | if got != tt.want { | |
| 29 | + | t.Errorf("HasAccents(%q) = %v, want %v", tt.char, got, tt.want) | |
| 30 | + | } | |
| 31 | + | }) | |
| 32 | + | } | |
| 33 | + | } | |
| 34 | + | ||
| 35 | + | func TestGetAccents(t *testing.T) { | |
| 36 | + | tests := []struct { | |
| 37 | + | name string | |
| 38 | + | char rune | |
| 39 | + | uppercase bool | |
| 40 | + | wantLen int | |
| 41 | + | wantFirst rune | |
| 42 | + | }{ | |
| 43 | + | {"lowercase e", 'e', false, 6, 'è'}, | |
| 44 | + | {"uppercase E", 'E', true, 6, 'È'}, | |
| 45 | + | {"lowercase a", 'a', false, 7, 'à'}, | |
| 46 | + | {"uppercase A", 'A', true, 7, 'À'}, | |
| 47 | + | {"lowercase c", 'c', false, 3, 'ç'}, | |
| 48 | + | {"uppercase C with shift", 'c', true, 3, 'Ç'}, | |
| 49 | + | {"no accents x", 'x', false, 0, 0}, | |
| 50 | + | } | |
| 51 | + | ||
| 52 | + | for _, tt := range tests { | |
| 53 | + | t.Run(tt.name, func(t *testing.T) { | |
| 54 | + | got := GetAccents(tt.char, tt.uppercase) | |
| 55 | + | ||
| 56 | + | if len(got) != tt.wantLen { | |
| 57 | + | t.Errorf("GetAccents(%q, %v) length = %d, want %d", tt.char, tt.uppercase, len(got), tt.wantLen) | |
| 58 | + | } | |
| 59 | + | ||
| 60 | + | if tt.wantLen > 0 && got[0] != tt.wantFirst { | |
| 61 | + | t.Errorf("GetAccents(%q, %v)[0] = %q, want %q", tt.char, tt.uppercase, got[0], tt.wantFirst) | |
| 62 | + | } | |
| 63 | + | }) | |
| 64 | + | } | |
| 65 | + | } | |
| 66 | + | ||
| 67 | + | func TestGetAccents_UppercaseFromLowercase(t *testing.T) { | |
| 68 | + | // When passing lowercase char with uppercase=true, should return uppercase accents | |
| 69 | + | uppercase := GetAccents('e', true) | |
| 70 | + | lowercase := GetAccents('e', false) | |
| 71 | + | ||
| 72 | + | if len(uppercase) == 0 { | |
| 73 | + | t.Fatal("expected accents for 'e' with uppercase=true") | |
| 74 | + | } | |
| 75 | + | ||
| 76 | + | if len(uppercase) != len(lowercase) { | |
| 77 | + | t.Fatalf("uppercase and lowercase accent counts differ: %d vs %d", len(uppercase), len(lowercase)) | |
| 78 | + | } | |
| 79 | + | ||
| 80 | + | // Verify each uppercase accent is different from the corresponding lowercase accent | |
| 81 | + | for i, upper := range uppercase { | |
| 82 | + | lower := lowercase[i] | |
| 83 | + | if upper == lower { | |
| 84 | + | t.Errorf("accent at index %d is same for uppercase and lowercase: %q", i, upper) | |
| 85 | + | } | |
| 86 | + | } | |
| 87 | + | ||
| 88 | + | // Verify first accent is È (uppercase of è) | |
| 89 | + | if uppercase[0] != 'È' { | |
| 90 | + | t.Errorf("expected first uppercase accent to be È, got %q", uppercase[0]) | |
| 91 | + | } | |
| 92 | + | } | |
| 93 | + | ||
| 94 | + | func TestAccentMapCoverage(t *testing.T) { | |
| 95 | + | // Verify all expected characters have accents | |
| 96 | + | expectedChars := []rune{'a', 'c', 'e', 'i', 'n', 'o', 's', 'u', 'y'} | |
| 97 | + | ||
| 98 | + | for _, char := range expectedChars { | |
| 99 | + | if !HasAccents(char) { | |
| 100 | + | t.Errorf("expected %q to have accents", char) | |
| 101 | + | } | |
| 102 | + | ||
| 103 | + | accents := GetAccents(char, false) | |
| 104 | + | if len(accents) == 0 { | |
| 105 | + | t.Errorf("expected %q to have at least one accent variant", char) | |
| 106 | + | } | |
| 107 | + | } | |
| 108 | + | } | |
| @@ -0,0 +1,108 @@ | |||||
| 1 | + | package entity | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "testing" | |||
| 5 | + | ) | |||
| 6 | + | ||||
| 7 | + | func TestHasAccents(t *testing.T) { | |||
| 8 | + | tests := []struct { | |||
| 9 | + | name string | |||
| 10 | + | char rune | |||
| 11 | + | want bool | |||
| 12 | + | }{ | |||
| 13 | + | {"lowercase e", 'e', true}, | |||
| 14 | + | {"uppercase E", 'E', true}, | |||
| 15 | + | {"lowercase a", 'a', true}, | |||
| 16 | + | {"uppercase A", 'A', true}, | |||
| 17 | + | {"lowercase c", 'c', true}, | |||
| 18 | + | {"lowercase n", 'n', true}, | |||
| 19 | + | {"lowercase x (no accents)", 'x', false}, | |||
| 20 | + | {"uppercase X (no accents)", 'X', false}, | |||
| 21 | + | {"digit 1 (no accents)", '1', false}, | |||
| 22 | + | {"space (no accents)", ' ', false}, | |||
| 23 | + | } | |||
| 24 | + | ||||
| 25 | + | for _, tt := range tests { | |||
| 26 | + | t.Run(tt.name, func(t *testing.T) { | |||
| 27 | + | got := HasAccents(tt.char) | |||
| 28 | + | if got != tt.want { | |||
| 29 | + | t.Errorf("HasAccents(%q) = %v, want %v", tt.char, got, tt.want) | |||
| 30 | + | } | |||
| 31 | + | }) | |||
| 32 | + | } | |||
| 33 | + | } | |||
| 34 | + | ||||
| 35 | + | func TestGetAccents(t *testing.T) { | |||
| 36 | + | tests := []struct { | |||
| 37 | + | name string | |||
| 38 | + | char rune | |||
| 39 | + | uppercase bool | |||
| 40 | + | wantLen int | |||
| 41 | + | wantFirst rune | |||
| 42 | + | }{ | |||
| 43 | + | {"lowercase e", 'e', false, 6, 'è'}, | |||
| 44 | + | {"uppercase E", 'E', true, 6, 'È'}, | |||
| 45 | + | {"lowercase a", 'a', false, 7, 'à'}, | |||
| 46 | + | {"uppercase A", 'A', true, 7, 'À'}, | |||
| 47 | + | {"lowercase c", 'c', false, 3, 'ç'}, | |||
| 48 | + | {"uppercase C with shift", 'c', true, 3, 'Ç'}, | |||
| 49 | + | {"no accents x", 'x', false, 0, 0}, | |||
| 50 | + | } | |||
| 51 | + | ||||
| 52 | + | for _, tt := range tests { | |||
| 53 | + | t.Run(tt.name, func(t *testing.T) { | |||
| 54 | + | got := GetAccents(tt.char, tt.uppercase) | |||
| 55 | + | ||||
| 56 | + | if len(got) != tt.wantLen { | |||
| 57 | + | t.Errorf("GetAccents(%q, %v) length = %d, want %d", tt.char, tt.uppercase, len(got), tt.wantLen) | |||
| 58 | + | } | |||
| 59 | + | ||||
| 60 | + | if tt.wantLen > 0 && got[0] != tt.wantFirst { | |||
| 61 | + | t.Errorf("GetAccents(%q, %v)[0] = %q, want %q", tt.char, tt.uppercase, got[0], tt.wantFirst) | |||
| 62 | + | } | |||
| 63 | + | }) | |||
| 64 | + | } | |||
| 65 | + | } | |||
| 66 | + | ||||
| 67 | + | func TestGetAccents_UppercaseFromLowercase(t *testing.T) { | |||
| 68 | + | // When passing lowercase char with uppercase=true, should return uppercase accents | |||
| 69 | + | uppercase := GetAccents('e', true) | |||
| 70 | + | lowercase := GetAccents('e', false) | |||
| 71 | + | ||||
| 72 | + | if len(uppercase) == 0 { | |||
| 73 | + | t.Fatal("expected accents for 'e' with uppercase=true") | |||
| 74 | + | } | |||
| 75 | + | ||||
| 76 | + | if len(uppercase) != len(lowercase) { | |||
| 77 | + | t.Fatalf("uppercase and lowercase accent counts differ: %d vs %d", len(uppercase), len(lowercase)) | |||
| 78 | + | } | |||
| 79 | + | ||||
| 80 | + | // Verify each uppercase accent is different from the corresponding lowercase accent | |||
| 81 | + | for i, upper := range uppercase { | |||
| 82 | + | lower := lowercase[i] | |||
| 83 | + | if upper == lower { | |||
| 84 | + | t.Errorf("accent at index %d is same for uppercase and lowercase: %q", i, upper) | |||
| 85 | + | } | |||
| 86 | + | } | |||
| 87 | + | ||||
| 88 | + | // Verify first accent is È (uppercase of è) | |||
| 89 | + | if uppercase[0] != 'È' { | |||
| 90 | + | t.Errorf("expected first uppercase accent to be È, got %q", uppercase[0]) | |||
| 91 | + | } | |||
| 92 | + | } | |||
| 93 | + | ||||
| 94 | + | func TestAccentMapCoverage(t *testing.T) { | |||
| 95 | + | // Verify all expected characters have accents | |||
| 96 | + | expectedChars := []rune{'a', 'c', 'e', 'i', 'n', 'o', 's', 'u', 'y'} | |||
| 97 | + | ||||
| 98 | + | for _, char := range expectedChars { | |||
| 99 | + | if !HasAccents(char) { | |||
| 100 | + | t.Errorf("expected %q to have accents", char) | |||
| 101 | + | } | |||
| 102 | + | ||||
| 103 | + | accents := GetAccents(char, false) | |||
| 104 | + | if len(accents) == 0 { | |||
| 105 | + | t.Errorf("expected %q to have at least one accent variant", char) | |||
| 106 | + | } | |||
| 107 | + | } | |||
| 108 | + | } | |||
A internal/infrastructure/textinput/focus_provider.go +39 -0
| @@ -0,0 +1,39 @@ | |||
| 1 | + | // Package textinput provides implementations for text input targets and focus tracking. | |
| 2 | + | package textinput | |
| 3 | + | ||
| 4 | + | import ( | |
| 5 | + | "sync" | |
| 6 | + | ||
| 7 | + | "github.com/bnema/dumber/internal/application/port" | |
| 8 | + | ) | |
| 9 | + | ||
| 10 | + | // FocusProvider tracks which text input target currently has focus. | |
| 11 | + | // It implements port.FocusedInputProvider. | |
| 12 | + | type FocusProvider struct { | |
| 13 | + | target port.TextInputTarget | |
| 14 | + | mu sync.RWMutex | |
| 15 | + | } | |
| 16 | + | ||
| 17 | + | // Compile-time interface check. | |
| 18 | + | var _ port.FocusedInputProvider = (*FocusProvider)(nil) | |
| 19 | + | ||
| 20 | + | // NewFocusProvider creates a new focus provider. | |
| 21 | + | func NewFocusProvider() *FocusProvider { | |
| 22 | + | return &FocusProvider{} | |
| 23 | + | } | |
| 24 | + | ||
| 25 | + | // GetFocusedInput returns the currently focused text input target. | |
| 26 | + | // Returns nil if no text input has focus. | |
| 27 | + | func (fp *FocusProvider) GetFocusedInput() port.TextInputTarget { | |
| 28 | + | fp.mu.RLock() | |
| 29 | + | defer fp.mu.RUnlock() | |
| 30 | + | return fp.target | |
| 31 | + | } | |
| 32 | + | ||
| 33 | + | // SetFocusedInput sets the currently focused text input target. | |
| 34 | + | // Pass nil to clear focus. | |
| 35 | + | func (fp *FocusProvider) SetFocusedInput(target port.TextInputTarget) { | |
| 36 | + | fp.mu.Lock() | |
| 37 | + | defer fp.mu.Unlock() | |
| 38 | + | fp.target = target | |
| 39 | + | } | |
| @@ -0,0 +1,39 @@ | |||||
| 1 | + | // Package textinput provides implementations for text input targets and focus tracking. | |||
| 2 | + | package textinput | |||
| 3 | + | ||||
| 4 | + | import ( | |||
| 5 | + | "sync" | |||
| 6 | + | ||||
| 7 | + | "github.com/bnema/dumber/internal/application/port" | |||
| 8 | + | ) | |||
| 9 | + | ||||
| 10 | + | // FocusProvider tracks which text input target currently has focus. | |||
| 11 | + | // It implements port.FocusedInputProvider. | |||
| 12 | + | type FocusProvider struct { | |||
| 13 | + | target port.TextInputTarget | |||
| 14 | + | mu sync.RWMutex | |||
| 15 | + | } | |||
| 16 | + | ||||
| 17 | + | // Compile-time interface check. | |||
| 18 | + | var _ port.FocusedInputProvider = (*FocusProvider)(nil) | |||
| 19 | + | ||||
| 20 | + | // NewFocusProvider creates a new focus provider. | |||
| 21 | + | func NewFocusProvider() *FocusProvider { | |||
| 22 | + | return &FocusProvider{} | |||
| 23 | + | } | |||
| 24 | + | ||||
| 25 | + | // GetFocusedInput returns the currently focused text input target. | |||
| 26 | + | // Returns nil if no text input has focus. | |||
| 27 | + | func (fp *FocusProvider) GetFocusedInput() port.TextInputTarget { | |||
| 28 | + | fp.mu.RLock() | |||
| 29 | + | defer fp.mu.RUnlock() | |||
| 30 | + | return fp.target | |||
| 31 | + | } | |||
| 32 | + | ||||
| 33 | + | // SetFocusedInput sets the currently focused text input target. | |||
| 34 | + | // Pass nil to clear focus. | |||
| 35 | + | func (fp *FocusProvider) SetFocusedInput(target port.TextInputTarget) { | |||
| 36 | + | fp.mu.Lock() | |||
| 37 | + | defer fp.mu.Unlock() | |||
| 38 | + | fp.target = target | |||
| 39 | + | } | |||
A internal/infrastructure/textinput/gtk_entry_target.go +96 -0
| @@ -0,0 +1,96 @@ | |||
| 1 | + | package textinput | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "context" | |
| 5 | + | ||
| 6 | + | "github.com/bnema/dumber/internal/application/port" | |
| 7 | + | "github.com/bnema/dumber/internal/logging" | |
| 8 | + | "github.com/jwijenbergh/puregotk/v4/gtk" | |
| 9 | + | ) | |
| 10 | + | ||
| 11 | + | // GTKEntryTarget implements TextInputTarget for GTK SearchEntry widgets. | |
| 12 | + | // It uses the Editable interface methods to insert and delete text. | |
| 13 | + | type GTKEntryTarget struct { | |
| 14 | + | entry *gtk.SearchEntry | |
| 15 | + | } | |
| 16 | + | ||
| 17 | + | // Compile-time interface check. | |
| 18 | + | var _ port.TextInputTarget = (*GTKEntryTarget)(nil) | |
| 19 | + | ||
| 20 | + | // NewGTKEntryTarget creates a new GTK entry target for a SearchEntry. | |
| 21 | + | func NewGTKEntryTarget(entry *gtk.SearchEntry) *GTKEntryTarget { | |
| 22 | + | return >KEntryTarget{entry: entry} | |
| 23 | + | } | |
| 24 | + | ||
| 25 | + | // InsertText inserts text at the current cursor position. | |
| 26 | + | func (t *GTKEntryTarget) InsertText(ctx context.Context, text string) error { | |
| 27 | + | log := logging.FromContext(ctx) | |
| 28 | + | ||
| 29 | + | if t.entry == nil { | |
| 30 | + | log.Warn().Msg("GTKEntryTarget: entry is nil") | |
| 31 | + | return nil | |
| 32 | + | } | |
| 33 | + | ||
| 34 | + | // Get current text and cursor position | |
| 35 | + | currentText := t.entry.GetText() | |
| 36 | + | pos := t.entry.GetPosition() | |
| 37 | + | ||
| 38 | + | // Convert to runes for proper Unicode handling | |
| 39 | + | runes := []rune(currentText) | |
| 40 | + | textRunes := []rune(text) | |
| 41 | + | ||
| 42 | + | // Clamp position to valid range | |
| 43 | + | if pos < 0 { | |
| 44 | + | pos = 0 | |
| 45 | + | } | |
| 46 | + | if pos > len(runes) { | |
| 47 | + | pos = len(runes) | |
| 48 | + | } | |
| 49 | + | ||
| 50 | + | // Build new text with insertion | |
| 51 | + | newRunes := make([]rune, 0, len(runes)+len(textRunes)) | |
| 52 | + | newRunes = append(newRunes, runes[:pos]...) | |
| 53 | + | newRunes = append(newRunes, textRunes...) | |
| 54 | + | newRunes = append(newRunes, runes[pos:]...) | |
| 55 | + | ||
| 56 | + | // Set new text and cursor position | |
| 57 | + | t.entry.SetText(string(newRunes)) | |
| 58 | + | newPos := pos + len(textRunes) | |
| 59 | + | t.entry.SetPosition(newPos) | |
| 60 | + | ||
| 61 | + | log.Debug(). | |
| 62 | + | Str("text", text). | |
| 63 | + | Int("pos", pos). | |
| 64 | + | Msg("inserted text into GTK entry") | |
| 65 | + | ||
| 66 | + | return nil | |
| 67 | + | } | |
| 68 | + | ||
| 69 | + | // DeleteBeforeCursor deletes n characters before the cursor position. | |
| 70 | + | func (t *GTKEntryTarget) DeleteBeforeCursor(ctx context.Context, n int) error { | |
| 71 | + | log := logging.FromContext(ctx) | |
| 72 | + | ||
| 73 | + | if t.entry == nil { | |
| 74 | + | log.Warn().Msg("GTKEntryTarget: entry is nil") | |
| 75 | + | return nil | |
| 76 | + | } | |
| 77 | + | ||
| 78 | + | pos := t.entry.GetPosition() | |
| 79 | + | if pos < n { | |
| 80 | + | n = pos // Don't delete more than available | |
| 81 | + | } | |
| 82 | + | ||
| 83 | + | if n <= 0 { | |
| 84 | + | return nil | |
| 85 | + | } | |
| 86 | + | ||
| 87 | + | // DeleteText takes start and end positions | |
| 88 | + | t.entry.DeleteText(pos-n, pos) | |
| 89 | + | ||
| 90 | + | log.Debug(). | |
| 91 | + | Int("n", n). | |
| 92 | + | Int("pos", pos). | |
| 93 | + | Msg("deleted text before cursor in GTK entry") | |
| 94 | + | ||
| 95 | + | return nil | |
| 96 | + | } | |
| @@ -0,0 +1,96 @@ | |||||
| 1 | + | package textinput | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | ||||
| 6 | + | "github.com/bnema/dumber/internal/application/port" | |||
| 7 | + | "github.com/bnema/dumber/internal/logging" | |||
| 8 | + | "github.com/jwijenbergh/puregotk/v4/gtk" | |||
| 9 | + | ) | |||
| 10 | + | ||||
| 11 | + | // GTKEntryTarget implements TextInputTarget for GTK SearchEntry widgets. | |||
| 12 | + | // It uses the Editable interface methods to insert and delete text. | |||
| 13 | + | type GTKEntryTarget struct { | |||
| 14 | + | entry *gtk.SearchEntry | |||
| 15 | + | } | |||
| 16 | + | ||||
| 17 | + | // Compile-time interface check. | |||
| 18 | + | var _ port.TextInputTarget = (*GTKEntryTarget)(nil) | |||
| 19 | + | ||||
| 20 | + | // NewGTKEntryTarget creates a new GTK entry target for a SearchEntry. | |||
| 21 | + | func NewGTKEntryTarget(entry *gtk.SearchEntry) *GTKEntryTarget { | |||
| 22 | + | return >KEntryTarget{entry: entry} | |||
| 23 | + | } | |||
| 24 | + | ||||
| 25 | + | // InsertText inserts text at the current cursor position. | |||
| 26 | + | func (t *GTKEntryTarget) InsertText(ctx context.Context, text string) error { | |||
| 27 | + | log := logging.FromContext(ctx) | |||
| 28 | + | ||||
| 29 | + | if t.entry == nil { | |||
| 30 | + | log.Warn().Msg("GTKEntryTarget: entry is nil") | |||
| 31 | + | return nil | |||
| 32 | + | } | |||
| 33 | + | ||||
| 34 | + | // Get current text and cursor position | |||
| 35 | + | currentText := t.entry.GetText() | |||
| 36 | + | pos := t.entry.GetPosition() | |||
| 37 | + | ||||
| 38 | + | // Convert to runes for proper Unicode handling | |||
| 39 | + | runes := []rune(currentText) | |||
| 40 | + | textRunes := []rune(text) | |||
| 41 | + | ||||
| 42 | + | // Clamp position to valid range | |||
| 43 | + | if pos < 0 { | |||
| 44 | + | pos = 0 | |||
| 45 | + | } | |||
| 46 | + | if pos > len(runes) { | |||
| 47 | + | pos = len(runes) | |||
| 48 | + | } | |||
| 49 | + | ||||
| 50 | + | // Build new text with insertion | |||
| 51 | + | newRunes := make([]rune, 0, len(runes)+len(textRunes)) | |||
| 52 | + | newRunes = append(newRunes, runes[:pos]...) | |||
| 53 | + | newRunes = append(newRunes, textRunes...) | |||
| 54 | + | newRunes = append(newRunes, runes[pos:]...) | |||
| 55 | + | ||||
| 56 | + | // Set new text and cursor position | |||
| 57 | + | t.entry.SetText(string(newRunes)) | |||
| 58 | + | newPos := pos + len(textRunes) | |||
| 59 | + | t.entry.SetPosition(newPos) | |||
| 60 | + | ||||
| 61 | + | log.Debug(). | |||
| 62 | + | Str("text", text). | |||
| 63 | + | Int("pos", pos). | |||
| 64 | + | Msg("inserted text into GTK entry") | |||
| 65 | + | ||||
| 66 | + | return nil | |||
| 67 | + | } | |||
| 68 | + | ||||
| 69 | + | // DeleteBeforeCursor deletes n characters before the cursor position. | |||
| 70 | + | func (t *GTKEntryTarget) DeleteBeforeCursor(ctx context.Context, n int) error { | |||
| 71 | + | log := logging.FromContext(ctx) | |||
| 72 | + | ||||
| 73 | + | if t.entry == nil { | |||
| 74 | + | log.Warn().Msg("GTKEntryTarget: entry is nil") | |||
| 75 | + | return nil | |||
| 76 | + | } | |||
| 77 | + | ||||
| 78 | + | pos := t.entry.GetPosition() | |||
| 79 | + | if pos < n { | |||
| 80 | + | n = pos // Don't delete more than available | |||
| 81 | + | } | |||
| 82 | + | ||||
| 83 | + | if n <= 0 { | |||
| 84 | + | return nil | |||
| 85 | + | } | |||
| 86 | + | ||||
| 87 | + | // DeleteText takes start and end positions | |||
| 88 | + | t.entry.DeleteText(pos-n, pos) | |||
| 89 | + | ||||
| 90 | + | log.Debug(). | |||
| 91 | + | Int("n", n). | |||
| 92 | + | Int("pos", pos). | |||
| 93 | + | Msg("deleted text before cursor in GTK entry") | |||
| 94 | + | ||||
| 95 | + | return nil | |||
| 96 | + | } | |||
A internal/infrastructure/textinput/webview_target.go +133 -0
| @@ -0,0 +1,133 @@ | |||
| 1 | + | package textinput | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "context" | |
| 5 | + | "fmt" | |
| 6 | + | "time" | |
| 7 | + | ||
| 8 | + | "github.com/bnema/dumber/internal/application/port" | |
| 9 | + | "github.com/bnema/dumber/internal/logging" | |
| 10 | + | "github.com/bnema/puregotk-webkit/webkit" | |
| 11 | + | ) | |
| 12 | + | ||
| 13 | + | // clipboardRestoreDelay is the delay before restoring the clipboard after paste. | |
| 14 | + | const clipboardRestoreDelay = 50 * time.Millisecond | |
| 15 | + | ||
| 16 | + | // deleteBeforeCursorJS is the JavaScript template to delete n characters before cursor. | |
| 17 | + | // It handles both input/textarea elements and contenteditable elements. | |
| 18 | + | const deleteBeforeCursorJS = ` | |
| 19 | + | (function() { | |
| 20 | + | const n = %d; | |
| 21 | + | const el = document.activeElement; | |
| 22 | + | if (!el) return; | |
| 23 | + | ||
| 24 | + | // Handle input and textarea elements | |
| 25 | + | if (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA') { | |
| 26 | + | const start = el.selectionStart; | |
| 27 | + | const end = el.selectionEnd; | |
| 28 | + | if (start === end && start >= n) { | |
| 29 | + | el.value = el.value.slice(0, start - n) + el.value.slice(end); | |
| 30 | + | el.selectionStart = el.selectionEnd = start - n; | |
| 31 | + | // Trigger input event for frameworks like React | |
| 32 | + | el.dispatchEvent(new Event('input', { bubbles: true })); | |
| 33 | + | } | |
| 34 | + | return; | |
| 35 | + | } | |
| 36 | + | ||
| 37 | + | // Handle contenteditable elements | |
| 38 | + | if (el.isContentEditable) { | |
| 39 | + | const sel = window.getSelection(); | |
| 40 | + | if (!sel.rangeCount) return; | |
| 41 | + | const range = sel.getRangeAt(0); | |
| 42 | + | for (let i = 0; i < n; i++) { | |
| 43 | + | sel.modify('extend', 'backward', 'character'); | |
| 44 | + | } | |
| 45 | + | sel.deleteFromDocument(); | |
| 46 | + | } | |
| 47 | + | })(); | |
| 48 | + | ` | |
| 49 | + | ||
| 50 | + | // WebViewTarget implements TextInputTarget for WebView text fields. | |
| 51 | + | // It uses the clipboard-paste pattern to insert text: | |
| 52 | + | // 1. Save current clipboard | |
| 53 | + | // 2. Write text to clipboard | |
| 54 | + | // 3. Execute paste command | |
| 55 | + | // 4. Restore original clipboard after a short delay | |
| 56 | + | type WebViewTarget struct { | |
| 57 | + | webView *webkit.WebView | |
| 58 | + | clipboard port.Clipboard | |
| 59 | + | } | |
| 60 | + | ||
| 61 | + | // Compile-time interface check. | |
| 62 | + | var _ port.TextInputTarget = (*WebViewTarget)(nil) | |
| 63 | + | ||
| 64 | + | // NewWebViewTarget creates a new WebView target. | |
| 65 | + | func NewWebViewTarget(webView *webkit.WebView, clipboard port.Clipboard) *WebViewTarget { | |
| 66 | + | return &WebViewTarget{ | |
| 67 | + | webView: webView, | |
| 68 | + | clipboard: clipboard, | |
| 69 | + | } | |
| 70 | + | } | |
| 71 | + | ||
| 72 | + | // InsertText inserts text at the current cursor position in the WebView. | |
| 73 | + | // Uses clipboard-paste pattern since WebView doesn't expose direct text insertion. | |
| 74 | + | func (t *WebViewTarget) InsertText(ctx context.Context, text string) error { | |
| 75 | + | log := logging.FromContext(ctx) | |
| 76 | + | ||
| 77 | + | if t.webView == nil { | |
| 78 | + | log.Warn().Msg("WebViewTarget: webView is nil") | |
| 79 | + | return nil | |
| 80 | + | } | |
| 81 | + | ||
| 82 | + | // Save current clipboard contents | |
| 83 | + | originalClipboard, _ := t.clipboard.ReadText(ctx) | |
| 84 | + | ||
| 85 | + | // Write the accent to clipboard | |
| 86 | + | if err := t.clipboard.WriteText(ctx, text); err != nil { | |
| 87 | + | log.Error().Err(err).Msg("failed to write text to clipboard") | |
| 88 | + | return err | |
| 89 | + | } | |
| 90 | + | ||
| 91 | + | // Execute paste command | |
| 92 | + | t.webView.ExecuteEditingCommand(webkit.EDITING_COMMAND_PASTE) | |
| 93 | + | ||
| 94 | + | log.Debug(). | |
| 95 | + | Str("text", text). | |
| 96 | + | Msg("inserted text into WebView via clipboard paste") | |
| 97 | + | ||
| 98 | + | // Restore original clipboard after a short delay | |
| 99 | + | // This allows the paste operation to complete first | |
| 100 | + | // Use context.Background() since this cleanup should complete regardless of ctx cancellation | |
| 101 | + | go func() { | |
| 102 | + | time.Sleep(clipboardRestoreDelay) | |
| 103 | + | if originalClipboard != "" { | |
| 104 | + | if err := t.clipboard.WriteText(context.Background(), originalClipboard); err != nil { | |
| 105 | + | log.Debug().Err(err).Msg("failed to restore clipboard") | |
| 106 | + | } | |
| 107 | + | } | |
| 108 | + | }() | |
| 109 | + | ||
| 110 | + | return nil | |
| 111 | + | } | |
| 112 | + | ||
| 113 | + | // DeleteBeforeCursor deletes n characters before the cursor in the WebView. | |
| 114 | + | // Uses JavaScript to manipulate the focused input element. | |
| 115 | + | func (t *WebViewTarget) DeleteBeforeCursor(ctx context.Context, n int) error { | |
| 116 | + | log := logging.FromContext(ctx) | |
| 117 | + | ||
| 118 | + | if t.webView == nil { | |
| 119 | + | log.Warn().Msg("WebViewTarget: webView is nil") | |
| 120 | + | return nil | |
| 121 | + | } | |
| 122 | + | ||
| 123 | + | if n <= 0 { | |
| 124 | + | return nil | |
| 125 | + | } | |
| 126 | + | ||
| 127 | + | // Execute JavaScript to delete characters | |
| 128 | + | script := fmt.Sprintf(deleteBeforeCursorJS, n) | |
| 129 | + | t.webView.EvaluateJavascript(script, -1, nil, nil, nil, nil, 0) | |
| 130 | + | ||
| 131 | + | log.Debug().Int("n", n).Msg("deleted characters before cursor in WebView") | |
| 132 | + | return nil | |
| 133 | + | } | |
| @@ -0,0 +1,133 @@ | |||||
| 1 | + | package textinput | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | "fmt" | |||
| 6 | + | "time" | |||
| 7 | + | ||||
| 8 | + | "github.com/bnema/dumber/internal/application/port" | |||
| 9 | + | "github.com/bnema/dumber/internal/logging" | |||
| 10 | + | "github.com/bnema/puregotk-webkit/webkit" | |||
| 11 | + | ) | |||
| 12 | + | ||||
| 13 | + | // clipboardRestoreDelay is the delay before restoring the clipboard after paste. | |||
| 14 | + | const clipboardRestoreDelay = 50 * time.Millisecond | |||
| 15 | + | ||||
| 16 | + | // deleteBeforeCursorJS is the JavaScript template to delete n characters before cursor. | |||
| 17 | + | // It handles both input/textarea elements and contenteditable elements. | |||
| 18 | + | const deleteBeforeCursorJS = ` | |||
| 19 | + | (function() { | |||
| 20 | + | const n = %d; | |||
| 21 | + | const el = document.activeElement; | |||
| 22 | + | if (!el) return; | |||
| 23 | + | ||||
| 24 | + | // Handle input and textarea elements | |||
| 25 | + | if (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA') { | |||
| 26 | + | const start = el.selectionStart; | |||
| 27 | + | const end = el.selectionEnd; | |||
| 28 | + | if (start === end && start >= n) { | |||
| 29 | + | el.value = el.value.slice(0, start - n) + el.value.slice(end); | |||
| 30 | + | el.selectionStart = el.selectionEnd = start - n; | |||
| 31 | + | // Trigger input event for frameworks like React | |||
| 32 | + | el.dispatchEvent(new Event('input', { bubbles: true })); | |||
| 33 | + | } | |||
| 34 | + | return; | |||
| 35 | + | } | |||
| 36 | + | ||||
| 37 | + | // Handle contenteditable elements | |||
| 38 | + | if (el.isContentEditable) { | |||
| 39 | + | const sel = window.getSelection(); | |||
| 40 | + | if (!sel.rangeCount) return; | |||
| 41 | + | const range = sel.getRangeAt(0); | |||
| 42 | + | for (let i = 0; i < n; i++) { | |||
| 43 | + | sel.modify('extend', 'backward', 'character'); | |||
| 44 | + | } | |||
| 45 | + | sel.deleteFromDocument(); | |||
| 46 | + | } | |||
| 47 | + | })(); | |||
| 48 | + | ` | |||
| 49 | + | ||||
| 50 | + | // WebViewTarget implements TextInputTarget for WebView text fields. | |||
| 51 | + | // It uses the clipboard-paste pattern to insert text: | |||
| 52 | + | // 1. Save current clipboard | |||
| 53 | + | // 2. Write text to clipboard | |||
| 54 | + | // 3. Execute paste command | |||
| 55 | + | // 4. Restore original clipboard after a short delay | |||
| 56 | + | type WebViewTarget struct { | |||
| 57 | + | webView *webkit.WebView | |||
| 58 | + | clipboard port.Clipboard | |||
| 59 | + | } | |||
| 60 | + | ||||
| 61 | + | // Compile-time interface check. | |||
| 62 | + | var _ port.TextInputTarget = (*WebViewTarget)(nil) | |||
| 63 | + | ||||
| 64 | + | // NewWebViewTarget creates a new WebView target. | |||
| 65 | + | func NewWebViewTarget(webView *webkit.WebView, clipboard port.Clipboard) *WebViewTarget { | |||
| 66 | + | return &WebViewTarget{ | |||
| 67 | + | webView: webView, | |||
| 68 | + | clipboard: clipboard, | |||
| 69 | + | } | |||
| 70 | + | } | |||
| 71 | + | ||||
| 72 | + | // InsertText inserts text at the current cursor position in the WebView. | |||
| 73 | + | // Uses clipboard-paste pattern since WebView doesn't expose direct text insertion. | |||
| 74 | + | func (t *WebViewTarget) InsertText(ctx context.Context, text string) error { | |||
| 75 | + | log := logging.FromContext(ctx) | |||
| 76 | + | ||||
| 77 | + | if t.webView == nil { | |||
| 78 | + | log.Warn().Msg("WebViewTarget: webView is nil") | |||
| 79 | + | return nil | |||
| 80 | + | } | |||
| 81 | + | ||||
| 82 | + | // Save current clipboard contents | |||
| 83 | + | originalClipboard, _ := t.clipboard.ReadText(ctx) | |||
| 84 | + | ||||
| 85 | + | // Write the accent to clipboard | |||
| 86 | + | if err := t.clipboard.WriteText(ctx, text); err != nil { | |||
| 87 | + | log.Error().Err(err).Msg("failed to write text to clipboard") | |||
| 88 | + | return err | |||
| 89 | + | } | |||
| 90 | + | ||||
| 91 | + | // Execute paste command | |||
| 92 | + | t.webView.ExecuteEditingCommand(webkit.EDITING_COMMAND_PASTE) | |||
| 93 | + | ||||
| 94 | + | log.Debug(). | |||
| 95 | + | Str("text", text). | |||
| 96 | + | Msg("inserted text into WebView via clipboard paste") | |||
| 97 | + | ||||
| 98 | + | // Restore original clipboard after a short delay | |||
| 99 | + | // This allows the paste operation to complete first | |||
| 100 | + | // Use context.Background() since this cleanup should complete regardless of ctx cancellation | |||
| 101 | + | go func() { | |||
| 102 | + | time.Sleep(clipboardRestoreDelay) | |||
| 103 | + | if originalClipboard != "" { | |||
| 104 | + | if err := t.clipboard.WriteText(context.Background(), originalClipboard); err != nil { | |||
| 105 | + | log.Debug().Err(err).Msg("failed to restore clipboard") | |||
| 106 | + | } | |||
| 107 | + | } | |||
| 108 | + | }() | |||
| 109 | + | ||||
| 110 | + | return nil | |||
| 111 | + | } | |||
| 112 | + | ||||
| 113 | + | // DeleteBeforeCursor deletes n characters before the cursor in the WebView. | |||
| 114 | + | // Uses JavaScript to manipulate the focused input element. | |||
| 115 | + | func (t *WebViewTarget) DeleteBeforeCursor(ctx context.Context, n int) error { | |||
| 116 | + | log := logging.FromContext(ctx) | |||
| 117 | + | ||||
| 118 | + | if t.webView == nil { | |||
| 119 | + | log.Warn().Msg("WebViewTarget: webView is nil") | |||
| 120 | + | return nil | |||
| 121 | + | } | |||
| 122 | + | ||||
| 123 | + | if n <= 0 { | |||
| 124 | + | return nil | |||
| 125 | + | } | |||
| 126 | + | ||||
| 127 | + | // Execute JavaScript to delete characters | |||
| 128 | + | script := fmt.Sprintf(deleteBeforeCursorJS, n) | |||
| 129 | + | t.webView.EvaluateJavascript(script, -1, nil, nil, nil, nil, 0) | |||
| 130 | + | ||||
| 131 | + | log.Debug().Int("n", n).Msg("deleted characters before cursor in WebView") | |||
| 132 | + | return nil | |||
| 133 | + | } | |||
M internal/infrastructure/webkit/messaging.go +2 -2
| @@ -60,8 +60,8 @@ func NewMessageRouter(ctx context.Context) *MessageRouter { | |||
| 60 | 60 | } | |
| 61 | 61 | ||
| 62 | 62 | return &MessageRouter{ | |
| 63 | - | handlers: make(map[string]handlerEntry), | |
| 64 | - | baseCtx: ctx, | |
| 63 | + | handlers: make(map[string]handlerEntry), | |
| 64 | + | baseCtx: ctx, | |
| 65 | 65 | syncedIDs: make(map[WebViewID]bool), | |
| 66 | 66 | } | |
| 67 | 67 | } | |
| @@ -60,8 +60,8 @@ func NewMessageRouter(ctx context.Context) *MessageRouter { | |||||
| 60 | } | 60 | } | ||
| 61 | 61 | ||||
| 62 | return &MessageRouter{ | 62 | return &MessageRouter{ | ||
| 63 | - | handlers: make(map[string]handlerEntry), | 63 | + | handlers: make(map[string]handlerEntry), |
| 64 | - | baseCtx: ctx, | 64 | + | baseCtx: ctx, |
| 65 | syncedIDs: make(map[WebViewID]bool), | 65 | syncedIDs: make(map[WebViewID]bool), | ||
| 66 | } | 66 | } | ||
| 67 | } | 67 | } | ||
M internal/ui/app.go +101 -0
| @@ -14,6 +14,7 @@ import ( | |||
| 14 | 14 | "github.com/bnema/dumber/internal/infrastructure/desktop" | |
| 15 | 15 | "github.com/bnema/dumber/internal/infrastructure/filtering" | |
| 16 | 16 | "github.com/bnema/dumber/internal/infrastructure/snapshot" | |
| 17 | + | "github.com/bnema/dumber/internal/infrastructure/textinput" | |
| 17 | 18 | "github.com/bnema/dumber/internal/infrastructure/webkit" | |
| 18 | 19 | ||
| 19 | 20 | "github.com/bnema/dumber/internal/infrastructure/webkit/handlers" | |
| @@ -108,6 +109,11 @@ type App struct { | |||
| 108 | 109 | ||
| 109 | 110 | movePaneToTabUC *usecase.MovePaneToTabUseCase | |
| 110 | 111 | ||
| 112 | + | // Accent picker for dead keys support | |
| 113 | + | accentPicker *component.AccentPicker | |
| 114 | + | insertAccentUC *usecase.InsertAccentUseCase | |
| 115 | + | accentFocusProvider *textinput.FocusProvider | |
| 116 | + | ||
| 111 | 117 | // lifecycle | |
| 112 | 118 | cancel context.CancelCauseFunc | |
| 113 | 119 | } | |
| @@ -217,6 +223,7 @@ func (a *App) onActivate(ctx context.Context) { | |||
| 217 | 223 | a.initLayoutInfrastructure() | |
| 218 | 224 | a.initAppToasterOverlay() | |
| 219 | 225 | a.initFocusAndBorderOverlay() | |
| 226 | + | a.initAccentPicker(ctx) | |
| 220 | 227 | ||
| 221 | 228 | a.initCoordinators(ctx) | |
| 222 | 229 | a.initKeyboardHandler(ctx) | |
| @@ -387,6 +394,49 @@ func (a *App) initFocusAndBorderOverlay() { | |||
| 387 | 394 | a.mainWindow.AddOverlay(gtkWidget) | |
| 388 | 395 | } | |
| 389 | 396 | ||
| 397 | + | func (a *App) initAccentPicker(ctx context.Context) { | |
| 398 | + | log := logging.FromContext(ctx) | |
| 399 | + | ||
| 400 | + | if a.widgetFactory == nil || a.mainWindow == nil { | |
| 401 | + | log.Debug().Msg("widget factory or main window not available, skipping accent picker") | |
| 402 | + | return | |
| 403 | + | } | |
| 404 | + | ||
| 405 | + | // Create accent picker component | |
| 406 | + | a.accentPicker = component.NewAccentPicker(a.widgetFactory) | |
| 407 | + | if a.accentPicker == nil { | |
| 408 | + | log.Warn().Msg("failed to create accent picker") | |
| 409 | + | return | |
| 410 | + | } | |
| 411 | + | ||
| 412 | + | // Add to main window overlay | |
| 413 | + | pickerWidget := a.accentPicker.Widget() | |
| 414 | + | if pickerWidget != nil { | |
| 415 | + | gtkWidget := pickerWidget.GtkWidget() | |
| 416 | + | if gtkWidget != nil { | |
| 417 | + | a.mainWindow.AddOverlay(gtkWidget) | |
| 418 | + | } | |
| 419 | + | } | |
| 420 | + | ||
| 421 | + | // Create focus provider (tracks which input has focus) | |
| 422 | + | a.accentFocusProvider = textinput.NewFocusProvider() | |
| 423 | + | ||
| 424 | + | // Create the use case with glib.IdleAdd for thread-safe GTK calls | |
| 425 | + | a.insertAccentUC = usecase.NewInsertAccentUseCase( | |
| 426 | + | a.accentFocusProvider, | |
| 427 | + | a.accentPicker, | |
| 428 | + | func(fn func()) { | |
| 429 | + | cb := glib.SourceFunc(func(_ uintptr) bool { | |
| 430 | + | fn() | |
| 431 | + | return false // Don't repeat | |
| 432 | + | }) | |
| 433 | + | glib.IdleAdd(&cb, 0) | |
| 434 | + | }, | |
| 435 | + | ) | |
| 436 | + | ||
| 437 | + | log.Debug().Msg("accent picker initialized") | |
| 438 | + | } | |
| 439 | + | ||
| 390 | 440 | func (a *App) initKeyboardHandler(ctx context.Context) { | |
| 391 | 441 | if a.mainWindow == nil || a.deps == nil || a.deps.Config == nil { | |
| 392 | 442 | return | |
| @@ -414,6 +464,10 @@ func (a *App) initKeyboardHandler(ctx context.Context) { | |||
| 414 | 464 | } | |
| 415 | 465 | return wsView.IsOmniboxVisible() | |
| 416 | 466 | }) | |
| 467 | + | // Wire accent handler for dead keys support | |
| 468 | + | if a.insertAccentUC != nil { | |
| 469 | + | a.keyboardHandler.SetAccentHandler(a.insertAccentUC) | |
| 470 | + | } | |
| 417 | 471 | a.keyboardHandler.AttachTo(a.mainWindow.Window()) | |
| 418 | 472 | ||
| 419 | 473 | // Create global shortcut handler for Alt+1-9 tab switching. | |
| @@ -461,6 +515,18 @@ func (a *App) initOmniboxConfig(ctx context.Context) { | |||
| 461 | 515 | log.Error().Err(err).Str("url", url).Msg("navigation failed") | |
| 462 | 516 | } | |
| 463 | 517 | }, | |
| 518 | + | OnFocusIn: func(entry *gtk.SearchEntry) { | |
| 519 | + | // Set omnibox entry as the focused input for accent picker | |
| 520 | + | if a.accentFocusProvider != nil && entry != nil { | |
| 521 | + | a.accentFocusProvider.SetFocusedInput(textinput.NewGTKEntryTarget(entry)) | |
| 522 | + | } | |
| 523 | + | }, | |
| 524 | + | OnFocusOut: func() { | |
| 525 | + | // When omnibox loses focus, set WebView as the focused input | |
| 526 | + | if a.accentFocusProvider != nil { | |
| 527 | + | a.accentFocusProvider.SetFocusedInput(a.getActiveWebViewTarget()) | |
| 528 | + | } | |
| 529 | + | }, | |
| 464 | 530 | } | |
| 465 | 531 | a.navCoord.SetOmniboxProvider(a) | |
| 466 | 532 | log.Debug().Msg("omnibox config stored, provider set") | |
| @@ -479,6 +545,18 @@ func (a *App) initFindBarConfig() { | |||
| 479 | 545 | } | |
| 480 | 546 | return wv.GetFindController() | |
| 481 | 547 | }, | |
| 548 | + | OnFocusIn: func(entry *gtk.SearchEntry) { | |
| 549 | + | // Set find bar entry as the focused input for accent picker | |
| 550 | + | if a.accentFocusProvider != nil && entry != nil { | |
| 551 | + | a.accentFocusProvider.SetFocusedInput(textinput.NewGTKEntryTarget(entry)) | |
| 552 | + | } | |
| 553 | + | }, | |
| 554 | + | OnFocusOut: func() { | |
| 555 | + | // When find bar loses focus, set WebView as the focused input | |
| 556 | + | if a.accentFocusProvider != nil { | |
| 557 | + | a.accentFocusProvider.SetFocusedInput(a.getActiveWebViewTarget()) | |
| 558 | + | } | |
| 559 | + | }, | |
| 482 | 560 | } | |
| 483 | 561 | } | |
| 484 | 562 | ||
| @@ -1219,6 +1297,12 @@ func (a *App) initCoordinators(ctx context.Context) { | |||
| 1219 | 1297 | a.deps.OnFirstWebViewShown(ctx) | |
| 1220 | 1298 | }) | |
| 1221 | 1299 | } | |
| 1300 | + | ||
| 1301 | + | // Set the WebView as the focused input for accent picker (if this is the active pane) | |
| 1302 | + | ws := a.activeWorkspace() | |
| 1303 | + | if ws != nil && ws.ActivePaneID == paneID && a.accentFocusProvider != nil { | |
| 1304 | + | a.accentFocusProvider.SetFocusedInput(a.getActiveWebViewTarget()) | |
| 1305 | + | } | |
| 1222 | 1306 | }) | |
| 1223 | 1307 | ||
| 1224 | 1308 | // 5. Keyboard Dispatcher | |
| @@ -1538,6 +1622,23 @@ func (a *App) activeWorkspaceView() *component.WorkspaceView { | |||
| 1538 | 1622 | return a.workspaceViews[activeTab.ID] | |
| 1539 | 1623 | } | |
| 1540 | 1624 | ||
| 1625 | + | // getActiveWebViewTarget returns a TextInputTarget for the active pane's WebView. | |
| 1626 | + | // Used by the accent picker to insert accented characters into web content. | |
| 1627 | + | func (a *App) getActiveWebViewTarget() port.TextInputTarget { | |
| 1628 | + | ws := a.activeWorkspace() | |
| 1629 | + | if ws == nil || a.contentCoord == nil || a.deps == nil || a.deps.Clipboard == nil { | |
| 1630 | + | return nil | |
| 1631 | + | } | |
| 1632 | + | ||
| 1633 | + | wv := a.contentCoord.GetWebView(ws.ActivePaneID) | |
| 1634 | + | if wv == nil { | |
| 1635 | + | return nil | |
| 1636 | + | } | |
| 1637 | + | ||
| 1638 | + | // Get the underlying webkit.WebView for the text input target | |
| 1639 | + | return textinput.NewWebViewTarget(wv.Widget(), a.deps.Clipboard) | |
| 1640 | + | } | |
| 1641 | + | ||
| 1541 | 1642 | // attachPopupToTab attaches a popup WebView to a newly created tab. | |
| 1542 | 1643 | // This is called when a popup uses tabbed behavior. | |
| 1543 | 1644 | func (a *App) attachPopupToTab(ctx context.Context, tabID entity.TabID, pane *entity.Pane, wv *webkit.WebView) { | |
| @@ -14,6 +14,7 @@ import ( | |||||
| 14 | "github.com/bnema/dumber/internal/infrastructure/desktop" | 14 | "github.com/bnema/dumber/internal/infrastructure/desktop" | ||
| 15 | "github.com/bnema/dumber/internal/infrastructure/filtering" | 15 | "github.com/bnema/dumber/internal/infrastructure/filtering" | ||
| 16 | "github.com/bnema/dumber/internal/infrastructure/snapshot" | 16 | "github.com/bnema/dumber/internal/infrastructure/snapshot" | ||
| 17 | + | "github.com/bnema/dumber/internal/infrastructure/textinput" | |||
| 17 | "github.com/bnema/dumber/internal/infrastructure/webkit" | 18 | "github.com/bnema/dumber/internal/infrastructure/webkit" | ||
| 18 | 19 | ||||
| 19 | "github.com/bnema/dumber/internal/infrastructure/webkit/handlers" | 20 | "github.com/bnema/dumber/internal/infrastructure/webkit/handlers" | ||
| @@ -108,6 +109,11 @@ type App struct { | |||||
| 108 | 109 | ||||
| 109 | movePaneToTabUC *usecase.MovePaneToTabUseCase | 110 | movePaneToTabUC *usecase.MovePaneToTabUseCase | ||
| 110 | 111 | ||||
| 112 | + | // Accent picker for dead keys support | |||
| 113 | + | accentPicker *component.AccentPicker | |||
| 114 | + | insertAccentUC *usecase.InsertAccentUseCase | |||
| 115 | + | accentFocusProvider *textinput.FocusProvider | |||
| 116 | + | ||||
| 111 | // lifecycle | 117 | // lifecycle | ||
| 112 | cancel context.CancelCauseFunc | 118 | cancel context.CancelCauseFunc | ||
| 113 | } | 119 | } | ||
| @@ -217,6 +223,7 @@ func (a *App) onActivate(ctx context.Context) { | |||||
| 217 | a.initLayoutInfrastructure() | 223 | a.initLayoutInfrastructure() | ||
| 218 | a.initAppToasterOverlay() | 224 | a.initAppToasterOverlay() | ||
| 219 | a.initFocusAndBorderOverlay() | 225 | a.initFocusAndBorderOverlay() | ||
| 226 | + | a.initAccentPicker(ctx) | |||
| 220 | 227 | ||||
| 221 | a.initCoordinators(ctx) | 228 | a.initCoordinators(ctx) | ||
| 222 | a.initKeyboardHandler(ctx) | 229 | a.initKeyboardHandler(ctx) | ||
| @@ -387,6 +394,49 @@ func (a *App) initFocusAndBorderOverlay() { | |||||
| 387 | a.mainWindow.AddOverlay(gtkWidget) | 394 | a.mainWindow.AddOverlay(gtkWidget) | ||
| 388 | } | 395 | } | ||
| 389 | 396 | ||||
| 397 | + | func (a *App) initAccentPicker(ctx context.Context) { | |||
| 398 | + | log := logging.FromContext(ctx) | |||
| 399 | + | ||||
| 400 | + | if a.widgetFactory == nil || a.mainWindow == nil { | |||
| 401 | + | log.Debug().Msg("widget factory or main window not available, skipping accent picker") | |||
| 402 | + | return | |||
| 403 | + | } | |||
| 404 | + | ||||
| 405 | + | // Create accent picker component | |||
| 406 | + | a.accentPicker = component.NewAccentPicker(a.widgetFactory) | |||
| 407 | + | if a.accentPicker == nil { | |||
| 408 | + | log.Warn().Msg("failed to create accent picker") | |||
| 409 | + | return | |||
| 410 | + | } | |||
| 411 | + | ||||
| 412 | + | // Add to main window overlay | |||
| 413 | + | pickerWidget := a.accentPicker.Widget() | |||
| 414 | + | if pickerWidget != nil { | |||
| 415 | + | gtkWidget := pickerWidget.GtkWidget() | |||
| 416 | + | if gtkWidget != nil { | |||
| 417 | + | a.mainWindow.AddOverlay(gtkWidget) | |||
| 418 | + | } | |||
| 419 | + | } | |||
| 420 | + | ||||
| 421 | + | // Create focus provider (tracks which input has focus) | |||
| 422 | + | a.accentFocusProvider = textinput.NewFocusProvider() | |||
| 423 | + | ||||
| 424 | + | // Create the use case with glib.IdleAdd for thread-safe GTK calls | |||
| 425 | + | a.insertAccentUC = usecase.NewInsertAccentUseCase( | |||
| 426 | + | a.accentFocusProvider, | |||
| 427 | + | a.accentPicker, | |||
| 428 | + | func(fn func()) { | |||
| 429 | + | cb := glib.SourceFunc(func(_ uintptr) bool { | |||
| 430 | + | fn() | |||
| 431 | + | return false // Don't repeat | |||
| 432 | + | }) | |||
| 433 | + | glib.IdleAdd(&cb, 0) | |||
| 434 | + | }, | |||
| 435 | + | ) | |||
| 436 | + | ||||
| 437 | + | log.Debug().Msg("accent picker initialized") | |||
| 438 | + | } | |||
| 439 | + | ||||
| 390 | func (a *App) initKeyboardHandler(ctx context.Context) { | 440 | func (a *App) initKeyboardHandler(ctx context.Context) { | ||
| 391 | if a.mainWindow == nil || a.deps == nil || a.deps.Config == nil { | 441 | if a.mainWindow == nil || a.deps == nil || a.deps.Config == nil { | ||
| 392 | return | 442 | return | ||
| @@ -414,6 +464,10 @@ func (a *App) initKeyboardHandler(ctx context.Context) { | |||||
| 414 | } | 464 | } | ||
| 415 | return wsView.IsOmniboxVisible() | 465 | return wsView.IsOmniboxVisible() | ||
| 416 | }) | 466 | }) | ||
| 467 | + | // Wire accent handler for dead keys support | |||
| 468 | + | if a.insertAccentUC != nil { | |||
| 469 | + | a.keyboardHandler.SetAccentHandler(a.insertAccentUC) | |||
| 470 | + | } | |||
| 417 | a.keyboardHandler.AttachTo(a.mainWindow.Window()) | 471 | a.keyboardHandler.AttachTo(a.mainWindow.Window()) | ||
| 418 | 472 | ||||
| 419 | // Create global shortcut handler for Alt+1-9 tab switching. | 473 | // Create global shortcut handler for Alt+1-9 tab switching. | ||
| @@ -461,6 +515,18 @@ func (a *App) initOmniboxConfig(ctx context.Context) { | |||||
| 461 | log.Error().Err(err).Str("url", url).Msg("navigation failed") | 515 | log.Error().Err(err).Str("url", url).Msg("navigation failed") | ||
| 462 | } | 516 | } | ||
| 463 | }, | 517 | }, | ||
| 518 | + | OnFocusIn: func(entry *gtk.SearchEntry) { | |||
| 519 | + | // Set omnibox entry as the focused input for accent picker | |||
| 520 | + | if a.accentFocusProvider != nil && entry != nil { | |||
| 521 | + | a.accentFocusProvider.SetFocusedInput(textinput.NewGTKEntryTarget(entry)) | |||
| 522 | + | } | |||
| 523 | + | }, | |||
| 524 | + | OnFocusOut: func() { | |||
| 525 | + | // When omnibox loses focus, set WebView as the focused input | |||
| 526 | + | if a.accentFocusProvider != nil { | |||
| 527 | + | a.accentFocusProvider.SetFocusedInput(a.getActiveWebViewTarget()) | |||
| 528 | + | } | |||
| 529 | + | }, | |||
| 464 | } | 530 | } | ||
| 465 | a.navCoord.SetOmniboxProvider(a) | 531 | a.navCoord.SetOmniboxProvider(a) | ||
| 466 | log.Debug().Msg("omnibox config stored, provider set") | 532 | log.Debug().Msg("omnibox config stored, provider set") | ||
| @@ -479,6 +545,18 @@ func (a *App) initFindBarConfig() { | |||||
| 479 | } | 545 | } | ||
| 480 | return wv.GetFindController() | 546 | return wv.GetFindController() | ||
| 481 | }, | 547 | }, | ||
| 548 | + | OnFocusIn: func(entry *gtk.SearchEntry) { | |||
| 549 | + | // Set find bar entry as the focused input for accent picker | |||
| 550 | + | if a.accentFocusProvider != nil && entry != nil { | |||
| 551 | + | a.accentFocusProvider.SetFocusedInput(textinput.NewGTKEntryTarget(entry)) | |||
| 552 | + | } | |||
| 553 | + | }, | |||
| 554 | + | OnFocusOut: func() { | |||
| 555 | + | // When find bar loses focus, set WebView as the focused input | |||
| 556 | + | if a.accentFocusProvider != nil { | |||
| 557 | + | a.accentFocusProvider.SetFocusedInput(a.getActiveWebViewTarget()) | |||
| 558 | + | } | |||
| 559 | + | }, | |||
| 482 | } | 560 | } | ||
| 483 | } | 561 | } | ||
| 484 | 562 | ||||
| @@ -1219,6 +1297,12 @@ func (a *App) initCoordinators(ctx context.Context) { | |||||
| 1219 | a.deps.OnFirstWebViewShown(ctx) | 1297 | a.deps.OnFirstWebViewShown(ctx) | ||
| 1220 | }) | 1298 | }) | ||
| 1221 | } | 1299 | } | ||
| 1300 | + | ||||
| 1301 | + | // Set the WebView as the focused input for accent picker (if this is the active pane) | |||
| 1302 | + | ws := a.activeWorkspace() | |||
| 1303 | + | if ws != nil && ws.ActivePaneID == paneID && a.accentFocusProvider != nil { | |||
| 1304 | + | a.accentFocusProvider.SetFocusedInput(a.getActiveWebViewTarget()) | |||
| 1305 | + | } | |||
| 1222 | }) | 1306 | }) | ||
| 1223 | 1307 | ||||
| 1224 | // 5. Keyboard Dispatcher | 1308 | // 5. Keyboard Dispatcher | ||
| @@ -1538,6 +1622,23 @@ func (a *App) activeWorkspaceView() *component.WorkspaceView { | |||||
| 1538 | return a.workspaceViews[activeTab.ID] | 1622 | return a.workspaceViews[activeTab.ID] | ||
| 1539 | } | 1623 | } | ||
| 1540 | 1624 | ||||
| 1625 | + | // getActiveWebViewTarget returns a TextInputTarget for the active pane's WebView. | |||
| 1626 | + | // Used by the accent picker to insert accented characters into web content. | |||
| 1627 | + | func (a *App) getActiveWebViewTarget() port.TextInputTarget { | |||
| 1628 | + | ws := a.activeWorkspace() | |||
| 1629 | + | if ws == nil || a.contentCoord == nil || a.deps == nil || a.deps.Clipboard == nil { | |||
| 1630 | + | return nil | |||
| 1631 | + | } | |||
| 1632 | + | ||||
| 1633 | + | wv := a.contentCoord.GetWebView(ws.ActivePaneID) | |||
| 1634 | + | if wv == nil { | |||
| 1635 | + | return nil | |||
| 1636 | + | } | |||
| 1637 | + | ||||
| 1638 | + | // Get the underlying webkit.WebView for the text input target | |||
| 1639 | + | return textinput.NewWebViewTarget(wv.Widget(), a.deps.Clipboard) | |||
| 1640 | + | } | |||
| 1641 | + | ||||
| 1541 | // attachPopupToTab attaches a popup WebView to a newly created tab. | 1642 | // attachPopupToTab attaches a popup WebView to a newly created tab. | ||
| 1542 | // This is called when a popup uses tabbed behavior. | 1643 | // This is called when a popup uses tabbed behavior. | ||
| 1543 | func (a *App) attachPopupToTab(ctx context.Context, tabID entity.TabID, pane *entity.Pane, wv *webkit.WebView) { | 1644 | func (a *App) attachPopupToTab(ctx context.Context, tabID entity.TabID, pane *entity.Pane, wv *webkit.WebView) { | ||
A internal/ui/component/accent_picker.go +319 -0
| @@ -0,0 +1,319 @@ | |||
| 1 | + | package component | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "sync" | |
| 5 | + | ||
| 6 | + | "github.com/bnema/dumber/internal/application/port" | |
| 7 | + | "github.com/bnema/dumber/internal/ui/layout" | |
| 8 | + | "github.com/jwijenbergh/puregotk/v4/gdk" | |
| 9 | + | "github.com/jwijenbergh/puregotk/v4/gtk" | |
| 10 | + | ) | |
| 11 | + | ||
| 12 | + | // Accent picker layout constants. | |
| 13 | + | const ( | |
| 14 | + | accentPickerSpacing = 8 // Spacing between accent items in pixels. | |
| 15 | + | maxNumberedAccentItems = 9 // Maximum items that can be selected with number keys (1-9). | |
| 16 | + | ) | |
| 17 | + | ||
| 18 | + | // AccentPicker displays an overlay with accent character options. | |
| 19 | + | // The user can select an accent using: | |
| 20 | + | // - Left/Right arrow keys to navigate | |
| 21 | + | // - Number keys 1-9 to select directly | |
| 22 | + | // - Enter to confirm selection | |
| 23 | + | // - Escape to cancel | |
| 24 | + | type AccentPicker struct { | |
| 25 | + | factory layout.WidgetFactory | |
| 26 | + | ||
| 27 | + | // Widgets | |
| 28 | + | container layout.BoxWidget // Outer container for positioning | |
| 29 | + | accentLabels []layout.LabelWidget // Individual accent character labels | |
| 30 | + | ||
| 31 | + | // State | |
| 32 | + | accents []rune | |
| 33 | + | selectedIndex int | |
| 34 | + | selectedCb func(rune) | |
| 35 | + | cancelCb func() | |
| 36 | + | visible bool | |
| 37 | + | keyController *gtk.EventControllerKey | |
| 38 | + | retainedClicked []interface{} // Keep click callbacks alive | |
| 39 | + | ||
| 40 | + | mu sync.Mutex | |
| 41 | + | } | |
| 42 | + | ||
| 43 | + | // Compile-time interface check. | |
| 44 | + | var _ port.AccentPickerUI = (*AccentPicker)(nil) | |
| 45 | + | ||
| 46 | + | // NewAccentPicker creates a new accent picker component. | |
| 47 | + | func NewAccentPicker(factory layout.WidgetFactory) *AccentPicker { | |
| 48 | + | // Create container box | |
| 49 | + | container := factory.NewBox(layout.OrientationHorizontal, accentPickerSpacing) | |
| 50 | + | container.AddCssClass("accent-picker") | |
| 51 | + | ||
| 52 | + | // Position at bottom-center | |
| 53 | + | container.SetHalign(gtk.AlignCenterValue) | |
| 54 | + | container.SetValign(gtk.AlignEndValue) | |
| 55 | + | ||
| 56 | + | // Don't expand | |
| 57 | + | container.SetHexpand(false) | |
| 58 | + | container.SetVexpand(false) | |
| 59 | + | ||
| 60 | + | // Make focusable so it can receive keyboard events | |
| 61 | + | container.SetFocusable(true) | |
| 62 | + | container.SetCanFocus(true) | |
| 63 | + | ||
| 64 | + | // Hidden by default | |
| 65 | + | container.SetVisible(false) | |
| 66 | + | ||
| 67 | + | ap := &AccentPicker{ | |
| 68 | + | factory: factory, | |
| 69 | + | container: container, | |
| 70 | + | selectedIndex: 0, | |
| 71 | + | } | |
| 72 | + | ||
| 73 | + | // Set up key event handling | |
| 74 | + | ap.setupKeyboardHandling() | |
| 75 | + | ||
| 76 | + | // Set up hover-to-focus: regain focus when mouse enters | |
| 77 | + | ap.setupHoverFocus() | |
| 78 | + | ||
| 79 | + | return ap | |
| 80 | + | } | |
| 81 | + | ||
| 82 | + | // Widget returns the accent picker's container widget. | |
| 83 | + | func (ap *AccentPicker) Widget() layout.Widget { | |
| 84 | + | return ap.container | |
| 85 | + | } | |
| 86 | + | ||
| 87 | + | // Show displays the accent picker with the given accent options. | |
| 88 | + | func (ap *AccentPicker) Show(accents []rune, selectedCb func(rune), cancelCb func()) { | |
| 89 | + | ap.mu.Lock() | |
| 90 | + | defer ap.mu.Unlock() | |
| 91 | + | ||
| 92 | + | ap.accents = accents | |
| 93 | + | ap.selectedCb = selectedCb | |
| 94 | + | ap.cancelCb = cancelCb | |
| 95 | + | ap.selectedIndex = 0 | |
| 96 | + | ap.visible = true | |
| 97 | + | ||
| 98 | + | // Clear existing labels | |
| 99 | + | ap.clearLabels() | |
| 100 | + | ||
| 101 | + | // Create new labels for each accent | |
| 102 | + | ap.accentLabels = make([]layout.LabelWidget, len(accents)) | |
| 103 | + | ap.retainedClicked = make([]interface{}, len(accents)) | |
| 104 | + | ||
| 105 | + | for i, accent := range accents { | |
| 106 | + | label := ap.factory.NewLabel(string(accent)) | |
| 107 | + | label.AddCssClass("accent-picker-item") | |
| 108 | + | ||
| 109 | + | // Add number hint for first maxNumberedAccentItems items | |
| 110 | + | if i < maxNumberedAccentItems { | |
| 111 | + | label.AddCssClass("accent-picker-numbered") | |
| 112 | + | } | |
| 113 | + | ||
| 114 | + | // Set up click handling | |
| 115 | + | clickCtrl := gtk.NewGestureClick() | |
| 116 | + | idx := i // Capture for closure | |
| 117 | + | clickCb := func(_ gtk.GestureClick, _ int, _ float64, _ float64) { | |
| 118 | + | ap.selectAccent(idx) | |
| 119 | + | } | |
| 120 | + | clickCtrl.ConnectPressed(&clickCb) | |
| 121 | + | ap.retainedClicked[i] = clickCb | |
| 122 | + | label.AddController(&clickCtrl.EventController) | |
| 123 | + | ||
| 124 | + | ap.accentLabels[i] = label | |
| 125 | + | ap.container.Append(label) | |
| 126 | + | } | |
| 127 | + | ||
| 128 | + | // Highlight first item | |
| 129 | + | ap.updateSelection() | |
| 130 | + | ||
| 131 | + | ap.container.SetVisible(true) | |
| 132 | + | ap.container.GrabFocus() | |
| 133 | + | } | |
| 134 | + | ||
| 135 | + | // Hide hides the accent picker. | |
| 136 | + | func (ap *AccentPicker) Hide() { | |
| 137 | + | ap.mu.Lock() | |
| 138 | + | defer ap.mu.Unlock() | |
| 139 | + | ||
| 140 | + | ap.visible = false | |
| 141 | + | ap.container.SetVisible(false) | |
| 142 | + | ap.clearLabels() | |
| 143 | + | } | |
| 144 | + | ||
| 145 | + | // IsVisible returns true if the accent picker is currently visible. | |
| 146 | + | func (ap *AccentPicker) IsVisible() bool { | |
| 147 | + | ap.mu.Lock() | |
| 148 | + | defer ap.mu.Unlock() | |
| 149 | + | return ap.visible | |
| 150 | + | } | |
| 151 | + | ||
| 152 | + | // clearLabels removes all accent labels from the container. | |
| 153 | + | func (ap *AccentPicker) clearLabels() { | |
| 154 | + | for _, label := range ap.accentLabels { | |
| 155 | + | label.Unparent() | |
| 156 | + | } | |
| 157 | + | ap.accentLabels = nil | |
| 158 | + | ap.retainedClicked = nil | |
| 159 | + | } | |
| 160 | + | ||
| 161 | + | // updateSelection updates the visual selection highlight. | |
| 162 | + | func (ap *AccentPicker) updateSelection() { | |
| 163 | + | for i, label := range ap.accentLabels { | |
| 164 | + | if i == ap.selectedIndex { | |
| 165 | + | label.AddCssClass("accent-picker-selected") | |
| 166 | + | } else { | |
| 167 | + | label.RemoveCssClass("accent-picker-selected") | |
| 168 | + | } | |
| 169 | + | } | |
| 170 | + | } | |
| 171 | + | ||
| 172 | + | // selectAccent selects the accent at the given index. | |
| 173 | + | func (ap *AccentPicker) selectAccent(index int) { | |
| 174 | + | ap.mu.Lock() | |
| 175 | + | if index < 0 || index >= len(ap.accents) || ap.selectedCb == nil { | |
| 176 | + | ap.mu.Unlock() | |
| 177 | + | return | |
| 178 | + | } | |
| 179 | + | accent := ap.accents[index] | |
| 180 | + | // Invoke callback while holding lock to avoid race with Hide() clearing callbacks | |
| 181 | + | ap.selectedCb(accent) | |
| 182 | + | ap.mu.Unlock() | |
| 183 | + | } | |
| 184 | + | ||
| 185 | + | // cancel cancels the accent picker. | |
| 186 | + | func (ap *AccentPicker) cancel() { | |
| 187 | + | ap.mu.Lock() | |
| 188 | + | if ap.cancelCb == nil { | |
| 189 | + | ap.mu.Unlock() | |
| 190 | + | return | |
| 191 | + | } | |
| 192 | + | // Invoke callback while holding lock to avoid race with Hide() clearing callbacks | |
| 193 | + | ap.cancelCb() | |
| 194 | + | ap.mu.Unlock() | |
| 195 | + | } | |
| 196 | + | ||
| 197 | + | // setupKeyboardHandling sets up key event handling for navigation. | |
| 198 | + | func (ap *AccentPicker) setupKeyboardHandling() { | |
| 199 | + | keyCtrl := gtk.NewEventControllerKey() | |
| 200 | + | ||
| 201 | + | keyPressedCb := func(_ gtk.EventControllerKey, keyval uint, _ uint, _ gdk.ModifierType) bool { | |
| 202 | + | return ap.handleKeyPress(keyval) | |
| 203 | + | } | |
| 204 | + | ||
| 205 | + | keyCtrl.ConnectKeyPressed(&keyPressedCb) | |
| 206 | + | ap.keyController = keyCtrl | |
| 207 | + | ap.container.AddController(&keyCtrl.EventController) | |
| 208 | + | } | |
| 209 | + | ||
| 210 | + | // setupHoverFocus sets up mouse hover to regain focus. | |
| 211 | + | // This ensures the picker regains focus when the mouse enters, | |
| 212 | + | // so keyboard navigation (including Escape) continues to work. | |
| 213 | + | func (ap *AccentPicker) setupHoverFocus() { | |
| 214 | + | motionCtrl := gtk.NewEventControllerMotion() | |
| 215 | + | ||
| 216 | + | enterCb := func(_ gtk.EventControllerMotion, _ float64, _ float64) { | |
| 217 | + | ap.mu.Lock() | |
| 218 | + | visible := ap.visible | |
| 219 | + | ap.mu.Unlock() | |
| 220 | + | ||
| 221 | + | if visible { | |
| 222 | + | ap.container.GrabFocus() | |
| 223 | + | } | |
| 224 | + | } | |
| 225 | + | ||
| 226 | + | motionCtrl.ConnectEnter(&enterCb) | |
| 227 | + | ap.container.AddController(&motionCtrl.EventController) | |
| 228 | + | } | |
| 229 | + | ||
| 230 | + | // handleKeyPress processes a key press event. | |
| 231 | + | // Returns true if the key was handled. | |
| 232 | + | func (ap *AccentPicker) handleKeyPress(keyval uint) bool { | |
| 233 | + | ap.mu.Lock() | |
| 234 | + | if !ap.visible { | |
| 235 | + | ap.mu.Unlock() | |
| 236 | + | return false | |
| 237 | + | } | |
| 238 | + | ||
| 239 | + | // Handle navigation keys | |
| 240 | + | if handled, shouldReturn := ap.handleNavigationKey(keyval); shouldReturn { | |
| 241 | + | return handled | |
| 242 | + | } | |
| 243 | + | ||
| 244 | + | // Handle number keys for direct selection | |
| 245 | + | if idx, ok := ap.numberKeyToIndex(keyval); ok { | |
| 246 | + | ap.mu.Unlock() | |
| 247 | + | ap.selectAccent(idx) | |
| 248 | + | return true | |
| 249 | + | } | |
| 250 | + | ||
| 251 | + | ap.mu.Unlock() | |
| 252 | + | return false | |
| 253 | + | } | |
| 254 | + | ||
| 255 | + | // handleNavigationKey handles arrow keys, Enter, and Escape. | |
| 256 | + | // Returns (handled, shouldReturn) - if shouldReturn is true, caller should return handled. | |
| 257 | + | func (ap *AccentPicker) handleNavigationKey(keyval uint) (bool, bool) { | |
| 258 | + | switch keyval { | |
| 259 | + | case uint(gdk.KEY_Left): | |
| 260 | + | if ap.selectedIndex > 0 { | |
| 261 | + | ap.selectedIndex-- | |
| 262 | + | ap.updateSelection() | |
| 263 | + | } | |
| 264 | + | ap.mu.Unlock() | |
| 265 | + | return true, true | |
| 266 | + | ||
| 267 | + | case uint(gdk.KEY_Right): | |
| 268 | + | if ap.selectedIndex < len(ap.accents)-1 { | |
| 269 | + | ap.selectedIndex++ | |
| 270 | + | ap.updateSelection() | |
| 271 | + | } | |
| 272 | + | ap.mu.Unlock() | |
| 273 | + | return true, true | |
| 274 | + | ||
| 275 | + | case uint(gdk.KEY_Return), uint(gdk.KEY_KP_Enter): | |
| 276 | + | idx := ap.selectedIndex | |
| 277 | + | ap.mu.Unlock() | |
| 278 | + | ap.selectAccent(idx) | |
| 279 | + | return true, true | |
| 280 | + | ||
| 281 | + | case uint(gdk.KEY_Escape): | |
| 282 | + | ap.mu.Unlock() | |
| 283 | + | ap.cancel() | |
| 284 | + | return true, true | |
| 285 | + | ||
| 286 | + | default: | |
| 287 | + | return false, false | |
| 288 | + | } | |
| 289 | + | } | |
| 290 | + | ||
| 291 | + | // numberKeyToIndex converts a number key (1-9) to an accent index. | |
| 292 | + | // Returns the index and true if the key is a valid number key. | |
| 293 | + | func (ap *AccentPicker) numberKeyToIndex(keyval uint) (int, bool) { | |
| 294 | + | keyMap := map[uint]int{ | |
| 295 | + | uint(gdk.KEY_1): 0, | |
| 296 | + | uint(gdk.KEY_KP_1): 0, | |
| 297 | + | uint(gdk.KEY_2): 1, | |
| 298 | + | uint(gdk.KEY_KP_2): 1, | |
| 299 | + | uint(gdk.KEY_3): 2, | |
| 300 | + | uint(gdk.KEY_KP_3): 2, | |
| 301 | + | uint(gdk.KEY_4): 3, | |
| 302 | + | uint(gdk.KEY_KP_4): 3, | |
| 303 | + | uint(gdk.KEY_5): 4, | |
| 304 | + | uint(gdk.KEY_KP_5): 4, | |
| 305 | + | uint(gdk.KEY_6): 5, | |
| 306 | + | uint(gdk.KEY_KP_6): 5, | |
| 307 | + | uint(gdk.KEY_7): 6, | |
| 308 | + | uint(gdk.KEY_KP_7): 6, | |
| 309 | + | uint(gdk.KEY_8): 7, | |
| 310 | + | uint(gdk.KEY_KP_8): 7, | |
| 311 | + | uint(gdk.KEY_9): 8, | |
| 312 | + | uint(gdk.KEY_KP_9): 8, | |
| 313 | + | } | |
| 314 | + | ||
| 315 | + | if idx, ok := keyMap[keyval]; ok { | |
| 316 | + | return idx, true | |
| 317 | + | } | |
| 318 | + | return 0, false | |
| 319 | + | } | |
| @@ -0,0 +1,319 @@ | |||||
| 1 | + | package component | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "sync" | |||
| 5 | + | ||||
| 6 | + | "github.com/bnema/dumber/internal/application/port" | |||
| 7 | + | "github.com/bnema/dumber/internal/ui/layout" | |||
| 8 | + | "github.com/jwijenbergh/puregotk/v4/gdk" | |||
| 9 | + | "github.com/jwijenbergh/puregotk/v4/gtk" | |||
| 10 | + | ) | |||
| 11 | + | ||||
| 12 | + | // Accent picker layout constants. | |||
| 13 | + | const ( | |||
| 14 | + | accentPickerSpacing = 8 // Spacing between accent items in pixels. | |||
| 15 | + | maxNumberedAccentItems = 9 // Maximum items that can be selected with number keys (1-9). | |||
| 16 | + | ) | |||
| 17 | + | ||||
| 18 | + | // AccentPicker displays an overlay with accent character options. | |||
| 19 | + | // The user can select an accent using: | |||
| 20 | + | // - Left/Right arrow keys to navigate | |||
| 21 | + | // - Number keys 1-9 to select directly | |||
| 22 | + | // - Enter to confirm selection | |||
| 23 | + | // - Escape to cancel | |||
| 24 | + | type AccentPicker struct { | |||
| 25 | + | factory layout.WidgetFactory | |||
| 26 | + | ||||
| 27 | + | // Widgets | |||
| 28 | + | container layout.BoxWidget // Outer container for positioning | |||
| 29 | + | accentLabels []layout.LabelWidget // Individual accent character labels | |||
| 30 | + | ||||
| 31 | + | // State | |||
| 32 | + | accents []rune | |||
| 33 | + | selectedIndex int | |||
| 34 | + | selectedCb func(rune) | |||
| 35 | + | cancelCb func() | |||
| 36 | + | visible bool | |||
| 37 | + | keyController *gtk.EventControllerKey | |||
| 38 | + | retainedClicked []interface{} // Keep click callbacks alive | |||
| 39 | + | ||||
| 40 | + | mu sync.Mutex | |||
| 41 | + | } | |||
| 42 | + | ||||
| 43 | + | // Compile-time interface check. | |||
| 44 | + | var _ port.AccentPickerUI = (*AccentPicker)(nil) | |||
| 45 | + | ||||
| 46 | + | // NewAccentPicker creates a new accent picker component. | |||
| 47 | + | func NewAccentPicker(factory layout.WidgetFactory) *AccentPicker { | |||
| 48 | + | // Create container box | |||
| 49 | + | container := factory.NewBox(layout.OrientationHorizontal, accentPickerSpacing) | |||
| 50 | + | container.AddCssClass("accent-picker") | |||
| 51 | + | ||||
| 52 | + | // Position at bottom-center | |||
| 53 | + | container.SetHalign(gtk.AlignCenterValue) | |||
| 54 | + | container.SetValign(gtk.AlignEndValue) | |||
| 55 | + | ||||
| 56 | + | // Don't expand | |||
| 57 | + | container.SetHexpand(false) | |||
| 58 | + | container.SetVexpand(false) | |||
| 59 | + | ||||
| 60 | + | // Make focusable so it can receive keyboard events | |||
| 61 | + | container.SetFocusable(true) | |||
| 62 | + | container.SetCanFocus(true) | |||
| 63 | + | ||||
| 64 | + | // Hidden by default | |||
| 65 | + | container.SetVisible(false) | |||
| 66 | + | ||||
| 67 | + | ap := &AccentPicker{ | |||
| 68 | + | factory: factory, | |||
| 69 | + | container: container, | |||
| 70 | + | selectedIndex: 0, | |||
| 71 | + | } | |||
| 72 | + | ||||
| 73 | + | // Set up key event handling | |||
| 74 | + | ap.setupKeyboardHandling() | |||
| 75 | + | ||||
| 76 | + | // Set up hover-to-focus: regain focus when mouse enters | |||
| 77 | + | ap.setupHoverFocus() | |||
| 78 | + | ||||
| 79 | + | return ap | |||
| 80 | + | } | |||
| 81 | + | ||||
| 82 | + | // Widget returns the accent picker's container widget. | |||
| 83 | + | func (ap *AccentPicker) Widget() layout.Widget { | |||
| 84 | + | return ap.container | |||
| 85 | + | } | |||
| 86 | + | ||||
| 87 | + | // Show displays the accent picker with the given accent options. | |||
| 88 | + | func (ap *AccentPicker) Show(accents []rune, selectedCb func(rune), cancelCb func()) { | |||
| 89 | + | ap.mu.Lock() | |||
| 90 | + | defer ap.mu.Unlock() | |||
| 91 | + | ||||
| 92 | + | ap.accents = accents | |||
| 93 | + | ap.selectedCb = selectedCb | |||
| 94 | + | ap.cancelCb = cancelCb | |||
| 95 | + | ap.selectedIndex = 0 | |||
| 96 | + | ap.visible = true | |||
| 97 | + | ||||
| 98 | + | // Clear existing labels | |||
| 99 | + | ap.clearLabels() | |||
| 100 | + | ||||
| 101 | + | // Create new labels for each accent | |||
| 102 | + | ap.accentLabels = make([]layout.LabelWidget, len(accents)) | |||
| 103 | + | ap.retainedClicked = make([]interface{}, len(accents)) | |||
| 104 | + | ||||
| 105 | + | for i, accent := range accents { | |||
| 106 | + | label := ap.factory.NewLabel(string(accent)) | |||
| 107 | + | label.AddCssClass("accent-picker-item") | |||
| 108 | + | ||||
| 109 | + | // Add number hint for first maxNumberedAccentItems items | |||
| 110 | + | if i < maxNumberedAccentItems { | |||
| 111 | + | label.AddCssClass("accent-picker-numbered") | |||
| 112 | + | } | |||
| 113 | + | ||||
| 114 | + | // Set up click handling | |||
| 115 | + | clickCtrl := gtk.NewGestureClick() | |||
| 116 | + | idx := i // Capture for closure | |||
| 117 | + | clickCb := func(_ gtk.GestureClick, _ int, _ float64, _ float64) { | |||
| 118 | + | ap.selectAccent(idx) | |||
| 119 | + | } | |||
| 120 | + | clickCtrl.ConnectPressed(&clickCb) | |||
| 121 | + | ap.retainedClicked[i] = clickCb | |||
| 122 | + | label.AddController(&clickCtrl.EventController) | |||
| 123 | + | ||||
| 124 | + | ap.accentLabels[i] = label | |||
| 125 | + | ap.container.Append(label) | |||
| 126 | + | } | |||
| 127 | + | ||||
| 128 | + | // Highlight first item | |||
| 129 | + | ap.updateSelection() | |||
| 130 | + | ||||
| 131 | + | ap.container.SetVisible(true) | |||
| 132 | + | ap.container.GrabFocus() | |||
| 133 | + | } | |||
| 134 | + | ||||
| 135 | + | // Hide hides the accent picker. | |||
| 136 | + | func (ap *AccentPicker) Hide() { | |||
| 137 | + | ap.mu.Lock() | |||
| 138 | + | defer ap.mu.Unlock() | |||
| 139 | + | ||||
| 140 | + | ap.visible = false | |||
| 141 | + | ap.container.SetVisible(false) | |||
| 142 | + | ap.clearLabels() | |||
| 143 | + | } | |||
| 144 | + | ||||
| 145 | + | // IsVisible returns true if the accent picker is currently visible. | |||
| 146 | + | func (ap *AccentPicker) IsVisible() bool { | |||
| 147 | + | ap.mu.Lock() | |||
| 148 | + | defer ap.mu.Unlock() | |||
| 149 | + | return ap.visible | |||
| 150 | + | } | |||
| 151 | + | ||||
| 152 | + | // clearLabels removes all accent labels from the container. | |||
| 153 | + | func (ap *AccentPicker) clearLabels() { | |||
| 154 | + | for _, label := range ap.accentLabels { | |||
| 155 | + | label.Unparent() | |||
| 156 | + | } | |||
| 157 | + | ap.accentLabels = nil | |||
| 158 | + | ap.retainedClicked = nil | |||
| 159 | + | } | |||
| 160 | + | ||||
| 161 | + | // updateSelection updates the visual selection highlight. | |||
| 162 | + | func (ap *AccentPicker) updateSelection() { | |||
| 163 | + | for i, label := range ap.accentLabels { | |||
| 164 | + | if i == ap.selectedIndex { | |||
| 165 | + | label.AddCssClass("accent-picker-selected") | |||
| 166 | + | } else { | |||
| 167 | + | label.RemoveCssClass("accent-picker-selected") | |||
| 168 | + | } | |||
| 169 | + | } | |||
| 170 | + | } | |||
| 171 | + | ||||
| 172 | + | // selectAccent selects the accent at the given index. | |||
| 173 | + | func (ap *AccentPicker) selectAccent(index int) { | |||
| 174 | + | ap.mu.Lock() | |||
| 175 | + | if index < 0 || index >= len(ap.accents) || ap.selectedCb == nil { | |||
| 176 | + | ap.mu.Unlock() | |||
| 177 | + | return | |||
| 178 | + | } | |||
| 179 | + | accent := ap.accents[index] | |||
| 180 | + | // Invoke callback while holding lock to avoid race with Hide() clearing callbacks | |||
| 181 | + | ap.selectedCb(accent) | |||
| 182 | + | ap.mu.Unlock() | |||
| 183 | + | } | |||
| 184 | + | ||||
| 185 | + | // cancel cancels the accent picker. | |||
| 186 | + | func (ap *AccentPicker) cancel() { | |||
| 187 | + | ap.mu.Lock() | |||
| 188 | + | if ap.cancelCb == nil { | |||
| 189 | + | ap.mu.Unlock() | |||
| 190 | + | return | |||
| 191 | + | } | |||
| 192 | + | // Invoke callback while holding lock to avoid race with Hide() clearing callbacks | |||
| 193 | + | ap.cancelCb() | |||
| 194 | + | ap.mu.Unlock() | |||
| 195 | + | } | |||
| 196 | + | ||||
| 197 | + | // setupKeyboardHandling sets up key event handling for navigation. | |||
| 198 | + | func (ap *AccentPicker) setupKeyboardHandling() { | |||
| 199 | + | keyCtrl := gtk.NewEventControllerKey() | |||
| 200 | + | ||||
| 201 | + | keyPressedCb := func(_ gtk.EventControllerKey, keyval uint, _ uint, _ gdk.ModifierType) bool { | |||
| 202 | + | return ap.handleKeyPress(keyval) | |||
| 203 | + | } | |||
| 204 | + | ||||
| 205 | + | keyCtrl.ConnectKeyPressed(&keyPressedCb) | |||
| 206 | + | ap.keyController = keyCtrl | |||
| 207 | + | ap.container.AddController(&keyCtrl.EventController) | |||
| 208 | + | } | |||
| 209 | + | ||||
| 210 | + | // setupHoverFocus sets up mouse hover to regain focus. | |||
| 211 | + | // This ensures the picker regains focus when the mouse enters, | |||
| 212 | + | // so keyboard navigation (including Escape) continues to work. | |||
| 213 | + | func (ap *AccentPicker) setupHoverFocus() { | |||
| 214 | + | motionCtrl := gtk.NewEventControllerMotion() | |||
| 215 | + | ||||
| 216 | + | enterCb := func(_ gtk.EventControllerMotion, _ float64, _ float64) { | |||
| 217 | + | ap.mu.Lock() | |||
| 218 | + | visible := ap.visible | |||
| 219 | + | ap.mu.Unlock() | |||
| 220 | + | ||||
| 221 | + | if visible { | |||
| 222 | + | ap.container.GrabFocus() | |||
| 223 | + | } | |||
| 224 | + | } | |||
| 225 | + | ||||
| 226 | + | motionCtrl.ConnectEnter(&enterCb) | |||
| 227 | + | ap.container.AddController(&motionCtrl.EventController) | |||
| 228 | + | } | |||
| 229 | + | ||||
| 230 | + | // handleKeyPress processes a key press event. | |||
| 231 | + | // Returns true if the key was handled. | |||
| 232 | + | func (ap *AccentPicker) handleKeyPress(keyval uint) bool { | |||
| 233 | + | ap.mu.Lock() | |||
| 234 | + | if !ap.visible { | |||
| 235 | + | ap.mu.Unlock() | |||
| 236 | + | return false | |||
| 237 | + | } | |||
| 238 | + | ||||
| 239 | + | // Handle navigation keys | |||
| 240 | + | if handled, shouldReturn := ap.handleNavigationKey(keyval); shouldReturn { | |||
| 241 | + | return handled | |||
| 242 | + | } | |||
| 243 | + | ||||
| 244 | + | // Handle number keys for direct selection | |||
| 245 | + | if idx, ok := ap.numberKeyToIndex(keyval); ok { | |||
| 246 | + | ap.mu.Unlock() | |||
| 247 | + | ap.selectAccent(idx) | |||
| 248 | + | return true | |||
| 249 | + | } | |||
| 250 | + | ||||
| 251 | + | ap.mu.Unlock() | |||
| 252 | + | return false | |||
| 253 | + | } | |||
| 254 | + | ||||
| 255 | + | // handleNavigationKey handles arrow keys, Enter, and Escape. | |||
| 256 | + | // Returns (handled, shouldReturn) - if shouldReturn is true, caller should return handled. | |||
| 257 | + | func (ap *AccentPicker) handleNavigationKey(keyval uint) (bool, bool) { | |||
| 258 | + | switch keyval { | |||
| 259 | + | case uint(gdk.KEY_Left): | |||
| 260 | + | if ap.selectedIndex > 0 { | |||
| 261 | + | ap.selectedIndex-- | |||
| 262 | + | ap.updateSelection() | |||
| 263 | + | } | |||
| 264 | + | ap.mu.Unlock() | |||
| 265 | + | return true, true | |||
| 266 | + | ||||
| 267 | + | case uint(gdk.KEY_Right): | |||
| 268 | + | if ap.selectedIndex < len(ap.accents)-1 { | |||
| 269 | + | ap.selectedIndex++ | |||
| 270 | + | ap.updateSelection() | |||
| 271 | + | } | |||
| 272 | + | ap.mu.Unlock() | |||
| 273 | + | return true, true | |||
| 274 | + | ||||
| 275 | + | case uint(gdk.KEY_Return), uint(gdk.KEY_KP_Enter): | |||
| 276 | + | idx := ap.selectedIndex | |||
| 277 | + | ap.mu.Unlock() | |||
| 278 | + | ap.selectAccent(idx) | |||
| 279 | + | return true, true | |||
| 280 | + | ||||
| 281 | + | case uint(gdk.KEY_Escape): | |||
| 282 | + | ap.mu.Unlock() | |||
| 283 | + | ap.cancel() | |||
| 284 | + | return true, true | |||
| 285 | + | ||||
| 286 | + | default: | |||
| 287 | + | return false, false | |||
| 288 | + | } | |||
| 289 | + | } | |||
| 290 | + | ||||
| 291 | + | // numberKeyToIndex converts a number key (1-9) to an accent index. | |||
| 292 | + | // Returns the index and true if the key is a valid number key. | |||
| 293 | + | func (ap *AccentPicker) numberKeyToIndex(keyval uint) (int, bool) { | |||
| 294 | + | keyMap := map[uint]int{ | |||
| 295 | + | uint(gdk.KEY_1): 0, | |||
| 296 | + | uint(gdk.KEY_KP_1): 0, | |||
| 297 | + | uint(gdk.KEY_2): 1, | |||
| 298 | + | uint(gdk.KEY_KP_2): 1, | |||
| 299 | + | uint(gdk.KEY_3): 2, | |||
| 300 | + | uint(gdk.KEY_KP_3): 2, | |||
| 301 | + | uint(gdk.KEY_4): 3, | |||
| 302 | + | uint(gdk.KEY_KP_4): 3, | |||
| 303 | + | uint(gdk.KEY_5): 4, | |||
| 304 | + | uint(gdk.KEY_KP_5): 4, | |||
| 305 | + | uint(gdk.KEY_6): 5, | |||
| 306 | + | uint(gdk.KEY_KP_6): 5, | |||
| 307 | + | uint(gdk.KEY_7): 6, | |||
| 308 | + | uint(gdk.KEY_KP_7): 6, | |||
| 309 | + | uint(gdk.KEY_8): 7, | |||
| 310 | + | uint(gdk.KEY_KP_8): 7, | |||
| 311 | + | uint(gdk.KEY_9): 8, | |||
| 312 | + | uint(gdk.KEY_KP_9): 8, | |||
| 313 | + | } | |||
| 314 | + | ||||
| 315 | + | if idx, ok := keyMap[keyval]; ok { | |||
| 316 | + | return idx, true | |||
| 317 | + | } | |||
| 318 | + | return 0, false | |||
| 319 | + | } | |||
M internal/ui/component/find_bar.go +43 -4
| @@ -39,16 +39,19 @@ type FindBar struct { | |||
| 39 | 39 | ||
| 40 | 40 | uc *usecase.FindInPageUseCase | |
| 41 | 41 | ||
| 42 | - | visible bool | |
| 43 | - | mu sync.RWMutex | |
| 44 | - | ctx context.Context | |
| 45 | - | onClose func() | |
| 42 | + | visible bool | |
| 43 | + | mu sync.RWMutex | |
| 44 | + | ctx context.Context | |
| 45 | + | onClose func() | |
| 46 | + | retainedCallbacks []interface{} // Keep callbacks alive for GC | |
| 46 | 47 | } | |
| 47 | 48 | ||
| 48 | 49 | // FindBarConfig holds configuration for creating a FindBar. | |
| 49 | 50 | type FindBarConfig struct { | |
| 50 | 51 | OnClose func() | |
| 51 | 52 | GetFindController func(paneID entity.PaneID) port.FindController | |
| 53 | + | OnFocusIn func(entry *gtk.SearchEntry) // Callback when entry gains focus (for accent picker) | |
| 54 | + | OnFocusOut func() // Callback when entry loses focus | |
| 52 | 55 | } | |
| 53 | 56 | ||
| 54 | 57 | // NewFindBar creates a new FindBar component. | |
| @@ -68,6 +71,7 @@ func NewFindBar(ctx context.Context, cfg FindBarConfig) *FindBar { | |||
| 68 | 71 | ||
| 69 | 72 | fb.setupHandlers() | |
| 70 | 73 | fb.bindUseCase() | |
| 74 | + | fb.setupFocusCallbacks(cfg.OnFocusIn, cfg.OnFocusOut) | |
| 71 | 75 | ||
| 72 | 76 | log.Debug().Msg("find bar created") | |
| 73 | 77 | return fb | |
| @@ -405,3 +409,38 @@ func (fb *FindBar) bindUseCase() { | |||
| 405 | 409 | glib.IdleAdd(&cb, 0) | |
| 406 | 410 | }) | |
| 407 | 411 | } | |
| 412 | + | ||
| 413 | + | // setupFocusCallbacks wires focus in/out callbacks for accent picker integration. | |
| 414 | + | func (fb *FindBar) setupFocusCallbacks(onFocusIn func(*gtk.SearchEntry), onFocusOut func()) { | |
| 415 | + | if onFocusIn == nil && onFocusOut == nil { | |
| 416 | + | return | |
| 417 | + | } | |
| 418 | + | ||
| 419 | + | if fb.entry == nil { | |
| 420 | + | return | |
| 421 | + | } | |
| 422 | + | ||
| 423 | + | focusController := gtk.NewEventControllerFocus() | |
| 424 | + | if focusController == nil { | |
| 425 | + | return | |
| 426 | + | } | |
| 427 | + | ||
| 428 | + | if onFocusIn != nil { | |
| 429 | + | entry := fb.entry // Capture for closure | |
| 430 | + | focusInCb := func(_ gtk.EventControllerFocus) { | |
| 431 | + | onFocusIn(entry) | |
| 432 | + | } | |
| 433 | + | fb.retainedCallbacks = append(fb.retainedCallbacks, focusInCb) | |
| 434 | + | focusController.ConnectEnter(&focusInCb) | |
| 435 | + | } | |
| 436 | + | ||
| 437 | + | if onFocusOut != nil { | |
| 438 | + | focusOutCb := func(_ gtk.EventControllerFocus) { | |
| 439 | + | onFocusOut() | |
| 440 | + | } | |
| 441 | + | fb.retainedCallbacks = append(fb.retainedCallbacks, focusOutCb) | |
| 442 | + | focusController.ConnectLeave(&focusOutCb) | |
| 443 | + | } | |
| 444 | + | ||
| 445 | + | fb.entry.AddController(&focusController.EventController) | |
| 446 | + | } | |
| @@ -39,16 +39,19 @@ type FindBar struct { | |||||
| 39 | 39 | ||||
| 40 | uc *usecase.FindInPageUseCase | 40 | uc *usecase.FindInPageUseCase | ||
| 41 | 41 | ||||
| 42 | - | visible bool | 42 | + | visible bool |
| 43 | - | mu sync.RWMutex | 43 | + | mu sync.RWMutex |
| 44 | - | ctx context.Context | 44 | + | ctx context.Context |
| 45 | - | onClose func() | 45 | + | onClose func() |
| 46 | + | retainedCallbacks []interface{} // Keep callbacks alive for GC | |||
| 46 | } | 47 | } | ||
| 47 | 48 | ||||
| 48 | // FindBarConfig holds configuration for creating a FindBar. | 49 | // FindBarConfig holds configuration for creating a FindBar. | ||
| 49 | type FindBarConfig struct { | 50 | type FindBarConfig struct { | ||
| 50 | OnClose func() | 51 | OnClose func() | ||
| 51 | GetFindController func(paneID entity.PaneID) port.FindController | 52 | GetFindController func(paneID entity.PaneID) port.FindController | ||
| 53 | + | OnFocusIn func(entry *gtk.SearchEntry) // Callback when entry gains focus (for accent picker) | |||
| 54 | + | OnFocusOut func() // Callback when entry loses focus | |||
| 52 | } | 55 | } | ||
| 53 | 56 | ||||
| 54 | // NewFindBar creates a new FindBar component. | 57 | // NewFindBar creates a new FindBar component. | ||
| @@ -68,6 +71,7 @@ func NewFindBar(ctx context.Context, cfg FindBarConfig) *FindBar { | |||||
| 68 | 71 | ||||
| 69 | fb.setupHandlers() | 72 | fb.setupHandlers() | ||
| 70 | fb.bindUseCase() | 73 | fb.bindUseCase() | ||
| 74 | + | fb.setupFocusCallbacks(cfg.OnFocusIn, cfg.OnFocusOut) | |||
| 71 | 75 | ||||
| 72 | log.Debug().Msg("find bar created") | 76 | log.Debug().Msg("find bar created") | ||
| 73 | return fb | 77 | return fb | ||
| @@ -405,3 +409,38 @@ func (fb *FindBar) bindUseCase() { | |||||
| 405 | glib.IdleAdd(&cb, 0) | 409 | glib.IdleAdd(&cb, 0) | ||
| 406 | }) | 410 | }) | ||
| 407 | } | 411 | } | ||
| 412 | + | ||||
| 413 | + | // setupFocusCallbacks wires focus in/out callbacks for accent picker integration. | |||
| 414 | + | func (fb *FindBar) setupFocusCallbacks(onFocusIn func(*gtk.SearchEntry), onFocusOut func()) { | |||
| 415 | + | if onFocusIn == nil && onFocusOut == nil { | |||
| 416 | + | return | |||
| 417 | + | } | |||
| 418 | + | ||||
| 419 | + | if fb.entry == nil { | |||
| 420 | + | return | |||
| 421 | + | } | |||
| 422 | + | ||||
| 423 | + | focusController := gtk.NewEventControllerFocus() | |||
| 424 | + | if focusController == nil { | |||
| 425 | + | return | |||
| 426 | + | } | |||
| 427 | + | ||||
| 428 | + | if onFocusIn != nil { | |||
| 429 | + | entry := fb.entry // Capture for closure | |||
| 430 | + | focusInCb := func(_ gtk.EventControllerFocus) { | |||
| 431 | + | onFocusIn(entry) | |||
| 432 | + | } | |||
| 433 | + | fb.retainedCallbacks = append(fb.retainedCallbacks, focusInCb) | |||
| 434 | + | focusController.ConnectEnter(&focusInCb) | |||
| 435 | + | } | |||
| 436 | + | ||||
| 437 | + | if onFocusOut != nil { | |||
| 438 | + | focusOutCb := func(_ gtk.EventControllerFocus) { | |||
| 439 | + | onFocusOut() | |||
| 440 | + | } | |||
| 441 | + | fb.retainedCallbacks = append(fb.retainedCallbacks, focusOutCb) | |||
| 442 | + | focusController.ConnectLeave(&focusOutCb) | |||
| 443 | + | } | |||
| 444 | + | ||||
| 445 | + | fb.entry.AddController(&focusController.EventController) | |||
| 446 | + | } | |||
M internal/ui/component/omnibox.go +34 -0
| @@ -132,6 +132,8 @@ type OmniboxConfig struct { | |||
| 132 | 132 | UIScale float64 // UI scale for favicon sizing | |
| 133 | 133 | OnNavigate func(url string) // Callback when user navigates via omnibox | |
| 134 | 134 | OnToast func(ctx context.Context, message string, level ToastLevel) // Callback to show toast notification | |
| 135 | + | OnFocusIn func(entry *gtk.SearchEntry) // Callback when entry gains focus (for accent picker) | |
| 136 | + | OnFocusOut func() // Callback when entry loses focus | |
| 135 | 137 | } | |
| 136 | 138 | ||
| 137 | 139 | // NewOmnibox creates a new native GTK4 omnibox widget. | |
| @@ -166,6 +168,7 @@ func NewOmnibox(ctx context.Context, cfg OmniboxConfig) *Omnibox { | |||
| 166 | 168 | ||
| 167 | 169 | o.setupKeyboardHandling() | |
| 168 | 170 | o.setupEntryChanged() | |
| 171 | + | o.setupFocusCallbacks(cfg.OnFocusIn, cfg.OnFocusOut) | |
| 169 | 172 | ||
| 170 | 173 | // Set navigation callback if provided | |
| 171 | 174 | if cfg.OnNavigate != nil { | |
| @@ -459,6 +462,37 @@ func (o *Omnibox) setupEntryChanged() { | |||
| 459 | 462 | o.entry.ConnectSearchChanged(&changedCb) | |
| 460 | 463 | } | |
| 461 | 464 | ||
| 465 | + | // setupFocusCallbacks wires focus in/out callbacks for accent picker integration. | |
| 466 | + | func (o *Omnibox) setupFocusCallbacks(onFocusIn func(*gtk.SearchEntry), onFocusOut func()) { | |
| 467 | + | if onFocusIn == nil && onFocusOut == nil { | |
| 468 | + | return | |
| 469 | + | } | |
| 470 | + | ||
| 471 | + | focusController := gtk.NewEventControllerFocus() | |
| 472 | + | if focusController == nil { | |
| 473 | + | return | |
| 474 | + | } | |
| 475 | + | ||
| 476 | + | if onFocusIn != nil { | |
| 477 | + | entry := o.entry // Capture for closure | |
| 478 | + | focusInCb := func(_ gtk.EventControllerFocus) { | |
| 479 | + | onFocusIn(entry) | |
| 480 | + | } | |
| 481 | + | o.retainedCallbacks = append(o.retainedCallbacks, focusInCb) | |
| 482 | + | focusController.ConnectEnter(&focusInCb) | |
| 483 | + | } | |
| 484 | + | ||
| 485 | + | if onFocusOut != nil { | |
| 486 | + | focusOutCb := func(_ gtk.EventControllerFocus) { | |
| 487 | + | onFocusOut() | |
| 488 | + | } | |
| 489 | + | o.retainedCallbacks = append(o.retainedCallbacks, focusOutCb) | |
| 490 | + | focusController.ConnectLeave(&focusOutCb) | |
| 491 | + | } | |
| 492 | + | ||
| 493 | + | o.entry.AddController(&focusController.EventController) | |
| 494 | + | } | |
| 495 | + | ||
| 462 | 496 | // handleKeyPress processes keyboard events. | |
| 463 | 497 | // Returns true if the event was handled. | |
| 464 | 498 | func (o *Omnibox) handleKeyPress(keyval, keycode uint, state gdk.ModifierType) bool { | |
| @@ -132,6 +132,8 @@ type OmniboxConfig struct { | |||||
| 132 | UIScale float64 // UI scale for favicon sizing | 132 | UIScale float64 // UI scale for favicon sizing | ||
| 133 | OnNavigate func(url string) // Callback when user navigates via omnibox | 133 | OnNavigate func(url string) // Callback when user navigates via omnibox | ||
| 134 | OnToast func(ctx context.Context, message string, level ToastLevel) // Callback to show toast notification | 134 | OnToast func(ctx context.Context, message string, level ToastLevel) // Callback to show toast notification | ||
| 135 | + | OnFocusIn func(entry *gtk.SearchEntry) // Callback when entry gains focus (for accent picker) | |||
| 136 | + | OnFocusOut func() // Callback when entry loses focus | |||
| 135 | } | 137 | } | ||
| 136 | 138 | ||||
| 137 | // NewOmnibox creates a new native GTK4 omnibox widget. | 139 | // NewOmnibox creates a new native GTK4 omnibox widget. | ||
| @@ -166,6 +168,7 @@ func NewOmnibox(ctx context.Context, cfg OmniboxConfig) *Omnibox { | |||||
| 166 | 168 | ||||
| 167 | o.setupKeyboardHandling() | 169 | o.setupKeyboardHandling() | ||
| 168 | o.setupEntryChanged() | 170 | o.setupEntryChanged() | ||
| 171 | + | o.setupFocusCallbacks(cfg.OnFocusIn, cfg.OnFocusOut) | |||
| 169 | 172 | ||||
| 170 | // Set navigation callback if provided | 173 | // Set navigation callback if provided | ||
| 171 | if cfg.OnNavigate != nil { | 174 | if cfg.OnNavigate != nil { | ||
| @@ -459,6 +462,37 @@ func (o *Omnibox) setupEntryChanged() { | |||||
| 459 | o.entry.ConnectSearchChanged(&changedCb) | 462 | o.entry.ConnectSearchChanged(&changedCb) | ||
| 460 | } | 463 | } | ||
| 461 | 464 | ||||
| 465 | + | // setupFocusCallbacks wires focus in/out callbacks for accent picker integration. | |||
| 466 | + | func (o *Omnibox) setupFocusCallbacks(onFocusIn func(*gtk.SearchEntry), onFocusOut func()) { | |||
| 467 | + | if onFocusIn == nil && onFocusOut == nil { | |||
| 468 | + | return | |||
| 469 | + | } | |||
| 470 | + | ||||
| 471 | + | focusController := gtk.NewEventControllerFocus() | |||
| 472 | + | if focusController == nil { | |||
| 473 | + | return | |||
| 474 | + | } | |||
| 475 | + | ||||
| 476 | + | if onFocusIn != nil { | |||
| 477 | + | entry := o.entry // Capture for closure | |||
| 478 | + | focusInCb := func(_ gtk.EventControllerFocus) { | |||
| 479 | + | onFocusIn(entry) | |||
| 480 | + | } | |||
| 481 | + | o.retainedCallbacks = append(o.retainedCallbacks, focusInCb) | |||
| 482 | + | focusController.ConnectEnter(&focusInCb) | |||
| 483 | + | } | |||
| 484 | + | ||||
| 485 | + | if onFocusOut != nil { | |||
| 486 | + | focusOutCb := func(_ gtk.EventControllerFocus) { | |||
| 487 | + | onFocusOut() | |||
| 488 | + | } | |||
| 489 | + | o.retainedCallbacks = append(o.retainedCallbacks, focusOutCb) | |||
| 490 | + | focusController.ConnectLeave(&focusOutCb) | |||
| 491 | + | } | |||
| 492 | + | ||||
| 493 | + | o.entry.AddController(&focusController.EventController) | |||
| 494 | + | } | |||
| 495 | + | ||||
| 462 | // handleKeyPress processes keyboard events. | 496 | // handleKeyPress processes keyboard events. | ||
| 463 | // Returns true if the event was handled. | 497 | // Returns true if the event was handled. | ||
| 464 | func (o *Omnibox) handleKeyPress(keyval, keycode uint, state gdk.ModifierType) bool { | 498 | func (o *Omnibox) handleKeyPress(keyval, keycode uint, state gdk.ModifierType) bool { | ||
M internal/ui/coordinator/content.go +15 -7
| @@ -61,13 +61,13 @@ type ContentCoordinator struct { | |||
| 61 | 61 | revealMu sync.Mutex | |
| 62 | 62 | pendingReveal map[entity.PaneID]bool | |
| 63 | 63 | ||
| 64 | - | appearanceMu sync.Mutex | |
| 65 | - | pendingScriptRefresh map[entity.PaneID]bool | |
| 66 | - | pendingThemePanes map[entity.PaneID]bool | |
| 67 | - | pendingThemeUpdate pendingThemeUpdate | |
| 68 | - | hasPendingThemeUpdate bool | |
| 69 | - | currentTheme pendingThemeUpdate | |
| 70 | - | hasCurrentTheme bool | |
| 64 | + | appearanceMu sync.Mutex | |
| 65 | + | pendingScriptRefresh map[entity.PaneID]bool | |
| 66 | + | pendingThemePanes map[entity.PaneID]bool | |
| 67 | + | pendingThemeUpdate pendingThemeUpdate | |
| 68 | + | hasPendingThemeUpdate bool | |
| 69 | + | currentTheme pendingThemeUpdate | |
| 70 | + | hasCurrentTheme bool | |
| 71 | 71 | ||
| 72 | 72 | // Gesture action handler for mouse button navigation | |
| 73 | 73 | gestureActionHandler input.ActionHandler | |
| @@ -92,6 +92,9 @@ type ContentCoordinator struct { | |||
| 92 | 92 | ||
| 93 | 93 | // Callback when fullscreen state changes (for hiding/showing tab bar) | |
| 94 | 94 | onFullscreenChanged func(entering bool) | |
| 95 | + | ||
| 96 | + | // Callback when WebView gains focus (for accent picker text input targeting) | |
| 97 | + | onWebViewFocused func(paneID entity.PaneID, wv *webkit.WebView) | |
| 95 | 98 | } | |
| 96 | 99 | ||
| 97 | 100 | type pendingThemeUpdate struct { | |
| @@ -165,6 +168,11 @@ func (c *ContentCoordinator) SetOnFullscreenChanged(fn func(entering bool)) { | |||
| 165 | 168 | c.onFullscreenChanged = fn | |
| 166 | 169 | } | |
| 167 | 170 | ||
| 171 | + | // SetOnWebViewFocused sets the callback for when a WebView gains focus. | |
| 172 | + | func (c *ContentCoordinator) SetOnWebViewFocused(fn func(paneID entity.PaneID, wv *webkit.WebView)) { | |
| 173 | + | c.onWebViewFocused = fn | |
| 174 | + | } | |
| 175 | + | ||
| 168 | 176 | // EnsureWebView acquires or reuses a WebView for the given pane. | |
| 169 | 177 | func (c *ContentCoordinator) EnsureWebView(ctx context.Context, paneID entity.PaneID) (*webkit.WebView, error) { | |
| 170 | 178 | log := logging.FromContext(ctx) | |
| @@ -61,13 +61,13 @@ type ContentCoordinator struct { | |||||
| 61 | revealMu sync.Mutex | 61 | revealMu sync.Mutex | ||
| 62 | pendingReveal map[entity.PaneID]bool | 62 | pendingReveal map[entity.PaneID]bool | ||
| 63 | 63 | ||||
| 64 | - | appearanceMu sync.Mutex | 64 | + | appearanceMu sync.Mutex |
| 65 | - | pendingScriptRefresh map[entity.PaneID]bool | 65 | + | pendingScriptRefresh map[entity.PaneID]bool |
| 66 | - | pendingThemePanes map[entity.PaneID]bool | 66 | + | pendingThemePanes map[entity.PaneID]bool |
| 67 | - | pendingThemeUpdate pendingThemeUpdate | 67 | + | pendingThemeUpdate pendingThemeUpdate |
| 68 | - | hasPendingThemeUpdate bool | 68 | + | hasPendingThemeUpdate bool |
| 69 | - | currentTheme pendingThemeUpdate | 69 | + | currentTheme pendingThemeUpdate |
| 70 | - | hasCurrentTheme bool | 70 | + | hasCurrentTheme bool |
| 71 | 71 | ||||
| 72 | // Gesture action handler for mouse button navigation | 72 | // Gesture action handler for mouse button navigation | ||
| 73 | gestureActionHandler input.ActionHandler | 73 | gestureActionHandler input.ActionHandler | ||
| @@ -92,6 +92,9 @@ type ContentCoordinator struct { | |||||
| 92 | 92 | ||||
| 93 | // Callback when fullscreen state changes (for hiding/showing tab bar) | 93 | // Callback when fullscreen state changes (for hiding/showing tab bar) | ||
| 94 | onFullscreenChanged func(entering bool) | 94 | onFullscreenChanged func(entering bool) | ||
| 95 | + | ||||
| 96 | + | // Callback when WebView gains focus (for accent picker text input targeting) | |||
| 97 | + | onWebViewFocused func(paneID entity.PaneID, wv *webkit.WebView) | |||
| 95 | } | 98 | } | ||
| 96 | 99 | ||||
| 97 | type pendingThemeUpdate struct { | 100 | type pendingThemeUpdate struct { | ||
| @@ -165,6 +168,11 @@ func (c *ContentCoordinator) SetOnFullscreenChanged(fn func(entering bool)) { | |||||
| 165 | c.onFullscreenChanged = fn | 168 | c.onFullscreenChanged = fn | ||
| 166 | } | 169 | } | ||
| 167 | 170 | ||||
| 171 | + | // SetOnWebViewFocused sets the callback for when a WebView gains focus. | |||
| 172 | + | func (c *ContentCoordinator) SetOnWebViewFocused(fn func(paneID entity.PaneID, wv *webkit.WebView)) { | |||
| 173 | + | c.onWebViewFocused = fn | |||
| 174 | + | } | |||
| 175 | + | ||||
| 168 | // EnsureWebView acquires or reuses a WebView for the given pane. | 176 | // EnsureWebView acquires or reuses a WebView for the given pane. | ||
| 169 | func (c *ContentCoordinator) EnsureWebView(ctx context.Context, paneID entity.PaneID) (*webkit.WebView, error) { | 177 | func (c *ContentCoordinator) EnsureWebView(ctx context.Context, paneID entity.PaneID) (*webkit.WebView, error) { | ||
| 170 | log := logging.FromContext(ctx) | 178 | log := logging.FromContext(ctx) | ||
M internal/ui/deps.go +3 -0
| @@ -60,6 +60,9 @@ type Dependencies struct { | |||
| 60 | 60 | FilterManager *filtering.Manager | |
| 61 | 61 | IdleInhibitor port.IdleInhibitor | |
| 62 | 62 | ||
| 63 | + | // Accent picker for dead keys support | |
| 64 | + | InsertAccentUC *usecase.InsertAccentUseCase | |
| 65 | + | ||
| 63 | 66 | // Session management | |
| 64 | 67 | SessionStateRepo repository.SessionStateRepository | |
| 65 | 68 | SessionRepo repository.SessionRepository | |
| @@ -60,6 +60,9 @@ type Dependencies struct { | |||||
| 60 | FilterManager *filtering.Manager | 60 | FilterManager *filtering.Manager | ||
| 61 | IdleInhibitor port.IdleInhibitor | 61 | IdleInhibitor port.IdleInhibitor | ||
| 62 | 62 | ||||
| 63 | + | // Accent picker for dead keys support | |||
| 64 | + | InsertAccentUC *usecase.InsertAccentUseCase | |||
| 65 | + | ||||
| 63 | // Session management | 66 | // Session management | ||
| 64 | SessionStateRepo repository.SessionStateRepository | 67 | SessionStateRepo repository.SessionStateRepository | ||
| 65 | SessionRepo repository.SessionRepository | 68 | SessionRepo repository.SessionRepository | ||
M internal/ui/input/keyboard.go +108 -26
| @@ -17,6 +17,20 @@ import ( | |||
| 17 | 17 | // Return an error if the action fails. | |
| 18 | 18 | type ActionHandler func(ctx context.Context, action Action) error | |
| 19 | 19 | ||
| 20 | + | // AccentHandler handles long-press accent detection. | |
| 21 | + | // Called for character keys that may have accent variants. | |
| 22 | + | type AccentHandler interface { | |
| 23 | + | // OnKeyPressed is called when a key is pressed. | |
| 24 | + | // Returns true if the accent handler is handling this key (picker visible or will show). | |
| 25 | + | OnKeyPressed(ctx context.Context, char rune, shiftHeld bool) bool | |
| 26 | + | // OnKeyReleased is called when a key is released. | |
| 27 | + | OnKeyReleased(ctx context.Context, char rune) | |
| 28 | + | // IsPickerVisible returns true if the accent picker is currently visible. | |
| 29 | + | IsPickerVisible() bool | |
| 30 | + | // Cancel cancels any pending long-press detection. | |
| 31 | + | Cancel(ctx context.Context) | |
| 32 | + | } | |
| 33 | + | ||
| 20 | 34 | // KeyboardHandler processes keyboard events and dispatches actions. | |
| 21 | 35 | // It manages modal input modes and routes key events to the appropriate handlers. | |
| 22 | 36 | type KeyboardHandler struct { | |
| @@ -28,12 +42,15 @@ type KeyboardHandler struct { | |||
| 28 | 42 | onAction ActionHandler | |
| 29 | 43 | // Optional bypass check (e.g., omnibox visible) | |
| 30 | 44 | shouldBypass func() bool | |
| 45 | + | // Optional accent handler for dead keys support | |
| 46 | + | accentHandler AccentHandler | |
| 31 | 47 | ||
| 32 | 48 | // GTK controller (nil until attached) | |
| 33 | 49 | controller *gtk.EventControllerKey | |
| 34 | 50 | ||
| 35 | 51 | // Callback retention: must stay reachable by Go GC. | |
| 36 | - | keyPressedCb func(gtk.EventControllerKey, uint, uint, gdk.ModifierType) bool | |
| 52 | + | keyPressedCb func(gtk.EventControllerKey, uint, uint, gdk.ModifierType) bool | |
| 53 | + | keyReleasedCb func(gtk.EventControllerKey, uint, uint, gdk.ModifierType) | |
| 37 | 54 | ||
| 38 | 55 | ctx context.Context | |
| 39 | 56 | mu sync.RWMutex | |
| @@ -75,6 +92,13 @@ func (h *KeyboardHandler) SetShouldBypassInput(fn func() bool) { | |||
| 75 | 92 | h.shouldBypass = fn | |
| 76 | 93 | } | |
| 77 | 94 | ||
| 95 | + | // SetAccentHandler sets the handler for long-press accent detection. | |
| 96 | + | func (h *KeyboardHandler) SetAccentHandler(handler AccentHandler) { | |
| 97 | + | h.mu.Lock() | |
| 98 | + | defer h.mu.Unlock() | |
| 99 | + | h.accentHandler = handler | |
| 100 | + | } | |
| 101 | + | ||
| 78 | 102 | // Mode returns the current input mode. | |
| 79 | 103 | func (h *KeyboardHandler) Mode() Mode { | |
| 80 | 104 | return h.modal.Mode() | |
| @@ -106,6 +130,12 @@ func (h *KeyboardHandler) AttachTo(window *gtk.ApplicationWindow) { | |||
| 106 | 130 | } | |
| 107 | 131 | h.controller.ConnectKeyPressed(&h.keyPressedCb) | |
| 108 | 132 | ||
| 133 | + | // Connect key released handler for accent detection. | |
| 134 | + | h.keyReleasedCb = func(_ gtk.EventControllerKey, keyval uint, _ uint, _ gdk.ModifierType) { | |
| 135 | + | h.handleKeyRelease(keyval) | |
| 136 | + | } | |
| 137 | + | h.controller.ConnectKeyReleased(&h.keyReleasedCb) | |
| 138 | + | ||
| 109 | 139 | // Add controller to window | |
| 110 | 140 | window.AddController(&h.controller.EventController) | |
| 111 | 141 | ||
| @@ -118,6 +148,7 @@ func (h *KeyboardHandler) AttachTo(window *gtk.ApplicationWindow) { | |||
| 118 | 148 | func (h *KeyboardHandler) Detach() { | |
| 119 | 149 | h.controller = nil | |
| 120 | 150 | h.keyPressedCb = nil | |
| 151 | + | h.keyReleasedCb = nil | |
| 121 | 152 | } | |
| 122 | 153 | ||
| 123 | 154 | // handleKeyPress processes a key press event. | |
| @@ -131,45 +162,71 @@ func (h *KeyboardHandler) handleKeyPress(keyval, keycode uint, state gdk.Modifie | |||
| 131 | 162 | ||
| 132 | 163 | h.mu.RLock() | |
| 133 | 164 | shouldBypass := h.shouldBypass | |
| 165 | + | accentHandler := h.accentHandler | |
| 134 | 166 | h.mu.RUnlock() | |
| 135 | - | if shouldBypass != nil && shouldBypass() { | |
| 136 | - | log.Debug().Uint("keyval", keyval).Uint("keycode", keycode).Uint("state", uint(state)).Msg("keyboard handler bypassed") | |
| 137 | - | return false | |
| 167 | + | ||
| 168 | + | // Check if accent picker is visible - it takes priority | |
| 169 | + | if accentHandler != nil && accentHandler.IsPickerVisible() { | |
| 170 | + | return false // Let the accent picker handle the key via its own controller | |
| 138 | 171 | } | |
| 139 | 172 | ||
| 140 | - | // Build KeyBinding from event | |
| 141 | 173 | modifiers := Modifier(state) & modifierMask | |
| 142 | 174 | ||
| 143 | - | // GTK reports shifted letters as uppercase keyvals (e.g. Shift+M -> gdk.KEY_M). | |
| 144 | - | // Normalize ASCII A-Z to lowercase so bindings can consistently use lowercase | |
| 145 | - | // keyvals and rely on ModShift to represent Shift. | |
| 146 | - | if keyval >= uint('A') && keyval <= uint('Z') { | |
| 147 | - | keyval += (uint('a') - uint('A')) | |
| 175 | + | // Check accent detection first - works in both normal and bypass mode | |
| 176 | + | // This enables accent picker for omnibox (GTK SearchEntry) as well as WebView | |
| 177 | + | if h.tryAccentDetection(accentHandler, keyval, modifiers) { | |
| 178 | + | return true | |
| 148 | 179 | } | |
| 149 | 180 | ||
| 150 | - | binding := KeyBinding{ | |
| 151 | - | Keyval: keyval, | |
| 152 | - | Modifiers: modifiers, | |
| 181 | + | if shouldBypass != nil && shouldBypass() { | |
| 182 | + | log.Debug().Uint("keyval", keyval).Uint("keycode", keycode).Uint("state", uint(state)).Msg("keyboard handler bypassed") | |
| 183 | + | return false | |
| 153 | 184 | } | |
| 154 | 185 | ||
| 155 | - | mode := h.modal.Mode() | |
| 186 | + | // Normalize uppercase letters for consistent binding lookup | |
| 187 | + | keyval = normalizeKeyval(keyval) | |
| 156 | 188 | ||
| 189 | + | binding := KeyBinding{Keyval: keyval, Modifiers: modifiers} | |
| 190 | + | mode := h.modal.Mode() | |
| 157 | 191 | action, found := h.lookupAction(log, binding, mode, modifiers, keycode) | |
| 158 | 192 | ||
| 159 | 193 | if !found { | |
| 160 | - | if mode != ModeNormal { | |
| 161 | - | // In modal mode, consume unrecognized keys to prevent WebView handling | |
| 162 | - | return true | |
| 163 | - | } | |
| 164 | - | // In normal mode, let the key pass through to WebView | |
| 194 | + | return mode != ModeNormal // Consume unrecognized keys in modal mode | |
| 195 | + | } | |
| 196 | + | ||
| 197 | + | return h.dispatchAction(action, mode) | |
| 198 | + | } | |
| 199 | + | ||
| 200 | + | // tryAccentDetection starts long-press detection for accent-eligible keys. | |
| 201 | + | // Returns true if the key should be suppressed (blocked from reaching the WebView). | |
| 202 | + | func (h *KeyboardHandler) tryAccentDetection(accentHandler AccentHandler, keyval uint, modifiers Modifier) bool { | |
| 203 | + | if h.modal.Mode() != ModeNormal || accentHandler == nil { | |
| 204 | + | return false | |
| 205 | + | } | |
| 206 | + | // Only consider keys without Ctrl/Alt modifiers (Shift is OK for uppercase) | |
| 207 | + | if modifiers != 0 && modifiers != ModShift { | |
| 165 | 208 | return false | |
| 166 | 209 | } | |
| 210 | + | if char := keyvalToRune(keyval); char != 0 { | |
| 211 | + | return accentHandler.OnKeyPressed(h.ctx, char, modifiers == ModShift) | |
| 212 | + | } | |
| 213 | + | return false | |
| 214 | + | } | |
| 215 | + | ||
| 216 | + | // normalizeKeyval converts uppercase A-Z to lowercase for consistent binding lookup. | |
| 217 | + | func normalizeKeyval(keyval uint) uint { | |
| 218 | + | if keyval >= uint('A') && keyval <= uint('Z') { | |
| 219 | + | return keyval + (uint('a') - uint('A')) | |
| 220 | + | } | |
| 221 | + | return keyval | |
| 222 | + | } | |
| 167 | 223 | ||
| 224 | + | // dispatchAction dispatches the action and handles mode-related logic. | |
| 225 | + | func (h *KeyboardHandler) dispatchAction(action Action, mode Mode) bool { | |
| 168 | 226 | if h.handleModeAction(action) { | |
| 169 | 227 | return true | |
| 170 | 228 | } | |
| 171 | 229 | ||
| 172 | - | // Dispatch action to handler | |
| 173 | 230 | h.mu.RLock() | |
| 174 | 231 | handler := h.onAction | |
| 175 | 232 | h.mu.RUnlock() | |
| @@ -177,19 +234,14 @@ func (h *KeyboardHandler) handleKeyPress(keyval, keycode uint, state gdk.Modifie | |||
| 177 | 234 | if handler != nil { | |
| 178 | 235 | if err := handler(h.ctx, action); err != nil { | |
| 179 | 236 | log := logging.FromContext(h.ctx) | |
| 180 | - | log.Error(). | |
| 181 | - | Err(err). | |
| 182 | - | Str("action", string(action)). | |
| 183 | - | Msg("action handler error") | |
| 237 | + | log.Error().Err(err).Str("action", string(action)).Msg("action handler error") | |
| 184 | 238 | } | |
| 185 | 239 | } | |
| 186 | 240 | ||
| 187 | - | // In resize mode, any resize keypress should extend the timeout. | |
| 188 | 241 | if mode == ModeResize && isResizeAction(action) { | |
| 189 | 242 | h.modal.ResetTimeout(h.ctx) | |
| 190 | 243 | } | |
| 191 | 244 | ||
| 192 | - | // Auto-exit modal mode after certain actions | |
| 193 | 245 | if mode != ModeNormal && ShouldAutoExitMode(action) { | |
| 194 | 246 | h.modal.ExitMode(h.ctx) | |
| 195 | 247 | } | |
| @@ -314,3 +366,33 @@ func (h *KeyboardHandler) EnterSessionMode() { | |||
| 314 | 366 | func (h *KeyboardHandler) ExitMode() { | |
| 315 | 367 | h.modal.ExitMode(h.ctx) | |
| 316 | 368 | } | |
| 369 | + | ||
| 370 | + | // handleKeyRelease processes a key release event for accent detection. | |
| 371 | + | func (h *KeyboardHandler) handleKeyRelease(keyval uint) { | |
| 372 | + | h.mu.RLock() | |
| 373 | + | accentHandler := h.accentHandler | |
| 374 | + | h.mu.RUnlock() | |
| 375 | + | ||
| 376 | + | if accentHandler == nil { | |
| 377 | + | return | |
| 378 | + | } | |
| 379 | + | ||
| 380 | + | // Convert keyval to rune for the accent handler | |
| 381 | + | if char := keyvalToRune(keyval); char != 0 { | |
| 382 | + | accentHandler.OnKeyReleased(h.ctx, char) | |
| 383 | + | } | |
| 384 | + | } | |
| 385 | + | ||
| 386 | + | // keyvalToRune converts a GTK keyval to a lowercase rune. | |
| 387 | + | // Returns 0 if the keyval is not a printable character with potential accents. | |
| 388 | + | func keyvalToRune(keyval uint) rune { | |
| 389 | + | // Handle lowercase a-z | |
| 390 | + | if keyval >= uint('a') && keyval <= uint('z') { | |
| 391 | + | return rune(keyval) | |
| 392 | + | } | |
| 393 | + | // Handle uppercase A-Z (convert to lowercase for accent lookup) | |
| 394 | + | if keyval >= uint('A') && keyval <= uint('Z') { | |
| 395 | + | return rune(keyval + ('a' - 'A')) | |
| 396 | + | } | |
| 397 | + | return 0 | |
| 398 | + | } | |
| @@ -17,6 +17,20 @@ import ( | |||||
| 17 | // Return an error if the action fails. | 17 | // Return an error if the action fails. | ||
| 18 | type ActionHandler func(ctx context.Context, action Action) error | 18 | type ActionHandler func(ctx context.Context, action Action) error | ||
| 19 | 19 | ||||
| 20 | + | // AccentHandler handles long-press accent detection. | |||
| 21 | + | // Called for character keys that may have accent variants. | |||
| 22 | + | type AccentHandler interface { | |||
| 23 | + | // OnKeyPressed is called when a key is pressed. | |||
| 24 | + | // Returns true if the accent handler is handling this key (picker visible or will show). | |||
| 25 | + | OnKeyPressed(ctx context.Context, char rune, shiftHeld bool) bool | |||
| 26 | + | // OnKeyReleased is called when a key is released. | |||
| 27 | + | OnKeyReleased(ctx context.Context, char rune) | |||
| 28 | + | // IsPickerVisible returns true if the accent picker is currently visible. | |||
| 29 | + | IsPickerVisible() bool | |||
| 30 | + | // Cancel cancels any pending long-press detection. | |||
| 31 | + | Cancel(ctx context.Context) | |||
| 32 | + | } | |||
| 33 | + | ||||
| 20 | // KeyboardHandler processes keyboard events and dispatches actions. | 34 | // KeyboardHandler processes keyboard events and dispatches actions. | ||
| 21 | // It manages modal input modes and routes key events to the appropriate handlers. | 35 | // It manages modal input modes and routes key events to the appropriate handlers. | ||
| 22 | type KeyboardHandler struct { | 36 | type KeyboardHandler struct { | ||
| @@ -28,12 +42,15 @@ type KeyboardHandler struct { | |||||
| 28 | onAction ActionHandler | 42 | onAction ActionHandler | ||
| 29 | // Optional bypass check (e.g., omnibox visible) | 43 | // Optional bypass check (e.g., omnibox visible) | ||
| 30 | shouldBypass func() bool | 44 | shouldBypass func() bool | ||
| 45 | + | // Optional accent handler for dead keys support | |||
| 46 | + | accentHandler AccentHandler | |||
| 31 | 47 | ||||
| 32 | // GTK controller (nil until attached) | 48 | // GTK controller (nil until attached) | ||
| 33 | controller *gtk.EventControllerKey | 49 | controller *gtk.EventControllerKey | ||
| 34 | 50 | ||||
| 35 | // Callback retention: must stay reachable by Go GC. | 51 | // Callback retention: must stay reachable by Go GC. | ||
| 36 | - | keyPressedCb func(gtk.EventControllerKey, uint, uint, gdk.ModifierType) bool | 52 | + | keyPressedCb func(gtk.EventControllerKey, uint, uint, gdk.ModifierType) bool |
| 53 | + | keyReleasedCb func(gtk.EventControllerKey, uint, uint, gdk.ModifierType) | |||
| 37 | 54 | ||||
| 38 | ctx context.Context | 55 | ctx context.Context | ||
| 39 | mu sync.RWMutex | 56 | mu sync.RWMutex | ||
| @@ -75,6 +92,13 @@ func (h *KeyboardHandler) SetShouldBypassInput(fn func() bool) { | |||||
| 75 | h.shouldBypass = fn | 92 | h.shouldBypass = fn | ||
| 76 | } | 93 | } | ||
| 77 | 94 | ||||
| 95 | + | // SetAccentHandler sets the handler for long-press accent detection. | |||
| 96 | + | func (h *KeyboardHandler) SetAccentHandler(handler AccentHandler) { | |||
| 97 | + | h.mu.Lock() | |||
| 98 | + | defer h.mu.Unlock() | |||
| 99 | + | h.accentHandler = handler | |||
| 100 | + | } | |||
| 101 | + | ||||
| 78 | // Mode returns the current input mode. | 102 | // Mode returns the current input mode. | ||
| 79 | func (h *KeyboardHandler) Mode() Mode { | 103 | func (h *KeyboardHandler) Mode() Mode { | ||
| 80 | return h.modal.Mode() | 104 | return h.modal.Mode() | ||
| @@ -106,6 +130,12 @@ func (h *KeyboardHandler) AttachTo(window *gtk.ApplicationWindow) { | |||||
| 106 | } | 130 | } | ||
| 107 | h.controller.ConnectKeyPressed(&h.keyPressedCb) | 131 | h.controller.ConnectKeyPressed(&h.keyPressedCb) | ||
| 108 | 132 | ||||
| 133 | + | // Connect key released handler for accent detection. | |||
| 134 | + | h.keyReleasedCb = func(_ gtk.EventControllerKey, keyval uint, _ uint, _ gdk.ModifierType) { | |||
| 135 | + | h.handleKeyRelease(keyval) | |||
| 136 | + | } | |||
| 137 | + | h.controller.ConnectKeyReleased(&h.keyReleasedCb) | |||
| 138 | + | ||||
| 109 | // Add controller to window | 139 | // Add controller to window | ||
| 110 | window.AddController(&h.controller.EventController) | 140 | window.AddController(&h.controller.EventController) | ||
| 111 | 141 | ||||
| @@ -118,6 +148,7 @@ func (h *KeyboardHandler) AttachTo(window *gtk.ApplicationWindow) { | |||||
| 118 | func (h *KeyboardHandler) Detach() { | 148 | func (h *KeyboardHandler) Detach() { | ||
| 119 | h.controller = nil | 149 | h.controller = nil | ||
| 120 | h.keyPressedCb = nil | 150 | h.keyPressedCb = nil | ||
| 151 | + | h.keyReleasedCb = nil | |||
| 121 | } | 152 | } | ||
| 122 | 153 | ||||
| 123 | // handleKeyPress processes a key press event. | 154 | // handleKeyPress processes a key press event. | ||
| @@ -131,45 +162,71 @@ func (h *KeyboardHandler) handleKeyPress(keyval, keycode uint, state gdk.Modifie | |||||
| 131 | 162 | ||||
| 132 | h.mu.RLock() | 163 | h.mu.RLock() | ||
| 133 | shouldBypass := h.shouldBypass | 164 | shouldBypass := h.shouldBypass | ||
| 165 | + | accentHandler := h.accentHandler | |||
| 134 | h.mu.RUnlock() | 166 | h.mu.RUnlock() | ||
| 135 | - | if shouldBypass != nil && shouldBypass() { | 167 | + | |
| 136 | - | log.Debug().Uint("keyval", keyval).Uint("keycode", keycode).Uint("state", uint(state)).Msg("keyboard handler bypassed") | 168 | + | // Check if accent picker is visible - it takes priority |
| 137 | - | return false | 169 | + | if accentHandler != nil && accentHandler.IsPickerVisible() { |
| 170 | + | return false // Let the accent picker handle the key via its own controller | |||
| 138 | } | 171 | } | ||
| 139 | 172 | ||||
| 140 | - | // Build KeyBinding from event | |||
| 141 | modifiers := Modifier(state) & modifierMask | 173 | modifiers := Modifier(state) & modifierMask | ||
| 142 | 174 | ||||
| 143 | - | // GTK reports shifted letters as uppercase keyvals (e.g. Shift+M -> gdk.KEY_M). | 175 | + | // Check accent detection first - works in both normal and bypass mode |
| 144 | - | // Normalize ASCII A-Z to lowercase so bindings can consistently use lowercase | 176 | + | // This enables accent picker for omnibox (GTK SearchEntry) as well as WebView |
| 145 | - | // keyvals and rely on ModShift to represent Shift. | 177 | + | if h.tryAccentDetection(accentHandler, keyval, modifiers) { |
| 146 | - | if keyval >= uint('A') && keyval <= uint('Z') { | 178 | + | return true |
| 147 | - | keyval += (uint('a') - uint('A')) | |||
| 148 | } | 179 | } | ||
| 149 | 180 | ||||
| 150 | - | binding := KeyBinding{ | 181 | + | if shouldBypass != nil && shouldBypass() { |
| 151 | - | Keyval: keyval, | 182 | + | log.Debug().Uint("keyval", keyval).Uint("keycode", keycode).Uint("state", uint(state)).Msg("keyboard handler bypassed") |
| 152 | - | Modifiers: modifiers, | 183 | + | return false |
| 153 | } | 184 | } | ||
| 154 | 185 | ||||
| 155 | - | mode := h.modal.Mode() | 186 | + | // Normalize uppercase letters for consistent binding lookup |
| 187 | + | keyval = normalizeKeyval(keyval) | |||
| 156 | 188 | ||||
| 189 | + | binding := KeyBinding{Keyval: keyval, Modifiers: modifiers} | |||
| 190 | + | mode := h.modal.Mode() | |||
| 157 | action, found := h.lookupAction(log, binding, mode, modifiers, keycode) | 191 | action, found := h.lookupAction(log, binding, mode, modifiers, keycode) | ||
| 158 | 192 | ||||
| 159 | if !found { | 193 | if !found { | ||
| 160 | - | if mode != ModeNormal { | 194 | + | return mode != ModeNormal // Consume unrecognized keys in modal mode |
| 161 | - | // In modal mode, consume unrecognized keys to prevent WebView handling | 195 | + | } |
| 162 | - | return true | 196 | + | |
| 163 | - | } | 197 | + | return h.dispatchAction(action, mode) |
| 164 | - | // In normal mode, let the key pass through to WebView | 198 | + | } |
| 199 | + | ||||
| 200 | + | // tryAccentDetection starts long-press detection for accent-eligible keys. | |||
| 201 | + | // Returns true if the key should be suppressed (blocked from reaching the WebView). | |||
| 202 | + | func (h *KeyboardHandler) tryAccentDetection(accentHandler AccentHandler, keyval uint, modifiers Modifier) bool { | |||
| 203 | + | if h.modal.Mode() != ModeNormal || accentHandler == nil { | |||
| 204 | + | return false | |||
| 205 | + | } | |||
| 206 | + | // Only consider keys without Ctrl/Alt modifiers (Shift is OK for uppercase) | |||
| 207 | + | if modifiers != 0 && modifiers != ModShift { | |||
| 165 | return false | 208 | return false | ||
| 166 | } | 209 | } | ||
| 210 | + | if char := keyvalToRune(keyval); char != 0 { | |||
| 211 | + | return accentHandler.OnKeyPressed(h.ctx, char, modifiers == ModShift) | |||
| 212 | + | } | |||
| 213 | + | return false | |||
| 214 | + | } | |||
| 215 | + | ||||
| 216 | + | // normalizeKeyval converts uppercase A-Z to lowercase for consistent binding lookup. | |||
| 217 | + | func normalizeKeyval(keyval uint) uint { | |||
| 218 | + | if keyval >= uint('A') && keyval <= uint('Z') { | |||
| 219 | + | return keyval + (uint('a') - uint('A')) | |||
| 220 | + | } | |||
| 221 | + | return keyval | |||
| 222 | + | } | |||
| 167 | 223 | ||||
| 224 | + | // dispatchAction dispatches the action and handles mode-related logic. | |||
| 225 | + | func (h *KeyboardHandler) dispatchAction(action Action, mode Mode) bool { | |||
| 168 | if h.handleModeAction(action) { | 226 | if h.handleModeAction(action) { | ||
| 169 | return true | 227 | return true | ||
| 170 | } | 228 | } | ||
| 171 | 229 | ||||
| 172 | - | // Dispatch action to handler | |||
| 173 | h.mu.RLock() | 230 | h.mu.RLock() | ||
| 174 | handler := h.onAction | 231 | handler := h.onAction | ||
| 175 | h.mu.RUnlock() | 232 | h.mu.RUnlock() | ||
| @@ -177,19 +234,14 @@ func (h *KeyboardHandler) handleKeyPress(keyval, keycode uint, state gdk.Modifie | |||||
| 177 | if handler != nil { | 234 | if handler != nil { | ||
| 178 | if err := handler(h.ctx, action); err != nil { | 235 | if err := handler(h.ctx, action); err != nil { | ||
| 179 | log := logging.FromContext(h.ctx) | 236 | log := logging.FromContext(h.ctx) | ||
| 180 | - | log.Error(). | 237 | + | log.Error().Err(err).Str("action", string(action)).Msg("action handler error") |
| 181 | - | Err(err). | |||
| 182 | - | Str("action", string(action)). | |||
| 183 | - | Msg("action handler error") | |||
| 184 | } | 238 | } | ||
| 185 | } | 239 | } | ||
| 186 | 240 | ||||
| 187 | - | // In resize mode, any resize keypress should extend the timeout. | |||
| 188 | if mode == ModeResize && isResizeAction(action) { | 241 | if mode == ModeResize && isResizeAction(action) { | ||
| 189 | h.modal.ResetTimeout(h.ctx) | 242 | h.modal.ResetTimeout(h.ctx) | ||
| 190 | } | 243 | } | ||
| 191 | 244 | ||||
| 192 | - | // Auto-exit modal mode after certain actions | |||
| 193 | if mode != ModeNormal && ShouldAutoExitMode(action) { | 245 | if mode != ModeNormal && ShouldAutoExitMode(action) { | ||
| 194 | h.modal.ExitMode(h.ctx) | 246 | h.modal.ExitMode(h.ctx) | ||
| 195 | } | 247 | } | ||
| @@ -314,3 +366,33 @@ func (h *KeyboardHandler) EnterSessionMode() { | |||||
| 314 | func (h *KeyboardHandler) ExitMode() { | 366 | func (h *KeyboardHandler) ExitMode() { | ||
| 315 | h.modal.ExitMode(h.ctx) | 367 | h.modal.ExitMode(h.ctx) | ||
| 316 | } | 368 | } | ||
| 369 | + | ||||
| 370 | + | // handleKeyRelease processes a key release event for accent detection. | |||
| 371 | + | func (h *KeyboardHandler) handleKeyRelease(keyval uint) { | |||
| 372 | + | h.mu.RLock() | |||
| 373 | + | accentHandler := h.accentHandler | |||
| 374 | + | h.mu.RUnlock() | |||
| 375 | + | ||||
| 376 | + | if accentHandler == nil { | |||
| 377 | + | return | |||
| 378 | + | } | |||
| 379 | + | ||||
| 380 | + | // Convert keyval to rune for the accent handler | |||
| 381 | + | if char := keyvalToRune(keyval); char != 0 { | |||
| 382 | + | accentHandler.OnKeyReleased(h.ctx, char) | |||
| 383 | + | } | |||
| 384 | + | } | |||
| 385 | + | ||||
| 386 | + | // keyvalToRune converts a GTK keyval to a lowercase rune. | |||
| 387 | + | // Returns 0 if the keyval is not a printable character with potential accents. | |||
| 388 | + | func keyvalToRune(keyval uint) rune { | |||
| 389 | + | // Handle lowercase a-z | |||
| 390 | + | if keyval >= uint('a') && keyval <= uint('z') { | |||
| 391 | + | return rune(keyval) | |||
| 392 | + | } | |||
| 393 | + | // Handle uppercase A-Z (convert to lowercase for accent lookup) | |||
| 394 | + | if keyval >= uint('A') && keyval <= uint('Z') { | |||
| 395 | + | return rune(keyval + ('a' - 'A')) | |||
| 396 | + | } | |||
| 397 | + | return 0 | |||
| 398 | + | } | |||
M internal/ui/layout/gtk_widgets.go +9 -0
| @@ -34,6 +34,7 @@ func (w *gtkWidget) IsVisible() bool { return w.inner.GetVisible() | |||
| 34 | 34 | func (w *gtkWidget) GrabFocus() bool { return w.inner.GrabFocus() } | |
| 35 | 35 | func (w *gtkWidget) HasFocus() bool { return w.inner.HasFocus() } | |
| 36 | 36 | func (w *gtkWidget) SetCanFocus(canFocus bool) { w.inner.SetCanFocus(canFocus) } | |
| 37 | + | func (w *gtkWidget) SetFocusable(focusable bool) { w.inner.SetFocusable(focusable) } | |
| 37 | 38 | func (w *gtkWidget) SetFocusOnClick(focus bool) { w.inner.SetFocusOnClick(focus) } | |
| 38 | 39 | func (w *gtkWidget) SetCanTarget(canTarget bool) { w.inner.SetCanTarget(canTarget) } | |
| 39 | 40 | func (w *gtkWidget) SetHexpand(expand bool) { w.inner.SetHexpand(expand) } | |
| @@ -89,6 +90,7 @@ func (p *gtkPaned) IsVisible() bool { return p.inner.GetVisible() | |||
| 89 | 90 | func (p *gtkPaned) GrabFocus() bool { return p.inner.GrabFocus() } | |
| 90 | 91 | func (p *gtkPaned) HasFocus() bool { return p.inner.HasFocus() } | |
| 91 | 92 | func (p *gtkPaned) SetCanFocus(canFocus bool) { p.inner.SetCanFocus(canFocus) } | |
| 93 | + | func (p *gtkPaned) SetFocusable(focusable bool) { p.inner.SetFocusable(focusable) } | |
| 92 | 94 | func (p *gtkPaned) SetFocusOnClick(focus bool) { p.inner.SetFocusOnClick(focus) } | |
| 93 | 95 | func (p *gtkPaned) SetCanTarget(canTarget bool) { p.inner.SetCanTarget(canTarget) } | |
| 94 | 96 | func (p *gtkPaned) SetHexpand(expand bool) { p.inner.SetHexpand(expand) } | |
| @@ -210,6 +212,7 @@ func (b *gtkBox) IsVisible() bool { return b.inner.GetVisible() } | |||
| 210 | 212 | func (b *gtkBox) GrabFocus() bool { return b.inner.GrabFocus() } | |
| 211 | 213 | func (b *gtkBox) HasFocus() bool { return b.inner.HasFocus() } | |
| 212 | 214 | func (b *gtkBox) SetCanFocus(canFocus bool) { b.inner.SetCanFocus(canFocus) } | |
| 215 | + | func (b *gtkBox) SetFocusable(focusable bool) { b.inner.SetFocusable(focusable) } | |
| 213 | 216 | func (b *gtkBox) SetFocusOnClick(focus bool) { b.inner.SetFocusOnClick(focus) } | |
| 214 | 217 | func (b *gtkBox) SetCanTarget(canTarget bool) { b.inner.SetCanTarget(canTarget) } | |
| 215 | 218 | func (b *gtkBox) SetHexpand(expand bool) { b.inner.SetHexpand(expand) } | |
| @@ -315,6 +318,7 @@ func (o *gtkOverlay) IsVisible() bool { return o.inner.GetVisible( | |||
| 315 | 318 | func (o *gtkOverlay) GrabFocus() bool { return o.inner.GrabFocus() } | |
| 316 | 319 | func (o *gtkOverlay) HasFocus() bool { return o.inner.HasFocus() } | |
| 317 | 320 | func (o *gtkOverlay) SetCanFocus(canFocus bool) { o.inner.SetCanFocus(canFocus) } | |
| 321 | + | func (o *gtkOverlay) SetFocusable(focusable bool) { o.inner.SetFocusable(focusable) } | |
| 318 | 322 | func (o *gtkOverlay) SetFocusOnClick(focus bool) { o.inner.SetFocusOnClick(focus) } | |
| 319 | 323 | func (o *gtkOverlay) SetCanTarget(canTarget bool) { o.inner.SetCanTarget(canTarget) } | |
| 320 | 324 | func (o *gtkOverlay) SetHexpand(expand bool) { o.inner.SetHexpand(expand) } | |
| @@ -428,6 +432,7 @@ func (l *gtkLabel) IsVisible() bool { return l.inner.GetVisible() | |||
| 428 | 432 | func (l *gtkLabel) GrabFocus() bool { return l.inner.GrabFocus() } | |
| 429 | 433 | func (l *gtkLabel) HasFocus() bool { return l.inner.HasFocus() } | |
| 430 | 434 | func (l *gtkLabel) SetCanFocus(canFocus bool) { l.inner.SetCanFocus(canFocus) } | |
| 435 | + | func (l *gtkLabel) SetFocusable(focusable bool) { l.inner.SetFocusable(focusable) } | |
| 431 | 436 | func (l *gtkLabel) SetFocusOnClick(focus bool) { l.inner.SetFocusOnClick(focus) } | |
| 432 | 437 | func (l *gtkLabel) SetCanTarget(canTarget bool) { l.inner.SetCanTarget(canTarget) } | |
| 433 | 438 | func (l *gtkLabel) SetHexpand(expand bool) { l.inner.SetHexpand(expand) } | |
| @@ -490,6 +495,7 @@ func (btn *gtkButton) IsVisible() bool { return btn.inner.GetVisib | |||
| 490 | 495 | func (btn *gtkButton) GrabFocus() bool { return btn.inner.GrabFocus() } | |
| 491 | 496 | func (btn *gtkButton) HasFocus() bool { return btn.inner.HasFocus() } | |
| 492 | 497 | func (btn *gtkButton) SetCanFocus(canFocus bool) { btn.inner.SetCanFocus(canFocus) } | |
| 498 | + | func (btn *gtkButton) SetFocusable(focusable bool) { btn.inner.SetFocusable(focusable) } | |
| 493 | 499 | func (btn *gtkButton) SetFocusOnClick(focus bool) { btn.inner.SetFocusOnClick(focus) } | |
| 494 | 500 | func (btn *gtkButton) SetCanTarget(canTarget bool) { btn.inner.SetCanTarget(canTarget) } | |
| 495 | 501 | func (btn *gtkButton) SetHexpand(expand bool) { btn.inner.SetHexpand(expand) } | |
| @@ -571,6 +577,7 @@ func (img *gtkImage) IsVisible() bool { return img.inner.GetVisibl | |||
| 571 | 577 | func (img *gtkImage) GrabFocus() bool { return img.inner.GrabFocus() } | |
| 572 | 578 | func (img *gtkImage) HasFocus() bool { return img.inner.HasFocus() } | |
| 573 | 579 | func (img *gtkImage) SetCanFocus(canFocus bool) { img.inner.SetCanFocus(canFocus) } | |
| 580 | + | func (img *gtkImage) SetFocusable(focusable bool) { img.inner.SetFocusable(focusable) } | |
| 574 | 581 | func (img *gtkImage) SetFocusOnClick(focus bool) { img.inner.SetFocusOnClick(focus) } | |
| 575 | 582 | func (img *gtkImage) SetCanTarget(canTarget bool) { img.inner.SetCanTarget(canTarget) } | |
| 576 | 583 | func (img *gtkImage) SetHexpand(expand bool) { img.inner.SetHexpand(expand) } | |
| @@ -642,6 +649,7 @@ func (p *gtkProgressBar) IsVisible() bool { return p.inner.GetVisi | |||
| 642 | 649 | func (p *gtkProgressBar) GrabFocus() bool { return p.inner.GrabFocus() } | |
| 643 | 650 | func (p *gtkProgressBar) HasFocus() bool { return p.inner.HasFocus() } | |
| 644 | 651 | func (p *gtkProgressBar) SetCanFocus(canFocus bool) { p.inner.SetCanFocus(canFocus) } | |
| 652 | + | func (p *gtkProgressBar) SetFocusable(focusable bool) { p.inner.SetFocusable(focusable) } | |
| 645 | 653 | func (p *gtkProgressBar) SetFocusOnClick(focus bool) { p.inner.SetFocusOnClick(focus) } | |
| 646 | 654 | func (p *gtkProgressBar) SetCanTarget(canTarget bool) { p.inner.SetCanTarget(canTarget) } | |
| 647 | 655 | func (p *gtkProgressBar) SetHexpand(expand bool) { p.inner.SetHexpand(expand) } | |
| @@ -700,6 +708,7 @@ func (s *gtkSpinner) IsVisible() bool { return s.inner.GetVisible( | |||
| 700 | 708 | func (s *gtkSpinner) GrabFocus() bool { return s.inner.GrabFocus() } | |
| 701 | 709 | func (s *gtkSpinner) HasFocus() bool { return s.inner.HasFocus() } | |
| 702 | 710 | func (s *gtkSpinner) SetCanFocus(canFocus bool) { s.inner.SetCanFocus(canFocus) } | |
| 711 | + | func (s *gtkSpinner) SetFocusable(focusable bool) { s.inner.SetFocusable(focusable) } | |
| 703 | 712 | func (s *gtkSpinner) SetFocusOnClick(focus bool) { s.inner.SetFocusOnClick(focus) } | |
| 704 | 713 | func (s *gtkSpinner) SetCanTarget(canTarget bool) { s.inner.SetCanTarget(canTarget) } | |
| 705 | 714 | func (s *gtkSpinner) SetHexpand(expand bool) { s.inner.SetHexpand(expand) } | |
| @@ -34,6 +34,7 @@ func (w *gtkWidget) IsVisible() bool { return w.inner.GetVisible() | |||||
| 34 | func (w *gtkWidget) GrabFocus() bool { return w.inner.GrabFocus() } | 34 | func (w *gtkWidget) GrabFocus() bool { return w.inner.GrabFocus() } | ||
| 35 | func (w *gtkWidget) HasFocus() bool { return w.inner.HasFocus() } | 35 | func (w *gtkWidget) HasFocus() bool { return w.inner.HasFocus() } | ||
| 36 | func (w *gtkWidget) SetCanFocus(canFocus bool) { w.inner.SetCanFocus(canFocus) } | 36 | func (w *gtkWidget) SetCanFocus(canFocus bool) { w.inner.SetCanFocus(canFocus) } | ||
| 37 | + | func (w *gtkWidget) SetFocusable(focusable bool) { w.inner.SetFocusable(focusable) } | |||
| 37 | func (w *gtkWidget) SetFocusOnClick(focus bool) { w.inner.SetFocusOnClick(focus) } | 38 | func (w *gtkWidget) SetFocusOnClick(focus bool) { w.inner.SetFocusOnClick(focus) } | ||
| 38 | func (w *gtkWidget) SetCanTarget(canTarget bool) { w.inner.SetCanTarget(canTarget) } | 39 | func (w *gtkWidget) SetCanTarget(canTarget bool) { w.inner.SetCanTarget(canTarget) } | ||
| 39 | func (w *gtkWidget) SetHexpand(expand bool) { w.inner.SetHexpand(expand) } | 40 | func (w *gtkWidget) SetHexpand(expand bool) { w.inner.SetHexpand(expand) } | ||
| @@ -89,6 +90,7 @@ func (p *gtkPaned) IsVisible() bool { return p.inner.GetVisible() | |||||
| 89 | func (p *gtkPaned) GrabFocus() bool { return p.inner.GrabFocus() } | 90 | func (p *gtkPaned) GrabFocus() bool { return p.inner.GrabFocus() } | ||
| 90 | func (p *gtkPaned) HasFocus() bool { return p.inner.HasFocus() } | 91 | func (p *gtkPaned) HasFocus() bool { return p.inner.HasFocus() } | ||
| 91 | func (p *gtkPaned) SetCanFocus(canFocus bool) { p.inner.SetCanFocus(canFocus) } | 92 | func (p *gtkPaned) SetCanFocus(canFocus bool) { p.inner.SetCanFocus(canFocus) } | ||
| 93 | + | func (p *gtkPaned) SetFocusable(focusable bool) { p.inner.SetFocusable(focusable) } | |||
| 92 | func (p *gtkPaned) SetFocusOnClick(focus bool) { p.inner.SetFocusOnClick(focus) } | 94 | func (p *gtkPaned) SetFocusOnClick(focus bool) { p.inner.SetFocusOnClick(focus) } | ||
| 93 | func (p *gtkPaned) SetCanTarget(canTarget bool) { p.inner.SetCanTarget(canTarget) } | 95 | func (p *gtkPaned) SetCanTarget(canTarget bool) { p.inner.SetCanTarget(canTarget) } | ||
| 94 | func (p *gtkPaned) SetHexpand(expand bool) { p.inner.SetHexpand(expand) } | 96 | func (p *gtkPaned) SetHexpand(expand bool) { p.inner.SetHexpand(expand) } | ||
| @@ -210,6 +212,7 @@ func (b *gtkBox) IsVisible() bool { return b.inner.GetVisible() } | |||||
| 210 | func (b *gtkBox) GrabFocus() bool { return b.inner.GrabFocus() } | 212 | func (b *gtkBox) GrabFocus() bool { return b.inner.GrabFocus() } | ||
| 211 | func (b *gtkBox) HasFocus() bool { return b.inner.HasFocus() } | 213 | func (b *gtkBox) HasFocus() bool { return b.inner.HasFocus() } | ||
| 212 | func (b *gtkBox) SetCanFocus(canFocus bool) { b.inner.SetCanFocus(canFocus) } | 214 | func (b *gtkBox) SetCanFocus(canFocus bool) { b.inner.SetCanFocus(canFocus) } | ||
| 215 | + | func (b *gtkBox) SetFocusable(focusable bool) { b.inner.SetFocusable(focusable) } | |||
| 213 | func (b *gtkBox) SetFocusOnClick(focus bool) { b.inner.SetFocusOnClick(focus) } | 216 | func (b *gtkBox) SetFocusOnClick(focus bool) { b.inner.SetFocusOnClick(focus) } | ||
| 214 | func (b *gtkBox) SetCanTarget(canTarget bool) { b.inner.SetCanTarget(canTarget) } | 217 | func (b *gtkBox) SetCanTarget(canTarget bool) { b.inner.SetCanTarget(canTarget) } | ||
| 215 | func (b *gtkBox) SetHexpand(expand bool) { b.inner.SetHexpand(expand) } | 218 | func (b *gtkBox) SetHexpand(expand bool) { b.inner.SetHexpand(expand) } | ||
| @@ -315,6 +318,7 @@ func (o *gtkOverlay) IsVisible() bool { return o.inner.GetVisible( | |||||
| 315 | func (o *gtkOverlay) GrabFocus() bool { return o.inner.GrabFocus() } | 318 | func (o *gtkOverlay) GrabFocus() bool { return o.inner.GrabFocus() } | ||
| 316 | func (o *gtkOverlay) HasFocus() bool { return o.inner.HasFocus() } | 319 | func (o *gtkOverlay) HasFocus() bool { return o.inner.HasFocus() } | ||
| 317 | func (o *gtkOverlay) SetCanFocus(canFocus bool) { o.inner.SetCanFocus(canFocus) } | 320 | func (o *gtkOverlay) SetCanFocus(canFocus bool) { o.inner.SetCanFocus(canFocus) } | ||
| 321 | + | func (o *gtkOverlay) SetFocusable(focusable bool) { o.inner.SetFocusable(focusable) } | |||
| 318 | func (o *gtkOverlay) SetFocusOnClick(focus bool) { o.inner.SetFocusOnClick(focus) } | 322 | func (o *gtkOverlay) SetFocusOnClick(focus bool) { o.inner.SetFocusOnClick(focus) } | ||
| 319 | func (o *gtkOverlay) SetCanTarget(canTarget bool) { o.inner.SetCanTarget(canTarget) } | 323 | func (o *gtkOverlay) SetCanTarget(canTarget bool) { o.inner.SetCanTarget(canTarget) } | ||
| 320 | func (o *gtkOverlay) SetHexpand(expand bool) { o.inner.SetHexpand(expand) } | 324 | func (o *gtkOverlay) SetHexpand(expand bool) { o.inner.SetHexpand(expand) } | ||
| @@ -428,6 +432,7 @@ func (l *gtkLabel) IsVisible() bool { return l.inner.GetVisible() | |||||
| 428 | func (l *gtkLabel) GrabFocus() bool { return l.inner.GrabFocus() } | 432 | func (l *gtkLabel) GrabFocus() bool { return l.inner.GrabFocus() } | ||
| 429 | func (l *gtkLabel) HasFocus() bool { return l.inner.HasFocus() } | 433 | func (l *gtkLabel) HasFocus() bool { return l.inner.HasFocus() } | ||
| 430 | func (l *gtkLabel) SetCanFocus(canFocus bool) { l.inner.SetCanFocus(canFocus) } | 434 | func (l *gtkLabel) SetCanFocus(canFocus bool) { l.inner.SetCanFocus(canFocus) } | ||
| 435 | + | func (l *gtkLabel) SetFocusable(focusable bool) { l.inner.SetFocusable(focusable) } | |||
| 431 | func (l *gtkLabel) SetFocusOnClick(focus bool) { l.inner.SetFocusOnClick(focus) } | 436 | func (l *gtkLabel) SetFocusOnClick(focus bool) { l.inner.SetFocusOnClick(focus) } | ||
| 432 | func (l *gtkLabel) SetCanTarget(canTarget bool) { l.inner.SetCanTarget(canTarget) } | 437 | func (l *gtkLabel) SetCanTarget(canTarget bool) { l.inner.SetCanTarget(canTarget) } | ||
| 433 | func (l *gtkLabel) SetHexpand(expand bool) { l.inner.SetHexpand(expand) } | 438 | func (l *gtkLabel) SetHexpand(expand bool) { l.inner.SetHexpand(expand) } | ||
| @@ -490,6 +495,7 @@ func (btn *gtkButton) IsVisible() bool { return btn.inner.GetVisib | |||||
| 490 | func (btn *gtkButton) GrabFocus() bool { return btn.inner.GrabFocus() } | 495 | func (btn *gtkButton) GrabFocus() bool { return btn.inner.GrabFocus() } | ||
| 491 | func (btn *gtkButton) HasFocus() bool { return btn.inner.HasFocus() } | 496 | func (btn *gtkButton) HasFocus() bool { return btn.inner.HasFocus() } | ||
| 492 | func (btn *gtkButton) SetCanFocus(canFocus bool) { btn.inner.SetCanFocus(canFocus) } | 497 | func (btn *gtkButton) SetCanFocus(canFocus bool) { btn.inner.SetCanFocus(canFocus) } | ||
| 498 | + | func (btn *gtkButton) SetFocusable(focusable bool) { btn.inner.SetFocusable(focusable) } | |||
| 493 | func (btn *gtkButton) SetFocusOnClick(focus bool) { btn.inner.SetFocusOnClick(focus) } | 499 | func (btn *gtkButton) SetFocusOnClick(focus bool) { btn.inner.SetFocusOnClick(focus) } | ||
| 494 | func (btn *gtkButton) SetCanTarget(canTarget bool) { btn.inner.SetCanTarget(canTarget) } | 500 | func (btn *gtkButton) SetCanTarget(canTarget bool) { btn.inner.SetCanTarget(canTarget) } | ||
| 495 | func (btn *gtkButton) SetHexpand(expand bool) { btn.inner.SetHexpand(expand) } | 501 | func (btn *gtkButton) SetHexpand(expand bool) { btn.inner.SetHexpand(expand) } | ||
| @@ -571,6 +577,7 @@ func (img *gtkImage) IsVisible() bool { return img.inner.GetVisibl | |||||
| 571 | func (img *gtkImage) GrabFocus() bool { return img.inner.GrabFocus() } | 577 | func (img *gtkImage) GrabFocus() bool { return img.inner.GrabFocus() } | ||
| 572 | func (img *gtkImage) HasFocus() bool { return img.inner.HasFocus() } | 578 | func (img *gtkImage) HasFocus() bool { return img.inner.HasFocus() } | ||
| 573 | func (img *gtkImage) SetCanFocus(canFocus bool) { img.inner.SetCanFocus(canFocus) } | 579 | func (img *gtkImage) SetCanFocus(canFocus bool) { img.inner.SetCanFocus(canFocus) } | ||
| 580 | + | func (img *gtkImage) SetFocusable(focusable bool) { img.inner.SetFocusable(focusable) } | |||
| 574 | func (img *gtkImage) SetFocusOnClick(focus bool) { img.inner.SetFocusOnClick(focus) } | 581 | func (img *gtkImage) SetFocusOnClick(focus bool) { img.inner.SetFocusOnClick(focus) } | ||
| 575 | func (img *gtkImage) SetCanTarget(canTarget bool) { img.inner.SetCanTarget(canTarget) } | 582 | func (img *gtkImage) SetCanTarget(canTarget bool) { img.inner.SetCanTarget(canTarget) } | ||
| 576 | func (img *gtkImage) SetHexpand(expand bool) { img.inner.SetHexpand(expand) } | 583 | func (img *gtkImage) SetHexpand(expand bool) { img.inner.SetHexpand(expand) } | ||
| @@ -642,6 +649,7 @@ func (p *gtkProgressBar) IsVisible() bool { return p.inner.GetVisi | |||||
| 642 | func (p *gtkProgressBar) GrabFocus() bool { return p.inner.GrabFocus() } | 649 | func (p *gtkProgressBar) GrabFocus() bool { return p.inner.GrabFocus() } | ||
| 643 | func (p *gtkProgressBar) HasFocus() bool { return p.inner.HasFocus() } | 650 | func (p *gtkProgressBar) HasFocus() bool { return p.inner.HasFocus() } | ||
| 644 | func (p *gtkProgressBar) SetCanFocus(canFocus bool) { p.inner.SetCanFocus(canFocus) } | 651 | func (p *gtkProgressBar) SetCanFocus(canFocus bool) { p.inner.SetCanFocus(canFocus) } | ||
| 652 | + | func (p *gtkProgressBar) SetFocusable(focusable bool) { p.inner.SetFocusable(focusable) } | |||
| 645 | func (p *gtkProgressBar) SetFocusOnClick(focus bool) { p.inner.SetFocusOnClick(focus) } | 653 | func (p *gtkProgressBar) SetFocusOnClick(focus bool) { p.inner.SetFocusOnClick(focus) } | ||
| 646 | func (p *gtkProgressBar) SetCanTarget(canTarget bool) { p.inner.SetCanTarget(canTarget) } | 654 | func (p *gtkProgressBar) SetCanTarget(canTarget bool) { p.inner.SetCanTarget(canTarget) } | ||
| 647 | func (p *gtkProgressBar) SetHexpand(expand bool) { p.inner.SetHexpand(expand) } | 655 | func (p *gtkProgressBar) SetHexpand(expand bool) { p.inner.SetHexpand(expand) } | ||
| @@ -700,6 +708,7 @@ func (s *gtkSpinner) IsVisible() bool { return s.inner.GetVisible( | |||||
| 700 | func (s *gtkSpinner) GrabFocus() bool { return s.inner.GrabFocus() } | 708 | func (s *gtkSpinner) GrabFocus() bool { return s.inner.GrabFocus() } | ||
| 701 | func (s *gtkSpinner) HasFocus() bool { return s.inner.HasFocus() } | 709 | func (s *gtkSpinner) HasFocus() bool { return s.inner.HasFocus() } | ||
| 702 | func (s *gtkSpinner) SetCanFocus(canFocus bool) { s.inner.SetCanFocus(canFocus) } | 710 | func (s *gtkSpinner) SetCanFocus(canFocus bool) { s.inner.SetCanFocus(canFocus) } | ||
| 711 | + | func (s *gtkSpinner) SetFocusable(focusable bool) { s.inner.SetFocusable(focusable) } | |||
| 703 | func (s *gtkSpinner) SetFocusOnClick(focus bool) { s.inner.SetFocusOnClick(focus) } | 712 | func (s *gtkSpinner) SetFocusOnClick(focus bool) { s.inner.SetFocusOnClick(focus) } | ||
| 704 | func (s *gtkSpinner) SetCanTarget(canTarget bool) { s.inner.SetCanTarget(canTarget) } | 713 | func (s *gtkSpinner) SetCanTarget(canTarget bool) { s.inner.SetCanTarget(canTarget) } | ||
| 705 | func (s *gtkSpinner) SetHexpand(expand bool) { s.inner.SetHexpand(expand) } | 714 | func (s *gtkSpinner) SetHexpand(expand bool) { s.inner.SetHexpand(expand) } | ||
M internal/ui/layout/mocks/mocks.go +360 -0
| @@ -908,6 +908,46 @@ func (_c *MockWidget_SetFocusOnClick_Call) RunAndReturn(run func(focusOnClick bo | |||
| 908 | 908 | return _c | |
| 909 | 909 | } | |
| 910 | 910 | ||
| 911 | + | // SetFocusable provides a mock function for the type MockWidget | |
| 912 | + | func (_mock *MockWidget) SetFocusable(focusable bool) { | |
| 913 | + | _mock.Called(focusable) | |
| 914 | + | return | |
| 915 | + | } | |
| 916 | + | ||
| 917 | + | // MockWidget_SetFocusable_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetFocusable' | |
| 918 | + | type MockWidget_SetFocusable_Call struct { | |
| 919 | + | *mock.Call | |
| 920 | + | } | |
| 921 | + | ||
| 922 | + | // SetFocusable is a helper method to define mock.On call | |
| 923 | + | // - focusable bool | |
| 924 | + | func (_e *MockWidget_Expecter) SetFocusable(focusable interface{}) *MockWidget_SetFocusable_Call { | |
| 925 | + | return &MockWidget_SetFocusable_Call{Call: _e.mock.On("SetFocusable", focusable)} | |
| 926 | + | } | |
| 927 | + | ||
| 928 | + | func (_c *MockWidget_SetFocusable_Call) Run(run func(focusable bool)) *MockWidget_SetFocusable_Call { | |
| 929 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 930 | + | var arg0 bool | |
| 931 | + | if args[0] != nil { | |
| 932 | + | arg0 = args[0].(bool) | |
| 933 | + | } | |
| 934 | + | run( | |
| 935 | + | arg0, | |
| 936 | + | ) | |
| 937 | + | }) | |
| 938 | + | return _c | |
| 939 | + | } | |
| 940 | + | ||
| 941 | + | func (_c *MockWidget_SetFocusable_Call) Return() *MockWidget_SetFocusable_Call { | |
| 942 | + | _c.Call.Return() | |
| 943 | + | return _c | |
| 944 | + | } | |
| 945 | + | ||
| 946 | + | func (_c *MockWidget_SetFocusable_Call) RunAndReturn(run func(focusable bool)) *MockWidget_SetFocusable_Call { | |
| 947 | + | _c.Run(run) | |
| 948 | + | return _c | |
| 949 | + | } | |
| 950 | + | ||
| 911 | 951 | // SetHalign provides a mock function for the type MockWidget | |
| 912 | 952 | func (_mock *MockWidget) SetHalign(align gtk.Align) { | |
| 913 | 953 | _mock.Called(align) | |
| @@ -2586,6 +2626,46 @@ func (_c *MockPanedWidget_SetFocusOnClick_Call) RunAndReturn(run func(focusOnCli | |||
| 2586 | 2626 | return _c | |
| 2587 | 2627 | } | |
| 2588 | 2628 | ||
| 2629 | + | // SetFocusable provides a mock function for the type MockPanedWidget | |
| 2630 | + | func (_mock *MockPanedWidget) SetFocusable(focusable bool) { | |
| 2631 | + | _mock.Called(focusable) | |
| 2632 | + | return | |
| 2633 | + | } | |
| 2634 | + | ||
| 2635 | + | // MockPanedWidget_SetFocusable_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetFocusable' | |
| 2636 | + | type MockPanedWidget_SetFocusable_Call struct { | |
| 2637 | + | *mock.Call | |
| 2638 | + | } | |
| 2639 | + | ||
| 2640 | + | // SetFocusable is a helper method to define mock.On call | |
| 2641 | + | // - focusable bool | |
| 2642 | + | func (_e *MockPanedWidget_Expecter) SetFocusable(focusable interface{}) *MockPanedWidget_SetFocusable_Call { | |
| 2643 | + | return &MockPanedWidget_SetFocusable_Call{Call: _e.mock.On("SetFocusable", focusable)} | |
| 2644 | + | } | |
| 2645 | + | ||
| 2646 | + | func (_c *MockPanedWidget_SetFocusable_Call) Run(run func(focusable bool)) *MockPanedWidget_SetFocusable_Call { | |
| 2647 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 2648 | + | var arg0 bool | |
| 2649 | + | if args[0] != nil { | |
| 2650 | + | arg0 = args[0].(bool) | |
| 2651 | + | } | |
| 2652 | + | run( | |
| 2653 | + | arg0, | |
| 2654 | + | ) | |
| 2655 | + | }) | |
| 2656 | + | return _c | |
| 2657 | + | } | |
| 2658 | + | ||
| 2659 | + | func (_c *MockPanedWidget_SetFocusable_Call) Return() *MockPanedWidget_SetFocusable_Call { | |
| 2660 | + | _c.Call.Return() | |
| 2661 | + | return _c | |
| 2662 | + | } | |
| 2663 | + | ||
| 2664 | + | func (_c *MockPanedWidget_SetFocusable_Call) RunAndReturn(run func(focusable bool)) *MockPanedWidget_SetFocusable_Call { | |
| 2665 | + | _c.Run(run) | |
| 2666 | + | return _c | |
| 2667 | + | } | |
| 2668 | + | ||
| 2589 | 2669 | // SetHalign provides a mock function for the type MockPanedWidget | |
| 2590 | 2670 | func (_mock *MockPanedWidget) SetHalign(align gtk.Align) { | |
| 2591 | 2671 | _mock.Called(align) | |
| @@ -4339,6 +4419,46 @@ func (_c *MockBoxWidget_SetFocusOnClick_Call) RunAndReturn(run func(focusOnClick | |||
| 4339 | 4419 | return _c | |
| 4340 | 4420 | } | |
| 4341 | 4421 | ||
| 4422 | + | // SetFocusable provides a mock function for the type MockBoxWidget | |
| 4423 | + | func (_mock *MockBoxWidget) SetFocusable(focusable bool) { | |
| 4424 | + | _mock.Called(focusable) | |
| 4425 | + | return | |
| 4426 | + | } | |
| 4427 | + | ||
| 4428 | + | // MockBoxWidget_SetFocusable_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetFocusable' | |
| 4429 | + | type MockBoxWidget_SetFocusable_Call struct { | |
| 4430 | + | *mock.Call | |
| 4431 | + | } | |
| 4432 | + | ||
| 4433 | + | // SetFocusable is a helper method to define mock.On call | |
| 4434 | + | // - focusable bool | |
| 4435 | + | func (_e *MockBoxWidget_Expecter) SetFocusable(focusable interface{}) *MockBoxWidget_SetFocusable_Call { | |
| 4436 | + | return &MockBoxWidget_SetFocusable_Call{Call: _e.mock.On("SetFocusable", focusable)} | |
| 4437 | + | } | |
| 4438 | + | ||
| 4439 | + | func (_c *MockBoxWidget_SetFocusable_Call) Run(run func(focusable bool)) *MockBoxWidget_SetFocusable_Call { | |
| 4440 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 4441 | + | var arg0 bool | |
| 4442 | + | if args[0] != nil { | |
| 4443 | + | arg0 = args[0].(bool) | |
| 4444 | + | } | |
| 4445 | + | run( | |
| 4446 | + | arg0, | |
| 4447 | + | ) | |
| 4448 | + | }) | |
| 4449 | + | return _c | |
| 4450 | + | } | |
| 4451 | + | ||
| 4452 | + | func (_c *MockBoxWidget_SetFocusable_Call) Return() *MockBoxWidget_SetFocusable_Call { | |
| 4453 | + | _c.Call.Return() | |
| 4454 | + | return _c | |
| 4455 | + | } | |
| 4456 | + | ||
| 4457 | + | func (_c *MockBoxWidget_SetFocusable_Call) RunAndReturn(run func(focusable bool)) *MockBoxWidget_SetFocusable_Call { | |
| 4458 | + | _c.Run(run) | |
| 4459 | + | return _c | |
| 4460 | + | } | |
| 4461 | + | ||
| 4342 | 4462 | // SetHalign provides a mock function for the type MockBoxWidget | |
| 4343 | 4463 | func (_mock *MockBoxWidget) SetHalign(align gtk.Align) { | |
| 4344 | 4464 | _mock.Called(align) | |
| @@ -5902,6 +6022,46 @@ func (_c *MockOverlayWidget_SetFocusOnClick_Call) RunAndReturn(run func(focusOnC | |||
| 5902 | 6022 | return _c | |
| 5903 | 6023 | } | |
| 5904 | 6024 | ||
| 6025 | + | // SetFocusable provides a mock function for the type MockOverlayWidget | |
| 6026 | + | func (_mock *MockOverlayWidget) SetFocusable(focusable bool) { | |
| 6027 | + | _mock.Called(focusable) | |
| 6028 | + | return | |
| 6029 | + | } | |
| 6030 | + | ||
| 6031 | + | // MockOverlayWidget_SetFocusable_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetFocusable' | |
| 6032 | + | type MockOverlayWidget_SetFocusable_Call struct { | |
| 6033 | + | *mock.Call | |
| 6034 | + | } | |
| 6035 | + | ||
| 6036 | + | // SetFocusable is a helper method to define mock.On call | |
| 6037 | + | // - focusable bool | |
| 6038 | + | func (_e *MockOverlayWidget_Expecter) SetFocusable(focusable interface{}) *MockOverlayWidget_SetFocusable_Call { | |
| 6039 | + | return &MockOverlayWidget_SetFocusable_Call{Call: _e.mock.On("SetFocusable", focusable)} | |
| 6040 | + | } | |
| 6041 | + | ||
| 6042 | + | func (_c *MockOverlayWidget_SetFocusable_Call) Run(run func(focusable bool)) *MockOverlayWidget_SetFocusable_Call { | |
| 6043 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 6044 | + | var arg0 bool | |
| 6045 | + | if args[0] != nil { | |
| 6046 | + | arg0 = args[0].(bool) | |
| 6047 | + | } | |
| 6048 | + | run( | |
| 6049 | + | arg0, | |
| 6050 | + | ) | |
| 6051 | + | }) | |
| 6052 | + | return _c | |
| 6053 | + | } | |
| 6054 | + | ||
| 6055 | + | func (_c *MockOverlayWidget_SetFocusable_Call) Return() *MockOverlayWidget_SetFocusable_Call { | |
| 6056 | + | _c.Call.Return() | |
| 6057 | + | return _c | |
| 6058 | + | } | |
| 6059 | + | ||
| 6060 | + | func (_c *MockOverlayWidget_SetFocusable_Call) RunAndReturn(run func(focusable bool)) *MockOverlayWidget_SetFocusable_Call { | |
| 6061 | + | _c.Run(run) | |
| 6062 | + | return _c | |
| 6063 | + | } | |
| 6064 | + | ||
| 5905 | 6065 | // SetHalign provides a mock function for the type MockOverlayWidget | |
| 5906 | 6066 | func (_mock *MockOverlayWidget) SetHalign(align gtk.Align) { | |
| 5907 | 6067 | _mock.Called(align) | |
| @@ -7161,6 +7321,46 @@ func (_c *MockLabelWidget_SetFocusOnClick_Call) RunAndReturn(run func(focusOnCli | |||
| 7161 | 7321 | return _c | |
| 7162 | 7322 | } | |
| 7163 | 7323 | ||
| 7324 | + | // SetFocusable provides a mock function for the type MockLabelWidget | |
| 7325 | + | func (_mock *MockLabelWidget) SetFocusable(focusable bool) { | |
| 7326 | + | _mock.Called(focusable) | |
| 7327 | + | return | |
| 7328 | + | } | |
| 7329 | + | ||
| 7330 | + | // MockLabelWidget_SetFocusable_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetFocusable' | |
| 7331 | + | type MockLabelWidget_SetFocusable_Call struct { | |
| 7332 | + | *mock.Call | |
| 7333 | + | } | |
| 7334 | + | ||
| 7335 | + | // SetFocusable is a helper method to define mock.On call | |
| 7336 | + | // - focusable bool | |
| 7337 | + | func (_e *MockLabelWidget_Expecter) SetFocusable(focusable interface{}) *MockLabelWidget_SetFocusable_Call { | |
| 7338 | + | return &MockLabelWidget_SetFocusable_Call{Call: _e.mock.On("SetFocusable", focusable)} | |
| 7339 | + | } | |
| 7340 | + | ||
| 7341 | + | func (_c *MockLabelWidget_SetFocusable_Call) Run(run func(focusable bool)) *MockLabelWidget_SetFocusable_Call { | |
| 7342 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 7343 | + | var arg0 bool | |
| 7344 | + | if args[0] != nil { | |
| 7345 | + | arg0 = args[0].(bool) | |
| 7346 | + | } | |
| 7347 | + | run( | |
| 7348 | + | arg0, | |
| 7349 | + | ) | |
| 7350 | + | }) | |
| 7351 | + | return _c | |
| 7352 | + | } | |
| 7353 | + | ||
| 7354 | + | func (_c *MockLabelWidget_SetFocusable_Call) Return() *MockLabelWidget_SetFocusable_Call { | |
| 7355 | + | _c.Call.Return() | |
| 7356 | + | return _c | |
| 7357 | + | } | |
| 7358 | + | ||
| 7359 | + | func (_c *MockLabelWidget_SetFocusable_Call) RunAndReturn(run func(focusable bool)) *MockLabelWidget_SetFocusable_Call { | |
| 7360 | + | _c.Run(run) | |
| 7361 | + | return _c | |
| 7362 | + | } | |
| 7363 | + | ||
| 7164 | 7364 | // SetHalign provides a mock function for the type MockLabelWidget | |
| 7165 | 7365 | func (_mock *MockLabelWidget) SetHalign(align gtk.Align) { | |
| 7166 | 7366 | _mock.Called(align) | |
| @@ -8631,6 +8831,46 @@ func (_c *MockButtonWidget_SetFocusOnClick_Call) RunAndReturn(run func(focusOnCl | |||
| 8631 | 8831 | return _c | |
| 8632 | 8832 | } | |
| 8633 | 8833 | ||
| 8834 | + | // SetFocusable provides a mock function for the type MockButtonWidget | |
| 8835 | + | func (_mock *MockButtonWidget) SetFocusable(focusable bool) { | |
| 8836 | + | _mock.Called(focusable) | |
| 8837 | + | return | |
| 8838 | + | } | |
| 8839 | + | ||
| 8840 | + | // MockButtonWidget_SetFocusable_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetFocusable' | |
| 8841 | + | type MockButtonWidget_SetFocusable_Call struct { | |
| 8842 | + | *mock.Call | |
| 8843 | + | } | |
| 8844 | + | ||
| 8845 | + | // SetFocusable is a helper method to define mock.On call | |
| 8846 | + | // - focusable bool | |
| 8847 | + | func (_e *MockButtonWidget_Expecter) SetFocusable(focusable interface{}) *MockButtonWidget_SetFocusable_Call { | |
| 8848 | + | return &MockButtonWidget_SetFocusable_Call{Call: _e.mock.On("SetFocusable", focusable)} | |
| 8849 | + | } | |
| 8850 | + | ||
| 8851 | + | func (_c *MockButtonWidget_SetFocusable_Call) Run(run func(focusable bool)) *MockButtonWidget_SetFocusable_Call { | |
| 8852 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 8853 | + | var arg0 bool | |
| 8854 | + | if args[0] != nil { | |
| 8855 | + | arg0 = args[0].(bool) | |
| 8856 | + | } | |
| 8857 | + | run( | |
| 8858 | + | arg0, | |
| 8859 | + | ) | |
| 8860 | + | }) | |
| 8861 | + | return _c | |
| 8862 | + | } | |
| 8863 | + | ||
| 8864 | + | func (_c *MockButtonWidget_SetFocusable_Call) Return() *MockButtonWidget_SetFocusable_Call { | |
| 8865 | + | _c.Call.Return() | |
| 8866 | + | return _c | |
| 8867 | + | } | |
| 8868 | + | ||
| 8869 | + | func (_c *MockButtonWidget_SetFocusable_Call) RunAndReturn(run func(focusable bool)) *MockButtonWidget_SetFocusable_Call { | |
| 8870 | + | _c.Run(run) | |
| 8871 | + | return _c | |
| 8872 | + | } | |
| 8873 | + | ||
| 8634 | 8874 | // SetHalign provides a mock function for the type MockButtonWidget | |
| 8635 | 8875 | func (_mock *MockButtonWidget) SetHalign(align gtk.Align) { | |
| 8636 | 8876 | _mock.Called(align) | |
| @@ -9833,6 +10073,46 @@ func (_c *MockImageWidget_SetFocusOnClick_Call) RunAndReturn(run func(focusOnCli | |||
| 9833 | 10073 | return _c | |
| 9834 | 10074 | } | |
| 9835 | 10075 | ||
| 10076 | + | // SetFocusable provides a mock function for the type MockImageWidget | |
| 10077 | + | func (_mock *MockImageWidget) SetFocusable(focusable bool) { | |
| 10078 | + | _mock.Called(focusable) | |
| 10079 | + | return | |
| 10080 | + | } | |
| 10081 | + | ||
| 10082 | + | // MockImageWidget_SetFocusable_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetFocusable' | |
| 10083 | + | type MockImageWidget_SetFocusable_Call struct { | |
| 10084 | + | *mock.Call | |
| 10085 | + | } | |
| 10086 | + | ||
| 10087 | + | // SetFocusable is a helper method to define mock.On call | |
| 10088 | + | // - focusable bool | |
| 10089 | + | func (_e *MockImageWidget_Expecter) SetFocusable(focusable interface{}) *MockImageWidget_SetFocusable_Call { | |
| 10090 | + | return &MockImageWidget_SetFocusable_Call{Call: _e.mock.On("SetFocusable", focusable)} | |
| 10091 | + | } | |
| 10092 | + | ||
| 10093 | + | func (_c *MockImageWidget_SetFocusable_Call) Run(run func(focusable bool)) *MockImageWidget_SetFocusable_Call { | |
| 10094 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 10095 | + | var arg0 bool | |
| 10096 | + | if args[0] != nil { | |
| 10097 | + | arg0 = args[0].(bool) | |
| 10098 | + | } | |
| 10099 | + | run( | |
| 10100 | + | arg0, | |
| 10101 | + | ) | |
| 10102 | + | }) | |
| 10103 | + | return _c | |
| 10104 | + | } | |
| 10105 | + | ||
| 10106 | + | func (_c *MockImageWidget_SetFocusable_Call) Return() *MockImageWidget_SetFocusable_Call { | |
| 10107 | + | _c.Call.Return() | |
| 10108 | + | return _c | |
| 10109 | + | } | |
| 10110 | + | ||
| 10111 | + | func (_c *MockImageWidget_SetFocusable_Call) RunAndReturn(run func(focusable bool)) *MockImageWidget_SetFocusable_Call { | |
| 10112 | + | _c.Run(run) | |
| 10113 | + | return _c | |
| 10114 | + | } | |
| 10115 | + | ||
| 9836 | 10116 | // SetFromFile provides a mock function for the type MockImageWidget | |
| 9837 | 10117 | func (_mock *MockImageWidget) SetFromFile(filename string) { | |
| 9838 | 10118 | _mock.Called(filename) | |
| @@ -11166,6 +11446,46 @@ func (_c *MockProgressBarWidget_SetFocusOnClick_Call) RunAndReturn(run func(focu | |||
| 11166 | 11446 | return _c | |
| 11167 | 11447 | } | |
| 11168 | 11448 | ||
| 11449 | + | // SetFocusable provides a mock function for the type MockProgressBarWidget | |
| 11450 | + | func (_mock *MockProgressBarWidget) SetFocusable(focusable bool) { | |
| 11451 | + | _mock.Called(focusable) | |
| 11452 | + | return | |
| 11453 | + | } | |
| 11454 | + | ||
| 11455 | + | // MockProgressBarWidget_SetFocusable_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetFocusable' | |
| 11456 | + | type MockProgressBarWidget_SetFocusable_Call struct { | |
| 11457 | + | *mock.Call | |
| 11458 | + | } | |
| 11459 | + | ||
| 11460 | + | // SetFocusable is a helper method to define mock.On call | |
| 11461 | + | // - focusable bool | |
| 11462 | + | func (_e *MockProgressBarWidget_Expecter) SetFocusable(focusable interface{}) *MockProgressBarWidget_SetFocusable_Call { | |
| 11463 | + | return &MockProgressBarWidget_SetFocusable_Call{Call: _e.mock.On("SetFocusable", focusable)} | |
| 11464 | + | } | |
| 11465 | + | ||
| 11466 | + | func (_c *MockProgressBarWidget_SetFocusable_Call) Run(run func(focusable bool)) *MockProgressBarWidget_SetFocusable_Call { | |
| 11467 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 11468 | + | var arg0 bool | |
| 11469 | + | if args[0] != nil { | |
| 11470 | + | arg0 = args[0].(bool) | |
| 11471 | + | } | |
| 11472 | + | run( | |
| 11473 | + | arg0, | |
| 11474 | + | ) | |
| 11475 | + | }) | |
| 11476 | + | return _c | |
| 11477 | + | } | |
| 11478 | + | ||
| 11479 | + | func (_c *MockProgressBarWidget_SetFocusable_Call) Return() *MockProgressBarWidget_SetFocusable_Call { | |
| 11480 | + | _c.Call.Return() | |
| 11481 | + | return _c | |
| 11482 | + | } | |
| 11483 | + | ||
| 11484 | + | func (_c *MockProgressBarWidget_SetFocusable_Call) RunAndReturn(run func(focusable bool)) *MockProgressBarWidget_SetFocusable_Call { | |
| 11485 | + | _c.Run(run) | |
| 11486 | + | return _c | |
| 11487 | + | } | |
| 11488 | + | ||
| 11169 | 11489 | // SetFraction provides a mock function for the type MockProgressBarWidget | |
| 11170 | 11490 | func (_mock *MockProgressBarWidget) SetFraction(fraction float64) { | |
| 11171 | 11491 | _mock.Called(fraction) | |
| @@ -12379,6 +12699,46 @@ func (_c *MockSpinnerWidget_SetFocusOnClick_Call) RunAndReturn(run func(focusOnC | |||
| 12379 | 12699 | return _c | |
| 12380 | 12700 | } | |
| 12381 | 12701 | ||
| 12702 | + | // SetFocusable provides a mock function for the type MockSpinnerWidget | |
| 12703 | + | func (_mock *MockSpinnerWidget) SetFocusable(focusable bool) { | |
| 12704 | + | _mock.Called(focusable) | |
| 12705 | + | return | |
| 12706 | + | } | |
| 12707 | + | ||
| 12708 | + | // MockSpinnerWidget_SetFocusable_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetFocusable' | |
| 12709 | + | type MockSpinnerWidget_SetFocusable_Call struct { | |
| 12710 | + | *mock.Call | |
| 12711 | + | } | |
| 12712 | + | ||
| 12713 | + | // SetFocusable is a helper method to define mock.On call | |
| 12714 | + | // - focusable bool | |
| 12715 | + | func (_e *MockSpinnerWidget_Expecter) SetFocusable(focusable interface{}) *MockSpinnerWidget_SetFocusable_Call { | |
| 12716 | + | return &MockSpinnerWidget_SetFocusable_Call{Call: _e.mock.On("SetFocusable", focusable)} | |
| 12717 | + | } | |
| 12718 | + | ||
| 12719 | + | func (_c *MockSpinnerWidget_SetFocusable_Call) Run(run func(focusable bool)) *MockSpinnerWidget_SetFocusable_Call { | |
| 12720 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 12721 | + | var arg0 bool | |
| 12722 | + | if args[0] != nil { | |
| 12723 | + | arg0 = args[0].(bool) | |
| 12724 | + | } | |
| 12725 | + | run( | |
| 12726 | + | arg0, | |
| 12727 | + | ) | |
| 12728 | + | }) | |
| 12729 | + | return _c | |
| 12730 | + | } | |
| 12731 | + | ||
| 12732 | + | func (_c *MockSpinnerWidget_SetFocusable_Call) Return() *MockSpinnerWidget_SetFocusable_Call { | |
| 12733 | + | _c.Call.Return() | |
| 12734 | + | return _c | |
| 12735 | + | } | |
| 12736 | + | ||
| 12737 | + | func (_c *MockSpinnerWidget_SetFocusable_Call) RunAndReturn(run func(focusable bool)) *MockSpinnerWidget_SetFocusable_Call { | |
| 12738 | + | _c.Run(run) | |
| 12739 | + | return _c | |
| 12740 | + | } | |
| 12741 | + | ||
| 12382 | 12742 | // SetHalign provides a mock function for the type MockSpinnerWidget | |
| 12383 | 12743 | func (_mock *MockSpinnerWidget) SetHalign(align gtk.Align) { | |
| 12384 | 12744 | _mock.Called(align) | |
| @@ -908,6 +908,46 @@ func (_c *MockWidget_SetFocusOnClick_Call) RunAndReturn(run func(focusOnClick bo | |||||
| 908 | return _c | 908 | return _c | ||
| 909 | } | 909 | } | ||
| 910 | 910 | ||||
| 911 | + | // SetFocusable provides a mock function for the type MockWidget | |||
| 912 | + | func (_mock *MockWidget) SetFocusable(focusable bool) { | |||
| 913 | + | _mock.Called(focusable) | |||
| 914 | + | return | |||
| 915 | + | } | |||
| 916 | + | ||||
| 917 | + | // MockWidget_SetFocusable_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetFocusable' | |||
| 918 | + | type MockWidget_SetFocusable_Call struct { | |||
| 919 | + | *mock.Call | |||
| 920 | + | } | |||
| 921 | + | ||||
| 922 | + | // SetFocusable is a helper method to define mock.On call | |||
| 923 | + | // - focusable bool | |||
| 924 | + | func (_e *MockWidget_Expecter) SetFocusable(focusable interface{}) *MockWidget_SetFocusable_Call { | |||
| 925 | + | return &MockWidget_SetFocusable_Call{Call: _e.mock.On("SetFocusable", focusable)} | |||
| 926 | + | } | |||
| 927 | + | ||||
| 928 | + | func (_c *MockWidget_SetFocusable_Call) Run(run func(focusable bool)) *MockWidget_SetFocusable_Call { | |||
| 929 | + | _c.Call.Run(func(args mock.Arguments) { | |||
| 930 | + | var arg0 bool | |||
| 931 | + | if args[0] != nil { | |||
| 932 | + | arg0 = args[0].(bool) | |||
| 933 | + | } | |||
| 934 | + | run( | |||
| 935 | + | arg0, | |||
| 936 | + | ) | |||
| 937 | + | }) | |||
| 938 | + | return _c | |||
| 939 | + | } | |||
| 940 | + | ||||
| 941 | + | func (_c *MockWidget_SetFocusable_Call) Return() *MockWidget_SetFocusable_Call { | |||
| 942 | + | _c.Call.Return() | |||
| 943 | + | return _c | |||
| 944 | + | } | |||
| 945 | + | ||||
| 946 | + | func (_c *MockWidget_SetFocusable_Call) RunAndReturn(run func(focusable bool)) *MockWidget_SetFocusable_Call { | |||
| 947 | + | _c.Run(run) | |||
| 948 | + | return _c | |||
| 949 | + | } | |||
| 950 | + | ||||
| 911 | // SetHalign provides a mock function for the type MockWidget | 951 | // SetHalign provides a mock function for the type MockWidget | ||
| 912 | func (_mock *MockWidget) SetHalign(align gtk.Align) { | 952 | func (_mock *MockWidget) SetHalign(align gtk.Align) { | ||
| 913 | _mock.Called(align) | 953 | _mock.Called(align) | ||
| @@ -2586,6 +2626,46 @@ func (_c *MockPanedWidget_SetFocusOnClick_Call) RunAndReturn(run func(focusOnCli | |||||
| 2586 | return _c | 2626 | return _c | ||
| 2587 | } | 2627 | } | ||
| 2588 | 2628 | ||||
| 2629 | + | // SetFocusable provides a mock function for the type MockPanedWidget | |||
| 2630 | + | func (_mock *MockPanedWidget) SetFocusable(focusable bool) { | |||
| 2631 | + | _mock.Called(focusable) | |||
| 2632 | + | return | |||
| 2633 | + | } | |||
| 2634 | + | ||||
| 2635 | + | // MockPanedWidget_SetFocusable_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetFocusable' | |||
| 2636 | + | type MockPanedWidget_SetFocusable_Call struct { | |||
| 2637 | + | *mock.Call | |||
| 2638 | + | } | |||
| 2639 | + | ||||
| 2640 | + | // SetFocusable is a helper method to define mock.On call | |||
| 2641 | + | // - focusable bool | |||
| 2642 | + | func (_e *MockPanedWidget_Expecter) SetFocusable(focusable interface{}) *MockPanedWidget_SetFocusable_Call { | |||
| 2643 | + | return &MockPanedWidget_SetFocusable_Call{Call: _e.mock.On("SetFocusable", focusable)} | |||
| 2644 | + | } | |||
| 2645 | + | ||||
| 2646 | + | func (_c *MockPanedWidget_SetFocusable_Call) Run(run func(focusable bool)) *MockPanedWidget_SetFocusable_Call { | |||
| 2647 | + | _c.Call.Run(func(args mock.Arguments) { | |||
| 2648 | + | var arg0 bool | |||
| 2649 | + | if args[0] != nil { | |||
| 2650 | + | arg0 = args[0].(bool) | |||
| 2651 | + | } | |||
| 2652 | + | run( | |||
| 2653 | + | arg0, | |||
| 2654 | + | ) | |||
| 2655 | + | }) | |||
| 2656 | + | return _c | |||
| 2657 | + | } | |||
| 2658 | + | ||||
| 2659 | + | func (_c *MockPanedWidget_SetFocusable_Call) Return() *MockPanedWidget_SetFocusable_Call { | |||
| 2660 | + | _c.Call.Return() | |||
| 2661 | + | return _c | |||
| 2662 | + | } | |||
| 2663 | + | ||||
| 2664 | + | func (_c *MockPanedWidget_SetFocusable_Call) RunAndReturn(run func(focusable bool)) *MockPanedWidget_SetFocusable_Call { | |||
| 2665 | + | _c.Run(run) | |||
| 2666 | + | return _c | |||
| 2667 | + | } | |||
| 2668 | + | ||||
| 2589 | // SetHalign provides a mock function for the type MockPanedWidget | 2669 | // SetHalign provides a mock function for the type MockPanedWidget | ||
| 2590 | func (_mock *MockPanedWidget) SetHalign(align gtk.Align) { | 2670 | func (_mock *MockPanedWidget) SetHalign(align gtk.Align) { | ||
| 2591 | _mock.Called(align) | 2671 | _mock.Called(align) | ||
| @@ -4339,6 +4419,46 @@ func (_c *MockBoxWidget_SetFocusOnClick_Call) RunAndReturn(run func(focusOnClick | |||||
| 4339 | return _c | 4419 | return _c | ||
| 4340 | } | 4420 | } | ||
| 4341 | 4421 | ||||
| 4422 | + | // SetFocusable provides a mock function for the type MockBoxWidget | |||
| 4423 | + | func (_mock *MockBoxWidget) SetFocusable(focusable bool) { | |||
| 4424 | + | _mock.Called(focusable) | |||
| 4425 | + | return | |||
| 4426 | + | } | |||
| 4427 | + | ||||
| 4428 | + | // MockBoxWidget_SetFocusable_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetFocusable' | |||
| 4429 | + | type MockBoxWidget_SetFocusable_Call struct { | |||
| 4430 | + | *mock.Call | |||
| 4431 | + | } | |||
| 4432 | + | ||||
| 4433 | + | // SetFocusable is a helper method to define mock.On call | |||
| 4434 | + | // - focusable bool | |||
| 4435 | + | func (_e *MockBoxWidget_Expecter) SetFocusable(focusable interface{}) *MockBoxWidget_SetFocusable_Call { | |||
| 4436 | + | return &MockBoxWidget_SetFocusable_Call{Call: _e.mock.On("SetFocusable", focusable)} | |||
| 4437 | + | } | |||
| 4438 | + | ||||
| 4439 | + | func (_c *MockBoxWidget_SetFocusable_Call) Run(run func(focusable bool)) *MockBoxWidget_SetFocusable_Call { | |||
| 4440 | + | _c.Call.Run(func(args mock.Arguments) { | |||
| 4441 | + | var arg0 bool | |||
| 4442 | + | if args[0] != nil { | |||
| 4443 | + | arg0 = args[0].(bool) | |||
| 4444 | + | } | |||
| 4445 | + | run( | |||
| 4446 | + | arg0, | |||
| 4447 | + | ) | |||
| 4448 | + | }) | |||
| 4449 | + | return _c | |||
| 4450 | + | } | |||
| 4451 | + | ||||
| 4452 | + | func (_c *MockBoxWidget_SetFocusable_Call) Return() *MockBoxWidget_SetFocusable_Call { | |||
| 4453 | + | _c.Call.Return() | |||
| 4454 | + | return _c | |||
| 4455 | + | } | |||
| 4456 | + | ||||
| 4457 | + | func (_c *MockBoxWidget_SetFocusable_Call) RunAndReturn(run func(focusable bool)) *MockBoxWidget_SetFocusable_Call { | |||
| 4458 | + | _c.Run(run) | |||
| 4459 | + | return _c | |||
| 4460 | + | } | |||
| 4461 | + | ||||
| 4342 | // SetHalign provides a mock function for the type MockBoxWidget | 4462 | // SetHalign provides a mock function for the type MockBoxWidget | ||
| 4343 | func (_mock *MockBoxWidget) SetHalign(align gtk.Align) { | 4463 | func (_mock *MockBoxWidget) SetHalign(align gtk.Align) { | ||
| 4344 | _mock.Called(align) | 4464 | _mock.Called(align) | ||
| @@ -5902,6 +6022,46 @@ func (_c *MockOverlayWidget_SetFocusOnClick_Call) RunAndReturn(run func(focusOnC | |||||
| 5902 | return _c | 6022 | return _c | ||
| 5903 | } | 6023 | } | ||
| 5904 | 6024 | ||||
| 6025 | + | // SetFocusable provides a mock function for the type MockOverlayWidget | |||
| 6026 | + | func (_mock *MockOverlayWidget) SetFocusable(focusable bool) { | |||
| 6027 | + | _mock.Called(focusable) | |||
| 6028 | + | return | |||
| 6029 | + | } | |||
| 6030 | + | ||||
| 6031 | + | // MockOverlayWidget_SetFocusable_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetFocusable' | |||
| 6032 | + | type MockOverlayWidget_SetFocusable_Call struct { | |||
| 6033 | + | *mock.Call | |||
| 6034 | + | } | |||
| 6035 | + | ||||
| 6036 | + | // SetFocusable is a helper method to define mock.On call | |||
| 6037 | + | // - focusable bool | |||
| 6038 | + | func (_e *MockOverlayWidget_Expecter) SetFocusable(focusable interface{}) *MockOverlayWidget_SetFocusable_Call { | |||
| 6039 | + | return &MockOverlayWidget_SetFocusable_Call{Call: _e.mock.On("SetFocusable", focusable)} | |||
| 6040 | + | } | |||
| 6041 | + | ||||
| 6042 | + | func (_c *MockOverlayWidget_SetFocusable_Call) Run(run func(focusable bool)) *MockOverlayWidget_SetFocusable_Call { | |||
| 6043 | + | _c.Call.Run(func(args mock.Arguments) { | |||
| 6044 | + | var arg0 bool | |||
| 6045 | + | if args[0] != nil { | |||
| 6046 | + | arg0 = args[0].(bool) | |||
| 6047 | + | } | |||
| 6048 | + | run( | |||
| 6049 | + | arg0, | |||
| 6050 | + | ) | |||
| 6051 | + | }) | |||
| 6052 | + | return _c | |||
| 6053 | + | } | |||
| 6054 | + | ||||
| 6055 | + | func (_c *MockOverlayWidget_SetFocusable_Call) Return() *MockOverlayWidget_SetFocusable_Call { | |||
| 6056 | + | _c.Call.Return() | |||
| 6057 | + | return _c | |||
| 6058 | + | } | |||
| 6059 | + | ||||
| 6060 | + | func (_c *MockOverlayWidget_SetFocusable_Call) RunAndReturn(run func(focusable bool)) *MockOverlayWidget_SetFocusable_Call { | |||
| 6061 | + | _c.Run(run) | |||
| 6062 | + | return _c | |||
| 6063 | + | } | |||
| 6064 | + | ||||
| 5905 | // SetHalign provides a mock function for the type MockOverlayWidget | 6065 | // SetHalign provides a mock function for the type MockOverlayWidget | ||
| 5906 | func (_mock *MockOverlayWidget) SetHalign(align gtk.Align) { | 6066 | func (_mock *MockOverlayWidget) SetHalign(align gtk.Align) { | ||
| 5907 | _mock.Called(align) | 6067 | _mock.Called(align) | ||
| @@ -7161,6 +7321,46 @@ func (_c *MockLabelWidget_SetFocusOnClick_Call) RunAndReturn(run func(focusOnCli | |||||
| 7161 | return _c | 7321 | return _c | ||
| 7162 | } | 7322 | } | ||
| 7163 | 7323 | ||||
| 7324 | + | // SetFocusable provides a mock function for the type MockLabelWidget | |||
| 7325 | + | func (_mock *MockLabelWidget) SetFocusable(focusable bool) { | |||
| 7326 | + | _mock.Called(focusable) | |||
| 7327 | + | return | |||
| 7328 | + | } | |||
| 7329 | + | ||||
| 7330 | + | // MockLabelWidget_SetFocusable_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetFocusable' | |||
| 7331 | + | type MockLabelWidget_SetFocusable_Call struct { | |||
| 7332 | + | *mock.Call | |||
| 7333 | + | } | |||
| 7334 | + | ||||
| 7335 | + | // SetFocusable is a helper method to define mock.On call | |||
| 7336 | + | // - focusable bool | |||
| 7337 | + | func (_e *MockLabelWidget_Expecter) SetFocusable(focusable interface{}) *MockLabelWidget_SetFocusable_Call { | |||
| 7338 | + | return &MockLabelWidget_SetFocusable_Call{Call: _e.mock.On("SetFocusable", focusable)} | |||
| 7339 | + | } | |||
| 7340 | + | ||||
| 7341 | + | func (_c *MockLabelWidget_SetFocusable_Call) Run(run func(focusable bool)) *MockLabelWidget_SetFocusable_Call { | |||
| 7342 | + | _c.Call.Run(func(args mock.Arguments) { | |||
| 7343 | + | var arg0 bool | |||
| 7344 | + | if args[0] != nil { | |||
| 7345 | + | arg0 = args[0].(bool) | |||
| 7346 | + | } | |||
| 7347 | + | run( | |||
| 7348 | + | arg0, | |||
| 7349 | + | ) | |||
| 7350 | + | }) | |||
| 7351 | + | return _c | |||
| 7352 | + | } | |||
| 7353 | + | ||||
| 7354 | + | func (_c *MockLabelWidget_SetFocusable_Call) Return() *MockLabelWidget_SetFocusable_Call { | |||
| 7355 | + | _c.Call.Return() | |||
| 7356 | + | return _c | |||
| 7357 | + | } | |||
| 7358 | + | ||||
| 7359 | + | func (_c *MockLabelWidget_SetFocusable_Call) RunAndReturn(run func(focusable bool)) *MockLabelWidget_SetFocusable_Call { | |||
| 7360 | + | _c.Run(run) | |||
| 7361 | + | return _c | |||
| 7362 | + | } | |||
| 7363 | + | ||||
| 7164 | // SetHalign provides a mock function for the type MockLabelWidget | 7364 | // SetHalign provides a mock function for the type MockLabelWidget | ||
| 7165 | func (_mock *MockLabelWidget) SetHalign(align gtk.Align) { | 7365 | func (_mock *MockLabelWidget) SetHalign(align gtk.Align) { | ||
| 7166 | _mock.Called(align) | 7366 | _mock.Called(align) | ||
| @@ -8631,6 +8831,46 @@ func (_c *MockButtonWidget_SetFocusOnClick_Call) RunAndReturn(run func(focusOnCl | |||||
| 8631 | return _c | 8831 | return _c | ||
| 8632 | } | 8832 | } | ||
| 8633 | 8833 | ||||
| 8834 | + | // SetFocusable provides a mock function for the type MockButtonWidget | |||
| 8835 | + | func (_mock *MockButtonWidget) SetFocusable(focusable bool) { | |||
| 8836 | + | _mock.Called(focusable) | |||
| 8837 | + | return | |||
| 8838 | + | } | |||
| 8839 | + | ||||
| 8840 | + | // MockButtonWidget_SetFocusable_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetFocusable' | |||
| 8841 | + | type MockButtonWidget_SetFocusable_Call struct { | |||
| 8842 | + | *mock.Call | |||
| 8843 | + | } | |||
| 8844 | + | ||||
| 8845 | + | // SetFocusable is a helper method to define mock.On call | |||
| 8846 | + | // - focusable bool | |||
| 8847 | + | func (_e *MockButtonWidget_Expecter) SetFocusable(focusable interface{}) *MockButtonWidget_SetFocusable_Call { | |||
| 8848 | + | return &MockButtonWidget_SetFocusable_Call{Call: _e.mock.On("SetFocusable", focusable)} | |||
| 8849 | + | } | |||
| 8850 | + | ||||
| 8851 | + | func (_c *MockButtonWidget_SetFocusable_Call) Run(run func(focusable bool)) *MockButtonWidget_SetFocusable_Call { | |||
| 8852 | + | _c.Call.Run(func(args mock.Arguments) { | |||
| 8853 | + | var arg0 bool | |||
| 8854 | + | if args[0] != nil { | |||
| 8855 | + | arg0 = args[0].(bool) | |||
| 8856 | + | } | |||
| 8857 | + | run( | |||
| 8858 | + | arg0, | |||
| 8859 | + | ) | |||
| 8860 | + | }) | |||
| 8861 | + | return _c | |||
| 8862 | + | } | |||
| 8863 | + | ||||
| 8864 | + | func (_c *MockButtonWidget_SetFocusable_Call) Return() *MockButtonWidget_SetFocusable_Call { | |||
| 8865 | + | _c.Call.Return() | |||
| 8866 | + | return _c | |||
| 8867 | + | } | |||
| 8868 | + | ||||
| 8869 | + | func (_c *MockButtonWidget_SetFocusable_Call) RunAndReturn(run func(focusable bool)) *MockButtonWidget_SetFocusable_Call { | |||
| 8870 | + | _c.Run(run) | |||
| 8871 | + | return _c | |||
| 8872 | + | } | |||
| 8873 | + | ||||
| 8634 | // SetHalign provides a mock function for the type MockButtonWidget | 8874 | // SetHalign provides a mock function for the type MockButtonWidget | ||
| 8635 | func (_mock *MockButtonWidget) SetHalign(align gtk.Align) { | 8875 | func (_mock *MockButtonWidget) SetHalign(align gtk.Align) { | ||
| 8636 | _mock.Called(align) | 8876 | _mock.Called(align) | ||
| @@ -9833,6 +10073,46 @@ func (_c *MockImageWidget_SetFocusOnClick_Call) RunAndReturn(run func(focusOnCli | |||||
| 9833 | return _c | 10073 | return _c | ||
| 9834 | } | 10074 | } | ||
| 9835 | 10075 | ||||
| 10076 | + | // SetFocusable provides a mock function for the type MockImageWidget | |||
| 10077 | + | func (_mock *MockImageWidget) SetFocusable(focusable bool) { | |||
| 10078 | + | _mock.Called(focusable) | |||
| 10079 | + | return | |||
| 10080 | + | } | |||
| 10081 | + | ||||
| 10082 | + | // MockImageWidget_SetFocusable_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetFocusable' | |||
| 10083 | + | type MockImageWidget_SetFocusable_Call struct { | |||
| 10084 | + | *mock.Call | |||
| 10085 | + | } | |||
| 10086 | + | ||||
| 10087 | + | // SetFocusable is a helper method to define mock.On call | |||
| 10088 | + | // - focusable bool | |||
| 10089 | + | func (_e *MockImageWidget_Expecter) SetFocusable(focusable interface{}) *MockImageWidget_SetFocusable_Call { | |||
| 10090 | + | return &MockImageWidget_SetFocusable_Call{Call: _e.mock.On("SetFocusable", focusable)} | |||
| 10091 | + | } | |||
| 10092 | + | ||||
| 10093 | + | func (_c *MockImageWidget_SetFocusable_Call) Run(run func(focusable bool)) *MockImageWidget_SetFocusable_Call { | |||
| 10094 | + | _c.Call.Run(func(args mock.Arguments) { | |||
| 10095 | + | var arg0 bool | |||
| 10096 | + | if args[0] != nil { | |||
| 10097 | + | arg0 = args[0].(bool) | |||
| 10098 | + | } | |||
| 10099 | + | run( | |||
| 10100 | + | arg0, | |||
| 10101 | + | ) | |||
| 10102 | + | }) | |||
| 10103 | + | return _c | |||
| 10104 | + | } | |||
| 10105 | + | ||||
| 10106 | + | func (_c *MockImageWidget_SetFocusable_Call) Return() *MockImageWidget_SetFocusable_Call { | |||
| 10107 | + | _c.Call.Return() | |||
| 10108 | + | return _c | |||
| 10109 | + | } | |||
| 10110 | + | ||||
| 10111 | + | func (_c *MockImageWidget_SetFocusable_Call) RunAndReturn(run func(focusable bool)) *MockImageWidget_SetFocusable_Call { | |||
| 10112 | + | _c.Run(run) | |||
| 10113 | + | return _c | |||
| 10114 | + | } | |||
| 10115 | + | ||||
| 9836 | // SetFromFile provides a mock function for the type MockImageWidget | 10116 | // SetFromFile provides a mock function for the type MockImageWidget | ||
| 9837 | func (_mock *MockImageWidget) SetFromFile(filename string) { | 10117 | func (_mock *MockImageWidget) SetFromFile(filename string) { | ||
| 9838 | _mock.Called(filename) | 10118 | _mock.Called(filename) | ||
| @@ -11166,6 +11446,46 @@ func (_c *MockProgressBarWidget_SetFocusOnClick_Call) RunAndReturn(run func(focu | |||||
| 11166 | return _c | 11446 | return _c | ||
| 11167 | } | 11447 | } | ||
| 11168 | 11448 | ||||
| 11449 | + | // SetFocusable provides a mock function for the type MockProgressBarWidget | |||
| 11450 | + | func (_mock *MockProgressBarWidget) SetFocusable(focusable bool) { | |||
| 11451 | + | _mock.Called(focusable) | |||
| 11452 | + | return | |||
| 11453 | + | } | |||
| 11454 | + | ||||
| 11455 | + | // MockProgressBarWidget_SetFocusable_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetFocusable' | |||
| 11456 | + | type MockProgressBarWidget_SetFocusable_Call struct { | |||
| 11457 | + | *mock.Call | |||
| 11458 | + | } | |||
| 11459 | + | ||||
| 11460 | + | // SetFocusable is a helper method to define mock.On call | |||
| 11461 | + | // - focusable bool | |||
| 11462 | + | func (_e *MockProgressBarWidget_Expecter) SetFocusable(focusable interface{}) *MockProgressBarWidget_SetFocusable_Call { | |||
| 11463 | + | return &MockProgressBarWidget_SetFocusable_Call{Call: _e.mock.On("SetFocusable", focusable)} | |||
| 11464 | + | } | |||
| 11465 | + | ||||
| 11466 | + | func (_c *MockProgressBarWidget_SetFocusable_Call) Run(run func(focusable bool)) *MockProgressBarWidget_SetFocusable_Call { | |||
| 11467 | + | _c.Call.Run(func(args mock.Arguments) { | |||
| 11468 | + | var arg0 bool | |||
| 11469 | + | if args[0] != nil { | |||
| 11470 | + | arg0 = args[0].(bool) | |||
| 11471 | + | } | |||
| 11472 | + | run( | |||
| 11473 | + | arg0, | |||
| 11474 | + | ) | |||
| 11475 | + | }) | |||
| 11476 | + | return _c | |||
| 11477 | + | } | |||
| 11478 | + | ||||
| 11479 | + | func (_c *MockProgressBarWidget_SetFocusable_Call) Return() *MockProgressBarWidget_SetFocusable_Call { | |||
| 11480 | + | _c.Call.Return() | |||
| 11481 | + | return _c | |||
| 11482 | + | } | |||
| 11483 | + | ||||
| 11484 | + | func (_c *MockProgressBarWidget_SetFocusable_Call) RunAndReturn(run func(focusable bool)) *MockProgressBarWidget_SetFocusable_Call { | |||
| 11485 | + | _c.Run(run) | |||
| 11486 | + | return _c | |||
| 11487 | + | } | |||
| 11488 | + | ||||
| 11169 | // SetFraction provides a mock function for the type MockProgressBarWidget | 11489 | // SetFraction provides a mock function for the type MockProgressBarWidget | ||
| 11170 | func (_mock *MockProgressBarWidget) SetFraction(fraction float64) { | 11490 | func (_mock *MockProgressBarWidget) SetFraction(fraction float64) { | ||
| 11171 | _mock.Called(fraction) | 11491 | _mock.Called(fraction) | ||
| @@ -12379,6 +12699,46 @@ func (_c *MockSpinnerWidget_SetFocusOnClick_Call) RunAndReturn(run func(focusOnC | |||||
| 12379 | return _c | 12699 | return _c | ||
| 12380 | } | 12700 | } | ||
| 12381 | 12701 | ||||
| 12702 | + | // SetFocusable provides a mock function for the type MockSpinnerWidget | |||
| 12703 | + | func (_mock *MockSpinnerWidget) SetFocusable(focusable bool) { | |||
| 12704 | + | _mock.Called(focusable) | |||
| 12705 | + | return | |||
| 12706 | + | } | |||
| 12707 | + | ||||
| 12708 | + | // MockSpinnerWidget_SetFocusable_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetFocusable' | |||
| 12709 | + | type MockSpinnerWidget_SetFocusable_Call struct { | |||
| 12710 | + | *mock.Call | |||
| 12711 | + | } | |||
| 12712 | + | ||||
| 12713 | + | // SetFocusable is a helper method to define mock.On call | |||
| 12714 | + | // - focusable bool | |||
| 12715 | + | func (_e *MockSpinnerWidget_Expecter) SetFocusable(focusable interface{}) *MockSpinnerWidget_SetFocusable_Call { | |||
| 12716 | + | return &MockSpinnerWidget_SetFocusable_Call{Call: _e.mock.On("SetFocusable", focusable)} | |||
| 12717 | + | } | |||
| 12718 | + | ||||
| 12719 | + | func (_c *MockSpinnerWidget_SetFocusable_Call) Run(run func(focusable bool)) *MockSpinnerWidget_SetFocusable_Call { | |||
| 12720 | + | _c.Call.Run(func(args mock.Arguments) { | |||
| 12721 | + | var arg0 bool | |||
| 12722 | + | if args[0] != nil { | |||
| 12723 | + | arg0 = args[0].(bool) | |||
| 12724 | + | } | |||
| 12725 | + | run( | |||
| 12726 | + | arg0, | |||
| 12727 | + | ) | |||
| 12728 | + | }) | |||
| 12729 | + | return _c | |||
| 12730 | + | } | |||
| 12731 | + | ||||
| 12732 | + | func (_c *MockSpinnerWidget_SetFocusable_Call) Return() *MockSpinnerWidget_SetFocusable_Call { | |||
| 12733 | + | _c.Call.Return() | |||
| 12734 | + | return _c | |||
| 12735 | + | } | |||
| 12736 | + | ||||
| 12737 | + | func (_c *MockSpinnerWidget_SetFocusable_Call) RunAndReturn(run func(focusable bool)) *MockSpinnerWidget_SetFocusable_Call { | |||
| 12738 | + | _c.Run(run) | |||
| 12739 | + | return _c | |||
| 12740 | + | } | |||
| 12741 | + | ||||
| 12382 | // SetHalign provides a mock function for the type MockSpinnerWidget | 12742 | // SetHalign provides a mock function for the type MockSpinnerWidget | ||
| 12383 | func (_mock *MockSpinnerWidget) SetHalign(align gtk.Align) { | 12743 | func (_mock *MockSpinnerWidget) SetHalign(align gtk.Align) { | ||
| 12384 | _mock.Called(align) | 12744 | _mock.Called(align) | ||
M internal/ui/layout/widgets.go +1 -0
| @@ -28,6 +28,7 @@ type Widget interface { | |||
| 28 | 28 | GrabFocus() bool | |
| 29 | 29 | HasFocus() bool | |
| 30 | 30 | SetCanFocus(canFocus bool) | |
| 31 | + | SetFocusable(focusable bool) // GTK4: makes widget able to receive keyboard focus | |
| 31 | 32 | SetFocusOnClick(focusOnClick bool) | |
| 32 | 33 | ||
| 33 | 34 | // Pointer events | |
| @@ -28,6 +28,7 @@ type Widget interface { | |||||
| 28 | GrabFocus() bool | 28 | GrabFocus() bool | ||
| 29 | HasFocus() bool | 29 | HasFocus() bool | ||
| 30 | SetCanFocus(canFocus bool) | 30 | SetCanFocus(canFocus bool) | ||
| 31 | + | SetFocusable(focusable bool) // GTK4: makes widget able to receive keyboard focus | |||
| 31 | SetFocusOnClick(focusOnClick bool) | 32 | SetFocusOnClick(focusOnClick bool) | ||
| 32 | 33 | ||||
| 33 | // Pointer events | 34 | // Pointer events | ||
A internal/ui/theme/accent_picker_css.go +56 -0
| @@ -0,0 +1,56 @@ | |||
| 1 | + | package theme | |
| 2 | + | ||
| 3 | + | // generateAccentPickerCSS creates accent picker overlay styles. | |
| 4 | + | // The picker displays a row of accented characters that the user can select. | |
| 5 | + | func generateAccentPickerCSS(p Palette) string { | |
| 6 | + | _ = p | |
| 7 | + | return `/* ===== Accent Picker Styling ===== */ | |
| 8 | + | ||
| 9 | + | /* Main container - horizontal row of accent characters */ | |
| 10 | + | .accent-picker { | |
| 11 | + | background-color: alpha(var(--surface-variant), 0.95); | |
| 12 | + | border: 0.0625em solid var(--border); | |
| 13 | + | border-radius: 0.25em; | |
| 14 | + | padding: 0.375em 0.5em; | |
| 15 | + | margin-bottom: 1em; | |
| 16 | + | box-shadow: 0 2px 8px alpha(black, 0.2); | |
| 17 | + | transition: border-color 100ms ease-in-out; | |
| 18 | + | } | |
| 19 | + | ||
| 20 | + | /* Focused state - accent border like omnibox */ | |
| 21 | + | .accent-picker:focus, | |
| 22 | + | .accent-picker:focus-within, | |
| 23 | + | .accent-picker:focus-visible { | |
| 24 | + | border-color: var(--accent); | |
| 25 | + | outline-style: none; | |
| 26 | + | outline-width: 0px; | |
| 27 | + | outline-color: transparent; | |
| 28 | + | } | |
| 29 | + | ||
| 30 | + | /* Individual accent character label */ | |
| 31 | + | .accent-picker-item { | |
| 32 | + | font-size: 1.5em; | |
| 33 | + | font-weight: 500; | |
| 34 | + | color: var(--text); | |
| 35 | + | padding: 0.25em 0.5em; | |
| 36 | + | border-radius: 0.125em; | |
| 37 | + | min-width: 1.5em; | |
| 38 | + | transition: background-color 100ms ease-in-out; | |
| 39 | + | } | |
| 40 | + | ||
| 41 | + | .accent-picker-item:hover { | |
| 42 | + | background-color: alpha(var(--accent), 0.15); | |
| 43 | + | } | |
| 44 | + | ||
| 45 | + | /* Selected accent character */ | |
| 46 | + | .accent-picker-selected { | |
| 47 | + | background-color: alpha(var(--accent), 0.25); | |
| 48 | + | color: var(--accent); | |
| 49 | + | } | |
| 50 | + | ||
| 51 | + | /* Numbered items (1-9) could show subtle number hints */ | |
| 52 | + | .accent-picker-numbered { | |
| 53 | + | /* Could add ::after pseudo-element for number hint if needed */ | |
| 54 | + | } | |
| 55 | + | ` | |
| 56 | + | } | |
| @@ -0,0 +1,56 @@ | |||||
| 1 | + | package theme | |||
| 2 | + | ||||
| 3 | + | // generateAccentPickerCSS creates accent picker overlay styles. | |||
| 4 | + | // The picker displays a row of accented characters that the user can select. | |||
| 5 | + | func generateAccentPickerCSS(p Palette) string { | |||
| 6 | + | _ = p | |||
| 7 | + | return `/* ===== Accent Picker Styling ===== */ | |||
| 8 | + | ||||
| 9 | + | /* Main container - horizontal row of accent characters */ | |||
| 10 | + | .accent-picker { | |||
| 11 | + | background-color: alpha(var(--surface-variant), 0.95); | |||
| 12 | + | border: 0.0625em solid var(--border); | |||
| 13 | + | border-radius: 0.25em; | |||
| 14 | + | padding: 0.375em 0.5em; | |||
| 15 | + | margin-bottom: 1em; | |||
| 16 | + | box-shadow: 0 2px 8px alpha(black, 0.2); | |||
| 17 | + | transition: border-color 100ms ease-in-out; | |||
| 18 | + | } | |||
| 19 | + | ||||
| 20 | + | /* Focused state - accent border like omnibox */ | |||
| 21 | + | .accent-picker:focus, | |||
| 22 | + | .accent-picker:focus-within, | |||
| 23 | + | .accent-picker:focus-visible { | |||
| 24 | + | border-color: var(--accent); | |||
| 25 | + | outline-style: none; | |||
| 26 | + | outline-width: 0px; | |||
| 27 | + | outline-color: transparent; | |||
| 28 | + | } | |||
| 29 | + | ||||
| 30 | + | /* Individual accent character label */ | |||
| 31 | + | .accent-picker-item { | |||
| 32 | + | font-size: 1.5em; | |||
| 33 | + | font-weight: 500; | |||
| 34 | + | color: var(--text); | |||
| 35 | + | padding: 0.25em 0.5em; | |||
| 36 | + | border-radius: 0.125em; | |||
| 37 | + | min-width: 1.5em; | |||
| 38 | + | transition: background-color 100ms ease-in-out; | |||
| 39 | + | } | |||
| 40 | + | ||||
| 41 | + | .accent-picker-item:hover { | |||
| 42 | + | background-color: alpha(var(--accent), 0.15); | |||
| 43 | + | } | |||
| 44 | + | ||||
| 45 | + | /* Selected accent character */ | |||
| 46 | + | .accent-picker-selected { | |||
| 47 | + | background-color: alpha(var(--accent), 0.25); | |||
| 48 | + | color: var(--accent); | |||
| 49 | + | } | |||
| 50 | + | ||||
| 51 | + | /* Numbered items (1-9) could show subtle number hints */ | |||
| 52 | + | .accent-picker-numbered { | |||
| 53 | + | /* Could add ::after pseudo-element for number hint if needed */ | |||
| 54 | + | } | |||
| 55 | + | ` | |||
| 56 | + | } | |||
M internal/ui/theme/css.go +4 -0
| @@ -126,6 +126,10 @@ func GenerateCSSFull(p Palette, scale float64, fonts FontConfig, modeColors Mode | |||
| 126 | 126 | ||
| 127 | 127 | // Tab picker styling | |
| 128 | 128 | sb.WriteString(generateTabPickerCSS(p)) | |
| 129 | + | sb.WriteString("\n") | |
| 130 | + | ||
| 131 | + | // Accent picker styling | |
| 132 | + | sb.WriteString(generateAccentPickerCSS(p)) | |
| 129 | 133 | ||
| 130 | 134 | return sb.String() | |
| 131 | 135 | } | |
| @@ -126,6 +126,10 @@ func GenerateCSSFull(p Palette, scale float64, fonts FontConfig, modeColors Mode | |||||
| 126 | 126 | ||||
| 127 | // Tab picker styling | 127 | // Tab picker styling | ||
| 128 | sb.WriteString(generateTabPickerCSS(p)) | 128 | sb.WriteString(generateTabPickerCSS(p)) | ||
| 129 | + | sb.WriteString("\n") | |||
| 130 | + | ||||
| 131 | + | // Accent picker styling | |||
| 132 | + | sb.WriteString(generateAccentPickerCSS(p)) | |||
| 129 | 133 | ||||
| 130 | return sb.String() | 134 | return sb.String() | ||
| 131 | } | 135 | } | ||