Overhaul StatusBar for better text use

- Can change item text
- Can position items by priority(true left, false right)
  and primary(sort for draw attempt)
- Fixes #1112
This commit is contained in:
Janne Valkealahti
2024-08-09 07:40:34 +01:00
parent 02011c898c
commit 9021238ede
2 changed files with 276 additions and 6 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2023 the original author or authors.
* Copyright 2023-2024 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.
@@ -17,12 +17,14 @@ package org.springframework.shell.component.view.control;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.ListIterator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.lang.Nullable;
import org.springframework.shell.component.message.ShellMessageBuilder;
import org.springframework.shell.component.view.event.MouseEvent;
import org.springframework.shell.component.view.event.MouseHandler;
@@ -30,17 +32,24 @@ import org.springframework.shell.component.view.screen.Screen;
import org.springframework.shell.component.view.screen.Screen.Writer;
import org.springframework.shell.geom.Rectangle;
import org.springframework.shell.style.StyleSettings;
import org.springframework.util.StringUtils;
/**
* {@link StatusBarView} shows {@link StatusItem items} horizontally and is
* typically used in layouts which builds complete terminal UI's.
*
* {@link StatusItem item} {@code primary} denotes if item is drawn to left
* or right, {@code priority} on which order items are drawn until bar runs
* out of space. Default {@code primary} is {@code true} and {@code priority}
* is {@code 0}.
*
* @author Janne Valkealahti
*/
public class StatusBarView extends BoxView {
private final Logger log = LoggerFactory.getLogger(StatusBarView.class);
private final List<StatusItem> items = new ArrayList<>();
private String itemSeparator = " | ";
public StatusBarView() {
this(new StatusItem[0]);
@@ -59,18 +68,64 @@ public class StatusBarView extends BoxView {
return StyleSettings.TAG_STATUSBAR_BACKGROUND;
}
/**
* Gets the item separator.
*
* @return a separator
*/
@Nullable
public String getItemSeparator() {
return itemSeparator;
}
/**
* Sets the item separator. Separator can be {@code null} or empty which
* essentially disables it.
*
* @param itemSeparator the item separator
*/
public void setItemSeparator(@Nullable String itemSeparator) {
this.itemSeparator = itemSeparator;
}
@Override
protected void drawInternal(Screen screen) {
Rectangle rect = getInnerRect();
log.debug("Drawing status bar to {}", rect);
Writer writer = screen.writerBuilder().build();
int x = rect.x();
int primaryX = rect.x();
int nonprimaryX = rect.x() + rect.width();
boolean primaryWritten = false;
boolean nonprimaryWritten = false;
ListIterator<StatusItem> iter = items.listIterator();
while (iter.hasNext()) {
StatusItem item = iter.next();
String text = String.format(" %s%s", item.getTitle(), iter.hasNext() ? " |" : "");
writer.text(text, x, rect.y());
x += text.length();
String text = item.getTitle();
if (text == null) {
continue;
}
String sep = getItemSeparator();
if (nonprimaryX - primaryX < (text.length() + (sep != null ? sep.length() : 0))) {
break;
}
if (item.primary) {
if (primaryWritten && StringUtils.hasText(sep)) {
text = sep + text;
}
writer.text(text, primaryX, rect.y());
primaryX += text.length();
primaryWritten = true;
}
else {
if (nonprimaryWritten && StringUtils.hasText(sep)) {
text = text + sep;
}
writer.text(text, nonprimaryX - text.length(), rect.y());
nonprimaryX -= text.length();
nonprimaryWritten = true;
}
}
super.drawInternal(screen);
}
@@ -121,6 +176,19 @@ public class StatusBarView extends BoxView {
public void setItems(List<StatusItem> items) {
this.items.clear();
this.items.addAll(items);
Collections.sort(this.items, (o1, o2) -> {
int ret = o1.priority - o2.priority;
if (ret == 0) {
if (o1.primary && !o2.primary) {
ret = -1;
}
else if (!o1.primary && o2.primary) {
ret = 1;
}
}
return ret;
});
// this.items.sort(null);
registerHotKeys();
}
@@ -152,6 +220,8 @@ public class StatusBarView extends BoxView {
private String title;
private Runnable action;
private Integer hotKey;
private boolean primary = true;
private int priority = 0;
public StatusItem(String title) {
this(title, null);
@@ -167,6 +237,14 @@ public class StatusBarView extends BoxView {
this.hotKey = hotKey;
}
public StatusItem(String title, Runnable action, Integer hotKey, boolean primary, int priority) {
this.title = title;
this.action = action;
this.hotKey = hotKey;
this.primary = primary;
this.priority = priority;
}
public static StatusItem of(String title) {
return new StatusItem(title);
}
@@ -179,10 +257,18 @@ public class StatusBarView extends BoxView {
return new StatusItem(title, action, hotKey);
}
public static StatusItem of(String title, Runnable action, Integer hotKey, boolean primary, int priority) {
return new StatusItem(title, action, hotKey, primary, priority);
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public Runnable getAction() {
return action;
}
@@ -201,6 +287,22 @@ public class StatusBarView extends BoxView {
return this;
}
public int getPriority() {
return priority;
}
public void setPriority(int priority) {
this.priority = priority;
}
public boolean isPrimary() {
return primary;
}
public void setPrimary(boolean primary) {
this.primary = primary;
}
}
/**

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2023 the original author or authors.
* Copyright 2023-2024 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.
@@ -17,6 +17,8 @@ package org.springframework.shell.component.view.control;
import java.time.Duration;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
@@ -55,6 +57,10 @@ class StatusBarViewTests extends AbstractViewTests {
view = new StatusBarView(Arrays.asList(StatusItem.of("item1")));
assertThat(view.getItems()).hasSize(1);
view = new StatusBarView(Arrays.asList(StatusItem.of("item1", null, null, false, 1)));
assertThat(view.getItems()).hasSize(1);
}
@Test
@@ -96,6 +102,66 @@ class StatusBarViewTests extends AbstractViewTests {
}
@Nested
class Sorting {
StatusItem p_0_1;
StatusItem p_0_2;
StatusItem p_0_3;
StatusItem p_1_1;
StatusItem p_2_1;
StatusItem n_0_1;
StatusItem n_0_2;
StatusItem n_0_3;
StatusBarView view;
@BeforeEach
void setup() {
p_0_1 = new StatusItem("p_0_1");
p_0_2 = new StatusItem("p_0_2");
p_0_3 = new StatusItem("p_0_3");
p_1_1 = new StatusItem("p_1_1");
p_1_1.setPriority(1);
p_2_1 = new StatusItem("p_2_1");
p_2_1.setPriority(2);
n_0_1 = new StatusItem("n_0_1");
n_0_1.setPrimary(false);
n_0_2 = new StatusItem("n_0_2");
n_0_2.setPrimary(false);
n_0_3 = new StatusItem("n_0_3");
n_0_3.setPrimary(false);
}
@Test
void defaultsOrderNotChanged() {
view = new StatusBarView(Arrays.asList(p_0_1, p_0_2, p_0_3));
assertThat(extractTitles()).containsExactly("p_0_1", "p_0_2", "p_0_3");
}
@Test
void primaryBeforeNonprimary() {
view = new StatusBarView(Arrays.asList(p_0_1, n_0_1));
assertThat(extractTitles()).containsExactly("p_0_1", "n_0_1");
view = new StatusBarView(Arrays.asList(n_0_1, p_0_1));
assertThat(extractTitles()).containsExactly("p_0_1", "n_0_1");
}
@Test
void priorityTakesOrder() {
view = new StatusBarView(Arrays.asList(p_0_1, p_1_1, p_2_1));
assertThat(extractTitles()).containsExactly("p_0_1", "p_1_1", "p_2_1");
view = new StatusBarView(Arrays.asList(p_2_1, p_0_1, p_1_1));
assertThat(extractTitles()).containsExactly("p_0_1", "p_1_1", "p_2_1");
}
private List<String> extractTitles() {
return view.getItems().stream().map(StatusItem::getTitle).collect(Collectors.toList());
}
}
@Nested
class Styling {
@@ -107,6 +173,108 @@ class StatusBarViewTests extends AbstractViewTests {
view.draw(screen24x80);
assertThat(forScreen(screen24x80)).hasBorder(0, 0, 80, 24);
}
@Test
void primaryItems() {
StatusItem item1 = new StatusItem("item1");
StatusItem item2 = new StatusItem("item2");
StatusBarView view = new StatusBarView(Arrays.asList(item1, item2));
view.setItemSeparator(null);
view.setRect(0, 0, 80, 1);
view.draw(screen1x80);
assertThat(forScreen(screen1x80)).hasHorizontalText("item1", 0, 0, 5);
assertThat(forScreen(screen1x80)).hasHorizontalText("item2", 5, 0, 5);
}
@Test
void nonprimaryItems() {
StatusItem item1 = new StatusItem("item1");
StatusItem item2 = new StatusItem("item2");
item1.setPrimary(false);
item2.setPrimary(false);
StatusBarView view = new StatusBarView(Arrays.asList(item1, item2));
view.setItemSeparator(null);
view.setRect(0, 0, 80, 1);
view.draw(screen1x80);
assertThat(forScreen(screen1x80)).hasHorizontalText("item1", 75, 0, 5);
assertThat(forScreen(screen1x80)).hasHorizontalText("item2", 70, 0, 5);
}
@Test
void primaryAndNonprimaryItems() {
StatusItem item1 = new StatusItem("item1");
StatusItem item2 = new StatusItem("item2");
item2.setPrimary(false);
StatusBarView view = new StatusBarView(Arrays.asList(item1, item2));
view.setItemSeparator(null);
view.setRect(0, 0, 80, 1);
view.draw(screen1x80);
assertThat(forScreen(screen1x80)).hasHorizontalText("item1", 0, 0, 5);
assertThat(forScreen(screen1x80)).hasHorizontalText("item2", 75, 0, 5);
}
@Test
void canChangeText() {
StatusItem item1 = new StatusItem("item1");
StatusBarView view = new StatusBarView(Arrays.asList(item1));
view.setItemSeparator(null);
view.setRect(0, 0, 80, 1);
view.draw(screen1x80);
assertThat(forScreen(screen1x80)).hasHorizontalText("item1", 0, 0, 5);
item1.setTitle("fake");
view.draw(screen1x80);
assertThat(forScreen(screen1x80)).hasHorizontalText("fake", 0, 0, 4);
}
@Test
void itemSeparator() {
StatusItem item1 = new StatusItem("item1");
StatusItem item2 = new StatusItem("item2");
StatusItem item3 = new StatusItem("item3");
item3.setPrimary(false);
StatusItem item4 = new StatusItem("item4");
item4.setPrimary(false);
StatusBarView view = new StatusBarView(Arrays.asList(item1, item2, item3, item4));
view.setRect(0, 0, 80, 1);
view.draw(screen1x80);
assertThat(forScreen(screen1x80)).hasHorizontalText("item1 | ", 0, 0, 8);
assertThat(forScreen(screen1x80)).hasHorizontalText("item2", 8, 0, 5);
assertThat(forScreen(screen1x80)).hasHorizontalText(" | item3", 72, 0, 8);
assertThat(forScreen(screen1x80)).hasHorizontalText("item4", 67, 0, 5);
view.setItemSeparator(null);
view.draw(screen1x80);
assertThat(forScreen(screen1x80)).hasHorizontalText("item1", 0, 0, 5);
assertThat(forScreen(screen1x80)).hasHorizontalText("item2", 5, 0, 5);
assertThat(forScreen(screen1x80)).hasHorizontalText("item3", 75, 0, 5);
assertThat(forScreen(screen1x80)).hasHorizontalText("item4", 70, 0, 5);
}
@Test
void skipItemsWhenOverflow() {
StatusItem item1 = new StatusItem("item11111111111111111111111111");
StatusItem item2 = new StatusItem("item22222222222222222222222222");
StatusItem item3 = new StatusItem("item33333333333333333333333333");
StatusBarView view = new StatusBarView(Arrays.asList(item1, item2, item3));
view.setItemSeparator(null);
view.setRect(0, 0, 80, 1);
view.draw(screen1x80);
assertThat(forScreen(screen1x80)).hasHorizontalText("item11111111111111111111111111", 0, 0, 30);
assertThat(forScreen(screen1x80)).hasHorizontalText("item22222222222222222222222222", 30, 0, 30);
assertThat(forScreen(screen1x80)).hasHorizontalText("", 60, 0, 20);
}
@Test
void nullTitleDontDraw() {
StatusItem item1 = new StatusItem("item1");
StatusItem item2 = new StatusItem(null);
StatusBarView view = new StatusBarView(Arrays.asList(item1, item2));
view.setItemSeparator(null);
view.setRect(0, 0, 80, 1);
view.draw(screen1x80);
assertThat(forScreen(screen1x80)).hasHorizontalText("item1", 0, 0, 5);
assertThat(forScreen(screen1x80)).hasHorizontalText("", 5, 0, 5);
}
}
@Nested