Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
X
ximper-shell-notification-center
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Registry
Registry
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Ximper Linux
ximper-shell-notification-center
Commits
accb16fb
Unverified
Commit
accb16fb
authored
Aug 26, 2025
by
Erik Reider
Committed by
GitHub
Aug 26, 2025
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Use GTK focus mechanics for keyboard navigation in the Control Center (#626)
parent
4b21efa6
Show whitespace changes
Inline
Side-by-side
Showing
3 changed files
with
82 additions
and
54 deletions
+82
-54
notifications.vala
src/controlCenter/widgets/notifications/notifications.vala
+78
-50
iterListBoxController.vala
src/iterHelpers/iterListBoxController.vala
+3
-4
notificationGroup.vala
src/notificationGroup/notificationGroup.vala
+1
-0
No files found.
src/controlCenter/widgets/notifications/notifications.vala
View file @
accb16fb
...
@@ -32,7 +32,6 @@ namespace SwayNotificationCenter.Widgets {
...
@@ -32,7 +32,6 @@ namespace SwayNotificationCenter.Widgets {
private
HashTable
<
string
,
unowned
NotificationGroup
>
noti_groups_name
=
private
HashTable
<
string
,
unowned
NotificationGroup
>
noti_groups_name
=
new
HashTable
<
string
,
unowned
NotificationGroup
>
(
str_hash
,
str_equal
);
new
HashTable
<
string
,
unowned
NotificationGroup
>
(
str_hash
,
str_equal
);
private
int
list_position
=
0
;
private
bool
list_reverse
=
false
;
private
bool
list_reverse
=
false
;
// Default config values
// Default config values
...
@@ -132,8 +131,15 @@ namespace SwayNotificationCenter.Widgets {
...
@@ -132,8 +131,15 @@ namespace SwayNotificationCenter.Widgets {
expanded_group
=
null
;
expanded_group
=
null
;
}
}
// Make sure to change focus to the sibling. Otherwise,
// the ListBox focuses the first notification.
if
(
list_reverse
)
{
navigate_up
(
group
,
true
);
}
else
{
navigate_down
(
group
,
true
);
}
list_box_controller
.
remove
(
group
);
list_box_controller
.
remove
(
group
);
navigate_list
(--
list_position
);
// Switches the stack page depending on the amount of notifications
// Switches the stack page depending on the amount of notifications
if
(
list_box_controller
.
length
<
1
)
{
if
(
list_box_controller
.
length
<
1
)
{
stack
.
set_visible_child_name
(
STACK_PLACEHOLDER_PAGE
);
stack
.
set_visible_child_name
(
STACK_PLACEHOLDER_PAGE
);
...
@@ -202,18 +208,9 @@ namespace SwayNotificationCenter.Widgets {
...
@@ -202,18 +208,9 @@ namespace SwayNotificationCenter.Widgets {
noti_groups_name
.
set
(
param
.
name_id
,
group
);
noti_groups_name
.
set
(
param
.
name_id
,
group
);
}
}
// Set the new list position when the group receives keyboard focus
Gtk
.
EventControllerFocus
focus_controller
=
new
Gtk
.
EventControllerFocus
();
group
.
add_controller
(
focus_controller
);
focus_controller
.
enter
.
connect
(()
=>
{
int
i
=
list_box_controller
.
get_children
().
index
(
group
);
if
(
list_position
!=
int
.
MAX
&&
list_position
!=
i
)
{
list_position
=
i
;
}
});
// Switches the stack page depending on the amount of notifications
// Switches the stack page depending on the amount of notifications
stack
.
set_visible_child_name
(
STACK_NOTIFICATIONS_PAGE
);
stack
.
set_visible_child_name
(
STACK_NOTIFICATIONS_PAGE
);
list_box_controller
.
append
(
group
);
list_box_controller
.
append
(
group
);
}
}
...
@@ -237,9 +234,8 @@ namespace SwayNotificationCenter.Widgets {
...
@@ -237,9 +234,8 @@ namespace SwayNotificationCenter.Widgets {
stderr
.
printf
(
e
.
message
+
"\n"
);
stderr
.
printf
(
e
.
message
+
"\n"
);
}
}
// Keep focus on currently focused notification
// Focus the incoming notification
list_box
.
grab_focus
();
group
.
grab_focus
();
navigate_list
(++
list_position
);
}
}
public
void
set_list_is_reversed
(
bool
reversed
)
{
public
void
set_list_is_reversed
(
bool
reversed
)
{
...
@@ -253,6 +249,9 @@ namespace SwayNotificationCenter.Widgets {
...
@@ -253,6 +249,9 @@ namespace SwayNotificationCenter.Widgets {
public
bool
key_press_event_cb
(
uint
keyval
,
uint
keycode
,
Gdk
.
ModifierType
state
)
{
public
bool
key_press_event_cb
(
uint
keyval
,
uint
keycode
,
Gdk
.
ModifierType
state
)
{
var
children
=
list_box_controller
.
get_children
();
var
children
=
list_box_controller
.
get_children
();
if
(!(
list_box
.
get_focus_child
()
is
NotificationGroup
))
{
focus_first_notification
();
}
var
group
=
(
NotificationGroup
)
list_box
.
get_focus_child
();
var
group
=
(
NotificationGroup
)
list_box
.
get_focus_child
();
switch
(
Gdk
.
keyval_name
(
keyval
))
{
switch
(
Gdk
.
keyval_name
(
keyval
))
{
case
"Return"
:
case
"Return"
:
...
@@ -267,32 +266,14 @@ namespace SwayNotificationCenter.Widgets {
...
@@ -267,32 +266,14 @@ namespace SwayNotificationCenter.Widgets {
break
;
break
;
case
"Delete"
:
case
"Delete"
:
case
"BackSpace"
:
case
"BackSpace"
:
if
(
group
!=
null
)
{
if
(
group
!=
null
&&
!
children
.
is_empty
())
{
int
len
=
(
int
)
children
.
length
();
if
(
len
==
0
)
break
;
// Add a delta so that we select the next notification
// due to it not being gone from the list yet due to
// the fade transition
int
delta
=
2
;
if
(
list_reverse
)
{
if
(
children
.
first
().
data
!=
group
)
{
delta
=
0
;
}
list_position
--;
}
else
{
if
(
list_position
>
0
)
list_position
--;
if
(
children
.
last
().
data
==
group
)
{
delta
=
0
;
}
}
var
noti
=
group
.
get_latest_notification
();
var
noti
=
group
.
get_latest_notification
();
if
(
group
.
only_single_notification
()
&&
noti
!=
null
)
{
if
(
group
.
only_single_notification
()
&&
noti
!=
null
)
{
close_notification
(
noti
.
param
.
applied_id
,
true
);
close_notification
(
noti
.
param
.
applied_id
,
true
);
break
;
break
;
}
}
group
.
close_all_notifications
();
group
.
close_all_notifications
();
navigate_list
(
list_position
+
delta
);
break
;
return
true
;
}
}
break
;
break
;
case
"C"
:
case
"C"
:
...
@@ -306,19 +287,16 @@ namespace SwayNotificationCenter.Widgets {
...
@@ -306,19 +287,16 @@ namespace SwayNotificationCenter.Widgets {
}
}
break
;
break
;
case
"Down"
:
case
"Down"
:
if
(
list_position
+
1
<
children
.
length
())
{
navigate_down
(
group
);
++
list_position
;
}
break
;
break
;
case
"Up"
:
case
"Up"
:
if
(
list_position
>
0
)
--
list_position
;
navigate_up
(
group
)
;
break
;
break
;
case
"Home"
:
case
"Home"
:
list_position
=
0
;
navigate_to_first_notification
()
;
break
;
break
;
case
"End"
:
case
"End"
:
list_position
=
((
int
)
children
.
length
())
-
1
;
navigate_to_last_notification
();
if
(
list_position
==
uint
.
MAX
)
list_position
=
0
;
break
;
break
;
default
:
default
:
// Pressing 1-9 to activate a notification action
// Pressing 1-9 to activate a notification action
...
@@ -333,7 +311,6 @@ namespace SwayNotificationCenter.Widgets {
...
@@ -333,7 +311,6 @@ namespace SwayNotificationCenter.Widgets {
}
}
break
;
break
;
}
}
navigate_list
(
list_position
);
// Override the builtin list navigation
// Override the builtin list navigation
return
true
;
return
true
;
}
}
...
@@ -391,29 +368,80 @@ namespace SwayNotificationCenter.Widgets {
...
@@ -391,29 +368,80 @@ namespace SwayNotificationCenter.Widgets {
}
}
private
void
navigate_list
(
int
i
)
{
private
void
navigate_list
(
int
i
)
{
if
(
list_box_controller
.
length
==
0
)
{
return
;
}
unowned
Gtk
.
ListBoxRow
?
widget
=
list_box
.
get_row_at_index
(
i
);
unowned
Gtk
.
ListBoxRow
?
widget
=
list_box
.
get_row_at_index
(
i
);
if
(
widget
==
null
)
{
if
(
widget
==
null
)
{
// Try getting the last widget
// Try getting the last widget
if
(
list_reverse
)
{
if
(
list_reverse
)
{
widget
=
list_box
.
get_row_at_index
(
0
);
widget
=
list_box
.
get_row_at_index
(
0
);
}
else
{
}
else
{
int
len
=
((
int
)
list_box_controller
.
length
)
-
1
;
int
len
=
list_box_controller
.
length
-
1
;
widget
=
list_box
.
get_row_at_index
(
len
);
widget
=
list_box
.
get_row_at_index
(
len
);
}
}
}
}
if
(
widget
!=
null
)
{
if
(
widget
!=
null
)
{
widget
.
grab_focus
();
widget
.
grab_focus
();
list_box
.
set_focus_child
(
widget
);
}
else
{
list_box
.
grab_focus
();
}
}
}
private
void
navigate_up
(
NotificationGroup
?
focused_group
,
bool
fallback_other_dir
=
false
)
{
if
(
list_box_controller
.
length
==
1
)
{
focus_first_notification
();
return
;
}
if
(!(
focused_group
is
NotificationGroup
)
||
list_box_controller
.
length
==
0
)
{
return
;
}
if
(
list_box
.
get_first_child
()
==
focused_group
)
{
if
(
fallback_other_dir
)
{
navigate_down
(
focused_group
,
false
);
}
return
;
}
focused_group
.
move_focus
(
Gtk
.
DirectionType
.
TAB_BACKWARD
);
}
private
void
navigate_down
(
NotificationGroup
?
focused_group
,
bool
fallback_other_dir
=
false
)
{
if
(
list_box_controller
.
length
==
1
)
{
focus_first_notification
();
return
;
}
if
(!(
focused_group
is
NotificationGroup
)
||
list_box_controller
.
length
==
0
)
{
return
;
}
if
(
list_box
.
get_last_child
()
==
focused_group
)
{
if
(
fallback_other_dir
)
{
navigate_up
(
focused_group
,
false
);
}
return
;
}
focused_group
.
move_focus
(
Gtk
.
DirectionType
.
TAB_FORWARD
);
}
private
void
navigate_to_first_notification
()
{
int
i
=
(
list_reverse
?
list_box_controller
.
length
-
1
:
0
)
.
clamp
(
0
,
list_box_controller
.
length
);
navigate_list
(
i
);
}
private
void
navigate_to_last_notification
()
{
int
i
=
(
list_reverse
?
0
:
list_box_controller
.
length
-
1
)
.
clamp
(
0
,
list_box_controller
.
length
);
navigate_list
(
i
);
}
}
private
void
focus_first_notification
()
{
private
void
focus_first_notification
()
{
// Focus the first notification
navigate_to_first_notification
();
list_position
=
(
list_reverse
?
(((
int
)
list_box_controller
.
length
)
-
1
)
:
0
)
.
clamp
(
0
,
(
int
)
list_box_controller
.
length
);
list_box
.
grab_focus
();
navigate_list
(
list_position
);
foreach
(
unowned
Gtk
.
Widget
w
in
list_box_controller
.
get_children
())
{
foreach
(
unowned
Gtk
.
Widget
w
in
list_box_controller
.
get_children
())
{
var
group
=
(
NotificationGroup
)
w
;
var
group
=
(
NotificationGroup
)
w
;
if
(
group
!=
null
)
group
.
update
();
if
(
group
!=
null
)
group
.
update
();
...
...
src/iterHelpers/iterListBoxController.vala
View file @
accb16fb
public
class
IterListBoxController
:
Object
{
public
class
IterListBoxController
:
Object
{
public
u
int
length
{
get
;
private
set
;
default
=
0
;
}
public
int
length
{
get
;
private
set
;
default
=
0
;
}
private
List
<
Gtk
.
Widget
>
children
=
new
List
<
Gtk
.
Widget
>
();
private
List
<
Gtk
.
Widget
>
children
=
new
List
<
Gtk
.
Widget
>
();
public
unowned
Gtk
.
ListBox
list_box
{
public
unowned
Gtk
.
ListBox
list_box
{
...
@@ -13,11 +13,10 @@ public class IterListBoxController : Object {
...
@@ -13,11 +13,10 @@ public class IterListBoxController : Object {
private
void
on_add
(
Gtk
.
Widget
child
)
{
private
void
on_add
(
Gtk
.
Widget
child
)
{
length
++;
length
++;
child
.
destroy
.
connect
(()
=>
{
child
.
destroy
.
connect
(
this
.
remove
);
children
.
remove
(
child
);
});
}
}
// NOTE: Not sorted
public
List
<
weak
Gtk
.
Widget
>
get_children
()
{
public
List
<
weak
Gtk
.
Widget
>
get_children
()
{
return
children
.
copy
();
return
children
.
copy
();
}
}
...
...
src/notificationGroup/notificationGroup.vala
View file @
accb16fb
...
@@ -31,6 +31,7 @@ namespace SwayNotificationCenter {
...
@@ -31,6 +31,7 @@ namespace SwayNotificationCenter {
set_child
(
dismissible
);
set_child
(
dismissible
);
Gtk
.
Overlay
overlay
=
new
Gtk
.
Overlay
();
Gtk
.
Overlay
overlay
=
new
Gtk
.
Overlay
();
overlay
.
set_can_focus
(
false
);
dismissible
.
child
=
overlay
;
dismissible
.
child
=
overlay
;
Gtk
.
Box
box
=
new
Gtk
.
Box
(
Gtk
.
Orientation
.
VERTICAL
,
0
);
Gtk
.
Box
box
=
new
Gtk
.
Box
(
Gtk
.
Orientation
.
VERTICAL
,
0
);
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment