Compare commits

..
2 Commits
Author SHA1 Message Date
Timi 7d9967bbb5 add ctrl+s and ctrl+w 2026-04-24 11:06:46 +08:00
Timi 3941040362 add drag sort 2026-04-24 10:40:08 +08:00
3 changed files with 160 additions and 20 deletions
@@ -5,8 +5,12 @@ import com.imyeyu.fx.ui.TimiFXUI;
import com.imyeyu.hosts.ui.ctrl.Main; import com.imyeyu.hosts.ui.ctrl.Main;
import com.imyeyu.inject.annotation.Inject; import com.imyeyu.inject.annotation.Inject;
import com.imyeyu.inject.annotation.TimiInjectApplication; import com.imyeyu.inject.annotation.TimiInjectApplication;
import javafx.application.Platform;
import javafx.application.Application; import javafx.application.Application;
import javafx.scene.Scene; import javafx.scene.Scene;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination;
import javafx.scene.input.KeyCombination;
import javafx.stage.Stage; import javafx.stage.Stage;
/** /**
@@ -27,6 +31,11 @@ public class FXApplication extends Application implements TimiFXUI {
Scene scene = new Scene(main, 900, 560); Scene scene = new Scene(main, 900, 560);
scene.getStylesheets().addAll(CSS_STYLE, CSS_FONT, "style.css"); scene.getStylesheets().addAll(CSS_STYLE, CSS_FONT, "style.css");
scene.getAccelerators().put(new KeyCodeCombination(KeyCode.S, KeyCombination.CONTROL_DOWN), main::saveHosts);
scene.getAccelerators().put(new KeyCodeCombination(KeyCode.W, KeyCombination.CONTROL_DOWN), () -> {
stage.close();
Platform.exit();
});
stage.setTitle("TimiHosts"); stage.setTitle("TimiHosts");
stage.setScene(scene); stage.setScene(scene);
stage.show(); stage.show();
@@ -10,16 +10,22 @@ import com.imyeyu.lang.multi.ResourcesMultilingual;
import javafx.beans.binding.DoubleBinding; import javafx.beans.binding.DoubleBinding;
import javafx.beans.property.BooleanProperty; import javafx.beans.property.BooleanProperty;
import javafx.beans.property.StringProperty; import javafx.beans.property.StringProperty;
import javafx.collections.ObservableList;
import javafx.geometry.Insets; import javafx.geometry.Insets;
import javafx.geometry.Pos; import javafx.geometry.Pos;
import javafx.scene.control.Button; import javafx.scene.control.Button;
import javafx.scene.control.CheckBox; import javafx.scene.control.CheckBox;
import javafx.scene.control.TableCell; import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn; import javafx.scene.control.TableColumn;
import javafx.scene.control.TableRow;
import javafx.scene.control.TableView; import javafx.scene.control.TableView;
import javafx.scene.control.TextField; import javafx.scene.control.TextField;
import javafx.scene.control.cell.PropertyValueFactory; import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.input.ClipboardContent;
import javafx.scene.input.Dragboard;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.ScrollEvent; import javafx.scene.input.ScrollEvent;
import javafx.scene.input.TransferMode;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
/** /**
@@ -31,7 +37,10 @@ import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor @RequiredArgsConstructor
public class HostTable extends TableView<Host> implements TimiFXUI { public class HostTable extends TableView<Host> implements TimiFXUI {
private static final String DRAG_INDEX_KEY = "host-table-row-index";
private final ResourcesMultilingual multilingual; private final ResourcesMultilingual multilingual;
private int dragSourceIndex = -1;
@PostConstruct @PostConstruct
private void postConstruct() { private void postConstruct() {
@@ -78,6 +87,10 @@ public class HostTable extends TableView<Host> implements TimiFXUI {
} }
}); });
getColumns().add(activated); getColumns().add(activated);
TableColumn<Host, Void> drag = createDragColumn();
getColumns().add(drag);
// IP // IP
TableColumn<Host, String> ip = new TableColumn<>("IP"); TableColumn<Host, String> ip = new TableColumn<>("IP");
ip.setCellValueFactory(new PropertyValueFactory<>("ip")); ip.setCellValueFactory(new PropertyValueFactory<>("ip"));
@@ -245,6 +258,7 @@ public class HostTable extends TableView<Host> implements TimiFXUI {
DoubleBinding db = widthProperty().subtract(8); DoubleBinding db = widthProperty().subtract(8);
db = db.subtract(activated.widthProperty()); db = db.subtract(activated.widthProperty());
db = db.subtract(drag.widthProperty());
db = db.subtract(ip.widthProperty()); db = db.subtract(ip.widthProperty());
db = db.subtract(description.widthProperty()); db = db.subtract(description.widthProperty());
db = db.subtract(delete.widthProperty()); db = db.subtract(delete.widthProperty());
@@ -256,5 +270,117 @@ public class HostTable extends TableView<Host> implements TimiFXUI {
// 滚动时编辑区主动失去焦点 // 滚动时编辑区主动失去焦点
addEventFilter(ScrollEvent.ANY, e -> requestFocus()); addEventFilter(ScrollEvent.ANY, e -> requestFocus());
setRowFactory(table -> createDraggableRow());
}
/**
* 创建拖动排序列。
*
* @return 拖动排序列
*/
private TableColumn<Host, Void> createDragColumn() {
TableColumn<Host, Void> drag = new TableColumn<>("");
drag.setPrefWidth(40);
drag.setSortable(false);
drag.setResizable(false);
drag.setReorderable(false);
drag.setCellFactory(cell -> new TableCell<>() {
private final Button dragButton = new Button("::");
{
dragButton.getStyleClass().add(CSS.BORDER_N);
dragButton.setFocusTraversable(false);
dragButton.setOnDragDetected(this::startRowDrag);
setAlignment(Pos.CENTER);
}
@Override
protected void updateItem(Void item, boolean empty) {
super.updateItem(item, empty);
if (empty || getTableRow() == null || getTableRow().getItem() == null) {
setGraphic(null);
} else {
setGraphic(dragButton);
}
}
/**
* 从拖动按钮发起行拖拽。
*
* @param event 鼠标事件
*/
private void startRowDrag(MouseEvent event) {
TableRow<Host> row = getTableRow();
if (row == null || row.isEmpty()) {
return;
}
dragSourceIndex = row.getIndex();
Dragboard dragboard = row.startDragAndDrop(TransferMode.MOVE);
ClipboardContent content = new ClipboardContent();
content.putString(DRAG_INDEX_KEY.formatted());
dragboard.setContent(content);
event.consume();
}
});
return drag;
}
/**
* 创建支持拖动排序的表格行。
*
* @return 表格行
*/
private TableRow<Host> createDraggableRow() {
TableRow<Host> row = new TableRow<>();
row.setOnDragOver(event -> {
if (dragSourceIndex < 0 || row.getIndex() == dragSourceIndex) {
return;
}
if (event.getDragboard().hasString()) {
event.acceptTransferModes(TransferMode.MOVE);
event.consume();
}
});
row.setOnDragDropped(event -> {
if (dragSourceIndex < 0) {
return;
}
int targetIndex = row.isEmpty() ? getItems().size() : row.getIndex();
moveItem(dragSourceIndex, targetIndex);
event.setDropCompleted(true);
event.consume();
});
row.setOnDragDone(event -> dragSourceIndex = -1);
return row;
}
/**
* 调整条目顺序。
*
* @param fromIndex 原索引
* @param toIndex 目标索引
*/
private void moveItem(int fromIndex, int toIndex) {
ObservableList<Host> items = getItems();
if (fromIndex < 0 || items.size() <= fromIndex) {
return;
}
if (toIndex < 0) {
toIndex = 0;
}
if (items.size() < toIndex) {
toIndex = items.size();
}
if (fromIndex == toIndex) {
return;
}
Host movedItem = items.remove(fromIndex);
if (fromIndex < toIndex) {
toIndex--;
}
items.add(toIndex, movedItem);
getSelectionModel().clearAndSelect(toIndex);
scrollTo(toIndex);
} }
} }
@@ -37,18 +37,7 @@ public class Main extends MainView {
private void init() { private void init() {
add.setOnAction(e -> table.getItems().add(new Host())); add.setOnAction(e -> table.getItems().add(new Host()));
// 绑定保存按钮事件 // 绑定保存按钮事件
save.setOnAction(e -> { save.setOnAction(e -> saveHosts());
try {
int count = hostService.save();
labelTips.info("已保存 %d 条映射".formatted(count));
} catch (AccessDeniedException ex) {
log.error("写入 hosts 失败: 权限不足", ex);
labelTips.error("保存失败: 权限不足,请以管理员身份运行");
} catch (IOException ex) {
log.error("写入 hosts 失败", ex);
labelTips.error("保存失败: %s".formatted(ex.getMessage()));
}
});
// 加载初始数据 // 加载初始数据
try { try {
int count = hostService.refresh(); int count = hostService.refresh();
@@ -65,6 +54,22 @@ public class Main extends MainView {
} }
} }
/**
* 保存当前 hosts 映射。
*/
public void saveHosts() {
try {
int count = hostService.save();
labelTips.info("已保存 %d 条映射".formatted(count));
} catch (AccessDeniedException ex) {
log.error("写入 hosts 失败: 权限不足", ex);
labelTips.error("保存失败: 权限不足,请以管理员身份运行");
} catch (IOException ex) {
log.error("写入 hosts 失败", ex);
labelTips.error("保存失败: %s".formatted(ex.getMessage()));
}
}
@PostConstruct @PostConstruct
private void label() { private void label() {
Callback updateStatus = () -> { Callback updateStatus = () -> {