All Downloads are FREE. Search and download functionalities are using the official Maven repository.

eu.fraho.libs.swing.widgets.form.WForm Maven / Gradle / Ivy

The newest version!
package eu.fraho.libs.swing.widgets.form;

import eu.fraho.libs.swing.exceptions.ChangeVetoException;
import eu.fraho.libs.swing.exceptions.FormCreateException;
import eu.fraho.libs.swing.widgets.WFileChooser;
import eu.fraho.libs.swing.widgets.WLabel;
import eu.fraho.libs.swing.widgets.WPathChooser;
import eu.fraho.libs.swing.widgets.WTextArea;
import eu.fraho.libs.swing.widgets.base.AbstractWComponent;
import eu.fraho.libs.swing.widgets.base.WComponent;
import eu.fraho.libs.swing.widgets.datepicker.ColorTheme;
import eu.fraho.libs.swing.widgets.datepicker.DefaultColorTheme;
import eu.fraho.libs.swing.widgets.datepicker.ThemeSupport;
import lombok.Getter;
import lombok.NonNull;
import lombok.Value;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import javax.swing.*;
import java.awt.*;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.AbstractMap.SimpleEntry;
import java.util.*;
import java.util.List;
import java.util.Map.Entry;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Stream;

@Slf4j
@SuppressWarnings("unused")
public class WForm extends AbstractWComponent implements ThemeSupport {
    private final AtomicBoolean modelChangeRunning = new AtomicBoolean(false);
    private final Map components = new HashMap<>();
    @Getter
    private int columns;
    @Getter
    private boolean readonly = false;
    @Getter
    private ColorTheme theme = new DefaultColorTheme();

    public WForm(@NotNull @NonNull T model) throws FormCreateException {
        this(model, 1);
    }

    public WForm(@NotNull @NonNull T model, int columns) throws FormCreateException {
        super(new JPanel(new GridBagLayout()), model);
        this.columns = columns;
        log.debug("{}: Building form for model {}", getName(), model);
        buildComponent(model);
    }

    @NotNull
    private static Map.Entry mapToEntry(@NotNull @NonNull Field field) {
        return new SimpleEntry<>(field, field.getAnnotation(FormField.class));
    }

    public void setTheme(ColorTheme theme) {
        log.debug("{}: Changing theme to {}", getName(), theme.getClass());
        this.theme = theme;

        components.entrySet().stream()
                .map(e -> e.getValue().getComponent())
                .filter(ThemeSupport.class::isInstance)
                .map(ThemeSupport.class::cast)
                .forEach(e -> e.setTheme(theme));
    }

    @NotNull
    private List> buildClassTree(@NotNull @NonNull T model) {
        List> classes = new ArrayList<>();
        Class clazz = model.getClass();
        do {
            classes.add(0, clazz);
            clazz = clazz.getSuperclass();
        } while (clazz != null && FormModel.class.isAssignableFrom(clazz));
        return classes;
    }

    private void buildComponent(@NotNull @NonNull T model) {
        List> classes = buildClassTree(model);
        GridBagConstraints gbc = new GridBagConstraints();

        gbc.ipadx = 4;
        gbc.gridx = 0;
        gbc.gridy = 0;
        gbc.insets = new Insets(2, 2, 2, 2);

        log.debug("{}: Got {} classes to build", getName(), classes.size());
        classes.forEach(clazz -> buildDeeper(model, gbc, clazz.getDeclaredFields()));
    }

    private void buildDeeper(@NotNull @NonNull T model, @NotNull @NonNull GridBagConstraints gbc, @NotNull @NonNull Field[] fields) {
        JPanel component = getComponent();
        log.debug("{}: Building deeper for fields {}", getName(), Arrays.toString(fields));
        Stream.of(fields)
                .filter(f -> f.isAnnotationPresent(FormField.class))
                .sequential()
                .map(WForm::mapToEntry)
                .forEach(entry -> createComponent(model, component, gbc, entry));
    }

    private void createComponent(@NotNull @NonNull T model, @NotNull @NonNull JPanel component, @NotNull @NonNull GridBagConstraints gbc, @NotNull @NonNull Entry entry) {
        int maxColumnIndex = columns * 3 - 1;

        Field field = entry.getKey();
        FormField anno = entry.getValue();

        log.debug("{}: Creating component for field '{}' with annotation '{}'", getName(), field, anno);
        if (gbc.gridx >= maxColumnIndex) {
            gbc.gridy++;
            gbc.gridx = 0;
        }

        gbc.gridwidth = 1;
        gbc.fill = GridBagConstraints.NONE;
        gbc.anchor = GridBagConstraints.NORTHEAST;

        component.add(new WLabel(anno.caption()), gbc);
        gbc.gridx++;

        WComponent wfield = FormElementFactory.createComponent(model, field, this::invokeListeners);
        gbc.gridwidth = 2;
        gbc.anchor = GridBagConstraints.NORTHWEST;

        if (wfield instanceof WFileChooser) {
            gbc.fill = GridBagConstraints.HORIZONTAL;
        } else if (wfield instanceof WPathChooser) {
            gbc.fill = GridBagConstraints.HORIZONTAL;
        } else if (wfield instanceof WTextArea) {
            gbc.fill = GridBagConstraints.BOTH;
        }

        // save field in map
        components.put(field.getName(), new FieldInfo(wfield, anno.readonly()));

        // add element to container if it's a component
        component.add((Component) wfield, gbc);
        gbc.gridx += 2;
    }

    @SuppressWarnings({"unchecked", "rawtypes"})
    private void checkAndUpdateFromModel(@NotNull @NonNull String key, @NotNull @NonNull FieldInfo value) {
        FormModel model = getValue();
        Object modelValue = getModelValue(model, key);
        if (!Objects.equals(modelValue, value.getComponent().getValue())) {
            log.debug("{}: Model field '{}' has changed, setting component value to '{}'.", getName(), key, modelValue);
            WComponent component = value.getComponent();
            component.setValue(modelValue);
            component.commitChanges();
        }
    }

    @Override
    public void commitChanges() {
        if (modelChangeRunning.compareAndSet(false, true)) {
            log.debug("{}: Committing changes", getName());
            try {
                components.values().stream()
                        .map(FieldInfo::getComponent)
                        .forEach(WComponent::commitChanges);
                super.commitChanges();
            } finally {
                modelChangeRunning.compareAndSet(true, false);
            }
        }
    }

    @Override
    protected void currentValueChanging(@Nullable T newVal) throws ChangeVetoException {
        if (newVal == null) {
            throw new ChangeVetoException("New model may not be null");
        }

        log.debug("{}: Got value changing event", getName());
        if (Objects.equals(getValue(), newVal)) {
            return;
        }
        log.debug("{}: Setting new value ", getName(), newVal);
        try {
            rebuild(newVal);
        } catch (FormCreateException mbe) {
            throw new ChangeVetoException("Invalid model.", mbe);
        }
    }

    public void setColumns(int columns) {
        log.debug("{}: Setting columns to {}", getName(), columns);
        this.columns = columns;
        rebuild(getValue());
    }

    @NotNull
    public T getValue() {
        T value = super.getValue();
        if (value == null) {
            throw new IllegalStateException("model is null, but shouldn't be");
        }
        return value;
    }

    /**
     * Returns the Component representing a specific model attribute.
     *
     * @param modelName The attribute of the model
     * @param        The datatype of the component
     * @return The component representing the model attribute
     * @throws NoSuchElementException The named property was not found in this form.
     */
    @SuppressWarnings("unchecked")
    @NotNull
    public  WComponent getComponent(@NotNull @NonNull String modelName) throws NoSuchElementException {
        return (WComponent) Optional.ofNullable(components.get(modelName)).orElseThrow(NoSuchElementException::new).getComponent();
    }

    @Nullable
    private Object getModelValue(@NotNull @NonNull FormModel model, @NotNull @NonNull String field) {
        Method getter = FormElementFactory.findGetter(model, field);
        try {
            log.debug("{}: Getting value from model with {}", getName(), getter);
            return getter.invoke(model);
        } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
            throw new FormCreateException("Unable to fetch new value for field '" + field + " in model '" + model.getClass() + "::" + model + "'.", e);
        }
    }

    @Override
    public boolean hasChanged() {
        return components.values().stream()
                .map(FieldInfo::getComponent)
                .anyMatch(WComponent::hasChanged);
    }

    @Override
    public void setReadonly(boolean readonly) {
        components.values().stream()
                .filter(f -> !f.isAnnotationReadonly())
                .map(FieldInfo::getComponent)
                .forEach(elem -> elem.setReadonly(readonly));
        this.readonly = readonly;
    }

    private void rebuild(@NotNull @NonNull T model) throws FormCreateException {
        log.debug("{}: Starting rebuild");
        JPanel component = getComponent();
        component.removeAll();
        components.clear();
        buildComponent(model);
        validate();
        repaint();
    }

    public void resetFromModel() {
        if (modelChangeRunning.compareAndSet(false, true)) {
            log.debug("{}: Resetting from model");
            try {
                components.forEach(this::checkAndUpdateFromModel);
            } finally {
                modelChangeRunning.compareAndSet(true, false);
            }
        }
    }

    @Override
    public void rollbackChanges() {
        if (modelChangeRunning.compareAndSet(false, true)) {
            log.debug("{}: Rolling back changes");
            try {
                components.values().stream()
                        .map(FieldInfo::getComponent)
                        .forEach(c -> {
                            try {
                                c.rollbackChanges();
                            } catch (ChangeVetoException cve) {
                                log.error("{}: Unable to rollback value for {}", getName(), ((JComponent) c).getName(), cve);
                            }
                        });
                super.rollbackChanges();
            } finally {
                modelChangeRunning.compareAndSet(true, false);
            }
        }
    }

    @Value
    private static class FieldInfo {
        @NotNull
        private WComponent component;
        private boolean annotationReadonly;
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy