workcraft/workcraft

View on GitHub
workcraft/CircuitPlugin/src/org/workcraft/plugins/circuit/tools/InitialisationAnalyserTool.java

Summary

Maintainability
C
1 day
Test Coverage
package org.workcraft.plugins.circuit.tools;

import org.workcraft.dom.Node;
import org.workcraft.dom.math.MathNode;
import org.workcraft.dom.visual.HitMan;
import org.workcraft.dom.visual.SizeHelper;
import org.workcraft.dom.visual.VisualComponent;
import org.workcraft.dom.visual.VisualModel;
import org.workcraft.dom.visual.connections.VisualConnection;
import org.workcraft.gui.controls.FlatHeaderRenderer;
import org.workcraft.gui.events.GraphEditorMouseEvent;
import org.workcraft.gui.layouts.WrapLayout;
import org.workcraft.gui.tools.*;
import org.workcraft.plugins.builtin.settings.AnalysisDecorationSettings;
import org.workcraft.plugins.builtin.settings.VisualCommonSettings;
import org.workcraft.plugins.circuit.*;
import org.workcraft.plugins.circuit.utils.CircuitUtils;
import org.workcraft.plugins.circuit.utils.InitialisationState;
import org.workcraft.plugins.circuit.utils.ResetUtils;
import org.workcraft.plugins.circuit.utils.StatsUtils;
import org.workcraft.types.Pair;
import org.workcraft.utils.ColorUtils;
import org.workcraft.utils.GuiUtils;
import org.workcraft.utils.TextUtils;
import org.workcraft.utils.WorkspaceUtils;
import org.workcraft.workspace.WorkspaceEntry;

import javax.swing.*;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.TableCellRenderer;
import java.awt.*;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.Arrays;
import java.util.Collection;
import java.util.function.Function;

public class InitialisationAnalyserTool extends AbstractGraphEditorTool {

    private static final String WARNING_SYMBOL = Character.toString((char) 0x26A0);
    private static final String WARNING_PREFIX = WARNING_SYMBOL + ' ';

    private InitialisationState initState = null;
    private final PortTable portTable = new PortTable();
    private final PinTable pinTable = new PinTable();

    @Override
    public JPanel getControlsPanel(final GraphEditor editor) {
        JPanel panel = new JPanel(new BorderLayout());
        panel.add(getLegendControlsPanel(), BorderLayout.NORTH);
        panel.add(getForcedControlsPanel(editor), BorderLayout.CENTER);
        panel.add(getResetControlsPanel(editor), BorderLayout.SOUTH);
        panel.setPreferredSize(new Dimension(0, 0));
        return panel;
    }

    private JPanel getLegendControlsPanel() {
        ColorLegendTable colorLegendTable = new ColorLegendTable(Arrays.asList(
                Pair.of(AnalysisDecorationSettings.getDontTouchColor(), "Don't touch zero delay"),
                Pair.of(AnalysisDecorationSettings.getProblemColor(), "Problem of initialisation"),
                Pair.of(VisualCommonSettings.getFillColor(), "Unknown initial state"),
                Pair.of(AnalysisDecorationSettings.getFixerColor(), "Forced initial state"),
                Pair.of(AnalysisDecorationSettings.getClearColor(), "Propagated initial state")
        ));

        String expectedPinLegend = getHtmlPinLegend("☐", "Expected high / low");
        String propagatedPinLegend = getHtmlPinLegend("■", "Propagated high / low");
        String forcedPinLegend = getHtmlPinLegend("◆", "Forced high / low");
        JLabel legendLabel = new JLabel("<html><b>Pin initial state:</b><br>" + expectedPinLegend + "<br>" + propagatedPinLegend + "<br>" + forcedPinLegend + "</html>");
        JPanel legendPanel = new JPanel(new BorderLayout());
        legendPanel.setBorder(GuiUtils.getTitledBorder("<html><b>Gate highlight legend</b></html>"));
        legendPanel.add(colorLegendTable, BorderLayout.CENTER);
        legendPanel.add(legendLabel, BorderLayout.SOUTH);
        return legendPanel;
    }

    private String getHtmlPinLegend(String key, String description) {
        String highKey = TextUtils.getHtmlSpanColor(key, CircuitSettings.getActiveWireColor());
        String lowKey = TextUtils.getHtmlSpanColor(key, CircuitSettings.getInactiveWireColor());
        return highKey + " / " + lowKey + ' ' + description;
    }

    private JPanel getForcedControlsPanel(final GraphEditor editor) {
        JButton tagForcedInitInputPortsButton = GuiUtils.createIconButton(
                "images/circuit-initialisation-input_ports.svg",
                "Force init all input ports (environment responsibility)",
                l -> changeForcedInit(editor, ResetUtils::tagForcedInitInputPorts));

        JButton tagForcedInitNecessaryPinsButton = GuiUtils.createIconButton(
                "images/circuit-initialisation-problematic_pins.svg",
                "Force init output pins with problematic initial state",
                l -> changeForcedInit(editor, ResetUtils::tagForcedInitProblematicPins));

        JButton tagForcedInitSequentialPinsButton = GuiUtils.createIconButton(
                "images/circuit-initialisation-sequential_pins.svg",
                "Force init output pins of sequential gates",
                l -> changeForcedInit(editor, ResetUtils::tagForcedInitSequentialPins));

        JButton tagForcedInitAutoAppendButton = GuiUtils.createIconButton(
                "images/circuit-initialisation-auto_append.svg",
                "Auto-append forced init pins as necessary to complete initialisation",
                l -> changeForcedInit(editor, ResetUtils::tagForcedInitAutoAppend));

        JButton tagForcedInitAutoDiscardButton = GuiUtils.createIconButton(
                "images/circuit-initialisation-auto_discard.svg",
                "Auto-discard forced init pins that are redundant for initialisation",
                l -> changeForcedInit(editor, ResetUtils::tagForcedInitAutoDiscard));

        JButton tagForcedInitClearAllButton = GuiUtils.createIconButton(
                "images/circuit-initialisation-clear_all.svg",
                "Clear all forced init ports and pins",
                l -> changeForcedInit(editor, ResetUtils::tagForcedInitClearAll));

        JPanel buttonPanel = new JPanel();
        buttonPanel.add(tagForcedInitInputPortsButton);
        buttonPanel.add(tagForcedInitNecessaryPinsButton);
        buttonPanel.add(tagForcedInitSequentialPinsButton);
        buttonPanel.add(tagForcedInitAutoAppendButton);
        buttonPanel.add(tagForcedInitAutoDiscardButton);
        buttonPanel.add(tagForcedInitClearAllButton);
        GuiUtils.setButtonPanelLayout(buttonPanel, tagForcedInitInputPortsButton.getPreferredSize());

        JPanel controlPanel = new JPanel(new WrapLayout());
        controlPanel.add(buttonPanel);

        JSplitPane splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT,
                new JScrollPane(portTable), new JScrollPane(pinTable));

        splitPane.setOneTouchExpandable(true);
        splitPane.setResizeWeight(0.5);

        JPanel forcePanel = new JPanel(new BorderLayout());
        forcePanel.add(controlPanel, BorderLayout.NORTH);
        forcePanel.add(splitPane, BorderLayout.CENTER);
        return forcePanel;
    }

    private void changeForcedInit(final GraphEditor editor, Function<Circuit, Collection<? extends Contact>> func) {
        WorkspaceEntry we = editor.getWorkspaceEntry();
        we.captureMemento();
        Circuit circuit = (Circuit) editor.getModel().getMathModel();
        Collection<? extends Contact> changedContacts = func.apply(circuit);
        if (changedContacts.isEmpty()) {
            we.uncaptureMemento();
        } else {
            we.saveMemento();
        }
        editor.requestFocus();
    }

    private JPanel getResetControlsPanel(final GraphEditor editor) {
        JButton insertHighResetButton = new JButton("<html><center>Insert reset<br>(active-high)</center></html>");
        insertHighResetButton.addActionListener(l -> insertReset(editor, false));

        JButton insertLowResetButton = new JButton("<html><center>Insert reset<br>(active-low)</center></html>");
        insertLowResetButton.addActionListener(l -> insertReset(editor, true));

        JPanel resetPanel = new JPanel(new WrapLayout());
        resetPanel.add(insertHighResetButton);
        resetPanel.add(insertLowResetButton);
        return resetPanel;
    }

    private void insertReset(final GraphEditor editor, boolean isActiveLow) {
        WorkspaceEntry we = editor.getWorkspaceEntry();
        VisualCircuit circuit = WorkspaceUtils.getAs(we, VisualCircuit.class);
        we.captureMemento();
        if (ResetUtils.insertReset(circuit, isActiveLow)) {
            we.saveMemento();
        }
        we.uncaptureMemento();
        editor.requestFocus();
    }

    @Override
    public String getLabel() {
        return "Initialisation analyser";
    }

    @Override
    public int getHotKeyCode() {
        return KeyEvent.VK_I;
    }

    @Override
    public Icon getIcon() {
        return GuiUtils.createIconFromSVG("images/circuit-tool-initialisation_analysis.svg");
    }

    @Override
    public Cursor getCursor(boolean menuKeyDown, boolean shiftKeyDown, boolean altKeyDown) {
        return Cursor.getPredefinedCursor(Cursor.HAND_CURSOR);
    }

    @Override
    public String getHintText(final GraphEditor editor) {
        return "Click on a driver contact to toggle its forced init state.";
    }

    @Override
    public void activated(final GraphEditor editor) {
        super.activated(editor);
        Circuit circuit = (Circuit) editor.getModel().getMathModel();
        CircuitUtils.correctInitialState(circuit);
        updateState(editor);
        editor.getWorkspaceEntry().addObserver(e -> updateState(editor));
    }

    @Override
    public void deactivated(final GraphEditor editor) {
        super.deactivated(editor);
        initState = null;
    }

    @Override
    public void setPermissions(final GraphEditor editor) {
        WorkspaceEntry we = editor.getWorkspaceEntry();
        we.setCanModify(true);
        we.setCanSelect(false);
        we.setCanCopy(false);
    }

    private void updateState(final GraphEditor editor) {
        Circuit circuit = (Circuit) editor.getModel().getMathModel();
        initState = new InitialisationState(circuit);
        portTable.refresh();
        pinTable.refresh();
    }

    @Override
    public void mousePressed(GraphEditorMouseEvent e) {
        boolean processed = false;
        GraphEditor editor = e.getEditor();
        VisualModel model = e.getModel();
        if (e.getButton() == MouseEvent.BUTTON1) {
            Node deepestNode = HitMan.hitDeepest(e.getPosition(), model.getRoot(),
                    node -> (node instanceof VisualFunctionComponent) || (node instanceof VisualContact));

            VisualContact contact = null;
            if (deepestNode instanceof VisualFunctionContact) {
                contact = (VisualFunctionContact) deepestNode;
            } else if (deepestNode instanceof VisualFunctionComponent) {
                VisualFunctionComponent component = (VisualFunctionComponent) deepestNode;
                contact = component.getMainVisualOutput();
            }

            if ((contact != null) && contact.isDriver() && !contact.isZeroDelayPin()) {
                editor.getWorkspaceEntry().saveMemento();
                contact.getReferencedComponent().setForcedInit(!contact.getReferencedComponent().getForcedInit());
                processed = true;
            }
        }
        if (!processed) {
            super.mousePressed(e);
        }
    }

    @Override
    public Decorator getDecorator(final GraphEditor editor) {
        return node -> {
            MathNode mathNode = null;
            if (node instanceof VisualComponent) {
                mathNode = ((VisualComponent) node).getReferencedComponent();
            } else if (node instanceof VisualConnection) {
                mathNode = ((VisualConnection) node).getReferencedConnection();
            }

            if (mathNode != null) {
                if (mathNode instanceof FunctionComponent) {
                    return getComponentDecoration((FunctionComponent) mathNode);
                } else if (mathNode instanceof Contact) {
                    Circuit circuit = (Circuit) editor.getModel().getMathModel();
                    Contact driver = CircuitUtils.findDriver(circuit, mathNode, false);
                    return getContactDecoration(driver);
                } else {
                    return getConnectionDecoration(mathNode);
                }
            }
            return null;
        };
    }

    private Decoration getComponentDecoration(FunctionComponent component) {
        boolean forcedInit = false;
        boolean initialised = true;
        boolean hasProblem = false;
        for (Contact contact : component.getOutputs()) {
            forcedInit |= contact.getForcedInit();
            initialised &= initState.isInitialisedPin(contact);
            hasProblem |= initState.isProblematicPin(contact);
        }
        final Color color = component.getIsZeroDelay() ? AnalysisDecorationSettings.getDontTouchColor()
                : hasProblem ? AnalysisDecorationSettings.getProblemColor()
                : forcedInit ? AnalysisDecorationSettings.getFixerColor()
                : initialised ? AnalysisDecorationSettings.getClearColor() : null;

        return new Decoration() {
            @Override
            public Color getColorisation() {
                return color;
            }

            @Override
            public Color getBackground() {
                return color;
            }
        };
    }

    private Decoration getContactDecoration(Contact contact) {
        if (initState.isHigh(contact)) {
            return getHighLevelDecoration(contact);
        }
        if (initState.isLow(contact)) {
            return getLowLevelDecoration(contact);
        }
        return getExpectedLevelDecoration(contact);
    }

    private Decoration getConnectionDecoration(MathNode node) {
        Color color = initState.isHigh(node) ? CircuitSettings.getActiveWireColor()
                : initState.isLow(node) ? CircuitSettings.getInactiveWireColor() : null;
        return new Decoration() {
            @Override
            public Color getColorisation() {
                return color;
            }

            @Override
            public Color getBackground() {
                return color;
            }
        };
    }

    private Decoration getLowLevelDecoration(MathNode node) {
        final boolean initialisationConflict = initState.isConflict(node);
        return new StateDecoration() {
            @Override
            public Color getColorisation() {
                return initialisationConflict ? CircuitSettings.getActiveWireColor() : CircuitSettings.getInactiveWireColor();
            }

            @Override
            public Color getBackground() {
                return CircuitSettings.getInactiveWireColor();
            }

            @Override
            public boolean showForcedInit() {
                return true;
            }
        };
    }

    private Decoration getHighLevelDecoration(MathNode node) {
        final boolean initialisationConflict = initState.isConflict(node);
        return new StateDecoration() {
            @Override
            public Color getColorisation() {
                return initialisationConflict ? CircuitSettings.getInactiveWireColor() : CircuitSettings.getActiveWireColor();
            }

            @Override
            public Color getBackground() {
                return CircuitSettings.getActiveWireColor();
            }

            @Override
            public boolean showForcedInit() {
                return true;
            }
        };
    }

    private Decoration getExpectedLevelDecoration(Contact contact) {
        return new StateDecoration() {
            @Override
            public Color getColorisation() {
                return (contact == null) ? null
                        : contact.getInitToOne() ? CircuitSettings.getActiveWireColor()
                        : CircuitSettings.getInactiveWireColor();
            }

            @Override
            public Color getBackground() {
                return null;
            }

            @Override
            public boolean showForcedInit() {
                return true;
            }
        };
    }

    private class PortTable extends JTable {

        PortTable() {
            setModel(new PortTableModel());
            setFocusable(false);
            setRowSelectionAllowed(false);
            setRowHeight(SizeHelper.getComponentHeightFromFont(getFont()));
            setAutoResizeMode(JTable.AUTO_RESIZE_LAST_COLUMN);
            getTableHeader().setDefaultRenderer(new FlatHeaderRenderer(false));
            getTableHeader().setReorderingAllowed(false);
            setDefaultRenderer(Object.class, new PortTableCellRenderer());
            addMouseListener(new MouseAdapter() {
                @Override
                public void mouseClicked(MouseEvent e) {
                    Contact contact = initState.getDriverPort(getSelectedRow());
                    if (contact != null) {
                        contact.setForcedInit(!contact.getForcedInit());
                    }
                }
            });
        }

        public void refresh() {
            int forcedPortCount = initState.getForcedPorts().size();
            int uninitialisedPortCount = initState.getDriverPortCount() - forcedPortCount;

            String header = StatsUtils.getHtmlStatsHeader("Input port assumptions",
                    null, uninitialisedPortCount, forcedPortCount, null);

            GuiUtils.setColumnHeader(this, 0, header);

            Color color = uninitialisedPortCount > 0 ? GuiUtils.getTableHeaderBackgroundColor()
                    : forcedPortCount > 0 ? AnalysisDecorationSettings.getFixerColor()
                    : GuiUtils.getTableHeaderBackgroundColor();

            getTableHeader().setBackground(color);
            GuiUtils.refreshTable(this);
        }
    }

    private class PortTableCellRenderer implements TableCellRenderer {

        private final JLabel label = new JLabel() {
            @Override
            public void paint(Graphics g) {
                g.setColor(getBackground());
                g.fillRect(0, 0, getWidth(), getHeight());
                super.paint(g);
            }
        };

        @Override
        public Component getTableCellRendererComponent(JTable table, Object value,
                boolean isSelected, boolean hasFocus, int row, int col) {

            JLabel result = null;
            if (value instanceof Contact) {
                Contact contact = (Contact) value;
                String ref = initState.getContactReference(contact);
                label.setText(ref);

                Color color = contact.getForcedInit() ? AnalysisDecorationSettings.getFixerColor() : null;
                label.setBackground(ColorUtils.colorise(GuiUtils.getTableCellBackgroundColor(), color));

                boolean fits = GuiUtils.getLabelTextWidth(label) < GuiUtils.getTableColumnTextWidth(table, col);
                label.setToolTipText(fits ? null : label.getText());
                label.setBorder(GuiUtils.getTableCellBorder());
                result = label;
            }
            return result;
        }
    }

    private class PortTableModel extends AbstractTableModel {
        @Override
        public int getColumnCount() {
            return 1;
        }

        @Override
        public int getRowCount() {
            return initState.getDriverPortCount();
        }

        @Override
        public Object getValueAt(int row, int column) {
            return initState.getDriverPort(row);
        }
    }

    private class PinTable extends JTable {

        PinTable() {
            setModel(new PinTableModel());
            setFocusable(false);
            setRowSelectionAllowed(false);
            setRowHeight(SizeHelper.getComponentHeightFromFont(getFont()));
            setAutoResizeMode(JTable.AUTO_RESIZE_LAST_COLUMN);
            getTableHeader().setReorderingAllowed(false);
            getTableHeader().setDefaultRenderer(new FlatHeaderRenderer(false));
            setDefaultRenderer(Object.class, new PinTableCellRenderer());
            GuiUtils.setColumnWidth(this, 1, 30);

            addMouseListener(new MouseAdapter() {
                @Override
                public void mouseClicked(MouseEvent e) {
                    Contact contact = initState.getDriverPin(getSelectedRow());
                    if (contact != null) {
                        contact.setForcedInit(!contact.getForcedInit());
                    }
                }
            });
        }

        public void refresh() {
            int problematicPinCount = initState.getProblematicPins().size();
            int uninitialisedPinCount = initState.getUninitialisedPins().size();
            int forcedPinCount = initState.getForcedPins().size();
            int clearPinCount = initState.getDriverPinCount() - uninitialisedPinCount;

            String header = StatsUtils.getHtmlStatsHeader("Driver pins",
                    problematicPinCount, uninitialisedPinCount, forcedPinCount, clearPinCount);

            GuiUtils.setColumnHeader(this, 0, header);

            final Color color = problematicPinCount > 0 ? AnalysisDecorationSettings.getProblemColor()
                    : uninitialisedPinCount > 0 ? GuiUtils.getTableHeaderBackgroundColor()
                    : forcedPinCount > 0 ? AnalysisDecorationSettings.getFixerColor()
                    : clearPinCount > 0 ? AnalysisDecorationSettings.getClearColor()
                    : GuiUtils.getTableHeaderBackgroundColor();

            getTableHeader().setBackground(color);
            GuiUtils.refreshTable(this);
        }
    }

    private class PinTableCellRenderer implements TableCellRenderer {

        private final JLabel label = new JLabel() {
            @Override
            public void paint(Graphics g) {
                g.setColor(getBackground());
                g.fillRect(0, 0, getWidth(), getHeight());
                super.paint(g);
            }
        };

        @Override
        public Component getTableCellRendererComponent(JTable table, Object value,
                boolean isSelected, boolean hasFocus, int row, int col) {

            JLabel result = null;
            if (value instanceof FunctionContact) {
                FunctionContact contact = (FunctionContact) value;
                String ref = initState.getContactReference(contact);
                String text = initState.isRedundantForcedInit(contact) ? WARNING_PREFIX + ref : ref;
                label.setText(text);

                Color color = initState.isProblematicPin(contact) ? AnalysisDecorationSettings.getProblemColor()
                        : !initState.isInitialisedPin(contact) ? VisualCommonSettings.getFillColor()
                        : contact.isForcedDriver() ? AnalysisDecorationSettings.getFixerColor()
                        : initState.isInitialisedPin(contact) ? AnalysisDecorationSettings.getClearColor()
                        : null;

                label.setBackground(ColorUtils.colorise(GuiUtils.getTableCellBackgroundColor(), color));

                boolean fits = GuiUtils.getLabelTextWidth(label) < GuiUtils.getTableColumnTextWidth(table, col);
                label.setToolTipText(fits ? null : label.getText());
                label.setBorder(GuiUtils.getTableCellBorder());
                result = label;
            }
            return result;
        }
    }

    private class PinTableModel extends AbstractTableModel {
        @Override
        public int getColumnCount() {
            return 1;
        }

        @Override
        public int getRowCount() {
            return initState.getDriverPinCount();
        }

        @Override
        public Object getValueAt(int row, int col) {
            return initState.getDriverPin(row);
        }
    }

}