Line data Source code
1 : /*
2 : * SPDX-License-Identifier: GPL-3.0-only
3 : * MuseScore-Studio-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 "notationviewinputcontroller.h"
24 :
25 : #include <set>
26 :
27 : #include <QApplication>
28 : #include <QMimeData>
29 : #include <QQuickItem>
30 : #include <QTimer>
31 : #include <QtMath>
32 :
33 : #include "defer.h"
34 : #include "log.h"
35 :
36 : #include "abstractelementpopupmodel.h"
37 :
38 : #include "engraving/dom/drumset.h"
39 : #include "engraving/dom/fret.h"
40 : #include "engraving/dom/harmony.h"
41 : #include "engraving/dom/mscore.h"
42 : #include "engraving/dom/shadownote.h"
43 : #include "engraving/dom/staff.h"
44 :
45 : #include "notation/inotation.h"
46 : #include "notation/inotationelements.h" // IWYU pragma: keep
47 : #include "notation/inotationnoteinput.h"
48 : #include "notation/inotationpainting.h" // IWYU pragma: keep
49 : #include "notation/inotationselection.h"
50 : #include "notation/inotationstyle.h"
51 : #include "notation/inotationviewstate.h" // IWYU pragma: keep
52 :
53 : #include "notationscene/notationcommands.h"
54 :
55 : using namespace muse::rcommand;
56 : using namespace mu;
57 : using namespace mu::notation;
58 : using namespace mu::engraving;
59 :
60 : static constexpr int PIXELSSTEPSFACTOR = 5;
61 :
62 2 : static bool seekAllowed(const mu::engraving::EngravingItem* element)
63 : {
64 2 : if (!element) {
65 0 : return false;
66 : }
67 :
68 : static const ElementTypeSet ALLOWED_TYPES {
69 : ElementType::NOTE,
70 : ElementType::REST,
71 : ElementType::MMREST,
72 : ElementType::MEASURE,
73 : ElementType::BAR_LINE
74 4 : };
75 :
76 2 : return muse::contains(ALLOWED_TYPES, element->type());
77 : }
78 :
79 12 : NotationViewInputController::NotationViewInputController(IControlledView* view, const muse::modularity::ContextPtr& iocCtx)
80 12 : : muse::Contextable(iocCtx), m_view(view)
81 : {
82 12 : }
83 :
84 24 : NotationViewInputController::~NotationViewInputController()
85 : {
86 24 : }
87 :
88 0 : void NotationViewInputController::init()
89 : {
90 0 : m_possibleZoomPercentages = configuration()->possibleZoomPercentageList();
91 :
92 0 : if (!m_readonly) {
93 0 : commandsController()->setViewController(this);
94 :
95 0 : onNotationChanged();
96 0 : projectSession()->currentNotationChanged().onNotify(this, [this]() {
97 0 : onNotationChanged();
98 0 : });
99 : }
100 0 : }
101 :
102 0 : void NotationViewInputController::deinit()
103 : {
104 0 : if (commandsController()->viewController() == this) {
105 0 : commandsController()->setViewController(nullptr);
106 : }
107 0 : }
108 :
109 0 : void NotationViewInputController::onNotationChanged()
110 : {
111 0 : INotationPtr currNotation = currentNotation();
112 0 : if (!currNotation) {
113 0 : return;
114 : }
115 :
116 0 : currNotation->interaction()->selectionChanged().onNotify(this, [this]() {
117 0 : const INotationPtr notation = currentNotation();
118 0 : if (!notation) {
119 0 : return;
120 : }
121 :
122 0 : const EngravingItem* selectedItem = notation->interaction()->selection()->element();
123 0 : ElementType type = selectedItem ? selectedItem->type() : ElementType::INVALID;
124 :
125 0 : bool noChanges = selectedItem && m_prevSelectedElement == selectedItem;
126 0 : m_prevSelectedElement = selectedItem;
127 :
128 0 : if (noChanges) {
129 0 : return;
130 : }
131 :
132 0 : m_view->hideContextMenu();
133 0 : m_view->hideElementPopup();
134 :
135 0 : if (!(QApplication::mouseButtons() & Qt::RightButton)
136 0 : && AbstractElementPopupModel::hasElementEditPopup(selectedItem)) {
137 0 : m_view->showElementPopup(type);
138 : }
139 0 : }, Asyncable::Mode::SetReplace /* FIXME */);
140 :
141 0 : currNotation->interaction()->textEditingStarted().onNotify(this, [this] {
142 0 : const INotationPtr notation = currentNotation();
143 0 : if (!notation) {
144 0 : return;
145 : }
146 :
147 0 : m_view->hideContextMenu();
148 :
149 0 : const TextBase* item = notation->interaction()->editedText();
150 0 : if (AbstractElementPopupModel::hasTextStylePopup(item) && item->selected()) {
151 : static const std::set<ElementType> TYPES_NEEDING_STAFF {
152 : ElementType::LYRICS,
153 : ElementType::FINGERING,
154 : ElementType::STICKING,
155 : ElementType::HARMONY,
156 : ElementType::FIGURED_BASS
157 0 : };
158 0 : const bool needToSeeStaffWhileEnteringText = muse::contains(TYPES_NEEDING_STAFF, item->type());
159 0 : if (!needToSeeStaffWhileEnteringText || !item->empty()) {
160 0 : m_view->showElementPopup(item->type());
161 0 : return;
162 : }
163 : }
164 :
165 0 : if (item->isDynamic()) {
166 0 : m_view->showElementPopup(item->type());
167 0 : return;
168 : }
169 :
170 0 : m_view->hideElementPopup();
171 0 : }, Asyncable::Mode::SetReplace /* FIXME */);
172 :
173 0 : currNotation->interaction()->textEditingChanged().onNotify(this, [this] {
174 0 : const INotationPtr notation = currentNotation();
175 0 : if (!notation) {
176 0 : return;
177 : }
178 :
179 0 : if (!notation->interaction()->isTextEditingStarted()) {
180 0 : return;
181 : }
182 :
183 0 : const TextBase* item = notation->interaction()->editedText();
184 0 : if (AbstractElementPopupModel::hasTextStylePopup(item) && item->cursor()->hasSelection() && item->selected()) {
185 0 : m_view->showElementPopup(item->type());
186 : }
187 0 : }, Asyncable::Mode::SetReplace /* FIXME */);
188 :
189 0 : currNotation->interaction()->textEditingEnded().onReceive(this, [this](const TextBase*) {
190 0 : m_view->hideElementPopup(PopupModelType::TYPE_TEXT);
191 0 : }, Asyncable::Mode::SetReplace /* FIXME */);
192 0 : }
193 :
194 0 : void NotationViewInputController::initZoom()
195 : {
196 0 : IF_ASSERT_FAILED(currentNotation()) {
197 0 : return;
198 : }
199 :
200 0 : ZoomType defaultZoomType = configuration()->defaultZoomType();
201 :
202 0 : currentNotation()->viewState()->setZoomType(defaultZoomType);
203 :
204 0 : switch (defaultZoomType) {
205 0 : case ZoomType::Percentage:
206 0 : doSetZoom(configuration()->defaultZoom(), {});
207 0 : break;
208 0 : case ZoomType::PageWidth:
209 0 : doZoomToPageWidth();
210 0 : break;
211 0 : case ZoomType::WholePage:
212 0 : doZoomToWholePage();
213 0 : break;
214 0 : case ZoomType::TwoPages:
215 0 : doZoomToTwoPages();
216 0 : break;
217 : }
218 :
219 0 : currentNotation()->viewState()->setMatrixInited(true);
220 : }
221 :
222 0 : void NotationViewInputController::initCanvasPos()
223 : {
224 0 : RectF notationContentRect = m_view->notationContentRect();
225 0 : double totalScoreWidth = notationContentRect.width();
226 0 : double totalScoreHeight = notationContentRect.height();
227 :
228 0 : double curScaling = m_view->currentScaling();
229 0 : double viewWidth = m_view->width() / curScaling;
230 0 : double viewHeight = m_view->height() / curScaling;
231 :
232 0 : const double canvasMargin = MScore::horizontalPageGapOdd;
233 :
234 0 : bool centerHorizontally = totalScoreWidth < viewWidth - 2 * canvasMargin;
235 0 : bool centerVertically = totalScoreHeight < viewHeight - 2 * canvasMargin;
236 :
237 0 : double xMove = centerHorizontally ? 0.5 * (viewWidth - totalScoreWidth) : canvasMargin;
238 0 : double yMove = centerVertically ? 0.5 * (viewHeight - totalScoreHeight) : canvasMargin;
239 :
240 0 : m_view->moveCanvas(xMove, yMove);
241 0 : }
242 :
243 0 : void NotationViewInputController::updateZoomAfterSizeChange()
244 : {
245 0 : IF_ASSERT_FAILED(currentNotation()) {
246 0 : return;
247 : }
248 :
249 0 : switch (currentNotation()->viewState()->zoomType().val) {
250 0 : case ZoomType::Percentage:
251 0 : break;
252 0 : case ZoomType::PageWidth:
253 0 : doZoomToPageWidth();
254 0 : break;
255 0 : case ZoomType::WholePage:
256 0 : doZoomToWholePage();
257 0 : break;
258 0 : case ZoomType::TwoPages:
259 0 : doZoomToTwoPages();
260 0 : break;
261 : }
262 : }
263 :
264 0 : bool NotationViewInputController::readonly() const
265 : {
266 0 : return m_readonly;
267 : }
268 :
269 0 : void NotationViewInputController::setReadonly(bool readonly)
270 : {
271 0 : m_readonly = readonly;
272 0 : }
273 :
274 0 : INotationPtr NotationViewInputController::currentNotation() const
275 : {
276 0 : return projectSession()->currentNotation();
277 : }
278 :
279 0 : INotationStylePtr NotationViewInputController::notationStyle() const
280 : {
281 0 : return currentNotation() ? currentNotation()->style() : nullptr;
282 : }
283 :
284 117 : INotationInteractionPtr NotationViewInputController::viewInteraction() const
285 : {
286 117 : return m_view->notationInteraction();
287 : }
288 :
289 0 : const INotationInteraction::HitElementContext& NotationViewInputController::hitElementContext() const
290 : {
291 0 : return viewInteraction()->hitElementContext();
292 : }
293 :
294 0 : void NotationViewInputController::zoomIn()
295 : {
296 0 : int maxIndex = m_possibleZoomPercentages.size() > 0 ? m_possibleZoomPercentages.size() - 1 : 0;
297 0 : int currentIndex = std::min(currentZoomIndex() + 1, maxIndex);
298 0 : int zoom = m_possibleZoomPercentages[currentIndex];
299 0 : doSetZoom(zoom, findZoomFocusPoint());
300 0 : }
301 :
302 0 : void NotationViewInputController::zoomOut()
303 : {
304 0 : int currentIndex = std::max(currentZoomIndex() - 1, 0);
305 0 : int zoom = m_possibleZoomPercentages[currentIndex];
306 0 : doSetZoom(zoom, findZoomFocusPoint());
307 0 : }
308 :
309 0 : PointF NotationViewInputController::findZoomFocusPoint() const
310 : {
311 0 : double resultX = 0.0;
312 0 : double resultY = 0.0;
313 :
314 0 : INotationSelectionPtr selection = m_view->notationInteraction()->selection();
315 :
316 0 : if (selection->isNone()) {
317 : // No selection: zoom at the center of the view
318 0 : resultX = m_view->width() / 2;
319 0 : resultY = m_view->height() / 2;
320 : } else {
321 : // Selection: zoom at the center of the selection
322 0 : PointF result = m_view->fromLogical(selection->canvasBoundingRect().center());
323 0 : resultX = result.x();
324 0 : resultY = result.y();
325 : }
326 :
327 0 : return PointF(std::clamp(resultX, 0.0, m_view->width()),
328 0 : std::clamp(resultY, 0.0, m_view->height()));
329 0 : }
330 :
331 0 : void NotationViewInputController::zoomToPageWidth()
332 : {
333 0 : IF_ASSERT_FAILED(currentNotation()) {
334 0 : return;
335 : }
336 :
337 0 : currentNotation()->viewState()->setZoomType(ZoomType::PageWidth);
338 0 : doZoomToPageWidth();
339 : }
340 :
341 0 : void NotationViewInputController::doZoomToPageWidth()
342 : {
343 0 : if (!notationStyle()) {
344 0 : return;
345 : }
346 :
347 0 : qreal pageWidth = notationStyle()->styleValue(mu::engraving::Sid::pageWidth).toDouble() * mu::engraving::DPI
348 0 : + 2 * MScore::horizontalPageGapOdd;
349 0 : double viewWidth = m_view->width();
350 :
351 0 : qreal scale = viewWidth / pageWidth;
352 0 : setScaling(scale, PointF(), false);
353 : }
354 :
355 0 : void NotationViewInputController::zoomToWholePage()
356 : {
357 0 : IF_ASSERT_FAILED(currentNotation()) {
358 0 : return;
359 : }
360 :
361 0 : currentNotation()->viewState()->setZoomType(ZoomType::WholePage);
362 0 : doZoomToWholePage();
363 : }
364 :
365 0 : void NotationViewInputController::doZoomToWholePage()
366 : {
367 0 : if (!notationStyle()) {
368 0 : return;
369 : }
370 :
371 0 : qreal pageWidth = notationStyle()->styleValue(mu::engraving::Sid::pageWidth).toDouble() * mu::engraving::DPI
372 0 : + 2 * MScore::horizontalPageGapOdd;
373 0 : qreal pageHeight = notationStyle()->styleValue(mu::engraving::Sid::pageHeight).toDouble() * mu::engraving::DPI
374 0 : + 2 * MScore::horizontalPageGapOdd;
375 :
376 0 : double viewWidth = m_view->width();
377 0 : double viewHeight = m_view->height();
378 :
379 0 : qreal pageWidthScale = viewWidth / pageWidth;
380 0 : qreal pageHeightScale = viewHeight / pageHeight;
381 :
382 0 : qreal scale = std::min(pageWidthScale, pageHeightScale);
383 0 : setScaling(scale, PointF(), false);
384 : }
385 :
386 0 : void NotationViewInputController::zoomToTwoPages()
387 : {
388 0 : IF_ASSERT_FAILED(currentNotation()) {
389 0 : return;
390 : }
391 :
392 0 : currentNotation()->viewState()->setZoomType(ZoomType::TwoPages);
393 0 : doZoomToTwoPages();
394 : }
395 :
396 0 : void NotationViewInputController::doZoomToTwoPages()
397 : {
398 0 : if (!notationStyle()) {
399 0 : return;
400 : }
401 :
402 0 : qreal viewWidth = m_view->width();
403 0 : qreal viewHeight = m_view->height();
404 0 : qreal pageWidth = notationStyle()->styleValue(mu::engraving::Sid::pageWidth).toDouble() * mu::engraving::DPI
405 0 : + 2 * MScore::horizontalPageGapOdd;
406 0 : qreal pageHeight = notationStyle()->styleValue(mu::engraving::Sid::pageHeight).toDouble() * mu::engraving::DPI
407 0 : + 2 * MScore::horizontalPageGapOdd;
408 :
409 0 : qreal pageHeightScale = 0.0;
410 0 : qreal pageWidthScale = 0.0;
411 :
412 0 : if (configuration()->canvasOrientation().val == muse::Orientation::Vertical) {
413 0 : constexpr qreal VERTICAL_PAGE_GAP = 5.0;
414 0 : pageHeightScale = viewHeight / (pageHeight * 2.0 + VERTICAL_PAGE_GAP);
415 0 : pageWidthScale = viewWidth / pageWidth;
416 : } else {
417 0 : constexpr qreal HORIZONTAL_PAGE_GAP = 50.0;
418 0 : pageHeightScale = viewHeight / pageHeight;
419 0 : pageWidthScale = viewWidth / (pageWidth * 2.0 + HORIZONTAL_PAGE_GAP);
420 : }
421 :
422 0 : qreal scale = std::min(pageHeightScale, pageWidthScale);
423 0 : setScaling(scale, PointF(), false);
424 : }
425 :
426 0 : int NotationViewInputController::currentZoomIndex() const
427 : {
428 0 : for (int index = 0; index < m_possibleZoomPercentages.size(); ++index) {
429 0 : if (m_possibleZoomPercentages[index] >= currentZoomPercentage()) {
430 0 : return index;
431 : }
432 : }
433 :
434 0 : return m_possibleZoomPercentages.isEmpty() ? 0 : m_possibleZoomPercentages.size() - 1;
435 : }
436 :
437 0 : int NotationViewInputController::currentZoomPercentage() const
438 : {
439 0 : return zoomPercentageFromScaling(m_view->currentScaling());
440 : }
441 :
442 0 : void NotationViewInputController::setScaling(qreal scaling, const PointF& pos, bool overrideZoomType)
443 : {
444 0 : qreal minScaling = scalingFromZoomPercentage(m_possibleZoomPercentages.first());
445 0 : qreal maxScaling = scalingFromZoomPercentage(m_possibleZoomPercentages.last());
446 0 : qreal correctedScaling = std::clamp(scaling, minScaling, maxScaling);
447 :
448 0 : m_view->setScaling(correctedScaling, pos, overrideZoomType);
449 0 : }
450 :
451 0 : void NotationViewInputController::setZoom(int zoomPercentage)
452 : {
453 0 : doSetZoom(zoomPercentage, findZoomFocusPoint());
454 0 : }
455 :
456 0 : void NotationViewInputController::doSetZoom(int zoomPercentage, const PointF& pos)
457 : {
458 0 : int minZoom = m_possibleZoomPercentages.first();
459 0 : int maxZoom = m_possibleZoomPercentages.last();
460 0 : int correctedZoom = std::clamp(zoomPercentage, minZoom, maxZoom);
461 :
462 0 : qreal scaling = scalingFromZoomPercentage(correctedZoom);
463 0 : m_view->setScaling(scaling, pos);
464 0 : }
465 :
466 0 : qreal NotationViewInputController::scalingFromZoomPercentage(int zoomPercentage) const
467 : {
468 0 : return contextConfiguration()->scalingFromZoomPercentage(zoomPercentage);
469 : }
470 :
471 0 : int NotationViewInputController::zoomPercentageFromScaling(qreal scaling) const
472 : {
473 0 : return contextConfiguration()->zoomPercentageFromScaling(scaling);
474 : }
475 :
476 0 : void NotationViewInputController::setViewMode(ViewMode viewMode)
477 : {
478 0 : auto notation = projectSession()->currentNotation();
479 0 : if (notation) {
480 0 : notation->viewState()->setViewMode(viewMode);
481 0 : notation->painting()->setViewMode(viewMode);
482 : }
483 0 : }
484 :
485 0 : void NotationViewInputController::openContextMenuOfSelection()
486 : {
487 0 : m_view->showContextMenu(selectionType(), m_view->fromLogical(selectionElementPos()).toQPointF());
488 0 : }
489 :
490 : constexpr qreal scrollStep = .8;
491 : constexpr qreal notationScreenPadding = 25.0;
492 :
493 0 : void NotationViewInputController::moveScreen(int direction)
494 : {
495 0 : auto notation = currentNotation();
496 0 : if (!notation || muse::RealIsNull(m_view->width())) {
497 0 : return;
498 : }
499 0 : auto scale = m_view->currentScaling();
500 0 : if (notation->viewMode() == ViewMode::LINE) {
501 0 : m_view->moveCanvasHorizontal(m_view->width() * direction * scrollStep / scale);
502 : } else {
503 0 : auto offset = m_view->toLogical(QPoint());
504 0 : auto rect = m_view->notationContentRect();
505 0 : if (direction > 0 && offset.y() <= 0.0) {
506 0 : if (offset.x() >= -notationScreenPadding) {
507 0 : m_view->moveCanvas(m_view->width() * direction * scrollStep / scale,
508 0 : offset.y() - notationScreenPadding - (rect.height() - m_view->height() / scale));
509 : }
510 0 : } else if (direction < 0 && offset.y() >= (rect.height() - m_view->height() / scale)) {
511 0 : auto dx = m_view->width() * direction * scrollStep / scale;
512 0 : if (offset.x() < rect.width() + notationScreenPadding + dx) {
513 0 : m_view->moveCanvas(dx, offset.y() + notationScreenPadding);
514 : }
515 : } else {
516 0 : m_view->moveCanvasVertical(m_view->height() * direction * scrollStep / scale);
517 : }
518 : }
519 0 : }
520 :
521 0 : void NotationViewInputController::movePage(int direction)
522 : {
523 0 : auto notation = currentNotation();
524 0 : if (!notation) {
525 0 : return;
526 : }
527 0 : if (notation->viewMode() != ViewMode::PAGE) {
528 0 : moveScreen(direction);
529 0 : return;
530 : }
531 0 : Page* page = notation->elements()->msScore()->pages().back();
532 0 : if (configuration()->canvasOrientation().val == muse::Orientation::Vertical) {
533 0 : qreal offset = std::min((page->height() + notationScreenPadding) * direction,
534 0 : m_view->toLogical(QPoint()).y() + notationScreenPadding);
535 0 : m_view->moveCanvasVertical(offset);
536 : } else {
537 0 : qreal offset = std::min((page->width() + notationScreenPadding) * direction,
538 0 : m_view->toLogical(QPoint()).x() + notationScreenPadding);
539 0 : m_view->moveCanvasHorizontal(offset);
540 : }
541 0 : }
542 :
543 0 : void NotationViewInputController::nextScreen()
544 : {
545 0 : moveScreen(-1);
546 0 : }
547 :
548 0 : void NotationViewInputController::previousScreen()
549 : {
550 0 : moveScreen(1);
551 0 : }
552 :
553 0 : void NotationViewInputController::nextPage()
554 : {
555 0 : movePage(-1);
556 0 : }
557 :
558 0 : void NotationViewInputController::previousPage()
559 : {
560 0 : movePage(1);
561 0 : }
562 :
563 0 : void NotationViewInputController::startOfScore()
564 : {
565 0 : auto offset = m_view->toLogical(QPoint());
566 0 : m_view->moveCanvas(offset.x() + notationScreenPadding, offset.y() + notationScreenPadding);
567 0 : }
568 :
569 0 : void NotationViewInputController::endOfScore()
570 : {
571 0 : auto notation = currentNotation();
572 0 : if (!notation) {
573 0 : return;
574 : }
575 0 : mu::engraving::MeasureBase* lastMeasure = notation->elements()->msScore()->lastMeasureMM();
576 0 : auto lmRect = lastMeasure->canvasBoundingRect();
577 0 : auto scale = m_view->currentScaling();
578 0 : qreal desiredX = std::max(-notationScreenPadding, lmRect.right() + notationScreenPadding - m_view->width() / scale);
579 0 : qreal desiredY = std::max(-notationScreenPadding, lmRect.bottom()
580 0 : + lastMeasure->score()->style().styleD(mu::engraving::Sid::spatium) * 5 - m_view->height() / scale);
581 0 : auto offset = m_view->toLogical(QPoint());
582 0 : m_view->moveCanvas(offset.x() - desiredX, offset.y() - desiredY);
583 0 : }
584 :
585 0 : void NotationViewInputController::pinchToZoom(qreal scaleFactor, const QPointF& pos)
586 : {
587 0 : double scale = m_view->currentScaling() * scaleFactor;
588 0 : setScaling(scale, PointF::fromQPointF(pos));
589 0 : }
590 :
591 4 : void NotationViewInputController::wheelEvent(QWheelEvent* event)
592 : {
593 4 : QPoint pixelsScrolled = event->pixelDelta();
594 4 : QPoint stepsScrolled = event->angleDelta();
595 :
596 4 : int dx = 0;
597 4 : int dy = 0;
598 4 : qreal stepsX = 0.0;
599 4 : qreal stepsY = 0.0;
600 :
601 : // pixelDelta is unreliable on X11
602 : #ifdef Q_OS_LINUX
603 4 : if (std::getenv("WAYLAND_DISPLAY") == NULL) {
604 : // Ignore pixelsScrolled unless Wayland is used
605 0 : pixelsScrolled.setX(0);
606 0 : pixelsScrolled.setY(0);
607 : }
608 : #endif
609 :
610 4 : if (!pixelsScrolled.isNull()) {
611 3 : dx = pixelsScrolled.x();
612 3 : dy = pixelsScrolled.y();
613 3 : stepsX = dx / static_cast<qreal>(PIXELSSTEPSFACTOR);
614 3 : stepsY = dy / static_cast<qreal>(PIXELSSTEPSFACTOR);
615 1 : } else if (!stepsScrolled.isNull()) {
616 1 : dx = (stepsScrolled.x() * qMax(2.0, m_view->width() / 10.0)) / static_cast<qreal>(QWheelEvent::DefaultDeltasPerStep);
617 1 : dy = (stepsScrolled.y() * qMax(2.0, m_view->height() / 10.0)) / static_cast<qreal>(QWheelEvent::DefaultDeltasPerStep);
618 1 : stepsX = static_cast<qreal>(stepsScrolled.x()) / static_cast<qreal>(QWheelEvent::DefaultDeltasPerStep);
619 1 : stepsY = static_cast<qreal>(stepsScrolled.y()) / static_cast<qreal>(QWheelEvent::DefaultDeltasPerStep);
620 : }
621 :
622 4 : Qt::KeyboardModifiers keyState = event->modifiers();
623 :
624 : // Windows touch pad pinches also execute this
625 4 : if (keyState & Qt::ControlModifier) {
626 0 : double zoomSpeed = qPow(2.0, 1.0 / configuration()->mouseZoomPrecision());
627 0 : qreal absSteps = sqrt(stepsX * stepsX + stepsY * stepsY) * (stepsY > -stepsX ? 1 : -1);
628 0 : double scale = m_view->currentScaling() * qPow(zoomSpeed, absSteps);
629 0 : setScaling(scale, PointF::fromQPointF(event->position()));
630 : } else {
631 4 : qreal correction = 1.0 / m_view->currentScaling();
632 4 : if (keyState & Qt::ShiftModifier) {
633 1 : int abs = sqrt(dx * dx + dy * dy) * (dy > -dx ? 1 : -1);
634 1 : m_view->moveCanvasHorizontal(abs * correction);
635 : } else {
636 3 : m_view->moveCanvas(dx * correction, dy * correction);
637 : }
638 : }
639 4 : }
640 :
641 12 : void NotationViewInputController::mousePressEvent(QMouseEvent* event)
642 : {
643 12 : PointF logicPos = m_view->toLogical(event->pos());
644 12 : Qt::MouseButton button = event->button();
645 :
646 12 : m_mouseDownInfo = {
647 : /*.dragAction =*/ MouseDownInfo::Other,
648 12 : /*.physicalBeginPoint =*/ event->pos(),
649 : /*.logicalBeginPoint =*/ logicPos
650 : };
651 :
652 12 : m_shouldStartEditOnLeftClickRelease = false;
653 12 : m_ignoreNextMouseContextMenuEvent = false;
654 :
655 : // When using MiddleButton, just start moving the canvas
656 12 : if (button == Qt::MiddleButton) {
657 0 : return;
658 : }
659 :
660 12 : EngravingItem* hitElement = nullptr;
661 12 : staff_idx_t hitStaffIndex = muse::nidx;
662 :
663 12 : if (!m_readonly) {
664 12 : INotationInteraction::HitElementContext context;
665 12 : context.element = viewInteraction()->hitElement(logicPos, hitWidth());
666 12 : context.staff = viewInteraction()->hitStaff(logicPos);
667 12 : viewInteraction()->setHitElementContext(context);
668 :
669 12 : hitElement = context.element;
670 12 : hitStaffIndex = context.staff ? context.staff->idx() : muse::nidx;
671 : }
672 :
673 : // note enter mode
674 12 : if (m_view->isNoteEnterMode()) {
675 0 : handleClickInNoteInputMode(event);
676 0 : return;
677 : }
678 :
679 : // triple click
680 12 : if (m_tripleClickPending) {
681 0 : if (viewInteraction()->isTextEditingStarted()) {
682 0 : viewInteraction()->selectText(mu::engraving::SelectTextType::All);
683 0 : return;
684 : }
685 : }
686 :
687 12 : if (playbackController()->isPlaying()) {
688 0 : playbackController()->seekElement(hitElement);
689 0 : return;
690 : }
691 :
692 12 : ClickContext ctx;
693 12 : ctx.logicClickPos = logicPos;
694 12 : ctx.hitElement = hitElement;
695 12 : ctx.hitStaff = hitStaffIndex;
696 12 : ctx.isHitGrip = viewInteraction()->isHitGrip(logicPos);
697 12 : ctx.event = event;
698 :
699 : // Grip
700 12 : bool consumed = mousePress_considerGrip(ctx);
701 12 : if (consumed) {
702 0 : return;
703 : }
704 :
705 : // End editing
706 12 : if (!ctx.hitElement) {
707 0 : viewInteraction()->endEditElement();
708 : }
709 :
710 : // Drag outgoing: element
711 12 : consumed = mousePress_considerDragOutgoingElement(ctx);
712 12 : if (consumed) {
713 0 : return;
714 : }
715 :
716 : // Drag outgoing: range
717 12 : consumed = mousePress_considerDragOutgoingRange(ctx);
718 12 : if (consumed) {
719 0 : return;
720 : }
721 :
722 : // Alt+click to paste range
723 12 : consumed = mousePress_considerStartPasteRangeOnRelease(ctx);
724 12 : if (consumed) {
725 0 : return;
726 : }
727 :
728 : // Select
729 12 : mousePress_considerSelect(ctx);
730 :
731 : // Seek
732 12 : mousePress_seekSelection(ctx);
733 :
734 : // Misc
735 12 : if (button == Qt::LeftButton) {
736 10 : handleLeftClick(ctx);
737 2 : } else if (button == Qt::RightButton) {
738 2 : handleRightClick();
739 : }
740 : }
741 :
742 0 : void NotationViewInputController::handleClickInNoteInputMode(QMouseEvent* event)
743 : {
744 0 : const PointF logicPos = m_view->toLogical(event->pos());
745 :
746 0 : if (event->button() == Qt::RightButton) {
747 0 : m_ignoreNextMouseContextMenuEvent = true;
748 0 : muse::rcommand::CommandQuery query(SCREEN_REMOVE_NOTE_COMMAND);
749 0 : query.set("pos_x", muse::Val(logicPos.x()));
750 0 : query.set("pos_y", muse::Val(logicPos.y()));
751 0 : commandDispatcher()->dispatch(query);
752 0 : } else {
753 0 : const Qt::KeyboardModifiers keyState = event->modifiers();
754 0 : const bool replace = keyState & Qt::ShiftModifier;
755 0 : const bool insert = keyState & Qt::ControlModifier;
756 :
757 0 : muse::rcommand::CommandQuery query(SCREEN_PUT_NOTE_COMMAND);
758 0 : query.set("pos_x", muse::Val(logicPos.x()));
759 0 : query.set("pos_y", muse::Val(logicPos.y()));
760 0 : query.set("replace", replace);
761 0 : query.set("insert", insert);
762 0 : commandDispatcher()->dispatch(query);
763 0 : }
764 0 : }
765 :
766 12 : bool NotationViewInputController::mousePress_considerGrip(const ClickContext& ctx)
767 : {
768 12 : if (ctx.isHitGrip && ctx.event->button() == Qt::LeftButton) {
769 0 : viewInteraction()->startEditGrip(ctx.logicClickPos);
770 0 : return true;
771 12 : } else if (viewInteraction()->isGripEditStarted()) {
772 0 : viewInteraction()->endEditGrip();
773 : }
774 :
775 12 : return false;
776 : }
777 :
778 12 : bool NotationViewInputController::mousePress_considerDragOutgoingElement(const ClickContext& ctx)
779 : {
780 12 : if (ctx.event->button() != Qt::LeftButton
781 12 : || ctx.event->modifiers() != (Qt::ShiftModifier | Qt::ControlModifier)) {
782 12 : return false;
783 : }
784 :
785 0 : if (!ctx.hitElement || !viewInteraction()->isOutgoingDragElementAllowed(ctx.hitElement)) {
786 0 : return false;
787 : }
788 :
789 0 : viewInteraction()->select({ ctx.hitElement }, SelectType::SINGLE, ctx.hitStaff);
790 0 : playbackController()->seekElement(ctx.hitElement);
791 0 : m_mouseDownInfo.dragAction = MouseDownInfo::DragOutgoingElement;
792 :
793 0 : return true;
794 : }
795 :
796 12 : bool NotationViewInputController::mousePress_considerStartPasteRangeOnRelease(const ClickContext& ctx)
797 : {
798 : // Check if this is a left click with Alt modifier
799 12 : if (ctx.event->button() != Qt::LeftButton || !(ctx.event->modifiers() & Qt::AltModifier)) {
800 12 : return false;
801 : }
802 :
803 : // Check if we have a range selection that can be copied
804 0 : const INotationSelectionPtr selection = viewInteraction()->selection();
805 0 : if (!selection->isRange() || !selection->canCopy()) {
806 0 : return false;
807 : }
808 :
809 0 : const INotationSelectionRangePtr range = selection->range();
810 0 : const Fraction sourceTick = range->startTick();
811 0 : const Fraction tickLength = range->endTick() - range->startTick();
812 0 : const engraving::staff_idx_t sourceStaffIdx = range->startStaffIndex();
813 0 : const size_t numStaves = range->endStaffIndex() - range->startStaffIndex();
814 :
815 0 : const bool started = viewInteraction()->startDropRange(sourceTick, tickLength, sourceStaffIdx, numStaves,
816 : /*preserveMeasureAlignment=*/ true);
817 0 : if (!started) {
818 0 : return false;
819 : }
820 :
821 0 : m_mouseDownInfo.dragAction = MouseDownInfo::PasteRangeOnRelease;
822 :
823 0 : const bool canDrop = viewInteraction()->updateDropRange(ctx.logicClickPos);
824 0 : m_view->asItem()->setCursor(canDrop ? Qt::DragCopyCursor : Qt::ForbiddenCursor);
825 :
826 0 : return true;
827 0 : }
828 :
829 12 : void NotationViewInputController::mousePress_considerSelect(const ClickContext& ctx)
830 : {
831 12 : if (!ctx.hitElement) {
832 0 : return;
833 : }
834 :
835 12 : m_hitElementWasAlreadySingleSelected = ctx.hitElement == viewInteraction()->selection()->element();
836 :
837 12 : if (ctx.event->button() == Qt::LeftButton) {
838 10 : if (ctx.event->modifiers() & Qt::ControlModifier) {
839 0 : const std::vector<EngravingItem*> overlappingHitElements = viewInteraction()->hitElements(ctx.logicClickPos, hitWidth());
840 0 : if (overlappingHitElements.size() > 1) {
841 0 : cycleOverlappingHitElements(overlappingHitElements, ctx.hitStaff);
842 0 : return;
843 : }
844 0 : }
845 2 : } else if (ctx.event->button() == Qt::RightButton) {
846 2 : if (ctx.hitElement->selected()) {
847 0 : return;
848 : }
849 : }
850 :
851 12 : SelectType selectType = SelectType::SINGLE;
852 12 : if (ctx.event->modifiers() & Qt::ShiftModifier) {
853 5 : selectType = SelectType::RANGE;
854 7 : } else if (ctx.event->modifiers() & Qt::ControlModifier) {
855 0 : selectType = SelectType::ADD;
856 : } else {
857 7 : const INotationSelectionPtr selection = viewInteraction()->selection();
858 :
859 7 : if (selection->isRange()
860 23 : && (selection->range()->containsItem(ctx.hitElement, ctx.hitStaff)
861 9 : || selection->range()->containsPoint(ctx.logicClickPos))) {
862 2 : m_shouldSelectOnLeftClickRelease = true;
863 2 : return;
864 5 : } else if (ctx.hitElement->selected()) {
865 4 : if (selection->elements().size() > 1) {
866 0 : m_shouldSelectOnLeftClickRelease = true;
867 : }
868 4 : return;
869 : }
870 7 : }
871 :
872 18 : viewInteraction()->select({ ctx.hitElement }, selectType, ctx.hitStaff);
873 : }
874 :
875 12 : void NotationViewInputController::mousePress_seekSelection(const ClickContext& ctx)
876 : {
877 12 : const INotationSelectionPtr selection = viewInteraction()->selection();
878 12 : const std::vector<EngravingItem*>& elements = selection->elements();
879 12 : if (elements.empty()) {
880 0 : return;
881 : }
882 :
883 12 : if (!selection->isRange()) {
884 3 : playbackController()->seekElement(elements.back());
885 3 : return;
886 : }
887 :
888 9 : EngravingItem* elementToSeek = ctx.hitElement;
889 :
890 20 : for (EngravingItem* element: elements) {
891 11 : if (!element || element == elementToSeek) {
892 9 : continue;
893 : }
894 :
895 2 : if (!seekAllowed(element)) {
896 0 : continue;
897 : }
898 :
899 2 : if (elementToSeek && elementToSeek->tick() <= element->tick()) {
900 0 : continue;
901 : }
902 :
903 2 : elementToSeek = element;
904 : }
905 :
906 9 : playbackController()->seekElement(elementToSeek);
907 12 : }
908 :
909 0 : void NotationViewInputController::cycleOverlappingHitElements(const std::vector<EngravingItem*>& hitElements,
910 : staff_idx_t hitStaffIndex)
911 : {
912 0 : const size_t numHitElements = hitElements.size();
913 0 : size_t currTop = numHitElements - 1;
914 0 : EngravingItem* e = hitElements[currTop];
915 0 : std::set<EngravingItem*> selectedAtPosition;
916 0 : bool found = false;
917 :
918 : // e is the topmost element in stacking order,
919 : // but we want to replace it with "first non-measure element after a selected element"
920 : // (if such an element exists)
921 0 : for (size_t i = 0; i <= numHitElements; ++i) {
922 0 : if (found) {
923 0 : e = hitElements[currTop];
924 0 : if (!e->isMeasure()) {
925 0 : break;
926 : }
927 0 : } else if (hitElements[currTop]->selected()) {
928 0 : found = true;
929 0 : selectedAtPosition.emplace(hitElements[currTop]);
930 0 : e = nullptr;
931 : }
932 0 : currTop = (currTop + 1) % numHitElements;
933 : }
934 :
935 0 : if (e && !e->selected()) {
936 0 : for (EngravingItem* selectedElem : selectedAtPosition) {
937 0 : selectedElem->score()->deselect(selectedElem);
938 : }
939 0 : viewInteraction()->select({ e }, SelectType::ADD, hitStaffIndex);
940 : }
941 0 : }
942 :
943 12 : bool NotationViewInputController::mousePress_considerDragOutgoingRange(const ClickContext& ctx)
944 : {
945 12 : if (ctx.event->button() != Qt::LeftButton) {
946 2 : return false;
947 : }
948 :
949 10 : if (!ctx.hitElement || ctx.hitElement->isMeasure()) {
950 5 : const INotationSelectionPtr selection = viewInteraction()->selection();
951 :
952 5 : if (selection->isRange() && selection->range()->containsPoint(ctx.logicClickPos)) {
953 0 : m_mouseDownInfo.dragAction = MouseDownInfo::DragOutgoingRange;
954 0 : return true;
955 : }
956 5 : }
957 :
958 10 : return false;
959 : }
960 :
961 10 : void NotationViewInputController::handleLeftClick(const ClickContext& ctx)
962 : {
963 10 : if (!ctx.hitElement || !ctx.hitElement->selected()) {
964 4 : return;
965 : }
966 :
967 : // If it is the only selected element, start editing if needed
968 18 : if (ctx.hitElement == viewInteraction()->selection()->element()
969 18 : && ctx.hitElement->needStartEditingAfterSelecting()) {
970 0 : viewInteraction()->startEditElement(ctx.hitElement);
971 : }
972 :
973 6 : if (ctx.hitElement->isPlayable()) {
974 9 : playbackController()->playElements({ ctx.hitElement });
975 : }
976 :
977 6 : if (viewInteraction()->isTextSelected()) {
978 1 : updateTextCursorPosition();
979 : }
980 : }
981 :
982 2 : void NotationViewInputController::handleRightClick()
983 : {
984 2 : m_mouseDownInfo.dragAction = MouseDownInfo::Nothing;
985 :
986 : // See also AbstractNotationPaintView::event for context menu event handling
987 2 : }
988 :
989 0 : bool NotationViewInputController::startTextEditingAllowed() const
990 : {
991 0 : INotationInteractionPtr interaction = viewInteraction();
992 0 : return interaction->isTextSelected() && !interaction->isTextEditingStarted();
993 0 : }
994 :
995 1 : void NotationViewInputController::updateTextCursorPosition()
996 : {
997 1 : if (viewInteraction()->isTextEditingStarted()) {
998 1 : viewInteraction()->changeTextCursorPosition(m_mouseDownInfo.logicalBeginPoint);
999 :
1000 : // Show text style popup
1001 1 : const TextBase* item = viewInteraction()->editedText();
1002 1 : if (AbstractElementPopupModel::hasTextStylePopup(item)
1003 1 : && (!item->isLyrics() || !item->empty()) && item->selected()) {
1004 0 : m_view->showElementPopup(item->type());
1005 : }
1006 : }
1007 1 : }
1008 :
1009 0 : bool NotationViewInputController::tryPercussionShortcut(QKeyEvent* event)
1010 : {
1011 0 : INotationNoteInputPtr noteInput = viewInteraction()->noteInput();
1012 0 : const mu::engraving::Drumset* drumset = noteInput ? noteInput->state().drumset() : nullptr;
1013 0 : if (!drumset) {
1014 0 : return false;
1015 : }
1016 :
1017 0 : const QKeyCombination noModifiers(Qt::Key(event->key()));
1018 0 : const int pitchToWrite = drumset->pitchForShortcut(QKeySequence(noModifiers).toString());
1019 0 : if (pitchToWrite < 0) {
1020 0 : return false;
1021 : }
1022 :
1023 0 : const Qt::KeyboardModifiers mods = event->modifiers();
1024 0 : NoteAddingMode addingMode = NoteAddingMode::NextChord;
1025 :
1026 0 : if (mods & Qt::ShiftModifier && (mods & Qt::ControlModifier || mods & Qt::MetaModifier)) {
1027 0 : addingMode = NoteAddingMode::InsertChord;
1028 0 : } else if (mods & Qt::ShiftModifier) {
1029 0 : addingMode = NoteAddingMode::CurrentChord;
1030 0 : } else if (mods & ~Qt::NoModifier) {
1031 : // Not a supported modifier combination...
1032 0 : return false;
1033 : }
1034 :
1035 : muse::rcommand::Params params = {
1036 0 : { "pitch", muse::Val(pitchToWrite) },
1037 0 : Contract<NoteAddingMode>::param(addingMode)
1038 0 : };
1039 :
1040 0 : commandDispatcher()->dispatch(ADD_DRUM_NOTE_COMMAND, params);
1041 :
1042 0 : return true;
1043 0 : }
1044 :
1045 0 : void NotationViewInputController::mouseMoveEvent(QMouseEvent* event)
1046 : {
1047 0 : if (m_mouseDownInfo.dragAction == MouseDownInfo::Nothing) {
1048 0 : return;
1049 : }
1050 :
1051 0 : const QPointF physicalDragDelta = event->pos() - m_mouseDownInfo.physicalBeginPoint;
1052 :
1053 0 : const bool isDragStarted = m_isCanvasDragged
1054 0 : || viewInteraction()->isDragStarted()
1055 0 : || viewInteraction()->isOutgoingDragStarted();
1056 0 : if (!isDragStarted) {
1057 : // only start drag operations after a minimum of movement:
1058 0 : const bool canStartDrag = physicalDragDelta.manhattanLength() > 4;
1059 0 : if (!canStartDrag) {
1060 0 : return;
1061 : }
1062 : }
1063 :
1064 0 : switch (m_mouseDownInfo.dragAction) {
1065 0 : case MouseDownInfo::DragOutgoingElement: {
1066 0 : if (!isDragStarted) {
1067 0 : const EngravingItem* element = viewInteraction()->hitElementContext().element;
1068 0 : const PointF hotSpot = m_mouseDownInfo.logicalBeginPoint - element->canvasBoundingRect().topLeft();
1069 :
1070 0 : viewInteraction()->startOutgoingDragElement(element, m_view->asItem(), hotSpot);
1071 : }
1072 0 : return;
1073 : }
1074 0 : case MouseDownInfo::DragOutgoingRange: {
1075 0 : if (!isDragStarted) {
1076 0 : viewInteraction()->startOutgoingDragRange(m_view->asItem());
1077 : }
1078 0 : return;
1079 : }
1080 0 : case MouseDownInfo::PasteRangeOnRelease: {
1081 0 : const PointF logicPos = m_view->toLogical(event->pos());
1082 :
1083 0 : std::optional<bool> preserveMeasureAlignment;
1084 0 : if (physicalDragDelta.manhattanLength() > 4) {
1085 0 : preserveMeasureAlignment = false;
1086 : }
1087 :
1088 0 : const bool canDrop = viewInteraction()->updateDropRange(logicPos, preserveMeasureAlignment);
1089 0 : m_view->asItem()->setCursor(canDrop ? Qt::DragCopyCursor : Qt::ForbiddenCursor);
1090 0 : return;
1091 : }
1092 0 : case MouseDownInfo::Nothing:
1093 0 : return;
1094 0 : case MouseDownInfo::Other:
1095 0 : break;
1096 : }
1097 :
1098 0 : const Qt::KeyboardModifiers keyState = event->modifiers();
1099 :
1100 0 : m_view->hideContextMenu();
1101 0 : if (!viewInteraction()->isTextEditingStarted()) {
1102 0 : m_view->hideElementPopup();
1103 : }
1104 :
1105 0 : const PointF logicPos = m_view->toLogical(event->pos());
1106 :
1107 0 : const bool isNoteEnterMode = m_view->isNoteEnterMode();
1108 0 : const bool isMiddleButton = (event->buttons() & Qt::MiddleButton);
1109 0 : const bool isDragObjectsAllowed = !(readonly() || isNoteEnterMode || playbackController()->isPlaying() || isMiddleButton);
1110 0 : if (isDragObjectsAllowed) {
1111 0 : const EngravingItem* hitElement = hitElementContext().element;
1112 :
1113 : // drag element
1114 0 : if ((hitElement && (hitElement->isMovable() || viewInteraction()->isEditingElement()))
1115 0 : || viewInteraction()->isGripEditStarted()) {
1116 0 : if (hitElement && !viewInteraction()->isDragStarted()) {
1117 0 : startDragElements(hitElement->type(), hitElement->offset());
1118 : }
1119 :
1120 0 : if (viewInteraction()->isGripEditStarted() && !viewInteraction()->isDragStarted()) {
1121 0 : const EngravingItem* selectedElement = viewInteraction()->selection()->element();
1122 0 : ElementType type = selectedElement ? selectedElement->type() : ElementType::INVALID;
1123 0 : startDragElements(type, selectedElement->offset());
1124 : }
1125 :
1126 0 : DragMode mode = DragMode::BothXY;
1127 0 : if (keyState & Qt::ShiftModifier) {
1128 0 : mode = DragMode::OnlyY;
1129 0 : } else if (keyState & Qt::ControlModifier) {
1130 0 : mode = DragMode::OnlyX;
1131 : }
1132 :
1133 0 : const std::vector<int> oldPitches = pitchesBeingDragged();
1134 0 : viewInteraction()->drag(m_mouseDownInfo.logicalBeginPoint, logicPos, mode);
1135 :
1136 0 : if (!oldPitches.empty() && oldPitches != pitchesBeingDragged()) {
1137 0 : playbackController()->playElements(m_notesBeingDragged);
1138 : }
1139 :
1140 0 : return;
1141 0 : } else if (hitElement == nullptr && (keyState & Qt::ShiftModifier)) {
1142 0 : if (!viewInteraction()->isDragStarted()) {
1143 0 : viewInteraction()->startDrag(std::vector<EngravingItem*>(), PointF(), [](const EngravingItem*) { return false; });
1144 : }
1145 0 : viewInteraction()->drag(m_mouseDownInfo.logicalBeginPoint, logicPos, DragMode::BothXY);
1146 :
1147 0 : return;
1148 : }
1149 : }
1150 :
1151 : // move canvas
1152 0 : if (!isNoteEnterMode || isMiddleButton) {
1153 0 : const PointF logicalDragDelta = logicPos - m_mouseDownInfo.logicalBeginPoint;
1154 0 : m_view->moveCanvas(logicalDragDelta.x(), logicalDragDelta.y());
1155 :
1156 0 : m_isCanvasDragged = true;
1157 : }
1158 : }
1159 :
1160 0 : void NotationViewInputController::startDragElements(ElementType elementsType, const PointF& elementsOffset)
1161 : {
1162 0 : if (elementsType == ElementType::INVALID) {
1163 0 : return;
1164 : }
1165 :
1166 0 : const std::vector<EngravingItem*>& elements = viewInteraction()->selection()->elements();
1167 0 : if (elements.empty()) {
1168 0 : return;
1169 : }
1170 :
1171 0 : bool isFilterType = viewInteraction()->selection()->isRange();
1172 0 : auto isDraggable = [isFilterType, elementsType](const EngravingItem* element) {
1173 0 : return element && element->selected() && (!isFilterType || elementsType == element->type());
1174 0 : };
1175 :
1176 0 : viewInteraction()->startDrag(elements, elementsOffset, isDraggable);
1177 :
1178 0 : if (!viewInteraction()->isDragStarted()) {
1179 0 : return;
1180 : }
1181 :
1182 0 : for (const EngravingItem* item : elements) {
1183 0 : if (item->isNote()) {
1184 0 : m_notesBeingDragged.push_back(item);
1185 : }
1186 : }
1187 : }
1188 :
1189 0 : void NotationViewInputController::mouseReleaseEvent(QMouseEvent* event)
1190 : {
1191 0 : if (m_mouseDownInfo.dragAction == MouseDownInfo::Nothing) {
1192 0 : return;
1193 : }
1194 :
1195 0 : INotationInteractionPtr interaction = viewInteraction();
1196 0 : INotationNoteInputPtr noteInput = interaction->noteInput();
1197 0 : const EngravingItem* hitElement = hitElementContext().element;
1198 :
1199 0 : DEFER {
1200 0 : m_mouseDownInfo.dragAction = MouseDownInfo::Nothing;
1201 0 : m_view->asItem()->setCursor(QCursor());
1202 0 : };
1203 :
1204 0 : if (m_mouseDownInfo.dragAction == MouseDownInfo::PasteRangeOnRelease) {
1205 0 : INotationSelectionPtr selection = interaction->selection();
1206 0 : if (!selection->isRange() || !selection->canCopy()) {
1207 0 : viewInteraction()->endDrop();
1208 0 : return;
1209 : }
1210 0 : muse::ByteArray rangeData = selection->mimeData();
1211 0 : if (rangeData.empty()) {
1212 0 : viewInteraction()->endDrop();
1213 0 : return;
1214 : }
1215 0 : PointF logicPos = m_view->toLogical(event->pos());
1216 0 : viewInteraction()->dropRange(rangeData.toQByteArrayNoCopy(), logicPos, false);
1217 0 : return;
1218 0 : }
1219 :
1220 0 : if (event->modifiers() != Qt::ControlModifier && event->modifiers() != Qt::ShiftModifier) {
1221 0 : if (!hitElement && !m_isCanvasDragged && !interaction->isGripEditStarted()
1222 0 : && !interaction->isDragStarted() && !noteInput->isNoteInputMode()) {
1223 0 : interaction->clearSelection();
1224 : }
1225 : }
1226 :
1227 0 : if (event->button() == Qt::LeftButton && event->modifiers() == Qt::NoModifier) {
1228 0 : handleLeftClickRelease(event->pos());
1229 : }
1230 :
1231 0 : m_isCanvasDragged = false;
1232 :
1233 0 : if (interaction->isDragStarted()) {
1234 : bool isDraggingHairpinSegmentStartEndGrip
1235 0 : = interaction->isGripEditStarted()
1236 0 : && interaction->selection()->element()
1237 0 : && interaction->selection()->element()->isHairpinSegment()
1238 0 : && interaction->isStartEndGripSelected();
1239 :
1240 0 : interaction->endDrag();
1241 :
1242 : // When dragging start or end of hairpin to a note or rest, open dynamic popup
1243 : // Check for note or rest happens in Score::addText which is called through addTextToItem in toggleDynamicPopup
1244 0 : if (isDraggingHairpinSegmentStartEndGrip) {
1245 0 : interaction->toggleDynamicPopup();
1246 : }
1247 : }
1248 :
1249 0 : if (interaction->isOutgoingDragStarted()) {
1250 0 : interaction->endOutgoingDrag();
1251 : }
1252 :
1253 0 : m_notesBeingDragged.clear();
1254 0 : }
1255 :
1256 0 : void NotationViewInputController::handleLeftClickRelease(const QPointF& releasePoint)
1257 : {
1258 0 : if (m_view->isNoteEnterMode() || playbackController()->isPlaying()) {
1259 0 : return;
1260 : }
1261 :
1262 0 : if (m_shouldStartEditOnLeftClickRelease) {
1263 0 : muse::rcommand::CommandQuery query(SCREEN_EDIT_ELEMENT_COMMAND);
1264 0 : query.set("pos_x", muse::Val(m_mouseDownInfo.logicalBeginPoint.x()));
1265 0 : query.set("pos_y", muse::Val(m_mouseDownInfo.logicalBeginPoint.y()));
1266 0 : commandDispatcher()->dispatch(query);
1267 0 : m_shouldStartEditOnLeftClickRelease = false;
1268 0 : return;
1269 0 : }
1270 :
1271 0 : const INotationInteraction::HitElementContext& ctx = hitElementContext();
1272 0 : if (!ctx.element) {
1273 0 : return;
1274 : }
1275 :
1276 0 : if (releasePoint != m_mouseDownInfo.physicalBeginPoint) {
1277 0 : return;
1278 : }
1279 :
1280 0 : if (m_shouldSelectOnLeftClickRelease) {
1281 0 : m_shouldSelectOnLeftClickRelease = false;
1282 0 : engraving::staff_idx_t staffIndex = ctx.staff ? ctx.staff->idx() : muse::nidx;
1283 :
1284 0 : INotationInteractionPtr interaction = viewInteraction();
1285 0 : interaction->select({ ctx.element }, SelectType::SINGLE, staffIndex);
1286 :
1287 0 : if (ctx.element->needStartEditingAfterSelecting()) {
1288 0 : viewInteraction()->startEditElement(ctx.element);
1289 0 : return;
1290 : }
1291 0 : }
1292 :
1293 0 : if (!m_hitElementWasAlreadySingleSelected) {
1294 0 : return;
1295 : }
1296 :
1297 : // Same element clicked again while it was already selected.
1298 : // Either toggle popup or start text editing.
1299 :
1300 0 : if (viewInteraction()->textEditingAllowed(ctx.element)) {
1301 0 : if (!viewInteraction()->isTextEditingStarted()) {
1302 0 : viewInteraction()->startEditText(ctx.element, m_mouseDownInfo.logicalBeginPoint);
1303 : }
1304 : } else {
1305 0 : togglePopupForItemIfSupports(ctx.element);
1306 : }
1307 : }
1308 :
1309 0 : void NotationViewInputController::mouseDoubleClickEvent(QMouseEvent* event)
1310 : {
1311 0 : if (m_view->isNoteEnterMode()) {
1312 0 : return;
1313 : }
1314 :
1315 : // Can't pass `this` as the second argument, because not QObject. But `m_view` works as well.
1316 0 : QTimer::singleShot(QApplication::doubleClickInterval(), m_view->asItem(), [this]() {
1317 0 : m_tripleClickPending = false;
1318 0 : });
1319 :
1320 0 : m_tripleClickPending = true;
1321 :
1322 0 : if (viewInteraction()->isTextEditingStarted()) {
1323 0 : viewInteraction()->selectText(mu::engraving::SelectTextType::Word);
1324 0 : return;
1325 : }
1326 :
1327 0 : EngravingItem* hitElement = hitElementContext().element;
1328 0 : if (!hitElement) {
1329 0 : return;
1330 : }
1331 :
1332 0 : if (viewInteraction()->textEditingAllowed(hitElement) && !playbackController()->isPlaying()) {
1333 0 : viewInteraction()->startEditText(hitElement, m_mouseDownInfo.logicalBeginPoint);
1334 0 : return;
1335 : }
1336 :
1337 0 : const Qt::KeyboardModifiers modifiers = event->modifiers();
1338 :
1339 0 : std::optional<SelectType> selectType;
1340 0 : switch (modifiers) {
1341 0 : case Qt::NoModifier: {
1342 0 : selectType = SelectType::REPLACE;
1343 0 : break;
1344 : }
1345 0 : case Qt::ControlModifier: {
1346 0 : selectType = SelectType::ADD;
1347 0 : break;
1348 : }
1349 0 : default: {
1350 0 : selectType = std::nullopt;
1351 0 : break;
1352 : }
1353 : }
1354 :
1355 0 : switch (hitElement->type()) {
1356 0 : case ElementType::INSTRUMENT_NAME: {
1357 0 : if (modifiers != Qt::NoModifier) {
1358 0 : break; // Doesn't support modifiers...
1359 : }
1360 0 : m_shouldStartEditOnLeftClickRelease = true;
1361 0 : break;
1362 : }
1363 0 : case ElementType::MEASURE: {
1364 0 : if (modifiers != Qt::NoModifier) {
1365 0 : break; // Doesn't support modifiers...
1366 : }
1367 0 : commandDispatcher()->dispatch(TOGGLE_NOTE_INPUT_COMMAND);
1368 0 : break;
1369 : }
1370 0 : case ElementType::NOTE: {
1371 0 : if (!selectType.has_value()) {
1372 0 : break; // Unsupported modifier(s)...
1373 : }
1374 0 : const Chord* chord = toNote(hitElement)->chord();
1375 0 : IF_ASSERT_FAILED(chord) {
1376 0 : break;
1377 : }
1378 0 : const std::vector<Note*>& notes = chord->notes();
1379 0 : if (selectType.value() == SelectType::ADD && std::all_of(notes.begin(), notes.end(), [](Note* n){ return n->selected(); })) {
1380 0 : for (Note* n : notes) {
1381 0 : n->score()->deselect(n);
1382 : }
1383 : } else {
1384 0 : viewInteraction()->select({ notes.begin(), notes.end() }, selectType.value());
1385 : }
1386 0 : break;
1387 : }
1388 0 : case ElementType::FRET_DIAGRAM: {
1389 0 : if (!selectType.has_value()) {
1390 0 : return; // Unsupported modifier(s)...
1391 : }
1392 0 : if (Harmony* harmony = toFretDiagram(hitElement)->harmony()) {
1393 0 : viewInteraction()->select({ hitElement, harmony }, selectType.value());
1394 : }
1395 0 : break;
1396 : }
1397 0 : default: break;
1398 : }
1399 : }
1400 :
1401 0 : void NotationViewInputController::hoverMoveEvent(QHoverEvent* event)
1402 : {
1403 0 : if (!m_view->isNoteEnterMode()) {
1404 0 : return;
1405 : }
1406 :
1407 0 : const PointF oldPos = m_view->toLogical(event->oldPosF());
1408 0 : PointF pos = m_view->toLogical(event->position());
1409 :
1410 0 : const ShadowNote* shadowNote = viewInteraction()->shadowNote();
1411 0 : if (shadowNote && m_view->elementPopupIsOpen(shadowNote->type())) {
1412 : // Lock the X position to the shadow note X (prevents the popup from jumping horizontally to other input positions)
1413 0 : pos.setX(shadowNote->canvasX());
1414 : }
1415 :
1416 0 : if (oldPos == pos) {
1417 0 : return;
1418 : }
1419 :
1420 0 : viewInteraction()->showShadowNoteForPosition(pos);
1421 :
1422 0 : if (event->modifiers() == Qt::ShiftModifier) {
1423 0 : updateShadowNotePopupVisibility();
1424 : }
1425 : }
1426 :
1427 0 : void NotationViewInputController::hoverLeaveEvent(QHoverEvent*)
1428 : {
1429 0 : viewInteraction()->hideShadowNote();
1430 0 : }
1431 :
1432 0 : bool NotationViewInputController::isAnchorEditingEvent(QKeyEvent* event) const
1433 : {
1434 0 : bool anchorEditingKeyCombo = (event->key() == Qt::Key_Left || event->key() == Qt::Key_Right)
1435 0 : && event->modifiers() & Qt::ShiftModifier;
1436 0 : EngravingItem* selectedItem = viewInteraction()->selection()->element();
1437 0 : return selectedItem && selectedItem->allowTimeAnchor() && anchorEditingKeyCombo;
1438 : }
1439 :
1440 0 : bool NotationViewInputController::shortcutOverrideEvent(QKeyEvent* event)
1441 : {
1442 0 : auto key = event->key();
1443 :
1444 0 : const bool editTextKeysFound = key == Qt::Key_Return || key == Qt::Key_Enter;
1445 0 : if (editTextKeysFound && startTextEditingAllowed()) {
1446 0 : return true;
1447 : }
1448 :
1449 0 : if (key == Qt::Key_Escape && m_mouseDownInfo.dragAction == MouseDownInfo::PasteRangeOnRelease) {
1450 0 : return true;
1451 : }
1452 :
1453 0 : if (viewInteraction()->isEditingElement()) {
1454 0 : return viewInteraction()->isEditAllowed(event);
1455 : }
1456 :
1457 0 : if (isAnchorEditingEvent(event)) {
1458 0 : return true;
1459 : }
1460 :
1461 0 : return tryPercussionShortcut(event);
1462 : }
1463 :
1464 0 : void NotationViewInputController::keyPressEvent(QKeyEvent* event)
1465 : {
1466 0 : auto key = event->key();
1467 :
1468 0 : if (startTextEditingAllowed() && (event->key() == Qt::Key_Return || event->key() == Qt::Key_Enter)) {
1469 0 : commandDispatcher()->dispatch(SCREEN_EDIT_TEXT_COMMAND);
1470 0 : event->accept();
1471 0 : } else if (event->key() == Qt::Key_Escape && m_mouseDownInfo.dragAction == MouseDownInfo::PasteRangeOnRelease) {
1472 : // Cancel "Alt+click to paste range"
1473 0 : viewInteraction()->endDrop();
1474 0 : m_mouseDownInfo.dragAction = MouseDownInfo::Nothing;
1475 0 : m_view->asItem()->setCursor({});
1476 0 : event->accept();
1477 0 : } else if (viewInteraction()->isEditingElement()) {
1478 0 : viewInteraction()->editElement(event);
1479 0 : if (key == Qt::Key_Shift) {
1480 0 : viewInteraction()->updateTimeTickAnchors(event);
1481 : }
1482 0 : } else if (key == Qt::Key_Shift) {
1483 0 : viewInteraction()->updateTimeTickAnchors(event);
1484 0 : updateShadowNotePopupVisibility();
1485 0 : return;
1486 0 : } else if (isAnchorEditingEvent(event)) {
1487 0 : viewInteraction()->moveElementAnchors(event);
1488 : }
1489 :
1490 0 : updateShadowNotePopupVisibility(/*forceHide*/ true);
1491 : }
1492 :
1493 0 : void NotationViewInputController::keyReleaseEvent(QKeyEvent* event)
1494 : {
1495 0 : if (event->key() != Qt::Key_Shift) {
1496 0 : return;
1497 : }
1498 :
1499 0 : viewInteraction()->editElement(event);
1500 0 : updateShadowNotePopupVisibility(/*forceHide*/ true);
1501 0 : viewInteraction()->updateTimeTickAnchors(event);
1502 : }
1503 :
1504 0 : void NotationViewInputController::inputMethodEvent(QInputMethodEvent* event)
1505 : {
1506 0 : if (viewInteraction()->isTextEditingStarted()) {
1507 0 : viewInteraction()->editText(event);
1508 : }
1509 0 : }
1510 :
1511 0 : bool NotationViewInputController::canHandleInputMethodQuery(Qt::InputMethodQuery query) const
1512 : {
1513 0 : if (!viewInteraction()->isTextEditingStarted()) {
1514 0 : return false;
1515 : }
1516 :
1517 : static const QList<Qt::InputMethodQuery> allowedQueries {
1518 : Qt::ImCursorRectangle,
1519 : Qt::ImEnabled,
1520 : Qt::ImHints
1521 0 : };
1522 :
1523 0 : return allowedQueries.contains(query);
1524 : }
1525 :
1526 : //! NOTE: Copied from ScoreView::inputMethodQuery
1527 0 : QVariant NotationViewInputController::inputMethodQuery(Qt::InputMethodQuery query) const
1528 : {
1529 0 : if (!viewInteraction()->isTextEditingStarted()) {
1530 0 : return QVariant();
1531 : }
1532 :
1533 0 : switch (query) {
1534 0 : case Qt::ImCursorRectangle: {
1535 0 : const TextBase* editedText = viewInteraction()->editedText();
1536 0 : RectF cursorRect = editedText->cursor()->cursorRect().translated(editedText->canvasPos());
1537 :
1538 0 : QRectF rect = m_view->fromLogical(cursorRect).toQRectF();
1539 0 : rect.setWidth(1); // InputMethod doesn't display properly if width left at 0
1540 0 : rect.setHeight(rect.height() + 10); // add a little margin under the cursor
1541 :
1542 0 : return rect;
1543 : }
1544 0 : case Qt::ImEnabled:
1545 0 : return true; // TextBase will always accept input method input
1546 0 : case Qt::ImHints:
1547 0 : return Qt::ImhNone; // No hints for now, but maybe in future will give hints
1548 0 : default:
1549 0 : break;
1550 : }
1551 :
1552 0 : return QVariant();
1553 : }
1554 :
1555 0 : void NotationViewInputController::dragEnterEvent(QDragEnterEvent* event)
1556 : {
1557 0 : if (m_readonly) {
1558 0 : event->ignore();
1559 0 : return;
1560 : }
1561 :
1562 0 : const QMimeData* mimeData = dragController()->mimeData(event);
1563 0 : IF_ASSERT_FAILED(mimeData) {
1564 0 : return;
1565 : }
1566 :
1567 0 : if (mimeData->hasFormat(mimeSymbolFormat)) {
1568 0 : event->setDropAction(Qt::CopyAction);
1569 0 : if (event->dropAction() != Qt::CopyAction) {
1570 0 : event->ignore();
1571 0 : return;
1572 : }
1573 :
1574 0 : QByteArray edata = mimeData->data(mimeSymbolFormat);
1575 0 : if (viewInteraction()->startDropSingle(edata)) {
1576 0 : event->accept();
1577 0 : return;
1578 : }
1579 :
1580 0 : event->ignore();
1581 0 : return;
1582 0 : }
1583 :
1584 0 : if (mimeData->hasFormat(mimeStaffListFormat)) {
1585 0 : bool isInternal = event->source() == m_view->asItem();
1586 0 : if (!isInternal || event->modifiers() & Qt::AltModifier) {
1587 0 : event->setDropAction(Qt::CopyAction);
1588 0 : if (event->dropAction() != Qt::CopyAction) {
1589 0 : event->ignore();
1590 0 : return;
1591 : }
1592 : } else {
1593 0 : event->setDropAction(Qt::MoveAction);
1594 0 : if (event->dropAction() != Qt::MoveAction) {
1595 0 : event->ignore();
1596 0 : return;
1597 : }
1598 : }
1599 :
1600 0 : QByteArray edata = mimeData->data(mimeStaffListFormat);
1601 0 : if (viewInteraction()->startDropRange(edata)) {
1602 0 : event->accept();
1603 0 : return;
1604 : }
1605 :
1606 0 : event->ignore();
1607 0 : return;
1608 0 : }
1609 :
1610 0 : for (const QUrl& url : mimeData->urls()) {
1611 0 : if (viewInteraction()->startDropImage(url)) {
1612 0 : event->accept();
1613 0 : return;
1614 : }
1615 0 : }
1616 :
1617 0 : event->ignore();
1618 : }
1619 :
1620 0 : void NotationViewInputController::dragMoveEvent(QDragMoveEvent* event)
1621 : {
1622 0 : const QMimeData* mimeData = dragController()->mimeData(event);
1623 0 : IF_ASSERT_FAILED(mimeData) {
1624 0 : return;
1625 : }
1626 :
1627 0 : PointF pos = m_view->toLogical(event->position());
1628 0 : Qt::KeyboardModifiers modifiers = event->modifiers();
1629 :
1630 0 : bool isAccepted = false;
1631 0 : if (mimeData->hasFormat(mimeStaffListFormat)) {
1632 0 : bool isInternal = event->source() == m_view->asItem();
1633 0 : if (!isInternal || modifiers & Qt::AltModifier) {
1634 0 : event->setDropAction(Qt::CopyAction);
1635 : } else {
1636 0 : event->setDropAction(Qt::MoveAction);
1637 : }
1638 :
1639 0 : isAccepted = viewInteraction()->updateDropRange(pos);
1640 : } else {
1641 0 : event->setDropAction(Qt::CopyAction);
1642 :
1643 0 : isAccepted = viewInteraction()->updateDropSingle(pos, modifiers);
1644 : }
1645 :
1646 0 : event->setAccepted(isAccepted);
1647 :
1648 0 : if (!uiConfiguration()->isSystemDragSupported()) {
1649 0 : m_lastDragMoveEvent = {
1650 0 : event->position(),
1651 0 : event->modifiers(),
1652 0 : event->dropAction(),
1653 0 : event->source()
1654 : };
1655 : }
1656 : }
1657 :
1658 0 : void NotationViewInputController::dragLeaveEvent(QDragLeaveEvent*)
1659 : {
1660 0 : if (!uiConfiguration()->isSystemDragSupported()) {
1661 0 : dropEvent(m_lastDragMoveEvent);
1662 : }
1663 :
1664 0 : viewInteraction()->endDrop();
1665 0 : m_mouseDownInfo.dragAction = MouseDownInfo::Nothing;
1666 0 : }
1667 :
1668 0 : void NotationViewInputController::dropEvent(QDropEvent* event)
1669 : {
1670 0 : if (!uiConfiguration()->isSystemDragSupported()) {
1671 0 : event->setAccepted(false);
1672 0 : return;
1673 : }
1674 :
1675 : DragMoveEvent de = {
1676 0 : event->position(),
1677 0 : event->modifiers(),
1678 0 : event->dropAction(),
1679 0 : event->source()
1680 0 : };
1681 :
1682 0 : bool isAccepted = dropEvent(de, event->mimeData());
1683 0 : event->setAccepted(isAccepted);
1684 : }
1685 :
1686 0 : bool NotationViewInputController::dropEvent(const DragMoveEvent& event, const QMimeData* mimeData)
1687 : {
1688 0 : if (m_readonly) {
1689 0 : return false;
1690 : }
1691 :
1692 0 : if (!mimeData) {
1693 0 : mimeData = dragController()->mimeData();
1694 : }
1695 :
1696 0 : IF_ASSERT_FAILED(mimeData) {
1697 0 : return false;
1698 : }
1699 :
1700 0 : PointF pos = m_view->toLogical(event.position);
1701 0 : Qt::KeyboardModifiers modifiers = event.modifiers;
1702 :
1703 0 : bool isAccepted = false;
1704 0 : if (mimeData->hasFormat(mimeStaffListFormat)) {
1705 0 : bool isInternal = event.source == m_view->asItem();
1706 0 : bool isMove = isInternal && event.dropAction == Qt::MoveAction;
1707 0 : isAccepted = viewInteraction()->dropRange(mimeData->data(mimeStaffListFormat), pos, isMove);
1708 : } else {
1709 0 : isAccepted = viewInteraction()->dropSingle(pos, modifiers);
1710 : }
1711 :
1712 0 : return isAccepted;
1713 : }
1714 :
1715 0 : std::vector<int> NotationViewInputController::pitchesBeingDragged() const
1716 : {
1717 0 : std::vector<int> pitches;
1718 0 : pitches.reserve(m_notesBeingDragged.size());
1719 :
1720 0 : for (const EngravingItem* item : m_notesBeingDragged) {
1721 0 : pitches.push_back(toNote(item)->pitch());
1722 : }
1723 :
1724 0 : return pitches;
1725 0 : }
1726 :
1727 12 : float NotationViewInputController::hitWidth() const
1728 : {
1729 12 : return configuration()->selectionProximity() * 0.5 / m_view->currentScaling();
1730 : }
1731 :
1732 0 : ElementType NotationViewInputController::selectionType() const
1733 : {
1734 0 : ElementType type = ElementType::INVALID;
1735 :
1736 0 : auto selection = viewInteraction()->selection();
1737 0 : if (!selection) {
1738 0 : return type;
1739 : }
1740 :
1741 0 : if (auto hitElement = hitElementContext().element) {
1742 0 : return hitElement->type();
1743 0 : } else if (selection->isRange()) {
1744 0 : return ElementType::MEASURE;
1745 0 : } else if (auto selectedElement = selection->element()) {
1746 0 : return selectedElement->type();
1747 : }
1748 :
1749 0 : return ElementType::PAGE;
1750 0 : }
1751 :
1752 0 : muse::PointF NotationViewInputController::selectionElementPos() const
1753 : {
1754 0 : auto selection = viewInteraction()->selection();
1755 0 : if (!selection) {
1756 0 : return muse::PointF();
1757 : }
1758 :
1759 0 : if (auto hitElement = hitElementContext().element) {
1760 0 : return hitElement->canvasBoundingRect().center();
1761 0 : } else if (selection->isRange()) {
1762 0 : auto range = selection->range();
1763 0 : return range->boundingArea().front().center();
1764 0 : } else if (auto selectedElement = selection->element()) {
1765 0 : return selectedElement->canvasBoundingRect().center();
1766 : }
1767 :
1768 0 : return muse::PointF();
1769 0 : }
1770 :
1771 0 : void NotationViewInputController::togglePopupForItemIfSupports(const EngravingItem* item)
1772 : {
1773 0 : if (!item) {
1774 0 : return;
1775 : }
1776 :
1777 0 : ElementType type = item->type();
1778 :
1779 0 : if (AbstractElementPopupModel::hasElementEditPopup(item)) {
1780 0 : m_view->toggleElementPopup(type);
1781 : }
1782 : }
1783 :
1784 0 : void NotationViewInputController::showSearch()
1785 : {
1786 0 : m_view->showSearch();
1787 0 : }
1788 :
1789 0 : void NotationViewInputController::updateShadowNotePopupVisibility(bool forceHide)
1790 : {
1791 0 : const mu::engraving::ShadowNote* shadowNote = viewInteraction()->shadowNote();
1792 0 : if (forceHide || !shadowNote || !AbstractElementPopupModel::hasElementEditPopup(shadowNote)) {
1793 0 : m_view->hideElementPopup(ElementType::SHADOW_NOTE);
1794 0 : return;
1795 : }
1796 :
1797 0 : m_view->showElementPopup(ElementType::SHADOW_NOTE);
1798 : }
1799 :
1800 0 : void NotationViewInputController::redrawView()
1801 : {
1802 0 : m_view->scheduleRedraw();
1803 0 : }
|