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