1 /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
3 * This file is part of the LibreOffice project.
5 * This Source Code Form is subject to the terms of the Mozilla Public
6 * License, v. 2.0. If a copy of the MPL was not distributed with this
7 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
9 * This file incorporates work covered by the following license notice:
11 * Licensed to the Apache Software Foundation (ASF) under one or more
12 * contributor license agreements. See the NOTICE file distributed
13 * with this work for additional information regarding copyright
14 * ownership. The ASF licenses this file to you under the Apache
15 * License, Version 2.0 (the "License"); you may not use this file
16 * except in compliance with the License. You may obtain a copy of
17 * the License at http://www.apache.org/licenses/LICENSE-2.0 .
20 #include <sal/config.h>
21 #include <osl/diagnose.h>
23 #include <vcl/window.hxx>
25 #include <osx/salinst.h>
26 #include <osx/saldata.hxx>
27 #include <osx/salframe.h>
28 #include <osx/salframeview.h>
29 #include <osx/salmenu.h>
30 #include <osx/salnsmenu.h>
32 @implementation SalNSMenu
34 +(BOOL)dispatchSpecialKeyEquivalents: (NSEvent*)pEvent
36 if( pEvent && [pEvent type] == NSEventTypeKeyDown )
38 // Related tdf#126638 and tdf#162010: match against -[NSEvent characters]
39 // When using some non-Western European keyboard layouts, the event's
40 // "characters ignoring modifiers" will be set to the original Unicode
41 // character instead of the resolved key equivalent character so match
42 // against the -[NSEvent characters] instead.
43 NSEventModifierFlags nModMask = ([pEvent modifierFlags] & (NSEventModifierFlagShift|NSEventModifierFlagControl|NSEventModifierFlagOption|NSEventModifierFlagCommand));
44 NSString *pCharacters = [pEvent characters];
45 if( nModMask == NSEventModifierFlagCommand )
47 if( [pCharacters isEqualToString: @"v"] )
49 if( [NSApp sendAction: @selector(paste:) to: nil from: nil] )
52 else if( [pCharacters isEqualToString: @"c"] )
54 if( [NSApp sendAction: @selector(copy:) to: nil from: nil] )
57 else if( [pCharacters isEqualToString: @"x"] )
59 if( [NSApp sendAction: @selector(cut:) to: nil from: nil] )
62 else if( [pCharacters isEqualToString: @"a"] )
64 if( [NSApp sendAction: @selector(selectAll:) to: nil from: nil] )
67 else if( [pCharacters isEqualToString: @"z"] )
69 if( [NSApp sendAction: @selector(undo:) to: nil from: nil] )
73 else if( nModMask == (NSEventModifierFlagCommand|NSEventModifierFlagShift) )
75 if( [pCharacters isEqualToString: @"z"] || [pCharacters isEqualToString: @"Z"] )
77 if( [NSApp sendAction: @selector(redo:) to: nil from: nil] )
86 -(id)initWithMenu: (AquaSalMenu*)pMenu
89 return [super initWithTitle: [NSString string]];
92 -(void)menuNeedsUpdate: (NSMenu*)pMenu
94 SolarMutexGuard aGuard;
98 const AquaSalFrame* pFrame = mpMenu->getFrame();
99 if( pFrame && AquaSalFrame::isAlive( pFrame ) )
101 SalMenuEvent aMenuEvt;
103 aMenuEvt.mpMenu = mpMenu->mpVCLMenu;
104 if( aMenuEvt.mpMenu )
106 pFrame->CallCallback(SalEvent::MenuActivate, &aMenuEvt);
107 pFrame->CallCallback(SalEvent::MenuDeactivate, &aMenuEvt);
110 OSL_FAIL( "unconnected menu" );
112 else if( mpMenu->mpVCLMenu )
114 mpMenu->mpVCLMenu->Activate();
115 mpMenu->mpVCLMenu->Deactivate();
117 // Hide disabled items
118 NSArray* elements = [pMenu itemArray];
119 NSEnumerator* it = [elements objectEnumerator];
121 while ( ( element = [it nextObject] ) != nil )
123 NSMenuItem* item = static_cast< NSMenuItem* >( element );
124 if( ![item isSeparatorItem] )
125 [item setHidden: ![item isEnabled]];
131 -(void)setSalMenu: (AquaSalMenu*)pMenu
137 @implementation SalNSMenuItem
138 -(id)initWithMenuItem: (AquaSalMenuItem*)pMenuItem
140 mpMenuItem = pMenuItem;
141 id ret = [super initWithTitle: [NSString string]
142 action: @selector(menuItemTriggered:)
143 keyEquivalent: [NSString string]];
144 [ret setTarget: self];
145 mbReallyEnabled = [ret isEnabled];
149 -(BOOL)isReallyEnabled
151 return mbReallyEnabled;
154 -(void)menuItemTriggered: (id)aSender
157 SolarMutexGuard aGuard;
159 // Commit uncommitted text before dispatching the selecting menu item. In
160 // certain cases such as selecting the Insert > Comment menu item in a
161 // Writer document while there is uncommitted text will call
162 // AquaSalFrame::EndExtTextInput() which will dispatch a
163 // SalEvent::EndExtTextInput event. Writer's handler for that event will
164 // delete the uncommitted text and then insert the committed text but
165 // LibreOffice will crash when deleting the uncommitted text because
166 // deletion of the text also removes and deletes the newly inserted
168 NSWindow* pKeyWin = [NSApp keyWindow];
169 if( pKeyWin && [pKeyWin isKindOfClass: [SalFrameWindow class]] )
170 [static_cast<SalFrameWindow*>(pKeyWin) endExtTextInput];
172 // tdf#49853 Keyboard shortcuts are also handled by the menu bar, but at least some of them
173 // must still end up in the view. This is necessary to handle common edit actions in docked
174 // windows (e.g. in toolbar fields).
177 // tdf#162010 match based on key equivalent instead of key event
178 // The original fix for tdf#49853 only looked worked in the
179 // case when both a key shortcut was pressed and the resulting
180 // key event was not an input method event. If either of these
181 // conditions weren't true, tdf#49853 would still occur.
182 // Since we know which menu item is being triggered, check if
183 // this menu item's key equivalent has been set to one of the
185 // This change basically expands the fix for tdf#49853 to
186 // include all cases that trigger a menu item, not just simple
188 NSEventModifierFlags nModMask = [self keyEquivalentModifierMask];
189 NSString* pCharacters = [self keyEquivalent];
190 if( nModMask == NSEventModifierFlagCommand &&
191 ( [pCharacters isEqualToString: @"v"] ||
192 [pCharacters isEqualToString: @"c"] ||
193 [pCharacters isEqualToString: @"x"] ||
194 [pCharacters isEqualToString: @"a"] ||
195 [pCharacters isEqualToString: @"z"] ) )
197 NSEvent* pKeyEvent = nil;
198 NSEvent* pEvent = [NSApp currentEvent];
201 switch( [pEvent type] )
203 case NSEventTypeKeyDown:
204 case NSEventTypeKeyUp:
205 case NSEventTypeFlagsChanged:
206 // tdf#162843 replace the event's string parameters
207 // When using the Dvorak - QWERTY keyboard, the
208 // event's charactersIgnoringModifiers string causes
209 // pasting to fail so replace both the event's
210 // characters and charactersIgnoringModifiers strings
211 // with this menu item's key equivalent.
212 pKeyEvent = [NSEvent keyEventWithType: [pEvent type] location: [pEvent locationInWindow] modifierFlags: nModMask timestamp: [pEvent timestamp] windowNumber: [pEvent windowNumber] context: nil characters: pCharacters charactersIgnoringModifiers: pCharacters isARepeat: [pEvent isARepeat] keyCode: [pEvent keyCode]];
221 // Native key events appear to set the location to the
222 // top left corner of the key window
223 NSPoint aPoint = NSMakePoint(0, [pKeyWin frame].size.height);
224 pKeyEvent = [NSEvent keyEventWithType: NSEventTypeKeyDown location: aPoint modifierFlags: nModMask timestamp: [[NSProcessInfo processInfo] systemUptime] windowNumber: [pKeyWin windowNumber] context: nil characters: pCharacters charactersIgnoringModifiers: pCharacters isARepeat: NO keyCode: 0];
227 [[pKeyWin contentView] keyDown: pKeyEvent];
232 const AquaSalFrame* pFrame = mpMenuItem->mpParentMenu ? mpMenuItem->mpParentMenu->getFrame() : nullptr;
233 if( pFrame && AquaSalFrame::isAlive( pFrame ) && ! pFrame->GetWindow()->IsInModalMode() )
235 SalMenuEvent aMenuEvt( mpMenuItem->mnId, mpMenuItem->mpVCLMenu );
236 pFrame->CallCallback(SalEvent::MenuCommand, &aMenuEvt);
238 else if( mpMenuItem->mpVCLMenu )
240 // if an item from submenu was selected. the corresponding Window does not exist because
241 // we use native popup menus, so we have to set the selected menuitem directly
242 // incidentally this of course works for top level popup menus, too
243 PopupMenu * pPopupMenu = dynamic_cast<PopupMenu *>(mpMenuItem->mpVCLMenu.get());
246 // FIXME: revise this ugly code
248 // select handlers in vcl are dispatch on the original menu
249 // if not consumed by the select handler of the current menu
250 // however since only the starting menu ever came into Execute
251 // the hierarchy is not build up. Workaround this by getting
252 // the menu it should have been
254 // get started from hierarchy in vcl menus
255 AquaSalMenu* pParentMenu = mpMenuItem->mpParentMenu;
256 Menu* pCurMenu = mpMenuItem->mpVCLMenu;
257 while( pParentMenu && pParentMenu->mpVCLMenu )
259 pCurMenu = pParentMenu->mpVCLMenu;
260 pParentMenu = pParentMenu->mpParentSalMenu;
263 pPopupMenu->SetSelectedEntry( mpMenuItem->mnId );
264 pPopupMenu->ImplSelectWithStart( pCurMenu );
267 OSL_FAIL( "menubar item without frame !" );
271 -(void)setReallyEnabled: (BOOL)bEnabled
273 mbReallyEnabled = bEnabled;
274 [self setEnabled: mbReallyEnabled];
277 -(BOOL)validateMenuItem: (NSMenuItem *)pMenuItem
279 // Related: tdf#126638 disable all menu items when displaying modal windows
280 // For some unknown reason, key shortcuts are dispatched to the LibreOffice
281 // menu items instead of the modal window so disable all LibreOffice menu
282 // items while a native modal dialog such as the native Open, Save, or
283 // Print dialog is displayed.
284 if (!pMenuItem || [NSApp modalWindow])
287 // Related: tdf#126638 return the last enabled state set by the LibreOffice code
288 // Apparently whatever is returned will be passed to
289 // -[NSMenuItem setEnabled:] which can cause the enabled state
290 // to be different than the enabled state that the LibreOffice
291 // code expects. This results in menu items failing to be
292 // reenabled after being temporarily disabled such as when a
293 // native modal dialog is closed. So, return the last enabled
294 // state set by the LibreOffice code.
295 if ([pMenuItem isKindOfClass: [SalNSMenuItem class]])
296 return [static_cast<SalNSMenuItem*>(pMenuItem) isReallyEnabled];
298 return [pMenuItem isEnabled];
302 @implementation OOStatusItemView
303 -(void)drawRect: (NSRect)aRect
305 NSGraphicsContext* pContext = [NSGraphicsContext currentContext];
306 [pContext saveGraphicsState];
307 SAL_WNODEPRECATED_DECLARATIONS_PUSH
308 // "'drawStatusBarBackgroundInRect:withHighlight:' is deprecated: first deprecated in macOS
309 // 10.14 - Use the standard button instead which handles highlight drawing, making this
311 [SalData::getStatusItem() drawStatusBarBackgroundInRect: aRect withHighlight: NO];
312 SAL_WNODEPRECATED_DECLARATIONS_POP
313 if( AquaSalMenu::pCurrentMenuBar )
315 const std::vector< AquaSalMenu::MenuBarButtonEntry >& rButtons( AquaSalMenu::pCurrentMenuBar->getButtons() );
316 NSRect aFrame = [self frame];
317 NSRect aImgRect = { { 2, 0 }, { 0, 0 } };
318 for( size_t i = 0; i < rButtons.size(); ++i )
320 const Size aPixSize = rButtons[i].maButton.maImage.GetSizePixel();
321 const NSRect aFromRect = { NSZeroPoint, NSMakeSize( aPixSize.Width(), aPixSize.Height()) };
322 aImgRect.origin.y = floor((aFrame.size.height - aFromRect.size.height)/2);
323 aImgRect.size = aFromRect.size;
324 if( rButtons[i].mpNSImage )
325 [rButtons[i].mpNSImage drawInRect: aImgRect fromRect: aFromRect operation: NSCompositingOperationSourceOver fraction: 1.0];
326 aImgRect.origin.x += aFromRect.size.width + 2;
329 [pContext restoreGraphicsState];
332 -(void)mouseUp: (NSEvent *)pEvent
334 /* check if button goes up inside one of our status buttons */
335 if( AquaSalMenu::pCurrentMenuBar )
337 const std::vector< AquaSalMenu::MenuBarButtonEntry >& rButtons( AquaSalMenu::pCurrentMenuBar->getButtons() );
338 NSRect aFrame = [self frame];
339 NSRect aImgRect = { { 2, 0 }, { 0, 0 } };
340 NSPoint aMousePt = [pEvent locationInWindow];
341 for( size_t i = 0; i < rButtons.size(); ++i )
343 const Size aPixSize = rButtons[i].maButton.maImage.GetSizePixel();
344 const NSRect aFromRect = { NSZeroPoint, NSMakeSize( aPixSize.Width(), aPixSize.Height()) };
345 aImgRect.origin.y = (aFrame.size.height - aFromRect.size.height)/2;
346 aImgRect.size = aFromRect.size;
347 if( aMousePt.x >= aImgRect.origin.x && aMousePt.x <= (aImgRect.origin.x+aImgRect.size.width) &&
348 aMousePt.y >= aImgRect.origin.y && aMousePt.y <= (aImgRect.origin.y+aImgRect.size.height) )
350 if( AquaSalMenu::pCurrentMenuBar->mpFrame && AquaSalFrame::isAlive( AquaSalMenu::pCurrentMenuBar->mpFrame ) )
352 SalMenuEvent aMenuEvt( rButtons[i].maButton.mnId, AquaSalMenu::pCurrentMenuBar->mpVCLMenu );
353 AquaSalMenu::pCurrentMenuBar->mpFrame->CallCallback(SalEvent::MenuButtonCommand, &aMenuEvt);
358 aImgRect.origin.x += aFromRect.size.width + 2;
365 NSStatusBar* pStatBar = [NSStatusBar systemStatusBar];
366 NSSize aSize = { 0, [pStatBar thickness] };
367 [self removeAllToolTips];
368 if( AquaSalMenu::pCurrentMenuBar )
370 const std::vector< AquaSalMenu::MenuBarButtonEntry >& rButtons( AquaSalMenu::pCurrentMenuBar->getButtons() );
371 if( ! rButtons.empty() )
374 for( size_t i = 0; i < rButtons.size(); ++i )
376 NSRect aImgRect = { { aSize.width,
377 static_cast<CGFloat>(floor((aSize.height-rButtons[i].maButton.maImage.GetSizePixel().Height())/2)) },
378 { static_cast<CGFloat>(rButtons[i].maButton.maImage.GetSizePixel().Width()),
379 static_cast<CGFloat>(rButtons[i].maButton.maImage.GetSizePixel().Height()) } };
380 if( rButtons[i].mpToolTipString )
381 [self addToolTipRect: aImgRect owner: rButtons[i].mpToolTipString userData: nullptr];
382 aSize.width += 2 + aImgRect.size.width;
386 [self setFrameSize: aSize];
390 @implementation SalNSMainMenu
392 - (id)initWithTitle:(NSString*)pTitle
394 mpLastPerformKeyEquivalentEvent = nil;
395 return [super initWithTitle:pTitle];
400 if (mpLastPerformKeyEquivalentEvent)
401 [mpLastPerformKeyEquivalentEvent release];
406 - (BOOL)performKeyEquivalent:(NSEvent*)pEvent
408 // Related: tdf#162843 prevent dispatch of the same event more than once
409 // When pressing Command-V with a Dvorak - QWERTY keyboard,
410 // that single event passes through this selector twice which
411 // causes content to be pasted twice in any text fields in the
412 // Find and Replace dialog.
413 if (pEvent == mpLastPerformKeyEquivalentEvent)
416 if (mpLastPerformKeyEquivalentEvent)
417 [mpLastPerformKeyEquivalentEvent release];
418 mpLastPerformKeyEquivalentEvent = pEvent;
419 if (mpLastPerformKeyEquivalentEvent)
420 [mpLastPerformKeyEquivalentEvent retain];
422 bool bRet = [super performKeyEquivalent: pEvent];
424 // tdf#126638 dispatch key shortcut events to modal windows
425 // Some modal windows, such as the native Open and Save dialogs,
426 // return NO from -[NSWindow performKeyEquivalent:]. Fortunately,
427 // the main menu's -[NSMenu performKeyEquivalent:] is then called
428 // so we can catch and redirect any modal window's key shortcut
429 // events without triggering the modal window's "disallowed
431 if( !bRet && [NSApp modalWindow] )
432 bRet = [SalNSMenu dispatchSpecialKeyEquivalents: pEvent];
439 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */