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 82ae7a50c..992b3c4e7 100644 Binary files a/bin/unit_tests/assets/html/eepp-ui-background-atlas.webp and b/bin/unit_tests/assets/html/eepp-ui-background-atlas.webp differ 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 1772856a9..dcbdb989b 100644 Binary files a/bin/unit_tests/assets/html/eepp-ui-border-rendering.webp and b/bin/unit_tests/assets/html/eepp-ui-border-rendering.webp differ diff --git a/bin/unit_tests/assets/html/eepp-uihtmltable-complex-layout.webp b/bin/unit_tests/assets/html/eepp-uihtmltable-complex-layout.webp index 3ca34f91c..c0027e6cd 100644 Binary files a/bin/unit_tests/assets/html/eepp-uihtmltable-complex-layout.webp and b/bin/unit_tests/assets/html/eepp-uihtmltable-complex-layout.webp differ diff --git a/include/eepp/ui/tools/uidiffview.hpp b/include/eepp/ui/tools/uidiffview.hpp index 4231763a6..1dd7e4ba5 100644 --- a/include/eepp/ui/tools/uidiffview.hpp +++ b/include/eepp/ui/tools/uidiffview.hpp @@ -6,12 +6,19 @@ #include #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;