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 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 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 <QtCore/qtimer.h>
41#include <QtGui/qfontdatabase.h>
42#include <qpa/qplatformtheme.h>
43
44#include <private/qfont_p.h>
45#include <private/qfontengine_p.h>
46#include <private/qfontengine_coretext_p.h>
47
48#include "qcocoafontdialoghelper.h"
49#include "qcocoahelpers.h"
50#include "qcocoaeventdispatcher.h"
51
52#import <AppKit/AppKit.h>
53
54#if !CGFLOAT_DEFINED
55typedef float CGFloat;  // Should only not be defined on 32-bit platforms
56#endif
57
58QT_USE_NAMESPACE
59
60static QFont qfontForCocoaFont(NSFont *cocoaFont, const QFont &resolveFont)
61{
62    QFont newFont;
63    if (cocoaFont) {
64        int pSize = qRound([cocoaFont pointSize]);
65        QCFType<CTFontDescriptorRef> font(CTFontCopyFontDescriptor((CTFontRef)cocoaFont));
66        QString family(QCFString((CFStringRef)CTFontDescriptorCopyAttribute(font, kCTFontFamilyNameAttribute)));
67        QString style(QCFString(((CFStringRef)CTFontDescriptorCopyAttribute(font, kCTFontStyleNameAttribute))));
68
69        newFont = QFontDatabase().font(family, style, pSize);
70        newFont.setUnderline(resolveFont.underline());
71        newFont.setStrikeOut(resolveFont.strikeOut());
72    }
73    return newFont;
74}
75
76@interface QT_MANGLE_NAMESPACE(QNSFontPanelDelegate) : NSObject<NSWindowDelegate, QNSPanelDelegate>
77- (void)restoreOriginalContentView;
78- (void)updateQtFont;
79- (void)changeFont:(id)sender;
80- (void)finishOffWithCode:(NSInteger)code;
81@end
82
83QT_NAMESPACE_ALIAS_OBJC_CLASS(QNSFontPanelDelegate);
84
85@implementation QNSFontPanelDelegate {
86    @public
87    NSFontPanel *mFontPanel;
88    QCocoaFontDialogHelper *mHelper;
89    NSView *mStolenContentView;
90    QNSPanelContentsWrapper *mPanelButtons;
91    QFont mQtFont;
92    NSInteger mResultCode;
93    BOOL mDialogIsExecuting;
94    BOOL mResultSet;
95}
96
97- (instancetype)init
98{
99    if ((self = [super init])) {
100        mFontPanel = [NSFontPanel sharedFontPanel];
101        mHelper = nullptr;
102        mStolenContentView = nil;
103        mPanelButtons = nil;
104        mResultCode = NSModalResponseCancel;
105        mDialogIsExecuting = false;
106        mResultSet = false;
107
108        [mFontPanel setRestorable:NO];
109        [mFontPanel setDelegate:self];
110
111        [mFontPanel retain];
112    }
113    return self;
114}
115
116- (void)dealloc
117{
118    [mStolenContentView release];
119    [mFontPanel setDelegate:nil];
120    [[NSNotificationCenter defaultCenter] removeObserver:self];
121
122    [super dealloc];
123}
124
125- (void)setDialogHelper:(QCocoaFontDialogHelper *)helper
126{
127    mHelper = helper;
128
129    [mFontPanel setTitle:helper->options()->windowTitle().toNSString()];
130
131    if (mHelper->options()->testOption(QFontDialogOptions::NoButtons)) {
132        [self restoreOriginalContentView];
133    } else if (!mStolenContentView) {
134        // steal the font panel's contents view
135        mStolenContentView = mFontPanel.contentView;
136        [mStolenContentView retain];
137        mFontPanel.contentView = nil;
138
139        // create a new content view and add the stolen one as a subview
140        mPanelButtons = [[QNSPanelContentsWrapper alloc] initWithPanelDelegate:self];
141        [mPanelButtons addSubview:mStolenContentView];
142        mPanelButtons.panelContentsMargins = NSEdgeInsetsMake(0, 0, 7, 0);
143        mFontPanel.contentView = mPanelButtons;
144        mFontPanel.defaultButtonCell = mPanelButtons.okButton.cell;
145    }
146}
147
148- (void)closePanel
149{
150    [mFontPanel close];
151}
152
153- (void)restoreOriginalContentView
154{
155    if (mStolenContentView) {
156        // return stolen stuff to its rightful owner
157        [mStolenContentView removeFromSuperview];
158        [mFontPanel setContentView:mStolenContentView];
159        mStolenContentView = nil;
160        [mPanelButtons release];
161        mPanelButtons = nil;
162    }
163}
164
165- (void)onOkClicked
166{
167    [mFontPanel close];
168    [self finishOffWithCode:NSModalResponseOK];
169}
170
171- (void)onCancelClicked
172{
173    if (mPanelButtons) {
174        [mFontPanel close];
175        mQtFont = QFont();
176        [self finishOffWithCode:NSModalResponseCancel];
177    }
178}
179
180- (void)changeFont:(id)sender
181{
182    Q_UNUSED(sender);
183    [self updateQtFont];
184}
185
186- (void)updateQtFont
187{
188    // Get selected font
189    NSFontManager *fontManager = [NSFontManager sharedFontManager];
190    NSFont *selectedFont = [fontManager selectedFont];
191    if (!selectedFont) {
192        selectedFont = [NSFont systemFontOfSize:[NSFont systemFontSize]];
193    }
194    NSFont *panelFont = [fontManager convertFont:selectedFont];
195    mQtFont = qfontForCocoaFont(panelFont, mQtFont);
196
197    if (mHelper)
198        emit mHelper->currentFontChanged(mQtFont);
199}
200
201- (void)showModelessPanel
202{
203    mDialogIsExecuting = false;
204    mResultSet = false;
205    [mFontPanel makeKeyAndOrderFront:mFontPanel];
206}
207
208- (BOOL)runApplicationModalPanel
209{
210    mDialogIsExecuting = true;
211    // Call processEvents in case the event dispatcher has been interrupted, and needs to do
212    // cleanup of modal sessions. Do this before showing the native dialog, otherwise it will
213    // close down during the cleanup.
214    qApp->processEvents(QEventLoop::ExcludeUserInputEvents | QEventLoop::ExcludeSocketNotifiers);
215
216    // Make sure we don't interrupt the runModalForWindow call.
217    QCocoaEventDispatcher::clearCurrentThreadCocoaEventDispatcherInterruptFlag();
218
219    [NSApp runModalForWindow:mFontPanel];
220    mDialogIsExecuting = false;
221    return (mResultCode == NSModalResponseOK);
222}
223
224// Future proofing in case _NSTargetForSendAction checks this
225// property before sending us the changeFont: message.
226- (BOOL)worksWhenModal
227{
228    return YES;
229}
230
231- (QPlatformDialogHelper::DialogCode)dialogResultCode
232{
233    return (mResultCode == NSModalResponseOK) ? QPlatformDialogHelper::Accepted : QPlatformDialogHelper::Rejected;
234}
235
236- (BOOL)windowShouldClose:(id)window
237{
238    Q_UNUSED(window);
239    if (!mPanelButtons)
240        [self updateQtFont];
241    if (mDialogIsExecuting) {
242        [self finishOffWithCode:NSModalResponseCancel];
243    } else {
244        mResultSet = true;
245        if (mHelper)
246            emit mHelper->reject();
247    }
248    return true;
249}
250
251- (void)finishOffWithCode:(NSInteger)code
252{
253    mResultCode = code;
254    if (mDialogIsExecuting) {
255        // We stop the current modal event loop. The control
256        // will then return inside -(void)exec below.
257        // It's important that the modal event loop is stopped before
258        // we accept/reject QFontDialog, since QFontDialog has its
259        // own event loop that needs to be stopped last.
260        [NSApp stopModalWithCode:code];
261    } else {
262        // Since we are not in a modal event loop, we can safely close
263        // down QFontDialog
264        // Calling accept() or reject() can in turn call closeCocoaFontPanel.
265        // This check will prevent any such recursion.
266        if (!mResultSet) {
267            mResultSet = true;
268            if (mResultCode == NSModalResponseCancel) {
269                emit mHelper->reject();
270            } else {
271                emit mHelper->accept();
272            }
273        }
274    }
275}
276
277@end
278
279QT_BEGIN_NAMESPACE
280
281class QCocoaFontPanel
282{
283public:
284    QCocoaFontPanel()
285    {
286        mDelegate = [[QNSFontPanelDelegate alloc] init];
287    }
288
289    ~QCocoaFontPanel()
290    {
291        [mDelegate release];
292    }
293
294    void init(QCocoaFontDialogHelper *helper)
295    {
296        [mDelegate setDialogHelper:helper];
297    }
298
299    void cleanup(QCocoaFontDialogHelper *helper)
300    {
301        if (mDelegate->mHelper == helper)
302            mDelegate->mHelper = nullptr;
303    }
304
305    bool exec()
306    {
307        // Note: If NSApp is not running (which is the case if e.g a top-most
308        // QEventLoop has been interrupted, and the second-most event loop has not
309        // yet been reactivated (regardless if [NSApp run] is still on the stack)),
310        // showing a native modal dialog will fail.
311        return [mDelegate runApplicationModalPanel];
312    }
313
314    bool show(Qt::WindowModality windowModality, QWindow *parent)
315    {
316        Q_UNUSED(parent);
317        if (windowModality != Qt::WindowModal)
318            [mDelegate showModelessPanel];
319        // no need to show a Qt::WindowModal dialog here, because it's necessary to call exec() in that case
320        return true;
321    }
322
323    void hide()
324    {
325        [mDelegate closePanel];
326    }
327
328    QFont currentFont() const
329    {
330        return mDelegate->mQtFont;
331    }
332
333    void setCurrentFont(const QFont &font)
334    {
335        NSFontManager *mgr = [NSFontManager sharedFontManager];
336        NSFont *nsFont = nil;
337
338        int weight = 5;
339        NSFontTraitMask mask = 0;
340        if (font.style() == QFont::StyleItalic) {
341            mask |= NSItalicFontMask;
342        }
343        if (font.weight() == QFont::Bold) {
344            weight = 9;
345            mask |= NSBoldFontMask;
346        }
347
348        QFontInfo fontInfo(font);
349        nsFont = [mgr fontWithFamily:fontInfo.family().toNSString()
350            traits:mask
351            weight:weight
352            size:fontInfo.pointSize()];
353
354        [mgr setSelectedFont:nsFont isMultiple:NO];
355        mDelegate->mQtFont = font;
356    }
357
358private:
359    QNSFontPanelDelegate *mDelegate;
360};
361
362Q_GLOBAL_STATIC(QCocoaFontPanel, sharedFontPanel)
363
364QCocoaFontDialogHelper::QCocoaFontDialogHelper()
365{
366}
367
368QCocoaFontDialogHelper::~QCocoaFontDialogHelper()
369{
370    sharedFontPanel()->cleanup(this);
371}
372
373void QCocoaFontDialogHelper::exec()
374{
375    if (sharedFontPanel()->exec())
376        emit accept();
377    else
378        emit reject();
379}
380
381bool QCocoaFontDialogHelper::show(Qt::WindowFlags, Qt::WindowModality windowModality, QWindow *parent)
382{
383    if (windowModality == Qt::WindowModal)
384        windowModality = Qt::ApplicationModal;
385    sharedFontPanel()->init(this);
386    return sharedFontPanel()->show(windowModality, parent);
387}
388
389void QCocoaFontDialogHelper::hide()
390{
391    sharedFontPanel()->hide();
392}
393
394void QCocoaFontDialogHelper::setCurrentFont(const QFont &font)
395{
396    sharedFontPanel()->init(this);
397    sharedFontPanel()->setCurrentFont(font);
398}
399
400QFont QCocoaFontDialogHelper::currentFont() const
401{
402    return sharedFontPanel()->currentFont();
403}
404
405QT_END_NAMESPACE
406