mirror of
https://github.com/subsurface/subsurface.git
synced 2025-02-19 22:16:15 +00:00
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:
parent
d0c26f42d7
commit
ebf9ce6d86
22 changed files with 979 additions and 592 deletions
|
@ -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);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue