Marble

MarbleLegendBrowser.cpp
1 // SPDX-FileCopyrightText: 2006-2007 Torsten Rahn <[email protected]>
2 // SPDX-FileCopyrightText: 2007 Inge Wallin <[email protected]>
3 // SPDX-FileCopyrightText: 2012 Illya Kovalevskyy <[email protected]>
4 // SPDX-FileCopyrightText: 2013 Yazeed Zoabi <[email protected]>
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 
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 
64 MarbleLegendBrowser::~MarbleLegendBrowser()
65 {
66  delete d;
67 }
68 
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 
80 QSize MarbleLegendBrowser::sizeHint() const
81 {
82  return QSize( 320, 320 );
83 }
84 
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 
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 
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 {
180  }
181 }
182 
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 
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 
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 
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 
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 
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 
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 
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 
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"
QString number(int n, int base)
QString translate(const char *context, const char *sourceText, const char *disambiguation, int n)
QString scheme() const const
QStringList split(const QString &sep, QString::SplitBehavior behavior, Qt::CaseSensitivity cs) const const
QString name() const const
bool openUrl(const QUrl &url)
QList::const_iterator constBegin() const const
void registerObject(const QString &id, QObject *object)
QString toString(QUrl::FormattingOptions options) const const
bool isEmpty() const const
QUrl fromLocalFile(const QString &localFile)
Binds a QML item to a specific geodetic location in screen coordinates.
AKONADI_CALENDAR_EXPORT KCalendarCore::Event::Ptr event(const Akonadi::Item &item)
QString & replace(int position, int n, QChar after)
QString host(QUrl::ComponentFormattingOptions options) const const
QList::const_iterator constEnd() const const
QString path(QUrl::ComponentFormattingOptions options) const const
QString path(const QString &relativePath)
const char * name(StandardAction id)
bool isValid() const const
const QList< QKeySequence > & end()
This file is part of the KDE documentation.
Documentation copyright © 1996-2023 The KDE developers.
Generated on Wed Oct 4 2023 04:09:42 by doxygen 1.8.17 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.