Notification on Redo events in Office 2010/2013

Add-in Express™ Support Service
That's what is more important than anything else

Notification on Redo events in Office 2010/2013
 
Igor Govorov




Posts: 83
Joined: 2014-02-12
Hi,

We are trying to intercept Paste event in Office as described at https://www.add-in-express.com/forum/read.php?FID=5&TID=12302.
We are checking last Undo record and if it is "Paste", then we can tell that user have done a Paste action.
However, there is a problem when user presses Undo and then Redo buttons after pasting some data to a document.
After Redo there will be "Paste" in Undo list, but no actual Paste was done at the moment.

Is there a way to catch Redo events in Office? Or is there more deterministic way to catch Paste (including paste from Gallery)?
Posted 29 Mar, 2015 05:50:47 Top
Andrei Smolin


Add-in Express team


Posts: 18825
Joined: 2006-05-11
Hello Igor,

You can use an ADXRibbonCommand to intercept the built-in command with IdMso="Redo". The Ribbon API provides no way to intercept (or get notified about) clicking an item in a gallery.


Andrei Smolin
Add-in Express Team Leader
Posted 30 Mar, 2015 01:50:03 Top
Igor Govorov




Posts: 83
Joined: 2014-02-12
Hello Andrei,

Andrei Smolin writes:
You can use an ADXRibbonCommand to intercept the built-in command with IdMso="Redo"


I've tried to do this and have got the error:

Error found in Custom UI XML of "addin":
Line: 1
Column: 133
Error Code 0x80004005 "Redo" is not a repurposable command. Only "button", toggleButton" and "checkbox" can have onAction callbacks in <commands>.
Posted 30 Mar, 2015 03:49:46 Top
Andrei Smolin


Add-in Express team


Posts: 18825
Joined: 2006-05-11
Igor,

That was my fault. I should have remembered that Undo and Redo are galleries in the Ribbon UI and you can do nothing with a built-in Ribbon gallery.

Here's the code I've used to read the state of the CommandBar version of the Undo and Redo controls:

Office.CommandBarComboBox undoComboBox = null;
Office.CommandBarButton redoButton = null;
private void ReadUndoandRedo_OnClick(object sender, IRibbonControl control, bool pressed) {
    Office.CommandBars commandBars = ExcelApp.CommandBars;
    undoComboBox = commandBars.FindControl(Office.MsoControlType.msoControlSplitDropdown, 128, Type.Missing, false) as Office.CommandBarComboBox;
    if (undoComboBox != null) {
        int count = 0;
        try {
            count = undoComboBox.ListCount;
        } catch { }
        System.Diagnostics.Debug.WriteLine("!!! printing undoComboBox. ListCount=" + count.ToString());

        for (int i = 1; i <= count; i++) {
            System.Diagnostics.Debug.WriteLine("!!! " + i.ToString() + ". " + undoComboBox.get_List(i));
        }
    } else {
        System.Diagnostics.Debug.WriteLine("!!! undoComboBox is null");
    }
    redoButton = commandBars.FindControl(Office.MsoControlType.msoControlButton, 37, Type.Missing, false) as Office.CommandBarButton;
    if (redoButton != null) {
        System.Diagnostics.Debug.WriteLine("!!! redoControl != null. Enabled=" + redoButton.Enabled.ToString() + ". Text=" + redoButton.Caption );
    } else {
        System.Diagnostics.Debug.WriteLine("!!! redoControl is null");
    }
    Marshal.ReleaseComObject(commandBars);
}


Here're my results; the below shows the sates of the Undo and Redo controls when I perform the following actions:

Start Excel
!!! printing undoComboBox. ListCount=0
!!! redoControl != null. Enabled=False. Text=Can't &Repeat

Enter some text to a cell
!!! printing undoComboBox. ListCount=1
!!! 1. Typing '123' in A1
!!! redoControl != null. Enabled=False. Text=Can't &Repeat

Copy the cell, paste to another cell, press {ESC} to quit the CutCopy mode (note that pressing {ENTER} instead of {ESC} adds two Paste entries to the Undo list):
!!! printing undoComboBox. ListCount=2
!!! 1. Paste
!!! 2. Typing '123' in A1
!!! redoControl != null. Enabled=False. Text=Can't &Repeat

Click Undo
!!! printing undoComboBox. ListCount=1
!!! 1. Typing '123' in A1
!!! redoControl != null. Enabled=True. Text=&Redo Paste

Click Redo
!!! printing undoComboBox. ListCount=2
!!! 1. Paste
!!! 2. Typing '123' in A1
!!! redoControl != null. Enabled=False. Text=Can't &Repeat

I believe you can use this approach to solve your task.


Andrei Smolin
Add-in Express Team Leader
Posted 30 Mar, 2015 05:04:05 Top
Igor Govorov




Posts: 83
Joined: 2014-02-12
Andrei,

Thanks for the code. We are already using similar code for the checks (But instead of Redo id=37 we have used id=129).

But we still have a problem when doing Redo action.
After your step "Click Redo" we have "Paste" as a first item in undoComboBox.
But you already have done Paste in "Copy the cell, paste to another cell..." step.
As a result we have "False Positive" for Paste action - we have an information associated with the current clipboard state, but in case of Redo this is not relevant.
I've thought that catching Redo click could solve the problem, but, as I see now - we can't use this approach.
Do you have any more suggestions or directions we can investigate to solve the problem?

Thank you,
Igor
Posted 30 Mar, 2015 07:05:29 Top
Andrei Smolin


Add-in Express team


Posts: 18825
Joined: 2006-05-11
Igor,

You need take into account the change of the Redo control's state. Although the state of the Undo and Redo controls is the same in 1.2 and 2.2 below, the state of the Redo control differs in 1.1 and 2.1:

1. ============
1.1. Enter some text to a cell
!!! printing undoComboBox. ListCount=1
!!! 1. Typing '123' in A1
!!! redoControl != null. Enabled=False. Text=Can't &Repeat

1.2. Copy the cell, paste to another cell, press {ESC} to quit the CutCopy mode (note that pressing {ENTER} instead of {ESC} adds two Paste entries to the Undo list):
!!! printing undoComboBox. ListCount=2
!!! 1. Paste
!!! 2. Typing '123' in A1
!!! redoControl != null. Enabled=False. Text=Can't &Repeat
=============

2. ============
2.1. Click Undo
!!! printing undoComboBox. ListCount=1
!!! 1. Typing '123' in A1
!!! redoControl != null. Enabled=True. Text=&Redo Paste

2.2. Click Redo
!!! printing undoComboBox. ListCount=2
!!! 1. Paste
!!! 2. Typing '123' in A1
!!! redoControl != null. Enabled=False. Text=Can't &Repeat
=============


Andrei Smolin
Add-in Express Team Leader
Posted 30 Mar, 2015 07:14:43 Top
Igor Govorov




Posts: 83
Joined: 2014-02-12
The states 1.1 and 2.1 are not interesting.
We are considering that there were paste when first item in undo list is "Paste" as after steps 1.2 and 2.2.
And after steps 1.2 and 2.2 the state of the Redo control is the same.
If instead of step 2.2 I'll make a paste of some information from another source then I'll get the same Undo and Redo controls state as in step 2.2.
Posted 30 Mar, 2015 07:26:53 Top
Andrei Smolin


Add-in Express team


Posts: 18825
Joined: 2006-05-11
I see. I will need some time to investigate this. I suppose I'll have something tomorrow or the day after tomorrow.


Andrei Smolin
Add-in Express Team Leader
Posted 30 Mar, 2015 09:34:32 Top
Andrei Smolin


Add-in Express team


Posts: 18825
Joined: 2006-05-11
Hello Igor,

I've looked into getting a notification when the clipboard changes. That is, at some moment - you need to define it; I suppose this should be WindowDeactivate - you connect to clipboard events. If you get a clipboard change notification, you disable the Redo Ribbon control - this leaves the user with the only choice: to paste the new clipboard content. Without this limitation you cannot identify whether the user pastes or clicks Redo. You disconnect from clipboard events at another moment, I think this should be WindowActivate.

Below are the code blocks I've used.

Still, this doesn't cover clicking an item on the Office Clipboard Pane.


Andrei Smolin
Add-in Express Team Leader

HiddenWindow hiddenWindow = null;
private void ConnectClipboardEvents() {
    if (hiddenWindow != null)
        hiddenWindow.Dispose();
    hiddenWindow = new HiddenWindow("hidden window caption", this);
    // the HiddenWindow handles clipboard events. When the clipboard changes, the ClipboardChanged method is called (see below)
}

private void DisconnectClipboardEvents() {
    if (hiddenWindow != null) {
        hiddenWindow.Dispose();
    }
    if (wasRedoDisabled) EnableRedo(true);
}

/// <summary>
/// Called by the HiddenWindow class when the clipboard changes
/// </summary>
public void ClipboardChanged() {
    EnableRedo(false);
}

bool wasRedoDisabled = false;
private void EnableRedo(bool newState) {
    wasRedoDisabled = !newState;
    adxRibbonCommandRedo.Enabled = newState;

    // optionaly disable the CommandBar-style version of Redo
    Office.CommandBars commandBars = ExcelApp.CommandBars;
    redoButton = commandBars.FindControl(Office.MsoControlType.msoControlButton, 37, Type.Missing, false) as Office.CommandBarButton;
    if (redoButton != null) {
        redoButton.Enabled = newState;
    } else {
        System.Diagnostics.Debug.WriteLine("!!! redoControl is null");
    }
    Marshal.ReleaseComObject(redoButton);
    Marshal.ReleaseComObject(commandBars);
}


/// <summary>
/// This class is based on code from https://web.archive.org/web/20131104125500/http://www.radsoftware.com.au/articles/clipboardmonitor.aspx.
/// </summary>
class HiddenWindow : System.Windows.Forms.NativeWindow, IDisposable {

    private bool disposed = false;
    private AddinExpress.MSO.ADXAddinModule module;
    public int resultValue = 0;

    /// <summary>
    /// allows ignoring the very first WM_DRAWCLIPBOARD; it looks like WM_DRAWCLIPBOARD occurs as soon as SetClipboardViewer is called
    /// </summary>
    private bool isReady = false;

    public HiddenWindow(string caption, AddinExpress.MSO.ADXAddinModule module) {
        this.module = module;
        CreateParams Params = new CreateParams();
        Params.Caption = caption;
        this.CreateHandle(Params);
        RegisterClipboardViewer();
        isReady = true;
    }

    public void Dispose() {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    private void Dispose(bool disposing) {
        if (!this.disposed) {
            UnregisterClipboardViewer();
        }
        if (!this.disposed) {
            if (!this.Handle.Equals(IntPtr.Zero)) {
                this.DestroyHandle();
            }
        }
        disposed = true;
    }

    ~HiddenWindow() {
        Dispose(false);
    }

    IntPtr _clipboardViewerNext = IntPtr.Zero;

    private void RegisterClipboardViewer() {
        _clipboardViewerNext = User32.SetClipboardViewer(this.Handle);
        Debug.WriteLine("!!! HiddenWindow. Clipboard events connected");
    }

    /// <summary>
    /// Remove this form from the Clipboard Viewer list
    /// </summary>
    private void UnregisterClipboardViewer() {
        User32.ChangeClipboardChain(this.Handle, _clipboardViewerNext);
        Debug.WriteLine("!!! HiddenWindow. Clipboard events disconnected");
    }

    protected override void WndProc(ref Message m) {
        if (!isReady) {
            // Let the window process the messages that we are
            // not interested in
            //
            base.WndProc(ref m);
            return;
        }
        switch ((Msgs)m.Msg) {
            //
            // The WM_DRAWCLIPBOARD message is sent to the first window 
            // in the clipboard viewer chain when the content of the 
            // clipboard changes. This enables a clipboard viewer 
            // window to display the new content of the clipboard. 
            //
            case Msgs.WM_DRAWCLIPBOARD:
                Debug.WriteLine("!!! WindowProc DRAWCLIPBOARD: " + m.Msg, "WndProc");
                // notify the module
                _ExcelPanes.AddinModule.CurrentInstance.ClipboardChanged();
                //
                // Each window that receives the WM_DRAWCLIPBOARD message 
                // must call the SendMessage function to pass the message 
                // on to the next window in the clipboard viewer chain.
                //
                User32.SendMessage(_clipboardViewerNext, m.Msg, m.WParam, m.LParam);
                break;
            //
            // The WM_CHANGECBCHAIN message is sent to the first window 
            // in the clipboard viewer chain when a window is being 
            // removed from the chain. 
            //
            case Msgs.WM_CHANGECBCHAIN:
                Debug.WriteLine("!!! WM_CHANGECBCHAIN: lParam: " + m.LParam, "WndProc");
                // When a clipboard viewer window receives the WM_CHANGECBCHAIN message, 
                // it should call the SendMessage function to pass the message to the 
                // next window in the chain, unless the next window is the window 
                // being removed. In this case, the clipboard viewer should save 
                // the handle specified by the lParam parameter as the next window in the chain. 
                //
                // wParam is the Handle to the window being removed from 
                // the clipboard viewer chain 
                // lParam is the Handle to the next window in the chain 
                // following the window being removed. 
                if (m.WParam == _clipboardViewerNext) {
                    //
                    // If wParam is the next clipboard viewer then it
                    // is being removed so update pointer to the next
                    // window in the clipboard chain
                    //
                    _clipboardViewerNext = m.LParam;
                } else {
                    User32.SendMessage(_clipboardViewerNext, m.Msg, m.WParam, m.LParam);
                }
                break;
            default:
                //
                // Let the window process the messages that we are
                // not interested in
                //
                base.WndProc(ref m);
                break;
        }
    }
}
Posted 31 Mar, 2015 05:46:04 Top
Igor Govorov




Posts: 83
Joined: 2014-02-12
Andrei,

Thank you for your reply.
We are already using clipboard viewer to detect clipboard changes.

Unfortunately, we can't disable the Redo Ribbon control since it will harm user experience.
Posted 01 Apr, 2015 08:51:44 Top