Fix CSS state rollback for transient properties. eproc: add keyboard navigation between search and process views, fix Send Signal menu events.

Store native and inherited fallback values outside local styles, then restore them when state rules stop matching. Handle indexed properties across overlapping states and retain a serialized value when an inherited source disappears. Add rollback regression tests and update the CSS documentation

Focus and select the first process row when pressing Down in the search input. Pressing Up from the first row returns focus to search in both table and tree views.
This commit is contained in:
Martín Lucas Golini
2026-09-29 14:56:01 -03:00
parent 3dada4857a
commit a090d1a666
14 changed files with 718 additions and 82 deletions
+6 -7
View File
@@ -27,13 +27,12 @@ but the layout model depends on the type of UI being styled.
* Attribute selectors are supported. For native widgets they operate on properties exposed by the
widget property system. HTML `data-*` attributes are also available to selectors on HTML widgets.
* State-dependent style rollback is not yet a complete CSS computed-style rollback implementation.
When a pseudo-class or other volatile selector introduces a property that has no matching
stateless declaration, eepp attempts to preserve the previous value by serializing the current
widget property and using it as a fallback. This works for many properties, but restoration may
be incomplete when a property cannot be serialized, when a property first appears during a
transition between non-normal states, or when the original value came from dynamic/inherited
state that later changes.
* When a pseudo-class or other volatile selector stops matching, eepp restores the current
underlying local or stylesheet declaration. If there is no such declaration, it restores the
current inherited value or a saved native widget value. Native fallback requires the widget to
serialize that property through `getPropertyString()`; properties without a serializable native
value may not roll back. Application changes to a property while a state-dependent declaration
overrides it have no defined rollback behavior.
* Relative resource URLs are resolved against the relevant stylesheet/document base URI when one is
available. eepp also supports resource locators for resources already loaded by the application.
+5 -1
View File
@@ -534,7 +534,11 @@ Use pseudo-classes for widget state managed by the UI system:
}
```
State changes cause eepp to recompute the matching style.
State changes cause eepp to recompute the matching style. When a state-dependent declaration stops
matching, a lower local or stylesheet declaration takes effect again. If there is no lower
declaration, eepp restores the current inherited value or the widget's saved native value. Native
rollback requires `getPropertyString()` to serialize the property. Changes made by application code
to a property while a state rule overrides it have no defined rollback behavior.
Common native state pseudo-classes include:
+2 -2
View File
@@ -25,7 +25,7 @@ class EE_API UIHTMLTable : public UIHTMLWidget {
virtual std::vector<PropertyId> getPropertiesImplemented() const;
virtual std::string getPropertyString( const PropertyDefinition* propertyDef,
const Uint32& state = 0 ) const;
const Uint32& propertyIndex = 0 ) const;
virtual bool applyProperty( const StyleSheetProperty& attribute );
@@ -57,7 +57,7 @@ class EE_API UIHTMLTableCell : public UIRichText {
virtual std::vector<PropertyId> getPropertiesImplemented() const;
virtual std::string getPropertyString( const PropertyDefinition* propertyDef,
const Uint32& state = 0 ) const;
const Uint32& propertyIndex = 0 ) const;
virtual bool applyProperty( const StyleSheetProperty& attribute );
+1 -1
View File
@@ -349,7 +349,7 @@ class EE_API UIHTMLWidget : public UILayout {
using UIWidget::getPropertyString;
virtual std::string getPropertyString( const PropertyDefinition* propertyDef,
const Uint32& state = 0 ) const;
const Uint32& propertyIndex = 0 ) const;
virtual bool applyProperty( const StyleSheetProperty& attribute );
+20 -1
View File
@@ -145,6 +145,14 @@ class EE_API UIStyle : public UIState {
* needs no container allocation; nested slots are retained for later reuse. */
std::unique_ptr<CSS::StyleSheetProperty> mPropertyResolutionSlot;
SmallVector<std::unique_ptr<CSS::StyleSheetProperty>, 2> mNestedPropertyResolutionSlots;
struct PropertyFallback {
CSS::PropertyId propertyId;
Uint32 index;
const CSS::PropertyDefinition* definition;
std::string nativeValue;
bool inherited;
};
SmallVector<PropertyFallback, 0> mPropertyFallbacks;
bool mChangingState;
bool mForceReapplyProperties;
bool mDisableAnimations;
@@ -175,7 +183,18 @@ class EE_API UIStyle : public UIState {
void removeRelatedWidgets();
void applyStyleSheetProperty( const CSS::StyleSheetProperty& property,
std::shared_ptr<CSS::ElementDefinition> prevDefinition );
std::shared_ptr<CSS::ElementDefinition> prevDefinition,
bool captureFallback = true );
void capturePropertyFallback( const CSS::StyleSheetProperty& property );
bool isTransientProperty( CSS::PropertyId propertyId ) const;
bool restorePropertyFallbacks( CSS::PropertyId propertyId,
std::shared_ptr<CSS::ElementDefinition> prevDefinition,
Uint32 firstIndex = 0 );
void clearPropertyFallbacks( CSS::PropertyId propertyId );
void updateAnimationsPlayState();
+2
View File
@@ -64,6 +64,8 @@ class EE_API UITreeView : public UIAbstractTableView {
void collapseAll( const ModelIndex& index = {} );
virtual void selectAll();
UIIcon* getExpandIcon() const;
void setExpandedIcon( UIIcon* expandIcon );
+4 -4
View File
@@ -31,7 +31,7 @@ std::vector<PropertyId> UIHTMLTable::getPropertiesImplemented() const {
}
std::string UIHTMLTable::getPropertyString( const PropertyDefinition* propertyDef,
const Uint32& state ) const {
const Uint32& propertyIndex ) const {
if ( NULL == propertyDef )
return "";
@@ -55,7 +55,7 @@ std::string UIHTMLTable::getPropertyString( const PropertyDefinition* propertyDe
case PropertyId::TableLayout:
return mTopEq;
default:
return UIHTMLWidget::getPropertyString( propertyDef );
return UIHTMLWidget::getPropertyString( propertyDef, propertyIndex );
}
}
@@ -223,7 +223,7 @@ std::vector<PropertyId> UIHTMLTableCell::getPropertiesImplemented() const {
}
std::string UIHTMLTableCell::getPropertyString( const PropertyDefinition* propertyDef,
const Uint32& state ) const {
const Uint32& propertyIndex ) const {
if ( NULL == propertyDef )
return "";
@@ -231,7 +231,7 @@ std::string UIHTMLTableCell::getPropertyString( const PropertyDefinition* proper
case PropertyId::ColSpan:
return String::format( "%lld", mColSpan );
default:
return UIHTMLWidget::getPropertyString( propertyDef );
return UIHTMLWidget::getPropertyString( propertyDef, propertyIndex );
}
}
+2 -2
View File
@@ -1191,7 +1191,7 @@ std::vector<PropertyId> UIHTMLWidget::getPropertiesImplemented() const {
}
std::string UIHTMLWidget::getPropertyString( const PropertyDefinition* propertyDef,
const Uint32& state ) const {
const Uint32& propertyIndex ) const {
if ( NULL == propertyDef )
return "";
@@ -1271,7 +1271,7 @@ std::string UIHTMLWidget::getPropertyString( const PropertyDefinition* propertyD
case PropertyId::JustifySelf:
return CSSJustifySelfHelper::toString( getJustifySelf() );
default:
return UILayout::getPropertyString( propertyDef );
return UILayout::getPropertyString( propertyDef, propertyIndex );
}
}
+135 -28
View File
@@ -1,3 +1,4 @@
#include <algorithm>
#include <eepp/scene/actions/actions.hpp>
#include <eepp/system/functionstring.hpp>
#include <eepp/ui/css/stylesheetpropertyanimation.hpp>
@@ -463,6 +464,9 @@ void UIStyle::onStateChange() {
if ( nullptr == property || NULL == property->getPropertyDefinition() ) {
const auto def = StyleSheetSpecification::instance()->getProperty( prop );
if ( restorePropertyFallbacks( prop, prevDefinition ) ) {
continue;
}
if ( def && def->isInherited() ) {
UIStyle* inheritedStyle = nullptr;
StyleSheetProperty* inheritedProp =
@@ -504,12 +508,30 @@ void UIStyle::onStateChange() {
}
}
} else {
const bool transient = isTransientProperty( prop );
if ( property->getPropertyDefinition()->isIndexed() ) {
for ( size_t i = 0; i < property->getPropertyIndexCount(); i++ ) {
applyStyleSheetProperty( property->getPropertyIndex( i ), prevDefinition );
// Capture every layer before applying any of them. An earlier layer can
// change the widget's indexed backing storage.
if ( transient ) {
for ( size_t i = 0; i < property->getPropertyIndexCount(); i++ ) {
capturePropertyFallback( property->getPropertyIndex( i ) );
}
}
for ( size_t i = 0; i < property->getPropertyIndexCount(); i++ ) {
applyStyleSheetProperty( property->getPropertyIndex( i ), prevDefinition,
false );
}
// A shorter transient declaration can leave higher indexes without a
// winner even while another state remains active.
restorePropertyFallbacks(
prop, prevDefinition,
static_cast<Uint32>( property->getPropertyIndexCount() ) );
if ( !transient )
clearPropertyFallbacks( prop );
} else {
applyStyleSheetProperty( *property, prevDefinition );
if ( !transient )
clearPropertyFallbacks( prop );
}
if ( property->getPropertyDefinition()->isInherited() )
@@ -543,26 +565,122 @@ UIStyle::getStatelessStyleSheetProperty( const PropertyId& propertyId ) const {
if ( propertyId == PropertyId::Invalid )
return nullptr;
if ( !mElementStyle->getSelector().hasPseudoClasses() ) {
const StyleSheetProperty* property = mElementStyle->getPropertyById( propertyId );
if ( property )
return property;
}
const StyleSheetProperty* winner = !mElementStyle->getSelector().hasPseudoClasses()
? mElementStyle->getPropertyById( propertyId )
: nullptr;
if ( nullptr == mDefinition )
return nullptr;
return winner;
for ( auto style : mDefinition->getStyles() ) {
if ( style->getSelector().isCacheable() && !style->getSelector().hasPseudoClasses() ) {
const StyleSheetProperty* property = style->getPropertyById( propertyId );
if ( property )
return property;
if ( property && ( !winner || property->getSpecificity() >= winner->getSpecificity() ) )
winner = property;
}
}
return nullptr;
return winner;
}
void UIStyle::capturePropertyFallback( const StyleSheetProperty& property ) {
const PropertyDefinition* definition = property.getPropertyDefinition();
if ( nullptr == definition )
return;
const StyleSheetProperty* stateless =
getStatelessStyleSheetProperty( property.getPropertyId() );
if ( stateless &&
( !definition->isIndexed() || property.getIndex() < stateless->getPropertyIndexCount() ) )
return;
for ( const auto& fallback : mPropertyFallbacks ) {
if ( fallback.propertyId == property.getPropertyId() &&
fallback.index == property.getIndex() )
return;
}
const bool inherited =
definition->isInherited() && getInheritedProperty( property.getPropertyId() );
// Keep a serialized value in case the inherited declaration disappears before rollback.
std::string value = mWidget->getPropertyString( definition, property.getIndex() );
if ( inherited || !value.empty() ) {
mPropertyFallbacks.push_back( { property.getPropertyId(), property.getIndex(), definition,
std::move( value ), inherited } );
}
}
bool UIStyle::isTransientProperty( PropertyId propertyId ) const {
const StyleSheetProperty* local =
mElementStyle ? mElementStyle->getPropertyById( propertyId ) : nullptr;
const StyleSheetProperty* winner =
mDefinition ? mDefinition->getProperty( propertyId ) : nullptr;
if ( nullptr == winner || ( local && local->getSpecificity() >= winner->getSpecificity() ) )
return false;
if ( winner->isVolatile() )
return true;
if ( mCurrentState == UIState::StateFlagNormal )
return false;
const StyleSheetProperty* sourceProperty = nullptr;
const StyleSheetStyle* sourceStyle = nullptr;
for ( const auto* style : mDefinition->getStyles() ) {
const StyleSheetProperty* candidate = style->getPropertyById( propertyId );
if ( candidate && ( !sourceProperty ||
candidate->getSpecificity() >= sourceProperty->getSpecificity() ) ) {
sourceProperty = candidate;
sourceStyle = style;
}
}
return sourceStyle && sourceStyle->getSelector().hasPseudoClasses();
}
bool UIStyle::restorePropertyFallbacks( PropertyId propertyId,
std::shared_ptr<ElementDefinition> prevDefinition,
Uint32 firstIndex ) {
bool restored = false;
for ( const auto& fallback : mPropertyFallbacks ) {
if ( fallback.propertyId != propertyId || fallback.index < firstIndex )
continue;
if ( fallback.inherited ) {
UIStyle* ownerStyle = nullptr;
StyleSheetProperty* inherited = getInheritedProperty( propertyId, &ownerStyle );
if ( inherited ) {
auto resolved = ownerStyle->resolveProperty( inherited );
StyleSheetProperty value( *resolved.get() );
value.setVolatile( false );
applyStyleSheetProperty( value, prevDefinition, false );
mWidget->propagateInheritedProperty( value );
} else if ( !fallback.nativeValue.empty() ) {
StyleSheetProperty value( fallback.definition, fallback.nativeValue,
fallback.index );
applyStyleSheetProperty( value, prevDefinition, false );
mWidget->propagateInheritedProperty( value );
}
} else {
StyleSheetProperty value( fallback.definition, fallback.nativeValue, fallback.index );
applyStyleSheetProperty( value, prevDefinition, false );
}
restored = true;
}
if ( restored ) {
mPropertyFallbacks.erase(
std::remove_if( mPropertyFallbacks.begin(), mPropertyFallbacks.end(),
[propertyId, firstIndex]( const PropertyFallback& fallback ) {
return fallback.propertyId == propertyId &&
fallback.index >= firstIndex;
} ),
mPropertyFallbacks.end() );
}
return restored;
}
void UIStyle::clearPropertyFallbacks( PropertyId propertyId ) {
mPropertyFallbacks.erase( std::remove_if( mPropertyFallbacks.begin(), mPropertyFallbacks.end(),
[propertyId]( const PropertyFallback& fallback ) {
return fallback.propertyId == propertyId;
} ),
mPropertyFallbacks.end() );
}
void UIStyle::updateState() {
@@ -628,26 +746,15 @@ void UIStyle::removeRelatedWidgets() {
}
void UIStyle::applyStyleSheetProperty( const StyleSheetProperty& originalProperty,
std::shared_ptr<ElementDefinition> prevDefinition ) {
std::shared_ptr<ElementDefinition> prevDefinition,
bool captureFallback ) {
auto resolvedProperty = resolveProperty( &originalProperty );
const StyleSheetProperty* property = resolvedProperty.get();
const PropertyDefinition* propertyDefinition = property->getPropertyDefinition();
// Save default value if possible and not available.
if ( mCurrentState != UIState::StateFlagNormal ||
( mCurrentState == UIState::StateFlagNormal && property->isVolatile() ) ) {
const StyleSheetProperty* oldAttribute =
getStatelessStyleSheetProperty( property->getPropertyId() );
if ( nullptr == oldAttribute && getPreviousState() == UIState::StateFlagNormal ) {
std::string value(
mWidget->getPropertyString( propertyDefinition, property->getIndex() ) );
if ( !value.empty() ) {
setStyleSheetProperty( StyleSheetProperty( propertyDefinition, value,
property->getIndex(), true, true ) );
}
}
}
if ( captureFallback && isTransientProperty( property->getPropertyId() ) )
capturePropertyFallback( *property );
if ( !mDisableAnimations && !mFirstState && !mWidget->isSceneNodeLoading() &&
NULL != propertyDefinition &&
+12
View File
@@ -94,6 +94,18 @@ void UITreeView::traverseTree( TreeViewCallback callback ) const {
}
}
void UITreeView::selectAll() {
if ( !getModel() )
return;
std::vector<ModelIndex> indexes;
indexes.reserve( getItemCount() );
traverseTree( [&]( const int&, const ModelIndex& index, const size_t&, const Float& ) {
indexes.push_back( index );
return IterationDecision::Continue;
} );
getSelection().set( indexes );
}
void UITreeView::createOrUpdateColumns( bool resetColumnData ) {
if ( !getModel() ) {
updateContentSize();
@@ -57,8 +57,20 @@ UTEST( EProcProcessTree, ParentAndChildIndexesFollowPids ) {
view->setPixelsSize( 500, 200 );
view->setModel( tree );
EXPECT_TRUE( view->getHeaderColumn( ProcessModel::ColMemory ) != nullptr );
view->setSelectionKind( UIAbstractView::SelectionKind::Multiple );
view->selectAll();
EXPECT_EQ( view->getSelection().size(), 2 );
EXPECT_TRUE( view->getSelection().contains( init ) );
EXPECT_FALSE( view->getSelection().contains( child ) );
view->setExpanded( init, true );
view->selectAll();
EXPECT_EQ( view->getSelection().size(), 3 );
EXPECT_TRUE( view->getSelection().contains( child ) );
EXPECT_FALSE( view->getSelection().contains( grandchild ) );
view->setExpanded( child, true );
view->selectAll();
EXPECT_EQ( view->getSelection().size(), 4 );
EXPECT_TRUE( view->getSelection().contains( grandchild ) );
EXPECT_TRUE( view->isExpanded( init ) );
EXPECT_TRUE( view->isExpanded( child ) );
}
@@ -724,6 +724,405 @@ UTEST( CSSVariables, ParentStateChildOpacityReverts ) {
EXPECT_EQ( child->getAlpha(), 0.f );
}
UTEST( CSSVariables, NativeFallbackSurvivesMultipleStates ) {
UIApplication app(
WindowSettings( 800, 600, "eepp - CSS Native Fallback Test", WindowStyle::Default,
WindowBackend::Default, 32 ),
UIApplication::Settings( Sys::getProcessPath() + ".." + FileSystem::getOSSlash(), 1 ) );
UIWidget* root = app.getUI()->loadLayoutFromString( R"html(
<vbox id="root">
<style>
#child:selected { opacity: 0.5; }
#child:pressed { opacity: 0.25; }
</style>
<div id="child"></div>
</vbox>
)html" );
UIWidget* child = root->querySelector( "#child" );
EXPECT_TRUE( child != nullptr );
EXPECT_EQ( child->getAlpha(), 255.f );
for ( int cycle = 0; cycle < 32; ++cycle ) {
child->pushState( UIState::StateSelected );
EXPECT_EQ( child->getAlpha(), 127.5f );
child->pushState( UIState::StatePressed );
EXPECT_EQ( child->getAlpha(), 63.75f );
child->popState( UIState::StatePressed );
EXPECT_EQ( child->getAlpha(), 127.5f );
child->popState( UIState::StateSelected );
EXPECT_EQ( child->getAlpha(), 255.f );
EXPECT_FALSE( child->getUIStyle()->hasLocalProperty( PropertyId::Opacity ) );
}
}
UTEST( CSSVariables, NativeFallbackFirstAppearsOutsideNormalState ) {
UIApplication app(
WindowSettings( 800, 600, "eepp - CSS Late Native Fallback Test", WindowStyle::Default,
WindowBackend::Default, 32 ),
UIApplication::Settings( Sys::getProcessPath() + ".." + FileSystem::getOSSlash(), 1 ) );
UIWidget* root = app.getUI()->loadLayoutFromString( R"html(
<vbox id="root">
<style>
#child:selected { opacity: 0.5; }
#child:pressed { background-color: blue; }
</style>
<div id="child"></div>
</vbox>
)html" );
UIWidget* child = root->querySelector( "#child" );
EXPECT_TRUE( child != nullptr );
const Color nativeBackground = child->getBackgroundColor();
child->pushState( UIState::StateSelected );
EXPECT_EQ( child->getAlpha(), 127.5f );
child->pushState( UIState::StatePressed );
EXPECT_TRUE( child->getBackgroundColor() == Color::Blue );
child->popState( UIState::StatePressed );
EXPECT_TRUE( child->getBackgroundColor() == nativeBackground );
child->popState( UIState::StateSelected );
EXPECT_EQ( child->getAlpha(), 255.f );
}
UTEST( CSSVariables, InlineLowerWinnerSurvivesStateRollback ) {
UIApplication app(
WindowSettings( 800, 600, "eepp - CSS Inline State Rollback Test", WindowStyle::Default,
WindowBackend::Default, 32 ),
UIApplication::Settings( Sys::getProcessPath() + ".." + FileSystem::getOSSlash(), 1 ) );
UIWidget* root = app.getUI()->loadLayoutFromString( R"html(
<vbox id="root">
<style>#child:selected { opacity: 0.5 !important; }</style>
<div id="child" style="opacity: 0.75;"></div>
</vbox>
)html" );
UIWidget* child = root->querySelector( "#child" );
EXPECT_TRUE( child != nullptr );
EXPECT_EQ( child->getAlpha(), 191.25f );
child->pushState( UIState::StateSelected );
EXPECT_EQ( child->getAlpha(), 127.5f );
child->popState( UIState::StateSelected );
EXPECT_EQ( child->getAlpha(), 191.25f );
EXPECT_TRUE( child->getUIStyle()->hasLocalProperty( PropertyId::Opacity ) );
}
UTEST( CSSVariables, VariableLowerWinnerUpdatesWhileOverridden ) {
UIApplication app(
WindowSettings( 800, 600, "eepp - CSS Variable State Rollback Test", WindowStyle::Default,
WindowBackend::Default, 32 ),
UIApplication::Settings( Sys::getProcessPath() + ".." + FileSystem::getOSSlash(), 1 ) );
UIWidget* root = app.getUI()->loadLayoutFromString( R"html(
<vbox id="root">
<style>
:root { --base-opacity: 0.75; }
#child { opacity: var(--base-opacity); }
#child:selected { opacity: 0.5; }
</style>
<div id="child"></div>
</vbox>
)html" );
UIWidget* child = root->querySelector( "#child" );
EXPECT_TRUE( child != nullptr );
EXPECT_EQ( child->getAlpha(), 191.25f );
child->pushState( UIState::StateSelected );
EXPECT_EQ( child->getAlpha(), 127.5f );
app.getUI()->combineStyleSheet( ":root { --base-opacity: 0.25; }" );
EXPECT_EQ( child->getAlpha(), 127.5f );
child->popState( UIState::StateSelected );
EXPECT_EQ( child->getAlpha(), 63.75f );
}
UTEST( CSSVariables, LightDarkLowerWinnerUpdatesWhileOverridden ) {
UIApplication app(
WindowSettings( 800, 600, "eepp - CSS Light Dark State Rollback Test", WindowStyle::Default,
WindowBackend::Default, 32 ),
UIApplication::Settings( Sys::getProcessPath() + ".." + FileSystem::getOSSlash(), 1 ) );
UISceneNode* sceneNode = app.getUI();
sceneNode->setColorSchemePreference( ColorSchemePreference::Light );
UIWidget* root = sceneNode->loadLayoutFromString( R"html(
<vbox id="root">
<style>
#child { opacity: light-dark(0.75, 0.25); }
#child:selected { opacity: 0.5; }
</style>
<div id="child"></div>
</vbox>
)html" );
UIWidget* child = root->querySelector( "#child" );
EXPECT_TRUE( child != nullptr );
EXPECT_EQ( child->getAlpha(), 191.25f );
child->pushState( UIState::StateSelected );
EXPECT_EQ( child->getAlpha(), 127.5f );
sceneNode->setColorSchemePreference( ColorSchemePreference::Dark );
child->popState( UIState::StateSelected );
EXPECT_EQ( child->getAlpha(), 63.75f );
}
UTEST( CSSVariables, StylesheetReloadUsesNewLowerWinner ) {
UIApplication app(
WindowSettings( 800, 600, "eepp - CSS Reload State Rollback Test", WindowStyle::Default,
WindowBackend::Default, 32 ),
UIApplication::Settings( Sys::getProcessPath() + ".." + FileSystem::getOSSlash(), 1 ) );
UISceneNode* sceneNode = app.getUI();
sceneNode->setStyleSheet( R"css(
#child { opacity: 0.75; }
#child:selected { opacity: 0.5; }
)css" );
UIWidget* child = UIWidget::New();
child->setId( "child" );
child->setParent( sceneNode->getRoot() );
sceneNode->update( Time::Zero );
EXPECT_EQ( child->getAlpha(), 191.25f );
child->pushState( UIState::StateSelected );
EXPECT_EQ( child->getAlpha(), 127.5f );
sceneNode->setStyleSheet( R"css(
#child { opacity: 0.25; }
#child:selected { opacity: 0.5; }
)css" );
sceneNode->update( Time::Zero );
EXPECT_EQ( child->getAlpha(), 127.5f );
child->popState( UIState::StateSelected );
EXPECT_EQ( child->getAlpha(), 63.75f );
}
UTEST( CSSVariables, NativeFallbackSurvivesLowerRuleReloads ) {
UIApplication app(
WindowSettings( 800, 600, "eepp - CSS Reload Native Fallback Test", WindowStyle::Default,
WindowBackend::Default, 32 ),
UIApplication::Settings( Sys::getProcessPath() + ".." + FileSystem::getOSSlash(), 1 ) );
UISceneNode* sceneNode = app.getUI();
sceneNode->setStyleSheet( "#child:selected { opacity: 0.5; }" );
UIWidget* child = UIWidget::New();
child->setId( "child" );
child->setParent( sceneNode->getRoot() );
sceneNode->update( Time::Zero );
child->pushState( UIState::StateSelected );
EXPECT_EQ( child->getAlpha(), 127.5f );
sceneNode->setStyleSheet( "#child { opacity: 0.25; } #child:selected { opacity: 0.5; }" );
sceneNode->update( Time::Zero );
sceneNode->setStyleSheet( "#child:selected { opacity: 0.5; }" );
sceneNode->update( Time::Zero );
child->popState( UIState::StateSelected );
EXPECT_EQ( child->getAlpha(), 255.f );
}
UTEST( CSSVariables, InheritedFallbackUsesCurrentParentValue ) {
UIApplication app(
WindowSettings( 800, 600, "eepp - CSS Inherited Fallback Test", WindowStyle::Default,
WindowBackend::Default, 32 ),
UIApplication::Settings( Sys::getProcessPath() + ".." + FileSystem::getOSSlash(), 1 ) );
UIWidget* root = app.getUI()->loadLayoutFromString( R"html(
<html>
<head><style>
#parent { color: white; }
#child:selected { color: red; }
</style></head>
<body><div id="parent"><span id="child">Text</span></div></body>
</html>
)html" );
UIWidget* parent = root->querySelector( "#parent" );
UITextSpan* child = root->querySelector( "#child" )->asType<UITextSpan>();
EXPECT_TRUE( parent != nullptr );
EXPECT_TRUE( child != nullptr );
EXPECT_TRUE( child->getFontColor() == Color::White );
child->pushState( UIState::StateSelected );
EXPECT_TRUE( child->getFontColor() == Color::Red );
parent->setStyleSheetInlineProperty( "color", "#00ff00" );
parent->reloadStyle( true, false, true, true );
child->popState( UIState::StateSelected );
EXPECT_TRUE( child->getFontColor() == Color( "#00ff00" ) );
}
UTEST( CSSVariables, InheritedFallbackRestoresWhenSourceDisappears ) {
UIApplication app(
WindowSettings( 800, 600, "eepp - CSS Inherited Source Removal Test", WindowStyle::Default,
WindowBackend::Default, 32 ),
UIApplication::Settings( Sys::getProcessPath() + ".." + FileSystem::getOSSlash(), 1 ) );
UISceneNode* sceneNode = app.getUI();
sceneNode->setStyleSheet( "#parent { color: white; } #child:selected { color: red; }" );
UIWidget* root = sceneNode->loadLayoutFromString(
"<html><body><div id=\"parent\"><span id=\"child\">Text</span></div></body></html>" );
UITextSpan* child = root->querySelector( "#child" )->asType<UITextSpan>();
EXPECT_TRUE( child != nullptr );
EXPECT_TRUE( child->getFontColor() == Color::White );
child->pushState( UIState::StateSelected );
EXPECT_TRUE( child->getFontColor() == Color::Red );
sceneNode->setStyleSheet( "#child:selected { color: red; }" );
sceneNode->update( Time::Zero );
child->popState( UIState::StateSelected );
EXPECT_TRUE( child->getFontColor() == Color::White );
}
UTEST( CSSVariables, VolatileNativeFallbackRestores ) {
UIApplication app(
WindowSettings( 800, 600, "eepp - CSS Volatile Fallback Test", WindowStyle::Default,
WindowBackend::Default, 32 ),
UIApplication::Settings( Sys::getProcessPath() + ".." + FileSystem::getOSSlash(), 1 ) );
UIWidget* root = app.getUI()->loadLayoutFromString( R"html(
<vbox id="root">
<style>#parent:selected > #child { opacity: 0.5; }</style>
<div id="parent"><div id="child"></div></div>
</vbox>
)html" );
UIWidget* parent = root->querySelector( "#parent" );
UIWidget* child = root->querySelector( "#child" );
EXPECT_TRUE( parent != nullptr );
EXPECT_TRUE( child != nullptr );
EXPECT_EQ( child->getAlpha(), 255.f );
parent->pushState( UIState::StateSelected );
EXPECT_EQ( child->getAlpha(), 127.5f );
parent->popState( UIState::StateSelected );
EXPECT_EQ( child->getAlpha(), 255.f );
}
UTEST( CSSVariables, SiblingStateNativeFallbackRestores ) {
UIApplication app(
WindowSettings( 800, 600, "eepp - CSS Sibling Fallback Test", WindowStyle::Default,
WindowBackend::Default, 32 ),
UIApplication::Settings( Sys::getProcessPath() + ".." + FileSystem::getOSSlash(), 1 ) );
UIWidget* root = app.getUI()->loadLayoutFromString( R"html(
<vbox id="root">
<style>#source:selected + #child { opacity: 0.5; }</style>
<div id="source"></div><div id="child"></div>
</vbox>
)html" );
UIWidget* source = root->querySelector( "#source" );
UIWidget* child = root->querySelector( "#child" );
EXPECT_TRUE( source != nullptr );
EXPECT_TRUE( child != nullptr );
EXPECT_EQ( child->getAlpha(), 255.f );
source->pushState( UIState::StateSelected );
EXPECT_EQ( child->getAlpha(), 127.5f );
source->popState( UIState::StateSelected );
EXPECT_EQ( child->getAlpha(), 255.f );
}
UTEST( CSSVariables, IndexedNativeFallbackRestoresEachLayer ) {
UIApplication app(
WindowSettings( 800, 600, "eepp - CSS Indexed Fallback Test", WindowStyle::Default,
WindowBackend::Default, 32 ),
UIApplication::Settings( Sys::getProcessPath() + ".." + FileSystem::getOSSlash(), 1 ) );
UIWidget* root = app.getUI()->loadLayoutFromString( R"html(
<vbox id="root">
<style>#child:selected { background-tint: red, blue; }</style>
<div id="child"></div>
</vbox>
)html" );
UIWidget* child = root->querySelector( "#child" );
EXPECT_TRUE( child != nullptr );
child->setBackgroundTint( Color( "#112233" ), 0 );
child->setBackgroundTint( Color( "#445566" ), 1 );
EXPECT_STDSTREQ( "#112233", child->getBackgroundTint( 0 ).toHexString() );
EXPECT_STDSTREQ( "#445566", child->getBackgroundTint( 1 ).toHexString() );
for ( int cycle = 0; cycle < 8; ++cycle ) {
child->pushState( UIState::StateSelected );
EXPECT_TRUE( child->getBackgroundTint( 0 ) == Color::Red );
EXPECT_TRUE( child->getBackgroundTint( 1 ) == Color::Blue );
child->popState( UIState::StateSelected );
EXPECT_TRUE( child->getBackgroundTint( 0 ) == Color( "#112233" ) );
const std::string secondTint = child->getBackgroundTint( 1 ).toHexString();
EXPECT_STDSTREQ( Color( "#445566" ).toHexString(), secondTint );
EXPECT_FALSE( child->getUIStyle()->hasLocalProperty( PropertyId::BackgroundTint ) );
}
}
UTEST( CSSVariables, IndexedFallbackRestoresLayerWhileAnotherStateRemains ) {
UIApplication app(
WindowSettings( 800, 600, "eepp - CSS Indexed State Rollback Test", WindowStyle::Default,
WindowBackend::Default, 32 ),
UIApplication::Settings( Sys::getProcessPath() + ".." + FileSystem::getOSSlash(), 1 ) );
UIWidget* root = app.getUI()->loadLayoutFromString( R"html(
<vbox id="root">
<style>
#child:selected { background-tint: red, blue; }
#child:pressed { background-tint: green; }
</style>
<div id="child"></div>
</vbox>
)html" );
UIWidget* child = root->querySelector( "#child" );
EXPECT_TRUE( child != nullptr );
const Color originalTint( "#445566" );
child->setBackgroundTint( originalTint, 1 );
child->pushState( UIState::StateSelected );
EXPECT_TRUE( child->getBackgroundTint( 1 ) == Color::Blue );
child->pushState( UIState::StatePressed );
child->popState( UIState::StateSelected );
EXPECT_TRUE( child->getBackgroundTint( 0 ) == Color::Green );
EXPECT_TRUE( child->getBackgroundTint( 1 ) == originalTint );
child->popState( UIState::StatePressed );
EXPECT_TRUE( child->getBackgroundTint( 1 ) == originalTint );
}
UTEST( CSSVariables, IndexedFallbackRestoresUncoveredLayer ) {
UIApplication app(
WindowSettings( 800, 600, "eepp - CSS Partial Indexed Fallback Test", WindowStyle::Default,
WindowBackend::Default, 32 ),
UIApplication::Settings( Sys::getProcessPath() + ".." + FileSystem::getOSSlash(), 1 ) );
UIWidget* root = app.getUI()->loadLayoutFromString( R"html(
<vbox id="root">
<style>
#child { background-tint: #112233; }
#child:selected { background-tint: red, blue; }
</style>
<div id="child"></div>
</vbox>
)html" );
UIWidget* child = root->querySelector( "#child" );
EXPECT_TRUE( child != nullptr );
child->setBackgroundTint( Color( "#445566" ), 1 );
child->pushState( UIState::StateSelected );
EXPECT_TRUE( child->getBackgroundTint( 0 ) == Color::Red );
EXPECT_TRUE( child->getBackgroundTint( 1 ) == Color::Blue );
child->popState( UIState::StateSelected );
EXPECT_TRUE( child->getBackgroundTint( 0 ) == Color( "#112233" ) );
EXPECT_TRUE( child->getBackgroundTint( 1 ) == Color( "#445566" ) );
}
UTEST( CSSVariables, HTMLTableIndexedPropertyGetterForwardsIndex ) {
UIApplication app(
WindowSettings( 800, 600, "eepp - CSS Table Indexed Getter Test", WindowStyle::Default,
WindowBackend::Default, 32 ),
UIApplication::Settings( Sys::getProcessPath() + ".." + FileSystem::getOSSlash(), 1 ) );
UIWidget* root = app.getUI()->loadLayoutFromString( R"html(
<html><body><table id="table"><tr><td id="cell">Text</td></tr></table></body></html>
)html" );
const PropertyDefinition* tint =
StyleSheetSpecification::instance()->getProperty( PropertyId::BackgroundTint );
for ( const char* selector : { "#table", "#cell" } ) {
UIWidget* widget = root->querySelector( selector );
EXPECT_TRUE( widget != nullptr );
widget->setBackgroundTint( Color( "#112233" ), 0 );
widget->setBackgroundTint( Color( "#445566" ), 1 );
EXPECT_STDSTREQ( "#445566", widget->getPropertyString( tint, 1 ) );
}
}
UTEST( CSSVariables, NativeFallbackTransitionsInBothDirections ) {
UIApplication app(
WindowSettings( 800, 600, "eepp - CSS Native Fallback Transition Test",
WindowStyle::Default, WindowBackend::Default, 32 ),
UIApplication::Settings( Sys::getProcessPath() + ".." + FileSystem::getOSSlash(), 1 ) );
UISceneNode* sceneNode = app.getUI();
UIWidget* root = sceneNode->loadLayoutFromString( R"html(
<vbox id="root">
<style>
#child { transition: opacity 0.1s; }
#child:selected { opacity: 0.5; }
</style>
<div id="child"></div>
</vbox>
)html" );
UIWidget* child = root->querySelector( "#child" );
EXPECT_TRUE( child != nullptr );
const PropertyDefinition* opacity =
StyleSheetSpecification::instance()->getProperty( PropertyId::Opacity );
child->pushState( UIState::StateSelected );
EXPECT_EQ( child->getActionsByTag( opacity->getId() ).size(), 1UL );
sceneNode->update( Seconds( 0.2f ) );
EXPECT_EQ( child->getAlpha(), 127.5f );
child->popState( UIState::StateSelected );
EXPECT_EQ( child->getActionsByTag( opacity->getId() ).size(), 1UL );
sceneNode->update( Seconds( 0.2f ) );
EXPECT_EQ( child->getAlpha(), 255.f );
}
UTEST( CSSVariables, ParentStateChildOpacityTransitionReverts ) {
UIApplication app(
WindowSettings( 800, 600, "eepp - CSS Parent State Child Opacity Transition Test",
+59 -36
View File
@@ -1,4 +1,5 @@
#include "eproc.hpp"
#include "process_view.hpp"
#include "settingspanel.hpp"
#include <args/args.hxx>
@@ -6,6 +7,7 @@
#include <eepp/ui/tools/uisettingspanel.hpp>
#include <eepp/ui/uimenu.hpp>
#include <eepp/ui/uimenuitem.hpp>
#include <eepp/ui/uiwidgetcreator.hpp>
#include <nlohmann/json.hpp>
#include <algorithm>
@@ -604,6 +606,8 @@ bool App::init() {
} );
}
UIWidgetCreator::registerWidget( "eproc-table-view", ProcessTableView::New );
UIWidgetCreator::registerWidget( "eproc-tree-view", ProcessTreeView::New );
mRoot = ui->loadLayoutFromString( R"xml(
<style>
<![CDATA[
@@ -785,10 +789,10 @@ bool App::init() {
<TextInput id="search_input" lw="0" lw8="1" lh="wc" margin-right="4dp" />
<DropDownList id="filter_dropdown" lw="120dp" lh="wc" margin-right="4dp" />
</hbox>
<TableView id="process_table" lw="mp" lh="0" lw8="1"
<eproc-table-view id="process_table" lw="mp" lh="0" lw8="1"
column-width-mode-menu="true"
table-flags="headers|row-search|focus-on-selection|auto-columns" />
<TreeView id="process_tree" lw="mp" lh="0" lw8="1" visible="false"
<eproc-tree-view id="process_tree" lw="mp" lh="0" lw8="1" visible="false"
column-width-mode-menu="true"
table-flags="headers|row-search|focus-on-selection|auto-columns" />
<hbox id="status_bar" lw="mp" lh="wc" padding="4dp">
@@ -1075,6 +1079,8 @@ void App::setupUI() {
mFilterDropdown = mRoot->find<UIDropDownList>( "filter_dropdown" );
mTableView = mRoot->find<UITableView>( "process_table" );
mTreeView = mRoot->find<UITreeView>( "process_tree" );
static_cast<ProcessTableView*>( mTableView )->setSearchInput( mSearchInput );
static_cast<ProcessTreeView*>( mTreeView )->setSearchInput( mSearchInput );
mStatusText = mRoot->find<UITextView>( "process_count" );
mCpuText = mRoot->find<UITextView>( "cpu_text" );
mMemText = mRoot->find<UITextView>( "mem_text" );
@@ -1117,8 +1123,15 @@ void App::setupUI() {
if ( mEndProcessBtn )
mEndProcessBtn->onClick( [this]( const MouseEvent* ) { onEndProcess(); } );
if ( mSearchInput )
if ( mSearchInput ) {
mSearchInput->on( Event::OnTextChanged, [this]( const Event* ) { onSearchChanged(); } );
mSearchInput->on( Event::KeyDown, [this]( const Event* event ) {
const KeyEvent* key = event->asKeyEvent();
if ( key->getKeyCode() != KEY_DOWN || key->getSanitizedMod() != KEYMOD_NONE )
return;
focusFirstProcessRow( activeProcessView() );
} );
}
if ( mFilterDropdown )
mFilterDropdown->on( Event::OnItemSelected, [this]( const Event* ) { onFilterChanged(); } );
@@ -1503,6 +1516,13 @@ void App::setupProcessTable() {
view->setColumnWidth( ProcessModel::ColIcon, PixelDensity::dpToPx( 26 ) );
view->setOnSelectionChange( [this]() { onSelectionChange(); } );
view->on( Event::KeyDown, [this, view]( const Event* event ) {
const KeyEvent* key = event->asKeyEvent();
if ( view == activeProcessView() && key->getKeyCode() == KEY_A &&
( key->getSanitizedMod() & KeyMod::getDefaultModifier() ) && !view->isEditing() ) {
view->selectAll();
}
} );
view->onModelEvent( [this]( const ModelEvent* event ) {
if ( event->getModelEventType() == ModelEventType::OpenMenu )
showProcessContextMenu( event->getModelIndex() );
@@ -1994,6 +2014,20 @@ void App::showProcessContextMenu( const ModelIndex& proxyIndex ) {
signalMenu->setId( "process_signal_menu" );
for ( const auto& item : signalItems )
signalMenu->add( mApp->getUI()->i18n( item.key, item.label ) )->setId( item.id );
// Menu item events go to their immediate menu, not the parent context menu.
signalMenu->on( Event::OnItemClicked, [this, pids]( const Event* event ) {
const auto* item = event->getNode()->asType<UIMenuItem>();
if ( !item )
return;
for ( const auto& sig : signalItems ) {
if ( item->getId() == sig.id ) {
requestSignal( pids, sig.signal,
mApp->getUI()->i18n( "eproc_send_signal", "Send Signal" ).toUtf8(),
false );
break;
}
}
} );
menu->addSubMenu( mApp->getUI()->i18n( "eproc_send_signal", "Send Signal" ), nullptr,
signalMenu )
->setId( "send-signal" );
@@ -2017,41 +2051,30 @@ void App::showProcessContextMenu( const ModelIndex& proxyIndex ) {
menu->add( forceKillLabel( mApp->getUI(), pids.size() ) )->setId( "kill-process" );
#endif
menu->on( Event::OnItemClicked, [this, pids, parentPid, tracerPid,
copyCommandLine =
std::move( copyCommandLine )]( const Event* event ) {
UIMenuItem* item = event->getNode()->asType<UIMenuItem>();
if ( !item )
return;
menu->on( Event::OnItemClicked,
[this, pids, parentPid, tracerPid,
copyCommandLine = std::move( copyCommandLine )]( const Event* event ) {
UIMenuItem* item = event->getNode()->asType<UIMenuItem>();
if ( !item )
return;
const std::string id( item->getId() );
const std::string id( item->getId() );
if ( id == "jump-parent" ) {
selectProcess( parentPid );
} else if ( id == "jump-tracer" ) {
selectProcess( tracerPid );
} else if ( id == "copy-command-line" ) {
if ( !copyCommandLine.empty() && mApp->getWindow()->getClipboard() )
mApp->getWindow()->getClipboard()->setText( copyCommandLine );
} else if ( id == "end-process" ) {
requestSignal( pids, kEndProcessSignal,
endProcessLabel( mApp->getUI(), pids.size() ).toUtf8(), true );
} else if ( id == "kill-process" ) {
requestSignal( pids, kForceKillSignal,
forceKillLabel( mApp->getUI(), pids.size() ).toUtf8(), true );
} else {
// Signals picked explicitly from the submenu are sent straight away: the original
// only asks for confirmation on End Process and Forcibly Kill.
for ( const auto& sig : signalItems ) {
if ( id == sig.id ) {
requestSignal(
pids, sig.signal,
mApp->getUI()->i18n( "eproc_send_signal", "Send Signal" ).toUtf8(), false );
break;
}
}
}
} );
if ( id == "jump-parent" ) {
selectProcess( parentPid );
} else if ( id == "jump-tracer" ) {
selectProcess( tracerPid );
} else if ( id == "copy-command-line" ) {
if ( !copyCommandLine.empty() && mApp->getWindow()->getClipboard() )
mApp->getWindow()->getClipboard()->setText( copyCommandLine );
} else if ( id == "end-process" ) {
requestSignal( pids, kEndProcessSignal,
endProcessLabel( mApp->getUI(), pids.size() ).toUtf8(), true );
} else if ( id == "kill-process" ) {
requestSignal( pids, kForceKillSignal,
forceKillLabel( mApp->getUI(), pids.size() ).toUtf8(), true );
}
} );
Vector2f pos( mApp->getWindow()->getInput()->getMousePos().asFloat() );
menu->nodeToWorldTranslation( pos );
+59
View File
@@ -0,0 +1,59 @@
#ifndef EPROC_PROCESS_VIEW_HPP
#define EPROC_PROCESS_VIEW_HPP
#include <eepp/ui/uitableview.hpp>
#include <eepp/ui/uitextinput.hpp>
#include <eepp/ui/uitreeview.hpp>
using namespace EE;
using namespace EE::UI;
namespace eproc {
inline bool focusFirstProcessRow( UIAbstractTableView* view ) {
if ( !view || !view->getModel() || view->getModel()->rowCount() == 0 )
return false;
const ModelIndex first = view->getModel()->index(
0, view->isType( UI_TYPE_TREEVIEW ) ? view->getModel()->treeColumn() : 0 );
if ( !first.isValid() )
return false;
view->setFocus();
view->setSelection( first, true, false );
return true;
}
inline bool focusSearchFromFirstRow( UIAbstractTableView* view, UITextInput* searchInput,
const KeyEvent& event ) {
if ( !searchInput || event.getKeyCode() != KEY_UP || event.getSanitizedMod() != KEYMOD_NONE ||
view->isEditing() || !view->getModel() || view->getSelection().size() != 1 )
return false;
const ModelIndex selected = view->getSelection().first();
if ( !selected.isValid() || selected.row() != 0 || selected.parent().isValid() )
return false;
searchInput->setFocus();
return true;
}
template <typename Base> class ProcessView : public Base {
public:
static ProcessView* New() { return eeNew( ProcessView, () ); }
ProcessView() : Base() {}
void setSearchInput( UITextInput* searchInput ) { mSearchInput = searchInput; }
protected:
Uint32 onKeyDown( const KeyEvent& event ) override {
return focusSearchFromFirstRow( this, mSearchInput, event ) ? 1 : Base::onKeyDown( event );
}
private:
UITextInput* mSearchInput{ nullptr };
};
using ProcessTableView = ProcessView<UITableView>;
using ProcessTreeView = ProcessView<UITreeView>;
} // namespace eproc
#endif