profile: port picture code to qt-quick

This was very painful, because I had to implement rearranging the
paint order of the QSGNodes. The resulting code appears quite
brittle. Let's see where that brings us.

Signed-off-by: Berthold Stoeger <bstoeger@mail.tuwien.ac.at>
This commit is contained in:
Berthold Stoeger 2023-08-12 22:59:56 +02:00
parent d0c26f42d7
commit ebf9ce6d86
22 changed files with 979 additions and 592 deletions

View file

@ -75,11 +75,8 @@ ProfileWidget2::ProfileWidget2(DivePlannerPointsModel *plannerModelIn, double dp
setAcceptDrops(true);
connect(Thumbnailer::instance(), &Thumbnailer::thumbnailChanged, this, &ProfileWidget2::updateThumbnail, Qt::QueuedConnection);
connect(&diveListNotifier, &DiveListNotifier::picturesRemoved, this, &ProfileWidget2::picturesRemoved);
connect(&diveListNotifier, &DiveListNotifier::picturesAdded, this, &ProfileWidget2::picturesAdded);
connect(&diveListNotifier, &DiveListNotifier::cylinderEdited, this, &ProfileWidget2::profileChanged);
connect(&diveListNotifier, &DiveListNotifier::eventsChanged, this, &ProfileWidget2::profileChanged);
connect(&diveListNotifier, &DiveListNotifier::pictureOffsetChanged, this, &ProfileWidget2::pictureOffsetChanged);
connect(&diveListNotifier, &DiveListNotifier::divesChanged, this, &ProfileWidget2::divesChanged);
connect(&diveListNotifier, &DiveListNotifier::deviceEdited, this, &ProfileWidget2::replot);
connect(&diveListNotifier, &DiveListNotifier::diveComputerEdited, this, &ProfileWidget2::replot);
@ -916,234 +913,6 @@ void ProfileWidget2::keyDeleteAction()
}
}
void ProfileWidget2::clearPictures()
{
pictures.clear();
}
static const double unscaledDurationLineWidth = 2.5;
static const double unscaledDurationLinePenWidth = 0.5;
// Reset the duration line after an image was moved or we found a new duration
void ProfileWidget2::updateDurationLine(PictureEntry &e)
{
if (e.duration.seconds > 0) {
// We know the duration of this video, reset the line symbolizing its extent accordingly
double begin = profileScene->timeAxis->posAtValue(e.offset.seconds);
double end = profileScene->timeAxis->posAtValue(e.offset.seconds + e.duration.seconds);
double y = e.thumbnail->y();
// Undo scaling for pen-width and line-width. For this purpose, we use the scaling of the y-axis.
double scale = transform().m22();
double durationLineWidth = unscaledDurationLineWidth / scale;
double durationLinePenWidth = unscaledDurationLinePenWidth / scale;
e.durationLine.reset(new QGraphicsRectItem(begin, y - durationLineWidth - durationLinePenWidth, end - begin, durationLineWidth));
e.durationLine->setPen(QPen(getColor(DURATION_LINE, profileScene->isGrayscale), durationLinePenWidth));
e.durationLine->setBrush(getColor(::BACKGROUND, profileScene->isGrayscale));
e.durationLine->setVisible(prefs.show_pictures_in_profile);
scene()->addItem(e.durationLine.get());
} else {
// This is either a picture or a video with unknown duration.
// In case there was a line (how could that be?) remove it.
e.durationLine.reset();
}
}
// This function is called asynchronously by the thumbnailer if a thumbnail
// was fetched from disk or freshly calculated.
void ProfileWidget2::updateThumbnail(QString filenameIn, QImage thumbnail, duration_t duration)
{
std::string filename = filenameIn.toStdString();
// Find the picture with the given filename
auto it = std::find_if(pictures.begin(), pictures.end(), [&filename](const PictureEntry &e)
{ return e.filename == filename; });
// If we didn't find a picture, it does either not belong to the current dive,
// or its timestamp is outside of the profile.
if (it != pictures.end()) {
// Replace the pixmap of the thumbnail with the newly calculated one.
int size = Thumbnailer::defaultThumbnailSize();
it->thumbnail->setPixmap(QPixmap::fromImage(thumbnail.scaled(size, size, Qt::KeepAspectRatio)));
// If the duration changed, update the line
if (duration.seconds != it->duration.seconds) {
it->duration = duration;
updateDurationLine(*it);
// If we created / removed a duration line, we have to update the thumbnail paint order.
updateThumbnailPaintOrder();
}
}
}
// Create a PictureEntry object and add its thumbnail to the scene if profile pictures are shown.
ProfileWidget2::PictureEntry::PictureEntry(offset_t offsetIn, const std::string &filenameIn, ProfileWidget2 *profile, bool synchronous) : offset(offsetIn),
filename(filenameIn),
thumbnail(new DivePictureItem)
{
QGraphicsScene *scene = profile->scene();
int size = Thumbnailer::defaultThumbnailSize();
scene->addItem(thumbnail.get());
thumbnail->setVisible(prefs.show_pictures_in_profile);
QImage img = Thumbnailer::instance()->fetchThumbnail(QString::fromStdString(filename), synchronous).scaled(size, size, Qt::KeepAspectRatio);
thumbnail->setPixmap(QPixmap::fromImage(img));
thumbnail->setFileUrl(QString::fromStdString(filename));
connect(thumbnail.get(), &DivePictureItem::removePicture, profile, &ProfileWidget2::removePicture);
}
// Define a default sort order for picture-entries: sort lexicographically by timestamp and filename.
bool ProfileWidget2::PictureEntry::operator< (const PictureEntry &e) const
{
// Use std::tie() for lexicographical sorting.
return std::tie(offset.seconds, filename) < std::tie(e.offset.seconds, e.filename);
}
// This function updates the paint order of the thumbnails and duration-lines, such that later
// thumbnails are painted on top of previous thumbnails and duration-lines on top of the thumbnail
// they belong to.
void ProfileWidget2::updateThumbnailPaintOrder()
{
if (!pictures.size())
return;
// To get the correct sort order, we place in thumbnails at equal z-distances
// between thumbnailBaseZValue and (thumbnailBaseZValue + 1.0).
// Duration-lines are placed between the thumbnails.
double z = thumbnailBaseZValue;
double step = 1.0 / (double)pictures.size();
for (PictureEntry &e: pictures) {
e.thumbnail->setBaseZValue(z);
if (e.durationLine)
e.durationLine->setZValue(z + step / 2.0);
z += step;
}
}
// Calculate the y-coordinates of the thumbnails, which are supposed to be sorted by x-coordinate.
// This will also change the order in which the thumbnails are painted, to avoid weird effects,
// when items are added later to the scene. This is done using the QGraphicsItem::packBefore() function.
// We can't use the z-value, because that will be modified on hoverEnter and hoverExit events.
void ProfileWidget2::calculatePictureYPositions()
{
double lastX = -1.0, lastY = 0.0;
const double yStart = 0.05; // At which depth the thumbnails start (in fraction of total depth).
const double yStep = 0.01; // Increase of depth for overlapping thumbnails (in fraction of total depth).
const double xSpace = 18.0 * profileScene->dpr; // Horizontal range in which thumbnails are supposed to be overlapping (in pixels).
const int maxDepth = 14; // Maximal depth of thumbnail stack (in thumbnails).
for (PictureEntry &e: pictures) {
// Invisible items are outside of the shown range - ignore.
if (!e.thumbnail->isVisible())
continue;
// Let's put the picture at the correct time, but at a fixed "depth" on the profile
// not sure this is ideal, but it seems to look right.
double x = e.thumbnail->x();
if (x < 0.0)
continue;
double y;
if (lastX >= 0.0 && fabs(x - lastX) < xSpace * profileScene->dpr && lastY <= (yStart + maxDepth * yStep) - 1e-10)
y = lastY + yStep;
else
y = yStart;
lastX = x;
lastY = y;
double yScreen = profileScene->timeAxis->screenPosition(y);
e.thumbnail->setY(yScreen);
updateDurationLine(e); // If we changed the y-position, we also have to change the duration-line.
}
updateThumbnailPaintOrder();
}
void ProfileWidget2::updateThumbnailXPos(PictureEntry &e)
{
// Here, we only set the x-coordinate of the picture. The y-coordinate
// will be set later in calculatePictureYPositions().
// Thumbnails outside of the shown range are hidden.
double time = e.offset.seconds;
if (time >= profileScene->timeAxis->minimum() && time <= profileScene->timeAxis->maximum()) {
double x = profileScene->timeAxis->posAtValue(time);
e.thumbnail->setX(x);
e.thumbnail->setVisible(true);
} else {
e.thumbnail->setVisible(false);
}
}
// This function resets the picture thumbnails of the current dive.
void ProfileWidget2::plotPictures()
{
plotPicturesInternal(d, false);
}
void ProfileWidget2::plotPicturesInternal(const struct dive *d, bool synchronous)
{
pictures.clear();
if (currentState == EDIT || currentState == PLAN)
return;
if (!d)
return;
// Fetch all pictures of the dive, but consider only those that are within the dive time.
// For each picture, create a PictureEntry object in the pictures-vector.
// emplace_back() constructs an object at the end of the vector. The parameters are passed directly to the constructor.
for (auto &picture: d->pictures) {
if (picture.offset.seconds > 0 && picture.offset.seconds <= d->duration.seconds)
pictures.emplace_back(picture.offset, picture.filename, this, synchronous);
}
if (pictures.empty())
return;
// Sort pictures by timestamp (and filename if equal timestamps).
// This will allow for proper location of the pictures on the profile plot.
std::sort(pictures.begin(), pictures.end());
updateThumbnails();
}
void ProfileWidget2::updateThumbnails()
{
// Calculate thumbnail positions. First the x-coordinates and and then the y-coordinates.
for (PictureEntry &e: pictures)
updateThumbnailXPos(e);
calculatePictureYPositions();
}
// Remove the pictures with the given filenames from the profile plot.
void ProfileWidget2::picturesRemoved(dive *d, QVector<QString> fileUrls)
{
// To remove the pictures, we use the std::remove_if() algorithm.
// std::remove_if() does not actually delete the elements, but moves
// them to the end of the given range. It returns an iterator to the
// end of the new range of non-deleted elements. A subsequent call to
// std::erase on the range of deleted elements then ultimately shrinks the vector.
// (c.f. erase-remove idiom: https://en.wikipedia.org/wiki/Erase%E2%80%93remove_idiom)
auto it = std::remove_if(pictures.begin(), pictures.end(), [&fileUrls](const PictureEntry &e)
// Check whether filename of entry is in list of provided filenames
{ return std::find(fileUrls.begin(), fileUrls.end(), QString::fromStdString(e.filename)) != fileUrls.end(); });
pictures.erase(it, pictures.end());
calculatePictureYPositions();
}
void ProfileWidget2::picturesAdded(dive *d, QVector<picture> pics)
{
for (const picture &pic: pics) {
if (pic.offset.seconds > 0 && pic.offset.seconds <= d->duration.seconds) {
pictures.emplace_back(pic.offset, pic.filename, this, false);
updateThumbnailXPos(pictures.back());
}
}
// Sort pictures by timestamp (and filename if equal timestamps).
// This will allow for proper location of the pictures on the profile plot.
std::sort(pictures.begin(), pictures.end());
calculatePictureYPositions();
}
void ProfileWidget2::removePicture(const QString &fileUrl)
{
if (d)
Command::removePictures({ { mutable_dive(), { fileUrl.toStdString() } } });
}
void ProfileWidget2::profileChanged(dive *dive)
{
if (dive != d)
@ -1153,126 +922,6 @@ void ProfileWidget2::profileChanged(dive *dive)
#endif
void ProfileWidget2::dropEvent(QDropEvent *event)
{
#ifndef SUBSURFACE_MOBILE
if (event->mimeData()->hasFormat("application/x-subsurfaceimagedrop") && d) {
QByteArray itemData = event->mimeData()->data("application/x-subsurfaceimagedrop");
QDataStream dataStream(&itemData, QIODevice::ReadOnly);
QString filename;
dataStream >> filename;
QPointF mappedPos = mapToScene(event->pos());
offset_t offset { .seconds = (int32_t)lrint(profileScene->timeAxis->valueAt(mappedPos)) };
Command::setPictureOffset(mutable_dive(), filename, offset);
if (event->source() == this) {
event->setDropAction(Qt::MoveAction);
event->accept();
} else {
event->acceptProposedAction();
}
} else {
event->ignore();
}
#endif
}
#ifndef SUBSURFACE_MOBILE
void ProfileWidget2::pictureOffsetChanged(dive *dIn, QString filenameIn, offset_t offset)
{
if (dIn != d)
return; // Picture of a different dive than the one shown changed.
std::string filename = filenameIn.toStdString(); // TODO: can we move std::string through Qt's signal/slot system?
// Calculate time in dive where picture was dropped and whether the new position is during the dive.
bool duringDive = d && offset.seconds > 0 && offset.seconds < d->duration.seconds;
// A picture was drag&dropped onto the profile: We have four cases to consider:
// 1a) The image was already shown on the profile and is moved to a different position on the profile.
// Calculate the new position and move the picture.
// 1b) The image was on the profile and is moved outside of the dive time.
// Remove the picture.
// 2a) The image was not on the profile and is moved into the dive time.
// Add the picture to the profile.
// 2b) The image was not on the profile and is moved outside of the dive time.
// Do nothing.
auto oldPos = std::find_if(pictures.begin(), pictures.end(), [filename](const PictureEntry &e)
{ return e.filename == filename; });
if (oldPos != pictures.end()) {
// Cases 1a) and 1b): picture is on profile
if (duringDive) {
// Case 1a): move to new position
// First, find new position. Note that we also have to compare filenames,
// because it is quite easy to generate equal offsets.
auto newPos = std::find_if(pictures.begin(), pictures.end(), [offset, &filename](const PictureEntry &e)
{ return std::tie(e.offset.seconds, e.filename) > std::tie(offset.seconds, filename); });
// Set new offset
oldPos->offset = offset;
updateThumbnailXPos(*oldPos);
// Move image from old to new position
int oldIndex = oldPos - pictures.begin();
int newIndex = newPos - pictures.begin();
move_in_range(pictures, oldIndex, oldIndex + 1, newIndex);
} else {
// Case 1b): remove picture
pictures.erase(oldPos);
}
// In both cases the picture list changed, therefore we must recalculate the y-coordinatesA.
calculatePictureYPositions();
} else {
// Cases 2a) and 2b): picture not on profile. We only have to take action for
// the first case: picture is moved into dive-time.
if (duringDive) {
// Case 2a): add the picture at the appropriate position.
// The case move from outside-to-outside of the profile plot was handled by
// the "&& duringDive" condition in the if above.
// As for case 1a), we have to also consider filenames in the case of equal offsets.
auto newPos = std::find_if(pictures.begin(), pictures.end(), [offset, &filename](const PictureEntry &e)
{ return std::tie(e.offset.seconds, e.filename) > std::tie(offset.seconds, filename); });
// emplace() constructs the element at the given position in the vector.
// The parameters are passed directly to the contructor.
// The call returns an iterator to the new element (which might differ from
// the old iterator, since the buffer might have been reallocated).
newPos = pictures.emplace(newPos, offset, filename, this, false);
updateThumbnailXPos(*newPos);
calculatePictureYPositions();
}
}
}
#endif
void ProfileWidget2::dragEnterEvent(QDragEnterEvent *event)
{
if (event->mimeData()->hasFormat("application/x-subsurfaceimagedrop")) {
if (event->source() == this) {
event->setDropAction(Qt::MoveAction);
event->accept();
} else {
event->acceptProposedAction();
}
} else {
event->ignore();
}
}
void ProfileWidget2::dragMoveEvent(QDragMoveEvent *event)
{
if (event->mimeData()->hasFormat("application/x-subsurfaceimagedrop")) {
if (event->source() == this) {
event->setDropAction(Qt::MoveAction);
event->accept();
} else {
event->acceptProposedAction();
}
} else {
event->ignore();
}
}
struct dive *ProfileWidget2::mutable_dive() const
{
return const_cast<dive *>(d);