1/****************************************************************************
2**
3** Copyright (C) 2018 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// This file is included from qnsview.mm, and only used to organize the code
41
42@implementation QNSView (ComplexTextAPI)
43
44- (void)cancelComposingText
45{
46    if (m_composingText.isEmpty())
47        return;
48
49    if (m_composingFocusObject) {
50        QInputMethodQueryEvent queryEvent(Qt::ImEnabled);
51        if (QCoreApplication::sendEvent(m_composingFocusObject, &queryEvent)) {
52            if (queryEvent.value(Qt::ImEnabled).toBool()) {
53                QInputMethodEvent e;
54                QCoreApplication::sendEvent(m_composingFocusObject, &e);
55            }
56        }
57    }
58
59    m_composingText.clear();
60    m_composingFocusObject = nullptr;
61}
62
63- (void)unmarkText
64{
65    if (!m_composingText.isEmpty()) {
66        if (QObject *fo = m_platformWindow->window()->focusObject()) {
67            QInputMethodQueryEvent queryEvent(Qt::ImEnabled);
68            if (QCoreApplication::sendEvent(fo, &queryEvent)) {
69                if (queryEvent.value(Qt::ImEnabled).toBool()) {
70                    QInputMethodEvent e;
71                    e.setCommitString(m_composingText);
72                    QCoreApplication::sendEvent(fo, &e);
73                }
74            }
75        }
76    }
77    m_composingText.clear();
78    m_composingFocusObject = nullptr;
79}
80
81@end
82
83@implementation QNSView (ComplexText)
84
85- (void)insertNewline:(id)sender
86{
87    Q_UNUSED(sender);
88    m_resendKeyEvent = true;
89}
90
91- (void)doCommandBySelector:(SEL)aSelector
92{
93    [self tryToPerform:aSelector with:self];
94}
95
96- (void)insertText:(id)aString replacementRange:(NSRange)replacementRange
97{
98    Q_UNUSED(replacementRange)
99
100    if (m_sendKeyEvent && m_composingText.isEmpty() && [aString isEqualToString:m_inputSource]) {
101        // don't send input method events for simple text input (let handleKeyEvent send key events instead)
102        return;
103    }
104
105    QString commitString;
106    if ([aString length]) {
107        if ([aString isKindOfClass:[NSAttributedString class]]) {
108            commitString = QString::fromCFString(reinterpret_cast<CFStringRef>([aString string]));
109        } else {
110            commitString = QString::fromCFString(reinterpret_cast<CFStringRef>(aString));
111        };
112    }
113    if (QObject *fo = m_platformWindow->window()->focusObject()) {
114        QInputMethodQueryEvent queryEvent(Qt::ImEnabled);
115        if (QCoreApplication::sendEvent(fo, &queryEvent)) {
116            if (queryEvent.value(Qt::ImEnabled).toBool()) {
117                QInputMethodEvent e;
118                e.setCommitString(commitString);
119                QCoreApplication::sendEvent(fo, &e);
120                // prevent handleKeyEvent from sending a key event
121                m_sendKeyEvent = false;
122            }
123        }
124    }
125
126    m_composingText.clear();
127    m_composingFocusObject = nullptr;
128}
129
130- (void)setMarkedText:(id)aString selectedRange:(NSRange)selectedRange replacementRange:(NSRange)replacementRange
131{
132    Q_UNUSED(replacementRange)
133    QString preeditString;
134
135    QList<QInputMethodEvent::Attribute> attrs;
136    attrs<<QInputMethodEvent::Attribute(QInputMethodEvent::Cursor, selectedRange.location + selectedRange.length, 1, QVariant());
137
138    if ([aString isKindOfClass:[NSAttributedString class]]) {
139        // Preedit string has attribution
140        preeditString = QString::fromCFString(reinterpret_cast<CFStringRef>([aString string]));
141        int composingLength = preeditString.length();
142        int index = 0;
143        // Create attributes for individual sections of preedit text
144        while (index < composingLength) {
145            NSRange effectiveRange;
146            NSRange range = NSMakeRange(index, composingLength-index);
147            NSDictionary *attributes = [aString attributesAtIndex:index
148                                            longestEffectiveRange:&effectiveRange
149                                                          inRange:range];
150            NSNumber *underlineStyle = [attributes objectForKey:NSUnderlineStyleAttributeName];
151            if (underlineStyle) {
152                QColor clr (Qt::black);
153                NSColor *color = [attributes objectForKey:NSUnderlineColorAttributeName];
154                if (color) {
155                    clr = qt_mac_toQColor(color);
156                }
157                QTextCharFormat format;
158                format.setFontUnderline(true);
159                format.setUnderlineColor(clr);
160                attrs<<QInputMethodEvent::Attribute(QInputMethodEvent::TextFormat,
161                                                    effectiveRange.location,
162                                                    effectiveRange.length,
163                                                    format);
164            }
165            index = effectiveRange.location + effectiveRange.length;
166        }
167    } else {
168        // No attributes specified, take only the preedit text.
169        preeditString = QString::fromCFString(reinterpret_cast<CFStringRef>(aString));
170    }
171
172    if (attrs.isEmpty()) {
173        QTextCharFormat format;
174        format.setFontUnderline(true);
175        attrs<<QInputMethodEvent::Attribute(QInputMethodEvent::TextFormat,
176                                            0, preeditString.length(), format);
177    }
178
179    m_composingText = preeditString;
180
181    if (QObject *fo = m_platformWindow->window()->focusObject()) {
182        m_composingFocusObject = fo;
183        QInputMethodQueryEvent queryEvent(Qt::ImEnabled);
184        if (QCoreApplication::sendEvent(fo, &queryEvent)) {
185            if (queryEvent.value(Qt::ImEnabled).toBool()) {
186                QInputMethodEvent e(preeditString, attrs);
187                QCoreApplication::sendEvent(fo, &e);
188                // prevent handleKeyEvent from sending a key event
189                m_sendKeyEvent = false;
190            }
191        }
192    }
193}
194
195- (BOOL)hasMarkedText
196{
197    return (m_composingText.isEmpty() ? NO: YES);
198}
199
200- (NSAttributedString *)attributedSubstringForProposedRange:(NSRange)aRange actualRange:(NSRangePointer)actualRange
201{
202    Q_UNUSED(actualRange)
203    QObject *fo = m_platformWindow->window()->focusObject();
204    if (!fo)
205        return nil;
206    QInputMethodQueryEvent queryEvent(Qt::ImEnabled | Qt::ImCurrentSelection);
207    if (!QCoreApplication::sendEvent(fo, &queryEvent))
208        return nil;
209    if (!queryEvent.value(Qt::ImEnabled).toBool())
210        return nil;
211
212    QString selectedText = queryEvent.value(Qt::ImCurrentSelection).toString();
213    if (selectedText.isEmpty())
214        return nil;
215
216    QCFString string(selectedText.mid(aRange.location, aRange.length));
217    const NSString *tmpString = reinterpret_cast<const NSString *>((CFStringRef)string);
218    return [[[NSAttributedString alloc]  initWithString:const_cast<NSString *>(tmpString)] autorelease];
219}
220
221- (NSRange)markedRange
222{
223    NSRange range;
224    if (!m_composingText.isEmpty()) {
225        range.location = 0;
226        range.length = m_composingText.length();
227    } else {
228        range.location = NSNotFound;
229        range.length = 0;
230    }
231    return range;
232}
233
234- (NSRange)selectedRange
235{
236    NSRange selectedRange = {0, 0};
237
238    QObject *fo = m_platformWindow->window()->focusObject();
239    if (!fo)
240        return selectedRange;
241    QInputMethodQueryEvent queryEvent(Qt::ImEnabled | Qt::ImCurrentSelection);
242    if (!QCoreApplication::sendEvent(fo, &queryEvent))
243        return selectedRange;
244    if (!queryEvent.value(Qt::ImEnabled).toBool())
245        return selectedRange;
246
247    QString selectedText = queryEvent.value(Qt::ImCurrentSelection).toString();
248
249    if (!selectedText.isEmpty()) {
250        selectedRange.location = 0;
251        selectedRange.length = selectedText.length();
252    }
253    return selectedRange;
254}
255
256- (NSRect)firstRectForCharacterRange:(NSRange)aRange actualRange:(NSRangePointer)actualRange
257{
258    Q_UNUSED(aRange)
259    Q_UNUSED(actualRange)
260
261    QObject *fo = m_platformWindow->window()->focusObject();
262    if (!fo)
263        return NSZeroRect;
264
265    QInputMethodQueryEvent queryEvent(Qt::ImEnabled);
266    if (!QCoreApplication::sendEvent(fo, &queryEvent))
267        return NSZeroRect;
268    if (!queryEvent.value(Qt::ImEnabled).toBool())
269        return NSZeroRect;
270
271    // The returned rect is always based on the internal cursor.
272    QRect mr = qApp->inputMethod()->cursorRectangle().toRect();
273    mr.moveBottomLeft(m_platformWindow->window()->mapToGlobal(mr.bottomLeft()));
274    return QCocoaScreen::mapToNative(mr);
275}
276
277- (NSUInteger)characterIndexForPoint:(NSPoint)aPoint
278{
279    // We don't support cursor movements using mouse while composing.
280    Q_UNUSED(aPoint);
281    return NSNotFound;
282}
283
284- (NSArray<NSString *> *)validAttributesForMarkedText
285{
286    if (!m_platformWindow)
287        return nil;
288
289    if (m_platformWindow->window() != QGuiApplication::focusWindow())
290        return nil;
291
292    QObject *fo = m_platformWindow->window()->focusObject();
293    if (!fo)
294        return nil;
295
296    QInputMethodQueryEvent queryEvent(Qt::ImEnabled);
297    if (!QCoreApplication::sendEvent(fo, &queryEvent))
298        return nil;
299    if (!queryEvent.value(Qt::ImEnabled).toBool())
300        return nil;
301
302    // Support only underline color/style.
303    return @[NSUnderlineColorAttributeName, NSUnderlineStyleAttributeName];
304}
305
306- (void)textInputContextKeyboardSelectionDidChangeNotification:(NSNotification *)textInputContextKeyboardSelectionDidChangeNotification
307{
308    Q_UNUSED(textInputContextKeyboardSelectionDidChangeNotification)
309    if (([NSApp keyWindow] == self.window) && self.window.firstResponder == self) {
310        if (QCocoaInputContext *ic = qobject_cast<QCocoaInputContext *>(QCocoaIntegration::instance()->inputContext()))
311            ic->updateLocale();
312    }
313}
314
315@end
316