Marble

TourWidget.cpp
1// SPDX-License-Identifier: LGPL-2.1-or-later
2//
3// SPDX-FileCopyrightText: 2013 Mihail Ivchenko <ematirov@gmail>
4// SPDX-FileCopyrightText: 2014 Sanjiban Bairagya <sanjiban22393@gmail.com>
5//
6
7#include "TourWidget.h"
8#include "FlyToEditWidget.h"
9#include "SoundCueEditWidget.h"
10#include "TourControlEditWidget.h"
11#include "TourItemDelegate.h"
12#include "WaitEditWidget.h"
13
14#include "EditPlacemarkDialog.h"
15#include "GeoDataAnimatedUpdate.h"
16#include "GeoDataCamera.h"
17#include "GeoDataChange.h"
18#include "GeoDataCreate.h"
19#include "GeoDataDelete.h"
20#include "GeoDataDocument.h"
21#include "GeoDataDocumentWriter.h"
22#include "GeoDataFlyTo.h"
23#include "GeoDataIconStyle.h"
24#include "GeoDataLookAt.h"
25#include "GeoDataPlacemark.h"
26#include "GeoDataPlaylist.h"
27#include "GeoDataSoundCue.h"
28#include "GeoDataStyle.h"
29#include "GeoDataTour.h"
30#include "GeoDataTourControl.h"
31#include "GeoDataTreeModel.h"
32#include "GeoDataUpdate.h"
33#include "GeoDataWait.h"
34#include "KmlElementDictionary.h"
35#include "MarbleDebug.h"
36#include "MarbleDirs.h"
37#include "MarbleModel.h"
38#include "MarblePlacemarkModel.h"
39#include "MarbleWidget.h"
40#include "MovieCapture.h"
41#include "ParsingRunnerManager.h"
42#include "PlaybackFlyToItem.h"
43#include "TourCaptureDialog.h"
44#include "TourPlayback.h"
45#include "ui_TourWidget.h"
46
47#include <QCloseEvent>
48#include <QDir>
49#include <QFileDialog>
50#include <QKeyEvent>
51#include <QMenu>
52#include <QMessageBox>
53#include <QModelIndex>
54#include <QPainter>
55#include <QPointer>
56#include <QToolButton>
57#include <QUrl>
58
59namespace Marble
60{
61
62class TourWidgetPrivate
63{
64public:
65 explicit TourWidgetPrivate(TourWidget *parent);
66 ~TourWidgetPrivate();
67 GeoDataFeature *getPlaylistFeature() const;
68 void updateRootIndex();
69
70public:
71 void openFile();
72 bool openFile(const QString &filename);
73 void createTour();
74 void saveTour();
75 void saveTourAs();
76 void mapCenterOn(const QModelIndex &index);
77 void addFlyTo();
78 void addWait();
79 void addSoundCue();
80 void addPlacemark();
81 void addRemovePlacemark();
82 void addChangePlacemark();
83 void addTourPrimitive(GeoDataTourPrimitive *primitive);
84 void deleteSelected();
85 void updateButtonsStates();
86 void moveUp();
87 void moveDown();
88 void captureTour();
89 void handlePlaybackProgress(const double position);
90 void handlePlaybackFinish();
91 GeoDataObject *rootIndexObject() const;
92
93private:
94 GeoDataTour *findTour(GeoDataFeature *feature) const;
95 bool openDocument(GeoDataDocument *document);
96 bool saveTourAs(const QString &filename);
97 bool overrideModifications();
98
99public:
100 TourWidget *q;
101 MarbleWidget *m_widget;
102 Ui::TourWidget m_tourUi;
103 TourCaptureDialog *m_tourCaptureDialog;
104 TourPlayback m_playback;
105 TourItemDelegate *m_delegate;
106 bool m_isChanged;
107 bool m_playState;
108 bool m_isLoopingStopped;
109 GeoDataDocument *m_document;
110 QAction *m_actionToggleLoopPlay;
111 QToolButton *m_addPrimitiveButton;
112 QAction *m_actionAddFlyTo;
113 QAction *m_actionAddWait;
114 QAction *m_actionAddSoundCue;
115 QAction *m_actionAddPlacemark;
116 QAction *m_actionAddRemovePlacemark;
117 QAction *m_actionAddChangePlacemark;
118};
119
120TourWidgetPrivate::TourWidgetPrivate(TourWidget *parent)
121 : q(parent)
122 , m_widget(nullptr)
123 , m_playback(nullptr)
124 , m_delegate(nullptr)
125 , m_isChanged(false)
126 , m_playState(false)
127 , m_document(nullptr)
128 , m_addPrimitiveButton(new QToolButton)
129{
130 m_tourUi.setupUi(parent);
131 m_tourUi.m_actionRecord->setEnabled(false);
132
133 QAction *separator = m_tourUi.m_toolBarControl->insertSeparator(m_tourUi.m_actionMoveUp);
134
135 m_addPrimitiveButton->setIcon(QIcon(QStringLiteral(":/marble/flag.png")));
136 m_addPrimitiveButton->setToolTip(QObject::tr("Add FlyTo"));
137 m_addPrimitiveButton->setPopupMode(QToolButton::MenuButtonPopup);
138
139 auto addPrimitiveMenu = new QMenu(q);
140
141 m_actionAddFlyTo = new QAction(QIcon(QStringLiteral(":/marble/flag.png")), QObject::tr("Add FlyTo"), addPrimitiveMenu);
142 addPrimitiveMenu->addAction(m_actionAddFlyTo);
143 m_actionAddWait = new QAction(QIcon(QStringLiteral(":/marble/player-time.png")), QObject::tr("Add Wait"), addPrimitiveMenu);
144 addPrimitiveMenu->addAction(m_actionAddWait);
145 m_actionAddSoundCue = new QAction(QIcon(QStringLiteral(":/marble/audio-x-generic.png")), QObject::tr("Add SoundCue"), addPrimitiveMenu);
146 addPrimitiveMenu->addAction(m_actionAddSoundCue);
147 addPrimitiveMenu->addSeparator();
148 m_actionAddPlacemark = new QAction(QIcon(QStringLiteral(":/icons/add-placemark.png")), QObject::tr("Add Placemark"), addPrimitiveMenu);
149 addPrimitiveMenu->addAction(m_actionAddPlacemark);
150 m_actionAddRemovePlacemark = new QAction(QIcon(QStringLiteral(":/icons/remove.png")), QObject::tr("Remove placemark"), addPrimitiveMenu);
151 addPrimitiveMenu->addAction(m_actionAddRemovePlacemark);
152 m_actionAddChangePlacemark = new QAction(QIcon(QStringLiteral(":/marble/document-edit.png")), QObject::tr("Change placemark"), addPrimitiveMenu);
153 addPrimitiveMenu->addAction(m_actionAddChangePlacemark);
154 m_actionToggleLoopPlay = new QAction(QObject::tr("Loop"), m_tourUi.m_slider);
155 m_actionToggleLoopPlay->setCheckable(true);
156 m_actionToggleLoopPlay->setChecked(false);
157 m_tourUi.m_slider->setContextMenuPolicy(Qt::ActionsContextMenu);
158 m_tourUi.m_slider->addAction(m_actionToggleLoopPlay);
159
160 m_addPrimitiveButton->setMenu(addPrimitiveMenu);
161 m_addPrimitiveButton->setEnabled(false);
162
163 m_tourUi.m_toolBarControl->insertWidget(separator, m_addPrimitiveButton);
164
165 QObject::connect(m_tourUi.m_listView, SIGNAL(activated(QModelIndex)), q, SLOT(mapCenterOn(QModelIndex)));
166 QObject::connect(m_addPrimitiveButton, SIGNAL(clicked()), q, SLOT(addFlyTo()));
167 QObject::connect(m_actionAddFlyTo, SIGNAL(triggered()), q, SLOT(addFlyTo()));
168 QObject::connect(m_actionAddWait, SIGNAL(triggered()), q, SLOT(addWait()));
169 QObject::connect(m_actionAddSoundCue, SIGNAL(triggered()), q, SLOT(addSoundCue()));
170 QObject::connect(m_actionAddPlacemark, SIGNAL(triggered()), q, SLOT(addPlacemark()));
171 QObject::connect(m_actionAddRemovePlacemark, SIGNAL(triggered()), q, SLOT(addRemovePlacemark()));
172 QObject::connect(m_actionAddChangePlacemark, SIGNAL(triggered()), q, SLOT(addChangePlacemark()));
173 QObject::connect(m_tourUi.m_actionDelete, SIGNAL(triggered()), q, SLOT(deleteSelected()));
174 QObject::connect(m_tourUi.m_actionMoveUp, SIGNAL(triggered()), q, SLOT(moveUp()));
175 QObject::connect(m_tourUi.m_actionMoveDown, SIGNAL(triggered()), q, SLOT(moveDown()));
176 QObject::connect(m_tourUi.m_actionNewTour, SIGNAL(triggered()), q, SLOT(createTour()));
177 QObject::connect(m_tourUi.m_actionOpenTour, SIGNAL(triggered()), q, SLOT(openFile()));
178 QObject::connect(m_tourUi.m_actionSaveTour, SIGNAL(triggered()), q, SLOT(saveTour()));
179 QObject::connect(m_tourUi.m_actionSaveTourAs, SIGNAL(triggered()), q, SLOT(saveTourAs()));
180 QObject::connect(m_tourUi.m_actionRecord, SIGNAL(triggered()), q, SLOT(captureTour()));
181 QObject::connect(&m_playback, SIGNAL(finished()), q, SLOT(stopPlaying()));
182 QObject::connect(&m_playback, SIGNAL(itemFinished(int)), q, SLOT(setHighlightedItemIndex(int)));
183}
184
185TourWidgetPrivate::~TourWidgetPrivate()
186{
187 delete m_delegate;
188}
189
190TourWidget::TourWidget(QWidget *parent, Qt::WindowFlags flags)
191 : QWidget(parent, flags)
192 , d(new TourWidgetPrivate(this))
193{
194 layout()->setContentsMargins({});
195
196 connect(d->m_tourUi.actionPlay, SIGNAL(triggered()), this, SLOT(togglePlaying()));
197 connect(d->m_tourUi.actionStop, SIGNAL(triggered()), this, SLOT(stopLooping()));
198 connect(d->m_tourUi.actionStop, SIGNAL(triggered()), this, SLOT(stopPlaying()));
199 connect(d->m_tourUi.m_slider, SIGNAL(sliderMoved(int)), this, SLOT(handleSliderMove(int)));
200
201 d->m_tourUi.m_toolBarPlayback->setDisabled(true);
202 d->m_tourUi.m_slider->setDisabled(true);
203 d->m_tourUi.m_listView->installEventFilter(this);
204}
205
206TourWidget::~TourWidget()
207{
208 delete d;
209}
210
211bool TourWidget::eventFilter(QObject *watched, QEvent *event)
212{
213 Q_UNUSED(watched);
214
215 Q_ASSERT(watched == d->m_tourUi.m_listView);
216 GeoDataObject *rootObject = d->rootIndexObject();
217
218 if (!rootObject) {
219 return false;
220 }
221
222 if (event->type() == QEvent::KeyPress) {
223 auto key = static_cast<QKeyEvent *>(event);
224 QModelIndexList selectedIndexes = d->m_tourUi.m_listView->selectionModel()->selectedIndexes();
225
226 if (key->key() == Qt::Key_Delete) {
227 if (!selectedIndexes.isEmpty()) {
228 deleteSelected();
229 }
230 return true;
231 }
232
233 if (key->key() == Qt::Key_PageDown && key->modifiers().testFlag(Qt::ControlModifier) && !selectedIndexes.isEmpty()) {
234 QModelIndexList::iterator end = selectedIndexes.end() - 1;
235 if (const GeoDataPlaylist *playlist = (rootObject ? geodata_cast<GeoDataPlaylist>(rootObject) : nullptr)) {
236 if (end->row() != playlist->size() - 1) {
237 moveDown();
238 }
239 }
240 return true;
241 }
242
243 if (key->key() == Qt::Key_PageUp && key->modifiers().testFlag(Qt::ControlModifier) && !selectedIndexes.isEmpty()) {
244 QModelIndexList::iterator start = selectedIndexes.begin();
245 if (start->row() != 0) {
246 moveUp();
247 }
248 return true;
249 }
250 }
251
252 return false;
253}
254
255void TourWidget::setMarbleWidget(MarbleWidget *widget)
256{
257 d->m_widget = widget;
258 delete d->m_delegate;
259 d->m_delegate = new TourItemDelegate(d->m_tourUi.m_listView, d->m_widget, this);
260 connect(d->m_delegate, SIGNAL(edited(QModelIndex)), this, SLOT(updateDuration()));
261 connect(d->m_delegate, SIGNAL(edited(QModelIndex)), &d->m_playback, SLOT(updateTracks()));
262 d->m_tourUi.m_listView->setItemDelegate(d->m_delegate);
263}
264
265void TourWidget::togglePlaying()
266{
267 if (!d->m_playState) {
268 d->m_playState = true;
269 startPlaying();
270 } else {
271 d->m_playState = false;
272 pausePlaying();
273 }
274}
275
276void TourWidget::startPlaying()
277{
278 setHighlightedItemIndex(0);
279 d->m_isLoopingStopped = false;
280 d->m_playback.play();
281 d->m_tourUi.actionPlay->setIcon(QIcon(QStringLiteral(":/marble/playback-pause.png")));
282 d->m_tourUi.actionPlay->setEnabled(true);
283 d->m_tourUi.actionStop->setEnabled(true);
284 d->m_tourUi.m_actionRecord->setEnabled(false);
285 d->m_delegate->setEditable(false);
286 d->m_addPrimitiveButton->setEnabled(false);
287 d->m_playState = true;
288}
289
290void TourWidget::pausePlaying()
291{
292 d->m_playback.pause();
293 d->m_tourUi.actionPlay->setIcon(QIcon(QStringLiteral(":/marble/playback-play.png")));
294 d->m_tourUi.actionPlay->setEnabled(true);
295 d->m_tourUi.actionStop->setEnabled(true);
296}
297
298void TourWidget::stopPlaying()
299{
300 removeHighlight();
301 d->m_playback.stop();
302 d->m_tourUi.actionPlay->setIcon(QIcon(QStringLiteral(":/marble/playback-play.png")));
303 d->m_tourUi.actionPlay->setEnabled(true);
304 d->m_tourUi.m_actionRecord->setEnabled(true);
305 d->m_tourUi.actionStop->setEnabled(false);
306 d->m_playState = false;
307 d->m_delegate->setEditable(true);
308 d->m_addPrimitiveButton->setEnabled(true);
309
310 // Loop if the option ( m_actionLoopPlay ) is checked
311 if (d->m_actionToggleLoopPlay->isChecked() && !d->m_isLoopingStopped) {
312 startPlaying();
313 }
314}
315
316void TourWidget::stopLooping()
317{
318 d->m_isLoopingStopped = true;
319}
320
321void TourWidget::closeEvent(QCloseEvent *event)
322{
323 if (!d->m_document || !d->m_isChanged) {
324 event->accept();
325 return;
326 }
327
328 const int result = QMessageBox::question(d->m_widget,
329 QObject::tr("Save tour"),
330 QObject::tr("There are unsaved Tours. Do you want to save your changes?"),
332
333 switch (result) {
335 d->saveTour();
336 event->accept();
337 break;
339 event->accept();
340 break;
342 event->ignore();
343 }
344}
345
346void TourWidget::handleSliderMove(int value)
347{
348 removeHighlight();
349 d->m_playback.seek(value / 100.0);
350 QTime nullTime(0, 0, 0);
351 QTime time = nullTime.addSecs(value / 100.0);
352 d->m_tourUi.m_elapsedTime->setText(QStringLiteral("%L1:%L2").arg(time.minute(), 2, 10, QLatin1Char('0')).arg(time.second(), 2, 10, QLatin1Char('0')));
353}
354
355void TourWidgetPrivate::openFile()
356{
357 if (overrideModifications()) {
358 QString const filename = QFileDialog::getOpenFileName(q, QObject::tr("Open Tour"), QDir::homePath(), QObject::tr("KML Tours (*.kml)"));
359 if (!filename.isEmpty()) {
360 ParsingRunnerManager manager(m_widget->model()->pluginManager());
361 GeoDataDocument *document = manager.openFile(filename);
362 m_playback.setBaseUrl(QUrl::fromLocalFile(filename));
363 openDocument(document);
364 }
365 }
366}
367
368bool TourWidgetPrivate::openFile(const QString &filename)
369{
370 if (overrideModifications()) {
371 if (!filename.isEmpty()) {
372 ParsingRunnerManager manager(m_widget->model()->pluginManager());
373 GeoDataDocument *document = manager.openFile(filename);
374 m_playback.setBaseUrl(QUrl::fromLocalFile(filename));
375 return openDocument(document);
376 }
377 }
378
379 return false;
380}
381
382GeoDataTour *TourWidgetPrivate::findTour(GeoDataFeature *feature) const
383{
384 if (GeoDataTour *tour = (feature ? geodata_cast<GeoDataTour>(feature) : nullptr)) {
385 return tour;
386 }
387
388 auto container = dynamic_cast<GeoDataContainer *>(feature);
389 if (container) {
390 QList<GeoDataFeature *>::Iterator end = container->end();
391 QList<GeoDataFeature *>::Iterator iter = container->begin();
392 for (; iter != end; ++iter) {
393 GeoDataTour *tour = findTour(*iter);
394 if (tour) {
395 return tour;
396 }
397 }
398 }
399 return nullptr;
400}
401
402void TourWidgetPrivate::mapCenterOn(const QModelIndex &index)
403{
404 QVariant coordinatesVariant = m_widget->model()->treeModel()->data(index, MarblePlacemarkModel::CoordinateRole);
405 if (!coordinatesVariant.isNull()) {
406 auto const coordinates = coordinatesVariant.value<GeoDataCoordinates>();
407 GeoDataLookAt lookat;
408 lookat.setCoordinates(coordinates);
409 lookat.setRange(coordinates.altitude());
410 m_widget->flyTo(lookat, Instant);
411 }
412}
413
414void TourWidgetPrivate::addFlyTo()
415{
416 auto flyTo = new GeoDataFlyTo();
417 auto lookat = new GeoDataLookAt(m_widget->lookAt());
418 lookat->setAltitude(lookat->range());
419 flyTo->setView(lookat);
420 bool isMainTrackEmpty = m_playback.mainTrackSize() == 0;
421 flyTo->setDuration(isMainTrackEmpty ? 0.0 : 1.0);
422 addTourPrimitive(flyTo);
423}
424
425void TourWidgetPrivate::addWait()
426{
427 auto wait = new GeoDataWait();
428 wait->setDuration(1.0);
429 addTourPrimitive(wait);
430}
431
432void TourWidgetPrivate::addSoundCue()
433{
434 auto soundCue = new GeoDataSoundCue();
435 addTourPrimitive(soundCue);
436}
437
438void TourWidgetPrivate::addPlacemark()
439{
440 // Get the normalized coordinates of the focus point. There will be automatically added a new
441 // placemark.
442 qreal lat = m_widget->focusPoint().latitude();
443 qreal lon = m_widget->focusPoint().longitude();
444 GeoDataCoordinates::normalizeLonLat(lon, lat);
445
446 auto document = new GeoDataDocument;
447 if (m_document->id().isEmpty()) {
448 if (m_document->name().isEmpty()) {
449 m_document->setId(QStringLiteral("untitled_tour"));
450 } else {
451 m_document->setId(m_document->name().trimmed().replace(QLatin1Char(' '), QLatin1Char('_')).toLower());
452 }
453 }
454 document->setTargetId(m_document->id());
455
456 auto placemark = new GeoDataPlacemark;
457 placemark->setCoordinate(lon, lat);
458 placemark->setVisible(true);
459 placemark->setBalloonVisible(true);
460 auto newStyle = new GeoDataStyle(*placemark->style());
461 newStyle->iconStyle().setIconPath(MarbleDirs::path(QStringLiteral("bitmaps/redflag_22.png")));
462 placemark->setStyle(GeoDataStyle::Ptr(newStyle));
463
464 document->append(placemark);
465
466 auto create = new GeoDataCreate;
467 create->append(document);
468 auto update = new GeoDataUpdate;
469 update->setCreate(create);
470 auto animatedUpdate = new GeoDataAnimatedUpdate;
471 animatedUpdate->setUpdate(update);
472
473 if (m_delegate->editAnimatedUpdate(animatedUpdate)) {
474 addTourPrimitive(animatedUpdate);
475 m_delegate->setDefaultFeatureId(placemark->id());
476 } else {
477 delete animatedUpdate;
478 }
479}
480
481void TourWidgetPrivate::addRemovePlacemark()
482{
483 auto deleteItem = new GeoDataDelete;
484 auto placemark = new GeoDataPlacemark;
485 placemark->setTargetId(m_delegate->defaultFeatureId());
486 deleteItem->append(placemark);
487 auto update = new GeoDataUpdate;
488 update->setDelete(deleteItem);
489 auto animatedUpdate = new GeoDataAnimatedUpdate;
490 animatedUpdate->setUpdate(update);
491 addTourPrimitive(animatedUpdate);
492}
493
494void TourWidgetPrivate::addChangePlacemark()
495{
496 auto change = new GeoDataChange;
497 GeoDataPlacemark *placemark = nullptr;
498 GeoDataFeature *lastFeature = m_delegate->findFeature(m_delegate->defaultFeatureId());
499 if (GeoDataPlacemark *target = (lastFeature != nullptr ? geodata_cast<GeoDataPlacemark>(lastFeature) : nullptr)) {
500 placemark = new GeoDataPlacemark(*target);
501 placemark->setTargetId(m_delegate->defaultFeatureId());
502 placemark->setId(QString());
503 } else {
504 placemark = new GeoDataPlacemark;
505 }
506 change->append(placemark);
507 auto update = new GeoDataUpdate;
508 update->setChange(change);
509 auto animatedUpdate = new GeoDataAnimatedUpdate;
510 animatedUpdate->setUpdate(update);
511 addTourPrimitive(animatedUpdate);
512}
513
514void TourWidgetPrivate::addTourPrimitive(GeoDataTourPrimitive *primitive)
515{
516 GeoDataObject *rootObject = rootIndexObject();
517 if (auto playlist = geodata_cast<GeoDataPlaylist>(rootObject)) {
518 QModelIndex currentIndex = m_tourUi.m_listView->currentIndex();
519 QModelIndex playlistIndex = m_widget->model()->treeModel()->index(playlist);
520 int row = currentIndex.isValid() ? currentIndex.row() + 1 : playlist->size();
521 m_widget->model()->treeModel()->addTourPrimitive(playlistIndex, primitive, row);
522 m_isChanged = true;
523 m_tourUi.m_actionSaveTour->setEnabled(true);
524
525 // Scrolling to the inserted item.
526 if (currentIndex.isValid()) {
527 m_tourUi.m_listView->scrollTo(currentIndex);
528 } else {
529 m_tourUi.m_listView->scrollToBottom();
530 }
531 }
532}
533
534void TourWidgetPrivate::deleteSelected()
535{
536 QString title = QObject::tr("Remove Selected Items");
537 QString text = QObject::tr("Are you sure want to remove selected items?");
539 dialog->setDefaultButton(QMessageBox::No);
540 if (dialog->exec() == QMessageBox::Yes) {
541 GeoDataObject *rootObject = rootIndexObject();
542 if (GeoDataPlaylist *playlist = (rootObject ? geodata_cast<GeoDataPlaylist>(rootObject) : nullptr)) {
543 QModelIndex playlistIndex = m_widget->model()->treeModel()->index(playlist);
544 QModelIndexList selected = m_tourUi.m_listView->selectionModel()->selectedIndexes();
545 std::sort(selected.begin(), selected.end(), [](const QModelIndex &a, const QModelIndex &b) {
546 return b < a;
547 });
548 QModelIndexList::iterator end = selected.end();
549 QModelIndexList::iterator iter = selected.begin();
550 for (; iter != end; ++iter) {
551 m_widget->model()->treeModel()->removeTourPrimitive(playlistIndex, iter->row());
552 }
553 m_isChanged = true;
554 m_tourUi.m_actionSaveTour->setEnabled(true);
555 }
556 }
557 delete dialog;
558}
559
560void TourWidgetPrivate::updateButtonsStates()
561{
562 QModelIndexList selectedIndexes = m_tourUi.m_listView->selectionModel()->selectedIndexes();
563 if (selectedIndexes.isEmpty()) {
564 m_tourUi.m_actionDelete->setEnabled(false);
565 m_tourUi.m_actionMoveDown->setEnabled(false);
566 m_tourUi.m_actionMoveUp->setEnabled(false);
567 } else {
568 m_tourUi.m_actionDelete->setEnabled(true);
569 std::sort(selectedIndexes.begin(), selectedIndexes.end(), std::less<QModelIndex>());
570 QModelIndexList::iterator end = selectedIndexes.end() - 1;
571 QModelIndexList::iterator start = selectedIndexes.begin();
572 m_tourUi.m_actionMoveUp->setEnabled((start->row() != 0)); // if we can move up enable action else disable.
573 GeoDataObject *rootObject = rootIndexObject();
574 if (GeoDataPlaylist *playlist = (rootObject ? geodata_cast<GeoDataPlaylist>(rootObject) : nullptr)) {
575 m_tourUi.m_actionMoveDown->setEnabled((end->row() != playlist->size() - 1)); // if we can move down enable action else disable.
576 }
577 }
578}
579
580void TourWidgetPrivate::moveUp()
581{
582 GeoDataObject *rootObject = rootIndexObject();
583 if (GeoDataPlaylist *playlist = (rootObject ? geodata_cast<GeoDataPlaylist>(rootObject) : nullptr)) {
584 QModelIndex playlistIndex = m_widget->model()->treeModel()->index(playlist);
585 QModelIndexList selected = m_tourUi.m_listView->selectionModel()->selectedIndexes();
586 std::sort(selected.begin(), selected.end(), std::less<QModelIndex>());
587 QModelIndexList::iterator end = selected.end();
588 QModelIndexList::iterator iter = selected.begin();
589 for (; iter != end; ++iter) {
590 int const index = iter->row();
591 Q_ASSERT(index > 0);
592 m_widget->model()->treeModel()->swapTourPrimitives(playlistIndex, index - 1, index);
593 }
594 m_isChanged = true;
595 m_tourUi.m_actionSaveTour->setEnabled(true);
596 updateButtonsStates();
597 }
598}
599
600void TourWidgetPrivate::moveDown()
601{
602 GeoDataObject *rootObject = rootIndexObject();
603 if (GeoDataPlaylist *playlist = (rootObject ? geodata_cast<GeoDataPlaylist>(rootObject) : nullptr)) {
604 QModelIndex playlistIndex = m_widget->model()->treeModel()->index(playlist);
605 QModelIndexList selected = m_tourUi.m_listView->selectionModel()->selectedIndexes();
606 std::sort(selected.begin(), selected.end(), [](const QModelIndex &a, const QModelIndex &b) {
607 return b < a;
608 });
609 QModelIndexList::iterator end = selected.end();
610 QModelIndexList::iterator iter = selected.begin();
611 for (; iter != end; ++iter) {
612 int const index = iter->row();
613 Q_ASSERT(index < playlist->size() - 1);
614 m_widget->model()->treeModel()->swapTourPrimitives(playlistIndex, index, index + 1);
615 }
616 m_isChanged = true;
617 m_tourUi.m_actionSaveTour->setEnabled(true);
618 updateButtonsStates();
619 }
620}
621
622GeoDataFeature *TourWidgetPrivate::getPlaylistFeature() const
623{
624 GeoDataObject *rootObject = rootIndexObject();
625 if (GeoDataPlaylist *playlist = (rootObject ? geodata_cast<GeoDataPlaylist>(rootObject) : nullptr)) {
626 GeoDataObject *object = playlist->parent();
627 if (GeoDataTour *tour = (object ? geodata_cast<GeoDataTour>(object) : nullptr)) {
628 return tour;
629 }
630 }
631 return nullptr;
632}
633
634void TourWidgetPrivate::updateRootIndex()
635{
636 GeoDataTour *tour = findTour(m_document);
637 if (tour) {
638 GeoDataPlaylist *playlist = tour->playlist();
639 if (playlist) {
640 m_tourUi.m_listView->setModel(m_widget->model()->treeModel());
641 m_tourUi.m_listView->setRootIndex(m_widget->model()->treeModel()->index(playlist));
642 QObject::connect(m_tourUi.m_listView->selectionModel(), SIGNAL(selectionChanged(QItemSelection, QItemSelection)), q, SLOT(updateButtonsStates()));
643 }
644 m_playback.setMarbleWidget(m_widget);
645 m_playback.setTour(tour);
646 m_tourUi.m_slider->setMaximum(m_playback.duration() * 100);
647 QTime nullTime(0, 0, 0);
648 QTime time = nullTime.addSecs(m_playback.duration());
649 m_tourUi.m_totalTime->setText(QStringLiteral("%L1:%L2").arg(time.minute(), 2, 10, QLatin1Char('0')).arg(time.second(), 2, 10, QLatin1Char('0')));
650 QObject::connect(&m_playback, SIGNAL(progressChanged(double)), q, SLOT(handlePlaybackProgress(double)));
651 q->stopPlaying();
652 m_tourUi.m_toolBarPlayback->setEnabled(true);
653 bool isPlaybackEmpty = m_playback.mainTrackSize() != 0;
654 m_tourUi.actionPlay->setEnabled(isPlaybackEmpty);
655 m_tourUi.m_slider->setEnabled(isPlaybackEmpty);
656 m_tourUi.m_actionRecord->setEnabled(isPlaybackEmpty);
657 m_tourUi.actionStop->setEnabled(false);
658 if (m_playback.mainTrackSize() > 0) {
659 if (dynamic_cast<PlaybackFlyToItem *>(m_playback.mainTrackItemAt(0))) {
660 QModelIndex playlistIndex = m_widget->model()->treeModel()->index(playlist);
661 for (int i = 0; playlist && i < playlist->size(); ++i) {
662 if (geodata_cast<GeoDataFlyTo>(playlist->primitive(i))) {
663 m_delegate->setFirstFlyTo(m_widget->model()->treeModel()->index(i, 0, playlistIndex));
664 break;
665 }
666 }
667 } else {
668 m_delegate->setFirstFlyTo(QPersistentModelIndex());
669 }
670 }
671 }
672}
673
674void TourWidget::addFlyTo()
675{
676 d->addFlyTo();
677 finishAddingItem();
678}
679
680void TourWidget::addWait()
681{
682 d->addWait();
683 finishAddingItem();
684}
685
686void TourWidget::addSoundCue()
687{
688 d->addSoundCue();
689 finishAddingItem();
690}
691
692void TourWidget::addPlacemark()
693{
694 d->addPlacemark();
695 finishAddingItem();
696}
697
698void TourWidget::addRemovePlacemark()
699{
700 d->addRemovePlacemark();
701 finishAddingItem();
702}
703
704void TourWidget::addChangePlacemark()
705{
706 d->addChangePlacemark();
707 finishAddingItem();
708}
709
710void TourWidget::deleteSelected()
711{
712 d->deleteSelected();
713 GeoDataFeature *feature = d->getPlaylistFeature();
714 if (feature) {
715 Q_EMIT featureUpdated(feature);
716 d->updateRootIndex();
717 }
718}
719
720void TourWidget::updateDuration()
721{
722 d->m_tourUi.m_slider->setMaximum(d->m_playback.duration() * 100);
723 QTime nullTime(0, 0, 0);
724 QTime totalTime = nullTime.addSecs(d->m_playback.duration());
725 d->m_tourUi.m_totalTime->setText(
726 QStringLiteral("%L1:%L2").arg(totalTime.minute(), 2, 10, QLatin1Char('0')).arg(totalTime.second(), 2, 10, QLatin1Char('0')));
727 d->m_tourUi.m_slider->setValue(0);
728 d->m_tourUi.m_elapsedTime->setText(QStringLiteral("%L1:%L2").arg(0, 2, 10, QLatin1Char('0')).arg(0, 2, 10, QLatin1Char('0')));
729}
730
731void TourWidget::finishAddingItem()
732{
733 GeoDataFeature *feature = d->getPlaylistFeature();
734 if (feature) {
735 Q_EMIT featureUpdated(feature);
736 d->updateRootIndex();
737 }
738}
739
740void TourWidget::moveDown()
741{
742 d->moveDown();
743 GeoDataFeature *feature = d->getPlaylistFeature();
744 if (feature) {
745 Q_EMIT featureUpdated(feature);
746 d->updateRootIndex();
747 }
748}
749
750void TourWidget::moveUp()
751{
752 d->moveUp();
753 GeoDataFeature *feature = d->getPlaylistFeature();
754 if (feature) {
755 Q_EMIT featureUpdated(feature);
756 d->updateRootIndex();
757 }
758}
759
760GeoDataObject *TourWidgetPrivate::rootIndexObject() const
761{
762 QModelIndex const rootIndex = m_tourUi.m_listView->rootIndex();
763 return rootIndex.isValid() ? static_cast<GeoDataObject *>(rootIndex.internalPointer()) : nullptr;
764}
765
766void TourWidgetPrivate::createTour()
767{
768 if (overrideModifications()) {
769 auto document = new GeoDataDocument();
770 document->setDocumentRole(UserDocument);
771 document->setName(QStringLiteral("New Tour"));
772 document->setId(QStringLiteral("new_tour"));
773 auto tour = new GeoDataTour();
774 tour->setName(QStringLiteral("New Tour"));
775 auto playlist = new GeoDataPlaylist;
776 tour->setPlaylist(playlist);
777 document->append(static_cast<GeoDataFeature *>(tour));
778 m_playback.setBaseUrl(QUrl::fromLocalFile(MarbleDirs::marbleDataPath()));
779 openDocument(document);
780 m_isChanged = true;
781 m_tourUi.m_actionSaveTour->setEnabled(true);
782 m_tourUi.m_slider->setEnabled(true);
783 }
784}
785
786bool TourWidgetPrivate::openDocument(GeoDataDocument *document)
787{
788 if (document) {
789 if (m_document) {
790 m_widget->model()->treeModel()->removeDocument(m_document);
791 delete m_document;
792 }
793 m_document = document;
794 m_widget->model()->treeModel()->addDocument(m_document);
795 m_isChanged = false;
796 updateRootIndex();
797 m_addPrimitiveButton->setEnabled(true);
798 m_tourUi.m_actionSaveTourAs->setEnabled(true);
799 m_tourUi.m_actionSaveTour->setEnabled(false);
800 m_isChanged = false;
801 return true;
802 }
803 return false;
804}
805
806void TourWidgetPrivate::saveTour()
807{
808 if (m_document) {
809 if (!m_document->fileName().isEmpty()) {
810 saveTourAs(m_document->fileName());
811 } else {
812 saveTourAs();
813 }
814 }
815}
816
817void TourWidgetPrivate::saveTourAs()
818{
819 if (m_document) {
820 QString const filename = QFileDialog::getSaveFileName(q, QObject::tr("Save Tour as"), QDir::homePath(), QObject::tr("KML Tours (*.kml)"));
821 if (!filename.isEmpty()) {
822 saveTourAs(filename);
823 }
824 }
825}
826
827bool TourWidgetPrivate::saveTourAs(const QString &filename)
828{
829 if (!filename.isEmpty()) {
830 if (GeoDataDocumentWriter::write(filename, *m_document)) {
831 m_tourUi.m_actionSaveTour->setEnabled(false);
832 m_isChanged = false;
833 GeoDataDocument *document = m_document;
834 if (!document->fileName().isNull()) {
835 m_widget->model()->removeGeoData(document->fileName());
836 }
837 m_widget->model()->addGeoDataFile(filename);
838 m_document->setFileName(filename);
839 return true;
840 }
841 }
842 return false;
843}
844
845void TourWidgetPrivate::captureTour()
846{
847 auto widget = new MarbleWidget;
848 widget->setMapThemeId(m_widget->mapThemeId());
849 widget->resize(1280, 720);
850
851 m_widget->model()->treeModel()->removeDocument(m_document);
852 widget->model()->treeModel()->addDocument(m_document);
853
854 GeoDataTour *tour = findTour(m_document);
855 auto playback = new TourPlayback;
856 playback->setMarbleWidget(widget);
857 playback->setTour(tour);
858
859 m_tourUi.m_listView->setModel(widget->model()->treeModel());
860 if (tour) {
861 m_tourUi.m_listView->setRootIndex(widget->model()->treeModel()->index(tour->playlist()));
862 m_tourUi.m_listView->repaint();
863
864 QPointer<TourCaptureDialog> tourCaptureDialog = new TourCaptureDialog(widget, m_widget);
865 tourCaptureDialog->setDefaultFilename(tour->name());
866 tourCaptureDialog->setTourPlayback(playback);
867 tourCaptureDialog->exec();
868 }
869
870 delete playback;
871 widget->model()->treeModel()->removeDocument(m_document);
872 m_widget->model()->treeModel()->addDocument(m_document);
873 updateRootIndex();
874 delete widget;
875}
876
877bool TourWidgetPrivate::overrideModifications()
878{
879 if (m_document && m_isChanged) {
880 QString title = QObject::tr("Discard Changes");
881 QString text = QObject::tr("Are you sure want to discard all unsaved changes and close current document?");
883 dialog->setDefaultButton(QMessageBox::No);
884 if (dialog->exec() != QMessageBox::Yes) {
885 delete dialog;
886 return false;
887 }
888 delete dialog;
889 }
890 return true;
891}
892
893bool TourWidget::openTour(const QString &filename)
894{
895 return d->openFile(filename);
896}
897
898void TourWidgetPrivate::handlePlaybackProgress(const double position)
899{
900 if (!m_tourUi.m_slider->isSliderDown()) {
901 m_tourUi.m_slider->setValue(position * 100);
902 QTime nullTime(0, 0, 0);
903 QTime time = nullTime.addSecs(position);
904 m_tourUi.m_elapsedTime->setText(QStringLiteral("%L1:%L2").arg(time.minute(), 2, 10, QLatin1Char('0')).arg(time.second(), 2, 10, QLatin1Char('0')));
905 }
906}
907
908void TourWidget::setHighlightedItemIndex(int index)
909{
910 GeoDataObject *rootObject = d->rootIndexObject();
911 auto playlist = static_cast<GeoDataPlaylist *>(rootObject);
912 QModelIndex playlistIndex = d->m_widget->model()->treeModel()->index(playlist);
913
914 // Only flyTo and wait items have duration, so the other types have to be skipped.
915 int searchedIndex = 0;
916 for (int i = 0; i < playlist->size(); i++) {
917 QModelIndex currentIndex = d->m_widget->model()->treeModel()->index(i, 0, playlistIndex);
918 auto object = qvariant_cast<GeoDataObject *>(currentIndex.data(MarblePlacemarkModel::ObjectPointerRole));
919
920 if (geodata_cast<GeoDataFlyTo>(object) || geodata_cast<GeoDataWait>(object))
921 ++searchedIndex;
922
923 if (index == searchedIndex) {
924 d->m_tourUi.m_listView->selectionModel()->setCurrentIndex(currentIndex, QItemSelectionModel::NoUpdate);
925 d->m_tourUi.m_listView->scrollTo(currentIndex);
926 break;
927 }
928 }
929 d->m_tourUi.m_listView->viewport()->update();
930}
931
932void TourWidget::removeHighlight()
933{
934 QModelIndex index;
935
936 // Restoring the CurrentIndex to the previously selected item
937 // or clearing it if there was no selected item.
938 if (d->m_tourUi.m_listView->selectionModel()->hasSelection()) {
939 index = d->m_tourUi.m_listView->selectionModel()->selectedIndexes().last();
940 } else {
941 index = QModelIndex();
942 }
943
944 d->m_tourUi.m_listView->selectionModel()->setCurrentIndex(index, QItemSelectionModel::NoUpdate);
945 d->m_tourUi.m_listView->viewport()->update();
946}
947
948bool TourWidget::isPlaying() const
949{
950 return d->m_playState;
951}
952
953}
954
955#include "moc_TourWidget.cpp"
This file contains the headers for MarbleModel.
This file contains the headers for MarbleWidget.
Q_SCRIPTABLE Q_NOREPLY void start()
AKONADI_CALENDAR_EXPORT KCalendarCore::Event::Ptr event(const Akonadi::Item &item)
void update(Part *part, const QByteArray &data, qint64 dataSize)
QAction * create(StandardAction id, const Receiver *recvr, Func slot, QObject *parent, std::optional< Qt::ConnectionType > connectionType=std::nullopt)
const QList< QKeySequence > & end()
Binds a QML item to a specific geodetic location in screen coordinates.
virtual QModelIndex index(int row, int column, const QModelIndex &parent) const const=0
void setIcon(const QIcon &icon)
QString homePath()
QString getOpenFileName(QWidget *parent, const QString &caption, const QString &dir, const QString &filter, QString *selectedFilter, Options options)
QString getSaveFileName(QWidget *parent, const QString &caption, const QString &dir, const QString &filter, QString *selectedFilter, Options options)
StandardButton question(QWidget *parent, const QString &title, const QString &text, StandardButtons buttons, StandardButton defaultButton)
QVariant data(int role) const const
void * internalPointer() const const
bool isValid() const const
const QAbstractItemModel * model() const const
int row() const const
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
QString tr(const char *sourceText, const char *disambiguation, int n)
bool isEmpty() const const
ActionsContextMenu
Key_Delete
ControlModifier
typedef WindowFlags
QFuture< ArgsType< Signal > > connect(Sender *sender, Signal signal)
QTime addSecs(int s) const const
int minute() const const
int second() const const
QUrl fromLocalFile(const QString &localFile)
void * data()
bool isNull() const const
T value() const const
void setContentsMargins(const QMargins &margins)
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Mon Nov 18 2024 12:15:46 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.