{ @abstract(This unit contains the TKHexEditor component and all supporting classes)
  @author(Tomas Krysl (tk@tkweb.eu))
  @created(12 Oct 2005)
  @lastmod(20 Jun 2010)

  This unit provides a powerfull hexadecimal editor component @link(TKHexEditor)
  with following major features:
  <UL>
  <LI><I>advanced editing capabilities</I></LI>
  <LI><I>advanced rendering styles</I></LI>
  <LI><I>clipboard operations</I></LI>
  <LI><I>virtually unlimited undo/redo operations</I></LI>
  <LI><I>key mapping functionality</I></LI>
  <LI><I>fast search/replace function</I></LI>
  <LI><I>print/preview function</I></LI>
  </UL>

  Copyright © 2006 Tomas Krysl (tk@@tkweb.eu)<BR><BR>

  <B>License:</B><BR>
  This code is distributed as a freeware. You are free to use it as part
  of your application for any purpose including freeware, commercial and
  shareware applications. The origin of this source code must not be
  misrepresented; you must not claim your authorship. You may modify this code
  solely for your own purpose. Please feel free to contact the author if you
  think your changes might be useful for other users. You may distribute only
  the original package. The author accepts no liability for any damage
  that may result from using this code. }

unit KHexEditor;

{$include kcontrols.inc}
{$WEAKPACKAGEUNIT ON}

interface

uses
{$IFDEF FPC}
  LCLType, LCLIntf, LMessages, LCLProc, LResources,
{$ELSE}
  Windows, Messages,
{$ENDIF}
  SysUtils, Classes, Graphics, Controls,
  ExtCtrls, StdCtrls, Forms, KFunctions, KControls, KEditCommon;

resourcestring
  { @exclude }
  sAddressText = 'Address area text';
  { @exclude }
  sAddressBkGnd = 'Address area background';
  { @exclude }
  sBkGnd = 'Editor background';
  { @exclude }
  sDigitTextEven = 'Digit area even column';
  { @exclude }
  sDigitTextOdd = 'Digit area odd column';
  { @exclude }
  sDigitBkgnd = 'Digit area background';
  { @exclude }
  sHorzLines = 'Horizontal lines';
  { @exclude }
  sInactiveCaretBkGnd = 'Inactive caret background';
  { @exclude }
  sInactiveCaretSelBkGnd = 'Selected inactive caret background';
  { @exclude }
  sInactiveCaretSelText = 'Selected inactive caret text';
  { @exclude }
  sInactiveCaretText = 'Inactive caret text';
  { @exclude }
  sLinesHighLight = 'Lines highlight';
  { @exclude }
  sSelBkGnd = 'Selection background';
  { @exclude }
  sSelBkGndFocused = 'Focused selection background';
  { @exclude }
  sSelText = 'Selection text';
  { @exclude }
  sSelTextFocused = 'Focused selection text';
  { @exclude }
  sSeparators = 'Area separating lines';
  { @exclude }
  sTextText = 'Text area text';
  { @exclude }
  sTextBkGnd = 'Text area background';
  { @exclude }
  sVertLines = 'Vertical lines';

type
  { Declares possible values for the @link(TKCustomHexEditor.AddressMode) property }
  TKHexEditorAddressMode = (
    { Address will be shown in decimal format }
    eamDec,
    { Address will be shown in hexadecimal format }
    eamHex
  );

  { Declares possible values e.g. for the @link(TKCustomHexEditor.EditArea) property }
  TKHexEditorArea = (
    { No area is selected, e.g. when clicked outside of visible text }
    eaNone,
    { Address area selected/used }
    eaAddress,
    { Digits area selected/used }
    eaDigits,
    { Text area selected/used }
    eaText
  );

  { @abstract(Contains dimensions of all areas in characters)
    <UL>
    <LH>Members:</LH>
    <LI><I>Address</I> - address area width</LI>
    <LI><I>AddressOut</I> - address area leadout</LI>
    <LI><I>Digits</I> - digits area width</LI>
    <LI><I>DigitsIn</I> - digits area leadin</LI>
    <LI><I>DigitsOut</I> - digits area leadout</LI>
    <LI><I>Text</I> - text area width</LI>
    <LI><I>TextIn</I> - text area leadin</LI>
    <LI><I>TotalHorz</I> - total width of all defined areas</LI>
    <LI><I>TotalVert</I> - total number of lines</LI>
    </UL>
  }
  TKHexEditorAreaDimensions = record
    Address,
    AddressOut,
    Digits,
    DigitsIn,
    DigitsOut,
    Text,
    TextIn,
    TotalHorz,
    TotalVert: Integer;
  end;

  { Declares possible indexes e.g. for the @link(TKHexEditorColors.Color) property. }
  TKHexEditorColorIndex = Integer;

  { @abstract(Declares @link(TKHexEditorColors) color item description)
    <UL>
    <LH>Members:</LH>
    <LI><I>Def</I> - default color value</LI>
    <LI><I>Name</I> - color name (can be localized)</LI>
    </UL>
  }
  TKHexEditorColorSpec = record
    Def: TColor;
    Name: string;
  end;

  { Declares possible values for the @link(TKCustomHexEditor.DisabledDrawStyle) property }
  TKHexEditorDisabledDrawStyle = (
    { The lines will be painted with brighter colors when editor is disabled }
    eddBright,
    { The lines will be painted with gray text and white background when editor is disabled }
    eddGrayed,
    { The lines will be painted normally when editor is disabled }
    eddNormal
  );

  { Declares drawing styles - possible values for the @link(TKCustomHexEditor.DrawStyles) property }
  TKHexEditorDrawStyle = (
    { Show adress area }
    edAddress,
    { Show digits area }
    edDigits,
    { Show text area }
    edText,
    { Show horizontal leading lines }
    edHorzLines,
    { Show caret position when editor is inactive (has no input focus) }
    edInactiveCaret,
    { Show vertical area separating lines }
    edSeparators,
    { Show vertical leading lines (digits area only) }
    edVertLines,
    { @link(TKHexEditorColors.BkGnd) is used for all areas if included }
    edSingleBkGnd
  );

  { Drawing styles can be arbitrary combined }
  TKHexEditorDrawStyles = set of TKHexEditorDrawStyle;

  { @abstract(Declares the paint data structure for the @link(TKCustomHexEditor.PaintLines) method)
    <UL>
    <LH>Members:</LH>
    <LI><I>Canvas</I> - destination canvas</LI>
    <LI><I>PainRect</I> - bounding rectangle for painted lines (no clipping necessary,
    this is performed by window/page client area)</LI>
    <LI><I>TopLine</I> - first line painted (vertical scroll offset)</LI>
    <LI><I>BottomLine</I> - last line painted</LI>
    <LI><I>LeftChar</I> - first character painted (horizontal scroll offset)</LI>
    <LI><I>CharWidth</I> - character width in pixels for supplied canvas</LI>
    <LI><I>CharHeight</I> - character height in pixels for supplied canvas</LI>
    <LI><I>CharSpacing</I> - inter-character spacing in pixels for supplied canvas</LI>
    <LI><I>Printing</I> - determines whether normal painting or page printing should be performed</LI>
    <LI><I>PaintAll</I> - when Printing is True, specifies whether all data or selection only
    should be painted, this applies only to the first and/or last painted line</LI>
    <LI><I>PaintColors</I> - when Printing is True, specifies whether to paint with colors or grayscale</LI>
    <LI><I>PaintSelection</I> - when Printing is True, specifies whether to indicate the selection</LI>
    </UL>
  }
  TKHexEditorPaintData = record
    Canvas: TCanvas;
    PaintRect: TRect;
    TopLine,
    BottomLine,
    LeftChar,
    CharWidth,
    CharHeight,
    CharSpacing: Integer;
    Printing,
    PaintAll,
    PaintColors,
    PaintSelection,
    CaretShown: Boolean;
  end;

  { @abstract(Declares the selection structure)
    <UL>
    <LH>Members:</LH>
    <LI><I>Index</I> - byte index</LI>
    <LI><I>Digit</I> - digit index</LI>
    </UL>
  }
  TKHexEditorSelection = record
    Index: Integer;
    Digit: Integer;
  end;

  { @abstract(Declares the structure for the @link(TKCustomHexEditor.SelText) property)
    <UL>
    <LH>Members:</LH>
    <LI><I>AsBinaryRaw</I> - selected data as binary characters not mapped</LI>
    <LI><I>AsBinaryMapped</I> - selected data as binary characters mapped</LI>
    <LI><I>AsDigits</I> - selected data as hexadecimal digits</LI>
    <LI><I>AsDigitsByteAligned</I> - selected data as hexadecimal digits
    without regarding cross-byte selections</LI>
    </UL>
  }
  TKHexEditorSelText = record
    AsBinaryRaw,
    AsBinaryMapped,
    AsDigits,
    AsDigitsByteAligned: AnsiString;
  end;

  { Declares hex editor states - possible values for the @link(TKCustomHexEditor.States) property
    (protected) }
  TKHexEditorState = (
    { Caret is visible }
    elCaretVisible,
    { Caret is being updated }
    elCaretUpdate,
    { Ignore following WM_CHAR message }
    elIgnoreNextChar,
    { Buffer modified }
    elModified,
    { Mouse captured }
    elMouseCapture,
    { Overwrite mode active }
    elOverwrite,
    { Read only editor }
    elReadOnly
  );

  { Hex editor states can be arbitrary combined }
  TKHexEditorStates = set of TKHexEditorState;

  { @abstract(Declares the color description structure returned by @link(TKHexEditorColors.ColorData) property)
    <UL>
    <LH>Members:</LH>
    <LI><I>Index</I> - color index</LI>
    <LI><I>Color</I> - current color value</LI>
    <LI><I>Default</I> - default color value</LI>
    <LI><I>Name</I> - color name</LI>
    </UL>
  }
  TKHexEditorColorData = record
    Index: TKHexEditorColorIndex;
    Color: TColor;
    Default: TColor;
    Name: string;
  end;

  { Declares possible values for the @link(TKHexEditorColors.ColorScheme) property }
  TKHexEditorColorScheme = (
    { GetColor returns normal color currently defined for each item }
    ecsNormal,
    { GetColor returns gray for text and line colors and white for background colors }
    ecsGrayed,
    { GetColor returns brighter version of normal color }
    ecsBright,
    { GetColor returns grayscaled color versions }
    ecsGrayScale
  );

const
  { Minimum for the @link(TKCustomHexEditor.AddressSize) property }
  cAddressSizeMin = 2;
  { Maximum for the @link(TKCustomHexEditor.AddressSize) property }
  cAddressSizeMax = 10;
  { Default value for the @link(TKCustomHexEditor.AddressSize) property }
  cAddressSizeDef = 8;

  { Minimum for the @link(TKCustomHexEditor.AreaSpacing) property }
  cAreaSpacingMin = 1;
  { Maximum for the @link(TKCustomHexEditor.AreaSpacing) property }
  cAreaSpacingMax = 20;
  { Default value for the @link(TKCustomHexEditor.AreaSpacing) property }
  cAreaSpacingDef = 1;

  { Minimum for the @link(TKCustomHexEditor.CharSpacing) property }
  cCharSpacingMin = 0;
  { Maximum for the @link(TKCustomHexEditor.CharSpacing) property }
  cCharSpacingMax = 100;
  { Default value for the @link(TKCustomHexEditor.CharSpacing) property }
  cCharSpacingDef = 0;

  { Minimum for the @link(TKCustomHexEditor.DigitGrouping) property }
  cDigitGroupingMin = 1;
  { Maximum for the @link(TKCustomHexEditor.DigitGrouping) property }
  cDigitGroupingMax = 8;
  { Default value for the @link(TKCustomHexEditor.DigitGrouping) property }
  cDigitGroupingDef = 2;

  { Minimum for the @link(TKCustomHexEditor.LineHeightPercent) property }
  cLineHeightPercentMin = 10;
  { Maximum for the @link(TKCustomHexEditor.LineHeightPercent) property }
  cLineHeightPercentMax = 1000;
  { Default value for the @link(TKCustomHexEditor.LineHeightPercent) property }
  cLineHeightPercentDef = 130;

  { Minimum for the @link(TKCustomHexEditor.UndoLimit) property }
  cUndoLimitMin = 100;
  { Maximum for the @link(TKCustomHexEditor.UndoLimit) property }
  cUndoLimitMax = 10000;
  { Default value for the @link(TKCustomHexEditor.UndoLimit) property }
  cUndoLimitDef = 1000;

  { Minimum for the @link(TKCustomHexEditor.LineSize) property }
  cLineSizeMin = 1;
  { Maximum for the @link(TKCustomHexEditor.LineSize) property }
  cLineSizeMax = 128;
  { Default value for the @link(TKCustomHexEditor.LineSize) property }
  cLineSizeDef = 16;

  { Minimum for the @link(TKCustomHexEditor.ScrollSpeed) property }
  cScrollSpeedMin = 50;
  { Maximum for the @link(TKCustomHexEditor.ScrollSpeed) property }
  cScrollSpeedMax = 1000;
  { Default value for the @link(TKCustomHexEditor.ScrollSpeed) property }
  cScrollSpeedDef = 100;

  { Minimum for the @link(TKHexEditor.Font).Size property }
  cFontSizeMin = 8;
  { Maximum for the @link(TKHexEditor.Font).Size property }
  cFontSizeMax = 100;
  { Default value for the @link(TKHexEditor.Font).Size property }
  cFontSizeDef = 11;

  { Default value for the @link(TKHexEditorColors.AddressText) color property }
  cAddressTextDef = clWindowText;
  { Default value for the @link(TKHexEditorColors.AddressBkGnd) color property }
  cAddressBkgndDef = clWindow;
  { Default value for the @link(TKHexEditorColors.BkGnd) color property }
  cBkGndDef = clWindow;
  { Default value for the @link(TKHexEditorColors.DigitTextEven) color property }
  cDigitTextEvenDef = clMaroon;
  { Default value for the @link(TKHexEditorColors.DigitTextOdd) color property }
  cDigitTextOddDef = clRed;
  { Default value for the @link(TKHexEditorColors.DigitBkGnd) color property }
  cDigitBkGndDef = clWindow;
  { Default value for the @link(TKHexEditorColors.HorzLines) color property }
  cHorzLinesDef = clWindowText;
  { Default value for the @link(TKHexEditorColors.InactiveCaretBkGnd) color property }
  cInactiveCaretBkGndDef = clBlack;
  { Default value for the @link(TKHexEditorColors.InactiveCaretSelBkGnd) color property }
  cInactiveCaretSelBkGndDef = clBlack;
  { Default value for the @link(TKHexEditorColors.InactiveCaretSelText) color property }
  cInactiveCaretSelTextDef = clYellow;
  { Default value for the @link(TKHexEditorColors.InactiveCaretText) color property }
  cInactiveCaretTextDef = clYellow;
  { Default value for the @link(TKHexEditorColors.LinesHighLight) color property }
  cLinesHighLightDef = clHighLightText;
  { Default value for the @link(TKHexEditorColors.SelBkGnd) color property }
  cSelBkGndDef = clGrayText;
  { Default value for the @link(TKHexEditorColors.SelBkGndFocused) color property }
  cSelBkGndFocusedDef = clHighlight;
  { Default value for the @link(TKHexEditorColors.SelText) color property }
  cSelTextDef = clHighlightText;
  { Default value for the @link(TKHexEditorColors.SelTextFocused) color property }
  cSelTextFocusedDef = clHighlightText;
  { Default value for the @link(TKHexEditorColors.Separators) color property }
  cSeparatorsDef = clWindowText;
  { Default value for the @link(TKHexEditorColors.TextText) color property }
  cTextTextDef = clWindowText;
  { Default value for the @link(TKHexEditorColors.TextBkgnd) color property }
  cTextBkgndDef = clWindow;
  { Default value for the @link(TKHexEditorColors.VertLines) color property }
  cVertLinesDef = clWindowText;

  { Index for the @link(TKHexEditorColors.AddressText) color property }
  ciAddressText = TKHexEditorColorIndex(0);
  { Index for the @link(TKHexEditorColors.AddressBkGnd) color property }
  ciAddressBkGnd = TKHexEditorColorIndex(1);
  { Index for the @link(TKHexEditorColors.BkGnd) color property }
  ciBkGnd = TKHexEditorColorIndex(2);
  { Index for the @link(TKHexEditorColors.DigitTextEven) color property }
  ciDigitTextEven = TKHexEditorColorIndex(3);
  { Index for the @link(TKHexEditorColors.DigitTextOdd) color property }
  ciDigitTextOdd = TKHexEditorColorIndex(4);
  { Index for the @link(TKHexEditorColors.DigitBkGnd) color property }
  ciDigitBkGnd = TKHexEditorColorIndex(5);
  { Index for the @link(TKHexEditorColors.HorzLines) color property }
  ciHorzLines = TKHexEditorColorIndex(6);
  { Index for the @link(TKHexEditorColors.InactiveCaretBkGnd) color property }
  ciInactiveCaretBkGnd = TKHexEditorColorIndex(7);
  { Index for the @link(TKHexEditorColors.InactiveCaretSelBkGnd) color property }
  ciInactiveCaretSelBkGnd = TKHexEditorColorIndex(8);
  { Index for the @link(TKHexEditorColors.InactiveCaretSelText) color property }
  ciInactiveCaretSelText = TKHexEditorColorIndex(9);
  { Index for the @link(TKHexEditorColors.InactiveCaretText) color property }
  ciInactiveCaretText = TKHexEditorColorIndex(10);
  { Index for the @link(TKHexEditorColors.LinesHighLight) color property }
  ciLinesHighLight = TKHexEditorColorIndex(11);
  { Index for the @link(TKHexEditorColors.SelBkGnd) color property }
  ciSelBkGnd = TKHexEditorColorIndex(12);
  { Index for the @link(TKHexEditorColors.SelBkGndFocused) color property }
  ciSelBkGndFocused = TKHexEditorColorIndex(13);
  { Index for the @link(TKHexEditorColors.SelText) color property }
  ciSelText = TKHexEditorColorIndex(14);
  { Index for the @link(TKHexEditorColors.SelTextFocused) color property }
  ciSelTextFocused = TKHexEditorColorIndex(15);
  { Index for the @link(TKHexEditorColors.Separators) color property }
  ciSeparators = TKHexEditorColorIndex(16);
  { Index for the @link(TKHexEditorColors.TextText) color property }
  ciTextText = TKHexEditorColorIndex(17);
  { Index for the @link(TKHexEditorColors.TextBkgnd) color property }
  ciTextBkGnd = TKHexEditorColorIndex(18);
  { Index for the @link(TKHexEditorColors.VertLines) color property }
  ciVertLines = TKHexEditorColorIndex(19);
  { Maximum color array index }
  ciHexEditorColorsMax = ciVertLines;

  { Default value for the @link(TKCustomHexEditor.AddressMode) property }
  cAddressModeDef = eamHex;

  { Default value for the @link(TKCustomHexEditor.Addressoffset) property }
  cAddressOffsetDef = 0;

  { Default value for the @link(TKCustomHexEditor.DisabledDrawStyle) property }
  cDisabledDrawStyleDef = eddBright;

  { Default value for the  @link(TKCustomHexEditor.DrawStyles) property }
  cDrawStylesDef = [edAddress, edDigits, edText, edInactiveCaret, edSeparators];

  { Default value for the @link(TKCustomHexEditor.AddressPrefix) property }
  cAddressPrefixDef = '0x';

  { Default value for the @link(TKHexEditor.Font).Name property }
  cFontNameDef = {$IFDEF MSWINDOWS}'Courier New'{$ELSE}'Courier'{$ENDIF};

  { Default value for the @link(TKHexEditor.Font).Style property }
  cFontStyleDef = [fsBold];

  { Declares the Index member of the @link(TKHexEditorSelection) record invalid}
  cInvalidIndex = -1;

  { Default value for the @link(TKCustomHexEditor.AddressCursor) property }
  cAddressCursorDef = crHandPoint;

  { Default value for the @link(TKHexEditor.Height) property }
  cHeight = 300;

  { Default value for the @link(TKHexEditor.Width) property }
  cWidth = 400;

type
  TKCustomHexEditor = class;

  { @abstract(Container for all colors used by @link(TKCustomHexEditor) class)
    This container allows to group many colors into one item in object inspector.
    Colors are accessible via published properties or several public Color*
    properties.
  }
  TKHexEditorColors = class(TPersistent)
  private
    FOwner: TKCustomHexEditor;
    FBrightColors: TKColorArray;
    FColors: TKColorArray;
    FColorScheme: TKHexEditorColorScheme;
    FSingleBkGnd: Boolean;
    function GetColor(Index: TKHexEditorColorIndex): TColor;
    function GetColorData(Index: TKHexEditorColorIndex): TKHexEditorColorData;
    function GetColorEx(Index: TKHexEditorColorIndex): TColor;
    function GetColorName(Index: TKHexEditorColorIndex): string;
    procedure SetColor(Index: TKHexEditorColorIndex; Value: TColor);
    procedure SetColorEx(Index: TKHexEditorColorIndex; Value: TColor);
    procedure SetColors(const Value: TKColorArray);
  public
    { Performs necessary initializations }
    constructor Create(AOwner: TKCustomHexEditor);
    { Takes property values from another TKHexEditorColors class }
    procedure Assign(Source: TPersistent); override;
    { Clears cached brighter colors }
    procedure ClearBrightColors;
    { Initializes the color array. }
    procedure Initialize; virtual;
    { Specifies color scheme for reading of published properties - see GetColor in source code}
    property ColorScheme: TKHexEditorColorScheme read FColorScheme write FColorScheme;
    { Returns always normal color - regardless of the ColorScheme setting }
    property Color[Index: TKHexEditorColorIndex]: TColor read GetColorEx write SetColorEx;
    { Returns always a complete color description }
    property ColorData[Index: TKHexEditorColorIndex]: TKHexEditorColorData read GetColorData;
    { Returns (localizable) color name }
    property ColorName[Index: TKHexEditorColorIndex]: string read GetColorName;
    { Returns array of normal colors }
    property Colors: TKColorArray read FColors write SetColors;
    { @link(TKHexEditorColors.BkGnd) is used for all areas if True - @link(edSingleBkGnd) forward }
    property SingleBkGnd: Boolean read FSingleBkGnd write FSingleBkGnd;
  published
    { Address area text color }
    property AddressText: TColor index ciAddressText read GetColor write SetColor default cAddressTextDef;
    { Address area background color }
    property AddressBkGnd: TColor index ciAddressBkgnd read GetColor write SetColor default cAddressBkGndDef;
    { Hex editor client area background }
    property BkGnd: TColor index ciBkGnd read GetColor write SetColor default cBkGndDef;
    { Digits area text color - even digit group }
    property DigitTextEven: TColor index ciDigitTextEven read GetColor write SetColor default cDigitTextEvenDef;
    { Digits area text color - odd digit group }
    property DigitTextOdd: TColor index ciDigitTextOdd read GetColor write SetColor default cDigitTextOddDef;
    { Digits area background color }
    property DigitBkGnd: TColor index ciDigitBkGnd read GetColor write SetColor default cDigitBkGndDef;
    { Color of the horizontal leading lines }
    property HorzLines: TColor index ciHorzLines read GetColor write SetColor default cHorzLinesDef;
    { Inactive (hex editor without focus) caret background color - caret mark is not part of a selection }
    property InactiveCaretBkGnd: TColor index ciInactiveCaretBkGnd read GetColor write SetColor default cInactiveCaretBkGndDef;
    { Inactive (hex editor without focus) caret background color - caret mark is part of a selection }
    property InactiveCaretSelBkGnd: TColor index ciInactiveCaretSelBkGnd read GetColor write SetColor default cInactiveCaretSelBkGndDef;
    { Inactive (hex editor without focus) caret text color - caret mark is part of a selection }
    property InactiveCaretSelText: TColor index ciInactiveCaretSelText read GetColor write SetColor default cInactiveCaretSelTextDef;
    { Inactive (hex editor without focus) caret text color - caret mark is not part of a selection }
    property InactiveCaretText: TColor index ciInactiveCaretText read GetColor write SetColor default cInactiveCaretTextDef;
    { Color of horizontal leading lines involved into a selection }
    property LinesHighLight: TColor index ciLinesHighLight read GetColor write SetColor default cLinesHighLightDef;
    { Selection background - inactive edit area }
    property SelBkGnd: TColor index ciSelBkGnd read GetColor write SetColor default cSelBkGndDef;
    { Selection background - active edit area }
    property SelBkGndFocused: TColor index ciSelBkGndFocused read GetColor write SetColor default cSelBkGndFocusedDef;
    { Selection text - inactive edit area }
    property SelText: TColor index ciSelText read GetColor write SetColor default cSelTextDef;
    { Selection text - active edit area }
    property SelTextFocused: TColor index ciSelTextFocused read GetColor write SetColor default cSelTextFocusedDef;
    { Color of the vertical area separating lines }
    property Separators: TColor index ciSeparators read GetColor write SetColor default cSeparatorsDef;
    { Text area text color }
    property TextText: TColor index ciTextText read GetColor write SetColor default cTextTextDef;
    { Text area background color }
    property TextBkgnd: TColor index ciTextBkgnd read GetColor write SetColor default cTextBkGndDef;
    { Color of the vertical leading lines }
    property VertLines: TColor index ciVertLines read GetColor write SetColor default cVertLinesDef;
  end;

  { Declares possible values for the ItemReason member of the @link(TKHexEditorChangeItem) structure }
  TKHexEditorChangeReason = (
    { Save caret position only }
    crCaretPos,
    { Save inserted character to be able to delete it }
    crDeleteChar,
    { Save inserted hexadecimal digits to be able to delete them }
    crDeleteDigits,
    { Save inserted binary string to be able to delete it }
    crDeleteString,
    { Save deleted character to be able to insert it }
    crInsertChar,
    { Save deleted hexadecimal digits to be able to insert them }
    crInsertDigits,
    { Save deleted binary string to be able to insert it }
    crInsertString
    );

  { @abstract(Declares @link(TKHexEditorChangeList.OnChange) event handler)
    <UL>
    <LH>Parameters:</LH>
    <LI><I>Sender</I> - identifies the event caller</LI>
    <LI><I>ItemReason</I> - specifies the undo/redo reason</LI>
    </UL>
  }
  TKHexEditorUndoChangeEvent = procedure(Sender: TObject;
    ItemReason: TKHexEditorChangeReason) of object;

  { @abstract(Declares the undo/redo item description structure used by the @link(TKHexEditorChangeList) class)
    <UL>
    <LH>Members:</LH>
    <LI><I>Data</I> - characters (binary or digit string) needed to execute this item</LI>
    <LI><I>EditArea</I> - active edit area at the time this item was recorded</LI>
    <LI><I>Group</I> - identifies the undo/redo group. Some editor modifications
      produce a sequence of 2 or more undo items. This sequence is called undo/redo
      group and is always interpreted as a single undo/redo item. Moreover,
      if there is eoGroupUndo in @link(TKCustomHexEditor.Options),
      a single ecUndo or ecRedo command manipulates all following undo groups
      of the same kind (reason) as if they were a single undo/redo item. </LI>
    <LI><I>GroupReason</I> - reason (kind) of this undo group</LI>
    <LI><I>ItemReason</I> - reason (kind) of this item</LI>
    <LI><I>SelEnd</I> - end of the selection at the time this item was recorded</LI>
    <LI><I>SelStart</I> - start of the selection at the time this item was recorded</LI>
    </UL>
  }
  TKHexEditorChangeItem = record
    Data: AnsiString;
    EditArea: TKHexEditorArea;
    Group: Cardinal;
    GroupReason: TKHexEditorChangeReason;
    Inserted: Boolean;
    ItemReason: TKHexEditorChangeReason;
    SelEnd: TKHexEditorSelection;
    SelStart: TKHexEditorSelection;
  end;

  { Pointer to @link(TKHexEditorChangeItem) }
  PKHexEditorChangeItem = ^TKHexEditorChangeItem;

  { @abstract(Change (undo/redo item) list manager) }
  TKHexEditorChangeList = class(TList)
  private
    FEditor: TKCustomHexEditor;
    FGroup: Cardinal;
    FGroupUseLock: Integer;
    FGroupReason: TKHexEditorChangeReason;
    FIndex: Integer;
    FModifiedIndex: Integer;
    FLimit: Integer;
    FRedoList: TKHexEditorChangeList;
    FOnChange: TKHexEditorUndoChangeEvent;
    function GetModified: Boolean;
    procedure SetLimit(Value: Integer);
    procedure SetModified(Value: Boolean);
  protected
    { Redefined to properly destroy the items }
    procedure Notify(Ptr: Pointer; Action: TListNotification); override;
  public
    { Performs necessary initializations
      <UL>
      <LH>Parameters:</LH>
      <LI><I>AEditor</I> - identifies the undo/redo list owner</LI>
      <LI><I>RedoList</I> - when this instance is used as undo list, specify
      a redo list to allow clear it at each valid AddChange call</LI>
      </UL>}
    constructor Create(AEditor: TKCustomHexEditor; RedoList: TKHexEditorChangeList);
    { Inserts a undo/redo item
      <UL>
      <LH>Parameters:</LH>
      <LI><I>ItemReason</I> - specifies the undo/redo item reason. The change list doesn't
      allow to insert succesive crCaretPos items unless Inserted is True</LI>
      <LI><I>Data</I> - specifies the item data. Some items (crCaretPos)
      don't need to supply any data</LI>
      <LI><I>Inserted</I> - for the urInsert* items, specifies whether the item
      was recorded with @link(TKCustomHexEditor.InsertMode) on (True) or
      off (False). See ItemReason for crCaretPos behavior.</LI>
      </UL>}
    procedure AddChange(ItemReason: TKHexEditorChangeReason; const Data: AnsiString = '';
      Inserted: Boolean = True); virtual;
    { Tells the undo list a new undo/redo group is about to be created. Each
      BeginGroup call must have a corresponding EndGroup call (use try-finally).
      BeginGroup calls may be nested, however, only the first call will create an
      undo/redo group. Use the GroupReason parameter to specify the reason of this group. }
    procedure BeginGroup(GroupReason: TKHexEditorChangeReason); virtual;
    { Informs whether there are any undo/redo items available - i.e. CanUndo/CanRedo}
    function CanPeek: Boolean;
    { Clears the entire list - overriden to execute some adjustments }
    procedure Clear; override;
    { Completes the undo/redo group. See @link(TKHexEditorChangeList.BeginGroup) for details }
    procedure EndGroup; virtual;
    { Returns the topmost item to handle or inspect it}
    function PeekItem: PKHexEditorChangeItem;
    { If there is no reason to handle an item returned by PeekItem, it has to be
    poked back with this function to become active for next undo/redo command }
    procedure PokeItem;
    { For redo list only - each undo command creates a redo command with the same
      group information - see source }
    procedure SetGroupData(Group: Integer; GroupReason: TKHexEditorChangeReason);
    { Specifies maximum number of items - not groups }
    property Limit: Integer read FLimit write SetLimit;
    { For undo list only - returns True if undo list contains some items with regard
      to the eoUndoAfterSave option }
    property Modified: Boolean read GetModified write SetModified;
    { Allows to call TKCustomHexEditor.@link(TKCustomHexEditor.OnChange) event}
    property OnChange: TKHexEditorUndoChangeEvent read FOnChange write FOnChange;
  end;

  { @abstract(Hexadecimal editor base component) }
  TKCustomHexEditor = class(TKCustomControl)
  private
    FAddressCursor: TCursor;
    FAddressMode: TKHexEditorAddressMode;
    FAddressOffset: Integer;
    FAddressPrefix: string;
    FAddressSize: Integer;
    FAreaSpacing: Integer;
    FBuffer: PBytes;
    FCharHeight: Integer;
    FCharMapping: TKEditCharMapping;
    FCharSpacing: Integer;
    FCharWidth: Integer;
    FClipboardFormat: Word;
    FColors: TKHexEditorColors;
    FDigitGrouping: Integer;
    FDisabledDrawStyle: TKHexEditorDisabledDrawStyle;
    FDrawStyles: TKHexEditorDrawStyles;
    FEditArea: TKHexEditorArea;
    FKeyMapping: TKEditKeyMapping;
    FLeftChar: Integer;
    FLineHeightPercent: Integer;
    FLineSize: Integer;
    FMouseWheelAccumulator: Integer;
    FOptions: TKEditOptions;
    FRedoList: TKHexEditorChangeList;
    FScrollBars: TScrollStyle;
    FScrollDeltaX: Integer;
    FScrollDeltaY: Integer;
    FScrollSpeed: Cardinal;
    FScrollTimer: TTimer;
    FSelEnd: TKHexEditorSelection;
    FSelStart: TKHexEditorSelection;
    FSize: Integer;
    FStates: TKHexEditorStates;
    FTopLine: Integer;
    FTotalCharSpacing: Integer;
    FUndoList: TKHexEditorChangeList;
    FOnChange: TNotifyEvent;
    FOnDropFiles: TKEditDropFilesEvent;
    FOnReplaceText: TKEditReplaceTextEvent;
    function GetCommandKey(Index: TKEditCommand): TKEditKey;
    function GetCaretVisible: Boolean;
    function GetData: TDataSize;
    function GetEmpty: Boolean;
    function GetFirstVisibleIndex: Integer;
    function GetInsertMode: Boolean;
    function GetLastVisibleIndex: Integer;
    function GetLineCount: Integer;
    function GetLines(Index: Integer): TDataSize;
    function GetModified: Boolean;
    function GetReadOnly: Boolean;
    function GetSelLength: TKHexEditorSelection;
    function GetSelText: TKHexEditorSelText;
    function GetUndoLimit: Integer;
    function IsAddressPrefixStored: Boolean;
    function IsDrawStylesStored: Boolean;
    function IsOptionsStored: Boolean;
    procedure ScrollTimerHandler(Sender: TObject);
    procedure SetAddressCursor(Value: TCursor);
    procedure SetAddressMode(Value: TKHexEditorAddressMode);
    procedure SetAddressOffset(Value: Integer);
    procedure SetAddressPrefix(const Value: string);
    procedure SetAddressSize(Value: Integer);
    procedure SetAreaSpacing(Value: Integer);
    procedure SetCharSpacing(Value: Integer);
    procedure SetColors(Value: TKHexEditorColors);
    procedure SetCommandKey(Index: TKEditCommand; Value: TKEditKey);
    procedure SetData(Value: TDataSize);
    procedure SetDigitGrouping(Value: Integer);
    procedure SetDisabledDrawStyle(Value: TKHexEditorDisabledDrawStyle);
    procedure SetDrawStyles(const Value: TKHexEditorDrawStyles);
    procedure SetEditArea(Value: TKHexEditorArea);
    procedure SetLeftChar(Value: Integer);
    procedure SetLineHeightPercent(Value: Integer);
    procedure SetLines(Index: Integer; const Value: TDataSize);
    procedure SetLineSize(Value: Integer);
    procedure SetModified(Value: Boolean);
    procedure SetOptions(const Value: TKEditOptions);
    procedure SetReadOnly(Value: Boolean);
    procedure SetScrollBars(Value: TScrollStyle);
    procedure SetScrollSpeed(Value: Cardinal);
    procedure SetSelEnd(Value: TKHexEditorSelection);
    procedure SetSelLength(Value: TKHexEditorSelection);
    procedure SetSelStart(Value: TKHexEditorSelection);
    procedure SetTopLine(Value: Integer);
    procedure SetUndoLimit(Value: Integer);
    procedure CMEnabledChanged(var Msg: TLMessage); message CM_ENABLEDCHANGED;
    procedure CMSysColorChange(var Msg: TLMessage); message CM_SYSCOLORCHANGE;
  {$IFNDEF FPC}
    // no way to get filenames in Lazarus inside control (why??)
    procedure WMDropFiles(var Msg: TLMessage); message LM_DROPFILES;
  {$ENDIF}
    procedure WMEraseBkgnd(var Msg: TLMessage); message LM_ERASEBKGND;
    procedure WMGetDlgCode(var Msg: TLMNoParams); message LM_GETDLGCODE;
    procedure WMHScroll(var Msg: TLMHScroll); message LM_HSCROLL;
    procedure WMKillFocus(var Msg: TLMKillFocus); message LM_KILLFOCUS;
    procedure WMSetFocus(var Msg: TLMSetFocus); message LM_SETFOCUS;
    procedure WMVScroll(var Msg: TLMVScroll); message LM_VSCROLL;
  protected
    { Inserts a single crCaretPos item into undo list. Unless Force is set to True,
      this change will be inserted only if previous undo item is not crCaretPos. }
    procedure AddUndoCaretPos(Force: Boolean = True);
    { Inserts a single byte change into undo list.
      <UL>
      <LH>Parameters:</LH>
      <LI><I>ItemReason</I> - specifies the undo/redo item reason - most likely
      crInsertChar or crDeleteChar.</LI>
      <LI><I>Data</I> - specifies the data byte needed to restore the original
      buffer state</LI>
      <LI><I>Inserted</I> - for the urInsert* items, specifies the current
      @link(TKCustomHexEditor.InsertMode) status.</LI>
      </UL>}
    procedure AddUndoByte(ItemReason: TKHexEditorChangeReason; Data: Byte;
      Inserted: Boolean = True);
    { Inserts a byte array change into undo list.
      <UL>
      <LH>Parameters:</LH>
      <LI><I>ItemReason</I> - specifies the undo/redo item reason - crInsert* or
      crDelete*.</LI>
      <LI><I>Data</I> - specifies the data bytes needed to restore the original
      buffer state</LI>
      <LI><I>Inserted</I> - for the urInsert* items, specifies the current
      @link(TKCustomHexEditor.InsertMode) status.</LI>
      </UL>}
    procedure AddUndoBytes(ItemReason: TKHexEditorChangeReason; Data: PBytes;
      Length: Integer; Inserted: Boolean = True);
    { Inserts a string change into undo list. Has the same functionality as AddUndoBytes
      only Data is supplied as a string. }
    procedure AddUndoString(ItemReason: TKHexEditorChangeReason; const S: AnsiString;
      Inserted: Boolean = True);
    { Begins a new undo group. Use the GroupReason parameter to label it. }
    procedure BeginUndoGroup(GroupReason: TKHexEditorChangeReason);
    { Performs necessary adjustments when the buffer is modified programatically
      (not by user) }
    procedure BufferChanged;
    { Determines whether an ecScroll* command can be executed }
    function CanScroll(Command: TKEditCommand): Boolean; virtual;
    { Clears a character at position At. Doesn't perform any succesive adjustments. }
    procedure ClearChar(At: Integer);
    { Clears a the digit fields both in SelStart and SelEnd. Doesn't perform any succesive adjustments.}
    procedure ClearDigitSelection;
    { Clears a string of the Size length at position At. Doesn't perform any succesive adjustments. }
    procedure ClearString(At, Size: Integer);
    { Overriden method - defines additional styles for the hex editor window (scrollbars etc.)}
    procedure CreateParams(var Params: TCreateParams); override;
    { Overriden method - adjusts file drag&drop functionality }
    procedure CreateWnd; override;
    { Overriden method - adjusts file drag&drop functionality }
    procedure DestroyWnd; override;
    { Calls the @link(TKCustomHexEditor.OnChange) event }
    procedure DoChange; virtual;
    { Overriden method - handles mouse wheel messages }
    function DoMouseWheel(Shift: TShiftState; WheelDelta: Integer;
      MousePos: TPoint): Boolean; override;
    { Validates the EditArea property after it has been modified }
    procedure EditAreaChanged; virtual;
    { Closes the undo group created by @link(TKCustomHexEditor.BeginUndoGroup) }
    procedure EndUndoGroup;
    { Ensures that font pitch is always fpFixed and Font.Size is not too small or big }
    procedure FontChange(Sender: TObject); virtual;
    { Returns the horizontal page extent for the current edit area. This function is
      used by the ecPageLeft and ecPageRight commands. }
    function GetPageHorz: Integer; virtual;
    { Determines if the editor has input focus. }
    function HasFocus: Boolean; virtual;
    { Hides the caret. }
    procedure HideEditorCaret; virtual;
    { Inserts a character at specified position. Doesn't perform any succesive adjustments.
      <UL>
      <LH>Parameters:</LH>
      <LI><I>At</I> - position where the character should be inserted.</LI>
      <LI><I>Value</I> - character (data byte)</LI>
      </UL> }
    procedure InsertChar(At: Integer; Value: Byte);
    { Inserts a string at specified position. Doesn't perform any succesive adjustments.
      <UL>
      <LH>Parameters:</LH>
      <LI><I>At</I> - position where the string should be inserted.</LI>
      <LI><I>Value</I> - data byte string</LI>
      <LI><I>Size</I> - length of the data byte string</LI>
      </UL> }
    procedure InsertString(At: Integer; const Value: AnsiString; Size: Integer);
    { Returns True if the control has a selection. }
    function InternalGetSelAvail: Boolean; override;
    { Moves the caret one position left. Doesn't perform any succesive adjustments.}
    procedure InternalMoveLeft; virtual;
    { Moves the caret one position right. Doesn't perform any succesive adjustments.}
    procedure InternalMoveRight; virtual;
    { Overriden method - processes virtual key strokes according to current @link(TKCustomHexEditor.KeyMapping) }
    procedure KeyDown(var Key: Word; Shift: TShiftState); override;
    { Overriden method - processes character key strokes - data editing }
    procedure KeyPress(var Key: Char); override;
    { Updates information about printed shape. }
    procedure MeasurePages(var Info: TKPrintMeasureInfo); override;
    { Processes scrollbar messages.
      <UL>
      <LH>Parameters:</LH>
      <LI><I>ScrollBar</I> - scrollbar type from OS</LI>
      <LI><I>ScrollCode</I> - scrollbar action from OS</LI>
      <LI><I>Delta</I> - scrollbar position change</LI>
      <LI><I>UpdateNeeded</I> - set to True if you want to invalidate
      and update caret position</LI>
      </UL> }
    procedure ModifyScrollBar(ScrollBar, ScrollCode, Delta: Integer;
      UpdateNeeded: Boolean);
    { Overriden method - updates caret position/selection }
    procedure MouseDown(Button: TMouseButton; Shift: TShiftState; X, Y: Integer); override;
    { Overriden method - updates caret position/selection and initializes scrolling
      when needed. }
    procedure MouseMove(Shift: TShiftState; X, Y: Integer); override;
    { Overriden method - releases mouse capture acquired by MouseDown }
    procedure MouseUp(Button: TMouseButton; Shift: TShiftState; X, Y: Integer); override;
    { Overriden method - calls PaintLines for drawing the hex editor outline
      into window client area }
    procedure PaintToCanvas(ACanvas: TCanvas); override;
    { Paints/prints hex editor outline. This function must retain its reentrancy.
      <UL>
      <LH>Parameters:</LH>
      <LI><I>Data</I> - paint settings</LI>
      </UL> }
    procedure PaintLines(const Data: TKHexEditorPaintData); virtual;
    { Paints a page to a printer/preview canvas. }
    procedure PaintPage; override;
    { Grants the input focus to the control when possible and the control has had none before }
    procedure SafeSetFocus;
    { Performs necessary adjustments after a selection property changed.
      <UL>
      <LH>Parameters:</LH>
      <LI><I>StartEqualEnd</I> - forces SelStart equal to SelEnd</LI>
      <LI><I>ScrollToView</I> - forces scrolling if SelEnd (caret) became invisible</LI>
      </UL> }
    procedure SelectionChanged(StartEqualEnd: Boolean; ScrollToView: Boolean = True);
    { Scrolls the hex editor window horizontaly by HChars characters and/or
      vertically by VChars characters }
    procedure ScrollBy(HChars, VChars: Integer; UpdateNeeded: Boolean);
    { Scrolls the hex editor window to ensure data under defined (mouse) coordinates are visible
      <UL>
      <LH>Parameters:</LH>
      <LI><I>Point</I> - (mouse) coordinates</LI>
      <LI><I>Timed</I> - set to True to continue scroll via a timer. The scrolling
      will continue until the mouse cursor is outside of the modified client rect
      (@link(TKCustomHexEditor.GetModifiedClientRect)).</LI>
      <LI><I>AlwaysScroll</I> - set to True to disable new line overscrolling</LI>
      </UL> }
    procedure ScrollTo(Point: TPoint; Timed, AlwaysScroll: Boolean); virtual;
    { Updates mouse cursor according to the state determined from current mouse
      position. Returns True if cursor has been changed. }
    function SetMouseCursor(X, Y: Integer): Boolean; override;
    { Shows the caret. }
    procedure ShowEditorCaret; virtual;
    { Calls the @link(TKCustomHexEditor.DoChange) method}
    procedure UndoChange(Sender: TObject; ItemReason: TKHexEditorChangeReason);
    { Updates caret position, shows/hides caret according to the input focus
      <UL>
      <LH>Parameters:</LH>
      <LI><I>Recreate</I> - set to True to recreate the caret after it has already
      been created and displayed</LI>
      </UL> }
    procedure UpdateEditorCaret(Recreate: Boolean = False); virtual;
    { Updates font based dimensions }
    procedure UpdateCharMetrics; virtual;
    { Updates mouse cursor }
    procedure UpdateMouseCursor; virtual;
    { Updates the scrolling range }
    procedure UpdateScrollRange; virtual;
    { Updates selection according to the supplied coordinates.
      <UL>
      <LH>Parameters:</LH>
      <LI><I>Point</I> - specifies the coordinates </LI>
      <LI><I>ClipToClient</I> - specifies whether the coordinates should be clipped
      to modified client rectangle (@link(TKCustomHexEditor.GetModifiedClientRect))
      first</LI>
      </UL> }
    procedure UpdateSelEnd(Point: TPoint; ClipToClient: Boolean); virtual;
    { Updates the control size. }
    procedure UpdateSize; override;
    { Data buffer - made accessible for descendant classes }
    property Buffer: PBytes read FBuffer write FBuffer;
    { Redo list manager - made accessible for descendant classes }
    property RedoList: TKHexEditorChangeList read FRedoList;
    { Data buffer size - made accessible for descendant classes }
    property Size: Integer read FSize write FSize;
    { States of this class - made accessible for descendant classes }
    property States: TKHexEditorStates read FStates write FStates;
    { Undo list manager - made accessible for descendant classes }
    property UndoList: TKHexEditorChangeList read FUndoList;
  public
    { Performs necessary initializations - default values to properties, create
      undo/redo list managers }
    constructor Create(AOwner: TComponent); override;
    { Destroy instance, undo/redo list managers, dispose buffer... }
    destructor Destroy; override;
    { Appends data at current position. Use TKHexEditor.Data.Size for At parameter
      to append at the end of the buffer. }
    procedure Append(At: Integer; Data: TDataSize); overload; virtual;
    { Appends data at current position. Use TKHexEditor.Data.Size for At parameter
      to append at the end of the buffer. }
    procedure Append(At: Integer; const Data: AnsiString); overload; virtual;
    { Takes property values from another TKCustomHexEditor class }
    procedure Assign(Source: TPersistent); override;
    { Determines whether the caret is visible }
    function CaretInView: Boolean;
    { Clears entire data buffer. Unlike ecClearAll this method clears everything
      inclusive undo a redo lists. }
    procedure Clear;
    { Clears undo (and redo) list }
    procedure ClearUndo;
    { Determines whether given command can be executed at this time. Use this
      function in TAction.OnUpdate events.
      <UL>
      <LH>Parameters:</LH>
      <LI><I>Command</I> - specifies the command to inspect</LI>
      </UL> }
    function CommandEnabled(Command: TKEditCommand): Boolean; virtual;
    { Executes given command. This function first calls CommandEnabled to
      assure given command can be executed.
      <UL>
      <LH>Parameters:</LH>
      <LI><I>Command</I> - specifies the command to execute</LI>
      <LI><I>Data</I> - specifies the data needed for the command</LI>
      </UL> }
    function ExecuteCommand(Command: TKEditCommand; Data: Pointer = nil): Boolean; virtual;
    { Returns dimensions of all 3 possible areas according to current area
      definition }
    function GetAreaDimensions: TKHexEditorAreaDimensions; virtual;
    { Returns current character mapping. }
    function GetCharMapping: TKEditCharMapping;
    { Returns number of characters that vertically fit into client window }
    function GetClientHeightChars: Integer; virtual;
    { Returns number of characters that horizontally fit into client window }
    function GetClientWidthChars: Integer; virtual;
    { Returns the current key stroke mapping scheme. }
    function GetKeyMapping: TKEditKeyMapping;
    { Returns modified client rect - a window client rect aligned to character width and
      character height }
    function GetModifiedClientRect: TRect; virtual;
    { Returns current maximum value for the @link(TKCustomHexEditor.LeftChar) property
      <UL>
      <LH>Parameters:</LH>
      <LI><I>Extent</I> - specify @link(TKHexEditorAreaDimensions).TotalHorz
      here, otherwise the function calculates it itself</LI>
      </UL> }
    function GetMaxLeftChar(Extent: Integer = 0): Integer; virtual;
    { Returns current maximum value for the @link(TKCustomHexEditor.TopLine) property
      <UL>
      <LH>Parameters:</LH>
      <LI><I>Extent</I> - specify @link(TKHexEditorAreaDimensions).TotalVert
      here, otherwise the function calculates it itself</LI>
      </UL> }
    function GetMaxTopLine(Extent: Integer = 0): Integer; virtual;
    { Returns "real" selection end - with always higher index value than selection
      start value }
    function GetRealSelEnd: TKHexEditorSelection;
    { Returns "real" selection start - with always lower index value than selection
      end value }
    function GetRealSelStart: TKHexEditorSelection;
    { Loads data from a file }
    procedure LoadFromFile(const FileName: TFileName);
    { Loads data from a stream - stream position remains untouched }
    procedure LoadFromStream(Stream: TStream);
    { Paints the editor outline to another canvas
      <UL>
      <LH>Parameters:</LH>
      <LI><I>ACanvas</I> - canvas to paint the outline to</LI>
      <LI><I>ARect</I> - given rectangle in the canvas</LI>
      <LI><I>ALeftChar</I> - first left visible character</LI>
      <LI><I>ATopLine</I> - first top visible line</LI>
      </UL> }
    procedure PaintToCanvasEx(ACanvas: TCanvas; ARect: TRect; ALeftChar, ATopLine: Integer);
    { Converts window coordinates into a selection
      <UL>
      <LH>Parameters:</LH>
      <LI><I>P</I> - window client coordinates</LI>
      <LI><I>OutOfArea</I> - uses the Area parameter to compute selection for
      this area even if the supplied coordinates are outside of the area outline</LI>
      <LI><I>Area</I> output parameter if OutOfArea = False, otherwise
      input parameter</LI>
      </UL> }
    function PointToSel(P: TPoint; OutOfArea: Boolean; var Area: TKHexEditorArea): TKHexEditorSelection; virtual;
    { Saves data into a file }
    procedure SaveToFile(const FileName: TFileName);
    { Saves data into a stream - stream position remains untouched }
    procedure SaveToStream(Stream: TStream);
    { Determines whether a seletion (not digit selection) is available }
    function SelAvail: Boolean;
    { Determines whether a given selection is valid for given area
      <UL>
      <LH>Parameters:</LH>
      <LI><I>Value</I> - selection to examine</LI>
      <LI><I>Area</I> - area for which the selection must be examined</LI>
      </UL> }
    function SelectionValid(Value: TKHexEditorSelection; Area: TKHexEditorArea): Boolean; virtual;
    { Converts a selection into window coordinates
      <UL>
      <LH>Parameters:</LH>
      <LI><I>Value</I> - selection to convert</LI>
      <LI><I>Area</I> - the same selection delivers another coordinates for each area</LI>
      </UL> }
    function SelToPoint(Value: TKHexEditorSelection; Area: TKHexEditorArea): TPoint; virtual;
    { Specifies character mapping. The main purpose of this is to avoid non-printable
      characters in the text area and in AsText copies. Avoid non-printable characters
      when delivering a new character mapping. }
    procedure SetCharMapping(const Value: TKEditCharMapping);
    { Specifies the current key stroke mapping scheme }
    procedure SetKeyMapping(const Value: TKEditKeyMapping);
    { Validates a selection for given area
      <UL>
      <LH>Parameters:</LH>
      <LI><I>Value</I> - selection to validate</LI>
      <LI><I>Area</I> - area for which the selection must be validated</LI>
      </UL> }
    procedure ValidateSelection(var Value: TKHexEditorSelection; Area: TKHexEditorArea); virtual;
    { Specifies the address area mouse cursor. Other areas have crIBeam - should not
      be needed to modify that }
    property AddressCursor: TCursor read FAddressCursor write SetAddressCursor default cAddressCursorDef;
    { Specifies the radix of addresses }
    property AddressMode: TKHexEditorAddressMode read FAddressMode write SetAddressMode default cAddressModeDef;
    { Specifies the address offset }
    property AddressOffset: Integer read FAddressOffset write SetAddressOffset default cAddressOffsetDef;
    { Specifies the address number prefix i.e. 0x or $ - modify together with AddressMode }
    property AddressPrefix: string read FAddressPrefix write SetAddressPrefix stored IsAddressPrefixStored;
    { Specifies the number of address digits - up to 10 for decimal addresses }
    property AddressSize: Integer read FAddressSize write SetAddressSize default cAddressSizeDef;
    { Defines space between neighbour areas }
    property AreaSpacing: Integer read FAreaSpacing write SetAreaSpacing default cAreaSpacingDef;
    { Returns current caret position = selection end }
    property CaretPos: TKHexEditorSelection read FSelEnd;
    { Returns True if caret is visible }
    property CaretVisible: Boolean read GetCaretVisible;
    { Returns current character width = not necessarily equal to font character width }
    property CharWidth: Integer read FCharWidth;
    { Defines additional inter-character spacing }
    property CharSpacing: Integer read FCharSpacing write SetCharSpacing default cCharSpacingDef;
    { Returns current character height = not equal to font character height }
    property CharHeight: Integer read FCharHeight;
    { Returns the binary data clipboard format }
    property ClipboardFormat: Word read FClipboardFormat;
    { Makes it possible to take all color properties from another TKCustomHexEditor class }
    property Colors: TKHexEditorColors read FColors write SetColors;
    { Specifies a new key stroke combination for given command }
    property CommandKey[Index: TKEditCommand]: TKEditKey read GetCommandKey write SetCommandKey;
    { This property provides direct access to the data buffer }
    property Data: TDataSize read GetData write SetData;
    { Specifies the byte grouping in the digits area }
    property DigitGrouping: Integer read FDigitGrouping write SetDigitGrouping default cDigitGroupingDef;
    { Specifies the style how the outline is drawn when editor is disabled }
    property DisabledDrawStyle: TKHexEditorDisabledDrawStyle read FDisabledDrawStyle write SetDisabledDrawStyle default cDisabledDrawStyleDef;
    { Defines areas to paint, whether to paint horizontal and vertical trailing lines,
      area separator lines and caret mark when the editor has no input focus }
    property DrawStyles: TKHexEditorDrawStyles read FDrawStyles write SetDrawStyles stored IsDrawStylesStored;
    { Specifies the current area for editing }
    property EditArea: TKHexEditorArea read FEditArea write SetEditArea default eaDigits;
    { Returns True if data buffer is empty }
    property Empty: Boolean read GetEmpty;
    { Returns the first visible index }
    property FirstVisibleIndex: Integer read GetFirstVisibleIndex;
    { Returns True if insert mode is on }
    property InsertMode: Boolean read GetInsertMode;
    { Returns the last visible index }
    property LastVisibleIndex: Integer read GetLastVisibleIndex;
    { Specifies the horizontal scroll position }
    property LeftChar: Integer read FLeftChar write SetLeftChar;
    { Determines the number of lines }
    property LineCount: Integer read GetLineCount;
    { Specifies the line height. 100% is the current font height }
    property LineHeightPercent: Integer read FLineHeightPercent write SetLineHeightPercent default cLineHeightPercentDef;
    { Allows to modify/add data lines. If greater than LineSize, the Size member
      of the supplied TDataSize structure will be always trimmed to LineSize.
      If Index points to last incomplete line or even higher, last line will be
      extended/completed, i.e new data will be added to the buffer }
    property Lines[Index: Integer]: TDataSize read GetLines write SetLines;
    { Specifies the size (length) of a single line }
    property LineSize: Integer read FLineSize write SetLineSize default cLineSizeDef;
    { Returns True if the buffer was modified - eoUndoAfterSave taken into
      account }
    property Modified: Boolean read GetModified write SetModified;
    { Specifies the editor options that do not affect painting }
    property Options: TKEditOptions read FOptions write SetOptions stored IsOptionsStored;
    { Specifies whether the editor has to be read only editor }
    property ReadOnly: Boolean read GetReadOnly write SetReadOnly default False;
    { Defines visible scrollbars - horizontal, vertical or both }
    property ScrollBars: TScrollStyle read FScrollBars write SetScrollBars default ssBoth;
    { Specifies how fast the scrolling by timer should be }
    property ScrollSpeed: Cardinal read FScrollSpeed write SetScrollSpeed default cScrollSpeedDef;
    { Specifies the current selection end }
    property SelEnd: TKHexEditorSelection read FSelEnd write SetSelEnd;
    { Specifies the current selection length. SelStart remains unchanged, SelEnd will be
      updated accordingly. To mark a selection, either set both SelStart and SelEnd properties
      or both SelStart and SelLength properties }
    property SelLength: TKHexEditorSelection read GetSelLength write SetSelLength;
    { Specifies the current selection start }
    property SelStart: TKHexEditorSelection read FSelStart write SetSelStart;
    { Returns selected text in many different formats }
    property SelText: TKHexEditorSelText read GetSelText;
    { Specifies the vertical scroll position }
    property TopLine: Integer read FTopLine write SetTopLine;
    { Specifies the maximum number of undo items. Please note this value
      affects the undo item limit, not undo group limit. }
    property UndoLimit: Integer read GetUndoLimit write SetUndoLimit default cUndoLimitDef;
    { When assigned, this event will be invoked at each buffer change, made either
      by the user or programmatically by public functions }
    property OnChange: TNotifyEvent read FOnChange write FOnChange;
    { When assigned, this event will be invoked when the user drops any files onto
      the window }
    property OnDropFiles: TKEditDropFilesEvent read FOnDropFiles write FOnDropFiles;
    { When assigned, this event will be invoked at each prompt-forced search match }
    property OnReplaceText: TKEditReplaceTextEvent read FOnReplaceText write FOnReplaceText;
  end;

  { @abstract(Hexadecimal editor design-time component) }
  TKHexEditor = class(TKCustomHexEditor)
  published
    { See TKCustomHexEditor.@link(TKCustomHexEditor.AddressCursor) for details }
    property AddressCursor;
    { See TKCustomHexEditor.@link(TKCustomHexEditor.AddressMode) for details }
    property AddressMode;
    { See TKCustomHexEditor.@link(TKCustomHexEditor.AddressOffset) for details }
    property AddressOffset;
    { See TKCustomHexEditor.@link(TKCustomHexEditor.AddressPrefix) for details }
    property AddressPrefix;
    { See TKCustomHexEditor.@link(TKCustomHexEditor.AddressSize) for details }
    property AddressSize;
    { Inherited property - see Delphi help }
    property Align;
    { Inherited property - see Delphi help }
    property Anchors;
    { See TKCustomControl.@link(TKCustomControl.BorderStyle) for details }
    property BorderStyle;
    { Inherited property - see Delphi help }
    property BorderWidth;
    { See TKCustomHexEditor.@link(TKCustomHexEditor.CharSpacing) for details }
    property CharSpacing;
    { See TKCustomHexEditor.@link(TKCustomHexEditor.Colors) for details }
    property Colors;
    { Inherited property - see Delphi help }
    property Constraints;
  {$IFNDEF FPC}
    { Inherited property - see Delphi help. }
    property Ctl3D;
  {$ENDIF}
    { See TKCustomHexEditor.@link(TKCustomHexEditor.DigitGrouping) for details }
    property DigitGrouping;
    { See TKCustomHexEditor.@link(TKCustomHexEditor.DisabledDrawStyle) for details }
    property DisabledDrawStyle;
    { Inherited property - see Delphi help }
    property DragCursor;
    { Inherited property - see Delphi help }
    property DragKind;
    { Inherited property - see Delphi help }
    property DragMode;
    { See TKCustomHexEditor.@link(TKCustomHexEditor.DrawStyles) for details }
    property DrawStyles;
    { See TKCustomHexEditor.@link(TKCustomHexEditor.EditArea) for details }
    property EditArea;
    { Inherited property - see Delphi help }
    property Enabled;
    { Inherited property - see Delphi help. Font pitch must always remain fpFixed
      - specify fixed fonts only. Font.Size will also be trimmed if too small or big }
    property Font;
    { Inherited property - see Delphi help }
    property Height default cHeight;
    { See TKCustomHexEditor.@link(TKCustomHexEditor.LineHeightPercent) for details }
    property LineHeightPercent;
    { See TKCustomHexEditor.@link(TKCustomHexEditor.LineSize) for details }
    property LineSize;
    { See TKCustomHexEditor.@link(TKCustomHexEditor.Options) for details }
    property Options;
    { Inherited property - see Delphi help }
    property ParentShowHint;
    { Inherited property - see Delphi help }
    property PopupMenu;
    { See TKCustomHexEditor.@link(TKCustomHexEditor.ReadOnly) for details }
    property ReadOnly;
    { See TKCustomHexEditor.@link(TKCustomHexEditor.ScrollBars) for details }
    property ScrollBars;
    { See TKCustomHexEditor.@link(TKCustomHexEditor.ScrollSpeed) for details }
    property ScrollSpeed;
    { Inherited property - see Delphi help }
    property ShowHint;
    { Inherited property - see Delphi help }
    property TabOrder;
    { Inherited property - see Delphi help }
    property TabStop default True;
    { See TKCustomHexEditor.@link(TKCustomHexEditor.UndoLimit) for details }
    property UndoLimit;
    { Inherited property - see Delphi help }
    property Visible;
    { Inherited property - see Delphi help }
    property Width default cWidth;
    { See TKCustomHexEditor.@link(TKCustomHexEditor.OnChange) for details }
    property OnChange;
    { Inherited property - see Delphi help }
    property OnClick;
    { Inherited property - see Delphi help }
    property OnContextPopup;
    { Inherited property - see Delphi help }
    property OnDblClick;
    { Inherited property - see Delphi help }
    property OnDockDrop;
    { Inherited property - see Delphi help }
    property OnDockOver;
    { Inherited property - see Delphi help }
    property OnDragDrop;
    { Inherited property - see Delphi help }
    property OnDragOver;
    { See TKCustomHexEditor.@link(TKCustomHexEditor.OnDropFiles) for details }
    property OnDropFiles;
    { Inherited property - see Delphi help }
    property OnEndDock;
    { Inherited property - see Delphi help }
    property OnEndDrag;
    { Inherited property - see Delphi help }
    property OnEnter;
    { Inherited property - see Delphi help }
    property OnExit;
    { Inherited property - see Delphi help }
    property OnGetSiteInfo;
    { Inherited property - see Delphi help }
    property OnKeyDown;
    { Inherited property - see Delphi help }
    property OnKeyPress;
    { Inherited property - see Delphi help }
    property OnKeyUp;
    { Inherited property - see Delphi help }
    property OnMouseDown;
    { Inherited property - see Delphi help }
    property OnMouseMove;
    { Inherited property - see Delphi help }
    property OnMouseUp;
    { Inherited property - see Delphi help }
    property OnMouseWheel;
    { Inherited property - see Delphi help }
    property OnMouseWheelDown;
    { Inherited property - see Delphi help }
    property OnMouseWheelUp;
    { See TKCustomControl.@link(TKCustomControl.OnPrintNotify) for details }
    property OnPrintNotify;
    { See TKCustomControl.@link(TKCustomControl.OnPrintPaint) for details }
    property OnPrintPaint;
    { See TKCustomHexEditor.@link(TKCustomHexEditor.OnReplaceText) for details }
    property OnReplaceText;
    { Inherited property - see Delphi help }
    property OnResize;
    { Inherited property - see Delphi help }
    property OnStartDock;
    { Inherited property - see Delphi help }
    property OnStartDrag;
    { Inherited property - see Delphi help }
    property OnUnDock;
  end;

{ Creates a selection structure from given Index and Digit parameters }
function MakeSelection(Index, Digit: Integer): TKHexEditorSelection;

{ Converts a hexadecimal digit character ('0'..'F') to binary value }
function DigitToBin(Value: AnsiChar): Integer;

{ Examines/converts hexadecimal digit string to binary value string. Returns
  True if the digit string is valid.
  <UL>
  <LH>Parameters:</LH>
  <LI><I>S</I> - hexadecimal digit string (e.g. 'AF01 DC05 3'). White spaces will
  be ignored. When Convert is True, the converted binary value string will be returned
  via this parameter (in this exammple '#A#F#0#1#D#C#0#5#3').</LI>
  <LI><I>Convert</I> - the digit string will be converted if True, otherwise it will
  be examined only.</LI>
  </UL> }
function DigitsToBinStr(var S: AnsiString; Convert: Boolean = True): Boolean;

{ Converts a binary value string into binary data. If the binary value string
  is not divisible by 2, it will be right padded with zero. Example:
  '#A#F#0#1#D#C#0#5#3' is converted into '#AF#01#DC#05#30'. }
function BinStrToBinary(const S: AnsiString): AnsiString;

{ Converts binary data into hexadecimal digit string.
  <UL>
  <LH>Parameters:</LH>
  <LI><I>Buffer</I> - binary data - intended for @link(TKCustomHexEditor.Buffer)</LI>
  <LI><I>SelStart, SelEnd</I> - specifies which part of the buffer is about to be
  converted. SelStart.Index must be lower or equal to SelEnd.Index - intended for
  @link(TKCustomHexEditor.GetRealSelStart) and @link(TKCustomHexEditor.GetRealSelEnd).</LI>
  </UL> }
function BinaryToDigits(Buffer: PBytes; SelStart, SelEnd: TKHexEditorSelection): AnsiString;

{ Converts binary data into text using given character mapping.
  <UL>
  <LH>Parameters:</LH>
  <LI><I>Buffer</I> - binary data - intended for @link(TKCustomHexEditor.Buffer)</LI>
  <LI><I>SelStart, SelEnd</I> - specifies which part of the buffer is about to be
  converted. SelStart must be lower or equal to SelEnd. These parameters are integers
  since no digit selections are necessary.</LI>
  <LI><I>CharMapping</I> - required character mapping scheme</LI>
  </UL> }
function BinaryToText(Buffer: PBytes; SelStart, SelEnd: Integer;
  CharMapping: PKEditCharMapping): AnsiString;

{ Replaces a hexadecimal digit in the given binary value. Returns the original
  value with a replaced digit.
  <UL>
  <LH>Parameters:</LH>
  <LI><I>Value</I> - original binary value</LI>
  <LI><I>Digit</I> - digit value (0..15)</LI>
  <LI><I>Pos</I> - digit position (order)</LI>
  </UL>
  Example: Value = $A18D, Digit = $C, Pos = 3: Result = $AC8D }
function ReplaceDigit(Value, Digit, Pos: Integer): Integer;

{ Returns the instance-independent color specification for
  the given color index }
function GetColorSpec(Index: TKHexEditorColorIndex): TKHexEditorColorSpec;

implementation

uses
{$IFDEF USE_THEMES}
  Themes,
{$ENDIF}
  Math,
{$IFDEF USE_WINAPI}
  ShellApi,
{$ENDIF}
  ClipBrd, Printers,
  Types, KGraphics;

const
  cFmtText = '%.2x';
  cBase = 16;
  cDigitCount = 2;

function MakeSelection(Index, Digit: Integer): TKHexEditorSelection;
begin
  Result.Index := Index;
  Result.Digit := Digit;
end;

function DigitToBin(Value: AnsiChar): Integer;
begin
  if ((Value >= 'a') and (Value <= 'f')) then Result := Ord(Value) - Ord('a') + 10
  else if ((Value >= 'A') and (Value <= 'F')) then Result := Ord(Value) - Ord('A') + 10
  else if ((Value >= '0') and (Value <= '9')) then Result := Ord(Value) - Ord('0')
  else Result := -1;
end;

function DigitsToBinStr(var S: AnsiString; Convert: Boolean = True): Boolean;
var
  I, J, K: Integer;
  T: AnsiString;
begin
  // check and convert text characters to hex values 0..15
  Result := True;
  if Convert then
    SetLength(T, Length(S));
  J := 0;
  for I := 1 to Length(S) do if not CharInSetEx(S[I], [#9, #32]) then
  begin
    K := DigitToBin(S[I]);
    if K >= 0 then
    begin
      if Convert then
      begin
        Inc(J);
        T[J] := AnsiChar(K)
      end;
    end else
    begin
      Result := False;
      Break;
    end;
  end;
  if Result and Convert then
  begin
    SetLength(T, J);
    S := T;
  end;
end;

function BinStrToBinary(const S: AnsiString): AnsiString;
var
  I, J, L: Integer;
  B1, B2: Byte;
begin
  L := Length(S);
  Result := '';
  if L > 0 then
  begin
    SetLength(Result, DivUp(L, 2));
    if L = 1 then
      Result := S
    else
    begin
      J := 1;
      for I := 1 to Length(Result) do
      begin
        B1 := Byte(S[J]); Inc(J);
        if J <= L then
        begin
          B2 := Byte(S[J]); Inc(J);
        end else
          B2 := 0;
        Result[I] := AnsiChar(B1 shl 4 + B2);
      end;
    end;
  end;
end;

function BinaryToDigits(Buffer: PBytes; SelStart, SelEnd: TKHexEditorSelection): AnsiString;
var
  I, J: Integer;
  S: AnsiString;
begin
  Result := '';
  S := '%s' + cFmtText;
  for I := SelStart.Index to SelEnd.Index do
  begin
    Result := AnsiString(Format(string(S), [Result, Buffer[I]]));
    if I = SelStart.Index then
    begin
      for J := 0 to SelStart.Digit - 1 do
        Delete(Result, 1, 1);
    end;
    if I = SelEnd.Index then
    begin
      for J := SelEnd.Digit to cDigitCount - 1 do
        Delete(Result, Length(Result), 1);
    end;
  end;
end;

function BinaryToText(Buffer: PBytes; SelStart, SelEnd: Integer;
  CharMapping: PKEditCharMapping): AnsiString;
var
  I: Integer;
begin
  if SelEnd > SelStart then
  begin
    SetLength(Result, SelEnd - SelStart);
    System.Move(Buffer[SelStart], Result[1], SelEnd - SelStart);
    if CharMapping <> nil then
      for I := 1 to Length(Result) do
        Result[I] := CharMapping^[Byte(Result[I])];
  end else
    Result := '';
end;

function ReplaceDigit(Value, Digit, Pos: Integer): Integer;
var
  I, Mask, O: Integer;
begin
  O := 1;
  for I := Pos to cDigitCount - 2 do
    O := O * cBase;
  Mask := cBase - 1;
  Result := (((Value div O) and not Mask) + (Digit and Mask)) * O + Value mod O;
end;

function OppositeReason(ItemReason: TKHexEditorChangeReason): TKHexEditorChangeReason;
begin
  case ItemReason of
    crDeleteChar: Result := crInsertChar;
    crDeleteDigits: Result := crInsertDigits;
    crDeleteString: Result := crInsertString;
    crInsertChar: Result := crDeleteChar;
    crInsertDigits: Result := crDeleteDigits;
    crInsertString: Result := crDeleteString;
  else
    Result := ItemReason;
  end;
end;

{ TKHexEditorColors }

constructor TKHexEditorColors.Create(AOwner: TKCustomHexEditor);
begin
  FOwner := AOwner;
  Initialize;
  ClearBrightColors;
end;

procedure TKHexEditorColors.Assign(Source: TPersistent);
begin
  if Source is TKHexEditorColors then
  begin
    Colors := TKHexEditorColors(Source).Colors;
    FOwner.Invalidate;
  end
  else
    inherited;
end;

procedure TKHexEditorColors.ClearBrightColors;
var
  I: TKHexEditorColorIndex;
begin
  for I := 0 to Length(FBrightColors) - 1 do
    FBrightColors[I] := clNone;
end;

function TKHexEditorColors.GetColor(Index: TKHexEditorColorIndex): TColor;
const
  AreaBkGndSet = [ciAddressBkgnd, ciDigitBkGnd, ciTextBkGnd];
  BkGndSet = [ciAddressBkgnd, ciBkGnd, ciDigitBkGnd, ciInactiveCaretBkGnd,
    ciInactiveCaretSelBkGnd, ciSelBkGnd, ciSelBkGndFocused, ciTextBkgnd];
begin
  case FColorScheme of
    ecsGrayed: if Index in BkGndSet then Result := clWindow else Result := clGrayText;
    ecsBright:
    begin
      if FBrightColors[Index] = clNone then
        FBrightColors[Index] := BrightColor(FColors[Index], 0.5, bsOfTop);
      if FSingleBkGnd and (Index in AreaBkGndSet) then
        Result := FBrightColors[ciBkGnd]
      else
        Result := FBrightColors[Index];
    end;
    ecsGrayScale: Result := ColorToGrayScale(FColors[Index]);
  else
    if FSingleBkGnd and (Index in AreaBkGndSet) then
      Result := FColors[ciBkGnd]
    else
      Result := FColors[Index];
  end;
end;

function TKHexEditorColors.GetColorData(Index: TKHexEditorColorIndex): TKHexEditorColorData;
var
  ColorSpec: TKHexEditorColorSpec;
begin
  Result.Index := Index;
  Result.Color := FColors[Index];
  ColorSpec := GetColorSpec(Index);
  Result.Default := ColorSpec.Def;
  Result.Name := ColorSpec.Name;
end;

function TKHexEditorColors.GetColorEx(Index: TKHexEditorColorIndex): TColor;
begin
  Result := FColors[Index];
end;

function TKHexEditorColors.GetColorName(Index: TKHexEditorColorIndex): string;
begin
  Result := GetColorSpec(Index).Name;
end;

procedure TKHexEditorColors.Initialize;
var
  I: TKHexEditorColorIndex;
begin
  SetLength(FColors, ciHexEditorColorsMax + 1);
  SetLength(FBrightColors, ciHexEditorColorsMax + 1);
  for I := 0 to Length(FColors) - 1 do
    FColors[I] := GetColorSpec(I).Def;
end;

procedure TKHexEditorColors.SetColor(Index: TKHexEditorColorIndex; Value: TColor);
begin
  if FColors[Index] <> Value then
  begin
    FColors[Index] := Value;
    FBrightColors[Index] := clNone;
    if not (csLoading in FOwner.ComponentState) and FOwner.HandleAllocated then
      FOwner.Invalidate;
  end;
end;

procedure TKHexEditorColors.SetColorEx(Index: TKHexEditorColorIndex; Value: TColor);
begin
  if FColors[Index] <> Value then
  begin
    FColors[Index] := Value;
    FBrightColors[Index] := clNone;
  end;
end;

procedure TKHexEditorColors.SetColors(const Value: TKColorArray);
begin
  FColors := Value;
  ClearBrightColors;
end;

{ TKHexEditorChangeList }

constructor TKHexEditorChangeList.Create(AEditor: TKCustomHexEditor;
  RedoList: TKHexEditorChangeList);
begin
  inherited Create;
  FEditor := AEditor;
  FGroupUseLock := 0;
  FLimit := cUndoLimitDef;
  FIndex := -1;
  FModifiedIndex := FIndex;
  FRedoList := RedoList;
  FOnChange := nil;
end;

procedure TKHexEditorChangeList.AddChange(ItemReason: TKHexEditorChangeReason;
  const Data: AnsiString; Inserted: Boolean);
var
  P: PKHexEditorChangeItem;
begin
  // don't allow succesive crCaretPos
  if (ItemReason = crCaretPos) and not Inserted and (FIndex >= 0) and
    (PKHexEditorChangeItem(Items[FIndex]).ItemReason = crCaretPos) then
    Exit;
  if FIndex < FLimit - 1 then
  begin
    if FIndex < Count - 1 then
      Inc(FIndex)
    else
      FIndex := Add(New(PKHexEditorChangeItem));
    P := Items[FIndex];
    if FGroupUseLock > 0 then
    begin
      P.Group := FGroup;
      P.GroupReason := FGroupReason;
    end else
    begin
      P.Group := 0;
      P.GroupReason := ItemReason;
    end;
    P.ItemReason := ItemReason;
    P.EditArea := FEditor.EditArea;
    P.SelEnd := FEditor.SelEnd;
    P.SelStart := FEditor.SelStart;
    P.Data := Data;
    P.Inserted := Inserted;
    if FRedoList <> nil then
      FRedoList.Clear;
    if Assigned(FOnChange) then
      FOnChange(Self, ItemReason);
  end;
end;

procedure TKHexEditorChangeList.BeginGroup(GroupReason: TKHexEditorChangeReason);
begin
  if FGroupUseLock = 0 then
  begin
    FGroupReason := GroupReason;
    Inc(FGroup);
    if FGroup = 0 then Inc(FGroup);
  end;
  Inc(FGroupUseLock);
end;

function TKHexEditorChangeList.CanPeek: Boolean;
begin
  Result := FIndex >= 0;
end;

procedure TKHexEditorChangeList.Clear;
begin
  inherited;
  FGroupUseLock := 0;
  FIndex := -1;
  FModifiedIndex := FIndex;
end;

procedure TKHexEditorChangeList.EndGroup;
begin
  if FGroupUseLock > 0 then
    Dec(FGroupUseLock);
end;

function TKHexEditorChangeList.GetModified: Boolean;

  function CaretPosOnly: Boolean;
  var
    I: Integer;
  begin
    Result := True;
    for I := FModifiedIndex + 1 to FIndex do
    begin
      if PKHexEditorChangeItem(Items[I]).ItemReason <> crCaretPos then
      begin
        Result := False;
        Exit;
      end;  
    end;
  end;

begin
  Result := (FIndex > FModifiedIndex) and not CaretPosOnly;
end;

procedure TKHexEditorChangeList.Notify(Ptr: Pointer; Action: TListNotification);
var
  P: PKHexEditorChangeItem;
begin
  case Action of
    lnDeleted:
      if Ptr <> nil then
      begin
        P := Ptr;
        Dispose(P);
      end;
  end;
end;

function TKHexEditorChangeList.PeekItem: PKHexEditorChangeItem;
begin
  if CanPeek then
  begin
    Result := Items[FIndex];
    Dec(FIndex);
  end else
    Result := nil;
end;

procedure TKHexEditorChangeList.PokeItem;
begin
  if FIndex < Count - 1 then
    Inc(FIndex);
end;

procedure TKHexEditorChangeList.SetGroupData(Group: Integer;
  GroupReason: TKHexEditorChangeReason);
begin
  FGroup := Group;
  FGroupReason := GroupReason;
  FGroupUseLock := 1;
end;

procedure TKHexEditorChangeList.SetLimit(Value: Integer);
begin
  if Value <> FLimit then
  begin
    FLimit := MinMax(Value, cUndoLimitMin, cUndoLimitMax);
    while Count > FLimit do
      Delete(0);
    FIndex := Min(FIndex, FLimit - 1);
  end;
end;

procedure TKHexEditorChangeList.SetModified(Value: Boolean);
begin
  if not Value then
    FModifiedIndex := FIndex;
end;

{ TKCustomHexEditor }

constructor TKCustomHexEditor.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
  Color := clWindow;
  ControlStyle := [csOpaque, csClickEvents, csDoubleClicks, csCaptureMouse];
  Font.Name := cFontNameDef;
  Font.Style := cFontStyleDef;
  Font.Size := cFontSizeDef;
  Font.Pitch := fpFixed;
  Font.OnChange := FontChange;
  Height := cHeight;
  ParentColor := False;
  ParentFont := False;
  TabStop := True;
  Width := cWidth;
  FAddressCursor := cAddressCursorDef;
  FAddressMode := cAddressModeDef;
  FAddressOffset := cAddressOffsetDef;
  FAddressPrefix := cAddressPrefixDef;
  FAddressSize := cAddressSizeDef;
  FAreaSpacing := cAreaSpacingDef;
  FBuffer := nil;
{$IFNDEF FPC}
  FClipBoardFormat := RegisterClipboardFormat('Any binary data');
{$ENDIF}
  FColors := TKHexEditorColors.Create(Self);
  FCharHeight := 8;
  FCharMapping := DefaultCharMapping;
  FCharSpacing := cCharSpacingDef;
  FCharWidth := 6;
  FDigitGrouping := cDigitGroupingDef;
  FDisabledDrawStyle := cDisabledDrawStyleDef;
  FDrawStyles := cDrawStylesDef;
  FEditArea := eaDigits;
  FLeftChar := 0;
  FLineHeightPercent := cLineHeightPercentDef;
  FLineSize := cLineSizeDef;
  FMouseWheelAccumulator := 0;
  FOptions := [eoGroupUndo];
  FKeyMapping := CreateDefaultKeyMapping;
  FRedoList := TKHexEditorChangeList.Create(Self, nil);
  FScrollBars := ssBoth;
  FScrollSpeed := cScrollSpeedDef;
  FScrollTimer := TTimer.Create(Self);
  FScrollTimer.Enabled := False;
  FScrollTimer.Interval := FScrollSpeed;
  FScrollTimer.OnTimer := ScrollTimerHandler;
  FSelStart := MakeSelection(0, 0);
  FSelEnd := MakeSelection(0, 0);
  FStates := [];
  FTopLine := 0;
  FTotalCharSpacing := 0;
  FUndoList := TKHexEditorChangeList.Create(Self, FRedoList);
  FUndoList.OnChange := UndoChange;
  FOnChange := nil;
  FOnReplaceText := nil;
  UpdateCharMetrics;
end;

destructor TKCustomHexEditor.Destroy;
begin
  inherited;
  FOnChange := nil;
  FColors.Free;
  FUndoList.Free;
  FRedoList.Free;
  FreeMem(FBuffer);
  FBuffer := nil;
end;

procedure TKCustomHexEditor.AddUndoCaretPos(Force: Boolean);
begin
  FUndoList.AddChange(crCaretPos, '', Force);
end;

procedure TKCustomHexEditor.AddUndoByte(ItemReason: TKHexEditorChangeReason; Data: Byte;
  Inserted: Boolean = True);
begin
  FUndoList.AddChange(ItemReason, AnsiChar(Data), Inserted);
end;

procedure TKCustomHexEditor.AddUndoBytes(ItemReason: TKHexEditorChangeReason;
  Data: PBytes; Length: Integer; Inserted: Boolean = True);
var
  S: AnsiString;
begin
  if Length > 0 then
  begin
    SetLength(S, Length);
    Move(Data^, S[1], Length);
    FUndoList.AddChange(ItemReason, S, Inserted);
  end;
end;

procedure TKCustomHexEditor.AddUndoString(ItemReason: TKHexEditorChangeReason;
  const S: AnsiString; Inserted: Boolean = True);
begin
  if S <> '' then
    FUndoList.AddChange(ItemReason, S, Inserted);
end;

procedure TKCustomHexEditor.Append(At: Integer; Data: TDataSize);
var
  S: AnsiString;
begin
  if (Data.Size > 0) and (Data.Data <> nil) then
  begin
    SetString(S, PAnsiChar(Data.Data), Data.Size);
    InsertString(At, S, Data.Size);
  end;
end;

procedure TKCustomHexEditor.Append(At: Integer; const Data: AnsiString);
begin
  InsertString(At, Data, Length(Data));
end;

procedure TKCustomHexEditor.Assign(Source: TPersistent);
begin
  if Source is TKCustomHexEditor then with Source as TKCustomHexEditor do
  begin
    Self.AddressCursor := AddressCursor;
    Self.AddressMode := AddressMode;
    Self.AddressPrefix := AddressPrefix;
    Self.AddressSize := AddressSize;
    Self.Align := Align;
    Self.Anchors := Anchors;
    Self.AutoSize := AutoSize;
    Self.BiDiMode := BiDiMode;
    Self.BorderStyle := BorderStyle;
    Self.BorderWidth := BorderWidth;
    Self.CharSpacing := CharSpacing;
    Self.Color := Color;
    Self.Colors := Colors;
    Self.Constraints.Assign(Constraints);
  {$IFNDEF FPC}
    Self.Ctl3D := Ctl3D;
  {$ENDIF}
    Self.Data := Data;
    Self.DigitGrouping := DigitGrouping;
    Self.DisabledDrawStyle := DisabledDrawStyle;
    Self.DragCursor := DragCursor;
    Self.DragKind := DragKind;
    Self.DragMode := DragMode;
    Self.DrawStyles := DrawStyles;
    Self.EditArea := EditArea;
    Self.Enabled := Enabled;
    Self.Font := Font;
  {$IFNDEF FPC}
    Self.ImeMode := ImeMode;
    Self.ImeName := ImeName;
  {$ENDIF}
    Self.LineHeightPercent := LineHeightPercent;
    Self.LineSize := LineSize;
    Self.Modified := False;
    Self.Options := Options;
    Self.ParentBiDiMode := ParentBiDiMode;
    Self.ParentColor := ParentColor;
  {$IFNDEF FPC}
    Self.ParentCtl3D := ParentCtl3D;
  {$ENDIF}
    Self.ParentFont := ParentFont;
    Self.ParentShowHint := ParentShowHint;
    Self.PopupMenu := PopupMenu;
    Self.ScrollBars := ScrollBars;
    Self.SelEnd := SelEnd;
    Self.SelStart := SelStart;
    Self.SetCharMapping(GetCharMapping);
    Self.SetKeyMapping(GetKeyMapping);
    Self.ShowHint := ShowHint;
    Self.TabOrder := TabOrder;
    Self.TabStop := TabStop;
    Self.Visible := Visible;
  end
  else
    inherited;
end;

procedure TKCustomHexEditor.BeginUndoGroup(GroupReason: TKHexEditorChangeReason);
begin
  FUndoList.BeginGroup(GroupReason);
end;

procedure TKCustomHexEditor.BufferChanged;
begin
  FUndoList.Clear;
  FRedoList.Clear;
  UpdateScrollRange;
  SelectionChanged(False);
  DoChange;
end;

function TKCustomHexEditor.CanScroll(Command: TKEditCommand): Boolean;
var
  XMax, YMax: Integer;
  P: TPoint;
  AD: TKHExEditorAreaDimensions;
begin
  AD := GetAreaDimensions;
  XMax := GetMaxLeftChar(AD.TotalHorz);
  YMax := GetMaxTopLine(AD.TotalVert);
  case Command of
    ecScrollUp:  Result := FTopLine > 0;
    ecScrollDown: Result := FTopLine < YMax;
    ecScrollLeft: Result := FLeftChar > 0;
    ecScrollRight: Result := FLeftChar < XMax;
    ecScrollCenter:
    begin
      P := SelToPoint(FSelEnd, FEditArea);
      P.X := P.X - ClientWidth div 2;
      P.Y := P.Y - ClientHeight div 2;
      Result := (FLeftChar > 0) and (P.X < 0) or (FLeftChar < XMax) and (P.X > FCharWidth) or
        (FTopLine > 0) and (P.Y < 0) or (FTopLine < YMax) and (P.Y > FCharHeight);
    end;
  else
    Result := False;
  end;
end;

function TKCustomHexEditor.CaretInView: Boolean;
begin
  Result := PtInRect(GetModifiedClientRect, SelToPoint(FSelEnd, FEditArea));
end;

procedure TKCustomHexEditor.Clear;
begin
  if FBuffer <> nil then
  begin
    FreeMem(FBuffer);
    FBuffer := nil;
    FSize := 0;
    BufferChanged;
  end;
end;

procedure TKCustomHexEditor.ClearChar(At: Integer);
begin
  ClearString(At, 1);
end;

procedure TKCustomHexEditor.ClearDigitSelection;
begin
  FSelStart.Digit := 0;
  FSelEnd.Digit := 0;
end;

procedure TKCustomHexEditor.ClearString(At, Size: Integer);
begin
  if (FBuffer <> nil) and (Size > 0) and (At >= 0) and (At + Size <= FSize) then
  begin
    Move(FBuffer[At + Size], FBuffer[At], (FSize - At - Size) * SizeOf(Byte));
    Dec(FSize, Size);
    ReallocMem(FBuffer, FSize);
    UpdateScrollRange;
    Invalidate;
  end;
end;

procedure TKCustomHexEditor.ClearUndo;
begin
  FUndoList.Clear;
  FRedoList.Clear;
end;

procedure TKCustomHexEditor.CMEnabledChanged(var Msg: TLMessage);
begin
  inherited;
  UpdateEditorCaret;
  Invalidate;
end;

procedure TKCustomHexEditor.CMSysColorChange(var Msg: TLMessage);
begin
  inherited;
  FColors.ClearBrightColors;
end;

function TKCustomHexEditor.CommandEnabled(Command: TKEditCommand): Boolean;
var
  L: TKHexEditorSelection;
begin
  if Enabled and Visible and not (csDesigning in ComponentState) then
  begin
    L := SelLength;
    case Command of
      // movement commands
      ecLeft, ecSelLeft: Result := (FSelEnd.Index > 0) or (FEditArea = eaDigits) and (FSelEnd.Digit > 0);
      ecRight, ecSelRight: Result := (FEditArea <> eaNone) and (FSelEnd.Index < FSize);
      ecUp, ecSelUp: Result := FSelEnd.Index >= FLineSize;
      ecDown, ecSelDown: Result := (FEditArea <> eaNone) and (FSelEnd.Index < FSize);
      ecLineStart, ecSelLineStart: Result := (FEditArea <> eaNone) and (FSelEnd.Index mod FLineSize > 0);
      ecLineEnd, ecSelLineEnd: Result := (FEditArea <> eaNone) and (FSelEnd.Index mod FLineSize < Min(FLineSize - 1, FSize));
      ecPageUp, ecSelPageUp: Result := FSelEnd.Index >= FlineSize;
      ecPageDown, ecSelPageDown: Result := (FEditArea <> eaNone) and (FSelEnd.Index < FSize div FLineSize * FLineSize);
      ecPageLeft, ecSelPageLeft: Result := (FEditArea <> eaNone) and (GetPageHorz > 0) and (FSelEnd.Index mod FLineSize > 0);
      ecPageRight, ecSelPageRight: Result := (FEditArea <> eaNone) and (GetPageHorz > 0) and (FSelEnd.Index mod FLineSize < Min(FLineSize - 1, FSize));
      ecPageTop, ecSelPageTop: Result := (FEditArea <> eaNone) and (FSelEnd.Index > 0) and (SelToPoint(MakeSelection(FSelEnd.Index, 0), FEditArea).Y div FCharHeight <> 0);
      ecPageBottom, ecSelPageBottom: Result := (FEditArea <> eaNone) and (FSelEnd.Index < FSize) and ((ClientHeight - SelToPoint(MakeSelection(FSelEnd.Index, 0), FEditArea).Y) div FCharHeight - 1 <> 0);
      ecEditorTop, ecSelEditorTop: Result := FSelEnd.Index > 0;
      ecEditorBottom, ecSelEditorBottom: Result := (FEditArea <> eaNone) and (FSelEnd.Index < FSize);
      ecGotoXY, ecSelGotoXY: Result := True;
      // scroll commands
      ecScrollUp, ecScrollDown, ecScrollLeft, ecScrollRight, ecScrollCenter: Result := CanScroll(Command);
      // editing commands
      ecUndo: Result := not ReadOnly and FUndoList.CanPeek;
      ecRedo: Result := not ReadOnly and FRedoList.CanPeek;
      ecCopy, ecCut: Result := not Empty and (not ReadOnly or (Command = ecCopy)) and ((L.Index <> 0) or (L.Digit <> 0));
      ecPaste: Result := not ReadOnly and (FEditArea <> eaNone) and (ClipBoard.FormatCount > 0);
      ecInsertChar: Result := not ReadOnly and (FEditArea <> eaNone);
      ecInsertDigits: Result := not ReadOnly and (FEditArea = eaDigits);
      ecInsertString: Result := not ReadOnly and (FEditArea <> eaNone);
      ecDeleteLastChar: Result := not (Empty or ReadOnly) and (FEditArea <> eaNone) and ((L.Index > 0) or (FSelEnd.Index > 0));
      ecDeleteChar: Result := not (Empty or ReadOnly) and (FEditArea <> eaNone) and ((L.Index > 0) or (FSelEnd.Index < FSize));
      ecDeleteBOL: Result := not (Empty or ReadOnly) and (FEditArea <> eaNone) and ((L.Index > 0) or (FSelEnd.Index mod FLineSize > 0));
      ecDeleteEOL: Result := not (Empty or ReadOnly) and (FEditArea <> eaNone) and ((L.Index > 0) or (FSelEnd.Index mod FLineSize < Min(FLineSize, FSize)));
      ecDeleteLine: Result := not (Empty or ReadOnly) and (FEditArea <> eaNone) and ((L.Index > 0) or (FSelEnd.Index mod FLineSize > 0) or (FSelEnd.Index < FSize));
      ecSelectAll: Result := not (Empty or ReadOnly) and (FEditArea <> eaNone);
      ecClearAll: Result := not (Empty or ReadOnly) and (FEditArea <> eaNone);
      ecClearIndexSelection, ecClearSelection: Result := not (Empty or ReadOnly) and (FEditArea <> eaNone) and (L.Index > 0);
      ecSearch: Result := not Empty;
      ecReplace: Result := not (Empty or ReadOnly);
      ecInsertMode: Result := elOverwrite in FStates;
      ecOverwriteMode: Result := not (elOverwrite in FStates);
    else
      Result := True;
    end;
  end else
    Result := False;
end;

procedure TKCustomHexEditor.CreateParams(var Params: TCreateParams);
begin
  inherited;
  with Params do
  begin
    if FScrollBars in [ssVertical, ssBoth] then Style := Style or WS_VSCROLL;
    if FScrollBars in [ssHorizontal, ssBoth] then Style := Style or WS_HSCROLL;
  end;
end;

procedure TKCustomHexEditor.CreateWnd;
begin
  inherited;
{$IFDEF USE_WINAPI}
  if (eoDropFiles in FOptions) and not (csDesigning in ComponentState) then
    DragAcceptFiles(Handle, TRUE);
{$ENDIF}
end;

procedure TKCustomHexEditor.DestroyWnd;
begin
{$IFDEF USE_WINAPI}
  if (eoDropFiles in FOptions) and not (csDesigning in ComponentState) then
    DragAcceptFiles(Handle, FALSE);
{$ENDIF}
  inherited;
end;

procedure TKCustomHexEditor.DoChange;
begin
  if Assigned(FOnChange) then
    FOnChange(Self);
end;

function TKCustomHexEditor.DoMouseWheel(Shift: TShiftState; WheelDelta: Integer;
  MousePos: TPoint): Boolean;
const
  WHEEL_DIVISOR = 120;
var
  LinesToScroll, WheelClicks: Integer;
begin
  Result := inherited DoMouseWheel(Shift, WheelDelta, MousePos);
  if not Result then
  begin
    if ssCtrl in Shift then
      LinesToScroll := GetModifiedClientRect.Bottom div FCharHeight
    else
      LinesToScroll := 3;
    Inc(FMouseWheelAccumulator, WheelDelta);
    WheelClicks := FMouseWheelAccumulator div WHEEL_DIVISOR;
    FMouseWheelAccumulator := FMouseWheelAccumulator mod WHEEL_DIVISOR;
    ScrollBy(0, - WheelClicks * LinesToScroll, True);
    Result := True;
  end;
end;

procedure TKCustomHexEditor.EditAreaChanged;
begin
  if FEditArea = eaNone then
    FEditArea := eaDigits;
  if not (edAddress in FDrawStyles) and (FEditArea = eaAddress) then
      FEditArea := eaDigits;
  if not (edDigits in FDrawStyles) and (FEditArea = eaDigits) then
      FEditArea := eaText;
  if not (edText in FDrawStyles) and (FEditArea = eaText) then
    if edDigits in FDrawStyles then
      FEditArea := eaDigits
    else
      FEditArea := eaNone;
end;

procedure TKCustomHexEditor.EndUndoGroup;
begin
  FUndoList.EndGroup;
end;

function TKCustomHexEditor.ExecuteCommand(Command: TKEditCommand;
  Data: Pointer): Boolean;
var
  I, J, K, M, N, O: Integer;
  CanInsert, MoreBytes, Found, MatchCase: Boolean;
  C1, C2, C3: AnsiChar;
  S, S_FirstChar, S_LastChar, T: AnsiString;
  P: TPoint;
  Area: TKHexEditorArea;
  L, OldSelStart, OldSelEnd, Sel1, Sel2: TKHexEditorSelection;
  PChI, PChI_First, PChI_Next: PKHexEditorChangeItem;
  PSD: PKEditSearchData;
  ReplaceAction: TKEditReplaceAction;
{$IFNDEF FPC}
  BA: PBytes;
  H: THandle;
{$ENDIF}
begin
  Result := False;
  if CommandEnabled(Command) then
  begin
    Result := True;
    L := SelLength;
    OldSelEnd := FSelEnd;
    OldSelStart := FSelStart;
    case Command of
      ecLeft..ecSelGotoXY: AddUndoCaretPos(False);
    end;  
    case Command of
      ecLeft, ecSelLeft:
      begin
        InternalMoveLeft;
        SelectionChanged(Command <> ecSelLeft);
      end;
      ecRight, ecSelRight:
      begin
        InternalMoveRight;
        SelectionChanged(Command <> ecSelRight);
      end;
      ecUp, ecSelUp:
      begin
        Dec(FSelEnd.Index, FLineSize);
        SelectionChanged(Command <> ecSelUp);
      end;
      ecDown, ecSelDown:
      begin
        Inc(FSelEnd.Index, FLineSize);
        SelectionChanged(Command <> ecSelDown);
      end;
      ecLineStart, ecSelLineStart:
      begin
        FSelEnd := MakeSelection((FSelEnd.Index div FLineSize) * FLineSize, 0);
        SelectionChanged(Command <> ecSelLineStart);
      end;
      ecLineEnd, ecSelLineEnd:
      begin
        FSelEnd := MakeSelection((FSelEnd.Index div FLineSize) * FLineSize + FLineSize - 1, cDigitCount - 1);
        SelectionChanged(Command <> ecSelLineEnd);
      end;
      ecPageUp, ecSelPageUp:
      begin
        Dec(FSelEnd.Index, Min(ClientHeight div FCharHeight, FSelEnd.Index div FLineSize) * FLineSize);
        SelectionChanged(Command <> ecSelPageUp);
      end;
      ecPageDown, ecSelPageDown:
      begin
        Inc(FSelEnd.Index, Min(ClientHeight div FCharHeight, (FSize - FSelEnd.Index) div FLineSize) * FLineSize);
        SelectionChanged(Command <> ecSelPageDown);
      end;
      ecPageLeft, ecSelPageLeft:
      begin
        Dec(FSelEnd.Index, Min(GetPageHorz, FSelEnd.Index mod FLineSize));
        SelectionChanged(Command <> ecSelPageLeft);
      end;
      ecPageRight, ecSelPageRight:
      begin
        Inc(FSelEnd.Index, Min(GetPageHorz, FLineSize - 1 - FSelEnd.Index mod FLineSize));
        SelectionChanged(Command <> ecSelPageRight);
      end;
      ecPageTop, ecSelPageTop:
      begin
        P := SelToPoint(MakeSelection(FSelEnd.Index, 0), FEditArea);
        Dec(FSelEnd.Index, P.Y div FCharHeight * FLineSize);
        SelectionChanged(Command <> ecSelPageTop);
      end;
      ecPageBottom, ecSelPageBottom:
      begin
        P := SelToPoint(MakeSelection(FSelEnd.Index, 0), FEditArea);
        Inc(FSelEnd.Index, ((ClientHeight - P.Y) div FCharHeight - 1) * FLineSize);
        SelectionChanged(Command <> ecSelPageBottom);
      end;
      ecEditorTop, ecSelEditorTop:
      begin
        FSelEnd := MakeSelection(0, 0);
        SelectionChanged(Command <> ecSelEditorTop);
      end;
      ecEditorBottom, ecSelEditorBottom:
      begin
        FSelEnd := MakeSelection(FSize, 0);
        SelectionChanged(Command <> ecSelEditorBottom);
      end;
      ecGotoXY, ecSelGotoXY:
      begin
        Sel1 := PointToSel(PPoint(Data)^, False, Area);
        if Area <> eaNone then
        begin
          FSelEnd := Sel1;
          FEditArea := Area;
          SelectionChanged(Command <> ecSelGotoXY);
        end else
          Result := False;
      end;
      // scroll commands
      ecScrollUp:
      begin
        if (FEditArea <> eaNone) and (SelToPoint(FSelEnd, FEditArea).Y >= GetModifiedClientRect.Bottom - FCharHeight) then
        begin
          ScrollBy(0, -1, False);
          Dec(FSelEnd.Index, FLineSize);
          SelectionChanged(True, False);
          Invalidate;
        end else
          ScrollBy(0, -1, True);
      end;
      ecScrollDown:
      begin
        if (FEditArea <> eaNone) and (SelToPoint(FSelEnd, FEditArea).Y <= GetModifiedClientRect.Top) then
        begin
          ScrollBy(0, 1, False);
          Inc(FSelEnd.Index, FLineSize);
          SelectionChanged(True, False);
          Invalidate;
        end else
          ScrollBy(0, 1, True);
      end;
      ecScrollLeft:
      begin
        if FEditArea <> eaNone then
        begin
          // overscroll check
          P := SelToPoint(MakeSelection(0, 0), FEditArea);
          if P.X < GetModifiedClientRect.Right - FCharWidth then
          begin
            ScrollBy(-1, 0, True);
            P := SelToPoint(FSelEnd, FEditArea);
            if (P.X >= GetModifiedClientRect.Right) and ((FSelEnd.Index mod FLineSize > 0) or (FSelEnd.Digit > 0)) then
              ExecuteCommand(ecLeft)
          end;
        end else
          ScrollBy(-1, 0, True);
      end;
      ecScrollRight:
      begin
        if FEditArea <> eaNone then
        begin
          // overscroll check
          P := SelToPoint(MakeSelection(FLineSize - 1, cDigitCount - 1), FEditArea);
          if P.X > 0 then
          begin
            ScrollBy(1, 0, True);
            P := SelToPoint(FSelEnd, FEditArea);
            if (P.X < 0) and ((FSelEnd.Index mod FLineSize < FLineSize - 1) or (FSelEnd.Digit < cDigitCount - 1)) then
              ExecuteCommand(ecRight)
          end;
        end else
          ScrollBy(1, 0, True);
      end;
      ecScrollCenter:
      begin
        P := SelToPoint(FSelEnd, FEditArea);
        I := (P.X - ClientWidth div 2) div FCharWidth;
        J := (P.Y - ClientHeight div 2) div FCharHeight;
        ScrollBy(I, J, True);
      end;
      // editing commands
      ecUndo:
      begin
        PChI := FUndoList.PeekItem;
        PChI_First := PChI;
        while PChI <> nil do
        begin
          I := Length(PChI.Data);
          J := Min(I, FSize - PChI.SelEnd.Index);
          FRedoList.SetGroupData(PChI.Group, PChI.GroupReason);
          case PChI.ItemReason of
            crCaretPos:
              FRedoList.AddChange(crCaretPos, '');
            crDeleteChar, crDeleteDigits, crDeleteString:
            begin
              if FBuffer <> nil then
              begin
                SetLength(S, J);
                System.Move(FBuffer[PChI.SelEnd.Index], S[1], J);
              end else
                S := '';
              FRedoList.AddChange(OppositeReason(PChI.ItemReason), S, PChI.Inserted);
            end;
            crInsertChar, crInsertDigits, crInsertString:
              FRedoList.AddChange(OppositeReason(PChI.ItemReason), PChI.Data);
          end;
          FSelEnd := PChI.SelEnd;
          FSelStart := PChI.SelStart;
          FEditArea := PChI.EditArea;
          case PChI.ItemReason of
            crDeleteChar, crDeleteDigits, crDeleteString:
            begin
              if PChI.Inserted then
                ClearString(PChI.SelEnd.Index, I)
              else if FBuffer <> nil then
              begin
                System.Move(PChI.Data[1], FBuffer[PChI.SelEnd.Index], J);
                Invalidate;
              end;
            end;
            crInsertChar, crInsertDigits, crInsertString:
              InsertString(GetRealSelStart.Index, PChI.Data, I);
          end;
          EditAreaChanged;
          SelectionChanged(False, False);
          if PChI.ItemReason <> crCaretPos then
            DoChange;
          PChI_Next := FUndoList.PeekItem;
          if (PChI_Next <> nil) and not ((PChI.Group <> 0) and (PChI.Group = PChI_Next.Group) or
            (eoGroupUndo in FOptions) and (PChI_First.GroupReason = PChI_Next.GroupReason)) then
          begin
            FUndoList.PokeItem;
            Break;
          end;
          PChI := PChI_Next;
        end;
        if not CaretInView then
          ExecuteCommand(ecScrollCenter);
      end;
      ecRedo:
      begin
        PChI := FRedoList.PeekItem;
        PChI_First := PChI;
        while PChI <> nil do
        begin
          FUndoList.PokeItem;
          I := Length(PChI.Data);
          Sel1 := GetRealSelStart;
          case PChI.ItemReason of
            crInsertChar, crInsertDigits, crInsertString:
            begin
              if PChI.Inserted then
                InsertString(Sel1.Index, PChI.Data, I)
              else if FBuffer <> nil then
              begin
                System.Move(PChI.Data[1], FBuffer[Sel1.Index], Min(I, FSize - FSelEnd.Index));
                Invalidate;
              end;
            end;
            crDeleteChar, crDeleteDigits, crDeleteString:
              ClearString(Sel1.Index, I);
          end;
          FSelEnd := PChI.SelEnd;
          FSelStart := PChI.SelStart;
          FEditArea := PChI.EditArea;
          EditAreaChanged;
          SelectionChanged(False, False);
          if PChI.ItemReason <> crCaretPos then
            DoChange;
          PChI_Next := FRedoList.PeekItem;
          if (PChI_Next <> nil) and not ((PChI.Group <> 0) and (PChI.Group = PChI_Next.Group) or
            (eoGroupUndo in FOptions) and (PChI_First.GroupReason = PChI_Next.GroupReason)) then
          begin
            FRedoList.PokeItem;
            Break;
          end;
          PChI := PChI_Next;
        end;
        if not CaretInView then
          ExecuteCommand(ecScrollCenter);
      end;
      ecCopy:
      begin
        Sel1 := GetRealSelStart;
        Sel2 := GetRealSelEnd;
      {$IFDEF FPC}
        ClipBoard.AsText := string(BinaryToDigits(FBuffer, Sel1, Sel2))
      {$ELSE}
        if FEditArea = eaDigits then
          ClipBoard.AsText := string(BinaryToDigits(FBuffer, Sel1, Sel2))
        else if L.Index <> 0 then
        begin
          S := BinaryToText(FBuffer, Sel1.Index, Sel2.Index, @FCharMapping);
          H := GlobalAlloc(GMEM_MOVEABLE + GMEM_DDESHARE, L.Index);
          try
            BA := GlobalLock(H);
            try
              System.Move(FBuffer[Sel1.Index], BA^, L.Index);
            finally
              GlobalUnlock(H);
            end;
            ClipBoard.Open;
            try
              ClipBoard.SetAsHandle(FClipboardFormat, H);
              ClipBoard.AsText := string(S);
            finally
              ClipBoard.Close;
            end;
          except
            GlobalFree(H);
          end;
        end;
      {$ENDIF}
      end;
      ecCut:
      begin
        ExecuteCommand(ecCopy);
        ExecuteCommand(ecClearSelection);
      end;
      ecPaste:
      begin
        if L.Index > 0 then
          ExecuteCommand(ecClearSelection);
        if ClipBoard.FormatCount > 0 then
        begin
          S := '';
        {$IFNDEF FPC}
          H := 0;
          // paste as binary data
          if ClipBoard.HasFormat(FClipboardFormat) then
            H := ClipBoard.GetAsHandle(FClipboardFormat) else
        {$ENDIF}
          if ClipBoard.HasFormat(CF_TEXT) then
          begin
            S := AnsiString(ClipBoard.AsText);
            if S <> '' then
            begin
              M := Length(S);
              if (FEditArea = eaDigits) and ExecuteCommand(ecInsertDigits, Pointer(S)) then
              begin
                S := '';
                if M >= cDigitCount then
                begin
                  Inc(FSelEnd.Index, M div cDigitCount)
                end else
                begin
                  Inc(FSelEnd.Digit, M);
                  if FSelEnd.Digit >= cDigitCount then
                  begin
                    Inc(FSelEnd.Index);
                    FSelEnd.Digit := FSelEnd.Digit mod cDigitCount;
                  end;
                end;
                SelectionChanged(True);
              end else
                ExecuteCommand(ecInsertString, Pointer(S));
            end;
          end
        {$IFNDEF FPC}
          else
            H := ClipBoard.GetAsHandle(ClipBoard.Formats[0]);
          if H <> 0 then
          begin
            BA := GlobalLock(H);
            try
              I := GlobalSize(H);
              if I > 0 then
              begin
                SetLength(S, I);
                System.Move(BA^, S[1], I);
              end;
            finally
              GlobalUnlock(H);
            end;
            if S <> '' then
              ExecuteCommand(ecInsertString, Pointer(S));
          end
        {$ENDIF}
          ;
          if S <> '' then
          begin
            Inc(FSelEnd.Index, Length(S));
            FSelEnd.Digit := 0;
            SelectionChanged(True);
          end;
        end;
      end;
      ecInsertChar:
      begin
        BeginUndoGroup(crInsertChar);
        try
          N := PByte(Data)^;
          if L.Index > 0 then
            ExecuteCommand(ecClearSelection);
          ValidateSelection(FSelEnd, FEditArea);
          if FBuffer <> nil then
            I := FBuffer[FSelEnd.Index]
          else
            I := 0;
          CanInsert := (FBuffer = nil) or (FSelEnd.Digit = 0) and
            (not (elOverwrite in FStates) or (FSelEnd.Index = FSize));
          AddUndoByte(crDeleteChar, I, CanInsert);
          if CanInsert then
            InsertChar(FSelEnd.Index, 0)
          else
            Invalidate;
          case FEditArea of
            eaDigits:
            begin
              FBuffer[FSelEnd.Index] := ReplaceDigit(FBuffer[FSelEnd.Index], N, FSelEnd.Digit);
              InternalMoveRight;
            end;
            eaText:
            begin
              FBuffer[FSelEnd.Index] := N;
              InternalMoveRight;
            end;
          end;
          SelectionChanged(True);
        finally
          EndUndoGroup;
        end;
      end;
      ecInsertDigits:
      begin
        S := AnsiString(Data);
        if (S <> '') and DigitsToBinStr(S) then
        begin
          BeginUndoGroup(crInsertDigits);
          try
            if L.Index > 0 then
              ExecuteCommand(ecClearSelection);
            ValidateSelection(FSelEnd, FEditArea);
            MoreBytes := Length(S) >= cDigitCount;
            if MoreBytes then
              // we don't move digit positions of the remaining block
              SetLength(S, Length(S) div cDigitCount * cDigitCount);
            J := 0;
            if (FBuffer <> nil) and (not MoreBytes or (FSelEnd.Digit > 0)) then
            begin
              I := FBuffer[FSelEnd.Index];
              S_FirstChar := AnsiChar(I);
              S_LastChar := S_FirstChar;
              // split current byte
              AddUndoByte(crInsertChar, I);
              ClearChar(FSelEnd.Index);
              N := Length(S);
              for I := FSelEnd.Digit to cDigitCount - 1 do
              begin
                if J < N then
                begin
                  Inc(J);
                  S_FirstChar := AnsiChar(ReplaceDigit(Ord(S_FirstChar[1]), Ord(S[J]), I));
                end else
                  Break;
              end;
              K := Length(S);
              if K > J then
                for I := FSelEnd.Digit - 1 downto 0 do
                begin
                  if K > J then
                  begin
                    S_LastChar := AnsiChar(ReplaceDigit(Ord(S_LastChar[1]), Ord(S[K]), I));
                    Dec(K);
                  end else
                    Break;
                end
              else
                S_LastChar := '';
              O := cDigitCount;
            end else
            begin
              S_FirstChar := '';
              S_LastChar := '';
              O := 0;
            end;
            T := '';
            if MoreBytes then
            begin
              N := Length(S) - O;
              O := J;
              for I := 0 to N div cDigitCount - 1 do
              begin
                K := 0;
                for J := 1 to cDigitCount do
                begin
                  K := K * cBase;
                  M := I * 2 + J + O;
                  Inc(K, Ord(S[M]));
                end;
                T := AnsiString(Format('%s%s', [T, Char(K)]));
              end;
            end;
            S := S_FirstChar + T + S_LastChar;
            // always insert (don't overwrite)
            AddUndoString(crDeleteDigits, S);
            InsertString(FSelEnd.Index, S, Length(S));
            SelectionChanged(True);
          finally
            EndUndoGroup;
          end;
        end else
          Result := False;
      end;
      ecInsertString:
      begin
        S := AnsiString(Data);
        if S <> '' then
        begin
          BeginUndoGroup(crInsertString);
          try
            if L.Index > 0 then
              ExecuteCommand(ecClearIndexSelection);
            // always insert (don't overwrite)
            AddUndoString(crDeleteString, S);
            InsertString(FSelEnd.Index, S, Length(S));
            SelectionChanged(True);
          finally
            EndUndoGroup;
          end;
        end else
          Result := False;
      end;
      ecDeleteLastChar:
      begin
        if L.Index <> 0 then ExecuteCommand(ecClearSelection) else
        begin
          BeginUndoGroup(crDeleteString);
          try
            AddUndoCaretPos;
            FSelStart.Index := FSelEnd.Index - 1;
            ExecuteCommand(ecClearIndexSelection)
          finally
            EndUndoGroup;
          end;
        end;
      end;
      ecDeleteChar:
      begin
        if L.Index <> 0 then ExecuteCommand(ecClearSelection) else
        begin
          BeginUndoGroup(crDeleteString);
          try
            AddUndoCaretPos;
            FSelStart.Index := FSelEnd.Index + 1;
            ExecuteCommand(ecClearIndexSelection)
          finally
            EndUndoGroup;
          end;
        end;
      end;
      ecDeleteBOL:
      begin
        if L.Index <> 0 then ExecuteCommand(ecClearSelection) else
        begin
          BeginUndoGroup(crDeleteString);
          try
            AddUndoCaretPos;
            FSelStart.Index := (FSelEnd.Index div FLineSize) * FLineSize;
            ExecuteCommand(ecClearIndexSelection)
          finally
            EndUndoGroup;
          end;
        end;
      end;
      ecDeleteEOL:
      begin
        if L.Index <> 0 then ExecuteCommand(ecClearSelection) else
        begin
          BeginUndoGroup(crDeleteString);
          try
            AddUndoCaretPos;
            FSelStart.Index := Min((FSelEnd.Index div FLineSize + 1) * FLineSize, FSize);
            ExecuteCommand(ecClearIndexSelection)
          finally
            EndUndoGroup;
          end;
        end;
      end;
      ecDeleteLine:
      begin
        if L.Index <> 0 then ExecuteCommand(ecClearSelection) else
        begin
          BeginUndoGroup(crDeleteString);
          try
            AddUndoCaretPos;
            FSelStart.Index := (FSelEnd.Index div FLineSize) * FLineSize;
            FSelEnd.Index := Min(FSelStart.Index + FLineSize, FSize);
            ExecuteCommand(ecClearIndexSelection)
          finally
            EndUndoGroup;
          end;
        end;
      end;
      ecSelectAll:
      begin
        AddUndoCaretPos;
        FSelStart := MakeSelection(0, 0);
        FSelEnd := MakeSelection(FSize, 0);
        SelectionChanged(False);
      end;
      ecClearAll:
      begin
        ExecuteCommand(ecSelectAll);
        ExecuteCommand(ecClearIndexSelection);
      end;
      ecClearIndexSelection:
      begin
        I := GetRealSelStart.Index;
        AddUndoBytes(crInsertString, PBytes(@FBuffer[I]), L.Index, True);
        ClearString(I, L.Index);
        FSelEnd := MakeSelection(I, 0);
        SelectionChanged(True);
      end;
      ecClearSelection:
      begin
        Sel1 := GetRealSelStart;
        Sel2 := GetRealSelEnd;
        if (Sel1.Digit > 0) {and (Sel1.Digit + Sel2.Digit = cDigitCount) }then
        begin
          BeginUndoGroup(crDeleteDigits);
          try
            // digit clear mode
            AddUndoCaretPos;
            FSelEnd := MakeSelection(Sel1.Index + 1, 0);
            FSelStart := FSelEnd;
            if Sel2.Digit = 0 then
            begin
              Dec(L.Index);
              N := FBuffer[Sel2.Index - 1];
            end else
              N := FBuffer[Sel2.Index];
            AddUndoBytes(crInsertDigits, PBytes(@FBuffer[FSelEnd.Index]), L.Index, True);
            ClearString(FSelEnd.Index, L.Index);
            FSelEnd := Sel1;
            AddUndoByte(crDeleteChar, FBuffer[Sel1.Index], False);
            for I := Sel1.Digit to cDigitCount - 1 do
            begin
              FBuffer[Sel1.Index] := ReplaceDigit(FBuffer[Sel1.Index], N mod cBase, I);
              N := N div cBase;
            end;
            SelectionChanged(True);
          finally
            EndUndoGroup;
          end;
        end else
          ExecuteCommand(ecClearIndexSelection);
      end;
      ecSearch, ecReplace:
      begin
        // doesn't search for single digits
        PSD := Data;
        if PSD <> nil then
        begin
          PSD.ErrorReason := eseOk;
          S := AnsiString(PSD.TextToFind);
          if Command = ecReplace then
          begin
            T := AnsiString(PSD.TextToReplace);
            ReplaceAction := eraYes;
          end;
          if esoSelectedOnly in PSD.Options then
            if esoFirstSearch in PSD.Options then
            begin
              PSD.SelStart := GetRealSelStart.Index;
              PSD.SelEnd := GetRealSelEnd.Index;
            end else
            begin
              PSD.SelStart := MinMax(PSD.SelStart, 0, FSize);
              PSD.SelEnd := MinMax(PSD.SelEnd, 0, FSize);
            end;
          if esoFirstSearch in PSD.Options then
            Exclude(PSD.Options, esoWereDigits);
          if esoTreatAsDigits in PSD.Options then
          begin
            if DigitsToBinStr(S) then
            begin
              S := BinStrToBinary(S);
              if Command = ecReplace then
              begin
                if DigitsToBinStr(T) then
                begin
                  T := BinStrToBinary(T);
                  PSD.TextToFind := string(S);
                  PSD.TextToReplace := string(T);
                  Exclude(PSD.Options, esoTreatAsDigits);
                  Include(PSD.Options, esoWereDigits);
                end else
                  PSD.ErrorReason := eseNoDigitsReplace;
              end else
              begin
                PSD.TextToFind := string(S);
                Exclude(PSD.Options, esoTreatAsDigits);
                Include(PSD.Options, esoWereDigits);
              end;
            end else
              PSD.ErrorReason := eseNoDigitsFind;
          end;
          if PSD.ErrorReason = eseOk then
          begin
            N := Length(S);
            if esoBackwards in PSD.Options then
            begin
              O := -1;
              if (esoEntireScope in PSD.Options) and (esoFirstSearch in PSD.Options) then
                I := FSize
              else
                I := GetRealSelStart.Index - 1;
              if esoSelectedOnly in PSD.Options then
              begin
                M := PSD.SelStart;
                if esoFirstSearch in PSD.Options then
                  I := PSD.SelEnd
              end else
                M := 0;
              I := Min(I, FSize - N);
              if I < M then
                PSD.ErrorReason := eseNoMatch
            end else
            begin
              O := 1;
              if (esoEntireScope in PSD.Options) and (esoFirstSearch in PSD.Options) then
                I := 0
              else
                I := GetRealSelEnd.Index;
              if esoSelectedOnly in PSD.Options then
              begin
                M := PSD.SelEnd;
                if esoFirstSearch in PSD.Options then
                  I := PSD.SelStart
              end else
                M := FSize;
              M := Min(M, FSize - N);
              if I >= M then
                PSD.ErrorReason := eseNoMatch
            end;
            if PSD.ErrorReason = eseOk then
            begin
              Found := False;
              MatchCase := PSD.Options * [esoMatchCase, esoWereDigits] <> [];
              if MatchCase then
                C1 := S[1]
              else
                C1 := UpCase(S[1]);
              I := MinMax(I, 0, FSize - 1);
              while I <> M do
              begin
                if MatchCase then
                  C2 := AnsiChar(FBuffer[I])
                else
                  C2 := UpCase(AnsiChar(FBuffer[I]));
                if C1 = C2 then
                begin
                  if FSize - I >= N then
                  begin
                    J := 2;
                    Dec(I);
                    while (J <= N) do
                    begin
                      if MatchCase then
                      begin
                        C2 := AnsiChar(FBuffer[I + J]);
                        C3 := S[J];
                      end else
                      begin
                        C2 := Upcase(AnsiChar(FBuffer[I + J]));
                        C3 := Upcase(S[J]);
                      end;
                      if C2 = C3 then
                        Inc(J)
                      else
                        Break;
                    end;
                    Inc(I);
                    if J = N + 1 then
                    begin
                      Found := True;
                      FSelStart := MakeSelection(I, 0);
                      FSelEnd := MakeSelection(I + N, 0);
                      if Command = ecReplace then
                      begin
                        if (esoPrompt in PSD.Options) and Assigned(FOnReplaceText) then
                        begin
                          SelectionChanged(False, False);
                          if not CaretInView then
                            ExecuteCommand(ecScrollCenter);
                          FOnReplaceText(Self, string(S), string(T), ReplaceAction)
                        end else
                          ReplaceAction := eraYes;
                        case ReplaceAction of
                          eraCancel: Break;
                          eraYes, eraAll:
                          begin
                            if T = '' then
                              ExecuteCommand(ecClearIndexSelection)
                            else
                              ExecuteCommand(ecInsertString, Pointer(T));
                            FSelEnd := MakeSelection(I + Length(T), 0);
                            AddUndoCaretPos;
                            if ReplaceAction  = eraAll then
                              Include(PSD.Options, esoAll);
                          end;
                        end;
                        if not (esoAll in PSD.Options) then
                          Break;
                      end else
                        Break;
                    end
                  end;
                end;
                Inc(I, O);
              end;
              if Found then
              begin
                SelectionChanged(False, False);
                if not CaretInView then
                  ExecuteCommand(ecScrollCenter);
              end else
                PSD.ErrorReason := eseNoMatch;
            end;
          end;
          Exclude(PSD.Options, esoFirstSearch);
        end else
          Result := False;
      end;
      ecInsertMode:
      begin
        Exclude(FStates, elOverwrite);
        UpdateEditorCaret(True);
      end;
      ecOverwriteMode:
      begin
        Include(FStates, elOverwrite);
        UpdateEditorCaret(True);
      end;
      ecToggleMode:
      begin
        if elOverwrite in FStates then
          Exclude(FStates, elOverwrite)
        else
          Include(FStates, elOverwrite);
        UpdateEditorCaret(True);
      end;
      // focus change
      ecGotFocus,
      ecLostFocus:
      begin
        UpdateEditorCaret;
        Invalidate;
      end;
    end;
    if (OldSelStart.Index <> OldSelEnd.Index) or (FSelStart.Index <> FSelEnd.Index) or
      (OldSelStart.Digit <> OldSelEnd.Digit) or (FSelStart.Digit <> FSelEnd.Digit) or
      not (elCaretVisible in FStates) and (edInactiveCaret in FDrawStyles) and
      ((FSelStart.Index <> OldSelStart.Index) or (FSelStart.Digit <> OldSelStart.Digit) or
      (FSelEnd.Index <> OldSelEnd.Index) or (FSelEnd.Digit <> OldSelEnd.Digit)) then
      Invalidate;
  end;
end;

procedure TKCustomHexEditor.FontChange(Sender: TObject);
begin
  if not (csDestroying in ComponentState) then
  begin
    Font.Pitch := fpFixed;
    if Font.Size >= 0 then
      Font.Size := MinMax(Font.Size, cFontSizeMin, cFontSizeMax);
    UpdateCharMetrics;
    UpdateScrollRange;
  end;
end;

function TKCustomHexEditor.GetAreaDimensions: TKHexEditorAreaDimensions;
begin
  FillChar(Result, SizeOf(Result), 0);
  with Result do
  begin
    if edAddress in FDrawStyles then
    begin
      Address := Length(FAddressPrefix) + FAddressSize;
      if FDrawStyles * [edDigits, edText] <> [] then
        AddressOut := FAreaSpacing;
    end;
    if edDigits in FDrawStyles then
    begin
      Digits := FLineSize * cDigitCount + FLineSize div FDigitGrouping;
      if FLineSize mod FDigitGrouping = 0 then
        Dec(Digits);
      if edAddress in FDrawStyles then
        DigitsIn := FAreaSpacing;
      if edText in FDrawStyles then
        DigitsOut := FAreaSpacing;
    end;
    if edText in FDrawStyles then
    begin
      Text := FLineSize;
      if FDrawStyles * [edAddress, edDigits] <> [] then
        TextIn := FAreaSpacing;
    end;
    TotalHorz := Address + AddressOut + Digits + DigitsIn + DigitsOut + Text + TextIn;
    if [edAddress, edDigits, edText] * FDrawStyles <> [] then
      TotalVert := LineCount
    else
      TotalVert := 0;
  end;
end;

function TKCustomHexEditor.GetCaretVisible: Boolean;
begin
  Result := elCaretVisible in FStates;
end;

function TKCustomHexEditor.GetCharMapping: TKEditCharMapping;
begin
  Result := FCharMapping;
end;

function TKCustomHexEditor.GetClientHeightChars: Integer;
begin
  Result := ClientHeight div FCharHeight;
end;

function TKCustomHexEditor.GetClientWidthChars: Integer;
begin
  Result := ClientWidth div FCharWidth;
end;

function TKCustomHexEditor.GetCommandKey(Index: TKEditCommand): TKEditKey;
var
  I: Integer;
begin
  Result.Key := 0;
  Result.Shift := [];
  for I := 0 to Length(FKeyMapping) - 1 do
    if FKeyMapping[I].Command = Index then
    begin
      Result := FKeyMapping[I].Key;
      Exit;
    end;
end;

function TKCustomHexEditor.GetData: TDataSize;
begin
  Result.Data := FBuffer;
  Result.Size := FSize;
end;

function TKCustomHexEditor.GetEmpty: Boolean;
begin
  Result := FBuffer = nil;
end;

function TKCustomHexEditor.GetFirstVisibleIndex: Integer;
begin
  Result := PointToSel(Point(0, 0), False, FEditArea).Index;
end;

function TKCustomHexEditor.GetInsertMode: Boolean;
begin
  Result := not (elOverwrite in FStates);
end;

function TKCustomHexEditor.GetKeyMapping: TKEditKeyMapping;
begin
  Result := FKeyMapping;
end;

function TKCustomHexEditor.GetLastVisibleIndex: Integer;
begin
  Result := PointToSel(GetModifiedClientRect.BottomRight, False, FEditArea).Index;
end;

function TKCustomHexEditor.GetLineCount: Integer;
begin
  Result := DivUp(FSize + 1, FLineSize);
end;

function TKCustomHexEditor.GetLines(Index: Integer): TDataSize;
var
  I: Integer;
begin
  I := Index * FLineSize;
  if (FBuffer <> nil) and (I >= 0) and (I < FSize) then
  begin
    Result.Data := @FBuffer[I];
    Result.Size := Min(FLineSize, FSize - I);
  end else
  begin
    Result.Data := nil;
    Result.Size := 0;
  end;
end;

function TKCustomHexEditor.GetModified: Boolean;
begin
  Result := (elModified in FStates) or FUndoList.Modified;
end;

function TKCustomHexEditor.GetModifiedClientRect: TRect;
begin
  Result := Rect(0, 0, GetClientWidthChars * FCharWidth, GetClientHeightChars * FCharHeight);
end;

function TKCustomHexEditor.GetMaxLeftChar(Extent: Integer): Integer;
begin
  if Extent <= 0 then
    Extent := GetAreaDimensions.TotalHorz;
  Result := Max(Extent - GetClientWidthChars, 0);
end;

function TKCustomHexEditor.GetMaxTopLine(Extent: Integer): Integer;
begin
  if Extent <= 0 then
    Extent := GetAreaDimensions.TotalVert;
  Result := Max(Extent - GetClientHeightChars, 0);
end;

function TKCustomHexEditor.GetPageHorz: Integer;
begin
  case FEditArea of
    eaDigits: Result := ClientWidth * FDigitgrouping div (FCharWidth * (cDigitCount * FDigitGrouping + 1));
    eaText: Result := ClientWidth div FCharWidth;
  else
    Result := 0;
  end;
end;

function TKCustomHexEditor.GetReadOnly: Boolean;
begin
  Result := elReadOnly in FStates;
end;

function TKCustomHexEditor.GetRealSelEnd: TKHexEditorSelection;
begin
  if FSelStart.Index <= FSelEnd.Index then
    Result := FSelEnd
  else
    Result := FSelStart;
end;

function TKCustomHexEditor.GetRealSelStart: TKHexEditorSelection;
begin
  if FSelStart.Index <= FSelEnd.Index then
    Result := FSelStart
  else
    Result := FSelEnd;
end;

function TKCustomHexEditor.GetSelLength: TKHexEditorSelection;
begin
  if FSelStart.Index <= FSelEnd.Index then
    Result.Index := FSelEnd.Index - FSelStart.Index
  else
    Result.Index := FSelStart.Index - FSelEnd.Index;
  if FSelStart.Digit <= FSelEnd.Digit then
    Result.Digit := FSelEnd.Digit - FSelStart.Digit
  else
    Result.Digit := FSelStart.Digit - FSelEnd.Digit;
end;

function TKCustomHexEditor.GetSelText: TKHexEditorSelText;
var
  L, Sel1, Sel2: TKHexEditorSelection;
begin
  L := SelLength;
  with Result do
  begin
    if L.Index > 0 then
    begin
      Sel1 := GetRealSelStart;
      Sel2 := GetRealSelEnd;
      AsBinaryRaw  := BinaryToText(FBuffer, Sel1.Index, Sel2.Index, nil);
      AsBinaryMapped := BinaryToText(FBuffer, Sel1.Index, Sel2.Index, @FCharMapping);
      AsDigits := BinaryToDigits(FBuffer, Sel1, Sel2);
      Sel1.Digit := 0;
      Sel2.Digit := 0;
      AsDigitsByteAligned := BinaryToDigits(FBuffer, Sel1, Sel2);
    end else
    begin
      AsBinaryRaw := '';
      AsBinaryMapped := '';
      AsDigits := '';
      AsDigitsByteAligned := '';
    end;
  end;
end;

function TKCustomHexEditor.GetUndoLimit: Integer;
begin
  Result := FUndoList.Limit;
end;

function TKCustomHexEditor.HasFocus: Boolean;
var
  Form: TCustomForm;
begin
  Form := GetParentForm(Self);
  if (Form <> nil) and Form.Visible and Form.Enabled and Form.Active then
    Result := (Form.ActiveControl = Self)
  else
    Result := False;
end;

procedure TKCustomHexEditor.InsertChar(At: Integer; Value: Byte);
begin
  InsertString(At, AnsiChar(Value), 1);
end;

procedure TKCustomHexEditor.InsertString(At: Integer; const Value: AnsiString; Size: Integer);
begin
  if (At >= 0) and (At <= FSize) and (Length(Value) > 0) then
  begin
    Inc(FSize, Size);
    ReallocMem(FBuffer, FSize);
    if At < FSize - Size then
      Move(FBuffer[At], FBuffer[At + Size], (FSize - At - Size) * SizeOf(Byte));
    Move(Value[1], FBuffer[At], Size);
    UpdateScrollRange;
  end;
end;

function TKCustomHexEditor.InternalGetSelAvail: Boolean;
begin
  Result := SelAvail;
end;

procedure TKCustomHexEditor.InternalMoveLeft;
begin
  if FEditArea = eaDigits then
  begin
    if FSelEnd.Digit > 0 then
      Dec(FSelEnd.Digit)
    else if FSelEnd.Index > 0 then
    begin
      FSelEnd.Digit := cDigitCount - 1;
      Dec(FSelEnd.Index);
    end
  end else
    Dec(FSelEnd.Index);
end;

procedure TKCustomHexEditor.InternalMoveRight;
begin
  if FEditArea = eaDigits then
  begin
    if (FSelEnd.Index < FSize) and (FSelEnd.Digit < cDigitCount - 1) then
      Inc(FSelEnd.Digit)
    else
    begin
      FSelEnd.Digit := 0;
      Inc(FSelEnd.Index);
    end
  end else
    Inc(FSelEnd.Index);
end;    

function TKCustomHexEditor.IsAddressPrefixStored: Boolean;
begin
  Result := FAddressPrefix <> '0x';
end;

function TKCustomHexEditor.IsDrawStylesStored: Boolean;
begin
  Result := FDrawStyles <> cDrawStylesDef;
end;

function TKCustomHexEditor.IsOptionsStored: Boolean;
begin
  Result := FOptions <> [eoGroupUndo];
end;

procedure TKCustomHexEditor.KeyDown(var Key: Word; Shift: TShiftState);
var
  I: Integer;
  HK: TKEditKey;
begin
  inherited;
  Exclude(FStates, elIgnoreNextChar);
  if not (csDesigning in ComponentState) then
  begin
    for I := 0 to Length(FKeyMapping) - 1 do
    begin
      HK := FKeyMapping[I].Key;
      if (HK.Key = Key) and (HK.Shift = Shift) then
      begin
        ExecuteCommand(FKeyMapping[I].Command);
        Key := 0;
        Include(FStates, elIgnoreNextChar);
        Exit;
      end;
    end;
    if Key = VK_ESCAPE then
      Include(FStates, elIgnoreNextChar);
  end;
end;

procedure TKCustomHexEditor.KeyPress(var Key: Char);
var
  I: Integer;
begin
  inherited;
  if not (csDesigning in ComponentState) then
  begin
    if not (elIgnoreNextChar in FStates) then
    begin
      case FEditArea of
        eaDigits: I := DigitToBin(AnsiChar(Key));
        eaText: I := Ord(Key);
      else
        I := -1;
      end;
      if I >= 0 then
        ExecuteCommand(ecInsertChar, @I);
    end else
      Exclude(FStates, elIgnoreNextChar);
  end;
end;

procedure TKCustomHexEditor.LoadFromFile(const FileName: TFileName);
var
  Stream: TFileStream;
begin
  Stream := TFileStream.Create(FileName, fmOpenRead);
  try
    LoadFromStream(Stream);
  finally
    Stream.Free;
  end;
end;

procedure TKCustomHexEditor.LoadFromStream(Stream: TStream);
var
  Size: Integer;
begin
  Size := Stream.Size - Stream.Position;
  if Size > 0 then
  begin
    Clear;
    FSize := Size;
    GetMem(FBuffer, FSize);
    Stream.Read(FBuffer^, FSize);
    BufferChanged;
  end;
end;

procedure TKCustomHexEditor.MeasurePages(var Info: TKPrintMeasureInfo);
var
  AD: TKHexEditorAreaDimensions;
  PageLines, ActiveLines: Integer;
  FitToPage, SelOnly: Boolean;
  Scale: Double;
  APageSetup: TKPrintPageSetup;
begin
  APageSetup := PageSetup;
  FitToPage := poFitToPage in APageSetup.Options;
  SelOnly := APageSetup.Range = prSelectedOnly;
  Scale := APageSetup.Scale / 100;
  AD := GetAreaDimensions;
  Info.OutlineWidth := AD.TotalHorz * FCharWidth;
  if FitToPage then
    Scale := APageSetup.PaintAreaWidth / Info.OutlineWidth;
  PageLines := Round(APageSetup.PaintAreaHeight / Scale) div FCharHeight;
  if SelOnly then
    ActiveLines := DivUp(GetRealSelEnd.Index, FLineSize) - GetRealSelStart.Index div FLineSize
  else
    ActiveLines := LineCount;
  Info.OutlineHeight := PageLines * FCharHeight;
  Info.HorzPageCount := 1; // cut text off
  Info.VertPageCount := DivUp(ActiveLines, PageLines);
  Info.PageCount := Info.VertPageCount;
end;

procedure TKCustomHexEditor.ModifyScrollBar(ScrollBar, ScrollCode,
  Delta: Integer; UpdateNeeded: Boolean);
var
  I, J, K: Integer;
  HasScrollBar: Boolean;
  SI: TScrollInfo;
begin
  HasScrollBar := (ScrollBar = SB_HORZ) and (ScrollBars = ssHorizontal) or
    (ScrollBar = SB_VERT) and (ScrollBars = ssVertical) or (ScrollBars = ssBoth);
  if HasScrollBar then
  begin
    FillChar(SI, SizeOf(TScrollInfo), 0);
    SI.cbSize := SizeOf(TScrollInfo);
    SI.fMask := SIF_PAGE or SIF_TRACKPOS;
    GetScrollInfo(Handle, ScrollBar, SI);
  {$IF DEFINED(LCLGTK2)}
    {.$WARNING "scrollbar arrows still not working properly on GTK2 in some cases!"}
    SI.nTrackPos := Delta;
  {$IFEND}
  end;
  if ScrollBar = SB_HORZ then
  begin
    I := FLeftChar;
    J := GetMaxLeftChar;
  end else
  begin
    I := FTopLine;
    J := GetMaxTopLine;
  end;
  K := I;
  if ScrollCode = cScrollDelta then
    Inc(I, Delta)
  else if HasScrollBar then
  case ScrollCode of
    SB_LINEUP: Dec(I);
    SB_LINEDOWN: Inc(I);
    SB_PAGEUP: Dec(I, SI.nPage);
    SB_PAGEDOWN: Inc(I, SI.nPage);
    SB_THUMBTRACK, SB_THUMBPOSITION: I := SI.nTrackPos;
  end;
  I := MinMax(I, 0, J);
  if K <> I then
  begin
    if HasScrollBar then
    begin
      FillChar(SI, SizeOf(TScrollInfo), 0);
      SI.nPos := I;
      SI.fMask := SIF_POS;
      SetScrollInfo(Handle, ScrollBar, SI, True);
    end;
    if ScrollBar = SB_HORZ then
      FLeftChar := I
    else
      FTopLine := I;
    if UpdateNeeded then
    begin
      UpdateEditorCaret;
      Invalidate;
    end;  
  end;
end;

procedure TKCustomHexEditor.MouseDown(Button: TMouseButton; Shift: TShiftState;
  X, Y: Integer);
var
  P: TPoint;
  Command: TKEditCommand;
begin
  inherited;
  if Enabled and (Button = mbLeft) and not (ssDouble in Shift) then
  begin
    SafeSetFocus;
    P := Point(X, Y);
    if ssShift in Shift then
      Command := ecSelGotoXY
    else
      Command := ecGotoXY;
    if ExecuteCommand(Command, @P) then
      Include(FStates, elMouseCapture);
  end;
end;

procedure TKCustomHexEditor.MouseMove(Shift: TShiftState; X, Y: Integer);
var
  P: TPoint;
  R: TRect;
begin
  inherited;
  if (elMouseCapture in FStates) then
  begin
    P := Point(X, Y);
    R := GetModifiedClientRect;
    if PtInRect(R, P) then
      UpdateSelEnd(P, False)
    else if not FScrollTimer.Enabled then
      ScrollTo(P, True, False);
  end;
end;

procedure TKCustomHexEditor.MouseUp(Button: TMouseButton; Shift: TShiftState;
  X, Y: Integer);
begin
  inherited;
  Exclude(FStates, elMouseCapture);
end;

procedure TKCustomHexEditor.PaintLines(const Data: TKHexEditorPaintData);
var
  HalfPosWidth, I, Index, J, K, L, M, MaxAddress, WHorz, WVert, WSep,
  LeftIndent, VTextIndent: Integer;
  BC1, BC2, FC1, FC2, PC1: TColor;
  EditorFocused, DrawInactiveCaret, DrawNormal, DigitSep, SelCondition: Boolean;
  S: AnsiString;
  Fmt: string;
  C: Char;
  R, R1, RClip: TRect;
  OldColorScheme: TKHexEditorColorScheme;
  ASelStart, ASelEnd: TKHexEditorSelection;
  AD: TKHexEditorAreaDimensions;
begin
  { this function must be reentrant because of print function
    so there is necessary to backup all changes to global properties}
  OldColorScheme := FColors.ColorScheme;
  with Data.Canvas do
  try
    R := Data.PaintRect;
    AD := GetAreaDimensions;
    // add possible inter-character spacing (in Lazarus not fully implemented yet)
    SetTextCharacterExtra(Handle, Data.CharSpacing);
    LeftIndent := R.Left - Data.LeftChar * Data.CharWidth;
    VTextIndent := (Data.CharHeight - Abs(Font.Height)) div 2;
    HalfPosWidth := Data.CharWidth div 2;
    Fmt := '';
    MaxAddress := 0;
    L := LineCount;
    DrawInactiveCaret := not (Data.Printing or Data.CaretShown) and
      (edInactiveCaret in FDrawStyles);
    DrawNormal := not Data.Printing;
    EditorFocused := HasFocus;
    if FSelStart.Index <= FSelEnd.Index then
    begin
      ASelStart := FSelStart;
      ASelEnd := FSelEnd;
    end else
    begin
      ASelStart := FSelEnd;
      ASelEnd := FSelStart;
    end;
    // preserve space for lines and separators
    if edHorzLines in FDrawStyles then
      WVert := Max(1, Data.CharHeight div 25)
    else
      WVert := 0;
    if edVertLines in FDrawStyles then
      WHorz := Max(1, Data.CharWidth div 20)
    else
      WHorz := 0;
    if edSeparators in FDrawStyles then
      WSep := Max(1, Data.CharWidth div 20)
    else
      WSep := 0;
    // address area pre-comp
    if edAddress in FDrawStyles then
    begin
      if FAddressMode = eamDec then
      begin
        C := 'd';
        J := 10;
      end else
      begin
        C := 'x';
        J := 16;
      end;
      Fmt := Format('%s%%.%d%s', [FAddressPrefix, FAddressSize, C]);
      MaxAddress := 1;
      for I := 1 to FAddressSize do
        MaxAddress := MaxAddress * J;
    end;
    // update color scheme
    if Data.Printing then
    begin
      if Data.PaintColors then
        FColors.ColorScheme := ecsNormal
      else
        FColors.ColorScheme := ecsGrayScale;
    end else
    begin
      if Enabled or (FDisabledDrawStyle = eddNormal) then
        FColors.ColorScheme := ecsNormal
      else if FDisabledDrawStyle = eddGrayed then
        FColors.ColorScheme := ecsGrayed
      else
        FColors.ColorScheme := ecsBright
    end;
    FColors.SingleBkGnd := edSingleBkGnd in FDrawStyles;
    // get clip box for updating;
    if Data.Printing then
      RClip := R
    else
      GetClipBox(Handle, {$IFDEF FPC}@{$ENDIF}RClip);
    // now paint text lines
    for I := Data.TopLine to Min(L - 1, Data.BottomLine) do
    begin
      Brush.Style := bsSolid;
      K := LeftIndent;
      R.Bottom := R.Top + Data.CharHeight - WVert;
      if (R.Top <= RClip.Bottom) and (R.Bottom >= RClip.Top) then
      begin
        if edAddress in FDrawStyles then
        begin
          Index := I * FLineSize;
          Brush.Color := clRed;
          if (DrawNormal or Data.PaintSelection) and ((ASelStart.Index <> ASelEnd.Index) or (ASelStart.Digit <> ASelEnd.Digit)) and
            (Index + FLineSize - 1 >= ASelStart.Index) and (Index < ASelEnd.Index) then
          begin
            PC1 := FColors.LinesHighLight;
            if (FEditArea = eaAddress) and (EditorFocused or Data.PaintSelection) then
            begin
              FC1 := FColors.SelTextFocused;
              BC1 := FColors.SelBkGndFocused;
            end else
            begin
              FC1 := FColors.SelText;
              BC1 := FColors.SelBkGnd;
            end;
          end else
          begin
            PC1 := FColors.HorzLines;
            FC1 := FColors.AddressText;
            BC1 := FColors.AddressBkGnd;
          end;
          Brush.Color := BC1;
          Font.Color := FC1;
          R.Left := K;
          Inc(K, AD.Address * Data.CharWidth);
          R.Right := K;
          J := I * FLineSize + FAddressOffset;
          if MaxAddress <> 0 then J := J mod MaxAddress;
          FillRect(R);
          TextOut(R.Left, R.Top + VTextIndent, Format(Fmt, [J]));
          if edHorzLines in FDrawStyles then
          begin
            Brush.Color := PC1;
            FillRect(Rect(R.Left, R.Bottom, R.Right, R.Bottom + WVert));
          end;
          if AD.AddressOut > 0 then
          begin
            R.Left := K;
            Inc(K, AD.AddressOut * Data.CharWidth);
            R.Right := K;
            Brush.Color := FColors.AddressBkGnd;
            FillRect(Rect(R.Left, R.Top, R.Right - WSep, R.Bottom));
            if edHorzLines in FDrawStyles then
            begin
              Brush.Color := FColors.HorzLines;
              FillRect(Rect(R.Left, R.Bottom, R.Right - WSep, R.Bottom + WVert));
            end;
          end;
        end;
        if edDigits in FDrawStyles then
        begin
          if AD.DigitsIn > 0 then
          begin
            R.Left := K;
            Inc(K, AD.DigitsIn * Data.CharWidth);
            R.Right := K;
            Brush.Color := FColors.DigitBkGnd;
            FillRect(Rect(R.Left + WSep, R.Top, R.Right, R.Bottom));
            if edHorzLines in FDrawStyles then
            begin
              Brush.Color := FColors.HorzLines;
              FillRect(Rect(R.Left + WSep, R.Bottom, R.Right, R.Bottom + WVert));
            end;
          end;
          Index := 0;
          for J := 0 to FLineSize - 1 do
          begin
            Index := I * FLineSize + J;
            DigitSep := (J < FLineSize - 1) and ((J + 1) mod FDigitGrouping = 0);
            R.Left := K;
            Inc(K, cDigitCount * Data.CharWidth);
            R.Right := K;
            if Index <= FSize then
            begin
              if Index < FSize then
                S := AnsiString(Format(cFmtText, [FBuffer[Index]]))
              else
                S := '  ';
              if (Index <> FSelStart.Index) and (Index <> FSelEnd.Index) then
              begin
                SelCondition := (Index >= ASelStart.Index) and (Index < ASelEnd.Index);
                if (DrawNormal or Data.PaintSelection) and SelCondition then
                begin
                  PC1 := FColors.LinesHighLight;
                  if (FEditArea = eaDigits) and (EditorFocused or Data.PaintSelection) then
                  begin
                    FC1 := FColors.SelTextFocused;
                    BC1 := FColors.SelBkGndFocused;
                  end else
                  begin
                    FC1 := FColors.SelText;
                    BC1 := FColors.SelBkGnd;
                  end;
                  FC2 := FColors.InactiveCaretSelText;
                  BC2 := FColors.InactiveCaretSelBkGnd;
                end else
                begin
                  PC1 := FColors.HorzLines;
                  if DrawNormal or Data.PaintAll or SelCondition then
                  begin
                    if (J div FDigitGrouping) and 1 = 0 then
                      FC1 := FColors.DigitTextEven
                    else
                      FC1 := FColors.DigitTextOdd;
                  end else
                    FC1 := FColors.DigitBkGnd;
                  BC1 := FColors.DigitBkGnd;
                  FC2 := FColors.InactiveCaretText;
                  BC2 := FColors.InactiveCaretBkGnd;
                end;
                Brush.Color := BC1;
                Font.Color := FC1;
                Brush.Style := bsSolid;
                FillRect(R);
                Brush.Style := bsClear;
                TextOut(R.Left, R.Top + VTextIndent, Char(S[1]));
                TextOut(R.Left + Data.CharWidth, R.Top + VTextIndent, Char(S[2]));
                if (Index = FSelEnd.Index) and DrawInactiveCaret then
                begin
                  // draw inactive caret - place into previous drawn text
                  R1 := R;
                  Inc(R1.Left, Data.CharWidth * Min(FSelEnd.Digit, cDigitCount - 1));
                  R1.Right := R1.Left + Data.CharWidth;
                  Font.Color := FC2;
                  Brush.Color := BC2;
                  Brush.Style := bsSolid;
                  FillRect(R1);
                  Brush.Style := bsClear;
                  TextOut(R1.Left, R1.Top + VTextIndent, string(S));
                end;
                if edHorzLines in FDrawStyles then
                begin
                  Brush.Color := PC1;
                  Brush.Style := bsSolid;
                  FillRect(Rect(R.Left, R.Bottom, R.Right, R.Bottom + WVert));
                end;
              end else
              begin
                R1 := R;
                R1.Right := R1.Left;
                Inc(R1.Right, Data.CharWidth);
                for M := 0 to cDigitCount - 1 do
                begin
                  SelCondition :=
                    (ASelStart.Index = ASelEnd.Index) and (
                    (M >= ASelStart.Digit) and (M < ASelEnd.Digit) or
                    (M >= ASelEnd.Digit) and (M < ASelStart.Digit)
                    )
                    or
                    (ASelStart.Index <> ASelEnd.Index) and (
                    (Index = ASelStart.Index) and (M >= ASelStart.Digit) or
                    (Index = ASelEnd.Index) and (M < ASelEnd.Digit)
                    );
                  if (DrawNormal or Data.PaintSelection) and SelCondition then
                  begin
                    PC1 := FColors.LinesHighLight;
                    if DrawInactiveCaret and (Index = FSelEnd.Index) and (M = FSelEnd.Digit) then
                    begin
                      FC1 := FColors.InactiveCaretSelText;
                      BC1 := FColors.InactiveCaretSelBkGnd;
                    end
                    else if (FEditArea = eaDigits) and (EditorFocused or Data.PaintSelection) then
                    begin
                      FC1 := FColors.SelTextFocused;
                      BC1 := FColors.SelBkGndFocused;
                    end else
                    begin
                      FC1 := FColors.SelText;
                      BC1 := FColors.SelBkGnd;
                    end;
                  end else
                  begin
                    PC1 := FColors.HorzLines;
                    if DrawInactiveCaret and (Index = FSelEnd.Index) and (M = FSelEnd.Digit) then
                    begin
                      FC1 := FColors.InactiveCaretText;
                      BC1 := FColors.InactiveCaretBkGnd;
                    end else
                    begin
                      if DrawNormal or Data.PaintAll or SelCondition then
                      begin
                        if (J div FDigitGrouping) and 1 = 0 then
                          FC1 := FColors.DigitTextEven
                        else
                          FC1 := FColors.DigitTextOdd;
                      end else
                        FC1 := FColors.DigitBkGnd;
                      BC1 := FColors.DigitBkGnd;
                    end;
                  end;
                  Brush.Color := BC1;
                  Font.Color := FC1;
                  Brush.Style := bsSolid;
                  FillRect(R1);
                  Brush.Style := bsClear;
                  TextOut(R1.Left, R1.Top + VTextIndent, Char(S[M + 1]));
                  if edHorzLines in FDrawStyles then
                  begin
                    Brush.Color := PC1;
                    Brush.Style := bsSolid;
                    FillRect(Rect(R1.Left, R1.Bottom, R1.Right, R1.Bottom + WVert));
                  end;
                  R1.Left := R1.Right;
                  Inc(R1.Right, Data.CharWidth);
                end;
              end;
              if DigitSep then
              begin
                if Index < FSize then
                  M := Data.CharWidth
                else
                  M := HalfPosWidth;
                Brush.Color := FColors.DigitBkGnd;
                Brush.Style := bsSolid;
                FillRect(Rect(R.Right, R.Top, R.Right + Data.CharWidth, R.Bottom));
                if edHorzLines in FDrawStyles then
                begin
                  Brush.Color := FColors.HorzLines;
                  FillRect(Rect(R.Right, R.Bottom, R.Right + M, R.Bottom + WVert));
                end;
                if edVertLines in FDrawStyles then
                begin
                  M := R.Right + HalfPosWidth;
                  Brush.Color := FColors.VertLines;
                  FillRect(Rect(M, R.Top, M + WHorz, R.Bottom));
                end;
                Inc(K, Data.CharWidth);
              end;
            end else
            begin
              Inc(K, Integer(DigitSep) * Data.CharWidth);
              Brush.Color := FColors.DigitBkGnd;
              Brush.Style := bsSolid;
              FillRect(Rect(R.Left, R.Top, K, R.Bottom + WVert));
            end;
          end;
          if AD.DigitsOut > 0 then
          begin
            R.Left := K;
            Inc(K, AD.DigitsOut * Data.CharWidth);
            R.Right := K;
            Brush.Style := bsSolid;
            Brush.Color := FColors.DigitBkGnd;
            FillRect(Rect(R.Left, R.Top, R.Right - WSep, R.Bottom));
            if edHorzLines in FDrawStyles then
            begin
              if Index < FSize then
                Brush.Color := FColors.HorzLines
              else
                Brush.Color := FColors.DigitBkGnd;
              FillRect(Rect(R.Left, R.Bottom, R.Right - WSep, R.Bottom + WVert));
            end;
          end;
        end;
        if edText in FDrawStyles then
        begin
          if AD.TextIn > 0 then
          begin
            R.Left := K;
            Inc(K, AD.TextIn * Data.CharWidth);
            R.Right := K;
            Brush.Color := FColors.TextBkGnd;
            Brush.Style := bsSolid;
            FillRect(Rect(R.Left + WSep, R.Top, R.Right, R.Bottom));
            if edHorzLines in FDrawStyles then
            begin
              Brush.Color := FColors.HorzLines;
              FillRect(Rect(R.Left + WSep, R.Bottom, R.Right, R.Bottom + WVert));
            end;
          end;
          for J := 0 to FLineSize - 1 do
          begin
            Index := I * FLineSize + J;
            R.Left := K;
            Inc(K, Data.CharWidth);
            R.Right := K;
            if Index <= FSize then
            begin
              SelCondition := (Index >= ASelStart.Index) and (Index < ASelEnd.Index);
              if (DrawNormal or Data.PaintSelection) and SelCondition then
              begin
                PC1 := FColors.LinesHighLight;
                if DrawInactiveCaret and (Index = FSelEnd.Index) then
                begin
                  FC1 := FColors.InactiveCaretSelText;
                  BC1 := FColors.InactiveCaretSelBkGnd;
                end
                else if (FEditArea = eaText) and (EditorFocused or Data.PaintSelection) then
                begin
                  FC1 := FColors.SelTextFocused;
                  BC1 := FColors.SelBkGndFocused;
                end else
                begin
                  FC1 := FColors.SelText;
                  BC1 := FColors.SelBkGnd;
                end;
              end else
              begin
                PC1 := FColors.HorzLines;
                if DrawInactiveCaret and (Index = FSelEnd.Index) then
                begin
                  FC1 := FColors.InactiveCaretText;
                  BC1 := FColors.InactiveCaretBkGnd;
                end else
                begin
                  if DrawNormal or Data.PaintAll or SelCondition then
                    FC1 := FColors.TextText
                  else
                    FC1 := FColors.TextBkgnd;
                  BC1 := FColors.TextBkgnd;
                end;
              end;
              Brush.Color := BC1;
              Brush.Style := bsSolid;
              FillRect(R);
              Brush.Style := bsClear;
              if Index < FSize then
              begin
                Font.Color := FC1;
                TextOut(R.Left, R.Top + VTextIndent, Char(FCharMapping[FBuffer[Index]]));
              end;
              if edHorzLines in FDrawStyles then
              begin
                Brush.Color := PC1;
                Brush.Style := bsSolid;
                FillRect(Rect(R.Left, R.Bottom, R.Right, R.Bottom + WVert));
              end;
            end else
            begin
              Brush.Color := FColors.TextBkGnd;
              Brush.Style := bsSolid;
              FillRect(Rect(R.Left, R.Top, K, R.Bottom + WVert));
            end;
          end;
        end;
      end;
      Inc(R.Top, Data.CharHeight);
    end;
    // now complete blank areas below text and optionally paint separators
    K := LeftIndent;
    R.Bottom := Data.PaintRect.Bottom;
    Brush.Style := bsSolid;
    if edAddress in FDrawStyles then
    begin
      R.Left := K;
      Inc(K, (AD.Address + AD.AddressOut) * Data.CharWidth);
      R.Right := K; if FDrawStyles * [edDigits, edText] <> [] then Dec(R.Right, WSep);
      if R.Top < R.Bottom then
      begin
        Brush.Color := FColors.AddressBkGnd;
        FillRect(R);
      end;
      if (edSeparators in FDrawStyles) and (FDrawStyles * [edDigits, edText] <> []) then
      begin
        Brush.Color := FColors.Separators;
        FillRect(Rect(K - WSep, Data.PaintRect.Top, K + WSep, Data.PaintRect.Bottom));
      end;
    end;
    if edDigits in FDrawStyles then
    begin
      R.Left := K; if edAddress in FDrawStyles then Inc(R.Left, WSep);
      Inc(K, (AD.Digits + AD.DigitsIn + AD.DigitsOut) * Data.CharWidth);
      R.Right := K; if edText in FDrawStyles then Dec(R.Right, WSep);
      if R.Top < R.Bottom then
      begin
        Brush.Color := FColors.DigitBkGnd;
        FillRect(R);
      end;
      if (edSeparators in FDrawStyles) and (edText in FDrawStyles) then
      begin
        Brush.Color := FColors.Separators;
        FillRect(Rect(K - WSep, Data.PaintRect.Top, K + WSep, Data.PaintRect.Bottom));
      end;
    end;
    if edText in FDrawStyles then
    begin
      R.Left := K; if FDrawStyles * [edAddress, edDigits] <> [] then Inc(R.Left, WSep);
      Inc(K, (AD.TextIn + AD.Text) * Data.CharWidth);
      R.Right := K;
      if R.Top < R.Bottom then
      begin
        Brush.Color := FColors.TextBkGnd;
        FillRect(R);
      end;
    end;
    if K < ClientWidth then
    begin
      Brush.Color := FColors.BkGnd;
      FillRect(Rect(K, 0, ClientWidth, ClientHeight));
    end;
  finally
    FColors.ColorScheme := OldColorScheme;
  end;
end;

procedure TKCustomHexEditor.PaintPage;
var
  ActiveLines, AreaWidth, AreaHeight, FirstLine, PageLines: Integer;
  SelOnly: Boolean;
  TmpRect, TmpRect1: TRect;
  APageSetup: TKPrintPageSetup;
  Data: TKHexEditorPaintData;
begin
  APageSetup := PageSetup;
  SelOnly := APageSetup.Range = prSelectedOnly;
  AreaWidth := Round(APageSetup.PaintAreaWidth / APageSetup.CurrentScale);
  AreaHeight := Round(APageSetup.PaintAreaHeight / APageSetup.CurrentScale);
  PageLines := AreaHeight div FCharHeight;
  if SelOnly then
  begin
    FirstLine := GetRealSelStart.Index div FLineSize;
    ActiveLines := DivUp(GetRealSelEnd.Index, FLineSize) - FirstLine;
  end else
  begin
    FirstLine := 0;
    ActiveLines := LineCount;
  end;
  TmpRect := Rect(0, 0, APageSetup.OutlineWidth, APageSetup.OutlineHeight);
  TmpRect1 := Rect(0, 0, AreaWidth, AreaHeight);
  IntersectRect(TmpRect, TmpRect, TmpRect1);
  TmpRect1 := TmpRect;
  TranslateRectToDevice(APageSetup.Canvas.Handle, TmpRect1);
  SelectClipRect(APageSetup.Canvas.Handle, TmpRect1);
  Data.Canvas := APageSetup.Canvas;
  Data.Canvas.Font := Font;
  Data.Canvas.Font.Height := Abs(Font.Height);
  Data.PaintRect := TmpRect;
  Data.TopLine := (APageSetup.CurrentPage - 1) * PageLines;
  Data.BottomLine := Min(Data.TopLine + PageLines, ActiveLines) - 1;
  Inc(Data.TopLine, FirstLine);
  Inc(Data.BottomLine, FirstLine);
  Data.LeftChar := 0;
  Data.CharWidth := FCharWidth;
  Data.CharHeight := FCharHeight;
  Data.CharSpacing := FTotalCharSpacing;
  Data.Printing := True;
  Data.PaintSelection := poPaintSelection in APageSetup.Options;
  Data.PaintAll := not SelOnly;
  Data.PaintColors := poUseColor in APageSetup.Options;
  PaintLines(Data);
end;

procedure TKCustomHexEditor.PaintToCanvas(ACanvas: TCanvas);
var
  Data: TKHexEditorPaintData;
begin
  ACanvas.Font := Font;
  with Data do
  begin
    Canvas := ACanvas;
    PaintRect := ClientRect;
    LeftChar := FLeftChar;
    TopLine := FTopLine;
    CharWidth := FCharWidth;
    CharHeight := FCharHeight;
    BottomLine := TopLine + ClientHeight div FCharHeight;
    CharSpacing := FTotalCharSpacing;
    Printing := False;
    PaintSelection := False;
    CaretShown := elCaretVisible in FStates;
  end;
{$IFDEF FPC}
  if Data.CaretShown then
    HideEditorCaret;
  try
{$ENDIF}
    PaintLines(Data);
{$IFDEF FPC}
  finally
    if Data.CaretShown then
      ShowEditorCaret;
  end;
{$ENDIF}
end;

procedure TKCustomHexEditor.PaintToCanvasEx(ACanvas: TCanvas; ARect: TRect; ALeftChar, ATopLine: Integer);
var
  Data: TKHexEditorPaintData;
  Region: HRGN;
begin
  ACanvas.Font := Font;
  with Data do
  begin
    Canvas := ACanvas;
    PaintRect := ARect;
    LeftChar := ALeftChar;
    TopLine := ATopLine;
    CharWidth := FCharWidth;
    CharHeight := FCharHeight;
    BottomLine := TopLine + (ARect.Bottom - ARect.Top) div FCharHeight;
    CharSpacing := FTotalCharSpacing;
    Printing := False;
    PaintSelection := False;
  end;
  Region := CreateRectRgnIndirect(ARect);
  try
    SelectClipRgn(ACanvas.Handle, Region);
    try
      PaintLines(Data);
    finally
      SelectClipRgn(ACanvas.Handle, 0);
    end;
  finally
    DeleteObject(Region);
  end;
end;

function TKCustomHexEditor.PointToSel(P: TPoint; OutOfArea: Boolean; var Area: TKHexEditorArea): TKHexEditorSelection;
var
  Digit, HalfPosWidth, I, X, X1, XMax: Integer;
  DigitSep: Boolean;
  AD: TKHexEditorAreaDimensions;
  Sel: TKHexEditorSelection;
begin
  Result := MakeSelection(cInvalidIndex, 0);
  P.X := P.X + FLeftChar * FCharWidth;
  P.Y := P.Y div FCharHeight + FTopLine;
  AD := GetAreaDimensions;
  HalfPosWidth := FCharWidth div 2;
  X := 0;
  if OutOfArea then
    P.Y := MinMax(P.Y, 0, LineCount - 1)
  else
    Area := eaNone;
  if P.Y < LineCount then
  begin
    if edAddress in FDrawStyles then
    begin
      XMax := X + (AD.Address + AD.AddressOut) * FCharWidth;
      if not OutOfArea or (Area = eaAddress) then
        if (P.X >= X) and (P.X < XMax) then
        begin
          Result := MakeSelection(P.Y * FLineSize, 0);
          Area := eaAddress;
        end
        else if Area = eaAddress then // OutOfArea = True
        begin
          Result.Index := P.Y * FLineSize;
          if P.X >= XMax then
            Inc(Result.Index, FLineSize);
        end;
      X := XMax;
    end;
    if (P.X >= X) or OutOfArea then
    begin
      if edDigits in FDrawStyles then
      begin
        XMax := X + (AD.Digits + AD.DigitsIn + AD.DigitsOut) * FCharWidth;
        if not OutOfArea or (Area = eaDigits) then
          if (P.X >= X) and (P.X < XMax) then
          begin
            Inc(X, AD.DigitsIn * FCharWidth);
            for I := 0 to FLineSize - 1 do
            begin
              DigitSep := (I < FLineSize - 1) and ((I + 1) mod FDigitGrouping = 0);
              X1 := X;
              Inc(X, cDigitCount * FCharWidth);
              if DigitSep then
                Inc(X, HalfPosWidth)
              else if I = FLineSize - 1 then
                Inc(X, AD.DigitsOut * FCharWidth);
              if P.X < X then
              begin
                Digit := (Max(P.X - X1, 0) + HalfPosWidth) div FCharWidth;
                Sel := MakeSelection(P.Y * FLineSize + I, Digit);
                if (Digit >= cDigitCount) and (Sel.Index < FSize) then // don't split the FSize character box
                begin
                  Inc(Sel.Index);
                  Sel.Digit := 0;
                end;
                if (Sel.Index <= FSize) or OutOfArea then
                begin
                  Result := Sel;
                  Area := eaDigits;
                end;
                Break;
              end;
              if DigitSep then
                Inc(X, HalfPosWidth);
            end;
          end
          else if Area = eaDigits then // OutOfArea = True
          begin
            Result.Index := P.Y * FLineSize;
            if P.X >= XMax then
              Inc(Result.Index, FLineSize);
          end;
        X := XMax;
      end;
      if ((P.X >= X) or OutOfArea) and (edText in FDrawStyles) then
      begin
        XMax := X + (AD.Text + AD.TextIn) * FCharWidth;
        if not OutOfArea or (Area = eaText) then
          if (P.X >= X) and (P.X < XMax) then
          begin
            Inc(X, AD.TextIn * FCharWidth);
            Sel := MakeSelection(P.Y * FLineSize, 0);
            I := Max(P.X - X, 0) div FCharWidth;
            if Sel.Index + I = FSize then
              Sel.Index := FSize // don't split the FSize character box
            else
              Inc(Sel.Index, (Max(P.X - X, 0) + HalfPosWidth) div FCharWidth);
            if (Sel.Index <= FSize) or OutOfArea then
            begin
              Result := Sel;
              Area := eaText;
            end;
          end
          else if Area = eaText then // OutOfArea = True
          begin
            Result.Index := P.Y * FLineSize;
            if P.X >= XMax then
              Inc(Result.Index, FLineSize);
          end;
      end;
    end;
  end;
  ValidateSelection(Result, Area);
end;

procedure TKCustomHexEditor.SafeSetFocus;
var
  Form: TCustomForm;
begin
  Form := GetParentForm(Self);
  if (Form <> nil) and Form.Visible and Form.Enabled and not (csDestroying in Form.ComponentState)
    and Visible and Enabled then
      Form.ActiveControl := Self;
end;

procedure TKCustomHexEditor.SaveToFile(const FileName: TFileName);
var
  Stream: TFileStream;
begin
  Stream := TFileStream.Create(FileName, fmCreate);
  try
    SaveToStream(Stream);
  finally
    Stream.Free;
  end;
end;

procedure TKCustomHexEditor.SaveToStream(Stream: TStream);
begin
  if FBuffer <> nil then
    Stream.Write(FBuffer^, FSize);
end;

procedure TKCustomHexEditor.ScrollBy(HChars, VChars: Integer; UpdateNeeded: Boolean);
begin
  if HChars <> 0 then
    ModifyScrollBar(SB_HORZ, cScrollDelta, HChars, UpdateNeeded);
  if VChars <> 0 then
    ModifyScrollBar(SB_VERT, cScrollDelta, VChars, UpdateNeeded);
end;

procedure TKCustomHexEditor.ScrollTo(Point: TPoint; Timed, AlwaysScroll: Boolean);
var
  ScrollHorz: Boolean;
  R: TRect;
begin
  // disable horizontal overscroll when scrolling e.g. with mouse
  ScrollHorz := AlwaysScroll or (FSelEnd.Index mod FLineSize <> 0) and
    (FSelEnd.Index < FSize) or (FSelEnd.Digit > 0);
  R := GetModifiedClientRect;
  if ScrollHorz then
  begin
    if Point.X < R.Left then
      FScrollDeltaX := DivDown(Point.X, FCharWidth)
    else if Point.X >= R.Right then
      FScrollDeltaX := (Point.X - R.Right) div FCharWidth + 1
    else
      FScrollDeltaX := 0;
  end else
    FScrollDeltaX := 0;
  if Point.Y < R.Top then
    FScrollDeltaY := DivDown(Point.Y, FCharHeight)
  else if Point.Y >= R.Bottom then
    FScrollDeltaY := (Point.Y - R.Bottom) div FCharHeight + 1
  else
    FScrollDeltaY := 0;
  if (FScrollDeltaX <> 0) or (FScrollDeltaY <> 0) then
    if Timed then
    begin
      ScrollBy(FScrollDeltaX, FScrollDeltaY, False);
      FScrollTimer.Enabled := True;
    end else
      ScrollBy(FScrollDeltaX, FScrollDeltaY, True);
  UpdateSelEnd(Point, True);
end;

procedure TKCustomHexEditor.ScrollTimerHandler(Sender: TObject);
var
  P: TPoint;
begin
  GetCursorPos(P);
  P := ScreenToClient(P);
  if (elMouseCapture in FStates) and not (Dragging or
    PtInRect(GetModifiedClientRect, P)) then
    ScrollTo(P, True, False)
  else
    FScrollTimer.Enabled := False;
end;

function TKCustomHexEditor.SelAvail: Boolean;
begin
  Result := SelLength.Index > 0;
end;

procedure TKCustomHexEditor.SelectionChanged(StartEqualEnd: Boolean; ScrollToView: Boolean = True);
begin
  ValidateSelection(FSelEnd, FEditArea);
  if StartEqualEnd then
    FSelStart := FSelEnd
  else
    ValidateSelection(FSelStart, FEditArea);
  if HasParent then
  begin
    if ScrollToView and (FEditArea <> eaNone) then
      ScrollTo(SelToPoint(FSelEnd, FEditArea), False, True);
    UpdateEditorCaret;
    Invalidate;
    InvalidatePageSetup;
  end;  
end;

function TKCustomHexEditor.SelectionValid(Value: TKHexEditorSelection; Area: TKHexEditorArea): Boolean;
begin
  Result := (Area <> eaNone) and (
    (Value.Index >= 0) and (Value.Index < FSize) or
    (Value.Index = FSize) and (Value.Digit = 0))
end;

function TKCustomHexEditor.SelToPoint(Value: TKHexEditorSelection; Area: TKHexEditorArea): TPoint;
var
  AD: TKHexEditorAreaDimensions;
begin
  Result := Point(0, 0);
  AD := GetAreaDimensions;
  ValidateSelection(Value, Area);
  if (Area = eaDigits) and (edDigits in FDrawStyles) then
  begin
    Result.X := ((Value.Index mod FLineSize) div FDigitGrouping * (cDigitCount * FDigitGrouping + 1) +
      (Value.Index mod FLineSize) mod FDigitGrouping * cDigitCount + Value.Digit + AD.DigitsIn)
  end else if (Area = eaText) and (edText in FDrawStyles) then
    Result.X := (Value.Index mod FLineSize + AD.DigitsIn + AD.Digits + AD.DigitsOut + AD.TextIn)
  else if Area = eaAddress then
  begin
    if edDigits in FDrawStyles then
      Result.X := AD.DigitsIn
    else if edText in FDrawStyles then
      Result.X := AD.TextIn;
  end;
  Result.X := (Result.X + AD.Address + AD.AddressOut - FLeftChar) * FCharWidth;
  Result.Y := (Value.Index div FLineSize - FTopLine) * FCharHeight;
end;

procedure TKCustomHexEditor.SetAddressCursor(Value: TCursor);
begin
  if Value <> FAddressCursor  then
  begin
    FAddressCursor := Value;
    UpdateMouseCursor;
  end;
end;

procedure TKCustomHexEditor.SetAddressMode(Value: TKHexEditorAddressMode);
begin
  if Value <> FAddressMode then
  begin
    FAddressMode := Value;
    Invalidate;
  end;
end;

procedure TKCustomHexEditor.SetAddressOffset(Value: Integer);
begin
  if Value <> FAddressOffset then
  begin
    FAddressOffset := Value;
    Invalidate;
  end;
end;

procedure TKCustomHexEditor.SetAddressPrefix(const Value: string);
begin
  if Value <> FAddressPrefix then
  begin
    FAddressPrefix := Value;
    UpdateScrollRange;
  end;
end;

procedure TKCustomHexEditor.SetAddressSize(Value: Integer);
begin
  Value := MinMax(Value, cAddressSizeMin, cAddressSizeMax);
  if Value <> FAddressSize then
  begin
    FAddressSize := Value;
    UpdateScrollRange;
  end;
end;

procedure TKCustomHexEditor.SetAreaSpacing(Value: Integer);
begin
  Value := MinMax(Value, cAreaSpacingMin, cAreaSpacingMax);
  if Value <> FAreaSpacing then
  begin
    FAreaSpacing := Value;
    UpdateScrollRange;
  end;
end;

procedure TKCustomHexEditor.SetCharMapping(const Value: TKEditCharMapping);
begin
  if not CompareMem(@Value, @FCharMapping, SizeOf(TKEditCharMapping)) and
    (edText in FDrawStyles) then
    Invalidate;
end;

procedure TKCustomHexEditor.SetCharSpacing(Value: Integer);
begin
  Value := MinMax(Value, cCharSpacingMin, cCharSpacingMax);
  if Value <> FCharSpacing then
  begin
    FCharSpacing := Value;
    UpdateCharMetrics;
    UpdateScrollRange;
  end;
end;

procedure TKCustomHexEditor.SetColors(Value: TKHexEditorColors);
begin
  FColors.Assign(Value);
end;

procedure TKCustomHexEditor.SetCommandKey(Index: TKEditCommand; Value: TKEditKey);
var
  I: Integer;
begin
  for I := 0 to Length(FKeyMapping) - 1 do
    if FKeyMapping[I].Command = Index then
    begin
      FKeyMapping[I].Key := Value;
      Exit;
    end;
end;

procedure TKCustomHexEditor.SetData(Value: TDataSize);
begin
  if (Value.Data <> FBuffer) or (Value.Size <> FSize) then
  begin
    Clear;
    if Value.Data <> nil then
    begin
      FSize := Value.Size;
      GetMem(FBuffer, FSize);
      System.Move(Value.Data^, FBuffer^, FSize);
      BufferChanged;
    end;
  end;
end;

procedure TKCustomHexEditor.SetDigitGrouping(Value: Integer);
begin
  Value := MinMax(Value, cDigitGroupingMin, Min(FLineSize, cDigitGroupingMax));
  if Value <> FDigitGrouping then
  begin
    FDigitGrouping := Value;
    UpdateScrollRange;
  end;
end;

procedure TKCustomHexEditor.SetDisabledDrawStyle(Value: TKHexEditorDisabledDrawStyle);
begin
  if Value <> FDisabledDrawStyle then
  begin
    FDisabledDrawStyle := Value;
    if not Enabled then
      Invalidate;
  end;
end;

procedure TKCustomHexEditor.SetDrawStyles(const Value: TKHexEditorDrawStyles);
begin
  if Value <> FDrawStyles then
  begin
    FDrawStyles := Value;
    EditAreaChanged; // must be called first
    UpdateScrollRange;
  end;
end;

procedure TKCustomHexEditor.SetEditArea(Value: TKHexEditorArea);
begin
  if Value <> FEditArea then
  begin
    FEditArea := Value;
    EditAreaChanged;
    if Value <> FEditArea then
      Invalidate;
  end;
end;

procedure TKCustomHexEditor.SetKeyMapping(const Value: TKEditKeyMapping);
begin
  SetLength(FKeyMapping, Length(Value));
  Move(Value, FKeyMapping, Length(Value) * SizeOf(TKEditCommandAssignment));
end;

procedure TKCustomHexEditor.SetLineHeightPercent(Value: Integer);
begin
  Value := MinMax(Value, cLineHeightPercentMin, cLineHeightPercentMax);
  if Value <> FLineHeightPercent then
  begin
    FLineHeightPercent := Value;
    UpdateCharMetrics;
    UpdateScrollRange;
  end;
end;

procedure TKCustomHexEditor.SetLeftChar(Value: Integer);
begin
  Value := MinMax(Value, 0, GetMaxLeftChar);
  if Value <> FLeftChar then
    ScrollBy(Value - FLeftChar, 0, True);
end;

procedure TKCustomHexEditor.SetLines(Index: Integer; const Value: TDataSize);
var
  I, Size: Integer;
begin
  I := Index * FLineSize;
  if (Value.Data <> nil) and (Value.Size > 0) and (I >= 0) and (I <= FSize) then
  begin
    Size := Min(FLineSize, Value.Size);
    if I + Size > FSize then
    begin
      FSize := Size;
      ReallocMem(FBuffer, FSize);
    end;
    System.Move(Value.Data^, FBuffer[I], Size);
    BufferChanged;
  end;
end;

procedure TKCustomHexEditor.SetLineSize(Value: Integer);
begin
  Value := MinMax(Value, cLineSizeMin, cLineSizeMax);
  if Value <> FLineSize then
  begin
    FLineSize := Value;
    UpdateScrollRange;
  end;
end;

procedure TKCustomHexEditor.SetModified(Value: Boolean);
begin
  if Value <> GetModified then
  begin
    if Value then
      Include(FStates, elModified)
    else
    begin
      Exclude(FStates, elModified);
      if eoUndoAfterSave in FOptions then
        FUndoList.Modified := False
      else
      begin
        FUndoList.Clear;
        FRedoList.Clear;
      end;
    end;
  end;
end;

function TKCustomHexEditor.SetMouseCursor(X, Y: Integer): Boolean;
var
  ACursor: TCursor;
  P: TPoint;
  Area: TKHexEditorArea;
begin
  P := Point(X, Y);
  PointToSel(P, False, Area);
  if PtInRect(ClientRect, P) then
  begin
    case Area of
      eaAddress: ACursor := FAddressCursor;
      eaDigits: ACursor := crIBeam;
      eaText: ACursor := crIBeam;
    else
      ACursor := crDefault;
    end;
  end else
    ACursor := crDefault;
{$IFDEF FPC}
  FCursor := ACursor;
  SetTempCursor(ACursor);
{$ELSE}
  Windows.SetCursor(Screen.Cursors[ACursor]);
{$ENDIF}
  Result := True;
end;

procedure TKCustomHexEditor.SetOptions(const Value: TKEditOptions);
{$IFDEF USE_WINAPI}
var
  UpdateDropFiles: Boolean;
{$ENDIF}
begin
  if Value <> FOptions then
  begin
  {$IFDEF USE_WINAPI}
    UpdateDropFiles := (eoDropFiles in Value) <> (eoDropFiles in FOptions);
    FOptions := Value;
    // (un)register HWND as drop target
    if UpdateDropFiles and not (csDesigning in ComponentState) and HandleAllocated then
      DragAcceptFiles(Handle, (eoDropFiles in fOptions));
  {$ELSE}
    FOptions := Value;
  {$ENDIF}
  end;
end;

procedure TKCustomHexEditor.SetReadOnly(Value: Boolean);
begin
  if Value <> GetReadOnly then
  begin
    if Value then
      Include(FStates, elReadOnly)
    else
      Exclude(FStates, elReadOnly);
  end;
end;

procedure TKCustomHexEditor.SetScrollBars(Value: TScrollStyle);
begin
  if Value <> FScrollBars then
  begin
    FScrollBars := Value;
  {$IFDEF FPC}
    UpdateSize;
  {$ELSE}
    RecreateWnd;
  {$ENDIF}
  end;
end;

procedure TKCustomHexEditor.SetScrollSpeed(Value: Cardinal);
begin
  Value := MinMax(Integer(Value), cScrollSpeedMin, cScrollSpeedMax);
  if Value <> FScrollSpeed then
  begin
    FScrollSpeed := Value;
    FScrollTimer.Enabled := False;
    FScrollTimer.Interval := FScrollSpeed;
  end;
end;

procedure TKCustomHexEditor.SetSelEnd(Value: TKHexEditorSelection);
begin
  if (Value.Index <> FSelEnd.Index) or (Value.Digit <> FSelEnd.Digit) then
  begin
    FSelEnd := Value;
    SelectionChanged(False, False);
    Invalidate;
  end;
end;

procedure TKCustomHexEditor.SetSelLength(Value: TKHexEditorSelection);
var
  X: TKHexEditorSelection;
begin
  X := GetSelLength;
  if (Value.Index <> X.Index) or (Value.Digit <> X.Digit) then
  begin
    FSelEnd.Index := FSelStart.Index + Value.Index;
    FSelEnd.Digit := FSelStart.Digit + Value.Digit;
    if FSelEnd.Digit >= cDigitCount then
      Inc(FSelEnd.Index);
    SelectionChanged(False, False);
    Invalidate;
  end;
end;

procedure TKCustomHexEditor.SetSelStart(Value: TKHexEditorSelection);
begin
  if (Value.Index <> FSelStart.Index) or (Value.Digit <> FSelStart.Digit) then
  begin
    FSelStart := Value;
    SelectionChanged(False, False);
    Invalidate;
  end;
end;

procedure TKCustomHexEditor.SetTopLine(Value: Integer);
begin
  Value := MinMax(Value, 0, GetMaxTopLine);
  if Value <> FTopLine then
    ScrollBy(0, Value - FTopLine, True);
end;

procedure TKCustomHexEditor.SetUndoLimit(Value: Integer);
begin
  Value := MinMax(Value, cUndoLimitMin, cUndoLimitMax);
  if Value <> FUndoList.Limit then
  begin
    FUndoList.Limit := Value;
    FRedoList.Limit := Value;
  end;
end;

procedure TKCustomHexEditor.HideEditorCaret;
var
  P: TPoint;
begin
  P := SelToPoint(FSelEnd, FEditArea);
  HideCaret(Handle);
  {$IFDEF FPC}SetCaretPosEx(Handle,{$ELSE}SetCaretPos({$ENDIF} P.X, P.Y + 1);
end;

procedure TKCustomHexEditor.ShowEditorCaret;
var
  P: TPoint;
begin
  P := SelToPoint(FSelEnd, FEditArea);
  {$IFDEF FPC}SetCaretPosEx(Handle,{$ELSE}SetCaretPos({$ENDIF} P.X, P.Y + 1);
  ShowCaret(Handle);
end;

procedure TKCustomHexEditor.UndoChange(Sender: TObject; ItemReason: TKHexEditorChangeReason);
begin
  if (Sender = FUndoList) and (ItemReason <> crCaretPos) then
    DoChange;
end;

procedure TKCustomHexEditor.UpdateEditorCaret(Recreate: Boolean = False);
var
  CW, CH: Integer;
begin
  Include(FStates, elCaretUpdate);
  try
    if Enabled and Focused and (FEditArea in [eaDigits, eaText]) and not (csDesigning in ComponentState) then
    begin
      if not (elCaretVisible in FStates) or Recreate then
      begin
        if elOverwrite in FStates then
          CW := FCharWidth
        else
          CW := Max(2, (Abs(Font.Height) * 2) div 25);
        if edHorzLines in FDrawStyles then
          CH := FCharHeight - Max(1, FCharHeight div 25)
        else
          CH := FCharHeight;
      {$IFDEF FPC}
        CreateCaret(Handle, 0, CW, CH - 2);
      {$ELSE}
        if CreateCaret(Handle, 0, CW, CH - 2) then
      {$ENDIF}
          Include(FStates, elCaretVisible);
        Invalidate;
      end;
      if elCaretVisible in FStates then
        ShowEditorCaret;
    end else
    begin
      Exclude(FStates, elCaretVisible);
      HideEditorCaret;
    {$IFDEF FPC}
      DestroyCaret(Handle);
    {$ELSE}
      DestroyCaret;
    {$ENDIF}
    end;
  finally
    Exclude(FStates, elCaretUpdate);
  end;
end;

procedure TKCustomHexEditor.UpdateCharMetrics;
var
  DC: HDC;
  TM: TTextMetric;
begin
  DC := GetDC(0);
  try
    SelectObject(DC, Font.Handle);
    GetTextMetrics(DC, TM);
    FTotalCharSpacing := FCharSpacing * 2;
    // ensure even char spacing because of PointToSel
    if TM.tmAveCharWidth and 1 <> 0 then
      Inc(FTotalCharSpacing);
    FCharWidth := TM.tmAveCharWidth + FTotalCharSpacing;
    FCharHeight := TM.tmHeight * FLineHeightPercent div 100;
  finally
    ReleaseDC(0, DC);
  end;
end;

procedure TKCustomHexEditor.UpdateMouseCursor;
var
  P: TPoint;
begin
  P := ScreenToClient(Mouse.CursorPos);
  SetMouseCursor(P.X, P.Y);
end;

procedure TKCustomHexEditor.UpdateScrollRange;
var
  I: Integer;
  AD: TKHexEditorAreaDimensions;
  SI: TScrollInfo;
begin
  if HandleAllocated then
  begin
    AD := GetAreaDimensions;
    // update horizontal scroll position
    I := FLeftChar - GetMaxLeftChar(AD.TotalHorz);
    if I > 0 then
      Dec(FLeftChar, I);
    FLeftChar := Max(FLeftChar, 0);
    // update vertical scroll position
    I := FTopLine - GetMaxTopLine(AD.TotalVert);
    if I > 0 then
      Dec(FTopLine, I);
    FTopLine := Max(FTopLine, 0);
    if FScrollBars in [ssBoth, ssHorizontal, ssVertical] then
    begin
      SI.cbSize := SizeOf(TScrollInfo);
      SI.fMask := SIF_RANGE or SIF_PAGE or SIF_POS {$IFDEF UNIX}or SIF_UPDATEPOLICY{$ENDIF};
      SI.nMin := 0;
    {$IFDEF UNIX}
      SI.ntrackPos := SB_POLICY_CONTINUOUS;
    {$ENDIF}
      if FScrollBars in [ssBoth, ssHorizontal] then
      begin
        SI.nMax := AD.TotalHorz{$IFNDEF FPC}- 1{$ENDIF};
        SI.nPage := GetClientWidthChars;
        SI.nPos := FLeftChar;
        SetScrollInfo(Handle, SB_HORZ, SI, True);
        ShowScrollBar(Handle, SB_HORZ, Integer(SI.nPage) < AD.TotalHorz);
      end else
        ShowScrollBar(Handle, SB_HORZ, False);
      if FScrollBars in [ssBoth, ssVertical] then
      begin
        SI.nMax := AD.TotalVert{$IFNDEF FPC}- 1{$ENDIF};
        SI.nPage := GetClientHeightChars;
        SI.nPos := FTopLine;
        SetScrollInfo(Handle, SB_VERT, SI, True);
        ShowScrollBar(Handle, SB_VERT, Integer(SI.nPage) < AD.TotalVert);
      end else
        ShowScrollBar(Handle, SB_VERT, False);
    end;
    UpdateEditorCaret(True);
    Invalidate;
    InvalidatePageSetup;
  end;
end;

procedure TKCustomHexEditor.UpdateSelEnd(Point: TPoint; ClipToClient: Boolean);
var
  R: TRect;
  Sel: TKHexEditorSelection;
begin
  if ClipToClient then
  begin
    R := GetModifiedClientRect;
    Dec(R.Right, FCharWidth);
    Dec(R.Bottom, FCharHeight);
    if CanScroll(ecScrollLeft) and (Point.X < R.Left) then
      Point.X := R.Left
    else if CanScroll(ecScrollRight) and (Point.X > R.Right) then
      Point.X := R.Right;
    if CanScroll(ecScrollUp) and (Point.Y < R.Top) then
      Point.Y := R.Top
    else if CanScroll(ecScrollDown) and (Point.Y > R.Bottom) then
      Point.Y := R.Bottom;
  end;
  Sel := PointToSel(Point, True, FEditArea);
  if (Sel.Index <> cInvalidIndex) and
    ((Sel.Index <> FSelEnd.Index) or (Sel.Digit <> FSelEnd.Digit)) then
  begin
    FSelEnd := Sel;
    UpdateEditorCaret;
    Invalidate;
    InvalidatePageSetup;
  end;
end;

procedure TKCustomHexEditor.UpdateSize;
begin
  UpdateScrollRange;
end;

procedure TKCustomHexEditor.ValidateSelection(var Value: TKHexEditorSelection; Area: TKHexEditorArea);
begin
  if Area <> eaNone then
  begin
    Value.Index := MinMax(Value.Index, 0, FSize);
    if Value.Index = FSize then
      Value.Digit := 0
    else
      Value.Digit := MinMax(Value.Digit, 0, cDigitCount - 1);
  end else
    Value := MakeSelection(cInvalidIndex, 0);
end;

{$IFNDEF FPC}
procedure TKCustomHexEditor.WMDropFiles(var Msg: TLMessage);
var
  I, FileCount: Integer;
  PathName: array[0..260] of Char;
  Point: TPoint;
  FilesList: TStringList;
begin
  try
    if Assigned(FOnDropFiles) then
    begin
      FilesList := TStringList.Create;
      try
        FileCount := DragQueryFile(THandle(Msg.wParam), Cardinal(-1), nil, 0);
        DragQueryPoint(THandle(Msg.wParam), Point);
        for i := 0 to FileCount - 1 do
        begin
          DragQueryFile(THandle(Msg.wParam), I, PathName, SizeOf(PathName));
          FilesList.Add(PathName);
        end;
        FOnDropFiles(Self, Point.X, Point.Y, FilesList);
      finally
        FilesList.Free;
      end;
    end;
  finally
    Msg.Result := 0;
    DragFinish(THandle(Msg.wParam));
  end;
end;
{$ENDIF}

procedure TKCustomHexEditor.WMEraseBkgnd(var Msg: TLMessage);
begin
  Msg.Result := 1;
end;

procedure TKCustomHexEditor.WMGetDlgCode(var Msg: TLMNoParams);
begin
  Msg.Result := DLGC_WANTARROWS;
end;

procedure TKCustomHexEditor.WMHScroll(var Msg: TLMHScroll);
begin
  SafeSetFocus;
  ModifyScrollBar(SB_HORZ, Msg.ScrollCode, Msg.Pos, True);
end;

procedure TKCustomHexEditor.WMKillFocus(var Msg: TLMKillFocus);
begin
  inherited;
  ExecuteCommand(ecLostFocus);
end;

procedure TKCustomHexEditor.WMSetFocus(var Msg: TLMSetFocus);
begin
  inherited;
  ExecuteCommand(ecGotFocus);
end;

procedure TKCustomHexEditor.WMVScroll(var Msg: TLMVScroll);
begin
  SafeSetFocus;
  ModifyScrollBar(SB_VERT, Msg.ScrollCode, Msg.Pos, True);
end;

function GetColorSpec(Index: TKHexEditorColorIndex): TKHexEditorColorSpec;
begin
  case Index of
    ciAddressText: begin Result.Def := cAddressTextDef; Result.Name := sAddressText; end;
    ciAddressBkGnd: begin Result.Def := cAddressBkgndDef; Result.Name := sAddressBkGnd; end;
    ciBkGnd: begin Result.Def := cBkGndDef; Result.Name := sBkGnd; end;
    ciDigitTextEven: begin Result.Def := cDigitTextEvenDef; Result.Name := sDigitTextEven; end;
    ciDigitTextOdd: begin Result.Def := cDigitTextOddDef; Result.Name := sDigitTextOdd; end;
    ciDigitBkGnd: begin Result.Def := cDigitBkGndDef; Result.Name := sDigitBkgnd; end;
    ciHorzLines: begin Result.Def := cHorzLinesDef; Result.Name := sHorzLines; end;
    ciInactiveCaretBkGnd: begin Result.Def := cInactiveCaretBkGndDef; Result.Name := sInactiveCaretBkGnd; end;
    ciInactiveCaretSelBkGnd: begin Result.Def := cInactiveCaretSelBkGndDef; Result.Name := sInactiveCaretSelBkGnd; end;
    ciInactiveCaretSelText: begin Result.Def := cInactiveCaretSelTextDef; Result.Name := sInactiveCaretSelText; end;
    ciInactiveCaretText: begin Result.Def := cInactiveCaretTextDef; Result.Name := sInactiveCaretText; end;
    ciLinesHighLight: begin Result.Def := cLinesHighLightDef; Result.Name := sLinesHighLight; end;
    ciSelBkGnd: begin Result.Def := cSelBkGndDef; Result.Name := sSelBkGnd; end;
    ciSelBkGndFocused: begin Result.Def := cSelBkGndFocusedDef; Result.Name := sSelBkGndFocused; end;
    ciSelText: begin Result.Def := cSelTextDef; Result.Name := sSelText; end;
    ciSelTextFocused: begin Result.Def := cSelTextFocusedDef; Result.Name := sSelTextFocused; end;
    ciSeparators: begin Result.Def := cSeparatorsDef; Result.Name := sSeparators; end;
    ciTextText: begin Result.Def := cTextTextDef; Result.Name := sTextText; end;
    ciTextBkGnd: begin Result.Def := cTextBkgndDef; Result.Name := sTextBkGnd; end;
    ciVertLines: begin Result.Def := cVertLinesDef; Result.Name := sVertLines; end;
  else
    Result.Def := clNone;
    Result.Name := '';
  end;
end;

end.