14. Режимы
Довольно часто программа подразумевает несколько режимов (вариантов) работы окна. Нам в задаче пригодятся панель информации и окно выбора файла. В этой главе мы пропишем их.
Пример готового проекта лежит здесь.
Для выполнения текущего задания Fork
от него делать запрещается.
В ходе работы вам будет нужно сделать несколько коммитов с промежуточными состояниями проекта, поэтому в прямом копировании нет смысла. Готовый проект даётся для того, чтобы было проще разобраться, как выполнить поставленное задание.
Если у вас не выполнено предыдущее задание или выполнено не полностью, то сделайте
Fork
готового проекта из предыдущей главы.
Панель информации
Варианты отображения окна будем называть режимами. Напишем для начала режим отображения информации.
Когда задача будет решена, мы будем переходить в режим вывода информации. По Esc
будет
выполняться выход.
Для начала введём перечисление режимов в классе Application
/**
* Режимы работы приложения
*/
public enum Mode {
/**
* Основной режим работы
*/
WORK,
/**
* Окно информации
*/
INFO,
/**
* работа с файлами
*/
FILE
}
Также добавим поле режима
/**
* Текущий режим(по умолчанию рабочий)
*/
public static Mode currentMode = Mode.WORK;
Мы сразу добавили режим и для работы с файлами. Теперь создадим пакет dialogs
, а
в нём класс PanelInfo
.
- PanelInfo.java
package dialogs;
import app.Application;
import controls.Button;
import controls.MultiLineLabel;
import io.github.humbleui.jwm.*;
import io.github.humbleui.skija.Canvas;
import misc.CoordinateSystem2i;
import misc.Vector2i;
import panels.Panel;
import static app.Colors.BUTTON_COLOR;
/**
* Панель управления
*/
public class PanelInfo extends Panel {
/**
* Отступы в панели управления
*/
private static final int CONTROL_PADDING = 5;
/**
* Кнопка принять
*/
private final Button accept;
/**
* заголовок
*/
private final MultiLineLabel infoLabel;
/**
* текст заголовка делаем статическим, чтобы можно было менять его
* из любого места, каждый экземпляр панели информации будет обращаться
* к этому полю при рисовании, но логика работы программы
* не предполагает создания нескольких экземпляров, так что всё ок
*/
private static String labelText;
/**
* Панель управления
*
* @param window окно
* @param drawBG флаг, нужно ли рисовать подложку
* @param color цвет подложки
* @param padding отступы
*/
public PanelInfo(Window window, boolean drawBG, int color, int padding) {
super(window, drawBG, color, padding);
// добавление вручную
infoLabel = new MultiLineLabel(window, false, backgroundColor, CONTROL_PADDING,
1, 2, 0, 0, 1, 1, "",
true, true);
accept = new Button(
window, false, BUTTON_COLOR, CONTROL_PADDING,
1, 2, 0, 1, 1, 1, "ОК",
true, true);
accept.setOnClick(() -> Application.currentMode = Application.Mode.WORK);
}
/**
* Вывести информацию
*
* @param text текст
*/
public static void show(String text) {
// задаём новый текст
labelText = text;
// переключаем вывод приложения на режим информации
Application.currentMode = Application.Mode.INFO;
}
/**
* Обработчик событий
*
* @param e событие
*/
@Override
public void accept(Event e) {
// вызываем обработчик предка
super.accept(e);
// событие движения мыши
if (e instanceof EventMouseMove ee) {
accept.checkOver(lastWindowCS.getRelativePos(new Vector2i(ee)));
// событие нажатия мыши
} else if (e instanceof EventMouseButton ee) {
if (!lastInside || !ee.isPressed())
return;
Vector2i relPos = lastWindowCS.getRelativePos(lastMove);
accept.click(relPos);
// перерисовываем окно
window.requestFrame();
// обработчик ввода текста
} else if (e instanceof EventKey ee) {
if (ee.isPressed()) {
// получаем код клавиши
Key key = ee.getKey();
// перебираем варианты
switch (key) {
// если esc
case ESCAPE -> Application.currentMode = Application.Mode.WORK;
// если enter
case ENTER -> Application.currentMode = Application.Mode.WORK;
}
}
}
}
/**
* Метод под рисование в конкретной реализации
*
* @param canvas область рисования
* @param windowCS СК окна
*/
@Override
public void paintImpl(Canvas canvas, CoordinateSystem2i windowCS) {
infoLabel.text = labelText;
accept.paint(canvas, windowCS);
infoLabel.paint(canvas, windowCS);
}
}
Код панели информации довольно тривиален. Мы выводим многострочный текст и снизу кнопку.
Если кликаем по кнопке, жмём Enter
или Esc
, возвращаемся в главное окно.
Чтобы переходить в режим рисования информации написан метод show(String text)
, который
задаёт значение статической переменной текста, а к ней уже обращается метод рисования панели.
Определим цвет диалогов в Colors
/**
* Цвет заливки панели
*/
public static final int DIALOG_BACKGROUND_COLOR = Misc.getColor(230, 70, 38, 83);
Теперь добавим панель информации в класс приложения Application
:
/**
* Панель информации
*/
private final PanelInfo panelInfo;
инициализируем её в конструкторе Application
...
// панель информации
panelInfo = new PanelInfo(window, true, DIALOG_BACKGROUND_COLOR, PANEL_PADDING);
...
В метод paint()
добавим рисование панелей
/**
* Рисование
*
* @param canvas низкоуровневый инструмент рисования примитивов от Skija
* @param windowCS СК окна
*/
public void paint(Canvas canvas, CoordinateSystem2i windowCS) {
// запоминаем изменения (пока что там просто заливка цветом)
canvas.save();
// очищаем канвас
canvas.clear(APP_BACKGROUND_COLOR);
// рисуем панели
panelRendering.paint(canvas, windowCS);
panelControl.paint(canvas, windowCS);
panelLog.paint(canvas, windowCS);
panelHelp.paint(canvas, windowCS);
// рисуем диалоги
switch (currentMode) {
case INFO -> panelInfo.paint(canvas, windowCS);
case FILE -> {}
}
canvas.restore();
}
Для режима файла пока что у нас написана заглушка
Теперь, в зависимости от режима, необходимо по-разному обрабатывать клавишу Esc
. Если
сейчас основной режим, тогда окно следует закрыть. А если другой режим, то перейти к рабочему.
Поэтому перепишем обработчик кнопки Esc
в классе Application
:
case ESCAPE -> {
// если сейчас основной режим
if (currentMode.equals(Mode.WORK)) {
// закрываем окно
window.close();
// завершаем обработку, иначе уже разрушенный контекст
// будет передан панелям
return;
} else if (currentMode.equals(Mode.INFO)) {
currentMode = Mode.WORK;
}
}
Также в конце обработчика перепишем вызовы обработчиков панелей в зависимости от режима. Для файлов тоже пока что оставим заглушку
switch (currentMode) {
case INFO -> panelInfo.accept(e);
case FILE -> {}
case WORK -> {
// передаём события на обработку панелям
panelControl.accept(e);
panelRendering.accept(e);
panelLog.accept(e);
}
}
Теперь дополним обработчик кнопки решения задачи в конструкторе PanelControl
solve.setOnClick(() -> {
if (!PanelRendering.task.isSolved()) {
PanelRendering.task.solve();
String s = "Задача решена\n" +
"Пересечений: " + PanelRendering.task.getCrossed().size() / 2 + "\n" +
"Отдельных точек: " + PanelRendering.task.getSingle().size();
PanelInfo.show(s + "\n\nНажмите Esc, чтобы вернуться");
PanelLog.success(s);
solve.text = "Сбросить";
} else {
cancelTask();
}
window.requestFrame();
});
Создайте новый коммит info panel
и отправьте его на сервер.
Панель файлов
Для начала напишем панель списка, а потом добавим её на панель выбора файлов.
Для начала определим цвета скроллера в классе Colors
/**
* Цвет заливки панели
*/
public static final int SCROLLER_BACKGROUND_COLOR = Misc.getColor(150, 83, 38, 70);
/**
* Цвет заливки панели
*/
public static final int SCROLLER_COLOR = Misc.getColor(255, 83, 38, 70);
Теперь пропишем панель со списком PanelList
в пакете panels
и панель выбора файла PanelSelectFile
в пакете dialogs
- PanelList.java
- PanelSelectFile.java
package panels;
import controls.Button;
import io.github.humbleui.jwm.*;
import io.github.humbleui.skija.Canvas;
import io.github.humbleui.skija.Paint;
import io.github.humbleui.skija.RRect;
import misc.CoordinateSystem2i;
import misc.Vector2i;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Supplier;
import static app.Colors.*;
/**
* Панель списка
*/
public class PanelList extends GridPanel {
/**
* Отступ между элементами списка
*/
public static final int LIST_PADDING = 5;
/**
* Отступ скроллера
*/
public static final int SCROLLER_PADDING = 20;
/**
* Ширина скроллера
*/
public static final int SCROLLER_WIDTH = 7;
/**
* поставщик списков строк
*/
private final Supplier<List<String>> lines;
/**
* кол-во отображаемых строк
*/
private final int renderLineCnt;
/**
* список кнопок для отображения элементов
*/
private final List<Button> buttons;
/**
* индекс первого отображаемого элемента
*/
private int start;
/**
* id выбранной кнопки
*/
int selectedButtonId;
/**
* Панель на сетке
*
* @param window окно
* @param drawBG флаг, нужно ли рисовать подложку
* @param backgroundColor цвет подложки
* @param padding отступы
* @param gridWidth кол-во ячеек сетки по ширине
* @param gridHeight кол-во ячеек сетки по высоте
* @param gridX координата в сетке x
* @param gridY координата в сетке y
* @param colspan кол-во колонок, занимаемых панелью
* @param rowspan кол-во строк, занимаемых панелью
* @param lines поставщик списков строк
* @param lineConsumer обработчик выбранного элемента списка
* @param renderLineCnt кол-во отображаемых линий
*/
public PanelList(
Window window, boolean drawBG, int backgroundColor, int padding, int gridWidth, int gridHeight,
int gridX, int gridY, int colspan, int rowspan, Supplier<List<String>> lines,
Consumer<String> lineConsumer, int renderLineCnt
) {
super(window, drawBG, backgroundColor, padding, gridWidth, gridHeight, gridX, gridY, colspan, rowspan);
this.lines = lines;
this.renderLineCnt = renderLineCnt;
buttons = new ArrayList<>();
// создаём кнопки для отображения элементов списка
for (int i = 0; i < renderLineCnt; i++) {
// создаём кнопку
Button elem = new Button(
window, false, BUTTON_COLOR, LIST_PADDING,
1, renderLineCnt, 0, i, 1, 1, "",
false, true);
// по клику кнопки в обработчик возвращаем её текст
elem.setOnClick(() -> lineConsumer.accept(elem.text));
buttons.add(elem);
}
}
/**
* Обработчик событий
* при перегрузке обязателен вызов реализации предка
*
* @param e событие
*/
@Override
public void accept(Event e) {
super.accept(e);
// событие движения мыши
if (e instanceof EventMouseMove ee) {
for (int i = 0; i < buttons.size(); i++) {
if (i == selectedButtonId) {
buttons.get(i).selected = true;
} else
buttons.get(i).checkOver(lastWindowCS.getRelativePos(new Vector2i(ee)));
}
// событие нажатия мыши
} else if (e instanceof EventMouseButton ee) {
if (!lastInside || !ee.isPressed())
return;
Vector2i relPos = lastWindowCS.getRelativePos(lastMove);
// пробуем кликнуть по всем кнопкам
for (int i = 0; i < buttons.size(); i++) {
buttons.get(i).click(relPos);
if (buttons.get(i).contains(relPos))
selectedButtonId = i;
}
// перерисовываем окно
window.requestFrame();
// обработчик ввода текста
} else if (e instanceof EventMouseScroll ee) {
if (lastMove != null && lastInside) {
// если строк для вывода меньше чем строк отображения
if (lines.get().size() <= renderLineCnt)
return;
start -= (int) ee.getDeltaY() / 100;
if (start >= lines.get().size() - renderLineCnt)
start = lines.get().size() - renderLineCnt - 1;
else if (start < 0)
start = 0;
}
window.requestFrame();
}
}
/**
* Метод под рисование в конкретной реализации
*
* @param canvas область рисования
* @param windowCS СК окна
*/
@Override
public void paintImpl(Canvas canvas, CoordinateSystem2i windowCS) {
// рисуем список кнопками
for (int i = start; i < Math.min(start + renderLineCnt, lines.get().size()); i++) {
buttons.get(i - start).text = lines.get().get(i);
buttons.get(i - start).paint(canvas, windowCS);
}
// если строк для вывода больше чем строк отображения
// рисуем скроллер
if (lines.get().size() >= renderLineCnt) {
// рисуем полосу прокрутки
canvas.save();
// создаём кисть
try (var paint = new Paint()) {
// сохраняем область рисования
canvas.save();
// задаём цвет подложки
paint.setColor(SCROLLER_BACKGROUND_COLOR);
// получаем высоту скроллера с учётом отступов
int realHeight = (windowCS.getSize().y - 2 * SCROLLER_PADDING);
// рисуем скруглённую подложку
canvas.drawRRect(RRect.makeXYWH(windowCS.getSize().x - SCROLLER_WIDTH, SCROLLER_PADDING, SCROLLER_WIDTH,
realHeight, 4), paint);
// задаём цвет ползунка
paint.setColor(SCROLLER_COLOR);
// рисуем его
canvas.drawRRect(RRect.makeXYWH(
windowCS.getSize().x - SCROLLER_WIDTH,
SCROLLER_PADDING + (float) realHeight * start / lines.get().size(), SCROLLER_WIDTH,
(float) realHeight * (renderLineCnt + 1) / lines.get().size(),
4), paint);
}
canvas.restore();
}
}
}
package dialogs;
import app.Application;
import controls.Button;
import controls.Input;
import controls.Label;
import controls.MultiLineLabel;
import io.github.humbleui.jwm.*;
import io.github.humbleui.skija.Canvas;
import misc.CoordinateSystem2i;
import misc.Vector2i;
import panels.Panel;
import panels.PanelList;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.function.Consumer;
import static app.Colors.*;
/**
* Панель управления
*/
public class PanelSelectFile extends Panel {
/**
* Отступы в панели управления
*/
private static final int CONTROL_PADDING = 5;
/**
* Строка для обозначения родительской папки
*/
private static final String PARENT_FOLDER_STR = "..";
/**
* Кнопка принять
*/
private final Button accept;
/**
* Кнопка отмена
*/
private final Button cancel;
/**
* заголовок
*/
private final MultiLineLabel infoLabel;
/**
* заголовок пути
*/
private final Input pathInput;
/**
* текст заголовка делаем статическим, чтобы можно было менять его
* из любого места, каждый экземпляр панели информации будет обращаться
* к этому полю при рисовании, но логика работы программы
* не предполагает создания нескольких экземпляров, так что всё ок
*/
private static String labelText;
/**
* Путь к файлу
*/
static String folderPath = "src/main/resources";
/**
* Панель списка
*/
private final PanelList listPanel;
/**
* путь к тексту
*/
private static String pathText = "";
/**
* Обработчик выбранного файла
*/
private static Consumer<String> processFile;
/**
* Заголовок поля ввода пути файла
*/
private static Label pathLabel;
/**
* Панель управления
*
* @param window окно
* @param drawBG флаг, нужно ли рисовать подложку
* @param color цвет подложки
* @param padding отступы
*/
public PanelSelectFile(Window window, boolean drawBG, int color, int padding) {
super(window, drawBG, color, padding);
// добавление вручную
infoLabel = new MultiLineLabel(window, false, backgroundColor, CONTROL_PADDING,
6, 5, 2, 0, 2, 1, "",
true, true);
listPanel = new PanelList(window, false, APP_BACKGROUND_COLOR, CONTROL_PADDING,
6, 5, 2, 1, 2, 2, PanelSelectFile::getFileList,
s -> {
processSelectedFile(s);
window.requestFrame();
}, 10);
pathInput = new Input(window, false, backgroundColor, CONTROL_PADDING,
6, 5, 2, 4, 2, 1, "",
true, MULTILINE_TEXT_COLOR);
pathLabel = new Label(window, false, backgroundColor, CONTROL_PADDING,
6, 5, 1, 4, 1, 1, "Путь к файлу:",
true, true);
accept = new Button(
window, false, BUTTON_COLOR, CONTROL_PADDING,
6, 5, 2, 3, 1, 1, "ОК",
true, true);
accept.setOnClick(PanelSelectFile::accept);
cancel = new Button(
window, false, BUTTON_COLOR, CONTROL_PADDING,
6, 5, 3, 3, 1, 1, "Отмена",
true, true);
cancel.setOnClick(() -> Application.currentMode = Application.Mode.WORK);
}
/**
* Выбрать текущий файл
*/
private static void accept(){
// переводим режим обратно в основной
Application.currentMode = Application.Mode.WORK;
// обрабатываем полученный файл
processFile.accept(pathText);
}
/**
* Вывести информацию
*
* @param caption текст заголовка
* @param processFileConsumer обработчик выбранного файла
*/
public static void show(String caption, Consumer<String> processFileConsumer) {
// задаём новый текст
labelText = caption;
// переключаем вывод приложения на режим информации
Application.currentMode = Application.Mode.FILE;
// сохраняем обработчик выбранного файла
processFile = processFileConsumer;
}
/**
* ОБработчик выбранного файла
*
* @param fileName название файла
*/
public static void processSelectedFile(String fileName) {
if (fileName.equals(PARENT_FOLDER_STR)) {
int lastIndex = folderPath.lastIndexOf("/");
if (lastIndex > 0)
folderPath = folderPath.substring(0, lastIndex);
} else if (new File(folderPath + "/" + fileName).isDirectory())
folderPath += "/" + fileName;
else
pathText = folderPath + "/" + fileName;
}
/**
* Получить список файлов
*
* @return список текстовых представлений файлов
*/
public static List<String> getFileList() {
ArrayList<String> lst = new ArrayList<>();
lst.add(PARENT_FOLDER_STR);
for (File file : Objects.requireNonNull(new File(folderPath).listFiles())) {
if (file.isDirectory() || file.getName().matches("[a-zA-Z-_0-9]*.json"))
lst.add(file.getName());
}
return lst;
}
/**
* Обработчик событий
*
* @param e событие
*/
@Override
public void accept(Event e) {
// вызываем обработчик предка
super.accept(e);
// событие движения мыши
if (e instanceof EventMouseMove ee) {
accept.checkOver(lastWindowCS.getRelativePos(new Vector2i(ee)));
cancel.checkOver(lastWindowCS.getRelativePos(new Vector2i(ee)));
listPanel.accept(e);
pathInput.accept(e);
// событие нажатия мыши
} else if (e instanceof EventMouseButton ee) {
if (!lastInside || !ee.isPressed())
return;
Vector2i relPos = lastWindowCS.getRelativePos(lastMove);
accept.click(relPos);
cancel.click(relPos);
listPanel.accept(e);
pathInput.accept(e);
// перерисовываем окно
window.requestFrame();
// обработчик ввода текста
} else if (e instanceof EventMouseScroll) {
listPanel.accept(e);
} else if (e instanceof EventKey ee) {
if (ee.isPressed()) {
// получаем код клавиши
Key key = ee.getKey();
// перебираем варианты
switch (key) {
// если esc
case ESCAPE -> Application.currentMode = Application.Mode.WORK;
// если enter
case ENTER -> Application.currentMode = Application.Mode.WORK;
}
}
pathInput.accept(e);
pathText = pathInput.getText();
} else if (e instanceof EventTextInput) {
pathInput.accept(e);
pathText = pathInput.getText();
}
}
/**
* Метод под рисование в конкретной реализации
*
* @param canvas область рисования
* @param windowCS СК окна
*/
@Override
public void paintImpl(Canvas canvas, CoordinateSystem2i windowCS) {
infoLabel.text = labelText;
pathInput.setText(pathText);
accept.paint(canvas, windowCS);
cancel.paint(canvas, windowCS);
infoLabel.paint(canvas, windowCS);
pathLabel.paint(canvas, windowCS);
pathInput.paint(canvas, windowCS);
listPanel.paint(canvas, windowCS);
}
}
Теперь нам остаётся только добавить панель в наше приложение (класс Application
) и связать её с режимом выбора файла.
Для начала добавим саму панель
/**
* Панель выбора файла
*/
private final PanelSelectFile panelSelectFile;
и инициализируем её в конструкторе
...
// Панель выбора файла
panelSelectFile = new PanelSelectFile(window, true, DIALOG_BACKGROUND_COLOR, PANEL_PADDING);
...
Теперь допишем вариант обработки событий в методе accept()
панелью выбора файлов
...
switch (currentMode) {
case INFO -> panelInfo.accept(e);
case FILE -> panelSelectFile.accept(e);
case WORK -> {
// передаём события на обработку панелям
panelControl.accept(e);
panelRendering.accept(e);
panelLog.accept(e);
}
}
...
и вариант рисования в методе paint()
...
// рисуем диалоги
switch (currentMode) {
case INFO -> panelInfo.paint(canvas, windowCS);
case FILE -> panelSelectFile.paint(canvas, windowCS);
}
...
Теперь перепишем методы сохранения и загрузки в PanelRendering
так, чтобы запускалась панель выбора файла, при
этом мы сразу указываем, что делать, если файл будет выбран.
/**
* Сохранить файл
*/
public static void save() {
PanelSelectFile.show("Выберите файл", path -> {
if (!path.isEmpty()) {
try {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.writeValue(new File(path), task);
PanelLog.success("Файл " + path + " успешно сохранён");
} catch (IOException e) {
PanelLog.error("не получилось записать файл \n" + e);
}
}
});
}
/**
* Загрузить файл
*/
public static void load() {
PanelSelectFile.show("Выберите файл", s -> {
if (!s.isEmpty()) {
PanelLog.info("load from " + s);
loadFromFile(s);
}
});
}
Теперь заработает выбор файла при загрузке и сохранении
Обратите внимание: снизу поле ввода, в него можно ввести любой путь.
Создайте новый коммит file open panel
и отправьте его на сервер.
Задание
Пример готового проекта лежит здесь.
Для выполнения первого задания Fork
от него делать запрещается.
В ходе работы вам будет нужно сделать несколько коммитов с промежуточными состояниями проекта, поэтому в прямом копировании нет смысла. Готовый проект даётся для того, чтобы было проще разобраться, как выполнить поставленное задание.
Если у вас не выполнено предыдущее задание или выполнено не полностью, то сделайте
Fork
готового проекта из предыдущей главы.
Вам необходимо создать свой репозиторий, в котором добавлены следующие коммиты (для каждого сначала указывается название, потом требования):
info pane
- напишите панель отображения результатов решения задачиfile open panel
- пропишите панель выбора файла
Ссылку на github
репозиторий необходимо отправить в поле ввода задания на сайте mdl
.