{ $Id: $} { -------------------------------------------- cocoascrollers.pas - Cocoa internal classes -------------------------------------------- This unit contains the private classhierarchy for the Cocoa implemetations ***************************************************************************** This file is part of the Lazarus Component Library (LCL) See the file COPYING.modifiedLGPL.txt, included in this distribution, for details about the license. ***************************************************************************** } unit CocoaScrollers; {$mode objfpc}{$H+} {$modeswitch objectivec1} {$modeswitch objectivec2} {$interfaces corba} {$include cocoadefines.inc} interface uses Math, Classes, SysUtils, LclType, Controls, Forms, MacOSAll, CocoaAll, CocoaUtils, CocoaPrivate, CocoaCallback, CocoaConfig; type { TCocoaScrollView } // TCocoaScrollView is based on NSScrollView with native Scroller. // it requires the document to have the full size it claims, so it is usually // suitable for components of limited size. // it corresponds to components such as ListBox, ListView, ScrollBox, Form. TCocoaScrollView = objcclass(NSScrollView) public callback: ICommonCallback; isCustomRange: Boolean; // the corresponding LCL ScrollInfo, // which are needed when documentView.frame changes. lclHorzScrollInfo: TScrollInfo; lclVertScrollInfo: TScrollInfo; docrect : NSRect; // have to remember old holdscroll : Integer; // do not send scroll messages procedure dealloc; override; procedure setFrame(aframe: NSRect); override; function acceptsFirstResponder: LCLObjCBoolean; override; function lclGetCallback: ICommonCallback; override; procedure lclClearCallback; override; function lclClientFrame: TRect; override; function lclContentView: NSView; override; procedure setDocumentView(aView: NSView); override; procedure scrollWheel(theEvent: NSEvent); override; procedure ensureDocumentViewSizeChanged(newSize: NSSize; ensureWidth: Boolean; ensureHeight: Boolean); message 'ensureDocumentViewSizeChanged:newSize:ensureWidth:'; procedure resetScrollData; message 'resetScrollData'; procedure lclUpdate; override; procedure lclInvalidateRect(const r: TRect); override; procedure lclInvalidate; override; procedure fillScrollInfo(barFlag: Integer; var scrollInfo: TScrollInfo); message 'fillScrollInfo:barFlag:'; procedure applyScrollInfo(barFlag: Integer; const scrollInfo: TScrollInfo); message 'applyScrollInfo:barFlag:'; end; { TCocoaManualScrollView is based on NSView without native Scroller. it doesn't require the document to have the full size it claims, so it is usually suitable for components of unlimited size. it corresponds to components such as TreeView, Grid, SynEdit. the following implements modern macOS scrollbars for TCocoaManualScrollView. Legacy and Overlay style Scroller were implemented. in the architecture design, it is mainly decomposed into four structures: 1. ScrollBar: the Scroll Info Modal, not related to a specific style 2. ScrollView: the Container, not related to a specific style 3. Manager: core component, called by ScrollBar and ScrollView. currently ther are two implementations: Legacy and Overlay style. 4. Effect: animation effects, called by the Manager. currently there are Alpha and Overlay Style. it would be more appropriate to use Interface to define the interface of each component, but FreePascal's Interface is so weak that it can only be replaced by abstract classes here. } { TCocoaScrollBarEffect } // there are many animation effects, which are concentrated in TCocoaScrollBarEffect. // currently there are Alpha and Overlay (DelayHidding/Expand) implementations. TCocoaScrollBarEffect = objcclass(NSObject) protected procedure onDestroy; message 'onDestroy'; end; { TCocoaScrollBarStyleManager } // manager for Scroller, with Legacy and Overlay style implementations TCocoaScrollBarStyleManager = class // called by TCocoaScrollBar when the value changes. procedure onKnobValueUpdated( scroller:NSScroller; var knobPosition:Double; var knobProportion:Double ); virtual; abstract; // draw the Knob, called by TCocoaScrollBar procedure onDrawKnob( scroller:NSScroller ); virtual; abstract; // draw the KnobSlot, called by TCocoaScrollBar function onDrawKnobSlot( scroller:NSScroller; var slotRect: NSRect ): Boolean; virtual; abstract; // called by TCocoaScrollBar after the mouse entered procedure onMouseEntered( scroller:NSScroller ); virtual; abstract; // called by TCocoaScrollBar after the mouse exited procedure onMouseExited( scroller:NSScroller ); virtual; abstract; // Create the corresponding Effect function createScrollBarEffect( scroller:NSScroller ): TCocoaScrollBarEffect; virtual; abstract; // Make isAvailableScrollBar return Ture(Available)/False(not Available) if necessary procedure availScrollBar( scroller:NSScroller; available:Boolean ); virtual; abstract; // Returns the Availability of the Scroller // with Legacy Style, Availability means 'Shown/not Hidden' // with Overlay Style, Availability means 'KnobProportion<1' function isAvailableScrollBar( scroller:NSScroller ): Boolean; virtual; abstract; // Show the Scroller if it can be made Available procedure showScrollBar( scroller:NSScroller; now:Boolean=True ); virtual; abstract; // Hide the Scroller but keep the Availability, only for Overlay Style procedure tempHideScrollBar( scroller:NSScroller ); virtual; abstract; end; TCocoaManualScrollView = objcclass; { TCocoaScrollStyleManager } // manager for ScrollView, with Legacy and Overlay style implementations. // TCocoaScrollStyleManager and TCocoaScrollBarStyleManager should be two // independent interfaces, but due to the limitations of FreePascal mentioned // earlier, only abstract classes can be used here, so they can only become // an inheritance relationship. // the existence of createForScrollBar() and createForScrollView() // is a compromise in this situation. TCocoaScrollStyleManager = class(TCocoaScrollBarStyleManager) private _scrollView: TCocoaManualScrollView; protected function isBarOccupyBound: Boolean; virtual; abstract; public // place the document, horizontal scroller, and vertical scroller // in the appropriate positions procedure updateLayout; virtual; public constructor createForScrollBar; constructor createForScrollView( scrollView:TCocoaManualScrollView ); end; { TCocoaScrollBar } TCocoaScrollBar = objcclass(NSScroller) private _scrollView: TCocoaManualScrollView; _manager: TCocoaScrollBarStyleManager; _effect: TCocoaScrollBarEffect; _trackingArea: NSTrackingArea; private procedure releaseManager; message 'releaseManager'; public callback: ICommonCallback; preventBlock : Boolean; // minInt,maxInt are used to calculate position for lclPos and lclSetPos minInt : Integer; maxInt : Integer; pageInt : Integer; suppressLCLMouse: Boolean; largeInc: Integer; smallInc: Integer; procedure dealloc; override; function manager: TCocoaScrollBarStyleManager; message 'manager'; function effect: TCocoaScrollBarEffect; message 'effect'; procedure setManager( newManager:TCocoaScrollBarStyleManager ); message 'setManager:'; // Note: // Do not override isCompatibleWithOverlayScrollers(), which will cause // the Scroller to enable NSView.Layer, leading to various problems. // class function isCompatibleWithOverlayScrollers: ObjCBOOL; override; procedure drawKnob; override; procedure drawKnobSlotInRect_highlight(slotRect: NSRect; flag: ObjCBOOL); override; procedure setDoubleValue(newValue: double); override; procedure setKnobProportion(newValue: CGFloat); override; procedure updateTrackingAreas; override; procedure mouseEntered(theEvent: NSEvent); override; procedure mouseExited(theEvent: NSEvent); override; procedure actionScrolling(sender: NSObject); message 'actionScrolling:'; function IsHorizontal: Boolean; message 'IsHorizontal'; function acceptsFirstResponder: LCLObjCBoolean; override; function lclGetCallback: ICommonCallback; override; procedure lclClearCallback; override; function lclPos: Integer; message 'lclPos'; procedure lclSetPos(aPos: integer); message 'lclSetPos:'; // mouse function acceptsFirstMouse(event: NSEvent): LCLObjCBoolean; override; procedure mouseDown(event: NSEvent); override; procedure mouseUp(event: NSEvent); override; procedure rightMouseDown(event: NSEvent); override; procedure rightMouseUp(event: NSEvent); override; procedure rightMouseDragged(event: NSEvent); override; procedure otherMouseDown(event: NSEvent); override; procedure otherMouseUp(event: NSEvent); override; procedure otherMouseDragged(event: NSEvent); override; procedure mouseDragged(event: NSEvent); override; procedure mouseMoved(event: NSEvent); override; procedure scrollWheel(event: NSEvent); override; end; { TCocoaManualScrollView } TCocoaManualScrollView = objcclass(NSView) private _manager: TCocoaScrollStyleManager; _tapping: Integer; // two finger tapping, SB_VERT / SB_HORZ / SB_BOTH _documentView: NSView; _horzScrollBar : TCocoaScrollBar; _vertScrollBar : TCocoaScrollBar; public callback: ICommonCallback; function initWithFrame(frameRect: NSRect): id; override; procedure dealloc; override; procedure setFrame(newValue: NSRect); override; procedure resetManager; message 'resetManager'; procedure onScrollerStyleUpdated; message 'onScrollerStyleUpdated'; function lclGetCallback: ICommonCallback; override; procedure lclClearCallback; override; function lclContentView: NSView; override; function lclClientFrame: TRect; override; function lclIsMouseInAuxArea(event: NSEvent): Boolean; override; procedure lclUpdate; override; procedure lclInvalidateRect(const r: TRect); override; procedure lclInvalidate; override; procedure setDocumentView(AView: NSView); message 'setDocumentView:'; function documentView: NSView; message 'documentView'; procedure delayShowScrollBars; message 'delayShowScrollBars'; procedure showScrollBarsAndAutoHide( tapping:Integer ); message 'showScrollBarsAndAutoHide:'; function isTapping( scroller:NSScroller ): Boolean; message 'isTapping:'; procedure onBarScrolled( scroller:NSScroller ); message 'onBarScrolled:'; procedure touchesBeganWithEvent(event: NSEvent); override; procedure touchesEndedWithEvent(event: NSEvent); override; procedure touchesCancelledWithEvent(event: NSEvent); override; procedure setHasVerticalScroller(doshow: Boolean); message 'setHasVerticalScroller:'; procedure setHasHorizontalScroller(doshow: Boolean); message 'setHasHorizontalScroller:'; function hasVerticalScroller: Boolean; message 'hasVerticalScroller'; function hasHorizontalScroller: Boolean; message 'hasHorizontalScroller'; function horizontalScroller: NSScroller; message 'horizontalScroller'; function verticalScroller: NSScroller; message 'verticalScroller'; function allocHorizontalScroller(avisible: Boolean): TCocoaScrollBar; message 'allocHorizontalScroller:'; function allocVerticalScroller(avisible: Boolean): TCocoaScrollBar; message 'allocVerticalScroller:'; // mouse function acceptsFirstMouse(event: NSEvent): LCLObjCBoolean; override; end; { TCocoaManualScrollHost } TCocoaManualScrollHost = objcclass(TCocoaScrollView) function lclContentView: NSView; override; function lclClientFrame: TRect; override; procedure scrollWheel(theEvent: NSEvent); override; procedure setFrame(newValue: NSRect); override; procedure setScrollerStyle(newValue: NSScrollerStyle); override; end; function createLegacyScroller: TCocoaScrollBar; function isMouseEventInScrollBar(host: TCocoaManualScrollView; event: NSEvent): Boolean; // These settings are set by a user in "System Preferences" // One can check the values by running the command line: // $defaults read // "AppleShowScrollBars": Automatic, Always, WhenScrolling function SysPrefScrollShow: string; // "AppleScrollerPagingBehavior": 0 - adjust by page, 1 - jump to the position function SysPrefScrollClick: Integer; procedure HandleMouseDown(sc: TCocoaScrollBar; locInWin: NSPoint; prt: NSScrollerPart); function AdjustScrollerPage(sc: TCocoaScrollBar; prt: NSScrollerPart): Boolean; implementation type { TCocoaScrollBarEffectAlpha } TCocoaScrollBarEffectAlpha = objcclass(TCocoaScrollBarEffect) private procedure onAlphaTimer( timer:NSTimer ); message 'onAlphaTimer:'; protected currentAlpha: Double; stepAlpha: Double; maxAlpha: Double; manager: TCocoaScrollBarStyleManager; scroller: NSScroller; alphaTimer: NSTimer; protected procedure fade( inc:Double; max:Double ); message 'fadeInc:max:'; procedure onDestroy; override; end; { TCocoaScrollBarEffectOverlay } TCocoaScrollBarEffectOverlay = objcclass(TCocoaScrollBarEffectAlpha) private procedure onDelayShowingTimer( timer:NSTimer ); message 'onDelayShowingTimer:'; procedure onDelayHidingTimer( timer:NSTimer ); message 'onDelayHidingTimer:'; procedure onExpandTimer( timer:NSTimer ); message 'onExpandTimer:'; procedure setDelayTimer( timeInterval:Double; onTimer:SEL ); message 'setDelayTimer:timeInterval:'; protected currentKnobPosition: Double; currentKnobProportion: Double; entered: Boolean; expandedSize: Integer; delayTimer: NSTimer; expandTimer: NSTimer; protected procedure setDelayShowingTimer; message 'setDelayShowingTimer'; procedure setDelayHidingTimer; message 'setDelayHidingTimer'; procedure setExpandTimer; message 'setExpandTimer'; procedure onDestroy; override; public procedure cancelDelay; message 'cancelDelay'; end; { TCocoaScrollStyleManagerLegacy } TCocoaScrollStyleManagerLegacy = class(TCocoaScrollStyleManager) protected function isBarOccupyBound: Boolean; override; public procedure onKnobValueUpdated( scroller:NSScroller; var knobPosition:Double; var knobProportion:Double ); override; procedure onDrawKnob( scroller:NSScroller ); override; function onDrawKnobSlot( scroller:NSScroller; var slotRect: NSRect ): Boolean; override; procedure onMouseEntered( scroller:NSScroller ); override; procedure onMouseExited( scroller:NSScroller ); override; function createScrollBarEffect( scroller:NSScroller ): TCocoaScrollBarEffect; override; procedure availScrollBar( scroller:NSScroller; available:Boolean ); override; function isAvailableScrollBar( scroller:NSScroller ): Boolean; override; procedure showScrollBar( scroller:NSScroller; now:Boolean=True ); override; procedure tempHideScrollBar( scroller:NSScroller ); override; end; { TCocoaScrollStyleManagerOverlay } TCocoaScrollStyleManagerOverlay = class(TCocoaScrollStyleManager) private procedure doShowScrollBar( scroller:NSScroller; now:Boolean=True ); protected function isBarOccupyBound: Boolean; override; public procedure onKnobValueUpdated( scroller:NSScroller; var knobPosition:Double; var knobProportion:Double ); override; procedure onDrawKnob( scroller:NSScroller ); override; function onDrawKnobSlot( scroller:NSScroller; var slotRect: NSRect ): Boolean; override; procedure onMouseEntered( scroller:NSScroller ); override; procedure onMouseExited( scroller:NSScroller ); override; function createScrollBarEffect( scroller:NSScroller ): TCocoaScrollBarEffect; override; procedure availScrollBar( scroller:NSScroller; available:Boolean ); override; function isAvailableScrollBar( scroller:NSScroller ): Boolean; override; procedure showScrollBar( scroller:NSScroller; now:Boolean=True ); override; procedure tempHideScrollBar( scroller:NSScroller ); override; end; function SysPrefScrollShow: string; begin Result := NSStringToString(NSUserDefaults.standardUserDefaults.stringForKey(NSSTR('AppleShowScrollBars'))); end; function SysPrefScrollClick: Integer; // 0 - adjust by page, 1 - jump to the position begin Result := Integer(NSUserDefaults.standardUserDefaults.integerForKey(NSSTR('AppleScrollerPagingBehavior'))); end; function isPagePart(prt: NSScrollerPart): Boolean; inline; begin Result := (prt = NSScrollerDecrementPage) or (prt = NSScrollerIncrementPage); end; function AdjustScrollerPage(sc: TCocoaScrollBar; prt: NSScrollerPart): Boolean; var adj : integer; sz : Integer; dlt : double; v : double; begin Result := false; case prt of NSScrollerDecrementPage: begin adj := -sc.largeInc; if adj = 0 then adj := -sc.pageInt; end; NSScrollerIncrementPage: begin adj := sc.largeInc; if adj = 0 then adj := sc.pageInt; end; NSScrollerDecrementLine: begin adj := -sc.smallInc; if adj = 0 then adj := -1; end; NSScrollerIncrementLine: begin adj := sc.smallInc; if adj = 0 then adj := 1; end; else adj := 0; end; if adj = 0 then Exit; sz := sc.maxInt - sc.minInt - sc.pageInt; if sz = 0 then Exit; // do nothing! dlt := adj / sz; v := sc.doubleValue; v := v + dlt; if v < 0 then v := 0 else if v > 1 then v := 1; // todo: call scroll event? sc.setDoubleValue(v); {$ifdef BOOLFIX} sc.setNeedsDisplay__(Ord(true)); {$else} sc.setNeedsDisplay_(true); {$endif} end; procedure HandleMouseDown(sc: TCocoaScrollBar; locInWin: NSPoint; prt: NSScrollerPart); var adj : Integer; sz : Integer; pt : NSPoint; ps : double; newPos: Integer; begin adj := SysPrefScrollClick; if adj = 0 then begin // single page jump AdjustScrollerPage(sc, prt); end else begin // direct jump pt := sc.convertPoint_fromView(locInWin, nil); if sc.IsHorizontal then begin if sc.frame.size.width = 0 then Exit; // do nothing ps := pt.x / sc.frame.size.width; end else begin if sc.frame.size.height = 0 then Exit; // do nothing ps := pt.y / sc.frame.size.height; end; sz := (sc.maxInt - sc.minInt - sc.pageInt); newPos := Round(sc.minInt + sz * ps); sc.lclSetPos(NewPos); end; end; // for the Scroller created separately through TCocoaWSScrollBar.CreateHandle(), // due to the lack of the control over the Layout by TCocoaManualScrollView, // only the Legacy Style can be used for compatibility. // it's the same logical relationship as NSScrollView and NSScroller. function createLegacyScroller: TCocoaScrollBar; var scrollBar: TCocoaScrollBar Absolute Result; manager: TCocoaScrollStyleManager; begin scrollBar:= TCocoaScrollBar(TCocoaScrollBar.alloc); manager:= TCocoaScrollStyleManagerLegacy.createForScrollBar; scrollBar.setManager( manager ); end; function allocScroller(parent: TCocoaManualScrollView; dst: NSRect; aVisible: Boolean) :TCocoaScrollBar; var scrollBar: TCocoaScrollBar Absolute Result; begin scrollBar:= TCocoaScrollBar(TCocoaScrollBar.alloc).initWithFrame(dst); scrollBar.setManager( parent._manager ); parent.addSubview(scrollBar); scrollBar.preventBlock := true; //Suppress scrollers notifications. scrollBar.callback := parent.callback; scrollBar.suppressLCLMouse := true; scrollBar.setTarget(scrollBar); scrollBar.setAction(objcselector('actionScrolling:')); end; { TCocoaManualScrollHost } function TCocoaManualScrollHost.lclContentView: NSView; begin if Assigned(documentView) then Result := documentView.lclContentView else Result := inherited lclContentView; end; function TCocoaManualScrollHost.lclClientFrame: TRect; begin if Assigned(documentView) then begin Result:=documentView.lclClientFrame; end else Result:=inherited lclClientFrame; end; procedure TCocoaManualScrollHost.scrollWheel(theEvent: NSEvent); var nr : NSResponder; begin nr := nextResponder; // do not call inherited scrollWheel, it suppresses the scroll event if Assigned(nr) then nr.scrollWheel(theEvent) else inherited scrollWheel(theEvent); end; procedure TCocoaManualScrollHost.setFrame(newValue: NSRect); var sc: TCocoaManualScrollView; scFrame: NSRect; begin inherited setFrame(newValue); sc:= TCocoaManualScrollView(self.documentView); scFrame.origin:= NSZeroPoint; scFrame.size:= self.contentSize; sc.setFrame( scFrame ); end; procedure TCocoaManualScrollHost.setScrollerStyle(newValue: NSScrollerStyle); begin inherited setScrollerStyle(newValue); if Assigned(self.lclGetTarget) and Assigned(self.documentView) then TCocoaManualScrollView(self.documentView).onScrollerStyleUpdated; end; { TCocoaManualScrollView } function TCocoaManualScrollView.initWithFrame(frameRect: NSRect): id; begin Result:= inherited; _tapping:= -1; resetManager; end; procedure TCocoaManualScrollView.dealloc; begin if Assigned(_horzScrollBar) then begin _horzScrollBar.removeFromSuperview; _horzScrollBar.setManager( nil ); _horzScrollBar.release; end; if Assigned(_vertScrollBar) then begin _vertScrollBar.removeFromSuperview; _vertScrollBar.setManager( nil ); _vertScrollBar.release; end; FreeAndNil( _manager ); end; procedure TCocoaManualScrollView.setFrame(newValue: NSRect); begin inherited setFrame(newValue); _manager.updateLayout; end; procedure TCocoaManualScrollView.resetManager; var oldManager: TCocoaScrollStyleManager; style: NSScrollerStyle; begin oldManager:= _manager; style:= CocoaConfigScroller.preferredStyle; if style < 0 then style:= NSScroller.preferredScrollerStyle; if style = NSScrollerStyleLegacy then _manager:= TCocoaScrollStyleManagerLegacy.createForScrollView(self) else _manager:= TCocoaScrollStyleManagerOverlay.createForScrollView(self); if Assigned(_horzScrollBar) then begin _horzScrollBar.setManager( _manager ); end; if Assigned(_vertScrollBar) then begin _vertScrollBar.setManager( _manager ); end; oldManager.Free; end; procedure TCocoaManualScrollView.onScrollerStyleUpdated; var horzAvailable: Boolean; vertAvailabl: Boolean; begin horzAvailable:= _manager.isAvailableScrollBar( _horzScrollBar ); vertAvailabl:= _manager.isAvailableScrollBar( _vertScrollBar ); self.resetManager; _manager.availScrollBar( _horzScrollBar, horzAvailable ); _manager.availScrollBar( _vertScrollBar, vertAvailabl ); if self.lclGetTarget is TWinControl then TWinControl(self.lclGetTarget).AdjustSize; _manager.updateLayout; _manager.showScrollBar(_horzScrollBar); _manager.showScrollBar(_vertScrollBar); end; function TCocoaManualScrollView.lclGetCallback: ICommonCallback; begin Result := callback; end; procedure TCocoaManualScrollView.lclClearCallback; begin callback := nil; end; function TCocoaManualScrollView.lclContentView: NSView; begin Result:=_documentView; end; function TCocoaManualScrollView.lclClientFrame: TRect; begin if Assigned(_documentView) then begin Result:=_documentView.lclClientFrame; end else Result:=inherited lclClientFrame; end; function TCocoaManualScrollView.lclIsMouseInAuxArea(event: NSEvent): Boolean; begin Result := isMouseEventInScrollBar(Self, event); end; procedure TCocoaManualScrollView.lclUpdate; begin documentView.lclUpdate; end; procedure TCocoaManualScrollView.lclInvalidateRect(const r: TRect); begin documentView.lclInvalidateRect(r); end; procedure TCocoaManualScrollView.lclInvalidate; begin documentView.lclInvalidate; end; procedure TCocoaManualScrollView.setDocumentView(AView: NSView); var f : NSrect; begin if _documentView=AView then Exit; if Assigned(_documentView) then _documentView.removeFromSuperview; _documentView:=AView; if Assigned(_documentView) then begin addSubview(_documentView); f:=_documentView.frame; f.origin.x:=0; f.origin.y:=0; _documentView.setFrame(f); _documentView.setAutoresizingMask(NSViewWidthSizable or NSViewHeightSizable); end; end; function TCocoaManualScrollView.documentView: NSView; begin Result:=_documentView; end; function TCocoaManualScrollView.isTapping(scroller: NSScroller): Boolean; begin if _tapping < 0 then Exit( False ); if _tapping = SB_BOTH then Exit( True ); if (_tapping = SB_HORZ) and (scroller=_horzScrollBar) then Exit( True ); if (_tapping = SB_VERT) and (scroller=_vertScrollBar) then Exit( True ); Result:= False; end; procedure TCocoaManualScrollView.onBarScrolled(scroller: NSScroller); begin if _tapping < 0 then Exit; if scroller=_vertScrollBar then begin _tapping:= SB_VERT; _manager.tempHideScrollBar( _horzScrollBar ); end else if scroller=_horzScrollBar then begin _tapping:= SB_HORZ; _manager.tempHideScrollBar( _vertScrollBar ); end; end; procedure TCocoaManualScrollView.delayShowScrollBars(); begin _manager.showScrollBar( _horzScrollBar, False ); _manager.showScrollBar( _vertScrollBar, False ); end; procedure TCocoaManualScrollView.showScrollBarsAndAutoHide( tapping:Integer ); begin if (tapping=SB_HORZ) or (tapping=SB_BOTH) then _manager.showScrollBar(_horzScrollBar); if (tapping=SB_VERT) or (tapping=SB_BOTH) then _manager.showScrollBar(_vertScrollBar); end; procedure TCocoaManualScrollView.touchesBeganWithEvent(event: NSEvent); var touches: NSSet; show: Boolean = False; begin inherited; touches:= event.touchesMatchingPhase_inView(NSTouchPhaseTouching, self); if touches.count = 2 then begin show:= True; _tapping:= SB_BOTH; end else begin if _tapping >= 0 then show:= True; _tapping:= -1; end; if show then delayShowScrollBars; end; procedure TCocoaManualScrollView.touchesEndedWithEvent(event: NSEvent); var touches: NSSet; lastTapping: Integer; show: Boolean = False; begin inherited; touches:= event.touchesMatchingPhase_inView(NSTouchPhaseTouching, self); if (touches.count=0) and (_tapping=SB_BOTH) then begin _tapping:= -1; show:= True; end else if (touches.count=2) and (_tapping<>SB_BOTH) then begin _tapping:= SB_BOTH; show:= True; end else if (touches.count<>2) and (_tapping>=0) then begin lastTapping:= _tapping; _tapping:= -1; showScrollBarsAndAutoHide( lastTapping ); end else begin _tapping:= -1; end; if show then delayShowScrollBars; end; procedure TCocoaManualScrollView.touchesCancelledWithEvent(event: NSEvent); var lastTapping: Integer; begin inherited; lastTapping:= _tapping; _tapping:= -1; showScrollBarsAndAutoHide( lastTapping ); end; procedure TCocoaManualScrollView.setHasVerticalScroller(doshow: Boolean); var available: Boolean; begin available:= _manager.isAvailableScrollBar(_vertScrollBar); if NOT Assigned(_vertScrollBar) and doshow then _vertScrollBar:= self.allocVerticalScroller( True ); _manager.availScrollBar( _vertScrollBar, doshow ); if available <> _manager.isAvailableScrollBar(_vertScrollBar) then _manager.updateLayout; end; procedure TCocoaManualScrollView.setHasHorizontalScroller(doshow: Boolean); var available: Boolean; begin available:= _manager.isAvailableScrollBar(_horzScrollBar); if NOT Assigned(_horzScrollBar) and doshow then _horzScrollBar:= self.allocHorizontalScroller( True ); _manager.availScrollBar( _horzScrollBar, doshow ); if available <> _manager.isAvailableScrollBar(_horzScrollBar) then _manager.updateLayout; end; function TCocoaManualScrollView.hasVerticalScroller: Boolean; begin Result:= _manager.isAvailableScrollBar(_vertScrollBar); end; function TCocoaManualScrollView.hasHorizontalScroller: Boolean; begin Result:= _manager.isAvailableScrollBar(_horzScrollBar); end; function TCocoaManualScrollView.horizontalScroller: NSScroller; begin Result:=_horzScrollBar; end; function TCocoaManualScrollView.verticalScroller: NSScroller; begin Result:=_vertScrollBar; end; function TCocoaManualScrollView.allocHorizontalScroller(avisible: Boolean): TCocoaScrollBar; var r : NSRect; f : NSRect; w : CGFloat; begin if Assigned(_horzScrollBar) then Result := _horzScrollBar else begin f := frame; w := NSScroller.scrollerWidthForControlSize_scrollerStyle( _horzScrollBar.controlSize, _horzScrollBar.preferredScrollerStyle); r := NSMakeRect(0, 0, Max(f.size.width,w+1), w); // width0) and (lclBar.Range>lclBar.Page) then newDocSize.Width:= lclBar.Range; lclBar:= lclControl.VertScrollBar; if lclBar.Visible and (lclBar.Range<>0) and (lclBar.Range>lclBar.Page) then newDocSize.Height:= lclBar.Range; documentView.setFrameSize(newDocSize); if lclHorzScrollInfo.fMask<>0 then applyScrollInfo(SB_Horz, lclHorzScrollInfo); if lclVertScrollInfo.fMask<>0 then applyScrollInfo(SB_Vert, lclVertScrollInfo); end; function TCocoaScrollView.acceptsFirstResponder: LCLObjCBoolean; begin Result := false; end; function TCocoaScrollView.lclGetCallback: ICommonCallback; begin Result := callback; end; procedure TCocoaScrollView.lclClearCallback; begin callback := nil; end; { TCocoaScrollBar } procedure TCocoaScrollBar.releaseManager; begin if NOT Assigned(_manager) then Exit; if NOT Assigned(_scrollView) then FreeAndNil(_manager); if Assigned(_effect) then begin _effect.onDestroy; _effect.release; _effect:= nil; end; end; procedure TCocoaScrollBar.dealloc; begin releaseManager; inherited dealloc; end; function TCocoaScrollBar.manager: TCocoaScrollBarStyleManager; begin Result:= _manager; end; function TCocoaScrollBar.effect: TCocoaScrollBarEffect; begin Result:= _effect; end; procedure TCocoaScrollBar.setManager( newManager: TCocoaScrollBarStyleManager); begin releaseManager; _scrollView:= nil; _manager:= newManager; if Assigned(_manager) then begin if _manager is TCocoaScrollStyleManager then _scrollView:= TCocoaScrollStyleManager(_manager)._scrollView; _effect:= _manager.createScrollBarEffect(self); end; end; procedure TCocoaScrollBar.drawKnob; begin _manager.onDrawKnob(self); end; procedure TCocoaScrollBar.drawKnobSlotInRect_highlight(slotRect: NSRect; flag: ObjCBOOL); var drawSlot: Boolean; begin drawSlot:= _manager.onDrawKnobSlot(self, slotRect); if drawSlot then Inherited; end; procedure TCocoaScrollBar.setDoubleValue(newValue: double); var proportion: CGFloat; begin if newValue < 0 then newValue:= 0; if newValue > 1 then newValue:= 1; proportion:= self.knobProportion; _manager.onKnobValueUpdated( self, newValue, proportion ); inherited; end; procedure TCocoaScrollBar.setKnobProportion(newValue: CGFloat); var position: CGFloat; begin if newValue < 0 then newValue:= 0; if newValue > 1 then newValue:= 1; position:= self.doubleValue; _manager.onKnobValueUpdated( self, position, newValue ); inherited; end; procedure TCocoaScrollBar.updateTrackingAreas; var i: NSTrackingArea; const options: NSTrackingAreaOptions = NSTrackingMouseEnteredAndExited or NSTrackingActiveAlways; begin if Assigned(_trackingArea) then begin removeTrackingArea(_trackingArea); _trackingArea:= nil; end; _trackingArea:= NSTrackingArea.alloc.initWithRect_options_owner_userInfo( self.bounds, options, self, nil); self.addTrackingArea( _trackingArea ); _trackingArea.release; end; procedure TCocoaScrollBar.mouseEntered(theEvent: NSEvent); begin _manager.onMouseEntered(self); end; procedure TCocoaScrollBar.mouseExited(theEvent: NSEvent); begin _manager.onMouseExited( self ); end; procedure TCocoaScrollBar.actionScrolling(sender: NSObject); var event : NSEvent; prt : NSScrollerPart; locInWin : NSPoint; begin event := NSApplication.sharedApplication.currentEvent; if not Assigned(event) then Exit; if not Assigned(event.window) then begin locInWin := event.mouseLocation; if Assigned(window) then locInWin := window.convertScreenToBase(locInWin); end else locInWin := event.locationInWindow; prt := testPart(locInWin); if isPagePart(prt) then HandleMouseDown(self, locInWin, prt); if Assigned(callback) then callback.scroll(not IsHorizontal(), lclPos, prt); end; function TCocoaScrollBar.IsHorizontal: Boolean; begin Result := frame.size.width > frame.size.height; end; function TCocoaScrollBar.lclPos: Integer; begin Result:=round( doubleValue * (maxint-minInt-pageInt)) + minInt; end; procedure TCocoaScrollBar.lclSetPos(aPos: integer); var d : integer; begin d := maxInt - minInt - pageInt; if d <= 0 then setDoubleValue(0) else begin if aPos < minInt then aPos:=minInt else if aPos > maxInt then aPos:=maxInt; setDoubleValue( (aPos - minInt) / d ); end; end; function TCocoaScrollBar.acceptsFirstMouse(event: NSEvent): LCLObjCBoolean; begin Result:=true; end; procedure TCocoaScrollBar.mouseDown(event: NSEvent); begin if suppressLCLMouse then begin inherited mouseDown(event); end else if not Assigned(callback) or not callback.MouseUpDownEvent(event, false, preventBlock) then begin inherited mouseDown(event); if Assigned(callback) then callback.MouseUpDownEvent(event, true, preventBlock); end; end; procedure TCocoaScrollBar.mouseUp(event: NSEvent); begin if suppressLCLMouse then begin inherited mouseDown(event) end else if not Assigned(callback) or not callback.MouseUpDownEvent(event, false, preventBlock) then inherited mouseUp(event); end; procedure TCocoaScrollBar.rightMouseDown(event: NSEvent); begin if suppressLCLMouse then inherited rightMouseDown(event) else if not Assigned(callback) or not callback.MouseUpDownEvent(event) then inherited rightMouseDown(event); end; procedure TCocoaScrollBar.rightMouseUp(event: NSEvent); begin if suppressLCLMouse then inherited rightMouseUp(event) else if not Assigned(callback) or not callback.MouseUpDownEvent(event) then inherited rightMouseUp(event); end; procedure TCocoaScrollBar.rightMouseDragged(event: NSEvent); begin if suppressLCLMouse then inherited rightMouseDragged(event) else if not Assigned(callback) or not callback.MouseUpDownEvent(event) then inherited rightMouseDragged(event); end; procedure TCocoaScrollBar.otherMouseDown(event: NSEvent); begin if suppressLCLMouse then inherited otherMouseDown(event) else if not Assigned(callback) or not callback.MouseUpDownEvent(event) then inherited otherMouseDown(event); end; procedure TCocoaScrollBar.otherMouseUp(event: NSEvent); begin if suppressLCLMouse then inherited otherMouseUp(event) else if not Assigned(callback) or not callback.MouseUpDownEvent(event) then inherited otherMouseUp(event); end; procedure TCocoaScrollBar.otherMouseDragged(event: NSEvent); begin if suppressLCLMouse then inherited otherMouseDragged(event) else if not Assigned(callback) or not callback.MouseMove(event) then inherited otherMouseDragged(event); end; procedure TCocoaScrollBar.mouseDragged(event: NSEvent); begin if suppressLCLMouse then inherited mouseDragged(event) else if not Assigned(callback) or not callback.MouseMove(event) then inherited mouseDragged(event); end; procedure TCocoaScrollBar.mouseMoved(event: NSEvent); begin if suppressLCLMouse then inherited mouseMoved(event) else if (not Assigned(callback) or not callback.MouseMove(event)) then inherited mouseMoved(event) end; procedure TCocoaScrollBar.scrollWheel(event: NSEvent); begin if Assigned(callback) then callback.scrollWheel(event) else Inherited; end; function TCocoaScrollBar.acceptsFirstResponder: LCLObjCBoolean; begin Result := True; end; function TCocoaScrollBar.lclGetCallback: ICommonCallback; begin Result := callback; end; procedure TCocoaScrollBar.lclClearCallback; begin callback := nil; end; { TCocoaScrollBarEffect } procedure TCocoaScrollBarEffect.onDestroy; begin end; { TCocoaScrollBarEffectAlpha } procedure TCocoaScrollBarEffectAlpha.onAlphaTimer(timer: NSTimer); var done: Boolean = false; begin if timer<>alphaTimer then begin timer.invalidate; Exit; end; self.currentAlpha:= self.currentAlpha + self.stepAlpha; if self.currentAlpha < 0.01 then self.currentAlpha:= 0; if self.stepAlpha > 0 then begin if self.currentAlpha >= self.maxAlpha then begin self.currentAlpha:= self.maxAlpha; done:= True; end; end else begin if self.currentAlpha <= self.maxAlpha then begin self.currentAlpha:= self.maxAlpha; done:= True; end; end; if done then begin timer.invalidate; alphaTimer:= nil; end; self.scroller.setNeedsDisplay_(True); end; procedure TCocoaScrollBarEffectAlpha.fade( inc: Double; max: Double ); begin if Assigned(self.alphaTimer) then self.alphaTimer.invalidate; self.stepAlpha:= inc; self.maxAlpha:= max; self.alphaTimer:= NSTimer.scheduledTimerWithTimeInterval_target_selector_userInfo_repeats( CocoaConfigScroller.fadeTimeInterval, self, ObjCSelector('onAlphaTimer:'), nil, true ); end; procedure TCocoaScrollBarEffectAlpha.onDestroy; begin if Assigned(self.alphaTimer) then begin self.alphaTimer.invalidate; self.alphaTimer:= nil; end; end; { TCocoaScrollBarEffectOverlay } procedure TCocoaScrollBarEffectOverlay.onDelayShowingTimer( timer: NSTimer ); begin self.delayTimer:= nil; self.manager.showScrollBar( self.scroller ); end; procedure TCocoaScrollBarEffectOverlay.onDelayHidingTimer( timer:NSTimer ); begin self.delayTimer:= nil; self.fade( CocoaConfigScroller.overlay.bar.alphaFadeStep, CocoaConfigScroller.overlay.bar.alphaFadeTo ); end; procedure TCocoaScrollBarEffectOverlay.onExpandTimer(timer: NSTimer); var done: Boolean = false; begin if timer<>expandTimer then begin timer.invalidate; Exit; end; if self.expandedSize < CocoaConfigScroller.overlay.bar.expandSize then begin self.expandedSize:= self.expandedSize + 1; end else begin done:= True; end; if done then begin timer.invalidate; self.expandTimer:= nil; end; self.scroller.setNeedsDisplay_(True); end; procedure TCocoaScrollBarEffectOverlay.setDelayTimer( timeInterval:Double; onTimer:SEL ); begin if Assigned(self.expandTimer) then begin self.expandTimer.invalidate; self.expandTimer:= nil; end; self.cancelDelay; self.delayTimer:= NSTimer.scheduledTimerWithTimeInterval_target_selector_userInfo_repeats( timeInterval, self, onTimer, nil, false ); end; procedure TCocoaScrollBarEffectOverlay.setDelayShowingTimer; begin self.setDelayTimer( CocoaConfigScroller.overlay.bar.autoShowDelayTime, ObjCSelector('onDelayShowingTimer:') ); end; procedure TCocoaScrollBarEffectOverlay.setDelayHidingTimer; begin self.setDelayTimer( CocoaConfigScroller.overlay.bar.autoHideDelayTime, ObjCSelector('onDelayHidingTimer:') ); end; procedure TCocoaScrollBarEffectOverlay.setExpandTimer; begin self.cancelDelay; if Assigned(self.expandTimer) then self.expandTimer.invalidate; self.expandTimer:= NSTimer.scheduledTimerWithTimeInterval_target_selector_userInfo_repeats( CocoaConfigScroller.overlay.bar.expandTimeInterval, self, ObjCSelector('onExpandTimer:'), nil, True ); end; procedure TCocoaScrollBarEffectOverlay.onDestroy; begin self.cancelDelay; if Assigned(self.expandTimer) then begin self.expandTimer.invalidate; self.expandTimer:= nil; end; inherited onDestroy; end; procedure TCocoaScrollBarEffectOverlay.cancelDelay; begin if Assigned(self.delayTimer) then begin self.delayTimer.invalidate; self.delayTimer:= nil; end; end; { TCocoaScrollStyleManager } procedure TCocoaScrollStyleManager.updateLayout; var doc: NSView; docFrame : NSRect; horzScroller: NSScroller; vertScroller: NSScroller; horzScrollerFrame : NSRect; vertScrollerFrame : NSRect; horzScrollerHeight : CGFLoat; vertScrollerWidth : CGFLoat; begin doc:= _scrollView.documentView; if NOT Assigned(doc) then Exit; docFrame := _scrollView.frame; docFrame.origin := NSZeroPoint; horzScrollerFrame := docFrame; vertScrollerFrame := docFrame; horzScroller:= _scrollView._horzScrollBar; vertScroller:= _scrollView._vertScrollBar; if Assigned(horzScroller) then begin if NOT isBarOccupyBound or isAvailableScrollBar(horzScroller) then begin horzScrollerHeight := NSScroller.scrollerWidthForControlSize_scrollerStyle( horzScroller.controlSize, horzScroller.scrollerStyle); horzScrollerFrame.size.height := horzScrollerHeight; if isBarOccupyBound then begin docFrame.size.height := docFrame.size.height - horzScrollerHeight; if docFrame.size.height < 0 then docFrame.size.height := 0; docFrame.origin.y := horzScrollerHeight; end; end; end; if Assigned(vertScroller) then begin if NOT isBarOccupyBound or isAvailableScrollBar(vertScroller) then begin vertScrollerWidth := NSScroller.scrollerWidthForControlSize_scrollerStyle( vertScroller.controlSize, vertScroller.scrollerStyle); vertScrollerFrame.size.width := vertScrollerWidth; if isBarOccupyBound then begin docFrame.size.width := docFrame.size.width - vertScrollerWidth; if docFrame.size.width < 0 then docFrame.size.width:= 0; end; end; end; horzScrollerFrame.size.width := docFrame.size.width; vertScrollerFrame.size.height := docFrame.size.height; if isBarOccupyBound then vertScrollerFrame.origin.x := docFrame.size.width else vertScrollerFrame.origin.x := docFrame.size.width - vertScrollerFrame.size.width; vertScrollerFrame.origin.y := docFrame.origin.y; if Assigned(horzScroller) then begin if horzScrollerFrame.size.width <= horzScrollerFrame.size.height then horzScrollerFrame.size.width:= horzScrollerFrame.size.height + 1; horzScroller.setFrame(horzScrollerFrame); end; if Assigned(vertScroller) then begin if vertScrollerFrame.size.height <= vertScrollerFrame.size.width then vertScrollerFrame.size.height:= vertScrollerFrame.size.width + 1; vertScroller.setFrame(vertScrollerFrame); end; if not NSEqualRects(doc.frame, docFrame) then begin doc.setFrame(docFrame); {$ifdef BOOLFIX} doc.setNeedsDisplay__(Ord(true)); {$else} doc.setNeedsDisplay_(true); {$endif} end; end; constructor TCocoaScrollStyleManager.createForScrollBar; begin end; constructor TCocoaScrollStyleManager.createForScrollView(scrollView: TCocoaManualScrollView ); begin _scrollView:= scrollView; end; { TCocoaScrollViewStyleManagerLegacy } function TCocoaScrollStyleManagerLegacy.createScrollBarEffect( scroller:NSScroller ): TCocoaScrollBarEffect; var effect: TCocoaScrollBarEffectAlpha; begin effect:= TCocoaScrollBarEffectAlpha.alloc.Init; effect.scroller:= scroller; effect.manager:= self; effect.currentAlpha:= CocoaConfigScroller.legacy.knob.alpha; Result:= effect; end; procedure TCocoaScrollStyleManagerLegacy.availScrollBar( scroller: NSScroller; available: Boolean); begin if NOT Assigned(scroller) then Exit; if NOT available then begin scroller.setHidden( True ); Exit; end; if scroller.knobProportion = 1 then Exit; if scroller.isHidden then begin scroller.setHidden( false ); scroller.setAlphaValue( 1 ); end; end; function TCocoaScrollStyleManagerLegacy.isAvailableScrollBar(scroller: NSScroller ): Boolean; begin Result:= Assigned(scroller) and NOT scroller.isHidden and (00; end; function TCocoaScrollStyleManagerOverlay.isBarOccupyBound: Boolean; begin Result:= False; end; procedure TCocoaScrollStyleManagerOverlay.onKnobValueUpdated( scroller:NSScroller; var knobPosition:Double; var knobProportion:Double ); var scrollBar: TCocoaScrollBar Absolute scroller; effect: TCocoaScrollBarEffectOverlay; slotRect: NSRect; slotSize: CGFloat; begin effect:= TCocoaScrollBarEffectOverlay(scrollBar.effect); slotRect:= scroller.rectForPart(NSScrollerKnobSlot); if scrollBar.IsHorizontal then slotSize:= slotRect.size.width else slotSize:= slotRect.size.height; if knobProportion*slotSize <= CocoaConfigScroller.overlay.knob.minSize then begin if slotSize<=CocoaConfigScroller.overlay.knob.minSize then knobProportion:= 0.99 else knobProportion:= CocoaConfigScroller.overlay.knob.minSize/slotSize; end; if (effect.currentKnobPosition=knobPosition) and (effect.currentKnobProportion=knobProportion) then Exit; if effect.currentKnobPosition <> knobPosition then _scrollView.onBarScrolled( scroller ); effect.currentKnobPosition:= knobPosition; effect.currentKnobProportion:= knobProportion; self.doShowScrollBar( scroller ); end; procedure TCocoaScrollStyleManagerOverlay.onMouseEntered(scroller: NSScroller); var scrollBar: TCocoaScrollBar Absolute scroller; effect: TCocoaScrollBarEffectOverlay; begin effect:= TCocoaScrollBarEffectOverlay(scrollBar.effect); effect.entered:= True; effect.setExpandTimer; scroller.setNeedsDisplay_(true); end; procedure TCocoaScrollStyleManagerOverlay.onMouseExited(scroller: NSScroller); var scrollBar: TCocoaScrollBar Absolute scroller; effect: TCocoaScrollBarEffectOverlay; begin effect:= TCocoaScrollBarEffectOverlay(scrollBar.effect); effect.entered:= False; effect.setDelayHidingTimer; end; function TCocoaScrollStyleManagerOverlay.createScrollBarEffect( scroller: NSScroller): TCocoaScrollBarEffect; var effect: TCocoaScrollBarEffectOverlay; begin effect:= TCocoaScrollBarEffectOverlay.alloc.Init; effect.scroller:= scroller; effect.manager:= self; effect.currentKnobPosition:= -1; effect.currentKnobProportion:= -1; effect.currentAlpha:= CocoaConfigScroller.overlay.bar.alpha; Result:= effect; end; procedure TCocoaScrollStyleManagerOverlay.availScrollBar( scroller: NSScroller; available: Boolean); begin if NOT Assigned(scroller) then Exit; if scroller.knobProportion=1 then begin scroller.setAlphaValue( 0 ); scroller.setHidden( True ); end; end; function TCocoaScrollStyleManagerOverlay.isAvailableScrollBar(scroller: NSScroller ): Boolean; begin Result:= Assigned(scroller) and (0