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