From f0d338bd4bb9b81319d197fb1bf069d92030478a Mon Sep 17 00:00:00 2001 From: Bonni Date: Wed, 21 Oct 2026 19:19:23 -0300 Subject: [PATCH] Touch: a finger held still opens what a right-click opens (#474) Android reports every press as the left button, a mouse's too, so a right-click never arrived. On a phone or tablet a long press on the drawn console or a generated panel opens the menu; a stop changes as the finger lifts, so a held one stays as it was. MASTERPIECE_TOUCH=1 turns it on with a mouse on a desktop. --- src/mp_ui/Console.cpp | 136 ++++++++++++++++++++++++----------- src/mp_ui/Console.h | 19 +++++ src/mp_ui/Mobile.cpp | 6 ++ src/mp_ui/Mobile.h | 47 ++++++++++++ src/mp_ui/Panels.cpp | 73 ++++++++++++++----- src/mp_ui/Panels.h | 6 ++ tests/test_accessibility.cpp | 102 ++++++++++++++++++++++++++ 7 files changed, 328 insertions(+), 61 deletions(-) diff --git a/src/mp_ui/Console.cpp b/src/mp_ui/Console.cpp index 00d0d4c7..95db673e 100644 --- a/src/mp_ui/Console.cpp +++ b/src/mp_ui/Console.cpp @@ -15,6 +15,7 @@ #include #include #include +#include namespace mp::ui { namespace { @@ -1145,28 +1146,32 @@ void ConsoleView::paint(juce::Graphics& g) { void ConsoleView::mouseDown(const juce::MouseEvent& e) { if (frozen_.isValid()) return; + // A second finger ends the first one's wait: Set held while a piston is + // pressed is two presses, and neither is a long press. + longPress_.end(); + tappedStop_ = 0; + if (e.mods.isPopupMenu()) { + openMenuAt(e.getPosition(), true); + return; + } // A drawn manual is playable: clicking a key sounds it. The click goes // through MidiKeyboardState, which is the same path a physical console - // takes, so stops and couplers apply to it identically. - // Right-click a manual: its MIDI window, the way GrandOrgue opens one -- - // which keyboard plays it, and the pistons that bring it to a keyboard. - if (e.mods.isPopupMenu()) { - for (auto it = keys_.rbegin(); it != keys_.rend(); ++it) - if (it->bounds.contains(e.getPosition()) && it->keyboardId != 0) { - ManualDialog::show(proc_, it->keyboardId); - return; - } + // takes, so stops and couplers apply to it identically. A key held is a + // note held, on a touch screen too. + int channel = 1; + const int note = keyAt(e.getPosition(), &channel); + if (note >= 0) { + heldKey_ = note; + heldChannel_ = channel; + proc_.keyboardState().noteOn(channel, note, 0.8f); + repaint(keysBounds()); + return; } - if (!e.mods.isPopupMenu()) { - int channel = 1; - const int note = keyAt(e.getPosition(), &channel); - if (note >= 0) { - heldKey_ = note; - heldChannel_ = channel; - proc_.keyboardState().noteOn(channel, note, 0.8f); - repaint(keysBounds()); - return; - } + + // Touch: a finger held still opens the menu. + if (touch_) { + if (!longPress_.onHeld) longPress_.onHeld = [this] { onLongPress(); }; + longPress_.begin(e.getPosition()); } // Topmost first: the controls are painted last, so they are hit first. @@ -1175,17 +1180,6 @@ void ConsoleView::mouseDown(const juce::MouseEvent& e) { if (it->switchId == 0 && it->controlId == 0) continue; if (!it->hitBounds.contains(e.getPosition())) continue; - if (e.mods.isPopupMenu()) { - if (it->switchId != 0) { - showMidiMenu(it->switchId, it->bounds); - } else { - const auto bounds = it->bounds; - showControlMidiMenu(proc_, it->controlId, localAreaToGlobal(bounds), - [this, bounds] { repaint(bounds); }); - } - return; - } - // A shoe or slider. The press only takes hold of it — the value does not // move until the mouse does, which is what "click it, hold, and drag" // means and what stops a stray click from throwing a setting across its @@ -1200,13 +1194,6 @@ void ConsoleView::mouseDown(const juce::MouseEvent& e) { return; } - // A stop with no pipes in this set is not drawn from the console: the - // click is refused, and its tooltip says it is not available. Nothing else - // draws it either (SwitchNetwork::holdOff); one shown drawn can be pushed in. - if (const auto* why = proc_.unavailableFor(it->switchId); - why != nullptr && why->noPipes && !proc_.shownSwitchEngaged(it->switchId)) - return; - // A button the organ marks momentary -- Load, Save, Reset, a piston -- // is pressed while the mouse is down and let go with it (#53). It used to // toggle like a drawstop and stay in, looking pressed until clicked again. @@ -1215,26 +1202,81 @@ void ConsoleView::mouseDown(const juce::MouseEvent& e) { const auto sw = proc_.organModel().switches.find(it->switchId); const bool momentary = sw != proc_.organModel().switches.end() && !sw->second.latching; if (momentary) { + if (const auto* why = proc_.unavailableFor(it->switchId); + why != nullptr && why->noPipes && !proc_.shownSwitchEngaged(it->switchId)) + return; heldButton_ = it->switchId; heldButtonBounds_ = it->bounds; proc_.setSwitchEngaged(it->switchId, true); + repaint(it->bounds); + } else if (touch_) { + // Drawn as the finger lifts, so a finger held on it opens its menu + // and leaves it as it was. + tappedStop_ = it->switchId; + tappedStopBounds_ = it->bounds.getUnion(it->hitBounds); } else { - proc_.setSwitchEngaged(it->switchId, !proc_.shownSwitchEngaged(it->switchId)); + toggleStop(it->switchId, it->bounds); } - repaint(it->bounds); return; } +} + +void ConsoleView::toggleStop(Id switchId, juce::Rectangle bounds) { + // A stop with no pipes in this set is not drawn from the console: the + // click is refused, and its tooltip says it is not available. Nothing else + // draws it either (SwitchNetwork::holdOff); one shown drawn can be pushed in. + if (const auto* why = proc_.unavailableFor(switchId); + why != nullptr && why->noPipes && !proc_.shownSwitchEngaged(switchId)) + return; + proc_.setSwitchEngaged(switchId, !proc_.shownSwitchEngaged(switchId)); + repaint(bounds); +} + +void ConsoleView::onLongPress() { + // The finger stays where it pressed: what it holds is let go, a stop is + // left as it was, and the menu opens. + tappedStop_ = 0; + heldControl_ = 0; + if (heldButton_ != 0) { + proc_.setSwitchEngaged(heldButton_, false); + repaint(heldButtonBounds_); + heldButton_ = 0; + } + openMenuAt(longPress_.where(), false); +} +void ConsoleView::openMenuAt(juce::Point p, bool keys) { + // Right-click a manual: its MIDI window, the way GrandOrgue opens one -- + // which keyboard plays it, and the pistons that bring it to a keyboard. + if (keys) + for (auto it = keys_.rbegin(); it != keys_.rend(); ++it) + if (it->bounds.contains(p) && it->keyboardId != 0) { + ManualDialog::show(proc_, it->keyboardId); + return; + } + for (auto it = items_.rbegin(); it != items_.rend(); ++it) { + if (!it->clickable) continue; + if (it->switchId == 0 && it->controlId == 0) continue; + if (!it->hitBounds.contains(p)) continue; + if (it->switchId != 0) { + showMidiMenu(it->switchId, it->bounds); + } else { + const auto bounds = it->bounds; + showControlMidiMenu(proc_, it->controlId, localAreaToGlobal(bounds), + [this, bounds] { repaint(bounds); }); + } + return; + } // A right-click on nothing cancels an armed learn, so an accidental arm is // easy to back out of. - if (e.mods.isPopupMenu() && proc_.midiMap().learning()) { + if (proc_.midiMap().learning()) { proc_.midiMap().cancelLearn(); repaint(); return; } // Otherwise it is the window's own menu, where the window has one: a page // window that the page fills has no other place to be right-clicked. - if (e.mods.isPopupMenu() && onBackgroundMenu) onBackgroundMenu(); + if (onBackgroundMenu) onBackgroundMenu(); } // What a stop that makes no sound is, said over its knob: on a demo set the @@ -1383,6 +1425,10 @@ void ConsoleView::setControlFromMouse(juce::Point p) { void ConsoleView::mouseDrag(const juce::MouseEvent& e) { if (frozen_.isValid()) return; + // A finger that moves is sliding or scrolling: no long press, and a stop + // slid off is left alone. + if (!longPress_.moved(e.getPosition()) && !tappedStopBounds_.contains(e.getPosition())) + tappedStop_ = 0; if (heldControl_ != 0) { // Deliberately not clamped to the image: a hand that slides off the side // of a shoe while pushing it is still pushing it, and the position @@ -1404,8 +1450,14 @@ void ConsoleView::mouseDrag(const juce::MouseEvent& e) { repaint(keysBounds()); } -void ConsoleView::mouseUp(const juce::MouseEvent&) { +void ConsoleView::mouseUp(const juce::MouseEvent& e) { if (frozen_.isValid()) return; + longPress_.end(); + if (tappedStop_ != 0) { + const Id stop = std::exchange(tappedStop_, 0); + if (!longPress_.held() && tappedStopBounds_.contains(e.getPosition())) + toggleStop(stop, tappedStopBounds_); + } heldControl_ = 0; if (heldButton_ != 0) { proc_.setSwitchEngaged(heldButton_, false); diff --git a/src/mp_ui/Console.h b/src/mp_ui/Console.h index d8cfd84a..a0d99929 100644 --- a/src/mp_ui/Console.h +++ b/src/mp_ui/Console.h @@ -16,6 +16,7 @@ #include "../mp_audio/MasterpieceProcessor.h" #include "AccessibleToggle.h" +#include "Mobile.h" #include #include @@ -102,6 +103,12 @@ class ConsoleView : public juce::Component, public juce::TooltipClient, private void mouseDown(const juce::MouseEvent& e) override; void mouseDrag(const juce::MouseEvent& e) override; void mouseUp(const juce::MouseEvent& e) override; + // Touch: a finger held still opens what a right-click opens (LongPress), + // and a stop is drawn as the finger lifts, so a held one opens its menu + // and stays as it was. On where touchInput() says. + void setTouchInput(bool on) { touch_ = on; } + // The long press under way, for a test to fire. + LongPress& longPress() { return longPress_; } // Position a dragged control from a mouse point inside its image. void setControlFromMouse(juce::Point p); // Keeps the drawn controls in step with the engine: a piston, a @@ -300,6 +307,18 @@ class ConsoleView : public juce::Component, public juce::TooltipClient, private Id heldControl_ = 0; // A momentary button held down with the mouse: released with it. Id heldButton_ = 0; + // Touch: the stop under the finger, drawn or retired as it lifts unless it + // was held or slid off. + bool touch_ = touchInput(); + Id tappedStop_ = 0; + juce::Rectangle tappedStopBounds_; + LongPress longPress_; + void onLongPress(); + // A stop clicked or tapped: drawn or retired, unless it has no pipes. + void toggleStop(Id switchId, juce::Rectangle bounds); + // What a right-click opens at `p`: a manual's MIDI window when `keys`, a + // control's MIDI menu, or the background's. + void openMenuAt(juce::Point p, bool keys); juce::Rectangle heldButtonBounds_; juce::Rectangle heldControlBounds_; bool heldControlHigherIsMore_ = true; diff --git a/src/mp_ui/Mobile.cpp b/src/mp_ui/Mobile.cpp index 5ef96f80..b47ef293 100644 --- a/src/mp_ui/Mobile.cpp +++ b/src/mp_ui/Mobile.cpp @@ -350,6 +350,12 @@ void importDocument(const juce::URL& document, std::function d (new ImportThread(document, std::move(done)))->launchThread(); } +bool touchInput() { + static const bool on = + kMobile || juce::SystemStats::getEnvironmentVariable("MASTERPIECE_TOUCH", "0") == "1"; + return on; +} + juce::Rectangle screenArea() { // The whole display: on a phone or tablet the console runs in kiosk mode, // with the system's bars hidden (see the app's main window). Each panel is diff --git a/src/mp_ui/Mobile.h b/src/mp_ui/Mobile.h index aa558db8..eb306e9e 100644 --- a/src/mp_ui/Mobile.h +++ b/src/mp_ui/Mobile.h @@ -13,6 +13,53 @@ inline constexpr bool kMobile = true; inline constexpr bool kMobile = false; #endif +// Whether presses are worked as a finger's: on phones and tablets, and +// elsewhere with MASTERPIECE_TOUCH=1, which tries it with a mouse. +bool touchInput(); + +// A touch screen has no right button, and Android reports a mouse's as the +// left one (#474): a finger held still for half a second opens what a +// right-click opens. begin() on the press, moved() on each drag, end() on +// the release; onHeld runs once the finger has stayed put long enough. +class LongPress : private juce::Timer { +public: + static constexpr int kMs = 500; + std::function onHeld; + void begin(juce::Point at) { + at_ = at; + held_ = false; + startTimer(kMs); + } + // A finger that moves is sliding or scrolling. True while it is still + // where it pressed. + bool moved(juce::Point to) { + if (isTimerRunning() && to.getDistanceFrom(at_) > 8) stopTimer(); + return isTimerRunning(); + } + void end() { stopTimer(); } + // The press became a long press: its release does nothing else. + bool held() const { return held_; } + bool waiting() const { return isTimerRunning(); } + juce::Point where() const { return at_; } + // What the timer does, for a test. + void fire() { + stopTimer(); + held_ = true; + if (onHeld) onHeld(); + } + +private: + void timerCallback() override { fire(); } + juce::Point at_; + bool held_ = false; +}; + +// A component with a long press of its own, for a test to fire. +struct LongPressHolder { + virtual ~LongPressHolder() = default; + virtual LongPress& longPress() = 0; +}; + // The part of the screen an app may draw in: the whole display less the // system's own bars. juce::Rectangle screenArea(); diff --git a/src/mp_ui/Panels.cpp b/src/mp_ui/Panels.cpp index d8084b68..4c1c24ae 100644 --- a/src/mp_ui/Panels.cpp +++ b/src/mp_ui/Panels.cpp @@ -555,7 +555,7 @@ Colours schemeColours(int scheme, size_t sectionIndex) { // ---- one rectangle ----------------------------------------------------------- -class PanelView::Cell : public juce::Component { +class PanelView::Cell : public juce::Component, public LongPressHolder { public: Cell(PanelView& owner, PanelElement e, size_t sectionIndex) : owner_(owner), e_(std::move(e)), section_(sectionIndex) {} @@ -676,40 +676,64 @@ class PanelView::Cell : public juce::Component { return; } if (ev.mods.isPopupMenu()) { - // The element's own mapping: a stop is mapped through its drawstop. - Id target = e_.id; - if (e_.kind == PanelElement::Kind::Stop) { - const auto s = p.organModel().stops.find(e_.id); - target = s != p.organModel().stops.end() ? s->second.controllingSwitchId : 0; - } - if (target != 0) MidiEventDialog::show(p, MidiTargetKind::Switch, target); + openMapping(); return; } - switch (e_.kind) { - case PanelElement::Kind::Stop: p.setStopEngaged(e_.id, !p.shownStopEngaged(e_.id)); break; - case PanelElement::Kind::Switch: p.setSwitchEngaged(e_.id, !p.shownSwitchEngaged(e_.id)); break; - case PanelElement::Kind::Piston: - // Fired on press, as on a console; let go with the finger. - pressed_ = true; - p.setSwitchEngaged(e_.id, true); - break; - case PanelElement::Kind::Control: break; + // Touch: a finger held still opens the mapping, and a stop or switch + // changes as the finger lifts, so a held one stays as it was. + if (owner_.touch_) { + if (!longPress_.onHeld) longPress_.onHeld = [this] { letGo(); openMapping(); }; + longPress_.begin(ev.getPosition()); + } + if (e_.kind == PanelElement::Kind::Piston) { + // Fired on press, as on a console; let go with the finger. + pressed_ = true; + p.setSwitchEngaged(e_.id, true); + repaint(); + } else if (!owner_.touch_) { + toggle(); } + } + void mouseDrag(const juce::MouseEvent& ev) override { longPress_.moved(ev.getPosition()); } + void mouseUp(const juce::MouseEvent& ev) override { + longPress_.end(); + if (owner_.touch_ && !longPress_.held() && e_.kind != PanelElement::Kind::Piston && !owner_.editing_ && + !ev.mods.isPopupMenu() && getLocalBounds().contains(ev.getPosition())) + toggle(); + letGo(); + } + LongPress& longPress() override { return longPress_; } + +private: + void toggle() { + auto& p = owner_.proc_; + if (e_.kind == PanelElement::Kind::Stop) p.setStopEngaged(e_.id, !p.shownStopEngaged(e_.id)); + if (e_.kind == PanelElement::Kind::Switch) p.setSwitchEngaged(e_.id, !p.shownSwitchEngaged(e_.id)); repaint(); } - void mouseUp(const juce::MouseEvent&) override { + void letGo() { if (!pressed_) return; pressed_ = false; owner_.proc_.setSwitchEngaged(e_.id, false); repaint(); } + // The element's own mapping: a stop is mapped through its drawstop. + void openMapping() { + auto& p = owner_.proc_; + Id target = e_.id; + if (e_.kind == PanelElement::Kind::Stop) { + const auto s = p.organModel().stops.find(e_.id); + target = s != p.organModel().stops.end() ? s->second.controllingSwitchId : 0; + } + if (target != 0) MidiEventDialog::show(p, MidiTargetKind::Switch, target); + } -private: PanelView& owner_; PanelElement e_; size_t section_ = 0; bool shown_ = false; bool pressed_ = false; + LongPress longPress_; }; // ---- one fader ----------------------------------------------------------------- @@ -764,9 +788,19 @@ class PanelView::Fader : public juce::Component { MidiEventDialog::show(owner_.proc_, MidiTargetKind::ContinuousControl, e_.id); return; } + // Touch: a finger held still on the name opens the mapping; the track + // moves with the finger as before. + if (owner_.touch_ && ev.position.y < 26.0f) { + if (!longPress_.onHeld) + longPress_.onHeld = [this] { MidiEventDialog::show(owner_.proc_, MidiTargetKind::ContinuousControl, e_.id); }; + longPress_.begin(ev.getPosition()); + return; + } mouseDrag(ev); } + void mouseUp(const juce::MouseEvent&) override { longPress_.end(); } void mouseDrag(const juce::MouseEvent& ev) override { + if (longPress_.waiting() && longPress_.moved(ev.getPosition())) return; if (owner_.editing_ || ev.mods.isPopupMenu()) return; const auto track = getLocalBounds().toFloat().reduced(2.0f).withTrimmedTop(22.0f).reduced(10.0f, 6.0f); const float frac = juce::jlimit(0.0f, 1.0f, (track.getBottom() - ev.position.y) / track.getHeight()); @@ -779,6 +813,7 @@ class PanelView::Fader : public juce::Component { PanelElement e_; size_t section_ = 0; int shown_ = -1; + LongPress longPress_; }; // ---- the panel ------------------------------------------------------------------- diff --git a/src/mp_ui/Panels.h b/src/mp_ui/Panels.h index 3271054c..57c814f4 100644 --- a/src/mp_ui/Panels.h +++ b/src/mp_ui/Panels.h @@ -13,6 +13,7 @@ #pragma once #include "../mp_audio/MasterpieceProcessor.h" +#include "Mobile.h" #include @@ -111,6 +112,7 @@ class PanelView : public juce::Component, private juce::Timer { for (auto* c : cells_) out.push_back(c->getBounds()); return out; } + juce::Component* cellForTest(int i) const { return cells_[i]; } juce::Rectangle viewAreaForTest() const { return {viewport_.getMaximumVisibleWidth(), viewport_.getMaximumVisibleHeight()}; } @@ -138,6 +140,9 @@ class PanelView : public juce::Component, private juce::Timer { void resized() override; void paint(juce::Graphics& g) override; bool keyPressed(const juce::KeyPress& key) override; + // Touch: a finger held still opens an element's mapping, and a stop + // changes as the finger lifts (LongPress). On where touchInput() says. + void setTouchInput(bool on) { touch_ = on; } private: class Cell; @@ -156,6 +161,7 @@ class PanelView : public juce::Component, private juce::Timer { PanelLayout layout_, saved_; std::vector sections_; bool editing_ = false; + bool touch_ = touchInput(); juce::OwnedArray sectionButtons_; juce::TextButton edit_{TRANS("Edit")}, save_{TRANS("Save")}, detach_{TRANS("Detach")}, rename_{TRANS("Rename")}; diff --git a/tests/test_accessibility.cpp b/tests/test_accessibility.cpp index 6a204a59..88019344 100644 --- a/tests/test_accessibility.cpp +++ b/tests/test_accessibility.cpp @@ -282,7 +282,109 @@ class ConsoleAccessibilityTest final : public mp::test::Test { } }; +// A press at `p` on `c`, with the left button unless `right`. +static juce::MouseEvent pressAt(juce::Component& c, juce::Point p, bool right = false) { + const auto now = juce::Time::getCurrentTime(); + return juce::MouseEvent(juce::Desktop::getInstance().getMainMouseSource(), p.toFloat(), + right ? juce::ModifierKeys::rightButtonModifier + : juce::ModifierKeys::leftButtonModifier, + juce::MouseInputSource::defaultPressure, 0.0f, 0.0f, 0.0f, 0.0f, &c, &c, + now, p.toFloat(), now, 1, false); +} + +// A touch screen has no right button, and Android reports a mouse's as the +// left one (#474): a finger held still opens what a right-click opens, and a +// stop changes as the finger lifts, so a held one stays as it was. +class TouchLongPressTest final : public mp::test::Test { +public: + TouchLongPressTest() : Test("functional.ui.touch-long-press", Category::Functional) {} + + void run() override { + TestOrgan organ; + mp::MasterpieceProcessor proc; + proc.prepareToPlay(48000.0, 256); + MP_CHECK(proc.loadOrgan(organ.odf, 0, false).ok, "the organ loads"); + + // The drawn console: the knob of stop 901 is at 100,200, 40 by 60. + mp::ui::ConsoleView console(proc); + console.setTouchInput(true); + int held = 0; + console.longPress().onHeld = [&held] { ++held; }; + const juce::Point knob(120, 230); + + console.mouseDown(pressAt(console, knob)); + runOneBlock(proc); + MP_CHECK(!proc.stopEngaged(901), "a finger on the stop leaves it as it is while it is down"); + MP_CHECK(console.longPress().waiting(), "and the wait for a long press has begun"); + console.mouseUp(pressAt(console, knob)); + runOneBlock(proc); + MP_CHECK(proc.stopEngaged(901), "a tap draws the stop as the finger lifts"); + MP_CHECK(held == 0, "a tap is not a long press"); + + console.mouseDown(pressAt(console, knob)); + console.longPress().fire(); + console.mouseUp(pressAt(console, knob)); + runOneBlock(proc); + MP_CHECK(held == 1, "a finger held still is a long press"); + MP_CHECK(proc.stopEngaged(901), "and the stop held stays drawn"); + + console.mouseDown(pressAt(console, knob)); + console.mouseDrag(pressAt(console, {300, 400})); + MP_CHECK(!console.longPress().waiting(), "a finger that slides is no long press"); + console.mouseUp(pressAt(console, {300, 400})); + runOneBlock(proc); + MP_CHECK(proc.stopEngaged(901), "and a stop slid off is left as it was"); + + console.mouseDown(pressAt(console, knob)); + console.mouseDrag(pressAt(console, knob + juce::Point(3, 3))); + MP_CHECK(console.longPress().waiting(), "a finger that trembles is still held"); + console.mouseUp(pressAt(console, knob + juce::Point(3, 3))); + runOneBlock(proc); + MP_CHECK(!proc.stopEngaged(901), "and its tap retires the stop"); + + // With a mouse, the stop changes on the press, as it always has. + console.setTouchInput(false); + console.mouseDown(pressAt(console, knob)); + runOneBlock(proc); + MP_CHECK(proc.stopEngaged(901), "a click draws the stop on the press"); + MP_CHECK(!console.longPress().waiting(), "and starts no long press"); + console.mouseUp(pressAt(console, knob)); + runOneBlock(proc); + MP_CHECK(proc.stopEngaged(901), "and the release leaves it drawn"); + + // The generated panel: its first cell is stop 901. + juce::MessageManager::getInstance(); + mp::ui::PanelView panel(proc, 0, mp::ui::PanelLayout{}); + panel.setTouchInput(true); + panel.setSize(1240, 660); + auto* cell = panel.cellForTest(0); + MP_CHECK(cell != nullptr, "the panel has a cell"); + if (cell == nullptr) return; + auto* cellPress = dynamic_cast(cell); + MP_CHECK(cellPress != nullptr, "the cell keeps a long press"); + if (cellPress == nullptr) return; + int cellHeld = 0; + cellPress->longPress().onHeld = [&cellHeld] { ++cellHeld; }; + const auto middle = cell->getLocalBounds().getCentre(); + + cell->mouseDown(pressAt(*cell, middle)); + runOneBlock(proc); + MP_CHECK(proc.stopEngaged(901), "the panel leaves the stop drawn while the finger is down"); + cell->mouseUp(pressAt(*cell, middle)); + runOneBlock(proc); + MP_CHECK(!proc.stopEngaged(901), "and its tap retires it as the finger lifts"); + + cell->mouseDown(pressAt(*cell, middle)); + cellPress->longPress().fire(); + cell->mouseUp(pressAt(*cell, middle)); + runOneBlock(proc); + MP_CHECK(cellHeld == 1, "a finger held on the cell is a long press"); + MP_CHECK(!proc.stopEngaged(901), "and leaves the stop as it was"); + } +}; + static StopListAccessibilityTest g_stopListAccessibility; static ConsoleAccessibilityTest g_consoleAccessibility; +static TouchLongPressTest g_touchLongPress; #endif // MP_TEST_HAS_AUDIO