1
+ import DropdownMenu from '../shared/dropdown/index.js' ;
2
+ import options from 'src/options.js' ;
3
+ import Variable from 'astal/variable' ;
4
+ import { bind } from 'astal/binding' ;
5
+ import { RevealerTransitionMap } from 'src/lib/constants/options.js' ;
6
+ import { App , Gtk } from 'astal/gtk3' ;
7
+ import Separator from 'src/components/shared/Separator.js' ;
8
+ import AstalApps from 'gi://AstalApps?version=0.1'
9
+ import { icon , launchApp } from 'src/lib/utils.js' ;
10
+ import { Entry , EntryProps , Scrollable } from 'astal/gtk3/widget' ;
11
+
12
+ import PopupWindow from '../shared/popup/index.js' ;
13
+
14
+ const apps = new AstalApps . Apps ( {
15
+ nameMultiplier : 2 ,
16
+ keywordsMultiplier : 2 ,
17
+ executableMultiplier : 1 ,
18
+ entryMultiplier : 0.5 ,
19
+ categoriesMultiplier : 0.5 ,
20
+ } ) ;
21
+
22
+ interface ApplicationItemProps {
23
+ app : AstalApps . Application ;
24
+ onLaunched ?: ( ) => void ;
25
+ }
26
+
27
+ const ApplicationItem = ( { app, onLaunched } : ApplicationItemProps ) : JSX . Element => {
28
+ return (
29
+ < button className = "notification-card" halign = { Gtk . Align . FILL } valign = { Gtk . Align . START } onClick = { ( ) => { launchApp ( app ) ; onLaunched ?.( ) } } >
30
+ < box spacing = { 5 } >
31
+ < icon className = "notification-card-image icon" margin = { 5 } halign = { Gtk . Align . CENTER } valign = { Gtk . Align . CENTER } vexpand = { false } icon = { icon ( app . iconName ) } />
32
+ < label halign = { Gtk . Align . START } valign = { Gtk . Align . CENTER } label = { app . name } hexpand vexpand truncate wrap />
33
+ </ box >
34
+ </ button >
35
+ ) ;
36
+ }
37
+
38
+
39
+ function useRef < T > ( ) {
40
+ let ref : T | null = null ;
41
+
42
+ return {
43
+ set : ( r : T ) => { ref = r } ,
44
+ get : ( ) => ref
45
+ }
46
+ }
47
+
48
+ function useApplicationsFilter ( ) {
49
+ const filter = Variable ( '' )
50
+
51
+ const list = bind ( filter ) . as ( ( f ) => {
52
+ // show all apps by default
53
+ if ( ! f ) return apps . get_list ( )
54
+ // if the filter is a single character, show all apps that start with that character
55
+ if ( f . length === 1 ) return apps . get_list ( ) . filter ( ( app ) => app . name . toLowerCase ( ) . startsWith ( f . toLowerCase ( ) ) )
56
+ // otherwise, do a fuzzy search (this method wont filter with a single character)
57
+ return apps . fuzzy_query ( f )
58
+ } )
59
+
60
+ return { filter, list }
61
+ }
62
+
63
+ interface ApplicationLauncherProps {
64
+ visible : Variable < boolean > ;
65
+ onLaunched ?: ( ) => void ;
66
+ }
67
+
68
+ const SearchBar = ( { value, setup, onActivate } : { value ?: Variable < string > ; setup ?: ( self : Entry ) => void ; onActivate ?: EntryProps [ 'onActivate' ] } ) => {
69
+ return (
70
+ < box className = "notification-menu-controls" expand = { false } vertical = { false } >
71
+ < box className = "menu-label-container notifications" halign = { Gtk . Align . START } valign = { Gtk . Align . CENTER } expand >
72
+ < entry onActivate = { onActivate } setup = { setup } className = "menu-label notifications" placeholderText = "Filter" text = { value && bind ( value ) } onChanged = { value && ( ( entry ) => value . set ( entry . text ) ) } />
73
+ </ box >
74
+ < box halign = { Gtk . Align . END } valign = { Gtk . Align . CENTER } expand = { false } >
75
+ < Separator
76
+ halign = { Gtk . Align . CENTER }
77
+ vexpand = { true }
78
+ className = "menu-separator notification-controls"
79
+ />
80
+ < label className = "clear-notifications-label txt-icon" label = "" />
81
+ </ box >
82
+ </ box >
83
+ )
84
+ }
85
+
86
+
87
+ const ApplicationLauncher = ( { visible, onLaunched } : ApplicationLauncherProps ) : JSX . Element => {
88
+ const entry = useRef < Entry > ( )
89
+ const scrollable = useRef < Scrollable > ( )
90
+
91
+ const { filter, list } = useApplicationsFilter ( )
92
+
93
+ const onFilterReturn = ( ) => {
94
+ const first = list . get ( ) [ 0 ]
95
+ if ( ! first ) return ;
96
+ launchApp ( first )
97
+ onLaunched ?.( )
98
+ }
99
+
100
+ // focus the entry when the menu is shown
101
+ const onShow = ( ) => {
102
+ entry . get ( ) ?. grab_focus ( )
103
+ }
104
+ visible . subscribe ( v => v && onShow ( ) ) ;
105
+
106
+ const onHide = ( ) => {
107
+ // clear the filter when the menu is hidden
108
+ filter . set ( '' )
109
+ // TODO: reset scroll position
110
+ }
111
+ visible . subscribe ( v => ! v && onHide ) ;
112
+
113
+ return (
114
+ < box className = "notification-menu-content" css = "padding: 1px; margin: -1px;" hexpand vexpand >
115
+ < box className = "notification-card-container menu" hexpand vexpand vertical >
116
+ < SearchBar value = { filter } setup = { entry . set } onActivate = { onFilterReturn } />
117
+ < scrollable vscroll = { Gtk . PolicyType . AUTOMATIC } setup = { scrollable . set } >
118
+ < box className = "menu-content-container notifications" halign = { Gtk . Align . FILL } valign = { Gtk . Align . START } spacing = { 0 } vexpand vertical >
119
+ { list . as ( apps => apps . map ( ( app ) => < ApplicationItem app = { app } onLaunched = { onLaunched } /> ) ) }
120
+ </ box >
121
+ </ scrollable >
122
+ </ box >
123
+ </ box >
124
+ )
125
+ }
126
+
127
+ /**
128
+ * track the visibility of a window
129
+ * this is necessary because menu are realized at startup and never destroyed
130
+ * making onRealize and onDestroy unreliable for lifecycle management
131
+ */
132
+ function useWindowVisibility ( windowName : string ) {
133
+ const visible = Variable ( ! ! App . get_window ( windowName ) ?. visible ) ;
134
+
135
+ App . connect ( 'window-toggled' , ( _ , window ) => {
136
+ if ( window . name !== windowName ) return ;
137
+ visible . set ( window . visible ) ;
138
+ } )
139
+
140
+ return visible ;
141
+ }
142
+
143
+ export const ApplicationsDropdownMenu = ( ) : JSX . Element => {
144
+ const visible = useWindowVisibility ( 'applicationsdropdownmenu' ) ;
145
+
146
+ const close = ( ) => App . get_window ( 'applicationsdropdownmenu' ) ?. set_visible ( false ) ;
147
+
148
+ return (
149
+ < DropdownMenu
150
+ name = { 'applicationsdropdownmenu' }
151
+ transition = { bind ( options . menus . transition ) . as ( ( transition ) => RevealerTransitionMap [ transition ] ) }
152
+ >
153
+ < ApplicationLauncher visible = { visible } onLaunched = { close } />
154
+ </ DropdownMenu >
155
+ ) ;
156
+ } ;
157
+
158
+
159
+ export const ApplicationsMenu = ( ) : JSX . Element => {
160
+ const visible = useWindowVisibility ( 'applicationsmenu' ) ;
161
+
162
+ const close = ( ) => App . get_window ( 'applicationsmenu' ) ?. set_visible ( false ) ;
163
+
164
+ return (
165
+ < PopupWindow name = { 'applicationsmenu' } transition = { bind ( options . menus . transition ) . as ( ( transition ) => RevealerTransitionMap [ transition ] ) } >
166
+ < ApplicationLauncher visible = { visible } onLaunched = { close } />
167
+ </ PopupWindow >
168
+ )
169
+ }
0 commit comments