]> git.sesse.net Git - kdenlive/blob - src/abstractscopewidget.cpp
9c3bc416cc63b0f6c58f82f89b55b673a7418f99
[kdenlive] / src / abstractscopewidget.cpp
1 /***************************************************************************
2  *   Copyright (C) 2010 by Simon Andreas Eugster (simon.eu@gmail.com)      *
3  *   This file is part of kdenlive. See www.kdenlive.org.                  *
4  *                                                                         *
5  *   This program is free software; you can redistribute it and/or modify  *
6  *   it under the terms of the GNU General Public License as published by  *
7  *   the Free Software Foundation; either version 2 of the License, or     *
8  *   (at your option) any later version.                                   *
9  ***************************************************************************/
10
11 #include "qtconcurrentrun.h"
12
13 #include "abstractscopewidget.h"
14 #include "renderer.h"
15 #include "monitor.h"
16
17 #include <QFuture>
18 #include <QColor>
19 #include <QMenu>
20 #include <QMouseEvent>
21 #include <QPainter>
22
23 // Uncomment for Scope debugging.
24 //#define DEBUG_ASW
25
26 #ifdef DEBUG_ASW
27 #include <QDebug>
28 #endif
29
30 const int REALTIME_FPS = 30;
31
32 const QColor light(250, 238, 226, 255);
33 const QColor dark(40,  40,  39, 255);
34 const QColor dark2(25,  25,  23, 255);
35
36 const QPen AbstractScopeWidget::penThick(QBrush(QColor(250, 250, 250)),     2, Qt::SolidLine);
37 const QPen AbstractScopeWidget::penThin(QBrush(QColor(250, 250, 250)),     1, Qt::SolidLine);
38 const QPen AbstractScopeWidget::penLight(QBrush(QColor(200, 200, 250, 150)), 1, Qt::SolidLine);
39 const QPen AbstractScopeWidget::penLightDots(QBrush(QColor(200, 200, 250, 150)), 1, Qt::DotLine);
40 const QPen AbstractScopeWidget::penDark(QBrush(QColor(0, 0, 20, 250)),      1, Qt::SolidLine);
41 const QPen AbstractScopeWidget::penDarkDots(QBrush(QColor(0, 0, 20, 250)),      1, Qt::DotLine);
42
43 const QString AbstractScopeWidget::directions[] =  {"North", "Northeast", "East", "Southeast"};
44
45 AbstractScopeWidget::AbstractScopeWidget(bool trackMouse, QWidget *parent) :
46         QWidget(parent),
47         m_mousePos(0, 0),
48         m_mouseWithinWidget(false),
49         offset(5),
50         m_accelFactorHUD(1),
51         m_accelFactorScope(1),
52         m_accelFactorBackground(1),
53         m_semaphoreHUD(1),
54         m_semaphoreScope(1),
55         m_semaphoreBackground(1),
56         initialDimensionUpdateDone(false),
57         m_requestForcedUpdate(false),
58         m_rescaleMinDist(4),
59         m_rescaleVerticalThreshold(2.0f)
60
61 {
62     m_scopePalette = QPalette();
63     m_scopePalette.setBrush(QPalette::Window, QBrush(dark2));
64     m_scopePalette.setBrush(QPalette::Base, QBrush(dark));
65     m_scopePalette.setBrush(QPalette::Button, QBrush(dark));
66     m_scopePalette.setBrush(QPalette::Text, QBrush(light));
67     m_scopePalette.setBrush(QPalette::WindowText, QBrush(light));
68     m_scopePalette.setBrush(QPalette::ButtonText, QBrush(light));
69     this->setPalette(m_scopePalette);
70     this->setAutoFillBackground(true);
71
72     m_aAutoRefresh = new QAction(i18n("Auto Refresh"), this);
73     m_aAutoRefresh->setCheckable(true);
74     m_aRealtime = new QAction(i18n("Realtime (with precision loss)"), this);
75     m_aRealtime->setCheckable(true);
76
77     m_menu = new QMenu(this);
78     m_menu->setPalette(m_scopePalette);
79     m_menu->addAction(m_aAutoRefresh);
80     m_menu->addAction(m_aRealtime);
81
82     this->setContextMenuPolicy(Qt::CustomContextMenu);
83
84     bool b = true;
85     b &= connect(this, SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(customContextMenuRequested(QPoint)));
86
87     b &= connect(this, SIGNAL(signalHUDRenderingFinished(uint, uint)), this, SLOT(slotHUDRenderingFinished(uint, uint)));
88     b &= connect(this, SIGNAL(signalScopeRenderingFinished(uint, uint)), this, SLOT(slotScopeRenderingFinished(uint, uint)));
89     b &= connect(this, SIGNAL(signalBackgroundRenderingFinished(uint, uint)), this, SLOT(slotBackgroundRenderingFinished(uint, uint)));
90     b &= connect(m_aRealtime, SIGNAL(toggled(bool)), this, SLOT(slotResetRealtimeFactor(bool)));
91     b &= connect(m_aAutoRefresh, SIGNAL(toggled(bool)), this, SLOT(slotAutoRefreshToggled(bool)));
92     Q_ASSERT(b);
93
94     // Enable mouse tracking if desired.
95     // Causes the mouseMoved signal to be emitted when the mouse moves inside the
96     // widget, even when no mouse button is pressed.
97     this->setMouseTracking(trackMouse);
98 }
99
100 AbstractScopeWidget::~AbstractScopeWidget()
101 {
102     writeConfig();
103
104     delete m_menu;
105     delete m_aAutoRefresh;
106     delete m_aRealtime;
107 }
108
109 void AbstractScopeWidget::init()
110 {
111     m_widgetName = widgetName();
112     readConfig();
113 }
114
115 void AbstractScopeWidget::readConfig()
116 {
117     KSharedConfigPtr config = KGlobal::config();
118     KConfigGroup scopeConfig(config, configName());
119     m_aAutoRefresh->setChecked(scopeConfig.readEntry("autoRefresh", true));
120     m_aRealtime->setChecked(scopeConfig.readEntry("realtime", false));
121     scopeConfig.sync();
122 }
123
124 void AbstractScopeWidget::writeConfig()
125 {
126     KSharedConfigPtr config = KGlobal::config();
127     KConfigGroup scopeConfig(config, configName());
128     scopeConfig.writeEntry("autoRefresh", m_aAutoRefresh->isChecked());
129     scopeConfig.writeEntry("realtime", m_aRealtime->isChecked());
130     scopeConfig.sync();
131 }
132
133 QString AbstractScopeWidget::configName()
134 {
135     return "Scope_" + m_widgetName;
136 }
137
138 void AbstractScopeWidget::prodHUDThread()
139 {
140     if (this->visibleRegion().isEmpty()) {
141 #ifdef DEBUG_ASW
142         qDebug() << "Scope " << m_widgetName << " is not visible. Not calculating HUD.";
143 #endif
144     } else {
145         if (m_semaphoreHUD.tryAcquire(1)) {
146             Q_ASSERT(!m_threadHUD.isRunning());
147
148             m_newHUDFrames.fetchAndStoreRelaxed(0);
149             m_newHUDUpdates.fetchAndStoreRelaxed(0);
150             m_threadHUD = QtConcurrent::run(this, &AbstractScopeWidget::renderHUD, m_accelFactorHUD);
151 #ifdef DEBUG_ASW
152             qDebug() << "HUD thread started in " << m_widgetName;
153 #endif
154
155         }
156 #ifdef DEBUG_ASW
157         else {
158             qDebug() << "HUD semaphore locked, not prodding in " << m_widgetName << ". Thread running: " << m_threadHUD.isRunning();
159         }
160 #endif
161     }
162 }
163
164 void AbstractScopeWidget::prodScopeThread()
165 {
166     // Only start a new thread if the scope is actually visible
167     // and not hidden by another widget on the stack and if user want the scope to update.
168     if (this->visibleRegion().isEmpty() || (!m_aAutoRefresh->isChecked() && !m_requestForcedUpdate)) {
169 #ifdef DEBUG_ASW
170         qDebug() << "Scope " << m_widgetName << " is not visible. Not calculating scope.";
171 #endif
172     } else {
173         // Try to acquire the semaphore. This must only succeed if m_threadScope is not running
174         // anymore. Therefore the semaphore must NOT be released before m_threadScope ends.
175         // If acquiring the semaphore fails, the thread is still running.
176         if (m_semaphoreScope.tryAcquire(1)) {
177             Q_ASSERT(!m_threadScope.isRunning());
178
179             m_newScopeFrames.fetchAndStoreRelaxed(0);
180             m_newScopeUpdates.fetchAndStoreRelaxed(0);
181
182             Q_ASSERT(m_accelFactorScope > 0);
183
184             // See http://doc.qt.nokia.com/latest/qtconcurrentrun.html#run about
185             // running member functions in a thread
186             m_threadScope = QtConcurrent::run(this, &AbstractScopeWidget::renderScope, m_accelFactorScope);
187             m_requestForcedUpdate = false;
188
189 #ifdef DEBUG_ASW
190             qDebug() << "Scope thread started in " << m_widgetName;
191 #endif
192
193         } else {
194 #ifdef DEBUG_ASW
195             qDebug() << "Scope semaphore locked, not prodding in " << m_widgetName << ". Thread running: " << m_threadScope.isRunning();
196 #endif
197         }
198     }
199 }
200 void AbstractScopeWidget::prodBackgroundThread()
201 {
202     if (this->visibleRegion().isEmpty()) {
203 #ifdef DEBUG_ASW
204         qDebug() << "Scope " << m_widgetName << " is not visible. Not calculating background.";
205 #endif
206     } else {
207         if (m_semaphoreBackground.tryAcquire(1)) {
208             Q_ASSERT(!m_threadBackground.isRunning());
209
210             m_newBackgroundFrames.fetchAndStoreRelaxed(0);
211             m_newBackgroundUpdates.fetchAndStoreRelaxed(0);
212             m_threadBackground = QtConcurrent::run(this, &AbstractScopeWidget::renderBackground, m_accelFactorBackground);
213
214 #ifdef DEBUG_ASW
215             qDebug() << "Background thread started in " << m_widgetName;
216 #endif
217
218         } else {
219 #ifdef DEBUG_ASW
220             qDebug() << "Background semaphore locked, not prodding in " << m_widgetName << ". Thread running: " << m_threadBackground.isRunning();
221 #endif
222         }
223     }
224 }
225
226 void AbstractScopeWidget::forceUpdate(bool doUpdate)
227 {
228 #ifdef DEBUG_ASW
229     qDebug() << "Forced update called in " << widgetName() << ". Arg: " << doUpdate;
230 #endif
231
232     if (!doUpdate) {
233         return;
234     }
235     m_requestForcedUpdate = true;
236     m_newHUDUpdates.fetchAndAddRelaxed(1);
237     m_newScopeUpdates.fetchAndAddRelaxed(1);
238     m_newBackgroundUpdates.fetchAndAddRelaxed(1);
239     prodHUDThread();
240     prodScopeThread();
241     prodBackgroundThread();
242 }
243 void AbstractScopeWidget::forceUpdateHUD()
244 {
245     m_newHUDUpdates.fetchAndAddRelaxed(1);
246     prodHUDThread();
247
248 }
249 void AbstractScopeWidget::forceUpdateScope()
250 {
251     m_newScopeUpdates.fetchAndAddRelaxed(1);
252     m_requestForcedUpdate = true;
253     prodScopeThread();
254
255 }
256 void AbstractScopeWidget::forceUpdateBackground()
257 {
258     m_newBackgroundUpdates.fetchAndAddRelaxed(1);
259     prodBackgroundThread();
260
261 }
262
263
264 ///// Events /////
265
266
267 void AbstractScopeWidget::resizeEvent(QResizeEvent *event)
268 {
269     // Update the dimension of the available rect for painting
270     m_scopeRect = scopeRect();
271     forceUpdate();
272
273     QWidget::resizeEvent(event);
274 }
275
276 void AbstractScopeWidget::showEvent(QShowEvent *event)
277 {
278     QWidget::showEvent(event);
279     m_scopeRect = scopeRect();
280 }
281
282 void AbstractScopeWidget::paintEvent(QPaintEvent *)
283 {
284     QPainter davinci(this);
285     davinci.drawImage(m_scopeRect.topLeft(), m_imgBackground);
286     davinci.drawImage(m_scopeRect.topLeft(), m_imgScope);
287     davinci.drawImage(m_scopeRect.topLeft(), m_imgHUD);
288 }
289
290 void AbstractScopeWidget::mousePressEvent(QMouseEvent *event)
291 {
292     if (event->button() == Qt::LeftButton) {
293         // Rescaling mode starts
294         m_rescaleActive = true;
295         m_rescalePropertiesLocked = false;
296         m_rescaleFirstRescaleDone = false;
297         m_rescaleStartPoint = event->pos();
298         m_rescaleModifiers = event->modifiers();
299     }
300 }
301 void AbstractScopeWidget::mouseReleaseEvent(QMouseEvent *event)
302 {
303     m_rescaleActive = false;
304     m_rescalePropertiesLocked = false;
305
306     if (!m_aAutoRefresh->isChecked()) {
307         m_requestForcedUpdate = true;
308     }
309     prodHUDThread();
310     prodScopeThread();
311     prodBackgroundThread();
312     QWidget::mouseReleaseEvent(event);
313 }
314 void AbstractScopeWidget::mouseMoveEvent(QMouseEvent *event)
315 {
316     m_mousePos = event->pos();
317     m_mouseWithinWidget = true;
318     emit signalMousePositionChanged();
319
320     QPoint movement = event->pos()-m_rescaleStartPoint;
321
322     if (m_rescaleActive) {
323         if (m_rescalePropertiesLocked) {
324             // Direction is known, now adjust parameters
325
326             // Reset the starting point to make the next moveEvent relative to the current one
327             m_rescaleStartPoint = event->pos();
328
329
330             if (!m_rescaleFirstRescaleDone) {
331                 // We have just learned the desired direction; Normalize the movement to one pixel
332                 // to avoid a jump by m_rescaleMinDist
333
334                 if (movement.x() != 0) {
335                     movement.setX(movement.x() / abs(movement.x()));
336                 }
337                 if (movement.y() != 0) {
338                     movement.setY(movement.y() / abs(movement.y()));
339                 }
340
341                 m_rescaleFirstRescaleDone = true;
342             }
343
344             handleMouseDrag(movement, m_rescaleDirection, m_rescaleModifiers);
345
346
347
348         } else {
349             // Detect the movement direction here.
350             // This algorithm relies on the aspect ratio of dy/dx (size and signum).
351             if (movement.manhattanLength() > m_rescaleMinDist) {
352                 float diff = ((float) movement.y())/movement.x();
353
354                 if (abs(diff) > m_rescaleVerticalThreshold || movement.x() == 0) {
355                     m_rescaleDirection = North;
356                 } else if (abs(diff) < 1/m_rescaleVerticalThreshold) {
357                     m_rescaleDirection = East;
358                 } else if (diff < 0) {
359                     m_rescaleDirection = Northeast;
360                 } else {
361                     m_rescaleDirection = Southeast;
362                 }
363 #ifdef DEBUG_ASW
364                 qDebug() << "Diff is " << diff << "; chose " << directions[m_rescaleDirection] << " as direction";
365 #endif
366                 m_rescalePropertiesLocked = true;
367             }
368         }
369     }
370 }
371
372 void AbstractScopeWidget::leaveEvent(QEvent *)
373 {
374     m_mouseWithinWidget = false;
375     emit signalMousePositionChanged();
376 }
377
378 void AbstractScopeWidget::customContextMenuRequested(const QPoint &pos)
379 {
380     m_menu->exec(this->mapToGlobal(pos));
381 }
382
383 uint AbstractScopeWidget::calculateAccelFactorHUD(uint oldMseconds, uint)
384 {
385     return ceil((float)oldMseconds*REALTIME_FPS / 1000);
386 }
387 uint AbstractScopeWidget::calculateAccelFactorScope(uint oldMseconds, uint)
388 {
389     return ceil((float)oldMseconds*REALTIME_FPS / 1000);
390 }
391 uint AbstractScopeWidget::calculateAccelFactorBackground(uint oldMseconds, uint)
392 {
393     return ceil((float)oldMseconds*REALTIME_FPS / 1000);
394 }
395
396
397 ///// Slots /////
398
399 void AbstractScopeWidget::slotHUDRenderingFinished(uint mseconds, uint oldFactor)
400 {
401 #ifdef DEBUG_ASW
402     qDebug() << "HUD rendering has finished in " << mseconds << " ms, waiting for termination in " << m_widgetName;
403 #endif
404     m_threadHUD.waitForFinished();
405     m_imgHUD = m_threadHUD.result();
406
407     m_semaphoreHUD.release(1);
408     this->update();
409
410     int accel;
411     if (m_aRealtime->isChecked()) {
412         accel = calculateAccelFactorHUD(mseconds, oldFactor);
413         if (m_accelFactorHUD < 1) {
414             accel = 1;
415         }
416         m_accelFactorHUD = accel;
417     }
418
419     if ((m_newHUDFrames > 0 && m_aAutoRefresh->isChecked()) || m_newHUDUpdates > 0) {
420 #ifdef DEBUG_ASW
421         qDebug() << "Trying to start a new HUD thread for " << m_widgetName
422                 << ". New frames/updates: " << m_newHUDFrames << "/" << m_newHUDUpdates;
423 #endif
424         prodHUDThread();;
425     }
426 }
427
428 void AbstractScopeWidget::slotScopeRenderingFinished(uint mseconds, uint oldFactor)
429 {
430     // The signal can be received before the thread has really finished. So we
431     // need to wait until it has really finished before starting a new thread.
432 #ifdef DEBUG_ASW
433     qDebug() << "Scope rendering has finished in " << mseconds << " ms, waiting for termination in " << m_widgetName;
434 #endif
435     m_threadScope.waitForFinished();
436     m_imgScope = m_threadScope.result();
437
438     // The scope thread has finished. Now we can release the semaphore, allowing a new thread.
439     // See prodScopeThread where the semaphore is acquired again.
440     m_semaphoreScope.release(1);
441     this->update();
442
443     // Calculate the acceleration factor hint to get «realtime» updates.
444     int accel;
445     if (m_aRealtime->isChecked()) {
446         accel = calculateAccelFactorScope(mseconds, oldFactor);
447         if (accel < 1) {
448             // If mseconds happens to be 0.
449             accel = 1;
450         }
451         // Don't directly calculate with m_accelFactorScope as we are dealing with concurrency.
452         // If m_accelFactorScope is set to 0 at the wrong moment, who knows what might happen
453         // then :) Therefore use a local variable.
454         m_accelFactorScope = accel;
455     }
456
457     if ((m_newScopeFrames > 0 && m_aAutoRefresh->isChecked()) || m_newScopeUpdates > 0) {
458 #ifdef DEBUG_ASW
459         qDebug() << "Trying to start a new scope thread for " << m_widgetName
460                 << ". New frames/updates: " << m_newScopeFrames << "/" << m_newScopeUpdates;
461 #endif
462         prodScopeThread();
463     }
464 }
465
466 void AbstractScopeWidget::slotBackgroundRenderingFinished(uint mseconds, uint oldFactor)
467 {
468 #ifdef DEBUG_ASW
469     qDebug() << "Background rendering has finished in " << mseconds << " ms, waiting for termination in " << m_widgetName;
470 #endif
471     m_threadBackground.waitForFinished();
472     m_imgBackground = m_threadBackground.result();
473
474     m_semaphoreBackground.release(1);
475     this->update();
476
477     int accel;
478     if (m_aRealtime->isChecked()) {
479         accel = calculateAccelFactorBackground(mseconds, oldFactor);
480         if (m_accelFactorBackground < 1) {
481             accel = 1;
482         }
483         m_accelFactorBackground = accel;
484     }
485
486     if ((m_newBackgroundFrames > 0 && m_aAutoRefresh->isChecked()) || m_newBackgroundUpdates > 0) {
487 #ifdef DEBUG_ASW
488         qDebug() << "Trying to start a new background thread for " << m_widgetName
489                 << ". New frames/updates: " << m_newBackgroundFrames << "/" << m_newBackgroundUpdates;
490 #endif
491         prodBackgroundThread();;
492     }
493 }
494
495 void AbstractScopeWidget::slotRenderZoneUpdated()
496 {
497     m_newHUDFrames.fetchAndAddRelaxed(1);
498     m_newScopeFrames.fetchAndAddRelaxed(1);
499     m_newBackgroundFrames.fetchAndAddRelaxed(1);
500
501 #ifdef DEBUG_ASW
502     qDebug() << "Monitor incoming at " << widgetName() << ". New frames total HUD/Scope/Background: " << m_newHUDFrames
503             << "/" << m_newScopeFrames << "/" << m_newBackgroundFrames;
504 #endif
505
506     if (this->visibleRegion().isEmpty()) {
507 #ifdef DEBUG_ASW
508         qDebug() << "Scope of widget " << m_widgetName << " is not at the top, not rendering.";
509 #endif
510     } else {
511         if (m_aAutoRefresh->isChecked()) {
512             prodHUDThread();
513             prodScopeThread();
514             prodBackgroundThread();
515         }
516     }
517 }
518
519 void AbstractScopeWidget::slotResetRealtimeFactor(bool realtimeChecked)
520 {
521     if (!realtimeChecked) {
522         m_accelFactorHUD = 1;
523         m_accelFactorScope = 1;
524         m_accelFactorBackground = 1;
525     }
526 }
527
528 bool AbstractScopeWidget::autoRefreshEnabled()
529 {
530     return m_aAutoRefresh->isChecked();
531 }
532
533 void AbstractScopeWidget::slotAutoRefreshToggled(bool autoRefresh)
534 {
535 #ifdef DEBUG_ASW
536     qDebug() << "Auto-refresh switched to " << autoRefresh << " in " << widgetName()
537             << " (Visible: " << isVisible() << "/" << this->visibleRegion().isEmpty() << ")";
538 #endif
539     if (isVisible()) {
540         // Notify listeners whether we accept new frames now
541         emit requestAutoRefresh(autoRefresh);
542     }
543     // TODO only if depends on input
544     if (autoRefresh) {
545         //forceUpdate();
546         m_requestForcedUpdate = true;
547     }
548 }
549
550 void AbstractScopeWidget::handleMouseDrag(const QPoint, const RescaleDirection, const Qt::KeyboardModifiers) { }
551
552
553 #ifdef DEBUG_ASW
554 #undef DEBUG_ASW
555 #endif