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.

- 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.

- update community features & changelog, also missing some of the horiz
  menu changes.
  • Loading branch information
nikodemus committed Jan 4, 2025
1 parent 4ca5ba7 commit 6980a90
Show file tree
Hide file tree
Showing 7 changed files with 175 additions and 32 deletions.
9 changes: 8 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,18 @@
#### <ins>Horizontal Menus</ins>
- 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.
- LPF & HPF.

#### <ins>Clip Name Display & Copying</ins>
- If a clip has no named "SECTION N" is displayed in place of the clip name, indicating which section the clip is in.
- 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 +590,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
13 changes: 9 additions & 4 deletions docs/community_features.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,13 @@ 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. ([#3118])
- LPF & HPF. ([#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`. ([#XXX])

## 3. General Improvements

Expand Down Expand Up @@ -1041,9 +1047,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, #XXX]) 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
115 changes: 115 additions & 0 deletions src/deluge/gui/menu_item/layered_menu.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/*
* 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 {

// TODO: given how submenu sometimes delegates things the same way, we might want DelegatingMenu which
// both inherit from, and then each just defines getDelegate()?
class LayeredMenu final : public MenuItem {
public:
LayeredMenu(std::initializer_list<MenuItem*> newItems)
: MenuItem(), items{newItems}, current_item_{items.begin()} {}

void shortcutAgain() final {
current_item_ = std::next(current_item_);
if (current_item_ == items.end()) {
current_item_ = items.begin();
}
}

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(); }

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

} // namespace deluge::gui::menu_item
4 changes: 4 additions & 0 deletions src/deluge/gui/menu_item/menu_item.h
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,10 @@ class MenuItem {
virtual void updateAutomationViewParameter() { return; }
void renderColumnLabel(int32_t startX, int32_t width, int32_t startY);

/// Called when same shortcut is entered twice in a row. Used by LayeredMenu to step to the next
/// layer.
virtual void shortcutAgain() {}

/// @}
};

Expand Down
3 changes: 2 additions & 1 deletion src/deluge/gui/ui/menus.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@
#include "gui/menu_item/integer_range.h"
#include "gui/menu_item/key_range.h"
#include "gui/menu_item/keyboard/layout.h"
#include "gui/menu_item/layered_menu.h"
#include "gui/menu_item/lfo/rate.h"
#include "gui/menu_item/lfo/sync.h"
#include "gui/menu_item/lfo/type.h"
Expand Down Expand Up @@ -213,7 +214,7 @@ namespace params = deluge::modulation::params;

// This menu item is special, it only exists on the grid (not in the menu hierarchy). To avoid confusion in
// autogenerated menu documentation, this item should be left alone..
unison::CountToStereoSpread numUnisonToStereoSpreadMenu{STRING_FOR_UNISON_NUMBER};
LayeredMenu numUnisonToStereoSpreadMenu{{&numUnisonMenu, &unison::stereoSpreadMenu}};

// Arp --------------------------------------------------------------------------------------
arpeggiator::PresetMode arpPresetModeMenu{STRING_FOR_PRESET, STRING_FOR_ARP_PRESET_MENU_TITLE};
Expand Down
61 changes: 36 additions & 25 deletions src/deluge/gui/ui/sound_editor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -948,7 +948,7 @@ ActionResult SoundEditor::potentialShortcutPadAction(int32_t x, int32_t y, bool
return ActionResult::REMIND_ME_OUTSIDE_CARD_ROUTINE;
}

const MenuItem* item = nullptr;
MenuItem* item = nullptr;
Submenu* parent = nullptr;

// session views (arranger, song, performance)
Expand Down Expand Up @@ -1109,16 +1109,20 @@ ActionResult SoundEditor::potentialShortcutPadAction(int32_t x, int32_t y, bool
}
}

// Things like LayeredMenu can adjust their behaviour when entered twice.
if (item == getCurrentMenuItem()) {
item->shortcutAgain();
}

// if we're in the menu and automation view is the root (background) UI
// and you're using a grid shortcut, only allow use of shortcuts for parameters / patch cables
MenuItem* newItem;
newItem = (MenuItem*)item;

// need to make sure we're already in the menu
// because at this point menu may not have been setup yet
// menu needs to be setup before menu items can call soundEditor.getCurrentModelStack()
if (getCurrentUI() == &soundEditor) {
deluge::modulation::params::Kind kind = newItem->getParamKind();
if ((newItem->getParamKind() == deluge::modulation::params::Kind::NONE)
deluge::modulation::params::Kind kind = item->getParamKind();
if ((item->getParamKind() == deluge::modulation::params::Kind::NONE)
&& getRootUI() == &automationView) {
return ActionResult::DEALT_WITH;
}
Expand Down Expand Up @@ -1403,7 +1407,7 @@ void SoundEditor::modEncoderButtonAction(uint8_t whichModEncoder, bool on) {
}
}

bool SoundEditor::setup(Clip* clip, const MenuItem* item, int32_t sourceIndex) {
bool SoundEditor::setup(Clip* clip, MenuItem* item, int32_t sourceIndex) {

Sound* newSound = nullptr;
ParamManagerForTimeline* newParamManager = nullptr;
Expand Down Expand Up @@ -1494,12 +1498,7 @@ bool SoundEditor::setup(Clip* clip, const MenuItem* item, int32_t sourceIndex) {
}
}

MenuItem* newItem;

if (item) {
newItem = (MenuItem*)item;
}
else {
if (!item) {
if (clip) {
actionLogger.deleteAllLogs();

Expand All @@ -1513,48 +1512,60 @@ bool SoundEditor::setup(Clip* clip, const MenuItem* item, int32_t sourceIndex) {
else if (outputType == OutputType::MIDI_OUT) {
soundEditorRootMenuMIDIOrCV.title = l10n::String::STRING_FOR_MIDI_INST_MENU_TITLE;
doMIDIOrCV:
newItem = &soundEditorRootMenuMIDIOrCV;
item = &soundEditorRootMenuMIDIOrCV;
}
else if (outputType == OutputType::CV) {
soundEditorRootMenuMIDIOrCV.title = l10n::String::STRING_FOR_CV_INSTRUMENT;
goto doMIDIOrCV;
}

else if ((outputType == OutputType::KIT) && instrumentClip->affectEntire) {
newItem = &soundEditorRootMenuKitGlobalFX;
item = &soundEditorRootMenuKitGlobalFX;
}

else if ((outputType == OutputType::KIT) && !instrumentClip->affectEntire
&& ((Kit*)output)->selectedDrum != nullptr
&& ((Kit*)output)->selectedDrum->type == DrumType::MIDI) {
newItem = &soundEditorRootMenuMidiDrum;
item = &soundEditorRootMenuMidiDrum;
}

else if ((outputType == OutputType::KIT) && !instrumentClip->affectEntire
&& ((Kit*)output)->selectedDrum != nullptr
&& ((Kit*)output)->selectedDrum->type == DrumType::GATE) {
newItem = &soundEditorRootMenuGateDrum;
item = &soundEditorRootMenuGateDrum;
}

else if ((outputType == OutputType::KIT) && !instrumentClip->affectEntire
&& ((Kit*)output)->selectedDrum != nullptr
&& ((Kit*)output)->selectedDrum->type == DrumType::MIDI) {
item = &soundEditorRootMenuMidiDrum;
}

else if ((outputType == OutputType::KIT) && !instrumentClip->affectEntire
&& ((Kit*)output)->selectedDrum != nullptr
&& ((Kit*)output)->selectedDrum->type == DrumType::GATE) {
item = &soundEditorRootMenuGateDrum;
}

else {
newItem = &soundEditorRootMenu;
item = &soundEditorRootMenu;
}
}

else {
newItem = &soundEditorRootMenuAudioClip;
item = &soundEditorRootMenuAudioClip;
}
}
else {
if ((currentUI == &performanceView) && !Buttons::isShiftButtonPressed()) {
newItem = &soundEditorRootMenuPerformanceView;
item = &soundEditorRootMenuPerformanceView;
}
else if ((currentUI == &sessionView || currentUI == &arrangerView || currentUI == &automationView)
&& !Buttons::isShiftButtonPressed()) {
newItem = &soundEditorRootMenuSongView;
item = &soundEditorRootMenuSongView;
}
else {
newItem = &settingsRootMenu;
item = &settingsRootMenu;
}
}
}
Expand All @@ -1570,7 +1581,7 @@ bool SoundEditor::setup(Clip* clip, const MenuItem* item, int32_t sourceIndex) {
// depth", it needs this.
currentParamManager = newParamManager;

MenuPermission result = newItem->checkPermissionToBeginSession(newModControllable, sourceIndex, &newRange);
MenuPermission result = item->checkPermissionToBeginSession(newModControllable, sourceIndex, &newRange);

if (result == MenuPermission::NO) {
display->displayPopup(deluge::l10n::get(deluge::l10n::String::STRING_FOR_PARAMETER_NOT_APPLICABLE));
Expand All @@ -1581,8 +1592,8 @@ bool SoundEditor::setup(Clip* clip, const MenuItem* item, int32_t sourceIndex) {
D_PRINTLN("must select range");

newRange = nullptr;
menu_item::multiRangeMenu.menuItemHeadingTo = newItem;
newItem = &menu_item::multiRangeMenu;
menu_item::multiRangeMenu.menuItemHeadingTo = item;
item = &menu_item::multiRangeMenu;
}

if (display->haveOLED()) {
Expand Down Expand Up @@ -1618,7 +1629,7 @@ bool SoundEditor::setup(Clip* clip, const MenuItem* item, int32_t sourceIndex) {

navigationDepth = 0;
shouldGoUpOneLevelOnBegin = false;
menuItemNavigationRecord[navigationDepth] = newItem;
menuItemNavigationRecord[navigationDepth] = item;

display->setNextTransitionDirection(1);

Expand Down
2 changes: 1 addition & 1 deletion src/deluge/gui/ui/sound_editor.h
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ class SoundEditor final : public UI {
bool pitchBendReceived(MIDICable& cable, uint8_t channel, uint8_t data1, uint8_t data2);
void selectEncoderAction(int8_t offset) override;
bool canSeeViewUnderneath() override { return true; }
bool setup(Clip* clip = nullptr, const MenuItem* item = nullptr, int32_t sourceIndex = 0);
bool setup(Clip* clip = nullptr, MenuItem* item = nullptr, int32_t sourceIndex = 0);
void enterOrUpdateSoundEditor(bool on);
void blinkShortcut();
ActionResult potentialShortcutPadAction(int32_t x, int32_t y, bool on);
Expand Down

0 comments on commit 6980a90

Please sign in to comment.