Revisit ProgressView
- Polish a lot of things - Do updates/redraw internally with eventloop as it'd be super inconvenient asking user to handle this externally - Rewrite sample to be more unified for various cases - Set eventloop from ViewComponent - Relates #995
This commit is contained in:
@@ -59,6 +59,7 @@ public class ViewComponent {
|
||||
this.view = view;
|
||||
this.viewComponentExecutor = viewComponentExecutor;
|
||||
this.eventLoop = terminalUI.getEventLoop();
|
||||
view.setEventLoop(this.eventLoop);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -15,22 +15,33 @@
|
||||
*/
|
||||
package org.springframework.shell.component.view.control;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import reactor.core.Disposable;
|
||||
import reactor.core.Disposables;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.support.MessageBuilder;
|
||||
import org.springframework.shell.component.message.ShellMessageBuilder;
|
||||
import org.springframework.shell.component.message.ShellMessageHeaderAccessor;
|
||||
import org.springframework.shell.component.message.StaticShellMessageHeaderAccessor;
|
||||
import org.springframework.shell.component.view.control.cell.TextCell;
|
||||
import org.springframework.shell.component.view.event.EventLoop;
|
||||
import org.springframework.shell.component.view.screen.Screen;
|
||||
import org.springframework.shell.geom.HorizontalAlign;
|
||||
import org.springframework.shell.geom.Rectangle;
|
||||
import org.springframework.shell.style.SpinnerSettings;
|
||||
import org.springframework.shell.style.ThemeResolver;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
|
||||
/**
|
||||
* {@code ProgressView} is used to show a progress indicator.
|
||||
@@ -100,26 +111,36 @@ public class ProgressView extends BoxView {
|
||||
};
|
||||
|
||||
/**
|
||||
* Construct view with {@code tickStart 0} and {@code tickEnd 100}.
|
||||
* Construct view with {@code tickStart 0} and {@code tickEnd 100}. Uses default
|
||||
* {@link ProgressViewItem}s.
|
||||
*/
|
||||
public ProgressView() {
|
||||
this(0, 100);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct view with given bounds for {@code tickStart} and {@code tickEnd}.
|
||||
* {@code tickStart} needs to be equal or more than zero. {@code tickEnd} needs
|
||||
* to be higher than {@code tickStart}. Defines default items for {@code text},
|
||||
* {@code spinner} and {@code percent}.
|
||||
* Construct view with {@code tickStart 0} and {@code tickEnd 100}. Uses default
|
||||
* {@link ProgressViewItem}s.
|
||||
*
|
||||
* @param tickStart the tick start
|
||||
* @param tickEnd the tick end
|
||||
* @param tickEnd the tick end
|
||||
*/
|
||||
public ProgressView(int tickStart, int tickEnd) {
|
||||
this(tickStart, tickEnd, new ProgressViewItem[] { ProgressViewItem.ofText(), ProgressViewItem.ofSpinner(),
|
||||
ProgressViewItem.ofPercent() });
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct view with given {@link ProgressViewItem}s using {@code tickStart 0}
|
||||
* and {@code tickEnd 100}.
|
||||
*
|
||||
* @param items the progress view items
|
||||
*/
|
||||
public ProgressView(ProgressViewItem... items) {
|
||||
this(0, 100, new ProgressViewItem[] { ProgressViewItem.ofText(), ProgressViewItem.ofSpinner(),
|
||||
ProgressViewItem.ofPercent() });
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct view with given bounds for {@code tickStart} and {@code tickEnd}.
|
||||
* {@code tickStart} needs to be equal or more than zero. {@code tickEnd} needs
|
||||
@@ -208,6 +229,9 @@ public class ProgressView extends BoxView {
|
||||
this.spinner = spinner;
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts a runtime logic. Call to already started progress has no effect.
|
||||
*/
|
||||
public void start() {
|
||||
if (running) {
|
||||
return;
|
||||
@@ -215,13 +239,62 @@ public class ProgressView extends BoxView {
|
||||
running = true;
|
||||
startTime = System.currentTimeMillis();
|
||||
ProgressState state = getState();
|
||||
scheduleTicks();
|
||||
dispatch(ShellMessageBuilder.ofView(this, ProgressViewStartEvent.of(this, state)));
|
||||
}
|
||||
|
||||
private Disposable.Composite disposables;
|
||||
private final String TAG_KEY = "ProgressView";
|
||||
private final String TAG_VALUE = UUID.randomUUID().toString();
|
||||
|
||||
private void scheduleTicks() {
|
||||
if (disposables != null) {
|
||||
return;
|
||||
}
|
||||
EventLoop eventLoop = getEventLoop();
|
||||
if (eventLoop == null) {
|
||||
return;
|
||||
}
|
||||
Flux<Message<?>> ticks = Flux.interval(Duration.ofMillis(50)).map(l -> {
|
||||
Message<Long> message = MessageBuilder
|
||||
.withPayload(l)
|
||||
.setHeader(ShellMessageHeaderAccessor.EVENT_TYPE, EventLoop.Type.USER)
|
||||
.setHeader(TAG_KEY, TAG_VALUE)
|
||||
.build();
|
||||
return message;
|
||||
});
|
||||
Disposable ticksDisposable = ticks.subscribe(m -> {
|
||||
eventLoop.dispatch(m);
|
||||
});
|
||||
Disposable eventsDisposable = eventLoop.events()
|
||||
.filter(m -> EventLoop.Type.USER.equals(StaticShellMessageHeaderAccessor.getEventType(m)))
|
||||
.filter(m -> ObjectUtils.nullSafeEquals(m.getHeaders().get(TAG_KEY), TAG_VALUE))
|
||||
.subscribe(m -> {
|
||||
requestRedraw();
|
||||
});
|
||||
disposables = Disposables.composite();
|
||||
disposables.add(eventsDisposable);
|
||||
disposables.add(ticksDisposable);
|
||||
}
|
||||
|
||||
private void requestRedraw() {
|
||||
EventLoop eventLoop = getEventLoop();
|
||||
if (eventLoop != null) {
|
||||
eventLoop.dispatch(ShellMessageBuilder.ofRedraw());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops a runtime logic.
|
||||
*/
|
||||
public void stop() {
|
||||
if (!running) {
|
||||
return;
|
||||
}
|
||||
if (disposables != null) {
|
||||
disposables.dispose();
|
||||
disposables = null;
|
||||
}
|
||||
running = false;
|
||||
ProgressState state = getState();
|
||||
dispatch(ShellMessageBuilder.ofView(this, ProgressViewEndEvent.of(this, state)));
|
||||
@@ -309,6 +382,7 @@ public class ProgressView extends BoxView {
|
||||
if (changed) {
|
||||
ProgressState state = getState();
|
||||
dispatch(ShellMessageBuilder.ofView(this, ProgressViewStateChangeEvent.of(this, state)));
|
||||
requestRedraw();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
package org.springframework.shell.component.view.control;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
@@ -27,6 +28,7 @@ import org.springframework.shell.component.view.control.ProgressView.ProgressVie
|
||||
import org.springframework.shell.component.view.control.ProgressView.ProgressViewItem;
|
||||
import org.springframework.shell.component.view.control.ProgressView.ProgressViewStartEvent;
|
||||
import org.springframework.shell.component.view.control.ProgressView.ProgressViewStateChangeEvent;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@@ -40,20 +42,23 @@ public class ProgressViewTests extends AbstractViewTests {
|
||||
@Test
|
||||
void constructDefault() {
|
||||
view = new ProgressView();
|
||||
assertThat(getViewItems(view)).hasSize(3);
|
||||
assertThat(view.getState().tickValue()).isEqualTo(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
void constructBounds() {
|
||||
view = new ProgressView(10, 30);
|
||||
assertThat(getViewItems(view)).hasSize(3);
|
||||
assertThat(view.getState().tickValue()).isEqualTo(10);
|
||||
assertThat(view.getState().tickStart()).isEqualTo(10);
|
||||
assertThat(view.getState().tickEnd()).isEqualTo(30);
|
||||
}
|
||||
|
||||
void xxx() {
|
||||
@Test
|
||||
void constructJustText() {
|
||||
view = new ProgressView(10, 30, ProgressViewItem.ofText());
|
||||
ProgressViewItem.ofText();
|
||||
assertThat(getViewItems(view)).hasSize(1);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -179,4 +184,10 @@ public class ProgressViewTests extends AbstractViewTests {
|
||||
|
||||
}
|
||||
|
||||
private static List<ProgressViewItem> getViewItems(ProgressView view) {
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
List<ProgressViewItem> items = (List<ProgressViewItem>) ReflectionTestUtils.getField(view, "items");
|
||||
return items;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,25 +15,17 @@
|
||||
*/
|
||||
package org.springframework.shell.samples.standard;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.support.MessageBuilder;
|
||||
import org.springframework.shell.command.annotation.Command;
|
||||
import org.springframework.shell.command.annotation.Option;
|
||||
import org.springframework.shell.component.ViewComponent;
|
||||
import org.springframework.shell.component.ViewComponent.ViewComponentRun;
|
||||
import org.springframework.shell.component.message.ShellMessageBuilder;
|
||||
import org.springframework.shell.component.message.ShellMessageHeaderAccessor;
|
||||
import org.springframework.shell.component.message.StaticShellMessageHeaderAccessor;
|
||||
import org.springframework.shell.component.view.TerminalUI;
|
||||
import org.springframework.shell.component.view.control.BoxView;
|
||||
import org.springframework.shell.component.view.control.InputView;
|
||||
import org.springframework.shell.component.view.control.ProgressView;
|
||||
import org.springframework.shell.component.view.control.ProgressView.ProgressViewItem;
|
||||
import org.springframework.shell.component.view.control.Spinner;
|
||||
import org.springframework.shell.component.view.event.EventLoop;
|
||||
import org.springframework.shell.geom.HorizontalAlign;
|
||||
import org.springframework.shell.geom.VerticalAlign;
|
||||
import org.springframework.shell.standard.AbstractShellComponent;
|
||||
@@ -97,131 +89,91 @@ public class ComponentUiCommands extends AbstractShellComponent {
|
||||
return String.format("Input was '%s'", input);
|
||||
}
|
||||
|
||||
private void runProgress(ProgressView view) {
|
||||
ViewComponent component = getViewComponentBuilder().build(view);
|
||||
|
||||
EventLoop eventLoop = component.getEventLoop();
|
||||
|
||||
Flux<Message<?>> ticks = Flux.interval(Duration.ofMillis(100)).map(l -> {
|
||||
Message<Long> message = MessageBuilder
|
||||
.withPayload(l)
|
||||
.setHeader(ShellMessageHeaderAccessor.EVENT_TYPE, EventLoop.Type.USER)
|
||||
.build();
|
||||
return message;
|
||||
});
|
||||
eventLoop.dispatch(ticks);
|
||||
|
||||
eventLoop.onDestroy(eventLoop.events()
|
||||
.filter(m -> EventLoop.Type.USER.equals(StaticShellMessageHeaderAccessor.getEventType(m)))
|
||||
.subscribe(m -> {
|
||||
if (m.getPayload() instanceof Long) {
|
||||
view.tickAdvance(5);
|
||||
eventLoop.dispatch(ShellMessageBuilder.ofRedraw());
|
||||
}
|
||||
}));
|
||||
|
||||
component.runAsync().await();
|
||||
}
|
||||
|
||||
@Command(command = "componentui progress1")
|
||||
public void progress1() {
|
||||
ProgressView view = new ProgressView();
|
||||
view.setDescription("name");
|
||||
@Command(command = "componentui progress")
|
||||
public void progress(
|
||||
@Option(defaultValue = "desc") String description,
|
||||
@Option(defaultValue = "true") boolean textEnabled,
|
||||
@Option(defaultValue = "true") boolean spinnerEnabled,
|
||||
@Option(defaultValue = "true") boolean percentEnabled,
|
||||
@Option(defaultValue = "0") int textSize,
|
||||
@Option(defaultValue = "0") int spinnerSize,
|
||||
@Option(defaultValue = "0") int percentSize,
|
||||
@Option(defaultValue = "CENTER") HorizontalAlign textAlign,
|
||||
@Option(defaultValue = "CENTER") HorizontalAlign spinnerAlign,
|
||||
@Option(defaultValue = "CENTER") HorizontalAlign percentAlign,
|
||||
@Option(defaultValue = "-1") int logMessagesRate,
|
||||
@Option(defaultValue = "200") long advanceSleep,
|
||||
@Option(defaultValue = "false") boolean logMessagesSleep
|
||||
) {
|
||||
ArrayList<ProgressViewItem> items = new ArrayList<>();
|
||||
if (textEnabled) {
|
||||
items.add(ProgressViewItem.ofText(textSize, textAlign));
|
||||
}
|
||||
if (spinnerEnabled) {
|
||||
items.add(ProgressViewItem.ofSpinner(spinnerSize, spinnerAlign));
|
||||
}
|
||||
if (percentEnabled) {
|
||||
items.add(ProgressViewItem.ofPercent(percentSize, percentAlign));
|
||||
}
|
||||
ProgressViewItem[] itemsArray = items.toArray(new ProgressViewItem[0]);
|
||||
ProgressView view = new ProgressView(itemsArray);
|
||||
view.setDescription(description);
|
||||
view.setRect(0, 0, 20, 1);
|
||||
view.start();
|
||||
|
||||
runProgress(view);
|
||||
}
|
||||
|
||||
@Command(command = "componentui progress2")
|
||||
public void progress2() {
|
||||
ProgressView view = new ProgressView(0, 100,
|
||||
ProgressViewItem.ofText(10, HorizontalAlign.LEFT),
|
||||
ProgressViewItem.ofSpinner(3, HorizontalAlign.LEFT),
|
||||
ProgressViewItem.ofPercent(0, HorizontalAlign.RIGHT));
|
||||
view.setDescription("name");
|
||||
view.setRect(0, 0, 20, 1);
|
||||
view.start();
|
||||
|
||||
runProgress(view);
|
||||
}
|
||||
|
||||
@Command(command = "componentui progress3")
|
||||
public void progress3() {
|
||||
ProgressView view = new ProgressView();
|
||||
view.setDescription("name");
|
||||
view.setRect(0, 0, 20, 1);
|
||||
view.setSpinner(Spinner.of(Spinner.DOTS1, 80));
|
||||
view.start();
|
||||
|
||||
runProgress(view);
|
||||
}
|
||||
|
||||
@Command(command = "componentui progress4")
|
||||
public void progress4() {
|
||||
ProgressView view = new ProgressView();
|
||||
view.setDescription("name");
|
||||
view.setRect(0, 0, 20, 1);
|
||||
view.setThemeResolver(getThemeResolver());
|
||||
view.start();
|
||||
|
||||
runProgress(view);
|
||||
}
|
||||
|
||||
@Command(command = "componentui progress5")
|
||||
public void progress5() {
|
||||
ProgressView view = new ProgressView(0, 100,
|
||||
ProgressViewItem.ofText(10, HorizontalAlign.LEFT),
|
||||
ProgressViewItem.ofSpinner(3, HorizontalAlign.LEFT),
|
||||
ProgressViewItem.ofPercent(0, HorizontalAlign.RIGHT));
|
||||
|
||||
view.setDescription("name");
|
||||
view.setRect(0, 0, 20, 1);
|
||||
view.start();
|
||||
|
||||
ViewComponent component = getViewComponentBuilder().build(view);
|
||||
component.setUseTerminalWidth(false);
|
||||
|
||||
Flux<Message<?>> ticks = Flux.interval(Duration.ofMillis(100)).map(l -> {
|
||||
Message<Long> message = MessageBuilder
|
||||
.withPayload(l)
|
||||
.setHeader(ShellMessageHeaderAccessor.EVENT_TYPE, EventLoop.Type.USER)
|
||||
.build();
|
||||
return message;
|
||||
});
|
||||
EventLoop eventLoop = component.getEventLoop();
|
||||
eventLoop.dispatch(ticks);
|
||||
eventLoop.onDestroy(eventLoop.events()
|
||||
.filter(m -> EventLoop.Type.USER.equals(StaticShellMessageHeaderAccessor.getEventType(m)))
|
||||
.subscribe(m -> {
|
||||
if (m.getPayload() instanceof Long) {
|
||||
view.tickAdvance(5);
|
||||
eventLoop.dispatch(ShellMessageBuilder.ofRedraw());
|
||||
}
|
||||
}));
|
||||
view.start();
|
||||
|
||||
ViewComponentRun run = component.runAsync();
|
||||
|
||||
for (int i = 0; i < 4; i++) {
|
||||
|
||||
for (int i = 0; i < 51; i++) {
|
||||
if (run.isDone()) {
|
||||
break;
|
||||
}
|
||||
try {
|
||||
Thread.sleep(2000);
|
||||
} catch (InterruptedException e) {
|
||||
}
|
||||
sleep(advanceSleep);
|
||||
if (run.isDone()) {
|
||||
break;
|
||||
}
|
||||
|
||||
String msg = String.format("%s ", i);
|
||||
getTerminal().writer().write(msg + System.lineSeparator());
|
||||
getTerminal().writer().flush();
|
||||
|
||||
if (logMessagesRate > 0 && (i % logMessagesRate) == 0) {
|
||||
int width = getTerminal().getWidth();
|
||||
String msg = String.format("%-" + width + "s", i);
|
||||
getTerminal().writer().write(msg + System.lineSeparator());
|
||||
getTerminal().writer().flush();
|
||||
}
|
||||
view.tickAdvance(1);
|
||||
}
|
||||
|
||||
if (logMessagesSleep) {
|
||||
view.stop();
|
||||
sleep(2000);
|
||||
view.start();
|
||||
}
|
||||
|
||||
for (int i = 51; i < 101; i++) {
|
||||
if (run.isDone()) {
|
||||
break;
|
||||
}
|
||||
sleep(advanceSleep);
|
||||
if (run.isDone()) {
|
||||
break;
|
||||
}
|
||||
if (logMessagesRate > 0 && (i % logMessagesRate) == 0) {
|
||||
int width = getTerminal().getWidth();
|
||||
String msg = String.format("%-" + width + "s", i);
|
||||
getTerminal().writer().write(msg + System.lineSeparator());
|
||||
getTerminal().writer().flush();
|
||||
}
|
||||
view.tickAdvance(1);
|
||||
}
|
||||
|
||||
view.stop();
|
||||
run.cancel();
|
||||
}
|
||||
|
||||
private static void sleep(long millis) {
|
||||
try {
|
||||
Thread.sleep(millis);
|
||||
} catch (InterruptedException e) {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user