Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I Prevent Shortcuts from Colliding/Interacting in Delphi?

I use the standard Cut, Copy, Paste actions on my Main Menu. They have the shortcuts Ctrl-X, Ctrl-C and Ctrl-V.

When I open a modal form, e.g. FindFilesForm.ShowModal, then all the shortcuts work from the form.

But when I open a non-modal form, e.g. FindFilesForm.Show, then the shortcuts do not work.

I would think that those actions should work if the FindFilesForm is the active form. It's modality should have nothing to do with it, or am I wrong in my thinking?

Never-the-less, how can I get the shortcuts to work on a non-modal form?


After Cary's response, I researched it further. It is not a problem with certain controls, e.g. TMemo or TEdit.

But it is for some others. Specifically, the ones where it happens include:

  1. the text in a TComboBox
  2. the text in a TFindDialog
  3. a TElTreeInplaceEdit control, part of LMD's ElPack

I'll see if there are others and add them to the list.

These are all on important Non-Modal forms in my program.

So I still need a solution.


Okay. I really need help with this. So this becomes the first question I am putting a bounty on.

My discussion with Cary that takes place through his answer and the comments there describe my problem in more detail.

And as I mentioned in one of those comments, a related problem seems to be discussed here.

What I need is a solution or a workaround, that will allow the Ctrl-X, Ctrl-C and Ctrl-V to always work in a TComboBox and TFindDialog in a Non-Modal window. If those two get solved, I'm sure my TElTreeInplaceEdit will work as well.

It takes only a couple of minutes to set up an simple test program as Cary describes. Hopefully someone will be able to solve this.

Just be wary that there seems to be something that allows it to work sometimes but not work other times. If I can isolate that in more detail, I'll report it here.

Thanks for any help you can offer me.


Mghie worked very hard to find a solution, and his OnExecute handler combined with his ActionListUpdate handler do the trick. So for his effort, I'm giving him the accepted solution and the bounty points.

But his actionlist update handler is not simple and you need to specify in it all the cases you want to handle. Let's say there's also Ctrl+A for select all or Ctrl-Y for undo you might want. A general procedure would be better.

So if you do come across this question in your search for the answer, try first the answer I supplied that adds an IsShortcut handler. It worked for me and should handle every case and does not need the OnExecute handlers, so is much simpler. Peter Below wrote that code and Uwe Molzhan gets finders fee.

Thanks Cary, mghie, Uwe and Peter for helping me solve this. Couldn't have done it without you. (Maybe I could have, but it might have taken me 6 months.)

like image 985
lkessler Avatar asked Dec 05 '09 18:12

lkessler


1 Answers

OK, first thing first: This has nothing to do with modal or non-modal forms, it is a limitation of the way the Delphi action components work (if you want to call it that).

Let me prove this by a simple example: Create a new application with a new form, drop a TMemo and a TComboBox onto it, and run the application. Both controls will have the system-provided context menu with the edit commands, and will correctly react on them. They will do the same for the menu shortcuts, with the exception of Ctrl + A which isn't supported for the combo box.

Now add a TActionList component with the three standard actions for Cut, Copy and Paste. Things will still work, no changes in behaviour.

Now add a main menu, and add the Edit Menu from the template. Delete all commands but those for Cut, Copy and Paste. Set the corresponding action components for the menu items, and run the application. Observe how the combo box still has the context menu and the commands there still work, but that the shortcuts do no longer work.


The problem is that the standard edit actions have been designed to work with TCustomEdit controls only. Have a look at the TEditAction.HandlesTarget() method in StdActns.pas. Since edit controls in combo boxes, inplace editors in tree controls or edit controls in native dialogs are not caught by this they will not be handled. The menu commands will always be disabled when one of those controls has the focus. As for the shortcuts working only some of the time - this depends on whether the VCL does at some point map the shortcuts to action commands or not. If it doesn't, then they will finally reach the native window procedure and initiate the edit command. In this case the shortcuts will still work. I assume that for modal dialogs the action handling is suspended, so the behaviour is different between modal and non-modal dialogs.

To work around this you can provide handlers for OnExecute of these standard actions. For example for the Paste command:

procedure TMainForm.EditPaste1Execute(Sender: TObject);
var
  FocusWnd: HWND;
begin
  FocusWnd := GetFocus;
  if IsWindow(FocusWnd) then
    SendMessage(FocusWnd, WM_PASTE, 0, 0);
end;

and similar handlers for the Cut command (WM_CUT) and the Copy command (WM_COPY). Doing this in the little demo app makes things work again for the combo box. You should try in your application, but I assume this will help. It's a harder task to correctly enable and disable the main menu commands for all native edit controls. Maybe you could send the EM_GETSEL message to check whether the focused edit control has a selection.

Edit:

More info why the behaviour is different between combo boxes on modal vs. non-modal dialogs (analysis done on Delphi 2009): The interesting code is in TWinControl.IsMenuKey() - it tries to find an action component in one of the action lists of the parent form of the focused control which handles the shortcut. If that fails it sends a CM_APPKEYDOWN message, which ultimately leads to the same check being performed with the action lists of the application's main form. But here's the thing: This will be done only if the window handle of the application's main form is enabled (see TApplication.IsShortCut() code). Now calling ShowModal() on a form will disable all other forms, so unless the modal dialog contains itself an action with the same shortcut the native shortcut handling will work.

Edit:

I could reproduce the problem - the key is to somehow get the edit actions become disabled. In retrospect this is obvious, the Enabled property of the actions needs of course to be updated too.

Please try with this additional event handler:

procedure TForm1.ActionList1Update(Action: TBasicAction; var Handled: Boolean);
var
  IsEditCtrl, HasSelection, IsReadOnly: boolean;
  FocusCtrl: TWinControl;
  FocusWnd: HWND;
  WndClassName: string;
  SelStart, SelEnd: integer;
  MsgRes: LRESULT;
begin
  if (Action = EditCut1) or (Action = EditCopy1) or (Action = EditPaste1) then
  begin
    IsEditCtrl := False;
    HasSelection := False;
    IsReadOnly := False;

    FocusCtrl := Screen.ActiveControl;
    if (FocusCtrl <> nil) and (FocusCtrl is TCustomEdit) then begin
      IsEditCtrl := True;
      HasSelection := TCustomEdit(FocusCtrl).SelLength > 0;
      IsReadOnly := TCustomEdit(FocusCtrl).ReadOnly;
    end else begin
      FocusWnd := GetFocus;
      if IsWindow(FocusWnd) then begin
        SetLength(WndClassName, 64);
        GetClassName(FocusWnd, PChar(WndClassName), 64);
        WndClassName := PChar(WndClassName);
        if AnsiCompareText(WndClassName, 'EDIT') = 0 then begin
          IsEditCtrl := True;
          SelStart := 0;
          SelEnd := 0;
          MsgRes := SendMessage(FocusWnd, EM_GETSEL, WPARAM(@SelStart),
            LPARAM(@SelEnd));
          HasSelection := (MsgRes <> 0) and (SelEnd > SelStart);
        end;
      end;
    end;

    EditCut1.Enabled := IsEditCtrl and HasSelection and not IsReadOnly;
    EditCopy1.Enabled := IsEditCtrl and HasSelection;
    // don't hit the clipboard three times
    if Action = EditPaste1 then begin
      EditPaste1.Enabled := IsEditCtrl and not IsReadOnly
        and Clipboard.HasFormat(CF_TEXT);
    end;
    Handled := TRUE;
  end;
end;

I didn't check for the native edit control being read-only, this could probably be done by adding this:

IsReadOnly := GetWindowLong(FocusWnd, GWL_STYLE) and ES_READONLY <> 0;

Note: I've given mghie the answer as he did a lot of work and his answer is correct, but I have implemented a simpler solution that I added as an answer myself

like image 134
mghie Avatar answered Sep 21 '22 18:09

mghie