| 1 | /* |
| 2 | * Jexer - Java Text User Interface |
| 3 | * |
| 4 | * The MIT License (MIT) |
| 5 | * |
| 6 | * Copyright (C) 2017 Kevin Lamonte |
| 7 | * |
| 8 | * Permission is hereby granted, free of charge, to any person obtaining a |
| 9 | * copy of this software and associated documentation files (the "Software"), |
| 10 | * to deal in the Software without restriction, including without limitation |
| 11 | * the rights to use, copy, modify, merge, publish, distribute, sublicense, |
| 12 | * and/or sell copies of the Software, and to permit persons to whom the |
| 13 | * Software is furnished to do so, subject to the following conditions: |
| 14 | * |
| 15 | * The above copyright notice and this permission notice shall be included in |
| 16 | * all copies or substantial portions of the Software. |
| 17 | * |
| 18 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
| 19 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
| 20 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL |
| 21 | * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
| 22 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING |
| 23 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER |
| 24 | * DEALINGS IN THE SOFTWARE. |
| 25 | * |
| 26 | * @author Kevin Lamonte [kevin.lamonte@gmail.com] |
| 27 | * @version 1 |
| 28 | */ |
| 29 | package jexer.teditor; |
| 30 | |
| 31 | import java.util.ArrayList; |
| 32 | import java.util.List; |
| 33 | |
| 34 | import jexer.bits.Cell; |
| 35 | import jexer.bits.CellAttributes; |
| 36 | |
| 37 | /** |
| 38 | * A Line represents a single line of text on the screen. Each character is |
| 39 | * a Cell, so it can have color attributes in addition to the basic char. |
| 40 | */ |
| 41 | public class Line implements Fragment { |
| 42 | |
| 43 | /** |
| 44 | * The cells of the line. |
| 45 | */ |
| 46 | private List<Cell> cells; |
| 47 | |
| 48 | /** |
| 49 | * The line number. |
| 50 | */ |
| 51 | private int lineNumber; |
| 52 | |
| 53 | /** |
| 54 | * The previous Fragment in the list. |
| 55 | */ |
| 56 | private Fragment prevFrag; |
| 57 | |
| 58 | /** |
| 59 | * The next Fragment in the list. |
| 60 | */ |
| 61 | private Fragment nextFrag; |
| 62 | |
| 63 | /** |
| 64 | * Construct a new Line from an existing text string. |
| 65 | */ |
| 66 | public Line() { |
| 67 | this(""); |
| 68 | } |
| 69 | |
| 70 | /** |
| 71 | * Construct a new Line from an existing text string. |
| 72 | * |
| 73 | * @param text the code points of the line |
| 74 | */ |
| 75 | public Line(final String text) { |
| 76 | cells = new ArrayList<Cell>(text.length()); |
| 77 | for (int i = 0; i < text.length(); i++) { |
| 78 | cells.add(new Cell(text.charAt(i))); |
| 79 | } |
| 80 | } |
| 81 | |
| 82 | /** |
| 83 | * Reset all colors of this Line to white-on-black. |
| 84 | */ |
| 85 | public void resetColors() { |
| 86 | setColors(new CellAttributes()); |
| 87 | } |
| 88 | |
| 89 | /** |
| 90 | * Set all colors of this Line to one color. |
| 91 | * |
| 92 | * @param color the new color to use |
| 93 | */ |
| 94 | public void setColors(final CellAttributes color) { |
| 95 | for (Cell cell: cells) { |
| 96 | cell.setTo(color); |
| 97 | } |
| 98 | } |
| 99 | |
| 100 | /** |
| 101 | * Set the color of one cell. |
| 102 | * |
| 103 | * @param index a cell number, between 0 and getCellCount() |
| 104 | * @param color the new color to use |
| 105 | * @throws IndexOutOfBoundsException if index is negative or not less |
| 106 | * than getCellCount() |
| 107 | */ |
| 108 | public void setColor(final int index, final CellAttributes color) { |
| 109 | cells.get(index).setTo(color); |
| 110 | } |
| 111 | |
| 112 | /** |
| 113 | * Get the raw text that will be rendered. |
| 114 | * |
| 115 | * @return the text |
| 116 | */ |
| 117 | public String getText() { |
| 118 | char [] text = new char[cells.size()]; |
| 119 | for (int i = 0; i < cells.size(); i++) { |
| 120 | text[i] = cells.get(i).getChar(); |
| 121 | } |
| 122 | return new String(text); |
| 123 | } |
| 124 | |
| 125 | /** |
| 126 | * Get the attributes for a cell. |
| 127 | * |
| 128 | * @param index a cell number, between 0 and getCellCount() |
| 129 | * @return the attributes |
| 130 | * @throws IndexOutOfBoundsException if index is negative or not less |
| 131 | * than getCellCount() |
| 132 | */ |
| 133 | public CellAttributes getColor(final int index) { |
| 134 | return cells.get(index); |
| 135 | } |
| 136 | |
| 137 | /** |
| 138 | * Get the number of graphical cells represented by this text. Note that |
| 139 | * a Unicode grapheme cluster can take any number of pixels, but this |
| 140 | * editor is intended to be used with a fixed-width font. So this count |
| 141 | * returns the number of fixed-width cells, NOT the number of grapheme |
| 142 | * clusters. |
| 143 | * |
| 144 | * @return the number of fixed-width cells this fragment's text will |
| 145 | * render to |
| 146 | */ |
| 147 | public int getCellCount() { |
| 148 | return cells.size(); |
| 149 | } |
| 150 | |
| 151 | /** |
| 152 | * Get the text to render for a specific fixed-width cell. |
| 153 | * |
| 154 | * @param index a cell number, between 0 and getCellCount() |
| 155 | * @return the codepoints to render for this fixed-width cell |
| 156 | * @throws IndexOutOfBoundsException if index is negative or not less |
| 157 | * than getCellCount() |
| 158 | */ |
| 159 | public Cell getCell(final int index) { |
| 160 | return cells.get(index); |
| 161 | } |
| 162 | |
| 163 | /** |
| 164 | * Get the text to render for several fixed-width cells. |
| 165 | * |
| 166 | * @param start a cell number, between 0 and getCellCount() |
| 167 | * @param length the number of cells to return |
| 168 | * @return the codepoints to render for this fixed-width cell |
| 169 | * @throws IndexOutOfBoundsException if start or (start + length) is |
| 170 | * negative or not less than getCellCount() |
| 171 | */ |
| 172 | public String getCells(final int start, final int length) { |
| 173 | char [] text = new char[length]; |
| 174 | for (int i = 0; i < length; i++) { |
| 175 | text[i] = cells.get(i + start).getChar(); |
| 176 | } |
| 177 | return new String(text); |
| 178 | } |
| 179 | |
| 180 | /** |
| 181 | * Sets (replaces) the text to render for a specific fixed-width cell. |
| 182 | * |
| 183 | * @param index a cell number, between 0 and getCellCount() |
| 184 | * @param ch the character for this fixed-width cell |
| 185 | * @throws IndexOutOfBoundsException if index is negative or not less |
| 186 | * than getCellCount() |
| 187 | */ |
| 188 | public void setCell(final int index, final char ch) { |
| 189 | cells.set(index, new Cell(ch)); |
| 190 | } |
| 191 | |
| 192 | /** |
| 193 | * Sets (replaces) the text to render for a specific fixed-width cell. |
| 194 | * |
| 195 | * @param index a cell number, between 0 and getCellCount() |
| 196 | * @param cell the new value for this fixed-width cell |
| 197 | * @throws IndexOutOfBoundsException if index is negative or not less |
| 198 | * than getCellCount() |
| 199 | */ |
| 200 | public void setCell(final int index, final Cell cell) { |
| 201 | cells.set(index, cell); |
| 202 | } |
| 203 | |
| 204 | /** |
| 205 | * Inserts a char to render for a specific fixed-width cell. |
| 206 | * |
| 207 | * @param index a cell number, between 0 and getCellCount() - 1 |
| 208 | * @param ch the character for this fixed-width cell |
| 209 | * @throws IndexOutOfBoundsException if index is negative or not less |
| 210 | * than getCellCount() |
| 211 | */ |
| 212 | public void insertCell(final int index, final char ch) { |
| 213 | cells.add(index, new Cell(ch)); |
| 214 | } |
| 215 | |
| 216 | /** |
| 217 | * Inserts a Cell to render for a specific fixed-width cell. |
| 218 | * |
| 219 | * @param index a cell number, between 0 and getCellCount() - 1 |
| 220 | * @param cell the new value for this fixed-width cell |
| 221 | * @throws IndexOutOfBoundsException if index is negative or not less |
| 222 | * than getCellCount() |
| 223 | */ |
| 224 | public void insertCell(final int index, final Cell cell) { |
| 225 | cells.add(index, cell); |
| 226 | } |
| 227 | |
| 228 | /** |
| 229 | * Delete a specific fixed-width cell. |
| 230 | * |
| 231 | * @param index a cell number, between 0 and getCellCount() - 1 |
| 232 | * @throws IndexOutOfBoundsException if index is negative or not less |
| 233 | * than getCellCount() |
| 234 | */ |
| 235 | public void deleteCell(final int index) { |
| 236 | cells.remove(index); |
| 237 | } |
| 238 | |
| 239 | /** |
| 240 | * Delete several fixed-width cells. |
| 241 | * |
| 242 | * @param start a cell number, between 0 and getCellCount() - 1 |
| 243 | * @param length the number of cells to delete |
| 244 | * @throws IndexOutOfBoundsException if index is negative or not less |
| 245 | * than getCellCount() |
| 246 | */ |
| 247 | public void deleteCells(final int start, final int length) { |
| 248 | for (int i = 0; i < length; i++) { |
| 249 | cells.remove(start); |
| 250 | } |
| 251 | } |
| 252 | |
| 253 | /** |
| 254 | * Appends a char to render for a specific fixed-width cell. |
| 255 | * |
| 256 | * @param ch the character for this fixed-width cell |
| 257 | */ |
| 258 | public void appendCell(final char ch) { |
| 259 | cells.add(new Cell(ch)); |
| 260 | } |
| 261 | |
| 262 | /** |
| 263 | * Inserts a Cell to render for a specific fixed-width cell. |
| 264 | * |
| 265 | * @param cell the new value for this fixed-width cell |
| 266 | */ |
| 267 | public void appendCell(final Cell cell) { |
| 268 | cells.add(cell); |
| 269 | } |
| 270 | |
| 271 | /** |
| 272 | * Get the next Fragment in the list, or null if this Fragment is the |
| 273 | * last node. |
| 274 | * |
| 275 | * @return the next Fragment, or null |
| 276 | */ |
| 277 | public Fragment next() { |
| 278 | return nextFrag; |
| 279 | } |
| 280 | |
| 281 | /** |
| 282 | * Get the previous Fragment in the list, or null if this Fragment is the |
| 283 | * first node. |
| 284 | * |
| 285 | * @return the previous Fragment, or null |
| 286 | */ |
| 287 | public Fragment prev() { |
| 288 | return prevFrag; |
| 289 | } |
| 290 | |
| 291 | /** |
| 292 | * See if this Fragment can be joined with the next Fragment in list. |
| 293 | * |
| 294 | * @return true if the join was possible, false otherwise |
| 295 | */ |
| 296 | public boolean isNextJoinable() { |
| 297 | if ((nextFrag != null) && (nextFrag instanceof Line)) { |
| 298 | return true; |
| 299 | } |
| 300 | return false; |
| 301 | } |
| 302 | |
| 303 | /** |
| 304 | * Join this Fragment with the next Fragment in list. |
| 305 | * |
| 306 | * @return true if the join was successful, false otherwise |
| 307 | */ |
| 308 | public boolean joinNext() { |
| 309 | if ((nextFrag == null) || !(nextFrag instanceof Line)) { |
| 310 | return false; |
| 311 | } |
| 312 | Line q = (Line) nextFrag; |
| 313 | ArrayList<Cell> newCells = new ArrayList<Cell>(this.cells.size() + |
| 314 | q.cells.size()); |
| 315 | newCells.addAll(this.cells); |
| 316 | newCells.addAll(q.cells); |
| 317 | this.cells = newCells; |
| 318 | ((Line) q.nextFrag).prevFrag = this; |
| 319 | nextFrag = q.nextFrag; |
| 320 | return true; |
| 321 | } |
| 322 | |
| 323 | /** |
| 324 | * See if this Fragment can be joined with the previous Fragment in list. |
| 325 | * |
| 326 | * @return true if the join was possible, false otherwise |
| 327 | */ |
| 328 | public boolean isPrevJoinable() { |
| 329 | if ((prevFrag != null) && (prevFrag instanceof Line)) { |
| 330 | return true; |
| 331 | } |
| 332 | return false; |
| 333 | } |
| 334 | |
| 335 | /** |
| 336 | * Join this Fragment with the previous Fragment in list. |
| 337 | * |
| 338 | * @return true if the join was successful, false otherwise |
| 339 | */ |
| 340 | public boolean joinPrev() { |
| 341 | if ((prevFrag == null) || !(prevFrag instanceof Line)) { |
| 342 | return false; |
| 343 | } |
| 344 | Line p = (Line) prevFrag; |
| 345 | ArrayList<Cell> newCells = new ArrayList<Cell>(this.cells.size() + |
| 346 | p.cells.size()); |
| 347 | newCells.addAll(p.cells); |
| 348 | newCells.addAll(this.cells); |
| 349 | this.cells = newCells; |
| 350 | ((Line) p.prevFrag).nextFrag = this; |
| 351 | prevFrag = p.prevFrag; |
| 352 | return true; |
| 353 | } |
| 354 | |
| 355 | /** |
| 356 | * Set the next Fragment in the list. Note that this performs no sanity |
| 357 | * checking or modifications on fragment; this function can break |
| 358 | * connectivity in the list. |
| 359 | * |
| 360 | * @param fragment the next Fragment, or null |
| 361 | */ |
| 362 | public void setNext(Fragment fragment) { |
| 363 | nextFrag = fragment; |
| 364 | } |
| 365 | |
| 366 | /** |
| 367 | * Set the previous Fragment in the list. Note that this performs no |
| 368 | * sanity checking or modifications on fragment; this function can break |
| 369 | * connectivity in the list. |
| 370 | * |
| 371 | * @param fragment the previous Fragment, or null |
| 372 | */ |
| 373 | public void setPrev(Fragment fragment) { |
| 374 | prevFrag = fragment; |
| 375 | } |
| 376 | |
| 377 | /** |
| 378 | * Split this Fragment into two. 'this' Fragment will contain length |
| 379 | * cells, 'this.next()' will contain (getCellCount() - length) cells. |
| 380 | * |
| 381 | * @param length the number of cells to leave in this Fragment |
| 382 | * @throws IndexOutOfBoundsException if length is negative, or 0, greater |
| 383 | * than (getCellCount() - 1) |
| 384 | */ |
| 385 | public void split(final int length) { |
| 386 | // Create the next node |
| 387 | Line q = new Line(); |
| 388 | q.nextFrag = nextFrag; |
| 389 | q.prevFrag = this; |
| 390 | ((Line) nextFrag).prevFrag = q; |
| 391 | nextFrag = q; |
| 392 | |
| 393 | // Split cells |
| 394 | q.cells = new ArrayList<Cell>(cells.size() - length); |
| 395 | q.cells.addAll(cells.subList(length, cells.size())); |
| 396 | cells = cells.subList(0, length); |
| 397 | } |
| 398 | |
| 399 | /** |
| 400 | * Insert a new Fragment at a position, splitting the contents of this |
| 401 | * Fragment into two around it. 'this' Fragment will contain the cells |
| 402 | * between 0 and index, 'this.next()' will be the inserted fragment, and |
| 403 | * 'this.next().next()' will contain the cells between 'index' and |
| 404 | * getCellCount() - 1. |
| 405 | * |
| 406 | * @param index the number of cells to leave in this Fragment |
| 407 | * @param fragment the Fragment to insert |
| 408 | * @throws IndexOutOfBoundsException if length is negative, or 0, greater |
| 409 | * than (getCellCount() - 1) |
| 410 | */ |
| 411 | public void split(final int index, Fragment fragment) { |
| 412 | // Create the next node and insert into the list. |
| 413 | Line q = new Line(); |
| 414 | q.nextFrag = nextFrag; |
| 415 | q.nextFrag.setPrev(q); |
| 416 | q.prevFrag = fragment; |
| 417 | fragment.setNext(q); |
| 418 | fragment.setPrev(this); |
| 419 | nextFrag = fragment; |
| 420 | |
| 421 | // Split cells |
| 422 | q.cells = new ArrayList<Cell>(cells.size() - index); |
| 423 | q.cells.addAll(cells.subList(index, cells.size())); |
| 424 | cells = cells.subList(0, index); |
| 425 | } |
| 426 | |
| 427 | /** |
| 428 | * Insert a new Fragment before this one. |
| 429 | * |
| 430 | * @param fragment the Fragment to insert |
| 431 | */ |
| 432 | public void insert(Fragment fragment) { |
| 433 | fragment.setNext(this); |
| 434 | fragment.setPrev(prevFrag); |
| 435 | prevFrag.setNext(fragment); |
| 436 | prevFrag = fragment; |
| 437 | } |
| 438 | |
| 439 | /** |
| 440 | * Append a new Fragment at the end of this one. |
| 441 | * |
| 442 | * @param fragment the Fragment to append |
| 443 | */ |
| 444 | public void append(Fragment fragment) { |
| 445 | fragment.setNext(nextFrag); |
| 446 | fragment.setPrev(this); |
| 447 | nextFrag.setPrev(fragment); |
| 448 | nextFrag = fragment; |
| 449 | } |
| 450 | |
| 451 | /** |
| 452 | * Delete this Fragment from the list, and return its next(). |
| 453 | * |
| 454 | * @return this Fragment's next(), or null if it was at the end of the |
| 455 | * list |
| 456 | */ |
| 457 | public Fragment deleteGetNext() { |
| 458 | Fragment result = nextFrag; |
| 459 | nextFrag.setPrev(prevFrag); |
| 460 | prevFrag.setNext(nextFrag); |
| 461 | prevFrag = null; |
| 462 | nextFrag = null; |
| 463 | return result; |
| 464 | } |
| 465 | |
| 466 | /** |
| 467 | * Delete this Fragment from the list, and return its prev(). |
| 468 | * |
| 469 | * @return this Fragment's next(), or null if it was at the beginning of |
| 470 | * the list |
| 471 | */ |
| 472 | public Fragment deleteGetPrev() { |
| 473 | Fragment result = prevFrag; |
| 474 | nextFrag.setPrev(prevFrag); |
| 475 | prevFrag.setNext(nextFrag); |
| 476 | prevFrag = null; |
| 477 | nextFrag = null; |
| 478 | return result; |
| 479 | } |
| 480 | |
| 481 | /** |
| 482 | * Get the anchor position. |
| 483 | * |
| 484 | * @return the anchor number |
| 485 | */ |
| 486 | public int getAnchor() { |
| 487 | return lineNumber; |
| 488 | } |
| 489 | |
| 490 | /** |
| 491 | * Set the anchor position. |
| 492 | * |
| 493 | * @param x the new anchor number |
| 494 | */ |
| 495 | public void setAnchor(final int x) { |
| 496 | lineNumber = x; |
| 497 | } |
| 498 | |
| 499 | } |