Commit | Line | Data |
---|---|---|
a3b510ab NR |
1 | /* |
2 | * This file is part of lanterna (http://code.google.com/p/lanterna/). | |
3 | * | |
4 | * lanterna is free software: you can redistribute it and/or modify | |
5 | * it under the terms of the GNU Lesser General Public License as published by | |
6 | * the Free Software Foundation, either version 3 of the License, or | |
7 | * (at your option) any later version. | |
8 | * | |
9 | * This program is distributed in the hope that it will be useful, | |
10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of | |
11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
12 | * GNU Lesser General Public License for more details. | |
13 | * | |
14 | * You should have received a copy of the GNU Lesser General Public License | |
15 | * along with this program. If not, see <http://www.gnu.org/licenses/>. | |
16 | * | |
17 | * Copyright (C) 2010-2015 Martin | |
18 | */ | |
19 | package com.googlecode.lanterna.terminal; | |
20 | ||
21 | import com.googlecode.lanterna.SGR; | |
22 | import com.googlecode.lanterna.TerminalPosition; | |
23 | import com.googlecode.lanterna.TerminalSize; | |
24 | import com.googlecode.lanterna.graphics.AbstractTextGraphics; | |
25 | import com.googlecode.lanterna.TextCharacter; | |
26 | import com.googlecode.lanterna.graphics.TextGraphics; | |
27 | ||
28 | import java.io.IOException; | |
29 | import java.util.HashMap; | |
30 | import java.util.Map; | |
31 | import java.util.concurrent.atomic.AtomicInteger; | |
32 | ||
33 | /** | |
34 | * This is the terminal's implementation of TextGraphics. Upon creation it takes a snapshot for the terminal's size, so | |
35 | * that it won't require to do an expensive lookup on every call to {@code getSize()}, but this also means that it can | |
36 | * go stale quickly if the terminal is resized. You should try to use the object quickly and then let it be GC:ed. It | |
37 | * will not pick up on terminal resize! Also, the state of the Terminal after an operation performed by this | |
38 | * TextGraphics implementation is undefined and you should probably re-initialize colors and modifiers. | |
39 | * <p/> | |
40 | * Any write operation that results in an IOException will be wrapped by a RuntimeException since the TextGraphics | |
41 | * interface doesn't allow throwing IOException | |
42 | */ | |
43 | class TerminalTextGraphics extends AbstractTextGraphics { | |
44 | ||
45 | private final Terminal terminal; | |
46 | private final TerminalSize terminalSize; | |
47 | ||
48 | private final Map<TerminalPosition, TextCharacter> writeHistory; | |
49 | ||
50 | private AtomicInteger manageCallStackSize; | |
51 | private TextCharacter lastCharacter; | |
52 | private TerminalPosition lastPosition; | |
53 | ||
54 | TerminalTextGraphics(Terminal terminal) throws IOException { | |
55 | this.terminal = terminal; | |
56 | this.terminalSize = terminal.getTerminalSize(); | |
57 | this.manageCallStackSize = new AtomicInteger(0); | |
58 | this.writeHistory = new HashMap<TerminalPosition, TextCharacter>(); | |
59 | this.lastCharacter = null; | |
60 | this.lastPosition = null; | |
61 | } | |
62 | ||
63 | @Override | |
64 | public TextGraphics setCharacter(int columnIndex, int rowIndex, TextCharacter textCharacter) { | |
65 | return setCharacter(new TerminalPosition(columnIndex, rowIndex), textCharacter); | |
66 | } | |
67 | ||
68 | @Override | |
69 | public synchronized TextGraphics setCharacter(TerminalPosition position, TextCharacter textCharacter) { | |
70 | try { | |
71 | if(manageCallStackSize.get() > 0) { | |
72 | if(lastCharacter == null || !lastCharacter.equals(textCharacter)) { | |
73 | applyGraphicState(textCharacter); | |
74 | lastCharacter = textCharacter; | |
75 | } | |
76 | if(lastPosition == null || !lastPosition.equals(position)) { | |
77 | terminal.setCursorPosition(position.getColumn(), position.getRow()); | |
78 | lastPosition = position; | |
79 | } | |
80 | } | |
81 | else { | |
82 | terminal.setCursorPosition(position.getColumn(), position.getRow()); | |
83 | applyGraphicState(textCharacter); | |
84 | } | |
85 | terminal.putCharacter(textCharacter.getCharacter()); | |
86 | if(manageCallStackSize.get() > 0) { | |
87 | lastPosition = position.withRelativeColumn(1); | |
88 | } | |
89 | writeHistory.put(position, textCharacter); | |
90 | } | |
91 | catch(IOException e) { | |
92 | throw new RuntimeException(e); | |
93 | } | |
94 | return this; | |
95 | } | |
96 | ||
97 | @Override | |
98 | public TextCharacter getCharacter(int column, int row) { | |
99 | return getCharacter(new TerminalPosition(column, row)); | |
100 | } | |
101 | ||
102 | @Override | |
103 | public synchronized TextCharacter getCharacter(TerminalPosition position) { | |
104 | return writeHistory.get(position); | |
105 | } | |
106 | ||
107 | private void applyGraphicState(TextCharacter textCharacter) throws IOException { | |
108 | terminal.resetColorAndSGR(); | |
109 | terminal.setForegroundColor(textCharacter.getForegroundColor()); | |
110 | terminal.setBackgroundColor(textCharacter.getBackgroundColor()); | |
111 | for(SGR sgr: textCharacter.getModifiers()) { | |
112 | terminal.enableSGR(sgr); | |
113 | } | |
114 | } | |
115 | ||
116 | @Override | |
117 | public TerminalSize getSize() { | |
118 | return terminalSize; | |
119 | } | |
120 | ||
121 | @Override | |
122 | public synchronized TextGraphics drawLine(TerminalPosition fromPoint, TerminalPosition toPoint, char character) { | |
123 | try { | |
124 | enterAtomic(); | |
125 | super.drawLine(fromPoint, toPoint, character); | |
126 | return this; | |
127 | } | |
128 | finally { | |
129 | leaveAtomic(); | |
130 | } | |
131 | } | |
132 | ||
133 | @Override | |
134 | public synchronized TextGraphics drawTriangle(TerminalPosition p1, TerminalPosition p2, TerminalPosition p3, char character) { | |
135 | try { | |
136 | enterAtomic(); | |
137 | super.drawTriangle(p1, p2, p3, character); | |
138 | return this; | |
139 | } | |
140 | finally { | |
141 | leaveAtomic(); | |
142 | } | |
143 | } | |
144 | ||
145 | @Override | |
146 | public synchronized TextGraphics fillTriangle(TerminalPosition p1, TerminalPosition p2, TerminalPosition p3, char character) { | |
147 | try { | |
148 | enterAtomic(); | |
149 | super.fillTriangle(p1, p2, p3, character); | |
150 | return this; | |
151 | } | |
152 | finally { | |
153 | leaveAtomic(); | |
154 | } | |
155 | } | |
156 | ||
157 | @Override | |
158 | public synchronized TextGraphics fillRectangle(TerminalPosition topLeft, TerminalSize size, char character) { | |
159 | try { | |
160 | enterAtomic(); | |
161 | super.fillRectangle(topLeft, size, character); | |
162 | return this; | |
163 | } | |
164 | finally { | |
165 | leaveAtomic(); | |
166 | } | |
167 | } | |
168 | ||
169 | @Override | |
170 | public synchronized TextGraphics drawRectangle(TerminalPosition topLeft, TerminalSize size, char character) { | |
171 | try { | |
172 | enterAtomic(); | |
173 | super.drawRectangle(topLeft, size, character); | |
174 | return this; | |
175 | } | |
176 | finally { | |
177 | leaveAtomic(); | |
178 | } | |
179 | } | |
180 | ||
181 | @Override | |
182 | public synchronized TextGraphics putString(int column, int row, String string) { | |
183 | try { | |
184 | enterAtomic(); | |
185 | return super.putString(column, row, string); | |
186 | } | |
187 | finally { | |
188 | leaveAtomic(); | |
189 | } | |
190 | } | |
191 | ||
192 | /** | |
193 | * It's tricky with this implementation because we can't rely on any state in between two calls to setCharacter | |
194 | * since the caller might modify the terminal's state outside of this writer. However, many calls inside | |
195 | * TextGraphics will indeed make multiple calls in setCharacter where we know that the state won't change (actually, | |
196 | * we can't be 100% sure since the caller might create a separate thread and maliciously write directly to the | |
197 | * terminal while call one of the draw/fill/put methods in here). We could just set the state before writing every | |
198 | * single character but that would be inefficient. Rather, we keep a counter of if we are inside an 'atomic' | |
199 | * (meaning we know multiple calls to setCharacter will have the same state). Some drawing methods call other | |
200 | * drawing methods internally for their implementation so that's why this is implemented with an integer value | |
201 | * instead of a boolean; when the counter reaches zero we remove the memory of what state the terminal is in. | |
202 | */ | |
203 | private void enterAtomic() { | |
204 | manageCallStackSize.incrementAndGet(); | |
205 | } | |
206 | ||
207 | private void leaveAtomic() { | |
208 | if(manageCallStackSize.decrementAndGet() == 0) { | |
209 | lastPosition = null; | |
210 | lastCharacter = null; | |
211 | } | |
212 | } | |
213 | } |