box drawing intersections
[fanfix.git] / src / jexer / TImage.java
CommitLineData
a69ed767
KL
1/*
2 * Jexer - Java Text User Interface
3 *
4 * The MIT License (MIT)
5 *
6 * Copyright (C) 2019 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 */
29package jexer;
30
31import java.awt.image.BufferedImage;
32
33import jexer.backend.ECMA48Terminal;
34import jexer.backend.MultiScreen;
35import jexer.backend.SwingTerminal;
36import jexer.bits.Cell;
37import jexer.event.TKeypressEvent;
38import jexer.event.TMouseEvent;
39import static jexer.TKeypress.*;
40
41/**
42 * TImage renders a piece of a bitmap image on screen.
43 */
44public class TImage extends TWidget {
45
46 // ------------------------------------------------------------------------
47 // Variables --------------------------------------------------------------
48 // ------------------------------------------------------------------------
49
50 /**
51 * The action to perform when the user clicks on the image.
52 */
53 private TAction clickAction;
54
55 /**
56 * The image to display.
57 */
58 private BufferedImage image;
59
60 /**
61 * The original image from construction time.
62 */
63 private BufferedImage originalImage;
64
65 /**
66 * The current scaling factor for the image.
67 */
68 private double scaleFactor = 1.0;
69
70 /**
71 * The current clockwise rotation for the image.
72 */
73 private int clockwise = 0;
74
75 /**
76 * Left column of the image. 0 is the left-most column.
77 */
78 private int left;
79
80 /**
81 * Top row of the image. 0 is the top-most row.
82 */
83 private int top;
84
85 /**
86 * The cells containing the broken up image pieces.
87 */
88 private Cell cells[][];
89
90 /**
91 * The number of rows in cells[].
92 */
93 private int cellRows;
94
95 /**
96 * The number of columns in cells[].
97 */
98 private int cellColumns;
99
100 /**
101 * Last text width value.
102 */
103 private int lastTextWidth = -1;
104
105 /**
106 * Last text height value.
107 */
108 private int lastTextHeight = -1;
109
110 // ------------------------------------------------------------------------
111 // Constructors -----------------------------------------------------------
112 // ------------------------------------------------------------------------
113
114 /**
115 * Public constructor.
116 *
117 * @param parent parent widget
118 * @param x column relative to parent
119 * @param y row relative to parent
120 * @param width number of text cells for width of the image
121 * @param height number of text cells for height of the image
122 * @param image the image to display
123 * @param left left column of the image. 0 is the left-most column.
124 * @param top top row of the image. 0 is the top-most row.
125 * @param clickAction function to call when mouse is pressed
126 */
127 public TImage(final TWidget parent, final int x, final int y,
128 final int width, final int height,
129 final BufferedImage image, final int left, final int top,
130 final TAction clickAction) {
131
132 // Set parent and window
133 super(parent, x, y, width, height);
134
135 setCursorVisible(false);
136 this.originalImage = image;
137 this.left = left;
138 this.top = top;
139 this.clickAction = clickAction;
140
141 sizeToImage(true);
142
143 getApplication().addImage(this);
144 }
145
146 // ------------------------------------------------------------------------
147 // Event handlers ---------------------------------------------------------
148 // ------------------------------------------------------------------------
149
150 /**
151 * Subclasses should override this method to cleanup resources. This is
152 * called by TWindow.onClose().
153 */
154 @Override
155 protected void close() {
156 getApplication().removeImage(this);
157 super.close();
158 }
159
160 /**
161 * Handle mouse press events.
162 *
163 * @param mouse mouse button press event
164 */
165 @Override
166 public void onMouseDown(final TMouseEvent mouse) {
167 if (clickAction != null) {
168 clickAction.DO();
169 return;
170 }
171 }
172
173 /**
174 * Handle keystrokes.
175 *
176 * @param keypress keystroke event
177 */
178 @Override
179 public void onKeypress(final TKeypressEvent keypress) {
180 if (!keypress.getKey().isFnKey()) {
181 if (keypress.getKey().getChar() == '+') {
182 // Make the image bigger.
183 scaleFactor *= 1.25;
184 image = null;
185 sizeToImage(true);
186 return;
187 }
188 if (keypress.getKey().getChar() == '-') {
189 // Make the image smaller.
190 scaleFactor *= 0.80;
191 image = null;
192 sizeToImage(true);
193 return;
194 }
195 }
196 if (keypress.equals(kbAltUp)) {
197 // Make the image bigger.
198 scaleFactor *= 1.25;
199 image = null;
200 sizeToImage(true);
201 return;
202 }
203 if (keypress.equals(kbAltDown)) {
204 // Make the image smaller.
205 scaleFactor *= 0.80;
206 image = null;
207 sizeToImage(true);
208 return;
209 }
210 if (keypress.equals(kbAltRight)) {
211 // Rotate clockwise.
212 clockwise++;
213 clockwise %= 4;
214 image = null;
215 sizeToImage(true);
216 return;
217 }
218 if (keypress.equals(kbAltLeft)) {
219 // Rotate counter-clockwise.
220 clockwise--;
221 if (clockwise < 0) {
222 clockwise = 3;
223 }
224 image = null;
225 sizeToImage(true);
226 return;
227 }
228
229 // Pass to parent for the things we don't care about.
230 super.onKeypress(keypress);
231 }
232
233 // ------------------------------------------------------------------------
234 // TWidget ----------------------------------------------------------------
235 // ------------------------------------------------------------------------
236
237 /**
238 * Draw the image.
239 */
240 @Override
241 public void draw() {
242 sizeToImage(false);
243
244 // We have already broken the image up, just draw the last set of
245 // cells.
246 for (int x = 0; (x < getWidth()) && (x + left < cellColumns); x++) {
247 if ((left + x) * lastTextWidth > image.getWidth()) {
248 continue;
249 }
250
251 for (int y = 0; (y < getHeight()) && (y + top < cellRows); y++) {
252 if ((top + y) * lastTextHeight > image.getHeight()) {
253 continue;
254 }
255 assert (x + left < cellColumns);
256 assert (y + top < cellRows);
257
258 getWindow().putCharXY(x, y, cells[x + left][y + top]);
259 }
260 }
261
262 }
263
264 // ------------------------------------------------------------------------
265 // TImage -----------------------------------------------------------------
266 // ------------------------------------------------------------------------
267
268 /**
269 * Size cells[][] according to the screen font size.
270 *
271 * @param always if true, always resize the cells
272 */
273 private void sizeToImage(final boolean always) {
274 int textWidth = 16;
275 int textHeight = 20;
276
277 if (getScreen() instanceof SwingTerminal) {
278 SwingTerminal terminal = (SwingTerminal) getScreen();
279
280 textWidth = terminal.getTextWidth();
281 textHeight = terminal.getTextHeight();
282 } if (getScreen() instanceof MultiScreen) {
283 MultiScreen terminal = (MultiScreen) getScreen();
284
285 textWidth = terminal.getTextWidth();
286 textHeight = terminal.getTextHeight();
287 } else if (getScreen() instanceof ECMA48Terminal) {
288 ECMA48Terminal terminal = (ECMA48Terminal) getScreen();
289
290 textWidth = terminal.getTextWidth();
291 textHeight = terminal.getTextHeight();
292 }
293
294 if (image == null) {
295 image = scaleImage(originalImage, scaleFactor);
296 image = rotateImage(image, clockwise);
297 }
298
299 if ((always == true) ||
300 ((textWidth > 0)
301 && (textWidth != lastTextWidth)
302 && (textHeight > 0)
303 && (textHeight != lastTextHeight))
304 ) {
305 cellColumns = image.getWidth() / textWidth;
306 if (cellColumns * textWidth < image.getWidth()) {
307 cellColumns++;
308 }
309 cellRows = image.getHeight() / textHeight;
310 if (cellRows * textHeight < image.getHeight()) {
311 cellRows++;
312 }
313
314 // Break the image up into an array of cells.
315 cells = new Cell[cellColumns][cellRows];
316
317 for (int x = 0; x < cellColumns; x++) {
318 for (int y = 0; y < cellRows; y++) {
319
320 int width = textWidth;
321 if ((x + 1) * textWidth > image.getWidth()) {
322 width = image.getWidth() - (x * textWidth);
323 }
324 int height = textHeight;
325 if ((y + 1) * textHeight > image.getHeight()) {
326 height = image.getHeight() - (y * textHeight);
327 }
328
329 Cell cell = new Cell();
330 cell.setImage(image.getSubimage(x * textWidth,
331 y * textHeight, width, height));
332
333 cells[x][y] = cell;
334 }
335 }
336
337 lastTextWidth = textWidth;
338 lastTextHeight = textHeight;
339 }
340
341 if ((left + getWidth()) > cellColumns) {
342 left = cellColumns - getWidth();
343 }
344 if (left < 0) {
345 left = 0;
346 }
347 if ((top + getHeight()) > cellRows) {
348 top = cellRows - getHeight();
349 }
350 if (top < 0) {
351 top = 0;
352 }
353 }
354
355 /**
356 * Get the top corner to render.
357 *
358 * @return the top row
359 */
360 public int getTop() {
361 return top;
362 }
363
364 /**
365 * Set the top corner to render.
366 *
367 * @param top the new top row
368 */
369 public void setTop(final int top) {
370 this.top = top;
371 if (this.top > cellRows - getHeight()) {
372 this.top = cellRows - getHeight();
373 }
374 if (this.top < 0) {
375 this.top = 0;
376 }
377 }
378
379 /**
380 * Get the left corner to render.
381 *
382 * @return the left column
383 */
384 public int getLeft() {
385 return left;
386 }
387
388 /**
389 * Set the left corner to render.
390 *
391 * @param left the new left column
392 */
393 public void setLeft(final int left) {
394 this.left = left;
395 if (this.left > cellColumns - getWidth()) {
396 this.left = cellColumns - getWidth();
397 }
398 if (this.left < 0) {
399 this.left = 0;
400 }
401 }
402
403 /**
404 * Get the number of text cell rows for this image.
405 *
406 * @return the number of rows
407 */
408 public int getRows() {
409 return cellRows;
410 }
411
412 /**
413 * Get the number of text cell columns for this image.
414 *
415 * @return the number of columns
416 */
417 public int getColumns() {
418 return cellColumns;
419 }
420
421 /**
422 * Scale an image by to be scaleFactor size.
423 *
424 * @param image the image to scale
425 * @param factor the scale to make the new image
426 */
427 private BufferedImage scaleImage(final BufferedImage image,
428 final double factor) {
429
430 if (Math.abs(factor - 1.0) < 0.03) {
431 // If we are within 3% of 1.0, just return the original image.
432 return image;
433 }
434
435 int width = (int) (image.getWidth() * factor);
436 int height = (int) (image.getHeight() * factor);
437
438 BufferedImage newImage = new BufferedImage(width, height,
439 BufferedImage.TYPE_INT_ARGB);
440
441 java.awt.Graphics gr = newImage.createGraphics();
442 gr.drawImage(image, 0, 0, width, height, null);
443 gr.dispose();
444
445 return newImage;
446 }
447
448 /**
449 * Rotate an image either clockwise or counterclockwise.
450 *
451 * @param image the image to scale
452 * @param clockwise number of turns clockwise
453 */
454 private BufferedImage rotateImage(final BufferedImage image,
455 final int clockwise) {
456
457 if (clockwise % 4 == 0) {
458 return image;
459 }
460
461 BufferedImage newImage = null;
462
463 if (clockwise % 4 == 1) {
464 // 90 degrees clockwise
465 newImage = new BufferedImage(image.getHeight(), image.getWidth(),
466 BufferedImage.TYPE_INT_ARGB);
467 for (int x = 0; x < image.getWidth(); x++) {
468 for (int y = 0; y < image.getHeight(); y++) {
469 newImage.setRGB(y, x,
470 image.getRGB(x, image.getHeight() - 1 - y));
471 }
472 }
473 } else if (clockwise % 4 == 2) {
474 // 180 degrees clockwise
475 newImage = new BufferedImage(image.getWidth(), image.getHeight(),
476 BufferedImage.TYPE_INT_ARGB);
477 for (int x = 0; x < image.getWidth(); x++) {
478 for (int y = 0; y < image.getHeight(); y++) {
479 newImage.setRGB(x, y,
480 image.getRGB(image.getWidth() - 1 - x,
481 image.getHeight() - 1 - y));
482 }
483 }
484 } else if (clockwise % 4 == 3) {
485 // 270 degrees clockwise
486 newImage = new BufferedImage(image.getHeight(), image.getWidth(),
487 BufferedImage.TYPE_INT_ARGB);
488 for (int x = 0; x < image.getWidth(); x++) {
489 for (int y = 0; y < image.getHeight(); y++) {
490 newImage.setRGB(y, x,
491 image.getRGB(image.getWidth() - 1 - x, y));
492 }
493 }
494 }
495
496 return newImage;
497 }
498
499}