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