Commit | Line | Data |
---|---|---|
4941d2d6 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 | */ | |
29 | package jexer.help; | |
30 | ||
31 | import java.util.ArrayList; | |
32 | import java.util.List; | |
33 | ||
34 | import jexer.THelpWindow; | |
35 | import jexer.TScrollableWidget; | |
36 | import jexer.TVScroller; | |
37 | import jexer.TWidget; | |
38 | import jexer.bits.CellAttributes; | |
39 | import jexer.bits.StringUtils; | |
40 | import jexer.event.TKeypressEvent; | |
41 | import jexer.event.TMouseEvent; | |
42 | import static jexer.TKeypress.*; | |
43 | ||
44 | /** | |
45 | * THelpText displays help text with clickable links in a scrollable text | |
46 | * area. It reflows automatically on resize. | |
47 | */ | |
48 | public class THelpText extends TScrollableWidget { | |
49 | ||
50 | // ------------------------------------------------------------------------ | |
51 | // Constants -------------------------------------------------------------- | |
52 | // ------------------------------------------------------------------------ | |
53 | ||
54 | /** | |
55 | * The number of lines to scroll on mouse wheel up/down. | |
56 | */ | |
57 | private static final int wheelScrollSize = 3; | |
58 | ||
59 | // ------------------------------------------------------------------------ | |
60 | // Variables -------------------------------------------------------------- | |
61 | // ------------------------------------------------------------------------ | |
62 | ||
63 | /** | |
64 | * The paragraphs in this text box. | |
65 | */ | |
66 | private List<TParagraph> paragraphs; | |
67 | ||
68 | // ------------------------------------------------------------------------ | |
69 | // Constructors ----------------------------------------------------------- | |
70 | // ------------------------------------------------------------------------ | |
71 | ||
72 | /** | |
73 | * Public constructor. | |
74 | * | |
75 | * @param parent parent widget | |
76 | * @param topic the topic to display | |
77 | * @param x column relative to parent | |
78 | * @param y row relative to parent | |
79 | * @param width width of text area | |
80 | * @param height height of text area | |
81 | */ | |
82 | public THelpText(final THelpWindow parent, final Topic topic, final int x, | |
83 | final int y, final int width, final int height) { | |
84 | ||
85 | // Set parent and window | |
86 | super(parent, x, y, width, height); | |
87 | ||
88 | vScroller = new TVScroller(this, getWidth() - 1, 0, | |
89 | Math.max(1, getHeight())); | |
90 | ||
91 | setTopic(topic); | |
92 | } | |
93 | ||
94 | // ------------------------------------------------------------------------ | |
95 | // TScrollableWidget ------------------------------------------------------ | |
96 | // ------------------------------------------------------------------------ | |
97 | ||
98 | /** | |
99 | * Override TWidget's width: we need to set child widget widths. | |
100 | * | |
101 | * @param width new widget width | |
102 | */ | |
103 | @Override | |
104 | public void setWidth(final int width) { | |
105 | super.setWidth(width); | |
106 | if (hScroller != null) { | |
107 | hScroller.setWidth(getWidth() - 1); | |
108 | } | |
109 | if (vScroller != null) { | |
110 | vScroller.setX(getWidth() - 1); | |
111 | } | |
112 | } | |
113 | ||
114 | /** | |
115 | * Override TWidget's height: we need to set child widget heights. | |
116 | * time. | |
117 | * | |
118 | * @param height new widget height | |
119 | */ | |
120 | @Override | |
121 | public void setHeight(final int height) { | |
122 | super.setHeight(height); | |
123 | if (hScroller != null) { | |
124 | hScroller.setY(getHeight() - 1); | |
125 | } | |
126 | if (vScroller != null) { | |
127 | vScroller.setHeight(Math.max(1, getHeight())); | |
128 | } | |
129 | } | |
130 | ||
131 | /** | |
132 | * Handle mouse press events. | |
133 | * | |
134 | * @param mouse mouse button press event | |
135 | */ | |
136 | @Override | |
137 | public void onMouseDown(final TMouseEvent mouse) { | |
138 | // Pass to children | |
139 | super.onMouseDown(mouse); | |
140 | ||
141 | if (mouse.isMouseWheelUp()) { | |
142 | for (int i = 0; i < wheelScrollSize; i++) { | |
143 | vScroller.decrement(); | |
144 | } | |
145 | reflowData(); | |
146 | return; | |
147 | } | |
148 | if (mouse.isMouseWheelDown()) { | |
149 | for (int i = 0; i < wheelScrollSize; i++) { | |
150 | vScroller.increment(); | |
151 | } | |
152 | reflowData(); | |
153 | return; | |
154 | } | |
155 | ||
156 | // User clicked on a paragraph, update the scrollbar accordingly. | |
157 | for (int i = 0; i < paragraphs.size(); i++) { | |
158 | if (paragraphs.get(i).isActive()) { | |
159 | setVerticalValue(i); | |
160 | return; | |
161 | } | |
162 | } | |
163 | } | |
164 | ||
165 | /** | |
166 | * Handle keystrokes. | |
167 | * | |
168 | * @param keypress keystroke event | |
169 | */ | |
170 | @Override | |
171 | public void onKeypress(final TKeypressEvent keypress) { | |
172 | if (keypress.equals(kbTab)) { | |
173 | getParent().switchWidget(true); | |
174 | } else if (keypress.equals(kbShiftTab)) { | |
175 | getParent().switchWidget(false); | |
176 | } else if (keypress.equals(kbUp)) { | |
177 | if (!paragraphs.get(getVerticalValue()).up()) { | |
178 | vScroller.decrement(); | |
179 | reflowData(); | |
180 | } | |
181 | } else if (keypress.equals(kbDown)) { | |
182 | if (!paragraphs.get(getVerticalValue()).down()) { | |
183 | vScroller.increment(); | |
184 | reflowData(); | |
185 | } | |
186 | } else if (keypress.equals(kbPgUp)) { | |
187 | vScroller.bigDecrement(); | |
188 | reflowData(); | |
189 | } else if (keypress.equals(kbPgDn)) { | |
190 | vScroller.bigIncrement(); | |
191 | reflowData(); | |
192 | } else if (keypress.equals(kbHome)) { | |
193 | vScroller.toTop(); | |
194 | reflowData(); | |
195 | } else if (keypress.equals(kbEnd)) { | |
196 | vScroller.toBottom(); | |
197 | reflowData(); | |
198 | } else { | |
199 | // Pass other keys on | |
200 | super.onKeypress(keypress); | |
201 | } | |
202 | } | |
203 | ||
204 | /** | |
205 | * Place the scrollbars on the edge of this widget, and adjust bigChange | |
206 | * to match the new size. This is called by onResize(). | |
207 | */ | |
208 | protected void placeScrollbars() { | |
209 | if (hScroller != null) { | |
210 | hScroller.setY(getHeight() - 1); | |
211 | hScroller.setWidth(getWidth() - 1); | |
212 | hScroller.setBigChange(getWidth() - 1); | |
213 | } | |
214 | if (vScroller != null) { | |
215 | vScroller.setX(getWidth() - 1); | |
216 | vScroller.setHeight(getHeight()); | |
217 | vScroller.setBigChange(getHeight()); | |
218 | } | |
219 | } | |
220 | ||
221 | /** | |
222 | * Resize text and scrollbars for a new width/height. | |
223 | */ | |
224 | @Override | |
225 | public void reflowData() { | |
226 | for (TParagraph paragraph: paragraphs) { | |
227 | paragraph.setWidth(getWidth() - 1); | |
228 | paragraph.reflowData(); | |
229 | } | |
230 | ||
231 | int top = getVerticalValue(); | |
232 | int paragraphsHeight = 0; | |
233 | for (TParagraph paragraph: paragraphs) { | |
234 | paragraphsHeight += paragraph.getHeight(); | |
235 | } | |
236 | if (paragraphsHeight <= getHeight()) { | |
237 | // All paragraphs fit in the window. | |
238 | int y = 0; | |
239 | for (int i = 0; i < paragraphs.size(); i++) { | |
240 | paragraphs.get(i).setEnabled(true); | |
241 | paragraphs.get(i).setVisible(true); | |
242 | paragraphs.get(i).setY(y); | |
243 | y += paragraphs.get(i).getHeight(); | |
244 | } | |
245 | activate(paragraphs.get(getVerticalValue())); | |
246 | return; | |
247 | } | |
248 | ||
249 | /* | |
250 | * Some paragraphs will not fit in the window. Find the number of | |
251 | * rows needed to display from the current vertical position to the | |
252 | * end: | |
253 | * | |
254 | * - If this meets or exceeds the available height, then draw from | |
255 | * the vertical position to the number of visible rows. | |
256 | * | |
257 | * - If this is less than the available height, back up until | |
258 | * meeting/exceeding the height, and draw from there to the end. | |
259 | * | |
260 | */ | |
261 | int rowsNeeded = 0; | |
262 | for (int i = getVerticalValue(); i <= getBottomValue(); i++) { | |
263 | rowsNeeded += paragraphs.get(i).getHeight(); | |
264 | } | |
265 | while (rowsNeeded < getHeight()) { | |
266 | // Decrease top until we meet/exceed the visible display. | |
267 | if (top == getTopValue()) { | |
268 | break; | |
269 | } | |
270 | top--; | |
271 | rowsNeeded += paragraphs.get(top).getHeight(); | |
272 | } | |
273 | ||
274 | // All set, now disable all paragraphs except the visible ones. | |
275 | for (TParagraph paragraph: paragraphs) { | |
276 | paragraph.setEnabled(false); | |
277 | paragraph.setVisible(false); | |
278 | paragraph.setY(-1); | |
279 | } | |
280 | int y = 0; | |
281 | for (int i = top; (i <= getBottomValue()) && (y < getHeight()); i++) { | |
282 | paragraphs.get(i).setEnabled(true); | |
283 | paragraphs.get(i).setVisible(true); | |
284 | paragraphs.get(i).setY(y); | |
285 | y += paragraphs.get(i).getHeight(); | |
286 | } | |
287 | activate(paragraphs.get(getVerticalValue())); | |
288 | } | |
289 | ||
290 | /** | |
291 | * Draw the text box. | |
292 | */ | |
293 | @Override | |
294 | public void draw() { | |
295 | // Setup my color | |
296 | CellAttributes color = getTheme().getColor("thelpwindow.text"); | |
297 | for (int y = 0; y < getHeight(); y++) { | |
298 | hLineXY(0, y, getWidth(), ' ', color); | |
299 | } | |
300 | } | |
301 | ||
302 | // ------------------------------------------------------------------------ | |
303 | // THelpText -------------------------------------------------------------- | |
304 | // ------------------------------------------------------------------------ | |
305 | ||
306 | /** | |
307 | * Set the topic. | |
308 | * | |
309 | * @param topic new topic to display | |
310 | */ | |
311 | public void setTopic(final Topic topic) { | |
312 | setTopic(topic, true); | |
313 | } | |
314 | ||
315 | /** | |
316 | * Set the topic. | |
317 | * | |
318 | * @param topic new topic to display | |
319 | * @param separator if true, separate paragraphs | |
320 | */ | |
321 | public void setTopic(final Topic topic, final boolean separator) { | |
322 | ||
323 | if (paragraphs != null) { | |
324 | getChildren().removeAll(paragraphs); | |
325 | } | |
326 | paragraphs = new ArrayList<TParagraph>(); | |
327 | ||
328 | // Add title paragraph at top. We explicitly set the separator to | |
329 | // false to achieve the underscore effect. | |
330 | List<TWord> title = new ArrayList<TWord>(); | |
331 | title.add(new TWord(topic.getTitle(), null)); | |
332 | TParagraph titleParagraph = new TParagraph(this, title); | |
333 | titleParagraph.separator = false; | |
334 | paragraphs.add(titleParagraph); | |
335 | title = new ArrayList<TWord>(); | |
336 | StringBuilder sb = new StringBuilder(); | |
337 | for (int i = 0; i < topic.getTitle().length(); i++) { | |
338 | sb.append('\u2580'); | |
339 | } | |
340 | title.add(new TWord(sb.toString(), null)); | |
341 | titleParagraph = new TParagraph(this, title); | |
342 | paragraphs.add(titleParagraph); | |
343 | ||
344 | // Now add the actual text as paragraphs. | |
345 | int wordIndex = 0; | |
346 | ||
347 | // Break up text into paragraphs | |
348 | String [] blocks = topic.getText().split("\n\n"); | |
349 | for (String block: blocks) { | |
350 | List<TWord> words = new ArrayList<TWord>(); | |
351 | String [] lines = block.split("\n"); | |
352 | for (String line: lines) { | |
353 | line = line.trim(); | |
354 | // System.err.println("line: " + line); | |
355 | String [] wordTokens = line.split("\\s+"); | |
356 | for (int i = 0; i < wordTokens.length; i++) { | |
357 | String wordStr = wordTokens[i].trim(); | |
358 | Link wordLink = null; | |
359 | for (Link link: topic.getLinks()) { | |
360 | if ((i + wordIndex >= link.getIndex()) | |
361 | && (i + wordIndex < link.getIndex() + link.getWordCount()) | |
362 | ) { | |
363 | // This word is part of a link. | |
364 | wordLink = link; | |
365 | wordStr = link.getText(); | |
366 | i += link.getWordCount() - 1; | |
367 | break; | |
368 | } | |
369 | } | |
370 | TWord word = new TWord(wordStr, wordLink); | |
371 | /* | |
372 | System.err.println("add word at " + (i + wordIndex) + " : " | |
373 | + wordStr + " " + wordLink); | |
374 | */ | |
375 | words.add(word); | |
376 | } // for (int i = 0; i < words.length; i++) | |
377 | wordIndex += wordTokens.length; | |
378 | } // for (String line: lines) | |
379 | TParagraph paragraph = new TParagraph(this, words); | |
380 | paragraph.separator = separator; | |
381 | paragraphs.add(paragraph); | |
382 | } // for (String block: blocks) | |
383 | ||
384 | setBottomValue(paragraphs.size() - 1); | |
385 | setVerticalValue(0); | |
386 | reflowData(); | |
387 | } | |
388 | ||
389 | } |