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