1 /*
2  * Copyright (c) 2019 Meltytech, LLC
3  *
4  * This program is free software: you can redistribute it and/or modify
5  * it under the terms of the GNU General Public License as published by
6  * the Free Software Foundation, either version 3 of the License, or
7  * (at your option) any later version.
8  *
9  * This program is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12  * GNU General Public License for more details.
13  *
14  * You should have received a copy of the GNU General Public License
15  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
16  */
17 
18 #include "videozoomscopewidget.h"
19 
20 #include "glwidget.h"
21 #include "videozoomwidget.h"
22 
23 #include <QLabel>
24 #include <QToolButton>
25 #include <QToolTip>
26 #include <QGridLayout>
27 #include <QHBoxLayout>
28 
29 #include <math.h>
30 
getSeparator()31 QWidget* getSeparator()
32 {
33     // Create a 1 pixel wide line separator with a contrasting color
34     QWidget* separator = new QWidget();
35     separator->setGeometry(0, 0, 300, 300);
36     separator->setMinimumSize(1, 1);
37     QPalette pal = separator->palette();
38     pal.setColor(QPalette::Background, pal.color(QPalette::WindowText));
39     separator->setAutoFillBackground(true);
40     separator->setPalette(pal);
41     separator->show();
42     return separator;
43 }
44 
getPlayerBoundingRect(Mlt::GLWidget * glw)45 QRect getPlayerBoundingRect(Mlt::GLWidget* glw)
46 {
47     // Get the global rectangle of the player that contains image.
48     // This function assumes that the player is zoomed to best fit the image
49     // so that all of the image is available and it fills the widget in one
50     // direction.
51     QRect rect;
52     double widgetAr = (double)glw->width() / (double)glw->height();
53     double vidAr = MLT.profile().dar();
54     if (widgetAr > vidAr) {
55         double width = (double)glw->height() * vidAr;
56         rect.setX((round((double)glw->width() - width) / 2));
57         rect.setY(0);
58         rect.setWidth(round(width));
59         rect.setHeight(glw->height());
60     } else {
61         double height = glw->width() / vidAr;
62         rect.setX(0);
63         rect.setY((round((double)glw->height() - height) / 2));
64         rect.setWidth(glw->width());
65         rect.setHeight(round(height));
66     }
67     return QRect(glw->mapToGlobal(rect.topLeft()) , rect.size());
68 }
69 
pixelToPlayerPos(const QRect & playerRect,const QPoint & pixel)70 QPoint pixelToPlayerPos(const QRect& playerRect, const QPoint& pixel)
71 {
72     // Convert a pixel index to the corresponding global screen position of that
73     // pixel in the player.
74     double xOffset = (double)playerRect.width() * (double)pixel.x() / (double)MLT.profile().width();
75     double yOffset = (double)playerRect.height() * (double)pixel.y() / (double)MLT.profile().height();
76     return playerRect.topLeft() + QPoint(round(xOffset), round(yOffset));
77 }
78 
playerPosToPixel(const QRect & playerRect,const QPoint & pos)79 QPoint playerPosToPixel(const QRect& playerRect, const QPoint& pos)
80 {
81     // Convert the global position of a point in the player to the corresponding
82     // pixel index.
83     QPoint offset = pos - playerRect.topLeft();
84     double xOffset = (double)MLT.profile().width() * (double)offset.x() / (double)playerRect.width();
85     double yOffset = (double)MLT.profile().height() * (double)offset.y() / (double)playerRect.height();
86     return QPoint(round(xOffset), round(yOffset));
87 }
88 
VideoZoomScopeWidget()89 VideoZoomScopeWidget::VideoZoomScopeWidget()
90   : ScopeWidget("VideoZoom")
91   , m_zoomWidget(new VideoZoomWidget())
92   , m_zoomLabel(new QLabel(this))
93   , m_pixelXLabel(new QLabel(this))
94   , m_pixelYLabel(new QLabel(this))
95   , m_rLabel(new QLabel(this))
96   , m_gLabel(new QLabel(this))
97   , m_bLabel(new QLabel(this))
98   , m_yLabel(new QLabel(this))
99   , m_uLabel(new QLabel(this))
100   , m_vLabel(new QLabel(this))
101   , m_lockButton(new QToolButton(this))
102 {
103     LOG_DEBUG() << "begin";
104     QFont font = QWidget::font();
105     int fontSize = font.pointSize() - (font.pointSize() > 10? 2 : (font.pointSize() > 8? 1 : 0));
106     font.setPointSize(fontSize);
107     QWidget::setFont(font);
108 
109     QHBoxLayout* hlayout = new QHBoxLayout(this);
110     hlayout->setContentsMargins(0, 0, 0, 0);
111     hlayout->setSpacing(0);
112     QGridLayout* glayout = new QGridLayout();
113     glayout->setContentsMargins(5, 5, 2, 0);
114     glayout->setHorizontalSpacing(0);
115     glayout->setVerticalSpacing(2);
116 
117     // Add labels
118     glayout->addWidget(m_zoomLabel, 0, 0, 1, 2);
119     glayout->addWidget(getSeparator(), 1, 0, 1, 2);
120     glayout->addWidget(new QLabel(tr("x")), 2, 0, Qt::AlignLeft);
121     glayout->addWidget(m_pixelXLabel, 2, 1, Qt::AlignRight);
122     glayout->addWidget(new QLabel(tr("y")), 3, 0, Qt::AlignLeft);
123     glayout->addWidget(m_pixelYLabel, 3, 1, Qt::AlignRight);
124     glayout->addWidget(getSeparator(), 4, 0, 1, 2);
125     glayout->addWidget(new QLabel(tr("R")), 5, 0, Qt::AlignLeft);
126     glayout->addWidget(m_rLabel, 5, 1, Qt::AlignRight);
127     glayout->addWidget(new QLabel(tr("G")), 6, 0, Qt::AlignLeft);
128     glayout->addWidget(m_gLabel, 6, 1, Qt::AlignRight);
129     glayout->addWidget(new QLabel(tr("B")), 7, 0, Qt::AlignLeft);
130     glayout->addWidget(m_bLabel, 7, 1, Qt::AlignRight);
131     glayout->addWidget(getSeparator(), 8, 0, 1, 2);
132     glayout->addWidget(new QLabel(tr("Y")), 9, 0, Qt::AlignLeft);
133     glayout->addWidget(m_yLabel, 9, 1, Qt::AlignRight);
134     glayout->addWidget(new QLabel(tr("U")), 10, 0, Qt::AlignLeft);
135     glayout->addWidget(m_uLabel, 10, 1, Qt::AlignRight);
136     glayout->addWidget(new QLabel(tr("V")), 11, 0, Qt::AlignLeft);
137     glayout->addWidget(m_vLabel, 11, 1, Qt::AlignRight);
138     glayout->addItem(new QSpacerItem(0, 0, QSizePolicy::Minimum, QSizePolicy::Expanding), 12, 0, 1, 2);
139     updateLabels();
140     onZoomChanged(m_zoomWidget->getZoom());
141 
142     // Add HBoxLayout for tool buttons
143     QHBoxLayout* toolLayout = new QHBoxLayout();
144     toolLayout->setContentsMargins(0, 0, 0, 0);
145     toolLayout->setSpacing(0);
146     glayout->addLayout(toolLayout, 13, 0, 1, 2);
147 
148     // Add pixel picker button
149     QToolButton* pickButton = new QToolButton(this);
150     pickButton->setToolTip(tr("Pick a pixel from the source player"));
151     pickButton->setIcon(QIcon::fromTheme("zoom-select", QIcon(":/icons/oxygen/32x32/actions/zoom-select")));
152     toolLayout->addWidget(pickButton);
153     connect(pickButton, SIGNAL(clicked()), this, SLOT(onScreenSelectStarted()));
154 
155     // Add pixel lock button
156     m_lockButton->setToolTip(tr("Lock/Unlock the selected pixel"));
157     m_lockButton->setIcon(QIcon::fromTheme("object-unlocked", QIcon(":/icons/oxygen/32x32/status/object-unlocked")));
158     m_lockButton->setCheckable(true);
159     m_lockButton->setChecked(false);
160     toolLayout->addWidget(m_lockButton);
161     connect(m_lockButton, SIGNAL(toggled(bool)), this, SLOT(onLockToggled(bool)));
162 
163     toolLayout->addStretch();
164 
165     // Set minimum size for the grid layout.
166     QRect col1Size = fontMetrics().boundingRect("X ");
167     glayout->setColumnMinimumWidth(0, col1Size.width());
168     QRect col2Size = fontMetrics().boundingRect("9999");
169     glayout->setColumnMinimumWidth(1, col2Size.width());
170 
171     hlayout->addLayout(glayout);
172     hlayout->addWidget(m_zoomWidget);
173 
174     connect(m_zoomWidget, SIGNAL(pixelSelected(const QPoint&)), this, SLOT(onPixelSelected(const QPoint&)));
175     connect(m_zoomWidget, SIGNAL(zoomChanged(int)), this, SLOT(onZoomChanged(int)));
176     connect(&m_selector, SIGNAL(screenSelected(const QRect&)), this, SLOT(onScreenRectSelected(const QRect&)));
177     connect(&m_selector, SIGNAL(pointSelected(const QPoint&)), this, SLOT(onScreenPointSelected(const QPoint&)));
178     LOG_DEBUG() << "end";
179 }
180 
onScreenSelectStarted()181 void VideoZoomScopeWidget::onScreenSelectStarted()
182 {
183     if (!MLT.producer() || !MLT.producer()->is_valid()) {
184         return;
185     }
186 
187     Mlt::GLWidget* glw = qobject_cast<Mlt::GLWidget*>(MLT.videoWidget());
188     // Toggle the zoom off on the player so that the entire image is displayed
189     // in the player. The user can toggle it back on if they want.
190     glw->toggleZoom(false);
191 
192     // Get the global rectangle in the player that has the image in it.
193     QRect boundingRect = getPlayerBoundingRect(glw);
194     m_selector.setBoundingRect(boundingRect);
195 
196     // Calculate the size of the zoom window to show over the image.
197     QSize selectionSize;
198     selectionSize.setWidth((double)boundingRect.width() * (((double)m_zoomWidget->width() / (double)m_zoomWidget->getZoom()) / (double)MLT.profile().width()));
199     selectionSize.setHeight((double)boundingRect.height() * (((double)m_zoomWidget->height() / (double)m_zoomWidget->getZoom()) / (double)MLT.profile().height()));
200     m_selector.setFixedSize(selectionSize);
201 
202     // Calculate the global position of the zoom window.
203     QRect zoomRect = m_zoomWidget->getPixelRect();
204     QRect selectedRect;
205     selectedRect.setTopLeft(pixelToPlayerPos(boundingRect, zoomRect.topLeft()));
206     selectedRect.setBottomRight(pixelToPlayerPos(boundingRect, zoomRect.bottomRight()));
207     m_selector.setSelectedRect(selectedRect);
208 
209     // Calculate the global position of the selected pixel.
210     QPoint startPoint = pixelToPlayerPos(boundingRect, m_zoomWidget->getSelectedPixel());
211     m_selector.startSelection(startPoint);
212 }
213 
onLockToggled(bool enabled)214 void VideoZoomScopeWidget::onLockToggled(bool enabled)
215 {
216     m_zoomWidget->lock(enabled);
217     if (enabled) {
218         m_lockButton->setIcon(QIcon::fromTheme("object-locked", QIcon(":/icons/oxygen/32x32/status/object-locked")));
219     } else {
220         m_lockButton->setIcon(QIcon::fromTheme("object-unlocked", QIcon(":/icons/oxygen/32x32/status/object-unlocked")));
221     }
222 }
223 
onScreenRectSelected(const QRect & rect)224 void VideoZoomScopeWidget::onScreenRectSelected(const QRect& rect)
225 {
226     Mlt::GLWidget* glw = qobject_cast<Mlt::GLWidget*>(MLT.videoWidget());
227     QRect boundingRect = getPlayerBoundingRect(glw);
228     QPoint pixel = playerPosToPixel(boundingRect, rect.topLeft());
229     m_zoomWidget->setOffset(pixel);
230 }
231 
onScreenPointSelected(const QPoint & point)232 void VideoZoomScopeWidget::onScreenPointSelected(const QPoint& point)
233 {
234     Mlt::GLWidget* glw = qobject_cast<Mlt::GLWidget*>(MLT.videoWidget());
235     QRect boundingRect = getPlayerBoundingRect(glw);
236     QPoint pixel = playerPosToPixel(boundingRect, point);
237     m_zoomWidget->setSelectedPixel(pixel);
238 }
239 
onPixelSelected(const QPoint & pixel)240 void VideoZoomScopeWidget::onPixelSelected(const QPoint& pixel)
241 {
242     Q_UNUSED(pixel);
243     updateLabels();
244 }
245 
onZoomChanged(int zoom)246 void VideoZoomScopeWidget::onZoomChanged(int zoom)
247 {
248     m_zoomLabel->setText(tr("%1x").arg(zoom));
249 }
250 
refreshScope(const QSize & size,bool full)251 void VideoZoomScopeWidget::refreshScope(const QSize& size, bool full)
252 {
253     Q_UNUSED(size)
254     Q_UNUSED(full)
255 
256     SharedFrame frame;
257 
258     while (m_queue.count() > 0) {
259         frame = m_queue.pop();
260     }
261 
262     if (frame.is_valid()) {
263         m_zoomWidget->putFrame(frame);
264         QMetaObject::invokeMethod(this, "updateLabels", Qt::QueuedConnection);
265     }
266 }
267 
updateLabels()268 void VideoZoomScopeWidget::updateLabels()
269 {
270     QPoint selectedPixel = m_zoomWidget->getSelectedPixel();
271 
272     if (selectedPixel.x() >= 0) {
273         VideoZoomWidget::PixelValues values = m_zoomWidget->getPixelValues(selectedPixel);
274         m_pixelXLabel->setText(QString::number(selectedPixel.x() + 1));
275         m_pixelYLabel->setText(QString::number(selectedPixel.y() + 1));
276         m_rLabel->setText(QString::number(values.r));
277         m_gLabel->setText(QString::number(values.g));
278         m_bLabel->setText(QString::number(values.b));
279         m_yLabel->setText(QString::number(values.y));
280         m_uLabel->setText(QString::number(values.u));
281         m_vLabel->setText(QString::number(values.v));
282     } else {
283         m_pixelXLabel->setText("");
284         m_pixelYLabel->setText("");
285         m_rLabel->setText("---");
286         m_gLabel->setText("---");
287         m_bLabel->setText("---");
288         m_yLabel->setText("---");
289         m_uLabel->setText("---");
290         m_vLabel->setText("---");
291     }
292 }
293 
getTitle()294 QString VideoZoomScopeWidget::getTitle()
295 {
296    return tr("Video Zoom");
297 }
298