diff --git a/spring-shell-core/src/main/java/org/springframework/shell/component/view/control/StatusBarView.java b/spring-shell-core/src/main/java/org/springframework/shell/component/view/control/StatusBarView.java index 85c73dce..a47be39c 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/component/view/control/StatusBarView.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/component/view/control/StatusBarView.java @@ -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 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 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 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; + } + } /** diff --git a/spring-shell-core/src/test/java/org/springframework/shell/component/view/control/StatusBarViewTests.java b/spring-shell-core/src/test/java/org/springframework/shell/component/view/control/StatusBarViewTests.java index 1f7f4e5b..b517aec6 100644 --- a/spring-shell-core/src/test/java/org/springframework/shell/component/view/control/StatusBarViewTests.java +++ b/spring-shell-core/src/test/java/org/springframework/shell/component/view/control/StatusBarViewTests.java @@ -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 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