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