1/****************************************************************************
2**
3** Copyright (C) 2016 The Qt Company Ltd.
4** Contact: https://www.qt.io/licensing/
5**
6** This file is part of the plugins of the Qt Toolkit.
7**
8** $QT_BEGIN_LICENSE:LGPL$
9** Commercial License Usage
10** Licensees holding valid commercial Qt licenses may use this file in
11** accordance with the commercial license agreement provided with the
12** Software or, alternatively, in accordance with the terms contained in
13** a written agreement between you and The Qt Company. For licensing terms
14** and conditions see https://www.qt.io/terms-conditions. For further
15** information use the contact form at https://www.qt.io/contact-us.
16**
17** GNU Lesser General Public License Usage
18** Alternatively, this file may be used under the terms of the GNU Lesser
19** General Public License version 3 as published by the Free Software
20** Foundation and appearing in the file LICENSE.LGPL3 included in the
21** packaging of this file. Please review the following information to
22** ensure the GNU Lesser General Public License version 3 requirements
23** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
24**
25** GNU General Public License Usage
26** Alternatively, this file may be used under the terms of the GNU
27** General Public License version 2.0 or (at your option) the GNU General
28** Public license version 3 or any later version approved by the KDE Free
29** Qt Foundation. The licenses are as published by the Free Software
30** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
31** included in the packaging of this file. Please review the following
32** information to ensure the GNU General Public License requirements will
33** be met: https://www.gnu.org/licenses/gpl-2.0.html and
34** https://www.gnu.org/licenses/gpl-3.0.html.
35**
36** $QT_END_LICENSE$
37**
38****************************************************************************/
39
40#include <qpa/qplatformtheme.h>
41
42#include "qcocoahelpers.h"
43#include "qnsview.h"
44
45#include <QtCore>
46#include <QtGui>
47#include <qpa/qplatformscreen.h>
48#include <private/qguiapplication_p.h>
49#include <private/qwindow_p.h>
50#include <QtGui/private/qcoregraphics_p.h>
51
52#ifndef QT_NO_WIDGETS
53#include <QtWidgets/QWidget>
54#endif
55
56#include <algorithm>
57
58QT_BEGIN_NAMESPACE
59
60Q_LOGGING_CATEGORY(lcQpaWindow, "qt.qpa.window");
61Q_LOGGING_CATEGORY(lcQpaDrawing, "qt.qpa.drawing");
62Q_LOGGING_CATEGORY(lcQpaMouse, "qt.qpa.input.mouse", QtCriticalMsg);
63Q_LOGGING_CATEGORY(lcQpaScreen, "qt.qpa.screen", QtCriticalMsg);
64
65//
66// Conversion Functions
67//
68
69QStringList qt_mac_NSArrayToQStringList(NSArray<NSString *> *array)
70{
71    QStringList result;
72    for (NSString *string in array)
73        result << QString::fromNSString(string);
74    return result;
75}
76
77NSMutableArray<NSString *> *qt_mac_QStringListToNSMutableArray(const QStringList &list)
78{
79    NSMutableArray<NSString *> *result = [NSMutableArray<NSString *> arrayWithCapacity:list.size()];
80    for (const QString &string : list)
81        [result addObject:string.toNSString()];
82    return result;
83}
84
85struct dndenum_mapper
86{
87    NSDragOperation mac_code;
88    Qt::DropAction qt_code;
89    bool Qt2Mac;
90};
91
92static dndenum_mapper dnd_enums[] = {
93    { NSDragOperationLink,  Qt::LinkAction, true },
94    { NSDragOperationMove,  Qt::MoveAction, true },
95    { NSDragOperationDelete,  Qt::MoveAction, true },
96    { NSDragOperationCopy,  Qt::CopyAction, true },
97    { NSDragOperationGeneric,  Qt::CopyAction, false },
98    { NSDragOperationEvery, Qt::ActionMask, false },
99    { NSDragOperationNone, Qt::IgnoreAction, false }
100};
101
102NSDragOperation qt_mac_mapDropAction(Qt::DropAction action)
103{
104    for (int i=0; dnd_enums[i].qt_code; i++) {
105        if (dnd_enums[i].Qt2Mac && (action & dnd_enums[i].qt_code)) {
106            return dnd_enums[i].mac_code;
107        }
108    }
109    return NSDragOperationNone;
110}
111
112NSDragOperation qt_mac_mapDropActions(Qt::DropActions actions)
113{
114    NSDragOperation nsActions = NSDragOperationNone;
115    for (int i=0; dnd_enums[i].qt_code; i++) {
116        if (dnd_enums[i].Qt2Mac && (actions & dnd_enums[i].qt_code))
117            nsActions |= dnd_enums[i].mac_code;
118    }
119    return nsActions;
120}
121
122Qt::DropAction qt_mac_mapNSDragOperation(NSDragOperation nsActions)
123{
124    Qt::DropAction action = Qt::IgnoreAction;
125    for (int i=0; dnd_enums[i].mac_code; i++) {
126        if (nsActions & dnd_enums[i].mac_code)
127            return dnd_enums[i].qt_code;
128    }
129    return action;
130}
131
132Qt::DropActions qt_mac_mapNSDragOperations(NSDragOperation nsActions)
133{
134    Qt::DropActions actions = Qt::IgnoreAction;
135
136    for (int i=0; dnd_enums[i].mac_code; i++) {
137        if (dnd_enums[i].mac_code == NSDragOperationEvery)
138            continue;
139
140        if (nsActions & dnd_enums[i].mac_code)
141            actions |= dnd_enums[i].qt_code;
142    }
143    return actions;
144}
145
146/*!
147    Returns the view cast to a QNSview if possible.
148
149    If the view is not a QNSView, nil is returned, which is safe to
150    send messages to, effectivly making [qnsview_cast(view) message]
151    a no-op.
152
153    For extra verbosity and clearer code, please consider checking
154    that the platform window is not a foreign window before using
155    this cast, via QPlatformWindow::isForeignWindow().
156
157    Do not use this method soley to check for foreign windows, as
158    that will make the code harder to read for people not working
159    primarily on macOS, who do not know the difference between the
160    NSView and QNSView cases.
161*/
162QNSView *qnsview_cast(NSView *view)
163{
164    return qt_objc_cast<QNSView *>(view);
165}
166
167//
168// Misc
169//
170
171// Sets the activation policy for this process to NSApplicationActivationPolicyRegular,
172// unless either LSUIElement or LSBackgroundOnly is set in the Info.plist.
173void qt_mac_transformProccessToForegroundApplication()
174{
175    bool forceTransform = true;
176    CFTypeRef value = CFBundleGetValueForInfoDictionaryKey(CFBundleGetMainBundle(),
177                                                           CFSTR("LSUIElement"));
178    if (value) {
179        CFTypeID valueType = CFGetTypeID(value);
180        // Officially it's supposed to be a string, a boolean makes sense, so we'll check.
181        // A number less so, but OK.
182        if (valueType == CFStringGetTypeID())
183            forceTransform = !(QString::fromCFString(static_cast<CFStringRef>(value)).toInt());
184        else if (valueType == CFBooleanGetTypeID())
185            forceTransform = !CFBooleanGetValue(static_cast<CFBooleanRef>(value));
186        else if (valueType == CFNumberGetTypeID()) {
187            int valueAsInt;
188            CFNumberGetValue(static_cast<CFNumberRef>(value), kCFNumberIntType, &valueAsInt);
189            forceTransform = !valueAsInt;
190        }
191    }
192
193    if (forceTransform) {
194        value = CFBundleGetValueForInfoDictionaryKey(CFBundleGetMainBundle(),
195                                                     CFSTR("LSBackgroundOnly"));
196        if (value) {
197            CFTypeID valueType = CFGetTypeID(value);
198            if (valueType == CFBooleanGetTypeID())
199                forceTransform = !CFBooleanGetValue(static_cast<CFBooleanRef>(value));
200            else if (valueType == CFStringGetTypeID())
201                forceTransform = !(QString::fromCFString(static_cast<CFStringRef>(value)).toInt());
202            else if (valueType == CFNumberGetTypeID()) {
203                int valueAsInt;
204                CFNumberGetValue(static_cast<CFNumberRef>(value), kCFNumberIntType, &valueAsInt);
205                forceTransform = !valueAsInt;
206            }
207        }
208    }
209
210    if (forceTransform) {
211        [[NSApplication sharedApplication] setActivationPolicy:NSApplicationActivationPolicyRegular];
212    }
213}
214
215QString qt_mac_applicationName()
216{
217    QString appName;
218    CFTypeRef string = CFBundleGetValueForInfoDictionaryKey(CFBundleGetMainBundle(), CFSTR("CFBundleName"));
219    if (string)
220        appName = QString::fromCFString(static_cast<CFStringRef>(string));
221
222    if (appName.isEmpty()) {
223        QString arg0 = QGuiApplicationPrivate::instance()->appName();
224        if (arg0.contains("/")) {
225            QStringList parts = arg0.split(QLatin1Char('/'));
226            appName = parts.at(parts.count() - 1);
227        } else {
228            appName = arg0;
229        }
230    }
231    return appName;
232}
233
234// -------------------------------------------------------------------------
235
236/*!
237    \fn QPointF qt_mac_flip(const QPointF &pos, const QRectF &reference)
238    \fn QRectF qt_mac_flip(const QRectF &rect, const QRectF &reference)
239
240    Flips the Y coordinate of the point/rect between quadrant I and IV.
241
242    The native coordinate system on macOS uses quadrant I, with origin
243    in bottom left, and Qt uses quadrant IV, with origin in top left.
244
245    By flipping the Y coordinate, we can map the point/rect between
246    the two coordinate systems.
247
248    The flip is always in relation to a reference rectangle, e.g.
249    the frame of the parent view, or the screen geometry. In the
250    latter case the specialized QCocoaScreen::mapFrom/To functions
251    should be used instead.
252*/
253QPointF qt_mac_flip(const QPointF &pos, const QRectF &reference)
254{
255    return QPointF(pos.x(), reference.height() - pos.y());
256}
257
258QRectF qt_mac_flip(const QRectF &rect, const QRectF &reference)
259{
260    return QRectF(qt_mac_flip(rect.bottomLeft(), reference), rect.size());
261}
262
263// -------------------------------------------------------------------------
264
265/*!
266  \fn Qt::MouseButton cocoaButton2QtButton(NSInteger buttonNum)
267
268  Returns the Qt::Button that corresponds to an NSEvent.buttonNumber.
269
270  \note AppKit will use buttonNumber 0 to indicate both "left button"
271  and "no button". Only NSEvents that describes mouse press/release
272  events (e.g NSEventTypeOtherMouseDown) will contain a valid
273  button number.
274*/
275Qt::MouseButton cocoaButton2QtButton(NSInteger buttonNum)
276{
277    if (buttonNum >= 0 && buttonNum <= 31)
278        return Qt::MouseButton(1 << buttonNum);
279    return Qt::NoButton;
280}
281
282/*!
283  \fn Qt::MouseButton cocoaButton2QtButton(NSEvent *event)
284
285  Returns the Qt::Button that corresponds to an NSEvent.buttonNumber.
286
287  \note AppKit will use buttonNumber 0 to indicate both "left button"
288  and "no button". Only NSEvents that describes mouse press/release/dragging
289  events (e.g NSEventTypeOtherMouseDown) will contain a valid
290  button number.
291
292  \note Wacom tablet might not return the correct button number for NSEvent buttonNumber
293  on right clicks. Decide here that the button is the "right" button.
294*/
295Qt::MouseButton cocoaButton2QtButton(NSEvent *event)
296{
297    if (cocoaEvent2QtMouseEvent(event) == QEvent::MouseMove)
298        return Qt::NoButton;
299
300    switch (event.type) {
301    case NSEventTypeRightMouseUp:
302    case NSEventTypeRightMouseDown:
303        return Qt::RightButton;
304
305    default:
306        break;
307    }
308
309    return cocoaButton2QtButton(event.buttonNumber);
310}
311
312/*!
313  \fn QEvent::Type cocoaEvent2QtMouseEvent(NSEvent *event)
314
315  Returns the QEvent::Type that corresponds to an NSEvent.type.
316*/
317QEvent::Type cocoaEvent2QtMouseEvent(NSEvent *event)
318{
319    switch (event.type) {
320    case NSEventTypeLeftMouseDown:
321    case NSEventTypeRightMouseDown:
322    case NSEventTypeOtherMouseDown:
323        return QEvent::MouseButtonPress;
324
325    case NSEventTypeLeftMouseUp:
326    case NSEventTypeRightMouseUp:
327    case NSEventTypeOtherMouseUp:
328        return QEvent::MouseButtonRelease;
329
330    case NSEventTypeLeftMouseDragged:
331    case NSEventTypeRightMouseDragged:
332    case NSEventTypeOtherMouseDragged:
333        return QEvent::MouseMove;
334
335    case NSEventTypeMouseMoved:
336        return QEvent::MouseMove;
337
338    default:
339        break;
340    }
341
342    return QEvent::None;
343}
344
345/*!
346  \fn Qt::MouseButtons cocoaMouseButtons2QtMouseButtons(NSInteger pressedMouseButtons)
347
348  Returns the Qt::MouseButtons that corresponds to an NSEvent.pressedMouseButtons.
349*/
350Qt::MouseButtons cocoaMouseButtons2QtMouseButtons(NSInteger pressedMouseButtons)
351{
352  return static_cast<Qt::MouseButton>(pressedMouseButtons & Qt::MouseButtonMask);
353}
354
355/*!
356  \fn Qt::MouseButtons currentlyPressedMouseButtons()
357
358  Returns the Qt::MouseButtons that corresponds to an NSEvent.pressedMouseButtons.
359*/
360Qt::MouseButtons currentlyPressedMouseButtons()
361{
362  return cocoaMouseButtons2QtMouseButtons(NSEvent.pressedMouseButtons);
363}
364
365QString qt_mac_removeAmpersandEscapes(QString s)
366{
367    return QPlatformTheme::removeMnemonics(s).trimmed();
368}
369
370QT_END_NAMESPACE
371
372/*! \internal
373
374    This NSView derived class is used to add OK/Cancel
375    buttons to NSColorPanel and NSFontPanel. It replaces
376    the panel's content view, while reparenting the former
377    content view into itself. It also takes care of setting
378    the target-action for the OK/Cancel buttons and making
379    sure the layout is consistent.
380 */
381@implementation QNSPanelContentsWrapper {
382    NSButton *_okButton;
383    NSButton *_cancelButton;
384    NSView *_panelContents;
385    NSEdgeInsets _panelContentsMargins;
386}
387
388@synthesize okButton = _okButton;
389@synthesize cancelButton = _cancelButton;
390@synthesize panelContents = _panelContents;
391@synthesize panelContentsMargins = _panelContentsMargins;
392
393- (instancetype)initWithPanelDelegate:(id<QNSPanelDelegate>)panelDelegate
394{
395    if ((self = [super initWithFrame:NSZeroRect])) {
396        // create OK and Cancel buttons and add these as subviews
397        _okButton = [self createButtonWithTitle:QPlatformDialogHelper::Ok];
398        _okButton.action = @selector(onOkClicked);
399        _okButton.target = panelDelegate;
400        _cancelButton = [self createButtonWithTitle:QPlatformDialogHelper::Cancel];
401        _cancelButton.action = @selector(onCancelClicked);
402        _cancelButton.target = panelDelegate;
403
404        _panelContents = nil;
405
406        _panelContentsMargins = NSEdgeInsetsMake(0, 0, 0, 0);
407    }
408
409    return self;
410}
411
412- (void)dealloc
413{
414    [_okButton release];
415    _okButton = nil;
416    [_cancelButton release];
417    _cancelButton = nil;
418
419    _panelContents = nil;
420
421    [super dealloc];
422}
423
424- (NSButton *)createButtonWithTitle:(QPlatformDialogHelper::StandardButton)type
425{
426    NSButton *button = [[NSButton alloc] initWithFrame:NSZeroRect];
427    button.buttonType = NSMomentaryLightButton;
428    button.bezelStyle = NSRoundedBezelStyle;
429    const QString &cleanTitle =
430         QPlatformTheme::removeMnemonics(QGuiApplicationPrivate::platformTheme()->standardButtonText(type));
431    // FIXME: Not obvious, from Cocoa's documentation, that QString::toNSString() makes a deep copy
432    button.title = (NSString *)cleanTitle.toCFString();
433    ((NSButtonCell *)button.cell).font =
434            [NSFont systemFontOfSize:[NSFont systemFontSizeForControlSize:NSControlSizeRegular]];
435    [self addSubview:button];
436    return button;
437}
438
439- (void)layout
440{
441    static const CGFloat ButtonMinWidth = 78.0; // 84.0 for Carbon
442    static const CGFloat ButtonMinHeight = 32.0;
443    static const CGFloat ButtonSpacing = 0.0;
444    static const CGFloat ButtonTopMargin = 0.0;
445    static const CGFloat ButtonBottomMargin = 7.0;
446    static const CGFloat ButtonSideMargin = 9.0;
447
448    NSSize frameSize = self.frame.size;
449
450    [self.okButton sizeToFit];
451    NSSize okSizeHint = self.okButton.frame.size;
452
453    [self.cancelButton sizeToFit];
454    NSSize cancelSizeHint = self.cancelButton.frame.size;
455
456    const CGFloat buttonWidth = qMin(qMax(ButtonMinWidth,
457                                          qMax(okSizeHint.width, cancelSizeHint.width)),
458                                     CGFloat((frameSize.width - 2.0 * ButtonSideMargin - ButtonSpacing) * 0.5));
459    const CGFloat buttonHeight = qMax(ButtonMinHeight,
460                                     qMax(okSizeHint.height, cancelSizeHint.height));
461
462    NSRect okRect = { { frameSize.width - ButtonSideMargin - buttonWidth,
463                        ButtonBottomMargin },
464                      { buttonWidth, buttonHeight } };
465    self.okButton.frame = okRect;
466    self.okButton.needsDisplay = YES;
467
468    NSRect cancelRect = { { okRect.origin.x - ButtonSpacing - buttonWidth,
469                            ButtonBottomMargin },
470                            { buttonWidth, buttonHeight } };
471    self.cancelButton.frame = cancelRect;
472    self.cancelButton.needsDisplay = YES;
473
474    // The third view should be the original panel contents. Cache it.
475    if (!self.panelContents)
476        for (NSView *view in self.subviews)
477            if (view != self.okButton && view != self.cancelButton) {
478                _panelContents = view;
479                break;
480            }
481
482    const CGFloat buttonBoxHeight = ButtonBottomMargin + buttonHeight + ButtonTopMargin;
483    const NSRect panelContentsFrame = NSMakeRect(
484                self.panelContentsMargins.left,
485                buttonBoxHeight + self.panelContentsMargins.bottom,
486                frameSize.width - (self.panelContentsMargins.left + self.panelContentsMargins.right),
487                frameSize.height - buttonBoxHeight - (self.panelContentsMargins.top + self.panelContentsMargins.bottom));
488    self.panelContents.frame = panelContentsFrame;
489    self.panelContents.needsDisplay = YES;
490
491    self.needsDisplay = YES;
492    [super layout];
493}
494
495// -------------------------------------------------------------------------
496
497io_object_t q_IOObjectRetain(io_object_t obj)
498{
499    kern_return_t ret = IOObjectRetain(obj);
500    Q_ASSERT(!ret);
501    return obj;
502}
503
504void q_IOObjectRelease(io_object_t obj)
505{
506    kern_return_t ret = IOObjectRelease(obj);
507    Q_ASSERT(!ret);
508}
509
510@end
511