diff options
Diffstat (limited to 'vcl/osx/salframeview.mm')
-rw-r--r-- | vcl/osx/salframeview.mm | 2595 |
1 files changed, 2595 insertions, 0 deletions
diff --git a/vcl/osx/salframeview.mm b/vcl/osx/salframeview.mm new file mode 100644 index 0000000000..995eeb5749 --- /dev/null +++ b/vcl/osx/salframeview.mm @@ -0,0 +1,2595 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0 . + */ + +#include <sal/config.h> + +#include <memory> + +#include <sal/macros.h> +#include <tools/helpers.hxx> +#include <tools/long.hxx> +#include <vcl/event.hxx> +#include <vcl/inputctx.hxx> +#include <vcl/settings.hxx> +#include <vcl/svapp.hxx> +#include <vcl/window.hxx> +#include <vcl/commandevent.hxx> + +#include <osx/a11yfactory.h> +#include <osx/salframe.h> +#include <osx/salframeview.h> +#include <osx/salinst.h> +#include <quartz/salgdi.h> +#include <quartz/utils.h> + +#if HAVE_FEATURE_SKIA +#include <vcl/skia/SkiaHelper.hxx> +#endif + +#define WHEEL_EVENT_FACTOR 1.5 + +static sal_uInt16 ImplGetModifierMask( unsigned int nMask ) +{ + sal_uInt16 nRet = 0; + if( (nMask & NSEventModifierFlagShift) != 0 ) + nRet |= KEY_SHIFT; + if( (nMask & NSEventModifierFlagControl) != 0 ) + nRet |= KEY_MOD3; + if( (nMask & NSEventModifierFlagOption) != 0 ) + nRet |= KEY_MOD2; + if( (nMask & NSEventModifierFlagCommand) != 0 ) + nRet |= KEY_MOD1; + return nRet; +} + +static sal_uInt16 ImplMapCharCode( sal_Unicode aCode ) +{ + static sal_uInt16 aKeyCodeMap[ 128 ] = + { + 0, 0, 0, 0, 0, 0, 0, 0, + KEY_BACKSPACE, KEY_TAB, KEY_RETURN, 0, 0, KEY_RETURN, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, KEY_TAB, 0, KEY_ESCAPE, 0, 0, 0, 0, + KEY_SPACE, 0, 0, 0, 0, 0, 0, 0, + 0, 0, KEY_MULTIPLY, KEY_ADD, KEY_COMMA, KEY_SUBTRACT, KEY_POINT, KEY_DIVIDE, + KEY_0, KEY_1, KEY_2, KEY_3, KEY_4, KEY_5, KEY_6, KEY_7, + KEY_8, KEY_9, 0, 0, KEY_LESS, KEY_EQUAL, KEY_GREATER, 0, + 0, KEY_A, KEY_B, KEY_C, KEY_D, KEY_E, KEY_F, KEY_G, + KEY_H, KEY_I, KEY_J, KEY_K, KEY_L, KEY_M, KEY_N, KEY_O, + KEY_P, KEY_Q, KEY_R, KEY_S, KEY_T, KEY_U, KEY_V, KEY_W, + KEY_X, KEY_Y, KEY_Z, 0, 0, 0, 0, 0, + KEY_QUOTELEFT, KEY_A, KEY_B, KEY_C, KEY_D, KEY_E, KEY_F, KEY_G, + KEY_H, KEY_I, KEY_J, KEY_K, KEY_L, KEY_M, KEY_N, KEY_O, + KEY_P, KEY_Q, KEY_R, KEY_S, KEY_T, KEY_U, KEY_V, KEY_W, + KEY_X, KEY_Y, KEY_Z, 0, 0, 0, KEY_TILDE, KEY_BACKSPACE + }; + + // Note: the mapping 0x7f should by rights be KEY_DELETE + // however if you press "backspace" 0x7f is reported + // whereas for "delete" 0xf728 gets reported + + // Note: the mapping of 0x19 to KEY_TAB is because for unknown reasons + // tab alone is reported as 0x09 (as expected) but shift-tab is + // reported as 0x19 (end of medium) + + static sal_uInt16 aFunctionKeyCodeMap[ 128 ] = + { + KEY_UP, KEY_DOWN, KEY_LEFT, KEY_RIGHT, KEY_F1, KEY_F2, KEY_F3, KEY_F4, + KEY_F5, KEY_F6, KEY_F7, KEY_F8, KEY_F9, KEY_F10, KEY_F11, KEY_F12, + KEY_F13, KEY_F14, KEY_F15, KEY_F16, KEY_F17, KEY_F18, KEY_F19, KEY_F20, + KEY_F21, KEY_F22, KEY_F23, KEY_F24, KEY_F25, KEY_F26, 0, 0, + 0, 0, 0, 0, 0, 0, 0, KEY_INSERT, + KEY_DELETE, KEY_HOME, 0, KEY_END, KEY_PAGEUP, KEY_PAGEDOWN, 0, 0, + 0, 0, 0, 0, 0, KEY_MENU, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, KEY_UNDO, KEY_REPEAT, KEY_FIND, KEY_HELP, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0 + }; + + sal_uInt16 nKeyCode = 0; + if( aCode < SAL_N_ELEMENTS( aKeyCodeMap) ) + nKeyCode = aKeyCodeMap[ aCode ]; + else if( aCode >= 0xf700 && aCode < 0xf780 ) + nKeyCode = aFunctionKeyCodeMap[ aCode - 0xf700 ]; + return nKeyCode; +} + +static sal_uInt16 ImplMapKeyCode(sal_uInt16 nKeyCode) +{ + /* + http://stackoverflow.com/questions/2080312/where-can-i-find-a-list-of-key-codes-for-use-with-cocoas-nsevent-class/2080324#2080324 + /System/Library/Frameworks/Carbon.framework/Versions/A/Frameworks/HIToolbox.framework/Versions/A/Headers/Events.h + */ + + static sal_uInt16 aKeyCodeMap[ 0x80 ] = + { + KEY_A, KEY_S, KEY_D, KEY_F, KEY_H, KEY_G, KEY_Z, KEY_X, + KEY_C, KEY_V, 0, KEY_B, KEY_Q, KEY_W, KEY_E, KEY_R, + KEY_Y, KEY_T, KEY_1, KEY_2, KEY_3, KEY_4, KEY_6, KEY_5, + KEY_EQUAL, KEY_9, KEY_7, KEY_SUBTRACT, KEY_8, KEY_0, KEY_BRACKETRIGHT, KEY_RIGHTCURLYBRACKET, + KEY_U, KEY_BRACKETLEFT, KEY_I, KEY_P, KEY_RETURN, KEY_L, KEY_J, KEY_QUOTERIGHT, + KEY_K, KEY_SEMICOLON, 0, KEY_COMMA, KEY_DIVIDE, KEY_N, KEY_M, KEY_POINT, + KEY_TAB, KEY_SPACE, KEY_QUOTELEFT, KEY_DELETE, 0, KEY_ESCAPE, 0, 0, + 0, KEY_CAPSLOCK, 0, 0, 0, 0, 0, 0, + KEY_F17, KEY_DECIMAL, 0, KEY_MULTIPLY, 0, KEY_ADD, 0, 0, + 0, 0, 0, KEY_DIVIDE, KEY_RETURN, 0, KEY_SUBTRACT, KEY_F18, + KEY_F19, KEY_EQUAL, 0, 0, 0, 0, 0, 0, + 0, 0, KEY_F20, 0, 0, 0, 0, 0, + KEY_F5, KEY_F6, KEY_F7, KEY_F3, KEY_F8, KEY_F9, 0, KEY_F11, + 0, KEY_F13, KEY_F16, KEY_F14, 0, KEY_F10, 0, KEY_F12, + 0, KEY_F15, KEY_HELP, KEY_HOME, KEY_PAGEUP, KEY_DELETE, KEY_F4, KEY_END, + KEY_F2, KEY_PAGEDOWN, KEY_F1, KEY_LEFT, KEY_RIGHT, KEY_DOWN, KEY_UP, 0 + }; + + if (nKeyCode < SAL_N_ELEMENTS(aKeyCodeMap)) + return aKeyCodeMap[nKeyCode]; + return 0; +} + +// store the frame the mouse last entered +static AquaSalFrame* s_pMouseFrame = nullptr; +// store the last pressed button for enter/exit events +// which lack that information +static sal_uInt16 s_nLastButton = 0; + +static AquaSalFrame* getMouseContainerFrame() +{ + AquaSalFrame* pDispatchFrame = nullptr; + NSArray* aWindows = [NSWindow windowNumbersWithOptions:0]; + for(NSUInteger i = 0; i < [aWindows count] && ! pDispatchFrame; i++ ) + { + NSWindow* pWin = [NSApp windowWithWindowNumber:[[aWindows objectAtIndex:i] integerValue]]; + if( pWin && [pWin isMemberOfClass: [SalFrameWindow class]] && [static_cast<SalFrameWindow*>(pWin) containsMouse] ) + pDispatchFrame = [static_cast<SalFrameWindow*>(pWin) getSalFrame]; + } + return pDispatchFrame; +} + +static NSArray *getMergedAccessibilityChildren(NSArray *pDefaultChildren, NSArray *pUnignoredChildrenToAdd) +{ + NSArray *pRet = pDefaultChildren; + + if (pUnignoredChildrenToAdd && [pUnignoredChildrenToAdd count]) + { + NSMutableArray *pNewChildren = [NSMutableArray arrayWithCapacity:(pRet ? [pRet count] : 0) + 1]; + if (pNewChildren) + { + if (pRet) + [pNewChildren addObjectsFromArray:pRet]; + + for (AquaA11yWrapper *pWrapper : pUnignoredChildrenToAdd) + { + if (pWrapper && ![pNewChildren containsObject:pWrapper]) + [pNewChildren addObject:pWrapper]; + } + + pRet = pNewChildren; + } + else + { + pRet = pUnignoredChildrenToAdd; + } + } + + return pRet; +} + +// Update ImplGetSVData()->mpWinData->mbIsLiveResize +static void updateWinDataInLiveResize(bool bInLiveResize) +{ + ImplSVData* pSVData = ImplGetSVData(); + assert( pSVData ); + if ( pSVData ) + { + if ( pSVData->mpWinData->mbIsLiveResize != bInLiveResize ) + { + pSVData->mpWinData->mbIsLiveResize = bInLiveResize; + Scheduler::Wakeup(); + } + } +} + +@interface NSResponder (SalFrameWindow) +-(BOOL)accessibilityIsIgnored; +@end + +@implementation SalFrameWindow +-(id)initWithSalFrame: (AquaSalFrame*)pFrame +{ + mDraggingDestinationHandler = nil; + mbInWindowDidResize = NO; + mpLiveResizeTimer = nil; + mpFrame = pFrame; + NSRect aRect = { { static_cast<CGFloat>(pFrame->maGeometry.x()), static_cast<CGFloat>(pFrame->maGeometry.y()) }, + { static_cast<CGFloat>(pFrame->maGeometry.width()), static_cast<CGFloat>(pFrame->maGeometry.height()) } }; + pFrame->VCLToCocoa( aRect ); + NSWindow* pNSWindow = [super initWithContentRect: aRect + styleMask: mpFrame->getStyleMask() + backing: NSBackingStoreBuffered + defer: Application::IsHeadlessModeEnabled()]; + + // Disallow full-screen mode on macOS >= 10.11 where it is enabled by default. We don't want it + // for now as it will just be confused with LibreOffice's home-grown full-screen concept, with + // which it has nothing to do, and one can get into all kinds of weird states by using them + // intermixedly. + + // Ideally we should use the system full-screen mode and adapt the code for the home-grown thing + // to be in sync with that instead. (And we would then not need the button to get out of + // full-screen mode, as the normal way to get out of it is to either click on the green bubble + // again, or invoke the keyboard command again.) + + // (Confusingly, at the moment the home-grown full-screen mode is bound to Cmd+Shift+F, which is + // the keyboard command normally used in apps to get in and out of the system full-screen mode.) + + // Disabling system full-screen mode makes the green button on the title bar (on macOS >= 10.11) + // show a plus sign instead, and clicking it becomes identical to double-clicking the title bar, + // i.e. it maximizes / unmaximises the window. Sure, that state can also be confused with LO's + // home-grown full-screen mode. Oh well. + + [pNSWindow setCollectionBehavior: NSWindowCollectionBehaviorFullScreenNone]; + + // Disable window restoration until we support it directly + [pNSWindow setRestorable: NO]; + + // tdf#137468: Restrict to 24-bit RGB as that is all that we can + // handle anyway. HDR is far off in the future for LibreOffice. + [pNSWindow setDynamicDepthLimit: NO]; + [pNSWindow setDepthLimit: NSWindowDepthTwentyfourBitRGB]; + + return static_cast<SalFrameWindow *>(pNSWindow); +} + +-(void)clearLiveResizeTimer +{ + if ( mpLiveResizeTimer ) + { + [mpLiveResizeTimer invalidate]; + [mpLiveResizeTimer release]; + mpLiveResizeTimer = nil; + } +} + +-(void)dealloc +{ + [self clearLiveResizeTimer]; + [super dealloc]; +} + +-(AquaSalFrame*)getSalFrame +{ + return mpFrame; +} + +-(void)displayIfNeeded +{ + if( GetSalData() && GetSalData()->mpInstance ) + { + SolarMutexGuard aGuard; + [super displayIfNeeded]; + } +} + +-(BOOL)containsMouse +{ + // is this event actually inside that NSWindow ? + NSPoint aPt = [NSEvent mouseLocation]; + NSRect aFrameRect = [self frame]; + bool bInRect = NSPointInRect( aPt, aFrameRect ); + return bInRect; +} + +-(BOOL)canBecomeKeyWindow +{ + if( (mpFrame->mnStyle & + ( SalFrameStyleFlags::FLOAT | + SalFrameStyleFlags::TOOLTIP | + SalFrameStyleFlags::INTRO + )) == SalFrameStyleFlags::NONE ) + return YES; + if( mpFrame->mnStyle & SalFrameStyleFlags::OWNERDRAWDECORATION ) + return YES; + if( mpFrame->mbFullScreen ) + return YES; + return [super canBecomeKeyWindow]; +} + +-(void)windowDidBecomeKey: (NSNotification*)pNotification +{ + (void)pNotification; + SolarMutexGuard aGuard; + + if( mpFrame && AquaSalFrame::isAlive( mpFrame ) ) + { + static const SalFrameStyleFlags nGuessDocument = SalFrameStyleFlags::MOVEABLE| + SalFrameStyleFlags::SIZEABLE| + SalFrameStyleFlags::CLOSEABLE; + + // Reset dark mode colors in HITheme controls after printing + // In dark mode, after an NSPrintOperation has completed, macOS draws + // HITheme controls with light mode colors so reset all dark mode + // colors when an NSWindow gains focus. + mpFrame->UpdateDarkMode(); + + if( mpFrame->mpMenu ) + mpFrame->mpMenu->setMainMenu(); + else if( ! mpFrame->mpParent && + ( (mpFrame->mnStyle & nGuessDocument) == nGuessDocument || // set default menu for e.g. help + mpFrame->mbFullScreen ) ) // set default menu for e.g. presentation + { + AquaSalMenu::setDefaultMenu(); + } + mpFrame->CallCallback( SalEvent::GetFocus, nullptr ); + mpFrame->SendPaintEvent(); // repaint controls as active + } + + // Prevent the same native input method popup that was cancelled in a + // previous call to [self windowDidResignKey:] from reappearing + [self endExtTextInput]; +} + +-(void)windowDidResignKey: (NSNotification*)pNotification +{ + (void)pNotification; + SolarMutexGuard aGuard; + + // Commit any uncommitted text and cancel the native input method session + // whenever a window loses focus like in Safari, Firefox, and Excel + [self endExtTextInput]; + + if( mpFrame && AquaSalFrame::isAlive( mpFrame ) ) + { + mpFrame->CallCallback(SalEvent::LoseFocus, nullptr); + mpFrame->SendPaintEvent(); // repaint controls as inactive + } +} + +-(void)windowDidChangeScreen: (NSNotification*)pNotification +{ + (void)pNotification; + SolarMutexGuard aGuard; + + if( mpFrame && AquaSalFrame::isAlive( mpFrame ) ) + mpFrame->screenParametersChanged(); +} + +-(void)windowDidMove: (NSNotification*)pNotification +{ + (void)pNotification; + SolarMutexGuard aGuard; + + if( mpFrame && AquaSalFrame::isAlive( mpFrame ) ) + { + mpFrame->UpdateFrameGeometry(); + mpFrame->CallCallback( SalEvent::Move, nullptr ); + } +} + +-(void)windowDidResize: (NSNotification*)pNotification +{ + SolarMutexGuard aGuard; + + if ( mbInWindowDidResize ) + return; + + mbInWindowDidResize = YES; + + if( mpFrame && AquaSalFrame::isAlive( mpFrame ) ) + { + mpFrame->UpdateFrameGeometry(); + mpFrame->CallCallback( SalEvent::Resize, nullptr ); + + updateWinDataInLiveResize( [self inLiveResize] ); + if ( ImplGetSVData()->mpWinData->mbIsLiveResize ) + { +#if HAVE_FEATURE_SKIA + // Related: tdf#152703 Eliminate empty window with Skia/Metal while resizing + // The window will clear its background so when Skia/Metal is + // enabled, explicitly flush the Skia graphics to the window + // during live resizing or else nothing will be drawn until after + // live resizing has ended. + // Also, flushing during [self windowDidResize:] eliminates flicker + // by forcing this window's SkSurface to recreate its underlying + // CAMetalLayer with the new size. Flushing in + // [self displayIfNeeded] does not eliminate flicker so apparently + // [self windowDidResize:] is called earlier. + if ( SkiaHelper::isVCLSkiaEnabled() ) + { + AquaSalGraphics* pGraphics = mpFrame->mpGraphics; + if ( pGraphics ) + pGraphics->Flush(); + } +#endif + + // tdf#152703 Force relayout during live resizing of window + // During a live resize, macOS floods the application with + // windowDidResize: notifications so sending a paint event does + // not trigger redrawing with the new size. + // Instead, force relayout by dispatching all pending internal + // events and firing any pending timers. + // Also, Application::Reschedule() can potentially display a + // modal dialog which will cause a hang so temporarily disable + // live resize by clamping the window's minimum and maximum sizes + // to the current frame size which in Application::Reschedule(). + NSRect aFrame = [self frame]; + NSSize aMinSize = [self minSize]; + NSSize aMaxSize = [self maxSize]; + [self setMinSize:aFrame.size]; + [self setMaxSize:aFrame.size]; + Application::Reschedule( true ); + [self setMinSize:aMinSize]; + [self setMaxSize:aMaxSize]; + + if ( ImplGetSVData()->mpWinData->mbIsLiveResize ) + { + // tdf#152703 Force repaint after live resizing ends + // Repost this notification so that this selector will be called + // at least once after live resizing ends + if ( !mpLiveResizeTimer ) + { + mpLiveResizeTimer = [NSTimer scheduledTimerWithTimeInterval:0.1f target:self selector:@selector(windowDidResizeWithTimer:) userInfo:pNotification repeats:YES]; + if ( mpLiveResizeTimer ) + { + [mpLiveResizeTimer retain]; + + // The timer won't fire without a call to + // Application::Reschedule() unless we copy the fix for + // #i84055# from vcl/osx/saltimer.cxx and add the timer + // to the NSEventTrackingRunLoopMode run loop mode + [[NSRunLoop currentRunLoop] addTimer:mpLiveResizeTimer forMode:NSEventTrackingRunLoopMode]; + } + } + } + } + else + { + [self clearLiveResizeTimer]; + } + + // tdf#158461 eliminate flicker during live resizing + // When using Skia/Metal, the window content will flicker while + // live resizing a window if we don't send a paint event. + mpFrame->SendPaintEvent(); + } + + mbInWindowDidResize = NO; +} + +-(void)windowDidMiniaturize: (NSNotification*)pNotification +{ + (void)pNotification; + SolarMutexGuard aGuard; + + if( mpFrame && AquaSalFrame::isAlive( mpFrame ) ) + { + mpFrame->mbShown = false; + mpFrame->UpdateFrameGeometry(); + mpFrame->CallCallback( SalEvent::Resize, nullptr ); + } +} + +-(void)windowDidDeminiaturize: (NSNotification*)pNotification +{ + (void)pNotification; + SolarMutexGuard aGuard; + + if( mpFrame && AquaSalFrame::isAlive( mpFrame ) ) + { + mpFrame->mbShown = true; + mpFrame->UpdateFrameGeometry(); + mpFrame->CallCallback( SalEvent::Resize, nullptr ); + } +} + +-(BOOL)windowShouldClose: (NSNotification*)pNotification +{ + (void)pNotification; + SolarMutexGuard aGuard; + + bool bRet = true; + if( mpFrame && AquaSalFrame::isAlive( mpFrame ) ) + { + // #i84461# end possible input + [self endExtTextInput]; + if( AquaSalFrame::isAlive( mpFrame ) ) + { + mpFrame->CallCallback( SalEvent::Close, nullptr ); + bRet = false; // application will close the window or not, AppKit shouldn't + AquaSalTimer *pTimer = static_cast<AquaSalTimer*>( ImplGetSVData()->maSchedCtx.mpSalTimer ); + assert( pTimer ); + pTimer->handleWindowShouldClose(); + } + } + + return bRet; +} + +-(void)windowDidEnterFullScreen: (NSNotification*)pNotification +{ + SolarMutexGuard aGuard; + + if( !mpFrame || !AquaSalFrame::isAlive( mpFrame)) + return; + mpFrame->mbFullScreen = true; + (void)pNotification; +} + +-(void)windowDidExitFullScreen: (NSNotification*)pNotification +{ + SolarMutexGuard aGuard; + + if( !mpFrame || !AquaSalFrame::isAlive( mpFrame)) + return; + mpFrame->mbFullScreen = false; + (void)pNotification; +} + +-(void)windowDidChangeBackingProperties:(NSNotification *)pNotification +{ + (void)pNotification; +#if HAVE_FEATURE_SKIA + SolarMutexGuard aGuard; + + sal::aqua::resetWindowScaling(); + + if( mpFrame && AquaSalFrame::isAlive( mpFrame ) ) + { + // tdf#147342 Notify Skia that the window's backing properties changed + if ( SkiaHelper::isVCLSkiaEnabled() ) + { + AquaSalGraphics* pGraphics = mpFrame->mpGraphics; + if ( pGraphics ) + pGraphics->WindowBackingPropertiesChanged(); + } + } +#endif +} + +-(void)windowWillStartLiveResize:(NSNotification *)pNotification +{ + SolarMutexGuard aGuard; + + updateWinDataInLiveResize(true); +} + +-(void)windowDidEndLiveResize:(NSNotification *)pNotification +{ + SolarMutexGuard aGuard; + + updateWinDataInLiveResize(false); +} + +-(void)dockMenuItemTriggered: (id)sender +{ + (void)sender; + SolarMutexGuard aGuard; + + if( mpFrame && AquaSalFrame::isAlive( mpFrame ) ) + mpFrame->ToTop( SalFrameToTop::RestoreWhenMin | SalFrameToTop::GrabFocus ); +} + +-(css::uno::Reference < css::accessibility::XAccessibleContext >)accessibleContext +{ + return mpFrame -> GetWindow() -> GetAccessible() -> getAccessibleContext(); +} + +-(BOOL)isIgnoredWindow +{ + SolarMutexGuard aGuard; + + // Treat tooltip windows as ignored + if( mpFrame && AquaSalFrame::isAlive( mpFrame ) ) + return (mpFrame->mnStyle & SalFrameStyleFlags::TOOLTIP) != SalFrameStyleFlags::NONE; + return YES; +} + +-(id)accessibilityApplicationFocusedUIElement +{ + return [self accessibilityFocusedUIElement]; +} + +-(id)accessibilityFocusedUIElement +{ + // Treat tooltip windows as ignored + if ([self isIgnoredWindow]) + return nil; + + return [super accessibilityFocusedUIElement]; +} + +-(BOOL)accessibilityIsIgnored +{ + // Treat tooltip windows as ignored + if ([self isIgnoredWindow]) + return YES; + + return [super accessibilityIsIgnored]; +} + +-(BOOL)isAccessibilityElement +{ + return ![self accessibilityIsIgnored]; +} + +-(NSDragOperation)draggingEntered:(id <NSDraggingInfo>)sender +{ + return [mDraggingDestinationHandler draggingEntered: sender]; +} + +-(NSDragOperation)draggingUpdated:(id <NSDraggingInfo>)sender +{ + return [mDraggingDestinationHandler draggingUpdated: sender]; +} + +-(void)draggingExited:(id <NSDraggingInfo>)sender +{ + [mDraggingDestinationHandler draggingExited: sender]; +} + +-(BOOL)prepareForDragOperation:(id <NSDraggingInfo>)sender +{ + return [mDraggingDestinationHandler prepareForDragOperation: sender]; +} + +-(BOOL)performDragOperation:(id <NSDraggingInfo>)sender +{ + return [mDraggingDestinationHandler performDragOperation: sender]; +} + +-(void)concludeDragOperation:(id <NSDraggingInfo>)sender +{ + [mDraggingDestinationHandler concludeDragOperation: sender]; +} + +-(void)registerDraggingDestinationHandler:(id)theHandler +{ + mDraggingDestinationHandler = theHandler; +} + +-(void)unregisterDraggingDestinationHandler:(id)theHandler +{ + (void)theHandler; + mDraggingDestinationHandler = nil; +} + +-(void)endExtTextInput +{ + [self endExtTextInput:EndExtTextInputFlags::Complete]; +} + +-(void)endExtTextInput:(EndExtTextInputFlags)nFlags +{ + SalFrameView *pView = static_cast<SalFrameView*>([self firstResponder]); + if (pView && [pView isKindOfClass:[SalFrameView class]]) + [pView endExtTextInput:nFlags]; +} + +-(void)windowDidResizeWithTimer:(NSTimer *)pTimer +{ + if ( pTimer ) + [self windowDidResize:[pTimer userInfo]]; +} + +@end + +@implementation SalFrameView ++(void)unsetMouseFrame: (AquaSalFrame*)pFrame +{ + if( pFrame == s_pMouseFrame ) + s_pMouseFrame = nullptr; +} + +-(id)initWithSalFrame: (AquaSalFrame*)pFrame +{ + if ((self = [super initWithFrame: [NSWindow contentRectForFrameRect: [pFrame->getNSWindow() frame] styleMask: pFrame->mnStyleMask]]) != nil) + { + mDraggingDestinationHandler = nil; + mpFrame = pFrame; + mpChildWrapper = nil; + mbNeedChildWrapper = NO; + mpLastEvent = nil; + mMarkedRange = NSMakeRange(NSNotFound, 0); + mSelectedRange = NSMakeRange(NSNotFound, 0); + mpMouseEventListener = nil; + mpLastSuperEvent = nil; + mfLastMagnifyTime = 0.0; + + mbInEndExtTextInput = NO; + mbInCommitMarkedText = NO; + mpLastMarkedText = nil; + mbTextInputWantsNonRepeatKeyDown = NO; + } + + return self; +} + +-(void)dealloc +{ + [self clearLastEvent]; + [self clearLastMarkedText]; + [self revokeWrapper]; + + [super dealloc]; +} + +-(AquaSalFrame*)getSalFrame +{ + return mpFrame; +} + +-(void)resetCursorRects +{ + if( mpFrame && AquaSalFrame::isAlive( mpFrame ) ) + { + // FIXME: does this leak the returned NSCursor of getCurrentCursor ? + const NSRect aRect = { NSZeroPoint, NSMakeSize(mpFrame->maGeometry.width(), mpFrame->maGeometry.height()) }; + [self addCursorRect: aRect cursor: mpFrame->getCurrentCursor()]; + } +} + +-(BOOL)acceptsFirstResponder +{ + return YES; +} + +-(BOOL)acceptsFirstMouse: (NSEvent*)pEvent +{ + (void)pEvent; + return YES; +} + +-(BOOL)isOpaque +{ + if( !mpFrame) + return YES; + if( !AquaSalFrame::isAlive( mpFrame)) + return YES; + if( !mpFrame->getClipPath()) + return YES; + return NO; +} + +-(void)drawRect: (NSRect)aRect +{ + ImplSVData* pSVData = ImplGetSVData(); + assert( pSVData ); + if ( !pSVData ) + return; + + SolarMutexGuard aGuard; + if (!mpFrame || !AquaSalFrame::isAlive(mpFrame)) + return; + + updateWinDataInLiveResize([self inLiveResize]); + + AquaSalGraphics* pGraphics = mpFrame->mpGraphics; + if (pGraphics) + { + pGraphics->UpdateWindow(aRect); + if (mpFrame->getClipPath()) + [mpFrame->getNSWindow() invalidateShadow]; + } +} + +-(void)sendMouseEventToFrame: (NSEvent*)pEvent button:(sal_uInt16)nButton eventtype:(SalEvent)nEvent +{ + SolarMutexGuard aGuard; + + AquaSalFrame* pDispatchFrame = AquaSalFrame::GetCaptureFrame(); + bool bIsCaptured = false; + if( pDispatchFrame ) + { + bIsCaptured = true; + if( nEvent == SalEvent::MouseLeave ) // no leave events if mouse is captured + nEvent = SalEvent::MouseMove; + } + else if( s_pMouseFrame ) + pDispatchFrame = s_pMouseFrame; + else + pDispatchFrame = mpFrame; + + /* #i81645# Cocoa reports mouse events while a button is pressed + to the window in which it was first pressed. This is reasonable and fine and + gets one around most cases where on other platforms one uses CaptureMouse or XGrabPointer, + however vcl expects mouse events to occur in the window the mouse is over, unless the + mouse is explicitly captured. So we need to find the window the mouse is actually + over for conformance with other platforms. + */ + if( ! bIsCaptured && nButton && pDispatchFrame && AquaSalFrame::isAlive( pDispatchFrame ) ) + { + // is this event actually inside that NSWindow ? + NSPoint aPt = [NSEvent mouseLocation]; + NSRect aFrameRect = [pDispatchFrame->getNSWindow() frame]; + + if ( ! NSPointInRect( aPt, aFrameRect ) ) + { + // no, it is not + // now we need to find the one it may be in + /* #i93756# we ant to get enumerate the application windows in z-order + to check if any contains the mouse. This could be elegantly done with this + code: + + // use NSApp to check windows in ZOrder whether they contain the mouse pointer + NSWindow* pWindow = [NSApp makeWindowsPerform: @selector(containsMouse) inOrder: YES]; + if( pWindow && [pWindow isMemberOfClass: [SalFrameWindow class]] ) + pDispatchFrame = [(SalFrameWindow*)pWindow getSalFrame]; + + However if a non SalFrameWindow is on screen (like e.g. the file dialog) + it can be hit with the containsMouse selector, which it doesn't support. + Sadly NSApplication:makeWindowsPerform does not check (for performance reasons + I assume) whether a window supports a selector before sending it. + */ + AquaSalFrame* pMouseFrame = getMouseContainerFrame(); + if( pMouseFrame ) + pDispatchFrame = pMouseFrame; + } + } + + if( pDispatchFrame && AquaSalFrame::isAlive( pDispatchFrame ) ) + { + pDispatchFrame->mnLastEventTime = static_cast<sal_uInt64>( [pEvent timestamp] * 1000.0 ); + pDispatchFrame->mnLastModifierFlags = [pEvent modifierFlags]; + + NSPoint aPt = [NSEvent mouseLocation]; + pDispatchFrame->CocoaToVCL( aPt ); + + sal_uInt16 nModMask = ImplGetModifierMask( [pEvent modifierFlags] ); + // #i82284# emulate ctrl left + if( nModMask == KEY_MOD3 && nButton == MOUSE_LEFT ) + { + nModMask = 0; + nButton = MOUSE_RIGHT; + } + + SalMouseEvent aEvent; + aEvent.mnTime = pDispatchFrame->mnLastEventTime; + aEvent.mnX = static_cast<tools::Long>(aPt.x) - pDispatchFrame->maGeometry.x(); + aEvent.mnY = static_cast<tools::Long>(aPt.y) - pDispatchFrame->maGeometry.y(); + aEvent.mnButton = nButton; + aEvent.mnCode = aEvent.mnButton | nModMask; + + if( AllSettings::GetLayoutRTL() ) + aEvent.mnX = pDispatchFrame->maGeometry.width() - 1 - aEvent.mnX; + + pDispatchFrame->CallCallback( nEvent, &aEvent ); + + // tdf#155266 force flush after scrolling + if (nButton == MOUSE_LEFT && nEvent == SalEvent::MouseMove) + mpFrame->mbForceFlush = true; + } +} + +-(void)mouseDown: (NSEvent*)pEvent +{ + if ( mpMouseEventListener != nil && + [mpMouseEventListener respondsToSelector: @selector(mouseDown:)]) + { + [mpMouseEventListener mouseDown: [pEvent copyWithZone: nullptr]]; + } + + s_nLastButton = MOUSE_LEFT; + [self sendMouseEventToFrame:pEvent button:MOUSE_LEFT eventtype:SalEvent::MouseButtonDown]; +} + +-(void)mouseDragged: (NSEvent*)pEvent +{ + if ( mpMouseEventListener != nil && + [mpMouseEventListener respondsToSelector: @selector(mouseDragged:)]) + { + [mpMouseEventListener mouseDragged: [pEvent copyWithZone: nullptr]]; + } + s_nLastButton = MOUSE_LEFT; + [self sendMouseEventToFrame:pEvent button:MOUSE_LEFT eventtype:SalEvent::MouseMove]; +} + +-(void)mouseUp: (NSEvent*)pEvent +{ + s_nLastButton = 0; + [self sendMouseEventToFrame:pEvent button:MOUSE_LEFT eventtype:SalEvent::MouseButtonUp]; +} + +-(void)mouseMoved: (NSEvent*)pEvent +{ + s_nLastButton = 0; + [self sendMouseEventToFrame:pEvent button:0 eventtype:SalEvent::MouseMove]; +} + +-(void)mouseEntered: (NSEvent*)pEvent +{ + s_pMouseFrame = mpFrame; + + // #i107215# the only mouse events we get when inactive are enter/exit + // actually we would like to have all of them, but better none than some + if( [NSApp isActive] ) + [self sendMouseEventToFrame:pEvent button:s_nLastButton eventtype:SalEvent::MouseMove]; +} + +-(void)mouseExited: (NSEvent*)pEvent +{ + if( s_pMouseFrame == mpFrame ) + s_pMouseFrame = nullptr; + + // #i107215# the only mouse events we get when inactive are enter/exit + // actually we would like to have all of them, but better none than some + if( [NSApp isActive] ) + [self sendMouseEventToFrame:pEvent button:s_nLastButton eventtype:SalEvent::MouseLeave]; +} + +-(void)rightMouseDown: (NSEvent*)pEvent +{ + s_nLastButton = MOUSE_RIGHT; + [self sendMouseEventToFrame:pEvent button:MOUSE_RIGHT eventtype:SalEvent::MouseButtonDown]; +} + +-(void)rightMouseDragged: (NSEvent*)pEvent +{ + s_nLastButton = MOUSE_RIGHT; + [self sendMouseEventToFrame:pEvent button:MOUSE_RIGHT eventtype:SalEvent::MouseMove]; +} + +-(void)rightMouseUp: (NSEvent*)pEvent +{ + s_nLastButton = 0; + [self sendMouseEventToFrame:pEvent button:MOUSE_RIGHT eventtype:SalEvent::MouseButtonUp]; +} + +-(void)otherMouseDown: (NSEvent*)pEvent +{ + if( [pEvent buttonNumber] == 2 ) + { + s_nLastButton = MOUSE_MIDDLE; + [self sendMouseEventToFrame:pEvent button:MOUSE_MIDDLE eventtype:SalEvent::MouseButtonDown]; + } + else + s_nLastButton = 0; +} + +-(void)otherMouseDragged: (NSEvent*)pEvent +{ + if( [pEvent buttonNumber] == 2 ) + { + s_nLastButton = MOUSE_MIDDLE; + [self sendMouseEventToFrame:pEvent button:MOUSE_MIDDLE eventtype:SalEvent::MouseMove]; + } + else + s_nLastButton = 0; +} + +-(void)otherMouseUp: (NSEvent*)pEvent +{ + s_nLastButton = 0; + if( [pEvent buttonNumber] == 2 ) + [self sendMouseEventToFrame:pEvent button:MOUSE_MIDDLE eventtype:SalEvent::MouseButtonUp]; +} + +- (void)magnifyWithEvent: (NSEvent*)pEvent +{ + SolarMutexGuard aGuard; + + // TODO: ?? -(float)magnification; + if( AquaSalFrame::isAlive( mpFrame ) ) + { + const NSTimeInterval fMagnifyTime = [pEvent timestamp]; + mpFrame->mnLastEventTime = static_cast<sal_uInt64>( fMagnifyTime * 1000.0 ); + mpFrame->mnLastModifierFlags = [pEvent modifierFlags]; + + // check if this is a new series of magnify events + static const NSTimeInterval fMaxDiffTime = 0.3; + const bool bNewSeries = (fMagnifyTime - mfLastMagnifyTime > fMaxDiffTime); + + if( bNewSeries ) + mfMagnifyDeltaSum = 0.0; + mfMagnifyDeltaSum += [pEvent magnification]; + + mfLastMagnifyTime = [pEvent timestamp]; +// TODO: change to 0.1 when CommandWheelMode::ZOOM handlers allow finer zooming control + static const float fMagnifyFactor = 0.25*500; // steps are 500 times smaller for -magnification + static const float fMinMagnifyStep = 15.0 / fMagnifyFactor; + if( fabs(mfMagnifyDeltaSum) <= fMinMagnifyStep ) + return; + + // adapt NSEvent-sensitivity to application expectations + // TODO: rather make CommandWheelMode::ZOOM handlers smarter + const float fDeltaZ = mfMagnifyDeltaSum * fMagnifyFactor; + int nDeltaZ = FRound( fDeltaZ ); + if( !nDeltaZ ) + { + // handle new series immediately + if( !bNewSeries ) + return; + nDeltaZ = (fDeltaZ >= 0.0) ? +1 : -1; + } + // eventually give credit for delta sum + mfMagnifyDeltaSum -= nDeltaZ / fMagnifyFactor; + + NSPoint aPt = [NSEvent mouseLocation]; + mpFrame->CocoaToVCL( aPt ); + + SalWheelMouseEvent aEvent; + aEvent.mnTime = mpFrame->mnLastEventTime; + aEvent.mnX = static_cast<tools::Long>(aPt.x) - mpFrame->maGeometry.x(); + aEvent.mnY = static_cast<tools::Long>(aPt.y) - mpFrame->maGeometry.y(); + aEvent.mnCode = ImplGetModifierMask( mpFrame->mnLastModifierFlags ); + aEvent.mnCode |= KEY_MOD1; // we want zooming, no scrolling + aEvent.mbDeltaIsPixel = true; + + if( AllSettings::GetLayoutRTL() ) + aEvent.mnX = mpFrame->maGeometry.width() - 1 - aEvent.mnX; + + aEvent.mnDelta = nDeltaZ; + aEvent.mnNotchDelta = (nDeltaZ >= 0) ? +1 : -1; + if( aEvent.mnDelta == 0 ) + aEvent.mnDelta = aEvent.mnNotchDelta; + aEvent.mbHorz = false; + sal_uInt32 nScrollLines = nDeltaZ; + if (nScrollLines == 0) + nScrollLines = 1; + aEvent.mnScrollLines = nScrollLines; + mpFrame->CallCallback( SalEvent::WheelMouse, &aEvent ); + } +} + +- (void)rotateWithEvent: (NSEvent*)pEvent +{ + //Rotation : -(float)rotation; + // TODO: create new CommandType so rotation is available to the applications + (void)pEvent; +} + +- (void)swipeWithEvent: (NSEvent*)pEvent +{ + SolarMutexGuard aGuard; + + if( AquaSalFrame::isAlive( mpFrame ) ) + { + mpFrame->mnLastEventTime = static_cast<sal_uInt64>( [pEvent timestamp] * 1000.0 ); + mpFrame->mnLastModifierFlags = [pEvent modifierFlags]; + + // merge pending scroll wheel events + CGFloat dX = 0.0; + CGFloat dY = 0.0; + for(;;) + { + dX += [pEvent deltaX]; + dY += [pEvent deltaY]; + NSEvent* pNextEvent = [NSApp nextEventMatchingMask: NSEventMaskScrollWheel + untilDate: nil inMode: NSDefaultRunLoopMode dequeue: YES ]; + if( !pNextEvent ) + break; + pEvent = pNextEvent; + } + + NSPoint aPt = [NSEvent mouseLocation]; + mpFrame->CocoaToVCL( aPt ); + + SalWheelMouseEvent aEvent; + aEvent.mnTime = mpFrame->mnLastEventTime; + aEvent.mnX = static_cast<tools::Long>(aPt.x) - mpFrame->maGeometry.x(); + aEvent.mnY = static_cast<tools::Long>(aPt.y) - mpFrame->maGeometry.y(); + aEvent.mnCode = ImplGetModifierMask( mpFrame->mnLastModifierFlags ); + aEvent.mbDeltaIsPixel = true; + + if( AllSettings::GetLayoutRTL() ) + aEvent.mnX = mpFrame->maGeometry.width() - 1 - aEvent.mnX; + + if( dX != 0.0 ) + { + aEvent.mnDelta = static_cast<tools::Long>(dX < 0 ? floor(dX) : ceil(dX)); + aEvent.mnNotchDelta = (dX < 0) ? -1 : +1; + if( aEvent.mnDelta == 0 ) + aEvent.mnDelta = aEvent.mnNotchDelta; + aEvent.mbHorz = true; + aEvent.mnScrollLines = SAL_WHEELMOUSE_EVENT_PAGESCROLL; + mpFrame->CallCallback( SalEvent::WheelMouse, &aEvent ); + } + if( dY != 0.0 && AquaSalFrame::isAlive( mpFrame )) + { + aEvent.mnDelta = static_cast<tools::Long>(dY < 0 ? floor(dY) : ceil(dY)); + aEvent.mnNotchDelta = (dY < 0) ? -1 : +1; + if( aEvent.mnDelta == 0 ) + aEvent.mnDelta = aEvent.mnNotchDelta; + aEvent.mbHorz = false; + aEvent.mnScrollLines = SAL_WHEELMOUSE_EVENT_PAGESCROLL; + mpFrame->CallCallback( SalEvent::WheelMouse, &aEvent ); + } + } +} + +-(void)scrollWheel: (NSEvent*)pEvent +{ + SolarMutexGuard aGuard; + + if( AquaSalFrame::isAlive( mpFrame ) ) + { + mpFrame->mnLastEventTime = static_cast<sal_uInt64>( [pEvent timestamp] * 1000.0 ); + mpFrame->mnLastModifierFlags = [pEvent modifierFlags]; + + // merge pending scroll wheel events + CGFloat dX = 0.0; + CGFloat dY = 0.0; + for(;;) + { + dX += [pEvent deltaX]; + dY += [pEvent deltaY]; + NSEvent* pNextEvent = [NSApp nextEventMatchingMask: NSEventMaskScrollWheel + untilDate: nil inMode: NSDefaultRunLoopMode dequeue: YES ]; + if( !pNextEvent ) + break; + pEvent = pNextEvent; + } + + NSPoint aPt = [NSEvent mouseLocation]; + mpFrame->CocoaToVCL( aPt ); + + SalWheelMouseEvent aEvent; + aEvent.mnTime = mpFrame->mnLastEventTime; + aEvent.mnX = static_cast<tools::Long>(aPt.x) - mpFrame->maGeometry.x(); + aEvent.mnY = static_cast<tools::Long>(aPt.y) - mpFrame->maGeometry.y(); + aEvent.mnCode = ImplGetModifierMask( mpFrame->mnLastModifierFlags ); + aEvent.mbDeltaIsPixel = false; + + if( AllSettings::GetLayoutRTL() ) + aEvent.mnX = mpFrame->maGeometry.width() - 1 - aEvent.mnX; + + if( dX != 0.0 ) + { + aEvent.mnDelta = static_cast<tools::Long>(dX < 0 ? floor(dX) : ceil(dX)); + aEvent.mnNotchDelta = (dX < 0) ? -1 : +1; + if( aEvent.mnDelta == 0 ) + aEvent.mnDelta = aEvent.mnNotchDelta; + aEvent.mbHorz = true; + sal_uInt32 nScrollLines = fabs(dX) / WHEEL_EVENT_FACTOR; + if (nScrollLines == 0) + nScrollLines = 1; + aEvent.mnScrollLines = nScrollLines; + + mpFrame->CallCallback( SalEvent::WheelMouse, &aEvent ); + } + if( dY != 0.0 && AquaSalFrame::isAlive( mpFrame ) ) + { + aEvent.mnDelta = static_cast<tools::Long>(dY < 0 ? floor(dY) : ceil(dY)); + aEvent.mnNotchDelta = (dY < 0) ? -1 : +1; + if( aEvent.mnDelta == 0 ) + aEvent.mnDelta = aEvent.mnNotchDelta; + aEvent.mbHorz = false; + sal_uInt32 nScrollLines = fabs(dY) / WHEEL_EVENT_FACTOR; + if (nScrollLines == 0) + nScrollLines = 1; + aEvent.mnScrollLines = nScrollLines; + + mpFrame->CallCallback( SalEvent::WheelMouse, &aEvent ); + } + + // tdf#155266 force flush after scrolling + mpFrame->mbForceFlush = true; + } +} + + +-(void)keyDown: (NSEvent*)pEvent +{ + SolarMutexGuard aGuard; + + if( AquaSalFrame::isAlive( mpFrame ) ) + { + // Retain the event as it will be released sometime before a key up + // event is dispatched + [self clearLastEvent]; + mpLastEvent = [pEvent retain]; + + mbInKeyInput = true; + mbNeedSpecialKeyHandle = false; + mbKeyHandled = false; + + mpFrame->mnLastEventTime = static_cast<sal_uInt64>( [pEvent timestamp] * 1000.0 ); + mpFrame->mnLastModifierFlags = [pEvent modifierFlags]; + + if( ! [self handleKeyDownException: pEvent] ) + { + sal_uInt16 nKeyCode = ImplMapKeyCode( [pEvent keyCode] ); + if ( nKeyCode == KEY_DELETE && mbTextInputWantsNonRepeatKeyDown ) + { + // tdf#42437 Enable press-and-hold special character input method + // Emulate the press-and-hold behavior of the TextEdit + // application by deleting the marked text when only the + // Delete key is pressed and keep the marked text when the + // Backspace key or Fn-Delete keys are pressed. + if ( [pEvent keyCode] == 51 ) + { + [self deleteTextInputWantsNonRepeatKeyDown]; + } + else + { + [self unmarkText]; + mbKeyHandled = true; + mbInKeyInput = false; + } + + [self endExtTextInput]; + return; + } + + NSArray* pArray = [NSArray arrayWithObject: pEvent]; + [self interpretKeyEvents: pArray]; + + // Handle repeat key events by explicitly inserting the text if + // -[NSResponder interpretKeyEvents:] does not insert or mark any + // text. Note: do not do this step if there is uncommitted text. + // Related: tdf#42437 Skip special press-and-hold handling for action keys + // Pressing and holding action keys such as arrow keys must not be + // handled like pressing and holding a character key as it will + // insert unexpected text. + if ( !mbKeyHandled && !mpLastMarkedText && mpLastEvent && [mpLastEvent type] == NSEventTypeKeyDown && [mpLastEvent isARepeat] ) + { + NSString *pChars = [mpLastEvent characters]; + if ( pChars ) + [self insertText:pChars replacementRange:NSMakeRange( 0, [pChars length] )]; + } + // tdf#42437 Enable press-and-hold special character input method + // Emulate the press-and-hold behavior of the TextEdit application + // by committing an empty string for key down events dispatched + // while the special character input method popup is displayed. + else if ( mpLastMarkedText && mbTextInputWantsNonRepeatKeyDown && mpLastEvent && [mpLastEvent type] == NSEventTypeKeyDown && ![mpLastEvent isARepeat] ) + { + // If the escape or return key is pressed, unmark the text to + // skip deletion of marked text + if ( nKeyCode == KEY_ESCAPE || nKeyCode == KEY_RETURN ) + [self unmarkText]; + [self insertText:[NSString string] replacementRange:NSMakeRange( NSNotFound, 0 )]; + } + } + + mbInKeyInput = false; + } +} + +-(BOOL)handleKeyDownException:(NSEvent*)pEvent +{ + // check for a very special set of modified characters + NSString* pUnmodifiedString = [pEvent charactersIgnoringModifiers]; + + if( pUnmodifiedString && [pUnmodifiedString length] == 1 ) + { + /* #i103102# key events with command and alternate don't make it through + interpretKeyEvents (why?). Try to dispatch them here first, + if not successful continue normally + */ + if( (mpFrame->mnLastModifierFlags & (NSEventModifierFlagOption | NSEventModifierFlagCommand)) + == (NSEventModifierFlagOption | NSEventModifierFlagCommand) ) + { + if( [self sendSingleCharacter: mpLastEvent] ) + return YES; + } + } + return NO; +} + +-(void)flagsChanged: (NSEvent*)pEvent +{ + SolarMutexGuard aGuard; + + if( AquaSalFrame::isAlive( mpFrame ) ) + { + mpFrame->mnLastEventTime = static_cast<sal_uInt64>( [pEvent timestamp] * 1000.0 ); + mpFrame->mnLastModifierFlags = [pEvent modifierFlags]; + } +} + +-(void)insertText:(id)aString replacementRange:(NSRange)replacementRange +{ + (void) replacementRange; // FIXME: surely it must be used + + SolarMutexGuard aGuard; + + [self deleteTextInputWantsNonRepeatKeyDown]; + + // Ignore duplicate events that are sometimes posted during cancellation + // of the native input method session. This usually happens when + // [self endExtTextInput] is called from [self windowDidBecomeKey:] and, + // if the native input method popup, that was cancelled in a + // previous call to [self windowDidResignKey:], has reappeared. In such + // cases, the native input context posts the reappearing popup's + // uncommitted text. + if (mbInEndExtTextInput && !mbInCommitMarkedText) + return; + + if( AquaSalFrame::isAlive( mpFrame ) ) + { + NSString* pInsert = nil; + if( [aString isKindOfClass: [NSAttributedString class]] ) + pInsert = [aString string]; + else + pInsert = aString; + + int nLen = 0; + if( pInsert && ( nLen = [pInsert length] ) > 0 ) + { + OUString aInsertString( GetOUString( pInsert ) ); + // aCharCode initializer is safe since aInsertString will at least contain '\0' + sal_Unicode aCharCode = *aInsertString.getStr(); + + if( nLen == 1 && + aCharCode < 0x80 && + aCharCode > 0x1f && + ! [self hasMarkedText ] + ) + { + sal_uInt16 nKeyCode = ImplMapCharCode( aCharCode ); + unsigned int nLastModifiers = mpFrame->mnLastModifierFlags; + + // #i99567# + // find out the unmodified key code + + // sanity check + if( mpLastEvent && ( [mpLastEvent type] == NSEventTypeKeyDown || [mpLastEvent type] == NSEventTypeKeyUp ) ) + { + // get unmodified string + NSString* pUnmodifiedString = [mpLastEvent charactersIgnoringModifiers]; + if( pUnmodifiedString && [pUnmodifiedString length] == 1 ) + { + // map the unmodified key code + unichar keyChar = [pUnmodifiedString characterAtIndex: 0]; + nKeyCode = ImplMapCharCode( keyChar ); + } + nLastModifiers = [mpLastEvent modifierFlags]; + + } + // #i99567# + // applications and vcl's edit fields ignore key events with ALT + // however we're at a place where we know text should be inserted + // so it seems we need to strip the Alt modifier here + if( (nLastModifiers & (NSEventModifierFlagControl | NSEventModifierFlagOption | NSEventModifierFlagCommand)) + == NSEventModifierFlagOption ) + { + nLastModifiers = 0; + } + [self sendKeyInputAndReleaseToFrame: nKeyCode character: aCharCode modifiers: nLastModifiers]; + } + else + { + SalExtTextInputEvent aEvent; + aEvent.maText = aInsertString; + aEvent.mpTextAttr = nullptr; + aEvent.mnCursorPos = aInsertString.getLength(); + aEvent.mnCursorFlags = 0; + mpFrame->CallCallback( SalEvent::ExtTextInput, &aEvent ); + if( AquaSalFrame::isAlive( mpFrame ) ) + mpFrame->CallCallback( SalEvent::EndExtTextInput, nullptr ); + } + } + else + { + SalExtTextInputEvent aEvent; + aEvent.maText.clear(); + aEvent.mpTextAttr = nullptr; + aEvent.mnCursorPos = 0; + aEvent.mnCursorFlags = 0; + mpFrame->CallCallback( SalEvent::ExtTextInput, &aEvent ); + if( AquaSalFrame::isAlive( mpFrame ) ) + mpFrame->CallCallback( SalEvent::EndExtTextInput, nullptr ); + + } + [self unmarkText]; + } + + // Mark event as handled even if the frame isn't valid like is done in + // [self setMarkedText:selectedRange:replacementRange:] and + // [self doCommandBySelector:] + mbKeyHandled = true; +} + +-(void)insertTab: (id)aSender +{ + (void)aSender; + [self sendKeyInputAndReleaseToFrame: KEY_TAB character: '\t' modifiers: 0]; +} + +-(void)insertBacktab: (id)aSender +{ + (void)aSender; + [self sendKeyInputAndReleaseToFrame: (KEY_TAB | KEY_SHIFT) character: '\t' modifiers: 0]; +} + +-(void)moveLeft: (id)aSender +{ + (void)aSender; + [self sendKeyInputAndReleaseToFrame: KEY_LEFT character: 0 modifiers: 0]; +} + +-(void)moveLeftAndModifySelection: (id)aSender +{ + (void)aSender; + [self sendKeyInputAndReleaseToFrame: KEY_LEFT character: 0 modifiers: NSEventModifierFlagShift]; +} + +-(void)moveBackwardAndModifySelection: (id)aSender +{ + (void)aSender; + [self sendKeyInputAndReleaseToFrame: css::awt::Key::SELECT_BACKWARD character: 0 modifiers: 0]; +} + +-(void)moveRight: (id)aSender +{ + (void)aSender; + [self sendKeyInputAndReleaseToFrame: KEY_RIGHT character: 0 modifiers: 0]; +} + +-(void)moveRightAndModifySelection: (id)aSender +{ + (void)aSender; + [self sendKeyInputAndReleaseToFrame: KEY_RIGHT character: 0 modifiers: NSEventModifierFlagShift]; +} + +-(void)moveForwardAndModifySelection: (id)aSender +{ + (void)aSender; + [self sendKeyInputAndReleaseToFrame: css::awt::Key::SELECT_FORWARD character: 0 modifiers: 0]; +} + +-(void)moveWordLeft: (id)aSender +{ + (void)aSender; + [self sendKeyInputAndReleaseToFrame: css::awt::Key::MOVE_WORD_BACKWARD character: 0 modifiers: 0]; +} + +-(void)moveWordBackward: (id)aSender +{ + (void)aSender; + [self sendKeyInputAndReleaseToFrame: css::awt::Key::MOVE_WORD_BACKWARD character: 0 modifiers: 0]; +} + +-(void)moveWordBackwardAndModifySelection: (id)aSender +{ + (void)aSender; + [self sendKeyInputAndReleaseToFrame: css::awt::Key::SELECT_WORD_BACKWARD character: 0 modifiers: 0]; +} + +-(void)moveWordLeftAndModifySelection: (id)aSender +{ + (void)aSender; + [self sendKeyInputAndReleaseToFrame: css::awt::Key::SELECT_WORD_BACKWARD character: 0 modifiers: 0]; +} + +-(void)moveWordRight: (id)aSender +{ + (void)aSender; + [self sendKeyInputAndReleaseToFrame: css::awt::Key::MOVE_WORD_FORWARD character: 0 modifiers: 0]; +} + +-(void)moveWordForward: (id)aSender +{ + (void)aSender; + [self sendKeyInputAndReleaseToFrame: css::awt::Key::MOVE_WORD_FORWARD character: 0 modifiers: 0]; +} + +-(void)moveWordForwardAndModifySelection: (id)aSender +{ + (void)aSender; + [self sendKeyInputAndReleaseToFrame: css::awt::Key::SELECT_WORD_FORWARD character: 0 modifiers: 0]; +} + +-(void)moveWordRightAndModifySelection: (id)aSender +{ + (void)aSender; + [self sendKeyInputAndReleaseToFrame: css::awt::Key::SELECT_WORD_FORWARD character: 0 modifiers: 0]; +} + +-(void)moveToEndOfLine: (id)aSender +{ + (void)aSender; + [self sendKeyInputAndReleaseToFrame: css::awt::Key::MOVE_TO_END_OF_LINE character: 0 modifiers: 0]; +} + +-(void)moveToRightEndOfLine: (id)aSender +{ + (void)aSender; + [self sendKeyInputAndReleaseToFrame: css::awt::Key::MOVE_TO_END_OF_LINE character: 0 modifiers: 0]; +} + +-(void)moveToEndOfLineAndModifySelection: (id)aSender +{ + (void)aSender; + [self sendKeyInputAndReleaseToFrame: css::awt::Key::SELECT_TO_END_OF_LINE character: 0 modifiers: 0]; +} + +-(void)moveToRightEndOfLineAndModifySelection: (id)aSender +{ + (void)aSender; + [self sendKeyInputAndReleaseToFrame: css::awt::Key::SELECT_TO_END_OF_LINE character: 0 modifiers: 0]; +} + +-(void)moveToBeginningOfLine: (id)aSender +{ + (void)aSender; + [self sendKeyInputAndReleaseToFrame: css::awt::Key::MOVE_TO_BEGIN_OF_LINE character: 0 modifiers: 0]; +} + +-(void)moveToLeftEndOfLine: (id)aSender +{ + (void)aSender; + [self sendKeyInputAndReleaseToFrame: css::awt::Key::MOVE_TO_BEGIN_OF_LINE character: 0 modifiers: 0]; +} + +-(void)moveToBeginningOfLineAndModifySelection: (id)aSender +{ + (void)aSender; + [self sendKeyInputAndReleaseToFrame: css::awt::Key::SELECT_TO_BEGIN_OF_LINE character: 0 modifiers: 0]; +} + +-(void)moveToLeftEndOfLineAndModifySelection: (id)aSender +{ + (void)aSender; + [self sendKeyInputAndReleaseToFrame: css::awt::Key::SELECT_TO_BEGIN_OF_LINE character: 0 modifiers: 0]; +} + +-(void)moveToEndOfParagraph: (id)aSender +{ + (void)aSender; + [self sendKeyInputAndReleaseToFrame: css::awt::Key::MOVE_TO_END_OF_PARAGRAPH character: 0 modifiers: 0]; +} + +-(void)moveToEndOfParagraphAndModifySelection: (id)aSender +{ + (void)aSender; + [self sendKeyInputAndReleaseToFrame: css::awt::Key::SELECT_TO_END_OF_PARAGRAPH character: 0 modifiers: 0]; +} + +-(void)moveParagraphForward: (id)aSender +{ + (void)aSender; + [self sendKeyInputAndReleaseToFrame: css::awt::Key::MOVE_TO_END_OF_PARAGRAPH character: 0 modifiers: 0]; +} + +-(void)moveParagraphForwardAndModifySelection: (id)aSender +{ + (void)aSender; + [self sendKeyInputAndReleaseToFrame: css::awt::Key::SELECT_TO_END_OF_PARAGRAPH character: 0 modifiers: 0]; +} + +-(void)moveToBeginningOfParagraph: (id)aSender +{ + (void)aSender; + [self sendKeyInputAndReleaseToFrame: css::awt::Key::MOVE_TO_BEGIN_OF_PARAGRAPH character: 0 modifiers: 0]; +} + +-(void)moveParagraphBackward: (id)aSender +{ + (void)aSender; + [self sendKeyInputAndReleaseToFrame: css::awt::Key::MOVE_TO_BEGIN_OF_PARAGRAPH character: 0 modifiers: 0]; +} + +-(void)moveToBeginningOfParagraphAndModifySelection: (id)aSender +{ + (void)aSender; + [self sendKeyInputAndReleaseToFrame: css::awt::Key::SELECT_TO_BEGIN_OF_PARAGRAPH character: 0 modifiers: 0]; +} + +-(void)moveParagraphBackwardAndModifySelection: (id)aSender +{ + (void)aSender; + [self sendKeyInputAndReleaseToFrame: css::awt::Key::SELECT_TO_BEGIN_OF_PARAGRAPH character: 0 modifiers: 0]; +} + +-(void)moveToEndOfDocument: (id)aSender +{ + (void)aSender; + [self sendKeyInputAndReleaseToFrame: css::awt::Key::MOVE_TO_END_OF_DOCUMENT character: 0 modifiers: 0]; +} + +-(void)scrollToEndOfDocument: (id)aSender +{ + (void)aSender; + // this is not exactly what we should do, but it makes "End" and "Shift-End" behave consistent + [self sendKeyInputAndReleaseToFrame: css::awt::Key::MOVE_TO_END_OF_DOCUMENT character: 0 modifiers: 0]; +} + +-(void)moveToEndOfDocumentAndModifySelection: (id)aSender +{ + (void)aSender; + [self sendKeyInputAndReleaseToFrame: css::awt::Key::SELECT_TO_END_OF_DOCUMENT character: 0 modifiers: 0]; +} + +-(void)moveToBeginningOfDocument: (id)aSender +{ + (void)aSender; + [self sendKeyInputAndReleaseToFrame: css::awt::Key::MOVE_TO_BEGIN_OF_DOCUMENT character: 0 modifiers: 0]; +} + +-(void)scrollToBeginningOfDocument: (id)aSender +{ + (void)aSender; + // this is not exactly what we should do, but it makes "Home" and "Shift-Home" behave consistent + [self sendKeyInputAndReleaseToFrame: css::awt::Key::MOVE_TO_BEGIN_OF_DOCUMENT character: 0 modifiers: 0]; +} + +-(void)moveToBeginningOfDocumentAndModifySelection: (id)aSender +{ + (void)aSender; + [self sendKeyInputAndReleaseToFrame: css::awt::Key::SELECT_TO_BEGIN_OF_DOCUMENT character: 0 modifiers: 0]; +} + +-(void)moveUp: (id)aSender +{ + (void)aSender; + [self sendKeyInputAndReleaseToFrame: KEY_UP character: 0 modifiers: 0]; +} + +-(void)moveDown: (id)aSender +{ + (void)aSender; + [self sendKeyInputAndReleaseToFrame: KEY_DOWN character: 0 modifiers: 0]; +} + +-(void)insertNewline: (id)aSender +{ + (void)aSender; + // #i91267# make enter and shift-enter work by evaluating the modifiers + [self sendKeyInputAndReleaseToFrame: KEY_RETURN character: '\n' modifiers: mpFrame->mnLastModifierFlags]; +} + +-(void)deleteBackward: (id)aSender +{ + (void)aSender; + [self sendKeyInputAndReleaseToFrame: KEY_BACKSPACE character: '\b' modifiers: 0]; +} + +-(void)deleteForward: (id)aSender +{ + (void)aSender; + [self sendKeyInputAndReleaseToFrame: KEY_DELETE character: 0x7f modifiers: 0]; +} + +-(void)deleteBackwardByDecomposingPreviousCharacter: (id)aSender +{ + (void)aSender; + [self sendKeyInputAndReleaseToFrame: KEY_BACKSPACE character: '\b' modifiers: 0]; +} + +-(void)deleteWordBackward: (id)aSender +{ + (void)aSender; + [self sendKeyInputAndReleaseToFrame: css::awt::Key::DELETE_WORD_BACKWARD character: 0 modifiers: 0]; +} + +-(void)deleteWordForward: (id)aSender +{ + (void)aSender; + [self sendKeyInputAndReleaseToFrame: css::awt::Key::DELETE_WORD_FORWARD character: 0 modifiers: 0]; +} + +-(void)deleteToBeginningOfLine: (id)aSender +{ + (void)aSender; + [self sendKeyInputAndReleaseToFrame: css::awt::Key::DELETE_TO_BEGIN_OF_LINE character: 0 modifiers: 0]; +} + +-(void)deleteToEndOfLine: (id)aSender +{ + (void)aSender; + [self sendKeyInputAndReleaseToFrame: css::awt::Key::DELETE_TO_END_OF_LINE character: 0 modifiers: 0]; +} + +-(void)deleteToBeginningOfParagraph: (id)aSender +{ + (void)aSender; + [self sendKeyInputAndReleaseToFrame: css::awt::Key::DELETE_TO_BEGIN_OF_PARAGRAPH character: 0 modifiers: 0]; +} + +-(void)deleteToEndOfParagraph: (id)aSender +{ + (void)aSender; + [self sendKeyInputAndReleaseToFrame: css::awt::Key::DELETE_TO_END_OF_PARAGRAPH character: 0 modifiers: 0]; +} + +-(void)insertLineBreak: (id)aSender +{ + (void)aSender; + [self sendKeyInputAndReleaseToFrame: css::awt::Key::INSERT_LINEBREAK character: 0 modifiers: 0]; +} + +-(void)insertParagraphSeparator: (id)aSender +{ + (void)aSender; + [self sendKeyInputAndReleaseToFrame: css::awt::Key::INSERT_PARAGRAPH character: 0 modifiers: 0]; +} + +-(void)selectWord: (id)aSender +{ + (void)aSender; + [self sendKeyInputAndReleaseToFrame: css::awt::Key::SELECT_WORD character: 0 modifiers: 0]; +} + +-(void)selectLine: (id)aSender +{ + (void)aSender; + [self sendKeyInputAndReleaseToFrame: css::awt::Key::SELECT_LINE character: 0 modifiers: 0]; +} + +-(void)selectParagraph: (id)aSender +{ + (void)aSender; + [self sendKeyInputAndReleaseToFrame: css::awt::Key::SELECT_PARAGRAPH character: 0 modifiers: 0]; +} + +-(void)selectAll: (id)aSender +{ + (void)aSender; + [self sendKeyInputAndReleaseToFrame: css::awt::Key::SELECT_ALL character: 0 modifiers: 0]; +} + +-(void)cancelOperation: (id)aSender +{ + (void)aSender; + [self sendKeyInputAndReleaseToFrame: KEY_ESCAPE character: 0x1b modifiers: 0]; +} + +-(void)noop: (id)aSender +{ + (void)aSender; + if( ! mbKeyHandled ) + { + if( ! [self sendSingleCharacter:mpLastEvent] ) + { + /* prevent recursion */ + if( mpLastEvent != mpLastSuperEvent && [NSApp respondsToSelector: @selector(sendSuperEvent:)] ) + { + id pLastSuperEvent = mpLastSuperEvent; + mpLastSuperEvent = mpLastEvent; + [NSApp performSelector:@selector(sendSuperEvent:) withObject: mpLastEvent]; + mpLastSuperEvent = pLastSuperEvent; + + std::map< NSEvent*, bool >::iterator it = GetSalData()->maKeyEventAnswer.find( mpLastEvent ); + if( it != GetSalData()->maKeyEventAnswer.end() ) + it->second = true; + } + } + } +} + +-(BOOL)sendKeyInputAndReleaseToFrame: (sal_uInt16)nKeyCode character: (sal_Unicode)aChar +{ + return [self sendKeyInputAndReleaseToFrame: nKeyCode character: aChar modifiers: mpFrame->mnLastModifierFlags]; +} + +-(BOOL)sendKeyInputAndReleaseToFrame: (sal_uInt16)nKeyCode character: (sal_Unicode)aChar modifiers: (unsigned int)nMod +{ + return [self sendKeyToFrameDirect: nKeyCode character: aChar modifiers: nMod] || + [self sendSingleCharacter: mpLastEvent]; +} + +-(BOOL)sendKeyToFrameDirect: (sal_uInt16)nKeyCode character: (sal_Unicode)aChar modifiers: (unsigned int)nMod +{ + SolarMutexGuard aGuard; + + bool nRet = false; + if( AquaSalFrame::isAlive( mpFrame ) ) + { + SalKeyEvent aEvent; + aEvent.mnCode = nKeyCode | ImplGetModifierMask( nMod ); + aEvent.mnCharCode = aChar; + aEvent.mnRepeat = FALSE; + nRet = mpFrame->CallCallback( SalEvent::KeyInput, &aEvent ); + std::map< NSEvent*, bool >::iterator it = GetSalData()->maKeyEventAnswer.find( mpLastEvent ); + if( it != GetSalData()->maKeyEventAnswer.end() ) + it->second = nRet; + if( AquaSalFrame::isAlive( mpFrame ) ) + mpFrame->CallCallback( SalEvent::KeyUp, &aEvent ); + } + return nRet; +} + + +-(BOOL)sendSingleCharacter: (NSEvent *)pEvent +{ + NSString* pUnmodifiedString = [pEvent charactersIgnoringModifiers]; + + if( pUnmodifiedString && [pUnmodifiedString length] == 1 ) + { + unichar keyChar = [pUnmodifiedString characterAtIndex: 0]; + sal_uInt16 nKeyCode = ImplMapCharCode( keyChar ); + if (nKeyCode == 0) + { + sal_uInt16 nOtherKeyCode = [pEvent keyCode]; + nKeyCode = ImplMapKeyCode(nOtherKeyCode); + } + if( nKeyCode != 0 ) + { + // don't send code points in the private use area + if( keyChar >= 0xf700 && keyChar < 0xf780 ) + keyChar = 0; + bool bRet = [self sendKeyToFrameDirect: nKeyCode character: keyChar modifiers: mpFrame->mnLastModifierFlags]; + mbInKeyInput = false; + + return bRet; + } + } + return NO; +} + + +// NSTextInput/NSTextInputClient protocol +- (NSArray *)validAttributesForMarkedText +{ + return [NSArray arrayWithObjects:NSUnderlineStyleAttributeName, nil]; +} + +- (BOOL)hasMarkedText +{ + bool bHasMarkedText; + + bHasMarkedText = ( mMarkedRange.location != NSNotFound ) && + ( mMarkedRange.length != 0 ); + // hack to check keys like "Control-j" + if( mbInKeyInput ) + { + mbNeedSpecialKeyHandle = true; + } + + // FIXME: + // #i106901# + // if we come here outside of mbInKeyInput, this is likely to be because + // of the keyboard viewer. For unknown reasons having no marked range + // in this case causes a crash. So we say we have a marked range anyway + // This is a hack, since it is not understood what a) causes that crash + // and b) why we should have a marked range at this point. + if( ! mbInKeyInput ) + bHasMarkedText = true; + + return bHasMarkedText; +} + +- (NSRange)markedRange +{ + // FIXME: + // #i106901# + // if we come here outside of mbInKeyInput, this is likely to be because + // of the keyboard viewer. For unknown reasons having no marked range + // in this case causes a crash. So we say we have a marked range anyway + // This is a hack, since it is not understood what a) causes that crash + // and b) why we should have a marked range at this point. Stop the native + // input method popup from appearing in the bottom left corner of the + // screen by returning the marked range if is valid when called outside of + // mbInKeyInput. If a zero length range is returned, macOS won't call + // [self firstRectForCharacterRange:actualRange:] for any newly appended + // uncommitted text. + if( ! mbInKeyInput ) + return mMarkedRange.location != NSNotFound ? mMarkedRange : NSMakeRange( 0, 0 ); + + return [self hasMarkedText] ? mMarkedRange : NSMakeRange( NSNotFound, 0 ); +} + +- (NSRange)selectedRange +{ + // tdf#42437 Enable press-and-hold special character input method + // Always return a valid range location. If the range location is + // NSNotFound, -[NSResponder interpretKeyEvents:] will not call + // [self firstRectForCharacterRange:actualRange:] and will not display the + // special character input method popup. + return ( mSelectedRange.location == NSNotFound ? NSMakeRange( 0, 0 ) : mSelectedRange ); +} + +- (void)setMarkedText:(id)aString selectedRange:(NSRange)selRange replacementRange:(NSRange)replacementRange +{ + (void) replacementRange; // FIXME - use it! + + SolarMutexGuard aGuard; + + [self deleteTextInputWantsNonRepeatKeyDown]; + + if( ![aString isKindOfClass:[NSAttributedString class]] ) + aString = [[[NSAttributedString alloc] initWithString:aString] autorelease]; + + // Reset cached state + [self unmarkText]; + + int len = [aString length]; + SalExtTextInputEvent aInputEvent; + if( len > 0 ) { + // Set the marked and selected ranges to the marked text and selected + // range parameters + mMarkedRange = NSMakeRange( 0, [aString length] ); + if (selRange.location == NSNotFound || selRange.location >= mMarkedRange.length) + mSelectedRange = NSMakeRange( NSNotFound, 0 ); + else + mSelectedRange = NSMakeRange( selRange.location, selRange.location + selRange.length > mMarkedRange.length ? mMarkedRange.length - selRange.location : selRange.length ); + + // If we are going to post uncommitted text, cache the string parameter + // as is needed in both [self endExtTextInput] and + // [self attributedSubstringForProposedRange:actualRange:] + mpLastMarkedText = [aString retain]; + + NSString *pString = [aString string]; + OUString aInsertString( GetOUString( pString ) ); + std::vector<ExtTextInputAttr> aInputFlags( std::max( 1, len ), ExtTextInputAttr::NONE ); + int nSelectionStart = (mSelectedRange.location == NSNotFound ? len : mSelectedRange.location); + int nSelectionEnd = (mSelectedRange.location == NSNotFound ? len : mSelectedRange.location + selRange.length); + for ( int i = 0; i < len; i++ ) + { + // Highlight all characters in the selected range. Normally + // uncommitted text is underlined but when an item is selected in + // the native input method popup or selecting a subblock of + // uncommitted text using the left or right arrow keys, the + // selection range is set and the selected range is either + // highlighted like in Excel or is bold underlined like in + // Safari. Highlighting the selected range was chosen because + // using bold and double underlines can get clipped making the + // selection range indistinguishable from the rest of the + // uncommitted text. + if (i >= nSelectionStart && i < nSelectionEnd) + { + aInputFlags[i] = ExtTextInputAttr::Highlight; + continue; + } + + unsigned int nUnderlineValue; + NSRange effectiveRange; + + effectiveRange = NSMakeRange(i, 1); + nUnderlineValue = [[aString attribute:NSUnderlineStyleAttributeName atIndex:i effectiveRange:&effectiveRange] unsignedIntValue]; + + switch (nUnderlineValue & 0xff) { + case NSUnderlineStyleSingle: + aInputFlags[i] = ExtTextInputAttr::Underline; + break; + case NSUnderlineStyleThick: + aInputFlags[i] = ExtTextInputAttr::BoldUnderline; + break; + case NSUnderlineStyleDouble: + aInputFlags[i] = ExtTextInputAttr::DoubleUnderline; + break; + default: + aInputFlags[i] = ExtTextInputAttr::Highlight; + break; + } + } + + aInputEvent.maText = aInsertString; + aInputEvent.mnCursorPos = nSelectionStart; + aInputEvent.mnCursorFlags = 0; + aInputEvent.mpTextAttr = aInputFlags.data(); + mpFrame->CallCallback( SalEvent::ExtTextInput, static_cast<void *>(&aInputEvent) ); + } else { + aInputEvent.maText.clear(); + aInputEvent.mnCursorPos = 0; + aInputEvent.mnCursorFlags = 0; + aInputEvent.mpTextAttr = nullptr; + mpFrame->CallCallback( SalEvent::ExtTextInput, static_cast<void *>(&aInputEvent) ); + mpFrame->CallCallback( SalEvent::EndExtTextInput, nullptr ); + } + mbKeyHandled= true; +} + +- (void)unmarkText +{ + [self clearLastMarkedText]; + + mSelectedRange = mMarkedRange = NSMakeRange(NSNotFound, 0); +} + +- (NSAttributedString *)attributedSubstringForProposedRange:(NSRange)aRange actualRange:(NSRangePointer)actualRange +{ + (void) aRange; + (void) actualRange; + + // FIXME - Implement + return nil; +} + +- (NSUInteger)characterIndexForPoint:(NSPoint)thePoint +{ + (void)thePoint; + // FIXME + return 0; +} + +- (NSInteger)conversationIdentifier +{ + return reinterpret_cast<long>(self); +} + +- (void)doCommandBySelector:(SEL)aSelector +{ + if( AquaSalFrame::isAlive( mpFrame ) ) + { + if( (mpFrame->mnICOptions & InputContextFlags::Text) && + aSelector != nullptr && [self respondsToSelector: aSelector] ) + { + [self performSelector: aSelector]; + } + else + { + [self sendSingleCharacter:mpLastEvent]; + } + } + + mbKeyHandled = true; +} + +-(void)clearLastEvent +{ + if (mpLastEvent) + { + [mpLastEvent release]; + mpLastEvent = nil; + } +} + +-(void)clearLastMarkedText +{ + if (mpLastMarkedText) + { + [mpLastMarkedText release]; + mpLastMarkedText = nil; + } + + mbTextInputWantsNonRepeatKeyDown = NO; +} + +- (NSRect)firstRectForCharacterRange:(NSRange)aRange actualRange:(NSRangePointer)actualRange +{ + // FIXME - These should probably be used? + (void) aRange; + (void) actualRange; + + SolarMutexGuard aGuard; + + // tdf#42437 Enable press-and-hold special character input method + // Some text entry controls, such as Writer comments or the cell editor in + // Calc's Formula Bar, need to have an input method session open or else + // the returned position won't be anywhere near the text cursor. So, + // dispatch an empty SalEvent::ExtTextInput event, fetch the position, + // and then dispatch a SalEvent::EndExtTextInput event. + NSString *pNewMarkedText = nullptr; + NSString *pChars = [mpLastEvent characters]; + bool bNeedsExtTextInput = ( pChars && mbInKeyInput && !mpLastMarkedText && mpLastEvent && [mpLastEvent type] == NSEventTypeKeyDown && [mpLastEvent isARepeat] ); + if ( bNeedsExtTextInput ) + { + // tdf#154708 Preserve selection for repeating Shift-arrow on Japanese keyboard + // Skip the posting of SalEvent::ExtTextInput and + // SalEvent::EndExtTextInput events for private use area characters. + NSUInteger nLen = [pChars length]; + auto const pBuf = std::make_unique<unichar[]>( nLen + 1 ); + NSUInteger nBufLen = 0; + for ( NSUInteger i = 0; i < nLen; i++ ) + { + unichar aChar = [pChars characterAtIndex:i]; + if ( aChar >= 0xf700 && aChar < 0xf780 ) + continue; + + pBuf[nBufLen++] = aChar; + } + pBuf[nBufLen] = 0; + + pNewMarkedText = [NSString stringWithCharacters:pBuf.get() length:nBufLen]; + if (!pNewMarkedText || ![pNewMarkedText length]) + bNeedsExtTextInput = false; + } + + if ( bNeedsExtTextInput ) + { + SalExtTextInputEvent aInputEvent; + aInputEvent.maText.clear(); + aInputEvent.mnCursorPos = 0; + aInputEvent.mnCursorFlags = 0; + aInputEvent.mpTextAttr = nullptr; + if ( mpFrame && AquaSalFrame::isAlive( mpFrame ) ) + mpFrame->CallCallback( SalEvent::ExtTextInput, static_cast<void *>(&aInputEvent) ); + } + + SalExtTextInputPosEvent aPosEvent; + if ( mpFrame && AquaSalFrame::isAlive( mpFrame ) ) + mpFrame->CallCallback( SalEvent::ExtTextInputPos, static_cast<void *>(&aPosEvent) ); + + if ( bNeedsExtTextInput ) + { + if ( mpFrame && AquaSalFrame::isAlive( mpFrame ) ) + mpFrame->CallCallback( SalEvent::EndExtTextInput, nullptr ); + + // tdf#42437 Enable press-and-hold special character input method + // Emulate the press-and-hold behavior of the TextEdit application by + // setting the marked text to the last key down event's characters. The + // characters will already have been committed by the special character + // input method so set the mbTextInputWantsNonRepeatKeyDown flag to + // indicate that the characters need to be deleted if the input method + // replaces the committed characters. + if ( pNewMarkedText ) + { + [self unmarkText]; + mpLastMarkedText = [[NSAttributedString alloc] initWithString:pNewMarkedText]; + mSelectedRange = mMarkedRange = NSMakeRange( 0, [mpLastMarkedText length] ); + mbTextInputWantsNonRepeatKeyDown = YES; + } + } + + NSRect rect; + + if ( mpFrame && AquaSalFrame::isAlive( mpFrame ) ) + { + rect.origin.x = aPosEvent.mnX + mpFrame->maGeometry.x(); + rect.origin.y = aPosEvent.mnY + mpFrame->maGeometry.y() + 4; // add some space for underlines + rect.size.width = aPosEvent.mnWidth; + rect.size.height = aPosEvent.mnHeight; + + mpFrame->VCLToCocoa( rect ); + } + else + { + rect = NSMakeRect( aPosEvent.mnX, aPosEvent.mnY, aPosEvent.mnWidth, aPosEvent.mnHeight ); + } + + return rect; +} + +-(id)parentAttribute { + return reinterpret_cast<NSView*>(mpFrame->getNSWindow()); + //TODO: odd cast really needed for fdo#74121? +} + +-(css::accessibility::XAccessibleContext *)accessibleContext +{ + SolarMutexGuard aGuard; + + [self insertRegisteredWrapperIntoWrapperRepository]; + if (mpChildWrapper) + return [mpChildWrapper accessibleContext]; + + return nil; +} + +-(NSWindow*)windowForParent +{ + return mpFrame->getNSWindow(); +} + +-(void)registerMouseEventListener: (id)theListener +{ + mpMouseEventListener = theListener; +} + +-(void)unregisterMouseEventListener: (id)theListener +{ + (void)theListener; + mpMouseEventListener = nil; +} + +-(NSDragOperation)draggingEntered:(id <NSDraggingInfo>)sender +{ + return [mDraggingDestinationHandler draggingEntered: sender]; +} + +-(NSDragOperation)draggingUpdated:(id <NSDraggingInfo>)sender +{ + return [mDraggingDestinationHandler draggingUpdated: sender]; +} + +-(void)draggingExited:(id <NSDraggingInfo>)sender +{ + [mDraggingDestinationHandler draggingExited: sender]; +} + +-(BOOL)prepareForDragOperation:(id <NSDraggingInfo>)sender +{ + return [mDraggingDestinationHandler prepareForDragOperation: sender]; +} + +-(BOOL)performDragOperation:(id <NSDraggingInfo>)sender +{ + return [mDraggingDestinationHandler performDragOperation: sender]; +} + +-(void)concludeDragOperation:(id <NSDraggingInfo>)sender +{ + [mDraggingDestinationHandler concludeDragOperation: sender]; +} + +-(void)registerDraggingDestinationHandler:(id)theHandler +{ + mDraggingDestinationHandler = theHandler; +} + +-(void)unregisterDraggingDestinationHandler:(id)theHandler +{ + (void)theHandler; + mDraggingDestinationHandler = nil; +} + +-(void)endExtTextInput +{ + [self endExtTextInput:EndExtTextInputFlags::Complete]; +} + +-(void)endExtTextInput:(EndExtTextInputFlags)nFlags +{ + // Prevent recursion from any additional [self insertText:] calls that + // may be called when cancelling the native input method session + if (mbInEndExtTextInput) + return; + + mbInEndExtTextInput = YES; + + SolarMutexGuard aGuard; + + NSTextInputContext *pInputContext = [NSTextInputContext currentInputContext]; + if (pInputContext) + { + // Cancel the native input method session + [pInputContext discardMarkedText]; + + // Commit any uncommitted text. Note: when the delete key is used to + // remove all uncommitted characters, the marked range will be zero + // length but a SalEvent::EndExtTextInput must still be dispatched. + if (mpLastMarkedText && [mpLastMarkedText length] && mMarkedRange.location != NSNotFound && mpFrame && AquaSalFrame::isAlive(mpFrame)) + { + // If there is any marked text, SalEvent::EndExtTextInput may leave + // the cursor hidden so commit the marked text to force the cursor + // to be visible. + mbInCommitMarkedText = YES; + if (nFlags & EndExtTextInputFlags::Complete) + { + // Retain the last marked text as it will be released in + // [self insertText:replacementText:] + NSAttributedString *pText = [mpLastMarkedText retain]; + [self insertText:pText replacementRange:NSMakeRange(0, [mpLastMarkedText length])]; + [pText release]; + } + else + { + [self insertText:[NSString string] replacementRange:NSMakeRange(0, 0)]; + } + mbInCommitMarkedText = NO; + } + + [self unmarkText]; + + // If a different view is the input context's client, commit that + // view's uncommitted text as well + id<NSTextInputClient> pClient = [pInputContext client]; + if (pClient != self) + { + SalFrameView *pView = static_cast<SalFrameView*>(pClient); + if ([pView isKindOfClass:[SalFrameView class]]) + [pView endExtTextInput]; + else + [pClient unmarkText]; + } + } + + mbInEndExtTextInput = NO; +} + +-(void)deleteTextInputWantsNonRepeatKeyDown +{ + SolarMutexGuard aGuard; + + // tdf#42437 Enable press-and-hold special character input method + // Emulate the press-and-hold behavior of the TextEdit application by + // dispatching backspace events to delete any marked characters. The + // special character input method commits the marked characters so we must + // delete the marked characters before the input method calls + // [self insertText:replacementRange:]. + if (mbTextInputWantsNonRepeatKeyDown) + { + if ( mpLastMarkedText ) + { + NSString *pChars = [mpLastMarkedText string]; + if ( pChars ) + { + NSUInteger nLength = [pChars length]; + for ( NSUInteger i = 0; i < nLength; i++ ) + [self deleteBackward:self]; + } + } + + [self unmarkText]; + } +} + +-(void)insertRegisteredWrapperIntoWrapperRepository +{ + SolarMutexGuard aGuard; + + if (!mbNeedChildWrapper) + return; + + vcl::Window *pWindow = mpFrame->GetWindow(); + if (!pWindow) + return; + + mbNeedChildWrapper = NO; + + ::com::sun::star::uno::Reference< ::com::sun::star::accessibility::XAccessibleContext > xAccessibleContext( pWindow->GetAccessible()->getAccessibleContext() ); + assert(!mpChildWrapper); + mpChildWrapper = [[SalFrameViewA11yWrapper alloc] initWithParent:self accessibleContext:xAccessibleContext]; + [AquaA11yFactory insertIntoWrapperRepository:mpChildWrapper forAccessibleContext:xAccessibleContext]; +} + +-(void)registerWrapper +{ + [self revokeWrapper]; + + mbNeedChildWrapper = YES; +} + +-(void)revokeWrapper +{ + mbNeedChildWrapper = NO; + + if (mpChildWrapper) + { + [AquaA11yFactory revokeWrapper:mpChildWrapper]; + [mpChildWrapper setAccessibilityParent:nil]; + [mpChildWrapper release]; + mpChildWrapper = nil; + } +} + +-(id)accessibilityAttributeValue:(NSString *)pAttribute +{ + SolarMutexGuard aGuard; + + [self insertRegisteredWrapperIntoWrapperRepository]; + if (mpChildWrapper) + return [mpChildWrapper accessibilityAttributeValue:pAttribute]; + else + return nil; +} + +-(BOOL)accessibilityIsIgnored +{ + SolarMutexGuard aGuard; + + [self insertRegisteredWrapperIntoWrapperRepository]; + if (mpChildWrapper) + return [mpChildWrapper accessibilityIsIgnored]; + else + return YES; +} + +-(NSArray *)accessibilityAttributeNames +{ + SolarMutexGuard aGuard; + + [self insertRegisteredWrapperIntoWrapperRepository]; + if (mpChildWrapper) + return [mpChildWrapper accessibilityAttributeNames]; + else + return [NSArray array]; +} + +-(BOOL)accessibilityIsAttributeSettable:(NSString *)pAttribute +{ + SolarMutexGuard aGuard; + + [self insertRegisteredWrapperIntoWrapperRepository]; + if (mpChildWrapper) + return [mpChildWrapper accessibilityIsAttributeSettable:pAttribute]; + else + return NO; +} + +-(NSArray *)accessibilityParameterizedAttributeNames +{ + SolarMutexGuard aGuard; + + [self insertRegisteredWrapperIntoWrapperRepository]; + if (mpChildWrapper) + return [mpChildWrapper accessibilityParameterizedAttributeNames]; + else + return [NSArray array]; +} + +-(BOOL)accessibilitySetOverrideValue:(id)pValue forAttribute:(NSString *)pAttribute +{ + SolarMutexGuard aGuard; + + [self insertRegisteredWrapperIntoWrapperRepository]; + if (mpChildWrapper) + return [mpChildWrapper accessibilitySetOverrideValue:pValue forAttribute:pAttribute]; + else + return NO; +} + +-(void)accessibilitySetValue:(id)pValue forAttribute:(NSString *)pAttribute +{ + SolarMutexGuard aGuard; + + [self insertRegisteredWrapperIntoWrapperRepository]; + if (mpChildWrapper) + [mpChildWrapper accessibilitySetValue:pValue forAttribute:pAttribute]; +} + +-(id)accessibilityAttributeValue:(NSString *)pAttribute forParameter:(id)pParameter +{ + SolarMutexGuard aGuard; + + [self insertRegisteredWrapperIntoWrapperRepository]; + if (mpChildWrapper) + return [mpChildWrapper accessibilityAttributeValue:pAttribute forParameter:pParameter]; + else + return nil; +} + +-(id)accessibilityFocusedUIElement +{ + SolarMutexGuard aGuard; + + [self insertRegisteredWrapperIntoWrapperRepository]; + if (mpChildWrapper) + return [mpChildWrapper accessibilityFocusedUIElement]; + else + return nil; +} + +-(NSString *)accessibilityActionDescription:(NSString *)pAction +{ + SolarMutexGuard aGuard; + + [self insertRegisteredWrapperIntoWrapperRepository]; + if (mpChildWrapper) + return [mpChildWrapper accessibilityActionDescription:pAction]; + else + return nil; +} + +-(void)accessibilityPerformAction:(NSString *)pAction +{ + SolarMutexGuard aGuard; + + [self insertRegisteredWrapperIntoWrapperRepository]; + if (mpChildWrapper) + [mpChildWrapper accessibilityPerformAction:pAction]; +} + +-(NSArray *)accessibilityActionNames +{ + SolarMutexGuard aGuard; + + [self insertRegisteredWrapperIntoWrapperRepository]; + if (mpChildWrapper) + return [mpChildWrapper accessibilityActionNames]; + else + return [NSArray array]; +} + +-(id)accessibilityHitTest:(NSPoint)aPoint +{ + SolarMutexGuard aGuard; + + [self insertRegisteredWrapperIntoWrapperRepository]; + if (mpChildWrapper) + return [mpChildWrapper accessibilityHitTest:aPoint]; + else + return nil; +} + +-(id)accessibilityParent +{ + return [self window]; +} + +-(NSArray *)accessibilityVisibleChildren +{ + return [self accessibilityChildren]; +} + +-(NSArray *)accessibilitySelectedChildren +{ + SolarMutexGuard aGuard; + + NSArray *pRet = [super accessibilityChildren]; + + [self insertRegisteredWrapperIntoWrapperRepository]; + if (mpChildWrapper) + pRet = getMergedAccessibilityChildren(pRet, [mpChildWrapper accessibilitySelectedChildren]); + + return pRet; +} + +-(NSArray *)accessibilityChildren +{ + SolarMutexGuard aGuard; + + NSArray *pRet = [super accessibilityChildren]; + + [self insertRegisteredWrapperIntoWrapperRepository]; + if (mpChildWrapper) + pRet = getMergedAccessibilityChildren(pRet, [mpChildWrapper accessibilityChildren]); + + return pRet; +} + +-(NSArray <id<NSAccessibilityElement>> *)accessibilityChildrenInNavigationOrder +{ + return [self accessibilityChildren]; +} + +@end + +@implementation SalFrameViewA11yWrapper + +-(id)initWithParent:(SalFrameView *)pParentView accessibleContext:(::com::sun::star::uno::Reference< ::com::sun::star::accessibility::XAccessibleContext >&)rxAccessibleContext +{ + [super init]; + + maReferenceWrapper.rAccessibleContext = rxAccessibleContext; + + mpParentView = pParentView; + if (mpParentView) + { + [mpParentView retain]; + [self setAccessibilityParent:mpParentView]; + } + + return self; +} + +-(void)dealloc +{ + if (mpParentView) + [mpParentView release]; + + [super dealloc]; +} + +-(id)parentAttribute +{ + if (mpParentView) + return NSAccessibilityUnignoredAncestor(mpParentView); + else + return nil; +} + +-(void)setAccessibilityParent:(id)pObject +{ + if (mpParentView) + { + [mpParentView release]; + mpParentView = nil; + } + + if (pObject && [pObject isKindOfClass:[SalFrameView class]]) + { + mpParentView = static_cast<SalFrameView *>(pObject); + [mpParentView retain]; + } + + [super setAccessibilityParent:mpParentView]; +} + +-(id)windowAttribute +{ + if (mpParentView) + return [mpParentView window]; + else + return nil; +} + +-(NSWindow *)windowForParent +{ + return [self windowAttribute]; +} + +@end + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ |