123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610 |
- package holeg.ui.view.inspector;
- import holeg.model.AbstractCanvasObject;
- import holeg.model.GroupNode;
- import holeg.model.HolonElement;
- import holeg.model.HolonElement.Priority;
- import holeg.model.HolonObject;
- 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.ui.view.image.Import;
- import holeg.utility.events.Action;
- import holeg.utility.listener.SimpleDocumentListener;
- import holeg.utility.pooling.Pool;
- 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.ArrayList;
- 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 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 net.miginfocom.swing.MigLayout;
- /**
- * This class shows all HolonElements of the selected HolonObjects.
- */
- public class InspectorTable extends JPanel {
- private static final Logger log = Logger.getLogger(InspectorTable.class.getName());
- private final static NumberFormatter doubleFormatter = generateNumberFormatter();
- private final Pool<ElementRow> rowPool = new Pool<>() {
- @Override
- public ElementRow create() {
- return new ElementRow();
- }
- };
- private final int maxDisplayedRowsNumber = 100;
- 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<SortButton<ElementRow>> headerButtonList = new ArrayList<>();
- // Events
- public Action<Set<HolonElement>> OnElementSelectionChanged = new Action<>();
- private int actualPage = 0;
- private int maxPageNumberForThisSelection = 0;
- // Colors
- // sorting
- private Comparator<ElementRow> actual_comp = (ElementRow a, ElementRow b) -> Float.compare(
- a.element.getEnergy(),
- b.element.getEnergy());
- private Thread populateRowsThread;
- private boolean abortThread = false;
- public InspectorTable(Control control) {
- control.OnSelectionChanged.addListener(this::updateInspectorUi);
- this.control = control;
- init();
- addHeader();
- }
- 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;
- }
- // Extract elements from a list of AbstractCanvasObjects
- static Stream<HolonElement> extractElements(Collection<AbstractCanvasObject> toInspect) {
- Stream<HolonElement> recursiveLayer = toInspect.stream()
- .filter(object -> object instanceof GroupNode).flatMap(
- obj -> ((GroupNode) obj).getAllHolonObjectsRecursive()
- .flatMap(HolonObject::elementsStream));
- Stream<HolonElement> thisLayer = toInspect.stream().filter(obj -> obj instanceof HolonObject)
- .flatMap(obj -> {
- HolonObject ho = (HolonObject) obj;
- return ho.elementsStream();
- });
- return Stream.concat(thisLayer, recursiveLayer);
- }
- 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 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 void initHeaderButtons() {
- Comparator<ElementRow> objectComp = Comparator.comparing(
- (ElementRow a) -> a.element.parentObject.getName());
- Comparator<ElementRow> idComp = Comparator.comparingInt(
- (ElementRow a) -> a.element.parentObject.getId());
- Comparator<ElementRow> deviceComp = Comparator.comparing((ElementRow a) -> a.element.getName());
- Comparator<ElementRow> energyComp = (ElementRow a, ElementRow b) -> Float.compare(
- a.element.getEnergy(),
- b.element.getEnergy());
- Comparator<ElementRow> priorityComp = Comparator.comparing(
- (ElementRow a) -> a.element.getPriority());
- Comparator<ElementRow> 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<ElementRow> 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 -> selectAllChanges(selectAllCheckBox.getSelectionState()));
- }
- private void selectAllChanges(TrippleCheckBox.State oldState) {
- switch (oldState) {
- 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<HolonObject> 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<AbstractCanvasObject> selection) {
- List<HolonElement> 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);
- this.OnElementSelectionChanged.broadcast(new HashSet<>());
- selectAllCheckBox.setSelectionState(State.selected);
- selectAllChanges(State.unselected);
- revalidate();
- repaint();
- });
- populateRowsThread.start();
- }
- private void updateInspectorUi() {
- // clone for concurrency
- Set<AbstractCanvasObject> selection = new HashSet<>(GuiSettings.getSelectedObjects());
- assignElementsToRowPool(selection);
- updateTableUi();
- }
- private void updateElementSelection() {
- Set<HolonElement> 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);
- }
- private enum PageAction {
- Increase, Decrease
- }
- private enum SortState {
- None, Descending, Ascending
- }
- /**
- * An ElementRow is a container to access an HolonElement with the right manipulation.
- */
- private class ElementRow {
- private final Container[] cellsInRow = new Container[7];
- private HolonElement element = null;
- // TextBoxes
- private JTextField elementNameTextField;
- private JCheckBox selectionBox;
- private JTextField idObjectTextField;
- private JFormattedTextField energyTextField;
- private JComboBox<Priority> comboBox;
- private JCheckBox activeCheckBox;
- private JTextField objectNameTextField;
- public ElementRow() {
- this.createEditFields();
- }
- public void addContainerToInspector() {
- for (Container cell : cellsInRow) {
- InspectorTable.this.add(cell);
- }
- }
- public boolean isSelected() {
- return selectionBox.isSelected();
- }
- public void setSelected(boolean value) {
- selectionBox.setSelected(value);
- // Color row
- for (Container cell : cellsInRow) {
- cell.setBackground(
- selectionBox.isSelected() ? ColorPreference.Inspector.Selected : Color.white);
- }
- }
- /**
- * Connects the ElementRow with a HolonElement.
- */
- 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);
- }
- /**
- * Removes the connection to the HolonElement.
- */
- 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;
- }
- }
- /**
- * This class handles the Sorting by tapping on the header of the InspectorTable.
- * @param <T>
- */
- private class SortButton<T> extends JButton {
- private final Comparator<T> comp;
- private SortState state = SortState.None;
- public SortButton(String text, Comparator<T> 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<ElementRow>) 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<T> getComp() {
- return switch (state) {
- case Descending -> comp.reversed();
- default -> comp;
- };
- }
- }
- }
|