Difference between revisions of "Cocoa Internals"

From Lazarus wiki
Jump to navigationJump to search
Line 32: Line 32:
 
* use '''protocol''' (similar to Interfaces in Object Pascal form). Any class, sub-classing from standard Cocoa classes, would have to implement the '''protocol'''. The downside of this approach is - a lot of code would be duplicated.
 
* use '''protocol''' (similar to Interfaces in Object Pascal form). Any class, sub-classing from standard Cocoa classes, would have to implement the '''protocol'''. The downside of this approach is - a lot of code would be duplicated.
 
* use '''categories''' (similar to Javascript prototypes, not available in Object Pascal. Class-helpers looks alike, but are not the same). Categories allow to add methods (only methods! adding variables is not allowed) to any base class of Objective-C. Any descendants of this class automatically acquire the method, and it's default implementation. Also any subclass, can override the default implementation with its own. Apple doesn't recommend the use of categories (due to ambiguity and possible conflicts among different libraries introducing their own extensions). See more at the [[Cocoa Internals/Extensions|Cocoa Internals Extensions]] page
 
* use '''categories''' (similar to Javascript prototypes, not available in Object Pascal. Class-helpers looks alike, but are not the same). Categories allow to add methods (only methods! adding variables is not allowed) to any base class of Objective-C. Any descendants of this class automatically acquire the method, and it's default implementation. Also any subclass, can override the default implementation with its own. Apple doesn't recommend the use of categories (due to ambiguity and possible conflicts among different libraries introducing their own extensions). See more at the [[Cocoa Internals/Extensions|Cocoa Internals Extensions]] page
 
==Handles==
 
* Window handle (HWND) is always NSView.
 
** TCustomWSForm it is content NSView.
 
** Any control that has scroll bars (i.e. TCustomWSList) the it its the embedding NSScrollView (TCocoaScrollView)
 
 
{| class="wikitable" style="width:100%"
 
! LCL Control
 
! Cocoa Class as .Handle
 
! Notes
 
|-
 
|TMemo
 
|NSTextView inside a NSScrollView
 
|
 
|-
 
|TGroupBox
 
|TCocoaGroupBox (NSBox)
 
|There's also a content view created to hold all the child controls within the box
 
|}
 
  
 
==Code Style==
 
==Code Style==

Revision as of 03:52, 20 June 2019

The page is about Cocoa Widgetset internal implementation. The page should be useful for Cocoa widgetset developers (maintainers and/or constributors) as well as any developers who need to use Cocoa specific API.

Minimum Version

  • some part of cocoa are written using Objective C 2.0 features. Objc 2.0 features used:
Enumeration in Objective-C classes
  • minimal macOS version is
  • 10.5 - spiritual
  • 10.6 - de facto (a lot of 10.6 only API's used without any actual verification of the proper macOS version)

There's a reasoning to keep the backwards compatibility for 10.6. This is the last system that supports Rosetta application allowing to run powerpc apps on Intel processor.

Recompilation

If you tried to modify Cocoa-Widget and the next attempt of the project or Cocoa compilation it throws an unexpected error like "cocoawscommon.pas(y:x) Error: identifier idents no member "lclFrame"", you need to delete all cocoa compiled units.

 ./lazarus/lcl/units/i386-darwin/cocoa

or

 ./lazarus/lcl/units/x86_64-darwin/cocoa

(to be reported - compiler issue with ignoring extensions)

Reconfigure LCL package

LCL configure cocoa to removeunits.png

Another way is to configure LCL package to always remove .o files before recompilation of the package itself. (The same effect as removing it from command line)

  • Open LCL package ( Package -> Open Loaded Package ... ->LCL )
  • Select (package) "Options"
  • Select "Compiler Command"
  • specify command for "Execute before"
sh -c "rm -f ../units/$(TargetCPU)-$(TargetOS)/$(LCLWidgetType)/*"

note that use of sh is necessary, because Lazarus IDE doesn't process * (asterisk)

LCL specific ObjC classes

In order to control and handle an NSView's behavior LCL uses decedent classes from standard Cocoa controls. I.e. for NSWindow TCocoaWindow is introduced. The decedent are used for the purpose of "overriding" default class implementation, where it is needed. In some cases using delegate classes is not enough.

There are two ways, in order to provide a common LCL(friendly) API interface for all Cocoa classes or sub-classes:

  • use protocol (similar to Interfaces in Object Pascal form). Any class, sub-classing from standard Cocoa classes, would have to implement the protocol. The downside of this approach is - a lot of code would be duplicated.
  • use categories (similar to Javascript prototypes, not available in Object Pascal. Class-helpers looks alike, but are not the same). Categories allow to add methods (only methods! adding variables is not allowed) to any base class of Objective-C. Any descendants of this class automatically acquire the method, and it's default implementation. Also any subclass, can override the default implementation with its own. Apple doesn't recommend the use of categories (due to ambiguity and possible conflicts among different libraries introducing their own extensions). See more at the Cocoa Internals Extensions page

Code Style

The following requirement applies for Cocoa widgetset. Other widgetset my be following some other rules (historically), but in general LCL follows the same requirements.

  • Operators Keep the operator separated by spaces between operands
 A := B; 
 A := B * C;
  • Blocks The main rule is to make 2 character spacing from the code block start. Begin / Else should start on a new line.
procedure B;
begin
  if A then // 2 character spacing from begin
  begin
    Start Here // 2 character spacing from begin
    Next Line
  end
  else
  begin
    Another Line
  end;
end;
  • Standard Function Name please keep naming in ProperCase. Reserved words should be lower case. Name of (global/local) variables and fields should match declaration. (local variables should start with lower case)
 if not Assigned(A) then
 begin
   Result := nil;
 end;

Using for-in instead of for

When dealing if arrays, it's common for Pascal to write the code like, using "Integer" type array index

var
  i : integer;
  l : TList;
...
 for i:=0 to l.Count-1 do
   ...

Cocoa provides it's own implementation of arrays using NSArray class. Writing the the loop in the same manner:

var
  i : integer;
  l : NSArray;
...
 for i:=0 to l.count-1 do
   ...

is possible, though not desired.

The reason for that, is that NSArray "count" is type of NSUInteger for Cocoa. The type has a different size on 32 and 64 platforms. (While "Integer" type is always 32 bit in FPC).

Thus while the code written above might work just fine on 32-platform, it might cause range check error (if such check is enabled) on 64 bit machine. For example, when count is zero. Here's a simplest (no ObjC) example of the problem

{$R+}
function Count: qword;
begin
  Result := 0;
end;
var
  i: Integer;
begin
  for i := 0 to Count-1 do // <- throws a range check error, because QWord result doesn't fit for Integer index
    WriteLn(i);
end.

However, running a loop from elements of an array is quite a common task. For example - changing attributes of all children controls, due to some action.

Instead of using a higher size Index variable (i.e. PtrInt or PtrUInt), it's handy to use for-in loop. Most common Objective C classes provide a special feature (NSFastEnumerator protocol) to do enumeration through its member. It works for 32 and well as 64 platform, in the same manner, but requires Objc 2.0 runtime version (with objectivec2 modeswitch enabled)

An iteration for-in loop might look like this:

var
  obj : NSObject;
  arr : NSArray;
...
 for obj in arr do
   ...

Writing a Cocoa app without the LCL

This is useful for testing hard to debug bugs in the LCL. Here is an example program written in Pascal-Cocoa which creates a window with 2 buttons and a simple set of menus:

program cocoa_without_lcl;

{$mode objfpc}{$H+}
{$modeswitch objectivec1}

uses
  CocoaAll, classes, sysutils;

type
  { TMyDelegate }
  TMyDelegate = objcclass(NSObject)
  public
    procedure HandleButtonClick_A(sender: id); message 'HandleButtonClick_A:';
    procedure HandleButtonClick_B(sender: id); message 'HandleButtonClick_B:';
    procedure HandleMenuClick_A(sender: id); message 'HandleMenuClick_A:';
    procedure HandleMenuClick_B(sender: id); message 'HandleMenuClick_B:';
  end;

var
  appName: NSString;
  window: NSWindow;
  pool: NSAutoreleasePool;
  lText: NSTextField;
  lButton: NSButton;
  lDelegate: TMyDelegate;
  //
  MainMenu_A: NSMenu;
  TopItem_A: NSMenuItem;
  TopMenu_A: NSMenu;
  MenuItem_A: NSMenuItem;
  //
  MainMenu_B: NSMenu;
  TopItem_B: NSMenuItem;
  TopMenu_B: NSMenu;
  MenuItem_B: NSMenuItem;

procedure TMyDelegate.HandleButtonClick_A(sender: id);
begin
  NSApp.setMainMenu(MainMenu_A);
end;

procedure TMyDelegate.HandleButtonClick_B(sender: id);
begin
  NSApp.setMainMenu(MainMenu_B);
end;

procedure TMyDelegate.HandleMenuClick_A(sender: id);
var
  Str: string;
begin
  Str := 'A '+inttostr(random(888));
  window.setTitle(NSSTR(@Str[1]));
end;

procedure TMyDelegate.HandleMenuClick_B(sender: id);
var
  Str: string;
begin
  Str := 'B '+inttostr(random(888));
  window.setTitle(NSSTR(@Str[1]));
end;

procedure CreateMenus;
var
  nsTitle: NSString;
  nsKey  : NSString;

  function CreateMenuItem(AName, AShortcut: string; ASelector: SEL): NSMenuItem;
  begin
    nsKey := NSSTR(PChar(AShortcut));
    nsTitle := NSSTR(PChar(AName));
    Result := NSMenuItem.alloc.initWithTitle_action_keyEquivalent(nsTitle, ASelector, nsKey);
    Result.setKeyEquivalentModifierMask(NSCommandKeyMask);
    nsTitle.release;
    nsKey.release;
    Result.setKeyEquivalentModifierMask(NSCommandKeyMask);
    Result.setTarget(lDelegate);
  end;

begin
  MainMenu_A := NSMenu.alloc.initWithTitle(NSSTR('Menu A'));
  TopItem_A := CreateMenuItem('TopItem A', '', nil);
  MainMenu_A.insertItem_atIndex(TopItem_A, 0);
  TopMenu_A := NSMenu.alloc.initWithTitle(NSSTR('TopMenu A'));
  TopItem_A.setSubmenu(TopMenu_A);
  MenuItem_A := CreateMenuItem('MenuItem A', 'A', objcselector('HandleMenuClick_A:'));
  MenuItem_A.setTarget(lDelegate);
  TopMenu_A.insertItem_atIndex(MenuItem_A, 0);

  MainMenu_B := NSMenu.alloc.initWithTitle(NSSTR('Menu B'));
  TopItem_B := CreateMenuItem('TopItem B', '', nil);
  MainMenu_B.insertItem_atIndex(TopItem_B, 0);
  TopMenu_B := NSMenu.alloc.initWithTitle(NSSTR('TopMenu B'));
  TopItem_B.setSubmenu(TopMenu_B);
  MenuItem_B := CreateMenuItem('MenuItem B', 'B', objcselector('HandleMenuClick_B:'));
  MenuItem_B.setTarget(lDelegate);
  TopMenu_B.insertItem_atIndex(MenuItem_B, 0);
end;


begin
  // Autorelease pool, app and window creation
  pool := NSAutoreleasePool.new;
  NSApp := NSApplication.sharedApplication;
  NSApp.setActivationPolicy(NSApplicationActivationPolicyRegular);
  appName := NSProcessInfo.processInfo.processName;
  window := NSWindow.alloc.initWithContentRect_styleMask_backing_defer(NSMakeRect(0, 0, 200, 200),
    NSTitledWindowMask or NSClosableWindowMask or NSMiniaturizableWindowMask,
    NSBackingStoreBuffered, False).autorelease;
  lDelegate := TMyDelegate.alloc.init;

  // text label
  lText := NSTextField.alloc.initWithFrame(NSMakeRect(50, 50, 120, 50)).autorelease;
  lText.setBezeled(False);
  lText.setDrawsBackground(False);
  lText.setEditable(False);
  lText.setSelectable(False);
  lText.setStringValue(NSSTR('NSTextField'));
  window.contentView.addSubview(lText);

  // button
  lButton := NSButton.alloc.initWithFrame(NSMakeRect(50, 100, 120, 50)).autorelease;
  window.contentView.addSubview(lButton);
  lButton.setTitle(NSSTR('Button A!'));
  lButton.setButtonType(NSMomentaryLightButton);
  lButton.setBezelStyle(NSRoundedBezelStyle);
  // button event handler setting
  lButton.setTarget(lDelegate);
  lButton.setAction(ObjCSelector(lDelegate.HandleButtonClick_A));

  // button B
  lButton := NSButton.alloc.initWithFrame(NSMakeRect(50, 150, 120, 50)).autorelease;
  window.contentView.addSubview(lButton);
  lButton.setTitle(NSSTR('Button B!'));
  lButton.setButtonType(NSMomentaryLightButton);
  lButton.setBezelStyle(NSRoundedBezelStyle);
  // button event handler setting
  lButton.setTarget(lDelegate);
  lButton.setAction(ObjCSelector(lDelegate.HandleButtonClick_B));

  // Menus
  CreateMenus();
  NSApp.setMainMenu(MainMenu_A);

  // Window showing and app running
  window.center;
  window.setTitle(appName);
  window.makeKeyAndOrderFront(nil);
  NSApp.activateIgnoringOtherApps(true);
  NSApp.run;
end.

Units

Unit Description
CocoaPrivate The unit contains most of the Cocoa classes overrides. The unit should stay LCL-classes free as much as possible (i.e. should not use Forms, StdCtrls, etc. Though using LCLTypes or LCLProc is ok). But this is not happening :(
CocoaWSCommon The unit contains common bindings between native Cocoa (obcjclasses implemented in CocoaPrivate) and LCL. The actual callbacks interfaces are implemented here.

Most of the "messaging" to LCL is also implemented here

CocoaInt

cocoawinapi.h

cocoalclapi.h

The Widgetset class API (winApi-style) are implemented here.

Callback Objects

  • ICommonCallback is the primary object for Cocoa objects to report back actions taken. It prevents "the handlers" from dealing with Objective-C apis over all.
  • some controls might introduce additional interface to process more events. They all should inherit from ICommonCallback
  • for each HANDLE created there should be exactly one CallBack object allocated, because
  • Callback object is released on Destruction of the HANDLE (from LCL perspective). (even though the actual ObjC object can be released later)

Why so Boolean?

Cocoa Widgetset introduces two ObjC-specific boolean types

  • LCLObjCBoolean - declared at CocoaPrivate and is commonly used for any Cocoa overridden methods
  • ObjCBool - declared at Cocoa_Extra and is used under BOOL8FIX.

Both types were introduced to handle the issues with Boolean type parameter. (Resulting Boolean value seems to be working as expected) on x86_64.

However, the intent of each type is different:

  • LCLObjCBoolean - is to compensate for FPC backwards incompatible change (targeted for 3.2.0 release) that's changing all "Boolean" type parameters to "Boolean8". Thus any overridden methods in LCL would stop compile. (Boolean8 is not an alias for Boolean). Unfortunatelly the type is expected to stay likely forever.
  • ObjCBool - is a need to resolve Boolean parameter problem with 3.0.4. For any call to boolean parameter method, additional methods were introduced to pass the parameter as byte parameter (unsigned char), and pass the actual value property. The type can be dropped as soon as LCL support for 3.0.4 is dropped. The type should not be used in any 3d party components.

See Also