2017-04-27 18:26:05 +00:00
|
|
|
// SPDX-License-Identifier: GPL-2.0
|
2016-04-05 05:02:03 +00:00
|
|
|
#include "desktop-widgets/simplewidgets.h"
|
|
|
|
#include "qt-models/filtermodels.h"
|
2013-06-04 21:51:27 +00:00
|
|
|
|
2013-09-27 15:52:01 +00:00
|
|
|
#include <QProcess>
|
2014-02-13 15:43:55 +00:00
|
|
|
#include <QFileDialog>
|
2014-04-25 17:39:40 +00:00
|
|
|
#include <QShortcut>
|
2015-02-09 20:58:40 +00:00
|
|
|
#include <QKeyEvent>
|
2015-02-11 15:58:23 +00:00
|
|
|
#include <QAction>
|
2015-11-16 04:56:24 +00:00
|
|
|
#include <QDesktopServices>
|
|
|
|
#include <QToolTip>
|
2020-06-20 20:32:01 +00:00
|
|
|
#include <QCompleter>
|
2015-01-17 09:43:52 +00:00
|
|
|
|
2016-04-05 05:02:03 +00:00
|
|
|
#include "core/file.h"
|
2020-05-27 21:09:30 +00:00
|
|
|
#include "core/filterpreset.h"
|
2019-03-04 22:20:29 +00:00
|
|
|
#include "core/divesite.h"
|
2016-04-05 05:02:03 +00:00
|
|
|
#include "desktop-widgets/mainwindow.h"
|
2018-06-03 20:15:19 +00:00
|
|
|
#include "core/qthelper.h"
|
2014-11-25 20:22:02 +00:00
|
|
|
#include "libdivecomputer/parser.h"
|
2016-04-05 05:02:03 +00:00
|
|
|
#include "desktop-widgets/divelistview.h"
|
2019-11-24 14:02:34 +00:00
|
|
|
#include "core/selection.h"
|
2015-09-03 18:56:37 +00:00
|
|
|
#include "profile-widget/profilewidget2.h"
|
2019-11-13 14:08:40 +00:00
|
|
|
#include "commands/command.h"
|
2018-03-15 19:21:40 +00:00
|
|
|
#include "core/metadata.h"
|
2024-05-28 19:31:11 +00:00
|
|
|
#include "core/range.h"
|
2019-05-30 16:29:36 +00:00
|
|
|
#include "core/tag.h"
|
2013-06-17 16:41:00 +00:00
|
|
|
|
2014-02-28 04:09:57 +00:00
|
|
|
void RenumberDialog::buttonClicked(QAbstractButton *button)
|
2013-06-17 16:41:00 +00:00
|
|
|
{
|
2015-02-28 04:42:37 +00:00
|
|
|
if (ui.buttonBox->buttonRole(button) == QDialogButtonBox::AcceptRole) {
|
Undo: fix multi-level undo of delete-dive and remove-dive-from-trip
The original undo-code was fundamentally broken. Not only did it leak
resources (copied trips were never freed), it also kept references
to trips or dives that could be changed by other commands. Thus,
anything more than a single undo could lead to crashes.
Two ways of fixing this were considered
1) Don't store pointers, but unique dive-ids and trip-ids.
Whereas such unique ids exist for dives, they would have to be
implemented for trips.
2) Don't free objects in the backend.
Instead, take ownership of deleted objects in the undo-object.
Thus, all references in previous undo-objects are guaranteed to
still exist (unless the objects are deleted elsewhere).
After some contemplation, the second method was chosen, because
it is significantly less intrusive. While touching the undo-objects,
clearly separate backend from ui-code, such that they can ultimately
be reused for mobile.
Note that if other parts of the code delete dives, crashes can still
be provoked. Notable examples are split/merge dives. These will have
to be fixed later. Nevertheless, the new code is a significant
improvement over the old state.
While touching the code, implement proper translation string based
on Qt's plural-feature (using %n).
Signed-off-by: Berthold Stoeger <bstoeger@mail.tuwien.ac.at>
2018-07-19 12:44:27 +00:00
|
|
|
// we remember a list from dive uuid to a new number
|
2018-07-30 13:55:29 +00:00
|
|
|
QVector<QPair<dive *, int>> renumberedDives;
|
2015-06-15 04:49:34 +00:00
|
|
|
int newNr = ui.spinBox->value();
|
2024-06-07 08:25:09 +00:00
|
|
|
for (auto &d: divelog.dives) {
|
2021-01-11 10:19:09 +00:00
|
|
|
if (!selectedOnly || d->selected)
|
2024-06-07 08:25:09 +00:00
|
|
|
renumberedDives.append({ d.get(), newNr++ });
|
2015-02-28 04:42:37 +00:00
|
|
|
}
|
2018-07-23 21:41:23 +00:00
|
|
|
Command::renumberDives(renumberedDives);
|
2015-02-28 04:42:37 +00:00
|
|
|
}
|
2013-06-17 16:41:00 +00:00
|
|
|
}
|
|
|
|
|
2020-05-27 12:00:47 +00:00
|
|
|
RenumberDialog::RenumberDialog(bool selectedOnlyIn, QWidget *parent) : QDialog(parent), selectedOnly(selectedOnlyIn)
|
2014-11-25 20:22:02 +00:00
|
|
|
{
|
|
|
|
ui.setupUi(this);
|
|
|
|
connect(ui.buttonBox, SIGNAL(clicked(QAbstractButton *)), this, SLOT(buttonClicked(QAbstractButton *)));
|
2022-02-08 19:24:53 +00:00
|
|
|
QShortcut *close = new QShortcut(QKeySequence(Qt::CTRL | Qt::Key_W), this);
|
2014-11-25 20:22:02 +00:00
|
|
|
connect(close, SIGNAL(activated()), this, SLOT(close()));
|
2022-02-08 19:24:53 +00:00
|
|
|
QShortcut *quit = new QShortcut(QKeySequence(Qt::CTRL | Qt::Key_Q), this);
|
2014-11-25 20:22:02 +00:00
|
|
|
connect(quit, SIGNAL(activated()), parent, SLOT(close()));
|
2020-05-27 12:00:47 +00:00
|
|
|
|
|
|
|
if (selectedOnly && amount_selected == 1)
|
|
|
|
ui.renumberText->setText(tr("New number"));
|
|
|
|
else
|
|
|
|
ui.renumberText->setText(tr("New starting number"));
|
|
|
|
|
|
|
|
if (selectedOnly)
|
|
|
|
ui.groupBox->setTitle(tr("Renumber selected dives"));
|
|
|
|
else
|
|
|
|
ui.groupBox->setTitle(tr("Renumber all dives"));
|
2014-11-25 20:22:02 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void SetpointDialog::buttonClicked(QAbstractButton *button)
|
|
|
|
{
|
2020-03-03 22:31:46 +00:00
|
|
|
if (ui.buttonBox->buttonRole(button) == QDialogButtonBox::AcceptRole)
|
|
|
|
Command::addEventSetpointChange(d, dcNr, time, pressure_t { (int)(1000.0 * ui.spinbox->value()) });
|
2014-11-25 20:22:02 +00:00
|
|
|
}
|
|
|
|
|
2020-03-03 22:17:08 +00:00
|
|
|
SetpointDialog::SetpointDialog(struct dive *dIn, int dcNrIn, int seconds) : QDialog(MainWindow::instance()),
|
|
|
|
d(dIn), dcNr(dcNrIn), time(seconds < 0 ? 0 : seconds)
|
2013-06-17 16:41:00 +00:00
|
|
|
{
|
2013-10-03 18:54:25 +00:00
|
|
|
ui.setupUi(this);
|
2020-03-03 22:13:07 +00:00
|
|
|
connect(ui.buttonBox, &QDialogButtonBox::clicked, this, &SetpointDialog::buttonClicked);
|
2022-02-08 19:24:53 +00:00
|
|
|
QShortcut *close = new QShortcut(QKeySequence(Qt::CTRL | Qt::Key_W), this);
|
2020-03-03 22:13:07 +00:00
|
|
|
connect(close, &QShortcut::activated, this, &QDialog::close);
|
2022-02-08 19:24:53 +00:00
|
|
|
QShortcut *quit = new QShortcut(QKeySequence(Qt::CTRL | Qt::Key_Q), this);
|
2020-03-03 22:13:07 +00:00
|
|
|
connect(quit, &QShortcut::activated, MainWindow::instance(), &QWidget::close);
|
2013-06-17 16:41:00 +00:00
|
|
|
}
|
2013-09-27 15:52:01 +00:00
|
|
|
|
2014-02-28 04:09:57 +00:00
|
|
|
void ShiftTimesDialog::buttonClicked(QAbstractButton *button)
|
2013-11-18 13:53:05 +00:00
|
|
|
{
|
|
|
|
int amount;
|
|
|
|
|
2014-01-16 04:50:56 +00:00
|
|
|
if (ui.buttonBox->buttonRole(button) == QDialogButtonBox::AcceptRole) {
|
2013-11-18 13:53:05 +00:00
|
|
|
amount = ui.timeEdit->time().hour() * 3600 + ui.timeEdit->time().minute() * 60;
|
|
|
|
if (ui.backwards->isChecked())
|
|
|
|
amount *= -1;
|
2020-01-03 15:04:54 +00:00
|
|
|
if (amount != 0)
|
2023-01-18 00:34:14 +00:00
|
|
|
Command::shiftTime(dives, amount);
|
2013-11-18 13:53:05 +00:00
|
|
|
}
|
2014-03-20 20:57:49 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void ShiftTimesDialog::changeTime()
|
|
|
|
{
|
|
|
|
int amount;
|
|
|
|
|
|
|
|
amount = ui.timeEdit->time().hour() * 3600 + ui.timeEdit->time().minute() * 60;
|
|
|
|
if (ui.backwards->isChecked())
|
|
|
|
amount *= -1;
|
|
|
|
|
2014-05-22 18:40:22 +00:00
|
|
|
ui.shiftedTime->setText(get_dive_date_string(amount + when));
|
2014-03-20 20:57:49 +00:00
|
|
|
}
|
|
|
|
|
2023-01-18 00:34:14 +00:00
|
|
|
ShiftTimesDialog::ShiftTimesDialog(std::vector<dive *> dives_in, QWidget *parent) : QDialog(parent),
|
|
|
|
when(0), dives(std::move(dives_in))
|
2013-11-18 13:53:05 +00:00
|
|
|
{
|
|
|
|
ui.setupUi(this);
|
2014-02-28 04:09:57 +00:00
|
|
|
connect(ui.buttonBox, SIGNAL(clicked(QAbstractButton *)), this, SLOT(buttonClicked(QAbstractButton *)));
|
2014-03-20 20:57:49 +00:00
|
|
|
connect(ui.timeEdit, SIGNAL(timeChanged(const QTime)), this, SLOT(changeTime()));
|
|
|
|
connect(ui.backwards, SIGNAL(toggled(bool)), this, SLOT(changeTime()));
|
2022-02-08 19:24:53 +00:00
|
|
|
QShortcut *close = new QShortcut(QKeySequence(Qt::CTRL | Qt::Key_W), this);
|
2014-04-25 17:39:40 +00:00
|
|
|
connect(close, SIGNAL(activated()), this, SLOT(close()));
|
2022-02-08 19:24:53 +00:00
|
|
|
QShortcut *quit = new QShortcut(QKeySequence(Qt::CTRL | Qt::Key_Q), this);
|
2014-04-25 17:39:40 +00:00
|
|
|
connect(quit, SIGNAL(activated()), parent, SLOT(close()));
|
2023-01-18 00:34:14 +00:00
|
|
|
when = dives[0]->when;
|
|
|
|
ui.currentTime->setText(get_dive_date_string(when));
|
|
|
|
ui.shiftedTime->setText(get_dive_date_string(when));
|
2013-11-18 13:53:05 +00:00
|
|
|
}
|
2014-01-27 13:44:26 +00:00
|
|
|
|
2014-02-13 15:43:55 +00:00
|
|
|
void ShiftImageTimesDialog::syncCameraClicked()
|
|
|
|
{
|
|
|
|
QPixmap picture;
|
|
|
|
QStringList fileNames = QFileDialog::getOpenFileNames(this,
|
2014-07-11 17:39:03 +00:00
|
|
|
tr("Open image file"),
|
2014-02-18 23:26:36 +00:00
|
|
|
DiveListView::lastUsedImageDir(),
|
2021-09-20 13:34:11 +00:00
|
|
|
QString("%1 (%2)").arg(tr("Image files"), imageExtensionFilters().join(" ")));
|
2014-02-13 15:43:55 +00:00
|
|
|
if (fileNames.isEmpty())
|
|
|
|
return;
|
|
|
|
|
2021-09-10 19:23:42 +00:00
|
|
|
ui.timeEdit->setEnabled(false);
|
|
|
|
ui.backwards->setEnabled(false);
|
|
|
|
ui.forward->setEnabled(false);
|
|
|
|
|
2014-02-13 15:43:55 +00:00
|
|
|
picture.load(fileNames.at(0));
|
2014-02-19 10:34:54 +00:00
|
|
|
ui.displayDC->setEnabled(true);
|
2014-02-28 04:09:57 +00:00
|
|
|
QGraphicsScene *scene = new QGraphicsScene(this);
|
2014-02-13 15:43:55 +00:00
|
|
|
|
|
|
|
scene->addPixmap(picture.scaled(ui.DCImage->size()));
|
|
|
|
ui.DCImage->setScene(scene);
|
2015-03-14 14:35:47 +00:00
|
|
|
|
2018-02-25 12:51:41 +00:00
|
|
|
dcImageEpoch = picture_get_timestamp(qPrintable(fileNames.at(0)));
|
2022-02-10 00:47:37 +00:00
|
|
|
QDateTime dcDateTime = QDateTime::fromSecsSinceEpoch(dcImageEpoch, Qt::UTC);
|
2014-02-13 15:43:55 +00:00
|
|
|
ui.dcTime->setDateTime(dcDateTime);
|
|
|
|
connect(ui.dcTime, SIGNAL(dateTimeChanged(const QDateTime &)), this, SLOT(dcDateTimeChanged(const QDateTime &)));
|
|
|
|
}
|
|
|
|
|
|
|
|
void ShiftImageTimesDialog::dcDateTimeChanged(const QDateTime &newDateTime)
|
|
|
|
{
|
2015-09-21 15:46:17 +00:00
|
|
|
QDateTime newtime(newDateTime);
|
2014-02-13 15:43:55 +00:00
|
|
|
if (!dcImageEpoch)
|
|
|
|
return;
|
2015-09-21 15:46:17 +00:00
|
|
|
newtime.setTimeSpec(Qt::UTC);
|
2021-09-10 19:23:42 +00:00
|
|
|
|
|
|
|
m_amount = dateTimeToTimestamp(newtime) - dcImageEpoch;
|
|
|
|
if (m_amount)
|
|
|
|
updateInvalid();
|
2014-02-13 15:43:55 +00:00
|
|
|
}
|
|
|
|
|
2015-09-11 09:31:02 +00:00
|
|
|
void ShiftImageTimesDialog::matchAllImagesToggled(bool state)
|
|
|
|
{
|
|
|
|
matchAllImages = state;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool ShiftImageTimesDialog::matchAll()
|
|
|
|
{
|
|
|
|
return matchAllImages;
|
|
|
|
}
|
|
|
|
|
2017-12-24 16:39:21 +00:00
|
|
|
ShiftImageTimesDialog::ShiftImageTimesDialog(QWidget *parent, QStringList fileNames) : QDialog(parent),
|
2015-10-02 02:02:26 +00:00
|
|
|
fileNames(fileNames),
|
|
|
|
m_amount(0),
|
|
|
|
matchAllImages(false)
|
2014-01-27 13:44:26 +00:00
|
|
|
{
|
|
|
|
ui.setupUi(this);
|
2021-10-25 23:54:43 +00:00
|
|
|
ui.timeEdit->setValidator(new QRegularExpressionValidator(QRegularExpression("\\d{0,6}:[0-5]\\d")));
|
2014-02-13 15:43:55 +00:00
|
|
|
connect(ui.syncCamera, SIGNAL(clicked()), this, SLOT(syncCameraClicked()));
|
2021-09-20 14:09:41 +00:00
|
|
|
connect(ui.timeEdit, &QLineEdit::textEdited, this, &ShiftImageTimesDialog::timeEdited);
|
|
|
|
connect(ui.backwards, &QCheckBox::toggled, this, &ShiftImageTimesDialog::backwardsChanged);
|
2015-09-11 09:31:02 +00:00
|
|
|
connect(ui.matchAllImages, SIGNAL(toggled(bool)), this, SLOT(matchAllImagesToggled(bool)));
|
2014-02-28 04:09:57 +00:00
|
|
|
dcImageEpoch = (time_t)0;
|
2017-12-24 16:39:21 +00:00
|
|
|
|
2018-09-17 15:34:13 +00:00
|
|
|
// Get times of all files. 0 means that the time couldn't be determined.
|
|
|
|
int numFiles = fileNames.size();
|
|
|
|
timestamps.resize(numFiles);
|
|
|
|
for (int i = 0; i < numFiles; ++i)
|
|
|
|
timestamps[i] = picture_get_timestamp(qPrintable(fileNames[i]));
|
2017-04-30 20:24:08 +00:00
|
|
|
updateInvalid();
|
2014-01-27 13:44:26 +00:00
|
|
|
}
|
2013-11-18 13:53:05 +00:00
|
|
|
|
2014-02-13 15:43:55 +00:00
|
|
|
time_t ShiftImageTimesDialog::amount() const
|
2014-02-08 22:56:47 +00:00
|
|
|
{
|
|
|
|
return m_amount;
|
|
|
|
}
|
|
|
|
|
2014-02-13 15:43:55 +00:00
|
|
|
void ShiftImageTimesDialog::setOffset(time_t offset)
|
2014-02-12 15:46:17 +00:00
|
|
|
{
|
2021-09-20 14:09:41 +00:00
|
|
|
int sign = offset >= 0 ? 1 : -1;
|
|
|
|
time_t value = sign * offset;
|
|
|
|
ui.timeEdit->setText(QString("%1:%2").arg(value / 3600).arg((value % 3600) / 60, 2, 10, QLatin1Char('0')));
|
|
|
|
if (offset >= 0)
|
2014-02-19 10:34:54 +00:00
|
|
|
ui.forward->setChecked(true);
|
2021-09-20 14:09:41 +00:00
|
|
|
else
|
2014-02-19 10:34:54 +00:00
|
|
|
ui.backwards->setChecked(true);
|
2014-02-12 15:46:17 +00:00
|
|
|
}
|
|
|
|
|
2015-03-14 16:44:24 +00:00
|
|
|
void ShiftImageTimesDialog::updateInvalid()
|
|
|
|
{
|
|
|
|
bool allValid = true;
|
|
|
|
ui.warningLabel->hide();
|
2017-04-30 18:31:56 +00:00
|
|
|
ui.invalidFilesText->hide();
|
2022-02-10 00:47:37 +00:00
|
|
|
QDateTime time_first = QDateTime::fromSecsSinceEpoch(first_selected_dive()->when, Qt::UTC);
|
|
|
|
QDateTime time_last = QDateTime::fromSecsSinceEpoch(last_selected_dive()->when, Qt::UTC);
|
2019-05-15 14:42:14 +00:00
|
|
|
if (first_selected_dive() == last_selected_dive()) {
|
2017-04-30 20:24:08 +00:00
|
|
|
ui.invalidFilesText->setPlainText(tr("Selected dive date/time") + ": " + time_first.toString());
|
2019-05-15 14:42:14 +00:00
|
|
|
} else {
|
2017-04-30 20:24:08 +00:00
|
|
|
ui.invalidFilesText->setPlainText(tr("First selected dive date/time") + ": " + time_first.toString());
|
|
|
|
ui.invalidFilesText->append(tr("Last selected dive date/time") + ": " + time_last.toString());
|
|
|
|
}
|
|
|
|
ui.invalidFilesText->append(tr("\nFiles with inappropriate date/time") + ":");
|
2015-03-14 16:44:24 +00:00
|
|
|
|
2018-09-17 15:34:13 +00:00
|
|
|
int numFiles = fileNames.size();
|
|
|
|
for (int i = 0; i < numFiles; ++i) {
|
|
|
|
if (picture_check_valid_time(timestamps[i], m_amount))
|
2015-03-14 16:44:24 +00:00
|
|
|
continue;
|
|
|
|
|
2018-09-17 15:34:13 +00:00
|
|
|
// We've found an invalid image
|
2022-02-10 00:47:37 +00:00
|
|
|
time_first.setSecsSinceEpoch(timestamps[i] + m_amount);
|
2018-09-17 15:34:13 +00:00
|
|
|
if (timestamps[i] == 0)
|
|
|
|
ui.invalidFilesText->append(fileNames[i] + " - " + tr("No Exif date/time found"));
|
2017-05-05 16:50:51 +00:00
|
|
|
else
|
2018-09-17 15:34:13 +00:00
|
|
|
ui.invalidFilesText->append(fileNames[i] + " - " + time_first.toString());
|
2015-03-14 16:44:24 +00:00
|
|
|
allValid = false;
|
|
|
|
}
|
|
|
|
|
2017-12-24 16:39:21 +00:00
|
|
|
if (!allValid) {
|
2015-03-14 16:44:24 +00:00
|
|
|
ui.warningLabel->show();
|
2017-04-30 18:31:56 +00:00
|
|
|
ui.invalidFilesText->show();
|
2015-03-14 16:44:24 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-09-20 14:09:41 +00:00
|
|
|
void ShiftImageTimesDialog::timeEdited(const QString &timeText)
|
2015-03-14 16:44:24 +00:00
|
|
|
{
|
2021-09-20 14:09:41 +00:00
|
|
|
// simplistic indication of whether the string is valid
|
|
|
|
if (ui.timeEdit->hasAcceptableInput()) {
|
|
|
|
ui.timeEdit->setStyleSheet("");
|
|
|
|
// parse based on the same reg exp used to validate...
|
2021-10-25 23:54:43 +00:00
|
|
|
QRegularExpression re("(\\d{0,6}):(\\d\\d)");
|
|
|
|
QRegularExpressionMatch match = re.match(timeText);
|
|
|
|
if (match.hasMatch()) {
|
|
|
|
time_t hours = match.captured(1).toInt();
|
|
|
|
time_t minutes = match.captured(2).toInt();
|
2021-09-20 14:09:41 +00:00
|
|
|
m_amount = (ui.backwards->isChecked() ? -1 : 1) * (3600 * hours + 60 * minutes);
|
|
|
|
updateInvalid();
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
ui.timeEdit->setStyleSheet("QLineEdit { color: red;}");
|
|
|
|
}
|
2015-03-14 16:44:24 +00:00
|
|
|
}
|
|
|
|
|
2021-09-20 14:09:41 +00:00
|
|
|
void ShiftImageTimesDialog::backwardsChanged(bool)
|
2017-04-30 20:24:08 +00:00
|
|
|
{
|
2021-09-20 14:09:41 +00:00
|
|
|
// simply use the timeEdit slot to deal with the sign change
|
|
|
|
timeEdited(ui.timeEdit->text());
|
2017-04-30 20:24:08 +00:00
|
|
|
}
|
|
|
|
|
2015-04-24 15:10:55 +00:00
|
|
|
URLDialog::URLDialog(QWidget *parent) : QDialog(parent)
|
|
|
|
{
|
|
|
|
ui.setupUi(this);
|
2022-02-08 19:24:53 +00:00
|
|
|
QShortcut *close = new QShortcut(QKeySequence(Qt::CTRL | Qt::Key_W), this);
|
2015-04-24 15:10:55 +00:00
|
|
|
connect(close, SIGNAL(activated()), this, SLOT(close()));
|
2022-02-08 19:24:53 +00:00
|
|
|
QShortcut *quit = new QShortcut(QKeySequence(Qt::CTRL | Qt::Key_Q), this);
|
2015-04-24 15:10:55 +00:00
|
|
|
connect(quit, SIGNAL(activated()), parent, SLOT(close()));
|
|
|
|
}
|
|
|
|
|
|
|
|
QString URLDialog::url() const
|
|
|
|
{
|
2022-03-31 20:39:47 +00:00
|
|
|
return ui.urlField->toPlainText();
|
2015-04-24 15:10:55 +00:00
|
|
|
}
|
|
|
|
|
2020-09-06 21:53:31 +00:00
|
|
|
AddFilterPresetDialog::AddFilterPresetDialog(const QString &defaultName, QWidget *parent)
|
2020-05-27 21:09:30 +00:00
|
|
|
{
|
|
|
|
ui.setupUi(this);
|
2020-09-06 21:53:31 +00:00
|
|
|
ui.name->setText(defaultName);
|
2020-05-27 21:09:30 +00:00
|
|
|
connect(ui.name, &QLineEdit::textChanged, this, &AddFilterPresetDialog::nameChanged);
|
|
|
|
connect(ui.buttonBox, &QDialogButtonBox::accepted, this, &AddFilterPresetDialog::accept);
|
|
|
|
connect(ui.buttonBox, &QDialogButtonBox::rejected, this, &AddFilterPresetDialog::reject);
|
|
|
|
nameChanged(ui.name->text());
|
2020-06-20 20:32:01 +00:00
|
|
|
|
|
|
|
// Create a completer so that the user can easily overwrite existing presets.
|
|
|
|
QStringList presets;
|
2024-06-08 16:54:23 +00:00
|
|
|
presets.reserve(divelog.filter_presets.size());
|
|
|
|
for (auto &filter_preset: divelog.filter_presets)
|
|
|
|
presets.push_back(QString::fromStdString(filter_preset.name));
|
2020-06-20 20:32:01 +00:00
|
|
|
QCompleter *completer = new QCompleter(presets, this);
|
|
|
|
completer->setCaseSensitivity(Qt::CaseInsensitive);
|
|
|
|
ui.name->setCompleter(completer);
|
2020-05-27 21:09:30 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void AddFilterPresetDialog::nameChanged(const QString &text)
|
|
|
|
{
|
|
|
|
QString trimmed = text.trimmed();
|
|
|
|
bool isEmpty = trimmed.isEmpty();
|
2024-06-08 16:54:23 +00:00
|
|
|
bool exists = !isEmpty && divelog.filter_presets.preset_id(trimmed.toStdString()) >= 0;
|
2020-05-27 21:09:30 +00:00
|
|
|
ui.duplicateWarning->setVisible(exists);
|
|
|
|
ui.buttonBox->button(QDialogButtonBox::Ok)->setEnabled(!isEmpty);
|
|
|
|
}
|
|
|
|
|
|
|
|
QString AddFilterPresetDialog::doit()
|
|
|
|
{
|
|
|
|
if (exec() == QDialog::Accepted)
|
|
|
|
return ui.name->text().trimmed();
|
|
|
|
return QString();
|
|
|
|
}
|
|
|
|
|
2015-11-16 04:56:24 +00:00
|
|
|
TextHyperlinkEventFilter::TextHyperlinkEventFilter(QTextEdit *txtEdit) : QObject(txtEdit),
|
|
|
|
textEdit(txtEdit),
|
|
|
|
scrollView(textEdit->viewport())
|
|
|
|
{
|
|
|
|
// If you install the filter on textEdit, you fail to capture any clicks.
|
|
|
|
// The clicks go to the viewport. http://stackoverflow.com/a/31582977/10278
|
|
|
|
textEdit->viewport()->installEventFilter(this);
|
|
|
|
}
|
|
|
|
|
|
|
|
bool TextHyperlinkEventFilter::eventFilter(QObject *target, QEvent *evt)
|
|
|
|
{
|
|
|
|
if (target != scrollView)
|
|
|
|
return false;
|
|
|
|
|
|
|
|
if (evt->type() != QEvent::MouseButtonPress &&
|
|
|
|
evt->type() != QEvent::ToolTip)
|
|
|
|
return false;
|
|
|
|
|
|
|
|
// --------------------
|
|
|
|
|
|
|
|
// Note: Qt knows that on Mac OSX, ctrl (and Control) are the command key.
|
|
|
|
const bool isCtrlClick = evt->type() == QEvent::MouseButtonPress &&
|
|
|
|
static_cast<QMouseEvent *>(evt)->modifiers() & Qt::ControlModifier &&
|
|
|
|
static_cast<QMouseEvent *>(evt)->button() == Qt::LeftButton;
|
|
|
|
|
|
|
|
const bool isTooltip = evt->type() == QEvent::ToolTip;
|
|
|
|
|
|
|
|
QString urlUnderCursor;
|
|
|
|
|
|
|
|
if (isCtrlClick || isTooltip) {
|
|
|
|
QTextCursor cursor = isCtrlClick ?
|
|
|
|
textEdit->cursorForPosition(static_cast<QMouseEvent *>(evt)->pos()) :
|
|
|
|
textEdit->cursorForPosition(static_cast<QHelpEvent *>(evt)->pos());
|
|
|
|
|
|
|
|
urlUnderCursor = tryToFormulateUrl(&cursor);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (isCtrlClick) {
|
|
|
|
handleUrlClick(urlUnderCursor);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (isTooltip) {
|
|
|
|
handleUrlTooltip(urlUnderCursor, static_cast<QHelpEvent *>(evt)->globalPos());
|
|
|
|
}
|
|
|
|
|
|
|
|
// 'return true' would mean that all event handling stops for this event.
|
|
|
|
// 'return false' lets Qt continue propagating the event to the target.
|
|
|
|
// Since our URL behavior is meant as 'additive' and not necessarily mutually
|
|
|
|
// exclusive with any default behaviors, it seems ok to return false to
|
|
|
|
// avoid unintentially hijacking any 'normal' event handling.
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
void TextHyperlinkEventFilter::handleUrlClick(const QString &urlStr)
|
|
|
|
{
|
|
|
|
if (!urlStr.isEmpty()) {
|
|
|
|
QUrl url(urlStr, QUrl::StrictMode);
|
|
|
|
QDesktopServices::openUrl(url);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void TextHyperlinkEventFilter::handleUrlTooltip(const QString &urlStr, const QPoint &pos)
|
|
|
|
{
|
|
|
|
if (urlStr.isEmpty()) {
|
|
|
|
QToolTip::hideText();
|
|
|
|
} else {
|
|
|
|
// per Qt docs, QKeySequence::toString does localization "tr()" on strings like Ctrl.
|
|
|
|
// Note: Qt knows that on Mac OSX, ctrl (and Control) are the command key.
|
2018-01-01 20:49:19 +00:00
|
|
|
const QString ctrlKeyName = QKeySequence(Qt::CTRL).toString(QKeySequence::NativeText);
|
2015-11-16 04:56:24 +00:00
|
|
|
// ctrlKeyName comes with a trailing '+', as in: 'Ctrl+'
|
|
|
|
QToolTip::showText(pos, tr("%1click to visit %2").arg(ctrlKeyName).arg(urlStr));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
bool TextHyperlinkEventFilter::stringMeetsOurUrlRequirements(const QString &maybeUrlStr)
|
|
|
|
{
|
|
|
|
QUrl url(maybeUrlStr, QUrl::StrictMode);
|
2017-04-18 21:41:55 +00:00
|
|
|
return url.isValid() && (!url.scheme().isEmpty()) && ((!url.authority().isEmpty()) || (!url.path().isEmpty()));
|
2015-11-16 04:56:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
QString TextHyperlinkEventFilter::tryToFormulateUrl(QTextCursor *cursor)
|
|
|
|
{
|
|
|
|
// tryToFormulateUrl exists because WordUnderCursor will not
|
|
|
|
// treat "http://m.abc.def" as a word.
|
|
|
|
|
|
|
|
// tryToFormulateUrl invokes fromCursorTilWhitespace two times (once
|
|
|
|
// with a forward moving cursor and once in the backwards direction) in
|
|
|
|
// order to expand the selection to try to capture a complete string
|
|
|
|
// like "http://m.abc.def"
|
|
|
|
|
|
|
|
// loosely inspired by advice here: http://stackoverflow.com/q/19262064/10278
|
|
|
|
|
|
|
|
cursor->select(QTextCursor::WordUnderCursor);
|
|
|
|
QString maybeUrlStr = cursor->selectedText();
|
|
|
|
|
|
|
|
const bool soFarSoGood = !maybeUrlStr.simplified().replace(" ", "").isEmpty();
|
|
|
|
|
|
|
|
if (soFarSoGood && !stringMeetsOurUrlRequirements(maybeUrlStr)) {
|
|
|
|
// If we don't yet have a full url, try to expand til we get one. Note:
|
|
|
|
// after requesting WordUnderCursor, empirically (all platforms, in
|
|
|
|
// Qt5), the 'anchor' is just past the end of the word.
|
|
|
|
|
|
|
|
QTextCursor cursor2(*cursor);
|
|
|
|
QString left = fromCursorTilWhitespace(cursor, true /*searchBackwards*/);
|
|
|
|
QString right = fromCursorTilWhitespace(&cursor2, false);
|
|
|
|
maybeUrlStr = left + right;
|
|
|
|
}
|
|
|
|
|
2024-01-15 20:22:20 +00:00
|
|
|
return stringMeetsOurUrlRequirements(maybeUrlStr) ? std::move(maybeUrlStr) : QString();
|
2015-11-16 04:56:24 +00:00
|
|
|
}
|
|
|
|
|
2020-02-06 21:56:10 +00:00
|
|
|
QString TextHyperlinkEventFilter::fromCursorTilWhitespace(QTextCursor *cursor, bool searchBackwards)
|
2015-11-16 04:56:24 +00:00
|
|
|
{
|
|
|
|
// fromCursorTilWhitespace calls cursor->movePosition repeatedly, while
|
|
|
|
// preserving the original 'anchor' (qt terminology) of the cursor.
|
|
|
|
// We widen the selection with 'movePosition' until hitting any whitespace.
|
|
|
|
|
|
|
|
QString result;
|
|
|
|
QString grownText;
|
|
|
|
QString noSpaces;
|
|
|
|
bool movedOk = false;
|
2017-04-18 21:35:43 +00:00
|
|
|
int oldSize = -1;
|
2015-11-16 04:56:24 +00:00
|
|
|
|
|
|
|
do {
|
|
|
|
result = grownText; // this is a no-op on the first visit.
|
|
|
|
|
|
|
|
if (searchBackwards) {
|
|
|
|
movedOk = cursor->movePosition(QTextCursor::PreviousWord, QTextCursor::KeepAnchor);
|
|
|
|
} else {
|
|
|
|
movedOk = cursor->movePosition(QTextCursor::NextWord, QTextCursor::KeepAnchor);
|
|
|
|
}
|
|
|
|
|
|
|
|
grownText = cursor->selectedText();
|
2017-12-24 16:39:21 +00:00
|
|
|
if (grownText.size() == oldSize)
|
|
|
|
movedOk = false;
|
2017-04-18 21:35:43 +00:00
|
|
|
oldSize = grownText.size();
|
2015-11-16 04:56:24 +00:00
|
|
|
noSpaces = grownText.simplified().replace(" ", "");
|
|
|
|
} while (grownText == noSpaces && movedOk);
|
|
|
|
|
|
|
|
// while growing the selection forwards, we have an extra step to do:
|
|
|
|
if (!searchBackwards) {
|
|
|
|
/*
|
|
|
|
The cursor keeps jumping to the start of the next word.
|
|
|
|
(for example) in the string "mn.abcd.edu is the spot" you land at
|
|
|
|
m,a,e,i (the 'i' in 'is). if we stop at e, then we only capture
|
|
|
|
"mn.abcd." for the url (wrong). So we have to go to 'i', to
|
|
|
|
capture "mn.abcd.edu " (with trailing space), and then clean it up.
|
|
|
|
*/
|
2021-10-25 23:54:43 +00:00
|
|
|
QStringList list = grownText.split(QRegularExpression("\\s"), SKIP_EMPTY);
|
2015-11-16 04:56:24 +00:00
|
|
|
if (!list.isEmpty()) {
|
|
|
|
result = list[0];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return result;
|
|
|
|
}
|