Initial ProgressView implementation
- Provides a basic feature set for ProgressView. - Progress can have a set of items which are driven by function style factory interface. - New class TextCell which is a base of progress items. - Not yet fully working and further changes, samples, docs will come in other commits. - Relates #995
This commit is contained in:
@@ -0,0 +1,439 @@
|
||||
/*
|
||||
* Copyright 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.
|
||||
* 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;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.springframework.shell.component.message.ShellMessageBuilder;
|
||||
import org.springframework.shell.component.view.control.cell.TextCell;
|
||||
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.ThemeResolver;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* {@code ProgressView} is used to show a progress indicator.
|
||||
*
|
||||
* Defaults to <textItem> <spinnerItem> <percentItem>
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public class ProgressView extends BoxView {
|
||||
|
||||
private final int tickStart;
|
||||
private final int tickEnd;
|
||||
private int tickValue;
|
||||
private boolean running = false;
|
||||
private String description;
|
||||
private Spinner spinner = Spinner.of(Spinner.LINE1, 130);
|
||||
private int spinnerFrame;
|
||||
private List<ProgressViewItem> items;
|
||||
private GridView grid;
|
||||
|
||||
private final static Function<Context, TextCell<Context>> DEFAULT_DESCRIPTION_FACTORY =
|
||||
(item) -> TextCell.of(item, ctx -> {
|
||||
return ctx.getDescription();
|
||||
});
|
||||
|
||||
private final static Function<Context, TextCell<Context>> DEFAULT_PERCENT_FACTORY =
|
||||
(item) -> {
|
||||
TextCell<Context> cell = TextCell.of(item, ctx -> {
|
||||
ProgressState state = ctx.getState();
|
||||
int percentAbs = state.tickEnd() - state.tickStart();
|
||||
int relativeValue = state.tickValue() - state.tickStart();
|
||||
int percent = (relativeValue * 100) / percentAbs;
|
||||
return String.format("%s%%", percent);
|
||||
});
|
||||
return cell;
|
||||
};
|
||||
|
||||
private final static Function<Context, TextCell<Context>> DEFAULT_SPINNER_FACTORY =
|
||||
item -> {
|
||||
TextCell<Context> cell = TextCell.of(item, ctx -> {
|
||||
Spinner spin = ctx.spinner();
|
||||
int spinState = ctx.getState().sprinnerFrame();
|
||||
return String.format("%s", spin.getFrames()[spinState]);
|
||||
});
|
||||
return cell;
|
||||
};
|
||||
|
||||
/**
|
||||
* Construct view with {@code tickStart 0} and {@code tickEnd 100}.
|
||||
*/
|
||||
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}.
|
||||
*
|
||||
* @param tickStart the tick start
|
||||
* @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 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}. Uses defined progress view items.
|
||||
*
|
||||
* @param tickStart the tick start
|
||||
* @param tickEnd the tick end
|
||||
* @param items the progress view items
|
||||
*/
|
||||
public ProgressView(int tickStart, int tickEnd, ProgressViewItem... items) {
|
||||
Assert.isTrue(tickStart >= 0, "Start tick value must be greater or equal than zero");
|
||||
Assert.isTrue(tickEnd > 0, "End tick value must be greater than zero");
|
||||
Assert.isTrue(tickEnd > tickStart, "End tick value must be greater than start tick value");
|
||||
this.tickStart = tickStart;
|
||||
this.tickEnd = tickEnd;
|
||||
this.tickValue = tickStart;
|
||||
this.items = Arrays.asList(items);
|
||||
initLayout();
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines an item within a progress view. Allows to set item's factory, size
|
||||
* and horizontal alignment.
|
||||
*/
|
||||
public static class ProgressViewItem {
|
||||
|
||||
private final Function<Context, TextCell<Context>> factory;
|
||||
private final int size;
|
||||
private final HorizontalAlign align;
|
||||
|
||||
public ProgressViewItem(Function<Context, TextCell<Context>> factory, int size, HorizontalAlign align) {
|
||||
this.factory = factory;
|
||||
this.size = size;
|
||||
this.align = align;
|
||||
}
|
||||
|
||||
public static ProgressViewItem ofText() {
|
||||
return ofText(0, HorizontalAlign.CENTER);
|
||||
}
|
||||
|
||||
public static ProgressViewItem ofText(int size, HorizontalAlign hAligh) {
|
||||
return new ProgressViewItem(DEFAULT_DESCRIPTION_FACTORY, size, hAligh);
|
||||
}
|
||||
|
||||
public static ProgressViewItem ofSpinner() {
|
||||
return ofSpinner(0, HorizontalAlign.CENTER);
|
||||
}
|
||||
|
||||
public static ProgressViewItem ofSpinner(int size, HorizontalAlign hAligh) {
|
||||
return new ProgressViewItem(DEFAULT_SPINNER_FACTORY, size, hAligh);
|
||||
}
|
||||
|
||||
public static ProgressViewItem ofPercent() {
|
||||
return ofPercent(0, HorizontalAlign.CENTER);
|
||||
}
|
||||
|
||||
public static ProgressViewItem ofPercent(int size, HorizontalAlign hAligh) {
|
||||
return new ProgressViewItem(DEFAULT_PERCENT_FACTORY, size, hAligh);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a progress description.
|
||||
*
|
||||
* @return a progress description
|
||||
*/
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a progress description. Used in items as a text item.
|
||||
*
|
||||
* @param description the progress description
|
||||
*/
|
||||
public void setDescription(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public void start() {
|
||||
if (running) {
|
||||
return;
|
||||
}
|
||||
running = true;
|
||||
ProgressState state = getState();
|
||||
dispatch(ShellMessageBuilder.ofView(this, ProgressViewStartEvent.of(this, state)));
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
if (!running) {
|
||||
return;
|
||||
}
|
||||
running = false;
|
||||
ProgressState state = getState();
|
||||
dispatch(ShellMessageBuilder.ofView(this, ProgressViewEndEvent.of(this, state)));
|
||||
}
|
||||
|
||||
private static class BoxWrapper extends BoxView {
|
||||
TextCell<Context> delegate;
|
||||
BoxWrapper(TextCell<Context> delegate) {
|
||||
this.delegate = delegate;
|
||||
}
|
||||
@Override
|
||||
protected void drawInternal(Screen screen) {
|
||||
Rectangle rect = getRect();
|
||||
delegate.setRect(rect.x(), rect.y(), rect.width(), rect.height());
|
||||
delegate.draw(screen);
|
||||
super.drawInternal(screen);
|
||||
}
|
||||
}
|
||||
|
||||
private void initLayout() {
|
||||
grid = new GridView();
|
||||
int[] columnSizes = new int[items.size()];
|
||||
int index = 0;
|
||||
for (ProgressViewItem item : items) {
|
||||
columnSizes[index] = item.size;
|
||||
TextCell<Context> cell = item.factory.apply(buildContext());
|
||||
cell.setHorizontalAlign(item.align);
|
||||
grid.addItem(new BoxWrapper(cell), 0, index, 1, 1, 0, 0);
|
||||
index++;
|
||||
}
|
||||
grid.setRowSize(0);
|
||||
grid.setColumnSize(columnSizes);
|
||||
|
||||
}
|
||||
|
||||
boolean needLastMillisInit = true;
|
||||
private long lastMillis;
|
||||
|
||||
@Override
|
||||
protected void drawInternal(Screen screen) {
|
||||
long current = System.currentTimeMillis();
|
||||
if (needLastMillisInit) {
|
||||
needLastMillisInit = false;
|
||||
lastMillis = current;
|
||||
}
|
||||
|
||||
long elapsedFromLast = current - lastMillis;
|
||||
if (elapsedFromLast > spinner.getInterval()) {
|
||||
spinnerFrame = (spinnerFrame + 1) % spinner.getFrames().length;
|
||||
lastMillis = current;
|
||||
}
|
||||
|
||||
Rectangle rect = getRect();
|
||||
int width = rect.width();
|
||||
width = width / 3;
|
||||
|
||||
grid.setRect(rect.x(), rect.y(), rect.width(), rect.height());
|
||||
grid.draw(screen);
|
||||
|
||||
super.drawInternal(screen);
|
||||
}
|
||||
|
||||
/**
|
||||
* Advance {@code tickValue} by a given count. Note that negative count
|
||||
* will advance backwards.
|
||||
*
|
||||
* @param count the count to advance tick value
|
||||
*/
|
||||
public void tickAdvance(int count) {
|
||||
setTickValue(tickValue + count);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a tick value. If value is lower or higher than {@code tickStart} or
|
||||
* {@code tickEnd} respectively {@code tickValue} will be set to low/high
|
||||
* bounds. This means {@code tickValue} is always kept within range inclusively.
|
||||
*
|
||||
* @param value the new tick value to set
|
||||
*/
|
||||
public void setTickValue(int value) {
|
||||
boolean changed = false;
|
||||
if (value > tickEnd) {
|
||||
changed = tickValue != tickEnd;
|
||||
tickValue = tickEnd;
|
||||
}
|
||||
else if (value < tickStart) {
|
||||
changed = tickValue != tickStart;
|
||||
tickValue = tickStart;
|
||||
}
|
||||
else {
|
||||
changed = tickValue != value;
|
||||
tickValue = value;
|
||||
}
|
||||
if (changed) {
|
||||
ProgressState state = getState();
|
||||
dispatch(ShellMessageBuilder.ofView(this, ProgressViewStateChangeEvent.of(this, state)));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a state of this {@code ProgressView}.
|
||||
*
|
||||
* @return a view progress state
|
||||
*/
|
||||
public ProgressState getState() {
|
||||
return ProgressState.of(tickStart, tickEnd, tickValue, running, spinnerFrame);
|
||||
}
|
||||
|
||||
private Context buildContext() {
|
||||
return new Context() {
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return ProgressView.this.getDescription();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProgressState getState() {
|
||||
return ProgressView.this.getState();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProgressView getView() {
|
||||
return ProgressView.this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int resolveThemeStyle(String tag, int defaultStyle) {
|
||||
return ProgressView.this.resolveThemeStyle(tag, defaultStyle);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Spinner spinner() {
|
||||
return ProgressView.this.spinner;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Context for {@code ProgressView} cell components.
|
||||
*/
|
||||
public interface Context {
|
||||
|
||||
/**
|
||||
* Get a {@link ProgressView} description.
|
||||
*
|
||||
* @return a progress description
|
||||
*/
|
||||
String getDescription();
|
||||
|
||||
/**
|
||||
* Get a state of a {@link ProgressView}.
|
||||
*
|
||||
* @return progress view state
|
||||
*/
|
||||
ProgressState getState();
|
||||
|
||||
/**
|
||||
* Gets an encapsulating owner view.
|
||||
*
|
||||
* @return an owner view
|
||||
*/
|
||||
ProgressView getView();
|
||||
|
||||
/**
|
||||
* Gets a {@link Spinner} frames.
|
||||
*
|
||||
* @return spinner frames
|
||||
*/
|
||||
Spinner spinner();
|
||||
|
||||
/**
|
||||
* Resolve style using existing {@link ThemeResolver} and {@code theme name}.
|
||||
* Use {@code defaultStyle} if resolving cannot happen.
|
||||
*
|
||||
* @param tag the style tag to use
|
||||
* @param defaultStyle the default style to use
|
||||
* @return resolved style
|
||||
*/
|
||||
int resolveThemeStyle(String tag, int defaultStyle);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Encapsulates a current running state of a {@link ProgressView}.
|
||||
*
|
||||
* @param tickStart the tick start value, zero or positive
|
||||
* @param tickEnd the tick end value, positive and more than tick start
|
||||
* @param tickValue the current tick value, within inclusive bounds of tick start/end
|
||||
* @param running the running state
|
||||
* @param spinnerFrame the current spinner frame index
|
||||
*/
|
||||
public record ProgressState(int tickStart, int tickEnd, int tickValue, boolean running, int sprinnerFrame) {
|
||||
|
||||
public static ProgressState of(int tickStart, int tickEnd, int tickValue, boolean running, int spinnerFrame) {
|
||||
return new ProgressState(tickStart, tickEnd, tickValue, running, spinnerFrame);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link ViewEventArgs} for events using {@link ProgressState}.
|
||||
*
|
||||
* @param state the progress state
|
||||
*/
|
||||
public record ProgressViewStateEventArgs(ProgressState state) implements ViewEventArgs {
|
||||
|
||||
public static ProgressViewStateEventArgs of(ProgressState state) {
|
||||
return new ProgressViewStateEventArgs(state);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link ViewEvent} indicating that proggress has been started.
|
||||
*
|
||||
* @param view the view sending an event
|
||||
* @param args the event args
|
||||
*/
|
||||
public record ProgressViewStartEvent(View view, ProgressViewStateEventArgs args) implements ViewEvent {
|
||||
|
||||
public static ProgressViewStartEvent of(View view, ProgressState state) {
|
||||
return new ProgressViewStartEvent(view, ProgressViewStateEventArgs.of(state));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link ViewEvent} indicating that proggress has been ended.
|
||||
*
|
||||
* @param view the view sending an event
|
||||
* @param args the event args
|
||||
*/
|
||||
public record ProgressViewEndEvent(View view, ProgressViewStateEventArgs args) implements ViewEvent {
|
||||
|
||||
public static ProgressViewEndEvent of(View view, ProgressState state) {
|
||||
return new ProgressViewEndEvent(view, ProgressViewStateEventArgs.of(state));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link ViewEvent} indicating that proggress state has been changed.
|
||||
*
|
||||
* @param view the view sending an event
|
||||
* @param args the event args
|
||||
*/
|
||||
public record ProgressViewStateChangeEvent(View view, ProgressViewStateEventArgs args) implements ViewEvent {
|
||||
|
||||
public static ProgressViewStateChangeEvent of(View view, ProgressState state) {
|
||||
return new ProgressViewStateChangeEvent(view, ProgressViewStateEventArgs.of(state));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,342 @@
|
||||
/*
|
||||
* Copyright 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.
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* {@code Spinner} represents how user is notified that something is happening
|
||||
* using a traditional spinner concept. Represented in a console with an array
|
||||
* of characters which are looped.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public interface Spinner {
|
||||
|
||||
/**
|
||||
* Gets a frame characters. Type is {@link String} to support unicode.
|
||||
*
|
||||
* @return a frame characters
|
||||
*/
|
||||
String[] getFrames();
|
||||
|
||||
/**
|
||||
* Gets an interval which should be used to estimate how often frame
|
||||
* should get changed. This is always an estimate as actual change
|
||||
* depends how ofter console gets redrawn.
|
||||
*
|
||||
* @return an interval in milliseconds
|
||||
*/
|
||||
int getInterval();
|
||||
|
||||
/**
|
||||
* Construct {@link Spinner} from given frames and interval.
|
||||
*
|
||||
* @param frames the spinner frames
|
||||
* @param interval the spinner interval
|
||||
* @return a Spinner implementation
|
||||
*/
|
||||
static Spinner of(String[] frames, int interval) {
|
||||
return new Spinner() {
|
||||
|
||||
@Override
|
||||
public String[] getFrames() {
|
||||
return frames;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getInterval() {
|
||||
return interval;
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
final static String[] LINE1 = new String[] {
|
||||
"-",
|
||||
"\\",
|
||||
"|",
|
||||
"/"
|
||||
};
|
||||
|
||||
final static String[] DOTS1 = new String[] {
|
||||
"⠋",
|
||||
"⠙",
|
||||
"⠹",
|
||||
"⠸",
|
||||
"⠼",
|
||||
"⠴",
|
||||
"⠦",
|
||||
"⠧",
|
||||
"⠇",
|
||||
"⠏"
|
||||
};
|
||||
|
||||
final static String[] DOTS2 = new String[] {
|
||||
"⣾",
|
||||
"⣽",
|
||||
"⣻",
|
||||
"⢿",
|
||||
"⡿",
|
||||
"⣟",
|
||||
"⣯",
|
||||
"⣷"
|
||||
};
|
||||
|
||||
final static String[] DOTS3 = new String[] {
|
||||
"⠋",
|
||||
"⠙",
|
||||
"⠚",
|
||||
"⠞",
|
||||
"⠖",
|
||||
"⠦",
|
||||
"⠴",
|
||||
"⠲",
|
||||
"⠳",
|
||||
"⠓"
|
||||
};
|
||||
|
||||
final static String[] DOTS4 = new String[] {
|
||||
"⠄",
|
||||
"⠆",
|
||||
"⠇",
|
||||
"⠋",
|
||||
"⠙",
|
||||
"⠸",
|
||||
"⠰",
|
||||
"⠠",
|
||||
"⠰",
|
||||
"⠸",
|
||||
"⠙",
|
||||
"⠋",
|
||||
"⠇",
|
||||
"⠆"
|
||||
};
|
||||
|
||||
final static String[] DOTS5 = new String[] {
|
||||
"⠋",
|
||||
"⠙",
|
||||
"⠚",
|
||||
"⠒",
|
||||
"⠂",
|
||||
"⠂",
|
||||
"⠒",
|
||||
"⠲",
|
||||
"⠴",
|
||||
"⠦",
|
||||
"⠖",
|
||||
"⠒",
|
||||
"⠐",
|
||||
"⠐",
|
||||
"⠒",
|
||||
"⠓",
|
||||
"⠋"
|
||||
};
|
||||
|
||||
final static String[] DOTS6 = new String[] {
|
||||
"⠁",
|
||||
"⠉",
|
||||
"⠙",
|
||||
"⠚",
|
||||
"⠒",
|
||||
"⠂",
|
||||
"⠂",
|
||||
"⠒",
|
||||
"⠲",
|
||||
"⠴",
|
||||
"⠤",
|
||||
"⠄",
|
||||
"⠄",
|
||||
"⠤",
|
||||
"⠴",
|
||||
"⠲",
|
||||
"⠒",
|
||||
"⠂",
|
||||
"⠂",
|
||||
"⠒",
|
||||
"⠚",
|
||||
"⠙",
|
||||
"⠉",
|
||||
"⠁"
|
||||
};
|
||||
|
||||
final static String[] DOTS7 = new String[] {
|
||||
"⠈",
|
||||
"⠉",
|
||||
"⠋",
|
||||
"⠓",
|
||||
"⠒",
|
||||
"⠐",
|
||||
"⠐",
|
||||
"⠒",
|
||||
"⠖",
|
||||
"⠦",
|
||||
"⠤",
|
||||
"⠠",
|
||||
"⠠",
|
||||
"⠤",
|
||||
"⠦",
|
||||
"⠖",
|
||||
"⠒",
|
||||
"⠐",
|
||||
"⠐",
|
||||
"⠒",
|
||||
"⠓",
|
||||
"⠋",
|
||||
"⠉",
|
||||
"⠈"
|
||||
};
|
||||
|
||||
final static String[] DOTS8 = new String[] {
|
||||
"⠁",
|
||||
"⠁",
|
||||
"⠉",
|
||||
"⠙",
|
||||
"⠚",
|
||||
"⠒",
|
||||
"⠂",
|
||||
"⠂",
|
||||
"⠒",
|
||||
"⠲",
|
||||
"⠴",
|
||||
"⠤",
|
||||
"⠄",
|
||||
"⠄",
|
||||
"⠤",
|
||||
"⠠",
|
||||
"⠠",
|
||||
"⠤",
|
||||
"⠦",
|
||||
"⠖",
|
||||
"⠒",
|
||||
"⠐",
|
||||
"⠐",
|
||||
"⠒",
|
||||
"⠓",
|
||||
"⠋",
|
||||
"⠉",
|
||||
"⠈",
|
||||
"⠈"
|
||||
};
|
||||
|
||||
final static String[] DOTS9 = new String[] {
|
||||
"⢹",
|
||||
"⢺",
|
||||
"⢼",
|
||||
"⣸",
|
||||
"⣇",
|
||||
"⡧",
|
||||
"⡗",
|
||||
"⡏"
|
||||
};
|
||||
|
||||
final static String[] DOTS10 = new String[] {
|
||||
"⢄",
|
||||
"⢂",
|
||||
"⢁",
|
||||
"⡁",
|
||||
"⡈",
|
||||
"⡐",
|
||||
"⡠"
|
||||
};
|
||||
|
||||
final static String[] DOTS11 = new String[] {
|
||||
"⠁",
|
||||
"⠂",
|
||||
"⠄",
|
||||
"⡀",
|
||||
"⢀",
|
||||
"⠠",
|
||||
"⠐",
|
||||
"⠈"
|
||||
};
|
||||
|
||||
final static String[] DOTS12 = new String[] {
|
||||
"⢀⠀",
|
||||
"⡀⠀",
|
||||
"⠄⠀",
|
||||
"⢂⠀",
|
||||
"⡂⠀",
|
||||
"⠅⠀",
|
||||
"⢃⠀",
|
||||
"⡃⠀",
|
||||
"⠍⠀",
|
||||
"⢋⠀",
|
||||
"⡋⠀",
|
||||
"⠍⠁",
|
||||
"⢋⠁",
|
||||
"⡋⠁",
|
||||
"⠍⠉",
|
||||
"⠋⠉",
|
||||
"⠋⠉",
|
||||
"⠉⠙",
|
||||
"⠉⠙",
|
||||
"⠉⠩",
|
||||
"⠈⢙",
|
||||
"⠈⡙",
|
||||
"⢈⠩",
|
||||
"⡀⢙",
|
||||
"⠄⡙",
|
||||
"⢂⠩",
|
||||
"⡂⢘",
|
||||
"⠅⡘",
|
||||
"⢃⠨",
|
||||
"⡃⢐",
|
||||
"⠍⡐",
|
||||
"⢋⠠",
|
||||
"⡋⢀",
|
||||
"⠍⡁",
|
||||
"⢋⠁",
|
||||
"⡋⠁",
|
||||
"⠍⠉",
|
||||
"⠋⠉",
|
||||
"⠋⠉",
|
||||
"⠉⠙",
|
||||
"⠉⠙",
|
||||
"⠉⠩",
|
||||
"⠈⢙",
|
||||
"⠈⡙",
|
||||
"⠈⠩",
|
||||
"⠀⢙",
|
||||
"⠀⡙",
|
||||
"⠀⠩",
|
||||
"⠀⢘",
|
||||
"⠀⡘",
|
||||
"⠀⠨",
|
||||
"⠀⢐",
|
||||
"⠀⡐",
|
||||
"⠀⠠",
|
||||
"⠀⢀",
|
||||
"⠀⡀"
|
||||
};
|
||||
|
||||
final static String[] DOTS13 = new String[] {
|
||||
"⣼",
|
||||
"⣹",
|
||||
"⢻",
|
||||
"⠿",
|
||||
"⡟",
|
||||
"⣏",
|
||||
"⣧",
|
||||
"⣶"
|
||||
};
|
||||
|
||||
final static String[] DOTS14 = new String[] {
|
||||
". ",
|
||||
" . ",
|
||||
" . ",
|
||||
" ."
|
||||
};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* Copyright 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.
|
||||
* 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 java.util.function.Function;
|
||||
|
||||
import org.springframework.shell.component.view.screen.Screen;
|
||||
import org.springframework.shell.component.view.screen.Screen.Writer;
|
||||
import org.springframework.shell.geom.HorizontalAlign;
|
||||
import org.springframework.shell.geom.Rectangle;
|
||||
import org.springframework.shell.geom.VerticalAlign;
|
||||
import org.springframework.shell.style.StyleSettings;
|
||||
|
||||
/**
|
||||
* Base implementation of a {@link TextCell}.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public abstract class AbstractTextCell<T> extends AbstractCell<T> implements TextCell<T> {
|
||||
|
||||
private Function<T, String> itemFunction;
|
||||
private HorizontalAlign hAlign = HorizontalAlign.CENTER;
|
||||
private VerticalAlign vAlign = VerticalAlign.CENTER;
|
||||
|
||||
public AbstractTextCell(T item, Function<T, String> itemFunction) {
|
||||
this(item, itemFunction, HorizontalAlign.CENTER, VerticalAlign.CENTER);
|
||||
}
|
||||
|
||||
public AbstractTextCell(T item, Function<T, String> itemFunction, HorizontalAlign hAlign, VerticalAlign vAlign) {
|
||||
super(item);
|
||||
this.itemFunction = itemFunction;
|
||||
this.hAlign = hAlign;
|
||||
this.vAlign = vAlign;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setHorizontalAlign(HorizontalAlign hAlign) {
|
||||
this.hAlign = hAlign;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setVerticalAlign(VerticalAlign vAlign) {
|
||||
this.vAlign = vAlign;
|
||||
}
|
||||
|
||||
protected String getBackgroundStyle() {
|
||||
return StyleSettings.TAG_BACKGROUND;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void drawBackground(Screen screen) {
|
||||
Rectangle rect = getRect();
|
||||
int bgColor = resolveThemeBackground(getBackgroundStyle(), getBackgroundColor(), -1);
|
||||
if (bgColor > -1) {
|
||||
screen.writerBuilder().build().background(rect, bgColor);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void drawContent(Screen screen) {
|
||||
String text = itemFunction.apply(getItem());
|
||||
if (text != null) {
|
||||
Rectangle rect = getRect();
|
||||
Writer writer = screen.writerBuilder().style(getStyle()).color(getForegroundColor()).build();
|
||||
writer.text(text, rect, hAlign, vAlign);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* Copyright 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.
|
||||
* 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 java.util.function.Function;
|
||||
|
||||
import org.springframework.shell.geom.HorizontalAlign;
|
||||
import org.springframework.shell.geom.VerticalAlign;
|
||||
|
||||
/**
|
||||
* Extension of a {@link Cell} to make it aware of an item style and selection state.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public interface TextCell<T> extends Cell<T> {
|
||||
|
||||
/**
|
||||
* Sets horizontal align for a text to draw. Defaults to
|
||||
* {@link HorizontalAlign#CENTER}.
|
||||
*
|
||||
* @param hAlign the horizontal align
|
||||
*/
|
||||
void setHorizontalAlign(HorizontalAlign hAlign);
|
||||
|
||||
/**
|
||||
* Sets vertical align for a text to draw. Defaults to
|
||||
* {@link VerticalAlign#CENTER}.
|
||||
*
|
||||
* @param vAlign the vertical align
|
||||
*/
|
||||
void setVerticalAlign(VerticalAlign vAlign);
|
||||
|
||||
/**
|
||||
* Helper method to build a {@code TextCell}.
|
||||
*
|
||||
* @param <T> type of an item
|
||||
* @param item the item
|
||||
* @param itemFunction the item function
|
||||
* @return a default text cell
|
||||
*/
|
||||
static <T> TextCell<T> of(T item, Function<T, String> itemFunction) {
|
||||
return new DefaultTextCell<T>(item, itemFunction);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to build a {@code TextCell}.
|
||||
*
|
||||
* @param <T> type of an item
|
||||
* @param item the item
|
||||
* @param itemFunction the item function
|
||||
* @param hAlign item horizontal alignment
|
||||
* @param vAlign item vertical alignment
|
||||
* @return
|
||||
*/
|
||||
static <T> TextCell<T> of(T item, Function<T, String> itemFunction, HorizontalAlign hAlign, VerticalAlign vAlign) {
|
||||
return new DefaultTextCell<T>(item, itemFunction, hAlign, vAlign);
|
||||
}
|
||||
|
||||
static class DefaultTextCell<T> extends AbstractTextCell<T> {
|
||||
|
||||
DefaultTextCell(T item, Function<T, String> itemFunction) {
|
||||
super(item, itemFunction);
|
||||
}
|
||||
|
||||
DefaultTextCell(T item, Function<T, String> itemFunction, HorizontalAlign hAlign, VerticalAlign vAlign) {
|
||||
super(item, itemFunction, hAlign, vAlign);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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.
|
||||
@@ -36,6 +36,7 @@ import org.springframework.test.util.ReflectionTestUtils;
|
||||
public class AbstractViewTests {
|
||||
|
||||
protected Screen screen24x80;
|
||||
protected Screen screen1x80;
|
||||
protected Screen screen7x10;
|
||||
protected Screen screen10x10;
|
||||
protected Screen screen0x0;
|
||||
@@ -44,6 +45,7 @@ public class AbstractViewTests {
|
||||
@BeforeEach
|
||||
void setup() {
|
||||
screen24x80 = new DefaultScreen(24, 80);
|
||||
screen1x80 = new DefaultScreen(1, 80);
|
||||
screen7x10 = new DefaultScreen(7, 10);
|
||||
screen0x0 = new DefaultScreen();
|
||||
screen10x10 = new DefaultScreen(10, 10);
|
||||
@@ -60,6 +62,7 @@ public class AbstractViewTests {
|
||||
|
||||
protected void clearScreens() {
|
||||
screen24x80.resize(24, 80);
|
||||
screen1x80.resize(1, 80);
|
||||
screen7x10.resize(7, 10);
|
||||
screen0x0.resize(0, 0);
|
||||
screen10x10.resize(10, 10);
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
/*
|
||||
* Copyright 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.
|
||||
* 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;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.test.StepVerifier;
|
||||
|
||||
import org.springframework.shell.component.view.control.ProgressView.ProgressViewEndEvent;
|
||||
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 static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
public class ProgressViewTests extends AbstractViewTests {
|
||||
|
||||
@Nested
|
||||
class Construction {
|
||||
|
||||
ProgressView view;
|
||||
|
||||
@Test
|
||||
void constructDefault() {
|
||||
view = new ProgressView();
|
||||
assertThat(view.getState().tickValue()).isEqualTo(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
void constructBounds() {
|
||||
view = new ProgressView(10, 30);
|
||||
assertThat(view.getState().tickValue()).isEqualTo(10);
|
||||
assertThat(view.getState().tickStart()).isEqualTo(10);
|
||||
assertThat(view.getState().tickEnd()).isEqualTo(30);
|
||||
}
|
||||
|
||||
void xxx() {
|
||||
view = new ProgressView(10, 30, ProgressViewItem.ofText());
|
||||
ProgressViewItem.ofText();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
class Visual {
|
||||
|
||||
ProgressView view;
|
||||
|
||||
@BeforeEach
|
||||
void setup() {
|
||||
view = new ProgressView();
|
||||
view.setDescription("name");
|
||||
view.setRect(0, 0, 80, 1);
|
||||
configure(view);
|
||||
}
|
||||
|
||||
@Test
|
||||
void defaultItems() {
|
||||
view.draw(screen1x80);
|
||||
assertThat(forScreen(screen1x80)).hasHorizontalText("name", 11, 0, 4);
|
||||
assertThat(forScreen(screen1x80)).hasHorizontalText("-", 39, 0, 1);
|
||||
assertThat(forScreen(screen1x80)).hasHorizontalText("0%", 65, 0, 2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void advance() {
|
||||
view.draw(screen1x80);
|
||||
assertThat(forScreen(screen1x80)).hasHorizontalText("0%", 65, 0, 2);
|
||||
view.tickAdvance(5);
|
||||
view.draw(screen1x80);
|
||||
assertThat(forScreen(screen1x80)).hasHorizontalText("5%", 65, 0, 2);
|
||||
view.tickAdvance(5);
|
||||
view.draw(screen1x80);
|
||||
assertThat(forScreen(screen1x80)).hasHorizontalText("10%", 65, 0, 3);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
class State {
|
||||
|
||||
ProgressView view;
|
||||
|
||||
@BeforeEach
|
||||
void setup() {
|
||||
view = new ProgressView();
|
||||
view.setRect(0, 0, 20, 1);
|
||||
configure(view);
|
||||
}
|
||||
|
||||
@Test
|
||||
void dontAdvanceBounds() {
|
||||
assertThat(view.getState().tickValue()).isEqualTo(0);
|
||||
view.tickAdvance(-1);
|
||||
assertThat(view.getState().tickValue()).isEqualTo(0);
|
||||
view.tickAdvance(100);
|
||||
assertThat(view.getState().tickValue()).isEqualTo(100);
|
||||
view.tickAdvance(1);
|
||||
assertThat(view.getState().tickValue()).isEqualTo(100);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
class Events {
|
||||
|
||||
ProgressView view;
|
||||
|
||||
@BeforeEach
|
||||
void setup() {
|
||||
view = new ProgressView();
|
||||
view.setRect(0, 0, 20, 1);
|
||||
configure(view);
|
||||
}
|
||||
|
||||
@Test
|
||||
void plainStartStop() {
|
||||
Flux<ProgressViewStartEvent> startEvents = eventLoop.viewEvents(ProgressViewStartEvent.class);
|
||||
Flux<ProgressViewEndEvent> endEvents = eventLoop.viewEvents(ProgressViewEndEvent.class);
|
||||
|
||||
StepVerifier startVerifier = StepVerifier.create(startEvents)
|
||||
.expectNextCount(1)
|
||||
.thenCancel()
|
||||
.verifyLater();
|
||||
StepVerifier endVerifier = StepVerifier.create(endEvents)
|
||||
.expectNextCount(1)
|
||||
.thenCancel()
|
||||
.verifyLater();
|
||||
|
||||
view.start();
|
||||
view.stop();
|
||||
startVerifier.verify(Duration.ofSeconds(1));
|
||||
endVerifier.verify(Duration.ofSeconds(1));
|
||||
}
|
||||
|
||||
@Test
|
||||
void stateChangeWithTickValue() {
|
||||
Flux<ProgressViewStateChangeEvent> changeEvents = eventLoop.viewEvents(ProgressViewStateChangeEvent.class);
|
||||
StepVerifier verifier = StepVerifier.create(changeEvents)
|
||||
.expectNextCount(1)
|
||||
.thenCancel()
|
||||
.verifyLater();
|
||||
|
||||
view.setTickValue(1);
|
||||
verifier.verify(Duration.ofSeconds(1));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
class Styling {
|
||||
|
||||
@Test
|
||||
void hasBorder() {
|
||||
ProgressView view = new ProgressView();
|
||||
configure(view);
|
||||
view.setShowBorder(true);
|
||||
view.setRect(0, 0, 80, 24);
|
||||
view.draw(screen24x80);
|
||||
assertThat(forScreen(screen24x80)).hasBorder(0, 0, 80, 24);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user