Skip to main content

14. Режимы

Довольно часто программа подразумевает несколько режимов (вариантов) работы окна. Нам в задаче пригодятся панель информации и окно выбора файла. В этой главе мы пропишем их.

info

Пример готового проекта лежит здесь. Для выполнения текущего задания Fork от него делать запрещается.

В ходе работы вам будет нужно сделать несколько коммитов с промежуточными состояниями проекта, поэтому в прямом копировании нет смысла. Готовый проект даётся для того, чтобы было проще разобраться, как выполнить поставленное задание.

Если у вас не выполнено предыдущее задание или выполнено не полностью, то сделайте Fork готового проекта из предыдущей главы.

Панель информации

Варианты отображения окна будем называть режимами. Напишем для начала режим отображения информации.

Когда задача будет решена, мы будем переходить в режим вывода информации. По Esc будет выполняться выход.

Для начала введём перечисление режимов в классе Application

    /**
* Режимы работы приложения
*/
public enum Mode {
/**
* Основной режим работы
*/
WORK,
/**
* Окно информации
*/
INFO,
/**
* работа с файлами
*/
FILE
}

Также добавим поле режима

    /**
* Текущий режим(по умолчанию рабочий)
*/
public static Mode currentMode = Mode.WORK;

Мы сразу добавили режим и для работы с файлами. Теперь создадим пакет dialogs, а в нём класс PanelInfo.

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

Создайте новый коммит 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

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();
}
}
}

Теперь нам остаётся только добавить панель в наше приложение (класс 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);
}
});
}

Теперь заработает выбор файла при загрузке и сохранении

info

Обратите внимание: снизу поле ввода, в него можно ввести любой путь.

Создайте новый коммит file open panel и отправьте его на сервер.

Задание

Пример готового проекта лежит здесь. Для выполнения первого задания Fork от него делать запрещается.

В ходе работы вам будет нужно сделать несколько коммитов с промежуточными состояниями проекта, поэтому в прямом копировании нет смысла. Готовый проект даётся для того, чтобы было проще разобраться, как выполнить поставленное задание.

Если у вас не выполнено предыдущее задание или выполнено не полностью, то сделайте Fork готового проекта из предыдущей главы.

Вам необходимо создать свой репозиторий, в котором добавлены следующие коммиты (для каждого сначала указывается название, потом требования):

  1. info pane - напишите панель отображения результатов решения задачи
  2. file open panel - пропишите панель выбора файла

d

Ссылку на github репозиторий необходимо отправить в поле ввода задания на сайте mdl.

Ссылка на контест