From 89028491ef392d8c9873411f5aaf15a5ddda0edf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Lucas=20Golini?= Date: Sat, 23 May 2026 14:17:48 -0300 Subject: [PATCH] Try fix macOS not passing tests due to incorrect Window sizing in macOS CI. Add support to diff images in UIDiffView. --- .../assets/html/eepp-ui-background-atlas.webp | Bin 6532 -> 6526 bytes .../assets/html/eepp-ui-border-rendering.webp | Bin 5158 -> 5158 bytes .../html/eepp-uihtmltable-complex-layout.webp | Bin 13546 -> 13548 bytes include/eepp/ui/tools/uidiffview.hpp | 38 +- include/eepp/ui/tools/uiimageviewer.hpp | 8 +- src/eepp/ui/tools/uicodeeditorsplitter.cpp | 9 +- src/eepp/ui/tools/uidiffview.cpp | 438 +++++++++++++++++- src/eepp/ui/tools/uiimageviewer.cpp | 6 +- src/tests/unit_tests/uidiffview_tests.cpp | 59 +++ src/tests/unit_tests/uihtml_tests.cpp | 6 +- src/tools/ecode/ecode.cpp | 10 +- src/tools/ecode/ecode.hpp | 4 +- src/tools/ecode/plugins/git/git.cpp | 11 + src/tools/ecode/plugins/git/git.hpp | 3 + src/tools/ecode/plugins/git/gitplugin.cpp | 42 +- .../ecode/plugins/plugincontextprovider.hpp | 4 +- 16 files changed, 609 insertions(+), 29 deletions(-) diff --git a/bin/unit_tests/assets/html/eepp-ui-background-atlas.webp b/bin/unit_tests/assets/html/eepp-ui-background-atlas.webp index 82ae7a50c766410322873f2f2fc728792ac3241c..992b3c4e771de0dc42b26a7b29393785dbb9adb9 100644 GIT binary patch delta 5931 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 diff --git a/bin/unit_tests/assets/html/eepp-ui-border-rendering.webp b/bin/unit_tests/assets/html/eepp-ui-border-rendering.webp index 1772856a9a8a0773fa7361d46449792baa08b95e..dcbdb989b3219f194946877f0474c49d4ecb90d5 100644 GIT binary patch delta 37 rcmZ3cu}nie$kWYDPK1FW+|?-{EWpA?2*}p|e|V!fw+IWjU #include -namespace EE { namespace UI { +namespace EE { + +namespace Graphics { +class Sprite; +} + +namespace UI { class UIScrollView; namespace Tools { +class UIImageViewer; class UIDiffEditorPlugin; class EE_API UIDiffView : public UIWidget, public WidgetCommandExecuter { @@ -21,7 +28,8 @@ class EE_API UIDiffView : public UIWidget, public WidgetCommandExecuter { static UIDiffView* New(); - static UIScrollView* NewMultiFileDiffViewer( const std::string& patchText ); + static UIScrollView* NewMultiFileDiffViewer( const std::string& patchText, + const std::string& repoPath = "" ); static std::vector splitDiff( const std::string& multiFileDiff ); @@ -32,7 +40,8 @@ class EE_API UIDiffView : public UIWidget, public WidgetCommandExecuter { virtual Uint32 getType() const override; virtual bool isType( const Uint32& type ) const override; - void loadFromPatch( const std::string& patchText, const std::string& originalFilePath = "" ); + void loadFromPatch( const std::string& patchText, const std::string& originalFilePath = "", + const std::string& oldFilePath = "", const std::string& repoPath = "" ); void loadFromStrings( const std::string& oldText, const std::string& newText, const std::string& originalFilePath = "" ); void loadFromFile( const std::string& oldFilePath, const std::string& newFilePath ); @@ -40,6 +49,8 @@ class EE_API UIDiffView : public UIWidget, public WidgetCommandExecuter { UICodeEditor* getEditor() const { return mEditor; } UICodeEditor* getLeftEditor() const { return mLeftEditor; } UICodeEditor* getRightEditor() const { return mRightEditor; } + UIImageViewer* getLeftImageViewer() const { return mLeftImageViewer; } + UIImageViewer* getRightImageViewer() const { return mRightImageViewer; } enum class DiffLineType { Common, Added, Removed, Header }; struct DiffLine { @@ -76,12 +87,20 @@ class EE_API UIDiffView : public UIWidget, public WidgetCommandExecuter { const String& getFileName() const { return mFileName; } + bool isImageDiff() const { return mIsImageDiff; } + + void setAutoDeleteOldTempImage( bool set ) { mAutoDeleteOldTempImage = set; } + protected: UICodeEditor* mEditor{ nullptr }; UICodeEditor* mLeftEditor{ nullptr }; UICodeEditor* mRightEditor{ nullptr }; + UIImageViewer* mLeftImageViewer{ nullptr }; + UIImageViewer* mRightImageViewer{ nullptr }; + UIImageViewer* mDiffImageViewer{ nullptr }; UISelectButton* mModeToggle{ nullptr }; UISelectButton* mCompleteViewToggle{ nullptr }; + Sprite* mSprite{ nullptr }; std::unique_ptr mPlugin; std::unique_ptr mLeftPlugin; std::unique_ptr mRightPlugin; @@ -93,8 +112,12 @@ class EE_API UIDiffView : public UIWidget, public WidgetCommandExecuter { bool mShowCompleteView{ false }; bool mCompleteViewToggleVisible{ true }; bool mHeadersVisible{ false }; + bool mIsImageDiff{ false }; + bool mAutoDeleteOldTempImage{ false }; std::shared_ptr mSyntaxDef; String mFileName; + std::string mImageDiffOldPath; + std::string mImageDiffNewPath; UIDiffView(); @@ -112,9 +135,16 @@ class EE_API UIDiffView : public UIWidget, public WidgetCommandExecuter { void computeSubLineDiff( DiffLine& oldLine, DiffLine& newLine ); void updateEditorsText(); void updateButtonsText(); + void createImageViewers(); + bool loadImageDiffFromPaths( const std::string& oldFilePath, const std::string& newFilePath ); + void updateImageDiffView(); + void resetToTextDiffView(); + void imageDisplayState( bool& displayDiffImage, bool& displayLeftImage ); + void updateImagesPosAndSize(); }; } // namespace Tools -}} // namespace EE::UI +} // namespace UI +} // namespace EE #endif // EE_UI_TOOLS_UIDIFFVIEW_HPP diff --git a/include/eepp/ui/tools/uiimageviewer.hpp b/include/eepp/ui/tools/uiimageviewer.hpp index 6e6125b5d..0bbec2580 100644 --- a/include/eepp/ui/tools/uiimageviewer.hpp +++ b/include/eepp/ui/tools/uiimageviewer.hpp @@ -74,6 +74,12 @@ class EE_API UIImageViewer : public UIWidget { virtual std::vector getPropertiesImplemented() const override; + void resetImageView(); + + bool isLoading() const { return mLoading; } + + bool hasImage() const; + protected: UILoader* mLoader{ nullptr }; UIImage* mImage{ nullptr }; @@ -104,8 +110,6 @@ class EE_API UIImageViewer : public UIWidget { virtual Uint32 onKeyDown( const KeyEvent& event ) override; void updateTextDisplay(); - - void resetImageView(); }; } // namespace EE::UI::Tools diff --git a/src/eepp/ui/tools/uicodeeditorsplitter.cpp b/src/eepp/ui/tools/uicodeeditorsplitter.cpp index f53a0b450..c87e8766c 100644 --- a/src/eepp/ui/tools/uicodeeditorsplitter.cpp +++ b/src/eepp/ui/tools/uicodeeditorsplitter.cpp @@ -577,8 +577,10 @@ UICodeEditorSplitter::createWidget( UIWidget* widget, const std::string& tabName UITabWidget* tabWidget = nullptr; UIWidget* curWidget = getCurWidget(); - if ( !curWidget ) + if ( !curWidget ) { + Log::error( "UICodeEditorSplitter::createWidget curWidget was nullptr!" ); return std::make_pair( (UITab*)nullptr, (UIWidget*)nullptr ); + } tabWidget = tabWidgetFromWidget( curWidget ); if ( !tabWidget ) { @@ -600,6 +602,11 @@ UICodeEditorSplitter::createWidgetInTabWidget( UITabWidget* tabWidget, UIWidget* return std::make_pair( (UITab*)nullptr, (UIWidget*)nullptr ); UITab* tab = tabWidget->add( tabName, widget ); widget->setData( (UintPtr)tab ); + // We use both events because there was an strange behavior that sometimes OnFocusWithin was not + // enough, so this is just in case. + widget->on( Event::OnFocus, [this]( const Event* event ) { + setCurrentWidget( event->getNode()->asType() ); + } ); widget->on( Event::OnFocusWithin, [this]( const Event* event ) { setCurrentWidget( event->getNode()->asType() ); } ); diff --git a/src/eepp/ui/tools/uidiffview.cpp b/src/eepp/ui/tools/uidiffview.cpp index 9fb860a05..47afee452 100644 --- a/src/eepp/ui/tools/uidiffview.cpp +++ b/src/eepp/ui/tools/uidiffview.cpp @@ -1,11 +1,16 @@ #include +#include #include +#include #include +#include #include #include #include #include #include +#include +#include #include #include #include @@ -16,7 +21,63 @@ namespace EE { namespace UI { namespace Tools { -UIScrollView* UIDiffView::NewMultiFileDiffViewer( const std::string& patchText ) { +static bool imagesHaveSameDimensions( const std::string& oldFilePath, + const std::string& newFilePath ) { + int oldWidth = 0; + int oldHeight = 0; + int oldChannels = 0; + int newWidth = 0; + int newHeight = 0; + int newChannels = 0; + + return Image::getInfo( oldFilePath, &oldWidth, &oldHeight, &oldChannels ) && + Image::getInfo( newFilePath, &newWidth, &newHeight, &newChannels ) && + oldWidth == newWidth && oldHeight == newHeight; +} + +static bool setImageViewerImageSize( UIImageViewer* viewer ) { + if ( !viewer || !viewer->getImage() || !viewer->getImage()->getDrawable() ) + return false; + + auto imageSize( viewer->getImage()->getDrawable()->getPixelsSize() ); + auto viewerSize( viewer->getPixelsSize() ); + auto scale( + viewerSize.x > 0 && viewerSize.y > 0 && + ( imageSize.x > viewerSize.x || imageSize.y > viewerSize.y ) + ? eemin( viewerSize.x / imageSize.getWidth(), viewerSize.y / imageSize.getHeight() ) + : 1.f ); + viewer->getImage()->setPixelsSize( eefloor( imageSize.x * scale ), + eefloor( imageSize.y * scale ) ); + viewer->getImage()->setScale( 1 ); + viewer->getImage()->setPixelsPosition( Vector2f::Zero ); + viewer->getImage()->setRotation( 0 ); + viewer->getImage()->setVisible( true ); + viewer->getImage()->center(); + return true; +} + +static Sprite* setImageViewerImage( UIImageViewer* viewer, Image* image ) { + if ( !viewer || !image || !image->getPixelsPtr() ) + return nullptr; + + auto texture = TextureFactory::instance()->loadFromPixels( + image->getPixelsPtr(), image->getWidth(), image->getHeight(), image->getChannels() ); + if ( !texture ) + return nullptr; + + auto sprite = Sprite::New(); + sprite->createStatic( texture ); + sprite->setAsTextureOwner( true ); + sprite->setAsTextureRegionOwner( true ); + + viewer->reset(); + viewer->getImage()->setDrawable( sprite, true ); + setImageViewerImageSize( viewer ); + return sprite; +} + +UIScrollView* UIDiffView::NewMultiFileDiffViewer( const std::string& patchText, + const std::string& repoPath ) { auto scrollView = UIScrollView::New(); auto vbox = UILinearLayout::NewVertical(); vbox->setParent( scrollView ); @@ -29,7 +90,7 @@ UIScrollView* UIDiffView::NewMultiFileDiffViewer( const std::string& patchText ) diffView->setLayoutSizePolicy( SizePolicy::MatchParent, SizePolicy::WrapContent ); diffView->setParent( vbox ); diffView->setHeadersVisible( true ); - diffView->loadFromPatch( diff ); + diffView->loadFromPatch( diff, "", "", repoPath ); } return scrollView; @@ -302,6 +363,7 @@ UIDiffView::UIDiffView() : createEditor( mEditor, mPlugin ); createEditor( mLeftEditor, mLeftPlugin ); createEditor( mRightEditor, mRightPlugin ); + createImageViewers(); mEditor->on( Event::OnFontChanged, [this]( auto ) { mPlugin->registerUpdate( mEditor ); } ); mLeftEditor->on( Event::OnFontChanged, [this]( auto ) { @@ -351,6 +413,7 @@ UIDiffView::UIDiffView() : mCompleteViewToggle->on( Event::OnSizeChange, [this]( auto ) { updateModeButton(); } ); updateButtonsText(); + updateImagesPosAndSize(); } UIDiffView::~UIDiffView() { @@ -362,6 +425,9 @@ UIDiffView::~UIDiffView() { if ( mRightEditor && mRightPlugin ) mRightEditor->unregisterPlugin( mRightPlugin.get() ); + + if ( mAutoDeleteOldTempImage ) + FileSystem::fileRemove( mImageDiffOldPath ); } Uint32 UIDiffView::getType() const { @@ -384,12 +450,50 @@ void UIDiffView::createEditor( UICodeEditor*& editor, editor->registerPlugin( plugin.get() ); } +void UIDiffView::createImageViewers() { + const auto initImageView = [this] { + auto iv = UIImageViewer::New(); + iv->setParent( this ); + iv->setVisible( false ); + iv->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed ); + iv->setDisplayOptions( UIImageViewer::DisplayDimensions ); + return iv; + }; + mLeftImageViewer = initImageView(); + mRightImageViewer = initImageView(); + mDiffImageViewer = initImageView(); +} + +void UIDiffView::resetToTextDiffView() { + mIsImageDiff = false; + mImageDiffOldPath.clear(); + mImageDiffNewPath.clear(); + if ( mLeftImageViewer ) { + mLeftImageViewer->reset(); + mLeftImageViewer->setVisible( false ); + } + if ( mRightImageViewer ) { + mRightImageViewer->reset(); + mRightImageViewer->setVisible( false ); + } + mEditor->setVisible( mViewMode == ViewMode::Unified ); + mLeftEditor->setVisible( mViewMode == ViewMode::SideBySide ); + mRightEditor->setVisible( mViewMode == ViewMode::SideBySide ); +} + void UIDiffView::setViewMode( ViewMode mode ) { if ( mViewMode == mode ) return; mViewMode = mode; + if ( mIsImageDiff ) { + onSizeChange(); + updateImageDiffView(); + updateButtonsText(); + return; + } + mLeftPlugin->registerUpdate( mLeftEditor ); mRightPlugin->registerUpdate( mRightEditor ); @@ -422,6 +526,10 @@ void UIDiffView::setCompleteView( bool complete ) { return; mShowCompleteView = complete; updateButtonsText(); + if ( mIsImageDiff ) { + updateImageDiffView(); + return; + } updateEditorsText(); } @@ -463,23 +571,83 @@ void UIDiffView::updateModeButton() { } void UIDiffView::onSizePolicyChange() { - if ( mHeightPolicy == SizePolicy::WrapContent && mEditor && mLeftEditor ) { - mEditor->setLayoutHeightPolicy( mHeightPolicy ); - mLeftEditor->setLayoutHeightPolicy( mHeightPolicy ); - mRightEditor->setLayoutHeightPolicy( mHeightPolicy ); + if ( mHeightPolicy == SizePolicy::WrapContent ) { + if ( mEditor ) + mEditor->setLayoutHeightPolicy( mHeightPolicy ); + if ( mLeftEditor ) + mLeftEditor->setLayoutHeightPolicy( mHeightPolicy ); + if ( mRightEditor ) + mRightEditor->setLayoutHeightPolicy( mHeightPolicy ); + onAutoSize(); } } void UIDiffView::onAutoSize() { - if ( mHeightPolicy == SizePolicy::WrapContent && mEditor && mLeftEditor ) { + if ( mHeightPolicy != SizePolicy::WrapContent ) + return; + + if ( mEditor && mLeftEditor && !mIsImageDiff ) { setPixelsSize( getPixelsSize().getWidth(), mViewMode == ViewMode::Unified ? mEditor->getPixelsSize().getHeight() : mLeftEditor->getPixelsSize().getHeight() ); } + + if ( mIsImageDiff && mLeftImageViewer && mRightImageViewer && mDiffImageViewer ) { + bool displayDiffImage; + bool displayLeftImage; + imageDisplayState( displayDiffImage, displayLeftImage ); + Float height = PixelDensity::dpToPx( 64 ); // force a min height + auto viewImageHeight = []( auto iv ) -> Float { + if ( iv && iv->getImage() && iv->getImage()->getDrawable() ) + return iv->getImage()->getDrawable()->getPixelsSize().getHeight(); + return 0; + }; + + height = std::max( height, viewImageHeight( mLeftImageViewer ) ); + height = std::max( height, viewImageHeight( mRightImageViewer ) ); + + if ( displayDiffImage ) + height = std::max( height, viewImageHeight( mDiffImageViewer ) ); + + setPixelsSize( getPixelsSize().getWidth(), height ); + } +} + +void UIDiffView::updateImagesPosAndSize() { + const Sizef size( getPixelsSize() ); + + bool displayDiffImage; + bool displayLeftImage; + imageDisplayState( displayDiffImage, displayLeftImage ); + + mLeftImageViewer->setVisible( true ); + mLeftImageViewer->setPixelsPosition( 0, 0 ); + mLeftImageViewer->setPixelsSize( { size.getWidth() * 0.5f, size.getHeight() } ); + setImageViewerImageSize( mLeftImageViewer ); + + mRightImageViewer->setVisible( true ); + mRightImageViewer->setPixelsSize( + displayLeftImage ? Sizef{ size.getWidth() * 0.5f, size.getHeight() } : size ); + mRightImageViewer->setPixelsPosition( + displayLeftImage ? std::floor( size.getWidth() * 0.5f ) : 0.f, 0.f ); + setImageViewerImageSize( mRightImageViewer ); + + mDiffImageViewer->setVisible( true ); + mDiffImageViewer->setPixelsPosition( 0, 0 ); + mDiffImageViewer->setPixelsSize( size ); + setImageViewerImageSize( mDiffImageViewer ); + + onAutoSize(); + updateModeButton(); } void UIDiffView::onSizeChange() { + if ( mIsImageDiff ) { + updateImagesPosAndSize(); + return; + } + if ( mViewMode == ViewMode::Unified ) { mEditor->setPixelsSize( { getPixelsSize().getWidth(), mHeightPolicy == SizePolicy::WrapContent @@ -559,6 +727,110 @@ void UIDiffView::updateEditorsText() { } } +bool UIDiffView::loadImageDiffFromPaths( const std::string& oldFilePath, + const std::string& newFilePath ) { + const bool hasOldImage = !oldFilePath.empty() && FileSystem::fileExists( oldFilePath ) && + Image::isImage( oldFilePath ); + const bool hasNewImage = !newFilePath.empty() && FileSystem::fileExists( newFilePath ) && + Image::isImage( newFilePath ); + if ( !hasOldImage && !hasNewImage ) + return false; + + mLines.clear(); + mViewLines.clear(); + mSyntaxDef.reset(); + mImageDiffOldPath = hasOldImage ? oldFilePath : ""; + mImageDiffNewPath = hasNewImage ? newFilePath : ""; + mIsImageDiff = true; + + mEditor->setVisible( false ); + mLeftEditor->setVisible( false ); + mRightEditor->setVisible( false ); + + if ( !mImageDiffNewPath.empty() ) + mFileName = FileSystem::fileNameFromPath( mImageDiffNewPath ); + else + mFileName = FileSystem::fileNameFromPath( mImageDiffOldPath ); + + if ( !mImageDiffOldPath.empty() && !mImageDiffNewPath.empty() ) + mViewMode = ViewMode::SideBySide; + else + mViewMode = ViewMode::Unified; + + setCompleteViewToggleVisible( !mImageDiffOldPath.empty() && !mImageDiffNewPath.empty() ); + updateButtonsText(); + onSizeChange(); + updateImageDiffView(); + return true; +} + +void UIDiffView::imageDisplayState( bool& displayDiffImage, bool& displayLeftImage ) { + if ( !mIsImageDiff ) { + displayDiffImage = displayLeftImage = false; + return; + } + + const bool showVisualDiff = + !mShowCompleteView && !mImageDiffOldPath.empty() && !mImageDiffNewPath.empty(); + const bool sameDimension = imagesHaveSameDimensions( mImageDiffOldPath, mImageDiffNewPath ); + displayDiffImage = showVisualDiff && sameDimension && mViewMode == ViewMode::Unified; + displayLeftImage = + !displayDiffImage && !mImageDiffOldPath.empty() && mViewMode == ViewMode::SideBySide; +} + +void UIDiffView::updateImageDiffView() { + if ( !mIsImageDiff ) + return; + + bool displayDiffImage; + bool displayLeftImage; + imageDisplayState( displayDiffImage, displayLeftImage ); + + mEditor->setVisible( false ); + mLeftEditor->setVisible( false ); + mRightEditor->setVisible( false ); + + mDiffImageViewer->setVisible( false ); + + if ( displayLeftImage ) { + mLeftImageViewer->setVisible( true ); + if ( !mLeftImageViewer->hasImage() ) + mLeftImageViewer->loadImageAsync( mImageDiffOldPath, false, false ); + } else { + mLeftImageViewer->reset(); + mLeftImageViewer->setVisible( false ); + } + + if ( displayDiffImage ) { + if ( nullptr == mSprite ) { + Image oldImage( mImageDiffOldPath, 4 ); + Image newImage( mImageDiffNewPath, 4 ); + if ( oldImage.getPixelsPtr() && newImage.getPixelsPtr() ) { + auto diff = oldImage.diff( newImage ); + if ( diff.diffImage ) + mSprite = setImageViewerImage( mDiffImageViewer, diff.diffImage ); + } + } + + mLeftImageViewer->setVisible( false ); + mRightImageViewer->setVisible( false ); + mDiffImageViewer->setVisible( true ); + return; + } + + const std::string& displayPath = + mImageDiffNewPath.empty() ? mImageDiffOldPath : mImageDiffNewPath; + + if ( !displayPath.empty() ) { + mRightImageViewer->setVisible( true ); + if ( !mRightImageViewer->hasImage() ) + mRightImageViewer->loadImageAsync( displayPath, false, false ); + } else { + mRightImageViewer->reset(); + mRightImageViewer->setVisible( false ); + } +} + void UIDiffView::computeSubLineDiff( DiffLine& oldLine, DiffLine& newLine ) { dtl::Diff diff( oldLine.text.view(), newLine.text.view() ); @@ -660,8 +932,127 @@ static void applySubLineDiff( } } -void UIDiffView::loadFromPatch( const std::string& patchText, - const std::string& originalFilePath ) { +struct BinaryImagePatch { + std::string oldPath; + std::string newPath; + std::string fileName; + bool isBinary{ false }; +}; + +static std::string stripDiffPathPrefix( std::string path ) { + path = String::trim( path ); + if ( String::startsWith( path, "\"" ) && String::endsWith( path, "\"" ) && path.size() > 1 ) + path = path.substr( 1, path.size() - 2 ); + if ( String::startsWith( path, "a/" ) || String::startsWith( path, "b/" ) ) + path = path.substr( 2 ); + return path; +} + +static bool isPatchNullFile( const std::string& path ) { + return stripDiffPathPrefix( path ) == "/dev/null"; +} + +static bool isImagePath( const std::string& path ) { + return !path.empty() && !isPatchNullFile( path ) && Image::isImageExtension( path ); +} + +static std::string resolveImagePatchPath( const std::string& patchPath, + const std::string& originalFilePath, + bool preferOriginalFile, const std::string& repoPath ) { + if ( patchPath.empty() || isPatchNullFile( patchPath ) ) + return ""; + + const std::string cleanPath( stripDiffPathPrefix( patchPath ) ); + + if ( preferOriginalFile && !originalFilePath.empty() && + Image::isImageExtension( originalFilePath ) && + FileSystem::fileExists( originalFilePath ) ) { + return originalFilePath; + } + + if ( FileSystem::fileExists( patchPath ) ) + return patchPath; + if ( FileSystem::fileExists( cleanPath ) ) + return cleanPath; + + if ( !repoPath.empty() ) { + std::string fullPath = repoPath + cleanPath; + if ( FileSystem::fileExists( fullPath ) ) + return fullPath; + } + + if ( !originalFilePath.empty() ) { + std::string basePath( FileSystem::fileRemoveFileName( originalFilePath ) ); + if ( !basePath.empty() ) { + std::string path( basePath + cleanPath ); + if ( FileSystem::fileExists( path ) ) + return path; + path = basePath + patchPath; + if ( FileSystem::fileExists( path ) ) + return path; + } + } + + return ""; +} + +static BinaryImagePatch parseBinaryImagePatch( const std::vector& lines, + const std::string& originalFilePath ) { + BinaryImagePatch patch; + + for ( const auto& line : lines ) { + if ( String::startsWith( line, "diff --git " ) ) { + auto paths = String::split( line.substr( 11 ), ' ', true ); + if ( paths.size() >= 2 ) { + patch.oldPath = paths[0]; + patch.newPath = paths[1]; + } + } else if ( String::startsWith( line, "--- " ) ) { + patch.oldPath = line.substr( 4 ); + } else if ( String::startsWith( line, "+++ " ) ) { + patch.newPath = line.substr( 4 ); + } else if ( String::startsWith( line, "Binary files " ) ) { + patch.isBinary = true; + const size_t oldStart = sizeof( "Binary files " ) - 1; + const size_t andPos = line.find( " and ", oldStart ); + const size_t differPos = line.rfind( " differ" ); + if ( andPos != std::string::npos && differPos != std::string::npos && + andPos < differPos ) { + patch.oldPath = line.substr( oldStart, andPos - oldStart ); + patch.newPath = line.substr( andPos + 5, differPos - andPos - 5 ); + } + } else if ( line == "GIT binary patch" || String::startsWith( line, "literal " ) ) { + patch.isBinary = true; + } + } + + if ( !patch.newPath.empty() && !isPatchNullFile( patch.newPath ) ) + patch.fileName = FileSystem::fileNameFromPath( stripDiffPathPrefix( patch.newPath ) ); + else if ( !patch.oldPath.empty() && !isPatchNullFile( patch.oldPath ) ) + patch.fileName = FileSystem::fileNameFromPath( stripDiffPathPrefix( patch.oldPath ) ); + else if ( !originalFilePath.empty() ) + patch.fileName = FileSystem::fileNameFromPath( originalFilePath ); + + if ( !patch.isBinary && ( isImagePath( patch.oldPath ) || isImagePath( patch.newPath ) ) ) { + for ( const auto& line : lines ) { + if ( String::startsWith( line, "Binary " ) || line == "GIT binary patch" ) { + patch.isBinary = true; + break; + } + } + } + + if ( patch.isBinary && !isImagePath( patch.oldPath ) && !isImagePath( patch.newPath ) && + !Image::isImageExtension( originalFilePath ) ) { + patch.isBinary = false; + } + + return patch; +} + +void UIDiffView::loadFromPatch( const std::string& patchText, const std::string& originalFilePath, + const std::string& oldFilePath, const std::string& repoPath ) { + resetToTextDiffView(); mLines.clear(); auto lines = String::split( patchText, '\n', true ); @@ -680,6 +1071,26 @@ void UIDiffView::loadFromPatch( const std::string& patchText, Int64 expectedNewLineNum = 1; std::string filename; + auto imagePatch = parseBinaryImagePatch( lines, originalFilePath ); + if ( imagePatch.isBinary ) { + std::string oldImagePath( oldFilePath ); + if ( oldImagePath.empty() ) { + oldImagePath = + resolveImagePatchPath( imagePatch.oldPath, originalFilePath, false, repoPath ); + } + std::string newImagePath( + resolveImagePatchPath( imagePatch.newPath, originalFilePath, true, repoPath ) ); + + if ( oldImagePath == newImagePath ) + oldImagePath.clear(); + + if ( loadImageDiffFromPaths( oldImagePath, newImagePath ) ) { + if ( !imagePatch.fileName.empty() ) + mFileName = std::move( imagePatch.fileName ); + return; + } + } + for ( const auto& line : lines ) { if ( String::startsWith( line, "diff " ) || String::startsWith( line, "index " ) || String::startsWith( line, "--- " ) || String::startsWith( line, "+++ " ) ) { @@ -795,6 +1206,7 @@ void UIDiffView::loadFromPatch( const std::string& patchText, void UIDiffView::loadFromStrings( const std::string& oldText, const std::string& newText, const std::string& originalFilePath ) { + resetToTextDiffView(); mLines.clear(); std::vector leftLines = String::split( oldText, '\n', true ); @@ -845,6 +1257,13 @@ void UIDiffView::loadFromStrings( const std::string& oldText, const std::string& } void UIDiffView::loadFromFile( const std::string& oldFilePath, const std::string& newFilePath ) { + if ( ( oldFilePath.empty() || Image::isImageExtension( oldFilePath ) ) && + ( newFilePath.empty() || Image::isImageExtension( newFilePath ) ) && + loadImageDiffFromPaths( oldFilePath, newFilePath ) ) { + return; + } + + resetToTextDiffView(); std::string oldText, newText; FileSystem::fileGet( oldFilePath, oldText ); FileSystem::fileGet( newFilePath, newText ); @@ -864,6 +1283,7 @@ void UIDiffView::updateButtonsText() { mModeToggle->setText( i18n( "diffview_side_by_side", "Side by Side" ) ); mModeToggle->setSelected( mViewMode != ViewMode::Unified ); mCompleteViewToggle->setText( i18n( "diffview_compact", "Compact" ) ); + mCompleteViewToggle->setVisible( !mIsImageDiff ); mCompleteViewToggle->setSelected( !mShowCompleteView ); } diff --git a/src/eepp/ui/tools/uiimageviewer.cpp b/src/eepp/ui/tools/uiimageviewer.cpp index 8c7b5f09d..866c82076 100644 --- a/src/eepp/ui/tools/uiimageviewer.cpp +++ b/src/eepp/ui/tools/uiimageviewer.cpp @@ -341,7 +341,7 @@ Uint32 UIImageViewer::onKeyDown( const KeyEvent& event ) { } else if ( event.getKeyCode() == KEY_R ) { mImage->setRotation( mImage->getRotation() + 90.0f ); } else if ( event.getKeyCode() == KEY_T ) { - resetImageView(); + resetImageView(); } return UIWidget::onKeyDown( event ); } @@ -458,4 +458,8 @@ std::vector UIImageViewer::getPropertiesImplemented() const { return props; } +bool UIImageViewer::hasImage() const { + return mLoading || ( mImage && mImage->getDrawable() != nullptr ); +} + } // namespace EE::UI::Tools diff --git a/src/tests/unit_tests/uidiffview_tests.cpp b/src/tests/unit_tests/uidiffview_tests.cpp index 5c365ad9d..06da314cd 100644 --- a/src/tests/unit_tests/uidiffview_tests.cpp +++ b/src/tests/unit_tests/uidiffview_tests.cpp @@ -1,7 +1,9 @@ #include "utest.h" +#include #include #include #include +#include #include #include @@ -100,3 +102,60 @@ UTEST( UIDiffView, LoadFromPatchAndVerifyCleanText ) { eeDelete( diffView ); } + +UTEST( UIDiffView, LoadFromFileImageDiffUsesImageViewers ) { + UIApplication app( WindowSettings{ 800, 600, "eepp - unit tests" } ); + + std::string oldImagePath( "/tmp/eepp-uidiffview-old.png" ); + std::string newImagePath( "/tmp/eepp-uidiffview-new.png" ); + Graphics::Image oldImage( 8, 8, 4, Color::Red ); + Graphics::Image newImage( 8, 8, 4, Color::Blue ); + ASSERT_TRUE( oldImage.saveToFile( oldImagePath, Graphics::Image::SaveType::PNG ) ); + ASSERT_TRUE( newImage.saveToFile( newImagePath, Graphics::Image::SaveType::PNG ) ); + + UIDiffView* diffView = UIDiffView::New(); + diffView->setPixelsSize( 400, 200 ); + diffView->loadFromFile( oldImagePath, newImagePath ); + + EXPECT_TRUE( diffView->isImageDiff() ); + EXPECT_TRUE( diffView->getLeftImageViewer()->isVisible() ); + EXPECT_TRUE( diffView->getRightImageViewer()->isVisible() ); + EXPECT_FALSE( diffView->getEditor()->isVisible() ); + EXPECT_FALSE( diffView->getLeftEditor()->isVisible() ); + EXPECT_FALSE( diffView->getRightEditor()->isVisible() ); + EXPECT_TRUE( diffView->getFileName().toUtf8() == "eepp-uidiffview-new.png" ); + + eeDelete( diffView ); + FileSystem::fileRemove( oldImagePath ); + FileSystem::fileRemove( newImagePath ); +} + +UTEST( UIDiffView, LoadFromPatchImageDiffUsesProvidedOldFile ) { + UIApplication app( WindowSettings{ 800, 600, "eepp - unit tests" } ); + + std::string oldImagePath( "/tmp/eepp-uidiffview-patch-old.png" ); + std::string newImagePath( "/tmp/eepp-uidiffview-patch-new.png" ); + Graphics::Image oldImage( 8, 8, 4, Color::Red ); + Graphics::Image newImage( 8, 8, 4, Color::Blue ); + ASSERT_TRUE( oldImage.saveToFile( oldImagePath, Graphics::Image::SaveType::PNG ) ); + ASSERT_TRUE( newImage.saveToFile( newImagePath, Graphics::Image::SaveType::PNG ) ); + + std::string patchText = R"patch(diff --git a/assets/logo.png b/assets/logo.png +index 1234567..89abcde 100644 +Binary files a/assets/logo.png and b/assets/logo.png differ +)patch"; + + UIDiffView* diffView = UIDiffView::New(); + diffView->setPixelsSize( 400, 200 ); + diffView->loadFromPatch( patchText, newImagePath, oldImagePath ); + + EXPECT_TRUE( diffView->isImageDiff() ); + EXPECT_EQ( UIDiffView::ViewMode::SideBySide, diffView->getViewMode() ); + EXPECT_TRUE( diffView->getLeftImageViewer()->isVisible() ); + EXPECT_TRUE( diffView->getRightImageViewer()->isVisible() ); + EXPECT_TRUE( diffView->getFileName().toUtf8() == "logo.png" ); + + eeDelete( diffView ); + FileSystem::fileRemove( oldImagePath ); + FileSystem::fileRemove( newImagePath ); +} diff --git a/src/tests/unit_tests/uihtml_tests.cpp b/src/tests/unit_tests/uihtml_tests.cpp index 4f1c5f7ca..51f94df75 100644 --- a/src/tests/unit_tests/uihtml_tests.cpp +++ b/src/tests/unit_tests/uihtml_tests.cpp @@ -50,7 +50,7 @@ static void init_ui_test() { UTEST( UIHTMLTable, complexLayout ) { auto win = Engine::instance()->createWindow( - WindowSettings( 1024, 653, "HTML Tables Test", WindowStyle::Default, WindowBackend::Default, + WindowSettings( 1024, 656, "HTML Tables Test", WindowStyle::Default, WindowBackend::Default, 32, {}, 1, false, true ), ContextSettings( false, 0, 0, GLv_default, true, false ) ); FileSystem::changeWorkingDirectory( Sys::getProcessPath() ); @@ -1578,7 +1578,7 @@ UTEST( UIHTMLDetails, lobstersInlineBlockCachesWidth ) { UTEST( UIBorder, renderingVariations ) { auto win = Engine::instance()->createWindow( - WindowSettings( 1024, 653, "Border Rendering Test", WindowStyle::Default, + WindowSettings( 1024, 656, "Border Rendering Test", WindowStyle::Default, WindowBackend::Default, 32, {}, 1, false, true ), ContextSettings( false, 0, 0, GLv_default, true, false ) ); FileSystem::changeWorkingDirectory( Sys::getProcessPath() ); @@ -2093,7 +2093,7 @@ UTEST( UIHTML, ContactFormLayout ) { UTEST( UIBackground, imageAtlasPositioning ) { auto win = Engine::instance()->createWindow( - WindowSettings( 1024, 653, "Background Atlas Test", WindowStyle::Default, + WindowSettings( 1024, 656, "Background Atlas Test", WindowStyle::Default, WindowBackend::Default, 32, {}, 1, false, true ), ContextSettings( false, 0, 0, GLv_default, true, false ) ); FileSystem::changeWorkingDirectory( Sys::getProcessPath() ); diff --git a/src/tools/ecode/ecode.cpp b/src/tools/ecode/ecode.cpp index add723a08..d3b490245 100644 --- a/src/tools/ecode/ecode.cpp +++ b/src/tools/ecode/ecode.cpp @@ -2637,14 +2637,15 @@ void App::loadAudioFromPath( const std::string& path, bool autoPlay ) { audioPlayer->loadFromPath( path, autoPlay ); } -void App::loadDiffFromMemory( const std::string& content, const std::string& originalFilePath ) { +void App::loadDiffFromMemory( const std::string& content, const std::string& originalFilePath, + const std::string& oldFilePath, const std::string& repoPath ) { if ( UIDiffView::isMultiFileDiff( content ) ) { auto diffViewTitle = i18n( "diff_viewer", "Diff Viewer" ) + ": " + originalFilePath; UIIcon* icon = getUISceneNode()->findIcon( "filetype-diff" ); if ( !icon ) icon = getUISceneNode()->findIcon( "file" ); - auto scrollView = UIDiffView::NewMultiFileDiffViewer( content ); + auto scrollView = UIDiffView::NewMultiFileDiffViewer( content, repoPath ); auto [tab, iv] = getSplitter()->createWidget( scrollView, diffViewTitle ); if ( icon ) tab->setIcon( icon->getSize( getMenuIconSize() ) ); @@ -2662,7 +2663,10 @@ void App::loadDiffFromMemory( const std::string& content, const std::string& ori auto diffViewTitle = i18n( "diff_viewer", "Diff Viewer" ); auto* diffView = Tools::UIDiffView::New(); + diffView->setAutoDeleteOldTempImage( true ); auto [tab, iv] = getSplitter()->createWidget( diffView, diffViewTitle ); + if ( !tab ) + return; if ( !originalFilePath.empty() ) { std::string fileName = FileSystem::fileNameFromPath( originalFilePath ); tab->setText( diffViewTitle + ": " + fileName ); @@ -2676,7 +2680,7 @@ void App::loadDiffFromMemory( const std::string& content, const std::string& ori if ( icon ) tab->setIcon( icon->getSize( getMenuIconSize() ) ); diffView->setHeadersVisible( true ); - diffView->loadFromPatch( content, originalFilePath ); + diffView->loadFromPatch( content, originalFilePath, oldFilePath ); diffView->setSyntaxColorScheme( *getCurrentColorScheme() ); registerUnlockedCommands( *diffView ); } diff --git a/src/tools/ecode/ecode.hpp b/src/tools/ecode/ecode.hpp index 091ef28d6..aaad561f1 100644 --- a/src/tools/ecode/ecode.hpp +++ b/src/tools/ecode/ecode.hpp @@ -608,7 +608,9 @@ class App : public UICodeEditorSplitter::Client, public PluginContextProvider { void loadDiffFromPaths( const std::string& oldPath, const std::string& newPath ); - void loadDiffFromMemory( const std::string& content, const std::string& originalFilePath = "" ); + void loadDiffFromMemory( const std::string& content, const std::string& originalFilePath = "", + const std::string& oldFilePath = "", + const std::string& repoPath = "" ); void loadDiffFromStrings( const std::string& str, const std::string& otherStr ); diff --git a/src/tools/ecode/plugins/git/git.cpp b/src/tools/ecode/plugins/git/git.cpp index 2b3250412..f75176e4a 100644 --- a/src/tools/ecode/plugins/git/git.cpp +++ b/src/tools/ecode/plugins/git/git.cpp @@ -249,6 +249,17 @@ Git::Result Git::diff( const std::string& file, bool isStaged, const std::string projectDir ); } +Git::Result Git::showFile( const std::string& file, const std::string& ref, + const std::string& projectDir ) { + std::string relativePath( file ); + const std::string& repoPath = projectDir.empty() ? mProjectPath : projectDir; + if ( !repoPath.empty() ) + FileSystem::filePathRemoveBasePath( repoPath, relativePath ); + + std::string refSpec( ref == ":" ? ":" + relativePath : ref + ":" + relativePath ); + return gitSimple( String::format( "show \"%s\"", refSpec ), projectDir ); +} + Git::Result Git::createBranch( const std::string& branchName, bool _checkout, const std::string& projectDir ) { auto res = gitSimple( String::format( "branch --no-track %s", branchName ), projectDir ); diff --git a/src/tools/ecode/plugins/git/git.hpp b/src/tools/ecode/plugins/git/git.hpp index 0e85c1678..875a7e0fd 100644 --- a/src/tools/ecode/plugins/git/git.hpp +++ b/src/tools/ecode/plugins/git/git.hpp @@ -269,6 +269,9 @@ class Git { Result diff( const std::string& file, bool isStaged, const std::string& projectDir = "" ); + Result showFile( const std::string& file, const std::string& ref, + const std::string& projectDir = "" ); + Result createBranch( const std::string& branchName, bool checkout = false, const std::string& projectDir = "" ); diff --git a/src/tools/ecode/plugins/git/gitplugin.cpp b/src/tools/ecode/plugins/git/gitplugin.cpp index ddc6c117a..1401dcac0 100644 --- a/src/tools/ecode/plugins/git/gitplugin.cpp +++ b/src/tools/ecode/plugins/git/gitplugin.cpp @@ -1,11 +1,13 @@ #include "gitplugin.hpp" #include "gitbranchmodel.hpp" #include "gitstatusmodel.hpp" +#include #include #include #include #include #include +#include #include #include #include @@ -34,6 +36,19 @@ namespace ecode { static constexpr auto DEFAULT_HIGHLIGHT_COLOR = "var(--font-highlight)"sv; static constexpr auto GIT_STATUS_UPDATE_TAG = String::hash( "git::status-update" ); +static std::string writeGitBlobTempFile( const std::string& contents, + const std::string& sourceFilePath ) { + if ( contents.empty() ) + return ""; + + std::string ext( FileSystem::fileExtension( sourceFilePath ) ); + std::string path( Sys::getTempPath() + ".ecode-git-diff-" + String::randString( 16 ) ); + if ( !ext.empty() ) + path += "." + ext; + + return FileSystem::fileWrite( path, contents ) ? path : ""; +} + std::string GitPlugin::statusTypeToString( Git::GitStatusType type ) { switch ( type ) { case Git::GitStatusType::Untracked: @@ -1087,7 +1102,7 @@ void GitPlugin::diff( const Git::DiffMode mode, const std::string& repoPath ) { return; std::string repoName = this->repoName( repoPath ); - getUISceneNode()->runOnMainThread( [this, mode, res, repoName] { + getUISceneNode()->runOnMainThread( [this, mode, res, repoName, repoPath] { std::string modeName; switch ( mode ) { case Git::DiffHead: { @@ -1099,7 +1114,8 @@ void GitPlugin::diff( const Git::DiffMode mode, const std::string& repoPath ) { break; } getPluginContext()->loadDiffFromMemory( - res.result, UIDiffView::isMultiFileDiff( res.result ) ? modeName : "" ); + res.result, UIDiffView::isMultiFileDiff( res.result ) ? modeName : "", "", + repoPath ); } ); } ); } @@ -1119,9 +1135,27 @@ void GitPlugin::diff( const std::string& file, Git::GitStatusType status ) { return; auto result = std::move( res.result ); + std::string oldImagePath; + std::string newImagePath( filePath ); + if ( EE::Graphics::Image::isImageExtension( filePath ) ) { + std::string repoPath( mGit->repoPath( file ) ); + auto oldBlob = mGit->showFile( + filePath, status == Git::GitStatusType::Staged ? "HEAD" : ":", repoPath ); + if ( oldBlob.success() ) + oldImagePath = writeGitBlobTempFile( oldBlob.result, filePath ); + + if ( status == Git::GitStatusType::Staged ) { + auto newBlob = mGit->showFile( filePath, ":", repoPath ); + if ( newBlob.success() ) + newImagePath = writeGitBlobTempFile( newBlob.result, filePath ); + } + } + getUISceneNode()->runOnMainThread( - [this, result = std::move( result ), filePath = std::move( filePath )] { - getPluginContext()->loadDiffFromMemory( result, filePath ); + [this, result = std::move( result ), filePath = std::move( filePath ), + oldImagePath = std::move( oldImagePath ), newImagePath = std::move( newImagePath )] { + getPluginContext()->loadDiffFromMemory( + result, newImagePath.empty() ? filePath : newImagePath, oldImagePath ); } ); } ); } diff --git a/src/tools/ecode/plugins/plugincontextprovider.hpp b/src/tools/ecode/plugins/plugincontextprovider.hpp index aa2809a3d..0e4c116b0 100644 --- a/src/tools/ecode/plugins/plugincontextprovider.hpp +++ b/src/tools/ecode/plugins/plugincontextprovider.hpp @@ -158,7 +158,9 @@ class PluginContextProvider { virtual void loadDiffFromPath( const std::string& path ) = 0; virtual void loadDiffFromMemory( const std::string& content, - const std::string& originalFilePath = "" ) = 0; + const std::string& originalFilePath = "", + const std::string& oldFilePath = "", + const std::string& repoPath = "" ) = 0; virtual void loadFolder( std::string path, bool forceNewWindow = false ) = 0;