fix(favorites): harden tag interaction lifecycle
- commit
- 6c7260d63057efc2652b59a4ee4277170c3b5e72
- author
- brice · · 2026-08-12 08:07 UTC
- parent
- 0f99bd1
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
6 files · +91 -20
M internal/ui/component/favorites_sidebar.go +5 -4
| @@ -72,6 +72,7 @@ type FavoritesSidebar struct { | |||
| 72 | 72 | retainedCallbacks []any | |
| 73 | 73 | tagCallbacks []any | |
| 74 | 74 | tagPromptCallbacks []any | |
| 75 | + | formCallbacks []any | |
| 75 | 76 | formTagMatchCallbacks []any | |
| 76 | 77 | ctx context.Context | |
| 77 | 78 | cancel context.CancelFunc | |
| @@ -232,7 +233,7 @@ func (fs *FavoritesSidebar) createOuterAndSearch() error { | |||
| 232 | 233 | } | |
| 233 | 234 | fs.searchEntry.AddCssClass("sidebar-search") | |
| 234 | 235 | fs.searchEntry.AddCssClass("favorites-sidebar-search") | |
| 235 | - | fs.trackTextInputFocus(&fs.searchEntry.Widget) | |
| 236 | + | fs.trackTextInputFocus(&fs.searchEntry.Widget, &fs.retainedCallbacks) | |
| 236 | 237 | fs.searchEntry.SetHexpand(true) | |
| 237 | 238 | placeholder := "Search favorites..." | |
| 238 | 239 | fs.searchEntry.SetPlaceholderText(&placeholder) | |
| @@ -259,8 +260,8 @@ func (fs *FavoritesSidebar) createTagControls() error { | |||
| 259 | 260 | return nil | |
| 260 | 261 | } | |
| 261 | 262 | ||
| 262 | - | func (fs *FavoritesSidebar) trackTextInputFocus(widget *gtk.Widget) { | |
| 263 | - | if fs == nil || widget == nil { | |
| 263 | + | func (fs *FavoritesSidebar) trackTextInputFocus(widget *gtk.Widget, callbacks *[]any) { | |
| 264 | + | if fs == nil || widget == nil || callbacks == nil { | |
| 264 | 265 | return | |
| 265 | 266 | } | |
| 266 | 267 | controller := gtk.NewEventControllerFocus() | |
| @@ -269,7 +270,7 @@ func (fs *FavoritesSidebar) trackTextInputFocus(widget *gtk.Widget) { | |||
| 269 | 270 | } | |
| 270 | 271 | entered := func(gtk.EventControllerFocus) { fs.setTextInputFocused(widget) } | |
| 271 | 272 | left := func(gtk.EventControllerFocus) { fs.clearTextInputFocus(widget) } | |
| 272 | - | fs.retainedCallbacks = append(fs.retainedCallbacks, entered, left) | |
| 273 | + | *callbacks = append(*callbacks, entered, left) | |
| 273 | 274 | controller.ConnectEnter(&entered) | |
| 274 | 275 | controller.ConnectLeave(&left) | |
| 275 | 276 | widget.AddController(&controller.EventController) | |
| @@ -72,6 +72,7 @@ type FavoritesSidebar struct { | |||||
| 72 | retainedCallbacks []any | 72 | retainedCallbacks []any | ||
| 73 | tagCallbacks []any | 73 | tagCallbacks []any | ||
| 74 | tagPromptCallbacks []any | 74 | tagPromptCallbacks []any | ||
| 75 | + | formCallbacks []any | |||
| 75 | formTagMatchCallbacks []any | 76 | formTagMatchCallbacks []any | ||
| 76 | ctx context.Context | 77 | ctx context.Context | ||
| 77 | cancel context.CancelFunc | 78 | cancel context.CancelFunc | ||
| @@ -232,7 +233,7 @@ func (fs *FavoritesSidebar) createOuterAndSearch() error { | |||||
| 232 | } | 233 | } | ||
| 233 | fs.searchEntry.AddCssClass("sidebar-search") | 234 | fs.searchEntry.AddCssClass("sidebar-search") | ||
| 234 | fs.searchEntry.AddCssClass("favorites-sidebar-search") | 235 | fs.searchEntry.AddCssClass("favorites-sidebar-search") | ||
| 235 | - | fs.trackTextInputFocus(&fs.searchEntry.Widget) | 236 | + | fs.trackTextInputFocus(&fs.searchEntry.Widget, &fs.retainedCallbacks) |
| 236 | fs.searchEntry.SetHexpand(true) | 237 | fs.searchEntry.SetHexpand(true) | ||
| 237 | placeholder := "Search favorites..." | 238 | placeholder := "Search favorites..." | ||
| 238 | fs.searchEntry.SetPlaceholderText(&placeholder) | 239 | fs.searchEntry.SetPlaceholderText(&placeholder) | ||
| @@ -259,8 +260,8 @@ func (fs *FavoritesSidebar) createTagControls() error { | |||||
| 259 | return nil | 260 | return nil | ||
| 260 | } | 261 | } | ||
| 261 | 262 | ||||
| 262 | - | func (fs *FavoritesSidebar) trackTextInputFocus(widget *gtk.Widget) { | 263 | + | func (fs *FavoritesSidebar) trackTextInputFocus(widget *gtk.Widget, callbacks *[]any) { |
| 263 | - | if fs == nil || widget == nil { | 264 | + | if fs == nil || widget == nil || callbacks == nil { |
| 264 | return | 265 | return | ||
| 265 | } | 266 | } | ||
| 266 | controller := gtk.NewEventControllerFocus() | 267 | controller := gtk.NewEventControllerFocus() | ||
| @@ -269,7 +270,7 @@ func (fs *FavoritesSidebar) trackTextInputFocus(widget *gtk.Widget) { | |||||
| 269 | } | 270 | } | ||
| 270 | entered := func(gtk.EventControllerFocus) { fs.setTextInputFocused(widget) } | 271 | entered := func(gtk.EventControllerFocus) { fs.setTextInputFocused(widget) } | ||
| 271 | left := func(gtk.EventControllerFocus) { fs.clearTextInputFocus(widget) } | 272 | left := func(gtk.EventControllerFocus) { fs.clearTextInputFocus(widget) } | ||
| 272 | - | fs.retainedCallbacks = append(fs.retainedCallbacks, entered, left) | 273 | + | *callbacks = append(*callbacks, entered, left) |
| 273 | controller.ConnectEnter(&entered) | 274 | controller.ConnectEnter(&entered) | ||
| 274 | controller.ConnectLeave(&left) | 275 | controller.ConnectLeave(&left) | ||
| 275 | widget.AddController(&controller.EventController) | 276 | widget.AddController(&controller.EventController) | ||
M internal/ui/component/favorites_sidebar_form_tags.go +10 -1
| @@ -28,7 +28,7 @@ func (fs *FavoritesSidebar) setupFormTagSearch() { | |||
| 28 | 28 | return | |
| 29 | 29 | } | |
| 30 | 30 | changed := func(_ gtk.SearchEntry) { fs.renderFormTagMatches(fs.formTagSearch.GetText()) } | |
| 31 | - | fs.retainedCallbacks = append(fs.retainedCallbacks, changed) | |
| 31 | + | fs.formCallbacks = append(fs.formCallbacks, changed) | |
| 32 | 32 | fs.formTagSearch.ConnectSearchChanged(&changed) | |
| 33 | 33 | } | |
| 34 | 34 | ||
| @@ -149,23 +149,32 @@ func (fs *FavoritesSidebar) updateFavoriteTags(favoriteID entity.FavoriteID, wan | |||
| 149 | 149 | wantedSet[id] = struct{}{} | |
| 150 | 150 | } | |
| 151 | 151 | currentSet := tagIDSet(current) | |
| 152 | + | mutated := false | |
| 152 | 153 | for id := range currentSet { | |
| 153 | 154 | if _, ok := wantedSet[id]; ok { | |
| 154 | 155 | continue | |
| 155 | 156 | } | |
| 156 | 157 | if err := uc.UntagFavorite(ctx, favoriteID, id); err != nil { | |
| 158 | + | if mutated { | |
| 159 | + | fs.startLoad() | |
| 160 | + | } | |
| 157 | 161 | fs.setNotice(err.Error()) | |
| 158 | 162 | return false | |
| 159 | 163 | } | |
| 164 | + | mutated = true | |
| 160 | 165 | } | |
| 161 | 166 | for id := range wantedSet { | |
| 162 | 167 | if _, ok := currentSet[id]; ok { | |
| 163 | 168 | continue | |
| 164 | 169 | } | |
| 165 | 170 | if err := uc.TagFavorite(ctx, favoriteID, id); err != nil { | |
| 171 | + | if mutated { | |
| 172 | + | fs.startLoad() | |
| 173 | + | } | |
| 166 | 174 | fs.setNotice(err.Error()) | |
| 167 | 175 | return false | |
| 168 | 176 | } | |
| 177 | + | mutated = true | |
| 169 | 178 | } | |
| 170 | 179 | return true | |
| 171 | 180 | } | |
| @@ -28,7 +28,7 @@ func (fs *FavoritesSidebar) setupFormTagSearch() { | |||||
| 28 | return | 28 | return | ||
| 29 | } | 29 | } | ||
| 30 | changed := func(_ gtk.SearchEntry) { fs.renderFormTagMatches(fs.formTagSearch.GetText()) } | 30 | changed := func(_ gtk.SearchEntry) { fs.renderFormTagMatches(fs.formTagSearch.GetText()) } | ||
| 31 | - | fs.retainedCallbacks = append(fs.retainedCallbacks, changed) | 31 | + | fs.formCallbacks = append(fs.formCallbacks, changed) |
| 32 | fs.formTagSearch.ConnectSearchChanged(&changed) | 32 | fs.formTagSearch.ConnectSearchChanged(&changed) | ||
| 33 | } | 33 | } | ||
| 34 | 34 | ||||
| @@ -149,23 +149,32 @@ func (fs *FavoritesSidebar) updateFavoriteTags(favoriteID entity.FavoriteID, wan | |||||
| 149 | wantedSet[id] = struct{}{} | 149 | wantedSet[id] = struct{}{} | ||
| 150 | } | 150 | } | ||
| 151 | currentSet := tagIDSet(current) | 151 | currentSet := tagIDSet(current) | ||
| 152 | + | mutated := false | |||
| 152 | for id := range currentSet { | 153 | for id := range currentSet { | ||
| 153 | if _, ok := wantedSet[id]; ok { | 154 | if _, ok := wantedSet[id]; ok { | ||
| 154 | continue | 155 | continue | ||
| 155 | } | 156 | } | ||
| 156 | if err := uc.UntagFavorite(ctx, favoriteID, id); err != nil { | 157 | if err := uc.UntagFavorite(ctx, favoriteID, id); err != nil { | ||
| 158 | + | if mutated { | |||
| 159 | + | fs.startLoad() | |||
| 160 | + | } | |||
| 157 | fs.setNotice(err.Error()) | 161 | fs.setNotice(err.Error()) | ||
| 158 | return false | 162 | return false | ||
| 159 | } | 163 | } | ||
| 164 | + | mutated = true | |||
| 160 | } | 165 | } | ||
| 161 | for id := range wantedSet { | 166 | for id := range wantedSet { | ||
| 162 | if _, ok := currentSet[id]; ok { | 167 | if _, ok := currentSet[id]; ok { | ||
| 163 | continue | 168 | continue | ||
| 164 | } | 169 | } | ||
| 165 | if err := uc.TagFavorite(ctx, favoriteID, id); err != nil { | 170 | if err := uc.TagFavorite(ctx, favoriteID, id); err != nil { | ||
| 171 | + | if mutated { | |||
| 172 | + | fs.startLoad() | |||
| 173 | + | } | |||
| 166 | fs.setNotice(err.Error()) | 174 | fs.setNotice(err.Error()) | ||
| 167 | return false | 175 | return false | ||
| 168 | } | 176 | } | ||
| 177 | + | mutated = true | |||
| 169 | } | 178 | } | ||
| 170 | return true | 179 | return true | ||
| 171 | } | 180 | } | ||
M internal/ui/component/favorites_sidebar_forms.go +9 -6
| @@ -75,12 +75,16 @@ func (fs *FavoritesSidebar) renderForm(fav *entity.Favorite) { | |||
| 75 | 75 | return | |
| 76 | 76 | } | |
| 77 | 77 | clearBoxChildren(fs.formBox) | |
| 78 | + | fs.formCallbacks = nil | |
| 79 | + | fs.formTagMatchCallbacks = nil | |
| 78 | 80 | fs.formURLEntry = gtk.NewSearchEntry() | |
| 79 | 81 | fs.formTitleEntry = gtk.NewSearchEntry() | |
| 80 | 82 | fs.formTagSearch = gtk.NewSearchEntry() | |
| 81 | 83 | fs.formTagMatches = gtk.NewBox(gtk.OrientationHorizontalValue, 3) | |
| 82 | 84 | fs.formShortcutEntry = gtk.NewSearchEntry() | |
| 85 | + | fs.mu.Lock() | |
| 83 | 86 | fs.formTagIDs = tagIDSet(favoriteTags(fav)) | |
| 87 | + | fs.mu.Unlock() | |
| 84 | 88 | entries := []struct { | |
| 85 | 89 | entry *gtk.SearchEntry | |
| 86 | 90 | label string | |
| @@ -96,7 +100,7 @@ func (fs *FavoritesSidebar) renderForm(fav *entity.Favorite) { | |||
| 96 | 100 | } | |
| 97 | 101 | placeholder := item.label | |
| 98 | 102 | item.entry.SetPlaceholderText(&placeholder) | |
| 99 | - | fs.trackTextInputFocus(&item.entry.Widget) | |
| 103 | + | fs.trackTextInputFocus(&item.entry.Widget, &fs.formCallbacks) | |
| 100 | 104 | fs.formBox.Append(&item.entry.Widget) | |
| 101 | 105 | } | |
| 102 | 106 | if fs.formTagMatches != nil { | |
| @@ -109,7 +113,7 @@ func (fs *FavoritesSidebar) renderForm(fav *entity.Favorite) { | |||
| 109 | 113 | cb := func(_ gtk.Button) { | |
| 110 | 114 | fs.submitForm() | |
| 111 | 115 | } | |
| 112 | - | fs.retainedCallbacks = append(fs.retainedCallbacks, cb) | |
| 116 | + | fs.formCallbacks = append(fs.formCallbacks, cb) | |
| 113 | 117 | fs.formSaveButton.ConnectClicked(&cb) | |
| 114 | 118 | fs.formBox.Append(&fs.formSaveButton.Widget) | |
| 115 | 119 | } | |
| @@ -179,12 +183,11 @@ func (fs *FavoritesSidebar) cancelManagement() bool { | |||
| 179 | 183 | if fs.formBox != nil { | |
| 180 | 184 | fs.formBox.SetVisible(false) | |
| 181 | 185 | clearBoxChildren(fs.formBox) | |
| 186 | + | fs.formCallbacks = nil | |
| 187 | + | fs.formTagMatchCallbacks = nil | |
| 182 | 188 | } | |
| 183 | 189 | if fs.tagPromptBox != nil { | |
| 184 | - | fs.clearTagPromptContent() | |
| 185 | - | fs.tagPromptBox.SetVisible(false) | |
| 186 | - | fs.tagNameEntry = nil | |
| 187 | - | fs.tagPromptSaveBtn = nil | |
| 190 | + | fs.hideTagPrompt() | |
| 188 | 191 | } | |
| 189 | 192 | fs.rebuildList() | |
| 190 | 193 | return active | |
| @@ -75,12 +75,16 @@ func (fs *FavoritesSidebar) renderForm(fav *entity.Favorite) { | |||||
| 75 | return | 75 | return | ||
| 76 | } | 76 | } | ||
| 77 | clearBoxChildren(fs.formBox) | 77 | clearBoxChildren(fs.formBox) | ||
| 78 | + | fs.formCallbacks = nil | |||
| 79 | + | fs.formTagMatchCallbacks = nil | |||
| 78 | fs.formURLEntry = gtk.NewSearchEntry() | 80 | fs.formURLEntry = gtk.NewSearchEntry() | ||
| 79 | fs.formTitleEntry = gtk.NewSearchEntry() | 81 | fs.formTitleEntry = gtk.NewSearchEntry() | ||
| 80 | fs.formTagSearch = gtk.NewSearchEntry() | 82 | fs.formTagSearch = gtk.NewSearchEntry() | ||
| 81 | fs.formTagMatches = gtk.NewBox(gtk.OrientationHorizontalValue, 3) | 83 | fs.formTagMatches = gtk.NewBox(gtk.OrientationHorizontalValue, 3) | ||
| 82 | fs.formShortcutEntry = gtk.NewSearchEntry() | 84 | fs.formShortcutEntry = gtk.NewSearchEntry() | ||
| 85 | + | fs.mu.Lock() | |||
| 83 | fs.formTagIDs = tagIDSet(favoriteTags(fav)) | 86 | fs.formTagIDs = tagIDSet(favoriteTags(fav)) | ||
| 87 | + | fs.mu.Unlock() | |||
| 84 | entries := []struct { | 88 | entries := []struct { | ||
| 85 | entry *gtk.SearchEntry | 89 | entry *gtk.SearchEntry | ||
| 86 | label string | 90 | label string | ||
| @@ -96,7 +100,7 @@ func (fs *FavoritesSidebar) renderForm(fav *entity.Favorite) { | |||||
| 96 | } | 100 | } | ||
| 97 | placeholder := item.label | 101 | placeholder := item.label | ||
| 98 | item.entry.SetPlaceholderText(&placeholder) | 102 | item.entry.SetPlaceholderText(&placeholder) | ||
| 99 | - | fs.trackTextInputFocus(&item.entry.Widget) | 103 | + | fs.trackTextInputFocus(&item.entry.Widget, &fs.formCallbacks) |
| 100 | fs.formBox.Append(&item.entry.Widget) | 104 | fs.formBox.Append(&item.entry.Widget) | ||
| 101 | } | 105 | } | ||
| 102 | if fs.formTagMatches != nil { | 106 | if fs.formTagMatches != nil { | ||
| @@ -109,7 +113,7 @@ func (fs *FavoritesSidebar) renderForm(fav *entity.Favorite) { | |||||
| 109 | cb := func(_ gtk.Button) { | 113 | cb := func(_ gtk.Button) { | ||
| 110 | fs.submitForm() | 114 | fs.submitForm() | ||
| 111 | } | 115 | } | ||
| 112 | - | fs.retainedCallbacks = append(fs.retainedCallbacks, cb) | 116 | + | fs.formCallbacks = append(fs.formCallbacks, cb) |
| 113 | fs.formSaveButton.ConnectClicked(&cb) | 117 | fs.formSaveButton.ConnectClicked(&cb) | ||
| 114 | fs.formBox.Append(&fs.formSaveButton.Widget) | 118 | fs.formBox.Append(&fs.formSaveButton.Widget) | ||
| 115 | } | 119 | } | ||
| @@ -179,12 +183,11 @@ func (fs *FavoritesSidebar) cancelManagement() bool { | |||||
| 179 | if fs.formBox != nil { | 183 | if fs.formBox != nil { | ||
| 180 | fs.formBox.SetVisible(false) | 184 | fs.formBox.SetVisible(false) | ||
| 181 | clearBoxChildren(fs.formBox) | 185 | clearBoxChildren(fs.formBox) | ||
| 186 | + | fs.formCallbacks = nil | |||
| 187 | + | fs.formTagMatchCallbacks = nil | |||
| 182 | } | 188 | } | ||
| 183 | if fs.tagPromptBox != nil { | 189 | if fs.tagPromptBox != nil { | ||
| 184 | - | fs.clearTagPromptContent() | 190 | + | fs.hideTagPrompt() |
| 185 | - | fs.tagPromptBox.SetVisible(false) | |||
| 186 | - | fs.tagNameEntry = nil | |||
| 187 | - | fs.tagPromptSaveBtn = nil | |||
| 188 | } | 191 | } | ||
| 189 | fs.rebuildList() | 192 | fs.rebuildList() | ||
| 190 | return active | 193 | return active | ||
M internal/ui/component/favorites_sidebar_keyboard.go +3 -2
| @@ -307,9 +307,10 @@ func shouldFocusSearchForSlash(searchFocused bool) bool { return !searchFocused | |||
| 307 | 307 | ||
| 308 | 308 | // shouldDeferToTextInput keeps the sidebar controller from treating text typed | |
| 309 | 309 | // into a descendant entry as a global sidebar command. | |
| 310 | - | // Tab remains the one sidebar command that deliberately traverses focus. | |
| 310 | + | // Tab traverses focus and Escape cancels sidebar management while text is focused. | |
| 311 | 311 | func shouldDeferToTextInput(textEditing bool, keyval uint) bool { | |
| 312 | - | return textEditing && keyval != uint(gdk.KEY_Tab) && keyval != uint(gdk.KEY_ISO_Left_Tab) | |
| 312 | + | return textEditing && keyval != uint(gdk.KEY_Tab) && | |
| 313 | + | keyval != uint(gdk.KEY_ISO_Left_Tab) && keyval != uint(gdk.KEY_Escape) | |
| 313 | 314 | } | |
| 314 | 315 | ||
| 315 | 316 | func (fs *FavoritesSidebar) routeDirectionalFocus(keyval uint) bool { | |
| @@ -307,9 +307,10 @@ func shouldFocusSearchForSlash(searchFocused bool) bool { return !searchFocused | |||||
| 307 | 307 | ||||
| 308 | // shouldDeferToTextInput keeps the sidebar controller from treating text typed | 308 | // shouldDeferToTextInput keeps the sidebar controller from treating text typed | ||
| 309 | // into a descendant entry as a global sidebar command. | 309 | // into a descendant entry as a global sidebar command. | ||
| 310 | - | // Tab remains the one sidebar command that deliberately traverses focus. | 310 | + | // Tab traverses focus and Escape cancels sidebar management while text is focused. |
| 311 | func shouldDeferToTextInput(textEditing bool, keyval uint) bool { | 311 | func shouldDeferToTextInput(textEditing bool, keyval uint) bool { | ||
| 312 | - | return textEditing && keyval != uint(gdk.KEY_Tab) && keyval != uint(gdk.KEY_ISO_Left_Tab) | 312 | + | return textEditing && keyval != uint(gdk.KEY_Tab) && |
| 313 | + | keyval != uint(gdk.KEY_ISO_Left_Tab) && keyval != uint(gdk.KEY_Escape) | |||
| 313 | } | 314 | } | ||
| 314 | 315 | ||||
| 315 | func (fs *FavoritesSidebar) routeDirectionalFocus(keyval uint) bool { | 316 | func (fs *FavoritesSidebar) routeDirectionalFocus(keyval uint) bool { | ||
M internal/ui/component/favorites_sidebar_tags.go +18 -6
| @@ -12,7 +12,7 @@ func (fs *FavoritesSidebar) showCreateTagPrompt() { | |||
| 12 | 12 | if fs == nil || fs.tagPromptBox == nil { | |
| 13 | 13 | return | |
| 14 | 14 | } | |
| 15 | - | fs.clearTagPromptContent() | |
| 15 | + | fs.hideTagPrompt() | |
| 16 | 16 | entry := gtk.NewEntry() | |
| 17 | 17 | if entry == nil { | |
| 18 | 18 | return | |
| @@ -20,15 +20,17 @@ func (fs *FavoritesSidebar) showCreateTagPrompt() { | |||
| 20 | 20 | placeholder := "New tag name" | |
| 21 | 21 | entry.SetPlaceholderText(&placeholder) | |
| 22 | 22 | entry.AddCssClass("favorites-sidebar-tag-name") | |
| 23 | - | fs.trackTextInputFocus(&entry.Widget) | |
| 23 | + | callbacks := make([]any, 0, 4) | |
| 24 | + | fs.trackTextInputFocus(&entry.Widget, &callbacks) | |
| 24 | 25 | entry.SetHexpand(true) | |
| 25 | - | callbacks := make([]any, 0, 2) | |
| 26 | 26 | activateCb := func(gtk.Entry) { fs.submitTagName(entry.GetText()) } | |
| 27 | 27 | callbacks = append(callbacks, activateCb) | |
| 28 | 28 | entry.ConnectActivate(&activateCb) | |
| 29 | + | fs.storeTagPromptCallbacks(callbacks) | |
| 29 | 30 | ||
| 30 | 31 | save := gtk.NewButtonWithLabel("Create") | |
| 31 | 32 | if save == nil { | |
| 33 | + | fs.hideTagPrompt() | |
| 32 | 34 | return | |
| 33 | 35 | } | |
| 34 | 36 | save.AddCssClass("favorites-sidebar-tag-create") | |
| @@ -201,22 +203,32 @@ func favoriteHasTag(fav *entity.Favorite, tagID entity.TagID) bool { | |||
| 201 | 203 | } | |
| 202 | 204 | ||
| 203 | 205 | func (fs *FavoritesSidebar) focusTagControl(index int) { | |
| 204 | - | if fs == nil || index < 0 || index >= len(fs.tagControls) || fs.tagControls[index] == nil { | |
| 206 | + | if fs == nil || index < 0 { | |
| 205 | 207 | return | |
| 206 | 208 | } | |
| 209 | + | fs.mu.RLock() | |
| 210 | + | controls := append([]*gtk.Button(nil), fs.tagControls...) | |
| 211 | + | fs.mu.RUnlock() | |
| 212 | + | if index >= len(controls) || controls[index] == nil { | |
| 213 | + | return | |
| 214 | + | } | |
| 215 | + | control := controls[index] | |
| 207 | 216 | fs.mu.Lock() | |
| 208 | 217 | if !fs.destroyed { | |
| 209 | 218 | fs.focusZone = favoritesSidebarFocusTags | |
| 210 | 219 | } | |
| 211 | 220 | fs.mu.Unlock() | |
| 212 | - | fs.tagControls[index].GrabFocus() | |
| 221 | + | control.GrabFocus() | |
| 213 | 222 | } | |
| 214 | 223 | ||
| 215 | 224 | func (fs *FavoritesSidebar) focusedTagControlIndex() int { | |
| 216 | 225 | if fs == nil { | |
| 217 | 226 | return -1 | |
| 218 | 227 | } | |
| 219 | - | for index, control := range fs.tagControls { | |
| 228 | + | fs.mu.RLock() | |
| 229 | + | controls := append([]*gtk.Button(nil), fs.tagControls...) | |
| 230 | + | fs.mu.RUnlock() | |
| 231 | + | for index, control := range controls { | |
| 220 | 232 | if control != nil && control.HasFocus() { | |
| 221 | 233 | return index | |
| 222 | 234 | } | |
| @@ -12,7 +12,7 @@ func (fs *FavoritesSidebar) showCreateTagPrompt() { | |||||
| 12 | if fs == nil || fs.tagPromptBox == nil { | 12 | if fs == nil || fs.tagPromptBox == nil { | ||
| 13 | return | 13 | return | ||
| 14 | } | 14 | } | ||
| 15 | - | fs.clearTagPromptContent() | 15 | + | fs.hideTagPrompt() |
| 16 | entry := gtk.NewEntry() | 16 | entry := gtk.NewEntry() | ||
| 17 | if entry == nil { | 17 | if entry == nil { | ||
| 18 | return | 18 | return | ||
| @@ -20,15 +20,17 @@ func (fs *FavoritesSidebar) showCreateTagPrompt() { | |||||
| 20 | placeholder := "New tag name" | 20 | placeholder := "New tag name" | ||
| 21 | entry.SetPlaceholderText(&placeholder) | 21 | entry.SetPlaceholderText(&placeholder) | ||
| 22 | entry.AddCssClass("favorites-sidebar-tag-name") | 22 | entry.AddCssClass("favorites-sidebar-tag-name") | ||
| 23 | - | fs.trackTextInputFocus(&entry.Widget) | 23 | + | callbacks := make([]any, 0, 4) |
| 24 | + | fs.trackTextInputFocus(&entry.Widget, &callbacks) | |||
| 24 | entry.SetHexpand(true) | 25 | entry.SetHexpand(true) | ||
| 25 | - | callbacks := make([]any, 0, 2) | |||
| 26 | activateCb := func(gtk.Entry) { fs.submitTagName(entry.GetText()) } | 26 | activateCb := func(gtk.Entry) { fs.submitTagName(entry.GetText()) } | ||
| 27 | callbacks = append(callbacks, activateCb) | 27 | callbacks = append(callbacks, activateCb) | ||
| 28 | entry.ConnectActivate(&activateCb) | 28 | entry.ConnectActivate(&activateCb) | ||
| 29 | + | fs.storeTagPromptCallbacks(callbacks) | |||
| 29 | 30 | ||||
| 30 | save := gtk.NewButtonWithLabel("Create") | 31 | save := gtk.NewButtonWithLabel("Create") | ||
| 31 | if save == nil { | 32 | if save == nil { | ||
| 33 | + | fs.hideTagPrompt() | |||
| 32 | return | 34 | return | ||
| 33 | } | 35 | } | ||
| 34 | save.AddCssClass("favorites-sidebar-tag-create") | 36 | save.AddCssClass("favorites-sidebar-tag-create") | ||
| @@ -201,22 +203,32 @@ func favoriteHasTag(fav *entity.Favorite, tagID entity.TagID) bool { | |||||
| 201 | } | 203 | } | ||
| 202 | 204 | ||||
| 203 | func (fs *FavoritesSidebar) focusTagControl(index int) { | 205 | func (fs *FavoritesSidebar) focusTagControl(index int) { | ||
| 204 | - | if fs == nil || index < 0 || index >= len(fs.tagControls) || fs.tagControls[index] == nil { | 206 | + | if fs == nil || index < 0 { |
| 205 | return | 207 | return | ||
| 206 | } | 208 | } | ||
| 209 | + | fs.mu.RLock() | |||
| 210 | + | controls := append([]*gtk.Button(nil), fs.tagControls...) | |||
| 211 | + | fs.mu.RUnlock() | |||
| 212 | + | if index >= len(controls) || controls[index] == nil { | |||
| 213 | + | return | |||
| 214 | + | } | |||
| 215 | + | control := controls[index] | |||
| 207 | fs.mu.Lock() | 216 | fs.mu.Lock() | ||
| 208 | if !fs.destroyed { | 217 | if !fs.destroyed { | ||
| 209 | fs.focusZone = favoritesSidebarFocusTags | 218 | fs.focusZone = favoritesSidebarFocusTags | ||
| 210 | } | 219 | } | ||
| 211 | fs.mu.Unlock() | 220 | fs.mu.Unlock() | ||
| 212 | - | fs.tagControls[index].GrabFocus() | 221 | + | control.GrabFocus() |
| 213 | } | 222 | } | ||
| 214 | 223 | ||||
| 215 | func (fs *FavoritesSidebar) focusedTagControlIndex() int { | 224 | func (fs *FavoritesSidebar) focusedTagControlIndex() int { | ||
| 216 | if fs == nil { | 225 | if fs == nil { | ||
| 217 | return -1 | 226 | return -1 | ||
| 218 | } | 227 | } | ||
| 219 | - | for index, control := range fs.tagControls { | 228 | + | fs.mu.RLock() |
| 229 | + | controls := append([]*gtk.Button(nil), fs.tagControls...) | |||
| 230 | + | fs.mu.RUnlock() | |||
| 231 | + | for index, control := range controls { | |||
| 220 | if control != nil && control.HasFocus() { | 232 | if control != nil && control.HasFocus() { | ||
| 221 | return index | 233 | return index | ||
| 222 | } | 234 | } | ||
M internal/ui/component/favorites_sidebar_test.go +46 -1
| @@ -41,6 +41,9 @@ type fakeFavoritesSidebarUC struct { | |||
| 41 | 41 | } | |
| 42 | 42 | err error | |
| 43 | 43 | errOnSetShortcut error | |
| 44 | + | errOnTag map[entity.TagID]error | |
| 45 | + | errOnUntag map[entity.TagID]error | |
| 46 | + | failUntagOnCall int | |
| 44 | 47 | getAllCalled chan struct{} | |
| 45 | 48 | } | |
| 46 | 49 | ||
| @@ -99,6 +102,9 @@ func (f *fakeFavoritesSidebarUC) TagFavorite(_ context.Context, favID entity.Fav | |||
| 99 | 102 | favID entity.FavoriteID | |
| 100 | 103 | tagID entity.TagID | |
| 101 | 104 | }{favID: favID, tagID: tagID}) | |
| 105 | + | if err := f.errOnTag[tagID]; err != nil { | |
| 106 | + | return err | |
| 107 | + | } | |
| 102 | 108 | return f.err | |
| 103 | 109 | } | |
| 104 | 110 | func (f *fakeFavoritesSidebarUC) UntagFavorite(_ context.Context, favID entity.FavoriteID, tagID entity.TagID) error { | |
| @@ -106,6 +112,12 @@ func (f *fakeFavoritesSidebarUC) UntagFavorite(_ context.Context, favID entity.F | |||
| 106 | 112 | favID entity.FavoriteID | |
| 107 | 113 | tagID entity.TagID | |
| 108 | 114 | }{favID: favID, tagID: tagID}) | |
| 115 | + | if err := f.errOnUntag[tagID]; err != nil { | |
| 116 | + | return err | |
| 117 | + | } | |
| 118 | + | if f.failUntagOnCall > 0 && len(f.untagged) == f.failUntagOnCall { | |
| 119 | + | return errors.New("untag failed") | |
| 120 | + | } | |
| 109 | 121 | return f.err | |
| 110 | 122 | } | |
| 111 | 123 | ||
| @@ -412,6 +424,22 @@ func TestFormTagCandidatesMatchesNamesAndKeepsSelectedTagsVisible(t *testing.T) | |||
| 412 | 424 | assert.Equal(t, []*entity.Tag{reading}, formTagCandidates([]*entity.Tag{dev, news, reading}, selected, "read")) | |
| 413 | 425 | } | |
| 414 | 426 | ||
| 427 | + | func TestFavoritesSidebarReloadsAfterPartiallyApplyingTagChanges(t *testing.T) { | |
| 428 | + | first := entity.Tag{ID: 10, Name: "first"} | |
| 429 | + | second := entity.Tag{ID: 11, Name: "second"} | |
| 430 | + | fav := &entity.Favorite{ID: 5, URL: "https://go.dev", Tags: []entity.Tag{first, second}} | |
| 431 | + | fs := newFavoritesSidebarHarness([]*entity.Favorite{fav}, nil) | |
| 432 | + | uc := fs.favoritesUC.(*fakeFavoritesSidebarUC) | |
| 433 | + | uc.failUntagOnCall = 2 | |
| 434 | + | fs.allFavorites = []*entity.Favorite{fav} | |
| 435 | + | fs.loadGen = 4 | |
| 436 | + | ||
| 437 | + | assert.False(t, fs.updateFavoriteTags(fav.ID, nil)) | |
| 438 | + | assert.Len(t, uc.untagged, 2) | |
| 439 | + | assert.Equal(t, uint64(5), fs.loadGen, "a partial tag mutation must refresh stale favorites") | |
| 440 | + | assert.Equal(t, "untag failed", fs.notice) | |
| 441 | + | } | |
| 442 | + | ||
| 415 | 443 | func TestFavoritesSidebarFormTagSelectionAndEditSynchronization(t *testing.T) { | |
| 416 | 444 | dev := entity.Tag{ID: 10, Name: "dev"} | |
| 417 | 445 | news := entity.Tag{ID: 11, Name: "news"} | |
| @@ -426,21 +454,34 @@ func TestFavoritesSidebarFormTagSelectionAndEditSynchronization(t *testing.T) { | |||
| 426 | 454 | assert.True(t, fs.updateFavoriteTags(fav.ID, fs.formTagIDsSnapshot())) | |
| 427 | 455 | require.Len(t, uc.tagged, 1) | |
| 428 | 456 | assert.Equal(t, news.ID, uc.tagged[0].tagID) | |
| 457 | + | fav.Tags = []entity.Tag{dev, news} | |
| 429 | 458 | ||
| 430 | 459 | fs.toggleFormTag(dev.ID) | |
| 431 | 460 | assert.True(t, fs.updateFavoriteTags(fav.ID, fs.formTagIDsSnapshot())) | |
| 432 | 461 | require.Len(t, uc.untagged, 1) | |
| 433 | 462 | assert.Equal(t, dev.ID, uc.untagged[0].tagID) | |
| 463 | + | assert.Len(t, uc.tagged, 1, "the synchronized persisted tags must not be added again") | |
| 434 | 464 | } | |
| 435 | 465 | ||
| 436 | 466 | func TestFavoritesSidebarDirectionalFocusMovesDirectlyBetweenSearchAndFirstFavorite(t *testing.T) { | |
| 437 | 467 | fav := &entity.Favorite{ID: 1, URL: "https://go.dev"} | |
| 438 | 468 | fs := newFavoritesSidebarHarness([]*entity.Favorite{fav}, nil) | |
| 469 | + | if !gtk.InitCheck() { | |
| 470 | + | t.Skip("GTK native display prerequisite unavailable (gtk.InitCheck returned false)") | |
| 471 | + | } | |
| 472 | + | listBox := gtk.NewListBox() | |
| 473 | + | require.NotNil(t, listBox) | |
| 474 | + | row := gtk.NewListBoxRow() | |
| 475 | + | require.NotNil(t, row) | |
| 476 | + | row.SetSelectable(true) | |
| 477 | + | listBox.Append(&row.Widget) | |
| 478 | + | fs.listBox = listBox | |
| 439 | 479 | fs.displayRows = []favoriteSidebarDisplayRow{{FavoriteID: fav.ID, Favorite: fav, URL: fav.URL, Selectable: true}} | |
| 440 | 480 | fs.focusZone = favoritesSidebarFocusSearch | |
| 441 | 481 | ||
| 442 | 482 | assert.True(t, fs.routeDirectionalFocus(uint(gdk.KEY_Down))) | |
| 443 | 483 | assert.Equal(t, favoritesSidebarFocusList, fs.focusZone) | |
| 484 | + | assert.Equal(t, row, fs.listBox.GetSelectedRow()) | |
| 444 | 485 | assert.True(t, fs.routeDirectionalFocus(uint(gdk.KEY_Up))) | |
| 445 | 486 | assert.Equal(t, favoritesSidebarFocusSearch, fs.focusZone) | |
| 446 | 487 | } | |
| @@ -549,6 +590,10 @@ func TestFavoritesSidebarExplicitTextInputFocusControlsKeyboardRouting(t *testin | |||
| 549 | 590 | fs.setTextInputFocused(input) | |
| 550 | 591 | assert.True(t, fs.inTextEditContext()) | |
| 551 | 592 | assert.True(t, shouldDeferToTextInput(fs.inTextEditContext(), uint(gdk.KEY_e))) | |
| 593 | + | assert.False(t, fs.handleSingleKeyCommand(uint(gdk.KEY_e), 0)) | |
| 594 | + | fs.mode = favoritesSidebarModeAdd | |
| 595 | + | assert.True(t, fs.cancelManagement()) | |
| 596 | + | assert.Equal(t, favoritesSidebarModeNone, fs.mode) | |
| 552 | 597 | ||
| 553 | 598 | fs.clearTextInputFocus(input) | |
| 554 | 599 | assert.False(t, fs.inTextEditContext()) | |
| @@ -565,13 +610,13 @@ func TestFavoritesSidebarTextInputDefersSidebarCommands(t *testing.T) { | |||
| 565 | 610 | uint(gdk.KEY_t), | |
| 566 | 611 | uint(gdk.KEY_plus), | |
| 567 | 612 | uint(gdk.KEY_Up), | |
| 568 | - | uint(gdk.KEY_Escape), | |
| 569 | 613 | uint(gdk.KEY_Return), | |
| 570 | 614 | } { | |
| 571 | 615 | assert.Truef(t, shouldDeferToTextInput(true, key), "key %d must reach the text input", key) | |
| 572 | 616 | } | |
| 573 | 617 | assert.False(t, shouldDeferToTextInput(true, uint(gdk.KEY_Tab))) | |
| 574 | 618 | assert.False(t, shouldDeferToTextInput(true, uint(gdk.KEY_ISO_Left_Tab))) | |
| 619 | + | assert.False(t, shouldDeferToTextInput(true, uint(gdk.KEY_Escape))) | |
| 575 | 620 | assert.False(t, shouldDeferToTextInput(false, uint(gdk.KEY_e))) | |
| 576 | 621 | } | |
| 577 | 622 | ||
| @@ -41,6 +41,9 @@ type fakeFavoritesSidebarUC struct { | |||||
| 41 | } | 41 | } | ||
| 42 | err error | 42 | err error | ||
| 43 | errOnSetShortcut error | 43 | errOnSetShortcut error | ||
| 44 | + | errOnTag map[entity.TagID]error | |||
| 45 | + | errOnUntag map[entity.TagID]error | |||
| 46 | + | failUntagOnCall int | |||
| 44 | getAllCalled chan struct{} | 47 | getAllCalled chan struct{} | ||
| 45 | } | 48 | } | ||
| 46 | 49 | ||||
| @@ -99,6 +102,9 @@ func (f *fakeFavoritesSidebarUC) TagFavorite(_ context.Context, favID entity.Fav | |||||
| 99 | favID entity.FavoriteID | 102 | favID entity.FavoriteID | ||
| 100 | tagID entity.TagID | 103 | tagID entity.TagID | ||
| 101 | }{favID: favID, tagID: tagID}) | 104 | }{favID: favID, tagID: tagID}) | ||
| 105 | + | if err := f.errOnTag[tagID]; err != nil { | |||
| 106 | + | return err | |||
| 107 | + | } | |||
| 102 | return f.err | 108 | return f.err | ||
| 103 | } | 109 | } | ||
| 104 | func (f *fakeFavoritesSidebarUC) UntagFavorite(_ context.Context, favID entity.FavoriteID, tagID entity.TagID) error { | 110 | func (f *fakeFavoritesSidebarUC) UntagFavorite(_ context.Context, favID entity.FavoriteID, tagID entity.TagID) error { | ||
| @@ -106,6 +112,12 @@ func (f *fakeFavoritesSidebarUC) UntagFavorite(_ context.Context, favID entity.F | |||||
| 106 | favID entity.FavoriteID | 112 | favID entity.FavoriteID | ||
| 107 | tagID entity.TagID | 113 | tagID entity.TagID | ||
| 108 | }{favID: favID, tagID: tagID}) | 114 | }{favID: favID, tagID: tagID}) | ||
| 115 | + | if err := f.errOnUntag[tagID]; err != nil { | |||
| 116 | + | return err | |||
| 117 | + | } | |||
| 118 | + | if f.failUntagOnCall > 0 && len(f.untagged) == f.failUntagOnCall { | |||
| 119 | + | return errors.New("untag failed") | |||
| 120 | + | } | |||
| 109 | return f.err | 121 | return f.err | ||
| 110 | } | 122 | } | ||
| 111 | 123 | ||||
| @@ -412,6 +424,22 @@ func TestFormTagCandidatesMatchesNamesAndKeepsSelectedTagsVisible(t *testing.T) | |||||
| 412 | assert.Equal(t, []*entity.Tag{reading}, formTagCandidates([]*entity.Tag{dev, news, reading}, selected, "read")) | 424 | assert.Equal(t, []*entity.Tag{reading}, formTagCandidates([]*entity.Tag{dev, news, reading}, selected, "read")) | ||
| 413 | } | 425 | } | ||
| 414 | 426 | ||||
| 427 | + | func TestFavoritesSidebarReloadsAfterPartiallyApplyingTagChanges(t *testing.T) { | |||
| 428 | + | first := entity.Tag{ID: 10, Name: "first"} | |||
| 429 | + | second := entity.Tag{ID: 11, Name: "second"} | |||
| 430 | + | fav := &entity.Favorite{ID: 5, URL: "https://go.dev", Tags: []entity.Tag{first, second}} | |||
| 431 | + | fs := newFavoritesSidebarHarness([]*entity.Favorite{fav}, nil) | |||
| 432 | + | uc := fs.favoritesUC.(*fakeFavoritesSidebarUC) | |||
| 433 | + | uc.failUntagOnCall = 2 | |||
| 434 | + | fs.allFavorites = []*entity.Favorite{fav} | |||
| 435 | + | fs.loadGen = 4 | |||
| 436 | + | ||||
| 437 | + | assert.False(t, fs.updateFavoriteTags(fav.ID, nil)) | |||
| 438 | + | assert.Len(t, uc.untagged, 2) | |||
| 439 | + | assert.Equal(t, uint64(5), fs.loadGen, "a partial tag mutation must refresh stale favorites") | |||
| 440 | + | assert.Equal(t, "untag failed", fs.notice) | |||
| 441 | + | } | |||
| 442 | + | ||||
| 415 | func TestFavoritesSidebarFormTagSelectionAndEditSynchronization(t *testing.T) { | 443 | func TestFavoritesSidebarFormTagSelectionAndEditSynchronization(t *testing.T) { | ||
| 416 | dev := entity.Tag{ID: 10, Name: "dev"} | 444 | dev := entity.Tag{ID: 10, Name: "dev"} | ||
| 417 | news := entity.Tag{ID: 11, Name: "news"} | 445 | news := entity.Tag{ID: 11, Name: "news"} | ||
| @@ -426,21 +454,34 @@ func TestFavoritesSidebarFormTagSelectionAndEditSynchronization(t *testing.T) { | |||||
| 426 | assert.True(t, fs.updateFavoriteTags(fav.ID, fs.formTagIDsSnapshot())) | 454 | assert.True(t, fs.updateFavoriteTags(fav.ID, fs.formTagIDsSnapshot())) | ||
| 427 | require.Len(t, uc.tagged, 1) | 455 | require.Len(t, uc.tagged, 1) | ||
| 428 | assert.Equal(t, news.ID, uc.tagged[0].tagID) | 456 | assert.Equal(t, news.ID, uc.tagged[0].tagID) | ||
| 457 | + | fav.Tags = []entity.Tag{dev, news} | |||
| 429 | 458 | ||||
| 430 | fs.toggleFormTag(dev.ID) | 459 | fs.toggleFormTag(dev.ID) | ||
| 431 | assert.True(t, fs.updateFavoriteTags(fav.ID, fs.formTagIDsSnapshot())) | 460 | assert.True(t, fs.updateFavoriteTags(fav.ID, fs.formTagIDsSnapshot())) | ||
| 432 | require.Len(t, uc.untagged, 1) | 461 | require.Len(t, uc.untagged, 1) | ||
| 433 | assert.Equal(t, dev.ID, uc.untagged[0].tagID) | 462 | assert.Equal(t, dev.ID, uc.untagged[0].tagID) | ||
| 463 | + | assert.Len(t, uc.tagged, 1, "the synchronized persisted tags must not be added again") | |||
| 434 | } | 464 | } | ||
| 435 | 465 | ||||
| 436 | func TestFavoritesSidebarDirectionalFocusMovesDirectlyBetweenSearchAndFirstFavorite(t *testing.T) { | 466 | func TestFavoritesSidebarDirectionalFocusMovesDirectlyBetweenSearchAndFirstFavorite(t *testing.T) { | ||
| 437 | fav := &entity.Favorite{ID: 1, URL: "https://go.dev"} | 467 | fav := &entity.Favorite{ID: 1, URL: "https://go.dev"} | ||
| 438 | fs := newFavoritesSidebarHarness([]*entity.Favorite{fav}, nil) | 468 | fs := newFavoritesSidebarHarness([]*entity.Favorite{fav}, nil) | ||
| 469 | + | if !gtk.InitCheck() { | |||
| 470 | + | t.Skip("GTK native display prerequisite unavailable (gtk.InitCheck returned false)") | |||
| 471 | + | } | |||
| 472 | + | listBox := gtk.NewListBox() | |||
| 473 | + | require.NotNil(t, listBox) | |||
| 474 | + | row := gtk.NewListBoxRow() | |||
| 475 | + | require.NotNil(t, row) | |||
| 476 | + | row.SetSelectable(true) | |||
| 477 | + | listBox.Append(&row.Widget) | |||
| 478 | + | fs.listBox = listBox | |||
| 439 | fs.displayRows = []favoriteSidebarDisplayRow{{FavoriteID: fav.ID, Favorite: fav, URL: fav.URL, Selectable: true}} | 479 | fs.displayRows = []favoriteSidebarDisplayRow{{FavoriteID: fav.ID, Favorite: fav, URL: fav.URL, Selectable: true}} | ||
| 440 | fs.focusZone = favoritesSidebarFocusSearch | 480 | fs.focusZone = favoritesSidebarFocusSearch | ||
| 441 | 481 | ||||
| 442 | assert.True(t, fs.routeDirectionalFocus(uint(gdk.KEY_Down))) | 482 | assert.True(t, fs.routeDirectionalFocus(uint(gdk.KEY_Down))) | ||
| 443 | assert.Equal(t, favoritesSidebarFocusList, fs.focusZone) | 483 | assert.Equal(t, favoritesSidebarFocusList, fs.focusZone) | ||
| 484 | + | assert.Equal(t, row, fs.listBox.GetSelectedRow()) | |||
| 444 | assert.True(t, fs.routeDirectionalFocus(uint(gdk.KEY_Up))) | 485 | assert.True(t, fs.routeDirectionalFocus(uint(gdk.KEY_Up))) | ||
| 445 | assert.Equal(t, favoritesSidebarFocusSearch, fs.focusZone) | 486 | assert.Equal(t, favoritesSidebarFocusSearch, fs.focusZone) | ||
| 446 | } | 487 | } | ||
| @@ -549,6 +590,10 @@ func TestFavoritesSidebarExplicitTextInputFocusControlsKeyboardRouting(t *testin | |||||
| 549 | fs.setTextInputFocused(input) | 590 | fs.setTextInputFocused(input) | ||
| 550 | assert.True(t, fs.inTextEditContext()) | 591 | assert.True(t, fs.inTextEditContext()) | ||
| 551 | assert.True(t, shouldDeferToTextInput(fs.inTextEditContext(), uint(gdk.KEY_e))) | 592 | assert.True(t, shouldDeferToTextInput(fs.inTextEditContext(), uint(gdk.KEY_e))) | ||
| 593 | + | assert.False(t, fs.handleSingleKeyCommand(uint(gdk.KEY_e), 0)) | |||
| 594 | + | fs.mode = favoritesSidebarModeAdd | |||
| 595 | + | assert.True(t, fs.cancelManagement()) | |||
| 596 | + | assert.Equal(t, favoritesSidebarModeNone, fs.mode) | |||
| 552 | 597 | ||||
| 553 | fs.clearTextInputFocus(input) | 598 | fs.clearTextInputFocus(input) | ||
| 554 | assert.False(t, fs.inTextEditContext()) | 599 | assert.False(t, fs.inTextEditContext()) | ||
| @@ -565,13 +610,13 @@ func TestFavoritesSidebarTextInputDefersSidebarCommands(t *testing.T) { | |||||
| 565 | uint(gdk.KEY_t), | 610 | uint(gdk.KEY_t), | ||
| 566 | uint(gdk.KEY_plus), | 611 | uint(gdk.KEY_plus), | ||
| 567 | uint(gdk.KEY_Up), | 612 | uint(gdk.KEY_Up), | ||
| 568 | - | uint(gdk.KEY_Escape), | |||
| 569 | uint(gdk.KEY_Return), | 613 | uint(gdk.KEY_Return), | ||
| 570 | } { | 614 | } { | ||
| 571 | assert.Truef(t, shouldDeferToTextInput(true, key), "key %d must reach the text input", key) | 615 | assert.Truef(t, shouldDeferToTextInput(true, key), "key %d must reach the text input", key) | ||
| 572 | } | 616 | } | ||
| 573 | assert.False(t, shouldDeferToTextInput(true, uint(gdk.KEY_Tab))) | 617 | assert.False(t, shouldDeferToTextInput(true, uint(gdk.KEY_Tab))) | ||
| 574 | assert.False(t, shouldDeferToTextInput(true, uint(gdk.KEY_ISO_Left_Tab))) | 618 | assert.False(t, shouldDeferToTextInput(true, uint(gdk.KEY_ISO_Left_Tab))) | ||
| 619 | + | assert.False(t, shouldDeferToTextInput(true, uint(gdk.KEY_Escape))) | |||
| 575 | assert.False(t, shouldDeferToTextInput(false, uint(gdk.KEY_e))) | 620 | assert.False(t, shouldDeferToTextInput(false, uint(gdk.KEY_e))) | ||
| 576 | } | 621 | } | ||
| 577 | 622 | ||||