/* * Copyright (c) 2012, 2014, Oracle and/or its affiliates. * All rights reserved. Use is subject to license terms. * * This file is available and licensed under the following license: * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * - Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * - Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the distribution. * - Neither the name of Oracle Corporation nor the names of its * contributors may be used to endorse or promote products derived * from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ package com.oracle.javafx.scenebuilder.kit.editor.panel.css; import com.oracle.javafx.scenebuilder.kit.editor.EditorController; import com.oracle.javafx.scenebuilder.kit.editor.EditorPlatform; import com.oracle.javafx.scenebuilder.kit.editor.EditorPlatform.Theme; import com.oracle.javafx.scenebuilder.kit.editor.drag.source.AbstractDragSource; import com.oracle.javafx.scenebuilder.kit.editor.i18n.I18N; import com.oracle.javafx.scenebuilder.kit.editor.panel.css.CssContentMaker.BeanPropertyState; import com.oracle.javafx.scenebuilder.kit.editor.panel.css.CssContentMaker.CssPropertyState; import com.oracle.javafx.scenebuilder.kit.editor.panel.css.CssContentMaker.CssPropertyState.CssStyle; import com.oracle.javafx.scenebuilder.kit.editor.panel.css.CssContentMaker.PropertyState; import com.oracle.javafx.scenebuilder.kit.editor.panel.css.CssValuePresenterFactory.CssValuePresenter; import com.oracle.javafx.scenebuilder.kit.editor.panel.css.NodeCssState.CssProperty; import com.oracle.javafx.scenebuilder.kit.editor.panel.css.SelectionPath.Item; import com.oracle.javafx.scenebuilder.kit.editor.panel.util.AbstractFxmlPanelController; import com.oracle.javafx.scenebuilder.kit.fxom.FXOMDocument; import com.oracle.javafx.scenebuilder.kit.fxom.FXOMObject; import com.oracle.javafx.scenebuilder.kit.editor.panel.css.SelectionPath.Path; import com.oracle.javafx.scenebuilder.kit.editor.selection.ObjectSelectionGroup; import com.oracle.javafx.scenebuilder.kit.editor.selection.Selection; import com.oracle.javafx.scenebuilder.kit.fxom.FXOMInstance; import com.oracle.javafx.scenebuilder.kit.metadata.Metadata; import com.oracle.javafx.scenebuilder.kit.metadata.property.ValuePropertyMetadata; import com.oracle.javafx.scenebuilder.kit.metadata.util.PropertyName; import com.oracle.javafx.scenebuilder.kit.util.CssInternal; import com.oracle.javafx.scenebuilder.kit.util.Deprecation; import javafx.css.Rule; import java.io.File; import java.io.IOException; import java.lang.reflect.Array; import java.net.MalformedURLException; import java.net.URISyntaxException; import java.net.URL; import java.text.MessageFormat; import java.util.*; import javafx.animation.FadeTransition; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.css.ParsedValue; import javafx.css.PseudoClass; import javafx.css.StyleOrigin; import javafx.event.ActionEvent; import javafx.event.EventHandler; import javafx.fxml.FXML; import javafx.geometry.Orientation; import javafx.geometry.Pos; import javafx.geometry.VPos; import javafx.scene.Node; import javafx.scene.Parent; import javafx.scene.control.*; import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.input.Clipboard; import javafx.scene.input.ClipboardContent; import javafx.scene.layout.AnchorPane; import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; import javafx.scene.layout.Region; import javafx.scene.layout.StackPane; import javafx.scene.layout.VBox; import javafx.scene.text.Text; import javafx.scene.web.WebView; import javafx.util.Callback; import javafx.util.Duration; /** * Controller for the CSS Panel. * */ public class CssPanelController extends AbstractFxmlPanelController { @FXML private StackPane cssPanelHost; @FXML private StackPane cssSearchPanelHost; @FXML private TableColumn beanApiColumn; @FXML private TableColumn builtinColumn; @FXML private TableColumn fxThemeColumn; @FXML private TableColumn inlineColumn; @FXML private TableColumn propertiesColumn; @FXML private TableColumn defaultColumn; @FXML private ToggleButton pick; @FXML private ToggleButton edit; @FXML SelectionPath selectionPath; @FXML private VBox root; @FXML private HBox header; @FXML WebView textPane; @FXML private VBox rulesBox; @FXML private ScrollPane rulesPane; @FXML private TableColumn stylesheetsColumn; @FXML private TableView table; @FXML private StackPane messagePane; @FXML private Label messageLabel; private TreeView rulesTree; private boolean advanced = false; private boolean styledOnly = false; private boolean tableColumnsOrderingReversed = false; private boolean dragOnGoing = false; private ObservableList model; private View currentView = View.TABLE; private String searchPattern; private static Image lookups = null; private static final String NO_MATCHING_RULES = I18N.getString("csspanel.no.matching.rule"); public enum View { TABLE, RULES, TEXT; } private Object selectedObject; // Can be either an FXOMObject (selection mode), or a Node (pick mode) private Selection selection; private final EditorController editorController; private final Delegate applicationDelegate; private final ObjectProperty cssStateProperty = new SimpleObjectProperty<>(); /** * Should be implemented by the application. * * @treatAsPrivate */ public static abstract class Delegate { public abstract void revealInspectorEditor(ValuePropertyMetadata propMeta); } /* * AbstractPanelController */ @Override protected void fxomDocumentDidChange(FXOMDocument oldDocument) { if (isCssPanelLoaded() && hasFxomDocument()) { updateSelectedObject(); refresh(); } } @Override protected void sceneGraphRevisionDidChange() { // System.out.println("CssPanelController.sceneGraphRevisionDidChange() called!"); if (isCssPanelLoaded() && hasFxomDocument()) { refresh(); } } @Override protected void cssRevisionDidChange() { // System.out.println("CssPanelController.cssRevisionDidChange() called!"); if (isCssPanelLoaded() && hasFxomDocument()) { refresh(); } } @Override protected void jobManagerRevisionDidChange() { // FXOMDocument has been modified by a job. // getEditorController().getJobManager().getLastJob() // is the job responsible of the change. // Since sceneGraphRevisionDidChange() will be called in this case, nothing to do here. } @Override protected void editorSelectionDidChange() { if (isCssPanelLoaded() && hasFxomDocument() && !dragOnGoing) { updateSelectedObject(); refresh(); } } /** * AbstractFxmlPanelController. * */ @Override protected void controllerDidLoadFxml() { // Remove scrollPane for rules root.getChildren().remove(rulesPane); root.getChildren().remove(textPane); root.getChildren().remove(table); pick.setOnAction(t -> editorController.setPickModeEnabled(true)); edit.setOnAction(t -> editorController.setPickModeEnabled(false)); editorController.pickModeEnabledProperty().addListener((ChangeListener) (ov, oldVal, newVal) -> setPickMode(newVal)); // Initialize the pick mode from the editorController value setPickMode(editorController.isPickModeEnabled()); table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY); disableColumnReordering(); final Callback, ObservableValue> valueFactory = new ValueFactory(); propertiesColumn.setCellValueFactory(valueFactory); propertiesColumn.setCellFactory(new PropertiesCellFactory()); builtinColumn.setCellValueFactory(valueFactory); builtinColumn.setCellFactory(new BuiltinCellFactory()); fxThemeColumn.setCellValueFactory(valueFactory); fxThemeColumn.setCellFactory(new FxThemeCellFactory()); beanApiColumn.setCellValueFactory(valueFactory); beanApiColumn.setCellFactory(new ModelCellFactory()); stylesheetsColumn.setCellValueFactory(valueFactory); stylesheetsColumn.setCellFactory(new AuthorCellFactory()); inlineColumn.setCellValueFactory(valueFactory); inlineColumn.setCellFactory(new InlineCellFactory()); defaultColumn.setCellValueFactory(valueFactory); defaultColumn.setCellFactory(new DefaultCellFactory()); editorController.themeProperty().addListener((ChangeListener) (ov, t, t1) -> refresh()); cssStateProperty.addListener((ChangeListener) (arg0, oldValue, newValue) -> fillPropertiesTable()); ChangeListener selectionListener = (arg0, oldvalue, newValue) -> { if (newValue != null && newValue.getItem() != null) { Node selectedSubNode = CssUtils.getNode(newValue.getItem()); selectedObject = selectedSubNode; refresh(); // Switch to pick mode editorController.setPickModeEnabled(true); // // Select the sub node selection = editorController.getSelection(); selection.select(getFXOMInstance(selection), selectedSubNode); } }; selectionPath.selected().addListener(selectionListener); // Listen the drag property changes getEditorController().getDragController().dragSourceProperty().addListener((ChangeListener) (ov, oldVal, newVal) -> { if (newVal != null) { // System.out.println("Drag started !"); dragOnGoing = true; } else { // System.out.println("Drag finished."); dragOnGoing = false; updateSelectedObject(); refresh(); } }); // View table by default changeView(CssPanelController.View.TABLE); editorSelectionDidChange(); } private static class ValueFactory implements Callback, ObservableValue> { @Override public ObservableValue call(TableColumn.CellDataFeatures param) { ObjectProperty val = new SimpleObjectProperty<>(); val.setValue(param.getValue()); return val; } } private static class PropertiesCellFactory implements Callback, TableCell> { @Override public TableCell call(TableColumn param) { return new CssPropertyTableCell(); } } private class BuiltinCellFactory implements Callback, TableCell> { @Override public TableCell call(TableColumn param) { return new BuiltinValueTableCell(); } } private class FxThemeCellFactory implements Callback, TableCell> { @Override public TableCell call(TableColumn param) { return new FxThemeValueTableCell(); } } private class ModelCellFactory implements Callback, TableCell> { @Override public TableCell call(TableColumn param) { // System.out.println("Creating new ModelValueTableCell..."); return new ModelValueTableCell(); } } private class AuthorCellFactory implements Callback, TableCell> { @Override public TableCell call(TableColumn param) { return new AuthorValueTableCell(); } } private class InlineCellFactory implements Callback, TableCell> { @Override public TableCell call(TableColumn param) { return new InlineValueTableCell(); } } private class DefaultCellFactory implements Callback, TableCell> { @Override public TableCell call(TableColumn param) { return new DefaultValueTableCell(); } } /* * * Public * */ public CssPanelController(EditorController c, Delegate delegate) { super(CssPanelController.class.getResource("CssPanel.fxml"), I18N.getBundle(), c); this.editorController = c; this.applicationDelegate = delegate; } public String getSearchPattern() { return searchPattern; } public void setSearchPattern(String searchPattern) { this.searchPattern = searchPattern; searchPatternDidChange(); } /** * * @param selectionListener selection listener. * @treatAsPrivate */ public void addSelectionListener(ChangeListener selectionListener) { selectionPath.selected().addListener(selectionListener); } /** * * @param path path. * @treatAsPrivate */ public void setSelectionPath(Path path) { selectionPath.setSelectionPath(path); } /** * @treatAsPrivate */ public void resetSelectionPath() { selectionPath.setSelectionPath(new Path(new ArrayList<>())); } /** * * @param mess message. * @treatAsPrivate */ public void viewMessage(String mess) { root.getChildren().removeAll(messagePane, header, table, rulesPane, textPane); // mainMenu.setDisable(true); // searchBox.setDisable(true); messageLabel.setText(mess); root.getChildren().add(messagePane); } /** * * @return node. * @treatAsPrivate */ public final Node getRulesPane() { return rulesPane; } /** * * @return node. * @treatAsPrivate */ public final Node getTextPane() { return textPane; } /** * * @param model model. * @param state state. * @treatAsPrivate */ public void setContent(ObservableList model, NodeCssState state) { changeView(currentView); initializeRulesTextPanes(state); this.model = FXCollections.observableArrayList(model); } /** * * @treatAsPrivate */ public void clearContent() { table.getItems().clear(); resetSelectionPath(); } public void filter(String pattern) { if (model == null) { return; } ObservableList filtered; if (pattern == null || pattern.trim().length() == 0) { filtered = model; } else { filtered = FXCollections.observableArrayList(); for (CssProperty p : model) { if (p.propertyName().get().contains(pattern.trim())) { filtered.add(p); } } } updateTable(filtered); } /** * * @param parent parent. * @param cssProp css property. * @param style css style. * @param applied applied. * @param isLookup lookup. * @treatAsPrivate */ public static void attachStyleProperty(TreeItem parent, CssPropertyState cssProp, CssStyle style, boolean applied, boolean isLookup) { if (isLookup) { String cssValue = CssValueConverter.toCssString(style.getCssProperty(), style.getCssRule(), style.getParsedValue()); TreeItem item = new TreeItem<>(getContent(style.getCssProperty(), cssValue, style.getParsedValue(), applied)); parent.getChildren().add(item); } else { attachStylePropertyNoLookup(parent, cssProp, style, applied); } } public void changeView(View view) { switch (view) { case TABLE: { root.getChildren().removeAll(messagePane, header, table, rulesPane, textPane); root.getChildren().addAll(header, table); break; } case RULES: { root.getChildren().removeAll(messagePane, header, rulesPane, table, textPane); root.getChildren().addAll(header, rulesPane); break; } case TEXT: { root.getChildren().removeAll(messagePane, header, textPane, table, rulesPane); root.getChildren().addAll(header, textPane); break; } } currentView = view; } /** * * @treatAsPrivate */ public void copyStyleablePath() { final ClipboardContent content = new ClipboardContent(); content.putString(selectionPath.toString()); Clipboard.getSystemClipboard().setContent(content); } public void splitDefaultsAction() { advanced = !advanced; unmerge(); } public void showStyledOnly() { styledOnly = !styledOnly; if (model == null) { return; } showStyled(model); } public void toggleTableColumnsOrdering() { // switch the table columns: // Default to Inline ==> Inline to Defaults // (and vice-versa) ObservableList> columns = table.getColumns(); FXCollections.reverse(columns); // Property column is always first TableColumn propertyColumn = columns.get(columns.size() - 1); columns.remove(propertyColumn); columns.add(0, propertyColumn); tableColumnsOrderingReversed = !tableColumnsOrderingReversed; } public boolean isTableColumnsOrderingReversed() { return tableColumnsOrderingReversed; } public void setTableColumnsOrderingReversed(boolean value) { if (table != null && tableColumnsOrderingReversed != value) { toggleTableColumnsOrdering(); } } /* * * FXML methods. * * @treatAsPrivate */ public void initialize() { } /* * * Private * */ private void refresh() { setCSSContent(); } private void updateSelectedObject() { selection = editorController.getSelection(); if (!isMultipleSelection()) { if (isPickMode()) { // In pick mode: // If the selected node is the "root" node ==> we get its FXOMInstance // Else, we don't have an FXOMInstance Object pickObject = selection.getCheckedHitNode(); FXOMInstance fxomInstance = getFXOMInstance(selection); if (fxomInstance != null && fxomInstance.getSceneGraphObject() == pickObject) { selectedObject = fxomInstance; } else { selectedObject = pickObject; } // System.out.println("(pick mode) selectedObject = " + selectedObject); } else { selectedObject = getFXOMInstance(selection); // System.out.println("selectedObject = " + selectedObject); } } } private static String getFirstStandardClassName(final Class type) { Class clazz = type; while (clazz != null) { if (clazz.getName().startsWith("javafx")) {//NOI18N return clazz.getSimpleName(); } clazz = clazz.getSuperclass(); } return type.getName(); } private void collectCss() { if (selectedObject != null) { NodeCssState state = CssContentMaker.getCssState(selectedObject); if (state == null) { return; } cssStateProperty.setValue(state); } } private void setCSSContent() { if (selectedObject instanceof Skinnable) { if (((Skinnable) selectedObject).getSkin() == null) { return; } } clearContent(); if (isMultipleSelection()) { viewMessage(I18N.getString("csspanel.multiselection")); return; } if (selectedObject != null) { // Update content. fillSelectionContent(); collectCss(); } } private boolean isMultipleSelection() { if (selection.getGroup() instanceof ObjectSelectionGroup) { return ((ObjectSelectionGroup) selection.getGroup()).getItems().size() > 1; } else { // GridSelectionGroup: consider the GridPane only return false; } } private boolean isCssPanelLoaded() { return root != null; } private boolean hasFxomDocument() { return getEditorController().getFxomDocument() != null; } private boolean isPickMode() { return editorController.isPickModeEnabled(); } private void setPickMode(boolean pickMode) { pick.setSelected(pickMode); edit.setSelected(!pickMode); } private void fillSelectionContent() { assert selectedObject != null; // Start from the Component; Node selectedRootNode = CssUtils.getSelectedNode(getFXOMInstance(selection)); if (selectedRootNode == null) { return; } Item rootItem = new Item(selectedRootNode, createItemName(selectedRootNode), createOptional(selectedRootNode));//NOI18N // Seems we can skip the skin now, which is not in the scene graph anymore. // if (componentRootNode instanceof Skinnable) { // assert ((Skinnable) componentRootNode).getSkin() != null; // Node skinNode = ((Skinnable) componentRootNode).getSkin().getNode(); // if (skinNode instanceof Parent) { // addSubStructure(componentRootNode, rootItem, (Parent) skinNode); // } // } else { if (selectedRootNode instanceof Parent) { addSubStructure(selectedRootNode, rootItem, (Parent) selectedRootNode); } // } Object selectedNode = CssUtils.getSelectedNode(selectedObject); List items = SelectionPath.lookupPath(rootItem, selectedNode); setSelectionPath(new Path(items)); } private void addSubStructure(Node componentRootNode, Item parentItem, Node node) { FXOMDocument fxomDoc = getEditorController().getFxomDocument(); assert fxomDoc != null; Node enclosingNode = getEnclosingNode(fxomDoc, node); // The componentRootNode can be a skin structure (Tab, Column), in this case the enclosingNode // is not == to the componentRootNode. That is why we need to compare the enclosingNode of both // n and componentRootNode nodes. Node componentRootNodeEnclosingNode = getEnclosingNode(fxomDoc, componentRootNode); // this is a skin's node and not a node from a component located inside // the skin (eg: SplitPane content being a Button is not part of the SplitPane Skin. boolean isOtherComponentNode = enclosingNode != componentRootNodeEnclosingNode; if (componentRootNode != node && !node.getStyleClass().isEmpty() && !isOtherComponentNode && !(node instanceof Skin)) { Item ni = new Item(node, createItemName(node), createOptional(node));//NOI18N parentItem.getChildren().add(ni); parentItem = ni; } if (node instanceof Parent && !isOtherComponentNode) { Parent parentNode = (Parent) node; for (Node child : parentNode.getChildrenUnmodifiable()) { addSubStructure(componentRootNode, parentItem, child); } } } private Node getEnclosingNode(FXOMDocument fxomDoc, Node n) { Node node = n; FXOMObject enclosingFXOMObj = fxomDoc.searchWithSceneGraphObject(node); while (enclosingFXOMObj == null) { node = node.getParent(); if (node == null) { return null; } enclosingFXOMObj = fxomDoc.searchWithSceneGraphObject(node); } Object enclosingObj = enclosingFXOMObj.getSceneGraphObject(); assert enclosingObj instanceof Node; return (Node) enclosingObj; } private void fillPropertiesTable() { NodeCssState state = cssStateProperty.getValue(); if (state == null) { return; } fillContent(state); filter(searchPattern); } private void fillContent(NodeCssState state) { ObservableList cssModel = FXCollections.observableArrayList(); Collection styleables = state.getAllStyleables(); for (CssProperty sp : styleables) { cssModel.add(sp); for (CssProperty sub : sp.getSubProperties()) { cssModel.add(sub); } } setContent(cssModel, state); } private void attachNotAppliedStyles(TreeItem ti, PropertyState css) { for (CssStyle style : css.getNotAppliedStyles()) { attachStyle(css, style, ti, false); } } private void attachSubProperties(TreeItem parent, PropertyState ss) { for (PropertyState sub : ss.getSubProperties()) { attachProperty(parent, sub); } attachNotAppliedStyles(parent, ss); } private void attachProperty(TreeItem parent, PropertyState ss) { boolean hasSubs = !ss.getSubProperties().isEmpty(); if (hasSubs) { if (ss instanceof CssPropertyState) { CssPropertyState cssProp = (CssPropertyState) ss; if (cssProp.getStyle() != null) { // Need to add the container, not the sub properties Node content = getContent(ss.getCssProperty(), ss.getCssValue(), ss.getFxValue(), true); TreeItem ti = newTreeItem(content, ss); parent.getChildren().add(ti); attachStyles(cssProp, ti); attachNotAppliedStyles(parent, ss); } else { attachSubProperties(parent, ss); } } else { attachSubProperties(parent, ss); } } else { Node content = getContent(ss.getCssProperty(), ss.getCssValue(), ss.getFxValue(), true); TreeItem ti = newTreeItem(content, ss); parent.getChildren().add(ti); if (ss instanceof CssPropertyState) { CssPropertyState css = (CssPropertyState) ss; attachStyles(css, ti); } else { if (ss instanceof BeanPropertyState) { BeanPropertyState beanProp = (BeanPropertyState) ss; String source = beanProp.getPropertyMeta().getName().toString(); StringBuilder contentBuilder = new StringBuilder(); contentBuilder.append(source); ti.getChildren().add(newTreeItem(new Label(contentBuilder.toString()), ss)); } } attachNotAppliedStyles(ti, ss); } } private void searchPatternDidChange() { filter(searchPattern); } private void updateTable(ObservableList currentModel) { showStyled(currentModel); unmerge(); } private void disableColumnReordering() { for (TableColumn column : table.getColumns()) { Deprecation.setTableColumnReordable(column, false); } } private void initializeRulesTextPanes(NodeCssState state) { rulesBox.getChildren().clear(); List rulesList = state.getMatchingRules(); HtmlStyler htmlStyler = new HtmlStyler(); rulesTree = new TreeView<>(); CopyHandler.attachContextMenu(rulesTree); rulesTree.setShowRoot(false); TreeItem ruleRoot = new TreeItem<>(new Text(""));//NOI18N rulesTree.setRoot(ruleRoot); for (NodeCssState.MatchingRule rule : rulesList) { List lst = rule.getDeclarations(); String selector = rule.getSelector(); String txt = selector + " { ";//NOI18N String source = nonNull(getSource(rule.getRule())); Text text = CopyHandler.makeCopyableNode(new Text(txt + source), txt); TreeItem start = new TreeItem<>(text); ruleRoot.getChildren().add(start); htmlStyler.cssRuleStart(selector, source); for (NodeCssState.MatchingDeclaration p : lst) { CssPropertyState prop = p.getProp(); attachStyleProperty(ruleRoot, prop, p.getStyle(), p.isApplied(), p.isLookup()); CssStyle style = p.getStyle(); String cssValue = CssValueConverter.toCssString(style.getCssProperty(), style.getCssRule(), style.getParsedValue()); htmlStyler.addProperty(p.getStyle().getCssProperty(), cssValue, p.isApplied()); } TreeItem end = new TreeItem<>(CopyHandler.createCopyableText("}"));//NOI18N ruleRoot.getChildren().add(end); setTreeHeight(rulesTree); htmlStyler.cssRuleEnd(); } if (ruleRoot.getChildren().isEmpty()) { rulesBox.getChildren().add(new Label(NO_MATCHING_RULES)); } else { rulesBox.getChildren().add(rulesTree); } if (htmlStyler.isEmpty()) { htmlStyler.addMessage(NO_MATCHING_RULES); } textPane.getEngine().loadContent(htmlStyler.getHtmlString()); } void close(ChangeListener selectionListener) { selectionPath.selected().removeListener(selectionListener); } private void unmerge() { defaultColumn.setVisible(!advanced); fxThemeColumn.setVisible(advanced); builtinColumn.setVisible(advanced); } private void showStyled(ObservableList currentModel) { if (styledOnly) { currentModel = extractStyled(currentModel); } table.getItems().setAll(currentModel); } private static ObservableList extractStyled(ObservableList currentModel) { ObservableList ret = FXCollections.observableArrayList(); for (CssProperty prop : currentModel) { if (prop.isAuthorSource() || prop.isInlineSource() || prop.isModelSource()) { ret.add(prop); } } return ret; } private static String nodeIdentifier(Node n) { if (n.getId() != null && !n.getId().equals("")) {//NOI18N return n.getId(); } else { return n.getClass().getSimpleName(); } } /* * * TABLE CELLS CONTENT * */ // "Properties" column private static class CssPropertyTableCell extends TableCell { CssPropertyTableCell() { getStyleClass().add("property-background");//NOI18N } @Override public void updateItem(final CssProperty item, boolean empty) { super.updateItem(item, empty); if (empty) { setGraphic(null); } else { Hyperlink hl = new Hyperlink(); hl.setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE); hl.setOnAction(new LinkActionListener(item)); hl.setAlignment(Pos.CENTER_LEFT); if (item.getMainProperty() != null) { hl.setText(" " + item.propertyName().get());//NOI18N } else { hl.setText(item.propertyName().get()); } setGraphic(hl); setContentDisplay(ContentDisplay.GRAPHIC_ONLY); } } } private static class LinkActionListener implements EventHandler { final CssProperty item; public LinkActionListener(CssProperty item) { this.item = item; } @Override public void handle(ActionEvent event) { try { // XXX jfdenise, for now can't do better than opening the file, no Anchor per property... // Retrieve defining class EditorPlatform.open(EditorPlatform.JAVADOC_HOME + "javafx/scene/doc-files/cssref.html#" + //NOI18N item.getTarget().getClass().getSimpleName().toLowerCase(Locale.ROOT)); } catch (IOException ex) { System.out.println(ex.getMessage()); } } } // Css value cell content private abstract class CssValueTableCell extends TableCell { private final List values = new ArrayList<>(); private VBox valueBox; private MenuButton navigationMenuButton; private FadeTransition fadeTransition; private final MenuItem revealInInspectorMenuItem = new MenuItem(I18N.getString("csspanel.reveal.inspector")); private final MenuItem revealInFileBrowserMenuItem = new MenuItem(); private MenuItem openStylesheetMenuItem = new MenuItem(); // TODO : UI should be in an fxml file private class Value extends AnchorPane { private final VBox vbox; private Label sourceLabel; private MenuButton navigationMenuButton; private Value(Node value) { vbox = new VBox(2); setAlignment(Pos.CENTER_LEFT); vbox.getChildren().add(value); getChildren().add(vbox); AnchorPane.setTopAnchor(vbox, 4.0); AnchorPane.setLeftAnchor(vbox, 4.0); AnchorPane.setRightAnchor(vbox, 4.0); AnchorPane.setBottomAnchor(vbox, 4.0); setOnMouseEntered(arg0 -> { if ((navigationMenuButton != null) && !navigationMenuButton.isShowing()) { fadeMenuButtonTo(1); } }); setOnMouseExited(arg0 -> { if ((navigationMenuButton != null) && !navigationMenuButton.isShowing()) { fadeMenuButtonTo(0); } }); } private void setSource(Label sourceLabel) { this.sourceLabel = sourceLabel; vbox.getChildren().add(sourceLabel); } private void setNavigation(Label navigationLabel, MenuButton navigationMenuButton) { this.navigationMenuButton = navigationMenuButton; vbox.getChildren().add(navigationLabel); if (navigationMenuButton != null) { getChildren().add(navigationMenuButton); AnchorPane.setTopAnchor(navigationMenuButton, 4.0); AnchorPane.setRightAnchor(navigationMenuButton, 4.0); } } private void showSource(boolean show) { if (sourceLabel != null) { sourceLabel.setVisible(show); } if (navigationMenuButton != null) { navigationMenuButton.setVisible(show); } } private void fadeMenuButtonTo(double toValue) { fadeTransition.stop(); fadeTransition.setFromValue(navigationMenuButton.getOpacity()); fadeTransition.setToValue(toValue); fadeTransition.play(); } } private CssValueTableCell() { // showSources.selectedProperty().addListener(new WeakChangeListener<>(sourceListener)); } /** * WARNING: TableCell instances are reused by TableView. It must be * stateless. In our case, value, sourceLabel and navigationLabel MUST * be cleared each time updateItem is called. * * @param item * @param empty */ @Override public void updateItem(CssProperty item, boolean empty) { super.updateItem(item, empty); // System.out.println("CssValueTableCell.updateItem() called for property: " + item.getStyleable().getProperty() ); values.clear(); setGraphic(null); if (!empty) { if (getStyle(item) != null && !getStyle(item).isUsed()) {//eg: -fx-backgroundfills return; } // A new node MUST be constructed on each call, otherwise TableView looses the UI<->model relationship valueBox = new VBox(2); Value currentValue; valueBox.setAlignment(Pos.CENTER); PropertyState cssState = getPropertyState(item); if (cssState != null) { Node n; n = createValueUI(item, cssState, cssState.getFxValue(), getStyle(item)); if (n == null) { Label label = new Label(cssState.getCssValue()); n = label; } currentValue = new Value(n); if (isWinner(item)) { currentValue.getStyleClass().add("winner-background");//NOI18N } values.add(currentValue); handleSource(currentValue, item, getStyle(item)); } handleNotApplied(item); displayValues(); } } private void displayValues() { for (Value v : values) { VBox.setVgrow(v, Priority.ALWAYS); valueBox.getChildren().add(v); } setGraphic(valueBox); setContentDisplay(ContentDisplay.GRAPHIC_ONLY); } protected abstract PropertyState getPropertyState(CssProperty item); protected abstract CssStyle getStyle(CssProperty item); protected abstract boolean isWinner(CssProperty item); protected abstract StyleOrigin getOrigin(CssProperty item); protected String getNavigation(CssProperty item, CssStyle style) { return getNavigationInfo(item, style, getOrigin(item)); } private void handleNotApplied(CssProperty item) { CssPropertyState ps = item.getWinner(); if (ps != null) { for (CssStyle style : ps.getNotAppliedStyles()) { if (style.getOrigin() == getOrigin(item) && !CssContentMaker.containsPseudoState(style.getSelector())) { Node n = createValueUI(item, style); if (n == null) { n = getLabel(style); } Value currentValue = new Value(n); values.add(currentValue); handleSource(currentValue, item, style); } } } // Case where an API call has been made, not applied fxTheme (if any) are hidden if (getOrigin(item) == StyleOrigin.USER_AGENT) { List styles = item.getFxThemeHiddenByModel(); for (CssPropertyState.CssStyle style : styles) { Node n = createValueUI(item, style); if (n == null) { String l = CssValueConverter.toCssString(style.getCssProperty(), style.getCssRule(), style.getParsedValue()); Label label = new Label(l); n = label; } Value currentValue = new Value(n); values.add(currentValue); handleSource(currentValue, item, style); } } } private void handleSource(Value currentValue, final CssProperty item, final CssStyle style) { if (style != null && !style.isUsed()) {//eg: -fx-background-fills; return; } final StyleOrigin origin = getOrigin(item); String source = getSourceInfo(item, style, origin); if (source != null) { Label sourceLabel = new Label(source); sourceLabel.getStyleClass().add("note-label");//NOI18N currentValue.setSource(sourceLabel); } String nav = getNavigation(item, style); if (nav != null) { Label navigationLabel = new Label(nav); navigationLabel.getStyleClass().add("note-label");//NOI18N if (origin != null && origin != StyleOrigin.USER_AGENT) {// No arrow for builtin and fxTheme createNavigationMenuButton(); openStylesheetMenuItem = new MenuItem(MessageFormat.format(I18N.getString("csspanel.open.stylesheet"), nav)); if ((origin == StyleOrigin.USER) || (origin == StyleOrigin.INLINE)) { // Inspector or Inline columns navigationMenuButton.getItems().add(revealInInspectorMenuItem); revealInInspectorMenuItem.setOnAction(event -> navigate(item, getPropertyState(item), style, origin)); if (CssPanelController.this.applicationDelegate == null) { // disable the menu item in this case revealInInspectorMenuItem.setDisable(true); } } else if (origin == StyleOrigin.AUTHOR) { // Stylesheets column navigationMenuButton.getItems().add(openStylesheetMenuItem); navigationMenuButton.getItems().add(revealInFileBrowserMenuItem); revealInFileBrowserMenuItem.setText(EditorPlatform.IS_MAC ? MessageFormat.format(I18N.getString("csspanel.reveal.finder"), nav) : MessageFormat.format(I18N.getString("csspanel.reveal.explorer"), nav)); revealInFileBrowserMenuItem.setOnAction(event -> navigate(item, getPropertyState(item), style, origin)); openStylesheetMenuItem.setOnAction(event -> open(item, getPropertyState(item), style, origin)); } } currentValue.setNavigation(navigationLabel, navigationMenuButton); } currentValue.showSource(true);//showSources.isSelected() } private void createNavigationMenuButton() { navigationMenuButton = new MenuButton(); Region region = new Region(); navigationMenuButton.setGraphic(region); region.getStyleClass().add("cog-shape"); //NOI18N navigationMenuButton.setOpacity(0); navigationMenuButton.getStyleClass().addAll("css-panel-cog-menubutton"); //NOI18N fadeTransition = new FadeTransition(Duration.millis(500), navigationMenuButton); } } // "API defaults" column private class BuiltinValueTableCell extends CssValueTableCell { @Override protected PropertyState getPropertyState(CssProperty item) { return item.builtinState().get(); } @Override protected boolean isWinner(CssProperty item) { return item.isBuiltinSource(); } @Override protected StyleOrigin getOrigin(CssProperty item) { return null; } @Override protected CssStyle getStyle(CssProperty item) { return null; } } // "FX Theme defaults" column private class FxThemeValueTableCell extends CssValueTableCell { @Override protected PropertyState getPropertyState(CssProperty item) { return item.fxThemeState().get(); } @Override protected boolean isWinner(CssProperty item) { return item.isFxThemeSource(); } @Override protected StyleOrigin getOrigin(CssProperty item) { return StyleOrigin.USER_AGENT; } @Override protected CssStyle getStyle(CssProperty item) { CssPropertyState ps = item.fxThemeState().get(); return ps == null ? null : ps.getStyle(); } } // "Inspector" column private class ModelValueTableCell extends CssValueTableCell { @Override protected PropertyState getPropertyState(CssProperty item) { return item.modelState().get(); } @Override protected boolean isWinner(CssProperty item) { return item.isModelSource(); } @Override protected StyleOrigin getOrigin(CssProperty item) { return StyleOrigin.USER; } @Override protected CssStyle getStyle(CssProperty item) { return null; } } // "Stylesheets" column private class AuthorValueTableCell extends CssValueTableCell { @Override protected PropertyState getPropertyState(CssProperty item) { return item.authorState().get(); } @Override protected boolean isWinner(CssProperty item) { return item.isAuthorSource(); } @Override protected StyleOrigin getOrigin(CssProperty item) { return StyleOrigin.AUTHOR; } @Override protected CssStyle getStyle(CssProperty item) { CssPropertyState ps = item.authorState().get(); return ps == null ? null : ps.getStyle(); } } // "Inline Styles" column private class InlineValueTableCell extends CssValueTableCell { @Override protected PropertyState getPropertyState(CssProperty item) { return item.inlineState().get(); } @Override protected boolean isWinner(CssProperty item) { return item.isInlineSource(); } @Override protected StyleOrigin getOrigin(CssProperty item) { return StyleOrigin.INLINE; } @Override protected CssStyle getStyle(CssProperty item) { CssPropertyState ps = item.inlineState().get(); return ps == null ? null : ps.getStyle(); } } /** * XXX jfdenise, handle case where the Fx Theme is not the winning style. * The complex case is that we need to return a propertyState BUT we don't * know if Fx Theme has been overriden, then we do return null. */ // "Defaults" column private class DefaultValueTableCell extends CssValueTableCell { @Override protected PropertyState getPropertyState(CssProperty item) { PropertyState ret = item.fxThemeState().get(); if (ret == null) { // Do we have an override boolean foundNotApplied = false; PropertyState winner = item.getWinner(); if (winner != null) { for (CssStyle np : winner.getNotAppliedStyles()) { // Not applied handling will had the value. if (np.getOrigin() == StyleOrigin.USER_AGENT) { foundNotApplied = true; break; } } } if (!foundNotApplied) { ret = item.builtinState().get(); } } return ret; } @Override protected CssStyle getStyle(CssProperty item) { CssStyle style = null; CssPropertyState fxTheme = item.fxThemeState().get(); if (fxTheme == null) { // Do we have an override PropertyState winner = item.getWinner(); if (winner != null) { for (CssStyle np : winner.getNotAppliedStyles()) { // Not applied handling will had the value. if (np.getOrigin() == StyleOrigin.USER_AGENT) { style = np; break; } } } } else { style = fxTheme.getStyle(); } return style; } @Override protected boolean isWinner(CssProperty item) { return item.isFxThemeSource() || item.isBuiltinSource(); } @Override protected StyleOrigin getOrigin(CssProperty item) { PropertyState state = getPropertyState(item); // If null is returned, means that there is a not applied for fxTheme if (state instanceof CssPropertyState || state == null) { return StyleOrigin.USER_AGENT; } else { return null; } } // Special case, display some info when merged. @Override protected String getNavigation(CssProperty item, CssStyle style) { PropertyState ps = getPropertyState(item); if (ps == null || ps instanceof CssPropertyState) { return I18N.getString("csspanel.fxtheme.defaults.navigation") + " (" + CssInternal.getThemeDisplayName(style.getStyle()) + ")";//NOI18N } else { return I18N.getString("csspanel.api.defaults.navigation"); } } } /* * * * NAVIGATION (to inspector, file explorer, css editor, ... * * */ private void open(CssProperty item, PropertyState state, CssStyle style, StyleOrigin origin) { navigate(item, state, style, origin, true); } private void navigate(CssProperty item, PropertyState state, CssStyle style, StyleOrigin origin) { navigate(item, state, style, origin, false); } private void navigate(CssProperty item, PropertyState state, CssStyle style, StyleOrigin origin, boolean open) { if (origin == StyleOrigin.USER) {// Navigate to property PropertyName propName = ((BeanPropertyState) state).getPropertyMeta().getName(); // Navigate to inspector property if (applicationDelegate != null) { applicationDelegate.revealInspectorEditor(getValuePropertyMeta(propName)); } } if (style != null) { if (style.getOrigin() == StyleOrigin.AUTHOR) {// Navigate to file URL url = style.getUrl(); String path = url.toExternalForm(); if (path.toLowerCase(Locale.ROOT).startsWith("file:/")) { //NOI18N try { if (open) { EditorPlatform.open(path); } else { File f = new File(url.toURI()); EditorPlatform.revealInFileBrowser(f); } } catch (URISyntaxException | IOException ex) { System.out.println(ex.getMessage() + ": " + ex); } } } else { if (style.getOrigin() == StyleOrigin.INLINE) { // Navigate to inspector style property if (applicationDelegate != null) { applicationDelegate.revealInspectorEditor( getValuePropertyMeta(new PropertyName("style"))); //NOI18N } } } } } private static String getNavigationInfo( CssProperty item, CssStyle cssStyle, StyleOrigin origin) { if (origin == StyleOrigin.USER_AGENT) { return CssInternal.getThemeDisplayName(cssStyle.getStyle()); } if (origin == StyleOrigin.USER) { BeanPropertyState state = (BeanPropertyState) item.modelState().get(); return item.getTarget().getClass().getSimpleName() + "." + state.getPropertyMeta().getName().getName();//NOI18N } if (origin == StyleOrigin.AUTHOR) { if (cssStyle != null) { URL url = cssStyle.getUrl(); String name = null; if (url != null) { name = url.toExternalForm(); if (name.toLowerCase(Locale.ROOT).startsWith("file:/")) { //NOI18N try { File f = new File(url.toURI()); name = f.getName(); } catch (URISyntaxException ex) { System.out.println(ex.getMessage() + ": " + ex); } } } return name; } } if (origin == StyleOrigin.INLINE) { Node n = item.getSourceNodeForInline(); if (n != null) { return nodeIdentifier(n); } return null; } return null; } /* * * Private static * */ private static FXOMInstance getFXOMInstance(Selection selection) { FXOMInstance fxomInstance = null; if (selection == null) { return null; } if (selection.getGroup() instanceof ObjectSelectionGroup) { final ObjectSelectionGroup osg = (ObjectSelectionGroup) selection.getGroup(); for (FXOMObject item : osg.getItems()) { if (item instanceof FXOMInstance) { fxomInstance = (FXOMInstance) item; } } } // In case of GridSelectionGroup, nothing to show (?) return fxomInstance; } private ValuePropertyMetadata getValuePropertyMeta(PropertyName propName) { ValuePropertyMetadata valuePropMeta = null; if (selectedObject instanceof FXOMInstance) { valuePropMeta = Metadata.getMetadata().queryValueProperty( (FXOMInstance) selectedObject, propName); } return valuePropMeta; } private static String getPseudoStates(Node node) { StringBuilder pseudoClasses = new StringBuilder(); Set pseudoClassSet = node.getPseudoClassStates(); for (PseudoClass pc : pseudoClassSet) { pseudoClasses.append(":").append(pc.getPseudoClassName()); //NOI18N } return pseudoClasses.toString(); } // Best effort to express a potential selector. There is more than one... private static String localSelector(Node node) { String ret = "";//NOI18N String pseudoClasses = getPseudoStates(node); if (!node.getStyleClass().isEmpty()) { ret = "." + node.getStyleClass().get(node.getStyleClass().size() - 1) + pseudoClasses;//NOI18N } else if (node.getId() != null && !node.getId().equals("")) {//NOI18N ret = "#" + node.getId() + pseudoClasses;//NOI18N } return ret; } private static String createItemName(Node n) { return localSelector(n); } private static String createOptional(Node n) { return "(" + getFirstStandardClassName(n.getClass()) + ")";//NOI18N } private static Node getContent(String property, String cssValue, Object value, boolean applied) { HBox hbox = new HBox(); Node l = createPropertyLabel(property + ": ", applied);//NOI18N hbox.getChildren().add(l); // Custom content. Mainly for paints and images Node n = getCustomContent(value); if (n != null) { hbox.getChildren().add(n); } else { Node cssValueNode = createLabel(cssValue + ";", applied);//NOI18N hbox.getChildren().add(cssValueNode); } return CopyHandler.makeCopyableNode(hbox, property + ": " + cssValue + ";");//NOI18N } private static Node getCustomContent(Object value) { Node ret = null; if (value instanceof ParsedValue) { ParsedValue pv = (ParsedValue) value; value = CssValueConverter.convert(pv); } if (value != null) { if (value.getClass().isArray()) { HBox hbox = new HBox(5); int size = Array.getLength(value); for (int i = 0; i < size; i++) { Node n = getCustomContent(Array.get(value, i)); if (n != null) { hbox.getChildren().add(n); if (i < size - 1) { hbox.getChildren().add(new Label(", "));//NOI18N } } } if (!hbox.getChildren().isEmpty()) { ret = hbox; } } else { if (value instanceof Collection) { HBox hbox = new HBox(5); Collection collection = (Collection) value; Iterator it = collection.iterator(); while (it.hasNext()) { Object obj = it.next(); Node n = getCustomContent(obj); if (n != null) { hbox.getChildren().add(n); if (it.hasNext()) { hbox.getChildren().add(new Label(", "));//NOI18N } } } if (!hbox.getChildren().isEmpty()) { ret = hbox; } } else {// Leaf value CssValuePresenter presenter = CssValuePresenterFactory.getInstance().newValuePresenter(value); Node customPresenter = presenter.getCustomPresenter(); ret = customPresenter; } } } return ret; } private static Node createLabel(String text, String styleclass, boolean isApplied) { Node node = new Label(text); if (styleclass != null) { node.getStyleClass().add(styleclass); } if (!isApplied) { node = createLine((Label) node); } return node; } private static Node createLabel(String text, boolean isApplied) { return createLabel(text, null, isApplied); } private static Node createPropertyLabel(String text, boolean isApplied) { return createLabel(text, "css-panel-property", isApplied);//NOI18N } private static Node createLine(Node node) { StackPane sp = new StackPane(); sp.getChildren().add(node); Separator s = new Separator(Orientation.HORIZONTAL); s.setValignment(VPos.CENTER); s.getStyleClass().add("notAppliedStyleLine");//NOI18N sp.getChildren().add(s); return sp; } private static TreeItem attachSource(PropertyState css, CssStyle cssStyle, TreeItem parent, boolean applied) { String source = getSource(cssStyle); TreeItem srcItem = null; if (source != null) { HBox hbox = new HBox(5); if (cssStyle.getOrigin() != StyleOrigin.INLINE) { Label selector = new Label(cssStyle.getSelector()); // Workaround RT layout bug selector.setMinWidth(30); hbox.getChildren().add(selector); hbox.getChildren().add(new Label("{"));//NOI18N } hbox.getChildren().add(createLabel(cssStyle.getCssProperty() + ": ", applied));//NOI18N Node n = getCustomContent(cssStyle.getParsedValue()); if (n != null) { hbox.getChildren().add(n); } Node label2 = createLabel(CssValueConverter.toCssString(cssStyle.getCssProperty(), cssStyle.getCssRule(), cssStyle.getParsedValue()) + ";", applied);//NOI18N hbox.getChildren().add(label2); if (cssStyle.getOrigin() != StyleOrigin.INLINE) { hbox.getChildren().add(new Label("}"));//NOI18N } Label label = new Label(source); hbox.getChildren().add(label); srcItem = newTreeItem(hbox, css); parent.getChildren().add(srcItem); } return srcItem; } private static void attachStyle(PropertyState css, CssStyle style, TreeItem parent, boolean applied) { attachStyle(css, style, parent, applied, null); } private static void attachStyle(PropertyState css, CssStyle style, TreeItem parent, boolean applied, ArrayList cssPropertyList) { TreeItem sourceItem = attachSource(css, style, parent, applied); if (cssPropertyList != null) { cssPropertyList.add(style.getCssProperty()); } if (sourceItem != null) { // Do we have a chain of lookups? for (CssStyle lookup : style.getLookupChain()) { if ((cssPropertyList != null) && cssPropertyList.contains(lookup.getCssProperty())) { // This css property has already been attached continue; } attachStyle(css, lookup, sourceItem, applied, cssPropertyList); } } } /** * * @param component component. * @param css css property state. * @param lookupRoot root css style. * @param parent parent. * @treatAsPrivate */ public static void attachLookupStyles(Object component, CssPropertyState css, CssStyle lookupRoot, TreeItem parent) { // Some lookup that comes from the SB itself, skip them. // This is expected, these lookups are superceeded by the // CssUtils.createCSSFrontier ArrayList cssPropertyList = new ArrayList<>(); // cssPropertyList will allow to check that the same css property is not added multiple times attachStyle(css, lookupRoot, parent, true, cssPropertyList); } private static void attachStyles(CssPropertyState css, TreeItem parent) { if (css.getStyle() != null) { attachStyle(css, css.getStyle(), parent, true); } } private static class HtmlStyler { private final static String INIT_STRING = ""; //NOI18N private final static String END_STRING = ""; //NOI18N private final StringBuilder builder = new StringBuilder(); private String html; HtmlStyler() { builder.append(INIT_STRING);//NOI18N } public void check() { if (html != null) { throw new IllegalArgumentException("Locked, html already generated");//NOI18N } } public void cssRuleStart(String selector, String source) { check(); builder.append("

");//NOI18N builder.append("").append(selector).append("");//NOI18N builder.append(" { ").append("/* ").append(source).append(" */");//NOI18N } public void cssRuleEnd() { check(); builder.append("
");//NOI18N builder.append("}");//NOI18N builder.append("

");//NOI18N } public void addProperty(String name, String content, boolean applied) { check(); String sepName = name + ": ";//NOI18N String propName = "" + (applied ? sepName : "" + sepName + "") + "";//NOI18N builder.append("
");//NOI18N content = applied ? content : "" + content + "";//NOI18N builder.append("").append(propName).append(content).append(";").append("");//NOI18N } public void addMessage(String mess) { check(); builder.append(mess); } public String getHtmlString() { if (html == null) { builder.append(END_STRING);//NOI18N html = builder.toString(); } return html; } public boolean isEmpty() { return builder.toString().equals(INIT_STRING); } } /** * * @treatAsPrivate */ public void copyRules() { CopyHandler.copy(rulesTree); } private static class CopyHandler { private static String getContent(TreeView tv) { StringBuilder builder = new StringBuilder(); for (TreeItem item : tv.getSelectionModel().getSelectedItems()) { Node n = item.getValue(); String str = (String) n.getProperties().get(CSS_TEXT); if (str != null) { builder.append(str); } } return builder.toString(); } private static void copy(final TreeView tv) { final String cssContent = getContent(tv); final ClipboardContent content = new ClipboardContent(); content.putString(cssContent); Clipboard.getSystemClipboard().setContent(content); } private static void attachContextMenu(final TreeView tv) { ContextMenu ctxMenu = new ContextMenu(); final MenuItem cssContentAction = new MenuItem(I18N.getString("csspanel.copy")); ctxMenu.setOnShowing(arg0 -> { }); tv.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); cssContentAction.setOnAction(arg0 -> copy(tv)); ctxMenu.getItems().add(cssContentAction); tv.setContextMenu(ctxMenu); } private static final String CSS_TEXT = "CSS_TEXT";//NOI18N private static T makeCopyableNode(T node, String str) { node.getProperties().put(CSS_TEXT, str + "\n");//NOI18N return node; } private static Text createCopyableText(String str) { Text text = new Text(str); text.getProperties().put(CSS_TEXT, str + "\n");//NOI18N return text; } } private static void setTreeHeight(TreeView tv) { // XXX jfdenise how to properly compute the height of the tree int minHeight = 70; int topLevelItems = tv.getRoot().getChildren().size(); int computed = topLevelItems == 0 ? 15 : (25 * topLevelItems); tv.setPrefHeight(Math.max(minHeight, computed)); } private static String nonNull(String string) { if (string == null) { return ""; //NOI18N } else { return string; } } private static TreeItem newTreeItem(Node content, PropertyState ss) { TreeItem ti = new TreeItem<>(content); return ti; } private static String getSource(CssStyle style) { URL url = style.getUrl(); String source = null; if (url != null) { source = getSource(url, style.getOrigin()); } return source; } private static String getSource(Rule rule) { URL url = null; StyleOrigin origin = null; // Workaround! if (rule != null) { try { url = new URL(rule.getStylesheet().getUrl()); } catch (MalformedURLException ex) { System.out.println("Invalid URL: " + ex); } origin = rule.getOrigin(); } return getSource(url, origin); } private static String getSource(URL url, StyleOrigin origin) { String source = null; if (url != null) { if (origin == StyleOrigin.USER_AGENT) { source = I18N.getString("csspanel.fxtheme.origin"); } else { if (origin == StyleOrigin.USER) { source = I18N.getString("csspanel.api.origin") + " " //NOI18N + I18N.getString("csspanel.node.property"); } else { if (origin == StyleOrigin.AUTHOR) { source = url.toExternalForm(); } } } } return source; } private static String getSourceInfo(CssProperty item, CssStyle style, StyleOrigin origin) { if (origin == StyleOrigin.USER_AGENT) { if (style != null) { return style.getSelector(); } return null; } if (origin == StyleOrigin.USER) { BeanPropertyState state = (BeanPropertyState) item.modelState().get(); return state.getPropertyMeta().getName().getName(); } if (origin == StyleOrigin.AUTHOR) { if (style != null) { return style.getSelector(); } return null; } if (origin == StyleOrigin.INLINE) { boolean inherited = item.isInlineInherited(); return "style" + (inherited ? " (" + I18N.getString("csspanel.inherited") + ")" : "");//NOI18N } return null; } private static Node createValueUI(CssProperty item, CssStyle style) { ParsedValue value = null; if (style != null && !style.getLookupChain().isEmpty()) { if (style.getLookupChain().size() == 1) { value = style.getLookupChain().get(0).getParsedValue(); } else { value = style.getParsedValue(); } } else { if (style != null) { value = style.getParsedValue(); } } return createValueUI(item, null, value, style); } private static Node createValueUI(CssProperty item, PropertyState ps, Object value, CssStyle style) { ParsedValue[] parsedValues = null; if (style != null) { ParsedValue pv = style.getParsedValue(); Object v = pv.getValue(); if (v instanceof ParsedValue[]) {//Means lookups parsedValues = (ParsedValue[]) v; } } return createValueUI(item, ps, value, style, parsedValues); } private static Node createValueUI(CssProperty item, PropertyState ps, Object value, CssStyle style, ParsedValue[] parsedValues) { Node ret = null; if (value instanceof ParsedValue) { ParsedValue pv = (ParsedValue) value; value = CssValueConverter.convert(pv); } if (value != null) { if (value.getClass().isArray()) { HBox hbox = new HBox(5); int size = Array.getLength(value); int lookupIndex = 0; for (int i = 0; i < size; i++) { Object v = Array.get(value, i); Node n = getLeaf(v); if (n == null) { break; } boolean lookup = false; if (parsedValues != null) { ParsedValue pv = parsedValues[i]; lookup = pv.isContainsLookups(); } if (lookup) { assert style != null; CssStyle lookupRoot = null; if (style.getLookupChain().size() - 1 < lookupIndex) { // We are in NOT APPLIED case, no lookup in matching Styles. // This is an RT bug logged. // XXX jfdenise, we can reconstruct the lookup chain based on the ParsedValue // We need to access private field ParsedValue.resolved // That is a null if this is the leaf of the Lookup // That is a ParsedValue with a getvalue that is a ParsedValue // to introspect. // ParsedValue pv = parsedValues[i]; // if(pv.getValue() instanceof String){ // // OK, this is a lookup name. // Object obj = pv.convert(null); // } else { // // } } else { lookupRoot = style.getLookupChain().get(lookupIndex); } lookupIndex += 1; Node lookupUI = createLookupUI(item, ps, style, lookupRoot, n); hbox.getChildren().add(lookupUI); } else { hbox.getChildren().add(n); } if (i < size - 1) { hbox.getChildren().add(new Label(","));//NOI18N } } if (!hbox.getChildren().isEmpty()) { ret = hbox; } } else { if (value instanceof Collection) { HBox hbox = new HBox(5); int lookupIndex = 0; Collection collection = (Collection) value; Iterator it = collection.iterator(); int index = 0; while (it.hasNext()) { Object v = it.next(); Node n = getLeaf(v); if (n == null) { break; } boolean lookup = false; if (parsedValues != null) { ParsedValue pv = parsedValues[index]; lookup = pv.isContainsLookups(); } if (lookup) { CssStyle lookupRoot = null; assert style != null; if (style.getLookupChain().size() - 1 < lookupIndex) { // We are in NOT APPLIED case, no lookup in matching Styles. // This is an RT bug logged. // XXX jfdenise, we can reconstruct the lookup chain based on the ParsedValue // We need to access private field ParsedValue.resolved // That is a null if this is the leaf of the Lookup // That is a ParsedValue with a getvalue that is a ParsedValue // to introspect. // ParsedValue pv = parsedValues[i]; // if(pv.getValue() instanceof String){ // // OK, this is a lookup name. // Object obj = pv.convert(null); // } else { // // } } else { lookupRoot = style.getLookupChain().get(lookupIndex); } Node lookupUI = createLookupUI(item, ps, style, lookupRoot, n); hbox.getChildren().add(lookupUI); lookupIndex += 1; } else { hbox.getChildren().add(n); } if (it.hasNext()) { hbox.getChildren().add(new Label(","));//NOI18N } index++; } if (!hbox.getChildren().isEmpty()) { ret = hbox; } } else {// Leaf value Node n = getLeaf(value); if (n == null && style != null) { n = getLabel(style); } if (style != null && !style.getLookupChain().isEmpty()) { ret = createLookupUI(item, ps, style, style, n); } else { ret = n; } } } } return ret; } private static Label getLabel(CssStyle style) { String l = CssValueConverter.toCssString(style.getCssProperty(), style.getCssRule(), style.getParsedValue()); return new Label(l); } private static synchronized Image getLookupImage() { if (lookups == null) { lookups = new Image( CssPanelController.class.getResource("images/css-lookup-icon.png").toExternalForm()); //NOI18N } return lookups; } private static Node createLookupUI( final CssProperty item, final PropertyState ps, final CssStyle style, final CssStyle lookupRoot, Node n) { // TODO: make an fxml file for this // MenuButton final HBox hbox = new HBox(); hbox.setMaxWidth(Region.USE_PREF_SIZE); ImageView imgView = new ImageView(); imgView.setImage(getLookupImage()); hbox.getChildren().addAll(n, imgView); MenuButton lookupMb = new MenuButton(); lookupMb.setGraphic(hbox); lookupMb.getStyleClass().add("lookup-button"); CustomMenuItem popupContentMi = new CustomMenuItem(); popupContentMi.setHideOnClick(false); lookupMb.getItems().add(popupContentMi); // Popup content StackPane popupContent = new StackPane(); popupContentMi.setContent(popupContent); TreeView lookupTv = new TreeView<>(); lookupTv.setPrefSize(400, 100); Object val = null; if (ps instanceof CssPropertyState) { val = ((CssPropertyState) ps).getFxValue(); } else { if (style != null) { val = style.getParsedValue(); } } assert val != null; TreeItem root = new TreeItem<>(); lookupTv.setRoot(root); lookupTv.setShowRoot(false); if (ps != null) { assert ps instanceof CssPropertyState; attachLookupStyles(item.getTarget(), ((CssPropertyState) ps), lookupRoot, root); } else { attachLookupStyles(item.getTarget(), null, lookupRoot, root); } popupContent.getChildren().add(lookupTv); return lookupMb; } private static Node getLeaf(Object value) { CssValuePresenterFactory.CssValuePresenter presenter = CssValuePresenterFactory.getInstance().newValuePresenter(value); Node customPresenter = presenter.getCustomPresenter(); return customPresenter; } private static void attachStylePropertyNoLookup(TreeItem parent, CssPropertyState ps, CssStyle style, boolean applied) { CssStyle cssStyle = applied ? ps.getStyle() : style; Object value = applied ? ps.getFxValue() : style.getParsedValue(); String cssValue = CssValueConverter.toCssString(cssStyle.getCssProperty(), cssStyle.getCssRule(), cssStyle.getParsedValue()); TreeItem item = new TreeItem<>(getContent(ps.getCssProperty(), cssValue, value, applied)); parent.getChildren().add(item); } }