1
0
Fork 0
mirror of https://github.com/juce-framework/JUCE.git synced 2026-01-10 23:44:24 +00:00
JUCE/extras/Projucer/Source/CodeEditor/jucer_LiveBuildCodeEditor.h

738 lines
25 KiB
C++

/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2017 - ROLI Ltd.
JUCE is an open source library subject to commercial or open-source
licensing.
By using JUCE, you agree to the terms of both the JUCE 5 End-User License
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
27th April 2017).
End User License Agreement: www.juce.com/juce-5-licence
Privacy Policy: www.juce.com/juce-5-privacy-policy
Or: You may also use this code under the terms of the GPL v3 (see
www.gnu.org/licenses).
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
#pragma once
class LiveBuildCodeEditorDocument;
//==============================================================================
class LiveBuildCodeEditor : public CppCodeEditorComponent,
private Timer
{
public:
LiveBuildCodeEditor (LiveBuildCodeEditorDocument& edDoc, CodeDocument& doc)
: CppCodeEditorComponent (edDoc.getFile(), doc),
editorDoc (edDoc),
classList (*this, edDoc)
{
}
~LiveBuildCodeEditor()
{
for (int i = getNumChildComponents(); --i >= 0;)
if (auto* c = dynamic_cast<DiagnosticOverlayComponent*> (getChildComponent (i)))
delete c;
}
CompileEngineChildProcess::Ptr getChildProcess() const
{
return editorDoc.getChildProcess();
}
Component* addDiagnosticOverlay (CodeDocument::Position start, CodeDocument::Position end,
DiagnosticMessage::Type diagType)
{
auto* d = new DiagnosticOverlayComponent (*this, start, end, diagType);
addAndMakeVisible (d);
return d;
}
private:
LiveBuildCodeEditorDocument& editorDoc;
//==============================================================================
struct OverlayComponent : public Component,
private GenericCodeEditorComponent::Listener,
private CodeDocument::Listener
{
OverlayComponent (CodeDocument::Position start,
CodeDocument::Position end)
: startPosition (start),
endPosition (end)
{
startPosition.setPositionMaintained (true);
endPosition.setPositionMaintained (true);
}
~OverlayComponent()
{
setEditor (nullptr);
}
void setEditor (GenericCodeEditorComponent* editor)
{
if (editor != codeEditor)
{
if (codeEditor != nullptr)
{
codeEditor->removeListener (this);
codeEditor->getDocument().removeListener (this);
codeEditor->removeChildComponent (this);
}
codeEditor = editor;
if (codeEditor != nullptr)
{
codeEditor->addListener (this);
codeEditor->getDocument().addListener (this);
codeEditor->addAndMakeVisible (this);
}
if (editor != nullptr)
updatePosition();
}
}
void codeEditorViewportMoved (CodeEditorComponent& editor) override
{
setEditor (dynamic_cast<GenericCodeEditorComponent*> (&editor));
updatePosition();
}
void codeDocumentTextInserted (const String&, int) override { updatePosition(); }
void codeDocumentTextDeleted (int, int) override { updatePosition(); }
void parentSizeChanged() override
{
updatePosition();
}
virtual void updatePosition() = 0;
Component::SafePointer<GenericCodeEditorComponent> codeEditor;
CodeDocument::Position startPosition, endPosition;
};
//==============================================================================
struct LaunchClassOverlayComponent : public OverlayComponent
{
LaunchClassOverlayComponent (GenericCodeEditorComponent& editor,
CodeDocument::Position start, CodeDocument::Position end,
const String className)
: OverlayComponent (start, end),
launchButton (className.fromLastOccurrenceOf ("::", false, false)),
name (className)
{
setAlwaysOnTop (true);
setEditor (&editor);
addAndMakeVisible (launchButton);
}
void updatePosition() override
{
if (codeEditor != nullptr)
{
jassert (isVisible());
const auto charArea = codeEditor->getCharacterBounds (startPosition);
const int height = charArea.getHeight() + 8;
Font f (height * 0.7f);
const int width = jmin (height * 2 + f.getStringWidth (launchButton.getName()),
jmax (120, codeEditor->proportionOfWidth (0.2f)));
setBounds (codeEditor->getWidth() - width - 10, charArea.getY() - 4,
width, height);
}
}
void resized() override
{
launchButton.setBounds (getLocalBounds());
}
struct LaunchButton : public Button
{
LaunchButton (const String& nm) : Button (nm)
{
setMouseCursor (MouseCursor::PointingHandCursor);
}
void paintButton (Graphics& g, bool isMouseOverButton, bool isButtonDown) override
{
Colour background (findColour (CodeEditorComponent::backgroundColourId)
.contrasting()
.overlaidWith (Colours::yellow.withAlpha (0.5f))
.withAlpha (0.4f));
g.setColour (background);
g.fillRoundedRectangle (getLocalBounds().toFloat(), 3.0f);
const Path& path = getIcons().play;
Colour col (background.contrasting (Colours::lightgreen, 0.6f));
Rectangle<int> r (getLocalBounds().reduced (getHeight() / 5));
Icon (path, col.withAlpha (isButtonDown ? 1.0f : (isMouseOverButton ? 0.8f : 0.5f)))
.draw (g, r.removeFromLeft (getHeight()).toFloat(), false);
g.setColour (Colours::white);
g.setFont (getHeight() * 0.7f);
g.drawFittedText (getName(), r, Justification::centredLeft, 1);
}
void clicked() override
{
if (auto* l = findParentComponentOfClass<LaunchClassOverlayComponent>())
l->launch();
}
};
void launch()
{
if (auto* e = findParentComponentOfClass<LiveBuildCodeEditor>())
e->launch (name);
}
private:
LaunchButton launchButton;
String name;
};
struct ComponentClassList : private Timer
{
ComponentClassList (GenericCodeEditorComponent& e, LiveBuildCodeEditorDocument& edDoc)
: owner (e),
childProcess (edDoc.getChildProcess()),
file (edDoc.getFile())
{
startTimer (600);
}
~ComponentClassList()
{
deleteOverlays();
}
void timerCallback() override
{
Array<ClassDatabase::Class*> newClasses;
if (childProcess != nullptr)
childProcess->getComponentList().globalNamespace.findClassesDeclaredInFile (newClasses, file);
for (int i = newClasses.size(); --i >= 0;)
if (! newClasses.getUnchecked(i)->getInstantiationFlags().canBeInstantiated())
newClasses.remove (i);
if (newClasses != classes)
{
classes = newClasses;
deleteOverlays();
for (auto& c : classes)
{
CodeDocument::Position pos (owner.getDocument(), c->getClassDeclarationRange().range.getStart());
overlays.add (new LaunchClassOverlayComponent (owner, pos, pos, c->getName()));
}
}
}
void deleteOverlays()
{
for (auto& o : overlays)
o.deleteAndZero();
overlays.clear();
}
GenericCodeEditorComponent& owner;
CompileEngineChildProcess::Ptr childProcess;
File file;
Array<ClassDatabase::Class*> classes;
Array<Component::SafePointer<Component>> overlays;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ComponentClassList)
};
ComponentClassList classList;
//==============================================================================
struct DiagnosticOverlayComponent : public OverlayComponent
{
DiagnosticOverlayComponent (GenericCodeEditorComponent& editor,
CodeDocument::Position start, CodeDocument::Position end,
DiagnosticMessage::Type diagType)
: OverlayComponent (start, end), diagnosticType (diagType)
{
setInterceptsMouseClicks (false, false);
setEditor (&editor);
}
void updatePosition() override
{
if (codeEditor != nullptr)
{
jassert (isVisible());
const auto charStartRect = codeEditor->getCharacterBounds (startPosition);
const auto charEndRect = codeEditor->getCharacterBounds (endPosition);
auto charHeight = charStartRect.getHeight();
const auto editorBounds = codeEditor->getBounds();
arrowXMin = static_cast<int> (jmin (charStartRect.getX(), charEndRect.getX()));
arrowXMax = static_cast<int> (jmax (charStartRect.getX() + charStartRect.getWidth(), charEndRect.getX() + charEndRect.getWidth()));
lineYMin = charStartRect.getY();
lineOffset = charHeight;
setBounds (0, lineYMin, editorBounds.getWidth(), lineOffset + charHeight);
repaint();
}
}
void paint (Graphics& g) override
{
const auto diagColour = diagnosticType == DiagnosticMessage::Type::error ? Colours::red
: Colour (200, 200, 64);
g.setColour (diagColour.withAlpha (0.2f));
g.fillRect (getLocalBounds().withTrimmedBottom (lineOffset));
Path path;
const float bottomY = getHeight() - (lineOffset / 2.0f);
path.addTriangle ((float) arrowXMin, bottomY,
(arrowXMax + arrowXMin) / 2.0f, (float) lineOffset,
(float) arrowXMax, bottomY);
g.setColour (diagColour.withAlpha (0.8f));
g.fillPath (path);
}
private:
int arrowXMin, arrowXMax;
int lineYMin, lineOffset;
const DiagnosticMessage::Type diagnosticType;
};
//==============================================================================
void timerCallback() override
{
if (isMouseButtonDownAnywhere())
return;
MouseInputSource mouse = Desktop::getInstance().getMainMouseSource();
Component* underMouse = mouse.getComponentUnderMouse();
if (underMouse != nullptr
&& (dynamic_cast<ControlsComponent*> (underMouse) != nullptr
|| underMouse->findParentComponentOfClass<ControlsComponent>() != nullptr))
return;
overlay = nullptr;
if (hasKeyboardFocus (true) && underMouse != nullptr
&& (underMouse == this || underMouse->isParentOf (this)))
{
Point<int> mousePos = getLocalPoint (nullptr, mouse.getScreenPosition()).toInt();
CodeDocument::Position start, end;
getDocument().findTokenContaining (getPositionAt (mousePos.x, mousePos.y), start, end);
if (end.getPosition() > start.getPosition())
{
Range<int> selection = optimiseSelection ({ start.getPosition(), end.getPosition() });
String text = getTextInRange (selection).toLowerCase();
if (isIntegerLiteral (text) || isFloatLiteral (text))
overlay = new LiteralHighlightOverlay (*this, selection, mightBeColourValue (text));
}
}
startTimerHz (10);
}
void hideOverlay()
{
stopTimer();
overlay = nullptr;
}
void focusLost (FocusChangeType) override
{
if (CompileEngineChildProcess::Ptr childProcess = getChildProcess())
childProcess->flushEditorChanges();
}
void mouseMove (const MouseEvent& e) override
{
if (overlay == nullptr)
startTimer (100);
CppCodeEditorComponent::mouseMove (e);
}
void mouseDrag (const MouseEvent& e) override
{
if (e.getDistanceFromDragStart() > 0)
hideOverlay();
CppCodeEditorComponent::mouseDrag (e);
}
void mouseDown (const MouseEvent& e) override
{
CppCodeEditorComponent::mouseDown (e);
}
void mouseUp (const MouseEvent& e) override
{
CppCodeEditorComponent::mouseUp (e);
}
bool keyPressed (const KeyPress& key) override
{
hideOverlay();
return CppCodeEditorComponent::keyPressed (key);
}
static bool isIntegerLiteral (const String& text) { return CppParserHelpers::parseSingleToken (text) == CPlusPlusCodeTokeniser::tokenType_integer; }
static bool isFloatLiteral (const String& text) { return CppParserHelpers::parseSingleToken (text) == CPlusPlusCodeTokeniser::tokenType_float; }
static bool mightBeColourValue (const String& text)
{
return isIntegerLiteral (text)
&& text.trim().startsWith ("0x")
&& text.trim().length() > 7;
}
Range<int> optimiseSelection (Range<int> selection)
{
String text (getTextInRange (selection));
if (CharacterFunctions::isDigit (text[0]) || text[0] == '.')
if (getTextInRange (Range<int> (selection.getStart() - 1, selection.getStart())) == "-")
selection.setStart (selection.getStart() - 1);
selection.setStart (selection.getStart() + (text.length() - text.trimStart().length()));
selection.setEnd (selection.getEnd() - (text.length() - text.trimEnd().length()));
return selection;
}
void launch (const String& name)
{
if (CompileEngineChildProcess::Ptr p = getChildProcess())
if (auto* cls = p->getComponentList().globalNamespace.findClass (name))
p->openPreview (*cls);
}
//==============================================================================
class ControlsComponent : public Component,
private Slider::Listener,
private ChangeListener
{
public:
ControlsComponent (CodeDocument& doc, const Range<int>& selection,
CompileEngineChildProcess* cp, bool showColourSelector)
: document (doc),
start (doc, selection.getStart()),
end (doc, selection.getEnd()),
childProcess (cp)
{
slider.setTextBoxStyle (Slider::NoTextBox, true, 0, 0);
slider.setWantsKeyboardFocus (false);
slider.setMouseClickGrabsKeyboardFocus (false);
setWantsKeyboardFocus (false);
setMouseClickGrabsKeyboardFocus (false);
addAndMakeVisible (&slider);
updateRange();
slider.addListener (this);
if (showColourSelector)
{
updateColourSelector();
selector.setWantsKeyboardFocus (false);
selector.setMouseClickGrabsKeyboardFocus (false);
addAndMakeVisible (&selector);
setSize (400, sliderHeight + 400);
selector.addChangeListener (this);
}
else
{
setSize (400, sliderHeight);
}
end.setPositionMaintained (true);
}
void updateRange()
{
double v = getValue();
if (isFloat())
slider.setRange (v - 10, v + 10);
else
slider.setRange (v - 100, v + 100);
slider.setValue (v, dontSendNotification);
}
private:
Slider slider;
ColourSelector selector;
CodeDocument& document;
CodeDocument::Position start, end;
CompileEngineChildProcess::Ptr childProcess;
static const int sliderHeight = 26;
void paint (Graphics& g) override
{
g.setColour (LiteralHighlightOverlay::getBackgroundColour());
g.fillRoundedRectangle (getLocalBounds().toFloat(), 8.0f);
}
void sliderValueChanged (Slider* s) override
{
const String oldText (document.getTextBetween (start, end));
const String newText (CppParserHelpers::getReplacementStringInSameFormat (oldText, s->getValue()));
if (oldText != newText)
document.replaceSection (start.getPosition(), end.getPosition(), newText);
if (childProcess != nullptr)
childProcess->flushEditorChanges();
updateColourSelector();
}
void sliderDragStarted (Slider*) override {}
void sliderDragEnded (Slider*) override { updateRange(); }
void changeListenerCallback (ChangeBroadcaster*) override
{
setNewColour (selector.getCurrentColour());
}
void updateColourSelector()
{
selector.setCurrentColour (getCurrentColour());
}
Colour getCurrentColour() const
{
int64 val;
if (CppParserHelpers::parseInt (document.getTextBetween (start, end), val))
return Colour ((uint32) val);
return Colours::white;
}
void setNewColour (const Colour& c)
{
const String oldText (document.getTextBetween (start, end));
const String newText (CppParserHelpers::getReplacementStringInSameFormat (oldText, (int64) c.getARGB()));
if (oldText != newText)
document.replaceSection (start.getPosition(), end.getPosition(), newText);
if (childProcess != nullptr)
childProcess->flushEditorChanges();
}
void resized() override
{
Rectangle<int> r (getLocalBounds());
slider.setBounds (r.removeFromTop (sliderHeight));
r.removeFromTop (10);
if (selector.isVisible())
selector.setBounds (r);
}
double getValue() const
{
const String text (document.getTextBetween (start, end));
if (text.containsChar ('.'))
{
double f;
if (CppParserHelpers::parseFloat (text, f))
return f;
}
else
{
int64 val;
if (CppParserHelpers::parseInt (text, val))
return (double) val;
}
jassertfalse;
return 0;
}
bool isFloat() const
{
return document.getTextBetween (start, end).containsChar ('.');
}
};
//==============================================================================
struct LiteralHighlightOverlay : public Component,
private CodeDocument::Listener
{
LiteralHighlightOverlay (LiveBuildCodeEditor& e, Range<int> section, bool showColourSelector)
: owner (e),
start (e.getDocument(), section.getStart()),
end (e.getDocument(), section.getEnd()),
controls (e.getDocument(), section, e.getChildProcess(), showColourSelector)
{
if (e.hasKeyboardFocus (true))
previouslyFocused = Component::getCurrentlyFocusedComponent();
start.setPositionMaintained (true);
end.setPositionMaintained (true);
setInterceptsMouseClicks (false, false);
if (Component* parent = owner.findParentComponentOfClass<ProjectContentComponent>())
parent->addAndMakeVisible (controls);
else
jassertfalse;
owner.addAndMakeVisible (this);
toBack();
updatePosition();
owner.getDocument().addListener (this);
}
~LiteralHighlightOverlay()
{
if (Component* p = getParentComponent())
{
p->removeChildComponent (this);
if (previouslyFocused != nullptr && ! previouslyFocused->hasKeyboardFocus (true))
previouslyFocused->grabKeyboardFocus();
}
owner.getDocument().removeListener (this);
}
void paint (Graphics& g) override
{
g.setColour (getBackgroundColour());
Rectangle<int> r (getLocalBounds());
g.fillRect (r.removeFromTop (borderSize));
g.fillRect (r.removeFromLeft (borderSize));
g.fillRect (r.removeFromRight (borderSize));
}
void updatePosition()
{
Rectangle<int> area = owner.getCharacterBounds (start)
.getUnion (owner.getCharacterBounds (end.movedBy (-1)))
.expanded (borderSize)
.withTrimmedBottom (borderSize);
setBounds (getParentComponent()->getLocalArea (&owner, area));
area.setPosition (area.getX() - controls.getWidth() / 2, area.getBottom());
area.setSize (controls.getWidth(), controls.getHeight());
controls.setBounds (controls.getParentComponent()->getLocalArea (&owner, area));
}
void codeDocumentTextInserted (const String&, int) override { updatePosition(); }
void codeDocumentTextDeleted (int, int) override { updatePosition(); }
LiveBuildCodeEditor& owner;
CodeDocument::Position start, end;
ControlsComponent controls;
Component::SafePointer<Component> previouslyFocused;
static const int borderSize = 4;
static Colour getBackgroundColour() { return Colour (0xcb5c7879); }
};
ScopedPointer<LiteralHighlightOverlay> overlay;
};
//==============================================================================
class LiveBuildCodeEditorDocument : public SourceCodeDocument
{
public:
LiveBuildCodeEditorDocument (Project* project, const File& file)
: SourceCodeDocument (project, file)
{
if (project != nullptr)
if (CompileEngineChildProcess::Ptr childProcess = getChildProcess())
childProcess->editorOpened (file, getCodeDocument());
}
struct Type : public SourceCodeDocument::Type
{
Document* openFile (Project* proj, const File& file) override
{
return new LiveBuildCodeEditorDocument (proj, file);
}
};
Component* createEditor() override
{
SourceCodeEditor* e = nullptr;
if (fileNeedsCppSyntaxHighlighting (getFile()))
e = new SourceCodeEditor (this, new LiveBuildCodeEditor (*this, getCodeDocument()));
else
e = new SourceCodeEditor (this, getCodeDocument());
applyLastState (*(e->editor));
return e;
}
// override save() to make a few more attempts at saving if it fails, since on Windows
// the compiler can interfere with things saving..
bool save() override
{
for (int i = 5; --i >= 0;)
{
if (SourceCodeDocument::save()) // should already re-try for up to half a second
return true;
Thread::sleep (100);
}
return false;
}
CompileEngineChildProcess::Ptr getChildProcess() const
{
return ProjucerApplication::getApp().childProcessCache->getExisting (*project);
}
};