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