Merge pull request #119 from informatik-ag-ngl/f/multi_selection

Added multi-selection to ComponentList
This commit is contained in:
Kai S. K. Engelbart 2020-03-20 22:12:40 +01:00 committed by GitHub
commit 34042f53df
5 changed files with 148 additions and 134 deletions

View File

@ -25,6 +25,7 @@ import envoy.client.net.WriteProxy;
import envoy.client.ui.ContextMenu; import envoy.client.ui.ContextMenu;
import envoy.client.ui.Theme; import envoy.client.ui.Theme;
import envoy.client.ui.list.ComponentList; import envoy.client.ui.list.ComponentList;
import envoy.client.ui.list.ComponentList.SelectionMode;
import envoy.client.ui.list.ComponentListModel; import envoy.client.ui.list.ComponentListModel;
import envoy.client.ui.primary.PrimaryButton; import envoy.client.ui.primary.PrimaryButton;
import envoy.client.ui.primary.PrimaryScrollPane; import envoy.client.ui.primary.PrimaryScrollPane;
@ -125,18 +126,21 @@ public class ChatWindow extends JFrame {
contentPane.setLayout(gbl_contentPane); contentPane.setLayout(gbl_contentPane);
messageList.setBorder(new EmptyBorder(space, space, space, space)); messageList.setBorder(new EmptyBorder(space, space, space, space));
messageList.setSelectionMode(SelectionMode.SINGLE);
Map<String, ActionListener> commands = new HashMap<>() { Map<String, ActionListener> commands = new HashMap<>() {
private static final long serialVersionUID = -2755235774946990126L; private static final long serialVersionUID = -2755235774946990126L;
{ {
put("forward selected message", put("forward selected message",
evt -> forwardMessageToMultipleUsers(messageList.getSelected().get(0), evt -> forwardMessageToMultipleUsers(messageList
ContactsChooserDialog.showForwardingDialog("Forward selected message to", messageList.getSelected().get(0), client))); .getSingleSelectedElement(),
ContactsChooserDialog
.showForwardingDialog("Forward selected message to", messageList.getSingleSelectedElement(), client)));
put("copy", evt -> { put("copy", evt -> {
// TODO should be enhanced to allow also copying of message attachments, // TODO should be enhanced to allow also copying of message attachments,
// especially pictures // especially pictures
StringSelection copy = new StringSelection(messageList.getSelected().get(0).getText()); StringSelection copy = new StringSelection(messageList.getSingleSelectedElement().getText());
Toolkit.getDefaultToolkit().getSystemClipboard().setContents(copy, copy); Toolkit.getDefaultToolkit().getSystemClipboard().setContents(copy, copy);
}); });
// TODO insert implementation to edit and delete messages // TODO insert implementation to edit and delete messages

View File

@ -15,6 +15,7 @@ import envoy.client.data.Settings;
import envoy.client.net.Client; import envoy.client.net.Client;
import envoy.client.ui.Theme; import envoy.client.ui.Theme;
import envoy.client.ui.list.ComponentList; import envoy.client.ui.list.ComponentList;
import envoy.client.ui.list.ComponentList.SelectionMode;
import envoy.client.ui.list.ComponentListModel; import envoy.client.ui.list.ComponentListModel;
import envoy.client.ui.renderer.MessageListRenderer; import envoy.client.ui.renderer.MessageListRenderer;
import envoy.client.ui.renderer.UserComponentListRenderer; import envoy.client.ui.renderer.UserComponentListRenderer;
@ -69,7 +70,7 @@ public class ContactsChooserDialog extends JDialog {
dialog.getContentPanel() dialog.getContentPanel()
.add(new MessageListRenderer(client.getSender().getId()).getListCellComponent(null, message, false), BorderLayout.NORTH); .add(new MessageListRenderer(client.getSender().getId()).getListCellComponent(null, message, false), BorderLayout.NORTH);
List<User> results = new ArrayList<>(); List<User> results = new ArrayList<>();
dialog.addOkButtonActionListener(e -> { results.addAll(dialog.getContactList().getSelected()); dialog.dispose(); }); dialog.addOkButtonActionListener(e -> { results.addAll(dialog.getContactList().getSelectedElements()); dialog.dispose(); });
ComponentListModel<User> contactListModel = dialog.getContactList().getModel(); ComponentListModel<User> contactListModel = dialog.getContactList().getModel();
client.getContacts().getContacts().forEach(user -> contactListModel.add(user)); client.getContacts().getContacts().forEach(user -> contactListModel.add(user));
dialog.setVisible(true); dialog.setVisible(true);
@ -82,7 +83,7 @@ public class ContactsChooserDialog extends JDialog {
* @since Envoy v0.1-beta * @since Envoy v0.1-beta
*/ */
private ContactsChooserDialog() { private ContactsChooserDialog() {
contactList.enableMultipleSelection(); contactList.setSelectionMode(SelectionMode.MULTIPLE);
// setBounds(100, 100, 450, 300); // setBounds(100, 100, 450, 300);
setModal(true); setModal(true);
getContentPane().setLayout(new BorderLayout()); getContentPane().setLayout(new BorderLayout());

View File

@ -3,8 +3,8 @@ package envoy.client.ui.list;
import java.awt.event.MouseAdapter; import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent; import java.awt.event.MouseEvent;
import java.awt.event.MouseListener; import java.awt.event.MouseListener;
import java.util.ArrayList; import java.util.HashSet;
import java.util.List; import java.util.Set;
import javax.swing.*; import javax.swing.*;
@ -25,15 +25,35 @@ public class ComponentList<E> extends JPanel {
private ComponentListModel<E> model; private ComponentListModel<E> model;
private ComponentListCellRenderer<E> renderer; private ComponentListCellRenderer<E> renderer;
private boolean multipleSelectionEnabled = false; private SelectionMode selectionMode = SelectionMode.NONE;
private Set<Integer> selection = new HashSet<>();
private List<Integer> currentSelections = new ArrayList<>();
private static final long serialVersionUID = 1759644503942876737L; private static final long serialVersionUID = 1759644503942876737L;
/** /**
* Sets the layout of this {@link ComponentList} to be a vertically oriented * Defines the possible modes of selection that can be performed by the user
* BoxLayout. *
* @since Envoy v0.1-beta
*/
public static enum SelectionMode {
/**
* Selection is completely ignored.
*/
NONE,
/**
* Only a single element can be selected.
*/
SINGLE,
/**
* Multiple elements can be selected regardless of their position.
*/
MULTIPLE
}
/**
* Initializes a default {@link ComponentList} without a model or a renderer.
* *
* @since Envoy v0.1-beta * @since Envoy v0.1-beta
*/ */
@ -79,8 +99,8 @@ public class ComponentList<E> extends JPanel {
// Synchronize with new model // Synchronize with new model
this.model = model; this.model = model;
if (model != null) this.model.setComponentList(this); if (model != null) model.setComponentList(this);
if (renderer != null) synchronizeModel(); synchronizeModel();
} }
/** /**
@ -92,11 +112,53 @@ public class ComponentList<E> extends JPanel {
public void synchronizeModel() { public void synchronizeModel() {
if (model != null && renderer != null) { if (model != null && renderer != null) {
removeAll(); removeAll();
model.forEach(this::add); model.forEach(this::addElement);
revalidate(); revalidate();
} }
} }
/**
* Selects a list element by index. If the element is already selected, it is
* removed from the selection.
*
* @param index the index of the selected component
* @since Envoy v0.1-beta
*/
public void selectElement(int index) {
if (selection.contains(index)) {
// Remove selection of element at index
updateElement(index, false);
selection.remove(index);
} else {
// Remove old selection if single selection is enabled
if (selectionMode == SelectionMode.SINGLE) clearSelection();
if (selectionMode != SelectionMode.NONE) {
// Assign new selection
selection.add(index);
// Update element
updateElement(index, true);
}
}
revalidate();
repaint();
}
/**
* Removes the current selection.
*
* @since Envoy v0.1-alpha
*/
public void clearSelection() {
selection.forEach(i -> updateElement(i, false));
selection.clear();
}
/** /**
* Adds an object to the list by rendering it with the current * Adds an object to the list by rendering it with the current
* {@link ComponentListCellRenderer}. * {@link ComponentListCellRenderer}.
@ -104,89 +166,26 @@ public class ComponentList<E> extends JPanel {
* @param elem the element to add * @param elem the element to add
* @since Envoy v0.3-alpha * @since Envoy v0.3-alpha
*/ */
void add(E elem) { add(elem, getComponentCount(), false); } void addElement(E elem) { addElement(elem, getComponentCount(), false); }
/** /**
* Adds an object to the list by rendering it with the current * Adds an object to the list by rendering it with the current
* {@link ComponentListRenderer}. * {@link ComponentListRenderer}. If the renderer is {@code null}, no action is
* performed.
* *
* @param elem the element to add * @param elem the element to add
* @param index the index at which to add the element * @param index the index at which to add the element
* @param isSelected the selection state of the element * @param isSelected the selection state of the element
* @since Envoy v0.1-beta * @since Envoy v0.1-beta
*/ */
private void add(E elem, int index, boolean isSelected) { private void addElement(E elem, int index, boolean isSelected) {
if (isSelected && !multipleSelectionEnabled) {
clearSelections();
currentSelections.add(index);
}
if (renderer != null) { if (renderer != null) {
final JComponent component = renderer.getListCellComponent(this, elem, isSelected); final JComponent component = renderer.getListCellComponent(this, elem, isSelected);
component.addMouseListener(getSelectionListener(index)); component.addMouseListener(getSelectionListener(index));
add(component, index); add(component, index);
} }
} }
/**
* @param componentIndex the index of the list component to which the mouse
* listener will be added
* @return a mouse listener calling the
* {@link ComponentList#componentSelected(int)} method with the
* component's index when a left click is performed by the user
* @since Envoy v0.1-beta
*/
private MouseListener getSelectionListener(int componentIndex) {
return new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) { if (SwingUtilities.isLeftMouseButton(e)) componentSelected(componentIndex); }
};
}
/**
* Gets called when a list component has been clicked on by the user. Any
* previous selections are then removed and the selected component gets
* redrawn.<br>
* <br>
* If the currently selected component gets selected again, the selection is
* removed.
*
* @param index the index of the selected component
* @since Envoy v0.1-beta
*/
private void componentSelected(int index) {
// removing selection of element at index
if (currentSelections.contains(index)) {
// Clear selection
updateSelection(index, false);
currentSelections.remove(Integer.valueOf(index));
} else {
// Remove old selection if multipleSelection is disabled
if (!multipleSelectionEnabled && currentSelections.size() > 0) clearSelections();
// Assign new selection
currentSelections.add(index);
// Update current selection
updateSelection(index, true);
}
revalidate();
repaint();
}
/**
* Clears all currently active selections.
*
* @since Envoy v0.1-beta
*/
private void clearSelections() {
currentSelections.forEach(index -> updateSelection(index, false));
currentSelections.clear();
}
/** /**
* Replaces a list element with a newly rendered instance of its contents. * Replaces a list element with a newly rendered instance of its contents.
* *
@ -194,21 +193,57 @@ public class ComponentList<E> extends JPanel {
* @param isSelected the selection state passed to the {@link ListCellRenderer} * @param isSelected the selection state passed to the {@link ListCellRenderer}
* @since Envoy v0.1-beta * @since Envoy v0.1-beta
*/ */
private void updateSelection(int index, boolean isSelected) { private void updateElement(int index, boolean isSelected) {
remove(index); remove(index);
add(model.get(index), index, isSelected); addElement(model.get(index), index, isSelected);
} }
/** /**
* @return the selected elements or null if none is selected * @param componentIndex the index of the list component to which the mouse
* listener will be added
* @return a mouse listener calling the
* {@link ComponentList#selectElement(int)} method with the
* component's index when a left click is performed by the user
* @since Envoy v0.1-beta * @since Envoy v0.1-beta
*/ */
public List<E> getSelected() { private MouseListener getSelectionListener(int componentIndex) {
List<E> selected = new ArrayList<>(); return new MouseAdapter() {
currentSelections.forEach(index -> selected.add(model.get(index)));
return selected; @Override
public void mouseClicked(MouseEvent e) { if (SwingUtilities.isLeftMouseButton(e)) selectElement(componentIndex); }
};
} }
/**
* @return a set of all selected indices
* @since Envoy v0.1-beta
*/
public Set<Integer> getSelection() { return selection; }
/**
* @return a set of all selected elements
* @since Envoy v0.1-beta
*/
public Set<E> getSelectedElements() {
var selectedElements = new HashSet<E>();
selection.forEach(i -> selectedElements.add(model.get(i)));
return selectedElements;
}
/**
* @return the index of an arbitrary selected element
* @throws java.util.NoSuchElementException if no selection is present
* @since Envoy v0.1-beta
*/
public int getSingleSelection() { return selection.iterator().next(); }
/**
* @return an arbitrary selected element
* @throws java.util.NoSuchElementException if no selection is present
* @since Envoy v0.1-beta
*/
public E getSingleSelectedElement() { return model.get(getSingleSelection()); }
/** /**
* @return the model * @return the model
* @since Envoy v0.1-beta * @since Envoy v0.1-beta
@ -228,37 +263,14 @@ public class ComponentList<E> extends JPanel {
public void setRenderer(ComponentListCellRenderer<E> renderer) { this.renderer = renderer; } public void setRenderer(ComponentListCellRenderer<E> renderer) { this.renderer = renderer; }
/** /**
* @return the multipleSelectionEnabled * @return the selection mode
* @since Envoy v0.1-beta * @since Envoy v0.1-beta
*/ */
public boolean isMultipleSelectionEnabled() { return multipleSelectionEnabled; } public SelectionMode getSelectionMode() { return selectionMode; }
/** /**
* @param multipleSelectionEnabled if true, multiple elements can be selected in * @param selectionMode the selection mode to set
* the component list
* @since Envoy v0.1-beta * @since Envoy v0.1-beta
*/ */
public void setMultipleSelectionEnabled(boolean multipleSelectionEnabled) { public void setSelectionMode(SelectionMode selectionMode) { this.selectionMode = selectionMode; }
this.multipleSelectionEnabled = multipleSelectionEnabled;
if (!multipleSelectionEnabled) clearSelections();
}
/**
* Enables the selection of multiple elements.
*
* @see ComponentList#disableMultipleSelection
* @since Envoy v0.1-beta
*/
public void enableMultipleSelection() { this.multipleSelectionEnabled = true; }
/**
* Allows only one element to be selected. True by default.
*
* @see ComponentList#enableMultipleSelection
* @since Envoy v0.1-beta
*/
public void disableMultipleSelection() {
this.multipleSelectionEnabled = false;
clearSelections();
}
} }

View File

@ -34,7 +34,7 @@ public final class ComponentListModel<E> implements Iterable<E>, Serializable {
*/ */
public boolean add(E e) { public boolean add(E e) {
if (componentList != null) { if (componentList != null) {
componentList.add(e); componentList.addElement(e);
componentList.revalidate(); componentList.revalidate();
} }
return elements.add(e); return elements.add(e);

View File

@ -9,6 +9,7 @@ import javax.swing.JLabel;
import javax.swing.JPanel; import javax.swing.JPanel;
import envoy.client.data.Settings; import envoy.client.data.Settings;
import envoy.client.ui.Color;
import envoy.client.ui.Theme; import envoy.client.ui.Theme;
import envoy.client.ui.list.ComponentList; import envoy.client.ui.list.ComponentList;
import envoy.client.ui.list.ComponentListCellRenderer; import envoy.client.ui.list.ComponentListCellRenderer;
@ -28,46 +29,42 @@ public class UserComponentListRenderer implements ComponentListCellRenderer<User
private static final long serialVersionUID = -2379244319112111284L; private static final long serialVersionUID = -2379244319112111284L;
@Override @Override
public JComponent getListCellComponent(ComponentList<? extends User> list, User value, boolean isSelected) { public JComponent getListCellComponent(ComponentList<? extends User> list, User user, boolean isSelected) {
final Theme theme = Settings.getInstance().getThemes().get(Settings.getInstance().getCurrentTheme());
final JPanel panel = new JPanel(); final JPanel panel = new JPanel();
panel.setLayout(new BorderLayout()); panel.setLayout(new BorderLayout());
final Theme theme = Settings.getInstance().getThemes().get(Settings.getInstance().getCurrentTheme());
// Panel background // Panel background
panel.setBackground(isSelected ? theme.getSelectionColor() : theme.getCellColor()); panel.setBackground(isSelected ? theme.getSelectionColor() : theme.getCellColor());
panel.setOpaque(true); panel.setOpaque(true);
panel.setPreferredSize(new Dimension(100, 35)); panel.setPreferredSize(new Dimension(100, 35));
// TODO add profile picture support in BorderLayout.West // TODO add profile picture support in BorderLayout.West
JLabel username = new JLabel(value.getName()); JLabel username = new JLabel(user.getName());
username.setForeground(theme.getUserNameColor()); username.setForeground(theme.getUserNameColor());
panel.add(username, BorderLayout.CENTER); panel.add(username, BorderLayout.CENTER);
final UserStatus status = value.getStatus(); final UserStatus status = user.getStatus();
JLabel statusLabel = new JLabel(status.toString()); JLabel statusLabel = new JLabel(status.toString());
java.awt.Color foreground; Color foreground;
switch (status) { switch (status) {
case OFFLINE:
foreground = java.awt.Color.LIGHT_GRAY;
break;
case AWAY: case AWAY:
foreground = java.awt.Color.YELLOW; foreground = Color.yellow;
break; break;
case BUSY: case BUSY:
foreground = java.awt.Color.BLUE; foreground = Color.blue;
break; break;
case ONLINE: case ONLINE:
foreground = java.awt.Color.GREEN; foreground = Color.green;
break; break;
default: default:
foreground = java.awt.Color.LIGHT_GRAY; foreground = Color.lightGray;
break; break;
} }
statusLabel.setForeground(foreground); statusLabel.setForeground(foreground);
panel.add(statusLabel, BorderLayout.NORTH); panel.add(statusLabel, BorderLayout.NORTH);
return panel; return panel;
} }
} }