feat(search): add collection search menu and filter settings

- add search menu with filter configuration and result actions
- add support for AND and OR filter relations
- add range filters for group number, average grade, and record book number
- update current collection menu to delegate search operations
This commit is contained in:
2026-09-01 13:27:21 +04:00
parent db66ebbc07
commit 84f9e2aa8d
5 changed files with 673 additions and 10 deletions
@@ -4,6 +4,7 @@ import com.googlecode.lanterna.gui2.ActionListBox;
import com.googlecode.lanterna.gui2.WindowBasedTextGUI;
import ru.project.model.AppState;
import ru.project.ui.base.BaseModalWindow;
import ru.project.ui.collection.search.SearchCollectionMenu;
import ru.project.ui.collection.sorting.SortCollectionMenu;
import ru.project.ui.common.CollectionDisplayWindow;
import ru.project.ui.common.CollectionFileSaveWindow;
@@ -13,8 +14,8 @@ public class CurrentCollectionMenu extends BaseModalWindow {
private final AppState state;
private final SortCollectionMenu sortCollectionMenu;
private final SearchCollectionMenu searchCollectionMenu;
// private final SearchCollectionMenu searchCollectionMenu;
// private final CountOccurrencesMenu countOccurrencesMenu;
public CurrentCollectionMenu(WindowBasedTextGUI gui, AppState state) {
@@ -25,18 +26,18 @@ public class CurrentCollectionMenu extends BaseModalWindow {
this.sortCollectionMenu = new SortCollectionMenu(gui, state);
// this.searchCollectionMenu = new SearchCollectionMenu(gui, state);
this.searchCollectionMenu = new SearchCollectionMenu(gui, state);
// this.countCollectionMenu = new CountOccurrencesMenu(gui, state);
ActionListBox menu = new ActionListBox();
menu.addItem("Показать коллекцию...", this::showCurrentCollection);
menu.addItem("Сохранить коллекцию в файл...", this::saveCurrentCollection);
menu.addItem("Меню сортировки коллекции", sortCollectionMenu::showModal);
menu.addItem("Меню поиска элемента в коллекции", this::searchCollection);
menu.addItem("Меню поиска элемента в коллекции", searchCollectionMenu::showModal);
menu.addItem("Меню подсчёта количества вхождений элемента", this::countOccurrences);
@@ -57,12 +58,6 @@ public class CurrentCollectionMenu extends BaseModalWindow {
window.showModal();
}
private void searchCollection() {
// SearchCollectionMenu searchCollectionMenu = new SearchCollectionMenu(gui, state);
//
// searchCollectionMenu.showModal();
}
private void countOccurrences() {
// CountOccurrencesMenu countOccurrencesMenu = new CountOccurrencesMenu(gui, state);
//
@@ -0,0 +1,6 @@
package ru.project.ui.collection.search;
public enum FilterRelation {
AND,
OR
}
@@ -0,0 +1,191 @@
package ru.project.ui.collection.search;
import com.googlecode.lanterna.gui2.ActionListBox;
import com.googlecode.lanterna.gui2.WindowBasedTextGUI;
import ru.project.collection.MyLinkedList;
import ru.project.collection.MyList;
import ru.project.model.AppState;
import ru.project.search.AverageGradeRangeCriteria;
import ru.project.search.CompositeStudentSearchCriteria;
import ru.project.search.GroupNumberRangeCriteria;
import ru.project.search.RecordBookNumberRangeCriteria;
import ru.project.search.SearchOperation;
import ru.project.search.StudentSearchCriteria;
import ru.project.search.StudentSearchService;
import ru.project.student.Student;
import ru.project.ui.base.BaseModalWindow;
import ru.project.ui.common.CollectionDisplayWindow;
import ru.project.ui.common.CollectionFileSaveWindow;
import ru.project.ui.common.MessageWindow;
public class SearchCollectionMenu extends BaseModalWindow {
private final AppState state;
private final SearchSettingsWindow searchSettingsWindow;
private SearchSettings searchSettings;
private MyList<Student> searchResultCollection;
public SearchCollectionMenu(WindowBasedTextGUI gui, AppState state) {
super("Меню поиска элемента в коллекции", gui);
this.state = state;
this.searchSettings = new SearchSettings();
this.searchResultCollection = new MyLinkedList<>();
this.searchSettingsWindow =
new SearchSettingsWindow(gui, settings -> this.searchSettings = settings);
ActionListBox menu = new ActionListBox();
menu.addItem("Выполнить поиск", this::updateSearchResult);
menu.addItem("Показать результат поиска", this::showSearchResult);
menu.addItem("Сохранить результат поиска в файл", this::saveSearchResult);
menu.addItem("Настроить процесс поиска...", searchSettingsWindow::showModal);
menu.addItem("Выйти", this::close);
setComponent(menu);
}
private void updateSearchResult() {
MyList<Student> collection = state.getMainCollection();
if (collection == null || collection.isEmpty()) {
MessageWindow.showModal(gui, "Коллекция для поиска пуста.");
return;
}
if (!hasEnabledFilters()) {
MessageWindow.showModal(gui, "Не выбран ни один фильтр.");
return;
}
if (!hasValidFilterRanges()) {
return;
}
StudentSearchCriteria criteria = createSearchCriteria();
StudentSearchService searchService = new StudentSearchService(criteria);
searchResultCollection = searchService.find(collection);
MessageWindow.showModal(gui, "Результат поиска обновлён.");
}
private StudentSearchCriteria createSearchCriteria() {
SearchOperation operation =
searchSettings.getRelation() == FilterRelation.AND
? SearchOperation.INTERSECTION
: SearchOperation.UNION;
CompositeStudentSearchCriteria compositeCriteria =
new CompositeStudentSearchCriteria(operation);
if (searchSettings.isGroupNumberFilterEnabled()) {
compositeCriteria.add(
new GroupNumberRangeCriteria(
searchSettings.getMinGroupNumber(), searchSettings.getMaxGroupNumber()));
}
if (searchSettings.isAverageGradeFilterEnabled()) {
compositeCriteria.add(
new AverageGradeRangeCriteria(
searchSettings.getMinAverageGrade(), searchSettings.getMaxAverageGrade()));
}
if (searchSettings.isRecordBookNumberFilterEnabled()) {
compositeCriteria.add(
new RecordBookNumberRangeCriteria(
searchSettings.getMinRecordBookNumber(), searchSettings.getMaxRecordBookNumber()));
}
return compositeCriteria;
}
private boolean hasEnabledFilters() {
return searchSettings.isGroupNumberFilterEnabled()
|| searchSettings.isAverageGradeFilterEnabled()
|| searchSettings.isRecordBookNumberFilterEnabled();
}
private boolean hasValidFilterRanges() {
StringBuilder errors = new StringBuilder();
if (!hasValidGroupNumberRange()) {
errors.append("Для фильтра по номеру группы задайте минимальное и максимальное значения.\n");
}
if (!hasValidAverageGradeRange()) {
errors.append("Для фильтра по среднему баллу задайте минимальное и максимальное значения.\n");
}
if (!hasValidRecordBookNumberRange()) {
errors.append(
"Для фильтра по номеру зачётной книжки задайте минимальное и максимальное значения.\n");
}
if (errors.length() > 0) {
MessageWindow.showModal(gui, errors.toString());
return false;
}
return true;
}
private boolean hasValidGroupNumberRange() {
if (!searchSettings.isGroupNumberFilterEnabled()) {
return true;
}
return searchSettings.getMinGroupNumber() != null && searchSettings.getMaxGroupNumber() != null;
}
private boolean hasValidAverageGradeRange() {
if (!searchSettings.isAverageGradeFilterEnabled()) {
return true;
}
return searchSettings.getMinAverageGrade() != null
&& searchSettings.getMaxAverageGrade() != null;
}
private boolean hasValidRecordBookNumberRange() {
if (!searchSettings.isRecordBookNumberFilterEnabled()) {
return true;
}
return searchSettings.getMinRecordBookNumber() != null
&& searchSettings.getMaxRecordBookNumber() != null;
}
private void showSearchResult() {
if (searchResultCollection == null || searchResultCollection.isEmpty()) {
MessageWindow.showModal(gui, "Результат поиска пуст.");
return;
}
CollectionDisplayWindow window = new CollectionDisplayWindow(gui, searchResultCollection);
window.showModal();
}
private void saveSearchResult() {
if (searchResultCollection == null || searchResultCollection.isEmpty()) {
MessageWindow.showModal(gui, "Результат поиска пуст.");
return;
}
CollectionFileSaveWindow window = new CollectionFileSaveWindow(gui, searchResultCollection);
window.showModal();
}
}
@@ -0,0 +1,116 @@
package ru.project.ui.collection.search;
public class SearchSettings {
private FilterRelation relation;
private boolean groupNumberFilterEnabled;
private boolean averageGradeFilterEnabled;
private boolean recordBookNumberFilterEnabled;
private String minGroupNumber;
private String maxGroupNumber;
private Double minAverageGrade;
private Double maxAverageGrade;
private Integer minRecordBookNumber;
private Integer maxRecordBookNumber;
public SearchSettings() {
this.relation = FilterRelation.AND;
this.groupNumberFilterEnabled = false;
this.averageGradeFilterEnabled = false;
this.recordBookNumberFilterEnabled = false;
this.minGroupNumber = null;
this.maxGroupNumber = null;
this.minAverageGrade = null;
this.maxAverageGrade = null;
this.minRecordBookNumber = null;
this.maxRecordBookNumber = null;
}
public FilterRelation getRelation() {
return relation;
}
public void setRelation(FilterRelation relation) {
this.relation = relation;
}
public boolean isGroupNumberFilterEnabled() {
return groupNumberFilterEnabled;
}
public void setGroupNumberFilterEnabled(boolean enabled) {
this.groupNumberFilterEnabled = enabled;
}
public boolean isAverageGradeFilterEnabled() {
return averageGradeFilterEnabled;
}
public void setAverageGradeFilterEnabled(boolean enabled) {
this.averageGradeFilterEnabled = enabled;
}
public boolean isRecordBookNumberFilterEnabled() {
return recordBookNumberFilterEnabled;
}
public void setRecordBookNumberFilterEnabled(boolean enabled) {
this.recordBookNumberFilterEnabled = enabled;
}
public String getMinGroupNumber() {
return minGroupNumber;
}
public void setMinGroupNumber(String minGroupNumber) {
this.minGroupNumber = minGroupNumber;
}
public String getMaxGroupNumber() {
return maxGroupNumber;
}
public void setMaxGroupNumber(String maxGroupNumber) {
this.maxGroupNumber = maxGroupNumber;
}
public Double getMinAverageGrade() {
return minAverageGrade;
}
public void setMinAverageGrade(Double minAverageGrade) {
this.minAverageGrade = minAverageGrade;
}
public Double getMaxAverageGrade() {
return maxAverageGrade;
}
public void setMaxAverageGrade(Double maxAverageGrade) {
this.maxAverageGrade = maxAverageGrade;
}
public Integer getMinRecordBookNumber() {
return minRecordBookNumber;
}
public void setMinRecordBookNumber(Integer minRecordBookNumber) {
this.minRecordBookNumber = minRecordBookNumber;
}
public Integer getMaxRecordBookNumber() {
return maxRecordBookNumber;
}
public void setMaxRecordBookNumber(Integer maxRecordBookNumber) {
this.maxRecordBookNumber = maxRecordBookNumber;
}
}
@@ -0,0 +1,355 @@
package ru.project.ui.collection.search;
import com.googlecode.lanterna.gui2.Button;
import com.googlecode.lanterna.gui2.CheckBox;
import com.googlecode.lanterna.gui2.Label;
import com.googlecode.lanterna.gui2.LinearLayout;
import com.googlecode.lanterna.gui2.Panel;
import com.googlecode.lanterna.gui2.RadioBoxList;
import com.googlecode.lanterna.gui2.WindowBasedTextGUI;
import java.util.function.Consumer;
import java.util.function.Predicate;
import ru.project.student.Student;
import ru.project.ui.base.BaseModalWindow;
import ru.project.ui.common.InputValueWindow;
import ru.project.ui.common.MessageWindow;
public class SearchSettingsWindow extends BaseModalWindow {
private final SearchSettings settings;
private final Consumer<SearchSettings> onApply;
private final CheckBox groupNumberFilterCheckBox;
private final CheckBox averageGradeFilterCheckBox;
private final CheckBox recordBookNumberFilterCheckBox;
private final Label minGroupNumberLabel;
private final Label maxGroupNumberLabel;
private final Label minAverageGradeLabel;
private final Label maxAverageGradeLabel;
private final Label minRecordBookNumberLabel;
private final Label maxRecordBookNumberLabel;
private final RadioBoxList<String> relationList;
public SearchSettingsWindow(WindowBasedTextGUI gui, Consumer<SearchSettings> onApply) {
super("Настройка процесса поиска", gui);
this.settings = new SearchSettings();
this.onApply = onApply;
Panel panel = new Panel();
panel.setLayoutManager(new LinearLayout());
panel.addComponent(new Label("Отношение фильтров:"));
relationList = new RadioBoxList<>();
relationList.addItem("И");
relationList.addItem("ИЛИ");
relationList.setCheckedItemIndex(0);
panel.addComponent(relationList);
groupNumberFilterCheckBox = new CheckBox("Фильтр по номеру группы");
averageGradeFilterCheckBox = new CheckBox("Фильтр по среднему баллу");
recordBookNumberFilterCheckBox = new CheckBox("Фильтр по номеру зачётной книжки");
panel.addComponent(groupNumberFilterCheckBox);
minGroupNumberLabel =
new Label("Минимум номера группы: " + getValueOrDefault(settings.getMinGroupNumber()));
maxGroupNumberLabel =
new Label("Максимум номера группы: " + getValueOrDefault(settings.getMaxGroupNumber()));
panel.addComponent(minGroupNumberLabel);
panel.addComponent(maxGroupNumberLabel);
panel.addComponent(new Button("Задать минимум номера группы...", this::setMinGroupNumber));
panel.addComponent(new Button("Задать максимум номера группы...", this::setMaxGroupNumber));
panel.addComponent(averageGradeFilterCheckBox);
minAverageGradeLabel =
new Label("Минимум среднего балла: " + getValueOrDefault(settings.getMinAverageGrade()));
maxAverageGradeLabel =
new Label("Максимум среднего балла: " + getValueOrDefault(settings.getMaxAverageGrade()));
panel.addComponent(minAverageGradeLabel);
panel.addComponent(maxAverageGradeLabel);
panel.addComponent(new Button("Задать минимум среднего балла...", this::setMinAverageGrade));
panel.addComponent(new Button("Задать максимум среднего балла...", this::setMaxAverageGrade));
panel.addComponent(recordBookNumberFilterCheckBox);
minRecordBookNumberLabel =
new Label(
"Минимум номера зачётной книжки: "
+ getValueOrDefault(settings.getMinRecordBookNumber()));
maxRecordBookNumberLabel =
new Label(
"Максимум номера зачётной книжки: "
+ getValueOrDefault(settings.getMaxRecordBookNumber()));
panel.addComponent(minRecordBookNumberLabel);
panel.addComponent(maxRecordBookNumberLabel);
panel.addComponent(
new Button("Задать минимум номера зачётной книжки...", this::setMinRecordBookNumber));
panel.addComponent(
new Button("Задать максимум номера зачётной книжки...", this::setMaxRecordBookNumber));
panel.addComponent(new Button("Применить", this::apply));
panel.addComponent(new Button("Выйти", this::close));
setComponent(panel);
}
private void setMinGroupNumber() {
showGroupNumberInput(
"Минимальный номер группы",
settings.getMinGroupNumber(),
value -> {
if (!value.matches(Student.getGroupNumberPattern())) {
MessageWindow.showModal(gui, "Номер группы должен соответствовать формату A12.");
return false;
}
if (settings.getMaxGroupNumber() != null
&& value.compareTo(settings.getMaxGroupNumber()) > 0) {
MessageWindow.showModal(
gui, "Минимальный номер группы не может быть больше максимального.");
return false;
}
settings.setMinGroupNumber(value);
minGroupNumberLabel.setText("Минимум номера группы: " + value);
return true;
});
}
private void setMaxGroupNumber() {
showGroupNumberInput(
"Максимальный номер группы",
settings.getMaxGroupNumber(),
value -> {
if (!value.matches(Student.getGroupNumberPattern())) {
MessageWindow.showModal(gui, "Номер группы должен соответствовать формату A12.");
return false;
}
if (settings.getMinGroupNumber() != null
&& value.compareTo(settings.getMinGroupNumber()) < 0) {
MessageWindow.showModal(
gui, "Максимальный номер группы не может быть меньше минимального.");
return false;
}
settings.setMaxGroupNumber(value);
maxGroupNumberLabel.setText("Максимум номера группы: " + value);
return true;
});
}
private void showGroupNumberInput(String title, String initialValue, Predicate<String> onSave) {
InputValueWindow window =
new InputValueWindow(
gui, title, "Введите номер группы (например, A12):", initialValue, onSave);
window.showModal();
}
private void setMinAverageGrade() {
showAverageGradeInput(
"Минимальный средний балл",
settings.getMinAverageGrade(),
value -> {
if (settings.getMaxAverageGrade() != null && value > settings.getMaxAverageGrade()) {
MessageWindow.showModal(
gui, "Минимальный средний балл не может быть больше максимального.");
return false;
}
settings.setMinAverageGrade(value);
minAverageGradeLabel.setText("Минимум среднего балла: " + value);
return true;
});
}
private void setMaxAverageGrade() {
showAverageGradeInput(
"Максимальный средний балл",
settings.getMaxAverageGrade(),
value -> {
if (settings.getMinAverageGrade() != null && value < settings.getMinAverageGrade()) {
MessageWindow.showModal(
gui, "Максимальный средний балл не может быть меньше минимального.");
return false;
}
settings.setMaxAverageGrade(value);
maxAverageGradeLabel.setText("Максимум среднего балла: " + value);
return true;
});
}
private void showAverageGradeInput(String title, Double initialValue, Predicate<Double> onSave) {
InputValueWindow window =
new InputValueWindow(
gui,
title,
"Введите значение от 0.0 до 5.0:",
initialValue == null ? null : String.valueOf(initialValue),
value -> {
try {
double parsedValue = Double.parseDouble(value);
if (parsedValue < Student.getMinAverageGrade()
|| parsedValue > Student.getMaxAverageGrade()) {
MessageWindow.showModal(gui, "Средний балл должен быть от 0.0 до 5.0.");
return false;
}
return onSave.test(parsedValue);
} catch (NumberFormatException e) {
MessageWindow.showModal(gui, "Введите корректное число.");
return false;
}
});
window.showModal();
}
private void setMinRecordBookNumber() {
showRecordBookNumberInput(
"Минимальный номер зачётной книжки",
settings.getMinRecordBookNumber(),
value -> {
if (settings.getMaxRecordBookNumber() != null
&& value > settings.getMaxRecordBookNumber()) {
MessageWindow.showModal(
gui, "Минимальный номер зачётной книжки не может быть больше максимального.");
return false;
}
settings.setMinRecordBookNumber(value);
minRecordBookNumberLabel.setText("Минимум номера зачётной книжки: " + value);
return true;
});
}
private void setMaxRecordBookNumber() {
showRecordBookNumberInput(
"Максимальный номер зачётной книжки",
settings.getMaxRecordBookNumber(),
value -> {
if (settings.getMinRecordBookNumber() != null
&& value < settings.getMinRecordBookNumber()) {
MessageWindow.showModal(
gui, "Максимальный номер зачётной книжки не может быть меньше минимального.");
return false;
}
settings.setMaxRecordBookNumber(value);
maxRecordBookNumberLabel.setText("Максимум номера зачётной книжки: " + value);
return true;
});
}
private void showRecordBookNumberInput(
String title, Integer initialValue, Predicate<Integer> onSave) {
InputValueWindow window =
new InputValueWindow(
gui,
title,
"Введите номер зачётной книжки:",
initialValue == null ? null : String.valueOf(initialValue),
value -> {
try {
int parsedValue = Integer.parseInt(value);
if (parsedValue < Student.getMinRecordBookNumber()) {
MessageWindow.showModal(gui, "Номер зачётной книжки должен быть положительным.");
return false;
}
return onSave.test(parsedValue);
} catch (NumberFormatException e) {
MessageWindow.showModal(gui, "Введите целое число.");
return false;
}
});
window.showModal();
}
private void apply() {
settings.setGroupNumberFilterEnabled(groupNumberFilterCheckBox.isChecked());
settings.setAverageGradeFilterEnabled(averageGradeFilterCheckBox.isChecked());
settings.setRecordBookNumberFilterEnabled(recordBookNumberFilterCheckBox.isChecked());
settings.setRelation(
relationList.getCheckedItemIndex() == 0 ? FilterRelation.AND : FilterRelation.OR);
onApply.accept(settings);
MessageWindow.showModal(gui, "Настройки процесса поиска успешно применены.");
}
private String getValueOrDefault(Object value) {
return value == null ? "не задан" : String.valueOf(value);
}
}