├── src ├── test │ ├── resources │ │ └── test_eventyr │ │ │ ├── notat.txt │ │ │ └── A00 │ │ │ ├── A3.txt │ │ │ ├── A2.txt │ │ │ ├── A0.txt │ │ │ └── A1.txt │ └── groovy │ │ └── no │ │ └── advide │ │ ├── RoomConverterTest.groovy │ │ ├── commands │ │ ├── CommandListTest.groovy │ │ ├── UnknownCommandTest.groovy │ │ ├── RemoveAlternativeCommandTest.groovy │ │ ├── ReinstateAlternativeCommandTest.groovy │ │ ├── ContinueCommandTest.groovy │ │ ├── CommandParserTest.groovy │ │ ├── ProseCommandTest.groovy │ │ ├── ConditionalCommandTest.groovy │ │ └── AlternativeCommandTest.groovy │ │ ├── FormattedLineTest.groovy │ │ ├── RoomHistoryTest.groovy │ │ ├── ui │ │ ├── LineRendererTest.groovy │ │ └── KeyInterpreterTest.groovy │ │ ├── AdventureTest.groovy │ │ ├── RoomEditorTest.groovy │ │ ├── RoomTest.groovy │ │ ├── PageEditorTest.groovy │ │ ├── PageTest.groovy │ │ ├── DocumentFragmentTest.groovy │ │ ├── WordWrapperTest.groovy │ │ ├── DocumentFragmentWithXTest.groovy │ │ ├── DocumentTest.groovy │ │ ├── CursorTest.groovy │ │ └── TextEditorTest.groovy └── main │ ├── resources │ ├── fix.png │ ├── typo.png │ ├── warning.png │ ├── background.jpg │ └── fix_active.png │ └── groovy │ └── no │ └── advide │ ├── FormatChange.groovy │ ├── RoomNumber.groovy │ ├── Fix.groovy │ ├── EventEmitter.groovy │ ├── RoomHistory.groovy │ ├── commands │ ├── UnknownCommand.groovy │ ├── CommandList.groovy │ ├── ReinstateAlternativeCommand.groovy │ ├── RemoveAlternativeCommand.groovy │ ├── ContinueCommand.groovy │ ├── ProseCommand.groovy │ ├── Command.groovy │ ├── ConditionalCommand.groovy │ ├── CommandParser.groovy │ ├── BlockCommand.groovy │ └── AlternativeCommand.groovy │ ├── Alternative.groovy │ ├── ui │ ├── BackgroundPanel.groovy │ ├── EditorPanel.groovy │ ├── AppFrame.groovy │ ├── LineRenderer.groovy │ ├── KeyInterpreter.groovy │ ├── Theme.groovy │ └── DocumentRenderer.groovy │ ├── RoomConverter.groovy │ ├── FormattedLine.groovy │ ├── PageEditor.groovy │ ├── PageFormatter.groovy │ ├── Page.groovy │ ├── RoomEditor.groovy │ ├── Adventure.groovy │ ├── WordWrapper.groovy │ ├── Room.groovy │ ├── TextEditor.groovy │ ├── Application.groovy │ ├── DocumentFragment.groovy │ ├── Cursor.groovy │ └── Document.groovy ├── .gitignore ├── README.md ├── ideer.txt ├── tanker.txt └── syntaks.txt /src/test/resources/test_eventyr/notat.txt: -------------------------------------------------------------------------------- 1 | Notatblokka -------------------------------------------------------------------------------- /src/test/resources/test_eventyr/A00/A3.txt: -------------------------------------------------------------------------------- 1 | #1 2 | 3 | #2 4 | #3 -------------------------------------------------------------------------------- /src/test/resources/test_eventyr/A00/A2.txt: -------------------------------------------------------------------------------- 1 | Side 1 2 | !!! 3 | Side 2 -------------------------------------------------------------------------------- /src/test/resources/test_eventyr/A00/A0.txt: -------------------------------------------------------------------------------- 1 | Dette er rom 0 med blåbærsyltetøy. -------------------------------------------------------------------------------- /src/test/resources/test_eventyr/A00/A1.txt: -------------------------------------------------------------------------------- 1 | Et rom med trailing whitespace. -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | *.ipr 3 | *.iws 4 | out 5 | build 6 | .gradle 7 | .DS_Store -------------------------------------------------------------------------------- /src/main/resources/fix.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magnars/Adventur-IDE/master/src/main/resources/fix.png -------------------------------------------------------------------------------- /src/main/resources/typo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magnars/Adventur-IDE/master/src/main/resources/typo.png -------------------------------------------------------------------------------- /src/main/resources/warning.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magnars/Adventur-IDE/master/src/main/resources/warning.png -------------------------------------------------------------------------------- /src/main/resources/background.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magnars/Adventur-IDE/master/src/main/resources/background.jpg -------------------------------------------------------------------------------- /src/main/resources/fix_active.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magnars/Adventur-IDE/master/src/main/resources/fix_active.png -------------------------------------------------------------------------------- /src/main/groovy/no/advide/FormatChange.groovy: -------------------------------------------------------------------------------- 1 | package no.advide 2 | 3 | import java.awt.Color 4 | 5 | class FormatChange { 6 | int index 7 | Color changeColor 8 | boolean revertColorChange = false 9 | def highlight 10 | Map prefix 11 | } 12 | -------------------------------------------------------------------------------- /src/main/groovy/no/advide/RoomNumber.groovy: -------------------------------------------------------------------------------- 1 | package no.advide 2 | 3 | class RoomNumber { 4 | def position 5 | int number 6 | 7 | boolean exists() { 8 | Adventure.current.roomExists(number) 9 | } 10 | 11 | int getLength() { 12 | number.toString().length() 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/groovy/no/advide/Fix.groovy: -------------------------------------------------------------------------------- 1 | package no.advide 2 | 3 | class Fix { 4 | Integer line 5 | def callback 6 | 7 | Fix(Integer line, callback) { 8 | this.line = line 9 | this.callback = callback 10 | } 11 | 12 | void fix() { 13 | callback.call() 14 | } 15 | 16 | String getIcon() { 17 | "fix" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/groovy/no/advide/EventEmitter.groovy: -------------------------------------------------------------------------------- 1 | package no.advide 2 | 3 | class EventEmitter { 4 | def callbacks = [:] 5 | 6 | def on(type, callback) { 7 | if (callbacks[type]) throw new UnsupportedOperationException("Only support for 1 callback at the moment") 8 | callbacks[type] = callback 9 | } 10 | 11 | def emit(type) { 12 | if (callbacks[type]) callbacks[type].call() 13 | } 14 | 15 | def emit(type, info) { 16 | if (callbacks[type]) callbacks[type].call(info) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/test/groovy/no/advide/RoomConverterTest.groovy: -------------------------------------------------------------------------------- 1 | package no.advide 2 | 3 | class RoomConverterTest extends GroovyTestCase { 4 | 5 | void test_should_strip_trailing_spaces_on_load() { 6 | assert ["Whitespace: "], RoomConverter.toNewStyle(["Whitespace: "]) 7 | } 8 | 9 | void test_should_convert_to_new_style() { 10 | assert ["-- fortsett --"], RoomConverter.toNewStyle(["!!!"]) 11 | } 12 | 13 | void test_should_convert_to_old_style() { 14 | assert ["!!!"], RoomConverter.toOldStyle(["-- fortsett --"]) 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/main/groovy/no/advide/RoomHistory.groovy: -------------------------------------------------------------------------------- 1 | package no.advide 2 | 3 | class RoomHistory { 4 | List history 5 | 6 | RoomHistory(Room startingRoom) { 7 | history = [startingRoom] 8 | } 9 | 10 | void push(int number) { 11 | history << Adventure.current.getRoom(number) 12 | } 13 | 14 | void push(Room room) { 15 | history << room 16 | } 17 | 18 | Room getCurrent() { 19 | history.last() 20 | } 21 | 22 | Room pop() { 23 | history.pop() 24 | } 25 | 26 | boolean empty() { 27 | history.size() == 0 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Adventur IDE 2 | ============ 3 | Den sjette editoren jeg skriver for Adventur Delux, denne gangen i Groovy med Swing Builder. 4 | 5 | Høydepunker 6 | ----------- 7 | * Fullscreen-mode inspirert av IAWriter uten forstyrrelser 8 | * Forbedret script-språk (kompatibilitet ved at editoren endrer til og fra det gamle i fart) 9 | * Endelig autocomplete 10 | * Enda bedre støtte for å rette skrivefeil, nå med tegnsettingsregler 11 | 12 | Utviklingsmiljø 13 | --------------- 14 | * Bygg og start: `gradle run` 15 | * Kjør tester: `gradle test` 16 | * Sett opp IntelliJ: `gradle idea` 17 | -------------------------------------------------------------------------------- /src/main/groovy/no/advide/commands/UnknownCommand.groovy: -------------------------------------------------------------------------------- 1 | package no.advide.commands 2 | 3 | import java.awt.Color 4 | import no.advide.DocumentFragment 5 | 6 | class UnknownCommand extends Command { 7 | 8 | static boolean matches(DocumentFragment fragment) { 9 | true 10 | } 11 | 12 | static int numMatchingLines(DocumentFragment fragment) { 13 | 1 14 | } 15 | 16 | UnknownCommand(DocumentFragment fragment) { 17 | super(fragment) 18 | if (fragment.length != 1) throw new IllegalArgumentException("takes 1 line"); 19 | } 20 | 21 | @Override 22 | Color getColor() { 23 | Color.gray 24 | } 25 | 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/main/groovy/no/advide/commands/CommandList.groovy: -------------------------------------------------------------------------------- 1 | package no.advide.commands 2 | 3 | import no.advide.Fix 4 | import no.advide.FormattedLine 5 | import no.advide.RoomNumber 6 | 7 | class CommandList extends ArrayList { 8 | 9 | List getFormattedLines() { 10 | (List) this*.formattedLines.flatten() 11 | } 12 | 13 | List getRoomNumbers() { 14 | (List) this*.roomNumbers.flatten() 15 | } 16 | 17 | List getFixes() { 18 | (List) this*.fixes.flatten() 19 | } 20 | 21 | List getAll(commandType) { 22 | findAll { it?.class == commandType } 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/test/groovy/no/advide/commands/CommandListTest.groovy: -------------------------------------------------------------------------------- 1 | package no.advide.commands 2 | 3 | class CommandListTest extends GroovyTestCase { 4 | 5 | CommandList list 6 | 7 | void setUp() { 8 | list = new CommandList() 9 | list << UnknownCommandTest.createTestCommand() 10 | list << UnknownCommandTest.createTestCommand() 11 | } 12 | 13 | void test_should_be_list() { 14 | assert list instanceof List 15 | } 16 | 17 | void test_should_collect_lines() { 18 | assert list.getFormattedLines().size() == 2 19 | } 20 | 21 | void test_should_get_room_numbers() { 22 | assert list.getRoomNumbers().size() == 0 23 | } 24 | 25 | void test_should_get_commands_of_particular_type() { 26 | list << null 27 | assert list.getAll(UnknownCommand).size() == 2 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/main/groovy/no/advide/Alternative.groovy: -------------------------------------------------------------------------------- 1 | package no.advide 2 | 3 | class Alternative { 4 | int index, number 5 | String text, room, requirement 6 | 7 | List toNewStyle() { 8 | def lines = [text] 9 | if (room) lines << roomAndRequirement 10 | lines 11 | } 12 | 13 | List toOldStyle_NoRequirements() { 14 | def lines = [text] 15 | if (room) lines << room.trim() 16 | lines 17 | } 18 | 19 | List toOldStyle_WithRequirements() { 20 | def lines = [text] 21 | if (room) lines << room.trim() 22 | if (requirement) lines << requirement 23 | lines 24 | } 25 | 26 | String getRoomAndRequirement() { 27 | if (hasRequirement()) "${room.trim()} ? $requirement" 28 | else room.trim() 29 | } 30 | 31 | boolean hasRequirement() { 32 | requirement && requirement != "-" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/groovy/no/advide/commands/ReinstateAlternativeCommand.groovy: -------------------------------------------------------------------------------- 1 | package no.advide.commands 2 | 3 | import no.advide.DocumentFragment 4 | import no.advide.RoomNumber 5 | 6 | class ReinstateAlternativeCommand extends Command { 7 | 8 | static def matches(DocumentFragment fragment) { 9 | fragment.lines.first() =~ /^\*\d+$/ 10 | } 11 | 12 | static int numMatchingLines(DocumentFragment fragment) { 13 | 1 14 | } 15 | 16 | ReinstateAlternativeCommand(DocumentFragment fragment) { 17 | super(fragment) 18 | } 19 | 20 | @Override 21 | List getRoomNumbers() { 22 | return [ new RoomNumber( 23 | number: number, 24 | position: fragment.translate([x:1, y:0]) 25 | ) ] 26 | } 27 | 28 | private int getNumber() { 29 | return fragment.lines.first().substring(1).toInteger() 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/main/groovy/no/advide/commands/RemoveAlternativeCommand.groovy: -------------------------------------------------------------------------------- 1 | package no.advide.commands 2 | 3 | import no.advide.DocumentFragment 4 | import no.advide.RoomNumber 5 | 6 | class RemoveAlternativeCommand extends Command { 7 | 8 | static def matches(DocumentFragment fragment) { 9 | fragment.lines.first() =~ /^#\d+$/ 10 | } 11 | 12 | static int numMatchingLines(DocumentFragment fragment) { 13 | 1 14 | } 15 | 16 | RemoveAlternativeCommand(DocumentFragment fragment) { 17 | super(fragment) 18 | if (fragment.length != 1) throw new IllegalArgumentException("takes 1 line"); 19 | } 20 | 21 | @Override 22 | List getRoomNumbers() { 23 | return [ new RoomNumber( 24 | number: fragment.lines.first().substring(1).toInteger(), 25 | position: fragment.translate([x:1, y:0]) 26 | ) ] 27 | } 28 | 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/main/groovy/no/advide/commands/ContinueCommand.groovy: -------------------------------------------------------------------------------- 1 | package no.advide.commands 2 | 3 | import no.advide.DocumentFragment 4 | import no.advide.FormattedLine 5 | 6 | class ContinueCommand extends Command { 7 | 8 | static boolean matches(DocumentFragment fragment) { 9 | fragment.lines.first() in ["!!!", "-- fortsett --"] 10 | } 11 | 12 | static int numMatchingLines(DocumentFragment fragment) { 13 | 1 14 | } 15 | 16 | ContinueCommand(fragment) { 17 | super(fragment) 18 | } 19 | 20 | @Override 21 | List getFormattedLines() { 22 | def lines = super.formattedLines 23 | lines.first().hasSeparatorLine = true 24 | return lines 25 | } 26 | 27 | @Override 28 | List toOldStyle() { 29 | ["!!!"] 30 | } 31 | 32 | @Override 33 | List toNewStyle() { 34 | ["-- fortsett --"] 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /ideer.txt: -------------------------------------------------------------------------------- 1 | Ny editor 2 | --------- 3 | 4 | Forbedret språk -> convert ved load/save 5 | -> ting som ikke mapper 1-1, som if-else 6 | 7 | Forståelse for alternativer -> ved bytte fra - til + så oppdateres Krav: automatisk 8 | 9 | Forståelse for paragrafer -> reformaterer linjer selv ved paragraf 10 | 11 | Autocomplete for krav 12 | 13 | Informasjon for krav -> sidebar? 14 | 15 | Stedsoversikt -> Manuell tegning av rom (sirkler) med navn, klikkbar 16 | 17 | Bedre skrivefeil -> liste, som før, men også andre regler 18 | 19 | Ting -> bilder, tekst, beskrivelse 20 | 21 | Støtte for Magnar-typiske [ ] sjekkbokser 22 | 23 | cmd+w i tekst: -> select ord -> select setning -> select avsnitt -> select tekst -> select hele rommet 24 | cmd+w i alternativ: -> select ord -> select setningen -> select alternativet -> select alle alternativer -> select hele rommet 25 | 26 | støtte både ctrl+k/y og cmd+c/v/x 27 | -------------------------------------------------------------------------------- /src/main/groovy/no/advide/ui/BackgroundPanel.groovy: -------------------------------------------------------------------------------- 1 | package no.advide.ui 2 | 3 | import java.awt.Graphics 4 | import java.awt.Image 5 | import java.awt.Rectangle 6 | import javax.swing.ImageIcon 7 | import javax.swing.JPanel 8 | 9 | class BackgroundPanel extends JPanel { 10 | 11 | Image img 12 | int imgWidth, imgHeight; 13 | 14 | BackgroundPanel() { 15 | img = new ImageIcon(ClassLoader.getSystemResource('background.jpg')).image 16 | imgWidth = img.getHeight(this); 17 | imgHeight = img.getWidth(this); 18 | } 19 | 20 | @Override 21 | protected void paintComponent(Graphics g) { 22 | int x, y; 23 | 24 | Rectangle clip = g.clipBounds; 25 | 26 | if (imgWidth > 0 && imgHeight > 0) { 27 | for (x = clip.x; x < (clip.x + clip.width) ; x += imgWidth) { 28 | for (y = clip.y; y < (clip.y + clip.height) ; y += imgHeight) { 29 | g.drawImage(img, x, y, this); 30 | } 31 | } 32 | } 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/test/groovy/no/advide/FormattedLineTest.groovy: -------------------------------------------------------------------------------- 1 | package no.advide 2 | 3 | import java.awt.Color 4 | 5 | class FormattedLineTest extends GroovyTestCase { 6 | FormattedLine line 7 | 8 | void setUp() { 9 | line = new FormattedLine(text: "Hei på deg") 10 | } 11 | 12 | void test_should_have_shortcut_for_setting_starting_color() { 13 | line.color = Color.red 14 | assert line.changes[0].changeColor == Color.red 15 | } 16 | 17 | void test_should_have_shortcut_for_getting_starting_color() { 18 | line.changes[0].changeColor = Color.blue 19 | assert line.color == Color.blue 20 | } 21 | 22 | void test_should_start_with_garish_default_color() { 23 | assert line.color == Color.pink 24 | } 25 | 26 | void test_should_set_substring_color() { 27 | line.formatSubstring(4, 2, Color.yellow) 28 | assert line.changes[4].changeColor == Color.yellow 29 | assert line.changes[6].revertColorChange 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/groovy/no/advide/commands/ProseCommand.groovy: -------------------------------------------------------------------------------- 1 | package no.advide.commands 2 | 3 | import java.awt.Color 4 | import no.advide.DocumentFragment 5 | import no.advide.WordWrapper 6 | import no.advide.ui.Theme 7 | 8 | class ProseCommand extends Command { 9 | 10 | static def matches(DocumentFragment fragment) { 11 | isProse(fragment.lines.first()) 12 | } 13 | 14 | static int numMatchingLines(DocumentFragment fragment) { 15 | for (int i = 0; i < fragment.length; i++) { 16 | if (!isProse(fragment.lines[i])) return i 17 | if (i > 0 && fragment.cursor && fragment.cursor.y == i) return i 18 | } 19 | return fragment.length 20 | } 21 | 22 | ProseCommand(fragment) { 23 | super(fragment) 24 | } 25 | 26 | @Override 27 | Color getColor() { 28 | Theme.prose 29 | } 30 | 31 | @Override 32 | void justifyProse(int width) { 33 | new WordWrapper(fragment, width).justify() 34 | } 35 | 36 | private static def isProse(String line) { 37 | line =~ /^"?([0-9]. )?[a-zA-ZæøåÆØÅ]/ 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/test/groovy/no/advide/RoomHistoryTest.groovy: -------------------------------------------------------------------------------- 1 | package no.advide 2 | 3 | class RoomHistoryTest extends GroovyTestCase { 4 | 5 | RoomHistory history 6 | 7 | void setUp() { 8 | AdventureTest.setUpCurrent() 9 | history = new RoomHistory(Adventure.current.getRoom(0)) 10 | } 11 | 12 | void test_should_have_starting_room() { 13 | assert history.current == Adventure.current.getRoom(0) 14 | } 15 | 16 | void test_should_push_rooms() { 17 | history.push(Adventure.current.getRoom(1)) 18 | assert history.current == Adventure.current.getRoom(1) 19 | } 20 | 21 | void test_should_push_room_numbers() { 22 | history.push(2) 23 | assert history.current == Adventure.current.getRoom(2) 24 | } 25 | 26 | void test_should_pop_history() { 27 | history.push(2) 28 | assert history.pop().number == 2 29 | assert history.current.number == 0 30 | } 31 | 32 | void test_should_know_if_history_is_empty() { 33 | assert !history.empty() 34 | history.pop() 35 | assert history.empty() 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/test/groovy/no/advide/ui/LineRendererTest.groovy: -------------------------------------------------------------------------------- 1 | package no.advide.ui 2 | 3 | import java.awt.Color 4 | import java.awt.Graphics2D 5 | import no.advide.FormatChange 6 | import no.advide.FormattedLine 7 | 8 | class LineRendererTest extends GroovyTestCase { 9 | 10 | def color 11 | def renderer 12 | 13 | void setUp() { 14 | renderer = new LineRenderer(new FormattedLine(), 0, 0, [setColor: { color = it }, getFontMetrics: { null }] as Graphics2D) 15 | } 16 | 17 | void test_should_stack_colors() { 18 | renderer.apply(new FormatChange(changeColor: Color.black)) 19 | assert color == Color.black 20 | 21 | renderer.apply(new FormatChange(changeColor: Color.blue)) 22 | assert color == Color.blue 23 | 24 | renderer.apply(new FormatChange(changeColor: Color.green)) 25 | assert color == Color.green 26 | 27 | renderer.apply(new FormatChange(revertColorChange: true)) 28 | assert color == Color.blue 29 | 30 | renderer.apply(new FormatChange(revertColorChange: true)) 31 | assert color == Color.black 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/main/groovy/no/advide/RoomConverter.groovy: -------------------------------------------------------------------------------- 1 | package no.advide 2 | 3 | import no.advide.commands.CommandParser 4 | 5 | class RoomConverter { 6 | 7 | static List toNewStyle(lines) { 8 | lines = replaceWithNewStyle(lines) 9 | lines = justifyProse(lines) 10 | return lines 11 | } 12 | 13 | private static def justifyProse(List lines) { 14 | def document = new Document(lines + [], [x: 0, y: 0]) 15 | def page = new Page(document) 16 | page.justifyProse() 17 | return document.lines 18 | } 19 | 20 | private static def replaceWithNewStyle(lines) { 21 | def document = new Document(lines, [x: 0, y: 0]) 22 | document.stripTrailingSpaces() 23 | new CommandParser(document).parse().each { it.replaceWithNewStyle() } 24 | return document.lines 25 | } 26 | 27 | static List toOldStyle(lines) { 28 | def document = new Document(lines, [x:0, y:0]) 29 | new CommandParser(document).parse().each { it.replaceWithOldStyle() } 30 | document.stripTrailingSpaces() 31 | return document.lines 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/main/groovy/no/advide/ui/EditorPanel.groovy: -------------------------------------------------------------------------------- 1 | package no.advide.ui 2 | 3 | import java.awt.Font 4 | import java.awt.Graphics 5 | import java.awt.Graphics2D 6 | import java.awt.RenderingHints 7 | import javax.swing.JPanel 8 | import no.advide.FormattedLine 9 | 10 | class EditorPanel extends JPanel { 11 | 12 | def textLayout 13 | def defaultFont 14 | 15 | EditorPanel() { 16 | setFocusTraversalKeysEnabled(false) 17 | defaultFont = new Font("Monaco", Font.PLAIN, 20) 18 | } 19 | 20 | @Override 21 | void paintComponent(Graphics graphics) { 22 | super.paintComponent(graphics) 23 | Graphics2D g = (Graphics2D) graphics 24 | g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY) 25 | g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON) 26 | g.setFont(defaultFont) 27 | new DocumentRenderer(textLayout, getWidth(), g).render() 28 | } 29 | 30 | void updateContents(List lines, cursor) { 31 | textLayout = [lines: lines, cursor: cursor] 32 | repaint() 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/test/groovy/no/advide/commands/UnknownCommandTest.groovy: -------------------------------------------------------------------------------- 1 | package no.advide.commands 2 | 3 | import java.awt.Color 4 | import no.advide.Document 5 | import no.advide.DocumentFragment 6 | 7 | class UnknownCommandTest extends GroovyTestCase { 8 | 9 | Command command 10 | 11 | static Command createTestCommand() { 12 | def document = new Document([":hm"], [x:0, y:0]) 13 | return new UnknownCommand(document.createFragment([x:0, y:0], 1)) 14 | } 15 | 16 | void setUp() { 17 | command = createTestCommand() 18 | } 19 | 20 | DocumentFragment createFragment(List lines) { 21 | new Document(lines, [x:0, y:0]).createFragment([x:0, y:0], lines.size()) 22 | } 23 | 24 | void test_line_should_match_input() { 25 | def lines = command.getFormattedLines() 26 | assert lines.size() == 1 27 | assert lines.first().text == ":hm" 28 | } 29 | 30 | void test_should_always_match() { 31 | assert UnknownCommand.matches(createFragment([""])) 32 | } 33 | 34 | void test_should_match_one_line() { 35 | assert UnknownCommand.numMatchingLines(createFragment(["", ""])) == 1 36 | } 37 | 38 | void test_should_format_lines_grey() { 39 | assert command.formattedLines.first().color == Color.gray 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/test/groovy/no/advide/commands/RemoveAlternativeCommandTest.groovy: -------------------------------------------------------------------------------- 1 | package no.advide.commands 2 | 3 | import no.advide.AdventureTest 4 | import no.advide.Document 5 | import no.advide.DocumentFragment 6 | 7 | class RemoveAlternativeCommandTest extends GroovyTestCase { 8 | 9 | def command 10 | 11 | void setUp() { 12 | AdventureTest.setUpCurrent() 13 | } 14 | 15 | DocumentFragment createFragment(List lines) { 16 | new Document(lines, [x:0, y:0]).createFragment([x:0, y:0], lines.size()) 17 | } 18 | 19 | void setUpCommand(line) { 20 | command = new RemoveAlternativeCommand(createFragment([line])) 21 | } 22 | 23 | void test_should_match_starting_hash_with_numbers() { 24 | assert RemoveAlternativeCommand.matches(createFragment(["#17", ""])) 25 | } 26 | 27 | void test_should_not_match_nonnumerical() { 28 | assert !RemoveAlternativeCommand.matches(createFragment(["#SAVE#", ""])) 29 | } 30 | 31 | void test_should_match_one_line() { 32 | assert RemoveAlternativeCommand.numMatchingLines(createFragment(["#17", "#23"])) == 1 33 | } 34 | 35 | void test_should_return_room_number() { 36 | setUpCommand("#123") 37 | assert command.roomNumbers.size() == 1 38 | assert command.roomNumbers.first().number == 123 39 | assert command.roomNumbers.first().position == [x:1, y:0] 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/test/groovy/no/advide/commands/ReinstateAlternativeCommandTest.groovy: -------------------------------------------------------------------------------- 1 | package no.advide.commands 2 | 3 | import no.advide.AdventureTest 4 | import no.advide.Document 5 | import no.advide.DocumentFragment 6 | 7 | class ReinstateAlternativeCommandTest extends GroovyTestCase { 8 | 9 | Command command 10 | 11 | void setUp() { 12 | AdventureTest.setUpCurrent() 13 | } 14 | 15 | void setUpCommand(line) { 16 | command = new ReinstateAlternativeCommand(createFragment([line])) 17 | } 18 | 19 | DocumentFragment createFragment(List lines) { 20 | new Document(lines, [x:0, y:0]).createFragment([x:0, y:0], lines.size()) 21 | } 22 | 23 | void test_should_match_old_form() { 24 | assert ReinstateAlternativeCommand.matches(createFragment(["*17", ""])) 25 | } 26 | 27 | void test_should_not_match_nonnumerical() { 28 | assert !ReinstateAlternativeCommand.matches(createFragment(["* En punktliste", ""])) 29 | } 30 | 31 | void test_should_match_one_line() { 32 | assert ReinstateAlternativeCommand.numMatchingLines(createFragment(["*17", "*23"])) == 1 33 | } 34 | 35 | void test_should_return_room_number_in_old_form() { 36 | setUpCommand("*123") 37 | assert command.roomNumbers.size() == 1 38 | assert command.roomNumbers.first().number == 123 39 | assert command.roomNumbers.first().position == [x:1, y:0] 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/main/groovy/no/advide/FormattedLine.groovy: -------------------------------------------------------------------------------- 1 | package no.advide 2 | 3 | import java.awt.Color 4 | import no.advide.ui.Theme 5 | 6 | class FormattedLine { 7 | String text 8 | Map changes = [:] 9 | boolean hasSeparatorLine 10 | boolean isEmbossedTop 11 | boolean isEmbossed 12 | boolean isEmbossedBottom 13 | String icon 14 | String prefix 15 | int prefixPosition = 0 16 | Color prefixColor = Theme.prefix 17 | 18 | FormattedLine() { 19 | changes[0] = new FormatChange(index: 0, changeColor: Color.pink) 20 | } 21 | 22 | void setColor(color) { 23 | changeAt(0).changeColor = color 24 | } 25 | 26 | Color getColor() { 27 | changeAt(0).changeColor 28 | } 29 | 30 | void formatSubstring(int startIndex, int length, Color color) { 31 | changeAt(startIndex).changeColor = color 32 | changeAt(startIndex + length).revertColorChange = true 33 | } 34 | 35 | void highlightSubstring(int startIndex, int length, Color color) { 36 | changeAt(startIndex).highlight = [length: length, color: color] 37 | } 38 | 39 | void cementPrefix() { 40 | if (prefix) changeAt(prefixPosition).prefix = [text: prefix, color: prefixColor] 41 | } 42 | 43 | FormatChange changeAt(int startIndex) { 44 | if (!changes[startIndex]) { 45 | changes[startIndex] = new FormatChange(index: startIndex) 46 | } 47 | changes[startIndex] 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/groovy/no/advide/commands/Command.groovy: -------------------------------------------------------------------------------- 1 | package no.advide.commands 2 | 3 | import java.awt.Color 4 | import no.advide.DocumentFragment 5 | import no.advide.Fix 6 | import no.advide.FormattedLine 7 | import no.advide.RoomNumber 8 | import no.advide.ui.Theme 9 | 10 | abstract class Command { 11 | 12 | final DocumentFragment fragment 13 | 14 | public Command(fragment) { 15 | this.fragment = fragment 16 | } 17 | 18 | Color getColor() { 19 | Theme.command 20 | } 21 | 22 | List getFormattedLines() { 23 | fragment.lines.collect { new FormattedLine(text: it, color: color)} 24 | } 25 | 26 | List toOldStyle() { 27 | fragment.lines 28 | } 29 | 30 | List toNewStyle() { 31 | fragment.lines 32 | } 33 | 34 | List getFixes() { 35 | isInNewStyle() ? [] : [new Fix(fragment.offset.y, { replaceWithNewStyle() })] 36 | } 37 | 38 | void replaceWithNewStyle() { 39 | if (!isInNewStyle()) fragment.replaceWith(toNewStyle()) 40 | } 41 | 42 | void replaceWithOldStyle() { 43 | if (!isInOldStyle()) fragment.replaceWith(toOldStyle()) 44 | } 45 | 46 | void justifyProse(int width) {} 47 | 48 | boolean isInOldStyle() { 49 | fragment.lines == toOldStyle() 50 | } 51 | 52 | boolean isInNewStyle() { 53 | fragment.lines == toNewStyle() 54 | } 55 | 56 | List getRoomNumbers() { 57 | [] 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /src/test/groovy/no/advide/commands/ContinueCommandTest.groovy: -------------------------------------------------------------------------------- 1 | package no.advide.commands 2 | 3 | import no.advide.Document 4 | import no.advide.DocumentFragment 5 | 6 | class ContinueCommandTest extends GroovyTestCase { 7 | 8 | DocumentFragment createFragment(List lines) { 9 | new Document(lines, [x:0, y:0]).createFragment([x:0, y:0], lines.size()) 10 | } 11 | 12 | void test_should_match_old_style() { 13 | assert ContinueCommand.matches(createFragment(["!!!"])) 14 | assert !ContinueCommand.matches(createFragment(["noe annet", "!!!"])) 15 | } 16 | 17 | void test_should_match_new_style() { 18 | assert ContinueCommand.matches(createFragment(["-- fortsett --"])) 19 | } 20 | 21 | void test_should_match_one_line() { 22 | assert ContinueCommand.numMatchingLines(createFragment(["!!!", "!!!"])) == 1 23 | } 24 | 25 | void test_should_render_with_separator_line() { 26 | def command = new ContinueCommand(createFragment(["!!!"])) 27 | assert command.formattedLines.first().hasSeparatorLine 28 | } 29 | 30 | void test_should_convert_to_old_script() { 31 | def command = new ContinueCommand(createFragment(["-- fortsett --"])) 32 | assert command.toOldStyle() == ["!!!"] 33 | } 34 | 35 | void test_should_convert_to_new_script() { 36 | def command = new ContinueCommand(createFragment(["!!!"])) 37 | assert command.toNewStyle() == ["-- fortsett --"] 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/main/groovy/no/advide/PageEditor.groovy: -------------------------------------------------------------------------------- 1 | package no.advide 2 | 3 | class PageEditor extends EventEmitter { 4 | 5 | Page page 6 | TextEditor textEditor 7 | 8 | def cursorMoves = [ 9 | "tab": { page.moveCursorTo page.nextRoomNumber }, 10 | "shift+tab": { page.moveCursorTo page.previousRoomNumber } 11 | ] 12 | 13 | def actions = [ 14 | "cmd+F": { page.nextFix?.fix() }, 15 | "ctrl+alt+cmd+O": { page.changeToOldStyle() }, 16 | "ctrl+alt+cmd+N": { page.changeToNewStyle() } 17 | ] 18 | 19 | PageEditor(Page page) { 20 | this.page = page 21 | textEditor = new TextEditor(page.document) 22 | textEditor.onChange { changed() } 23 | textEditor.onCursorMove { cursorMoved() } 24 | } 25 | 26 | def charTyped(c) { 27 | textEditor.charTyped(c) 28 | } 29 | 30 | def actionTyped(k) { 31 | def a = actions[k] 32 | def c = cursorMoves[k] 33 | if (a) { 34 | a.call() 35 | changed() 36 | } else if (c) { 37 | c.call() 38 | cursorMoved() 39 | } else { 40 | textEditor.actionTyped(k) 41 | } 42 | } 43 | 44 | def onChange(callback) { 45 | on('change', callback) 46 | } 47 | 48 | def changed() { 49 | emit('change') 50 | } 51 | 52 | def onCursorMove(callback) { 53 | on('cursorMove', callback) 54 | } 55 | 56 | def cursorMoved() { 57 | emit('cursorMove') 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /src/main/groovy/no/advide/PageFormatter.groovy: -------------------------------------------------------------------------------- 1 | package no.advide 2 | 3 | import no.advide.commands.CommandList 4 | import no.advide.ui.Theme 5 | 6 | class PageFormatter { 7 | 8 | Page page 9 | boolean modified 10 | 11 | PageFormatter(Page page, boolean isModified) { 12 | this.page = page 13 | this.modified = isModified 14 | } 15 | 16 | CommandList getCommands() { 17 | page.commands 18 | } 19 | 20 | List getFormattedLines() { 21 | def lines = commands.formattedLines 22 | formatLines(lines) 23 | addFixIcons(lines) 24 | lines 25 | } 26 | 27 | void addFixIcons(List lines) { 28 | page.fixes.each { lines[it.line].icon = it.icon } 29 | if (page.nextFix) { lines[page.nextFix.line].icon = "${page.nextFix.icon}_active" } 30 | } 31 | 32 | def formatLines(List lines) { 33 | colorRoomNumbers(lines) 34 | if (!modified) highlightTargetRoomNumber(lines) 35 | } 36 | 37 | void colorRoomNumbers(List lines) { 38 | commands.roomNumbers.each { RoomNumber r -> 39 | lines[r.position.y].formatSubstring(r.position.x, r.length, r.exists() ? Theme.roomExists : Theme.roomDoesntExist) 40 | } 41 | } 42 | 43 | void highlightTargetRoomNumber(List lines) { 44 | def r = page.targetRoomNumber 45 | if (r) lines[r.position.y].highlightSubstring(r.position.x, r.length, Theme.roomHighlight) 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /src/main/groovy/no/advide/commands/ConditionalCommand.groovy: -------------------------------------------------------------------------------- 1 | package no.advide.commands 2 | 3 | import no.advide.DocumentFragment 4 | 5 | class ConditionalCommand extends BlockCommand { 6 | 7 | ConditionalCommand(fragment) { 8 | super(fragment) 9 | } 10 | 11 | static boolean matches(DocumentFragment fragment) { 12 | matchesOldForm(fragment) || matchesNewForm(fragment) 13 | } 14 | 15 | private static def matchesOldForm(DocumentFragment fragment) { 16 | fragment.lines.first().startsWith("[!]") 17 | } 18 | 19 | private static def matchesNewForm(DocumentFragment fragment) { 20 | fragment.lines.first().startsWith("? ") 21 | } 22 | 23 | static int numMatchingLines(DocumentFragment fragment) { 24 | if (matchesOldForm(fragment)) return numMatchingLinesOldForm(fragment) 25 | if (matchesNewForm(fragment)) return numMatchingLinesNewForm(fragment) 26 | 0 27 | } 28 | 29 | @Override 30 | boolean isOldForm() { 31 | matchesOldForm(fragment) 32 | } 33 | 34 | @Override 35 | boolean isNewForm() { 36 | matchesNewForm(fragment) 37 | } 38 | 39 | @Override 40 | List toNewStyle() { 41 | toNewStyle("? ${requirement}") 42 | } 43 | 44 | @Override 45 | List toOldStyle() { 46 | toOldStyle("[!]${requirement}") 47 | } 48 | 49 | String getRequirement() { 50 | matchesNewForm(fragment) ? fragment.lines.first().substring(2) : fragment.lines.first().substring(3) 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /src/test/groovy/no/advide/AdventureTest.groovy: -------------------------------------------------------------------------------- 1 | package no.advide 2 | 3 | class AdventureTest extends GroovyTestCase { 4 | 5 | static void setUpCurrent() { 6 | def directory = new File(ClassLoader.getSystemResource('test_eventyr').toURI()) 7 | Adventure.current = new Adventure(directory) 8 | } 9 | 10 | Adventure adventure 11 | 12 | void setUp() { 13 | setUpCurrent() 14 | adventure = Adventure.current 15 | } 16 | 17 | void test_should_know_if_room_exists() { 18 | assert adventure.roomExists(0) 19 | assert !adventure.roomExists(17) 20 | } 21 | 22 | void test_path_to_room_number() { 23 | assert adventure.pathTo(0).endsWith("/A00/A0.txt") 24 | assert adventure.pathTo(101).endsWith("/A01/A101.txt") 25 | assert adventure.pathTo(217).endsWith("/A02/A217.txt") 26 | } 27 | 28 | void test_should_load_room() { 29 | def room = adventure.getRoom(0) 30 | assert room.name == "Rom 0" 31 | assert room.lines == ["Dette er rom 0 med blåbærsyltetøy."] 32 | } 33 | 34 | void test_should_load_notes() { 35 | def notes = adventure.getNotes() 36 | assert notes.name == "Notatblokk" 37 | assert notes.lines == ["Notatblokka"] 38 | } 39 | 40 | void test_should_format_adventure_name() { 41 | assert adventure.name == "Test Eventyr" 42 | } 43 | 44 | void test_should_treat_pages_as_entities_and_not_give_out_copies() { 45 | assert adventure.getRoom(2) == adventure.getRoom(2) 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /src/test/groovy/no/advide/RoomEditorTest.groovy: -------------------------------------------------------------------------------- 1 | package no.advide 2 | 3 | class RoomEditorTest extends GroovyTestCase { 4 | 5 | Page page 6 | 7 | void setUp() { 8 | AdventureTest.setUpCurrent() 9 | page = getPage(3) // ["#1", "", "#2", "#3"] 10 | } 11 | 12 | Page getPage(int number) { 13 | new Page(Adventure.current.getRoom(number).createDocument()) 14 | } 15 | 16 | void test_cmd_S_should_save_room() { 17 | def called = false 18 | def editor = new RoomEditor(page, new RoomHistory([ save: { called = true } ] as Room)) 19 | 20 | editor.actionTyped("cmd+S") 21 | assert called 22 | } 23 | 24 | void test_cmd_Z_should_undo() { 25 | def called = false 26 | def editor = new RoomEditor(page, new RoomHistory([ undo: { called = true } ] as Room)) 27 | 28 | editor.actionTyped("cmd+Z") 29 | assert called 30 | } 31 | 32 | void test_shift_cmd_Z_should_undo() { 33 | def called = false 34 | def editor = new RoomEditor(page, new RoomHistory([ redo: { called = true } ] as Room)) 35 | 36 | editor.actionTyped("shift+cmd+Z") 37 | assert called 38 | } 39 | 40 | void test_jump() { 41 | def history = new RoomHistory([isModified: { false }] as Room) 42 | def editor = new RoomEditor(page, history) 43 | def called = false 44 | editor.onRoomChange { 45 | called = true 46 | } 47 | editor.charTyped("'") 48 | assert called 49 | assert history.current.number == 1 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/test/groovy/no/advide/RoomTest.groovy: -------------------------------------------------------------------------------- 1 | package no.advide 2 | 3 | class RoomTest extends GroovyTestCase { 4 | 5 | void setUp() { 6 | AdventureTest.setUpCurrent() 7 | } 8 | 9 | void test_should_save_file_in_old_style() { 10 | def file = new File("PageTest.tmp") 11 | file.setText(["-- fortsett --", "Hei på deg"].join("\n"), 'UTF-8') 12 | def room = new Room(1, file) 13 | room.save() 14 | assert file.getText('UTF-8') == ["!!!", "Hei på deg"].join("\n") 15 | file.delete() 16 | } 17 | 18 | void test_should_know_if_modified() { 19 | def room = Adventure.current.getRoom(0) 20 | assert !room.modified 21 | room.lines = ["endret"] 22 | assert room.modified 23 | } 24 | 25 | void test_should_undo_and_redo() { 26 | def room = Adventure.current.getRoom(0) 27 | assert room.lines == ["Dette er rom 0 med blåbærsyltetøy."] 28 | room.lines = ["endret"] 29 | room.lines = ["endret mer"] 30 | 31 | room.undo() 32 | assert room.lines == ["endret"] 33 | 34 | room.redo() 35 | assert room.lines == ["endret mer"] 36 | 37 | room.redo() 38 | assert room.lines == ["endret mer"] 39 | 40 | room.undo() 41 | room.undo() 42 | assert room.lines == ["Dette er rom 0 med blåbærsyltetøy."] 43 | 44 | room.undo() 45 | assert room.lines == ["Dette er rom 0 med blåbærsyltetøy."] 46 | 47 | room.redo() 48 | assert room.lines == ["endret"] 49 | 50 | room.lines = ["branch in history"] 51 | assert room.lines == ["branch in history"] 52 | 53 | room.redo() 54 | assert room.lines == ["branch in history"] 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /src/main/groovy/no/advide/Page.groovy: -------------------------------------------------------------------------------- 1 | package no.advide 2 | 3 | import no.advide.commands.CommandList 4 | import no.advide.commands.CommandParser 5 | 6 | class Page { 7 | Document document 8 | CommandList commands 9 | 10 | Page(document) { 11 | this.document = document 12 | this.commands = new CommandParser(this.document).parse() 13 | } 14 | 15 | RoomNumber getTargetRoomNumber() { 16 | commands.roomNumbers.find { it.position.y >= document.cursor.y } 17 | } 18 | 19 | RoomNumber getCurrentRoomNumber() { 20 | commands.roomNumbers.find { it.position.y == document.cursor.y } 21 | } 22 | 23 | RoomNumber getNextRoomNumber() { 24 | commands.roomNumbers.find { it.position.y > document.cursor.y } 25 | } 26 | 27 | RoomNumber getPreviousRoomNumber() { 28 | def prev = commands.roomNumbers.findAll { it.position.y < document.cursor.y } 29 | prev.empty ? null : prev.last() 30 | } 31 | 32 | void moveCursorTo(RoomNumber number) { 33 | if (number) cursor = number.position 34 | } 35 | 36 | List getFixes() { 37 | commands.fixes 38 | } 39 | 40 | Fix getNextFix() { 41 | commands.fixes.find { it.line >= cursor.y } 42 | } 43 | 44 | void justifyProse() { 45 | commands.each { it.justifyProse(80) } 46 | } 47 | 48 | void changeToOldStyle() { 49 | commands.each { it.replaceWithOldStyle() } 50 | } 51 | 52 | void changeToNewStyle() { 53 | commands.each { it.replaceWithNewStyle() } 54 | } 55 | 56 | def getCursor() { 57 | document.cursor 58 | } 59 | 60 | void setCursor(cursor) { 61 | document.cursor = cursor 62 | } 63 | 64 | def getLines() { 65 | document.lines 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /src/test/groovy/no/advide/commands/CommandParserTest.groovy: -------------------------------------------------------------------------------- 1 | package no.advide.commands 2 | 3 | import no.advide.Document 4 | 5 | class CommandParserTest extends GroovyTestCase { 6 | 7 | def commands 8 | 9 | void setUpCommands(List lines) { 10 | def document = new Document(lines, [x:0, y:0]) 11 | commands = new CommandParser(document).parse() 12 | } 13 | 14 | void test_should_convert_strings_to_command_list() { 15 | setUpCommands(["", "", ""]) 16 | assert commands.size() == 3 17 | } 18 | 19 | void test_should_parse_UnknownCommand() { 20 | setUpCommands([":h"]) 21 | assert commands.first().class == UnknownCommand.class 22 | } 23 | 24 | void test_should_parse_RemoveAlternativeCommand() { 25 | setUpCommands(["#100"]) 26 | assert commands.first().class == RemoveAlternativeCommand.class 27 | } 28 | 29 | void test_should_parse_ProseCommand() { 30 | setUpCommands(["hei", "du"]) 31 | assert commands.first().class == ProseCommand.class 32 | } 33 | 34 | void test_should_parse_ContinueCommand() { 35 | setUpCommands(["!!!"]) 36 | assert commands.first().class == ContinueCommand.class 37 | } 38 | 39 | void test_should_parse_ReinstateAlternativeCommand() { 40 | setUpCommands(["*123"]) 41 | assert commands.first().class == ReinstateAlternativeCommand.class 42 | } 43 | 44 | void test_should_parse_ConditionalCommand() { 45 | setUpCommands(["? KRAV"]) 46 | assert commands.first().class == ConditionalCommand.class 47 | } 48 | 49 | void test_should_parse_AlternativeCommand() { 50 | setUpCommands(["-", "1", "Tekst", "123"]) 51 | assert commands.first().class == AlternativeCommand.class 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /src/test/groovy/no/advide/PageEditorTest.groovy: -------------------------------------------------------------------------------- 1 | package no.advide 2 | 3 | class PageEditorTest extends GroovyTestCase { 4 | 5 | void setUp() { 6 | AdventureTest.setUpCurrent() 7 | } 8 | 9 | Page getPage(int number) { 10 | new Page(Adventure.current.getRoom(number).createDocument()) 11 | } 12 | 13 | void test_can_toggle_styles() { 14 | def page = getPage(2) 15 | assert page.lines == ["Side 1", "-- fortsett --", "Side 2"] 16 | def editor = new PageEditor(page) 17 | editor.actionTyped("ctrl+alt+cmd+O") 18 | assert page.lines == ["Side 1", "!!!", "Side 2"] 19 | editor.actionTyped("ctrl+alt+cmd+N") 20 | assert page.lines == ["Side 1", "-- fortsett --", "Side 2"] 21 | } 22 | 23 | void test_tabbing_between_room_numbers() { 24 | def page = getPage(3) 25 | assert page.lines == ["#1", "", "#2", "#3"] 26 | assert page.cursor == [x:0, y:0] 27 | def editor = new PageEditor(page) 28 | editor.actionTyped("tab") 29 | assert page.cursor == [x:1, y:2] 30 | editor.actionTyped("tab") 31 | assert page.cursor == [x:1, y:3] 32 | editor.actionTyped("tab") 33 | assert page.cursor == [x:1, y:3] 34 | editor.actionTyped("shift+tab") 35 | assert page.cursor == [x:1, y:2] 36 | editor.actionTyped("shift+tab") 37 | assert page.cursor == [x:1, y:0] 38 | editor.actionTyped("shift+tab") 39 | assert page.cursor == [x:1, y:0] 40 | } 41 | 42 | void test_fixing() { 43 | def page = getPage(2) 44 | page.changeToOldStyle() 45 | assert page.lines == ["Side 1", "!!!", "Side 2"] 46 | def editor = new PageEditor(page) 47 | editor.actionTyped("cmd+F") 48 | assert page.lines == ["Side 1", "-- fortsett --", "Side 2"] 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/main/groovy/no/advide/RoomEditor.groovy: -------------------------------------------------------------------------------- 1 | package no.advide 2 | 3 | class RoomEditor extends EventEmitter { 4 | 5 | Page page 6 | RoomHistory roomHistory 7 | PageEditor pageEditor 8 | 9 | def actions = [ 10 | "jump": { if (page.targetRoomNumber) roomHistory.push(page.targetRoomNumber.number) }, 11 | "escape": { if (room.modified) { room.restoreOriginal() } else { roomHistory.pop() } }, 12 | "cmd+S": { room.save() }, 13 | "cmd+Z": { room.undo() }, 14 | "shift+cmd+Z": { room.redo() } 15 | ] 16 | 17 | RoomEditor(Page page, RoomHistory history) { 18 | this.page = page 19 | this.roomHistory = history 20 | pageEditor = new PageEditor(page) 21 | pageEditor.onChange { documentChanged() } 22 | pageEditor.onCursorMove { cursorMoved() } 23 | } 24 | 25 | Room getRoom() { 26 | roomHistory.current 27 | } 28 | 29 | def charTyped(c) { 30 | if (isJump(c)) { 31 | actionTyped('jump') 32 | } else { 33 | pageEditor.charTyped(c) 34 | } 35 | } 36 | 37 | private boolean isJump(c) { 38 | return c == "'" && !room.modified 39 | } 40 | 41 | def actionTyped(k) { 42 | def a = actions[k] 43 | if (a) { 44 | a.call() 45 | roomChanged() 46 | } else { 47 | pageEditor.actionTyped(k) 48 | } 49 | } 50 | 51 | def onDocumentChange(callback) { 52 | on('documentChange', callback) 53 | } 54 | 55 | def documentChanged() { 56 | emit('documentChange') 57 | } 58 | 59 | def onRoomChange(callback) { 60 | on('roomChange', callback) 61 | } 62 | 63 | def roomChanged() { 64 | emit('roomChange') 65 | } 66 | 67 | def onCursorMove(callback) { 68 | on('cursorMove', callback) 69 | } 70 | 71 | def cursorMoved() { 72 | emit('cursorMove') 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /src/main/groovy/no/advide/Adventure.groovy: -------------------------------------------------------------------------------- 1 | package no.advide 2 | 3 | import org.apache.commons.lang3.text.WordUtils 4 | 5 | class Adventure { 6 | 7 | static Adventure current 8 | 9 | static void choose() { 10 | /* 11 | 12 | // Bør bruke java.awt.FileDialog her, pga Mac look-n-feel 13 | 14 | JFileChooser fc = null 15 | new SwingBuilder().edt { 16 | fc = fileChooser(dialogTitle: "Velg en mappe med eventyr", 17 | id: "openDirectoryDialog", fileSelectionMode: JFileChooser.DIRECTORIES_ONLY) {} 18 | } 19 | if (fc.showOpenDialog(null) != JFileChooser.APPROVE_OPTION) System.exit(0) 20 | current = new Adventure(fc.selectedFile) 21 | */ 22 | current = new Adventure(new File("/Users/fimasvee/projects/adventur/eventyr/master")) 23 | } 24 | 25 | String directoryPath 26 | 27 | Adventure(File directory) { 28 | directoryPath = directory.absolutePath 29 | } 30 | 31 | boolean roomExists(int roomNumber) { 32 | roomFile(roomNumber).exists() 33 | } 34 | 35 | private File roomFile(int roomNumber) { 36 | if (roomNumber == -1) { return new File("${directoryPath}/notat.txt") } 37 | else return new File(pathTo(roomNumber)) 38 | } 39 | 40 | String pathTo(int roomNumber) { 41 | "${directoryPath}/${subdir(roomNumber)}/A${roomNumber}.txt" 42 | } 43 | 44 | String subdir(int roomNumber) { 45 | int hundreds = roomNumber / 100 46 | hundreds < 10 ? "A0${hundreds}" : "A${hundreds}" 47 | } 48 | 49 | Room getNotes() { 50 | getRoom(-1) 51 | } 52 | 53 | def rooms = [:] 54 | 55 | Room getRoom(int number) { 56 | if (!rooms[number]) { 57 | rooms[number] = new Room(number, roomFile(number)) 58 | } 59 | rooms[number] 60 | } 61 | 62 | String getName() { 63 | WordUtils.capitalize(new File(directoryPath).name.replace("_", " ")) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/main/groovy/no/advide/WordWrapper.groovy: -------------------------------------------------------------------------------- 1 | package no.advide 2 | 3 | import org.apache.commons.lang3.StringUtils 4 | import org.apache.commons.lang3.text.WordUtils 5 | 6 | class WordWrapper { 7 | 8 | DocumentFragment fragment 9 | int width 10 | 11 | WordWrapper(DocumentFragment documentFragment, int width) { 12 | this.fragment = documentFragment 13 | this.width = width 14 | } 15 | 16 | void setWidth(int width) { 17 | this.width = width 18 | } 19 | 20 | void justify() { 21 | if (!alreadyJustified()) { 22 | concatenateToOneLine() 23 | wrapWordsInLine(0) 24 | } 25 | } 26 | 27 | boolean alreadyJustified() { 28 | fragment.lines == wrapWords(fragment.lines.join(" ")) 29 | } 30 | 31 | private def concatenateToOneLine() { 32 | while (fragment.length > 1) { 33 | addSpaceBetweenLinesAsNewlineReplacement(0) 34 | fragment.mergeLineWithPrevious(1) 35 | } 36 | } 37 | 38 | private def wrapWordsInLine(int y) { 39 | def lines = wrapWords(fragment.lines[y]) 40 | for (int i = 0; i < lines.size() - 1; i++) { 41 | fragment.splitAt(lines[i].size() + 1, y + i) 42 | fragment.chop(y + i) 43 | } 44 | } 45 | 46 | private def addSpaceBetweenLinesAsNewlineReplacement(int y) { 47 | if (noWhiteSpaceAtEnd(y) || cursorAtEndOfLine(y)) fragment.appendTo_butDontMoveCursor(y, " ") 48 | } 49 | 50 | private boolean cursorAtEndOfLine(int y) { 51 | fragment.cursor == [x: fragment.lines[y].size(), y: y] 52 | } 53 | 54 | private boolean noWhiteSpaceAtEnd(int y) { 55 | return !fragment.lines[y].endsWith(" ") 56 | } 57 | 58 | private List wrapWords(String string) { 59 | if (string.startsWith(" ")) throw new UnsupportedOperationException("word wrapper does not behave nicely on strings starting with whitespace") 60 | StringUtils.splitPreserveAllTokens(WordUtils.wrap(string, width), '\n') 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/test/groovy/no/advide/commands/ProseCommandTest.groovy: -------------------------------------------------------------------------------- 1 | package no.advide.commands 2 | 3 | import no.advide.Document 4 | import no.advide.DocumentFragment 5 | import no.advide.ui.Theme 6 | 7 | class ProseCommandTest extends GroovyTestCase { 8 | 9 | ProseCommand command 10 | 11 | DocumentFragment createFragment(List lines) { 12 | new Document(lines, [x:0, y:0]).createFragment([x:0, y:0], lines.size()) 13 | } 14 | 15 | void setUpCommand(List lines) { 16 | command = new ProseCommand(createFragment(lines)) 17 | } 18 | 19 | void test_should_match_lines_starting_with_letters() { 20 | assert ProseCommand.matches(createFragment(["Hei"])) 21 | assert !ProseCommand.matches(createFragment(["#12"])) 22 | } 23 | 24 | void test_should_match_lines_starting_with_quote() { 25 | assert ProseCommand.matches(createFragment(['"Hei'])) 26 | assert !ProseCommand.matches(createFragment(['"'])) 27 | } 28 | 29 | void test_should_match_lines_starting_with_numbers() { 30 | assert ProseCommand.matches(createFragment(['13 baller'])) 31 | assert !ProseCommand.matches(createFragment(['13'])) 32 | } 33 | 34 | void test_should_request_all_continuous_lines_of_prose() { 35 | assert ProseCommand.numMatchingLines(createFragment(["Hei", "på", "deg", "!!!"])) == 3 36 | assert ProseCommand.numMatchingLines(createFragment(["Hei", "du"])) == 2 37 | } 38 | 39 | void test_should_stop_when_cursor_on_next_line() { 40 | def fragment = createFragment(["Hei", "på", "deg", "!!!"]) 41 | fragment.document.cursor = [x:1, y:2] 42 | assert ProseCommand.numMatchingLines(fragment) == 2 43 | } 44 | 45 | void test_should_color_lines() { 46 | setUpCommand(["Hei"]) 47 | assert command.formattedLines.first().color == Theme.prose 48 | } 49 | 50 | void test_should_optimize_document() { 51 | setUpCommand(["Hei", "på du"]) 52 | command.justifyProse(7) 53 | assert command.fragment.lines == ["Hei på", "du"] 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /src/main/groovy/no/advide/Room.groovy: -------------------------------------------------------------------------------- 1 | package no.advide 2 | 3 | class Room { 4 | final int number 5 | final File file 6 | List history 7 | int historyIndex = 0 8 | int originalIndex = 0 9 | 10 | Room() {} 11 | 12 | Room(int num, File f) { 13 | number = num 14 | file = f 15 | history = [[ 16 | lines: RoomConverter.toNewStyle(file.readLines('UTF-8')), 17 | cursor: [x:0, y:0, scrollTop:0] 18 | ]] 19 | } 20 | 21 | Document createDocument() { 22 | new Document(lines, cursor) 23 | } 24 | 25 | void update(Document document) { 26 | lines = [] + document.lines 27 | cursor = [x:document.cursor._x, y:document.cursor._y, scrollTop:document.cursor.scrollTop] 28 | } 29 | 30 | void save() { 31 | originalIndex = historyIndex 32 | file.setText(RoomConverter.toOldStyle(lines).join("\n"), 'UTF-8') 33 | } 34 | 35 | void restoreOriginal() { 36 | lines = original 37 | cursor = originalCursor 38 | } 39 | 40 | boolean isModified() { 41 | lines != original 42 | } 43 | 44 | String getName() { 45 | if (number == -1) "Notatblokk" 46 | else "Rom $number" 47 | } 48 | 49 | void setLines(l) { 50 | if (l != lines) { 51 | history = history.subList(0, historyIndex + 1) 52 | history << [lines: l, cursor: getCursor()] 53 | historyIndex += 1 54 | } 55 | } 56 | 57 | void setCursor(c) { 58 | history[historyIndex].cursor = c 59 | } 60 | 61 | def getCursor() { 62 | def c = history[historyIndex].cursor 63 | [x:c.x, y:c.y, scrollTop:c.scrollTop] 64 | } 65 | 66 | def getOriginalCursor() { 67 | def c = history[originalIndex].cursor 68 | [x:c.x, y:c.y, scrollTop:c.scrollTop] 69 | } 70 | 71 | List getLines() { 72 | [] + history[historyIndex].lines 73 | } 74 | 75 | List getOriginal() { 76 | [] + history[originalIndex].lines 77 | } 78 | 79 | void undo() { 80 | if (historyIndex > 0) historyIndex-- 81 | } 82 | 83 | void redo() { 84 | if (historyIndex < history.size() - 1) historyIndex++ 85 | } 86 | 87 | } 88 | -------------------------------------------------------------------------------- /src/main/groovy/no/advide/TextEditor.groovy: -------------------------------------------------------------------------------- 1 | package no.advide 2 | 3 | class TextEditor extends EventEmitter { 4 | Document document 5 | 6 | TextEditor(Document document) { 7 | this.document = document 8 | } 9 | 10 | def cursorMoves = [ 11 | "right": { document.cursor.right() }, 12 | "left": { document.cursor.left() }, 13 | "up": { document.cursor.up() }, 14 | "down": { document.cursor.down() } 15 | ] 16 | 17 | def actions = [ 18 | "enter": { enter() }, 19 | "backspace": { backspace() } 20 | ] 21 | 22 | def enter() { 23 | def indentation = currentIndentation() 24 | document.splitAt(document.cursor.x, document.cursor.y) 25 | document.insertAt(document.cursor.x, document.cursor.y, indentation) 26 | } 27 | 28 | String currentIndentation() { 29 | int x = 0 30 | String currentLine = document.lines[document.cursor.y] 31 | while (currentLine.startsWith(" ", x) || currentLine.startsWith("? ", x)) x += 2 32 | return " " * x 33 | } 34 | 35 | def backspace() { 36 | if (document.cursor.x != 0) { 37 | document.removeCharBefore(document.cursor.x, document.cursor.y) 38 | } else if (!document.cursor.y != 0) { 39 | document.mergeLineWithPrevious(document.cursor.y) 40 | } 41 | } 42 | 43 | def charTyped(c) { 44 | document.insertAt(document.cursor.x, document.cursor.y, c) 45 | changed() 46 | } 47 | 48 | def actionTyped(k) { 49 | checkActions(k) 50 | checkCursorMoves(k) 51 | } 52 | 53 | def checkActions(k) { 54 | def a = actions[k] 55 | if (a) { 56 | a.call() 57 | changed() 58 | } 59 | } 60 | 61 | def checkCursorMoves(k) { 62 | def a = cursorMoves[k] 63 | if (a) { 64 | a.call() 65 | cursorMoved() 66 | } 67 | } 68 | 69 | def onChange(callback) { 70 | on('change', callback) 71 | } 72 | 73 | def changed() { 74 | emit('change', document) 75 | } 76 | 77 | def onCursorMove(callback) { 78 | on('cursorMove', callback) 79 | } 80 | 81 | def cursorMoved() { 82 | emit('cursorMove', document) 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /src/main/groovy/no/advide/Application.groovy: -------------------------------------------------------------------------------- 1 | package no.advide 2 | 3 | import no.advide.ui.AppFrame 4 | import no.advide.ui.EditorPanel 5 | import no.advide.ui.KeyInterpreter 6 | 7 | class Application { 8 | 9 | public static final String NAME = "Adventur IDE" 10 | 11 | static main(args) { 12 | macify() 13 | Adventure.choose() 14 | new Application().open(Adventure.current.notes) 15 | } 16 | 17 | EditorPanel editorPanel 18 | AppFrame appFrame 19 | KeyInterpreter keys 20 | RoomHistory roomHistory 21 | 22 | Application() { 23 | editorPanel = new EditorPanel() 24 | appFrame = new AppFrame(editorPanel) 25 | keys = new KeyInterpreter(editorPanel) 26 | } 27 | 28 | void open(Room startingRoom) { 29 | roomHistory = new RoomHistory(startingRoom) 30 | roomChanged() 31 | keys.onAction "cmd+enter", { appFrame.toggleFullScreen() } 32 | appFrame.show() 33 | } 34 | 35 | Room getRoom() { 36 | roomHistory.current 37 | } 38 | 39 | void roomChanged() { 40 | def page = new Page(room.createDocument()) 41 | page.justifyProse() 42 | renderPage(page) 43 | updateAppFrame() 44 | waitForEvents(page) 45 | } 46 | 47 | private void updateAppFrame() { 48 | appFrame.setHeaderText("${Adventure.current.name} - ${room.name}${room.modified ? ' (endret)' : ''}") 49 | appFrame.setModified(room.modified) 50 | } 51 | 52 | private void renderPage(Page page) { 53 | editorPanel.updateContents(new PageFormatter(page, room.modified).formattedLines, page.cursor) 54 | } 55 | 56 | private void waitForEvents(Page page) { 57 | def editor = new RoomEditor(page, roomHistory) 58 | editor.onCursorMove { 59 | renderPage(page) 60 | updateAppFrame() 61 | waitForEvents(page) 62 | } 63 | editor.onDocumentChange { 64 | room.update(page.document) 65 | roomChanged() 66 | } 67 | editor.onRoomChange { 68 | if (roomHistory.empty()) System.exit(0) 69 | roomChanged() 70 | } 71 | keys.setListener(editor) 72 | } 73 | 74 | private static def macify() { 75 | System.setProperty("apple.laf.useScreenMenuBar", "true"); 76 | System.setProperty("com.apple.mrj.application.apple.menu.about.name", NAME) 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /src/main/groovy/no/advide/commands/CommandParser.groovy: -------------------------------------------------------------------------------- 1 | package no.advide.commands 2 | 3 | import no.advide.Document 4 | import no.advide.DocumentFragment 5 | 6 | class CommandParser { 7 | 8 | static List commandTypes = [ 9 | AlternativeCommand, 10 | ConditionalCommand, 11 | ContinueCommand, 12 | ProseCommand, 13 | ReinstateAlternativeCommand, 14 | RemoveAlternativeCommand, 15 | UnknownCommand 16 | ] 17 | 18 | List strings 19 | CommandList commands 20 | DocumentFragment document 21 | int index 22 | 23 | CommandParser(Document doc) { 24 | this(doc.createFragment([x:0, y:0], doc.lines.size())) 25 | } 26 | 27 | CommandParser(DocumentFragment doc) { 28 | document = doc 29 | strings = document.lines 30 | commands = new CommandList() 31 | index = 0 32 | if (fragmentTypeCache.size() > 100000) { fragmentTypeCache = [:] } // don't grow into the heavens 33 | } 34 | 35 | CommandList parse() { 36 | while (index < strings.size()) { 37 | commands << findMatchingCommand() 38 | } 39 | return commands 40 | } 41 | 42 | def static fragmentTypeCache = [:] 43 | 44 | private Command findMatchingCommand() { 45 | def fragment = document.createFragment([x:0, y:index], strings.size() - index) 46 | def type = findCommandType(fragment) 47 | return createCommand(type, fragment) 48 | } 49 | 50 | private def findCommandType(DocumentFragment fragment) { 51 | def lines = fragment.lines 52 | def cacheHit = fragmentTypeCache[lines] 53 | if (cacheHit) { 54 | return cacheHit 55 | } 56 | def type = scanForType(fragment) 57 | fragmentTypeCache[lines] = type 58 | return type 59 | } 60 | 61 | private def scanForType(DocumentFragment fragment) { 62 | for (type in commandTypes) { 63 | if (type.matches(fragment)) { 64 | return type 65 | } 66 | } 67 | throw new IllegalStateException("no matching commands") 68 | } 69 | 70 | private Command createCommand(type, fragment) { 71 | int numLines = type.numMatchingLines(fragment) 72 | fragment.length = numLines 73 | Command command = type.newInstance(fragment) 74 | index += numLines 75 | return command 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /src/test/groovy/no/advide/ui/KeyInterpreterTest.groovy: -------------------------------------------------------------------------------- 1 | package no.advide.ui 2 | 3 | import java.awt.event.KeyEvent 4 | import javax.swing.JPanel 5 | 6 | class KeyInterpreterTest extends GroovyTestCase { 7 | 8 | def listener 9 | def interpreter 10 | 11 | void setUp() { 12 | listener = new MockKeyListener() 13 | interpreter = new KeyInterpreter(new JPanel()) 14 | interpreter.setListener(listener) 15 | } 16 | 17 | void test_normal_key() { 18 | assertKeyRecevied "Char: T", new MockKeyEvent(keyChar: "T") 19 | } 20 | 21 | void test_norwegian_key() { 22 | assertKeyRecevied "Char: Ø", new MockKeyEvent(keyCode: 59, shift: true) 23 | } 24 | 25 | void test_curly_braces() { 26 | assertKeyRecevied "Char: {", new MockKeyEvent(keyCode: 56, alt: true, shift: true) 27 | } 28 | 29 | void test_action_key() { 30 | assertKeyRecevied "Action: enter", new MockKeyEvent(keyCode: 10) 31 | } 32 | 33 | void test_action_with_modifier() { 34 | assertKeyRecevied "Action: ctrl+tab", new MockKeyEvent(keyCode: 9, ctrl: true) 35 | } 36 | 37 | void test_pure_modifier() { 38 | assertKeyRecevied "Action: shift", new MockKeyEvent(keyCode: 16, shift: true) 39 | } 40 | 41 | void test_modified_modifier() { 42 | assertKeyRecevied "Action: ctrl+shift", new MockKeyEvent(keyCode: 16, shift: true, ctrl: true) 43 | } 44 | 45 | void test_modified_char() { 46 | assertKeyRecevied "Action: ctrl+shift+alt+cmd+D", new MockKeyEvent(keyCode: 68, ctrl: true, cmd: true, alt: true, shift: true) 47 | } 48 | 49 | void assertKeyRecevied(recevied, event) { 50 | interpreter.handleKeyPress(event) 51 | assert listener.received == recevied 52 | } 53 | 54 | static class MockKeyListener { 55 | def received 56 | void actionTyped(k) { received = "Action: ${k}" } 57 | void charTyped(c) { received = "Char: ${c}" } 58 | } 59 | 60 | static class MockKeyEvent extends KeyEvent { 61 | MockKeyEvent() { super(new JPanel(), 0, 0L, 0, 0, " ".charAt(0)) } 62 | 63 | def keyCode, keyChar, alt, shift, ctrl, cmd, action 64 | boolean isAltDown() { alt } 65 | boolean isShiftDown() { shift } 66 | boolean isControlDown() { ctrl } 67 | boolean isMetaDown() { cmd } 68 | boolean isActionKey() { action } 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /src/main/groovy/no/advide/ui/AppFrame.groovy: -------------------------------------------------------------------------------- 1 | package no.advide.ui 2 | 3 | import groovy.swing.SwingBuilder 4 | import java.awt.BorderLayout 5 | import java.awt.Dimension 6 | import java.awt.GraphicsDevice 7 | import java.awt.GraphicsEnvironment 8 | import java.awt.Toolkit 9 | import javax.swing.BorderFactory 10 | import javax.swing.BoxLayout 11 | import javax.swing.JFrame 12 | import javax.swing.JLabel 13 | import javax.swing.WindowConstants 14 | import no.advide.Application 15 | 16 | class AppFrame extends JFrame { 17 | 18 | EditorPanel editorPanel 19 | GraphicsDevice device 20 | Dimension screenSize 21 | JLabel header 22 | 23 | AppFrame(edPanel) { 24 | editorPanel = edPanel 25 | device = GraphicsEnvironment.localGraphicsEnvironment.defaultScreenDevice 26 | screenSize = Toolkit.getDefaultToolkit().getScreenSize() 27 | setupUI() 28 | } 29 | 30 | void setModified(modified) { 31 | rootPane.putClientProperty("Window.documentModified", modified) 32 | } 33 | 34 | void toggleFullScreen() { 35 | if (device.fullScreenSupported) { 36 | this.dispose() 37 | if (isFullScreen()) { exitFullScreen() } else { enterFullScreen() } 38 | this.show() 39 | } 40 | } 41 | 42 | void setHeaderText(text) { 43 | header.text = text 44 | } 45 | 46 | private void setupUI() { 47 | new SwingBuilder().edt { 48 | frame(this, title: Application.NAME, size: frameSize(), location: [50, 30], show: false, defaultCloseOperation: WindowConstants.DISPOSE_ON_CLOSE) { 49 | borderLayout() 50 | panel(Theme.panel, constraints: BorderLayout.CENTER, border: BorderFactory.createEmptyBorder(5, 10, 5, 10)) { 51 | boxLayout(axis: BoxLayout.Y_AXIS) 52 | panel(maximumSize: [1000, 150], minimumSize: [0, 150], opaque: false) { 53 | header = label(foreground: Theme.headerText) 54 | } 55 | panel(editorPanel, maximumSize: [1000, 2000], focusable: true, opaque: false) 56 | } 57 | } 58 | } 59 | } 60 | 61 | private def frameSize() { 62 | return [1120, screenSize.height.intValue() - 100] 63 | } 64 | 65 | private def enterFullScreen() { 66 | this.undecorated = true 67 | device.fullScreenWindow = this 68 | } 69 | 70 | private def exitFullScreen() { 71 | this.undecorated = false 72 | device.fullScreenWindow = null 73 | } 74 | 75 | private boolean isFullScreen() { 76 | return undecorated 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/test/groovy/no/advide/PageTest.groovy: -------------------------------------------------------------------------------- 1 | package no.advide 2 | 3 | class PageTest extends GroovyTestCase { 4 | 5 | def page 6 | 7 | void setUp() { 8 | AdventureTest.setUpCurrent() 9 | } 10 | 11 | Page getPage(int number) { 12 | new Page(Adventure.current.getRoom(number).createDocument()) 13 | } 14 | 15 | void test_should_know_next_room_number_after_cursor() { 16 | page = getPage(3) 17 | assert page.lines == ["#1", "", "#2", "#3"] 18 | 19 | page.cursor = [x:0, y:0] 20 | assert page.nextRoomNumber.number == 2 21 | 22 | page.cursor = [x:0, y:1] 23 | assert page.nextRoomNumber.number == 2 24 | 25 | page.cursor = [x:0, y:2] 26 | assert page.nextRoomNumber.number == 3 27 | 28 | page.cursor = [x:0, y:3] 29 | assert page.nextRoomNumber == null 30 | } 31 | 32 | void test_should_know_room_number_on_cursor() { 33 | page = getPage(3) 34 | assert page.lines == ["#1", "", "#2", "#3"] 35 | 36 | page.cursor = [x:0, y:0] 37 | assert page.currentRoomNumber.number == 1 38 | 39 | page.cursor = [x:0, y:1] 40 | assert page.currentRoomNumber == null 41 | } 42 | 43 | void test_should_know_target_room_number_for_jumping() { 44 | page = getPage(3) 45 | assert page.lines == ["#1", "", "#2", "#3"] 46 | 47 | page.cursor = [x:0, y:0] 48 | assert page.targetRoomNumber.number == 1 49 | 50 | page.cursor = [x:0, y:1] 51 | assert page.targetRoomNumber.number == 2 52 | 53 | page.cursor = [x:0, y:2] 54 | assert page.targetRoomNumber.number == 2 55 | 56 | page.cursor = [x:0, y:3] 57 | assert page.targetRoomNumber.number == 3 58 | } 59 | 60 | void test_should_know_previous_room_number_before_cursor() { 61 | page = getPage(3) 62 | assert page.lines == ["#1", "", "#2", "#3"] 63 | 64 | page.cursor = [x:0, y:0] 65 | assert page.previousRoomNumber == null 66 | 67 | page.cursor = [x:0, y:1] 68 | assert page.previousRoomNumber.number == 1 69 | 70 | page.cursor = [x:0, y:2] 71 | assert page.previousRoomNumber.number == 1 72 | 73 | page.cursor = [x:0, y:3] 74 | assert page.previousRoomNumber.number == 2 75 | } 76 | 77 | void test_next_fix_should_be_after_cursor() { 78 | page = getPage(2) 79 | page.changeToOldStyle() // ["Side 1", "!!!", "Side 2"] 80 | 81 | page.cursor = [x:0, y:0] 82 | assert page.nextFix.line == 1 83 | 84 | page.cursor = [x:0, y:2] 85 | assert page.nextFix == null 86 | } 87 | 88 | } 89 | -------------------------------------------------------------------------------- /src/main/groovy/no/advide/DocumentFragment.groovy: -------------------------------------------------------------------------------- 1 | package no.advide 2 | 3 | class DocumentFragment { 4 | 5 | Document document 6 | def offset 7 | int length 8 | 9 | DocumentFragment(offset, length, Document document) { 10 | this.document = document 11 | this.offset = offset 12 | this.length = length 13 | } 14 | 15 | def getCursor() { 16 | if (inside(document.cursor.y)) { [x: document.cursor.x - offset.x, y: document.cursor.y - offset.y] } 17 | } 18 | 19 | boolean inside(int y) { 20 | y - offset.y >= 0 && y - offset.y < length 21 | } 22 | 23 | List getLines() { 24 | document.lines[ offset.y..(offset.y + length - 1) ].collect { line -> line.substring((int)offset.x)} 25 | } 26 | 27 | void mergeLineWithPrevious(int y) { 28 | stripOutsideLeftFromLine(y) 29 | document.mergeLineWithPrevious(offset.y + y) 30 | } 31 | 32 | String getOutsideLeft(int y) { 33 | document.lines[y + offset.y].substring(0, (int)offset.x) 34 | } 35 | 36 | private void stripOutsideLeftFromLine(int y) { 37 | for (int i = 0; i < offset.x; i++) document.removeCharBefore(1, offset.y + y) 38 | } 39 | 40 | void appendTo(int y, String s) { 41 | document.insertAt(offset.x + lines[y].size(), offset.y + y, s) 42 | } 43 | 44 | void appendTo_butDontMoveCursor(int y, String s) { 45 | def old = [x: document.cursor._x, y: document.cursor._y] 46 | appendTo(y, s) 47 | document.cursor._x = old.x 48 | document.cursor._y = old.y 49 | } 50 | 51 | void splitAt(int x, int y) { 52 | document.splitAt(offset.x + x, offset.y + y) 53 | document.insertAt(0, offset.y + y + 1, getOutsideLeft(y)) 54 | } 55 | 56 | void chop(int y) { 57 | document.removeCharBefore(offset.x + lines[y].size(), offset.y + y) 58 | } 59 | 60 | void replaceWith(List strings) { 61 | for (int i = 0; i < strings.size() && i < length; i++) { 62 | document.replaceLine(offset.y + i, getOutsideLeft(0) + strings[i]) 63 | } 64 | while (length < strings.size()) { 65 | document.addLineAfter(offset.y + length - 1, getOutsideLeft(0) + strings[length]) 66 | } 67 | while (length > strings.size()) { 68 | if (cursor && cursor.y == strings.size()) document.cursor.up() 69 | document.removeLine(offset.y + strings.size()) 70 | } 71 | } 72 | 73 | def translate(cursor) { 74 | [x: cursor.x + offset.x, y: cursor.y + offset.y] 75 | } 76 | 77 | DocumentFragment createFragment(o, length) { 78 | document.createFragment([x:o.x + offset.x, y: o.y + offset.y], length) 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /src/main/groovy/no/advide/ui/LineRenderer.groovy: -------------------------------------------------------------------------------- 1 | package no.advide.ui 2 | 3 | import java.awt.Color 4 | import java.awt.FontMetrics 5 | import java.awt.Graphics2D 6 | import java.awt.Image 7 | import javax.swing.ImageIcon 8 | import no.advide.FormatChange 9 | import no.advide.FormattedLine 10 | 11 | class LineRenderer { 12 | int x, y 13 | Graphics2D g 14 | String text 15 | Map changes 16 | FontMetrics metrics 17 | FormattedLine line 18 | 19 | LineRenderer(FormattedLine line, int x, int y, Graphics2D g) { 20 | line.cementPrefix() 21 | changes = line.changes 22 | text = line.text 23 | this.line = line 24 | this.x = x 25 | this.y = y 26 | this.g = g 27 | metrics = g.getFontMetrics() 28 | } 29 | 30 | int index 31 | List colorStack = [ Theme.defaultFallbackColor ] 32 | 33 | void render() { 34 | index = 0 35 | renderFromIndex() 36 | if (line.icon) { 37 | Image img = new ImageIcon(ClassLoader.getSystemResource("${line.icon}.png")).image 38 | g.drawImage(img, 2, centerIconY(), null) 39 | } 40 | } 41 | 42 | private int centerIconY() { 43 | return y + (metrics.height / 2) - 9 44 | } 45 | 46 | void renderFromIndex() { 47 | apply(changes[index]) 48 | if (atLastChange()) { 49 | drawRemaining() 50 | } else { 51 | drawUpToNextChange() 52 | index = nextChangeIndex() 53 | renderFromIndex() 54 | } 55 | } 56 | 57 | private def drawRemaining() { 58 | draw(text.substring(index)) 59 | } 60 | 61 | void drawUpToNextChange() { 62 | draw(text.substring(index, nextChangeIndex())) 63 | } 64 | 65 | boolean atLastChange() { 66 | index == changes.keySet().max() 67 | } 68 | 69 | int nextChangeIndex() { 70 | changes.keySet().findAll { it > index }.min() 71 | } 72 | 73 | void draw(String s) { 74 | g.drawString(s, x, y + metrics.ascent) 75 | x += metrics.stringWidth(s) 76 | } 77 | 78 | void apply(FormatChange change) { 79 | if (change.prefix) { g.setColor(change.prefix.color); draw(change.prefix.text) } 80 | if (change.revertColorChange) { colorStack.pop() } 81 | if (change.changeColor) { colorStack.push(change.changeColor) } 82 | if (change.highlight) { highlightNextChars(change.highlight.length, change.highlight.color) } 83 | g.setColor(colorStack.last()) 84 | } 85 | 86 | private def highlightNextChars(length, color) { 87 | g.setColor(color) 88 | g.fillRect(x, y, metrics.stringWidth(nextChars(length)), metrics.height) 89 | } 90 | 91 | private String nextChars(length) { 92 | return text.substring(index, (int) index + length) 93 | } 94 | 95 | 96 | } 97 | -------------------------------------------------------------------------------- /src/main/groovy/no/advide/ui/KeyInterpreter.groovy: -------------------------------------------------------------------------------- 1 | package no.advide.ui 2 | 3 | import java.awt.event.KeyEvent 4 | import javax.swing.JComponent 5 | 6 | class KeyInterpreter { 7 | def listeners = [] 8 | def actionCallbacks = [:] 9 | 10 | void setListener(l) { 11 | listeners = [l] 12 | } 13 | 14 | void onAction(action, callback) { 15 | if (!actionCallbacks[action]) actionCallbacks[action] = [] 16 | actionCallbacks[action] << callback 17 | } 18 | 19 | KeyInterpreter(JComponent component) { 20 | component.keyPressed = { KeyEvent e -> handleKeyPress(e) } 21 | } 22 | 23 | def handleKeyPress(KeyEvent e) { 24 | if (modifier(e)) return notifyAction(modifier(e)) 25 | if (specialChar(e)) return notifyChar(specialChar(e)) 26 | if (!isActionPress(e)) return notifyChar(e.keyChar) 27 | def keys = currentModifiers(e) + pressedKey(e) 28 | notifyAction(keys.join("+")) 29 | } 30 | 31 | def pressedKey(KeyEvent e) { 32 | return actionKey(e) ? actionKey(e) : KeyEvent.getKeyText(e.keyCode) 33 | } 34 | 35 | def isActionPress(KeyEvent e) { 36 | return e.isControlDown() || e.isAltDown() || e.isMetaDown() || e.isActionKey() || actionKey(e) 37 | } 38 | 39 | def notifyChar(c) { 40 | listeners.each { it.charTyped c.toString() } 41 | } 42 | 43 | def notifyAction(a) { 44 | listeners.each { it.actionTyped a } 45 | if (actionCallbacks[a]) 46 | actionCallbacks[a].each { it.call() } 47 | } 48 | 49 | def modifier(KeyEvent e) { 50 | return e.keyCode in [16, 17, 18, 157] ? currentModifiers(e).join("+") : null 51 | } 52 | 53 | def currentModifiers(KeyEvent e) { 54 | def keys = [] 55 | if (e.isControlDown()) keys << "ctrl" 56 | if (e.isShiftDown()) keys << "shift" 57 | if (e.isAltDown()) keys << "alt" 58 | if (e.isMetaDown()) keys << "cmd" 59 | return keys 60 | } 61 | 62 | def actionKey(KeyEvent e) { 63 | switch (e.keyCode) { 64 | case 8: return "backspace" 65 | case 9: return "tab" 66 | case 10: return "enter" 67 | case 37: return "left" 68 | case 40: return "down" 69 | case 39: return "right" 70 | case 38: return "up" 71 | case 27: return "escape" 72 | } 73 | } 74 | 75 | def specialChar(KeyEvent e) { 76 | if (e.isAltDown() && e.keyCode == 55) return e.isShiftDown() ? "\\" : "|" 77 | if (e.isAltDown() && e.keyCode == 56) return e.isShiftDown() ? "{" : "[" 78 | if (e.isAltDown() && e.keyCode == 57) return e.isShiftDown() ? "}" : "]" 79 | switch (e.keyCode) { 80 | case 59: return e.isShiftDown() ? "Ø" : "ø" 81 | case 222: return e.isShiftDown() ? "Æ" : "æ" 82 | case 91: return e.isShiftDown() ? "Å" : "å" 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/main/groovy/no/advide/ui/Theme.groovy: -------------------------------------------------------------------------------- 1 | package no.advide.ui 2 | 3 | import java.awt.Color 4 | import javax.swing.JPanel 5 | 6 | class Theme { 7 | 8 | /** 9 | // papir 10 | static panel = new BackgroundPanel() 11 | static headerText = new Color(150, 150, 140) 12 | 13 | static prose = new Color(0, 0, 0) 14 | static command = new Color(90, 90, 80) 15 | static prefix = prose 16 | 17 | static cursorHighlight = new Color(0, 0, 0, 4) 18 | static cursor = Color.black 19 | 20 | static roomExists = command 21 | static roomDoesntExist = Color.red 22 | static roomHighlight = new Color(0, 0, 150, 20) 23 | 24 | static brackets = command 25 | static alternatives = prose 26 | static altArrow = new Color(0, 0, 0, 60) 27 | static separatorLine = new Color(150, 150, 100, 30) 28 | 29 | static embossedTop = new Color(0, 0, 0, 30) 30 | static embossed = new Color(0, 0, 0, 7) 31 | static embossedBottom = new Color(255, 255, 255, 150) 32 | 33 | static defaultFallbackColor = command 34 | /**/ 35 | 36 | /**/ 37 | // svart 38 | static panel = new JPanel(background: Color.black) 39 | static headerText = new Color(90, 90, 90) 40 | 41 | static prose = new Color(150, 150, 150); 42 | static command = new Color(90, 90, 90) 43 | static prefix = prose 44 | 45 | static cursorHighlight = new Color(0, 0, 0, 0) 46 | static cursor = Color.white 47 | 48 | static roomExists = command 49 | static roomDoesntExist = new Color(210, 30, 30) 50 | static roomHighlight = new Color(30, 30, 70) 51 | 52 | static brackets = command 53 | static alternatives = prose 54 | static altArrow = new Color(60, 60, 60) 55 | static separatorLine = new Color(60, 60, 60) 56 | 57 | static embossedTop = new Color(20, 20, 50) 58 | static embossed = new Color(100, 100, 255, 25) 59 | static embossedBottom = embossedTop 60 | 61 | static defaultFallbackColor = command 62 | /**/ 63 | 64 | /* 65 | // gamle editor 66 | final static Color COL_BACKGROUND = Color.black; 67 | final static Color COL_FOREGROUND = new Color(150, 150, 150); 68 | 69 | final Color COL_KOMMANDO = new Color(90, 90, 90); 70 | final Color COL_DELKOMMANDO = new Color(120, 120, 120); 71 | final Color COL_ANTALL_ALTERNATIVER = new Color(150, 150, 150); 72 | final Color COL_ALTERNATIVER = new Color(130, 130, 130); 73 | final Color COL_UTILGJENGELIG = new Color(210, 30, 30); 74 | final Color COL_TING = new Color(255, 255, 255); 75 | final Color COL_BRUKT = new Color(100, 100, 210); 76 | final Color COL_UKJENT = new Color(210, 30, 30); 77 | final Color COL_COMMENT = new Color(60, 60, 160); 78 | final Color COL_SPESIELL = new Color(170, 170, 255); 79 | final Color COL_ANNET_EVENTYR = new Color(210, 100, 210); 80 | final Color COL_TYPO = new Color(255, 90, 90); 81 | final Color COL_ACTIVE_TYPO = new Color(255, 130, 130); 82 | final Color COL_HIGHLIGHT = new Color(255, 255, 130); 83 | */ 84 | 85 | } 86 | -------------------------------------------------------------------------------- /src/main/groovy/no/advide/Cursor.groovy: -------------------------------------------------------------------------------- 1 | package no.advide 2 | 3 | class Cursor { 4 | int _x 5 | int _y 6 | private List lines 7 | def scrollTop 8 | 9 | Cursor(List lines, x, y, scrollTop) { 10 | this.lines = lines 11 | this._x = x 12 | this._y = y 13 | this.scrollTop = scrollTop 14 | } 15 | 16 | Cursor(List lines, x, y) { 17 | this(lines, x, y, 0) 18 | } 19 | 20 | boolean at(LinkedHashMap coords) { 21 | coords.x == x && coords.y == y 22 | } 23 | 24 | boolean atLine(y) { 25 | getY() == y 26 | } 27 | 28 | int getX() { 29 | Math.min(_x, maxX) 30 | } 31 | 32 | int getY() { 33 | _y = Math.min(_y, maxY) 34 | _y 35 | } 36 | 37 | void anchor() { 38 | _x = getX() 39 | _y = getY() 40 | } 41 | 42 | void right() { 43 | if (!atEndOfLine()) { 44 | _x = x + 1 45 | } else if (!atLastLine()) { 46 | _y = y + 1 47 | _x = 0 48 | } 49 | } 50 | 51 | void left() { 52 | if (!atStartOfLine()) { 53 | _x = x - 1 54 | } else if (!atFirstLine()) { 55 | _y = y - 1 56 | _x = maxX 57 | } 58 | } 59 | 60 | void down() { 61 | if (!atLastLine()) { 62 | _y = y + 1 63 | } else if (!atEndOfLine()) { 64 | _x = maxX 65 | } 66 | } 67 | 68 | void up() { 69 | if (!atFirstLine()) { 70 | _y = y - 1 71 | } else if (!atStartOfLine()) { 72 | _x = 0 73 | } 74 | } 75 | 76 | void allRight() { 77 | _x = maxX 78 | } 79 | 80 | void allLeft() { 81 | _x = 0 82 | } 83 | 84 | void allDown() { 85 | _y = maxY 86 | } 87 | 88 | void allUp() { 89 | _y = 0 90 | } 91 | 92 | private boolean atFirstLine() { 93 | getY() == 0 94 | } 95 | 96 | private boolean atStartOfLine() { 97 | getX() == 0 98 | } 99 | 100 | private boolean atLastLine() { 101 | getY() == getMaxY() 102 | } 103 | 104 | private boolean atEndOfLine() { 105 | getX() == getMaxX() 106 | } 107 | 108 | private int getMaxX() { 109 | return width 110 | } 111 | 112 | private int getMaxY() { 113 | return height - 1 114 | } 115 | 116 | private int getHeight() { 117 | return lines.size() 118 | } 119 | 120 | private int getWidth() { 121 | return currentLine.size() 122 | } 123 | 124 | private String getCurrentLine() { 125 | return lines[y] 126 | } 127 | 128 | @Override 129 | String toString() { 130 | return "[x:$x,y:$y]" 131 | } 132 | 133 | @Override 134 | boolean equals(Object obj) { 135 | obj.x == x && obj.y == y 136 | } 137 | 138 | int calculateScrollTop(height) { 139 | if (y >= height + scrollTop) { 140 | scrollTop = y - height + 1 141 | } 142 | if (scrollTop >= y) { 143 | scrollTop = y - 1 144 | } 145 | scrollTop = Math.max(0, (int)scrollTop) 146 | return scrollTop 147 | } 148 | 149 | } 150 | -------------------------------------------------------------------------------- /src/test/groovy/no/advide/DocumentFragmentTest.groovy: -------------------------------------------------------------------------------- 1 | package no.advide 2 | 3 | class DocumentFragmentTest extends GroovyTestCase { 4 | 5 | Document document 6 | DocumentFragment fragment 7 | 8 | void setUp() { 9 | document = new Document(["outside", "outside", "inside 1", "inside 2", "inside 3", "outside"], [x:0, y:0]) 10 | fragment = document.createFragment([x:0, y:2], 3) 11 | } 12 | 13 | void test_should_get_lines_from_document() { 14 | assert fragment.lines == ["inside 1", "inside 2", "inside 3"] 15 | document.lines[2] = "changed" 16 | assert fragment.lines == ["changed", "inside 2", "inside 3"] 17 | } 18 | 19 | void test_should_combine_lines() { 20 | fragment.mergeLineWithPrevious(1) 21 | assert fragment.lines == ["inside 1inside 2", "inside 3"] 22 | assert document.lines == ["outside", "outside", "inside 1inside 2", "inside 3", "outside"] 23 | } 24 | 25 | void test_should_update_y_when_outside_contracts() { 26 | document.mergeLineWithPrevious(1) 27 | assert fragment.lines == ["inside 1", "inside 2", "inside 3"] 28 | } 29 | 30 | void test_should_append_to_line() { 31 | fragment.appendTo(0, " ") 32 | assert fragment.lines == ["inside 1 ", "inside 2", "inside 3"] 33 | } 34 | 35 | void test_should_split_line() { 36 | fragment.splitAt(2, 0) 37 | assert fragment.lines == ["in", "side 1", "inside 2", "inside 3"] 38 | } 39 | 40 | void test_should_update_y_when_outside_expands() { 41 | document.splitAt(2, 0) 42 | assert fragment.lines == ["inside 1", "inside 2", "inside 3"] 43 | } 44 | 45 | void test_should_chop_line() { 46 | fragment.chop(2) 47 | assert fragment.lines == ["inside 1", "inside 2", "inside "] 48 | } 49 | 50 | void test_should_have_relative_cursor() { 51 | document.cursor = [x:0, y:3] 52 | assert fragment.cursor == [x:0, y:1] 53 | document.cursor.up() 54 | assert fragment.cursor == [x:0, y:0] 55 | } 56 | 57 | void test_should_have_no_cursor_when_outside() { 58 | assert fragment.cursor == null 59 | } 60 | 61 | void test_inside() { 62 | assert !fragment.inside(1) 63 | assert fragment.inside(2) 64 | assert fragment.inside(4) 65 | assert !fragment.inside(5) 66 | } 67 | 68 | void test_should_replace_with_fewer_lines() { 69 | document.cursor = [x:0, y:4] 70 | fragment.replaceWith(["new"]) 71 | assert document.lines == ["outside", "outside", "new", "outside"] 72 | assert fragment.lines == ["new"] 73 | assert document.cursor == [x:0, y:2] 74 | } 75 | 76 | void test_should_replace_with_more_lines() { 77 | document.cursor = [x:0, y:4] 78 | fragment.replaceWith(["1", "2", "3", "4"]) 79 | assert document.lines == ["outside", "outside", "1", "2", "3", "4", "outside"] 80 | assert fragment.lines == ["1", "2", "3", "4"] 81 | assert document.cursor == [x:0, y:4] 82 | } 83 | 84 | void test_should_translate_cursor() { 85 | assert fragment.translate([x:2, y:2]) == [x:2, y:4] 86 | } 87 | 88 | } 89 | -------------------------------------------------------------------------------- /src/test/groovy/no/advide/WordWrapperTest.groovy: -------------------------------------------------------------------------------- 1 | package no.advide 2 | 3 | class WordWrapperTest extends GroovyTestCase { 4 | 5 | Document document 6 | DocumentFragment fragment 7 | WordWrapper wrapper 8 | 9 | void setUpFormatter(List lines) { 10 | document = new Document(lines, [x:0, y:0]) 11 | fragment = document.createFragment([x:0, y:0], lines.size()) 12 | wrapper = new WordWrapper(fragment, 80) 13 | } 14 | 15 | void test_should_concatenate_lines() { 16 | setUpFormatter(["Hei", "du"]) 17 | wrapper.justify() 18 | assert document.lines == ["Hei du"] 19 | } 20 | 21 | void test_should_avoid_double_spaces() { 22 | setUpFormatter(["Hei ", "du"]) 23 | wrapper.justify() 24 | assert document.lines == ["Hei du"] 25 | } 26 | 27 | void test_should_wrap_long_lines() { 28 | setUpFormatter(["En ganske lang linje gitt width"]) 29 | wrapper.width = 15 30 | wrapper.justify() 31 | assert document.lines == ["En ganske lang", "linje gitt", "width"] 32 | } 33 | 34 | void test_should_allow_cursor_to_be_placed_at_end_of_wrapped_lines() { 35 | def lines = ["Det er noe snålt som skjer med cursoren når den når slutten av en linje som", "splittes"] 36 | setUpFormatter(lines) 37 | document.cursor = [x:75, y:0] 38 | wrapper.justify() 39 | assert document.lines == lines 40 | assert document.cursor == [x:75, y:0] 41 | } 42 | 43 | void test_should_place_cursor_correctly_when_word_unwraps() { 44 | setUpFormatter(["En ganske", "lang linje"]) 45 | document.cursor = [x:9, y:0] 46 | wrapper.width = 15 47 | wrapper.justify() 48 | assert document.lines == ["En ganske lang", "linje"] 49 | assert document.cursor == [x:9, y:0] 50 | } 51 | 52 | void test_full_line_when_press_space_should_end_up_on_new_blank_line() { 53 | setUpFormatter(["En full linje"]) 54 | wrapper.width = 13 55 | document.cursor = [x:13, y:0] 56 | document.insertAt(document.cursor.x, document.cursor.y, " ") 57 | wrapper.justify() 58 | assert document.lines == ["En full linje", ""] 59 | assert document.cursor == [x:0, y:1] 60 | } 61 | 62 | void test_trailing_space_before_cursor_shouldnt_be_used_as_newline() { 63 | setUpFormatter(["En overfull", "linje"]) 64 | wrapper.width = 13 65 | document.cursor = [x:11, y:0] 66 | document.insertAt(document.cursor.x, document.cursor.y, " ") 67 | wrapper.justify() 68 | assert document.lines == ["En overfull ", "linje"] 69 | assert document.cursor == [x:12, y:0] 70 | } 71 | 72 | void test_shouldnt_be_confused_by_fragments_with_x_offset() { 73 | def lines = [" En full linje"] 74 | 75 | document = new Document(lines, [x:15, y:0]) 76 | fragment = document.createFragment([x:2, y:0], 1) 77 | wrapper = new WordWrapper(fragment, 13) 78 | 79 | document.insertAt(document.cursor.x, document.cursor.y, " ") 80 | wrapper.justify() 81 | assert document.lines == [" En full linje", " "] 82 | assert document.cursor == [x:2, y:1] 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /src/test/groovy/no/advide/DocumentFragmentWithXTest.groovy: -------------------------------------------------------------------------------- 1 | package no.advide 2 | 3 | class DocumentFragmentWithXTest extends GroovyTestCase { 4 | 5 | Document document 6 | DocumentFragment fragment 7 | 8 | void setUp() { 9 | document = new Document(["outside", "outside", " inside 1", " inside 2", " inside 3", "outside"], [x:0, y:0]) 10 | fragment = document.createFragment([x:2, y:2], 3) 11 | } 12 | 13 | void test_should_get_lines_from_document() { 14 | assert fragment.lines == ["inside 1", "inside 2", "inside 3"] 15 | document.lines[2] = " changed" 16 | assert fragment.lines == ["changed", "inside 2", "inside 3"] 17 | } 18 | 19 | void test_should_combine_lines() { 20 | fragment.mergeLineWithPrevious(1) 21 | assert fragment.lines == ["inside 1inside 2", "inside 3"] 22 | assert document.lines == ["outside", "outside", " inside 1inside 2", " inside 3", "outside"] 23 | } 24 | 25 | void test_should_update_y_when_outside_contracts() { 26 | document.mergeLineWithPrevious(1) 27 | assert fragment.lines == ["inside 1", "inside 2", "inside 3"] 28 | } 29 | 30 | void test_should_append_to_line() { 31 | fragment.appendTo(0, "x") 32 | assert fragment.lines == ["inside 1x", "inside 2", "inside 3"] 33 | } 34 | 35 | void test_should_split_line() { 36 | fragment.splitAt(2, 0) 37 | assert fragment.lines == ["in", "side 1", "inside 2", "inside 3"] 38 | } 39 | 40 | void test_should_update_y_when_outside_expands() { 41 | document.splitAt(2, 0) 42 | assert fragment.lines == ["inside 1", "inside 2", "inside 3"] 43 | } 44 | 45 | void test_should_chop_line() { 46 | fragment.chop(2) 47 | assert fragment.lines == ["inside 1", "inside 2", "inside "] 48 | } 49 | 50 | void test_should_have_relative_cursor() { 51 | document.cursor = [x:2, y:3] 52 | assert fragment.cursor == [x:0, y:1] 53 | document.cursor.up() 54 | assert fragment.cursor == [x:0, y:0] 55 | } 56 | 57 | void test_should_have_no_cursor_when_outside() { 58 | assert fragment.cursor == null 59 | } 60 | 61 | void test_inside() { 62 | assert !fragment.inside(1) 63 | assert fragment.inside(2) 64 | assert fragment.inside(4) 65 | assert !fragment.inside(5) 66 | } 67 | 68 | void test_should_replace_with_fewer_lines() { 69 | document.cursor = [x:2, y:4] 70 | fragment.replaceWith(["new"]) 71 | assert document.lines == ["outside", "outside", " new", "outside"] 72 | assert fragment.lines == ["new"] 73 | assert document.cursor == [x:2, y:2] 74 | } 75 | 76 | void test_should_replace_with_more_lines() { 77 | document.cursor = [x:2, y:4] 78 | fragment.replaceWith(["1", "2", "3", "4"]) 79 | assert document.lines == ["outside", "outside", " 1", " 2", " 3", " 4", "outside"] 80 | assert fragment.lines == ["1", "2", "3", "4"] 81 | assert document.cursor == [x:3, y:4] // this can be fixed with TranslatedCursor 82 | } 83 | 84 | void test_should_translate_cursor() { 85 | assert fragment.translate([x:2, y:2]) == [x:4, y:4] 86 | } 87 | 88 | } 89 | -------------------------------------------------------------------------------- /src/main/groovy/no/advide/Document.groovy: -------------------------------------------------------------------------------- 1 | package no.advide 2 | 3 | import antlr.StringUtils 4 | 5 | class Document { 6 | List lines 7 | Cursor cursor 8 | 9 | Document(List lines, coords) { 10 | this.lines = lines 11 | this.cursor = new Cursor(lines, coords.x, coords.y, coords.scrollTop) 12 | } 13 | 14 | void setCursor(LinkedHashMap coords) { 15 | this.cursor._x = coords.x 16 | this.cursor._y = coords.y 17 | } 18 | 19 | void removeCharBefore(Integer x, Integer y) { 20 | cursor.anchor() 21 | if (cursor.y == y && cursor.x >= x) cursor.left() 22 | 23 | def pre = lines[y].substring(0, x - 1) 24 | def post = lines[y].substring(x) 25 | lines[y] = pre + post 26 | } 27 | 28 | void mergeLineWithPrevious(Integer y) { 29 | cursor.anchor() 30 | if (cursor.y == y) cursor._x += lines[y - 1].size() 31 | if (cursor.y >= y) cursor.up() 32 | 33 | fragmentsAt(y).each { it.length -= 1 } 34 | fragmentsAfter(y).each { it.offset.y -= 1 } 35 | 36 | lines[y - 1] += lines[y] 37 | lines.remove(y) 38 | } 39 | 40 | void splitAt(Integer x, Integer y) { 41 | def pre = lines[y].substring(0, x) 42 | def post = lines[y].substring(x) 43 | def split = [pre, post] 44 | 45 | cursor.anchor() 46 | if (cursor.y == y && cursor.x >= x) { cursor._y += 1; cursor._x -= pre.size() } 47 | else if (cursor.y > y) cursor.down() 48 | 49 | fragmentsAt(y).each { it.length += 1 } 50 | fragmentsAfter(y).each { it.offset.y += 1 } 51 | 52 | lines.remove(y) 53 | lines.addAll(y, split) 54 | } 55 | 56 | void removeLine(Integer y) { 57 | cursor.anchor() 58 | if (cursor.y == y) cursor.allLeft() 59 | if (cursor.y > y) cursor.up() 60 | 61 | lines.remove(y) 62 | 63 | fragmentsAt(y).each { it.length -= 1 } 64 | fragmentsAfter(y).each { it.offset.y -= 1 } 65 | } 66 | 67 | void replaceLine(Integer y, String string) { 68 | lines[y] = string 69 | cursor.anchor() 70 | if (cursor.y == y && cursor.x > 0) cursor.allRight() 71 | } 72 | 73 | void addLineAfter(Integer y, String s) { 74 | cursor.anchor() 75 | if (cursor.y > y) cursor.down() 76 | 77 | lines.add(y + 1, s) 78 | 79 | fragmentsAt(y).each { it.length += 1 } 80 | fragmentsAfter(y).each { it.offset.y += 1 } 81 | } 82 | 83 | void insertAt(Integer x, Integer y, s) { 84 | cursor.anchor() 85 | if (cursor.y == y && cursor.x >= x) cursor._x += s.size() 86 | def pre = lines[y].substring(0, x) 87 | def post = lines[y].substring(x) 88 | lines[y] = pre + s + post 89 | } 90 | 91 | void stripTrailingSpaces() { 92 | lines.eachWithIndex { line, i -> 93 | lines[i] = StringUtils.stripBack(line, " ") 94 | } 95 | } 96 | 97 | List fragments = [] 98 | 99 | DocumentFragment createFragment(offset, length) { 100 | def f = new DocumentFragment(offset, length, this) 101 | fragments << f 102 | return f 103 | } 104 | 105 | private List fragmentsAt(int y) { 106 | fragments.findAll { it.inside(y) } 107 | } 108 | 109 | private List fragmentsAfter(int y) { 110 | fragments.findAll { it.offset.y > y } 111 | } 112 | 113 | } 114 | -------------------------------------------------------------------------------- /src/main/groovy/no/advide/ui/DocumentRenderer.groovy: -------------------------------------------------------------------------------- 1 | package no.advide.ui 2 | 3 | import java.awt.FontMetrics 4 | import java.awt.Graphics2D 5 | import no.advide.Cursor 6 | import no.advide.FormattedLine 7 | 8 | class DocumentRenderer { 9 | 10 | private static final int LEFT_PADDING = 20 11 | 12 | def render() { 13 | int startIndex = cursor.calculateScrollTop(maxLines - 1) 14 | int numLinesToRender = maxLines + 1 // want the overflow line to show partially 15 | int stopIndex = startIndex + numLinesToRender 16 | for (int i = startIndex; i < stopIndex && i < lines.size(); i++) { 17 | renderLine(i, lines[i]) 18 | y = y + fontHeight 19 | } 20 | } 21 | 22 | private int getMaxLines() { 23 | g.clipBounds.height / fontHeight 24 | } 25 | 26 | private def renderLine(i, FormattedLine line) { 27 | if (cursor.atLine(i)) renderCursorIndicatorBar() 28 | if (line.isEmbossedTop) renderEmbossedTop() 29 | if (line.isEmbossed) renderEmbossed() 30 | if (line.isEmbossedBottom) renderEmbossedBottom() 31 | if (line.hasSeparatorLine) renderSeparatorLine(line) 32 | renderText(line) 33 | if (cursor.atLine(i)) renderCursor(line) 34 | } 35 | 36 | private def renderEmbossedTop() { 37 | g.setColor(Theme.embossedTop) 38 | g.drawLine(0, y, componentWidth, y) 39 | } 40 | 41 | private def renderEmbossed() { 42 | g.setColor(Theme.embossed) 43 | g.fillRect(0, y, componentWidth, fontHeight) 44 | } 45 | 46 | private def renderEmbossedBottom() { 47 | g.setColor(Theme.embossedBottom) 48 | g.drawLine(0, y + fontHeight, componentWidth, y + fontHeight) 49 | } 50 | 51 | private def renderSeparatorLine(line) { 52 | int centerY = 1 + y + fontHeight / 2 53 | int endOfText = LEFT_PADDING + fontMetrics.stringWidth(line.text) + prefixWidth(line) 54 | g.setColor(Theme.separatorLine) 55 | g.drawRect(endOfText, centerY, componentWidth - endOfText, 1) 56 | } 57 | 58 | private def renderCursor(FormattedLine line) { 59 | g.setColor(Theme.cursor) 60 | g.drawRect LEFT_PADDING + cursorX(line), y, 1, fontHeight - 1 61 | } 62 | 63 | private int prefixWidth(line) { 64 | return line.prefix ? fontMetrics.stringWidth(line.prefix) : 0 65 | } 66 | 67 | private int cursorX(FormattedLine line) { 68 | int prefix_shift = line.prefix && cursor.x >= line.prefixPosition ? prefixWidth(line) : 0 69 | return prefix_shift + fontMetrics.stringWidth(line.text.substring(0, (int)cursor.x)) 70 | } 71 | 72 | private def renderText(FormattedLine line) { 73 | new LineRenderer(line, LEFT_PADDING, y, g).render() 74 | } 75 | 76 | private def renderCursorIndicatorBar() { 77 | g.setColor(Theme.cursorHighlight) 78 | g.fillRect 0, y, componentWidth, fontHeight - 1 79 | } 80 | 81 | def lines 82 | int componentWidth 83 | Cursor cursor 84 | Graphics2D g 85 | FontMetrics fontMetrics 86 | int ascent 87 | int fontHeight 88 | int y 89 | 90 | DocumentRenderer(layout, width, graphics) { 91 | g = graphics 92 | lines = layout.lines 93 | cursor = layout.cursor 94 | componentWidth = width 95 | y = 0 96 | fontMetrics = g.getFontMetrics() 97 | ascent = fontMetrics.getAscent() 98 | fontHeight = ascent + fontMetrics.getDescent() 99 | } 100 | 101 | } 102 | -------------------------------------------------------------------------------- /src/test/groovy/no/advide/DocumentTest.groovy: -------------------------------------------------------------------------------- 1 | package no.advide 2 | 3 | class DocumentTest extends GroovyTestCase { 4 | 5 | Document document 6 | 7 | void setUp() { 8 | document = new Document(["abc", "def", "ghi", "jkl"], [x:0, y:0]) 9 | } 10 | 11 | void test_mergeLineWithPrevious_preserves_cursor_position() { 12 | document.cursor = [x:0, y:3] 13 | document.mergeLineWithPrevious(2) 14 | assert document.cursor == [x:0, y:2] 15 | } 16 | 17 | void test_mergeLineWithPrevious_preserves_cursor_position_even_when_in_merging_lines() { 18 | document.cursor = [x:2, y:2] 19 | document.mergeLineWithPrevious(2) 20 | assert document.cursor == [x:5, y:1] 21 | } 22 | 23 | void test_insertAt() { 24 | document.insertAt(1, 0, "in") 25 | assert document.lines[0] == "ainbc" 26 | assert document.cursor == [x:0, y:0] 27 | } 28 | 29 | void test_insertAt_moves_cursor_if_after() { 30 | document.insertAt(0, 0, "in") 31 | assert document.lines[0] == "inabc" 32 | assert document.cursor == [x:2, y:0] 33 | } 34 | 35 | void test_splitAt_moves_cursor_down_when_on_line_below() { 36 | document.cursor = [x:0, y:2] 37 | document.splitAt(1, 0) 38 | assert document.lines == ["a", "bc", "def", "ghi", "jkl"] 39 | assert document.cursor == [x:0, y:3] 40 | } 41 | 42 | void test_splitAt_preserves_cursor_position_when_after_split_in_the_line() { 43 | document.cursor = [x:2, y:1] 44 | document.splitAt(2, 1) 45 | assert document.lines == ["abc", "de", "f", "ghi", "jkl"] 46 | assert document.cursor == [x:0, y:2] 47 | } 48 | 49 | void test_splitAt_holds_cursor_when_before_split_in_the_line() { 50 | document.cursor = [x:1, y:1] 51 | document.splitAt(2, 1) 52 | assert document.lines == ["abc", "de", "f", "ghi", "jkl"] 53 | assert document.cursor == [x:1, y:1] 54 | } 55 | 56 | void test_removeCharBefore_cursor_follows_left_when_after() { 57 | document.cursor = [x:2, y:3] 58 | document.removeCharBefore(2, 3) 59 | assert document.lines == ["abc", "def", "ghi", "jl"] 60 | assert document.cursor == [x:1, y:3] 61 | } 62 | 63 | void test_removeCharBefore_cursor_stays_put_when_before() { 64 | document.cursor = [x:1, y:3] 65 | document.removeCharBefore(2, 3) 66 | assert document.lines == ["abc", "def", "ghi", "jl"] 67 | assert document.cursor == [x:1, y:3] 68 | } 69 | 70 | void test_removeLine_moves_cursor_to_x_0_if_on_same_line() { 71 | document.cursor = [x:2, y:2] 72 | document.removeLine(2) 73 | assert document.lines == ["abc", "def", "jkl"] 74 | assert document.cursor == [x:0, y:2] 75 | } 76 | 77 | void test_removeLine_ensures_cursor_y_is_inside() { 78 | document.cursor = [x:2, y:3] 79 | document.removeLine(3) 80 | assert document.lines == ["abc", "def", "ghi"] 81 | assert document.cursor == [x:0, y:2] 82 | } 83 | 84 | void test_removeLine_moves_cursor_up_if_after() { 85 | document.cursor = [x:2, y:3] 86 | document.removeLine(1) 87 | assert document.lines == ["abc", "ghi", "jkl"] 88 | assert document.cursor == [x:2, y:2] 89 | } 90 | 91 | void test_replaceLine_moves_cursor_to_end() { 92 | document.cursor = [x:2, y:2] 93 | document.replaceLine(2, "heisann") 94 | assert document.lines == ["abc", "def", "heisann", "jkl"] 95 | assert document.cursor == [x:7, y:2] 96 | } 97 | 98 | void test_replaceLine_keeps_cursor_at_start() { 99 | document.cursor = [x:0, y:2] 100 | document.replaceLine(2, "heisann") 101 | assert document.lines == ["abc", "def", "heisann", "jkl"] 102 | assert document.cursor == [x:0, y:2] 103 | } 104 | 105 | 106 | } 107 | -------------------------------------------------------------------------------- /src/main/groovy/no/advide/commands/BlockCommand.groovy: -------------------------------------------------------------------------------- 1 | package no.advide.commands 2 | 3 | import no.advide.DocumentFragment 4 | import no.advide.Fix 5 | import no.advide.FormattedLine 6 | import no.advide.RoomNumber 7 | import no.advide.ui.Theme 8 | 9 | abstract class BlockCommand extends Command { 10 | 11 | static int numMatchingLinesNewForm(DocumentFragment fragment) { 12 | int size = fragment.lines.size() 13 | for (int i = 1; i < size; i++) { 14 | if (!fragment.lines[i].startsWith(" ")) return i 15 | } 16 | return size 17 | } 18 | 19 | static int numMatchingLinesOldForm(DocumentFragment fragment) { 20 | int size = fragment.lines.size() 21 | int numBlocks = 0 22 | for (int i = 1; i < size; i++) { 23 | if (fragment.lines[i] == "{") numBlocks++ 24 | if (fragment.lines[i] == "}") numBlocks-- 25 | if (numBlocks == 0) return i + 1 26 | } 27 | return size 28 | } 29 | 30 | CommandList commands 31 | 32 | BlockCommand(fragment) { 33 | super(fragment) 34 | commands = parseCommands() 35 | } 36 | 37 | CommandList parseCommands() { 38 | if (fragment.length == 1) return [] 39 | new CommandParser(commandFragment).parse() 40 | } 41 | 42 | DocumentFragment getCommandFragment() { 43 | int x = isOldForm() ? 0 : 2 44 | if (bracketed) { 45 | fragment.createFragment([x:x, y:2], fragment.length - 3) // chop off [!], {, } 46 | } else { 47 | fragment.createFragment([x:x, y:1], fragment.length - 1) // chop off [!] or ? 48 | } 49 | } 50 | 51 | boolean isBracketed() { 52 | return fragment.lines[1] == "{" 53 | } 54 | 55 | abstract boolean isOldForm() 56 | abstract boolean isNewForm() 57 | 58 | @Override 59 | List getFixes() { 60 | myFixes() + subcommandFixes() 61 | } 62 | 63 | private def subcommandFixes() { 64 | (List) commands.collect { it.fixes }.flatten() 65 | } 66 | 67 | private def myFixes() { 68 | newForm ? [] : [new Fix(fragment.offset.y, { replaceWithNewStyle() })] 69 | } 70 | 71 | @Override 72 | List getFormattedLines() { 73 | def lines = [new FormattedLine(text: fragment.lines[0], color: color)] 74 | lines = addFormattedLinesForCommands(lines) 75 | if (bracketed) addBrackets(lines) 76 | if (fragment.cursor) emboss(lines) 77 | (List) lines 78 | } 79 | 80 | private def addBrackets(lines) { 81 | lines.add(1, new FormattedLine(text: "{", color: Theme.brackets)) 82 | if (fragment.lines.last() == "}") lines << new FormattedLine(text: "}", color: Theme.brackets) 83 | } 84 | 85 | private def addFormattedLinesForCommands(lines) { 86 | def commandLines = commands.collect { it.formattedLines }.flatten() 87 | if (isNewForm()) commandLines.each { it.text = " ${it.text}"; it.prefixPosition += 2 } 88 | lines << commandLines 89 | lines.flatten() 90 | } 91 | 92 | private def emboss(lines) { 93 | lines.first().isEmbossedTop = true 94 | lines.each { it.isEmbossed = true } 95 | lines.last().isEmbossedBottom = true 96 | } 97 | 98 | @Override 99 | List getRoomNumbers() { 100 | (List) commands.collect { it.roomNumbers }.flatten() 101 | } 102 | 103 | @Override 104 | void justifyProse(int width) { 105 | commands.each { it.justifyProse(width - 2) } 106 | } 107 | 108 | List toNewStyle(firstLine) { 109 | def lines = [firstLine] 110 | commands.collect { it.toNewStyle() }.flatten().each { lines << " $it" } 111 | lines 112 | } 113 | 114 | List toOldStyle(firstLine) { 115 | def lines = [firstLine] 116 | if (commands.empty) { 117 | lines << "" 118 | } else { 119 | if (oldStyleNeedsBrackets()) lines << "{" 120 | commands.collect { it.toOldStyle() }.flatten().each { lines << it } 121 | if (oldStyleNeedsBrackets()) lines << "}" 122 | } 123 | lines 124 | } 125 | 126 | private boolean oldStyleNeedsBrackets() { 127 | return commands.size() > 1 || (commands.size() > 0 && commands.first().toOldStyle().size() > 1) 128 | } 129 | 130 | } 131 | -------------------------------------------------------------------------------- /src/main/groovy/no/advide/commands/AlternativeCommand.groovy: -------------------------------------------------------------------------------- 1 | package no.advide.commands 2 | 3 | import java.awt.Color 4 | import no.advide.Alternative 5 | import no.advide.DocumentFragment 6 | import no.advide.FormattedLine 7 | import no.advide.RoomNumber 8 | import no.advide.ui.Theme 9 | 10 | class AlternativeCommand extends Command { 11 | 12 | AlternativeCommand(fragment) { 13 | super(fragment) 14 | } 15 | 16 | static boolean matches(DocumentFragment fragment) { 17 | matchesOldStyle(fragment) || matchesNewStyle(fragment) 18 | } 19 | 20 | private static def matchesOldStyle(DocumentFragment fragment) { 21 | fragment.lines.size() > 1 && 22 | fragment.lines[0] in ["-", "+"] && 23 | fragment.lines[1] =~ /^ ?\d+$/ 24 | } 25 | 26 | private static def matchesNewStyle(DocumentFragment fragment) { 27 | fragment.lines.first() == "--" 28 | } 29 | 30 | static int numMatchingLines(DocumentFragment fragment) { 31 | fragment.lines.size() 32 | } 33 | 34 | @Override 35 | List getRoomNumbers() { 36 | alternatives.collect { 37 | if (it.room?.isInteger()) new RoomNumber(number: it.room.toInteger(), position: fragment.translate([x:0, y:it.index + 1])) 38 | }.findAll { it != null } 39 | } 40 | 41 | @Override 42 | Color getColor() { 43 | Theme.alternatives 44 | } 45 | 46 | @Override 47 | List getFormattedLines() { 48 | def lines = super.getFormattedLines() 49 | matchesNewStyle(fragment) ? formatNewStyle(lines) : formatOldStyle(lines) 50 | } 51 | 52 | private def formatOldStyle(List lines) { 53 | lines[1].prefix = "Antall alternativer: " 54 | 55 | alternatives.each { alt -> 56 | lines[alt.index].prefix = "${alt.number}. " 57 | lines[alt.index + 1]?.prefix = "Rom#: " 58 | if (alt.requirement) lines[alt.index + 2]?.prefix = "Krav: " 59 | } 60 | lines 61 | } 62 | 63 | private def formatNewStyle(List lines) { 64 | def extraSpace = alternatives.size() >= 10 ? " " : "" 65 | alternatives.each { alt -> 66 | if (alt.number == 10) extraSpace = "" 67 | lines[alt.index].prefix = "${extraSpace}${alt.number}. " 68 | lines[alt.index + 1]?.prefix = "--> " 69 | lines[alt.index + 1]?.prefixColor = Theme.altArrow 70 | } 71 | lines 72 | } 73 | 74 | @Override 75 | List toNewStyle() { 76 | (["--"] + alternatives.collect { it.toNewStyle() }).flatten() 77 | } 78 | 79 | @Override 80 | List toOldStyle() { 81 | if (alternatives.any { it.hasRequirement() }) { 82 | (["+", alternatives.size().toString()] + alternatives.collect { it.toOldStyle_WithRequirements() }).flatten() 83 | } else { 84 | (["-", alternatives.size().toString()] + alternatives.collect { it.toOldStyle_NoRequirements() }).flatten() 85 | } 86 | } 87 | 88 | List getAlternatives() { 89 | matchesNewStyle(fragment) ? getNewStyleAlternatives() : getOldStyleAlternatives() 90 | } 91 | 92 | List getOldStyleAlternatives() { 93 | def alternatives = [] 94 | def lines = fragment.lines 95 | int linesBetweenAlternatives = lines.first() == "+" ? 3 : 2 96 | for (int i = 2; i < lines.size(); i += linesBetweenAlternatives) { 97 | alternatives << new Alternative( 98 | index: i, 99 | number: ((i-2)/linesBetweenAlternatives) + 1, 100 | text: lines[i], 101 | room: lines[i+1], 102 | requirement: linesBetweenAlternatives == 3 ? lines[i+2] : "-" 103 | ) 104 | } 105 | return alternatives 106 | } 107 | 108 | List getNewStyleAlternatives() { 109 | def alternatives = [] 110 | def lines = fragment.lines 111 | int linesBetweenAlternatives = 2 112 | for (int i = 1; i < lines.size(); i += linesBetweenAlternatives) { 113 | alternatives << new Alternative( 114 | index: i, 115 | number: ((i-1)/2) + 1, 116 | text: lines[i], 117 | room: lines[i+1]?.split(" ? ")?.first(), 118 | requirement: lines[i+1]?.contains(" ? ") ? lines[i+1]?.split(" ? ")?.last() : "-" 119 | ) 120 | } 121 | return alternatives 122 | } 123 | 124 | } 125 | -------------------------------------------------------------------------------- /src/test/groovy/no/advide/CursorTest.groovy: -------------------------------------------------------------------------------- 1 | package no.advide 2 | 3 | class CursorTest extends GroovyTestCase { 4 | 5 | void test_should_move_right_within_boundaries() { 6 | def cursor = new Cursor([" "], 0, 0) 7 | cursor.right() 8 | assert cursor.at(x:1, y:0) 9 | cursor.right() 10 | assert cursor.at(x:1, y:0) 11 | } 12 | 13 | void test_should_wrap_when_moving_right() { 14 | def cursor = new Cursor([" ", ""], 0, 0) 15 | cursor.right() 16 | cursor.right() 17 | assert cursor.at(x:0, y:1) 18 | } 19 | 20 | void test_should_move_left_within_boundaries() { 21 | def cursor = new Cursor([" "], 0, 0) 22 | cursor.right() 23 | cursor.left() 24 | assert cursor.at(x:0, y:0) 25 | cursor.left() 26 | assert cursor.at(x:0, y:0) 27 | } 28 | 29 | void test_should_wrap_when_moving_left() { 30 | def cursor = new Cursor([" ", ""], 0, 0) 31 | cursor.down() 32 | cursor.left() 33 | assert cursor.at(x:1, y:0) 34 | } 35 | 36 | void test_should_move_down_within_boundaries() { 37 | def cursor = new Cursor(["", ""], 0, 0) 38 | cursor.down() 39 | assert cursor.at(x:0, y:1) 40 | cursor.down() 41 | assert cursor.at(x:0, y:1) 42 | } 43 | 44 | void test_should_wrap_when_moving_down() { 45 | def cursor = new Cursor(["", " "], 0, 0) 46 | cursor.down() 47 | cursor.down() 48 | assert cursor.at(x:1, y:1) 49 | } 50 | 51 | void test_should_move_up_within_boundaries() { 52 | def cursor = new Cursor(["", ""], 0, 0) 53 | cursor.down() 54 | cursor.up() 55 | assert cursor.at(x:0, y:0) 56 | cursor.up() 57 | assert cursor.at(x:0, y:0) 58 | } 59 | 60 | void test_should_wrap_when_moving_up() { 61 | def cursor = new Cursor([" "], 0, 0) 62 | cursor.right() 63 | cursor.up() 64 | assert cursor.at(x:0, y:0) 65 | } 66 | 67 | void test_should_adjust_y_if_somehow_outside() { 68 | def cursor = new Cursor(["", ""], 0, 0) 69 | cursor._y = 99 70 | assert cursor.at(x:0, y:1) 71 | cursor.up() 72 | assert cursor.at(x:0, y:0) 73 | } 74 | 75 | void test_should_account_for_adjusted_y_if_moved_while_somehow_outside() { 76 | def cursor = new Cursor(["", ""], 0, 0) 77 | cursor._y = 99 78 | cursor.up() 79 | assert cursor.at(x:0, y:0) 80 | } 81 | 82 | void test_should_move_to_dummy_x_if_outside() { 83 | def cursor = new Cursor(["long line", "short", "long line"], 0, 0) 84 | cursor._x = 9 85 | cursor.down() 86 | assert cursor.at(x:5, y:1) 87 | cursor.down() 88 | assert cursor.at(x:9, y:2) 89 | } 90 | 91 | void test_should_accept_dummy_x_value_if_moved_horizontally() { 92 | def cursor = new Cursor(["long line", "short", "long line"], 0, 0) 93 | cursor._x = 9 94 | cursor.down() 95 | cursor.left() 96 | assert cursor.at(x:4, y:1) 97 | } 98 | 99 | void test_should_move_all_right() { 100 | def cursor = new Cursor(["abc"], 0, 0) 101 | cursor.allRight() 102 | assert cursor.at(x:3, y:0) 103 | } 104 | 105 | void test_should_move_all_left() { 106 | def cursor = new Cursor(["abc"], 0, 0) 107 | cursor.allRight() 108 | cursor.allLeft() 109 | assert cursor.at(x:0, y:0) 110 | } 111 | 112 | void test_should_move_all_down() { 113 | def cursor = new Cursor(["", "", ""], 0, 0) 114 | cursor.allDown() 115 | assert cursor.at(x:0, y:2) 116 | } 117 | 118 | void test_should_move_all_up() { 119 | def cursor = new Cursor(["", "", ""], 0, 0) 120 | cursor.allDown() 121 | cursor.allUp() 122 | assert cursor.at(x:0, y:0) 123 | } 124 | 125 | void test_should_move_scrollTop_down_if_below_height_and_stay() { 126 | def cursor = new Cursor(["", "", "", "", ""], 0, 5, 0) 127 | assert cursor.calculateScrollTop(3) == 2 128 | cursor.up() 129 | assert cursor.calculateScrollTop(3) == 2 130 | } 131 | 132 | void test_should_move_scrollTop_up_if_under_cursor() { 133 | def cursor = new Cursor(["", "", "", "", ""], 0, 0, 2) 134 | assert cursor.calculateScrollTop(3) == 0 135 | cursor.down() 136 | assert cursor.calculateScrollTop(3) == 0 137 | } 138 | 139 | void test_should_move_scrollTop_down_so_line_under_cursor_shows() { 140 | def cursor = new Cursor(["", "", "", "", ""], 0, 3, 0) 141 | assert cursor.calculateScrollTop(3) == 1 142 | } 143 | 144 | void test_should_move_scrollTop_up_so_line_over_cursor_shows() { 145 | def cursor = new Cursor(["", "", "", "", ""], 0, 2, 2) 146 | assert cursor.calculateScrollTop(3) == 1 147 | } 148 | 149 | } 150 | -------------------------------------------------------------------------------- /src/test/groovy/no/advide/commands/ConditionalCommandTest.groovy: -------------------------------------------------------------------------------- 1 | package no.advide.commands 2 | 3 | import no.advide.Document 4 | import no.advide.DocumentFragment 5 | import no.advide.ui.Theme 6 | 7 | class ConditionalCommandTest extends GroovyTestCase { 8 | 9 | DocumentFragment createFragment(List lines) { 10 | new Document(lines, [x:0, y:0]).createFragment([x:0, y:0], lines.size()) 11 | } 12 | 13 | void test_should_match_old_form() { 14 | assert ConditionalCommand.matches(createFragment(["[!]KRAV", "abc", "def"])) 15 | assert !ConditionalCommand.matches(createFragment(["[X!]4", ""])) 16 | } 17 | 18 | void test_should_match_two_lines_without_brackets() { 19 | assert ConditionalCommand.numMatchingLines(createFragment(["[!]KRAV", "abc", "def"])) == 2 20 | } 21 | 22 | void test_should_match_entire_block_with_brackets() { 23 | assert ConditionalCommand.numMatchingLines(createFragment(["[!]KRAV", "{", "abc", "def", "}", "outside"])) == 5 24 | } 25 | 26 | void test_should_not_get_confused_by_nested_blocks() { 27 | assert ConditionalCommand.numMatchingLines(createFragment(["[!]KRAV", "{", "[!]TMP", "{", "abc", "}", "def", "}", "outside"])) == 8 28 | } 29 | 30 | void test_should_get_room_numbers_from_subcommands() { 31 | def command = new ConditionalCommand(createFragment(["[!]KRAV", "#123"])) 32 | assert command.roomNumbers.size() == 1 33 | } 34 | 35 | void test_should_color_brackets_according_to_theme() { 36 | def command = new ConditionalCommand(createFragment(["[!]KRAV", "{", "abc", "}"])) 37 | assert command.formattedLines[1].text == "{" 38 | assert command.formattedLines[1].color == Theme.brackets 39 | assert command.formattedLines[3].text == "}" 40 | assert command.formattedLines[3].color == Theme.brackets 41 | } 42 | 43 | void test_should_emboss_entire_block() { 44 | def command = new ConditionalCommand(createFragment(["[!]KRAV", "{", "abc", "}"])) 45 | assert command.formattedLines.first().isEmbossedTop 46 | assert command.formattedLines[0].isEmbossed 47 | assert command.formattedLines[1].isEmbossed 48 | assert command.formattedLines[2].isEmbossed 49 | assert command.formattedLines[3].isEmbossed 50 | assert command.formattedLines.last().isEmbossedBottom 51 | } 52 | 53 | // New form ////////////////////////////////////////////////////////////////////////////////////////////////////////// 54 | 55 | void test_should_match_new_form() { 56 | assert ConditionalCommand.matches(createFragment(["? KRAV", " abc", "def"])) 57 | assert !ConditionalCommand.matches(createFragment(["@12 ? KRAV", ""])) 58 | } 59 | 60 | void test_should_match_indented_lines() { 61 | assert ConditionalCommand.numMatchingLines(createFragment(["? KRAV", "abc", "def"])) == 1 62 | assert ConditionalCommand.numMatchingLines(createFragment(["? KRAV", " abc", "def"])) == 2 63 | assert ConditionalCommand.numMatchingLines(createFragment(["? KRAV", " abc", " def"])) == 3 64 | } 65 | 66 | void test_should_get_room_numbers_from_subcommands_in_new_form_too() { 67 | def command = new ConditionalCommand(createFragment(["? KRAV", " #123"])) 68 | assert command.roomNumbers.size() == 1 69 | assert command.roomNumbers.first().number == 123 70 | assert command.roomNumbers.first().position == [x:3, y:1] 71 | } 72 | 73 | void test_should_keep_indentation_in_new_form() { 74 | def command = new ConditionalCommand(createFragment(["? KRAV", " abc", " #123"])) 75 | assert command.formattedLines.first().text == "? KRAV" 76 | assert command.formattedLines.last().text == " #123" 77 | } 78 | 79 | void test_should_accept_new_form_without_commands() { 80 | def command = new ConditionalCommand(createFragment(["? KRAV"])) 81 | assert command.commands.size() == 0 82 | } 83 | 84 | // Switching //////////// 85 | 86 | void test_should_convert_to_new_style() { 87 | def command = new ConditionalCommand(createFragment(["[!]KRAV", "{", "abc", "def", "!!!", "}"])) 88 | assert command.toNewStyle() == ["? KRAV", " abc", " def", " -- fortsett --"] 89 | } 90 | 91 | void test_should_convert_to_old_style() { 92 | def command = new ConditionalCommand(createFragment(["? KRAV", " abc", " -- fortsett --"])) 93 | assert command.toOldStyle() == ["[!]KRAV", "{", "abc", "!!!", "}"] 94 | } 95 | 96 | void test_should_avoid_unneccessary_braces_in_old_style() { 97 | def command = new ConditionalCommand(createFragment(["? KRAV", " -- fortsett --"])) 98 | assert command.toOldStyle() == ["[!]KRAV", "!!!"] 99 | } 100 | 101 | void test_should_use_brackets_for_single_command_with_multiple_lines_in_old_style() { 102 | def command = new ConditionalCommand(createFragment(["? KRAV", " ? NESTED", " abc"])) 103 | assert command.toOldStyle() == ["[!]KRAV", "{", "[!]NESTED", "abc", "}"] 104 | } 105 | 106 | void test_invalid_new_style_should_add_blank_line_to_avoid_corrupting_old_lines() { 107 | def command = new ConditionalCommand(createFragment(["? KRAV"])) 108 | assert command.toOldStyle() == ["[!]KRAV", ""] 109 | } 110 | 111 | void test_should_get_fixes_also_for_subcommands() { 112 | def command = new ConditionalCommand(createFragment(["[!]KRAV", "!!!", "}"])) 113 | assert command.fixes.size() == 2 114 | } 115 | 116 | } 117 | -------------------------------------------------------------------------------- /src/test/groovy/no/advide/TextEditorTest.groovy: -------------------------------------------------------------------------------- 1 | package no.advide 2 | 3 | class TextEditorTest extends GroovyTestCase { 4 | 5 | TextEditor editor 6 | Document document 7 | 8 | def setUpEditor(lines, cursor) { 9 | document = new Document(lines, cursor) 10 | editor = new TextEditor(document) 11 | } 12 | 13 | void test_when_empty_arrows_does_nothing() { 14 | setUpEditor([""], [x:0, y:0]) 15 | 16 | editor.actionTyped("up") 17 | assert document.cursor == [x:0, y:0] 18 | editor.actionTyped("right") 19 | assert document.cursor == [x:0, y:0] 20 | editor.actionTyped("down") 21 | assert document.cursor == [x:0, y:0] 22 | editor.actionTyped("left") 23 | assert document.cursor == [x:0, y:0] 24 | } 25 | 26 | void test_right_arrow_moves_right_until_end() { 27 | setUpEditor(["yo"], [x:0, y:0]) 28 | 29 | editor.actionTyped("right") 30 | assert document.cursor == [x:1, y:0] 31 | editor.actionTyped("right") 32 | assert document.cursor == [x:2, y:0] 33 | editor.actionTyped("right") 34 | assert document.cursor == [x:2, y:0] 35 | } 36 | 37 | void test_right_arrow_goes_down_on_end() { 38 | setUpEditor(["", ""], [x:0, y:0]) 39 | 40 | editor.actionTyped("right") 41 | assert document.cursor == [x:0, y:1] 42 | } 43 | 44 | void test_left_arrow_moves_left_until_start() { 45 | setUpEditor(["yo"], [x:2, y:0]) 46 | 47 | editor.actionTyped("left") 48 | assert document.cursor == [x:1, y:0] 49 | editor.actionTyped("left") 50 | assert document.cursor == [x:0, y:0] 51 | editor.actionTyped("left") 52 | assert document.cursor == [x:0, y:0] 53 | } 54 | 55 | void test_left_arrow_moves_up_on_start() { 56 | setUpEditor(["yo", ""], [x:0, y:1]) 57 | 58 | editor.actionTyped("left") 59 | assert document.cursor == [x:2, y:0] 60 | } 61 | 62 | void test_up_arrow_moves_up_until_start() { 63 | setUpEditor(["", "", ""], [x:0, y:2]) 64 | 65 | editor.actionTyped("up") 66 | assert document.cursor == [x:0, y:1] 67 | editor.actionTyped("up") 68 | assert document.cursor == [x:0, y:0] 69 | editor.actionTyped("up") 70 | assert document.cursor == [x:0, y:0] 71 | } 72 | 73 | void test_up_arrow_moves_left_on_start() { 74 | setUpEditor(["yo"], [x:2, y:0]) 75 | 76 | editor.actionTyped("up") 77 | assert document.cursor == [x:0, y:0] 78 | } 79 | 80 | void test_up_arrow_adjust_x_for_length() { 81 | setUpEditor(["yo", "dude"], [x:4, y:1]) 82 | 83 | editor.actionTyped("up") 84 | assert document.cursor == [x:2, y:0] 85 | } 86 | 87 | void test_down_arrow_moves_down_until_end() { 88 | setUpEditor(["", "", ""], [x:0, y:0]) 89 | 90 | editor.actionTyped("down") 91 | assert document.cursor == [x:0, y:1] 92 | editor.actionTyped("down") 93 | assert document.cursor == [x:0, y:2] 94 | editor.actionTyped("down") 95 | assert document.cursor == [x:0, y:2] 96 | } 97 | 98 | void test_down_arrow_moves_right_on_end() { 99 | setUpEditor(["yo"], [x:0, y:0]) 100 | 101 | editor.actionTyped("down") 102 | assert document.cursor == [x:2, y:0] 103 | } 104 | 105 | void test_down_arrow_adjust_x_for_length() { 106 | setUpEditor(["hei", "du"], [x:3, y:0]) 107 | 108 | editor.actionTyped("down") 109 | assert document.cursor == [x:2, y:1] 110 | } 111 | 112 | void test_char_typed_start() { 113 | setUpEditor(["ei"], [x:0, y:0]) 114 | editor.charTyped("H") 115 | assert document.lines == ["Hei"] 116 | assert document.cursor == [x:1, y:0] 117 | } 118 | 119 | void test_char_typed_middle() { 120 | setUpEditor(["Hi"], [x:1, y:0]) 121 | editor.charTyped("e") 122 | assert document.lines == ["Hei"] 123 | assert document.cursor == [x:2, y:0] 124 | } 125 | 126 | void test_char_typed_end() { 127 | setUpEditor(["He"], [x:2, y:0]) 128 | editor.charTyped("i") 129 | assert document.lines == ["Hei"] 130 | assert document.cursor == [x:3, y:0] 131 | } 132 | 133 | void test_enter_splits_line() { 134 | setUpEditor(["Føretter"], [x:3, y:0]) 135 | editor.actionTyped("enter") 136 | assert document.lines == ["Før", "etter"] 137 | assert document.cursor == [x:0, y:1] 138 | } 139 | 140 | void test_enter_knows_about_new_style_indentation_starters() { 141 | setUpEditor(["? KRAV"], [x:6, y:0]) 142 | editor.actionTyped("enter") 143 | assert document.lines == ["? KRAV", " "] 144 | assert document.cursor == [x:2, y:1] 145 | } 146 | 147 | void test_enter_keeps_indentation() { 148 | setUpEditor([" Føretter"], [x:5, y:0]) 149 | editor.actionTyped("enter") 150 | assert document.lines == [" Før", " etter"] 151 | assert document.cursor == [x:2, y:1] 152 | } 153 | 154 | void test_enter_keeps_deep_indentation() { 155 | setUpEditor([" Føretter"], [x:7, y:0]) 156 | editor.actionTyped("enter") 157 | assert document.lines == [" Før", " etter"] 158 | assert document.cursor == [x:4, y:1] 159 | } 160 | 161 | void test_backspace_removes_char() { 162 | setUpEditor(["Hei"], [x:2, y:0]) 163 | editor.actionTyped("backspace") 164 | assert document.lines, ["Hi"] 165 | assert document.cursor == [x:1, y:0] 166 | } 167 | 168 | void test_backspace_joins_line() { 169 | setUpEditor(["H", "ei"], [x:0, y:1]) 170 | editor.actionTyped("backspace") 171 | assert document.lines == ["Hei"] 172 | assert document.cursor == [x:1, y:0] 173 | } 174 | 175 | void test_should_callback_when_changed() { 176 | setUpEditor([""], [x:0, y:0]) 177 | def called = false 178 | def passed = null 179 | editor.onChange { doc -> 180 | called = true 181 | passed = doc 182 | } 183 | editor.changed() 184 | assert called 185 | assert passed == document 186 | } 187 | 188 | void test_should_anchor_cursor_before_changing_document_to_avoid_strange_jumps_when_hidden_x_is_different_from_apparent_x() { 189 | setUpEditor(["abc", "", ""], [x:3, y:0]) 190 | document.cursor.down() 191 | editor.charTyped("b") 192 | assert document.lines == ["abc", "b", ""] 193 | assert document.cursor == [x:1, y:1] 194 | } 195 | 196 | } 197 | -------------------------------------------------------------------------------- /tanker.txt: -------------------------------------------------------------------------------- 1 | 2 | Alternativene trenger refaktorering. De må antagelig bli mer selvstendige domeneobjekter. Eksempelvis trenger en Page å vite 3 | om det er alternativer på siden. 4 | 5 | -> min tanke nå er å kaste alternativ-koden og starte på nytt. Gjøre AlternativeCommand mye 6 | mindre, og at Alternative blir rike, og også får sine egne DocumentFragments. 7 | 8 | -------------------- 9 | 10 | Interessante utfordringer som gjenstår: 11 | * Krav 12 | * Selection 13 | * Muspeker 14 | * Skrivefeil 15 | * Autocomplete 16 | 17 | -------------------- 18 | 19 | Krav: 20 | Forskjell på å gi krav, fjerne krav og bruke krav. 21 | Autocomplete midt i setningen. 22 | 23 | -------------------- 24 | 25 | Selection: 26 | Cursor får en selectionOrigin 27 | Selection bruker Document med sin lines og Cursor 28 | - har selectionStart og selectionEnd, der den ene er cursor (avhengig av om Cursor er først eller sist) 29 | PageEditor og RoomEditor fjerner selection ved endring. 30 | TextEditor har ctrl+c/v/x og fikser alt med selection. 31 | PageRenderer tar selection og formatterer (husk å fjerne annen formattering på linjene) 32 | 33 | -------------------- 34 | 35 | Muspeker: 36 | Regn ut hvilken linje vha fontHeight 37 | Regn ut hvilket tegn vha fontMetrics --> cursor-posisjon! No problem. 38 | Klikke på lyspærer og blyanter er temmelig enkelt etter det. 39 | Select er kjipere, med drag og greier. 40 | 41 | -------------------- 42 | 43 | * Husk at Prose i gammel form skal være på en lang linje. 44 | Hmm ... [C] og alternativer kan selv si ifra til Prose om å rendre seg som én lang linje, de andre kan fortsette å bruke prose som før. 45 | 46 | * Prose takler for øyeblikket dårlig lange linjer fra gammel editor som inneholder ekstra spacing for å få 80-char linjer 47 | 48 | * Husk ´ og é i KeyInterpreter 49 | 50 | * Kjenn kobling mellom )(VINDSKROLL og ()BRUKT:VINDSKROLL 51 | 52 | -------------------- 53 | 54 | Jeg har hatt litt problemer med å se prosessen. Slik er den: 55 | 56 | 1. Det finnes et Room med tekstlinjer med en cursor. Dette er utgangspunktet og starten på flyten. 57 | 2. Room må vises fram. Hvordan? Det vet Page, ved hjelp av Commands. Linjene parses til Commands. 58 | 3. Commands blir bedt om å rendre seg selv. De lager formatterte linjer som matcher linjene i dokumentet. 59 | 4. De formatterte linjene blir rendret. 60 | 5. Editoren lytter etter endringer. Når de kommer så gjøres endringer i tekstdokumentet. Dette via Document, som har ansvar for å at cursor-posisjon er bra. 61 | 6. Endringene fra Document går til Room, som så starter prosessen på nytt. 62 | 63 | Altså, skrevet med litt andre ord: 64 | 65 | 1. Vent på Event. (kan være endring fra TextEditor, PageEditor eller Application (ved første åpning)) 66 | 2. Eventen sender et Room. Den har name, file, lines og cursor. Observer at dette er enkle objecter. String, File, List, [int, int] 67 | 3. En Page lages ut fra Room. Den oppretter Document som brukes til å lage Commands, for å holde oversikt. 68 | 4. Page brukes av PageFormatter til å lage formattere linjer for rendring. 69 | 5. TextEditor (vha Document) og PageEditor (vha Page) tolker tastetrykkene som kommer og gjør endringer. 70 | 6. Room får oppdatert sin lines og cursor. 71 | 7. Endring rapporteres. 72 | 73 | ------------------ 74 | 75 | Commands vet alt om kommandoene i Adventur. De kan rendre seg som ny og som gammel, og vet hvor de kan finne 76 | romnummer og krav. De endrer ikke på dokumenter uten å bli bedt om det. 77 | De kan svare på "er dette den gamle eller nye formen?", slik at editoren kan la deg trykke cmd+f for å endre til ny form. 78 | 79 | Det gjøres bare automatisk konvertering til newScript ved innlasting av rom. 80 | 81 | Commands får altså fortsatt et DocumentFragment som er sitt eget. Også bruker de det til å finne sine romnummer og krav. 82 | 83 | Editor spør kommando: 84 | "Hei, inneholder du noen romnummer?" --> er cursoren der nå? Vis info om rom 85 | "Hei, inneholder du noen krav?" --> er cursoren der nå? Vis info om krav 86 | "Hei, inneholder du noen feil?" --> er cursoren der nå? Vis info om feil --> vis advarselstrekant 87 | "Hei, kan du oppgraderes til ny form?" --> vis lyspære ---.__ cmd+f = fiks 88 | "Hei, inneholder du noen skrivefeil?" --> vis blyant ---' 89 | 90 | ------------------- 91 | 92 | Istedet for automatisk fiksing mens du skriver, så vil jeg ha små lyspærer i margen som ordner. 93 | 94 | -------------------- 95 | 96 | Room holdes åpne i Adventure (og RoomHistory). 97 | - Det er en måte å beholde cursor-posisionen. 98 | - Da er det teoretisk mulig å la spilleren hoppe fram og tilbake mellom rommene uten å lagre. 99 | Er det ålreit? Jeg er veldig usikker. Da nærmer vi oss mer normal dokumenthåndtering, med tabs og skit. 100 | Da føler jeg at fokus forsvinner. 101 | 102 | -------------------- 103 | 104 | Command får et DocumentFragment 105 | - et DocumentFragment er et view inn i dokumentet 106 | - med en offset og en lengde 107 | - når dokumentet endres, oppdaterer den tilkoblede fragmenter 108 | - DocumentFragment tilbyr metoder for å endre teksten 109 | - oversettes til Document-index og sendes rett videre 110 | 111 | -------------------- 112 | 113 | En paragraf representeres med ':' 114 | - en paragraf med cursor på linja er uten ':' 115 | - en paragraf på øverste nivå er uten ':' 116 | 117 | --------------------- 118 | 119 | TextRenderer vet ikke om annet enn rendring. Tar bare imot en rekke linjer og en cursor. 120 | 121 | TextEditor kjenner til et document, som består av tekst og en cursor. Vet ingenting annet. 122 | PageEditor har en Page, kjenner til Commands, og er mer opptatt av domenet. Delegerer videre til TextEditor. 123 | RoomEditor kjenner til Rooms, kan lagre dem, undo, redo, hoppe mellom, etc. Delegerer videre til PageEditor. 124 | 125 | ------------------- 126 | 127 | Kommandoen har flere roller: 128 | - static: parse tekst til seg selv (old + new) 129 | - rendre seg som i ny og gammel form 130 | - vite hvilke romnummer og krav som er i teksten sin 131 | 132 | [Y] + [X] kan løses ved at de vet om hverandre 133 | - ved rendring av Lines, sender [Y] en tom array hvis den er helt standard 134 | 135 | --------------------- 136 | 137 | Vanlig skrivefeil for Grethe: "Ingen tvil om det"; tenker du. ; --> , 138 | Vanlig skrivefeil: Alternativ som begynner eller slutter med " uten å ha noen flere 139 | 140 | Som gammel skrivefeilretter, så må den støtte stor og små bokstav, og erstatte riktig. 141 | Men hvis feil.lowercase == riktig.lowercase så er det tydeligvis casingen som var feil da. 142 | 143 | --------------------- 144 | 145 | Document testes gjennom tre forskjellige klasser: 146 | - DocumentTest bryr seg først og fremst om at cursoren oppdateres riktig når teksten endres. 147 | - DocumentFragmentTest sjekker grunnfunksjonalitet når den tester integrasjon, samt oppdatering av Fragments. 148 | - EditorTest sjekker at de "lettere abstrakte" Document-metodene mapper godt over til lettforståelige tastetrykk. 149 | 150 | --------------------- 151 | 152 | * Vurdere et felles repo for romnummer ... hva da med offline? hver bruker kan få tildelt 300 nummer i slengen, og kontakter 153 | repo for nye nummer når den bare har 100 igjen. Ikke lengre noe gjenbruk av romnummer. 154 | 155 | -------------------------------------------------------------------------------- /src/test/groovy/no/advide/commands/AlternativeCommandTest.groovy: -------------------------------------------------------------------------------- 1 | package no.advide.commands 2 | 3 | import no.advide.Document 4 | import no.advide.DocumentFragment 5 | 6 | class AlternativeCommandTest extends GroovyTestCase { 7 | 8 | DocumentFragment createFragment(List lines) { 9 | new Document(lines, [x:0, y:0]).createFragment([x:0, y:0], lines.size()) 10 | } 11 | 12 | void test_should_match_old_form() { 13 | assert AlternativeCommand.matches(createFragment(["-", "2", "Alt 1", "13", "Alt 2", "14"])) 14 | assert AlternativeCommand.matches(createFragment(["+", "2", "Alt 1", "13", "-", "Alt 2", "14", "KRAV"])) 15 | assert !AlternativeCommand.matches(createFragment(["---", "en slemming"])) 16 | } 17 | 18 | void test_should_only_match_old_form_with_number_specified() { 19 | assert !AlternativeCommand.matches(createFragment(["-", ""])) 20 | assert !AlternativeCommand.matches(createFragment(["-", "2 kaniner står og preiker"])) 21 | } 22 | 23 | void test_should_request_rest_of_lines() { 24 | assert AlternativeCommand.numMatchingLines(createFragment(["-", "1", "", "", "", ""])) == 6 25 | assert AlternativeCommand.numMatchingLines(createFragment(["+", "2", "Alt 1", "13", "-", "Alt 2", "14", "KRAV"])) == 8 26 | } 27 | 28 | void test_should_return_roomNumbers_for_old_form() { 29 | def command = new AlternativeCommand(createFragment(["-", "2", "Alt 1", "13", "Alt 2", "14"])) 30 | assert command.roomNumbers.size() == 2 31 | assert command.roomNumbers.first().number == 13 32 | assert command.roomNumbers.first().position == [x:0, y:3] 33 | assert command.roomNumbers.last().number == 14 34 | assert command.roomNumbers.last().position == [x:0, y:5] 35 | } 36 | 37 | void test_should_not_stumble_over_non_numbers() { 38 | def command = new AlternativeCommand(createFragment(["-", "2", "Alt 1", "Safran", "Alt 2", "14"])) 39 | assert command.roomNumbers.size() == 1 40 | assert command.roomNumbers.first().number == 14 41 | assert command.roomNumbers.first().position == [x:0, y:5] 42 | } 43 | 44 | void test_should_not_stumble_over_unfinished_alternatives() { 45 | def command = new AlternativeCommand(createFragment(["-", "2", "Alt 1"])) 46 | assert command.roomNumbers.size() == 0 47 | assert command.formattedLines.size() == 3 48 | } 49 | 50 | void test_should_return_roomNumbers_for_old_form_with_requirements() { 51 | def command = new AlternativeCommand(createFragment(["+", "2", "Alt 1", "16", "-", "Alt 2", "17", "KRAV"])) 52 | assert command.roomNumbers.size() == 2 53 | assert command.roomNumbers.first().number == 16 54 | assert command.roomNumbers.first().position == [x:0, y:3] 55 | assert command.roomNumbers.last().number == 17 56 | assert command.roomNumbers.last().position == [x:0, y:6] 57 | } 58 | 59 | void test_should_format_old_alternatives_correctly() { 60 | def command = new AlternativeCommand(createFragment(["+", "2", "Alt 1", "16", "-", "Alt 2", "17", "KRAV"])) 61 | assert command.formattedLines.size() == 8 62 | assert command.formattedLines[1].prefix == "Antall alternativer: " 63 | assert command.formattedLines[2].prefix == "1. " 64 | assert command.formattedLines[3].prefix == "Rom#: " 65 | assert command.formattedLines[4].prefix == "Krav: " 66 | assert command.formattedLines[5].prefix == "2. " 67 | assert command.formattedLines[6].prefix == "Rom#: " 68 | assert command.formattedLines[7].prefix == "Krav: " 69 | } 70 | 71 | void test_get_alternatives() { 72 | def command = new AlternativeCommand(createFragment(["-", "2", "Alt 1", "16", "Alt 2", "17"])) 73 | assert command.alternatives.size() == 2 74 | 75 | assert command.alternatives.first().index == 2 76 | assert command.alternatives.first().number == 1 77 | assert command.alternatives.first().text == "Alt 1" 78 | assert command.alternatives.first().room == "16" 79 | assert command.alternatives.first().requirement == "-" 80 | 81 | assert command.alternatives.last().index == 4 82 | assert command.alternatives.last().number == 2 83 | assert command.alternatives.last().text == "Alt 2" 84 | assert command.alternatives.last().room == "17" 85 | assert command.alternatives.last().requirement == "-" 86 | } 87 | 88 | void test_get_alternatives_with_requirements() { 89 | def command = new AlternativeCommand(createFragment(["+", "2", "Alt 1", "16", "-", "Alt 2", "17", "KRAV"])) 90 | assert command.alternatives.size() == 2 91 | 92 | assert command.alternatives.first().index == 2 93 | assert command.alternatives.first().number == 1 94 | assert command.alternatives.first().text == "Alt 1" 95 | assert command.alternatives.first().room == "16" 96 | assert command.alternatives.first().requirement == "-" 97 | 98 | assert command.alternatives.last().index == 5 99 | assert command.alternatives.last().number == 2 100 | assert command.alternatives.last().text == "Alt 2" 101 | assert command.alternatives.last().room == "17" 102 | assert command.alternatives.last().requirement == "KRAV" 103 | } 104 | 105 | // ---------------- new style --------------------------------------------- 106 | 107 | void test_should_convert_to_new_style() { 108 | def command = new AlternativeCommand(createFragment(["-", "2", "Alt 1", "13", "Alt 2", "14"])) 109 | assert command.toNewStyle() == ["--", "Alt 1", "13", "Alt 2", "14"] 110 | } 111 | 112 | void test_should_convert_to_old_style() { 113 | def command = new AlternativeCommand(createFragment(["--", "Alt 1", "13", "Alt 2", "14"])) 114 | assert command.toOldStyle() == ["-", "2", "Alt 1", "13", "Alt 2", "14"] 115 | } 116 | 117 | void test_should_convert_to_new_style_with_requirements() { 118 | def command = new AlternativeCommand(createFragment(["+", "2", "Alt 1", "13", "-", "Alt 2", "14", "KRAV"])) 119 | assert command.toNewStyle() == ["--", "Alt 1", "13", "Alt 2", "14 ? KRAV"] 120 | } 121 | 122 | void test_should_convert_to_old_style_with_requirements() { 123 | def command = new AlternativeCommand(createFragment(["--", "Alt 1", "13", "Alt 2", "14 ? KRAV"])) 124 | assert command.toOldStyle() == ["+", "2", "Alt 1", "13", "-", "Alt 2", "14", "KRAV"] 125 | } 126 | 127 | void test_should_match_new_style() { 128 | assert AlternativeCommand.matches(createFragment(["--"])) 129 | assert !AlternativeCommand.matches(createFragment(["---"])) 130 | } 131 | 132 | void test_should_get_alternatives_new_style() { 133 | def command = new AlternativeCommand(createFragment(["--", "Alt 1", "16", "Alt 2", "17 ? KRAV"])) 134 | assert command.alternatives.size() == 2 135 | 136 | assert command.alternatives.first().index == 1 137 | assert command.alternatives.first().number == 1 138 | assert command.alternatives.first().text == "Alt 1" 139 | assert command.alternatives.first().room == "16" 140 | assert command.alternatives.first().requirement == "-" 141 | 142 | assert command.alternatives.last().index == 3 143 | assert command.alternatives.last().number == 2 144 | assert command.alternatives.last().text == "Alt 2" 145 | assert command.alternatives.last().room == "17" 146 | assert command.alternatives.last().requirement == "KRAV" 147 | } 148 | 149 | void test_should_format_new_style_properly() { 150 | def command = new AlternativeCommand(createFragment(["--", "Alt 1", "16", "Alt 2", "17 ? KRAV"])) 151 | assert command.formattedLines.size() == 5 152 | assert command.formattedLines[1].prefix == "1. " 153 | assert command.formattedLines[2].prefix == "--> " 154 | assert command.formattedLines[3].prefix == "2. " 155 | assert command.formattedLines[4].prefix == "--> " 156 | } 157 | 158 | void test_should_indent_numbers_if_more_than_9() { 159 | def command = new AlternativeCommand(createFragment(["--", 160 | "Alt 1", "1", 161 | "Alt 2", "2", 162 | "Alt 3", "3", 163 | "Alt 4", "4", 164 | "Alt 5", "5", 165 | "Alt 6", "6", 166 | "Alt 7", "7", 167 | "Alt 8", "8", 168 | "Alt 9", "9", 169 | "Alt 10", "10" 170 | ])) 171 | assert command.formattedLines.size() == 21 172 | assert command.formattedLines[1].prefix == " 1. " 173 | assert command.formattedLines[2].prefix == "--> " 174 | assert command.formattedLines[3].prefix == " 2. " 175 | assert command.formattedLines[4].prefix == "--> " 176 | assert command.formattedLines[19].prefix == "10. " 177 | assert command.formattedLines[20].prefix == "--> " 178 | } 179 | 180 | } 181 | -------------------------------------------------------------------------------- /syntaks.txt: -------------------------------------------------------------------------------- 1 | 2 | Få på plass støtte for @@@ og ### (fra skribent), slippe at import skal være så enormt med hassle. 3 | 4 | 5 | Kommandoer 6 | =========== 7 | Prøver å finne en bedre syntaks på kommandoer: 8 | 9 | Gammel Ny 10 | ------------------------------------------------ 11 | 12 | ()KRAV + KRAV 13 | 14 | )(KRAV - KRAV 15 | 16 | @97 @97 17 | 18 | {@}97 {@}97 19 | 20 | (@)97 (@)97 21 | 22 | [@]30 @30 ? KRAV 23 | KRAV 24 | 25 | [@]30 @30 ? 26 | - 27 | 28 | {}1 (@)73 om 1 ... 29 | 73 30 | 31 | #26 #26 32 | #173 #! (fjern dette rommet, eks. rom 173) 33 | 34 | *26 - #26 35 | 36 | $VAR = 17 $VAR = 17 37 | 38 | [!]KRAV ? KRAV 39 | Hallo Hallo 40 | 41 | [!]KRAV ? KRAV 42 | { Flere 43 | Flere linjer 44 | linjer 45 | } 46 | 47 | ]C[LAGRE LAGRE => Linje 48 | Linje 49 | 50 | [C]LAGRET <= LAGRET 51 | 52 | +++ :snillere => "Han gjorde slik og slik" 53 | "Han gjorde slik og slik" 54 | 55 | --- :slemmere => "Han gjorde hint og pist" 56 | "Han gjorde hint og pist" 57 | 58 | kr.2 :rupi +2 59 | kr.-5 :rupi -5 60 | 61 | %%% :drap +3 62 | 3 63 | 64 | $$$ :tidshopp +1 65 | 66 | #UFERDIG# randorf :uferdig => randorf 67 | 68 | #SAVE# på torget i Dypvann :save => på torget i Dypvann 69 | 70 | [Y] :besøk ++ 71 | [Y]7 :besøk ++ (max 7) 72 | 73 | [X!]1 ? 1. besøk 74 | [X!]2 ? 2. besøk 75 | [X!]3 ? 3. besøk 76 | 77 | [X]2 @17 ? 2. besøk 78 | 17 79 | 80 | !!! -- fortsett -- 81 | 82 | [$]$TALL Det er ($TALL bananer|en banan). 83 | Det er $$ bananer. 84 | Det er en banan. 85 | 86 | $RNG = Tilfeldig: 0 til 2 Samme 87 | 88 | 89 | 90 | Oppbygging av krav 91 | ================== 92 | [!]+05PØLSEBRØD ? PØLSE og BRØD 93 | [!])(TØNNE ? ikke TØNNE 94 | [!]+07)(PØLSEBRØD ? ikke PØLSE men BRØD 95 | [!]+07)(PØLSE)(BRØD ? verken PØLSE eller BRØD 96 | [!])(+07)(PØLSE)(BRØD ? PØLSE eller BRØD 97 | [!])(+05PØLSEBRØD ? ikke både PØLSE og BRØD 98 | 99 | [!]nr34 ? #34 100 | 101 | [!]kr.100 ? minst 100 rupi 102 | [!])(kr.200 ? ikke 200 rupi 103 | [!]+06kr.100)(kr.101 ? akkurat 100 rupi 104 | 105 | [!]*0* ? 1. 106 | [!]*1* ? 2. 107 | 108 | 109 | 110 | 111 | Og nye tillegg: 112 | 113 | ? 1. besøk 114 | Denne teksten vises ved første besøk. 115 | ... 116 | Denne teksten vises ellers. 117 | 118 | 119 | 120 | Den kodes til: 121 | 122 | ()ELLERS 123 | [X!]1 124 | { 125 | )(ELLERS 126 | Denne teksten vises ved første besøk. 127 | } 128 | [!]ELLERS 129 | { 130 | Denne teksten vises ellers. 131 | } 132 | 133 | da fungerer det faktisk med nesting: 134 | 135 | ? 1. besøk 136 | En tekst ved første besøk. 137 | ... 138 | ? 2.besøk 139 | En tekst ved andre besøk. 140 | ... 141 | En tekst ellers. 142 | 143 | så kan jeg tillate syntaktisk sukker: 144 | 145 | ? 1. besøk 146 | En tekst ved første besøk. 147 | ... ? 2.besøk 148 | En tekst ved andre besøk. 149 | ... ? 3.besøk 150 | En tekst ved tredje besøk. 151 | ... 152 | En tekst ellers. 153 | 154 | for å slippe så mye nesting. 155 | 156 | 157 | -- trenger ikke opplyse om :besøk med mindre du vil ha noe annet enn normal Max 158 | -- setter bare inn [Y] med max = høyeste + 1 159 | 160 | 161 | 162 | Alternativer har ikke egen linje for krav -> ingen problemer med +/- 163 | istedet: 164 | 165 | 166 | 1. Jeg plukker opp sabotasjetønna og legger den i sekken. 167 | --> 1256 ? ikke SABOTASJETØNNE 168 | 169 | 170 | 171 | (@)6168 172 | ØRKENGÅING => @155 173 | @1981 ? STAKK-AV-ETTER-Å-HA-DREPT-SMULTRINGSELGEREN 174 | @636 ? ikke HAR-OVERHØRT-LURIFAKS-TORGTALE 175 | @1438 ? IKKE-VELKOMMEN-PÅ-DATTERENS-FEST 176 | @2784 ? DREPTE-DRAGEN-MAGNUS og (SETTDOMMEDAG eller ikke ORKVSDYPVANN) 177 | ? 7. besøk 178 | + SMULTRINGER 179 | ? 8. besøk 180 | - #1343 181 | :besøk ++ (max 9) 182 | - TORE.VENT.MS 183 | Du står på torget. Det er en bunke mennesker her som selger diverse saker. 184 | ? 1. besøk 185 | Et ubehagelig støynivå preger ditt aller første møte med et skikkelig bytorg. 186 | Folk skriker og bærer seg, mens de reklamerer for varer og pruter på priser. 187 | #171 188 | #1343 189 | ? 2. besøk 190 | Det lukter fisk fra fiskedelen av markedet og grønnsaker fra bondedelen. 191 | ? ikke HARVÆRTHOSGUIDE 192 | Du ser en kar som står og blar i noen papirer. Han har et lite skilt festet 193 | til jakkeslaget som det står "GUIDE" på. Det ser ut som om han har kontroll 194 | på det meste som skjer rundt omkring her. Hodet pryder han med en oransje hatt. 195 | - #171 196 | ? SMULTRINGER men ikke DREPTE-SMULTRINGMANNEN 197 | Du kan dufte smultringer fra ei lita bod i utkanten av torget. Det lukter ganske så godt! 198 | ? 3. besøk 199 | - BORGERMESTERDUDE 200 | ? 4. besøk 201 | - SLÅSSKAMP-OM-BORGERMESTERENS-DATTER 202 | ? VANNFLASKEMANNEN 203 | "Vannflasker til salgs!" roper en fyr fra en bod. "Vannflasker!" 204 | @5647 ? ENSKOKKMEDVANNFLASKER 205 | @5589 ? ALLEVANNFLASKENEERBORTE 206 | @5288 ? NØKKELENTILVANNFLASKESKUR 207 | ? 6. besøk 208 | @4775 209 | ? ikke VANNFLASKE 210 | ? 3. besøk 211 | En ny fyr står på torget. "Vannflasker!" roper han. "Vannflasker selges!" 212 | + VANNFLASKEMANNEN 213 | 214 | @13791 ? ikke HAR-SETT-SLÅSSKAMP-OM-BORGERMESTERENS-DATTER 215 | ? 4. besøk 216 | ? STARTET-I-DYPVANN 217 | + PINNSVINDAME-PÅ-PLASS 218 | *1343 219 | ? PINNSVINDAME-PÅ-PLASS 220 | : 221 | En gammel dame med stor hatt roper "Har noen sett pinnsvinet mitt!?" 222 | ? ORKVSDYPVANN men ikke DOMMEDAGBORTE 223 | : 224 | Da ser du en snål fyr. Han har langt skjegg og uflidd hår. To store skilt 225 | henger rundt halsen. På det ene står det "DOMMEDAG ER NÆR", det andre ser 226 | du ikke ettersom det er på ryggen. Han jaller ut "ENDEN ER NÆR! VEND OM 227 | ELLER DØ! ORKENE VAR BARE DET FØRSTE! SNART VIL HELE VERDEN BLI OVERSVØMT 228 | AV FÆLE VESENER MED ONDT I SINNE!" Folk ser irritert på ham. 229 | + SETTDOMMEDAG 230 | ? VETOMKEN men ikke #1514 231 | : 232 | Tre menn med grå/hvite kjortler står og snakker foran et telt på torget. 233 | ? REDDABORGERMESTERENSDATTER 234 | : 235 | Midt på torget står det en stor og flott statue av DEG i rent gull! Den 236 | glinser i sola og ser ganske så tøff ut. Et evig monument på ditt heltemot. 237 | ? DIDRIKK_I_DYPVANN 238 | : 239 | Didrikk sitter pladask i en fluktstol ved siden av den store luftballongen. 240 | :save => på torget i Dypvann 241 | -- 242 | 1. Jeg roper: "ER DET NOEN SOM HAR SETT EN PLANKE?" 243 | --> 15649 ? SKAL_HENTE_PLANK og (verken PLANK5 eller FÅTT_INFO_OM_SJØRØVERTATT_PLANK) 244 | 2. Jeg stikker bort og bryter opp slåsskampen. 245 | --> 13792 ? SLÅSSKAMP-OM-BORGERMESTERENS-DATTER 246 | 3. Jeg går og prater med de tre kara ved teltet. 247 | --> 1514 ? VETOMKEN 248 | 4. Jeg tusler bort til den snåle dommedagsprofeten. 249 | --> 2362 ? ORKVSDYPVANN men ikke DOMMEDAGBORTE 250 | 5. Jeg labber over til han som fortalte om borgermesterens datter. 251 | --> 6974 ? BORGERMESTERDUDE 252 | 6. Jeg tar en tur bort til guiden og forhører meg om hva som skjer. 253 | --> 2525 ? ikke ERFAREN 254 | 7. Jeg går tilbake til guiden. 255 | --> 2526 ? ERFAREN og (VETOMSVARTHAUG og HARVÆRTPÅBANANØYAHARVÆRTHOSGUIDE) 256 | 8. Jeg stikker en tur bort til fyren som selger vannflasker. 257 | --> 4947 ? VANNFLASKEMANNEN 258 | 9. Jeg leter etter Trulsen, skopusseren som mista beina. 259 | --> 5189 ? #5188 260 | 10. Jeg snakker med damen som leter etter pinnsvinet sitt. 261 | --> 168 ? PINNSVINDAME-PÅ-PLASS 262 | 11. Jeg kjøper smultringer. 263 | --> 167 ? SMULTRINGER 264 | 12. Jeg ser etter en mann med en stor, grønn hatt ved navn Draume. 265 | --> 1167 ? DRAUME men ikke DRAUMEDØD 266 | 13. Jeg lusker bort til en luguber mann, og spørre om han vil lære deg å dirke. 267 | --> 1165 ? LÆREDIRKE 268 | 14. Jeg roper høyt ut "VIL NOEN LÆRE MEG Å DIRKE!?" 269 | --> 1164 ? LÆREDIRKE 270 | 15. Jeg hilser på den fyren som lesser noen tønner på en vogn. 271 | --> 439 ? verken SABOTASJETØNNE eller DATO3110 272 | 16. Jeg hilser på den fyren som lesser noen gresskar på en vogn. 273 | --> 439 ? ikke GRESSKARLYKT men DATO3110 274 | 17. Jeg går bort til fiskedelen av torget. 275 | --> 6485 ? minst 1 rupi eller VÆRTI6485 276 | 18. Jeg stikker bort til Didrikk og luftballongen. 277 | --> 7821 ? DIDRIKK_I_DYPVANN 278 | 19. Jeg går rundt i byen og ser meg litt om. 279 | --> 171 280 | 20. Jeg forlater byen og finner på noe annet. 281 | --> 1343 282 | --------------------------------------------------------------------------------