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) 2021 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 "menuview.h"
24 :
25 : #include "log.h"
26 :
27 : using namespace muse::uicomponents;
28 :
29 : static const QString MENU_VIEW_CONTENT_OBJECT_NAME("_MenuViewContent");
30 :
31 : // Padding so that our menus don't "collide" with the top/bottom of our screen...
32 : static const int TOP_BOTTOM_EDGE_PADDING = 16;
33 :
34 0 : MenuView::MenuView(QQuickItem* parent)
35 0 : : PopupView(parent)
36 : {
37 0 : setObjectName("MenuView");
38 :
39 0 : setShowArrow(false);
40 0 : setPadding(8);
41 0 : }
42 :
43 0 : bool MenuView::isSearchable() const
44 : {
45 0 : return m_isSearchable;
46 : }
47 :
48 0 : void MenuView::setIsSearchable(bool isSearchable)
49 : {
50 0 : if (m_isSearchable == isSearchable) {
51 0 : return;
52 : }
53 0 : m_isSearchable = isSearchable;
54 0 : emit isSearchableChanged();
55 : }
56 :
57 0 : bool MenuView::isSearching() const
58 : {
59 0 : return m_isSearching;
60 : }
61 :
62 0 : void MenuView::setIsSearching(bool isSearching)
63 : {
64 0 : IF_ASSERT_FAILED(m_isSearchable || !isSearching) {
65 0 : return;
66 : }
67 0 : if (m_isSearching == isSearching) {
68 0 : return;
69 : }
70 0 : m_isSearching = isSearching;
71 0 : emit isSearchingChanged();
72 : }
73 :
74 0 : int MenuView::viewMargins() const
75 : {
76 0 : return 4;
77 : }
78 :
79 0 : Qt::AlignmentFlag MenuView::cascadeAlign() const
80 : {
81 0 : return m_cascadeAlign;
82 : }
83 :
84 0 : void MenuView::setCascadeAlign(Qt::AlignmentFlag cascadeAlign)
85 : {
86 0 : if (m_cascadeAlign == cascadeAlign) {
87 0 : return;
88 : }
89 :
90 0 : m_cascadeAlign = cascadeAlign;
91 0 : emit cascadeAlignChanged(m_cascadeAlign);
92 : }
93 :
94 0 : void MenuView::componentComplete()
95 : {
96 0 : m_contentItem->setObjectName(m_contentItem->objectName() + MENU_VIEW_CONTENT_OBJECT_NAME);
97 :
98 0 : PopupView::componentComplete();
99 0 : }
100 :
101 0 : void MenuView::updateGeometry()
102 : {
103 : // Desired height & width come from QML - these represent the hypothetical size of the
104 : // menu if there were no restrictions from the screen...
105 0 : setContentHeight(m_desiredHeight);
106 0 : setContentWidth(m_desiredWidth);
107 :
108 0 : const QQuickItem* parent = parentItem();
109 0 : IF_ASSERT_FAILED(parent) {
110 0 : return;
111 : }
112 :
113 0 : const QPointF parentTopLeft = parent->mapToGlobal(QPoint(0, 0));
114 :
115 0 : if (m_globalPos.isNull()) {
116 0 : m_globalPos = parentTopLeft;
117 : }
118 :
119 0 : const QRectF paddedAnchorRect = anchorGeometry().adjusted(0, TOP_BOTTOM_EDGE_PADDING, 0, -TOP_BOTTOM_EDGE_PADDING);
120 0 : QRectF viewRect = viewGeometry();
121 :
122 : //! NOTE: should be after resolving anchor geometry
123 : //! because we can move out of the screen
124 0 : m_globalPos += m_localPos;
125 :
126 0 : PopupPosition::Type newPopupPos = popupPosition();
127 0 : setCascadeAlign(Qt::AlignmentFlag::AlignRight);
128 :
129 0 : const auto movePos = [this, &viewRect](qreal x, qreal y) {
130 0 : m_globalPos.setX(x);
131 0 : m_globalPos.setY(y);
132 :
133 0 : viewRect.moveTopLeft(m_globalPos);
134 0 : };
135 :
136 : // Shrink and move the popup down if we detect an overlap with the top of the anchor rect...
137 0 : const auto clampTopIfNeed = [&]() {
138 0 : const qreal topOverlap = paddedAnchorRect.top() - viewRect.y();
139 0 : if (topOverlap > 0) {
140 0 : setContentHeight(m_desiredHeight - topOverlap);
141 0 : movePos(m_globalPos.x(), m_globalPos.y() + topOverlap);
142 : }
143 0 : };
144 :
145 0 : const QQuickItem* parentMenuContentItem = this->parentMenuContentItem();
146 0 : const bool isCascade = parentMenuContentItem != nullptr;
147 :
148 0 : if (isCascade) {
149 : // Position submenus to the right of the parent, at the same height...
150 0 : movePos(parentTopLeft.x() + parent->width(), m_globalPos.y() - parent->height() - viewMargins());
151 : }
152 :
153 0 : if (viewRect.left() < paddedAnchorRect.left()) { // The left of this menu overlaps the left of the anchor (doesn't fit)...
154 0 : movePos(m_globalPos.x() + paddedAnchorRect.left() - viewRect.left(), m_globalPos.y()); // Move to the right
155 : }
156 :
157 0 : const bool isSearchingAbove = isSearching() && popupPosition() == PopupPosition::Top;
158 0 : const bool isSearchingRight = isSearching() && popupPosition() == PopupPosition::Right;
159 0 : const bool isSearchingBottom = isSearching() && popupPosition() == PopupPosition::Bottom;
160 :
161 0 : const auto doRepositionResize = [&]() { // This gets quite complicated - lambda avoids nesting...
162 : // See usage of this lambda - it should only be called if we're searching or if we've detected
163 : // an overlap with the bottom of the anchor rect...
164 :
165 0 : const qreal bottomOverlap = viewRect.bottom() - paddedAnchorRect.bottom();
166 :
167 0 : if (isCascade) {
168 : // Submenu overlaps bottom - move up by overlap amount (don't change X)...
169 0 : movePos(m_globalPos.x(), m_globalPos.y() - bottomOverlap);
170 0 : clampTopIfNeed();
171 0 : return;
172 : }
173 :
174 0 : if (isSearchingBottom) {
175 0 : if (bottomOverlap > 0) {
176 : // Resize the popup so that it doesn't extend beyond the bottom of the screen...
177 0 : setContentHeight(m_desiredHeight - bottomOverlap);
178 : }
179 0 : return; // We're searching, so don't reposition the popup...
180 : }
181 :
182 0 : qreal desiredY = parentTopLeft.y() - viewRect.height();
183 0 : const qreal topOverlap = paddedAnchorRect.top() - desiredY;
184 0 : if (isSearchingAbove && topOverlap > 0) {
185 0 : setContentHeight(m_desiredHeight - topOverlap);
186 0 : viewRect.setHeight(viewRect.height() - topOverlap);
187 :
188 0 : desiredY = parentTopLeft.y() - viewRect.height(); // height changed - recompute...
189 : // No early return - we still need to actively position above...
190 : }
191 :
192 : // Searching right is an intermediate state that should never actually happen. When the popup is initially
193 : // positioned to the right and we start a search, we immediately reposition above...
194 0 : if (isSearchingAbove || isSearchingRight || paddedAnchorRect.top() < desiredY) {
195 : // Place above...
196 0 : movePos(m_globalPos.x(), desiredY);
197 0 : newPopupPos = PopupPosition::Top;
198 0 : return;
199 : }
200 :
201 0 : if (!isSearching()) {
202 : // Place to the right...
203 0 : movePos(parentTopLeft.x() + parent->width(), m_globalPos.y() - bottomOverlap);
204 0 : clampTopIfNeed();
205 0 : newPopupPos = PopupPosition::Right;
206 : }
207 0 : };
208 :
209 : // When searching a menu - we need to actively preserve the position / resize...
210 0 : const bool overlapsBottom = viewRect.bottom() > paddedAnchorRect.bottom();
211 0 : if (isSearching() || overlapsBottom) {
212 0 : doRepositionResize();
213 : }
214 :
215 0 : const Qt::AlignmentFlag parentCascadeAlign = this->parentCascadeAlign(parentMenuContentItem);
216 0 : if (viewRect.right() > paddedAnchorRect.right() || parentCascadeAlign != Qt::AlignmentFlag::AlignRight) {
217 0 : if (isCascade) {
218 : // move to the right of the parent
219 0 : movePos(parentTopLeft.x() - viewRect.width() + padding() * 2, m_globalPos.y());
220 0 : setCascadeAlign(Qt::AlignmentFlag::AlignLeft);
221 0 : newPopupPos = PopupPosition::Right;
222 : } else {
223 : // move to the left to an area that doesn't fit
224 0 : movePos(m_globalPos.x() - (viewRect.right() - paddedAnchorRect.right()) + padding() * 2, m_globalPos.y());
225 : }
226 : }
227 :
228 : // remove padding for arrow
229 0 : setPopupPosition(newPopupPos);
230 0 : movePos(m_globalPos.x() - padding(), m_globalPos.y());
231 0 : updateContentPosition();
232 : }
233 :
234 0 : void MenuView::updateContentPosition()
235 : {
236 0 : if (popupPosition() == PopupPosition::Top) {
237 0 : contentItem()->setY(padding());
238 : } else {
239 0 : contentItem()->setY(-padding());
240 : }
241 0 : }
242 :
243 0 : QRect MenuView::viewGeometry() const
244 : {
245 0 : return QRect(m_globalPos.toPoint(), QSize(contentWidth() + padding() * 2, contentHeight() + padding() * 2));
246 : }
247 :
248 0 : Qt::AlignmentFlag MenuView::parentCascadeAlign(const QQuickItem* parent) const
249 : {
250 0 : if (!parent) {
251 0 : return Qt::AlignmentFlag::AlignRight;
252 : }
253 :
254 0 : return static_cast<Qt::AlignmentFlag>(parent->property("cascadeAlign").toInt());
255 : }
256 :
257 0 : QQuickItem* MenuView::parentMenuContentItem() const
258 : {
259 0 : QQuickItem* parent = parentItem();
260 0 : while (parent) {
261 0 : if (parent->objectName().contains(MENU_VIEW_CONTENT_OBJECT_NAME)) {
262 0 : return parent;
263 : }
264 :
265 0 : parent = parent->parentItem();
266 : }
267 :
268 0 : return nullptr;
269 : }
270 :
271 0 : int MenuView::contentWidth() const
272 : {
273 0 : return m_contentWidth;
274 : }
275 :
276 0 : void MenuView::setContentWidth(int newContentWidth)
277 : {
278 0 : if (m_contentWidth == newContentWidth) {
279 0 : return;
280 : }
281 :
282 0 : m_contentWidth = newContentWidth;
283 0 : emit contentWidthChanged();
284 : }
285 :
286 0 : int MenuView::contentHeight() const
287 : {
288 0 : return m_contentHeight;
289 : }
290 :
291 0 : void MenuView::setContentHeight(int newContentHeight)
292 : {
293 0 : if (m_contentHeight == newContentHeight) {
294 0 : return;
295 : }
296 :
297 0 : m_contentHeight = newContentHeight;
298 0 : emit contentHeightChanged();
299 : }
300 :
301 0 : int MenuView::desiredHeight() const
302 : {
303 0 : return m_desiredHeight;
304 : }
305 :
306 0 : void MenuView::setDesiredHeight(int desiredHeight)
307 : {
308 0 : if (m_desiredHeight == desiredHeight) {
309 0 : return;
310 : }
311 :
312 0 : m_desiredHeight = desiredHeight;
313 0 : emit desiredHeightChanged();
314 :
315 0 : QMetaObject::invokeMethod(this, [this] {
316 0 : repositionWindowIfNeed();
317 0 : }, Qt::QueuedConnection);
318 : }
319 :
320 0 : int MenuView::desiredWidth() const
321 : {
322 0 : return m_desiredWidth;
323 : }
324 :
325 0 : void MenuView::setDesiredWidth(int desiredWidth)
326 : {
327 0 : if (m_desiredWidth == desiredWidth) {
328 0 : return;
329 : }
330 :
331 0 : m_desiredWidth = desiredWidth;
332 0 : emit desiredWidthChanged();
333 :
334 0 : QMetaObject::invokeMethod(this, [this] {
335 0 : repositionWindowIfNeed();
336 0 : }, Qt::QueuedConnection);
337 : }
|