From 4789bfb5f92f777daad3a3fd0fde69f046f208e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Lucas=20Golini?= Date: Tue, 11 Aug 2026 15:43:22 -0300 Subject: [PATCH] ecode: add configurable screenshot support Add an unlocked take-screenshot command and expose it under Settings -> Window with a default Print Screen binding. Allow configuring the screenshot destination, filename pattern, and output format. Save screenshots under the ecode configuration directory by default, notify users after capture, and provide an action to open the result in the integrated image viewer. Addresses a request in SpartanJ/ecode#950. --- bin/assets/i18n/de.xml | 16 ++++++ bin/assets/i18n/en.xml | 16 ++++++ bin/assets/i18n/fr.xml | 16 ++++++ bin/assets/i18n/zh.xml | 16 ++++++ src/tools/ecode/appconfig.cpp | 9 +++ src/tools/ecode/appconfig.hpp | 7 +++ src/tools/ecode/datetimecontroller.hpp | 6 +- src/tools/ecode/ecode.cpp | 68 ++++++++++++++++++++++ src/tools/ecode/ecode.hpp | 11 ++++ src/tools/ecode/notificationcenter.cpp | 4 +- src/tools/ecode/settingsactions.cpp | 79 ++++++++++++++++++++++++++ src/tools/ecode/settingsactions.hpp | 6 ++ src/tools/ecode/settingsmenu.cpp | 24 +++++++- 13 files changed, 271 insertions(+), 7 deletions(-) diff --git a/bin/assets/i18n/de.xml b/bin/assets/i18n/de.xml index 37f12229a..262635114 100644 --- a/bin/assets/i18n/de.xml +++ b/bin/assets/i18n/de.xml @@ -930,4 +930,20 @@ Für sichtbare Änderung ecode neu starten. Hereinzoomen Herauszoomen Zoom zurücksetzen + Bildschirmfoto aufnehmen + Bildschirmfoto-Einstellungen + Speicherpfad + Dateinamensmuster + Speicherformat + Dateinamensmuster für Bildschirmfotos festlegen + Legen Sie das Dateinamensmuster für Bildschirmfotos fest: +Verwendet strftime-Formatbezeichner. Die Dateierweiterung richtet sich nach dem ausgewählten Bildschirmfotoformat. + Speicherformat für Bildschirmfotos festlegen + Wählen Sie das Speicherformat für Bildschirmfotos: + Das Dateinamensmuster für Bildschirmfotos ist ungültig. + Ungültiges Speicherformat für Bildschirmfotos. + Das Verzeichnis für Bildschirmfotos konnte nicht erstellt werden. + Es konnte kein verfügbarer Dateiname für das Bildschirmfoto gefunden werden. + Das Bildschirmfoto konnte nicht gespeichert werden. + Bildschirmfoto gespeichert: diff --git a/bin/assets/i18n/en.xml b/bin/assets/i18n/en.xml index 63704b28e..3e29de6bc 100644 --- a/bin/assets/i18n/en.xml +++ b/bin/assets/i18n/en.xml @@ -914,4 +914,20 @@ Restart ecode to see the changes. Zoom In Zoom Out Zoom Reset + Take Screenshot + Screenshot Settings + Save Path + Filename Pattern + Save Format + Set Screenshot Filename Pattern + Set the screenshot filename pattern: +Uses strftime format specifiers. The file extension follows the selected screenshot format. + Set Screenshot Save Format + Select the screenshot save format: + The screenshot filename pattern is invalid. + Invalid screenshot save format. + Couldn't create the screenshot directory. + Couldn't find an available screenshot filename. + Couldn't save the screenshot. + Screenshot saved: diff --git a/bin/assets/i18n/fr.xml b/bin/assets/i18n/fr.xml index fab4e7f9f..449f784cd 100644 --- a/bin/assets/i18n/fr.xml +++ b/bin/assets/i18n/fr.xml @@ -909,4 +909,20 @@ Redémarrer ecode pour voir les changements. Ouvrir le lien Coller dans le terminal Renommer le terminal + Prendre une capture d’écran + Paramètres des captures d’écran + Dossier d’enregistrement + Modèle de nom de fichier + Format d’enregistrement + Définir le modèle de nom des captures d’écran + Définissez le modèle de nom des captures d’écran : +Utilise les spécificateurs de format strftime. L’extension du fichier correspond au format de capture sélectionné. + Définir le format des captures d’écran + Sélectionnez le format d’enregistrement des captures d’écran : + Le modèle de nom des captures d’écran n’est pas valide. + Le format d’enregistrement des captures d’écran n’est pas valide. + Impossible de créer le dossier des captures d’écran. + Impossible de trouver un nom de fichier disponible pour la capture d’écran. + Impossible d’enregistrer la capture d’écran. + Capture d’écran enregistrée : diff --git a/bin/assets/i18n/zh.xml b/bin/assets/i18n/zh.xml index 7e61a50a8..4e6ef0d87 100644 --- a/bin/assets/i18n/zh.xml +++ b/bin/assets/i18n/zh.xml @@ -688,4 +688,20 @@ file in the directory tree. 字小 字大 重设字体大小 + 截取屏幕截图 + 屏幕截图设置 + 保存路径 + 文件名格式 + 保存格式 + 设置屏幕截图文件名格式 + 设置屏幕截图文件名格式: +使用 strftime 格式说明符。文件扩展名由所选屏幕截图格式决定。 + 设置屏幕截图保存格式 + 选择屏幕截图保存格式: + 屏幕截图文件名格式无效。 + 屏幕截图保存格式无效。 + 无法创建屏幕截图目录。 + 无法找到可用的屏幕截图文件名。 + 无法保存屏幕截图。 + 屏幕截图已保存: diff --git a/src/tools/ecode/appconfig.cpp b/src/tools/ecode/appconfig.cpp index 00a52e1c4..b25caccd4 100644 --- a/src/tools/ecode/appconfig.cpp +++ b/src/tools/ecode/appconfig.cpp @@ -127,6 +127,12 @@ void AppConfig::load( const std::string& confPath, std::string& keybindingsPath, windowState.lastRunVersion = iniState.getValueU( "editor", "last_run_version", 0 ); windowState.sidePanelTabsOrder = String::split( iniState.getValue( "ui", "side_panel_tabs_order", "" ), ',' ); + screenshot.savePath = ini.getValue( "screenshots", "save_path", "" ); + screenshot.filenamePattern = + ini.getValue( "screenshots", "filename_pattern", "ecode-%Y-%m-%d-%H-%M-%S.png" ); + screenshot.saveFormat = ini.getValue( "screenshots", "save_format", "png" ); + if ( Image::extensionToSaveType( screenshot.saveFormat ) == Image::SaveType::Unknown ) + screenshot.saveFormat = "png"; editor.showLineNumbers = ini.getValueB( "editor", "show_line_numbers", true ); editor.showWhiteSpaces = ini.getValueB( "editor", "show_white_spaces", true ); editor.showLineEndings = ini.getValueB( "editor", "show_line_endings", false ); @@ -366,6 +372,9 @@ void AppConfig::save( const std::vector& recentFiles, ini.setValue( "ui", "font_hinting", FontTrueType::fontHintingToString( ui.fontHinting ) ); ini.setValue( "ui", "font_antialiasing", FontTrueType::fontAntialiasingToString( ui.fontAntialiasing ) ); + ini.setValue( "screenshots", "save_path", screenshot.savePath ); + ini.setValue( "screenshots", "filename_pattern", screenshot.filenamePattern ); + ini.setValue( "screenshots", "save_format", screenshot.saveFormat ); ini.setValueB( "ui", "editor_font_in_input_fields", ui.editorFontInInputFields ); iniState.setValue( "ui", "side_panel_tabs_order", String::join( windowState.sidePanelTabsOrder, ',' ) ); diff --git a/src/tools/ecode/appconfig.hpp b/src/tools/ecode/appconfig.hpp index 1ce071ba4..0cba80b72 100644 --- a/src/tools/ecode/appconfig.hpp +++ b/src/tools/ecode/appconfig.hpp @@ -96,6 +96,12 @@ struct WindowStateConfig { std::vector sidePanelTabsOrder; }; +struct ScreenshotConfig { + std::string savePath; + std::string filenamePattern{ "ecode-%Y-%m-%d-%H-%M-%S.png" }; + std::string saveFormat{ "png" }; +}; + struct CodeEditorConfig { std::string colorScheme{ "ecode" }; StyleSheetLength fontSize{ 11, StyleSheetLength::Dp }; @@ -296,6 +302,7 @@ struct TabWidgetCbs { class AppConfig { public: WindowStateConfig windowState; + ScreenshotConfig screenshot; ContextSettings context; CodeEditorConfig editor; DocumentConfig doc; diff --git a/src/tools/ecode/datetimecontroller.hpp b/src/tools/ecode/datetimecontroller.hpp index e77e5c21f..5086db95e 100644 --- a/src/tools/ecode/datetimecontroller.hpp +++ b/src/tools/ecode/datetimecontroller.hpp @@ -20,13 +20,13 @@ class DateTimeController { void registerCommands( TextDocument& doc ); - private: - PluginContextProvider* mContext{ nullptr }; - static bool isValidDateFormat( const std::string& format ); static std::string formatCurrentDate( const std::string& format ); + private: + PluginContextProvider* mContext{ nullptr }; + void insertDate( const std::string& format ); void setCustomDateFormat(); diff --git a/src/tools/ecode/ecode.cpp b/src/tools/ecode/ecode.cpp index 2964a5cd3..fe963b6cd 100644 --- a/src/tools/ecode/ecode.cpp +++ b/src/tools/ecode/ecode.cpp @@ -392,6 +392,14 @@ std::string App::getDefaultFileDialogFolder() const { return mLastFileFolder.empty() ? getLastUsedFolder() : mLastFileFolder; } +std::string App::getDefaultScreenshotPath() const { + std::string path( mConfigPath ); + FileSystem::dirAddSlashAtEnd( path ); + path += "screenshots"; + FileSystem::dirAddSlashAtEnd( path ); + return path; +} + std::string App::getLastUsedFolder() const { if ( !mCurrentProject.empty() && mCurrentProject != getPlaygroundPath() ) return mCurrentProject; @@ -2101,6 +2109,7 @@ static Uint32 DefaultSwitchToStatusPanelModifier = KEYMOD_LALT; std::map App::getLocalKeybindings() { return { + { { KEY_PRINTSCREEN, KEYMOD_NONE }, "take-screenshot" }, { { KEY_RETURN, KEYMOD_LALT | KEYMOD_LCTRL }, "fullscreen-toggle" }, { { KEY_F3, KEYMOD_NONE }, "repeat-find" }, { { KEY_F3, KEYMOD_SHIFT }, "find-prev" }, @@ -2167,6 +2176,10 @@ std::map App::getMigrateKeybindings() { std::vector App::getUnlockedCommands() { return { + "take-screenshot", + "screenshot-save-path", + "screenshot-filename-pattern", + "screenshot-save-format", "create-new", "create-new-terminal", "create-new-welcome-tab", @@ -2852,6 +2865,61 @@ void App::fullscreenToggle() { mSettings->updateViewMenu(); } +void App::takeScreenshot() { + auto format = Image::extensionToSaveType( mConfig.screenshot.saveFormat ); + if ( format == Image::SaveType::Unknown ) + format = Image::SaveType::PNG; + + std::string filename = + DateTimeController::formatCurrentDate( mConfig.screenshot.filenamePattern ); + if ( filename.empty() ) { + errorMsgBox( i18n( "invalid_screenshot_filename_pattern", + "The screenshot filename pattern is invalid." ) ); + return; + } + + const std::string extension = "." + Image::saveTypeToExtension( format ); + const std::string configuredExtension = FileSystem::fileExtension( filename ); + if ( !configuredExtension.empty() ) + filename.resize( filename.size() - configuredExtension.size() - 1 ); + filename += extension; + + std::string savePath = mConfig.screenshot.savePath.empty() ? getDefaultScreenshotPath() + : mConfig.screenshot.savePath; + FileSystem::dirAddSlashAtEnd( savePath ); + if ( !FileSystem::isDirectory( savePath ) && !FileSystem::makeDir( savePath, true ) ) { + errorMsgBox( i18n( "couldnt_create_screenshot_directory", + "Couldn't create the screenshot directory." ) ); + return; + } + + std::string filepath = savePath + filename; + if ( FileSystem::fileExists( filepath ) ) { + const std::string stem = FileSystem::fileRemoveExtension( filename ); + bool availablePathFound = false; + for ( Uint32 suffix = 2; suffix < 10000; ++suffix ) { + filepath = savePath + stem + "-" + String::toString( suffix ) + extension; + if ( !FileSystem::fileExists( filepath ) ) { + availablePathFound = true; + break; + } + } + if ( !availablePathFound ) { + errorMsgBox( i18n( "couldnt_find_available_screenshot_filename", + "Couldn't find an available screenshot filename." ) ); + return; + } + } + + if ( mWindow->takeScreenshot( filepath, format ) ) { + mNotificationCenter->addInteractiveNotification( + i18n( "screenshot_saved", "Screenshot saved:" ) + "\n" + filepath, + i18n( "open", "Open" ), [this, filepath] { openFileFromPath( filepath ); } ); + } else { + errorMsgBox( i18n( "couldnt_save_screenshot", "Couldn't save the screenshot." ) ); + } +} + void App::showGlobalSearch( bool searchAndReplace, std::optional pathFilters ) { mGlobalSearchController->showGlobalSearch( searchAndReplace, pathFilters ); } diff --git a/src/tools/ecode/ecode.hpp b/src/tools/ecode/ecode.hpp index 7659266ed..6db3695d7 100644 --- a/src/tools/ecode/ecode.hpp +++ b/src/tools/ecode/ecode.hpp @@ -77,6 +77,8 @@ class App : public UICodeEditorSplitter::Client, public PluginContextProvider { std::string getDefaultFileDialogFolder() const; + std::string getDefaultScreenshotPath() const; + void openFolderDialog(); void openFontDialog( std::string& fontPath, bool loadingMonoFont, bool terminalFont = false, @@ -131,6 +133,8 @@ class App : public UICodeEditorSplitter::Client, public PluginContextProvider { void fullscreenToggle(); + void takeScreenshot(); + void downloadFileWebDialog(); void showGlobalSearch( bool searchAndReplace, std::optional pathFilters = {} ); @@ -342,6 +346,13 @@ class App : public UICodeEditorSplitter::Client, public PluginContextProvider { t.setCommand( "about-ecode", [this] { mSettingsActions->aboutEcode(); } ); t.setCommand( "ecode-source", [this] { mSettingsActions->ecodeSource(); } ); t.setCommand( "ui-scale-factor", [this] { mSettingsActions->setUIScaleFactor(); } ); + t.setCommand( "take-screenshot", [this] { takeScreenshot(); } ); + t.setCommand( "screenshot-save-path", + [this] { mSettingsActions->setScreenshotSavePath(); } ); + t.setCommand( "screenshot-filename-pattern", + [this] { mSettingsActions->setScreenshotFilenamePattern(); } ); + t.setCommand( "screenshot-save-format", + [this] { mSettingsActions->setScreenshotSaveFormat(); } ); t.setCommand( "show-side-panel", [this] { switchSidePanel(); } ); t.setCommand( "toggle-status-bar", [this] { switchStatusBar(); } ); t.setCommand( "toggle-menu-bar", [this] { switchMenuBar(); } ); diff --git a/src/tools/ecode/notificationcenter.cpp b/src/tools/ecode/notificationcenter.cpp index e7618b5ae..66f0751b3 100644 --- a/src/tools/ecode/notificationcenter.cpp +++ b/src/tools/ecode/notificationcenter.cpp @@ -68,7 +68,7 @@ void NotificationCenter::addShowRequest( const String& uri, const String& action auto action = [this, uri, actionText, delay]() { static const auto layout = R"xml( - + @@ -103,7 +103,7 @@ void NotificationCenter::addInteractiveNotification( String text, String actionT allowCopy, onInteraction = std::move( onInteraction )]() { static const auto layout = R"xml( - + diff --git a/src/tools/ecode/settingsactions.cpp b/src/tools/ecode/settingsactions.cpp index 743fe260f..7965036e3 100644 --- a/src/tools/ecode/settingsactions.cpp +++ b/src/tools/ecode/settingsactions.cpp @@ -1,4 +1,5 @@ #include "settingsactions.hpp" +#include "datetimecontroller.hpp" #include "ecode.hpp" #include "version.hpp" @@ -414,6 +415,84 @@ void SettingsActions::setUIPanelFontSize() { mApp->setFocusEditorOnClose( msgBox ); } +void SettingsActions::setScreenshotSavePath() { + std::string initialPath = mApp->getConfig().screenshot.savePath; + if ( !FileSystem::isDirectory( initialPath ) ) { + initialPath = mApp->getDefaultScreenshotPath(); + FileSystem::makeDir( initialPath, true ); + } + UIFileDialog* dialog = UIFileDialog::New( + UIFileDialog::DefaultFlags | UIFileDialog::AllowFolderSelect | + UIFileDialog::ShowOnlyFolders | + ( mApp->getConfig().ui.nativeFileDialogs ? UIFileDialog::UseNativeFileDialog : 0 ), + "*", initialPath ); + dialog->setWindowFlags( UI_WIN_DEFAULT_FLAGS | UI_WIN_MAXIMIZE_BUTTON | UI_WIN_MODAL ); + dialog->setTitle( i18n( "screenshot_save_path", "Screenshot Save Path" ) ); + dialog->setCloseShortcut( KEY_ESCAPE ); + dialog->on( Event::OpenFile, [this]( const Event* event ) { + std::string path = event->getNode()->asType()->getFullPath(); + if ( FileSystem::isDirectory( path ) ) { + FileSystem::dirAddSlashAtEnd( path ); + mApp->getConfig().screenshot.savePath = std::move( path ); + mApp->saveConfig(); + } + } ); + dialog->center(); + dialog->show(); +} + +void SettingsActions::setScreenshotFilenamePattern() { + UIMessageBox* msgBox = UIMessageBox::New( + UIMessageBox::INPUT, + i18n( "set_screenshot_filename_pattern_message", + "Set the screenshot filename pattern:\nUses strftime format specifiers. The file " + "extension follows the selected screenshot format." ) ); + msgBox->setTitle( + i18n( "set_screenshot_filename_pattern", "Set Screenshot Filename Pattern" ) ); + msgBox->setCloseShortcut( { KEY_ESCAPE, 0 } ); + msgBox->getTextInput()->setText( mApp->getConfig().screenshot.filenamePattern ); + msgBox->showWhenReady(); + msgBox->on( Event::OnConfirm, [this, msgBox]( const Event* ) { + std::string pattern = msgBox->getTextInput()->getText().toUtf8(); + std::string filename = DateTimeController::formatCurrentDate( pattern ); + if ( !DateTimeController::isValidDateFormat( pattern ) || filename.empty() || + filename.find_first_of( "<>:\"/\\|?*" ) != std::string::npos ) { + msgBox->getTextInput()->addClass( "error" ); + mApp->errorMsgBox( i18n( "invalid_screenshot_filename_pattern", + "The screenshot filename pattern is invalid." ) ); + return; + } + mApp->getConfig().screenshot.filenamePattern = std::move( pattern ); + mApp->saveConfig(); + msgBox->closeWindow(); + } ); +} + +void SettingsActions::setScreenshotSaveFormat() { + UIMessageBox* msgBox = UIMessageBox::New( + UIMessageBox::DROPDOWNLIST, + i18n( "set_screenshot_save_format_message", "Select the screenshot save format:" ) ); + msgBox->setTitle( i18n( "set_screenshot_save_format", "Set Screenshot Save Format" ) ); + msgBox->setCloseShortcut( { KEY_ESCAPE, 0 } ); + msgBox->getDropDownList()->getListBox()->addListBoxItems( + { "PNG", "JPG", "WEBP", "QOI", "BMP", "TGA", "DDS" } ); + msgBox->getDropDownList()->getListBox()->setSelected( + String( mApp->getConfig().screenshot.saveFormat ).toUpper() ); + msgBox->showWhenReady(); + msgBox->on( Event::OnConfirm, [this, msgBox]( const Event* ) { + String formatText( msgBox->getDropDownList()->getText() ); + std::string format = formatText.toLower().toUtf8(); + if ( Image::extensionToSaveType( format ) == Image::SaveType::Unknown ) { + mApp->errorMsgBox( + i18n( "invalid_screenshot_save_format", "Invalid screenshot save format." ) ); + return; + } + mApp->getConfig().screenshot.saveFormat = std::move( format ); + mApp->saveConfig(); + msgBox->closeWindow(); + } ); +} + String SettingsActions::i18n( const std::string& key, const String& def ) { return mApp->i18n( key, def ); } diff --git a/src/tools/ecode/settingsactions.hpp b/src/tools/ecode/settingsactions.hpp index 9353b24fb..38c04c98c 100644 --- a/src/tools/ecode/settingsactions.hpp +++ b/src/tools/ecode/settingsactions.hpp @@ -40,6 +40,12 @@ class SettingsActions { void setUIPanelFontSize(); + void setScreenshotSavePath(); + + void setScreenshotFilenamePattern(); + + void setScreenshotSaveFormat(); + private: App* mApp{ nullptr }; diff --git a/src/tools/ecode/settingsmenu.cpp b/src/tools/ecode/settingsmenu.cpp index e42296bbc..8c5eda4a7 100644 --- a/src/tools/ecode/settingsmenu.cpp +++ b/src/tools/ecode/settingsmenu.cpp @@ -1732,13 +1732,33 @@ UIMenu* SettingsMenu::createWindowMenu() { "to choose a language again when opening these files." ) ) ->setId( "reset-global-file-associations" ); - mWindowMenu->addSeparator(); - mWindowMenu ->addCheckBox( i18n( "welcome_screen_enable", "Enable Welcome Screen" ), mApp->getConfig().ui.welcomeScreen ) ->setId( "welcome-screen-enable" ); + mWindowMenu->addSeparator(); + + UIPopUpMenu* screenshotMenu = UIPopUpMenu::New(); + screenshotMenu->add( i18n( "screenshot_save_path", "Save Path" ) ) + ->setId( "screenshot-save-path" ); + screenshotMenu->add( i18n( "screenshot_filename_pattern", "Filename Pattern" ) ) + ->setId( "screenshot-filename-pattern" ); + screenshotMenu->add( i18n( "screenshot_save_format", "Save Format" ) ) + ->setId( "screenshot-save-format" ); + screenshotMenu->on( Event::OnItemClicked, [this]( const Event* event ) { + if ( event->getNode()->isType( UI_TYPE_MENUITEM ) ) + runCommand( event->getNode()->getId() ); + } ); + mWindowMenu->addSubMenu( i18n( "screenshot_settings", "Screenshot Settings" ), + findIcon( "image" ), screenshotMenu ); + + mWindowMenu->addSeparator(); + mWindowMenu + ->add( i18n( "take_screenshot", "Take Screenshot" ), findIcon( "image" ), + getKeybind( "take-screenshot" ) ) + ->setId( "take-screenshot" ); + mWindowMenu->on( Event::OnItemClicked, [this]( const Event* event ) { if ( !event->getNode()->isType( UI_TYPE_MENUITEM ) ) return;