diff --git a/bin/assets/ui/breeze.css b/bin/assets/ui/breeze.css index 4a28a36b4..a25630ad9 100644 --- a/bin/assets/ui/breeze.css +++ b/bin/assets/ui/breeze.css @@ -490,6 +490,11 @@ Loader { fill-color: var(--primary); } +CodeEditor > Loader { + background-color: #0000002d; + radius: 64dp; +} + Window::close { width: 12dp; height: 12dp; diff --git a/include/eepp/core/string.hpp b/include/eepp/core/string.hpp index 0263d0a19..81aee7d5e 100644 --- a/include/eepp/core/string.hpp +++ b/include/eepp/core/string.hpp @@ -255,6 +255,11 @@ class EE_API String { static int valueIndex( const std::string& val, const std::string& strings, int defValue = -1, char delim = ';' ); + /** Creates a random string using the dictionary characters. */ + static std::string randString( + size_t len, + std::string dictionary = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" ); + /** Converts from any basic type to std::string */ template static std::string toString( const T& i ) { std::ostringstream ss; diff --git a/include/eepp/network/http.hpp b/include/eepp/network/http.hpp index 97460713d..d85d713be 100644 --- a/include/eepp/network/http.hpp +++ b/include/eepp/network/http.hpp @@ -174,7 +174,7 @@ class EE_API Http : NonCopyable { ///< target resource. Patch, ///< The PATCH method is used to apply partial modifications to a resource. Connect ///< The CONNECT method starts two-way communications with the requested - ///< resource. It can be used to open a tunnel. + ///< resource. It can be used to open a tunnel. }; /** @brief Enumerate the available states for a request */ @@ -636,6 +636,9 @@ class EE_API Http : NonCopyable { const Request::FieldTable& headers = Request::FieldTable(), const std::string& body = "", const bool& validateCertificate = true, const URI& proxy = URI() ); + /** It will try to get the proxy from the environment variables. */ + static URI getEnvProxyURI(); + private: class AsyncRequest : public Thread { public: diff --git a/include/eepp/network/uri.hpp b/include/eepp/network/uri.hpp index 1d437e478..b37ed320c 100644 --- a/include/eepp/network/uri.hpp +++ b/include/eepp/network/uri.hpp @@ -150,8 +150,11 @@ class EE_API URI { /** @returns The path part of the URI. */ const std::string& getPath() const; + /** @returns The last path segment. */ + std::string getLastPathSegment() const; + /** Sets the path part of the URI. */ - void getPath( const std::string& path ); + void setPath( const std::string& path ); /** @returns the query part of the URI. */ std::string getQuery() const; diff --git a/include/eepp/ui/doc/textdocument.hpp b/include/eepp/ui/doc/textdocument.hpp index 9e773c054..664020991 100644 --- a/include/eepp/ui/doc/textdocument.hpp +++ b/include/eepp/ui/doc/textdocument.hpp @@ -1,7 +1,9 @@ #ifndef EE_UI_DOC_TEXTDOCUMENT #define EE_UI_DOC_TEXTDOCUMENT +#include #include +#include #include #include #include @@ -18,6 +20,7 @@ #include using namespace EE::System; +using namespace EE::Network; namespace EE { namespace UI { namespace Doc { @@ -44,8 +47,8 @@ class EE_API TextDocument { const size_t& newCount ) = 0; virtual void onDocumentLineChanged( const Int64& lineIndex ) = 0; virtual void onDocumentSaved( TextDocument* ) = 0; - virtual void onDocumentClosed( TextDocument* ) {} - virtual void onDocumentDirtyOnFileSystem( TextDocument* ) {} + virtual void onDocumentClosed( TextDocument* ) = 0; + virtual void onDocumentDirtyOnFileSystem( TextDocument* ) = 0; }; TextDocument( bool verbose = true ); @@ -68,6 +71,22 @@ class EE_API TextDocument { bool loadFromPack( Pack* pack, std::string filePackPath ); + /** + * @brief loadFromURL + * @param url Resources URL. + * @param headers Key value map of headers + * @return + */ + bool loadFromURL( + const std::string& url, + const EE::Network::Http::Request::FieldTable& headers = Http::Request::FieldTable() ); + + bool loadAsyncFromURL( const std::string& url, + const Http::Request::FieldTable& headers = Http::Request::FieldTable(), + std::function onLoaded = + std::function(), + const Http::Request::ProgressCallback& progressCallback = nullptr ); + bool reload(); bool save(); @@ -377,6 +396,8 @@ class EE_API TextDocument { void sanitizeCurrentSelection(); + bool isLoading() const; + protected: friend class UndoStack; UndoStack mUndoStack; @@ -386,6 +407,7 @@ class EE_API TextDocument { TextRange mSelection; std::unordered_set mClients; LineEnding mLineEnding{ LineEnding::LF }; + std::atomic mLoading{ false }; bool mIsBOM{ false }; bool mAutoDetectIndentType{ true }; bool mForceNewLineAtEndOfFile{ false }; diff --git a/include/eepp/ui/uicodeeditor.hpp b/include/eepp/ui/uicodeeditor.hpp index 45ec92fa1..66c755a5e 100644 --- a/include/eepp/ui/uicodeeditor.hpp +++ b/include/eepp/ui/uicodeeditor.hpp @@ -18,10 +18,19 @@ class Font; namespace EE { namespace UI { class UICodeEditor; +class UIWindow; class UIScrollBar; +class UILoader; class UICodeEditorModule { public: + virtual std::string getTitle() = 0; + virtual std::string getDescription() = 0; + virtual bool hasGUIConfig() { return false; } + virtual bool hasFileConfig() { return false; } + virtual UIWindow* getGUIConfig() { return nullptr; } + virtual std::string getFileConfigPath() { return ""; } + virtual void onRegister( UICodeEditor* ) = 0; virtual void onUnregister( UICodeEditor* ) = 0; virtual bool onKeyDown( UICodeEditor*, const KeyEvent& ) { return false; } @@ -102,6 +111,15 @@ class EE_API UICodeEditor : public UIWidget, public TextDocument::Client { bool loadFromFile( const std::string& path ); + bool loadFromURL( + const std::string& url, + const EE::Network::Http::Request::FieldTable& headers = Http::Request::FieldTable() ); + + bool loadAsyncFromURL( const std::string& url, + const Http::Request::FieldTable& headers = Http::Request::FieldTable(), + std::function, bool )> onLoaded = + std::function, bool )>() ); + bool save(); bool save( const std::string& path ); @@ -378,6 +396,12 @@ class EE_API UICodeEditor : public UIWidget, public TextDocument::Client { void setInteractiveLinks( bool newInteractiveLinks ); + UILoader* getLoader(); + + bool getDisplayLoaderIfDocumentLoading() const; + + void setDisplayLoaderIfDocumentLoading( bool newDisplayLoaderIfDocumentLoading ); + protected: struct LastXOffset { TextPosition position; @@ -388,21 +412,22 @@ class EE_API UICodeEditor : public UIWidget, public TextDocument::Client { std::shared_ptr mDoc; Vector2f mScrollPos; Clock mBlinkTimer; - bool mDirtyEditor; - bool mCursorVisible; - bool mMouseDown; - bool mShowLineNumber; - bool mShowWhitespaces; - bool mLocked; - bool mHighlightCurrentLine; - bool mHighlightMatchingBracket; - bool mHighlightSelectionMatch; - bool mEnableColorPickerOnSelection; - bool mHorizontalScrollBarEnabled; - bool mLongestLineWidthDirty; - bool mColorPreview; - bool mInteractiveLinks; + bool mDirtyEditor{ false }; + bool mCursorVisible{ false }; + bool mMouseDown{ false }; + bool mShowLineNumber{ true }; + bool mShowWhitespaces{ true }; + bool mLocked{ false }; + bool mHighlightCurrentLine{ true }; + bool mHighlightMatchingBracket{ true }; + bool mHighlightSelectionMatch{ true }; + bool mEnableColorPickerOnSelection{ false }; + bool mHorizontalScrollBarEnabled{ false }; + bool mLongestLineWidthDirty{ true }; + bool mColorPreview{ false }; + bool mInteractiveLinks{ true }; bool mHandShown{ false }; + bool mDisplayLoaderIfDocumentLoading{ true }; Uint32 mTabWidth; Vector2f mScroll; Float mMouseWheelScroll; @@ -436,6 +461,7 @@ class EE_API UICodeEditor : public UIWidget, public TextDocument::Client { Color mPreviewColor; TextRange mPreviewColorRange; std::vector mModules; + UILoader* mLoader{ nullptr }; UICodeEditor( const std::string& elementTag, const bool& autoRegisterBaseCommands = true, const bool& autoRegisterBaseKeybindings = true ); @@ -498,7 +524,7 @@ class EE_API UICodeEditor : public UIWidget, public TextDocument::Client { virtual void onDocumentSaved( TextDocument* ); - virtual void onDocumentClosed( TextDocument* doc ); + void onDocumentClosed( TextDocument* doc ); virtual void onDocumentDirtyOnFileSystem( TextDocument* doc ); diff --git a/include/eepp/ui/uitextinput.hpp b/include/eepp/ui/uitextinput.hpp index fa17dd7fa..f7ea05d73 100644 --- a/include/eepp/ui/uitextinput.hpp +++ b/include/eepp/ui/uitextinput.hpp @@ -111,7 +111,7 @@ class EE_API UITextInput : public UITextView, public TextDocument::Client { bool mOnlyNumbers; bool mAllowFloat; bool mMouseDown; - Uint32 mMaxLength{0}; + Uint32 mMaxLength{ 0 }; KeyBindings mKeyBindings; Clock mLastDoubleClick; @@ -177,6 +177,10 @@ class EE_API UITextInput : public UITextView, public TextDocument::Client { virtual void onDocumentSaved( TextDocument* ); + void onDocumentClosed( TextDocument* ){}; + + void onDocumentDirtyOnFileSystem( TextDocument* ){}; + void registerKeybindings(); void registerCommands(); diff --git a/src/eepp/core/string.cpp b/src/eepp/core/string.cpp index b7e3c569f..07b5592a3 100644 --- a/src/eepp/core/string.cpp +++ b/src/eepp/core/string.cpp @@ -7,6 +7,7 @@ #include #include #include +#include namespace EE { @@ -721,6 +722,13 @@ int String::valueIndex( const std::string& val, const std::string& strings, int return defValue; } +std::string String::randString( size_t len, std::string dictionary ) { + std::random_device rd; + std::mt19937 generator( rd() ); + std::shuffle( dictionary.begin(), dictionary.end(), generator ); + return dictionary.substr( 0, len ); +} + std::string String::fromFloat( const Float& value, const std::string& append, const std::string& prepend ) { return prepend + toString( value ) + append; diff --git a/src/eepp/network/http.cpp b/src/eepp/network/http.cpp index 1fcc244da..22e610986 100644 --- a/src/eepp/network/http.cpp +++ b/src/eepp/network/http.cpp @@ -255,6 +255,20 @@ const std::string& Http::Request::getField( const std::string& field ) const { } } +URI Http::getEnvProxyURI() { + char* http_proxy = getenv( "http_proxy" ); + std::string httpProxy; + URI proxy; + + if ( NULL != http_proxy ) { + httpProxy = std::string( http_proxy ); + if ( !httpProxy.empty() && httpProxy.find( "://" ) == std::string::npos ) + httpProxy = "http://" + httpProxy; + proxy = URI( httpProxy ); + } + return proxy; +} + const char* Http::Response::statusToString( const Http::Response::Status& status ) { switch ( status ) { // 2xx: success @@ -834,6 +848,8 @@ Http::Response Http::downloadRequest( const Http::Request& request, IOStream& wr // Convert the request to string and send it through the connected socket std::string requestStr = toSend.prepare( *this ); + eePRINTL( "%s", requestStr.c_str() ); + if ( !requestStr.empty() ) { Socket::Status status; @@ -954,21 +970,34 @@ Http::Response Http::downloadRequest( const Http::Request& request, IOStream& wr request.getMaxRedirects() ) { std::string location( received.getField( "location" ) ); URI uri( location ); - Http http( uri.getHost(), uri.getPort(), - uri.getScheme() == "https" ? true : false ); - Http::Request newRequest( request ); - newRequest.setUri( uri.getPathEtc() ); // Close the connection if ( !mConnection->isKeepAlive() ) mConnection->disconnect(); - request.mRedirectionCount++; - eeSAFE_DELETE( chunkedStream ); eeSAFE_DELETE( inflateStream ); - return http.downloadRequest( request, writeTo, - timeout ); + + Http::Request newRequest( request ); + newRequest.setUri( uri.getPathAndQuery() ); + + request.mRedirectionCount++; + newRequest.mRedirectionCount = + request.mRedirectionCount; + + // Same host, expects a path in the same domain + if ( uri.getHost().empty() || + uri.getHost() == getHost() ) { + return downloadRequest( newRequest, writeTo, + timeout ); + } else { + // New host, we need to solve the host + Http http( uri.getHost(), uri.getPort(), + uri.getScheme() == "https" ? true + : false ); + return http.downloadRequest( request, writeTo, + timeout ); + } } } @@ -1190,7 +1219,7 @@ struct WGetAsyncRequest { Http* http; Http::Request request; Http::AsyncResponseCallback cb; - IOStream* writeTo{nullptr}; + IOStream* writeTo{ nullptr }; }; void emscripten_async_wget2_got_data( unsigned, void* vwget, void* buffer, unsigned bufferSize ) { diff --git a/src/eepp/network/uri.cpp b/src/eepp/network/uri.cpp index e3b9b9a31..a4e7554a1 100644 --- a/src/eepp/network/uri.cpp +++ b/src/eepp/network/uri.cpp @@ -225,7 +225,17 @@ std::string URI::getSchemeAndAuthority() const { return getScheme() + "://" + getAuthority(); } -void URI::getPath( const std::string& path ) { +std::string URI::getLastPathSegment() const { + std::string path( getPath() ); + if ( !path.empty() ) { + auto split = String::split( path, '/' ); + if ( !split.empty() ) + return split[split.size() - 1]; + } + return ""; +} + +void URI::setPath( const std::string& path ) { mPath.clear(); decode( path, mPath ); } diff --git a/src/eepp/ui/doc/syntaxdefinitionmanager.cpp b/src/eepp/ui/doc/syntaxdefinitionmanager.cpp index 409b0ecb9..f90a7e980 100644 --- a/src/eepp/ui/doc/syntaxdefinitionmanager.cpp +++ b/src/eepp/ui/doc/syntaxdefinitionmanager.cpp @@ -38,7 +38,7 @@ SyntaxDefinitionManager::SyntaxDefinitionManager() { }, {}, "", - { "<%?xml" } } ); + { "<%?xml", "" } } ); // CSS add( { "CSS", diff --git a/src/eepp/ui/doc/textdocument.cpp b/src/eepp/ui/doc/textdocument.cpp index 7fe1b86a2..584a84613 100644 --- a/src/eepp/ui/doc/textdocument.cpp +++ b/src/eepp/ui/doc/textdocument.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include #include #include @@ -12,6 +13,8 @@ #include #include +using namespace EE::Network; + namespace EE { namespace UI { namespace Doc { // Text document is loosely based on the SerenityOS (https://github.com/SerenityOS/serenity) @@ -75,6 +78,7 @@ bool TextDocument::loadFromStream( IOStream& file ) { } bool TextDocument::loadFromStream( IOStream& file, std::string path, bool callReset ) { + mLoading = true; Clock clock; if ( callReset ) reset(); @@ -160,6 +164,7 @@ bool TextDocument::loadFromStream( IOStream& file, std::string path, bool callRe if ( mVerbose ) Log::info( "Document \"%s\" loaded in %.2fms.", path.c_str(), clock.getElapsedTime().asMilliseconds() ); + mLoading = false; return true; } @@ -256,6 +261,7 @@ bool TextDocument::getBOM() const { } bool TextDocument::loadFromFile( const std::string& path ) { + mLoading = true; if ( !FileSystem::fileExists( path ) && PackManager::instance()->isFallbackToPacksActive() ) { std::string pathFix( path ); Pack* pack = PackManager::instance()->exists( pathFix ); @@ -272,6 +278,7 @@ bool TextDocument::loadFromFile( const std::string& path ) { mFileRealPath = FileInfo::isLink( mFilePath ) ? FileInfo( FileInfo( mFilePath ).linksTo() ) : FileInfo( mFilePath ); resetSyntax(); + mLoading = false; return ret; } @@ -291,6 +298,65 @@ bool TextDocument::loadFromPack( Pack* pack, std::string filePackPath ) { return ret; } +static std::string getTempPathFromURI( const URI& uri ) { + std::string lastSegment( uri.getLastPathSegment() ); + std::string name( String::randString( 8 ) + + ( lastSegment.empty() ? ".txt" : "." + lastSegment ) ); + std::string tmpPath( Sys::getTempPath() + name ); + return tmpPath; +} + +bool TextDocument::loadFromURL( const std::string& url, const Http::Request::FieldTable& headers ) { + URI uri( url ); + + if ( uri.getScheme().empty() ) + return false; + + mLoading = true; + + Http::Response response = + Http::get( uri, Seconds( 10 ), nullptr, headers, "", true, Http::getEnvProxyURI() ); + + if ( response.getStatus() <= Http::Response::Ok ) { + std::string path( getTempPathFromURI( uri ) ); + FileSystem::fileWrite( path, (const Uint8*)response.getBody().c_str(), + response.getBody().size() ); + loadFromFile( path ); + return true; + } + + mLoading = false; + return false; +} + +bool TextDocument::loadAsyncFromURL( const std::string& url, + const Http::Request::FieldTable& headers, + std::function onLoaded, + const Http::Request::ProgressCallback& progressCallback ) { + URI uri( url ); + + if ( uri.getScheme().empty() || ( uri.getScheme() != "https" && uri.getScheme() != "http" ) ) + return false; + + mLoading = true; + + Http::getAsync( + [=]( const Http&, Http::Request&, Http::Response& response ) { + if ( response.getStatus() <= Http::Response::Ok ) { + std::string path( getTempPathFromURI( uri ) ); + FileSystem::fileWrite( path, (const Uint8*)response.getBody().c_str(), + response.getBody().size() ); + if ( loadFromFile( path ) && onLoaded ) + onLoaded( this, true ); + } else { + onLoaded( this, false ); + } + mLoading = false; + }, + uri, Seconds( 10 ), progressCallback, headers, "", true, Http::getEnvProxyURI() ); + return true; +} + bool TextDocument::reload() { bool ret = false; std::string path( mFilePath ); @@ -402,6 +468,10 @@ void TextDocument::sanitizeCurrentSelection() { setSelection( newSelection ); } +bool TextDocument::isLoading() const { + return mLoading; +} + std::string TextDocument::getFilename() const { return FileSystem::fileNameFromPath( mFilePath ); } diff --git a/src/eepp/ui/tools/uicodeeditorsplitter.cpp b/src/eepp/ui/tools/uicodeeditorsplitter.cpp index 45a0ac534..95cc5e931 100644 --- a/src/eepp/ui/tools/uicodeeditorsplitter.cpp +++ b/src/eepp/ui/tools/uicodeeditorsplitter.cpp @@ -299,8 +299,15 @@ bool UICodeEditorSplitter::loadFileFromPath( const std::string& path, UICodeEdit if ( nullptr == codeEditor ) codeEditor = mCurEditor; codeEditor->setColorScheme( mColorSchemes[mCurrentColorScheme] ); - bool ret = codeEditor->loadFromFile( path ); - mClient->onDocumentLoaded( codeEditor, path ); + bool isUrl = String::startsWith( path, "https://" ) || String::startsWith( path, "http://" ); + bool ret = isUrl ? codeEditor->loadAsyncFromURL( + path, Http::Request::FieldTable(), + [&, codeEditor, path]( std::shared_ptr, bool ) { + mClient->onDocumentLoaded( codeEditor, path ); + } ) + : codeEditor->loadFromFile( path ); + if ( ret && !isUrl ) + mClient->onDocumentLoaded( codeEditor, path ); return ret; } diff --git a/src/eepp/ui/uicodeeditor.cpp b/src/eepp/ui/uicodeeditor.cpp index 81564d890..1ab28f21d 100644 --- a/src/eepp/ui/uicodeeditor.cpp +++ b/src/eepp/ui/uicodeeditor.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -100,20 +101,6 @@ UICodeEditor::UICodeEditor( const std::string& elementTag, const bool& autoRegis UIWidget( elementTag ), mFont( FontManager::instance()->getByName( "monospace" ) ), mDoc( std::make_shared() ), - mDirtyEditor( false ), - mCursorVisible( false ), - mMouseDown( false ), - mShowLineNumber( true ), - mShowWhitespaces( true ), - mLocked( false ), - mHighlightCurrentLine( true ), - mHighlightMatchingBracket( true ), - mHighlightSelectionMatch( true ), - mEnableColorPickerOnSelection( false ), - mHorizontalScrollBarEnabled( false ), - mLongestLineWidthDirty( true ), - mColorPreview( false ), - mInteractiveLinks( true ), mTabWidth( 4 ), mMouseWheelScroll( 50 ), mFontSize( mFontStyleConfig.getFontCharacterSize() ), @@ -284,6 +271,16 @@ void UICodeEditor::draw() { for ( auto& module : mModules ) module->postDraw( this, startScroll, lineHeight, cursor ); + + if ( mDisplayLoaderIfDocumentLoading && mDoc->isLoading() ) { + UILoader* loader = getLoader(); + loader->setParent( this ); + loader->setVisible( true ); + loader->setEnabled( false ); + loader->setPixelsSize( getPixelsSize() ); + } else if ( mLoader != nullptr && !mDoc->isLoading() && mLoader->isVisible() ) { + mLoader->setVisible( false ); + } } void UICodeEditor::scheduledUpdate( const Time& ) { @@ -337,12 +334,61 @@ void UICodeEditor::reset() { bool UICodeEditor::loadFromFile( const std::string& path ) { bool ret = mDoc->loadFromFile( path ); - invalidateEditor(); - updateLongestLineWidth(); - mHighlighter.changeDoc( mDoc.get() ); - invalidateDraw(); - DocEvent event( this, mDoc.get(), Event::OnDocumentLoaded ); - sendEvent( &event ); + if ( ret ) { + invalidateEditor(); + updateLongestLineWidth(); + mHighlighter.changeDoc( mDoc.get() ); + invalidateDraw(); + DocEvent event( this, mDoc.get(), Event::OnDocumentLoaded ); + sendEvent( &event ); + } + return ret; +} + +bool UICodeEditor::loadFromURL( const std::string& url, const Http::Request::FieldTable& headers ) { + bool ret = mDoc->loadFromURL( url, headers ); + if ( ret ) { + invalidateEditor(); + updateLongestLineWidth(); + mHighlighter.changeDoc( mDoc.get() ); + invalidateDraw(); + DocEvent event( this, mDoc.get(), Event::OnDocumentLoaded ); + sendEvent( &event ); + } + return ret; +} + +bool UICodeEditor::loadAsyncFromURL( + const std::string& url, const Http::Request::FieldTable& headers, + std::function, bool )> onLoaded ) { + bool wasLocked = isLocked(); + if ( !wasLocked ) + setLocked( true ); + bool ret = mDoc->loadAsyncFromURL( + url, headers, + [this, onLoaded, wasLocked]( TextDocument*, bool success ) { + runOnMainThread( [&, onLoaded] { + invalidateEditor(); + updateLongestLineWidth(); + mHighlighter.changeDoc( mDoc.get() ); + invalidateDraw(); + DocEvent event( this, mDoc.get(), Event::OnDocumentLoaded ); + sendEvent( &event ); + if ( !wasLocked ) + setLocked( false ); + if ( onLoaded ) + onLoaded( mDoc, success ); + } ); + }, + [&]( const Http&, const Http::Request&, const Http::Response&, + const Http::Request::Status& status, size_t /*totalBytes*/, size_t /*currentBytes*/ ) { + if ( status == Http::Request::ContentReceived ) { + runOnMainThread( [&] { invalidateDraw(); } ); + } + return true; + } ); + if ( !ret && !wasLocked ) + setLocked( false ); return ret; } @@ -822,7 +868,7 @@ Uint32 UICodeEditor::onMouseClick( const Vector2i& position, const Uint32& flags mDoc->selectLine(); } else if ( ( flags & EE_BUTTON_MMASK ) && isMouseOverMeOrChilds() ) { auto txt( getUISceneNode()->getWindow()->getClipboard()->getText() ); - if ( !txt.empty() ) { + if ( !isLocked() && !txt.empty() ) { if ( mDoc->hasSelection() ) { auto selTxt = mDoc->getSelectedText(); if ( !selTxt.empty() ) @@ -1001,6 +1047,25 @@ void UICodeEditor::setInteractiveLinks( bool newInteractiveLinks ) { mInteractiveLinks = newInteractiveLinks; } +UILoader* UICodeEditor::getLoader() { + if ( nullptr == mLoader ) + mLoader = UILoader::New(); + return mLoader; +} + +bool UICodeEditor::getDisplayLoaderIfDocumentLoading() const { + return mDisplayLoaderIfDocumentLoading; +} + +void UICodeEditor::setDisplayLoaderIfDocumentLoading( bool newDisplayLoaderIfDocumentLoading ) { + mDisplayLoaderIfDocumentLoading = newDisplayLoaderIfDocumentLoading; + if ( !mDisplayLoaderIfDocumentLoading && mLoader != nullptr && mLoader->isVisible() ) { + mLoader->setVisible( false ); + mLoader->close(); + mLoader = nullptr; + } +} + void UICodeEditor::updateEditor() { mDoc->setPageSize( getVisibleLinesCount() ); if ( mDoc->getActiveClient() == this ) diff --git a/src/eepp/ui/uimessagebox.cpp b/src/eepp/ui/uimessagebox.cpp index cb6ae9c47..f49fce483 100644 --- a/src/eepp/ui/uimessagebox.cpp +++ b/src/eepp/ui/uimessagebox.cpp @@ -39,6 +39,7 @@ UIMessageBox::UIMessageBox( const Type& type, const String& message, const Uint3 ->setParent( vlay ) ->addEventListener( Event::OnPressEnter, [&]( const Event* ) { sendCommonEvent( Event::MsgBoxConfirmClick ); + closeWindow(); } ); } diff --git a/src/tools/codeeditor/autocompletemodule.hpp b/src/tools/codeeditor/autocompletemodule.hpp index 8c2d8ba91..f79a20c48 100644 --- a/src/tools/codeeditor/autocompletemodule.hpp +++ b/src/tools/codeeditor/autocompletemodule.hpp @@ -22,6 +22,13 @@ class AutoCompleteModule : public UICodeEditorModule { virtual ~AutoCompleteModule(); + std::string getTitle() { return "Auto Complete"; } + + std::string getDescription() { + return "Auto complete shows the completion popup as you type, so you can fill\n" + "in long words by typing only a few characters."; + } + void onRegister( UICodeEditor* ); void onUnregister( UICodeEditor* ); bool onKeyDown( UICodeEditor*, const KeyEvent& ); @@ -64,27 +71,27 @@ class AutoCompleteModule : public UICodeEditorModule { Mutex mLangSymbolsMutex; Mutex mSuggestionsMutex; Mutex mDocMutex; - Time mUpdateFreq{Seconds( 5 )}; + Time mUpdateFreq{ Seconds( 5 ) }; std::unordered_map> mEditors; std::set mDocs; std::unordered_map mEditorDocs; - bool mDirty{false}; - bool mClosing{false}; - bool mReplacing{false}; + bool mDirty{ false }; + bool mClosing{ false }; + bool mReplacing{ false }; struct DocCache { - Uint64 changeId{static_cast( -1 )}; + Uint64 changeId{ static_cast( -1 ) }; SymbolsList symbols; }; std::unordered_map mDocCache; std::unordered_map mLangCache; SymbolsList mLangDirty; - int mSuggestionIndex{0}; + int mSuggestionIndex{ 0 }; std::vector mSuggestions; - Uint32 mSuggestionsMaxVisible{8}; - UICodeEditor* mSuggestionsEditor{nullptr}; + Uint32 mSuggestionsMaxVisible{ 8 }; + UICodeEditor* mSuggestionsEditor{ nullptr }; - Float mRowHeight{0}; + Float mRowHeight{ 0 }; Rectf mBoxRect; void resetSuggestions( UICodeEditor* editor ); diff --git a/src/tools/codeeditor/codeeditor.cpp b/src/tools/codeeditor/codeeditor.cpp index 37d568758..0abcbd818 100644 --- a/src/tools/codeeditor/codeeditor.cpp +++ b/src/tools/codeeditor/codeeditor.cpp @@ -222,6 +222,10 @@ void App::openFontDialog( std::string& fontPath ) { dialog->show(); } +void App::downloadFileWeb( const std::string& url ) { + loadFileFromPath( url, true ); +} + UIFileDialog* App::saveFileDialog( UICodeEditor* editor, bool focusOnClose ) { if ( !editor ) return nullptr; @@ -413,7 +417,9 @@ void App::updateRecentFiles() { const String& txt = event->getNode()->asType()->getText(); if ( txt != "Clear Menu" ) { std::string path( txt.toUtf8() ); - if ( FileSystem::fileExists( path ) && !FileSystem::isDirectory( path ) ) { + if ( ( FileSystem::fileExists( path ) && !FileSystem::isDirectory( path ) ) || + String::startsWith( path, "https://" ) || + String::startsWith( path, "http://" ) ) { loadFileFromPath( path ); } } else { @@ -1131,6 +1137,7 @@ std::map App::getLocalKeybindings() { { { KEY_F, KEYMOD_CTRL }, "find-replace" }, { { KEY_Q, KEYMOD_CTRL }, "close-app" }, { { KEY_O, KEYMOD_CTRL }, "open-file" }, + { { KEY_W, KEYMOD_CTRL | KEYMOD_SHIFT }, "download-file-web" }, { { KEY_O, KEYMOD_CTRL | KEYMOD_SHIFT }, "open-folder" }, { { KEY_F6, KEYMOD_NONE }, "debug-draw-highlight-toggle" }, { { KEY_F7, KEYMOD_NONE }, "debug-draw-boxes-toggle" }, @@ -1145,9 +1152,9 @@ std::map App::getLocalKeybindings() { } std::vector App::getUnlockedCommands() { - return { "fullscreen-toggle", "open-file", "open-folder", - "console-toggle", "close-app", "open-locatebar", - "open-global-search", "menu-toggle", "switch-side-panel" }; + return { "fullscreen-toggle", "open-file", "open-folder", "console-toggle", + "close-app", "open-locatebar", "open-global-search", "menu-toggle", + "switch-side-panel", "download-file-web" }; } void App::closeEditors() { @@ -1388,6 +1395,21 @@ void App::onCodeEditorCreated( UICodeEditor* editor, TextDocument& doc ) { doc.setCommand( "load-current-dir", [&] { loadCurrentDirectory(); } ); doc.setCommand( "menu-toggle", [&] { toggleSettingsMenu(); } ); doc.setCommand( "switch-side-panel", [&] { switchSidePanel(); } ); + doc.setCommand( "download-file-web", [&] { + UIMessageBox* msgBox = + UIMessageBox::New( UIMessageBox::INPUT, "Please enter the file URL..." ); + + msgBox->setTitle( mWindowTitle ); + msgBox->getTextInput()->setHint( "Any https or http URL" ); + msgBox->setCloseShortcut( { KEY_ESCAPE, 0 } ); + msgBox->showWhenReady(); + msgBox->addEventListener( Event::MsgBoxConfirmClick, [&, msgBox]( const Event* ) { + std::string url( msgBox->getTextInput()->getText().toUtf8() ); + downloadFileWeb( url ); + if ( mEditorSplitter->getCurEditor() ) + mEditorSplitter->getCurEditor()->setFocus(); + } ); + } ); editor->addEventListener( Event::OnDocumentSave, [&]( const Event* event ) { UICodeEditor* editor = event->getNode()->asType(); @@ -1444,15 +1466,15 @@ void App::onCodeEditorCreated( UICodeEditor* editor, TextDocument& doc ) { if ( config.autoComplete && !mAutoCompleteModule ) setAutoComplete( config.autoComplete ); - if ( config.autoComplete && mAutoCompleteModule ) - editor->registerModule( mAutoCompleteModule ); - if ( config.linter && !mLinterModule ) setLinter( config.linter ); if ( config.formatter && !mFormatterModule ) setFormatter( config.formatter ); + if ( config.autoComplete && mAutoCompleteModule ) + editor->registerModule( mAutoCompleteModule ); + if ( config.linter && mLinterModule ) editor->registerModule( mLinterModule ); @@ -1561,6 +1583,8 @@ void App::createSettingsMenu() { mSettingsMenu->add( "Open File...", findIcon( "document-open" ), getKeybind( "open-file" ) ); mSettingsMenu->add( "Open Folder...", findIcon( "document-open" ), getKeybind( "open-folder" ) ); + mSettingsMenu->add( "Open File from Web...", findIcon( "download-cloud" ), + getKeybind( "download-file-web" ) ); mSettingsMenu->addSubMenu( "Recent Files", findIcon( "document-recent" ), UIPopUpMenu::New() ); mSettingsMenu->addSubMenu( "Recent Folders", findIcon( "document-recent" ), UIPopUpMenu::New() ); @@ -1595,6 +1619,8 @@ void App::createSettingsMenu() { runCommand( "open-file" ); } else if ( name == "Open Folder..." ) { runCommand( "open-folder" ); + } else if ( name == "Open File from Web..." ) { + runCommand( "download-file-web" ); } else if ( name == "Save" ) { runCommand( "save-doc" ); } else if ( name == "Save as..." ) { @@ -2339,6 +2365,7 @@ void App::init( const std::string& file, const Float& pidelDensity ) { { "list-view", 0xecf1 }, { "menu-unfold", 0xef40 }, { "menu-fold", 0xef3d }, + { "download-cloud", 0xec58 }, }; for ( const auto& icon : icons ) iconTheme->add( UIGlyphIcon::New( icon.first, iconFont, icon.second ) ); diff --git a/src/tools/codeeditor/codeeditor.hpp b/src/tools/codeeditor/codeeditor.hpp index 286a54eef..f15af5506 100644 --- a/src/tools/codeeditor/codeeditor.hpp +++ b/src/tools/codeeditor/codeeditor.hpp @@ -34,6 +34,8 @@ class App : public UICodeEditorSplitter::Client { void openFontDialog( std::string& fontPath ); + void downloadFileWeb( const std::string& url ); + UIFileDialog* saveFileDialog( UICodeEditor* editor, bool focusOnClose = true ); void closeApp(); diff --git a/src/tools/codeeditor/formattermodule.cpp b/src/tools/codeeditor/formattermodule.cpp index 2c9b62790..db90824b5 100644 --- a/src/tools/codeeditor/formattermodule.cpp +++ b/src/tools/codeeditor/formattermodule.cpp @@ -80,13 +80,6 @@ void FormatterModule::load( const std::string& formatterPath ) { Log::error( "Parsing formatter failed:\n%s", e.what() ); } } -static std::string randString( size_t len ) { - std::string str( "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" ); - std::random_device rd; - std::mt19937 generator( rd() ); - std::shuffle( str.begin(), str.end(), generator ); - return str.substr( 0, len ); -} void FormatterModule::formatDoc( UICodeEditor* editor ) { if ( !mReady ) @@ -102,11 +95,12 @@ void FormatterModule::formatDoc( UICodeEditor* editor ) { if ( doc->isDirty() || !doc->hasFilepath() || formatter.type == FormatterType::Inplace ) { std::string tmpPath; if ( !doc->hasFilepath() ) { - tmpPath = Sys::getTempPath() + ".ecode-" + doc->getFilename() + "." + randString( 8 ); + tmpPath = + Sys::getTempPath() + ".ecode-" + doc->getFilename() + "." + String::randString( 8 ); } else { std::string fileDir( FileSystem::fileRemoveFileName( doc->getFilePath() ) ); FileSystem::dirAddSlashAtEnd( fileDir ); - tmpPath = fileDir + "." + randString( 8 ) + "." + doc->getFilename(); + tmpPath = fileDir + "." + String::randString( 8 ) + "." + doc->getFilename(); } doc->save( fileString, true ); diff --git a/src/tools/codeeditor/formattermodule.hpp b/src/tools/codeeditor/formattermodule.hpp index fbeb63188..8855c22c3 100644 --- a/src/tools/codeeditor/formattermodule.hpp +++ b/src/tools/codeeditor/formattermodule.hpp @@ -16,6 +16,10 @@ class FormatterModule : public UICodeEditorModule { virtual ~FormatterModule(); + std::string getTitle() { return "Auto Formatter"; } + + std::string getDescription() { return "Enables the code formatter/prettifier module."; } + void onRegister( UICodeEditor* ); void onUnregister( UICodeEditor* ); diff --git a/src/tools/codeeditor/lintermodule.hpp b/src/tools/codeeditor/lintermodule.hpp index 6655bf563..d8bee22ee 100644 --- a/src/tools/codeeditor/lintermodule.hpp +++ b/src/tools/codeeditor/lintermodule.hpp @@ -39,6 +39,13 @@ class LinterModule : public UICodeEditorModule { virtual ~LinterModule(); + std::string getTitle() { return "Linter"; } + + std::string getDescription() { + return "Use static code analysis tool used to flag programming errors, bugs,\n" + "stylistic errors, and suspicious constructs."; + } + void onRegister( UICodeEditor* ); void onUnregister( UICodeEditor* );