View Javadoc
1   /*
2    * License : The MIT License
3    * Copyright(c) 2019 Olyutorskii
4    */
5   
6   package io.github.olyutorskii.quetexj;
7   
8   import java.awt.EventQueue;
9   import java.awt.Point;
10  import java.awt.Rectangle;
11  import java.awt.event.ComponentAdapter;
12  import java.awt.event.ComponentEvent;
13  import java.util.Objects;
14  import javax.swing.BoundedRangeModel;
15  import javax.swing.JTextArea;
16  import javax.swing.SwingConstants;
17  import javax.swing.text.BadLocationException;
18  import javax.swing.text.Document;
19  import javax.swing.text.JTextComponent;
20  
21  /**
22   * Automatically keep height of text component
23   * by chopping head of Document model.
24   *
25   * <p>Component is chopped row by row.
26   *
27   * <p>Physical text-line layout is preserved.
28   *
29   * <p>Relative position of vertical BoundedRangeModel is adjusted
30   * to keep JViewport view in JScrollPane
31   * as much as possible.
32   *
33   * <p>PlainDocument only supported.
34   */
35  public class HeightKeeper {
36  
37      /** Default height limit. */
38      public static final int DEF_HEIGHTLIMIT = 3000;
39      /** Default new height. */
40      public static final int DEF_NEWHEIGHT   = 2500;
41  
42      private static final Rectangle DMY_RECT = new Rectangle();
43  
44  
45      private final JTextArea textComp;
46  
47      private final BoundedRangeModel rangeModel;
48  
49      private int heightLimit;
50      private int newHeight;
51      private final Object condLock = new Object();
52  
53      private final SizeWatcher watcher = new SizeWatcher();
54  
55  
56      /**
57       * Constructor.
58       *
59       * <p>Condition parameters are default value.
60       *
61       * @param textComp text component
62       * @param rangeModel bounded range model
63       */
64      public HeightKeeper(JTextArea textComp, BoundedRangeModel rangeModel) {
65          this(textComp, rangeModel, DEF_HEIGHTLIMIT, DEF_NEWHEIGHT);
66          return;
67      }
68  
69      /**
70       * Constructor.
71       *
72       * <ul>
73       * <li>newHeight must be positive integer value.
74       * <li>newHeight must be smaller than heightLimit.
75       * </ul>
76       *
77       * @param textComp text component
78       * @param rangeModel bounded range model
79       * @param heightLimit height limit condition
80       * @param newHeight new height when over limit
81       * @throws IllegalArgumentException illegal integer argument
82       */
83      public HeightKeeper(JTextArea textComp, BoundedRangeModel rangeModel,
84              int heightLimit, int newHeight) {
85          super();
86  
87          Objects.requireNonNull(rangeModel);
88  
89          if (newHeight <= 0) throw new IllegalArgumentException();
90          if (heightLimit <= newHeight) throw new IllegalArgumentException();
91  
92          this.textComp = textComp;
93          this.textComp.addComponentListener(this.watcher);
94  
95          this.heightLimit = heightLimit;
96          this.newHeight = newHeight;
97  
98          this.rangeModel = rangeModel;
99  
100         return;
101     }
102 
103     /**
104      * Return associated text component.
105      *
106      * @return text component
107      */
108     public JTextComponent getTextComponent() {
109         return this.textComp;
110     }
111 
112     /**
113      * Return associated BoundedRangeModel.
114      *
115      * @return BoundedRangeModel
116      */
117     public BoundedRangeModel getBoundedRangeModel() {
118         return this.rangeModel;
119     }
120 
121     /**
122      * Return height limit condition.
123      *
124      * @return height limit
125      */
126     public int getHeightLimit() {
127         return this.heightLimit;
128     }
129 
130     /**
131      * Return new height.
132      *
133      * @return new height
134      */
135     public int getNewHeight() {
136         return this.newHeight;
137     }
138 
139     /**
140      * Set height condition values.
141      *
142      * <ul>
143      * <li>newHeightArg must be positive integer value.
144      * <li>newHeightArg must be smaller than heightLimitArg.
145      * </ul>
146      *
147      * @param heightLimitArg height limit condition
148      * @param newHeightArg new height when over limit
149      * @throws IllegalArgumentException illegal integer argument
150      */
151     public void setConditions(int heightLimitArg, int newHeightArg)
152             throws IllegalArgumentException {
153         if (newHeightArg <= 0) {
154             throw new IllegalArgumentException();
155         }
156         if (heightLimitArg <= newHeightArg) {
157             throw new IllegalArgumentException();
158         }
159 
160         synchronized (this.condLock) {
161             this.heightLimit = heightLimitArg;
162             this.newHeight = newHeightArg;
163         }
164 
165         if (EventQueue.isDispatchThread()) {
166             eventResized();
167         } else {
168             EventQueue.invokeLater(() -> {
169                 eventResized();
170             });
171         }
172 
173         return;
174     }
175 
176     /**
177      * Receive component resized event.
178      *
179      * <p>If component height is smaller than height limit, do nothing.
180      */
181     void eventResized() {
182         int condHeightLimit;
183         int condNewHeight;
184         synchronized (this.condLock) {
185             condHeightLimit = this.heightLimit;
186             condNewHeight   = this.newHeight;
187         }
188 
189         int compHeight = this.textComp.getHeight();
190         if (compHeight < condHeightLimit) return;
191 
192         int chopHeight = compHeight - condNewHeight;
193         int oldRangeVal = this.rangeModel.getValue();
194 
195         chopHeadHeightRowBounds(chopHeight);
196 
197         adjustBoundedRangeModel(chopHeight, oldRangeVal);
198 
199         return;
200     }
201 
202     /**
203      * Chop text component height from ceiling.
204      *
205      * <p>Component height will be shrunk between row bounds.
206      *
207      * <p>Text component will be resized later EventQueue.
208      *
209      * @param chopRegionHeight Chopping height from ceiling.
210      */
211     private void chopHeadHeightRowBounds(int chopRegionHeight) {
212         int docLastPos = chopHeightToLinedOffset(chopRegionHeight);
213         chopHeadHeightByDocPos(docLastPos);
214         return;
215     }
216 
217     /**
218      * Convert from head chop height
219      * to physical line-end offset in Document model.
220      *
221      * @param chopHeight head chop height in text component
222      * @return offset in Document model. -1 if undefined.
223      */
224     private int chopHeightToLinedOffset(int chopHeight) {
225         int chopWidth  = this.textComp.getWidth();
226 
227         // Diagonal corner of shrink region
228         Point edgePoint = new Point(chopWidth  - 1, chopHeight - 1);
229 
230         int docOffset = this.textComp.viewToModel(edgePoint);
231 
232         return docOffset;
233     }
234 
235     /**
236      * Chop text component height by chopping head of Document model.
237      *
238      * <p>If Document position is negative, do nothing.
239      *
240      * <p>Text component will be resized later EventQueue.
241      *
242      * @param docLastPos last char position of chop-text in Document model.
243      */
244     private void chopHeadHeightByDocPos(int docLastPos) {
245         if (docLastPos < 0) return;
246 
247         Document document = this.textComp.getDocument();
248         int docLength = document.getLength();
249         if (docLength <= 0) return;
250 
251         int regionLength = Integer.min(docLastPos + 1, docLength);
252 
253         try {
254             document.remove(0, regionLength);
255         } catch (BadLocationException e) {
256             assert false;
257         }
258 
259         return;
260     }
261 
262     /**
263      * Adjust BoundedRangeModel to keep JViewport view in JScrollPane.
264      *
265      * @param chopHeight chop height
266      * @param oldRangeVal BoundedRangeModel value before chopping
267      * @return adjusted BoundedRangeModel value.
268      */
269     private int adjustBoundedRangeModel(int chopHeight, int oldRangeVal) {
270         int realChopHeight = getRealChopHeight(chopHeight);
271         int newRangeVal = oldRangeVal - realChopHeight;
272         this.rangeModel.setValue(newRangeVal);
273         return newRangeVal;
274     }
275 
276     /**
277      * Convert head height to row-bounds height.
278      *
279      * <p>Result is multiple of row height.
280      *
281      * @param chopHeight chop height
282      * @return row-bounds height
283      */
284     private int getRealChopHeight(int chopHeight) {
285         int insetsTop = this.textComp.getInsets().top;
286         int bodyHeight = chopHeight - insetsTop;
287         int rowHeight = getRowHeight();
288         int chopRows = bodyHeight / rowHeight + 1;
289 
290         int realChopHeight = rowHeight * chopRows + insetsTop;
291 
292         return realChopHeight;
293     }
294 
295     /**
296      * Get row height of text component.
297      *
298      * @return row height
299      * @see JTextArea#getRowHeight()
300      */
301     private int getRowHeight() {
302         int result =
303                 this.textComp.getScrollableUnitIncrement(
304                         DMY_RECT, SwingConstants.VERTICAL, 0);
305         return result;
306     }
307 
308 
309     /**
310      * Component resize watcher.
311      */
312     private class SizeWatcher extends ComponentAdapter {
313 
314         /**
315          * Constructor.
316          */
317         SizeWatcher() {
318             super();
319             return;
320         }
321 
322         /**
323          * {@inheritDoc}
324          *
325          * @param ev {@inheritDoc}
326          */
327         @Override
328         public void componentResized(ComponentEvent ev) {
329             eventResized();
330             return;
331         }
332 
333     }
334 
335 }