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:
Janne Valkealahti
2023-09-09 08:59:37 +03:00
parent a679de0067
commit 51c250608b
12 changed files with 971 additions and 195 deletions

View File

@@ -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();
}
/**

View File

@@ -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;

View File

@@ -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;
}
}

View File

@@ -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);
}

View File

@@ -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);
}
}
}