OSDN Git Service

ee416a8f71bd9509ad05dcbb1479c4613a7a0a63
[jindolf/Jindolf.git] / src / main / java / jp / sfjp / jindolf / view / TabBrowser.java
1 /*
2  * period viewer with tab access
3  *
4  * License : The MIT License
5  * Copyright(c) 2008 olyutorskii
6  */
7
8 package jp.sfjp.jindolf.view;
9
10 import java.awt.Component;
11 import java.awt.event.ActionListener;
12 import java.util.ArrayList;
13 import java.util.List;
14 import java.util.Objects;
15 import javax.swing.BorderFactory;
16 import javax.swing.JComponent;
17 import javax.swing.JPanel;
18 import javax.swing.JScrollPane;
19 import javax.swing.JTabbedPane;
20 import javax.swing.SwingConstants;
21 import javax.swing.border.Border;
22 import jp.sfjp.jindolf.data.DialogPref;
23 import jp.sfjp.jindolf.data.Period;
24 import jp.sfjp.jindolf.data.Village;
25 import jp.sfjp.jindolf.glyph.AnchorHitListener;
26 import jp.sfjp.jindolf.glyph.Discussion;
27 import jp.sfjp.jindolf.glyph.FontInfo;
28
29 /**
30  * タブを用いて村情報と各Periodを切り替え表示するためのコンポーネント。
31  *
32  * <p>村情報タブのビューはVillageInfoPanel、
33  * PeriodタブのビューはPeriodViewが担当する。
34  *
35  * <p>PeriodViewの描画下請Discussionへのアクセス、
36  * およびフォント管理、会話描画設定を提供する。
37  */
38 @SuppressWarnings("serial")
39 public final class TabBrowser extends JTabbedPane{
40
41     private Village village;
42
43     private final VillageInfoPanel villageInfo = new VillageInfoPanel();
44
45     private FontInfo fontInfo;
46     private DialogPref dialogPref;
47
48
49     /**
50      * コンストラクタ。
51      *
52      * <p>村が指定されていない状態のタブパネルを生成する。
53      */
54     public TabBrowser(){
55         super();
56
57         setTabPlacement(SwingConstants.TOP);
58         // Mac Aqua L&F ignore WRAP_TAB_LAYOUT
59         setTabLayoutPolicy(JTabbedPane.WRAP_TAB_LAYOUT);
60
61         JComponent infoPane = decorateVillageInfo();
62         addTab("村情報", infoPane);
63
64         initTab();
65
66         return;
67     }
68
69
70     /**
71      * 村情報表示コンポーネントを装飾する。
72      *
73      * @return 装飾済みコンポーネント
74      */
75     private JComponent decorateVillageInfo(){
76         Border border = BorderFactory.createEmptyBorder(5, 5, 5, 5);
77         this.villageInfo.setBorder(border);
78         JScrollPane result = new JScrollPane(this.villageInfo);
79         return result;
80     }
81
82     /**
83      * タブ初期化。
84      *
85      * <p>Periodタブは全て消え村情報タブのみになる。
86      */
87     private void initTab(){
88         modifyTabCount(0);
89
90         updateVillageInfo();
91         selectVillageInfoTab();
92
93         repaint();
94         revalidate();
95
96         return;
97     }
98
99     /**
100      * 指定した数のPeriodが収まるよう必要十分なタブ数を用意する。
101      *
102      * @param periods Periodの数(エピローグを含む)
103      */
104     private void modifyTabCount(int periods){ // TODO 0でも大丈夫?
105         int maxPeriodDays = periods - 1;
106
107         for(;;){   // 短ければタブ追加
108             int lastTabIndex = getTabCount() - 1;
109             if(tabIndexToPeriodDays(lastTabIndex) >= maxPeriodDays) break;
110             String tabTitle = "";
111             Component dummy = new JPanel();
112             addTab(tabTitle, dummy);
113         }
114
115         for(;;){   // 長ければ余分なタブ削除
116             int lastTabIndex = getTabCount() - 1;
117             if(tabIndexToPeriodDays(lastTabIndex) <= maxPeriodDays) break;
118             remove(lastTabIndex);
119         }
120
121         return;
122     }
123
124     /**
125      * Period日付指定からタブインデックス値への変換。
126      *
127      * <p>エピローグ(0日目)のタブインデックスは1。
128      * 1日目Periodのタブインデックスは2。
129      *
130      * @param days Period日付指定
131      * @return タブインデックス。存在しないタブの場合は負の値。
132      */
133     public int periodDaysToTabIndex(int days){
134         if(days < 0) return -1;
135         int tabIndex = days+1;
136         if(tabIndex >= getTabCount()) return -1;
137         return tabIndex;
138     }
139
140     /**
141      * タブインデックス値からPeriod日付指定への変換。
142      *
143      * <p>エピローグタブのPeriod日付は0。
144      * 1日目PeriodタブのPeriod日付は1。
145      *
146      * @param tabIndex タブインデックス
147      * @return Period日付指定。存在しないタブの場合は負の値。
148      */
149     private int tabIndexToPeriodDays(int tabIndex){
150         if(tabIndex < 0) return -1;
151         if(tabIndex >= getTabCount()) return -1;
152         int days = tabIndex - 1;
153         return days;
154     }
155
156     /**
157      * 設定された村を返す。
158      *
159      * @return 設定された村
160      */
161     public Village getVillage(){
162         return this.village;
163     }
164
165     /**
166      * 新規に村を設定する。
167      *
168      * <p>村のPeriod数に応じてタブの数は変化する。
169      *
170      * @param village 新しい村
171      */
172     public final void setVillage(Village village){
173         Village oldVillage = this.village;
174         if(oldVillage != null && village != oldVillage){
175             oldVillage.unloadPeriods();
176         }
177
178         this.village = village;
179         if(this.village == null){
180             initTab();
181             return;
182         }
183
184         if(this.village != oldVillage){
185             selectVillageInfoTab();
186         }
187
188         updateVillageInfo();
189
190         int periodNum = this.village.getPeriodSize();
191         modifyTabCount(periodNum);
192
193         for(int periodDays = 0; periodDays < periodNum; periodDays++){
194             Period period = this.village.getPeriod(periodDays);
195             PeriodView periodView = buildPeriodView(period);
196
197             int tabIndex = periodDaysToTabIndex(periodDays);
198             setComponentAt(tabIndex, periodView);
199
200             String caption = period.getCaption();
201             setTitleAt(tabIndex, caption);
202         }
203
204         repaint();
205         revalidate();
206
207         return;
208     }
209
210     /**
211      * 村情報閲覧用のコンポーネントを更新する。
212      */
213     private void updateVillageInfo(){
214         Village target = getVillage();
215         this.villageInfo.updateVillage(target);
216         return;
217     }
218
219     /**
220      * 村情報表示タブを選択表示する。
221      */
222     private void selectVillageInfoTab(){
223         setSelectedIndex(0);
224         return;
225     }
226
227     /**
228      * PeriodViewインスタンスを生成する。
229      *
230      * <p>フォント設定、会話表示設定、各種リスナの設定が行われる。
231      *
232      * @param period Period
233      * @return PeriodViewインスタンス
234      */
235     private PeriodView buildPeriodView(Period period){
236         Objects.nonNull(period);
237
238         PeriodView result;
239
240         result = new PeriodView(period);
241         result.setFontInfo(this.fontInfo);
242         result.setDialogPref(this.dialogPref);
243
244         Discussion discussion = result.getDiscussion();
245         for(ActionListener listener : getActionListeners()){
246             discussion.addActionListener(listener);
247         }
248         for(AnchorHitListener listener : getAnchorHitListeners()){
249             discussion.addAnchorHitListener(listener);
250         }
251
252         return result;
253     }
254
255     /**
256      * PeriodView一覧を得る。
257      *
258      * @return PeriodView の List
259      */
260     public List<PeriodView> getPeriodViewList(){
261         int tabCount = getTabCount();
262         List<PeriodView> result = new ArrayList<>(tabCount - 1);
263
264         for(int tabIndex = 1; tabIndex < tabCount; tabIndex++){
265             Component component = getComponent(tabIndex);
266             PeriodView periodView = (PeriodView) component;
267             result.add(periodView);
268         }
269
270         return result;
271     }
272
273     /**
274      * 指定したタブインデックスに関連付けられたPeriodViewを返す。
275      *
276      * <p>Periodに関係ないタブが指定されたらnullを返す。
277      *
278      * @param tabIndex タブインデックス
279      * @return 指定されたPeriodView
280      */
281     public PeriodView getPeriodView(int tabIndex){
282         if(tabIndexToPeriodDays(tabIndex) < 0) return null;
283         if(tabIndex >= getTabCount()) return null;
284
285         Component component = getComponentAt(tabIndex);
286         if( ! (component instanceof PeriodView) ) return null;
287         PeriodView periodView = (PeriodView) component;
288
289         return periodView;
290     }
291
292     /**
293      * 現在タブ選択中のPeriodViewを返す。
294      *
295      * <p>Periodに関係ないタブが選択されていたらnullを返す。
296      *
297      * @return 現在選択中のPeriodView
298      */
299     public PeriodView currentPeriodView(){
300         int tabIndex = getSelectedIndex();
301         PeriodView result = getPeriodView(tabIndex);
302         return result;
303     }
304
305     /**
306      * 指定したタブインデックスに関連付けられたDiscussionを返す。
307      *
308      * <p>Periodに関係ないタブが指定されたらnullを返す。
309      *
310      * @param tabIndex タブインデックス
311      * @return 指定されたDiscussion
312      */
313     private Discussion getDiscussion(int tabIndex){
314         PeriodView periodView = getPeriodView(tabIndex);
315         if(periodView == null) return null;
316
317         Discussion result = periodView.getDiscussion();
318         return result;
319     }
320
321     /**
322      * 現在タブ選択中のDiscussionを返す。
323      *
324      * <p>Periodに関係ないタブが選択されていたらnullを返す。
325      *
326      * @return 現在選択中のDiscussion
327      */
328     public Discussion currentDiscussion(){
329         int tabIndex = getSelectedIndex();
330         Discussion result = getDiscussion(tabIndex);
331         return result;
332     }
333
334     /**
335      * フォント描画設定を変更する。
336      *
337      * <p>設定は各PeriodViewに委譲される。
338      *
339      * @param fontInfo フォント
340      */
341     public void setFontInfo(FontInfo fontInfo){
342         Objects.nonNull(fontInfo);
343         this.fontInfo = fontInfo;
344
345         getPeriodViewList().forEach(periodView -> {
346             periodView.setFontInfo(this.fontInfo);
347         });
348
349         return;
350     }
351
352     /**
353      * 発言表示設定を変更する。
354      *
355      * <p>設定は各PeriodViewに委譲される。
356      *
357      * @param dialogPref 発言表示設定
358      */
359     public void setDialogPref(DialogPref dialogPref){
360         Objects.nonNull(dialogPref);
361         this.dialogPref = dialogPref;
362
363         getPeriodViewList().forEach(periodView -> {
364             periodView.setDialogPref(this.dialogPref);
365         });
366
367         return;
368     }
369
370     /**
371      * ActionListenerを追加する。
372      *
373      * <p>配下のDiscussionへもリスナは登録される。
374      *
375      * @param listener リスナー
376      */
377     public void addActionListener(ActionListener listener){
378         this.listenerList.add(ActionListener.class, listener);
379
380         getPeriodViewList().stream()
381                 .map(PeriodView::getDiscussion)
382                 .forEach(discussion ->{
383                     discussion.addActionListener(listener);
384                 });
385
386         return;
387     }
388
389     /**
390      * ActionListenerを削除する。
391      *
392      * @param listener リスナー
393      */
394     public void removeActionListener(ActionListener listener){
395         this.listenerList.remove(ActionListener.class, listener);
396
397         getPeriodViewList().stream()
398                 .map(PeriodView::getDiscussion)
399                 .forEach(discussion ->{
400                     discussion.removeActionListener(listener);
401                 });
402
403         return;
404     }
405
406     /**
407      * ActionListenerを列挙する。
408      *
409      * @return すべてのActionListener
410      */
411     public ActionListener[] getActionListeners(){
412         return getListeners(ActionListener.class);
413     }
414
415     /**
416      * AnchorHitListenerを追加する。
417      *
418      * <p>配下のDiscussionへもリスナは登録される。
419      *
420      * @param listener リスナー
421      */
422     public void addAnchorHitListener(AnchorHitListener listener){
423         this.listenerList.add(AnchorHitListener.class, listener);
424
425         getPeriodViewList().stream()
426                 .map(PeriodView::getDiscussion)
427                 .forEach(discussion -> {
428                     discussion.addAnchorHitListener(listener);
429                 });
430
431         return;
432     }
433
434     /**
435      * AnchorHitListenerを削除する。
436      *
437      * @param listener リスナー
438      */
439     public void removeAnchorHitListener(AnchorHitListener listener){
440         this.listenerList.remove(AnchorHitListener.class, listener);
441
442         getPeriodViewList().stream()
443                 .map(PeriodView::getDiscussion)
444                 .forEach(discussion -> {
445                     discussion.removeAnchorHitListener(listener);
446                 });
447
448         return;
449     }
450
451     /**
452      * AnchorHitListenerを列挙する。
453      *
454      * @return すべてのAnchorHitListener
455      */
456     public AnchorHitListener[] getAnchorHitListeners(){
457         return getListeners(AnchorHitListener.class);
458     }
459
460 }