Add update for Ordering. New TableView. Build MSI
This commit is contained in:
96
pom.xml
96
pom.xml
@@ -13,6 +13,8 @@
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<junit.version>5.12.1</junit.version>
|
||||
<poi.version>5.2.5</poi.version>
|
||||
<!-- Версия для Windows Installer (формат: major.minor.build) -->
|
||||
<app.version>${project.version}</app.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
@@ -104,17 +106,95 @@
|
||||
<groupId>org.openjfx</groupId>
|
||||
<artifactId>javafx-maven-plugin</artifactId>
|
||||
<version>0.0.8</version>
|
||||
<configuration>
|
||||
<mainClass>com.dsol.pki_management.app/com.dsol.pki_management.app.PKIApplication</mainClass>
|
||||
<launcher>app</launcher>
|
||||
<jlinkZipName>app</jlinkZipName>
|
||||
<jlinkImageName>app</jlinkImageName>
|
||||
<noManPages>true</noManPages>
|
||||
<stripDebug>true</stripDebug>
|
||||
<noHeaderFiles>true</noHeaderFiles>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-dependency-plugin</artifactId>
|
||||
<version>3.6.1</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>default-cli</id>
|
||||
<id>copy-dependencies</id>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>copy-dependencies</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<mainClass>com.dsol.pki_management.app/com.dsol.pki_management.app.PKIApplication</mainClass>
|
||||
<launcher>app</launcher>
|
||||
<jlinkZipName>app</jlinkZipName>
|
||||
<jlinkImageName>app</jlinkImageName>
|
||||
<noManPages>true</noManPages>
|
||||
<stripDebug>true</stripDebug>
|
||||
<noHeaderFiles>true</noHeaderFiles>
|
||||
<outputDirectory>${project.build.directory}/jpackage-input/lib</outputDirectory>
|
||||
<includeScope>runtime</includeScope>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-antrun-plugin</artifactId>
|
||||
<version>3.1.0</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>prepare-jpackage-input</id>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>run</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<target>
|
||||
<copy file="${project.build.directory}/${project.build.finalName}.jar"
|
||||
tofile="${project.build.directory}/jpackage-input/${project.build.finalName}.jar"/>
|
||||
</target>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.codehaus.mojo</groupId>
|
||||
<artifactId>exec-maven-plugin</artifactId>
|
||||
<version>3.1.1</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>jpackage</id>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>exec</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<executable>jpackage</executable>
|
||||
<workingDirectory>${project.build.directory}</workingDirectory>
|
||||
<arguments>
|
||||
<argument>--input</argument>
|
||||
<argument>${project.build.directory}/jpackage-input</argument>
|
||||
<argument>--name</argument>
|
||||
<argument>PKI Management</argument>
|
||||
<argument>--main-jar</argument>
|
||||
<argument>${project.build.finalName}.jar</argument>
|
||||
<argument>--main-class</argument>
|
||||
<argument>com.dsol.pki_management.app.Launcher</argument>
|
||||
<argument>--type</argument>
|
||||
<argument>msi</argument>
|
||||
<argument>--dest</argument>
|
||||
<argument>${project.build.directory}</argument>
|
||||
<argument>--app-version</argument>
|
||||
<argument>1.0.0</argument>
|
||||
<argument>--vendor</argument>
|
||||
<argument>DSOL</argument>
|
||||
<argument>--description</argument>
|
||||
<argument>PKI Management System</argument>
|
||||
<argument>--win-dir-chooser</argument>
|
||||
<argument>--win-menu</argument>
|
||||
<argument>--win-shortcut</argument>
|
||||
<argument>--win-menu-group</argument>
|
||||
<argument>PKI Management</argument>
|
||||
<argument>--java-options</argument>
|
||||
<argument>-Dfile.encoding=UTF-8</argument>
|
||||
</arguments>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
|
||||
@@ -1,13 +1,30 @@
|
||||
package com.dsol.pki_management.app;
|
||||
|
||||
import com.dsol.pki_management.config.LoggerConfig;
|
||||
import javafx.application.Application;
|
||||
|
||||
import java.util.logging.Logger;
|
||||
|
||||
/**
|
||||
* Точка входа в приложение
|
||||
*/
|
||||
public class Launcher {
|
||||
private static final Logger logger = Logger.getLogger(Launcher.class.getName());
|
||||
|
||||
public static void main(String[] args) {
|
||||
Application.launch(PKIApplication.class, args);
|
||||
// Инициализируем логирование как можно раньше
|
||||
LoggerConfig.initialize();
|
||||
|
||||
logger.info("Запуск приложения PKI Management");
|
||||
logger.info("Аргументы командной строки: " + java.util.Arrays.toString(args));
|
||||
|
||||
try {
|
||||
Application.launch(PKIApplication.class, args);
|
||||
} catch (Exception e) {
|
||||
logger.severe("КРИТИЧЕСКАЯ ОШИБКА при запуске приложения: " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
System.exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,19 +7,56 @@ import javafx.scene.Scene;
|
||||
import javafx.stage.Stage;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
/**
|
||||
* Главный класс приложения PKI Management
|
||||
*/
|
||||
public class PKIApplication extends Application {
|
||||
private static final Logger logger = Logger.getLogger(PKIApplication.class.getName());
|
||||
|
||||
@Override
|
||||
public void start(Stage stage) throws IOException {
|
||||
FXMLLoader fxmlLoader = new FXMLLoader(PKIApplication.class.getResource("/com/dsol/pki_management/views/main.fxml"));
|
||||
fxmlLoader.setController(new MainController());
|
||||
Scene scene = new Scene(fxmlLoader.load());
|
||||
stage.setTitle("PKI Management");
|
||||
stage.setScene(scene);
|
||||
stage.show();
|
||||
public void init() throws Exception {
|
||||
super.init();
|
||||
logger.info("Инициализация JavaFX Application");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start(Stage stage) {
|
||||
logger.info("Запуск метода start() JavaFX Application");
|
||||
|
||||
try {
|
||||
logger.info("Загрузка FXML файла: /com/dsol/pki_management/views/main.fxml");
|
||||
FXMLLoader fxmlLoader = new FXMLLoader(PKIApplication.class.getResource("/com/dsol/pki_management/views/main.fxml"));
|
||||
fxmlLoader.setController(new MainController());
|
||||
|
||||
logger.info("Создание сцены");
|
||||
Scene scene = new Scene(fxmlLoader.load());
|
||||
|
||||
logger.info("Настройка окна");
|
||||
stage.setTitle("PKI Management");
|
||||
stage.setScene(scene);
|
||||
|
||||
logger.info("Отображение окна");
|
||||
stage.show();
|
||||
|
||||
logger.info("Приложение успешно запущено");
|
||||
|
||||
} catch (IOException e) {
|
||||
logger.severe("ОШИБКА при загрузке FXML: " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
throw new RuntimeException("Не удалось загрузить главное окно", e);
|
||||
} catch (Exception e) {
|
||||
logger.severe("ОШИБКА при запуске приложения: " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
throw new RuntimeException("Критическая ошибка при запуске", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() throws Exception {
|
||||
super.stop();
|
||||
logger.info("Остановка приложения");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -40,6 +40,7 @@ public class TableViewEnhancer {
|
||||
|
||||
setupCellSelection(tableView);
|
||||
setupCopyHandler(tableView);
|
||||
setupDragSelectionPerCell(tableView);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -124,81 +125,6 @@ public class TableViewEnhancer {
|
||||
}
|
||||
});
|
||||
|
||||
// Обработчик перетаскивания мыши для выделения диапазона (drag selection)
|
||||
tableView.setOnMouseDragged(event -> {
|
||||
TablePosition<?, ?> startPos = dragStartMap.get(tableView);
|
||||
if (startPos == null) return;
|
||||
|
||||
// Находим ячейку под курсором мыши
|
||||
javafx.geometry.Point2D localPoint = tableView.screenToLocal(event.getScreenX(), event.getScreenY());
|
||||
int draggedRow = -1;
|
||||
int draggedCol = -1;
|
||||
|
||||
// Вычисляем строку на основе Y координаты
|
||||
// Учитываем заголовок таблицы (если есть)
|
||||
double headerHeight = tableView.lookup(".column-header-background") != null
|
||||
? tableView.lookup(".column-header-background").getBoundsInLocal().getHeight()
|
||||
: 0;
|
||||
double y = localPoint.getY() - headerHeight;
|
||||
|
||||
// Вычисляем индекс строки
|
||||
double rowHeight = tableView.getFixedCellSize() > 0
|
||||
? tableView.getFixedCellSize()
|
||||
: 25.0; // примерная высота строки по умолчанию
|
||||
|
||||
if (y >= 0) {
|
||||
draggedRow = (int) (y / rowHeight);
|
||||
}
|
||||
|
||||
// Вычисляем колонку на основе X координаты
|
||||
double x = localPoint.getX();
|
||||
double currentX = 0;
|
||||
for (int i = 0; i < tableView.getColumns().size(); i++) {
|
||||
TableColumn<T, ?> col = tableView.getColumns().get(i);
|
||||
double colWidth = col.getWidth();
|
||||
if (x >= currentX && x < currentX + colWidth) {
|
||||
draggedCol = i;
|
||||
break;
|
||||
}
|
||||
currentX += colWidth;
|
||||
}
|
||||
|
||||
// Ограничиваем индексы валидными значениями
|
||||
if (draggedRow < 0) draggedRow = 0;
|
||||
if (draggedRow >= tableView.getItems().size()) draggedRow = tableView.getItems().size() - 1;
|
||||
if (draggedCol < 0) draggedCol = 0;
|
||||
if (draggedCol >= tableView.getColumns().size()) draggedCol = tableView.getColumns().size() - 1;
|
||||
|
||||
// Выделяем диапазон
|
||||
if (draggedRow >= 0 && draggedRow < tableView.getItems().size()
|
||||
&& draggedCol >= 0 && draggedCol < tableView.getColumns().size()) {
|
||||
int startRow = Math.min(startPos.getRow(), draggedRow);
|
||||
int endRow = Math.max(startPos.getRow(), draggedRow);
|
||||
int startCol = Math.min(startPos.getColumn(), draggedCol);
|
||||
int endCol = Math.max(startPos.getColumn(), draggedCol);
|
||||
|
||||
selectionModel.clearSelection();
|
||||
trackedCells.clear();
|
||||
|
||||
for (int r = startRow; r <= endRow; r++) {
|
||||
for (int c = startCol; c <= endCol; c++) {
|
||||
if (r >= 0 && r < tableView.getItems().size()
|
||||
&& c >= 0 && c < tableView.getColumns().size()) {
|
||||
TableColumn<T, ?> col = tableView.getColumns().get(c);
|
||||
selectionModel.select(r, col);
|
||||
TablePosition<T, ?> pos = new TablePosition<>(tableView, r, col);
|
||||
trackedCells.add(pos);
|
||||
}
|
||||
}
|
||||
}
|
||||
event.consume();
|
||||
}
|
||||
});
|
||||
|
||||
// Обработчик окончания перетаскивания
|
||||
tableView.setOnMouseReleased(event -> {
|
||||
dragStartMap.remove(tableView);
|
||||
});
|
||||
|
||||
// Сброс выделения при клике вне таблицы
|
||||
// Устанавливаем обработчик на сцену, если она доступна
|
||||
@@ -279,6 +205,122 @@ public class TableViewEnhancer {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Настраивает drag-селекцию на уровне каждой ячейки
|
||||
* Это работает надежно даже когда внутри ячейки есть HBox, Text и т.д.
|
||||
* Метод можно вызывать повторно - он обновит существующие cellFactory, сохранив обработчики drag
|
||||
*/
|
||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||
private static <T> void setupDragSelectionPerCell(TableView<T> tableView) {
|
||||
TableSelectionModel<T> selectionModel = tableView.getSelectionModel();
|
||||
ObservableList<TablePosition<?, ?>> trackedCells = selectedCellsMap.get(tableView);
|
||||
|
||||
for (TableColumn<T, ?> col : tableView.getColumns()) {
|
||||
// Сохраняем существующий cellFactory (используем raw type для совместимости)
|
||||
javafx.util.Callback existingFactory = col.getCellFactory();
|
||||
|
||||
// Создаем обертку, которая добавляет обработчики drag к любой ячейке
|
||||
// Используем raw type Callback и явное приведение для избежания проблем с wildcard capture
|
||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||
javafx.util.Callback wrapperFactory = (javafx.util.Callback) (javafx.util.Callback<TableColumn, TableCell>) (TableColumn column) -> {
|
||||
// Создаем ячейку через существующий factory или стандартную
|
||||
TableCell<T, ?> cell;
|
||||
if (existingFactory != null) {
|
||||
cell = (TableCell<T, ?>) existingFactory.call(column);
|
||||
} else {
|
||||
// Стандартная ячейка, если factory нет
|
||||
cell = new TableCell<T, Object>() {
|
||||
@Override
|
||||
protected void updateItem(Object item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
setText(empty || item == null ? "" : item.toString());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Добавляем обработчики drag к ячейке
|
||||
addDragHandlersToCell(tableView, cell, (TableColumn<T, ?>) column, selectionModel, trackedCells);
|
||||
|
||||
return cell;
|
||||
};
|
||||
|
||||
col.setCellFactory(wrapperFactory);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Добавляет обработчики drag к ячейке
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private static <T> void addDragHandlersToCell(
|
||||
TableView<T> tableView,
|
||||
TableCell<T, ?> cell,
|
||||
TableColumn<T, ?> column,
|
||||
TableSelectionModel<T> selectionModel,
|
||||
ObservableList<TablePosition<?, ?>> trackedCells) {
|
||||
|
||||
// Начало перетаскивания
|
||||
cell.setOnDragDetected(e -> {
|
||||
TableRow<T> row = cell.getTableRow();
|
||||
if (row != null && !cell.isEmpty() && row.getIndex() >= 0) {
|
||||
tableView.startFullDrag();
|
||||
TablePosition<T, ?> startPos = new TablePosition<>(tableView, row.getIndex(), column);
|
||||
dragStartMap.put(tableView, startPos);
|
||||
|
||||
// Если не Shift+Click и не Ctrl+Click, очищаем выделение
|
||||
if (!e.isShiftDown() && !e.isControlDown()) {
|
||||
selectionModel.clearSelection();
|
||||
trackedCells.clear();
|
||||
selectionModel.select(row.getIndex(), column);
|
||||
trackedCells.add(startPos);
|
||||
}
|
||||
e.consume();
|
||||
}
|
||||
});
|
||||
|
||||
// При входе курсора в другую ячейку во время drag
|
||||
cell.setOnMouseDragEntered(e -> {
|
||||
TablePosition<?, ?> startPos = dragStartMap.get(tableView);
|
||||
if (startPos == null) return;
|
||||
|
||||
TableRow<T> row = cell.getTableRow();
|
||||
if (row == null || cell.isEmpty() || row.getIndex() < 0) return;
|
||||
|
||||
int startRow = Math.min(startPos.getRow(), row.getIndex());
|
||||
int endRow = Math.max(startPos.getRow(), row.getIndex());
|
||||
int startCol = Math.min(startPos.getColumn(), findColumnIndex(tableView, column));
|
||||
int endCol = Math.max(startPos.getColumn(), findColumnIndex(tableView, column));
|
||||
|
||||
selectionModel.clearSelection();
|
||||
trackedCells.clear();
|
||||
|
||||
for (int r = startRow; r <= endRow; r++) {
|
||||
for (int c = startCol; c <= endCol; c++) {
|
||||
if (r >= 0 && r < tableView.getItems().size()
|
||||
&& c >= 0 && c < tableView.getColumns().size()) {
|
||||
TableColumn<T, ?> tc = tableView.getColumns().get(c);
|
||||
selectionModel.select(r, tc);
|
||||
trackedCells.add(new TablePosition<>(tableView, r, tc));
|
||||
}
|
||||
}
|
||||
}
|
||||
e.consume();
|
||||
});
|
||||
|
||||
// Завершение drag
|
||||
cell.setOnMouseDragReleased(e -> {
|
||||
dragStartMap.remove(tableView);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Публичный метод для обновления drag-обработчиков после изменения cellFactory
|
||||
* Можно вызывать после установки кастомных cellFactory
|
||||
*/
|
||||
public static <T> void refreshDragHandlers(TableView<T> tableView) {
|
||||
setupDragSelectionPerCell(tableView);
|
||||
}
|
||||
|
||||
/**
|
||||
* Настраивает обработчик копирования (Ctrl+C)
|
||||
*/
|
||||
@@ -363,6 +405,134 @@ public class TableViewEnhancer {
|
||||
clipboard.setContent(content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Находит TableCell, поднимаясь по иерархии узлов
|
||||
* Надежный метод, который работает даже когда внутри ячейки есть HBox, Text и т.д.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private static <T> TableCell<T, ?> findTableCell(javafx.scene.Node node) {
|
||||
javafx.scene.Node current = node;
|
||||
int maxDepth = 30; // Чуть глубже для надежности
|
||||
|
||||
// Проходим вверх по иерархии узлов
|
||||
while (current != null && maxDepth-- > 0) {
|
||||
if (current instanceof TableCell<?, ?> tc) {
|
||||
return (TableCell<T, ?>) tc;
|
||||
}
|
||||
// Останавливаемся, если дошли до TableView
|
||||
if (current instanceof TableView) {
|
||||
break;
|
||||
}
|
||||
current = current.getParent();
|
||||
}
|
||||
|
||||
// Fallback — пытаемся через lookup по стилю (некоторые обёртки теряют parent-ссылку)
|
||||
if (node != null && node.getScene() != null) {
|
||||
javafx.scene.Node root = node.getScene().getRoot();
|
||||
if (root != null) {
|
||||
// Ищем все TableCell на сцене
|
||||
java.util.Set<javafx.scene.Node> cells = root.lookupAll(".table-cell");
|
||||
javafx.geometry.Bounds nodeScreenBounds = node.localToScreen(node.getBoundsInLocal());
|
||||
|
||||
for (javafx.scene.Node n : cells) {
|
||||
if (n instanceof TableCell<?, ?> tc) {
|
||||
javafx.geometry.Bounds cellScreenBounds = tc.localToScreen(tc.getBoundsInLocal());
|
||||
// Проверяем, попадает ли узел в границы ячейки
|
||||
if (cellScreenBounds.contains(nodeScreenBounds.getMinX(), nodeScreenBounds.getMinY())) {
|
||||
return (TableCell<T, ?>) tc;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Находит индекс колонки в таблице (включая вложенные колонки)
|
||||
*/
|
||||
private static <T> int findColumnIndex(TableView<T> tableView, TableColumn<T, ?> targetColumn) {
|
||||
// Сначала ищем в основных колонках
|
||||
int index = tableView.getColumns().indexOf(targetColumn);
|
||||
if (index >= 0) {
|
||||
return index;
|
||||
}
|
||||
|
||||
// Если не нашли, ищем рекурсивно во вложенных колонках
|
||||
for (int i = 0; i < tableView.getColumns().size(); i++) {
|
||||
TableColumn<T, ?> col = tableView.getColumns().get(i);
|
||||
if (col.getColumns().contains(targetColumn)) {
|
||||
// Если колонка вложена, возвращаем индекс родительской колонки
|
||||
// В этом случае точный индекс вложенной колонки сложнее определить
|
||||
// Для простоты возвращаем индекс родительской
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Находит индекс колонки по X координате
|
||||
*/
|
||||
private static <T> int findColumnByX(TableView<T> tableView, double x) {
|
||||
double currentX = 0;
|
||||
for (int i = 0; i < tableView.getColumns().size(); i++) {
|
||||
TableColumn<T, ?> col = tableView.getColumns().get(i);
|
||||
double colWidth = col.getWidth();
|
||||
if (x >= currentX && x < currentX + colWidth) {
|
||||
return i;
|
||||
}
|
||||
currentX += colWidth;
|
||||
}
|
||||
// Если не нашли, возвращаем последнюю колонку или первую
|
||||
return tableView.getColumns().isEmpty() ? -1 : tableView.getColumns().size() - 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Находит индекс строки по Y координате с учетом прокрутки
|
||||
*/
|
||||
private static <T> int findRowByY(TableView<T> tableView, double y, double headerHeight, double tableHeight) {
|
||||
if (y < 0 || tableView.getItems().isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Получаем высоту строки
|
||||
double rowHeight = tableView.getFixedCellSize() > 0
|
||||
? tableView.getFixedCellSize()
|
||||
: 25.0; // примерная высота по умолчанию
|
||||
|
||||
ScrollBar vScrollBar = (ScrollBar) tableView.lookup(".scroll-bar:vertical");
|
||||
int totalRows = tableView.getItems().size();
|
||||
|
||||
if (vScrollBar != null && vScrollBar.isVisible() && totalRows > 0) {
|
||||
int visibleRows = Math.max(1, (int) (tableHeight / rowHeight));
|
||||
|
||||
if (totalRows > visibleRows) {
|
||||
double scrollValue = vScrollBar.getValue();
|
||||
double maxScroll = vScrollBar.getMax();
|
||||
|
||||
if (maxScroll > 0) {
|
||||
// Более точный расчет первой видимой строки
|
||||
// scrollValue от 0 до 1, где 1 = полностью прокручено вниз
|
||||
int firstVisibleRow = (int) (scrollValue * (totalRows - visibleRows));
|
||||
int visibleRowIndex = (int) (y / rowHeight);
|
||||
int calculatedRow = firstVisibleRow + visibleRowIndex;
|
||||
return Math.max(0, Math.min(calculatedRow, totalRows - 1));
|
||||
} else {
|
||||
return Math.min((int)(y / rowHeight), totalRows - 1);
|
||||
}
|
||||
} else {
|
||||
// Все строки видны, прокрутки нет
|
||||
return Math.min((int)(y / rowHeight), totalRows - 1);
|
||||
}
|
||||
} else {
|
||||
// Нет прокрутки или прокрутка не видна
|
||||
return Math.min((int)(y / rowHeight), totalRows - 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Получает значение ячейки по позиции
|
||||
*/
|
||||
@@ -387,6 +557,15 @@ public class TableViewEnhancer {
|
||||
|
||||
// Получаем значение через cell value factory
|
||||
Object cellValue = column.getCellData(rowItem);
|
||||
|
||||
// Если значение является ObservableValue (например, StringProperty), получаем его значение
|
||||
if (cellValue instanceof javafx.beans.value.ObservableValue) {
|
||||
@SuppressWarnings("unchecked")
|
||||
javafx.beans.value.ObservableValue<?> observableValue = (javafx.beans.value.ObservableValue<?>) cellValue;
|
||||
Object value = observableValue.getValue();
|
||||
return value != null ? value : "";
|
||||
}
|
||||
|
||||
return cellValue != null ? cellValue : "";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
package com.dsol.pki_management.config;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.logging.FileHandler;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
import java.util.logging.SimpleFormatter;
|
||||
|
||||
/**
|
||||
* Конфигурация логирования приложения
|
||||
* Настраивает запись логов в файл в директории конфигурации
|
||||
*/
|
||||
public class LoggerConfig {
|
||||
private static final String LOG_FILE_NAME = "application.log";
|
||||
private static final int MAX_LOG_SIZE = 5 * 1024 * 1024; // 5 MB
|
||||
private static final int LOG_FILE_COUNT = 2; // Только текущий и один архивный
|
||||
private static boolean initialized = false;
|
||||
|
||||
/**
|
||||
* Инициализирует систему логирования
|
||||
* Должен быть вызван как можно раньше при запуске приложения
|
||||
*/
|
||||
public static void initialize() {
|
||||
if (initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Получаем директорию конфигурации
|
||||
ConfigManager configManager = ConfigManager.getInstance();
|
||||
Path configDir = configManager.getConfigDir();
|
||||
|
||||
// Создаем директорию, если её нет
|
||||
if (!Files.exists(configDir)) {
|
||||
Files.createDirectories(configDir);
|
||||
}
|
||||
|
||||
// Путь к файлу логов (не скрытый)
|
||||
Path logFile = configDir.resolve(LOG_FILE_NAME);
|
||||
|
||||
// Настраиваем FileHandler с ротацией логов
|
||||
FileHandler fileHandler = new FileHandler(
|
||||
logFile.toString(),
|
||||
MAX_LOG_SIZE,
|
||||
LOG_FILE_COUNT,
|
||||
true // append
|
||||
);
|
||||
|
||||
// Устанавливаем простой форматтер
|
||||
fileHandler.setFormatter(new SimpleFormatter());
|
||||
fileHandler.setLevel(Level.INFO); // INFO и выше (INFO, WARNING, SEVERE)
|
||||
|
||||
// Получаем root logger и настраиваем его
|
||||
Logger rootLogger = Logger.getLogger("");
|
||||
rootLogger.addHandler(fileHandler);
|
||||
rootLogger.setLevel(Level.INFO); // INFO и выше (INFO, WARNING, SEVERE)
|
||||
|
||||
// Логируем начало работы
|
||||
Logger logger = Logger.getLogger(LoggerConfig.class.getName());
|
||||
logger.info("=========================================");
|
||||
logger.info("Логирование инициализировано");
|
||||
logger.info("Файл логов: " + logFile);
|
||||
logger.info("Версия Java: " + System.getProperty("java.version"));
|
||||
logger.info("ОС: " + System.getProperty("os.name") + " " + System.getProperty("os.version"));
|
||||
logger.info("Пользователь: " + System.getProperty("user.name"));
|
||||
logger.info("=========================================");
|
||||
|
||||
initialized = true;
|
||||
|
||||
} catch (IOException e) {
|
||||
// Если не удалось создать файл логов, выводим в консоль
|
||||
System.err.println("ОШИБКА: Не удалось инициализировать логирование в файл: " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
} catch (Exception e) {
|
||||
System.err.println("ОШИБКА: Неожиданная ошибка при инициализации логирования: " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Получает путь к файлу логов
|
||||
*/
|
||||
public static Path getLogFile() {
|
||||
ConfigManager configManager = ConfigManager.getInstance();
|
||||
return configManager.getConfigDir().resolve(LOG_FILE_NAME);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,8 @@ import com.dsol.pki_management.modules.purchases.PurchasesDataStore;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.fxml.FXMLLoader;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Pane;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
@@ -43,6 +45,10 @@ public class MainController {
|
||||
private VBox menuContent;
|
||||
@FXML
|
||||
private StackPane contentArea;
|
||||
@FXML
|
||||
private HBox excelParsingStatusBar;
|
||||
@FXML
|
||||
private Label excelParsingStatusLabel;
|
||||
|
||||
// Статический список всех SubMenuItem для доступа из других контроллеров
|
||||
private static final List<SubMenuItem> allSubMenuItems = new ArrayList<>();
|
||||
@@ -131,6 +137,11 @@ public class MainController {
|
||||
@Override
|
||||
protected Void call() {
|
||||
try {
|
||||
// Показываем строку состояния
|
||||
Platform.runLater(() -> {
|
||||
showParsingStatus("Сканирование файла...");
|
||||
});
|
||||
|
||||
PurchasesDataStore.getInstance().setStatus("Сканирование файла...");
|
||||
PurchasesExcelValidator validator = new PurchasesExcelValidator();
|
||||
var results = validator.validate(path);
|
||||
@@ -141,17 +152,42 @@ public class MainController {
|
||||
System.out.println("[WARN] Лист '" + r.sheetName + "' — отсутствуют заголовки: " + String.join(", ", r.missingHeaders));
|
||||
}
|
||||
});
|
||||
|
||||
// Обновляем статус перед парсингом
|
||||
Platform.runLater(() -> {
|
||||
showParsingStatus("Парсинг данных...");
|
||||
});
|
||||
|
||||
// Парсим данные и кладём в стор
|
||||
PurchasesExcelParser parser = new PurchasesExcelParser();
|
||||
var rows = parser.parse(path);
|
||||
Platform.runLater(() -> {
|
||||
PurchasesDataStore.getInstance().setRows(rows);
|
||||
PurchasesDataStore.getInstance().setStatus("Загружено записей: " + rows.size());
|
||||
hideParsingStatus();
|
||||
});
|
||||
} catch (Exception ex) {
|
||||
System.err.println("[ERROR] Ошибка валидации/парсинга Excel при старте: " + ex.getMessage());
|
||||
ex.printStackTrace();
|
||||
Platform.runLater(() -> PurchasesDataStore.getInstance().setStatus("Ошибка: " + ex.getMessage()));
|
||||
Platform.runLater(() -> {
|
||||
PurchasesDataStore.getInstance().setStatus("Ошибка: " + ex.getMessage());
|
||||
showParsingStatus("Ошибка: " + ex.getMessage());
|
||||
// Скрываем строку состояния через 3 секунды после ошибки
|
||||
javafx.concurrent.Service<Void> delayService = new javafx.concurrent.Service<>() {
|
||||
@Override
|
||||
protected javafx.concurrent.Task<Void> createTask() {
|
||||
return new javafx.concurrent.Task<>() {
|
||||
@Override
|
||||
protected Void call() throws Exception {
|
||||
Thread.sleep(3000);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
delayService.setOnSucceeded(e -> hideParsingStatus());
|
||||
delayService.start();
|
||||
});
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -270,5 +306,26 @@ public class MainController {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Показывает строку состояния парсинга Excel
|
||||
*/
|
||||
private void showParsingStatus(String message) {
|
||||
if (excelParsingStatusBar != null && excelParsingStatusLabel != null) {
|
||||
excelParsingStatusLabel.setText(message);
|
||||
excelParsingStatusBar.setVisible(true);
|
||||
excelParsingStatusBar.setManaged(true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Скрывает строку состояния парсинга Excel
|
||||
*/
|
||||
private void hideParsingStatus() {
|
||||
if (excelParsingStatusBar != null) {
|
||||
excelParsingStatusBar.setVisible(false);
|
||||
excelParsingStatusBar.setManaged(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -118,7 +118,11 @@ public class SettingsController {
|
||||
private void choosePurchasesExcelFile() {
|
||||
FileChooser chooser = new FileChooser();
|
||||
chooser.setTitle("Выберите Excel файл закупок");
|
||||
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Excel (*.xlsx)", "*.xlsx"));
|
||||
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(
|
||||
"Excel файлы (*.xlsx, *.xlsm, *.xlsb, *.xltx, *.xltm, *.xls)",
|
||||
"*.xlsx", "*.xlsm", "*.xlsb", "*.xltx", "*.xltm", "*.xls"
|
||||
));
|
||||
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Все файлы", "*.*"));
|
||||
File initialDir = new File(System.getProperty("user.home"));
|
||||
if (initialDir.exists()) chooser.setInitialDirectory(initialDir);
|
||||
File file = chooser.showOpenDialog(null);
|
||||
|
||||
@@ -36,14 +36,17 @@ public class MesApiClient {
|
||||
* Получает список строк для регулярных выражений из API MES
|
||||
*
|
||||
* @return Список строк для подсветки, или пустой список в случае ошибки
|
||||
* @throws IOException если сервер недоступен или произошла ошибка сети
|
||||
* @throws RuntimeException если URL не настроены (не критическая ошибка)
|
||||
*/
|
||||
public List<String> fetchMatchErrorStrings() {
|
||||
public List<String> fetchMatchErrorStrings() throws IOException {
|
||||
String mesUrl = configManager.getProperty(AppConfig.KEY_MES_URL, "");
|
||||
String matchErrorUrl = configManager.getProperty(AppConfig.KEY_MES_MATCH_ERROR_URL, "");
|
||||
|
||||
if (mesUrl == null || mesUrl.trim().isEmpty() ||
|
||||
matchErrorUrl == null || matchErrorUrl.trim().isEmpty()) {
|
||||
logger.warning("URL MES или URL Match Error не настроены");
|
||||
logger.info("URL MES или URL Match Error не настроены - подсветка отключена");
|
||||
// Не бросаем исключение, если URL не настроены - это нормальная ситуация
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
@@ -66,13 +69,25 @@ public class MesApiClient {
|
||||
if (response.statusCode() == 200) {
|
||||
return parseJsonResponse(response.body());
|
||||
} else {
|
||||
logger.warning("API MES вернул статус " + response.statusCode() + " для URL: " + fullUrl);
|
||||
return new ArrayList<>();
|
||||
String errorMsg = "API MES вернул статус " + response.statusCode() + " для URL: " + fullUrl;
|
||||
logger.warning(errorMsg);
|
||||
throw new IOException(errorMsg);
|
||||
}
|
||||
} catch (IOException | InterruptedException e) {
|
||||
logger.severe("Ошибка при запросе к API MES: " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
return new ArrayList<>();
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new IOException("Запрос к API MES был прерван: " + e.getMessage(), e);
|
||||
} catch (java.net.http.HttpTimeoutException e) {
|
||||
String errorMsg = "Таймаут при подключении к серверу MES: " + fullUrl;
|
||||
logger.severe(errorMsg);
|
||||
throw new IOException(errorMsg, e);
|
||||
} catch (java.net.ConnectException e) {
|
||||
String errorMsg = "Не удалось подключиться к серверу MES: " + fullUrl;
|
||||
logger.severe(errorMsg);
|
||||
throw new IOException(errorMsg, e);
|
||||
} catch (IOException e) {
|
||||
String errorMsg = "Ошибка при запросе к API MES (" + fullUrl + "): " + e.getMessage();
|
||||
logger.severe(errorMsg);
|
||||
throw new IOException(errorMsg, e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -130,6 +130,8 @@ public class PurchasesController {
|
||||
cell.setHighlightStrings(highlightStrings);
|
||||
return cell;
|
||||
});
|
||||
// Обновляем drag-обработчики после изменения cellFactory
|
||||
TableViewEnhancer.refreshDragHandlers(purchasesTable);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -138,7 +140,7 @@ public class PurchasesController {
|
||||
private void loadHighlightData() {
|
||||
Task<List<String>> task = new Task<>() {
|
||||
@Override
|
||||
protected List<String> call() {
|
||||
protected List<String> call() throws Exception {
|
||||
return mesApiClient.fetchMatchErrorStrings();
|
||||
}
|
||||
};
|
||||
@@ -158,18 +160,44 @@ public class PurchasesController {
|
||||
});
|
||||
|
||||
task.setOnFailed(e -> {
|
||||
// В случае ошибки просто логируем, подсветка не будет работать
|
||||
System.err.println("Ошибка при загрузке данных для подсветки: " + task.getException().getMessage());
|
||||
if (task.getException() != null) {
|
||||
task.getException().printStackTrace();
|
||||
// В случае ошибки логируем и показываем уведомление пользователю
|
||||
Throwable exception = task.getException();
|
||||
String errorMessage = exception != null ? exception.getMessage() : "Неизвестная ошибка";
|
||||
|
||||
System.err.println("Ошибка при загрузке данных для подсветки: " + errorMessage);
|
||||
if (exception != null) {
|
||||
exception.printStackTrace();
|
||||
}
|
||||
|
||||
// Показываем уведомление пользователю только если это ошибка подключения
|
||||
// (не показываем, если URL просто не настроены)
|
||||
if (exception instanceof java.io.IOException) {
|
||||
Platform.runLater(() -> {
|
||||
showMesServerErrorNotification(errorMessage);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Запускаем задачу в фоновом потоке
|
||||
Thread thread = new Thread(task);
|
||||
thread.setDaemon(true);
|
||||
thread.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Показывает уведомление об ошибке подключения к серверу MES
|
||||
*/
|
||||
private void showMesServerErrorNotification(String errorMessage) {
|
||||
javafx.scene.control.Alert alert = new javafx.scene.control.Alert(javafx.scene.control.Alert.AlertType.WARNING);
|
||||
alert.setTitle("MES сервер недоступен");
|
||||
alert.setHeaderText("Не удалось подключиться к серверу MES");
|
||||
alert.setContentText("Сервер MES системы недоступен или не отвечает.\n\n" +
|
||||
"Детали ошибки: " + errorMessage + "\n\n" +
|
||||
"Подсветка ошибочных наименований будет недоступна до восстановления связи с сервером.\n" +
|
||||
"Проверьте настройки подключения в разделе \"Настройки\" → \"MES\".");
|
||||
alert.showAndWait();
|
||||
}
|
||||
|
||||
private void initFilters() {
|
||||
// Настраиваем обработчики для всех фильтров
|
||||
filterType.textProperty().addListener((obs, oldVal, newVal) -> applyFilters());
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package com.dsol.pki_management.modules.purchases;
|
||||
|
||||
import org.apache.poi.ss.usermodel.*;
|
||||
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
@@ -37,7 +36,7 @@ public class PurchasesExcelParser {
|
||||
return List.of();
|
||||
}
|
||||
List<PurchaseRow> result = new ArrayList<>();
|
||||
try (InputStream in = Files.newInputStream(xlsxPath); Workbook wb = new XSSFWorkbook(in)) {
|
||||
try (InputStream in = Files.newInputStream(xlsxPath); Workbook wb = WorkbookFactory.create(in)) {
|
||||
for (int i = 0; i < wb.getNumberOfSheets(); i++) {
|
||||
Sheet sheet = wb.getSheetAt(i);
|
||||
Map<String, Integer> headerToIndex = readHeaderIndexes(sheet);
|
||||
|
||||
@@ -4,7 +4,7 @@ import org.apache.poi.ss.usermodel.Cell;
|
||||
import org.apache.poi.ss.usermodel.Row;
|
||||
import org.apache.poi.ss.usermodel.Sheet;
|
||||
import org.apache.poi.ss.usermodel.Workbook;
|
||||
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
|
||||
import org.apache.poi.ss.usermodel.WorkbookFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
@@ -13,7 +13,7 @@ import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Валидатор Excel (XLSX) по шаблону закупок.
|
||||
* Валидатор Excel файлов (XLSX, XLSM, XLSB, XLTX, XLTM, XLS) по шаблону закупок.
|
||||
* Проверяет, что на каждой странице (Sheet) в строке 4 присутствуют необходимые заголовки.
|
||||
*/
|
||||
public class PurchasesExcelValidator {
|
||||
@@ -50,7 +50,7 @@ public class PurchasesExcelValidator {
|
||||
if (xlsxPath == null || !Files.exists(xlsxPath)) {
|
||||
throw new IOException("Файл не найден: " + xlsxPath);
|
||||
}
|
||||
try (InputStream in = Files.newInputStream(xlsxPath); Workbook wb = new XSSFWorkbook(in)) {
|
||||
try (InputStream in = Files.newInputStream(xlsxPath); Workbook wb = WorkbookFactory.create(in)) {
|
||||
List<SheetValidationResult> results = new ArrayList<>();
|
||||
for (int i = 0; i < wb.getNumberOfSheets(); i++) {
|
||||
Sheet sheet = wb.getSheetAt(i);
|
||||
|
||||
@@ -40,6 +40,19 @@
|
||||
</children>
|
||||
</StackPane>
|
||||
</center>
|
||||
<bottom>
|
||||
<HBox fx:id="excelParsingStatusBar"
|
||||
alignment="CENTER_LEFT"
|
||||
style="-fx-background-color: #0d6efd; -fx-padding: 8 16 8 16;"
|
||||
visible="false"
|
||||
managed="false">
|
||||
<children>
|
||||
<Label fx:id="excelParsingStatusLabel"
|
||||
style="-fx-text-fill: white; -fx-font-size: 14px;"
|
||||
text="Парсинг Excel файла..."/>
|
||||
</children>
|
||||
</HBox>
|
||||
</bottom>
|
||||
</BorderPane>
|
||||
|
||||
<Pane fx:id="menuOverlay"
|
||||
|
||||
Reference in New Issue
Block a user