{
 carbonrichmemo.pas 
 
 Author: Dmitry 'skalogryz' Boyarintsev 

 *****************************************************************************
 *                                                                           *
 *  This file is part of the Lazarus Component Library (LCL)                 *
 *                                                                           *
 *  See the file COPYING.modifiedLGPL.txt, included in this distribution,    *
 *  for details about the copyright.                                         *
 *                                                                           *
 *  This program is distributed in the hope that it will be useful,          *
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of           *
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.                     *
 *                                                                           *
 *****************************************************************************
}

unit CarbonRichMemo;

{$mode objfpc}{$H+}

interface

uses
  MacOSAll,

  LCLType, Classes, SysUtils,

  Controls, Graphics, StdCtrls,

  WSRichMemo,

  CarbonDef, CarbonUtils,
  CarbonProc, CarbonEdits;

type

  { TCarbonRichEdit }

  TCarbonRichEdit = class(TCarbonMemo)
  private
    fCaretVisible : Boolean;
    fisBackBlack  : Boolean;
    target  : EventTargetRef;
  protected
    function GetCreationOptions: TXNFrameOptions; override;
    procedure PostDraw(acontext: CGContextRef);
    procedure RegisterEvents; override;
  public
    function GetIndexedRunInfoFromRange(iIndex: ItemCount;   iStartOffset, iEndOffset: TXNOffset;
      var oRunStartOffset, oRunEndOffset: TXNOffset;
      oRunDataType: TXNDataTypePtr; iTypeAttributeCount: ItemCount;
      ioTypeAttributes: TXNTypeAttributesPtr): Boolean;
    function GetContinuousTypeAttributes(var oContinuousFlags: TXNContinuousFlags;
      iCount: ItemCount; var ioTypeAttributes: array of TXNTypeAttributes): Boolean;
    function SetTypeAttributes(iCount: ItemCount; const iTypeAttributes: array of TXNTypeAttributes;
      StartOffset, EndOffset: Integer): Boolean;
    procedure SetColor(const AColor: TColor); override;
    procedure InDelText(const text: WideString; replstart, repllength: Integer);
  end;


  { TCarbonWSCustomRichMemo }

  TCarbonWSCustomRichMemo = class(TWSCustomRichMemo)
    class procedure CutToClipboard(const AWinControl: TWinControl); override;
    class procedure CopyToClipboard(const AWinControl: TWinControl); override;
    class procedure PasteFromClipboard(const AWinControl: TWinControl); override;

    class function CreateHandle(const AWinControl: TWinControl; const AParams: TCreateParams): HWND; override;
    class function GetStyleRange(const AWinControl: TWinControl; TextStart: Integer;
      var RangeStart, RangeLen: Integer): Boolean; override;
    class function GetTextAttributes(const AWinControl: TWinControl; TextStart: Integer;
      var Params: TIntFontParams): Boolean; override;
    class procedure SetTextAttributes(const AWinControl: TWinControl; TextStart, TextLen: Integer;
      {Mask: TTextStyleMask;} const Params: TIntFontParams); override;
    class procedure SetHideSelection(const ACustomEdit: TCustomEdit; AHideSelection: Boolean); override;
    class procedure InDelText(const AWinControl: TWinControl; const TextUTF8: String; DstStart, DstLen: Integer); override;
    class function LoadRichText(const AWinControl: TWinControl; Src: TStream): Boolean; override;
    class function SaveRichText(const AWinControl: TWinControl; Dst: TStream): Boolean; override;
  end;

implementation

// Notes:

// http://developer.apple.com/DOCUMENTATION/Carbon/Reference/Multilingual_Text_Engine/Reference/reference.html
// TXNFlattenObjectToCFDataRef
//
//oDataRef
//   On input, points to a structure of type CFDataRef. On output, points to a flattened
//   version of the text object in the format specified by the iTXNDataType parameter.
//   You are responsible to retain the returned CFDataRef.
//
// It's unclear (though expected), if a user is responsible to release returned CFData object.
//
// Releasing is necessary, as noted this mailling list discussion
// http://lists.apple.com/archives/carbon-dev/2005/Feb/msg00657.html


const
  TXNAttributesMax = 10;

function GetATSUFontName(AStyle: ATSUStyle): String;
var
  fontid: ATSUFontID;
begin
  ATSUGetAttribute(AStyle, kATSUFontTag, sizeof(ATSUFontID), @fontid, nil);
  Result := CarbonFontIDToFontName(fontid);
end;

function GetATSUFontSize(ASTyle: ATSUStyle): Integer;
var
  sz  : fixed;
begin
  ATSUGetAttribute(AStyle, kATSUSizeTag, sizeof(fixed), @sz, nil);
  Result := Fix2Long(sz);
end;

procedure GetATSUFontRGBAColor(AStyle: ATSUStyle; var r,g,b,a: Byte);
var
  rgba : ATSURGBAlphaColor;
begin
  ATSUGetAttribute(AStyle, kATSURGBAlphaColorTag, sizeof(ATSURGBAlphaColor), @rgba, nil);
  r := Round(rgba.red*255);
  g := Round(rgba.green*255);
  b := Round(rgba.blue*255);
  a := Round(rgba.alpha*255);
end;

function GetATSUFontColor(AStyle: ATSUStyle): TColor;
var
  r,g,b,a: Byte;
begin
  GetATSUFontRGBAColor(AStyle, r,g,b,a);
  Result := (b shl 16) or (g shl 8) or r;
end;

function GetATSUFontStyles(AStyle: ATSUStyle): TFontStyles;
var
  b : Boolean;
begin
  b:=false;
  Result := [];
  ATSUGetAttribute(AStyle, kATSUQDBoldfaceTag, sizeof(Boolean), @b, nil);
  if b then Include(Result, fsBold);
  ATSUGetAttribute(AStyle, kATSUQDItalicTag, sizeof(Boolean), @b, nil);
  if b then Include(Result, fsItalic);
  ATSUGetAttribute(AStyle, kATSUQDUnderlineTag, sizeof(Boolean), @b, nil);
  if b then Include(Result, fsUnderline);
  ATSUGetAttribute(AStyle, kATSUStyleStrikeThroughTag , sizeof(Boolean), @b, nil);
  if b then Include(Result, fsStrikeOut);
end;

function GetValidRichEdit(AWinControl: TWinControl): TCarbonRichEdit;
begin
  if Assigned(AWinControl) and (AWinControl.Handle<>0) and (TObject(AWinControl.Handle) is TCarbonRichEdit) then
    Result := TCarbonRichEdit(AWinControl.Handle)
  else
    Result := nil;
end;

procedure AttrSetFontName(const FontName: String; var Attr: TXNTypeAttributes);
begin
  Attr.tag := kATSUFontTag;
  Attr.size := SizeOf(ATSUFontID);
  Attr.data.dataValue := FindCarbonFontID(FontName);
end;

procedure AttrSetColor(var MacColor: RGBColor; var Attr: TXNTypeAttributes);
begin
  Attr.tag := kTXNQDFontColorAttribute;
  Attr.size := kTXNQDFontColorAttributeSize;
  Attr.data.dataPtr := @MacColor;
end;

procedure AttrSetSize(FontSize: Integer; var Attr: TXNTypeAttributes);
begin
  Attr.tag := kTXNQDFontSizeAttribute;
  Attr.size := kTXNQDFontSizeAttributeSize;
  Attr.data.dataValue := Long2Fix(FontSize);
end;

procedure AttrSetStyle(FontStyle: TFontStyles; var Attr: TXNTypeAttributes);
begin
  Attr.tag := kTXNQDFontStyleAttribute;
  Attr.size := kTXNQDFontStyleAttributeSize;
  Attr.data.dataValue := FontStyleToQDStyle(FontStyle)
end;

procedure AttrSetATSUStyle(AStyle: ATSUStyle; var Attr: TXNTypeAttributes);
begin
  Attr.tag := kTXNATSUIStyle;
  Attr.size := kTXNATSUIStyleSize;
  Attr.data.dataPtr := astyle;
end;

procedure ParamsToTXNAttribs({ParamsMask: TTextStyleMask;} const Params: TIntFontParams;
  var Attr: array of TXNTypeAttributes; var AttrCount: Integer; var MacColor: RGBColor);
begin
  AttrCount := 0;
  //todo: replace QuickDraw style by ATSU style

  //if tsm_Color in ParamsMask then begin
    MacColor := ColorToRGBColor(Params.Color);
    AttrSetColor(MacColor, Attr[AttrCount] );
    inc(AttrCount);
  //end;
  

  //if tsm_Name in ParamsMask then begin
    AttrSetFontName(Params.Name, Attr[AttrCount] );
    inc(AttrCount);
  //end;

  //if tsm_Size in ParamsMask then begin
    AttrSetSize(Params.Size, Attr[AttrCount] );
    inc(AttrCount);
  //end;

  //if tsm_Styles in ParamsMask then begin
    AttrSetStyle(Params.Style, Attr[AttrCount]);
    inc(AttrCount);
  //end;
end;

{ TCarbonWSCustomRichMemo }

class procedure TCarbonWSCustomRichMemo.CutToClipboard(const AWinControl: TWinControl);
var
  memo      : TCarbonRichEdit;
begin
  memo := GetValidRichEdit(AWinControl);
  if not Assigned(memo) then Exit;
  TXNCut(memo.GetTextObject);
end;

class procedure TCarbonWSCustomRichMemo.CopyToClipboard(const AWinControl: TWinControl);
var
  memo      : TCarbonRichEdit;
begin
  memo := GetValidRichEdit(AWinControl);
  if not Assigned(memo) then Exit;
  TXNCopy(memo.GetTextObject);
end;

class procedure TCarbonWSCustomRichMemo.PasteFromClipboard(const AWinControl:TWinControl);
var
  memo      : TCarbonRichEdit;
begin
  memo := GetValidRichEdit(AWinControl);
  if not Assigned(memo) then Exit;
  TXNPaste(memo.GetTextObject);
end;

class function TCarbonWSCustomRichMemo.CreateHandle(const AWinControl: TWinControl;
  const AParams: TCreateParams): HWND;
begin
  Result := TLCLIntfHandle(TCarbonRichEdit.Create(AWinControl, AParams));
end;

class function TCarbonWSCustomRichMemo.GetStyleRange(const AWinControl: TWinControl;
  TextStart: Integer; var RangeStart, RangeLen: Integer): Boolean;
var
  edit      : TCarbonRichEdit;
  RngStart  : TXNOffset;
  RngEnd    : TXNOffset;
begin
  Result := false;
  edit := GetValidRichEdit(AWinControl);
  if not Assigned(edit) then Exit;

  Result := edit.GetIndexedRunInfoFromRange(0, TextStart, TextStart, RngStart, RngEnd, nil, 0, nil);
  if Result then begin
    RangeStart := RngStart;
    RangeLen := RngEnd - RngStart;
  end;   

end;

class function TCarbonWSCustomRichMemo.GetTextAttributes(const AWinControl: TWinControl;
  TextStart: Integer; var Params: TIntFontParams): Boolean;
var
  edit  : TCarbonRichEdit;
  attr  : array [0..2] of TXNTypeAttributes;
  flags   : TXNContinuousFlags;

  astyle  : ATSUStyle;
  maccolor  : RGBColor;

  oStart  : TXNOffset;
  oEnd    : TXNOffset;
  txobj   : TXNObject;
begin
  Result := false;
  edit := GetValidRichEdit(AWinControl);
  if not Assigned(edit) then Exit;

  txobj := HITextViewGetTXNObject(edit.Widget);
  if not Assigned(txobj) then Exit;
  
  TXNGetSelection(txobj, oStart, oEnd);
  TXNSetSelection(txobj, TextStart, TextStart+1);
 
  ATSUCreateStyle(astyle);
  AttrSetATSUStyle(astyle, attr[0]);
  AttrSetStyle([], attr[1]);
  FillChar(maccolor, sizeof(maccolor), 0);
  AttrSetColor(maccolor, attr[2]);

  Result := edit.GetContinuousTypeAttributes(flags, 3, attr);
  Params.Name := GetATSUFontName(astyle);
  Params.Color := RGBColorToColor(maccolor);
  Params.Style := GetATSUFontStyles(astyle) + QDStyleToFontStyle(attr[1].data.dataValue);
  Params.Size := GetATSUFontSize(astyle);

  ATSUDisposeStyle(astyle);

  TXNSetSelection(txobj, oStart, oEnd);
end;

class procedure TCarbonWSCustomRichMemo.SetTextAttributes(const AWinControl: TWinControl;
  TextStart, TextLen: Integer; {Mask: TTextStyleMask; }const Params: TIntFontParams);
var
  memo      : TCarbonRichEdit;
  Attr      : array [0..TXNAttributesMax-1] of TXNTypeAttributes;
  Count     : Integer;
  maccolor  : RGBColor;
begin
  memo := GetValidRichEdit(AWinControl);
  if not Assigned(memo) then Exit;

  ParamsToTXNAttribs(Params, attr, Count, maccolor);

  memo.SetTypeAttributes(Count, Attr, TextStart, TextStart+TextLen);
end;

class procedure TCarbonWSCustomRichMemo.SetHideSelection(const ACustomEdit: TCustomEdit; AHideSelection: Boolean);
begin
  //todo:
end;

class procedure TCarbonWSCustomRichMemo.InDelText(const AWinControl: TWinControl; const TextUTF8: String; DstStart, DstLen: Integer);
var
  memo   : TCarbonRichEdit;
begin
  memo := GetValidRichEdit(AWinControl);
  if not Assigned(memo) then Exit;
  memo.InDelText(UTF8Decode(TextUTF8), DstStart, DstLen);
end;

function GetTempFileUniqueName(forcedir: Boolean=true): String;
var
  g : TGUID;
  d : String;
begin
  repeat
    CreateGUID(g);
    Result := GetTempFileName +  GUIDToString(g) +'.rtf';
  until not FileExists(Result);
  if forcedir then begin
    d := ExtractFileDir(Result);
    if not DirectoryExists(d) then ForceDirectories(d);
  end;
end;

class function TCarbonWSCustomRichMemo.LoadRichText(const AWinControl: TWinControl;
  Src: TStream): Boolean;
var
  edit  : TCarbonRichEdit;
  filename: String;
  fs  : TFileStream;
  cf  : CFStringRef;
  url : CFURLRef;
begin
  Result := false;
  edit := GetValidRichEdit(AWinControl);
  if not Assigned(edit) then Exit;

  Result := false;
  filename := GetTempFileUniqueName;

  try
    fs := TFileStream.Create(filename, fmCreate);
    try
      fs.CopyFrom(Src, Src.Size - Src.Position);
    finally
      fs.Free;
    end;

    CreateCFString(filename, cf);
    url := CFURLCreateWithFileSystemPath (kCFAllocatorDefault, cf, kCFURLPOSIXPathStyle, false);
    try
      TXNSelectAll(HITextViewGetTXNObject(edit.Widget));
      Result := TXNSetDataFromCFURLRef( HITextViewGetTXNObject(edit.Widget), url, kTXNStartOffset, kTXNEndOffset) = noErr;
    finally
      CFRelease(url);
      FreeCFString(cf);
    end;

  except
    Result := false;
  end;

  if FileExists(filename) then DeleteFile(filename);
end;

class function TCarbonWSCustomRichMemo.SaveRichText(const AWinControl: TWinControl;
  Dst: TStream): Boolean;
var
  edit  : TCarbonRichEdit;
  data  : CFDataRef;
  sz    : Integer;
  ptr   : PByteArray;
begin
  Result := false;
  edit := GetValidRichEdit(AWinControl);
  if not Assigned(edit) or not Assigned(Dst) then Exit;

  Result := TXNFlattenObjectToCFDataRef(HITextViewGetTXNObject(edit.Widget), kTXNRichTextFormatData, data) = noErr;
  if not Result and Assigned(data) then Exit;

  try
    sz := CFDataGetLength(data);
    ptr := CFDataGetBytePtr(data);
    if Assigned(ptr) and (sz > 0) then Dst.Write(ptr^, sz);
    Result := false;
  finally
    // see TXNFlattenObjectToCFDataRef notes
    CFRelease(data);
  end;
end;

{ TCarbonRichEdit }

function TCarbonRichEdit.GetCreationOptions: TXNFrameOptions;
begin
  Result := kOutputTextInUnicodeEncodingMask;
end;

procedure TCarbonRichEdit.PostDraw(acontext: CGContextRef);
var
  point : HIPoint;
  obj   : TXNObject;
  pst,
  pend  : TXNOffset;
  //x,y   : Integer;
  //w,h     : Fixed;
  //linenum : UInt32;
  r       : CGRect;
  //flags   : TXNContinuousFlags;
  //attr    : TXNTypeAttributes;
  event   : EventRef;
  ref     : ControlRef;
  refcon  : Integer;
  //ofs     : LongWord;
  hh      : Word;
const
  caretHorzOffset = 1;
  caretHorzWidth  = 2;
begin
  if not fisBackBlack then Exit;

  fCaretVisible := not fCaretVisible ;
  if not fCaretVisible then Exit;

  obj := HITextViewGetTXNObject(Widget);
  if not Assigned(obj) then Exit;

  TXNGetSelection(obj, pst, pend);
  if TXNOffsetToHIPoint(obj, pst, point) <> noErr then Exit;

  // getting current line-height, anyone knows a better way?
  CreateEvent(kCFAllocatorDefault, kEventClassTextInput, kEventTextInputOffsetToPos, 0, 0, event);
  ref := Widget;
  refcon := 0;
  SetEventParameter(event, kEventParamTextInputSendComponentInstance, typeControlRef, sizeof(Widget), ref);
  SetEventParameter(event, kEventParamTextInputSendRefCon, typeLongInteger, sizeof(refcon), @refcon);
  SetEventParameter(event, kEventParamTextInputSendTextOffset, typeLongInteger, sizeof(pst), @pst);
  SendEventToEventTarget(event, target);
  GetEventParameter(event, kEventParamTextInputReplyLineHeight, typeShortInteger, nil, sizeof(hh), nil, @hh);
  CFRelease(event);

  point.x := point.x-caretHorzOffset;
  r.origin := point;
  r.size.width:=caretHorzWidth;
  r.size.height:=hh;

  CGContextSetRGBFillColor(aContext, 1,1,1,1);
  CGContextFillRect(aContext, r);
end;

function CarbonRichEdit_Draw(ANextHandler: EventHandlerCallRef;
  AEvent: EventRef;
  AWidget: TCarbonWidget): OSStatus; {$IFDEF darwin}mwpascal;{$ENDIF}
var
  context  : CGContextRef;
begin
  Result := CallNextEventHandler(ANextHandler, AEvent);
  if Assigned(AWidget) then
  begin
    GetEventParameter(AEvent, kEventParamCGContextRef, typeCGContextRef, nil, sizeof(context), nil, @context);
    if not Assigned(context) then Exit;
    TCarbonRichEdit(AWidget).PostDraw(context);
  end;
end;

procedure TCarbonRichEdit.RegisterEvents;
var
  TmpSpec: EventTypeSpec;
begin
  inherited RegisterEvents;

  if GetEditPart >= 0 then
  begin
    TmpSpec := MakeEventSpec(kEventClassControl, kEventControlDraw);
    InstallControlEventHandler(Widget,
      RegisterEventHandler(@CarbonRichEdit_Draw),
      1, @TmpSpec, Pointer(Self), nil);
  end;

  target :=HIViewGetEventTarget(Widget);
end;

function TCarbonRichEdit.GetIndexedRunInfoFromRange(iIndex: ItemCount;
  iStartOffset, iEndOffset: TXNOffset;
  var oRunStartOffset, oRunEndOffset: TXNOffset;
  oRunDataType: TXNDataTypePtr; iTypeAttributeCount: ItemCount;
  ioTypeAttributes: TXNTypeAttributesPtr): Boolean;
begin
  Result := TXNGetIndexedRunInfoFromRange( HITextViewGetTXNObject(Widget),
    iIndex, iStartOffset, iEndOffset, @oRunStartOffset, @oRunEndOffset,
    oRunDataType, iTypeAttributeCount, ioTypeAttributes ) = noErr;
end;

function TCarbonRichEdit.GetContinuousTypeAttributes(
  var oContinuousFlags: TXNContinuousFlags; iCount: ItemCount;
  var ioTypeAttributes: array of TXNTypeAttributes): Boolean;
begin
  Result := TXNGetContinuousTypeAttributes(HITextViewGetTXNObject(Widget),
              oContinuousFlags, iCount, @ioTypeAttributes[0]) = noErr;
end;

function TCarbonRichEdit.SetTypeAttributes(iCount: ItemCount;
  const iTypeAttributes: array of TXNTypeAttributes; StartOffset,
  EndOffset: Integer): Boolean;
begin
  Result := TXNSetTypeAttributes(HITextViewGetTXNObject(Widget), iCount,
              @iTypeAttributes[0], StartOffset, EndOffset) = noErr;
end;

procedure TCarbonRichEdit.SetColor(const AColor: TColor);
begin
  fisBackBlack := AColor = clBlack;
  inherited SetColor(AColor);
end;

procedure TCarbonRichEdit.InDelText(const text: WideString; replstart, repllength: Integer); 
var
  data    : UnivPtr;
  datasz  : ByteCount;
  replend : Integer;
begin
  if text = '' then begin
    data := nil;
    datasz := 0;
  end else begin
    data := @text[1];
    datasz := length(text)*2;
  end;
  if repllength < 0 then replend  := kTXNEndOffset
  else replend := replstart+repllength;                                                                                         
  TXNSetData(HITextViewGetTXNObject(Widget), kTXNUnicodeTextData, data, datasz, replstart, replend);
end;

end.

