diff --git a/common/src/main/java/me/chrr/scribble/ScribbleConfig.java b/common/src/main/java/me/chrr/scribble/ScribbleConfig.java index 45017ff..8d77414 100644 --- a/common/src/main/java/me/chrr/scribble/ScribbleConfig.java +++ b/common/src/main/java/me/chrr/scribble/ScribbleConfig.java @@ -25,6 +25,8 @@ public class ScribbleConfig extends ReflectedConfig { public Value copyFormattingCodes = value(true); public Value editHistorySize = value(32) .range(8, 128, 1); + public Value overflowWhenTyping = value(false); + public Value pasteBehavior = value(PasteBehavior.DENY); @Category("miscellaneous") public Value openVanillaBookScreenOnShift = value(false); @@ -43,4 +45,10 @@ public enum ShowActionButtons { WHEN_EDITING, NEVER, } + + public enum PasteBehavior { + DENY, + FIT_PAGE, + OVERFLOW, + } } diff --git a/common/src/main/java/me/chrr/scribble/book/RichText.java b/common/src/main/java/me/chrr/scribble/book/RichText.java index 7990207..a2e8666 100644 --- a/common/src/main/java/me/chrr/scribble/book/RichText.java +++ b/common/src/main/java/me/chrr/scribble/book/RichText.java @@ -508,6 +508,19 @@ public String getAsFormattedString() { return out.toString(); } + /** + * Remove carriage return characters (\r) from the text. This is needed because + * Windows line endings (CRLF) contain \r characters that Minecraft books display + * as visible symbols instead of treating them as whitespace. + * + * @return a new RichText with all carriage returns removed. + */ + public RichText filterCarriageReturns() { + String plain = this.getPlainText(); + if (!plain.contains("\r")) return this; + return RichText.fromFormattedString(this.getAsFormattedString().replace("\r", "")); + } + /** * Get the rich text as a vanilla {@link MutableComponent}. Note that this text content is valid for * this client only! diff --git a/common/src/main/java/me/chrr/scribble/book/TextOverflowHandler.java b/common/src/main/java/me/chrr/scribble/book/TextOverflowHandler.java new file mode 100644 index 0000000..d46c34d --- /dev/null +++ b/common/src/main/java/me/chrr/scribble/book/TextOverflowHandler.java @@ -0,0 +1,89 @@ +package me.chrr.scribble.book; + +import me.chrr.scribble.history.HistoryListener; +import me.chrr.scribble.history.command.OverflowCommand; +import net.minecraft.ChatFormatting; +import net.minecraft.client.gui.Font; +import net.minecraft.network.chat.Style; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.function.Consumer; + +@NullMarked +public class TextOverflowHandler { + private static final int PAGE_WIDTH = 114; + private static final int LINE_LIMIT = 14; + + private final HistoryListener listener; + private final Font font; + private final Consumer onCommand; + + public TextOverflowHandler(HistoryListener listener, Font font, Consumer onCommand) { + this.listener = listener; + this.font = font; + this.onCommand = onCommand; + } + + public boolean insertWithOverflow(int page, RichText text, int cursor, RichText insert, + @Nullable ChatFormatting color, Set modifiers) { + int maxPages = 100 - listener.getTotalPages() + 1; + if (cursor != text.getLength() || maxPages < 1) return false; + + // Truncate insert to max pages × ~400 chars/page (conservative estimate) + insert = insert.subText(0, Math.min(insert.getLength(), maxPages * 400)); + + // Combine text + insert first, then split with word-wrapping + RichText combined = text.insert(cursor, insert); + + List pages = new ArrayList<>(); + RichText remaining = combined; + for (; remaining.getLength() > 0 && pages.size() < maxPages; ) { + int len = findFittingLength(remaining); + pages.add(remaining.subText(0, len)); + remaining = remaining.subText(len, remaining.getLength()); + } + + // Reject only if no overflow pages were created (at page limit with no room) + // Allow truncation when overflow pages were successfully added + if (remaining.getLength() > 0 && pages.size() == 1) { + return false; + } + + OverflowCommand cmd = new OverflowCommand(page, listener.getPageContent(page), pages); + cmd.execute(listener); + onCommand.accept(cmd); + return true; + } + + private int findFittingLength(RichText text) { + if (!wouldOverflow(text)) return text.getLength(); + + // Binary search for max chars that fit on one page + int lo = 1, hi = text.getLength(), best = 1; + while (lo <= hi) { + int mid = (lo + hi) / 2; + if (!wouldOverflow(text.subText(0, mid))) { best = mid; lo = mid + 1; } + else hi = mid - 1; + } + + // Find last space in the fitted text for word wrapping + String plain = text.subText(0, best).getPlainText(); + int lastNewline = plain.lastIndexOf('\n'); + int searchStart = Math.max(0, lastNewline); // Only look for space after last newline + int lastSpace = plain.lastIndexOf(' ', best - 1); + + // Only use space if it's on the last visual line (after last newline) + if (lastSpace > searchStart) { + return lastSpace + 1; + } + return best; + } + + private boolean wouldOverflow(RichText text) { + return font.getSplitter().splitLines(text, PAGE_WIDTH, Style.EMPTY).size() > LINE_LIMIT; + } +} diff --git a/common/src/main/java/me/chrr/scribble/gui/edit/OverflowHandler.java b/common/src/main/java/me/chrr/scribble/gui/edit/OverflowHandler.java new file mode 100644 index 0000000..0575ff3 --- /dev/null +++ b/common/src/main/java/me/chrr/scribble/gui/edit/OverflowHandler.java @@ -0,0 +1,42 @@ +package me.chrr.scribble.gui.edit; + +import me.chrr.scribble.book.RichText; +import net.minecraft.ChatFormatting; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +import java.util.Set; + +/** + * Interface for handling text overflow situations in the book editor. + * This is called when text operations would cause the page to overflow. + */ +@NullMarked +public interface OverflowHandler { + /** + * Called when typing or pasting would overflow the current page. + * + * @param currentText the current text on the page + * @param cursor the cursor position + * @param insert the text to insert + * @param color the current color + * @param modifiers the current modifiers + * @return true if the overflow was handled, false if the operation should be rejected + */ + boolean handleOverflow(RichText currentText, int cursor, RichText insert, + @Nullable ChatFormatting color, Set modifiers); + + /** + * Called when Enter is pressed at the end of a page. + * + * @return true if a new page was created + */ + boolean handleEnterAtEnd(); + + /** + * Called when Backspace is pressed on a completely empty page. + * + * @return true if the page was deleted + */ + boolean handleBackspaceOnEmpty(); +} diff --git a/common/src/main/java/me/chrr/scribble/gui/edit/RichEditBox.java b/common/src/main/java/me/chrr/scribble/gui/edit/RichEditBox.java index 277f5e8..ade3e9f 100644 --- a/common/src/main/java/me/chrr/scribble/gui/edit/RichEditBox.java +++ b/common/src/main/java/me/chrr/scribble/gui/edit/RichEditBox.java @@ -2,11 +2,14 @@ import com.mojang.blaze3d.platform.cursor.CursorTypes; import com.mojang.datafixers.util.Pair; +import me.chrr.scribble.Scribble; +import me.chrr.scribble.ScribbleConfig; import me.chrr.scribble.book.RichText; import me.chrr.scribble.gui.TextArea; import me.chrr.scribble.history.command.Command; import me.chrr.scribble.history.command.EditCommand; import net.minecraft.ChatFormatting; +import net.minecraft.client.Minecraft; import net.minecraft.client.gui.Font; import net.minecraft.client.gui.GuiGraphicsExtractor; import net.minecraft.client.gui.components.MultiLineEditBox; @@ -32,6 +35,7 @@ public class RichEditBox extends MultiLineEditBox implements TextArea { private final @Nullable Runnable onInvalidateFormat; private final @Nullable Consumer onHistoryPush; + private final @Nullable OverflowHandler overflowHandler; public @Nullable ChatFormatting color = ChatFormatting.BLACK; public Set modifiers = new HashSet<>(); @@ -39,11 +43,13 @@ public class RichEditBox extends MultiLineEditBox implements TextArea private RichEditBox(Font font, int x, int y, int width, int height, Component placeholder, Component message, int textColor, boolean textShadow, int cursorColor, boolean hasBackground, boolean hasOverlay, - @Nullable Runnable onInvalidateFormat, @Nullable Consumer onHistoryPush) { + @Nullable Runnable onInvalidateFormat, @Nullable Consumer onHistoryPush, + @Nullable OverflowHandler overflowHandler) { super(font, x, y, width, height, placeholder, message, textColor, textShadow, cursorColor, hasBackground, hasOverlay); this.onInvalidateFormat = onInvalidateFormat; this.onHistoryPush = onHistoryPush; + this.overflowHandler = overflowHandler; this.textField = new RichMultiLineTextField( font, width - this.totalInnerPadding(), @@ -197,6 +203,30 @@ protected void extractContents(GuiGraphicsExtractor graphics, int mouseX, int mo @Override public boolean charTyped(CharacterEvent event) { if (this.visible && this.isFocused() && event.isAllowedChatCharacter()) { + RichMultiLineTextField tf = this.getRichTextField(); + RichText currentText = tf.getRichText(); + int cursor = tf.cursor; + + // Create the text to insert + RichText insert = new RichText(event.codepointAsString(), + Optional.ofNullable(color).orElse(ChatFormatting.BLACK), modifiers); + + // Check if this would overflow + RichText result = tf.hasSelection() + ? currentText.replace(tf.getSelected().beginIndex(), tf.getSelected().endIndex(), insert) + : currentText.insert(cursor, insert); + + boolean wouldOverflow = tf.hasLineLimit() && tf.font.getSplitter() + .splitLines(result, tf.width, net.minecraft.network.chat.Style.EMPTY).size() > tf.lineLimit; + + // If would overflow and we have an overflow handler, try to handle it + if (wouldOverflow && this.overflowHandler != null && cursor == currentText.getLength() && !tf.hasSelection()) { + if (this.overflowHandler.handleOverflow(currentText, cursor, insert, color, modifiers)) { + return true; + } + } + + // Normal behavior EditCommand command = new EditCommand(this, (textField) -> textField.insertText(event.codepointAsString())); command.executeEdit(this.getRichTextField()); @@ -226,17 +256,84 @@ public boolean keyPressed(KeyEvent event) { } } - // Wrap the operation with an edit command if it edits the text. - if (event.isCut() || event.isPaste() || - List.of(GLFW.GLFW_KEY_ENTER, GLFW.GLFW_KEY_KP_ENTER, - GLFW.GLFW_KEY_BACKSPACE, GLFW.GLFW_KEY_DELETE).contains(event.key())) { - EditCommand command = new EditCommand(this, - (textField) -> textField.keyPressed(event)); - command.executeEdit(this.getRichTextField()); + RichMultiLineTextField tf = this.getRichTextField(); + RichText currentText = tf.getRichText(); + int cursor = tf.cursor; + boolean isAtEndOfText = cursor == currentText.getLength() && !tf.hasSelection(); + + // Handle Enter at end of full page - create new page if overflow handler exists + if ((event.key() == GLFW.GLFW_KEY_ENTER || event.key() == GLFW.GLFW_KEY_KP_ENTER) && isAtEndOfText) { + boolean pageFull = tf.hasLineLimit() && tf.font.getSplitter() + .splitLines(currentText, tf.width, net.minecraft.network.chat.Style.EMPTY).size() >= tf.lineLimit; + if (pageFull) { + if (this.overflowHandler != null && this.overflowHandler.handleEnterAtEnd()) { + return true; + } + return true; // Block enter on full page if overflow disabled + } + } + + // Handle Backspace on empty page - delete page if overflow handler exists + if (event.key() == GLFW.GLFW_KEY_BACKSPACE && currentText.isEmpty() && !tf.hasSelection()) { + if (this.overflowHandler != null && this.overflowHandler.handleBackspaceOnEmpty()) { + return true; + } + } + + // Handle Paste with different behaviors based on config + if (event.isPaste()) { + String clipboardText = Minecraft.getInstance().keyboardHandler.getClipboard().replace("\r", ""); + boolean keepFormatting = Scribble.CONFIG.copyFormattingCodes.get() ^ event.hasShiftDown(); + if (!keepFormatting) clipboardText = ChatFormatting.stripFormatting(clipboardText); + + RichText insert = ChatFormatting.stripFormatting(clipboardText).equals(clipboardText) + ? new RichText(clipboardText, Optional.ofNullable(color).orElse(ChatFormatting.BLACK), modifiers) + : RichText.fromFormattedString(clipboardText); + + int start = tf.hasSelection() ? tf.getSelected().beginIndex() : cursor; + int end = tf.hasSelection() ? tf.getSelected().endIndex() : cursor; + RichText result = tf.hasSelection() ? currentText.replace(start, end, insert) : currentText.insert(cursor, insert); + boolean wouldOverflow = tf.hasLineLimit() && tf.font.getSplitter() + .splitLines(result, tf.width, net.minecraft.network.chat.Style.EMPTY).size() > tf.lineLimit; + + if (wouldOverflow) { + ScribbleConfig.PasteBehavior behavior = Scribble.CONFIG.pasteBehavior.get(); + if (behavior == ScribbleConfig.PasteBehavior.FIT_PAGE) { + int lo = 0, hi = insert.getLength(), best = 0; + while (lo <= hi) { + int mid = (lo + hi) / 2; + RichText partial = insert.subText(0, mid); + RichText partialResult = tf.hasSelection() ? currentText.replace(start, end, partial) : currentText.insert(cursor, partial); + if (tf.font.getSplitter().splitLines(partialResult, tf.width, net.minecraft.network.chat.Style.EMPTY).size() <= tf.lineLimit) { + best = mid; lo = mid + 1; + } else hi = mid - 1; + } + if (best > 0) { + String truncated = insert.subText(0, best).getAsFormattedString(); + EditCommand cmd = new EditCommand(this, t -> t.insertText(truncated)); + cmd.executeEdit(tf); + this.pushHistory(cmd); + } + return true; + } else if (behavior == ScribbleConfig.PasteBehavior.OVERFLOW && this.overflowHandler != null && isAtEndOfText) { + if (this.overflowHandler.handleOverflow(currentText, cursor, insert, color, modifiers)) return true; + } + } + + EditCommand command = new EditCommand(this, t -> t.keyPressed(event)); + command.executeEdit(tf); this.pushHistory(command); return true; } + // Wrap the operation with an edit command if it edits the text. + if (event.isCut() || List.of(GLFW.GLFW_KEY_ENTER, GLFW.GLFW_KEY_KP_ENTER, + GLFW.GLFW_KEY_BACKSPACE, GLFW.GLFW_KEY_DELETE).contains(event.key())) { + EditCommand command = new EditCommand(this, t -> t.keyPressed(event)); + command.executeEdit(tf); + this.pushHistory(command); + return true; + } return super.keyPressed(event); } @@ -267,6 +364,8 @@ public static class Builder extends MultiLineEditBox.Builder { private Runnable onInvalidateFormat = null; @Nullable private Consumer onHistoryPush = null; + @Nullable + private OverflowHandler overflowHandler = null; public Builder onInvalidateFormat(Runnable onInvalidateFormat) { this.onInvalidateFormat = onInvalidateFormat; @@ -278,13 +377,19 @@ public Builder onHistoryPush(Consumer onHistoryPush) { return this; } + public Builder onOverflow(OverflowHandler overflowHandler) { + this.overflowHandler = overflowHandler; + return this; + } + @Override public MultiLineEditBox build(Font font, int width, int height, Component message) { return new RichEditBox(font, this.x, this.y, width, height, this.placeholder, message, this.textColor, this.textShadow, this.cursorColor, this.showBackground, - this.showDecorations, this.onInvalidateFormat, this.onHistoryPush); + this.showDecorations, this.onInvalidateFormat, this.onHistoryPush, + this.overflowHandler); } } } diff --git a/common/src/main/java/me/chrr/scribble/history/HistoryListener.java b/common/src/main/java/me/chrr/scribble/history/HistoryListener.java index c438fd3..927df3d 100644 --- a/common/src/main/java/me/chrr/scribble/history/HistoryListener.java +++ b/common/src/main/java/me/chrr/scribble/history/HistoryListener.java @@ -16,5 +16,14 @@ public interface HistoryListener { void insertPageAt(int page, @Nullable RichText content); - void deletePage(int page); + /** @param navigateDirection negative = go left, 0 or positive = stay/go right */ + void deletePage(int page, int navigateDirection); + + int getTotalPages(); + + RichText getPageContent(int page); + + void setPageContent(int page, RichText content); + + void refreshPages(); } diff --git a/common/src/main/java/me/chrr/scribble/history/command/OverflowCommand.java b/common/src/main/java/me/chrr/scribble/history/command/OverflowCommand.java new file mode 100644 index 0000000..c596252 --- /dev/null +++ b/common/src/main/java/me/chrr/scribble/history/command/OverflowCommand.java @@ -0,0 +1,41 @@ +package me.chrr.scribble.history.command; + +import me.chrr.scribble.book.RichText; +import me.chrr.scribble.gui.edit.RichMultiLineTextField; +import me.chrr.scribble.history.HistoryListener; + +import java.util.List; + +public class OverflowCommand implements Command { + private final int page; + private final RichText before; + private final List after; + + public OverflowCommand(int page, RichText before, List after) { + this.page = page; + this.before = before; + this.after = List.copyOf(after); + } + + @Override + public void execute(HistoryListener listener) { + int newPages = after.size() - 1; + for (int i = 0; i < newPages; i++) listener.insertPageAt(page + 1 + i, null); + for (int i = 0; i < after.size(); i++) listener.setPageContent(page + i, after.get(i)); + listener.refreshPages(); + setCursor(listener.switchAndFocusPage(page + newPages), after.get(newPages).getLength()); + } + + @Override + public void rollback(HistoryListener listener) { + for (int i = after.size() - 1; i > 0; i--) listener.deletePage(page + i, 0); + listener.setPageContent(page, before); + listener.refreshPages(); + setCursor(listener.switchAndFocusPage(page), before.getLength()); + } + + private void setCursor(RichMultiLineTextField tf, int pos) { + tf.cursor = tf.selectCursor = pos; + tf.onValueChange(); + } +} diff --git a/common/src/main/java/me/chrr/scribble/history/command/PageDeleteCommand.java b/common/src/main/java/me/chrr/scribble/history/command/PageDeleteCommand.java index ff17bab..07edcc6 100644 --- a/common/src/main/java/me/chrr/scribble/history/command/PageDeleteCommand.java +++ b/common/src/main/java/me/chrr/scribble/history/command/PageDeleteCommand.java @@ -8,15 +8,17 @@ public class PageDeleteCommand implements Command { private final int page; private final RichText content; + private final int navigateDirection; - public PageDeleteCommand(int page, RichText content) { + public PageDeleteCommand(int page, RichText content, int navigateDirection) { this.page = page; this.content = content; + this.navigateDirection = navigateDirection; } @Override public void execute(HistoryListener listener) { - listener.deletePage(page); + listener.deletePage(page, navigateDirection); } @Override diff --git a/common/src/main/java/me/chrr/scribble/history/command/PageInsertCommand.java b/common/src/main/java/me/chrr/scribble/history/command/PageInsertCommand.java index 66b116d..7622743 100644 --- a/common/src/main/java/me/chrr/scribble/history/command/PageInsertCommand.java +++ b/common/src/main/java/me/chrr/scribble/history/command/PageInsertCommand.java @@ -18,6 +18,6 @@ public void execute(HistoryListener listener) { @Override public void rollback(HistoryListener listener) { - listener.deletePage(page); + listener.deletePage(page, -1); } } diff --git a/common/src/main/java/me/chrr/scribble/screen/ScribbleBookEditScreen.java b/common/src/main/java/me/chrr/scribble/screen/ScribbleBookEditScreen.java index 1f623ce..ef22749 100644 --- a/common/src/main/java/me/chrr/scribble/screen/ScribbleBookEditScreen.java +++ b/common/src/main/java/me/chrr/scribble/screen/ScribbleBookEditScreen.java @@ -5,10 +5,12 @@ import me.chrr.scribble.book.BookFile; import me.chrr.scribble.book.FileChooser; import me.chrr.scribble.book.RichText; +import me.chrr.scribble.book.TextOverflowHandler; import me.chrr.scribble.gui.TextArea; import me.chrr.scribble.gui.button.ColorSwatchWidget; import me.chrr.scribble.gui.button.IconButtonWidget; import me.chrr.scribble.gui.button.ModifierButtonWidget; +import me.chrr.scribble.gui.edit.OverflowHandler; import me.chrr.scribble.gui.edit.RichEditBox; import me.chrr.scribble.gui.edit.RichMultiLineTextField; import me.chrr.scribble.history.CommandManager; @@ -70,6 +72,7 @@ public class ScribbleBookEditScreen extends ScribbleBookScreen impleme private @Nullable IconButtonWidget redoButton; private final List insertPageButtons = new ArrayList<>(); + private final List deletePageButtons = new ArrayList<>(); private @Nullable ModifierButtonWidget boldButton; private @Nullable ModifierButtonWidget italicButton; @@ -79,6 +82,8 @@ public class ScribbleBookEditScreen extends ScribbleBookScreen impleme private List colorSwatches = List.of(); + private @Nullable TextOverflowHandler overflowHandler; + public ScribbleBookEditScreen(Player player, ItemStack itemStack, InteractionHand hand, WritableBookContent book) { super(Component.translatable("book.edit.title")); @@ -143,6 +148,7 @@ private void invalidateActionButtons() { @Override protected void initPageButtons(int y) { this.insertPageButtons.clear(); + this.deletePageButtons.clear(); for (int i = 0; i < this.pagesToShow; i++) { int xOffset = this.pagesToShow == 1 @@ -162,17 +168,17 @@ protected void initPageButtons(int y) { () -> { PageInsertCommand command = new PageInsertCommand(this.currentPage + pageOffset); command.execute(this); - commandManager.push(command); + this.pushCommand(command); }, getBackgroundX() + 78 + xOffset + i * 126, y + 2, 12, 90, 12, 12))); - addRenderableWidget(new IconButtonWidget(deleteText, + this.deletePageButtons.add(addRenderableWidget(new IconButtonWidget(deleteText, () -> { PageDeleteCommand command = new PageDeleteCommand(this.currentPage + pageOffset, - this.pages.get(this.currentPage + pageOffset)); + this.pages.get(this.currentPage + pageOffset), 1); // Navigate right command.execute(this); - commandManager.push(command); + this.pushCommand(command); }, - getBackgroundX() + 94 + xOffset + i * 126, y + 2, 0, 90, 12, 12)); + getBackgroundX() + 94 + xOffset + i * 126, y + 2, 0, 90, 12, 12))); } } @@ -180,6 +186,7 @@ protected void initPageButtons(int y) { public void updateCurrentPages() { super.updateCurrentPages(); this.insertPageButtons.forEach((button) -> button.visible = this.getTotalPages() < 100); + this.deletePageButtons.forEach((button) -> button.visible = this.getTotalPages() > 1); } @Override @@ -204,9 +211,20 @@ protected void setInitialFocus() { @Override protected TextArea createTextArea(int x, int y, int width, int height, int pageOffset) { + // Create the overflow handler lazily (needs font) + if (this.overflowHandler == null) { + this.overflowHandler = new TextOverflowHandler(this, this.font, this::pushCommand); + } + + // Create an overflow handler for this specific edit box + OverflowHandler editBoxOverflowHandler = Scribble.CONFIG.overflowWhenTyping.get() + ? createOverflowHandlerForPage(pageOffset) + : null; + RichEditBox editBox = (RichEditBox) new RichEditBox.Builder() .onHistoryPush((command) -> this.pushCommand(pageOffset, command)) .onInvalidateFormat(this::invalidateFormattingButtons) + .onOverflow(editBoxOverflowHandler) .setShowDecorations(false) .setTextColor(0xff000000).setCursorColor(0xff000000) .setShowBackground(false).setTextShadow(false) @@ -227,6 +245,43 @@ protected TextArea createTextArea(int x, int y, int width, int height, return editBox; } + private OverflowHandler createOverflowHandlerForPage(int pageOffset) { + return new OverflowHandler() { + @Override + public boolean handleOverflow(RichText currentText, int cursor, RichText insert, + @Nullable ChatFormatting color, Set modifiers) { + if (overflowHandler == null || getTotalPages() >= 100) + return false; + int page = currentPage + pageOffset; + return overflowHandler.insertWithOverflow(page, currentText, cursor, insert, color, modifiers); + } + + @Override + public boolean handleEnterAtEnd() { + if (getTotalPages() >= 100) + return false; + int page = currentPage + pageOffset; + PageInsertCommand command = new PageInsertCommand(page + 1); + command.execute(ScribbleBookEditScreen.this); + pushCommand(command); + return true; + } + + @Override + public boolean handleBackspaceOnEmpty() { + if (getTotalPages() <= 1) + return false; + int page = currentPage + pageOffset; + if (page <= 0) + return false; + PageDeleteCommand command = new PageDeleteCommand(page, pages.get(page), -1); // Navigate left + command.execute(ScribbleBookEditScreen.this); + pushCommand(command); + return true; + } + }; + } + private void updateFocusedEditBox() { if (this.getFocused() instanceof RichEditBox focusedEditBox && this.lastFocusedEditBox != focusedEditBox) { this.lastFocusedEditBox = focusedEditBox; @@ -430,7 +485,7 @@ protected RichText getPage(int page) { } @Override - protected int getTotalPages() { + public int getTotalPages() { return this.pages.size(); } @@ -480,10 +535,15 @@ private boolean isEmpty() { //endregion //region History + public void pushCommand(Command command) { + this.pushCommand(0, command); + } + public void pushCommand(int pageOffset, Command command) { if (command instanceof EditCommand editCommand) { editCommand.page = this.currentPage + pageOffset; } + // OverflowCommand, PageInsertCommand, PageDeleteCommand have their state set at construction time this.commandManager.push(command); this.dirty = true; @@ -521,12 +581,27 @@ public void insertPageAt(int page, @Nullable RichText content) { } @Override - public void deletePage(int page) { + public void deletePage(int page, int navigateDirection) { this.pages.remove(page); this.dirty = true; + int target = navigateDirection < 0 ? page - 1 : Math.min(page, this.pages.size() - 1); + this.showPage(Math.max(0, target), false); + this.updateCurrentPages(); + } - if (page >= this.pages.size() - 1) - this.showPage(page - 1, false); + @Override + public RichText getPageContent(int page) { + return this.pages.get(page); + } + + @Override + public void setPageContent(int page, RichText content) { + this.pages.set(page, content); + this.dirty = true; + } + + @Override + public void refreshPages() { this.updateCurrentPages(); } //endregion diff --git a/common/src/main/resources/assets/scribble/lang/de_de.json b/common/src/main/resources/assets/scribble/lang/de_de.json index 011e6d7..2703d37 100644 --- a/common/src/main/resources/assets/scribble/lang/de_de.json +++ b/common/src/main/resources/assets/scribble/lang/de_de.json @@ -1,19 +1,33 @@ { "config.scribble.title": "Scribble-Einstellungen", - "config.scribble.option.copy_formatting_codes": "Formatierungscodes kopieren", - "config.scribble.description.copy_formatting_codes": "Diese Option kann vorübergehend deaktiviert werden, indem\ndie Umschalttaste beim Kopieren oder Einfügen von Text gedrückt wird.", + "config.scribble.category.appearance": "Darstellung", + "config.scribble.option.double_page_viewing": "Zwei Seiten anzeigen", "config.scribble.option.center_book_gui": "Buch-GUI vertikal zentrieren", + "config.scribble.option.show_formatting_buttons": "Formatierungsknöpfe anzeigen", "config.scribble.option.show_action_buttons": "Aktionsknöpfe anzeigen", - "config.scribble.option.show_action_buttons.always": "Immer", - "config.scribble.option.show_action_buttons.when_editing": "Beim bearbeiten", - "config.scribble.option.show_action_buttons.never": "Nie", + "config.scribble.value.show_action_buttons.always": "Immer", + "config.scribble.value.show_action_buttons.when_editing": "Nur beim Bearbeiten", + "config.scribble.value.show_action_buttons.never": "Nie", + "config.scribble.category.behaviour": "Funktion", + "config.scribble.option.copy_formatting_codes": "Formatierungscodes kopieren", + "config.scribble.description.copy_formatting_codes": "Diese Option kann vorübergehend deaktiviert werden, indem\ndie Umschalttaste beim Kopieren oder Einfügen von Text gedrückt wird.", "config.scribble.option.edit_history_size": "Länge der Rückgängig-/Wiederherstellungshistorie", + "config.scribble.option.overflow_when_typing": "Automatischer Seitenumbruch", + "config.scribble.option.paste_behavior": "Einfügeverhalten", + "config.scribble.value.paste_behavior.deny": "Ablehnen wenn es nicht passt", + "config.scribble.value.paste_behavior.fit_page": "Seite füllen", + "config.scribble.value.paste_behavior.overflow": "Auf neuen Seiten fortsetzen", + "config.scribble.category.miscellaneous": "Sonstiges", + "config.scribble.option.open_vanilla_book_screen_on_shift": "Vanilla-GUIs öffnen wenn SHIFT gehalten wird", + "text.tapestry.config.reset": "Auf Standardwert zurücksetzen", "text.scribble.action.delete_page": "Seite löschen", "text.scribble.action.insert_new_page": "Neue Seite einfügen", + "text.scribble.action.insert_new_page_here": "Neue Seite hier einfügen", "text.scribble.action.undo": "Rückgängig", "text.scribble.action.redo": "Wiederherstellen", "text.scribble.action.save_book_to_file": "Buch in Datei speichern...", "text.scribble.action.load_book_from_file": "Buch aus Datei laden...", + "text.scribble.action.settings": "Einstellungen öffnen...", "text.scribble.overwrite_warning.title": "Möchten Sie dieses Buch wirklich überschreiben?", "text.scribble.overwrite_warning.description": "Der aktuelle Inhalt geht unwiderruflich verloren! (Für immer!)", "text.scribble.quit_without_saving.title": "Möchten Sie wirklich ohne Speichern beenden?", diff --git a/common/src/main/resources/assets/scribble/lang/en_us.json b/common/src/main/resources/assets/scribble/lang/en_us.json index 2cc7fb4..2643ed6 100644 --- a/common/src/main/resources/assets/scribble/lang/en_us.json +++ b/common/src/main/resources/assets/scribble/lang/en_us.json @@ -11,6 +11,11 @@ "config.scribble.category.behaviour": "Behaviour", "config.scribble.option.copy_formatting_codes": "Copy formatting codes", "config.scribble.option.edit_history_size": "Edit history limit", + "config.scribble.option.overflow_when_typing": "Overflow when typing", + "config.scribble.option.paste_behavior": "Paste behavior", + "config.scribble.value.paste_behavior.deny": "Deny when it doesn't fit", + "config.scribble.value.paste_behavior.fit_page": "Fill page", + "config.scribble.value.paste_behavior.overflow": "Allow overflow to new pages", "config.scribble.category.miscellaneous": "Miscellaneous", "config.scribble.option.open_vanilla_book_screen_on_shift": "Open vanilla GUIs when holding SHIFT", "text.scribble.action.delete_page": "Delete this page",