Merge commit '77d3a60869e7a780c6ae069e51530e1eacece5e2'
[fanfix.git] / src / jexer / help / THelpText.java
CommitLineData
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 */
29package jexer.help;
30
31import java.util.ArrayList;
32import java.util.List;
33
34import jexer.THelpWindow;
35import jexer.TScrollableWidget;
36import jexer.TVScroller;
37import jexer.TWidget;
38import jexer.bits.CellAttributes;
39import jexer.bits.StringUtils;
40import jexer.event.TKeypressEvent;
41import jexer.event.TMouseEvent;
42import 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 */
48public 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}