package holeg.ui.view.inspector; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Container; import java.awt.Dimension; import java.awt.event.ActionEvent; import java.awt.event.InputEvent; import java.awt.event.KeyEvent; import java.math.RoundingMode; import java.text.NumberFormat; import java.util.Collection; import java.util.Comparator; import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Optional; import java.util.Set; import java.util.logging.Logger; import java.util.stream.Collectors; import java.util.stream.Stream; import java.util.ArrayList; import javax.swing.AbstractAction; import javax.swing.BorderFactory; import javax.swing.Box; import javax.swing.BoxLayout; import javax.swing.ImageIcon; import javax.swing.JButton; import javax.swing.JCheckBox; import javax.swing.JComboBox; import javax.swing.JFormattedTextField; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JTextField; import javax.swing.KeyStroke; import javax.swing.SwingConstants; import javax.swing.text.NumberFormatter; import holeg.model.AbstractCanvasObject; import holeg.model.GroupNode; import holeg.model.HolonElement; import holeg.model.HolonObject; import holeg.model.HolonElement.Priority; import holeg.preferences.ColorPreference; import holeg.preferences.ImagePreference; import holeg.ui.controller.Control; import holeg.ui.model.GuiSettings; import holeg.ui.view.component.TrippleCheckBox; import holeg.ui.view.component.TrippleCheckBox.State; import holeg.utility.events.Action; import holeg.ui.view.image.Import; import holeg.utility.listener.SimpleDocumentListener; import holeg.utility.pooling.Pool; import net.miginfocom.swing.MigLayout; public class InspectorTable extends JPanel { private static final Logger log = Logger.getLogger(InspectorTable.class.getName()); private final Pool rowPool = new Pool<>() { @Override public ElementRow create() { return new ElementRow(); } }; private final int maxDisplayedRowsNumber = 100; private int actualPage = 0; private int maxPageNumberForThisSelection = 0; private final Control control; // UI private final TrippleCheckBox selectAllCheckBox = new TrippleCheckBox(); private final JButton addButton = new JButton(); private final JButton duplicateButton = new JButton(); private final JButton deleteButton = new JButton(); private final JPanel buttonPanel = new JPanel(); private final JButton pageIncreaseButton = new JButton(); private final JButton pageDecreaseButton = new JButton(); private final JLabel pageInformationLabel = new JLabel(); private final JPanel pageSelectionPanel = new JPanel(); private final ArrayList> headerButtonList = new ArrayList<>(); private final static NumberFormatter doubleFormatter = generateNumberFormatter(); // sorting private Comparator actual_comp = (ElementRow a, ElementRow b) -> Float.compare(a.element.getEnergy(), b.element.getEnergy()); // Colors // Events public Action> OnElementSelectionChanged = new Action<>(); private Thread populateRowsThread; private boolean abortThread = false; public InspectorTable(Control control) { control.OnSelectionChanged.addListener(this::updateInspectorUi); this.control = control; init(); addHeader(); } private void init() { MigLayout layout = new MigLayout("insets 0,gap 0,wrap 7", // Layout Constraints "[][fill, grow][fill][fill, grow][fill, grow][][fill]", // Column constraints "[25!][20:20:20]"); // Row constraints this.setLayout(layout); initSelectAllCheckBox(); initButtons(); initKeyControls(); initHeaderButtons(); } private void initKeyControls() { this.getInputMap(WHEN_IN_FOCUSED_WINDOW) .put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, InputEvent.ALT_DOWN_MASK), "PageRight"); this.getInputMap(WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, InputEvent.ALT_DOWN_MASK), "PageLeft"); this.getActionMap().put("PageRight", new AbstractAction() { @Override public void actionPerformed(ActionEvent e) { performPageAction(PageAction.Increase); } }); this.getActionMap().put("PageLeft", new AbstractAction() { @Override public void actionPerformed(ActionEvent e) { performPageAction(PageAction.Decrease); } }); } private enum PageAction { Increase, Decrease }; private void performPageAction(PageAction action) { int newPageNumber = switch (action) { case Decrease -> Math.max(actualPage - 1, 0); default -> Math.min(actualPage + 1, maxPageNumberForThisSelection); }; if (newPageNumber != actualPage) { actualPage = newPageNumber; updateTableUi(); updatePageButtonAppearance(); } } private void updatePageButtonAppearance() { this.pageDecreaseButton.setEnabled(actualPage != 0); this.pageIncreaseButton.setEnabled(actualPage != maxPageNumberForThisSelection); } private static NumberFormatter generateNumberFormatter() { NumberFormat doubleFormat = NumberFormat.getNumberInstance(Locale.US); doubleFormat.setMinimumFractionDigits(1); doubleFormat.setMaximumFractionDigits(10); doubleFormat.setRoundingMode(RoundingMode.HALF_UP); doubleFormat.setGroupingUsed(false); NumberFormatter doubleFormatter = new NumberFormatter(doubleFormat); doubleFormatter.setCommitsOnValidEdit(true); doubleFormatter.setValueClass(Double.class); return doubleFormatter; } private void initHeaderButtons() { Comparator objectComp = Comparator.comparing((ElementRow a) -> a.element.parentObject.getName()); Comparator idComp = Comparator.comparingInt((ElementRow a) -> a.element.parentObject.getId()); Comparator deviceComp = Comparator.comparing((ElementRow a) -> a.element.getName()); Comparator energyComp = (ElementRow a, ElementRow b) -> Float.compare(a.element.getEnergy(), b.element.getEnergy()); Comparator priorityComp = Comparator.comparing((ElementRow a) -> a.element.getPriority()); Comparator activeComp = (ElementRow a, ElementRow b) -> Boolean.compare(a.element.active, b.element.active); headerButtonList.add(new SortButton<>("Object", objectComp)); headerButtonList.add(new SortButton<>("Id", idComp)); headerButtonList.add(new SortButton<>("Device", deviceComp)); headerButtonList.add(new SortButton<>("Energy", energyComp)); headerButtonList.add(new SortButton<>("Priority", priorityComp)); headerButtonList.add(new SortButton<>("Active", activeComp)); } private void addHeader() { this.add(selectAllCheckBox); for (SortButton button : headerButtonList) { this.add(button); } } private void initSelectAllCheckBox() { selectAllCheckBox.setBorder(BorderFactory.createEmptyBorder(2, 0, 0, 0)); // Pixel Perfect alignment selectAllCheckBox.setBorder(BorderFactory.createEmptyBorder(2, 3, 0, 0)); selectAllCheckBox.addActionListener(clicked -> { switch (selectAllCheckBox.getSelectionState()) { case mid_state_selection: case selected: { rowPool.getBorrowedStream().forEach(row -> row.setSelected(false)); duplicateButton.setEnabled(false); deleteButton.setEnabled(false); } break; case unselected: if (rowPool.getBorrowedCount() != 0) { rowPool.getBorrowedStream().forEach(row -> row.setSelected(true)); duplicateButton.setEnabled(true); deleteButton.setEnabled(true); } default: break; } updateElementSelection(); }); } private void initButtons() { buttonPanel.setLayout(new BoxLayout(buttonPanel, BoxLayout.LINE_AXIS)); buttonPanel.add(Box.createRigidArea(new Dimension(2, 0))); addButton.setIcon(new ImageIcon(Import.loadImage(ImagePreference.Button.Inspector.Add, 16, 16))); addButton.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 0)); addButton.addActionListener(clicked -> { Optional last = GuiSettings.getSelectedObjects().stream() .filter(obj -> obj instanceof HolonObject).reduce((prev, next) -> next) .map(obj -> (HolonObject) obj); last.ifPresent(obj -> { obj.add(new HolonElement(obj, "Element", 0.0f)); control.updateStateForCurrentIteration(); control.OnSelectionChanged.broadcast(); }); }); buttonPanel.add(addButton); duplicateButton.setIcon(new ImageIcon(Import.loadImage(ImagePreference.Button.Inspector.Duplicate, 16, 16))); duplicateButton.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 0)); duplicateButton.addActionListener(clicked -> { rowPool.getBorrowedStream().forEach(row -> { if (row.isSelected()) { row.element.parentObject.add(new HolonElement(row.element)); } }); control.updateStateForCurrentIteration(); control.OnSelectionChanged.broadcast(); }); buttonPanel.add(duplicateButton); deleteButton.setIcon(new ImageIcon(Import.loadImage(ImagePreference.Button.Inspector.Remove, 16, 16))); deleteButton.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 0)); deleteButton.addActionListener(clicked -> { log.info("DeleteButton"); rowPool.getBorrowedStream().forEach(row -> { if (row.isSelected()) { row.element.parentObject.remove(row.element); } }); log.info("row deleted"); control.updateStateForCurrentIteration(); log.info("updated"); control.OnSelectionChanged.broadcast(); log.info("selectionChanged"); }); buttonPanel.add(deleteButton); pageIncreaseButton.setIcon(new ImageIcon(Import.loadImage("images/buttons/page_increase.png", 16, 16))); pageIncreaseButton.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 0)); pageIncreaseButton.addActionListener(clicked -> this.performPageAction(PageAction.Increase)); pageDecreaseButton.setIcon(new ImageIcon(Import.loadImage("images/buttons/page_decrease.png", 16, 16))); pageDecreaseButton.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 0)); pageDecreaseButton.addActionListener(clicked -> this.performPageAction(PageAction.Decrease)); pageInformationLabel.setForeground(Color.gray); pageSelectionPanel.setLayout(new BoxLayout(pageSelectionPanel, BoxLayout.LINE_AXIS)); pageSelectionPanel.add(Box.createRigidArea(new Dimension(2, 0))); pageSelectionPanel.add(this.pageInformationLabel); pageSelectionPanel.add(Box.createHorizontalGlue()); pageSelectionPanel.add(this.pageDecreaseButton); pageSelectionPanel.add(this.pageIncreaseButton); } private void assignElementsToRowPool(Set selection) { List elementList = extractElements(selection).toList(); rowPool.getBorrowedStream().forEach(InspectorTable.ElementRow::clear); rowPool.clear(); for (HolonElement element : elementList) { ElementRow row = rowPool.get(); row.setElement(element); } actualPage = 0; this.maxPageNumberForThisSelection = elementList.size() / this.maxDisplayedRowsNumber; updatePageButtonAppearance(); } private void updateTableUi() { // Maybe abort current thread and join them if (populateRowsThread != null) { try { abortThread = true; populateRowsThread.join(); abortThread = false; } catch (InterruptedException e) { e.printStackTrace(); } } populateRowsThread = new Thread(() -> { int numberOfRows = rowPool.getBorrowedCount(); this.removeAll(); addHeader(); rowPool.getBorrowedStream().sorted(actual_comp).skip((long) actualPage * maxDisplayedRowsNumber) .limit(maxDisplayedRowsNumber).takeWhile(row -> !abortThread).forEach(ElementRow::addContainerToInspector); if (numberOfRows > maxDisplayedRowsNumber) { int lastDisplayedElementNumber = Math.min(numberOfRows, (actualPage + 1) * maxDisplayedRowsNumber); pageInformationLabel.setText(String.format("%d - %d from %d", 1 + actualPage * maxDisplayedRowsNumber, lastDisplayedElementNumber, numberOfRows)); this.add(pageSelectionPanel, "span, grow"); } this.add(buttonPanel, "span"); boolean isAtLeastOneHolonObjectSelected = GuiSettings.getSelectedObjects().stream() .anyMatch(object -> object instanceof HolonObject); this.addButton.setEnabled(isAtLeastOneHolonObjectSelected); duplicateButton.setEnabled(false); deleteButton.setEnabled(false); selectAllCheckBox.setSelectionState(State.unselected); revalidate(); repaint(); this.OnElementSelectionChanged.broadcast(new HashSet<>()); }); populateRowsThread.start(); } private void updateInspectorUi() { // clone for concurrency Set selection = new HashSet<>(GuiSettings.getSelectedObjects()); assignElementsToRowPool(selection); updateTableUi(); } private void updateElementSelection() { Set eleSet = rowPool.getBorrowedStream().filter(ElementRow::isSelected).map(row -> row.element) .collect(Collectors.toSet()); this.OnElementSelectionChanged.broadcast(eleSet); } private void updateButtonAppearance() { long count = rowPool.getBorrowedStream().filter(ElementRow::isSelected).count(); if (count == rowPool.getBorrowedCount()) { selectAllCheckBox.setSelectionState(State.selected); } else if (count == 0) { selectAllCheckBox.setSelectionState(State.unselected); } else { selectAllCheckBox.setSelectionState(State.mid_state_selection); } duplicateButton.setEnabled(count != 0); deleteButton.setEnabled(count != 0); } // Extract elements from a list of AbstractCanvasObjects static Stream extractElements(Collection toInspect) { Stream recursiveLayer = toInspect.stream().filter(object -> object instanceof GroupNode).flatMap( obj -> ((GroupNode) obj).getAllHolonObjectsRecursive().flatMap(HolonObject::elementsStream)); Stream thisLayer = toInspect.stream().filter(obj -> obj instanceof HolonObject).flatMap(obj -> { HolonObject ho = (HolonObject) obj; return ho.elementsStream(); }); return Stream.concat(thisLayer, recursiveLayer); } private class ElementRow { private HolonElement element = null; private final Container[] cellsInRow = new Container[7]; // TextBoxes private JTextField elementNameTextField; private JCheckBox selectionBox; private JTextField idObjectTextField; private JFormattedTextField energyTextField; private JComboBox comboBox; private JCheckBox activeCheckBox; private JTextField objectNameTextField; public ElementRow() { this.createEditFields(); } public void addContainerToInspector() { for (Container cell : cellsInRow) { InspectorTable.this.add(cell); } } public void setSelected(boolean value) { selectionBox.setSelected(value); // Color row for (Container cell : cellsInRow) { cell.setBackground(selectionBox.isSelected() ? ColorPreference.Inspector.Selected : Color.white); } } public boolean isSelected() { return selectionBox.isSelected(); } public void setElement(HolonElement element) { objectNameTextField.setText(element.parentObject.getName()); idObjectTextField.setText(Integer.toString(element.parentObject.getId())); elementNameTextField.setText(element.getName()); comboBox.setSelectedItem(element.getPriority()); activeCheckBox.setSelected(element.active); energyTextField.setValue(element.getEnergy()); this.element = element; setSelected(false); } public void clear(){ this.element = null; } private void createEditFields() { // Selected JPanel selectedColumnPanel = new JPanel(new BorderLayout()); selectedColumnPanel.setBackground(Color.white); selectedColumnPanel.setBorder(BorderFactory.createLineBorder(ColorPreference.Inspector.Border)); selectionBox = new JCheckBox(); selectionBox.addActionListener(clicked -> { setSelected(selectionBox.isSelected()); updateButtonAppearance(); updateElementSelection(); }); int columnHeight = 20; selectedColumnPanel.setMinimumSize(new Dimension(columnHeight, columnHeight)); selectedColumnPanel.setPreferredSize(new Dimension(columnHeight, columnHeight)); selectedColumnPanel.setMaximumSize(new Dimension(columnHeight, columnHeight)); selectionBox.setBorder(BorderFactory.createEmptyBorder(0, 2, 0, 0)); selectionBox.setOpaque(false); selectedColumnPanel.add(selectionBox, BorderLayout.CENTER); cellsInRow[0] = selectedColumnPanel; // ObjectName and ID objectNameTextField = new JTextField(); objectNameTextField.getDocument().addDocumentListener((SimpleDocumentListener) e -> { if(this.element != null){ this.element.parentObject.setName(objectNameTextField.getText()); } }); objectNameTextField.addActionListener(ae -> updateInspectorUi()); cellsInRow[1] = objectNameTextField; idObjectTextField = new JTextField(); idObjectTextField.setMinimumSize(idObjectTextField.getPreferredSize()); idObjectTextField.setBackground(Color.white); idObjectTextField.setEditable(false); idObjectTextField.setEnabled(false); cellsInRow[2] = idObjectTextField; // Name elementNameTextField = new JTextField(); elementNameTextField.getDocument().addDocumentListener((SimpleDocumentListener) e -> { if(this.element != null){ this.element.setName(elementNameTextField.getText()); } }); elementNameTextField.setBackground(Color.white); cellsInRow[3] = elementNameTextField; // Energy energyTextField = new JFormattedTextField(doubleFormatter); energyTextField.setInputVerifier(getInputVerifier()); energyTextField.setBackground(Color.white); energyTextField.addPropertyChangeListener(actionEvent -> { try { float energy = Float.parseFloat(energyTextField.getText()); if (this.element != null && this.element.getEnergy() != energy) { this.element.setEnergy(energy); control.updateStateForCurrentIteration(); } } catch (NumberFormatException e) { // Dont Update } }); cellsInRow[4] = energyTextField; // Priority comboBox = new JComboBox<>(Priority.values()); comboBox.setBackground(Color.white); comboBox.setEditable(false); comboBox.addActionListener(ae -> { if(this.element != null){ this.element.setPriority((Priority) comboBox.getSelectedItem()); control.updateStateForCurrentIteration(); } }); cellsInRow[5] = comboBox; JPanel checkBoxWrapperPanel = new JPanel(new BorderLayout()); checkBoxWrapperPanel.setBorder(BorderFactory.createLineBorder(ColorPreference.Inspector.Border)); checkBoxWrapperPanel.setBackground(Color.white); checkBoxWrapperPanel.setMinimumSize(new Dimension(columnHeight, columnHeight)); checkBoxWrapperPanel.setMaximumSize(new Dimension(Integer.MAX_VALUE, columnHeight)); // Active activeCheckBox = new JCheckBox(); activeCheckBox.setBorder(BorderFactory.createEmptyBorder(0, 2, 0, 0)); activeCheckBox.setOpaque(false); activeCheckBox.addActionListener(actionEvent -> { if(this.element != null){ this.element.active = activeCheckBox.isSelected(); control.updateStateForCurrentIteration(); } }); checkBoxWrapperPanel.add(activeCheckBox, BorderLayout.CENTER); cellsInRow[6] = checkBoxWrapperPanel; } } private enum SortState { None, Descending, Ascending }; private class SortButton extends JButton { private SortState state = SortState.None; private final Comparator comp; public SortButton(String text, Comparator comp) { super(text); this.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 0)); this.setContentAreaFilled(false); this.setBorderPainted(false); this.setFocusPainted(false); this.setHorizontalAlignment(SwingConstants.LEFT); this.comp = comp; this.addActionListener(onClick -> changeStateOnClick()); } @SuppressWarnings("unchecked") private void changeStateOnClick() { setState((this.state == SortState.Ascending) ? SortState.Descending : SortState.Ascending); headerButtonList.stream().filter(button -> (button != this)) .forEach(button -> button.setState(SortState.None)); actual_comp = (Comparator) getComp(); updateInspectorUi(); } public void setState(SortState state) { this.state = state; String text = this.getText(); // remove order symbols from text text = text.replaceAll("[\u25bc\u25b2]", ""); // update text switch (state) { case Descending -> this.setText(text + "\u25bc"); case Ascending -> this.setText(text + "\u25b2"); default -> this.setText(text); } } public Comparator getComp() { return switch (state) { case Descending -> comp.reversed(); default -> comp; }; } } }