mirror of
https://github.com/SpartanJ/eepp.git
synced 2026-10-18 11:33:23 +03:00
Merge branch 'develop' into feature/a11y
This commit is contained in:
@@ -307,6 +307,7 @@ Text selbst.</string>
|
||||
<string name="escape_sequence_tooltip">Unicode-Zeichen \, \t, \n, \r und ( ersetzen</string>
|
||||
<string name="escape_sequences">Escape-Sequenzen</string>
|
||||
<string name="exclusive_mode">Exklusivmodus</string>
|
||||
<string name="enable_exclusive_mode">Exklusivmodus aktivieren</string>
|
||||
<string name="executable_to_run">Auszuführende Programmdatei</string>
|
||||
<string name="execute_dir_in_terminal">Verzeichnis in Konsole öffnen</string>
|
||||
<string name="execute_in_terminal">In Konsole ausführen</string>
|
||||
|
||||
@@ -291,6 +291,7 @@ is hover a word that represents a color.</string>
|
||||
<string name="escape_sequence_tooltip">Replace \, \t, \n, \r and (Unicode characters</string>
|
||||
<string name="escape_sequences">Escape Sequences</string>
|
||||
<string name="exclusive_mode">Exclusive Mode</string>
|
||||
<string name="enable_exclusive_mode">Enable Exclusive Mode</string>
|
||||
<string name="executable_to_run">Executable to Run</string>
|
||||
<string name="execute_dir_in_terminal">Open directory in terminal</string>
|
||||
<string name="execute_in_terminal">Execute in terminal</string>
|
||||
@@ -886,14 +887,14 @@ the directory tree and in file dialogs to open a folder or file.</string>
|
||||
<string name="type">Type</string>
|
||||
<string name="type_to_locate">Type to Locate</string>
|
||||
<string name="ui_font_and_size_ellipsis">UI Font & Size...</string>
|
||||
<string name="ui_font_size">Ui Font Size</string>
|
||||
<string name="ui_font_size">UI Font Size</string>
|
||||
<string name="ui_language">UI Language</string>
|
||||
<string name="ui_multisamples_level">Multisample Anti-Aliasing Level</string>
|
||||
<string name="ui_panel_font_size">Ui Panel Font Size</string>
|
||||
<string name="ui_panel_font_size">UI Panel Font Size</string>
|
||||
<string name="ui_prefes_color_scheme">UI Prefers Color Scheme</string>
|
||||
<string name="ui_renderer">Renderer</string>
|
||||
<string name="ui_renderer_version">Renderer Version</string>
|
||||
<string name="ui_scale_factor">Ui Scale Factor</string>
|
||||
<string name="ui_scale_factor">UI Scale Factor</string>
|
||||
<string name="ui_thene">UI Theme</string>
|
||||
<string name="uicodeeditor_copy">Copy</string>
|
||||
<string name="uicodeeditor_copy_containing_folder_path">Copy Containing Folder Path</string>
|
||||
|
||||
@@ -290,6 +290,7 @@ texte lui-même.</string>
|
||||
<string name="escape_sequence_tooltip">Remplacer \, \t, \n, \r et (caractères Unicode)</string>
|
||||
<string name="escape_sequences">Séquences d'échappement</string>
|
||||
<string name="exclusive_mode">Mode exclusif</string>
|
||||
<string name="enable_exclusive_mode">Activer le mode exclusif</string>
|
||||
<string name="executable_to_run">Exécutable à exécuter</string>
|
||||
<string name="execute_dir_in_terminal">Ouvrir le répertoire dans le terminal</string>
|
||||
<string name="execute_in_terminal">Exécuter dans le terminal</string>
|
||||
|
||||
@@ -223,6 +223,7 @@
|
||||
,
|
||||
和 (Unicode字符</string>
|
||||
<string name="exclusive_mode">Exclusive Mode</string>
|
||||
<string name="enable_exclusive_mode">启用独占模式</string>
|
||||
<string name="execute_dir_in_terminal">在终端打开文件夹</string>
|
||||
<string name="execute_in_terminal">在终端执行</string>
|
||||
<string name="expand_all">Expand All</string>
|
||||
@@ -680,7 +681,7 @@ file in the directory tree.</string>
|
||||
<string name="type">Type</string>
|
||||
<string name="type_to_locate">Type to Locate</string>
|
||||
<string name="ui_font_and_size_ellipsis">界面字体和大小...</string>
|
||||
<string name="ui_font_size">Ui字体大小</string>
|
||||
<string name="ui_font_size">UI字体大小</string>
|
||||
<string name="ui_language">界面语言</string>
|
||||
<string name="ui_multisamples_level">多重采样抗锯齿级别</string>
|
||||
<string name="ui_panel_font_size">界面面板字体大小</string>
|
||||
|
||||
@@ -96,6 +96,7 @@
|
||||
#include <eepp/ui/tools/uidocfindreplace.hpp>
|
||||
#include <eepp/ui/tools/uifontpickerdialog.hpp>
|
||||
#include <eepp/ui/tools/uiimageviewer.hpp>
|
||||
#include <eepp/ui/tools/uisettingspanel.hpp>
|
||||
#include <eepp/ui/tools/uitabwidgetsplitter.hpp>
|
||||
#include <eepp/ui/tools/uiwidgetinspector.hpp>
|
||||
#include <eepp/ui/uiapplication.hpp>
|
||||
|
||||
@@ -27,7 +27,7 @@ class EE_API StyleSheetSelector {
|
||||
|
||||
bool hasPseudoClasses() const;
|
||||
|
||||
std::vector<UIWidget*> getRelatedElements( UIWidget* element, bool applyPseudo = true ) const;
|
||||
SmallVector<UIWidget*, 8> getRelatedElements( UIWidget* element, bool applyPseudo = true ) const;
|
||||
|
||||
bool isStructurallyVolatile() const;
|
||||
|
||||
|
||||
@@ -123,7 +123,7 @@ template <typename T> class UIProperty {
|
||||
}
|
||||
|
||||
/** @return All currently connected widgets, or an empty vector after expiration. */
|
||||
std::vector<UIWidget*> widgets() const {
|
||||
SmallVector<UIWidget*, 8> widgets() const {
|
||||
auto state = mState.lock();
|
||||
if ( !state || !state->property || !state->property->databind().isInitialized() )
|
||||
return {};
|
||||
|
||||
@@ -0,0 +1,221 @@
|
||||
#ifndef EE_UI_TOOLS_UISETTINGSPANEL_HPP
|
||||
#define EE_UI_TOOLS_UISETTINGSPANEL_HPP
|
||||
|
||||
#include <eepp/core/string.hpp>
|
||||
#include <eepp/ui/uilinearlayout.hpp>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <variant>
|
||||
#include <vector>
|
||||
|
||||
namespace pugi {
|
||||
class xml_node;
|
||||
}
|
||||
|
||||
namespace EE::UI {
|
||||
class UICheckBox;
|
||||
}
|
||||
|
||||
namespace EE::UI::Tools {
|
||||
|
||||
struct SettingView;
|
||||
|
||||
struct EE_API SettingDescriptor {
|
||||
std::string id;
|
||||
std::string category;
|
||||
String name;
|
||||
String description;
|
||||
String group;
|
||||
};
|
||||
|
||||
struct EE_API BoolPointerSetting {
|
||||
bool* value{ nullptr };
|
||||
std::function<void( bool )> apply;
|
||||
};
|
||||
|
||||
struct EE_API BoolSetting {
|
||||
std::function<bool()> get;
|
||||
std::function<void( bool )> set;
|
||||
};
|
||||
|
||||
struct EE_API ChoiceSetting {
|
||||
std::vector<String> choices;
|
||||
std::vector<String> descriptions;
|
||||
std::function<size_t()> get;
|
||||
std::function<void( size_t )> set;
|
||||
};
|
||||
|
||||
struct EE_API EditableChoiceSetting {
|
||||
std::vector<String> choices;
|
||||
std::function<String()> get;
|
||||
std::function<bool( const String& )> set;
|
||||
};
|
||||
|
||||
struct EE_API IntegerSetting {
|
||||
int min{ 0 };
|
||||
int max{ 0 };
|
||||
std::function<int()> get;
|
||||
std::function<void( int )> set;
|
||||
};
|
||||
|
||||
struct EE_API TextSetting {
|
||||
std::function<std::string()> get;
|
||||
std::function<bool( const std::string& )> set;
|
||||
bool commitOnFocusLoss{ false };
|
||||
bool password{ false };
|
||||
};
|
||||
|
||||
struct EE_API FloatSetting {
|
||||
double min{ 0 };
|
||||
double max{ 0 };
|
||||
double step{ 0 };
|
||||
std::function<double()> get;
|
||||
std::function<void( double )> set;
|
||||
};
|
||||
|
||||
struct EE_API ActionSetting {
|
||||
String buttonText;
|
||||
std::function<void()> action;
|
||||
};
|
||||
|
||||
using SettingValue =
|
||||
std::variant<BoolPointerSetting, BoolSetting, ChoiceSetting, EditableChoiceSetting,
|
||||
IntegerSetting, TextSetting, FloatSetting, ActionSetting>;
|
||||
|
||||
struct EE_API SettingDefinition {
|
||||
SettingDescriptor descriptor;
|
||||
SettingValue value;
|
||||
bool enabled{ true };
|
||||
};
|
||||
|
||||
struct EE_API SettingsCategory {
|
||||
std::string id;
|
||||
String parent;
|
||||
String name;
|
||||
};
|
||||
|
||||
struct EE_API SettingsGroup {
|
||||
std::string category;
|
||||
String name;
|
||||
size_t beforeSetting{ 0 };
|
||||
};
|
||||
|
||||
class EE_API SettingsModel {
|
||||
public:
|
||||
void clear();
|
||||
|
||||
bool addCategory( SettingsCategory category );
|
||||
|
||||
bool addGroup( SettingsGroup group );
|
||||
|
||||
bool addSetting( SettingDefinition setting );
|
||||
|
||||
bool hasCategory( const std::string& id ) const;
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
class EE_API UISettingsPanel : public UILinearLayout {
|
||||
public:
|
||||
static UISettingsPanel* New( UIWidget* parent );
|
||||
|
||||
virtual ~UISettingsPanel();
|
||||
|
||||
SettingsModel& getModel();
|
||||
|
||||
const SettingsModel& getModel() const;
|
||||
|
||||
bool addCategory( std::string id, String parent, String name );
|
||||
|
||||
bool addGroup( std::string category, String name );
|
||||
|
||||
bool addBool( SettingDescriptor descriptor, bool* value,
|
||||
std::function<void( bool )> apply = {} );
|
||||
|
||||
bool addBool( SettingDescriptor descriptor, std::function<bool()> get,
|
||||
std::function<void( bool )> set );
|
||||
|
||||
bool addChoice( SettingDescriptor descriptor, std::vector<String> choices,
|
||||
std::function<size_t()> get, std::function<void( size_t )> set,
|
||||
std::vector<String> choiceDescriptions = {} );
|
||||
|
||||
bool addEditableChoice( SettingDescriptor descriptor, std::vector<String> choices,
|
||||
std::function<String()> get, std::function<bool( const String& )> set );
|
||||
|
||||
bool addInteger( SettingDescriptor descriptor, int min, int max, std::function<int()> get,
|
||||
std::function<void( int )> set );
|
||||
|
||||
bool addText( SettingDescriptor descriptor, std::function<std::string()> get,
|
||||
std::function<bool( const std::string& )> set, bool commitOnFocusLoss = false );
|
||||
|
||||
bool addFloat( SettingDescriptor descriptor, double min, double max, double step,
|
||||
std::function<double()> get, std::function<void( double )> set );
|
||||
|
||||
bool addAction( SettingDescriptor descriptor, String buttonText, std::function<void()> action );
|
||||
|
||||
void build();
|
||||
|
||||
void selectCategory( const std::string& category );
|
||||
|
||||
void setCategoryEnabled( const std::string& category, bool enabled,
|
||||
const std::string& excludedSetting = {} );
|
||||
|
||||
void refreshTextSetting( const std::string& id );
|
||||
|
||||
void setSearchResultsText( String text );
|
||||
|
||||
void setFilter( String filter );
|
||||
|
||||
void focusSearch();
|
||||
|
||||
void focusCategories();
|
||||
|
||||
bool isBuilt() const;
|
||||
|
||||
protected:
|
||||
struct Impl;
|
||||
std::unique_ptr<Impl> mImpl;
|
||||
|
||||
explicit UISettingsPanel( UIWidget* parent );
|
||||
|
||||
void selectCategory( Impl& panel, const std::string& category );
|
||||
|
||||
void addCategory( Impl& panel, const std::string& id, const String& parent,
|
||||
const String& name );
|
||||
|
||||
void addSubcategoryHeading( Impl& panel, const std::string& category, const String& name );
|
||||
|
||||
void setupCategories( Impl& panel );
|
||||
|
||||
UIWidget* createRow( Impl& panel, SettingDefinition& setting, SettingView& view,
|
||||
pugi::xml_node layout );
|
||||
|
||||
UICheckBox* createBoolControl( Impl& panel, SettingDefinition& setting, SettingView& view );
|
||||
|
||||
void materializeCategory( Impl& panel, const std::string& category );
|
||||
|
||||
void materializeVisibleSettings( Impl& panel, const String& query );
|
||||
|
||||
void setCategoryEnabled( Impl& panel, const std::string& category, bool enabled,
|
||||
const std::string& excludedSetting = {} );
|
||||
|
||||
void refreshTextSetting( Impl& panel, const std::string& id );
|
||||
|
||||
void filter( Impl& panel );
|
||||
};
|
||||
|
||||
} // namespace EE::UI::Tools
|
||||
|
||||
#endif
|
||||
@@ -1516,8 +1516,14 @@ class EE_API UINode : public Node {
|
||||
/** @brief Get a widget's computed absolute font size in pixels. */
|
||||
Float getAbsoluteFontSize( const UIWidget* widget ) const;
|
||||
|
||||
/** Returns true if the node is currently being created, this state is not used by all node
|
||||
* types.
|
||||
*/
|
||||
bool isCreatingNode() const;
|
||||
|
||||
/** Forces a left mouse click event over the node */
|
||||
void click();
|
||||
|
||||
protected:
|
||||
Vector2f mDpPos;
|
||||
Sizef mDpSize;
|
||||
|
||||
@@ -864,7 +864,8 @@ class EE_API UISceneNode : public SceneNode {
|
||||
* @param marker Marker for style association.
|
||||
* @return Vector of root widgets created.
|
||||
*/
|
||||
std::vector<UIWidget*> loadNode( pugi::xml_node node, Node* parent, const Uint32& marker = 0 );
|
||||
SmallVector<UIWidget*, 8> loadNode( pugi::xml_node node, Node* parent,
|
||||
const Uint32& marker = 0 );
|
||||
|
||||
/** Sets the document / scene URI used to resolve paths of inner elements */
|
||||
void setURI( const URI& uri );
|
||||
|
||||
@@ -26,6 +26,7 @@ class UITooltip;
|
||||
class UIStyle;
|
||||
class AccessibilityWidgetResolver;
|
||||
struct AccessibilityProperties;
|
||||
class UIWidget;
|
||||
|
||||
struct MarginAuto {
|
||||
static constexpr auto Left = ( 1 << 0 );
|
||||
@@ -34,6 +35,8 @@ struct MarginAuto {
|
||||
static constexpr auto Bottom = ( 1 << 3 );
|
||||
};
|
||||
|
||||
using WidgetQueryResult = SmallVector<UIWidget*, 8>;
|
||||
|
||||
/**
|
||||
* @brief Base class for all UI widgets in the eepp framework.
|
||||
*
|
||||
@@ -1153,7 +1156,7 @@ class EE_API UIWidget : public UINode {
|
||||
* @param className The CSS class to search for.
|
||||
* @return Vector of matching UIWidget pointers.
|
||||
*/
|
||||
std::vector<UIWidget*> findAllByClass( const std::string& className );
|
||||
WidgetQueryResult findAllByClass( const std::string& className );
|
||||
|
||||
/**
|
||||
* @brief Finds all widgets by CSS tag.
|
||||
@@ -1163,7 +1166,7 @@ class EE_API UIWidget : public UINode {
|
||||
* @param tag The CSS tag to search for.
|
||||
* @return Vector of matching UIWidget pointers.
|
||||
*/
|
||||
std::vector<UIWidget*> findAllByTag( const std::string& tag );
|
||||
WidgetQueryResult findAllByTag( const std::string& tag );
|
||||
|
||||
/**
|
||||
* @brief Finds a widget by CSS class.
|
||||
@@ -1225,7 +1228,7 @@ class EE_API UIWidget : public UINode {
|
||||
* @param selector The CSS selector to use.
|
||||
* @return Vector of all matching UIWidget pointers.
|
||||
*/
|
||||
std::vector<UIWidget*> querySelectorAll( const CSS::StyleSheetSelector& selector );
|
||||
WidgetQueryResult querySelectorAll( const CSS::StyleSheetSelector& selector );
|
||||
|
||||
/**
|
||||
* @brief Queries all widgets using a CSS selector string.
|
||||
@@ -1235,7 +1238,7 @@ class EE_API UIWidget : public UINode {
|
||||
* @param selector The CSS selector string to use.
|
||||
* @return Vector of all matching UIWidget pointers.
|
||||
*/
|
||||
std::vector<UIWidget*> querySelectorAll( const std::string& selector );
|
||||
WidgetQueryResult querySelectorAll( const std::string& selector );
|
||||
|
||||
/**
|
||||
* @brief Gets a property value as a string.
|
||||
|
||||
@@ -7,6 +7,10 @@ namespace EE { namespace Window {
|
||||
|
||||
class EE_API Clipboard {
|
||||
public:
|
||||
using Data = std::vector<Uint8>;
|
||||
using DataCallback = std::function<const void*( const char* mimeType, size_t* size )>;
|
||||
using CleanupCallback = std::function<void()>;
|
||||
|
||||
virtual ~Clipboard();
|
||||
|
||||
/** @return The Clipboard Text if available */
|
||||
@@ -30,6 +34,29 @@ class EE_API Clipboard {
|
||||
/** Set the current clipboard primary selection text */
|
||||
virtual void setPrimarySelectionText( const std::string& text ) {}
|
||||
|
||||
/**
|
||||
* Offer non-text clipboard data in one or more MIME types. The data callback is invoked lazily
|
||||
* when the operating system requests a format, and with a null MIME type when the offer is
|
||||
* cleared or replaced. The returned data must remain valid until the cleanup callback is
|
||||
* invoked. Only implemented by the SDL3 backend.
|
||||
*/
|
||||
virtual bool setData( DataCallback callback, CleanupCallback cleanup,
|
||||
const std::vector<std::string>& mimeTypes ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Clear all clipboard data. Only implemented by the SDL3 backend. */
|
||||
virtual bool clearData() { return false; }
|
||||
|
||||
/** Get clipboard data for a MIME type. Only implemented by the SDL3 backend. */
|
||||
virtual Data getData( const std::string& mimeType ) { return {}; }
|
||||
|
||||
/** @return The MIME types currently offered by the clipboard. SDL3 backend only. */
|
||||
virtual std::vector<std::string> getMimeTypes() { return {}; }
|
||||
|
||||
/** @return Whether clipboard data is available for a MIME type. SDL3 backend only. */
|
||||
virtual bool hasData( const std::string& mimeType ) const { return false; }
|
||||
|
||||
protected:
|
||||
friend class Window;
|
||||
|
||||
|
||||
@@ -237,10 +237,10 @@ bool StyleSheetSelector::selectComplex( UIWidget* element, const bool& applyPseu
|
||||
return true;
|
||||
}
|
||||
|
||||
std::vector<UIWidget*> StyleSheetSelector::getRelatedElements( UIWidget* element,
|
||||
bool applyPseudo ) const {
|
||||
static std::vector<UIWidget*> EMPTY_ELEMENTS;
|
||||
std::vector<UIWidget*> elements;
|
||||
SmallVector<UIWidget*, 8> StyleSheetSelector::getRelatedElements( UIWidget* element,
|
||||
bool applyPseudo ) const {
|
||||
static SmallVector<UIWidget*, 8> EMPTY_ELEMENTS;
|
||||
SmallVector<UIWidget*, 8> elements;
|
||||
if ( mSelectorRules.empty() )
|
||||
return elements;
|
||||
|
||||
|
||||
@@ -0,0 +1,976 @@
|
||||
#include <algorithm>
|
||||
#include <deque>
|
||||
#include <eepp/system/log.hpp>
|
||||
#include <eepp/ui/databinding/uibindinggroup.hpp>
|
||||
#include <eepp/ui/databinding/uidatabind.hpp>
|
||||
#include <eepp/ui/models/itemlistmodel.hpp>
|
||||
#include <eepp/ui/tools/uisettingspanel.hpp>
|
||||
#include <eepp/ui/uicheckbox.hpp>
|
||||
#include <eepp/ui/uicombobox.hpp>
|
||||
#include <eepp/ui/uidropdownmodellist.hpp>
|
||||
#include <eepp/ui/uipushbutton.hpp>
|
||||
#include <eepp/ui/uiscenenode.hpp>
|
||||
#include <eepp/ui/uiscrollview.hpp>
|
||||
#include <eepp/ui/uispinbox.hpp>
|
||||
#include <eepp/ui/uitextinput.hpp>
|
||||
#include <eepp/ui/uitextview.hpp>
|
||||
#include <eepp/ui/uitreeview.hpp>
|
||||
|
||||
#define PUGIXML_HEADER_ONLY
|
||||
#include <pugixml/pugixml.hpp>
|
||||
|
||||
using namespace EE::UI::Models;
|
||||
|
||||
namespace EE::UI::Tools {
|
||||
|
||||
class UISettingsCategoryModel final : public Model {
|
||||
public:
|
||||
struct Node {
|
||||
std::string id;
|
||||
std::string text;
|
||||
Node* parent{ nullptr };
|
||||
std::vector<Node*> children;
|
||||
std::vector<Node*> visibleChildren;
|
||||
};
|
||||
|
||||
static std::shared_ptr<UISettingsCategoryModel>
|
||||
create( const std::vector<std::pair<std::string, std::vector<std::string>>>& categories,
|
||||
const UnorderedMap<std::string, std::string>& ids ) {
|
||||
return std::make_shared<UISettingsCategoryModel>( categories, ids );
|
||||
}
|
||||
|
||||
UISettingsCategoryModel(
|
||||
const std::vector<std::pair<std::string, std::vector<std::string>>>& categories,
|
||||
const UnorderedMap<std::string, std::string>& ids ) {
|
||||
mNodes.emplace_back();
|
||||
mRoot = &mNodes.back();
|
||||
for ( const auto& [parent, children] : categories ) {
|
||||
std::string parentId;
|
||||
if ( !children.empty() ) {
|
||||
auto id = ids.find( parent + '/' + children.front() );
|
||||
if ( id != ids.end() ) {
|
||||
auto separator = id->second.find( '.' );
|
||||
parentId = id->second.substr( 0, separator ) + ".*";
|
||||
}
|
||||
}
|
||||
mNodes.push_back( { std::move( parentId ), parent, mRoot } );
|
||||
auto* parentNode = &mNodes.back();
|
||||
mRoot->children.push_back( parentNode );
|
||||
for ( const auto& child : children ) {
|
||||
auto id = ids.find( parent + '/' + child );
|
||||
mNodes.push_back(
|
||||
{ id == ids.end() ? std::string{} : id->second, child, parentNode } );
|
||||
parentNode->children.push_back( &mNodes.back() );
|
||||
}
|
||||
}
|
||||
filter( {}, {} );
|
||||
}
|
||||
|
||||
size_t rowCount( const ModelIndex& parent = {} ) const {
|
||||
auto* node = parent.isValid() ? static_cast<Node*>( parent.internalData() ) : mRoot;
|
||||
return node->visibleChildren.size();
|
||||
}
|
||||
|
||||
size_t columnCount( const ModelIndex& = {} ) const { return 1; }
|
||||
|
||||
ModelIndex index( int row, int column, const ModelIndex& parent = {} ) const {
|
||||
auto* node = parent.isValid() ? static_cast<Node*>( parent.internalData() ) : mRoot;
|
||||
if ( row < 0 || column != 0 || static_cast<size_t>( row ) >= node->visibleChildren.size() )
|
||||
return {};
|
||||
return createIndex( row, column, node->visibleChildren[row] );
|
||||
}
|
||||
|
||||
ModelIndex parentIndex( const ModelIndex& index ) const {
|
||||
if ( !index.isValid() )
|
||||
return {};
|
||||
auto* node = static_cast<Node*>( index.internalData() );
|
||||
if ( !node->parent || node->parent == mRoot )
|
||||
return {};
|
||||
auto* parent = node->parent;
|
||||
auto found =
|
||||
std::find( mRoot->visibleChildren.begin(), mRoot->visibleChildren.end(), parent );
|
||||
return found == mRoot->visibleChildren.end()
|
||||
? ModelIndex{}
|
||||
: createIndex( std::distance( mRoot->visibleChildren.begin(), found ), 0,
|
||||
parent );
|
||||
}
|
||||
|
||||
Variant data( const ModelIndex& index, ModelRole role = ModelRole::Display ) const {
|
||||
if ( !index.isValid() || role != ModelRole::Display )
|
||||
return {};
|
||||
return Variant( static_cast<Node*>( index.internalData() )->text );
|
||||
}
|
||||
|
||||
void filter( const std::string_view query,
|
||||
const UnorderedSet<std::string>& matchingCategories ) {
|
||||
mRoot->visibleChildren.clear();
|
||||
for ( auto* parent : mRoot->children ) {
|
||||
parent->visibleChildren.clear();
|
||||
const bool parentMatches = query.empty() || String::icontains( parent->text, query );
|
||||
for ( auto* child : parent->children ) {
|
||||
if ( parentMatches || String::icontains( child->text, query ) ||
|
||||
matchingCategories.contains( child->id ) )
|
||||
parent->visibleChildren.push_back( child );
|
||||
}
|
||||
if ( !parent->visibleChildren.empty() )
|
||||
mRoot->visibleChildren.push_back( parent );
|
||||
}
|
||||
invalidate( Model::UpdateFlag::InvalidateAllIndexes );
|
||||
}
|
||||
|
||||
private:
|
||||
std::deque<Node> mNodes;
|
||||
Node* mRoot{ nullptr };
|
||||
};
|
||||
|
||||
static constexpr const char* SETTINGS_PANEL_LAYOUT = R"xml(
|
||||
<style>
|
||||
<![CDATA[
|
||||
.settings_panel #settings_sidebar {
|
||||
background-color: var(--list-back);
|
||||
padding: 10dp 8dp 8dp 8dp;
|
||||
}
|
||||
.settings_panel #settings_filter {
|
||||
margin-bottom: 10dp;
|
||||
}
|
||||
.settings_panel #settings_categories * {
|
||||
focusable: false;
|
||||
}
|
||||
.settings_panel #settings_categories {
|
||||
background-color: var(--list-back);
|
||||
}
|
||||
.settings_panel #settings_categories treeview::row {
|
||||
border-left: 0dp solid var(--primary);
|
||||
transition: border-left-width 0.1s;
|
||||
}
|
||||
.settings_panel #settings_categories treeview::row:selected {
|
||||
background-color: var(--tab-hover);
|
||||
border-left: 2dp solid var(--primary);
|
||||
}
|
||||
.settings_panel #settings_categories treeview::row:selected treeview::cell {
|
||||
color: var(--font);
|
||||
}
|
||||
.settings_panel #settings_rows {
|
||||
max-width: 820dp;
|
||||
padding: 20dp 28dp 28dp 28dp;
|
||||
layout-gravity: center_horizontal;
|
||||
}
|
||||
.settings_panel #settings_page_title {
|
||||
font-size: 18dp;
|
||||
margin-bottom: 18dp;
|
||||
font-weight: bold;
|
||||
}
|
||||
.settings_panel .settings_category_heading {
|
||||
font-size: 14dp;
|
||||
font-weight: bold;
|
||||
margin: 12dp 0dp 6dp 0dp;
|
||||
padding-bottom: 6dp;
|
||||
border-bottom: 1dp solid var(--tab-line);
|
||||
}
|
||||
.settings_panel .settings_subcategory_heading {
|
||||
font-size: 12dp;
|
||||
font-weight: bold;
|
||||
margin: 18dp 4dp 3dp 4dp;
|
||||
padding-bottom: 6dp;
|
||||
border-bottom: 1dp solid var(--tab-line);
|
||||
}
|
||||
.settings_panel .settings_option {
|
||||
border-bottom: 1dp solid var(--disabled-border);
|
||||
padding: 9dp 4dp 11dp 4dp;
|
||||
margin-bottom: 0dp;
|
||||
}
|
||||
.settings_panel .settings_option:disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
.settings_panel .settings_option_name {
|
||||
font-style: normal;
|
||||
}
|
||||
.settings_panel .settings_option_description {
|
||||
color: var(--disabled-color);
|
||||
font-size: 10dp;
|
||||
margin-top: 3dp;
|
||||
word-wrap: true;
|
||||
font-style: normal;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.settings_panel .settings_option_name,
|
||||
.settings_panel .settings_option_description {
|
||||
font-style: shadow;
|
||||
}
|
||||
.settings_panel .settings_option_description {
|
||||
color: var(--font-hint);
|
||||
}
|
||||
}
|
||||
.settings_panel .settings_boolean_option #setting_info,
|
||||
.settings_panel .settings_boolean_option .settings_option_name,
|
||||
.settings_panel .settings_boolean_option .settings_option_description {
|
||||
cursor: pointer;
|
||||
}
|
||||
.settings_panel .settings_option_control {
|
||||
layout-width: 210dp;
|
||||
gravity: right|center_vertical;
|
||||
margin-left: 20dp;
|
||||
layout-gravity: center_vertical;
|
||||
}
|
||||
.settings_panel .settings_bool,
|
||||
.settings_panel .settings_action {
|
||||
layout-width: wrap_content;
|
||||
layout-height: wrap_content;
|
||||
}
|
||||
.settings_panel .settings_bool {
|
||||
check-mode: button;
|
||||
}
|
||||
.settings_panel .settings_choice {
|
||||
layout-width: 190dp;
|
||||
layout-height: wrap_content;
|
||||
}
|
||||
.settings_panel .settings_editable_choice {
|
||||
layout-width: 210dp;
|
||||
layout-height: wrap_content;
|
||||
}
|
||||
.settings_panel .settings_text {
|
||||
layout-width: 210dp;
|
||||
layout-height: wrap_content;
|
||||
}
|
||||
.settings_panel .settings_text.error {
|
||||
border-color: var(--theme-error);
|
||||
}
|
||||
.settings_panel .settings_integer {
|
||||
layout-width: 110dp;
|
||||
layout-height: wrap_content;
|
||||
}
|
||||
]]>
|
||||
</style>
|
||||
<vbox lw="mp" lh="mp" class="settings_panel">
|
||||
<Splitter id="settings_splitter" lw="mp" lh="mp" orientation="horizontal" splitter-partition="220dp">
|
||||
<vbox id="settings_sidebar" lw="0" lh="0" min-width="160dp">
|
||||
<TextInput id="settings_filter" lw="mp" lh="wc" hint="@string(search_settings, Search settings...)" />
|
||||
<TreeView id="settings_categories" lw="mp" lh="o" lw8="1" />
|
||||
</vbox>
|
||||
<ScrollView id="settings_scroll" lw="0" lw8="1" lh="mp" focusable="false">
|
||||
<vbox id="settings_rows" lw="mp" lh="wc">
|
||||
<TextView id="settings_page_title" lw="mp" lh="wc" focusable="false" />
|
||||
</vbox>
|
||||
</ScrollView>
|
||||
</Splitter>
|
||||
</vbox>
|
||||
)xml";
|
||||
|
||||
static std::string settingsRowLayout( std::string_view control ) {
|
||||
return R"xml(
|
||||
<vbox lw="mp" lh="wc" class="settings_option">
|
||||
<hbox lw="mp" lh="wc" class="settings_option_content">
|
||||
<vbox id="setting_info" lw="0" lw8="1" lh="wc">
|
||||
<TextView id="setting_name" lw="mp" lh="wc" class="settings_option_name" focusable="false" />
|
||||
<TextView id="setting_description" lw="mp" lh="wc" class="settings_option_description" focusable="false" />
|
||||
</vbox>
|
||||
<hbox id="setting_control" lw="wc" lh="wc" class="settings_option_control">
|
||||
)xml" + std::string( control ) +
|
||||
R"xml(
|
||||
</hbox>
|
||||
</hbox>
|
||||
</vbox>
|
||||
)xml";
|
||||
}
|
||||
|
||||
class SettingsLayoutTemplate {
|
||||
public:
|
||||
explicit SettingsLayoutTemplate( const std::string& layout ) {
|
||||
[[maybe_unused]] auto result =
|
||||
mDocument.load_string( layout.c_str(), pugi::parse_default | pugi::parse_ws_pcdata );
|
||||
eeASSERT( result );
|
||||
}
|
||||
|
||||
pugi::xml_node root() const { return mDocument.first_child(); }
|
||||
|
||||
private:
|
||||
pugi::xml_document mDocument;
|
||||
};
|
||||
|
||||
static constexpr const char* SETTINGS_CATEGORY_HEADING_LAYOUT = R"xml(
|
||||
<vbox lw="mp" lh="wc" visible="false">
|
||||
<TextView id="settings_category_heading" lw="mp" lh="wc" class="settings_category_heading" visible="false" focusable="false" />
|
||||
<vbox id="settings_category_rows" lw="mp" lh="wc" />
|
||||
</vbox>
|
||||
)xml";
|
||||
static constexpr const char* SETTINGS_SUBCATEGORY_HEADING_LAYOUT = R"xml(
|
||||
<TextView lw="mp" lh="wc" class="settings_subcategory_heading" visible="false" focusable="false" />
|
||||
)xml";
|
||||
static const SettingsLayoutTemplate SETTINGS_BOOL_ROW_LAYOUT( settingsRowLayout(
|
||||
R"xml(<CheckBox id="setting_control_widget" class="settings_bool" />)xml" ) );
|
||||
static const SettingsLayoutTemplate SETTINGS_CHOICE_ROW_LAYOUT( settingsRowLayout(
|
||||
R"xml(<DropDownModelList id="setting_control_widget" class="settings_choice" />)xml" ) );
|
||||
static const SettingsLayoutTemplate SETTINGS_EDITABLE_CHOICE_ROW_LAYOUT( settingsRowLayout(
|
||||
R"xml(<ComboBox id="setting_control_widget" class="settings_editable_choice" popup-to-root="true" />)xml" ) );
|
||||
static const SettingsLayoutTemplate SETTINGS_INTEGER_ROW_LAYOUT( settingsRowLayout(
|
||||
R"xml(<SpinBox id="setting_control_widget" class="settings_integer" />)xml" ) );
|
||||
static const SettingsLayoutTemplate SETTINGS_TEXT_ROW_LAYOUT( settingsRowLayout(
|
||||
R"xml(<TextInput id="setting_control_widget" class="settings_text" />)xml" ) );
|
||||
static const SettingsLayoutTemplate SETTINGS_ACTION_ROW_LAYOUT( settingsRowLayout(
|
||||
R"xml(<PushButton id="setting_control_widget" class="settings_action" />)xml" ) );
|
||||
|
||||
static void disableTabFocusTree( Node* node ) {
|
||||
if ( node->isWidget() )
|
||||
node->asType<UIWidget>()->unsetTabFocusable();
|
||||
for ( auto* child = node->getFirstChild(); child; child = child->getNextNode() )
|
||||
disableTabFocusTree( child );
|
||||
}
|
||||
|
||||
struct SettingView {
|
||||
UIWidget* row{ nullptr };
|
||||
};
|
||||
|
||||
struct SubcategoryHeading {
|
||||
std::string category;
|
||||
String name;
|
||||
UITextView* heading{ nullptr };
|
||||
};
|
||||
|
||||
struct UISettingsPanel::Impl {
|
||||
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::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;
|
||||
String searchResultsText{ "Search Results" };
|
||||
bool built{ false };
|
||||
};
|
||||
|
||||
void SettingsModel::clear() {
|
||||
mCategories.clear();
|
||||
mGroups.clear();
|
||||
mSettings.clear();
|
||||
}
|
||||
|
||||
bool SettingsModel::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 SettingsModel::addGroup( SettingsGroup group ) {
|
||||
if ( !hasCategory( group.category ) )
|
||||
return false;
|
||||
mGroups.emplace_back( std::move( group ) );
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SettingsModel::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 SettingsModel::hasCategory( const std::string& id ) const {
|
||||
return std::any_of( mCategories.begin(), mCategories.end(),
|
||||
[&id]( const auto& item ) { return item.id == id; } );
|
||||
}
|
||||
|
||||
UISettingsPanel* UISettingsPanel::New( UIWidget* parent ) {
|
||||
return eeNew( UISettingsPanel, ( parent ) );
|
||||
}
|
||||
|
||||
UISettingsPanel::UISettingsPanel( UIWidget* parent ) :
|
||||
UILinearLayout( "settingspanel", UIOrientation::Vertical ), mImpl( std::make_unique<Impl>() ) {
|
||||
setParent( parent );
|
||||
setLayoutSizePolicy( SizePolicy::MatchParent, SizePolicy::MatchParent );
|
||||
auto* layout = getUISceneNode()->loadLayoutFromString( SETTINGS_PANEL_LAYOUT, this );
|
||||
mImpl->search = layout->find<UITextInput>( "settings_filter" );
|
||||
mImpl->categories = layout->find<UITreeView>( "settings_categories" );
|
||||
mImpl->settings = layout->find<UILinearLayout>( "settings_rows" );
|
||||
mImpl->pageTitle = layout->find<UITextView>( "settings_page_title" );
|
||||
mImpl->scroll = layout->find<UIScrollView>( "settings_scroll" );
|
||||
mImpl->scroll->setVerticalScrollMode( ScrollBarMode::Auto );
|
||||
mImpl->scroll->setHorizontalScrollMode( ScrollBarMode::AlwaysOff );
|
||||
disableTabFocusTree( mImpl->categories->getVerticalScrollBar() );
|
||||
disableTabFocusTree( mImpl->categories->getHorizontalScrollBar() );
|
||||
disableTabFocusTree( mImpl->scroll->getVerticalScrollBar() );
|
||||
disableTabFocusTree( mImpl->scroll->getHorizontalScrollBar() );
|
||||
}
|
||||
|
||||
UISettingsPanel::~UISettingsPanel() = default;
|
||||
|
||||
SettingsModel& UISettingsPanel::getModel() {
|
||||
return mImpl->model;
|
||||
}
|
||||
|
||||
const SettingsModel& UISettingsPanel::getModel() const {
|
||||
return mImpl->model;
|
||||
}
|
||||
|
||||
bool UISettingsPanel::addCategory( std::string id, String parent, String name ) {
|
||||
if ( mImpl->built || !mImpl->model.addCategory( { id, parent, name } ) )
|
||||
return false;
|
||||
addCategory( *mImpl, id, parent, name );
|
||||
return true;
|
||||
}
|
||||
|
||||
bool UISettingsPanel::addGroup( std::string category, String name ) {
|
||||
if ( mImpl->built )
|
||||
return false;
|
||||
return mImpl->model.addGroup(
|
||||
{ std::move( category ), std::move( name ), mImpl->model.settings().size() } );
|
||||
}
|
||||
|
||||
bool UISettingsPanel::addBool( SettingDescriptor descriptor, bool* value,
|
||||
std::function<void( bool )> apply ) {
|
||||
return !mImpl->built &&
|
||||
mImpl->model.addSetting(
|
||||
{ std::move( descriptor ), BoolPointerSetting{ value, std::move( apply ) } } );
|
||||
}
|
||||
|
||||
bool UISettingsPanel::addBool( SettingDescriptor descriptor, std::function<bool()> get,
|
||||
std::function<void( bool )> set ) {
|
||||
return !mImpl->built &&
|
||||
mImpl->model.addSetting(
|
||||
{ std::move( descriptor ), BoolSetting{ std::move( get ), std::move( set ) } } );
|
||||
}
|
||||
|
||||
bool UISettingsPanel::addChoice( SettingDescriptor descriptor, std::vector<String> choices,
|
||||
std::function<size_t()> get, std::function<void( size_t )> set,
|
||||
std::vector<String> choiceDescriptions ) {
|
||||
return !mImpl->built &&
|
||||
mImpl->model.addSetting(
|
||||
{ std::move( descriptor ),
|
||||
ChoiceSetting{ std::move( choices ), std::move( choiceDescriptions ),
|
||||
std::move( get ), std::move( set ) } } );
|
||||
}
|
||||
|
||||
bool UISettingsPanel::addEditableChoice( SettingDescriptor descriptor, std::vector<String> choices,
|
||||
std::function<String()> get,
|
||||
std::function<bool( const String& )> set ) {
|
||||
return !mImpl->built &&
|
||||
mImpl->model.addSetting( { std::move( descriptor ),
|
||||
EditableChoiceSetting{ std::move( choices ), std::move( get ),
|
||||
std::move( set ) } } );
|
||||
}
|
||||
|
||||
bool UISettingsPanel::addInteger( SettingDescriptor descriptor, int min, int max,
|
||||
std::function<int()> get, std::function<void( int )> set ) {
|
||||
return !mImpl->built && min <= max &&
|
||||
mImpl->model.addSetting(
|
||||
{ std::move( descriptor ),
|
||||
IntegerSetting{ min, max, std::move( get ), std::move( set ) } } );
|
||||
}
|
||||
|
||||
bool UISettingsPanel::addText( SettingDescriptor descriptor, std::function<std::string()> get,
|
||||
std::function<bool( const std::string& )> set,
|
||||
bool commitOnFocusLoss ) {
|
||||
return !mImpl->built &&
|
||||
mImpl->model.addSetting(
|
||||
{ std::move( descriptor ),
|
||||
TextSetting{ std::move( get ), std::move( set ), commitOnFocusLoss } } );
|
||||
}
|
||||
|
||||
bool UISettingsPanel::addFloat( SettingDescriptor descriptor, double min, double max, double step,
|
||||
std::function<double()> get, std::function<void( double )> set ) {
|
||||
return !mImpl->built && min <= max && step > 0 &&
|
||||
mImpl->model.addSetting(
|
||||
{ std::move( descriptor ),
|
||||
FloatSetting{ min, max, step, std::move( get ), std::move( set ) } } );
|
||||
}
|
||||
|
||||
bool UISettingsPanel::addAction( SettingDescriptor descriptor, String buttonText,
|
||||
std::function<void()> action ) {
|
||||
return !mImpl->built && action &&
|
||||
mImpl->model.addSetting(
|
||||
{ std::move( descriptor ),
|
||||
ActionSetting{ std::move( buttonText ), std::move( action ) } } );
|
||||
}
|
||||
|
||||
void UISettingsPanel::build() {
|
||||
if ( mImpl->built )
|
||||
return;
|
||||
mImpl->built = true;
|
||||
mImpl->settings->beginAttributesTransaction();
|
||||
materializeCategory( *mImpl, mImpl->selectedCategory );
|
||||
setupCategories( *mImpl );
|
||||
mImpl->settings->endAttributesTransaction();
|
||||
selectCategory( *mImpl, mImpl->selectedCategory );
|
||||
mImpl->connections += mImpl->search->connect( Event::OnTextChanged, [this]( const Event* ) {
|
||||
String query = mImpl->search->getText();
|
||||
query.trim();
|
||||
const UintPtr debounceTag = reinterpret_cast<UintPtr>( this );
|
||||
if ( query.size() < 2 ) {
|
||||
mImpl->search->removeActionsByTag( debounceTag );
|
||||
filter( *mImpl );
|
||||
return;
|
||||
}
|
||||
mImpl->search->debounce( [this] { filter( *mImpl ); }, Milliseconds( 150 ), debounceTag );
|
||||
} );
|
||||
}
|
||||
|
||||
void UISettingsPanel::selectCategory( const std::string& category ) {
|
||||
selectCategory( *mImpl, category );
|
||||
}
|
||||
|
||||
void UISettingsPanel::setCategoryEnabled( const std::string& category, bool enabled,
|
||||
const std::string& excludedSetting ) {
|
||||
setCategoryEnabled( *mImpl, category, enabled, excludedSetting );
|
||||
}
|
||||
|
||||
void UISettingsPanel::refreshTextSetting( const std::string& id ) {
|
||||
refreshTextSetting( *mImpl, id );
|
||||
}
|
||||
|
||||
void UISettingsPanel::setSearchResultsText( String text ) {
|
||||
mImpl->searchResultsText = std::move( text );
|
||||
}
|
||||
|
||||
void UISettingsPanel::setFilter( String filterText ) {
|
||||
const UintPtr debounceTag = reinterpret_cast<UintPtr>( this );
|
||||
mImpl->search->setText( std::move( filterText ) );
|
||||
mImpl->search->removeActionsByTag( debounceTag );
|
||||
filter( *mImpl );
|
||||
}
|
||||
|
||||
void UISettingsPanel::focusSearch() {
|
||||
mImpl->search->setFocus();
|
||||
mImpl->search->getDocument().selectAll();
|
||||
}
|
||||
|
||||
void UISettingsPanel::focusCategories() {
|
||||
auto selected = mImpl->categories->getSelection().first();
|
||||
if ( selected.isValid() )
|
||||
mImpl->categories->setSelection( selected );
|
||||
mImpl->categories->setFocus();
|
||||
}
|
||||
|
||||
bool UISettingsPanel::isBuilt() const {
|
||||
return mImpl->built;
|
||||
}
|
||||
|
||||
void UISettingsPanel::addCategory( Impl& panel, const std::string& id, const String& parent,
|
||||
const String& name ) {
|
||||
const auto parentText = parent.toUtf8();
|
||||
const auto nameText = name.toUtf8();
|
||||
auto parentItems =
|
||||
std::find_if( panel.categoryItems.begin(), panel.categoryItems.end(),
|
||||
[&parentText]( const auto& item ) { return item.first == parentText; } );
|
||||
if ( parentItems == panel.categoryItems.end() ) {
|
||||
panel.categoryItems.emplace_back( parentText, std::vector<std::string>{ nameText } );
|
||||
} else {
|
||||
parentItems->second.emplace_back( nameText );
|
||||
}
|
||||
panel.categoryIds[parentText + '/' + nameText] = id;
|
||||
panel.categorySearchText[id] = parent + " " + name;
|
||||
panel.categoryTitles[id] = name;
|
||||
auto* section =
|
||||
getUISceneNode()->loadLayoutFromString( SETTINGS_CATEGORY_HEADING_LAYOUT, panel.settings );
|
||||
auto* heading = section->find<UITextView>( "settings_category_heading" );
|
||||
heading->setText( name );
|
||||
heading->setId( "settings_category_" + id );
|
||||
panel.categoryHeadings[id] = heading;
|
||||
panel.categorySections[id] = section;
|
||||
panel.categoryContainers[id] = section->find<UILinearLayout>( "settings_category_rows" );
|
||||
if ( panel.selectedCategory.empty() )
|
||||
panel.selectedCategory = id;
|
||||
}
|
||||
|
||||
void UISettingsPanel::selectCategory( Impl& panel, const std::string& category ) {
|
||||
if ( category.empty() || !panel.categories )
|
||||
return;
|
||||
String title;
|
||||
if ( String::endsWith( category, ".*" ) ) {
|
||||
const std::string prefix = category.substr( 0, category.size() - 1 );
|
||||
for ( const auto& [parent, children] : panel.categoryItems ) {
|
||||
if ( children.empty() )
|
||||
continue;
|
||||
auto id = panel.categoryIds.find( parent + '/' + children.front() );
|
||||
if ( id != panel.categoryIds.end() && String::startsWith( id->second, prefix ) ) {
|
||||
title = String::fromUtf8( parent );
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if ( auto found = panel.categoryTitles.find( category );
|
||||
found != panel.categoryTitles.end() ) {
|
||||
title = found->second;
|
||||
}
|
||||
if ( title.empty() )
|
||||
return;
|
||||
panel.selectedCategory = category;
|
||||
auto index = panel.categories->findRowWithText(
|
||||
title.toUtf8(), true, UIAbstractView::FindRowWithTextMatchKind::Equals );
|
||||
if ( index.isValid() )
|
||||
panel.categories->setSelection( index );
|
||||
filter( panel );
|
||||
panel.scroll->getVerticalScrollBar()->setValue( 0, false );
|
||||
}
|
||||
|
||||
void UISettingsPanel::addSubcategoryHeading( Impl& panel, const std::string& category,
|
||||
const String& name ) {
|
||||
panel.model.addGroup( { category, name, panel.model.settings().size() } );
|
||||
}
|
||||
|
||||
void UISettingsPanel::setupCategories( Impl& panel ) {
|
||||
auto model = UISettingsCategoryModel::create( panel.categoryItems, panel.categoryIds );
|
||||
panel.categoryModel = model;
|
||||
panel.categories->setHeadersVisible( false );
|
||||
panel.categories->setAutoExpandOnSingleColumn( true );
|
||||
panel.categories->setFocusOnSelection( true );
|
||||
panel.categories->setModel( model );
|
||||
panel.categories->expandAll();
|
||||
panel.connections +=
|
||||
panel.categories->connect( Event::OnSelectionChanged, [this, &panel]( const Event* ) {
|
||||
auto index = panel.categories->getSelection().first();
|
||||
if ( !index.isValid() )
|
||||
return;
|
||||
auto* node = static_cast<UISettingsCategoryModel::Node*>( index.internalData() );
|
||||
if ( !node || node->id.empty() )
|
||||
return;
|
||||
panel.selectedCategory = node->id;
|
||||
panel.pageTitle->setText( node->text );
|
||||
filter( panel );
|
||||
panel.scroll->getVerticalScrollBar()->setValue( 0, false );
|
||||
} );
|
||||
}
|
||||
|
||||
UIWidget* UISettingsPanel::createRow( Impl& panel, SettingDefinition& setting, SettingView& view,
|
||||
pugi::xml_node layout ) {
|
||||
auto& binding = setting.descriptor;
|
||||
auto container = panel.categoryContainers.find( binding.category );
|
||||
eeASSERT( container != panel.categoryContainers.end() );
|
||||
auto* row = getUISceneNode()->loadLayoutNodes( layout, container->second, 0 );
|
||||
row->setId( "setting_" + binding.id );
|
||||
row->find<UITextView>( "setting_name" )->setText( binding.name );
|
||||
auto* description = row->find<UITextView>( "setting_description" );
|
||||
description->setText( binding.description );
|
||||
view.row = row;
|
||||
return row;
|
||||
}
|
||||
|
||||
UICheckBox* UISettingsPanel::createBoolControl( Impl& panel, SettingDefinition& setting,
|
||||
SettingView& view ) {
|
||||
auto* row = createRow( panel, setting, view, SETTINGS_BOOL_ROW_LAYOUT.root() );
|
||||
row->addClass( "settings_boolean_option" );
|
||||
auto* check = row->find<UICheckBox>( "setting_control_widget" );
|
||||
auto toggle = [check]( const Event* event ) {
|
||||
if ( event->asMouseEvent()->getFlags() & EE_BUTTON_LMASK )
|
||||
check->setChecked( !check->isChecked() );
|
||||
};
|
||||
panel.connections +=
|
||||
row->find<UITextView>( "setting_name" )->connect( Event::MouseClick, toggle );
|
||||
panel.connections +=
|
||||
row->find<UITextView>( "setting_description" )->connect( Event::MouseClick, toggle );
|
||||
panel.connections += row->find<UILinearLayout>( "setting_info" )
|
||||
->connect( Event::MouseClick, std::move( toggle ) );
|
||||
return check;
|
||||
}
|
||||
|
||||
void UISettingsPanel::refreshTextSetting( Impl& panel, const std::string& id ) {
|
||||
const auto& settings = panel.model.settings();
|
||||
for ( size_t i = 0; i < settings.size(); ++i ) {
|
||||
if ( settings[i].descriptor.id != id || i >= panel.settingViews.size() ||
|
||||
!panel.settingViews[i].row )
|
||||
continue;
|
||||
auto* value = std::get_if<TextSetting>( &settings[i].value );
|
||||
auto* input = panel.settingViews[i].row->find<UITextInput>( "setting_control_widget" );
|
||||
if ( value && input )
|
||||
input->setText( String::fromUtf8( value->get() ) );
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
static void setNodeTreeEnabled( Node* node, bool enabled );
|
||||
|
||||
void UISettingsPanel::materializeCategory( Impl& panel, const std::string& category ) {
|
||||
if ( category.empty() || panel.materializedCategories.contains( category ) )
|
||||
return;
|
||||
auto container = panel.categoryContainers.find( category );
|
||||
if ( container == panel.categoryContainers.end() )
|
||||
return;
|
||||
auto& settings = panel.model.settings();
|
||||
panel.settingViews.resize( settings.size() );
|
||||
container->second->beginAttributesTransaction();
|
||||
for ( size_t i = 0; i < settings.size(); ++i ) {
|
||||
auto& setting = settings[i];
|
||||
if ( setting.descriptor.category != category )
|
||||
continue;
|
||||
for ( const auto& group : panel.model.groups() ) {
|
||||
if ( group.category != category || group.beforeSetting != i )
|
||||
continue;
|
||||
auto* heading =
|
||||
getUISceneNode()
|
||||
->loadLayoutFromString( SETTINGS_SUBCATEGORY_HEADING_LAYOUT, container->second )
|
||||
->asType<UITextView>();
|
||||
heading->setText( group.name );
|
||||
panel.subcategoryHeadings.push_back( { group.category, group.name, heading } );
|
||||
}
|
||||
auto& view = panel.settingViews[i];
|
||||
if ( auto* value = std::get_if<BoolPointerSetting>( &setting.value ) ) {
|
||||
auto* check = createBoolControl( panel, setting, view );
|
||||
auto binding = UIDataBind<bool>::New( value->value, check,
|
||||
UIValueConverter<bool>::converterBool() );
|
||||
binding->onValueChangeCb = value->apply;
|
||||
panel.bindingGroup += std::move( binding );
|
||||
} else if ( auto* value = std::get_if<BoolSetting>( &setting.value ) ) {
|
||||
auto* check = createBoolControl( panel, setting, view );
|
||||
check->setChecked( value->get() );
|
||||
panel.connections +=
|
||||
check->connect( Event::OnValueChange, [check, value]( const Event* ) {
|
||||
value->set( check->isChecked() );
|
||||
} );
|
||||
} else if ( auto* value = std::get_if<ChoiceSetting>( &setting.value ) ) {
|
||||
auto* row = createRow( panel, setting, view, SETTINGS_CHOICE_ROW_LAYOUT.root() );
|
||||
auto* dropDown = row->find<UIDropDownModelList>( "setting_control_widget" );
|
||||
auto model = ItemListOwnerModel<String>::create( value->choices );
|
||||
dropDown->setModel( model );
|
||||
const size_t selected = value->get();
|
||||
if ( selected < value->choices.size() ) {
|
||||
dropDown->getListView()->getSelection().set( model->index( selected, 0 ) );
|
||||
dropDown->setText( value->choices[selected] );
|
||||
}
|
||||
if ( selected < value->descriptions.size() )
|
||||
dropDown->setTooltipText( value->descriptions[selected] );
|
||||
panel.connections +=
|
||||
dropDown->connect( Event::OnValueChange, [dropDown, value]( const Event* ) {
|
||||
if ( dropDown->getListView()->getSelection().isEmpty() )
|
||||
return;
|
||||
const size_t selected = dropDown->getListView()->getSelection().first().row();
|
||||
if ( selected < value->descriptions.size() )
|
||||
dropDown->setTooltipText( value->descriptions[selected] );
|
||||
value->set( selected );
|
||||
} );
|
||||
} else if ( auto* value = std::get_if<EditableChoiceSetting>( &setting.value ) ) {
|
||||
auto* row =
|
||||
createRow( panel, setting, view, SETTINGS_EDITABLE_CHOICE_ROW_LAYOUT.root() );
|
||||
auto* combo = row->find<UIComboBox>( "setting_control_widget" );
|
||||
for ( const auto& choice : value->choices )
|
||||
combo->getListBox()->addListBoxItem( choice );
|
||||
combo->setText( value->get() );
|
||||
panel.connections +=
|
||||
combo->connect( Event::OnValueChange, [combo, value]( const Event* ) {
|
||||
if ( !value->set( combo->getText() ) ) {
|
||||
combo->addClass( "error" );
|
||||
combo->getDropDownList()->addClass( "error" );
|
||||
return;
|
||||
}
|
||||
combo->removeClass( "error" );
|
||||
combo->getDropDownList()->removeClass( "error" );
|
||||
} );
|
||||
} else if ( auto* value = std::get_if<IntegerSetting>( &setting.value ) ) {
|
||||
auto* row = createRow( panel, setting, view, SETTINGS_INTEGER_ROW_LAYOUT.root() );
|
||||
auto* spin = row->find<UISpinBox>( "setting_control_widget" );
|
||||
spin->setMinValue( value->min )->setMaxValue( value->max );
|
||||
spin->unsetTabFocusable();
|
||||
spin->getButtonPushUp()->asType<UIWidget>()->unsetTabFocusable();
|
||||
spin->getButtonPushDown()->asType<UIWidget>()->unsetTabFocusable();
|
||||
spin->setValue( value->get() );
|
||||
panel.connections +=
|
||||
spin->connect( Event::OnValueChange, [spin, value]( const Event* ) {
|
||||
value->set( static_cast<int>( spin->getValue() ) );
|
||||
} );
|
||||
} else if ( auto* value = std::get_if<TextSetting>( &setting.value ) ) {
|
||||
auto* row = createRow( panel, setting, view, SETTINGS_TEXT_ROW_LAYOUT.root() );
|
||||
auto* input = row->find<UITextInput>( "setting_control_widget" );
|
||||
if ( value->password )
|
||||
input->setMode( UITextInput::TextInputMode::Password );
|
||||
input->setText( String::fromUtf8( value->get() ) );
|
||||
auto commit = [input, value] {
|
||||
if ( !value->set( input->getText().toUtf8() ) ) {
|
||||
input->addClass( "error" );
|
||||
return;
|
||||
}
|
||||
input->removeClass( "error" );
|
||||
};
|
||||
if ( value->commitOnFocusLoss ) {
|
||||
const auto debounceTag = reinterpret_cast<Action::UniqueID>( input );
|
||||
panel.connections += input->connect(
|
||||
Event::OnTextChanged, [input, commit, debounceTag]( const Event* ) {
|
||||
input->debounce( commit, Milliseconds( 500 ), debounceTag );
|
||||
} );
|
||||
auto flush = [input, commit, debounceTag]( const Event* ) {
|
||||
input->removeActionsByTag( debounceTag );
|
||||
commit();
|
||||
};
|
||||
panel.connections += input->connect( Event::OnPressEnter, flush );
|
||||
panel.connections += input->connect( Event::OnFocusLoss, std::move( flush ) );
|
||||
} else {
|
||||
panel.connections +=
|
||||
input->connect( Event::OnTextChanged,
|
||||
[commit = std::move( commit )]( const Event* ) { commit(); } );
|
||||
}
|
||||
} else if ( auto* value = std::get_if<FloatSetting>( &setting.value ) ) {
|
||||
auto* row = createRow( panel, setting, view, SETTINGS_INTEGER_ROW_LAYOUT.root() );
|
||||
auto* spin = row->find<UISpinBox>( "setting_control_widget" );
|
||||
spin->setMinValue( value->min )->setMaxValue( value->max )->setClickStep( value->step );
|
||||
spin->allowFloatingPoint( true )->setValue( value->get() );
|
||||
spin->unsetTabFocusable();
|
||||
spin->getButtonPushUp()->asType<UIWidget>()->unsetTabFocusable();
|
||||
spin->getButtonPushDown()->asType<UIWidget>()->unsetTabFocusable();
|
||||
panel.connections +=
|
||||
spin->connect( Event::OnValueChange,
|
||||
[spin, value]( const Event* ) { value->set( spin->getValue() ); } );
|
||||
} else if ( auto* value = std::get_if<ActionSetting>( &setting.value ) ) {
|
||||
auto* row = createRow( panel, setting, view, SETTINGS_ACTION_ROW_LAYOUT.root() );
|
||||
auto* button = row->find<UIPushButton>( "setting_control_widget" );
|
||||
button->setText( value->buttonText );
|
||||
panel.connections += button->connect( Event::MouseClick, [value]( const Event* event ) {
|
||||
if ( event->asMouseEvent()->getFlags() & EE_BUTTON_LMASK )
|
||||
value->action();
|
||||
} );
|
||||
}
|
||||
if ( view.row && !setting.enabled )
|
||||
setNodeTreeEnabled( view.row, false );
|
||||
}
|
||||
container->second->endAttributesTransaction();
|
||||
panel.materializedCategories.insert( category );
|
||||
}
|
||||
|
||||
void UISettingsPanel::materializeVisibleSettings( Impl& panel, const String& query ) {
|
||||
const std::string queryUtf8 = query.toUtf8();
|
||||
const bool aggregate = String::endsWith( panel.selectedCategory, ".*" );
|
||||
const std::string aggregatePrefix =
|
||||
aggregate ? panel.selectedCategory.substr( 0, panel.selectedCategory.size() - 1 )
|
||||
: std::string{};
|
||||
for ( const auto& category : panel.model.categories() ) {
|
||||
bool materialize =
|
||||
query.empty() ? category.id == panel.selectedCategory ||
|
||||
( aggregate && String::startsWith( category.id, aggregatePrefix ) )
|
||||
: String::icontains( category.parent, query ) ||
|
||||
String::icontains( category.name, query );
|
||||
if ( !materialize && !query.empty() ) {
|
||||
for ( const auto& setting : panel.model.settings() ) {
|
||||
const auto& descriptor = setting.descriptor;
|
||||
if ( descriptor.category == category.id &&
|
||||
( String::icontains( descriptor.name, query ) ||
|
||||
String::icontains( descriptor.description, query ) ||
|
||||
String::icontains( descriptor.group, query ) ||
|
||||
String::icontains( descriptor.id, queryUtf8 ) ) ) {
|
||||
materialize = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if ( materialize )
|
||||
materializeCategory( panel, category.id );
|
||||
}
|
||||
}
|
||||
|
||||
static void setNodeTreeEnabled( Node* node, bool enabled ) {
|
||||
node->setEnabled( enabled );
|
||||
for ( Node* child = node->getFirstChild(); child; child = child->getNextNode() )
|
||||
setNodeTreeEnabled( child, enabled );
|
||||
}
|
||||
|
||||
void UISettingsPanel::setCategoryEnabled( Impl& panel, const std::string& category, bool enabled,
|
||||
const std::string& excludedSetting ) {
|
||||
auto& settings = panel.model.settings();
|
||||
for ( size_t i = 0; i < settings.size(); ++i ) {
|
||||
auto& setting = settings[i];
|
||||
const auto& descriptor = setting.descriptor;
|
||||
if ( descriptor.category != category || descriptor.id == excludedSetting )
|
||||
continue;
|
||||
setting.enabled = enabled;
|
||||
if ( i < panel.settingViews.size() && panel.settingViews[i].row )
|
||||
setNodeTreeEnabled( panel.settingViews[i].row, enabled );
|
||||
}
|
||||
}
|
||||
|
||||
void UISettingsPanel::filter( Impl& panel ) {
|
||||
String query = panel.search ? panel.search->getText() : String{};
|
||||
query.trim().toLower();
|
||||
if ( query.size() < 2 )
|
||||
query.clear();
|
||||
materializeVisibleSettings( panel, query );
|
||||
if ( !query.empty() ) {
|
||||
panel.pageTitle->setText( panel.searchResultsText );
|
||||
} else if ( auto title = panel.categoryTitles.find( panel.selectedCategory );
|
||||
title != panel.categoryTitles.end() ) {
|
||||
panel.pageTitle->setText( title->second );
|
||||
}
|
||||
UnorderedSet<std::string> matchingCategories;
|
||||
const std::string queryUtf8 = query.toUtf8();
|
||||
const bool aggregate = String::endsWith( panel.selectedCategory, ".*" );
|
||||
const std::string aggregatePrefix =
|
||||
aggregate ? panel.selectedCategory.substr( 0, panel.selectedCategory.size() - 1 )
|
||||
: std::string{};
|
||||
if ( !query.empty() ) {
|
||||
for ( const auto& setting : panel.model.settings() ) {
|
||||
const auto& binding = setting.descriptor;
|
||||
if ( String::icontains( binding.name, query ) ||
|
||||
String::icontains( binding.description, query ) ||
|
||||
String::icontains( binding.group, query ) ||
|
||||
String::icontains( binding.id, queryUtf8 ) )
|
||||
matchingCategories.insert( binding.category );
|
||||
}
|
||||
}
|
||||
if ( queryUtf8 != panel.categoryFilter ) {
|
||||
panel.categoryFilter = queryUtf8;
|
||||
if ( auto model = std::static_pointer_cast<UISettingsCategoryModel>( panel.categoryModel ) )
|
||||
model->filter( queryUtf8, matchingCategories );
|
||||
}
|
||||
panel.categories->expandAll();
|
||||
for ( auto& [category, heading] : panel.categoryHeadings )
|
||||
heading->setVisible( query.empty() && aggregate &&
|
||||
String::startsWith( category, aggregatePrefix ) );
|
||||
const auto& settings = panel.model.settings();
|
||||
for ( size_t i = 0; i < settings.size(); ++i ) {
|
||||
const auto& binding = settings[i].descriptor;
|
||||
const auto categoryName = panel.categorySearchText.find( binding.category );
|
||||
const bool categoryMatches = categoryName != panel.categorySearchText.end() &&
|
||||
String::icontains( categoryName->second, query );
|
||||
const bool matches =
|
||||
query.empty()
|
||||
? binding.category == panel.selectedCategory ||
|
||||
( aggregate && String::startsWith( binding.category, aggregatePrefix ) )
|
||||
: categoryMatches || String::icontains( binding.name, query ) ||
|
||||
String::icontains( binding.description, query ) ||
|
||||
String::icontains( binding.group, query ) ||
|
||||
String::icontains( binding.id, queryUtf8 );
|
||||
if ( i < panel.settingViews.size() && panel.settingViews[i].row )
|
||||
panel.settingViews[i].row->setVisible( matches );
|
||||
}
|
||||
for ( const auto& category : panel.model.categories() ) {
|
||||
auto section = panel.categorySections.find( category.id );
|
||||
if ( section == panel.categorySections.end() )
|
||||
continue;
|
||||
bool visible = false;
|
||||
for ( size_t i = 0; i < settings.size(); ++i ) {
|
||||
if ( settings[i].descriptor.category == category.id && i < panel.settingViews.size() &&
|
||||
panel.settingViews[i].row && panel.settingViews[i].row->isVisible() ) {
|
||||
visible = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
section->second->setVisible( visible );
|
||||
}
|
||||
for ( auto& subcategory : panel.subcategoryHeadings ) {
|
||||
if ( !subcategory.heading )
|
||||
continue;
|
||||
const bool hasVisibleSetting = std::any_of(
|
||||
settings.begin(), settings.end(), [&panel, &subcategory]( const auto& setting ) {
|
||||
const auto& binding = setting.descriptor;
|
||||
const size_t index = &setting - panel.model.settings().data();
|
||||
return binding.category == subcategory.category &&
|
||||
binding.group == subcategory.name && index < panel.settingViews.size() &&
|
||||
panel.settingViews[index].row && panel.settingViews[index].row->isVisible();
|
||||
} );
|
||||
subcategory.heading->setVisible( hasVisibleSetting );
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace EE::UI::Tools
|
||||
@@ -1905,4 +1905,10 @@ bool UINode::isCreatingNode() const {
|
||||
return mFlags & UI_CREATING_NODE;
|
||||
}
|
||||
|
||||
void UINode::click() {
|
||||
Vector2f pos;
|
||||
nodeToWorld( pos );
|
||||
getEventDispatcher()->sendMouseClick( this, pos.ceil().asInt(), EE_BUTTON_LMASK );
|
||||
}
|
||||
|
||||
}} // namespace EE::UI
|
||||
|
||||
@@ -548,12 +548,12 @@ bool UISceneNode::windowExists( UIWindow* win ) {
|
||||
return mWindowsList.end() != std::find( mWindowsList.begin(), mWindowsList.end(), win );
|
||||
}
|
||||
|
||||
std::vector<UIWidget*> UISceneNode::loadNode( pugi::xml_node node, Node* parent,
|
||||
SmallVector<UIWidget*, 8> UISceneNode::loadNode( pugi::xml_node node, Node* parent,
|
||||
const Uint32& marker ) {
|
||||
Uint32 oldMarker = mCurrentMarker;
|
||||
mCurrentMarker = marker;
|
||||
|
||||
std::vector<UIWidget*> rootWidgets;
|
||||
SmallVector<UIWidget*, 8> rootWidgets;
|
||||
|
||||
if ( NULL == parent )
|
||||
parent = this;
|
||||
@@ -648,7 +648,7 @@ UIWidget* UISceneNode::loadLayoutNodes( pugi::xml_node node, Node* parent, const
|
||||
std::string id( node.attribute( "id" ).as_string() );
|
||||
mIsLoading = true;
|
||||
Clock innerClock;
|
||||
std::vector<UIWidget*> widgets = loadNode( node, parent, marker );
|
||||
SmallVector<UIWidget*, 8> widgets = loadNode( node, parent, marker );
|
||||
|
||||
if ( mVerbose ) {
|
||||
std::sort(
|
||||
|
||||
@@ -581,8 +581,7 @@ void UIStyle::subscribeNonCacheableStyles() {
|
||||
return;
|
||||
for ( auto& style : mGlobalDefinition->getStyles() ) {
|
||||
if ( !style->getSelector().isCacheable() ) {
|
||||
std::vector<UIWidget*> elements =
|
||||
style->getSelector().getRelatedElements( mWidget, false );
|
||||
auto elements = style->getSelector().getRelatedElements( mWidget, false );
|
||||
|
||||
if ( !elements.empty() ) {
|
||||
for ( auto& element : elements ) {
|
||||
|
||||
+10
-11
@@ -1705,8 +1705,8 @@ const Uint32& UIWidget::getStylePreviousState() const {
|
||||
return NULL != mStyle ? mStyle->getPreviousState() : mState;
|
||||
}
|
||||
|
||||
std::vector<UIWidget*> UIWidget::findAllByClass( const std::string& className ) {
|
||||
std::vector<UIWidget*> widgets;
|
||||
WidgetQueryResult UIWidget::findAllByClass( const std::string& className ) {
|
||||
WidgetQueryResult widgets;
|
||||
|
||||
if ( !isClosing() && hasClass( className ) && !inClosingTree() ) {
|
||||
widgets.push_back( this );
|
||||
@@ -1716,8 +1716,7 @@ std::vector<UIWidget*> UIWidget::findAllByClass( const std::string& className )
|
||||
|
||||
while ( NULL != child ) {
|
||||
if ( child->isWidget() ) {
|
||||
std::vector<UIWidget*> foundWidgets =
|
||||
child->asType<UIWidget>()->findAllByClass( className );
|
||||
WidgetQueryResult foundWidgets = child->asType<UIWidget>()->findAllByClass( className );
|
||||
|
||||
if ( !foundWidgets.empty() )
|
||||
widgets.insert( widgets.end(), foundWidgets.begin(), foundWidgets.end() );
|
||||
@@ -1729,8 +1728,8 @@ std::vector<UIWidget*> UIWidget::findAllByClass( const std::string& className )
|
||||
return widgets;
|
||||
}
|
||||
|
||||
std::vector<UIWidget*> UIWidget::findAllByTag( const std::string& tag ) {
|
||||
std::vector<UIWidget*> widgets;
|
||||
WidgetQueryResult UIWidget::findAllByTag( const std::string& tag ) {
|
||||
WidgetQueryResult widgets;
|
||||
|
||||
if ( !isClosing() && getElementTag() == tag && !inClosingTree() ) {
|
||||
widgets.push_back( this );
|
||||
@@ -1740,7 +1739,7 @@ std::vector<UIWidget*> UIWidget::findAllByTag( const std::string& tag ) {
|
||||
|
||||
while ( NULL != child ) {
|
||||
if ( child->isWidget() ) {
|
||||
std::vector<UIWidget*> foundWidgets = child->asType<UIWidget>()->findAllByTag( tag );
|
||||
WidgetQueryResult foundWidgets = child->asType<UIWidget>()->findAllByTag( tag );
|
||||
|
||||
if ( !foundWidgets.empty() )
|
||||
widgets.insert( widgets.end(), foundWidgets.begin(), foundWidgets.end() );
|
||||
@@ -1815,8 +1814,8 @@ UIWidget* UIWidget::querySelector( const CSS::StyleSheetSelector& selector ) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
std::vector<UIWidget*> UIWidget::querySelectorAll( const CSS::StyleSheetSelector& selector ) {
|
||||
std::vector<UIWidget*> widgets;
|
||||
WidgetQueryResult UIWidget::querySelectorAll( const CSS::StyleSheetSelector& selector ) {
|
||||
WidgetQueryResult widgets;
|
||||
|
||||
if ( !isClosing() && !inClosingTree() && selector.select( this ) ) {
|
||||
widgets.push_back( this );
|
||||
@@ -1826,7 +1825,7 @@ std::vector<UIWidget*> UIWidget::querySelectorAll( const CSS::StyleSheetSelector
|
||||
|
||||
while ( NULL != child ) {
|
||||
if ( child->isWidget() ) {
|
||||
std::vector<UIWidget*> foundWidgets =
|
||||
WidgetQueryResult foundWidgets =
|
||||
child->asType<UIWidget>()->querySelectorAll( selector );
|
||||
|
||||
if ( !foundWidgets.empty() )
|
||||
@@ -1862,7 +1861,7 @@ UIWidget* UIWidget::querySelector( const std::string& selector ) {
|
||||
return querySelector( CSS::StyleSheetSelector( selector ) );
|
||||
}
|
||||
|
||||
std::vector<UIWidget*> UIWidget::querySelectorAll( const std::string& selector ) {
|
||||
WidgetQueryResult UIWidget::querySelectorAll( const std::string& selector ) {
|
||||
return querySelectorAll( CSS::StyleSheetSelector( selector ) );
|
||||
}
|
||||
|
||||
|
||||
@@ -2,10 +2,32 @@
|
||||
#include <eepp/window/backend/SDL3/clipboardsdl3.hpp>
|
||||
#include <eepp/window/backend/SDL3/windowsdl3.hpp>
|
||||
|
||||
#include <memory>
|
||||
|
||||
#ifdef EE_BACKEND_SDL3
|
||||
|
||||
namespace EE { namespace Window { namespace Backend { namespace SDL3 {
|
||||
|
||||
namespace {
|
||||
|
||||
struct ClipboardDataContext {
|
||||
Clipboard::DataCallback callback;
|
||||
Clipboard::CleanupCallback cleanup;
|
||||
};
|
||||
|
||||
const void* SDLCALL clipboardDataCallback( void* userdata, const char* mimeType, size_t* size ) {
|
||||
auto context = static_cast<ClipboardDataContext*>( userdata );
|
||||
return context->callback( mimeType, size );
|
||||
}
|
||||
|
||||
void SDLCALL clipboardCleanupCallback( void* userdata ) {
|
||||
std::unique_ptr<ClipboardDataContext> context( static_cast<ClipboardDataContext*>( userdata ) );
|
||||
if ( context->cleanup )
|
||||
context->cleanup();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ClipboardSDL::ClipboardSDL( EE::Window::Window* window ) : Clipboard( window ) {}
|
||||
|
||||
ClipboardSDL::~ClipboardSDL() {}
|
||||
@@ -40,6 +62,60 @@ String ClipboardSDL::getWideText() {
|
||||
return String::fromUtf8( getText() );
|
||||
}
|
||||
|
||||
bool ClipboardSDL::setData( DataCallback callback, CleanupCallback cleanup,
|
||||
const std::vector<std::string>& mimeTypes ) {
|
||||
if ( !callback || mimeTypes.empty() )
|
||||
return false;
|
||||
|
||||
std::vector<const char*> mimeTypePointers;
|
||||
mimeTypePointers.reserve( mimeTypes.size() );
|
||||
for ( const auto& mimeType : mimeTypes )
|
||||
mimeTypePointers.emplace_back( mimeType.c_str() );
|
||||
|
||||
auto context = std::make_unique<ClipboardDataContext>(
|
||||
ClipboardDataContext{ std::move( callback ), std::move( cleanup ) } );
|
||||
if ( !SDL_SetClipboardData( clipboardDataCallback, clipboardCleanupCallback, context.get(),
|
||||
mimeTypePointers.data(), mimeTypePointers.size() ) )
|
||||
return false;
|
||||
|
||||
context.release();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ClipboardSDL::clearData() {
|
||||
return SDL_ClearClipboardData();
|
||||
}
|
||||
|
||||
Clipboard::Data ClipboardSDL::getData( const std::string& mimeType ) {
|
||||
size_t size = 0;
|
||||
void* data = SDL_GetClipboardData( mimeType.c_str(), &size );
|
||||
if ( !data )
|
||||
return {};
|
||||
|
||||
const auto bytes = static_cast<const Uint8*>( data );
|
||||
Data result( bytes, bytes + size );
|
||||
SDL_free( data );
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<std::string> ClipboardSDL::getMimeTypes() {
|
||||
size_t count = 0;
|
||||
char** mimeTypes = SDL_GetClipboardMimeTypes( &count );
|
||||
if ( !mimeTypes )
|
||||
return {};
|
||||
|
||||
std::vector<std::string> result;
|
||||
result.reserve( count );
|
||||
for ( size_t i = 0; i < count; ++i )
|
||||
result.emplace_back( mimeTypes[i] );
|
||||
SDL_free( mimeTypes );
|
||||
return result;
|
||||
}
|
||||
|
||||
bool ClipboardSDL::hasData( const std::string& mimeType ) const {
|
||||
return SDL_HasClipboardData( mimeType.c_str() );
|
||||
}
|
||||
|
||||
}}}} // namespace EE::Window::Backend::SDL3
|
||||
|
||||
#endif
|
||||
|
||||
@@ -27,6 +27,17 @@ class EE_API ClipboardSDL : public Clipboard {
|
||||
|
||||
void setPrimarySelectionText( const std::string& text );
|
||||
|
||||
bool setData( DataCallback callback, CleanupCallback cleanup,
|
||||
const std::vector<std::string>& mimeTypes );
|
||||
|
||||
bool clearData();
|
||||
|
||||
Data getData( const std::string& mimeType );
|
||||
|
||||
std::vector<std::string> getMimeTypes();
|
||||
|
||||
bool hasData( const std::string& mimeType ) const;
|
||||
|
||||
protected:
|
||||
friend class WindowSDL;
|
||||
|
||||
|
||||
@@ -1876,7 +1876,10 @@ void TerminalDisplay::onSizeChange() {
|
||||
mSession->resize( gridSize.getWidth(), gridSize.getHeight(),
|
||||
gridSize.getWidth() * cellSize.getWidth(),
|
||||
gridSize.getHeight() * cellSize.getHeight() );
|
||||
mDirtyLines.resize( gridSize.getHeight(), 1 );
|
||||
// Rendering must continue using the currently published snapshot until the worker
|
||||
// publishes the resized grid. consumeSnapshot() updates mRows and mDirtyLines together;
|
||||
// shrinking the dirty vector here leaves drawGrid() indexing it with the old mRows.
|
||||
mDirtyLines.resize( std::max<std::size_t>( mRows, gridSize.getHeight() ), true );
|
||||
}
|
||||
|
||||
if ( mFrameBuffer && ( mFrameBuffer->getWidth() < mSize.getWidth() ||
|
||||
|
||||
@@ -5837,7 +5837,7 @@ UTEST( UIHTML, RonStonerDeferredImagesUpdateDocumentHeight ) {
|
||||
ASSERT_TRUE( documentScene != nullptr );
|
||||
|
||||
UIWidget* body = nullptr;
|
||||
std::vector<UIWidget*> images;
|
||||
WidgetQueryResult images;
|
||||
for ( int i = 0; i < 300; ++i ) {
|
||||
pump();
|
||||
body = documentScene->getRoot()->findByType( UI_TYPE_HTML_BODY )->asType<UIWidget>();
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
#include "utest.hpp"
|
||||
|
||||
#include <eepp/system/filesystem.hpp>
|
||||
#include <eepp/system/sys.hpp>
|
||||
#include <eepp/ui/tools/uisettingspanel.hpp>
|
||||
#include <eepp/ui/uiapplication.hpp>
|
||||
#include <eepp/ui/uicheckbox.hpp>
|
||||
#include <eepp/ui/uiscenenode.hpp>
|
||||
#include <eepp/ui/uitextview.hpp>
|
||||
|
||||
using namespace EE;
|
||||
using namespace EE::System;
|
||||
using namespace EE::UI;
|
||||
using namespace EE::UI::Tools;
|
||||
using namespace EE::Window;
|
||||
|
||||
UTEST( UISettingsPanelModel, validatesCategoriesGroupsAndSettings ) {
|
||||
SettingsModel model;
|
||||
|
||||
EXPECT_FALSE( model.addCategory( { {}, "General", "Behavior" } ) );
|
||||
EXPECT_TRUE( model.addCategory( { "general.behavior", "General", "Behavior" } ) );
|
||||
EXPECT_FALSE( model.addCategory( { "general.behavior", "General", "Other" } ) );
|
||||
EXPECT_TRUE( model.hasCategory( "general.behavior" ) );
|
||||
EXPECT_FALSE( model.hasCategory( "general.missing" ) );
|
||||
|
||||
EXPECT_FALSE( model.addGroup( { "general.missing", "Display", 0 } ) );
|
||||
EXPECT_TRUE( model.addGroup( { "general.behavior", "Display", 0 } ) );
|
||||
|
||||
bool value = false;
|
||||
EXPECT_FALSE( model.addSetting( { { {}, "general.behavior", "Empty", "Empty identifier", {} },
|
||||
BoolPointerSetting{ &value, {} } } ) );
|
||||
EXPECT_FALSE(
|
||||
model.addSetting( { { "missing", "general.missing", "Missing", "Missing category", {} },
|
||||
BoolPointerSetting{ &value, {} } } ) );
|
||||
EXPECT_TRUE( model.addSetting(
|
||||
{ { "enabled", "general.behavior", "Enabled", "Enable the option", "Display" },
|
||||
BoolPointerSetting{ &value, {} } } ) );
|
||||
EXPECT_FALSE( model.addSetting(
|
||||
{ { "enabled", "general.behavior", "Duplicate", "Duplicate identifier", {} },
|
||||
BoolPointerSetting{ &value, {} } } ) );
|
||||
|
||||
EXPECT_EQ( 1u, model.categories().size() );
|
||||
EXPECT_EQ( 1u, model.groups().size() );
|
||||
EXPECT_EQ( 1u, model.settings().size() );
|
||||
EXPECT_STDSTREQ( "enabled", model.settings().front().descriptor.id );
|
||||
}
|
||||
|
||||
UTEST( UISettingsPanelModel, clearRemovesAllRules ) {
|
||||
SettingsModel model;
|
||||
bool value = true;
|
||||
EXPECT_TRUE( model.addCategory( { "editor.document", "Editor", "Document" } ) );
|
||||
EXPECT_TRUE( model.addGroup( { "editor.document", "Files", 0 } ) );
|
||||
EXPECT_TRUE(
|
||||
model.addSetting( { { "trim", "editor.document", "Trim", "Trim whitespace", "Files" },
|
||||
BoolPointerSetting{ &value, {} } } ) );
|
||||
|
||||
model.clear();
|
||||
|
||||
EXPECT_TRUE( model.categories().empty() );
|
||||
EXPECT_TRUE( model.groups().empty() );
|
||||
EXPECT_TRUE( model.settings().empty() );
|
||||
}
|
||||
|
||||
UTEST( UISettingsPanel, buildsAndMaterializesCategoriesLazily ) {
|
||||
UIApplication app(
|
||||
WindowSettings( 800, 600, "eepp - UISettingsPanel Test", WindowStyle::Default,
|
||||
WindowBackend::Default, 32 ),
|
||||
UIApplication::Settings( Sys::getProcessPath() + ".." + FileSystem::getOSSlash(), 1 ) );
|
||||
auto* panel = UISettingsPanel::New( app.getUI()->getRoot() );
|
||||
bool firstValue = false;
|
||||
bool secondValue = true;
|
||||
|
||||
EXPECT_TRUE( panel->addCategory( "general.behavior", "General", "Behavior" ) );
|
||||
EXPECT_TRUE( panel->addCategory( "editor.display", "Editor", "Display" ) );
|
||||
EXPECT_TRUE( panel->addBool( { "first", "general.behavior", "First", "The first setting", {} },
|
||||
&firstValue ) );
|
||||
EXPECT_TRUE( panel->addBool( { "second", "editor.display", "Second", "The second setting", {} },
|
||||
&secondValue ) );
|
||||
EXPECT_EQ( nullptr, panel->find<UIWidget>( "setting_first" ) );
|
||||
EXPECT_EQ( nullptr, panel->find<UIWidget>( "setting_second" ) );
|
||||
|
||||
panel->build();
|
||||
|
||||
EXPECT_TRUE( panel->isBuilt() );
|
||||
auto* firstRow = panel->find<UIWidget>( "setting_first" );
|
||||
EXPECT_NE( nullptr, firstRow );
|
||||
EXPECT_TRUE( firstRow->isVisible() );
|
||||
EXPECT_EQ( nullptr, panel->find<UIWidget>( "setting_second" ) );
|
||||
EXPECT_FALSE( panel->addCategory( "late.category", "Late", "Category" ) );
|
||||
|
||||
panel->selectCategory( "editor.display" );
|
||||
auto* secondRow = panel->find<UIWidget>( "setting_second" );
|
||||
EXPECT_NE( nullptr, secondRow );
|
||||
EXPECT_FALSE( firstRow->isVisible() );
|
||||
EXPECT_TRUE( secondRow->isVisible() );
|
||||
EXPECT_STDSTREQ( "Display",
|
||||
panel->find<UITextView>( "settings_page_title" )->getText().toUtf8() );
|
||||
|
||||
panel->setCategoryEnabled( "editor.display", false );
|
||||
EXPECT_FALSE( secondRow->isEnabled() );
|
||||
panel->setCategoryEnabled( "editor.display", true );
|
||||
EXPECT_TRUE( secondRow->isEnabled() );
|
||||
}
|
||||
|
||||
UTEST( UISettingsPanel, filtersAcrossUnmaterializedCategories ) {
|
||||
UIApplication app(
|
||||
WindowSettings( 800, 600, "eepp - UISettingsPanel Filter Test", WindowStyle::Default,
|
||||
WindowBackend::Default, 32 ),
|
||||
UIApplication::Settings( Sys::getProcessPath() + ".." + FileSystem::getOSSlash(), 1 ) );
|
||||
auto* panel = UISettingsPanel::New( app.getUI()->getRoot() );
|
||||
bool firstValue = false;
|
||||
bool secondValue = false;
|
||||
panel->setSearchResultsText( "Filtered Settings" );
|
||||
EXPECT_TRUE( panel->addCategory( "general.behavior", "General", "Behavior" ) );
|
||||
EXPECT_TRUE( panel->addCategory( "editor.display", "Editor", "Display" ) );
|
||||
EXPECT_TRUE( panel->addBool( { "first", "general.behavior", "First", "Ordinary option", {} },
|
||||
&firstValue ) );
|
||||
EXPECT_TRUE( panel->addBool(
|
||||
{ "second", "editor.display", "Second", "Unique searchable phrase", {} }, &secondValue ) );
|
||||
panel->build();
|
||||
|
||||
panel->setFilter( "searchable" );
|
||||
|
||||
auto* firstRow = panel->find<UIWidget>( "setting_first" );
|
||||
auto* secondRow = panel->find<UIWidget>( "setting_second" );
|
||||
EXPECT_NE( nullptr, secondRow );
|
||||
EXPECT_FALSE( firstRow->isVisible() );
|
||||
EXPECT_TRUE( secondRow->isVisible() );
|
||||
EXPECT_STDSTREQ( "Filtered Settings",
|
||||
panel->find<UITextView>( "settings_page_title" )->getText().toUtf8() );
|
||||
|
||||
panel->setFilter( {} );
|
||||
EXPECT_TRUE( firstRow->isVisible() );
|
||||
EXPECT_FALSE( secondRow->isVisible() );
|
||||
}
|
||||
+29
-14
@@ -556,6 +556,8 @@ void App::maximizeTabWidget() {
|
||||
UIWindow::StyleConfig winCfg;
|
||||
winCfg.WinFlags = UI_WIN_SHADOW | UI_WIN_MODAL | UI_WIN_EPHEMERAL | UI_WIN_NO_DECORATION;
|
||||
UIWindow* win = UIWindow::NewOpt( UIWindow::SIMPLE_LAYOUT, winCfg );
|
||||
mMaximizedTabWidgetWindow = win;
|
||||
mMaximizedTabWidget = curTabWidget;
|
||||
win->setPixelsSize( getUISceneNode()->getPixelsSize() - PixelDensity::dpToPx( 64 ) );
|
||||
win->setId( "detached_tab_widget_win" );
|
||||
win->addClass( "tab_widget_cont" );
|
||||
@@ -587,6 +589,7 @@ void App::maximizeTabWidget() {
|
||||
bool wasFirstSplit = tabWidgetParent->isType( UI_TYPE_SPLITTER ) &&
|
||||
tabWidgetParent->asType<UISplitter>()->getFirstWidget() == curTabWidget;
|
||||
auto nodeLink = UINodeLink::NewLink( curTabWidget );
|
||||
mMaximizedTabWidgetLink = nodeLink;
|
||||
if ( wasFirstSplit )
|
||||
nodeLink->setClass( "was_first_split" );
|
||||
curTabWidget->setParent( win );
|
||||
@@ -626,27 +629,24 @@ void App::maximizeTabWidget() {
|
||||
void App::restoreMaximizedTabWidget() {
|
||||
if ( !App::instance() || !SceneManager::isActive() )
|
||||
return;
|
||||
auto sceneNode = appInstance->getUISceneNode();
|
||||
if ( !sceneNode )
|
||||
return;
|
||||
auto nodeLink = sceneNode->getRoot()->find( "nodelink_tab_widget" );
|
||||
if ( !nodeLink )
|
||||
return;
|
||||
auto splitterParent = nodeLink->getParent();
|
||||
nodeLink->setParent( sceneNode );
|
||||
auto curTabWidget = sceneNode->getRoot()->find<UIWidget>( "detached_tab_widget" );
|
||||
if ( !curTabWidget )
|
||||
if ( !mMaximizedTabWidgetLink || !mMaximizedTabWidget )
|
||||
return;
|
||||
auto* nodeLink = mMaximizedTabWidgetLink;
|
||||
auto* curTabWidget = mMaximizedTabWidget;
|
||||
auto* win = mMaximizedTabWidgetWindow;
|
||||
auto* splitterParent = nodeLink->getParent();
|
||||
nodeLink->setParent( mUISceneNode );
|
||||
curTabWidget->setParent( splitterParent );
|
||||
curTabWidget->setAnchors( 0 );
|
||||
curTabWidget->setId( "" );
|
||||
if ( nodeLink->asType<UIWidget>()->hasClass( "was_first_split" ) &&
|
||||
splitterParent->isType( UI_TYPE_SPLITTER ) ) {
|
||||
if ( nodeLink->hasClass( "was_first_split" ) && splitterParent->isType( UI_TYPE_SPLITTER ) ) {
|
||||
splitterParent->asType<UISplitter>()->swap();
|
||||
}
|
||||
nodeLink->close();
|
||||
auto win = mUISceneNode->find( "detached_tab_widget_win" );
|
||||
if ( win )
|
||||
mMaximizedTabWidgetLink = nullptr;
|
||||
mMaximizedTabWidget = nullptr;
|
||||
mMaximizedTabWidgetWindow = nullptr;
|
||||
if ( win && !win->isClosing() )
|
||||
win->close();
|
||||
}
|
||||
|
||||
@@ -1734,6 +1734,16 @@ void App::onTabCreated( UITab* tab, UIWidget* ) {
|
||||
|
||||
if ( tab->getOwnedWidget()->isType( UI_TYPE_CODEEDITOR ) ||
|
||||
tab->getOwnedWidget()->isType( UI_TYPE_TERMINAL ) ) {
|
||||
if ( tab->getOwnedWidget()->isType( UI_TYPE_TERMINAL ) ) {
|
||||
menu->addSeparator();
|
||||
auto* terminal = tab->getOwnedWidget()->asType<UITerminal>();
|
||||
menu->addCheckBox( i18n( "enable_exclusive_mode", "Enable Exclusive Mode" ),
|
||||
terminal->getExclusiveMode(),
|
||||
getKeybind( UITerminal::getExclusiveModeToggleCommandName() ) )
|
||||
->setId( UITerminal::getExclusiveModeToggleCommandName() );
|
||||
menuAdd( "rename_session", "Rename Session", "", "terminal-rename" );
|
||||
}
|
||||
|
||||
menu->addSeparator();
|
||||
|
||||
bool enabled = tab->getTabWidget()->getTabCount() > 1;
|
||||
@@ -5073,6 +5083,11 @@ void App::init( InitParameters& params ) {
|
||||
std::function<void()> onMsgBoxCloseCb ) -> bool {
|
||||
if ( widget == nullptr || widget->getData() == 0 )
|
||||
return true;
|
||||
if ( auto* detachedTabWidget =
|
||||
mUISceneNode->getRoot()->find<UIWidget>( "detached_tab_widget" );
|
||||
detachedTabWidget && detachedTabWidget->inParentTreeOf( widget ) ) {
|
||||
restoreMaximizedTabWidget();
|
||||
}
|
||||
if ( widget->isType( UI_TYPE_CODEEDITOR ) ) {
|
||||
return mSplitter->tryCodeEditorClose( widget->asType<UICodeEditor>(),
|
||||
focusTabBehavior, onMsgBoxCloseCb );
|
||||
|
||||
@@ -680,6 +680,9 @@ class App : public UICodeEditorSplitter::Client, public PluginContextProvider {
|
||||
UIMainLayout* mMainLayout{ nullptr };
|
||||
UILayout* mBaseLayout{ nullptr };
|
||||
UILayout* mImageLayout{ nullptr };
|
||||
UIWindow* mMaximizedTabWidgetWindow{ nullptr };
|
||||
UITabWidget* mMaximizedTabWidget{ nullptr };
|
||||
UINodeLink* mMaximizedTabWidgetLink{ nullptr };
|
||||
UITextView* mDocInfo{ nullptr };
|
||||
std::string mDocInfoLineAbbr;
|
||||
std::string mDocInfoColAbbr;
|
||||
|
||||
@@ -774,7 +774,7 @@ std::string Git::setSafeDirectory( const std::string& projectDir ) const {
|
||||
}
|
||||
|
||||
Git::Result Git::pull( const std::string& projectDir ) {
|
||||
return gitSimple( "pull", projectDir );
|
||||
return gitSimple( "pull --prune", projectDir );
|
||||
}
|
||||
|
||||
Git::Result Git::push( const std::string& projectDir ) {
|
||||
@@ -979,6 +979,12 @@ Git::Result Git::deleteRemoteBranch( const std::string& remote, const std::strin
|
||||
const std::string& projectDir ) {
|
||||
Result result;
|
||||
result.returnCode = git( { "push", remote, "--delete", branch }, projectDir, result.result );
|
||||
if ( result.success() ) {
|
||||
std::string cleanupResult;
|
||||
result.returnCode = git( { "update-ref", "-d", "refs/remotes/" + remote + "/" + branch },
|
||||
projectDir, cleanupResult );
|
||||
result.result.append( cleanupResult );
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -1148,7 +1154,7 @@ Git::Branch parseLocalBranch( const std::string_view& raw ) {
|
||||
|
||||
static Git::Branch parseRemoteBranch( std::string_view raw ) {
|
||||
auto split = String::split( raw, '\t', true );
|
||||
if ( split.size() < 4 )
|
||||
if ( split.size() < 4 || ( split.size() > 5 && !split[5].empty() ) )
|
||||
return {};
|
||||
std::string name( std::string{ split[1] } );
|
||||
std::string remote( std::string{ split[1] } );
|
||||
@@ -1176,7 +1182,7 @@ static Git::Branch parseTag( std::string_view raw ) {
|
||||
std::vector<Git::Branch> Git::getAllBranchesAndTags( RefType ref, std::string_view filterBranch,
|
||||
const std::string& projectDir ) {
|
||||
// clang-format off
|
||||
std::string args( "for-each-ref --format '%(refname) %(refname:short) %(upstream:short) %(objectname) %(upstream:track,nobracket)' --sort=v:refname" );
|
||||
std::string args( "for-each-ref --format '%(refname) %(refname:short) %(upstream:short) %(objectname) %(upstream:track,nobracket) %(symref)' --sort=v:refname" );
|
||||
// clang-format on
|
||||
|
||||
if ( filterBranch.empty() ) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -157,7 +157,7 @@ class GitPlugin : public PluginBase {
|
||||
UIDropDownList* mPanelSwicher{ nullptr };
|
||||
UIDropDownList* mRepoDropDown{ nullptr };
|
||||
UIStackWidget* mStackWidget{ nullptr };
|
||||
std::vector<UIWidget*> mStackMap;
|
||||
SmallVector<UIWidget*, 4> mStackMap;
|
||||
UIWidget* mGitContentView{ nullptr };
|
||||
UIWidget* mGitNoContentView{ nullptr };
|
||||
UIWidget* mConflictStateBar{ nullptr };
|
||||
|
||||
@@ -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
|
||||
|
||||
+18
-1
@@ -398,7 +398,7 @@ void App::registerKeyBindings() {
|
||||
} else if ( key == KEY_KP_MULTIPLY || key == KEY_Z ) {
|
||||
fitImage();
|
||||
} else if ( key == KEY_KP_DIVIDE ) {
|
||||
setImgScale( mConfig.DefaultImageZoom );
|
||||
resetImageZoom();
|
||||
} else if ( key == KEY_N && mImageViewer->getImage()->getDrawable() ) {
|
||||
const Sizef size( mImageViewer->getImage()->getDrawable()->getPixelsSize() );
|
||||
getWindow()->setSize( size.getWidth(), size.getHeight() );
|
||||
@@ -470,6 +470,23 @@ void App::fitImage() {
|
||||
mImageViewer->resetImageView();
|
||||
}
|
||||
|
||||
void App::resetImageZoom() {
|
||||
if ( !mImageViewer || !mImageViewer->getImage() || !mImageViewer->getImage()->getDrawable() )
|
||||
return;
|
||||
|
||||
UIImage* image = mImageViewer->getImage();
|
||||
const Sizef displayedSize( image->getPixelsSize() );
|
||||
const Sizef nativeSize( image->getDrawable()->getPixelsSize() );
|
||||
if ( displayedSize.getWidth() == 0.f || displayedSize.getHeight() == 0.f )
|
||||
return;
|
||||
|
||||
const Float pixelDensity = PixelDensity::getPixelDensity();
|
||||
image->setScale( { mConfig.DefaultImageZoom * nativeSize.getWidth() /
|
||||
( displayedSize.getWidth() * pixelDensity ),
|
||||
mConfig.DefaultImageZoom * nativeSize.getHeight() /
|
||||
( displayedSize.getHeight() * pixelDensity ) } );
|
||||
}
|
||||
|
||||
Sprite* App::getImageSprite() const {
|
||||
if ( !mImageViewer || !mImageViewer->getImage() || !mImageViewer->getImage()->getDrawable() )
|
||||
return nullptr;
|
||||
|
||||
@@ -69,6 +69,7 @@ class App {
|
||||
void updateSlideShow();
|
||||
void toggleHelp();
|
||||
void fitImage();
|
||||
void resetImageZoom();
|
||||
Sprite* getImageSprite() const;
|
||||
void processDroppedFiles();
|
||||
|
||||
|
||||
@@ -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
|
||||
+558
-188
File diff suppressed because it is too large
Load Diff
@@ -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