Overhaul ListView to support checked states
- ListView can be defined to use nocheck, checked or radio - List can now scroll through up/down - Actual visual is handled in a DefaultListCell - Modify/add scenarios and catalog app - Relates #865
This commit is contained in:
@@ -16,9 +16,15 @@
|
||||
package org.springframework.shell.component.view.control;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
import java.util.ListIterator;
|
||||
import java.util.Set;
|
||||
import java.util.function.BiFunction;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.shell.component.view.control.cell.ListCell;
|
||||
import org.springframework.shell.component.view.event.KeyEvent.Key;
|
||||
import org.springframework.shell.component.view.event.MouseEvent;
|
||||
@@ -27,69 +33,63 @@ import org.springframework.shell.component.view.message.ShellMessageBuilder;
|
||||
import org.springframework.shell.component.view.screen.Screen;
|
||||
import org.springframework.shell.component.view.screen.ScreenItem;
|
||||
import org.springframework.shell.style.StyleSettings;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* {@link ListView} shows {@code list items} vertically.
|
||||
* {@code ListView} is a {@link View} showing items in a vertical list.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public class ListView<T> extends BoxView {
|
||||
|
||||
private final List<T> items = new ArrayList<>();
|
||||
private int selected = -1;
|
||||
|
||||
private final List<ListCell<T>> cells = new ArrayList<>();
|
||||
private Function<ListView<T>, ListCell<T>> factory = listView -> new ListCell<>();
|
||||
private final ItemStyle itemStyle;
|
||||
private int start = 0;
|
||||
private int pos = 0;
|
||||
private final Set<Integer> selected = new HashSet<>();
|
||||
private BiFunction<ListView<T>, T, ListCell<T>> factory = (listView, item) -> ListCell.of(item,
|
||||
listView.getItemStyle());
|
||||
|
||||
/**
|
||||
* Construct list view with no initial items.
|
||||
* Specifies how a item shows selection state.
|
||||
*/
|
||||
public enum ItemStyle {
|
||||
|
||||
/**
|
||||
* The item will be shown normally, with no check indicator. The default.
|
||||
*/
|
||||
NOCHECK,
|
||||
|
||||
/**
|
||||
* The item will indicate checked/un-checked state.
|
||||
*/
|
||||
CHECKED,
|
||||
|
||||
/**
|
||||
* The item is part of a radio group and will indicate selected state.
|
||||
*/
|
||||
RADIO
|
||||
}
|
||||
|
||||
public ListView() {
|
||||
this.itemStyle = ItemStyle.NOCHECK;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void drawInternal(Screen screen) {
|
||||
Rectangle rect = getInnerRect();
|
||||
int y = rect.y();
|
||||
|
||||
int selectedStyle = resolveThemeStyle(StyleSettings.TAG_HIGHLIGHT, ScreenItem.STYLE_BOLD);
|
||||
int i = 0;
|
||||
for (ListCell<T> c : cells) {
|
||||
c.setRect(rect.x(), y++, rect.width(), 1);
|
||||
if (i == selected) {
|
||||
c.updateSelected(true);
|
||||
c.setStyle(selectedStyle);
|
||||
}
|
||||
else {
|
||||
c.updateSelected(false);
|
||||
c.setBackgroundColor(-1);
|
||||
c.setStyle(-1);
|
||||
}
|
||||
c.updateSelected(i == selected);
|
||||
c.draw(screen);
|
||||
i++;
|
||||
}
|
||||
super.drawInternal(screen);
|
||||
public ListView(ItemStyle itemStyle) {
|
||||
Assert.notNull(itemStyle, "item style must be set");
|
||||
this.itemStyle = itemStyle;
|
||||
setItems(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a cell factory.
|
||||
*
|
||||
* @param factory the cell factory
|
||||
*/
|
||||
public void setCellFactory(Function<ListView<T>, ListCell<T>> factory) {
|
||||
this.factory = factory;
|
||||
public ListView(T[] items, ItemStyle itemStyle) {
|
||||
this(items != null ? Arrays.asList(items) : Collections.emptyList(), itemStyle);
|
||||
}
|
||||
|
||||
public void setItems(List<T> items) {
|
||||
this.items.clear();
|
||||
this.items.addAll(items);
|
||||
this.cells.clear();
|
||||
for (T i : items) {
|
||||
ListCell<T> c = factory.apply(this);
|
||||
cells.add(c);
|
||||
c.updateItem(i);
|
||||
}
|
||||
public ListView(@Nullable List<T> items, ItemStyle itemStyle) {
|
||||
Assert.notNull(itemStyle, "item style must be set");
|
||||
this.itemStyle = itemStyle;
|
||||
setItems(items);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -97,60 +97,187 @@ public class ListView<T> extends BoxView {
|
||||
registerKeyBinding(Key.CursorUp, () -> up());
|
||||
registerKeyBinding(Key.CursorDown, () -> down());
|
||||
registerKeyBinding(Key.Enter, () -> enter());
|
||||
registerKeyBinding(Key.Space, () -> space());
|
||||
|
||||
registerMouseBinding(MouseEvent.Type.Wheel | MouseEvent.Button.WheelUp, () -> up());
|
||||
registerMouseBinding(MouseEvent.Type.Wheel | MouseEvent.Button.WheelDown, () -> down());
|
||||
registerMouseBinding(MouseEvent.Type.Released | MouseEvent.Button.Button1, event -> click(event));
|
||||
}
|
||||
|
||||
private void up() {
|
||||
updateIndex(-1);
|
||||
dispatch(ShellMessageBuilder.ofView(this, ListViewSelectedItemChangedEvent.of(this, selectedItem())));
|
||||
public ItemStyle getItemStyle() {
|
||||
return itemStyle;
|
||||
}
|
||||
|
||||
private void down() {
|
||||
updateIndex(1);
|
||||
dispatch(ShellMessageBuilder.ofView(this, ListViewSelectedItemChangedEvent.of(this, selectedItem())));
|
||||
}
|
||||
|
||||
private void enter() {
|
||||
dispatch(ShellMessageBuilder.ofView(this, ListViewOpenSelectedItemEvent.of(this, selectedItem())));
|
||||
}
|
||||
|
||||
private void click(MouseEvent event) {
|
||||
int index = event.y() - getInnerRect().y();
|
||||
if (index > -1 && index < items.size()) {
|
||||
setSelected(index);
|
||||
private void updateCells() {
|
||||
cells.clear();
|
||||
for (T i : items) {
|
||||
ListCell<T> c = factory.apply(this, i);
|
||||
c.setItemStyle(getItemStyle());
|
||||
cells.add(c);
|
||||
}
|
||||
}
|
||||
|
||||
public void setSelected(int selected) {
|
||||
if (this.selected != selected) {
|
||||
this.selected = selected;
|
||||
public void setItems(@Nullable List<T> items) {
|
||||
this.items.clear();
|
||||
if (items != null) {
|
||||
this.items.addAll(items);
|
||||
}
|
||||
if (this.items.isEmpty()) {
|
||||
start = -1;
|
||||
pos = -1;
|
||||
}
|
||||
else {
|
||||
start = 0;
|
||||
pos = 0;
|
||||
}
|
||||
updateCells();
|
||||
}
|
||||
|
||||
private void updateSelectionStates() {
|
||||
int active = start + pos;
|
||||
if (itemStyle == ItemStyle.CHECKED) {
|
||||
boolean removed = selected.remove(active);
|
||||
if (!removed) {
|
||||
selected.add(active);
|
||||
}
|
||||
}
|
||||
else if (itemStyle == ItemStyle.RADIO) {
|
||||
selected.clear();
|
||||
selected.add(active);
|
||||
}
|
||||
ListIterator<ListCell<T>> iter = cells.listIterator();
|
||||
while (iter.hasNext()) {
|
||||
int index = iter.nextIndex();
|
||||
ListCell<T> c = iter.next();
|
||||
c.setSelected(selected.contains(index));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void drawInternal(Screen screen) {
|
||||
if (start > -1 && pos > -1) {
|
||||
Rectangle rect = getInnerRect();
|
||||
int y = rect.y();
|
||||
int selectedStyle = resolveThemeStyle(StyleSettings.TAG_HIGHLIGHT, ScreenItem.STYLE_BOLD);
|
||||
int i = 0;
|
||||
|
||||
for (ListCell<T> c : cells) {
|
||||
if (i < start) {
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
c.setRect(rect.x(), y++, rect.width(), 1);
|
||||
if (i == start + pos) {
|
||||
c.setStyle(selectedStyle);
|
||||
}
|
||||
else {
|
||||
c.setBackgroundColor(-1);
|
||||
c.setStyle(-1);
|
||||
}
|
||||
c.draw(screen);
|
||||
i++;
|
||||
if (i - start >= rect.height()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
super.drawInternal(screen);
|
||||
}
|
||||
|
||||
public void setCellFactory(BiFunction<ListView<T>, T, ListCell<T>> factory) {
|
||||
Assert.notNull(factory, "cell factory must be set");
|
||||
this.factory = factory;
|
||||
}
|
||||
|
||||
private void scrollIndex(boolean up) {
|
||||
int size = items.size();
|
||||
int maxItems = getInnerRect().height();
|
||||
int active = start + pos;
|
||||
if (up) {
|
||||
if (start > 0 && pos == 0) {
|
||||
start--;
|
||||
}
|
||||
else if (start + pos <= 0) {
|
||||
start = size - Math.min(maxItems, size);
|
||||
pos = Math.min(maxItems, size) - 1;
|
||||
}
|
||||
else {
|
||||
pos--;
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (start + pos + 1 < Math.min(maxItems, size)) {
|
||||
pos++;
|
||||
}
|
||||
else if (start + pos + 1 >= size) {
|
||||
start = 0;
|
||||
pos = 0;
|
||||
}
|
||||
else {
|
||||
start++;
|
||||
}
|
||||
}
|
||||
if (active != start + pos) {
|
||||
dispatch(ShellMessageBuilder.ofView(this, ListViewSelectedItemChangedEvent.of(this, selectedItem())));
|
||||
}
|
||||
}
|
||||
|
||||
private void scrollIndex(int step) {
|
||||
if (start < 0 && pos < 0) {
|
||||
return;
|
||||
}
|
||||
if (step < 0) {
|
||||
for (int i = step; i < 0; i++) {
|
||||
scrollIndex(true);
|
||||
}
|
||||
}
|
||||
else if (step > 0) {
|
||||
for (int i = step; i > 0; i--) {
|
||||
scrollIndex(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private T selectedItem() {
|
||||
T selectedItem = null;
|
||||
if (selected >= 0 && selected < items.size()) {
|
||||
selectedItem = items.get(selected);
|
||||
int active = start + pos;
|
||||
if (active >= 0 && active < items.size()) {
|
||||
selectedItem = items.get(active);
|
||||
}
|
||||
return selectedItem;
|
||||
}
|
||||
|
||||
private void updateIndex(int step) {
|
||||
int size = items.size();
|
||||
if (step > 0) {
|
||||
if (selected + step < size) {
|
||||
selected += step;
|
||||
}
|
||||
}
|
||||
else if (step < 0) {
|
||||
if (selected - step > 0) {
|
||||
selected += step;
|
||||
private void up() {
|
||||
scrollIndex(-1);
|
||||
}
|
||||
|
||||
private void down() {
|
||||
scrollIndex(1);
|
||||
}
|
||||
|
||||
private void enter() {
|
||||
if (itemStyle == ItemStyle.NOCHECK) {
|
||||
dispatch(ShellMessageBuilder.ofView(this, ListViewOpenSelectedItemEvent.of(this, selectedItem())));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private void space() {
|
||||
updateSelectionStates();
|
||||
}
|
||||
|
||||
private void click(MouseEvent event) {
|
||||
int index = event.y() - getInnerRect().y();
|
||||
int active = start + index;
|
||||
if (active >= 0 && active < items.size()) {
|
||||
pos = index;
|
||||
if (itemStyle == ItemStyle.NOCHECK) {
|
||||
dispatch(ShellMessageBuilder.ofView(this, ListViewSelectedItemChangedEvent.of(this, selectedItem())));
|
||||
return;
|
||||
}
|
||||
}
|
||||
updateSelectionStates();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -16,16 +16,23 @@
|
||||
package org.springframework.shell.component.view.control.cell;
|
||||
|
||||
import org.springframework.shell.component.view.control.AbstractControl;
|
||||
import org.springframework.shell.component.view.control.Cell;
|
||||
|
||||
/**
|
||||
* Base implementation of a {@link Cell}.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public abstract class AbstractCell<T> extends AbstractControl implements Cell<T> {
|
||||
|
||||
private boolean selected;
|
||||
private T item;
|
||||
private int style = -1;
|
||||
private int foregroundColor = -1;
|
||||
private int backgroundColor = -1;
|
||||
|
||||
public AbstractCell(T item) {
|
||||
this.item = item;
|
||||
}
|
||||
|
||||
@Override
|
||||
public T getItem() {
|
||||
return item;
|
||||
@@ -36,16 +43,6 @@ public abstract class AbstractCell<T> extends AbstractControl implements Cell<T>
|
||||
this.item = item;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSelected() {
|
||||
return selected;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateSelected(boolean selected) {
|
||||
this.selected = selected;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setStyle(int style) {
|
||||
this.style = style;
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* Copyright 2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.shell.component.view.control.cell;
|
||||
|
||||
import org.springframework.shell.component.view.control.ListView.ItemStyle;
|
||||
import org.springframework.shell.component.view.geom.Rectangle;
|
||||
import org.springframework.shell.component.view.screen.Screen;
|
||||
import org.springframework.shell.component.view.screen.Screen.Writer;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Base implementation of a {@link ListCell}.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public abstract class AbstractListCell<T> extends AbstractCell<T> implements ListCell<T> {
|
||||
|
||||
private ItemStyle itemStyle;
|
||||
private boolean selected;
|
||||
|
||||
public AbstractListCell(T item) {
|
||||
super(item);
|
||||
}
|
||||
|
||||
public AbstractListCell(T item, ItemStyle itemStyle) {
|
||||
super(item);
|
||||
this.itemStyle = itemStyle;
|
||||
}
|
||||
|
||||
protected String getText() {
|
||||
return getItem().toString();
|
||||
}
|
||||
|
||||
protected String getIndicator() {
|
||||
if (getItemStyle() == ItemStyle.CHECKED || getItemStyle() == ItemStyle.RADIO) {
|
||||
return selected ? "[x]" : "[ ]";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(Screen screen) {
|
||||
String indicator = getIndicator();
|
||||
String text = null;
|
||||
if (StringUtils.hasText(indicator)) {
|
||||
text = String.format("%s %s", indicator, getText());
|
||||
}
|
||||
else {
|
||||
text = getText();
|
||||
}
|
||||
Rectangle rect = getRect();
|
||||
Writer writer = screen.writerBuilder().style(getStyle()).color(getForegroundColor()).build();
|
||||
writer.text(text, rect.x(), rect.y());
|
||||
writer.background(rect, getBackgroundColor());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setItemStyle(ItemStyle itemStyle) {
|
||||
this.itemStyle = itemStyle;
|
||||
}
|
||||
|
||||
public ItemStyle getItemStyle() {
|
||||
return itemStyle;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSelected(boolean selected) {
|
||||
this.selected = selected;
|
||||
}
|
||||
|
||||
public boolean isSelected() {
|
||||
return selected;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* Copyright 2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.shell.component.view.control.cell;
|
||||
|
||||
import org.springframework.shell.component.view.control.Control;
|
||||
import org.springframework.shell.component.view.screen.Screen;
|
||||
|
||||
/**
|
||||
* Base interface for all cells. Typically a {@link Cell} is a building block in
|
||||
* a {@link View} not needing to be aware of how it is drawn into a {@link Screen}
|
||||
* but needs to aware of its "item", bounds via {@link Control} and other
|
||||
* properties like {@code background}.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public interface Cell<T> extends Control {
|
||||
|
||||
/**
|
||||
* Get item bound to a cell.
|
||||
*
|
||||
* @return item bound to a cell
|
||||
*/
|
||||
T getItem();
|
||||
|
||||
/**
|
||||
* Sets an item to bound into a cell.
|
||||
*
|
||||
* @param item item to bound into a cell
|
||||
*/
|
||||
void setItem(T item);
|
||||
|
||||
/**
|
||||
* Sets a style.
|
||||
*
|
||||
* @param style the style
|
||||
*/
|
||||
void setStyle(int style);
|
||||
|
||||
/**
|
||||
* Sets a foreground color.
|
||||
*
|
||||
* @param foregroundColor the background color
|
||||
*/
|
||||
void setForegroundColor(int foregroundColor);
|
||||
|
||||
/**
|
||||
* Sets a background color.
|
||||
*
|
||||
* @param backgroundColor the background color
|
||||
*/
|
||||
void setBackgroundColor(int backgroundColor);
|
||||
|
||||
}
|
||||
@@ -15,32 +15,45 @@
|
||||
*/
|
||||
package org.springframework.shell.component.view.control.cell;
|
||||
|
||||
import org.springframework.shell.component.view.control.Cell;
|
||||
import org.springframework.shell.component.view.control.ListView;
|
||||
import org.springframework.shell.component.view.geom.Rectangle;
|
||||
import org.springframework.shell.component.view.screen.Screen;
|
||||
import org.springframework.shell.component.view.screen.Screen.Writer;
|
||||
import org.springframework.shell.component.view.control.ListView.ItemStyle;
|
||||
|
||||
/**
|
||||
* The {@link Cell} type used within {@link ListView} instances
|
||||
* Extension of a {@link Cell} to make it aware of an item style and selection state.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public class ListCell<T> extends AbstractCell<T> {
|
||||
public interface ListCell<T> extends Cell<T> {
|
||||
|
||||
protected String text;
|
||||
/**
|
||||
* Set {@link ItemStyle}.
|
||||
*
|
||||
* @param itemStyle the item style
|
||||
*/
|
||||
void setItemStyle(ItemStyle itemStyle);
|
||||
|
||||
@Override
|
||||
public void draw(Screen screen) {
|
||||
Rectangle rect = getRect();
|
||||
Writer writer = screen.writerBuilder().style(getStyle()).color(getForegroundColor()).build();
|
||||
writer.text(text, rect.x(), rect.y());
|
||||
writer.background(rect, getBackgroundColor());
|
||||
/**
|
||||
* Set selection state.
|
||||
*
|
||||
* @param selected the selection state
|
||||
*/
|
||||
void setSelected(boolean selected);
|
||||
|
||||
/**
|
||||
* Helper method to build a {@code ListCell}.
|
||||
*
|
||||
* @param item the item
|
||||
* @param itemStyle the item style
|
||||
* @return a default list cell
|
||||
*/
|
||||
static <T> ListCell<T> of(T item, ItemStyle itemStyle) {
|
||||
return new DefaultListCell<T>(item, itemStyle);
|
||||
}
|
||||
|
||||
public void updateItem(T item) {
|
||||
setItem(item);
|
||||
this.text = item.toString();
|
||||
}
|
||||
static class DefaultListCell<T> extends AbstractListCell<T> {
|
||||
|
||||
DefaultListCell(T item, ItemStyle itemStyle) {
|
||||
super(item, itemStyle);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user