unit VirtualTrees.Filter;

interface

//{$DEFINE USE_VTFILTER_COMBOBOX}

uses
  Winapi.Windows, Winapi.oleacc, Winapi.Messages,

  System.SysUtils, System.Variants, System.Classes, System.UITypes,
  System.Types,

  Generics.Collections,
  Generics.Defaults,

  VirtualTrees,
  VirtualTrees.Utils,

  Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.StdCtrls;

{$IFDEF USE_VTFILTER_COMBOBOX}
const
  MAX_COMBO_ITEM = 10;
{$ENDIF}

type
  TFilterDict = class // (TDictionary <Integer, String>)
  private
    fInnerDict: TDictionary <Integer, String>;
    function GetFilter(Index: Integer): String;
    procedure SetFilter(Index: Integer; const Value: String);
    function GetCount: Integer;
  protected
  public
    constructor Create;
    destructor Destroy; override;

    procedure Assign(aFilterDict: TFilterDict);
  public
    property Filter[Index: Integer]: String read GetFilter write SetFilter; default;
    property Count: Integer read GetCount;
  end;

  TBlockedColumns = class (TDictionary<TColumnIndex, Boolean>)
  private
  protected
  public
    function CheckBlocked(aColumnIndex: TColumnIndex): Boolean;
  end;

  TRedirectColumns = class (TDictionary<TColumnIndex, TColumnIndex>)
  private
  protected
  public
    function CheckRedirect(aColumnIndex: TColumnIndex): TColumnIndex;
  end;


  TCheckNodeVisibleEvent = function (Sender: TBaseVirtualTree; aNode: PVirtualNode; var ContinueGetText: Boolean): Boolean of Object;
  TCheckForceInvisibleEvent = function (Sender: TBaseVirtualTree; aNode: PVirtualNode; var IgnoreTarget: Boolean): Boolean of Object; // 추가 18_0416 10:37:13 sunk
  TAfterFilterEvent = procedure (Sender: TBaseVirtualTree) of Object;

  // 필터 라인
  TVtFilterLine  = (flTop, flBottom, flRight);
  TVtFilterLines = set of TVtFilterLine;

  // 필터 적용 단계
  TVtFilterApplyLevel = (flvRoot, flvAll);

{$IFDEF USE_VTFILTER_COMBOBOX}
  TVtFilterEdit = class (TCustomComboBox)
{$ELSE}
  TVtFilterEdit = class (TCustomEdit)
{$ENDIF}
  private
    FilterDict_: TFilterDict;
    FilterLines_: TVtFilterLines;
    FilterApplyLevel_: TVtFilterApplyLevel;
    NotEditColumns_: TBlockedColumns;
    NotFiltedColumns_: TBlockedColumns;
    RedirectColumns_: TRedirectColumns;
    bUse_Ctrl_F_: Boolean;

  // 자신의 하위 노드의 Visible이 모두 false 인 경우,
  // 상위의 Visible도 false로 변경할껀가? 18_0416 10:03:33 sunk
    bForceInvisible_: Boolean;

    VT_: TVirtualStringTree;
    ColumnIndex_: TColumnIndex;
    EvOnCheckNodeVisible_: TCheckNodeVisibleEvent;
    EvOnCheckForceInvisible_: TCheckForceInvisibleEvent;
    EvOnAfterFilter_: TAfterFilterEvent;
  private
    procedure CMExit(var Message: TMessage); message CM_EXIT;
    procedure CMRelease(var Message: TMessage); message CM_RELEASE;
    procedure WMGetDlgCode(var Message: TWMGetDlgCode); message WM_GETDLGCODE;
  {$IFDEF USE_VTFILTER_COMBOBOX}
    procedure OnCbKeyPress(Sender: TObject; var Key: Char);
    procedure OnCbKeyUp(Sender: TObject; var Key: Word; Shift: TShiftState);
  {$ELSE}
    procedure WMChar(var Message: TWMChar); message WM_CHAR;
    procedure WMKeyDown(var Message: TWMKeyDown); message WM_KEYDOWN;
  {$ENDIF}

  private
    OrgVSTWndProc_: TWndMethod;
    FilterBitmap_: TBitmap;
  {$IFDEF USE_VTFILTER_COMBOBOX}
    sHighOwnerName_: String;
    procedure SaveComboBoxItems;
    procedure LoadComboBoxItems;
  {$ENDIF}
    procedure VSTWndProc(var Message: TMessage);
  protected
  public
    constructor Create(aOwner: TComponent; aVST: TVirtualStringTree); reintroduce;
    destructor Destroy; override;

    function IsNodeVisible(aNode: PVirtualNode): Boolean;
    procedure InitFilter;
    procedure ApplyFilter(bSaveInfo: Boolean = true);
    procedure Show(aColumnIndex: TColumnIndex; aInvalidateVST: Boolean = false);
    procedure Hide(aFocusToVST, aInvalidateVST: Boolean);

    procedure Clear; override;
  public
    property Dict: TFilterDict read FilterDict_;
    property Lines: TVtFilterLines read FilterLines_ write FilterLines_;
    property ApplyLevel: TVtFilterApplyLevel read FilterApplyLevel_ write FilterApplyLevel_;
    property NotEditColumns: TBlockedColumns read NotEditColumns_;
    property NotFiltedColumns: TBlockedColumns read NotFiltedColumns_;
    property RedirectColumns: TRedirectColumns read RedirectColumns_;

    property Use_Ctrl_F: Boolean read bUse_Ctrl_F_ write bUse_Ctrl_F_;
    property ForceInvisible: Boolean write bForceInvisible_;

    property OnCheckNodeVisible: TCheckNodeVisibleEvent read EvOnCheckNodeVisible_ write EvOnCheckNodeVisible_;
    property OnCheckForceInvisibleEvent: TCheckForceInvisibleEvent read EvOnCheckForceInvisible_ write EvOnCheckForceInvisible_;
    property OnAfterFilterEvent: TAfterFilterEvent read EvOnAfterFilter_ write EvOnAfterFilter_;
  end;

  TVirtualStringTreeHelper = class helper for TBaseVirtualTree
  public
    function GetFilterEditCtrl: TVtFilterEdit;
    function CreateFilterEdit: TVtFilterEdit;
    procedure DestroyFilterEdit;
  end;


implementation

uses
  System.IniFiles,
  Tocsg.Safe,
  Tocsg.PCRE, Tocsg.Strings, Tocsg.Path, VirtualTrees.Types;

resourcestring
  RS_Q_Clear = '해당 컬럼의 검색 기록을 초기화 하시겠습니까?';


{ TFilterDict }

constructor TFilterDict.Create;
begin
  fInnerDict:= TDictionary <Integer, String>.Create;

end;

destructor TFilterDict.Destroy;
begin
  FreeAndNil(fInnerDict);

  inherited;
end;

function TFilterDict.GetCount: Integer;
begin
  Result := fInnerDict.Count;
end;

function TFilterDict.GetFilter(Index: Integer): String;
begin
  if fInnerDict.ContainsKey(Index) then
    Result := fInnerDict[Index]
  else
    Result := '';
end;

procedure TFilterDict.SetFilter(Index: Integer; const Value: String);
begin
  if fInnerDict.ContainsKey(Index) then
  begin
    if (Value.Length = 0) or (Value = '*') then
      fInnerDict.Remove(Index)
    else
      fInnerDict[Index] := Value;
  end else
  if (Value.Length > 0) and (Value <> '*') then
  begin
    fInnerDict.Add(Index, Value);
  end;
end;

procedure TFilterDict.Assign(aFilterDict: TFilterDict);
var
  Item: TPair<Integer, String>;
begin
  fInnerDict.Clear;

  for Item in aFilterDict.fInnerDict do
    fInnerDict.Add(Item.Key, Item.Value);
end;

{ TBlockedColumns }

function TBlockedColumns.CheckBlocked(aColumnIndex: TColumnIndex): Boolean;
begin
  Result := ContainsKey(aColumnIndex) and Items[aColumnIndex];
end;


{ TRedirectColumns }

function TRedirectColumns.CheckRedirect(aColumnIndex: TColumnIndex): TColumnIndex;
begin
  if ContainsKey(aColumnIndex) then
    Result := Items[aColumnIndex]
  else
    Result := aColumnIndex;
end;


{ TVtFilterEdit }

type
  TVirtualTreeDesc = class (TBaseVirtualTree);
  TVirtualTreeColumnsDesc = class (TVirtualTreeColumns);

constructor TVtFilterEdit.Create(aOwner: TComponent; aVST: TVirtualStringTree);
begin
  inherited Create(aOwner);

  bForceInvisible_ := false;
  FilterDict_:= TFilterDict.Create;
  NotEditColumns_:= TBlockedColumns.Create;
  NotFiltedColumns_ := TBlockedColumns.Create;
  RedirectColumns_:= TRedirectColumns.Create;

  bUse_Ctrl_F_ := true;

  VT_ := aVST;
  Visible := False;
{$IFDEF USE_VTFILTER_COMBOBOX}
  sHighOwnerName_ := '';
  OnKeyPress := OnCbKeyPress;
  OnKeyUp := OnCbKeyUp;
  AutoComplete := false;
{$ELSE}
  BorderStyle := bsSingle;
{$ENDIF}
  AutoSize := False;

  OrgVSTWndProc_ := VT_.WindowProc;
  VT_.WindowProc := VSTWndProc;

  FilterBitmap_:= TBitmap.Create;
  FilterBitmap_.PixelFormat := pf32Bit;
end;

destructor TVtFilterEdit.Destroy;
begin
  VT_.WindowProc := OrgVSTWndProc_;

  FreeAndNil(FilterBitmap_);
  FreeAndNil(FilterDict_);
  FreeAndNil(NotEditColumns_);
  FreeAndNil(NotFiltedColumns_);
  FreeAndNil(RedirectColumns_);

  inherited;
end;

procedure TVtFilterEdit.CMExit(var Message: TMessage);
begin
  if FilterDict_[ColumnIndex_] <> Trim(Text) then
  begin
    //WriteLn('필터 수정: ', Text);
    FilterDict_[ColumnIndex_] := Trim(Text);


    //if Use_Ctrl_F then VT_.FocusedColumn := ColumnIndex_;
    ApplyFilter();

    Hide(true, false);
  end else
    Hide(true, true);
end;

procedure TVtFilterEdit.CMRelease(var Message: TMessage);
begin
  Free;
end;

{$IFDEF USE_VTFILTER_COMBOBOX}
function GetOwnerFormName(aWinControl: TWinControl): String;
var
  OwnerCtrl: TComponent;
begin
  Result := '';

  OwnerCtrl := aWinControl.Owner;
  while OwnerCtrl <> nil do
  begin
    if (OwnerCtrl is TForm) or
       (OwnerCtrl is TFrame) then
    begin
      Result := OwnerCtrl.ClassName;
      exit;
    end;
    OwnerCtrl := OwnerCtrl.Owner;
  end;
end;

procedure TVtFilterEdit.SaveComboBoxItems;

  function GetDivStrings(aStrings: TStrings): String;
  var
    i: Integer;
  begin
    Result := '';
    for i := 0 to aStrings.Count - 1 do
      Result := Result + aStrings[i] + '§';
  end;

var
  ini: TIniFile;
  nIdx: Integer;
  sTemp: String;
  i: Integer;
begin
  if (ColumnIndex_ = -1) or
     (ColumnIndex_ >= VT_.Header.Columns.Count) then
    exit;

  if sHighOwnerName_ = '' then
  begin
    sHighOwnerName_ := GetOwnerFormName(VT_);
    if sHighOwnerName_ = '' then
      exit;
    sHighOwnerName_ := sHighOwnerName_ + '.' + VT_.Name;
  end;

  sTemp := Text;
  nIdx := Items.IndexOf(sTemp);
  if nIdx <> -1 then
    Items.Delete(nIdx);
  Items.Insert(0, sTemp);
// 검색 기록 갯수 제한 19_1205 09:43:48 sunk
  if Items.Count > MAX_COMBO_ITEM then
    for i := Items.Count downto 0 do
    begin
      Items.Delete(i);
      if Items.Count <= MAX_COMBO_ITEM then
        break;
    end;
  ItemIndex := 0;

  Guard(ini, TIniFile.Create(CutFileExt(GetRunExePath) + 'Filters.ini'));
  ini.WriteString(sHighOwnerName_, Format('COL%d', [ColumnIndex_]), GetDivStrings(Items));
end;

procedure TVtFilterEdit.LoadComboBoxItems;
var
  sTemp: String;
  ini: TIniFile;
begin
  Items.Clear;
  if (ColumnIndex_ = -1) or
     (ColumnIndex_ >= VT_.Header.Columns.Count) then
    exit;

  if sHighOwnerName_ = '' then
  begin
    sHighOwnerName_ := GetOwnerFormName(VT_);
    if sHighOwnerName_ = '' then
      exit;
    sHighOwnerName_ := sHighOwnerName_ + '.' + VT_.Name;
  end;

  sTemp := CutFileExt(GetRunExePath) + 'Filters.ini';
  if FileExists(sTemp) then
  begin
    Guard(ini, TIniFile.Create(sTemp));
    sTemp := ini.ReadString(sHighOwnerName_, Format('COL%d', [ColumnIndex_]), '');
    SplitString(sTemp, '§', Items);
  end;
end;

// 콤보박스에서는 서브클래싱이 안먹혀서 이렇게 처리 19_1204 15:49:31 sunk
procedure TVtFilterEdit.OnCbKeyPress(Sender: TObject; var Key: Char);
var
  Run: TColumnIndex;

  function GetLeftRun: TColumnIndex;
  begin
    Result := VT_.Header.Columns.GetPreviousColumn(ColumnIndex_);

    while (Result >= 0) and NotEditColumns.CheckBlocked(Result) do
      Result := VT_.Header.Columns.GetPreviousColumn(Result);

    //WriteLn(', 이전컬럼 ', Result);

    if Result < 0 then
    begin
      Result := TVirtualTreeColumnsDesc(VT_.Header.Columns).PositionToIndex[VT_.Header.Columns.Count-1];

      while (Result >= 0) and NotEditColumns.CheckBlocked(Result) do
        Result := VT_.Header.Columns.GetPreviousColumn(Result);

      //WriteLn('되돌이컬럼 : ', Result);
    end;
  end;

  function GetRightRun: TColumnIndex;
  begin
    Result := VT_.Header.Columns.GetNextColumn(ColumnIndex_);

    while (Result >= 0) and NotEditColumns.CheckBlocked(Result) do
      Result := VT_.Header.Columns.GetNextColumn(Result);

    //WriteLn(', 다음컬럼 ', Run);

    if Result < 0 then
    begin
      Result := TVirtualTreeColumnsDesc(VT_.Header.Columns).PositionToIndex[0];

      while (Result >= 0) and NotEditColumns.CheckBlocked(Result) do
        Result := VT_.Header.Columns.GetNextColumn(Result);

      //WriteLn('되돌이컬럼 : ', Run);
    end;
  end;

begin
  case Integer(Key) of
    VK_RETURN :
    begin
      Key := #0;
      if FilterDict_[ColumnIndex_] <> Trim(Text) then
      begin
        //WriteLn('필터 수정: ', Text);
        FilterDict_[ColumnIndex_] := Trim(Text);
        //if Use_Ctrl_F then VT_.FocusedColumn := ColumnIndex_;
        ApplyFilter();

        Hide(true, false);
      end else
        Hide(true, true);
    end;

    VK_TAB :
    begin
      //Write('탭키눌림 현재 컬럼 ', ColumnIndex_);

      if GetKeyState(VK_SHIFT) < 0 then
        Run := GetLeftRun
      else
        Run := GetRightRun;

      Perform(CM_EXIT, 0, 0);

      if Run = ColumnIndex_ then
      begin

      end else
      begin
        Show(Run);
      end;
    end;

    VK_ESCAPE :
    begin
      Hide(true, true);
    end;
  end;
end;

procedure TVtFilterEdit.OnCbKeyUp(Sender: TObject; var Key: Word; Shift: TShiftState);
begin
  case Key of
    VK_DELETE :
    begin
//      if (ssCtrl in Shift) and (ssCtrl in Shift) then // 음... 이렇게 했는데 Ctrl + Del 만으로도 된다...머지 19_1217 15:31:29 sunk
      if ssCtrl in Shift then // 그래서 그냥 Ctrl + Del 로만으로 변경 19_1217 15:31:59 sunk
      begin
        if MessageBox(Handle, PChar(RS_Q_Clear), 'Clear',
            MB_ICONQUESTION or MB_YESNO) = IDNO then exit;

        Items.Clear;
        SaveComboBoxItems;
      end;
    end;
  end;
end;
{$ELSE}
procedure TVtFilterEdit.WMChar(var Message: TWMChar);
begin
// 엔터 누르면 띠링~ 소리 안나오도록 보완 17_1229 10:26:32 sunk
  if Message.CharCode = VK_RETURN then
    exit
  else
  if not (Message.CharCode in [VK_ESCAPE, VK_TAB]) then
    inherited;
end;


procedure TVtFilterEdit.WMKeyDown(var Message: TWMKeyDown);
var
  Run: TColumnIndex;

  function GetLeftRun: TColumnIndex;
  begin
    Result := VT_.Header.Columns.GetPreviousColumn(ColumnIndex_);

    while (Result >= 0) and NotEditColumns.CheckBlocked(Result) do
      Result := VT_.Header.Columns.GetPreviousColumn(Result);

    //WriteLn(', 이전컬럼 ', Result);

    if Result < 0 then
    begin
      Result := TVirtualTreeColumnsDesc(VT_.Header.Columns).PositionToIndex[VT_.Header.Columns.Count-1];

      while (Result >= 0) and NotEditColumns.CheckBlocked(Result) do
        Result := VT_.Header.Columns.GetPreviousColumn(Result);

      //WriteLn('되돌이컬럼 : ', Result);
    end;
  end;

  function GetRightRun: TColumnIndex;
  begin
    Result := VT_.Header.Columns.GetNextColumn(ColumnIndex_);

    while (Result >= 0) and NotEditColumns.CheckBlocked(Result) do
      Result := VT_.Header.Columns.GetNextColumn(Result);

    //WriteLn(', 다음컬럼 ', Run);

    if Result < 0 then
    begin
      Result := TVirtualTreeColumnsDesc(VT_.Header.Columns).PositionToIndex[0];

      while (Result >= 0) and NotEditColumns.CheckBlocked(Result) do
        Result := VT_.Header.Columns.GetNextColumn(Result);

      //WriteLn('되돌이컬럼 : ', Run);
    end;
  end;

begin
  case Message.CharCode of
    VK_TAB:
    begin
      //Write('탭키눌림 현재 컬럼 ', ColumnIndex_);

      if GetKeyState(VK_SHIFT) < 0 then
        Run := GetLeftRun
      else
        Run := GetRightRun;

      Perform(CM_EXIT, 0, 0);

      if Run = ColumnIndex_ then
      begin

      end else
      begin
        Show(Run);
      end;
    end;

    {
    VK_LEFT:
    if //(Self.SelLength = 0) and
      (Self.SelStart = 0) then
    begin
      Run := GetLeftRun;
      Perform(CM_EXIT, 0, 0);

      if Run = ColumnIndex_ then
      begin

      end else
      begin
        Show(Run);
      end;
      inherited;
    end else
      inherited;

    VK_RIGHT:
    if //(Self.SelLength = 0) and
      (Self.SelStart = Length(Self.Text)) then
    begin
      Run := GetRightRun;
      Perform(CM_EXIT, 0, 0);

      if Run = ColumnIndex_ then
      begin

      end else
      begin
        Show(Run);
      end;
      inherited;
    end else
      inherited;
    }

    VK_ESCAPE:
    begin
      Hide(true, true);
    end;

    VK_RETURN:
    begin
      if FilterDict_[ColumnIndex_] <> Trim(Text) then
      begin
        //WriteLn('필터 수정: ', Text);
        FilterDict_[ColumnIndex_] := Trim(Text);
        //if Use_Ctrl_F then VT_.FocusedColumn := ColumnIndex_;
        ApplyFilter();

        Hide(true, false);
      end else
        Hide(true, true);
    end;
  else
    inherited;
  end;

  case Message.CharCode of
    VK_LEFT:
    if (Length(Self.Text) = 0) then
       //(Length(Self.Text) = Self.SelLength) then
       //(Self.SelStart = 0) then
    begin
      Run := GetLeftRun;
      Perform(CM_EXIT, 0, 0);

      if Run = ColumnIndex_ then
      begin

      end else
      begin
        Show(Run);
      end;
    end;

    VK_RIGHT:
    if (Length(Self.Text) = 0) then
       //(Length(Self.Text) = Self.SelLength) then
       //(Self.SelStart = Length(Self.Text)) then
    begin
      Run := GetRightRun;
      Perform(CM_EXIT, 0, 0);

      if Run = ColumnIndex_ then
      begin

      end else
      begin
        Show(Run);
      end;
    end;
  end;

end;
{$ENDIF}

procedure TVtFilterEdit.WMGetDlgCode(var Message: TWMGetDlgCode);
begin
  inherited;

  Message.Result := Message.Result or DLGC_WANTALLKEYS or DLGC_WANTTAB or DLGC_WANTARROWS;
end;

function MatchFilterStr(sFilterStr, sStr: String): Boolean;
var
  FStrList: TStringList;
  i, nFind: Integer;
  sFindStr: String;
  c: Char;
Label
  LB_DoFind;
begin
  Result := false;
  if sStr.Length = 0 then
    exit;

  if Pos('#RX:', sFilterStr) = 1 then
  begin
  // 정규 표현식으로 처리
    Delete(sFilterStr, 1, 4);
    if sFilterStr.Length = 0 then
      exit;

    Result := TTgPcre.GetMatchValues(sStr, sFilterStr, sFindStr) > 0;
  end else begin
    Guard(FStrList, TStringList.Create);
    SplitString(sFilterStr, ';', FStrList);

    nFind := 0;
    FStrList.Text := StringReplace(FStrList.Text, '\SCL', ';', [rfReplaceAll]);
    for i := 0 to FStrList.Count - 1 do
    begin
      sFindStr := FStrList[i];
      c := sFindStr[1];
      case c of
        '!', '&' :
        begin
          if sFindStr.Length = 1 then
            goto LB_DoFind;
          Delete(sFindStr, 1, 1);
          if Pos(sFindStr, sStr) > 0 then
          begin
            if c = '!' then exit
            else Inc(nFind);
          end else
          begin
            if c = '!' then Inc(nFind)
            else exit;
          end;
        end;
        else
        begin
        LB_DoFind :
          if Pos(sFindStr, sStr) > 0 then
            Inc(nFind);
        end;
      end;
    end;
    Result := nFind > 0;
  end;
end;

function TVtFilterEdit.IsNodeVisible(aNode: PVirtualNode): Boolean;
var
  ContinueGetText: Boolean;
  i: Integer;
  ColumnIndex: TColumnIndex;

  FilterStr,
  ColumnStr: String;

begin
  Result := true;
  ContinueGetText := true;

  // 노드 체크 이벤트가 정의된 경우, 여기서 false라면 필터검사는 생략.
  if Assigned(EvOnCheckNodeVisible_) then
  begin
    Result := EvOnCheckNodeVisible_(VT_, aNode, ContinueGetText);
    if not Result then Exit;
    if not ContinueGetText then Exit;
  end;

  if FilterDict_.Count = 0 then Exit;

  for i := 0 to VT_.Header.Columns.Count-1 do
    if not NotEditColumns.CheckBlocked(i) and
       not NotFiltedColumns.CheckBlocked(i) then
    begin
      //RedirectColumns
      ColumnIndex := RedirectColumns.CheckRedirect(i);

      FilterStr := UpperCase(FilterDict_[i]);
      if FilterStr.Length = 0 then Continue;

      VT_.OnGetText(VT_, aNode, ColumnIndex, ttNormal, ColumnStr);
      ColumnStr := UpperCase(ColumnStr);
      //if ColumnStr.Length = 0 then Continue;

    // 값이 비어 있는 경우만 볼수 있는 필터 추가 17_1229 10:09:44 sunk
      if ((FilterStr = '#NULL') or (FilterStr = '#NIL')) and
         (ColumnStr.Length = 0) then continue;

    // 값이 비어있으면 보이지 않도록 수정 17_1229 09:57:31 sunk
      if not MatchFilterStr(FilterStr, ColumnStr) then
      begin
        Result := false;
        Exit;
      end;
    end;
end;

procedure TVtFilterEdit.InitFilter;
var
  Node: PVirtualNode;
  ContinueGetText: Boolean;


  procedure SubNodeProc(aNode: PVirtualNode);
  var
    SubNode: PVirtualNode;
  begin
    SubNode := VT_.GetFirstChild(aNode);
    while Assigned(SubNode) do
    begin
      //VT_.IsVisible[SubNode] := IsNodeVisible(SubNode);
      //SubNode := VT_.GetNextSibling(SubNode);
      if Assigned(EvOnCheckNodeVisible_) then
        VT_.IsVisible[SubNode] := EvOnCheckNodeVisible_(VT_, SubNode, ContinueGetText)
      else
        VT_.IsVisible[SubNode] := true;

      SubNodeProc(SubNode);

      SubNode := VT_.GetNextSibling(SubNode);
    end;
  end;


begin
  Hide(false, false);
  FilterDict_.fInnerDict.Clear;

  // 필터링된 노드 모두 표시.
  VT_.BeginUpdate;
  try
    // VST 전체 노드를 루프를 돌면서..
    Node := VT_.GetFirst();
    while Assigned(Node) do
    begin
      // 노드 체크 이벤트가 정의된 경우
      if Assigned(EvOnCheckNodeVisible_) then
        VT_.IsVisible[Node] := EvOnCheckNodeVisible_(VT_, Node, ContinueGetText)
      else
        VT_.IsVisible[Node] := true;

      if ApplyLevel = flvAll then SubNodeProc(Node);

      Node := VT_.GetNextSibling(Node);
    end;
  finally
    VT_.EndUpdate;
  end;

  VT_.Invalidate;
end;


procedure TVtFilterEdit.ApplyFilter(bSaveInfo: Boolean = true);

// 추가 18_0416 10:05:45 sunk
  function CheckInvisibleSubAll(aNode: PVirtualNode): Boolean;
  var
    pSubN: PVirtualNode;
  begin
    Result := true;

    pSubN := aNode.FirstChild;
    while pSubN <> nil do
    begin
      if VT_.IsVisible[pSubN] then
      begin
        Result := false;
        exit;
      end;

      pSubN := pSubN.NextSibling;
    end;
  end;

  procedure SubNodeProc(aNode: PVirtualNode);
  var
    bIgnoreS,
    IsVisible: Boolean;
    SubNode: PVirtualNode;
  begin
    SubNode := VT_.GetFirstChild(aNode);
    while Assigned(SubNode) do
    begin
      IsVisible := IsNodeVisible(SubNode);
      VT_.IsVisible[SubNode] := IsVisible;

      if IsVisible then
      begin
        SubNodeProc(SubNode);
      // 하위가 모두 숨겨져 있다면 자신도 숨길지 18_0416 10:05:45 sunk
        if bForceInvisible_ then
        begin
          bIgnoreS := false;
          if Assigned(EvOnCheckForceInvisible_) then
          begin
          // 필터 체크 대상인지 확인해서 대상이라면 제외 시켜줘야 한다. 18_0416 10:30:01 sunk
            EvOnCheckForceInvisible_(VT_, SubNode, bIgnoreS);
          end;

          if not bIgnoreS then
            VT_.IsVisible[SubNode] := not CheckInvisibleSubAll(SubNode);
        end;
      end;

      SubNode := VT_.GetNextSibling(SubNode);
    end;
  end;

var
  Node: PVirtualNode;
  bIgnore: Boolean;

begin
  if not Assigned(VT_.OnGetText) then
    exit;

  if bSaveInfo then
  begin
  {$IFDEF USE_VTFILTER_COMBOBOX}
    SaveComboBoxItems;
  {$ENDIF}
  end;

  VT_.BeginUpdate;
  try
    with VT_.Header do
    begin
      if SortColumn <> -1 then
      begin
        Columns[SortColumn].Options := Columns[SortColumn].Options + [coParentColor];
        SortColumn := -1;
      end;
    end;

    // VST 전체 노드를 루프를 돌면서..
    Node := VT_.GetFirst();
    while Assigned(Node) do
    begin
      VT_.IsVisible[Node] := IsNodeVisible(Node);

      // 서브노드도 처리
      if ApplyLevel = flvAll then
      begin
        SubNodeProc(Node);
      // 하위가 모두 숨겨져 있다면 자신도 숨길지 18_0416 10:05:45 sunk
        if bForceInvisible_ then
        begin
          bIgnore := false;
          if Assigned(EvOnCheckForceInvisible_) then
          begin
          // 필터 체크 대상인지 확인해서 대상이라면 제외 시켜줘야 한다. 18_0416 10:30:01 sunk
            EvOnCheckForceInvisible_(VT_, Node, bIgnore);
          end;

          if not bIgnore then
            VT_.IsVisible[Node] := not CheckInvisibleSubAll(Node);
        end;
      end;

      Node := VT_.GetNextSibling(Node);
    end;

    if Assigned(EvOnAfterFilter_) then
      EvOnAfterFilter_(VT_);
  finally
    VT_.EndUpdate;
  end;
end;


procedure TVtFilterEdit.Show(aColumnIndex: TColumnIndex; aInvalidateVST: Boolean);
var
  //TargetCanvas: TCanvas;
  OffsetPos: TPoint;
  TargetRect: TRect;
begin
  Hide(false, aInvalidateVST);

  if aColumnIndex < 0 then Exit;
  if aColumnIndex >= VT_.Header.Columns.Count then Exit;

  if NotEditColumns.CheckBlocked(aColumnIndex) then Exit;

  ColumnIndex_ := aColumnIndex;
  VT_.FocusedColumn := ColumnIndex_;

  TargetRect := TVirtualTreeDesc(VT_).HeaderRect;
  OffsetPos  := TargetRect.TopLeft;

  VT_.Header.Columns.GetColumnBounds(aColumnIndex, TargetRect.Left, TargetRect.Right);

  if TargetRect.Left < 0 then
  begin
    VT_.OffsetX := VT_.OffsetX - TargetRect.Left;
    OffsetRect(TargetRect, -TargetRect.Left, 0);
    //OffsetRect(TargetRect, VT_.OffsetX, 0);
  end else
  if TargetRect.Left + 30 > TVirtualTreeDesc(VT_).HeaderRect.Right then
  begin
    //VT_.OffsetX := VT_.OffsetX - (TargetRect.Left + TargetRect.Width);
    //VT_.OffsetX := VT_.OffsetX - TargetRect.Width;
    //VT_.OffsetX := -TargetRect.Right;
    VT_.OffsetX :=
      VT_.OffsetX -
      (
        TargetRect.Left -
        TVirtualTreeDesc(VT_).HeaderRect.Right +
        TargetRect.Width
      );

    //VT_.Header.Columns.GetColumnBounds(aColumnIndex, TargetRect.Left, TargetRect.Right);
    OffsetRect(
      TargetRect,
      -(
        TargetRect.Left -
        TVirtualTreeDesc(VT_).HeaderRect.Right +
        TargetRect.Width
      ),
      0
    );
  end;

  if (TargetRect.Right > TVirtualTreeDesc(VT_).HeaderRect.Right) then
    TargetRect.Right := TVirtualTreeDesc(VT_).HeaderRect.Right;

  if IsRectEmpty(TargetRect) then Exit;

  OffsetRect(TargetRect, VT_.Left, VT_.Top + VT_.Header.Height);

  Self.Parent := VT_.Parent;
  Self.Font.Assign(VT_.Header.Font);
  Self.Font.Style := [fsBold];
  with TargetRect do
  begin
    //fFilterEdit.Height := Height - 2;
    Self.Left := Left + 5 + OffsetPos.X;
    Self.Width := Width - 10;
    Self.Top :=
      Top +
      (Width div 2 - Self.Width div 2) -
      OffsetPos.Y - 1;
    //fFilterEdit.SetBounds(Left, Top, Width, Height);

  end;
  //Self.Text := 'Filter - ' + IntToStr(Run);
{$IFDEF USE_VTFILTER_COMBOBOX}
  LoadComboBoxItems;
{$ENDIF}
  Self.Text := FilterDict_[aColumnIndex];

  Self.Visible := true;
  Self.SetFocus;

  //ColumnIndex_ := aColumnIndex;
end;

procedure TVtFilterEdit.Hide(aFocusToVST, aInvalidateVST: Boolean);
begin
  if not Visible then Exit;
  Visible := false;

  if aFocusToVST then VT_.SetFocus;
  if aInvalidateVST then VT_.Invalidate;
end;

procedure TVtFilterEdit.Clear;
begin
  //inherited;
  InitFilter; // 음... 결국 이걸 쓰면 되긴했는데... 이왕 이렇게 된거 범용적으로 쓰이는 함수에 연결... 17_1229 10:32:25 sunk
end;


procedure TVtFilterEdit.VSTWndProc(var Message: TMessage);
var
  DC: HDC;
  MPos: TPoint;

  FilterRect: TRect;
  //TargetCanvas: TCanvas;
  TargetRect: TRect;
  DrawTextRect: TRect;
  Run: TColumnIndex;
  Text: String;
begin
  case Message.Msg of
    //WM_ERASEBKGND,
    WM_PAINT:
    begin
      FilterRect := TVirtualTreeDesc(VT_).HeaderRect;
      OffsetRect(FilterRect, 0, VT_.Header.Height);

      FilterBitmap_.SetSize(FilterRect.Width, FilterRect.Height);

      with FilterBitmap_.Canvas do
      begin
        Font.Assign(VT_.Header.Font);
        Font.Style := [fsBold];
        //Brush.Color := clYellow;
        //Brush.Color := clBtnFace;
        //Brush.Color := clBtnShadow;
        //Brush.Color := clSilver;
        //Brush.Color := RGB($CF, $CF, $CF);
        Brush.Style := bsSolid;
        FillRect(Bounds(0, 0, FilterRect.Width, FilterRect.Height));
        //Brush.Style := bsClear;

        Pen.Color := VT_.Colors.GridLineColor;

        // 윗선 그리기
        if flTop in FilterLines_ then
        begin
          MoveTo(0, 0);
          LineTo(FilterRect.Width, 0);
        end;
        // 아래선 그리기
        if flBottom in FilterLines_ then
        begin
          MoveTo(0, FilterRect.Height-1);
          LineTo(FilterRect.Width, FilterRect.Height-1);
        end;

        TargetRect := Bounds(0 - VT_.OffsetX, 0, 0, FilterRect.Height);

        Run := VT_.Header.Columns.ColumnFromPosition(TargetRect.TopLeft, false);
        if Run >= 0 then
          VT_.Header.Columns.GetColumnBounds(Run, TargetRect.Left, TargetRect.Right);

        while (Run >= 0) and (TargetRect.Left < VT_.ClientWidth) do
        begin
          if not NotEditColumns.CheckBlocked(Run) then
          begin
            //Text := 'Filter - ' + IntToStr(Run);
            Text := FilterDict_[Run];
            if Text.Length = 0 then Text := '*';

            //WriteLn(Run, ': ', TargetRect.Left, ', ', TargetRect.Right);

            //ClipCanvas(fFilterBitmap.Canvas, TargetRect);

            DrawTextRect := TargetRect;
            InflateRect(DrawTextRect, -8, 0);
            if TextWidth(Text) > DrawTextRect.Width then
              Text := ShortenString(Handle, Text, DrawTextRect.Width);

            Winapi.Windows.DrawTextW(
              Handle,
              PWideChar(Text),
              Length(Text),
              DrawTextRect,
              DT_LEFT or
              DT_VCENTER or DT_NOPREFIX or DT_SINGLELINE
            );
          end;

          // 우측선 그리기
          if flRight in FilterLines_ then
          begin
            MoveTo(TargetRect.Right-1, 0);
            LineTo(TargetRect.Right-1, TargetRect.Bottom);
          end;
          Inc(TargetRect.Left, VT_.Header.Columns[Run].Width);

        // 숨겨진 컬럼이 있을경우 밀려서 그려주기 때문에 아래처럼 보완. 18_0102 11:23:24 sunk
          Run := VT_.Header.Columns.GetNextVisibleColumn(Run);

          if Run >= 0 then
            TargetRect.Right := TargetRect.Left + VT_.Header.Columns[Run].Width;
        end;

        DC := GetDCEx(VT_.Handle, 0, DCX_CACHE or DCX_CLIPSIBLINGS or DCX_WINDOW or DCX_VALIDATE);
        try
          BitBlt(
            DC,
            FilterRect.Left, FilterRect.Top,
            FilterBitmap_.Width,
            FilterBitmap_.Height,
            Handle,
            0, 0, SRCCOPY);
        finally
          ReleaseDC(VT_.Handle, DC);
        end;

        FilterBitmap_.FreeImage;
      end;
    end;
  end;

  OrgVSTWndProc_(Message);

  case Message.Msg of
    WM_NCCALCSIZE:
    with TWMNCCalcSize(Message) do
    begin
      // 헤더 높이만큼 필터영역 확보.
      Inc(
        CalcSize_Params^.rgrc[0].Top,
        VT_.Header.Height
      );
    end;

    WM_NCHITTEST:
    //if Message.Result <> HTCLIENT then
    with TWMNCHitTest(Message) do
    if Message.Result = Winapi.Windows.HTNOWHERE then
    begin
      MPos := VT_.ScreenToClient(SmallPointToPoint(Pos));
      if MPos.Y <= 0 then
      begin
        //WriteLn(GetTickCount, ': ' , Result, ', ', MPos.x, ', ', MPos.y );
        Result := HTBORDER;
      end;
    end;

    WM_NCLBUTTONDOWN,
    WM_NCLBUTTONDBLCLK:
    with TWMNCHitMessage(Message) do
    begin
      MPos := VT_.ScreenToClient(Point(XCursor, YCursor + VT_.Header.Height));
      //Inc(MPos.Y, VST.Header.Height);
      //WriteLn('NC 다운 또는 더블클릭 ', MPos.X, ', ', MPos.Y);
      //ReleaseCapture;//SetCapture(VST.Handle);
      FilterRect := TVirtualTreeDesc(VT_).HeaderRect;
      //OffsetRect(FilterRect, 0, -VST.Header.Height);

      Hide(false, false);

      if PtInRect(FilterRect, MPos) then
      begin
        //VT_.SetFocus;
        Self.Visible := false;

        //Write('  필터 영역 ');
        Run := VT_.Header.Columns.ColumnFromPosition(Point(MPos.X - VT_.OffsetX, 0), false);
        //WriteLn(Run);

        if Run >= 0 then
        begin
          ReleaseCapture;
          Show(Run);
        end;

        VT_.Invalidate;
      end;
    end;

    {
    WM_NCLBUTTONUP:
    with TWMNCHitMessage(Message) do
    begin
      WriteLn('NC 업');
      //ReleaseCapture;
    end;
    }

    WM_KEYDOWN:
    with TWMKeyDown(Message) do
    case CharCode of
      Ord('F'):
      if Use_Ctrl_F then
      if GetKeyState(VK_CONTROL) < 0 then
      if not NotEditColumns.CheckBlocked(VT_.FocusedColumn) then
      begin
        //WriteLn('Ctrl+F. 필터단축키 ', VT_.FocusedColumn);
        Show(VT_.FocusedColumn);
      end;
    end;
  end;
end;

{ TVirtualStringTreeHelper }

function TVirtualStringTreeHelper.GetFilterEditCtrl: TVtFilterEdit;
var
  i: Integer;
begin
  for i := 0 to ComponentCount - 1 do
    if Components[i] is TVtFilterEdit then
    begin
      Result := TVtFilterEdit(Components[i]);
      exit;
    end;
  Result := nil;
end;

function TVirtualStringTreeHelper.CreateFilterEdit: TVtFilterEdit;
begin
  if (Self is TVirtualStringTree) and (GetFilterEditCtrl = nil) then
  begin
    Result := TVtFilterEdit.Create(Self, TVirtualStringTree(Self));
    Result.Lines := [flBottom, flRight];
    Result.NotEditColumns.Add(0, true);

    if TVirtualStringTree(Self).DefaultNodeHeight = 18 then
      TVirtualStringTree(Self).Header.Height := 24;
  end else
    Result := nil;
end;

procedure TVirtualStringTreeHelper.DestroyFilterEdit;
var
  VtFilterEdit: TVtFilterEdit;
begin
  if Self is TVirtualStringTree then
  begin
    VtFilterEdit := GetFilterEditCtrl;
    if VtFilterEdit <> nil then
    begin
      VtFilterEdit.InitFilter; // 수동으로 해제 시 필터링된 목록 초기화(Clear) 추가 18_0102 09:54:22 sunk
      VtFilterEdit.Free;
      TVirtualStringTree(Self).Header.Height := TVirtualStringTree(Self).DefaultNodeHeight;
    end;
  end;
end;

end.
