Skip to content

Commit

Permalink
LayeredMenu: allows multiple presses on a single shortcut to cycle
Browse files Browse the repository at this point in the history
- replaces the SELECT press changing unison count to unison stereo spread:
  does the same thing, but works by pressing the same shortcut again.

- the same mechanism should be usable as-is for eg. providing fast access
  to new arp parameters, and has been tested to work with horizontal menus
  as well. (no combined horizontal and layered menus are part of this PR,
  cominng after)

- LayeredShortcut is a MenuItem class that encapsulates other menu items,
  switching between them via SoundEditor calling MenuItem::nextLayer().

- Moved some Submenu methods to MenuItem, so that we can put both Submenus
  and LayeredShortcuts in the parents tables. Some Submenu items made explicitly
  final.

- SoundEditor navigation stack cleanup.

- Removed const qualifiers from SoundEditor that were only getting cast away,
  making the code harder to follow.

- Replaced NO_NAVIGATION with a proper tombstone object, so it can have methods
  called on it.

- minor cleanup in sound_editor.cpp: we had `const MenuItem*` in a couple
  of places, where we then had to explicitly cast the constness away --
  better not have it in the first place.
  • Loading branch information
nikodemus committed Jan 5, 2025
1 parent e936bb8 commit fdd7ab4
Show file tree
Hide file tree
Showing 14 changed files with 437 additions and 122 deletions.
8 changes: 7 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@
- If a clip has a name, it is displayed with a number prefix, indicating its section, eg. "3: CHORUS".
- When clips are copied, the clip name is copied as well. If the target track already has a clip with the same name, an integer suffix starting from 2 is added unless the name already has an integer suffix. This integer suffix is incremented until the clip name is unique on the target track. Ie. copying a clip named "BRIDGE" to the same otherwise empty track will first create "BRIDGE2", then "BRIDGE3", etc.

#### <ins>Layered Shortcuts<ins>
- Layered Shortcuts mechanism allows multiple shortcuts to be accessed under a single shotcut pad. Holding shift and pressing the same shortcut again cycles between shortcuts. The items available under layered shortcuts can also be accessed from menu: layered shortcut is never the only access method.
- Following shortcuts have layers:
- `UNISON NUMBER`: cycles between `UNISON AMOUNT` and `UNISON STEREO SPREAD`, replacing the previous "press select when in `UNISON AMOUNT` access mechanism for `UNISON STEREO SPREAD`.

#### <ins>Tempo</ins>
- Added Community Feature toggle (`Settings > Community Features > Alternative Tap Tempo Behaviour (TAPT)`) to adjust number of `TAP TEMPO` button presses to engage `TAP TEMPO` to `FOUR (4)` to avoid mistakingly changing tempo.

Expand Down Expand Up @@ -584,7 +589,8 @@ also affect normal sequenced notes while arpeggiator is Off.
- `WAVEFOLD` distortion has been added and occurs pre-filter. The parameter pad shortcut is between `SATURATION`
and `LPF CUTOFF`.
- `UNISON STEREO SPREAD` has been added and can be dialed to spread the unison parts across the stereo field.
Click `SELECT` when in `UNISON AMOUNT` to reveal the parameter.
~~Click `SELECT` when in `UNISON AMOUNT` to reveal the parameter.~~ Since 1.3: hold `SHIFT` and press the
`UNISON NUMBER` shortcut pad twice, or access via `SOUND > VOICE > UNISON` menu.

##### Filter

Expand Down
14 changes: 10 additions & 4 deletions docs/community_features.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,12 @@ at velocity 0 it would look the same as its tail (but you can't have 0 velocity)

#### 2.2 Horizontal Menus
- The menus for the following items have been updated on OLED, with multiple values visible and editable at the same time. Hold `SHIFT` and turn `SELECT` to edit them. This feature is on by default, and can be disabled via `SETTINGS > COMMUNITY FEATURES`.
- Envelope 1 & 2.
- Envelope 1 & 2. ([#3147])

#### 2.3 Layered Shortcuts
- Layered Shortcuts mechanism allows multiple shortcuts to be accessed under a single shotcut pad. Holding shift and pressing the same shortcut again cycles between shortcuts. The items available under layered shortcuts can also be accessed from menu: layered shortcut is never the only access method.
- Following shortcuts have layers:
- `UNISON NUMBER`: cycles between `UNISON AMOUNT` and `UNISON STEREO SPREAD`. ([#3209])

## 3. General Improvements

Expand Down Expand Up @@ -1041,9 +1046,8 @@ to each individual note onset. ([#1978])
- ([#157]) Add a `MOD MATRIX` entry to the `SOUND` menu which shows a list of all currently active modulations.

#### 4.5.2 - Unison Stereo Spread

- ([#223]) The unison parts can be spread accross the stereo field. Press `SELECT` when in the `UNISON NUMBER` menu to
access the new unison spread parameter.
- ([#223], [#3209]) The unison parts can be spread accross the stereo field. Access through `SOUND > VOICE > UNISON` menu,
or though layered shortcut on `UNISON NUMBER` pad.

#### 4.5.3 - Waveform Loop Lock

Expand Down Expand Up @@ -1570,6 +1574,8 @@ different firmware

[#3079]: https://github.com/SynthstromAudible/DelugeFirmware/pull/3079

[#3209]: https://github.com/SynthstromAudible/DelugeFirmware/pull/3209

[Automation View Documentation]: features/automation_view.md

[Arpeggiator Documentation]: features/arpeggiator.md
Expand Down
3 changes: 2 additions & 1 deletion src/deluge/gui/menu_item/bend_range/per_finger.h
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ class PerFinger final : public BendRange {
}
}
bool isRelevant(ModControllableAudio* modControllable, int32_t whichThing) override {
return soundEditor.navigationDepth == 1 || soundEditor.editingKit();
// Why the depth check?
return soundEditor.getNavigationDepth() == 1 || soundEditor.editingKit();
}
};
} // namespace deluge::gui::menu_item::bend_range
141 changes: 141 additions & 0 deletions src/deluge/gui/menu_item/layered_menu.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
/*
* Copyright © 2025 Synthstrom Audible Limited
*
* This file is part of The Synthstrom Audible Deluge Firmware.
*
* The Synthstrom Audible Deluge Firmware is free software: you can redistribute it and/or modify it under the
* terms of the GNU General Public License as published by the Free Software Foundation,
* either version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program.
* If not, see <https://www.gnu.org/licenses/>.
*/

#pragma once

#include "gui/menu_item/menu_item.h"
#include <initializer_list>

namespace deluge::gui::menu_item {

// This is a bit brittle to maintain: if anyone adds a virtual method to MenuItem with a default
// implementation and doesn't update this class, things will break.
//
// Should probably make MenuItem purely virtual, and put the default implementations in StandardMenuItem
// for most classes to inherit - but Nikodemus didn't want to do that up-front before this was otherwise
// considered a valid design.

/// @brief Used to stack multiple menu items on a single shortcut pad: SoundEditor calls nextLayer() when
/// a shortcut is activated twice in a row. This is a no-op for regular menu items, but allows LayeredShortcut
/// to change the current_item_, to most other MenuItem methods are delegated.
class LayeredShortcut final : public MenuItem {
public:
LayeredShortcut(std::initializer_list<MenuItem*> newItems)
: MenuItem(), items{newItems}, current_item_{items.begin()} {}

/// @brief Activates the next layer of a LayeredShortcut; called by soundEditor when a shortcut which
/// was already active is activated again.
/// @return The currently active layer number. Used to build a patchIndex for indirection magic for
/// envalopes and other menu-items which use it -- doesn't do anything if the menu item doesn't use
/// soundEditor.patchIndex.
int32_t nextLayer() final {
current_item_ = std::next(current_item_);
if (current_item_ == items.end()) {
current_item_ = items.begin();
}
return std::distance(items.begin(), current_item_);
}

/// @brief Called by SoundEditor when menu item loses focus. Used to reset the active layer back to
/// first.
void lostFocus() final {
// Tell the current layer it lost focus.
(*current_item_)->lostFocus();
// Reset to first layer.
current_item_ = items.begin();
}

/// @brief Returns the actual active menu item.
MenuItem* actual() final { return (*current_item_)->actual(); }

ActionResult buttonAction(deluge::hid::Button b, bool on, bool inCardRoutine) final {
return (*current_item_)->buttonAction(b, on, inCardRoutine);
}
void horizontalEncoderAction(int32_t offset) final { return (*current_item_)->horizontalEncoderAction(offset); }
void verticalEncoderAction(int32_t offset) final { return (*current_item_)->verticalEncoderAction(offset); }
void selectEncoderAction(int32_t offset) final { return (*current_item_)->selectEncoderAction(offset); }
bool selectEncoderActionEditsInstrument() final { return (*current_item_)->selectEncoderActionEditsInstrument(); }
MenuItem* selectButtonPress() final { return (*current_item_)->selectButtonPress(); }
ActionResult timerCallback() final { return (*current_item_)->timerCallback(); }
bool usesAffectEntire() final { return (*current_item_)->usesAffectEntire(); }
MenuPermission checkPermissionToBeginSession(ModControllableAudio* modControllable, int32_t whichThing,
MultiRange** currentRange) final {
return (*current_item_)->checkPermissionToBeginSession(modControllable, whichThing, currentRange);
}
void beginSession(MenuItem* navigatedBackwardFrom = nullptr) final {
(*current_item_)->beginSession(navigatedBackwardFrom);
}
void readValueAgain() final { return (*current_item_)->readValueAgain(); }
void readCurrentValue() final { return (*current_item_)->readCurrentValue(); }
uint8_t getIndexOfPatchedParamToBlink() final { return (*current_item_)->getIndexOfPatchedParamToBlink(); }
deluge::modulation::params::Kind getParamKind() final { return (*current_item_)->getParamKind(); }
uint32_t getParamIndex() final { return (*current_item_)->getParamIndex(); }
uint8_t shouldBlinkPatchingSourceShortcut(PatchSource s, uint8_t* colour) final {
return (*current_item_)->shouldBlinkPatchingSourceShortcut(s, colour);
}
MenuItem* patchingSourceShortcutPress(PatchSource s, bool previousPressStillActive = false) final {
return (*current_item_)->patchingSourceShortcutPress(s, previousPressStillActive);
}
void learnKnob(MIDICable* cable, int32_t whichKnob, int32_t modKnobMode, int32_t midiChannel) final {
return (*current_item_)->learnKnob(cable, whichKnob, modKnobMode, midiChannel);
}
bool allowsLearnMode() final { return (*current_item_)->allowsLearnMode(); }
bool learnNoteOn(MIDICable& cable, int32_t channel, int32_t noteCode) final {
return (*current_item_)->learnNoteOn(cable, channel, noteCode);
}
void learnProgramChange(MIDICable& cable, int32_t channel, int32_t programNumber) final {
return (*current_item_)->learnProgramChange(cable, channel, programNumber);
}
void learnCC(MIDICable& cable, int32_t channel, int32_t ccNumber, int32_t value) final {
return (*current_item_)->learnCC(cable, channel, ccNumber, value);
}
bool shouldBlinkLearnLed() final { return (*current_item_)->shouldBlinkLearnLed(); }
void unlearnAction() final { return (*current_item_)->unlearnAction(); }
bool isRangeDependent() final { return (*current_item_)->isRangeDependent(); }
void renderOLED() final { return (*current_item_)->renderOLED(); }
void drawPixelsForOled() final { return (*current_item_)->drawPixelsForOled(); }
std::string_view getTitle() const final { return (*current_item_)->getTitle(); }
uint8_t shouldDrawDotOnName() final { return (*current_item_)->shouldDrawDotOnName(); }
void drawName() final { return (*current_item_)->drawName(); }
std::string_view getName() const final { return (*current_item_)->getName(); }
std::string_view getShortName() const final { return (*current_item_)->getShortName(); }
bool isRelevant(ModControllableAudio* modControllable, int32_t whichThing) final {
return (*current_item_)->isRelevant(modControllable, whichThing);
}
bool shouldEnterSubmenu() final { return (*current_item_)->shouldEnterSubmenu(); }
int32_t getSubmenuItemTypeRenderLength() final { return (*current_item_)->getSubmenuItemTypeRenderLength(); }
int32_t getSubmenuItemTypeRenderIconStart() final { return (*current_item_)->getSubmenuItemTypeRenderIconStart(); }
void renderSubmenuItemTypeForOled(int32_t yPixel) final {
return (*current_item_)->renderSubmenuItemTypeForOled(yPixel);
}
void renderInHorizontalMenu(int32_t startX, int32_t width, int32_t startY, int32_t height) final {
return (*current_item_)->renderInHorizontalMenu(startX, width, startY, height);
}
bool isSubmenu() final { return (*current_item_)->isSubmenu(); }
void setupNumberEditor() final { return (*current_item_)->setupNumberEditor(); }
void updatePadLights() final { return (*current_item_)->updatePadLights(); }
void updateAutomationViewParameter() final { return (*current_item_)->updateAutomationViewParameter(); }

bool focusChild(MenuItem* item) final { return (*current_item_)->focusChild(item); }
bool supportsHorizontalRendering() final { return (*current_item_)->supportsHorizontalRendering(); }

private:
deluge::vector<MenuItem*> items;
typename decltype(items)::iterator current_item_;
};

} // namespace deluge::gui::menu_item
13 changes: 13 additions & 0 deletions src/deluge/gui/menu_item/menu_item.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
#include "gui/ui/sound_editor.h"
#include "hid/display/display.h"
#include "hid/display/oled.h" //todo: this probably shouldn't be needed
#include "model/settings/runtime_feature_settings.h"
#include <string_view>

using namespace deluge;
Expand Down Expand Up @@ -117,6 +118,16 @@ void MenuItem::updatePadLights() {
soundEditor.updatePadLightsFor(this);
}

MenuItem::RenderingStyle MenuItem::renderingStyle() {
if (display->haveOLED() && this->supportsHorizontalRendering()
&& runtimeFeatureSettings.isOn(RuntimeFeatureSettingType::HorizontalMenus)) {
return MenuItem::RenderingStyle::HORIZONTAL;
}
else {
return MenuItem::RenderingStyle::VERTICAL;
}
}

bool isItemRelevant(MenuItem* item) {
if (item == nullptr) {
return false;
Expand All @@ -125,3 +136,5 @@ bool isItemRelevant(MenuItem* item) {
return item->isRelevant(soundEditor.currentModControllable, soundEditor.currentSourceIndex);
}
}

MenuItem noNavigation{};
25 changes: 24 additions & 1 deletion src/deluge/gui/menu_item/menu_item.h
Original file line number Diff line number Diff line change
Expand Up @@ -279,10 +279,33 @@ class MenuItem {
virtual void updateAutomationViewParameter() { return; }
void renderColumnLabel(int32_t startX, int32_t width, int32_t startY);

/// Called by SoundEditor when same shortcut is entered twice in a row. Used by LayeredShortcut to step to the next
/// layer. Returns the number of the current layer.
virtual int32_t nextLayer() { return 0; }

/// Used to focus a menu on a specific item. Returns true if the item was available and relevant.
virtual bool focusChild(MenuItem* item) { return false; }

/// Menus which support horizontal rendering need to override this.
virtual bool supportsHorizontalRendering() { return false; }

/// Called when the manu item stops being the current one.
virtual void lostFocus() {};

/// Returns the underlying menu item: for submenus returns the currently active (focused) child,
/// for layered shortcuts returns the menu item of the active layer.
virtual MenuItem* actual() { return this; }

enum RenderingStyle { VERTICAL, HORIZONTAL };
RenderingStyle renderingStyle();

/// @}
};

#define NO_NAVIGATION ((MenuItem*)0xFFFFFFFF)
/// Tombstone / no-value-value for menu-items. Used to communicate backwards navigation, etc.
extern MenuItem noNavigation;

#define NO_NAVIGATION &noNavigation

/// @brief Returns true if the item is relevant using current soundEditor
/// modControllable and sourceIndex.
Expand Down
2 changes: 1 addition & 1 deletion src/deluge/gui/menu_item/source/patched_param.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,6 @@ namespace deluge::gui::menu_item::source {
class PatchedParam : public menu_item::patched_param::Integer {
public:
using Integer::Integer;
uint8_t getP() override { return menu_item::PatchedParam::getP() + soundEditor.currentSourceIndex; }
uint8_t getP() override { return menu_item::PatchedParam::getP() + soundEditor.currentPatchIndex; }
};
} // namespace deluge::gui::menu_item::source
25 changes: 13 additions & 12 deletions src/deluge/gui/menu_item/submenu.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
#include "gui/views/automation_view.h"
#include "hid/display/display.h"
#include "hid/display/oled.h"
#include "model/settings/runtime_feature_settings.h"
#include "util/container/static_vector.hpp"

namespace deluge::gui::menu_item {
Expand All @@ -14,7 +13,7 @@ void Submenu::beginSession(MenuItem* navigatedBackwardFrom) {
}
}

bool Submenu::focusChild(const MenuItem* child) {
bool Submenu::focusChild(MenuItem* child) {
// Set new current item.
auto prev = current_item_;
if (child != nullptr) {
Expand Down Expand Up @@ -249,6 +248,9 @@ void Submenu::selectEncoderAction(int32_t offset) {
}

bool Submenu::shouldForwardButtons() {
if (current_item_ == items.end()) {
return false;
}
// Should we deliver buttons to selected menu item instead?
return (*current_item_)->isSubmenu() == false && renderingStyle() == RenderingStyle::HORIZONTAL;
}
Expand Down Expand Up @@ -320,16 +322,6 @@ bool Submenu::learnNoteOn(MIDICable& cable, int32_t channel, int32_t noteCode) {
return false;
}

Submenu::RenderingStyle Submenu::renderingStyle() {
if (display->haveOLED() && this->supportsHorizontalRendering()
&& runtimeFeatureSettings.isOn(RuntimeFeatureSettingType::HorizontalMenus)) {
return RenderingStyle::HORIZONTAL;
}
else {
return RenderingStyle::VERTICAL;
}
}

void Submenu::updatePadLights() {
if (renderingStyle() == RenderingStyle::HORIZONTAL && current_item_ != items.end()) {
soundEditor.updatePadLightsFor(*current_item_);
Expand All @@ -348,4 +340,13 @@ MenuItem* Submenu::patchingSourceShortcutPress(PatchSource s, bool previousPress
}
}

MenuItem* Submenu::actual() {
if (current_item_ == items.end()) {
return nullptr;
}
else {
return (*current_item_)->actual();
}
}

} // namespace deluge::gui::menu_item
24 changes: 10 additions & 14 deletions src/deluge/gui/menu_item/submenu.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,6 @@ namespace deluge::gui::menu_item {

class Submenu : public MenuItem {
public:
enum RenderingStyle { VERTICAL, HORIZONTAL };

Submenu(l10n::String newName, std::initializer_list<MenuItem*> newItems)
: MenuItem(newName), items{newItems}, current_item_{items.end()} {}
Submenu(l10n::String newName, std::span<MenuItem*> newItems)
Expand All @@ -48,22 +46,20 @@ class Submenu : public MenuItem {
void unlearnAction() final;
bool allowsLearnMode() final;
void learnKnob(MIDICable* cable, int32_t whichKnob, int32_t modKnobMode, int32_t midiChannel) final;
void learnProgramChange(MIDICable& cable, int32_t channel, int32_t programNumber) override;
void learnProgramChange(MIDICable& cable, int32_t channel, int32_t programNumber) final;
bool learnNoteOn(MIDICable& cable, int32_t channel, int32_t noteCode) final;
void drawPixelsForOled() override;
void drawPixelsForOled() final;
void drawSubmenuItemsForOled(std::span<MenuItem*> options, const int32_t selectedOption);
/// @brief Indicates if the menu-like object should wrap-around. Destined to be virtualized.
/// At the moment implements the legacy behaviour of wrapping on 7seg but not on OLED.
bool wrapAround();
bool isSubmenu() override { return true; }
virtual bool focusChild(const MenuItem* child);
/// Submenus which support horizontal rendering need to override this.
virtual bool supportsHorizontalRendering() { return false; }
RenderingStyle renderingStyle();
void updatePadLights() override;
MenuItem* patchingSourceShortcutPress(PatchSource s, bool previousPressStillActive = false) override;
deluge::modulation::params::Kind getParamKind() override;
uint32_t getParamIndex() override;
bool isSubmenu() final { return true; }
bool focusChild(MenuItem* child) final;
void updatePadLights() final;
MenuItem* patchingSourceShortcutPress(PatchSource s, bool previousPressStillActive = false) final;
deluge::modulation::params::Kind getParamKind() final;
uint32_t getParamIndex() final;
MenuItem* actual() final;

protected:
void drawVerticalMenu();
Expand All @@ -83,7 +79,7 @@ class HorizontalMenu : public Submenu {
: Submenu(newName, title, newItems) {}
HorizontalMenu(l10n::String newName, l10n::String title, std::span<MenuItem*> newItems)
: Submenu(newName, title, newItems) {}
bool supportsHorizontalRendering() { return true; }
bool supportsHorizontalRendering() final { return true; }
};

} // namespace deluge::gui::menu_item
Loading

0 comments on commit fdd7ab4

Please sign in to comment.