diff --git a/.gitignore b/.gitignore index 2be1234..99faba9 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,7 @@ !/script/jlink/*.bat /script/jpackage/* !/script/jpackage/build.* +!/script/jpackage/LabelPlusFXDict.lnk lpfx.iml lpfx.ipr diff --git a/CHANGELOG b/CHANGELOG.md similarity index 91% rename from CHANGELOG rename to CHANGELOG.md index b8270cf..d965c7f 100644 --- a/CHANGELOG +++ b/CHANGELOG.md @@ -12,10 +12,36 @@ TODO: - 备份文件上限(Next) - 保存specify(Next) +# 2.3.4 + +Add + + - 编辑框添加快捷输入短语 + - 树状图菜单添加复制文本和粘贴文本,并支持cv快捷键操作 + - 选中右键树状图中的Label支持移动序号 + - 图片区添加QW切换图片的快捷键 + - 添加百度翻译key设置用于配置繁简体转换 + - 现在更新版本会尝试加载旧版本的配置文件 + +Fix + +- 尝试通过限制加载时图片的大小以及给予更多内存空间的方式修复图片文件尺寸过大导致卡死的问题 +- 尝试修复图片区数字快捷键报错、 + +Change + +- 切换图片时默认选中第一个Label + + # 2.3.3 Fix - 修正了当换页后选中的Label索引值与上一页选中的Label索引值相同时无法输入翻译文本的问题; + - 修正了Label有时候会放歪的问题; + - 修正了框选Label后关闭文档会产生异常的问题; + - 修正了保存文件格式不匹配的问题; + - 修正了无法编辑项目图片的问题; + - 修正了排序报错的问题; Add - 现在可以直接使用滚轮来缩放图片(需要设置); - 现在可以在启动时直接打开上次文件(需要设置); diff --git a/README.md b/README.md index 9613346..c0a8fc8 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ +[简体中文](/README_ZH.md) | English

diff --git a/README_ZH.md b/README_ZH.md new file mode 100644 index 0000000..5db6038 --- /dev/null +++ b/README_ZH.md @@ -0,0 +1,130 @@ + + + + + +简体中文 | [English](/README.md) +
+

+ + Logo + +

Label Plus FX

+

+ 一个跨平台的Label Plus +
+
+ 用户手册 + · + 反馈问题 + · + 提交建议 +

+

+ + + +
+

目录

+
    +
  1. + 关于本项目 +
  2. +
  3. + 开始 + +
  4. +
  5. 说明
  6. +
  7. 许可协议
  8. +
  9. 联系方式
  10. +
+
+ + + +## 关于本项目 + +[![Product Screen Shot][product-screenshot]]() + +本项目受到 [LabelPlus](https://noodlefighter.com/label_plus/)的启发。 + + + +## 开始 + +复制本项目并启动需要以下几个简单的步骤 + +### 环境 + + * [Liberica JDK 17 (完整版本)](https://bell-sw.com/pages/downloads/#/java-17-lts%20/%20current) : 用于主应用程序; + + * [可选] [Visual Studio 2019](https://visualstudio.microsoft.com/zh-hans/downloads/) : 用于Windows IME JNI接口; + + +### 启动步骤 + +1.克隆仓库 + ```sh + git clone https://github.com/Meodinger/LabelPlusFX.git + ``` +2. 运行Maven命令 `package` + +3. 运行脚本, `link.bat` `build.bat` 都可以 + +4. 对于Windows用户, 构建封装器库 `IMEWrapper` 然后复制 `IMEInterface.dll` 和 `IMEWrapper.dll` 到 `LabelPlusFX.exe` (使用`jpackage`)或 `runtime\java.exe`(使用`jlink`) 所在的文件夹下. + +> 如果不想使用Windows IME JNI接口, 可以使用 `run.bat --disable-jni` 或`LabelPlusFX.exe --disable-jni`方式启动 + +> 在IDE中运行LPFX, 可以执行 `exec:java@run` 命令 + + +## 说明 + +Label Plus FX的功能设计基于 [LabelPlus](https://noodlefighter.com/label_plus/) + +更多示例,请参考用户手册和Wiki [User Manual](https://www.kdocs.cn/l/seRSJCKVOn0Y) 和 [Wiki](https://github.com/Meodinger/LabelPlusFX/wiki) + + + +## 贡献 + +开源社区因贡献而变得如此美好,充满学习、启发和创造。非常感谢您所做的**任何贡献** + +1. Fork项目 +2. 创建功能分支 (`git checkout -b feature/AmazingFeature`) +3. 提交更改 (`git commit -m '添加了一些很棒的改进'`) +4. 推送到分支 (`git push origin feature/AmazingFeature`) +5. 发起拉取请求 + + + +## 许可协议 + +根据AGPLv3许可证分发。有关更多信息,请参见`LICENSE`页面。 + + + +## 联系方式 + +Meodinger Wang - [@Meodinger_Wang](https://twitter.com/Meodinger_Wang) - meodinger@qq.com + +项目链接: [https://github.com/Meodinger/LabelPlusFX](https://github.com/Meodinger/LabelPlusFX) + + + +## 赞助 + + + Aifadian + + +[product-screenshot]: https://s2.loli.net/2022/02/04/2H7bguJ9rcyBjUO.png \ No newline at end of file diff --git a/pom.xml b/pom.xml index 4e5229d..839fc0f 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ ink.meodinger lpfx - 2.3.3-SNAPSHOT + 2.3.4 jar lpfx @@ -118,6 +118,10 @@ org.apache.maven.plugins maven-compiler-plugin 3.8.1 + + 17 + 17 + org.apache.maven.plugins diff --git a/script/jpackage/LabelPlusFXDict.lnk b/script/jpackage/LabelPlusFXDict.lnk new file mode 100644 index 0000000..4d2445b Binary files /dev/null and b/script/jpackage/LabelPlusFXDict.lnk differ diff --git a/script/jpackage/build.bat b/script/jpackage/build.bat index 45a30bc..d5b50ba 100644 --- a/script/jpackage/build.bat +++ b/script/jpackage/build.bat @@ -6,7 +6,7 @@ rd /S /Q ".\LabelPlusFX" set MODULES="%DIR%\target\build" set ICON="%DIR%\images\icons\cat.ico" -jpackage --verbose --type app-image --app-version 2.3.3 --copyright "Meodinger Tech (C) 2022" --name LabelPlusFX --icon %ICON% --dest . --module-path %MODULES% --add-modules lpfx,jdk.crypto.cryptoki --module lpfx/ink.meodinger.lpfx.LauncherKt +jpackage --verbose --type app-image --app-version 2.3.3 --copyright "Meodinger Tech (C) 2022" --name LabelPlusFX --icon %ICON% --dest . --module-path %MODULES% --add-modules lpfx,jdk.crypto.cryptoki --module lpfx/ink.meodinger.lpfx.LauncherKt --java-options "-Dprism.maxvram=2G" echo: -echo All completed, remember to copy dlls! \ No newline at end of file +echo All completed, remember to copy dlls! diff --git a/src/main/kotlin/ink/meodinger/lpfx/Controller.kt b/src/main/kotlin/ink/meodinger/lpfx/Controller.kt index 472e958..1d798f5 100644 --- a/src/main/kotlin/ink/meodinger/lpfx/Controller.kt +++ b/src/main/kotlin/ink/meodinger/lpfx/Controller.kt @@ -104,7 +104,7 @@ class Controller(private val state: State) { } } - private var accumulator: Long = 16 * 60 * 60 * ONE_SECOND + private var accumulator: Long = 16 * 60 * 60 * ONE_SECOND // Shift to 1970/01/02 00:00 GMT+8 private val accumulatorFormatter = SimpleDateFormat("HH:mm:ss") private val accumulatorManager = TimerTaskManager(0, ONE_SECOND) { if (state.isOpened) { @@ -143,7 +143,13 @@ class Controller(private val state: State) { // Opened and selected val file = state.getPicFileNow() if (file.exists()) { - val imageByFX = Image(file.toURI().toURL().toString()) + var imageByFX = Image(file.toURI().toURL().toString()) + + //if the image is too large,limit the size of image + if(imageByFX.width > 5000 || imageByFX.height > 5000) { + Logger.info("limit the size of image because `$file` is too large ", "Controller") + imageByFX = Image(file.toURI().toURL().toString(),5000.0,5000.0,true,true) + } if (!imageByFX.isError) { imageByFX @@ -202,12 +208,7 @@ class Controller(private val state: State) { Logger.info("Controller initialized", "Controller") // Display default image - cLabelPane.isVisible = false - Platform.runLater { - // re-locate after the initial rendering - cLabelPane.moveToCenter() - cLabelPane.isVisible = true - } + cLabelPane.moveToCenter() } /** @@ -353,7 +354,7 @@ class Controller(private val state: State) { } WorkMode.LabelMode -> { val transLabel = state.transFile.getTransLabel(state.currentPicName, it.labelIndex) - val transGroup = state.transFile.getTransGroup(transLabel.groupId) + val transGroup = state.transFile.groupList[transLabel.groupId] cLabelPane.showText(transGroup.name, transGroup.color, it.displayX, it.displayY) } } @@ -387,7 +388,7 @@ class Controller(private val state: State) { WorkMode.LabelMode -> { if (state.currentGroupId == NOT_FOUND) return@handler - val transGroup = state.transFile.getTransGroup(state.currentGroupId) + val transGroup = state.transFile.groupList[state.currentGroupId] cLabelPane.showText(transGroup.name, transGroup.color, it.displayX, it.displayY) } } @@ -405,10 +406,10 @@ class Controller(private val state: State) { if (it != NOT_FOUND) { if (cTreeView.isFocused) { // if the change is result of CTreeView selection, add - cTreeView.selectGroup(state.transFile.getTransGroup(it).name, clear = false, scrollTo = false) + cTreeView.selectGroup(state.transFile.groupList[it].name, clear = false, scrollTo = false) } else { // if the change is result of GroupBar/Box selection, set - cTreeView.selectGroup(state.transFile.getTransGroup(it).name, clear = true, scrollTo = true) + cTreeView.selectGroup(state.transFile.groupList[it].name, clear = true, scrollTo = true) } } } else { @@ -433,8 +434,10 @@ class Controller(private val state: State) { cPicBox.itemsProperty().bind(picNamesBinding) cPicBox.indexProperty().addListener(onNew { if (state.isOpened) { - // PicBox index should never be -1 (value should never be null) - state.currentPicName = state.transFile.sortedPicNames[it] + if (it != NOT_FOUND) { + // PicBox index should never be -1 except when removing a picture + state.currentPicName = state.transFile.sortedPicNames[it] + } } else { // Closed, do nothing. Let State set current-pic-name to empty string } @@ -512,10 +515,13 @@ class Controller(private val state: State) { // This could clear the label-index related bindings like TransArea text state.currentPicNameProperty().addListener(onChange { // If switch picture in CTreeView, the fours on TreeCell will not clear automatically + //clear selection + state.currentLabelIndex = NOT_FOUND // So we should manually clear it to make sure we start from the first label - cTreeView.selectRoot(clear = true, scrollTo = false) + cTreeView.selectFirst(clear = true, scrollTo = false) + cLabelPane.moveToLabel(cTreeView.selectedLabel) // Clear here, because the already happened selection may change it - state.currentLabelIndex = NOT_FOUND +// state.currentLabelIndex = NOT_FOUND }) Logger.info("Listened for current-pic-name change for clear label-index selection", "Controller") @@ -598,7 +604,6 @@ class Controller(private val state: State) { */ private fun transform() { Logger.info("Applying Transformations...", "Controller") - // Transform tab press in CTreeView to ViewModeBtn click cTreeView.addEventFilter(KeyEvent.KEY_PRESSED) { if (it.code == KeyCode.TAB) { @@ -621,6 +626,22 @@ class Controller(private val state: State) { } Logger.info("Transformed Tab on CLabelPane", "Controller") + val changePicHandler = EventHandler handler@{ + if (it.isControlDown || it.isMetaDown || it.isShiftDown || it.isAltDown || it.code.isDigitKey) return@handler + // Mark immediately when this event will be consumed + it.consume() // stop further propagation + + when (it.code) { + KeyCode.Q -> cPicBox.back() + KeyCode.W -> cPicBox.next() + else -> return@handler + } + cTreeView.selectFirst() + it.consume() // Consume used event + } + cLabelPane.addEventHandler(KeyEvent.KEY_PRESSED, changePicHandler) + Logger.info("Transformed A/D", "Controller") + // Transform number key press to CTreeView select val numberBuilder = StringBuilder() view.addEventHandler(KeyEvent.KEY_PRESSED) handler@{ @@ -631,17 +652,17 @@ class Controller(private val state: State) { // Mark immediately when this event will be consumed it.consume() // stop further propagation - val number = it.code.char.toInt() + val number = it.text.toInt() if (numberBuilder.isEmpty()) { // Not parsing if (number == 0) { // Start parse numberBuilder.append(0) - } else if (number in 1..state.transFile.groupCount) { + } else if (state.transFileProperty().isNotNull.value && number in 1..state.transFile.groupCount) { // Try select val index = number - 1 if (state.viewMode == ViewMode.GroupMode) { - cTreeView.selectGroup(state.transFile.getTransGroup(index).name, clear = true, scrollTo = false) + cTreeView.selectGroup(state.transFile.groupList[index].name, clear = true, scrollTo = false) } else { state.currentGroupId = index } @@ -652,10 +673,10 @@ class Controller(private val state: State) { // Parsing numberBuilder.append(number) val index = numberBuilder.toString().toInt() - 1 - if (index in 0 until state.transFile.groupCount) { + if ( state.transFileProperty().isNotNull.value &&index in 0 until state.transFile.groupCount) { // Try select if (state.viewMode == ViewMode.GroupMode) { - cTreeView.selectGroup(state.transFile.getTransGroup(index).name, clear = true, scrollTo = false) + cTreeView.selectGroup(state.transFile.groupList[index].name, clear = true, scrollTo = false) } else { state.currentGroupId = index } @@ -668,23 +689,6 @@ class Controller(private val state: State) { } Logger.info("Transformed num-key pressed", "Controller") - // Transform Ctrl + Left/Right KeyEvent to CPicBox button click - val arrowKeyChangePicHandler = EventHandler handler@{ - if (!(it.isControlDown || it.isMetaDown)) return@handler - - when (it.code) { - KeyCode.LEFT -> cPicBox.back() - KeyCode.RIGHT -> cPicBox.next() - else -> return@handler - } - - it.consume() // Consume used event - } - cLabelPane.addEventHandler(KeyEvent.KEY_PRESSED, arrowKeyChangePicHandler) - cTransArea.addEventHandler(KeyEvent.KEY_PRESSED, arrowKeyChangePicHandler) - cTreeView.addEventHandler(KeyEvent.KEY_PRESSED, arrowKeyChangePicHandler) - Logger.info("Transformed Ctrl + Left/Right", "Controller") - /** * Find next LabelItem as int index. * @return NOT_FOUND when have no next @@ -703,6 +707,40 @@ class Controller(private val state: State) { } } + fun moveCurrLabelTo(direction: Int) { + var itemIndex = getNextLabelItemIndex(cTreeView.selectionModel.selectedIndex, direction) + if (itemIndex == NOT_FOUND) { + // if selected first and try getting previous, return last; + // if selected last and try getting next, return first; + itemIndex = getNextLabelItemIndex(if (direction == 1) 0 else cTreeView.expandedItemCount, direction) + } + if(itemIndex == NOT_FOUND) { + return + } + val item = cTreeView.getTreeItem(itemIndex) as CTreeLabelItem + + cLabelPane.moveToLabel(item.transLabel.index) + cTreeView.selectLabel(item.transLabel.index, clear = true, scrollTo = true) + } + + // Transform Ctrl + Left/Right KeyEvent to CPicBox button click + val arrowKeyChangePicHandler = EventHandler handler@{ + if (!(it.isControlDown || it.isMetaDown)) return@handler + + when (it.code) { + KeyCode.LEFT -> cPicBox.back() + KeyCode.RIGHT -> cPicBox.next() + else -> return@handler + } + cTreeView.selectFirst() + it.consume() // Consume used event + } + cLabelPane.addEventHandler(KeyEvent.KEY_PRESSED, arrowKeyChangePicHandler) + cTransArea.addEventHandler(KeyEvent.KEY_PRESSED, arrowKeyChangePicHandler) + cTreeView.addEventHandler(KeyEvent.KEY_PRESSED, arrowKeyChangePicHandler) + Logger.info("Transformed Ctrl + Left/Right", "Controller") + + // Transform Ctrl + Up/Down KeyEvent to CTreeView select (and have effect: move to label) val arrowKeyChangeLabelHandler = EventHandler handler@{ if (!((it.isControlDown || it.isMetaDown) && it.code.isArrowKey)) return@handler @@ -717,16 +755,7 @@ class Controller(private val state: State) { // Mark immediately when this event will be consumed it.consume() // stop further propagation - var itemIndex = getNextLabelItemIndex(cTreeView.selectionModel.selectedIndex, itemShift) - if (itemIndex == NOT_FOUND) { - // if selected first and try getting previous, return last; - // if selected last and try getting next, return first; - itemIndex = getNextLabelItemIndex(if (itemShift == 1) 0 else cTreeView.expandedItemCount, itemShift) - } - val item = cTreeView.getTreeItem(itemIndex) as CTreeLabelItem - - cLabelPane.moveToLabel(item.transLabel.index) - cTreeView.selectLabel(item.transLabel.index, clear = true, scrollTo = true) + moveCurrLabelTo(itemShift) } cLabelPane.addEventHandler(KeyEvent.KEY_PRESSED, arrowKeyChangeLabelHandler) cTransArea.addEventHandler(KeyEvent.KEY_PRESSED, arrowKeyChangeLabelHandler) @@ -742,20 +771,58 @@ class Controller(private val state: State) { // transform if (it.isShiftDown) { // Met the bounds, change picture - if (itemIndex == NOT_FOUND) cLabelPane.fireEvent(keyEvent(it, code = KeyCode.LEFT, character = "", text = "")) - // Go to previous label - cLabelPane.fireEvent(keyEvent(it, code = KeyCode.UP, character = "", text = "")) + if (itemIndex == NOT_FOUND) { + cPicBox.back() + cTreeView.selectLast() + } else { + // Go to previous label + moveCurrLabelTo(direction = -1) + } } else { // Met the bounds, change picture - if (itemIndex == NOT_FOUND) cLabelPane.fireEvent(keyEvent(it, code = KeyCode.RIGHT, character = "", text = "")) - // Go to previous label - cLabelPane.fireEvent(keyEvent(it, code = KeyCode.DOWN, character = "", text = "")) + if (itemIndex == NOT_FOUND) { + cPicBox.next() + cTreeView.selectFirst() + } else { + // Go to previous label + moveCurrLabelTo(direction = 1) + } } } cLabelPane.addEventHandler(KeyEvent.KEY_PRESSED, enterKeyTransformerHandler) cTransArea.addEventHandler(KeyEvent.KEY_PRESSED, enterKeyTransformerHandler) Logger.info("Transformed Ctrl + Enter", "Controller") + + + + val copyLabelHandler = EventHandler handler@{ + if (!(it.isControlDown || it.isMetaDown)) return@handler + when (it.code) { + KeyCode.C -> // cTreeView.pasteLabelsText(selectItems.map { it.transLabel.index },state) + { + @Suppress("UNCHECKED_CAST") val treeItem = cTreeView.getTreeItem(cTreeView.selectionModel.selectedIndex) as CTreeLabelItem + cTreeView.copyLabelText(treeItem.transLabel.index) + } + KeyCode.V -> { + @Suppress("UNCHECKED_CAST") val selectItems:Collection = + cTreeView.selectionModel.selectedIndices.map { cTreeView.getTreeItem(it)} + .filter { it is CTreeLabelItem } as List + cTreeView.pasteLabelsText(selectItems.map { it.transLabel.index },state) + } + else ->// cTreeView.pasteLabelsText(selectItems.map { it.transLabel.index },state) + // cTreeView.pasteLabelsText(selectItems.map { it.transLabel.index },state) + { + return@handler + } + } + it.consume() // Consume used event + } + cTreeView.addEventHandler(KeyEvent.KEY_PRESSED, copyLabelHandler) + Logger.info("Transformed Ctrl + C/V", "Controller") +// + + } // Controller Methods @@ -932,8 +999,8 @@ class Controller(private val state: State) { state.currentLabelIndex = labelIndex.takeIf { state.transFile.getTransList(state.currentPicName).any { l -> l.index == it } } ?: NOT_FOUND // Move to center - // FIXME: May throw NoSuchElementException if render not complete if (labelIndex != NOT_FOUND) { + // NotNow: May throw NoSuchElementException if render not complete cTreeView.selectLabel(labelIndex, clear = true, scrollTo = true) cLabelPane.moveToLabel(labelIndex) } @@ -965,7 +1032,7 @@ class Controller(private val state: State) { } // Use temp if overwrite - val exportDest = if (overwrite) File.createTempFile(file.path, "temp").apply(File::deleteOnExit) else file + val exportDest = if (overwrite) File.createTempFile("LPFX", ".${file.extension}").apply(File::deleteOnExit) else file // Export try { @@ -1140,38 +1207,38 @@ class Controller(private val state: State) { val version = fetchLatestSync() if (version != Version.V0) Logger.info("Got latest version: $version (current $V)", "Controller") - if (version > V) Platform.runLater { - val suppressNoticeButtonType = ButtonType(I18N["update.dialog.suppress"], ButtonBar.ButtonData.OK_DONE) - - val dialog = Dialog() - dialog.initOwner(this@Controller.state.stage) - dialog.title = I18N["update.dialog.title"] - dialog.graphic = ImageView(IMAGE_INFO.resizeByRadius(GENERAL_ICON_RADIUS)) - dialog.dialogPane.buttonTypes.addAll(suppressNoticeButtonType, ButtonType.CLOSE) - dialog.dialogPane.withContent(VBox()) { - add(Label(String.format(I18N["update.dialog.content.s"], version))) - add(Separator()) { - padding = Insets(8.0, 0.0, 8.0, 0.0) - } - add(Hyperlink(I18N["update.dialog.link"])) { - padding = Insets(0.0) - setOnAction { this@Controller.state.application.hostServices.showDocument(release) } + Platform.runLater { + if (version > V) { + val suppressNoticeButtonType = ButtonType(I18N["update.dialog.suppress"], ButtonBar.ButtonData.OK_DONE) + + val dialog = Dialog() + dialog.initOwner(this@Controller.state.stage) + dialog.title = I18N["update.dialog.title"] + dialog.graphic = ImageView(IMAGE_INFO.resizeByRadius(GENERAL_ICON_RADIUS)) + dialog.dialogPane.buttonTypes.addAll(suppressNoticeButtonType, ButtonType.CLOSE) + dialog.dialogPane.withContent(VBox()) { + add(Label(String.format(I18N["update.dialog.content.s"], version))) + add(Separator()) { + padding = Insets(8.0, 0.0, 8.0, 0.0) + } + add(Hyperlink(I18N["update.dialog.link"])) { + padding = Insets(0.0) + setOnAction { this@Controller.state.application.hostServices.showDocument(release) } + } } - } - val suppressButton = dialog.dialogPane.lookupButton(suppressNoticeButtonType) - ButtonBar.setButtonUniformSize(suppressButton, false) + val suppressButton = dialog.dialogPane.lookupButton(suppressNoticeButtonType) + ButtonBar.setButtonUniformSize(suppressButton, false) - dialog.showAndWait().ifPresent { type -> - if (type == suppressNoticeButtonType) { - Preference.lastUpdateNotice = time - Logger.info("Check suppressed, next notice time is ${time + delay}", - "Controller" - ) + dialog.showAndWait().ifPresent { type -> + if (type == suppressNoticeButtonType) { + Preference.lastUpdateNotice = time + Logger.info("Check suppressed, next notice time is ${time + delay}", "Controller") + } } + } else if (showWhenUpdated) { + showInfo(this@Controller.state.stage, I18N["update.info.updated"]) } - } else if (showWhenUpdated) Platform.runLater { - showInfo(this@Controller.state.stage, I18N["update.info.updated"]) } }() } diff --git a/src/main/kotlin/ink/meodinger/lpfx/LabelPlusFXDict.kt b/src/main/kotlin/ink/meodinger/lpfx/LabelPlusFXDict.kt index c8a5017..9f6b950 100644 --- a/src/main/kotlin/ink/meodinger/lpfx/LabelPlusFXDict.kt +++ b/src/main/kotlin/ink/meodinger/lpfx/LabelPlusFXDict.kt @@ -30,6 +30,7 @@ class LabelPlusFXDict: Application() { showException(primaryStage, e) } // Set up the Stage + primaryStage.icons.add(ICON) primaryStage.title = "LPFX Dictionary (Standalone)" primaryStage.scene = OnlineDict().scene primaryStage.width = 400.0 diff --git a/src/main/kotlin/ink/meodinger/lpfx/State.kt b/src/main/kotlin/ink/meodinger/lpfx/State.kt index c2d88b5..13e27ec 100644 --- a/src/main/kotlin/ink/meodinger/lpfx/State.kt +++ b/src/main/kotlin/ink/meodinger/lpfx/State.kt @@ -172,21 +172,21 @@ class State { private val undoStack: Stack = ArrayStack() private val redoStack: Stack = ArrayStack() - private val canUndoProperty: BooleanProperty = SimpleBooleanProperty(false) + private val undoableProperty: BooleanProperty = SimpleBooleanProperty(false) /** * Whether undo is available */ - fun undoableProperty(): ReadOnlyBooleanProperty = canUndoProperty + fun undoableProperty(): ReadOnlyBooleanProperty = undoableProperty /** * @see undoableProperty */ - val isUndoable: Boolean by canUndoProperty + val isUndoable: Boolean by undoableProperty private val redoableProperty: BooleanProperty = SimpleBooleanProperty(false) /** * Whether redo is available */ - fun canRedoProperty(): ReadOnlyBooleanProperty = redoableProperty + fun redoableProperty(): ReadOnlyBooleanProperty = redoableProperty /** * @see redoableProperty */ @@ -201,7 +201,7 @@ class State { Logger.info("Action committed", "State") isChanged = true - canUndoProperty.set(true) + undoableProperty.set(true) redoableProperty.set(false) } @@ -214,7 +214,7 @@ class State { redoStack.push(undoStack.pop().apply(Action::revert)) Logger.info("Action reverted", "State") - canUndoProperty.set(!undoStack.isEmpty()) + undoableProperty.set(!undoStack.isEmpty()) redoableProperty.set(true) } @@ -227,18 +227,16 @@ class State { undoStack.push(redoStack.pop().apply(Action::commit)) Logger.info("Action re-committed", "State") - canUndoProperty.set(true) + undoableProperty.set(true) redoableProperty.set(!redoStack.isEmpty()) } // endregion /** - * Reset the entire worksapce, be ready to open another translation. + * Reset the entire workspace, be ready to open another translation. */ fun reset() { - if (!isOpened) return - controller.reset() undoStack.empty() diff --git a/src/main/kotlin/ink/meodinger/lpfx/View.kt b/src/main/kotlin/ink/meodinger/lpfx/View.kt index 0fbbfb2..f7264b2 100644 --- a/src/main/kotlin/ink/meodinger/lpfx/View.kt +++ b/src/main/kotlin/ink/meodinger/lpfx/View.kt @@ -124,9 +124,11 @@ class View(private val state: State) : BorderPane() { */ val cTransArea: CLigatureArea = CLigatureArea() + // Private Components private val statsBar: HBox = HBox() private val cTreeMenu: CTreeMenu = CTreeMenu(state, cTreeView) + private val cTextMenu: CTextMenu = CTextMenu(cTransArea) // endregion @@ -230,6 +232,9 @@ class View(private val state: State) : BorderPane() { item(I18N["m.bak_recovery"]) { does { bakRecovery() } } + item(I18N["m.settings"]) { + does { settings() } + } separator() item(I18N["m.exit"]) { does { exitApplication() } @@ -243,7 +248,7 @@ class View(private val state: State) : BorderPane() { } item(I18N["m.redo"]) { does { state.redo() } - disableProperty().bind(!state.canRedoProperty()) + disableProperty().bind(!state.redoableProperty()) accelerator = KeyCodeCombination(KeyCode.Z, KeyCombination.SHORTCUT_DOWN, KeyCombination.SHIFT_DOWN) } separator() @@ -311,9 +316,6 @@ class View(private val state: State) : BorderPane() { } } menu(I18N["mm.about"]) { - item(I18N["m.settings"]) { - does { settings() } - } item(I18N["m.logs"]) { does { logs() } } @@ -771,6 +773,7 @@ class View(private val state: State) : BorderPane() { Settings.DefaultGroupColorHexList -> Settings.defaultGroupColorHexList .setAll(value as List) Settings.IsGroupCreateOnNewTrans -> Settings.isGroupCreateOnNewTransList .setAll(value as List) Settings.LigatureRules -> Settings.ligatureRules .setAll(value as List>) + Settings.QuickInputTexts -> Settings.quickInputTexts .setAll(value as List) Settings.ViewModes -> Settings.viewModes .setAll(value as List) Settings.NewPictureScale -> Settings.newPictureScalePicture = value as CLabelPane.NewPictureScale Settings.UseWheelToScale -> Settings.useWheelToScale = value as Boolean @@ -784,6 +787,9 @@ class View(private val state: State) : BorderPane() { Settings.UseMeoFileAsDefault -> Settings.useMeoFileAsDefault = value as Boolean Settings.UseExportNameTemplate -> Settings.useExportNameTemplate = value as Boolean Settings.ExportNameTemplate -> Settings.exportNameTemplate = value as String + Settings.UseCustomBaiduKey -> Settings.useCustomBaiduKey = value as Boolean + Settings.BaiduTransLateKey -> Settings.baiduTransLateKey = value as String + Settings.BaiduTransLateAppId -> Settings.baiduTransLateAppId = value as String else -> doNothing() } } diff --git a/src/main/kotlin/ink/meodinger/lpfx/action/GroupAction.kt b/src/main/kotlin/ink/meodinger/lpfx/action/GroupAction.kt index 618d631..cd1dbdf 100644 --- a/src/main/kotlin/ink/meodinger/lpfx/action/GroupAction.kt +++ b/src/main/kotlin/ink/meodinger/lpfx/action/GroupAction.kt @@ -58,21 +58,20 @@ class GroupAction( throw IllegalArgumentException(String.format(I18N["exception.action.group_repeated.s"], transGroup.name)) state.transFile.groupListObservable.add(groupId, transGroup) - for (labels in state.transFile.transMapObservable.values) for (label in labels) if (label.groupId >= groupId) label.groupId++ + @Suppress("DEPRECATION") state.transFile.installGroup(transGroup) Logger.info("Added TransGroup: $transGroup", "Action") } private fun removeTransGroup(transGroup: TransGroup) { - val groupId = state.transFile.getGroupIdByName(transGroup.name) - if (state.transFile.isGroupStillInUse(groupId)) + if (state.transFile.isGroupStillInUse(transGroup.name)) throw IllegalArgumentException(String.format(I18N["exception.action.group_still_in_use.s"], transGroup.name)) + @Suppress("DEPRECATION") state.transFile.disposeGroup(transGroup) for (labels in state.transFile.transMapObservable.values) for (label in labels) - if (label.groupId >= groupId) label.groupId-- - - state.transFile.groupListObservable.removeAt(groupId) + if (label.groupId >= transGroup.index) label.groupId-- + state.transFile.groupListObservable.removeAt(transGroup.index) Logger.info("Removed TransGroup: $transGroup", "Action") } diff --git a/src/main/kotlin/ink/meodinger/lpfx/action/LabelAction.kt b/src/main/kotlin/ink/meodinger/lpfx/action/LabelAction.kt index 7153f24..3b36243 100644 --- a/src/main/kotlin/ink/meodinger/lpfx/action/LabelAction.kt +++ b/src/main/kotlin/ink/meodinger/lpfx/action/LabelAction.kt @@ -50,7 +50,16 @@ class LabelAction( if (newLabelIndex != NOT_FOUND) { builder.append("@index: ${targetTransLabel.index.pad(2)} -> ${index.pad(2)}; ") - targetTransLabel.index = index + if (targetTransLabel.index == index) return + val transLabel = TransLabel( + index, + targetTransLabel.groupId, + targetTransLabel.x, + targetTransLabel.y, + targetTransLabel.text + ) + removeTransLabel(targetPicName,targetTransLabel) + addTransLabel(targetPicName,transLabel) } if (newGroupId != NOT_FOUND) { builder.append("@groupId: ${targetTransLabel.groupId.pad(2)} -> ${groupId.pad(2)}; ") @@ -82,7 +91,7 @@ class LabelAction( throw IllegalArgumentException(String.format(I18N["exception.action.label_group_invalid.i"], transLabel.groupId)) for (label in list) if (label.index >= transLabel.index) label.index++ - list.add(transLabel) + list.add(transLabel.index -1 ,transLabel) @Suppress("DEPRECATION") state.transFile.installLabel(transLabel) Logger.info("Added $picName @ $transLabel", "Action") diff --git a/src/main/kotlin/ink/meodinger/lpfx/component/CLabelPane.kt b/src/main/kotlin/ink/meodinger/lpfx/component/CLabelPane.kt index 7539762..3a6a82f 100644 --- a/src/main/kotlin/ink/meodinger/lpfx/component/CLabelPane.kt +++ b/src/main/kotlin/ink/meodinger/lpfx/component/CLabelPane.kt @@ -845,7 +845,8 @@ class CLabelPane : ScrollPane() { * @param labelIndex Index of the label which will be displaye at the center */ fun moveToLabel(labelIndex: Int) { - val label = labelNodes.first { it.index == labelIndex } + val label = labelNodes.firstOrNull { it.index == labelIndex } ?:return + vvalue = 0.0 hvalue = 0.0 diff --git a/src/main/kotlin/ink/meodinger/lpfx/component/CTextMenu.kt b/src/main/kotlin/ink/meodinger/lpfx/component/CTextMenu.kt new file mode 100644 index 0000000..de36a29 --- /dev/null +++ b/src/main/kotlin/ink/meodinger/lpfx/component/CTextMenu.kt @@ -0,0 +1,99 @@ +package ink.meodinger.lpfx.component + +import ink.meodinger.lpfx.I18N +import ink.meodinger.lpfx.get +import ink.meodinger.lpfx.options.Logger +import ink.meodinger.lpfx.options.Settings +import ink.meodinger.lpfx.util.property.onChange +import javafx.event.EventHandler +import javafx.scene.control.* + +class CTextMenu( + private val textField: TextInputControl +) : ContextMenu() { + + // add default menu items + private val undoMI = MenuItem(I18N["input.menu.undo"]).apply { + onAction = EventHandler { textField.undo() } + } + private val redoMI = MenuItem(I18N["input.menu.redo"]).apply { + onAction = EventHandler { textField.redo() } + } + private val cutMI = MenuItem(I18N["input.menu.cut"]).apply { + onAction = EventHandler { textField.cut() } + } + private val copyMI = MenuItem(I18N["input.menu.copy"]).apply { + onAction = EventHandler { textField.copy() } + } + private val pasteMI = MenuItem(I18N["input.menu.paste"]).apply { + onAction = EventHandler { textField.paste() } + } + private val deleteMI = MenuItem(I18N["input.menu.delete_selection"]).apply { + onAction = EventHandler { deleteSelectedText(textField) } + } + private val selectAllMI = MenuItem(I18N["input.menu.select_all"]).apply { + onAction = EventHandler { textField.selectAll() } + } + + // add custom menu items + private val quickInput = Menu(I18N["input.menu.quick_input"]) + + + init { + textField.undoableProperty() + .addListener { _, _, newValue -> + undoMI.isDisable = + !newValue!! + } + textField.redoableProperty() + .addListener { _, _, newValue -> + redoMI.isDisable = + !newValue!! + } + textField.selectionProperty() + .addListener { _, _, newValue -> + cutMI.isDisable = + newValue.length == 0 + copyMI.isDisable = newValue.length == 0 + deleteMI.isDisable = newValue.length == 0 + selectAllMI.isDisable = newValue.length == newValue.end + } + + //init QuickInputItems + initQuickInputItems(quickInput) + textField.contextMenu = ContextMenu( + undoMI, redoMI, cutMI, copyMI, pasteMI, deleteMI, SeparatorMenuItem(), selectAllMI, quickInput + ) + } + + private fun deleteSelectedText(t: TextInputControl) { + val range = t.selection + if (range.length == 0) { + return + } + val text = t.text + val newText = text.substring(0, range.start) + text.substring(range.end) + t.text = newText + t.positionCaret(range.start) + } + + private fun initQuickInputItems(menu: Menu) { + menu.items.clear() + menu.items.addAll(getQuickInputItems(textField)) + Settings.quickInputTextsProperty.addListener( onChange { + Logger.info("refresh the quick input items", "CTextMenu") + menu.items.clear() + menu.items.addAll(getQuickInputItems(textField)) + }) + } + + private fun getQuickInputItems(t: TextInputControl): List { + return Settings.quickInputTexts.map { + MenuItem(it).apply { + onAction = EventHandler { t.appendText(text) } + } + } + } + + +} diff --git a/src/main/kotlin/ink/meodinger/lpfx/component/CTreeMenu.kt b/src/main/kotlin/ink/meodinger/lpfx/component/CTreeMenu.kt index 4f365e6..d4055fe 100644 --- a/src/main/kotlin/ink/meodinger/lpfx/component/CTreeMenu.kt +++ b/src/main/kotlin/ink/meodinger/lpfx/component/CTreeMenu.kt @@ -9,6 +9,7 @@ import ink.meodinger.lpfx.type.TransGroup import ink.meodinger.lpfx.util.color.toHexRGB import ink.meodinger.lpfx.util.component.withContent import ink.meodinger.lpfx.component.dialog.showError +import ink.meodinger.lpfx.type.TransLabel import ink.meodinger.lpfx.util.doNothing import ink.meodinger.lpfx.util.property.transform @@ -117,7 +118,7 @@ class CTreeMenu( // do Action state.doAction(GroupAction( ActionType.CHANGE, state, - state.transFile.getTransGroup(state.transFile.getGroupIdByName(it.source as String)), + state.transFile.getTransGroup(it.source as String), newName = newName )) } @@ -128,7 +129,7 @@ class CTreeMenu( private val gChangeColorHandler = EventHandler { state.doAction(GroupAction( ActionType.CHANGE, state, - state.transFile.getTransGroup(state.transFile.getGroupIdByName(it.source as String)), + state.transFile.getTransGroup(it.source as String), newColorHex = (it.target as ColorPicker).value.toHexRGB() )) } @@ -142,7 +143,7 @@ class CTreeMenu( state.doAction(GroupAction( ActionType.REMOVE, state, - state.transFile.getTransGroup(state.transFile.getGroupIdByName(it.source as String)) + state.transFile.getTransGroup(it.source as String) )) // Select the first group if TransFile has @@ -163,14 +164,14 @@ class CTreeMenu( } val choice = dialog.showAndWait() if (!choice.isPresent) return@EventHandler - val newGroupId = state.transFile.getGroupIdByName(choice.get()) + val transGroup = state.transFile.getTransGroup(choice.get()) val labelActions = items.map { LabelAction( ActionType.CHANGE, state, state.currentPicName, state.transFile.getTransLabel(state.currentPicName, it.transLabel.index), - newGroupId = newGroupId + newGroupId = transGroup.index ) } val moveAction = FunctionAction( @@ -194,6 +195,54 @@ class CTreeMenu( } private val lDeleteItem = MenuItem(I18N["context.delete_label"]) + private val lMoveToIndexHandler = EventHandler { event -> + @Suppress("UNCHECKED_CAST") val items = event.source as List + // choose the first transLabel + val item = items[0] + val labels = state.transFile.getTransList(state.currentPicName).map(TransLabel::index) + + val dialog = ChoiceDialog(labels[0], labels).apply { + initOwner(state.stage) + title = I18N["context.move_to_index.dialog.title"] + contentText = I18N["context.move_to_index.dialog.header"] + } + val choice = dialog.showAndWait() + if (!choice.isPresent) return@EventHandler +// val transGroup = state.transFile.getTransGroup(choice.get()) + val labelAction= LabelAction( + ActionType.CHANGE, state, + state.currentPicName, + state.transFile.getTransLabel(state.currentPicName, item.transLabel.index), + newLabelIndex = choice.get() + ) + + val moveAction = FunctionAction( + { labelAction.commit(); state.controller.requestUpdateTree() }, + { labelAction.revert(); state.controller.requestUpdateTree() } + ) + state.doAction(moveAction) + } + + private val lMoveToIndexItem = MenuItem(I18N["context.move_to_index"]) + + private val lCopyLabelTextHandler = EventHandler { event -> + @Suppress("UNCHECKED_CAST") val items = event.source as List + // choose the first transLabel + val item = items[0] + view.copyLabelText(item.transLabel.index) + } + + private val lCopyLabelTextItem = MenuItem(I18N["context.copy_label_text"]) + + private val lPasteLabelTextHandler = EventHandler { event -> + @Suppress("UNCHECKED_CAST") val items = event.source as List + view.pasteLabelsText(items.map { it.transLabel.index },state) + } + + private val lPasteLabelTextItem = MenuItem(I18N["context.paste_label_text"]) + + + // endregion init { @@ -223,7 +272,7 @@ class CTreeMenu( val groupItem = selectedItems[0] as CTreeGroupItem gChangeColorPicker.value = groupItem.transGroup.color - gDeleteItem.isDisable = state.transFile.isGroupStillInUse(state.transFile.getGroupIdByName(groupItem.value)) + gDeleteItem.isDisable = state.transFile.isGroupStillInUse(groupItem.value) gRenameItem.setOnAction { gRenameHandler.handle(ActionEvent(groupItem.transGroup.name, gRenameItem)) } gChangeColorPicker.setOnAction { gChangeColorHandler.handle(ActionEvent(groupItem.transGroup.name, gChangeColorPicker)) } @@ -238,17 +287,24 @@ class CTreeMenu( // NOTE: we cannot change names here, so it is safe to store names val groupNames = selectedItems.map { (it as CTreeGroupItem).transGroup.name } - gDeleteItem.isDisable = groupNames.any { state.transFile.isGroupStillInUse(state.transFile.getGroupIdByName(it)) } + gDeleteItem.isDisable = groupNames.any { state.transFile.isGroupStillInUse(it) } gDeleteItem.setOnAction { groupNames.forEach { gDeleteHandler.handle(ActionEvent(it, gDeleteItem)) } } items.add(gDeleteItem) } else if (rootCount == 0 && groupCount == 0 && labelCount > 0) { // label(s) + lMoveToIndexItem.setOnAction { lMoveToIndexHandler.handle(ActionEvent(selectedItems, lMoveToIndexItem)) } lMoveToItem.setOnAction { lMoveToHandler.handle(ActionEvent(selectedItems, lMoveToItem)) } + lCopyLabelTextItem.setOnAction { lCopyLabelTextHandler.handle(ActionEvent(selectedItems, lCopyLabelTextItem)) } + lPasteLabelTextItem.setOnAction { lPasteLabelTextHandler.handle(ActionEvent(selectedItems, lPasteLabelTextItem)) } lDeleteItem.setOnAction { lDeleteHandler.handle(ActionEvent(selectedItems, lDeleteItem)) } + items.add(lMoveToIndexItem) items.add(lMoveToItem) items.add(SeparatorMenuItem()) + items.add(lCopyLabelTextItem) + items.add(lPasteLabelTextItem) + items.add(SeparatorMenuItem()) items.add(lDeleteItem) } else { // other diff --git a/src/main/kotlin/ink/meodinger/lpfx/component/CTreeView.kt b/src/main/kotlin/ink/meodinger/lpfx/component/CTreeView.kt index 6776821..43942c7 100644 --- a/src/main/kotlin/ink/meodinger/lpfx/component/CTreeView.kt +++ b/src/main/kotlin/ink/meodinger/lpfx/component/CTreeView.kt @@ -1,6 +1,11 @@ package ink.meodinger.lpfx.component import ink.meodinger.lpfx.* +import ink.meodinger.lpfx.action.Action +import ink.meodinger.lpfx.action.ActionType +import ink.meodinger.lpfx.action.FunctionAction +import ink.meodinger.lpfx.action.LabelAction +import ink.meodinger.lpfx.options.Logger import ink.meodinger.lpfx.type.TransGroup import ink.meodinger.lpfx.type.TransLabel import ink.meodinger.lpfx.util.component.expandAll @@ -13,6 +18,11 @@ import javafx.collections.FXCollections import javafx.collections.ListChangeListener import javafx.collections.ObservableList import javafx.scene.control.* +import javafx.scene.input.Clipboard +import javafx.scene.input.ClipboardContent + + + /** @@ -68,6 +78,15 @@ class CTreeView: TreeView() { private val groupItems: MutableList = ArrayList() private val labelItems: MutableList = ArrayList() +// private val copyTextProperty: StringProperty = SimpleStringProperty(emptyString()) +// +// /** +// * Selected copyText +// */ +// var copyText: String by copyTextProperty +// private set + + init { // Init @@ -128,6 +147,7 @@ class CTreeView: TreeView() { for ((groupId, transGroup) in groups.withIndex()) createGroupItem(transGroup, groupId) for (transLabel in labels) createLabelItem(transLabel) + selectLabel(selectedLabel, clear = true, scrollTo = true) } private fun createGroupItem(transGroup: TransGroup, groupId: Int) { val groupItem = CTreeGroupItem(transGroup) @@ -181,6 +201,22 @@ class CTreeView: TreeView() { selectionModel.select(root) if (scrollTo) scrollTo(getRow(root)) } + fun selectFirst(clear: Boolean = true, scrollTo: Boolean = true) : Int { + if(labelItems.isEmpty()) { + selectRoot(clear, scrollTo) + return NOT_FOUND + } + selectLabel(labelItems[0].transLabel.index, clear, scrollTo) + return labelItems[0].transLabel.index + } + fun selectLast(clear: Boolean = true, scrollTo: Boolean = true) : Int { + if(labelItems.isEmpty()) { + selectRoot(clear, scrollTo) + return NOT_FOUND + } + selectLabel(labelItems.last().transLabel.index, clear, scrollTo) + return labelItems.last().transLabel.index + } fun selectGroup(groupName: String, clear: Boolean, scrollTo: Boolean) { // In IndexMode this is not available if (viewMode == ViewMode.IndexMode) return @@ -192,8 +228,8 @@ class CTreeView: TreeView() { if (scrollTo) scrollTo(getRow(item)) } fun selectLabel(labelIndex: Int, clear: Boolean, scrollTo: Boolean) { + val item = labelItems.firstOrNull{ it.transLabel.index == labelIndex } ?:return if (clear) clearSelection() - val item = labelItems.first { it.transLabel.index == labelIndex } selectionModel.select(item) if (scrollTo) scrollTo(getRow(item)) @@ -206,6 +242,36 @@ class CTreeView: TreeView() { if (scrollTo) scrollTo(getRow(items.first())) } + fun copyLabelText(labelIndex: Int) { + val item = labelItems.firstOrNull { it.transLabel.index == labelIndex } ?:return + val clipboard = Clipboard.getSystemClipboard() + val clipboardContent = ClipboardContent() + clipboardContent.putString(item.transLabel.text) + Logger.info("Copy text from the label of $labelIndex", "CTreeView") + clipboard.setContent(clipboardContent) + } + + fun pasteLabelsText(labelIndexes: Collection, state: State) { + val indexes = labelItems.map { it.transLabel.index }.filter { labelIndexes.any { i -> i == it } } + val clipboard = Clipboard.getSystemClipboard() + if (!clipboard.hasString()||indexes.isEmpty()) return + Logger.info("Paste text into the labels of ${indexes.joinToString(separator = ",")}", "CTreeView") + val labelActions = indexes.map { + LabelAction( + ActionType.CHANGE, state, + state.currentPicName, + state.transFile.getTransLabel(state.currentPicName, it), + newText = clipboard.string + ) + } + val pasteAction = FunctionAction( + { labelActions.forEach(Action::commit) }, + { labelActions.forEach(Action::revert) } + ) + state.doAction(pasteAction) + } + + /** * This will also clear the selected-index */ @@ -219,6 +285,8 @@ class CTreeView: TreeView() { * Request the TreeView to re-render. This function is useful * when some labels' group change in IndexMode. */ - fun requestUpdate() { update() } + fun requestUpdate() { + update() + } } diff --git a/src/main/kotlin/ink/meodinger/lpfx/component/properties/DialogSettings.kt b/src/main/kotlin/ink/meodinger/lpfx/component/properties/DialogSettings.kt index fb700ac..3502ad3 100644 --- a/src/main/kotlin/ink/meodinger/lpfx/component/properties/DialogSettings.kt +++ b/src/main/kotlin/ink/meodinger/lpfx/component/properties/DialogSettings.kt @@ -46,6 +46,7 @@ class DialogSettings : AbstractPropertiesDialog() { companion object { private const val gRowShift = 1 private const val rRowShift = 1 + private const val qRowShift = 1 private const val rIsFrom = "C_Is_From" private const val rRuleIndex = "C_Rule_Index" } @@ -71,6 +72,14 @@ class DialogSettings : AbstractPropertiesDialog() { private val rLabelFrom = Label(I18N["settings.ligature.from"]) private val rLabelTo = Label(I18N["settings.ligature.to"]) + private val qGridPane = GridPane().apply { + alignment = Pos.TOP_CENTER + padding = Insets(16.0) + vgap = 16.0 + hgap = 16.0 + } + private val qLabelHint = Label(I18N["settings.quick_input.hint"]) + private val mComboInput = CComboBox() private val mComboLabel = CComboBox() private val mComboScale = CComboBox() @@ -91,6 +100,9 @@ class DialogSettings : AbstractPropertiesDialog() { private val xCheckUseMeo = CheckBox(I18N["settings.other.meo_default"]) private val xCheckUseTmp = CheckBox(I18N["settings.other.template.enable"]) private val xFieldTemplate = TextField() + private val xCheckUseCustomBaiduKey = CheckBox(I18N["settings.other.Translate_keys.enable"]) + private val xFieldBaiduTranslateKey = TextField() + private val xFieldBaiduTranslateAppId = TextField() init { title = I18N["settings.title"] @@ -130,6 +142,22 @@ class DialogSettings : AbstractPropertiesDialog() { } } } + add(I18N["settings.quick_input.title"]) { + withContent(BorderPane()) { + val stackPane = StackPane(qGridPane) + val scrollPane = ScrollPane(stackPane) + stackPane.prefWidthProperty().bind(scrollPane.widthProperty() - 16.0) + + center(scrollPane) { style = "-fx-background-color:transparent;" } + bottom(HBox()) { + alignment = Pos.CENTER_RIGHT + padding = Insets(16.0, 8.0, 8.0, 16.0) + add(Label(I18N["settings.quick_input.sample"])) + add(HBox()) { hgrow = Priority.ALWAYS } + add(Button(I18N["settings.quick_input.add"])) { does { createQuickInputRow() } } + } + } + } add(I18N["settings.mode.title"]) { withContent(GridPane()) { alignment = Pos.TOP_CENTER @@ -348,6 +376,16 @@ class DialogSettings : AbstractPropertiesDialog() { showDelay = Duration(500.0) } } + add(xCheckUseCustomBaiduKey, 0, 7, 2, 1) + add(Label(I18N["settings.other.Translate_keys.key"]), 0, 8) + add(xFieldBaiduTranslateKey, 1, 8) { + disableProperty().bind(!xCheckUseCustomBaiduKey.selectedProperty()) + + } + add(Label(I18N["settings.other.Translate_keys.app_id"]), 0, 9) + add(xFieldBaiduTranslateAppId, 1, 9) { + disableProperty().bind(!xCheckUseCustomBaiduKey.selectedProperty()) + } } } } @@ -471,6 +509,50 @@ class DialogSettings : AbstractPropertiesDialog() { } } + // ----- Quick Input ----- // + private fun initQuickInputTab() { + qGridPane.children.clear() + + val quickInputTextsList = Settings.quickInputTexts + + if (quickInputTextsList.isEmpty()) { + qGridPane.add(qLabelHint, 0, 0) + } else { + for ( text in quickInputTextsList) createQuickInputRow(text) + } + } + private fun createQuickInputRow(text: String = "") { + val newRowIndex = if (qGridPane.rowCount == 0) 1 else qGridPane.rowCount + if (qGridPane.children.size == 1 || qGridPane.rowCount == 0) { // Only hint || nothing + qGridPane.children.clear() + } + val textField = TextField(text).apply { + textFormatter = genGeneralFormatter() + } + val button = Button(I18N["common.delete"]) does { removeQuickInputRow(GridPane.getRowIndex(this)) } + // 0 1 2 + // 1 textField ________ Delete + qGridPane.add(textField, 0, newRowIndex) + qGridPane.add(button, 1, newRowIndex) + } + private fun removeQuickInputRow(index: Int) { + val toRemoveSet = HashSet() + for (node in qGridPane.children) { + val row = GridPane.getRowIndex(node) ?: 0 + if (row == index) toRemoveSet.add(node) + if (row > index) { + GridPane.setRowIndex(node, row - 1) + } + } + qGridPane.children.removeAll(toRemoveSet) + + if (qGridPane.rowCount == qRowShift) { + qGridPane.children.removeAll() + qGridPane.add(qLabelHint, 0, 0) + } + } + + // ----- Initialize Properties ----- // override fun initProperties() { // Group @@ -479,6 +561,9 @@ class DialogSettings : AbstractPropertiesDialog() { // Ligature Rule initLigatureTab() + // quick Input + initQuickInputTab() + // Mode mComboInput.select(Settings.viewModes[0]) mComboLabel.select(Settings.viewModes[1]) @@ -505,6 +590,9 @@ class DialogSettings : AbstractPropertiesDialog() { xCheckUseMeo.isSelected = Settings.useMeoFileAsDefault xCheckUseTmp.isSelected = Settings.useExportNameTemplate xFieldTemplate.text = Settings.exportNameTemplate + xCheckUseCustomBaiduKey.isSelected = Settings.useCustomBaiduKey + xFieldBaiduTranslateKey.text = Settings.baiduTransLateKey + xFieldBaiduTranslateAppId.text = Settings.baiduTransLateAppId } // ----- Result convert ---- // @@ -557,6 +645,22 @@ class DialogSettings : AbstractPropertiesDialog() { return map } + private fun convertQuickInput(): Map { + val map = HashMap() + val size = qGridPane.rowCount - qRowShift + if (size < 0) return emptyMap() + + val textList = MutableList(size) { "" } + for (node in qGridPane.children) { + val groupId = GridPane.getRowIndex(node) - qRowShift + if (groupId < 0) continue + when (node) { + is TextField -> textList[groupId] = node.text + } + } + map[Settings.QuickInputTexts] = textList + return map + } private fun convertMode(): Map { val map = HashMap() @@ -585,6 +689,9 @@ class DialogSettings : AbstractPropertiesDialog() { map[Settings.UseMeoFileAsDefault] = xCheckUseMeo.isSelected map[Settings.UseExportNameTemplate] = xCheckUseTmp.isSelected map[Settings.ExportNameTemplate] = xFieldTemplate.text + map[Settings.UseCustomBaiduKey] = xCheckUseCustomBaiduKey.isSelected + map[Settings.BaiduTransLateKey] = xFieldBaiduTranslateKey.text + map[Settings.BaiduTransLateAppId] = xFieldBaiduTranslateAppId.text return map } @@ -593,6 +700,7 @@ class DialogSettings : AbstractPropertiesDialog() { return HashMap().apply { putAll(convertGroup()) putAll(convertLigatureRule()) + putAll(convertQuickInput()) putAll(convertMode()) putAll(convertLabel()) putAll(convertOther()) diff --git a/src/main/kotlin/ink/meodinger/lpfx/component/tools/FormatChecker.kt b/src/main/kotlin/ink/meodinger/lpfx/component/tools/FormatChecker.kt index 5e4cba0..00ee039 100644 --- a/src/main/kotlin/ink/meodinger/lpfx/component/tools/FormatChecker.kt +++ b/src/main/kotlin/ink/meodinger/lpfx/component/tools/FormatChecker.kt @@ -123,6 +123,7 @@ class FormatChecker(private val state: State) : Stage() { // Select Range state.view.cTransArea.selectRange(match.range.first, match.range.last + 1) }) + closeOnEscape() } diff --git a/src/main/kotlin/ink/meodinger/lpfx/component/tools/OnlineDict.kt b/src/main/kotlin/ink/meodinger/lpfx/component/tools/OnlineDict.kt index 6c5eaf4..f1b41e4 100644 --- a/src/main/kotlin/ink/meodinger/lpfx/component/tools/OnlineDict.kt +++ b/src/main/kotlin/ink/meodinger/lpfx/component/tools/OnlineDict.kt @@ -110,13 +110,23 @@ class OnlineDict : Stage() { transState = TransState.values()[(transState.ordinal + 1) % TransState.values().size] } - if (Config.enableIMEAssistance) addEventHandler(MouseEvent.MOUSE_CLICKED) { - if (it.isDoubleClick && getCurrentLanguage().startsWith(JA)) { - setImeConversionMode( - getCurrentWindow(), - ImeSentenceMode.AUTOMATIC, - ImeConversionMode.JA_HIRAGANA - ) + if (Config.enableIMEAssistance) { + focusedProperty().addListener(onNew { + // We do not set the IMEConversion mode here because switch language need time. + if (it) { + oriLang = getCurrentLanguage() + // Focus gain will take place after the rendering, so it's safe to set by sync. + AvailableLanguages.firstOrNull { lang -> lang.startsWith(JA) }?.apply(::setCurrentLanguage) + } else { + // If set immediately after lose focus will cause focus on other stages fail. + // Use runLater to set language after the rendering. + Platform.runLater { setCurrentLanguage(oriLang) } + } + }) + addEventHandler(MouseEvent.MOUSE_CLICKED) { + if (it.isDoubleClick && getCurrentLanguage().startsWith(JA)) { + setImeConversionMode(getCurrentWindow(), ImeSentenceMode.AUTOMATIC, ImeConversionMode.JA_HIRAGANA) + } } } } @@ -132,18 +142,6 @@ class OnlineDict : Stage() { } }) - if (Config.enableIMEAssistance) focusedProperty().addListener(onNew { - // We do not set the IMEConversion mode here because switch language need time. - if (it) { - oriLang = getCurrentLanguage() - // Focus gain will take place after the rendering, so it's safe to set by sync. - AvailableLanguages.firstOrNull { lang -> lang.startsWith(JA) }?.apply(::setCurrentLanguage) - } else { - // If set immediately after lose focus will cause focus on other stages fail. - // Use runLater to set language after the rendering. - Platform.runLater { setCurrentLanguage(oriLang) } - } - }) closeOnEscape() } diff --git a/src/main/kotlin/ink/meodinger/lpfx/io/Translate.kt b/src/main/kotlin/ink/meodinger/lpfx/io/Translate.kt index 5649d5a..a81ba7e 100644 --- a/src/main/kotlin/ink/meodinger/lpfx/io/Translate.kt +++ b/src/main/kotlin/ink/meodinger/lpfx/io/Translate.kt @@ -1,6 +1,8 @@ package ink.meodinger.lpfx.util.translator import com.fasterxml.jackson.databind.ObjectMapper +import ink.meodinger.lpfx.options.Logger +import ink.meodinger.lpfx.options.Settings import java.io.IOException import java.net.* import java.nio.charset.Charset @@ -31,9 +33,12 @@ private fun md5(text: String): String { } private fun query(q: String, from: String, to: String): String { val salt = floor(Math.random() * 10000) - val sign = md5("$ID$q$salt$KEY").lowercase() + val key = if (Settings.useCustomBaiduKey) Settings.baiduTransLateKey else KEY + val appId = if (Settings.useCustomBaiduKey) Settings.baiduTransLateAppId else ID + val sign = md5("$appId$q$salt$key").lowercase() + Logger.info("TranslateAppId:${Settings.baiduTransLateAppId},TranslateAppId:${Settings.baiduTransLateKey}", "Translate") - return "$ROOT?q=${URLEncoder.encode(q, utf8Charset)}&from=$from&to=$to&appid=$ID&salt=$salt&sign=$sign" + return "$ROOT?q=${URLEncoder.encode(q, utf8Charset)}&from=$from&to=$to&appid=$appId&salt=$salt&sign=$sign" } /** diff --git a/src/main/kotlin/ink/meodinger/lpfx/options/AbstractProperties.kt b/src/main/kotlin/ink/meodinger/lpfx/options/AbstractProperties.kt index 74091fb..004ea87 100644 --- a/src/main/kotlin/ink/meodinger/lpfx/options/AbstractProperties.kt +++ b/src/main/kotlin/ink/meodinger/lpfx/options/AbstractProperties.kt @@ -53,9 +53,9 @@ abstract class AbstractProperties(val name: String, val path: Path) { while (index < lines.size && lines[index].startsWith(CProperty.LIST_SEPARATOR)) { propList.add(lines[index++].substring(1)) } - instance[prop[0]].set(propList) + instance.find(prop[0])?.set(propList) } else { - instance[prop[0]].set(prop[1]) + instance.find(prop[0])?.set(prop[1]) } } } catch { e: IndexOutOfBoundsException -> @@ -105,7 +105,9 @@ abstract class AbstractProperties(val name: String, val path: Path) { properties.addAll(default.map(CProperty::copy)) } - operator fun get(key: String): CProperty = properties.first { it.key == key } + operator fun get(key: String): CProperty = properties.first() { it.key == key } + + fun find(key: String): CProperty? = properties.firstOrNull() { it.key == key } override fun toString(): String = properties.joinToString(", \n", transform = CProperty::toString) diff --git a/src/main/kotlin/ink/meodinger/lpfx/options/Options.kt b/src/main/kotlin/ink/meodinger/lpfx/options/Options.kt index 85f6b45..2953203 100644 --- a/src/main/kotlin/ink/meodinger/lpfx/options/Options.kt +++ b/src/main/kotlin/ink/meodinger/lpfx/options/Options.kt @@ -5,6 +5,7 @@ import ink.meodinger.lpfx.V import ink.meodinger.lpfx.component.dialog.showError import ink.meodinger.lpfx.component.dialog.showException import ink.meodinger.lpfx.get +import ink.meodinger.lpfx.util.Version import ink.meodinger.lpfx.util.doNothing import ink.meodinger.lpfx.util.file.transfer import ink.meodinger.lpfx.util.once @@ -89,7 +90,11 @@ object Options { private fun loadProperties(instance: AbstractProperties) { // Unknown properties will be defaults, so we just need to // make sure the file we will load exists. - if (Files.notExists(instance.path)) Files.createFile(instance.path) + if (Files.notExists(instance.path)) { + val file = Files.createFile(instance.path).toFile() + findOldProperties(instance.path)?.let { transfer(it,file) } + Logger.info("Loaded ${instance.name} in old version to use for the new version ", "Options") + } try { instance.load() @@ -97,10 +102,12 @@ object Options { } catch (e: Throwable) { // Copy invalid properties file to temp, prepare for sending val tempFile = createTempFile().toFile() + Logger.info("tempFile: ${tempFile.path}", "Options") try { transfer(instance.path.toFile(), tempFile) } catch (e: Throwable) { doNothing() + Logger.error("create ${instance.name} properties failed, using default", "Options") } finally { tempFile.deleteOnExit() } @@ -116,6 +123,17 @@ object Options { showException(null, e, tempFile) } } + // find the old Properties to use for the new version + private fun findOldProperties(nowPropsPath :Path) :File?{ + val parentDir = nowPropsPath.parent.parent + if(Files.list(parentDir).count() <=1) return null + val oldPropsPath = Files.list(parentDir) + .toList() + .filter { Files.isDirectory(it) and (it.fileName.toString() != V.toString()) } + .maxWithOrNull(compareBy { Version(it.fileName.toString())}) + + return oldPropsPath?.resolve(nowPropsPath.fileName)?.toFile() + } private fun saveProperties(instance: AbstractProperties) { try { diff --git a/src/main/kotlin/ink/meodinger/lpfx/options/Settings.kt b/src/main/kotlin/ink/meodinger/lpfx/options/Settings.kt index eabd45d..3afc4d1 100644 --- a/src/main/kotlin/ink/meodinger/lpfx/options/Settings.kt +++ b/src/main/kotlin/ink/meodinger/lpfx/options/Settings.kt @@ -37,6 +37,7 @@ object Settings : AbstractProperties("Settings", Options.settings) { const val DefaultGroupColorHexList = "DefaultGroupColorList" const val IsGroupCreateOnNewTrans = "IsGroupCreateOnNew" const val LigatureRules = "LigatureRules" + const val QuickInputTexts = "QuickInputTexts" const val ViewModes = "ViewMode" const val NewPictureScale = "ScaleOnNewPicture" const val UseWheelToScale = "UseWheelToScale" @@ -51,6 +52,9 @@ object Settings : AbstractProperties("Settings", Options.settings) { const val UseExportNameTemplate = "UseExportNameTemplate" const val ExportNameTemplate = "ExportNameTemplate" const val LogLevel = "LogLevel" + const val UseCustomBaiduKey = "UseCustomBaiduKey" + const val BaiduTransLateKey = "BaiduTransLateKey" + const val BaiduTransLateAppId = "BaiduTransLateAppId" // ----- Default ----- // @@ -70,6 +74,19 @@ object Settings : AbstractProperties("Settings", Options.settings) { "cc" to "◎", "*" to "※", ), + CProperty(QuickInputTexts, + "「", + "」", + "『", + "』", + "⭐", + "♢", + "♡", + "♪", + "◎", + "※", + "♥", + ), CProperty(ViewModes, ViewMode.IndexMode.ordinal, ViewMode.GroupMode.ordinal), CProperty(NewPictureScale, CLabelPane.NewPictureScale.DEFAULT.ordinal), CProperty(UseWheelToScale, false), @@ -84,6 +101,9 @@ object Settings : AbstractProperties("Settings", Options.settings) { CProperty(UseExportNameTemplate, false), CProperty(ExportNameTemplate, I18N["settings.other.template.default"]), CProperty(LogLevel, Logger.LogLevel.INFO.ordinal), + CProperty(UseCustomBaiduKey, false), + CProperty(BaiduTransLateKey, "lkjooeJUgW0spOctSbZb"), + CProperty(BaiduTransLateAppId, "20200730000529751"), ) private val defaultGroupNameListProperty: ListProperty = SimpleListProperty() @@ -102,6 +122,11 @@ object Settings : AbstractProperties("Settings", Options.settings) { fun ligatureRulesProperty(): ListProperty> = ligatureRulesProperty var ligatureRules: ObservableList> by ligatureRulesProperty + val quickInputTextsProperty: ListProperty = SimpleListProperty() + fun quickInputTextsProperty(): ListProperty = quickInputTextsProperty + var quickInputTexts: ObservableList by quickInputTextsProperty + + private val viewModesProperty: ListProperty = SimpleListProperty() fun viewModesProperty(): ListProperty = viewModesProperty var viewModes: ObservableList by viewModesProperty @@ -158,6 +183,18 @@ object Settings : AbstractProperties("Settings", Options.settings) { fun logLevelProperty(): ObjectProperty = logLevelProperty var logLevel: Logger.LogLevel by logLevelProperty + private val useCustomBaiduKeyProperty: BooleanProperty = SimpleBooleanProperty() + fun useCustomBaiduKeyProperty(): BooleanProperty = useCustomBaiduKeyProperty + var useCustomBaiduKey: Boolean by useCustomBaiduKeyProperty + + private val baiduTransLateKeyProperty: StringProperty = SimpleStringProperty() + fun baiduTransLateKeyProperty(): StringProperty = baiduTransLateKeyProperty + var baiduTransLateKey: String by baiduTransLateKeyProperty + + private val baiduTransLateAppIdProperty: StringProperty = SimpleStringProperty() + fun baiduTransLateAppIdProperty(): StringProperty = baiduTransLateAppIdProperty + var baiduTransLateAppId: String by baiduTransLateAppIdProperty + init { useDefault() } @@ -169,6 +206,7 @@ object Settings : AbstractProperties("Settings", Options.settings) { defaultGroupColorHexList = FXCollections.observableList(this[DefaultGroupColorHexList].asStringList()) isGroupCreateOnNewTransList = FXCollections.observableList(this[IsGroupCreateOnNewTrans].asBooleanList()) ligatureRules = FXCollections.observableList(this[LigatureRules].asPairList()) + quickInputTexts = FXCollections.observableList(this[QuickInputTexts].asStringList()) viewModes = FXCollections.observableList(this[ViewModes].asIntegerList().map(ViewMode.values()::get)) newPictureScalePicture = CLabelPane.NewPictureScale.values()[this[NewPictureScale].asInteger()] useWheelToScale = this[UseWheelToScale].asBoolean() @@ -183,6 +221,9 @@ object Settings : AbstractProperties("Settings", Options.settings) { useExportNameTemplate = this[UseExportNameTemplate].asBoolean() exportNameTemplate = this[ExportNameTemplate].asString() logLevel = Logger.LogLevel.values()[this[LogLevel].asInteger()] + useCustomBaiduKey = this[UseCustomBaiduKey].asBoolean() + baiduTransLateKey = this[BaiduTransLateKey].asString() + baiduTransLateAppId = this[BaiduTransLateAppId].asString() } @Throws(IOException::class) @@ -191,6 +232,7 @@ object Settings : AbstractProperties("Settings", Options.settings) { this[DefaultGroupColorHexList].set(defaultGroupColorHexList) this[IsGroupCreateOnNewTrans] .set(isGroupCreateOnNewTransList) this[LigatureRules] .set(ligatureRules) + this[QuickInputTexts] .set(quickInputTexts) this[ViewModes] .set(viewModes.map(Enum<*>::ordinal)) this[NewPictureScale] .set(newPictureScalePicture.ordinal) this[UseWheelToScale] .set(useWheelToScale) @@ -205,7 +247,9 @@ object Settings : AbstractProperties("Settings", Options.settings) { this[UseExportNameTemplate] .set(useExportNameTemplate) this[ExportNameTemplate] .set(exportNameTemplate) this[LogLevel] .set(logLevel.ordinal) - + this[UseCustomBaiduKey] .set(useCustomBaiduKey) + this[BaiduTransLateKey] .set(baiduTransLateKey) + this[BaiduTransLateAppId] .set(baiduTransLateAppId) save(this) } diff --git a/src/main/kotlin/ink/meodinger/lpfx/type/TransFile.kt b/src/main/kotlin/ink/meodinger/lpfx/type/TransFile.kt index 5409450..ac24c72 100644 --- a/src/main/kotlin/ink/meodinger/lpfx/type/TransFile.kt +++ b/src/main/kotlin/ink/meodinger/lpfx/type/TransFile.kt @@ -16,7 +16,7 @@ import java.io.File */ /** - * A MEO Translation file + * A Translation file */ @JsonIncludeProperties("version", "comment", "groupList", "transMap") class TransFile @JsonCreator constructor( @@ -91,11 +91,15 @@ class TransFile @JsonCreator constructor( // endregion // region Properties + // Only internal use to avoid accidentally invoking their `set` methods - // Note1: version is immutable - // Note2: all backing list/map/set should be mutable - // Note3: groups' properties' changes will be listened - // Note4: transMap use LinkedHashMap to preserve key order when export. + // Note1: version is immutable. + // Note2: all backing list/map/set should be mutable. + // Note3: groups' properties' changes will be listened. + // Note4: transMap use LinkedHashMap to preserve the key order when exported, + // and for a more general constructor with `Map` instead of `MutableMap`. + // LinkedHashMap doesn't slower than an ordinary HashMap, please see + // https://stackoverflow.com/a/17708526/15969136. private val versionProperty: ReadOnlyListProperty = SimpleListProperty(FXCollections.observableList(version)) private val commentProperty: StringProperty = SimpleStringProperty(comment) @@ -108,12 +112,13 @@ class TransFile @JsonCreator constructor( // region Accessible Fields - // Following properties provide JSON getters + // Following properties provide JSON getters/setters val version: List by versionProperty var comment: String by commentProperty val groupList: List by groupListProperty val transMap: Map> by transMapProperty + // Only use these when you want an ObservableValue or modify properties (except sorted-pic-names) val groupListObservable: ObservableList by groupListProperty val transMapObservable: ObservableMap> by transMapProperty val sortedPicNamesObservable: ObservableList by sortedPicNamesProperty @@ -126,22 +131,47 @@ class TransFile @JsonCreator constructor( init { @Suppress("DEPRECATION") for (labels in transMap.values) for (label in labels) installLabel(label) + @Suppress("DEPRECATION") for (transGroup in groupList) installGroup(transGroup) } // region TransGroup - fun getGroupIdByName(name: String): Int { - return groupList.first { it.name == name }.let(groupList::indexOf) - } - fun isGroupStillInUse(groupId: Int): Boolean { + /** + * Whether a TransGroup is still in use + * @param groupName The target TransGroup's name + */ + fun isGroupStillInUse(groupName: String): Boolean { + val groupId = groupList.indexOfFirst { it.name == groupName } return transMap.values.flatten().any { label -> label.groupId == groupId } } + /** + * Install the color-property of TransLabel based on this TransFile. + * This should only be called within an Action. + */ + @Deprecated(level = DeprecationLevel.WARNING, message = "Only in Action") + fun installGroup(transGroup: TransGroup) { + @Suppress("DEPRECATION") + TransGroup.installIndex(transGroup, groupListProperty.observableIndexOf(transGroup)) + } + /** + * Dispose the color-property of TransLabel. + * This should only be called within an Action. + */ + @Suppress("DeprecatedCallableAddReplaceWith") + @Deprecated(level = DeprecationLevel.WARNING, message = "Only in Action") + fun disposeGroup(transGroup: TransGroup) { + @Suppress("DEPRECATION") + TransGroup.disposeIndex(transGroup) + } + // endregion // region TransLabel + /** - * Install the color-property of TransLabel based on this TransFile + * Install the index-property of TransGroup based on this TransFile. + * This should only be called within an Action. */ @Deprecated(level = DeprecationLevel.WARNING, message = "Only in Action") fun installLabel(transLabel: TransLabel) { @@ -149,8 +179,10 @@ class TransFile @JsonCreator constructor( TransLabel.installColor(transLabel, groupListProperty.valueAt(transLabel.groupIdProperty()).transform(TransGroup::color)) } /** - * Dispose the color-property of TransLabel + * Dispose the index-property of TransGroup. + * This should only be called within an Action. */ + @Suppress("DeprecatedCallableAddReplaceWith") @Deprecated(level = DeprecationLevel.WARNING, message = "Only in Action") fun disposeLabel(transLabel: TransLabel) { @Suppress("DEPRECATION") @@ -161,8 +193,8 @@ class TransFile @JsonCreator constructor( // region Getters - fun getTransGroup(groupId: Int): TransGroup { - return groupListObservable[groupId] + fun getTransGroup(groupName: String): TransGroup { + return groupListObservable.first { it.name == groupName } } fun getTransList(picName: String): List { return transMapObservable[picName]!! diff --git a/src/main/kotlin/ink/meodinger/lpfx/type/TransGroup.kt b/src/main/kotlin/ink/meodinger/lpfx/type/TransGroup.kt index 86fd827..babe24f 100644 --- a/src/main/kotlin/ink/meodinger/lpfx/type/TransGroup.kt +++ b/src/main/kotlin/ink/meodinger/lpfx/type/TransGroup.kt @@ -1,6 +1,7 @@ package ink.meodinger.lpfx.type import ink.meodinger.lpfx.I18N +import ink.meodinger.lpfx.NOT_FOUND import ink.meodinger.lpfx.get import ink.meodinger.lpfx.util.color.isColorHex import ink.meodinger.lpfx.util.property.getValue @@ -8,6 +9,7 @@ import ink.meodinger.lpfx.util.property.readonly import ink.meodinger.lpfx.util.property.transform import com.fasterxml.jackson.annotation.* +import javafx.beans.binding.IntegerExpression import javafx.beans.property.* import javafx.scene.paint.Color @@ -28,6 +30,23 @@ class TransGroup @JsonCreator constructor( ) { companion object { private var ACC = 0 + + /** + * Bind TransGroup's index-property, This function should only be called in TransFile + */ + @Deprecated(level = DeprecationLevel.WARNING, message = "Only in TransFile") + fun installIndex(transGroup: TransGroup, property: IntegerExpression) { + transGroup.indexProperty.bind(property) + } + + /** + * Unbind TransGroup's index-property, This function should only be called in TransFile + */ + @Deprecated(level = DeprecationLevel.WARNING, message = "Only in TransFile") + fun disposeIndex(transGroup: TransGroup) { + transGroup.indexProperty.unbind() + } + } // region Properties @@ -66,6 +85,17 @@ class TransGroup @JsonCreator constructor( */ val color: Color by colorProperty + private val indexProperty: IntegerProperty = SimpleIntegerProperty(NOT_FOUND) + /** + * This property represents the group-id of this TransGroup + * if this TransGroup was installed by a TransFile, or NOT_FOUND. + */ + fun indexProperty(): ReadOnlyIntegerProperty = indexProperty + /** + * @see indexProperty + */ + val index: Int by indexProperty + // endregion override fun toString(): String = "TransGroup(name=$name, color=$colorHex)" diff --git a/src/main/kotlin/ink/meodinger/lpfx/type/TransLabel.kt b/src/main/kotlin/ink/meodinger/lpfx/type/TransLabel.kt index 0a9d137..874e567 100644 --- a/src/main/kotlin/ink/meodinger/lpfx/type/TransLabel.kt +++ b/src/main/kotlin/ink/meodinger/lpfx/type/TransLabel.kt @@ -33,7 +33,7 @@ class TransLabel @JsonCreator constructor( companion object { /** - * Bind TransLabel's ColorProperty, This function should only be called in TransFile + * Bind TransLabel's color-property, This function should only be called within a TransFile. */ @Deprecated(level = DeprecationLevel.WARNING, message = "Only in TransFile") fun installColor(transLabel: TransLabel, property: ObjectExpression) { @@ -41,7 +41,7 @@ class TransLabel @JsonCreator constructor( } /** - * Unbind TransLabel's ColorProperty, This function should only be called in TransFile + * Unbind TransLabel's color-property, This function should only be called within a TransFile. */ @Deprecated(level = DeprecationLevel.WARNING, message = "Only in TransFile") fun disposeColor(transLabel: TransLabel) { @@ -77,7 +77,7 @@ class TransLabel @JsonCreator constructor( var x: Double get() = xProperty.get() set(value) { - if (value < 0 || value > 1) + if (!value.isNaN() && (value < 0 || value > 1)) throw IllegalArgumentException(String.format(I18N["exception.trans_label.x_invalid.d"], value)) xProperty.set(value) } @@ -87,7 +87,7 @@ class TransLabel @JsonCreator constructor( var y: Double get() = yProperty.get() set(value) { - if (value < 0 || value > 1) + if (!value.isNaN() && (value < 0 || value > 1)) throw IllegalArgumentException(String.format(I18N["exception.trans_label.y_invalid.d"], value)) yProperty.set(value) } diff --git a/src/main/kotlin/ink/meodinger/lpfx/util/CUtil.kt b/src/main/kotlin/ink/meodinger/lpfx/util/CUtil.kt index 3945852..f63a990 100644 --- a/src/main/kotlin/ink/meodinger/lpfx/util/CUtil.kt +++ b/src/main/kotlin/ink/meodinger/lpfx/util/CUtil.kt @@ -42,8 +42,27 @@ data class Version(val a: Int, val b: Int, val c: Int): Comparable { return Version(l[0].substring(1).toInt(), l[1].toInt(), l[2].toInt()) } + /** + * Construct a Version from a String in the format "vX.Y.Z" + */ + operator fun invoke(version: String): Version { + val trimmedVersion = version.trim() + if (!trimmedVersion.startsWith("v")) return V0 + val parts = trimmedVersion.substring(1).split(".") + + if (parts.size != 3) return V0 // 确保有三部分 + val (major, minor, patch) = parts.map { it.toIntOrNull() } // 转换为整数 + + return if (major != null && minor != null && patch != null) { + Version(major, minor, patch) + } else { + V0 // 如果转换失败,返回 V0 + } + } } + + init { check(a) check(b) @@ -53,9 +72,14 @@ data class Version(val a: Int, val b: Int, val c: Int): Comparable { override fun toString(): String = "v$a.$b.$c" override operator fun compareTo(other: Version): Int { + return this - other + } + + operator fun minus(other: Version): Int { return (this.a - other.a) * 10000 + (this.b - other.b) * 100 + (this.c - other.c) } + } /** diff --git a/src/main/kotlin/ink/meodinger/lpfx/util/property/Extension.kt b/src/main/kotlin/ink/meodinger/lpfx/util/property/Extension.kt index 7897dea..0e339a2 100644 --- a/src/main/kotlin/ink/meodinger/lpfx/util/property/Extension.kt +++ b/src/main/kotlin/ink/meodinger/lpfx/util/property/Extension.kt @@ -1,5 +1,7 @@ package ink.meodinger.lpfx.util.property +import javafx.beans.binding.Bindings +import javafx.beans.binding.IntegerBinding import javafx.collections.* @@ -11,8 +13,8 @@ import javafx.collections.* /** - * Return a ObservableSet that reflect the keys and their changes of the ObservableMap - * @return An unmodifiable ObservableSet + * Return a ObservableSet that reflect the keys and their changes of the ObservableMap. + * @return An ObservableSet that **MUTABLE** but you should better **NOT** change it. */ fun ObservableMap.observableKeySet(): ObservableSet { val set = FXCollections.observableSet(HashSet(keys)) @@ -30,28 +32,44 @@ fun ObservableMap.observableKeySet(): ObservableSet { } }) - return FXCollections.unmodifiableObservableSet(set) + // It's a compromise with the WeakListener attached to UnmodifiableSet which often offline. + return set } /** * Return a ObservableList that reflect the sorted elements and their changes of the ObservableSet - * @return An unmodifiable ObservableList + * @return An ObservableList that **MUTABLE** but you should better **NOT** change it. */ fun ObservableSet.observableSorted(sorter: (Set) -> List): ObservableList { val list = FXCollections.observableArrayList(sorter(this)) - addListener(SetChangeListener { list.setAll(sorter(it.set)) }) + addListener(SetChangeListener { + // Sort the set whenever it changes + list.setAll(sorter(it.set)) + }) - return FXCollections.unmodifiableObservableList(list) + // It's a compromise with the WeakListener attached to UnmodifiableList which often offline. + return list } /** * Return a ObservableList that reflect the sorted elements and their changes of the ObservableList - * @return An unmodifiable ObservableList + * @return An ObservableList that **MUTABLE** but you should better **NOT** change it. */ fun ObservableList.observableSorted(sorter: (List) -> List): ObservableList { val list = FXCollections.observableArrayList(sorter(this)) - addListener(ListChangeListener { list.setAll(sorter(it.list)) }) + addListener(ListChangeListener { + // Sort the list whenever it changes + list.setAll(sorter(it.list)) + }) + + // It's a compromise with the WeakListener attached to UnmodifiableList which often offline. + return list +} - return FXCollections.unmodifiableObservableList(list) +/** + * Return a IntegerBinding that represents the index in the list of the given element + */ +fun ObservableList.observableIndexOf(element: E): IntegerBinding { + return Bindings.createIntegerBinding({ indexOf(element) }, this) } diff --git a/src/main/kotlin/ink/meodinger/lpfx/util/string/Number.kt b/src/main/kotlin/ink/meodinger/lpfx/util/string/Number.kt index e5bae50..4ca2995 100644 --- a/src/main/kotlin/ink/meodinger/lpfx/util/string/Number.kt +++ b/src/main/kotlin/ink/meodinger/lpfx/util/string/Number.kt @@ -11,6 +11,8 @@ package ink.meodinger.lpfx.util.string * Is the String a mathematical natural number */ fun String.isMathematicalNatural(): Boolean { + if (isEmpty()) return false + val iterator = this.toCharArray().iterator() while (iterator.hasNext()) if (iterator.nextChar() !in '0'..'9') @@ -23,9 +25,10 @@ fun String.isMathematicalNatural(): Boolean { * Is the String a mathematical integer */ fun String.isMathematicalInteger(): Boolean { + if (isEmpty()) return false + val chars = this.toCharArray() val isNegative = chars[0] == '-' - return if (isNegative) substring(1).isMathematicalNatural() else isMathematicalNatural() } @@ -33,8 +36,9 @@ fun String.isMathematicalInteger(): Boolean { * Is the String a mathematical decimal */ fun String.isMathematicalDecimal(): Boolean { - val chars = this.toCharArray() + if (isEmpty()) return false + val chars = this.toCharArray() var index = 0 val isNegative = chars[index] == '-' if (isNegative) index++ diff --git a/src/main/resources/ink/meodinger/lpfx/LabelPlusFX.properties b/src/main/resources/ink/meodinger/lpfx/LabelPlusFX.properties index acbf2f0..84923a7 100644 --- a/src/main/resources/ink/meodinger/lpfx/LabelPlusFX.properties +++ b/src/main/resources/ink/meodinger/lpfx/LabelPlusFX.properties @@ -1,6 +1,6 @@ # application info application.name = LabelPlus FX -application.vendor = Meodinger (meodinger@qq.com) +application.vendor = Meodinger (meodinger@qq.com)、Yeding(kanaumachi@gmail.com) application.link = LabelPlusFX on GitHub application.url = https://github.com/Meodinger/LabelPlusFX application.help = https://www.kdocs.cn/l/seRSJCKVOn0Y \ No newline at end of file diff --git a/src/main/resources/ink/meodinger/lpfx/Lang_en.properties b/src/main/resources/ink/meodinger/lpfx/Lang_en.properties index 1762517..c92f8b0 100644 --- a/src/main/resources/ink/meodinger/lpfx/Lang_en.properties +++ b/src/main/resources/ink/meodinger/lpfx/Lang_en.properties @@ -81,8 +81,23 @@ context.move_to = Move To context.move_to.dialog.title = Move To Other Group context.move_to.dialog.header = Please choose the group you want to move the label to context.move_to.dialog.header.pl = Please choose the group you want to move these labels to +context.move_to_index = Move To Other Index +context.move_to_index.dialog.title = Please choose the Index you want to move the label to +context.move_to_index.dialog.header =Please choose the Index you want to move the label to +context.copy_label_text = Copy Text +context.paste_label_text = Paste Text context.delete_label = Delete +## input +input.menu.undo=Undo +input.menu.redo=Redo +input.menu.cut=Cut +input.menu.copy=Copy +input.menu.paste=Paste +input.menu.select_all=Select All +input.menu.quick_input=Quick Input + + # mode mode.work.label = Label Mode mode.work.input = Input Mode @@ -225,6 +240,11 @@ settings.ligature.from = From settings.ligature.to = To settings.ligature.add = Add Rule settings.ligature.sample = For example: From `star` to `⭐`, Input `\\star` will auto transform to `⭐` +## quick input +settings.quick_input.title = Quick Input +settings.quick_input.hint = No Phrase, you can create one +settings.quick_input.add = Add Phrase +settings.quick_input.sample = Right-click the text area,and you can use the quick input ## mode settings.mode.title = Mode settings.mode.scale.label = Scale on new picture @@ -245,6 +265,9 @@ settings.other.meo_default = Use MeoFile as default save format settings.other.template.enable = Use template when export translation settings.other.template.hint = Available variables: %FILE% Filename, %DIR% Dirname, %PROJECT% Project name\nCannot contain chars of * : ? < > | / " \\ settings.other.template.default = %FILE% Translator:XXX +settings.other.Translate_keys.enable = User your keys of baidu translate +settings.other.Translate_keys.key = Key +settings.other.Translate_keys.app_id = APP ID # logs logs.title = Logs diff --git a/src/main/resources/ink/meodinger/lpfx/Lang_zh_CN.properties b/src/main/resources/ink/meodinger/lpfx/Lang_zh_CN.properties index 911efa1..a9627bb 100644 --- a/src/main/resources/ink/meodinger/lpfx/Lang_zh_CN.properties +++ b/src/main/resources/ink/meodinger/lpfx/Lang_zh_CN.properties @@ -63,7 +63,6 @@ m.externalPic.dialog.header = 以下文件名有冲突!将不会被添加到 stats.not_backed = 未进行过备份 stats.last_backup.s = 上次备份于:%s stats.accumulator.s = 累计编辑时间:%s - # context menu context.error.same_group_name = 已经有相同名称分组 ## root @@ -77,12 +76,28 @@ context.rename_group.dialog.title = 重命名分组 context.rename_group.dialog.header = 请输入新的分组名称 context.delete_group = 删除 ## label(s) -context.move_to = 移动到… +context.move_to = 移动分组 context.move_to.dialog.title = 移动到其他分组 context.move_to.dialog.header = 请选择目标分组 context.move_to.dialog.header.pl = 请选择目标分组 +context.move_to_index = 移动序号 +context.move_to_index.dialog.title = 移动到其它序号 +context.move_to_index.dialog.header = 请选择目标序号 +context.copy_label_text = 复制文本 +context.paste_label_text = 粘贴文本 context.delete_label = 删除 +## input +input.menu.undo=撤销 +input.menu.redo=重做 +input.menu.cut=剪切 +input.menu.copy=复制 +input.menu.paste=粘贴 +input.menu.delete_selection=删除 +input.menu.select_all=全选 +input.menu.quick_input=快捷输入 + + # mode mode.work.input = 输入模式 mode.work.label = 标号模式 @@ -217,6 +232,11 @@ settings.ligature.from = 从 settings.ligature.to = 到 settings.ligature.add = 添加规则 settings.ligature.sample = 例如:从 * 到 ※ ,输入 \\* 会自动变成 ※ +## quick input +settings.quick_input.title = 快捷输入 +settings.quick_input.hint = 没有短语,你可以创建一个 +settings.quick_input.add = 添加短语 +settings.quick_input.sample = 编辑时右键编辑框便可使用快捷输入 ## mode settings.mode.title = 模式 settings.mode.scale.label = 切换到新图片时的缩放比例 @@ -237,6 +257,9 @@ settings.other.meo_default = 使用喵版翻译文件作为默认保存格式 settings.other.template.enable = 导出翻译文件时使用预设模板 settings.other.template.hint = 可用的变量:%FILE% 文件名称,%DIR% 目录名称,%PROJECT% 项目目录名称\n不能含有 * : ? < > | / " \\ settings.other.template.default = %FILE% 翻译:XXX +settings.other.Translate_keys.enable = 使用自定义的百度翻译密钥(用于繁简体转换) +settings.other.Translate_keys.key = 密钥 +settings.other.Translate_keys.app_id = APP ID # logs logs.title = Logs diff --git a/src/main/resources/ink/meodinger/lpfx/Lang_zh_TW.properties b/src/main/resources/ink/meodinger/lpfx/Lang_zh_TW.properties index 7914a57..e590ff9 100644 --- a/src/main/resources/ink/meodinger/lpfx/Lang_zh_TW.properties +++ b/src/main/resources/ink/meodinger/lpfx/Lang_zh_TW.properties @@ -81,8 +81,23 @@ context.move_to = 移動到… context.move_to.dialog.title = 移動到其他分組 context.move_to.dialog.header = 請選擇目標分組 context.move_to.dialog.header.pl = 請選擇目標分組 +context.move_to_index = 移動序號 +context.move_to_index.dialog.title = 移動到其他序號 +context.move_to_index.dialog.header = 请选择目标序號 +context.copy_label_text = 複製文本 +context.paste_label_text = 粘貼文本 context.delete_label = 删除 +## input +input.menu.undo=還原 +input.menu.redo=重做 +input.menu.cut=剪下 +input.menu.copy=複製 +input.menu.paste=貼上 +input.menu.delete_selection=刪除 +input.menu.select_all=全選 +input.menu.quick_input=快捷输入 + # mode mode.work.input = 輸入模式 mode.work.label = 標號模式 @@ -217,6 +232,11 @@ settings.ligature.from = 從 settings.ligature.to = 到 settings.ligature.add = 添加規則 settings.ligature.sample = 例如:從 * 到 ※ ,輸入 \\* 會自動變成 ※ +## quick input +settings.quick_input.title = 快捷輸入 +settings.quick_input.hint = 沒有短語,你可以創建一個 +settings.quick_input.add = 添加短語 +settings.quick_input.sample = 編輯時右鍵編輯框便可使用快捷輸入 ## mode settings.mode.title = 模式 settings.mode.scale.label = 切換到新圖片時的縮放比例 @@ -237,6 +257,9 @@ settings.other.meo_default = 使用喵版翻譯檔案作為默認保存格式 settings.other.template.enable = 匯出翻譯檔案時使用預設範本 settings.other.template.hint = 可用的變數:%FILE% 檔案名稱,%DIR% 目錄名稱,%PROJECT% 項目目錄名稱\n不能含有 * : ? < > | / " \\ settings.other.template.default = %FILE% 翻譯:XXX +settings.other.Translate_keys.enable = 使用自定義的百度翻譯密鑰(用於繁簡體轉換) +settings.other.Translate_keys.key = 密鑰 +settings.other.Translate_keys.app_id = APP ID # logs logs.title = Logs diff --git a/src/test/kotlin/ink/meodinger/lpfx/util/string/CStringKtTest.kt b/src/test/kotlin/ink/meodinger/lpfx/util/string/CStringKtTest.kt index ad6ffa7..efb28a6 100644 --- a/src/test/kotlin/ink/meodinger/lpfx/util/string/CStringKtTest.kt +++ b/src/test/kotlin/ink/meodinger/lpfx/util/string/CStringKtTest.kt @@ -44,6 +44,8 @@ class CStringKtTest { @Test fun isMathNaturalTest() { + assertFalse("".isMathematicalNatural()) + assertTrue("123".isMathematicalNatural()) assertFalse("-123".isMathematicalNatural()) @@ -70,6 +72,7 @@ class CStringKtTest { @Test fun isMathIntegerTest() { + assertFalse("".isMathematicalInteger()) assertTrue("123".isMathematicalInteger()) assertTrue("-123".isMathematicalInteger()) @@ -96,6 +99,7 @@ class CStringKtTest { @Test fun isMathDecimalTest() { + assertFalse("".isMathematicalDecimal()) assertTrue("123".isMathematicalDecimal()) assertTrue("-123".isMathematicalDecimal())