Falkon Develop
Cross-platform Qt-based web browser
locationcompleterdelegate.cpp
Go to the documentation of this file.
1/* ============================================================
2* Falkon - Qt web browser
3* Copyright (C) 2010-2018 David Rosca <nowrep@gmail.com>
4*
5* This program is free software: you can redistribute it and/or modify
6* it under the terms of the GNU General Public License as published by
7* the Free Software Foundation, either version 3 of the License, or
8* (at your option) any later version.
9*
10* This program is distributed in the hope that it will be useful,
11* but WITHOUT ANY WARRANTY; without even the implied warranty of
12* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13* GNU General Public License for more details.
14*
15* You should have received a copy of the GNU General Public License
16* along with this program. If not, see <http://www.gnu.org/licenses/>.
17* ============================================================ */
20#include "locationbar.h"
21#include "iconprovider.h"
22#include "qzsettings.h"
23#include "mainapplication.h"
24#include "bookmarkitem.h"
25
26#include <algorithm>
27
28#include <QPainter>
29#include <QApplication>
30#include <QMouseEvent>
31#include <QTextLayout>
32#include <QtGuiVersion>
33
35 : QStyledItemDelegate(parent)
36 , m_rowHeight(0)
37 , m_padding(0)
38{
39}
40
41void LocationCompleterDelegate::paint(QPainter* painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
42{
43 QStyleOptionViewItem opt = option;
44 initStyleOption(&opt, index);
45
46 const QWidget* w = opt.widget;
47 const QStyle* style = w ? w->style() : QApplication::style();
48
49 const int height = opt.rect.height();
50 const int center = height / 2 + opt.rect.top();
51
52 // Prepare link font
53 QFont linkFont = opt.font;
54 linkFont.setPointSize(linkFont.pointSize() - 1);
55
56 const QFontMetrics linkMetrics(linkFont);
57
58 int leftPosition = m_padding * 2;
59 int rightPosition = opt.rect.right() - m_padding;
60
61 opt.state |= QStyle::State_Active;
62
63 const QIcon::Mode iconMode = opt.state & QStyle::State_Selected ? QIcon::Selected : QIcon::Normal;
64
65 const QPalette::ColorRole colorRole = opt.state & QStyle::State_Selected ? QPalette::HighlightedText : QPalette::Text;
66 const QPalette::ColorRole colorLinkRole = opt.state & QStyle::State_Selected ? QPalette::HighlightedText : QPalette::Link;
67
68#ifdef Q_OS_WIN
69 opt.palette.setColor(QPalette::All, QPalette::HighlightedText, opt.palette.color(QPalette::Active, QPalette::Text));
70 opt.palette.setColor(QPalette::All, QPalette::Highlight, opt.palette.base().color().darker(108));
71#endif
72
73 QPalette textPalette = opt.palette;
74 textPalette.setCurrentColorGroup(opt.state & QStyle::State_Enabled ? QPalette::Normal : QPalette::Disabled);
75
76 // Draw background
77 style->drawPrimitive(QStyle::PE_PanelItemViewItem, &opt, painter, w);
78
79 const bool isVisitSearchItem = index.data(LocationCompleterModel::VisitSearchItemRole).toBool();
80 const bool isSearchSuggestion = index.data(LocationCompleterModel::SearchSuggestionRole).toBool();
81
82 LocationBar::LoadAction loadAction;
83 bool isWebSearch = isSearchSuggestion;
84
85 BookmarkItem *bookmark = static_cast<BookmarkItem*>(index.data(LocationCompleterModel::BookmarkItemRole).value<void*>());
86
87 if (isVisitSearchItem) {
88 loadAction = LocationBar::loadAction(index.data(LocationCompleterModel::SearchStringRole).toString());
89 isWebSearch = loadAction.type == LocationBar::LoadAction::Search;
90 if (!m_forceVisitItem) {
91 bookmark = loadAction.bookmark;
92 }
93 }
94
95 // Draw icon
96 const int iconSize = 16;
97 const int iconYPos = center - (iconSize / 2);
98 QRect iconRect(leftPosition, iconYPos, iconSize, iconSize);
99 QPixmap pixmap = index.data(Qt::DecorationRole).value<QIcon>().pixmap(iconSize);
100 if (isSearchSuggestion || (isVisitSearchItem && isWebSearch)) {
101 pixmap = QIcon::fromTheme(QSL("edit-find"), QIcon(QSL(":icons/menu/search-icon.svg"))).pixmap(iconSize, iconMode);
102 }
103 if (isVisitSearchItem && bookmark) {
104 pixmap = bookmark->icon().pixmap(iconSize);
105 } else if (loadAction.type == LocationBar::LoadAction::Search) {
106 if (loadAction.searchEngine.name != LocationBar::searchEngine().name) {
107 pixmap = loadAction.searchEngine.icon.pixmap(iconSize);
108 }
109 }
110 painter->drawPixmap(iconRect, pixmap);
111 leftPosition = iconRect.right() + m_padding * 2;
112
113 // Draw star to bookmark items
114 int starPixmapWidth = 0;
115 if (bookmark) {
116 const QIcon icon = IconProvider::instance()->bookmarkIcon();
117 const QSize starSize(16, 16);
118 starPixmapWidth = starSize.width();
119 QPoint pos(rightPosition - starPixmapWidth, center - starSize.height() / 2);
120 QRect starRect(pos, starSize);
121 painter->drawPixmap(starRect, icon.pixmap(starSize, iconMode));
122 }
123
124 QString searchText = index.data(LocationCompleterModel::SearchStringRole).toString();
125
126 // Draw title
127 leftPosition += 2;
128 QRect titleRect(leftPosition, center - opt.fontMetrics.height() / 2, opt.rect.width() * 0.6, opt.fontMetrics.height());
129 QString title = index.data(LocationCompleterModel::TitleRole).toString();
130 painter->setFont(opt.font);
131
132 if (isVisitSearchItem) {
133 if (bookmark) {
134 title = bookmark->title();
135 } else {
136 title = index.data(LocationCompleterModel::SearchStringRole).toString();
137 searchText.clear();
138 }
139 }
140
141 leftPosition += viewItemDrawText(painter, &opt, titleRect, title, textPalette.color(colorRole), searchText);
142 leftPosition += m_padding * 2;
143
144 // Trim link to maximum number of characters that can be visible, otherwise there may be perf issue with huge URLs
145 const int maxChars = (opt.rect.width() - leftPosition) / opt.fontMetrics.horizontalAdvance(QL1C('i'));
146 QString link;
147 const QByteArray linkArray = index.data(Qt::DisplayRole).toByteArray();
148 if (!linkArray.startsWith("data") && !linkArray.startsWith("javascript")) {
149 link = QString::fromUtf8(QByteArray::fromPercentEncoding(linkArray)).left(maxChars);
150 } else {
151 link = QString::fromLatin1(linkArray.left(maxChars));
152 }
153
154 if (isVisitSearchItem || isSearchSuggestion) {
155 if (!opt.state.testFlag(QStyle::State_Selected) && !opt.state.testFlag(QStyle::State_MouseOver)) {
156 link.clear();
157 } else if (isVisitSearchItem && (!isWebSearch || m_forceVisitItem)) {
158 link = tr("Visit");
159 } else {
160 QString searchEngineName = loadAction.searchEngine.name;
161 if (searchEngineName.isEmpty()) {
162 searchEngineName = LocationBar::searchEngine().name;
163 }
164 link = tr("Search with %1").arg(searchEngineName);
165 }
166 }
167
168 if (bookmark) {
169 link = bookmark->url().toString();
170 }
171
172 // Draw separator
173 if (!link.isEmpty()) {
174 QChar separator = QL1C('-');
175 QRect separatorRect(leftPosition, center - linkMetrics.height() / 2, linkMetrics.horizontalAdvance(separator), linkMetrics.height());
176 style->drawItemText(painter, separatorRect, Qt::AlignCenter, textPalette, true, separator, colorRole);
177 leftPosition += separatorRect.width() + m_padding * 2;
178 }
179
180 // Draw link
181 const int leftLinkEdge = leftPosition;
182 const int rightLinkEdge = rightPosition - m_padding - starPixmapWidth;
183 QRect linkRect(leftLinkEdge, center - linkMetrics.height() / 2, rightLinkEdge - leftLinkEdge, linkMetrics.height());
184 painter->setFont(linkFont);
185
186 // Draw url (or switch to tab)
187 int tabPos = index.data(LocationCompleterModel::TabPositionTabRole).toInt();
188
189 if (qzSettings->showSwitchTab && !m_forceVisitItem && tabPos != -1) {
190 const QIcon tabIcon = QIcon(QSL(":icons/menu/tab.svg"));
191 QRect iconRect(linkRect);
192 iconRect.setX(iconRect.x());
193 iconRect.setWidth(16);
194 painter->drawPixmap(iconRect, tabIcon.pixmap(iconRect.size(), iconMode));
195
196 QRect textRect(linkRect);
197 textRect.setX(textRect.x() + m_padding + 16 + m_padding);
198 viewItemDrawText(painter, &opt, textRect, tr("Switch to tab"), textPalette.color(colorLinkRole));
199 } else if (isVisitSearchItem || isSearchSuggestion) {
200 viewItemDrawText(painter, &opt, linkRect, link, textPalette.color(colorLinkRole));
201 } else {
202 viewItemDrawText(painter, &opt, linkRect, link, textPalette.color(colorLinkRole), searchText);
203 }
204
205 // Draw line at the very bottom of item if the item is not highlighted
206 if (!(opt.state & QStyle::State_Selected)) {
207 QRect lineRect(opt.rect.left(), opt.rect.bottom(), opt.rect.width(), 1);
208 painter->fillRect(lineRect, opt.palette.color(QPalette::AlternateBase));
209 }
210}
211
212QSize LocationCompleterDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const
213{
214 Q_UNUSED(index)
215
216 if (!m_rowHeight) {
217 QStyleOptionViewItem opt(option);
218 initStyleOption(&opt, index);
219
220 const QWidget* w = opt.widget;
221 const QStyle* style = w ? w->style() : QApplication::style();
222 const int padding = style->pixelMetric(QStyle::PM_FocusFrameHMargin, nullptr) + 1;
223
224 m_padding = padding > 3 ? padding : 3;
225 m_rowHeight = 4 * m_padding + qMax(16, opt.fontMetrics.height());
226 }
227
228 return QSize(200, m_rowHeight);
229}
230
232{
233 m_forceVisitItem = enable;
234}
235
236static bool sizeBiggerThan(const QString &s1, const QString &s2)
237{
238 return s1.size() > s2.size();
239}
240
241static QSizeF viewItemTextLayout(QTextLayout &textLayout, int lineWidth)
242{
243 qreal height = 0;
244 qreal widthUsed = 0;
245 textLayout.beginLayout();
246 QTextLine line = textLayout.createLine();
247 if (line.isValid()) {
248 line.setLineWidth(lineWidth);
249 line.setPosition(QPointF(0, height));
250 height += line.height();
251 widthUsed = qMax(widthUsed, line.naturalTextWidth());
252
253 textLayout.endLayout();
254 }
255 return QSizeF(widthUsed, height);
256}
257
258// most of codes taken from QCommonStylePrivate::viewItemDrawText()
259// added highlighting and simplified for single-line textlayouts
260int LocationCompleterDelegate::viewItemDrawText(QPainter *p, const QStyleOptionViewItem *option, const QRect &rect,
261 const QString &text, const QColor &color, const QString &searchText) const
262{
263 if (text.isEmpty()) {
264 return 0;
265 }
266
267 const QFontMetrics fontMetrics(p->font());
268 QString elidedText = fontMetrics.elidedText(text, option->textElideMode, rect.width());
269 QTextOption textOption;
270 textOption.setWrapMode(QTextOption::NoWrap);
271 textOption.setAlignment(QStyle::visualAlignment(textOption.textDirection(), option->displayAlignment));
272 QTextLayout textLayout;
273 textLayout.setFont(p->font());
274 textLayout.setText(elidedText);
275 textLayout.setTextOption(textOption);
276
277 if (!searchText.isEmpty()) {
278 QList<int> delimiters;
279 QStringList searchStrings = searchText.split(QLatin1Char(' '), Qt::SkipEmptyParts);
280 // Look for longer parts first
281 std::sort(searchStrings.begin(), searchStrings.end(), sizeBiggerThan);
282
283 for (const QString &string : std::as_const(searchStrings)) {
284 int delimiter = text.indexOf(string, 0, Qt::CaseInsensitive);
285
286 while (delimiter != -1) {
287 int start = delimiter;
288 int end = delimiter + string.length();
289 bool alreadyContains = false;
290 for (int i = 0; i < delimiters.count(); ++i) {
291 int dStart = delimiters.at(i);
292 int dEnd = delimiters.at(++i);
293
294 if (dStart <= start && dEnd >= end) {
295 alreadyContains = true;
296 break;
297 }
298 }
299 if (!alreadyContains) {
300 delimiters.append(start);
301 delimiters.append(end);
302 }
303
304 delimiter = text.indexOf(string, end, Qt::CaseInsensitive);
305 }
306 }
307
308 // We need to sort delimiters to properly paint all parts that user typed
309 std::sort(delimiters.begin(), delimiters.end());
310
311 // If we don't find any match, just paint it without any highlight
312 if (!delimiters.isEmpty() && !(delimiters.count() % 2)) {
313 QList<QTextLayout::FormatRange> highlightParts;
314
315 while (!delimiters.isEmpty()) {
316 QTextLayout::FormatRange highlightedPart;
317 int start = delimiters.takeFirst();
318 int end = delimiters.takeFirst();
319 highlightedPart.start = start;
320 highlightedPart.length = end - start;
321 highlightedPart.format.setFontWeight(QFont::Bold);
322 highlightedPart.format.setUnderlineStyle(QTextCharFormat::SingleUnderline);
323
324 highlightParts << highlightedPart;
325 }
326
327 textLayout.setFormats(highlightParts);
328 }
329 }
330
331 // do layout
332 viewItemTextLayout(textLayout, rect.width());
333
334 if (textLayout.lineCount() <= 0) {
335 return 0;
336 }
337
338 QTextLine textLine = textLayout.lineAt(0);
339
340 // if elidedText after highlighting is longer
341 // than available width then re-elide it and redo layout
342 int diff = textLine.naturalTextWidth() - rect.width();
343 if (diff > 0) {
344 elidedText = fontMetrics.elidedText(elidedText, option->textElideMode, rect.width() - diff);
345
346 textLayout.setText(elidedText);
347 // redo layout
348 viewItemTextLayout(textLayout, rect.width());
349
350 if (textLayout.lineCount() <= 0) {
351 return 0;
352 }
353 textLine = textLayout.lineAt(0);
354 }
355
356 // draw line
357 p->setPen(color);
358 qreal width = qMax<qreal>(rect.width(), textLayout.lineAt(0).width());
359 const QRect &layoutRect = QStyle::alignedRect(option->direction, option->displayAlignment, QSize(int(width), int(textLine.height())), rect);
360 const QPointF &position = layoutRect.topLeft();
361
362 textLine.draw(p, position);
363
364 return qMin<int>(rect.width(), textLayout.lineAt(0).naturalTextWidth());
365}
QUrl url() const
QString title() const
QIcon bookmarkIcon
Definition: iconprovider.h:41
static IconProvider * instance()
static SearchEngine searchEngine()
static LoadAction loadAction(const QString &text)
void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override
QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override
LocationCompleterDelegate(QObject *parent=nullptr)
i
Definition: i18n.py:23
#define QL1C(x)
Definition: qzcommon.h:48
#define QSL(x)
Definition: qzcommon.h:40
#define qzSettings
Definition: qzsettings.h:69
BookmarkItem * bookmark
Definition: locationbar.h:55
SearchEngine searchEngine
Definition: locationbar.h:54