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;