Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qcocoacolordialoghelper.mm
Go to the documentation of this file.
1// Copyright (C) 2016 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3// Qt-Security score:significant reason:default
4
5#include <AppKit/AppKit.h>
6
7#include <QtCore/qdebug.h>
8#include <QtCore/qtimer.h>
9#include <qpa/qplatformtheme.h>
10
12#include "qcocoahelpers.h"
14#include "private/qcoregraphics_p.h"
15
16QT_USE_NAMESPACE
17
18@interface QT_MANGLE_NAMESPACE(QNSColorPanelDelegate) : NSObject<NSWindowDelegate, QNSPanelDelegate>
19- (void)restoreOriginalContentView;
20- (void)updateQtColor;
21- (void)finishOffWithCode:(NSInteger)code;
22@end
23
24QT_NAMESPACE_ALIAS_OBJC_CLASS(QNSColorPanelDelegate);
25
26@implementation QNSColorPanelDelegate {
27 @public
28 NSColorPanel *mColorPanel;
29 QCocoaColorDialogHelper *mHelper;
30 NSView *mStolenContentView;
31 QNSPanelContentsWrapper *mPanelButtons;
32 QColor mQtColor;
33 NSInteger mResultCode;
34 BOOL mDialogIsExecuting;
35 BOOL mResultSet;
36 BOOL mClosingDueToKnownButton;
37}
38
39- (instancetype)init
40{
41 self = [super init];
42 mColorPanel = [NSColorPanel sharedColorPanel];
43 mHelper = nullptr;
44 mStolenContentView = nil;
45 mPanelButtons = nil;
46 mResultCode = NSModalResponseCancel;
47 mDialogIsExecuting = false;
48 mResultSet = false;
49 mClosingDueToKnownButton = false;
50
51 [mColorPanel setRestorable:NO];
52
53 [[NSNotificationCenter defaultCenter] addObserver:self
54 selector:@selector(colorChanged:)
55 name:NSColorPanelColorDidChangeNotification
56 object:mColorPanel];
57
58 [[NSNotificationCenter defaultCenter] addObserver:self
59 selector:@selector(windowWillClose:)
60 name:NSWindowWillCloseNotification
61 object:mColorPanel];
62
63 [mColorPanel retain];
64 return self;
65}
66
67- (void)dealloc
68{
69 [mStolenContentView release];
70 [mColorPanel setDelegate:nil];
71 [[NSNotificationCenter defaultCenter] removeObserver:self];
72
73 [super dealloc];
74}
75
76- (void)setDialogHelper:(QCocoaColorDialogHelper *)helper
77{
78 mHelper = helper;
79
80 if (mHelper->options()->testOption(QColorDialogOptions::NoButtons)) {
81 [self restoreOriginalContentView];
82 } else if (!mStolenContentView) {
83 // steal the color panel's contents view
84 mStolenContentView = mColorPanel.contentView;
85 [mStolenContentView retain];
86 mColorPanel.contentView = nil;
87
88 // create a new content view and add the stolen one as a subview
89 mPanelButtons = [[QNSPanelContentsWrapper alloc] initWithPanelDelegate:self];
90 [mPanelButtons addSubview:mStolenContentView];
91 mColorPanel.contentView = mPanelButtons;
92 mColorPanel.defaultButtonCell = mPanelButtons.okButton.cell;
93 }
94}
95
96- (void)closePanel
97{
98 [mColorPanel close];
99}
100
101- (void)colorChanged:(NSNotification *)notification
102{
103 Q_UNUSED(notification);
104 [self updateQtColor];
105}
106
107- (void)windowWillClose:(NSNotification *)notification
108{
109 Q_UNUSED(notification);
110 if (mPanelButtons && mHelper && !mClosingDueToKnownButton) {
111 mClosingDueToKnownButton = true; // prevent repeating emit
112 emit mHelper->reject();
113 }
114}
115
116- (void)restoreOriginalContentView
117{
118 if (mStolenContentView) {
119 // return stolen stuff to its rightful owner
120 [mStolenContentView removeFromSuperview];
121 [mColorPanel setContentView:mStolenContentView];
122 [mStolenContentView release];
123 mStolenContentView = nil;
124 [mPanelButtons release];
125 mPanelButtons = nil;
126 }
127}
128
129- (void)onOkClicked
130{
131 mClosingDueToKnownButton = true;
132 [mColorPanel close];
133 [self updateQtColor];
134 [self finishOffWithCode:NSModalResponseOK];
135}
136
137- (void)onCancelClicked
138{
139 if (mPanelButtons) {
140 mClosingDueToKnownButton = true;
141 [mColorPanel close];
142 mQtColor = QColor();
143 [self finishOffWithCode:NSModalResponseCancel];
144 }
145}
146
147- (void)updateQtColor
148{
149 // Discard the color space and pass the color components to QColor. This
150 // is a good option as long as QColor is color-unmanaged: we preserve the
151 // exact RGB value from the color picker, which is predictable. Further,
152 // painting with the color will reproduce the same color on-screen, as
153 // long as the the same screen is used for selecting the color.
154 NSColor *componentColor = [[mColorPanel color] colorUsingType:NSColorTypeComponentBased];
155 switch (componentColor.colorSpace.colorSpaceModel)
156 {
157 case NSColorSpaceModelGray: {
158 CGFloat white = 0, alpha = 0;
159 [componentColor getWhite:&white alpha:&alpha];
160 mQtColor.setRgbF(white, white, white, alpha);
161 } break;
162 case NSColorSpaceModelRGB: {
163 CGFloat red = 0, green = 0, blue = 0, alpha = 0;
164 [componentColor getRed:&red green:&green blue:&blue alpha:&alpha];
165 mQtColor.setRgbF(red, green, blue, alpha);
166 } break;
167 case NSColorSpaceModelCMYK: {
168 CGFloat cyan = 0, magenta = 0, yellow = 0, black = 0, alpha = 0;
169 [componentColor getCyan:&cyan magenta:&magenta yellow:&yellow black:&black alpha:&alpha];
170 mQtColor.setCmykF(cyan, magenta, yellow, black, alpha);
171 } break;
172 default:
173 qWarning("QNSColorPanelDelegate: Unsupported color space model");
174 break;
175 }
176
177 if (mHelper)
178 emit mHelper->currentColorChanged(mQtColor);
179}
180
181- (void)showModelessPanel
182{
183 mDialogIsExecuting = false;
184 mResultSet = false;
185 mClosingDueToKnownButton = false;
186 // Make this an asynchronous call, so the panel is made key only
187 // in the next event loop run. This is to make sure that by
188 // the time the modal loop is run in runModalForWindow below,
189 // which internally also sets the panel to key window,
190 // the panel is not yet key, and the NSApp still has the right
191 // reference to the _previousKeyWindow. Otherwise both NSApp.key
192 // and NSApp._previousKeyWindow would wrongly point to the panel,
193 // loosing any reference to the window that was key before.
194 dispatch_async(dispatch_get_main_queue(), ^{
195 [mColorPanel makeKeyAndOrderFront:mColorPanel];
196 });
197}
198
199- (BOOL)runApplicationModalPanel
200{
201 mDialogIsExecuting = true;
202 [mColorPanel setDelegate:self];
203 [mColorPanel setContinuous:YES];
204 // Call processEvents in case the event dispatcher has been interrupted, and needs to do
205 // cleanup of modal sessions. Do this before showing the native dialog, otherwise it will
206 // close down during the cleanup.
207 qApp->processEvents(QEventLoop::ExcludeUserInputEvents | QEventLoop::ExcludeSocketNotifiers);
208
209 // Make sure we don't interrupt the runModalForWindow call.
210 QCocoaEventDispatcher::clearCurrentThreadCocoaEventDispatcherInterruptFlag();
211
212 [NSApp runModalForWindow:mColorPanel];
213 mDialogIsExecuting = false;
214
215 // Wake up the event dispatcher so it can check whether the
216 // current event loop should continue spinning or not.
217 QCoreApplication::eventDispatcher()->wakeUp();
218
219 return (mResultCode == NSModalResponseOK);
220}
221
222- (QPlatformDialogHelper::DialogCode)dialogResultCode
223{
224 return (mResultCode == NSModalResponseOK) ? QPlatformDialogHelper::Accepted : QPlatformDialogHelper::Rejected;
225}
226
227- (BOOL)windowShouldClose:(id)window
228{
229 Q_UNUSED(window);
230 if (!mPanelButtons)
231 [self updateQtColor];
232 if (mDialogIsExecuting) {
233 [self finishOffWithCode:NSModalResponseCancel];
234 } else {
235 mResultSet = true;
236 if (mHelper)
237 emit mHelper->reject();
238 }
239 return true;
240}
241
242- (void)finishOffWithCode:(NSInteger)code
243{
244 mResultCode = code;
245 if (mDialogIsExecuting) {
246 // We stop the current modal event loop. The control
247 // will then return inside -(void)exec below.
248 // It's important that the modal event loop is stopped before
249 // we accept/reject QColorDialog, since QColorDialog has its
250 // own event loop that needs to be stopped last.
251 [NSApp stopModalWithCode:code];
252 } else {
253 // Since we are not in a modal event loop, we can safely close
254 // down QColorDialog
255 // Calling accept() or reject() can in turn call closeCocoaColorPanel.
256 // This check will prevent any such recursion.
257 if (!mResultSet) {
258 mResultSet = true;
259 if (mResultCode == NSModalResponseCancel) {
260 emit mHelper->reject();
261 } else {
262 emit mHelper->accept();
263 }
264 }
265 }
266}
267
268@end
269
270QT_BEGIN_NAMESPACE
271
272class QCocoaColorPanel
273{
274public:
275 QCocoaColorPanel()
276 {
277 mDelegate = [[QNSColorPanelDelegate alloc] init];
278 }
279
280 ~QCocoaColorPanel()
281 {
282 [mDelegate release];
283 }
284
285 void init(QCocoaColorDialogHelper *helper)
286 {
287 [mDelegate setDialogHelper:helper];
288 }
289
290 void cleanup(QCocoaColorDialogHelper *helper)
291 {
292 if (mDelegate->mHelper == helper)
293 mDelegate->mHelper = nullptr;
294 }
295
296 bool exec()
297 {
298 // Note: If NSApp is not running (which is the case if e.g a top-most
299 // QEventLoop has been interrupted, and the second-most event loop has not
300 // yet been reactivated (regardless if [NSApp run] is still on the stack)),
301 // showing a native modal dialog will fail.
302 return [mDelegate runApplicationModalPanel];
303 }
304
305 bool show(Qt::WindowModality windowModality, QWindow *parent)
306 {
307 Q_UNUSED(parent);
308 if (windowModality != Qt::ApplicationModal)
309 [mDelegate showModelessPanel];
310 // no need to show a Qt::ApplicationModal dialog here, because it will be shown in runApplicationModalPanel
311 return true;
312 }
313
314 void hide()
315 {
316 [mDelegate closePanel];
317 }
318
319 QColor currentColor() const
320 {
321 return mDelegate->mQtColor;
322 }
323
324 void setCurrentColor(const QColor &color)
325 {
326 // make sure that if ShowAlphaChannel option is set then also setShowsAlpha
327 // needs to be set, otherwise alpha value is omitted
328 if (color.alpha() < 255)
329 [mDelegate->mColorPanel setShowsAlpha:YES];
330
331 NSColor *nsColor;
332 const QColor::Spec spec = color.spec();
333 if (spec == QColor::Cmyk) {
334 nsColor = [NSColor colorWithDeviceCyan:color.cyanF()
335 magenta:color.magentaF()
336 yellow:color.yellowF()
337 black:color.blackF()
338 alpha:color.alphaF()];
339 } else {
340 nsColor = [NSColor colorWithCalibratedRed:color.redF()
341 green:color.greenF()
342 blue:color.blueF()
343 alpha:color.alphaF()];
344 }
345 mDelegate->mQtColor = color;
346 [mDelegate->mColorPanel setColor:nsColor];
347 }
348
349private:
350 QNSColorPanelDelegate *mDelegate;
351};
352
354
355QCocoaColorDialogHelper::QCocoaColorDialogHelper()
356{
357}
358
359QCocoaColorDialogHelper::~QCocoaColorDialogHelper()
360{
361 sharedColorPanel()->cleanup(this);
362}
363
364void QCocoaColorDialogHelper::exec()
365{
366 if (sharedColorPanel()->exec())
367 emit accept();
368 else
369 emit reject();
370}
371
372bool QCocoaColorDialogHelper::show(Qt::WindowFlags, Qt::WindowModality windowModality, QWindow *parent)
373{
374 if (windowModality == Qt::ApplicationModal)
375 windowModality = Qt::WindowModal;
376 // Workaround for Apple rdar://25792119: If you invoke
377 // -setShowsAlpha: multiple times before showing the color
378 // picker, its height grows irrevocably. Instead, only
379 // invoke it once, when we show the dialog.
380 [[NSColorPanel sharedColorPanel] setShowsAlpha:
381 options()->testOption(QColorDialogOptions::ShowAlphaChannel)];
382
383 sharedColorPanel()->init(this);
384 return sharedColorPanel()->show(windowModality, parent);
385}
386
387void QCocoaColorDialogHelper::hide()
388{
389 sharedColorPanel()->hide();
390}
391
392void QCocoaColorDialogHelper::setCurrentColor(const QColor &color)
393{
394 sharedColorPanel()->init(this);
395 sharedColorPanel()->setCurrentColor(color);
396}
397
398QColor QCocoaColorDialogHelper::currentColor() const
399{
400 return sharedColorPanel()->currentColor();
401}
402
403QT_END_NAMESPACE
Q_GLOBAL_STATIC(QReadWriteLock, g_updateMutex)