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 &parameters, 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 &parameters) 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