1 /**************************************************************************
2 * Otter Browser: Web browser controlled by the user, not vice-versa.
3 * Copyright (C) 2015 - 2017 Michal Dutkiewicz aka Emdek <michal@emdek.pl>
4 * Copyright (C) 2017 Jan Bajer aka bajasoft <jbajer@gmail.com>
5 *
6 * This program is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation, either version 3 of the License, or
9 * (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program. If not, see <http://www.gnu.org/licenses/>.
18 *
19 **************************************************************************/
20
21 #include "LineEditWidget.h"
22 #include "Action.h"
23 #include "MainWindow.h"
24 #include "ToolBarWidget.h"
25 #include "../core/NotesManager.h"
26
27 #include <QtCore/QMimeData>
28 #include <QtCore/QStringListModel>
29 #include <QtCore/QTimer>
30 #include <QtGui/QClipboard>
31 #include <QtWidgets/QApplication>
32 #include <QtWidgets/QMenu>
33
34 namespace Otter
35 {
36
PopupViewWidget(LineEditWidget * parent)37 PopupViewWidget::PopupViewWidget(LineEditWidget *parent) : ItemViewWidget(nullptr),
38 m_lineEditWidget(parent)
39 {
40 setEditTriggers(QAbstractItemView::NoEditTriggers);
41 setFocusPolicy(Qt::NoFocus);
42 setWindowFlags(Qt::Popup);
43 header()->setStretchLastSection(true);
44 header()->hide();
45 viewport()->setAttribute(Qt::WA_Hover);
46 viewport()->setMouseTracking(true);
47 viewport()->installEventFilter(this);
48
49 connect(this, &PopupViewWidget::needsActionsUpdate, this, &PopupViewWidget::updateHeight);
50 connect(this, &PopupViewWidget::entered, this, &PopupViewWidget::handleIndexEntered);
51 }
52
keyPressEvent(QKeyEvent * event)53 void PopupViewWidget::keyPressEvent(QKeyEvent *event)
54 {
55 if (!m_lineEditWidget)
56 {
57 ItemViewWidget::keyPressEvent(event);
58
59 return;
60 }
61
62 m_lineEditWidget->event(event);
63
64 if (event->isAccepted())
65 {
66 if (!m_lineEditWidget->hasFocus())
67 {
68 m_lineEditWidget->hidePopup();
69 }
70
71 return;
72 }
73
74 switch (event->key())
75 {
76 case Qt::Key_Up:
77 case Qt::Key_Down:
78 case Qt::Key_PageUp:
79 case Qt::Key_PageDown:
80 case Qt::Key_End:
81 ItemViewWidget::keyPressEvent(event);
82
83 return;
84 case Qt::Key_Enter:
85 case Qt::Key_Return:
86 case Qt::Key_Tab:
87 case Qt::Key_Backtab:
88 case Qt::Key_Escape:
89 case Qt::Key_F4:
90 if (event->key() == Qt::Key_F4 && !event->modifiers().testFlag(Qt::AltModifier))
91 {
92 break;
93 }
94
95 if (event->key() == Qt::Key_Enter || event->key() == Qt::Key_Return)
96 {
97 const QModelIndex index(getCurrentIndex());
98
99 if (index.isValid())
100 {
101 emit clicked(index);
102 }
103 }
104
105 m_lineEditWidget->hidePopup();
106
107 break;
108 default:
109 break;
110 }
111 }
112
handleIndexEntered(const QModelIndex & index)113 void PopupViewWidget::handleIndexEntered(const QModelIndex &index)
114 {
115 if (index.isValid())
116 {
117 setCurrentIndex(index);
118 }
119
120 QStatusTipEvent statusTipEvent(index.data(Qt::StatusTipRole).toString());
121
122 QApplication::sendEvent(m_lineEditWidget, &statusTipEvent);
123 }
124
updateHeight()125 void PopupViewWidget::updateHeight()
126 {
127 int completionHeight(5);
128 const int rowCount(qMin(20, getRowCount()));
129
130 for (int i = 0; i < rowCount; ++i)
131 {
132 completionHeight += sizeHintForRow(i);
133 }
134
135 setFixedHeight(completionHeight);
136
137 viewport()->setFixedHeight(completionHeight - 3);
138 }
139
event(QEvent * event)140 bool PopupViewWidget::event(QEvent *event)
141 {
142 switch (event->type())
143 {
144 case QEvent::Close:
145 case QEvent::Hide:
146 case QEvent::Leave:
147 if (m_lineEditWidget)
148 {
149 QString statusTip;
150 QStatusTipEvent statusTipEvent(statusTip);
151
152 QApplication::sendEvent(m_lineEditWidget, &statusTipEvent);
153 }
154
155 break;
156 case QEvent::InputMethod:
157 case QEvent::ShortcutOverride:
158 if (m_lineEditWidget)
159 {
160 QApplication::sendEvent(m_lineEditWidget, event);
161 }
162
163 break;
164 case QEvent::MouseButtonPress:
165 if (m_lineEditWidget && !viewport()->underMouse())
166 {
167 m_lineEditWidget->hidePopup();
168 }
169
170 break;
171 default:
172 break;
173 }
174
175 return ItemViewWidget::event(event);
176 }
177
LineEditWidget(const QString & text,QWidget * parent)178 LineEditWidget::LineEditWidget(const QString &text, QWidget *parent) : QLineEdit(text, parent), ActionExecutor(),
179 m_popupViewWidget(nullptr),
180 m_completer(nullptr),
181 m_dropMode(PasteDropMode),
182 m_selectionStart(-1),
183 m_shouldClearOnEscape(false),
184 m_shouldIgnoreCompletion(false),
185 m_shouldSelectAllOnFocus(false),
186 m_shouldSelectAllOnRelease(false),
187 m_hadSelection(false),
188 m_wasEmpty(text.isEmpty())
189 {
190 initialize();
191 }
192
LineEditWidget(QWidget * parent)193 LineEditWidget::LineEditWidget(QWidget *parent) : QLineEdit(parent),
194 m_popupViewWidget(nullptr),
195 m_completer(nullptr),
196 m_dropMode(PasteDropMode),
197 m_selectionStart(-1),
198 m_shouldClearOnEscape(false),
199 m_shouldIgnoreCompletion(false),
200 m_shouldSelectAllOnFocus(false),
201 m_shouldSelectAllOnRelease(false),
202 m_hadSelection(false),
203 m_wasEmpty(true)
204 {
205 initialize();
206 }
207
~LineEditWidget()208 LineEditWidget::~LineEditWidget()
209 {
210 if (m_popupViewWidget)
211 {
212 m_popupViewWidget->deleteLater();
213 }
214 }
215
initialize()216 void LineEditWidget::initialize()
217 {
218 setDragEnabled(true);
219
220 connect(this, &LineEditWidget::selectionChanged, this, &LineEditWidget::handleSelectionChanged);
221 connect(this, &LineEditWidget::textChanged, this, &LineEditWidget::handleTextChanged);
222 connect(QGuiApplication::clipboard(), &QClipboard::dataChanged, this, &LineEditWidget::notifyPasteActionStateChanged);
223 }
224
resizeEvent(QResizeEvent * event)225 void LineEditWidget::resizeEvent(QResizeEvent *event)
226 {
227 QLineEdit::resizeEvent(event);
228
229 if (m_popupViewWidget)
230 {
231 m_popupViewWidget->move(mapToGlobal(contentsRect().bottomLeft()));
232 m_popupViewWidget->setFixedWidth(width());
233 }
234 }
235
focusInEvent(QFocusEvent * event)236 void LineEditWidget::focusInEvent(QFocusEvent *event)
237 {
238 MainWindow *mainWindow(MainWindow::findMainWindow(this));
239
240 if (mainWindow)
241 {
242 mainWindow->setActiveEditorExecutor(ActionExecutor::Object(this, this));
243 }
244
245 QLineEdit::focusInEvent(event);
246 }
247
keyPressEvent(QKeyEvent * event)248 void LineEditWidget::keyPressEvent(QKeyEvent *event)
249 {
250 switch (event->key())
251 {
252 case Qt::Key_Backspace:
253 case Qt::Key_Delete:
254 if (!m_completion.isEmpty())
255 {
256 m_shouldIgnoreCompletion = true;
257 }
258
259 break;
260 case Qt::Key_Escape:
261 if (m_shouldClearOnEscape)
262 {
263 clear();
264 }
265
266 break;
267 default:
268 break;
269 }
270
271 QLineEdit::keyPressEvent(event);
272 }
273
contextMenuEvent(QContextMenuEvent * event)274 void LineEditWidget::contextMenuEvent(QContextMenuEvent *event)
275 {
276 ActionExecutor::Object executor(this, this);
277 QMenu menu(this);
278 menu.addAction(new Action(ActionsManager::UndoAction, {}, executor, &menu));
279 menu.addAction(new Action(ActionsManager::RedoAction, {}, executor, &menu));
280 menu.addSeparator();
281 menu.addAction(new Action(ActionsManager::CutAction, {}, executor, &menu));
282 menu.addAction(new Action(ActionsManager::CopyAction, {}, executor, &menu));
283 menu.addAction(new Action(ActionsManager::PasteAction, {}, executor, &menu));
284 menu.addAction(new Action(ActionsManager::DeleteAction, {}, executor, &menu));
285 menu.addSeparator();
286 menu.addAction(new Action(ActionsManager::CopyToNoteAction, {}, executor, &menu));
287 menu.addSeparator();
288 menu.addAction(new Action(ActionsManager::ClearAllAction, {}, executor, &menu));
289 menu.addAction(new Action(ActionsManager::SelectAllAction, {}, executor, &menu));
290
291 const ToolBarWidget *toolBar(qobject_cast<ToolBarWidget*>(parentWidget()));
292
293 if (toolBar)
294 {
295 menu.addSeparator();
296 menu.addMenu(ToolBarWidget::createCustomizationMenu(toolBar->getIdentifier(), {}, &menu));
297 }
298
299 menu.exec(event->globalPos());
300 }
301
mousePressEvent(QMouseEvent * event)302 void LineEditWidget::mousePressEvent(QMouseEvent *event)
303 {
304 if (event->button() == Qt::LeftButton)
305 {
306 m_selectionStart = selectionStart();
307 }
308
309 QLineEdit::mousePressEvent(event);
310 }
311
mouseReleaseEvent(QMouseEvent * event)312 void LineEditWidget::mouseReleaseEvent(QMouseEvent *event)
313 {
314 if (m_shouldSelectAllOnRelease)
315 {
316 disconnect(this, &LineEditWidget::selectionChanged, this, &LineEditWidget::clearSelectAllOnRelease);
317
318 selectAll();
319
320 m_shouldSelectAllOnRelease = false;
321 }
322
323 QLineEdit::mouseReleaseEvent(event);
324 }
325
dropEvent(QDropEvent * event)326 void LineEditWidget::dropEvent(QDropEvent *event)
327 {
328 if (m_selectionStart >= 0)
329 {
330 const int selectionEnd(m_selectionStart + event->mimeData()->text().length());
331 int dropPosition(cursorPositionAt(event->pos()));
332
333 if (dropPosition < m_selectionStart || dropPosition > selectionEnd)
334 {
335 if (dropPosition > selectionEnd)
336 {
337 dropPosition -= event->mimeData()->text().length();
338 }
339
340 setSelection(m_selectionStart, event->mimeData()->text().length());
341 del();
342 setCursorPosition(dropPosition);
343 insert(event->mimeData()->text());
344 }
345 }
346 else if (m_dropMode == ReplaceDropMode || m_dropMode == ReplaceAndNotifyDropMode)
347 {
348 clear();
349 insert(event->mimeData()->text());
350
351 if (m_dropMode == ReplaceAndNotifyDropMode)
352 {
353 emit textDropped(event->mimeData()->text());
354 }
355 }
356 else
357 {
358 QLineEdit::dropEvent(event);
359 }
360 }
361
triggerAction(int identifier,const QVariantMap & parameters,ActionsManager::TriggerType trigger)362 void LineEditWidget::triggerAction(int identifier, const QVariantMap ¶meters, ActionsManager::TriggerType trigger)
363 {
364 Q_UNUSED(trigger)
365
366 switch (identifier)
367 {
368 case ActionsManager::UndoAction:
369 if (!isReadOnly())
370 {
371 undo();
372 }
373
374 break;
375 case ActionsManager::RedoAction:
376 if (!isReadOnly())
377 {
378 redo();
379 }
380
381 break;
382 case ActionsManager::CutAction:
383 if (!isReadOnly())
384 {
385 cut();
386 }
387
388 break;
389 case ActionsManager::CopyAction:
390 copy();
391
392 break;
393 case ActionsManager::CopyToNoteAction:
394 {
395 const QString text(hasSelectedText() ? selectedText() : this->text());
396
397 if (!text.isEmpty())
398 {
399 NotesManager::addNote(BookmarksModel::UrlBookmark, {{BookmarksModel::DescriptionRole, text}});
400 }
401 }
402
403 break;
404 case ActionsManager::PasteAction:
405 if (!isReadOnly())
406 {
407 if (parameters.contains(QLatin1String("note")))
408 {
409 const BookmarksModel::Bookmark *bookmark(NotesManager::getModel()->getBookmark(parameters[QLatin1String("note")].toULongLong()));
410
411 if (bookmark)
412 {
413 insert(bookmark->getDescription());
414 }
415 }
416 else if (parameters.contains(QLatin1String("text")))
417 {
418 insert(parameters[QLatin1String("text")].toString());
419 }
420 else
421 {
422 paste();
423 }
424 }
425
426 break;
427 case ActionsManager::DeleteAction:
428 if (!isReadOnly())
429 {
430 del();
431 }
432
433 break;
434 case ActionsManager::SelectAllAction:
435 selectAll();
436
437 break;
438 case ActionsManager::UnselectAction:
439 deselect();
440
441 break;
442 case ActionsManager::ClearAllAction:
443 if (!isReadOnly())
444 {
445 clear();
446 }
447
448 break;
449 default:
450 break;
451 }
452 }
453
clearSelectAllOnRelease()454 void LineEditWidget::clearSelectAllOnRelease()
455 {
456 if (m_shouldSelectAllOnRelease && hasSelectedText())
457 {
458 disconnect(this, &LineEditWidget::selectionChanged, this, &LineEditWidget::clearSelectAllOnRelease);
459
460 m_shouldSelectAllOnRelease = false;
461 }
462 }
463
activate(Qt::FocusReason reason)464 void LineEditWidget::activate(Qt::FocusReason reason)
465 {
466 if (!hasFocus() && isEnabled() && focusPolicy() != Qt::NoFocus)
467 {
468 setFocus(reason);
469
470 return;
471 }
472
473 if (!text().trimmed().isEmpty())
474 {
475 if (m_shouldSelectAllOnFocus && reason == Qt::MouseFocusReason)
476 {
477 m_shouldSelectAllOnRelease = true;
478
479 connect(this, &LineEditWidget::selectionChanged, this, &LineEditWidget::clearSelectAllOnRelease);
480
481 return;
482 }
483
484 if (m_shouldSelectAllOnFocus && (reason == Qt::ShortcutFocusReason || reason == Qt::TabFocusReason || reason == Qt::BacktabFocusReason))
485 {
486 QTimer::singleShot(0, this, &LineEditWidget::selectAll);
487 }
488 else if (reason != Qt::PopupFocusReason)
489 {
490 deselect();
491 }
492 }
493 }
494
showPopup()495 void LineEditWidget::showPopup()
496 {
497 if (!m_popupViewWidget)
498 {
499 getPopup();
500 }
501
502 m_popupViewWidget->updateHeight();
503 m_popupViewWidget->move(mapToGlobal(contentsRect().bottomLeft()));
504 m_popupViewWidget->setFixedWidth(width());
505 m_popupViewWidget->setFocusProxy(this);
506 m_popupViewWidget->show();
507 }
508
hidePopup()509 void LineEditWidget::hidePopup()
510 {
511 if (m_popupViewWidget)
512 {
513 m_popupViewWidget->hide();
514 m_popupViewWidget->deleteLater();
515 m_popupViewWidget = nullptr;
516
517 QString statusTip;
518 QStatusTipEvent statusTipEvent(statusTip);
519
520 QApplication::sendEvent(this, &statusTipEvent);
521 }
522 }
523
handleSelectionChanged()524 void LineEditWidget::handleSelectionChanged()
525 {
526 if (hasSelectedText() != m_hadSelection)
527 {
528 m_hadSelection = hasSelectedText();
529
530 emit arbitraryActionsStateChanged({ActionsManager::CutAction, ActionsManager::CopyAction, ActionsManager::CopyToNoteAction, ActionsManager::PasteAction, ActionsManager::DeleteAction, ActionsManager::UnselectAction});
531 }
532 }
533
handleTextChanged(const QString & text)534 void LineEditWidget::handleTextChanged(const QString &text)
535 {
536 if (text.isEmpty() != m_wasEmpty)
537 {
538 m_wasEmpty = text.isEmpty();
539
540 emit arbitraryActionsStateChanged({ActionsManager::UndoAction, ActionsManager::RedoAction, ActionsManager::SelectAllAction, ActionsManager::ClearAllAction});
541 }
542 }
543
notifyPasteActionStateChanged()544 void LineEditWidget::notifyPasteActionStateChanged()
545 {
546 emit arbitraryActionsStateChanged({ActionsManager::PasteAction});
547 }
548
setCompletion(const QString & completion)549 void LineEditWidget::setCompletion(const QString &completion)
550 {
551 if (completion != m_completion)
552 {
553 m_completion = completion;
554
555 if (m_shouldIgnoreCompletion)
556 {
557 m_shouldIgnoreCompletion = false;
558
559 return;
560 }
561
562 if (!m_completer)
563 {
564 m_completer = new QCompleter(this);
565 m_completer->setCompletionMode(QCompleter::InlineCompletion);
566
567 setCompleter(m_completer);
568 }
569
570 m_completer->setModel(new QStringListModel({completion}, m_completer));
571 m_completer->complete();
572 }
573 }
574
setDropMode(LineEditWidget::DropMode mode)575 void LineEditWidget::setDropMode(LineEditWidget::DropMode mode)
576 {
577 m_dropMode = mode;
578 }
579
setClearOnEscape(bool clear)580 void LineEditWidget::setClearOnEscape(bool clear)
581 {
582 m_shouldClearOnEscape = clear;
583 }
584
setSelectAllOnFocus(bool select)585 void LineEditWidget::setSelectAllOnFocus(bool select)
586 {
587 if (m_shouldSelectAllOnFocus && !select)
588 {
589 disconnect(this, &LineEditWidget::selectionChanged, this, &LineEditWidget::clearSelectAllOnRelease);
590 }
591 else if (!m_shouldSelectAllOnFocus && select)
592 {
593 connect(this, &LineEditWidget::selectionChanged, this, &LineEditWidget::clearSelectAllOnRelease);
594 }
595
596 m_shouldSelectAllOnFocus = select;
597 }
598
getPopup()599 PopupViewWidget* LineEditWidget::getPopup()
600 {
601 if (!m_popupViewWidget)
602 {
603 m_popupViewWidget = new PopupViewWidget(this);
604
605 connect(m_popupViewWidget, &PopupViewWidget::clicked, this, &LineEditWidget::popupClicked);
606 }
607
608 return m_popupViewWidget;
609 }
610
getActionState(int identifier,const QVariantMap & parameters) const611 ActionsManager::ActionDefinition::State LineEditWidget::getActionState(int identifier, const QVariantMap ¶meters) const
612 {
613 const ActionsManager::ActionDefinition definition(ActionsManager::getActionDefinition(identifier));
614 ActionsManager::ActionDefinition::State state(definition.getDefaultState());
615 state.isEnabled = false;
616
617 if (definition.scope == ActionsManager::ActionDefinition::EditorScope)
618 {
619 switch (definition.identifier)
620 {
621 case ActionsManager::UndoAction:
622 state.isEnabled = (!isReadOnly() && isUndoAvailable());
623
624 break;
625 case ActionsManager::RedoAction:
626 state.isEnabled = (!isReadOnly() && isRedoAvailable());
627
628 break;
629 case ActionsManager::CutAction:
630 state.isEnabled = (!isReadOnly() && hasSelectedText());
631
632 break;
633 case ActionsManager::CopyAction:
634 state.isEnabled = hasSelectedText();
635
636 break;
637 case ActionsManager::CopyToNoteAction:
638 state.isEnabled = hasSelectedText();
639
640 break;
641 case ActionsManager::PasteAction:
642 state.isEnabled = (!isReadOnly() && (parameters.contains(QLatin1String("note")) || parameters.contains(QLatin1String("text")) || (QApplication::clipboard()->mimeData() && QApplication::clipboard()->mimeData()->hasText())));
643
644 break;
645 case ActionsManager::DeleteAction:
646 state.isEnabled = (!isReadOnly() && hasSelectedText());
647
648 break;
649 case ActionsManager::SelectAllAction:
650 state.isEnabled = !text().isEmpty();
651
652 break;
653 case ActionsManager::UnselectAction:
654 state.isEnabled = hasSelectedText();
655
656 break;
657 case ActionsManager::ClearAllAction:
658 state.isEnabled = (!isReadOnly() && !text().isEmpty());
659
660 break;
661 default:
662 break;
663 }
664 }
665
666 return state;
667 }
668
isPopupVisible() const669 bool LineEditWidget::isPopupVisible() const
670 {
671 return (m_popupViewWidget && m_popupViewWidget->isVisible());
672 }
673
674 }
675