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.
This commit is contained in:
Martín Lucas Golini
2026-08-11 15:43:22 -03:00
parent a71e062dfb
commit 4789bfb5f9
13 changed files with 271 additions and 7 deletions
+9
View File
@@ -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<std::string>& 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, ',' ) );
+7
View File
@@ -96,6 +96,12 @@ struct WindowStateConfig {
std::vector<std::string> 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;
+3 -3
View File
@@ -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();
+68
View File
@@ -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<KeyBindings::Shortcut, std::string> 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<std::string, std::string> App::getMigrateKeybindings() {
std::vector<std::string> 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<std::string> pathFilters ) {
mGlobalSearchController->showGlobalSearch( searchAndReplace, pathFilters );
}
+11
View File
@@ -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<std::string> 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(); } );
+2 -2
View File
@@ -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(
<vbox lw="mp" class="notification">
<TextView lw="mp" wordwrap="true" />
<TextView lw="mp" lh="wc" word-wrap="true" />
<hbox lg="right">
<PushButton />
</hbox>
@@ -103,7 +103,7 @@ void NotificationCenter::addInteractiveNotification( String text, String actionT
allowCopy, onInteraction = std::move( onInteraction )]() {
static const auto layout = R"xml(
<vbox lw="mp" class="notification">
<TextView lw="mp" wordwrap="true" />
<TextView lw="mp" lh="wc" word-wrap="true" />
<hbox lg="right">
<PushButton />
</hbox>
+79
View File
@@ -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<UIFileDialog>()->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 );
}
+6
View File
@@ -40,6 +40,12 @@ class SettingsActions {
void setUIPanelFontSize();
void setScreenshotSavePath();
void setScreenshotFilenamePattern();
void setScreenshotSaveFormat();
private:
App* mApp{ nullptr };
+22 -2
View File
@@ -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;