From 9d2aec8e149ecdbe2a8374322ec120fac8b428cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Lucas=20Golini?= Date: Sun, 28 Sep 2025 00:39:17 -0300 Subject: [PATCH] eepp: Added UIImageViewer new tool widget to view images. Added a new UIMessageBox event: `Event::OnDiscard`, this will triggered when window is closed without confirmation (`Event::OnConfirm`). And now `Event::OnCancel` is only triggered when is manually cancelled (click the cancel button). ecode: Greatly improved image viewing experience. Now you can switch images from the same directory with the mouse wheel, also zoom, translate and rotate the images, images open in a new tab by default but quick-preview can be activated in `Settings -> Window -> Quick Preview Images` (SpartanJ/ecode#96). --- bin/assets/ui/breeze.css | 5 + docs/articles/cssspecification.md | 18 + include/eepp/graphics/image.hpp | 2 + include/eepp/scene/event.hpp | 1 + include/eepp/ui/css/propertydefinition.hpp | 1 + include/eepp/ui/tools/uiimageviewer.hpp | 113 +++++ include/eepp/ui/uihelper.hpp | 1 + include/eepp/ui/uitextview.hpp | 2 + src/eepp/graphics/image.cpp | 38 ++ src/eepp/ui/css/stylesheetspecification.cpp | 2 + src/eepp/ui/doc/languages/css.cpp | 1 + src/eepp/ui/tools/uiimageviewer.cpp | 428 ++++++++++++++++++ src/eepp/ui/uimessagebox.cpp | 5 +- src/eepp/ui/uitextview.cpp | 13 + src/eepp/ui/uiwidgetcreator.cpp | 2 + src/tools/ecode/appconfig.cpp | 14 + src/tools/ecode/appconfig.hpp | 1 + src/tools/ecode/applayout.xml.hpp | 18 +- src/tools/ecode/ecode.cpp | 105 +++-- src/tools/ecode/ecode.hpp | 2 +- src/tools/ecode/plugins/git/gitplugin.cpp | 4 +- .../ecode/plugins/plugincontextprovider.hpp | 6 + src/tools/ecode/settingsactions.cpp | 2 +- src/tools/ecode/settingsmenu.cpp | 17 +- 24 files changed, 736 insertions(+), 65 deletions(-) create mode 100644 include/eepp/ui/tools/uiimageviewer.hpp create mode 100644 src/eepp/ui/tools/uiimageviewer.cpp diff --git a/bin/assets/ui/breeze.css b/bin/assets/ui/breeze.css index 1db9ff9f9..302a599da 100644 --- a/bin/assets/ui/breeze.css +++ b/bin/assets/ui/breeze.css @@ -1092,6 +1092,11 @@ TextInput.table_cell_edit { padding-bottom: 0!important; } +ImageViewer > TextView { + text-stroke-width: 1dp; + text-stroke-color: var(--back); +} + @media (prefers-color-scheme: light) { :root { diff --git a/docs/articles/cssspecification.md b/docs/articles/cssspecification.md index 9e938a826..cfd51e4a8 100644 --- a/docs/articles/cssspecification.md +++ b/docs/articles/cssspecification.md @@ -557,6 +557,24 @@ Multiple flags are separated by `|`. --- +### display-options + +Allows to set what information should be displayed of the current image. +Multiple flags can be set, flags are separated by `|`. + +* Applicable to: Any child of a EE::UI::UIImageViewer (ImageViewer) +* Data Type: [string-list](#string-list-data-type) +* Value List: + * `name`: Displays the file name + * `dimensions`: Displays the image dimensions + * `path`: Displays the file path + * `gallery_position`: Displays the gallery position of the current image (ex: `4 / 15`) + * `size`: Displays the file size + * `type`: Displays the image type +* Default value: _No value_ + +--- + ### display-percent Enables/disables displaying the percentage of progress in the progress bar. diff --git a/include/eepp/graphics/image.hpp b/include/eepp/graphics/image.hpp index f32ed63ac..2abefe862 100644 --- a/include/eepp/graphics/image.hpp +++ b/include/eepp/graphics/image.hpp @@ -43,6 +43,8 @@ class EE_API Image { WEBP = 15, }; + static std::string formatToString( Format format ); + /** @enum PixelFormat Format Pixel formats to write into a texture image. */ enum PixelFormat { PIXEL_FORMAT_RED, diff --git a/include/eepp/scene/event.hpp b/include/eepp/scene/event.hpp index 0213e9451..f907057c9 100644 --- a/include/eepp/scene/event.hpp +++ b/include/eepp/scene/event.hpp @@ -121,6 +121,7 @@ class EE_API Event { OnBeforeFoldUnfoldRange, OnFoldUnfoldRange, OnResourceLoaded, + OnDiscard, NoEvent = eeINDEX_NOT_FOUND }; diff --git a/include/eepp/ui/css/propertydefinition.hpp b/include/eepp/ui/css/propertydefinition.hpp index 4ce3b741e..14780c951 100644 --- a/include/eepp/ui/css/propertydefinition.hpp +++ b/include/eepp/ui/css/propertydefinition.hpp @@ -224,6 +224,7 @@ enum class PropertyId : Uint32 { DisableCodeEditorFlags = String::hash( "disable-editor-flags" ), LineWrapMode = String::hash( "line-wrap-mode" ), LineWrapType = String::hash( "line-wrap-type" ), + DisplayOptions = String::hash( "display-options" ), }; enum class PropertyType : Uint32 { diff --git a/include/eepp/ui/tools/uiimageviewer.hpp b/include/eepp/ui/tools/uiimageviewer.hpp new file mode 100644 index 000000000..ee1cfdc1f --- /dev/null +++ b/include/eepp/ui/tools/uiimageviewer.hpp @@ -0,0 +1,113 @@ +#ifndef EE_UI_TOOLS_UIIMAGEVIEWER_HPP +#define EE_UI_TOOLS_UIIMAGEVIEWER_HPP + +#include + +#include + +namespace EE::Graphics { +class Texture; +} + +namespace EE::UI { +class UIImage; +class UILoader; +class UITextView; +} // namespace EE::UI + +namespace EE::UI::Tools { + +class EE_API UIImageViewer : public UIWidget { + public: + enum DisplayOptions { + DisplayName = 1 << 0, + DisplayPath = 1 << 1, + DisplayDimensions = 1 << 2, + DisplayGalleryPosition = 1 << 3, + DisplaySize = 1 << 4, + DisplayType = 1 << 5, + }; + + static Uint32 displayOptionsFromString( std::string_view ); + + static std::string displayOptionsToString( Uint32 opt ); + + static UIImageViewer* New(); + + UIImageViewer(); + + virtual ~UIImageViewer(); + + virtual Uint32 getType() const override; + + virtual bool isType( const Uint32& type ) const override; + + void loadImageAsync( std::string_view pathOrContents, bool isContents = false, + bool loadGallery = true ); + + UILoader* getLoader() const { return mLoader; } + + UIImage* getImage() const { return mImage; } + + void reset(); + + Float getMinScale() { return mMinScale; } + + Float getMaxScale() { return mMaxScale; } + + void setMinScale( Float scale ) { mMinScale = scale; } + + void setMaxScale( Float scale ) { mMaxScale = scale; } + + const std::string& getGalleryPath() const; + + const std::string& getImageName() const; + + std::string getImagePath() const; + + void setDisplayOptions( Uint32 opt ); + + Uint32 getDisplayOptions() const; + + virtual bool applyProperty( const StyleSheetProperty& attribute ) override; + + virtual std::string getPropertyString( const PropertyDefinition* propertyDef, + const Uint32& propertyIndex = 0 ) const override; + + virtual std::vector getPropertiesImplemented() const override; + + protected: + UILoader* mLoader{ nullptr }; + UIImage* mImage{ nullptr }; + UITextView* mTextView{ nullptr }; + Vector2f mMouseMiddleStartClick; + Float mMinScale{ 0.25f }; + Float mMaxScale{ 5.f }; + std::string mGalleryPath; + std::vector mGalleryFiles; + Int64 mGalleryImageIndex{ 0 }; + std::atomic mLoading{ 0 }; + bool mMouseMiddlePressing{ false }; + std::atomic mGalleryLoaderShouldAbort{ false }; + std::atomic mClosing{ false }; + bool mHasGallery{ false }; + Float mInitialScale; + Mutex mGalleryMutex; + Int64 mCurFileSize{ 0 }; + Image::Format mCurFileType; + Uint32 mDisplayOptions{ 0 }; + + virtual void onSizeChange() override; + + virtual Uint32 onMessage( const NodeMessage* ) override; + + virtual Uint32 onKeyDown( const KeyEvent& event ) override; + + void updateTextDisplay(); + + void resetImageView(); +}; + +} // namespace EE::UI::Tools + +#endif \ No newline at end of file diff --git a/include/eepp/ui/uihelper.hpp b/include/eepp/ui/uihelper.hpp index 8413820a7..fd27ba3c1 100644 --- a/include/eepp/ui/uihelper.hpp +++ b/include/eepp/ui/uihelper.hpp @@ -103,6 +103,7 @@ enum UINodeType { UI_TYPE_LISTVIEW, UI_TYPE_CONSOLE, UI_TYPE_STACK_LAYOUT, + UI_TYPE_IMAGE_VIEWER, UI_TYPE_MODULES = 10000, UI_TYPE_TERMINAL = 10001, UI_TYPE_USER = 200000 diff --git a/include/eepp/ui/uitextview.hpp b/include/eepp/ui/uitextview.hpp index 7dd460253..0cc55756c 100644 --- a/include/eepp/ui/uitextview.hpp +++ b/include/eepp/ui/uitextview.hpp @@ -49,6 +49,8 @@ class EE_API UITextView : public UIWidget { virtual UITextView* setText( const String& text ); + virtual UITextView* setText( String&& text ); + const Color& getFontColor() const; UITextView* setFontColor( const Color& color ); diff --git a/src/eepp/graphics/image.cpp b/src/eepp/graphics/image.cpp index 3ffc1ed52..6bdbd4edc 100644 --- a/src/eepp/graphics/image.cpp +++ b/src/eepp/graphics/image.cpp @@ -291,6 +291,44 @@ Image* Image::New( IOStream& stream, const unsigned int& forceChannels, return eeNew( Image, ( stream, forceChannels, formatConfiguration ) ); } +std::string Image::formatToString( Format format ) { + switch ( format ) { + case Format::JPEG: + return "JPEG"; + case Format::PNG: + return "PNG"; + case Format::BMP: + return "BMP"; + case Format::GIF: + return "GIF"; + case Format::TGA: + return "TGA"; + case Format::PSD: + return "PSD"; + case Format::PIC: + return "PIC"; + case Format::PNM: + return "PNM"; + case Format::DDS: + return "DDS"; + case Format::PVR: + return "PVR"; + case Format::PKM: + return "PKM"; + case Format::HDR: + return "HDR"; + case Format::QOI: + return "QOI"; + case Format::SVG: + return "SVG"; + case Format::WEBP: + return "WEBP"; + case Format::Unknown: + break; + } + return "unknown"; +} + std::string Image::saveTypeToExtension( Image::SaveType Format ) { switch ( Format ) { case Image::SaveType::TGA: diff --git a/src/eepp/ui/css/stylesheetspecification.cpp b/src/eepp/ui/css/stylesheetspecification.cpp index 9b983d138..71490209b 100644 --- a/src/eepp/ui/css/stylesheetspecification.cpp +++ b/src/eepp/ui/css/stylesheetspecification.cpp @@ -420,6 +420,8 @@ void StyleSheetSpecification::registerDefaultProperties() { registerProperty( "line-wrap-mode", "nowrap" ).setType( PropertyType::String ); registerProperty( "line-wrap-type", "viewport" ).setType( PropertyType::String ); + registerProperty( "display-options", "" ).setType( PropertyType::String ); + // Shorthands registerShorthand( "margin", { "margin-top", "margin-right", "margin-bottom", "margin-left" }, "box" ); diff --git a/src/eepp/ui/doc/languages/css.cpp b/src/eepp/ui/doc/languages/css.cpp index b690cd4a5..399d9151e 100644 --- a/src/eepp/ui/doc/languages/css.cpp +++ b/src/eepp/ui/doc/languages/css.cpp @@ -423,6 +423,7 @@ void addCSS() { { "Tooltip", "keyword" }, { "Menu", "keyword" }, { "PopUpMenu", "keyword" }, + { "ImageViewer", "keyword" }, }, "", diff --git a/src/eepp/ui/tools/uiimageviewer.cpp b/src/eepp/ui/tools/uiimageviewer.cpp new file mode 100644 index 000000000..6be7b1918 --- /dev/null +++ b/src/eepp/ui/tools/uiimageviewer.cpp @@ -0,0 +1,428 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace EE::Graphics; + +namespace EE::UI::Tools { + +Uint32 UIImageViewer::displayOptionsFromString( std::string_view str ) { + Uint32 opt = 0; + String::readBySeparator( + str, + [&opt]( std::string_view chunk ) { + if ( "name" == chunk ) + opt |= DisplayOptions::DisplayName; + else if ( "path" == chunk ) + opt |= DisplayOptions::DisplayPath; + else if ( "dimensions" == chunk ) + opt |= DisplayOptions::DisplayDimensions; + else if ( "size" == chunk ) + opt |= DisplayOptions::DisplaySize; + else if ( "type" == chunk ) + opt |= DisplayOptions::DisplayType; + else if ( "pos" == chunk || "position" == chunk || "gallery_position" == chunk || + "gallery-position" == chunk ) + opt |= DisplayOptions::DisplayGalleryPosition; + }, + '|' ); + return opt; +} + +std::string UIImageViewer::displayOptionsToString( Uint32 opt ) { + std::string str; + if ( opt & DisplayOptions::DisplayName ) + str += "name|"; + if ( opt & DisplayOptions::DisplayPath ) + str += "path|"; + if ( opt & DisplayOptions::DisplayDimensions ) + str += "dimensions|"; + if ( opt & DisplayOptions::DisplaySize ) + str += "size|"; + if ( opt & DisplayOptions::DisplayType ) + str += "type|"; + if ( opt & DisplayOptions::DisplayGalleryPosition ) + str += "pos|"; + if ( str.size() ) + str.pop_back(); + return str; +} + +UIImageViewer* UIImageViewer::New() { + return eeNew( UIImageViewer, () ); +} + +UIImageViewer::UIImageViewer() : UIWidget( "imageviewer" ) { + mImage = UIImage::New(); + mImage->setParent( this ); + mImage->setVisible( false ); + mImage->setDragEnabled( true ); + mImage->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed ); + mImage->setScaleType( UIScaleType::FitInside ); + mLoader = UILoader::New(); + mLoader->setParent( this ); + mLoader->setIndeterminate( true ); + mLoader->setVisible( false ); + mLoader->setEnabled( false ); + mLoader->setSize( { 64, 64 } ); + mLoader->setOutlineThickness( PixelDensity::dpToPx( 6 ) ); + mTextView = UITextView::New(); + mTextView->setParent( this ); + mTextView->setEnabled( false ); + setClipType( ClipType::ContentBox ); +} + +UIImageViewer::~UIImageViewer() { + mClosing = true; + mGalleryLoaderShouldAbort = true; + while ( mLoading ) + Sys::sleep( Milliseconds( 1 ) ); +} + +Uint32 UIImageViewer::getType() const { + return UI_TYPE_IMAGE_VIEWER; +} + +bool UIImageViewer::isType( const Uint32& type ) const { + return UIImageViewer::getType() == type ? true : UIWidget::isType( type ); +} + +void UIImageViewer::loadImageAsync( std::string_view path, bool isContents, bool loadGallery ) { + if ( !getUISceneNode()->hasThreadPool() ) + return; + mLoader->setVisible( true ); + std::string ipath( path ); + + mLoading++; + + getUISceneNode()->getThreadPool()->run( [path = std::move( ipath ), this, isContents, + loadGallery] { + ScopedOp op( [] {}, [this] { mLoading--; } ); + + auto folder( FileSystem::fileRemoveFileName( path ) ); + FileSystem::dirAddSlashAtEnd( folder ); + mHasGallery = false; + if ( loadGallery && !isContents ) { + Lock l( mGalleryMutex ); + bool sameFolder = folder == mGalleryPath; + bool newFolder = false; + if ( !sameFolder && FileSystem::isDirectory( folder ) ) { + mHasGallery = true; + newFolder = true; + mGalleryPath = folder; + mGalleryFiles = + FileSystem::filesGetInPath( mGalleryPath, true, true, false, [this] { + return mGalleryLoaderShouldAbort.load(); + } ); + std::erase_if( mGalleryFiles, []( const std::string& filename ) { + return !Image::isImageExtension( filename ); + } ); + } else if ( sameFolder ) { + mHasGallery = true; + } + + if ( sameFolder || newFolder ) { + auto fileName( FileSystem::fileNameFromPath( path ) ); + auto it = std::find( mGalleryFiles.begin(), mGalleryFiles.end(), fileName ); + if ( it != mGalleryFiles.end() ) { + mGalleryImageIndex = std::distance( mGalleryFiles.begin(), it ); + } else { + mGalleryImageIndex = 0; + } + } + } else { + Lock l( mGalleryMutex ); + mGalleryPath = folder; + mGalleryFiles = { FileSystem::fileNameFromPath( path ) }; + mGalleryImageIndex = 0; + } + + Image::Format format = + isContents ? Image::getFormat( reinterpret_cast( path.c_str() ), + path.size() ) + : Image::getFormat( path ); + + if ( format == Image::Format::Unknown ) + return; + + Drawable* image = nullptr; + + if ( mClosing ) + return; + + if ( format != Image::Format::GIF ) { + image = isContents + ? TextureFactory::instance()->loadFromMemory( + reinterpret_cast( path.c_str() ), path.size() ) + : TextureFactory::instance()->loadFromFile( path ); + } else { + IOStream* stream = isContents + ? (IOStream*)new IOStreamMemory( path.c_str(), path.size() ) + : (IOStream*)new IOStreamFile( path ); + Sprite* sprite = Sprite::fromGif( *stream ); + sprite->setAutoAnimate( false ); + image = sprite; + delete stream; + } + + if ( mClosing ) + return; + + if ( image ) { + + if ( isContents ) + mCurFileSize = path.size(); + else + mCurFileSize = FileSystem::fileSize( path ); + + mCurFileType = format; + + runOnMainThread( [this, image] { + mImage->setDrawable( image, true ); + updateTextDisplay(); + auto s( image->getPixelsSize() ); + auto scale( s.x > mSize.x || s.y > mSize.y + ? eemin( mSize.x / s.getWidth(), mSize.y / s.getHeight() ) + : 1.f ); + mImage->setPixelsSize( eefloor( image->getPixelsSize().x * scale ), + eefloor( image->getPixelsSize().y * scale ) ); + resetImageView(); + sendCommonEvent( Event::OnResourceLoaded ); + } ); + } + + runOnMainThread( [this] { mLoader->setVisible( false ); } ); + } ); +} + +void UIImageViewer::onSizeChange() { + mLoader->center(); +} + +void UIImageViewer::reset() { + mImage->setDrawable( nullptr )->setVisible( false ); +} + +Uint32 UIImageViewer::onMessage( const NodeMessage* msg ) { + if ( msg->getMsg() == NodeMessage::MouseDown ) { + if ( ( msg->getFlags() & EE_BUTTON_LMASK ) && getEventDispatcher() && + getEventDispatcher()->getNodeDragging() == nullptr ) { + mImage->setFocus(); + mImage->startDragging( getEventDispatcher()->getMousePosf() ); + return 1; + } + if ( msg->getFlags() & EE_BUTTON_RMASK ) { + Line2f line( getEventDispatcher()->getMousePosf(), + mImage->getScreenPos() + mImage->getPixelsSize() * 0.5f ); + mImage->setRotation( line.getAngle() ); + return 1; + } + if ( msg->getFlags() & EE_BUTTON_MMASK ) { + if ( !mMouseMiddlePressing ) { + mMouseMiddlePressing = true; + mInitialScale = mImage->getScale().x; + mMouseMiddleStartClick = getEventDispatcher()->getMousePosf(); + Vector2f localPos( mImage->convertToNodeSpace( mMouseMiddleStartClick ) ); + auto size( mImage->getPixelsSize() ); + localPos.x = eeclamp( localPos.x, 0.f, size.x ); + localPos.y = eeclamp( localPos.y, 0.f, size.y ); + mImage->setScaleOriginPointPixels( { localPos.x, localPos.y } ); + } + } + } else if ( msg->getMsg() == NodeMessage::MouseMove ) { + if ( msg->getFlags() & EE_BUTTON_MMASK ) { + mMouseMiddlePressing = true; + Vector2f v1( mMouseMiddleStartClick ); + Vector2f v2( getEventDispatcher()->getMousePosf() ); + Line2f l1( v1, v2 ); + Float dist = eeabs( v1.y - v2.y ) * 0.01f; + Float ang = l1.getAngle(); + if ( dist ) { + if ( ang >= 0.0f && ang <= 180.0f ) { + Float scale = eemax( mInitialScale - dist, mMinScale ); + mImage->setScale( scale ); + } else { + Float scale = eemin( mInitialScale + dist, mMaxScale ); + mImage->setScale( scale ); + } + } + } + } else if ( msg->getMsg() == NodeMessage::MouseUp ) { + if ( msg->getFlags() & EE_BUTTON_MMASK ) { + mMouseMiddlePressing = false; + } + + if ( getInput()->isKeyModPressed() ) { + Vector2f localPos( mImage->convertToNodeSpace( getEventDispatcher()->getMousePosf() ) ); + auto size( mImage->getPixelsSize() ); + localPos.x = eeclamp( localPos.x, 0.f, size.x ); + localPos.y = eeclamp( localPos.y, 0.f, size.y ); + OriginPoint origin( { localPos.x, localPos.y } ); + + if ( msg->getFlags() & EE_BUTTON_WUMASK ) { + mImage->setScale( eemin( mMaxScale, mImage->getScale().x + mMinScale ), origin ); + } else if ( msg->getFlags() & EE_BUTTON_WDMASK ) { + mImage->setScale( eemax( mMinScale, mImage->getScale().x - mMinScale ), origin ); + } + } else { + if ( msg->getFlags() & EE_BUTTON_WUMASK ) { + if ( mGalleryImageIndex - 1 >= 0 ) { + loadImageAsync( mGalleryPath + mGalleryFiles[--mGalleryImageIndex] ); + } + } else if ( msg->getFlags() & EE_BUTTON_WDMASK ) { + if ( mGalleryImageIndex + 1 < (Int64)mGalleryFiles.size() ) { + loadImageAsync( mGalleryPath + mGalleryFiles[++mGalleryImageIndex] ); + } + } + } + } + return 0; +} + +void UIImageViewer::resetImageView() { + mImage->setScale( 1 ); + mImage->setPixelsPosition( Vector2f::Zero ); + mImage->setRotation( 0 ); + mImage->setVisible( true ); + mImage->center(); +} + +Uint32 UIImageViewer::onKeyDown( const KeyEvent& event ) { + if ( event.getKeyCode() == KEY_KP_MINUS ) { + mImage->setScale( + eemax( mMinScale, mImage->getScale().x - + (Float)getUISceneNode()->getElapsed().asSeconds() * 8.f ) ); + return 1; + } else if ( event.getKeyCode() == KEY_KP_PLUS ) { + mImage->setScale( + eemin( mMaxScale, mImage->getScale().x + + (Float)getUISceneNode()->getElapsed().asSeconds() * 8.f ) ); + return 1; + } else if ( event.getKeyCode() == KEY_T ) { + resetImageView(); + } + return UIWidget::onKeyDown( event ); +} + +const std::string& UIImageViewer::getGalleryPath() const { + return mGalleryPath; +} + +const std::string& UIImageViewer::getImageName() const { + static std::string EMPTY = ""; + return mGalleryImageIndex >= 0 && mGalleryImageIndex < (Int64)mGalleryFiles.size() + ? mGalleryFiles[mGalleryImageIndex] + : EMPTY; +} + +std::string UIImageViewer::getImagePath() const { + return getImageName().empty() ? getImageName() : ( mGalleryPath + getImageName() ); +} + +void UIImageViewer::setDisplayOptions( Uint32 opt ) { + if ( opt != mDisplayOptions ) { + mDisplayOptions = opt; + updateTextDisplay(); + } +} + +Uint32 UIImageViewer::getDisplayOptions() const { + return mDisplayOptions; +} + +void UIImageViewer::updateTextDisplay() { + bool hasValidPath = mHasGallery && !mGalleryPath.empty() && mGalleryImageIndex >= 0 && + mGalleryImageIndex < (Int64)mGalleryFiles.size(); + + String str; + + if ( hasValidPath && ( mDisplayOptions & DisplayOptions::DisplayGalleryPosition ) ) { + str += String::format( "%d / %d", mGalleryImageIndex + 1, mGalleryFiles.size() ); + str += "\n"; + } + + if ( hasValidPath && ( mDisplayOptions & DisplayOptions::DisplayName ) ) { + str += i18n( "filename_colon", "File name:" ); + str += " "; + str += mGalleryFiles[mGalleryImageIndex]; + str += "\n"; + } + + if ( hasValidPath && ( mDisplayOptions & DisplayOptions::DisplayPath ) ) { + str += i18n( "filepath_colon", "File path:" ); + str += " "; + str += mGalleryPath + mGalleryFiles[mGalleryImageIndex]; + str += "\n"; + } + + if ( ( mDisplayOptions & DisplayOptions::DisplayDimensions ) && mImage->getDrawable() ) { + str += i18n( "dimensions_colon", "Dimensions:" ); + str += " "; + str += String::format( "%d x %d", (int)mImage->getDrawable()->getPixelsSize().x, + (int)mImage->getDrawable()->getPixelsSize().y ); + str += "\n"; + } + + if ( hasValidPath && ( mDisplayOptions & DisplayOptions::DisplaySize ) ) { + str += i18n( "filesize_colon", "File size:" ); + str += " "; + str += FileSystem::sizeToString( mCurFileSize ); + str += "\n"; + } + + if ( hasValidPath && ( mDisplayOptions & DisplayOptions::DisplayType ) ) { + str += i18n( "filetype_colon", "File type:" ); + str += " "; + str += Image::formatToString( mCurFileType ); + str += "\n"; + } + + mTextView->setText( str ); +} + +bool UIImageViewer::applyProperty( const StyleSheetProperty& attribute ) { + if ( !checkPropertyDefinition( attribute ) ) + return false; + + switch ( attribute.getPropertyDefinition()->getPropertyId() ) { + case PropertyId::DisplayOptions: { + setDisplayOptions( displayOptionsFromString( attribute.asString() ) ); + break; + } + default: + return UIWidget::applyProperty( attribute ); + } + + return true; +} + +std::string UIImageViewer::getPropertyString( const PropertyDefinition* propertyDef, + const Uint32& propertyIndex ) const { + if ( NULL == propertyDef ) + return ""; + + switch ( propertyDef->getPropertyId() ) { + case PropertyId::DisplayOptions: + return displayOptionsToString( mDisplayOptions ); + default: + return UIWidget::getPropertyString( propertyDef, propertyIndex ); + } +} + +std::vector UIImageViewer::getPropertiesImplemented() const { + auto props = UIWidget::getPropertiesImplemented(); + auto local = { PropertyId::DisplayOptions }; + props.insert( props.end(), local.begin(), local.end() ); + return props; +} + +} // namespace EE::UI::Tools \ No newline at end of file diff --git a/src/eepp/ui/uimessagebox.cpp b/src/eepp/ui/uimessagebox.cpp index 80d88e173..2898c5dcd 100644 --- a/src/eepp/ui/uimessagebox.cpp +++ b/src/eepp/ui/uimessagebox.cpp @@ -92,7 +92,7 @@ UIMessageBox::UIMessageBox( const Type& type, const String& message, const Uint3 } ); } - on( Event::OnWindowCloseClick, [this]( const Event* ) { sendCommonEvent( Event::OnCancel ); } ); + on( Event::OnWindowCloseClick, [this]( const Event* ) { sendCommonEvent( Event::OnDiscard ); } ); UILinearLayout* hlay = UILinearLayout::NewHorizontal(); hlay->setLayoutMargin( Rectf( 0, 8, 0, 0 ) ) @@ -178,6 +178,7 @@ Uint32 UIMessageBox::onMessage( const NodeMessage* Msg ) { sendCommonEvent( Event::OnConfirm ); closeWindow(); } else if ( Msg->getSender() == mButtonCancel ) { + sendCommonEvent( Event::OnDiscard ); sendCommonEvent( Event::OnCancel ); closeWindow(); } @@ -205,7 +206,7 @@ UIPushButton* UIMessageBox::getButtonCancel() const { Uint32 UIMessageBox::onKeyUp( const KeyEvent& event ) { if ( mCloseShortcut && event.getKeyCode() == mCloseShortcut && ( mCloseShortcut.mod == 0 || ( event.getMod() & mCloseShortcut.mod ) ) ) { - sendCommonEvent( Event::OnCancel ); + sendCommonEvent( Event::OnDiscard ); closeWindow(); } diff --git a/src/eepp/ui/uitextview.cpp b/src/eepp/ui/uitextview.cpp index 15a993368..ad508cd86 100644 --- a/src/eepp/ui/uitextview.cpp +++ b/src/eepp/ui/uitextview.cpp @@ -204,6 +204,19 @@ UITextView* UITextView::setText( const String& text ) { return this; } +UITextView* UITextView::setText( String&& text ) { + if ( mString != text ) { + mString = std::move( text ); + mTextCache->setString( mString ); + + recalculate(); + onTextChanged(); + notifyLayoutAttrChange(); + } + + return this; +} + const Color& UITextView::getFontColor() const { return mFontStyleConfig.FontColor; } diff --git a/src/eepp/ui/uiwidgetcreator.cpp b/src/eepp/ui/uiwidgetcreator.cpp index e8f9b13cd..cf0f2d991 100644 --- a/src/eepp/ui/uiwidgetcreator.cpp +++ b/src/eepp/ui/uiwidgetcreator.cpp @@ -1,3 +1,4 @@ +#include #include #include #include @@ -102,6 +103,7 @@ void UIWidgetCreator::createBaseWidgetList() { registeredWidget["menuseparator"] = UIMenuSeparator::New; registeredWidget["anchor"] = UIAnchor::New; registeredWidget["textureviewer"] = Tools::UITextureViewer::New; + registeredWidget["imageviewer"] = Tools::UIImageViewer::New; registeredWidget["hbox"] = UILinearLayout::NewHorizontal; registeredWidget["vbox"] = UILinearLayout::NewVertical; diff --git a/src/tools/ecode/appconfig.cpp b/src/tools/ecode/appconfig.cpp index cb3d588a3..fcaebf38e 100644 --- a/src/tools/ecode/appconfig.cpp +++ b/src/tools/ecode/appconfig.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include @@ -125,6 +126,7 @@ void AppConfig::load( const std::string& confPath, std::string& keybindingsPath, ui.openFilesInNewWindow = ini.getValueB( "ui", "open_files_in_new_window", false ); ui.openProjectInNewWindow = ini.getValueB( "ui", "open_project_in_new_window", false ); ui.nativeFileDialogs = ini.getValueB( "ui", "native_file_dialogs", false ); + ui.imagesQuickPreview = ini.getValueB( "ui", "images_quick_preview", false ); ui.panelPosition = panelPositionFromString( ini.getValue( "ui", "panel_position", "left" ) ); ui.serifFont = ini.getValue( "ui", "serif_font", "fonts/NotoSans-Regular.ttf" ); ui.monospaceFont = ini.getValue( "ui", "monospace_font", "fonts/DejaVuSansMono.ttf" ); @@ -309,6 +311,7 @@ void AppConfig::save( const std::vector& recentFiles, ini.setValueB( "ui", "open_files_in_new_window", ui.openFilesInNewWindow ); ini.setValueB( "ui", "open_project_in_new_window", ui.openProjectInNewWindow ); ini.setValueB( "ui", "native_file_dialogs", ui.nativeFileDialogs ); + ini.setValueB( "ui", "images_quick_preview", ui.imagesQuickPreview ); ini.setValue( "ui", "panel_position", panelPositionToString( ui.panelPosition ) ); ini.setValue( "ui", "serif_font", ui.serifFont ); ini.setValue( "ui", "monospace_font", ui.monospaceFont ); @@ -474,6 +477,14 @@ json AppConfig::saveNode( Node* node ) { if ( term->isUsingCustomTitle() ) f["title"] = term->getTitle(); files.emplace_back( f ); + } else if ( ownedWidget->isType( UI_TYPE_IMAGE_VIEWER ) ) { + UIImageViewer* iv = ownedWidget->asType(); + if ( iv->getImagePath().empty() ) + continue; + json f; + f["type"] = "image_viewer"; + f["path"] = iv->getImagePath(); + files.emplace_back( f ); } else if ( node->isWidget() ) { UIWidget* widget = ownedWidget->asType(); if ( widget->getClasses().size() == 1 ) { @@ -727,6 +738,9 @@ void AppConfig::loadDocuments( UICodeEditorSplitter* editorSplitter, json j, if ( curTabWidget->getTabCount() == totalToLoad ) curTabWidget->setTabSelected( eeclamp( currentPage, 0, curTabWidget->getTabCount() - 1 ) ); + } else if ( file["type"] == "image_viewer" ) { + if ( file.contains( "path" ) && file["path"].is_string() ) + app->loadImageFromMedium( file["path"].get(), false, false, true ); } else { auto found = tabWidgetTypes.find( file["type"] ); if ( found != tabWidgetTypes.end() ) { diff --git a/src/tools/ecode/appconfig.hpp b/src/tools/ecode/appconfig.hpp index 259e6b7b6..9bd754333 100644 --- a/src/tools/ecode/appconfig.hpp +++ b/src/tools/ecode/appconfig.hpp @@ -35,6 +35,7 @@ struct UIConfig { bool openFilesInNewWindow{ false }; bool openProjectInNewWindow{ false }; bool nativeFileDialogs{ false }; + bool imagesQuickPreview{ false }; PanelPosition panelPosition{ PanelPosition::Left }; std::string serifFont; std::string monospaceFont; diff --git a/src/tools/ecode/applayout.xml.hpp b/src/tools/ecode/applayout.xml.hpp index 81876b133..a24d542a3 100644 --- a/src/tools/ecode/applayout.xml.hpp +++ b/src/tools/ecode/applayout.xml.hpp @@ -515,6 +515,19 @@ textview.dragged_cell { border-radius: 4dp; padding: 4dp; } +ImageViewer { + display-options: name|pos|dimensions; +} +ImageViewer > TextView { + x: 4dp; + y: 24dp; +} +TabWidget::container > ImageViewer { + background-color: var(--list-back); +} +TabWidget::container > ImageViewer > TextView { + y: 4dp; +} @media (prefers-color-scheme: light) { @@ -559,9 +572,8 @@ R"html( - - - + + diff --git a/src/tools/ecode/ecode.cpp b/src/tools/ecode/ecode.cpp index a3019d307..22d9fcec2 100644 --- a/src/tools/ecode/ecode.cpp +++ b/src/tools/ecode/ecode.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include @@ -496,7 +497,7 @@ void App::openFontDialog( std::string& fontPath, bool loadingMonoFont, bool term msgBox->on( Event::OnConfirm, [loadMonoFont, fontMono]( const Event* ) { loadMonoFont( fontMono ); } ); - msgBox->on( Event::OnCancel, [fontMono]( const Event* ) { + msgBox->on( Event::OnDiscard, [fontMono]( const Event* ) { FontManager::instance()->remove( fontMono ); } ); msgBox->setTitle( i18n( "confirm_loading_font", "Font loading confirmation" ) ); @@ -2280,55 +2281,53 @@ void App::createDocManyLangsAlert( UICodeEditor* editor ) { Seconds( 30.f ) ); } -void App::loadImageFromMedium( const std::string& path, bool isMemory ) { - UIImage* imageView = mImageLayout->findByType( UI_TYPE_IMAGE ); - UILoader* loaderView = mImageLayout->findByType( UI_TYPE_LOADER ); - if ( imageView ) { - mImageLayout->setEnabled( true )->setVisible( true ); - loaderView->setVisible( true ); - mThreadPool->run( [this, imageView, loaderView, path, isMemory]() { - Image::Format format = - isMemory ? Image::getFormat( reinterpret_cast( path.c_str() ), - path.size() ) - : Image::getFormat( path ); +void App::loadImageFromMedium( const std::string& path, bool isMemory, bool forcePreview, + bool forceTab ) { + if ( !forceTab && ( mConfig.ui.imagesQuickPreview || forcePreview ) ) { + UIImageViewer* imageView = mImageLayout->findByType( UI_TYPE_IMAGE_VIEWER ); - if ( format == Image::Format::Unknown ) - return; - - Drawable* image = nullptr; - - if ( format != Image::Format::GIF ) { - image = isMemory ? TextureFactory::instance()->loadFromMemory( - reinterpret_cast( path.c_str() ), - path.size() ) - : TextureFactory::instance()->loadFromFile( path ); - } else { - IOStream* stream = isMemory - ? (IOStream*)new IOStreamMemory( path.c_str(), path.size() ) - : (IOStream*)new IOStreamFile( path ); - Sprite* sprite = Sprite::fromGif( *stream ); - sprite->setAutoAnimate( false ); - image = sprite; - delete stream; - } - - if ( mImageLayout->isVisible() ) { - imageView->runOnMainThread( [this, imageView, loaderView, image]() { - mImageLayout->setFocus(); - imageView->setDrawable( image, true ); - loaderView->setVisible( false ); + if ( imageView ) { + mImageLayout->setEnabled( true )->setVisible( true ); + if ( !imageView->hasEventsOfType( Event::OnResourceLoaded ) ) { + imageView->on( Event::OnResourceLoaded, [this, imageView]( auto ) { + if ( mImageLayout->isVisible() ) { + mImageLayout->getFirstChild()->setFocus(); + } else { + imageView->reset(); + } } ); - } else { - eeSAFE_DELETE( image ); - imageView->setDrawable( nullptr ); - loaderView->setVisible( false ); } - } ); + imageView->loadImageAsync( path, isMemory, true ); + } + } else { + UIImageViewer* imageView = UIImageViewer::New(); + auto [tab, iv] = + mSplitter->createWidget( imageView, i18n( "image_viewer", "Image Viewer" ) ); + + if ( imageView ) { + imageView->on( Event::OnResourceLoaded, [tab, imageView, this]( auto ) { + tab->setText( imageView->getImageName().empty() + ? i18n( "image_viewer", "Image Viewer" ) + : String( imageView->getImageName() ) ); + std::string path( imageView->getImagePath() ); + tab->setTooltipText( path ); + if ( mConfig.editor.syncProjectTreeWithEditor ) { + mProjectTreeView->setFocusOnSelection( false ); + if ( !mCurrentProject.empty() && String::startsWith( path, mCurrentProject ) ) { + mProjectTreeView->openRowWithPath( path.substr( mCurrentProject.size() ) ); + } else { + mProjectTreeView->openRowWithPath( FileSystem::fileNameFromPath( path ) ); + } + mProjectTreeView->setFocusOnSelection( true ); + } + } ); + imageView->loadImageAsync( path, isMemory, true ); + } } } void App::loadImageFromMemory( const std::string& content ) { - loadImageFromMedium( content, true ); + loadImageFromMedium( content, true, true ); } void App::loadImageFromPath( const std::string& path ) { @@ -2349,6 +2348,7 @@ void App::openFileFromPath( const std::string& path ) { msgBox->getButtonOK()->getIcon()->setVisible( false ); msgBox->getButtonCancel()->setText( i18n( "open_externally", "Open externally" ) ); msgBox->getButtonCancel()->getIcon()->setVisible( false ); + msgBox->setCloseShortcut( { KEY_ESCAPE } ); msgBox->on( Event::OnConfirm, [this, path]( auto ) { loadFileFromPath( path ); } ); msgBox->on( Event::OnCancel, [path]( auto ) { FileInfo f( path ); @@ -3326,19 +3326,18 @@ void App::initProjectTreeView( std::string path, bool openClean ) { } void App::initImageView() { - mImageLayout->on( Event::MouseClick, [this]( const Event* ) { - mImageLayout->findByType( UI_TYPE_IMAGE )->setDrawable( nullptr ); + const auto hideImagePreview = [this] { + mImageLayout->findByType( UI_TYPE_IMAGE_VIEWER )->reset(); mImageLayout->setEnabled( false )->setVisible( false ); if ( mSplitter->getCurWidget() ) mSplitter->getCurWidget()->setFocus(); + }; + mImageLayout->find( "image_close" )->on( Event::MouseClick, [hideImagePreview]( auto ) { + hideImagePreview(); } ); - mImageLayout->on( Event::KeyDown, [this]( const Event* event ) { - if ( event->asKeyEvent()->getKeyCode() == KEY_ESCAPE ) { - mImageLayout->findByType( UI_TYPE_IMAGE )->setDrawable( nullptr ); - mImageLayout->setEnabled( false )->setVisible( false ); - if ( mSplitter->getCurWidget() ) - mSplitter->getCurWidget()->setFocus(); - } + mImageLayout->on( Event::KeyDown, [hideImagePreview]( const Event* event ) { + if ( event->asKeyEvent()->getKeyCode() == KEY_ESCAPE ) + hideImagePreview(); } ); } @@ -4064,7 +4063,7 @@ void App::init( InitParameters& params ) { onFileDropped( file, true ); } } ); - msgBox->on( Event::OnCancel, + msgBox->on( Event::OnDiscard, [this, pathsToLoad = mPathsToLoad]( const Event* ) { for ( const auto& file : pathsToLoad ) { if ( !FileSystem::isDirectory( file ) && diff --git a/src/tools/ecode/ecode.hpp b/src/tools/ecode/ecode.hpp index b193c4760..521942f36 100644 --- a/src/tools/ecode/ecode.hpp +++ b/src/tools/ecode/ecode.hpp @@ -504,7 +504,7 @@ class App : public UICodeEditorSplitter::Client, public PluginContextProvider { void setTheme( const std::string& path ); - void loadImageFromMedium( const std::string& path, bool isMemory ); + void loadImageFromMedium( const std::string& path, bool isMemory, bool forcePreview = false, bool forceTab = false ); void loadImageFromPath( const std::string& path ); diff --git a/src/tools/ecode/plugins/git/gitplugin.cpp b/src/tools/ecode/plugins/git/gitplugin.cpp index ce0f46559..32245a9d6 100644 --- a/src/tools/ecode/plugins/git/gitplugin.cpp +++ b/src/tools/ecode/plugins/git/gitplugin.cpp @@ -693,7 +693,7 @@ void GitPlugin::checkout( Git::Branch branch ) { UIMessageBox* msgBox = UIMessageBox::New( UIMessageBox::YES_NO, i18n( "git_create_local_branch", "Create local branch?" ) ); msgBox->on( Event::OnConfirm, [checkOutFn]( const Event* ) { checkOutFn( true ); } ); - msgBox->on( Event::OnCancel, [checkOutFn]( const Event* ) { checkOutFn( false ); } ); + msgBox->on( Event::OnDiscard, [checkOutFn]( const Event* ) { checkOutFn( false ); } ); msgBox->setTitle( i18n( "git_checkout", "Check Out" ) ); msgBox->center(); msgBox->showWhenReady(); @@ -912,7 +912,7 @@ void GitPlugin::commit( const std::string& repoPath ) { true, true, true, true, true ); } ); - msgBox->on( Event::OnCancel, [this, msgBox]( const Event* ) { + msgBox->on( Event::OnDiscard, [this, msgBox]( const Event* ) { mLastCommitMsg = msgBox->getTextEdit()->getText(); } ); diff --git a/src/tools/ecode/plugins/plugincontextprovider.hpp b/src/tools/ecode/plugins/plugincontextprovider.hpp index 4b963d0d2..f9b7bde3a 100644 --- a/src/tools/ecode/plugins/plugincontextprovider.hpp +++ b/src/tools/ecode/plugins/plugincontextprovider.hpp @@ -136,6 +136,12 @@ class PluginContextProvider { virtual bool isDirTreeReady() const = 0; virtual bool pluginsDisabled() const = 0; + + virtual void loadImageFromMedium( const std::string& path, bool isMemory, + + bool forcePreview = false, bool forceTab = false ) = 0; + + virtual void loadImageFromPath( const std::string& path ) = 0; }; } // namespace ecode diff --git a/src/tools/ecode/settingsactions.cpp b/src/tools/ecode/settingsactions.cpp index d4c683eb4..96617cf99 100644 --- a/src/tools/ecode/settingsactions.cpp +++ b/src/tools/ecode/settingsactions.cpp @@ -236,7 +236,7 @@ void SettingsActions::setIndentTabCharacter() { msgBoxAlert->setCloseShortcut( { KEY_ESCAPE, 0 } ); } } ); - msgBox->on( Event::OnCancel, [this]( const Event* ) { + msgBox->on( Event::OnDiscard, [this]( const Event* ) { String txt = String::fromUtf8( mApp->getConfig().editor.tabIndentCharacter ); if ( txt.size() != 1 ) return; diff --git a/src/tools/ecode/settingsmenu.cpp b/src/tools/ecode/settingsmenu.cpp index 22261c0dc..98a8d7758 100644 --- a/src/tools/ecode/settingsmenu.cpp +++ b/src/tools/ecode/settingsmenu.cpp @@ -1288,11 +1288,18 @@ UIMenu* SettingsMenu::createWindowMenu() { mWindowMenu ->addCheckBox( i18n( "use_native_file_dialogs", "Enable Native File Dialogs" ), mApp->getConfig().ui.nativeFileDialogs ) - ->setTooltipText( - i18n( "use_native_file_dialogs_tooltip", - "Try to use the OS native file dialogs if they are available." ) ) + ->setTooltipText( i18n( "use_native_file_dialogs_tooltip", + "Try to use the OS native file dialogs if they are available." ) ) ->setId( "native-file-dialogs" ); + mWindowMenu + ->addCheckBox( i18n( "quick_preview_images", "Quick Preview Images" ), + mApp->getConfig().ui.imagesQuickPreview ) + ->setTooltipText( + i18n( "quick_preview_images_tooltip", + "Instead of opening a new tab to view an image uses a quick-preview." ) ) + ->setId( "quick-preview-images" ); + mWindowMenu->addSeparator(); mWindowMenu @@ -1326,6 +1333,10 @@ UIMenu* SettingsMenu::createWindowMenu() { bool active = item->asType()->isActive(); mApp->getConfig().ui.nativeFileDialogs = active; mApp->saveConfig(); + } else if ( "quick-preview-images" == item->getId() ) { + bool active = item->asType()->isActive(); + mApp->getConfig().ui.imagesQuickPreview = active; + mApp->saveConfig(); } else { String text = String( event->getNode()->asType()->getId() ).toLower(); String::replaceAll( text, " ", "-" );