Marble

MarbleLegendBrowser.cpp
1// SPDX-FileCopyrightText: 2006-2007 Torsten Rahn <tackat@kde.org>
2// SPDX-FileCopyrightText: 2007 Inge Wallin <ingwa@kde.org>
3// SPDX-FileCopyrightText: 2012 Illya Kovalevskyy <illya.kovalevskyy@gmail.com>
4// SPDX-FileCopyrightText: 2013 Yazeed Zoabi <yazeedz.zoabi@gmail.com>
5//
6// SPDX-License-Identifier: LGPL-2.1-or-later
7
8#include "MarbleLegendBrowser.h"
9
10#include <QCoreApplication>
11#include <QDesktopServices>
12#include <QEvent>
13#include <QFile>
14#include <QMouseEvent>
15#include <QPainter>
16#include <QRegularExpression>
17#include <QUrl>
18
19#ifndef MARBLE_NO_WEBKITWIDGETS
20#include <QWebChannel>
21#include <QWebEnginePage>
22#endif
23
24#include <QTextDocument>
25
26#include "GeoSceneDocument.h"
27#include "GeoSceneHead.h"
28#include "GeoSceneIcon.h"
29#include "GeoSceneItem.h"
30#include "GeoSceneLegend.h"
31#include "GeoSceneProperty.h"
32#include "GeoSceneSection.h"
33#include "GeoSceneSettings.h"
34#include "MarbleDebug.h"
35#include "MarbleDirs.h"
36#include "MarbleModel.h"
37#include "TemplateDocument.h"
38
39namespace Marble
40{
41
42class MarbleLegendBrowserPrivate
43{
44public:
45 MarbleModel *m_marbleModel;
46 QMap<QString, bool> m_checkBoxMap;
47 QMap<QString, QPixmap> m_symbolMap;
48 QString m_currentThemeId;
49 MarbleJsWrapper *m_jsWrapper;
50};
51
52// ================================================================
53
54MarbleLegendBrowser::MarbleLegendBrowser(QWidget *parent)
55 : MarbleWebView(parent)
56 , d(new MarbleLegendBrowserPrivate)
57{
58 d->m_marbleModel = nullptr;
59 d->m_jsWrapper = new MarbleJsWrapper(this);
60}
61
62MarbleLegendBrowser::~MarbleLegendBrowser()
63{
64 delete d;
65}
66
67void MarbleLegendBrowser::setMarbleModel(MarbleModel *marbleModel)
68{
69 // We need this to be able to get to the MapTheme.
70 d->m_marbleModel = marbleModel;
71
72 if (d->m_marbleModel) {
73 connect(d->m_marbleModel, &MarbleModel::themeChanged, this, &MarbleLegendBrowser::initTheme);
74 }
75}
76
77QSize MarbleLegendBrowser::sizeHint() const
78{
79 return {320, 320};
80}
81
82void MarbleLegendBrowser::initTheme()
83{
84 // Check for a theme specific legend.html first
85 if (d->m_marbleModel != nullptr && d->m_marbleModel->mapTheme() != nullptr) {
86 const GeoSceneDocument *currentMapTheme = d->m_marbleModel->mapTheme();
87
88 d->m_checkBoxMap.clear();
89
90 for (const GeoSceneProperty *property : currentMapTheme->settings()->allProperties()) {
91 if (property->available()) {
92 d->m_checkBoxMap[property->name()] = property->value();
93 }
94 }
95
96 disconnect(currentMapTheme, SIGNAL(valueChanged(QString, bool)), nullptr, nullptr);
97 connect(currentMapTheme, SIGNAL(valueChanged(QString, bool)), this, SLOT(setCheckedProperty(QString, bool)));
98 }
99
100 if (isVisible()) {
101 loadLegend();
102 }
103}
104
105void MarbleLegendBrowser::loadLegend()
106{
107 if (!d->m_marbleModel) {
108 return;
109 }
110
111#ifndef MARBLE_NO_WEBKITWIDGETS
112 if (d->m_currentThemeId != d->m_marbleModel->mapThemeId()) {
113 d->m_currentThemeId = d->m_marbleModel->mapThemeId();
114 } else {
115 return;
116 }
117
118 // Read the html string.
119 QString legendPath;
120
121 // Check for a theme specific legend.html first
122 if (d->m_marbleModel->mapTheme() != nullptr) {
123 const GeoSceneDocument *currentMapTheme = d->m_marbleModel->mapTheme();
124
125 legendPath = MarbleDirs::path(QLatin1StringView("maps/") + currentMapTheme->head()->target() + QLatin1Char('/') + currentMapTheme->head()->theme()
126 + QLatin1StringView("/legend.html"));
127 }
128 if (legendPath.isEmpty()) {
129 legendPath = MarbleDirs::path(QStringLiteral("legend.html"));
130 }
131
132 QString finalHtml = readHtml(QUrl::fromLocalFile(legendPath));
133
134 TemplateDocument doc(finalHtml);
135 finalHtml = doc.finalText();
136
137 injectWebChannel(finalHtml);
138 reverseSupportCheckboxes(finalHtml);
139
140 // Generate some parts of the html from the MapTheme <Legend> tag.
141 const QString sectionsHtml = generateSectionsHtml();
142
143 // And then create the final html from these two parts.
144 finalHtml.replace(QStringLiteral("<!-- ##customLegendEntries:all## -->"), sectionsHtml);
145
146 translateHtml(finalHtml);
147
148 QUrl baseUrl = QUrl::fromLocalFile(legendPath);
149
150 // Set the html string in the QTextBrowser.
151 auto page = new MarbleWebPage(this);
152 connect(page, SIGNAL(linkClicked(QUrl)), this, SLOT(openLinkExternally(QUrl)));
153 page->setHtml(finalHtml, baseUrl);
154 setPage(page);
155
156 auto channel = new QWebChannel(page);
157 channel->registerObject(QStringLiteral("Marble"), d->m_jsWrapper);
158 page->setWebChannel(channel);
159
160 if (d->m_marbleModel) {
161 page->toHtml([=](QString document) {
162 d->m_marbleModel->setLegend(new QTextDocument(document));
163 });
164 }
165#endif
166}
167
168void MarbleLegendBrowser::openLinkExternally(const QUrl &url)
169{
170 if (url.scheme() == QLatin1StringView("tour")) {
171 Q_EMIT tourLinkClicked(QLatin1StringView("maps/") + url.host() + url.path());
172 } else {
174 }
175}
176
177bool MarbleLegendBrowser::event(QEvent *event)
178{
179 // "Delayed initialization": legend gets created only
180 if (event->type() == QEvent::Show) {
181 loadLegend();
182 }
183
184 return MarbleWebView::event(event);
185}
186
187QString MarbleLegendBrowser::readHtml(const QUrl &name)
188{
189 QString html;
190
191 QFile data(name.toLocalFile());
192 if (data.open(QFile::ReadOnly)) {
193 QTextStream in(&data);
194 html = in.readAll();
195 data.close();
196 }
197
198 return html;
199}
200
201void MarbleLegendBrowser::translateHtml(QString &html)
202{
203 // must match string extraction in Messages.sh
204 QString s = html;
205 QRegularExpression rx(QStringLiteral(R"(</?\w+((\s+\w+(\s*=\s*(?:".*"|'.*'|[^'">\s]+))?)+\s*|\s*)/?>)"),
206 QRegularExpression::InvertedGreedinessOption); // PORT_QT6: double check
207 /*
208 QRegExp rx( "</?\\w+((\\s+\\w+(\\s*=\\s*(?:\".*\"|'.*'|[^'\">\\s]+))?)+\\s*|\\s*)/?>");
209 rx.setMinimal( true );
210 */
211 s.replace(rx, QLatin1StringView("\n"));
212 s.replace(QRegularExpression(QStringLiteral("\\s*\n\\s*")), QStringLiteral("\n"));
213 const QStringList words = s.split(QLatin1Char('\n'), Qt::SkipEmptyParts);
214
217 for (; i != end; ++i)
218 html.replace(*i, QCoreApplication::translate("Legends", (*i).toUtf8().constData()));
219}
220
221void MarbleLegendBrowser::injectWebChannel(QString &html)
222{
223 QString webChannelCode = QStringLiteral(R"(<script type="text/javascript" src="qrc:///qtwebchannel/qwebchannel.js"></script>)");
224 webChannelCode += QStringLiteral(
225 "<script> document.addEventListener(\"DOMContentLoaded\", function() {"
226 "new QWebChannel(qt.webChannelTransport, function (channel) {"
227 "Marble = channel.objects.Marble;"
228 "});"
229 "}); </script>"
230 "</head>");
231 html.replace(QStringLiteral("</head>"), webChannelCode);
232}
233
234void MarbleLegendBrowser::reverseSupportCheckboxes(QString &html)
235{
236 const QString old = QStringLiteral("<a href=\"checkbox:cities\"/>");
237
238 QString checked;
239 if (d->m_checkBoxMap[QStringLiteral("cities")])
240 checked = QStringLiteral("checked");
241
242 const QString repair = QLatin1StringView(
243 "<input style=\"position: relative; top: -4px;\" type=\"checkbox\" "
244 "onchange=\"Marble.setCheckedProperty(this.name, this.checked);\" ")
245 + checked + QLatin1StringView(" name=\"cities\"/>");
246
247 html.replace(old, repair);
248}
249
250QString MarbleLegendBrowser::generateSectionsHtml()
251{
252 // Generate HTML to include into legend.html here.
253
254 QString customLegendString;
255
256 if (d->m_marbleModel == nullptr || d->m_marbleModel->mapTheme() == nullptr)
257 return {};
258
259 const GeoSceneDocument *currentMapTheme = d->m_marbleModel->mapTheme();
260
261 d->m_symbolMap.clear();
262
263 /* Okay, if you are reading it now, be ready for hell!
264 * We can't optimize this part of Legend Browser, but we will
265 * do it, anyway. It's complicated a lot, the most important
266 * thing is to understand everything.
267 */
268 for (const GeoSceneSection *section : currentMapTheme->legend()->sections()) {
269 // Each section is divided into the "well"
270 // Well is like a block of data with rounded corners
271 customLegendString += QLatin1StringView("<div class=\"well well-small well-legend\">");
272
273 const QString heading = QCoreApplication::translate("DGML", section->heading().toUtf8().constData());
274 QString checkBoxString;
275 if (section->checkable()) {
276 // If it's needed to make a checkbox here, we will
277 QString const checked = d->m_checkBoxMap[section->connectTo()] ? QStringLiteral("checked") : QString();
278 /* Important comment:
279 * We inject Marble object into JavaScript of each legend html file
280 * This is only one way to handle checkbox changes we see, so
281 * Marble.setCheckedProperty is a function that does it
282 */
283 if (!section->radio().isEmpty()) {
284 checkBoxString = QLatin1StringView(
285 "<label class=\"section-head\">"
286 "<input style=\"position: relative; top: -4px;\" type=\"radio\" "
287 "onchange=\"Marble.setRadioCheckedProperty(this.value, this.name ,this.checked);\" ")
288 + checked + QLatin1StringView(" value=\"") + section->connectTo() + QLatin1StringView("\" name=\"") + section->radio()
289 + QLatin1StringView("\" /><span>") + heading + QLatin1StringView("</span></label>");
290
291 } else {
292 checkBoxString = QLatin1StringView(
293 "<label class=\"section-head\">"
294 "<input style=\"position: relative; top: -4px;\" type=\"checkbox\" "
295 "onchange=\"Marble.setCheckedProperty(this.name, this.checked);\" ")
296 + checked + QLatin1StringView(" name=\"") + section->connectTo() + QLatin1StringView("\" /><span>") + heading
297 + QLatin1StringView("</span></label>");
298 }
299 customLegendString += checkBoxString;
300
301 } else {
302 customLegendString += QLatin1StringView("<h4 class=\"section-head\">") + heading + QLatin1StringView("</h4>");
303 }
304
305 for (const GeoSceneItem *item : section->items()) {
306 // checkbox for item
307 QString checkBoxString;
308 if (item->checkable()) {
309 QString const checked = d->m_checkBoxMap[item->connectTo()] ? QStringLiteral("checked") : QString();
310 checkBoxString = QLatin1StringView(
311 "<input type=\"checkbox\" "
312 "onchange=\"Marble.setCheckedProperty(this.name, this.checked);\" ")
313 + checked + QLatin1StringView(" name=\"") + item->connectTo() + QLatin1StringView("\" />");
314 }
315
316 // pixmap and text
317 QString src;
318 QString styleDiv;
319 int pixmapWidth = 24;
320 int pixmapHeight = 12;
321 if (!item->icon()->pixmap().isEmpty()) {
322 QString path = MarbleDirs::path(item->icon()->pixmap());
323 const QPixmap oncePixmap(path);
324 pixmapWidth = oncePixmap.width();
325 pixmapHeight = oncePixmap.height();
326 src = QUrl::fromLocalFile(path).toString();
327 styleDiv = QLatin1StringView("width: ") + QString::number(pixmapWidth) + QLatin1StringView("px; height: ") + QString::number(pixmapHeight)
328 + QLatin1StringView("px;");
329 } else {
330 // Workaround for rendered border around empty images in webkit
331 src = QStringLiteral("data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7");
332 }
333 // NOTICE. There are some pixmaps without image, so we should
334 // create just a plain rectangle with set color
335 if (QColor(item->icon()->color()).isValid()) {
336 const QColor color = item->icon()->color();
337 styleDiv = QLatin1StringView("width: ") + QString::number(pixmapWidth) + QLatin1StringView("px; height: ") + QString::number(pixmapHeight)
338 + QLatin1StringView("px; background-color: ") + color.name() + QLatin1Char(';');
339 }
340 styleDiv += QStringLiteral(" position: relative; top: -3px;");
341 const QString text = QCoreApplication::translate("DGML", item->text().toUtf8().constData());
343 "<div class=\"legend-entry\">"
344 " <label>")
345 + checkBoxString + QLatin1StringView(R"( <img class="image-pic" src=")") + src + QLatin1StringView("\" style=\"") + styleDiv
347 "\"/>"
348 " <span class=\"kotation\" >")
349 + text
351 "</span>"
352 " </label>"
353 "</div>");
354 customLegendString += html;
355 }
356 customLegendString += QLatin1StringView("</div>"); // <div class="well">
357 }
358
359 return customLegendString;
360}
361
362void MarbleLegendBrowser::setCheckedProperty(const QString &name, bool checked)
363{
364 if (checked != d->m_checkBoxMap[name]) {
365 d->m_checkBoxMap[name] = checked;
366 Q_EMIT toggledShowProperty(name, checked);
367 }
368}
369
370void MarbleLegendBrowser::setRadioCheckedProperty(const QString &value, const QString &name, bool checked)
371{
372 Q_UNUSED(value)
373 if (checked != d->m_checkBoxMap[name]) {
374 d->m_checkBoxMap[name] = checked;
375 Q_EMIT toggledShowProperty(name, checked);
376 }
377}
378
379}
380
381#include "moc_MarbleLegendBrowser.cpp"
This file contains the headers for MarbleModel.
AKONADI_CALENDAR_EXPORT KCalendarCore::Event::Ptr event(const Akonadi::Item &item)
QString path(const QString &relativePath)
QString name(StandardAction id)
const QList< QKeySequence > & end()
Binds a QML item to a specific geodetic location in screen coordinates.
bool isValid() const const
QString name(NameFormat format) const const
QString translate(const char *context, const char *sourceText, const char *disambiguation, int n)
bool openUrl(const QUrl &url)
const_iterator constBegin() const const
const_iterator constEnd() const const
virtual bool event(QEvent *e)
void clear()
bool isEmpty() const const
QString number(double n, char format, int precision)
QString & replace(QChar before, QChar after, Qt::CaseSensitivity cs)
QStringList split(QChar sep, Qt::SplitBehavior behavior, Qt::CaseSensitivity cs) const const
SkipEmptyParts
QFuture< ArgsType< Signal > > connect(Sender *sender, Signal signal)
QUrl fromLocalFile(const QString &localFile)
QString host(ComponentFormattingOptions options) const const
QString path(ComponentFormattingOptions options) const const
QString scheme() const const
QString toString(FormattingOptions options) const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Fri Dec 27 2024 11:51:00 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.