|  | /*
  ==============================================================================
   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.reset();
        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.reset();
    }
    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);
    }
};
 |