tdf#130857 qt weld: Hide widget marked for deletion
[LibreOffice.git] / vcl / osx / salnsmenu.mm
blobaeb3cfdd9f4eafa05d956b0ac1820ad88f2c912c
1 /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
2 /*
3  * This file is part of the LibreOffice project.
4  *
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/.
8  *
9  * This file incorporates work covered by the following license notice:
10  *
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 .
18  */
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 )
37     {
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 )
46         {
47             if( [pCharacters isEqualToString: @"v"] )
48             {
49                 if( [NSApp sendAction: @selector(paste:) to: nil from: nil] )
50                     return YES;
51             }
52             else if( [pCharacters isEqualToString: @"c"] )
53             {
54                 if( [NSApp sendAction: @selector(copy:) to: nil from: nil] )
55                     return YES;
56             }
57             else if( [pCharacters isEqualToString: @"x"] )
58             {
59                 if( [NSApp sendAction: @selector(cut:) to: nil from: nil] )
60                     return YES;
61             }
62             else if( [pCharacters isEqualToString: @"a"] )
63             {
64                 if( [NSApp sendAction: @selector(selectAll:) to: nil from: nil] )
65                     return YES;
66             }
67             else if( [pCharacters isEqualToString: @"z"] )
68             {
69                 if( [NSApp sendAction: @selector(undo:) to: nil from: nil] )
70                     return YES;
71             }
72         }
73         else if( nModMask == (NSEventModifierFlagCommand|NSEventModifierFlagShift) )
74         {
75             if( [pCharacters isEqualToString: @"z"] || [pCharacters isEqualToString: @"Z"] )
76             {
77                 if( [NSApp sendAction: @selector(redo:) to: nil from: nil] )
78                     return YES;
79             }
80         }
81     }
83     return NO;
86 -(id)initWithMenu: (AquaSalMenu*)pMenu
88     mpMenu = pMenu;
89     return [super initWithTitle: [NSString string]];
92 -(void)menuNeedsUpdate: (NSMenu*)pMenu
94     SolarMutexGuard aGuard;
96     if( mpMenu )
97     {
98         const AquaSalFrame* pFrame = mpMenu->getFrame();
99         if( pFrame && AquaSalFrame::isAlive( pFrame ) )
100         {
101             SalMenuEvent aMenuEvt;
102             aMenuEvt.mnId   = 0;
103             aMenuEvt.mpMenu = mpMenu->mpVCLMenu;
104             if( aMenuEvt.mpMenu )
105             {
106                 pFrame->CallCallback(SalEvent::MenuActivate, &aMenuEvt);
107                 pFrame->CallCallback(SalEvent::MenuDeactivate, &aMenuEvt);
108             }
109             else
110                 OSL_FAIL( "unconnected menu" );
111         }
112         else if( mpMenu->mpVCLMenu )
113         {
114             mpMenu->mpVCLMenu->Activate();
115             mpMenu->mpVCLMenu->Deactivate();
117             // Hide disabled items
118             NSArray* elements = [pMenu itemArray];
119             NSEnumerator* it = [elements objectEnumerator];
120             id element;
121             while ( ( element = [it nextObject] ) != nil )
122             {
123                 NSMenuItem* item = static_cast< NSMenuItem* >( element );
124                 if( ![item isSeparatorItem] )
125                     [item setHidden: ![item isEnabled]];
126             }
127         }
128     }
131 -(void)setSalMenu: (AquaSalMenu*)pMenu
133     mpMenu = pMenu;
135 @end
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];
146     return ret;
149 -(BOOL)isReallyEnabled
151     return mbReallyEnabled;
154 -(void)menuItemTriggered: (id)aSender
156     (void)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
167     // comment.
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).
175     if( pKeyWin )
176     {
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
184         // edit actions.
185         // This change basically expands the fix for tdf#49853 to
186         // include all cases that trigger a menu item, not just simple
187         // key events.
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"] ) )
196         {
197             NSEvent* pKeyEvent = nil;
198             NSEvent* pEvent = [NSApp currentEvent];
199             if( pEvent )
200             {
201                 switch( [pEvent type] )
202                 {
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]];
213                         break;
214                     default:
215                         break;
216                 }
217             }
219             if( !pKeyEvent )
220             {
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];
225             }
227             [[pKeyWin contentView] keyDown: pKeyEvent];
228             return;
229         }
230     }
232     const AquaSalFrame* pFrame = mpMenuItem->mpParentMenu ? mpMenuItem->mpParentMenu->getFrame() : nullptr;
233     if( pFrame && AquaSalFrame::isAlive( pFrame ) && ! pFrame->GetWindow()->IsInModalMode() )
234     {
235         SalMenuEvent aMenuEvt( mpMenuItem->mnId, mpMenuItem->mpVCLMenu );
236         pFrame->CallCallback(SalEvent::MenuCommand, &aMenuEvt);
237     }
238     else if( mpMenuItem->mpVCLMenu )
239     {
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());
244         if( pPopupMenu )
245         {
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 )
258             {
259                 pCurMenu = pParentMenu->mpVCLMenu;
260                 pParentMenu = pParentMenu->mpParentSalMenu;
261             }
263             pPopupMenu->SetSelectedEntry( mpMenuItem->mnId );
264             pPopupMenu->ImplSelectWithStart( pCurMenu );
265         }
266         else
267             OSL_FAIL( "menubar item without frame !" );
268     }
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])
285         return NO;
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];
297     else
298         return [pMenuItem isEnabled];
300 @end
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
310         // method obsolete"
311     [SalData::getStatusItem() drawStatusBarBackgroundInRect: aRect withHighlight: NO];
312 SAL_WNODEPRECATED_DECLARATIONS_POP
313     if( AquaSalMenu::pCurrentMenuBar )
314     {
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 )
319         {
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;
327         }
328     }
329     [pContext restoreGraphicsState];
332 -(void)mouseUp: (NSEvent *)pEvent
334     /* check if button goes up inside one of our status buttons */
335     if( AquaSalMenu::pCurrentMenuBar )
336     {
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 )
342         {
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) )
349             {
350                 if( AquaSalMenu::pCurrentMenuBar->mpFrame && AquaSalFrame::isAlive( AquaSalMenu::pCurrentMenuBar->mpFrame ) )
351                 {
352                     SalMenuEvent aMenuEvt( rButtons[i].maButton.mnId, AquaSalMenu::pCurrentMenuBar->mpVCLMenu );
353                     AquaSalMenu::pCurrentMenuBar->mpFrame->CallCallback(SalEvent::MenuButtonCommand, &aMenuEvt);
354                 }
355                 return;
356             }
358             aImgRect.origin.x += aFromRect.size.width + 2;
359         }
360     }
363 -(void)layout
365     NSStatusBar* pStatBar = [NSStatusBar systemStatusBar];
366     NSSize aSize = { 0, [pStatBar thickness] };
367     [self removeAllToolTips];
368     if( AquaSalMenu::pCurrentMenuBar )
369     {
370         const std::vector< AquaSalMenu::MenuBarButtonEntry >& rButtons( AquaSalMenu::pCurrentMenuBar->getButtons() );
371         if( ! rButtons.empty() )
372         {
373             aSize.width = 2;
374             for( size_t i = 0; i < rButtons.size(); ++i )
375             {
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;
383             }
384         }
385     }
386     [self setFrameSize: aSize];
388 @end
390 @implementation SalNSMainMenu
392 - (id)initWithTitle:(NSString*)pTitle
394     mpLastPerformKeyEquivalentEvent = nil;
395     return [super initWithTitle:pTitle];
398 - (void)dealloc
400     if (mpLastPerformKeyEquivalentEvent)
401         [mpLastPerformKeyEquivalentEvent release];
403     [super dealloc];
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)
414         return false;
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
430     // action" beep.
431     if( !bRet && [NSApp modalWindow] )
432         bRet = [SalNSMenu dispatchSpecialKeyEquivalents: pEvent];
434     return bRet;
437 @end
439 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */