superspinner.pas 52 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656
  1. // SPDX-License-Identifier: LGPL-3.0-linking-exception
  2. {
  3. The BGRASpinner is basically a spinner that just spins like an encoder pulse
  4. wheel. You can set many specific details to render and operate this control.
  5. Remeber you capture the pulses as the knob spins, it does retain the angular
  6. position, but typically that is not really used. Events for key operations
  7. including movement, wrapping, etc.
  8. In addition specific events for clicking on the center button (if enabled) or
  9. the spinner area if desired.
  10. When using these controls it best to have the form scaling set to FALSE, since
  11. some aspects of the compoent reference SIZE of the client area and scaling
  12. will update your sizes of Width and Height, good luck.
  13. Lastly the Resolution of the spinner can be controled, but since angular control
  14. or trying to set it for all conditions became a problem, the settings are
  15. from Higest to Lowest. Highest being maximum resolution of the mouse movements
  16. and Lowest makes it more like an old iPod with larger movements (clicks).
  17. }
  18. {******************************* CONTRIBUTOR(S) ******************************
  19. - Sandy Ganz | [email protected]
  20. 02/20/2025 - Begat conversion from BGRASpinner, loads of changes to support
  21. the way a Spinner works, new events and props. Updated code style
  22. to be more similar to SuperGauge.
  23. ***************************** END CONTRIBUTOR(S) *****************************}
  24. unit SuperSpinner;
  25. {$I bgracontrols.inc}
  26. interface
  27. uses
  28. Classes, SysUtils, {$IFDEF FPC}LResources,{$ENDIF} Forms, Controls, Graphics,
  29. {$IFNDEF FPC}BGRAGraphics, GraphType, FPImage, {$ENDIF}
  30. BCBaseCtrls, BGRAGradients, BGRABitmap, BGRABitmapTypes, SuperSpinnerCommon;
  31. const
  32. VERSIONSTR = '1.00'; // spinner version
  33. WHEEL_SPEED_FACTOR = 0.005; // used to calculate mouse wheel speed
  34. RESOLUTION_HIGHEST = 1; // used for setting spinners resolution
  35. RESOLUTION_HIGH = 2; // Keeps the number of position somewhat hidden
  36. RESOLUTION_HIGH_MEDIUM = 3;
  37. RESOLUTION_MEDIUM = 4;
  38. RESOLUTION_MEDIUM_LOW = 5;
  39. RESOLUTION_LOW = 10;
  40. RESOLUTION_LOWEST = 20;
  41. type
  42. TSSHitType = (shtNone, shtCap, shtKnob); // for sub component hit test
  43. TSSResolution = (srHighest, srHigh, srHighMedium, srMedium, srMediumLow, srLow, srLowest);
  44. TSSpinnerPosChangedEvent = procedure(Sender: TObject; Shift: TShiftState; Value: single; MoveDir : TSSDirection) of object;
  45. TSSpinnerCapClickEvent = procedure(Sender: TObject; Button: TMouseButton; Shift: TShiftState) of object;
  46. TSSpinnerKnobClickEvent = procedure(Sender: TObject; Button: TMouseButton; Shift: TShiftState) of object;
  47. TSSpinnerWrappedEvent = procedure(Sender: TObject; Shift: TShiftState; OldAngle, NewAngle: single; MoveDir : TSSDirection) of object;
  48. TSSpinnerCapEnterEvent = procedure(Sender: TObject; Shift: TShiftState; X,Y: Integer) of object;
  49. TSSpinnerCapLeaveEvent = procedure(Sender: TObject; Shift: TShiftState; X,Y: Integer) of object;
  50. TSSpinnerKnobEnterEvent = procedure(Sender: TObject; Shift: TShiftState;X,Y: Integer) of object;
  51. TSSpinnerKnobLeaveEvent = procedure(Sender: TObject; Shift: TShiftState; X,Y: Integer) of object;
  52. TResolveSizes = Record
  53. MinRadius: integer;
  54. MinWH: integer;
  55. FrameBorderWidth: integer;
  56. CapRadius: integer;
  57. CapEdgeThickness: integer;
  58. PositionRadius: integer;
  59. PositionMargin: integer;
  60. PositionCenterMargin: integer;
  61. PositionLineWidth: integer;
  62. KnobEdgeThickness: integer;
  63. // add anything here that might need autosize
  64. // also initialize these in the constructor
  65. end;
  66. { TCustomSuperSpinner }
  67. TCustomSuperSpinner = class(TBGRAGraphicCtrl)
  68. private
  69. { Private declarations }
  70. FDirty: boolean;
  71. // Settings
  72. FAutoScale: boolean;
  73. FResolvedSizes: TResolveSizes;
  74. FPositionSettings: TSSPositionSettings;
  75. FCapSettings: TSSCapSettings;
  76. FFrameSettings: TSSFrameSettings;
  77. FKnobSettings: TSSKnobSettings;
  78. FMouseDownAnglePos: single;
  79. FMouseDownExistingPos : single;
  80. FCapMouseDown: boolean;
  81. FKnobMouseDown: boolean;
  82. FInCap: boolean;
  83. FInKnob: boolean;
  84. FSpinnerBmp: TBGRABitmap; // Main assembled image
  85. FFrameBmp: TBGRABitmap; // Draws just the frame
  86. FKnobBmp: TBGRABitmap; // Draws just the knob
  87. FCapBmp: TBGRABitmap; // Draws just the cap that sits in the middle of the knob
  88. FPositionBmp: TBGRABitmap; // Draws just the position (lines, finger hole)
  89. FAngularPos: single; // In RADIANS
  90. FCWSkipCounter: integer;
  91. FCCWSkipCounter: integer;
  92. FSpinnerResolution: TSSResolution;
  93. FSpinnerResolutionCount: integer;
  94. FSettingAngularPos: boolean;
  95. FPositionSnap: boolean;
  96. FOnSpinnerPosChange: TSSpinnerPosChangedEvent;
  97. FOnCapClick: TSSpinnerCapClickEvent;
  98. FOnKnobClick: TSSpinnerKnobClickEvent;
  99. FOnWrapped: TSSpinnerWrappedEvent;
  100. FOnMouseCapEnter: TSSpinnerCapEnterEvent;
  101. FOnMouseCapLeave: TSSpinnerCapLeaveEvent;
  102. FOnMouseKnobEnter: TSSpinnerKnobEnterEvent;
  103. FOnMouseKnobLeave: TSSpinnerKnobLeaveEvent;
  104. FLocked: boolean; // Keeps Mouse from doing most things
  105. FWheelSpeed: byte; // 0 : no wheel, 1 slowest, 255 fastest
  106. FMinRadius: integer; // Computed minimum dimension for radius of spinner including Margin
  107. function GetAngle: single;
  108. function RadPosToDeg(RadPos: single): single;
  109. function DegPosToAngular(DegPos: single): single;
  110. procedure SetAngle(AValue: single);
  111. procedure SetPositionSnap(const AValue: boolean);
  112. function CalcAngularPos(X, Y: integer) : single;
  113. procedure UpdateAngularPos(Shift: TShiftState; AngularPos: single);
  114. function CapHitTest(X, Y: integer): boolean;
  115. function KnobHitTest(X, Y: integer): boolean;
  116. function HitTest(X, Y: integer): TSSHitType;
  117. procedure SetAutoScale(AValue: boolean);
  118. procedure SetWheelSpeed(AValue: byte);
  119. procedure SetLocked(AValue: boolean);
  120. procedure SetPositionSettings(AValue: TSSPositionSettings);
  121. procedure SetCapSettings(AValue: TSSCapSettings);
  122. procedure SetFrameSettings(AValue: TSSFrameSettings);
  123. procedure SetKnobSettings(AValue: TSSKnobSettings);
  124. procedure SetResolution(const AValue: TSSResolution);
  125. protected
  126. { Protected declarations }
  127. class function GetControlClassDefaultSize: TSize; override;
  128. procedure DoChange({%H-}Sender: TObject);
  129. procedure DoSetBounds(ALeft, ATop, AWidth, AHeight: Integer); override;
  130. procedure MouseDown(Button: TMouseButton; Shift: TShiftState; X, Y: integer); override;
  131. procedure MouseUp(Button: TMouseButton; Shift: TShiftState; X, Y: integer); override;
  132. procedure MouseMove(Shift: TShiftState; X, Y: integer); override;
  133. function GetMinSize: integer;
  134. procedure ResolveSizes;
  135. procedure Paint; override;
  136. procedure DrawFrame;
  137. procedure DrawKnob;
  138. procedure DrawCap;
  139. procedure DrawPosition;
  140. function DoMouseWheel(Shift: TShiftState; WheelDelta: integer; MousePos: TPoint): boolean; override;
  141. procedure MouseWheelPos({%H-}Shift: TShiftState; WheelDelta: integer); virtual;
  142. public
  143. { Public declarations }
  144. constructor Create(AOwner: TComponent); override;
  145. destructor Destroy; override;
  146. public
  147. { Streaming }
  148. {$IFDEF FPC}
  149. procedure SaveToFile(AFileName: string);
  150. procedure LoadFromFile(AFileName: string);
  151. {$ENDIF}
  152. procedure OnFindClass({%H-}Reader: TReader; const AClassName: string; var ComponentClass: TComponentClass);
  153. procedure Bump(Direction: TSSDirection; Degrees: single);
  154. procedure Spin(Direction: TSSDirection; Degrees: single; Count: integer; ProcessMessages: Boolean = True);
  155. published
  156. { Published declarations }
  157. property AutoScale: boolean read FAutoScale write SetAutoScale default False;
  158. property PositionSettings: TSSPositionSettings read FPositionSettings write SetPositionSettings;
  159. property CapSettings: TSSCapSettings read FCapSettings write SetCapSettings;
  160. property FrameSettings: TSSFrameSettings read FFrameSettings write SetFrameSettings;
  161. property KnobSettings: TSSKnobSettings read FKnobSettings write SetKnobSettings;
  162. property PositionSnap: boolean read FPositionSnap write SetPositionSnap default False;
  163. property Angle: single read GetAngle write SetAngle nodefault;
  164. property SpinResolution: TSSResolution read FSpinnerResolution write SetResolution default srHighest;
  165. property WheelSpeed: byte read FWheelSpeed write SetWheelSpeed default 0;
  166. property Locked: boolean read FLocked write SetLocked default False; // TODO : Check if we need to cancel mouse movement, etc
  167. property OnPosChanged: TSSpinnerPosChangedEvent read FOnSpinnerPosChange write FOnSpinnerPosChange;
  168. property OnCapClick: TSSpinnerCapClickEvent read FOnCapClick write FOnCapClick;
  169. property OnKnobClick: TSSpinnerKnobClickEvent read FOnKnobClick write FOnKnobClick;
  170. property OnWrapped: TSSpinnerWrappedEvent read FOnWrapped write FOnWrapped;
  171. property OnMouseCapEnter: TSSpinnerCapEnterEvent read FOnMouseCapEnter write FOnMouseCapEnter;
  172. property OnMouseCapLeave: TSSpinnerCapLeaveEvent read FOnMouseCapLeave write FOnMouseCapLeave;
  173. property OnMouseKnobEnter: TSSpinnerKnobEnterEvent read FOnMouseKnobEnter write FOnMouseKnobEnter;
  174. property OnMouseKnobLeave: TSSpinnerKnobLeaveEvent read FOnMouseKnobLeave write FOnMouseKnobLeave;
  175. property OnMouseWheel;
  176. property OnClick;
  177. property OnDblClick;
  178. property OnMouseDown;
  179. property OnMouseUp;
  180. property OnMouseMove;
  181. property OnMouseEnter;
  182. property OnMouseLeave;
  183. property Visible;
  184. end;
  185. { TSuperSpinner }
  186. TSuperSpinner = class(TCustomSuperSpinner)
  187. private
  188. { Private declarations }
  189. protected
  190. { Protected declarations }
  191. public
  192. { Public declarations }
  193. published
  194. { Published declarations }
  195. property Anchors;
  196. property Color default clNone;
  197. property Hint;
  198. property ShowHint;
  199. end;
  200. {$IFDEF FPC}
  201. procedure Register;
  202. {$ENDIF}
  203. implementation
  204. uses Math;
  205. {$IFDEF FPC}
  206. procedure Register;
  207. begin
  208. RegisterComponents('BGRA Controls', [TSuperSpinner]);
  209. end;
  210. {$ENDIF}
  211. { TCustomSuperSpinner }
  212. constructor TCustomSuperSpinner.Create(AOwner: TComponent);
  213. begin
  214. inherited Create(AOwner);
  215. // remember if form is scaled CX, CY values will be too!
  216. // this may not do anything!!!
  217. with GetControlClassDefaultSize do
  218. SetInitialBounds(0, 0, CX, CY);
  219. // Position Settings
  220. FPositionSettings := TSSPositionSettings.Create;
  221. FPositionSettings.OnChange := DoChange;
  222. // Spinner Cap Settings
  223. FCapSettings := TSSCapSettings.Create;
  224. FCapSettings.OnChange := DoChange;
  225. // Frame Settings
  226. FFrameSettings := TSSFrameSettings.Create;
  227. FFrameSettings.OnChange := DoChange;
  228. // Knob Settings
  229. FKnobSettings := TSSKnobSettings.Create;
  230. FKnobSettings.OnChange := DoChange;
  231. // Bitmaps
  232. FFrameBmp := TBGRABitmap.Create;
  233. FKnobBmp := TBGRABitmap.Create;
  234. FCapBmp := TBGRABitmap.Create;
  235. FPositionBmp := TBGRABitmap.Create;
  236. FSpinnerBmp := TBGRABitmap.Create;
  237. // General Inits
  238. FOnSpinnerPosChange := nil;
  239. FOnCapClick := nil;
  240. FOnKnobClick := nil;
  241. FOnWrapped := nil;
  242. FPositionSnap := False;
  243. FWheelSpeed := 0; // 0, no wheel, 1 slowest, 255 fastest
  244. FLocked := False;
  245. FMouseDownAnglePos := 0;
  246. FMouseDownExistingPos := 0;
  247. FCapMouseDown := False;
  248. FKnobMouseDown := False;
  249. FSettingAngularPos := False;
  250. FInCap := False;
  251. Color := clNone;
  252. FCWSkipCounter := 0;
  253. FCCWSkipCounter := 0;
  254. FSpinnerResolutionCount := RESOLUTION_HIGHEST; // how many clicks it takes to make a revolution
  255. SetAngle(0); // Does NOT call any events
  256. FMinRadius := 0; // Can't know just yet
  257. // set up baseline values from the defaults, good starting point any-a-ways
  258. FResolvedSizes.MinRadius := 0; // can't know MinRadius or MinWH yet, not resolved
  259. FResolvedSizes.MinWH := 0;
  260. FResolvedSizes.FrameBorderWidth := FFrameSettings.BorderWidth;
  261. FResolvedSizes.CapRadius := FCapSettings.Radius;
  262. FResolvedSizes.CapEdgeThickness := FCapSettings.EdgeThickness;
  263. FResolvedSizes.PositionRadius := FPositionSettings.Radius;
  264. FResolvedSizes.PositionMargin := FPositionSettings.Margin;
  265. FResolvedSizes.PositionCenterMargin := FPositionSettings.CenterMargin;
  266. FResolvedSizes.PositionLineWidth := FPositionSettings.LineWidth;
  267. FResolvedSizes.KnobEdgeThickness := FKnobSettings.EdgeThickness;
  268. FDirty := True; // Always force initial paint/draw on everything!
  269. end;
  270. destructor TCustomSuperSpinner.Destroy;
  271. begin
  272. // Free up the bitmaps
  273. FSpinnerBmp.Free;
  274. FPositionBmp.Free;
  275. FFrameBmp.Free;
  276. FKnobBmp.Free;
  277. FCapBmp.Free;
  278. // Handlers (May not be needed, but good idea)
  279. FOnSpinnerPosChange := nil;
  280. FOnCapClick := nil;
  281. FOnKnobClick := nil;
  282. FOnWrapped := nil;
  283. // Position Settings
  284. FPositionSettings.OnChange := nil;
  285. FPositionSettings.Free;
  286. // Cap Settings
  287. FCapSettings.OnChange := nil;
  288. FCapSettings.Free;
  289. // Frame Settings
  290. FFrameSettings.OnChange := nil;
  291. FFrameSettings.Free;
  292. // Knob Settings
  293. FKnobSettings.OnChange := nil;
  294. FKnobSettings.Free;
  295. inherited Destroy;
  296. end;
  297. // Override the base class which has a rectangular dimension
  298. class function TCustomSuperSpinner.GetControlClassDefaultSize: TSize;
  299. begin
  300. // Note the preferred size for the control is 150x150, however in highdpi modes
  301. // on windows (maybe others) the control is scaled since the by default the forms
  302. // scale will affect the actual value on creation. So as an example, Windows 11,
  303. // 4k monitor, 150% scaling (windows settings), will cause the component to be
  304. // created and rendered with the size of 150x150. So these numbers get scaled
  305. // UP in this instance. If you run the scaling on Windows 11 at 100%, settings
  306. // after LCL does it's business is 100x100. This is tricky since some spinner
  307. // setting are NOT referenced by the size of the component but by pixels. So
  308. // the Cap for example is in non-scaled pixels, lines for the position is
  309. // based on component width so kinda' works OK, but not the cap. I remember
  310. // when pixels were just pixels...
  311. Result.CX := 100;
  312. Result.CY := 100;
  313. end;
  314. function TCustomSuperSpinner.GetMinSize: integer;
  315. begin
  316. // Take the smallest width or height so we can use for max size spinner
  317. if ClientWidth < ClientHeight then
  318. Exit(ClientWidth)
  319. else
  320. Exit(ClientHeight);
  321. end;
  322. procedure TCustomSuperSpinner.ResolveSizes;
  323. var
  324. scale: single;
  325. begin
  326. // Compute the size of the drawing elements of the spinner based
  327. // on the FMinRadius size. If AutoScale is enabled for the control
  328. // will calculate the drawing elements needed. If not will return
  329. // the correct properties so all the testing for the AutoScale
  330. // option setting is done here.
  331. // Drawing sized based on proportions of the DEFAULT component values and
  332. // may not always look right based on settings of cap, position, borders, etc.
  333. // Get the minimum size for the drawing of the spinner
  334. // Todo : Not sure if FMinRadius belongs here...
  335. FResolvedSizes.MinWH := GetMinSize;
  336. FResolvedSizes.MinRadius := FResolvedSizes.MinWH div 2;
  337. FMinRadius := FResolvedSizes.MinRadius;
  338. scale := FResolvedSizes.MinWH / 150.0;
  339. if FAutoScale then
  340. begin
  341. // AutoScale based on 150x150 spinner size. Computes ratios from that to any size
  342. // Will it always look good? Hard to say, but can use break points on sizes to
  343. // also help with some edge cases like tiny and large if needed
  344. FResolvedSizes.FrameBorderWidth := Round(FFrameSettings.BorderWidth * scale);
  345. FResolvedSizes.CapRadius := Round(FCapSettings.Radius * scale);
  346. FResolvedSizes.CapEdgeThickness := Round(FCapSettings.EdgeThickness * scale);
  347. FResolvedSizes.PositionRadius := Round(FPositionSettings.Radius * scale);
  348. FResolvedSizes.PositionMargin := Round(FPositionSettings.Margin * scale);
  349. FResolvedSizes.PositionCenterMargin := Round(FPositionSettings.CenterMargin * scale);
  350. FResolvedSizes.PositionLineWidth := Round(FPositionSettings.LineWidth * scale);
  351. FResolvedSizes.KnobEdgeThickness := Round(FKnobSettings.EdgeThickness * scale);
  352. end
  353. else
  354. begin
  355. // Easy, not scaling
  356. FResolvedSizes.FrameBorderWidth := FFrameSettings.BorderWidth;
  357. FResolvedSizes.CapRadius := FCapSettings.Radius;
  358. FResolvedSizes.CapEdgeThickness := FCapSettings.EdgeThickness;
  359. FResolvedSizes.PositionRadius := FPositionSettings.Radius;
  360. FResolvedSizes.PositionMargin := FPositionSettings.Margin;
  361. FResolvedSizes.PositionCenterMargin := FPositionSettings.CenterMargin;
  362. FResolvedSizes.PositionLineWidth := FPositionSettings.LineWidth;
  363. FResolvedSizes.KnobEdgeThickness := FKnobSettings.EdgeThickness
  364. end;
  365. end;
  366. procedure TCustomSuperSpinner.SetAutoScale(AValue: boolean);
  367. begin
  368. if FAutoScale = AValue then
  369. Exit;
  370. FAutoScale := AValue;
  371. FDirty := True;
  372. DoChange(self);
  373. end;
  374. procedure TCustomSuperSpinner.SetPositionSettings(AValue: TSSPositionSettings);
  375. begin
  376. if FPositionSettings = AValue then
  377. Exit;
  378. FPositionSettings := AValue;
  379. FPositionSettings.Dirty := True;
  380. DoChange(self);
  381. end;
  382. procedure TCustomSuperSpinner.SetCapSettings(AValue: TSSCapSettings);
  383. begin
  384. if FCapSettings = AValue then
  385. Exit;
  386. FCapSettings := AValue;
  387. FCapSettings.Dirty := True;
  388. DoChange(self);
  389. end;
  390. procedure TCustomSuperSpinner.SetFrameSettings(AValue: TSSFrameSettings);
  391. begin
  392. if FFrameSettings = AValue then
  393. Exit;
  394. FFrameSettings := AValue;
  395. FFrameSettings.Dirty := True;
  396. DoChange(self);
  397. end;
  398. procedure TCustomSuperSpinner.SetKnobSettings(AValue: TSSKnobSettings);
  399. begin
  400. if FKnobSettings = AValue then
  401. Exit;
  402. FKnobSettings := AValue;
  403. FKnobSettings.Dirty := True;
  404. DoChange(self);
  405. end;
  406. procedure TCustomSuperSpinner.DoChange(Sender: TObject);
  407. begin
  408. Invalidate;
  409. end;
  410. // Handler to force redraw when in design mode
  411. procedure TCustomSuperSpinner.DoSetBounds(ALeft, ATop, AWidth, AHeight: Integer);
  412. begin
  413. inherited;
  414. FDirty := true; // Called on Resize of component
  415. end;
  416. function TCustomSuperSpinner.RadPosToDeg(RadPos: single): single;
  417. begin
  418. // helper to convert AnglePos in radians to degrees, wraps as needed
  419. Result := RadPos * 180 / Pi;
  420. if Result < 0 then
  421. Result := Result + 360;
  422. Result := 270 - Result; // adjusts for screen coords
  423. if Result < 0 then
  424. Result := Result + 360;
  425. if Result > 360 then
  426. Result := Result - 360;
  427. end;
  428. function TCustomSuperSpinner.DegPosToAngular(DegPos: single): single;
  429. begin
  430. // helper to convert Angle in degrees to radians, wraps as needed
  431. // 3 * pi/2 = 270 degrees, degs to radians = degs * pi/180
  432. Result := 3 * Pi / 2 - DegPos * Pi / 180;
  433. if Result > Pi then
  434. Result := Result - (2 * Pi);
  435. if Result < -Pi then
  436. Result := Result + (2 * Pi);
  437. end;
  438. procedure TCustomSuperSpinner.SetWheelSpeed(AValue: byte);
  439. begin
  440. // Sets the mouse wheel speed
  441. FWheelSpeed := AValue;
  442. end;
  443. procedure TCustomSuperSpinner.SetLocked(AValue: boolean);
  444. begin
  445. // If we are locking, this may cause some issues as we are disabling
  446. // some of the mouse control. So reset back to a clean state if needed
  447. if AValue = FLocked then
  448. Exit;
  449. FLocked := AValue;
  450. // if we are not locked now we should reset stuff to a clean state.
  451. // this MIGHT be needed if the lock happens while clicking or moving
  452. // in the spinner. The user should be starting fresh if this happens
  453. // (Or so I think)
  454. if not FLocked then
  455. begin
  456. // Reset Skip Counters
  457. FCWSkipCounter := 0;
  458. FCCWSkipCounter := 0;
  459. // If mouse was down in cap or knob reset too, we lose that tracking
  460. FCapMouseDown := False;
  461. FKnobMouseDown := False;
  462. FInCap := False;
  463. FInKnob := False;
  464. // Finally stop any mouse tracking
  465. FSettingAngularPos := False;
  466. end;
  467. end;
  468. procedure TCustomSuperSpinner.SetAngle(AValue: single);
  469. begin
  470. // Sets the angle (in Degrees) of the Knobs position. This
  471. // will NOT call the OnPosChange event, and not affected by
  472. // the spinners resolution
  473. if DegPosToAngular(AValue) = FAngularPos then
  474. Exit;
  475. FAngularPos := DegPosToAngular(AValue);
  476. DoChange(self);
  477. end;
  478. function TCustomSuperSpinner.GetAngle: single;
  479. begin
  480. Result := RadPosToDeg(FAngularPos);
  481. end;
  482. // Sets if the spinner position should snap to the mouse when clicked
  483. // otherwise will allow the mouse to spin the knob without first 'snapping'
  484. // to the mouse down position
  485. procedure TCustomSuperSpinner.SetPositionSnap(const AValue: boolean);
  486. begin
  487. if FPositionSnap = AValue then
  488. exit;
  489. FPositionSnap := AValue;
  490. DoChange(self);
  491. end;
  492. procedure TCustomSuperSpinner.SetResolution(const AValue: TSSResolution);
  493. begin
  494. if AValue = FSpinnerResolution then
  495. Exit;
  496. FSpinnerResolution := AValue;
  497. // In general It's best to have it at srHighest. If you want it more like
  498. // an old iPod spinner try Low or Lowest. These are essentially
  499. // messing with the number of clicks per revolution, but I decided
  500. // not to try to calculate an exact value so these are just abstracting
  501. // that
  502. case AValue of
  503. srHighest: FSpinnerResolutionCount := RESOLUTION_HIGHEST;
  504. srHigh: FSpinnerResolutionCount := RESOLUTION_HIGH;
  505. srHighMedium: FSpinnerResolutionCount := RESOLUTION_HIGH_MEDIUM;
  506. srMedium: FSpinnerResolutionCount :=RESOLUTION_MEDIUM;
  507. srMediumLow: FSpinnerResolutionCount := RESOLUTION_MEDIUM_LOW;
  508. srLow: FSpinnerResolutionCount := RESOLUTION_LOW;
  509. srLowest: FSpinnerResolutionCount := RESOLUTION_LOWEST;
  510. end;
  511. end;
  512. function TCustomSuperSpinner.CalcAngularPos(X, Y: integer) : single;
  513. begin
  514. // returns -pi to pi based on the XY of the mouse in the client box
  515. Result := ArcTan2(-1 * (Y - ClientHeight / 2) / ClientHeight, (X - ClientWidth / 2) / ClientWidth);
  516. end;
  517. procedure TCustomSuperSpinner.Bump(Direction: TSSDirection; Degrees: single);
  518. var
  519. Offset: single;
  520. begin
  521. if (Degrees < 0) or (Degrees > 359.99999) then
  522. Exit;
  523. Offset := GetAngle();
  524. if Direction = sdCW then
  525. Offset := Offset + Degrees
  526. else
  527. Offset := Offset - Degrees;
  528. // Force move, since UpdateAngularPos() PRE-Increments the Skip counters we
  529. // Must be one less or this trick won't work
  530. //
  531. // Since we may be forcing a specific degree move here, it can
  532. // shift the position of the spinner to an off increment angle than
  533. // the mouse is moving since that angle to bump to is arbitrary.
  534. // In general Spin and Bump are not great to use for this reason unless needed.
  535. // Must invalidate both as we don't know the current direction it's moving
  536. // so one will get reset, the other will trigger, so always works.
  537. FCWSkipCounter := FSpinnerResolutionCount - 1;
  538. FCCWSkipCounter := FCWSkipCounter;
  539. UpdateAngularPos([], DegPosToAngular(Offset));
  540. end;
  541. procedure TCustomSuperSpinner.Spin(Direction: TSSDirection; Degrees: single; Count: integer; ProcessMessages: Boolean = True);
  542. var
  543. i, processRate: integer;
  544. begin
  545. // This is something that likely should not be used more so then bump. It is easy to animate
  546. // a movement to a number of events triggered. This is tricky as you need to
  547. // call ProcessMessages or the update of the spinner will/could show up just
  548. // at the finish point since it will just do it fast if no movement will be shown.
  549. // Some tricky-ness can be done, for example if you want to do a Count of 100 at
  550. // 1 Degree per, that will be quickly animated, if you want to slow it down
  551. // you can try 0.1 Degrees per, and 1000 for the Count and only process
  552. // 1 out of 10 movement events to make it the same, the spinner will go slower
  553. // as it's rendering at a higher resolution, this is a hack for sure.
  554. // Degrees will be validated in Bump()
  555. if (Count < 1) then
  556. Exit;
  557. // Super Hack
  558. //
  559. // Try to keep fast for fine moves or moves with a lot of steps so looks nice
  560. // Tries to keep down calls to ProcessMessages, but Still update the display
  561. // As the Count goes up or the Degree granularity goes up (smaller Degree) the
  562. // processRate is smaller to have more screen updated UNLESS the count is
  563. // just too large, and then it slows down a lot. This is all testing
  564. // on a fast machine, fast video, Low or high res, lower speed CPU or Video
  565. // would totally impact this code.
  566. //
  567. // SUPER HACK
  568. if (Degrees < 1.0) or (Count < 25) then
  569. processRate := 2 // process a lot of screen updates
  570. else
  571. processRate := 4; // Less
  572. // If we have a lot of resolution can turn down the process rate a lot
  573. if (Count / Degrees) > 500 then
  574. processRate := 16; // A lot less since movement is very small, not worth a lot of updates
  575. for i := 0 to Count - 1 do
  576. begin
  577. // Bump will call the event handler for movement for each
  578. Bump(Direction, Degrees);
  579. // Call ProcessMessages at a slower rate for small Degrees or large Count
  580. // Not sure if their is a better way to move and update the visuals. This
  581. // may not be needed IF the PosChanged event handler actually does a lot of
  582. // stuff, but I think (on Windows) the drawing of the spinners are all
  583. // coalesced until the message loop is caught up and only the last update
  584. // to the screen is seen. Application. ProcessMessages an optional call
  585. // and can let the handler deal with it as needed.
  586. if (i mod processRate = 0) and ProcessMessages then
  587. Application.ProcessMessages;
  588. end;
  589. end;
  590. procedure TCustomSuperSpinner.UpdateAngularPos(Shift: TShiftState; AngularPos: single);
  591. var
  592. Direction: TSSDirection;
  593. currAngle, newAngle: single;
  594. begin
  595. // AngularPos is in Rads, Wrap range if needed (Radians wrap)
  596. if AngularPos > Pi then
  597. AngularPos := AngularPos - (2 * Pi);
  598. if AngularPos < -Pi then
  599. AngularPos := AngularPos + (2 * Pi);
  600. // See which direction we are going, check start (Current)
  601. // is less than the new. This will give us the direction
  602. // This works EXCEPT at wrap around from 359 to 0 and 0 to 359
  603. // so either bring in the X, Y and do it sector by sector or
  604. // hack and say that if in the lower 2 sectors and track around
  605. // that. The 270 is a big delta, and unlikely, so unless a very large
  606. // update it works great. Remember that setting the position by
  607. // Angle does NOT cause the handler to be called ONLY this update method.
  608. currAngle := GetAngle(); // Degs
  609. newAngle := RadPosToDeg(AngularPos); // Degs
  610. // need this for skipping first
  611. if newAngle - currAngle > 270 then // crossing CCW over 359 to 0
  612. Direction := sdCCw
  613. else
  614. if currAngle - newAngle > 270 then // crossing CW over 0 to 359
  615. Direction := sdCW
  616. else
  617. if currAngle < newAngle then
  618. Direction := sdCW
  619. else
  620. Direction := sdCCW;
  621. // Must take into account direction changes so we can
  622. // have fresh counts in the correct direction or it
  623. // would have an inconsistant value if moving back and forth!
  624. if Direction = sdCW then
  625. begin
  626. Inc(FCWSkipCounter);
  627. FCCWSkipCounter := 0;
  628. end
  629. else
  630. begin
  631. Inc(FCCWSkipCounter);
  632. FCWSkipCounter := 0;
  633. end;
  634. // 1 is never skip since we pre-inc the numbers above, 2 is skip every other and so on
  635. if (FCWSkipCounter = FSpinnerResolutionCount) or (FCCWSkipCounter = FSpinnerResolutionCount) then
  636. begin
  637. // We are moving, so can reset BOTH, and set the new position, then update
  638. FCWSkipCounter := 0;
  639. FCCWSkipCounter := 0;
  640. // Need to check wrap here before we update the positions
  641. if newAngle - currAngle > 270 then // crossing CCW over 359 to 0
  642. begin
  643. if Assigned(FOnWrapped) then
  644. FOnWrapped(Self, Shift, currAngle, newAngle, sdCCW);
  645. end
  646. else
  647. if currAngle - newAngle > 270 then // crossing CW over 0 to 359
  648. begin
  649. if Assigned(FOnWrapped) then
  650. FOnWrapped(Self, Shift, currAngle, newAngle, sdCW);
  651. end;
  652. FAngularPos := AngularPos;
  653. if Assigned(FOnSpinnerPosChange) then
  654. FOnSpinnerPosChange(Self, Shift, Angle, Direction);
  655. DoChange(self);
  656. end;
  657. end;
  658. procedure TCustomSuperSpinner.MouseDown(Button: TMouseButton; Shift: TShiftState; X, Y: integer);
  659. var
  660. hitIn: TSSHitType;
  661. begin
  662. inherited MouseDown(Button, Shift, X, Y);
  663. if FLocked then
  664. Exit;
  665. // See if anything clicked on the cap, and then the knob
  666. // If we do have a Cap hit and it's enabled, then we
  667. // do not need to check the Knob, as it can't be in it!
  668. // HEADS UP : If the knob porting is not clicked on, ie, blank client area,
  669. // the border, the cap (and future stuff) the state of FSettingAngularPos
  670. // will NOT change. Mouse movement once started does NOT look at any boundries
  671. hitIn := HitTest(X, Y);
  672. if hitIn = shtCap then
  673. FCapMouseDown := True
  674. else
  675. if hitIn = shtKnob then
  676. FKnobMouseDown := True;
  677. // if user has pressed the left mouse button, then start tracking
  678. // skip any movement if mouse down in the cap (button enabled)
  679. if (Button = mbLeft) and (not FCapMouseDown) and (FKnobMouseDown) then
  680. begin
  681. FSettingAngularPos := True; // start of dragging the spinner, update the state
  682. // save the angle of the mouse down, this will later
  683. // be used to offset to the current position with existing angle
  684. // to allow the user to grab anywhere on the knob and spin
  685. FMouseDownAnglePos := CalcAngularPos(X, Y);
  686. if FPositionSnap then
  687. begin
  688. // If we have position snap enabled, when the mouse clicks on it, will spin
  689. // the spinners angle to it, position to it, but will NOT update anything
  690. // else or call the handler for movement
  691. FAngularPos := FMouseDownAnglePos;
  692. DoChange(self);
  693. end;
  694. FMouseDownExistingPos := FAngularPos; // after update always set this
  695. end;
  696. end;
  697. procedure TCustomSuperSpinner.MouseUp(Button: TMouseButton; Shift: TShiftState; X, Y: integer);
  698. var
  699. hitIn: TSSHitType;
  700. begin
  701. inherited MouseUp(Button, Shift, X, Y);
  702. if FLocked then
  703. Exit;
  704. if Button = mbLeft then
  705. FSettingAngularPos := False; // Change state to not moving
  706. // can have different mouse buttons click on the cap, catch then all I guess
  707. // let the OnClicks sort it out if needed
  708. // see if we are still in the cap, if so call back as this is a OnClick
  709. // style event. Always clear the state in anycase as a left mouse up
  710. // should stop tracking the down events
  711. hitIn := HitTest(X, Y);
  712. if FCapMouseDown and (hitIn = shtCap) then
  713. begin
  714. if Assigned(FOnCapClick) then
  715. FOnCapClick(Self, Button, Shift);
  716. end
  717. else
  718. if FKnobMouseDown and (hitIn = shtKnob) then
  719. begin
  720. if Assigned(FOnKnobClick) then
  721. FOnKnobClick(Self, Button, Shift);
  722. end;
  723. FCapMouseDown := False; // wipes all potential mouse downs
  724. FKnobMouseDown := False;
  725. end;
  726. procedure TCustomSuperSpinner.MouseMove(Shift: TShiftState; X, Y: integer);
  727. var
  728. hitIn: TSSHitType;
  729. begin
  730. inherited MouseMove(Shift, X, Y);
  731. if FLocked then
  732. Exit;
  733. // being nice, here is a specific event for the cap/knob enter/exit
  734. // can do some nice stuff with it like highlight when over them
  735. hitIn := HitTest(X, Y);
  736. // See what's cooking with the cap first. If cap disabled always False
  737. if FInCap <> (hitIn = shtCap) then
  738. begin
  739. if FInCap then
  740. begin
  741. // we are in the cap, then exiting
  742. FInCap := False;
  743. if Assigned(FOnMouseCapLeave) then
  744. FOnMouseCapLeave(Self, Shift, X, Y);
  745. end
  746. else
  747. begin
  748. // Out of the cap, then entering
  749. FInCap := True;
  750. if Assigned(FOnMouseCapEnter) then
  751. FOnMouseCapEnter(Self, Shift, X, Y);
  752. end;
  753. end;
  754. // now the Knob part
  755. if FInKnob <> (hitIn = shtKnob) then
  756. begin
  757. if FInKnob then
  758. begin
  759. // we are in the cap, then exiting
  760. FInKnob := False;
  761. if Assigned(FOnMouseKnobLeave) then
  762. FOnMouseKnobLeave(Self, Shift, X, Y);
  763. end
  764. else
  765. begin
  766. // Out of the cap, then entering
  767. FInKnob := True;
  768. if Assigned(FOnMouseKnobEnter) then
  769. FOnMouseKnobEnter(Self, Shift, X, Y);
  770. end;
  771. end;
  772. if FSettingAngularPos then
  773. begin
  774. // Move the spinner, takes into account the MouseDown values
  775. // to either snap the wheels angle to the mouse or ignore. This
  776. // is all done in MouseDown based on the PositionSnap setting.
  777. FKnobMouseDown := False; // Cancel KnobMouseDown so we don't allow click if moving
  778. UpdateAngularPos(Shift, FMouseDownExistingPos + CalcAngularPos(X, Y) - FMouseDownAnglePos);
  779. end;
  780. end;
  781. function TCustomSuperSpinner.CapHitTest(X, Y: integer) : boolean;
  782. begin
  783. // Easy check, if mouse distance from center of client is
  784. // within center radius (also at center of client) we can
  785. // hit test the cap circle
  786. // see if we need to even do anything, also prevents error if radius is 0
  787. if FCapSettings.Style = csNone then // safe-tee
  788. Exit(False);
  789. // If the distance of the mouse to center is less than the radius of the cap
  790. // and the edge we are in the cap, remember dealing with the RADIUS not Diameter
  791. // The center of the measure is not 0,0 but half the the min size. So if the
  792. // size of the MinRadius is 75, the line is measured from 75,75 to the Mouse
  793. // X,Y which is in terms of the client area. Tricky but works. Similar for
  794. // the Knob. We need to use the client sizes for width and height here to get the center!
  795. Result := Sqrt(((FSpinnerBmp.Width div 2 - X)** 2 + (FSpinnerBmp.Height div 2 - Y)** 2))
  796. <= (FResolvedSizes.CapRadius + FResolvedSizes.CapEdgeThickness - 1);
  797. end;
  798. function TCustomSuperSpinner.KnobHitTest(X, Y: integer) : boolean;
  799. begin
  800. // if are using the cap as a button, and it's a hit,
  801. // get out, we don't count that as a knob hit as
  802. // it's excluded in this case, so a bit slower to call this first
  803. // but what can you do unless you want to do more math below...
  804. if CapHitTest(X, Y) then
  805. Exit(False);
  806. // Get the current Radius of the knob, GetMinRadius returns the smaller of
  807. // width/height of the client and less the frame width.
  808. // Todo : May just use FMinRadius as it must be calculated if we had
  809. // a paint event done. So might not need to recompute
  810. // shortRadius := FMinRadius - FFrameSettings.BorderWidth;
  811. // Test if the distance from the mouse to the center is less then the short radius
  812. // we are in the knob. Remeber we tested for cap and if in that we are not here
  813. // as the radius must be longer then the cap! We need to use the client sizes
  814. // for width and height here to get the center!
  815. Result := Sqrt(((FSpinnerBmp.Width div 2 - X)** 2 + (FSpinnerBmp.Height div 2 - Y)** 2))
  816. <= (FResolvedSizes.MinRadius - FResolvedSizes.FrameBorderWidth);
  817. end;
  818. // This done for future expansion of sub items in a spinner, for now simple
  819. function TCustomSuperSpinner.HitTest(X, Y: integer) : TSSHitType;
  820. begin
  821. // if are using the cap as a button, and it's a hit all done, only one
  822. // can be hit at a time
  823. if CapHitTest(X, Y) then
  824. Exit(shtCap);
  825. // now on with the knob
  826. if KnobHitTest(X, Y) then
  827. Exit(shtknob);
  828. Result := shtNone;
  829. end;
  830. procedure TCustomSuperSpinner.Paint;
  831. var
  832. offsetX, offsetY: integer;
  833. begin
  834. if (ClientWidth = 0) or (ClientHeight = 0) then
  835. exit;
  836. // Generally all objects should draw in the center of the client area, so
  837. // the spinner is ALWAYS square. IF the initializebitmap routine is used
  838. // it also has min size for width and height, but you still need to div/2
  839. // to get the radius, this can save a bit of calcs in loops if it can be used.
  840. // ResolveSizes calculates a bunch of sizes for the component based on the
  841. // setting of the AutoScale. It MUST be called prior to paint so all needed
  842. // sizes and dimensions for drawing the elements are resolved!
  843. ResolveSizes;
  844. // IF the component is resized OR moved (this is safer) we
  845. // need to make sure EVERYTHING redraws. The base class will
  846. // also do it's own thing to invalidate and redraw it all.
  847. if FDirty then
  848. begin
  849. FPositionSettings.Dirty := True; // sjg - this is ALWAYS dirty for drawing
  850. FCapSettings.Dirty := True;
  851. FFrameSettings.Dirty := True;
  852. FKnobSettings.Dirty := True;
  853. FDirty := False; // everything here marked, so can reset
  854. end;
  855. // no cost on SetSize if same sizes!
  856. FSpinnerBmp.SetSize(Width, Height);
  857. // If the spinner color is clNone then we start with a transparent background,
  858. // Otherwise we start with the users color.
  859. if Color = clNone then
  860. FSpinnerBmp.Fill(BGRA(0, 0, 0, 0)) // fill transparent
  861. else
  862. FSpinnerBmp.Fill(ColorToBGRA(Color, 255)); // fill solid color
  863. // If the frame changes we must dirty the knob as the frame
  864. // changes could impact size of the knob
  865. if FFrameSettings.Dirty then
  866. FKnobSettings.Dirty := True;
  867. DrawFrame;
  868. FSpinnerBmp.BlendImage(0, 0, FFrameBmp, boLinearBlend);
  869. DrawKnob;
  870. offsetX := FSpinnerBmp.Width div 2 - FKnobBmp.Width div 2;
  871. offsetY := FSpinnerBmp.Height div 2 - FKnobBmp.Height div 2;
  872. FSpinnerBmp.BlendImage(offsetX, offsetY, FKnobBmp, boLinearBlend);
  873. // Position is most always rendered and drawn. Could optimize
  874. // by drawing the position and moving around the spinnerbmp
  875. // by getting the correct position. Left as an exercise for the
  876. // coder...
  877. DrawPosition;
  878. FSpinnerBmp.BlendImage(0, 0, FPositionBmp, boLinearBlend);
  879. // Draw Cap last as it can be a nice look over lines if needed
  880. if FCapSettings.Style <> csNone then
  881. begin
  882. DrawCap;
  883. offsetX := FSpinnerBmp.Width div 2 - FCapBmp.Width div 2;
  884. offsetY := FSpinnerBmp.Height div 2 - FCapBmp.Height div 2;
  885. FSpinnerBmp.BlendImage(offsetX, offsetY, FCapBmp, boLinearBlend);
  886. end;
  887. // draw other stuff as needed here before the canvas draw
  888. FSpinnerBmp.Draw(Canvas, 0, 0, False);
  889. end;
  890. procedure TCustomSuperSpinner.DrawFrame;
  891. var
  892. Origin: TSSOrigin;
  893. r: integer;
  894. begin
  895. if not FFrameSettings.Dirty then
  896. Exit;
  897. FFrameSettings.Dirty := False;
  898. // Origin has the correct Max size the radius can be!
  899. Origin := Initializebitmap(FFrameBmp, Width, Height);
  900. // skip doing anything further if border is 0
  901. if FResolvedSizes.FrameBorderWidth < 1 then
  902. Exit;
  903. // Get the radius of the frame, less border so we can fit
  904. r := FResolvedSizes.MinRadius - FResolvedSizes.FrameBorderWidth div 2 - 1;
  905. // Draw thin antialiased border to smooth against background
  906. FFrameBmp.EllipseAntialias(Origin.CenterPoint.x, Origin.CenterPoint.y,
  907. r, r,
  908. FFrameSettings.BorderColor,
  909. FResolvedSizes.FrameBorderWidth);
  910. end;
  911. procedure TCustomSuperSpinner.DrawKnob;
  912. var
  913. xy: integer;
  914. h: single;
  915. d2: single;
  916. v: TPointF;
  917. p: PBGRAPixel;
  918. Center: TPointF;
  919. yb: integer;
  920. xb: integer;
  921. mask: TBGRABitmap;
  922. Map: TBGRABitmap;
  923. begin
  924. if not FKnobSettings.Dirty then
  925. Exit;
  926. FKnobSettings.Dirty := False;
  927. // set the knob size less the radius
  928. xy := FResolvedSizes.MinWH;
  929. FKnobBmp.SetSize(xy, xy);
  930. // Clear bitmap to transparent
  931. FKnobBmp.Fill(BGRA(0, 0, 0, 0));
  932. Center := PointF(xy / 2, xy / 2);
  933. case FKnobSettings.Style of
  934. ssFlat:
  935. begin // draw flat knob
  936. // This will draw it filled with an edge, must remove both
  937. // the knob's edge and the frames edge thickness to get the right size
  938. FKnobBmp.EllipseAntialias(Center.x, Center.y,
  939. FResolvedSizes.MinRadius - FResolvedSizes.FrameBorderWidth - FResolvedSizes.KnobEdgeThickness div 2,
  940. FResolvedSizes.MinRadius - FResolvedSizes.FrameBorderWidth - FResolvedSizes.KnobEdgeThickness div 2,
  941. FKnobSettings.EdgeColor,
  942. FResolvedSizes.KnobEdgeThickness,
  943. FKnobSettings.FillColor);
  944. end;
  945. ssShaded:
  946. begin // shaded knob
  947. FKnobBmp.FillEllipseLinearColorAntialias(Center.x, Center.y,
  948. FResolvedSizes.MinRadius - FResolvedSizes.FrameBorderWidth,
  949. FResolvedSizes.MinRadius - FResolvedSizes.FrameBorderWidth,
  950. FKnobSettings.EdgeColor,
  951. FKnobSettings.FillColor);
  952. FKnobBmp.EllipseAntialias(Center.x, Center.y,
  953. FResolvedSizes.MinRadius - FResolvedSizes.FrameBorderWidth - FResolvedSizes.KnobEdgeThickness div 2,
  954. FResolvedSizes.MinRadius - FResolvedSizes.FrameBorderWidth - FResolvedSizes.KnobEdgeThickness div 2,
  955. FKnobSettings.EdgeColor,
  956. FResolvedSizes.KnobEdgeThickness);
  957. end;
  958. ssPhong:
  959. begin // Phong shaded knob
  960. // compute spinner height map
  961. Map := TBGRABitmap.Create(xy, xy);
  962. for yb := 0 to xy - 1 do
  963. begin
  964. p := map.ScanLine[yb];
  965. for xb := 0 to xy - 1 do
  966. begin
  967. // compute vector between center and current pixel
  968. v := PointF(xb, yb) - Center;
  969. // scale down to unit circle (with 1 pixel margin for soft border)
  970. v.x := v.x / (xy / 2 + 1);
  971. v.y := v.y / (xy / 2 + 1);
  972. // compute squared distance with scalar product
  973. d2 := v {$if FPC_FULLVERSION < 30203}*{$ELSE}**{$ENDIF} v;
  974. // interpolate as quadratic curve and apply power function
  975. if d2 > 1 then
  976. h := 0
  977. else
  978. h := power(1 - d2, FKnobSettings.CurveExponent);
  979. p^ := MapHeightToBGRA(h, 255);
  980. Inc(p);
  981. end;
  982. end;
  983. mask := TBGRABitmap.Create(xy, xy, BGRABlack);
  984. // Adjust Size for frame AND knob edge. Note this is a FILL so no div 2
  985. Mask.FillEllipseAntialias(Center.x, Center.y,
  986. FResolvedSizes.MinRadius - FResolvedSizes.FrameBorderWidth - FResolvedSizes.KnobEdgeThickness +1,
  987. FResolvedSizes.MinRadius - FResolvedSizes.FrameBorderWidth - FResolvedSizes.KnobEdgeThickness +1,
  988. BGRAWhite);
  989. map.ApplyMask(mask);
  990. Mask.Free;
  991. KnobSettings.FPhong.Draw(FKnobBmp, Map, 30, 0, 0, FKnobSettings.FillColor);
  992. Map.Free;
  993. // Fill the edge now
  994. FKnobBmp.EllipseAntialias(Center.x, Center.y,
  995. FResolvedSizes.MinRadius - FResolvedSizes.FrameBorderWidth - FResolvedSizes.KnobEdgeThickness div 2,
  996. FResolvedSizes.MinRadius - FResolvedSizes.FrameBorderWidth - FResolvedSizes.KnobEdgeThickness div 2,
  997. FKnobSettings.EdgeColor,
  998. FResolvedSizes.KnobEdgeThickness);
  999. end;
  1000. end;
  1001. end;
  1002. procedure TCustomSuperSpinner.DrawCap;
  1003. var
  1004. Origin: TSSOrigin;
  1005. sizeWH : integer;
  1006. pCapEdge : integer;
  1007. xy: integer;
  1008. xyFDiv2: single;
  1009. h: single;
  1010. d2: single;
  1011. v: TPointF;
  1012. p: PBGRAPixel;
  1013. Center: TPointF;
  1014. yb: integer;
  1015. xb: integer;
  1016. mask: TBGRABitmap;
  1017. Map: TBGRABitmap;
  1018. begin
  1019. // skip drawing if nothing changed
  1020. if not FCapSettings.Dirty then
  1021. Exit;
  1022. FCapSettings.Dirty := False;
  1023. // drawing is the size of the cap, not of the entire knob!
  1024. sizeWH := (FResolvedSizes.CapRadius + FResolvedSizes.CapEdgeThickness) * 2 + 2;
  1025. Origin := Initializebitmap(FCapBmp, SizeWH, SizeWH);
  1026. // can skip drawing if nothing to draw, but still needed to init the bmp
  1027. if FCapSettings.Style = csNone then
  1028. Exit;
  1029. pCapEdge := FResolvedSizes.CapRadius + FResolvedSizes.CapEdgeThickness div 2;
  1030. case FCapSettings.Style of
  1031. csFlat:
  1032. begin
  1033. // Draw the flat cap, but make sure size is similar to the shaded below or will be odd
  1034. FCapBmp.EllipseAntialias(Origin.CenterPoint.x, Origin.CenterPoint.y,
  1035. pCapEdge,
  1036. pCapEdge,
  1037. FCapSettings.EdgeColor,
  1038. FResolvedSizes.CapEdgeThickness,
  1039. FCapSettings.FillColor);
  1040. end;
  1041. csShaded:
  1042. begin
  1043. // Regular shading
  1044. FCapBmp.FillEllipseLinearColorAntialias(Origin.CenterPoint.x, Origin.CenterPoint.y,
  1045. pCapEdge,
  1046. pCapEdge,
  1047. FCapSettings.FillColor,
  1048. FCapSettings.EdgeColor);
  1049. // draw edge since the shading is backwards ending on fill color not Edge
  1050. FCapBmp.EllipseAntialias(Origin.CenterPoint.x, Origin.CenterPoint.y,
  1051. pCapEdge,
  1052. pCapEdge,
  1053. FCapSettings.EdgeColor,
  1054. FResolvedSizes.CapEdgeThickness);
  1055. end;
  1056. csPhong:
  1057. begin
  1058. // Phong shaded cap
  1059. // Draw a flat radius around the cap if set, must be alpha 0 or will not
  1060. // be an outline. Draw First, fixes some issues with Phong drawing
  1061. xy := FResolvedSizes.CapRadius * 2 ;
  1062. xyFDiv2 := FResolvedSizes.CapRadius;
  1063. if xy = 0 then
  1064. Exit;
  1065. if FResolvedSizes.CapEdgeThickness > 0 then
  1066. FCapBmp.EllipseAntialias(Origin.CenterPoint.x, Origin.CenterPoint.y,
  1067. pCapEdge - 1, // suck in a little to make sure we are under it all
  1068. pCapEdge - 1,
  1069. FCapSettings.EdgeColor,
  1070. FResolvedSizes.CapEdgeThickness);
  1071. // compute knob height map
  1072. Center := PointF(xyFDiv2 , xyFDiv2);
  1073. Map := TBGRABitmap.Create(xy, xy);
  1074. for yb := 0 to xy - 1 do
  1075. begin
  1076. p := map.ScanLine[yb];
  1077. for xb := 0 to xy - 1 do
  1078. begin
  1079. // compute vector between center and current pixel
  1080. v := PointF(xb, yb) - Center;
  1081. // scale down to unit circle (with 1 pixel margin for soft border)
  1082. v.x := v.x / (xyFDiv2 + 1);
  1083. v.y := v.y / (xyFDiv2 + 1);
  1084. // compute squared distance with scalar product
  1085. d2 := v {$if FPC_FULLVERSION < 30203}*{$ELSE}**{$ENDIF} v;
  1086. // interpolate as quadratic curve and apply power function
  1087. if d2 > 1 then
  1088. h := 0
  1089. else
  1090. h := power(1 - d2, FCapSettings.CurveExponent);
  1091. p^ := MapHeightToBGRA(h, 255);
  1092. Inc(p);
  1093. end;
  1094. end;
  1095. // mask image round with and antialiased border
  1096. mask := TBGRABitmap.Create(xy, xy, BGRABlack);
  1097. // Shrink the size by one as the antialias gets chopped on the right edge
  1098. // if the image is full size. Looks nicer too.
  1099. Mask.FillEllipseAntialias(Center.x, Center.y, xyFDiv2 - 1, xyFDiv2 - 1 , BGRAWhite);
  1100. map.ApplyMask(mask);
  1101. Mask.Free;
  1102. // now draw it all
  1103. FCapSettings.FPhong.Draw(FCapBmp, Map, 30,
  1104. Origin.CenterPoint.x - xy div 2, Origin.CenterPoint.y - xy div 2,
  1105. FCapSettings.FillColor);
  1106. Map.Free;
  1107. end;
  1108. csOutline:
  1109. begin
  1110. // Just an outline
  1111. if FResolvedSizes.CapEdgeThickness > 0 then
  1112. FCapBmp.EllipseAntialias(Origin.CenterPoint.x, Origin.CenterPoint.y,
  1113. pCapEdge,
  1114. pCapEdge,
  1115. FCapSettings.EdgeColor,
  1116. FResolvedSizes.CapEdgeThickness);
  1117. end;
  1118. end;
  1119. end;
  1120. procedure TCustomSuperSpinner.DrawPosition;
  1121. var
  1122. Center, Pos: TPointF;
  1123. PosColor: TBGRAPixel;
  1124. PosLen, x,y,xt,yt: single;
  1125. i, n : integer;
  1126. begin
  1127. // Note this is mostly always be dirty, if the knob moves or a setting
  1128. // changes it's dirty so always, no need to currently check dirty flag
  1129. // Do some magic since we can adjust opacity with an additional property
  1130. // This sometimes draws different color in design vs. runtime BGRA issue??
  1131. PosColor := ColorToBGRA(ColorToRGB(FPositionSettings.FillColor), FPositionSettings.Opacity);
  1132. // set up positions for position indicator, use ResolvedSizes!
  1133. // Pos.X and Pos.Y should be both based on the minimum sized dimension
  1134. Center := PointF(ClientWidth / 2, ClientHeight / 2);
  1135. Pos.X := Cos(FAngularPos) * (FResolvedSizes.MinWH / 2);
  1136. Pos.Y := -Sin(FAngularPos) * (FResolvedSizes.MinWH / 2);
  1137. PosLen := VectLen(Pos);
  1138. Pos := Pos * ((PosLen - FResolvedSizes.PositionMargin - FResolvedSizes.PositionRadius) / PosLen);
  1139. Pos := Center + Pos;
  1140. // Size and Clear bitmap to transparent, keep full size bitmap
  1141. FPositionBmp.SetSize(ClientWidth, ClientHeight);
  1142. FPositionBmp.Fill(BGRA(0, 0, 0, 0));
  1143. case PositionSettings.Style of
  1144. psFilledCircle:
  1145. begin
  1146. FPositionBmp.FillEllipseAntialias(Pos.X, Pos.Y,
  1147. FResolvedSizes.PositionRadius, FResolvedSizes.PositionRadius,
  1148. PosColor);
  1149. end;
  1150. psHollowCircle:
  1151. begin
  1152. FPositionBmp.EllipseAntialias(Pos.X, Pos.Y,
  1153. FResolvedSizes.PositionRadius, FResolvedSizes.PositionRadius,
  1154. PosColor, FPositionSettings.LineWidth);
  1155. end;
  1156. psShaded:
  1157. begin
  1158. // Regular shading similar to Cap
  1159. FPositionBmp.FillEllipseLinearColorAntialias(Pos.X, Pos.Y,
  1160. FResolvedSizes.PositionRadius, FResolvedSizes.PositionRadius,
  1161. Poscolor,
  1162. FPositionSettings.EdgeColor);
  1163. end;
  1164. psIndentCircle:
  1165. begin
  1166. // hack to give some indented depth, Doing colors
  1167. // backwards to make it look nicer.
  1168. FPositionBmp.FillEllipseLinearColorAntialias(Pos.X, Pos.Y,
  1169. FResolvedSizes.PositionRadius, FResolvedSizes.PositionRadius,
  1170. PosColor, FKnobSettings.EdgeColor);
  1171. FPositionBmp.EllipseAntialias(Pos.X, Pos.Y,
  1172. FResolvedSizes.PositionRadius, FResolvedSizes.PositionRadius,
  1173. PosColor, 1);
  1174. end;
  1175. psLines:
  1176. begin
  1177. FPositionBmp.LineCap := pecRound; // ensure correct cap mode
  1178. n := FPositionSettings.LineCount;
  1179. // Skip if number of lines is 0
  1180. if n > 0 then
  1181. for i := 0 to n - 1 do
  1182. begin
  1183. // Center Point
  1184. x := Center.x - FResolvedSizes.PositionCenterMargin * cos((i * 360 / n) * Pi / 180 - FAngularPos - PI);
  1185. y := Center.y - FResolvedSizes.PositionCenterMargin * sin((i * 360 / n) * Pi / 180 - FAngularPos - PI);
  1186. // Draw to Outer Point
  1187. xt := Center.x - (FResolvedSizes.MinRadius - FResolvedSizes.PositionMargin) * cos((i * 360 / n) * Pi / 180 - FAngularPos - PI);
  1188. yt := Center.y - (FResolvedSizes.MinRadius - FResolvedSizes.PositionMargin)* sin((i * 360 / n) * Pi / 180 - FAngularPos - PI);
  1189. FPositionBmp.DrawLineAntialias(x, y, xt, yt, PosColor, FResolvedSizes.PositionLineWidth);
  1190. end;
  1191. end;
  1192. end;
  1193. // Draw outer circle border if desired, only for circle types
  1194. if (FPositionSettings.EdgeThickness > 0) and (FPositionSettings.Style <> psLines)
  1195. and (FPositionSettings.Style <> psNone) then
  1196. begin
  1197. FPositionBmp.EllipseAntialias(Pos.X, Pos.Y,
  1198. FResolvedSizes.PositionRadius + FPositionSettings.EdgeThickness div 2,
  1199. FResolvedSizes.PositionRadius + FPositionSettings.EdgeThickness div 2,
  1200. FPositionSettings.EdgeColor, FPositionSettings.EdgeThickness);
  1201. end;
  1202. end;
  1203. {$IFDEF FPC}
  1204. procedure TCustomSuperSpinner.SaveToFile(AFileName: string);
  1205. var
  1206. AStream: TMemoryStream;
  1207. begin
  1208. AStream := TMemoryStream.Create;
  1209. try
  1210. WriteComponentAsTextToStream(AStream, Self);
  1211. AStream.SaveToFile(AFileName);
  1212. finally
  1213. AStream.Free;
  1214. end;
  1215. end;
  1216. procedure TCustomSuperSpinner.LoadFromFile(AFileName: string);
  1217. var
  1218. AStream: TMemoryStream;
  1219. begin
  1220. AStream := TMemoryStream.Create;
  1221. try
  1222. AStream.LoadFromFile(AFileName);
  1223. ReadComponentFromTextStream(AStream, TComponent(Self), OnFindClass);
  1224. finally
  1225. AStream.Free;
  1226. end;
  1227. end;
  1228. {$ENDIF}
  1229. procedure TCustomSuperSpinner.OnFindClass(Reader: TReader; const AClassName: string;
  1230. var ComponentClass: TComponentClass);
  1231. begin
  1232. if CompareText(AClassName, 'TCustomSuperSpinner') = 0 then
  1233. ComponentClass := TCustomSuperSpinner;
  1234. end;
  1235. function TCustomSuperSpinner.DoMouseWheel(Shift: TShiftState; WheelDelta: integer;
  1236. MousePos: TPoint): boolean;
  1237. begin
  1238. Result := inherited DoMouseWheel(Shift, WheelDelta, MousePos);
  1239. MouseWheelPos(Shift, WheelDelta);
  1240. end;
  1241. procedure TCustomSuperSpinner.MouseWheelPos(Shift: TShiftState; WheelDelta: integer);
  1242. var
  1243. newValue: single;
  1244. begin
  1245. if FLocked then
  1246. Exit;
  1247. // WheelSpeed is a Base Value and a factor to slow or speed up the wheel affect.
  1248. // FWheelSpeed = 0 then no wheel, 1 slowest movement, 255 fastest movement
  1249. // Wheel speed still just does one step no matter what the wheel angle is set to
  1250. // so the WheelSpeed just really adjust the look of how fast the knob spins
  1251. if FWheelSpeed > 0 then
  1252. begin
  1253. // WheelDelta should just catch direction, negative or positive
  1254. // not sure if 0 is ever possible????
  1255. if WheelDelta >= 0 then
  1256. newValue := -1.0
  1257. else
  1258. newValue := 1.0;
  1259. // Must invalidate both as we don't know the current direction it's moving
  1260. // so one will get reset, the other will trigger, so always works.
  1261. // This is used in UpdateAngularPos to help with direction changes
  1262. FCWSkipCounter := FSpinnerResolutionCount - 1;
  1263. FCCWSkipCounter := FCWSkipCounter;
  1264. // Scale the Wheel rate so 1-255 will give good dynamic range of really slow to really fast
  1265. // TIP : To make the mouse movement sorta' match the Resolution you can change
  1266. // the Wheel speed to make it more closely match if resolution is not the highest
  1267. UpdateAngularPos(Shift, FAngularPos + WHEEL_SPEED_FACTOR * newValue * FWheelSpeed);
  1268. end; // wheel speed enabled
  1269. end;
  1270. end.