@@ -183,6 +183,7 @@ class CommandInput final
183183 : public Component
184184 , public KeyListener
185185 , public CommandProcessor
186+ , public FocusChangeListener
186187 , public MarkupDisplay::URLHandler {
187188public:
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