Line data Source code
1 : /*
2 : * SPDX-License-Identifier: GPL-3.0-only
3 : * MuseScore-CLA-applies
4 : *
5 : * MuseScore Studio
6 : * Music Composition & Notation
7 : *
8 : * Copyright (C) 2025 MuseScore Limited and others
9 : *
10 : * This program is free software: you can redistribute it and/or modify
11 : * it under the terms of the GNU General Public License version 3 as
12 : * published by the Free Software Foundation.
13 : *
14 : * This program is distributed in the hope that it will be useful,
15 : * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 : * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 : * GNU General Public License for more details.
18 : *
19 : * You should have received a copy of the GNU General Public License
20 : * along with this program. If not, see <https://www.gnu.org/licenses/>.
21 : */
22 :
23 : #include "popupview.h"
24 : #include "modularity/ioc.h"
25 :
26 : #if defined(Q_OS_MAC)
27 : #include "internal/platform/macos/macospopupviewclosecontroller.h"
28 : #elif defined(Q_OS_WIN)
29 : #include "internal/platform/win/winpopupviewclosecontroller.h"
30 : #endif
31 :
32 : using namespace muse::uicomponents;
33 :
34 0 : static Qt::WindowFlags resolveWindowFlags()
35 : {
36 0 : Qt::WindowFlags flags;
37 0 : if (qGuiApp->platformName().contains("wayland")) {
38 0 : flags = Qt::Popup;
39 : } else {
40 0 : flags = Qt::Tool;
41 : }
42 :
43 : static int sIsKde = -1;
44 0 : if (sIsKde == -1) {
45 0 : QString desktop = qEnvironmentVariable("XDG_CURRENT_DESKTOP").toLower();
46 0 : QString session = qEnvironmentVariable("XDG_SESSION_DESKTOP").toLower();
47 0 : sIsKde = desktop.contains("kde") || session.contains("kde");
48 0 : }
49 0 : if (!sIsKde) {
50 0 : flags |= Qt::BypassWindowManagerHint; // Otherwise, it does not work correctly on Gnome (Linux) when resizing)
51 : }
52 :
53 : flags |= Qt::FramelessWindowHint // Without border
54 0 : | Qt::NoDropShadowWindowHint; // Without system shadow
55 :
56 0 : return flags;
57 : }
58 :
59 0 : PopupView::PopupView(QQuickItem* parent)
60 0 : : WindowView(parent)
61 : {
62 0 : setObjectName("PopupView");
63 :
64 0 : setShowArrow(true);
65 0 : setPadding(12);
66 0 : }
67 :
68 0 : PopupView::~PopupView()
69 : {
70 0 : if (m_closeController) {
71 0 : delete m_closeController;
72 : }
73 0 : }
74 :
75 0 : void PopupView::initView()
76 : {
77 0 : QQuickWindow::setDefaultAlphaBuffer(true);
78 :
79 0 : WindowView::initView();
80 :
81 0 : m_view->setFlags(resolveWindowFlags());
82 0 : m_view->setColor(Qt::transparent);
83 0 : }
84 :
85 0 : void PopupView::initCloseController()
86 : {
87 0 : muse::modularity::ContextPtr iocCtx = muse::iocCtxForQmlObject(this)();
88 : #if defined(Q_OS_MAC)
89 : m_closeController = new MacOSPopupViewCloseController(iocCtx);
90 : #elif defined(Q_OS_WIN)
91 : m_closeController = new WinPopupViewCloseController(iocCtx);
92 : #else
93 0 : m_closeController = new PopupViewCloseController(iocCtx);
94 : #endif
95 :
96 0 : m_closeController->init();
97 :
98 0 : m_closeController->setParentItem(parentItem());
99 0 : m_closeController->setWindow(window());
100 0 : m_closeController->setIsCloseOnPressOutsideParent(m_closePolicies & ClosePolicy::CloseOnPressOutsideParent);
101 0 : m_closeController->setCanClose(!m_closePolicies.testFlag(ClosePolicy::NoAutoClose));
102 :
103 0 : m_closeController->closeNotification().onNotify(this, [this]() {
104 0 : close();
105 0 : });
106 0 : }
107 :
108 0 : void PopupView::setParentItem(QQuickItem* parent)
109 : {
110 0 : WindowView::setParentItem(parent);
111 :
112 0 : if (m_closeController) {
113 0 : m_closeController->setParentItem(parent);
114 : }
115 0 : }
116 :
117 0 : void PopupView::beforeOpen()
118 : {
119 0 : WindowView::beforeOpen();
120 :
121 0 : if (!m_closeController) {
122 0 : initCloseController();
123 : }
124 :
125 0 : m_closeController->setActive(true);
126 :
127 0 : qApp->installEventFilter(this);
128 0 : }
129 :
130 0 : void PopupView::onHidden()
131 : {
132 0 : WindowView::onHidden();
133 :
134 0 : if (m_closeController) {
135 0 : m_closeController->setActive(false);
136 : }
137 :
138 0 : qApp->removeEventFilter(this);
139 0 : }
140 :
141 0 : bool PopupView::eventFilter(QObject* watched, QEvent* event)
142 : {
143 0 : if (event->type() == QEvent::Move && watched == m_parentWindow) {
144 0 : repositionWindowIfNeed();
145 : }
146 :
147 0 : return WindowView::eventFilter(watched, event);
148 : }
149 :
150 0 : PopupView::ClosePolicies PopupView::closePolicies() const
151 : {
152 0 : return m_closePolicies;
153 : }
154 :
155 0 : void PopupView::setClosePolicies(ClosePolicies closePolicies)
156 : {
157 0 : if (m_closePolicies == closePolicies) {
158 0 : return;
159 : }
160 :
161 0 : m_closePolicies = closePolicies;
162 :
163 0 : if (m_closeController) {
164 0 : m_closeController->setIsCloseOnPressOutsideParent(closePolicies & ClosePolicy::CloseOnPressOutsideParent);
165 : }
166 :
167 0 : emit closePoliciesChanged(closePolicies);
168 : }
169 :
170 0 : QQuickItem* PopupView::anchorItem() const
171 : {
172 0 : return m_anchorItem;
173 : }
174 :
175 0 : void PopupView::setAnchorItem(QQuickItem* anchorItem)
176 : {
177 0 : if (m_anchorItem == anchorItem) {
178 0 : return;
179 : }
180 :
181 0 : m_anchorItem = anchorItem;
182 0 : emit anchorItemChanged(m_anchorItem);
183 : }
184 :
185 0 : QRectF PopupView::anchorGeometry() const
186 : {
187 0 : QRectF geometry = currentScreenGeometry();
188 0 : if (m_anchorItem) {
189 0 : QPointF anchorItemTopLeft = m_anchorItem->mapToGlobal(QPoint(0, 0));
190 0 : geometry &= QRectF(anchorItemTopLeft, m_anchorItem->size());
191 : }
192 :
193 0 : return geometry;
194 : }
195 :
196 0 : PopupView::PlacementPolicies PopupView::placementPolicies() const
197 : {
198 0 : return m_placementPolicies;
199 : }
200 :
201 0 : void PopupView::setPlacementPolicies(PlacementPolicies placementPolicies)
202 : {
203 0 : if (m_placementPolicies == placementPolicies) {
204 0 : return;
205 : }
206 :
207 0 : m_placementPolicies = placementPolicies;
208 0 : emit placementPoliciesChanged(placementPolicies);
209 : }
210 :
211 0 : PopupPosition::Type PopupView::popupPosition() const
212 : {
213 0 : return m_popupPosition;
214 : }
215 :
216 0 : void PopupView::setPopupPosition(PopupPosition::Type position)
217 : {
218 0 : if (m_popupPosition == position) {
219 0 : return;
220 : }
221 :
222 0 : m_popupPosition = position;
223 0 : emit popupPositionChanged(m_popupPosition);
224 : }
225 :
226 0 : qreal PopupView::localX() const
227 : {
228 0 : return m_localPos.x();
229 : }
230 :
231 0 : qreal PopupView::localY() const
232 : {
233 0 : return m_localPos.y();
234 : }
235 :
236 0 : void PopupView::setLocalX(qreal x)
237 : {
238 0 : if (qFuzzyCompare(m_localPos.x(), x)) {
239 0 : return;
240 : }
241 :
242 0 : m_localPos.setX(x);
243 0 : emit xChanged(m_localPos.x());
244 :
245 0 : repositionWindowIfNeed();
246 : }
247 :
248 0 : void PopupView::setLocalY(qreal y)
249 : {
250 0 : if (qFuzzyCompare(m_localPos.y(), y)) {
251 0 : return;
252 : }
253 :
254 0 : m_localPos.setY(y);
255 0 : emit yChanged(m_localPos.y());
256 :
257 0 : repositionWindowIfNeed();
258 : }
259 :
260 0 : bool PopupView::showArrow() const
261 : {
262 0 : return m_showArrow;
263 : }
264 :
265 0 : void PopupView::setShowArrow(bool showArrow)
266 : {
267 0 : if (m_showArrow == showArrow) {
268 0 : return;
269 : }
270 :
271 0 : m_showArrow = showArrow;
272 0 : emit showArrowChanged(m_showArrow);
273 : }
274 :
275 0 : int PopupView::arrowX() const
276 : {
277 0 : return m_arrowX;
278 : }
279 :
280 0 : int PopupView::arrowY() const
281 : {
282 0 : return m_arrowY;
283 : }
284 :
285 0 : void PopupView::setArrowX(int arrowX)
286 : {
287 0 : if (m_arrowX == arrowX) {
288 0 : return;
289 : }
290 :
291 0 : m_arrowX = arrowX;
292 0 : emit arrowXChanged(m_arrowX);
293 : }
294 :
295 0 : void PopupView::setArrowY(int arrowY)
296 : {
297 0 : if (m_arrowY == arrowY) {
298 0 : return;
299 : }
300 :
301 0 : m_arrowY = arrowY;
302 0 : emit arrowYChanged(m_arrowY);
303 : }
304 :
305 0 : int PopupView::padding() const
306 : {
307 0 : return m_padding;
308 : }
309 :
310 0 : void PopupView::setPadding(int padding)
311 : {
312 0 : if (m_padding == padding) {
313 0 : return;
314 : }
315 :
316 0 : m_padding = padding;
317 0 : emit paddingChanged(m_padding);
318 : }
319 :
320 0 : void PopupView::close(bool force)
321 : {
322 0 : if (m_closeController) {
323 0 : m_closeController->setCanClose(true);
324 : }
325 0 : WindowView::close(force);
326 0 : }
327 :
328 0 : void PopupView::repositionWindowIfNeed()
329 : {
330 0 : if (!isOpened()) {
331 0 : return;
332 : }
333 :
334 0 : m_globalPos = QPointF();
335 0 : updateGeometry();
336 0 : m_view->setPosition(m_globalPos.toPoint());
337 0 : m_globalPos = QPoint();
338 : }
339 :
340 0 : void PopupView::updateGeometry()
341 : {
342 0 : const QQuickItem* parent = parentItem();
343 0 : IF_ASSERT_FAILED(parent) {
344 0 : return;
345 : }
346 :
347 0 : QPointF parentTopLeft = parent->mapToGlobal(QPoint(0, 0));
348 :
349 0 : if (m_globalPos.isNull()) {
350 0 : m_globalPos = parentTopLeft + m_localPos;
351 : }
352 :
353 0 : QRectF anchorRect = anchorGeometry();
354 0 : QRectF viewRect = viewGeometry();
355 :
356 0 : auto movePos = [this, &viewRect](qreal x, qreal y) {
357 0 : m_globalPos.setX(x);
358 0 : m_globalPos.setY(y);
359 :
360 0 : viewRect.moveTopLeft(m_globalPos);
361 0 : };
362 :
363 0 : bool ignoreFit = m_placementPolicies.testFlag(PlacementPolicy::IgnoreFit);
364 0 : bool canFitAbove = !ignoreFit ? viewRect.height() < parentTopLeft.y() : true;
365 0 : bool canFitBelow = !ignoreFit ? viewRect.bottom() < anchorRect.bottom() : true;
366 0 : bool canFitLeft = !ignoreFit ? viewRect.width() < parentTopLeft.x() : true;
367 0 : bool canFitRight = !ignoreFit ? viewRect.right() < anchorRect.right() : true;
368 :
369 0 : auto moveBelow = [&]() {
370 0 : movePos(m_globalPos.x(), parentTopLeft.y() + parent->height());
371 0 : setPopupPosition(PopupPosition::Bottom);
372 0 : };
373 :
374 0 : auto moveAbove = [&]() {
375 0 : movePos(m_globalPos.x(), parentTopLeft.y() - viewRect.height());
376 0 : setPopupPosition(PopupPosition::Top);
377 0 : };
378 :
379 0 : auto moveLeft = [&]() {
380 0 : movePos(parentTopLeft.x() - viewRect.width(), m_globalPos.y());
381 0 : setPopupPosition(PopupPosition::Left);
382 0 : };
383 :
384 0 : auto moveRight = [&]() {
385 0 : movePos(parentTopLeft.x() + parent->width(), m_globalPos.y());
386 0 : setPopupPosition(PopupPosition::Right);
387 0 : };
388 :
389 0 : bool placementDefault = m_placementPolicies.testFlag(PlacementPolicy::Default);
390 0 : bool preferBelow = m_placementPolicies.testFlag(PlacementPolicy::PreferBelow);
391 0 : bool preferAbove = m_placementPolicies.testFlag(PlacementPolicy::PreferAbove);
392 0 : bool preferLeft = m_placementPolicies.testFlag(PlacementPolicy::PreferLeft);
393 0 : bool preferRight = m_placementPolicies.testFlag(PlacementPolicy::PreferRight);
394 :
395 0 : if ((preferBelow || placementDefault) && canFitBelow) {
396 0 : moveBelow();
397 0 : } else if ((preferAbove || placementDefault) && canFitAbove) {
398 0 : moveAbove();
399 0 : } else if (preferLeft && canFitLeft) {
400 0 : moveLeft();
401 0 : } else if (preferRight && canFitRight) {
402 0 : moveRight();
403 0 : } else if (!canFitBelow && canFitAbove && (preferBelow || placementDefault)) {
404 0 : moveAbove();
405 0 : } else if (!canFitAbove && canFitBelow && (preferAbove || placementDefault)) {
406 0 : moveBelow();
407 0 : } else if (!canFitLeft && canFitRight && preferLeft) {
408 0 : moveRight();
409 0 : } else if (!canFitRight && canFitLeft && preferRight) {
410 0 : moveLeft();
411 : } else {
412 : // move to the right of the parent and move to top to an area that doesn't fit
413 0 : movePos(parentTopLeft.x() + parent->width(), m_globalPos.y() - (viewRect.bottom() - anchorRect.bottom()) + padding());
414 0 : setPopupPosition(PopupPosition::Right);
415 : }
416 :
417 0 : if (viewRect.left() < anchorRect.left()) {
418 : // move to the right to an area that doesn't fit
419 0 : movePos(m_globalPos.x() + anchorRect.left() - viewRect.left(), m_globalPos.y());
420 : }
421 :
422 0 : if (viewRect.right() > anchorRect.right()) {
423 : // move to the left to an area that doesn't fit
424 0 : movePos(m_globalPos.x() - (viewRect.right() - anchorRect.right()), m_globalPos.y());
425 : }
426 :
427 0 : if (!showArrow()) {
428 0 : if (popupPosition() == PopupPosition::Bottom || popupPosition() == PopupPosition::Top) {
429 0 : movePos(m_globalPos.x() - padding(), m_globalPos.y());
430 0 : } else if (popupPosition() == PopupPosition::Left || popupPosition() == PopupPosition::Right) {
431 0 : movePos(m_globalPos.x(), m_globalPos.y() - padding());
432 : }
433 : }
434 :
435 0 : updateContentPosition();
436 : }
437 :
438 0 : void PopupView::updateContentPosition()
439 : {
440 0 : if (!showArrow()) {
441 0 : return;
442 : }
443 :
444 0 : const QQuickItem* parent = parentItem();
445 0 : IF_ASSERT_FAILED(parent) {
446 0 : return;
447 : }
448 :
449 0 : QPointF parentTopLeft = parent->mapToGlobal(QPoint(0, 0));
450 :
451 0 : QRect viewGeometry = this->viewGeometry();
452 0 : QPointF viewTopLeft = QPointF(viewGeometry.x(), viewGeometry.y());
453 0 : QPointF viewTopRight = QPointF(viewGeometry.x() + viewGeometry.width(), viewGeometry.y());
454 :
455 0 : if (parentTopLeft.x() < viewTopLeft.x() || parentTopLeft.x() > viewTopRight.x()) {
456 0 : setArrowX(viewGeometry.width() / 2);
457 : } else {
458 0 : setArrowX(parentTopLeft.x() + (parent->width() / 2) - m_globalPos.x());
459 : }
460 :
461 0 : if (parentTopLeft.y() < viewTopLeft.y() || parentTopLeft.y() > viewGeometry.bottom()) {
462 0 : setArrowY(viewGeometry.height() / 2);
463 : } else {
464 0 : setArrowY(parentTopLeft.y() + (parent->height() / 2) - m_globalPos.y());
465 : }
466 : }
|