diff --git a/src-ui-wx/app-shell/KeyBindings.cpp b/src-ui-wx/app-shell/KeyBindings.cpp index 181a99cda1..098d25efc5 100644 --- a/src-ui-wx/app-shell/KeyBindings.cpp +++ b/src-ui-wx/app-shell/KeyBindings.cpp @@ -119,7 +119,7 @@ static std::vector> KeyBindingTypes = { "CANCEL_RENDER", KBSCOPE::Sequence }, { "TOGGLE_RENDER", KBSCOPE::Sequence }, { "PRESETS_TOGGLE", KBSCOPE::Sequence }, - { "FOCUS_SEQUENCER", KBSCOPE::All }, // This forces focus to the sequencer for situations where keys dont seem to work. It must be mapped to function key + { "FOCUS_SEQUENCER", KBSCOPE::All }, // This forces focus to the sequencer for situations where keys don't seem to work. It must be mapped to function key { "VALUECURVES_TOGGLE", KBSCOPE::Sequence }, { "COLOR_DROPPER_TOGGLE", KBSCOPE::Sequence }, { "AUDIO_FULL_SPEED", KBSCOPE::Sequence }, @@ -167,137 +167,137 @@ static std::vector> KeyBindingTypes = }; static std::vector> keyBindingTips = { - { "TIMING_ADD", "Add a timing mark." }, - { "TIMING_SPLIT", "Split a timing mark." }, - { "ZOOM_IN", "Zoom into the effects grid." }, - { "ZOOM_OUT", "Zoom out of the effects grid." }, - { "ZOOM_SEL", "Zoom so selected timeline fills the screen." }, - { "RANDOM", "Insert random effects." }, - { "RENDER_ALL", "Render all." }, - { "SAVE_CURRENT_TAB", "Save the currently selected tab." }, - { "LIGHTS_TOGGLE", "Toggle output to lights on/off." }, - { "OPEN_SEQUENCE", "Open a sequence." }, - { "CLOSE_SEQUENCE", "Close the open sequence." }, - { "NEW_SEQUENCE", "Create a new sequence." }, - { "PASTE_BY_CELL", "Put cut/copy/paste into Paste By Cell mode." }, - { "PASTE_BY_TIME", "Put cut/copy/paste into Paste By Time mode." }, - { "BACKUP", "Backup your show folder." }, - { "ALTERNATE_BACKUP", "Backup your show folder to the alternate backup location." }, - { "SELECT_SHOW_FOLDER", "Change your current show folder." }, - { "SAVEAS_SEQUENCE", "Save the current sequence to a new file." }, + { "TIMING_ADD", "Add a timing mark at the playback cursor on the active timing track." }, + { "TIMING_SPLIT", "Split the timing mark under the cursor into two at the cursor position." }, + { "ZOOM_IN", "Zoom the effects grid in for finer timing detail." }, + { "ZOOM_OUT", "Zoom the effects grid out to see more of the timeline." }, + { "ZOOM_SEL", "Zoom the timeline so the currently selected time range fills the grid." }, + { "RANDOM", "Insert randomly chosen effects into the selected cells." }, + { "RENDER_ALL", "Render every model in the sequence from scratch." }, + { "SAVE_CURRENT_TAB", "Save whichever tab is currently active (sequence, layout, or setup)." }, + { "LIGHTS_TOGGLE", "Turn live output to your controllers/lights on or off." }, + { "OPEN_SEQUENCE", "Open an existing sequence from the show folder." }, + { "CLOSE_SEQUENCE", "Close the currently open sequence (prompts to save if changed)." }, + { "NEW_SEQUENCE", "Create a new, empty sequence." }, + { "PASTE_BY_CELL", "Switch paste mode so copied effects align to timing cells rather than exact times." }, + { "PASTE_BY_TIME", "Switch paste mode so copied effects keep their original absolute times." }, + { "BACKUP", "Back up the show folder to the configured backup location." }, + { "ALTERNATE_BACKUP", "Back up the show folder to the alternate (secondary) backup location." }, + { "SELECT_SHOW_FOLDER", "Switch to a different show folder." }, + { "SAVEAS_SEQUENCE", "Save the current sequence under a new file name." }, { "SAVE_SEQUENCE", "Save the current sequence." }, - { "EFFECT_SETTINGS_TOGGLE", "Toggle display of the effect settings panel." }, - { "EFFECT_ASSIST_TOGGLE", "Toggle display of the effect assist panel." }, - { "COLOR_TOGGLE", "Toggle display of the color panel." }, - { "LAYER_SETTING_TOGGLE", "Toggle display of the layer settings panel." }, - { "LAYER_BLENDING_TOGGLE", "Toggle display of the layer blending panel." }, - { "MODEL_PREVIEW_TOGGLE", "Toggle display of the model preview panel." }, - { "HOUSE_PREVIEW_TOGGLE", "Toggle display of the house preview panel." }, - { "EFFECTS_TOGGLE", "Toggle display of the effect dropper panel." }, - { "DISPLAY_ELEMENTS_TOGGLE", "Toggle display of the display elements panel." }, - { "JUKEBOX_TOGGLE", "Toggle display of the jukebox panel." }, - { "SEQUENCE_SETTINGS", "Display the sequence settings." }, - { "LOCK_EFFECT", "Lock the selected effects." }, - { "UNLOCK_EFFECT", "Unlock the selected effects." }, - { "MARK_SPOT", "Mark the current spot in the sequencer so you can return to it." }, - { "RETURN_TO_SPOT", "Return to the previously marked spot in the sequencer." }, - { "EFFECT_DESCRIPTION", "Open the effect description dialog." }, - { "EFFECT_ALIGN_START", "Align the selected effects to have the same start times." }, - { "EFFECT_ALIGN_END", "Align the selected effects to have the same end times." }, - { "EFFECT_ALIGN_BOTH", "Align the selected effects to have the same start and end times." }, - { "INSERT_LAYER_ABOVE", "Insert a sequencing layer above the current row." }, - { "INSERT_LAYER_BELOW", "Insert a sequencing layer below the current row." }, - { "TOGGLE_ELEMENT_EXPAND", "Expand the current element." }, - { "SELECT_ALL", "Select all effects and timing marks." }, - { "SELECT_ALL_NO_TIMING", "Select all effects but not timing marks." }, - { "SHOW_PRESETS", "Show the effect presets panel." }, - { "SEARCH_TOGGLE", "Toggle display of the effect search panel." }, - { "FILTER_SEQUENCER", "Show/hide the sequencer prop filter box to jump to a prop." }, - { "PERSPECTIVES_TOGGLE", "Toggle display of the perspectives panel." }, - { "EFFECT_UPDATE", "Apply the current effect settings to all selected effects." }, - { "COLOR_UPDATE", "Apply the current colors to all selected effects." }, - { "PLAY_LOOP", "Play the selected part of the song repeatedly." }, - { "PLAY", "Play the song." }, - { "TOGGLE_PLAY", "Play/Stop playing the song." }, - { "START_OF_SONG", "Jump to the start of the song." }, - { "END_OF_SONG", "Jump to the end of the song." }, - { "STOP", "Stop sequence playback." }, - { "PAUSE", "Pause sequence playback." }, - { "EFFECT", "Insert an effect." }, - { "APPLYSETTING", "Apply setting to selected effects." }, - { "PRESET", "Insert a preset effect." }, - { "LOCK_MODEL", "Lock the selected models." }, - { "UNLOCK_MODEL", "Unlock the selected models." }, - { "GROUP_MODELS", "Create a group from the selected models." }, - { "WIRING_VIEW", "Display the wiring view for the selected model." }, - { "EXPORT_MODEL_CAD", "Export the selected model as a DXF or STL or VRML File." }, - { "EXPORT_LAYOUT_DXF", "Export the default layout as a DXF File." }, - { "NODE_LAYOUT", "Display the node layout for the selected model." }, - { "SAVE_LAYOUT", "Save the layout tab." }, - { "SELECT_ALL_MODELS", "Select all models." }, - { "MODEL_ALIGN_TOP", "Align the selected models to the top edge." }, - { "MODEL_ALIGN_BOTTOM", "Align the selected models to the bottom edge." }, - { "MODEL_ALIGN_LEFT", "Align the selected models to the left edge." }, - { "MODEL_ALIGN_RIGHT", "Align the selected models to the right edge." }, - { "MODEL_ALIGN_CENTER_VERT", "Align the selected models to be vertically centered." }, - { "MODEL_ALIGN_CENTER_HORIZ", "Align the selected models to be horizontally centered." }, - { "MODEL_ALIGN_FRONTS", "Align the selected models to the front edge." }, - { "MODEL_ALIGN_BACKS", "Align the selected models to the back edge." }, - { "MODEL_ALIGN_GROUND", "Align the selected models to the ground." }, - { "MODEL_DISTRIBUTE_HORIZ", "Distribute the selected model horizontally." }, - { "MODEL_DISTRIBUTE_VERT", "Distribute the selected models vertically." }, - { "MODEL_FLIP_HORIZ", "Flip the selected models horizontally." }, - { "MODEL_FLIP_VERT", "Flip the selected models vertically." }, - { "CANCEL_RENDER", "Cancel current rendering activity." }, - { "TOGGLE_RENDER", "Toggle background rendering." }, - { "PRESETS_TOGGLE", "Toggle display of the presets panel." }, - { "FOCUS_SEQUENCER", "Force keyboard focus to the effects gid." }, // This forces focus to the sequencer for situations where keys dont seem to work. It must be mapped to function key - { "VALUECURVES_TOGGLE", "Toggle display of the value curves droppper panel." }, - { "COLOR_DROPPER_TOGGLE", "Toggle display of the color dropper panel." }, - { "AUDIO_FULL_SPEED", "Playback audio at normal speed." }, - { "AUDIO_F_1_5_SPEED", "Playback audio at 1.5 times speed." }, - { "AUDIO_F_2_SPEED", "Playback audio at 2 times speed." }, - { "AUDIO_F_3_SPEED", "Playback audio at 3 times speed." }, - { "AUDIO_F_4_SPEED", "Playback audio at 4 times speed." }, - { "AUDIO_S_3_4_SPEED", "Playback audio at 3/4 speed." }, - { "AUDIO_S_1_2_SPEED", "Playback audio at 1/2 speed." }, - { "AUDIO_S_1_4_SPEED", "Playback audio at 1/4 speed." }, - { "PRIOR_TAG", "Jump to prior audio tag." }, - { "NEXT_TAG", "Jump to next audio tag." }, - { "PLAY_PRIOR_TAG", "Play from prior audio tag." }, - { "PLAY_NEXT_TAG", "Play from next audio tag." }, - { "MODEL_SUBMODELS", "Edit model submodels." }, - { "MODEL_FACES", "Edit model faces." }, - { "MODEL_STATES", "Edit model states." }, - { "MODEL_MODELDATA", "Edit custom model data." }, - { "MODEL_TOGGLE", "Toggle (Enable/Disable) rendering of the selected model in the sequencer" }, - { "MODEL_DISABLE", "Disable rendering of the selected model in the sequencer" }, - { "MODEL_ENABLE", "Enable rendering of the selected model in the sequencer" }, - { "EFFECT_TOGGLE", "Toggle (Enable/Disable) rendering of the selected effects in the sequencer" }, - { "EFFECT_DISABLE", "Disable rendering of the selected effects in the sequencer" }, - { "EFFECT_ENABLE", "Enable rendering of the selected effects in the sequencer" }, - { "MODEL_EFFECT_TOGGLE", "Toggle (Enable/Disable) rendering of the selected model or the effects in the sequencer" }, - { "EFFECTS_TO_TIMING", "Convert selected effects to timing marks." }, - { "SELECT_TIMING_1", "Select first timing." }, - { "SELECT_TIMING_2", "Select second timing." }, - { "SELECT_TIMING_3", "Select third timing." }, - { "SELECT_TIMING_4", "Select fourth timing." }, - { "SELECT_TIMING_5", "Select fifth timing." }, - { "SELECT_TIMING_6", "Select sixth timing." }, - { "SELECT_TIMING_7", "Select seventh timing." }, - { "SELECT_TIMING_8", "Select eighth timing." }, - { "SELECT_TIMING_9", "Select ninth timing." }, - { "SELECT_NO_TIMING", "Select no timing tracks." }, - { "INCREASE_SPEED", "Increase speed." }, - { "DECREASE_SPEED", "Decrease speed." }, - { "JUKEBOX_BTN_1", "Jukebox Button 1." }, - { "JUKEBOX_BTN_2", "Jukebox Button 2." }, - { "JUKEBOX_BTN_3", "Jukebox Button 3." }, - { "JUKEBOX_BTN_4", "Jukebox Button 4." }, - { "JUKEBOX_BTN_5", "Jukebox Button 5." }, - { "FPP_CONNECT", "Run FPP Connect" }, - { "COMMAND_PALETTE", "Open the command palette." }, - { "IMPORT_EFFECTS", "Open the Import Effects dialog." }, + { "EFFECT_SETTINGS_TOGGLE", "Show or hide the Effect Settings panel where the selected effect's options are edited." }, + { "EFFECT_ASSIST_TOGGLE", "Show or hide the Effect Assist panel (visual editor for effects that support it)." }, + { "COLOR_TOGGLE", "Show or hide the Colors panel used to set an effect's palette." }, + { "LAYER_SETTING_TOGGLE", "Show or hide the Layer Settings panel (buffer style, transform, blur, etc.)." }, + { "LAYER_BLENDING_TOGGLE", "Show or hide the Layer Blending panel that controls how stacked layers combine." }, + { "MODEL_PREVIEW_TOGGLE", "Show or hide the per-model preview panel." }, + { "HOUSE_PREVIEW_TOGGLE", "Show or hide the whole-house preview panel." }, + { "EFFECTS_TOGGLE", "Show or hide the Effects panel you drag effects from onto the grid." }, + { "DISPLAY_ELEMENTS_TOGGLE", "Show or hide the Display Elements panel for choosing which models/views appear in the grid." }, + { "JUKEBOX_TOGGLE", "Show or hide the Jukebox panel of saved effect buttons." }, + { "SEQUENCE_SETTINGS", "Open the Sequence Settings dialog (timing, media, metadata)." }, + { "LOCK_EFFECT", "Lock the selected effects so they can't be moved or edited." }, + { "UNLOCK_EFFECT", "Unlock the selected effects so they can be moved or edited again." }, + { "MARK_SPOT", "Remember the current timeline position so you can jump back to it later." }, + { "RETURN_TO_SPOT", "Jump back to the timeline position you previously marked." }, + { "EFFECT_DESCRIPTION", "Add or edit a text note/description on the selected effect." }, + { "EFFECT_ALIGN_START", "Move the selected effects so they all share the first one's start time." }, + { "EFFECT_ALIGN_END", "Move the selected effects so they all share the first one's end time." }, + { "EFFECT_ALIGN_BOTH", "Stretch the selected effects so they all share the first one's start and end times." }, + { "INSERT_LAYER_ABOVE", "Add a new, empty effect layer above the current model row." }, + { "INSERT_LAYER_BELOW", "Add a new, empty effect layer below the current model row." }, + { "TOGGLE_ELEMENT_EXPAND", "Expand or collapse the selected model row to show/hide its strands, submodels and nodes." }, + { "SELECT_ALL", "Select every effect and timing mark on the current row." }, + { "SELECT_ALL_NO_TIMING", "Select every effect on the current row, leaving timing marks unselected." }, + { "SHOW_PRESETS", "Open the Effect Presets panel of saved effect definitions." }, + { "SEARCH_TOGGLE", "Show or hide the effect search panel." }, + { "FILTER_SEQUENCER", "Jump to the sequencer's prop filter box to type a model name and narrow the visible rows." }, + { "PERSPECTIVES_TOGGLE", "Show or hide the Perspectives panel for saving/restoring panel layouts." }, + { "EFFECT_UPDATE", "Copy the current effect's settings onto all other selected effects." }, + { "COLOR_UPDATE", "Copy the current effect's colors onto all other selected effects." }, + { "PLAY_LOOP", "Play the selected time range over and over." }, + { "PLAY", "Start playback from the playback cursor." }, + { "TOGGLE_PLAY", "Start playback, or stop it if already playing." }, + { "START_OF_SONG", "Move the playback cursor to the start of the sequence." }, + { "END_OF_SONG", "Move the playback cursor to the end of the sequence." }, + { "STOP", "Stop playback and return output to idle." }, + { "PAUSE", "Pause playback, keeping the cursor where it is." }, + { "EFFECT", "Drop a specific effect (configured on this binding) into the selected cells." }, + { "APPLYSETTING", "Apply a saved setting (configured on this binding) to the selected effects." }, + { "PRESET", "Drop a saved preset effect (configured on this binding) into the selected cells." }, + { "LOCK_MODEL", "Lock the selected models so they can't be moved on the layout." }, + { "UNLOCK_MODEL", "Unlock the selected models so they can be moved again." }, + { "GROUP_MODELS", "Create a new model group containing the selected models." }, + { "WIRING_VIEW", "Open the wiring view showing node order and connections for the selected model." }, + { "EXPORT_MODEL_CAD", "Export the selected model as a CAD file (DXF, STL, or VRML)." }, + { "EXPORT_LAYOUT_DXF", "Export the entire default layout as a DXF file." }, + { "NODE_LAYOUT", "Open the node layout view showing each node's position for the selected model." }, + { "SAVE_LAYOUT", "Save changes made on the Layout tab." }, + { "SELECT_ALL_MODELS", "Select every model on the layout." }, + { "MODEL_ALIGN_TOP", "Align the selected models so their top edges match the first selected model." }, + { "MODEL_ALIGN_BOTTOM", "Align the selected models so their bottom edges match the first selected model." }, + { "MODEL_ALIGN_LEFT", "Align the selected models so their left edges match the first selected model." }, + { "MODEL_ALIGN_RIGHT", "Align the selected models so their right edges match the first selected model." }, + { "MODEL_ALIGN_CENTER_VERT", "Align the selected models to share the same vertical center line." }, + { "MODEL_ALIGN_CENTER_HORIZ", "Align the selected models to share the same horizontal center line." }, + { "MODEL_ALIGN_FRONTS", "Align the selected models' front (near) edges in 3D." }, + { "MODEL_ALIGN_BACKS", "Align the selected models' back (far) edges in 3D." }, + { "MODEL_ALIGN_GROUND", "Drop the selected models so they sit on the ground plane in 3D." }, + { "MODEL_DISTRIBUTE_HORIZ", "Space the selected models evenly left-to-right." }, + { "MODEL_DISTRIBUTE_VERT", "Space the selected models evenly top-to-bottom." }, + { "MODEL_FLIP_HORIZ", "Mirror the selected models left-to-right." }, + { "MODEL_FLIP_VERT", "Mirror the selected models top-to-bottom." }, + { "CANCEL_RENDER", "Cancel any rendering currently in progress." }, + { "TOGGLE_RENDER", "Turn automatic background rendering on or off." }, + { "PRESETS_TOGGLE", "Show or hide the presets panel." }, + { "FOCUS_SEQUENCER", "Force keyboard focus back to the effects grid when shortcuts stop responding (map this to a function key)." }, // This forces focus to the sequencer for situations where keys don't seem to work. It must be mapped to function key + { "VALUECURVES_TOGGLE", "Show or hide the Value Curves panel you drag value curves from." }, + { "COLOR_DROPPER_TOGGLE", "Show or hide the Color Dropper panel for picking colors." }, + { "AUDIO_FULL_SPEED", "Play audio back at normal (1x) speed." }, + { "AUDIO_F_1_5_SPEED", "Play audio back at 1.5x speed." }, + { "AUDIO_F_2_SPEED", "Play audio back at 2x speed." }, + { "AUDIO_F_3_SPEED", "Play audio back at 3x speed." }, + { "AUDIO_F_4_SPEED", "Play audio back at 4x speed." }, + { "AUDIO_S_3_4_SPEED", "Play audio back at 3/4 (0.75x) speed." }, + { "AUDIO_S_1_2_SPEED", "Play audio back at 1/2 (0.5x) speed." }, + { "AUDIO_S_1_4_SPEED", "Play audio back at 1/4 (0.25x) speed." }, + { "PRIOR_TAG", "Move the cursor to the previous audio tag." }, + { "NEXT_TAG", "Move the cursor to the next audio tag." }, + { "PLAY_PRIOR_TAG", "Start playback from the previous audio tag." }, + { "PLAY_NEXT_TAG", "Start playback from the next audio tag." }, + { "MODEL_SUBMODELS", "Open the submodels editor for the selected model." }, + { "MODEL_FACES", "Open the faces editor for the selected model." }, + { "MODEL_STATES", "Open the states editor for the selected model." }, + { "MODEL_MODELDATA", "Open the custom-model data grid for the selected model." }, + { "MODEL_TOGGLE", "Enable or disable rendering of the selected model in the sequence." }, + { "MODEL_DISABLE", "Disable rendering of the selected model so it produces no output." }, + { "MODEL_ENABLE", "Re-enable rendering of the selected model." }, + { "EFFECT_TOGGLE", "Enable or disable rendering of the selected effects." }, + { "EFFECT_DISABLE", "Disable the selected effects so they produce no output." }, + { "EFFECT_ENABLE", "Re-enable the selected effects." }, + { "MODEL_EFFECT_TOGGLE", "Enable/disable rendering of the selected effects, or the whole model if none are selected." }, + { "EFFECTS_TO_TIMING", "Create timing marks at the start/end of each selected effect." }, + { "SELECT_TIMING_1", "Make the 1st timing track the active one." }, + { "SELECT_TIMING_2", "Make the 2nd timing track the active one." }, + { "SELECT_TIMING_3", "Make the 3rd timing track the active one." }, + { "SELECT_TIMING_4", "Make the 4th timing track the active one." }, + { "SELECT_TIMING_5", "Make the 5th timing track the active one." }, + { "SELECT_TIMING_6", "Make the 6th timing track the active one." }, + { "SELECT_TIMING_7", "Make the 7th timing track the active one." }, + { "SELECT_TIMING_8", "Make the 8th timing track the active one." }, + { "SELECT_TIMING_9", "Make the 9th timing track the active one." }, + { "SELECT_NO_TIMING", "Deselect the active timing track." }, + { "INCREASE_SPEED", "Increase the playback speed." }, + { "DECREASE_SPEED", "Decrease the playback speed." }, + { "JUKEBOX_BTN_1", "Trigger Jukebox button 1." }, + { "JUKEBOX_BTN_2", "Trigger Jukebox button 2." }, + { "JUKEBOX_BTN_3", "Trigger Jukebox button 3." }, + { "JUKEBOX_BTN_4", "Trigger Jukebox button 4." }, + { "JUKEBOX_BTN_5", "Trigger Jukebox button 5." }, + { "FPP_CONNECT", "Open FPP Connect to upload sequences/config to your players." }, + { "COMMAND_PALETTE", "Open the command palette to search and run any command." }, + { "IMPORT_EFFECTS", "Open the Import Effects dialog to bring effects in from another sequence." }, { "ALTERNATE_PASTE", "Paste effects using the opposite of the configured 'Paste As' mode (Relative vs As Layers)." } }; diff --git a/src-ui-wx/app-shell/KeyBindingsDialog.h b/src-ui-wx/app-shell/KeyBindingsDialog.h new file mode 100644 index 0000000000..a64457e65d --- /dev/null +++ b/src-ui-wx/app-shell/KeyBindingsDialog.h @@ -0,0 +1,72 @@ +#pragma once + +/*************************************************************** + * This source files comes from the xLights project + * https://www.xlights.org + * https://github.com/xLightsSequencer/xLights + * See the github commit history for a record of contributing + * developers. + * Copyright claimed based on commit dates recorded in Github + * License: https://github.com/xLightsSequencer/xLights/blob/master/License.txt + **************************************************************/ + +#include + +#include +#include +#include +#include + +#include "preferences/KeyBindingsSettingsPanel.h" + +class xLightsFrame; + +// Standalone, MODELESS Key Bindings editor. Hosts the filterable, category- +// scoped, described KeyBindingsSettingsPanel so the window can stay open while +// you keep working in Layout/Sequencer. Edits apply to the live KeyBindingMap +// immediately; they are persisted (keyBindings.Save(), via the panel's +// TransferDataFromWindow) when the window is closed. +class KeyBindingsDialog : public wxDialog { +public: + KeyBindingsDialog(wxWindow* parent, xLightsFrame* frame, std::function onClosed) + : wxDialog(parent, wxID_ANY, _("Key Bindings"), + wxDefaultPosition, wxDefaultSize, + wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER) + { + auto* top = new wxBoxSizer(wxVERTICAL); + _panel = new KeyBindingsSettingsPanel(this, frame); + top->Add(_panel, 1, wxEXPAND | wxALL, 5); + + // Save commits the staged edits; Cancel (and closing the window) + // discards them - editing is not applied in real time. + auto* btnRow = new wxBoxSizer(wxHORIZONTAL); + btnRow->AddStretchSpacer(1); + auto* cancel = new wxButton(this, wxID_CANCEL, _("Cancel")); + auto* save = new wxButton(this, wxID_SAVE, _("Save")); + btnRow->Add(cancel, 0, wxRIGHT, 8); + btnRow->Add(save, 0); + top->Add(btnRow, 0, wxEXPAND | wxALL, 8); + save->SetDefault(); + + // Size from the panel's content (list columns + editor + wheel); do not + // hard-code a size, which used to open too small and squash the wheel. + SetSizerAndFit(top); + SetMinSize(GetSize()); + + Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { + if (_panel != nullptr) _panel->CommitChanges(); + Close(); + }, wxID_SAVE); + Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { Close(); }, wxID_CANCEL); + Bind(wxEVT_CLOSE_WINDOW, [this, onClosed](wxCloseEvent&) { + // No implicit save - closing without Save discards the working copy. + if (onClosed) { + onClosed(); + } + Destroy(); + }); + } + +private: + KeyBindingsSettingsPanel* _panel = nullptr; +}; diff --git a/src-ui-wx/preferences/KeyBindingsSettingsPanel.cpp b/src-ui-wx/preferences/KeyBindingsSettingsPanel.cpp new file mode 100644 index 0000000000..40573d4bab --- /dev/null +++ b/src-ui-wx/preferences/KeyBindingsSettingsPanel.cpp @@ -0,0 +1,970 @@ + +/*************************************************************** + * This source file comes from the xLights project + * https://www.xlights.org + * https://github.com/xLightsSequencer/xLights + * See the github commit history for a record of contributing + * developers. + * Copyright claimed based on commit dates recorded in Github + * License: https://github.com/xLightsSequencer/xLights/blob/master/License.txt + **************************************************************/ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "KeyBindingsSettingsPanel.h" +#include "app-shell/KeyBindings.h" +#include "effects/EffectManager.h" +#include "effects/RenderableEffect.h" +#include "xLightsMain.h" +#include "sequencer/MainSequencer.h" + + +KeyBindingsSettingsPanel::KeyBindingsSettingsPanel(wxWindow* parent, xLightsFrame* f, wxWindowID id, const wxPoint& pos, const wxSize& size) +{ + Create(parent, id, pos, size, wxTAB_TRAVERSAL, _T("KeyBindingsSettingsPanel")); + _xLights = f; + _liveKeyBindings = &f->GetMainSequencer()->keyBindings; + _working = *_liveKeyBindings; // edit a copy; commit only on Save + _keyBindings = &_working; + _effectManager = &f->GetEffectManager(); + + auto* topSizer = new wxBoxSizer(wxVERTICAL); + + auto* topRow = new wxFlexGridSizer(0, 2, 0, 0); + topRow->AddGrowableCol(1); + topRow->Add(new wxStaticText(this, wxID_ANY, _("Category:")), 0, wxALL | wxALIGN_CENTER_VERTICAL, 5); + Choice_Category = new wxChoice(this, wxID_ANY); + Choice_Category->AppendString(_("All")); + Choice_Category->AppendString(_("Effects / Wheel of Effects")); + Choice_Category->AppendString(_("Presets")); + Choice_Category->AppendString(_("Apply Settings")); + Choice_Category->AppendString(_("Commands")); + Choice_Category->SetStringSelection(_("All")); + topRow->Add(Choice_Category, 1, wxALL | wxEXPAND, 5); + topRow->Add(new wxStaticText(this, wxID_ANY, _("Scope:")), 0, wxALL | wxALIGN_CENTER_VERTICAL, 5); + Choice_Scope = new wxChoice(this, wxID_ANY); + Choice_Scope->AppendString("All"); + Choice_Scope->AppendString("Layout"); + Choice_Scope->AppendString("Sequencer"); + Choice_Scope->AppendString("All tabs"); + Choice_Scope->SetStringSelection("All"); + topRow->Add(Choice_Scope, 1, wxALL | wxEXPAND, 5); + topRow->Add(new wxStaticText(this, wxID_ANY, _("Filter:")), 0, wxALL | wxALIGN_CENTER_VERTICAL, 5); + _filterCtrl = new wxSearchCtrl(this, wxID_ANY); + _filterCtrl->ShowCancelButton(true); + _filterCtrl->SetDescriptiveText(_("Filter actions, shortcuts or descriptions")); + topRow->Add(_filterCtrl, 1, wxALL | wxEXPAND, 5); + topSizer->Add(topRow, 0, wxEXPAND); + + // wxDataViewListCtrl gives native drag-reorder with a drop line, and keys + // live in the two right-most columns (modifiers + key). Grip glyph column + // (only on effect rows) signals which rows can be dragged. + _dvList = new wxDataViewListCtrl(this, wxID_ANY, wxDefaultPosition, wxSize(-1, 360), wxDV_ROW_LINES | wxDV_SINGLE); + _colGrip = _dvList->AppendTextColumn(wxEmptyString, wxDATAVIEW_CELL_INERT, FromDIP(30), wxALIGN_CENTER); + _colAction = _dvList->AppendTextColumn(_("Action"), wxDATAVIEW_CELL_INERT, FromDIP(170), wxALIGN_LEFT, wxDATAVIEW_COL_RESIZABLE); + _colPos = _dvList->AppendTextColumn(_("Position"), wxDATAVIEW_CELL_INERT, FromDIP(84), wxALIGN_CENTER); // wheel clock position (effects) + _colDetails = _dvList->AppendTextColumn(_("Details"), wxDATAVIEW_CELL_INERT, FromDIP(260), wxALIGN_LEFT, wxDATAVIEW_COL_RESIZABLE); + _colMods = _dvList->AppendTextColumn(_("Modifiers"), wxDATAVIEW_CELL_INERT, FromDIP(84), wxALIGN_CENTER); + _colKey = _dvList->AppendTextColumn(_("Key"), wxDATAVIEW_CELL_INERT, FromDIP(64), wxALIGN_CENTER); + // Trailing spacer: wxDataViewCtrl stretches its LAST column to fill the + // control, which made Key absurdly wide and squeezed the others. Giving it + // an empty column to stretch instead lets every real column keep its width. + _dvList->AppendTextColumn(wxEmptyString, wxDATAVIEW_CELL_INERT, FromDIP(1), wxALIGN_LEFT); + _dvList->EnableDragSource(wxDF_UNICODETEXT); + _dvList->EnableDropTarget(wxDF_UNICODETEXT); + _dvList->Bind(wxEVT_SIZE, [this](wxSizeEvent& e) { e.Skip(); FitColumns(); }); + // List on the left, edit column on the right (like the original dialog). + auto* mainRow = new wxBoxSizer(wxHORIZONTAL); + mainRow->Add(_dvList, 1, wxEXPAND | wxALL, 4); + + auto* editBox = new wxStaticBoxSizer(wxVERTICAL, this, _("Edit selected binding")); + _editorTitle = new wxStaticText(this, wxID_ANY, _("Select a binding to edit it.")); + _editorTitle->Wrap(FromDIP(230)); + editBox->Add(_editorTitle, 0, wxALL, 4); + + auto* grid = new wxFlexGridSizer(0, 2, 5, 8); + grid->AddGrowableCol(1); + auto addLabel = [&](const wxString& t) { grid->Add(new wxStaticText(this, wxID_ANY, t), 0, wxALIGN_CENTER_VERTICAL); }; + + addLabel(_("Key:")); + _shortcutField = new wxTextCtrl(this, wxID_ANY, "", wxDefaultPosition, FromDIP(wxSize(96, -1)), wxTE_READONLY | wxTE_CENTRE); + _shortcutField->SetHint(_("press a key")); + grid->Add(_shortcutField, 0); // not wxEXPAND: a single keystroke needs no more + addLabel(_("Control:")); + _cbControl = new wxCheckBox(this, wxID_ANY, wxEmptyString); + grid->Add(_cbControl, 0); + addLabel(_("Alt:")); + _cbAlt = new wxCheckBox(this, wxID_ANY, wxEmptyString); + grid->Add(_cbAlt, 0); + addLabel(_("Shift:")); + _cbShift = new wxCheckBox(this, wxID_ANY, wxEmptyString); + grid->Add(_cbShift, 0); + addLabel(_("Raw Control:")); + _cbRawControl = new wxCheckBox(this, wxID_ANY, wxEmptyString); + grid->Add(_cbRawControl, 0); + + _valueLabel = new wxStaticText(this, wxID_ANY, _("Effect:")); + grid->Add(_valueLabel, 0, wxALIGN_CENTER_VERTICAL); + _effectChoice = new wxChoice(this, wxID_ANY); + _effectChoice->SetMaxSize(FromDIP(wxSize(220, -1))); + _effectChoice->Append(""); + for (const auto& it : *_effectManager) _effectChoice->Append(it->Name()); + _presetChoice = new wxChoice(this, wxID_ANY); + _presetChoice->SetMaxSize(FromDIP(wxSize(220, -1))); + _presetChoice->Append(""); + for (const auto& it : f->GetPresets()) _presetChoice->Append(it); + _settingCtrl = new wxTextCtrl(this, wxID_ANY, ""); + auto* valueSizer = new wxBoxSizer(wxHORIZONTAL); + valueSizer->Add(_effectChoice, 1, wxEXPAND); + valueSizer->Add(_presetChoice, 1, wxEXPAND); + valueSizer->Add(_settingCtrl, 1, wxEXPAND); + grid->Add(valueSizer, 1, wxEXPAND); + editBox->Add(grid, 0, wxEXPAND | wxALL, 4); + + _clearShortcutBtn = new wxButton(this, wxID_ANY, _("Clear Shortcut")); + editBox->Add(_clearShortcutBtn, 0, wxLEFT | wxTOP | wxBOTTOM, 4); + + // Fixed-width right column. Its content (wrapped description text, and the + // wheel appearing/disappearing) would otherwise change its width on every + // selection, resizing the list and resetting the Details column. + const int kRightW = FromDIP(260); + _editorTitle->SetMinSize(wxSize(kRightW - FromDIP(20), -1)); + editBox->SetMinSize(wxSize(kRightW, -1)); + auto* rightCol = new wxBoxSizer(wxVERTICAL); + rightCol->SetMinSize(wxSize(kRightW, -1)); + rightCol->Add(editBox, 0, wxEXPAND); + _wheelPanel = new wxPanel(this, wxID_ANY); + _wheelPanel->SetMinSize(wxSize(kRightW - FromDIP(16), FromDIP(230))); + _wheelPanel->SetBackgroundStyle(wxBG_STYLE_PAINT); + _wheelPanel->Bind(wxEVT_PAINT, &KeyBindingsSettingsPanel::OnPaintWheel, this); + _wheelBox = new wxStaticBoxSizer(wxVERTICAL, this, _("Wheel of Effects")); + _wheelBox->Add(_wheelPanel, 1, wxEXPAND | wxALL, 2); + rightCol->Add(_wheelBox, 1, wxEXPAND | wxTOP, 8); + mainRow->Add(rightCol, 0, wxEXPAND | wxLEFT | wxRIGHT, 4); + topSizer->Add(mainRow, 1, wxEXPAND); + + auto* btnRow = new wxBoxSizer(wxHORIZONTAL); + _moveUpBtn = new wxButton(this, wxID_ANY, wxString(wxUniChar(0x2191)) + _(" Move Up")); + _moveDownBtn = new wxButton(this, wxID_ANY, wxString(wxUniChar(0x2193)) + _(" Move Down")); + auto* addEffect = new wxButton(this, wxID_ANY, _("Add Effect")); + auto* addPreset = new wxButton(this, wxID_ANY, _("Add Preset")); + auto* addApply = new wxButton(this, wxID_ANY, _("Add Apply Setting")); + auto* removeBtn = new wxButton(this, wxID_ANY, _("Remove")); + btnRow->Add(_moveUpBtn, 0, wxRIGHT, 6); + btnRow->Add(_moveDownBtn, 0, wxRIGHT, 16); + btnRow->Add(addEffect, 0, wxRIGHT, 6); + btnRow->Add(addPreset, 0, wxRIGHT, 6); + btnRow->Add(addApply, 0, wxRIGHT, 16); + btnRow->Add(removeBtn, 0); + topSizer->Add(btnRow, 0, wxALIGN_CENTER_HORIZONTAL | wxALL, 6); + _moveUpBtn->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { MoveSelectedEffect(-1); }); + _moveDownBtn->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { MoveSelectedEffect(1); }); + removeBtn->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { RemoveSelected(); }); + + auto* note = new wxStaticText(this, wxID_ANY, + _("Select a binding, click Key and press a key, and tick the modifiers. " + "Effect bindings (grip shown) can be reordered with Move Up/Down or by " + "dragging - the order carries to the Wheel of Effects.")); + note->SetForegroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_GRAYTEXT)); + note->Wrap(700); + topSizer->Add(note, 0, wxLEFT | wxRIGHT | wxBOTTOM, 8); + + SetSizer(topSizer); + // Wide enough for the list columns plus the right-hand editor + wheel. + SetMinSize(FromDIP(wxSize(1060, 620))); + + LoadList(); + + Choice_Category->Bind(wxEVT_CHOICE, [this](wxCommandEvent&) { LoadList(); PopulateEditor(); UpdateWheelVisibility(); }); + Choice_Scope->Bind(wxEVT_CHOICE, &KeyBindingsSettingsPanel::OnChoice_ScopeSelect, this); + _filterCtrl->Bind(wxEVT_TEXT, [this](wxCommandEvent&) { _filter = _filterCtrl->GetValue().Lower(); LoadList(); PopulateEditor(); }); + _filterCtrl->Bind(wxEVT_SEARCHCTRL_CANCEL_BTN, [this](wxCommandEvent&) { _filterCtrl->ChangeValue(""); _filter.clear(); LoadList(); PopulateEditor(); }); + _dvList->Bind(wxEVT_DATAVIEW_SELECTION_CHANGED, [this](wxDataViewEvent&) { PopulateEditor(); }); + _dvList->Bind(wxEVT_DATAVIEW_ITEM_ACTIVATED, [this](wxDataViewEvent&) { if (_shortcutField->IsEnabled()) _shortcutField->SetFocus(); }); + _dvList->Bind(wxEVT_DATAVIEW_ITEM_BEGIN_DRAG, &KeyBindingsSettingsPanel::OnDvBeginDrag, this); + _dvList->Bind(wxEVT_DATAVIEW_ITEM_DROP_POSSIBLE, &KeyBindingsSettingsPanel::OnDvDropPossible, this); + _dvList->Bind(wxEVT_DATAVIEW_ITEM_DROP, &KeyBindingsSettingsPanel::OnDvDrop, this); + _shortcutField->Bind(wxEVT_KEY_DOWN, &KeyBindingsSettingsPanel::OnShortcutKey, this); + _clearShortcutBtn->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { ClearShortcut(); }); + auto onModifier = [this](wxCommandEvent&) { + KeyBinding* b = SelectedBinding(); + if (b == nullptr) return; + b->SetControl(_cbControl->GetValue()); + b->SetAlt(_cbAlt->GetValue()); + b->SetShift(_cbShift->GetValue()); + b->SetRawControl(_cbRawControl->GetValue()); + int index = GetSelectedKeyBindingIndex(); + if (index >= 0) RefreshRow(index, *b); + }; + _cbControl->Bind(wxEVT_CHECKBOX, onModifier); + _cbAlt->Bind(wxEVT_CHECKBOX, onModifier); + _cbShift->Bind(wxEVT_CHECKBOX, onModifier); + _cbRawControl->Bind(wxEVT_CHECKBOX, onModifier); + _effectChoice->Bind(wxEVT_CHOICE, [this](wxCommandEvent&) { ApplyEffectChoice(); }); + _presetChoice->Bind(wxEVT_CHOICE, [this](wxCommandEvent&) { ApplyEffectChoice(); }); + _settingCtrl->Bind(wxEVT_TEXT, [this](wxCommandEvent&) { ApplyEffectChoice(); }); + addEffect->Bind(wxEVT_BUTTON, &KeyBindingsSettingsPanel::OnButton_AddEffectClick, this); + addPreset->Bind(wxEVT_BUTTON, &KeyBindingsSettingsPanel::OnButtonAddPresetClick, this); + addApply->Bind(wxEVT_BUTTON, &KeyBindingsSettingsPanel::OnButtonAddApplySettingClick, this); + + PopulateEditor(); + UpdateWheelVisibility(); + // Fit again once the dialog has actually been laid out - at this point the + // list still reports its pre-layout width. + CallAfter([this]() { FitColumns(); }); +} + +KeyBinding* KeyBindingsSettingsPanel::SelectedBinding() const +{ + const long id = RowBindingId(GetSelectedKeyBindingIndex()); + if (id < 0) return nullptr; + return &_keyBindings->GetBinding((int)id); +} + +long KeyBindingsSettingsPanel::RowBindingId(int row) const +{ + if (row < 0 || row >= (int)_rowIds.size()) return -1; + return _rowIds[row]; +} + +void KeyBindingsSettingsPanel::SyncModifierChecks(const KeyBinding& b) +{ + _cbControl->SetValue(b.RequiresControl()); + _cbAlt->SetValue(b.RequiresAlt()); + _cbShift->SetValue(b.RequiresShift()); + _cbRawControl->SetValue(b.RequiresRawControl()); +} + +void KeyBindingsSettingsPanel::PopulateEditor() +{ + if (_wheelPanel != nullptr) _wheelPanel->Refresh(); // highlight the selection + KeyBinding* b = SelectedBinding(); + const bool has = (b != nullptr); + _shortcutField->Enable(has); + _clearShortcutBtn->Enable(has); + _cbControl->Enable(has); + _cbAlt->Enable(has); + _cbShift->Enable(has); + _cbRawControl->Enable(has); + const bool selEffect = has && b->GetType() == "EFFECT"; + if (_moveUpBtn != nullptr) _moveUpBtn->Enable(selEffect); + if (_moveDownBtn != nullptr) _moveDownBtn->Enable(selEffect); + if (!has) { + _editorTitle->SetLabel(_("Select a binding to edit it.")); + _shortcutField->ChangeValue(""); + _cbControl->SetValue(false); + _cbAlt->SetValue(false); + _cbShift->SetValue(false); + _cbRawControl->SetValue(false); + _effectChoice->Hide(); + _presetChoice->Hide(); + _settingCtrl->Hide(); + _valueLabel->Hide(); + if (_lastEditorShape != 0) { + _lastEditorShape = 0; + Layout(); + } + return; + } + + const std::string type = b->GetType(); + _editorTitle->SetLabel(FriendlyName(type) + "\n" + wxString::FromUTF8(b->GetTip())); + _editorTitle->Wrap(_editorTitle->GetMinSize().GetWidth()); + _shortcutField->ChangeValue(RenderKey(*b)); + SyncModifierChecks(*b); + + const bool isEffect = (type == "EFFECT"); + const bool isPreset = (type == "PRESET"); + const bool isSetting = (type == "EFFECT" || type == "APPLYSETTING"); + _effectChoice->Show(isEffect); + _presetChoice->Show(isPreset); + _settingCtrl->Show(isSetting); + _valueLabel->Show(isEffect || isPreset || isSetting); + if (isEffect) { + _valueLabel->SetLabel(_("Effect:")); + if (_effectChoice->SetStringSelection(b->GetEffectName()) == false) _effectChoice->SetSelection(0); + } else if (isPreset) { + _valueLabel->SetLabel(_("Preset:")); + if (_presetChoice->SetStringSelection(b->GetEffectName()) == false) _presetChoice->SetSelection(0); + } + if (isSetting) { + if (!isEffect) _valueLabel->SetLabel(_("Setting:")); + _settingCtrl->ChangeValue(b->GetEffectString()); + } + // Only relayout when the visible control set changed - laying out on every + // row click resized the list and reset the Details column width. + const int shape = 1 | (isEffect ? 2 : 0) | (isPreset ? 4 : 0) | (isSetting ? 8 : 0); + if (shape != _lastEditorShape) { + _lastEditorShape = shape; + Layout(); + } +} + +void KeyBindingsSettingsPanel::OnShortcutKey(wxKeyEvent& event) +{ + KeyBinding* b = SelectedBinding(); + if (b == nullptr) { event.Skip(); return; } + const int kc = event.GetKeyCode(); + // Ignore pure modifier presses - wait for the actual key. + if (kc == WXK_SHIFT || kc == WXK_CONTROL || kc == WXK_ALT || kc == WXK_RAW_CONTROL || kc == WXK_NONE) { + return; + } + if (kc == WXK_ESCAPE) { return; } // leave unchanged + b->SetKey((wxKeyCode)kc); + // Reflect any modifiers held during the press into the checkboxes too, so + // pressing the whole combo works as well as ticking modifiers by hand. + b->SetShift(event.ShiftDown()); + b->SetAlt(event.AltDown()); + b->SetControl(event.CmdDown()); // Cmd on macOS, Ctrl elsewhere + b->SetRawControl(event.RawControlDown()); // physical Ctrl on macOS + _shortcutField->ChangeValue(RenderKey(*b)); + SyncModifierChecks(*b); + // Assigning a key enables the binding, which can put it on the wheel and + // renumber every other effect's slot - rebuild the list, not just one row. + const int keptId = (int)b->GetId(); + LoadList(); + SelectKey(keptId); +} + +void KeyBindingsSettingsPanel::ClearShortcut() +{ + KeyBinding* b = SelectedBinding(); + if (b == nullptr) return; + b->SetKey(WXK_NONE); + b->SetShift(false); + b->SetAlt(false); + b->SetControl(false); + b->SetRawControl(false); + _shortcutField->ChangeValue(RenderKey(*b)); + SyncModifierChecks(*b); + // Clearing the key disables the binding, dropping it off the wheel and + // renumbering the rest - same full rebuild as assigning one. + const int keptId = (int)b->GetId(); + LoadList(); + SelectKey(keptId); +} + +void KeyBindingsSettingsPanel::ApplyEffectChoice() +{ + KeyBinding* b = SelectedBinding(); + if (b == nullptr) return; + const std::string type = b->GetType(); + if (type == "EFFECT") { + b->SetEffectName(_effectChoice->GetStringSelection().ToStdString()); + } else if (type == "PRESET") { + b->SetEffectName(_presetChoice->GetStringSelection().ToStdString()); + } + if (type == "EFFECT" || type == "APPLYSETTING") { + b->SetEffectString(_settingCtrl->GetValue().ToStdString()); + } + int index = GetSelectedKeyBindingIndex(); + if (index >= 0) RefreshRow(index, *b); +} + +void KeyBindingsSettingsPanel::OnDvBeginDrag(wxDataViewEvent& event) +{ + // Only effect bindings are reorderable. + const long id = RowBindingId(_dvList->ItemToRow(event.GetItem())); + if (id < 0 || _keyBindings->GetBinding((int)id).GetType() != "EFFECT") { + event.Veto(); + _dragFromId = -1; + return; + } + _dragFromId = id; + event.SetDataObject(new wxTextDataObject(wxString::Format("%ld", id))); + event.SetDragFlags(wxDrag_DefaultMove); +} + +void KeyBindingsSettingsPanel::MoveSelectedEffect(int delta) +{ + KeyBinding* sel = SelectedBinding(); + if (sel == nullptr || sel->GetType() != "EFFECT") return; + const long id = (long)sel->GetId(); + auto& v = _keyBindings->GetBindings(); + int selIdx = -1; + for (int i = 0; i < (int)v.size(); ++i) if ((long)v[i].GetId() == id) { selIdx = i; break; } + if (selIdx < 0) return; + // Swap with the nearest other EFFECT binding in the requested direction, + // so the effects reorder among themselves regardless of the filter. + int j = selIdx + delta; + while (j >= 0 && j < (int)v.size() && v[j].GetType() != "EFFECT") j += delta; + if (j < 0 || j >= (int)v.size()) return; + std::swap(v[selIdx], v[j]); + LoadList(); + SelectKey((int)id); + PopulateEditor(); +} + +void KeyBindingsSettingsPanel::OnDvDropPossible(wxDataViewEvent& event) +{ + // wxDataViewCtrl draws the drop line only where a drop is allowed - allow + // it only onto another effect row. + const long id = RowBindingId(_dvList->ItemToRow(event.GetItem())); + if (id < 0 || _keyBindings->GetBinding((int)id).GetType() != "EFFECT") { + event.Veto(); + } +} + +void KeyBindingsSettingsPanel::OnDvDrop(wxDataViewEvent& event) +{ + const long fromId = _dragFromId; + _dragFromId = -1; + if (fromId < 0) return; + const long toId = RowBindingId(_dvList->ItemToRow(event.GetItem())); + if (toId < 0 || toId == fromId) return; + if (_keyBindings->GetBinding((int)toId).GetType() != "EFFECT") return; // effects only + + // Move the dragged binding to just before the drop target in the backing + // vector, then rebuild the list. Persisted order = vector order. + auto& v = _keyBindings->GetBindings(); + auto findById = [&](long id) -> int { + for (int i = 0; i < (int)v.size(); ++i) if ((long)v[i].GetId() == id) return i; + return -1; + }; + int from = findById(fromId); + int to = findById(toId); + if (from < 0 || to < 0 || from == to) return; + KeyBinding moved = v[from]; + v.erase(v.begin() + from); + if (from < to) --to; + v.insert(v.begin() + to, moved); + LoadList(); + SelectKey((int)fromId); + PopulateEditor(); +} + +void KeyBindingsSettingsPanel::RemoveSelected() +{ + KeyBinding* b = SelectedBinding(); + if (b == nullptr) return; + // Only the user-added effect/preset/apply bindings can be removed; the + // built-in command bindings are fixed (matches the old Delete-key rule). + const std::string type = b->GetType(); + if (type != "EFFECT" && type != "PRESET" && type != "APPLYSETTING") { wxBell(); return; } + const long id = (long)b->GetId(); + auto& v = _keyBindings->GetBindings(); + for (int i = 0; i < (int)v.size(); ++i) { + if ((long)v[i].GetId() == id) { v.erase(v.begin() + i); break; } + } + LoadList(); + PopulateEditor(); +} + +int KeyBindingsSettingsPanel::GetSelectedKeyBindingIndex() const { + return _dvList->GetSelectedRow(); // wxNOT_FOUND (-1) when nothing is selected +} + +void KeyBindingsSettingsPanel::RefreshRow(int row, const KeyBinding& b) +{ + if (row < 0) return; + _dvList->SetValue(wxVariant(BuildDetails(b)), row, 3); + _dvList->SetValue(wxVariant(RenderModifiers(b)), row, 4); + _dvList->SetValue(wxVariant(RenderKey(b)), row, 5); + RefreshDuplicateHighlights(); // re-applies the conflict marker across rows +} + +KeyBindingsSettingsPanel::~KeyBindingsSettingsPanel() +{ + //(*Destroy(KeyBindingsSettingsPanel) + //*) +} + +// Category dropdown index (0 = All): 1 Effects, 2 Presets, 3 Apply Settings, +// 4 Commands. Compared by index so the filter is locale-independent (the visible +// labels are translated via _()). +int KeyBindingsSettingsPanel::CategoryIndexOf(const std::string& type) +{ + if (type == "EFFECT") return 1; + if (type == "PRESET") return 2; + if (type == "APPLYSETTING") return 3; + return 4; +} + +static KBSCOPE EncodeScope(const wxString& scope) +{ + if (scope == "Controller") return KBSCOPE::Setup; + if (scope == "Layout") return KBSCOPE::Layout; + if (scope == "Sequencer") return KBSCOPE::Sequence; + return KBSCOPE::All; +} + +void KeyBindingsSettingsPanel::LoadList() +{ + _dvList->Freeze(); + _dvList->DeleteAllItems(); + _rowIds.clear(); + const wxString scopeSel = Choice_Scope->GetStringSelection(); + const bool showAll = (scopeSel == "All"); + const KBSCOPE scope = EncodeScope(scopeSel); + const int categorySel = Choice_Category->GetSelection(); // 0 = All + + // Collect the visible rows first so the Action column can be shown + // alphabetically regardless of the bindings' storage order. + struct Row { + wxString friendly; + wxString shortcut; + wxString details; + long id; + const KeyBinding* binding; + }; + std::vector rows; + for (const auto& it : _keyBindings->GetBindings()) + { + if (!showAll && !it.InScope(scope)) + continue; + if (categorySel > 0 && CategoryIndexOf(it.GetType()) != categorySel) + continue; + + const wxString friendly = FriendlyName(it.GetType()); + const wxString shortcut = RenderShortcut(it); + const wxString details = BuildDetails(it); + + // Whitespace-tokenised AND filter over action / type / shortcut / details. + if (!_filter.empty()) { + const wxString hay = (friendly + " " + it.GetType() + " " + shortcut + " " + details).Lower(); + bool match = true; + wxStringTokenizer tok(_filter, " "); + while (tok.HasMoreTokens()) { + if (hay.Find(tok.GetNextToken()) == wxNOT_FOUND) { match = false; break; } + } + if (!match) continue; + } + + rows.push_back({ friendly, shortcut, details, (long)it.GetId(), &it }); + } + + // No alphabetical sort: rows follow the backing vector order so drag-to- + // reorder is meaningful and persists. + const wxString grip = wxUniChar(0x2261); // triple-bar drag handle glyph + const wxString warn = wxString(wxUniChar(0x26A0)) + " "; // conflict marker + + // Wheel-of-Effects clock position. Only the bindings the wheel actually + // takes get a position; effects past the cut-off are flagged as off-wheel. + // Exact clock time, not the nearest hour: with 18 slots several adjacent + // slots round to the same hour, so reordering looked like it did nothing. + auto clockLabel = [](int i, int n) -> wxString { + if (n <= 0) return wxEmptyString; + const int mins = (int)std::lround(i * 720.0 / n); // 720 min = 12 hours + int hour = (mins / 60) % 12; + if (hour == 0) hour = 12; + return wxString::Format("%d:%02d", hour, mins % 60); + }; + std::map effectPos; + { + const auto wheel = WheelBindings(); + for (int i = 0; i < (int)wheel.size(); ++i) { + effectPos[(long)wheel[i]->GetId()] = clockLabel(i, (int)wheel.size()); + } + } + + for (const auto& r : rows) + { + // Grip only on effect rows - they're the only reorderable ones. + const bool reorderable = (r.binding->GetType() == "EFFECT"); + const bool dup = r.binding->GetKey() != WXK_NONE && _keyBindings->IsDuplicateKey(*r.binding); + wxVector cols; + cols.push_back(wxVariant(reorderable ? grip : wxString())); + cols.push_back(wxVariant(r.friendly)); + // Distinguish the two reasons an effect has no wheel slot: it has no + // key yet (a keyless binding is disabled, so the wheel skips it), or + // it sits past the wheel's slot limit. + wxString posText; + if (reorderable) { + if (effectPos.count(r.id)) posText = effectPos[r.id]; + else if (r.binding->GetKey() == WXK_NONE) posText = _("needs a key"); + else posText = _("not on wheel"); + } + cols.push_back(wxVariant(posText)); + cols.push_back(wxVariant(r.details)); + cols.push_back(wxVariant(RenderModifiers(*r.binding))); + cols.push_back(wxVariant((dup ? warn : wxString()) + RenderKey(*r.binding))); + cols.push_back(wxVariant(wxString())); // trailing spacer column + _dvList->AppendItem(cols); + _rowIds.push_back(r.id); + } + if (rows.empty()) { + // Explain the blank table rather than leaving the user guessing - some + // Category/Scope pairs genuinely have no bindings (e.g. effects are all + // Sequencer-scoped, so Effects + Layout is always empty). + wxVector cols; + cols.push_back(wxVariant(wxString())); + cols.push_back(wxVariant(_("(none)"))); + cols.push_back(wxVariant(wxString())); + cols.push_back(wxVariant(_("No bindings match this Category / Scope / Filter combination."))); + cols.push_back(wxVariant(wxString())); + cols.push_back(wxVariant(wxString())); + cols.push_back(wxVariant(wxString())); + _dvList->AppendItem(cols); + _rowIds.push_back(-1); // sentinel: not a real binding, never selectable + } + if (!_rowIds.empty() && _rowIds[0] >= 0 && _dvList->GetSelectedRow() == wxNOT_FOUND) { + _dvList->SelectRow(0); + } + _dvList->Thaw(); + if (_wheelPanel != nullptr) _wheelPanel->Refresh(); // effect order may have changed +} + +wxString KeyBindingsSettingsPanel::BuildDetails(const KeyBinding& b) const +{ + wxString details = b.GetTip(); + wxString effect; + if (b.GetEffectName() != "" && b.GetEffectString() != "") { + effect = b.GetEffectName() + ":" + b.GetEffectString(); + } else if (b.GetEffectString() != "") { + effect = b.GetEffectString(); + } else if (b.GetEffectName() != "") { + effect = b.GetEffectName(); + } + if (!effect.empty()) { + details = details.empty() ? effect : details + " (" + effect + ")"; + } + return details; +} + +// Re-apply the conflict marker (a changed key can create or resolve a clash on +// another row) by rewriting each row's Key cell in place. +void KeyBindingsSettingsPanel::RefreshDuplicateHighlights() +{ + const wxString warn = wxString(wxUniChar(0x26A0)) + " "; + for (int i = 0; i < (int)_rowIds.size(); ++i) { + const KeyBinding& rb = _keyBindings->GetBinding((int)_rowIds[i]); + const bool dup = rb.GetKey() != WXK_NONE && _keyBindings->IsDuplicateKey(rb); + _dvList->SetValue(wxVariant((dup ? warn : wxString()) + RenderKey(rb)), i, 5); + } +} + +// Give Details the space left over after the fixed columns. SetWidth can fire +// another size event, so guard against re-entering. +void KeyBindingsSettingsPanel::FitColumns() +{ + if (_dvList == nullptr || _fitting || _colDetails == nullptr) return; + const int client = _dvList->GetClientSize().GetWidth(); + if (client < FromDIP(200)) return; // not laid out yet - a stale width here + // made Details too wide and pushed the + // Modifiers/Key columns off-screen + _fitting = true; + const int wMods = FromDIP(84), wKey = FromDIP(64); + const int wGrip = _colGrip->IsHidden() ? 0 : FromDIP(30); + const int wPos = _colPos->IsHidden() ? 0 : FromDIP(84); + const int wAction = _colAction->IsHidden() ? 0 : FromDIP(170); + // Set every width explicitly so nothing can end up collapsed. + if (wGrip) _colGrip->SetWidth(wGrip); + if (wAction) _colAction->SetWidth(wAction); + if (wPos) _colPos->SetWidth(wPos); + _colMods->SetWidth(wMods); + _colKey->SetWidth(wKey); + // Reserve room for the vertical scrollbar and the spacer column, otherwise + // the row overflows and the right-hand columns scroll out of view. + const int reserve = FromDIP(30); + const int avail = client - (wGrip + wAction + wPos + wMods + wKey) - reserve; + // Cap Details so Modifiers/Key are always on screen at the default size - + // long descriptions ellipsize rather than pushing the key columns off the + // right edge. The spacer column absorbs anything left over. + _colDetails->SetWidth(std::min(std::max(avail, FromDIP(140)), FromDIP(300))); + _fitting = false; +} + +void KeyBindingsSettingsPanel::RebuildScopeChoices() +{ + if (Choice_Scope == nullptr) return; + const int cat = Choice_Category->GetSelection(); + + // Which scopes actually occur among the bindings in this category? + bool hasLayout = false, hasSeq = false, hasAllTabs = false; + for (const auto& b : _keyBindings->GetBindings()) { + if (cat > 0 && CategoryIndexOf(b.GetType()) != cat) continue; + switch (b.GetScope()) { + case KBSCOPE::Layout: hasLayout = true; break; + case KBSCOPE::Sequence: hasSeq = true; break; + case KBSCOPE::All: hasAllTabs = true; break; + default: break; + } + } + + // If every binding in this category shares one scope, the per-scope entry + // would just duplicate "All" - offer only "All". + const int distinct = (hasLayout ? 1 : 0) + (hasSeq ? 1 : 0) + (hasAllTabs ? 1 : 0); + const wxString prev = Choice_Scope->GetStringSelection(); + Choice_Scope->Freeze(); + Choice_Scope->Clear(); + Choice_Scope->AppendString("All"); + if (distinct > 1) { + if (hasLayout) Choice_Scope->AppendString("Layout"); + if (hasSeq) Choice_Scope->AppendString("Sequencer"); + if (hasAllTabs) Choice_Scope->AppendString("All tabs"); + } + if (Choice_Scope->SetStringSelection(prev) == false) { + Choice_Scope->SetStringSelection("All"); // previous scope no longer offered + Choice_Scope->Thaw(); + LoadList(); + return; + } + Choice_Scope->Thaw(); +} + +// The wheel and the clock Position column only describe effect bindings, so +// show them just for that category. +void KeyBindingsSettingsPanel::UpdateWheelVisibility() +{ + const int cat = Choice_Category->GetSelection(); + const bool show = (cat == 1); // Effects / Wheel of Effects + + RebuildScopeChoices(); + + if (_colGrip != nullptr && _colPos != nullptr && _colAction != nullptr) { + _colGrip->SetHidden(!show); // drag grip - effects only + _colPos->SetHidden(!show); // clock Position - effects only + // In the Effects view every Action cell just reads "Effect" - the + // effect's identity is in Details/Position, so drop the dead column. + _colAction->SetHidden(show); + FitColumns(); + } + if (_wheelBox == nullptr) return; + if (_wheelBox->IsShown(size_t(0)) != show) { + _wheelBox->ShowItems(show); + Layout(); + } +} + +std::vector KeyBindingsSettingsPanel::WheelBindings() const +{ + // Mirrors EffectsGrid.cpp's selection exactly: enabled, Sequencer-scoped + // EFFECT bindings, first kWheelSlots only. + std::vector out; + for (const auto& kb : _keyBindings->GetBindings()) { + if (!kb.IsDisabled() && kb.GetType() == "EFFECT" && kb.InScope(KBSCOPE::Sequence)) { + out.push_back(&kb); + if ((int)out.size() >= kWheelSlots) break; + } + } + return out; +} + +void KeyBindingsSettingsPanel::OnPaintWheel(wxPaintEvent&) +{ + constexpr double kPi = 3.14159265358979323846; + wxAutoBufferedPaintDC dc(_wheelPanel); + dc.SetBackground(wxBrush(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW))); + dc.Clear(); + std::unique_ptr gc(wxGraphicsContext::Create(dc)); + if (gc == nullptr) return; + + // Exactly what the sequencer's wheel would show (first kWheelSlots enabled, + // Sequencer-scoped effects), sector 0 at 12 o'clock. + struct E { long id; wxString name; }; + std::vector effs; + for (const auto* b : WheelBindings()) { + wxString nm = b->GetEffectName().empty() ? _("(unset)") : wxString::FromUTF8(b->GetEffectName()); + effs.push_back({ (long)b->GetId(), nm }); + } + + const wxSize sz = _wheelPanel->GetClientSize(); + const double cx = sz.x / 2.0, cy = sz.y / 2.0; + // Leave room for the outside labels, but keep a usable circle even when the + // panel is short (a too-eager margin used to shrink it away to nothing). + const double margin = std::min((double)FromDIP(44), std::min(cx, cy) * 0.45); + const double R = std::min(cx, cy) - margin; + const wxColour fg = wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT); + const wxColour hi(0, 120, 215); + if (R < 8) return; + + gc->SetPen(wxPen(wxColour(fg.Red(), fg.Green(), fg.Blue(), 90), 1)); + gc->SetBrush(*wxTRANSPARENT_BRUSH); + gc->DrawEllipse(cx - R, cy - R, 2 * R, 2 * R); + + if (effs.empty()) { + gc->SetFont(*wxNORMAL_FONT, wxColour(fg.Red(), fg.Green(), fg.Blue(), 150)); + double tw, th; + const wxString msg = _("No effects assigned"); + gc->GetTextExtent(msg, &tw, &th); + gc->DrawText(msg, cx - tw / 2, cy - th / 2); + return; + } + + const long selId = SelectedBinding() != nullptr ? (long)SelectedBinding()->GetId() : -1; + const int N = (int)effs.size(); + gc->SetFont(*wxSMALL_FONT, fg); + for (int i = 0; i < N; ++i) { + const double ang = -kPi / 2.0 + i * 2.0 * kPi / N; // 12 o'clock, clockwise + const double x = cx + R * std::cos(ang); + const double y = cy + R * std::sin(ang); + const bool sel = effs[i].id == selId; + const double dot = sel ? FromDIP(7.0) : FromDIP(4.0); + gc->SetPen(*wxTRANSPARENT_PEN); + gc->SetBrush(wxBrush(sel ? hi : fg)); + gc->DrawEllipse(x - dot, y - dot, 2 * dot, 2 * dot); + + gc->SetFont(*wxSMALL_FONT, sel ? hi : fg); + double tw, th; + gc->GetTextExtent(effs[i].name, &tw, &th); + double lx = cx + (R + FromDIP(8)) * std::cos(ang); + double ly = cy + (R + FromDIP(8)) * std::sin(ang); + const double c = std::cos(ang); + if (c < -0.3) lx -= tw; // left side: right-align + else if (c <= 0.3) lx -= tw / 2.0; // top/bottom: centre + gc->DrawText(effs[i].name, lx, ly - th / 2.0); + } +} + +wxString KeyBindingsSettingsPanel::FriendlyName(const std::string& type) +{ + // Most enum names humanise cleanly (split on '_', title-case); override the + // handful that don't read well that way. + static const std::map overrides = { + { "AUDIO_FULL_SPEED", "Audio: Full Speed" }, + { "AUDIO_F_1_5_SPEED", "Audio: 1.5x Speed" }, + { "AUDIO_F_2_SPEED", "Audio: 2x Speed" }, + { "AUDIO_F_3_SPEED", "Audio: 3x Speed" }, + { "AUDIO_F_4_SPEED", "Audio: 4x Speed" }, + { "AUDIO_S_3_4_SPEED", "Audio: 3/4 Speed" }, + { "AUDIO_S_1_2_SPEED", "Audio: 1/2 Speed" }, + { "AUDIO_S_1_4_SPEED", "Audio: 1/4 Speed" }, + { "VALUECURVES_TOGGLE", "Value Curves Panel" }, + { "EXPORT_MODEL_CAD", "Export Model (CAD)" }, + { "EXPORT_LAYOUT_DXF", "Export Layout (DXF)" }, + { "FPP_CONNECT", "FPP Connect" }, + { "FOCUS_SEQUENCER", "Focus Effects Grid" }, + { "COPY_MODEL_LAYERS_TO_MODELS", "Copy Layers/SubModels to Models" }, + }; + auto o = overrides.find(type); + if (o != overrides.end()) return o->second; + + wxString out; + bool newWord = true; + for (char c : type) { + if (c == '_') { + out += ' '; + newWord = true; + } else if (newWord) { + out += (char)std::toupper((unsigned char)c); + newWord = false; + } else { + out += (char)std::tolower((unsigned char)c); + } + } + return out; +} + +wxString KeyBindingsSettingsPanel::RenderModifiers(const KeyBinding& b) +{ + if (b.GetKey() == WXK_NONE) return wxEmptyString; + wxString mods; +#ifdef __WXOSX__ + if (b.RequiresControl()) mods += wxUniChar(0x2318); // Command + if (b.RequiresRawControl()) mods += wxUniChar(0x2303); // Control + if (b.RequiresAlt()) mods += wxUniChar(0x2325); // Option + if (b.RequiresShift()) mods += wxUniChar(0x21E7); // Shift +#else + if (b.RequiresControl()) mods += "Ctrl+"; + if (b.RequiresRawControl()) mods += "RCtrl+"; + if (b.RequiresAlt()) mods += "Alt+"; + if (b.RequiresShift()) mods += "Shift+"; +#endif + return mods; +} + +wxString KeyBindingsSettingsPanel::RenderKey(const KeyBinding& b) +{ + if (b.GetKey() == WXK_NONE) return _("(unassigned)"); + return b.EncodeKey(b.GetKey(), false); +} + +wxString KeyBindingsSettingsPanel::RenderShortcut(const KeyBinding& b) +{ + if (b.GetKey() == WXK_NONE) return _("(unassigned)"); + return RenderModifiers(b) + RenderKey(b); +} + +void KeyBindingsSettingsPanel::OnChoice_ScopeSelect(wxCommandEvent& event) +{ + LoadList(); +} + +void KeyBindingsSettingsPanel::SelectKey(int id) +{ + for (int i = 0; i < (int)_rowIds.size(); ++i) { + if (_rowIds[i] == (long)id) { + _dvList->SelectRow((unsigned)i); + _dvList->EnsureVisible(_dvList->RowToItem(i)); + return; + } + } +} + +// A freshly added binding is invisible if the current Category/Scope/Filter +// excludes it (which made the Add buttons look like they did nothing). Switch +// the view to the matching category and clear the filter, then select it. +void KeyBindingsSettingsPanel::RevealBinding(int id, int category) +{ + if (_filterCtrl != nullptr) { + _filterCtrl->ChangeValue(""); + _filter.clear(); + } + Choice_Category->SetSelection(category); + RebuildScopeChoices(); + Choice_Scope->SetStringSelection("All"); + LoadList(); + SelectKey(id); + PopulateEditor(); + UpdateWheelVisibility(); + if (_shortcutField != nullptr) _shortcutField->SetFocus(); +} + +void KeyBindingsSettingsPanel::OnButton_AddEffectClick(wxCommandEvent& event) +{ + int id = _keyBindings->AddKey(KeyBinding(_(""), false, _("On"), _(""), _("2020.15"), false, false, false)); + RevealBinding(id, 1); // Effects +} + +void KeyBindingsSettingsPanel::OnButtonAddApplySettingClick(wxCommandEvent& event) +{ + int id = _keyBindings->AddKey(KeyBinding(false, _(""), _(""), _("2020.15"), false, false, false, false)); + RevealBinding(id, 3); // Apply Settings +} + +void KeyBindingsSettingsPanel::OnButtonAddPresetClick(wxCommandEvent& event) +{ + int id = _keyBindings->AddKey(KeyBinding(false, _(""), _(""), false, false, false, false)); + RevealBinding(id, 2); // Presets +} + + +bool KeyBindingsSettingsPanel::TransferDataToWindow() { + LoadList(); + return true; +} + +bool KeyBindingsSettingsPanel::TransferDataFromWindow() { + // Staged edits are committed explicitly via CommitChanges() (Save button), + // never automatically - so a closed-without-saving dialog changes nothing. + return true; +} + +void KeyBindingsSettingsPanel::CommitChanges() { + if (_liveKeyBindings == nullptr) return; + _liveKeyBindings->GetBindings() = _working.GetBindings(); + _liveKeyBindings->Save(); +} \ No newline at end of file diff --git a/src-ui-wx/preferences/KeyBindingsSettingsPanel.h b/src-ui-wx/preferences/KeyBindingsSettingsPanel.h new file mode 100644 index 0000000000..d27e1c6e24 --- /dev/null +++ b/src-ui-wx/preferences/KeyBindingsSettingsPanel.h @@ -0,0 +1,153 @@ +#pragma once + +/*************************************************************** + * This source file comes from the xLights project + * https://www.xlights.org + * https://github.com/xLightsSequencer/xLights + * See the github commit history for a record of contributing + * developers. + * Copyright claimed based on commit dates recorded in Github + * License: https://github.com/xLightsSequencer/xLights/blob/master/License.txt + **************************************************************/ + +#include +#include // wxDataViewListCtrl - native drag-reorder + drop line + +#include +#include + +#include "app-shell/KeyBindings.h" // KeyBindingMap held by value (working copy) + +class KeyBindingMap; +class KeyBinding; +class EffectManager; +class xLightsFrame; +class wxButton; +class wxChoice; +class wxListCtrl; +class wxSearchCtrl; +class wxListEvent; +class wxCommandEvent; +class wxMouseEvent; + +// The Key Bindings editor body, hosted by the modeless KeyBindingsDialog. +// Filterable, category/scope-scoped bindings list on the left; the selected +// binding is edited inline on the right (key captured by keypress + modifier +// checkboxes), with a live Wheel-of-Effects preview for effect bindings. +// Edits are staged on a working copy and only written to the live KeyBindingMap +// by CommitChanges() (the dialog's Save button) - Cancel discards them. +class KeyBindingsSettingsPanel : public wxPanel +{ + EffectManager* _effectManager = nullptr; + KeyBindingMap* _keyBindings = nullptr; // points at _working (edits are staged) + KeyBindingMap* _liveKeyBindings = nullptr; // the real map; only touched on Save + KeyBindingMap _working; // edited copy; discarded on Cancel + xLightsFrame* _xLights = nullptr; + + void LoadList(); + wxString BuildDetails(const KeyBinding& b) const; + void RefreshDuplicateHighlights(); + void RefreshRow(int row, const KeyBinding& b); + int GetSelectedKeyBindingIndex() const; // selected row, or -1 + long RowBindingId(int row) const; // binding id for a row, or -1 + void SelectKey(int id); + void RemoveSelected(); // delete the selected effect binding + + // Inline editor (replaces the old modal popup): the selected binding is + // edited directly below the list - shortcut captured by keypress, and the + // effect/preset/setting chosen with a compact control. + KeyBinding* SelectedBinding() const; + void PopulateEditor(); + void OnShortcutKey(wxKeyEvent& event); + void ClearShortcut(); + void ApplyEffectChoice(); + + // Reorder the effect bindings (drag with a drop line, or Move Up/Down). + // Only EFFECT-type bindings reorder - they drive the Wheel of Effects. + void OnDvBeginDrag(wxDataViewEvent& event); + void OnDvDropPossible(wxDataViewEvent& event); + void OnDvDrop(wxDataViewEvent& event); + void MoveSelectedEffect(int delta); + long _dragFromId = -1; + wxButton* _moveUpBtn = nullptr; + wxButton* _moveDownBtn = nullptr; + + wxStaticText* _editorTitle = nullptr; + wxTextCtrl* _shortcutField = nullptr; // read-only; captures the base key + wxButton* _clearShortcutBtn = nullptr; + wxCheckBox* _cbControl = nullptr; // Command on macOS + wxCheckBox* _cbAlt = nullptr; + wxCheckBox* _cbShift = nullptr; + wxCheckBox* _cbRawControl = nullptr; // physical Control on macOS + wxStaticText* _valueLabel = nullptr; + wxChoice* _effectChoice = nullptr; + wxChoice* _presetChoice = nullptr; + wxTextCtrl* _settingCtrl = nullptr; + void SyncModifierChecks(const KeyBinding& b); + // Which value controls were last shown. Re-laying out on every selection + // change resized the list (and reset the Details column width), so only + // relayout when the editor's shape actually changes. + int _lastEditorShape = -1; + + // Live mini Wheel-of-Effects preview: draws each effect at its clock + // position, highlighting the selected one, and repaints on reorder. Only + // shown for the Effects category - it means nothing for other bindings. + wxPanel* _wheelPanel = nullptr; + wxSizer* _wheelBox = nullptr; + void OnPaintWheel(wxPaintEvent& event); + // The bindings the sequencer's Wheel of Effects actually shows: the first + // kWheelSlots enabled, Sequencer-scoped EFFECT bindings, in order (see + // EffectsGrid.cpp). Effects past that cut-off never appear on the wheel. + static constexpr int kWheelSlots = 18; + std::vector WheelBindings() const; + void UpdateWheelVisibility(); + void FitColumns(); // size Details to the leftover space + // Rebuild the Scope dropdown so it only offers scopes that actually occur + // in the currently selected Category (no more empty combinations). + void RebuildScopeChoices(); + // Make a just-added binding visible (switch category, clear filter, select). + void RevealBinding(int id, int category); + bool _fitting = false; // re-entrancy guard (SetWidth can re-fire wxEVT_SIZE) + + static wxString RenderShortcut(const KeyBinding& b); + static wxString RenderModifiers(const KeyBinding& b); // just the modifier symbols + static wxString RenderKey(const KeyBinding& b); // just the base key + + wxChoice* Choice_Category = nullptr; // filters the list by binding kind + wxChoice* Choice_Scope = nullptr; + wxDataViewListCtrl* _dvList = nullptr; + // Hold the columns by pointer: wxDataViewCtrl's positional GetColumn(i) + // shifts once a column is hidden, which silently hit the wrong column. + wxDataViewColumn* _colGrip = nullptr; + wxDataViewColumn* _colAction = nullptr; + wxDataViewColumn* _colPos = nullptr; + wxDataViewColumn* _colDetails = nullptr; + wxDataViewColumn* _colMods = nullptr; + wxDataViewColumn* _colKey = nullptr; + std::vector _rowIds; // row index -> binding id + wxSearchCtrl* _filterCtrl = nullptr; + wxString _filter; // lower-cased; whitespace-tokenised AND match in LoadList + + void OnChoice_ScopeSelect(wxCommandEvent& event); + void OnButton_AddEffectClick(wxCommandEvent& event); + void OnButtonAddApplySettingClick(wxCommandEvent& event); + void OnButtonAddPresetClick(wxCommandEvent& event); + + // Broad category a binding falls into ("Effects", "Presets", + // "Apply Settings" or "Commands"), used by the Category dropdown filter. + static int CategoryIndexOf(const std::string& type); + +public: + // Human-readable name for a binding type (e.g. "Timing: Add"). + static wxString FriendlyName(const std::string& type); + + KeyBindingsSettingsPanel(wxWindow* parent, xLightsFrame* f, wxWindowID id = wxID_ANY, const wxPoint& pos = wxDefaultPosition, const wxSize& size = wxDefaultSize); + virtual ~KeyBindingsSettingsPanel(); + + virtual bool TransferDataFromWindow() override; + virtual bool TransferDataToWindow() override; + + // Apply the staged edits to the real key-binding map and persist. Called + // by the dialog's Save button; Cancel/close just discards the working copy. + void CommitChanges(); +}; diff --git a/src-ui-wx/xLightsMain.cpp b/src-ui-wx/xLightsMain.cpp index 233783b7bf..601b1a1dc4 100644 --- a/src-ui-wx/xLightsMain.cpp +++ b/src-ui-wx/xLightsMain.cpp @@ -85,6 +85,7 @@ #include "setup/IPEntryDialog.h" #include "media/JukeboxPanel.h" #include "app-shell/KeyBindingEditDialog.h" +#include "app-shell/KeyBindingsDialog.h" #include "layout/ControllerListPanel.h" #include "layout/LayoutGroup.h" #include "layout/LayoutPanel.h" @@ -8860,9 +8861,14 @@ void xLightsFrame::OnMenuItemBulkControllerUploadSelected(wxCommandEvent& event) void xLightsFrame::OnMenuItem_KeyBindingsSelected(wxCommandEvent& event) { - KeyBindingEditDialog dlg(this, &GetMainSequencer()->keyBindings, &effectManager); - - dlg.ShowModal(); + // Modeless so it can stay open while you keep working. Reuse the open + // instance rather than stacking duplicates. + if (_keyBindingsDialog != nullptr) { + _keyBindingsDialog->Raise(); + return; + } + _keyBindingsDialog = new KeyBindingsDialog(this, this, [this]() { _keyBindingsDialog = nullptr; }); + _keyBindingsDialog->Show(); } void xLightsFrame::OnMenuItem_ExportControllerConnectionsSelected(wxCommandEvent& event) diff --git a/src-ui-wx/xLightsMain.h b/src-ui-wx/xLightsMain.h index 8851d868fe..85c002f69f 100755 --- a/src-ui-wx/xLightsMain.h +++ b/src-ui-wx/xLightsMain.h @@ -118,6 +118,7 @@ class ControllerCaps; class Discovery; class DiscoveryDelegate; class EffectTreeDialog; +class KeyBindingsDialog; class FPP; class ConvertDialog; class ConvertLogDialog; @@ -1958,6 +1959,7 @@ private : std::vector PreviewWindows; ColorManager color_mgr; EffectTreeDialog *EffectTreeDlg = nullptr; + KeyBindingsDialog* _keyBindingsDialog = nullptr; // modeless; nulled on close bool _effectPresetsInitialized = false; ModelGroup* GetSelectedModelGroup() const; diff --git a/xLights/Xlights.vcxproj b/xLights/Xlights.vcxproj index ebd87389c5..1d6a45bf24 100644 --- a/xLights/Xlights.vcxproj +++ b/xLights/Xlights.vcxproj @@ -966,6 +966,7 @@ xcopy /Y /S /I "$(SolutionDir)..\resources\effectmetadata\*" "$(TargetDir)effect + @@ -1538,6 +1539,7 @@ xcopy /Y /S /I "$(SolutionDir)..\resources\effectmetadata\*" "$(TargetDir)effect + @@ -1631,6 +1633,7 @@ xcopy /Y /S /I "$(SolutionDir)..\resources\effectmetadata\*" "$(TargetDir)effect + diff --git a/xLights/Xlights.vcxproj.filters b/xLights/Xlights.vcxproj.filters index 0a071ac4d1..0bd0850609 100644 --- a/xLights/Xlights.vcxproj.filters +++ b/xLights/Xlights.vcxproj.filters @@ -681,6 +681,9 @@ Preferences + + Preferences + Preferences @@ -1615,6 +1618,7 @@ + @@ -2230,6 +2234,9 @@ Preferences + + Preferences + Preferences diff --git a/xLights/xLights.cbp b/xLights/xLights.cbp index a4a68adb9e..72e4890bc5 100644 --- a/xLights/xLights.cbp +++ b/xLights/xLights.cbp @@ -409,6 +409,7 @@ + @@ -1389,6 +1390,8 @@ + +