1/****************************************************************************
2**
3** Copyright (C) 2015 The Qt Company Ltd.
4** Contact: http://www.qt.io/licensing/
5**
6** This file is part of the QtGui module 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 http://www.qt.io/terms-conditions. For further
15** information use the contact form at http://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 2.1 or version 3 as published by the Free
20** Software Foundation and appearing in the file LICENSE.LGPLv21 and
21** LICENSE.LGPLv3 included in the packaging of this file. Please review the
22** following information to ensure the GNU Lesser General Public License
23** requirements will be met: https://www.gnu.org/licenses/lgpl.html and
24** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
25**
26** As a special exception, The Qt Company gives you certain additional
27** rights. These rights are described in The Qt Company LGPL Exception
28** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
29**
30** GNU General Public License Usage
31** Alternatively, this file may be used under the terms of the GNU
32** General Public License version 3.0 as published by the Free Software
33** Foundation and appearing in the file LICENSE.GPL included in the
34** packaging of this file.  Please review the following information to
35** ensure the GNU General Public License version 3.0 requirements will be
36** met: http://www.gnu.org/copyleft/gpl.html.
37**
38** $QT_END_LICENSE$
39**
40****************************************************************************/
41
42#include "qcolordialog_p.h"
43#if !defined(QT_NO_COLORDIALOG) && defined(Q_WS_MAC)
44#include <qapplication.h>
45#include <qtimer.h>
46#include <qdialogbuttonbox.h>
47#include <qabstracteventdispatcher.h>
48#include <private/qapplication_p.h>
49#include <private/qt_mac_p.h>
50#include <qdebug.h>
51#import <AppKit/AppKit.h>
52#import <Foundation/Foundation.h>
53
54#if !CGFLOAT_DEFINED
55typedef float CGFloat;  // Should only not be defined on 32-bit platforms
56#endif
57
58
59#if MAC_OS_X_VERSION_MAX_ALLOWED <= MAC_OS_X_VERSION_10_5
60@protocol NSWindowDelegate <NSObject>
61- (void)windowDidResize:(NSNotification *)notification;
62- (BOOL)windowShouldClose:(id)window;
63@end
64#endif
65
66QT_USE_NAMESPACE
67
68@class QT_MANGLE_NAMESPACE(QCocoaColorPanelDelegate);
69
70@interface QT_MANGLE_NAMESPACE(QCocoaColorPanelDelegate) : NSObject<NSWindowDelegate> {
71    NSColorPanel *mColorPanel;
72    NSView *mStolenContentView;
73    NSButton *mOkButton;
74    NSButton *mCancelButton;
75    QColorDialogPrivate *mPriv;
76    QColor *mQtColor;
77    CGFloat mMinWidth;  // currently unused
78    CGFloat mExtraHeight;   // currently unused
79    BOOL mHackedPanel;
80    NSInteger mResultCode;
81    BOOL mDialogIsExecuting;
82    BOOL mResultSet;
83}
84- (id)initWithColorPanel:(NSColorPanel *)panel
85       stolenContentView:(NSView *)stolenContentView
86                okButton:(NSButton *)okButton
87            cancelButton:(NSButton *)cancelButton
88                    priv:(QColorDialogPrivate *)priv;
89- (void)colorChanged:(NSNotification *)notification;
90- (void)relayout;
91- (void)onOkClicked;
92- (void)onCancelClicked;
93- (void)updateQtColor;
94- (NSColorPanel *)colorPanel;
95- (QColor)qtColor;
96- (void)finishOffWithCode:(NSInteger)result;
97- (void)showColorPanel;
98- (void)exec;
99- (void)setResultSet:(BOOL)result;
100@end
101
102@implementation QT_MANGLE_NAMESPACE(QCocoaColorPanelDelegate)
103- (id)initWithColorPanel:(NSColorPanel *)panel
104       stolenContentView:(NSView *)stolenContentView
105                okButton:(NSButton *)okButton
106            cancelButton:(NSButton *)cancelButton
107                    priv:(QColorDialogPrivate *)priv
108{
109    self = [super init];
110
111    mColorPanel = panel;
112    mStolenContentView = stolenContentView;
113    mOkButton = okButton;
114    mCancelButton = cancelButton;
115    mPriv = priv;
116    mMinWidth = 0.0;
117    mExtraHeight = 0.0;
118    mHackedPanel = (okButton != 0);
119    mResultCode = NSCancelButton;
120    mDialogIsExecuting = false;
121    mResultSet = false;
122
123#if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_7
124    if (QSysInfo::MacintoshVersion >= QSysInfo::MV_10_7)
125        [mColorPanel setRestorable:NO];
126#endif
127
128    if (mHackedPanel) {
129        [self relayout];
130
131        [okButton setAction:@selector(onOkClicked)];
132        [okButton setTarget:self];
133
134        [cancelButton setAction:@selector(onCancelClicked)];
135        [cancelButton setTarget:self];
136    }
137
138    [[NSNotificationCenter defaultCenter] addObserver:self
139        selector:@selector(colorChanged:)
140        name:NSColorPanelColorDidChangeNotification
141        object:mColorPanel];
142
143    mQtColor = new QColor();
144    return self;
145}
146
147- (void)dealloc
148{
149    QMacCocoaAutoReleasePool pool;
150    if (mHackedPanel) {
151        NSView *ourContentView = [mColorPanel contentView];
152
153        // return stolen stuff to its rightful owner
154        [mStolenContentView removeFromSuperview];
155        [mColorPanel setContentView:mStolenContentView];
156
157        [mOkButton release];
158        [mCancelButton release];
159        [ourContentView release];
160    }
161    [mColorPanel setDelegate:nil];
162    [[NSNotificationCenter defaultCenter] removeObserver:self];
163    delete mQtColor;
164    [super dealloc];
165}
166
167- (void)setResultSet:(BOOL)result
168{
169    mResultSet = result;
170}
171
172- (BOOL)windowShouldClose:(id)window
173{
174    Q_UNUSED(window);
175    if (!mHackedPanel)
176        [self updateQtColor];
177    if (mDialogIsExecuting) {
178        [self finishOffWithCode:NSCancelButton];
179    } else {
180        mResultSet = true;
181        mPriv->colorDialog()->reject();
182    }
183    return true;
184}
185
186- (void)windowDidResize:(NSNotification *)notification
187{
188    Q_UNUSED(notification);
189    if (mHackedPanel)
190        [self relayout];
191}
192
193- (void)colorChanged:(NSNotification *)notification
194{
195    Q_UNUSED(notification);
196    [self updateQtColor];
197}
198
199- (void)relayout
200{
201    Q_ASSERT(mHackedPanel);
202
203    NSRect rect = [[mStolenContentView superview] frame];
204
205    // should a priori be kept in sync with qfontdialog_mac.mm
206    const CGFloat ButtonMinWidth = 78.0; // 84.0 for Carbon
207    const CGFloat ButtonMinHeight = 32.0;
208    const CGFloat ButtonSpacing = 0.0;
209    const CGFloat ButtonTopMargin = 0.0;
210    const CGFloat ButtonBottomMargin = 7.0;
211    const CGFloat ButtonSideMargin = 9.0;
212
213    [mOkButton sizeToFit];
214    NSSize okSizeHint = [mOkButton frame].size;
215
216    [mCancelButton sizeToFit];
217    NSSize cancelSizeHint = [mCancelButton frame].size;
218
219    const CGFloat ButtonWidth = qMin(qMax(ButtonMinWidth,
220                                          qMax(okSizeHint.width, cancelSizeHint.width)),
221                                     CGFloat((rect.size.width - 2.0 * ButtonSideMargin - ButtonSpacing) * 0.5));
222    const CGFloat ButtonHeight = qMax(ButtonMinHeight,
223                                     qMax(okSizeHint.height, cancelSizeHint.height));
224
225    NSRect okRect = { { rect.size.width - ButtonSideMargin - ButtonWidth,
226                        ButtonBottomMargin },
227                      { ButtonWidth, ButtonHeight } };
228    [mOkButton setFrame:okRect];
229    [mOkButton setNeedsDisplay:YES];
230
231    NSRect cancelRect = { { okRect.origin.x - ButtonSpacing - ButtonWidth,
232                            ButtonBottomMargin },
233                            { ButtonWidth, ButtonHeight } };
234    [mCancelButton setFrame:cancelRect];
235    [mCancelButton setNeedsDisplay:YES];
236
237    const CGFloat Y = ButtonBottomMargin + ButtonHeight + ButtonTopMargin;
238    NSRect stolenCVRect = { { 0.0, Y },
239                            { rect.size.width, rect.size.height - Y } };
240    [mStolenContentView setFrame:stolenCVRect];
241    [mStolenContentView setNeedsDisplay:YES];
242
243    [[mStolenContentView superview] setNeedsDisplay:YES];
244    mMinWidth = 2 * ButtonSideMargin + ButtonSpacing + 2 * ButtonWidth;
245    mExtraHeight = Y;
246}
247
248- (void)onOkClicked
249{
250    Q_ASSERT(mHackedPanel);
251    [[mStolenContentView window] close];
252    [self updateQtColor];
253    [self finishOffWithCode:NSOKButton];
254}
255
256- (void)onCancelClicked
257{
258    if (mHackedPanel) {
259        [[mStolenContentView window] close];
260        delete mQtColor;
261        mQtColor = new QColor();
262        [self finishOffWithCode:NSCancelButton];
263    }
264}
265
266- (void)updateQtColor
267{
268    delete mQtColor;
269    mQtColor = new QColor();
270    NSColor *color = [mColorPanel color];
271    NSString *colorSpaceName = [color colorSpaceName];
272    if (colorSpaceName == NSDeviceCMYKColorSpace) {
273        CGFloat cyan = 0, magenta = 0, yellow = 0, black = 0, alpha = 0;
274        [color getCyan:&cyan magenta:&magenta yellow:&yellow black:&black alpha:&alpha];
275        mQtColor->setCmykF(cyan, magenta, yellow, black, alpha);
276    } else if (colorSpaceName == NSCalibratedRGBColorSpace || colorSpaceName == NSDeviceRGBColorSpace)  {
277        CGFloat red = 0, green = 0, blue = 0, alpha = 0;
278        [color getRed:&red green:&green blue:&blue alpha:&alpha];
279        mQtColor->setRgbF(red, green, blue, alpha);
280    } else if (colorSpaceName == NSNamedColorSpace) {
281        NSColor *tmpColor = [color colorUsingColorSpaceName:NSCalibratedRGBColorSpace];
282        CGFloat red = 0, green = 0, blue = 0, alpha = 0;
283        [tmpColor getRed:&red green:&green blue:&blue alpha:&alpha];
284        mQtColor->setRgbF(red, green, blue, alpha);
285    } else {
286        NSColorSpace *colorSpace = [color colorSpace];
287        if ([colorSpace colorSpaceModel] == NSCMYKColorSpaceModel && [color numberOfComponents] == 5){
288            CGFloat components[5];
289            [color getComponents:components];
290            mQtColor->setCmykF(components[0], components[1], components[2], components[3], components[4]);
291        } else {
292            NSColor *tmpColor = [color colorUsingColorSpaceName:NSCalibratedRGBColorSpace];
293            CGFloat red = 0, green = 0, blue = 0, alpha = 0;
294            [tmpColor getRed:&red green:&green blue:&blue alpha:&alpha];
295            mQtColor->setRgbF(red, green, blue, alpha);
296        }
297    }
298
299    mPriv->setCurrentQColor(*mQtColor);
300}
301
302- (NSColorPanel *)colorPanel
303{
304    return mColorPanel;
305}
306
307- (QColor)qtColor
308{
309    return *mQtColor;
310}
311
312- (void)finishOffWithCode:(NSInteger)code
313{
314    mResultCode = code;
315    if (mDialogIsExecuting) {
316        // We stop the current modal event loop. The control
317        // will then return inside -(void)exec below.
318        // It's important that the modal event loop is stopped before
319        // we accept/reject QColorDialog, since QColorDialog has its
320        // own event loop that needs to be stopped last.
321        [[NSApplication sharedApplication] stopModalWithCode:code];
322    } else {
323        // Since we are not in a modal event loop, we can safely close
324        // down QColorDialog
325        // Calling accept() or reject() can in turn call closeCocoaColorPanel.
326        // This check will prevent any such recursion.
327        if (!mResultSet) {
328            mResultSet = true;
329            if (mResultCode == NSCancelButton) {
330                mPriv->colorDialog()->reject();
331            } else {
332                mPriv->colorDialog()->accept();
333            }
334        }
335    }
336}
337
338- (void)showColorPanel
339{
340    mDialogIsExecuting = false;
341    [mColorPanel makeKeyAndOrderFront:mColorPanel];
342}
343
344- (void)exec
345{
346    QBoolBlocker nativeDialogOnTop(QApplicationPrivate::native_modal_dialog_active);
347    QMacCocoaAutoReleasePool pool;
348    mDialogIsExecuting = true;
349    bool modalEnded = false;
350    while (!modalEnded) {
351#ifndef QT_NO_EXCEPTIONS
352        @try {
353            [[NSApplication sharedApplication] runModalForWindow:mColorPanel];
354            modalEnded = true;
355        } @catch (NSException *) {
356            // For some reason, NSColorPanel throws an exception when
357            // clicking on 'SelectedMenuItemColor' from the 'Developer'
358            // palette (tab three).
359        }
360#else
361        [[NSApplication sharedApplication] runModalForWindow:mColorPanel];
362        modalEnded = true;
363#endif
364    }
365
366    QAbstractEventDispatcher::instance()->interrupt();
367    if (mResultCode == NSCancelButton)
368        mPriv->colorDialog()->reject();
369    else
370        mPriv->colorDialog()->accept();
371}
372
373@end
374
375QT_BEGIN_NAMESPACE
376
377extern void macStartInterceptNSPanelCtor();
378extern void macStopInterceptNSPanelCtor();
379extern NSButton *macCreateButton(const char *text, NSView *superview);
380
381void QColorDialogPrivate::openCocoaColorPanel(const QColor &initial,
382        QWidget *parent, const QString &title, QColorDialog::ColorDialogOptions options)
383{
384    Q_UNUSED(parent);   // we would use the parent if only NSColorPanel could be a sheet
385    QMacCocoaAutoReleasePool pool;
386
387    if (!delegate) {
388        /*
389           The standard Cocoa color panel has no OK or Cancel button and
390           is created as a utility window, whereas we want something like
391           the Carbon color panel. We need to take the following steps:
392
393           1. Intercept the color panel constructor to turn off the
394           NSUtilityWindowMask flag. This is done by temporarily
395           replacing initWithContentRect:styleMask:backing:defer:
396           in NSPanel by our own method.
397
398           2. Modify the color panel so that its content view is part
399           of a new content view that contains it as well as two
400           buttons (OK and Cancel).
401
402           3. Lay out the original content view and the buttons when
403           the color panel is shown and whenever it is resized.
404
405           4. Clean up after ourselves.
406         */
407
408        bool hackColorPanel = !(options & QColorDialog::NoButtons);
409
410        if (hackColorPanel)
411            macStartInterceptNSPanelCtor();
412        NSColorPanel *colorPanel = [NSColorPanel sharedColorPanel];
413        if (hackColorPanel)
414            macStopInterceptNSPanelCtor();
415
416        [colorPanel setHidesOnDeactivate:false];
417
418        // set up the Cocoa color panel
419        [colorPanel setShowsAlpha:options & QColorDialog::ShowAlphaChannel];
420        [colorPanel setTitle:(NSString*)(CFStringRef)QCFString(title)];
421
422        NSView *stolenContentView = 0;
423        NSButton *okButton = 0;
424        NSButton *cancelButton = 0;
425
426        if (hackColorPanel) {
427            // steal the color panel's contents view
428            stolenContentView = [colorPanel contentView];
429            [stolenContentView retain];
430            [colorPanel setContentView:0];
431
432            // create a new content view and add the stolen one as a subview
433            NSRect frameRect = { { 0.0, 0.0 }, { 0.0, 0.0 } };
434            NSView *ourContentView = [[NSView alloc] initWithFrame:frameRect];
435            [ourContentView addSubview:stolenContentView];
436
437            // create OK and Cancel buttons and add these as subviews
438            okButton = macCreateButton("&OK", ourContentView);
439            cancelButton = macCreateButton("Cancel", ourContentView);
440
441            [colorPanel setContentView:ourContentView];
442            [colorPanel setDefaultButtonCell:[okButton cell]];
443        }
444
445        delegate = [[QT_MANGLE_NAMESPACE(QCocoaColorPanelDelegate) alloc] initWithColorPanel:colorPanel
446            stolenContentView:stolenContentView
447            okButton:okButton
448            cancelButton:cancelButton
449            priv:this];
450        [colorPanel setDelegate:static_cast<QT_MANGLE_NAMESPACE(QCocoaColorPanelDelegate) *>(delegate)];
451    }
452    [static_cast<QT_MANGLE_NAMESPACE(QCocoaColorPanelDelegate) *>(delegate) setResultSet:NO];
453    setCocoaPanelColor(initial);
454    [static_cast<QT_MANGLE_NAMESPACE(QCocoaColorPanelDelegate) *>(delegate) showColorPanel];
455}
456
457void QColorDialogPrivate::closeCocoaColorPanel()
458{
459    [static_cast<QT_MANGLE_NAMESPACE(QCocoaColorPanelDelegate) *>(delegate) onCancelClicked];
460}
461
462void QColorDialogPrivate::releaseCocoaColorPanelDelegate()
463{
464    [static_cast<QT_MANGLE_NAMESPACE(QCocoaColorPanelDelegate) *>(delegate) release];
465}
466
467void QColorDialogPrivate::mac_nativeDialogModalHelp()
468{
469    // Do a queued meta-call to open the native modal dialog so it opens after the new
470    // event loop has started to execute (in QDialog::exec). Using a timer rather than
471    // a queued meta call is intentional to ensure that the call is only delivered when
472    // [NSApplication run] runs (timers are handeled special in cocoa). If NSApplication is not
473    // running (which is the case if e.g a top-most QEventLoop has been
474    // interrupted, and the second-most event loop has not yet been reactivated (regardless
475    // if [NSApplication run] is still on the stack)), showing a native modal dialog will fail.
476    if (delegate){
477        Q_Q(QColorDialog);
478        QTimer::singleShot(1, q, SLOT(_q_macRunNativeAppModalPanel()));
479    }
480}
481
482void QColorDialogPrivate::_q_macRunNativeAppModalPanel()
483{
484    [static_cast<QT_MANGLE_NAMESPACE(QCocoaColorPanelDelegate) *>(delegate) exec];
485}
486
487void QColorDialogPrivate::setCocoaPanelColor(const QColor &color)
488{
489    QMacCocoaAutoReleasePool pool;
490    QT_MANGLE_NAMESPACE(QCocoaColorPanelDelegate) *theDelegate = static_cast<QT_MANGLE_NAMESPACE(QCocoaColorPanelDelegate) *>(delegate);
491    NSColor *nsColor;
492    const QColor::Spec spec = color.spec();
493    if (spec == QColor::Cmyk) {
494        nsColor = [NSColor colorWithDeviceCyan:color.cyanF()
495                                       magenta:color.magentaF()
496                                        yellow:color.yellowF()
497                                         black:color.blackF()
498                                         alpha:color.alphaF()];
499    } else {
500        nsColor = [NSColor colorWithCalibratedRed:color.redF()
501                                            green:color.greenF()
502                                             blue:color.blueF()
503                                            alpha:color.alphaF()];
504    }
505    [[theDelegate colorPanel] setColor:nsColor];
506}
507
508QT_END_NAMESPACE
509
510#endif
511