1
{%mainunit carbonwsextctrls.pas}
6
TCDCocoaTrayIcon = objcclass(NSObject)
9
LCLTrayIcon: TCustomTrayIcon;
12
WSBitmap: TCocoaBitmap;//image: NSImage;
14
EmptyMenuTitle: CFStringRef;
15
(* // The following lists store the items and are used
16
// to be able to release them in ReleaseMenu
18
// SubMenuOwners: Holds all internal owners of the submenus
19
// SubMenuItems: Holds all items in submenus
20
SubMenuOwners: array of NSMenu;
21
SubMenuItems: array of NSMenuItem;
22
SubMenuImages: array of NSImage;
23
{ Structural Methods }
24
constructor Create; override;
25
destructor Destroy; override;
26
class function getClass: lobjc.id; override;
27
procedure AddMethods; override;
29
function TrimAllChar(const S: string; const ch: Char): string;
30
function CreateMenu(APopUpMenu: TPopUpMenu): NSMenu;
31
function RecursiveCreateMenuItems(AMenuItem: TMenuItem;
32
ACallbackName: string; ACallbackClass: NSObject): NSMenuItem;
33
function CreateMenuItem(AMenuItem: TMenuItem;
34
ACallbackName: string; ACallbackClass: NSObject): NSMenuItem;
35
procedure ReleaseMenu();
36
procedure RemoveIcon();*)
37
function ConvertTIconToWSBitmap(AIcon: TIcon): TCocoaBitmap; message 'converticon:';
38
function ConvertTBitmapToWSBitmap(ABitmap: TBitmap): TCocoaBitmap; message 'convertbitmap:';
39
(* function IsMenuVisible: Boolean;
40
{ Objective-C compatible methods }
41
class procedure HandleMenuItemClick(_self: lobjc.id; _cmd: SEL; sender: lobjc.id); cdecl; //static;
42
class procedure HandleMenuWillOpen(_self: lobjc.id; _cmd: SEL; sender: lobjc.id); cdecl; //static;
43
class procedure HandleMenuDidClose(_self: lobjc.id; _cmd: SEL; sender: lobjc.id); cdecl; //static;*)
47
Str_TPrivateCocoaCarbonTrayIcon = 'TTrayIcon';
49
Str_HandleMenuItemClick = 'HandleMenuItemClick';
50
Str_HandleMenuWillOpen = 'menuWillOpen:';
51
Str_HandleMenuDidClose = 'menuDidClose:';
53
{ TPrivateCocoaCarbonTrayIcon }
56
Adds methods to the class
58
Details of the parameters string:
60
The first parameter is the result (v = void),
61
followed by self and _cmd (@ = id and : = SEL),
62
and on the end "sender" (@ = id)
64
procedure TPrivateCocoaCarbonTrayIcon.AddMethods;
66
AddMethod(Str_HandleMenuItemClick, 'v@:@', Pointer(HandleMenuItemClick));
67
AddMethod(Str_HandleMenuWillOpen, 'v@:@', Pointer(HandleMenuWillOpen));
68
AddMethod(Str_HandleMenuDidClose, 'v@:@', Pointer(HandleMenuDidClose));
71
constructor TPrivateCocoaCarbonTrayIcon.Create;
73
{ The class is registered on the Objective-C runtime before the NSObject constructor is called }
74
// The original plan was to create a descendent class, but causes wierd crashes
75
// so now we just stuck our method into NSObject
76
// if not CreateClassDefinition(Str_TPrivateCocoaCarbonTrayIcon, Str_NSObject) then WriteLn('Failed to create lobjc class');
78
EmptyMenuTitle := CFStringCreateWithPascalString(nil, '', kCFStringEncodingUTF8);
83
destructor TPrivateCocoaCarbonTrayIcon.Destroy;
85
CFRelease(EmptyMenuTitle);
106
class function TPrivateCocoaCarbonTrayIcon.getClass: lobjc.id;
108
Result := objc_getClass({Str_TPrivateCocoaCarbonTrayIcon} Str_NSObject);
111
{Removes/replaces all occurences of a character from a string}
112
function TPrivateCocoaCarbonTrayIcon.TrimAllChar(const S: string; const ch: Char): string;
118
{while Pos finds a blank}
119
while (Pos(ch, buf) > 0) do
121
{copy the substrings before the blank in to Result}
122
Result := Result + Copy(buf, 1, Pos(ch, buf) - 1);
123
buf := Copy(buf, Pos(ch, buf) + 1, Length(buf) - Pos(ch, buf));
125
{There will still be a remainder in buf, so copy remainder into Result}
126
Result := Result + buf;
129
{ Creates a NSMenu structure representing a TPopUpMenu }
130
function TPrivateCocoaCarbonTrayIcon.CreateMenu(APopUpMenu: TPopUpMenu): NSMenu;
135
Result := NSMenu.initWithTitle(EmptyMenuTitle);
136
// Result.setVersion(0);
137
Result.setDelegate(Self.Handle);
138
Result.setAutoenablesItems(LongBool(NO)); // For menu enabling/disabling
140
for i := 0 to APopUpMenu.Items.Count - 1 do
142
{ If the submenu has a submenu it needs special treatment }
143
if APopUpMenu.Items[i].Count > 0 then
144
Item := RecursiveCreateMenuItems(APopUpMenu.Items[i], Str_HandleMenuItemClick, Self)
146
Item := CreateMenuItem(APopUpMenu.Items[i], Str_HandleMenuItemClick, Self);
149
Result.addItem(Item.Handle);
153
function TPrivateCocoaCarbonTrayIcon.RecursiveCreateMenuItems(
154
AMenuItem: TMenuItem; ACallbackName: string; ACallbackClass: NSObject): NSMenuItem;
156
j, subindex: Integer;
157
InternalOwner: NSMenu;
160
// First create the menu
161
Result := CreateMenuItem(AMenuItem, Str_HandleMenuItemClick, Self);
163
// Then a owner for the children
164
InternalOwner := NSMenu.initWithTitle(EmptyMenuTitle);
165
InternalOwner.setAutoenablesItems(LongBool(NO)); // For menu enabling/disabling
167
subindex := Length(SubMenuOwners);
168
SetLength(SubMenuOwners, subindex + 1);
169
SubMenuOwners[subindex] := InternalOwner;
171
{ Add all submenus in this submenu }
172
for j := 0 to AMenuItem.Count - 1 do
174
if AMenuItem.Items[j].Count > 0 then
175
SubItem := RecursiveCreateMenuItems(AMenuItem.Items[j], Str_HandleMenuItemClick, Self)
177
SubItem := CreateMenuItem(AMenuItem.Items[j], Str_HandleMenuItemClick, Self);
179
if SubItem <> nil then
180
InternalOwner.addItem(SubItem.Handle);
183
// And set the submenu to the item
184
Result.setSubmenu(InternalOwner.Handle);
187
function TPrivateCocoaCarbonTrayIcon.CreateMenuItem(AMenuItem: TMenuItem;
188
ACallbackName: string; ACallbackClass: NSObject): NSMenuItem;
190
ItemText: CFStringRef;
191
KeyText: CFStringRef;
192
subitemindex: Integer;
193
subimageindex: Integer;
196
// Default property implementation (=bold)
197
FontManager: NSFontManager;
198
AttrString: NSAttributedString;
199
AttrStringFont: NSFont;
200
AttrDictionary: NSDictionary;
204
{ Check visibility, invisible menus are implemented by not adding them at all,
205
because NSMenuItem.setHidden was only added in Mac OS X 10.5 }
206
if not AMenuItem.Visible then Exit;
208
{ The MenuItem is a separator }
209
if AMenuItem.Caption = '-' then
211
Result := NSMenuItem.separatorItem();
213
{ A normal menu item }
216
{ While creating the menus we ignore the & shortcut identifiers }
217
StrBuffer := TrimAllChar(AMenuItem.Caption, '&');
218
KeyText := CFStringCreateWithPascalString(nil, '', kCFStringEncodingUTF8);
219
ItemText := CFStringCreateWithPascalString(nil, StrBuffer, kCFStringEncodingUTF8);
220
{$ifdef VerboseCarbonTrayIcon}
221
WriteLn(' ItemText: ', IntToHex(Int64(ItemText), 8), ' ATitle: ', AMenuItem.Caption);
224
Result := NSMenuItem.initWithTitle_action_keyEquivalent(ItemText, nil, KeyText);
226
{ Assign the OnClick event handler }
227
Result.setTarget(ACallbackClass.Handle);
228
Result.setAction(sel_registerName(PChar(ACallbackName)));
230
{ Assign the checked state }
231
if AMenuItem.Checked then Result.setState(NSOnState)
232
else Result.setState(NSOffState);
234
{ Assign default (=bold) state }
235
if AMenuItem.Default then
237
FontManager := NSFontManager.sharedFontManager;
238
// For now hard-code the menu font to 14, because the default size
239
// is 13, which is wrong, and looks bad.
240
AttrStringFont := NSFont.menuFontOfSize(14); // 0 = default size
241
AttrStringFont.Handle := FontManager.convertFont_toHaveTrait_(AttrStringFont.Handle, NSBoldFontMask);
242
AttrDictionary := NSDictionary.dictionaryWithObject_forKey(AttrStringFont.Handle, lobjc.id(NSFontAttributeName));
243
AttrString := NSAttributedString.initWithString_attributes(ItemText, CFDictionaryRef(AttrDictionary.Handle));
244
if AttrString.Handle <> nil then
245
Result.setAttributedTitle(AttrString.Handle);
247
// Only objects acquired with routines with alloc,
248
// init or copy in the name should be manually released
250
AttrDictionary.Handle := nil;
252
AttrStringFont.Handle := nil;
254
FontManager.Handle := nil;
258
{ Assign enabled/disabled state }
259
if AMenuItem.Enabled then Result.setEnabled(LongBool(YES))
260
else Result.setEnabled(LongBool(NO));
262
{ Assign the item image, if any }
263
if (AMenuItem.Bitmap <> nil) and (not AMenuItem.Bitmap.Empty) then
265
AImage := ConvertTBitmapToNSImage(AMenuItem.Bitmap);
266
Result.setImage(AImage.Handle);
268
// We also need to free the images
269
subimageindex := Length(SubMenuImages);
270
SetLength(SubMenuImages, subimageindex + 1);
271
SubMenuImages[subimageindex] := AImage;
274
{ We use the Tag to hold the LCL MenuItem
275
RepresentedObject was also tried, by it crashed.
276
Cocoa probably tryes to use it as a real Cocoa object }
277
Result.setTag(PtrInt(AMenuItem));
279
{ Never add separators to the list of items to be freed }
280
subitemindex := Length(SubMenuItems);
281
SetLength(SubMenuItems, subitemindex + 1);
282
SubMenuItems[subitemindex] := Result;
286
procedure TPrivateCocoaCarbonTrayIcon.ReleaseMenu();
290
for i := 0 to Length(SubMenuOwners) - 1 do
291
if SubMenuOwners[i] <> nil then SubMenuOwners[i].Free;
292
for i := 0 to Length(SubMenuItems) - 1 do
293
if (SubMenuItems[i] <> nil) then SubMenuItems[i].Free;
294
for i := 0 to Length(SubMenuImages) - 1 do
295
if (SubMenuImages[i] <> nil) then SubMenuImages[i].Free;
297
SetLength(SubMenuOwners, 0);
298
SetLength(SubMenuItems, 0);
299
SetLength(SubMenuImages, 0);
311
procedure TPrivateCocoaCarbonTrayIcon.RemoveIcon();
314
bar.removeStatusItem(item.Handle);
317
function TCDCocoaTrayIcon.ConvertTIconToWSBitmap(AIcon: TIcon): TCocoaBitmap;
321
AcurrentContext: NSGraphicsContext;*)
325
(* if (AIcon = nil) or (AIcon.Empty) then Exit;
327
{ Convert our CFImageRef to a NSImage }
329
ASize.width := TCarbonBitmap(AIcon.Handle).Width;
330
ASize.height := TCarbonBitmap(AIcon.Handle).Height;
331
ACGRect.size.width := ASize.width;
332
ACGRect.size.height := ASize.height;
333
ACGRect.origin.x := 0;
334
ACGRect.origin.y := 0;
336
Result := NSImage.initWithSize(ASize);
337
Result.setCacheMode(NSImageCacheNever);
339
AcurrentContext := NSGraphicsContext.currentContext();
340
CGContextDrawImage(AcurrentContext.graphicsPort, ACGRect, TCarbonBitmap(AIcon.Handle).CGImage);
341
{$ifdef VerboseCarbonTrayIcon}
342
WriteLn('::[TCarbonWSCustomTrayIcon.Show]',
343
' AcurrentContext ', IntToHex(PtrUInt(Pointer(AcurrentContext)), 8),
344
' AcurrentContext.ClassID ', IntToHex(Int64(AcurrentContext.ClassID), 8),
345
' AcurrentContext.Handle ', IntToHex(Int64(AcurrentContext.Handle), 8),
346
' AcurrentContext.graphicsPort ', IntToHex(Int64(AcurrentContext.graphicsPort), 8)
348
{$endif VerboseCarbonTrayIcon}
349
Result.unlockFocus;*)
352
function TCDCocoaTrayIcon.ConvertTBitmapToWSBitmap(ABitmap: TBitmap): TCocoaBitmap;
356
AcurrentContext: NSGraphicsContext;*)
360
(* if (ABitmap = nil) or (ABitmap.Empty) then Exit;
362
{ Convert our CFImageRef to a NSImage }
364
ASize.width := TCarbonBitmap(ABitmap.Handle).Width;
365
ASize.height := TCarbonBitmap(ABitmap.Handle).Height;
366
ACGRect.size.width := ASize.width;
367
ACGRect.size.height := ASize.height;
368
ACGRect.origin.x := 0;
369
ACGRect.origin.y := 0;
371
Result := NSImage.initWithSize(ASize);
372
Result.setCacheMode(NSImageCacheNever);
374
AcurrentContext := NSGraphicsContext.currentContext();
375
CGContextDrawImage(AcurrentContext.graphicsPort, ACGRect, TCarbonBitmap(ABitmap.Handle).CGImage);
376
{$ifdef VerboseCarbonTrayIcon}
377
WriteLn('::[TCarbonWSCustomTrayIcon.Show]',
378
' AcurrentContext ', IntToHex(PtrUInt(Pointer(AcurrentContext)), 8),
379
' AcurrentContext.ClassID ', IntToHex(Int64(AcurrentContext.ClassID), 8),
380
' AcurrentContext.Handle ', IntToHex(Int64(AcurrentContext.Handle), 8),
381
' AcurrentContext.graphicsPort ', IntToHex(Int64(AcurrentContext.graphicsPort), 8)
383
{$endif VerboseCarbonTrayIcon}
384
Result.unlockFocus;*)
387
(*// Using private APIs might cause a rejection in the Apple AppStore
388
// See: http://bugs.freepascal.org/view.php?id=19025
389
// But on the other hand there is no other way to check if the menu is visible
390
// http://www.cocoabuilder.com/archive/cocoa/100570-checking-if-menu-is-visible.html
391
{$ifdef CarbonUsePrivateAPIs}
392
function _NSGetCarbonMenu(AMenu: lobjc.id {NSMenu}): MenuRef; cdecl; external name '_NSGetCarbonMenu';
395
function TPrivateCocoaCarbonTrayIcon.IsMenuVisible: Boolean;
396
{$ifdef CarbonUsePrivateAPIs}
399
theMenuTrackingData: MenuTrackingData;
403
if menu = nil then Exit;
405
CarbonMenu := _NSGetCarbonMenu(menu.Handle);
406
if CarbonMenu = nil then Exit;
408
Result := GetMenuTrackingData(CarbonMenu, theMenuTrackingData) = noErr;
416
{ Here we try to get the LCL MenuItem from the Tag and then call
417
it's OnClick method }
418
class procedure TPrivateCocoaCarbonTrayIcon.HandleMenuItemClick(_self: lobjc.id;
419
_cmd: SEL; sender: lobjc.id); cdecl; //static;
421
AMenuItem: NSMenuItem;
424
AMenuItem := NSMenuItem.CreateWithHandle(lobjc.id(_cmd));
425
LCLMenu := TMenuItem(PtrInt(AMenuItem.Tag()));
426
if (LCLMenu <> nil) and Assigned(LCLMenu.OnClick) then LCLMenu.OnClick(LCLMenu);
429
class procedure TPrivateCocoaCarbonTrayIcon.HandleMenuWillOpen(_self: lobjc.id;
430
_cmd: SEL; sender: lobjc.id); cdecl; //static;
433
//LCLMenu: TPopUpMenu;
435
AMenu := NSMenu.CreateWithHandle(sender);
437
// LCLMenu := TPopUpMenu(PtrInt(AMenu.menuRepresentation()));
438
// if (LCLMenu <> nil) and Assigned(LCLMenu.OnPopUp) then LCLMenu.OnPopUp(LCLMenu);
441
class procedure TPrivateCocoaCarbonTrayIcon.HandleMenuDidClose(_self: lobjc.id;
442
_cmd: SEL; sender: lobjc.id); cdecl; //static;
445
//LCLMenu: TPopUpMenu;
447
AMenu := NSMenu.CreateWithHandle(sender);
448
// LCLMenu := TPopUpMenu(PtrInt(AMenu.menuRepresentation()));
449
// if (LCLMenu <> nil) and Assigned(LCLMenu.OnClose) then LCLMenu.OnClose(LCLMenu);
452
{ TCarbonWSCustomTrayIcon }
454
class function TCDWSCustomTrayIcon.Hide(const ATrayIcon: TCustomTrayIcon): Boolean;
459
class function TCDWSCustomTrayIcon.Show(const ATrayIcon: TCustomTrayIcon): Boolean;
461
ATrayIconHandle: TCDCocoaTrayIcon;
463
{$ifdef VerboseCDTrayIcon}
464
DebugLn(':>[TCarbonWSCustomTrayIcon.Show]');
465
{$endif VerboseCDTrayIcon}
469
{ Creates the handle }
471
ATrayIconHandle := TCDCocoaTrayIcon.alloc.init;
472
ATrayIconHandle.bar := NSStatusBar.systemStatusBar();
473
ATrayIconHandle.LCLTrayIcon := ATrayIcon;
474
ATrayIcon.Handle := HWND(ATrayIconHandle);
476
{ Converts the icon to NSImage }
478
ATrayIconHandle.WSBitmap := ATrayIconHandle.ConvertTIconToWSBitmap(ATrayIcon.Icon);
482
if ATrayIconHandle.item <> nil then Exit(True);
484
ATrayIconHandle.item := ATrayIconHandle.bar.statusItemWithLength(NSSquareStatusItemLength);
485
ATrayIconHandle.item.retain();
486
if Assigned(ATrayIconHandle.WSBitmap) and Assigned(ATrayIconHandle.WSBitmap.image) then
487
ATrayIconHandle.item.setImage(ATrayIconHandle.WSBitmap.image);
488
ATrayIconHandle.item.setHighlightMode(True);
490
(* { Inserts the menu }
492
if ATrayIcon.PopUpMenu <> nil then
494
APrivateTrayIcon.menu := APrivateTrayIcon.CreateMenu(ATrayIcon.PopUpMenu);
495
if APrivateTrayIcon.item <> nil then
496
APrivateTrayIcon.item.setMenu(APrivateTrayIcon.menu.Handle);
501
(* {$ifdef VerboseCDTrayIcon}
502
WriteLn(':<[TCarbonWSCustomTrayIcon.Show]',
503
' Handle: ', IntToHex(ATrayIcon.Handle, 8),
504
' ACGRect.size.width: ', ACGRect.size.width,
505
' ACGRect.size.height: ', ACGRect.size.height,
506
' ACGRect.origin.x: ', ACGRect.origin.x,
507
' ACGRect.origin.y: ', ACGRect.origin.y,
508
' TCarbonBitmap(ATrayIcon.Icon.Handle).CGImage ', IntToHex(Int64(TCarbonBitmap(ATrayIcon.Icon.Handle).CGImage), 8)
510
{$endif VerboseCDTrayIcon}*)
513
class procedure TCDWSCustomTrayIcon.InternalUpdate(const ATrayIcon: TCustomTrayIcon);
515
(* APrivateTrayIcon := TPrivateCocoaCarbonTrayIcon(ATrayIcon.Handle);
517
if APrivateTrayIcon = nil then Exit;
519
// The update is only necessary for a visible TTrayIcon
520
if not ATrayIcon.Visible then Exit;
522
{ Updates the image }
524
if Assigned(APrivateTrayIcon.Image) then
525
APrivateTrayIcon.image.Free;
527
if Assigned(ATrayIcon.Icon) then
529
APrivateTrayIcon.image := APrivateTrayIcon.ConvertTIconToNSImage(ATrayIcon.Icon);
530
APrivateTrayIcon.item.setImage(APrivateTrayIcon.image.Handle);
535
APrivateTrayIcon.ReleaseMenu();
536
if ATrayIcon.PopUpMenu <> nil then
538
APrivateTrayIcon.menu := APrivateTrayIcon.CreateMenu(ATrayIcon.PopUpMenu);
539
if APrivateTrayIcon.item <> nil then
540
APrivateTrayIcon.item.setMenu(APrivateTrayIcon.menu.Handle);
544
class function TCDWSCustomTrayIcon.ShowBalloonHint(const ATrayIcon: TCustomTrayIcon): Boolean;
549
class function TCDWSCustomTrayIcon.GetPosition(const ATrayIcon: TCustomTrayIcon): Types.TPoint;
551
Result := Types.Point(0, 0);