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