1 { Copyleft implementation of TTrayIcon for
2 Unity applications indicators
3 Created 2015 by Anthony Walter sysrpl@gmail.com }
4
5 unit UnityWSCtrls;
6
7 interface
8
9 {$mode delphi}
10 uses
11 GLib2, Gtk2, Gdk2Pixbuf,
12 Classes, SysUtils, dynlibs,
13 Graphics, Controls, Forms, ExtCtrls, WSExtCtrls, LCLType, LazUTF8,
14 FileUtil;
15
16 { Changed October 2019, we now try and identify those Linux distributions that
17 need to use LibAppIndicator3 and allow the remainder to use the older and
18 more functional SystemTray. Only a few old distributions can use LibAppIndicator_1
19 so don't bother to try it, rely, here on LibAppIndicator3
20
21 The 'look up table' in NeedAppIndicator() can be overridden.
22 Introduce an optional env var, LAZUSEAPPIND that can be unset or set to
23 YES, NO or INFO - YES forces an attempt to use LibAppIndicator3, NO prevents
24 an attempt, any non blank value (eg INFO) displays to std out what is happening.
25
26 Note we assume this env var will only be used in Linux were its always safe to
27 write to stdout.
28 DRB
29 }
30
31 { TUnityWSCustomTrayIcon is the class for tray icons on systems
32 running the Unity desktop environment.
33
34 Unity allows only AppIndicator objects in its tray. These objects
35 have the following reduced functionality:
36
37 Tooltips are not allowed
38 Icons do not receive mouse events
39 Indicators display a menu when clicked by any mouse button
40
41 See also: http://www.markshuttleworth.com/archives/347
42 "Clicking on an indicator will open its menu..."
43 "There’ll be no ability for arbitrary applications to define arbitrary
44 behaviours to arbitrary events on indicators"
45
46 Personal observations:
47
48 A popup menu is required always
49 You can only create one AppIndicator per appplication
50 You cannot use a different popupmenu once one has been used }
51
52 type
53 TUnityWSCustomTrayIcon = class(TWSCustomTrayIcon)
54 published
Hidenull55 class function Hide(const ATrayIcon: TCustomTrayIcon): Boolean; override;
Shownull56 class function Show(const ATrayIcon: TCustomTrayIcon): Boolean; override;
57 class procedure InternalUpdate(const ATrayIcon: TCustomTrayIcon); override;
GetPositionnull58 class function GetPosition(const {%H-}ATrayIcon: TCustomTrayIcon): TPoint; override;
59 end;
60
61 { UnityAppIndicatorInit returns true if libappindicator_3 library can be loaded }
62
UnityAppIndicatorInitnull63 function UnityAppIndicatorInit: Boolean;
64
65 implementation
66
67 const
68 libappindicator_3 = 'libappindicator3.so.1';
69
70 {const
71 APP_INDICATOR_SIGNAL_NEW_ICON = 'new-icon';
72 APP_INDICATOR_SIGNAL_NEW_ATTENTION_ICON = 'new-attention-icon';
73 APP_INDICATOR_SIGNAL_NEW_STATUS = 'new-status';
74 APP_INDICATOR_SIGNAL_NEW_LABEL = 'new-label';
75 APP_INDICATOR_SIGNAL_CONNECTION_CHANGED = 'connection-changed';
76 APP_INDICATOR_SIGNAL_NEW_ICON_THEME_PATH = 'new-icon-theme-path';
77 }
78 type
79 TAppIndicatorCategory = (
80 APP_INDICATOR_CATEGORY_APPLICATION_STATUS,
81 APP_INDICATOR_CATEGORY_COMMUNICATIONS,
82 APP_INDICATOR_CATEGORY_SYSTEM_SERVICES,
83 APP_INDICATOR_CATEGORY_HARDWARE,
84 APP_INDICATOR_CATEGORY_OTHER
85 );
86
87 TAppIndicatorStatus = (
88 APP_INDICATOR_STATUS_PASSIVE,
89 APP_INDICATOR_STATUS_ACTIVE,
90 APP_INDICATOR_STATUS_ATTENTION
91 );
92
93 PAppIndicator = Pointer;
94
95 var
96 { GlobalAppIndicator creation routines }
GTypenull97 app_indicator_get_type: function: GType; cdecl;
dnull98 app_indicator_new: function(id, icon_name: PGChar; category: TAppIndicatorCategory): PAppIndicator; cdecl;
dnull99 app_indicator_new_with_path: function(id, icon_name: PGChar; category: TAppIndicatorCategory; icon_theme_path: PGChar): PAppIndicator; cdecl;
100 { Set properties }
101 app_indicator_set_status: procedure(self: PAppIndicator; status: TAppIndicatorStatus); cdecl;
102 app_indicator_set_attention_icon: procedure(self: PAppIndicator; icon_name: PGChar); cdecl;
103 app_indicator_set_menu: procedure(self: PAppIndicator; menu: PGtkMenu); cdecl;
104 app_indicator_set_icon: procedure(self: PAppIndicator; icon_name: PGChar); cdecl;
105 app_indicator_set_label: procedure(self: PAppIndicator; _label, guide: PGChar); cdecl;
106 app_indicator_set_icon_theme_path: procedure(self: PAppIndicator; icon_theme_path: PGChar); cdecl;
107 app_indicator_set_ordering_index: procedure(self: PAppIndicator; ordering_index: guint32); cdecl;
108 { Get properties }
elfnull109 app_indicator_get_id: function(self: PAppIndicator): PGChar; cdecl;
elfnull110 app_indicator_get_category: function(self: PAppIndicator): TAppIndicatorCategory; cdecl;
elfnull111 app_indicator_get_status: function(self: PAppIndicator): TAppIndicatorStatus; cdecl;
elfnull112 app_indicator_get_icon: function(self: PAppIndicator): PGChar; cdecl;
elfnull113 app_indicator_get_icon_theme_path: function(self: PAppIndicator): PGChar; cdecl;
elfnull114 app_indicator_get_attention_icon: function(self: PAppIndicator): PGChar; cdecl;
elfnull115 app_indicator_get_menu: function(self: PAppIndicator): PGtkMenu; cdecl;
elfnull116 app_indicator_get_label: function(self: PAppIndicator): PGChar; cdecl;
elfnull117 app_indicator_get_label_guide: function(self: PAppIndicator): PGChar; cdecl;
elfnull118 app_indicator_get_ordering_index: function(self: PAppIndicator): guint32; cdecl;
119
120 { TUnityTrayIconHandle }
121
122 type
123 TUnityTrayIconHandle = class
124 private
125 FTrayIcon: TCustomTrayIcon;
126 FName: string;
127 FIconName: string;
128 public
129 constructor Create(TrayIcon: TCustomTrayIcon);
130 destructor Destroy; override;
131 procedure Update;
132 end;
133
134 { It seems to me, and please tell me otherwise if untrue, that the only way
135 to load icons for AppIndicator is through files }
136
137 const
138 IconThemePath = '/tmp/appindicators/';
139 IconType = 'png';
140
141 { It seems to me, and please tell me otherwise if untrue, that you can only
142 create one working AppIndicator for your program over its lifetime }
143
144 var
145 GlobalAppIndicator: PAppIndicator;
146 GlobalIcon: Pointer;
147 GlobalIconPath: string;
148
149 constructor TUnityTrayIconHandle.Create(TrayIcon: TCustomTrayIcon);
150 var
151 NewIcon: Pointer;
152 begin
153 inherited Create;
154 FTrayIcon := TrayIcon;
155 FName := 'app-' + IntToHex(IntPtr(Application), SizeOf(IntPtr) * 2);
156 NewIcon := {%H-}Pointer(FTrayIcon.Icon.Handle);
157 if NewIcon = nil then
158 NewIcon := {%H-}Pointer(Application.Icon.Handle);
159 if NewIcon <> GlobalIcon then
160 begin
161 GlobalIcon := NewIcon;
162 ForceDirectories(IconThemePath);
163 FIconName := FName + '-' + IntToHex({%H-}IntPtr(GlobalIcon), SizeOf(GlobalIcon) * 2);
164 if FileExists(GlobalIconPath) then
165 DeleteFile(GlobalIconPath);
166 GlobalIconPath := IconThemePath + FIconName + '.' + IconType;
167 gdk_pixbuf_save(GlobalIcon, PChar(GlobalIconPath), IconType, nil, [nil]);
168 if GlobalAppIndicator <> nil then
169 app_indicator_set_icon(GlobalAppIndicator, PChar(FIconName));
170 end
171 else
172 FIconName := FName + '-' + IntToHex({%H-}IntPtr(GlobalIcon), SizeOf(GlobalIcon) * 2);
173 { Only the first created AppIndicator is functional }
174 if GlobalAppIndicator = nil then
175 { It seems that icons can only come from files :( }
176 GlobalAppIndicator := app_indicator_new_with_path(PChar(FName), PChar(FIconName),
177 APP_INDICATOR_CATEGORY_APPLICATION_STATUS, IconThemePath);
178 Update;
179 end;
180
181 destructor TUnityTrayIconHandle.Destroy;
182 begin
183 { Hide the global AppIndicator }
184 app_indicator_set_status(GlobalAppIndicator, APP_INDICATOR_STATUS_PASSIVE);
185 inherited Destroy;
186 end;
187
188 procedure TUnityTrayIconHandle.Update;
189 var
190 NewIcon: Pointer;
191 begin
192 NewIcon := {%H-}Pointer(FTrayIcon.Icon.Handle);
193 if NewIcon = nil then
194 NewIcon := {%H-}Pointer(Application.Icon.Handle);
195 if NewIcon <> GlobalIcon then
196 begin
197 GlobalIcon := NewIcon;
198 FIconName := FName + '-' + IntToHex({%H-}IntPtr(GlobalIcon), SizeOf(GlobalIcon) * 2);
199 ForceDirectories(IconThemePath);
200 if FileExists(GlobalIconPath) then
201 DeleteFile(GlobalIconPath);
202 GlobalIconPath := IconThemePath + FIconName + '.' + IconType;
203 gdk_pixbuf_save(GlobalIcon, PChar(GlobalIconPath), IconType, nil, [nil]);
204 { Again it seems that icons can only come from files }
205 app_indicator_set_icon(GlobalAppIndicator, PChar(FIconName));
206 end;
207 { It seems to me you can only set the menu once for an AppIndicator }
208 if (app_indicator_get_menu(GlobalAppIndicator) = nil) and (FTrayIcon.PopUpMenu <> nil) then
209 app_indicator_set_menu(GlobalAppIndicator, {%H-}PGtkMenu(FTrayIcon.PopUpMenu.Handle));
210 app_indicator_set_status(GlobalAppIndicator, APP_INDICATOR_STATUS_ACTIVE);
211 end;
212
213 { TUnityWSCustomTrayIcon }
214
TUnityWSCustomTrayIcon.Hidenull215 class function TUnityWSCustomTrayIcon.Hide(const ATrayIcon: TCustomTrayIcon): Boolean;
216 var
217 T: TUnityTrayIconHandle;
218 begin
219 if ATrayIcon.Handle <> 0 then
220 begin
221 T := TUnityTrayIconHandle(ATrayIcon.Handle);
222 ATrayIcon.Handle := 0;
223 T.Free;
224 end;
225 Result := True;
226 end;
227
TUnityWSCustomTrayIcon.Shownull228 class function TUnityWSCustomTrayIcon.Show(const ATrayIcon: TCustomTrayIcon): Boolean;
229 var
230 T: TUnityTrayIconHandle;
231 begin
232 if ATrayIcon.Handle = 0 then
233 begin
234 T := TUnityTrayIconHandle.Create(ATrayIcon);
235 ATrayIcon.Handle := HWND(T);
236 end;
237 Result := True;
238 end;
239
240 class procedure TUnityWSCustomTrayIcon.InternalUpdate(const ATrayIcon: TCustomTrayIcon);
241 var
242 T: TUnityTrayIconHandle;
243 begin
244 if ATrayIcon.Handle <> 0 then
245 begin
246 T := TUnityTrayIconHandle(ATrayIcon.Handle);
247 T.Update;
248 end;
249 end;
250
TUnityWSCustomTrayIcon.GetPositionnull251 class function TUnityWSCustomTrayIcon.GetPosition(const ATrayIcon: TCustomTrayIcon): TPoint;
252 begin
253 Result := Point(0, 0);
254 end;
255
256 { UnityAppIndicatorInit }
257
258 var
259 Loaded: Boolean;
260 Initialized: Boolean;
261
UnityAppIndicatorInitnull262 function UnityAppIndicatorInit: Boolean;
263 var
264 Module: HModule;
265 UseAppInd : string;
266
NeedAppIndicatornull267 function NeedAppIndicator: boolean;
268 var
269 DeskTop, VersionSt : String;
270 ProcFile: TextFile;
271 begin
272 DeskTop := GetEnvironmentVariableUTF8('XDG_CURRENT_DESKTOP');
273 // See the wiki for details of what extras these desktops require !!
274 if (DeskTop = 'Unity')
275 or (Desktop = 'Enlightenment')
276 then exit(True);
277 if (DeskTop = 'GNOME') then begin
278 {$PUSH}
279 {$IOChecks off}
280 AssignFile(ProcFile, '/proc/version');
281 reset(ProcFile);
282 if IOResult<>0 then exit(false);
283 {$POP}
284 readln(ProcFile, VersionSt);
285 CloseFile(ProcFile);
286 if ( (pos('mageia', VersionSt) > 0) or
287 (pos('Debian', VersionSt) > 0) or
288 (pos('Red Hat', VersionSt) > 0) or
289 (pos('SUSE', VersionSt) > 0) )
290 // 19.04 and earlier Ubuntu Gnome does not need LibAppIndicator3
291 then exit(True);
292 end;
293 Result := False;
294 end;
295
296
297
TryLoadnull298 function TryLoad(const ProcName: string; var Proc: Pointer): Boolean;
299 begin
300 Proc := GetProcAddress(Module, ProcName);
301 Result := Proc <> nil;
302 end;
303
304 begin
305 Result := False;
306 if Loaded then
307 Exit(Initialized);
308 Loaded := True;
309 if Initialized then
310 Exit(True);
311 UseAppInd := getEnvironmentVariable('LAZUSEAPPIND');
312 if UseAppInd = 'NO' then
313 begin
314 Initialized := False;
315 writeln('APPIND Debug : Choosing to not try AppIndicator3');
316 Exit;
317 end;
318 if (UseAppInd <> 'YES') and (not NeedAppIndicator()) then // ie its NO or blank or INFO
319 begin
320 Initialized := False;
321 if UseAppInd <> '' then
322 writeln('APPIND Debug : Will not use AppIndicator3');
323 Exit;
324 end;
325 if UseAppInd = 'YES' then // either a YES or OS needs it
326 writeln('APPIND Debug : Will try to force AppIndicator3')
327 else
328 if UseAppInd <> '' then writeln('APPIND Debug : OS and Desktop request AppIndicator3');
329 Module := LoadLibrary(libappindicator_3); // might have several package names, see wiki
330 if Module = 0 then
331 Exit;
332 Result :=
333 TryLoad('app_indicator_get_type', @app_indicator_get_type) and
334 TryLoad('app_indicator_new', @app_indicator_new) and
335 TryLoad('app_indicator_new_with_path', @app_indicator_new_with_path) and
336 TryLoad('app_indicator_set_status', @app_indicator_set_status) and
337 TryLoad('app_indicator_set_attention_icon', @app_indicator_set_attention_icon) and
338 TryLoad('app_indicator_set_menu', @app_indicator_set_menu) and
339 TryLoad('app_indicator_set_icon', @app_indicator_set_icon) and
340 TryLoad('app_indicator_set_label', @app_indicator_set_label) and
341 TryLoad('app_indicator_set_icon_theme_path', @app_indicator_set_icon_theme_path) and
342 TryLoad('app_indicator_set_ordering_index', @app_indicator_set_ordering_index) and
343 TryLoad('app_indicator_get_id', @app_indicator_get_id) and
344 TryLoad('app_indicator_get_category', @app_indicator_get_category) and
345 TryLoad('app_indicator_get_status', @app_indicator_get_status) and
346 TryLoad('app_indicator_get_icon', @app_indicator_get_icon) and
347 TryLoad('app_indicator_get_icon_theme_path', @app_indicator_get_icon_theme_path) and
348 TryLoad('app_indicator_get_attention_icon', @app_indicator_get_attention_icon) and
349 TryLoad('app_indicator_get_menu', @app_indicator_get_menu) and
350 TryLoad('app_indicator_get_label', @app_indicator_get_label) and
351 TryLoad('app_indicator_get_label_guide', @app_indicator_get_label_guide) and
352 TryLoad('app_indicator_get_ordering_index', @app_indicator_get_ordering_index);
353 if UseAppInd <> '' then
354 writeln('APPIND Debug : AppIndicator3 has loaded ' + booltostr(Result, True));
355 Initialized := Result;
356 end;
357
358 initialization
359 GlobalAppIndicator := nil;
360 GlobalIconPath := '';
361 finalization
362 if FileExists(GlobalIconPath) then
363 DeleteFile(GlobalIconPath);
364 if GlobalAppIndicator <> nil then
365 g_object_unref(GlobalAppIndicator);
366 end.
367