package org.fxmisc.cssfx.impl; /* * #%L * CSSFX * %% * Copyright (C) 2014 CSSFX by Matthieu Brouillard * %% * 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. * #L% */ import static org.fxmisc.cssfx.impl.log.CSSFXLogger.logger; import java.nio.file.Path; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.CopyOnWriteArrayList; import javafx.application.Platform; import javafx.beans.property.IntegerProperty; import javafx.beans.property.ObjectProperty; import javafx.beans.property.ReadOnlyObjectProperty; import javafx.beans.property.SimpleIntegerProperty; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; import javafx.scene.Node; import javafx.scene.Parent; import javafx.scene.Scene; import javafx.stage.Stage; import org.fxmisc.cssfx.api.URIToPathConverter; import org.fxmisc.cssfx.impl.events.CSSFXEvent; import org.fxmisc.cssfx.impl.events.CSSFXEvent.EventType; import org.fxmisc.cssfx.impl.events.CSSFXEventListener; import org.fxmisc.cssfx.impl.monitoring.PathsWatcher; /** * CSSFXMonitor is the central controller of the CSS monitoring feature. * * @author Matthieu Brouillard */ public class CSSFXMonitor { private PathsWatcher pw; // keep insertion order private List<URIToPathConverter> knownConverters = new CopyOnWriteArrayList<>(); private ObservableList<Stage> stages; private ObservableList<Scene> scenes; private ObservableList<Node> nodes; private List<CSSFXEventListener> eventListeners = new CopyOnWriteArrayList<>(); public CSSFXMonitor() { } public void setStages(ObservableList<Stage> stages) { this.stages = stages; } public void setScenes(ObservableList<Scene> scenes) { this.scenes = scenes; } public void setNodes(ObservableList<Node> nodes) { this.nodes = nodes; } public void addAllConverters(Collection<URIToPathConverter> converters) { knownConverters.addAll(converters); } public void addAllConverters(URIToPathConverter... converters) { knownConverters.addAll(Arrays.asList(converters)); } public void addConverter(URIToPathConverter newConverter) { knownConverters.add(newConverter); } public void removeConverter(URIToPathConverter converter) { knownConverters.remove(converter); } public void addEventListener(CSSFXEventListener listener) { eventListeners.add(listener); } public void removeEventListener(CSSFXEventListener listener) { eventListeners.remove(listener); } public void start() { logger(CSSFXMonitor.class).info("CSS Monitoring is about to start"); pw = new PathsWatcher(); // start to monitor stage changes if (stages != null) { monitorStages(stages); } else if (scenes != null) { monitorScenes(scenes); } else if (nodes != null) { monitorChildren(nodes); } pw.watch(); logger(CSSFXMonitor.class).info("CSS Monitoring started"); } public void stop() { pw.stop(); } private void monitorStages(ObservableList<Stage> observableStages) { // first listen for changes observableStages.addListener(new ListChangeListener<Stage>() { @Override public void onChanged(javafx.collections.ListChangeListener.Change<? extends Stage> c) { while (c.next()) { if (c.wasRemoved()) { for (Stage removedStage : c.getRemoved()) { unregisterStage(removedStage); } } if (c.wasAdded()) { for (Stage addedStage : c.getAddedSubList()) { registerStage(addedStage); } } } } }); // then process already existing stages for (Stage stage : observableStages) { registerStage(stage); } } private void monitorStageScene(ReadOnlyObjectProperty<Scene> stageSceneProperty) { // first listen to changes stageSceneProperty.addListener(new ChangeListener<Scene>() { @Override public void changed(ObservableValue<? extends Scene> ov, Scene o, Scene n) { if (o != null) { unregisterScene(o); } if (n != null) { registerScene(n); } } }); if (stageSceneProperty.getValue() != null) { registerScene(stageSceneProperty.getValue()); } } private void monitorRoot(ObjectProperty<Parent> rootProperty) { // register on modification rootProperty.addListener((ov, o, n) -> { if (o != null) { unregisterNode(o); } if (n != null) { registerNode(n); } }); // check current value if (rootProperty.getValue() != null) { registerNode(rootProperty.getValue()); } } private void unregisterNode(Node removedNode) { eventNotify(CSSFXEvent.newEvent(EventType.NODE_REMOVED, removedNode)); } private void registerNode(Node node) { if (node instanceof Parent) { Parent p = (Parent) node; monitorStylesheets(p.getStylesheets()); monitorChildren(p.getChildrenUnmodifiable()); } eventNotify(CSSFXEvent.newEvent(EventType.NODE_ADDED, node)); } private void monitorScenes(ObservableList<Scene> observableScenes) { // first listen for changes observableScenes.addListener(new ListChangeListener<Scene>() { @Override public void onChanged(javafx.collections.ListChangeListener.Change<? extends Scene> c) { while (c.next()) { if (c.wasRemoved()) { for (Scene removedScene : c.getRemoved()) { unregisterScene(removedScene); } } if (c.wasAdded()) { for (Scene addedScene : c.getAddedSubList()) { registerScene(addedScene); } } } } }); // then add existing values for (Scene s : observableScenes) { registerScene(s); } } private void monitorChildren(ObservableList<Node> childrenUnmodifiable) { // first listen to changes childrenUnmodifiable.addListener(new ListChangeListener<Node>() { @Override public void onChanged(javafx.collections.ListChangeListener.Change<? extends Node> c) { while (c.next()) { if (c.wasRemoved()) { for (Node removedNode : c.getRemoved()) { unregisterNode(removedNode); } } if (c.wasAdded()) { for (Node addedNode : c.getAddedSubList()) { registerNode(addedNode); } } } } }); // then look already existing children for (Node node : childrenUnmodifiable) { registerNode(node); } } private void monitorStylesheets(ObservableList<String> stylesheets) { final URIRegistrar registrar = new URIRegistrar(knownConverters, pw); // first register for changes stylesheets.addListener(new StyleSheetChangeListener(registrar)); // then look already set stylesheets uris for (String uri : stylesheets) { registrar.register(uri, stylesheets); } } private void registerScene(Scene scene) { eventNotify(CSSFXEvent.newEvent(EventType.SCENE_ADDED, scene)); monitorStylesheets(scene.getStylesheets()); monitorRoot(scene.rootProperty()); } private void unregisterScene(Scene removedScene) { eventNotify(CSSFXEvent.newEvent(EventType.SCENE_REMOVED, removedScene)); } private void registerStage(Stage stage) { eventNotify(CSSFXEvent.newEvent(EventType.STAGE_ADDED, stage)); monitorStageScene(stage.sceneProperty()); } private void unregisterStage(Stage removedStage) { if (removedStage.getScene() != null) { eventNotify(CSSFXEvent.newEvent(EventType.SCENE_REMOVED, removedStage.getScene())); } eventNotify(CSSFXEvent.newEvent(EventType.STAGE_REMOVED, removedStage)); } private void eventNotify(CSSFXEvent<?> e) { for (CSSFXEventListener listener : eventListeners) { listener.onEvent(e); } } private static class URIRegistrar { final Map<String, Set<ObservableList<? extends String>>> stylesheetsContainingURI = new HashMap<>(); final Map<String, Path> sourceURIs = new HashMap<>(); final List<URIToPathConverter> converters; private PathsWatcher wp; URIRegistrar(List<URIToPathConverter> c, PathsWatcher wp) { converters = c; this.wp = wp; } private void register(String uri, ObservableList<? extends String> stylesheets) { if (!sourceURIs.containsKey(uri)) { logger(CSSFXMonitor.class).debug("searching source for css[%s]", uri); // we do not yet have a source mapping for the URI // let's register this URI Set<ObservableList<? extends String>> uriUsedIn = stylesheetsContainingURI.computeIfAbsent(uri, k -> new HashSet<>()); uriUsedIn.add(stylesheets); evaluateSourceMappingForURI(uri); } } @SuppressWarnings("unchecked") private void evaluateSourceMappingForURI(String uri) { for (URIToPathConverter c : converters) { Path sourceFile = c.convert(uri); if (sourceFile != null) { logger(CSSFXMonitor.class).info("css[%s] will be mapped to source[%s]", uri, sourceFile); Path directory = sourceFile.getParent(); // let's see if other mappings were not waiting for a source mapping final Set<ObservableList<? extends String>> set = stylesheetsContainingURI.get(uri); for (Iterator<ObservableList<? extends String>> it = set.iterator(); it.hasNext();) { ObservableList<? extends String> waitingStylesheets = it.next(); if (!waitingStylesheets.contains(uri)) { it.remove(); } else { wp.monitor(directory.toAbsolutePath().normalize(), sourceFile.toAbsolutePath().normalize(), new URIStyleUpdater(uri, sourceFile.toUri().toString(), (ObservableList<String>) waitingStylesheets)); } } stylesheetsContainingURI.remove(uri); sourceURIs.put(sourceFile.toUri().toString(), sourceFile); break; } } } private void unregister(String uri) { } /** * Reevaluate not mapped uris */ @SuppressWarnings("unused") private void reevaluate() { for (String uri : stylesheetsContainingURI.keySet()) { evaluateSourceMappingForURI(uri); } } } private static class StyleSheetChangeListener implements ListChangeListener<String> { private URIRegistrar registrar; private StyleSheetChangeListener(URIRegistrar registrar) { this.registrar = registrar; } @Override public void onChanged(javafx.collections.ListChangeListener.Change<? extends String> c) { while (c.next()) { if (c.wasRemoved()) { for (String removedURI : c.getRemoved()) { registrar.unregister(removedURI); } } if (c.wasAdded()) { for (String newURI : c.getAddedSubList()) { registrar.register(newURI, c.getList()); } } } } } final ListChangeListener<String> styleSheetChangeListener = new ListChangeListener<String>() { @Override public void onChanged(javafx.collections.ListChangeListener.Change<? extends String> c) { } }; private static class URIStyleUpdater implements Runnable { private final String sourceURI; private final String originalURI; private final ObservableList<String> cssURIs; URIStyleUpdater(String originalURI, String sourceURI, ObservableList<String> cssURIs) { this.originalURI = originalURI; this.sourceURI = sourceURI; this.cssURIs = cssURIs; } @Override public void run() { IntegerProperty positionIndex = new SimpleIntegerProperty(); Platform.runLater(() -> { positionIndex.set(cssURIs.indexOf(originalURI)); if (positionIndex.get() != -1) { cssURIs.remove(originalURI); } if (positionIndex.get() == -1) { positionIndex.set(cssURIs.indexOf(sourceURI)); } cssURIs.remove(sourceURI); }); Platform.runLater(() -> { if (positionIndex.get() >= 0) { cssURIs.add(positionIndex.get(), sourceURI); } else { cssURIs.add(sourceURI); } }); } } }