OSDN Git Service

OSX上のJDK9で実行する場合にスクリーンメニューのabout, preferences, quitのメニューが効かなくなっていた問題の修正
[charactermanaj/CharacterManaJ.git] / src / main / java / charactermanaj / Main.java
1 package charactermanaj;
2
3 import java.awt.Font;
4 import java.awt.GraphicsEnvironment;
5 import java.io.File;
6 import java.lang.Thread.UncaughtExceptionHandler;
7 import java.lang.reflect.Method;
8 import java.util.Enumeration;
9 import java.util.HashSet;
10 import java.util.Locale;
11 import java.util.Properties;
12 import java.util.TreeSet;
13 import java.util.logging.Level;
14 import java.util.logging.Logger;
15
16 import javax.management.JMException;
17 import javax.swing.JOptionPane;
18 import javax.swing.SwingUtilities;
19 import javax.swing.UIManager;
20 import javax.swing.plaf.FontUIResource;
21
22 import charactermanaj.clipboardSupport.ImageSelection;
23 import charactermanaj.graphics.io.ImageCacheMBeanImpl;
24 import charactermanaj.model.AppConfig;
25 import charactermanaj.model.util.StartupSupport;
26 import charactermanaj.ui.MainFrame;
27 import charactermanaj.ui.MainFramePartialForMacOSX9;
28 import charactermanaj.ui.ProfileListManager;
29 import charactermanaj.ui.SelectCharatersDirDialog;
30 import charactermanaj.util.AWTExceptionLoggingHandler;
31 import charactermanaj.util.ApplicationLoggerConfigurator;
32 import charactermanaj.util.ConfigurationDirUtilities;
33 import charactermanaj.util.DirectoryConfig;
34 import charactermanaj.util.ErrorMessageHelper;
35 import charactermanaj.util.JavaVersionUtils;
36 import charactermanaj.util.LocalizedResourcePropertyLoader;
37
38 /**
39  * エントリポイント用クラス
40  *
41  * @author seraphy
42  */
43 public final class Main implements Runnable {
44
45         /**
46          * ロガー.<br>
47          */
48         private static final Logger logger = Logger.getLogger(Main.class.getName());
49
50         /**
51          * Mac OS Xであるか?
52          */
53         private static final boolean isMacOSX;
54
55         /**
56          * Linuxであるか?
57          */
58         private static final boolean isLinux;
59
60         /**
61          * クラスイニシャライザ.<br>
62          * 実行環境に関する定数を取得・設定する.<br>
63          */
64         static {
65                 // Mac OS Xでの実行判定
66                 // システムプロパティos.nameは、すべてのJVM実装に存在する.
67                 // 基本ディレクトリの位置の決定に使うため、
68                 // なによりも、まず、これを判定しないとダメ.(順序が重要)
69                 String lcOS = System.getProperty("os.name").toLowerCase();
70                 isMacOSX = lcOS.startsWith("mac os x");
71                 isLinux = lcOS.indexOf("linux") >= 0;
72         }
73
74         /**
75          * ロガーの初期化.<br>
76          * 失敗しても継続する.<br>
77          */
78         private static void initLogger() {
79                 try {
80                         // ロガーの準備
81
82                         // ローカルファイルシステム上のユーザ定義ディレクトリから
83                         // ログの設定を読み取る.(OSにより、設定ファイルの位置が異なることに注意)
84                         ApplicationLoggerConfigurator.configure();
85
86                         if (JavaVersionUtils.getJavaVersion() >= 1.7) {
87                                 // java7以降は、sun.awt.exception.handlerが使えないので、
88                                 // EDTスレッドで未処理例外ハンドラを明示的に設定する.
89                                 final AWTExceptionLoggingHandler logHandler = new AWTExceptionLoggingHandler();
90                                 SwingUtilities.invokeLater(new Runnable() {
91                                         public void run() {
92                                                 final UncaughtExceptionHandler handler = Thread
93                                                                 .getDefaultUncaughtExceptionHandler();
94                                                 Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionHandler() {
95                                                         public void uncaughtException(Thread t, Throwable ex) {
96                                                                 logHandler.handle(ex);
97                                                                 if (handler != null) {
98                                                                         handler.uncaughtException(t, ex);
99                                                                 }
100                                                         }
101                                                 });
102                                         }
103                                 });
104
105                         } else {
106                                 // SwingのEDT内の例外ハンドラの設定 (ロギングするだけ)
107                                 // (ただし、unofficial trickである.)
108                                 System.setProperty("sun.awt.exception.handler",
109                                                 AWTExceptionLoggingHandler.class.getName());
110                         }
111
112                 } catch (Throwable ex) {
113                         // ロガーの準備に失敗した場合はロガーがないかもなので
114                         // コンソールに出力する.
115                         ex.printStackTrace();
116                         logger.log(Level.SEVERE, "logger initiation failed. " + ex, ex);
117                 }
118         }
119
120
121         /**
122          * UIをセットアップする.
123          *
124          * @throws Exception
125          *             いろいろな失敗
126          */
127         private static void setupUIManager(AppConfig appConfig) throws Exception {
128                 // System.setProperty("swing.aatext", "true");
129                 // System.setProperty("awt.useSystemAAFontSettings", "on");
130
131                 if (isMacOSX()) {
132                         // MacOSXであれば、スクリーンメニューを有効化
133                         System.setProperty("apple.laf.useScreenMenuBar", "true");
134                         System.setProperty(
135                                         "com.apple.mrj.application.apple.menu.about.name",
136                                         "CharacterManaJ");
137
138                         // Java7以降であればノーマライズをセットアップする.
139                         if (JavaVersionUtils.getJavaVersion() >= 1.7) {
140                                 charactermanaj.util.FileNameNormalizer.setupNFCNormalizer();
141                         }
142                 }
143
144                 // 実行プラットフォームのネイティブな外観にする.
145                 UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
146
147                 // JSpliderのvalueを非表示 (GTKではデフォルトで有効のため)
148                 UIManager.put("Slider.paintValue", Boolean.FALSE);
149
150
151                 // 優先するフォントファミリ中の実在するフォントファミリのセット(大文字小文字の区別なし)
152                 TreeSet<String> availablePriorityFontSets = new TreeSet<String>(
153                                 String.CASE_INSENSITIVE_ORDER);
154
155                 // 少なくともメニューが表示できるようなフォントを選択する
156                 Properties strings = LocalizedResourcePropertyLoader.getCachedInstance()
157                                 .getLocalizedProperties("menu/menu");
158                 HashSet<Character> useChars = new HashSet<Character>();
159                 Enumeration<?> enmStrings = strings.propertyNames();
160                 while (enmStrings.hasMoreElements()) {
161                         String propertyName = (String) enmStrings.nextElement();
162                         String propertyValue = strings.getProperty(propertyName);
163                         for (char ch : propertyValue.toCharArray()) {
164                                 useChars.add(ch);
165                         }
166                 }
167
168                 // 優先するフォントファミリの実在チェックと、もっとも優先されるフォントファミリの確定
169                 String selectedFontFamily = null;
170                 String fontPriorityStr = appConfig.getFontPriority();
171                 if (fontPriorityStr.trim().length() > 0) {
172                         String[] fontPriority = fontPriorityStr.split(",");
173                         for (String availableFontFamily : GraphicsEnvironment
174                                         .getLocalGraphicsEnvironment().getAvailableFontFamilyNames(Locale.ENGLISH)) {
175                                 for (String fontFamily : fontPriority) {
176                                         fontFamily = fontFamily.trim();
177                                         if (fontFamily.length() > 0) {
178                                                 if (availableFontFamily.equalsIgnoreCase(fontFamily)) {
179                                                         // 見つかった実在フォントが、現在のロケールのメニューを正しく表示できるか?
180                                                         Font font = Font.decode(availableFontFamily);
181                                                         logger.log(Level.INFO, "実在するフォントの確認:" + availableFontFamily);
182                                                         boolean canDisplay = false;
183                                                         for (char ch : useChars) {
184                                                                 canDisplay = font.canDisplay(ch);
185                                                                 if (!canDisplay) {
186                                                                         logger.log(Level.INFO,
187                                                                                         "このフォントはメニュー表示に使用できません: "
188                                                                                                         + selectedFontFamily + "/ch=" + ch);
189                                                                         break;
190                                                                 }
191                                                         }
192                                                         if (canDisplay) {
193                                                                 if (selectedFontFamily == null) {
194                                                                         // 最初に見つかったメニューを表示可能な優先フォント
195                                                                         selectedFontFamily = availableFontFamily;
196                                                                 }
197                                                                 // メニューを表示可能なフォントのみ候補に入れる
198                                                                 availablePriorityFontSets.add(fontFamily);
199                                                         }
200                                                 }
201                                         }
202                                 }
203                         }
204                         if (selectedFontFamily == null) {
205                                 // フォールバック用フォントとして「Dialog」を用いる.
206                                 // 仮想フォントファミリである「Dialog」は日本語も表示可能である.
207                                 selectedFontFamily = "Dialog";
208                         }
209                 }
210
211                 // デフォルトのフォントサイズ、0以下の場合はシステム標準のまま
212                 int defFontSize = appConfig.getDefaultFontSize();
213
214                 // UIデフォルトのフォント設定で、優先フォント以外のフォントファミリが指定されているものを
215                 // すべて最優先フォントファミリに設定する.
216                 // また、設定されたフォントサイズが0よりも大きければ、そのサイズに設定する.
217                 for (java.util.Map.Entry<?, ?> entry : UIManager.getDefaults()
218                                 .entrySet()) {
219                         Object key = entry.getKey();
220                         Object val = UIManager.get(key);
221                         if (val instanceof FontUIResource) {
222                                 FontUIResource fontUIResource = (FontUIResource) val;
223                                 int fontSize = fontUIResource.getSize();
224                                 String fontFamily = fontUIResource.getFamily();
225
226                                 if (defFontSize > 0) {
227                                         fontSize = defFontSize;
228                                 }
229
230                                 if (selectedFontFamily != null
231                                                 && !availablePriorityFontSets.contains(fontFamily)) {
232                                         // 現在のデフォルトUIに指定された優先フォント以外が設定されており、
233                                         // 且つ、優先フォントの指定があれば、優先フォントに差し替える.
234                                         if (logger.isLoggable(Level.FINE)) {
235                                                 logger.log(Level.FINE, "UIDefaultFont: " + key +
236                                                                 "= " + fontFamily + " -> " + selectedFontFamily);
237                                         }
238                                         fontFamily = selectedFontFamily;
239                                 }
240
241                                 fontUIResource = new FontUIResource(fontFamily,
242                                                 fontUIResource.getStyle(), fontSize);
243                                 UIManager.put(entry.getKey(), fontUIResource);
244                         }
245                 }
246         }
247
248         /**
249          * 初期処理およびメインフレームを構築する.<br>
250          * SwingのUIスレッドで実行される.<br>
251          */
252         public void run() {
253                 try {
254                         // アプリケーション設定の読み込み
255                         AppConfig appConfig = AppConfig.getInstance();
256                         appConfig.loadConfig();
257
258                         // UIManagerのセットアップ.
259                         try {
260                                 setupUIManager(appConfig);
261
262                         } catch (Exception ex) {
263                                 // UIManagerの設定に失敗した場合はログに書いて継続する.
264                                 ex.printStackTrace();
265                                 logger.log(Level.WARNING, "UIManager setup failed.", ex);
266                         }
267
268                         // クリップボードサポートの設定
269                         if (!ImageSelection.setupSystemFlavorMap()) {
270                                 logger.log(Level.WARNING,
271                                                 "failed to set the clipboard-support.");
272                         }
273
274                         // LANG, またはLC_CTYPEが設定されていない場合はエラーを表示する
275                         // OSXのJava7(Oracle)を実行する場合、環境変数LANGまたはLC_CTYPEに正しくファイル名の文字コードが設定されていないと
276                         // ファイル名を正しく取り扱えず文字化けするため、実行前に確認し警告を表示する。
277                         // ただし、この挙動はJava7u60では修正されているので、それ以降であれば除外する.
278                         int[] versions = JavaVersionUtils.getJavaVersions();
279                         if (isMacOSX()
280                                         && (versions[0] == 1 && versions[1] == 7 && versions[3] < 60)) {
281                                 String lang = System.getenv("LANG");
282                                 String lcctype = System.getenv("LC_CTYPE");
283                                 if ((lang == null || lang.trim().length() == 0)
284                                                 && (lcctype == null || lcctype.trim().length() == 0)) {
285                                         JOptionPane
286                                                         .showMessageDialog(
287                                                                         null,
288                                                                         "\"LANG\" or \"LC_CTYPE\" environment variable must be set.",
289                                                                         "Configuration Error",
290                                                                         JOptionPane.ERROR_MESSAGE);
291                                 }
292                         }
293
294                         // 起動時のシステムプロパティでキャラクターディレクトリが指定されていて実在すれば、それを優先する.
295                         File currentCharacterDir = null;
296                         String charactersDir = System.getProperty("charactersDir");
297                         if (charactersDir != null && charactersDir.length() > 0) {
298                                 File charsDir = new File(charactersDir);
299                                 if (charsDir.exists() && charsDir.isDirectory()) {
300                                         currentCharacterDir = charsDir;
301                                 }
302                         }
303
304                         if (currentCharacterDir == null) {
305                                 // キャラクターセットディレクトリの選択
306                                 File defaultCharacterDir = ConfigurationDirUtilities
307                                                 .getDefaultCharactersDir();
308                                 currentCharacterDir = SelectCharatersDirDialog
309                                                 .getCharacterDir(defaultCharacterDir);
310                                 if (currentCharacterDir == null) {
311                                         // キャンセルされたので終了する.
312                                         logger.info("luncher canceled.");
313                                         return;
314                                 }
315                         }
316
317                         // キャラクターデータフォルダの設定
318                         DirectoryConfig.getInstance().setCharactersDir(currentCharacterDir);
319
320                         // スタートアップ時の初期化
321                         StartupSupport.getInstance().doStartup();
322
323                         // デフォルトのプロファイルを開く.
324                         // (最後に使ったプロファイルがあれば、それが開かれる.)
325                         final MainFrame mainFrame = ProfileListManager.openDefaultProfile();
326                         if (isMacOSX()) {
327                                 try {
328                                         if (JavaVersionUtils.getJavaVersion() >= 9) {
329                                                 // OSXでJava9以降であればOracle実装でスクリーンメニュー類を設定する.
330                                                 MainFramePartialForMacOSX9.setupScreenMenu(mainFrame);
331
332                                         } else {
333                                                 // Java9未満であればeawtでスクリーンメニュー類を設定する.
334                                                 // MacOSXであればスクリーンメニューからのイベントをハンドルできるようにする.
335                                                 // OSXにしか存在しないクラスを利用するためリフレクションとしている.
336                                                 // ただしJDKによっては、Apple Java Extensionsがないことも予想されるので、
337                                                 // その場合はエラーにしない。
338                                                 Class<?> clz = Class
339                                                                 .forName("charactermanaj.ui.MainFramePartialForMacOSX");
340                                                 Method mtd = clz.getMethod("setupScreenMenu",
341                                                                 MainFrame.class);
342                                                 mtd.invoke(null, mainFrame);
343                                         }
344
345                                 } catch (Throwable ex) {
346                                         logger.log(Level.CONFIG, "Failed to setup the screen menu.", ex);
347                                 }
348                         }
349
350                         // 表示(および位置あわせ)
351                         mainFrame.showMainFrame();
352
353                 } catch (Throwable ex) {
354                         // なんらかの致命的な初期化エラーがあった場合、ログとコンソールに表示
355                         // ダイアログが表示されるかどうかは状況次第.
356                         ex.printStackTrace();
357                         logger.log(Level.SEVERE, "Application initiation failed.", ex);
358                         ErrorMessageHelper.showErrorDialog(null, ex);
359
360                         // メインフレームを破棄します.
361                         MainFrame.closeAllProfiles();
362                 }
363         }
364
365         /**
366          * エントリポイント.<br>
367          * 最初のメインフレームを開いたときにMac OS Xであればスクリーンメニューの登録も行う.<br>
368          *
369          * @param args
370          *            引数(未使用)
371          */
372         public static void main(String[] args) {
373                 // ロガー等の初期化
374                 initLogger();
375
376                 // MBeanのセットアップ
377                 try {
378                         ImageCacheMBeanImpl.setupMBean();
379
380                 } catch (JMException ex) {
381                         // 失敗しても無視して継続する.
382                         logger.log(Level.SEVERE, ex.getMessage(), ex);
383                 }
384
385                 // フレームの生成等は、SwingのEDTで実行する.
386                 SwingUtilities.invokeLater(new Main());
387         }
388
389         /**
390          * Mac OS Xで動作しているか?
391          *
392          * @return Max OS X上であればtrue
393          */
394         public static boolean isMacOSX() {
395                 return isMacOSX;
396         }
397
398         /**
399          * Mac OS X、もしくはlinuxで動作しているか?
400          *
401          * @return Mac OS X、もしくはlinuxで動作していればtrue
402          */
403         public static boolean isLinuxOrMacOSX() {
404                 return isLinux || isMacOSX;
405         }
406 }