1/*
2 Copyright (C) 2014 by Elvis Angelaccio <elvis.angelaccio@kdemail.net>
3
4 This file is part of Kronometer.
5
6 Kronometer is free software: you can redistribute it and/or modify
7 it under the terms of the GNU General Public License as published by
8 the Free Software Foundation, either version 2 of the License, or
9 (at your option) any later version.
10
11 Kronometer is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 GNU General Public License for more details.
15
16 You should have received a copy of the GNU General Public License
17 along with Kronometer. If not, see <http://www.gnu.org/licenses/>.
18*/
19
20#include "mainwindow.h"
21
22#include <KApplication>
23#include <KLocale>
24#include <KAction>
25#include <KActionCollection>
26#include <KStatusBar>
27#include <KConfigDialog>
28#include <KFileDialog>
29#include <KMessageBox>
30#include <KIO/NetAccess>
31#include <KSaveFile>
32
33#include <QTableView>
34#include <QSplitter>
35#include <QClipboard>
36#include <QSortFilterProxyModel>
37#include <QDomDocument>
38#include <QPointer>
39
40#include "stopwatch.h"
41#include "timedisplay.h"
42#include "lapmodel.h"
43#include "settings.h"
44#include "widgets/generalsettings.h"
45#include "widgets/fontsettings.h"
46#include "widgets/colorsettings.h"
47#include "widgets/savesettings.h"
48#include "widgets/guisettings.h"
49
50namespace
51{
52 const QString START_KEY = "start";
53 const QString PAUSE_KEY = "pause";
54 const QString RESET_KEY = "reset";
55 const QString LAP_KEY = "lap";
56 const QString EXPORT_KEY = "export_laps";
57
58 const QString WINDOW_TITLE = "Kronometer"; /** Default Window title */
59 const QString QT_PLACE_HOLDER = "[*]"; /** Qt standard placeholder for setWindowModified() */
60
61 const QString XML_MIMETYPE = "application/xml";
62 const QString CSV_MIMETYPE = "text/csv";
63 const QString XML_EXTENSION = ".xml";
64 const QString CSV_EXTENSION = ".csv";
65
66 // kronometerui.rc states
67 const QString INACTIVE_STATE = "inactive";
68 const QString RUNNING_STATE = "running";
69 const QString PAUSED_STATE = "paused";
70 const QString PAUSED_FILE_STATE = "pausedFile"; /** An open file has been paused */
71
72 // XML strings
73 const QString STOPWATCH_TAG = "stopwatch";
74 const QString LAPS_TAG = "laps";
75 const QString LAP_TAG = "lap";
76 const QString TIME_TAG = "time";
77 const QString ROOT_TAG = "kronometer";
78 const QString PERSISTENCE_ATTR = "msec";
79 const QString TYPE_ATTR = "type";
80 const QString LAP_ID_ATTR = "id";
81 const QString REL_TYPE = "relative";
82 const QString ABS_TYPE = "absolute";
83}
84
85
86MainWindow::MainWindow(QWidget *parent, const QString& file) : KXmlGuiWindow(parent), unsavedTimes(false)
87{
88 stopwatch = new Stopwatch(this);
89 stopwatchDisplay = new TimeDisplay(this);
90 connect(stopwatch, SIGNAL(time(QTime)), stopwatchDisplay, SLOT(onTime(QTime))); // bind stopwatch to its display
91
92 setupCentralWidget();
93 setupStatusBar();
94 setupActions();
95 loadSettings();
96
97 setWindowTitle(WINDOW_TITLE + QT_PLACE_HOLDER);
98
99 if (not file.isEmpty()) {
100 openFile(file);
101 }
102}
103
104bool MainWindow::queryClose()
105{
106 if (stopwatch->isInactive() or not KronometerConfig::askOnExit()) {
107 return true; // exit without ask
108 }
109
110 if (stopwatch->isRunning()) {
111 stopwatch->onPause();
112 paused();
113 }
114
115 int buttonCode;
116
117 if (fileName.isEmpty()) {
118 buttonCode = KMessageBox::warningYesNoCancel(this, i18n("Save times on a new file?"));
119
120 switch (buttonCode) {
121 case KMessageBox::Yes:
122 return saveFileAs();
123 case KMessageBox::No:
124 return true;
125 default: // cancel
126 return false;
127 }
128 }
129 else if (unsavedTimes) {
130 QFileInfo fileInfo(fileName);
131 buttonCode = KMessageBox::warningYesNoCancel(this, i18n("Save times to file %1?", fileInfo.fileName()));
132
133 switch (buttonCode) {
134 case KMessageBox::Yes:
135 // save document here. If saving fails, return false;
136 return saveFile();
137 case KMessageBox::No:
138 return true;
139 default: // cancel
140 return false;
141 }
142 }
143
144 return true; // there is an open file, but times are already saved.
145}
146
147void MainWindow::running()
148{
149 statusLabel->setText(i18n("Running..."));
150
151 unsavedTimes = true;
152 setWindowModified(unsavedTimes);
153
154 stateChanged(RUNNING_STATE);
155}
156
157void MainWindow::paused()
158{
159 startAction->setText(i18n("Re&sume"));
160 statusLabel->setText(i18n("Paused"));
161
162 if (not fileName.isEmpty()) {
163 stateChanged(PAUSED_FILE_STATE);
164 }
165 else {
166 stateChanged(PAUSED_STATE);
167 }
168
169 // the export action can be used only if there are laps (in both the paused states).
170 // so, it can't be enabled directly from kronometerui.rc
171 if (not lapModel->isEmpty()) {
172 exportAction->setEnabled(true);
173 }
174}
175
176void MainWindow::inactive()
177{
178 startAction->setText(i18n("&Start"));
179 statusLabel->setText(i18n("Inactive"));
180
181 unsavedTimes = false;
182 setWindowModified(unsavedTimes);
183
184 stateChanged(INACTIVE_STATE);
185}
186
187void MainWindow::showSettings()
188{
189 if (KConfigDialog::showDialog("settings")) {
190 return;
191 }
192
193 KConfigDialog* dialog = new KConfigDialog(this, "settings", KronometerConfig::self());
194
195 dialog->showButtonSeparator(true);
196
197 KPageWidgetItem *generalPage = dialog->addPage(new GeneralSettings(this), i18n("General settings"));
198 generalPage->setIcon(KIcon(KApplication::windowIcon()));
199
200 KPageWidgetItem *fontPage = dialog->addPage(new FontSettings(this), i18n("Font settings"));
201 fontPage->setIcon(KIcon("preferences-desktop-font"));
202
203 KPageWidgetItem *colorPage = dialog->addPage(new ColorSettings(this), i18n("Color settings"));
204 colorPage->setIcon(KIcon("fill-color"));
205
206 KPageWidgetItem *guiPage = dialog->addPage(new GuiSettings(this), i18n("Interface settings"));
207 guiPage->setIcon(KIcon("preferences-desktop-theme"));
208
209 KPageWidgetItem *savePage = dialog->addPage(new SaveSettings(this), i18n("Save settings"));
210 savePage->setIcon(KIcon("document-save"));
211
212 connect(dialog, SIGNAL(settingsChanged(QString)), this, SLOT(writeSettings(QString)));
213
214 dialog->show();
215}
216
217void MainWindow::writeSettings(const QString& dialogName)
218{
219 Q_UNUSED(dialogName);
220 KronometerConfig::self()->writeConfig();
221
222 loadSettings();
223}
224
225void MainWindow::updateLapDock()
226{
227 lapView->resizeColumnsToContents();
228 lapView->horizontalHeader()->setStretchLastSection(true);
229 lapView->selectRow(lapModel->rowCount(QModelIndex()) - 1); // rows indexes start from 0
230}
231
232void MainWindow::newFile()
233{
234 MainWindow *window = new MainWindow();
235 window->show();
236}
237
238void MainWindow::openFile()
239{
240 QPointer<KFileDialog> dialog = new KFileDialog(KUrl(), QString(), this);
241 dialog->setOperationMode(KFileDialog::Opening);
242 dialog->setWindowTitle(i18n("Choose a Kronometer save file"));
243
244 QStringList mimeTypes;
245 mimeTypes << XML_MIMETYPE;
246 dialog->setMimeFilter(mimeTypes);
247
248 if (dialog->exec() == QDialog::Accepted) {
249 QString file = dialog->selectedFile();
250
251 if (not file.isEmpty()) {
252 MainWindow *window = new MainWindow(nullptr, file);
253 window->show();
254 }
255 }
256
257 delete dialog;
258}
259
260bool MainWindow::saveFile()
261{
262 return saveFileAs(fileName);
263}
264
265bool MainWindow::saveFileAs()
266{
267 QPointer<KFileDialog> dialog = new KFileDialog(KUrl(), QString(), this);
268 dialog->setOperationMode(KFileDialog::Saving);
269 dialog->setConfirmOverwrite(true);
270 dialog->setWindowTitle(i18n("Choose Kronometer save file destination"));
271
272 QStringList mimeTypes;
273 mimeTypes << XML_MIMETYPE;
274 dialog->setMimeFilter(mimeTypes);
275
276 bool rc = false;
277 if (dialog->exec() == QDialog::Accepted) {
278 rc = saveFileAs(dialog->selectedFile());
279 }
280
281 delete dialog;
282 return rc;
283}
284
285void MainWindow::exportLapsAs()
286{
287 QPointer<KFileDialog> dialog = new KFileDialog(KUrl(), QString(), this);
288 dialog->setOperationMode(KFileDialog::Saving);
289 dialog->setConfirmOverwrite(true);
290 dialog->setWindowTitle(i18n("Choose export file destination"));
291
292 QStringList mimeTypes;
293 mimeTypes << XML_MIMETYPE << CSV_MIMETYPE;
294 dialog->setMimeFilter(mimeTypes, CSV_MIMETYPE);
295
296 if (dialog->exec() == QDialog::Accepted) {
297 exportLapsAs(dialog->selectedFile(), dialog->currentMimeFilter());
298 }
299
300 delete dialog;
301}
302
303void MainWindow::copyToClipboard()
304{
305 KApplication::clipboard()->setText(stopwatchDisplay->currentTime());
306}
307
308void MainWindow::setupCentralWidget()
309{
310 centralSplitter = new QSplitter(this);
311
312 lapModel = new LapModel(this);
313 proxyModel = new QSortFilterProxyModel(this);
314 proxyModel->setSourceModel(lapModel);
315
316 lapView = new QTableView(this);
317 lapView->setModel(proxyModel);
318 lapView->setSelectionBehavior(QAbstractItemView::SelectRows);
319 lapView->setGridStyle(Qt::DotLine);
320 lapView->verticalHeader()->hide();
321 lapView->resizeColumnsToContents();
322 lapView->horizontalHeader()->setStretchLastSection(true);
323 lapView->setSortingEnabled(true);
324 lapView->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Ignored);
325
326 centralSplitter->setOrientation(Qt::Horizontal);
327 centralSplitter->setChildrenCollapsible(false);
328 centralSplitter->addWidget(stopwatchDisplay);
329 centralSplitter->addWidget(lapView);
330
331 setCentralWidget(centralSplitter);
332}
333
334void MainWindow::setupStatusBar()
335{
336 statusLabel = new QLabel(this);
337 statusLabel->setToolTip(i18n("Current chronometer status"));
338
339 statusBar()->addWidget(statusLabel);
340}
341
342void MainWindow::setupActions()
343{
344 startAction = new KAction(this);
345 pauseAction = new KAction(this);
346 resetAction = new KAction(this);
347 lapAction = new KAction(this);
348 exportAction = new KAction(this);
349
350 startAction->setIcon(KIcon("player-time"));
351 startAction->setShortcut(Qt::Key_Space);
352
353 pauseAction->setText(i18n("&Pause")); // pauseAction/resetAction have fixed text (startAction don't)
354 pauseAction->setIcon(KIcon("media-playback-pause"));
355 pauseAction->setShortcut(Qt::Key_Space);
356
357 resetAction->setText(i18n("&Reset"));
358 resetAction->setIcon(KIcon("edit-clear-history"));
359 resetAction->setShortcut(Qt::Key_F5);
360
361 lapAction->setText(i18n("&Lap"));
362 lapAction->setIcon(KIcon("chronometer"));
363 lapAction->setShortcut(Qt::Key_Return);
364
365 exportAction->setText(i18n("&Export laps as..."));
366 exportAction->setIcon(KIcon("document-export"));
367
368 actionCollection()->addAction(START_KEY, startAction);
369 actionCollection()->addAction(PAUSE_KEY, pauseAction);
370 actionCollection()->addAction(RESET_KEY, resetAction);
371 actionCollection()->addAction(LAP_KEY, lapAction);
372 actionCollection()->addAction(EXPORT_KEY, exportAction);
373
374 // triggers for Stopwatch "behavioral" slots
375 connect(startAction, SIGNAL(triggered(bool)), stopwatch, SLOT(onStart()));
376 connect(pauseAction, SIGNAL(triggered(bool)), stopwatch, SLOT(onPause()));
377 connect(resetAction, SIGNAL(triggered(bool)), stopwatch, SLOT(onReset()));
378 connect(lapAction, SIGNAL(triggered(bool)), stopwatch, SLOT(onLap()));
379
380 // triggers for LapModel slots
381 connect(resetAction, SIGNAL(triggered(bool)), lapModel, SLOT(onClear()));
382 connect(stopwatch, SIGNAL(lap(QTime)), lapModel, SLOT(onLap(QTime)));
383
384 // triggers for MainWindow "gui" slots
385 connect(startAction, SIGNAL(triggered(bool)), this, SLOT(running()));
386 connect(pauseAction, SIGNAL(triggered(bool)), this, SLOT(paused()));
387 connect(resetAction, SIGNAL(triggered(bool)), this, SLOT(inactive()));
388 connect(lapAction, SIGNAL(triggered(bool)), this, SLOT(updateLapDock()));
389
390 // File menu triggers
391 KStandardAction::quit(this, SLOT(close()), actionCollection());
392 KStandardAction::preferences(this, SLOT(showSettings()), actionCollection());
393 KStandardAction::openNew(this, SLOT(newFile()), actionCollection());
394 KStandardAction::save(this, SLOT(saveFile()), actionCollection());
395 KStandardAction::saveAs(this, SLOT(saveFileAs()), actionCollection());
396 KStandardAction::open(this, SLOT(openFile()), actionCollection());
397 KStandardAction::copy(this, SLOT(copyToClipboard()), actionCollection());
398 connect(exportAction, SIGNAL(triggered(bool)), this, SLOT(exportLapsAs()));
399
400 setupGUI(Default, "kronometerui.rc");
401
402 inactive(); // inactive state is the default
403}
404
405
406void MainWindow::loadSettings()
407{
408 TimeFormat timeFormat(
409 KronometerConfig::showHours(),
410 KronometerConfig::showMinutes(),
411 KronometerConfig::showSeconds(),
412 KronometerConfig::showTenths(),
413 KronometerConfig::showHundredths(),
414 KronometerConfig::showMilliseconds()
415 );
416
417 lapModel->setTimeFormat(timeFormat);
418 timeFormat.showDividers(false);
419 stopwatchDisplay->setTimeFormat(timeFormat);
420 stopwatchDisplay->setHourFont(KronometerConfig::hourFont());
421 stopwatchDisplay->setMinFont(KronometerConfig::minFont());
422 stopwatchDisplay->setSecFont(KronometerConfig::secFont());
423 stopwatchDisplay->setFracFont(KronometerConfig::fracFont());
424 stopwatchDisplay->setBackgroundColor(KronometerConfig::backgroundColor());
425 stopwatchDisplay->setTextColor(KronometerConfig::textColor());
426 stopwatchDisplay->showHeaders(KronometerConfig::showTimeHeaders());
427
428 setupGranularity(KronometerConfig::showTenths(), KronometerConfig::showHundredths(), KronometerConfig::showMilliseconds());
429}
430
431
432void MainWindow::setupGranularity(bool tenths, bool hundredths, bool msec)
433{
434 if (msec) {
435 stopwatch->setGranularity(Stopwatch::MILLISECONDS);
436 }
437 else if (hundredths) {
438 stopwatch->setGranularity(Stopwatch::HUNDREDTHS);
439 }
440 else if (tenths) {
441 stopwatch->setGranularity(Stopwatch::TENTHS);
442 }
443 else {
444 stopwatch->setGranularity(Stopwatch::SECONDS);
445 }
446}
447
448bool MainWindow::saveFileAs(const QString& name)
449{
450 if (name.isEmpty()) {
451 return false;
452 }
453
454 QString saveName = name;
455
456 if (not saveName.endsWith(XML_EXTENSION)) {
457 saveName += XML_EXTENSION;
458 }
459
460 KSaveFile saveFile(saveName);
461 if (!saveFile.open()) {
462 KMessageBox::error(this, i18n("Failed to open file"));
463 return false;
464 }
465
466 // OLD: persistence using binary files
467 //QDataStream stream(&saveFile);
468 //stopwatch->serialize(stream); // save stopwatch time
469 //stream << *lapModel; // save laps
470
471 // NEW: persistence using XML files
472 QTextStream stream(&saveFile);
473 createXmlSaveFile(stream);
474
475
476 bool isSaveSuccessfull = saveFile.finalize();
477 saveFile.close();
478
479 if (isSaveSuccessfull) {
480 fileName = saveName;
481
482 unsavedTimes = false;
483 setWindowModified(unsavedTimes);
484 return true;
485 }
486 else {
487 return false;
488 }
489}
490
491void MainWindow::openFile(const QString& name)
492{
493 QString buffer;
494
495 if (KIO::NetAccess::download(name, buffer, this)) {
496 QFile file(buffer);
497 file.open(QIODevice::ReadOnly);
498
499 // OLD: persistence using binary files
500 //QDataStream stream(&file);
501 //stopwatch->deserialize(stream); // load stopwatch time
502 //stream >> *lapModel; // load laps
503
504 // NEW: persistence using XML files
505 QDomDocument doc;
506 QString errorMsg;
507
508 if (doc.setContent(&file, &errorMsg)) {
509 if (parseXmlSaveFile(doc)) {
510 paused(); // enter in paused state
511 fileName = name;
512
513 KIO::NetAccess::removeTempFile(buffer);
514 QFileInfo fileInfo(fileName);
515 setWindowTitle(WINDOW_TITLE + " - " + fileInfo.fileName() + QT_PLACE_HOLDER);
516 }
517 else {
518 KIO::NetAccess::removeTempFile(buffer);
519 close(); // files are opened in a new window, so if the open fails the new window has to be closed.
520 }
521 }
522 else {
523 KMessageBox::error(this, i18n("Cannot open file: %1", errorMsg));
524 KIO::NetAccess::removeTempFile(buffer);
525 close();
526 }
527 }
528 else {
529 KMessageBox::error(this, KIO::NetAccess::lastErrorString());
530 }
531}
532
533void MainWindow::createXmlSaveFile(QTextStream& out)
534{
535 QDomDocument doc;
536 QDomProcessingInstruction metaData = doc.createProcessingInstruction("xml", "version='1.0' encoding='UTF-8'");
537 QDomComment timestampComment = doc.createComment(timestampMessage());
538 QDomElement rootElement = doc.createElement(ROOT_TAG);
539
540 QDomElement stopwatchElement = doc.createElement(STOPWATCH_TAG);
541 stopwatch->serialize(stopwatchElement, PERSISTENCE_ATTR);
542 QDomElement stopwatchTime = doc.createElement(TIME_TAG);
543 stopwatchTime.setAttribute(TYPE_ATTR, ABS_TYPE);
544 stopwatchTime.appendChild(doc.createTextNode(stopwatchDisplay->currentTime()));
545 stopwatchElement.appendChild(stopwatchTime);
546
547 QDomElement lapsElement = doc.createElement(LAPS_TAG);
548
549 for (int i = 0; i < lapModel->rowCount(QModelIndex()); i++) {
550 QDomElement lap = doc.createElement(LAP_TAG);
551 lap.setAttribute(LAP_ID_ATTR, i);
552 lapModel->lapToXml(lap, PERSISTENCE_ATTR, i);
553
554 QDomElement relTime = doc.createElement(TIME_TAG);
555 relTime.setAttribute(TYPE_ATTR, REL_TYPE);
556 relTime.appendChild(doc.createTextNode(lapModel->relativeLapTime(i)));
557
558 QDomElement absTime = doc.createElement(TIME_TAG);
559 absTime.setAttribute(TYPE_ATTR, ABS_TYPE);
560 absTime.appendChild(doc.createTextNode(lapModel->absoluteLapTime(i)));
561
562 lap.appendChild(relTime);
563 lap.appendChild(absTime);
564 lapsElement.appendChild(lap);
565 }
566
567 rootElement.appendChild(stopwatchElement);
568 rootElement.appendChild(lapsElement);
569 doc.appendChild(metaData);
570 doc.appendChild(timestampComment);
571 doc.appendChild(rootElement);
572 doc.save(out, KronometerConfig::saveFileIndentSize());
573}
574
575bool MainWindow::parseXmlSaveFile(const QDomDocument& doc)
576{
577 QDomElement rootElement = doc.namedItem(ROOT_TAG).toElement();
578
579 if (rootElement.isNull()) {
580 KMessageBox::error(this, i18n("Invalid XML file"));
581 return false;
582 }
583
584 QDomElement stopwatchElement = rootElement.namedItem(STOPWATCH_TAG).toElement();
585 QDomElement lapsElement = rootElement.namedItem(LAPS_TAG).toElement();
586
587 if (stopwatchElement.isNull() or lapsElement.isNull()) {
588 KMessageBox::error(this, i18n("Incomplete Kronometer save file"));
589 return false;
590 }
591
592 stopwatch->deserialize(stopwatchElement, PERSISTENCE_ATTR);
593
594 QDomElement lap = lapsElement.firstChildElement(LAP_TAG);
595
596 while (not lap.isNull()) {
597 lapModel->lapFromXml(lap, PERSISTENCE_ATTR);
598 lap = lap.nextSiblingElement(LAP_TAG);
599 }
600
601 return true;
602}
603
604void MainWindow::exportLapsAs(const QString& name, const QString& mimetype)
605{
606 if (name.isEmpty()) {
607 return;
608 }
609
610 QString exportName = name;
611
612 if (mimetype == XML_MIMETYPE) {
613 if (not exportName.endsWith(XML_EXTENSION)) {
614 exportName += XML_EXTENSION;
615 }
616
617 KSaveFile exportFile(exportName);
618 exportFile.open();
619
620 QTextStream stream(&exportFile);
621 exportLapsAsXml(stream);
622
623 exportFile.finalize();
624 exportFile.close();
625 }
626
627 else if (mimetype == CSV_MIMETYPE) {
628 if (not exportName.endsWith(CSV_EXTENSION)) {
629 exportName += CSV_EXTENSION;
630 }
631
632 KSaveFile exportFile(exportName);
633 exportFile.open();
634
635 QTextStream stream(&exportFile);
636 exportLapsAsCsv(stream);
637
638 exportFile.finalize();
639 exportFile.close();
640 }
641}
642
643void MainWindow::exportLapsAsXml(QTextStream& out)
644{
645 QDomDocument doc;
646 QDomProcessingInstruction metaData = doc.createProcessingInstruction("xml", "version='1.0' encoding='UTF-8'");
647 QDomComment timestampComment = doc.createComment(timestampMessage());
648 QDomElement rootElement = doc.createElement(ROOT_TAG);
649
650 QDomElement lapsElement = doc.createElement(LAPS_TAG);
651
652 for (int i = 0; i < lapModel->rowCount(QModelIndex()); i++) {
653 QDomElement lap = doc.createElement(LAP_TAG);
654 lap.setAttribute(LAP_ID_ATTR, i);
655
656 QDomElement relTime = doc.createElement(TIME_TAG);
657 relTime.setAttribute(TYPE_ATTR, REL_TYPE);
658 relTime.appendChild(doc.createTextNode(lapModel->relativeLapTime(i)));
659
660 QDomElement absTime = doc.createElement(TIME_TAG);
661 absTime.setAttribute(TYPE_ATTR, ABS_TYPE);
662 absTime.appendChild(doc.createTextNode(lapModel->absoluteLapTime(i)));
663
664 lap.appendChild(relTime);
665 lap.appendChild(absTime);
666 lapsElement.appendChild(lap);
667 }
668
669 rootElement.appendChild(lapsElement);
670 doc.appendChild(metaData);
671 doc.appendChild(timestampComment);
672 doc.appendChild(rootElement);
673 doc.save(out, KronometerConfig::exportedXmlIndentSize());
674}
675
676void MainWindow::exportLapsAsCsv(QTextStream& out)
677{
678 out << '#' << timestampMessage() << '\r' << '\n';
679 out << '#' << i18n("Lap number,Lap time,Global time") << '\r' << '\n';
680
681 for (int i = 0; i < lapModel->rowCount(QModelIndex()); i++) {
682 out << i << ',' << lapModel->relativeLapTime(i) << ',' << lapModel->absoluteLapTime(i) << '\r' << '\n';
683 }
684}
685
686QString MainWindow::timestampMessage()
687{
688 QDateTime timestamp = QDateTime::currentDateTime();
689
690 return i18n("Created by Kronometer on %1", timestamp.toString(Qt::DefaultLocaleLongDate));
691}
692
693