{*******************************************************}
{                                                       }
{       Tocsg.Clipboard                                 }
{                                                       }
{       Copyright (C) 2019 kku                          }
{                                                       }
{*******************************************************}
unit Tocsg.Clipboard;

// 잠깐 만들던건.. 여기 Tocsg.Clipboard_Old
interface

uses
  System.SysUtils, Vcl.Clipbrd, Winapi.Windows, Winapi.Messages, System.Classes;

//const
//  arrCbImgFormat: array [0..26] of DWORD = (
//        CF_DIB, CF_DIBV5, CF_BITMAP,
//      // 윈도우 기본 캡쳐 시 사용되는 이미지 포맷 25_0306 16:31:21 kku
//        491661, 49350, 50384, 50209, 48171,
//      // 엑셀 셀 복사 시 사용하는 이미지 포맷
//        16,
//      // 엑셀, 파워포인트에서 사용하는 이미지 포맷
//        49718,
//      // 파워포인트에서 사용하는 이미지 포맷
//        50706, 49350, 49523, 49161, 50707, 49348,
//        49171, 49431, 50703, 49436, 50152, 49166,
//        49349, 49935, 49360, 49298, 14
//        );

// 클립보드 수행시 뜨는 포맷 목록들
//Text
//Debug Output: OnDrawClipboard() - FormatCount : 4 Process kku Assister.exe (4708)
//Debug Output: OnDrawClipboard() - Format(0) : 13 Process kku Assister.exe (4708)
//Debug Output: OnDrawClipboard() - Format(1) : 16 Process kku Assister.exe (4708)
//Debug Output: OnDrawClipboard() - Format(2) : 1 Process kku Assister.exe (4708)
//Debug Output: OnDrawClipboard() - Format(3) : 7 Process kku Assister.exe (4708)
//Capture
//Debug Output: OnDrawClipboard() - FormatCount : 3 Process kku Assister.exe (4708)
//Debug Output: OnDrawClipboard() - Format(0) : 2 Process kku Assister.exe (4708)
//Debug Output: OnDrawClipboard() - Format(1) : 8 Process kku Assister.exe (4708)
//Debug Output: OnDrawClipboard() - Format(2) : 17 Process kku Assister.exe (4708)
//File
//Debug Output: OnDrawClipboard() - FormatCount : 11 Process kku Assister.exe (4708)
//Debug Output: OnDrawClipboard() - Format(0) : 49161 Process kku Assister.exe (4708)
//Debug Output: OnDrawClipboard() - Format(1) : 49359 Process kku Assister.exe (4708)
//Debug Output: OnDrawClipboard() - Format(2) : 50043 Process kku Assister.exe (4708)
//Debug Output: OnDrawClipboard() - Format(3) : 50044 Process kku Assister.exe (4708)
//Debug Output: OnDrawClipboard() - Format(4) : 49439 Process kku Assister.exe (4708)
//Debug Output: OnDrawClipboard() - Format(5) : 49414 Process kku Assister.exe (4708)
//Debug Output: OnDrawClipboard() - Format(6) : 49463 Process kku Assister.exe (4708)
//Debug Output: OnDrawClipboard() - Format(7) : 15 Process kku Assister.exe (4708)
//Debug Output: OnDrawClipboard() - Format(8) : 49158 Process kku Assister.exe (4708)
//Debug Output: OnDrawClipboard() - Format(9) : 49159 Process kku Assister.exe (4708)
//Debug Output: OnDrawClipboard() - Format(10) : 49171 Process kku Assister.exe (4708)
type
// AllocateHWnd사용으로 thread에서 사용주의
  TDrawClipboard = class;
  TNotifyClipboard = procedure(Sender: TDrawClipboard) of object;
  TDrawClipboard = class(TClipboard)
  private
    hWnd_: HWND;
    hNextCBWnd_,
    hAllocHWND_: HWND;
    bReseting_: Boolean;
    procedure WndProc(var msg: TMessage); override;
  protected
    evDrawCB_: TNotifyClipboard;
    procedure SetDrawCBEvent(evDrawCB: TNotifyClipboard);
    procedure process_WM_CHANGECBCHAIN(var msg: TMessage);
    procedure process_WM_DRAWCLIPBOARD(var msg: TMessage);
//    procedure process_WM_CLIPBOARDUPDATE(var msg: TMessage);
  public
    Constructor Create(h: HWND = 0);
    Destructor Destroy; override;
    procedure Stop;
    procedure Reset;

    property OnDrawClipboard: TNotifyClipboard read evDrawCB_ write SetDrawCBEvent;
    property NextCBWnd: HWND read hNextCBWnd_ write hNextCBWnd_;
  end;

  TClipboardHelper = class helper for TClipboard
  public
    function GetSize: Integer;
  end;


function CopyToClipboard(const S: String; bDoClear: Boolean): Boolean;
function ClearClipboard(bImageOnly: Boolean = false): Boolean;
function OpenCloseClipboardTest: Boolean;
function IsClipboardEmpty: Boolean;
function GetClipboardSize: Integer;
function GetClipboardHandles: String;

var
  CF_HTML: DWORD;
  CF_BS1Empty: DWORD;
{
Clipboard has 8 Formats.
49161 = DataObject
49268 = Shell IDList Array
15 = CF_hDrop
49158 = FileName
49159 = FileNameW
49389 = Preferred DropEffect
49380 = Shell Object Offsets
49171 = Ole Private Data
}
implementation
uses
  Vcl.Forms, Tocsg.Exception, Tocsg.Trace, Tocsg.Safe, Vcl.Graphics,
  System.Generics.Collections, Tocsg.Strings;

{ TDrawClipboard }

Constructor TDrawClipboard.Create(h: HWND = 0);
begin
  Inherited Create;
  hWnd_ := h;
  bReseting_ := false;

  hAllocHWND_ := 0;
  Reset;
//  hAllocHWND_ := AllocateHWnd(WndProc);
//  ASSERT(AddClipboardFormatListener(hAllocHWND_) = true);
end;

Destructor TDrawClipboard.Destroy;
begin
  Stop;
//  RemoveClipboardFormatListener(hAllocHWND_);
//  DeallocateHWnd(hAllocHWND_);
//  hAllocHWND_ := 0;
  Inherited;
end;

procedure TDrawClipboard.Stop;
begin
  if hAllocHWND_ <> 0 then
  begin
    if hNextCBWnd_ <> 0 then
    begin
      ChangeClipboardChain(hAllocHWND_, hNextCBWnd_);
      hNextCBWnd_ := 0;
    end;
    DeallocateHWnd(hAllocHWND_);
    hAllocHWND_ := 0;
  end else
  if hWnd_ <> 0 then
  begin
    if hNextCBWnd_ <> 0 then
    begin
      ChangeClipboardChain(hWnd_, hNextCBWnd_);
      hNextCBWnd_ := 0;
    end;
  end;
end;

procedure TDrawClipboard.Reset;
begin
//  bReseting_ := true; // 초기화 할때 재감지 되도록 다시 롤백 24_0716 18:06:45 kku
  try
    Stop;
    if hWnd_ <> 0 then
    begin
      hNextCBWnd_ := SetClipboardViewer(hWnd_);
    end else begin
      hAllocHWND_ := AllocateHWnd(WndProc);
      hNextCBWnd_ := SetClipboardViewer(hAllocHWND_);
    end;
  finally
    bReseting_ := false;
  end;
end;

procedure TDrawClipboard.SetDrawCBEvent(evDrawCB: TNotifyClipboard);
begin
  if @evDrawCB_ <> @evDrawCB then
    evDrawCB_ := evDrawCB;
end;

procedure TDrawClipboard.WndProc(var msg: TMessage);
begin
  case msg.Msg of
    WM_CHANGECBCHAIN : process_WM_CHANGECBCHAIN(msg);
    WM_DRAWCLIPBOARD : process_WM_DRAWCLIPBOARD(msg);
//    WM_CLIPBOARDUPDATE : process_WM_CLIPBOARDUPDATE(msg);
    else
      msg.Result := DefWindowProc(hAllocHWND_, msg.Msg, msg.WParam, msg.LParam);
  end;
end;

procedure TDrawClipboard.process_WM_CHANGECBCHAIN(var msg: TMessage);
begin
  with TWMChangeCBChain(msg) do
  begin
    if (Remove = hNextCBWnd_) then
      hNextCBWnd_ := Next
    else if hNextCBWnd_ <> 0 then
      SendMessage(hNextCBWnd_, WM_CHANGECBCHAIN, Remove, Next);
  end;
end;

procedure TDrawClipboard.process_WM_DRAWCLIPBOARD(var msg: TMessage);
begin
  if bReseting_ then
    exit;

// msg as TWMDrawClipboard
  if Assigned(evDrawCB_) then
    evDrawCB_(Self);
  if hNextCBWnd_ <> 0 then
    SendMessage(hNextCBWnd_, msg.Msg, msg.WParam, msg.LParam);
//    SendMessage(hNextCBWnd_, WM_DRAWCLIPBOARD, 0, 0);
end;

//procedure TDrawClipboard.process_WM_CLIPBOARDUPDATE(var msg: TMessage);
//begin
//  if Assigned(evDrawCB_) then
//    evDrawCB_(Self);
//end;

{
// 클립보드에 CF_HTML 형태로 입력
// _____________________________________________________________________________
procedure SetClipBoardHTML(s: String);
const
  START_FRAGMENT='<!--StartFragment-->';
  END_FRAGMENT  ='<!--EndFragment-->';
var
  gMem: HGLOBAL;
  pBytes: PByteArray;
  i: Integer;
  tmpStr: String;
  LBuffer: TBytes;
begin
  // HTML Format에 맞게 세팅
  tmpStr:='Version:1.0'+#13#10;
  tmpStr:=tmpStr+'StartHTML:~~~~~~~~'+#13#10;
  tmpStr:=tmpStr+'EndHTML:########'+#13#10;
  tmpStr:=tmpStr+'StartFragment:~~~~~~~~'+#13#10;
  tmpStr:=tmpStr+'EndFragment:########'+#13#10;
  tmpStr:=tmpStr+'<html><body>'+#13#10;
  tmpStr:=tmpStr+'<!--StartFragment-->'+s+'<!--EndFragment-->'+#13#10;
  tmpStr:=StringReplace(tmpStr, '~~~~~~~~', Format('%.08d', [Pos('<html', tmpStr)-1]), [rfReplaceAll]);
  tmpStr:=StringReplace(tmpStr, '########', Format('%.08d', [Pos(END_FRAGMENT, tmpStr)+Length(END_FRAGMENT)]), [rfReplaceAll]);
  // 버퍼에 복사
  SetLength(LBuffer, Length(tmpStr)+1);
  for i:=0 to Length(tmpStr) do LBuffer[i]:=Byte(tmpStr[i+1]);
  // 클립보드용 버퍼에 복사
  gMem:=GlobalAlloc(GHND, Length(LBuffer)*2);
  pBytes:=GlobalLock(gMem);
  try
    for i:=0 to Length(LBuffer)+1 do pBytes[i]:=LBuffer[i];
  finally
    GlobalUnlock(gMem);
  end;
  // 클립보드에 쓰기
  OpenClipBoard(0);
  EmptyClipBoard;
  SetClipBoardData(RegisterClipBoardFormat('HTML Format'), gMem);
  CloseClipBoard;
end;
// 클립보드의 CF_HTML 포맷데이터 가져오기
// _____________________________________________________________________________
function GetClipBoardHTML: String;
var
  gMem: HGLOBAL;
  pBytes: PByteArray;
  lenBuffer: DWORD;
  i: Integer;
  CF_HTML: UINT;
begin
  Result:='';
  // CF_HTML 등록
  CF_HTML:=RegisterClipboardFormat('HTML Format');
  // 클립보드를 열어 CF_HTML 데이터가 있으면 읽어와서 반환
  OpenClipBoard(0);
  if ClipBoard.HasFormat(CF_HTML) then begin
    gMem:=GetClipboardData(CF_HTML);
    pBytes:=GlobalLock(gMem);
    try
      lenBuffer:=Length(PChar(pBytes))*2;
      SetLength(Result, lenBuffer);
      for i:=0 to lenBuffer do Result[i+1]:=Chr(pBytes[i]);
      Result:=TEncoding.UTF8.GetString(TBytes(pBytes), 0, lenBuffer);
    finally
      GlobalUnlock(gMem);
    end;
  end;
  CloseClipBoard;
end;
}

{ TClipboardHelper }

const
{$IF DEFINED(UNICODE)}
  CTextFormat = CF_UNICODETEXT;
{$ELSE}
  CTextFormat = CF_TEXT;
{$ENDIF}

function TClipboardHelper.GetSize: Integer;
var
  Data: THandle;
begin
  Result := 0;
  try
    Open;
    Data := GetClipboardData(CTextFormat);
    try
      if Data <> 0 then
        Result := GlobalSize(Data);
    finally
      if Data <> 0 then
        GlobalUnlock(Data);
      Close;
    end;
  except
    on E: Exception do
      ETgException.TraceException(Self, E, 'Fail .. GetSize()');
  end;
end;

{ Function }

function OpenAndClearClipboard: Boolean;
begin
  Result := OpenClipboard(0);
  if Result then
    EmptyClipboard;
end;

function CopyToClipboard(const S: String; bDoClear: Boolean): Boolean;
var
  nDataSize, nBufferSize: Integer;
  hData: HGLOBAL;
  pData: Pointer;
begin
  nDataSize := Length(S) * 2;
  if nDataSize > 0 then
  begin
    if bDoClear then
    begin
      if not OpenAndClearClipboard then
      begin
        Result := false;
        exit;
      end;
    end;
    nBufferSize := nDataSize + 2;
    hData := GlobalAlloc(GMEM_MOVEABLE or GMEM_DDESHARE, nBufferSize);
    if hData <> 0 then
    begin
      pData := GlobalLock(hData);
      if pData <> nil then
      begin
        Move(S[1], pData^, nBufferSize);
        GlobalUnlock(hData);
        SetClipboardData(CF_UNICODETEXT, hData);
      end;
    end;
    if bDoClear then
      CloseClipboard;
  end;
  Result := True;
end;

procedure RemoveClipboardImageFormat;
type
  PCbEnt = ^TCbEnt;
  TCbEnt = record
    uType: UINT;
    pData: Pointer;
    nSize: Integer;
  end;
var
  uFormat: UINT;
  CbTxtFmtList: TList<PCbEnt>; // 해제 필요
  pEnt: PCbEnt;
  hData: THandle;
  hCopy: THandle;
  pData: Pointer;
  i: Integer;
begin
  CbTxtFmtList := TList<PCbEnt>.Create;
  try
    uFormat := EnumClipboardFormats(0);
    while uFormat <> 0 do
    begin
      case uFormat of
        CF_TEXT,
        CF_OEMTEXT,
        CF_UNICODETEXT : ;
        else begin
          if uFormat <> CF_HTML then
          begin
            uFormat := EnumClipboardFormats(uFormat);
            continue;
          end;
        end;
      end;

      if (uFormat <> CF_HTML) and (uFormat > 16) then
      begin
        uFormat := EnumClipboardFormats(uFormat);
        continue;
      end;

      New(pEnt);
      ZeroMemory(pEnt, SizeOf(TCbEnt));
      pEnt.uType := uFormat;
      CbTxtFmtList.Add(pEnt);
      uFormat := EnumClipboardFormats(uFormat);
    end;

    for pEnt in CbTxtFmtList do
    begin
      hData := GetClipboardData(pEnt.uType);
      if hData <> 0 then
      begin
        try
          pEnt.nSize := GlobalSize(hData);
        except
        // 사이즈 가져올때 오류 발생 가능성
          continue;
        end;

        if pEnt.nSize > 0 then
        begin
          pEnt.pData := AllocMem(pEnt.nSize);
          pData := GlobalLock(hData);
          try
            Move(pData^, pEnt.pData^, pEnt.nSize);
          finally
            GlobalUnlock(hData);
          end;
        end;
      end;
    end;

  // 클립보드 비우기
    EmptyClipboard;

  // 기존 클립보드 데이터를 복원
    for pEnt in CbTxtFmtList do
    begin
      hData := GlobalAlloc(GMEM_MOVEABLE or GMEM_DDESHARE, pEnt.nSize);
      if hData <> 0 then
      begin
        try
          pData := GlobalLock(hData);
          try
            CopyMemory(pData, pEnt.pData, pEnt.nSize);
          finally
            GlobalUnlock(hData);
          end;
          SetClipboardData(pEnt.uType, hData);
        except
          GlobalFree(hData);
        end;
      end;

      if pEnt.pData <> nil then
      begin
        FreeMem(pEnt.pData);
        pEnt.pData := nil;
      end;
    end;
  finally
    FreeAndNil(CbTxtFmtList);
  end;
end;

function ClearClipboard(bImageOnly: Boolean = false): Boolean;
var
  i: Integer;
  hData: THandle;
  pData: Pointer;
begin
  Result := false;
  try
//    Guard(CB, TClipboard.Create);
//    CB.AsText := '';
//    Result := CB.AsText = '';
//    CB.Clear

//bImageOnly := true;
    if not OpenClipboard(0) then
    begin
    // todo : 실패하면 재시도 할지... 25_0310 10:12:10 kku
    {$IFDEF DEBUG}
      TTgTrace.T('ClearClipboard() .. Fail .. OpenClipboard()');
    {$ENDIF}
      exit;
    end;

    try
      if bImageOnly then
      begin
//        TTgTrace.T('ClearClipboard() .. Image');
        RemoveClipboardImageFormat;
      end else begin
        EmptyClipboard;
      end;

    {$IFDEF _HE_}
      hData := GlobalAlloc(GMEM_MOVEABLE or GMEM_DDESHARE, 1);
      if hData <> 0 then
      begin
        try
          pData := GlobalLock(hData);
          try
            CopyMemory(pData, PAnsiChar(#0), 1);
            SetClipboardData(CF_BS1Empty, hData);
          finally
            GlobalUnlock(hData);
          end;
        except
          GlobalFree(hData);
        end;
      end;
    {$ENDIF}

      Result := true;
    finally
      CloseClipboard;
    end;
  except
    on E: Exception do
      ETgException.TraceException(E, 'Fail .. ClearClipboard()');
  end;
end;

function OpenCloseClipboardTest: Boolean;
begin
  Result := false;
  try
    if not OpenClipboard(0) then
      exit;
    CloseClipboard;
    Result := true;
  except
    on E: Exception do
      ETgException.TraceException(E, 'Fail .. OpenCloseClipboardTest()');
  end;
end;

function IsClipboardEmpty: Boolean;
var
  uFormat: UINT;
  i, nCnt: Integer;
begin
// 사용하지 않음. 나중에 지워도 됨 25_0310 10:11:31 kku
  Result := false;

  if not OpenClipboard(0) then
    exit;

  try
    nCnt := 0;
    while uFormat <> 0 do
    begin
      Inc(nCnt);
      uFormat := EnumClipboardFormats(uFormat);
    end;
    Result := nCnt > 0;
  finally
    CloseClipboard;
  end;
end;

function GetClipboardSize: Integer;
var
  uFormat: UINT;
  hData: THandle;
begin
// 사용하지 않음. 나중에 지워도 됨 25_0310 10:11:31 kku
  Result := 0;
  if not OpenClipboard(0) then
    exit;
  try
    uFormat := EnumClipboardFormats(0);
    while uFormat <> 0 do
    begin
      hData := GetClipboardData(uFormat);
      try
        if hData <> 0 then
          Inc(Result, GlobalSize(hData));
      except
      {$IFDEF DEBUG}
        TTgTrace.T('Fail .. GetClipboardSize() .. GlobalSize() .. hData = %d', [hData]);
      {$ENDIF}
      end;
      uFormat := EnumClipboardFormats(uFormat);
    end;
  finally
    CloseClipboard;
  end;
end;

function GetClipboardHandles: String;
var
  uFormat: UINT;
  hData: THandle;
begin
// 사용하지 않음. 나중에 지워도 됨 25_0310 10:11:31 kku
  Result := '';
  if not OpenClipboard(0) then
  begin
    TTgTrace.T('Fail .. GetClipboardHandles() .. 1, Error=%d', [GetLastError]);
    exit;
  end;
  try
    uFormat := EnumClipboardFormats(0);
    if uFormat = 0 then
      TTgTrace.T('Fail .. GetClipboardHandles() .. 2');
    while uFormat <> 0 do
    begin
      hData := GetClipboardData(uFormat);
      SumString(Result, IntToStr(hData), '|');
      uFormat := EnumClipboardFormats(uFormat);
    end;
  finally
    CloseClipboard;
  end;
end;

Initialization
  CF_HTML := RegisterClipboardFormat('HTML Format');
  CF_BS1Empty := RegisterClipboardFormat('BSOne CB Empty'); { Do not localize }

end.
