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
qnsview_complextext.mm
Go to the documentation of this file.
1// Copyright (C) 2021 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// This file is included from qnsview.mm, and only used to organize the code
6
7@implementation QNSView (ComplexText)
8
9// ------------- Text insertion -------------
10
11- (QObject*)focusObject
12{
13 // The text input system may still hold a reference to our QNSView,
14 // even after QCocoaWindow has been destructed, delivering text input
15 // events to us, so we need to guard for this situation explicitly.
16 if (!m_platformWindow)
17 return nullptr;
18
19 return m_platformWindow->window()->focusObject();
20}
21
22/*
23 Inserts the given text, potentially replacing existing text.
24
25 The text input management system calls this as a result of:
26
27 - A normal key press, via [NSView interpretKeyEvents:] or
28 [NSInputContext handleEvent:]
29
30 - An input method finishing (confirming) composition
31
32 - Pressing a key in the Keyboard Viewer panel
33
34 - Confirming an inline input area (accent popup e.g.)
35
36 \a replacementRange refers to the existing text to replace.
37 Under normal circumstances this is {NSNotFound, 0}, and the
38 implementation should replace either the existing marked text,
39 the current selection, or just insert the text at the current
40 cursor location.
41*/
42- (void)insertText:(id)text replacementRange:(NSRange)replacementRange
43{
44 qCDebug(lcQpaKeys).nospace() << "Inserting \"" << text << "\""
45 << ", replacing range " << replacementRange;
46
47 NSString *string = [self stringForText:text];
48
49 if (m_composingText.isEmpty()) {
50 // The input method may have transformed the incoming key event
51 // to text that doesn't match what the original key event would
52 // have produced, for example when 'Pinyin - Simplified' does smart
53 // replacement of quotes. If that's the case we can't rely on
54 // handleKeyEvent for sending the text.
55 if ([string isEqualToString:m_currentlyInterpretedKeyEvent.characters]) {
56 // We do not send input method events for simple text input,
57 // and instead let handleKeyEvent send the key event.
58 qCDebug(lcQpaKeys) << "Ignoring text insertion for simple text";
59 m_sendKeyEvent = true;
60 return;
61 }
62 }
63
64 if (queryInputMethod(self.focusObject)) {
65 QInputMethodEvent inputMethodEvent;
66
67 QString commitString = QString::fromNSString(string);
68
69 // Ensure we have a valid replacement range
70 replacementRange = [self sanitizeReplacementRange:replacementRange];
71
72 // Qt's QInputMethodEvent has different semantics for the replacement
73 // range than AppKit does, so we need to sanitize the range first.
74 auto [replaceFrom, replaceLength] = [self inputMethodRangeForRange:replacementRange];
75
76 if (replaceFrom == NSNotFound) {
77 qCWarning(lcQpaKeys) << "Failed to compute valid replacement range for text insertion";
78 inputMethodEvent.setCommitString(commitString);
79 } else {
80 qCDebug(lcQpaKeys) << "Replacing from" << replaceFrom << "with length" << replaceLength
81 << "based on replacement range" << replacementRange;
82 inputMethodEvent.setCommitString(commitString, replaceFrom, replaceLength);
83 }
84
85 QCoreApplication::sendEvent(self.focusObject, &inputMethodEvent);
86 }
87
88 m_composingText.clear();
89 m_composingFocusObject = nullptr;
90}
91
92- (void)insertNewline:(id)sender
93{
94 Q_UNUSED(sender);
95
96 if (!m_platformWindow)
97 return;
98
99 // Depending on the input method, pressing enter may
100 // result in simply dismissing the input method editor,
101 // without confirming the composition. In other cases
102 // it may confirm the composition as well. And in some
103 // cases the IME will produce an explicit new line, which
104 // brings us here.
105
106 // Semantically, the input method has asked us to insert
107 // a newline, and we should do so via an QInputMethodEvent,
108 // either directly or via [self insertText:@"\r"]. This is
109 // also how NSTextView handles the command. But, if we did,
110 // we would bypass all the code in Qt (and clients) that
111 // assume that pressing the return key results in a key
112 // event, for example the QLineEdit::returnPressed logic.
113 // To ensure that clients will still see the Qt::Key_Return
114 // key event, we send it as a normal key event.
115
116 // But, we can not fall back to handleKeyEvent for this,
117 // as the original key event may have text that reflects
118 // the combination of the inserted text and the newline,
119 // e.g. "~\r". We have already inserted the composition,
120 // so we need to follow up with a single newline event.
121
122 KeyEvent newlineEvent(m_currentlyInterpretedKeyEvent);
123 newlineEvent.type = QEvent::KeyPress;
124
125 const bool isEnter = newlineEvent.modifiers & Qt::KeypadModifier;
126 newlineEvent.key = isEnter ? Qt::Key_Enter : Qt::Key_Return;
127 newlineEvent.text = isEnter ? QLatin1Char(kEnterCharCode)
128 : QLatin1Char(kReturnCharCode);
129 newlineEvent.nativeVirtualKey = isEnter ? quint32(kVK_ANSI_KeypadEnter)
130 : quint32(kVK_Return);
131
132 qCDebug(lcQpaKeys) << "Inserting newline via" << newlineEvent;
133 newlineEvent.sendWindowSystemEvent(m_platformWindow->window());
134}
135
136// ------------- Text composition -------------
137
138/*
139 Updates the composed text, potentially replacing existing text.
140
141 The NSTextInputClient protocol refers to composed text as "marked",
142 since it is "marked differently from the selection, using temporary
143 attributes that affect only display, not layout or storage.""
144
145 The concept maps to the preeditString of our QInputMethodEvent.
146
147 \a selectedRange refers to the part of the marked text that
148 is considered selected, for example when composing text with
149 multiple clause segments (Hiragana - Kana e.g.).
150
151 \a replacementRange refers to the existing text to replace.
152 Under normal circumstances this is {NSNotFound, 0}, and the
153 implementation should replace either the existing marked text,
154 the current selection, or just insert the text at the current
155 cursor location. But when initiating composition of existing
156 committed text (Hiragana - Kana e.g.), the range will be valid.
157*/
158- (void)setMarkedText:(id)text selectedRange:(NSRange)selectedRange replacementRange:(NSRange)replacementRange
159{
160 qCDebug(lcQpaKeys).nospace() << "Marking \"" << text << "\""
161 << " with selected range " << selectedRange
162 << ", replacing range " << replacementRange;
163
164 const bool isAttributedString = [text isKindOfClass:NSAttributedString.class];
165 QString preeditString = QString::fromNSString([self stringForText:text]);
166
167 QList<QInputMethodEvent::Attribute> preeditAttributes;
168
169 // The QInputMethodEvent::Cursor specifies that the length
170 // determines whether the cursor is visible or not, but uses
171 // logic opposite of that of native AppKit application, where
172 // the cursor is visible if there's no selection, and hidden
173 // if there's a selection. Instead of passing on the length
174 // directly we need to inverse the logic.
175 const bool showCursor = !selectedRange.length;
176 preeditAttributes << QInputMethodEvent::Attribute(
177 QInputMethodEvent::Cursor, selectedRange.location, showCursor);
178
179 // QInputMethodEvent::Selection unfortunately doesn't apply to the
180 // preedit text, and QInputMethodEvent::Cursor which does, doesn't
181 // support setting a selection. Until we've introduced attributes
182 // that allow us to propagate the preedit selection semantically
183 // we resort to styling the selection via the TextFormat attribute,
184 // so that the preedit selection is visible to the user.
185 QTextCharFormat selectionFormat;
186 auto *platformTheme = QGuiApplicationPrivate::platformTheme();
187 auto *systemPalette = platformTheme->palette();
188 selectionFormat.setBackground(systemPalette->color(QPalette::Highlight));
189 preeditAttributes << QInputMethodEvent::Attribute(
190 QInputMethodEvent::TextFormat,
191 selectedRange.location, selectedRange.length,
192 selectionFormat);
193
194 int index = 0;
195 int composingLength = preeditString.length();
196 while (index < composingLength) {
197 NSRange range = NSMakeRange(index, composingLength - index);
198
199 static NSDictionary *defaultMarkedTextAttributes = []{
200 NSTextView *textView = [[NSTextView new] autorelease];
201 return [textView.markedTextAttributes retain];
202 }();
203
204 NSDictionary *attributes = isAttributedString
205 ? [text attributesAtIndex:index longestEffectiveRange:&range inRange:range]
206 : defaultMarkedTextAttributes;
207
208 qCDebug(lcQpaKeys) << "Decorating range" << range << "based on" << attributes;
209 QTextCharFormat format;
210
211 if (NSNumber *underlineStyle = attributes[NSUnderlineStyleAttributeName]) {
212 format.setFontUnderline(true);
213 NSUnderlineStyle style = underlineStyle.integerValue;
214 if (style & NSUnderlineStylePatternDot)
215 format.setUnderlineStyle(QTextCharFormat::DotLine);
216 else if (style & NSUnderlineStylePatternDash)
217 format.setUnderlineStyle(QTextCharFormat::DashUnderline);
218 else if (style & NSUnderlineStylePatternDashDot)
219 format.setUnderlineStyle(QTextCharFormat::DashDotLine);
220 if (style & NSUnderlineStylePatternDashDotDot)
221 format.setUnderlineStyle(QTextCharFormat::DashDotDotLine);
222 else
223 format.setUnderlineStyle(QTextCharFormat::SingleUnderline);
224
225 // Unfortunately QTextCharFormat::UnderlineStyle does not distinguish
226 // between NSUnderlineStyle{Single,Thick,Double}, which is used by CJK
227 // input methods to highlight the selected clause segments.
228 }
229 if (NSColor *underlineColor = attributes[NSUnderlineColorAttributeName])
230 format.setUnderlineColor(qt_mac_toQColor(underlineColor));
231 if (NSColor *foregroundColor = attributes[NSForegroundColorAttributeName])
232 format.setForeground(qt_mac_toQColor(foregroundColor));
233 if (NSColor *backgroundColor = attributes[NSBackgroundColorAttributeName])
234 format.setBackground(qt_mac_toQColor(backgroundColor));
235
236 if (format != QTextCharFormat()) {
237 preeditAttributes << QInputMethodEvent::Attribute(
238 QInputMethodEvent::TextFormat, range.location, range.length, format);
239 }
240
241 index = range.location + range.length;
242 }
243
244 // Ensure we have a valid replacement range
245 replacementRange = [self sanitizeReplacementRange:replacementRange];
246
247 // Qt's QInputMethodEvent has different semantics for the replacement
248 // range than AppKit does, so we need to sanitize the range first.
249 auto [replaceFrom, replaceLength] = [self inputMethodRangeForRange:replacementRange];
250
251 // Update the composition, now that we've computed the replacement range
252 m_composingText = preeditString;
253
254 if (QObject *focusObject = self.focusObject) {
255 m_composingFocusObject = focusObject;
256 if (queryInputMethod(focusObject)) {
257 QInputMethodEvent event(preeditString, preeditAttributes);
258 if (replaceLength > 0) {
259 // The input method may extend the preedit into already
260 // committed text. If so, we need to replace existing text
261 // by committing an empty string.
262 qCDebug(lcQpaKeys) << "Replacing from" << replaceFrom << "with length"
263 << replaceLength << "based on replacement range" << replacementRange;
264 event.setCommitString(QString(), replaceFrom, replaceLength);
265 }
266 QCoreApplication::sendEvent(focusObject, &event);
267 }
268 }
269}
270
271- (NSArray<NSString *> *)validAttributesForMarkedText
272{
273 return @[
274 NSUnderlineColorAttributeName,
275 NSUnderlineStyleAttributeName,
276 NSForegroundColorAttributeName,
277 NSBackgroundColorAttributeName
278 ];
279}
280
281- (BOOL)hasMarkedText
282{
283 return !m_composingText.isEmpty();
284}
285
286/*
287 Returns the range of marked text or {cursorPosition, 0} if there's none.
288
289 This maps to the location and length of the current preedit (composited) string.
290
291 The returned range measures from the start of the receiver’s text storage,
292 that is, from 0 to the document length.
293*/
294- (NSRange)markedRange
295{
296 if (auto queryResult = queryInputMethod(self.focusObject, Qt::ImAbsolutePosition)) {
297 int absoluteCursorPosition = queryResult.value(Qt::ImAbsolutePosition).toInt();
298
299 // The cursor position as reflected by Qt::ImAbsolutePosition is not
300 // affected by the offset of the cursor in the preedit area. That means
301 // that when composing text, the cursor position stays the same, at the
302 // preedit insertion point, regardless of where the cursor is positioned within
303 // the preedit string by the QInputMethodEvent::Cursor attribute. This means
304 // we can use the cursor position to determine the range of the marked text.
305
306 // The NSTextInputClient documentation says {NSNotFound, 0} should be returned if there
307 // is no marked text, but in practice NSTextView seems to report {cursorPosition, 0},
308 // so we do the same.
309 return NSMakeRange(absoluteCursorPosition, m_composingText.length());
310 } else {
311 return {NSNotFound, 0};
312 }
313}
314
315/*
316 Confirms the marked (composed) text.
317
318 The marked text is accepted as if it had been inserted normally,
319 and the preedit string is cleared.
320
321 If there is no marked text this method has no effect.
322*/
323- (void)unmarkText
324{
325 // FIXME: Match cancelComposingText in early exit and focus object handling
326
327 qCDebug(lcQpaKeys) << "Unmarking" << m_composingText
328 << "for focus object" << m_composingFocusObject;
329
330 if (!m_composingText.isEmpty()) {
331 QObject *focusObject = self.focusObject;
332 if (queryInputMethod(focusObject)) {
333 QInputMethodEvent e;
334 e.setCommitString(m_composingText);
335 QCoreApplication::sendEvent(focusObject, &e);
336 }
337 }
338
339 m_composingText.clear();
340 m_composingFocusObject = nullptr;
341}
342
343/*
344 Cancels composition.
345
346 The marked text is discarded, and the preedit string is cleared.
347
348 If there is no marked text this method has no effect.
349*/
350- (void)cancelComposingText
351{
352 if (m_composingText.isEmpty())
353 return;
354
355 qCDebug(lcQpaKeys) << "Canceling composition" << m_composingText
356 << "for focus object" << m_composingFocusObject;
357
358 if (queryInputMethod(m_composingFocusObject)) {
359 QInputMethodEvent e;
360 QCoreApplication::sendEvent(m_composingFocusObject, &e);
361 }
362
363 m_composingText.clear();
364 m_composingFocusObject = nullptr;
365}
366
367// ------------- Key binding command handling -------------
368
369- (void)doCommandBySelector:(SEL)selector
370{
371 // Note: if the selector cannot be invoked, then doCommandBySelector:
372 // should not pass this message up the responder chain (nor should it
373 // call super, as the NSResponder base class would in that case pass
374 // the message up the responder chain, which we don't want). We will
375 // pass the originating key event up the responder chain if applicable.
376
377 qCDebug(lcQpaKeys) << "Trying to perform command" << selector;
378 if (![self tryToPerform:selector with:self]) {
379 m_sendKeyEvent = true;
380
381 if (![NSStringFromSelector(selector) hasPrefix:@"insert"]) {
382 // The text input system determined that the key event was not
383 // meant for text insertion, and instead asked us to treat it
384 // as a (possibly noop) command. This typically happens for key
385 // events with either ⌘ or ⌃, function keys such as F1-F35,
386 // arrow keys, etc. We reflect that when sending the key event
387 // later on, by removing the text from the event, so that the
388 // event does not result in text insertion on the client side.
389 m_sendKeyEventWithoutText = true;
390 }
391 }
392}
393
394// ------------- Various text properties -------------
395
396/*
397 Returns the range of selected text, or {cursorPosition, 0} if there's none.
398
399 The returned range measures from the start of the receiver’s text storage,
400 that is, from 0 to the document length.
401*/
402- (NSRange)selectedRange
403{
404 if (auto queryResult = queryInputMethod(self.focusObject,
405 Qt::ImCursorPosition | Qt::ImAbsolutePosition | Qt::ImAnchorPosition)) {
406
407 // Unfortunately the Qt::InputMethodQuery values are all relative
408 // to the start of the current editing block (paragraph), but we
409 // need them in absolute values relative to the entire text.
410 // Luckily we have one property, Qt::ImAbsolutePosition, that
411 // we can use to compute the offset.
412 int cursorPosition = queryResult.value(Qt::ImCursorPosition).toInt();
413 int absoluteCursorPosition = queryResult.value(Qt::ImAbsolutePosition).toInt();
414 int absoluteOffset = absoluteCursorPosition - cursorPosition;
415
416 int anchorPosition = absoluteOffset + queryResult.value(Qt::ImAnchorPosition).toInt();
417 int selectionStart = anchorPosition >= absoluteCursorPosition ? absoluteCursorPosition : anchorPosition;
418 int selectionEnd = selectionStart == anchorPosition ? absoluteCursorPosition : anchorPosition;
419 int selectionLength = selectionEnd - selectionStart;
420
421 // Note: The cursor position as reflected by these properties are not
422 // affected by the offset of the cursor in the preedit area. That means
423 // that when composing text, the cursor position stays the same, at the
424 // preedit insertion point, regardless of where the cursor is positioned within
425 // the preedit string by the QInputMethodEvent::Cursor attribute.
426
427 // The NSTextInputClient documentation says {NSNotFound, 0} should be returned if there is no
428 // selection, but in practice NSTextView seems to report {cursorPosition, 0}, so we do the same.
429 return NSMakeRange(selectionStart, selectionLength);
430 } else {
431 return {NSNotFound, 0};
432 }
433}
434
435/*
436 Returns an attributed string derived from the given range
437 in the underlying focus object's text storage.
438
439 Input methods may call this with a proposed range that is
440 out of bounds. For example, the InkWell text input service
441 may ask for the contents of the text input client that extends
442 beyond the document's range. To remedy this we always compute
443 the intersection between the proposed range and the available
444 text.
445
446 If the intersection is completely outside of the available text
447 this method returns nil.
448*/
449- (NSAttributedString *)attributedSubstringForProposedRange:(NSRange)range actualRange:(NSRangePointer)actualRange
450{
451 if (auto queryResult = queryInputMethod(self.focusObject,
452 Qt::ImAbsolutePosition | Qt::ImTextBeforeCursor | Qt::ImTextAfterCursor)) {
453 const int absoluteCursorPosition = queryResult.value(Qt::ImAbsolutePosition).toInt();
454 const QString textBeforeCursor = queryResult.value(Qt::ImTextBeforeCursor).toString();
455 const QString textAfterCursor = queryResult.value(Qt::ImTextAfterCursor).toString();
456
457 // The documentation doesn't say whether the marked text should be included
458 // in the available text, but observing NSTextView shows that this is the
459 // case, so we follow suit.
460 const QString availableText = textBeforeCursor + m_composingText + textAfterCursor;
461 const NSRange availableRange = NSMakeRange(absoluteCursorPosition - textBeforeCursor.length(),
462 availableText.length());
463
464 const NSRange intersectedRange = NSIntersectionRange(range, availableRange);
465 if (actualRange)
466 *actualRange = intersectedRange;
467
468 if (!intersectedRange.length)
469 return nil;
470
471 NSString *substring = QStringView(availableText).mid(
472 intersectedRange.location - availableRange.location,
473 intersectedRange.length).toNSString();
474
475 return [[[NSAttributedString alloc] initWithString:substring] autorelease];
476
477 } else {
478 return nil;
479 }
480}
481
482/*
483 Returns the first logical boundary rectangle for characters in the given range,
484 in screen coordinates.
485
486 The "first" in the name refers to the rectangle enclosing the first line when
487 the range encompasses multiple lines of text. In that case, actualRange should
488 be set to the range covered by the first rect, so all line fragments can
489 be queried by invoking this method repeatedly.
490
491 If the length of range is 0 (as it would be if there is nothing selected at
492 the insertion point), then the rectangle coincides with the insertion point.
493*/
494- (NSRect)firstRectForCharacterRange:(NSRange)range actualRange:(NSRangePointer)actualRange
495{
496 Q_UNUSED(range);
497 Q_UNUSED(actualRange);
498
499 QWindow *window = m_platformWindow ? m_platformWindow->window() : nullptr;
500 if (window && queryInputMethod(window->focusObject())) {
501 if (range.length) // FIXME: Handle the case when range is non-zero
502 qCWarning(lcQpaKeys) << "Can't satisfy firstRectForCharacterRange for" << range;
503 QRect cursorRect = qApp->inputMethod()->cursorRectangle().toRect();
504 cursorRect.moveBottomLeft(window->mapToGlobal(cursorRect.bottomLeft()));
505 return QCocoaScreen::mapToNative(cursorRect);
506 } else {
507 return NSZeroRect;
508 }
509}
510
511- (NSUInteger)characterIndexForPoint:(NSPoint)point
512{
513 // We don't support cursor movements using mouse while composing.
514 Q_UNUSED(point);
515 return NSNotFound;
516}
517
518/*
519 Returns the window level of the text input.
520
521 This allows the input method to place its input panel
522 above the text input.
523*/
524- (NSInteger)windowLevel
525{
526 // The default level assumed by input methods is NSFloatingWindowLevel,
527 // but our NSWindow level could be higher than that for many reasons,
528 // including being set via QWindow::setFlags() or directly on the
529 // NSWindow, or because we're embedded into a native view hierarchy.
530 // Return the actual window level to account for this.
531 auto level = m_platformWindow ? m_platformWindow->nativeWindow().level
532 : NSNormalWindowLevel;
533
534 // The logic above only covers our own window though. In some cases,
535 // such as when a completer is active, the text input has a lower
536 // window level than another window that's also visible, and we don't
537 // want the input panel to be sandwiched between these two windows.
538 // Account for this by explicitly using NSPopUpMenuWindowLevel as
539 // the minimum window level, which corresponds to the highest level
540 // one can get via QWindow::setFlags(), except for Qt::ToolTip.
541 return qMax(level, NSPopUpMenuWindowLevel);
542}
543
544// ------------- Helper functions -------------
545
546/*
547 Sanitizes the replacement range, ensuring it's valid.
548
549 If \a range is not valid the range of the current
550 marked text will be used.
551
552 If there's no marked text the range of the current
553 selection will be used.
554
555 If there's no selection the range will be {cursorPosition, 0}.
556*/
557- (NSRange)sanitizeReplacementRange:(NSRange)range
558{
559 if (range.location != NSNotFound)
560 return range; // Use as is
561
562 // If the replacement range is not specified we are expected to compute
563 // the range ourselves, based on the current state of the input context.
564
565 const auto markedRange = [self markedRange];
566 const auto selectedRange = [self selectedRange];
567
568 if (markedRange.length)
569 return markedRange;
570 else if (selectedRange.length)
571 return selectedRange;
572 else
573 return markedRange; // Represents cursor position when length is 0
574
575}
576
577/*
578 Computes the QInputMethodEvent commit string range,
579 based on the NSTextInputClient replacement range.
580
581 The two APIs have different semantics.
582*/
583- (std::pair<long long, long long>)inputMethodRangeForRange:(NSRange)replacementRange
584{
585 long long replaceFrom = replacementRange.location;
586 long long replaceLength = replacementRange.length;
587
588 const auto markedRange = [self markedRange];
589 const auto selectedRange = [self selectedRange];
590
591 if (markedRange.length && selectedRange.length) {
592 // We assume below that we have either marked text or selected text
593 qCWarning(lcQpaKeys) << "Got both markedRange" << markedRange
594 << "and selectedRange" << selectedRange;
595 }
596
597 if (markedRange.length) {
598 // The replacement length of QInputMethodEvent already includes
599 // the preedit string, as the documentation says that "When doing
600 // replacement, the area of the preedit string is ignored".
601 replaceLength -= markedRange.length;
602
603 // The QInputMethodEvent replacement start is relative to the start
604 // of the marked text (the location of the preedit string).
605 replaceFrom -= markedRange.location;
606 } else if (selectedRange.length) {
607 if (!NSEqualRanges(NSIntersectionRange(replacementRange, selectedRange), selectedRange)) {
608 qCWarning(lcQpaKeys) << "Replacement range" << replacementRange
609 << "is a subset of selection" << selectedRange;
610 // FIXME: To support this case we would need to extract parts of the
611 // selection into the committed text. But for now we ignore it, as we
612 // don't know if it happens in practice.
613 }
614
615 // Our input method protocol specifies that the entire selection
616 // should be removed as the first step, and the replacement length
617 // of the QInputMethodEvent refers to any additional text that should
618 // be removed/replaced.
619 replaceLength -= selectedRange.length;
620
621 // Once the selection has been removed the cursor position will be
622 // at the leftmost point of the selection, regardless of whether the
623 // cursor was at the start or end of the selection. The replacement
624 // start of QInputMethodEvent should be relative to this position.
625 replaceFrom -= selectedRange.location;
626 } else if (markedRange.location != NSNotFound) {
627 // The QInputMethodEvent replacement start is relative to the cursor
628 // position.
629 replaceFrom -= markedRange.location;
630 } else{
631 replaceFrom = 0;
632 }
633
634 // What we're left with is any _additional_ replacement.
635 // Make sure it's valid before passing it on.
636 replaceLength = qMax(0ll, replaceLength);
637
638 return {replaceFrom, replaceLength};
639}
640
641- (NSString*)stringForText:(id)text
642{
643 return [text isKindOfClass:NSAttributedString.class] ? [text string] : text;
644}
645
646@end
647
648@implementation QNSView (ServicesMenu)
649
650// Support for reading and writing from service menu pasteboards. If the text
651// input client supports returning the selection as a QMimeData we can convert
652// that to rich text. Otherwise we fall back to plain text, which means that we
653// lose any styling the selection might have when fed through a service that
654// changes the text.
655
656- (id)validRequestorForSendType:(NSPasteboardType)sendType returnType:(NSPasteboardType)returnType
657{
658 if (auto queryResult = queryInputMethod(self.focusObject, Qt::ImReadOnly | Qt::ImCurrentSelection)) {
659 bool canWriteToPasteboard = false;
660 bool canReadFromPastboard = false;
661
662 auto currentSelection = queryResult.value(Qt::ImCurrentSelection);
663 if (auto *mimeData = currentSelection.value<QMimeData*>()) {
664 // If the client reports the selection as mime-data we assume
665 // it can also insert mime-data via QInputMethodEvent::MimeData
666 auto scope = QUtiMimeConverter::HandlerScopeFlag::Clipboard;
667 auto availableConverters = QMacMimeRegistry::all(scope);
668 auto sendUti = [self utiForPasteboardType:sendType];
669 auto returnUti = [self utiForPasteboardType:returnType];
670 const auto mimeFormats = mimeData->formats();
671 for (const auto *c : availableConverters) {
672 if (mimeFormats.contains(c->mimeForUti(sendUti)))
673 canWriteToPasteboard = true;
674 if (mimeFormats.contains(c->mimeForUti(returnUti)))
675 canReadFromPastboard = true;
676 if (canWriteToPasteboard && canReadFromPastboard)
677 break; // No need to continue looking
678 }
679 } else {
680 canWriteToPasteboard = [sendType isEqualToString:NSPasteboardTypeString]
681 && !currentSelection.toString().isEmpty();
682 canReadFromPastboard = [returnType isEqualToString:NSPasteboardTypeString]
683 && !queryResult.value(Qt::ImReadOnly).toBool();
684 }
685
686 if (!((sendType && !canWriteToPasteboard) || (returnType && !canReadFromPastboard))) {
687 qCDebug(lcQpaServices) << "Accepting service interaction for send" << sendType << "and receive" << returnType;
688 return self;
689 }
690 }
691
692 return [super validRequestorForSendType:sendType returnType:returnType];
693}
694
695- (BOOL)writeSelectionToPasteboard:(NSPasteboard *)pasteboard types:(NSArray<NSPasteboardType> *)types
696{
697 bool didWrite = false;
698
699 if (auto queryResult = queryInputMethod(self.focusObject, Qt::ImCurrentSelection)) {
700 auto currentSelection = queryResult.value(Qt::ImCurrentSelection);
701 if (auto *mimeData = currentSelection.value<QMimeData*>()) {
702 auto mimeFormats = mimeData->formats();
703 auto scope = QUtiMimeConverter::HandlerScopeFlag::Clipboard;
704 auto availableConverters = QMacMimeRegistry::all(scope);
705 for (NSPasteboardType type in types) {
706 auto uti = [self utiForPasteboardType:type];
707 if (uti.isEmpty()) {
708 qCWarning(lcQpaServices) << "Did not find UTI for type" << type;
709 continue;
710 }
711 for (const auto *converter : availableConverters) {
712 auto mime = converter->mimeForUti(uti);
713 if (mimeFormats.contains(mime)) {
714 auto utiDataList = converter->convertFromMime(mime,
715 mimeData->data(mime), uti);
716 if (utiDataList.isEmpty())
717 continue;
718 auto utiData = utiDataList.first();
719 qCDebug(lcQpaServices) << "Writing" << utiData << "to service pasteboard"
720 << "with UTI" << uti << "for type" << type << "based on mime" << mime;
721 didWrite |= [pasteboard setData:utiData.toNSData() forType:type];
722 break;
723 }
724 }
725 }
726 }
727
728 // Try plain text fallback if we didn't have QMimeData, or didn't write anything
729 if (!didWrite && ([types containsObject:NSPasteboardTypeString]
730 || QT_IGNORE_DEPRECATIONS([types containsObject:NSStringPboardType]))) {
731 auto selectedText = currentSelection.toString();
732 qCDebug(lcQpaServices) << "Writing" << selectedText << "to service pasteboard"
733 << "as pain text" << "for type" << NSPasteboardTypeString;
734 didWrite |= [pasteboard writeObjects:@[ selectedText.toNSString() ]];
735 }
736 }
737
738 return didWrite;
739}
740
741- (BOOL)readSelectionFromPasteboard:(NSPasteboard *)pasteboard
742{
743 if (queryInputMethod(self.focusObject)) {
744 auto scope = QUtiMimeConverter::HandlerScopeFlag::Clipboard;
745 QMacPasteboard macPasteboard(CFStringRef(pasteboard.name), scope);
746 auto *mimeData = macPasteboard.mimeData();
747 if (mimeData->formats().isEmpty()) {
748 qCWarning(lcQpaServices) << "Failed to resolve mime data from" << pasteboard.types;
749 return NO;
750 }
751
752 qCDebug(lcQpaServices) << "Replacing selected range" << [self selectedRange]
753 << "with mime data" << [&]() {
754 QMap<QString, QByteArray> formatMap;
755 for (const auto &format : mimeData->formats())
756 formatMap.insert(format, mimeData->data(format));
757 return formatMap;
758 }() << "from service pasteboard" << pasteboard.name;
759
760 QList<QInputMethodEvent::Attribute> attributes;
761 attributes << QInputMethodEvent::Attribute(
762 QInputMethodEvent::MimeData,
763 0, 0, QVariant::fromValue(mimeData));
764
765 QInputMethodEvent inputMethodEvent(QString(), attributes);
766 // Pass the plain text data as the commit string, for clients
767 // that don't know how to handle the new MimeData attribute.
768 // This also ensures that we clear the existing selected text.
769 inputMethodEvent.setCommitString(mimeData->text());
770 QCoreApplication::sendEvent(self.focusObject, &inputMethodEvent);
771 return YES;
772 } else {
773 return NO;
774 }
775}
776
777- (QString)utiForPasteboardType:(NSPasteboardType)pasteboardType
778{
779 if (!pasteboardType)
780 return QString();
781
782 UTType *uttype = [UTType typeWithIdentifier:pasteboardType];
783 if (!uttype) {
784 // Although NSPasteboard types are declared as obsolete
785 // we still get callbacks for these types. As these types
786 // are not UTIs, we need to resolve the underlying UTI
787 // ourselves.
788 uttype = [UTType typeWithTag:pasteboardType
789 tagClass:QT_IGNORE_DEPRECATIONS((NSString*)kUTTagClassNSPboardType)
790 conformingToType:nil];
791 }
792 return QString::fromNSString(uttype.identifier);
793}
794
795@end
796
797#if QT_MACOS_PLATFORM_SDK_EQUAL_OR_ABOVE(150000)
798@implementation QNSView (ContentSelectionInfo)
799
800/*
801 This method is used by AppKit for positioning of context menus in
802 response to the context menu keyboard hotkey, and for placement of
803 the Writing Tools popup.
804*/
805- (NSRect)selectionAnchorRect
806{
807 if (queryInputMethod(self.focusObject)) {
808 // We don't have a way of querying the selection rectangle via
809 // the input method protocol (yet), so we use crude heuristics.
810 const auto *inputMethod = qApp->inputMethod();
811 auto cursorRect = inputMethod->cursorRectangle();
812 auto anchorRect = inputMethod->anchorRectangle();
813 auto selectionRect = cursorRect.united(anchorRect);
814 if (cursorRect.top() != anchorRect.top()) {
815 // Multi line selection. Assume the selections extends to
816 // the entire width of the input item. This does not account
817 // for center-aligned text and a bunch of other cases. FIXME
818 auto itemClipRect = inputMethod->inputItemClipRectangle();
819 selectionRect.setLeft(itemClipRect.left());
820 selectionRect.setRight(itemClipRect.right());
821 }
822 return selectionRect.toCGRect();
823 } else {
824 return NSZeroRect;
825 }
826}
827@end
828#endif // macOS 15 SDK
\macro QT_RESTRICTED_CAST_FROM_ASCII
Definition qstring.h:177
unsigned long NSUInteger
long NSInteger
Q_FORWARD_DECLARE_OBJC_CLASS(NSString)