Mercurial > hg > svgui
comparison layer/TextLayer.cpp @ 35:10ba9276a315
* Add TextModel and TextLayer types
* Make View refresh work better when editing a model (previously edits might
not be refreshed if their visible changed area extended beyond the strict
frame range that was being modified in the model)
* Add phase-adjusted instantaneous frequency display to spectrogram layer
(still a work in progress)
* Pull maths aliases out into a separate header in dsp/maths so MathUtilities
can be included without introducing them
author | Chris Cannam |
---|---|
date | Mon, 20 Feb 2006 13:33:36 +0000 |
parents | |
children | c28ebb4ba4de |
comparison
equal
deleted
inserted
replaced
34:c43f2c4f66f2 | 35:10ba9276a315 |
---|---|
1 /* -*- c-basic-offset: 4 -*- vi:set ts=8 sts=4 sw=4: */ | |
2 | |
3 /* | |
4 A waveform viewer and audio annotation editor. | |
5 Chris Cannam, Queen Mary University of London, 2005-2006 | |
6 | |
7 This is experimental software. Not for distribution. | |
8 */ | |
9 | |
10 #include "TextLayer.h" | |
11 | |
12 #include "base/Model.h" | |
13 #include "base/RealTime.h" | |
14 #include "base/Profiler.h" | |
15 #include "base/View.h" | |
16 | |
17 #include "model/TextModel.h" | |
18 | |
19 #include <QPainter> | |
20 #include <QMouseEvent> | |
21 | |
22 #include <iostream> | |
23 #include <cmath> | |
24 | |
25 TextLayer::TextLayer(View *w) : | |
26 Layer(w), | |
27 m_model(0), | |
28 m_editing(false), | |
29 m_originalPoint(0, 0.0, tr("Empty Label")), | |
30 m_editingPoint(0, 0.0, tr("Empty Label")), | |
31 m_editingCommand(0), | |
32 m_colour(255, 150, 50) // orange | |
33 { | |
34 m_view->addLayer(this); | |
35 } | |
36 | |
37 void | |
38 TextLayer::setModel(TextModel *model) | |
39 { | |
40 if (m_model == model) return; | |
41 m_model = model; | |
42 | |
43 connect(m_model, SIGNAL(modelChanged()), this, SIGNAL(modelChanged())); | |
44 connect(m_model, SIGNAL(modelChanged(size_t, size_t)), | |
45 this, SIGNAL(modelChanged(size_t, size_t))); | |
46 | |
47 connect(m_model, SIGNAL(completionChanged()), | |
48 this, SIGNAL(modelCompletionChanged())); | |
49 | |
50 std::cerr << "TextLayer::setModel(" << model << ")" << std::endl; | |
51 | |
52 emit modelReplaced(); | |
53 } | |
54 | |
55 Layer::PropertyList | |
56 TextLayer::getProperties() const | |
57 { | |
58 PropertyList list; | |
59 list.push_back(tr("Colour")); | |
60 return list; | |
61 } | |
62 | |
63 Layer::PropertyType | |
64 TextLayer::getPropertyType(const PropertyName &name) const | |
65 { | |
66 return ValueProperty; | |
67 } | |
68 | |
69 int | |
70 TextLayer::getPropertyRangeAndValue(const PropertyName &name, | |
71 int *min, int *max) const | |
72 { | |
73 //!!! factor this colour handling stuff out into a colour manager class | |
74 | |
75 int deft = 0; | |
76 | |
77 if (name == tr("Colour")) { | |
78 | |
79 if (min) *min = 0; | |
80 if (max) *max = 5; | |
81 | |
82 if (m_colour == Qt::black) deft = 0; | |
83 else if (m_colour == Qt::darkRed) deft = 1; | |
84 else if (m_colour == Qt::darkBlue) deft = 2; | |
85 else if (m_colour == Qt::darkGreen) deft = 3; | |
86 else if (m_colour == QColor(200, 50, 255)) deft = 4; | |
87 else if (m_colour == QColor(255, 150, 50)) deft = 5; | |
88 | |
89 } else { | |
90 | |
91 deft = Layer::getPropertyRangeAndValue(name, min, max); | |
92 } | |
93 | |
94 return deft; | |
95 } | |
96 | |
97 QString | |
98 TextLayer::getPropertyValueLabel(const PropertyName &name, | |
99 int value) const | |
100 { | |
101 if (name == tr("Colour")) { | |
102 switch (value) { | |
103 default: | |
104 case 0: return tr("Black"); | |
105 case 1: return tr("Red"); | |
106 case 2: return tr("Blue"); | |
107 case 3: return tr("Green"); | |
108 case 4: return tr("Purple"); | |
109 case 5: return tr("Orange"); | |
110 } | |
111 } | |
112 return tr("<unknown>"); | |
113 } | |
114 | |
115 void | |
116 TextLayer::setProperty(const PropertyName &name, int value) | |
117 { | |
118 if (name == tr("Colour")) { | |
119 switch (value) { | |
120 default: | |
121 case 0: setBaseColour(Qt::black); break; | |
122 case 1: setBaseColour(Qt::darkRed); break; | |
123 case 2: setBaseColour(Qt::darkBlue); break; | |
124 case 3: setBaseColour(Qt::darkGreen); break; | |
125 case 4: setBaseColour(QColor(200, 50, 255)); break; | |
126 case 5: setBaseColour(QColor(255, 150, 50)); break; | |
127 } | |
128 } | |
129 } | |
130 | |
131 void | |
132 TextLayer::setBaseColour(QColor colour) | |
133 { | |
134 if (m_colour == colour) return; | |
135 m_colour = colour; | |
136 emit layerParametersChanged(); | |
137 } | |
138 | |
139 bool | |
140 TextLayer::isLayerScrollable() const | |
141 { | |
142 QPoint discard; | |
143 return !m_view->shouldIlluminateLocalFeatures(this, discard); | |
144 } | |
145 | |
146 | |
147 TextModel::PointList | |
148 TextLayer::getLocalPoints(int x, int y) const | |
149 { | |
150 if (!m_model) return TextModel::PointList(); | |
151 | |
152 long frame0 = getFrameForX(-150); | |
153 long frame1 = getFrameForX(m_view->width() + 150); | |
154 | |
155 TextModel::PointList points(m_model->getPoints(frame0, frame1)); | |
156 | |
157 TextModel::PointList rv; | |
158 QFontMetrics metrics = QPainter().fontMetrics(); | |
159 | |
160 for (TextModel::PointList::iterator i = points.begin(); | |
161 i != points.end(); ++i) { | |
162 | |
163 const TextModel::Point &p(*i); | |
164 | |
165 int px = getXForFrame(p.frame); | |
166 int py = getYForHeight(p.height); | |
167 | |
168 QString label = p.label; | |
169 if (label == "") { | |
170 label = tr("<no text>"); | |
171 } | |
172 | |
173 QRect rect = metrics.boundingRect | |
174 (QRect(0, 0, 150, 200), | |
175 Qt::AlignLeft | Qt::AlignTop | Qt::TextWordWrap, label); | |
176 | |
177 if (py + rect.height() > m_view->height()) { | |
178 if (rect.height() > m_view->height()) py = 0; | |
179 else py = m_view->height() - rect.height() - 1; | |
180 } | |
181 | |
182 if (x >= px && x < px + rect.width() && | |
183 y >= py && y < py + rect.height()) { | |
184 rv.insert(p); | |
185 } | |
186 } | |
187 | |
188 return rv; | |
189 } | |
190 | |
191 QString | |
192 TextLayer::getFeatureDescription(QPoint &pos) const | |
193 { | |
194 int x = pos.x(); | |
195 | |
196 if (!m_model || !m_model->getSampleRate()) return ""; | |
197 | |
198 TextModel::PointList points = getLocalPoints(x, pos.y()); | |
199 | |
200 if (points.empty()) { | |
201 if (!m_model->isReady()) { | |
202 return tr("In progress"); | |
203 } else { | |
204 return ""; | |
205 } | |
206 } | |
207 | |
208 long useFrame = points.begin()->frame; | |
209 | |
210 RealTime rt = RealTime::frame2RealTime(useFrame, m_model->getSampleRate()); | |
211 | |
212 QString text; | |
213 | |
214 if (points.begin()->label == "") { | |
215 text = QString(tr("Time:\t%1\nHeight:\t%2\nLabel:\t%3")) | |
216 .arg(rt.toText(true).c_str()) | |
217 .arg(points.begin()->height) | |
218 .arg(points.begin()->label); | |
219 } | |
220 | |
221 pos = QPoint(getXForFrame(useFrame), getYForHeight(points.begin()->height)); | |
222 return text; | |
223 } | |
224 | |
225 | |
226 //!!! too much overlap with TimeValueLayer/TimeInstantLayer | |
227 | |
228 bool | |
229 TextLayer::snapToFeatureFrame(int &frame, | |
230 size_t &resolution, | |
231 SnapType snap) const | |
232 { | |
233 if (!m_model) { | |
234 return Layer::snapToFeatureFrame(frame, resolution, snap); | |
235 } | |
236 | |
237 resolution = m_model->getResolution(); | |
238 TextModel::PointList points; | |
239 | |
240 if (snap == SnapNeighbouring) { | |
241 | |
242 points = getLocalPoints(getXForFrame(frame), -1); | |
243 if (points.empty()) return false; | |
244 frame = points.begin()->frame; | |
245 return true; | |
246 } | |
247 | |
248 points = m_model->getPoints(frame, frame); | |
249 int snapped = frame; | |
250 bool found = false; | |
251 | |
252 for (TextModel::PointList::const_iterator i = points.begin(); | |
253 i != points.end(); ++i) { | |
254 | |
255 if (snap == SnapRight) { | |
256 | |
257 if (i->frame > frame) { | |
258 snapped = i->frame; | |
259 found = true; | |
260 break; | |
261 } | |
262 | |
263 } else if (snap == SnapLeft) { | |
264 | |
265 if (i->frame <= frame) { | |
266 snapped = i->frame; | |
267 found = true; // don't break, as the next may be better | |
268 } else { | |
269 break; | |
270 } | |
271 | |
272 } else { // nearest | |
273 | |
274 TextModel::PointList::const_iterator j = i; | |
275 ++j; | |
276 | |
277 if (j == points.end()) { | |
278 | |
279 snapped = i->frame; | |
280 found = true; | |
281 break; | |
282 | |
283 } else if (j->frame >= frame) { | |
284 | |
285 if (j->frame - frame < frame - i->frame) { | |
286 snapped = j->frame; | |
287 } else { | |
288 snapped = i->frame; | |
289 } | |
290 found = true; | |
291 break; | |
292 } | |
293 } | |
294 } | |
295 | |
296 frame = snapped; | |
297 return found; | |
298 } | |
299 | |
300 int | |
301 TextLayer::getYForHeight(float height) const | |
302 { | |
303 int h = m_view->height(); | |
304 return h - int(height * h); | |
305 } | |
306 | |
307 float | |
308 TextLayer::getHeightForY(int y) const | |
309 { | |
310 int h = m_view->height(); | |
311 return float(h - y) / h; | |
312 } | |
313 | |
314 void | |
315 TextLayer::paint(QPainter &paint, QRect rect) const | |
316 { | |
317 if (!m_model || !m_model->isOK()) return; | |
318 | |
319 int sampleRate = m_model->getSampleRate(); | |
320 if (!sampleRate) return; | |
321 | |
322 // Profiler profiler("TextLayer::paint", true); | |
323 | |
324 int x0 = rect.left(), x1 = rect.right(); | |
325 long frame0 = getFrameForX(x0); | |
326 long frame1 = getFrameForX(x1); | |
327 | |
328 TextModel::PointList points(m_model->getPoints(frame0, frame1)); | |
329 if (points.empty()) return; | |
330 | |
331 QColor brushColour(m_colour); | |
332 brushColour.setAlpha(80); | |
333 paint.setBrush(brushColour); | |
334 | |
335 if (m_view->hasLightBackground()) { | |
336 paint.setPen(Qt::black); | |
337 } else { | |
338 paint.setPen(Qt::white); | |
339 } | |
340 | |
341 // std::cerr << "TextLayer::paint: resolution is " | |
342 // << m_model->getResolution() << " frames" << std::endl; | |
343 | |
344 QPoint localPos; | |
345 long illuminateFrame = -1; | |
346 | |
347 if (m_view->shouldIlluminateLocalFeatures(this, localPos)) { | |
348 TextModel::PointList localPoints = getLocalPoints(localPos.x(), | |
349 localPos.y()); | |
350 if (!localPoints.empty()) illuminateFrame = localPoints.begin()->frame; | |
351 } | |
352 | |
353 int boxMaxWidth = 150; | |
354 int boxMaxHeight = 200; | |
355 | |
356 paint.save(); | |
357 paint.setClipRect(rect.x(), 0, rect.width() + boxMaxWidth, m_view->height()); | |
358 | |
359 for (TextModel::PointList::const_iterator i = points.begin(); | |
360 i != points.end(); ++i) { | |
361 | |
362 const TextModel::Point &p(*i); | |
363 | |
364 int x = getXForFrame(p.frame); | |
365 int y = getYForHeight(p.height); | |
366 | |
367 if (illuminateFrame == p.frame) { | |
368 /* | |
369 //!!! aside from the problem of choosing a colour, it'd be | |
370 //better to save the highlighted rects and draw them at | |
371 //the end perhaps | |
372 | |
373 //!!! not equipped to illuminate the right section in line | |
374 //or curve mode | |
375 | |
376 if (m_plotStyle != PlotCurve && | |
377 m_plotStyle != PlotLines) { | |
378 paint.setPen(Qt::black);//!!! | |
379 if (m_plotStyle != PlotSegmentation) { | |
380 paint.setBrush(Qt::black);//!!! | |
381 } | |
382 } | |
383 */ | |
384 } | |
385 | |
386 QString label = p.label; | |
387 if (label == "") { | |
388 label = tr("<no text>"); | |
389 } | |
390 | |
391 QRect boxRect = paint.fontMetrics().boundingRect | |
392 (QRect(0, 0, boxMaxWidth, boxMaxHeight), | |
393 Qt::AlignLeft | Qt::AlignTop | Qt::TextWordWrap, label); | |
394 | |
395 QRect textRect = QRect(3, 2, boxRect.width(), boxRect.height()); | |
396 boxRect = QRect(0, 0, boxRect.width() + 6, boxRect.height() + 2); | |
397 | |
398 if (y + boxRect.height() > m_view->height()) { | |
399 if (boxRect.height() > m_view->height()) y = 0; | |
400 else y = m_view->height() - boxRect.height() - 1; | |
401 } | |
402 | |
403 boxRect = QRect(x, y, boxRect.width(), boxRect.height()); | |
404 textRect = QRect(x + 3, y + 2, textRect.width(), textRect.height()); | |
405 | |
406 // boxRect = QRect(x, y, boxRect.width(), boxRect.height()); | |
407 // textRect = QRect(x + 3, y + 2, textRect.width(), textRect.height()); | |
408 | |
409 paint.setRenderHint(QPainter::Antialiasing, false); | |
410 paint.drawRect(boxRect); | |
411 | |
412 paint.setRenderHint(QPainter::Antialiasing, true); | |
413 paint.drawText(textRect, | |
414 Qt::AlignLeft | Qt::AlignTop | Qt::TextWordWrap, | |
415 label); | |
416 | |
417 /// if (p.label != "") { | |
418 /// paint.drawText(x + 5, y - paint.fontMetrics().height() + paint.fontMetrics().ascent(), p.label); | |
419 /// } | |
420 } | |
421 | |
422 paint.restore(); | |
423 | |
424 // looks like save/restore doesn't deal with this: | |
425 paint.setRenderHint(QPainter::Antialiasing, false); | |
426 } | |
427 | |
428 void | |
429 TextLayer::drawStart(QMouseEvent *e) | |
430 { | |
431 std::cerr << "TextLayer::drawStart(" << e->x() << "," << e->y() << ")" << std::endl; | |
432 | |
433 if (!m_model) { | |
434 std::cerr << "TextLayer::drawStart: no model" << std::endl; | |
435 return; | |
436 } | |
437 | |
438 long frame = getFrameForX(e->x()); | |
439 if (frame < 0) frame = 0; | |
440 frame = frame / m_model->getResolution() * m_model->getResolution(); | |
441 | |
442 float height = getHeightForY(e->y()); | |
443 | |
444 m_editingPoint = TextModel::Point(frame, height, ""); | |
445 m_originalPoint = m_editingPoint; | |
446 | |
447 if (m_editingCommand) m_editingCommand->finish(); | |
448 m_editingCommand = new TextModel::EditCommand(m_model, "Add Label"); | |
449 m_editingCommand->addPoint(m_editingPoint); | |
450 | |
451 m_editing = true; | |
452 } | |
453 | |
454 void | |
455 TextLayer::drawDrag(QMouseEvent *e) | |
456 { | |
457 std::cerr << "TextLayer::drawDrag(" << e->x() << "," << e->y() << ")" << std::endl; | |
458 | |
459 if (!m_model || !m_editing) return; | |
460 | |
461 long frame = getFrameForX(e->x()); | |
462 if (frame < 0) frame = 0; | |
463 frame = frame / m_model->getResolution() * m_model->getResolution(); | |
464 | |
465 float height = getHeightForY(e->y()); | |
466 | |
467 m_editingCommand->deletePoint(m_editingPoint); | |
468 m_editingPoint.frame = frame; | |
469 m_editingPoint.height = height; | |
470 m_editingCommand->addPoint(m_editingPoint); | |
471 } | |
472 | |
473 void | |
474 TextLayer::drawEnd(QMouseEvent *e) | |
475 { | |
476 std::cerr << "TextLayer::drawEnd(" << e->x() << "," << e->y() << ")" << std::endl; | |
477 if (!m_model || !m_editing) return; | |
478 m_editingCommand->finish(); | |
479 m_editingCommand = 0; | |
480 m_editing = false; | |
481 } | |
482 | |
483 void | |
484 TextLayer::editStart(QMouseEvent *e) | |
485 { | |
486 std::cerr << "TextLayer::editStart(" << e->x() << "," << e->y() << ")" << std::endl; | |
487 | |
488 if (!m_model) return; | |
489 | |
490 TextModel::PointList points = getLocalPoints(e->x(), e->y()); | |
491 if (points.empty()) return; | |
492 | |
493 m_editingPoint = *points.begin(); | |
494 m_originalPoint = m_editingPoint; | |
495 | |
496 if (m_editingCommand) { | |
497 m_editingCommand->finish(); | |
498 m_editingCommand = 0; | |
499 } | |
500 | |
501 m_editing = true; | |
502 } | |
503 | |
504 void | |
505 TextLayer::editDrag(QMouseEvent *e) | |
506 { | |
507 if (!m_model || !m_editing) return; | |
508 | |
509 long frame = getFrameForX(e->x()); | |
510 if (frame < 0) frame = 0; | |
511 frame = frame / m_model->getResolution() * m_model->getResolution(); | |
512 | |
513 float height = getHeightForY(e->y()); | |
514 | |
515 if (!m_editingCommand) { | |
516 m_editingCommand = new TextModel::EditCommand(m_model, tr("Drag Label")); | |
517 } | |
518 | |
519 m_editingCommand->deletePoint(m_editingPoint); | |
520 m_editingPoint.frame = frame; | |
521 m_editingPoint.height = height; | |
522 m_editingCommand->addPoint(m_editingPoint); | |
523 } | |
524 | |
525 void | |
526 TextLayer::editEnd(QMouseEvent *e) | |
527 { | |
528 std::cerr << "TextLayer::editEnd(" << e->x() << "," << e->y() << ")" << std::endl; | |
529 if (!m_model || !m_editing) return; | |
530 | |
531 if (m_editingCommand) { | |
532 | |
533 QString newName = m_editingCommand->getName(); | |
534 | |
535 if (m_editingPoint.frame != m_originalPoint.frame) { | |
536 if (m_editingPoint.height != m_originalPoint.height) { | |
537 newName = tr("Move Label"); | |
538 } else { | |
539 newName = tr("Relocate Label"); | |
540 } | |
541 } else { | |
542 newName = tr("Change Point Height"); | |
543 } | |
544 | |
545 m_editingCommand->setName(newName); | |
546 m_editingCommand->finish(); | |
547 } | |
548 | |
549 m_editingCommand = 0; | |
550 m_editing = false; | |
551 } | |
552 | |
553 QString | |
554 TextLayer::toXmlString(QString indent, QString extraAttributes) const | |
555 { | |
556 return Layer::toXmlString(indent, extraAttributes + | |
557 QString(" colour=\"%1\"") | |
558 .arg(encodeColour(m_colour))); | |
559 } | |
560 | |
561 void | |
562 TextLayer::setProperties(const QXmlAttributes &attributes) | |
563 { | |
564 QString colourSpec = attributes.value("colour"); | |
565 if (colourSpec != "") { | |
566 QColor colour(colourSpec); | |
567 if (colour.isValid()) { | |
568 setBaseColour(QColor(colourSpec)); | |
569 } | |
570 } | |
571 } | |
572 | |
573 | |
574 #ifdef INCLUDE_MOCFILES | |
575 #include "TextLayer.moc.cpp" | |
576 #endif | |
577 |