Skip to content

Commit 75fe4e5

Browse files
committed
Command input improvements: show common commands when focused, improve man command, move caret to end when inserting commands
1 parent adefd3b commit 75fe4e5

1 file changed

Lines changed: 131 additions & 17 deletions

File tree

Source/Sidebar/CommandInput.h

Lines changed: 131 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,7 @@ class CommandInput final
183183
: public Component
184184
, public KeyListener
185185
, public CommandProcessor
186+
, public FocusChangeListener
186187
, public MarkupDisplay::URLHandler {
187188
public:
188189
explicit CommandInput(PluginEditor* editor)
@@ -261,12 +262,69 @@ class CommandInput final
261262
commandInput.setText(currentCommand);
262263

263264
updateSize();
265+
266+
updateHelperCommands();
267+
Desktop::getInstance().addFocusChangeListener(this);
264268
}
265269

266-
void updateSize()
270+
void updateHelperCommands()
267271
{
268-
int newHeight = std::max(commandInput.getTextHeight() + 4, 30);
269-
setBounds(getX(), getBottom() - newHeight, getWidth(), newHeight);
272+
auto isGlobalTarget = consoleTargetName == ">" || consoleTargetName == "lua >";
273+
auto& currentHelpers = isGlobalTarget ? helperCommands : objectHelperCommands;
274+
275+
helperButtons.clear();
276+
277+
for (auto const& cmd : currentHelpers) {
278+
auto* btn = helperButtons.add(new TextButton(cmd));
279+
btn->setWantsKeyboardFocus(false);
280+
btn->setColour(TextButton::buttonColourId, Colours::transparentBlack);
281+
btn->setColour(TextButton::buttonOnColourId, Colours::transparentBlack);
282+
btn->setColour(TextButton::textColourOffId, findColour(PlugDataColour::toolbarTextColourId));
283+
btn->setColour(ComboBox::outlineColourId, Colours::transparentBlack);
284+
btn->onClick = [this, cmd] {
285+
commandInput.setText(cmd + " ", sendNotification);
286+
commandInput.grabKeyboardFocus();
287+
commandInput.moveCaretToEnd();
288+
};
289+
addChildComponent(btn);
290+
btn->setVisible(hasInputFocus);
291+
}
292+
resized();
293+
}
294+
295+
void updateSize(bool animate = false)
296+
{
297+
int const extraHeight = hasInputFocus ? helperRowHeight : 0;
298+
int const newHeight = std::max(commandInput.getTextHeight() + 4, 30) + extraHeight;
299+
300+
auto const fromBounds = getBounds();
301+
auto const targetBounds = Rectangle<int>(fromBounds.getX(),fromBounds.getBottom() - newHeight, fromBounds.getWidth(), newHeight);
302+
303+
if (fromBounds == targetBounds)
304+
return;
305+
306+
if (!animate) {
307+
sizeAnimator.complete();
308+
setBounds(targetBounds);
309+
return;
310+
}
311+
312+
sizeAnimator = ValueAnimatorBuilder{}
313+
.withEasing(Easings::createEaseInOut())
314+
.withDurationMs(180)
315+
.withValueChangedCallback([this, fromBounds, targetBounds](auto v) {
316+
auto start = std::make_tuple(fromBounds.getX(), fromBounds.getY(), fromBounds.getWidth(), fromBounds.getHeight());
317+
auto end = std::make_tuple(targetBounds.getX(), targetBounds.getY(), targetBounds.getWidth(), targetBounds.getHeight());
318+
auto const [x, y, w, h] = makeAnimationLimits(start, end).lerp(v);
319+
setBounds(x, y, w, h);
320+
})
321+
.build();
322+
323+
animatorUpdater.addAnimator(sizeAnimator, [this](){
324+
for (auto* btn : helperButtons)
325+
btn->setVisible(hasInputFocus);
326+
});
327+
sizeAnimator.start();
270328
}
271329

272330
static int countBraces(String const& text)
@@ -561,48 +619,54 @@ class CommandInput final
561619

562620
case hash("?"):
563621
case hash("help"):
564-
pd->logMessage(argv[2] + ": Show help");
622+
pd->logMessage(argv[1] + ": Show help");
565623
break;
566624

567625
case hash("script"):
568-
pd->logMessage(argv[2] + ": Excute a Lua script from your search path. Usage: script <filename>");
626+
pd->logMessage(argv[1] + ": Excute a Lua script from your search path. Usage: script <filename>");
627+
break;
628+
629+
case hash("pd"):
630+
pd->logMessage(argv[1] + ": Send a message to pd. Usage: " + argv[1] + " <message>");
569631
break;
570632

571633
case hash("cnv"):
572634
case hash("canvas"):
573-
pd->logMessage(argv[2] + ": Send a message to current canvas. Usage: " + argv[2] + " <message>");
635+
pd->logMessage(argv[1] + ": Send a message to current canvas. Usage: " + argv[1] + " <message>");
574636
break;
575637

576638
case hash("clear"):
577-
pd->logMessage(argv[2] + ": Clear console and command history");
639+
pd->logMessage(argv[1] + ": Clear console and command history");
578640
break;
579641

580642
case hash("reset"):
581-
pd->logMessage(argv[2] + ": Reset Lua interpreter state");
643+
pd->logMessage(argv[1] + ": Reset Lua interpreter state");
582644
break;
583645

584646
case hash("sel"):
585647
case hash("select"):
586-
pd->logMessage(argv[2] + ": Select an object by ID or index. After selecting objects, you can send messages to them. Usage: " + argv[2] + " <id> or " + argv[2] + " <index>");
648+
pd->logMessage(argv[1] + ": Select an object by ID or index. After selecting objects, you can send messages to them. Usage: " + argv[1] + " <id> or " + argv[1] + " <index>");
587649
break;
588650

589651
case hash(">"):
590652
case hash("deselect"):
591-
pd->logMessage(argv[2] + ": Deselects all on current canvas");
653+
pd->logMessage(argv[1] + ": Deselects all on current canvas");
592654
break;
593655

594656
case hash("ls"):
595657
case hash("list"):
596-
pd->logMessage(argv[2] + ": Print a list of all object IDs on current canvas");
658+
pd->logMessage(argv[1] + ": Print a list of all object IDs on current canvas");
597659
break;
598660

599661
case hash("find"):
600662
case hash("search"):
601-
pd->logMessage(argv[2] + ": Search object IDs on current canvas. Usage: " + argv[2] + " <id>.");
663+
pd->logMessage(argv[1] + ": Search object IDs on current canvas. Usage: " + argv[1] + " <id>.");
602664
break;
603665
default:
666+
pd->logMessage("man: No manual for command: " + argv[1]);
604667
break;
605668
}
669+
break;
606670
}
607671
case hash("?"):
608672
case hash("help"): {
@@ -696,20 +760,41 @@ class CommandInput final
696760

697761
~CommandInput() override
698762
{
763+
Desktop::getInstance().removeFocusChangeListener(this);
699764
onDismiss();
700765
}
701766

767+
void globalFocusChanged(Component* focusedComponent) override
768+
{
769+
bool const focused = focusedComponent != nullptr
770+
&& (focusedComponent == &commandInput || isParentOf(focusedComponent));
771+
772+
if (focused == hasInputFocus)
773+
return;
774+
775+
hasInputFocus = focused;
776+
for (auto* btn : helperButtons)
777+
btn->setVisible(false);
778+
779+
updateSize(true); // animate on focus change
780+
}
781+
702782
void handleURL(String const& url) override // when documentation links or codeblocks are clicked
703783
{
704784
commandInput.setText(url);
785+
commandInput.moveCaretToEnd();
705786
}
706787

707788
void paintOverChildren(Graphics& g) override
708789
{
709790
auto bounds = getLocalBounds();
791+
int const inputHeight = std::max(commandInput.getTextHeight() + 4, 30);
792+
auto const inputRow = bounds.removeFromBottom(inputHeight);
793+
710794
g.setColour(PlugDataColours::dataColour);
711795
g.setFont(Fonts::getSemiBoldFont().withHeight(15));
712-
g.drawText(consoleTargetName, bounds.getX() + 9, bounds.getY(), consoleTargetLength, bounds.getHeight() - 1, Justification::centredLeft);
796+
g.drawText(consoleTargetName, inputRow.getX() + 9, inputRow.getY(),
797+
consoleTargetLength, inputRow.getHeight() - 1, Justification::centredLeft);
713798
}
714799

715800
void paint(Graphics& g) override
@@ -721,10 +806,22 @@ class CommandInput final
721806

722807
void resized() override
723808
{
724-
auto inputBounds = getLocalBounds();
725-
commandInput.setBounds(inputBounds.withTrimmedLeft(consoleTargetLength + 4).withTrimmedRight(30));
726-
auto const buttonBounds = inputBounds.removeFromRight(30);
727-
clearButton.setBounds(buttonBounds);
809+
auto bounds = getLocalBounds();
810+
int const inputHeight = std::max(commandInput.getTextHeight() + 4, 30);
811+
812+
if (hasInputFocus) {
813+
auto const helperSpace = std::max(0, bounds.getHeight() - inputHeight);
814+
auto helperBounds = bounds.removeFromTop(helperSpace);
815+
helperBounds.removeFromLeft(8);
816+
for (auto* btn : helperButtons) {
817+
auto const w = CachedStringWidth<14>::calculateStringWidth(btn->getButtonText()) + 15;
818+
btn->setBounds(helperBounds.removeFromLeft(w).reduced(1, 3));
819+
}
820+
}
821+
822+
auto const clearBounds = bounds.removeFromRight(30).removeFromBottom(inputHeight);
823+
clearButton.setBounds(clearBounds);
824+
commandInput.setBounds(bounds.withTrimmedLeft(consoleTargetLength + 4));
728825
}
729826

730827
void setConsoleTargetName(String const& target)
@@ -734,6 +831,8 @@ class CommandInput final
734831
consoleTargetName = ">";
735832
consoleTargetLength = CachedStringWidth<15>::calculateStringWidth(consoleTargetName) + 4;
736833
commandInput.setBounds(commandInput.getBounds().withLeft(consoleTargetLength + 4));
834+
835+
updateHelperCommands();
737836
repaint();
738837
}
739838

@@ -752,6 +851,7 @@ class CommandInput final
752851
} else {
753852
currentHistoryIndex = commandHistory.size() - 1;
754853
}
854+
commandInput.moveCaretToEnd();
755855
}
756856

757857
bool keyPressed(KeyPress const& key, Component*) override
@@ -806,6 +906,20 @@ class CommandInput final
806906
SmallIconButton clearButton = SmallIconButton(Icons::ClearText);
807907
SmallIconButton helpButton = SmallIconButton(Icons::Help);
808908

909+
OwnedArray<TextButton> helperButtons;
910+
bool hasInputFocus = false;
911+
static constexpr int helperRowHeight = 26;
912+
913+
VBlankAnimatorUpdater animatorUpdater { this };
914+
Animator sizeAnimator = ValueAnimatorBuilder{}.build();
915+
916+
static inline StringArray const helperCommands = {
917+
"help", "man", "ls", "sel", "cnv", "pd"
918+
};
919+
static inline StringArray const objectHelperCommands = {
920+
"deselect",
921+
};
922+
809923
static inline String documentationString = {
810924
"Command input allows you to quickly send commands to objects, pd or the canvas.\n"
811925
"The following commands are available:\n"

0 commit comments

Comments
 (0)