unit CocoaRichMemo; interface {$mode delphi} {$modeswitch objectivec1} {$modeswitch objectivec2} uses CocoaAll, Classes, SysUtils, LCLType, Graphics, Controls, StdCtrls, CocoaPrivate, CocoaUtils, CocoaWSCommon, WSRichMemo, RichMemo; type TCocoaRichView = objcclass(TCocoaTextView) public scale : Double; end; { TCocoaWSCustomRichMemo } TCocoaWSCustomRichMemo = class(TWSCustomRichMemo) public class function CreateHandle(const AWinControl: TWinControl; const AParams: TCreateParams): TLCLIntfHandle; override; class function GetTextAttributes(const AWinControl: TWinControl; TextStart: Integer; var Params: TIntFontParams): Boolean; override; class procedure SetTextAttributes(const AWinControl: TWinControl; TextStart, TextLen: Integer; const Params: TIntFontParams); override; class procedure SetParaAlignment(const AWinControl: TWinControl; TextStart, TextLen: Integer; const AAlign: TIntParaAlignment); override; class function GetParaAlignment(const AWinControl: TWinControl; TextStart: Integer; var AAlign: TIntParaAlignment): Boolean; override; class function GetParaMetric(const AWinControl: TWinControl; TextStart: Integer; var AMetric: TIntParaMetric): Boolean; override; class procedure SetParaMetric(const AWinControl: TWinControl; TextStart, TextLen: Integer; const AMetric: TIntParaMetric); override; class procedure SetParaTabs(const AWinControl: TWinControl; TextStart, TextLen: Integer; const AStopList: TTabStopList); override; class function GetParaTabs(const AWinControl: TWinControl; TextStart: integer; var AStopList: TTabStopList): Boolean; override; class procedure SetZoomFactor(const AWinControl: TWinControl; AZoomFactor: Double); override; class procedure InDelText(const AWinControl: TWinControl; const TextUTF8: String; DstStart, DstLen: Integer); override; class function LoadRichText(const AWinControl: TWinControl; Source: TStream): Boolean; override; end; implementation function MemoTextView(AWinControl: TWinControl): TCocoaTextView; begin if not Assigned(AWinControl) or (AWinControl.Handle=0) then Result := nil else Result := TCocoaTextView(NSScrollView(AWinControl.Handle).documentView); end; function ParaRange(txt: NSString; textOffset, TextLen: Integer): NSRange; overload; begin Result.location:=textOffset; if textOffset+TextLen>txt.length then Result.length:=txt.length-textOffset else Result.length:=TextLen; Result:=txt.paragraphRangeForRange(Result); end; function ParaRange(txt: NSTextStorage; textOffset, TextLen: Integer): NSRange; inline; overload; begin Result:=ParaRange(txt.string_, textOffset, textLen); end; function TextRange(txt: NSTextStorage; textOffset, TextLen: Integer): NSRange; inline; overload; begin Result.location:=textOffset; if textOffset+TextLen>txt.length then Result.length:=txt.length-textOffset else Result.length:=TextLen; end; function GetDict(txt: NSTextStorage; textOffset: integer): NSDictionary; begin if textOffset>=txt.string_.length then textOffset:=txt.string_.length-1; Result := txt.attributesAtIndex_effectiveRange(textOffset, nil); end; function GetPara(txt: NSTextStorage; textOffset: integer; isReadOnly, useDefault: Boolean): NSParagraphStyle; var dict: NSDictionary; op : NSParagraphStyle; begin Result:=nil; if not Assigned(txt) then Exit; dict:=GetDict(txt, textOffset); op:=nil; if Assigned(dict) then op:=NSParagraphStyle( dict.objectForKey(NSParagraphStyleAttributeName) ); if not Assigned(op) then begin if not useDefault then Exit; op:=NSParagraphStyle.defaultParagraphStyle; end; if isReadOnly then Result := op else Result := op.mutableCopyWithZone(nil) end; function GetWritePara(txt: NSTextStorage; textOffset: integer): NSMutableParagraphStyle; begin Result:=NSMutableParagraphStyle(GetPara(txt, textOffset, false, true)); end; function GetReadPara(txt: NSTextStorage; textOffset: integer; useDefault: Boolean = false): NSParagraphStyle; begin Result:=GetPara(txt, textOffset, true, useDefault); end; type TNSFontParams = record font : NSFont; fontColor : NSColor; backColor : NSColor; ulnStyle : NSNumber; strStyle : NSNumber; end; procedure ReadNSFontParams(dict: NSDictionary; var params: TNSFontParams); begin if not Assigned(dict) then FillChar(params, sizeof(params),0) else begin params.font:=dict.objectForKey(NSFontAttributeName); params.fontColor:=dict.objectForKey(NSForegroundColorAttributeName); params.backColor:=dict.objectForKey(NSBackgroundColorAttributeName); params.ulnStyle:=dict.objectForKey(NSUnderlineStyleAttributeName); params.strStyle:=dict.objectForKey(NSStrikethroughStyleAttributeName); end; end; { TCocoaWSCustomRichMemo } const VerticalScrollerVisible: array[TScrollStyle] of boolean = ( {ssNone } false, {ssHorizontal } false, {ssVertical } true, {ssBoth } true, {ssAutoHorizontal} false, {ssAutoVertical } true, {ssAutoBoth } true ); HorizontalScrollerVisible: array[TScrollStyle] of boolean = ( {ssNone } false, {ssHorizontal } true, {ssVertical } false, {ssBoth } true, {ssAutoHorizontal} true, {ssAutoVertical } false, {ssAutoBoth } true ); ScrollerAutoHide: array[TScrollStyle] of boolean = ( {ssNone } false, {ssHorizontal } false, {ssVertical } false, {ssBoth } false, {ssAutoHorizontal} true, {ssAutoVertical } true, {ssAutoBoth } true ); class function TCocoaWSCustomRichMemo.CreateHandle( const AWinControl: TWinControl; const AParams: TCreateParams): TLCLIntfHandle; var txt: TCocoaRichView; ns: NSString; scr: TCocoaScrollView; nr:NSRect; r:TRect; begin scr := TCocoaScrollView(NSView(TCocoaScrollView.alloc).lclInitWithCreateParams(AParams)); nr.origin.x:=0; nr.origin.x:=0; nr.size.height:=0; nr.size.width:=AParams.Width; txt := TCocoaRichView.alloc.initwithframe(nr); txt.scale := 1.0; scr.setDocumentView(txt); scr.setHasVerticalScroller(VerticalScrollerVisible[TMemo(AWinControl).ScrollBars]); scr.setHasHorizontalScroller(HorizontalScrollerVisible[TMemo(AWinControl).ScrollBars]); scr.setAutohidesScrollers(ScrollerAutoHide[TMemo(AWinControl).ScrollBars]); if TCustomMemo(AWinControl).BorderStyle=bsSingle then scr.setBorderType(NSBezelBorder); nr:=scr.documentVisibleRect; txt.setFrame(nr); txt.textContainer.setLineFragmentPadding(0); txt.callback := TLCLCommonCallback.Create(txt, AWinControl); ns := NSStringUtf8(AParams.Caption); txt.setString(ns); ns.release; scr.callback := txt.callback; Result := TLCLIntfHandle(scr); end; class function TCocoaWSCustomRichMemo.GetTextAttributes( const AWinControl: TWinControl; TextStart: Integer; var Params: TIntFontParams ): Boolean; var view : TCocoaTextView; txt : NSTextStorage; dict : NSDictionary; prm : TNSFontParams; trt : NSFontSymbolicTraits; begin InitFontParams(Params); view:=MemoTextView(AWinControl); Result:=false; if not Assigned(view) then Exit; txt:=view.textStorage; dict:=GetDict(txt, textStart); ReadNSFontParams(dict, prm); if Assigned(prm.font) then begin Params.Name:=NSStringToString(prm.font.familyName); Params.Size:=round(prm.font.pointSize); trt:=prm.font.fontDescriptor.symbolicTraits; if (trt and NSFontItalicTrait) > 0 then Include(Params.Style, fsItalic); if (trt and NSFontBoldTrait) > 0 then Include(Params.Style, fsBold); end; if Assigned(prm.ulnStyle) and (prm.ulnStyle.integerValue <> NSUnderlineStyleNone) then Include(Params.Style, fsUnderline); if Assigned(prm.strStyle) and (prm.strStyle.integerValue <> NSUnderlineStyleNone) then Include(Params.Style, fsStrikeOut); if Assigned(prm.fontColor) then Params.Color:=NSColorToColorRef(prm.fontColor); Params.HasBkClr:=Assigned(prm.backColor); if Params.HasBkClr then Params.BkColor:=NSColorToColorRef(prm.backColor); Result:=true; end; function FindFont(const FamilyName: String; astyle: TFontStyles): NSFontDescriptor; var fd : NSFontDescriptor; old : NSFontDescriptor; fdd : NSFontDescriptor; trt : NSFontSymbolicTraits; ns : NSString; i : Integer; const fallback : array [0..1] of NSFontSymbolicTraits = (NSFontItalicTrait, NSFontBoldTrait); begin trt:=0; ns:=NSStringUtf8(FamilyName); if fsItalic in aStyle then trt:=trt or NSFontItalicTrait; if fsBold in aStyle then trt:=trt or NSFontBoldTrait; fd:=NSFontDescriptor(NSFontDescriptor.alloc).initWithFontAttributes(nil); fd:=fd.fontDescriptorWithFamily(ns); fd:=fd.fontDescriptorWithSymbolicTraits(trt); fdd:=fd.matchingFontDescriptorWithMandatoryKeys(nil); i:=0; while not Assigned(fdd) and (itxt.textStorage.string_.length then rng.length:=txt.textStorage.string_.length-TextStart; rng:=txt.textStorage.string_.paragraphRangeForRange(rng); txt.setAlignment_range(TxtAlign[AAlign], rng); end; class function TCocoaWSCustomRichMemo.GetParaAlignment( const AWinControl: TWinControl; TextStart: Integer; var AAlign: TIntParaAlignment): Boolean; var txt : TCocoaTextView; rng : NSRange; cur : NSRange; al : NSTextAlignment; const TxtAlign : array [TIntParaAlignment] of integer = ( NSLeftTextAlignment, NSRightTextAlignment, NSCenterTextAlignment, NSJustifiedTextAlignment ); begin txt:=MemoTextView(AWinControl); if not Assigned(txt) then begin Result:=false; Exit; end; cur:=txt.selectedRange; rng.location:=TextStart; rng.length:=1; if TextStart+1>txt.textStorage.string_.length then rng.length:=txt.textStorage.string_.length-TextStart; rng:=txt.textStorage.string_.paragraphRangeForRange(rng); txt.setSelectedRange(rng); al:=txt.alignment; case al of NSRightTextAlignment: AAlign:=paRight; NSCenterTextAlignment: AAlign:=paCenter; NSJustifiedTextAlignment: AAlign:=paJustify; else AAlign:=paLeft; end; txt.setSelectedRange(cur); Result:=true; end; class function TCocoaWSCustomRichMemo.GetParaMetric( const AWinControl: TWinControl; TextStart: Integer; var AMetric: TIntParaMetric): Boolean; var view : TCocoaTextView; txt : NSTextStorage; par : NSParagraphStyle; begin InitParaMetric(AMetric); view:=MemoTextView(AWinControl); Result:=false; if not Assigned(view) then Exit; txt:=view.textStorage; par:=GetReadPara(txt, TextStart, true); AMetric.FirstLine := par.firstLineHeadIndent; AMetric.HeadIndent := par.headIndent; AMetric.TailIndent := par.tailIndent; AMetric.LineSpacing := par.lineSpacing; AMetric.SpaceAfter := par.paragraphSpacing; AMetric.SpaceBefore := par.paragraphSpacingBefore; Result:=true; end; class procedure TCocoaWSCustomRichMemo.SetParaMetric( const AWinControl: TWinControl; TextStart, TextLen: Integer; const AMetric: TIntParaMetric); var view : TCocoaTextView; txt : NSTextStorage; par : NSMutableParagraphStyle; begin view:=MemoTextView(AWinControl); if not Assigned(view) then Exit; txt:=view.textStorage; par:=GetWritePara(txt, TextStart); par.setFirstLineHeadIndent(AMetric.FirstLine); par.setHeadIndent(AMetric.HeadIndent); par.setTailIndent(AMetric.TailIndent); par.setLineSpacing(AMetric.LineSpacing); par.setParagraphSpacing(AMetric.SpaceAfter); par.setParagraphSpacingBefore(AMetric.SpaceBefore); txt.addAttribute_value_range( NSParagraphStyleAttributeName, par, ParaRange(txt, TextStart, textLen)); par.release; end; class procedure TCocoaWSCustomRichMemo.SetParaTabs( const AWinControl: TWinControl; TextStart, TextLen: Integer; const AStopList: TTabStopList); var view : TCocoaTextView; txt : NSTextStorage; par : NSMutableParagraphStyle; tabs : NSMutableArray; tab : NSTextTab; i : Integer; rng : NSRange; const TabAlignMap : array [TTabAlignment] of NSTextTabType = ( NSLeftTabStopType, // tabLeft, NSCenterTabStopType, // tabCenter, NSRightTabStopType, // tabRight, NSDecimalTabStopType, // tabDecimal NSLeftTabStopType // tabWordBar - not supported ); begin view:=MemoTextView(AWinControl); if not Assigned(view) then Exit; txt := view.textStorage; par:=GetWritePara(txt, TextStart); if AStopList.Count>0 then begin tabs := NSMutableArray.alloc.init; for i:=0 to AStopList.Count-1 do begin tab := NSTextTab.alloc.initWithType_location( TabAlignMap[AStopList.Tabs[i].Align], AStopList.Tabs[i].Offset ); tabs.addObject( tab ); tab.release; end; end; par.setTabStops(tabs); txt.addAttribute_value_range( NSParagraphStyleAttributeName, par, ParaRange(txt, TextStart, textLen)); par.release; end; class function TCocoaWSCustomRichMemo.GetParaTabs( const AWinControl: TWinControl; TextStart: integer; var AStopList: TTabStopList): Boolean; var view : TCocoaTextView; txt : NSTextStorage; par : NSParagraphStyle; tabs : NSArray; tab : NSTextTab; i : Integer; begin InitTabStopList(AStopList); view:=MemoTextView(AWinControl); Result:=false; if not Assigned(view) then Exit; txt:=view.textStorage; par:=GetReadPara(txt, textStart, false); if not Assigned(par) then Exit; tabs:=par.tabStops; if not Assigned(tabs) then Exit; AStopList.Count:=tabs.count; SetLength(AStopList.Tabs, AStopList.Count); for i:=0 to tabs.count-1 do begin tab:=NSTextTab(tabs.objectAtIndex(i)); AStopList.Tabs[i].Offset:=tab.location; case tab.tabStopType of NSCenterTabStopType: AStopList.Tabs[i].Align:= tabCenter; NSRightTabStopType: AStopList.Tabs[i].Align:= tabRight; NSDecimalTabStopType: AStopList.Tabs[i].Align:= tabDecimal; else AStopList.Tabs[i].Align:=taLeft; end; end; end; class procedure TCocoaWSCustomRichMemo.SetZoomFactor( const AWinControl: TWinControl; AZoomFactor: Double); var view : TCocoaRichView; sz : NSSize; begin view:=TCocoaRichView(MemoTextView(AWinControl)); if not Assigned(view) then Exit; // reset Scaling if view.scale<>1.0 then begin sz.width:=1/view.scale; sz.height:=1/view.scale; view.scaleUnitSquareToSize(sz); end; // set new scaling sz.width:=AZoomFactor; sz.height:=AZoomFactor; view.scaleUnitSquareToSize(sz); view.layoutManager.ensureLayoutForTextContainer(view.textContainer); view.scale:=AZoomFactor; end; class procedure TCocoaWSCustomRichMemo.InDelText( const AWinControl: TWinControl; const TextUTF8: String; DstStart, DstLen: Integer); var txt : TCocoaTextView; str : NSString; begin txt:=MemoTextView(AWinControl); if not Assigned(txt) then Exit; str := NSStringUtf8(TextUtf8); txt.textStorage.replaceCharactersInRange_withString(NSMakeRange(DstStart, DstLen), str); str.release; end; class function TCocoaWSCustomRichMemo.LoadRichText( const AWinControl: TWinControl; Source: TStream): Boolean; var data: NSMutableData; rng : NSRange; txt : TCocoaTextView; begin //todo: avoid copying data. Result:=false; if not Assigned(Source) or not Assigned(AWinControl) or (AWinControl.Handle=0) then Exit; txt:=MemoTextView(AWinControl); if Source.size>0 then begin data:=NSMutableData(NSMutableData.alloc).initWithLength(Source.size); Source.Read(data.mutableBytes^, Source.Size); rng.length:=txt.textStorage.string_.length; rng.location:=0; txt.replaceCharactersInRange_withRTF(rng, data); data.release; end; Result:=true; end; end.