{-----------------------------------------------------------------------------
The contents of this file are subject to the Mozilla Public License
Version 1.1 (the "License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.mozilla.org/MPL/MPL-1.1.html

Software distributed under the License is distributed on an "AS IS" basis,
WITHOUT WARRANTY OF ANY KIND, either expressed or implied. See the License for
the specific language governing rights and limitations under the License.

The Original Code is: JvFixedEditPopUp.PAS, released 2003-03-01.

The Initial Developer of the Original Code is Peter Thornqvist (peter3 at sourceforge dot net)
Portions created by Peter Thornqvist are Copyright (C) 2002 Peter Thornqvist .
All Rights Reserved.

Contributor(s):
Steve Magruder
Remko Bonte

You may retrieve the latest version of this file at the Project JEDI's JVCL home page,
located at http://jvcl.sourceforge.net

Description:
  A unit that can be used to replace the default system popup menu for edit controls. The problem with
  MS implementation is that the Paste command is enabled even when the edit is set to read-only

  By overriding TCustomEdit.GetPopupMenu (virtual), you can return an instance of this popup menu
  instead of the default, i.e:
  interface:

    function GetPopupMenu: TPopupMenu; override;

  implementation:

    function TMyEdit.GetPopupMenu: TPopupMenu;
    begin
      Result := inherited GetPopupMenu;
      if Result = nil then // user has not assigned his own popup menu, so use fixed default
        Result := FixedDefaultEditPopUp(Self);
    end;

  The popup is constructed as a singelton shared between all edit controls using it, so it is
  as resource friendly as we could make it and you should NOT free it after use. The popup is
  not created until first use, so if you don't use it, it doesn't take any resources.

  The popup automatically handles cut, copy, paste, select all, clear and undo events and it's aware
  of and can also handle the ClipboardCommands property in some JVCL edits. Menu items
  are automatically enabled / disabled according to the current state of the edit.

  The popup is "self-translating" based on Windows locale. If you want to
  use resourcestrings and supply your own translations, call FixedDefaultEditPopUseResourceString(True);
  (yes, the name is that long on purpose) *before the first call* to FixedDefaultEditPopUp.

  UPDATE 2003-07-14:
    Rewritten to handle any TWinControl descendant. To make a TWinControl
    component compatible it should implement the following messages and styles:
      * If the control is readonly, it should set ES_READONLY in GWL_STYLE
        (this can be done using the EM_SETREADONLY message for edit descendants)
      * If text can be selected, the control should implement the EM_GETSEL message
      * If the control has undo capability, it should implement the EM_CANUNDO message

    The control should also react to the following messages:
      * Undo: WM_UNDO
      * Cut: WM_CUT
      * Copy: WM_COPY
      * Paste: WM_PASTE
      * Clear: WM_CLEAR
      * Select All: EM_SETSEL with wParam=0 and lParam=-1

History:
3.0:
  2003-09-19:
    - introduced IFixedPopupIntf
    - speed optimation in THiddenPopupObject.GetPopupMenu
-----------------------------------------------------------------------------}
// $Id: JvFixedEditPopUp.pas 11303 2007-05-30 16:57:05Z ahuser $

unit JvFixedEditPopUp;

{$I jvcl.inc}

interface

uses
  {$IFDEF UNITVERSIONING}
  JclUnitVersioning,
  {$ENDIF UNITVERSIONING}
  {$IFDEF CLR}
  System.Text, System.Security, System.Runtime.InteropServices,
  {$ENDIF CLR}
  Windows, Messages, SysUtils, Classes, Controls, Menus, StdCtrls, TypInfo,
  JvTypes;

type
  { IFixedPopupIntf is implemented by a component that supports the
    FixedPopupMenu. }
  IFixedPopupIntf = interface
    ['{2ECA1438-EFA5-460A-B586-C30C04B85FF3}']
    function CanUndo: Boolean;
    function CanRedo: Boolean; // unused
    function CanCut: Boolean;
    function CanCopy: Boolean;
    function CanPaste: Boolean;
    function CanSelectAll: Boolean;
    function HasSelection: Boolean;
    procedure Undo;
    procedure Redo; // unused
    procedure Cut;
    procedure Copy;
    procedure Paste;
    { Delete() deletes the selected text without storing the content to the
      clipboard. It's enabled/disabled in the same way as Cut. }
    procedure Delete;
    procedure SelectAll;
  end;

// Returns a popup menu with the standard actions associated with edit controls (Undo, Cut, Copy, Paste, Delete, Select All).
// The actions are handled autmatically by sending messages (WM_COPY, WM_CUT etc) to the control
function FixedDefaultEditPopup(AEdit: TWinControl; Update: Boolean = True): TPopupMenu;
// Call with Value set to True to use the internal resourcestrings instead of those
// provided by Windows. These strings can subsequently be translated using the ITE.
// By default, the Windows provided strings are used.
procedure FixedDefaultEditPopUseResourceString(Value: Boolean);

// Updates the menu items enabled property
procedure FixedDefaultEditPopupUpdate(AEdit: TWinControl);

{$IFDEF UNITVERSIONING}
const
  UnitVersioning: TUnitVersionInfo = (
    RCSfile: '$URL: https://jvcl.svn.sourceforge.net/svnroot/jvcl/tags/JVCL3_32/run/JvFixedEditPopUp.pas $';
    Revision: '$Revision: 11303 $';
    Date: '$Date: 2007-05-30 18:57:05 +0200 (mer., 30 mai 2007) $';
    LogPath: 'JVCL\run'
  );
{$ENDIF UNITVERSIONING}

implementation

uses
  {$IFDEF CLR}
  JclBase,
  {$ENDIF CLR}
  JvVCL5Utils, JvResources;

type
  THiddenPopupObject = class(TComponent)
  private
    FEdit: TWinControl;
    FPopupMenu: TPopupMenu;
    procedure GetDefaultMenuCaptions;
    procedure DoSelectAll(Sender: TObject);
    procedure DoUndo(Sender: TObject);
    procedure DoDelete(Sender: TObject);
    procedure DoPaste(Sender: TObject);
    procedure DoCut(Sender: TObject);
    procedure DoCopy(Sender: TObject);
    function CanUndo: Boolean;
    function ReadOnly: Boolean;
    function GetTextLen: Integer;
    function SelLength: Integer;
    //function GetPopupMenu: TPopupMenu;
    function GetPopupMenuEx(Update: Boolean): TPopupMenu;
    procedure SetEdit(const Value: TWinControl);
    function GetClipboardCommands: TJvClipboardCommands;
    procedure UpdateItems;
    function GetEditHandle: THandle;
    property EditHandle: THandle read GetEditHandle;
  protected
    procedure Notification(AComponent: TComponent; Operation: TOperation); override;
  public
    destructor Destroy; override;
    property Edit: TWinControl read FEdit write SetEdit;
    // property PopupMenu: TPopupMenu read GetPopupMenu;
  end;

var
  GlobalHiddenPopup: THiddenPopupObject = nil;
  GlobalUseResourceStrings: Boolean = False;

function FixedDefaultEditPopup(AEdit: TWinControl; Update: Boolean = True): TPopupMenu;
begin
  if GlobalHiddenPopup = nil then
    GlobalHiddenPopup := THiddenPopupObject.Create(nil);
  GlobalHiddenPopup.Edit := AEdit;
  Result := GlobalHiddenPopup.GetPopupMenuEx(Update);
end;

procedure FixedDefaultEditPopUseResourceString(Value: Boolean);
begin
  GlobalUseResourceStrings := Value;
end;

procedure FixedDefaultEditPopupUpdate(AEdit: TWinControl);
begin
  if (GlobalHiddenPopup <> nil) and (GlobalHiddenPopup.Edit = AEdit) then
    GlobalHiddenPopup.UpdateItems;
end;

//=== { THiddenPopupObject } =================================================

function THiddenPopupObject.CanUndo: Boolean;
begin
  if Assigned(Edit) and Edit.HandleAllocated then
    Result := SendMessage(EditHandle, EM_CANUNDO, 0, 0) <> 0
  else
    Result := False;
end;

procedure THiddenPopupObject.DoCopy(Sender: TObject);
var
  PopupIntf: IFixedPopupIntf;
begin
  if Assigned(Edit) and Edit.HandleAllocated then
    if Supports(Edit, IFixedPopupIntf, PopupIntf) then
      PopupIntf.Copy
    else
      Edit.Perform(WM_COPY, 0, 0);
end;

procedure THiddenPopupObject.DoCut(Sender: TObject);
var
  PopupIntf: IFixedPopupIntf;
begin
  if Assigned(Edit) and Edit.HandleAllocated then
    if Supports(Edit, IFixedPopupIntf, PopupIntf) then
      PopupIntf.Cut
    else
      Edit.Perform(WM_CUT, 0, 0);
end;

procedure THiddenPopupObject.DoDelete(Sender: TObject);
var
  PopupIntf: IFixedPopupIntf;
begin
  if Assigned(Edit) and Edit.HandleAllocated then
    if Supports(Edit, IFixedPopupIntf, PopupIntf) then
      PopupIntf.Delete
    else
      Edit.Perform(WM_CLEAR, 0, 0);
end;

procedure THiddenPopupObject.DoPaste(Sender: TObject);
var
  PopupIntf: IFixedPopupIntf;
begin
  if Assigned(Edit) and Edit.HandleAllocated then
    if Supports(Edit, IFixedPopupIntf, PopupIntf) then
      PopupIntf.Paste
    else
      Edit.Perform(WM_PASTE, 0, 0);
end;

procedure THiddenPopupObject.DoSelectAll(Sender: TObject);
var
  PopupIntf: IFixedPopupIntf;
begin
  if Assigned(Edit) and Edit.HandleAllocated then
    if Supports(Edit, IFixedPopupIntf, PopupIntf) then
      PopupIntf.SelectAll
    else
      Edit.Perform(EM_SETSEL, 0, -1);
end;

procedure THiddenPopupObject.DoUndo(Sender: TObject);
var
  PopupIntf: IFixedPopupIntf;
begin
  if Assigned(Edit) and Edit.HandleAllocated then
  begin
    if Supports(Edit, IFixedPopupIntf, PopupIntf) then
      PopupIntf.Undo
    else
    begin
      {$IFDEF COMPILER6_UP} // Delphi 5 is not supported
      if Edit is TCustomCombo then
        SendMessage(EditHandle, WM_UNDO, 0, 0)
      else
      {$ENDIF COMPILER6_UP}
        Edit.Perform(WM_UNDO, 0, 0);
    end;
  end;
end;

type
  TIntegerSet = set of 0..SizeOf(Integer) * 8 - 1;

function THiddenPopupObject.GetClipboardCommands: TJvClipboardCommands;
const
  cClipboardCommands = 'ClipboardCommands';
var
  Value: TIntegerSet;
  I: Integer;
begin
  if IsPublishedProp(Edit, cClipboardCommands) then
  begin
    Result := [];
    // does it really have to be this complicated ?!
    Value := TIntegerSet(GetOrdProp(Edit, cClipboardCommands));
    for I := 0 to SizeOf(Integer) * 8 - 1 do
      if I in Value then
        Include(Result, TJvClipboardCommand(I));
  end
  else
    Result := [caCopy, caCut, caPaste, caUndo];
end;

{$IFDEF CLR}
[SuppressUnmanagedCodeSecurity, DllImport(user32, CharSet = CharSet.Auto, SetLastError = True, EntryPoint = 'LoadMenu')]
function LoadMenu(hInstance: HINST; lpMenuId: Integer): HMENU; overload; external;
{$ENDIF CLR}


procedure THiddenPopupObject.GetDefaultMenuCaptions;
const
  BufLen = 255;
var
  H: HMODULE;
  hMenu, hSubMenu: THandle;
  {$IFDEF CLR}
  Buf: StringBuilder;
  {$ELSE}
  Buf: array [0..BufLen] of Char;
  {$ENDIF CLR}
begin
  // get the translated captions from Windows' own default popup:
  H := GetModuleHandle('user32.dll');
  {$IFDEF CLR}
  hMenu := LoadMenu(H, 1);
  {$ELSE}
  hMenu := LoadMenu(H, MakeIntResource(1));
  {$ENDIF CLR}
  if hMenu = 0 then
    Exit;
  try
    hSubMenu := GetSubMenu(hMenu, 0);
    if hSubMenu = 0 then
      Exit;

    {$IFDEF CLR}
    Buf := StringBuilder.Create(BufLen);
    {$ENDIF CLR}
    if GetMenuString(hSubMenu, WM_UNDO, Buf, BufLen, MF_BYCOMMAND) <> 0 then
      FPopupMenu.Items[0].Caption := Buf{$IFDEF CLR}.ToString{$ENDIF};
    if GetMenuString(hSubMenu, WM_CUT, Buf, BufLen, MF_BYCOMMAND) <> 0 then
      FPopupMenu.Items[2].Caption := Buf{$IFDEF CLR}.ToString{$ENDIF};
    if GetMenuString(hSubMenu, WM_COPY, Buf, BufLen, MF_BYCOMMAND) <> 0 then
      FPopupMenu.Items[3].Caption := Buf{$IFDEF CLR}.ToString{$ENDIF};
    if GetMenuString(hSubMenu, WM_PASTE, Buf, BufLen, MF_BYCOMMAND) <> 0 then
      FPopupMenu.Items[4].Caption := Buf{$IFDEF CLR}.ToString{$ENDIF};
    if GetMenuString(hSubMenu, WM_CLEAR, Buf, BufLen, MF_BYCOMMAND) <> 0 then
      FPopupMenu.Items[5].Caption := Buf{$IFDEF CLR}.ToString{$ENDIF};
    if GetMenuString(hSubMenu, EM_SETSEL, Buf, BufLen, MF_BYCOMMAND) <> 0 then
      FPopupMenu.Items[7].Caption := Buf{$IFDEF CLR}.ToString{$ENDIF};
  finally
    DestroyMenu(hMenu);
  end;
end;

{function THiddenPopupObject.GetPopupMenu: TPopupMenu;
begin
  Result := GetPopupMenuEx(True);
end;}

function THiddenPopupObject.GetPopupMenuEx(Update: Boolean): TPopupMenu;
var
  m: TMenuItem;
begin
  if FPopupMenu = nil then
  begin
    FPopupMenu := TPopupMenu.Create(Self);
    { build menu:
      Undo
      -
      Cut
      Copy
      Paste
      Delete
      -
      Select All
    }

    // start off with resourcestrings (in case GetDefaultMenuCaptions fails)
    m := TMenuItem.Create(Self);
    m.Caption := RsUndoItem;
    m.OnClick := DoUndo;
    FPopupMenu.Items.Add(m);

    m := TMenuItem.Create(Self);
    m.Caption := '-';
    FPopupMenu.Items.Add(m);

    m := TMenuItem.Create(Self);
    m.Caption := RsCutItem;
    m.OnClick := DoCut;
    FPopupMenu.Items.Add(m);

    m := TMenuItem.Create(Self);
    m.Caption := RsCopyItem;
    m.OnClick := DoCopy;
    FPopupMenu.Items.Add(m);

    m := TMenuItem.Create(Self);
    m.Caption := RsPasteItem;
    m.OnClick := DoPaste;
    FPopupMenu.Items.Add(m);

    m := TMenuItem.Create(Self);
    m.Caption := RsDeleteItem;
    m.OnClick := DoDelete;
    FPopupMenu.Items.Add(m);

    m := TMenuItem.Create(Self);
    m.Caption := '-';
    FPopupMenu.Items.Add(m);

    m := TMenuItem.Create(Self);
    m.Caption := RsSelectAllItem;
    m.OnClick := DoSelectAll;
    FPopupMenu.Items.Add(m);

    if not GlobalUseResourceStrings then
      GetDefaultMenuCaptions;
  end;
  if Update then
    UpdateItems;
  Result := FPopupMenu;
end;

procedure THiddenPopupObject.UpdateItems;
var
  cc: TJvClipboardCommands;
  ASelLength: Integer;
  AReadOnly: Boolean;
  ATextLen: Integer;
  PopupIntf: IFixedPopupIntf;
begin
  if (Edit <> nil) and Edit.HandleAllocated then
  begin
    cc := GetClipboardCommands;
    FPopupMenu.PopupComponent := Edit;

    if Supports(Edit, IFixedPopupIntf, PopupIntf) then
    begin
      // undo
      FPopupMenu.Items[0].Enabled := (caUndo in cc) and PopupIntf.CanUndo;
      // cut
      FPopupMenu.Items[2].Enabled := (caCut in cc) and PopupIntf.HasSelection and PopupIntf.CanCut;
      // copy
      FPopupMenu.Items[3].Enabled := (caCopy in cc) and PopupIntf.HasSelection and PopupIntf.CanCopy;
      // paste
      FPopupMenu.Items[4].Enabled := (caPaste in cc) and PopupIntf.CanPaste;
      // delete
      FPopupMenu.Items[5].Enabled := PopupIntf.HasSelection and PopupIntf.CanCut;
      // select all
      FPopupMenu.Items[7].Enabled := PopupIntf.CanSelectAll;
    end
    else
    begin
      ASelLength := SelLength;
      AReadOnly := ReadOnly;
      ATextLen := GetTextLen;

      // undo
      FPopupMenu.Items[0].Enabled := (caUndo in cc) and CanUndo;
      // cut
      FPopupMenu.Items[2].Enabled := (ASelLength > 0) and not AReadOnly and (caCut in cc);
      // copy
      FPopupMenu.Items[3].Enabled := (ASelLength > 0) and (caCopy in cc);
      // paste
      FPopupMenu.Items[4].Enabled := not AReadOnly and (caPaste in cc);
      // delete
      FPopupMenu.Items[5].Enabled := (ASelLength > 0) and not AReadOnly { and (caCut in cc)};
      // select all
      FPopupMenu.Items[7].Enabled := (ATextLen > 0) and (ASelLength <> ATextLen);
    end;
  end;
end;


{$IFDEF COMPILER6_UP} // Delphi 5 is not supported
type
  TOpenCustomCombo = class(TCustomCombo);
{$ENDIF COMPILER6_UP}

function THiddenPopupObject.GetEditHandle: THandle;
begin
  Result := Edit.Handle;
  {$IFDEF COMPILER6_UP} // Delphi 5 is not supported
  if Edit is TCustomCombo then
    Result := TOpenCustomCombo(Edit).FEditHandle;
  {$ENDIF COMPILER6_UP}
end;

function THiddenPopupObject.GetTextLen: Integer;
begin
  if (Edit <> nil) and Edit.HandleAllocated then
    Result := Edit.GetTextLen
  else
    Result := 0;
end;

function THiddenPopupObject.ReadOnly: Boolean;
begin
  if (Edit <> nil) and Edit.HandleAllocated then
    Result := GetWindowLong(EditHandle, GWL_STYLE) and ES_READONLY = ES_READONLY
  else
    Result := False;
end;

function THiddenPopupObject.SelLength: Integer;
var
  StartPos, EndPos: Longint;
  MsgResult: Longint;
  {$IFDEF CLR}
  p1, p2: IntPtr;
  {$ENDIF CLR}
begin
  Result := 0;
  if (Edit <> nil) and Edit.HandleAllocated then
  begin
    StartPos := 0;
    EndPos := 0;
    {$IFDEF CLR}
    p1 := Marshal.AllocHGlobal(SizeOf(StartPos));
    p2 := Marshal.AllocHGlobal(SizeOf(EndPos));
    try
      Marshal.StructureToPtr(TObject(StartPos), p1, True);
      Marshal.StructureToPtr(TObject(EndPos), p2, True);
      MsgResult := SendMessage(EditHandle, EM_GETSEL, Longint(p1), Longint(p2));
      StartPos := Marshal.ReadInt32(p1);
      EndPos := Marshal.ReadInt32(p2);
    finally
      Marshal.FreeHGlobal(p1);
      Marshal.FreeHGlobal(p2);
    end;
    {$ELSE}
    MsgResult := SendMessage(EditHandle, EM_GETSEL, Longint(@StartPos), Longint(@EndPos));
    {$ENDIF CLR}
    Result := EndPos - StartPos;
    if (Result <= 0) and (MsgResult > 0) then
      {$IFDEF CLR}
      Result := (MsgResult shr 16) - MsgResult and $0000FFFF;
      {$ELSE}
      Result := LongRec(MsgResult).Hi - LongRec(MsgResult).Lo;
      {$ENDIF CLR}
  end;
end;

procedure THiddenPopupObject.SetEdit(const Value: TWinControl);
begin
  if FEdit <> Value then
  begin
    if FEdit <> nil then
      FEdit.RemoveFreeNotification(Self);
    FEdit := Value;
    if FEdit <> nil then
      FEdit.FreeNotification(Self);
  end;
end;

procedure THiddenPopupObject.Notification(AComponent: TComponent;
  Operation: TOperation);
begin
  inherited Notification(AComponent, Operation);
  if (Operation = opRemove) and (AComponent = FEdit) then
    Edit := nil;
end;

destructor THiddenPopupObject.Destroy;
begin
  Edit := nil;
  inherited Destroy;
end;

initialization
  {$IFDEF UNITVERSIONING}
  RegisterUnitVersion(HInstance, UnitVersioning);
  {$ENDIF UNITVERSIONING}

finalization
  {$IFNDEF CLR}
  FreeAndNil(GlobalHiddenPopup);
  {$ENDIF !CLR}
  {$IFDEF UNITVERSIONING}
  UnregisterUnitVersion(HInstance);
  {$ENDIF UNITVERSIONING}

end.

