From 55096a5d7746cb6e9369f2eb54e91abc7c235f4f Mon Sep 17 00:00:00 2001 From: Janne Valkealahti Date: Mon, 21 Aug 2023 08:56:52 +0300 Subject: [PATCH] Initial ButtonView - Relates #854 --- .../component/view/control/ButtonView.java | 122 ++++++++++++++ .../view/control/ButtonViewTests.java | 149 ++++++++++++++++++ 2 files changed, 271 insertions(+) create mode 100644 spring-shell-core/src/main/java/org/springframework/shell/component/view/control/ButtonView.java create mode 100644 spring-shell-core/src/test/java/org/springframework/shell/component/view/control/ButtonViewTests.java diff --git a/spring-shell-core/src/main/java/org/springframework/shell/component/view/control/ButtonView.java b/spring-shell-core/src/main/java/org/springframework/shell/component/view/control/ButtonView.java new file mode 100644 index 00000000..7efb5e50 --- /dev/null +++ b/spring-shell-core/src/main/java/org/springframework/shell/component/view/control/ButtonView.java @@ -0,0 +1,122 @@ +/* + * 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; + +import org.springframework.shell.component.view.event.KeyEvent.Key; +import org.springframework.shell.component.view.event.KeyHandler; +import org.springframework.shell.component.view.event.MouseEvent; +import org.springframework.shell.component.view.event.MouseHandler; +import org.springframework.shell.component.view.geom.Dimension; +import org.springframework.shell.component.view.geom.HorizontalAlign; +import org.springframework.shell.component.view.geom.Rectangle; +import org.springframework.shell.component.view.geom.VerticalAlign; +import org.springframework.shell.component.view.message.ShellMessageBuilder; +import org.springframework.shell.component.view.screen.Screen; +import org.springframework.shell.component.view.screen.Screen.Writer; + +/** + * {@code ButtonView} is a {@link View} with border and text acting as a button. + * + * @author Janne Valkealahti + */ +public class ButtonView extends BoxView { + + private String text; + private Runnable action; + + public ButtonView() { + this(null, null); + } + + public ButtonView(String text) { + this(text, null); + } + + public ButtonView(String text, Runnable action) { + this.text = text; + this.action = action; + } + + @Override + protected void initInternal() { + registerKeyBinding(Key.Enter, () -> keySelect()); + registerMouseBinding(MouseEvent.Type.Released | MouseEvent.Button.Button1, event -> mouseSelect(event)); + } + + @Override + public KeyHandler getKeyHandler() { + return super.getKeyHandler(); + } + + @Override + public MouseHandler getMouseHandler() { + return super.getMouseHandler(); + } + + @Override + protected void drawInternal(Screen screen) { + Rectangle rect = getInnerRect(); + Writer writer = screen.writerBuilder().layer(getLayer()).build(); + if (text != null) { + writer.border(rect.x(), rect.y(), rect.width(), rect.height()); + writer.text(text, rect, HorizontalAlign.CENTER, VerticalAlign.CENTER); + } + + super.drawInternal(screen); + } + + public Dimension getPreferredDimension() { + return null; + } + + public void setText(String text) { + this.text = text; + } + + public void setAction(Runnable action) { + this.action = action; + } + + private void keySelect() { + dispatch(); + } + + private void mouseSelect(MouseEvent event) { + dispatch(); + } + + private void dispatch() { + dispatch(ShellMessageBuilder.ofView(this, ButtonViewSelectEvent.of(this))); + if (action != null) { + dispatchRunnable(action); + } + } + + public record ButtonViewItemEventArgs() implements ViewEventArgs { + + public static ButtonViewItemEventArgs of() { + return new ButtonViewItemEventArgs(); + } + } + + public record ButtonViewSelectEvent(View view, ButtonViewItemEventArgs args) implements ViewEvent { + + public static ButtonViewSelectEvent of(View view) { + return new ButtonViewSelectEvent(view, ButtonViewItemEventArgs.of()); + } + } + +} diff --git a/spring-shell-core/src/test/java/org/springframework/shell/component/view/control/ButtonViewTests.java b/spring-shell-core/src/test/java/org/springframework/shell/component/view/control/ButtonViewTests.java new file mode 100644 index 00000000..f92e628c --- /dev/null +++ b/spring-shell-core/src/test/java/org/springframework/shell/component/view/control/ButtonViewTests.java @@ -0,0 +1,149 @@ +/* + * 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; + +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.ButtonView.ButtonViewSelectEvent; +import org.springframework.shell.component.view.event.KeyEvent; +import org.springframework.shell.component.view.event.KeyHandler.KeyHandlerResult; +import org.springframework.shell.component.view.event.MouseEvent; +import org.springframework.shell.component.view.event.MouseHandler.MouseHandlerResult; + +import static org.assertj.core.api.Assertions.assertThat; + +class ButtonViewTests extends AbstractViewTests { + + static final String TEXT_FIELD = "text"; + static final String ACTION_FIELD = "action"; + private ButtonView view; + + @Nested + class Construction { + + @Test + void defaultView() { + view = new ButtonView(); + assertThat(getStringField(view, TEXT_FIELD)).isNull(); + assertThat(getRunnableField(view, ACTION_FIELD)).isNull(); + } + + @Test + void text() { + view = new ButtonView("text"); + assertThat(getStringField(view, TEXT_FIELD)).isEqualTo("text"); + assertThat(getRunnableField(view, ACTION_FIELD)).isNull(); + } + + @Test + void textAndAction() { + view = new ButtonView("text", () -> {}); + assertThat(getStringField(view, TEXT_FIELD)).isEqualTo("text"); + assertThat(getRunnableField(view, ACTION_FIELD)).isNotNull(); + } + + @Test + void canSetTextAndAction() { + view = new ButtonView(); + view.setText("text"); + view.setAction(() -> {}); + assertThat(getStringField(view, TEXT_FIELD)).isEqualTo("text"); + assertThat(getRunnableField(view, ACTION_FIELD)).isNotNull(); + } + + } + + @Nested + class Events { + + @BeforeEach + void setup() { + view = new ButtonView(); + view.setRect(0, 0, 10, 10); + configure(view); + } + + @Test + void handlesMouseClick() { + MouseEvent click = mouseClick(1, 1); + + Flux actions = eventLoop + .viewEvents(ButtonViewSelectEvent.class); + StepVerifier verifier = StepVerifier.create(actions) + .expectNextCount(1) + .thenCancel() + .verifyLater(); + + MouseHandlerResult result = handleMouseClick(view, click); + + assertThat(result).isNotNull().satisfies(r -> { + assertThat(r.consumed()).isTrue(); + }); + verifier.verify(Duration.ofSeconds(1)); + } + + @Test + void handlesKeyEnter() { + Flux actions = eventLoop + .viewEvents(ButtonViewSelectEvent.class); + StepVerifier verifier = StepVerifier.create(actions) + .expectNextCount(1) + .thenCancel() + .verifyLater(); + + KeyHandlerResult result = handleKey(view, KeyEvent.Key.Enter); + + assertThat(result).isNotNull().satisfies(r -> { + assertThat(r.consumed()).isTrue(); + }); + verifier.verify(Duration.ofSeconds(1)); + } + + } + + @Nested + class Visual { + + @BeforeEach + void setup() { + view = new ButtonView("text"); + } + + @Test + void hasButtonText() { + view.setRect(0, 0, 10, 10); + view.draw(screen10x10); + assertThat(forScreen(screen10x10)).hasBorder(0, 0, 10, 10); + assertThat(forScreen(screen10x10)).hasHorizontalText("text", 3, 5, 4); + } + + @Test + void hasButton() { + view.setRect(2, 2, 6, 3); + view.draw(screen10x10); + assertThat(forScreen(screen10x10)).hasBorder(2, 2, 6, 3); + assertThat(forScreen(screen10x10)).hasHorizontalText("text", 2, 2, 4); + } + + } + +}