Chris@303: /* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
Chris@303: 
Chris@303: /*
Chris@303:     Sonic Visualiser
Chris@303:     An audio file viewer and annotation editor.
Chris@303:     Centre for Digital Music, Queen Mary, University of London.
Chris@303:     This file copyright 2006 Chris Cannam.
Chris@303:     
Chris@303:     This program is free software; you can redistribute it and/or
Chris@303:     modify it under the terms of the GNU General Public License as
Chris@303:     published by the Free Software Foundation; either version 2 of the
Chris@303:     License, or (at your option) any later version.  See the file
Chris@303:     COPYING included with this distribution for more information.
Chris@303: */
Chris@303: 
Chris@303: #include "ImageLayer.h"
Chris@303: 
Chris@303: #include "data/model/Model.h"
Chris@303: #include "base/RealTime.h"
Chris@303: #include "base/Profiler.h"
Chris@303: #include "view/View.h"
Chris@303: 
Chris@303: #include "data/model/ImageModel.h"
Chris@318: #include "data/fileio/FileSource.h"
Chris@303: 
Chris@303: #include "widgets/ImageDialog.h"
Chris@378: #include "widgets/ProgressDialog.h"
Chris@303: 
Chris@303: #include <QPainter>
Chris@303: #include <QMouseEvent>
Chris@303: #include <QInputDialog>
Chris@305: #include <QMutexLocker>
Chris@316: #include <QTextStream>
Chris@360: #include <QMessageBox>
Chris@1608: #include <QFileInfo>
Chris@1608: #include <QImageReader>
Chris@303: 
Chris@303: #include <iostream>
Chris@303: #include <cmath>
Chris@303: 
Chris@303: ImageLayer::ImageMap
Chris@303: ImageLayer::m_images;
Chris@303: 
Chris@1608: ImageLayer::FileSourceMap
Chris@1608: ImageLayer::m_fileSources;
Chris@1608: 
Chris@305: QMutex
Chris@1608: ImageLayer::m_staticMutex;
Chris@305: 
Chris@303: ImageLayer::ImageLayer() :
Chris@303:     m_editing(false),
Chris@1408:     m_editingCommand(nullptr)
Chris@303: {
Chris@305: }
Chris@305: 
Chris@305: ImageLayer::~ImageLayer()
Chris@305: {
Chris@464:     for (FileSourceMap::iterator i = m_fileSources.begin();
Chris@464:          i != m_fileSources.end(); ++i) {
Chris@305:         delete i->second;
Chris@305:     }
Chris@303: }
Chris@303: 
Chris@1469: int
Chris@1469: ImageLayer::getCompletion(LayerGeometryProvider *) const
Chris@1469: {
Chris@1469:     auto model = ModelById::get(m_model);
Chris@1469:     if (model) return model->getCompletion();
Chris@1469:     else return 0;
Chris@1469: }
Chris@1469: 
Chris@303: void
Chris@1469: ImageLayer::setModel(ModelId modelId)
Chris@303: {
Chris@1471:     auto newModel = ModelById::getAs<ImageModel>(modelId);
Chris@1471:     
Chris@1471:     if (!modelId.isNone() && !newModel) {
Chris@1471:         throw std::logic_error("Not an ImageModel");
Chris@1471:     }
Chris@1471:     
Chris@1469:     if (m_model == modelId) return;
Chris@1469:     m_model = modelId;
Chris@303: 
Chris@1471:     if (newModel) {
Chris@1471:         connectSignals(m_model);
Chris@1471:     }
Chris@305: 
Chris@303:     emit modelReplaced();
Chris@303: }
Chris@303: 
Chris@303: Layer::PropertyList
Chris@303: ImageLayer::getProperties() const
Chris@303: {
Chris@303:     return Layer::getProperties();
Chris@303: }
Chris@303: 
Chris@303: QString
Chris@805: ImageLayer::getPropertyLabel(const PropertyName &) const
Chris@303: {
Chris@303:     return "";
Chris@303: }
Chris@303: 
Chris@303: Layer::PropertyType
Chris@303: ImageLayer::getPropertyType(const PropertyName &name) const
Chris@303: {
Chris@303:     return Layer::getPropertyType(name);
Chris@303: }
Chris@303: 
Chris@303: int
Chris@303: ImageLayer::getPropertyRangeAndValue(const PropertyName &name,
Chris@1266:                                     int *min, int *max, int *deflt) const
Chris@303: {
Chris@303:     return Layer::getPropertyRangeAndValue(name, min, max, deflt);
Chris@303: }
Chris@303: 
Chris@303: QString
Chris@303: ImageLayer::getPropertyValueLabel(const PropertyName &name,
Chris@1266:                                  int value) const
Chris@303: {
Chris@303:     return Layer::getPropertyValueLabel(name, value);
Chris@303: }
Chris@303: 
Chris@303: void
Chris@303: ImageLayer::setProperty(const PropertyName &name, int value)
Chris@303: {
Chris@303:     Layer::setProperty(name, value);
Chris@303: }
Chris@303: 
Chris@303: bool
Chris@905: ImageLayer::getValueExtents(double &, double &, bool &, QString &) const
Chris@303: {
Chris@303:     return false;
Chris@303: }
Chris@303: 
Chris@303: bool
Chris@918: ImageLayer::isLayerScrollable(const LayerGeometryProvider *) const
Chris@303: {
Chris@304:     return true;
Chris@303: }
Chris@303: 
Chris@1438: EventVector
Chris@918: ImageLayer::getLocalPoints(LayerGeometryProvider *v, int x, int ) const
Chris@303: {
Chris@1469:     auto model = ModelById::getAs<ImageModel>(m_model);
Chris@1469:     if (!model) return {};
Chris@303: 
Chris@587: //    SVDEBUG << "ImageLayer::getLocalPoints(" << x << "," << y << "):";
Chris@1469:     EventVector points(model->getAllEvents());
Chris@303: 
Chris@1438:     EventVector rv;
Chris@303: 
Chris@1438:     for (EventVector::const_iterator i = points.begin(); i != points.end(); ) {
Chris@303: 
Chris@1438:         Event p(*i);
Chris@1438:         int px = v->getXForFrame(p.getFrame());
Chris@304:         if (px > x) break;
Chris@303: 
Chris@304:         ++i;
Chris@304:         if (i != points.end()) {
Chris@1438:             int nx = v->getXForFrame(i->getFrame());
Chris@304:             if (nx < x) {
Chris@304:                 // as we aim not to overlap the images, if the following
Chris@304:                 // image begins to the left of a point then the current
Chris@304:                 // one may be assumed to end to the left of it as well.
Chris@304:                 continue;
Chris@304:             }
Chris@304:         }
Chris@303: 
Chris@304:         // this image is a candidate, test it properly
Chris@304: 
Chris@304:         int width = 32;
Chris@1438:         if (m_scaled[v].find(p.getURI()) != m_scaled[v].end()) {
Chris@1438:             width = m_scaled[v][p.getURI()].width();
Chris@587: //            SVDEBUG << "scaled width = " << width << endl;
Chris@304:         }
Chris@304: 
Chris@304:         if (x >= px && x < px + width) {
Chris@1438:             rv.push_back(p);
Chris@303:         }
Chris@303:     }
Chris@303: 
Chris@682: //    cerr << rv.size() << " point(s)" << endl;
Chris@303: 
Chris@303:     return rv;
Chris@303: }
Chris@303: 
Chris@303: QString
Chris@918: ImageLayer::getFeatureDescription(LayerGeometryProvider *v, QPoint &pos) const
Chris@303: {
Chris@303:     int x = pos.x();
Chris@303: 
Chris@1469:     auto model = ModelById::getAs<ImageModel>(m_model);
Chris@1469:     if (!model || !model->getSampleRate()) return "";
Chris@303: 
Chris@1438:     EventVector points = getLocalPoints(v, x, pos.y());
Chris@303: 
Chris@303:     if (points.empty()) {
Chris@1469:         if (!model->isReady()) {
Chris@1266:             return tr("In progress");
Chris@1266:         } else {
Chris@1266:             return "";
Chris@1266:         }
Chris@303:     }
Chris@303: 
Chris@806: //    int useFrame = points.begin()->frame;
Chris@303: 
Chris@1469: //    RealTime rt = RealTime::frame2RealTime(useFrame, model->getSampleRate());
Chris@303: 
Chris@303:     QString text;
Chris@303: /*    
Chris@303:     if (points.begin()->label == "") {
Chris@1266:         text = QString(tr("Time:\t%1\nHeight:\t%2\nLabel:\t%3"))
Chris@1266:             .arg(rt.toText(true).c_str())
Chris@1266:             .arg(points.begin()->height)
Chris@1266:             .arg(points.begin()->label);
Chris@303:     }
Chris@303: 
Chris@303:     pos = QPoint(v->getXForFrame(useFrame),
Chris@1266:                  getYForHeight(v, points.begin()->height));
Chris@303: */
Chris@303:     return text;
Chris@303: }
Chris@303: 
Chris@303: 
Chris@303: //!!! too much overlap with TimeValueLayer/TimeInstantLayer/TextLayer
Chris@303: 
Chris@303: bool
Chris@918: ImageLayer::snapToFeatureFrame(LayerGeometryProvider *v, sv_frame_t &frame,
Chris@1438:                                int &resolution,
Chris@1547:                                SnapType snap, int ycoord) const
Chris@303: {
Chris@1469:     auto model = ModelById::getAs<ImageModel>(m_model);
Chris@1469:     if (!model) {
Chris@1547:         return Layer::snapToFeatureFrame(v, frame, resolution, snap, ycoord);
Chris@303:     }
Chris@303: 
Chris@1469:     resolution = model->getResolution();
Chris@303: 
Chris@303:     if (snap == SnapNeighbouring) {
Chris@1438:         EventVector points = getLocalPoints(v, v->getXForFrame(frame), -1);
Chris@1266:         if (points.empty()) return false;
Chris@1438:         frame = points.begin()->getFrame();
Chris@1266:         return true;
Chris@303:     }    
Chris@303: 
Chris@1438:     Event e;
Chris@1469:     if (model->getNearestEventMatching
Chris@1438:         (frame,
Chris@1438:          [](Event) { return true; },
Chris@1438:          snap == SnapLeft ? EventSeries::Backward : EventSeries::Forward,
Chris@1438:          e)) {
Chris@1438:         frame = e.getFrame();
Chris@1438:         return true;
Chris@303:     }
Chris@303: 
Chris@1438:     return false;
Chris@303: }
Chris@303: 
Chris@303: void
Chris@916: ImageLayer::paint(LayerGeometryProvider *v, QPainter &paint, QRect rect) const
Chris@303: {
Chris@1469:     auto model = ModelById::getAs<ImageModel>(m_model);
Chris@1469:     if (!model || !model->isOK()) return;
Chris@303: 
Chris@1469:     sv_samplerate_t sampleRate = model->getSampleRate();
Chris@303:     if (!sampleRate) return;
Chris@303: 
Chris@303: //    Profiler profiler("ImageLayer::paint", true);
Chris@303: 
Chris@304: //    int x0 = rect.left(), x1 = rect.right();
Chris@918:     int x0 = 0, x1 = v->getPaintWidth();
Chris@304: 
Chris@905:     sv_frame_t frame0 = v->getFrameForX(x0);
Chris@905:     sv_frame_t frame1 = v->getFrameForX(x1);
Chris@303: 
Chris@1469:     EventVector points(model->getEventsWithin(frame0, frame1 - frame0, 2));
Chris@303:     if (points.empty()) return;
Chris@303: 
Chris@304:     paint.save();
Chris@918:     paint.setClipRect(rect.x(), 0, rect.width(), v->getPaintHeight());
Chris@304: 
Chris@303:     QColor penColour;
Chris@303:     penColour = v->getForeground();
Chris@303: 
Chris@304:     QColor brushColour;
Chris@304:     brushColour = v->getBackground();
Chris@303: 
Chris@304:     int h, s, val;
Chris@304:     brushColour.getHsv(&h, &s, &val);
Chris@304:     brushColour.setHsv(h, s, 255, 240);
Chris@303: 
Chris@304:     paint.setPen(penColour);
Chris@304:     paint.setBrush(brushColour);
Chris@304:     paint.setRenderHint(QPainter::Antialiasing, true);
Chris@303: 
Chris@1438:     for (EventVector::const_iterator i = points.begin();
Chris@1266:          i != points.end(); ++i) {
Chris@303: 
Chris@1438:         Event p(*i);
Chris@303: 
Chris@1438:         int x = v->getXForFrame(p.getFrame());
Chris@303: 
Chris@303:         int nx = x + 2000;
Chris@1438:         EventVector::const_iterator j = i;
Chris@303:         ++j;
Chris@303:         if (j != points.end()) {
Chris@1438:             int jx = v->getXForFrame(j->getFrame());
Chris@303:             if (jx < nx) nx = jx;
Chris@303:         }
Chris@303: 
Chris@304:         drawImage(v, paint, p, x, nx);
Chris@304:     }
Chris@303: 
Chris@304:     paint.setRenderHint(QPainter::Antialiasing, false);
Chris@304:     paint.restore();
Chris@304: }
Chris@303: 
Chris@304: void
Chris@1438: ImageLayer::drawImage(LayerGeometryProvider *v, QPainter &paint, const Event &p,
Chris@304:                       int x, int nx) const
Chris@304: {
Chris@1438:     QString label = p.getLabel();
Chris@1438:     QString imageName = p.getURI();
Chris@304: 
Chris@304:     QImage image;
Chris@304:     QString additionalText;
Chris@304: 
Chris@304:     QSize imageSize;
Chris@304:     if (!getImageOriginalSize(imageName, imageSize)) {
Chris@304:         image = QImage(":icons/emptypage.png");
Chris@304:         imageSize = image.size();
Chris@304:         additionalText = imageName;
Chris@304:     }
Chris@304: 
Chris@304:     int topMargin = 10;
Chris@304:     int bottomMargin = 10;
Chris@304:     int spacing = 5;
Chris@304: 
Chris@918:     if (v->getPaintHeight() < 100) {
Chris@304:         topMargin = 5;
Chris@304:         bottomMargin = 5;
Chris@304:     }
Chris@304: 
Chris@918:     int maxBoxHeight = v->getPaintHeight() - topMargin - bottomMargin;
Chris@304: 
Chris@304:     int availableWidth = nx - x - 3;
Chris@304:     if (availableWidth < 20) availableWidth = 20;
Chris@304: 
Chris@304:     QRect labelRect;
Chris@304: 
Chris@304:     if (label != "") {
Chris@304: 
Chris@918:         int likelyHeight = v->getPaintHeight() / 4;
Chris@304: 
Chris@304:         int likelyWidth = // available height times image aspect
Chris@304:             ((maxBoxHeight - likelyHeight) * imageSize.width())
Chris@304:             / imageSize.height();
Chris@304: 
Chris@304:         if (likelyWidth > imageSize.width()) {
Chris@304:             likelyWidth = imageSize.width();
Chris@303:         }
Chris@303: 
Chris@304:         if (likelyWidth > availableWidth) {
Chris@304:             likelyWidth = availableWidth;
Chris@303:         }
Chris@303: 
Chris@1471:         // Qt 5.13 deprecates QFontMetrics::width(), but its suggested
Chris@1471:         // replacement (horizontalAdvance) was only added in Qt 5.11
Chris@1471:         // which is too new for us
Chris@1471: #pragma GCC diagnostic ignored "-Wdeprecated-declarations"
Chris@1471: 
Chris@304:         int singleWidth = paint.fontMetrics().width(label);
Chris@304:         if (singleWidth < availableWidth && singleWidth < likelyWidth * 2) {
Chris@304:             likelyWidth = singleWidth + 4;
Chris@303:         }
Chris@303: 
Chris@304:         labelRect = paint.fontMetrics().boundingRect
Chris@304:             (QRect(0, 0, likelyWidth, likelyHeight),
Chris@304:              Qt::AlignCenter | Qt::TextWordWrap, label);
Chris@303: 
Chris@304:         labelRect.setWidth(labelRect.width() + 6);
Chris@303:     }
Chris@303: 
Chris@304:     if (image.isNull()) {
Chris@304:         image = getImage(v, imageName,
Chris@304:                          QSize(availableWidth,
Chris@304:                                maxBoxHeight - labelRect.height()));
Chris@304:     }
Chris@304: 
Chris@304:     int boxWidth = image.width();
Chris@304:     if (boxWidth < labelRect.width()) {
Chris@304:         boxWidth = labelRect.width();
Chris@304:     }
Chris@304: 
Chris@304:     int boxHeight = image.height();
Chris@304:     if (label != "") {
Chris@304:         boxHeight += labelRect.height() + spacing;
Chris@304:     }
Chris@304: 
Chris@304:     int division = image.height();
Chris@304: 
Chris@304:     if (additionalText != "") {
Chris@304: 
Chris@304:         paint.save();
Chris@304: 
Chris@304:         QFont font(paint.font());
Chris@304:         font.setItalic(true);
Chris@304:         paint.setFont(font);
Chris@304: 
Chris@304:         int tw = paint.fontMetrics().width(additionalText);
Chris@304:         if (tw > availableWidth) {
Chris@304:             tw = availableWidth;
Chris@304:         }
Chris@304:         if (boxWidth < tw) {
Chris@304:             boxWidth = tw;
Chris@304:         }
Chris@304:         boxHeight += paint.fontMetrics().height();
Chris@304:         division += paint.fontMetrics().height();
Chris@304:     }                
Chris@304: 
Chris@918:     bottomMargin = v->getPaintHeight() - topMargin - boxHeight;
Chris@918:     if (bottomMargin > topMargin + v->getPaintHeight()/7) {
Chris@918:         topMargin += v->getPaintHeight()/8;
Chris@918:         bottomMargin -= v->getPaintHeight()/8;
Chris@304:     }
Chris@304: 
Chris@304:     paint.drawRect(x - 1,
Chris@304:                    topMargin - 1,
Chris@304:                    boxWidth + 2,
Chris@304:                    boxHeight + 2);
Chris@304: 
Chris@304:     int imageY;
Chris@304:     if (label != "") {
Chris@304:         imageY = topMargin + labelRect.height() + spacing;
Chris@304:     } else {
Chris@304:         imageY = topMargin;
Chris@304:     }
Chris@304: 
Chris@304:     paint.drawImage(x + (boxWidth - image.width())/2,
Chris@304:                     imageY,
Chris@304:                     image);
Chris@304: 
Chris@304:     if (additionalText != "") {
Chris@304:         paint.drawText(x,
Chris@304:                        imageY + image.height() + paint.fontMetrics().ascent(),
Chris@304:                        additionalText);
Chris@304:         paint.restore();
Chris@304:     }
Chris@304: 
Chris@304:     if (label != "") {
Chris@304:         paint.drawLine(x,
Chris@304:                        topMargin + labelRect.height() + spacing,
Chris@304:                        x + boxWidth, 
Chris@304:                        topMargin + labelRect.height() + spacing);
Chris@304: 
Chris@304:         paint.drawText(QRect(x,
Chris@304:                              topMargin,
Chris@304:                              boxWidth,
Chris@304:                              labelRect.height()),
Chris@304:                        Qt::AlignCenter | Qt::TextWordWrap,
Chris@304:                        label);
Chris@304:     }
Chris@303: }
Chris@303: 
Chris@303: void
Chris@918: ImageLayer::setLayerDormant(const LayerGeometryProvider *v, bool dormant)
Chris@303: {
Chris@303:     if (dormant) {
Chris@303:         // Delete the images named in the view's scaled map from the
Chris@303:         // general image map as well.  They can always be re-loaded
Chris@303:         // if it turns out another view still needs them.
Chris@1608:         QMutexLocker locker(&m_staticMutex);
Chris@303:         for (ImageMap::iterator i = m_scaled[v].begin();
Chris@303:              i != m_scaled[v].end(); ++i) {
Chris@303:             m_images.erase(i->first);
Chris@303:         }
Chris@303:         m_scaled.erase(v);
Chris@303:     }
Chris@303: }
Chris@303: 
Chris@303: //!!! how to reap no-longer-used images?
Chris@303: 
Chris@304: bool
Chris@304: ImageLayer::getImageOriginalSize(QString name, QSize &size) const
Chris@303: {
Chris@682: //    cerr << "getImageOriginalSize: \"" << name << "\"" << endl;
Chris@305: 
Chris@1608:     QMutexLocker locker(&m_staticMutex);
Chris@303:     if (m_images.find(name) == m_images.end()) {
Chris@682: //        cerr << "don't have, trying to open local" << endl;
Chris@305:         m_images[name] = QImage(getLocalFilename(name));
Chris@303:     }
Chris@304:     if (m_images[name].isNull()) {
Chris@682: //        cerr << "null image" << endl;
Chris@304:         return false;
Chris@304:     } else {
Chris@304:         size = m_images[name].size();
Chris@304:         return true;
Chris@304:     }
Chris@303: }
Chris@303: 
Chris@303: QImage 
Chris@918: ImageLayer::getImage(LayerGeometryProvider *v, QString name, QSize maxSize) const
Chris@303: {
Chris@587: //    SVDEBUG << "ImageLayer::getImage(" << v << ", " << name << ", ("
Chris@585: //              << maxSize.width() << "x" << maxSize.height() << "))" << endl;
Chris@303: 
Chris@304:     if (!m_scaled[v][name].isNull()  &&
Chris@304:         ((m_scaled[v][name].width()  == maxSize.width() &&
Chris@304:           m_scaled[v][name].height() <= maxSize.height()) ||
Chris@304:          (m_scaled[v][name].width()  <= maxSize.width() &&
Chris@304:           m_scaled[v][name].height() == maxSize.height()))) {
Chris@682: //        cerr << "cache hit" << endl;
Chris@303:         return m_scaled[v][name];
Chris@303:     }
Chris@303: 
Chris@1608:     QMutexLocker locker(&m_staticMutex);
Chris@305: 
Chris@303:     if (m_images.find(name) == m_images.end()) {
Chris@305:         m_images[name] = QImage(getLocalFilename(name));
Chris@303:     }
Chris@303: 
Chris@303:     if (m_images[name].isNull()) {
Chris@682: //        cerr << "null image" << endl;
Chris@303:         m_scaled[v][name] = QImage();
Chris@304:     } else if (m_images[name].width() <= maxSize.width() &&
Chris@304:                m_images[name].height() <= maxSize.height()) {
Chris@304:         m_scaled[v][name] = m_images[name];
Chris@303:     } else {
Chris@303:         m_scaled[v][name] =
Chris@303:             m_images[name].scaled(maxSize,
Chris@303:                                   Qt::KeepAspectRatio,
Chris@303:                                   Qt::SmoothTransformation);
Chris@303:     }
Chris@303: 
Chris@303:     return m_scaled[v][name];
Chris@303: }
Chris@303: 
Chris@303: void
Chris@918: ImageLayer::drawStart(LayerGeometryProvider *v, QMouseEvent *e)
Chris@303: {
Chris@587: //    SVDEBUG << "ImageLayer::drawStart(" << e->x() << "," << e->y() << ")" << endl;
Chris@303: 
Chris@1469:     auto model = ModelById::getAs<ImageModel>(m_model);
Chris@1469:     if (!model) {
Chris@1266:         SVDEBUG << "ImageLayer::drawStart: no model" << endl;
Chris@1266:         return;
Chris@303:     }
Chris@303: 
Chris@905:     sv_frame_t frame = v->getFrameForX(e->x());
Chris@303:     if (frame < 0) frame = 0;
Chris@1469:     frame = frame / model->getResolution() * model->getResolution();
Chris@303: 
Chris@1438:     m_editingPoint = Event(frame);
Chris@303:     m_originalPoint = m_editingPoint;
Chris@303: 
Chris@376:     if (m_editingCommand) finish(m_editingCommand);
Chris@1470:     m_editingCommand = new ChangeEventsCommand(m_model.untyped, "Add Image");
Chris@1438:     m_editingCommand->add(m_editingPoint);
Chris@303: 
Chris@303:     m_editing = true;
Chris@303: }
Chris@303: 
Chris@303: void
Chris@918: ImageLayer::drawDrag(LayerGeometryProvider *v, QMouseEvent *e)
Chris@303: {
Chris@587: //    SVDEBUG << "ImageLayer::drawDrag(" << e->x() << "," << e->y() << ")" << endl;
Chris@303: 
Chris@1469:     auto model = ModelById::getAs<ImageModel>(m_model);
Chris@1469:     if (!model || !m_editing) return;
Chris@303: 
Chris@905:     sv_frame_t frame = v->getFrameForX(e->x());
Chris@303:     if (frame < 0) frame = 0;
Chris@1469:     frame = frame / model->getResolution() * model->getResolution();
Chris@303: 
Chris@1438:     m_editingCommand->remove(m_editingPoint);
Chris@1438:     m_editingPoint = m_editingPoint
Chris@1438:         .withFrame(frame);
Chris@1438:     m_editingCommand->add(m_editingPoint);
Chris@303: }
Chris@303: 
Chris@303: void
Chris@918: ImageLayer::drawEnd(LayerGeometryProvider *, QMouseEvent *)
Chris@303: {
Chris@587: //    SVDEBUG << "ImageLayer::drawEnd(" << e->x() << "," << e->y() << ")" << endl;
Chris@1469:     auto model = ModelById::getAs<ImageModel>(m_model);
Chris@1469:     if (!model || !m_editing) return;
Chris@303: 
Chris@307:     ImageDialog dialog(tr("Select image"), "", "");
Chris@305: 
Chris@1438:     m_editingCommand->remove(m_editingPoint);
Chris@1438: 
Chris@303:     if (dialog.exec() == QDialog::Accepted) {
Chris@305: 
Chris@1608:         checkAddSourceAndConnect(dialog.getImage());
Chris@305: 
Chris@1438:         m_editingPoint = m_editingPoint
Chris@1438:             .withURI(dialog.getImage())
Chris@1438:             .withLabel(dialog.getLabel());
Chris@1438:         m_editingCommand->add(m_editingPoint);
Chris@303:     }
Chris@303: 
Chris@376:     finish(m_editingCommand);
Chris@1408:     m_editingCommand = nullptr;
Chris@303:     m_editing = false;
Chris@303: }
Chris@303: 
Chris@312: bool
Chris@1608: ImageLayer::isImageFileSupported(QString url)
Chris@1608: {
Chris@1608:     QMutexLocker locker(&m_staticMutex);
Chris@1608:     QString filename = getLocalFilename(url);
Chris@1608:     QString ext = QFileInfo(filename).suffix().toLower();
Chris@1608:     auto formats = QImageReader::supportedImageFormats();
Chris@1608:     for (auto f: formats) {
Chris@1608:         if (QString::fromLatin1(f).toLower() == ext) {
Chris@1608:             return true;
Chris@1608:         }
Chris@1608:     }
Chris@1608:     return false;
Chris@1608: }
Chris@1608: 
Chris@1608: bool
Chris@905: ImageLayer::addImage(sv_frame_t frame, QString url)
Chris@312: {
Chris@1608:     {
Chris@1608:         QMutexLocker locker(&m_staticMutex);
Chris@1608:         QImage image(getLocalFilename(url));
Chris@1608:         if (image.isNull()) {
Chris@1608:             SVCERR << "Failed to open image from url \"" << url << "\" (local filename \"" << getLocalFilename(url) << "\"" << endl;
Chris@1608:             delete m_fileSources[url];
Chris@1608:             m_fileSources.erase(url);
Chris@1608:             return false;
Chris@1608:         }
Chris@312:     }
Chris@312: 
Chris@1438:     Event point = Event(frame).withURI(url);
Chris@1469:     auto command =
Chris@1470:         new ChangeEventsCommand(m_model.untyped, "Add Image");
Chris@1438:     command->add(point);
Chris@376:     finish(command);
Chris@312:     return true;
Chris@312: }
Chris@312: 
Chris@303: void
Chris@918: ImageLayer::editStart(LayerGeometryProvider *v, QMouseEvent *e)
Chris@303: {
Chris@587: //    SVDEBUG << "ImageLayer::editStart(" << e->x() << "," << e->y() << ")" << endl;
Chris@303: 
Chris@1469:     auto model = ModelById::getAs<ImageModel>(m_model);
Chris@1469:     if (!model) return;
Chris@303: 
Chris@1438:     EventVector points = getLocalPoints(v, e->x(), e->y());
Chris@303:     if (points.empty()) return;
Chris@303: 
Chris@303:     m_editOrigin = e->pos();
Chris@303:     m_editingPoint = *points.begin();
Chris@303:     m_originalPoint = m_editingPoint;
Chris@303: 
Chris@303:     if (m_editingCommand) {
Chris@1266:         finish(m_editingCommand);
Chris@1408:         m_editingCommand = nullptr;
Chris@303:     }
Chris@303: 
Chris@303:     m_editing = true;
Chris@303: }
Chris@303: 
Chris@303: void
Chris@918: ImageLayer::editDrag(LayerGeometryProvider *v, QMouseEvent *e)
Chris@303: {
Chris@1469:     auto model = ModelById::getAs<ImageModel>(m_model);
Chris@1469:     if (!model || !m_editing) return;
Chris@303: 
Chris@905:     sv_frame_t frameDiff = v->getFrameForX(e->x()) - v->getFrameForX(m_editOrigin.x());
Chris@1438:     sv_frame_t frame = m_originalPoint.getFrame() + frameDiff;
Chris@303: 
Chris@303:     if (frame < 0) frame = 0;
Chris@1469:     frame = (frame / model->getResolution()) * model->getResolution();
Chris@303: 
Chris@303:     if (!m_editingCommand) {
Chris@1470:         m_editingCommand = new ChangeEventsCommand(m_model.untyped, tr("Move Image"));
Chris@303:     }
Chris@303: 
Chris@1438:     m_editingCommand->remove(m_editingPoint);
Chris@1438:     m_editingPoint = m_editingPoint
Chris@1438:         .withFrame(frame);
Chris@1438:     m_editingCommand->add(m_editingPoint);
Chris@303: }
Chris@303: 
Chris@303: void
Chris@918: ImageLayer::editEnd(LayerGeometryProvider *, QMouseEvent *)
Chris@303: {
Chris@587: //    SVDEBUG << "ImageLayer::editEnd(" << e->x() << "," << e->y() << ")" << endl;
Chris@1469:     auto model = ModelById::getAs<ImageModel>(m_model);
Chris@1469:     if (!model || !m_editing) return;
Chris@303: 
Chris@303:     if (m_editingCommand) {
Chris@1266:         finish(m_editingCommand);
Chris@303:     }
Chris@303:     
Chris@1408:     m_editingCommand = nullptr;
Chris@303:     m_editing = false;
Chris@303: }
Chris@303: 
Chris@303: bool
Chris@918: ImageLayer::editOpen(LayerGeometryProvider *v, QMouseEvent *e)
Chris@303: {
Chris@1469:     auto model = ModelById::getAs<ImageModel>(m_model);
Chris@1469:     if (!model) return false;
Chris@303: 
Chris@1438:     EventVector points = getLocalPoints(v, e->x(), e->y());
Chris@303:     if (points.empty()) return false;
Chris@303: 
Chris@1438:     QString image = points.begin()->getURI();
Chris@1438:     QString label = points.begin()->getLabel();
Chris@303: 
Chris@303:     ImageDialog dialog(tr("Select image"),
Chris@303:                        image,
Chris@303:                        label);
Chris@303: 
Chris@303:     if (dialog.exec() == QDialog::Accepted) {
Chris@305: 
Chris@1608:         checkAddSourceAndConnect(dialog.getImage());
Chris@305: 
Chris@1469:         auto command =
Chris@1470:             new ChangeEventsCommand(m_model.untyped, tr("Edit Image"));
Chris@1438:         command->remove(*points.begin());
Chris@1438:         command->add(points.begin()->
Chris@1438:                      withURI(dialog.getImage()).withLabel(dialog.getLabel()));
Chris@1438:         finish(command);
Chris@303:     }
Chris@303: 
Chris@303:     return true;
Chris@303: }    
Chris@303: 
Chris@303: void
Chris@905: ImageLayer::moveSelection(Selection s, sv_frame_t newStartFrame)
Chris@303: {
Chris@1469:     auto model = ModelById::getAs<ImageModel>(m_model);
Chris@1469:     if (!model) return;
Chris@303: 
Chris@1469:     auto command =
Chris@1470:         new ChangeEventsCommand(m_model.untyped, tr("Drag Selection"));
Chris@303: 
Chris@1438:     EventVector points =
Chris@1469:         model->getEventsStartingWithin(s.getStartFrame(), s.getDuration());
Chris@303: 
Chris@1438:     for (Event p: points) {
Chris@1438:         command->remove(p);
Chris@1438:         Event moved = p.withFrame(p.getFrame() +
Chris@1438:                                   newStartFrame - s.getStartFrame());
Chris@1438:         command->add(moved);
Chris@303:     }
Chris@303: 
Chris@376:     finish(command);
Chris@303: }
Chris@303: 
Chris@303: void
Chris@303: ImageLayer::resizeSelection(Selection s, Selection newSize)
Chris@303: {
Chris@1469:     auto model = ModelById::getAs<ImageModel>(m_model);
Chris@1469:     if (!model) return;
Chris@303: 
Chris@1469:     auto command =
Chris@1470:         new ChangeEventsCommand(m_model.untyped, tr("Resize Selection"));
Chris@303: 
Chris@1438:     EventVector points =
Chris@1469:         model->getEventsStartingWithin(s.getStartFrame(), s.getDuration());
Chris@303: 
Chris@1438:     double ratio = double(newSize.getDuration()) / double(s.getDuration());
Chris@1438:     double oldStart = double(s.getStartFrame());
Chris@1438:     double newStart = double(newSize.getStartFrame());
Chris@1438:     
Chris@1438:     for (Event p: points) {
Chris@303: 
Chris@1438:         double newFrame = (double(p.getFrame()) - oldStart) * ratio + newStart;
Chris@303: 
Chris@1438:         Event newPoint = p
Chris@1438:             .withFrame(lrint(newFrame));
Chris@1438:         command->remove(p);
Chris@1438:         command->add(newPoint);
Chris@303:     }
Chris@303: 
Chris@376:     finish(command);
Chris@303: }
Chris@303: 
Chris@303: void
Chris@303: ImageLayer::deleteSelection(Selection s)
Chris@303: {
Chris@1469:     auto model = ModelById::getAs<ImageModel>(m_model);
Chris@1469:     if (!model) return;
Chris@303: 
Chris@1469:     auto command =
Chris@1470:         new ChangeEventsCommand(m_model.untyped, tr("Delete Selection"));
Chris@303: 
Chris@1438:     EventVector points =
Chris@1469:         model->getEventsStartingWithin(s.getStartFrame(), s.getDuration());
Chris@303: 
Chris@1438:     for (Event p: points) {
Chris@1438:         command->remove(p);
Chris@303:     }
Chris@303: 
Chris@376:     finish(command);
Chris@303: }
Chris@303: 
Chris@303: void
Chris@918: ImageLayer::copy(LayerGeometryProvider *v, Selection s, Clipboard &to)
Chris@303: {
Chris@1469:     auto model = ModelById::getAs<ImageModel>(m_model);
Chris@1469:     if (!model) return;
Chris@303: 
Chris@1438:     EventVector points =
Chris@1469:         model->getEventsStartingWithin(s.getStartFrame(), s.getDuration());
Chris@303: 
Chris@1438:     for (Event p: points) {
Chris@1438:         to.addPoint(p.withReferenceFrame(alignToReference(v, p.getFrame())));
Chris@303:     }
Chris@303: }
Chris@303: 
Chris@303: bool
Chris@1533: ImageLayer::paste(LayerGeometryProvider *v, const Clipboard &from,
Chris@1533:                   sv_frame_t /* frameOffset */, bool /* interactive */)
Chris@303: {
Chris@1469:     auto model = ModelById::getAs<ImageModel>(m_model);
Chris@1469:     if (!model) return false;
Chris@303: 
Chris@1423:     const EventVector &points = from.getPoints();
Chris@303: 
Chris@360:     bool realign = false;
Chris@360: 
Chris@360:     if (clipboardHasDifferentAlignment(v, from)) {
Chris@360: 
Chris@360:         QMessageBox::StandardButton button =
Chris@918:             QMessageBox::question(v->getView(), tr("Re-align pasted items?"),
Chris@360:                                   tr("The items you are pasting came from a layer with different source material from this one.  Do you want to re-align them in time, to match the source material for this layer?"),
Chris@360:                                   QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel,
Chris@360:                                   QMessageBox::Yes);
Chris@360: 
Chris@360:         if (button == QMessageBox::Cancel) {
Chris@360:             return false;
Chris@360:         }
Chris@360: 
Chris@360:         if (button == QMessageBox::Yes) {
Chris@360:             realign = true;
Chris@360:         }
Chris@360:     }
Chris@360: 
Chris@1470:     auto command = new ChangeEventsCommand(m_model.untyped, tr("Paste"));
Chris@303: 
Chris@1423:     for (EventVector::const_iterator i = points.begin();
Chris@303:          i != points.end(); ++i) {
Chris@303:         
Chris@905:         sv_frame_t frame = 0;
Chris@360: 
Chris@360:         if (!realign) {
Chris@360:             
Chris@360:             frame = i->getFrame();
Chris@360: 
Chris@360:         } else {
Chris@360: 
Chris@1423:             if (i->hasReferenceFrame()) {
Chris@360:                 frame = i->getReferenceFrame();
Chris@360:                 frame = alignFromReference(v, frame);
Chris@360:             } else {
Chris@360:                 frame = i->getFrame();
Chris@360:             }
Chris@303:         }
Chris@360: 
Chris@1533:         Event p = i->withFrame(frame);
Chris@1533: 
Chris@1438:         Event newPoint = p;
Chris@303: 
Chris@303:         //!!! inadequate
Chris@303:         
Chris@1438:         if (!p.hasLabel()) {
Chris@1438:             if (p.hasValue()) {
Chris@1438:                 newPoint = newPoint.withLabel(QString("%1").arg(p.getValue()));
Chris@1438:             } else {
Chris@1438:                 newPoint = newPoint.withLabel(tr("New Point"));
Chris@1438:             }
Chris@303:         }
Chris@303:         
Chris@1438:         command->add(newPoint);
Chris@303:     }
Chris@303: 
Chris@376:     finish(command);
Chris@303:     return true;
Chris@303: }
Chris@303: 
Chris@303: QString
Chris@1608: ImageLayer::getLocalFilename(QString img)
Chris@305: {
Chris@1608:     // called with mutex held
Chris@1608:     
Chris@464:     if (m_fileSources.find(img) == m_fileSources.end()) {
Chris@1608:         checkAddSource(img, false);
Chris@464:         if (m_fileSources.find(img) == m_fileSources.end()) {
Chris@312:             return img;
Chris@312:         }
Chris@305:     }
Chris@1608:     
Chris@464:     return m_fileSources[img]->getLocalFilename();
Chris@305: }
Chris@305: 
Chris@305: void
Chris@1608: ImageLayer::checkAddSourceAndConnect(QString img)
Chris@1608: {
Chris@1608:     checkAddSource(img, true);
Chris@1608: 
Chris@1608:     QMutexLocker locker(&m_staticMutex);
Chris@1608:     if (m_fileSources.find(img) != m_fileSources.end()) {
Chris@1608:         connect(m_fileSources.at(img), SIGNAL(ready()),
Chris@1608:                 this, SLOT(fileSourceReady()));
Chris@1608:     }
Chris@1608: }
Chris@1608: 
Chris@1608: void
Chris@1608: ImageLayer::checkAddSource(QString img, bool synchronise)
Chris@305: {
Chris@587:     SVDEBUG << "ImageLayer::checkAddSource(" << img << "): yes, trying..." << endl;
Chris@305: 
Chris@1608:     QMutexLocker locker(synchronise ? &m_staticMutex : nullptr);
Chris@1608: 
Chris@464:     if (m_fileSources.find(img) != m_fileSources.end()) {
Chris@464:         return;
Chris@464:     }
Chris@312: 
Chris@464:     ProgressDialog dialog(tr("Opening image URL..."), true, 2000);
Chris@464:     FileSource *rf = new FileSource(img, &dialog);
Chris@464:     if (rf->isOK()) {
Chris@1608:         SVDEBUG << "ok, adding it (local filename = " << rf->getLocalFilename() << ")" << endl;
Chris@464:         m_fileSources[img] = rf;
Chris@464:     } else {
Chris@464:         delete rf;
Chris@305:     }
Chris@305: }
Chris@305: 
Chris@305: void
Chris@464: ImageLayer::checkAddSources()
Chris@305: {
Chris@1469:     auto model = ModelById::getAs<ImageModel>(m_model);
Chris@1469:     const EventVector &points(model->getAllEvents());
Chris@305: 
Chris@1438:     for (EventVector::const_iterator i = points.begin();
Chris@1266:          i != points.end(); ++i) {
Chris@305:         
Chris@1608:         checkAddSourceAndConnect(i->getURI());
Chris@305:     }
Chris@305: }
Chris@305: 
Chris@305: void
Chris@464: ImageLayer::fileSourceReady()
Chris@305: {
Chris@587: //    SVDEBUG << "ImageLayer::fileSourceReady" << endl;
Chris@305: 
Chris@318:     FileSource *rf = dynamic_cast<FileSource *>(sender());
Chris@305:     if (!rf) return;
Chris@305: 
Chris@1608:     bool shouldEmit = false;
Chris@1608:     
Chris@1608:     {
Chris@1608:         QMutexLocker locker(&m_staticMutex);
Chris@1608:     
Chris@1608:         QString img;
Chris@1608:         for (FileSourceMap::const_iterator i = m_fileSources.begin();
Chris@1608:              i != m_fileSources.end(); ++i) {
Chris@1608:             if (i->second == rf) {
Chris@1608:                 img = i->first;
Chris@682: //            cerr << "it's image \"" << img << "\"" << endl;
Chris@1608:                 break;
Chris@1608:             }
Chris@1608:         }
Chris@1608:         if (img == "") return;
Chris@1608: 
Chris@1608:         m_images.erase(img);
Chris@1608:         for (ViewImageMap::iterator i = m_scaled.begin(); i != m_scaled.end(); ++i) {
Chris@1608:             i->second.erase(img);
Chris@1608:             shouldEmit = true;
Chris@305:         }
Chris@305:     }
Chris@305: 
Chris@1608:     if (shouldEmit) {
Chris@1481:         emit modelChanged(getModel());
Chris@305:     }
Chris@305: }
Chris@305: 
Chris@316: void
Chris@316: ImageLayer::toXml(QTextStream &stream,
Chris@316:                   QString indent, QString extraAttributes) const
Chris@303: {
Chris@316:     Layer::toXml(stream, indent, extraAttributes);
Chris@303: }
Chris@303: 
Chris@303: void
Chris@805: ImageLayer::setProperties(const QXmlAttributes &)
Chris@303: {
Chris@303: }
Chris@303: