1 {
2  *****************************************************************************
3  *                               gtk3WSTrayIcon.pas                          *
4  *                               ------------------                          *
5  *                                                                           *
6  *                                                                           *
7  *****************************************************************************
8 
9  *****************************************************************************
10   This file is part of the Lazarus Component Library (LCL)
11 
12   See the file COPYING.modifiedLGPL.txt, included in this distribution,
13   for details about the license.
14  *****************************************************************************
15 
16 A unit that uses LibAppIndicator3 to display a TrayIcon in GTK3.   Based on a
17 GTK2 version by Anthony Walter, now works with many common Linux systems "out
18 of the box" and almost all of the remainder with addition of LibAppIndicator3
19 and TopIconsPlus or similar Gnome Extension.
20 
21 See Wiki for details and Limitations (Menu only, one Icon only....)
22 Also refer to discussion in ../gtk2/UnityWSCtrls.pas
23 }
24 
25 
26 unit gtk3wstrayicon;
27 
28 interface
29 
30 {$mode delphi}
31 uses
32   GLib2, LazGtk3, LazGdkPixbuf2, gtk3widgets,
33   Classes, SysUtils, dynlibs,
34   Graphics, Controls, Forms, ExtCtrls, WSExtCtrls, LCLType, LazUTF8,
35   FileUtil;
36 
37 type
38   TGtk3WSTrayIcon = class(TWSCustomTrayIcon)
39   published
Hidenull40     class function Hide(const ATrayIcon: TCustomTrayIcon): Boolean; override;
Shownull41     class function Show(const ATrayIcon: TCustomTrayIcon): Boolean; override;
42     class procedure InternalUpdate(const ATrayIcon: TCustomTrayIcon); override;
GetPositionnull43     class function GetPosition(const {%H-}ATrayIcon: TCustomTrayIcon): TPoint; override;
44   end;
45 
46 { Gtk3AppIndicatorInit returns true if LibAppIndicator_3 library has been loaded }
Gtk3AppIndicatorInitnull47 function Gtk3AppIndicatorInit: Boolean;
48 
49 implementation
50 
51 uses gtk3objects;     // TGtk3Image
52 
53 const
54   libappindicator_3 = 'libappindicator3.so.1';
55 
56 type
57   TAppIndicatorCategory = (
58     APP_INDICATOR_CATEGORY_APPLICATION_STATUS,
59     APP_INDICATOR_CATEGORY_COMMUNICATIONS,
60     APP_INDICATOR_CATEGORY_SYSTEM_SERVICES,
61     APP_INDICATOR_CATEGORY_HARDWARE,
62     APP_INDICATOR_CATEGORY_OTHER
63   );
64 
65   TAppIndicatorStatus = (
66     APP_INDICATOR_STATUS_PASSIVE,
67     APP_INDICATOR_STATUS_ACTIVE,
68     APP_INDICATOR_STATUS_ATTENTION
69   );
70 
71   PAppIndicator = Pointer;
72 
73 var
74   { GlobalAppIndicator creation routines }
GTypenull75   app_indicator_get_type: function: GType; cdecl;
dnull76   app_indicator_new: function(id, icon_name: PGChar; category: TAppIndicatorCategory): PAppIndicator; cdecl;
dnull77   app_indicator_new_with_path: function(id, icon_name: PGChar; category: TAppIndicatorCategory; icon_theme_path: PGChar): PAppIndicator; cdecl;
78   { Set properties }
79   app_indicator_set_status: procedure(self: PAppIndicator; status: TAppIndicatorStatus); cdecl;
80   app_indicator_set_attention_icon: procedure(self: PAppIndicator; icon_name: PGChar); cdecl;
81   app_indicator_set_menu: procedure(self: PAppIndicator; menu: PGtkMenu); cdecl;
82   app_indicator_set_icon: procedure(self: PAppIndicator; icon_name: PGChar); cdecl;
83   app_indicator_set_label: procedure(self: PAppIndicator; _label, guide: PGChar); cdecl;
84   app_indicator_set_icon_theme_path: procedure(self: PAppIndicator; icon_theme_path: PGChar); cdecl;
85   app_indicator_set_ordering_index: procedure(self: PAppIndicator; ordering_index: guint32); cdecl;
86   { Get properties }
elfnull87   app_indicator_get_id: function(self: PAppIndicator): PGChar; cdecl;
elfnull88   app_indicator_get_category: function(self: PAppIndicator): TAppIndicatorCategory; cdecl;
elfnull89   app_indicator_get_status: function(self: PAppIndicator): TAppIndicatorStatus; cdecl;
elfnull90   app_indicator_get_icon: function(self: PAppIndicator): PGChar; cdecl;
elfnull91   app_indicator_get_icon_theme_path: function(self: PAppIndicator): PGChar; cdecl;
elfnull92   app_indicator_get_attention_icon: function(self: PAppIndicator): PGChar; cdecl;
elfnull93   app_indicator_get_menu: function(self: PAppIndicator): PGtkMenu; cdecl;
elfnull94   app_indicator_get_label: function(self: PAppIndicator): PGChar; cdecl;
elfnull95   app_indicator_get_label_guide: function(self: PAppIndicator): PGChar; cdecl;
elfnull96   app_indicator_get_ordering_index: function(self: PAppIndicator): guint32; cdecl;
97 
98 { TAppIndTrayIconHandle }
99 
100 type
101   TAppIndTrayIconHandle = class
102   private
103     FTrayIcon: TCustomTrayIcon;
104     FName: string;
105     FIconName: string;
106   public
107     constructor Create(TrayIcon: TCustomTrayIcon);
108     destructor Destroy; override;
109     procedure Update;
110   end;
111 
112 const
113   IconThemePath = '/tmp/appindicators/'; // We must write our icon to a file.
114   IconType = 'png';
115 
116 var
117   GlobalAppIndicator: PAppIndicator;
118   GlobalIcon: Pointer;
119   GlobalIconPath: string;
120 
121 constructor TAppIndTrayIconHandle.Create(TrayIcon: TCustomTrayIcon);
122 var
123   NewIcon: Pointer;
124 begin
125   inherited Create;
126   FTrayIcon := TrayIcon;
127   FName := 'app-' + IntToHex(IntPtr(Application), SizeOf(IntPtr) * 2);
128   NewIcon := {%H-}Pointer(TGtk3Image(FTrayIcon.Icon.Handle).handle);
129   if NewIcon = nil then
130     NewIcon := {%H-}Pointer(Application.Icon.Handle);
131   if NewIcon <> GlobalIcon then
132   begin
133     GlobalIcon := NewIcon;
134     ForceDirectories(IconThemePath);
135     FIconName := FName + '-' + IntToHex({%H-}IntPtr(GlobalIcon), SizeOf(GlobalIcon) * 2);
136     if FileExists(GlobalIconPath) then
137       DeleteFile(GlobalIconPath);
138     GlobalIconPath := IconThemePath + FIconName + '.' + IconType;
139     gdk_pixbuf_save(GlobalIcon, PChar(GlobalIconPath), IconType, nil, [nil]);
140     if GlobalAppIndicator <> nil then
141         app_indicator_set_icon(GlobalAppIndicator, PChar(FIconName));
142   end
143   else
144     FIconName := FName + '-' + IntToHex({%H-}IntPtr(GlobalIcon), SizeOf(GlobalIcon) * 2);
145   { Only the first created AppIndicator is functional }
146   if GlobalAppIndicator = nil then
147     { It seems that icons can only come from files :( }
148     GlobalAppIndicator := app_indicator_new_with_path(PChar(FName), PChar(FIconName),
149       APP_INDICATOR_CATEGORY_APPLICATION_STATUS, IconThemePath);
150   Update;
151 end;
152 
153 destructor TAppIndTrayIconHandle.Destroy;
154 begin
155   { Hide the global AppIndicator }
156   app_indicator_set_status(GlobalAppIndicator, APP_INDICATOR_STATUS_PASSIVE);
157   inherited Destroy;
158 end;
159 
160 procedure TAppIndTrayIconHandle.Update;
161 var
162   NewIcon: Pointer;
163 begin
164   NewIcon := {%H-}Pointer(TGTK3Image(FTrayIcon.Icon.Handle).Handle);
165   if NewIcon = nil then
166     NewIcon := {%H-}Pointer(Application.Icon.Handle);
167   if NewIcon <> GlobalIcon then
168   begin
169     GlobalIcon := NewIcon;
170     FIconName := FName + '-' + IntToHex({%H-}IntPtr(GlobalIcon), SizeOf(GlobalIcon) * 2);
171     ForceDirectories(IconThemePath);
172     if FileExists(GlobalIconPath) then
173       DeleteFile(GlobalIconPath);
174     GlobalIconPath := IconThemePath + FIconName + '.' + IconType;
175     gdk_pixbuf_save(GlobalIcon, PChar(GlobalIconPath), IconType, nil, [nil]);
176     { Again it seems that icons can only come from files }
177     app_indicator_set_icon(GlobalAppIndicator, PChar(FIconName));
178   end;
179   { It seems to me you can only set the menu once for an AppIndicator }
180   if (app_indicator_get_menu(GlobalAppIndicator) = nil) and (FTrayIcon.PopUpMenu <> nil) then
181     //app_indicator_set_menu(GlobalAppIndicator, {%H-}PGtkMenu(FTrayIcon.PopUpMenu.Handle));
182     app_indicator_set_menu(GlobalAppIndicator, {%H-}PGtkMenu(TGTK3Menu(FTrayIcon.PopUpMenu.Handle).Widget));
183   app_indicator_set_status(GlobalAppIndicator, APP_INDICATOR_STATUS_ACTIVE);
184 end;
185 
186 { TAppIndWSCustomTrayIcon }
187 
TGtk3WSTrayIcon.Hidenull188 class function TGtk3WSTrayIcon.Hide(const ATrayIcon: TCustomTrayIcon): Boolean;
189 var
190   T: TAppIndTrayIconHandle;
191 begin
192   if ATrayIcon.Handle <> 0 then
193   begin
194     T := TAppIndTrayIconHandle(ATrayIcon.Handle);
195     ATrayIcon.Handle := 0;
196     T.Free;
197   end;
198   Result := True;
199 end;
200 
TGtk3WSTrayIcon.Shownull201 class function TGtk3WSTrayIcon.Show(const ATrayIcon: TCustomTrayIcon): Boolean;
202 var
203   T: TAppIndTrayIconHandle;
204 begin
205   if ATrayIcon.Handle = 0 then
206   begin
207     T := TAppIndTrayIconHandle.Create(ATrayIcon);
208     ATrayIcon.Handle := HWND(T);
209   end;
210   Result := True;
211 end;
212 
213 class procedure TGtk3WSTrayIcon.InternalUpdate(const ATrayIcon: TCustomTrayIcon);
214 var
215   T: TAppIndTrayIconHandle;
216 begin
217   if ATrayIcon.Handle <> 0 then
218   begin
219     T := TAppIndTrayIconHandle(ATrayIcon.Handle);
220     T.Update;
221   end;
222 end;
223 
TGtk3WSTrayIcon.GetPositionnull224 class function TGtk3WSTrayIcon.GetPosition(const ATrayIcon: TCustomTrayIcon): TPoint;
225 begin
226   Result := Point(0, 0);
227 end;
228 
229 { AppIndicatorInit }
230 
231 var
232   Loaded: Boolean;
233   Initialized: Boolean;
234 
Gtk3AppIndicatorInitnull235 function Gtk3AppIndicatorInit: Boolean;
236 var
237   Module: HModule;
238 
TryLoadnull239   function TryLoad(const ProcName: string; var Proc: Pointer): Boolean;
240   begin
241     Proc := GetProcAddress(Module, ProcName);
242     Result := Proc <> nil;
243   end;
244 
245 begin
246   Result := False;
247   if Loaded then
248     Exit(Initialized);
249   Loaded := True;
250   if Initialized then
251     Exit(True);
252   Module := LoadLibrary(libappindicator_3);        // might have several package names, see wiki
253   if Module = 0 then
254      Exit;
255   Result :=
256     TryLoad('app_indicator_get_type', @app_indicator_get_type) and
257     TryLoad('app_indicator_new', @app_indicator_new) and
258     TryLoad('app_indicator_new_with_path', @app_indicator_new_with_path) and
259     TryLoad('app_indicator_set_status', @app_indicator_set_status) and
260     TryLoad('app_indicator_set_attention_icon', @app_indicator_set_attention_icon) and
261     TryLoad('app_indicator_set_menu', @app_indicator_set_menu) and
262     TryLoad('app_indicator_set_icon', @app_indicator_set_icon) and
263     TryLoad('app_indicator_set_label', @app_indicator_set_label) and
264     TryLoad('app_indicator_set_icon_theme_path', @app_indicator_set_icon_theme_path) and
265     TryLoad('app_indicator_set_ordering_index', @app_indicator_set_ordering_index) and
266     TryLoad('app_indicator_get_id', @app_indicator_get_id) and
267     TryLoad('app_indicator_get_category', @app_indicator_get_category) and
268     TryLoad('app_indicator_get_status', @app_indicator_get_status) and
269     TryLoad('app_indicator_get_icon', @app_indicator_get_icon) and
270     TryLoad('app_indicator_get_icon_theme_path', @app_indicator_get_icon_theme_path) and
271     TryLoad('app_indicator_get_attention_icon', @app_indicator_get_attention_icon) and
272     TryLoad('app_indicator_get_menu', @app_indicator_get_menu) and
273     TryLoad('app_indicator_get_label', @app_indicator_get_label) and
274     TryLoad('app_indicator_get_label_guide', @app_indicator_get_label_guide) and
275     TryLoad('app_indicator_get_ordering_index', @app_indicator_get_ordering_index);
276   Initialized := Result;
277 end;
278 
279 initialization
280   GlobalAppIndicator := nil;
281   GlobalIconPath := '';
282 finalization
283   if FileExists(GlobalIconPath) then
284     DeleteFile(GlobalIconPath);
285   if GlobalAppIndicator <> nil then
286     g_object_unref(GlobalAppIndicator);
287 end.
288