tdf#126638 dispatch key shortcut events to modal windows

Some modal windows, such as the native Open and Save dialogs,
return NO from -[NSWindow performKeyEquivalent:]. Fortunately,
the main menu's -[NSMenu performKeyEquivalent:] is then called
so we can catch and redirect any modal window's key shortcut
events without triggering the modal window's "disallowed
action" beep.

Change-Id: Ib1fff68ab159361ceb847881e3a4da4736a33f51
Reviewed-on: https://gerrit.libreoffice.org/c/core/+/163605
Tested-by: Jenkins
Reviewed-by: Patrick Luby <guibomacdev@gmail.com>
(cherry picked from commit 64ca3756416f0355b2008f39120e68ac42269784)
Reviewed-on: https://gerrit.libreoffice.org/c/core/+/163618
Reviewed-by: Adolfo Jayme Barrientos <fitojb@ubuntu.com>
diff --git a/vcl/inc/osx/salnsmenu.h b/vcl/inc/osx/salnsmenu.h
index 696abca..9e0f9ac 100644
--- a/vcl/inc/osx/salnsmenu.h
+++ b/vcl/inc/osx/salnsmenu.h
@@ -35,17 +35,25 @@ class AquaSalMenuItem;
{
    AquaSalMenu* mpMenu;
}
+ (BOOL)dispatchSpecialKeyEquivalents:(NSEvent*)pEvent;
- (id)initWithMenu:(AquaSalMenu*)pMenu;
- (void)menuNeedsUpdate:(NSMenu*)pMenu;
- (void)setSalMenu:(AquaSalMenu*)pMenu;
@end

@interface SalNSMenuItem : NSMenuItem
@interface SalNSMenuItem : NSMenuItem <NSMenuItemValidation>
{
    AquaSalMenuItem* mpMenuItem;
}
- (id)initWithMenuItem:(AquaSalMenuItem*)pMenuItem;
- (void)menuItemTriggered:(id)aSender;
- (BOOL)validateMenuItem:(NSMenuItem*)pMenuItem;
@end

@interface SalNSMainMenu : NSMenu
{
}
- (BOOL)performKeyEquivalent:(NSEvent*)pEvent;
@end

#endif // INCLUDED_VCL_INC_OSX_SALNSMENU_H
diff --git a/vcl/osx/salmenu.cxx b/vcl/osx/salmenu.cxx
index b3d02587..6ea16a6 100644
--- a/vcl/osx/salmenu.cxx
+++ b/vcl/osx/salmenu.cxx
@@ -125,7 +125,9 @@ static void initAppMenu()
    NSMenu* pAppMenu = nil;
    NSMenuItem* pNewItem = nil;

    NSMenu* pMainMenu = [[[NSMenu alloc] initWithTitle: @"Main Menu"] autorelease];
    // Related: tdf#126638 use NSMenu subclass to catch and redirect key
    // shortcuts when a modal window is displayed
    SalNSMainMenu* pMainMenu = [[[SalNSMainMenu alloc] initWithTitle: @"Main Menu"] autorelease];
    pNewItem = [pMainMenu addItemWithTitle: @"Application"
        action: nil
        keyEquivalent: @""];
@@ -230,12 +232,19 @@ AquaSalMenu::AquaSalMenu( bool bMenuBar ) :
    {
        mpMenu = [[SalNSMenu alloc] initWithMenu: this];
        [mpMenu setDelegate: reinterpret_cast< id<NSMenuDelegate> >(mpMenu)];

        // Related: tdf#126638 enable the menu's "autoenabledItems" property
        // Enable the menu's "autoenabledItems" property so that
        // -[SalNSMenuItem validateMenuItem:] will be called before handling
        // a key shortcut and the menu item can be temporarily disabled if a
        // modal window is displayed.
        [mpMenu setAutoenablesItems: YES];
    }
    else
    {
        mpMenu = [NSApp mainMenu];
        [mpMenu setAutoenablesItems: NO];
    }
    [mpMenu setAutoenablesItems: NO];
}

AquaSalMenu::~AquaSalMenu()
diff --git a/vcl/osx/salnsmenu.mm b/vcl/osx/salnsmenu.mm
index b2df2da..31627a3 100644
--- a/vcl/osx/salnsmenu.mm
+++ b/vcl/osx/salnsmenu.mm
@@ -30,6 +30,53 @@
#include <osx/salnsmenu.h>

@implementation SalNSMenu

+(BOOL)dispatchSpecialKeyEquivalents: (NSEvent*)pEvent
{
    if( pEvent && [pEvent type] == NSEventTypeKeyDown )
    {
        unsigned int nModMask = ([pEvent modifierFlags] & (NSEventModifierFlagShift|NSEventModifierFlagControl|NSEventModifierFlagOption|NSEventModifierFlagCommand));
        if( nModMask == NSEventModifierFlagCommand )
        {
            if( [[pEvent charactersIgnoringModifiers] isEqualToString: @"v"] )
            {
                if( [NSApp sendAction: @selector(paste:) to: nil from: nil] )
                    return YES;
            }
            else if( [[pEvent charactersIgnoringModifiers] isEqualToString: @"c"] )
            {
                if( [NSApp sendAction: @selector(copy:) to: nil from: nil] )
                    return YES;
            }
            else if( [[pEvent charactersIgnoringModifiers] isEqualToString: @"x"] )
            {
                if( [NSApp sendAction: @selector(cut:) to: nil from: nil] )
                    return YES;
            }
            else if( [[pEvent charactersIgnoringModifiers] isEqualToString: @"a"] )
            {
                if( [NSApp sendAction: @selector(selectAll:) to: nil from: nil] )
                    return YES;
            }
            else if( [[pEvent charactersIgnoringModifiers] isEqualToString: @"z"] )
            {
                if( [NSApp sendAction: @selector(undo:) to: nil from: nil] )
                    return YES;
            }
        }
        else if( nModMask == (NSEventModifierFlagCommand|NSEventModifierFlagShift) )
        {
            if( [[pEvent charactersIgnoringModifiers] isEqualToString: @"Z"] )
            {
                if( [NSApp sendAction: @selector(redo:) to: nil from: nil] )
                    return YES;
            }
        }
    }

    return NO;
}

-(id)initWithMenu: (AquaSalMenu*)pMenu
{
    mpMenu = pMenu;
@@ -167,6 +214,19 @@
            OSL_FAIL( "menubar item without frame !" );
    }
}

-(BOOL)validateMenuItem: (NSMenuItem *)pMenuItem
{
    // Related: tdf#126638 disable all menu items when displaying modal windows
    // For some unknown reason, key shortcuts are dispatched to the LibreOffice
    // menu items instead of the modal window so disable all LibreOffice menu
    // items while a native modal dialog such as the native Open, Save, or
    // Print dialog is displayed.
    if (!pMenuItem || [NSApp modalWindow])
        return NO;

    return [pMenuItem isEnabled];
}
@end

@implementation OOStatusItemView
@@ -257,5 +317,25 @@ SAL_WNODEPRECATED_DECLARATIONS_POP
}
@end

@implementation SalNSMainMenu

- (BOOL)performKeyEquivalent:(NSEvent*)pEvent
{
    BOOL bRet = [super performKeyEquivalent: pEvent];

    // tdf#126638 dispatch key shortcut events to modal windows
    // Some modal windows, such as the native Open and Save dialogs,
    // return NO from -[NSWindow performKeyEquivalent:]. Fortunately,
    // the main menu's -[NSMenu performKeyEquivalent:] is then called
    // so we can catch and redirect any modal window's key shortcut
    // events without triggering the modal window's "disallowed
    // action" beep.
    if( !bRet && [NSApp modalWindow] )
        bRet = [SalNSMenu dispatchSpecialKeyEquivalents: pEvent];

    return bRet;
}

@end

/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/vcl/osx/vclnsapp.mm b/vcl/osx/vclnsapp.mm
index 5daf923..cd60cb0 100644
--- a/vcl/osx/vclnsapp.mm
+++ b/vcl/osx/vclnsapp.mm
@@ -34,6 +34,7 @@
#include <osx/salframe.h>
#include <osx/salframeview.h>
#include <osx/salinst.h>
#include <osx/salnsmenu.h>
#include <osx/vclnsapp.h>
#include <quartz/utils.h>

@@ -170,44 +171,8 @@
            // precondition: this ONLY works because CMD-V (paste), CMD-C (copy) and CMD-X (cut) are
            // NOT localized, that is the same in all locales. Should this be
            // different in any locale, this hack will fail.
            unsigned int nModMask = ([pEvent modifierFlags] & (NSEventModifierFlagShift|NSEventModifierFlagControl|NSEventModifierFlagOption|NSEventModifierFlagCommand));
            if( nModMask == NSEventModifierFlagCommand )
            {

                if( [[pEvent charactersIgnoringModifiers] isEqualToString: @"v"] )
                {
                    if( [NSApp sendAction: @selector(paste:) to: nil from: nil] )
                        return;
                }
                else if( [[pEvent charactersIgnoringModifiers] isEqualToString: @"c"] )
                {
                    if( [NSApp sendAction: @selector(copy:) to: nil from: nil] )
                        return;
                }
                else if( [[pEvent charactersIgnoringModifiers] isEqualToString: @"x"] )
                {
                    if( [NSApp sendAction: @selector(cut:) to: nil from: nil] )
                        return;
                }
                else if( [[pEvent charactersIgnoringModifiers] isEqualToString: @"a"] )
                {
                    if( [NSApp sendAction: @selector(selectAll:) to: nil from: nil] )
                        return;
                }
                else if( [[pEvent charactersIgnoringModifiers] isEqualToString: @"z"] )
                {
                    if( [NSApp sendAction: @selector(undo:) to: nil from: nil] )
                        return;
                }
            }
            else if( nModMask == (NSEventModifierFlagCommand|NSEventModifierFlagShift) )
            {
                if( [[pEvent charactersIgnoringModifiers] isEqualToString: @"Z"] )
                {
                    if( [NSApp sendAction: @selector(redo:) to: nil from: nil] )
                        return;
                }
            }
            if( [SalNSMenu dispatchSpecialKeyEquivalents:pEvent] )
                return;
        }
    }
    [super sendEvent: pEvent];
@@ -315,6 +280,14 @@
-(NSApplicationTerminateReply)applicationShouldTerminate: (NSApplication *) app
{
    (void)app;

    // Related: tdf#126638 disable all menu items when displaying modal windows
    // Although -[SalNSMenuItem validateMenuItem:] disables almost all menu
    // items when a modal window is displayed, the standard Quit menu item
    // does not get disabled so disable it here.
    if ([NSApp modalWindow])
        return NSTerminateCancel;

    NSApplicationTerminateReply aReply = NSTerminateNow;
    {
        SolarMutexGuard aGuard;