mirror of
https://github.com/SpartanJ/eepp.git
synced 2026-10-01 18:50:16 +03:00
eepp: add reusable UISettingsPanel
- add the generic settings model and panel to EE::UI::Tools - support category navigation, filtering, and lazy materialization - add UISettingsPanel model and filtering tests ecode: migrate settings to UISettingsPanel - replace the application-specific settings model - use the generic settings panel implementation - add monitor refresh rate as a frame limit option - normalize UI labels and translations eterm: add persistent configuration and settings panel - persist application preferences and window state - use saved values as defaults for command-line options - persist command-line overrides - add graphical settings with live application - expose renderer, terminal behavior, appearance, font, and scale options
This commit is contained in:
@@ -31,6 +31,7 @@
|
||||
#include <eepp/ui/uitooltip.hpp>
|
||||
#include <eepp/ui/uitreeview.hpp>
|
||||
#include <eepp/ui/uiwidgetcreator.hpp>
|
||||
#include <eepp/window/clipboard.hpp>
|
||||
#include <eepp/window/engine.hpp>
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
|
||||
@@ -1,148 +0,0 @@
|
||||
#ifndef ECODE_SETTINGSMODEL_HPP
|
||||
#define ECODE_SETTINGSMODEL_HPP
|
||||
|
||||
#include <algorithm>
|
||||
#include <eepp/ee.hpp>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <variant>
|
||||
#include <vector>
|
||||
|
||||
namespace ecode {
|
||||
|
||||
struct SettingDescriptor {
|
||||
std::string id;
|
||||
std::string category;
|
||||
String name;
|
||||
String description;
|
||||
String group;
|
||||
};
|
||||
|
||||
struct BoolPointerSetting {
|
||||
bool* value{ nullptr };
|
||||
std::function<void( bool )> apply;
|
||||
};
|
||||
|
||||
struct BoolSetting {
|
||||
std::function<bool()> get;
|
||||
std::function<void( bool )> set;
|
||||
};
|
||||
|
||||
struct ChoiceSetting {
|
||||
std::vector<String> choices;
|
||||
std::vector<String> descriptions;
|
||||
std::function<size_t()> get;
|
||||
std::function<void( size_t )> set;
|
||||
};
|
||||
|
||||
struct EditableChoiceSetting {
|
||||
std::vector<String> choices;
|
||||
std::function<String()> get;
|
||||
std::function<bool( const String& )> set;
|
||||
};
|
||||
|
||||
struct IntegerSetting {
|
||||
int min{ 0 };
|
||||
int max{ 0 };
|
||||
std::function<int()> get;
|
||||
std::function<void( int )> set;
|
||||
};
|
||||
|
||||
struct TextSetting {
|
||||
std::function<std::string()> get;
|
||||
std::function<bool( const std::string& )> set;
|
||||
bool commitOnFocusLoss{ false };
|
||||
bool password{ false };
|
||||
};
|
||||
|
||||
struct FloatSetting {
|
||||
double min{ 0 };
|
||||
double max{ 0 };
|
||||
double step{ 0 };
|
||||
std::function<double()> get;
|
||||
std::function<void( double )> set;
|
||||
};
|
||||
|
||||
struct ActionSetting {
|
||||
String buttonText;
|
||||
std::function<void()> action;
|
||||
};
|
||||
|
||||
using SettingValue =
|
||||
std::variant<BoolPointerSetting, BoolSetting, ChoiceSetting, EditableChoiceSetting,
|
||||
IntegerSetting, TextSetting, FloatSetting, ActionSetting>;
|
||||
|
||||
struct SettingDefinition {
|
||||
SettingDescriptor descriptor;
|
||||
SettingValue value;
|
||||
bool enabled{ true };
|
||||
};
|
||||
|
||||
struct SettingsCategory {
|
||||
std::string id;
|
||||
String parent;
|
||||
String name;
|
||||
};
|
||||
|
||||
struct SettingsGroup {
|
||||
std::string category;
|
||||
String name;
|
||||
size_t beforeSetting{ 0 };
|
||||
};
|
||||
|
||||
class SettingsModel {
|
||||
public:
|
||||
void clear() {
|
||||
mCategories.clear();
|
||||
mGroups.clear();
|
||||
mSettings.clear();
|
||||
}
|
||||
|
||||
bool addCategory( SettingsCategory category ) {
|
||||
if ( category.id.empty() ||
|
||||
std::any_of( mCategories.begin(), mCategories.end(),
|
||||
[&category]( const auto& item ) { return item.id == category.id; } ) )
|
||||
return false;
|
||||
mCategories.emplace_back( std::move( category ) );
|
||||
return true;
|
||||
}
|
||||
|
||||
bool addGroup( SettingsGroup group ) {
|
||||
if ( !hasCategory( group.category ) )
|
||||
return false;
|
||||
mGroups.emplace_back( std::move( group ) );
|
||||
return true;
|
||||
}
|
||||
|
||||
bool addSetting( SettingDefinition setting ) {
|
||||
if ( setting.descriptor.id.empty() || !hasCategory( setting.descriptor.category ) ||
|
||||
std::any_of( mSettings.begin(), mSettings.end(), [&setting]( const auto& item ) {
|
||||
return item.descriptor.id == setting.descriptor.id;
|
||||
} ) )
|
||||
return false;
|
||||
mSettings.emplace_back( std::move( setting ) );
|
||||
return true;
|
||||
}
|
||||
|
||||
bool hasCategory( const std::string& id ) const {
|
||||
return std::any_of( mCategories.begin(), mCategories.end(),
|
||||
[&id]( const auto& item ) { return item.id == id; } );
|
||||
}
|
||||
|
||||
const std::vector<SettingsCategory>& categories() const { return mCategories; }
|
||||
|
||||
const std::vector<SettingsGroup>& groups() const { return mGroups; }
|
||||
|
||||
std::vector<SettingDefinition>& settings() { return mSettings; }
|
||||
|
||||
const std::vector<SettingDefinition>& settings() const { return mSettings; }
|
||||
|
||||
private:
|
||||
std::vector<SettingsCategory> mCategories;
|
||||
std::vector<SettingsGroup> mGroups;
|
||||
std::vector<SettingDefinition> mSettings;
|
||||
};
|
||||
|
||||
} // namespace ecode
|
||||
|
||||
#endif
|
||||
@@ -2,7 +2,12 @@
|
||||
#define ECODE_SETTINGSPAGE_HPP
|
||||
|
||||
#include "settingsdocument.hpp"
|
||||
#include "settingsmodel.hpp"
|
||||
|
||||
#include <eepp/ui/tools/uisettingspanel.hpp>
|
||||
|
||||
using namespace EE;
|
||||
using namespace EE::System;
|
||||
using namespace EE::UI::Tools;
|
||||
|
||||
namespace ecode {
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,11 +1,9 @@
|
||||
#ifndef ECODE_SETTINGSPANEL_HPP
|
||||
#define ECODE_SETTINGSPANEL_HPP
|
||||
|
||||
#include "settingsmodel.hpp"
|
||||
#include <eepp/ee.hpp>
|
||||
#include <eepp/scene/mainthreadlifetime.hpp>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
namespace ecode {
|
||||
@@ -22,39 +20,11 @@ class SettingsPanel {
|
||||
void show( Scope scope, const std::string& category = {} );
|
||||
|
||||
protected:
|
||||
struct SettingView {
|
||||
UIWidget* row{ nullptr };
|
||||
};
|
||||
struct SubcategoryHeading {
|
||||
std::string category;
|
||||
String name;
|
||||
UITextView* heading{ nullptr };
|
||||
};
|
||||
|
||||
struct PanelState {
|
||||
UIWindow* window{ nullptr };
|
||||
UISettingsPanel* panel{ nullptr };
|
||||
EventConnectionList connections;
|
||||
UITextInput* search{ nullptr };
|
||||
UITreeView* categories{ nullptr };
|
||||
UIScrollView* scroll{ nullptr };
|
||||
UILinearLayout* settings{ nullptr };
|
||||
UITextView* pageTitle{ nullptr };
|
||||
std::shared_ptr<Model> categoryModel;
|
||||
UIBindingGroup bindingGroup;
|
||||
SettingsModel model;
|
||||
std::vector<std::shared_ptr<SettingsDocument>> documents;
|
||||
std::vector<std::pair<std::string, std::vector<std::string>>> categoryItems;
|
||||
UnorderedMap<std::string, std::string> categoryIds;
|
||||
UnorderedMap<std::string, String> categorySearchText;
|
||||
UnorderedMap<std::string, String> categoryTitles;
|
||||
UnorderedMap<std::string, UITextView*> categoryHeadings;
|
||||
UnorderedMap<std::string, UIWidget*> categorySections;
|
||||
UnorderedMap<std::string, UILinearLayout*> categoryContainers;
|
||||
UnorderedSet<std::string> materializedCategories;
|
||||
std::vector<SubcategoryHeading> subcategoryHeadings;
|
||||
std::vector<SettingView> settingViews;
|
||||
std::string selectedCategory;
|
||||
std::string categoryFilter;
|
||||
|
||||
void reset();
|
||||
};
|
||||
@@ -76,13 +46,9 @@ class SettingsPanel {
|
||||
|
||||
void addProjectSettings( PanelState& state );
|
||||
|
||||
void addCategory( PanelState& state, const std::string& id, const String& parent,
|
||||
const String& name );
|
||||
void addCategory( PanelState& state, std::string id, String parent, String name );
|
||||
|
||||
void addSubcategoryHeading( PanelState& state, const std::string& category,
|
||||
const String& name );
|
||||
|
||||
void setupCategories( PanelState& state );
|
||||
void addSubcategoryHeading( PanelState& state, std::string category, String name );
|
||||
|
||||
void addBool( PanelState& state, SettingDescriptor binding, bool* value,
|
||||
std::function<void( bool )> apply = {} );
|
||||
@@ -110,22 +76,11 @@ class SettingsPanel {
|
||||
|
||||
void addAction( PanelState& state, SettingDescriptor binding, const String& buttonText,
|
||||
std::function<void()> action );
|
||||
|
||||
void refreshTextSetting( PanelState& state, const std::string& id );
|
||||
|
||||
UIWidget* createRow( PanelState& state, SettingDefinition& setting, SettingView& view,
|
||||
pugi::xml_node layout );
|
||||
|
||||
UICheckBox* createBoolControl( PanelState& state, SettingDefinition& setting,
|
||||
SettingView& view );
|
||||
|
||||
void materializeCategory( PanelState& state, const std::string& category );
|
||||
|
||||
void materializeVisibleSettings( PanelState& state, const String& query );
|
||||
|
||||
void setCategoryEnabled( PanelState& state, const std::string& category, bool enabled,
|
||||
const std::string& excludedSetting = {} );
|
||||
|
||||
void filter( PanelState& state );
|
||||
};
|
||||
|
||||
} // namespace ecode
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
#include "appconfig.hpp"
|
||||
#include <eepp/system/filesystem.hpp>
|
||||
#include <eepp/window/window.hpp>
|
||||
|
||||
using namespace EE::Graphics;
|
||||
using namespace EE::System;
|
||||
using namespace EE::UI;
|
||||
using namespace eterm::Terminal;
|
||||
using EE::eemax;
|
||||
|
||||
namespace eterm {
|
||||
|
||||
AppConfig::AppConfig( std::string configPath ) : mConfigPath( std::move( configPath ) ) {
|
||||
FileSystem::dirAddSlashAtEnd( mConfigPath );
|
||||
mIni.path( mConfigPath + "eterm.ini" );
|
||||
mState.path( mConfigPath + "eterm.state.ini" );
|
||||
}
|
||||
|
||||
void AppConfig::load() {
|
||||
mIni.readFile();
|
||||
mState.readFile();
|
||||
terminal.shell = mIni.getValue( "terminal", "shell", terminal.shell );
|
||||
terminal.shellArguments =
|
||||
mIni.getValue( "terminal", "shell_arguments", terminal.shellArguments );
|
||||
terminal.workingDirectory =
|
||||
mIni.getValue( "terminal", "working_directory", terminal.workingDirectory );
|
||||
terminal.executeInShell =
|
||||
mIni.getValue( "terminal", "execute_in_shell", terminal.executeInShell );
|
||||
terminal.historySize = mIni.getValueU( "terminal", "scrollback", terminal.historySize );
|
||||
terminal.cursorStyle = TerminalCursorHelper::modeFromString( mIni.getValue(
|
||||
"terminal", "cursor_style", TerminalCursorHelper::modeToString( terminal.cursorStyle ) ) );
|
||||
terminal.useFrameBuffer = mIni.getValueB( "terminal", "framebuffer", terminal.useFrameBuffer );
|
||||
terminal.closeOnExit = mIni.getValueB( "terminal", "close_on_exit", terminal.closeOnExit );
|
||||
terminal.initialTabs = mIni.getValueU( "terminal", "initial_tabs", terminal.initialTabs );
|
||||
terminal.exclusiveMode = mIni.getValueB( "terminal", "exclusive_mode", terminal.exclusiveMode );
|
||||
const auto newTerminalBehavior =
|
||||
mIni.getValue( "terminal", "new_terminal_behavior", "current" );
|
||||
terminal.newTerminalBehavior =
|
||||
newTerminalBehavior == "vertical" ? NewTerminalBehavior::VerticalSplit
|
||||
: newTerminalBehavior == "horizontal" ? NewTerminalBehavior::HorizontalSplit
|
||||
: NewTerminalBehavior::CurrentTabBar;
|
||||
terminal.scrollBarType = mIni.getValue( "terminal", "scrollbar_type", "overlay" ) == "outside"
|
||||
? ScrollViewType::Outside
|
||||
: ScrollViewType::Overlay;
|
||||
const auto scrollBarMode = mIni.getValue( "terminal", "scrollbar_mode", "auto" );
|
||||
terminal.scrollBarMode = scrollBarMode == "always_on" ? ScrollBarMode::AlwaysOn
|
||||
: scrollBarMode == "always_off" ? ScrollBarMode::AlwaysOff
|
||||
: ScrollBarMode::Auto;
|
||||
font.path = mIni.getValue( "font", "path", font.path );
|
||||
font.fallbackPath = mIni.getValue( "font", "fallback_path", font.fallbackPath );
|
||||
font.size = mIni.getValueF( "font", "size", font.size );
|
||||
font.hinting = Font::fontHintingFromString(
|
||||
mIni.getValue( "font", "hinting", Font::fontHintingToString( font.hinting ) ) );
|
||||
font.antialiasing = Font::fontAntialiasingFromString( mIni.getValue(
|
||||
"font", "antialiasing", Font::fontAntialiasingToString( font.antialiasing ) ) );
|
||||
font.uiPath = mIni.getValue( "font", "ui_path", font.uiPath );
|
||||
font.uiSize = mIni.getValueF( "font", "ui_size", font.uiSize );
|
||||
|
||||
window.pixelDensity = mIni.getValueF( "window", "pixel_density", window.pixelDensity );
|
||||
window.maxFPS = mIni.getValueU( "window", "frame_rate_limit", window.maxFPS );
|
||||
window.vsync = mIni.getValueB( "window", "vsync", window.vsync );
|
||||
window.benchmarkMode = mIni.getValueB( "window", "benchmark_mode", window.benchmarkMode );
|
||||
window.warnBeforeClose =
|
||||
mIni.getValueB( "window", "warn_before_closing", window.warnBeforeClose );
|
||||
window.alwaysShowTabBar =
|
||||
mIni.getValueB( "window", "always_show_tab_bar", window.alwaysShowTabBar );
|
||||
window.rendererVersion = Renderer::glVersionFromString(
|
||||
mIni.getValue( "window", "renderer_version",
|
||||
Renderer::graphicsLibraryVersionToString( window.rendererVersion ) ) );
|
||||
window.multisamples = mIni.getValueU( "window", "multisamples", window.multisamples );
|
||||
theme.colorScheme = mIni.getValue( "theme", "color_scheme", theme.colorScheme );
|
||||
theme.uiColorScheme = ColorSchemePreferences::fromStringExt( mIni.getValue(
|
||||
"theme", "ui_color_scheme", ColorSchemePreferences::toString( theme.uiColorScheme ) ) );
|
||||
|
||||
windowState.size.setWidth( mState.getValueI( "window", "width", windowState.size.getWidth() ) );
|
||||
windowState.size.setHeight(
|
||||
mState.getValueI( "window", "height", windowState.size.getHeight() ) );
|
||||
windowState.position.x = mState.getValueI( "window", "x", windowState.position.x );
|
||||
windowState.position.y = mState.getValueI( "window", "y", windowState.position.y );
|
||||
windowState.displayIndex =
|
||||
mState.getValueI( "window", "display_index", windowState.displayIndex );
|
||||
windowState.maximized = mState.getValueB( "window", "maximized", windowState.maximized );
|
||||
}
|
||||
|
||||
bool AppConfig::save() {
|
||||
if ( !FileSystem::isDirectory( mConfigPath ) && !FileSystem::makeDir( mConfigPath, true ) )
|
||||
return false;
|
||||
return savePreferences() && saveWindowState();
|
||||
}
|
||||
|
||||
bool AppConfig::savePreferences() {
|
||||
if ( !FileSystem::isDirectory( mConfigPath ) && !FileSystem::makeDir( mConfigPath, true ) )
|
||||
return false;
|
||||
mIni.deleteValue( "theme", "language" );
|
||||
mIni.deleteValue( "window", "max_fps" );
|
||||
mIni.setValue( "terminal", "shell", terminal.shell );
|
||||
mIni.setValue( "terminal", "shell_arguments", terminal.shellArguments );
|
||||
mIni.setValue( "terminal", "working_directory", terminal.workingDirectory );
|
||||
mIni.setValue( "terminal", "execute_in_shell", terminal.executeInShell );
|
||||
mIni.setValueU( "terminal", "scrollback", terminal.historySize );
|
||||
mIni.setValue( "terminal", "cursor_style",
|
||||
TerminalCursorHelper::modeToString( terminal.cursorStyle ) );
|
||||
mIni.setValueB( "terminal", "framebuffer", terminal.useFrameBuffer );
|
||||
mIni.setValueB( "terminal", "close_on_exit", terminal.closeOnExit );
|
||||
mIni.setValueU( "terminal", "initial_tabs", terminal.initialTabs );
|
||||
mIni.setValueB( "terminal", "exclusive_mode", terminal.exclusiveMode );
|
||||
mIni.setValue( "terminal", "new_terminal_behavior",
|
||||
terminal.newTerminalBehavior == NewTerminalBehavior::VerticalSplit ? "vertical"
|
||||
: terminal.newTerminalBehavior == NewTerminalBehavior::HorizontalSplit
|
||||
? "horizontal"
|
||||
: "current" );
|
||||
mIni.setValue( "terminal", "scrollbar_type",
|
||||
terminal.scrollBarType == ScrollViewType::Overlay ? "overlay" : "outside" );
|
||||
mIni.setValue( "terminal", "scrollbar_mode",
|
||||
terminal.scrollBarMode == ScrollBarMode::AlwaysOn ? "always_on"
|
||||
: terminal.scrollBarMode == ScrollBarMode::AlwaysOff ? "always_off"
|
||||
: "auto" );
|
||||
mIni.setValue( "font", "path", font.path );
|
||||
mIni.setValue( "font", "fallback_path", font.fallbackPath );
|
||||
mIni.setValueF( "font", "size", font.size );
|
||||
mIni.setValue( "font", "hinting", Font::fontHintingToString( font.hinting ) );
|
||||
mIni.setValue( "font", "antialiasing", Font::fontAntialiasingToString( font.antialiasing ) );
|
||||
mIni.setValue( "font", "ui_path", font.uiPath );
|
||||
mIni.setValueF( "font", "ui_size", font.uiSize );
|
||||
mIni.setValueF( "window", "pixel_density", window.pixelDensity );
|
||||
mIni.setValueU( "window", "frame_rate_limit", window.maxFPS );
|
||||
mIni.setValueB( "window", "vsync", window.vsync );
|
||||
mIni.setValueB( "window", "benchmark_mode", window.benchmarkMode );
|
||||
mIni.setValueB( "window", "warn_before_closing", window.warnBeforeClose );
|
||||
mIni.setValueB( "window", "always_show_tab_bar", window.alwaysShowTabBar );
|
||||
mIni.setValue( "window", "renderer_version",
|
||||
Renderer::graphicsLibraryVersionToString( window.rendererVersion ) );
|
||||
mIni.setValueU( "window", "multisamples", window.multisamples );
|
||||
mIni.setValue( "theme", "color_scheme", theme.colorScheme );
|
||||
mIni.setValue( "theme", "ui_color_scheme",
|
||||
ColorSchemePreferences::toString( theme.uiColorScheme ) );
|
||||
return mIni.writeFile();
|
||||
}
|
||||
|
||||
bool AppConfig::saveWindowState() {
|
||||
if ( !FileSystem::isDirectory( mConfigPath ) && !FileSystem::makeDir( mConfigPath, true ) )
|
||||
return false;
|
||||
mState.setValueI( "window", "width", windowState.size.getWidth() );
|
||||
mState.setValueI( "window", "height", windowState.size.getHeight() );
|
||||
mState.setValueI( "window", "x", windowState.position.x );
|
||||
mState.setValueI( "window", "y", windowState.position.y );
|
||||
mState.setValueI( "window", "display_index", windowState.displayIndex );
|
||||
mState.setValueB( "window", "maximized", windowState.maximized );
|
||||
return mState.writeFile();
|
||||
}
|
||||
|
||||
void AppConfig::captureWindowState( EE::Window::Window* window ) {
|
||||
if ( !window )
|
||||
return;
|
||||
windowState.size = Sys::getPlatformType() == Sys::PlatformType::macOS
|
||||
? window->getSizeInScreenCoordinates()
|
||||
: window->getLastWindowedSizeInScreenCoordinates();
|
||||
windowState.position = window->getPosition();
|
||||
windowState.position.x = eemax( 0, windowState.position.x );
|
||||
windowState.position.y = eemax( 0, windowState.position.y );
|
||||
windowState.displayIndex = window->getCurrentDisplayIndex();
|
||||
windowState.maximized = window->isMaximized();
|
||||
}
|
||||
|
||||
} // namespace eterm
|
||||
@@ -0,0 +1,103 @@
|
||||
#ifndef ETERM_CONFIG_HPP
|
||||
#define ETERM_CONFIG_HPP
|
||||
|
||||
#include <eepp/graphics/font.hpp>
|
||||
#include <eepp/graphics/renderer/renderer.hpp>
|
||||
#include <eepp/math/size.hpp>
|
||||
#include <eepp/math/vector2.hpp>
|
||||
#include <eepp/system/inifile.hpp>
|
||||
#include <eepp/ui/colorschemepreferences.hpp>
|
||||
#include <eepp/ui/uiscrollview.hpp>
|
||||
#include <eepp/window/window.hpp>
|
||||
#include <eterm/terminal/terminaltypes.hpp>
|
||||
#include <string>
|
||||
|
||||
namespace eterm {
|
||||
|
||||
using namespace EE;
|
||||
using namespace EE::Graphics;
|
||||
using namespace EE::Math;
|
||||
using namespace EE::System;
|
||||
using namespace EE::UI;
|
||||
|
||||
enum class NewTerminalBehavior { CurrentTabBar, VerticalSplit, HorizontalSplit };
|
||||
|
||||
struct TerminalConfig {
|
||||
std::string shell;
|
||||
std::string shellArguments;
|
||||
std::string workingDirectory;
|
||||
std::string executeInShell;
|
||||
size_t historySize{ 10000 };
|
||||
size_t initialTabs{ 1 };
|
||||
Terminal::TerminalCursorMode cursorStyle{ Terminal::TerminalCursorMode::SteadyUnderline };
|
||||
bool useFrameBuffer{ false };
|
||||
bool closeOnExit{ false };
|
||||
bool exclusiveMode{ false };
|
||||
NewTerminalBehavior newTerminalBehavior{ NewTerminalBehavior::CurrentTabBar };
|
||||
ScrollViewType scrollBarType{ ScrollViewType::Overlay };
|
||||
ScrollBarMode scrollBarMode{ ScrollBarMode::Auto };
|
||||
};
|
||||
|
||||
struct FontConfig {
|
||||
std::string path;
|
||||
std::string fallbackPath;
|
||||
FontHinting hinting{ FontHinting::Full };
|
||||
FontAntialiasing antialiasing{ FontAntialiasing::Grayscale };
|
||||
Float size{ 11 };
|
||||
std::string uiPath;
|
||||
Float uiSize{ 11 };
|
||||
};
|
||||
|
||||
struct WindowConfig {
|
||||
Float pixelDensity{ 0 };
|
||||
Uint32 maxFPS{ static_cast<Uint32>( ContextSettings::FrameRateLimitScreenRefreshRate ) };
|
||||
bool vsync{ false };
|
||||
bool benchmarkMode{ false };
|
||||
bool warnBeforeClose{ false };
|
||||
bool alwaysShowTabBar{ false };
|
||||
GraphicsLibraryVersion rendererVersion{ GLv_default };
|
||||
Uint32 multisamples{ 0 };
|
||||
};
|
||||
|
||||
struct ThemeConfig {
|
||||
std::string colorScheme;
|
||||
ColorSchemeExtPreference uiColorScheme{ ColorSchemeExtPreference::System };
|
||||
};
|
||||
|
||||
struct WindowStateConfig {
|
||||
Sizei size{ 1280, 720 };
|
||||
Vector2i position{ -1, -1 };
|
||||
int displayIndex{ 0 };
|
||||
bool maximized{ false };
|
||||
};
|
||||
|
||||
struct AppConfig {
|
||||
TerminalConfig terminal;
|
||||
FontConfig font;
|
||||
WindowConfig window;
|
||||
ThemeConfig theme;
|
||||
WindowStateConfig windowState;
|
||||
|
||||
explicit AppConfig( std::string configPath );
|
||||
|
||||
void load();
|
||||
|
||||
bool save();
|
||||
|
||||
bool savePreferences();
|
||||
|
||||
bool saveWindowState();
|
||||
|
||||
void captureWindowState( EE::Window::Window* window );
|
||||
|
||||
const std::string& getConfigPath() const { return mConfigPath; }
|
||||
|
||||
private:
|
||||
std::string mConfigPath;
|
||||
IniFile mIni;
|
||||
IniFile mState;
|
||||
};
|
||||
|
||||
} // namespace eterm
|
||||
|
||||
#endif
|
||||
+273
-180
@@ -1,116 +1,21 @@
|
||||
#include <args/args.hxx>
|
||||
#include <eepp/core/small_vector.hpp>
|
||||
#include <eepp/ee.hpp>
|
||||
#include <eepp/ui/iconmanager.hpp>
|
||||
#include <eepp/ui/tools/uitabwidgetsplitter.hpp>
|
||||
#include <eepp/ui/tools/uiwidgetinspector.hpp>
|
||||
#include <eepp/ui/uiapplication.hpp>
|
||||
#include <eepp/ui/uilinearlayout.hpp>
|
||||
#include <eepp/ui/uimessagebox.hpp>
|
||||
#include <eterm/ui/uiterminal.hpp>
|
||||
#include "eterm.hpp"
|
||||
#include "settingspanel.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <iostream>
|
||||
#include <iterator>
|
||||
#include <map>
|
||||
#include <unordered_map>
|
||||
|
||||
using namespace EE;
|
||||
using namespace EE::Graphics;
|
||||
using namespace EE::Scene;
|
||||
using namespace EE::System;
|
||||
using namespace EE::UI;
|
||||
using namespace EE::UI::Tools;
|
||||
using namespace EE::Window;
|
||||
using namespace eterm::Terminal;
|
||||
using namespace eterm::UI;
|
||||
namespace eterm {
|
||||
|
||||
namespace {
|
||||
|
||||
struct TerminalLaunchConfig {
|
||||
std::string program;
|
||||
std::vector<std::string> arguments;
|
||||
std::string workingDirectory;
|
||||
std::string executeInShell;
|
||||
size_t historySize{ 10000 };
|
||||
TerminalCursorMode cursorStyle{ TerminalCursorMode::SteadyUnderline };
|
||||
FontHinting fontHinting{ FontHinting::Full };
|
||||
FontAntialiasing fontAntialiasing{ FontAntialiasing::Grayscale };
|
||||
bool useFrameBuffer{ false };
|
||||
bool keepAlive{ true };
|
||||
bool closeOnExit{ false };
|
||||
};
|
||||
|
||||
class EtermApp {
|
||||
public:
|
||||
int run( int argc, char* argv[] );
|
||||
|
||||
class TerminalSplitterClient : public UITabWidgetSplitter::Client {
|
||||
public:
|
||||
explicit TerminalSplitterClient( EtermApp& app ) : mApp( app ) {}
|
||||
|
||||
void onTabCreated( UITab* tab, UIWidget* ) override;
|
||||
void onWidgetFocusChange( UIWidget* ) override;
|
||||
|
||||
private:
|
||||
EtermApp& mApp;
|
||||
};
|
||||
|
||||
private:
|
||||
std::string getResourcePath() const;
|
||||
String i18n( const std::string& key, const String& defaultValue ) const;
|
||||
void loadColorSchemes( const std::string& resPath );
|
||||
static UITerminal* terminalFromTab( UITab* tab );
|
||||
void updateWindowTitle();
|
||||
bool hasTerminals() const;
|
||||
static bool hasRunningChildren( UITab* tab );
|
||||
void closeTab( UITab* tab );
|
||||
void queueExitCloseTab( UITab* tab );
|
||||
void queueExitedTabs();
|
||||
void requestCloseTab( UITab* tab );
|
||||
void renameSession( UITerminal* terminal );
|
||||
void maximizeTabWidget( UITabWidget* tabWidget );
|
||||
void restoreMaximizedTabWidget();
|
||||
void configureTab( UITab* tab );
|
||||
UITerminal* createTerminal( UITabWidget* target = nullptr );
|
||||
UITerminal* createTerminalSplit( SplitDirection direction, UITerminal* terminal );
|
||||
void addTabKeyBindings( UITerminal* terminal );
|
||||
bool closeWindow( EE::Window::Window* );
|
||||
|
||||
EE::Window::Window* appWindow{ nullptr };
|
||||
UISceneNode* scene{ nullptr };
|
||||
UILinearLayout* mainLayout{ nullptr };
|
||||
UITabWidgetSplitter* tabSplitter{ nullptr };
|
||||
FontTrueType* terminalFont{ nullptr };
|
||||
UIIcon* terminalIcon{ nullptr };
|
||||
UIMessageBox* closeDialog{ nullptr };
|
||||
UIWidget* closeDialogWidget{ nullptr };
|
||||
UIWindow* maximizedTabWidgetWindow{ nullptr };
|
||||
UITabWidget* maximizedTabWidget{ nullptr };
|
||||
UINodeLink* maximizedTabWidgetLink{ nullptr };
|
||||
TerminalLaunchConfig terminalConfig;
|
||||
std::map<std::string, TerminalColorScheme> terminalColorSchemes;
|
||||
const TerminalColorScheme* selectedColorScheme{ nullptr };
|
||||
Float terminalFontSize{ 12 };
|
||||
bool warnBeforeClose{ false };
|
||||
bool closeApproved{ false };
|
||||
bool benchmarkMode{ false };
|
||||
Clock secondsCounter;
|
||||
SmallVector<UITab*, 8> pendingExitCloseTabs;
|
||||
TerminalSplitterClient splitterClient{ *this };
|
||||
};
|
||||
|
||||
void EtermApp::TerminalSplitterClient::onTabCreated( UITab* tab, UIWidget* ) {
|
||||
void App::TerminalSplitterClient::onTabCreated( UITab* tab, UIWidget* ) {
|
||||
if ( mApp.terminalIcon )
|
||||
tab->setIcon( mApp.terminalIcon->createDrawable( PixelDensity::dpToPxI( 12 ) ) );
|
||||
mApp.configureTab( tab );
|
||||
}
|
||||
|
||||
void EtermApp::TerminalSplitterClient::onWidgetFocusChange( UIWidget* ) {
|
||||
void App::TerminalSplitterClient::onWidgetFocusChange( UIWidget* ) {
|
||||
mApp.updateWindowTitle();
|
||||
}
|
||||
|
||||
std::string EtermApp::getResourcePath() const {
|
||||
std::string App::getResourcePath() const {
|
||||
std::string resPath = Sys::getProcessPath();
|
||||
#if EE_PLATFORM == EE_PLATFORM_MACOS
|
||||
if ( String::contains( resPath, "ecode.app" ) ) {
|
||||
@@ -130,11 +35,11 @@ std::string EtermApp::getResourcePath() const {
|
||||
return resPath;
|
||||
}
|
||||
|
||||
String EtermApp::i18n( const std::string& key, const String& defaultValue ) const {
|
||||
String App::i18n( const std::string& key, const String& defaultValue ) const {
|
||||
return scene ? scene->i18n( key, defaultValue ) : defaultValue;
|
||||
}
|
||||
|
||||
void EtermApp::loadColorSchemes( const std::string& resPath ) {
|
||||
void App::loadColorSchemes( const std::string& resPath ) {
|
||||
auto colorSchemes =
|
||||
TerminalColorScheme::loadFromFile( resPath + "colorschemes/terminalcolorschemes.conf" );
|
||||
const std::string configColorSchemesPath =
|
||||
@@ -153,13 +58,106 @@ void EtermApp::loadColorSchemes( const std::string& resPath ) {
|
||||
}
|
||||
}
|
||||
|
||||
UITerminal* EtermApp::terminalFromTab( UITab* tab ) {
|
||||
UITerminal* App::terminalFromTab( UITab* tab ) {
|
||||
return tab && tab->getOwnedWidget() && tab->getOwnedWidget()->isType( UI_TYPE_TERMINAL )
|
||||
? tab->getOwnedWidget()->asType<UITerminal>()
|
||||
: nullptr;
|
||||
}
|
||||
|
||||
void EtermApp::updateWindowTitle() {
|
||||
void App::savePreferences() {
|
||||
if ( config && !config->savePreferences() )
|
||||
Log::error( "Could not save eterm configuration to %s", config->getConfigPath() );
|
||||
}
|
||||
|
||||
void App::forEachTerminal( const std::function<void( UITerminal* )>& fn ) {
|
||||
if ( !tabSplitter )
|
||||
return;
|
||||
tabSplitter->forEachWidgetType(
|
||||
UI_TYPE_TERMINAL, [&fn]( UIWidget* widget ) { fn( widget->asType<UITerminal>() ); } );
|
||||
}
|
||||
|
||||
void App::createNewTerminal() {
|
||||
auto* current = tabSplitter ? tabSplitter->getCurWidget() : nullptr;
|
||||
auto* terminal =
|
||||
current && current->isType( UI_TYPE_TERMINAL ) ? current->asType<UITerminal>() : nullptr;
|
||||
switch ( config->terminal.newTerminalBehavior ) {
|
||||
case NewTerminalBehavior::VerticalSplit:
|
||||
createTerminalSplit( SplitDirection::Right, terminal );
|
||||
break;
|
||||
case NewTerminalBehavior::HorizontalSplit:
|
||||
createTerminalSplit( SplitDirection::Bottom, terminal );
|
||||
break;
|
||||
default:
|
||||
createTerminal();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void App::openFontPicker( bool uiFont, bool fallbackFont ) {
|
||||
const Uint32 flags = UIFontPickerDialog::ShowStyle |
|
||||
( fallbackFont ? 0 : UIFontPickerDialog::ShowSize ) |
|
||||
( !uiFont && !fallbackFont ? UIFontPickerDialog::MonospaceOnly : 0 );
|
||||
auto* dialog = UIFontPickerDialog::New( flags );
|
||||
dialog->setTitle( i18n( "select_font", "Select Font" ) );
|
||||
dialog->setCloseShortcut( KEY_ESCAPE );
|
||||
std::string currentPath = uiFont ? config->font.uiPath
|
||||
: fallbackFont ? config->font.fallbackPath
|
||||
: config->font.path;
|
||||
if ( !currentPath.empty() )
|
||||
dialog->setSelectedFont( currentPath );
|
||||
if ( !fallbackFont ) {
|
||||
auto selection = dialog->getSelection();
|
||||
selection.size = static_cast<Uint32>( uiFont ? config->font.uiSize : config->font.size );
|
||||
dialog->setSelection( selection );
|
||||
}
|
||||
dialog->setOnFontPicked( [this, uiFont, fallbackFont]( const UIFontSelection& selection ) {
|
||||
if ( selection.font.path.empty() )
|
||||
return;
|
||||
auto& resourceScope = *scene->getResourceScope();
|
||||
if ( uiFont ) {
|
||||
auto font = FontTrueType::New( "eterm-ui-font", resourceScope );
|
||||
if ( font->loadFromFile( selection.font.path ) ) {
|
||||
config->font.uiPath = selection.font.path;
|
||||
config->font.uiSize = selection.size;
|
||||
scene->getUIThemeManager()->setDefaultFont( font.get() );
|
||||
scene->getUIThemeManager()->setDefaultFontSize( config->font.uiSize );
|
||||
scene->getRoot()->reloadStyle( true, true, true, true, true );
|
||||
}
|
||||
} else if ( fallbackFont ) {
|
||||
auto font = FontTrueType::New( "eterm-fallback-font", resourceScope );
|
||||
if ( font->loadFromFile( selection.font.path ) ) {
|
||||
config->font.fallbackPath = selection.font.path;
|
||||
resourceScope.getFontService().addFallbackFont( std::move( font ) );
|
||||
}
|
||||
} else {
|
||||
auto font = FontTrueType::New( "eterm-monospace", resourceScope );
|
||||
if ( font->loadFromFile( selection.font.path ) ) {
|
||||
config->font.path = selection.font.path;
|
||||
config->font.size = selection.size;
|
||||
terminalFont = font.get();
|
||||
terminalFontSize = PixelDensity::dpToPx( config->font.size );
|
||||
FontFamily::loadFromRegular( terminalFont );
|
||||
forEachTerminal( [this]( UITerminal* terminal ) {
|
||||
terminal->setFont( terminalFont );
|
||||
terminal->setFontSize( terminalFontSize );
|
||||
} );
|
||||
}
|
||||
}
|
||||
savePreferences();
|
||||
} );
|
||||
dialog->show();
|
||||
}
|
||||
|
||||
void App::showSettings() {
|
||||
if ( settingsWindow ) {
|
||||
settingsWindow->show();
|
||||
settingsWindow->toFront();
|
||||
return;
|
||||
}
|
||||
settingsWindow = eterm::SettingsPanel::create( *this );
|
||||
}
|
||||
|
||||
void App::updateWindowTitle() {
|
||||
if ( !appWindow )
|
||||
return;
|
||||
String title{ "eterm" };
|
||||
@@ -181,7 +179,7 @@ void EtermApp::updateWindowTitle() {
|
||||
appWindow->setTitle( title );
|
||||
}
|
||||
|
||||
bool EtermApp::hasTerminals() const {
|
||||
bool App::hasTerminals() const {
|
||||
bool found = false;
|
||||
if ( tabSplitter )
|
||||
tabSplitter->forEachWidgetStoppable( [&found]( UIWidget* ) {
|
||||
@@ -191,27 +189,27 @@ bool EtermApp::hasTerminals() const {
|
||||
return found;
|
||||
}
|
||||
|
||||
bool EtermApp::hasRunningChildren( UITab* tab ) {
|
||||
bool App::hasRunningChildren( UITab* tab ) {
|
||||
auto* terminal = terminalFromTab( tab );
|
||||
return terminal && terminal->getTerm() &&
|
||||
Sys::processHasChildren( terminal->getTerm()->getProcessId() );
|
||||
}
|
||||
|
||||
void EtermApp::closeTab( UITab* tab ) {
|
||||
void App::closeTab( UITab* tab ) {
|
||||
if ( !tabSplitter || !tab || !tab->getOwnedWidget() )
|
||||
return;
|
||||
tabSplitter->closeTab( tab->getOwnedWidget()->asType<UIWidget>(),
|
||||
UITabWidget::FocusTabBehavior::Default );
|
||||
}
|
||||
|
||||
void EtermApp::queueExitCloseTab( UITab* tab ) {
|
||||
void App::queueExitCloseTab( UITab* tab ) {
|
||||
if ( tab && std::find( pendingExitCloseTabs.begin(), pendingExitCloseTabs.end(), tab ) ==
|
||||
pendingExitCloseTabs.end() ) {
|
||||
pendingExitCloseTabs.emplace_back( tab );
|
||||
}
|
||||
}
|
||||
|
||||
void EtermApp::queueExitedTabs() {
|
||||
void App::queueExitedTabs() {
|
||||
if ( !terminalConfig.closeOnExit )
|
||||
return;
|
||||
tabSplitter->forEachTab( [this]( UITab* tab ) {
|
||||
@@ -225,7 +223,7 @@ void EtermApp::queueExitedTabs() {
|
||||
} );
|
||||
}
|
||||
|
||||
void EtermApp::requestCloseTab( UITab* tab ) {
|
||||
void App::requestCloseTab( UITab* tab ) {
|
||||
if ( !warnBeforeClose || !hasRunningChildren( tab ) ) {
|
||||
closeTab( tab );
|
||||
return;
|
||||
@@ -250,7 +248,7 @@ void EtermApp::requestCloseTab( UITab* tab ) {
|
||||
closeDialog->showWhenReady();
|
||||
}
|
||||
|
||||
void EtermApp::renameSession( UITerminal* terminal ) {
|
||||
void App::renameSession( UITerminal* terminal ) {
|
||||
if ( !terminal )
|
||||
return;
|
||||
auto* msgBox =
|
||||
@@ -266,7 +264,7 @@ void EtermApp::renameSession( UITerminal* terminal ) {
|
||||
msgBox->showWhenReady();
|
||||
}
|
||||
|
||||
void EtermApp::maximizeTabWidget( UITabWidget* tabWidget ) {
|
||||
void App::maximizeTabWidget( UITabWidget* tabWidget ) {
|
||||
if ( !tabWidget || scene->getRoot()->hasChild( "detached_tab_widget_win" ) )
|
||||
return;
|
||||
|
||||
@@ -284,7 +282,7 @@ void EtermApp::maximizeTabWidget( UITabWidget* tabWidget ) {
|
||||
win->setKeyBindingCommand( "close-maximized-tab-widget",
|
||||
[this] { restoreMaximizedTabWidget(); } );
|
||||
win->getKeyBindings().addKeybind( { KEY_ESCAPE }, "close-maximized-tab-widget" );
|
||||
win->setCheckEphemeralCloseFn( [this]( Node* focusNode ) {
|
||||
win->setCheckEphemeralCloseFn( []( Node* focusNode ) {
|
||||
if ( focusNode->isType( UI_TYPE_POPUPMENU ) )
|
||||
return false;
|
||||
if ( focusNode->getSceneNode()->isUISceneNode() ) {
|
||||
@@ -335,7 +333,7 @@ void EtermApp::maximizeTabWidget( UITabWidget* tabWidget ) {
|
||||
} );
|
||||
}
|
||||
|
||||
void EtermApp::restoreMaximizedTabWidget() {
|
||||
void App::restoreMaximizedTabWidget() {
|
||||
if ( !scene || !SceneManager::isActive() )
|
||||
return;
|
||||
if ( !maximizedTabWidgetLink || !maximizedTabWidget )
|
||||
@@ -359,7 +357,7 @@ void EtermApp::restoreMaximizedTabWidget() {
|
||||
win->close();
|
||||
}
|
||||
|
||||
void EtermApp::configureTab( UITab* tab ) {
|
||||
void App::configureTab( UITab* tab ) {
|
||||
tab->on( Event::OnCreateContextMenu, [this]( const Event* event ) {
|
||||
const auto* menuEvent = static_cast<const ContextMenuEvent*>( event );
|
||||
auto* menu = menuEvent->getMenu();
|
||||
@@ -392,6 +390,7 @@ void EtermApp::configureTab( UITab* tab ) {
|
||||
terminal->getExclusiveMode() );
|
||||
exclusiveMode->setId( UITerminal::getExclusiveModeToggleCommandName() );
|
||||
addItem( i18n( "rename_session", "Rename Session" ), "", "terminal-rename" );
|
||||
addItem( i18n( "settings", "Settings" ), "settings", "open-settings" );
|
||||
|
||||
menu->addSeparator();
|
||||
const bool canMove = clickedTab->getTabWidget()->getTabCount() > 1;
|
||||
@@ -427,6 +426,8 @@ void EtermApp::configureTab( UITab* tab ) {
|
||||
maximizeTabWidget( clickedTab->getTabWidget() );
|
||||
} else if ( command == "restore-maximized-tab-widget" ) {
|
||||
restoreMaximizedTabWidget();
|
||||
} else if ( command == "open-settings" ) {
|
||||
showSettings();
|
||||
} else {
|
||||
terminal->execute( command );
|
||||
}
|
||||
@@ -437,7 +438,7 @@ void EtermApp::configureTab( UITab* tab ) {
|
||||
} );
|
||||
}
|
||||
|
||||
UITerminal* EtermApp::createTerminal( UITabWidget* target ) {
|
||||
UITerminal* App::createTerminal( UITabWidget* target ) {
|
||||
if ( !target && tabSplitter ) {
|
||||
auto* current = tabSplitter->getCurWidget();
|
||||
target = current ? tabSplitter->tabWidgetFromWidget( current )
|
||||
@@ -464,6 +465,9 @@ UITerminal* EtermApp::createTerminal( UITabWidget* target ) {
|
||||
terminal->getTerm()->setCursorMode( terminalConfig.cursorStyle );
|
||||
terminal->getTerm()->setFontHinting( terminalConfig.fontHinting );
|
||||
terminal->getTerm()->setFontAntialiasing( terminalConfig.fontAntialiasing );
|
||||
terminal->setExclusiveMode( config->terminal.exclusiveMode );
|
||||
terminal->setScrollViewType( config->terminal.scrollBarType );
|
||||
terminal->setVerticalScrollMode( config->terminal.scrollBarMode );
|
||||
if ( selectedColorScheme )
|
||||
terminal->setColorScheme( *selectedColorScheme );
|
||||
|
||||
@@ -481,6 +485,30 @@ UITerminal* EtermApp::createTerminal( UITabWidget* target ) {
|
||||
if ( terminalConfig.closeOnExit && event.type == TerminalDisplay::EventType::PROCESS_EXIT )
|
||||
queueExitCloseTab( tab );
|
||||
} );
|
||||
terminal->on( Event::OnCreateContextMenu, [this, terminal]( const Event* event ) {
|
||||
auto menu = static_cast<const ContextMenuEvent*>( event )->getMenu();
|
||||
const auto addItem = [this, menu]( const String& text, const std::string& icon,
|
||||
const std::string& command ) {
|
||||
DrawablePtr drawable;
|
||||
if ( auto* menuIcon = scene->findIcon( icon ) )
|
||||
drawable = menuIcon->createDrawable( PixelDensity::dpToPxI( 12 ) );
|
||||
auto* item = menu->add( text, std::move( drawable ) );
|
||||
item->setId( command );
|
||||
return item;
|
||||
};
|
||||
menu->addSeparator();
|
||||
addItem( i18n( "settings", "Settings" ), "settings", "open-settings" );
|
||||
menu->on( Event::OnItemClicked, [this, terminal]( const Event* itemEvent ) {
|
||||
if ( !itemEvent->getNode()->isType( UI_TYPE_MENUITEM ) )
|
||||
return;
|
||||
const auto& command = itemEvent->getNode()->getId();
|
||||
auto* previous = tabSplitter->getCurWidget();
|
||||
tabSplitter->setCurrentWidget( terminal );
|
||||
terminal->execute( command );
|
||||
if ( previous && tabSplitter->ownedWidgetExists( previous ) )
|
||||
tabSplitter->setCurrentWidget( previous );
|
||||
} );
|
||||
} );
|
||||
tabSplitter->setCurrentWidget( terminal );
|
||||
if ( !terminalConfig.executeInShell.empty() )
|
||||
terminal->executeFile( terminalConfig.executeInShell );
|
||||
@@ -489,15 +517,16 @@ UITerminal* EtermApp::createTerminal( UITabWidget* target ) {
|
||||
return terminal;
|
||||
}
|
||||
|
||||
UITerminal* EtermApp::createTerminalSplit( SplitDirection direction, UITerminal* terminal ) {
|
||||
UITerminal* App::createTerminalSplit( SplitDirection direction, UITerminal* terminal ) {
|
||||
auto* source = terminal ? tabSplitter->tabWidgetFromWidget( terminal ) : nullptr;
|
||||
auto* target = source ? tabSplitter->splitTabWidget( direction, source ) : nullptr;
|
||||
return target ? createTerminal( target ) : nullptr;
|
||||
}
|
||||
|
||||
void EtermApp::addTabKeyBindings( UITerminal* terminal ) {
|
||||
void App::addTabKeyBindings( UITerminal* terminal ) {
|
||||
tabSplitter->registerSplitterCommands( *terminal );
|
||||
terminal->setCommand( "create-new-terminal", [this] { createTerminal(); } );
|
||||
terminal->setCommand( "create-new-terminal", [this] { createNewTerminal(); } );
|
||||
terminal->setCommand( "open-settings", [this] { showSettings(); } );
|
||||
terminal->setCommand( "debug-widget-tree-view",
|
||||
[this] { UIWidgetInspector::create( scene ); } );
|
||||
terminal->setCommand( "terminal-rename", [this, terminal] { renameSession( terminal ); } );
|
||||
@@ -528,6 +557,7 @@ void EtermApp::addTabKeyBindings( UITerminal* terminal ) {
|
||||
[this, terminal] { tabSplitter->switchNextSplit( terminal ); } );
|
||||
terminal->addKeyBinding( { KEY_T, KeyMod::getDefaultModifier() | KEYMOD_SHIFT },
|
||||
"create-new-terminal" );
|
||||
terminal->addKeyBinding( { KEY_COMMA, KeyMod::getDefaultModifier() }, "open-settings" );
|
||||
terminal->addKeyBinding( { KEY_W, KeyMod::getDefaultModifier() | KEYMOD_SHIFT }, "close-tab" );
|
||||
terminal->addKeyBinding( { KEY_F11, KeyMod::getDefaultModifier() | KEYMOD_SHIFT },
|
||||
"debug-widget-tree-view" );
|
||||
@@ -551,7 +581,7 @@ void EtermApp::addTabKeyBindings( UITerminal* terminal ) {
|
||||
"switch-to-next-split" );
|
||||
}
|
||||
|
||||
bool EtermApp::closeWindow( EE::Window::Window* ) {
|
||||
bool App::closeWindow( EE::Window::Window* ) {
|
||||
if ( closeApproved || !warnBeforeClose )
|
||||
return true;
|
||||
bool running = false;
|
||||
@@ -578,25 +608,30 @@ bool EtermApp::closeWindow( EE::Window::Window* ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int EtermApp::run( int argc, char* argv[] ) {
|
||||
int App::run( int argc, char* argv[] ) {
|
||||
#ifdef EE_DEBUG
|
||||
Log::instance()->setLogToStdOut( !Runtime::isOffscreen() );
|
||||
Log::instance()->setLiveWrite( true );
|
||||
#endif
|
||||
config = std::make_unique<eterm::AppConfig>( Sys::getConfigPath( "eterm" ) );
|
||||
config->load();
|
||||
args::ArgumentParser parser( "eterm" );
|
||||
args::HelpFlag help( parser, "help", "Display this help menu", { 'h', "help" } );
|
||||
args::ValueFlag<std::string> shell( parser, "shell", "Shell name or path", { 's', "shell" },
|
||||
"" );
|
||||
config->terminal.shell );
|
||||
args::ValueFlag<std::string> shellArgs( parser, "shell-args", "Shell command line arguments",
|
||||
{ "shell-args" }, "" );
|
||||
{ "shell-args" }, config->terminal.shellArguments );
|
||||
args::ValueFlag<size_t> historySize( parser, "scrollback", "Maximum history size (lines)",
|
||||
{ 'l', "scrollback" }, 10000 );
|
||||
{ 'l', "scrollback" }, config->terminal.historySize );
|
||||
args::Flag fb( parser, "framebuffer", "Use frame buffer (more memory usage, less CPU usage)",
|
||||
{ "fb", "framebuffer" } );
|
||||
args::ValueFlag<std::string> fontPath( parser, "fontpath", "Font path", { 'f', "font" } );
|
||||
args::ValueFlag<std::string> fontPath( parser, "fontpath", "Font path", { 'f', "font" },
|
||||
config->font.path );
|
||||
args::ValueFlag<std::string> fallbackFontPath( parser, "fallback-fontpath",
|
||||
"Fallback Font path", { "fallback-font" } );
|
||||
args::ValueFlag<Float> fontSize( parser, "fontsize", "Font size (in dp)", { "fontsize" }, 11 );
|
||||
"Fallback Font path", { "fallback-font" },
|
||||
config->font.fallbackPath );
|
||||
args::ValueFlag<Float> fontSize( parser, "fontsize", "Font size (in dp)", { "fontsize" },
|
||||
config->font.size );
|
||||
const std::unordered_map<std::string, FontHinting> fontHintingMap{
|
||||
{ "none", FontHinting::None },
|
||||
{ "slight", FontHinting::Slight },
|
||||
@@ -604,7 +639,7 @@ int EtermApp::run( int argc, char* argv[] ) {
|
||||
};
|
||||
args::MapFlag<std::string, FontHinting> fontHinting(
|
||||
parser, "font-hinting", "Font hinting mode (accepted values: none, slight, full)",
|
||||
{ "font-hinting" }, fontHintingMap, FontHinting::Full );
|
||||
{ "font-hinting" }, fontHintingMap, config->font.hinting );
|
||||
const std::unordered_map<std::string, FontAntialiasing> fontAntialiasingMap{
|
||||
{ "none", FontAntialiasing::None },
|
||||
{ "grayscale", FontAntialiasing::Grayscale },
|
||||
@@ -613,33 +648,35 @@ int EtermApp::run( int argc, char* argv[] ) {
|
||||
args::MapFlag<std::string, FontAntialiasing> fontAntialiasing(
|
||||
parser, "font-antialiasing",
|
||||
"Font antialiasing mode (accepted values: none, grayscale, subpixel)",
|
||||
{ "font-antialiasing" }, fontAntialiasingMap, FontAntialiasing::Grayscale );
|
||||
args::ValueFlag<Float> width( parser, "winwidth", "Window width (in dp)", { "width" }, 1280 );
|
||||
{ "font-antialiasing" }, fontAntialiasingMap, config->font.antialiasing );
|
||||
args::ValueFlag<Float> width( parser, "winwidth", "Window width (in dp)", { "width" },
|
||||
config->windowState.size.getWidth() );
|
||||
args::ValueFlag<Float> height( parser, "winheight", "Window height (in dp)", { "height" },
|
||||
720 );
|
||||
config->windowState.size.getHeight() );
|
||||
args::ValueFlag<Float> pixelDensity( parser, "pixel-density",
|
||||
"Set default application pixel density",
|
||||
{ 'd', "pixel-density" } );
|
||||
args::Positional<std::string> wd( parser, "wording-dir", "Working Directory / executable" );
|
||||
args::Flag closeOnExit( parser, "close-on-exit",
|
||||
"close the application when the executable exits", { 'c', "close" } );
|
||||
args::ValueFlag<std::string> executeInShell(
|
||||
parser, "execute-in-shell", "execute program in shell", { 'e', "execute" }, "" );
|
||||
args::ValueFlag<std::string> executeInShell( parser, "execute-in-shell",
|
||||
"execute program in shell", { 'e', "execute" },
|
||||
config->terminal.executeInShell );
|
||||
args::Flag vsync( parser, "vsync", "Enable vsync", { "vsync" } );
|
||||
args::ValueFlag<std::string> colorScheme( parser, "color-scheme", "Load color scheme",
|
||||
{ "color-scheme" }, "" );
|
||||
{ "color-scheme" }, config->theme.colorScheme );
|
||||
args::Flag listColorSchemes( parser, "color-schemes", "Lists color schemes",
|
||||
{ "list-color-schemes" } );
|
||||
args::ValueFlag<Uint32> maxFPS( parser, "max-fps",
|
||||
"Maximum rendering frames per second of the terminal. Default "
|
||||
"value will be the refresh rate of the screen.",
|
||||
{ "max-fps" }, 0 );
|
||||
{ "max-fps" }, config->window.maxFPS );
|
||||
args::MapFlag<std::string, TerminalCursorMode> cursorStyle(
|
||||
parser, "cursor-style",
|
||||
"Sets the cursor-style (accepted values: blinking_block, steady_block, blink_underline, "
|
||||
"steady_underline, blink_bar, steady_bar)",
|
||||
{ "cursor-style" }, TerminalCursorHelper::getTerminalCursorModeMap(),
|
||||
TerminalCursorMode::SteadyUnderline );
|
||||
config->terminal.cursorStyle );
|
||||
args::Flag benchmarkModeFlag(
|
||||
parser, "benchmark-mode",
|
||||
"Render as much as possible to measure the rendering performance.", { "benchmark-mode" } );
|
||||
@@ -651,7 +688,7 @@ int EtermApp::run( int argc, char* argv[] ) {
|
||||
"Always show the tab bar, even with a single tab.",
|
||||
{ "always-show-tab-bar" } );
|
||||
args::ValueFlag<size_t> initialTabs( parser, "tabs", "Number of initial terminal tabs",
|
||||
{ "tabs" }, 1 );
|
||||
{ "tabs" }, config->terminal.initialTabs );
|
||||
|
||||
try {
|
||||
parser.ParseCLI( argc, argv );
|
||||
@@ -667,56 +704,105 @@ int EtermApp::run( int argc, char* argv[] ) {
|
||||
std::cerr << parser;
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
if ( !config->savePreferences() )
|
||||
Log::error( "Could not save eterm configuration to %s", config->getConfigPath() );
|
||||
|
||||
config->terminal.shell = shell.Get();
|
||||
config->terminal.shellArguments = shellArgs.Get();
|
||||
config->terminal.historySize = historySize.Get();
|
||||
config->font.path = fontPath.Get();
|
||||
config->font.fallbackPath = fallbackFontPath.Get();
|
||||
config->font.size = fontSize.Get();
|
||||
config->font.hinting = fontHinting.Get();
|
||||
config->font.antialiasing = fontAntialiasing.Get();
|
||||
config->windowState.size = { static_cast<int>( width.Get() ),
|
||||
static_cast<int>( height.Get() ) };
|
||||
if ( pixelDensity )
|
||||
config->window.pixelDensity = pixelDensity.Get();
|
||||
if ( wd )
|
||||
config->terminal.workingDirectory = wd.Get();
|
||||
config->terminal.executeInShell = executeInShell.Get();
|
||||
config->theme.colorScheme = colorScheme.Get();
|
||||
config->window.maxFPS = maxFPS.Get();
|
||||
config->terminal.cursorStyle = cursorStyle.Get();
|
||||
config->terminal.initialTabs = initialTabs.Get();
|
||||
config->terminal.useFrameBuffer |= fb.Get();
|
||||
config->terminal.closeOnExit |= closeOnExit.Get();
|
||||
config->window.vsync |= vsync.Get();
|
||||
config->window.benchmarkMode |= benchmarkModeFlag.Get();
|
||||
config->window.warnBeforeClose |= warnBeforeCloseFlag.Get();
|
||||
config->window.alwaysShowTabBar |= alwaysShowTabBar.Get();
|
||||
|
||||
const std::string initialWorkingDirectory = FileSystem::getCurrentWorkingDirectory();
|
||||
const std::string resPath = getResourcePath();
|
||||
if ( listColorSchemes.Get() || colorScheme )
|
||||
loadColorSchemes( resPath );
|
||||
loadColorSchemes( resPath );
|
||||
if ( listColorSchemes.Get() ) {
|
||||
std::cout << "Color schemes:\n";
|
||||
for ( const auto& colorSchemeEntry : terminalColorSchemes )
|
||||
std::cout << "\t" << colorSchemeEntry.first << "\n";
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
if ( colorScheme ) {
|
||||
auto colorSchemeIt = terminalColorSchemes.find( colorScheme.Get() );
|
||||
if ( !config->theme.colorScheme.empty() ) {
|
||||
auto colorSchemeIt = terminalColorSchemes.find( config->theme.colorScheme );
|
||||
if ( colorSchemeIt != terminalColorSchemes.end() )
|
||||
selectedColorScheme = &colorSchemeIt->second;
|
||||
}
|
||||
|
||||
DisplayManager* displayManager = Engine::instance()->getDisplayManager();
|
||||
Display* currentDisplay = displayManager->getDisplayIndex( 0 );
|
||||
Display* currentDisplay = displayManager->getDisplayIndex(
|
||||
config->windowState.displayIndex >= 0 &&
|
||||
config->windowState.displayIndex < displayManager->getDisplayCount()
|
||||
? config->windowState.displayIndex
|
||||
: 0 );
|
||||
if ( !currentDisplay ) {
|
||||
std::cerr << "Display not found, exiting" << std::endl;
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
Sizei windowSize( width.Get(), height.Get() );
|
||||
Sizei windowSize( config->windowState.size );
|
||||
const auto displaySize = currentDisplay->getUsableBounds().getSize();
|
||||
if ( displaySize.getWidth() > 0 && windowSize.getWidth() >= displaySize.getWidth() )
|
||||
windowSize.setWidth( static_cast<int>( displaySize.getWidth() * 0.8f ) );
|
||||
if ( displaySize.getHeight() > 0 && windowSize.getHeight() >= displaySize.getHeight() )
|
||||
windowSize.setHeight( static_cast<int>( displaySize.getHeight() * 0.75f ) );
|
||||
|
||||
FontTrueTypePtr uiFont;
|
||||
if ( !config->font.uiPath.empty() && FileSystem::fileExists( config->font.uiPath ) ) {
|
||||
uiFont = FontTrueType::New( "eterm-ui-font" );
|
||||
if ( !uiFont->loadFromFile( config->font.uiPath ) )
|
||||
uiFont.reset();
|
||||
}
|
||||
UIApplication::Settings appSettings;
|
||||
appSettings.basePath = FileSystem::removeLastFolderFromPath( resPath );
|
||||
appSettings.pixelDensity =
|
||||
pixelDensity ? pixelDensity.Get() : currentDisplay->getPixelDensity();
|
||||
appSettings.fontHinting = fontHinting.Get();
|
||||
appSettings.fontAntialiasing = fontAntialiasing.Get();
|
||||
appSettings.pixelDensity = config->window.pixelDensity > 0 ? config->window.pixelDensity
|
||||
: currentDisplay->getPixelDensity();
|
||||
appSettings.fontHinting = config->font.hinting;
|
||||
appSettings.fontAntialiasing = config->font.antialiasing;
|
||||
appSettings.baseFont = uiFont.get();
|
||||
const Int32 frameRateLimit =
|
||||
benchmarkModeFlag.Get()
|
||||
? 0
|
||||
: static_cast<Int32>( maxFPS.Get() ? maxFPS.Get() : currentDisplay->getRefreshRate() );
|
||||
UIApplication app( WindowSettings( windowSize.getWidth(), windowSize.getHeight(), "eterm",
|
||||
WindowStyle::Default, WindowBackend::Default, 32,
|
||||
resPath + "icon/eterm.png",
|
||||
appSettings.pixelDensity.value() ),
|
||||
appSettings, ContextSettings( vsync.Get(), frameRateLimit ) );
|
||||
config->window.benchmarkMode ? 0 : static_cast<Int32>( config->window.maxFPS );
|
||||
UIApplication app(
|
||||
WindowSettings( windowSize.getWidth(), windowSize.getHeight(), "eterm",
|
||||
WindowStyle::Default, WindowBackend::Default, 32,
|
||||
resPath + "icon/eterm.png", appSettings.pixelDensity.value() ),
|
||||
appSettings,
|
||||
ContextSettings( config->window.vsync, frameRateLimit, config->window.multisamples,
|
||||
config->window.rendererVersion ) );
|
||||
appWindow = app.getWindow();
|
||||
scene = app.getUI();
|
||||
if ( !appWindow || !appWindow->isOpen() || !scene )
|
||||
return EXIT_FAILURE;
|
||||
scene->setColorSchemePreference( config->theme.uiColorScheme );
|
||||
scene->getUIThemeManager()->setDefaultFontSize( config->font.uiSize );
|
||||
if ( config->windowState.position != Vector2i( -1, -1 ) ) {
|
||||
appWindow->setPosition( config->windowState.position.x +
|
||||
( config->windowState.maximized ? -1 : 0 ),
|
||||
config->windowState.position.y );
|
||||
}
|
||||
#if EE_PLATFORM != EE_PLATFORM_EMSCRIPTEN && EE_PLATFORM != EE_PLATFORM_MACOS
|
||||
if ( config->windowState.maximized )
|
||||
appWindow->maximize();
|
||||
#endif
|
||||
FileSystem::changeWorkingDirectory( initialWorkingDirectory );
|
||||
appWindow->setClearColor( RGB( 0, 0, 0 ) );
|
||||
scene->getUIThemeManager()->setDefaultEffectsEnabled( false );
|
||||
@@ -748,9 +834,9 @@ int EtermApp::run( int argc, char* argv[] ) {
|
||||
"eterm", remixIconFont.get(), noniconsFont.get(), codIconFont.get() ) );
|
||||
terminalIcon = scene->findIcon( "terminal" );
|
||||
}
|
||||
if ( fontPath && FileSystem::fileExists( fontPath.Get() ) ) {
|
||||
if ( !config->font.path.empty() && FileSystem::fileExists( config->font.path ) ) {
|
||||
terminalFont = FontTrueType::New( "eterm-monospace", resourceScope ).get();
|
||||
if ( terminalFont->loadFromFile( fontPath.Get() ) )
|
||||
if ( terminalFont->loadFromFile( config->font.path ) )
|
||||
FontFamily::loadFromRegular( terminalFont );
|
||||
else
|
||||
terminalFont = nullptr;
|
||||
@@ -764,34 +850,37 @@ int EtermApp::run( int argc, char* argv[] ) {
|
||||
FontFamily::loadFromRegular( terminalFont, "DejaVuSansMono" );
|
||||
}
|
||||
|
||||
if ( fallbackFontPath ) {
|
||||
if ( FileSystem::fileExists( fallbackFontPath.Get() ) ) {
|
||||
if ( !config->font.fallbackPath.empty() ) {
|
||||
if ( FileSystem::fileExists( config->font.fallbackPath ) ) {
|
||||
auto fallback = FontTrueType::New( "eterm-fallback-font", resourceScope );
|
||||
if ( fallback->loadFromFile( fallbackFontPath.Get() ) )
|
||||
if ( fallback->loadFromFile( config->font.fallbackPath ) )
|
||||
resourceScope.getFontService().addFallbackFont( std::move( fallback ) );
|
||||
}
|
||||
} else if ( auto fallback = resourceScope.findFont( "DroidSansFallbackFull" ) ) {
|
||||
resourceScope.getFontService().addFallbackFont( std::move( fallback ) );
|
||||
}
|
||||
|
||||
const std::string launchPath = wd ? wd.Get() : initialWorkingDirectory;
|
||||
const std::string launchPath = config->terminal.workingDirectory.empty()
|
||||
? initialWorkingDirectory
|
||||
: config->terminal.workingDirectory;
|
||||
FileInfo launchFile( launchPath );
|
||||
const bool launchExecutable = launchFile.isRegularFile() && launchFile.isExecutable();
|
||||
terminalConfig.program = launchExecutable ? launchFile.getFilepath() : shell.Get();
|
||||
terminalConfig.arguments =
|
||||
shellArgs ? String::split( shellArgs.Get() ) : std::vector<std::string>{};
|
||||
terminalConfig.program = launchExecutable ? launchFile.getFilepath() : config->terminal.shell;
|
||||
terminalConfig.arguments = config->terminal.shellArguments.empty()
|
||||
? std::vector<std::string>{}
|
||||
: String::split( config->terminal.shellArguments );
|
||||
terminalConfig.workingDirectory = launchFile.getDirectoryPath();
|
||||
terminalConfig.executeInShell = executeInShell.Get();
|
||||
terminalConfig.historySize = historySize.Get();
|
||||
terminalConfig.cursorStyle = cursorStyle.Get();
|
||||
terminalConfig.fontHinting = fontHinting.Get();
|
||||
terminalConfig.fontAntialiasing = fontAntialiasing.Get();
|
||||
terminalConfig.useFrameBuffer = fb.Get();
|
||||
terminalConfig.keepAlive = !launchExecutable && !shell;
|
||||
terminalConfig.closeOnExit = closeOnExit.Get();
|
||||
warnBeforeClose = warnBeforeCloseFlag.Get();
|
||||
benchmarkMode = benchmarkModeFlag.Get();
|
||||
terminalFontSize = PixelDensity::dpToPx( fontSize.Get() );
|
||||
terminalConfig.executeInShell = config->terminal.executeInShell;
|
||||
terminalConfig.historySize = config->terminal.historySize;
|
||||
terminalConfig.cursorStyle = config->terminal.cursorStyle;
|
||||
terminalConfig.fontHinting = config->font.hinting;
|
||||
terminalConfig.fontAntialiasing = config->font.antialiasing;
|
||||
terminalConfig.useFrameBuffer = config->terminal.useFrameBuffer;
|
||||
terminalConfig.keepAlive = !launchExecutable && config->terminal.shell.empty();
|
||||
terminalConfig.closeOnExit = config->terminal.closeOnExit;
|
||||
warnBeforeClose = config->window.warnBeforeClose;
|
||||
benchmarkMode = config->window.benchmarkMode;
|
||||
terminalFontSize = PixelDensity::dpToPx( config->font.size );
|
||||
|
||||
mainLayout = UILinearLayout::NewVertical();
|
||||
mainLayout->setParent( scene->getRoot() );
|
||||
@@ -799,7 +888,7 @@ int EtermApp::run( int argc, char* argv[] ) {
|
||||
mainLayout->setPixelsSize( appWindow->getSize().asFloat() );
|
||||
|
||||
tabSplitter = UITabWidgetSplitter::New( &splitterClient, scene );
|
||||
tabSplitter->setHideTabBarOnSingleTab( !alwaysShowTabBar.Get() );
|
||||
tabSplitter->setHideTabBarOnSingleTab( !config->window.alwaysShowTabBar );
|
||||
tabSplitter->setCanCreateSplitFn( [this]( SplitDirection, UIWidget* ) {
|
||||
restoreMaximizedTabWidget();
|
||||
return true;
|
||||
@@ -841,7 +930,8 @@ int EtermApp::run( int argc, char* argv[] ) {
|
||||
}
|
||||
mainLayout->updateLayout();
|
||||
|
||||
for ( size_t tab = 0; tab < eemax( static_cast<size_t>( 1 ), initialTabs.Get() ); ++tab ) {
|
||||
for ( size_t tab = 0; tab < eemax( static_cast<size_t>( 1 ), config->terminal.initialTabs );
|
||||
++tab ) {
|
||||
if ( !createTerminal( tabs ) ) {
|
||||
appWindow->showMessageBox(
|
||||
EE::Window::Window::MessageBoxType::Error, "eterm",
|
||||
@@ -878,12 +968,15 @@ int EtermApp::run( int argc, char* argv[] ) {
|
||||
secondsCounter.restart();
|
||||
}
|
||||
} );
|
||||
config->captureWindowState( appWindow );
|
||||
if ( !config->saveWindowState() )
|
||||
Log::error( "Could not save eterm window state to %s", config->getConfigPath() );
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace eterm
|
||||
|
||||
EE_MAIN_FUNC int main( int argc, char* argv[] ) {
|
||||
EtermApp app;
|
||||
App app;
|
||||
return app.run( argc, argv );
|
||||
}
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
#pragma once
|
||||
|
||||
#include "appconfig.hpp"
|
||||
#include <args/args.hxx>
|
||||
#include <eepp/core/small_vector.hpp>
|
||||
#include <eepp/ee.hpp>
|
||||
#include <eepp/ui/iconmanager.hpp>
|
||||
#include <eepp/ui/tools/uifontpickerdialog.hpp>
|
||||
#include <eepp/ui/tools/uisettingspanel.hpp>
|
||||
#include <eepp/ui/tools/uitabwidgetsplitter.hpp>
|
||||
#include <eepp/ui/tools/uiwidgetinspector.hpp>
|
||||
#include <eepp/ui/uiapplication.hpp>
|
||||
#include <eepp/ui/uilinearlayout.hpp>
|
||||
#include <eepp/ui/uimessagebox.hpp>
|
||||
#include <eterm/ui/uiterminal.hpp>
|
||||
|
||||
using namespace EE;
|
||||
using namespace EE::Graphics;
|
||||
using namespace EE::Scene;
|
||||
using namespace EE::System;
|
||||
using namespace EE::UI;
|
||||
using namespace EE::UI::Tools;
|
||||
using namespace EE::Window;
|
||||
using namespace eterm::Terminal;
|
||||
using namespace eterm::UI;
|
||||
using namespace eterm;
|
||||
|
||||
namespace eterm {
|
||||
|
||||
struct TerminalLaunchConfig {
|
||||
std::string program;
|
||||
std::vector<std::string> arguments;
|
||||
std::string workingDirectory;
|
||||
std::string executeInShell;
|
||||
size_t historySize{ 10000 };
|
||||
TerminalCursorMode cursorStyle{ TerminalCursorMode::SteadyUnderline };
|
||||
FontHinting fontHinting{ FontHinting::Full };
|
||||
FontAntialiasing fontAntialiasing{ FontAntialiasing::Grayscale };
|
||||
bool useFrameBuffer{ false };
|
||||
bool keepAlive{ true };
|
||||
bool closeOnExit{ false };
|
||||
};
|
||||
|
||||
class App {
|
||||
public:
|
||||
int run( int argc, char* argv[] );
|
||||
|
||||
class TerminalSplitterClient : public UITabWidgetSplitter::Client {
|
||||
public:
|
||||
explicit TerminalSplitterClient( App& app ) : mApp( app ) {}
|
||||
|
||||
void onTabCreated( UITab* tab, UIWidget* ) override;
|
||||
void onWidgetFocusChange( UIWidget* ) override;
|
||||
|
||||
private:
|
||||
App& mApp;
|
||||
};
|
||||
|
||||
private:
|
||||
friend struct SettingsPanel;
|
||||
|
||||
std::string getResourcePath() const;
|
||||
String i18n( const std::string& key, const String& defaultValue ) const;
|
||||
void loadColorSchemes( const std::string& resPath );
|
||||
static UITerminal* terminalFromTab( UITab* tab );
|
||||
void updateWindowTitle();
|
||||
bool hasTerminals() const;
|
||||
static bool hasRunningChildren( UITab* tab );
|
||||
void closeTab( UITab* tab );
|
||||
void queueExitCloseTab( UITab* tab );
|
||||
void queueExitedTabs();
|
||||
void requestCloseTab( UITab* tab );
|
||||
void renameSession( UITerminal* terminal );
|
||||
void maximizeTabWidget( UITabWidget* tabWidget );
|
||||
void restoreMaximizedTabWidget();
|
||||
void configureTab( UITab* tab );
|
||||
UITerminal* createTerminal( UITabWidget* target = nullptr );
|
||||
UITerminal* createTerminalSplit( SplitDirection direction, UITerminal* terminal );
|
||||
void addTabKeyBindings( UITerminal* terminal );
|
||||
void showSettings();
|
||||
void openFontPicker( bool uiFont, bool fallbackFont = false );
|
||||
void forEachTerminal( const std::function<void( UITerminal* )>& fn );
|
||||
void savePreferences();
|
||||
void createNewTerminal();
|
||||
bool closeWindow( EE::Window::Window* );
|
||||
|
||||
EE::Window::Window* appWindow{ nullptr };
|
||||
UISceneNode* scene{ nullptr };
|
||||
UILinearLayout* mainLayout{ nullptr };
|
||||
UITabWidgetSplitter* tabSplitter{ nullptr };
|
||||
FontTrueType* terminalFont{ nullptr };
|
||||
UIIcon* terminalIcon{ nullptr };
|
||||
UIMessageBox* closeDialog{ nullptr };
|
||||
UIWindow* settingsWindow{ nullptr };
|
||||
UIWidget* closeDialogWidget{ nullptr };
|
||||
UIWindow* maximizedTabWidgetWindow{ nullptr };
|
||||
UITabWidget* maximizedTabWidget{ nullptr };
|
||||
UINodeLink* maximizedTabWidgetLink{ nullptr };
|
||||
TerminalLaunchConfig terminalConfig;
|
||||
std::unique_ptr<eterm::AppConfig> config;
|
||||
std::map<std::string, TerminalColorScheme> terminalColorSchemes;
|
||||
const TerminalColorScheme* selectedColorScheme{ nullptr };
|
||||
Float terminalFontSize{ 12 };
|
||||
bool warnBeforeClose{ false };
|
||||
bool closeApproved{ false };
|
||||
bool benchmarkMode{ false };
|
||||
Clock secondsCounter;
|
||||
SmallVector<UITab*, 8> pendingExitCloseTabs;
|
||||
TerminalSplitterClient splitterClient{ *this };
|
||||
};
|
||||
|
||||
} // namespace eterm
|
||||
@@ -0,0 +1,468 @@
|
||||
#include "settingspanel.hpp"
|
||||
#include "eterm.hpp"
|
||||
|
||||
#include <eepp/ui/tools/uisettingspanel.hpp>
|
||||
#include <eepp/ui/uiscenenode.hpp>
|
||||
#include <eepp/ui/uiwindow.hpp>
|
||||
|
||||
#include <string>
|
||||
|
||||
using namespace EE::UI::Tools;
|
||||
|
||||
namespace eterm {
|
||||
|
||||
UIWindow* SettingsPanel::create( App& app ) {
|
||||
UIWindow::StyleConfig windowStyle{ UI_WIN_DEFAULT_FLAGS | UI_WIN_MAXIMIZE_BUTTON |
|
||||
UI_WIN_MODAL };
|
||||
auto settingsWindow = UIWindow::NewOpt( UIWindow::SIMPLE_LAYOUT, windowStyle );
|
||||
settingsWindow->setId( "settings_panel" );
|
||||
settingsWindow->setTitle( app.i18n( "settings", "Settings" ) );
|
||||
const auto sceneSize = app.scene->getPixelsSize();
|
||||
settingsWindow->setPixelsSize( { eeclamp( sceneSize.getWidth() * 0.82f, 720.f, 1200.f ),
|
||||
eeclamp( sceneSize.getHeight() * 0.82f, 520.f, 850.f ) } );
|
||||
settingsWindow->setMinWindowSize( 640, 440 );
|
||||
auto* panel = UISettingsPanel::New( settingsWindow->getContainer() );
|
||||
panel->setSearchResultsText( app.i18n( "search_results", "Search Results" ) );
|
||||
|
||||
panel->addCategory( "appearance.theme", app.i18n( "appearance", "Appearance" ),
|
||||
app.i18n( "theme_and_language", "Theme & Language" ) );
|
||||
panel->addChoice(
|
||||
{ "uiColorScheme", "appearance.theme",
|
||||
app.i18n( "ui_prefes_color_scheme", "UI Prefers Color Scheme" ),
|
||||
app.i18n(
|
||||
"ui_prefers_color_scheme_desc",
|
||||
"Choose whether the interface follows the system, light, or dark appearance." ) },
|
||||
{ app.i18n( "system", "System" ), app.i18n( "light", "Light" ),
|
||||
app.i18n( "dark", "Dark" ) },
|
||||
[&app] {
|
||||
return app.config->theme.uiColorScheme == ColorSchemeExtPreference::System ? size_t{ 0 }
|
||||
: app.config->theme.uiColorScheme == ColorSchemeExtPreference::Light
|
||||
? size_t{ 1 }
|
||||
: size_t{ 2 };
|
||||
},
|
||||
[&app]( size_t selected ) {
|
||||
static constexpr ColorSchemeExtPreference values[] = { ColorSchemeExtPreference::System,
|
||||
ColorSchemeExtPreference::Light,
|
||||
ColorSchemeExtPreference::Dark };
|
||||
app.config->theme.uiColorScheme = values[std::min( selected, size_t{ 2 } )];
|
||||
app.scene->setColorSchemePreference( app.config->theme.uiColorScheme );
|
||||
app.savePreferences();
|
||||
} );
|
||||
panel->addCategory( "appearance.fonts", app.i18n( "appearance", "Appearance" ),
|
||||
app.i18n( "fonts_and_scale", "Fonts & Scale" ) );
|
||||
panel->addAction(
|
||||
{ "uiFont", "appearance.fonts",
|
||||
app.i18n( "ui_font_and_size_ellipsis", "UI Font & Size..." ),
|
||||
app.i18n( "ui_font_desc", "Choose the proportional font used by the interface." ) },
|
||||
app.i18n( "choose_font", "Choose Font..." ), [&app] { app.openFontPicker( true ); } );
|
||||
panel->addAction(
|
||||
{ "terminalFont", "appearance.fonts",
|
||||
app.i18n( "terminal_font_and_size_ellipsis", "Terminal Font & Size..." ),
|
||||
app.i18n( "terminal_font_desc", "Choose the monospace font used by terminals." ) },
|
||||
app.i18n( "choose_font", "Choose Font..." ), [&app] { app.openFontPicker( false ); } );
|
||||
panel->addAction(
|
||||
{ "fallbackFont", "appearance.fonts",
|
||||
app.i18n( "fallback_font_ellipsis", "Fallback Font..." ),
|
||||
app.i18n( "fallback_font_desc", "Choose the font used for missing glyphs." ) },
|
||||
app.i18n( "choose_font", "Choose Font..." ),
|
||||
[&app] { app.openFontPicker( false, true ); } );
|
||||
panel->addFloat(
|
||||
{ "uiFontSize", "appearance.fonts", app.i18n( "ui_font_size", "UI Font Size" ),
|
||||
app.i18n( "ui_font_size_desc", "Set the font size used by the application UI." ) },
|
||||
6, 72, 0.5, [&app] { return app.config->font.uiSize; },
|
||||
[&app]( double value ) {
|
||||
app.config->font.uiSize = value;
|
||||
app.scene->getUIThemeManager()->setDefaultFontSize( value );
|
||||
app.scene->getRoot()->reloadStyle( true, true, true, true, true );
|
||||
app.savePreferences();
|
||||
} );
|
||||
panel->addFloat(
|
||||
{ "terminalFontSize", "appearance.fonts",
|
||||
app.i18n( "terminal_font_size", "Terminal Font Size" ),
|
||||
app.i18n( "terminal_font_size_desc", "Set the default terminal font size." ) },
|
||||
6, 72, 0.5, [&app] { return app.config->font.size; },
|
||||
[&app]( double value ) {
|
||||
app.config->font.size = value;
|
||||
app.terminalFontSize = PixelDensity::dpToPx( value );
|
||||
app.forEachTerminal(
|
||||
[&app]( UITerminal* terminal ) { terminal->setFontSize( app.terminalFontSize ); } );
|
||||
app.savePreferences();
|
||||
} );
|
||||
panel->addFloat(
|
||||
{ "uiScaleFactor", "appearance.fonts", app.i18n( "ui_scale_factor", "UI Scale Factor" ),
|
||||
app.i18n( "ui_scale_factor_desc",
|
||||
"Scale the complete user interface. Restart required." ) },
|
||||
1, 6, 0.1,
|
||||
[&app] {
|
||||
return app.config->window.pixelDensity > 0 ? app.config->window.pixelDensity
|
||||
: PixelDensity::getPixelDensity();
|
||||
},
|
||||
[&app]( double value ) {
|
||||
app.config->window.pixelDensity = value;
|
||||
app.savePreferences();
|
||||
} );
|
||||
panel->addChoice(
|
||||
{ "fontHinting", "appearance.fonts", app.i18n( "ui_font_hint", "Font Hinting" ),
|
||||
app.i18n( "ui_font_hint_desc", "Control glyph alignment to the pixel grid." ) },
|
||||
{ app.i18n( "none", "None" ), app.i18n( "slight", "Slight" ), app.i18n( "full", "Full" ) },
|
||||
[&app] { return static_cast<size_t>( app.config->font.hinting ); },
|
||||
[&app]( size_t selected ) {
|
||||
static constexpr FontHinting values[] = { FontHinting::None, FontHinting::Slight,
|
||||
FontHinting::Full };
|
||||
app.config->font.hinting = values[std::min( selected, size_t{ 2 } )];
|
||||
app.scene->getResourceScope()->getFontService().setHinting( app.config->font.hinting );
|
||||
app.forEachTerminal(
|
||||
[]( UITerminal* terminal ) { terminal->syncFontRenderingConfig(); } );
|
||||
app.savePreferences();
|
||||
} );
|
||||
panel->addChoice(
|
||||
{ "fontAntialiasing", "appearance.fonts",
|
||||
app.i18n( "ui_font_antialiasing", "Font Anti-Aliasing" ),
|
||||
app.i18n( "ui_font_antialiasing_desc", "Choose how glyph edges are smoothed." ) },
|
||||
{ app.i18n( "none", "None" ), app.i18n( "grayscale", "Grayscale" ),
|
||||
app.i18n( "subpixel", "Subpixel" ) },
|
||||
[&app] { return static_cast<size_t>( app.config->font.antialiasing ); },
|
||||
[&app]( size_t selected ) {
|
||||
static constexpr FontAntialiasing values[] = {
|
||||
FontAntialiasing::None, FontAntialiasing::Grayscale, FontAntialiasing::Subpixel };
|
||||
app.config->font.antialiasing = values[std::min( selected, size_t{ 2 } )];
|
||||
app.scene->getResourceScope()->getFontService().setAntialiasing(
|
||||
app.config->font.antialiasing );
|
||||
app.forEachTerminal(
|
||||
[]( UITerminal* terminal ) { terminal->syncFontRenderingConfig(); } );
|
||||
app.savePreferences();
|
||||
} );
|
||||
|
||||
panel->addCategory( "window.renderer", app.i18n( "window", "Window" ),
|
||||
app.i18n( "renderer", "Renderer" ) );
|
||||
panel->addBool(
|
||||
{ "vsync", "window.renderer", app.i18n( "vsync", "VSync" ),
|
||||
app.i18n( "vsync_desc", "Synchronize rendering with the display. Restart required." ) },
|
||||
&app.config->window.vsync, [&app]( bool ) { app.savePreferences(); } );
|
||||
const String monitorRefreshRate = app.i18n( "monitor_refresh_rate", "Monitor Refresh Rate" );
|
||||
const String unlimitedFrameRate = app.i18n( "unlimited", "Unlimited" );
|
||||
panel->addEditableChoice(
|
||||
{ "frameRateLimit", "window.renderer", app.i18n( "frame_rate_limit", "Frame Rate Limit" ),
|
||||
app.i18n( "frame_rate_limit_desc", "Limit rendered frames per second, follow the monitor "
|
||||
"refresh rate, or disable the limit." ) },
|
||||
{ monitorRefreshRate, unlimitedFrameRate, "30", "60", "75", "120", "144", "165", "240" },
|
||||
[&app, monitorRefreshRate, unlimitedFrameRate] {
|
||||
return app.config->window.maxFPS ==
|
||||
static_cast<Uint32>( ContextSettings::FrameRateLimitScreenRefreshRate )
|
||||
? monitorRefreshRate
|
||||
: app.config->window.maxFPS == 0
|
||||
? unlimitedFrameRate
|
||||
: String( String::toString( app.config->window.maxFPS ) );
|
||||
},
|
||||
[&app, monitorRefreshRate, unlimitedFrameRate]( const String& selection ) {
|
||||
Uint32 value;
|
||||
if ( selection == monitorRefreshRate )
|
||||
value = ContextSettings::FrameRateLimitScreenRefreshRate;
|
||||
else if ( selection == unlimitedFrameRate )
|
||||
value = 0;
|
||||
else if ( !String::fromString( value, selection ) || value > 1000 )
|
||||
return false;
|
||||
app.config->window.maxFPS = value;
|
||||
app.appWindow->setFrameRateLimit( value );
|
||||
app.savePreferences();
|
||||
return true;
|
||||
} );
|
||||
auto rendererVersions = Renderer::getAvailableGraphicsLibraryVersions();
|
||||
std::vector<String> rendererNames;
|
||||
for ( auto version : rendererVersions )
|
||||
rendererNames.emplace_back( Renderer::graphicsLibraryVersionToString( version ) );
|
||||
if ( !rendererVersions.empty() )
|
||||
panel->addChoice(
|
||||
{ "rendererVersion", "window.renderer",
|
||||
app.i18n( "ui_renderer_version", "Renderer Version" ),
|
||||
app.i18n( "ui_renderer_version_desc",
|
||||
"Select the graphics API. Restart required." ) },
|
||||
rendererNames,
|
||||
[&app, rendererVersions] {
|
||||
auto found = std::find( rendererVersions.begin(), rendererVersions.end(),
|
||||
app.config->window.rendererVersion );
|
||||
return found == rendererVersions.end()
|
||||
? size_t{ 0 }
|
||||
: static_cast<size_t>( found - rendererVersions.begin() );
|
||||
},
|
||||
[&app, rendererVersions]( size_t selected ) {
|
||||
app.config->window.rendererVersion =
|
||||
rendererVersions[std::min( selected, rendererVersions.size() - 1 )];
|
||||
app.savePreferences();
|
||||
} );
|
||||
panel->addChoice(
|
||||
{ "multisamples", "window.renderer",
|
||||
app.i18n( "ui_multisamples_level", "Multisample Anti-Aliasing Level" ),
|
||||
app.i18n( "ui_multisamples_level_desc",
|
||||
"Set renderer multisampling. Restart required." ) },
|
||||
{ "0", "2", "4", "8", "16" },
|
||||
[&app] {
|
||||
static constexpr Uint32 values[] = { 0, 2, 4, 8, 16 };
|
||||
auto found = std::find( std::begin( values ), std::end( values ),
|
||||
app.config->window.multisamples );
|
||||
return found == std::end( values )
|
||||
? size_t{ 0 }
|
||||
: static_cast<size_t>( found - std::begin( values ) );
|
||||
},
|
||||
[&app]( size_t selected ) {
|
||||
static constexpr Uint32 values[] = { 0, 2, 4, 8, 16 };
|
||||
app.config->window.multisamples = values[std::min( selected, size_t{ 4 } )];
|
||||
app.savePreferences();
|
||||
} );
|
||||
panel->addBool(
|
||||
{ "benchmarkMode", "window.renderer", app.i18n( "benchmark_mode", "Benchmark Mode" ),
|
||||
app.i18n( "benchmark_mode_desc", "Render continuously to measure performance." ) },
|
||||
&app.config->window.benchmarkMode, [&app]( bool value ) {
|
||||
app.benchmarkMode = value;
|
||||
app.savePreferences();
|
||||
} );
|
||||
|
||||
panel->addCategory( "terminal.behavior", app.i18n( "terminal", "Terminal" ),
|
||||
app.i18n( "behavior", "Behavior" ) );
|
||||
panel->addChoice(
|
||||
{ "newTerminalBehavior", "terminal.behavior",
|
||||
app.i18n( "new_terminal_behavior", "New Terminal Behavior" ),
|
||||
app.i18n( "new_terminal_behavior_desc",
|
||||
"Choose where new terminal sessions are opened." ) },
|
||||
{ app.i18n( "open_in_same_tabbar", "Open in Current Tab Bar" ),
|
||||
app.i18n( "open_in_vertical_split", "Open in New Vertical Split" ),
|
||||
app.i18n( "open_in_horizontal_split", "Open in New Horizontal Split" ) },
|
||||
[&app] { return static_cast<size_t>( app.config->terminal.newTerminalBehavior ); },
|
||||
[&app]( size_t selected ) {
|
||||
app.config->terminal.newTerminalBehavior =
|
||||
static_cast<NewTerminalBehavior>( std::min( selected, size_t{ 2 } ) );
|
||||
app.savePreferences();
|
||||
} );
|
||||
panel->addBool(
|
||||
{ "terminalExclusiveMode", "terminal.behavior",
|
||||
app.i18n( "enable_exclusive_mode_by_default", "Enable Exclusive Mode by Default" ),
|
||||
app.i18n( "enable_exclusive_mode_by_default_tooltip",
|
||||
"Disable global keybindings in newly created terminals." ) },
|
||||
&app.config->terminal.exclusiveMode, [&app]( bool value ) {
|
||||
app.forEachTerminal(
|
||||
[value]( UITerminal* terminal ) { terminal->setExclusiveMode( value ); } );
|
||||
app.savePreferences();
|
||||
} );
|
||||
panel->addBool(
|
||||
{ "closeTerminalTabOnExit", "terminal.behavior",
|
||||
app.i18n( "close_terminal_tab_on_exit", "Close Terminal Tab on Exit" ),
|
||||
app.i18n( "close_terminal_tab_on_exit_tooltip", "Close a tab when its process exits." ) },
|
||||
&app.config->terminal.closeOnExit, [&app]( bool value ) {
|
||||
app.terminalConfig.closeOnExit = value;
|
||||
app.forEachTerminal(
|
||||
[value]( UITerminal* terminal ) { terminal->getTerm()->setKeepAlive( !value ); } );
|
||||
app.savePreferences();
|
||||
} );
|
||||
panel->addBool( { "warnBeforeClosingTerminal", "terminal.behavior",
|
||||
app.i18n( "warn_before_closing_tab", "Warn Before Closing Tab" ),
|
||||
app.i18n( "warn_before_closing_tab_tooltip",
|
||||
"Ask before closing a terminal while a program is running." ) },
|
||||
&app.config->window.warnBeforeClose, [&app]( bool value ) {
|
||||
app.warnBeforeClose = value;
|
||||
app.savePreferences();
|
||||
} );
|
||||
const std::vector<std::string> knownShells{ "bash", "sh", "zsh", "fish", "nu", "csh",
|
||||
"tcsh", "ksh", "dash", "cmd", "powershell" };
|
||||
std::vector<String> installedShells;
|
||||
for ( const auto& shell : knownShells ) {
|
||||
auto path = Sys::which( shell );
|
||||
if ( !path.empty() )
|
||||
installedShells.emplace_back( std::move( path ) );
|
||||
}
|
||||
auto currentShell = app.config->terminal.shell;
|
||||
if ( currentShell.empty() ) {
|
||||
if ( const char* shell = std::getenv( "SHELL" ); shell )
|
||||
currentShell = FileSystem::fileExists( shell ) ? shell : Sys::which( shell );
|
||||
if ( currentShell.empty() )
|
||||
currentShell = Sys::which(
|
||||
Sys::getPlatformType() == Sys::PlatformType::Windows ? "powershell" : "bash" );
|
||||
}
|
||||
if ( !currentShell.empty() && std::find( installedShells.begin(), installedShells.end(),
|
||||
String( currentShell ) ) == installedShells.end() )
|
||||
installedShells.emplace_back( currentShell );
|
||||
panel->addEditableChoice(
|
||||
{ "terminalShell", "terminal.behavior", app.i18n( "terminal_shell", "Terminal Shell" ),
|
||||
app.i18n( "terminal_shell_desc", "Set the shell executable used by new terminals." ) },
|
||||
installedShells, [currentShell] { return String( currentShell ); },
|
||||
[&app]( const String& selection ) {
|
||||
auto shell = selection.toUtf8();
|
||||
if ( Sys::which( shell ).empty() && !FileSystem::fileExists( shell ) )
|
||||
return false;
|
||||
app.config->terminal.shell = shell;
|
||||
app.terminalConfig.program = shell;
|
||||
app.savePreferences();
|
||||
return true;
|
||||
} );
|
||||
panel->addText(
|
||||
{ "terminalShellArguments", "terminal.behavior",
|
||||
app.i18n( "terminal_shell_arguments", "Terminal Shell Arguments" ),
|
||||
app.i18n( "terminal_shell_arguments_desc",
|
||||
"Set shell arguments used by new terminals." ) },
|
||||
[&app] { return app.config->terminal.shellArguments; },
|
||||
[&app]( const std::string& value ) {
|
||||
app.config->terminal.shellArguments = value;
|
||||
app.terminalConfig.arguments = String::split( value );
|
||||
app.savePreferences();
|
||||
return true;
|
||||
},
|
||||
true );
|
||||
panel->addInteger(
|
||||
{ "terminalScrollback", "terminal.behavior",
|
||||
app.i18n( "terminal_scrollback", "Terminal Scrollback" ),
|
||||
app.i18n( "configure_terminal_scrollback_desc",
|
||||
"Set retained terminal history lines." ) },
|
||||
0, std::numeric_limits<int>::max(),
|
||||
[&app] {
|
||||
return static_cast<int>( std::min<size_t>( app.config->terminal.historySize,
|
||||
std::numeric_limits<int>::max() ) );
|
||||
},
|
||||
[&app]( int value ) {
|
||||
app.config->terminal.historySize = value;
|
||||
app.terminalConfig.historySize = value;
|
||||
app.savePreferences();
|
||||
} );
|
||||
panel->addBool(
|
||||
{ "terminalFrameBuffer", "terminal.behavior", app.i18n( "framebuffer", "Use Framebuffer" ),
|
||||
app.i18n(
|
||||
"framebuffer_desc",
|
||||
"Render terminals through an FBO instead of the default VBO path. This can improve "
|
||||
"rendering performance by using a different invalidation strategy." ) },
|
||||
&app.config->terminal.useFrameBuffer, [&app]( bool value ) {
|
||||
app.terminalConfig.useFrameBuffer = value;
|
||||
app.savePreferences();
|
||||
} );
|
||||
|
||||
panel->addCategory( "terminal.appearance", app.i18n( "terminal", "Terminal" ),
|
||||
app.i18n( "appearance", "Appearance" ) );
|
||||
std::vector<String> schemeNames{ app.i18n( "default", "Default" ) };
|
||||
std::vector<std::string> schemeIds{ "" };
|
||||
for ( const auto& [name, scheme] : app.terminalColorSchemes ) {
|
||||
schemeNames.emplace_back( name );
|
||||
schemeIds.emplace_back( name );
|
||||
}
|
||||
if ( !schemeIds.empty() )
|
||||
panel->addChoice(
|
||||
{ "terminalColorScheme", "terminal.appearance",
|
||||
app.i18n( "terminal_color_scheme", "Terminal Color Scheme" ),
|
||||
app.i18n( "terminal_color_scheme_desc", "Choose the colors used by terminals." ) },
|
||||
schemeNames,
|
||||
[&app, schemeIds] {
|
||||
auto found =
|
||||
std::find( schemeIds.begin(), schemeIds.end(), app.config->theme.colorScheme );
|
||||
return found == schemeIds.end() ? size_t{ 0 }
|
||||
: static_cast<size_t>( found - schemeIds.begin() );
|
||||
},
|
||||
[&app, schemeIds]( size_t selected ) {
|
||||
app.config->theme.colorScheme =
|
||||
schemeIds[std::min( selected, schemeIds.size() - 1 )];
|
||||
if ( app.config->theme.colorScheme.empty() ) {
|
||||
app.selectedColorScheme = nullptr;
|
||||
auto defaultScheme = TerminalColorScheme::getDefault();
|
||||
app.forEachTerminal( [&defaultScheme]( UITerminal* terminal ) {
|
||||
terminal->setColorScheme( defaultScheme );
|
||||
} );
|
||||
app.savePreferences();
|
||||
return;
|
||||
}
|
||||
auto found = app.terminalColorSchemes.find( app.config->theme.colorScheme );
|
||||
if ( found != app.terminalColorSchemes.end() ) {
|
||||
app.selectedColorScheme = &found->second;
|
||||
app.forEachTerminal( [&app]( UITerminal* terminal ) {
|
||||
terminal->setColorScheme( *app.selectedColorScheme );
|
||||
} );
|
||||
}
|
||||
app.savePreferences();
|
||||
} );
|
||||
panel->addChoice(
|
||||
{ "terminalScrollbarType", "terminal.appearance",
|
||||
app.i18n( "scrollbar_type", "Scrollbar Type" ),
|
||||
app.i18n( "scrollbar_type_desc",
|
||||
"Place the scrollbar over or outside terminal content." ) },
|
||||
{ app.i18n( "overlay", "Overlay" ), app.i18n( "outside", "Outside" ) },
|
||||
[&app] { return app.config->terminal.scrollBarType == ScrollViewType::Overlay ? 0 : 1; },
|
||||
[&app]( size_t selected ) {
|
||||
app.config->terminal.scrollBarType =
|
||||
selected == 0 ? ScrollViewType::Overlay : ScrollViewType::Outside;
|
||||
app.forEachTerminal( [&app]( UITerminal* terminal ) {
|
||||
terminal->setScrollViewType( app.config->terminal.scrollBarType );
|
||||
} );
|
||||
app.savePreferences();
|
||||
} );
|
||||
panel->addChoice(
|
||||
{ "terminalCursorStyle", "terminal.appearance", app.i18n( "cursor_style", "Cursor Style" ),
|
||||
app.i18n( "cursor_style_desc", "Choose the terminal cursor shape and animation." ) },
|
||||
{ app.i18n( "blinking_block", "Blinking Block" ),
|
||||
app.i18n( "steady_block", "Steady Block" ),
|
||||
app.i18n( "blink_underline", "Blink Underline" ),
|
||||
app.i18n( "steady_underline", "Steady Underline" ), app.i18n( "blink_bar", "Blink Bar" ),
|
||||
app.i18n( "steady_bar", "Steady Bar" ) },
|
||||
[&app] {
|
||||
static constexpr TerminalCursorMode modes[] = {
|
||||
TerminalCursorMode::BlinkingBlock, TerminalCursorMode::SteadyBlock,
|
||||
TerminalCursorMode::BlinkUnderline, TerminalCursorMode::SteadyUnderline,
|
||||
TerminalCursorMode::BlinkBar, TerminalCursorMode::SteadyBar };
|
||||
auto found = std::find( std::begin( modes ), std::end( modes ),
|
||||
app.config->terminal.cursorStyle );
|
||||
return found == std::end( modes ) ? size_t{ 0 }
|
||||
: static_cast<size_t>( found - std::begin( modes ) );
|
||||
},
|
||||
[&app]( size_t selected ) {
|
||||
static constexpr TerminalCursorMode modes[] = {
|
||||
TerminalCursorMode::BlinkingBlock, TerminalCursorMode::SteadyBlock,
|
||||
TerminalCursorMode::BlinkUnderline, TerminalCursorMode::SteadyUnderline,
|
||||
TerminalCursorMode::BlinkBar, TerminalCursorMode::SteadyBar };
|
||||
app.config->terminal.cursorStyle = modes[std::min( selected, size_t{ 5 } )];
|
||||
app.terminalConfig.cursorStyle = app.config->terminal.cursorStyle;
|
||||
app.forEachTerminal( [&app]( UITerminal* terminal ) {
|
||||
terminal->getTerm()->setCursorMode( app.config->terminal.cursorStyle );
|
||||
} );
|
||||
app.savePreferences();
|
||||
} );
|
||||
panel->addChoice(
|
||||
{ "terminalScrollbarMode", "terminal.appearance",
|
||||
app.i18n( "scrollbar_mode", "Scrollbar Mode" ),
|
||||
app.i18n( "scrollbar_mode_desc", "Control when the terminal scrollbar is visible." ) },
|
||||
{ app.i18n( "auto", "Auto" ), app.i18n( "always_visible", "Always Visible" ),
|
||||
app.i18n( "always_hidden", "Always Hidden" ) },
|
||||
[&app] {
|
||||
return app.config->terminal.scrollBarMode == ScrollBarMode::Auto ? 0
|
||||
: app.config->terminal.scrollBarMode == ScrollBarMode::AlwaysOn ? 1
|
||||
: 2;
|
||||
},
|
||||
[&app]( size_t selected ) {
|
||||
app.config->terminal.scrollBarMode = selected == 0 ? ScrollBarMode::Auto
|
||||
: selected == 1 ? ScrollBarMode::AlwaysOn
|
||||
: ScrollBarMode::AlwaysOff;
|
||||
app.forEachTerminal( [&app]( UITerminal* terminal ) {
|
||||
terminal->setVerticalScrollMode( app.config->terminal.scrollBarMode );
|
||||
} );
|
||||
app.savePreferences();
|
||||
} );
|
||||
panel->addBool( { "alwaysShowTabBar", "terminal.appearance",
|
||||
app.i18n( "always_show_tab_bar", "Always Show Tab Bar" ),
|
||||
app.i18n( "always_show_tab_bar_desc",
|
||||
"Show the tab bar even when it contains one tab." ) },
|
||||
&app.config->window.alwaysShowTabBar, [&app]( bool value ) {
|
||||
app.tabSplitter->setHideTabBarOnSingleTab( !value );
|
||||
app.savePreferences();
|
||||
} );
|
||||
|
||||
panel->build();
|
||||
settingsWindow->setKeyBindingCommand( "closeWindow", [&app] {
|
||||
if ( app.settingsWindow )
|
||||
app.settingsWindow->closeWindow();
|
||||
} );
|
||||
settingsWindow->getKeyBindings().addKeybind( { KEY_ESCAPE }, "closeWindow" );
|
||||
settingsWindow->on( Event::OnWindowClose, [&app]( const Event* ) {
|
||||
app.savePreferences();
|
||||
app.settingsWindow = nullptr;
|
||||
if ( app.tabSplitter && app.tabSplitter->getCurWidget() )
|
||||
app.tabSplitter->getCurWidget()->setFocus();
|
||||
} );
|
||||
settingsWindow->center();
|
||||
settingsWindow->showWhenReady();
|
||||
panel->focusSearch();
|
||||
return settingsWindow;
|
||||
}
|
||||
|
||||
} // namespace eterm
|
||||
@@ -0,0 +1,21 @@
|
||||
#pragma once
|
||||
|
||||
#include <eepp/core/string.hpp>
|
||||
|
||||
namespace EE::UI {
|
||||
class UIWindow;
|
||||
class UISceneNode;
|
||||
} // namespace EE::UI
|
||||
|
||||
using namespace EE;
|
||||
using namespace EE::UI;
|
||||
|
||||
namespace eterm {
|
||||
|
||||
class App;
|
||||
|
||||
struct SettingsPanel {
|
||||
static UIWindow* create( App& );
|
||||
};
|
||||
|
||||
} // namespace eterm
|
||||
Reference in New Issue
Block a user