Chris@35: /* -*- c-basic-offset: 4 -*- vi:set ts=8 sts=4 sw=4: */ Chris@35: Chris@35: /* Chris@35: A waveform viewer and audio annotation editor. Chris@35: Chris Cannam, Queen Mary University of London, 2005-2006 Chris@35: Chris@35: This is experimental software. Not for distribution. Chris@35: */ Chris@35: Chris@35: #include "TextLayer.h" Chris@35: Chris@35: #include "base/Model.h" Chris@35: #include "base/RealTime.h" Chris@35: #include "base/Profiler.h" Chris@35: #include "base/View.h" Chris@35: Chris@35: #include "model/TextModel.h" Chris@35: Chris@35: #include Chris@35: #include Chris@36: #include Chris@35: Chris@35: #include Chris@35: #include Chris@35: Chris@35: TextLayer::TextLayer(View *w) : Chris@35: Layer(w), Chris@35: m_model(0), Chris@35: m_editing(false), Chris@35: m_originalPoint(0, 0.0, tr("Empty Label")), Chris@35: m_editingPoint(0, 0.0, tr("Empty Label")), Chris@35: m_editingCommand(0), Chris@35: m_colour(255, 150, 50) // orange Chris@35: { Chris@35: m_view->addLayer(this); Chris@35: } Chris@35: Chris@35: void Chris@35: TextLayer::setModel(TextModel *model) Chris@35: { Chris@35: if (m_model == model) return; Chris@35: m_model = model; Chris@35: Chris@35: connect(m_model, SIGNAL(modelChanged()), this, SIGNAL(modelChanged())); Chris@35: connect(m_model, SIGNAL(modelChanged(size_t, size_t)), Chris@35: this, SIGNAL(modelChanged(size_t, size_t))); Chris@35: Chris@35: connect(m_model, SIGNAL(completionChanged()), Chris@35: this, SIGNAL(modelCompletionChanged())); Chris@35: Chris@36: // std::cerr << "TextLayer::setModel(" << model << ")" << std::endl; Chris@35: Chris@35: emit modelReplaced(); Chris@35: } Chris@35: Chris@35: Layer::PropertyList Chris@35: TextLayer::getProperties() const Chris@35: { Chris@35: PropertyList list; Chris@35: list.push_back(tr("Colour")); Chris@35: return list; Chris@35: } Chris@35: Chris@35: Layer::PropertyType Chris@35: TextLayer::getPropertyType(const PropertyName &name) const Chris@35: { Chris@35: return ValueProperty; Chris@35: } Chris@35: Chris@35: int Chris@35: TextLayer::getPropertyRangeAndValue(const PropertyName &name, Chris@35: int *min, int *max) const Chris@35: { Chris@35: //!!! factor this colour handling stuff out into a colour manager class Chris@35: Chris@35: int deft = 0; Chris@35: Chris@35: if (name == tr("Colour")) { Chris@35: Chris@35: if (min) *min = 0; Chris@35: if (max) *max = 5; Chris@35: Chris@35: if (m_colour == Qt::black) deft = 0; Chris@35: else if (m_colour == Qt::darkRed) deft = 1; Chris@35: else if (m_colour == Qt::darkBlue) deft = 2; Chris@35: else if (m_colour == Qt::darkGreen) deft = 3; Chris@35: else if (m_colour == QColor(200, 50, 255)) deft = 4; Chris@35: else if (m_colour == QColor(255, 150, 50)) deft = 5; Chris@35: Chris@35: } else { Chris@35: Chris@35: deft = Layer::getPropertyRangeAndValue(name, min, max); Chris@35: } Chris@35: Chris@35: return deft; Chris@35: } Chris@35: Chris@35: QString Chris@35: TextLayer::getPropertyValueLabel(const PropertyName &name, Chris@35: int value) const Chris@35: { Chris@35: if (name == tr("Colour")) { Chris@35: switch (value) { Chris@35: default: Chris@35: case 0: return tr("Black"); Chris@35: case 1: return tr("Red"); Chris@35: case 2: return tr("Blue"); Chris@35: case 3: return tr("Green"); Chris@35: case 4: return tr("Purple"); Chris@35: case 5: return tr("Orange"); Chris@35: } Chris@35: } Chris@35: return tr(""); Chris@35: } Chris@35: Chris@35: void Chris@35: TextLayer::setProperty(const PropertyName &name, int value) Chris@35: { Chris@35: if (name == tr("Colour")) { Chris@35: switch (value) { Chris@35: default: Chris@35: case 0: setBaseColour(Qt::black); break; Chris@35: case 1: setBaseColour(Qt::darkRed); break; Chris@35: case 2: setBaseColour(Qt::darkBlue); break; Chris@35: case 3: setBaseColour(Qt::darkGreen); break; Chris@35: case 4: setBaseColour(QColor(200, 50, 255)); break; Chris@35: case 5: setBaseColour(QColor(255, 150, 50)); break; Chris@35: } Chris@35: } Chris@35: } Chris@35: Chris@35: void Chris@35: TextLayer::setBaseColour(QColor colour) Chris@35: { Chris@35: if (m_colour == colour) return; Chris@35: m_colour = colour; Chris@35: emit layerParametersChanged(); Chris@35: } Chris@35: Chris@35: bool Chris@35: TextLayer::isLayerScrollable() const Chris@35: { Chris@35: QPoint discard; Chris@35: return !m_view->shouldIlluminateLocalFeatures(this, discard); Chris@35: } Chris@35: Chris@35: Chris@35: TextModel::PointList Chris@35: TextLayer::getLocalPoints(int x, int y) const Chris@35: { Chris@35: if (!m_model) return TextModel::PointList(); Chris@35: Chris@35: long frame0 = getFrameForX(-150); Chris@35: long frame1 = getFrameForX(m_view->width() + 150); Chris@35: Chris@35: TextModel::PointList points(m_model->getPoints(frame0, frame1)); Chris@35: Chris@35: TextModel::PointList rv; Chris@35: QFontMetrics metrics = QPainter().fontMetrics(); Chris@35: Chris@35: for (TextModel::PointList::iterator i = points.begin(); Chris@35: i != points.end(); ++i) { Chris@35: Chris@35: const TextModel::Point &p(*i); Chris@35: Chris@35: int px = getXForFrame(p.frame); Chris@35: int py = getYForHeight(p.height); Chris@35: Chris@35: QString label = p.label; Chris@35: if (label == "") { Chris@35: label = tr(""); Chris@35: } Chris@35: Chris@35: QRect rect = metrics.boundingRect Chris@35: (QRect(0, 0, 150, 200), Chris@35: Qt::AlignLeft | Qt::AlignTop | Qt::TextWordWrap, label); Chris@35: Chris@35: if (py + rect.height() > m_view->height()) { Chris@35: if (rect.height() > m_view->height()) py = 0; Chris@35: else py = m_view->height() - rect.height() - 1; Chris@35: } Chris@35: Chris@35: if (x >= px && x < px + rect.width() && Chris@35: y >= py && y < py + rect.height()) { Chris@35: rv.insert(p); Chris@35: } Chris@35: } Chris@35: Chris@35: return rv; Chris@35: } Chris@35: Chris@35: QString Chris@35: TextLayer::getFeatureDescription(QPoint &pos) const Chris@35: { Chris@35: int x = pos.x(); Chris@35: Chris@35: if (!m_model || !m_model->getSampleRate()) return ""; Chris@35: Chris@35: TextModel::PointList points = getLocalPoints(x, pos.y()); Chris@35: Chris@35: if (points.empty()) { Chris@35: if (!m_model->isReady()) { Chris@35: return tr("In progress"); Chris@35: } else { Chris@35: return ""; Chris@35: } Chris@35: } Chris@35: Chris@35: long useFrame = points.begin()->frame; Chris@35: Chris@35: RealTime rt = RealTime::frame2RealTime(useFrame, m_model->getSampleRate()); Chris@35: Chris@35: QString text; Chris@35: Chris@35: if (points.begin()->label == "") { Chris@35: text = QString(tr("Time:\t%1\nHeight:\t%2\nLabel:\t%3")) Chris@35: .arg(rt.toText(true).c_str()) Chris@35: .arg(points.begin()->height) Chris@35: .arg(points.begin()->label); Chris@35: } Chris@35: Chris@35: pos = QPoint(getXForFrame(useFrame), getYForHeight(points.begin()->height)); Chris@35: return text; Chris@35: } Chris@35: Chris@35: Chris@35: //!!! too much overlap with TimeValueLayer/TimeInstantLayer Chris@35: Chris@35: bool Chris@35: TextLayer::snapToFeatureFrame(int &frame, Chris@35: size_t &resolution, Chris@35: SnapType snap) const Chris@35: { Chris@35: if (!m_model) { Chris@35: return Layer::snapToFeatureFrame(frame, resolution, snap); Chris@35: } Chris@35: Chris@35: resolution = m_model->getResolution(); Chris@35: TextModel::PointList points; Chris@35: Chris@35: if (snap == SnapNeighbouring) { Chris@35: Chris@35: points = getLocalPoints(getXForFrame(frame), -1); Chris@35: if (points.empty()) return false; Chris@35: frame = points.begin()->frame; Chris@35: return true; Chris@35: } Chris@35: Chris@35: points = m_model->getPoints(frame, frame); Chris@35: int snapped = frame; Chris@35: bool found = false; Chris@35: Chris@35: for (TextModel::PointList::const_iterator i = points.begin(); Chris@35: i != points.end(); ++i) { Chris@35: Chris@35: if (snap == SnapRight) { Chris@35: Chris@35: if (i->frame > frame) { Chris@35: snapped = i->frame; Chris@35: found = true; Chris@35: break; Chris@35: } Chris@35: Chris@35: } else if (snap == SnapLeft) { Chris@35: Chris@35: if (i->frame <= frame) { Chris@35: snapped = i->frame; Chris@35: found = true; // don't break, as the next may be better Chris@35: } else { Chris@35: break; Chris@35: } Chris@35: Chris@35: } else { // nearest Chris@35: Chris@35: TextModel::PointList::const_iterator j = i; Chris@35: ++j; Chris@35: Chris@35: if (j == points.end()) { Chris@35: Chris@35: snapped = i->frame; Chris@35: found = true; Chris@35: break; Chris@35: Chris@35: } else if (j->frame >= frame) { Chris@35: Chris@35: if (j->frame - frame < frame - i->frame) { Chris@35: snapped = j->frame; Chris@35: } else { Chris@35: snapped = i->frame; Chris@35: } Chris@35: found = true; Chris@35: break; Chris@35: } Chris@35: } Chris@35: } Chris@35: Chris@35: frame = snapped; Chris@35: return found; Chris@35: } Chris@35: Chris@35: int Chris@35: TextLayer::getYForHeight(float height) const Chris@35: { Chris@35: int h = m_view->height(); Chris@35: return h - int(height * h); Chris@35: } Chris@35: Chris@35: float Chris@35: TextLayer::getHeightForY(int y) const Chris@35: { Chris@35: int h = m_view->height(); Chris@35: return float(h - y) / h; Chris@35: } Chris@35: Chris@35: void Chris@35: TextLayer::paint(QPainter &paint, QRect rect) const Chris@35: { Chris@35: if (!m_model || !m_model->isOK()) return; Chris@35: Chris@35: int sampleRate = m_model->getSampleRate(); Chris@35: if (!sampleRate) return; Chris@35: Chris@35: // Profiler profiler("TextLayer::paint", true); Chris@35: Chris@35: int x0 = rect.left(), x1 = rect.right(); Chris@35: long frame0 = getFrameForX(x0); Chris@35: long frame1 = getFrameForX(x1); Chris@35: Chris@35: TextModel::PointList points(m_model->getPoints(frame0, frame1)); Chris@35: if (points.empty()) return; Chris@35: Chris@35: QColor brushColour(m_colour); Chris@35: Chris@36: int h, s, v; Chris@36: brushColour.getHsv(&h, &s, &v); Chris@36: brushColour.setHsv(h, s, 255, 100); Chris@36: Chris@36: QColor penColour; Chris@35: if (m_view->hasLightBackground()) { Chris@36: penColour = Qt::black; Chris@35: } else { Chris@36: penColour = Qt::white; Chris@35: } Chris@35: Chris@35: // std::cerr << "TextLayer::paint: resolution is " Chris@35: // << m_model->getResolution() << " frames" << std::endl; Chris@35: Chris@35: QPoint localPos; Chris@35: long illuminateFrame = -1; Chris@35: Chris@35: if (m_view->shouldIlluminateLocalFeatures(this, localPos)) { Chris@35: TextModel::PointList localPoints = getLocalPoints(localPos.x(), Chris@35: localPos.y()); Chris@35: if (!localPoints.empty()) illuminateFrame = localPoints.begin()->frame; Chris@35: } Chris@35: Chris@35: int boxMaxWidth = 150; Chris@35: int boxMaxHeight = 200; Chris@35: Chris@35: paint.save(); Chris@35: paint.setClipRect(rect.x(), 0, rect.width() + boxMaxWidth, m_view->height()); Chris@35: Chris@35: for (TextModel::PointList::const_iterator i = points.begin(); Chris@35: i != points.end(); ++i) { Chris@35: Chris@35: const TextModel::Point &p(*i); Chris@35: Chris@35: int x = getXForFrame(p.frame); Chris@35: int y = getYForHeight(p.height); Chris@35: Chris@35: if (illuminateFrame == p.frame) { Chris@36: paint.setBrush(penColour); Chris@36: if (m_view->hasLightBackground()) { Chris@36: paint.setPen(Qt::white); Chris@36: } else { Chris@36: paint.setPen(Qt::black); Chris@36: } Chris@36: } else { Chris@36: paint.setPen(penColour); Chris@36: paint.setBrush(brushColour); Chris@35: } Chris@35: Chris@35: QString label = p.label; Chris@35: if (label == "") { Chris@35: label = tr(""); Chris@35: } Chris@35: Chris@35: QRect boxRect = paint.fontMetrics().boundingRect Chris@35: (QRect(0, 0, boxMaxWidth, boxMaxHeight), Chris@35: Qt::AlignLeft | Qt::AlignTop | Qt::TextWordWrap, label); Chris@35: Chris@35: QRect textRect = QRect(3, 2, boxRect.width(), boxRect.height()); Chris@35: boxRect = QRect(0, 0, boxRect.width() + 6, boxRect.height() + 2); Chris@35: Chris@35: if (y + boxRect.height() > m_view->height()) { Chris@35: if (boxRect.height() > m_view->height()) y = 0; Chris@35: else y = m_view->height() - boxRect.height() - 1; Chris@35: } Chris@35: Chris@35: boxRect = QRect(x, y, boxRect.width(), boxRect.height()); Chris@35: textRect = QRect(x + 3, y + 2, textRect.width(), textRect.height()); Chris@35: Chris@35: // boxRect = QRect(x, y, boxRect.width(), boxRect.height()); Chris@35: // textRect = QRect(x + 3, y + 2, textRect.width(), textRect.height()); Chris@35: Chris@35: paint.setRenderHint(QPainter::Antialiasing, false); Chris@35: paint.drawRect(boxRect); Chris@35: Chris@35: paint.setRenderHint(QPainter::Antialiasing, true); Chris@35: paint.drawText(textRect, Chris@35: Qt::AlignLeft | Qt::AlignTop | Qt::TextWordWrap, Chris@35: label); Chris@35: Chris@35: /// if (p.label != "") { Chris@35: /// paint.drawText(x + 5, y - paint.fontMetrics().height() + paint.fontMetrics().ascent(), p.label); Chris@35: /// } Chris@35: } Chris@35: Chris@35: paint.restore(); Chris@35: Chris@35: // looks like save/restore doesn't deal with this: Chris@35: paint.setRenderHint(QPainter::Antialiasing, false); Chris@35: } Chris@35: Chris@35: void Chris@35: TextLayer::drawStart(QMouseEvent *e) Chris@35: { Chris@36: // std::cerr << "TextLayer::drawStart(" << e->x() << "," << e->y() << ")" << std::endl; Chris@35: Chris@35: if (!m_model) { Chris@35: std::cerr << "TextLayer::drawStart: no model" << std::endl; Chris@35: return; Chris@35: } Chris@35: Chris@35: long frame = getFrameForX(e->x()); Chris@35: if (frame < 0) frame = 0; Chris@35: frame = frame / m_model->getResolution() * m_model->getResolution(); Chris@35: Chris@35: float height = getHeightForY(e->y()); Chris@35: Chris@35: m_editingPoint = TextModel::Point(frame, height, ""); Chris@35: m_originalPoint = m_editingPoint; Chris@35: Chris@35: if (m_editingCommand) m_editingCommand->finish(); Chris@35: m_editingCommand = new TextModel::EditCommand(m_model, "Add Label"); Chris@35: m_editingCommand->addPoint(m_editingPoint); Chris@35: Chris@35: m_editing = true; Chris@35: } Chris@35: Chris@35: void Chris@35: TextLayer::drawDrag(QMouseEvent *e) Chris@35: { Chris@36: // std::cerr << "TextLayer::drawDrag(" << e->x() << "," << e->y() << ")" << std::endl; Chris@35: Chris@35: if (!m_model || !m_editing) return; Chris@35: Chris@35: long frame = getFrameForX(e->x()); Chris@35: if (frame < 0) frame = 0; Chris@35: frame = frame / m_model->getResolution() * m_model->getResolution(); Chris@35: Chris@35: float height = getHeightForY(e->y()); Chris@35: Chris@35: m_editingCommand->deletePoint(m_editingPoint); Chris@35: m_editingPoint.frame = frame; Chris@35: m_editingPoint.height = height; Chris@35: m_editingCommand->addPoint(m_editingPoint); Chris@35: } Chris@35: Chris@35: void Chris@35: TextLayer::drawEnd(QMouseEvent *e) Chris@35: { Chris@36: // std::cerr << "TextLayer::drawEnd(" << e->x() << "," << e->y() << ")" << std::endl; Chris@35: if (!m_model || !m_editing) return; Chris@36: Chris@36: bool ok = false; Chris@36: QString label = QInputDialog::getText(m_view, tr("Enter label"), Chris@36: tr("Please enter a new label:"), Chris@36: QLineEdit::Normal, "", &ok); Chris@36: Chris@36: if (ok) { Chris@36: TextModel::RelabelCommand *command = Chris@36: new TextModel::RelabelCommand(m_model, m_editingPoint, label); Chris@36: m_editingCommand->addCommand(command); Chris@36: } Chris@36: Chris@35: m_editingCommand->finish(); Chris@35: m_editingCommand = 0; Chris@35: m_editing = false; Chris@35: } Chris@35: Chris@35: void Chris@35: TextLayer::editStart(QMouseEvent *e) Chris@35: { Chris@36: // std::cerr << "TextLayer::editStart(" << e->x() << "," << e->y() << ")" << std::endl; Chris@35: Chris@35: if (!m_model) return; Chris@35: Chris@35: TextModel::PointList points = getLocalPoints(e->x(), e->y()); Chris@35: if (points.empty()) return; Chris@35: Chris@36: m_editOrigin = e->pos(); Chris@35: m_editingPoint = *points.begin(); Chris@35: m_originalPoint = m_editingPoint; Chris@35: Chris@35: if (m_editingCommand) { Chris@35: m_editingCommand->finish(); Chris@35: m_editingCommand = 0; Chris@35: } Chris@35: Chris@35: m_editing = true; Chris@35: } Chris@35: Chris@35: void Chris@35: TextLayer::editDrag(QMouseEvent *e) Chris@35: { Chris@35: if (!m_model || !m_editing) return; Chris@35: Chris@36: long frameDiff = getFrameForX(e->x()) - getFrameForX(m_editOrigin.x()); Chris@36: float heightDiff = getHeightForY(e->y()) - getHeightForY(m_editOrigin.y()); Chris@36: Chris@36: long frame = m_originalPoint.frame + frameDiff; Chris@36: float height = m_originalPoint.height + heightDiff; Chris@36: Chris@36: // long frame = getFrameForX(e->x()); Chris@35: if (frame < 0) frame = 0; Chris@36: frame = (frame / m_model->getResolution()) * m_model->getResolution(); Chris@35: Chris@36: // float height = getHeightForY(e->y()); Chris@35: Chris@35: if (!m_editingCommand) { Chris@35: m_editingCommand = new TextModel::EditCommand(m_model, tr("Drag Label")); Chris@35: } Chris@35: Chris@35: m_editingCommand->deletePoint(m_editingPoint); Chris@35: m_editingPoint.frame = frame; Chris@35: m_editingPoint.height = height; Chris@35: m_editingCommand->addPoint(m_editingPoint); Chris@35: } Chris@35: Chris@35: void Chris@35: TextLayer::editEnd(QMouseEvent *e) Chris@35: { Chris@36: // std::cerr << "TextLayer::editEnd(" << e->x() << "," << e->y() << ")" << std::endl; Chris@35: if (!m_model || !m_editing) return; Chris@35: Chris@35: if (m_editingCommand) { Chris@35: Chris@35: QString newName = m_editingCommand->getName(); Chris@35: Chris@35: if (m_editingPoint.frame != m_originalPoint.frame) { Chris@35: if (m_editingPoint.height != m_originalPoint.height) { Chris@35: newName = tr("Move Label"); Chris@35: } else { Chris@36: newName = tr("Move Label Horizontally"); Chris@35: } Chris@35: } else { Chris@36: newName = tr("Move Label Vertically"); Chris@35: } Chris@35: Chris@35: m_editingCommand->setName(newName); Chris@35: m_editingCommand->finish(); Chris@35: } Chris@35: Chris@35: m_editingCommand = 0; Chris@35: m_editing = false; Chris@35: } Chris@35: Chris@36: void Chris@36: TextLayer::editOpen(QMouseEvent *e) Chris@36: { Chris@36: std::cerr << "TextLayer::editOpen" << std::endl; Chris@36: Chris@36: if (!m_model) return; Chris@36: Chris@36: TextModel::PointList points = getLocalPoints(e->x(), e->y()); Chris@36: if (points.empty()) return; Chris@36: Chris@36: QString label = points.begin()->label; Chris@36: Chris@36: bool ok = false; Chris@36: label = QInputDialog::getText(m_view, tr("Enter label"), Chris@36: tr("Please enter a new label:"), Chris@36: QLineEdit::Normal, label, &ok); Chris@36: if (ok && label != points.begin()->label) { Chris@36: TextModel::RelabelCommand *command = Chris@36: new TextModel::RelabelCommand(m_model, *points.begin(), label); Chris@36: CommandHistory::getInstance()->addCommand(command, true); Chris@36: } Chris@36: } Chris@36: Chris@35: QString Chris@35: TextLayer::toXmlString(QString indent, QString extraAttributes) const Chris@35: { Chris@35: return Layer::toXmlString(indent, extraAttributes + Chris@35: QString(" colour=\"%1\"") Chris@35: .arg(encodeColour(m_colour))); Chris@35: } Chris@35: Chris@35: void Chris@35: TextLayer::setProperties(const QXmlAttributes &attributes) Chris@35: { Chris@35: QString colourSpec = attributes.value("colour"); Chris@35: if (colourSpec != "") { Chris@35: QColor colour(colourSpec); Chris@35: if (colour.isValid()) { Chris@35: setBaseColour(QColor(colourSpec)); Chris@35: } Chris@35: } Chris@35: } Chris@35: Chris@35: Chris@35: #ifdef INCLUDE_MOCFILES Chris@35: #include "TextLayer.moc.cpp" Chris@35: #endif Chris@35: