1/* 2Copyright (C) 2011 by Mike McQuaid 3Copyright (C) 2018-2021 by Jonas Kvinge <jonas@jkvinge.net> 4 5Permission is hereby granted, free of charge, to any person obtaining a copy 6of this software and associated documentation files (the "Software"), to deal 7in the Software without restriction, including without limitation the rights 8to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9copies of the Software, and to permit persons to whom the Software is 10furnished to do so, subject to the following conditions: 11 12The above copyright notice and this permission notice shall be included in 13all copies or substantial portions of the Software. 14 15THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21THE SOFTWARE. 22*/ 23 24#include "qsearchfield.h" 25#include "qocoa_mac.h" 26 27#import "Foundation/NSAutoreleasePool.h" 28#import "Foundation/NSNotification.h" 29#import "AppKit/NSSearchField.h" 30 31#include <QApplication> 32#include <QWindow> 33#include <QString> 34#include <QClipboard> 35#include <QBoxLayout> 36#include <QShowEvent> 37#include <QKeyEvent> 38 39class QSearchFieldPrivate : public QObject { 40public: 41 QSearchFieldPrivate(QSearchField *qSearchField, NSSearchField *nsSearchField) 42 : QObject(qSearchField), qSearchField(qSearchField), nsSearchField(nsSearchField) {} 43 44 void textDidChange(const QString &text) { 45 if (qSearchField) emit qSearchField->textChanged(text); 46 } 47 48 void textDidEndEditing() { 49 if (qSearchField) 50 emit qSearchField->editingFinished(); 51 } 52 53 void returnPressed() { 54 if (qSearchField) { 55 emit qSearchField->returnPressed(); 56 QKeyEvent *event = new QKeyEvent(QEvent::KeyPress, Qt::Key_Return, Qt::NoModifier); 57 QApplication::postEvent(qSearchField, event); 58 } 59 } 60 61 void keyDownPressed() { 62 if (qSearchField) { 63 QKeyEvent *event = new QKeyEvent(QEvent::KeyPress, Qt::Key_Down, Qt::NoModifier); 64 QApplication::postEvent(qSearchField, event); 65 } 66 } 67 68 void keyUpPressed() { 69 if (qSearchField) { 70 QKeyEvent *event = new QKeyEvent(QEvent::KeyPress, Qt::Key_Up, Qt::NoModifier); 71 QApplication::postEvent(qSearchField, event); 72 } 73 } 74 75 QPointer<QSearchField> qSearchField; 76 NSSearchField *nsSearchField; 77 78}; 79 80@interface QSearchFieldDelegate : NSObject<NSTextFieldDelegate> { 81@public 82 QPointer<QSearchFieldPrivate> pimpl; 83} 84-(void)controlTextDidChange:(NSNotification*)notification; 85-(void)controlTextDidEndEditing:(NSNotification*)notification; 86@end 87 88@implementation QSearchFieldDelegate 89-(void)controlTextDidChange:(NSNotification*)notification { 90 Q_ASSERT(pimpl); 91 if (pimpl) pimpl->textDidChange(toQString([[notification object] stringValue])); 92} 93 94-(void)controlTextDidEndEditing:(NSNotification*)notification { 95 Q_UNUSED(notification); 96 // No Q_ASSERT here as it is called on destruction. 97 if (!pimpl) return; 98 pimpl->textDidEndEditing(); 99 if ([[[notification userInfo] objectForKey:@"NSTextMovement"] intValue] == NSReturnTextMovement) 100 pimpl->returnPressed(); 101} 102 103-(BOOL)control: (NSControl*)control textView: (NSTextView*)textView doCommandBySelector: (SEL)commandSelector { 104 Q_UNUSED(control); 105 Q_UNUSED(textView); 106 Q_ASSERT(pimpl); 107 if (!pimpl) return NO; 108 if (commandSelector == @selector(moveDown:)) { 109 pimpl->keyDownPressed(); 110 return YES; 111 } 112 else if (commandSelector == @selector(moveUp:)) { 113 pimpl->keyUpPressed(); 114 return YES; 115 } 116 return NO; 117} 118 119@end 120 121@interface QocoaSearchField : NSSearchField 122-(BOOL)performKeyEquivalent:(NSEvent*)event; 123@end 124 125@implementation QocoaSearchField 126-(BOOL)performKeyEquivalent:(NSEvent*)event { 127 // First, check if we have the focus. 128 // If no, it probably means this event isn't for us. 129 NSResponder *firstResponder = [[NSApp keyWindow] firstResponder]; 130 if ([firstResponder isKindOfClass:[NSText class]] && (NSSearchField*)([(NSText*)firstResponder delegate]) == self) { 131 132 if ([event type] == NSEventTypeKeyDown && [event modifierFlags] & NSEventModifierFlagCommand) { 133 QString keyString = toQString([event characters]); 134 if (keyString == "a") // Cmd+a 135 { 136 [self performSelector:@selector(selectText:)]; 137 return YES; 138 } 139 else if (keyString == "c") // Cmd+c 140 { 141 [[self currentEditor] copy: nil]; 142 return YES; 143 } 144 else if (keyString == "v") // Cmd+v 145 { 146 [[self currentEditor] paste: nil]; 147 return YES; 148 } 149 else if (keyString == "x") // Cmd+x 150 { 151 [[self currentEditor] cut: nil]; 152 return YES; 153 } 154 } 155 } 156 157 return NO; 158} 159@end 160 161QSearchField::QSearchField(QWidget *parent) : QWidget(parent) { 162 163 NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; 164 NSSearchField *search = [[QocoaSearchField alloc] init]; 165 QSearchFieldDelegate *delegate = [[QSearchFieldDelegate alloc] init]; 166 pimpl = delegate->pimpl = new QSearchFieldPrivate(this, search); 167 [search setDelegate:(id<NSSearchFieldDelegate>)delegate]; 168 169 new QVBoxLayout(this); 170 layout()->setContentsMargins(0, 0, 0, 0); 171 setAttribute(Qt::WA_NativeWindow); 172 setFixedHeight(24); 173 setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); 174 175 [pool drain]; 176 177} 178 179void QSearchField::setIconSize(const int iconsize) { 180 Q_UNUSED(iconsize); 181} 182 183void QSearchField::setText(const QString &text) { 184 Q_ASSERT(pimpl); 185 if (!pimpl) return; 186 187 NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; 188 [pimpl->nsSearchField setStringValue:fromQString(text)]; 189 if (!text.isEmpty()) { 190 [pimpl->nsSearchField selectText:pimpl->nsSearchField]; 191 [[pimpl->nsSearchField currentEditor] setSelectedRange:NSMakeRange([[pimpl->nsSearchField stringValue] length], 0)]; 192 } 193 [pool drain]; 194} 195 196void QSearchField::setPlaceholderText(const QString &text) { 197 Q_ASSERT(pimpl); 198 if (!pimpl) return; 199 NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; 200 [[pimpl->nsSearchField cell] setPlaceholderString:fromQString(text)]; 201 [pool drain]; 202} 203 204void QSearchField::clear() { 205 Q_ASSERT(pimpl); 206 if (!pimpl) return; 207 [pimpl->nsSearchField setStringValue:@""]; 208 emit textChanged(QString()); 209} 210 211void QSearchField::selectAll() { 212 Q_ASSERT(pimpl); 213 if (!pimpl) return; 214 [pimpl->nsSearchField performSelector:@selector(selectText:)]; 215} 216 217QString QSearchField::text() const { 218 Q_ASSERT(pimpl); 219 if (!pimpl) return QString(); 220 return toQString([pimpl->nsSearchField stringValue]); 221} 222 223QString QSearchField::placeholderText() const { 224 Q_ASSERT(pimpl); 225 return toQString([[pimpl->nsSearchField cell] placeholderString]); 226} 227 228void QSearchField::setFocus(Qt::FocusReason) {} 229 230void QSearchField::setFocus() { 231 setFocus(Qt::OtherFocusReason); 232} 233 234void QSearchField::showEvent(QShowEvent *e) { 235 236 if (!e->spontaneous()) { 237 for (int i = 0; i < layout()->count(); ++i) { 238 QWidget *widget = layout()->itemAt(i)->widget(); 239 layout()->removeWidget(widget); 240 delete widget; 241 } 242 layout()->addWidget(QWidget::createWindowContainer(QWindow::fromWinId(WId(pimpl->nsSearchField)), this)); 243 } 244 245 QWidget::showEvent(e); 246 247} 248 249void QSearchField::resizeEvent(QResizeEvent *resizeEvent) { 250 QWidget::resizeEvent(resizeEvent); 251} 252 253bool QSearchField::eventFilter(QObject *o, QEvent *e) { 254 return QWidget::eventFilter(o, e); 255} 256