diff --git a/CSS-rework.md b/CSS-rework.md new file mode 100644 index 00000000000..3f9e87d231b --- /dev/null +++ b/CSS-rework.md @@ -0,0 +1,478 @@ +# CSS Engine Rework Plan + +## Status (2026-05-07) + +- **Phase 0 — mechanical cleanups: all merged.** + - `BootstrapTheme3x` removal (#3975). + - `IOException` removal from `CSSEngine` String overloads (#3976). + - Dead `SACConstants` parser entries removed (#3977). + - Unused CSS serializer classes and dead color converter config (#3978). +- **Phase 1 — test safety net: all merged.** + - Phase 1 selector matching tests (#3970). + - Phase 2 parser round-trip tests (#3974). + - Padding gap-fill (#3979). Remaining handler gaps from the Phase 3 + audit in `css-testing.md` are scoped down; revisit only on regression. + - CSS selector integration tests for tab selection, `.active` class, + and preference pseudo (#3983, in review): pins three engine + behaviours that the matcher / parser unit tests do not exercise + end-to-end. +- **Phase 2 + Phase 3 step 1 — in flight on a single branch + (`vogella/css-engine-rework`).** Seven commits stacked, intended to + be pushed out as individual PRs once stable. + - `AbstractCSSEngine` merged into `CSSEngineImpl`. + - `AbstractCSSSWTEngineImpl` merged into `CSSSWTEngineImpl`. + - `ICSSPropertyHandler2` and `ICSSPropertyHandler2Delegate` folded + into `ICSSPropertyHandler` via Java 21 default methods. + - Unused `PropertyHelper` and its self-test deleted. + - The vendored 3,205-line `URI` copy replaced with a Require-Bundle + on `org.eclipse.emf.common`. + - Internal CSS Selector AST + matcher introduced (Phase 3 step 1 + foundation). + - **Phase 3 step 1 wiring (this commit):** SAC→`Selectors` + translator added at the parser-output boundary + (`CSSDocumentHandlerImpl.startSelector`); `CSSEngine.matches` and + `parseSelectors` switched to the internal `Selectors.Selector` + type; `CSSEngineImpl.matches` delegates to `SelectorMatcher`; + `applyConditionalPseudoStyle` rewritten to walk the internal AST; + parser configured with Batik's stock `DefaultSelectorFactory` / + `DefaultConditionFactory`; 23 vendored `impl/sac/*` selector and + condition wrappers deleted; the dead + `ExtendedDocumentCSS.queryConditionSelector` / + `querySelector` (and the `SAC_*_CONDITION` int constants behind + them) removed; `CSSEngineTest` and `SelectorTest` rewritten on + the internal AST. `CSSDocumentHandlerImpl`, + `DocumentHandlerFactoryImpl`, and `SACParserFactoryImpl` are the + only `impl/sac/*` classes still standing; they go in step 2. + Net ~−4,750 LOC plus +711 LOC of Selector AST scaffolding plus + +160 LOC of translator; all existing tests pass. + Phase 2 leftovers (helper consolidation, SAC-bound Abstract-class + merges, factory layer collapse) deferred: helper consolidation + is high blast radius, the SAC-bound work is Phase 4 collateral. +- **Phase 3 step 1 — drop SAC types from the engine:** complete on + `css-engine-rework`. Engine has zero SAC selector/condition types + in its public API or matcher; SAC selectors only appear at the + parser boundary inside `CSSDocumentHandlerImpl.startSelector`, + where the translator hands them to the internal AST. +- **Phase 3 step 2 — replace Batik with hand-written tokenizer:** not + started. Plan agreed: two commits (parser added but not wired, + then cut over + delete Batik and SAC plumbing). No fallback flag, + no parallel-parser smoke harness; existing test suite is the + gate. Phase 0 test gap-fill (`!important` parse-and-pin, + `:hover`/`:focus`/`:active` parser tolerance, string escapes in + attribute selectors, etc.) considered and skipped as not critical. +- **Phase 4 — DOM mirror replacement:** not started. +- **Phases 5–6:** not started. + +Goal: trim the e4 CSS stack (`org.eclipse.e4.ui.css.core`, +`org.eclipse.e4.ui.css.swt`, `org.eclipse.e4.ui.css.swt.theme`, +~30k LOC across 263 classes) to roughly two thirds of its current size +while keeping shipped Eclipse themes working. The shipped CSS subset is +small (see `css-testing.md`), and most of the bulk is dead-API plumbing, +parser wrapping, and one-class-per-property handler files. + +## Current state + +- Three bundles, ~30k LOC, 263 classes. Bundle `css.core` is internal + (`Export-Package: ...;x-friends:=` and `x-internal:=true` everywhere), + so signatures can change. +- Parser today: Apache Batik 1.9.x (2017) accessed through W3C SAC + (`org.w3c.css.sac` 1.3.0, last revised ~2003). 26 classes under + `impl/sac/*` plus 3 façade classes under `core/sac/*` adapt SAC into + the engine. ~40 files across the three bundles plus tests import SAC + types directly. +- A second mirror layer (`impl/dom/*`, ~32 classes) reimplements W3C + DOM-CSS types (`CSSStyleSheet`, `CSSStyleRule`, `CSSValue`, etc.) for + internal use only. Nothing outside the engine creates or mutates those + objects. +- A registry-driven handler dispatch (`RegistryCSSPropertyHandlerProvider`, + ~1.1k LOC) reads two extension points (`elementProvider`, + `propertyHandler`) that no in-tree contributor outside `css.swt` uses. +- 74 property-handler classes, most of them 40 to 50 line wrappers around + one setter call (`CSSPropertyMaximizeVisibleSWTHandler`, etc.). +- Phase 1 of `css-testing.md` is merged: `CSSEngineTest` covers + selector matching. Phases 2 (parser round-trip) and 3 (property + handlers) are the safety net for the rest of this plan. + +## Guiding constraints + +- Preserve the high-level engine contract used by callers: + `CSSEngine.applyStyles`, `matches`, `parseStyleSheet`. Internal types + (`impl/dom/*`, `impl/sac/*`, registry providers) are fair game. +- Limit semantic scope to what shipped Eclipse stylesheets use: type / + class / id selectors, `@import`, attribute selectors `=` and `~=`, + child / descendant combinators, `:selected` and `:disabled`. Out: + `@media`, `@font-face`, `:hover` / `:focus` / `:active`, `!important`. +- No regressions in shipped themes (`platform`, `dark`, + `org.eclipse.ui.themes` contributions). Each phase ships behind the + Phase 1 + 2 + 3 tests as the gate. +- Each phase ships as one (or a small number of) PR. No mega-PRs. + +## Phases + +The phases are roughly in landing order. Earlier phases unblock later +ones; risk grows as we move down. + +### Phase 0 — small mechanical cleanups (independent) + +Ship now, no design needed. + +- Drop `throws IOException` from `CSSEngine.parseSelectors(String)`, + `parsePropertyValue(String)`, and any other engine method whose String + overload only throws because of a `StringReader` hop. The signature + comes from SAC's `Parser.parseSelectors(InputSource)` and is a checked + exception that can never fire. Bundle is internal; callers fix in one + pass. +- Delete dead parser entries from `SACConstants` (`SteadyState`, the + unused Flute CSS3 variant, etc.). +- ~~Drop the `BootstrapTheme3x` shim and any other Eclipse 3.x compatibility + glue in `css.swt.theme`. The 3.x bridge runtime is no longer the + expected target.~~ Done (#3975). +- Audit and delete unused converter / serializer classes under `css.swt` + (the `converter` and `serializer` subpackages each have a couple of + entries with no in-tree caller). + +Effort: ~1 day. ~500 LOC removed. Low risk. + +### Phase 1 — finish the test safety net (from css-testing.md) + +- Phase 2: `StyleSheetStructureTest` for parser round-trip. +- Phase 3a: property-handler audit (no PR, just the gap list). +- Phase 3b: fill in handler tests using + `EclipsePreferencesHandlerTest` / `CSSSWTTestCase` patterns. + +Effort: 3 to 4 days, 2 PRs. ~1,000 LOC added. Without this in place the +later phases are flying blind. + +### Phase 2 — flatten engine and helper hierarchies (low risk, no API impact) + +Pure refactor. No behaviour change. Each bullet is its own PR. + +- Merge `AbstractCSSEngine` (1,113 LOC) and `CSSEngineImpl` (~95 LOC) + into a single class. Same for `AbstractCSSSWTEngineImpl` / + `CSSSWTEngineImpl`. These hierarchies have one concrete subclass each. +- Collapse `Abstract*Selector` and `Abstract*Condition` classes whose + only subclass is the concrete `*Impl`. Several of these go away + naturally during the SAC swap; do whichever ones can be done now + without touching SAC, leave the rest for Phase 4. +- Collapse the factory layers (`CSSSelectorFactoryImpl`, + `CSSConditionFactoryImpl`, `DocumentHandlerFactoryImpl`) where there + is only one concrete factory. +- Consolidate the 9 SWT helpers into 3 cohesive ones: `SwtCssColors` + (current `CSSSWTColorHelper`), `SwtCssFonts`, `SwtCssWidgets`. Delete + `PropertyHelper`, `CSSSWTHelpers`, `SWTStyleHelpers` after their + callers move. +- Merge `ICSSPropertyHandler2` and `ICSSPropertyHandler2Delegate` into + `ICSSPropertyHandler` using Java 21 default methods. The bundle uses + `BREE: JavaSE-21`; this is mechanical. + +Effort: 4 to 5 days, 4 to 5 PRs. ~2,000 LOC removed, ~25 classes +removed. Low risk. + +### Phase 3 — drop SAC, then drop Batik + +The original plan named "swap Batik" and "drop SAC" as one phase. They +are not the same change: empirically, Eclipse depends on Batik for +exactly one runtime artifact (`org.apache.batik.css.parser.Parser`, +loaded reflectively); everything else under our `impl/sac/*` is a +22-year-old vendored copy of Batik selector wrappers. So Phase 3 +splits cleanly into two steps that are individually shippable. + +**Step 1 — drop SAC types from the engine, keep Batik.** + +The engine API today exposes SAC types +(`org.w3c.css.sac.SelectorList parseSelectors(...)`, +`boolean matches(Selector, Object, String)`), and the 26 vendored +classes under `impl/sac/*` implement SAC interfaces +(`ExtendedSelector`, `ExtendedCondition`) extended with +`match(Element, pseudo)` and `getSpecificity()`. Replace this with: + +- An internal `Selector` AST as a sealed interface plus records: + `TypeSelector`, `ClassSelector`, `IdSelector`, `AttributeSelector`, + `PseudoClassSelector`, `CompoundSelector`, `DescendantSelector`, + `ChildSelector`, `SelectorList`. Engine-internal, no SAC. +- A `SelectorMatcher` service with `boolean matches(Selector, Element, + String pseudo)` that walks the new AST. +- A small translator that consumes the SAC selector trees the Batik + parser still produces and emits the new internal AST. Lives at the + parser-output boundary; called once per stylesheet load. Uses + Batik's stock SAC `DefaultSelectorFactory` / + `DefaultConditionFactory` instead of our vendored copies. +- `CSSEngine.matches` and `parseSelectors` change to return / accept + the internal `Selector` type. Internal API only (`x-friends`); no + external API break. +- Delete the 26 vendored classes under `impl/sac/*` and the 3 façade + classes under `core/sac/*` once they are no longer reachable. + +The Phase 1 selector-matching tests (`CSSEngineTest`, merged via +#3970) already assert through `engine.matches(...)`. They get rewritten +to drop the SAC `Selector` import and use the new internal type +directly. The Phase 2 round-trip tests do not touch selectors and stay +unchanged. + +Effort: 4 to 5 days, 1 PR. ~600 LOC added (AST + matcher + translator) +and ~2,000 LOC removed (the 26 vendored wrappers and the 3 façades). +Net ~−1,400 LOC. Medium risk: specificity calculation must match +current behaviour exactly so cascade ordering does not shift. + +**Step 2 — replace Batik with a hand-written CSS3 tokenizer.** + +After step 1 the only Batik touch-point is the reflectively-loaded +`org.apache.batik.css.parser.Parser` class plus a thin SAC layer +around it (3 remaining `impl/sac/*` parser-plumbing classes, 6 +`core/sac/*` façades). Replace all of that with a small hand-written +tokenizer + recursive-descent parser scoped to the CSS subset Eclipse +and downstream RCP applications use. That subset is not as small as +`css-testing.md` originally suggested: in addition to type / class / +id / `:pseudo` / attribute / child / descendant selectors, the engine +machinery actively supports `:focus` (Control), `:active` (Shell), +arbitrary pseudo-classes through `isPseudoInstanceOf`, and parses +(without applying) `!important`, `@media`, `@font-face`. The new +tokenizer must accept all of this even though no in-repo stylesheet +uses the dormant features. + +Rough sizing: + +| Piece | LOC | +|---|---| +| Tokenizer | 250–350 | +| Selector parser | 150–250 | +| Declaration / value parser | 150–250 | +| AST records | 100–200 | +| Specificity calculation | ~30 | +| **Total** | **~700–1,100** | + +Two commits, each shippable as its own PR. + +1. **Add the new parser, not yet wired.** New package + `impl/parser/` with the tokenizer, selector parser (emits + `Selectors.SelectorList` directly — no SAC, no translator), + declaration/value parser (emits the existing `impl/dom/*` types + so Phase 4 can replace those types in a focused follow-up), + stylesheet parser (emits `CSSStyleSheetImpl` / + `CSSStyleRuleImpl` / `CSSImportRuleImpl`; parses `@media` / + `@font-face` / `!important` and discards them), plus a + `CssParseException` replacing `org.w3c.css.sac.CSSException`. + Inline unit tests for tokenizer and parser. Pure addition; + reviewable in isolation. + +2. **Cut over and delete.** Wire the new parser into + `CSSEngineImpl.makeCSSParser`, drop + `Require-Bundle: org.apache.batik.css` and the SAC + `Import-Package`, delete the 3 remaining `impl/sac/*` classes, + the 6 `core/sac/*` façades, `SacTranslator`, `SACConstants`, + `core/dom/parsers/CSSParser` + `CSSParserFactory` + + `ICSSParserFactory`, `impl/dom/parsers/AbstractCSSParser` + + `CSSParserFactoryImpl`, and the four `InputSource` overloads on + `CSSEngine` (`Reader` / `InputStream` overloads remain). + Migrate the few non-engine callers off `InputSource`: + `ThemeEngine`, `ParserTestUtil`, `ImportTest`, the css.swt + margin/padding handlers' `CSSException` references. Bump + `org.eclipse.e4.ui.css.core` Bundle-Version (no API baseline + failure since the bundle's exports are all `x-internal` / + `x-friends`). + +No fallback: the legacy parser is removed in commit 2 in one go, +not behind a system property. No parallel-parser smoke harness: +the existing test suite (CSSEngineTest, SelectorMatcherTest, +StyleSheetStructureTest, CascadeTest, ValueTest, ImportTest, +MediaRulesTest, FontFaceRulesTest, InheritTest, ViewCSSTest) is +the gate. The Phase 0 test gap-fill (`!important` parse-and-pin, +`:hover`/`:focus`/`:active` parser tolerance, string escapes in +attribute selectors, expanded `url()` forms, `rgba`, +trailing-semicolon-optional, etc.) was considered and skipped as +not critical: if the new parser is wrong about any of these, the +shipped themes will fail visibly rather than silently. + +Effort: 3 to 5 days, 2 PRs. Net ~+900 / −3,400 across the two +commits, ~−2,500 net. + +Combined Phase 3 (step 1 + step 2): 2 PRs landed for step 1, 2 +PRs for step 2, net ~−3,900 LOC, drops one external runtime +dependency, leaves zero SAC types in the codebase. + +### Phase 4 — replace the W3C DOM mirror with internal POJOs (re-ordered, was Phase 3) + +`impl/dom/*` (~32 classes, 3,500 to 4,500 LOC) implements `CSSStyleSheet`, +`CSSStyleRule`, `CSSImportRule`, `CSSValueList`, `RGBColor`, `Measure`, +etc., for an SWT consumer that never asks for DOM compliance. After +Phase 3 the new `BatikStyleSheetParser` is the only producer of these +types and the SAC layer is gone, so the mirror can be replaced with +plain rule, selector, and value records: + +```java +record StyleSheet(List rules) {} +sealed interface Rule permits StyleRule, ImportRule {} +record StyleRule(List selectors, Map declarations) {} +record ImportRule(URI href) {} +sealed interface CssValue permits ColorValue, NumberValue, ListValue, KeywordValue, StringValue {} +``` + +Phase 3's parser now emits the POJOs instead of the wrapper classes; +engine and property handlers shift to read the new types directly. The +Phase 1 + Phase 2 tests in `css-testing.md` need to be rewritten on +the new types as part of this phase, since they currently assert +against the W3C DOM-CSS interfaces. + +Risk: medium. Cascade ordering, specificity, and `@import` resolution +all live here. Drop them carefully. + +Effort: 8 to 12 days, 1 large PR or 2 medium PRs. ~3,500 LOC removed, +20 to 25 classes removed. + +### Phase 5 — collapse trivial property-handler classes + +Today: `RegistryCSSPropertyHandlerProvider` reads the +`org.eclipse.e4.ui.css.core.propertyHandler` extension point, builds a +map keyed by element-class + property name, and dispatches into one of +74 handler classes. Most handlers are stateless one-liners — 15 to 20 +of them are near-identical boolean / int / color setters wrapped in +boilerplate. + +The registry-based dispatch stays. Clients override our handlers by +contributing to the same extension point, so we cannot bypass it for +our own handlers without breaking the override contract. The +consolidation happens inside the handler classes: + +- One `GenericBooleanSWTHandler` registered in `plugin.xml` for every + boolean SWT property (`maximize-visible`, `minimize-visible`, + `mru-visible`, ...). Its `applyCSSProperty(element, property, value, + ...)` dispatches on `property` to a small lookup map of + `BiConsumer`. +- Same shape for `GenericIntSWTHandler`, + `GenericColorSWTHandler`, etc., where the property-to-setter + mapping is regular. +- The non-trivial appliers (margins, paddings, preferences, + CTabFolder visual rendering) keep their dedicated classes; their + logic does not collapse cleanly. + +Plugin.xml contribution shape stays one entry per (element-class, +property): the registry still finds external overrides at the same +granularity. We just gain one bit of property dispatch inside the +shared handler. Net effect: ~30 wrapper classes deleted, ~3 to 5 +generic handlers added, no schema or contract change. + +Out of scope: removing or deprecating the `propertyHandler` / +`elementProvider` extension points, or `RegistryCSSPropertyHandlerProvider` +itself. They stay public and functional for downstream RCP products +that contribute custom handlers, including overrides of the new +generic handlers. This caps the LOC delta but keeps external +contracts intact. + +Effort: 5 to 7 days across 2 to 3 PRs. ~1,500 to 1,800 LOC removed, +~30 wrapper classes removed. Medium risk; the override path is +exercised by external contributors, so the new generic handlers must +not change observable behaviour for any single (element, property) +pair. + +### Phase 6 — merge `css.swt.theme` into `css.swt` + +`css.swt.theme` is 7 classes / ~1,100 LOC of theme manager wiring. It +does not justify its own bundle, MANIFEST, feature.xml entry, p2 IU, and +test bundle. Inline as an internal package of `css.swt`. + +This is logistics-heavy (feature.xml, target platform updates, +downstream build files reference the bundle by name) more than +code-heavy. Do it last, when no other phase is touching the bundle +boundary. + +Effort: 2 to 3 days. ~200 LOC net. Medium risk (build-system blast +radius). + +## Order of work + +| Order | Phase | LOC delta | Risk | +|---|---|---|---| +| 1 | Phase 0 — mechanical cleanups | ~-500 | Low | +| 2 | Phase 1 — finish test safety net | ~+1,000 | Low | +| 3 | Phase 2 — flatten hierarchies and helpers | ~-2,000 | Low | +| 4 | Phase 3 step 1 — drop SAC types, keep Batik | ~-1,400 | Medium | +| 5 | Phase 3 step 2 — replace Batik with hand-written tokenizer | ~-2,500 | Medium | +| 6 | Phase 4 — replace DOM mirror with POJOs | ~-3,500 | Medium | +| 7 | Phase 5 — collapse trivial property-handler classes | ~-1,700 | Medium | +| 8 | Phase 6 — merge `css.swt.theme` into `css.swt` | ~-200 | Medium | +| **Total** | | **~-10,800** | | + +Phases 3 and 4 swapped relative to the original plan: the parser is the +only producer of the W3C DOM mirror types, so replacing the parser +first leaves a single, well-scoped change to delete the mirror in +Phase 4. The original "DOM-mirror first, parser second" ordering would +have required a temporary W3C → POJO conversion layer to keep the SAC +parser feeding into a new model, plus rewriting the Phase 2 round-trip +tests twice (once when the model changes, again when the parser does). + +Roughly a third of the current LOC, in line with the upper end of the +analyses in `temp1`, `temp2`, `temp3`. + +## Performance & Optimization Benchmarks + +Following the integration of Phase 3 Step 1 (internal AST and matcher), profiling identified a performance regression during CSS theme swaps. A detailed optimization effort was carried out to establish a new performance baseline. + +### Bottlenecks Identified +1. **Dynamic Parent Resolution**: `SelectorMatcher` dynamically traversed parent nodes by invoking `element.getParentNode()`. In SWT, this triggers costly Map lookups and potential DOM element adapter instantiations. +2. **Traversal Overlap**: Redundant styling passes (`Shell.reskin(SWT.ALL)` followed immediately by recursive `CSSEngine.applyStyles`) caused elements to be styled multiple times in a single theme swap. + +### Optimizations Implemented +1. **Pre-computed Ancestor Hierarchy**: The matching path was overloaded to accept a pre-computed array of ancestor elements, avoiding dynamic `getParentNode()` lookups. +2. **Styling Sessions**: A thread-local `styledElements` session was introduced to track and prevent duplicate styling of widgets within a single theme swap operation. + +### Benchmark Results +The optimizations were verified using the stress test `CssThemeSwapPerformanceTest` (which styles a workbench containing 4,000+ SWT widgets and 20 Java editors). + +| Metric | Unoptimized Baseline (Regression) | Optimized Implementation | Improvement | +| :--- | :--- | :--- | :--- | +| **Median Time** | 2091.30 ms | 1322.55 ms (up to 1136.51 ms) | ~36.8% (up to ~45.6%) | +| **Mean Time** | 2119.23 ms | 1339.35 ms | ~36.8% | +| **Minimum Time** | 1890.94 ms | 1129.77 ms | ~40.3% | +| **Maximum Time** | 2425.58 ms | 2050.99 ms | ~15.4% | + +## Risks worth calling out + +- **Test coverage is thin.** Phase 1 (the test net) is non-negotiable. + Skipping it roughly doubles iteration counts on every later phase. +- **Theme regressions.** Add a smoke test that parses every `.css` under + `bundles/**/css/` with both old and new parser during Phase 4 and + compares selector text + declaration counts. Fail on divergence. +- **Closed-source RCP consumers.** Custom `CSSEngine` subclasses likely + have external users; the `propertyHandler` and `elementProvider` + extension points might. Both extension points stay public (see Phase + 5 — only our in-tree contributions move to a static dispatch). Phase + 6 (`css.swt.theme` inlining) is the only phase that removes a public + surface; ship it with a deprecation cycle. +- **Pseudo-element semantics.** The current SAC matcher has a quirk: + `CSSPseudoClassConditionImpl.match` returns + `!isStaticPseudoInstance(value)` when `pseudoE == null`. Lock this in + a Phase 1 test before Phase 4 starts, or define the new behaviour + explicitly and migrate the few `ElementAdapter` subclasses that depend + on the static-pseudo registration. +- **Specificity calculation.** Cascade order depends on it. Phase 2 + test additions must include a specificity case before Phase 3 lands. +- **Batik tightening.** Going from a SAC façade to direct Batik usage + couples us more to Batik. Acceptable in exchange for ~2k LOC removed + today; revisit if Batik itself ever needs to be replaced. + +## Out of scope + +- Replacing Batik with ph-css or a hand-written tokenizer. Defer until + after Phase 5 if at all. Mixing the parser swap with a dependency swap + is what makes /temp3 estimate Phase 4 at "high risk". +- Adding new CSS features (`:hover`, `!important`, `@media`). +- Deprecating or removing the `elementProvider` and `propertyHandler` + extension points. They stay public so downstream RCP products can + keep contributing custom handlers. Phase 5 only collapses our own + in-tree contributions; the registry-based dispatch path stays alive + for external contributors. +- Replacing the engine for non-SWT clients. The engine is SWT-only in + practice; treat that as an invariant. + +## Low-priority follow-ups + +- **Replace `CSSSWTTestCase` inheritance with a JUnit 5 extension.** + About 25 widget tests in `tests/org.eclipse.e4.ui.tests.css.swt` + currently extend `CSSSWTTestCase` to inherit a `display` field, an + engine factory, and a tearDown that disposes shells. Convert it to a + `BeforeEachCallback`/`AfterEachCallback` extension registered via + `@RegisterExtension CssSwtEngine css = new CssSwtEngine()`, which + drops the protected mutable state, lets tests use multiple engines + or stylesheets per test, and frees the test classes to extend other + bases. Defer until after Phase 3 of `css-testing.md` lands so this + migration does not merge-conflict against the gap-fill PRs. diff --git a/bundles/org.eclipse.e4.ui.css.core/.options b/bundles/org.eclipse.e4.ui.css.core/.options new file mode 100644 index 00000000000..a817bf6e9e9 --- /dev/null +++ b/bundles/org.eclipse.e4.ui.css.core/.options @@ -0,0 +1,13 @@ +# Debugging options for the org.eclipse.e4.ui.css.core plugin. + +# Master switch. Must be true for any of the more specific debug +# options below to take effect. +org.eclipse.e4.ui.css.core/debug=false + +# Accumulate per-phase timings inside the CSS engine (applyStyles, +# getComputedStyle, selector match, applyStyleDeclaration, +# applyCSSProperty) and print a summary to stdout after each +# top-level applyStyles call. Useful for measuring where time goes +# during a theme swap. Has no effect unless the master "debug" +# option is also true. +org.eclipse.e4.ui.css.core/debug/perf=false diff --git a/bundles/org.eclipse.e4.ui.css.core/META-INF/MANIFEST.MF b/bundles/org.eclipse.e4.ui.css.core/META-INF/MANIFEST.MF index 76e17ec59af..cb1459510d9 100644 --- a/bundles/org.eclipse.e4.ui.css.core/META-INF/MANIFEST.MF +++ b/bundles/org.eclipse.e4.ui.css.core/META-INF/MANIFEST.MF @@ -30,7 +30,8 @@ Export-Package: org.eclipse.e4.ui.css.core;x-internal:=true, org.eclipse.e4.ui.css.core.impl.dom;x-internal:=true, org.eclipse.e4.ui.css.core.impl.dom.parsers;x-internal:=true, org.eclipse.e4.ui.css.core.impl.dom.properties;x-friends:="org.eclipse.e4.ui.css.swt", - org.eclipse.e4.ui.css.core.impl.engine;x-friends:="org.eclipse.e4.ui.css.swt,org.eclipse.e4.ui.workbench.swt", + org.eclipse.e4.ui.css.core.impl.engine;x-friends:="org.eclipse.e4.ui.css.swt,org.eclipse.e4.ui.workbench.swt,org.eclipse.ui.tests.performance", + org.eclipse.e4.ui.css.core.impl.engine.selector;x-friends:="org.eclipse.e4.ui.tests.css.core", org.eclipse.e4.ui.css.core.impl.sac;x-internal:=true, org.eclipse.e4.ui.css.core.resources;x-friends:="org.eclipse.e4.ui.css.swt,org.eclipse.e4.ui.workbench.renderers.swt", org.eclipse.e4.ui.css.core.sac;x-internal:=true, diff --git a/bundles/org.eclipse.e4.ui.css.core/build.properties b/bundles/org.eclipse.e4.ui.css.core/build.properties index b845fbdbe42..883afd2dc17 100644 --- a/bundles/org.eclipse.e4.ui.css.core/build.properties +++ b/bundles/org.eclipse.e4.ui.css.core/build.properties @@ -16,7 +16,8 @@ bin.includes = META-INF/,\ .,\ plugin.xml,\ about.html,\ - plugin.properties + plugin.properties,\ + .options src.includes = schema/,\ apache_about_files/,\ about.html diff --git a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/dom/ExtendedCSSRule.java b/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/dom/ExtendedCSSRule.java index ae6bd4c11f5..1d50bb853ed 100644 --- a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/dom/ExtendedCSSRule.java +++ b/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/dom/ExtendedCSSRule.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2008, 2013 Angelo Zerr and others. + * Copyright (c) 2008, 2026 Angelo Zerr and others. * * This program and the accompanying materials * are made available under the terms of the Eclipse Public License 2.0 @@ -13,8 +13,7 @@ *******************************************************************************/ package org.eclipse.e4.ui.css.core.dom; -import org.w3c.css.sac.Selector; -import org.w3c.css.sac.SelectorList; +import org.eclipse.e4.ui.css.core.impl.engine.selector.Selectors; import org.w3c.dom.css.CSSRule; /** @@ -28,7 +27,7 @@ public interface ExtendedCSSRule extends CSSRule { public CSSPropertyList getCSSPropertyList(); /** - * Return the list of {@link Selector} of this {@link CSSRule}. + * Return the list of selectors of this {@link CSSRule}. */ - public SelectorList getSelectorList(); + public Selectors.SelectorList getSelectorList(); } diff --git a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/dom/ExtendedDocumentCSS.java b/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/dom/ExtendedDocumentCSS.java index f23d30d74cf..d3d7bc717d1 100644 --- a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/dom/ExtendedDocumentCSS.java +++ b/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/dom/ExtendedDocumentCSS.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2008, 2018 Angelo Zerr and others. + * Copyright (c) 2008, 2026 Angelo Zerr and others. * * This program and the accompanying materials * are made available under the terms of the Eclipse Public License 2.0 @@ -15,9 +15,6 @@ package org.eclipse.e4.ui.css.core.dom; import java.util.EventListener; -import java.util.List; -import org.w3c.css.sac.Condition; -import org.w3c.css.sac.Selector; import org.w3c.dom.css.DocumentCSS; import org.w3c.dom.stylesheets.StyleSheet; @@ -26,21 +23,10 @@ */ public interface ExtendedDocumentCSS extends DocumentCSS { - public static final Integer SAC_ID_CONDITION = Integer.valueOf(Condition.SAC_ID_CONDITION); - public static final Integer SAC_CLASS_CONDITION = Integer.valueOf(Condition.SAC_CLASS_CONDITION); - public static final Integer SAC_PSEUDO_CLASS_CONDITION = Integer.valueOf(Condition.SAC_PSEUDO_CLASS_CONDITION); - public static final Integer OTHER_SAC_CONDITIONAL_SELECTOR = Integer.valueOf(Selector.SAC_CONDITIONAL_SELECTOR); - - public static final Integer OTHER_SAC_SELECTOR = Integer.valueOf(999); - public void addStyleSheet(StyleSheet styleSheet); public void removeAllStyleSheets(); - public List queryConditionSelector(int conditionType); - - public List querySelector(int selectorType, int conditionType); - /** * @since 0.12.200 */ diff --git a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/dom/properties/ICSSPropertyHandler.java b/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/dom/properties/ICSSPropertyHandler.java index c7cef57e669..c7279da95be 100644 --- a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/dom/properties/ICSSPropertyHandler.java +++ b/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/dom/properties/ICSSPropertyHandler.java @@ -63,4 +63,23 @@ public default String retrieveCSSProperty(Object element, String property, Strin return null; } + /** + * Callback method called once after all CSS properties of a single + * declaration have been applied. Handlers that need to perform a final + * step (re-layout, redraw, batched commit, ...) override this method; + * the default is a no-op. + */ + default void onAllCSSPropertiesApplied(Object element, CSSEngine engine) throws Exception { + // do nothing + } + + /** + * Variant of {@link #onAllCSSPropertiesApplied(Object, CSSEngine)} that + * also receives the pseudo class. Defaults to delegating to the + * pseudo-less form. + */ + default void onAllCSSPropertiesApplied(Object element, CSSEngine engine, String pseudo) throws Exception { + onAllCSSPropertiesApplied(element, engine); + } + } diff --git a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/dom/properties/ICSSPropertyHandler2.java b/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/dom/properties/ICSSPropertyHandler2.java deleted file mode 100644 index 7461186491f..00000000000 --- a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/dom/properties/ICSSPropertyHandler2.java +++ /dev/null @@ -1,39 +0,0 @@ -/******************************************************************************* - * Copyright (c) 2008, 2015 Angelo Zerr and others. - * - * This program and the accompanying materials - * are made available under the terms of the Eclipse Public License 2.0 - * which accompanies this distribution, and is available at - * https://www.eclipse.org/legal/epl-2.0/ - * - * SPDX-License-Identifier: EPL-2.0 - * - * Contributors: - * Angelo Zerr - initial API and implementation - *******************************************************************************/ -package org.eclipse.e4.ui.css.core.dom.properties; - -import org.eclipse.e4.ui.css.core.engine.CSSEngine; - -/** - * CSS Property Handler to intercept when all CSS Properties are applied. - * - * @version 1.0.0 - * @author Angelo ZERR - */ -public interface ICSSPropertyHandler2 { - - /** - * Callback method called when all CSS properties are applied. - */ - default void onAllCSSPropertiesApplyed(Object element, CSSEngine engine) throws Exception { - // do nothing - } - - /** - * Callback method called when all CSS properties are applied. - */ - default void onAllCSSPropertiesApplyed(Object element, CSSEngine engine, String pseudo) throws Exception { - onAllCSSPropertiesApplyed(element, engine); - } -} diff --git a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/dom/properties/ICSSPropertyHandler2Delegate.java b/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/dom/properties/ICSSPropertyHandler2Delegate.java deleted file mode 100644 index db26da9c36e..00000000000 --- a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/dom/properties/ICSSPropertyHandler2Delegate.java +++ /dev/null @@ -1,30 +0,0 @@ -/******************************************************************************* - * Copyright (c) 2008, 2013 Angelo Zerr and others. - * - * This program and the accompanying materials - * are made available under the terms of the Eclipse Public License 2.0 - * which accompanies this distribution, and is available at - * https://www.eclipse.org/legal/epl-2.0/ - * - * SPDX-License-Identifier: EPL-2.0 - * - * Contributors: - * Angelo Zerr - initial API and implementation - *******************************************************************************/ -package org.eclipse.e4.ui.css.core.dom.properties; - -/** - * - * {@link ICSSPropertyHandler2} delegate. - * - * @version 1.0.0 - * @author Angelo ZERR - */ -public interface ICSSPropertyHandler2Delegate { - - /** - * Return {@link ICSSPropertyHandler2} to call when all CSS Properties are - * applied . - */ - public ICSSPropertyHandler2 getCSSPropertyHandler2(); -} diff --git a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/engine/CSSEngine.java b/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/engine/CSSEngine.java index a6624b84852..f036cf16154 100644 --- a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/engine/CSSEngine.java +++ b/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/engine/CSSEngine.java @@ -20,11 +20,10 @@ import org.eclipse.e4.ui.css.core.dom.IElementProvider; import org.eclipse.e4.ui.css.core.dom.properties.ICSSPropertyHandler; import org.eclipse.e4.ui.css.core.dom.properties.converters.ICSSValueConverter; +import org.eclipse.e4.ui.css.core.impl.engine.selector.Selectors; import org.eclipse.e4.ui.css.core.resources.IResourcesRegistry; import org.eclipse.e4.ui.css.core.util.resources.IResourcesLocatorManager; import org.w3c.css.sac.InputSource; -import org.w3c.css.sac.Selector; -import org.w3c.css.sac.SelectorList; import org.w3c.dom.Element; import org.w3c.dom.css.CSSStyleDeclaration; import org.w3c.dom.css.CSSStyleSheet; @@ -105,27 +104,27 @@ public interface CSSEngine { /** * Parse Selectors from String value. */ - SelectorList parseSelectors(String text); + Selectors.SelectorList parseSelectors(String text); /** * Parse Selectors from InputSource value. */ - SelectorList parseSelectors(InputSource source) throws IOException; + Selectors.SelectorList parseSelectors(InputSource source) throws IOException; /** * Parse Selectors from InputStream. */ - SelectorList parseSelectors(InputStream stream) throws IOException; + Selectors.SelectorList parseSelectors(InputStream stream) throws IOException; /** * Parse Selectors from String value. */ - SelectorList parseSelectors(Reader reader) throws IOException; + Selectors.SelectorList parseSelectors(Reader reader) throws IOException; /** * Check if the selector matches the object node. */ - boolean matches(Selector selector, Object node, String pseudo); + boolean matches(Selectors.Selector selector, Object node, String pseudo); /*--------------- Apply styles -----------------*/ diff --git a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/dom/CSSStyleRuleImpl.java b/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/dom/CSSStyleRuleImpl.java index 01e5822bd4a..23200bae6c2 100644 --- a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/dom/CSSStyleRuleImpl.java +++ b/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/dom/CSSStyleRuleImpl.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2008, 2015 Angelo Zerr and others. + * Copyright (c) 2008, 2026 Angelo Zerr and others. * * This program and the accompanying materials * are made available under the terms of the Eclipse Public License 2.0 @@ -17,8 +17,7 @@ import org.eclipse.e4.ui.css.core.dom.CSSPropertyList; import org.eclipse.e4.ui.css.core.dom.ExtendedCSSRule; -import org.w3c.css.sac.Selector; -import org.w3c.css.sac.SelectorList; +import org.eclipse.e4.ui.css.core.impl.engine.selector.Selectors; import org.w3c.dom.DOMException; import org.w3c.dom.css.CSSRule; import org.w3c.dom.css.CSSStyleDeclaration; @@ -27,10 +26,10 @@ public class CSSStyleRuleImpl extends CSSRuleImpl implements CSSStyleRule, ExtendedCSSRule { - private final SelectorList selectors; + private final Selectors.SelectorList selectors; private CSSStyleDeclaration styleDeclaration; - public CSSStyleRuleImpl(CSSStyleSheet parentStyleSheet, CSSRule parentRule, SelectorList selectors) { + public CSSStyleRuleImpl(CSSStyleSheet parentStyleSheet, CSSRule parentRule, Selectors.SelectorList selectors) { super(parentStyleSheet, parentRule); this.selectors = selectors; } @@ -55,17 +54,7 @@ public String getCssText() { @Override public String getSelectorText() { - StringBuilder sb = new StringBuilder(); - for (int selID = 0; selID < getSelectorList().getLength(); selID++) { - Selector item = getSelectorList().item(selID); - sb.append(item.toString()); - sb.append(", "); - } - if (getSelectorList().getLength() > 0) { - sb.delete(sb.length() - 2, sb.length()); - } - - return sb.toString(); + return selectors.text(); } @Override @@ -83,7 +72,7 @@ public void setSelectorText(String selectorText) throws DOMException { // Additional methods @Override - public SelectorList getSelectorList() { + public Selectors.SelectorList getSelectorList() { return selectors; } diff --git a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/dom/DocumentCSSImpl.java b/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/dom/DocumentCSSImpl.java index f9fba83d961..b87b42b143c 100644 --- a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/dom/DocumentCSSImpl.java +++ b/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/dom/DocumentCSSImpl.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2008, 2018 Angelo Zerr and others. + * Copyright (c) 2008, 2026 Angelo Zerr and others. * * This program and the accompanying materials * are made available under the terms of the Eclipse Public License 2.0 @@ -18,19 +18,10 @@ package org.eclipse.e4.ui.css.core.impl.dom; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; -import java.util.Map; -import org.eclipse.e4.ui.css.core.dom.ExtendedCSSRule; import org.eclipse.e4.ui.css.core.dom.ExtendedDocumentCSS; -import org.w3c.css.sac.ConditionalSelector; -import org.w3c.css.sac.Selector; -import org.w3c.css.sac.SelectorList; import org.w3c.dom.Element; -import org.w3c.dom.css.CSSRule; -import org.w3c.dom.css.CSSRuleList; import org.w3c.dom.css.CSSStyleDeclaration; -import org.w3c.dom.css.CSSStyleSheet; import org.w3c.dom.css.DocumentCSS; import org.w3c.dom.stylesheets.StyleSheet; import org.w3c.dom.stylesheets.StyleSheetList; @@ -42,11 +33,6 @@ public class DocumentCSSImpl implements ExtendedDocumentCSS { private final StyleSheetListImpl styleSheetList = new StyleSheetListImpl(); - /** - * key=selector type, value = CSSStyleDeclaration - */ - private Map> styleDeclarationMap; - private final List styleSheetChangeListeners = new ArrayList<>(1); @Override @@ -72,93 +58,6 @@ public void removeAllStyleSheets() { styleSheetChangeListeners.forEach(l -> l.styleSheetRemoved(styleSheet)); } styleSheetList.removeAllStyleSheets(); - this.styleDeclarationMap = null; - } - - @Override - public List queryConditionSelector(int conditionType) { - return querySelector(Selector.SAC_CONDITIONAL_SELECTOR, conditionType); - } - - @Override - public List querySelector(int selectorType, int conditionType) { - List list = getCSSStyleDeclarationList(selectorType, conditionType); - if (list != null) { - return list; - } - int l = styleSheetList.getLength(); - for (int i = 0; i < l; i++) { - CSSStyleSheet styleSheet = (CSSStyleSheet) styleSheetList.item(i); - CSSRuleList ruleList = styleSheet.getCssRules(); - list = querySelector(ruleList, selectorType, conditionType); - setCSSStyleDeclarationList(list, selectorType, conditionType); - } - return list; - } - - protected List querySelector(CSSRuleList ruleList, int selectorType, int selectorConditionType) { - List list = new ArrayList<>(); - if (selectorType == Selector.SAC_CONDITIONAL_SELECTOR) { - int length = ruleList.getLength(); - for (int i = 0; i < length; i++) { - CSSRule rule = ruleList.item(i); - if (rule.getType() == CSSRule.STYLE_RULE && rule instanceof ExtendedCSSRule r) { - SelectorList selectorList = r.getSelectorList(); - // Loop for SelectorList - int l = selectorList.getLength(); - for (int j = 0; j < l; j++) { - Selector selector = selectorList.item(j); - if (selector.getSelectorType() == selectorType) { - // It's conditional selector - ConditionalSelector conditionalSelector = (ConditionalSelector) selector; - short conditionType = conditionalSelector.getCondition().getConditionType(); - if (selectorConditionType == conditionType) { - // current selector match the current CSS - // Rule - // CSSStyleRule styleRule = (CSSStyleRule) - // rule; - list.add(selector); - } - } - } - } - } - } - return list; - } - - protected List getCSSStyleDeclarationList(int selectorType, int conditionType) { - Integer key = getKey(selectorType, conditionType); - return getStyleDeclarationMap().get(key); - } - - protected void setCSSStyleDeclarationList(List list, int selectorType, int conditionType) { - Integer key = getKey(selectorType, conditionType); - getStyleDeclarationMap().put(key, list); - } - - protected Integer getKey(int selectorType, int conditionType) { - if (selectorType == Selector.SAC_CONDITIONAL_SELECTOR) { - if (conditionType == SAC_CLASS_CONDITION.intValue()) { - return SAC_CLASS_CONDITION; - } - if (conditionType == SAC_ID_CONDITION.intValue()) { - return SAC_ID_CONDITION; - } - if (conditionType == SAC_PSEUDO_CLASS_CONDITION.intValue()) { - return SAC_PSEUDO_CLASS_CONDITION; - } - return OTHER_SAC_CONDITIONAL_SELECTOR; - } - - return OTHER_SAC_SELECTOR; - } - - protected Map> getStyleDeclarationMap() { - if (styleDeclarationMap == null) { - styleDeclarationMap = new HashMap<>(); - } - return styleDeclarationMap; } @Override diff --git a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/dom/ViewCSSImpl.java b/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/dom/ViewCSSImpl.java index c628ce9cf41..080e3deeb10 100644 --- a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/dom/ViewCSSImpl.java +++ b/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/dom/ViewCSSImpl.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2008, 2018 Angelo Zerr and others. + * Copyright (c) 2008, 2026 Angelo Zerr and others. * * This program and the accompanying materials * are made available under the terms of the Eclipse Public License 2.0 @@ -21,11 +21,10 @@ import java.util.List; import org.eclipse.e4.ui.css.core.dom.ExtendedCSSRule; import org.eclipse.e4.ui.css.core.dom.ExtendedDocumentCSS; -import org.eclipse.e4.ui.css.core.impl.sac.ExtendedSelector; -import org.w3c.css.sac.Selector; -import org.w3c.css.sac.SelectorList; +import org.eclipse.e4.ui.css.core.impl.engine.CSSCorePolicy; +import org.eclipse.e4.ui.css.core.impl.engine.selector.SelectorMatcher; +import org.eclipse.e4.ui.css.core.impl.engine.selector.Selectors; import org.w3c.dom.Element; -import org.w3c.dom.Node; import org.w3c.dom.css.CSSRule; import org.w3c.dom.css.CSSRuleList; import org.w3c.dom.css.CSSStyleDeclaration; @@ -112,47 +111,62 @@ private List getCombinedRules() { } private CSSStyleDeclaration getComputedStyle(List ruleList, Element elt, String pseudoElt) { - Node parent = elt.getParentNode(); - - Node[] hierarchy = null; - if (parent != null) { - List hierarchyList = new ArrayList<>(); - for (Node n = parent; n != null; n = n.getParentNode()) { - hierarchyList.add(n); + final boolean trace = CSSCorePolicy.DEBUG_PERF; + long t0 = trace ? System.nanoTime() : 0; + try { + return getComputedStyleInternal(ruleList, elt, pseudoElt, trace); + } finally { + if (trace) { + CSSCorePolicy.getComputedStyleNs.addAndGet(System.nanoTime() - t0); + CSSCorePolicy.getComputedStyleCount.incrementAndGet(); } - hierarchy = hierarchyList.toArray(new Node[hierarchyList.size()]); } + } + private CSSStyleDeclaration getComputedStyleInternal(List ruleList, Element elt, String pseudoElt, + boolean trace) { List styleDeclarations = null; StyleWrapper firstStyleDeclaration = null; int position = 0; + + int depth = 0; + for (org.w3c.dom.Node n = elt; n instanceof Element; n = n.getParentNode()) { + depth++; + } + Element[] hierarchy = new Element[depth]; + int idx = 0; + for (org.w3c.dom.Node n = elt; n instanceof Element; n = n.getParentNode()) { + hierarchy[idx++] = (Element) n; + } + for (CSSRule rule : ruleList) { if (rule.getType() != CSSRule.STYLE_RULE || (!(rule instanceof ExtendedCSSRule)) ) { continue; // we only handle the CSSRule.STYLE_RULE and ExtendedCSSRule case } CSSStyleRule styleRule = (CSSStyleRule) rule; ExtendedCSSRule r = (ExtendedCSSRule) rule; - SelectorList selectorList = r.getSelectorList(); - // Loop for SelectorList - int l = selectorList.getLength(); - for (int j = 0; j < l; j++) { - Selector selector = selectorList.item(j); - if (selector instanceof ExtendedSelector extendedSelector) { - if (extendedSelector.match(elt, hierarchy, 0, pseudoElt)) { - CSSStyleDeclaration style = styleRule.getStyle(); - int specificity = extendedSelector.getSpecificity(); - StyleWrapper wrapper = new StyleWrapper(style, specificity, position++); - if (firstStyleDeclaration == null) { - firstStyleDeclaration = wrapper; - } else { - // There is several Style Declarations which - // match the current element - if (styleDeclarations == null) { - styleDeclarations = new ArrayList<>(); - styleDeclarations.add(firstStyleDeclaration); - } - styleDeclarations.add(wrapper); + Selectors.SelectorList selectorList = r.getSelectorList(); + for (Selectors.Selector selector : selectorList.alternatives()) { + long m0 = trace ? System.nanoTime() : 0; + boolean matched = SelectorMatcher.matches(selector, elt, pseudoElt, hierarchy, 0); + if (trace) { + CSSCorePolicy.selectorMatchNs.addAndGet(System.nanoTime() - m0); + CSSCorePolicy.selectorMatchCount.incrementAndGet(); + } + if (matched) { + CSSStyleDeclaration style = styleRule.getStyle(); + int specificity = selector.specificity(); + StyleWrapper wrapper = new StyleWrapper(style, specificity, position++); + if (firstStyleDeclaration == null) { + firstStyleDeclaration = wrapper; + } else { + // There is several Style Declarations which + // match the current element + if (styleDeclarations == null) { + styleDeclarations = new ArrayList<>(); + styleDeclarations.add(firstStyleDeclaration); } + styleDeclarations.add(wrapper); } } } diff --git a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/engine/AbstractCSSEngine.java b/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/engine/AbstractCSSEngine.java deleted file mode 100644 index 66823482a9c..00000000000 --- a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/engine/AbstractCSSEngine.java +++ /dev/null @@ -1,1122 +0,0 @@ -/******************************************************************************* - * Copyright (c) 2008, 2020 Angelo Zerr and others. - * - * This program and the accompanying materials - * are made available under the terms of the Eclipse Public License 2.0 - * which accompanies this distribution, and is available at - * https://www.eclipse.org/legal/epl-2.0/ - * - * SPDX-License-Identifier: EPL-2.0 - * - * Contributors: - * Angelo Zerr - initial API and implementation - * IBM Corporation - ongoing development - * Red Hat Inc. (mistria) - Fixes suggested by FindBugs - * Red Hat Inc. (mistria) - Bug 413348: fix stream leak - * Lars Vogel - Bug 428715 - * Brian de Alwis (MTI) - Performance tweaks (Bug 430829) - * Dirk Fauth - Bug 479896 - * Patrik Suzzi - Bug 500402 - * Daniel Raap - Bug 511836 - *******************************************************************************/ -package org.eclipse.e4.ui.css.core.impl.engine; - -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.io.Reader; -import java.io.StringReader; -import java.net.URL; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.function.BiConsumer; -import org.eclipse.core.runtime.FileLocator; -import org.eclipse.core.runtime.IPath; -import org.eclipse.e4.ui.css.core.dom.CSSStylableElement; -import org.eclipse.e4.ui.css.core.dom.ChildVisibilityAwareElement; -import org.eclipse.e4.ui.css.core.dom.ExtendedCSSRule; -import org.eclipse.e4.ui.css.core.dom.ExtendedDocumentCSS; -import org.eclipse.e4.ui.css.core.dom.IElementProvider; -import org.eclipse.e4.ui.css.core.dom.IStreamingNodeList; -import org.eclipse.e4.ui.css.core.dom.parsers.CSSParser; -import org.eclipse.e4.ui.css.core.dom.properties.ICSSPropertyCompositeHandler; -import org.eclipse.e4.ui.css.core.dom.properties.ICSSPropertyHandler; -import org.eclipse.e4.ui.css.core.dom.properties.ICSSPropertyHandler2; -import org.eclipse.e4.ui.css.core.dom.properties.ICSSPropertyHandler2Delegate; -import org.eclipse.e4.ui.css.core.dom.properties.ICSSPropertyHandlerProvider; -import org.eclipse.e4.ui.css.core.dom.properties.converters.ICSSValueConverter; -import org.eclipse.e4.ui.css.core.engine.CSSElementContext; -import org.eclipse.e4.ui.css.core.engine.CSSEngine; -import org.eclipse.e4.ui.css.core.engine.CSSErrorHandler; -import org.eclipse.e4.ui.css.core.exceptions.UnsupportedPropertyException; -import org.eclipse.e4.ui.css.core.impl.dom.CSSRuleListImpl; -import org.eclipse.e4.ui.css.core.impl.dom.CSSStyleSheetImpl; -import org.eclipse.e4.ui.css.core.impl.dom.DocumentCSSImpl; -import org.eclipse.e4.ui.css.core.impl.dom.ViewCSSImpl; -import org.eclipse.e4.ui.css.core.impl.sac.ExtendedSelector; -import org.eclipse.e4.ui.css.core.resources.IResourcesRegistry; -import org.eclipse.e4.ui.css.core.resources.ResourceRegistryKeyFactory; -import org.eclipse.e4.ui.css.core.util.impl.resources.ResourcesLocatorManager; -import org.eclipse.e4.ui.css.core.util.resources.IResourcesLocatorManager; -import org.eclipse.e4.ui.css.core.utils.StringUtils; -import org.w3c.css.sac.AttributeCondition; -import org.w3c.css.sac.CombinatorCondition; -import org.w3c.css.sac.Condition; -import org.w3c.css.sac.ConditionalSelector; -import org.w3c.css.sac.DescendantSelector; -import org.w3c.css.sac.InputSource; -import org.w3c.css.sac.Selector; -import org.w3c.css.sac.SelectorList; -import org.w3c.dom.Element; -import org.w3c.dom.Node; -import org.w3c.dom.NodeList; -import org.w3c.dom.css.CSSImportRule; -import org.w3c.dom.css.CSSRule; -import org.w3c.dom.css.CSSRuleList; -import org.w3c.dom.css.CSSStyleDeclaration; -import org.w3c.dom.css.CSSStyleSheet; -import org.w3c.dom.css.CSSValue; -import org.w3c.dom.css.DocumentCSS; -import org.w3c.dom.css.ViewCSS; -import org.w3c.dom.stylesheets.StyleSheet; - -/** - * Abstract CSS Engine manage style sheet parsing and store the - * {@link CSSStyleSheet} into {@link DocumentCSS}. - * - * To apply styles, call the {@link #applyStyles(Object, boolean, boolean)} - * method. This method check if {@link ICSSPropertyHandler} is registered for - * apply the CSS property. - * - * @version 1.0.0 - * @author Angelo ZERR - */ -public abstract class AbstractCSSEngine implements CSSEngine { - - /** - * Archives are deliberately identified by exclamation mark in URLs - */ - private static final String ARCHIVE_IDENTIFIER = "!"; - - /** - * Default {@link IResourcesLocatorManager} used to get InputStream, Reader - * resource like Image. - */ - private static final IResourcesLocatorManager defaultResourcesLocatorManager = ResourcesLocatorManager.INSTANCE; - - /** - * w3c {@link DocumentCSS}. - */ - private final ExtendedDocumentCSS documentCSS; - - /** - * w3c {@link ViewCSS}. - */ - private final ViewCSS viewCSS; - - /** - * {@link IElementProvider} used to retrieve w3c Element linked to the - * widget. - */ - private IElementProvider elementProvider; - - protected boolean computeDefaultStyle = false; - - private Map elementsContext = null; - - /** - * CSS Error Handler to intercept error while parsing, applying styles. - */ - private CSSErrorHandler errorHandler; - - private IResourcesLocatorManager resourcesLocatorManager; - - private IResourcesRegistry resourcesRegistry; - - /** - * An ordered list of ICSSPropertyHandlerProvider - */ - protected List propertyHandlerProviders = new ArrayList<>(); - // for performance hold a map of handlers to singleton list - private final Map> propertyHandler2InstanceMap = new HashMap<>(); - - private Map currentCSSPropertiesApplied; - - private boolean throwError; - - private Map valueConverters = null; - - private int parseImport; - - private ResourceRegistryKeyFactory keyFactory; - - public AbstractCSSEngine() { - this(new DocumentCSSImpl()); - } - - public AbstractCSSEngine(ExtendedDocumentCSS documentCSS) { - this.documentCSS = documentCSS; - this.viewCSS = new ViewCSSImpl(documentCSS); - keyFactory = new ResourceRegistryKeyFactory(); - } - - /*--------------- Parse style sheet -----------------*/ - - @Override - public StyleSheet parseStyleSheet(Reader reader) throws IOException { - InputSource source = new InputSource(); - source.setCharacterStream(reader); - return parseStyleSheet(source); - } - - @Override - public StyleSheet parseStyleSheet(InputStream stream) throws IOException { - InputSource source = new InputSource(); - source.setByteStream(stream); - return parseStyleSheet(source); - } - - @Override - public StyleSheet parseStyleSheet(InputSource source) throws IOException { - // Check that CharacterStream or ByteStream is not null - checkInputSource(source); - CSSParser parser = makeCSSParser(); - CSSStyleSheet styleSheet = parser.parseStyleSheet(source); - - CSSRuleList rules = styleSheet.getCssRules(); - int length = rules.getLength(); - CSSRuleListImpl masterList = new CSSRuleListImpl(); - int counter; - for (counter = 0; counter < length; counter++) { - CSSRule rule = rules.item(counter); - if (rule.getType() != CSSRule.IMPORT_RULE) { - break; - } - // processing an import CSS - CSSImportRule importRule = (CSSImportRule) rule; - URL url = null; - if (importRule.getHref().startsWith("platform")) { - url = FileLocator.resolve(new URL(importRule.getHref())); - } else { - IPath p = IPath.fromOSString(source.getURI()); - IPath trim = p.removeLastSegments(1); - boolean isArchive = source.getURI().contains(ARCHIVE_IDENTIFIER); - url = FileLocator - .resolve(new URL(trim.addTrailingSeparator().toString() + ((CSSImportRule) rule).getHref())); - File testFile = new File(url.getFile()); - if (!isArchive&&!testFile.exists()) { - // look in platform default - String path = getResourcesLocatorManager().resolve((importRule).getHref()); - testFile = new File(new URL(path).getFile()); - if (testFile.exists()) { - url = new URL(path); - } - } - } - try (InputStream stream = url.openStream()) { - InputSource tempStream = new InputSource(); - tempStream.setURI(url.toString()); - tempStream.setByteStream(stream); - parseImport++; - try { - styleSheet = (CSSStyleSheet) this.parseStyleSheet(tempStream); - } finally { - parseImport--; - } - CSSRuleList tempRules = styleSheet.getCssRules(); - for (int j = 0; j < tempRules.getLength(); j++) { - masterList.add(tempRules.item(j)); - } - } - } - - // add remaining non import rules - for (int i = counter; i < length; i++) { - masterList.add(rules.item(i)); - } - - // final stylesheet - CSSStyleSheetImpl s = new CSSStyleSheetImpl(); - s.setRuleList(masterList); - if (parseImport == 0) { - documentCSS.addStyleSheet(s); - } - return s; - } - - private void processNodeList(NodeList nodes, BiConsumer consumer, boolean applyStylesToChildNodes) { - if (nodes instanceof IStreamingNodeList) { - ((IStreamingNodeList) nodes).stream().forEach(child -> { - consumer.accept(child, applyStylesToChildNodes); - }); - } else { - int length = nodes.getLength(); - for (int k = 0; k < length; k++) { - consumer.accept(nodes.item(k), applyStylesToChildNodes); - } - } - } - - /** - * Return true if source is valid and false otherwise. - */ - private void checkInputSource(InputSource source) throws IOException { - Reader reader = source.getCharacterStream(); - InputStream stream = source.getByteStream(); - if (reader == null && stream == null) { - throw new IOException( - "CharacterStream or ByteStream cannot be null for the InputSource."); - } - } - - /*--------------- Parse style declaration -----------------*/ - - @Override - public CSSStyleDeclaration parseStyleDeclaration(String style) { - try { - return parseStyleDeclaration(new StringReader(style)); - } catch (IOException e) { - throw new IllegalStateException("StringReader cannot throw IOException", e); //$NON-NLS-1$ - } - } - - @Override - public CSSStyleDeclaration parseStyleDeclaration(Reader reader) throws IOException { - InputSource source = new InputSource(); - source.setCharacterStream(reader); - return parseStyleDeclaration(source); - } - - @Override - public CSSStyleDeclaration parseStyleDeclaration(InputStream stream) throws IOException { - InputSource source = new InputSource(); - source.setByteStream(stream); - return parseStyleDeclaration(source); - } - - @Override - public CSSStyleDeclaration parseStyleDeclaration(InputSource source) throws IOException { - checkInputSource(source); - CSSParser parser = makeCSSParser(); - return parser.parseStyleDeclaration(source); - } - - /*--------------- Parse CSS Selector -----------------*/ - - @Override - public SelectorList parseSelectors(String selector) { - try { - return parseSelectors(new StringReader(selector)); - } catch (IOException e) { - throw new IllegalStateException("StringReader cannot throw IOException", e); //$NON-NLS-1$ - } - } - - @Override - public SelectorList parseSelectors(Reader reader) throws IOException { - InputSource source = new InputSource(); - source.setCharacterStream(reader); - return parseSelectors(source); - } - - @Override - public SelectorList parseSelectors(InputStream stream) throws IOException { - InputSource source = new InputSource(); - source.setByteStream(stream); - return parseSelectors(source); - } - - @Override - public SelectorList parseSelectors(InputSource source) throws IOException { - checkInputSource(source); - CSSParser parser = makeCSSParser(); - return parser.parseSelectors(source); - } - - /*--------------- Parse CSS Property Value-----------------*/ - - @Override - public CSSValue parsePropertyValue(Reader reader) throws IOException { - InputSource source = new InputSource(); - source.setCharacterStream(reader); - return parsePropertyValue(source); - } - - @Override - public CSSValue parsePropertyValue(InputStream stream) throws IOException { - InputSource source = new InputSource(); - source.setByteStream(stream); - return parsePropertyValue(source); - } - - @Override - public CSSValue parsePropertyValue(String value) { - try { - return parsePropertyValue(new StringReader(value)); - } catch (IOException e) { - throw new IllegalStateException("StringReader cannot throw IOException", e); //$NON-NLS-1$ - } - } - - @Override - public CSSValue parsePropertyValue(InputSource source) throws IOException { - checkInputSource(source); - CSSParser parser = makeCSSParser(); - return parser.parsePropertyValue(source); - } - - /*--------------- Apply styles -----------------*/ - - @Override - public void applyStyles(Object element, boolean applyStylesToChildNodes) { - applyStyles(element, applyStylesToChildNodes, computeDefaultStyle); - } - - @Override - public void applyStyles(Object element, boolean applyStylesToChildNodes, boolean computeDefaultStyle) { - Element elt = getElement(element); - if (elt == null || !isVisible(elt)) { - return; - } - - /* - * Compute new Style to apply. - */ - CSSStyleDeclaration style = viewCSS.getComputedStyle(elt, null); - if (computeDefaultStyle) { - if (applyStylesToChildNodes) { - this.computeDefaultStyle = computeDefaultStyle; - } - /* - * Apply default style. - */ - applyDefaultStyleDeclaration(element, false, style, null); - } - - /* - * Manage static pseudo instances - */ - String[] pseudoInstances = getStaticPseudoInstances(elt); - if (pseudoInstances != null && pseudoInstances.length > 0) { - // there are static pseudo instances defined, loop for it and - // apply styles for each pseudo instance. - for (String pseudoInstance : pseudoInstances) { - CSSStyleDeclaration styleWithPseudoInstance = viewCSS.getComputedStyle(elt, pseudoInstance); - if (computeDefaultStyle) { - /* - * Apply default style for the current pseudo instance. - */ - applyDefaultStyleDeclaration(element, false, styleWithPseudoInstance, pseudoInstance); - } - - if (styleWithPseudoInstance != null) { - CSSRule parentRule = styleWithPseudoInstance.getParentRule(); - if (parentRule instanceof ExtendedCSSRule) { - applyConditionalPseudoStyle((ExtendedCSSRule) parentRule, pseudoInstance, element, styleWithPseudoInstance); - } else { - applyStyleDeclaration(elt, styleWithPseudoInstance, pseudoInstance); - } - } - } - } - - if (style != null) { - applyStyleDeclaration(elt, style, null); - } - try { - // Apply inline style - applyInlineStyle(elt, false); - } catch (Exception e) { - handleExceptions(e); - } - - if (applyStylesToChildNodes) { - /* - * Style all children recursive. - */ - NodeList nodes = elt instanceof ChildVisibilityAwareElement c - ? c.getVisibleChildNodes() - : elt.getChildNodes(); - if (nodes != null) { - processNodeList(nodes, this::applyStyles, applyStylesToChildNodes); - onStylesAppliedToChildNodes(elt, nodes); - } - } - } - - /** - * Allow the CSS engine to skip particular elements if they are not visible. - * Elements need to be restyled when they become visible. - * - * @return true if the element is visible, false if not visible. - */ - protected boolean isVisible(Element elt) { - Node parentNode = elt.getParentNode(); - if (parentNode instanceof ChildVisibilityAwareElement) { - NodeList l = ((ChildVisibilityAwareElement) parentNode).getVisibleChildNodes(); - if (l != null) { - if (l instanceof IStreamingNodeList) { - return ((IStreamingNodeList) l).stream().anyMatch(node -> node == elt); - } else { - int length = l.getLength(); - for (int i = 0; i < length; i++) { - if (l.item(i) == elt) { - return true; - } - } - } - } - return false; - } - return true; - } - - private void applyConditionalPseudoStyle(ExtendedCSSRule parentRule, String pseudoInstance, Object element, CSSStyleDeclaration styleWithPseudoInstance) { - SelectorList selectorList = parentRule.getSelectorList(); - for (int j = 0; j < selectorList.getLength(); j++) { - Selector item = selectorList.item(j); - // search for conditional selectors - ConditionalSelector conditional = null; - if (item instanceof ConditionalSelector) { - conditional = (ConditionalSelector) item; - } else if (item instanceof DescendantSelector) { - if (((DescendantSelector) item).getSimpleSelector() instanceof ConditionalSelector) { - conditional = (ConditionalSelector) ((DescendantSelector) item).getSimpleSelector(); - } else if (((DescendantSelector) item).getAncestorSelector() instanceof ConditionalSelector) { - conditional = (ConditionalSelector) ((DescendantSelector) item).getAncestorSelector(); - } - } - if (conditional != null) { - Condition condition = conditional.getCondition(); - // we're only interested in attribute selector conditions - AttributeCondition attr = null; - if (condition instanceof AttributeCondition) { - attr = (AttributeCondition) condition; - } else if (condition instanceof CombinatorCondition) { - if (((CombinatorCondition) condition).getSecondCondition() instanceof AttributeCondition) { - attr = (AttributeCondition) ((CombinatorCondition) condition).getSecondCondition(); - } else if (((CombinatorCondition) condition).getFirstCondition() instanceof AttributeCondition) { - attr = (AttributeCondition) ((CombinatorCondition) condition).getFirstCondition(); - } - } - if (attr != null) { - String value = attr.getValue(); - if (value.equals(pseudoInstance)) { - // if we match the pseudo, apply the style - applyStyleDeclaration(element, styleWithPseudoInstance, pseudoInstance); - return; - } - } - } - } - } - - protected String[] getStaticPseudoInstances(Element element) { - if (element instanceof CSSStylableElement stylableElement) { - return stylableElement.getStaticPseudoInstances(); - } - return null; - } - - /** - * Callback method called when styles applied of nodes - * children of the element. - */ - protected void onStylesAppliedToChildNodes(Element element, NodeList nodes) { - if (element instanceof CSSStylableElement) { - ((CSSStylableElement) element).onStylesApplied(nodes); - } - } - - /*--------------- Apply style declaration -----------------*/ - - @Override - public void applyStyleDeclaration(Object element, CSSStyleDeclaration style, String pseudo) { - // Apply style - boolean avoidanceCacheInstalled = currentCSSPropertiesApplied == null; - if (avoidanceCacheInstalled) { - currentCSSPropertiesApplied = new HashMap<>(); - } - List handlers2 = Collections.emptyList(); - for (int i = 0; i < style.getLength(); i++) { - String property = style.item(i); - CSSValue value = style.getPropertyCSSValue(property); - try { - ICSSPropertyHandler handler = this.applyCSSProperty(element, property, value, pseudo); - ICSSPropertyHandler2 propertyHandler2 = null; - if (handler instanceof ICSSPropertyHandler2) { - propertyHandler2 = (ICSSPropertyHandler2) handler; - } else if (handler instanceof ICSSPropertyHandler2Delegate) { - propertyHandler2 = ((ICSSPropertyHandler2Delegate) handler).getCSSPropertyHandler2(); - } - if (propertyHandler2 != null) { - switch (handlers2.size()) { - case 0: - handlers2 = propertyHandler2InstanceMap.computeIfAbsent(propertyHandler2, - Collections::singletonList); - break; - case 1: - handlers2 = new ArrayList<>(handlers2); - handlers2.add(propertyHandler2); - break; - default: - if (!handlers2.contains(propertyHandler2)) { - handlers2.add(propertyHandler2); - } - } - } - } catch (Exception e) { - if (throwError || (!throwError && !(e instanceof UnsupportedPropertyException))) { - handleExceptions(e); - } - } - } - for (ICSSPropertyHandler2 handler2 : handlers2) { - try { - handler2.onAllCSSPropertiesApplyed(element, this, pseudo); - } catch (Exception e) { - handleExceptions(e); - } - } - if (avoidanceCacheInstalled) { - currentCSSPropertiesApplied = null; - } - - } - - @Override - public CSSStyleDeclaration parseAndApplyStyleDeclaration(Object node, - Reader reader) throws IOException { - CSSStyleDeclaration style = parseStyleDeclaration(reader); - this.applyStyleDeclaration(node, style, null); - return style; - } - - @Override - public CSSStyleDeclaration parseAndApplyStyleDeclaration(Object node, InputStream stream) throws IOException { - CSSStyleDeclaration style = parseStyleDeclaration(stream); - this.applyStyleDeclaration(node, style, null); - return style; - } - - @Override - public CSSStyleDeclaration parseAndApplyStyleDeclaration(Object node, InputSource source) throws IOException { - CSSStyleDeclaration style = parseStyleDeclaration(source); - this.applyStyleDeclaration(node, style, null); - return style; - } - - @Override - public CSSStyleDeclaration parseAndApplyStyleDeclaration(Object node, String style) throws IOException { - CSSStyleDeclaration styleDeclaration = parseStyleDeclaration(style); - this.applyStyleDeclaration(node, styleDeclaration, null); - return styleDeclaration; - } - - /*--------------- Apply inline style -----------------*/ - - @Override - public void applyInlineStyle(Object node, boolean applyStylesToChildNodes) { - Element elt = getElement(node); - if (elt != null) { - if (elt instanceof CSSStylableElement stylableElement) { - String style = stylableElement.getCSSStyle(); - if (style != null && style.length() > 0) { - try { - parseAndApplyStyleDeclaration(stylableElement.getNativeWidget(), style); - } catch (IOException e) { - handleExceptions(e); - } - } - } - if (applyStylesToChildNodes) { - /* - * Style all children recursive. - */ - NodeList nodes = elt.getChildNodes(); - if (nodes != null) { - processNodeList(nodes, this::applyInlineStyle, applyStylesToChildNodes); - } - } - } - } - - /*--------------- Initial Style -----------------*/ - - @Override - public CSSStyleDeclaration getDefaultStyleDeclaration(Object element, String pseudoE) { - return getDefaultStyleDeclaration(element, null, pseudoE); - } - - public CSSStyleDeclaration getDefaultStyleDeclaration(Object widget, CSSStyleDeclaration newStyle, String pseudoE) { - CSSStyleDeclaration style = null; - for (ICSSPropertyHandlerProvider provider : propertyHandlerProviders) { - try { - style = provider.getDefaultCSSStyleDeclaration(this, widget, newStyle, pseudoE); - } catch (Exception e) { - handleExceptions(e); - } - } - return style; - } - - @Override - public void applyDefaultStyleDeclaration(Object element, boolean applyStylesToChildNodes) { - applyDefaultStyleDeclaration(element, applyStylesToChildNodes, null, null); - } - - public void applyDefaultStyleDeclaration(Object element, boolean applyStylesToChildNodes, - CSSStyleDeclaration newStyle, String pseudoE) { - // Initial styles must be computed or applied - Element elt = getElement(element); - if (elt != null) { - if (elt instanceof CSSStylableElement stylableElement) { - CSSStyleDeclaration oldDefaultStyleDeclaration = stylableElement.getDefaultStyleDeclaration(pseudoE); - CSSStyleDeclaration defaultStyleDeclaration = getDefaultStyleDeclaration( - element, newStyle, pseudoE); - if (oldDefaultStyleDeclaration != null) { - // Second apply styles, apply the initial style - // before apply the new style - try { - throwError = false; - applyStyleDeclaration(element, defaultStyleDeclaration, pseudoE); - } finally { - throwError = true; - } - } - } - if (applyStylesToChildNodes) { - /* - * Style all children recursive. - */ - NodeList nodes = elt.getChildNodes(); - if (nodes != null) { - processNodeList(nodes, this::applyDefaultStyleDeclaration, applyStylesToChildNodes); - onStylesAppliedToChildNodes(elt, nodes); - } - } - } - } - - /** - * Delegates the handle method. - * - * @param element - * may be a widget or a node or some object - */ - @Override - public ICSSPropertyHandler applyCSSProperty(Object element, String property, CSSValue value, String pseudo) - throws Exception { - if (currentCSSPropertiesApplied != null && currentCSSPropertiesApplied.containsKey(property)) { - // CSS Property was already applied, ignore it. - return null; - } - - element = getElement(element); // in case we're passed a node - if ("inherit".equals(value.getCssText())) { - // go to parent node - Element actualElement = (Element) element; - Node parentNode = actualElement.getParentNode(); - // get CSS property value - String parentValueString = retrieveCSSProperty(parentNode, property, pseudo); - // and convert it to a CSS value, overriding the "inherit" setting - // with the parent value - value = parsePropertyValue(parentValueString); - } - - for (ICSSPropertyHandlerProvider provider : propertyHandlerProviders) { - Collection handlers = provider.getCSSPropertyHandlers(element, property); - if (handlers == null) { - continue; - } - for (ICSSPropertyHandler handler : handlers) { - try { - boolean result = handler.applyCSSProperty(element, property, value, pseudo, this); - if (result) { - // Add CSS Property to flag that this CSS Property was - // applied. - if (currentCSSPropertiesApplied != null) { - currentCSSPropertiesApplied.put(property, property); - } - return handler; - } - } catch (Exception e) { - if (throwError || (!throwError && !(e instanceof UnsupportedPropertyException))) { - handleExceptions(e); - } - } - } - } - - return null; - } - - @Override - public String retrieveCSSProperty(Object element, String property, String pseudo) { - try { - element = getElement(element); // in case we're passed a node - for (ICSSPropertyHandlerProvider provider : propertyHandlerProviders) { - Collection handlers = provider.getCSSPropertyHandlers(element, property); - if (handlers == null) { - continue; - } - for (ICSSPropertyHandler handler : handlers) { - String value = handler.retrieveCSSProperty(element, property, pseudo, this); - if (!StringUtils.isEmpty(value)) { - return value; - } - } - } - } catch (Exception e) { - handleExceptions(e); - } - return null; - } - - @Override - public String[] getCSSCompositePropertiesNames(String property) { - try { - Collection handlers = getCSSPropertyHandlers(property); - if (handlers == null) { - return null; - } - for (ICSSPropertyHandler handler : handlers) { - if (handler instanceof ICSSPropertyCompositeHandler compositeHandler) { - if (compositeHandler.isCSSPropertyComposite(property)) { - return compositeHandler.getCSSPropertiesNames(property); - } - } - } - } catch (Exception e) { - handleExceptions(e); - } - return null; - } - - protected Collection getCSSPropertyHandlers(String property) throws Exception { - Collection handlers = new ArrayList<>(); - for (ICSSPropertyHandlerProvider provider : propertyHandlerProviders) { - Collection h = provider.getCSSPropertyHandlers(property); - if (handlers == null) { - handlers = h; - } else { - handlers = new ArrayList<>(handlers); - handlers.addAll(h); - } - } - return handlers; - } - - /** - * Return the set of property names and handlers for the provided node. - * - * @return the property names and handlers - */ - @Override - public Collection getCSSProperties(Object element) { - Set properties = new HashSet<>(); - for (ICSSPropertyHandlerProvider provider : propertyHandlerProviders) { - properties.addAll(provider.getCSSProperties(element)); - } - return properties; - } - - /*--------------- Dynamic pseudo classes -----------------*/ - - @Override - public IElementProvider getElementProvider() { - return elementProvider; - } - - @Override - public void setElementProvider(IElementProvider elementProvider) { - this.elementProvider = elementProvider; - } - - /** - * Return the w3c Element linked to the Object element. - */ - @Override - public Element getElement(Object element) { - Element elt = null; - if (element == null) { - return elt; - } - CSSElementContext elementContext = getCSSElementContext(element); - if (elementContext != null) { - if (!elementContext.elementMustBeRefreshed(elementProvider)) { - return elementContext.getElement(); - } - } - if (element instanceof Element) { - elt = (Element) element; - } else if (elementProvider != null) { - elt = elementProvider.getElement(element, this); - } - if (elt != null) { - if (elementContext == null) { - elementContext = new CSSElementContextImpl(); - Object nativeWidget = getNativeWidget(element); - hookNativeWidget(nativeWidget); - getElementsContext().put(nativeWidget, elementContext); - } - elementContext.setElementProvider(elementProvider); - elementContext.setElement(elt); - if (elt instanceof CSSStylableElement) { - // Initialize CSS stylable element - ((CSSStylableElement)elt).initialize(); - } - - } - return elt; - } - - /** - * Called when an element context is created for a native widget and - * registered with this engine. Subclasses should override and install - * a listener on the widget that will call {@link #handleWidgetDisposed(Object)} - * when the widget is disposed. - *

- * The default implementation of this method does nothing. - *

- * - * @param widget the native widget to hook - */ - protected void hookNativeWidget(Object widget) { - } - - /** - * Called when a widget is disposed. Removes the element context - * from the element contexts map and the widgets map. Overriding - * classes must call the super implementation. - */ - @Override - public void handleWidgetDisposed(Object widget) { - if (elementsContext != null) { - elementsContext.remove(widget); - } - } - - @Override - public CSSElementContext getCSSElementContext(Object element) { - Object o = getNativeWidget(element); - return getElementsContext().get(o); - } - - public Object getNativeWidget(Object element) { - Object o = element; - if (element instanceof CSSStylableElement) { - o = ((CSSStylableElement) o).getNativeWidget(); - } - return o; - } - - protected Map getElementsContext() { - if (elementsContext == null) { - elementsContext = new HashMap<>(); - } - return elementsContext; - } - - @Override - public boolean matches(Selector selector, Object element, String pseudoElt) { - Element elt = getElement(element); - if (elt == null) { - return false; - } - if (selector instanceof ExtendedSelector extendedSelector) { - return extendedSelector.match(elt, pseudoElt); - } else { - // TODO : selector is not batik ExtendedSelector, - // Manage this case... - } - return false; - } - - /*--------------- Error Handler -----------------*/ - - /** - * Handle exceptions thrown while parsing, applying styles. By default this - * method call CSS Error Handler if it is initialized. - */ - @Override - public void handleExceptions(Exception e) { - if (errorHandler != null) { - errorHandler.error(e); - } - } - - @Override - public CSSErrorHandler getErrorHandler() { - return errorHandler; - } - - /** - * Set the CSS Error Handler to manage exception. - */ - @Override - public void setErrorHandler(CSSErrorHandler errorHandler) { - this.errorHandler = errorHandler; - } - - /*--------------- Resources Locator Manager -----------------*/ - - @Override - public IResourcesLocatorManager getResourcesLocatorManager() { - if (resourcesLocatorManager == null) { - return defaultResourcesLocatorManager; - } - return resourcesLocatorManager; - } - - @Override - public void setResourcesLocatorManager( - IResourcesLocatorManager resourcesLocatorManager) { - this.resourcesLocatorManager = resourcesLocatorManager; - } - - /*--------------- Document/View CSS -----------------*/ - - @Override - public DocumentCSS getDocumentCSS() { - return documentCSS; - } - - @Override - public ViewCSS getViewCSS() { - return viewCSS; - } - - @Override - public void dispose() { - reset(); - // Call dispose for each CSSStylableElement which was registered - Collection contexts = elementsContext.values(); - for (CSSElementContext context : contexts) { - Element element = context.getElement(); - if (element instanceof CSSStylableElement) { - ((CSSStylableElement) element).dispose(); - } - } - // FIXME: should dispose element provider and the property handler - // providers - elementsContext = null; - if (resourcesRegistry != null) { - resourcesRegistry.dispose(); - } - } - - @Override - public void reset() { - // Remove All Style Sheets - documentCSS.removeAllStyleSheets(); - } - - /*--------------- Resources Registry -----------------*/ - - @Override - public IResourcesRegistry getResourcesRegistry() { - return resourcesRegistry; - } - - @Override - public void setResourcesRegistry(IResourcesRegistry resourcesRegistry) { - this.resourcesRegistry = resourcesRegistry; - } - - public void registerCSSPropertyHandlerProvider(ICSSPropertyHandlerProvider handlerProvider) { - propertyHandlerProviders.add(handlerProvider); - } - - public void unregisterCSSPropertyHandlerProvider(ICSSPropertyHandlerProvider handlerProvider) { - propertyHandlerProviders.remove(handlerProvider); - } - - /*--------------- CSS Value Converter -----------------*/ - - @Override - public void registerCSSValueConverter(ICSSValueConverter converter) { - if (valueConverters == null) { - valueConverters = new HashMap<>(); - } - valueConverters.put(converter.getToType(), converter); - } - - @Override - public void unregisterCSSValueConverter(ICSSValueConverter converter) { - if (valueConverters == null) { - return; - } - valueConverters.remove(converter); - } - - @Override - public ICSSValueConverter getCSSValueConverter(Object toType) { - if (valueConverters != null) { - return valueConverters.get(toType); - } - return null; - } - - @Override - public Object convert(CSSValue value, Object toType, Object context) throws Exception { - if ("unset".equals(value.getCssText())) { - return null; - } - Object key = keyFactory.createKey(value); - Object newValue = getResource(toType, key); - - if (newValue == null) { - ICSSValueConverter converter = getCSSValueConverter(toType); - if (converter != null) { - newValue = converter.convert(value, this, context); - // cache it - registerResource(toType, key, newValue); - } - } - return newValue; - } - - private Object getResource(Object toType, Object key) { - if (key != null && getResourcesRegistry() != null) { - return getResourcesRegistry().getResource(toType, key); - } - return null; - } - - private void registerResource(Object toType, Object key, Object resource) { - if (key != null && resource != null && getResourcesRegistry() != null) { - getResourcesRegistry().registerResource(toType, key, resource); - } - } - - @Override - public String convert(Object value, Object toType, Object context) - throws Exception { - if (value == null) { - return null; - } - ICSSValueConverter converter = getCSSValueConverter(toType); - if (converter != null) { - return converter.convert(value, this, context); - } - return null; - } - - /*--------------- Abstract methods -----------------*/ - - /** - * Return instance of CSS Parser. - */ - public abstract CSSParser makeCSSParser(); - - protected void setResourceRegistryKeyFactory(ResourceRegistryKeyFactory keyFactory) { - this.keyFactory = keyFactory; - } -} diff --git a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/engine/CSSCorePolicy.java b/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/engine/CSSCorePolicy.java new file mode 100644 index 00000000000..0ae7b273626 --- /dev/null +++ b/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/engine/CSSCorePolicy.java @@ -0,0 +1,147 @@ +/******************************************************************************* + * Copyright (c) 2026 Lars Vogel and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + *******************************************************************************/ +package org.eclipse.e4.ui.css.core.impl.engine; + +import java.util.concurrent.atomic.AtomicLong; + +import org.eclipse.core.runtime.Platform; + +/** + * Debug / tracing policy for the CSS core engine. Reads its booleans from the + * standard Eclipse {@code .options} file at the bundle root (visible in the + * Tracing tab of an Eclipse run configuration). + * + *

+ * When {@link #DEBUG_PERF} is true, the engine accumulates per-phase wall + * times and prints a summary after each top-level applyStyles call. Tests may + * also flip the flag programmatically via {@link #setPerfEnabled(boolean)}. + *

+ */ +public final class CSSCorePolicy { + + private static final String PLUGIN_ID = "org.eclipse.e4.ui.css.core"; //$NON-NLS-1$ + + public static volatile boolean DEBUG = false; + public static volatile boolean DEBUG_PERF = false; + + static { + if (option("/debug")) { //$NON-NLS-1$ + DEBUG = true; + DEBUG_PERF = option("/debug/perf"); //$NON-NLS-1$ + } + } + + private static boolean option(String suffix) { + return "true".equalsIgnoreCase(Platform.getDebugOption(PLUGIN_ID + suffix)); //$NON-NLS-1$ + } + + public static void setPerfEnabled(boolean enabled) { + DEBUG = enabled || DEBUG; + DEBUG_PERF = enabled; + } + + // Counters and total nanoseconds for the instrumented hot paths. + public static final AtomicLong applyStylesCount = new AtomicLong(); + public static final AtomicLong applyStylesNs = new AtomicLong(); + public static final AtomicLong getComputedStyleCount = new AtomicLong(); + public static final AtomicLong getComputedStyleNs = new AtomicLong(); + public static final AtomicLong selectorMatchCount = new AtomicLong(); + public static final AtomicLong selectorMatchNs = new AtomicLong(); + public static final AtomicLong applyStyleDeclarationCount = new AtomicLong(); + public static final AtomicLong applyStyleDeclarationNs = new AtomicLong(); + public static final AtomicLong applyCSSPropertyCount = new AtomicLong(); + public static final AtomicLong applyCSSPropertyNs = new AtomicLong(); + public static final AtomicLong parseStyleSheetCount = new AtomicLong(); + public static final AtomicLong parseStyleSheetNs = new AtomicLong(); + public static final AtomicLong resetCount = new AtomicLong(); + public static final AtomicLong resetNs = new AtomicLong(); + public static final AtomicLong applyInlineStyleCount = new AtomicLong(); + public static final AtomicLong applyInlineStyleNs = new AtomicLong(); + public static final AtomicLong applyStylesEntryNs = new AtomicLong(); + public static final AtomicLong pseudoCascadeNs = new AtomicLong(); + public static final AtomicLong childRecursionNs = new AtomicLong(); + public static final AtomicLong handlerInvokeCount = new AtomicLong(); + public static final AtomicLong handlerInvokeNs = new AtomicLong(); + public static final AtomicLong handlerLookupCount = new AtomicLong(); + public static final AtomicLong handlerLookupNs = new AtomicLong(); + public static final AtomicLong reapplyCount = new AtomicLong(); + public static final AtomicLong reapplyNs = new AtomicLong(); + public static final AtomicLong reskinCount = new AtomicLong(); + public static final AtomicLong reskinNs = new AtomicLong(); + + public static void reset() { + applyStylesCount.set(0); + applyStylesNs.set(0); + getComputedStyleCount.set(0); + getComputedStyleNs.set(0); + selectorMatchCount.set(0); + selectorMatchNs.set(0); + applyStyleDeclarationCount.set(0); + applyStyleDeclarationNs.set(0); + applyCSSPropertyCount.set(0); + applyCSSPropertyNs.set(0); + parseStyleSheetCount.set(0); + parseStyleSheetNs.set(0); + resetCount.set(0); + resetNs.set(0); + applyInlineStyleCount.set(0); + applyInlineStyleNs.set(0); + applyStylesEntryNs.set(0); + pseudoCascadeNs.set(0); + childRecursionNs.set(0); + handlerInvokeCount.set(0); + handlerInvokeNs.set(0); + handlerLookupCount.set(0); + handlerLookupNs.set(0); + reapplyCount.set(0); + reapplyNs.set(0); + reskinCount.set(0); + reskinNs.set(0); + } + + public static String summary() { + StringBuilder sb = new StringBuilder(); + sb.append("[CSS-PERF-TRACE]\n"); //$NON-NLS-1$ + line(sb, "reapply (top) ", reapplyCount, reapplyNs); //$NON-NLS-1$ + line(sb, " reskin ", reskinCount, reskinNs); //$NON-NLS-1$ + line(sb, "parseStyleSheet ", parseStyleSheetCount, parseStyleSheetNs); //$NON-NLS-1$ + line(sb, "engine.reset ", resetCount, resetNs); //$NON-NLS-1$ + line(sb, "applyStyles ", applyStylesCount, applyStylesNs); //$NON-NLS-1$ + line(sb, "getComputedStyle ", getComputedStyleCount, getComputedStyleNs); //$NON-NLS-1$ + line(sb, "selectorMatch ", selectorMatchCount, selectorMatchNs); //$NON-NLS-1$ + line(sb, "applyStyleDeclar.", applyStyleDeclarationCount, applyStyleDeclarationNs); //$NON-NLS-1$ + line(sb, "applyCSSProperty ", applyCSSPropertyCount, applyCSSPropertyNs); //$NON-NLS-1$ + line(sb, " handlerLookup ", handlerLookupCount, handlerLookupNs); //$NON-NLS-1$ + line(sb, " handlerInvoke ", handlerInvokeCount, handlerInvokeNs); //$NON-NLS-1$ + line(sb, "applyInlineStyle ", applyInlineStyleCount, applyInlineStyleNs); //$NON-NLS-1$ + lineTotalOnly(sb, "applyStyles entry", applyStylesEntryNs); //$NON-NLS-1$ + lineTotalOnly(sb, "pseudoCascade ", pseudoCascadeNs); //$NON-NLS-1$ + lineTotalOnly(sb, "childRecursion ", childRecursionNs); //$NON-NLS-1$ + return sb.toString(); + } + + private static void lineTotalOnly(StringBuilder sb, String label, AtomicLong ns) { + double totalMs = ns.get() / 1_000_000.0; + sb.append(String.format(" %s total=%9.2fms%n", label, totalMs)); //$NON-NLS-1$ + } + + private static void line(StringBuilder sb, String label, AtomicLong count, AtomicLong ns) { + long c = count.get(); + long n = ns.get(); + double totalMs = n / 1_000_000.0; + double avgUs = c == 0 ? 0 : n / 1000.0 / c; + sb.append(String.format(" %s count=%9d total=%9.2fms avg=%9.2fus%n", label, c, totalMs, avgUs)); //$NON-NLS-1$ + } + + private CSSCorePolicy() { + // statics only + } +} diff --git a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/engine/CSSEngineImpl.java b/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/engine/CSSEngineImpl.java index a463e82452d..993d908b882 100644 --- a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/engine/CSSEngineImpl.java +++ b/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/engine/CSSEngineImpl.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2008, 2015 Angelo Zerr and others. + * Copyright (c) 2008, 2020 Angelo Zerr and others. * * This program and the accompanying materials * are made available under the terms of the Eclipse Public License 2.0 @@ -10,56 +10,1259 @@ * * Contributors: * Angelo Zerr - initial API and implementation - * Brian de Alwis (MTI) - move out registry-specific element provisioning + * IBM Corporation - ongoing development + * Red Hat Inc. (mistria) - Fixes suggested by FindBugs + * Red Hat Inc. (mistria) - Bug 413348: fix stream leak + * Lars Vogel - Bug 428715 + * Brian de Alwis (MTI) - Performance tweaks (Bug 430829) + * Dirk Fauth - Bug 479896 + * Patrik Suzzi - Bug 500402 + * Daniel Raap - Bug 511836 *******************************************************************************/ package org.eclipse.e4.ui.css.core.impl.engine; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.Reader; +import java.io.StringReader; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.BiConsumer; +import org.eclipse.core.runtime.FileLocator; +import org.eclipse.core.runtime.IPath; +import org.eclipse.e4.ui.css.core.dom.CSSStylableElement; +import org.eclipse.e4.ui.css.core.dom.ChildVisibilityAwareElement; +import org.eclipse.e4.ui.css.core.dom.ExtendedCSSRule; import org.eclipse.e4.ui.css.core.dom.ExtendedDocumentCSS; +import org.eclipse.e4.ui.css.core.dom.IElementProvider; +import org.eclipse.e4.ui.css.core.dom.IStreamingNodeList; import org.eclipse.e4.ui.css.core.dom.parsers.CSSParser; import org.eclipse.e4.ui.css.core.dom.parsers.CSSParserFactory; import org.eclipse.e4.ui.css.core.dom.parsers.ICSSParserFactory; +import org.eclipse.e4.ui.css.core.dom.properties.ICSSPropertyCompositeHandler; import org.eclipse.e4.ui.css.core.dom.properties.ICSSPropertyHandler; +import org.eclipse.e4.ui.css.core.dom.properties.ICSSPropertyHandlerProvider; import org.eclipse.e4.ui.css.core.dom.properties.converters.CSSValueBooleanConverterImpl; +import org.eclipse.e4.ui.css.core.dom.properties.converters.ICSSValueConverter; import org.eclipse.e4.ui.css.core.dom.properties.providers.CSSPropertyHandlerLazyProviderImpl; import org.eclipse.e4.ui.css.core.dom.properties.providers.CSSPropertyHandlerSimpleProviderImpl; -import org.eclipse.e4.ui.css.core.impl.sac.CSSConditionFactoryImpl; -import org.eclipse.e4.ui.css.core.impl.sac.CSSSelectorFactoryImpl; -import org.w3c.css.sac.ConditionFactory; +import org.eclipse.e4.ui.css.core.engine.CSSElementContext; +import org.eclipse.e4.ui.css.core.engine.CSSEngine; +import org.eclipse.e4.ui.css.core.engine.CSSErrorHandler; +import org.eclipse.e4.ui.css.core.exceptions.UnsupportedPropertyException; +import org.eclipse.e4.ui.css.core.impl.dom.CSSRuleListImpl; +import org.eclipse.e4.ui.css.core.impl.dom.CSSStyleSheetImpl; +import org.eclipse.e4.ui.css.core.impl.dom.DocumentCSSImpl; +import org.eclipse.e4.ui.css.core.impl.dom.ViewCSSImpl; +import org.eclipse.e4.ui.css.core.impl.engine.selector.SacTranslator; +import org.eclipse.e4.ui.css.core.impl.engine.selector.SelectorMatcher; +import org.eclipse.e4.ui.css.core.impl.engine.selector.Selectors; +import org.eclipse.e4.ui.css.core.resources.IResourcesRegistry; +import org.eclipse.e4.ui.css.core.resources.ResourceRegistryKeyFactory; +import org.eclipse.e4.ui.css.core.util.impl.resources.ResourcesLocatorManager; +import org.eclipse.e4.ui.css.core.util.resources.IResourcesLocatorManager; +import org.eclipse.e4.ui.css.core.utils.StringUtils; +import org.apache.batik.css.parser.DefaultConditionFactory; +import org.apache.batik.css.parser.DefaultSelectorFactory; +import org.w3c.css.sac.InputSource; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.w3c.dom.css.CSSImportRule; +import org.w3c.dom.css.CSSRule; +import org.w3c.dom.css.CSSRuleList; +import org.w3c.dom.css.CSSStyleDeclaration; +import org.w3c.dom.css.CSSStyleSheet; +import org.w3c.dom.css.CSSValue; +import org.w3c.dom.css.DocumentCSS; +import org.w3c.dom.css.ViewCSS; +import org.w3c.dom.stylesheets.StyleSheet; -public abstract class CSSEngineImpl extends AbstractCSSEngine { +/** + * Abstract CSS Engine manage style sheet parsing and store the + * {@link CSSStyleSheet} into {@link DocumentCSS}. + * + * To apply styles, call the {@link #applyStyles(Object, boolean, boolean)} + * method. This method check if {@link ICSSPropertyHandler} is registered for + * apply the CSS property. + * + * @version 1.0.0 + * @author Angelo ZERR + */ +public abstract class CSSEngineImpl implements CSSEngine { + + /** + * Archives are deliberately identified by exclamation mark in URLs + */ + private static final String ARCHIVE_IDENTIFIER = "!"; + + /** + * Default {@link IResourcesLocatorManager} used to get InputStream, Reader + * resource like Image. + */ + private static final IResourcesLocatorManager defaultResourcesLocatorManager = ResourcesLocatorManager.INSTANCE; + + /** + * w3c {@link DocumentCSS}. + */ + private final ExtendedDocumentCSS documentCSS; + + /** + * w3c {@link ViewCSS}. + */ + private final ViewCSS viewCSS; + + /** + * {@link IElementProvider} used to retrieve w3c Element linked to the + * widget. + */ + private IElementProvider elementProvider; + + protected boolean computeDefaultStyle = false; + + private Map elementsContext = null; + + /** + * CSS Error Handler to intercept error while parsing, applying styles. + */ + private CSSErrorHandler errorHandler; + + private IResourcesLocatorManager resourcesLocatorManager; - public static final ConditionFactory CONDITIONFACTORY_INSTANCE = new CSSConditionFactoryImpl( - null, "class", null, "id"); + private IResourcesRegistry resourcesRegistry; + + /** + * An ordered list of ICSSPropertyHandlerProvider + */ + protected List propertyHandlerProviders = new ArrayList<>(); + // for performance hold a map of handlers to singleton list + private final Map> propertyHandlerInstanceMap = new HashMap<>(); + + private Map currentCSSPropertiesApplied; + + private boolean throwError; + + private Map valueConverters = null; + + private int parseImport; + + private ResourceRegistryKeyFactory keyFactory; private CSSPropertyHandlerSimpleProviderImpl handlerProvider; private CSSPropertyHandlerLazyProviderImpl lazyHandlerProvider; public CSSEngineImpl() { - super(); - - // Register SWT Boolean CSSValue Converter - super.registerCSSValueConverter(CSSValueBooleanConverterImpl.INSTANCE); + this(new DocumentCSSImpl()); } public CSSEngineImpl(ExtendedDocumentCSS documentCSS) { - super(documentCSS); - // Register SWT Boolean CSSValue Converter - super.registerCSSValueConverter(CSSValueBooleanConverterImpl.INSTANCE); + this.documentCSS = documentCSS; + this.viewCSS = new ViewCSSImpl(documentCSS); + keyFactory = new ResourceRegistryKeyFactory(); + registerCSSValueConverter(CSSValueBooleanConverterImpl.INSTANCE); } + /*--------------- Parse style sheet -----------------*/ + @Override - public CSSParser makeCSSParser() { - // Create CSS Parser - ICSSParserFactory factory = CSSParserFactory.newInstance(); - CSSParser parser = factory.makeCSSParser(); + public StyleSheet parseStyleSheet(Reader reader) throws IOException { + InputSource source = new InputSource(); + source.setCharacterStream(reader); + return parseStyleSheet(source); + } + + @Override + public StyleSheet parseStyleSheet(InputStream stream) throws IOException { + InputSource source = new InputSource(); + source.setByteStream(stream); + return parseStyleSheet(source); + } + + @Override + public StyleSheet parseStyleSheet(InputSource source) throws IOException { + final boolean trace = CSSCorePolicy.DEBUG_PERF; + long t0 = trace ? System.nanoTime() : 0; + try { + return parseStyleSheetInternal(source); + } finally { + if (trace) { + CSSCorePolicy.parseStyleSheetNs.addAndGet(System.nanoTime() - t0); + CSSCorePolicy.parseStyleSheetCount.incrementAndGet(); + } + } + } + + private StyleSheet parseStyleSheetInternal(InputSource source) throws IOException { + // Check that CharacterStream or ByteStream is not null + checkInputSource(source); + CSSParser parser = makeCSSParser(); + CSSStyleSheet styleSheet = parser.parseStyleSheet(source); + + CSSRuleList rules = styleSheet.getCssRules(); + int length = rules.getLength(); + CSSRuleListImpl masterList = new CSSRuleListImpl(); + int counter; + for (counter = 0; counter < length; counter++) { + CSSRule rule = rules.item(counter); + if (rule.getType() != CSSRule.IMPORT_RULE) { + break; + } + // processing an import CSS + CSSImportRule importRule = (CSSImportRule) rule; + URL url = null; + if (importRule.getHref().startsWith("platform")) { + url = FileLocator.resolve(new URL(importRule.getHref())); + } else { + IPath p = IPath.fromOSString(source.getURI()); + IPath trim = p.removeLastSegments(1); + boolean isArchive = source.getURI().contains(ARCHIVE_IDENTIFIER); + url = FileLocator + .resolve(new URL(trim.addTrailingSeparator().toString() + ((CSSImportRule) rule).getHref())); + File testFile = new File(url.getFile()); + if (!isArchive&&!testFile.exists()) { + // look in platform default + String path = getResourcesLocatorManager().resolve((importRule).getHref()); + testFile = new File(new URL(path).getFile()); + if (testFile.exists()) { + url = new URL(path); + } + } + } + try (InputStream stream = url.openStream()) { + InputSource tempStream = new InputSource(); + tempStream.setURI(url.toString()); + tempStream.setByteStream(stream); + parseImport++; + try { + styleSheet = (CSSStyleSheet) this.parseStyleSheet(tempStream); + } finally { + parseImport--; + } + CSSRuleList tempRules = styleSheet.getCssRules(); + for (int j = 0; j < tempRules.getLength(); j++) { + masterList.add(tempRules.item(j)); + } + } + } + + // add remaining non import rules + for (int i = counter; i < length; i++) { + masterList.add(rules.item(i)); + } + + // final stylesheet + CSSStyleSheetImpl s = new CSSStyleSheetImpl(); + s.setRuleList(masterList); + if (parseImport == 0) { + documentCSS.addStyleSheet(s); + } + return s; + } + + private void processNodeList(NodeList nodes, BiConsumer consumer, boolean applyStylesToChildNodes) { + if (nodes instanceof IStreamingNodeList) { + ((IStreamingNodeList) nodes).stream().forEach(child -> { + consumer.accept(child, applyStylesToChildNodes); + }); + } else { + int length = nodes.getLength(); + for (int k = 0; k < length; k++) { + consumer.accept(nodes.item(k), applyStylesToChildNodes); + } + } + } + + /** + * Return true if source is valid and false otherwise. + */ + private void checkInputSource(InputSource source) throws IOException { + Reader reader = source.getCharacterStream(); + InputStream stream = source.getByteStream(); + if (reader == null && stream == null) { + throw new IOException( + "CharacterStream or ByteStream cannot be null for the InputSource."); + } + } + + /*--------------- Parse style declaration -----------------*/ + + @Override + public CSSStyleDeclaration parseStyleDeclaration(String style) { + try { + return parseStyleDeclaration(new StringReader(style)); + } catch (IOException e) { + throw new IllegalStateException("StringReader cannot throw IOException", e); //$NON-NLS-1$ + } + } + + @Override + public CSSStyleDeclaration parseStyleDeclaration(Reader reader) throws IOException { + InputSource source = new InputSource(); + source.setCharacterStream(reader); + return parseStyleDeclaration(source); + } + + @Override + public CSSStyleDeclaration parseStyleDeclaration(InputStream stream) throws IOException { + InputSource source = new InputSource(); + source.setByteStream(stream); + return parseStyleDeclaration(source); + } + + @Override + public CSSStyleDeclaration parseStyleDeclaration(InputSource source) throws IOException { + checkInputSource(source); + CSSParser parser = makeCSSParser(); + return parser.parseStyleDeclaration(source); + } + + /*--------------- Parse CSS Selector -----------------*/ + + @Override + public Selectors.SelectorList parseSelectors(String selector) { + try { + return parseSelectors(new StringReader(selector)); + } catch (IOException e) { + throw new IllegalStateException("StringReader cannot throw IOException", e); //$NON-NLS-1$ + } + } + + @Override + public Selectors.SelectorList parseSelectors(Reader reader) throws IOException { + InputSource source = new InputSource(); + source.setCharacterStream(reader); + return parseSelectors(source); + } + + @Override + public Selectors.SelectorList parseSelectors(InputStream stream) throws IOException { + InputSource source = new InputSource(); + source.setByteStream(stream); + return parseSelectors(source); + } + + @Override + public Selectors.SelectorList parseSelectors(InputSource source) throws IOException { + checkInputSource(source); + CSSParser parser = makeCSSParser(); + return SacTranslator.translate(parser.parseSelectors(source)); + } + + /*--------------- Parse CSS Property Value-----------------*/ + + @Override + public CSSValue parsePropertyValue(Reader reader) throws IOException { + InputSource source = new InputSource(); + source.setCharacterStream(reader); + return parsePropertyValue(source); + } + + @Override + public CSSValue parsePropertyValue(InputStream stream) throws IOException { + InputSource source = new InputSource(); + source.setByteStream(stream); + return parsePropertyValue(source); + } + + @Override + public CSSValue parsePropertyValue(String value) { + try { + return parsePropertyValue(new StringReader(value)); + } catch (IOException e) { + throw new IllegalStateException("StringReader cannot throw IOException", e); //$NON-NLS-1$ + } + } + + @Override + public CSSValue parsePropertyValue(InputSource source) throws IOException { + checkInputSource(source); + CSSParser parser = makeCSSParser(); + return parser.parsePropertyValue(source); + } + + /*--------------- Apply styles -----------------*/ + + private final ThreadLocal> styledElements = new ThreadLocal<>(); + + public void startStylingSession() { + styledElements.set(new HashSet<>()); + } + + public void stopStylingSession() { + styledElements.remove(); + } + + public boolean isElementStyled(Object element) { + Set set = styledElements.get(); + return set != null && set.contains(element); + } + + public void markElementStyled(Object element) { + Set set = styledElements.get(); + if (set != null) { + set.add(element); + } + } + + @Override + public void applyStyles(Object element, boolean applyStylesToChildNodes) { + applyStyles(element, applyStylesToChildNodes, computeDefaultStyle); + } + + @Override + public void applyStyles(Object element, boolean applyStylesToChildNodes, boolean computeDefaultStyle) { + final boolean trace = CSSCorePolicy.DEBUG_PERF; + long t0 = trace ? System.nanoTime() : 0; + try { + applyStylesInternal(element, applyStylesToChildNodes, computeDefaultStyle); + } finally { + if (trace) { + CSSCorePolicy.applyStylesNs.addAndGet(System.nanoTime() - t0); + CSSCorePolicy.applyStylesCount.incrementAndGet(); + } + } + } + + private void applyStylesInternal(Object element, boolean applyStylesToChildNodes, boolean computeDefaultStyle) { + final boolean trace = CSSCorePolicy.DEBUG_PERF; + long entryT0 = trace ? System.nanoTime() : 0; + Element elt = getElement(element); + if (elt == null || !isVisible(elt)) { + if (trace) { + CSSCorePolicy.applyStylesEntryNs.addAndGet(System.nanoTime() - entryT0); + } + return; + } + if (trace) { + CSSCorePolicy.applyStylesEntryNs.addAndGet(System.nanoTime() - entryT0); + } + + if (isElementStyled(element)) { + if (applyStylesToChildNodes) { + NodeList nodes = elt instanceof ChildVisibilityAwareElement c + ? c.getVisibleChildNodes() + : elt.getChildNodes(); + if (nodes != null) { + processNodeList(nodes, this::applyStyles, applyStylesToChildNodes); + onStylesAppliedToChildNodes(elt, nodes); + } + } + return; + } + + markElementStyled(element); + + /* + * Compute new Style to apply. + */ + CSSStyleDeclaration style = viewCSS.getComputedStyle(elt, null); + if (computeDefaultStyle) { + if (applyStylesToChildNodes) { + this.computeDefaultStyle = computeDefaultStyle; + } + /* + * Apply default style. + */ + applyDefaultStyleDeclaration(element, false, style, null); + } + + /* + * Manage static pseudo instances + */ + long pseudoT0 = trace ? System.nanoTime() : 0; + String[] pseudoInstances = getStaticPseudoInstances(elt); + if (pseudoInstances != null && pseudoInstances.length > 0) { + // there are static pseudo instances defined, loop for it and + // apply styles for each pseudo instance. + for (String pseudoInstance : pseudoInstances) { + CSSStyleDeclaration styleWithPseudoInstance = viewCSS.getComputedStyle(elt, pseudoInstance); + if (computeDefaultStyle) { + /* + * Apply default style for the current pseudo instance. + */ + applyDefaultStyleDeclaration(element, false, styleWithPseudoInstance, pseudoInstance); + } + + if (styleWithPseudoInstance != null) { + CSSRule parentRule = styleWithPseudoInstance.getParentRule(); + if (parentRule instanceof ExtendedCSSRule) { + applyConditionalPseudoStyle((ExtendedCSSRule) parentRule, pseudoInstance, element, styleWithPseudoInstance); + } else { + applyStyleDeclaration(elt, styleWithPseudoInstance, pseudoInstance); + } + } + } + } + if (trace) { + CSSCorePolicy.pseudoCascadeNs.addAndGet(System.nanoTime() - pseudoT0); + } + + if (style != null) { + applyStyleDeclaration(elt, style, null); + } + long inlineT0 = trace ? System.nanoTime() : 0; + try { + // Apply inline style + applyInlineStyle(elt, false); + } catch (Exception e) { + handleExceptions(e); + } + if (trace) { + CSSCorePolicy.applyInlineStyleNs.addAndGet(System.nanoTime() - inlineT0); + CSSCorePolicy.applyInlineStyleCount.incrementAndGet(); + } + + if (applyStylesToChildNodes) { + long childT0 = trace ? System.nanoTime() : 0; + /* + * Style all children recursive. + */ + NodeList nodes = elt instanceof ChildVisibilityAwareElement c + ? c.getVisibleChildNodes() + : elt.getChildNodes(); + if (nodes != null) { + processNodeList(nodes, this::applyStyles, applyStylesToChildNodes); + onStylesAppliedToChildNodes(elt, nodes); + } + if (trace) { + CSSCorePolicy.childRecursionNs.addAndGet(System.nanoTime() - childT0); + } + } + } + + /** + * Allow the CSS engine to skip particular elements if they are not visible. + * Elements need to be restyled when they become visible. + * + * @return true if the element is visible, false if not visible. + */ + protected boolean isVisible(Element elt) { + Node parentNode = elt.getParentNode(); + if (parentNode instanceof ChildVisibilityAwareElement) { + NodeList l = ((ChildVisibilityAwareElement) parentNode).getVisibleChildNodes(); + if (l != null) { + if (l instanceof IStreamingNodeList) { + return ((IStreamingNodeList) l).stream().anyMatch(node -> node == elt); + } else { + int length = l.getLength(); + for (int i = 0; i < length; i++) { + if (l.item(i) == elt) { + return true; + } + } + } + } + return false; + } + return true; + } + + private void applyConditionalPseudoStyle(ExtendedCSSRule parentRule, String pseudoInstance, Object element, + CSSStyleDeclaration styleWithPseudoInstance) { + Selectors.SelectorList selectorList = parentRule.getSelectorList(); + for (Selectors.Selector alternative : selectorList.alternatives()) { + if (matchesPseudoInstanceAttribute(alternative, pseudoInstance)) { + applyStyleDeclaration(element, styleWithPseudoInstance, pseudoInstance); + return; + } + } + } + + /** + * Returns {@code true} if {@code selector} carries a pseudo-class or + * attribute selector (anywhere in a compound or descendant combinator) + * whose target value equals {@code pseudoInstance}. Mirrors the legacy + * SAC walker, which handled both {@code :selected} (a pseudo-class) and + * {@code Shell[active='true']} (an attribute) through SAC's shared + * {@code AttributeCondition} interface. + */ + private static boolean matchesPseudoInstanceAttribute(Selectors.Selector selector, String pseudoInstance) { + if (selector instanceof Selectors.PseudoClass pc) { + return pseudoInstance.equals(pc.name()); + } + if (selector instanceof Selectors.AttributeSelector attr) { + return pseudoInstance.equals(attr.value()); + } + if (selector instanceof Selectors.AttributeIncludes attr) { + return pseudoInstance.equals(attr.value()); + } + if (selector instanceof Selectors.AttributeBeginHyphen attr) { + return pseudoInstance.equals(attr.value()); + } + if (selector instanceof Selectors.And and) { + return matchesPseudoInstanceAttribute(and.left(), pseudoInstance) + || matchesPseudoInstanceAttribute(and.right(), pseudoInstance); + } + if (selector instanceof Selectors.Descendant d) { + return matchesPseudoInstanceAttribute(d.descendant(), pseudoInstance) + || matchesPseudoInstanceAttribute(d.ancestor(), pseudoInstance); + } + if (selector instanceof Selectors.Child c) { + return matchesPseudoInstanceAttribute(c.child(), pseudoInstance) + || matchesPseudoInstanceAttribute(c.parent(), pseudoInstance); + } + if (selector instanceof Selectors.Adjacent a) { + return matchesPseudoInstanceAttribute(a.second(), pseudoInstance) + || matchesPseudoInstanceAttribute(a.first(), pseudoInstance); + } + return false; + } + + protected String[] getStaticPseudoInstances(Element element) { + if (element instanceof CSSStylableElement stylableElement) { + return stylableElement.getStaticPseudoInstances(); + } + return null; + } + + /** + * Callback method called when styles applied of nodes + * children of the element. + */ + protected void onStylesAppliedToChildNodes(Element element, NodeList nodes) { + if (element instanceof CSSStylableElement) { + ((CSSStylableElement) element).onStylesApplied(nodes); + } + } + + /*--------------- Apply style declaration -----------------*/ + + @Override + public void applyStyleDeclaration(Object element, CSSStyleDeclaration style, String pseudo) { + final boolean trace = CSSCorePolicy.DEBUG_PERF; + long t0 = trace ? System.nanoTime() : 0; + try { + applyStyleDeclarationInternal(element, style, pseudo); + } finally { + if (trace) { + CSSCorePolicy.applyStyleDeclarationNs.addAndGet(System.nanoTime() - t0); + CSSCorePolicy.applyStyleDeclarationCount.incrementAndGet(); + } + } + } + + private void applyStyleDeclarationInternal(Object element, CSSStyleDeclaration style, String pseudo) { + // Apply style + boolean avoidanceCacheInstalled = currentCSSPropertiesApplied == null; + if (avoidanceCacheInstalled) { + currentCSSPropertiesApplied = new HashMap<>(); + } + List appliedHandlers = Collections.emptyList(); + for (int i = 0; i < style.getLength(); i++) { + String property = style.item(i); + CSSValue value = style.getPropertyCSSValue(property); + try { + ICSSPropertyHandler handler = this.applyCSSProperty(element, property, value, pseudo); + if (handler != null) { + switch (appliedHandlers.size()) { + case 0: + appliedHandlers = propertyHandlerInstanceMap.computeIfAbsent(handler, + Collections::singletonList); + break; + case 1: + appliedHandlers = new ArrayList<>(appliedHandlers); + appliedHandlers.add(handler); + break; + default: + if (!appliedHandlers.contains(handler)) { + appliedHandlers.add(handler); + } + } + } + } catch (Exception e) { + if (throwError || (!throwError && !(e instanceof UnsupportedPropertyException))) { + handleExceptions(e); + } + } + } + for (ICSSPropertyHandler handler : appliedHandlers) { + try { + handler.onAllCSSPropertiesApplied(element, this, pseudo); + } catch (Exception e) { + handleExceptions(e); + } + } + if (avoidanceCacheInstalled) { + currentCSSPropertiesApplied = null; + } + + } + + @Override + public CSSStyleDeclaration parseAndApplyStyleDeclaration(Object node, + Reader reader) throws IOException { + CSSStyleDeclaration style = parseStyleDeclaration(reader); + this.applyStyleDeclaration(node, style, null); + return style; + } + + @Override + public CSSStyleDeclaration parseAndApplyStyleDeclaration(Object node, InputStream stream) throws IOException { + CSSStyleDeclaration style = parseStyleDeclaration(stream); + this.applyStyleDeclaration(node, style, null); + return style; + } + + @Override + public CSSStyleDeclaration parseAndApplyStyleDeclaration(Object node, InputSource source) throws IOException { + CSSStyleDeclaration style = parseStyleDeclaration(source); + this.applyStyleDeclaration(node, style, null); + return style; + } + + @Override + public CSSStyleDeclaration parseAndApplyStyleDeclaration(Object node, String style) throws IOException { + CSSStyleDeclaration styleDeclaration = parseStyleDeclaration(style); + this.applyStyleDeclaration(node, styleDeclaration, null); + return styleDeclaration; + } + + /*--------------- Apply inline style -----------------*/ + + @Override + public void applyInlineStyle(Object node, boolean applyStylesToChildNodes) { + Element elt = getElement(node); + if (elt != null) { + if (elt instanceof CSSStylableElement stylableElement) { + String style = stylableElement.getCSSStyle(); + if (style != null && style.length() > 0) { + try { + parseAndApplyStyleDeclaration(stylableElement.getNativeWidget(), style); + } catch (IOException e) { + handleExceptions(e); + } + } + } + if (applyStylesToChildNodes) { + /* + * Style all children recursive. + */ + NodeList nodes = elt.getChildNodes(); + if (nodes != null) { + processNodeList(nodes, this::applyInlineStyle, applyStylesToChildNodes); + } + } + } + } + + /*--------------- Initial Style -----------------*/ + + @Override + public CSSStyleDeclaration getDefaultStyleDeclaration(Object element, String pseudoE) { + return getDefaultStyleDeclaration(element, null, pseudoE); + } + + public CSSStyleDeclaration getDefaultStyleDeclaration(Object widget, CSSStyleDeclaration newStyle, String pseudoE) { + CSSStyleDeclaration style = null; + for (ICSSPropertyHandlerProvider provider : propertyHandlerProviders) { + try { + style = provider.getDefaultCSSStyleDeclaration(this, widget, newStyle, pseudoE); + } catch (Exception e) { + handleExceptions(e); + } + } + return style; + } + + @Override + public void applyDefaultStyleDeclaration(Object element, boolean applyStylesToChildNodes) { + applyDefaultStyleDeclaration(element, applyStylesToChildNodes, null, null); + } + + public void applyDefaultStyleDeclaration(Object element, boolean applyStylesToChildNodes, + CSSStyleDeclaration newStyle, String pseudoE) { + // Initial styles must be computed or applied + Element elt = getElement(element); + if (elt != null) { + if (elt instanceof CSSStylableElement stylableElement) { + CSSStyleDeclaration oldDefaultStyleDeclaration = stylableElement.getDefaultStyleDeclaration(pseudoE); + CSSStyleDeclaration defaultStyleDeclaration = getDefaultStyleDeclaration( + element, newStyle, pseudoE); + if (oldDefaultStyleDeclaration != null) { + // Second apply styles, apply the initial style + // before apply the new style + try { + throwError = false; + applyStyleDeclaration(element, defaultStyleDeclaration, pseudoE); + } finally { + throwError = true; + } + } + } + if (applyStylesToChildNodes) { + /* + * Style all children recursive. + */ + NodeList nodes = elt.getChildNodes(); + if (nodes != null) { + processNodeList(nodes, this::applyDefaultStyleDeclaration, applyStylesToChildNodes); + onStylesAppliedToChildNodes(elt, nodes); + } + } + } + } + + /** + * Delegates the handle method. + * + * @param element + * may be a widget or a node or some object + */ + @Override + public ICSSPropertyHandler applyCSSProperty(Object element, String property, CSSValue value, String pseudo) + throws Exception { + final boolean trace = CSSCorePolicy.DEBUG_PERF; + long t0 = trace ? System.nanoTime() : 0; + try { + return applyCSSPropertyInternal(element, property, value, pseudo); + } finally { + if (trace) { + CSSCorePolicy.applyCSSPropertyNs.addAndGet(System.nanoTime() - t0); + CSSCorePolicy.applyCSSPropertyCount.incrementAndGet(); + } + } + } + + private ICSSPropertyHandler applyCSSPropertyInternal(Object element, String property, CSSValue value, String pseudo) + throws Exception { + if (currentCSSPropertiesApplied != null && currentCSSPropertiesApplied.containsKey(property)) { + // CSS Property was already applied, ignore it. + return null; + } + + element = getElement(element); // in case we're passed a node + if ("inherit".equals(value.getCssText())) { + // go to parent node + Element actualElement = (Element) element; + Node parentNode = actualElement.getParentNode(); + // get CSS property value + String parentValueString = retrieveCSSProperty(parentNode, property, pseudo); + // and convert it to a CSS value, overriding the "inherit" setting + // with the parent value + value = parsePropertyValue(parentValueString); + } + + final boolean trace = CSSCorePolicy.DEBUG_PERF; + for (ICSSPropertyHandlerProvider provider : propertyHandlerProviders) { + long lookupT0 = trace ? System.nanoTime() : 0; + Collection handlers = provider.getCSSPropertyHandlers(element, property); + if (trace) { + CSSCorePolicy.handlerLookupNs.addAndGet(System.nanoTime() - lookupT0); + CSSCorePolicy.handlerLookupCount.incrementAndGet(); + } + if (handlers == null) { + continue; + } + for (ICSSPropertyHandler handler : handlers) { + try { + long invokeT0 = trace ? System.nanoTime() : 0; + boolean result = handler.applyCSSProperty(element, property, value, pseudo, this); + if (trace) { + CSSCorePolicy.handlerInvokeNs.addAndGet(System.nanoTime() - invokeT0); + CSSCorePolicy.handlerInvokeCount.incrementAndGet(); + } + if (result) { + // Add CSS Property to flag that this CSS Property was + // applied. + if (currentCSSPropertiesApplied != null) { + currentCSSPropertiesApplied.put(property, property); + } + return handler; + } + } catch (Exception e) { + if (throwError || (!throwError && !(e instanceof UnsupportedPropertyException))) { + handleExceptions(e); + } + } + } + } + + return null; + } + + @Override + public String retrieveCSSProperty(Object element, String property, String pseudo) { + try { + element = getElement(element); // in case we're passed a node + for (ICSSPropertyHandlerProvider provider : propertyHandlerProviders) { + Collection handlers = provider.getCSSPropertyHandlers(element, property); + if (handlers == null) { + continue; + } + for (ICSSPropertyHandler handler : handlers) { + String value = handler.retrieveCSSProperty(element, property, pseudo, this); + if (!StringUtils.isEmpty(value)) { + return value; + } + } + } + } catch (Exception e) { + handleExceptions(e); + } + return null; + } + + @Override + public String[] getCSSCompositePropertiesNames(String property) { + try { + Collection handlers = getCSSPropertyHandlers(property); + if (handlers == null) { + return null; + } + for (ICSSPropertyHandler handler : handlers) { + if (handler instanceof ICSSPropertyCompositeHandler compositeHandler) { + if (compositeHandler.isCSSPropertyComposite(property)) { + return compositeHandler.getCSSPropertiesNames(property); + } + } + } + } catch (Exception e) { + handleExceptions(e); + } + return null; + } + + protected Collection getCSSPropertyHandlers(String property) throws Exception { + Collection handlers = new ArrayList<>(); + for (ICSSPropertyHandlerProvider provider : propertyHandlerProviders) { + Collection h = provider.getCSSPropertyHandlers(property); + if (handlers == null) { + handlers = h; + } else { + handlers = new ArrayList<>(handlers); + handlers.addAll(h); + } + } + return handlers; + } + + /** + * Return the set of property names and handlers for the provided node. + * + * @return the property names and handlers + */ + @Override + public Collection getCSSProperties(Object element) { + Set properties = new HashSet<>(); + for (ICSSPropertyHandlerProvider provider : propertyHandlerProviders) { + properties.addAll(provider.getCSSProperties(element)); + } + return properties; + } + + /*--------------- Dynamic pseudo classes -----------------*/ + + @Override + public IElementProvider getElementProvider() { + return elementProvider; + } + + @Override + public void setElementProvider(IElementProvider elementProvider) { + this.elementProvider = elementProvider; + } + + /** + * Return the w3c Element linked to the Object element. + */ + @Override + public Element getElement(Object element) { + Element elt = null; + if (element == null) { + return elt; + } + CSSElementContext elementContext = getCSSElementContext(element); + if (elementContext != null) { + if (!elementContext.elementMustBeRefreshed(elementProvider)) { + return elementContext.getElement(); + } + } + if (element instanceof Element) { + elt = (Element) element; + } else if (elementProvider != null) { + elt = elementProvider.getElement(element, this); + } + if (elt != null) { + if (elementContext == null) { + elementContext = new CSSElementContextImpl(); + Object nativeWidget = getNativeWidget(element); + hookNativeWidget(nativeWidget); + getElementsContext().put(nativeWidget, elementContext); + } + elementContext.setElementProvider(elementProvider); + elementContext.setElement(elt); + if (elt instanceof CSSStylableElement) { + // Initialize CSS stylable element + ((CSSStylableElement)elt).initialize(); + } + + } + return elt; + } + + /** + * Called when an element context is created for a native widget and + * registered with this engine. Subclasses should override and install + * a listener on the widget that will call {@link #handleWidgetDisposed(Object)} + * when the widget is disposed. + *

+ * The default implementation of this method does nothing. + *

+ * + * @param widget the native widget to hook + */ + protected void hookNativeWidget(Object widget) { + } + + /** + * Called when a widget is disposed. Removes the element context + * from the element contexts map and the widgets map. Overriding + * classes must call the super implementation. + */ + @Override + public void handleWidgetDisposed(Object widget) { + if (elementsContext != null) { + elementsContext.remove(widget); + } + } + + @Override + public CSSElementContext getCSSElementContext(Object element) { + Object o = getNativeWidget(element); + return getElementsContext().get(o); + } + + public Object getNativeWidget(Object element) { + Object o = element; + if (element instanceof CSSStylableElement) { + o = ((CSSStylableElement) o).getNativeWidget(); + } + return o; + } + + protected Map getElementsContext() { + if (elementsContext == null) { + elementsContext = new HashMap<>(); + } + return elementsContext; + } + + @Override + public boolean matches(Selectors.Selector selector, Object element, String pseudoElt) { + Element elt = getElement(element); + if (elt == null) { + return false; + } + int depth = 0; + for (Node n = elt; n instanceof Element; n = n.getParentNode()) { + depth++; + } + Element[] hierarchy = new Element[depth]; + int idx = 0; + for (Node n = elt; n instanceof Element; n = n.getParentNode()) { + hierarchy[idx++] = (Element) n; + } + return SelectorMatcher.matches(selector, elt, pseudoElt, hierarchy, 0); + } + + /*--------------- Error Handler -----------------*/ + + /** + * Handle exceptions thrown while parsing, applying styles. By default this + * method call CSS Error Handler if it is initialized. + */ + @Override + public void handleExceptions(Exception e) { + if (errorHandler != null) { + errorHandler.error(e); + } + } + + @Override + public CSSErrorHandler getErrorHandler() { + return errorHandler; + } + + /** + * Set the CSS Error Handler to manage exception. + */ + @Override + public void setErrorHandler(CSSErrorHandler errorHandler) { + this.errorHandler = errorHandler; + } + + /*--------------- Resources Locator Manager -----------------*/ + + @Override + public IResourcesLocatorManager getResourcesLocatorManager() { + if (resourcesLocatorManager == null) { + return defaultResourcesLocatorManager; + } + return resourcesLocatorManager; + } + + @Override + public void setResourcesLocatorManager( + IResourcesLocatorManager resourcesLocatorManager) { + this.resourcesLocatorManager = resourcesLocatorManager; + } + + /*--------------- Document/View CSS -----------------*/ + + @Override + public DocumentCSS getDocumentCSS() { + return documentCSS; + } + + @Override + public ViewCSS getViewCSS() { + return viewCSS; + } + + @Override + public void dispose() { + reset(); + // Call dispose for each CSSStylableElement which was registered + Collection contexts = elementsContext.values(); + for (CSSElementContext context : contexts) { + Element element = context.getElement(); + if (element instanceof CSSStylableElement) { + ((CSSStylableElement) element).dispose(); + } + } + // FIXME: should dispose element provider and the property handler + // providers + elementsContext = null; + if (resourcesRegistry != null) { + resourcesRegistry.dispose(); + } + } + + @Override + public void reset() { + final boolean trace = CSSCorePolicy.DEBUG_PERF; + long t0 = trace ? System.nanoTime() : 0; + try { + documentCSS.removeAllStyleSheets(); + } finally { + if (trace) { + CSSCorePolicy.resetNs.addAndGet(System.nanoTime() - t0); + CSSCorePolicy.resetCount.incrementAndGet(); + } + } + } + + /*--------------- Resources Registry -----------------*/ + + @Override + public IResourcesRegistry getResourcesRegistry() { + return resourcesRegistry; + } - // Register Batik CSS Selector factory. - parser.setSelectorFactory(CSSSelectorFactoryImpl.INSTANCE); + @Override + public void setResourcesRegistry(IResourcesRegistry resourcesRegistry) { + this.resourcesRegistry = resourcesRegistry; + } - // Register Custom CSS Condition factory. - parser.setConditionFactory(CONDITIONFACTORY_INSTANCE); + public void registerCSSPropertyHandlerProvider(ICSSPropertyHandlerProvider handlerProvider) { + propertyHandlerProviders.add(handlerProvider); + } + public void unregisterCSSPropertyHandlerProvider(ICSSPropertyHandlerProvider handlerProvider) { + propertyHandlerProviders.remove(handlerProvider); + } + + /*--------------- CSS Value Converter -----------------*/ + + @Override + public void registerCSSValueConverter(ICSSValueConverter converter) { + if (valueConverters == null) { + valueConverters = new HashMap<>(); + } + valueConverters.put(converter.getToType(), converter); + } + + @Override + public void unregisterCSSValueConverter(ICSSValueConverter converter) { + if (valueConverters == null) { + return; + } + valueConverters.remove(converter); + } + + @Override + public ICSSValueConverter getCSSValueConverter(Object toType) { + if (valueConverters != null) { + return valueConverters.get(toType); + } + return null; + } + + @Override + public Object convert(CSSValue value, Object toType, Object context) throws Exception { + if ("unset".equals(value.getCssText())) { + return null; + } + Object key = keyFactory.createKey(value); + Object newValue = getResource(toType, key); + + if (newValue == null) { + ICSSValueConverter converter = getCSSValueConverter(toType); + if (converter != null) { + newValue = converter.convert(value, this, context); + // cache it + registerResource(toType, key, newValue); + } + } + return newValue; + } + + private Object getResource(Object toType, Object key) { + if (key != null && getResourcesRegistry() != null) { + return getResourcesRegistry().getResource(toType, key); + } + return null; + } + + private void registerResource(Object toType, Object key, Object resource) { + if (key != null && resource != null && getResourcesRegistry() != null) { + getResourcesRegistry().registerResource(toType, key, resource); + } + } + + @Override + public String convert(Object value, Object toType, Object context) + throws Exception { + if (value == null) { + return null; + } + ICSSValueConverter converter = getCSSValueConverter(toType); + if (converter != null) { + return converter.convert(value, this, context); + } + return null; + } + + /** + * Return instance of CSS Parser, configured with Batik's stock selector + * and condition factories. Selectors flow through {@link SacTranslator} + * before they reach engine code, so we no longer need our vendored copies + * of the SAC factory classes. + */ + public CSSParser makeCSSParser() { + ICSSParserFactory factory = CSSParserFactory.newInstance(); + CSSParser parser = factory.makeCSSParser(); + parser.setSelectorFactory(DefaultSelectorFactory.INSTANCE); + parser.setConditionFactory(DefaultConditionFactory.INSTANCE); return parser; } @@ -71,7 +1274,7 @@ public void registerCSSPropertyHandler(Class cl, ICSSPropertyHandler handler) private void initHandlerProviderIfNeed() { if (handlerProvider == null) { handlerProvider = new CSSPropertyHandlerSimpleProviderImpl(); - super.registerCSSPropertyHandlerProvider(handlerProvider); + registerCSSPropertyHandlerProvider(handlerProvider); } } @@ -83,7 +1286,7 @@ public void registerCSSProperty(String propertyName, Class - initial API and implementation + *******************************************************************************/ +package org.eclipse.e4.ui.css.core.impl.engine.selector; + +import java.util.ArrayList; +import java.util.List; +import org.eclipse.e4.ui.css.core.impl.engine.selector.Selectors.Adjacent; +import org.eclipse.e4.ui.css.core.impl.engine.selector.Selectors.And; +import org.eclipse.e4.ui.css.core.impl.engine.selector.Selectors.AttributeBeginHyphen; +import org.eclipse.e4.ui.css.core.impl.engine.selector.Selectors.AttributeIncludes; +import org.eclipse.e4.ui.css.core.impl.engine.selector.Selectors.AttributeSelector; +import org.eclipse.e4.ui.css.core.impl.engine.selector.Selectors.Child; +import org.eclipse.e4.ui.css.core.impl.engine.selector.Selectors.ClassSelector; +import org.eclipse.e4.ui.css.core.impl.engine.selector.Selectors.Descendant; +import org.eclipse.e4.ui.css.core.impl.engine.selector.Selectors.ElementType; +import org.eclipse.e4.ui.css.core.impl.engine.selector.Selectors.IdSelector; +import org.eclipse.e4.ui.css.core.impl.engine.selector.Selectors.PseudoClass; +import org.eclipse.e4.ui.css.core.impl.engine.selector.Selectors.Selector; +import org.eclipse.e4.ui.css.core.impl.engine.selector.Selectors.Universal; +import org.w3c.css.sac.AttributeCondition; +import org.w3c.css.sac.CombinatorCondition; +import org.w3c.css.sac.Condition; +import org.w3c.css.sac.ConditionalSelector; +import org.w3c.css.sac.DescendantSelector; +import org.w3c.css.sac.ElementSelector; +import org.w3c.css.sac.SelectorList; +import org.w3c.css.sac.SiblingSelector; +import org.w3c.css.sac.SimpleSelector; + +/** + * Converts a SAC selector tree (as produced by the Batik parser) into the + * engine's internal {@link Selectors} AST. + * + *

+ * The translator is the single boundary between the SAC parser output and the + * rest of the engine. Once a stylesheet has been parsed, only the internal + * AST flows through {@code CSSEngine.matches}, the rule list, and + * {@link SelectorMatcher}. SAC types do not cross this boundary. + *

+ * + *

+ * Specificity is preserved exactly: the internal records compute it the same + * way the legacy SAC wrappers did (100 per id, 10 per class / attribute / + * pseudo-class, 1 per element, 0 for {@code *}). Combinators sum operands. + *

+ */ +public final class SacTranslator { + + private SacTranslator() { + // statics only + } + + /** Translate an entire {@link SelectorList} into the internal form. */ + public static Selectors.SelectorList translate(SelectorList sacList) { + List alternatives = new ArrayList<>(sacList.getLength()); + for (int i = 0; i < sacList.getLength(); i++) { + alternatives.add(translate(sacList.item(i))); + } + return new Selectors.SelectorList(alternatives); + } + + /** Translate a single SAC {@link org.w3c.css.sac.Selector}. */ + public static Selector translate(org.w3c.css.sac.Selector sac) { + return switch (sac.getSelectorType()) { + case org.w3c.css.sac.Selector.SAC_ELEMENT_NODE_SELECTOR -> translateElement((ElementSelector) sac); + case org.w3c.css.sac.Selector.SAC_PSEUDO_ELEMENT_SELECTOR -> translatePseudoElement((ElementSelector) sac); + case org.w3c.css.sac.Selector.SAC_CONDITIONAL_SELECTOR -> translateConditional((ConditionalSelector) sac); + case org.w3c.css.sac.Selector.SAC_DESCENDANT_SELECTOR -> { + DescendantSelector d = (DescendantSelector) sac; + yield new Descendant(translate(d.getAncestorSelector()), translateSimple(d.getSimpleSelector())); + } + case org.w3c.css.sac.Selector.SAC_CHILD_SELECTOR -> { + DescendantSelector c = (DescendantSelector) sac; + yield new Child(translate(c.getAncestorSelector()), translateSimple(c.getSimpleSelector())); + } + case org.w3c.css.sac.Selector.SAC_DIRECT_ADJACENT_SELECTOR -> { + SiblingSelector s = (SiblingSelector) sac; + yield new Adjacent(translate(s.getSelector()), translateSimple(s.getSiblingSelector())); + } + default -> throw new IllegalArgumentException( + "Unsupported SAC selector type: " + sac.getSelectorType() + " (" + sac + ")"); //$NON-NLS-1$ //$NON-NLS-2$ + }; + } + + private static Selector translateSimple(SimpleSelector simple) { + return translate(simple); + } + + private static Selector translateElement(ElementSelector sel) { + String name = sel.getLocalName(); + return name == null ? new Universal() : new ElementType(name); + } + + private static Selector translatePseudoElement(ElementSelector sel) { + // Pseudo-element form (::first-line). The engine has never matched + // these; treat as the element it appears on (if any) or universal. + // In practice the parser only emits this when a stylesheet uses :: , + // which the supported subset does not. + String name = sel.getLocalName(); + return name == null ? new Universal() : new PseudoClass(name); + } + + private static Selector translateConditional(ConditionalSelector sel) { + Selector left = translate(sel.getSimpleSelector()); + Selector right = translateCondition(sel.getCondition()); + if (left instanceof Universal) { + return right; + } + return new And(left, right); + } + + private static Selector translateCondition(Condition condition) { + return switch (condition.getConditionType()) { + case Condition.SAC_CLASS_CONDITION -> new ClassSelector(((AttributeCondition) condition).getValue()); + case Condition.SAC_ID_CONDITION -> new IdSelector(((AttributeCondition) condition).getValue()); + case Condition.SAC_PSEUDO_CLASS_CONDITION -> new PseudoClass(((AttributeCondition) condition).getValue()); + case Condition.SAC_LANG_CONDITION -> { + // Modeled as a presence-form attribute selector keyed on lang; + // nothing in the supported subset uses :lang(), but the parser + // can still emit it. + AttributeCondition lang = (AttributeCondition) condition; + yield new AttributeSelector("lang", lang.getValue()); //$NON-NLS-1$ + } + case Condition.SAC_ATTRIBUTE_CONDITION -> { + AttributeCondition attr = (AttributeCondition) condition; + // Batik's stock Parser always calls createAttributeCondition with + // specified=false, regardless of whether the source was [attr] or + // [attr='value']. Distinguish the two by whether a value was + // supplied: null means the presence form [attr]. + yield new AttributeSelector(attr.getLocalName(), attr.getValue()); + } + case Condition.SAC_ONE_OF_ATTRIBUTE_CONDITION -> { + AttributeCondition attr = (AttributeCondition) condition; + yield new AttributeIncludes(attr.getLocalName(), attr.getValue()); + } + case Condition.SAC_BEGIN_HYPHEN_ATTRIBUTE_CONDITION -> { + AttributeCondition attr = (AttributeCondition) condition; + yield new AttributeBeginHyphen(attr.getLocalName(), attr.getValue()); + } + case Condition.SAC_AND_CONDITION -> { + CombinatorCondition combo = (CombinatorCondition) condition; + yield new And(translateCondition(combo.getFirstCondition()), + translateCondition(combo.getSecondCondition())); + } + default -> throw new IllegalArgumentException( + "Unsupported SAC condition type: " + condition.getConditionType() + " (" + condition + ")"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + }; + } +} diff --git a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/engine/selector/SelectorMatcher.java b/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/engine/selector/SelectorMatcher.java new file mode 100644 index 00000000000..82e97624dd3 --- /dev/null +++ b/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/engine/selector/SelectorMatcher.java @@ -0,0 +1,288 @@ +/******************************************************************************* + * Copyright (c) 2026 Lars Vogel and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Lars Vogel - initial API and implementation + *******************************************************************************/ +package org.eclipse.e4.ui.css.core.impl.engine.selector; + +import org.eclipse.e4.ui.css.core.dom.CSSStylableElement; +import org.eclipse.e4.ui.css.core.impl.engine.selector.Selectors.Adjacent; +import org.eclipse.e4.ui.css.core.impl.engine.selector.Selectors.And; +import org.eclipse.e4.ui.css.core.impl.engine.selector.Selectors.AttributeBeginHyphen; +import org.eclipse.e4.ui.css.core.impl.engine.selector.Selectors.AttributeIncludes; +import org.eclipse.e4.ui.css.core.impl.engine.selector.Selectors.AttributeSelector; +import org.eclipse.e4.ui.css.core.impl.engine.selector.Selectors.Child; +import org.eclipse.e4.ui.css.core.impl.engine.selector.Selectors.ClassSelector; +import org.eclipse.e4.ui.css.core.impl.engine.selector.Selectors.Descendant; +import org.eclipse.e4.ui.css.core.impl.engine.selector.Selectors.ElementType; +import org.eclipse.e4.ui.css.core.impl.engine.selector.Selectors.IdSelector; +import org.eclipse.e4.ui.css.core.impl.engine.selector.Selectors.PseudoClass; +import org.eclipse.e4.ui.css.core.impl.engine.selector.Selectors.Selector; +import org.eclipse.e4.ui.css.core.impl.engine.selector.Selectors.SelectorList; +import org.eclipse.e4.ui.css.core.impl.engine.selector.Selectors.Universal; +import org.w3c.dom.Element; +import org.w3c.dom.Node; + +/** + * Matches a {@link Selector} against an {@link Element}. + * + *

+ * Every method is static; the matcher carries no state. Callers pass the + * element being tested plus an optional pseudo-element string (the same + * argument the SAC engine carried) so that pseudo-class matching can defer + * to the existing {@link CSSStylableElement#isPseudoInstanceOf} contract. + *

+ * + *

+ * Tag-name comparison is case sensitive, matching the existing SAC matcher + * (Phase 1 test {@code testTagNameCaseSensitivity} in {@code CSSEngineTest} + * locks this in). Pseudo-class semantics also follow the existing engine: + * the static-pseudo-instance carve-out from + * {@code CSSPseudoClassConditionImpl} is preserved so cascade behaviour + * does not shift. + *

+ */ +public final class SelectorMatcher { + + private SelectorMatcher() { + // statics only + } + + /** + * @return {@code true} if {@code selector} matches {@code element} for + * the given pseudo state. + */ + public static boolean matches(Selector selector, Element element, String pseudoElement) { + return matches(selector, element, pseudoElement, null, 0); + } + + /** + * @return {@code true} if {@code selector} matches {@code element} for + * the given pseudo state, using a pre-computed ancestor hierarchy array if available. + */ + public static boolean matches(Selector selector, Element element, String pseudoElement, Element[] hierarchy, int hierarchyIndex) { + if (element == null) { + return false; + } + if (selector instanceof Universal) { + return true; + } + if (selector instanceof ElementType type) { + return matchesElementType(type, element); + } + if (selector instanceof ClassSelector cls) { + return matchesClass(cls, element); + } + if (selector instanceof IdSelector id) { + return matchesId(id, element); + } + if (selector instanceof AttributeSelector attr) { + return matchesAttribute(attr, element); + } + if (selector instanceof AttributeIncludes inc) { + return matchesAttributeIncludes(inc, element); + } + if (selector instanceof AttributeBeginHyphen beg) { + return matchesAttributeBeginHyphen(beg, element); + } + if (selector instanceof PseudoClass pc) { + return matchesPseudoClass(pc, element, pseudoElement); + } + if (selector instanceof And and) { + return matches(and.left(), element, pseudoElement, hierarchy, hierarchyIndex) + && matches(and.right(), element, pseudoElement, hierarchy, hierarchyIndex); + } + if (selector instanceof Descendant d) { + return matchesDescendant(d, element, pseudoElement, hierarchy, hierarchyIndex); + } + if (selector instanceof Child c) { + return matchesChild(c, element, pseudoElement, hierarchy, hierarchyIndex); + } + if (selector instanceof Adjacent a) { + return matchesAdjacent(a, element, pseudoElement, hierarchy, hierarchyIndex); + } + if (selector instanceof SelectorList list) { + return matchesAny(list, element, pseudoElement, hierarchy, hierarchyIndex); + } + throw new IllegalStateException("Unknown selector kind: " + selector.getClass()); //$NON-NLS-1$ + } + + private static boolean matchesElementType(ElementType type, Element element) { + String localName = type.localName(); + if (localName == null) { + return true; + } + String elementName = element.getPrefix() == null ? element.getNodeName() : element.getLocalName(); + return localName.equals(elementName); + } + + private static boolean matchesClass(ClassSelector cls, Element element) { + if (!(element instanceof CSSStylableElement stylable)) { + return false; + } + String elementClass = stylable.getCSSClass(); + if (elementClass == null) { + return false; + } + // CSS class attribute can be a whitespace-separated list of classes. + // Walk the string manually to avoid the regex compile + array allocation + // String.split forces on every match evaluation. + return containsWord(elementClass, cls.className()); + } + + private static boolean matchesId(IdSelector id, Element element) { + if (!(element instanceof CSSStylableElement stylable)) { + return false; + } + return id.id().equals(stylable.getCSSId()); + } + + private static boolean matchesAttribute(AttributeSelector attr, Element element) { + String name = attr.name(); + if (!element.hasAttribute(name)) { + return false; + } + String required = attr.value(); + if (required == null) { + // presence form: [attr] + return true; + } + return required.equals(element.getAttribute(name)); + } + + private static boolean matchesAttributeIncludes(AttributeIncludes inc, Element element) { + String actual = element.getAttribute(inc.name()); + if (actual == null) { + return false; + } + return containsWord(actual, inc.value()); + } + + /** + * Returns {@code true} if {@code haystack} contains {@code word} as a + * whitespace-separated token. Equivalent to splitting on + * {@code \s+} and checking for an exact token match, but without the + * regex compile and array allocation each call. + */ + private static boolean containsWord(String haystack, String word) { + if (word == null || word.isEmpty()) { + return false; + } + int wordLength = word.length(); + int length = haystack.length(); + int i = 0; + while (i < length) { + while (i < length && Character.isWhitespace(haystack.charAt(i))) { + i++; + } + int start = i; + while (i < length && !Character.isWhitespace(haystack.charAt(i))) { + i++; + } + if (i - start == wordLength && haystack.regionMatches(start, word, 0, wordLength)) { + return true; + } + } + return false; + } + + private static boolean matchesAttributeBeginHyphen(AttributeBeginHyphen beg, Element element) { + String actual = element.getAttribute(beg.name()); + if (actual == null) { + return false; + } + String value = beg.value(); + return actual.equals(value) || actual.startsWith(value + "-"); + } + + private static boolean matchesPseudoClass(PseudoClass pseudo, Element element, String pseudoElement) { + String name = pseudo.name(); + // If the caller is iterating a static-pseudo cascade, only match the + // pseudo argument on the way down. + if (pseudoElement != null && !pseudoElement.equals(name)) { + return false; + } + if (!(element instanceof CSSStylableElement stylable)) { + return false; + } + if (!stylable.isPseudoInstanceOf(name)) { + return false; + } + if (pseudoElement == null) { + // Same carve-out as CSSPseudoClassConditionImpl: when no pseudo + // element argument is supplied, pseudos that the element + // publishes only as static instances do not match the regular + // cascade. They get applied separately via the default style + // declaration map. + return !stylable.isStaticPseudoInstance(name); + } + return true; + } + + private static boolean matchesDescendant(Descendant d, Element element, String pseudoElement, Element[] hierarchy, int hierarchyIndex) { + if (!matches(d.descendant(), element, pseudoElement, hierarchy, hierarchyIndex)) { + return false; + } + if (hierarchy != null) { + for (int i = hierarchyIndex + 1; i < hierarchy.length; i++) { + if (matches(d.ancestor(), hierarchy[i], null, hierarchy, i)) { + return true; + } + } + return false; + } else { + Node parent = element.getParentNode(); + while (parent instanceof Element parentElement) { + if (matches(d.ancestor(), parentElement, null, null, 0)) { + return true; + } + parent = parentElement.getParentNode(); + } + return false; + } + } + + private static boolean matchesChild(Child c, Element element, String pseudoElement, Element[] hierarchy, int hierarchyIndex) { + if (!matches(c.child(), element, pseudoElement, hierarchy, hierarchyIndex)) { + return false; + } + if (hierarchy != null) { + int parentIdx = hierarchyIndex + 1; + if (parentIdx < hierarchy.length) { + return matches(c.parent(), hierarchy[parentIdx], null, hierarchy, parentIdx); + } + return false; + } else { + Node parent = element.getParentNode(); + return parent instanceof Element parentElement && matches(c.parent(), parentElement, null, null, 0); + } + } + + private static boolean matchesAdjacent(Adjacent a, Element element, String pseudoElement, Element[] hierarchy, int hierarchyIndex) { + if (!matches(a.second(), element, pseudoElement, hierarchy, hierarchyIndex)) { + return false; + } + Node previous = element.getPreviousSibling(); + while (previous != null && !(previous instanceof Element)) { + previous = previous.getPreviousSibling(); + } + return previous instanceof Element previousElement && matches(a.first(), previousElement, null, null, 0); + } + + private static boolean matchesAny(SelectorList list, Element element, String pseudoElement, Element[] hierarchy, int hierarchyIndex) { + for (Selector alternative : list.alternatives()) { + if (matches(alternative, element, pseudoElement, hierarchy, hierarchyIndex)) { + return true; + } + } + return false; + } +} diff --git a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/engine/selector/Selectors.java b/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/engine/selector/Selectors.java new file mode 100644 index 00000000000..ebca60b165b --- /dev/null +++ b/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/engine/selector/Selectors.java @@ -0,0 +1,357 @@ +/******************************************************************************* + * Copyright (c) 2026 Lars Vogel and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Lars Vogel - initial API and implementation + *******************************************************************************/ +package org.eclipse.e4.ui.css.core.impl.engine.selector; + +import java.util.List; + +/** + * Internal CSS selector AST. + * + *

+ * The engine historically exposed W3C SAC selectors + * ({@code org.w3c.css.sac.Selector} and friends) and matched against them + * through a hierarchy of vendored Batik wrapper classes under + * {@code impl/sac/*}. This package replaces both with a small set of records + * that the engine owns end to end. The W3C SAC types stay only as long as + * the parser still emits them; a translator turns the SAC selector tree + * produced by the Batik SAC parser into one of these records before it + * reaches the engine matcher. + *

+ * + *

+ * Specificity follows CSS 2.1: 100 per id, 10 per class / attribute / + * pseudo-class, 1 per element, 0 for the universal selector. Combinators + * sum the specificity of their operands; selector lists report the maximum + * specificity over their alternatives. + *

+ */ +public final class Selectors { + + private Selectors() { + // constants only + } + + /** A parsed CSS selector. Sealed; pattern-match in the matcher. */ + public sealed interface Selector + permits Universal, ElementType, ClassSelector, IdSelector, AttributeSelector, + AttributeIncludes, AttributeBeginHyphen, PseudoClass, And, Descendant, Child, Adjacent, SelectorList { + + /** CSS specificity contribution of this selector. */ + int specificity(); + + /** Best-effort textual reproduction of the selector. */ + String text(); + } + + /** {@code *} — matches any element. */ + public record Universal() implements Selector { + @Override + public int specificity() { + return 0; + } + + @Override + public String text() { + return "*"; //$NON-NLS-1$ + } + + @Override + public String toString() { + return text(); + } + } + + /** {@code Button} — matches by local element name. */ + public record ElementType(String localName) implements Selector { + @Override + public int specificity() { + return 1; + } + + @Override + public String text() { + return localName; + } + + @Override + public String toString() { + return text(); + } + } + + /** {@code .foo} — matches by CSS class. */ + public record ClassSelector(String className) implements Selector { + @Override + public int specificity() { + return 10; + } + + @Override + public String text() { + return "." + className; //$NON-NLS-1$ + } + + @Override + public String toString() { + return text(); + } + } + + /** {@code #foo} — matches by CSS id. */ + public record IdSelector(String id) implements Selector { + @Override + public int specificity() { + return 100; + } + + @Override + public String text() { + return "#" + id; //$NON-NLS-1$ + } + + @Override + public String toString() { + return text(); + } + } + + /** + * {@code [attr]} or {@code [attr='value']}. {@code value} is {@code null} + * for the presence form and the empty string for {@code [attr='']}. + */ + public record AttributeSelector(String name, String value) implements Selector { + @Override + public int specificity() { + return 10; + } + + @Override + public String text() { + return value == null ? "[" + name + "]" : "[" + name + "='" + value + "']"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ + } + + @Override + public String toString() { + return text(); + } + } + + /** {@code [attr~='value']} — matches when {@code attr} contains {@code value} as a whitespace-separated word. */ + public record AttributeIncludes(String name, String value) implements Selector { + @Override + public int specificity() { + return 10; + } + + @Override + public String text() { + return "[" + name + "~='" + value + "']"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + } + + @Override + public String toString() { + return text(); + } + } + + /** {@code [attr|='value']} — matches when {@code attr} equals {@code value} or starts with {@code value-}. */ + public record AttributeBeginHyphen(String name, String value) implements Selector { + @Override + public int specificity() { + return 10; + } + + @Override + public String text() { + return "[" + name + "|='" + value + "']"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + } + + @Override + public String toString() { + return text(); + } + } + + /** + * {@code :name} — matches when the element answers true to + * {@code isPseudoInstanceOf(name)} on its CSS-stylable element wrapper. + */ + public record PseudoClass(String name) implements Selector { + @Override + public int specificity() { + return 10; + } + + @Override + public String text() { + return ":" + name; //$NON-NLS-1$ + } + + @Override + public String toString() { + return text(); + } + } + + /** + * Compound selector: every operand must match the same element. Built + * by the translator for forms like {@code Button.primary#go} or + * {@code [a][b]} where multiple simple selectors apply to one element. + */ + public record And(Selector left, Selector right) implements Selector { + @Override + public int specificity() { + return left.specificity() + right.specificity(); + } + + @Override + public String text() { + return left.text() + right.text(); + } + + @Override + public String toString() { + return text(); + } + } + + /** {@code ancestor descendant} — descendant combinator. */ + public record Descendant(Selector ancestor, Selector descendant) implements Selector { + @Override + public int specificity() { + return ancestor.specificity() + descendant.specificity(); + } + + @Override + public String text() { + return ancestor.text() + " " + descendant.text(); //$NON-NLS-1$ + } + + @Override + public String toString() { + return text(); + } + } + + /** {@code parent > child} — child combinator. */ + public record Child(Selector parent, Selector child) implements Selector { + @Override + public int specificity() { + return parent.specificity() + child.specificity(); + } + + @Override + public String text() { + return parent.text() + " > " + child.text(); //$NON-NLS-1$ + } + + @Override + public String toString() { + return text(); + } + } + + /** {@code first + second} — direct adjacent sibling combinator. */ + public record Adjacent(Selector first, Selector second) implements Selector { + @Override + public int specificity() { + return first.specificity() + second.specificity(); + } + + @Override + public String text() { + return first.text() + " + " + second.text(); //$NON-NLS-1$ + } + + @Override + public String toString() { + return text(); + } + } + + /** + * {@code a, b} — selector list. Specificity reports the maximum over the + * alternatives so cascade ordering can match a list against an element by + * iterating its alternatives. + * + *

+ * A regular final class rather than a record because the cascade reads + * {@link #specificity()} once per matched alternative and we want it + * precomputed; record components cannot host derived state. + *

+ */ + public static final class SelectorList implements Selector { + + private final List alternatives; + private final int specificity; + + public SelectorList(List alternatives) { + this.alternatives = List.copyOf(alternatives); + int max = 0; + for (Selector alternative : this.alternatives) { + int s = alternative.specificity(); + if (s > max) { + max = s; + } + } + this.specificity = max; + } + + public List alternatives() { + return alternatives; + } + + /** Number of alternatives in the list. SAC-style accessor for callers iterating the list. */ + public int getLength() { + return alternatives.size(); + } + + /** {@code i}-th alternative. SAC-style accessor for callers iterating the list. */ + public Selector item(int i) { + return alternatives.get(i); + } + + @Override + public int specificity() { + return specificity; + } + + @Override + public String text() { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < alternatives.size(); i++) { + if (i > 0) { + sb.append(", "); //$NON-NLS-1$ + } + sb.append(alternatives.get(i).text()); + } + return sb.toString(); + } + + @Override + public boolean equals(Object o) { + return o instanceof SelectorList other && alternatives.equals(other.alternatives); + } + + @Override + public int hashCode() { + return alternatives.hashCode(); + } + + @Override + public String toString() { + return text(); + } + } +} diff --git a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/AbstractAttributeCondition.java b/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/AbstractAttributeCondition.java deleted file mode 100644 index fabffbad828..00000000000 --- a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/AbstractAttributeCondition.java +++ /dev/null @@ -1,86 +0,0 @@ -/* - - Copyright 2002, 2014 The Apache Software Foundation - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - */ - -/* This class copied from org.apache.batik.css.engine.sac */ - -package org.eclipse.e4.ui.css.core.impl.sac; - -import java.util.Objects; -import org.w3c.css.sac.AttributeCondition; - -/** - * This class provides an abstract implementation of the {@link - * org.w3c.css.sac.AttributeCondition} interface. - */ -public abstract class AbstractAttributeCondition implements AttributeCondition, - ExtendedCondition { - - /** - * The attribute value. - */ - protected String value; - - /** - * Creates a new AbstractAttributeCondition object. - */ - protected AbstractAttributeCondition(String value) { - this.value = value; - } - - /** - * Indicates whether some other object is "equal to" this one. - * - * @param obj - * the reference object with which to compare. - */ - @Override - public boolean equals(Object obj) { - if (obj == null || (obj.getClass() != getClass())) { - return false; - } - AbstractAttributeCondition c = (AbstractAttributeCondition) obj; - return c.value.equals(value); - } - - /** - * equal objects should have equal hashCodes. - * - * @return hashCode of this AbstractAttributeCondition - */ - @Override - public int hashCode() { - return Objects.hashCode(value); - } - - /** - * Returns the specificity of this condition. - */ - @Override - public int getSpecificity() { - return 1 << 8; - } - - /** - * SAC: Implements {@link - * org.w3c.css.sac.AttributeCondition#getValue()}. - */ - @Override - public String getValue() { - return value; - } -} diff --git a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/AbstractCombinatorCondition.java b/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/AbstractCombinatorCondition.java deleted file mode 100644 index 0de6c81b86c..00000000000 --- a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/AbstractCombinatorCondition.java +++ /dev/null @@ -1,93 +0,0 @@ -/* - - Copyright 2002, 2014 The Apache Software Foundation - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - */ - -/* This class copied from org.apache.batik.css.engine.sac */ - -package org.eclipse.e4.ui.css.core.impl.sac; - -import org.w3c.css.sac.CombinatorCondition; -import org.w3c.css.sac.Condition; - -/** - * This class provides an abstract implementation of the {@link - * org.w3c.css.sac.CombinatorCondition} interface. - */ -public abstract class AbstractCombinatorCondition implements - CombinatorCondition, ExtendedCondition { - - /** - * The first condition. - */ - protected Condition firstCondition; - - /** - * The second condition. - */ - protected Condition secondCondition; - - /** - * Creates a new CombinatorCondition object. - */ - protected AbstractCombinatorCondition(Condition c1, Condition c2) { - firstCondition = c1; - secondCondition = c2; - } - - /** - * Indicates whether some other object is "equal to" this one. - * - * @param obj - * the reference object with which to compare. - */ - @Override - public boolean equals(Object obj) { - if (obj == null || (obj.getClass() != getClass())) { - return false; - } - AbstractCombinatorCondition c = (AbstractCombinatorCondition) obj; - return (c.firstCondition.equals(firstCondition) && c.secondCondition - .equals(secondCondition)); - } - - /** - * Returns the specificity of this condition. - */ - @Override - public int getSpecificity() { - return ((ExtendedCondition) getFirstCondition()).getSpecificity() - + ((ExtendedCondition) getSecondCondition()).getSpecificity(); - } - - /** - * SAC: Implements {@link - * org.w3c.css.sac.CombinatorCondition#getFirstCondition()}. - */ - @Override - public Condition getFirstCondition() { - return firstCondition; - } - - /** - * SAC: Implements {@link - * org.w3c.css.sac.CombinatorCondition#getSecondCondition()}. - */ - @Override - public Condition getSecondCondition() { - return secondCondition; - } -} diff --git a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/AbstractDescendantSelector.java b/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/AbstractDescendantSelector.java deleted file mode 100644 index f183af1ccae..00000000000 --- a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/AbstractDescendantSelector.java +++ /dev/null @@ -1,93 +0,0 @@ -/* - - Copyright 2002, 2014 The Apache Software Foundation - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - */ - -/* This class copied from org.apache.batik.css.engine.sac */ - -package org.eclipse.e4.ui.css.core.impl.sac; - -import org.w3c.css.sac.DescendantSelector; -import org.w3c.css.sac.Selector; -import org.w3c.css.sac.SimpleSelector; - -/** - * This class provides an abstract implementation of the {@link - * org.w3c.css.sac.DescendantSelector} interface. - */ -public abstract class AbstractDescendantSelector - implements DescendantSelector, - ExtendedSelector { - - /** - * The ancestor selector. - */ - protected Selector ancestorSelector; - - /** - * The simple selector. - */ - protected SimpleSelector simpleSelector; - - /** - * Creates a new DescendantSelector object. - */ - protected AbstractDescendantSelector(Selector ancestor, - SimpleSelector simple) { - ancestorSelector = ancestor; - simpleSelector = simple; - } - - /** - * Indicates whether some other object is "equal to" this one. - * @param obj the reference object with which to compare. - */ - @Override - public boolean equals(Object obj) { - if (obj == null || (obj.getClass() != getClass())) { - return false; - } - AbstractDescendantSelector s = (AbstractDescendantSelector)obj; - return s.simpleSelector.equals(simpleSelector); - } - - /** - * Returns the specificity of this selector. - */ - @Override - public int getSpecificity() { - return ((ExtendedSelector)ancestorSelector).getSpecificity() + - ((ExtendedSelector)simpleSelector).getSpecificity(); - } - - /** - * SAC: Implements {@link - * org.w3c.css.sac.DescendantSelector#getAncestorSelector()}. - */ - @Override - public Selector getAncestorSelector() { - return ancestorSelector; - } - - /** - * SAC: Implements {@link - * org.w3c.css.sac.DescendantSelector#getSimpleSelector()}. - */ - @Override - public SimpleSelector getSimpleSelector() { - return simpleSelector; - } -} diff --git a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/AbstractElementSelector.java b/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/AbstractElementSelector.java deleted file mode 100644 index 529b86a0fbb..00000000000 --- a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/AbstractElementSelector.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - - Copyright 2002, 2015 The Apache Software Foundation - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - */ - -/* This class copied from org.apache.batik.css.engine.sac */ - -package org.eclipse.e4.ui.css.core.impl.sac; - -import java.util.Set; -import org.w3c.css.sac.ElementSelector; - -/** - * This class provides an abstract implementation of the ElementSelector - * interface. - */ -public abstract class AbstractElementSelector implements ElementSelector, ExtendedSelector { - - /** - * The namespace URI. - */ - protected String namespaceURI; - - /** - * The local name. - */ - protected String localName; - - /** - * Creates a new ElementSelector object. - */ - protected AbstractElementSelector(String uri, String name) { - namespaceURI = uri; - localName = name; - } - - /** - * Indicates whether some other object is "equal to" this one. - * @param obj the reference object with which to compare. - */ - @Override - public boolean equals(Object obj) { - if (obj == null || (obj.getClass() != getClass())) { - return false; - } - AbstractElementSelector s = (AbstractElementSelector)obj; - return (s.namespaceURI.equals(namespaceURI) && s.localName.equals(localName)); - } - - /** - * SAC: Implements {@link - * org.w3c.css.sac.ElementSelector#getNamespaceURI()}. - */ - @Override - public String getNamespaceURI() { - return namespaceURI; - } - - /** - * SAC: Implements {@link - * org.w3c.css.sac.ElementSelector#getLocalName()}. - */ - @Override - public String getLocalName() { - return localName; - } - - /** - * Fills the given set with the attribute names found in this selector. - */ - @Override - public void fillAttributeSet(Set attrSet) { - } -} diff --git a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/AbstractSiblingSelector.java b/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/AbstractSiblingSelector.java deleted file mode 100644 index bb061dd13e0..00000000000 --- a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/AbstractSiblingSelector.java +++ /dev/null @@ -1,108 +0,0 @@ -/* - - Copyright 2002, 2014 The Apache Software Foundation - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - */ - -/* This class copied from org.apache.batik.css.engine.sac */ - -package org.eclipse.e4.ui.css.core.impl.sac; - -import org.w3c.css.sac.Selector; -import org.w3c.css.sac.SiblingSelector; -import org.w3c.css.sac.SimpleSelector; - -/** - * This class provides an abstract implementation of the {@link - * org.w3c.css.sac.SiblingSelector} interface. - */ -public abstract class AbstractSiblingSelector implements SiblingSelector, - ExtendedSelector { - - /** - * The node type. - */ - protected short nodeType; - - /** - * The selector. - */ - protected Selector selector; - - /** - * The simple selector. - */ - protected SimpleSelector simpleSelector; - - /** - * Creates a new SiblingSelector object. - */ - protected AbstractSiblingSelector(short type, Selector sel, - SimpleSelector simple) { - nodeType = type; - selector = sel; - simpleSelector = simple; - } - - /** - * Returns the node type. - */ - @Override - public short getNodeType() { - return nodeType; - } - - /** - * Indicates whether some other object is "equal to" this one. - * - * @param obj - * the reference object with which to compare. - */ - @Override - public boolean equals(Object obj) { - if (obj == null || (obj.getClass() != getClass())) { - return false; - } - AbstractSiblingSelector s = (AbstractSiblingSelector) obj; - return s.simpleSelector.equals(simpleSelector); - } - - /** - * Returns the specificity of this selector. - */ - @Override - public int getSpecificity() { - return ((ExtendedSelector) selector).getSpecificity() - + ((ExtendedSelector) simpleSelector).getSpecificity(); - } - - /** - * SAC: Implements {@link - * org.w3c.css.sac.SiblingSelector#getSelector()}. - */ - @Override - public Selector getSelector() { - return selector; - } - - /** - * SAC: Implements {@link - * org.w3c.css.sac.SiblingSelector#getSiblingSelector()}. - */ - @Override - public SimpleSelector getSiblingSelector() { - return simpleSelector; - } -} diff --git a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSAndConditionImpl.java b/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSAndConditionImpl.java deleted file mode 100644 index b9d528d05ad..00000000000 --- a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSAndConditionImpl.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - - Copyright 2002, 2015 The Apache Software Foundation - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - */ - -/* This class copied from org.apache.batik.css.engine.sac */ - -package org.eclipse.e4.ui.css.core.impl.sac; - -import java.util.Set; -import org.w3c.css.sac.Condition; -import org.w3c.dom.Element; - -/** - * This class provides an implementation of the - * {@link org.w3c.css.sac.CombinatorCondition} interface. - */ -public class CSSAndConditionImpl extends AbstractCombinatorCondition { - /** - * Creates a new CombinatorCondition object. - */ - public CSSAndConditionImpl(Condition c1, Condition c2) { - super(c1, c2); - } - - /** - * SAC: Implements {@link - * org.w3c.css.sac.Condition#getConditionType()}. - */ - @Override - public short getConditionType() { - return SAC_AND_CONDITION; - } - - /** - * Tests whether this condition matches the given element. - */ - @Override - public boolean match(Element e, String pseudoE) { - return ((ExtendedCondition)getFirstCondition()).match(e, pseudoE) && - ((ExtendedCondition)getSecondCondition()).match(e, pseudoE); - } - - /** - * Fills the given set with the attribute names found in this selector. - */ - @Override - public void fillAttributeSet(Set attrSet) { - ((ExtendedCondition)getFirstCondition()).fillAttributeSet(attrSet); - ((ExtendedCondition)getSecondCondition()).fillAttributeSet(attrSet); - } - - /** - * Returns a text representation of this object. - */ - @Override - public String toString() { - return String.valueOf( getFirstCondition() ) + getSecondCondition(); - } -} diff --git a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSAttributeConditionImpl.java b/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSAttributeConditionImpl.java deleted file mode 100644 index 3467d068938..00000000000 --- a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSAttributeConditionImpl.java +++ /dev/null @@ -1,153 +0,0 @@ -/* - - Copyright 2002, 2015 The Apache Software Foundation - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - */ - -/* This class copied from org.apache.batik.css.engine.sac */ - -package org.eclipse.e4.ui.css.core.impl.sac; - -import java.util.Set; -import org.w3c.dom.Element; - -/** - * This class provides an implementation of the - * {@link org.w3c.css.sac.AttributeCondition} interface. - */ -public class CSSAttributeConditionImpl extends AbstractAttributeCondition { - /** - * The attribute's local name. - */ - protected String localName; - - /** - * The attribute's namespace URI. - */ - protected String namespaceURI; - - /** - * Whether this condition applies to specified attributes. - */ - protected boolean specified; - - /** - * Creates a new CSSAttributeCondition object. - */ - public CSSAttributeConditionImpl(String localName, String namespaceURI, - boolean specified, String value) { - super(value); - this.localName = localName; - this.namespaceURI = namespaceURI; - this.specified = specified; - } - - /** - * Indicates whether some other object is "equal to" this one. - * - * @param obj - * the reference object with which to compare. - */ - @Override - public boolean equals(Object obj) { - if (!super.equals(obj)) { - return false; - } - CSSAttributeConditionImpl c = (CSSAttributeConditionImpl) obj; - return (c.namespaceURI.equals(namespaceURI) - && c.localName.equals(localName) && c.specified == specified); - } - - /** - * equal objects should have equal hashCodes. - * - * @return hashCode of this CSSAttributeCondition - */ - @Override - public int hashCode() { - return namespaceURI.hashCode() ^ localName.hashCode() - ^ Boolean.hashCode(specified); - } - - /** - * SAC: Implements {@link - * org.w3c.css.sac.Condition#getConditionType()}. - */ - @Override - public short getConditionType() { - return SAC_ATTRIBUTE_CONDITION; - } - - /** - * SAC: Implements {@link - * org.w3c.css.sac.AttributeCondition#getNamespaceURI()}. - */ - @Override - public String getNamespaceURI() { - return namespaceURI; - } - - /** - * SAC: Implements {@link - * org.w3c.css.sac.AttributeCondition#getLocalName()}. - */ - @Override - public String getLocalName() { - return localName; - } - - /** - * SAC: Implements {@link - * org.w3c.css.sac.AttributeCondition#getSpecified()}. - */ - @Override - public boolean getSpecified() { - return specified; - } - - /** - * Tests whether this condition matches the given element. - */ - @Override - public boolean match(Element e, String pseudoE) { - if (!e.hasAttribute(getLocalName())) { - return false; - } - String val = getValue(); - if (val == null) { - return true; - } - return e.getAttribute(getLocalName()).equals(val); - } - - /** - * Fills the given set with the attribute names found in this selector. - */ - @Override - public void fillAttributeSet(Set attrSet) { - attrSet.add(localName); - } - - /** - * Returns a text representation of this object. - */ - @Override - public String toString() { - if (value == null) { - return '[' + localName + ']'; - } - return '[' + localName + "=\"" + value + "\"]"; - } -} diff --git a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSBeginHyphenAttributeConditionImpl.java b/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSBeginHyphenAttributeConditionImpl.java deleted file mode 100644 index 12064bb8e47..00000000000 --- a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSBeginHyphenAttributeConditionImpl.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - - Copyright 2002, 2014 The Apache Software Foundation - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - */ - -/* This class copied from org.apache.batik.css.engine.sac */ - -package org.eclipse.e4.ui.css.core.impl.sac; - -import org.w3c.dom.Element; - -/** - * This class provides an implementation of the - * {@link org.w3c.css.sac.AttributeCondition} interface. - */ -public class CSSBeginHyphenAttributeConditionImpl extends - CSSAttributeConditionImpl { - - /** - * Creates a new CSSAttributeCondition object. - */ - public CSSBeginHyphenAttributeConditionImpl(String localName, - String namespaceURI, boolean specified, String value) { - super(localName, namespaceURI, specified, value); - } - - /** - * SAC: Implements {@link - * org.w3c.css.sac.Condition#getConditionType()}. - */ - @Override - public short getConditionType() { - return SAC_BEGIN_HYPHEN_ATTRIBUTE_CONDITION; - } - - /** - * Tests whether this condition matches the given element. - */ - @Override - public boolean match(Element e, String pseudoE) { - return e.getAttribute(getLocalName()).startsWith(getValue()); - } - - /** - * Returns a text representation of this object. - */ - @Override - public String toString() { - return '[' + getLocalName() + "|=\"" + getValue() + "\"]"; - } -} diff --git a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSChildSelectorImpl.java b/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSChildSelectorImpl.java deleted file mode 100644 index 450031025f2..00000000000 --- a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSChildSelectorImpl.java +++ /dev/null @@ -1,102 +0,0 @@ -/* - - Copyright 2002, 2015 The Apache Software Foundation - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - */ - -/* This class copied from org.apache.batik.css.engine.sac */ - -package org.eclipse.e4.ui.css.core.impl.sac; - -import java.util.Set; -import org.w3c.css.sac.Selector; -import org.w3c.css.sac.SimpleSelector; -import org.w3c.dom.Element; -import org.w3c.dom.Node; - -/** - * This class provides an implementation of the - * {@link org.w3c.css.sac.DescendantSelector} interface. - */ -public class CSSChildSelectorImpl extends AbstractDescendantSelector { - - /** - * Creates a new CSSChildSelector object. - */ - public CSSChildSelectorImpl(Selector ancestor, SimpleSelector simple) { - super(ancestor, simple); - } - - /** - * SAC: Implements {@link - * org.w3c.css.sac.Selector#getSelectorType()}. - */ - @Override - public short getSelectorType() { - return SAC_CHILD_SELECTOR; - } - - /** - * Tests whether this selector matches the given element. - */ - @Override - public boolean match(Element e, Node[] hierarchy, int parentIndex, String pseudoE) { - if (hierarchy == null || parentIndex >= hierarchy.length) { - return false; - } - - Node n = hierarchy[parentIndex]; - if (n != null && n.getNodeType() == Node.ELEMENT_NODE) { - return ((ExtendedSelector) getAncestorSelector()).match((Element) n, hierarchy, parentIndex + 1, null) - && ((ExtendedSelector) getSimpleSelector()).match(e, hierarchy, parentIndex + 1, pseudoE); - } - return false; - } - - /** - * Tests whether this selector matches the given element. - */ - @Override - public boolean match(Element e, String pseudoE) { - Node n = e.getParentNode(); - if (n != null && n.getNodeType() == Node.ELEMENT_NODE) { - return ((ExtendedSelector) getAncestorSelector()).match((Element) n, - null) - && ((ExtendedSelector) getSimpleSelector()).match(e, pseudoE); - } - return false; - } - - /** - * Fills the given set with the attribute names found in this selector. - */ - @Override - public void fillAttributeSet(Set attrSet) { - ((ExtendedSelector)getAncestorSelector()).fillAttributeSet(attrSet); - ((ExtendedSelector)getSimpleSelector()).fillAttributeSet(attrSet); - } - - /** - * Returns a representation of the selector. - */ - @Override - public String toString() { - SimpleSelector s = getSimpleSelector(); - if (s.getSelectorType() == SAC_PSEUDO_ELEMENT_SELECTOR) { - return String.valueOf( getAncestorSelector() ) + s; - } - return getAncestorSelector() + " > " + s; - } -} diff --git a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSClassConditionImpl.java b/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSClassConditionImpl.java deleted file mode 100644 index 521c7743af1..00000000000 --- a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSClassConditionImpl.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - - Copyright 2002, 2014 The Apache Software Foundation - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - */ - -/* This class copied from org.apache.batik.css.engine.sac */ - -package org.eclipse.e4.ui.css.core.impl.sac; - -import org.eclipse.e4.ui.css.core.dom.CSSStylableElement; -import org.w3c.dom.Element; - -/** - * This class provides an implementation of the - * {@link org.w3c.css.sac.AttributeCondition} interface. - */ -public class CSSClassConditionImpl extends CSSAttributeConditionImpl { - - /** - * Creates a new CSSAttributeCondition object. - */ - public CSSClassConditionImpl(String localName, String namespaceURI, - String value) { - super(localName, namespaceURI, true, value); - } - - @Override - public boolean match(Element e, String pseudoE) { - String attr = null; - if ((e instanceof CSSStylableElement)) { - attr = ((CSSStylableElement) e).getCSSClass(); - } else { - attr = e.getAttribute("class"); - } - if (attr == null || attr.length() < 1) { - return false; - } - String val = getValue(); - int attrLen = attr.length(); - int valLen = val.length(); - for (int i = attr.indexOf(val); i != -1; i = attr.indexOf(val, i - + valLen)) { - if ((i == 0 || Character.isSpaceChar(attr.charAt(i - 1))) - && (i + valLen == attrLen || Character.isSpaceChar(attr - .charAt(i + valLen)))) { - return true; - } - } - - return false; - } -} diff --git a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSConditionFactoryImpl.java b/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSConditionFactoryImpl.java deleted file mode 100644 index 24676814228..00000000000 --- a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSConditionFactoryImpl.java +++ /dev/null @@ -1,213 +0,0 @@ -/* - - Copyright 2002, 2014 The Apache Software Foundation - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - */ - -/* This class copied from org.apache.batik.css.engine.sac */ - -package org.eclipse.e4.ui.css.core.impl.sac; - -import org.w3c.css.sac.AttributeCondition; -import org.w3c.css.sac.CSSException; -import org.w3c.css.sac.CombinatorCondition; -import org.w3c.css.sac.Condition; -import org.w3c.css.sac.ConditionFactory; -import org.w3c.css.sac.ContentCondition; -import org.w3c.css.sac.LangCondition; -import org.w3c.css.sac.NegativeCondition; -import org.w3c.css.sac.PositionalCondition; - -/** - * This class provides an implementation of the - * {@link org.w3c.css.sac.ConditionFactory} interface. - */ -public class CSSConditionFactoryImpl implements ConditionFactory { - - private static final String NOT_IMPLEMENTED_IN_CSS2 = "Not implemented in CSS2"; //$NON-NLS-1$ - - /** - * The class attribute namespace URI. - */ - protected String classNamespaceURI; - - /** - * The class attribute local name. - */ - protected String classLocalName; - - /** - * The id attribute namespace URI. - */ - protected String idNamespaceURI; - - /** - * The id attribute local name. - */ - protected String idLocalName; - - /** - * Creates a new condition factory. - */ - public CSSConditionFactoryImpl(String cns, String cln, String idns, - String idln) { - classNamespaceURI = cns; - classLocalName = cln; - idNamespaceURI = idns; - idLocalName = idln; - } - - /** - * SAC: Implements {@link - * ConditionFactory#createAndCondition(Condition,Condition)}. - */ - @Override - public CombinatorCondition createAndCondition(Condition first, - Condition second) throws CSSException { - return new CSSAndConditionImpl(first, second); - } - - /** - * SAC: Implements {@link - * ConditionFactory#createOrCondition(Condition,Condition)}. - */ - @Override - public CombinatorCondition createOrCondition(Condition first, - Condition second) throws CSSException { - throw new CSSException(NOT_IMPLEMENTED_IN_CSS2); - } - - /** - * SAC: Implements {@link - * org.w3c.css.sac.ConditionFactory#createNegativeCondition(Condition)}. - */ - @Override - public NegativeCondition createNegativeCondition(Condition condition) - throws CSSException { - throw new CSSException(NOT_IMPLEMENTED_IN_CSS2); - } - - /** - * SAC: Implements {@link - * ConditionFactory#createPositionalCondition(int,boolean,boolean)}. - */ - @Override - public PositionalCondition createPositionalCondition(int position, - boolean typeNode, boolean type) throws CSSException { - throw new CSSException(NOT_IMPLEMENTED_IN_CSS2); - } - - /** - * SAC: Implements {@link - * ConditionFactory#createAttributeCondition(String,String,boolean,String)}. - */ - @Override - public AttributeCondition createAttributeCondition(String localName, - String namespaceURI, boolean specified, String value) - throws CSSException { - return new CSSAttributeConditionImpl(localName, namespaceURI, specified, - value); - } - - /** - * SAC: Implements {@link - * org.w3c.css.sac.ConditionFactory#createIdCondition(String)}. - */ - @Override - public AttributeCondition createIdCondition(String value) - throws CSSException { - return new CSSIdConditionImpl(idNamespaceURI, idLocalName, value); - } - - /** - * SAC: Implements {@link - * org.w3c.css.sac.ConditionFactory#createLangCondition(String)}. - */ - @Override - public LangCondition createLangCondition(String lang) throws CSSException { - return new CSSLangConditionImpl(lang); - } - - /** - * SAC: Implements {@link - * ConditionFactory#createOneOfAttributeCondition(String,String,boolean,String)}. - */ - @Override - public AttributeCondition createOneOfAttributeCondition(String localName, - String nsURI, boolean specified, String value) throws CSSException { - return new CSSOneOfAttributeConditionImpl(localName, nsURI, specified, - value); - } - - /** - * SAC: Implements {@link - * ConditionFactory#createBeginHyphenAttributeCondition(String,String,boolean,String)}. - */ - @Override - public AttributeCondition createBeginHyphenAttributeCondition( - String localName, String namespaceURI, boolean specified, - String value) throws CSSException { - return new CSSBeginHyphenAttributeConditionImpl(localName, - namespaceURI, specified, value); - } - - /** - * SAC: Implements {@link - * org.w3c.css.sac.ConditionFactory#createClassCondition(String,String)}. - */ - @Override - public AttributeCondition createClassCondition(String namespaceURI, - String value) throws CSSException { - return new CSSClassConditionImpl(classLocalName, classNamespaceURI, value); - } - - /** - * SAC: Implements {@link - * ConditionFactory#createPseudoClassCondition(String,String)}. - */ - @Override - public AttributeCondition createPseudoClassCondition(String namespaceURI, - String value) throws CSSException { - return new CSSPseudoClassConditionImpl(namespaceURI, value); - } - - /** - * SAC: Implements {@link - * org.w3c.css.sac.ConditionFactory#createOnlyChildCondition()}. - */ - @Override - public Condition createOnlyChildCondition() throws CSSException { - throw new CSSException(NOT_IMPLEMENTED_IN_CSS2); - } - - /** - * SAC: Implements {@link - * org.w3c.css.sac.ConditionFactory#createOnlyTypeCondition()}. - */ - @Override - public Condition createOnlyTypeCondition() throws CSSException { - throw new CSSException(NOT_IMPLEMENTED_IN_CSS2); - } - - /** - * SAC: Implements {@link - * org.w3c.css.sac.ConditionFactory#createContentCondition(String)}. - */ - @Override - public ContentCondition createContentCondition(String data) - throws CSSException { - throw new CSSException(NOT_IMPLEMENTED_IN_CSS2); - } -} diff --git a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSConditionalSelectorImpl.java b/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSConditionalSelectorImpl.java deleted file mode 100644 index c4cc09ad9c1..00000000000 --- a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSConditionalSelectorImpl.java +++ /dev/null @@ -1,135 +0,0 @@ -/* - - Copyright 2002, 2015 The Apache Software Foundation - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - */ - -/* This class copied from org.apache.batik.css.engine.sac */ - -package org.eclipse.e4.ui.css.core.impl.sac; - -import java.util.Set; -import org.w3c.css.sac.Condition; -import org.w3c.css.sac.ConditionalSelector; -import org.w3c.css.sac.SimpleSelector; -import org.w3c.dom.Element; -import org.w3c.dom.Node; - -/** - * This class provides an implementation of the - * {@link org.w3c.css.sac.ConditionalSelector} interface. - */ -public class CSSConditionalSelectorImpl implements ConditionalSelector, ExtendedSelector { - - /** - * The simple selector. - */ - protected SimpleSelector simpleSelector; - - /** - * The condition. - */ - protected Condition condition; - - /** - * Creates a new ConditionalSelector object. - */ - public CSSConditionalSelectorImpl(SimpleSelector s, Condition c) { - simpleSelector = s; - condition = c; - } - - /** - * Indicates whether some other object is "equal to" this one. - * @param obj the reference object with which to compare. - */ - @Override - public boolean equals(Object obj) { - if (obj == null || (obj.getClass() != getClass())) { - return false; - } - CSSConditionalSelectorImpl s = (CSSConditionalSelectorImpl)obj; - return (s.simpleSelector.equals(simpleSelector) && - s.condition.equals(condition)); - } - - /** - * SAC: Implements {@link - * org.w3c.css.sac.Selector#getSelectorType()}. - */ - @Override - public short getSelectorType() { - return SAC_CONDITIONAL_SELECTOR; - } - - @Override - public boolean match(Element e, Node[] hierarchy, int parentIndex, String pseudoE) { - return ((ExtendedSelector)getSimpleSelector()).match(e, hierarchy, parentIndex, pseudoE) && - ((ExtendedCondition)getCondition()).match(e, pseudoE); - } - - /** - * Tests whether this selector matches the given element. - */ - @Override - public boolean match(Element e, String pseudoE) { - return ((ExtendedSelector)getSimpleSelector()).match(e, pseudoE) && - ((ExtendedCondition)getCondition()).match(e, pseudoE); - } - - /** - * Fills the given set with the attribute names found in this selector. - */ - @Override - public void fillAttributeSet(Set attrSet) { - ((ExtendedSelector)getSimpleSelector()).fillAttributeSet(attrSet); - ((ExtendedCondition)getCondition()).fillAttributeSet(attrSet); - } - - /** - * Returns the specificity of this selector. - */ - @Override - public int getSpecificity() { - return ((ExtendedSelector)getSimpleSelector()).getSpecificity() + - ((ExtendedCondition)getCondition()).getSpecificity(); - } - - /** - * SAC: Implements {@link - * org.w3c.css.sac.ConditionalSelector#getSimpleSelector()}. - */ - @Override - public SimpleSelector getSimpleSelector() { - return simpleSelector; - } - - /** - * SAC: Implements {@link - * org.w3c.css.sac.ConditionalSelector#getCondition()}. - */ - @Override - public Condition getCondition() { - return condition; - } - - /** - * Returns a representation of the selector. - */ - @Override - public String toString() { - return String.valueOf( simpleSelector ) + condition; - } -} diff --git a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSDescendantSelectorImpl.java b/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSDescendantSelectorImpl.java deleted file mode 100644 index 44f3568dfc3..00000000000 --- a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSDescendantSelectorImpl.java +++ /dev/null @@ -1,109 +0,0 @@ -/* - - Copyright 2002, 2015 The Apache Software Foundation - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - */ - -/* This class copied from org.apache.batik.css.engine.sac */ - -package org.eclipse.e4.ui.css.core.impl.sac; - -import java.util.Set; -import org.w3c.css.sac.Selector; -import org.w3c.css.sac.SimpleSelector; -import org.w3c.dom.Element; -import org.w3c.dom.Node; - -/** - * This class provides an implementation for the - * {@link org.w3c.css.sac.DescendantSelector} interface. - */ -public class CSSDescendantSelectorImpl extends AbstractDescendantSelector { - - /** - * Creates a new CSSDescendantSelector object. - */ - public CSSDescendantSelectorImpl(Selector ancestor, SimpleSelector simple) { - super(ancestor, simple); - } - - /** - * SAC: Implements {@link - * org.w3c.css.sac.Selector#getSelectorType()}. - */ - @Override - public short getSelectorType() { - return SAC_DESCENDANT_SELECTOR; - } - - /** - * Tests whether this selector matches the given element. - */ - @Override - public boolean match(Element e, Node[] hierarchy, int parentIndex, String pseudoE) { - ExtendedSelector p = (ExtendedSelector) getAncestorSelector(); - if (!((ExtendedSelector) getSimpleSelector()).match(e, hierarchy, parentIndex, pseudoE)) { - return false; - } - - if (hierarchy == null) { - return false; - } - - Node n; - int length = hierarchy.length; - for (int i = parentIndex; i < length; i++) { - n = hierarchy[i]; - if (n != null && n.getNodeType() == Node.ELEMENT_NODE - && p.match((Element) n, hierarchy, i + 1, null)) { - return true; - } - } - return false; - } - - /** - * Tests whether this selector matches the given element. - */ - @Override - public boolean match(Element e, String pseudoE) { - ExtendedSelector p = (ExtendedSelector) getAncestorSelector(); - if (!((ExtendedSelector) getSimpleSelector()).match(e, pseudoE)) { - return false; - } - for (Node n = e.getParentNode(); n != null; n = n.getParentNode()) { - if (n.getNodeType() == Node.ELEMENT_NODE && p.match((Element) n, null)) { - return true; - } - } - return false; - } - - /** - * Fills the given set with the attribute names found in this selector. - */ - @Override - public void fillAttributeSet(Set attrSet) { - ((ExtendedSelector)getSimpleSelector()).fillAttributeSet(attrSet); - } - - /** - * Returns a representation of the selector. - */ - @Override - public String toString() { - return getAncestorSelector() + " " + getSimpleSelector(); - } -} diff --git a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSDirectAdjacentSelectorImpl.java b/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSDirectAdjacentSelectorImpl.java deleted file mode 100644 index 73eea3eb684..00000000000 --- a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSDirectAdjacentSelectorImpl.java +++ /dev/null @@ -1,106 +0,0 @@ -/* - - Copyright 2002, 2015 The Apache Software Foundation - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - */ - -/* This class copied from org.apache.batik.css.engine.sac */ - -package org.eclipse.e4.ui.css.core.impl.sac; - -import java.util.Set; -import org.w3c.css.sac.Selector; -import org.w3c.css.sac.SimpleSelector; -import org.w3c.dom.Element; -import org.w3c.dom.Node; - -/** - * This class provides an implementation for the - * {@link org.w3c.css.sac.DescendantSelector} interface. - */ -public class CSSDirectAdjacentSelectorImpl extends AbstractSiblingSelector { - - /** - * Creates a new CSSDirectAdjacentSelector object. - */ - public CSSDirectAdjacentSelectorImpl(short type, Selector parent, SimpleSelector simple) { - super(type, parent, simple); - } - - /** - * SAC: Implements {@link - * org.w3c.css.sac.Selector#getSelectorType()}. - */ - @Override - public short getSelectorType() { - return SAC_DIRECT_ADJACENT_SELECTOR; - } - - /** - * Tests whether this selector matches the given element. - */ - @Override - public boolean match(Element e, Node[] hiearchy, int parentIndex, String pseudoE) { - Node n = e; - if (!((ExtendedSelector) getSiblingSelector()).match(e, hiearchy, parentIndex, pseudoE)) { - return false; - } - - while ((n = n.getPreviousSibling()) != null && n.getNodeType() != Node.ELEMENT_NODE) { - } - - if (n == null) { - return false; - } - - return ((ExtendedSelector) getSelector()).match((Element) n, hiearchy, parentIndex, null); - } - - /** - * Tests whether this selector matches the given element. - */ - @Override - public boolean match(Element e, String pseudoE) { - Node n = e; - if (!((ExtendedSelector)getSiblingSelector()).match(e, pseudoE)) { - return false; - } - while ((n = n.getPreviousSibling()) != null && n.getNodeType() != Node.ELEMENT_NODE) { - } - - if (n == null) { - return false; - } - - return ((ExtendedSelector)getSelector()).match((Element)n, null); - } - - /** - * Fills the given set with the attribute names found in this selector. - */ - @Override - public void fillAttributeSet(Set attrSet) { - ((ExtendedSelector)getSelector()).fillAttributeSet(attrSet); - ((ExtendedSelector)getSiblingSelector()).fillAttributeSet(attrSet); - } - - /** - * Returns a representation of the selector. - */ - @Override - public String toString() { - return getSelector() + " + " + getSiblingSelector(); - } -} diff --git a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSDocumentHandlerImpl.java b/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSDocumentHandlerImpl.java index 150c95c4b7a..9456b6a94e2 100644 --- a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSDocumentHandlerImpl.java +++ b/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSDocumentHandlerImpl.java @@ -25,6 +25,7 @@ import org.eclipse.e4.ui.css.core.impl.dom.CSSUnknownRuleImpl; import org.eclipse.e4.ui.css.core.impl.dom.CSSValueFactory; import org.eclipse.e4.ui.css.core.impl.dom.MediaListImpl; +import org.eclipse.e4.ui.css.core.impl.engine.selector.SacTranslator; import org.eclipse.e4.ui.css.core.sac.ExtendedDocumentHandler; import org.w3c.css.sac.CSSException; import org.w3c.css.sac.InputSource; @@ -165,9 +166,10 @@ public void endFontFace() throws CSSException { @Override public void startSelector(SelectorList selectors) throws CSSException { - // Create the style rule and add it to the rule list + // Translate the SAC selector list into the engine's internal AST at + // this boundary so nothing downstream needs to touch SAC types. CSSStyleRuleImpl rule = new CSSStyleRuleImpl(parentStyleSheet, null, - selectors); + SacTranslator.translate(selectors)); if (!getNodeStack().empty()) { ((CSSRuleListImpl) getNodeStack().peek()).add(rule); } diff --git a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSElementSelectorImpl.java b/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSElementSelectorImpl.java deleted file mode 100644 index f24e1aeea7f..00000000000 --- a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSElementSelectorImpl.java +++ /dev/null @@ -1,98 +0,0 @@ -/* - - Copyright 2002, 2014 The Apache Software Foundation - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - */ - -/* This class copied from org.apache.batik.css.engine.sac */ - -package org.eclipse.e4.ui.css.core.impl.sac; - -import org.w3c.dom.Element; - -/** - * This class implements the {@link org.w3c.css.sac.ElementSelector} interface. - */ -public class CSSElementSelectorImpl extends AbstractElementSelector { - - /** - * Creates a new ElementSelector object. - */ - public CSSElementSelectorImpl(String uri, String name) { - super(uri, name); - } - - /** - * SAC: Implements {@link - * org.w3c.css.sac.Selector#getSelectorType()}. - */ - @Override - public short getSelectorType() { - return SAC_ELEMENT_NODE_SELECTOR; - } - - /** - * Tests whether this selector matches the given element. - */ - @Override - public boolean match(Element e, String pseudoE) { - String name = getLocalName(); - if (name == null) { - if (namespaceURI != null) { - return namespaceURI.equals(e.getNamespaceURI()); - } else { - return true; - } - } - String eName; - if (e.getPrefix() == null) { - eName = e.getNodeName(); - } else { - eName = e.getLocalName(); - } - // According to CSS 2 section 5.1 element - // names in selectors are case-sensitive for XML. - if (eName.equals(name)) { - if (namespaceURI != null) { - return namespaceURI.equals(e.getNamespaceURI()); - } else { - return true; - } - } - return false; - // For HTML - // return eName.equalsIgnoreCase(name); - } - - /** - * Returns the specificity of this selector. - */ - @Override - public int getSpecificity() { - return (getLocalName() == null) ? 0 : 1; - } - - /** - * Returns a representation of the selector. - */ - @Override - public String toString() { - String name = getLocalName(); - if (name == null) { - return "*"; - } - return name; - } -} diff --git a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSIdConditionImpl.java b/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSIdConditionImpl.java deleted file mode 100644 index eec162ea03d..00000000000 --- a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSIdConditionImpl.java +++ /dev/null @@ -1,129 +0,0 @@ -/* - - Copyright 2002, 2015 The Apache Software Foundation - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - */ - -/* This class copied from org.apache.batik.css.engine.sac */ - -package org.eclipse.e4.ui.css.core.impl.sac; - -import java.util.Set; -import org.eclipse.e4.ui.css.core.dom.CSSStylableElement; -import org.w3c.dom.Element; - -/** - * This class provides an implementation of the - * {@link org.w3c.css.sac.AttributeCondition} interface. - */ -public class CSSIdConditionImpl extends AbstractAttributeCondition { - - /** - * The id attribute namespace URI. - */ - protected String namespaceURI; - - /** - * The id attribute local name. - */ - protected String localName; - - /** - * Creates a new CSSAttributeCondition object. - */ - public CSSIdConditionImpl(String ns, String ln, String value) { - super(value); - namespaceURI = ns; - localName = ln; - } - - /** - * SAC: Implements {@link - * org.w3c.css.sac.Condition#getConditionType()}. - */ - @Override - public short getConditionType() { - return SAC_ID_CONDITION; - } - - /** - * SAC: Implements {@link - * org.w3c.css.sac.AttributeCondition#getNamespaceURI()}. - */ - @Override - public String getNamespaceURI() { - return namespaceURI; - } - - /** - * SAC: Implements {@link - * org.w3c.css.sac.AttributeCondition#getLocalName()}. - */ - @Override - public String getLocalName() { - return localName; - } - - /** - * SAC: Implements {@link - * org.w3c.css.sac.AttributeCondition#getSpecified()}. - */ - @Override - public boolean getSpecified() { - return true; - } - - /** - * Tests whether this condition matches the given element. - */ - @Override - public boolean match(Element e, String pseudoE) { - String id = null; - if (e instanceof CSSStylableElement) { - id = ((CSSStylableElement) e).getCSSId(); - } else { - id = e.getAttribute("id"); - } - if (id == null) { - return false; - } - return id.equals(getValue()); - // return super.match(e, pseudoE); - } - - /** - * Fills the given set with the attribute names found in this selector. - */ - @Override - public void fillAttributeSet(Set attrSet) { - attrSet.add(localName); - } - - /** - * Returns the specificity of this condition. - */ - @Override - public int getSpecificity() { - return 1 << 16; - } - - /** - * Returns a text representation of this object. - */ - @Override - public String toString() { - return '#' + getValue(); - } -} diff --git a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSLangConditionImpl.java b/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSLangConditionImpl.java deleted file mode 100644 index 4789469f7fa..00000000000 --- a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSLangConditionImpl.java +++ /dev/null @@ -1,117 +0,0 @@ -/* - - Copyright 2002, 2015 The Apache Software Foundation - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - */ - -/* This class copied from org.apache.batik.css.engine.sac */ - -package org.eclipse.e4.ui.css.core.impl.sac; - -import java.util.Set; -import org.w3c.css.sac.LangCondition; -import org.w3c.dom.Element; - -/** - * This class provides an implementation of the - * {@link org.w3c.css.sac.LangCondition} interface. - */ -public class CSSLangConditionImpl implements LangCondition, ExtendedCondition { - /** - * The language. - */ - protected String lang; - - /** - * The language with a hyphen suffixed. - */ - protected String langHyphen; - - /** - * Creates a new LangCondition object. - */ - public CSSLangConditionImpl(String lang) { - this.lang = lang.toLowerCase(); - this.langHyphen = lang + '-'; - } - - /** - * Indicates whether some other object is "equal to" this one. - * @param obj the reference object with which to compare. - */ - @Override - public boolean equals(Object obj) { - if (obj == null || (obj.getClass() != getClass())) { - return false; - } - CSSLangConditionImpl c = (CSSLangConditionImpl)obj; - return c.lang.equals(lang); - } - - /** - * SAC: Implements {@link - * org.w3c.css.sac.Condition#getConditionType()}. - */ - @Override - public short getConditionType() { - return SAC_LANG_CONDITION; - } - - /** - * SAC: Implements {@link org.w3c.css.sac.LangCondition#getLang()}. - */ - @Override - public String getLang() { - return lang; - } - - /** - * Returns the specificity of this condition. - */ - @Override - public int getSpecificity() { - return 1 << 8; - } - - /** - * Tests whether this condition matches the given element. - */ - @Override - public boolean match(Element e, String pseudoE) { - String s = e.getAttribute("lang").toLowerCase(); - if (s.equals(lang) || s.startsWith(langHyphen)) { - return true; - } - // s = e.getAttributeNS(XMLConstants.XML_NAMESPACE_URI, - // XMLConstants.XML_LANG_ATTRIBUTE).toLowerCase(); - return s.equals(lang) || s.startsWith(langHyphen); - } - - /** - * Fills the given set with the attribute names found in this selector. - */ - @Override - public void fillAttributeSet(Set attrSet) { - attrSet.add("lang"); - } - - /** - * Returns a text representation of this object. - */ - @Override - public String toString() { - return ":lang(" + lang + ')'; - } -} diff --git a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSOneOfAttributeConditionImpl.java b/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSOneOfAttributeConditionImpl.java deleted file mode 100644 index fb4ebbd0428..00000000000 --- a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSOneOfAttributeConditionImpl.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - - Copyright 2002, 2015 The Apache Software Foundation - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - */ - -/******************************************************************************* - * Contributors: - * This class was copied from org.apache.batik.css.engine.sac - * Apache Batik project - initial API and implementation - * Alain Le Guennec - Bug 458334 - *******************************************************************************/ -package org.eclipse.e4.ui.css.core.impl.sac; - -import java.util.StringTokenizer; -import org.w3c.dom.Element; - -/** - * This class provides an implementation of the - * {@link org.w3c.css.sac.AttributeCondition} interface. - */ -public class CSSOneOfAttributeConditionImpl extends CSSAttributeConditionImpl { - /** - * Creates a new CSSAttributeCondition object. - */ - public CSSOneOfAttributeConditionImpl(String localName, - String namespaceURI, boolean specified, String value) { - super(localName, namespaceURI, specified, value); - } - - /** - * SAC: Implements {@link - * org.w3c.css.sac.Condition#getConditionType()}. - */ - @Override - public short getConditionType() { - return SAC_ONE_OF_ATTRIBUTE_CONDITION; - } - - /** - * Tests whether this condition matches the given element. - */ - @Override - public boolean match(Element e, String pseudoE) { - String attr = e.getAttribute(getLocalName()); - String val = getValue(); - for (StringTokenizer tok = new StringTokenizer(attr); tok.hasMoreElements();) { - String candidate = tok.nextToken(); - if (val.equals(candidate)) { - return true; - } - } - return false; - } - - /** - * Returns a text representation of this object. - */ - @Override - public String toString() { - return "[" + getLocalName() + "~=\"" + getValue() + "\"]"; - } -} diff --git a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSPseudoClassConditionImpl.java b/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSPseudoClassConditionImpl.java deleted file mode 100644 index 1c459d6637b..00000000000 --- a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSPseudoClassConditionImpl.java +++ /dev/null @@ -1,145 +0,0 @@ -/* - - Copyright 2002, 2015 The Apache Software Foundation - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - */ - -/* This class copied from org.apache.batik.css.engine.sac */ - -package org.eclipse.e4.ui.css.core.impl.sac; - -import java.util.Set; -import org.eclipse.e4.ui.css.core.dom.CSSStylableElement; -import org.w3c.dom.Element; - -/** - * This class provides an implementation of the - * {@link org.w3c.css.sac.AttributeCondition} interface. - */ -public class CSSPseudoClassConditionImpl extends AbstractAttributeCondition { - /** - * The namespaceURI. - */ - protected String namespaceURI; - - /** - * Creates a new CSSAttributeCondition object. - */ - public CSSPseudoClassConditionImpl(String namespaceURI, String value) { - super(value); - this.namespaceURI = namespaceURI; - } - - /** - * Indicates whether some other object is "equal to" this one. - * - * @param obj - * the reference object with which to compare. - */ - @Override - public boolean equals(Object obj) { - if (!super.equals(obj)) { - return false; - } - CSSPseudoClassConditionImpl c = (CSSPseudoClassConditionImpl) obj; - return c.namespaceURI.equals(namespaceURI); - } - - /** - * equal objects should have equal hashCodes. - * - * @return hashCode of this CSSPseudoClassCondition - */ - @Override - public int hashCode() { - return namespaceURI.hashCode(); - } - - /** - * SAC: Implements {@link - * org.w3c.css.sac.Condition#getConditionType()}. - */ - @Override - public short getConditionType() { - return SAC_PSEUDO_CLASS_CONDITION; - } - - /** - * SAC: Implements {@link - * org.w3c.css.sac.AttributeCondition#getNamespaceURI()}. - */ - @Override - public String getNamespaceURI() { - return namespaceURI; - } - - /** - * SAC: Implements {@link - * org.w3c.css.sac.AttributeCondition#getLocalName()}. - */ - @Override - public String getLocalName() { - return null; - } - - /** - * SAC: Implements {@link - * org.w3c.css.sac.AttributeCondition#getSpecified()}. - */ - @Override - public boolean getSpecified() { - return false; - } - - /** - * Tests whether this selector matches the given element. - */ - @Override - public boolean match(Element e, String pseudoE) { - if (pseudoE != null && !pseudoE.equals(getValue())) { - // pseudo instance is filled, it is not valid. - return false; - } - if (!(e instanceof CSSStylableElement element)) { - return false; - } - boolean isPseudoInstanceOf = element.isPseudoInstanceOf(getValue()); - if (!isPseudoInstanceOf) { - return false; - } - if (pseudoE == null) { - // pseudo element is not filled. - // test if this CSSPseudoClassCondition is NOT a static pseudo - // instance - return (!element.isStaticPseudoInstance(getValue())); - } - return true; - } - - /** - * Fills the given set with the attribute names found in this selector. - */ - @Override - public void fillAttributeSet(Set attrSet) { - } - - /** - * Returns a text representation of this object. - */ - @Override - public String toString() { - return ":" + getValue(); - } -} diff --git a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSPseudoElementSelectorImpl.java b/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSPseudoElementSelectorImpl.java deleted file mode 100644 index 5353e2c4885..00000000000 --- a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSPseudoElementSelectorImpl.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - - Copyright 2002, 2014 The Apache Software Foundation - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - */ - -/* This class copied from org.apache.batik.css.engine.sac */ - -package org.eclipse.e4.ui.css.core.impl.sac; - -import org.w3c.dom.Element; - -/** - * This class implements the {@link org.w3c.css.sac.ElementSelector} interface. - */ -public class CSSPseudoElementSelectorImpl extends AbstractElementSelector { - - /** - * Creates a new CSSPseudoElementSelector object. - */ - public CSSPseudoElementSelectorImpl(String uri, String name) { - super(uri, name); - } - - /** - * SAC: Implements {@link - * org.w3c.css.sac.Selector#getSelectorType()}. - */ - @Override - public short getSelectorType() { - return SAC_PSEUDO_ELEMENT_SELECTOR; - } - - /** - * Tests whether this selector matches the given element. - */ - @Override - public boolean match(Element e, String pseudoE) { - return getLocalName().equalsIgnoreCase(pseudoE); - } - - /** - * Returns the specificity of this selector. - */ - @Override - public int getSpecificity() { - return 0; - } - - /** - * Returns a representation of the selector. - */ - @Override - public String toString() { - return ":" + getLocalName(); - } -} diff --git a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSSelectorFactoryImpl.java b/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSSelectorFactoryImpl.java deleted file mode 100644 index 00370115317..00000000000 --- a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSSelectorFactoryImpl.java +++ /dev/null @@ -1,182 +0,0 @@ -/* - - Copyright 2002, 2014 The Apache Software Foundation - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - */ - -/* This class copied from org.apache.batik.css.engine.sac */ - -package org.eclipse.e4.ui.css.core.impl.sac; - -import org.w3c.css.sac.CSSException; -import org.w3c.css.sac.CharacterDataSelector; -import org.w3c.css.sac.Condition; -import org.w3c.css.sac.ConditionalSelector; -import org.w3c.css.sac.DescendantSelector; -import org.w3c.css.sac.ElementSelector; -import org.w3c.css.sac.NegativeSelector; -import org.w3c.css.sac.ProcessingInstructionSelector; -import org.w3c.css.sac.Selector; -import org.w3c.css.sac.SelectorFactory; -import org.w3c.css.sac.SiblingSelector; -import org.w3c.css.sac.SimpleSelector; - -/** - * This class implements the {@link org.w3c.css.sac.SelectorFactory} interface. - */ -public class CSSSelectorFactoryImpl implements SelectorFactory { - - private static final String NOT_IMPLEMENTED_IN_CSS2 = "Not implemented in CSS2"; //$NON-NLS-1$ - - /** - * The instance of this class. - */ - public static final SelectorFactory INSTANCE = new CSSSelectorFactoryImpl(); - - /** - * This class does not need to be instantiated. - */ - protected CSSSelectorFactoryImpl() { - } - - /** - * SAC: Implements {@link - * SelectorFactory#createConditionalSelector(SimpleSelector,Condition)}. - */ - @Override - public ConditionalSelector createConditionalSelector( - SimpleSelector selector, Condition condition) throws CSSException { - return new CSSConditionalSelectorImpl(selector, condition); - } - - /** - * SAC: Implements {@link - * org.w3c.css.sac.SelectorFactory#createAnyNodeSelector()}. - */ - @Override - public SimpleSelector createAnyNodeSelector() throws CSSException { - throw new CSSException(NOT_IMPLEMENTED_IN_CSS2); - } - - /** - * SAC: Implements {@link - * org.w3c.css.sac.SelectorFactory#createRootNodeSelector()}. - */ - @Override - public SimpleSelector createRootNodeSelector() throws CSSException { - throw new CSSException(NOT_IMPLEMENTED_IN_CSS2); - } - - /** - * SAC: Implements {@link - * org.w3c.css.sac.SelectorFactory#createNegativeSelector(SimpleSelector)}. - */ - @Override - public NegativeSelector createNegativeSelector(SimpleSelector selector) - throws CSSException { - throw new CSSException(NOT_IMPLEMENTED_IN_CSS2); - } - - /** - * SAC: Implements {@link - * org.w3c.css.sac.SelectorFactory#createElementSelector(String,String)}. - */ - @Override - public ElementSelector createElementSelector(String namespaceURI, - String tagName) throws CSSException { - return new CSSElementSelectorImpl(namespaceURI, tagName); - } - - /** - * SAC: Implements {@link - * org.w3c.css.sac.SelectorFactory#createTextNodeSelector(String)}. - */ - @Override - public CharacterDataSelector createTextNodeSelector(String data) - throws CSSException { - throw new CSSException(NOT_IMPLEMENTED_IN_CSS2); - } - - /** - * SAC: Implements {@link - * org.w3c.css.sac.SelectorFactory#createCDataSectionSelector(String)}. - */ - @Override - public CharacterDataSelector createCDataSectionSelector(String data) - throws CSSException { - throw new CSSException(NOT_IMPLEMENTED_IN_CSS2); - } - - /** - * SAC: Implements {@link - * SelectorFactory#createProcessingInstructionSelector(String,String)}. - */ - @Override - public ProcessingInstructionSelector createProcessingInstructionSelector( - String target, String data) throws CSSException { - throw new CSSException(NOT_IMPLEMENTED_IN_CSS2); - } - - /** - * SAC: Implements {@link - * org.w3c.css.sac.SelectorFactory#createCommentSelector(String)}. - */ - @Override - public CharacterDataSelector createCommentSelector(String data) - throws CSSException { - throw new CSSException(NOT_IMPLEMENTED_IN_CSS2); - } - - /** - * SAC: Implements {@link - * SelectorFactory#createPseudoElementSelector(String,String)}. - */ - @Override - public ElementSelector createPseudoElementSelector(String namespaceURI, - String pseudoName) throws CSSException { - return new CSSPseudoElementSelectorImpl(namespaceURI, pseudoName); - } - - /** - * SAC: Implements {@link - * SelectorFactory#createDescendantSelector(Selector,SimpleSelector)}. - */ - @Override - public DescendantSelector createDescendantSelector(Selector parent, - SimpleSelector descendant) throws CSSException { - return new CSSDescendantSelectorImpl(parent, descendant); - } - - /** - * SAC: Implements {@link - * SelectorFactory#createChildSelector(Selector,SimpleSelector)}. - */ - @Override - public DescendantSelector createChildSelector(Selector parent, - SimpleSelector child) throws CSSException { - return new CSSChildSelectorImpl(parent, child); - } - - /** - * SAC: Implements {@link - * SelectorFactory#createDirectAdjacentSelector(short,Selector,SimpleSelector)}. - */ - @Override - public SiblingSelector createDirectAdjacentSelector(short nodeType, - Selector child, SimpleSelector directAdjacent) throws CSSException { - return new CSSDirectAdjacentSelectorImpl(nodeType, child, - directAdjacent); - } -} diff --git a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/ExtendedCondition.java b/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/ExtendedCondition.java deleted file mode 100644 index edc785b6760..00000000000 --- a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/ExtendedCondition.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - - Copyright 2002, 2015 The Apache Software Foundation - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - */ - -/* This class copied from org.apache.batik.css.engine.sac */ - -package org.eclipse.e4.ui.css.core.impl.sac; - -import java.util.Set; -import org.w3c.css.sac.Condition; -import org.w3c.dom.Element; - -/** - * This interface provides additional features to the - * {@link org.w3c.css.sac.Condition} interface. - */ -public interface ExtendedCondition extends Condition { - - /** - * Tests whether this condition matches the given element. - */ - boolean match(Element e, String pseudoE); - - /** - * Returns the specificity of this condition. - */ - int getSpecificity(); - - /** - * Fills the given set with the attribute names found in this selector. - */ - void fillAttributeSet(Set attrSet); -} diff --git a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/ExtendedSelector.java b/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/ExtendedSelector.java deleted file mode 100644 index b30db5d0db1..00000000000 --- a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/ExtendedSelector.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - - Copyright 2002, 2015 The Apache Software Foundation - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - */ - -/* This class copied from org.apache.batik.css.engine.sac */ - -package org.eclipse.e4.ui.css.core.impl.sac; - -import java.util.Set; -import org.w3c.css.sac.Selector; -import org.w3c.dom.Element; -import org.w3c.dom.Node; - -/** - * This interface extends the {@link org.w3c.css.sac.Selector}. - */ -public interface ExtendedSelector extends Selector { - - default boolean match(Element e, Node[] ancestors, int parentIndex, String pseudoE) { - return match(e, pseudoE); - } - - /** - * Tests whether this selector matches the given element. - */ - boolean match(Element e, String pseudoE); - - /** - * Returns the specificity of this selector. - */ - int getSpecificity(); - - /** - * Fills the given set with the attribute names found in this selector. - */ - void fillAttributeSet(Set attrSet); -} diff --git a/bundles/org.eclipse.e4.ui.css.swt/src/org/eclipse/e4/ui/css/swt/engine/AbstractCSSSWTEngineImpl.java b/bundles/org.eclipse.e4.ui.css.swt/src/org/eclipse/e4/ui/css/swt/engine/AbstractCSSSWTEngineImpl.java deleted file mode 100644 index cb81d021c2d..00000000000 --- a/bundles/org.eclipse.e4.ui.css.swt/src/org/eclipse/e4/ui/css/swt/engine/AbstractCSSSWTEngineImpl.java +++ /dev/null @@ -1,141 +0,0 @@ -/******************************************************************************* - * Copyright (c) 2008, 2014 Angelo Zerr and others. - * - * This program and the accompanying materials - * are made available under the terms of the Eclipse Public License 2.0 - * which accompanies this distribution, and is available at - * https://www.eclipse.org/legal/epl-2.0/ - * - * SPDX-License-Identifier: EPL-2.0 - * - * Contributors: - * Angelo Zerr - initial API and implementation - * IBM Corporation - *******************************************************************************/ -package org.eclipse.e4.ui.css.swt.engine; - -import org.eclipse.e4.ui.css.core.dom.CSSStylableElement; -import org.eclipse.e4.ui.css.core.engine.CSSElementContext; -import org.eclipse.e4.ui.css.core.impl.engine.CSSEngineImpl; -import org.eclipse.e4.ui.css.core.resources.IResourcesRegistry; -import org.eclipse.e4.ui.css.swt.dom.WidgetElement; -import org.eclipse.e4.ui.css.swt.properties.converters.CSSValueSWTColorConverterImpl; -import org.eclipse.e4.ui.css.swt.properties.converters.CSSValueSWTCursorConverterImpl; -import org.eclipse.e4.ui.css.swt.properties.converters.CSSValueSWTFontConverterImpl; -import org.eclipse.e4.ui.css.swt.properties.converters.CSSValueSWTFontDataConverterImpl; -import org.eclipse.e4.ui.css.swt.properties.converters.CSSValueSWTGradientConverterImpl; -import org.eclipse.e4.ui.css.swt.properties.converters.CSSValueSWTImageConverterImpl; -import org.eclipse.e4.ui.css.swt.properties.converters.CSSValueSWTRGBConverterImpl; -import org.eclipse.e4.ui.css.swt.resources.SWTResourceRegistryKeyFactory; -import org.eclipse.e4.ui.css.swt.resources.SWTResourcesRegistry; -import org.eclipse.swt.widgets.Display; -import org.eclipse.swt.widgets.Widget; -import org.w3c.dom.Element; - -/** - * CSS SWT Engine implementation which configure CSSEngineImpl to apply styles - * to SWT widgets. - */ -public abstract class AbstractCSSSWTEngineImpl extends CSSEngineImpl { - - protected Display display; - - public AbstractCSSSWTEngineImpl(Display display) { - this(display, false); - } - - public AbstractCSSSWTEngineImpl(Display display, boolean lazyApplyingStyles) { - this.display = display; - - /** Initialize SWT CSSValue converter * */ - - // Register SWT RGB CSSValue Converter - super.registerCSSValueConverter(CSSValueSWTRGBConverterImpl.INSTANCE); - // Register SWT Color CSSValue Converter - super.registerCSSValueConverter(CSSValueSWTColorConverterImpl.INSTANCE); - // Register SWT Gradient CSSValue Converter - super.registerCSSValueConverter(CSSValueSWTGradientConverterImpl.INSTANCE); - // Register SWT Cursor CSSValue Converter - super.registerCSSValueConverter(CSSValueSWTCursorConverterImpl.INSTANCE); - // Register SWT Font CSSValue Converter - super.registerCSSValueConverter(CSSValueSWTFontConverterImpl.INSTANCE); - // Register SWT FontData CSSValue Converter - super.registerCSSValueConverter(CSSValueSWTFontDataConverterImpl.INSTANCE); - // Register SWT Image CSSValue Converter - super.registerCSSValueConverter(CSSValueSWTImageConverterImpl.INSTANCE); - - if (lazyApplyingStyles) { - new CSSSWTApplyStylesListener(display, this); - } - - initializeCSSElementProvider(); - initializeCSSPropertyHandlers(); - - setResourceRegistryKeyFactory(new SWTResourceRegistryKeyFactory()); - } - - protected abstract void initializeCSSPropertyHandlers(); - - protected abstract void initializeCSSElementProvider(); - - @Override - public IResourcesRegistry getResourcesRegistry() { - IResourcesRegistry resourcesRegistry = super.getResourcesRegistry(); - if (resourcesRegistry == null) { - super.setResourcesRegistry(new SWTResourcesRegistry(display)); - } - return super.getResourcesRegistry(); - } - - @Override - public Element getElement(Object element) { - if (element instanceof CSSStylableElement - && ((CSSStylableElement) element).getNativeWidget() instanceof Widget) { - return (CSSStylableElement) element; - } else if (element instanceof Widget) { - if (isStylable((Widget) element)) { - return super.getElement(element); - } - } else { - // FIXME: we need to pass through the ThemeElementDefinitions; - // perhaps they should be handled by a separate engine - return super.getElement(element); - } - return null; - } - - /** - * Return true if the given widget can be styled - * - * @param widget - * the widget - * @return true if the widget can be styled - */ - protected boolean isStylable(Widget widget) { - // allows widgets to be selectively excluded from styling - return !widget.isDisposed() - && !Boolean.TRUE.equals(widget.getData("org.eclipse.e4.ui.css.disabled")); //$NON-NLS-1$ - } - - @Override - public void reset() { - for (CSSElementContext elementContext : getElementsContext().values()) { - Element element = elementContext.getElement(); - if (element instanceof WidgetElement - && isApplicableToReset((WidgetElement) element)) { - ((WidgetElement) element).reset(); - } - } - - getResourcesRegistry().dispose(); - super.reset(); - } - - private boolean isApplicableToReset(WidgetElement element) { - if (element.getNativeWidget() instanceof Widget) { - return !((Widget) element.getNativeWidget()).isDisposed(); - } - return false; - } - -} diff --git a/bundles/org.eclipse.e4.ui.css.swt/src/org/eclipse/e4/ui/css/swt/engine/CSSSWTEngineImpl.java b/bundles/org.eclipse.e4.ui.css.swt/src/org/eclipse/e4/ui/css/swt/engine/CSSSWTEngineImpl.java index c406a848ea6..01da9199385 100644 --- a/bundles/org.eclipse.e4.ui.css.swt/src/org/eclipse/e4/ui/css/swt/engine/CSSSWTEngineImpl.java +++ b/bundles/org.eclipse.e4.ui.css.swt/src/org/eclipse/e4/ui/css/swt/engine/CSSSWTEngineImpl.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2008, 2019 Angelo Zerr and others. + * Copyright (c) 2008, 2026 Angelo Zerr and others. * * This program and the accompanying materials * are made available under the terms of the Eclipse Public License 2.0 @@ -16,34 +16,63 @@ import org.eclipse.core.runtime.ILog; import org.eclipse.core.runtime.RegistryFactory; +import org.eclipse.e4.ui.css.core.dom.CSSStylableElement; +import org.eclipse.e4.ui.css.core.engine.CSSElementContext; +import org.eclipse.e4.ui.css.core.impl.engine.CSSEngineImpl; import org.eclipse.e4.ui.css.core.impl.engine.RegistryCSSElementProvider; import org.eclipse.e4.ui.css.core.impl.engine.RegistryCSSPropertyHandlerProvider; +import org.eclipse.e4.ui.css.core.resources.IResourcesRegistry; +import org.eclipse.e4.ui.css.swt.dom.WidgetElement; +import org.eclipse.e4.ui.css.swt.properties.converters.CSSValueSWTColorConverterImpl; +import org.eclipse.e4.ui.css.swt.properties.converters.CSSValueSWTCursorConverterImpl; +import org.eclipse.e4.ui.css.swt.properties.converters.CSSValueSWTFontConverterImpl; +import org.eclipse.e4.ui.css.swt.properties.converters.CSSValueSWTFontDataConverterImpl; +import org.eclipse.e4.ui.css.swt.properties.converters.CSSValueSWTGradientConverterImpl; +import org.eclipse.e4.ui.css.swt.properties.converters.CSSValueSWTImageConverterImpl; +import org.eclipse.e4.ui.css.swt.properties.converters.CSSValueSWTRGBConverterImpl; +import org.eclipse.e4.ui.css.swt.resources.SWTResourceRegistryKeyFactory; +import org.eclipse.e4.ui.css.swt.resources.SWTResourcesRegistry; import org.eclipse.swt.SWT; import org.eclipse.swt.events.DisposeListener; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Shell; import org.eclipse.swt.widgets.Widget; +import org.w3c.dom.Element; /** - * CSS SWT Engine implementation which configure CSSEngineImpl to apply styles - * to SWT widgets with static handler strategy. + * CSS SWT Engine. Configures {@link CSSEngineImpl} with the SWT-specific + * value converters and the registry-driven element + property handler + * providers, and applies styles to SWT widgets. */ -public class CSSSWTEngineImpl extends AbstractCSSSWTEngineImpl { +public class CSSSWTEngineImpl extends CSSEngineImpl { - private DisposeListener disposeListener; + protected Display display; + + private final DisposeListener disposeListener = e -> handleWidgetDisposed(e.widget); public CSSSWTEngineImpl(Display display) { - super(display); - init(); + this(display, false); } public CSSSWTEngineImpl(Display display, boolean lazyApplyingStyles) { - super(display, lazyApplyingStyles); - init(); - } + this.display = display; - private void init() { - disposeListener = e -> handleWidgetDisposed(e.widget); + registerCSSValueConverter(CSSValueSWTRGBConverterImpl.INSTANCE); + registerCSSValueConverter(CSSValueSWTColorConverterImpl.INSTANCE); + registerCSSValueConverter(CSSValueSWTGradientConverterImpl.INSTANCE); + registerCSSValueConverter(CSSValueSWTCursorConverterImpl.INSTANCE); + registerCSSValueConverter(CSSValueSWTFontConverterImpl.INSTANCE); + registerCSSValueConverter(CSSValueSWTFontDataConverterImpl.INSTANCE); + registerCSSValueConverter(CSSValueSWTImageConverterImpl.INSTANCE); + + if (lazyApplyingStyles) { + new CSSSWTApplyStylesListener(display, this); + } + + setElementProvider(new RegistryCSSElementProvider(RegistryFactory.getRegistry())); + propertyHandlerProviders.add(new RegistryCSSPropertyHandlerProvider(RegistryFactory.getRegistry())); + + setResourceRegistryKeyFactory(new SWTResourceRegistryKeyFactory()); } @Override @@ -54,29 +83,90 @@ protected void hookNativeWidget(Object widget) { } @Override - protected void initializeCSSPropertyHandlers() { - propertyHandlerProviders.add(new RegistryCSSPropertyHandlerProvider(RegistryFactory.getRegistry())); + public IResourcesRegistry getResourcesRegistry() { + IResourcesRegistry resourcesRegistry = super.getResourcesRegistry(); + if (resourcesRegistry == null) { + super.setResourcesRegistry(new SWTResourcesRegistry(display)); + } + return super.getResourcesRegistry(); } @Override - protected void initializeCSSElementProvider() { - setElementProvider(new RegistryCSSElementProvider(RegistryFactory.getRegistry())); + public Element getElement(Object element) { + if (element instanceof CSSStylableElement + && ((CSSStylableElement) element).getNativeWidget() instanceof Widget) { + return (CSSStylableElement) element; + } else if (element instanceof Widget) { + if (isStylable((Widget) element)) { + return super.getElement(element); + } + } else { + // FIXME: we need to pass through the ThemeElementDefinitions; + // perhaps they should be handled by a separate engine + return super.getElement(element); + } + return null; + } + + /** + * Return true if the given widget can be styled. + */ + protected boolean isStylable(Widget widget) { + return !widget.isDisposed() + && !Boolean.TRUE.equals(widget.getData("org.eclipse.e4.ui.css.disabled")); //$NON-NLS-1$ } @Override - public void reapply() { - Shell[] shells = display.getShells(); - for (Shell s : shells) { - try { - s.setRedraw(false); - s.reskin(SWT.ALL); - applyStyles(s, true); - } catch (Exception e) { - ILog.of(getClass()).error(e.getMessage(), e); - } finally { - s.setRedraw(true); + public void reset() { + for (CSSElementContext elementContext : getElementsContext().values()) { + Element element = elementContext.getElement(); + if (element instanceof WidgetElement + && isApplicableToReset((WidgetElement) element)) { + ((WidgetElement) element).reset(); } } + + getResourcesRegistry().dispose(); + super.reset(); + } + + private boolean isApplicableToReset(WidgetElement element) { + if (element.getNativeWidget() instanceof Widget) { + return !((Widget) element.getNativeWidget()).isDisposed(); + } + return false; } + @Override + public void reapply() { + final boolean trace = org.eclipse.e4.ui.css.core.impl.engine.CSSCorePolicy.DEBUG_PERF; + long t0 = trace ? System.nanoTime() : 0; + startStylingSession(); + try { + Shell[] shells = display.getShells(); + for (Shell s : shells) { + try { + s.setRedraw(false); + long r0 = trace ? System.nanoTime() : 0; + s.reskin(SWT.ALL); + if (trace) { + org.eclipse.e4.ui.css.core.impl.engine.CSSCorePolicy.reskinNs + .addAndGet(System.nanoTime() - r0); + org.eclipse.e4.ui.css.core.impl.engine.CSSCorePolicy.reskinCount.incrementAndGet(); + } + applyStyles(s, true); + } catch (Exception e) { + ILog.of(getClass()).error(e.getMessage(), e); + } finally { + s.setRedraw(true); + } + } + } finally { + stopStylingSession(); + if (trace) { + org.eclipse.e4.ui.css.core.impl.engine.CSSCorePolicy.reapplyNs.addAndGet(System.nanoTime() - t0); + org.eclipse.e4.ui.css.core.impl.engine.CSSCorePolicy.reapplyCount.incrementAndGet(); + } + } + } } diff --git a/bundles/org.eclipse.e4.ui.css.swt/src/org/eclipse/e4/ui/css/swt/helpers/PropertyHelper.java b/bundles/org.eclipse.e4.ui.css.swt/src/org/eclipse/e4/ui/css/swt/helpers/PropertyHelper.java deleted file mode 100644 index b018a6beb84..00000000000 --- a/bundles/org.eclipse.e4.ui.css.swt/src/org/eclipse/e4/ui/css/swt/helpers/PropertyHelper.java +++ /dev/null @@ -1,80 +0,0 @@ -/******************************************************************************* - * Copyright (c) 2010, 2015 Tom Schindl and others. - * - * This program and the accompanying materials - * are made available under the terms of the Eclipse Public License 2.0 - * which accompanies this distribution, and is available at - * https://www.eclipse.org/legal/epl-2.0/ - * - * SPDX-License-Identifier: EPL-2.0 - * - * Contributors: - * Tom Schindl - initial API and implementation - *******************************************************************************/ -package org.eclipse.e4.ui.css.swt.helpers; - -import java.util.HashMap; - -import java.lang.reflect.Method; - -import java.util.Map; - -import java.beans.IntrospectionException; -import java.beans.Introspector; -import java.beans.PropertyDescriptor; - -public class PropertyHelper { - private static final Map NOTNESTEDCACHE = new HashMap<>(); - - public static Object getProperty(Object bean, String attr) - throws Exception { - String key = bean.getClass().getName() + "#" + attr; - - if (attr.indexOf('.') == -1) { - Method readMethod = NOTNESTEDCACHE.get(key); - if (readMethod != null) { - return readMethod.invoke(bean); - } - } - - Method readMethod = null; - Object value = bean; - for (String part : attr.split("\\.")) { - PropertyDescriptor desc = getPropertyDescriptor(value.getClass(), - part); - if (desc != null) { - readMethod = desc.getReadMethod(); - } - - if (readMethod == null) { - throw new IllegalArgumentException("Attribute '" + part - + "' is not known in '" + value + "'"); - } else { - value = readMethod.invoke(value); - } - } - - if (attr.indexOf('.') == -1) { - NOTNESTEDCACHE.put(key,readMethod); - } - - return value; - } - - private static PropertyDescriptor getPropertyDescriptor(Class clazz, - String name) throws IntrospectionException { - PropertyDescriptor[] descs = getPropertyDescriptor(clazz); - for (PropertyDescriptor desc : descs) { - if (desc.getName().equals(name)) { - return desc; - } - } - return null; - } - - private static PropertyDescriptor[] getPropertyDescriptor(Class clazz) - throws IntrospectionException { - return Introspector.getBeanInfo(clazz).getPropertyDescriptors(); - } - -} \ No newline at end of file diff --git a/bundles/org.eclipse.e4.ui.css.swt/src/org/eclipse/e4/ui/css/swt/properties/css2/CSSPropertyBorderSWTHandler.java b/bundles/org.eclipse.e4.ui.css.swt/src/org/eclipse/e4/ui/css/swt/properties/css2/CSSPropertyBorderSWTHandler.java index f8a46ac2858..e7a4c6d0802 100644 --- a/bundles/org.eclipse.e4.ui.css.swt/src/org/eclipse/e4/ui/css/swt/properties/css2/CSSPropertyBorderSWTHandler.java +++ b/bundles/org.eclipse.e4.ui.css.swt/src/org/eclipse/e4/ui/css/swt/properties/css2/CSSPropertyBorderSWTHandler.java @@ -14,7 +14,6 @@ package org.eclipse.e4.ui.css.swt.properties.css2; import org.eclipse.e4.ui.css.core.dom.properties.CSSBorderProperties; -import org.eclipse.e4.ui.css.core.dom.properties.ICSSPropertyHandler2; import org.eclipse.e4.ui.css.core.dom.properties.css2.AbstractCSSPropertyBorderHandler; import org.eclipse.e4.ui.css.core.dom.properties.css2.ICSSPropertyBorderHandler; import org.eclipse.e4.ui.css.core.engine.CSSEngine; @@ -28,8 +27,7 @@ import org.w3c.dom.css.CSSPrimitiveValue; import org.w3c.dom.css.CSSValue; -public class CSSPropertyBorderSWTHandler extends -AbstractCSSPropertyBorderHandler implements ICSSPropertyHandler2 { +public class CSSPropertyBorderSWTHandler extends AbstractCSSPropertyBorderHandler { public static final ICSSPropertyBorderHandler INSTANCE = new CSSPropertyBorderSWTHandler(); @@ -70,7 +68,7 @@ public boolean applyCSSProperty(Object element, String property, } @Override - public void onAllCSSPropertiesApplyed(Object element, CSSEngine engine) + public void onAllCSSPropertiesApplied(Object element, CSSEngine engine) throws Exception { Control control = SWTElementHelpers.getControl(element); if (control != null) { diff --git a/bundles/org.eclipse.e4.ui.css.swt/src/org/eclipse/e4/ui/css/swt/properties/css2/CSSPropertyFontSWTHandler.java b/bundles/org.eclipse.e4.ui.css.swt/src/org/eclipse/e4/ui/css/swt/properties/css2/CSSPropertyFontSWTHandler.java index 886c70f8051..a5a6afe595b 100644 --- a/bundles/org.eclipse.e4.ui.css.swt/src/org/eclipse/e4/ui/css/swt/properties/css2/CSSPropertyFontSWTHandler.java +++ b/bundles/org.eclipse.e4.ui.css.swt/src/org/eclipse/e4/ui/css/swt/properties/css2/CSSPropertyFontSWTHandler.java @@ -16,7 +16,6 @@ *******************************************************************************/ package org.eclipse.e4.ui.css.swt.properties.css2; -import org.eclipse.e4.ui.css.core.dom.properties.ICSSPropertyHandler2; import org.eclipse.e4.ui.css.core.dom.properties.css2.AbstractCSSPropertyFontHandler; import org.eclipse.e4.ui.css.core.dom.properties.css2.CSS2FontProperties; import org.eclipse.e4.ui.css.core.dom.properties.css2.ICSSPropertyFontHandler; @@ -39,8 +38,7 @@ import org.w3c.dom.css.CSSStyleDeclaration; import org.w3c.dom.css.CSSValue; -public class CSSPropertyFontSWTHandler extends AbstractCSSPropertyFontHandler -implements ICSSPropertyHandler2 { +public class CSSPropertyFontSWTHandler extends AbstractCSSPropertyFontHandler { public static final ICSSPropertyFontHandler INSTANCE = new CSSPropertyFontSWTHandler(); @@ -232,7 +230,7 @@ public String retrieveCSSPropertyFontWeight(Object element, String pseudo, } @Override - public void onAllCSSPropertiesApplyed(Object element, CSSEngine engine) + public void onAllCSSPropertiesApplied(Object element, CSSEngine engine) throws Exception { final Widget widget = SWTElementHelpers.getWidget(element); if (widget == null || widget instanceof CTabItem) { diff --git a/bundles/org.eclipse.jface.text/META-INF/MANIFEST.MF b/bundles/org.eclipse.jface.text/META-INF/MANIFEST.MF index 653c7b05d9d..ef7567b50e6 100644 --- a/bundles/org.eclipse.jface.text/META-INF/MANIFEST.MF +++ b/bundles/org.eclipse.jface.text/META-INF/MANIFEST.MF @@ -2,7 +2,7 @@ Manifest-Version: 1.0 Bundle-ManifestVersion: 2 Bundle-Name: %pluginName Bundle-SymbolicName: org.eclipse.jface.text -Bundle-Version: 3.31.0.qualifier +Bundle-Version: 3.31.100.qualifier Bundle-Vendor: %providerName Bundle-Localization: plugin Export-Package: @@ -38,7 +38,6 @@ Require-Bundle: org.eclipse.text;bundle-version="[3.8.0,4.0.0)";visibility:=reexport, org.eclipse.swt;bundle-version="[3.133.0,4.0.0)", org.eclipse.jface;bundle-version="[3.39.0,4.0.0)" -Import-Package: com.ibm.icu.text Bundle-RequiredExecutionEnvironment: JavaSE-21 Automatic-Module-Name: org.eclipse.jface.text Bundle-Activator: org.eclipse.jface.text.Activator diff --git a/bundles/org.eclipse.jface.text/src/org/eclipse/jface/internal/text/codemining/CodeMiningLineHeaderAnnotation.java b/bundles/org.eclipse.jface.text/src/org/eclipse/jface/internal/text/codemining/CodeMiningLineHeaderAnnotation.java index 657e34cfd7f..45f814abf5c 100644 --- a/bundles/org.eclipse.jface.text/src/org/eclipse/jface/internal/text/codemining/CodeMiningLineHeaderAnnotation.java +++ b/bundles/org.eclipse.jface.text/src/org/eclipse/jface/internal/text/codemining/CodeMiningLineHeaderAnnotation.java @@ -133,20 +133,28 @@ static int getMultilineHeight(GC gc, List minings, StyledText style } private static int calculateLineHeight(List minings, GC gc, StyledText styledText, boolean ignoreFirstLine, int sumLineHeight, int miningIndex, String[] splitted) { + int styledTextLineHeight= 0; + if (styledText != null) { + styledTextLineHeight= styledText.getLineHeight(); + } for (int j= 0; j < splitted.length; j++) { String line= splitted[j]; if (j == 0 && ignoreFirstLine) { continue; } - if (j == splitted.length - 1 && miningIndex + 1 < minings.size()) { // last line, take first line from next mining - String nextLabel= minings.get(miningIndex + 1).getLabel(); - if (nextLabel != null) { - String firstFromNext= nextLabel.split("\\r?\\n|\\r")[0]; //$NON-NLS-1$ - line+= firstFromNext; + if (styledText != null) { + sumLineHeight+= styledTextLineHeight + styledText.getLineSpacing(); + } else { + if (j == splitted.length - 1 && miningIndex + 1 < minings.size()) { // last line, take first line from next mining + String nextLabel= minings.get(miningIndex + 1).getLabel(); + if (nextLabel != null) { + String firstFromNext= nextLabel.split("\\r?\\n|\\r")[0]; //$NON-NLS-1$ + line+= firstFromNext; + } } + Point ext= gc.textExtent(line); + sumLineHeight+= ext.y; } - Point ext= gc.textExtent(line); - sumLineHeight+= ext.y + (styledText != null ? styledText.getLineSpacing() : 0); } return sumLineHeight; } diff --git a/bundles/org.eclipse.jface.text/src/org/eclipse/jface/text/DefaultTextDoubleClickStrategy.java b/bundles/org.eclipse.jface.text/src/org/eclipse/jface/text/DefaultTextDoubleClickStrategy.java index 6b1b370b8e4..c814e8f60be 100644 --- a/bundles/org.eclipse.jface.text/src/org/eclipse/jface/text/DefaultTextDoubleClickStrategy.java +++ b/bundles/org.eclipse.jface.text/src/org/eclipse/jface/text/DefaultTextDoubleClickStrategy.java @@ -14,12 +14,10 @@ package org.eclipse.jface.text; +import java.text.BreakIterator; import java.text.CharacterIterator; import java.util.Locale; -import com.ibm.icu.text.BreakIterator; - - /** * Standard implementation of @@ -223,9 +221,54 @@ protected IRegion findExtendedDoubleClickSelection(IDocument document, int offse * @since 3.5 */ protected IRegion findWord(IDocument document, int offset) { + IRegion identifier= findIdentifierAt(document, offset); + if (identifier != null) { + return identifier; + } return findWord(document, offset, getWordBreakIterator()); } + /** + * If the offset lies on an ASCII identifier character ({@code [A-Za-z0-9_]}), or + * just after one, returns the maximal contiguous identifier run. Otherwise + * returns {@code null} so the caller falls back to the locale-aware + * {@link BreakIterator}. This handles identifier-style words containing runs + * of {@code '_'} (e.g. {@code foo__bar}, {@code __aaaa}) consistently across + * JDK versions, since {@link BreakIterator#getWordInstance()} places word + * boundaries between consecutive underscores while users expect such tokens + * to be selected as a single word. + */ + private static IRegion findIdentifierAt(IDocument document, int offset) { + try { + IRegion line= document.getLineInformationOfOffset(offset); + int lineStart= line.getOffset(); + int lineEnd= lineStart + line.getLength(); + int probe; + if (offset < lineEnd && isIdentifierPart(document.getChar(offset))) { + probe= offset; + } else if (offset > lineStart && isIdentifierPart(document.getChar(offset - 1))) { + probe= offset - 1; + } else { + return null; + } + int start= probe; + while (start > lineStart && isIdentifierPart(document.getChar(start - 1))) { + start--; + } + int end= probe + 1; + while (end < lineEnd && isIdentifierPart(document.getChar(end))) { + end++; + } + return new Region(start, end - start); + } catch (BadLocationException e) { + return null; + } + } + + private static boolean isIdentifierPart(char c) { + return c == '_' || (c < 128 && (c >= '0' && c <= '9' || c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z')); + } + /** * Returns the locale specific word break iterator. * diff --git a/bundles/org.eclipse.jface.text/src/org/eclipse/jface/text/codemining/AbstractCodeMining.java b/bundles/org.eclipse.jface.text/src/org/eclipse/jface/text/codemining/AbstractCodeMining.java index 4894583d6a1..3547e2daa6b 100644 --- a/bundles/org.eclipse.jface.text/src/org/eclipse/jface/text/codemining/AbstractCodeMining.java +++ b/bundles/org.eclipse.jface.text/src/org/eclipse/jface/text/codemining/AbstractCodeMining.java @@ -145,7 +145,7 @@ public Point draw(GC gc, StyledText textWidget, Color color, int x, int y) { String title= getLabel() != null ? getLabel() : "no command"; //$NON-NLS-1$ gc.drawString(title, x, y, true); Point result= gc.stringExtent(title); - result.y+= textWidget.getLineSpacing(); + result.y= textWidget.getLineHeight() + textWidget.getLineSpacing(); return result; } diff --git a/bundles/org.eclipse.jface.text/src/org/eclipse/jface/text/codemining/LineHeaderCodeMining.java b/bundles/org.eclipse.jface.text/src/org/eclipse/jface/text/codemining/LineHeaderCodeMining.java index 46721f2d736..8c310ee2d2e 100644 --- a/bundles/org.eclipse.jface.text/src/org/eclipse/jface/text/codemining/LineHeaderCodeMining.java +++ b/bundles/org.eclipse.jface.text/src/org/eclipse/jface/text/codemining/LineHeaderCodeMining.java @@ -84,6 +84,8 @@ public Point draw(GC gc, StyledText textWidget, Color color, int x, int y) { } static Point draw(String label, GC gc, StyledText textWidget, int x, int y, Callable superDrawCallable) { + int lineHeight= textWidget.getLineHeight(); + int lineSpacing= textWidget.getLineSpacing(); String title= label != null ? label : "no command"; //$NON-NLS-1$ String[] lines= title.split("\\r?\\n|\\r"); //$NON-NLS-1$ if (lines.length > 1) { @@ -92,8 +94,8 @@ static Point draw(String label, GC gc, StyledText textWidget, int x, int y, Call gc.drawString(line, x, y, true); Point ext= gc.stringExtent(line); result.x= Math.max(result.x, ext.x); - result.y+= ext.y + textWidget.getLineSpacing(); - y+= ext.y + textWidget.getLineSpacing(); + result.y+= lineHeight + lineSpacing; + y+= lineHeight + lineSpacing; } return result; } else { diff --git a/bundles/org.eclipse.ui.editors/META-INF/MANIFEST.MF b/bundles/org.eclipse.ui.editors/META-INF/MANIFEST.MF index 8e375d7187a..8cdb3e39545 100644 --- a/bundles/org.eclipse.ui.editors/META-INF/MANIFEST.MF +++ b/bundles/org.eclipse.ui.editors/META-INF/MANIFEST.MF @@ -2,7 +2,7 @@ Manifest-Version: 1.0 Bundle-ManifestVersion: 2 Bundle-Name: %pluginName Bundle-SymbolicName: org.eclipse.ui.editors; singleton:=true -Bundle-Version: 3.22.0.qualifier +Bundle-Version: 3.22.100.qualifier Bundle-Activator: org.eclipse.ui.internal.editors.text.EditorsPlugin Bundle-ActivationPolicy: lazy Bundle-Vendor: %providerName diff --git a/bundles/org.eclipse.ui.editors/src/org/eclipse/ui/editors/text/templates/ContributionTemplateStore.java b/bundles/org.eclipse.ui.editors/src/org/eclipse/ui/editors/text/templates/ContributionTemplateStore.java index 1d9167b0126..eec6ebf5d91 100644 --- a/bundles/org.eclipse.ui.editors/src/org/eclipse/ui/editors/text/templates/ContributionTemplateStore.java +++ b/bundles/org.eclipse.ui.editors/src/org/eclipse/ui/editors/text/templates/ContributionTemplateStore.java @@ -17,6 +17,9 @@ import java.io.BufferedInputStream; import java.io.IOException; import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; import java.net.URL; import java.util.ArrayList; import java.util.Collection; @@ -128,12 +131,12 @@ private void readIncludedTemplates(Collection templates String file= element.getAttribute(FILE); if (file != null) { Bundle plugin = Platform.getBundle(element.getContributor().getName()); - URL url= FileLocator.find(plugin, IPath.fromOSString(file), null); + URL url= resolveResource(plugin, file); if (url != null) { ResourceBundle bundle= null; String translations= element.getAttribute(TRANSLATIONS); if (translations != null) { - URL bundleURL= FileLocator.find(plugin, IPath.fromOSString(translations), null); + URL bundleURL= resolveResource(plugin, translations); if (bundleURL != null) { try (InputStream bundleStream= bundleURL.openStream()) { bundle= new PropertyResourceBundle(bundleStream); @@ -160,6 +163,30 @@ private void readIncludedTemplates(Collection templates } } + /** + * Resolves a resource location to a URL. Supports plain bundle-relative paths + * as well as platform:/plugin/ URIs, which allow referencing files + * contributed by other plug-ins. Other platform:/ schemes (e.g. + * platform:/resource/, platform:/fragment/) are not + * handled here and are treated as bundle-relative paths. + * + * @param plugin the contributing bundle, used for bundle-relative lookups + * @param location the resource location (a bundle-relative path or a + * platform:/plugin/ URI) + * @return the resolved URL, or null if it could not be found + */ + private static URL resolveResource(Bundle plugin, String location) { + if (location.startsWith("platform:/plugin/")) { //$NON-NLS-1$ + try { + return FileLocator.find(new URI(location).toURL()); + } catch (URISyntaxException | MalformedURLException e) { + EditorsPlugin.log(e); + return null; + } + } + return FileLocator.find(plugin, IPath.fromOSString(location), null); + } + /** * Validates a template against the context type registered in the context * type registry. Returns always true if no registry is diff --git a/bundles/org.eclipse.ui.editors/src/org/eclipse/ui/internal/texteditor/stickyscroll/StickyScrollingControl.java b/bundles/org.eclipse.ui.editors/src/org/eclipse/ui/internal/texteditor/stickyscroll/StickyScrollingControl.java index 12bb922768e..ed21303fb73 100644 --- a/bundles/org.eclipse.ui.editors/src/org/eclipse/ui/internal/texteditor/stickyscroll/StickyScrollingControl.java +++ b/bundles/org.eclipse.ui.editors/src/org/eclipse/ui/internal/texteditor/stickyscroll/StickyScrollingControl.java @@ -331,7 +331,10 @@ private void calculateAndSetStickyLinesCanvasBounds() { StyledText textWidget= sourceViewer.getTextWidget(); int numberStickyLines= getNumberStickyLines(); - int lineHeight= stickyLineText.getLineHeight() * numberStickyLines; + int lineHeight = 0; + for (int i = 0; i < numberStickyLines; i++) { + lineHeight += stickyLineText.getLineHeight(stickyLineText.getOffsetAtLine(i)); + } int spacingHeight= stickyLineText.getLineSpacing() * (numberStickyLines - 1); int separatorHeight= bottomSeparator.getBounds().height; @@ -450,7 +453,8 @@ private boolean areStickyLinesOutDated(StyledText textWidget) { } private void limitVisibleStickyLinesToTextWidgetHeight(StyledText textWidget) { - int lineHeight= textWidget.getLineHeight() + textWidget.getLineSpacing(); + int topOffset = textWidget.getOffsetAtLine(textWidget.getTopIndex()); + int lineHeight = textWidget.getLineHeight(topOffset) + textWidget.getLineSpacing(); int textWidgetHeight= textWidget.getBounds().height; int visibleLinesInTextWidget= textWidgetHeight / lineHeight; diff --git a/bundles/org.eclipse.ui.workbench/eclipseui/org/eclipse/ui/internal/dialogs/PropertyDialog.java b/bundles/org.eclipse.ui.workbench/eclipseui/org/eclipse/ui/internal/dialogs/PropertyDialog.java index 598111cce77..c420c79d02e 100644 --- a/bundles/org.eclipse.ui.workbench/eclipseui/org/eclipse/ui/internal/dialogs/PropertyDialog.java +++ b/bundles/org.eclipse.ui.workbench/eclipseui/org/eclipse/ui/internal/dialogs/PropertyDialog.java @@ -17,6 +17,7 @@ import java.util.Iterator; import org.eclipse.core.runtime.Adapters; +import org.eclipse.jface.dialogs.IDialogConstants; import org.eclipse.jface.dialogs.MessageDialog; import org.eclipse.jface.preference.IPreferenceNode; import org.eclipse.jface.preference.PreferenceManager; @@ -24,6 +25,7 @@ import org.eclipse.jface.viewers.IStructuredSelection; import org.eclipse.jface.viewers.StructuredSelection; import org.eclipse.osgi.util.NLS; +import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Shell; import org.eclipse.ui.PlatformUI; @@ -90,6 +92,20 @@ public static PropertyDialog createDialogOn(Shell shell, final String propertyPa return propertyDialog; } + + @Override + protected void buttonPressed(int buttonId) { + if (buttonId == IDialogConstants.CLOSE_ID) { + cancelPressed(); + return; + } + super.buttonPressed(buttonId); + } + + @Override + protected void createButtonsForButtonBar(Composite parent) { + createButton(parent, IDialogConstants.CLOSE_ID, IDialogConstants.CLOSE_LABEL, true); + } @Override protected void addButtonsToHelpControl(Control control) { @@ -162,6 +178,11 @@ protected String getSelectedNodePreference() { return lastPropertyId; } + @Override + public void updateButtons() { + // This function is overridden to remove the Apply and close button + } + /** * Get the name of the selected item preference */ diff --git a/tests/org.eclipse.e4.ui.tests.css.core/src/org/eclipse/e4/ui/css/core/impl/engine/AbstractCSSEngineTest.java b/tests/org.eclipse.e4.ui.tests.css.core/src/org/eclipse/e4/ui/css/core/impl/engine/CSSEngineImplTest.java similarity index 84% rename from tests/org.eclipse.e4.ui.tests.css.core/src/org/eclipse/e4/ui/css/core/impl/engine/AbstractCSSEngineTest.java rename to tests/org.eclipse.e4.ui.tests.css.core/src/org/eclipse/e4/ui/css/core/impl/engine/CSSEngineImplTest.java index 4490d49cd68..a6b1e12ce1d 100644 --- a/tests/org.eclipse.e4.ui.tests.css.core/src/org/eclipse/e4/ui/css/core/impl/engine/AbstractCSSEngineTest.java +++ b/tests/org.eclipse.e4.ui.tests.css.core/src/org/eclipse/e4/ui/css/core/impl/engine/CSSEngineImplTest.java @@ -17,29 +17,23 @@ import java.util.Objects; -import org.eclipse.e4.ui.css.core.dom.parsers.CSSParser; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.w3c.dom.Element; -public class AbstractCSSEngineTest { +public class CSSEngineImplTest { - private AbstractCSSEngine objectUnderTest; + private CSSEngineImpl objectUnderTest; @BeforeEach public void setUp() { - objectUnderTest = new AbstractCSSEngine() { + objectUnderTest = new CSSEngineImpl() { @Override public void reapply() { // mock does nothing } - @Override - public CSSParser makeCSSParser() { - return null; - } - }; objectUnderTest.setElementProvider((element, engine) -> { // throws NPE if parameter is null diff --git a/tests/org.eclipse.e4.ui.tests.css.core/src/org/eclipse/e4/ui/css/core/impl/engine/selector/SelectorMatcherTest.java b/tests/org.eclipse.e4.ui.tests.css.core/src/org/eclipse/e4/ui/css/core/impl/engine/selector/SelectorMatcherTest.java new file mode 100644 index 00000000000..31162dd52c7 --- /dev/null +++ b/tests/org.eclipse.e4.ui.tests.css.core/src/org/eclipse/e4/ui/css/core/impl/engine/selector/SelectorMatcherTest.java @@ -0,0 +1,223 @@ +/******************************************************************************* + * Copyright (c) 2026 Lars Vogel and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Lars Vogel - initial API and implementation + *******************************************************************************/ +package org.eclipse.e4.ui.css.core.impl.engine.selector; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; + +import org.eclipse.e4.ui.css.core.impl.engine.CSSEngineImpl; +import org.eclipse.e4.ui.css.core.impl.engine.selector.Selectors.Adjacent; +import org.eclipse.e4.ui.css.core.impl.engine.selector.Selectors.And; +import org.eclipse.e4.ui.css.core.impl.engine.selector.Selectors.AttributeBeginHyphen; +import org.eclipse.e4.ui.css.core.impl.engine.selector.Selectors.AttributeIncludes; +import org.eclipse.e4.ui.css.core.impl.engine.selector.Selectors.AttributeSelector; +import org.eclipse.e4.ui.css.core.impl.engine.selector.Selectors.Child; +import org.eclipse.e4.ui.css.core.impl.engine.selector.Selectors.ClassSelector; +import org.eclipse.e4.ui.css.core.impl.engine.selector.Selectors.Descendant; +import org.eclipse.e4.ui.css.core.impl.engine.selector.Selectors.ElementType; +import org.eclipse.e4.ui.css.core.impl.engine.selector.Selectors.IdSelector; +import org.eclipse.e4.ui.css.core.impl.engine.selector.Selectors.PseudoClass; +import org.eclipse.e4.ui.css.core.impl.engine.selector.Selectors.SelectorList; +import org.eclipse.e4.ui.css.core.impl.engine.selector.Selectors.Universal; +import org.eclipse.e4.ui.tests.css.core.util.TestElement; +import org.junit.jupiter.api.Test; + +/** + * Unit tests for {@link SelectorMatcher}. The cases mirror those in the + * Phase 1 {@code CSSEngineTest}, but go through the new internal selector + * AST instead of SAC. When Phase 3 Step 1 wires the engine to use this + * matcher, the SAC-based duplicate tests can be retired. + */ +class SelectorMatcherTest { + + private static final TestCSSEngine ENGINE = new TestCSSEngine(); + + private static final class TestCSSEngine extends CSSEngineImpl { + @Override + public void reapply() { + } + } + + private static TestElement element(String tag, String cssClass, String id) { + TestElement e = new TestElement(tag, ENGINE); + if (cssClass != null) { + e.setClass(cssClass); + } + if (id != null) { + e.setId(id); + } + return e; + } + + @Test + void universalMatchesAnything() { + assertTrue(SelectorMatcher.matches(new Universal(), element("Button", null, null), null)); + assertTrue(SelectorMatcher.matches(new Universal(), element("Label", "x", "y"), null)); + } + + @Test + void typeSelectorIsCaseSensitive() { + assertTrue(SelectorMatcher.matches(new ElementType("Button"), element("Button", null, null), null)); + assertFalse(SelectorMatcher.matches(new ElementType("Button"), element("button", null, null), null)); + assertFalse(SelectorMatcher.matches(new ElementType("Button"), element("Label", null, null), null)); + } + + @Test + void classSelector() { + assertTrue(SelectorMatcher.matches(new ClassSelector("foo"), element("Button", "foo", null), null)); + assertFalse(SelectorMatcher.matches(new ClassSelector("foo"), element("Button", "bar", null), null)); + assertFalse(SelectorMatcher.matches(new ClassSelector("foo"), element("Button", null, null), null)); + } + + @Test + void classSelectorMatchesOneOfMultipleClasses() { + assertTrue(SelectorMatcher.matches(new ClassSelector("foo"), element("Button", "foo bar", null), null)); + assertTrue(SelectorMatcher.matches(new ClassSelector("bar"), element("Button", "foo bar", null), null)); + assertFalse(SelectorMatcher.matches(new ClassSelector("baz"), element("Button", "foo bar", null), null)); + } + + @Test + void idSelector() { + assertTrue(SelectorMatcher.matches(new IdSelector("go"), element("Button", null, "go"), null)); + assertFalse(SelectorMatcher.matches(new IdSelector("go"), element("Button", null, "stop"), null)); + assertFalse(SelectorMatcher.matches(new IdSelector("go"), element("Button", null, null), null)); + } + + @Test + void compoundSelector() { + Selectors.Selector selector = new And(new And(new ElementType("Button"), new ClassSelector("primary")), + new IdSelector("go")); + assertTrue(SelectorMatcher.matches(selector, element("Button", "primary", "go"), null)); + assertFalse(SelectorMatcher.matches(selector, element("Label", "primary", "go"), null)); + assertFalse(SelectorMatcher.matches(selector, element("Button", "secondary", "go"), null)); + assertFalse(SelectorMatcher.matches(selector, element("Button", "primary", "stop"), null)); + } + + @Test + void descendantCombinator() { + Selectors.Selector selector = new Descendant(new ElementType("Composite"), new ElementType("Button")); + TestElement composite = element("Composite", null, null); + TestElement intermediate = new TestElement("Group", composite, ENGINE); + TestElement button = new TestElement("Button", intermediate, ENGINE); + assertTrue(SelectorMatcher.matches(selector, button, null)); + + TestElement orphan = element("Button", null, null); + assertFalse(SelectorMatcher.matches(selector, orphan, null)); + } + + @Test + void childCombinator() { + Selectors.Selector selector = new Child(new ElementType("Composite"), new ElementType("Button")); + TestElement composite = element("Composite", null, null); + TestElement direct = new TestElement("Button", composite, ENGINE); + assertTrue(SelectorMatcher.matches(selector, direct, null)); + + TestElement intermediate = new TestElement("Group", composite, ENGINE); + TestElement grandchild = new TestElement("Button", intermediate, ENGINE); + assertFalse(SelectorMatcher.matches(selector, grandchild, null)); + } + + @Test + void attributePresentMatchesEvenWithEmptyValue() { + AttributeSelector selector = new AttributeSelector("style", null); + TestElement withAttr = element("Button", null, null); + withAttr.setAttribute("style", "SWT.PUSH"); + assertTrue(SelectorMatcher.matches(selector, withAttr, null)); + assertFalse(SelectorMatcher.matches(selector, element("Button", null, null), null)); + } + + @Test + void attributeIncludesIsWordBoundaryMatch() { + AttributeIncludes selector = new AttributeIncludes("style", "SWT.CHECK"); + TestElement match = element("Button", null, null); + match.setAttribute("style", "SWT.CHECK SWT.BORDER"); + assertTrue(SelectorMatcher.matches(selector, match, null)); + + TestElement substring = element("Button", null, null); + substring.setAttribute("style", "SWT.CHECK_DELAYED"); + // 'CHECK' is a substring of 'CHECK_DELAYED' but not a whitespace-separated word. + assertFalse(SelectorMatcher.matches(new AttributeIncludes("style", "CHECK"), substring, null)); + } + + @Test + void attributeBeginHyphen() { + AttributeBeginHyphen selector = new AttributeBeginHyphen("lang", "en"); + TestElement exact = element("p", null, null); + exact.setAttribute("lang", "en"); + TestElement prefixed = element("p", null, null); + prefixed.setAttribute("lang", "en-US"); + TestElement other = element("p", null, null); + other.setAttribute("lang", "fr"); + assertTrue(SelectorMatcher.matches(selector, exact, null)); + assertTrue(SelectorMatcher.matches(selector, prefixed, null)); + assertFalse(SelectorMatcher.matches(selector, other, null)); + } + + @Test + void pseudoClassMatchesViaIsPseudoInstanceOf() { + PseudoClass selector = new PseudoClass("selected"); + TestElement on = new TestElement("Button", ENGINE) { + @Override + public boolean isPseudoInstanceOf(String s) { + return "selected".equals(s); + } + }; + TestElement off = element("Button", null, null); + assertTrue(SelectorMatcher.matches(selector, on, null)); + assertFalse(SelectorMatcher.matches(selector, off, null)); + } + + @Test + void selectorListMatchesAnyAlternative() { + SelectorList list = new SelectorList(List.of(new ClassSelector("a"), new ClassSelector("b"))); + assertTrue(SelectorMatcher.matches(list, element("Button", "a", null), null)); + assertTrue(SelectorMatcher.matches(list, element("Button", "b", null), null)); + assertFalse(SelectorMatcher.matches(list, element("Button", "c", null), null)); + } + + @Test + void specificityMatchesCss21() { + assertEquals(0, new Universal().specificity()); + assertEquals(1, new ElementType("Button").specificity()); + assertEquals(10, new ClassSelector("foo").specificity()); + assertEquals(100, new IdSelector("go").specificity()); + assertEquals(11, new And(new ElementType("Button"), new ClassSelector("primary")).specificity()); + assertEquals(111, new And(new And(new ElementType("Button"), new ClassSelector("primary")), + new IdSelector("go")).specificity()); + } + + @Test + void specificityOfSelectorListIsMaxOverAlternatives() { + // "Button, .foo, #go": max specificity is the id (100). + SelectorList list = new SelectorList( + List.of(new ElementType("Button"), new ClassSelector("foo"), new IdSelector("go"))); + assertEquals(100, list.specificity()); + } + + @Test + void adjacentSiblingCombinatorRequiresSiblingSupport() { + // TestElement's ElementAdapter base returns null from getPreviousSibling, + // so adjacent matching cannot succeed against it. Locks in that the + // matcher returns false rather than throwing on elements without + // sibling support. + Adjacent selector = new Adjacent(new ElementType("Label"), new ElementType("Button")); + TestElement parent = element("Composite", null, null); + new TestElement("Label", parent, ENGINE); + TestElement second = new TestElement("Button", parent, ENGINE); + assertFalse(SelectorMatcher.matches(selector, second, null)); + } +} diff --git a/tests/org.eclipse.e4.ui.tests.css.core/src/org/eclipse/e4/ui/tests/css/core/CSSEngineTest.java b/tests/org.eclipse.e4.ui.tests.css.core/src/org/eclipse/e4/ui/tests/css/core/CSSEngineTest.java index fa16ea5395e..31916477fd6 100644 --- a/tests/org.eclipse.e4.ui.tests.css.core/src/org/eclipse/e4/ui/tests/css/core/CSSEngineTest.java +++ b/tests/org.eclipse.e4.ui.tests.css.core/src/org/eclipse/e4/ui/tests/css/core/CSSEngineTest.java @@ -23,10 +23,10 @@ import org.eclipse.e4.ui.css.core.engine.CSSEngine; import org.eclipse.e4.ui.css.core.impl.engine.CSSEngineImpl; +import org.eclipse.e4.ui.css.core.impl.engine.selector.Selectors; +import org.eclipse.e4.ui.css.core.impl.engine.selector.Selectors.Selector; import org.eclipse.e4.ui.tests.css.core.util.TestElement; import org.junit.jupiter.api.Test; -import org.w3c.css.sac.Selector; -import org.w3c.css.sac.SelectorList; import org.w3c.dom.Element; class CSSEngineTest { @@ -55,7 +55,7 @@ private static Selector parse(CSSEngine engine, String selector) throws Exceptio @Test void testSelectorMatch() throws Exception { TestCSSEngine engine = new TestCSSEngine(); - SelectorList list = engine.parseSelectors("Date"); + Selectors.SelectorList list = engine.parseSelectors("Date"); engine.setElementProvider((element, engine1) -> new TestElement(element.getClass().getSimpleName(), engine1)); assertFalse(engine.matches(list.item(0), new Object(), null)); @@ -249,7 +249,7 @@ void testNegativeMatch() throws Exception { @Test void testSelectorListMatch() throws Exception { TestCSSEngine engine = new TestCSSEngine(); - SelectorList list = engine.parseSelectors(".a, .b"); + Selectors.SelectorList list = engine.parseSelectors(".a, .b"); TestElement a = createElement(engine, "Button", "a", null); TestElement b = createElement(engine, "Button", "b", null); TestElement c = createElement(engine, "Button", "c", null); @@ -274,9 +274,9 @@ void testTagNameCaseSensitivity() throws Exception { assertFalse(engine.matches(lower, capitalElement, null)); } - private static boolean matchesAny(CSSEngine engine, SelectorList list, Element element) { - for (int i = 0; i < list.getLength(); i++) { - if (engine.matches(list.item(i), element, null)) { + private static boolean matchesAny(CSSEngine engine, Selectors.SelectorList list, Element element) { + for (Selector selector : list.alternatives()) { + if (engine.matches(selector, element, null)) { return true; } } diff --git a/tests/org.eclipse.e4.ui.tests.css.core/src/org/eclipse/e4/ui/tests/css/core/CssCoreTestSuite.java b/tests/org.eclipse.e4.ui.tests.css.core/src/org/eclipse/e4/ui/tests/css/core/CssCoreTestSuite.java index 60911cc40ec..63b3ca7cd77 100644 --- a/tests/org.eclipse.e4.ui.tests.css.core/src/org/eclipse/e4/ui/tests/css/core/CssCoreTestSuite.java +++ b/tests/org.eclipse.e4.ui.tests.css.core/src/org/eclipse/e4/ui/tests/css/core/CssCoreTestSuite.java @@ -15,7 +15,7 @@ *******************************************************************************/ package org.eclipse.e4.ui.tests.css.core; -import org.eclipse.e4.ui.css.core.impl.engine.AbstractCSSEngineTest; +import org.eclipse.e4.ui.css.core.impl.engine.CSSEngineImplTest; import org.eclipse.e4.ui.tests.css.core.dom.CSSPropertyHandlerProviderTest; import org.eclipse.e4.ui.tests.css.core.parser.CascadeTest; import org.eclipse.e4.ui.tests.css.core.parser.FontFaceRulesTest; @@ -44,7 +44,7 @@ CSSEngineTest.class, ImportTest.class, InheritTest.class, - AbstractCSSEngineTest.class, + CSSEngineImplTest.class, CSSPropertyHandlerProviderTest.class }) @Suite diff --git a/tests/org.eclipse.e4.ui.tests.css.core/src/org/eclipse/e4/ui/tests/css/core/parser/SelectorTest.java b/tests/org.eclipse.e4.ui.tests.css.core/src/org/eclipse/e4/ui/tests/css/core/parser/SelectorTest.java index f3419a83eb0..43a3be1899b 100644 --- a/tests/org.eclipse.e4.ui.tests.css.core/src/org/eclipse/e4/ui/tests/css/core/parser/SelectorTest.java +++ b/tests/org.eclipse.e4.ui.tests.css.core/src/org/eclipse/e4/ui/tests/css/core/parser/SelectorTest.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2013, 2014 IBM Corporation and others. + * Copyright (c) 2013, 2026 IBM Corporation and others. * * This program and the accompanying materials * are made available under the terms of the Eclipse Public License 2.0 @@ -21,11 +21,11 @@ import java.io.IOException; import org.eclipse.e4.ui.css.core.engine.CSSEngine; +import org.eclipse.e4.ui.css.core.impl.engine.selector.Selectors; import org.eclipse.e4.ui.tests.css.core.util.ParserTestUtil; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.w3c.css.sac.CSSParseException; -import org.w3c.css.sac.SelectorList; public class SelectorTest { private CSSEngine engine; @@ -37,35 +37,37 @@ public void setUp() throws Exception { @Test void testSimpleSelector() throws Exception { - SelectorList list = engine.parseSelectors("Type1"); + Selectors.SelectorList list = engine.parseSelectors("Type1"); assertNotNull(list); assertEquals(1, list.getLength()); - assertEquals("Type1", list.item(0).toString()); + assertEquals("Type1", list.item(0).text()); } @Test void testMultipleSelectors() throws Exception { - SelectorList list = engine.parseSelectors("Type1, Type2"); + Selectors.SelectorList list = engine.parseSelectors("Type1, Type2"); assertNotNull(list); assertEquals(2, list.getLength()); - assertEquals("Type1", list.item(0).toString()); - assertEquals("Type2", list.item(1).toString()); + assertEquals("Type1", list.item(0).text()); + assertEquals("Type2", list.item(1).text()); } @Test void testClassSelector() throws Exception { - SelectorList list = engine.parseSelectors(".Class1"); + Selectors.SelectorList list = engine.parseSelectors(".Class1"); assertNotNull(list); assertEquals(1, list.getLength()); - assertEquals("*[class=\"Class1\"]", list.item(0).toString()); + assertEquals(".Class1", list.item(0).text()); } @Test void testAttributeSelector() throws Exception { - SelectorList list = engine.parseSelectors("*[class='Class1']"); + Selectors.SelectorList list = engine.parseSelectors("*[class='Class1']"); assertNotNull(list); assertEquals(1, list.getLength()); - assertEquals("*[class=\"Class1\"]", list.item(0).toString()); + // The Universal selector ('*') is folded away since the AttributeSelector + // alone carries the full match condition. + assertEquals("[class='Class1']", list.item(0).text()); } @Test diff --git a/tests/org.eclipse.e4.ui.tests.css.core/src/org/eclipse/e4/ui/tests/css/core/util/ParserTestUtil.java b/tests/org.eclipse.e4.ui.tests.css.core/src/org/eclipse/e4/ui/tests/css/core/util/ParserTestUtil.java index 21fd0fffcbc..e72b95fd186 100644 --- a/tests/org.eclipse.e4.ui.tests.css.core/src/org/eclipse/e4/ui/tests/css/core/util/ParserTestUtil.java +++ b/tests/org.eclipse.e4.ui.tests.css.core/src/org/eclipse/e4/ui/tests/css/core/util/ParserTestUtil.java @@ -19,7 +19,7 @@ import org.eclipse.e4.ui.css.core.dom.parsers.CSSParser; import org.eclipse.e4.ui.css.core.engine.CSSEngine; import org.eclipse.e4.ui.css.core.engine.CSSErrorHandler; -import org.eclipse.e4.ui.css.core.impl.engine.AbstractCSSEngine; +import org.eclipse.e4.ui.css.core.impl.engine.CSSEngineImpl; import org.eclipse.e4.ui.css.swt.engine.CSSSWTEngineImpl; import org.eclipse.swt.widgets.Display; import org.w3c.css.sac.InputSource; @@ -51,7 +51,7 @@ public static CSSStyleSheet parseCss(String css) */ public static CSSStyleSheet parseCssWithoutImports(String css) throws IOException { - CSSParser parser = ((AbstractCSSEngine) createEngine()).makeCSSParser(); + CSSParser parser = ((CSSEngineImpl) createEngine()).makeCSSParser(); InputSource source = new InputSource(); source.setCharacterStream(new StringReader(css)); return parser.parseStyleSheet(source); diff --git a/tests/org.eclipse.e4.ui.tests.css.swt/src/org/eclipse/e4/ui/tests/css/swt/TestPropertyHelper.java b/tests/org.eclipse.e4.ui.tests.css.swt/src/org/eclipse/e4/ui/tests/css/swt/TestPropertyHelper.java deleted file mode 100644 index 2b40fc72ff3..00000000000 --- a/tests/org.eclipse.e4.ui.tests.css.swt/src/org/eclipse/e4/ui/tests/css/swt/TestPropertyHelper.java +++ /dev/null @@ -1,84 +0,0 @@ -/******************************************************************************* - * Copyright (c) 2013, 2014 IBM Corporation and others. - * - * This program and the accompanying materials - * are made available under the terms of the Eclipse Public License 2.0 - * which accompanies this distribution, and is available at - * https://www.eclipse.org/legal/epl-2.0/ - * - * SPDX-License-Identifier: EPL-2.0 - * - * Contributors: - * IBM Corporation - initial API and implementation - * Thibault Le Ouay - Bug 443094 - *******************************************************************************/ -package org.eclipse.e4.ui.tests.css.swt; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -import org.eclipse.e4.ui.css.swt.helpers.PropertyHelper; -import org.junit.jupiter.api.Test; - -public class TestPropertyHelper { - public static class Base { - private String a = "A"; - public String getA() { - return a; - } - public void setA(String a) { - this.a = a; - } - - public String getC() { - return "C"; - } - - public boolean isD() { - return true; - } - } - - public static class Impl extends Base { - private String b = "B"; - private Base nested = new Base(); - { - nested.a = "Nested"; - } - - public String getB() { - return b; - } - - public void setB(String b) { - this.b = b; - } - - public Base getNested() { - return nested; - } - - public void setNested(Base nested) { - this.nested = nested; - } - } - - @Test - void testReadWriteProperty() throws Exception { - Impl bean = new Impl(); - assertEquals("A",PropertyHelper.getProperty(bean, "a")); - assertEquals("B",PropertyHelper.getProperty(bean, "b")); - } - - @Test - void testReadOnlyProperty() throws Exception { - Impl bean = new Impl(); - assertEquals("C",PropertyHelper.getProperty(bean, "c")); - assertEquals(true,PropertyHelper.getProperty(bean, "d")); - } - - @Test - void testNestedProperty() throws Exception { - Impl bean = new Impl(); - assertEquals("Nested",PropertyHelper.getProperty(bean, "nested.a")); - } -} diff --git a/tests/org.eclipse.jface.text.tests/src/org/eclipse/jface/text/tests/DefaultTextDoubleClickStrategyTest.java b/tests/org.eclipse.jface.text.tests/src/org/eclipse/jface/text/tests/DefaultTextDoubleClickStrategyTest.java index 450d94d729b..f706ebb32a1 100644 --- a/tests/org.eclipse.jface.text.tests/src/org/eclipse/jface/text/tests/DefaultTextDoubleClickStrategyTest.java +++ b/tests/org.eclipse.jface.text.tests/src/org/eclipse/jface/text/tests/DefaultTextDoubleClickStrategyTest.java @@ -55,6 +55,113 @@ public void testClickAtLineEnd() throws Exception { assertEquals("you", document.get(selection.getOffset(), selection.getLength()), "Unexpected selection"); } + @Test + public void testClickJustPastIdentifierSelectsThatIdentifier() throws Exception { + String content= "foo bar baz"; + IDocument document= new Document(content); + TestSpecificDefaultTextDoubleClickStrategy strategy= new TestSpecificDefaultTextDoubleClickStrategy(); + // Click at offset 3: the space right after "foo". + IRegion selection= strategy.findWord(document, 3); + assertNotNull(selection); + assertEquals("foo", document.get(selection.getOffset(), selection.getLength())); + } + + @Test + public void testClickAtIdentifierStartSelectsWholeIdentifier() throws Exception { + String content= "foo __aaaa bar"; + IDocument document= new Document(content); + TestSpecificDefaultTextDoubleClickStrategy strategy= new TestSpecificDefaultTextDoubleClickStrategy(); + // Click at offset 4: the first '_' starting "__aaaa". + IRegion selection= strategy.findWord(document, 4); + assertNotNull(selection); + assertEquals("__aaaa", document.get(selection.getOffset(), selection.getLength())); + } + + @Test + public void testIdentifierAtLineStartAndEnd() throws Exception { + String content= "_foo___\nbar_baz"; + IDocument document= new Document(content); + TestSpecificDefaultTextDoubleClickStrategy strategy= new TestSpecificDefaultTextDoubleClickStrategy(); + // First line: every offset 0..7 should yield "_foo___". + for (int offset= 0; offset <= 7; offset++) { + IRegion selection= strategy.findWord(document, offset); + assertNotNull(selection, "no selection at offset " + offset); + assertEquals("_foo___", document.get(selection.getOffset(), selection.getLength()), + "unexpected selection at offset " + offset); + } + // Second line. + IRegion selection= strategy.findWord(document, 11); + assertNotNull(selection); + assertEquals("bar_baz", document.get(selection.getOffset(), selection.getLength())); + } + + @Test + public void testSingleLineDocument() throws Exception { + String content= "abc"; + IDocument document= new Document(content); + TestSpecificDefaultTextDoubleClickStrategy strategy= new TestSpecificDefaultTextDoubleClickStrategy(); + IRegion selection= strategy.findWord(document, 0); + assertNotNull(selection); + assertEquals("abc", document.get(selection.getOffset(), selection.getLength())); + selection= strategy.findWord(document, document.getLength()); + assertNotNull(selection); + assertEquals("abc", document.get(selection.getOffset(), selection.getLength())); + } + + @Test + public void testIdentifierSurroundedByPunctuation() throws Exception { + String content= "(foo_bar);"; + IDocument document= new Document(content); + TestSpecificDefaultTextDoubleClickStrategy strategy= new TestSpecificDefaultTextDoubleClickStrategy(); + // Click in the middle of the identifier. + IRegion selection= strategy.findWord(document, 4); + assertNotNull(selection); + assertEquals("foo_bar", document.get(selection.getOffset(), selection.getLength())); + } + + @Test + public void testCjkWordSelection() throws Exception { + // Japanese text without spaces. The word break iterator segments it into a + // Hiragana run ("こんにちは") followed by a Kanji run + // ("世界"). This segmentation is locale-independent, so double-click + // selects the script run the click lands in rather than the whole line. + String content= "こんにちは世界"; + IDocument document= new Document(content); + TestSpecificDefaultTextDoubleClickStrategy strategy= new TestSpecificDefaultTextDoubleClickStrategy(); + // Click inside the Hiragana run. + IRegion selection= strategy.findWord(document, 2); + assertNotNull(selection); + assertEquals("こんにちは", document.get(selection.getOffset(), selection.getLength())); + // Click inside the Kanji run. + selection= strategy.findWord(document, 6); + assertNotNull(selection); + assertEquals("世界", document.get(selection.getOffset(), selection.getLength())); + } + + @Test + public void testCjkTokenBetweenSpaces() throws Exception { + String content= "foo 我是 bar"; + IDocument document= new Document(content); + TestSpecificDefaultTextDoubleClickStrategy strategy= new TestSpecificDefaultTextDoubleClickStrategy(); + // Click inside the CJK token. + IRegion selection= strategy.findWord(document, 5); + assertNotNull(selection); + assertEquals("我是", document.get(selection.getOffset(), selection.getLength())); + } + + @Test + public void testThaiTokenBetweenSpaces() throws Exception { + // Dictionary-based segmentation of a contiguous Thai run only happens under a + // Thai locale, so this test delimits the token with spaces to stay + // locale-independent: double-click selects the whole Thai token. + String content= "foo ไทย bar"; + IDocument document= new Document(content); + TestSpecificDefaultTextDoubleClickStrategy strategy= new TestSpecificDefaultTextDoubleClickStrategy(); + IRegion selection= strategy.findWord(document, 5); + assertNotNull(selection); + assertEquals("ไทย", document.get(selection.getOffset(), selection.getLength())); + } + private static final class TestSpecificDefaultTextDoubleClickStrategy extends DefaultTextDoubleClickStrategy { @Override diff --git a/tests/org.eclipse.jface.text.tests/src/org/eclipse/jface/text/tests/codemining/CodeMiningLineHeaderAnnotationTest.java b/tests/org.eclipse.jface.text.tests/src/org/eclipse/jface/text/tests/codemining/CodeMiningLineHeaderAnnotationTest.java index 1fc1c03b33a..859e3294178 100644 --- a/tests/org.eclipse.jface.text.tests/src/org/eclipse/jface/text/tests/codemining/CodeMiningLineHeaderAnnotationTest.java +++ b/tests/org.eclipse.jface.text.tests/src/org/eclipse/jface/text/tests/codemining/CodeMiningLineHeaderAnnotationTest.java @@ -14,6 +14,7 @@ import static org.junit.jupiter.api.Assertions.assertNotEquals; import java.util.Arrays; +import java.util.List; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Assertions; @@ -22,6 +23,8 @@ import org.eclipse.swt.SWT; import org.eclipse.swt.custom.StyledText; +import org.eclipse.swt.graphics.Font; +import org.eclipse.swt.graphics.FontData; import org.eclipse.swt.graphics.GC; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.layout.FillLayout; @@ -123,4 +126,95 @@ public String getLabel() { // https: //github.com/eclipse-platform/eclipse.platform.ui/issues/2786 assertNotEquals(0, cut.getHeight()); // getHeight should not return 0, otherwise editor content starts jumping around } + + /** + * Verifies that a multi-line line-header code mining contributes a height which is an exact + * multiple of the StyledText's regular line height (plus line spacing). Otherwise the line + * drawn below the code mining would no longer be vertically aligned with the regular text + * lines. This was visible on Windows with Consolas at odd point sizes (e.g. 9, 11, 13), where + * {@code gc.stringExtent(line).y} returned a different value than + * {@link StyledText#getLineHeight()}, so the lines below a multiline code mining were shifted + * by a few pixels relative to the regular grid. + */ + @Test + public void testTwoLineCodeMiningHeightMatchesTextWidgetLineHeight() throws Exception { + var doc= fViewer.getDocument(); + doc.set("line0\nline1\nline2"); + var textWidget= fViewer.getTextWidget(); + // A line spacing != 0 makes a regression in either factor immediately visible. + textWidget.setLineSpacing(3); + // Try to use Consolas at an odd size on Windows since this is the configuration in which + // the original bug was reported. On other platforms / when Consolas is not available the + // test still runs with the default font - the assertions below must hold for any font. + Font consolasFont= tryCreateConsolasFont(textWidget, 11); + try { + if (consolasFont != null) { + textWidget.setFont(consolasFont); + } + String codeMiningLabel= "code mining line1\ncode mining line2"; + var mining= new LineHeaderCodeMining(0, doc, null, null) { + @Override + public String getLabel() { + return codeMiningLabel; + } + }; + int normalLineHeight= textWidget.getLineHeight(); + int lineSpacing= textWidget.getLineSpacing(); + int numMiningLines= codeMiningLabel.split("\n").length; + int expectedMiningHeight= numMiningLines * (normalLineHeight + lineSpacing); + + // 1) The size returned by LineHeaderCodeMining.draw() must be a multiple of the + // StyledText's regular line height + line spacing - it must not depend on + // gc.stringExtent(...).y, which on Consolas/odd sizes differs from getLineHeight(). + var gc= new GC(textWidget); + try { + Point result= mining.draw(gc, textWidget, null, 0, 0); + assertEquals(expectedMiningHeight, result.y, + "two-line code mining height must be 2 * (textWidget.getLineHeight() + lineSpacing)"); + } finally { + gc.dispose(); + } + + // 2) CodeMiningLineHeaderAnnotation.getHeight(GC) drives the line vertical indent of the + // line below the mining (see InlinedAnnotationDrawingStrategy). Its result must equal + // expectedMiningHeight - meaning the first regular text line below the mining ends up + // at exactly that pixel offset above its un-indented position, on the same vertical + // grid as all other text lines. + var annotation= new CodeMiningLineHeaderAnnotation(new Position(0, 0), fViewer); + var support= new InlinedAnnotationSupport(); + support.install(fViewer, new AnnotationPainter(fViewer, null)); + var setSupport= AbstractInlinedAnnotation.class.getDeclaredMethod("setSupport", InlinedAnnotationSupport.class); + setSupport.setAccessible(true); + setSupport.invoke(annotation, support); + annotation.update(List.of(mining), null); + + GC gc2= new GC(textWidget); + try { + int multilineHeight= annotation.getHeight(gc2); + assertEquals(expectedMiningHeight, multilineHeight, + "multiline mining height must be a whole-line multiple of the StyledText line height"); + // The pixel position of the line directly below the code mining is + // (multilineHeight) above its un-indented y position, and that offset must be a + // whole number of regular text line heights so the lines below the mining stay + // aligned with the regular grid. + assertEquals(0, multilineHeight % (normalLineHeight + lineSpacing), + "line below the code mining must land on a regular text-line boundary"); + } finally { + gc2.dispose(); + } + } finally { + if (consolasFont != null) { + textWidget.setFont(null); + consolasFont.dispose(); + } + } + } + + private static Font tryCreateConsolasFont(StyledText textWidget, int height) { + FontData[] available= textWidget.getDisplay().getFontList("Consolas", true); //$NON-NLS-1$ + if (available == null || available.length == 0) { + return null; + } + return new Font(textWidget.getDisplay(), "Consolas", height, SWT.NORMAL); //$NON-NLS-1$ + } } diff --git a/tests/org.eclipse.jface.text.tests/src/org/eclipse/jface/text/tests/rules/DefaultPartitionerTest.java b/tests/org.eclipse.jface.text.tests/src/org/eclipse/jface/text/tests/rules/DefaultPartitionerTest.java index eb807c8ffcc..586e787494a 100644 --- a/tests/org.eclipse.jface.text.tests/src/org/eclipse/jface/text/tests/rules/DefaultPartitionerTest.java +++ b/tests/org.eclipse.jface.text.tests/src/org/eclipse/jface/text/tests/rules/DefaultPartitionerTest.java @@ -21,11 +21,14 @@ @Deprecated public class DefaultPartitionerTest extends FastPartitionerTest { + @Deprecated + @SuppressWarnings("removal") @Override protected IDocumentPartitioner createPartitioner(IPartitionTokenScanner scanner) { return new DefaultPartitioner(scanner, new String[] { DEFAULT, COMMENT }); } + @Deprecated @Override @Test public void testPR130900() throws Exception { diff --git a/tests/org.eclipse.ui.editors.tests/src/org/eclipse/ui/internal/texteditor/stickyscroll/StickyScrollingControlTest.java b/tests/org.eclipse.ui.editors.tests/src/org/eclipse/ui/internal/texteditor/stickyscroll/StickyScrollingControlTest.java index babf3580e22..d55ab75f6cb 100644 --- a/tests/org.eclipse.ui.editors.tests/src/org/eclipse/ui/internal/texteditor/stickyscroll/StickyScrollingControlTest.java +++ b/tests/org.eclipse.ui.editors.tests/src/org/eclipse/ui/internal/texteditor/stickyscroll/StickyScrollingControlTest.java @@ -17,6 +17,7 @@ import static org.hamcrest.Matchers.greaterThan; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -236,6 +237,33 @@ void testStyling() { assertEquals(hoverColor, stickyLineText.getForeground()); } + @Test + void testCanvasBoundsHeightAdjustsForVariableLineHeights() { + sourceViewer.getTextWidget().setBounds(0, 0, 200, 200); + + // Step 1: Set 2 plain-text sticky lines and record canvas height + List plainLines = List.of(new StickyLineStub("line 1", 0), new StickyLineStub("line 2", 1)); + stickyScrollingControl.setStickyLines(plainLines); + Canvas stickyControlCanvas = getStickyControlCanvas(shell); + int heightWithPlainText = stickyControlCanvas.getBounds().height; + + // Step 2: Replace second sticky line with line requiring space + Font largerFont = new Font(Display.getDefault(), + new FontData(shell.getFont().getFontData()[0].getName(), 40, SWT.NORMAL)); + String bigText = "line 2 big"; //$NON-NLS-1$ + StyleRange bigFontRange = new StyleRange(0, bigText.length(), null, null); + bigFontRange.font = largerFont; + List linesWithLargerFont = List.of(new StickyLineStub("line 1", 0), + new StickyLineStub(bigText, 1, new StyleRange[] { bigFontRange })); + stickyScrollingControl.setStickyLines(linesWithLargerFont); + int heightWithLargerFont = getStickyControlCanvas(shell).getBounds().height; + + assertTrue(heightWithLargerFont > heightWithPlainText, + "Canvas height must increase when one sticky line has a larger font"); //$NON-NLS-1$ + + largerFont.dispose(); + } + @Test void testLayoutStickyLinesCanvasOnResize() { sourceViewer.getTextWidget().setBounds(0, 0, 200, 200); diff --git a/tests/org.eclipse.ui.tests.performance/META-INF/MANIFEST.MF b/tests/org.eclipse.ui.tests.performance/META-INF/MANIFEST.MF index 8c0d2929e3d..f7adc266c7a 100644 --- a/tests/org.eclipse.ui.tests.performance/META-INF/MANIFEST.MF +++ b/tests/org.eclipse.ui.tests.performance/META-INF/MANIFEST.MF @@ -19,7 +19,13 @@ Require-Bundle: org.eclipse.ui;bundle-version="3.208.0", org.eclipse.e4.core.contexts, org.eclipse.ui.navigator, org.eclipse.ui.navigator.resources, - org.eclipse.ui.genericeditor + org.eclipse.ui.genericeditor, + org.eclipse.e4.ui.css.core, + org.eclipse.e4.ui.css.swt, + org.eclipse.e4.ui.css.swt.theme, + org.eclipse.ui.themes, + org.eclipse.jdt.core, + org.eclipse.jdt.launching Import-Package: org.junit.jupiter.api;version="[5.14.0,6.0.0)", org.junit.jupiter.api.extension;version="[5.14.0,6.0.0)", org.junit.jupiter.params;version="[5.14.0,6.0.0)", diff --git a/tests/org.eclipse.ui.tests.performance/plugin.xml b/tests/org.eclipse.ui.tests.performance/plugin.xml index 8f051aab1f6..7116adc1286 100644 --- a/tests/org.eclipse.ui.tests.performance/plugin.xml +++ b/tests/org.eclipse.ui.tests.performance/plugin.xml @@ -69,6 +69,13 @@ id="org.eclipse.ui.tests.performance.problemsView" name="Performance Problems View"/> - - + + + + + diff --git a/tests/org.eclipse.ui.tests.performance/src/org/eclipse/ui/tests/performance/CssThemeSwapPerformanceTest.java b/tests/org.eclipse.ui.tests.performance/src/org/eclipse/ui/tests/performance/CssThemeSwapPerformanceTest.java new file mode 100644 index 00000000000..0cb50abbbe9 --- /dev/null +++ b/tests/org.eclipse.ui.tests.performance/src/org/eclipse/ui/tests/performance/CssThemeSwapPerformanceTest.java @@ -0,0 +1,313 @@ +/******************************************************************************* + * Copyright (c) 2026 Lars Vogel and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + *******************************************************************************/ +package org.eclipse.ui.tests.performance; + +import static org.eclipse.ui.tests.harness.util.UITestUtil.processEvents; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IFolder; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IProjectDescription; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.e4.ui.css.core.impl.engine.CSSCorePolicy; +import org.eclipse.e4.ui.css.swt.theme.IThemeEngine; +import org.eclipse.jdt.core.IClasspathEntry; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.JavaCore; +import org.eclipse.jdt.launching.JavaRuntime; +import org.eclipse.swt.widgets.Display; +import org.eclipse.ui.IPerspectiveDescriptor; +import org.eclipse.ui.IViewPart; +import org.eclipse.ui.IWorkbenchPage; +import org.eclipse.ui.IWorkbenchWindow; +import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.ide.IDE; +import org.eclipse.ui.navigator.CommonNavigator; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +/** + * Benchmark for the workbench CSS theme engine. Opens a real workbench with a + * Java project, 20 editors, a CSS stress view (~4000 mixed widgets), and a set + * of standard views, then times repeated theme swaps between the e4 light and + * dark themes. + * + * Not part of the default test run. Invoke explicitly via + * mvn verify -pl :org.eclipse.ui.tests.performance -Pbuild-individual-bundles \ + * -Dtest=CssThemeSwapPerformanceTest -DskipTests=false + */ +@Tag("performance") +public class CssThemeSwapPerformanceTest { + + private static final String LIGHT_THEME = "org.eclipse.e4.ui.css.theme.e4_default"; + private static final String DARK_THEME = "org.eclipse.e4.ui.css.theme.e4_dark"; + private static final String THEME_ENGINE_KEY = "org.eclipse.e4.ui.css.swt.theme"; + + private static final String CSS_STRESS_VIEW = "org.eclipse.ui.tests.performance.cssStressView"; + private static final String PDE_PERSPECTIVE = "org.eclipse.pde.ui.PDEPerspective"; + + /** + * Views opened to make the workbench widget tree look like a real session. + * Some IDs may not be registered in the test runtime; failures are tolerated. + */ + private static final List STANDARD_VIEWS = List.of( + "org.eclipse.ui.navigator.ProjectExplorer", + "org.eclipse.ui.views.ProblemView", + "org.eclipse.ui.views.ContentOutline", + "org.eclipse.ui.views.TaskList", + "org.eclipse.ui.views.BookmarkView", + "org.eclipse.ui.views.AllMarkersView", + "org.eclipse.ui.views.ProgressView", + "org.eclipse.ui.views.PropertySheet", + "org.eclipse.search.ui.views.SearchView", + "org.eclipse.ui.console.ConsoleView", + "org.eclipse.pde.runtime.LogView", + "org.eclipse.team.ui.GenericHistoryView", + "org.eclipse.team.sync.views.SynchronizeView", + "org.eclipse.help.ui.HelpView"); + + private static final int EDITOR_COUNT = 20; + private static final int WARMUP_ROUNDS = 3; + private static final int MEASURE_ROUNDS = 10; + private static final long PRE_SWAP_WAIT_MS = 5_000; + + private IWorkbenchWindow window; + private final List openedViews = new ArrayList<>(); + private IProject project; + + @AfterEach + public void cleanup() throws Exception { + if (window != null) { + IWorkbenchPage page = window.getActivePage(); + if (page != null) { + page.closeAllEditors(false); + for (IViewPart v : openedViews) { + page.hideView(v); + } + processEvents(); + } + } + if (project != null && project.exists()) { + project.delete(true, true, null); + } + } + + @Test + public void themeSwap_realWorkbench() throws Exception { + window = PlatformUI.getWorkbench().getActiveWorkbenchWindow(); + assertNotNull(window, "Active workbench window must be available"); + window.getShell().setMaximized(true); + IWorkbenchPage page = window.getActivePage(); + + IPerspectiveDescriptor pde = PlatformUI.getWorkbench().getPerspectiveRegistry() + .findPerspectiveWithId(PDE_PERSPECTIVE); + if (pde != null) { + page.setPerspective(pde); + System.out.println("[CSS-PERF] switched to PDE perspective"); + } else { + System.out.println("[CSS-PERF] PDE perspective not available; keeping current (" + + (page.getPerspective() == null ? "" : page.getPerspective().getId()) + ")"); + } + processEvents(); + + List files = createJavaProjectWithFiles("CssPerf", EDITOR_COUNT); + for (IFile f : files) { + IDE.openEditor(page, f); + } + System.out.println("[CSS-PERF] opened " + files.size() + " editors"); + + IViewPart cssView = page.showView(CSS_STRESS_VIEW); + openedViews.add(cssView); + IViewPart projectExplorer = null; + int viewsOpened = 1; + for (String id : STANDARD_VIEWS) { + try { + IViewPart v = page.showView(id); + openedViews.add(v); + if ("org.eclipse.ui.navigator.ProjectExplorer".equals(id)) { + projectExplorer = v; + } + viewsOpened++; + } catch (Exception e) { + System.out.println("[CSS-PERF] skipping view " + id + ": " + e.getMessage()); + } + } + System.out.println("[CSS-PERF] opened " + viewsOpened + " views"); + + // keep the CSS stress view on top of its stack even after activating + // the project explorer + page.showView(CSS_STRESS_VIEW); + + if (projectExplorer != null) { + if (projectExplorer instanceof CommonNavigator nav) { + nav.getCommonViewer().expandToLevel(3); + } + page.activate(projectExplorer); + } + processEvents(); + + Display display = window.getShell().getDisplay(); + + System.out.println("[CSS-PERF] workbench populated; waiting " + + (PRE_SWAP_WAIT_MS / 1000) + "s for things to settle before the theme swap..."); + pumpEvents(display, PRE_SWAP_WAIT_MS); + + IThemeEngine themeEngine = (IThemeEngine) display.getData(THEME_ENGINE_KEY); + assertNotNull(themeEngine, "IThemeEngine must be available on the workbench Display"); + + System.out.println("[CSS-PERF] registered themes:"); + for (var t : themeEngine.getThemes()) { + System.out.println("[CSS-PERF] - " + t.getId() + " (" + t.getLabel() + ")"); + } + System.out.println("[CSS-PERF] active theme: " + + (themeEngine.getActiveTheme() == null ? "" : themeEngine.getActiveTheme().getId())); + + boolean hasDark = themeEngine.getThemes().stream().anyMatch(t -> DARK_THEME.equals(t.getId())); + boolean hasLight = themeEngine.getThemes().stream().anyMatch(t -> LIGHT_THEME.equals(t.getId())); + + String originalTheme = themeEngine.getActiveTheme() == null + ? LIGHT_THEME + : themeEngine.getActiveTheme().getId(); + + CSSCorePolicy.setPerfEnabled(true); + CSSCorePolicy.reset(); + if (hasDark && hasLight) { + runThemeSwapBenchmark(themeEngine); + } else { + runApplyStylesBenchmark(themeEngine, window); + } + System.out.print(CSSCorePolicy.summary()); + CSSCorePolicy.setPerfEnabled(false); + + swap(themeEngine, originalTheme); + } + + private List createJavaProjectWithFiles(String name, int fileCount) throws Exception { + IProject p = ResourcesPlugin.getWorkspace().getRoot().getProject(name); + if (p.exists()) { + p.delete(true, true, null); + } + p.create(null); + p.open(null); + IProjectDescription desc = p.getDescription(); + desc.setNatureIds(new String[] { JavaCore.NATURE_ID }); + p.setDescription(desc, null); + + IFolder src = p.getFolder("src"); + src.create(true, true, null); + + IJavaProject jp = JavaCore.create(p); + IClasspathEntry[] cp = new IClasspathEntry[] { + JavaCore.newSourceEntry(src.getFullPath()), + JavaRuntime.getDefaultJREContainerEntry() }; + jp.setRawClasspath(cp, p.getFolder("bin").getFullPath(), null); + + List files = new ArrayList<>(fileCount); + for (int i = 0; i < fileCount; i++) { + IFile file = src.getFile("PerfClass" + i + ".java"); + String content = """ + public class PerfClass%d { + private int counter; + public void method1() { + for (int i = 0; i < 10; i++) { + counter++; + } + } + public String greet(String name) { + return "hello " + name; + } + public int square(int x) { + return x * x; + } + } + """.formatted(i); + file.create(new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8)), true, null); + files.add(file); + } + project = p; + return files; + } + + private void runThemeSwapBenchmark(IThemeEngine themeEngine) { + for (int i = 0; i < WARMUP_ROUNDS; i++) { + swap(themeEngine, i % 2 == 0 ? DARK_THEME : LIGHT_THEME); + } + long[] samples = new long[MEASURE_ROUNDS]; + for (int i = 0; i < MEASURE_ROUNDS; i++) { + String target = i % 2 == 0 ? DARK_THEME : LIGHT_THEME; + long t0 = System.nanoTime(); + swap(themeEngine, target); + samples[i] = System.nanoTime() - t0; + } + report("theme swap dark<->light (real workbench)", samples); + } + + private void runApplyStylesBenchmark(IThemeEngine themeEngine, IWorkbenchWindow window) { + System.out.println("[CSS-PERF] dark/light themes not available; falling back to applyStyles benchmark"); + for (int i = 0; i < WARMUP_ROUNDS; i++) { + themeEngine.applyStyles(window.getShell(), true); + processEvents(); + } + long[] samples = new long[MEASURE_ROUNDS]; + for (int i = 0; i < MEASURE_ROUNDS; i++) { + long t0 = System.nanoTime(); + themeEngine.applyStyles(window.getShell(), true); + processEvents(); + samples[i] = System.nanoTime() - t0; + } + report("applyStyles full workbench shell", samples); + } + + private void swap(IThemeEngine engine, String themeId) { + engine.setTheme(themeId, false); + processEvents(); + } + + private void pumpEvents(Display display, long millis) { + long endAt = System.currentTimeMillis() + millis; + while (System.currentTimeMillis() < endAt) { + if (!display.readAndDispatch()) { + try { + Thread.sleep(10); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return; + } + } + } + } + + private void report(String label, long[] ns) { + long[] sorted = ns.clone(); + Arrays.sort(sorted); + long min = sorted[0]; + long median = sorted[sorted.length / 2]; + long p95 = sorted[(int) Math.min(sorted.length - 1, sorted.length * 0.95)]; + long max = sorted[sorted.length - 1]; + long sum = 0; + for (long v : ns) { + sum += v; + } + double meanMs = sum / 1_000_000.0 / ns.length; + System.out.printf( + "[CSS-PERF] %s rounds=%d min=%.2fms median=%.2fms p95=%.2fms max=%.2fms mean=%.2fms%n", + label, ns.length, + min / 1e6, median / 1e6, p95 / 1e6, max / 1e6, meanMs); + } +} diff --git a/tests/org.eclipse.ui.tests.performance/src/org/eclipse/ui/tests/performance/OpenProjectExplorerFolderTest.java b/tests/org.eclipse.ui.tests.performance/src/org/eclipse/ui/tests/performance/OpenProjectExplorerFolderTest.java index fcef754b640..4ce3b7f548d 100644 --- a/tests/org.eclipse.ui.tests.performance/src/org/eclipse/ui/tests/performance/OpenProjectExplorerFolderTest.java +++ b/tests/org.eclipse.ui.tests.performance/src/org/eclipse/ui/tests/performance/OpenProjectExplorerFolderTest.java @@ -13,7 +13,7 @@ *******************************************************************************/ package org.eclipse.ui.tests.performance; -import static org.junit.Assert.fail; +import static org.junit.jupiter.api.Assertions.fail; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; @@ -28,14 +28,14 @@ import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.NullProgressMonitor; import org.eclipse.core.runtime.Platform; -import org.eclipse.test.performance.PerformanceTestCaseJunit4; +import org.eclipse.test.performance.PerformanceTestCaseJunit5; import org.eclipse.ui.IViewPart; import org.eclipse.ui.IWorkbenchPage; import org.eclipse.ui.PartInitException; import org.eclipse.ui.PlatformUI; import org.eclipse.ui.navigator.resources.ProjectExplorer; -import org.junit.ClassRule; -import org.junit.Test; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; import org.osgi.framework.Bundle; @@ -61,10 +61,10 @@ * "sleep" to simulate computations, it only effects Elapsed Time (not CPU * Time). */ -public class OpenProjectExplorerFolderTest extends PerformanceTestCaseJunit4 { +public class OpenProjectExplorerFolderTest extends PerformanceTestCaseJunit5 { - @ClassRule - public static final UIPerformanceTestRule uiPerformanceTestRule = new UIPerformanceTestRule(); + @RegisterExtension + static UIPerformanceTestRule uiPerformanceTestRule = new UIPerformanceTestRule(); /* * performance testcase for bug 106158 diff --git a/tests/org.eclipse.ui.tests.performance/src/org/eclipse/ui/tests/performance/parts/CssStressViewPart.java b/tests/org.eclipse.ui.tests.performance/src/org/eclipse/ui/tests/performance/parts/CssStressViewPart.java new file mode 100644 index 00000000000..bced017b851 --- /dev/null +++ b/tests/org.eclipse.ui.tests.performance/src/org/eclipse/ui/tests/performance/parts/CssStressViewPart.java @@ -0,0 +1,209 @@ +/******************************************************************************* + * Copyright (c) 2026 Lars Vogel and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + *******************************************************************************/ +package org.eclipse.ui.tests.performance.parts; + +import org.eclipse.e4.ui.css.swt.dom.WidgetElement; +import org.eclipse.swt.SWT; +import org.eclipse.swt.custom.CCombo; +import org.eclipse.swt.custom.CLabel; +import org.eclipse.swt.custom.ScrolledComposite; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Combo; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.DateTime; +import org.eclipse.swt.widgets.ExpandBar; +import org.eclipse.swt.widgets.ExpandItem; +import org.eclipse.swt.widgets.Group; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Link; +import org.eclipse.swt.widgets.List; +import org.eclipse.swt.widgets.ProgressBar; +import org.eclipse.swt.widgets.Scale; +import org.eclipse.swt.widgets.Spinner; +import org.eclipse.swt.widgets.Table; +import org.eclipse.swt.widgets.TableColumn; +import org.eclipse.swt.widgets.TableItem; +import org.eclipse.swt.widgets.Text; +import org.eclipse.swt.widgets.ToolBar; +import org.eclipse.swt.widgets.ToolItem; +import org.eclipse.swt.widgets.Tree; +import org.eclipse.swt.widgets.TreeItem; +import org.eclipse.ui.part.ViewPart; + +/** + * View used by CSS theme-swap performance tests. Builds a large, varied widget + * tree so the CSS engine has meaningful work to do when a theme is applied. + */ +public class CssStressViewPart extends ViewPart { + + public static final String ID = "org.eclipse.ui.tests.performance.cssStressView"; + + private static final int WIDGET_COUNT = 4000; + private static final int GROUP_SIZE = 40; + + private Composite root; + + @Override + public void createPartControl(Composite parent) { + ScrolledComposite scroll = new ScrolledComposite(parent, SWT.V_SCROLL | SWT.H_SCROLL); + root = new Composite(scroll, SWT.NONE); + root.setLayout(new GridLayout(8, true)); + buildTree(root, WIDGET_COUNT); + scroll.setContent(root); + scroll.setExpandHorizontal(true); + scroll.setExpandVertical(true); + scroll.setMinSize(root.computeSize(SWT.DEFAULT, SWT.DEFAULT)); + } + + private void buildTree(Composite parent, int target) { + int remaining = target; + int groupIndex = 0; + while (remaining > 0) { + Group group = new Group(parent, SWT.NONE); + group.setText("Group " + groupIndex); + group.setLayout(new GridLayout(4, false)); + int n = Math.min(GROUP_SIZE, remaining); + for (int i = 0; i < n; i++) { + addWidget(group, (groupIndex * GROUP_SIZE + i) % 14); + } + remaining -= n; + groupIndex++; + } + } + + private void addWidget(Composite parent, int kind) { + switch (kind) { + case 0: + Label l = new Label(parent, SWT.NONE); + l.setText("label-" + kind); + if (kind % 7 == 0) { + WidgetElement.setCSSClass(l, "hot"); + } + break; + case 1: + Button push = new Button(parent, SWT.PUSH); + push.setText("push"); + break; + case 2: + Button check = new Button(parent, SWT.CHECK); + check.setText("check"); + break; + case 3: + Button radio = new Button(parent, SWT.RADIO); + radio.setText("radio"); + break; + case 4: + Text text = new Text(parent, SWT.BORDER); + text.setText("text"); + break; + case 5: + Combo combo = new Combo(parent, SWT.READ_ONLY); + combo.setItems("a", "b", "c"); + combo.select(0); + break; + case 6: + Spinner spinner = new Spinner(parent, SWT.BORDER); + spinner.setSelection(5); + break; + case 7: + List list = new List(parent, SWT.BORDER | SWT.SINGLE); + list.setItems("item1", "item2", "item3"); + GridData ld = new GridData(); + ld.heightHint = 40; + list.setLayoutData(ld); + break; + case 8: + Link link = new Link(parent, SWT.NONE); + link.setText("linked"); + break; + case 9: + ProgressBar pb = new ProgressBar(parent, SWT.HORIZONTAL); + pb.setSelection(40); + break; + case 10: + CLabel clabel = new CLabel(parent, SWT.NONE); + clabel.setText("clabel"); + break; + case 11: + CCombo ccombo = new CCombo(parent, SWT.BORDER); + ccombo.setItems(new String[] { "x", "y", "z" }); + ccombo.select(0); + break; + case 12: + Scale scale = new Scale(parent, SWT.HORIZONTAL); + scale.setSelection(30); + break; + case 13: + DateTime dt = new DateTime(parent, SWT.DATE | SWT.SHORT); + // no extra setup + if (dt == null) { + throw new IllegalStateException(); + } + break; + default: + break; + } + + if (kind == 4) { + // once per group cycle, add a small Tree / Table / ExpandBar / ToolBar so + // the engine sees container widgets too + addContainerWidgets(parent); + } + } + + private void addContainerWidgets(Composite parent) { + ToolBar tb = new ToolBar(parent, SWT.FLAT); + new ToolItem(tb, SWT.PUSH).setText("a"); + new ToolItem(tb, SWT.SEPARATOR); + new ToolItem(tb, SWT.PUSH).setText("b"); + + Tree tree = new Tree(parent, SWT.BORDER); + GridData td = new GridData(); + td.heightHint = 50; + tree.setLayoutData(td); + TreeItem root = new TreeItem(tree, SWT.NONE); + root.setText("root"); + new TreeItem(root, SWT.NONE).setText("child-1"); + new TreeItem(root, SWT.NONE).setText("child-2"); + + Table table = new Table(parent, SWT.BORDER | SWT.FULL_SELECTION); + table.setHeaderVisible(true); + TableColumn c1 = new TableColumn(table, SWT.LEFT); + c1.setText("name"); + c1.setWidth(60); + TableColumn c2 = new TableColumn(table, SWT.LEFT); + c2.setText("value"); + c2.setWidth(60); + TableItem ti = new TableItem(table, SWT.NONE); + ti.setText(new String[] { "k1", "v1" }); + GridData tabd = new GridData(); + tabd.heightHint = 50; + table.setLayoutData(tabd); + + ExpandBar bar = new ExpandBar(parent, SWT.NONE); + ExpandItem ei = new ExpandItem(bar, SWT.NONE); + ei.setText("section"); + Composite inner = new Composite(bar, SWT.NONE); + inner.setLayout(new GridLayout(1, false)); + new Label(inner, SWT.NONE).setText("inner"); + ei.setControl(inner); + ei.setHeight(40); + } + + @Override + public void setFocus() { + if (root != null && !root.isDisposed()) { + root.setFocus(); + } + } +} diff --git a/tests/org.eclipse.ui.tests/plugin.xml b/tests/org.eclipse.ui.tests/plugin.xml index eb2e3396754..5dbc053ba98 100644 --- a/tests/org.eclipse.ui.tests/plugin.xml +++ b/tests/org.eclipse.ui.tests/plugin.xml @@ -15,12 +15,6 @@ - - - - - + diff --git a/tests/org.eclipse.ui.workbench.texteditor.tests/META-INF/MANIFEST.MF b/tests/org.eclipse.ui.workbench.texteditor.tests/META-INF/MANIFEST.MF index c39e7b8fbdd..8fc2605ffed 100644 --- a/tests/org.eclipse.ui.workbench.texteditor.tests/META-INF/MANIFEST.MF +++ b/tests/org.eclipse.ui.workbench.texteditor.tests/META-INF/MANIFEST.MF @@ -22,9 +22,10 @@ Require-Bundle: Bundle-RequiredExecutionEnvironment: JavaSE-21 Eclipse-BundleShape: dir Automatic-Module-Name: org.eclipse.ui.workbench.texteditor.tests -Import-Package: org.mockito, - org.mockito.stubbing;version="5.5.0", - org.junit.jupiter.api;version="[5.14.0,6.0.0)", +Import-Package: org.junit.jupiter.api;version="[5.14.0,6.0.0)", org.junit.jupiter.api.function;version="[5.14.0,6.0.0)", org.junit.platform.suite.api;version="[1.14.0,2.0.0)", + org.mockito, + org.mockito.invocation;version="[5.23.0,6.0.0)", + org.mockito.stubbing;version="5.5.0", org.opentest4j;version="[1.3.0,2.0.0)" diff --git a/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/internal/findandreplace/FindReplaceUITest.java b/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/internal/findandreplace/FindReplaceUITest.java index c253586ccc3..efca3df0db4 100644 --- a/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/internal/findandreplace/FindReplaceUITest.java +++ b/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/internal/findandreplace/FindReplaceUITest.java @@ -376,6 +376,80 @@ public void testReplaceIfSelectedOnStart() { assertThat(fTextViewer.getDocument().get(), is("abaaefg")); } + @Test + public void testSearchTermStoredInHistoryAfterSearch() { + // Performing a search must persist the search term so that the user can + // navigate back to it via history in a subsequent session. + initializeTextViewerWithFindReplaceUI("foo bar foo"); + dialog.setFindText("foo"); + dialog.simulateKeyboardInteractionInFindInputField(SWT.CR, false); + + dialog.selectFindHistoryEntry(0); + assertEquals("foo", dialog.getFindText()); + } + + @Test + public void testSearchHistoryContainsAllRecentTermsNewestFirst() { + // Multiple searches must all appear in history ordered newest-first, so that + // index 0 always yields the most recently used term. + initializeTextViewerWithFindReplaceUI("foo bar baz foo bar baz"); + dialog.setFindText("foo"); + dialog.simulateKeyboardInteractionInFindInputField(SWT.CR, false); + dialog.setFindText("bar"); + dialog.simulateKeyboardInteractionInFindInputField(SWT.CR, false); + + dialog.selectFindHistoryEntry(0); + assertEquals("bar", dialog.getFindText()); + + dialog.selectFindHistoryEntry(1); + assertEquals("foo", dialog.getFindText()); + } + + @Test + public void testSearchHistoryDeduplicatesRepeatedSearchTerms() { + // Searching for the same term twice must not create a duplicate entry in + // history. If it did, index 1 would show "foo" again instead of "bar". + initializeTextViewerWithFindReplaceUI("foo bar foo bar"); + dialog.setFindText("bar"); + dialog.simulateKeyboardInteractionInFindInputField(SWT.CR, false); + dialog.setFindText("foo"); + dialog.simulateKeyboardInteractionInFindInputField(SWT.CR, false); + // Search "foo" a second time — must not insert a second "foo" entry. + dialog.setFindText("foo"); + dialog.simulateKeyboardInteractionInFindInputField(SWT.CR, false); + + dialog.selectFindHistoryEntry(0); + assertEquals("foo", dialog.getFindText()); + // A duplicate "foo" would appear here instead of "bar". + dialog.selectFindHistoryEntry(1); + assertEquals("bar", dialog.getFindText()); + } + + @Test + public void testSearchTermStoredInHistoryAfterReplaceAll() { + // A replace-all operation must persist the search term to history so that + // it is available for future searches. + initializeTextViewerWithFindReplaceUI("foo foo foo"); + dialog.setFindText("foo"); + dialog.setReplaceText("bar"); + dialog.performReplaceAll(); + + dialog.selectFindHistoryEntry(0); + assertEquals("foo", dialog.getFindText()); + } + + @Test + public void testSearchTermStoredInHistoryAfterSingleReplace() { + // A single replace operation must also persist the search term to history. + initializeTextViewerWithFindReplaceUI("foo bar"); + dialog.setFindText("foo"); + dialog.setReplaceText("baz"); + dialog.performReplace(); + + dialog.selectFindHistoryEntry(0); + assertEquals("foo", dialog.getFindText()); + } + protected AccessType getDialog() { return dialog; } diff --git a/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/internal/findandreplace/HistoryStoreTest.java b/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/internal/findandreplace/HistoryStoreTest.java new file mode 100644 index 00000000000..ddc5cdd285d --- /dev/null +++ b/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/internal/findandreplace/HistoryStoreTest.java @@ -0,0 +1,255 @@ +/******************************************************************************* + * Copyright (c) 2025 Vector Informatik GmbH and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Vector Informatik GmbH - initial API and implementation + *******************************************************************************/ +package org.eclipse.ui.internal.findandreplace; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import org.eclipse.jface.dialogs.IDialogSettings; + +public class HistoryStoreTest { + + /** + * Returns a minimal {@link IDialogSettings} stub that stores and retrieves + * {@code String[]} values in memory, keyed by section name. All other + * {@code IDialogSettings} methods are left as Mockito no-ops / default returns. + */ + private IDialogSettings createInMemoryDialogSettings() { + Map store = new HashMap<>(); + IDialogSettings settings = mock(IDialogSettings.class); + when(settings.getArray(anyString())).thenAnswer(inv -> store.get(inv.getArgument(0))); + doAnswer(inv -> { + store.put(inv.getArgument(0), inv.getArgument(1)); + return null; + }).when(settings).put(anyString(), any(String[].class)); + return settings; + } + + @Test + public void testConstructorThrowsOnNullSectionName() { + assertThrows(IllegalStateException.class, + () -> new HistoryStore(createInMemoryDialogSettings(), null, 5)); + } + + @Test + public void testNewStoreIsEmpty() { + HistoryStore store = new HistoryStore(createInMemoryDialogSettings(), "section", 5); + assertTrue(store.isEmpty()); + assertEquals(0, store.size()); + } + + @Test + public void testAddSingleItem() { + HistoryStore store = new HistoryStore(createInMemoryDialogSettings(), "section", 5); + + store.add("item"); + + assertFalse(store.isEmpty()); + assertEquals(1, store.size()); + assertEquals("item", store.get(0)); + } + + @Test + public void testAddNullIsIgnored() { + // Null items must not be stored; history stays empty. + HistoryStore store = new HistoryStore(createInMemoryDialogSettings(), "section", 5); + + store.add(null); + + assertTrue(store.isEmpty()); + } + + @Test + public void testAddEmptyStringIsIgnored() { + // Empty strings must not be stored; an empty search term is not useful history. + HistoryStore store = new HistoryStore(createInMemoryDialogSettings(), "section", 5); + + store.add(""); + + assertTrue(store.isEmpty()); + } + + @Test + public void testMostRecentlyAddedItemIsFirst() { + // The most recently added item should appear at index 0 so that history + // navigation reaches recent searches first. + HistoryStore store = new HistoryStore(createInMemoryDialogSettings(), "section", 5); + + store.add("first"); + store.add("second"); + store.add("third"); + + assertEquals("third", store.get(0)); + assertEquals("second", store.get(1)); + assertEquals("first", store.get(2)); + assertEquals(3, store.size()); + } + + @Test + public void testAddingExistingItemMovesItToFront() { + // Re-adding a term already in history should move it to index 0 without + // creating a duplicate. This mirrors how every search toolbar works: the most + // recently used term comes first. + HistoryStore store = new HistoryStore(createInMemoryDialogSettings(), "section", 5); + store.add("first"); + store.add("second"); + + store.add("first"); + + assertEquals(2, store.size()); + assertEquals("first", store.get(0)); + assertEquals("second", store.get(1)); + } + + @Test + public void testAddingItemAlreadyAtFrontKeepsItThere() { + HistoryStore store = new HistoryStore(createInMemoryDialogSettings(), "section", 5); + store.add("item"); + + store.add("item"); + + assertEquals(1, store.size()); + assertEquals("item", store.get(0)); + } + + @Test + public void testHistorySizeLimitDropsOldestEntries() { + // Once the capacity is reached, the oldest (highest-index) entry must be + // dropped to make room for the new one. + HistoryStore store = new HistoryStore(createInMemoryDialogSettings(), "section", 3); + store.add("first"); + store.add("second"); + store.add("third"); + + store.add("fourth"); + + assertEquals(3, store.size()); + assertEquals("fourth", store.get(0)); + assertEquals("third", store.get(1)); + assertEquals("second", store.get(2)); + } + + @Test + public void testRemoveExistingItem() { + HistoryStore store = new HistoryStore(createInMemoryDialogSettings(), "section", 5); + store.add("first"); + store.add("second"); + + store.remove("first"); + + assertEquals(1, store.size()); + assertEquals("second", store.get(0)); + } + + @Test + public void testRemoveItemAtFront() { + HistoryStore store = new HistoryStore(createInMemoryDialogSettings(), "section", 5); + store.add("first"); + store.add("second"); + + store.remove("second"); + + assertEquals(1, store.size()); + assertEquals("first", store.get(0)); + } + + @Test + public void testRemoveNonExistentItemIsNoOp() { + // Removing a term that was never stored must not alter the history. + HistoryStore store = new HistoryStore(createInMemoryDialogSettings(), "section", 5); + store.add("item"); + + store.remove("nonexistent"); + + assertEquals(1, store.size()); + assertEquals("item", store.get(0)); + } + + @Test + public void testIndexOfReturnsCorrectPositions() { + HistoryStore store = new HistoryStore(createInMemoryDialogSettings(), "section", 5); + store.add("first"); + store.add("second"); + store.add("third"); + + assertEquals(0, store.indexOf("third")); + assertEquals(1, store.indexOf("second")); + assertEquals(2, store.indexOf("first")); + } + + @Test + public void testIndexOfReturnsMinusOneForAbsentItem() { + HistoryStore store = new HistoryStore(createInMemoryDialogSettings(), "section", 5); + store.add("item"); + + assertEquals(-1, store.indexOf("nonexistent")); + } + + @Test + public void testGetIterableReturnsItemsNewestFirst() { + HistoryStore store = new HistoryStore(createInMemoryDialogSettings(), "section", 5); + store.add("first"); + store.add("second"); + + List items = new ArrayList<>(); + for (String item : store.get()) { + items.add(item); + } + + assertEquals(List.of("second", "first"), items); + } + + @Test + public void testHistoryPersistedAcrossInstances() { + // Two HistoryStore instances pointing to the same IDialogSettings section must + // share the same data, modelling persistence across workbench sessions. + IDialogSettings sharedSettings = createInMemoryDialogSettings(); + HistoryStore store1 = new HistoryStore(sharedSettings, "section", 5); + store1.add("first"); + store1.add("second"); + + HistoryStore store2 = new HistoryStore(sharedSettings, "section", 5); + + assertEquals(2, store2.size()); + assertEquals("second", store2.get(0)); + assertEquals("first", store2.get(1)); + } + + @Test + public void testDistinctSectionsAreIndependent() { + // Two stores sharing the same IDialogSettings but using different section names + // must not interfere with each other (models separate find/replace histories). + IDialogSettings sharedSettings = createInMemoryDialogSettings(); + HistoryStore findHistory = new HistoryStore(sharedSettings, "findhistory", 5); + HistoryStore replaceHistory = new HistoryStore(sharedSettings, "replacehistory", 5); + findHistory.add("findterm"); + + assertTrue(replaceHistory.isEmpty()); + } + +} diff --git a/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/internal/findandreplace/IFindReplaceUIAccess.java b/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/internal/findandreplace/IFindReplaceUIAccess.java index 57a4edd6f04..55d60f7f685 100644 --- a/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/internal/findandreplace/IFindReplaceUIAccess.java +++ b/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/internal/findandreplace/IFindReplaceUIAccess.java @@ -49,6 +49,8 @@ public interface IFindReplaceUIAccess { void performReplaceAndFind(); + void selectFindHistoryEntry(int index); + void assertInitialConfiguration(); void assertUnselected(SearchOptions option); diff --git a/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/internal/findandreplace/overlay/FindReplaceOverlayTest.java b/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/internal/findandreplace/overlay/FindReplaceOverlayTest.java index ee073791589..e74eb92d3b4 100644 --- a/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/internal/findandreplace/overlay/FindReplaceOverlayTest.java +++ b/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/internal/findandreplace/overlay/FindReplaceOverlayTest.java @@ -21,6 +21,7 @@ import org.junit.jupiter.api.Test; +import org.eclipse.swt.SWT; import org.eclipse.swt.graphics.Point; import org.eclipse.core.runtime.preferences.IEclipsePreferences; @@ -184,4 +185,34 @@ public void testDisableOverlayViaPreference() { } } + @Test + public void testSearchTermStoredInHistoryAfterSearchForward() { + // After a forward search, the term must be retrievable from history so that + // the user can navigate back to it in a subsequent session. + initializeTextViewerWithFindReplaceUI("foo bar foo"); + OverlayAccess dialog= getDialog(); + dialog.setFindText("foo"); + dialog.pressSearch(true); + + // Down-arrow navigates to the most recently stored entry (index 0). + dialog.setFindText(""); + dialog.simulateKeyboardInteractionInFindInputField(SWT.ARROW_DOWN, false); + + assertEquals("foo", dialog.getFindText()); + } + + @Test + public void testSearchTermStoredInHistoryAfterSearchBackward() { + // Backward search must persist the term to history just like forward search. + initializeTextViewerWithFindReplaceUI("foo bar foo"); + OverlayAccess dialog= getDialog(); + dialog.setFindText("foo"); + dialog.pressSearch(false); + + dialog.setFindText(""); + dialog.simulateKeyboardInteractionInFindInputField(SWT.ARROW_DOWN, false); + + assertEquals("foo", dialog.getFindText()); + } + } diff --git a/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/internal/findandreplace/overlay/OverlayAccess.java b/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/internal/findandreplace/overlay/OverlayAccess.java index ae07a9643ec..16de0671b77 100644 --- a/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/internal/findandreplace/overlay/OverlayAccess.java +++ b/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/internal/findandreplace/overlay/OverlayAccess.java @@ -216,6 +216,17 @@ private Predicate isOptionSelected() { }; } + @Override + public void selectFindHistoryEntry(int index) { + // Clear the field directly (no SWT.Modify, so no incremental search fires), + // then step down once per index position. From an empty field ARROW_DOWN + // always lands on index 0 (newest); each additional press moves one step older. + find.setText(""); + for (int i = 0; i <= index; i++) { + simulateKeyboardInteractionInFindInputField(SWT.ARROW_DOWN, false); + } + } + public void pressSearch(boolean forward) { if (forward) { searchForward.notifyListeners(SWT.Selection, null); diff --git a/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/workbench/texteditor/tests/DialogAccess.java b/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/workbench/texteditor/tests/DialogAccess.java index 29247c3c0f1..9d5a10dddb0 100644 --- a/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/workbench/texteditor/tests/DialogAccess.java +++ b/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/workbench/texteditor/tests/DialogAccess.java @@ -230,6 +230,11 @@ public void performReplace() { } } + @Override + public void selectFindHistoryEntry(int index) { + findCombo.select(index); + } + public void performFindNext() { if (findNextButton.getEnabled()) { findNextButton.notifyListeners(SWT.Selection, null); diff --git a/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/workbench/texteditor/tests/WorkbenchTextEditorTestSuite.java b/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/workbench/texteditor/tests/WorkbenchTextEditorTestSuite.java index 30ba98d200c..d2ad80884ed 100644 --- a/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/workbench/texteditor/tests/WorkbenchTextEditorTestSuite.java +++ b/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/workbench/texteditor/tests/WorkbenchTextEditorTestSuite.java @@ -17,6 +17,7 @@ import org.junit.platform.suite.api.SelectClasses; import org.eclipse.ui.internal.findandreplace.FindReplaceLogicTest; +import org.eclipse.ui.internal.findandreplace.HistoryStoreTest; import org.eclipse.ui.internal.findandreplace.overlay.FindReplaceOverlayTest; import org.eclipse.ui.workbench.texteditor.tests.minimap.MinimapPageTest; @@ -48,6 +49,7 @@ FindReplaceDialogTest.class, FindReplaceOverlayTest.class, FindReplaceLogicTest.class, + HistoryStoreTest.class, }) public class WorkbenchTextEditorTestSuite { // see @SelectClasses