public class BindableValue<T> extends ObservableValueBase<T> {

    private T value;

    public BindableValue( T value ) {
        this.value = value;
    }

    public void setValue( T value ) {
        this.value = value;
        fireValueChangedEvent();
    }

    @Override
    public T getValue() {
        return value;
    }

} public class Config {

    private static final Logger log = LoggerFactory.getLogger( Config.class );

    private Path path;
    private final ConfigProperties properties;
    private final List<Runnable> reloadActions = new ArrayList<>( 2 );

    public Config( Path path, TaskRunner taskRunner ) {
        this.path = path;
        this.properties = new ConfigProperties();

        if ( path.toFile().exists() ) {
            readConfigFile();
        } else {
            properties.highlightGroups.getDefault()
                    .add( new HighlightExpression( "WARN", Color.YELLOW, Color.RED, false ) );
        }

        // make this a singleton object so that it can be remembered when we try to run it many times below
        final Runnable updateConfigFile = () -> dumpConfigToFile( taskRunner );

        log.debug( "Listening to changes on observable Lists" );

        InvalidationListener listener = ( event ) ->
                taskRunner.runWithMaxFrequency( updateConfigFile, 2000L, 1000L );

        properties.standardLogColors.addListener( listener );
        properties.highlightGroups.setListener( listener );
        properties.observableFiles.addListener( listener );
        properties.panesOrientation.addListener( listener );
        properties.windowBounds.addListener( listener );
        properties.paneDividerPositions.addListener( listener );
        properties.font.addListener( listener );
        properties.enableFilters.addListener( listener );

        // keep track of logFiles's groups
        properties.observableFiles.forEach( f -> f.highlightGroupProperty().addListener( listener ) );

        properties.observableFiles.addListener( ( SetChangeListener<LogFile> ) change -> {
            if ( change.wasAdded() ) {
                change.getElementAdded().highlightGroupProperty().addListener( listener );
            }
            if ( change.wasRemoved() ) {
                change.getElementRemoved().highlightGroupProperty().removeListener( listener );
            }
        } );
    }

    public SimpleObjectProperty<LogLineColors> standardLogColorsProperty() {
        return properties.standardLogColors;
    }

    public HighlightGroups getHighlightGroups() {
        return properties.highlightGroups;
    }

    public ObservableSet<LogFile> getObservableFiles() {
        return properties.observableFiles;
    }

    public SimpleObjectProperty<Orientation> panesOrientationProperty() {
        return properties.panesOrientation;
    }

    public SimpleObjectProperty<Bounds> windowBoundsProperty() {
        return properties.windowBounds;
    }

    public ObservableList<Double> getPaneDividerPositions() {
        return properties.paneDividerPositions;
    }

    public BindableValue<Font> fontProperty() {
        return properties.font;
    }

    public BooleanProperty filtersEnabledProperty() {
        return properties.enableFilters;
    }

    public void loadConfig( Path path ) {
        if ( path.toFile().isFile() ) {
            this.path = path;
            log.info( "Clearing LogFX properties" );
            properties.clear();
            log.info( "Loading config file: {}", path );
            readConfigFile();
            for ( Runnable reloadAction : reloadActions ) {
                reloadAction.run();
            }
        } else {
            var message = "Config file is not a file: " + path;
            log.info( message );
            Dialog.showMessage( message + "\n\n", Dialog.MessageLevel.WARNING );
        }
    }

    public Path getCurrentConfigPath() {
        return path;
    }

    private void readConfigFile() {
        try {
            Iterator<String> lines = Files.lines( path ).iterator();
            new ConfigParser( properties ).parseConfigFile( null, lines );
        } catch ( Exception e ) {
            log.warn( "Error loading config", e );
            Dialog.showMessage( "Could not read config file: " + path +
                    "\n\n" + e, Dialog.MessageLevel.WARNING );
        }
    }

    /**
     * Add an action to run when config is loaded. This happens when the user changes which project is open.
     *
     * @param reloadAction action to run on config reload
     */
    public void onReload( Runnable reloadAction ) {
        reloadActions.add( reloadAction );
    }

    /**
     * The properties are all set in the JavaFX Thread, therefore we need to make copies of everything
     * in the JavaFX Thread before being able to safely use them in another Thread,
     * where we write the config file.
     *
     * @param taskRunner runner
     */
    private void dumpConfigToFile( TaskRunner taskRunner ) {
        var data = new ConfigData();
        data.path = path.toFile();

        Platform.runLater( () -> data.logLineColors = properties.standardLogColors.get() );
        Platform.runLater( () -> data.highlightExpressions = new HashMap<>( properties.highlightGroups.toMap() ) );
        Platform.runLater( () -> data.enableFilters = properties.enableFilters.getValue() );
        Platform.runLater( () -> data.files = new LinkedHashSet<>( properties.observableFiles ) );
        Platform.runLater( () -> data.orientation = properties.panesOrientation.get() );
        Platform.runLater( () -> data.windowBounds = properties.windowBounds.get() );
        Platform.runLater( () -> data.dividerPositions = new ArrayList<>( properties.paneDividerPositions ) );
        Platform.runLater( () -> data.font = properties.font.getValue() );

        // go to the JavaFX Thread to wait for all previous tasks to complete, then dump the file, finally.
        Platform.runLater( () -> taskRunner.runAsync( () -> dumpConfigToFile( data ) ) );
    }

    private static void dumpConfigToFile( ConfigData data ) {
        log.debug( "Writing config to {}", data.path );

        try ( FileWriter writer = new FileWriter( data.path ) ) {

            writer.write( "version:\n  " );
            writer.write( ConfigParser.ConfigVersion.V3.name() );
            writer.write( "\nstandard-log-colors:\n" );
            writer.write( "  " + data.logLineColors.getBackground() + " " + data.logLineColors.getFill() );
            writer.write( "\n" );

            for ( Map.Entry<String, Collection<HighlightExpression>> entry : data.highlightExpressions.entrySet() ) {
                String group = entry.getKey();
                writer.write( "expressions:\n" );
                if ( !group.isEmpty() ) {
                    writer.write( "  @name@" );
                    writer.write( group );
                    writer.write( '\n' );
                }
                for ( HighlightExpression expression : entry.getValue() ) {
                    writer.write( "  " + expression.getBkgColor() );
                    writer.write( " " + expression.getFillColor() );
                    writer.write( " " + expression.isFiltered() );
                    writer.write( " " + expression.getPattern().pattern() );
                    writer.write( "\n" );
                }
            }

            writer.write( "filters:\n  " );
            writer.write( data.enableFilters ? "enable\n" : "disable\n" );

            writer.write( "auto_update:\n  " );
            writer.write( data.autoUpdate ? "enable\n" : "disable\n" );

            writer.write( "files:\n" );
            for ( LogFile file : data.files ) {
                String group = file.getHighlightGroup();
                writer.write( "  " );
                if ( !group.isEmpty() ) {
                    writer.write( '[' );
                    writer.write( group );
                    writer.write( ']' );
                }
                writer.write( file.file.getAbsolutePath() );
                writer.write( "\n" );
            }

            writer.write( "gui:\n" );
            writer.write( "  orientation " + data.orientation.name() );
            if ( data.windowBounds != null ) {
                writer.write( String.format( Locale.US, "%n  window %.1f %.1f %.1f %.1f",
                        data.windowBounds.getMinX(), data.windowBounds.getMinY(),
                        data.windowBounds.getWidth(), data.windowBounds.getHeight() ) );
            }
            if ( !data.dividerPositions.isEmpty() ) {
                writer.write( "\n  pane-dividers " );
                writer.write( data.dividerPositions.stream()
                        .map( n -> String.format( Locale.US, "%.2f", n ) )
                        .collect( joining( "," ) ) );
            }
            writer.write( "\n  font " + data.font.getSize() );
            writer.write( " " + data.font.getFamily() );

            writer.write( "\n" );
        } catch ( IOException e ) {
            Dialog.showMessage( "Could not write config file: " + data.path +
                    "\n\n" + e, Dialog.MessageLevel.ERROR );
        }
    }

    private static final class ConfigData {
        volatile LogLineColors logLineColors;
        volatile Map<String, Collection<HighlightExpression>> highlightExpressions;
        volatile boolean enableFilters;
        volatile boolean autoUpdate;
        volatile Set<LogFile> files;
        volatile Orientation orientation;
        volatile Bounds windowBounds;
        volatile List<Double> dividerPositions;
        volatile Font font;
        volatile File path;
    }
} public final class HighlightGroups {
    private final ObservableSet<LogFile> observableFiles;
    private final ObservableMap<String, ObservableList<HighlightExpression>> configByGroupName;

    private InvalidationListener invalidationListener;

    public HighlightGroups( ObservableSet<LogFile> observableFiles ) {
        this.observableFiles = observableFiles;
        configByGroupName = FXCollections.observableMap( new LinkedHashMap<>( 4 ) );
        configByGroupName.put( "", createNewExpressions( null ) );
    }

    public ObservableList<HighlightExpression> getByName( String name ) {
        return configByGroupName.get( name );
    }

    public ObservableList<HighlightExpression> getDefault() {
        return configByGroupName.get( "" );
    }

    public ObservableList<HighlightExpression> add( String groupName ) {
        return configByGroupName.computeIfAbsent( groupName,
                this::createNewExpressions );
    }

    public void remove( String groupName ) {
        if ( groupName.isEmpty() ) {
            return;
        }
        configByGroupName.remove( groupName );
        observableFiles.stream()
                .filter( it -> groupName.equals( it.getHighlightGroup() ) )
                .forEach( file -> file.highlightGroupProperty().setValue( "" ) );
    }

    public void renameGroup( String oldName, String newName ) {
        configByGroupName.put( newName, configByGroupName.remove( oldName ) );
        observableFiles.stream()
                .filter( it -> oldName.equals( it.getHighlightGroup() ) )
                .forEach( file -> file.highlightGroupProperty().setValue( newName ) );
    }

    public Set<String> groupNames() {
        return Collections.unmodifiableSet( configByGroupName.keySet() );
    }

    public Map<String, ObservableList<HighlightExpression>> toMap() {
        return Collections.unmodifiableMap( configByGroupName );
    }

    public void addGroupNameListener( InvalidationListener listener ) {
        configByGroupName.addListener( listener );
    }

    public void removeGroupNameListener( InvalidationListener listener ) {
        configByGroupName.removeListener( listener );
    }

    public void clear() {
        configByGroupName.clear();
        configByGroupName.put( "", createNewExpressions( null ) );
    }

    void setListener( InvalidationListener listener ) {
        if ( invalidationListener != null ) throw new RuntimeException( "invalidation listener has already been set" );
        invalidationListener = listener;
        configByGroupName.addListener( listener );
        configByGroupName.values().forEach( ( v ) -> v.addListener( listener ) );
    }

    private ObservableList<HighlightExpression> createNewExpressions( Object ignore ) {
        ObservableList<HighlightExpression> newList = FXCollections.observableArrayList();
        if ( invalidationListener != null ) newList.addListener( invalidationListener );
        return newList;
    }
} public final class LogFile {
    public final File file;
    private final BindableValue<String> highlightGroup = new BindableValue<>( "" );

    public LogFile( File file ) {
        this.file = file;
    }

    public LogFile( File file, String highlightGroupName ) {
        this.file = file;
        highlightGroup.setValue( highlightGroupName );
    }

    public BindableValue<String> highlightGroupProperty() {
        return highlightGroup;
    }

    public String getHighlightGroup() {
        return highlightGroup.getValue();
    }

    @Override
    public boolean equals( Object other ) {
        if ( this == other ) return true;
        if ( !( other instanceof LogFile ) ) return false;
        LogFile logFile = ( LogFile ) other;
        return Objects.equals( file, logFile.file );
    }

    @Override
    public int hashCode() {
        return Objects.hash( file );
    }

} public final class HighlightExpression {

    private final Pattern expression;
    private final Paint bkgColor;
    private final Paint fillColor;
    private final boolean isFiltered;

    public HighlightExpression( String expression, Paint bkgColor, Paint fillColor, boolean isFiltered ) {
        this( Pattern.compile( expression ), bkgColor, fillColor, isFiltered );
    }

    public HighlightExpression( Pattern expression, Paint bkgColor, Paint fillColor, boolean isFiltered ) {
        this.expression = expression;
        this.bkgColor = bkgColor;
        this.fillColor = fillColor;
        this.isFiltered = isFiltered;
    }

    public Paint getBkgColor() {
        return bkgColor;
    }

    public Paint getFillColor() {
        return fillColor;
    }

    public Pattern getPattern() {
        return expression;
    }

    public boolean isFiltered() {
        return isFiltered;
    }

    public LogLineColors getLogLineColors() {
        return new LogLineColors( bkgColor, fillColor );
    }

    public boolean matches( String text ) {
        if ( text == null ) {
            text = "";
        }

        // the find method does not anchor the String by default, unlike matches()
        return expression.matcher( text ).find();
    }

    public HighlightExpression withFilter( boolean enable ) {
        return new HighlightExpression( this.expression, this.bkgColor, this.fillColor, enable );
    }

    @Override
    public String toString() {
        return "{" +
                "expression=" + expression +
                ", bkgColor=" + bkgColor +
                ", fillColor=" + fillColor +
                ", isFiltered=" + isFiltered +
                '}';
    }

    @Override
    public boolean equals( Object o ) {
        if ( this == o ) return true;
        if ( o == null || getClass() != o.getClass() ) return false;
        HighlightExpression that = ( HighlightExpression ) o;
        return isFiltered == that.isFiltered &&
                expression.toString().equals( that.expression.toString() ) &&
                bkgColor.equals( that.bkgColor ) &&
                fillColor.equals( that.fillColor );
    }

    @Override
    public int hashCode() {
        return Objects.hash( expression.toString(), bkgColor, fillColor, isFiltered );
    }
} public static final String DUPLICATE = "\uE62F"; public static final String PENCIL = "\ue61c"; public static final String PLUS = "\ue61a"; public static final String TRASH = "\ue605"; static Color nextColor() {
    return Color.color( Math.random(), Math.random(), Math.random() );
}
import com.athaydes.logfx.binding.BindableValue;
import com.athaydes.logfx.config.Config;
import com.athaydes.logfx.config.HighlightGroups;
import com.athaydes.logfx.data.LogFile;
import com.athaydes.logfx.text.HighlightExpression;
import javafx.application.Platform;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.scene.control.Button;
import javafx.scene.control.ChoiceBox;
import javafx.scene.control.Label;
import javafx.scene.control.Tooltip;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import static com.athaydes.logfx.ui.AwesomeIcons.DUPLICATE;
import static com.athaydes.logfx.ui.AwesomeIcons.PENCIL;
import static com.athaydes.logfx.ui.AwesomeIcons.PLUS;
import static com.athaydes.logfx.ui.AwesomeIcons.TRASH;
import static com.athaydes.logfx.ui.HighlightOptions.nextColor;
package com.athaydes.logfx.ui;



public class HighlightGroupsView extends BorderPane {
    private HighlightOptions defaultOption;
    private final ChoiceBox<HighlightOptions> optionsChoiceBox;
    private final HighlightGroups groups;
    private HighlightOptionsSelectorConverter selectorConverter;

    public HighlightGroupsView( Config config ) {
        groups = config.getHighlightGroups();

        optionsChoiceBox = new ChoiceBox<>();

        defaultOption = populateChoiceBoxAndReturnDefaultOption( config, optionsChoiceBox.getItems(), groups );

        assert defaultOption != null;

        selectorConverter = new HighlightOptionsSelectorConverter(
                optionsChoiceBox.getItems(), defaultOption, groups );

        optionsChoiceBox.setConverter( selectorConverter );

        Button deleteButton = AwesomeIcons.createIconButton( TRASH );
        deleteButton.setTooltip( new Tooltip( "Delete the selected group of highlight rules" ) );
        deleteButton.setDisable( true );
        deleteButton.setOnAction( handleDeleteGroup( groups ) );

        Button editButton = AwesomeIcons.createIconButton( PENCIL );
        editButton.setTooltip( new Tooltip( "Edit the selected group" ) );
        editButton.setOnAction( handleEditGroup( groups ) );

        Button addButton = AwesomeIcons.createIconButton( PLUS );
        addButton.setTooltip( new Tooltip( "Create a new group of highlight rules. " +
                "Each log file can be associated with a group" ) );
        addButton.setOnAction( handleAddGroup( config, groups ) );

        Button duplicateButton = AwesomeIcons.createIconButton( DUPLICATE );