charactermanaj/0000755000175000017500000000000011767047610013742 5ustar paulliupaulliucharactermanaj/build.xml0000644000175000017500000001111711767047610015564 0ustar paulliupaulliu CharacterManaJ charactermanaj/icon.icns0000644000175000017500000001154711767047610015560 0ustar paulliupaulliuicnsgis32wyrzuvþuvzypwسÃvwnoēu赝uoӥo赤rtss{nĪrqpˬlݡltq7"CE=#.7<B_84E9o֠jm"0?E>n|,K'hȶi|:DUQz۶o怠᳆̺ܵ}Z]fV\ZgYkYZk\\VyaZcZjhZUiUoYgvYUvUu{Wk~XXX\ϔS~ΗWbVUȪRϴxRXUΎx(:;4'040oͶ:.4,obӠ{NS05rD2h\e"?"Ngiu,5OCTlksxzw⛤ }Z]bV\ZbYkY[g\[Ws`ZcZghZV}fUoYevXVqUu{Wh~XXzX\ďTyÑW`WTɾRƫxSYUňr'672%.3/oõ;5F;o_Ӡjm"06mF>n|,K'O}k|:DUQRܷp{w}ؑ 䰓s8mkil32 Jsy|trxv{tv˜s|zyz£twxwxm½ļ©vwyx{}uxvDŽxnļ~uxýtxuyuyꩼtxxvѭ™txtyxnҪ›txxw©wwxysvznļwxxw´|vxysq{q½Ŀ{wxwǺuxxuq{vqĺvxvĽuxx*ww}ygļÿtxxɯŌtxyy{yeǼɬǑsyvʏtxzwzl{Ľߣŝt{vŕuzuqzpvСltv鴬nxln{soţlnᄁjmg|vgގUK{mRD-"?TU^hM1EKPȑg>"] !"%)0' =I꿛N1 185 'ذwd8\f`F)!|Ŋ=dolX1 lz7A{veywe; YAjҭM>HQ9%498yɥyU4] !$'*1& .1oƠO1 185 'ج_Xwd8\f`F([bNJ=dolX1 z70]t~{veywe; Y;O\}M charactermanaj/launch4j.xml0000644000175000017500000000126211767047610016175 0ustar paulliupaulliu false gui CharacterManaJ.jar charactermanaj.exe normal http://java.com/download false false icon.ico 1.5.0 preferJre 64 128 charactermanaj/.settings/0000755000175000017500000000000011767047610015660 5ustar paulliupaulliucharactermanaj/.settings/org.eclipse.core.runtime.prefs0000644000175000017500000000012311767047610023540 0ustar paulliupaulliu#Wed Jul 21 22:48:08 JST 2010 eclipse.preferences.version=1 line.separator=\r\n charactermanaj/.settings/org.eclipse.ltk.core.refactoring.prefs0000644000175000017500000000021111767047610025147 0ustar paulliupaulliu#Wed Jul 21 22:46:49 JST 2010 eclipse.preferences.version=1 org.eclipse.ltk.core.refactoring.enable.project.refactoring.history=false charactermanaj/.settings/org.eclipse.jdt.core.prefs0000644000175000017500000001164211767047610022646 0ustar paulliupaulliu#Fri Mar 11 06:56:48 JST 2011 eclipse.preferences.version=1 org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.5 org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve org.eclipse.jdt.core.compiler.compliance=1.5 org.eclipse.jdt.core.compiler.debug.lineNumber=generate org.eclipse.jdt.core.compiler.debug.localVariable=generate org.eclipse.jdt.core.compiler.debug.sourceFile=generate org.eclipse.jdt.core.compiler.problem.annotationSuperInterface=warning org.eclipse.jdt.core.compiler.problem.assertIdentifier=error org.eclipse.jdt.core.compiler.problem.autoboxing=ignore org.eclipse.jdt.core.compiler.problem.comparingIdentical=warning org.eclipse.jdt.core.compiler.problem.deadCode=warning org.eclipse.jdt.core.compiler.problem.deprecation=warning org.eclipse.jdt.core.compiler.problem.deprecationInDeprecatedCode=disabled org.eclipse.jdt.core.compiler.problem.deprecationWhenOverridingDeprecatedMethod=disabled org.eclipse.jdt.core.compiler.problem.discouragedReference=warning org.eclipse.jdt.core.compiler.problem.emptyStatement=ignore org.eclipse.jdt.core.compiler.problem.enumIdentifier=error org.eclipse.jdt.core.compiler.problem.fallthroughCase=ignore org.eclipse.jdt.core.compiler.problem.fatalOptionalError=enabled org.eclipse.jdt.core.compiler.problem.fieldHiding=ignore org.eclipse.jdt.core.compiler.problem.finalParameterBound=warning org.eclipse.jdt.core.compiler.problem.finallyBlockNotCompletingNormally=warning org.eclipse.jdt.core.compiler.problem.forbiddenReference=ignore org.eclipse.jdt.core.compiler.problem.hiddenCatchBlock=warning org.eclipse.jdt.core.compiler.problem.incompatibleNonInheritedInterfaceMethod=warning org.eclipse.jdt.core.compiler.problem.incompleteEnumSwitch=ignore org.eclipse.jdt.core.compiler.problem.indirectStaticAccess=ignore org.eclipse.jdt.core.compiler.problem.localVariableHiding=ignore org.eclipse.jdt.core.compiler.problem.methodWithConstructorName=warning org.eclipse.jdt.core.compiler.problem.missingDeprecatedAnnotation=ignore org.eclipse.jdt.core.compiler.problem.missingHashCodeMethod=ignore org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotation=ignore org.eclipse.jdt.core.compiler.problem.missingSerialVersion=warning org.eclipse.jdt.core.compiler.problem.missingSynchronizedOnInheritedMethod=ignore org.eclipse.jdt.core.compiler.problem.noEffectAssignment=warning org.eclipse.jdt.core.compiler.problem.noImplicitStringConversion=warning org.eclipse.jdt.core.compiler.problem.nonExternalizedStringLiteral=ignore org.eclipse.jdt.core.compiler.problem.nullReference=warning org.eclipse.jdt.core.compiler.problem.overridingPackageDefaultMethod=warning org.eclipse.jdt.core.compiler.problem.parameterAssignment=ignore org.eclipse.jdt.core.compiler.problem.possibleAccidentalBooleanAssignment=ignore org.eclipse.jdt.core.compiler.problem.potentialNullReference=ignore org.eclipse.jdt.core.compiler.problem.rawTypeReference=warning org.eclipse.jdt.core.compiler.problem.redundantNullCheck=ignore org.eclipse.jdt.core.compiler.problem.redundantSuperinterface=ignore org.eclipse.jdt.core.compiler.problem.specialParameterHidingField=disabled org.eclipse.jdt.core.compiler.problem.staticAccessReceiver=warning org.eclipse.jdt.core.compiler.problem.suppressWarnings=enabled org.eclipse.jdt.core.compiler.problem.syntheticAccessEmulation=ignore org.eclipse.jdt.core.compiler.problem.typeParameterHiding=warning org.eclipse.jdt.core.compiler.problem.uncheckedTypeOperation=warning org.eclipse.jdt.core.compiler.problem.undocumentedEmptyBlock=ignore org.eclipse.jdt.core.compiler.problem.unhandledWarningToken=warning org.eclipse.jdt.core.compiler.problem.unnecessaryElse=ignore org.eclipse.jdt.core.compiler.problem.unnecessaryTypeCheck=ignore org.eclipse.jdt.core.compiler.problem.unqualifiedFieldAccess=ignore org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownException=ignore org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionExemptExceptionAndThrowable=enabled org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionIncludeDocCommentReference=enabled org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionWhenOverriding=disabled org.eclipse.jdt.core.compiler.problem.unusedImport=warning org.eclipse.jdt.core.compiler.problem.unusedLabel=warning org.eclipse.jdt.core.compiler.problem.unusedLocal=warning org.eclipse.jdt.core.compiler.problem.unusedParameter=ignore org.eclipse.jdt.core.compiler.problem.unusedParameterIncludeDocCommentReference=enabled org.eclipse.jdt.core.compiler.problem.unusedParameterWhenImplementingAbstract=disabled org.eclipse.jdt.core.compiler.problem.unusedParameterWhenOverridingConcrete=disabled org.eclipse.jdt.core.compiler.problem.unusedPrivateMember=warning org.eclipse.jdt.core.compiler.problem.unusedWarningToken=warning org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning org.eclipse.jdt.core.compiler.source=1.5 charactermanaj/.settings/org.eclipse.jst.jsp.core.prefs0000644000175000017500000000235511767047610023461 0ustar paulliupaulliu#Wed Jul 21 22:46:49 JST 2010 eclipse.preferences.version=1 validation.actions-missing-required-attribute=1 validation.actions-non-empty-inline-tag=2 validation.actions-unknown-attribute=2 validation.directive-include-fragment-file-not-found=1 validation.directive-include-fragment-file-not-specified=1 validation.directive-taglib-duplicate-prefixes-different-uris=1 validation.directive-taglib-duplicate-prefixes-same-uris=-1 validation.directive-taglib-missing-prefix=1 validation.directive-taglib-missing-uri-or-tagdir=1 validation.directive-taglib-unresolvable-uri-or-tagdir=1 validation.el-general-syntax=1 validation.el-lexical-failure=-1 validation.java-=-1 validation.java-local-variable-is-never-used=-1 validation.java-null-local-variable-reference=-1 validation.java-potential-null-local-variable-reference=-1 validation.java-unused-import=-1 validation.translation-tag-class-not-found=2 validation.translation-tei-class-not-found=2 validation.translation-tei-class-not-instantiated=2 validation.translation-tei-class-runtime-exception=2 validation.translation-tei-message=1 validation.translation-usebean-ambiguous-type-info=2 validation.translation-usebean-invalid-id=1 validation.translation-usebean-missing-type-info=1 charactermanaj/.settings/org.eclipse.core.resources.prefs0000644000175000017500000000026711767047610024100 0ustar paulliupaulliu#Mon Jan 17 00:43:30 JST 2011 eclipse.preferences.version=1 encoding//resources/appinfo/about.html=UTF-8 encoding//resources/appinfo/about_ja.html=UTF-8 encoding/=UTF-8 charactermanaj/CharacterManaJ.app/0000755000175000017500000000000011767047443017330 5ustar paulliupaulliucharactermanaj/CharacterManaJ.app/Contents/0000755000175000017500000000000011767047477021134 5ustar paulliupaulliucharactermanaj/CharacterManaJ.app/Contents/Info.plist0000644000175000017500000000252111767047477023104 0ustar paulliupaulliu CFBundleName CharacterManaJ CFBundleIdentifier charactermanaj.Main CFBundleVersion 1.0 CFBundleAllowMixedLocalizations true CFBundleExecutable JavaApplicationStub CFBundleDevelopmentRegion Japan CFBundlePackageType APPL CFBundleShortVersionString 1.0 CFBundleSignature ???? CFBundleInfoDictionaryVersion 6.0 CFBundleIconFile icon.icns Java Properties appbase.dir $APP_PACKAGE/../ VMOptions -Xmx128m -Xms96m MainClass charactermanaj.Main JVMVersion 1.5+ ClassPath $JAVAROOT/CharacterManaJ.jar SplashFile $APP_PACKAGE/Contents/Resources/splash.png charactermanaj/CharacterManaJ.app/Contents/Resources/0000755000175000017500000000000011767047477023106 5ustar paulliupaulliucharactermanaj/CharacterManaJ.app/Contents/Resources/icon.icns0000644000175000017500000001154711767047477024724 0ustar paulliupaulliuicnsgis32wyrzuvþuvzypwسÃvwnoēu赝uoӥo赤rtss{nĪrqpˬlݡltq7"CE=#.7<B_84E9o֠jm"0?E>n|,K'hȶi|:DUQz۶o怠᳆̺ܵ}Z]fV\ZgYkYZk\\VyaZcZjhZUiUoYgvYUvUu{Wk~XXX\ϔS~ΗWbVUȪRϴxRXUΎx(:;4'040oͶ:.4,obӠ{NS05rD2h\e"?"Ngiu,5OCTlksxzw⛤ }Z]bV\ZbYkY[g\[Ws`ZcZghZV}fUoYevXVqUu{Wh~XXzX\ďTyÑW`WTɾRƫxSYUňr'672%.3/oõ;5F;o_Ӡjm"06mF>n|,K'O}k|:DUQRܷp{w}ؑ 䰓s8mkil32 Jsy|trxv{tv˜s|zyz£twxwxm½ļ©vwyx{}uxvDŽxnļ~uxýtxuyuyꩼtxxvѭ™txtyxnҪ›txxw©wwxysvznļwxxw´|vxysq{q½Ŀ{wxwǺuxxuq{vqĺvxvĽuxx*ww}ygļÿtxxɯŌtxyy{yeǼɬǑsyvʏtxzwzl{Ľߣŝt{vŕuzuqzpvСltv鴬nxln{soţlnᄁjmg|vgގUK{mRD-"?TU^hM1EKPȑg>"] !"%)0' =I꿛N1 185 'ذwd8\f`F)!|Ŋ=dolX1 lz7A{veywe; YAjҭM>HQ9%498yɥyU4] !$'*1& .1oƠO1 185 'ج_Xwd8\f`F([bNJ=dolX1 z70]t~{veywe; Y;O\}M׋infObN~N>! ?F?aĆ=5`5_M'Tq. VJp8dasZHOLn}&wVQygE0  HPEaP@<14r?#{2u$jtbDA{6=Q<("qCA*Oy\V;噹sM^|vWGyz?W15s-_̗)UKuZ17ߟl;=..s7VgjHUO^gc)1&v!.K `m)m$``/]?[xF QT*d4o(/lșmSqens}nk~8X<R5 vz)Ӗ9R,bRPCRR%eKUbvؙn9BħJeRR~NցoEx pHYs   IDATxx\y`3(EUX,ˎ,;n&l/q6/uҞYr\e9bYT(Eb `zo %[Ypp{?o+G.Y.lh.$3ɬD7l6^3 p̌w.2uf %*ZJ|*QNI6PPe Cs\Dc^bYl*cp8NL&L'NS(KJ%JDgGlyEI>K(9JIY,EݡD<;Fܵ6A+N\զYTJM6Ӆ+^umedD"LtL&H$bX<OtB,~Fr'6׽ekNTR%J$:#&a4PZ> I|Q%%lT.+7>5P$OD u^Qj e΢1i Da:R8dɹp*;1rTV7̉Xd=#NVYѯ+rZ>P*UBV1|.0Dfѱ1^fmmZ-.-LioWjAkTrH4 'I,.)7O8/jp-h8I燦}Aޢo2)2r9CX_NJ}1#2x4'ghRiLT$oհS\YjԗT< jq0^Եy淺ii]<.t Fsl2kxr^Vk_.ƢLcN›ʄPPKA_,J(&iHl$`ˤ>V}ryC~3&ہH(6=f3f^fQjKŅ<(=ت"st:F(tRQkZj6K\|SU=^G*?l:1,6˴*UU8dGYBV1* !_LՖme.%0A. 9(LKT|wsB>j Ccse MppEI&?4D#BFU:ljjRa)I+ߓ#peҴ^mX.B (j 毅*)YTHpB%QR O yCO$c)QѪO |pm/g4+J #}De1e/`L ,1\C\t3Ͳĭ*g AkRxʇx=\t떵ެg/YG*?ږ&ɀ%S f=D D$| n2IDB^)lUֹƾ JRUJVkٟ7>BN\:a *>UTAĹwA4*OcĐwcdUԖ^r'f:BosOɬi---?P;w`44ohY^_''w{ey`{RJMu^T5olv׸50)K?WyՆFz:fYUEU-*eZ6Z/*=JhrmOZtfӂB@y6WKgAu!t"ɲ7T6h،7175LDܵd6s wvڶmڪ߼^{ŋn#Ȫu.W877_/W\fW4^~1LádmFV%1PԌwޖwd" įY!R3+6؅0R̊lA _X|^QQEHC!PR x1LyuuEfcZJDSY"4=Ó-;**fҊb3^~ݻw+_җ8pe7ډ;Kaju&k ߥBUNt Xj)MC*-p^Zq94\ +(J'3-+JƣԗED)PR(U5eQPfYj*gTFm}[[i0T%º:twbbog.O]bz S|/?dߎYMo Lޫ z+l0dy "-8¨ F@ v:֠V(̰S4I¡ϷdKEԉK78~T{-%8-U3fx< օFx<8ʼK7?!T/0JWOqkhXz mky=71a^ţ?}? 媽r(eAv_ZRfq)qVxG@=O}\VӄBqkP(-4I_X~_}G\-_sekpDX7_F^oqoݾ ɠꇽ} Ru.xF߸yX gƼS]cSLyEv8voLP>ӗu]:IwV2,ъ2Dw/}o5⏽ŕ?i8vwRcLSaӶZ<_\XSeZa¡~m1p:h:|7F='fq|*﬍UX +mYpFKA_RkZw7B؟]sOww/WW:G~qe=U+]c%^0tl|t˖-_ʗs8iwɠ!eaxWotxLp%dXݧեׂM"7}8:-jfq6v-l /js\QثoW g|dg>Yx7x#t{8}c3 ;>XI;7/kva@gMgY9\ÑrD~)_ԯxm])8췸B @,"*7fc;Pu*5KԬvCk,OCy^/x; %^¨RUֻ|~^Osfk!* s{:ܾ3csNtC I2a)msCPZ?_3‰Kp9<7?O@JmTͱTG|̚OZ$}A3=:˽ ##[>Tf}$!-Zav2s%`U. t+Dz5&M7lj'0>rg<4ۍԐYe'N{}ӖygfC5#p8>:SN:;g>G9DGUH > siQ&vzk](?0w~?FTKm\KC3rW-$geaɍ/YsGÀd"J(7^DA-3t-Mft |p|rm07! f~n=;TaiUZr$J]JXPY _SXYU VF*8xZTU^8YQYNiA%'*E78voqWuvuk};+ʴC<1Tj""!!yI4P\6]SF 97a_܊uKS}3IW+i9vTBӣMn82^d&Z IjԡtTXиҚDDPsvYgT'iNssuNxтW\_Q/>TdeR4iC9T&֪n=QJid{F'|q\dH@Fe}OR2۫g&0!z*a1! -h?rH  ٲ!GGhQ›\K;\לmwC(Nz>ݗK$IwZ ݲ{.z=AlVKrhPK|@+j\7nHNlv1X|¥:K[T?ˡIIDO4ḾQ>oͩܗ%E‚oGCOcOѽѺf*7"s˖&%0["76IeE S;=fjӧ+k,N i \Ha39֨'[[#z``#Ql{zճkp';p Fgzl;*/#*"M] F|5S^H_Y=X6/]8vAe*(:UNf4e%O*ЩR{GܚKيIcL -[ƻ|US$_6#Hh3#nZ*"’fp~R_2LM9v–#"XS էNؒcM"͕~H%*̳&D:X[ډ#?K} =5.wH$;q?7gv]!BOFaqs`5psSW, yp,ɱC Ql, cMrep4$$0y1gJNYIeοq "AYE^IOL}}G;;}3 j 2A*wرg_E۬/#d?xoCd E&;$Tɩy+2X/~M h~Sg[l#UPb;1_+843f?9JdðU!zd$*$uqמ]IJ(^f )7/bduu _nf;D|,XbtT@jNi'A7c3;LzPJXRO'KfK2t`&62;~z_\tvvj.Äv%"Rz@Z=LܾryJ: 3GyWd\ ^~ߤ;.ݓN =9HX80Hjl5|֫xef!S;~M.6_ɴFkNcQe F]sI YL%K6'wc:u:-9)4%6dqp)s;`)_h+60JMy%l\(pI1 ѽho2[,~Dro2!KF!BPf3yVM]h Jv IDAT`򤍞ĪܓCQ<Zulj>6XBHeF*Jǝ6ZoIAtcVV:bΡ#;v-#pɯySn˒# Tt8&bx\r&:gdC'F C3d&X3 v2R,'YOƅю0Wf=!PAԴKP-^>ft.% K.IK)mиv\:ߺ6U8/ ꓧ "b+J0,Lb'm"^#W#hhltzpϡ-B&X gѠ̈́kiW>U8.ߩ|5%?w y{kF[mr *N)c]Դ?s£r X1<8Ne14O Xێu.,e#" HzNfiQLNN>VP`fȐRPK,*H PDAhH&,iXub1BR$ɫ\(_!^ДeeUlfmz vH dRZkj,tj߼#B5xx7v7McOGjܼWxZ&&.? }<كkeɢd6sS,Hx09/Bډ7Bgw v S-ꏨL4c ̰OH,렺rWC,-+JBFf"[=c?[k/CXWGĺ>aK7x3`Q@}|`*J$ȼ+#Fx%n(QF.Pd.rB`ݞ1HƜsQLQwvx,}∹SYzǖbk!:Ow_׀xǐ|j+|,( VhyYBsxu-UժT C0s%Q`ɅQALL͈j1OK.QiC 5XZZ,m/-wۇІD> DoW\uS\}4[oG֝nܶ{ \*1;9b|31QA>|݀>_zy1苺k#B<#?AG'yi2@vYat TN c{;?~ݱgΊtP z=ˀ-a1sRR)[?ERt-hVg+hTzm{7WC~N!)`^fKDxDey\XftT LU: kfg2/U5[peF_^]bސ!:O+zC[-iΞ7OcmφH^B=N1ti<A/|S-c3|SaK@*|y=<( Fhךl3KԛwdshKd* `.2B-xB !d:0 PH]Ih ]sG/uU/!|EQQ&B:? ݦ9O _kg5S/B; ZY%o9]V%bLlb8`~{. mՆ)NH(Sdq1S&3i"Z ut<\Un07T'4.P9 8Lstj_ %X"1@4 P ?슉Xbm[L6hߴ|w*+-8ݐj0}gjIeOގc~΅*k]g/6hTdS~('C)7=" oڻ>Mpj9SS0A66v.CӮ vK$"E ^TeZ|M|$uِx_$<*1HGiҘAH7q?3L=%mZ E A`e{a (25. 6Ky)cc1`9d_u3 .bg~p~ڲ=]s{_|HX:x-65:ad[)K`$% VF O)B̷ 9% Yai$@"Rg:ǥۤbi2XJ ͝/\2 QoԢudLG& }f:[D68drr!MА*Jrse-aEFFFsICY o#bɐԅ;U)%}NZ"Ys/o432Kru&=}L꪿1D 6VjAq[b L@+k-$hXv-MpdJd`h&nMGP@/P2&*Ϥۂ $S!4BIjNuryB Bږ3 0rZ-A?Eq¾|9oXa'N:F2Sp `uv*8T|$%YC1+(-L(i ;dgg%Z{SΊb 5M|n~]c\1V$#{8y5{P["PXK"XoIR",@⸐W ת6Q'18M {4YODFLj-lU1R{'maQq$-Fүh<쮯~#'6 KHhFA|X&pF^ S" ^x˒3|I"唓LH/]F/}=Z^|k W>ץhb̑ c{{>)WJu9 DlcDc-%2 ɋAnqe$V"ɠdlr!WAb G%Bc܂2L ӑ}"mȪbX?@ iYq@R"\P&TWeiwF'ŪC݂gqY~y5!Z=߀|RǔZW%f&KXp 1,5;;3K /9>:Xy7zώ~O#}Q`[(gaCg0LpRj 0 wJh\LK|"&lD\gF!"ic$K5حft' G?F8BQv: #b#_^hTIOLQo2WN ԤLNV `p`wRZv Aegr7aGZK/vy2'j;RaԒpY`GaY|J=6yHRd12F)aB}#SѤbw#ARQauG m9XX:S\֍C/KMkI1q +*+Ӿ|WE ԀWsd`.|/ZtՎ֬?1ZG*,b*\^ʖBanګ nO_zvy䫯2.ϼ0z1, 3`x@Mx#`+ZN$6 lli4z87j2jJx"DMaT4fáHHԇC lȀ!f>?6 w !$jo !zɹ4; rQ uHd1 9il^! @x.i+-x~su4DO-{ia[tM $(ޘȶCHq_ c/?Zye]pur|4h-DwJVO0WV;2 fN|TQ܈&ZqyJU>&#,cP~qqkMBdҫR܌7(%}Rb~9$ᮻԡ T 0'qzB):QYӬS.YHYlwot $!92c?ܳrfec?› %U8Fy[>t/B= FcBZY2V҃?M߸Cnϕ IQPH0:93M 'B`U9« CL(EK&*"!|#s4y5'a 9BRsw&&^Tb]qgyC^e6M ׻^&t VOd6lglY[hS4LO}-3ŞR4}1gB) t.^(_$* R|1- ˉ7j2篿1Vp4J=ᙺJvêTp]lD7y(6uիz43H[o\T0 :/̖}wĠ TT CNx$F3/Anԇe G[ AP^fp_'R:]fJfpj!|Hւ9V{Zb8ݪRV)L%_4WH)p(߻u绐SqPefnWhL80mf77nyF~d0/T>#,ژJH*zRzʻbXC 2FebjBgǿގ-]\Yx9*O;?~Il3/KF1;_޼; QC? ,RWܷqإi(4Ņ_`@[6ϒvn2 Tq1-JZ!>rPH*?.I;r zJ!B}e4/QX h4UH̭BMTLƐ7#@'ue2+r0Bj.X~1iwUDzwt9M-8CnA[+eopp9矎 L^*M Dc F(bOnڿjsP[w'ΌY b(,QF lq{m=6vm _ u gJćSh!N[ΌYjD"PߤAz`eUg;64 `ӔiA{vi3Wiq_}m_Zjߴ%4dR>榅WK7y{bi^YE{[wȽ+s:9<uۉv[JD5y/aD 5?&!Q*EozN>y5Qn%Ju,{Dα8QTb: $\];1L@F9v,Dgr!o-[PbG`5:\E3ۚ7 jT.:@mRTbScls8: /l΍[kyhw oS, W,;5jF' IDATF{`JI(HY}~ﮏޣS[g'8_;:GxeW}s^4C !P,Q"vML ֺgwfcM g͠,KVHYAdFXEDWeN`ջ6 ɐP(ėfFsirj0U"¤CB$rQDdYYtnM:SEJ{.%/,)hc ˸:BUjdž©UwAܼoCյwF^-\GOt裷uW"7%iZiWexY3pA(ܻk'x$$FCYNd`|7xON@Xee"{2&CӜ/WNm*r,mo!U$ɒ>2$ T8zqHJ7&LΑiwpjO$$D$qE3mMF}8`-`/Ńd]7P*GFmi@ %a_8d*Չ55oD_ySKN_[vO`S/_8a ֫z8I+\YG~v&`̄}XAQbn|SlZ# l.9{ZG ;'xK@E*sd2ir3g,|W t!;T+w͍M04޶EӲdX, p˥`b  OfT*HM3;,.A$T08̫G$¡J; `łM7!z*kF;Wֹr 8H*~:K}'q%&9m.`&M:g4h !8>C  A!(loYS` +.xu޿| U~&F 0K%K۴?zC[yɴJS(a\·efԄvn±XHM&DR%Bݵ_}H4Y o\vEBrk:^Us;N~ X*B#oΛ3FRʮ| ŋy?xIEBRuyJk}o2Cino&2o1CdqQJe}UP-APQDqWCi4fi6k>wp e2T(O ͭ|9ooBmķ_:AYw4j|ϰ;k>c8kWQ .ZX~҂+QF713*c GDᶮT.}G4:وGDKݖӽnyŻ\mzˬrUD8HMAФVl <Ɨm['K+l9GEl,Z>#PV1|iLUY nWY=29MՈJFoXLZ\LR @X('E]׶Ppo$6xTEмyĀӪ UZ%l_6m̌Pߣ j"LTdvNI4:ϩCڬ5UV[݁h{d FMmYnuAFm#iM"+Ye{ Eh`')>ǎ{'y VirY.ŷYoi񁮘 7\5SPvֶ)<7R13D "kņT|4l׻/ {9+ z)bU\]T81Oڬ7z?'?~[?z_!["/p BJ?76QΠy zGhDP4 5{:$wmSkuUNgpxz!Yv1;::&''A]4hYYAf5&PH&"MMLseC9"8 ncM+j r`[΃'?zd)B ~8mO y!(A e&JI;]K](!7|_{wNAEѨK4:AlJCuaS'])@#jQG _2=?_n@r9Z5iuiFp8<00PWWW 2/DH7UmզEskUu.N9<\{Laeϲ<yeNɵN`,fˊKJ1x◽|&{Z&ʧ`M%@/|B7>G ~˚ #/~o?,/sߵaF}B>,>JR:*! |G#<95Íp5f٨54|PUd7 G立'$e J>s`Tj& Fa87FР f-$ʡKc,PR`9>ݶk8_x6t[IqQH]QZF_!wOC<߄o/ofb8y[`]N\G˹<>[c%pb S{ƀ0!.HE +(BeN =̏~?,)shTt؝UZpHFFF{7 D΁֍p Ts0{ +W..U-i!sP1Bl*l[;GG:Zfdj6?'>_~m=+4l|pwPCCEѣg;~?}0cس]Sc[Yrv/t̹P(,n./  Q.lXo -ՄBD8 V4V_멫z 􍣕oM A憇\W_dYM̀9oVw\$r~v^[ED| r'}cmݴ|M\rW7sv[/\,q\\ ⳿oͣ;ooU]^WVa\9ct*8 W9\]g&ooܳ+L75];}t\y~ɧ&&9GoT^A^yo[Rpb].Vl47/j}2D8M77TkV0zdZԎ0HtʂeY2E8陌S^?Mbr[͎>~<-:6Ӣ|^JVy"}~ Fɑ] ,!Y<r.ԕ?ٽm3ߘ捲2SdwZnj'aU"[;nS$~\"G*U tjaU'3|fŮvViUj;;90;z<ݮu֕mp+-i챱C0JL~?}Dl 8%=M<>o<ɔ'`&2}crzG`ItOU cG@@P,XVKܱGy]_S!vχ0UFWNM|es&o'i .T4jb{~Ž +bBn5*Yh 4|IE|7]M]:e`I{Iao8oP$5da7R' kGG|{ ctѨ8ߕQā475>];.M #~+TyHK/5M '8nR=w.imcޑxNUտ{q>al\8Xzu(|4J/ˮk}fb赧BJ2P:W۞FX/^?|Yp6`9LMf5_eϓ;- G{sh*RivZJک7B]֛Vߵr>ǵ)cڢ}xVECEB`& n1>at!^T3;:l oO߇i YK}BYGWΖ8@ߧ9x7Ô! i۫7l%c- &9;&Cϔ&%VSmS;=Ao,^O0u] ]^9#c";:ԹDVk8~Ua~p:%{:d|[8Ld>[>ubf9 .≳cp-S:Xeo(4rVG\YYsmsw4x>?ʐiƒi$8^3/|m@•; XOB89V)QM$/Mj.4# ϵml??W?,ix,eyBEfX8p<|WwF}Rvsuں==,HO |`d8=Ҵ~ɉxW%7ᩣKeMnd΍|'7y< J؛l{rRz-ϟORpJ!V0G0ʜ4IL{plL$Z`AI6uvj)) ;pY'fQdġw߂._SY?0xjWaO.ʺ_p:ҩ;WZahU8?#\]@]eRtBY_BT7-8^B}IEQEiW\pSbccT}py"ho}[x3DҞHH V!ɨ]yb jH[Lk)CYng?}nzk'/tStw⿋4jNhVdD-ؓ-N.2X8<4(4H|PuJ[R ,h .<=u7S frh"9NqˎN2_uXAVnXkp?߮}7߷7"9a$h(@"2쉢M#cR) @,Nve f?$xa_ T0>Ʉ7UWȅO:vZYڐl`r'iM+W[g' O?lkt\x dl :8DQ(p]O2﷒N@D$4a>ɋB:']i:y(;␇~ԉ-7pՋ[I˼-ja*8jG'VZc%뚘wu;Np4v:ZOOXQn~OՕ h+ZGFފmu+?'ޡc%7s\D9Qd4(Uy۪$d$@q0_WSg Ah 3'% =T)Rxho_4{iЊl)Ztu\|b[ot|6sb"1$k[\sm^xf߻Ѩ/瓩\2% [,KV \ψ!| Z"T;{ F0F'tRtS%b% !@1H>{{EJދ-[cjB |$^RHB yCfk%mӮgZ57lb4jvf{9sj|$W F\#!9 !eLON h4= a{ |s%SImpyNBEyXj,b}P<$A|FX aD=bI,L: Pf-9I7pH.|vάADxVmy˯BHbP v,0gpbHNeΌ[ \ `,)mOF@9/(+T8|!MPdHZQQ|8AΑe('Jr%ܭ p8Dt8_&xQIut0|p\_7=x / n(H(($G"I$C\[K4 ^)> hf́& vtRfHQo~n!ۗ)%UAd |pK>Y4E0=VLB~!Jb@|@'0ϣy8u. @#i4ukeBo;w p@tщw-YE'`( ο].TCL,O'}rDDVMjv&*sLVeU~96\NFvѣG8j5Y|>S,b )0%F@XI}Wjmt>_G.Iʆ3z}6HǕa j'z (!/4w&lY|п,+olA˾%u"6KEݠ&K8S@ yavF3&_ tH3 Tm IDAT<.m?"$DoH܅|~ٲ_/`RKv O8tj9͟nXP yߟm[&-Mn?.]h) .+'ojhtNd#0t& D1/4 ݂B0 ?|⥋c.w,$7xJ#^ ĝL&FxQӝ6Qg zr##±bv67qm,(qa{l}eWC* d1w΂ŕׁZE)boI5g0,lM 2H'PβK:/м {l/ p2LQr$ Dfq9ryf޾I4AȤ"!Tz^=|O>jy˳55 aV&Z&WP"i;Fo+C#'c" BG|~M.Px#kkk}y5k9x )4X/D= AqngSsj %\ 1`V^SC&͕+*H !Gc&n+f ׃~qu*kNJa;եmJ%0c29M唙6AW;"}Ͻ~O5kji5 H8No1؀(`uhÅ!&L~s^Co\k$Yl?&[mR3}; %YZ(~pPa*F2UY=j.x(9\`LBH+W"Boӧ8jkHاnY;#UJE٩53 ͒ŔA eᆌD,#V9W,>1ʻd,&H$ *+C,*i CbL4;`39 *sÆӢD_l۲bC(T1XpF'^0 c4/ =(UyaŚShnq(3Phpbq[F)*6-a?ģ0# w^_"GHWInn 8\^3)Nw .g O,da+hw#`M S*'CaPa ̨HbBd2Qy` -m|Aftx d: 8,;' ?޸a=`}ă/y~ҥR]ྤJR$ !ABoK>(y ra bIgIPP vJ8Lj%)DY2Y<4 f2EHЯ:[>S/t\S4m %$X[nu SvL%lK#Àdܩ[&m %I&"oDOK%0<5P i"hLO~!_Pĸ/`D.$^N4 ^M?AT:a=pQyAA< G֖K=с .0!1$|t3jgER|^ZoD(И1pEf.jZut5E cavY\>'I 8#p ld3d:7E[Ͷyㆽw]$S kP88,k5]2cLB%*AСHc>RXW[s֦c=>=moj"bnO QR pk F}? bPbs 0A3x,Jm_ 9|O>`>ѹB!aw .c1eW7 W{i%[q⫴]6No8\h~U?~m% "vh ".Wy}@(OɁPO_+uC͇CmzOrqK q4'!~ |S}}}bfAoP&ۥJ*]>Ckl G.SUy9YM'[urX(!4 h6 6] PGIdBv˾`UY5u9۳4ڧij,J|S-(RWU3= xC0Å HEcJ*DɌ T6n1w[FW"7D$|xjnor"f"+7sG~`͘.WX}#ݱW"!~HY.7kXJ}=OMW>A_~yT: :S[}(|MaQnI|U 05T:NW2 LYxg1ڈݩ[Lb֬X XY^S5ލ˗r ^;,nokmհJIP7AڞUm(bݙ~x3-Mg{v@ݎ\.("ˊ9" 1MV)oʲs8s/ơpD΂5 ԧO2OSV]wd֬؏~A#X(Jʊ@#<^AijH\\tJaFLu˖3wѾC˼"a NB,QΘ=XI Z'˖h3>\s =+qoT醩B]~hdX^i&NJxa#_8 EW hgg?~t+]WM?Yf]#L\bp(|LBgzukW f{@ !@9Aٺrr KQd 2uubwf3 Y,9C"7{`Ӈ3lU~E%! F -l|Pe05$' 3vL?_U3CmT`3;y;ۻoKiȮ{O>*RvXM[6"Jiyi%0{*<3' LaxE|$~<|ۺ o^ D*#!PͶ5e̤43w^#UIJKmi4|NL$sPҿ=_xviXlzUܑ74PCd/3%95]zk}P4Tתԙ:~$r 2u{h-x !;ofDq{Q?WLBg}YyꙹV\Q__aMS8(0@UVزdX,X!ʯBv:C?io2YKSNٲi.KDlCTˉ2[',糯\1^{~+7_ //W*S1}.Aұ)J@PN뷘o+gN7}H 0&!0L'xMND܊ʊcGOtM] 7[+t,]"&ҭ?믈 ʟܚP(qHºpJ29ήmvljv]`sͼJvpor@kd?ݽZ&[ףfpaP(~/uo}n.-&`r.j*p"=+.^Vœ83Nx.P4o|`{~h |Ji0^Ӯ;h?{ ;Z!0hZpK֛UkWר{Q`gW5lqY\tLQ^?L8AƽiyLӝ?50wb ESyNC\ux^]^;`zz8gNc_?V[cO}J 9ߴI =2|l% O2Jv+n{-|(!ZBdp ?2l߼]ΜK*/.+^b! Iu7, ^/qHxAAQ"Ii^T3pʢB:!ѹ<M'JbB0,JJY_W=.9,-;%rɈ4ӤrѴY8,.I߯;e=~suEƔprZtR(G"`ߠu\F6 -a(D縧wW3=8Y:\-VfJx"Ѫ7h M^V'VP4t_ {`bmk ټe"񌲢ESz9%P`] IDAT(UJYF@D, '\3M;}s1W40X<+|`[>k 7&3J ܁ T*z5 8cs/ZIxo *Lٶ+TOV]/I 79W(@kS-K/P+(麂ݺ7Yʌ\vZ1 p7kH[{z#@.wJ~?s9le]K ,SP`TvgΩ) MޫoqYuͷd+`Oc F< q7՝;Lº]{"sVUŃ{8[;XL֚+Vy ֽXR֬y`lH0Z":`S jEmaA4zߴm攪oNXV 8 L߸#PF5Qk3M8wtɎ˖HUFJ:}u}̂0.P%O`ꯜx j4暠)yء;|:[ 7-g ,lMFɁ<n˞xht> > aeҽ;xH-e25__bz- % p[$˫ -ho~4sکS yRZ|&@_K|f0D+$8'z>}~Jgۧ U; L=榆=cŚ1l`/5 hHCjq09tyz<~ӧm޾kGSs/J9u3Uj"&wm!D%w3H&JN3Te"L T6EVң "+Bd&`_ȧz۬ vzl-$WQYqf ޫ7XZtF;++*rVWVV@:ߴm'>߲h^ou0%!xH D%`0)** JX~El[M*3ޞƶx$Vf)I[,>e+Te/;_&ݑ0//l @Ę+c xoR=(l"C"PuDcirH^T|zEzzwm b8- a`?8EtD!BU"ZMTՈ ` ΔQGʪe-X2d4tW_qcm?t {/~0*K-[@ȅv&Ůā<:D ~IaWAa^aY NA^,km64 h]Œ1OrAKAXG\  0ED.R`s` %@x]R<|0@oZ8Un>ظNb YgP8fc  wS`P>𭃴ZUQJ\P. u] O ˔bª Ӱ1%DBX6x+ e{LDZfLSsFmnhR6h2á~r;:pĥTc<% zN#e6 rV_]rs<@$&ytNh8ڮь} Q">][`VxiC|w6ˀK ˜3; S]kkJVE$,,%%bXz|`Kÿ5KC?IڣC0BD2>JWrϦ7^lֿ_K"^\-ɀQZgryC]8+x&!ޙk2c.bp mnz /d9.I(O=ym9*ڐ 5Blo;z %S*:;l4W/*WKE! "ep,@{)&RA1 I^ (eX,jl;vnFӟ}ǹ|[ZTh2(vƆ 1P[dUhM9L& 1=.c^CM[N|C+`z:;?4ҪJHB֌I“-{2ɮsg.~5o2k\4X$F86#a)s)I 9Nod$k;j5sW{{јW3L4|0ڛPxh Yi wO=69@(Ȃ-P/ױB|9`sykV0\Fa6̱}VM#bi,1&SfMxA @Md!>qB-&ogjM8qV%|>Zg\ǥX4.&QBRHK(Zױ|_ΞqlR^- z=T " ~_0gAq]cyBÚ>|.vXXXdQDG3Y"zLnwn^<32K+]ř'M~o0$*fTIn)Τ538$Iegm~]Z )nϤK׸:IwMBBl%)TXK\M:R.D@'&s{템SZ_P$uVx,8`ןh8kVteԕ%YťB(! p]lc މB%O8D&PQ0Wh(?Yp,D{5,3Q)U_cQI6Jֱu1T{AcQg:|P%3̖-0d2,&鱸%X_SRT.ȁ@^Qfa8~Z>V >7h4%53fD0ZcQ+&3;G q'vp&y D7#d5}^-f#c tOP&Y=@.ǽv?y3'LƠ֜wGBg]?Ң2 ,S[F4k Î=G x\TUgi=>%0v)3rBg8#S$ƞLiK,BU[&!T(X3Npn6z`IM,} O-n_pA C3LRô~4b60IK|?&l,aAD!8_K t;:l&;7`FI%ĘΎ,jybI}w9rNn@=np_ʷvoM㢸Bxce˖@eDaW( BDC{茄&O*n#'9h(l vyG0a </9+H#g5 (3aL|h2%Z,l8,ļ ȱ.Oԃpnno,΀Ge)a_c,:*fU/oϾ{z%#+QHqS#q8}LE~#H˙ө˖n߼wʥYڑ4[2(v}!eDbV |Z|NmҐ\h)Ԋ o/E7_ nPΐEDN3* nM:pAY 6K*b :. HX"=ݺ%eIn^q&sf]%dy@Хsg/Q3A L_0j G~z/Ι[+ԙZ0~wLY% h*%ބp+QbdS7s-!0eX`to*_!*՞x[mل` xMNUwF!u~LOwԲGWg/O7}ퟨD[^|YB+$?Bu(Qex ! /}YIT@u\Ûr5h 'deaqj {vSา_ޱ@]-;zɬęUIv̚oR~Kb/,Zӧ4|%~V !jRbI+^w gߞvBbHuoygQ`%w5okyx' L"9z ,n;{A\HtJȱ!{G1ZɴeG + F 8ń:uٽgo}O[7jnE?Ĩ Psm[>OgVV/9^L'4n׽ol#ZZNn /~ݣ./2K95KуJ0@N+7`޲18mh GObbddpY[[:!,GfUL\ճc_>IK&G6K ]8l9oQ(}6I ȸPaH,eWRܚC>mӋS>og.Yu)B`@@]lp<"zE/s ! I$ve }p(5?> b b-֨*i~:ξkZÉc;zG] !od;_x`C]˕)>:D28/}><(AB=ÀxrBn;iJ WwW9Z͡oFmx4g jDag U_&jtp_?x7`gCFQc]sܟ/tefMz,$xMR?~6>m˦QWi=; RUNO?^H/ ,k| <_,u]"%]SԷĀgjidk6 Ά =yI_LӇ@Q=bgȨJ?-ZJ5Z)33f 弄JoT_%B|M| @OܦA0Cs__ȮX2Do* Rצ2msΊ&i5}s~"Ϯo-^,Ort?|gb'zx]ʇ\jL:9_hbE\Fz7%1#am߸1U}/H %֤)JF5޼y|[Dbڿhk\P"d O۩3h~+~aù_F:*kdIltȚ~g=nȝ5j-P ~pyDоuSټ}\Ӯ6#=rǝ7~sG&gTTB-J^7܎?L:}+oT@,q%24#Y?c#x0cësKpGO_[? Om<ʃ1_O5kŵ%JmtZ)UrN!ޅ[@㼸YIDATU Xw. G ~˗] VQtN 49CcП9f;H8 \P|E&c]#CyJ'L`*qؾeB]H:aR*VlGr3TrY6#۶,yR;a.}h4ԋ?mNwIB%"Ac(78TL+]K'BT^3U5UfNR?Ia n>؉BТGy5MV9'Q fP #1&gd,f_w 5rg.\t !N)\<ŕAdB0/ R !d||O}C#v)]VU`[?ű dD= {<1Y<NҒG P9j!gU@&y+ {֮AͰw(Eā! D̜R8{Ce{F^Uo=Fy}7Cp@oq=}[5 E<.RdJz cKNA;0tee,ui9~uYvJBP ү @Q9jR\Vg:zuF&L,UrVv7-HJO~hk={~ `i pҐM՞o*.YIp,#ŤI]X@<,0Hzby\A;jx]j&7p9aŨXt7*C`IDgw!'9]Ê5Wۚ1 yN(R1-?#[M` y{GA"50#Nñf[#Ll1))WN6}JJ1o~b>98g4ÌH]RƗJblU,G,K)DMz ,R(3 GPc[5{20[t9a|\zix]zs1#XBRHN_kݨy%#&HB4RL84UXP 6ͪ&ʍR| PBjN@LL `"wҍb0‰r2CR,}8HW8qL)*(L }PRWPOPwR-*K)eq)լu>m iI%8ETp*zɈj6tֶgXh#%+;H^zZ #"-4mz6@z vj;'[fI;!%"\v;1FڶP9:P;P -'mim 8 x`+m]`TzZe^opbyI)UUr!Ը IK::dT3'@YZ 6ܒ L,#C*AahPcuN*3?L N$[[CH@t|T fd. L:[Li=5FU)d/;7}响# *bޝɽ`!TFSVS풀[+4O39_h~N] jF:_m>e=6|7Ģٷ6t<Jv='a^dԂ5ZFkjjqe߳]O#nb6&d.%jvps2chܓp:LFnUbނh8t^-M]憮۩eW]+&ftd|^_ gI_/i`z]7 by3<AaeCK|~ taRn|]>gJSgLyIIͷU4 n{;LCb # `BZsh?5֧~FjKA47~o>Wپ6N< A)7${wMh65ڀDQEՕփǩIX*+`xOdP$HP&˗][>'B\k&Z 8a$'39v.O^59nyz/9D|Xxmf)]:A*,}5Hg"BC/~tC{x+F >$SAa lj,..Ѭ}8>"F|.0)ކryFm.^ysuTj LϔR,35 < /`%IT]} /#"- х~id >!6!?-?^JTnХB@sy)_-.b=*kHx屡T3^Xp)~BX$sE]? pRBG', !BfXHHS=e}i?5{f\{`Pn.T aۏ,)+Wl6?+wz$[SΛSd՜9J4U;Q&e0QӔCeAmAz{‡wϧ7px!_jr@.~y+I{6SW\-)&W3AlBLUO$|Q1Υ(ڠ|:= 4{bv( A}mœ?iY]G}KY v@+ +*4.A3J?1!M3?;tj;M43{;*) z5Cs?B!7a.rgϻef}WL(*eQIά!uUoCK_H|b~AVZŗKrIVUT#f^SJ59n7| (Su0H  :,gZ & eqji$D{]9p'w\(ZRX3:m^IQ]zG/.H*r@}!u_< I`Z_2'vg]``DV>".׫]dLJ[)\\]d1(jNP3WV(؀Nz$9SD1%w,P)N enr(k^ MJ@#O{I]͛ 8k71*Odꊳuφ gϖOyBNDb'ALN4ACx$WbI]z5EXp7F~ْ tcڎϯ͛q@W^v5K,`s;<+:%CVg}W:L=*|F?o[?OȞ5҅wރqώP0+>?~cҢH5RcϪpFmuD8T-]NQحlڸYfY)ٳCYA x)0C`ի\Ȯ(wB"{ۓ1fmujF*Vp\ ~$~3x^z4)laA=kQM30b:=6#j00kW>3glKKu8T+NcZN[RZ[0]eפZ'5ٔ(eBBT%Q<أJ7D{"mH||DV ]U 9=Gۂ!?,gzˁQ;0bjWCqv8XJP= _4k6ǪQ26mb8Y*׆'-x1ݣx͐:k#_uLin(Ғo{jwkBwkR|(}gGTTJE!ydn'X`FX'ZJjF'55bIaOl8iոhW{IaMĎ汦ZJ~NJG!9Mt ?'2P6++_ !HuQ+Ҡ*V' YF65·U28&SG^Tk=\ؗgKFȩ NbSȮAs^::*1HL-B%Bƨc>dcG8_l{3YS) [q "sŧpVX`qb\ NW-NjLLh[LhwLhLshLahLOhL=hL+hLhLhLh:LzhILp/..ERROR: Could not change the current working directory to %s ERRORCFURLGetFileSystemRepresentation() failed on main bundle CFBundleCopyBundleURL() failed on main bundle JavaCFBundleGetMainBundle() failed  888    XzRx 4 5 4T  * < N ` r        __" `BUBQ@___stack_chk_guardQr@___stderrp@dyld_stub_binderr@___CFConstantStringClassReferencer @_CFBundleCopyBundleURLr(@_CFBundleGetInfoDictionaryr0@_CFBundleGetMainBundler8@_CFDictionaryGetValuer@@_CFReleaserH@_CFURLGetFileSystemRepresentationrP@_MRJApplicationMainArgsrX@___stack_chk_failr`@_chdirrh@_exitrp@_fprintfrx@_fwriter@_launchJavaApplicationr@_perrorr@_strlcat__mh_execute_header (08@HPX`hpx    e<BEa/Jaw$,CKT  @@  __mh_execute_header_CFBundleCopyBundleURL_CFBundleGetInfoDictionary_CFBundleGetMainBundle_CFDictionaryGetValue_CFRelease_CFURLGetFileSystemRepresentation_MRJApplicationMainArgs___CFConstantStringClassReference___stack_chk_fail___stack_chk_guard___stderrp_chdir_exit_fprintf_fwrite_launchJavaApplication_perror_strlcatdyld_stub_binderradr://5614542 *$B v0&@ com.apple.JavaApplicationStubYGk"MNmxZ.= . ?4 pp}Vo%H$m0ȧ#8_a00U0U% 0 +0U00UiwBNVBQ 0U#0+iGv k.@GM^06U/0-0+)'%http://www.apple.com/appleca/root.crl0  *H cLrѦKX~̫м'0=ƭaV }lٝRb$0ыbj?[DޫۭK?QKK:IaNՀzľ[Wb*Fר7?8 8B]㫚V>C '"W:vG0<9sb̡G˟!+oW6f㍙D4EY X')7dCf)d^+-?BpK"hͤo000  *H 0b1 0 UUS10U  Apple Inc.1&0$U Apple Certification Authority10U Apple Root CA0 060425214036Z 350209214036Z0b1 0 UUS10U  Apple Inc.1&0$U Apple Certification Authority10U Apple Root CA0"0  *H 0 䑩 GP^y-6WLUKl"0>P Af$kУ*z G[73Mir]_%UM] d5#KYPXPg ˬ, op?0C=+I(ε^=: !.t< bqGSU/ApLE~LkPAtb A30XZ2hesg^eIv3ew-z0v0U0U00U+iGv k.@GM^0U#0+iGv k.@GM^0U 00 *Hcd00*+https://www.apple.com/appleca/0+0Reliance on this certificate by any party assumes acceptance of the then applicable standard terms and conditions of use, certificate policy and certification practice statements.0  *H \6L-x팛wvw0O=G7@,ԱؾsdyO4آ>xk}9S 8ıO k+Y |@Vtӷ#;Go$ѷpE'mx~"5%kԢ$#s`[ /DH`8=&g 3j /Sj[dc3w:,V!ںsO6U٧2Bq~RB$*M^cKP 7uu!000  *H 01 0 UUS10U  Apple Inc.1&0$U Apple Certification Authority1301U*Apple Code Signing Certification Authority0 070223220256Z 150114220256Z0V1 0 UUS10U  Apple Inc.10U Apple Software10USoftware Signing0"0  *H 0 Mza/]3NE.·;sD`ϬA91Mٸ=Acz&ҵ/&!InvR")-||f]ny 2;d:*KEA3%|k=bK_; {|}}S7,mn%6 s=ҬSEKǿ눗|{a(tEw']ruS,V3[SR@Uῢ00U0 U00U% 0 +0U sS!p386CL0U#0iwBNVBQ 0U 00 *Hcd00)+http://www.apple.com/appleca/0+0Reliance on this certificate by any party assumes acceptance of the then applicable standard terms and conditions of use, certificate policy and certification practice statements.0=U604020.,http://www.apple.com/appleca/codesigning.crl0  *H +\_5$r*.;)ẃ,M|j@Zl*YEzI$ݴug7SF;KkW4ܗ/Jw.N| /T!DיWP) ; GIuC"P!Bə e@)狐q~uLȤ_y99+!bpQ|z#r|U=mfs a;~teQVyɁ10001 0 UUS10U  Apple Inc.1&0$U Apple Certification Authority1301U*Apple Code Signing Certification Authority0 +0  *H WG~}[1xʐ.{figqH akufEoERTj([ס@XWຼIMIQdb,?ݚbPo+f2{ĒZLD'"~]pܡ7e)ٙ)$z@.8!NyܪeNRM鰐ݎӖZI3KH_(g<{ 6J3@|g)H5m\J\ cKJb;P%J(8__PAGEZERO__TEXT__text__TEXT`,` __cstring__TEXT$__unwind_info__TEXTHH__DATA __dyld__DATA __cfstring__DATA __data__DATA __common__DATA( 4__IMPORT0 __pointers__IMPORT0 __jump_table__IMPORT@0U@ 8__LINKEDIT@ 0P0(<2, P11 /usr/lib/dyldWflz?P` /System/Library/PrivateFrameworks/JavaApplicationLauncher.framework/Versions/A/JavaApplicationLauncher `,/System/Library/Frameworks/Foundation.framework/Versions/C/Foundation 4/usr/lib/libgcc_s.1.dylib 4o/usr/lib/libSystem.B.dylib h/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundationp5j]\$ML$ˉ\$UWVS,u E]5 $  ɍAu/D@Pu؉  ux0tС0tbED$$\UED$$FEt$/0E|$ \$t$$u$h% % U8]u}[$DžD$ D$D$<$Ɖ<$vD$KD$$$W@u7D$OD$$7$=pD$$1YD$ D$9D$%D$ D$.D$$]u}U8]u}[EEE ETDžtS$Aƅt.$D$4$3tt$ED$E$PIMU]u}'(D$ D$D$$]u}__dyld_make_delayed_module_initializer_calls__dyld_mod_term_funcs/..ERROR: Could not change the current working directory to %s ERRORCFURLGetFileSystemRepresentation() failed on main bundle CFBundleCopyBundleURL() failed on main bundle JavaCFBundleGetMainBundle() failed 444 <BEa$   4( K, h0 4 8 < @ D H .L PP tT  X ;Su   % !"@#$&'_NXArgc_NXArgv___progname__mh_execute_header_catch_exception_raise_catch_exception_raise_state_catch_exception_raise_state_identity_clock_alarm_reply_do_mach_notify_dead_name_do_mach_notify_no_senders_do_mach_notify_port_deleted_do_mach_notify_send_once_do_seqnos_mach_notify_dead_name_do_seqnos_mach_notify_no_senders_do_seqnos_mach_notify_port_deleted_do_seqnos_mach_notify_send_once_environ_receive_samples_CFBundleCopyBundleURL_CFBundleGetInfoDictionary_CFBundleGetMainBundle_CFDictionaryGetValue_CFRelease_CFURLGetFileSystemRepresentation_MRJApplicationMainArgs___CFConstantStringClassReference___keymgr_dwarf2_register_sections___sF__cthread_init_routine_atexit_chdir_errno_exit_fprintf_fwrite_launchJavaApplication_mach_init_routine_perror_strlcatradr://5614542 >$V v05p com.apple.JavaApplicationStubYGk"MNmxZ9)QJ}wSܼa{?9c(]F#IhWCQT.d8d4<L,7*OXW47S^3 l XlibSystem.B.dyliblibSystem.B.dylib 0 *H 01 0 +0 *H  000  *H 0b1 0 UUS10U  Apple Inc.1&0$U Apple Certification Authority10U Apple Root CA0 070214211919Z 150214211919Z01 0 UUS10U  Apple Inc.1&0$U Apple Certification Authority1301U*Apple Code Signing Certification Authority0"0  *H 0 q k% ]kYZ9#59H!' d8k P[]Tes/b8drm|iGa&,$j?̬)LIyuv LSxv2ʑ9Pjf K;-Ɉ-i¤*a/ۇ!L-rn?̣@Q-YN]e3'g$LJy]i>H$m0ȧ#8_a00U0U% 0 +0U00UiwBNVBQ 0U#0+iGv k.@GM^06U/0-0+)'%http://www.apple.com/appleca/root.crl0  *H cLrѦKX~̫м'0=ƭaV }lٝRb$0ыbj?[DޫۭK?QKK:IaNՀzľ[Wb*Fר7?8 8B]㫚V>C '"W:vG0<9sb̡G˟!+oW6f㍙D4EY X')7dCf)d^+-?BpK"hͤo000  *H 0b1 0 UUS10U  Apple Inc.1&0$U Apple Certification Authority10U Apple Root CA0 060425214036Z 350209214036Z0b1 0 UUS10U  Apple Inc.1&0$U Apple Certification Authority10U Apple Root CA0"0  *H 0 䑩 GP^y-6WLUKl"0>P Af$kУ*z G[73Mir]_%UM] d5#KYPXPg ˬ, op?0C=+I(ε^=: !.t< bqGSU/ApLE~LkPAtb A30XZ2hesg^eIv3ew-z0v0U0U00U+iGv k.@GM^0U#0+iGv k.@GM^0U 00 *Hcd00*+https://www.apple.com/appleca/0+0Reliance on this certificate by any party assumes acceptance of the then applicable standard terms and conditions of use, certificate policy and certification practice statements.0  *H \6L-x팛wvw0O=G7@,ԱؾsdyO4آ>xk}9S 8ıO k+Y |@Vtӷ#;Go$ѷpE'mx~"5%kԢ$#s`[ /DH`8=&g 3j /Sj[dc3w:,V!ںsO6U٧2Bq~RB$*M^cKP 7uu!000  *H 01 0 UUS10U  Apple Inc.1&0$U Apple Certification Authority1301U*Apple Code Signing Certification Authority0 070223220256Z 150114220256Z0V1 0 UUS10U  Apple Inc.10U Apple Software10USoftware Signing0"0  *H 0 Mza/]3NE.·;sD`ϬA91Mٸ=Acz&ҵ/&!InvR")-||f]ny 2;d:*KEA3%|k=bK_; {|}}S7,mn%6 s=ҬSEKǿ눗|{a(tEw']ruS,V3[SR@Uῢ00U0 U00U% 0 +0U sS!p386CL0U#0iwBNVBQ 0U 00 *Hcd00)+http://www.apple.com/appleca/0+0Reliance on this certificate by any party assumes acceptance of the then applicable standard terms and conditions of use, certificate policy and certification practice statements.0=U604020.,http://www.apple.com/appleca/codesigning.crl0  *H +\_5$r*.;)ẃ,M|j@Zl*YEzI$ݴug7SF;KkW4ܗ/Jw.N| /T!DיWP) ; GIuC"P!Bə e@)狐q~uLȤ_y99+!bpQ|z#r|U=mfs a;~teQVyɁ10001 0 UUS10U  Apple Inc.1&0$U Apple Certification Authority1301U*Apple Code Signing Certification Authority0 +0  *H C'Wș"Sܳ4,<*#+ge"=qfSG@,D0'ݞSlaE.bxk>lkO}lT[k Kl&H1z$B!9XC"ID [ɇFoĔ{==tJp[@n*fOv- raml4J#mGc\KdJrUg  <8__PAGEZEROH__TEXT __text__TEXT __symbol_stub1__TEXT*__cstring__TEXT,__const__TEXT/__DATA0 __dyld__DATA0 __la_symbol_ptr__DATA0 -__nl_symbol_ptr__DATA0 Z__cfstring__DATA0 __data__DATA1!__common__DATA14__bss__DATA18__LINKEDIT@ 00F5 P33_3H  /usr/lib/dyldFU>@docD{q_( /System/Library/PrivateFrameworks/JavaApplicationLauncher.framework/Versions/A/JavaApplicationLauncher `,/System/Library/Frameworks/Foundation.framework/Versions/C/Foundation 4o/usr/lib/libSystem.B.dylib:|: x8!T!48!z8;cW{:|H |<@!= |#x|yx|+x!b11<@1$/8I@0= 9),H4|i|lxN!HL//@|Ix8B|t/@<@fx"1H8/@<@;FB0B/A|I|LxN!<@B0ĀB/A|I|LxN!H m<@B1/A|I|LxN!<`8<8c,H<| | xN!<`8@8c-H<`888c-H<`8H8c-4H<`8L8c-LH<`8P8c-lHA@/A(8/AH/AL/AP/A|I|Lx?;;-N!|~xHЀ8x| | xN!8CH//@|Cx8B|t/@=`9 9k- |t/A |H9k|t@`9)K؀Hx| | xN!/A<Lx8| | xN!/AP| | xN!,@;@A0<`8D8c-HUaD/AH <@8xexB0Fx#xHyH =0}=9N =`k0}iN `|B!H M<_BlB/A+A<8cDH <8cdH 8!P|N ``||}x!8`-|#xH/aLA|~x8AL```@(@^/(A H/AD8^|xKL8`-H /A| x| xN!xHH|dx8`-HQ8!`|N `|a||x!|#x8`8(HE8(|}x8`-}Hx}8`-H8!`a|N |BA!|zy<_; <_; <_;b AT?<x; 8 xHxxx|~xCxHdxex||xxCxH|{x<_xa8x" 8889HU8!pA|N |B!H|~yA<8 LH/AX<8 \H/AD<88c `H/A,<88H-H,A|i|lxN!8!Px|H<8!P|N |AB!|#x|+xHe|~yA;888xH9|}xxH/Al<8x8HxH)/@0<<xc @88cH<8c HIH\KcxDxH8`HH<<889 @8c 8H <<88. @8c H8H8`8!`A|N |B!|{x|#xH|}yAHu|yyA<8 H|~yA<888`8 H|dx8`H%||xxH|}xHa@4/AxH)<`@A<x8 H|xy@<x8 H!|~yAH%|}xHM@;X<x8@`xH/A;8x8 ;xHxH8c}!I88BTB>+A 8;<88cH}/@<_8 H <_8 /AxHa/AH xHM/AxKHPKycxDx%xHH<xdxExK H(<<88 ,8c 8H8`8! |N |B|H|=9 /@!|> x|_xlptx|>^~h<_8bHM|lx<_8B ~h^>|xtpl!|}N ||dxB8_!p8a98H 8a8|}xH1=?<_|~x9id)d;p|]xK x}=.BHu,@|}xxHmHI8!|N =`0}N =`0}N =`0$}N =`0}N =`0}N =`00}N =`04}N =`0,}N =`0}N =`0(}N =`0}N =`0<}N =`08}N =`0}N =`0 }N =`0}N =`0}N =`0}N =`0}N =`0}N =`0X}N =`0\}N =`0}N =`0}N =`0`}N =`0p}N =`0t}N =`0l}N =`0x}N =`0}N =`0|}N =`0d}N =`0}N =`0}N =`0h}N =`0}N =`0}N =`0P}N =`0 }N =`0H}N =`0L}N =`0T}N =`0}N =`0@}N =`0D}N __dyld_make_delayed_module_initializer_calls__dyld_image_count__dyld_get_image_name__dyld_get_image_header__dyld_NSLookupSymbolInImage__dyld_NSAddressOfSymbollibobjc__objcInit__dyld_mod_term_funcsCannot launch applicationIt requires a newer javaQuitOldJavaAlertCannotLaunchApplicationProductVersion10.3/usr/lib/libobjc.dylib/..ERROR: Could not change the current working directory to %s ERRORCFURLGetFileSystemRepresentation() failed on main bundle CFBundleCopyBundleURL() failed on main bundle Java/System/Library/PrivateFrameworks/JavaApplicationLauncher.frameworkJNLPJVMVersion/System/Library/Frameworks/JavaVM.framework/Versions/1.3.11.3.11.3CFBundleGetMainBundle() failed fprintf$LDBL128libSystem."$"$"$"$"$"$"$"$"$"$"$"$"$"$"$"$"$"$"$"$"$"$"$"$"$"$"$"$"$"$"$"$"$"$"$"$"$"$"$"$"$"$"$"$"$---- ...,// C/P/X "P<aEB 1  1 1 4 1K 1h 1 1 1 1 1 1  1. 1P 1t 1 1 1%?Xo9Q]p*;AX{ "(.6M`hnv~0+P0+P0+P1+P1+P1 +P10+P1@+P1P+P1`+P1p+P !"#$%&'()*,012345678:;<=?@ABCDE8?$<!DE@A=B'(,6:201354;7#* %)&"C>9/-._NXArgc_NXArgv___progname__mh_execute_header_catch_exception_raise_catch_exception_raise_state_catch_exception_raise_state_identity_clock_alarm_reply_do_mach_notify_dead_name_do_mach_notify_no_senders_do_mach_notify_port_deleted_do_mach_notify_send_once_do_seqnos_mach_notify_dead_name_do_seqnos_mach_notify_no_senders_do_seqnos_mach_notify_port_deleted_do_seqnos_mach_notify_send_once_environ_receive_samples_CFBundleCopyBundleURL_CFBundleCopyLocalizedString_CFBundleCreate_CFBundleGetInfoDictionary_CFBundleGetMainBundle_CFBundleGetVersionNumber_CFDictionaryContainsKey_CFDictionaryGetTypeID_CFDictionaryGetValue_CFGetTypeID_CFRelease_CFStringGetCString_CFStringGetTypeID_CFStringHasPrefix_CFURLCreateWithFileSystemPath_CFURLGetFileSystemRepresentation_CFUserNotificationDisplayNotice_MRJApplicationMainArgs_NSAddImage_NSAddressOfSymbol_NSIsSymbolNameDefinedWithHint_NSLookupAndBindSymbolWithHint_NSLookupSymbolInImage__CFCopySystemVersionDictionary___CFConstantStringClassReference___keymgr_dwarf2_register_sections___keymgr_global___sF__cthread_init_routine__dyld_register_func_for_add_image__dyld_register_func_for_remove_image__init_keymgr__keymgr_get_and_lock_processwide_ptr__keymgr_set_and_unlock_processwide_ptr_abort_atexit_calloc_chdir_errno_exit_free_fwrite_launchJavaApplication_mach_init_routine_perror_stat_strcmp_strcpy_strlcat_strlcpy_strlenradr://5614542 N$f v0: com.apple.JavaApplicationStub;幑A:F2 S2" ڊ J5 /CGwVF9s@0_$˖q$N%Gb_ej!*4("c~ |L 0com.apple.translate 0libSystem.B.dylib 0 *H 01 0 +0 *H  000  *H 0b1 0 UUS10U  Apple Inc.1&0$U Apple Certification Authority10U Apple Root CA0 070214211919Z 150214211919Z01 0 UUS10U  Apple Inc.1&0$U Apple Certification Authority1301U*Apple Code Signing Certification Authority0"0  *H 0 q k% ]kYZ9#59H!' d8k P[]Tes/b8drm|iGa&,$j?̬)LIyuv LSxv2ʑ9Pjf K;-Ɉ-i¤*a/ۇ!L-rn?̣@Q-YN]e3'g$LJy]i>H$m0ȧ#8_a00U0U% 0 +0U00UiwBNVBQ 0U#0+iGv k.@GM^06U/0-0+)'%http://www.apple.com/appleca/root.crl0  *H cLrѦKX~̫м'0=ƭaV }lٝRb$0ыbj?[DޫۭK?QKK:IaNՀzľ[Wb*Fר7?8 8B]㫚V>C '"W:vG0<9sb̡G˟!+oW6f㍙D4EY X')7dCf)d^+-?BpK"hͤo000  *H 0b1 0 UUS10U  Apple Inc.1&0$U Apple Certification Authority10U Apple Root CA0 060425214036Z 350209214036Z0b1 0 UUS10U  Apple Inc.1&0$U Apple Certification Authority10U Apple Root CA0"0  *H 0 䑩 GP^y-6WLUKl"0>P Af$kУ*z G[73Mir]_%UM] d5#KYPXPg ˬ, op?0C=+I(ε^=: !.t< bqGSU/ApLE~LkPAtb A30XZ2hesg^eIv3ew-z0v0U0U00U+iGv k.@GM^0U#0+iGv k.@GM^0U 00 *Hcd00*+https://www.apple.com/appleca/0+0Reliance on this certificate by any party assumes acceptance of the then applicable standard terms and conditions of use, certificate policy and certification practice statements.0  *H \6L-x팛wvw0O=G7@,ԱؾsdyO4آ>xk}9S 8ıO k+Y |@Vtӷ#;Go$ѷpE'mx~"5%kԢ$#s`[ /DH`8=&g 3j /Sj[dc3w:,V!ںsO6U٧2Bq~RB$*M^cKP 7uu!000  *H 01 0 UUS10U  Apple Inc.1&0$U Apple Certification Authority1301U*Apple Code Signing Certification Authority0 070223220256Z 150114220256Z0V1 0 UUS10U  Apple Inc.10U Apple Software10USoftware Signing0"0  *H 0 Mza/]3NE.·;sD`ϬA91Mٸ=Acz&ҵ/&!InvR")-||f]ny 2;d:*KEA3%|k=bK_; {|}}S7,mn%6 s=ҬSEKǿ눗|{a(tEw']ruS,V3[SR@Uῢ00U0 U00U% 0 +0U sS!p386CL0U#0iwBNVBQ 0U 00 *Hcd00)+http://www.apple.com/appleca/0+0Reliance on this certificate by any party assumes acceptance of the then applicable standard terms and conditions of use, certificate policy and certification practice statements.0=U604020.,http://www.apple.com/appleca/codesigning.crl0  *H +\_5$r*.;)ẃ,M|j@Zl*YEzI$ݴug7SF;KkW4ܗ/Jw.N| /T!DיWP) ; GIuC"P!Bə e@)狐q~uLȤ_y99+!bpQ|z#r|U=mfs a;~teQVyɁ10001 0 UUS10U  Apple Inc.1&0$U Apple Certification Authority1301U*Apple Code Signing Certification Authority0 +0  *H %QVh4Dts78)1@DmtHd7Rͼ8mszEX)RWg'#o,vxMû.rQR݌̰s7GVZcEf!e2N/uV5]v~/(㱇R !R>΢&%#X}ތ`ɂ9*NZ4wrF[잖^e)U=^charactermanaj/CharacterManaJ.app/Contents/PkgInfo0000644000175000017500000000001011767047477022403 0ustar paulliupaulliuAPPL????charactermanaj/extlib/0000755000175000017500000000000011767047544015237 5ustar paulliupaulliucharactermanaj/.fbprefs0000644000175000017500000001610411767047610015374 0ustar paulliupaulliu#FindBugs User Preferences #Sun Feb 20 00:21:29 JST 2011 detectorAppendingToAnObjectOutputStream=AppendingToAnObjectOutputStream|true detectorBadAppletConstructor=BadAppletConstructor|false detectorBadResultSetAccess=BadResultSetAccess|true detectorBadSyntaxForRegularExpression=BadSyntaxForRegularExpression|true detectorBadUseOfReturnValue=BadUseOfReturnValue|true detectorBadlyOverriddenAdapter=BadlyOverriddenAdapter|true detectorBooleanReturnNull=BooleanReturnNull|true detectorCallToUnsupportedMethod=CallToUnsupportedMethod|false detectorCheckImmutableAnnotation=CheckImmutableAnnotation|true detectorCheckTypeQualifiers=CheckTypeQualifiers|true detectorCloneIdiom=CloneIdiom|true detectorComparatorIdiom=ComparatorIdiom|true detectorConfusedInheritance=ConfusedInheritance|true detectorConfusionBetweenInheritedAndOuterMethod=ConfusionBetweenInheritedAndOuterMethod|true detectorCrossSiteScripting=CrossSiteScripting|true detectorDoInsideDoPrivileged=DoInsideDoPrivileged|true detectorDontCatchIllegalMonitorStateException=DontCatchIllegalMonitorStateException|true detectorDontIgnoreResultOfPutIfAbsent=DontIgnoreResultOfPutIfAbsent|true detectorDontUseEnum=DontUseEnum|true detectorDroppedException=DroppedException|true detectorDumbMethodInvocations=DumbMethodInvocations|true detectorDumbMethods=DumbMethods|true detectorDuplicateBranches=DuplicateBranches|true detectorEmptyZipFileEntry=EmptyZipFileEntry|true detectorEqualsOperandShouldHaveClassCompatibleWithThis=EqualsOperandShouldHaveClassCompatibleWithThis|true detectorFinalizerNullsFields=FinalizerNullsFields|true detectorFindBadCast2=FindBadCast2|true detectorFindBadForLoop=FindBadForLoop|true detectorFindCircularDependencies=FindCircularDependencies|false detectorFindDeadLocalStores=FindDeadLocalStores|true detectorFindDoubleCheck=FindDoubleCheck|true detectorFindEmptySynchronizedBlock=FindEmptySynchronizedBlock|true detectorFindFieldSelfAssignment=FindFieldSelfAssignment|true detectorFindFinalizeInvocations=FindFinalizeInvocations|true detectorFindFloatEquality=FindFloatEquality|true detectorFindHEmismatch=FindHEmismatch|true detectorFindInconsistentSync2=FindInconsistentSync2|true detectorFindJSR166LockMonitorenter=FindJSR166LockMonitorenter|true detectorFindLocalSelfAssignment2=FindLocalSelfAssignment2|true detectorFindMaskedFields=FindMaskedFields|true detectorFindMismatchedWaitOrNotify=FindMismatchedWaitOrNotify|true detectorFindNakedNotify=FindNakedNotify|true detectorFindNonSerializableStoreIntoSession=FindNonSerializableStoreIntoSession|true detectorFindNonSerializableValuePassedToWriteObject=FindNonSerializableValuePassedToWriteObject|true detectorFindNonShortCircuit=FindNonShortCircuit|true detectorFindNullDeref=FindNullDeref|true detectorFindNullDerefsInvolvingNonShortCircuitEvaluation=FindNullDerefsInvolvingNonShortCircuitEvaluation|true detectorFindOpenStream=FindOpenStream|true detectorFindPuzzlers=FindPuzzlers|true detectorFindRefComparison=FindRefComparison|true detectorFindReturnRef=FindReturnRef|true detectorFindRunInvocations=FindRunInvocations|true detectorFindSelfComparison=FindSelfComparison|true detectorFindSelfComparison2=FindSelfComparison2|true detectorFindSleepWithLockHeld=FindSleepWithLockHeld|true detectorFindSpinLoop=FindSpinLoop|true detectorFindSqlInjection=FindSqlInjection|true detectorFindTwoLockWait=FindTwoLockWait|true detectorFindUncalledPrivateMethods=FindUncalledPrivateMethods|true detectorFindUnconditionalWait=FindUnconditionalWait|true detectorFindUninitializedGet=FindUninitializedGet|true detectorFindUnrelatedTypesInGenericContainer=FindUnrelatedTypesInGenericContainer|true detectorFindUnreleasedLock=FindUnreleasedLock|true detectorFindUnsatisfiedObligation=FindUnsatisfiedObligation|true detectorFindUnsyncGet=FindUnsyncGet|true detectorFindUselessControlFlow=FindUselessControlFlow|true detectorFormatStringChecker=FormatStringChecker|true detectorHugeSharedStringConstants=HugeSharedStringConstants|true detectorIDivResultCastToDouble=IDivResultCastToDouble|true detectorIncompatMask=IncompatMask|true detectorInconsistentAnnotations=InconsistentAnnotations|true detectorInefficientMemberAccess=InefficientMemberAccess|false detectorInefficientToArray=InefficientToArray|true detectorInfiniteLoop=InfiniteLoop|true detectorInfiniteRecursiveLoop=InfiniteRecursiveLoop|true detectorInfiniteRecursiveLoop2=InfiniteRecursiveLoop2|false detectorInheritanceUnsafeGetResource=InheritanceUnsafeGetResource|true detectorInitializationChain=InitializationChain|true detectorInstantiateStaticClass=InstantiateStaticClass|true detectorInvalidJUnitTest=InvalidJUnitTest|true detectorIteratorIdioms=IteratorIdioms|true detectorLazyInit=LazyInit|true detectorLoadOfKnownNullValue=LoadOfKnownNullValue|true detectorLostLoggerDueToWeakReference=LostLoggerDueToWeakReference|true detectorMethodReturnCheck=MethodReturnCheck|true detectorMultithreadedInstanceAccess=MultithreadedInstanceAccess|true detectorMutableLock=MutableLock|true detectorMutableStaticFields=MutableStaticFields|true detectorNaming=Naming|true detectorNumberConstructor=NumberConstructor|true detectorOverridingEqualsNotSymmetrical=OverridingEqualsNotSymmetrical|true detectorPreferZeroLengthArrays=PreferZeroLengthArrays|true detectorPublicSemaphores=PublicSemaphores|false detectorQuestionableBooleanAssignment=QuestionableBooleanAssignment|true detectorReadOfInstanceFieldInMethodInvokedByConstructorInSuperclass=ReadOfInstanceFieldInMethodInvokedByConstructorInSuperclass|true detectorReadReturnShouldBeChecked=ReadReturnShouldBeChecked|true detectorRedundantInterfaces=RedundantInterfaces|true detectorRepeatedConditionals=RepeatedConditionals|true detectorRuntimeExceptionCapture=RuntimeExceptionCapture|true detectorSerializableIdiom=SerializableIdiom|true detectorStartInConstructor=StartInConstructor|true detectorStaticCalendarDetector=StaticCalendarDetector|true detectorStringConcatenation=StringConcatenation|true detectorSuperfluousInstanceOf=SuperfluousInstanceOf|true detectorSuspiciousThreadInterrupted=SuspiciousThreadInterrupted|true detectorSwitchFallthrough=SwitchFallthrough|true detectorSynchronizeAndNullCheckField=SynchronizeAndNullCheckField|true detectorSynchronizeOnClassLiteralNotGetClass=SynchronizeOnClassLiteralNotGetClass|true detectorSynchronizingOnContentsOfFieldToProtectField=SynchronizingOnContentsOfFieldToProtectField|true detectorURLProblems=URLProblems|true detectorUncallableMethodOfAnonymousClass=UncallableMethodOfAnonymousClass|true detectorUnnecessaryMath=UnnecessaryMath|true detectorUnreadFields=UnreadFields|true detectorUseObjectEquals=UseObjectEquals|false detectorUselessSubclassMethod=UselessSubclassMethod|false detectorVarArgsProblems=VarArgsProblems|true detectorVolatileUsage=VolatileUsage|true detectorWaitInLoop=WaitInLoop|true detectorWrongMapIterator=WrongMapIterator|true detectorXMLFactoryBypass=XMLFactoryBypass|true detector_threshold=2 effort=default filter_settings=Medium|BAD_PRACTICE,CORRECTNESS,MT_CORRECTNESS,PERFORMANCE,STYLE|false filter_settings_neg=MALICIOUS_CODE,NOISE,I18N,SECURITY,EXPERIMENTAL| run_at_full_build=false charactermanaj/icon.ico0000644000175000017500000001246611767047610015377 0ustar paulliupaulliu  & h( @ tyx}~+#+x[xgb{YyyVx[~dgk}]|<1;||@B@oOoס˖ϜשΜќw2,2Y^{|.*.p׼տӳM7MTX}|~,#,gv¬<6<9-9@4@?7?UOUu^B^dddAA[v}|}%%pRps0)0C0CfLfgNgQ" !!!$$"''%**)110&&' ..=13Iopos``@@U88Kci{~x~rxsyX\m?@R33D""-"-1?>BT>BUHK^QUh92>nhn|\|e,",K?K'"'ONh}{jNjmSm""00065?mr;:85.4F4E;,9ooo_brx'(7"6:C7;E24=#%'..0734</0BRRlxxSRlYXtUUqTSny~WWr`bWVqTUp{{WWrhk~~XXtXXszXXs\\{ooYYuegvvXYuVUoqvUUouuccZZvgjhhZZwVUn}fiUUoYYu[Zvgk\\z[\yWVpsy`aZZw}}ZZw]]ybfVVr\\zZZubgYYvkkcharactermanaj/.project0000644000175000017500000000060611767047610015413 0ustar paulliupaulliu CharacterManaJ org.eclipse.jdt.core.javabuilder org.eclipse.jdt.core.javanature charactermanaj/lib/0000755000175000017500000000000011767047552014515 5ustar paulliupaulliucharactermanaj/characters/0000755000175000017500000000000011767047443016065 5ustar paulliupaulliucharactermanaj/bin/0000755000175000017500000000000011767047552014517 5ustar paulliupaulliucharactermanaj/docs/0000755000175000017500000000000011767047544014700 5ustar paulliupaulliucharactermanaj/src/0000755000175000017500000000000011767047543014536 5ustar paulliupaulliucharactermanaj/src/org/0000755000175000017500000000000011767047543015325 5ustar paulliupaulliucharactermanaj/src/org/apache/0000755000175000017500000000000011767047543016546 5ustar paulliupaulliucharactermanaj/src/org/apache/tools/0000755000175000017500000000000011767047543017706 5ustar paulliupaulliucharactermanaj/src/org/apache/tools/zip/0000755000175000017500000000000011767047544020511 5ustar paulliupaulliucharactermanaj/src/org/apache/tools/zip/UnparseableExtraFieldData.java0000644000175000017500000000761011767047544026363 0ustar paulliupaulliu/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.tools.zip; /** * Wrapper for extra field data that doesn't conform to the recommended format of header-tag + size + data. * *

The header-id is artificial (and not listed as a know ID in * {@link http://www.pkware.com/documents/casestudies/APPNOTE.TXT * APPNOTE.TXT}. Since it isn't used anywhere except to satisfy the * ZipExtraField contract it shouldn't matter anyway.

* @since Ant 1.8.1 */ public final class UnparseableExtraFieldData implements CentralDirectoryParsingZipExtraField { private static final ZipShort HEADER_ID = new ZipShort(0xACC1); private byte[] localFileData; private byte[] centralDirectoryData; /** * The Header-ID. * * @return a completely arbitrary value that should be ignored. */ public ZipShort getHeaderId() { return HEADER_ID; } /** * Length of the complete extra field in the local file data. * * @return The LocalFileDataLength value */ public ZipShort getLocalFileDataLength() { return new ZipShort(localFileData == null ? 0 : localFileData.length); } /** * Length of the complete extra field in the central directory. * * @return The CentralDirectoryLength value */ public ZipShort getCentralDirectoryLength() { return centralDirectoryData == null ? getLocalFileDataLength() : new ZipShort(centralDirectoryData.length); } /** * The actual data to put into local file data. * * @return The LocalFileDataData value */ public byte[] getLocalFileDataData() { return ZipUtil.copy(localFileData); } /** * The actual data to put into central directory. * * @return The CentralDirectoryData value */ public byte[] getCentralDirectoryData() { return centralDirectoryData == null ? getLocalFileDataData() : ZipUtil.copy(centralDirectoryData); } /** * Populate data from this array as if it was in local file data. * * @param buffer the buffer to read data from * @param offset offset into buffer to read data * @param length the length of data */ public void parseFromLocalFileData(byte[] buffer, int offset, int length) { localFileData = new byte[length]; System.arraycopy(buffer, offset, localFileData, 0, length); } /** * Populate data from this array as if it was in central directory data. * * @param buffer the buffer to read data from * @param offset offset into buffer to read data * @param length the length of data * @exception ZipException on error */ public void parseFromCentralDirectoryData(byte[] buffer, int offset, int length) { centralDirectoryData = new byte[length]; System.arraycopy(buffer, offset, centralDirectoryData, 0, length); if (localFileData == null) { parseFromLocalFileData(buffer, offset, length); } } } charactermanaj/src/org/apache/tools/zip/CentralDirectoryParsingZipExtraField.java0000644000175000017500000000274511767047544030620 0ustar paulliupaulliu/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package org.apache.tools.zip; import java.util.zip.ZipException; /** * {@link ZipExtraField ZipExtraField} that knows how to parse central * directory data. * * @since Ant 1.8.0 */ public interface CentralDirectoryParsingZipExtraField extends ZipExtraField { /** * Populate data from this array as if it was in central directory data. * @param data an array of bytes * @param offset the start offset * @param length the number of bytes in the array from offset * * @throws ZipException on error */ void parseFromCentralDirectoryData(byte[] data, int offset, int length) throws ZipException; } charactermanaj/src/org/apache/tools/zip/Simple8BitZipEncoding.java0000644000175000017500000001700011767047544025464 0ustar paulliupaulliu/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.tools.zip; import java.io.IOException; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Collections; import java.util.List; /** * This ZipEncoding implementation implements a simple 8bit character * set, which mets the following restrictions: * *
    *
  • Characters 0x0000 to 0x007f are encoded as the corresponding * byte values 0x00 to 0x7f.
  • *
  • All byte codes from 0x80 to 0xff are mapped to a unique unicode * character in the range 0x0080 to 0x7fff. (No support for * UTF-16 surrogates) *
* *

These restrictions most notably apply to the most prominent * omissions of java-1.4's {@link java.nio.charset.Charset Charset} * implementation, Cp437 and Cp850.

* *

The methods of this class are reentrant.

*/ @SuppressWarnings("unchecked") class Simple8BitZipEncoding implements ZipEncoding { /** * A character entity, which is put to the reverse mapping table * of a simple encoding. */ @SuppressWarnings("rawtypes") private static final class Simple8BitChar implements Comparable { public final char unicode; public final byte code; Simple8BitChar(byte code, char unicode) { this.code = code; this.unicode = unicode; } public int compareTo(Object o) { Simple8BitChar a = (Simple8BitChar) o; return this.unicode - a.unicode; } public String toString() { return "0x" + Integer.toHexString(0xffff & (int) unicode) + "->0x" + Integer.toHexString(0xff & (int) code); } } /** * The characters for byte values of 128 to 255 stored as an array of * 128 chars. */ private final char[] highChars; /** * A list of {@link Simple8BitChar} objects sorted by the unicode * field. This list is used to binary search reverse mapping of * unicode characters with a character code greater than 127. */ @SuppressWarnings("rawtypes") private final List reverseMapping; /** * @param highChars The characters for byte values of 128 to 255 * stored as an array of 128 chars. */ @SuppressWarnings("rawtypes") public Simple8BitZipEncoding(char[] highChars) { this.highChars = highChars; this.reverseMapping = new ArrayList(this.highChars.length); byte code = 127; for (int i = 0; i < this.highChars.length; ++i) { this.reverseMapping.add(new Simple8BitChar(++code, this.highChars[i])); } Collections.sort(this.reverseMapping); } /** * Return the character code for a given encoded byte. * * @param b The byte to decode. * @return The associated character value. */ public char decodeByte(byte b) { // code 0-127 if (b >= 0) { return (char) b; } // byte is signed, so 128 == -128 and 255 == -1 return this.highChars[128 + (int) b]; } /** * @param c The character to encode. * @return Whether the given unicode character is covered by this encoding. */ public boolean canEncodeChar(char c) { if (c >= 0 && c < 128) { return true; } Simple8BitChar r = this.encodeHighChar(c); return r != null; } /** * Pushes the encoded form of the given character to the given byte buffer. * * @param bb The byte buffer to write to. * @param c The character to encode. * @return Whether the given unicode character is covered by this encoding. * If false is returned, nothing is pushed to the * byte buffer. */ public boolean pushEncodedChar(ByteBuffer bb, char c) { if (c >= 0 && c < 128) { bb.put((byte) c); return true; } Simple8BitChar r = this.encodeHighChar(c); if (r == null) { return false; } bb.put(r.code); return true; } /** * @param c A unicode character in the range from 0x0080 to 0x7f00 * @return A Simple8BitChar, if this character is covered by this encoding. * A null value is returned, if this character is not * covered by this encoding. */ private Simple8BitChar encodeHighChar(char c) { // for performance an simplicity, yet another reincarnation of // binary search... int i0 = 0; int i1 = this.reverseMapping.size(); while (i1 > i0) { int i = i0 + (i1 - i0) / 2; Simple8BitChar m = (Simple8BitChar) this.reverseMapping.get(i); if (m.unicode == c) { return m; } if (m.unicode < c) { i0 = i + 1; } else { i1 = i; } } if (i0 >= this.reverseMapping.size()) { return null; } Simple8BitChar r = (Simple8BitChar) this.reverseMapping.get(i0); if (r.unicode != c) { return null; } return r; } /** * @see * org.apache.tools.zip.ZipEncoding#canEncode(java.lang.String) */ public boolean canEncode(String name) { for (int i=0;iThe extra data is expected to follow the recommendation of * {@link http://www.pkware.com/documents/casestudies/APPNOTE.TXT * APPNOTE.txt}:

*
    *
  • the extra byte array consists of a sequence of extra fields
  • *
  • each extra fields starts by a two byte header id followed by * a two byte sequence holding the length of the remainder of * data.
  • *
* *

Any extra data that cannot be parsed by the rules above will be * consumed as "unparseable" extra data and treated differently by the * methods of this class. Versions prior to Apache Commons Compress * 1.1 would have thrown an exception if any attempt was made to read * or write extra data not conforming to the recommendation.

* */ @SuppressWarnings({"unchecked", "rawtypes"}) public class ZipEntry extends java.util.zip.ZipEntry implements Cloneable { public static final int PLATFORM_UNIX = 3; public static final int PLATFORM_FAT = 0; private static final int SHORT_MASK = 0xFFFF; private static final int SHORT_SHIFT = 16; private int internalAttributes = 0; private int platform = PLATFORM_FAT; private long externalAttributes = 0; private LinkedHashMap/**/ extraFields = null; private UnparseableExtraFieldData unparseableExtra = null; private String name = null; /** * Creates a new zip entry with the specified name. * @param name the name of the entry * @since 1.1 */ public ZipEntry(String name) { super(name); } /** * Creates a new zip entry with fields taken from the specified zip entry. * @param entry the entry to get fields from * @since 1.1 * @throws ZipException on error */ public ZipEntry(java.util.zip.ZipEntry entry) throws ZipException { super(entry); byte[] extra = entry.getExtra(); if (extra != null) { setExtraFields(ExtraFieldUtils.parse(extra, true, ExtraFieldUtils .UnparseableExtraField.READ)); } else { // initializes extra data to an empty byte array setExtra(); } } /** * Creates a new zip entry with fields taken from the specified zip entry. * @param entry the entry to get fields from * @throws ZipException on error * @since 1.1 */ public ZipEntry(ZipEntry entry) throws ZipException { this((java.util.zip.ZipEntry) entry); setInternalAttributes(entry.getInternalAttributes()); setExternalAttributes(entry.getExternalAttributes()); setExtraFields(entry.getExtraFields(true)); } /** * @since 1.9 */ protected ZipEntry() { super(""); } /** * Overwrite clone. * @return a cloned copy of this ZipEntry * @since 1.1 */ public Object clone() { ZipEntry e = (ZipEntry) super.clone(); e.setInternalAttributes(getInternalAttributes()); e.setExternalAttributes(getExternalAttributes()); e.setExtraFields(getExtraFields(true)); return e; } /** * Retrieves the internal file attributes. * * @return the internal file attributes * @since 1.1 */ public int getInternalAttributes() { return internalAttributes; } /** * Sets the internal file attributes. * @param value an int value * @since 1.1 */ public void setInternalAttributes(int value) { internalAttributes = value; } /** * Retrieves the external file attributes. * @return the external file attributes * @since 1.1 */ public long getExternalAttributes() { return externalAttributes; } /** * Sets the external file attributes. * @param value an long value * @since 1.1 */ public void setExternalAttributes(long value) { externalAttributes = value; } /** * Sets Unix permissions in a way that is understood by Info-Zip's * unzip command. * @param mode an int value * @since Ant 1.5.2 */ public void setUnixMode(int mode) { // CheckStyle:MagicNumberCheck OFF - no point setExternalAttributes((mode << SHORT_SHIFT) // MS-DOS read-only attribute | ((mode & 0200) == 0 ? 1 : 0) // MS-DOS directory flag | (isDirectory() ? 0x10 : 0)); // CheckStyle:MagicNumberCheck ON platform = PLATFORM_UNIX; } /** * Unix permission. * @return the unix permissions * @since Ant 1.6 */ public int getUnixMode() { return platform != PLATFORM_UNIX ? 0 : (int) ((getExternalAttributes() >> SHORT_SHIFT) & SHORT_MASK); } /** * Platform specification to put into the "version made * by" part of the central file header. * * @return PLATFORM_FAT unless {@link #setUnixMode setUnixMode} * has been called, in which case PLATORM_UNIX will be returned. * * @since Ant 1.5.2 */ public int getPlatform() { return platform; } /** * Set the platform (UNIX or FAT). * @param platform an int value - 0 is FAT, 3 is UNIX * @since 1.9 */ protected void setPlatform(int platform) { this.platform = platform; } /** * Replaces all currently attached extra fields with the new array. * @param fields an array of extra fields * @since 1.1 */ public void setExtraFields(ZipExtraField[] fields) { extraFields = new LinkedHashMap(); for (int i = 0; i < fields.length; i++) { if (fields[i] instanceof UnparseableExtraFieldData) { unparseableExtra = (UnparseableExtraFieldData) fields[i]; } else { extraFields.put(fields[i].getHeaderId(), fields[i]); } } setExtra(); } /** * Retrieves all extra fields that have been parsed successfully. * @return an array of the extra fields */ public ZipExtraField[] getExtraFields() { return getExtraFields(false); } /** * Retrieves extra fields. * @param includeUnparseable whether to also return unparseable * extra fields as {@link UnparseableExtraFieldData} if such data * exists. * @return an array of the extra fields * @since 1.1 */ public ZipExtraField[] getExtraFields(boolean includeUnparseable) { if (extraFields == null) { return !includeUnparseable || unparseableExtra == null ? new ZipExtraField[0] : new ZipExtraField[] { unparseableExtra }; } List result = new ArrayList(extraFields.values()); if (includeUnparseable && unparseableExtra != null) { result.add(unparseableExtra); } return (ZipExtraField[]) result.toArray(new ZipExtraField[0]); } /** * Adds an extra field - replacing an already present extra field * of the same type. * *

If no extra field of the same type exists, the field will be * added as last field.

* @param ze an extra field * @since 1.1 */ public void addExtraField(ZipExtraField ze) { if (ze instanceof UnparseableExtraFieldData) { unparseableExtra = (UnparseableExtraFieldData) ze; } else { if (extraFields == null) { extraFields = new LinkedHashMap(); } extraFields.put(ze.getHeaderId(), ze); } setExtra(); } /** * Adds an extra field - replacing an already present extra field * of the same type. * *

The new extra field will be the first one.

* @param ze an extra field * @since 1.1 */ public void addAsFirstExtraField(ZipExtraField ze) { if (ze instanceof UnparseableExtraFieldData) { unparseableExtra = (UnparseableExtraFieldData) ze; } else { LinkedHashMap copy = extraFields; extraFields = new LinkedHashMap(); extraFields.put(ze.getHeaderId(), ze); if (copy != null) { copy.remove(ze.getHeaderId()); extraFields.putAll(copy); } } setExtra(); } /** * Remove an extra field. * @param type the type of extra field to remove * @since 1.1 */ public void removeExtraField(ZipShort type) { if (extraFields == null) { throw new java.util.NoSuchElementException(); } if (extraFields.remove(type) == null) { throw new java.util.NoSuchElementException(); } setExtra(); } /** * Removes unparseable extra field data. */ public void removeUnparseableExtraFieldData() { if (unparseableExtra == null) { throw new java.util.NoSuchElementException(); } unparseableExtra = null; setExtra(); } /** * Looks up an extra field by its header id. * * @return null if no such field exists. */ public ZipExtraField getExtraField(ZipShort type) { if (extraFields != null) { return (ZipExtraField) extraFields.get(type); } return null; } /** * Looks up extra field data that couldn't be parsed correctly. * * @return null if no such field exists. */ public UnparseableExtraFieldData getUnparseableExtraFieldData() { return unparseableExtra; } /** * Parses the given bytes as extra field data and consumes any * unparseable data as an {@link UnparseableExtraFieldData} * instance. * @param extra an array of bytes to be parsed into extra fields * @throws RuntimeException if the bytes cannot be parsed * @since 1.1 * @throws RuntimeException on error */ public void setExtra(byte[] extra) throws RuntimeException { try { ZipExtraField[] local = ExtraFieldUtils.parse(extra, true, ExtraFieldUtils.UnparseableExtraField.READ); mergeExtraFields(local, true); } catch (Exception e) { // actually this is not be possible as of Ant 1.8.1 throw new RuntimeException("Error parsing extra fields for entry: " + getName() + " - " + e.getMessage(), e); } } /** * Unfortunately {@link java.util.zip.ZipOutputStream * java.util.zip.ZipOutputStream} seems to access the extra data * directly, so overriding getExtra doesn't help - we need to * modify super's data directly. * * @since 1.1 */ protected void setExtra() { super.setExtra(ExtraFieldUtils.mergeLocalFileDataData(getExtraFields(true))); } /** * Sets the central directory part of extra fields. */ public void setCentralDirectoryExtra(byte[] b) { try { ZipExtraField[] central = ExtraFieldUtils.parse(b, false, ExtraFieldUtils.UnparseableExtraField.READ); mergeExtraFields(central, false); } catch (Exception e) { throw new RuntimeException(e.getMessage(), e); } } /** * Retrieves the extra data for the local file data. * @return the extra data for local file * @since 1.1 */ public byte[] getLocalFileDataExtra() { byte[] extra = getExtra(); return extra != null ? extra : new byte[0]; } /** * Retrieves the extra data for the central directory. * @return the central directory extra data * @since 1.1 */ public byte[] getCentralDirectoryExtra() { return ExtraFieldUtils.mergeCentralDirectoryData(getExtraFields(true)); } /** * Make this class work in JDK 1.1 like a 1.2 class. * *

This either stores the size for later usage or invokes * setCompressedSize via reflection.

* @param size the size to use * @deprecated since 1.7. * Use setCompressedSize directly. * @since 1.2 */ public void setComprSize(long size) { setCompressedSize(size); } /** * Get the name of the entry. * @return the entry name * @since 1.9 */ public String getName() { return name == null ? super.getName() : name; } /** * Is this entry a directory? * @return true if the entry is a directory * @since 1.10 */ public boolean isDirectory() { return getName().endsWith("/"); } /** * Set the name of the entry. * @param name the name to use */ protected void setName(String name) { this.name = name; } /** * Get the hashCode of the entry. * This uses the name as the hashcode. * @return a hashcode. * @since Ant 1.7 */ public int hashCode() { // this method has severe consequences on performance. We cannot rely // on the super.hashCode() method since super.getName() always return // the empty string in the current implemention (there's no setter) // so it is basically draining the performance of a hashmap lookup return getName().hashCode(); } /** * The equality method. In this case, the implementation returns 'this == o' * which is basically the equals method of the Object class. * @param o the object to compare to * @return true if this object is the same as o * @since Ant 1.7 */ public boolean equals(Object o) { return (this == o); } /** * If there are no extra fields, use the given fields as new extra * data - otherwise merge the fields assuming the existing fields * and the new fields stem from different locations inside the * archive. * @param f the extra fields to merge * @param local whether the new fields originate from local data */ private void mergeExtraFields(ZipExtraField[] f, boolean local) throws ZipException { if (extraFields == null) { setExtraFields(f); } else { for (int i = 0; i < f.length; i++) { ZipExtraField existing; if (f[i] instanceof UnparseableExtraFieldData) { existing = unparseableExtra; } else { existing = getExtraField(f[i].getHeaderId()); } if (existing == null) { addExtraField(f[i]); } else { if (local || !(existing instanceof CentralDirectoryParsingZipExtraField)) { byte[] b = f[i].getLocalFileDataData(); existing.parseFromLocalFileData(b, 0, b.length); } else { byte[] b = f[i].getCentralDirectoryData(); ((CentralDirectoryParsingZipExtraField) existing) .parseFromCentralDirectoryData(b, 0, b.length); } } } setExtra(); } } } charactermanaj/src/org/apache/tools/zip/UnicodePathExtraField.java0000644000175000017500000000527111767047544025534 0ustar paulliupaulliu/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package org.apache.tools.zip; /** * Info-ZIP Unicode Path Extra Field (0x7075): * *

Stores the UTF-8 version of the file name field as stored in the * local header and central directory header.

* *
 *         Value         Size        Description
 *         -----         ----        -----------
 * (UPath) 0x7075        Short       tag for this extra block type ("up")
 *         TSize         Short       total data size for this block
 *         Version       1 byte      version of this extra field, currently 1
 *         NameCRC32     4 bytes     File Name Field CRC32 Checksum
 *         UnicodeName   Variable    UTF-8 version of the entry File Name
 * 
*/ public class UnicodePathExtraField extends AbstractUnicodeExtraField { public static final ZipShort UPATH_ID = new ZipShort(0x7075); public UnicodePathExtraField () { } /** * Assemble as unicode path extension from the name given as * text as well as the encoded bytes actually written to the archive. * * @param text The file name * @param bytes the bytes actually written to the archive * @param off The offset of the encoded filename in bytes. * @param len The length of the encoded filename or comment in * bytes. */ public UnicodePathExtraField(String text, byte[] bytes, int off, int len) { super(text, bytes, off, len); } /** * Assemble as unicode path extension from the name given as * text as well as the encoded bytes actually written to the archive. * * @param name The file name * @param bytes the bytes actually written to the archive */ public UnicodePathExtraField(String name, byte[] bytes) { super(name, bytes); } public ZipShort getHeaderId() { return UPATH_ID; } } charactermanaj/src/org/apache/tools/zip/ZipUtil.java0000644000175000017500000000250111767047544022752 0ustar paulliupaulliu/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package org.apache.tools.zip; /** * Utility class for handling DOS and Java time conversions. * @since Ant 1.8.1 */ public abstract class ZipUtil { /** * Create a copy of the given array - or return null if the * argument is null. */ static byte[] copy(byte[] from) { if (from != null) { byte[] to = new byte[from.length]; System.arraycopy(from, 0, to, 0, to.length); return to; } return null; } } charactermanaj/src/org/apache/tools/zip/ZipShort.java0000644000175000017500000001060511767047544023140 0ustar paulliupaulliu/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package org.apache.tools.zip; /** * Utility class that represents a two byte integer with conversion * rules for the big endian byte order of ZIP files. * */ public final class ZipShort implements Cloneable { private static final int BYTE_MASK = 0xFF; private static final int BYTE_1_MASK = 0xFF00; private static final int BYTE_1_SHIFT = 8; private int value; /** * Create instance from a number. * @param value the int to store as a ZipShort * @since 1.1 */ public ZipShort (int value) { this.value = value; } /** * Create instance from bytes. * @param bytes the bytes to store as a ZipShort * @since 1.1 */ public ZipShort (byte[] bytes) { this(bytes, 0); } /** * Create instance from the two bytes starting at offset. * @param bytes the bytes to store as a ZipShort * @param offset the offset to start * @since 1.1 */ public ZipShort (byte[] bytes, int offset) { value = ZipShort.getValue(bytes, offset); } /** * Get value as two bytes in big endian byte order. * @return the value as a a two byte array in big endian byte order * @since 1.1 */ public byte[] getBytes() { byte[] result = new byte[2]; result[0] = (byte) (value & BYTE_MASK); result[1] = (byte) ((value & BYTE_1_MASK) >> BYTE_1_SHIFT); return result; } /** * Get value as Java int. * @return value as a Java int * @since 1.1 */ public int getValue() { return value; } /** * Get value as two bytes in big endian byte order. * @param value the Java int to convert to bytes * @return the converted int as a byte array in big endian byte order */ public static byte[] getBytes(int value) { byte[] result = new byte[2]; result[0] = (byte) (value & BYTE_MASK); result[1] = (byte) ((value & BYTE_1_MASK) >> BYTE_1_SHIFT); return result; } /** * Helper method to get the value as a java int from two bytes starting at given array offset * @param bytes the array of bytes * @param offset the offset to start * @return the correspondanding java int value */ public static int getValue(byte[] bytes, int offset) { int value = (bytes[offset + 1] << BYTE_1_SHIFT) & BYTE_1_MASK; value += (bytes[offset] & BYTE_MASK); return value; } /** * Helper method to get the value as a java int from a two-byte array * @param bytes the array of bytes * @return the correspondanding java int value */ public static int getValue(byte[] bytes) { return getValue(bytes, 0); } /** * Override to make two instances with same value equal. * @param o an object to compare * @return true if the objects are equal * @since 1.1 */ public boolean equals(Object o) { if (o == null || !(o instanceof ZipShort)) { return false; } return value == ((ZipShort) o).getValue(); } /** * Override to make two instances with same value equal. * @return the value stored in the ZipShort * @since 1.1 */ public int hashCode() { return value; } public Object clone() { try { return super.clone(); } catch (CloneNotSupportedException cnfe) { // impossible throw new RuntimeException(cnfe); } } } charactermanaj/src/org/apache/tools/zip/ZipLong.java0000644000175000017500000001170211767047544022737 0ustar paulliupaulliu/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package org.apache.tools.zip; /** * Utility class that represents a four byte integer with conversion * rules for the big endian byte order of ZIP files. * */ public final class ZipLong implements Cloneable { private static final int WORD = 4; // private static final int BYTE_BIT_SIZE = 8; private static final int BYTE_MASK = 0xFF; private static final int BYTE_1 = 1; private static final int BYTE_1_MASK = 0xFF00; private static final int BYTE_1_SHIFT = 8; private static final int BYTE_2 = 2; private static final int BYTE_2_MASK = 0xFF0000; private static final int BYTE_2_SHIFT = 16; private static final int BYTE_3 = 3; private static final long BYTE_3_MASK = 0xFF000000L; private static final int BYTE_3_SHIFT = 24; private long value; /** * Create instance from a number. * @param value the long to store as a ZipLong * @since 1.1 */ public ZipLong(long value) { this.value = value; } /** * Create instance from bytes. * @param bytes the bytes to store as a ZipLong * @since 1.1 */ public ZipLong (byte[] bytes) { this(bytes, 0); } /** * Create instance from the four bytes starting at offset. * @param bytes the bytes to store as a ZipLong * @param offset the offset to start * @since 1.1 */ public ZipLong (byte[] bytes, int offset) { value = ZipLong.getValue(bytes, offset); } /** * Get value as four bytes in big endian byte order. * @since 1.1 * @return value as four bytes in big endian order */ public byte[] getBytes() { return ZipLong.getBytes(value); } /** * Get value as Java long. * @since 1.1 * @return value as a long */ public long getValue() { return value; } /** * Get value as four bytes in big endian byte order. * @param value the value to convert * @return value as four bytes in big endian byte order */ public static byte[] getBytes(long value) { byte[] result = new byte[WORD]; result[0] = (byte) ((value & BYTE_MASK)); result[BYTE_1] = (byte) ((value & BYTE_1_MASK) >> BYTE_1_SHIFT); result[BYTE_2] = (byte) ((value & BYTE_2_MASK) >> BYTE_2_SHIFT); result[BYTE_3] = (byte) ((value & BYTE_3_MASK) >> BYTE_3_SHIFT); return result; } /** * Helper method to get the value as a Java long from four bytes starting at given array offset * @param bytes the array of bytes * @param offset the offset to start * @return the correspondanding Java long value */ public static long getValue(byte[] bytes, int offset) { long value = (bytes[offset + BYTE_3] << BYTE_3_SHIFT) & BYTE_3_MASK; value += (bytes[offset + BYTE_2] << BYTE_2_SHIFT) & BYTE_2_MASK; value += (bytes[offset + BYTE_1] << BYTE_1_SHIFT) & BYTE_1_MASK; value += (bytes[offset] & BYTE_MASK); return value; } /** * Helper method to get the value as a Java long from a four-byte array * @param bytes the array of bytes * @return the correspondanding Java long value */ public static long getValue(byte[] bytes) { return getValue(bytes, 0); } /** * Override to make two instances with same value equal. * @param o an object to compare * @return true if the objects are equal * @since 1.1 */ public boolean equals(Object o) { if (o == null || !(o instanceof ZipLong)) { return false; } return value == ((ZipLong) o).getValue(); } /** * Override to make two instances with same value equal. * @return the value stored in the ZipLong * @since 1.1 */ public int hashCode() { return (int) value; } public Object clone() { try { return super.clone(); } catch (CloneNotSupportedException cnfe) { // impossible throw new RuntimeException(cnfe); } } } charactermanaj/src/org/apache/tools/zip/ZipFile.java0000644000175000017500000006500411767047544022723 0ustar paulliupaulliu/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package org.apache.tools.zip; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.RandomAccessFile; import java.util.Calendar; import java.util.Collections; import java.util.Date; import java.util.Enumeration; import java.util.HashMap; import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; import java.util.zip.CRC32; import java.util.zip.Inflater; import java.util.zip.InflaterInputStream; import java.util.zip.ZipException; /** * Replacement for java.util.ZipFile. * *

This class adds support for file name encodings other than UTF-8 * (which is required to work on ZIP files created by native zip tools * and is able to skip a preamble like the one found in self * extracting archives. Furthermore it returns instances of * org.apache.tools.zip.ZipEntry instead of * java.util.zip.ZipEntry.

* *

It doesn't extend java.util.zip.ZipFile as it would * have to reimplement all methods anyway. Like * java.util.ZipFile, it uses RandomAccessFile under the * covers and supports compressed and uncompressed entries.

* *

The method signatures mimic the ones of * java.util.zip.ZipFile, with a couple of exceptions: * *

    *
  • There is no getName method.
  • *
  • entries has been renamed to getEntries.
  • *
  • getEntries and getEntry return * org.apache.tools.zip.ZipEntry instances.
  • *
  • close is allowed to throw IOException.
  • *
* */ @SuppressWarnings({"unchecked", "rawtypes"}) public class ZipFile { private static final Logger logger = Logger.getLogger(ZipFile.class.getName()); private static final int HASH_SIZE = 509; private static final int SHORT = 2; private static final int WORD = 4; private static final int NIBLET_MASK = 0x0f; private static final int BYTE_SHIFT = 8; private static final int POS_0 = 0; private static final int POS_1 = 1; private static final int POS_2 = 2; private static final int POS_3 = 3; /** * Maps ZipEntrys to Longs, recording the offsets of the local * file headers. */ private final Map entries = new HashMap(HASH_SIZE); /** * Maps String to ZipEntrys, name -> actual entry. */ private final Map nameMap = new HashMap(HASH_SIZE); private static final class OffsetEntry { private long headerOffset = -1; private long dataOffset = -1; } /** * The encoding to use for filenames and the file comment. * *

For a list of possible values see http://java.sun.com/j2se/1.5.0/docs/guide/intl/encoding.doc.html. * Defaults to the platform's default character encoding.

*/ private String encoding = null; /** * The zip encoding to use for filenames and the file comment. */ private final ZipEncoding zipEncoding; /** * The actual data source. */ private RandomAccessFile archive; /** * Whether to look for and use Unicode extra fields. */ private final boolean useUnicodeExtraFields; /** * Opens the given file for reading, assuming the platform's * native encoding for file names. * * @param f the archive. * * @throws IOException if an error occurs while reading the file. */ public ZipFile(File f) throws IOException { this(f, null); } /** * Opens the given file for reading, assuming the platform's * native encoding for file names. * * @param name name of the archive. * * @throws IOException if an error occurs while reading the file. */ public ZipFile(String name) throws IOException { this(new File(name), null); } /** * Opens the given file for reading, assuming the specified * encoding for file names, scanning unicode extra fields. * * @param name name of the archive. * @param encoding the encoding to use for file names * * @throws IOException if an error occurs while reading the file. */ public ZipFile(String name, String encoding) throws IOException { this(new File(name), encoding, true); } /** * Opens the given file for reading, assuming the specified * encoding for file names and scanning for unicode extra fields. * * @param f the archive. * @param encoding the encoding to use for file names, use null * for the platform's default encoding * * @throws IOException if an error occurs while reading the file. */ public ZipFile(File f, String encoding) throws IOException { this(f, encoding, true); } /** * Opens the given file for reading, assuming the specified * encoding for file names. * * @param f the archive. * @param encoding the encoding to use for file names, use null * for the platform's default encoding * @param useUnicodeExtraFields whether to use InfoZIP Unicode * Extra Fields (if present) to set the file names. * * @throws IOException if an error occurs while reading the file. */ public ZipFile(File f, String encoding, boolean useUnicodeExtraFields) throws IOException { this.encoding = encoding; this.zipEncoding = ZipEncodingHelper.getZipEncoding(encoding); this.useUnicodeExtraFields = useUnicodeExtraFields; archive = new RandomAccessFile(f, "r"); boolean success = false; try { Map entriesWithoutUTF8Flag = populateFromCentralDirectory(); resolveLocalFileHeaderData(entriesWithoutUTF8Flag); success = true; } finally { if (!success) { try { archive.close(); } catch (IOException e2) { // swallow, throw the original exception instead } } } } /** * The encoding to use for filenames and the file comment. * * @return null if using the platform's default character encoding. */ public String getEncoding() { return encoding; } /** * Closes the archive. * @throws IOException if an error occurs closing the archive. */ public void close() throws IOException { archive.close(); } /** * close a zipfile quietly; throw no io fault, do nothing * on a null parameter * @param zipfile file to close, can be null */ public static void closeQuietly(ZipFile zipfile) { if (zipfile != null) { try { zipfile.close(); } catch (IOException e) { //ignore } } } /** * Returns all entries. * @return all entries as {@link ZipEntry} instances */ public Enumeration getEntries() { return Collections.enumeration(entries.keySet()); } /** * Returns a named entry - or null if no entry by * that name exists. * @param name name of the entry. * @return the ZipEntry corresponding to the given name - or * null if not present. */ public ZipEntry getEntry(String name) { return (ZipEntry) nameMap.get(name); } /** * Returns an InputStream for reading the contents of the given entry. * @param ze the entry to get the stream for. * @return a stream to read the entry from. * @throws IOException if unable to create an input stream from the zipenty * @throws ZipException if the zipentry has an unsupported * compression method */ public InputStream getInputStream(ZipEntry ze) throws IOException, ZipException { OffsetEntry offsetEntry = (OffsetEntry) entries.get(ze); if (offsetEntry == null) { return null; } long start = offsetEntry.dataOffset; BoundedInputStream bis = new BoundedInputStream(start, ze.getCompressedSize()); switch (ze.getMethod()) { case ZipEntry.STORED: return bis; case ZipEntry.DEFLATED: bis.addDummy(); return new InflaterInputStream(bis, new Inflater(true)); default: throw new ZipException("Found unsupported compression method " + ze.getMethod()); } } private static final int CFH_LEN = /* version made by */ SHORT /* version needed to extract */ + SHORT /* general purpose bit flag */ + SHORT /* compression method */ + SHORT /* last mod file time */ + SHORT /* last mod file date */ + SHORT /* crc-32 */ + WORD /* compressed size */ + WORD /* uncompressed size */ + WORD /* filename length */ + SHORT /* extra field length */ + SHORT /* file comment length */ + SHORT /* disk number start */ + SHORT /* internal file attributes */ + SHORT /* external file attributes */ + WORD /* relative offset of local header */ + WORD; /** * Reads the central directory of the given archive and populates * the internal tables with ZipEntry instances. * *

The ZipEntrys will know all data that can be obtained from * the central directory alone, but not the data that requires the * local file header or additional data to be read.

* * @return a Map<ZipEntry, NameAndComment>> of * zipentries that didn't have the language encoding flag set when * read. */ private Map populateFromCentralDirectory() throws IOException { HashMap noUTF8Flag = new HashMap(); positionAtCentralDirectory(); byte[] cfh = new byte[CFH_LEN]; byte[] signatureBytes = new byte[WORD]; archive.readFully(signatureBytes); long sig = ZipLong.getValue(signatureBytes); final long cfhSig = ZipLong.getValue(ZipOutputStream.CFH_SIG); if (sig != cfhSig && startsWithLocalFileHeader()) { throw new IOException("central directory is empty, can't expand" + " corrupt archive."); } while (sig == cfhSig) { archive.readFully(cfh); int off = 0; ZipEntry ze = new ZipEntry(); int versionMadeBy = ZipShort.getValue(cfh, off); off += SHORT; ze.setPlatform((versionMadeBy >> BYTE_SHIFT) & NIBLET_MASK); off += SHORT; // skip version info final int generalPurposeFlag = ZipShort.getValue(cfh, off); final boolean hasUTF8Flag = (generalPurposeFlag & ZipOutputStream.UFT8_NAMES_FLAG) != 0; final ZipEncoding entryEncoding = hasUTF8Flag ? ZipEncodingHelper.UTF8_ZIP_ENCODING : zipEncoding; off += SHORT; ze.setMethod(ZipShort.getValue(cfh, off)); off += SHORT; // FIXME this is actually not very cpu cycles friendly as we are converting from // dos to java while the underlying Sun implementation will convert // from java to dos time for internal storage... long time = dosToJavaTime(ZipLong.getValue(cfh, off)); ze.setTime(time); off += WORD; ze.setCrc(ZipLong.getValue(cfh, off)); off += WORD; ze.setCompressedSize(ZipLong.getValue(cfh, off)); off += WORD; ze.setSize(ZipLong.getValue(cfh, off)); off += WORD; int fileNameLen = ZipShort.getValue(cfh, off); off += SHORT; int extraLen = ZipShort.getValue(cfh, off); off += SHORT; int commentLen = ZipShort.getValue(cfh, off); off += SHORT; off += SHORT; // disk number ze.setInternalAttributes(ZipShort.getValue(cfh, off)); off += SHORT; ze.setExternalAttributes(ZipLong.getValue(cfh, off)); off += WORD; byte[] fileName = new byte[fileNameLen]; archive.readFully(fileName); ze.setName(entryEncoding.decode(fileName)); // LFH offset, OffsetEntry offset = new OffsetEntry(); offset.headerOffset = ZipLong.getValue(cfh, off); // data offset will be filled later entries.put(ze, offset); nameMap.put(ze.getName(), ze); byte[] cdExtraData = new byte[extraLen]; archive.readFully(cdExtraData); ze.setCentralDirectoryExtra(cdExtraData); byte[] comment = new byte[commentLen]; archive.readFully(comment); ze.setComment(entryEncoding.decode(comment)); archive.readFully(signatureBytes); sig = ZipLong.getValue(signatureBytes); if (!hasUTF8Flag && useUnicodeExtraFields) { noUTF8Flag.put(ze, new NameAndComment(fileName, comment)); } } return noUTF8Flag; } private static final int MIN_EOCD_SIZE = /* end of central dir signature */ WORD /* number of this disk */ + SHORT /* number of the disk with the */ /* start of the central directory */ + SHORT /* total number of entries in */ /* the central dir on this disk */ + SHORT /* total number of entries in */ /* the central dir */ + SHORT /* size of the central directory */ + WORD /* offset of start of central */ /* directory with respect to */ /* the starting disk number */ + WORD /* zipfile comment length */ + SHORT; private static final int MAX_EOCD_SIZE = MIN_EOCD_SIZE /* maximum length of zipfile comment */ + 0xFFFF; private static final int CFD_LOCATOR_OFFSET = /* end of central dir signature */ WORD /* number of this disk */ + SHORT /* number of the disk with the */ /* start of the central directory */ + SHORT /* total number of entries in */ /* the central dir on this disk */ + SHORT /* total number of entries in */ /* the central dir */ + SHORT /* size of the central directory */ + WORD; /** * Searches for the "End of central dir record", parses * it and positions the stream at the first central directory * record. */ private void positionAtCentralDirectory() throws IOException { boolean found = false; long off = archive.length() - MIN_EOCD_SIZE; long stopSearching = Math.max(0L, archive.length() - MAX_EOCD_SIZE); if (off >= 0) { archive.seek(off); byte[] sig = ZipOutputStream.EOCD_SIG; int curr = archive.read(); while (off >= stopSearching && curr != -1) { if (curr == sig[POS_0]) { curr = archive.read(); if (curr == sig[POS_1]) { curr = archive.read(); if (curr == sig[POS_2]) { curr = archive.read(); if (curr == sig[POS_3]) { found = true; break; } } } } archive.seek(--off); curr = archive.read(); } } if (!found) { throw new ZipException("archive is not a ZIP archive"); } archive.seek(off + CFD_LOCATOR_OFFSET); byte[] cfdOffset = new byte[WORD]; archive.readFully(cfdOffset); archive.seek(ZipLong.getValue(cfdOffset)); } /** * Number of bytes in local file header up to the "length of * filename" entry. */ private static final long LFH_OFFSET_FOR_FILENAME_LENGTH = /* local file header signature */ WORD /* version needed to extract */ + SHORT /* general purpose bit flag */ + SHORT /* compression method */ + SHORT /* last mod file time */ + SHORT /* last mod file date */ + SHORT /* crc-32 */ + WORD /* compressed size */ + WORD /* uncompressed size */ + WORD; /** * Walks through all recorded entries and adds the data available * from the local file header. * *

Also records the offsets for the data to read from the * entries.

*/ private void resolveLocalFileHeaderData(Map entriesWithoutUTF8Flag) throws IOException { Enumeration e = getEntries(); while (e.hasMoreElements()) { ZipEntry ze = (ZipEntry) e.nextElement(); OffsetEntry offsetEntry = (OffsetEntry) entries.get(ze); long offset = offsetEntry.headerOffset; archive.seek(offset + LFH_OFFSET_FOR_FILENAME_LENGTH); byte[] b = new byte[SHORT]; archive.readFully(b); int fileNameLen = ZipShort.getValue(b); archive.readFully(b); int extraFieldLen = ZipShort.getValue(b); int lenToSkip = fileNameLen; while (lenToSkip > 0) { int skipped = archive.skipBytes(lenToSkip); if (skipped <= 0) { throw new RuntimeException("failed to skip file name in" + " local file header"); } lenToSkip -= skipped; } byte[] localExtraData = new byte[extraFieldLen]; archive.readFully(localExtraData); ze.setExtra(localExtraData); /*dataOffsets.put(ze, new Long(offset + LFH_OFFSET_FOR_FILENAME_LENGTH + SHORT + SHORT + fileNameLen + extraFieldLen)); */ offsetEntry.dataOffset = offset + LFH_OFFSET_FOR_FILENAME_LENGTH + SHORT + SHORT + fileNameLen + extraFieldLen; if (entriesWithoutUTF8Flag.containsKey(ze)) { setNameAndCommentFromExtraFields(ze, (NameAndComment) entriesWithoutUTF8Flag.get(ze)); } } } /** * Convert a DOS date/time field to a Date object. * * @param zipDosTime contains the stored DOS time. * @return a Date instance corresponding to the given time. */ protected static Date fromDosTime(ZipLong zipDosTime) { long dosTime = zipDosTime.getValue(); return new Date(dosToJavaTime(dosTime)); } /* * Converts DOS time to Java time (number of milliseconds since epoch). */ private static long dosToJavaTime(long dosTime) { Calendar cal = Calendar.getInstance(); // CheckStyle:MagicNumberCheck OFF - no point cal.set(Calendar.YEAR, (int) ((dosTime >> 25) & 0x7f) + 1980); cal.set(Calendar.MONTH, (int) ((dosTime >> 21) & 0x0f) - 1); cal.set(Calendar.DATE, (int) (dosTime >> 16) & 0x1f); cal.set(Calendar.HOUR_OF_DAY, (int) (dosTime >> 11) & 0x1f); cal.set(Calendar.MINUTE, (int) (dosTime >> 5) & 0x3f); cal.set(Calendar.SECOND, (int) (dosTime << 1) & 0x3e); // CheckStyle:MagicNumberCheck ON return cal.getTime().getTime(); } /** * Retrieve a String from the given bytes using the encoding set * for this ZipFile. * * @param bytes the byte array to transform * @return String obtained by using the given encoding * @throws ZipException if the encoding cannot be recognized. */ protected String getString(byte[] bytes) throws ZipException { try { return ZipEncodingHelper.getZipEncoding(encoding).decode(bytes); } catch (IOException ex) { throw new ZipException("Failed to decode name: " + ex.getMessage()); } } /** * Checks whether the archive starts with a LFH. If it doesn't, * it may be an empty archive. */ private boolean startsWithLocalFileHeader() throws IOException { archive.seek(0); final byte[] start = new byte[WORD]; archive.readFully(start); for (int i = 0; i < start.length; i++) { if (start[i] != ZipOutputStream.LFH_SIG[i]) { return false; } } return true; } /** * If the entry has Unicode*ExtraFields and the CRCs of the * names/comments match those of the extra fields, transfer the * known Unicode values from the extra field. */ private void setNameAndCommentFromExtraFields(ZipEntry ze, NameAndComment nc) { UnicodePathExtraField name = (UnicodePathExtraField) ze.getExtraField(UnicodePathExtraField.UPATH_ID); String originalName = ze.getName(); String newName = getUnicodeStringIfOriginalMatches(name, nc.name); if (newName != null && !originalName.equals(newName)) { ze.setName(newName); nameMap.remove(originalName); nameMap.put(newName, ze); } if (nc.comment != null && nc.comment.length > 0) { UnicodeCommentExtraField cmt = (UnicodeCommentExtraField) ze.getExtraField(UnicodeCommentExtraField.UCOM_ID); String newComment = getUnicodeStringIfOriginalMatches(cmt, nc.comment); if (newComment != null) { ze.setComment(newComment); } } } /** * If the stored CRC matches the one of the given name, return the * Unicode name of the given field. * *

If the field is null or the CRCs don't match, return null * instead.

*/ private String getUnicodeStringIfOriginalMatches(AbstractUnicodeExtraField f, byte[] orig) { if (f != null) { CRC32 crc32 = new CRC32(); crc32.update(orig); long origCRC32 = crc32.getValue(); if (origCRC32 == f.getNameCRC32()) { try { return ZipEncodingHelper .UTF8_ZIP_ENCODING.decode(f.getUnicodeName()); } catch (IOException ex) { // UTF-8 unsupported? should be impossible the // Unicode*ExtraField must contain some bad bytes logger.log(Level.WARNING, "ZipFile: UTF-8 unsupported." + " should be impossible the Unicode*ExtraField must contain some bad bytes.", ex); return null; } } } return null; } /** * InputStream that delegates requests to the underlying * RandomAccessFile, making sure that only bytes from a certain * range can be read. */ private class BoundedInputStream extends InputStream { private long remaining; private long loc; private boolean addDummyByte = false; BoundedInputStream(long start, long remaining) { this.remaining = remaining; loc = start; } public int read() throws IOException { if (remaining-- <= 0) { if (addDummyByte) { addDummyByte = false; return 0; } return -1; } synchronized (archive) { archive.seek(loc++); return archive.read(); } } public int read(byte[] b, int off, int len) throws IOException { if (remaining <= 0) { if (addDummyByte) { addDummyByte = false; b[off] = 0; return 1; } return -1; } if (len <= 0) { return 0; } if (len > remaining) { len = (int) remaining; } int ret = -1; synchronized (archive) { archive.seek(loc); ret = archive.read(b, off, len); } if (ret > 0) { loc += ret; remaining -= ret; } return ret; } /** * Inflater needs an extra dummy byte for nowrap - see * Inflater's javadocs. */ void addDummy() { addDummyByte = true; } } private static final class NameAndComment { private final byte[] name; private final byte[] comment; private NameAndComment(byte[] name, byte[] comment) { this.name = name; this.comment = comment; } } } charactermanaj/src/org/apache/tools/zip/ZipOutputStream.java0000644000175000017500000010327211767047544024520 0ustar paulliupaulliu/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package org.apache.tools.zip; import java.io.File; import java.io.FileOutputStream; import java.io.FilterOutputStream; import java.io.IOException; import java.io.OutputStream; import java.io.RandomAccessFile; import java.nio.ByteBuffer; import java.util.Date; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.zip.CRC32; import java.util.zip.Deflater; import java.util.zip.ZipException; /** * Reimplementation of {@link java.util.zip.ZipOutputStream * java.util.zip.ZipOutputStream} that does handle the extended * functionality of this package, especially internal/external file * attributes and extra fields with different layouts for local file * data and central directory entries. * *

This class will try to use {@link java.io.RandomAccessFile * RandomAccessFile} when you know that the output is going to go to a * file.

* *

If RandomAccessFile cannot be used, this implementation will use * a Data Descriptor to store size and CRC information for {@link * #DEFLATED DEFLATED} entries, this means, you don't need to * calculate them yourself. Unfortunately this is not possible for * the {@link #STORED STORED} method, here setting the CRC and * uncompressed size information is required before {@link * #putNextEntry putNextEntry} can be called.

* */ @SuppressWarnings({"unchecked", "rawtypes"}) public class ZipOutputStream extends FilterOutputStream { private static final int BYTE_MASK = 0xFF; private static final int SHORT = 2; private static final int WORD = 4; private static final int BUFFER_SIZE = 512; /* * Apparently Deflater.setInput gets slowed down a lot on Sun JVMs * when it gets handed a really big buffer. See * https://issues.apache.org/bugzilla/show_bug.cgi?id=45396 * * Using a buffer size of 8 kB proved to be a good compromise */ private static final int DEFLATER_BLOCK_SIZE = 8192; /** * Compression method for deflated entries. * * @since 1.1 */ public static final int DEFLATED = java.util.zip.ZipEntry.DEFLATED; /** * Default compression level for deflated entries. * * @since Ant 1.7 */ public static final int DEFAULT_COMPRESSION = Deflater.DEFAULT_COMPRESSION; /** * Compression method for stored entries. * * @since 1.1 */ public static final int STORED = java.util.zip.ZipEntry.STORED; /** * default encoding for file names and comment. */ static final String DEFAULT_ENCODING = null; /** * General purpose flag, which indicates that filenames are * written in utf-8. */ public static final int UFT8_NAMES_FLAG = 1 << 11; /** * General purpose flag, which indicates that filenames are * written in utf-8. * @deprecated use {@link #UFT8_NAMES_FLAG} instead */ public static final int EFS_FLAG = UFT8_NAMES_FLAG; /** * Current entry. * * @since 1.1 */ private ZipEntry entry; /** * The file comment. * * @since 1.1 */ private String comment = ""; /** * Compression level for next entry. * * @since 1.1 */ private int level = DEFAULT_COMPRESSION; /** * Has the compression level changed when compared to the last * entry? * * @since 1.5 */ private boolean hasCompressionLevelChanged = false; /** * Default compression method for next entry. * * @since 1.1 */ private int method = java.util.zip.ZipEntry.DEFLATED; /** * List of ZipEntries written so far. * * @since 1.1 */ private final List entries = new LinkedList(); /** * CRC instance to avoid parsing DEFLATED data twice. * * @since 1.1 */ private final CRC32 crc = new CRC32(); /** * Count the bytes written to out. * * @since 1.1 */ private long written = 0; /** * Data for local header data * * @since 1.1 */ private long dataStart = 0; /** * Offset for CRC entry in the local file header data for the * current entry starts here. * * @since 1.15 */ private long localDataStart = 0; /** * Start of central directory. * * @since 1.1 */ private long cdOffset = 0; /** * Length of central directory. * * @since 1.1 */ private long cdLength = 0; /** * Helper, a 0 as ZipShort. * * @since 1.1 */ private static final byte[] ZERO = {0, 0}; /** * Helper, a 0 as ZipLong. * * @since 1.1 */ private static final byte[] LZERO = {0, 0, 0, 0}; /** * Holds the offsets of the LFH starts for each entry. * * @since 1.1 */ private final Map offsets = new HashMap(); /** * The encoding to use for filenames and the file comment. * *

For a list of possible values see http://java.sun.com/j2se/1.5.0/docs/guide/intl/encoding.doc.html. * Defaults to the platform's default character encoding.

* * @since 1.3 */ private String encoding = null; /** * The zip encoding to use for filenames and the file comment. * * This field is of internal use and will be set in {@link * #setEncoding(String)}. */ private ZipEncoding zipEncoding = ZipEncodingHelper.getZipEncoding(DEFAULT_ENCODING); // CheckStyle:VisibilityModifier OFF - bc /** * This Deflater object is used for output. * *

This attribute is only protected to provide a level of API * backwards compatibility. This class used to extend {@link * java.util.zip.DeflaterOutputStream DeflaterOutputStream} up to * Revision 1.13.

* * @since 1.14 */ protected Deflater def = new Deflater(level, true); /** * This buffer servers as a Deflater. * *

This attribute is only protected to provide a level of API * backwards compatibility. This class used to extend {@link * java.util.zip.DeflaterOutputStream DeflaterOutputStream} up to * Revision 1.13.

* * @since 1.14 */ protected byte[] buf = new byte[BUFFER_SIZE]; // CheckStyle:VisibilityModifier ON /** * Optional random access output. * * @since 1.14 */ private RandomAccessFile raf = null; /** * whether to use the general purpose bit flag when writing UTF-8 * filenames or not. */ private boolean useUTF8Flag = true; /** * Whether to encode non-encodable file names as UTF-8. */ private boolean fallbackToUTF8 = false; /** * whether to create UnicodePathExtraField-s for each entry. */ private UnicodeExtraFieldPolicy createUnicodeExtraFields = UnicodeExtraFieldPolicy.NEVER; /** * Creates a new ZIP OutputStream filtering the underlying stream. * @param out the outputstream to zip * @since 1.1 */ public ZipOutputStream(OutputStream out) { super(out); } /** * Creates a new ZIP OutputStream writing to a File. Will use * random access if possible. * @param file the file to zip to * @since 1.14 * @throws IOException on error */ public ZipOutputStream(File file) throws IOException { super(null); try { raf = new RandomAccessFile(file, "rw"); raf.setLength(0); } catch (IOException e) { if (raf != null) { try { raf.close(); } catch (IOException inner) { // ignore } raf = null; } out = new FileOutputStream(file); } } /** * This method indicates whether this archive is writing to a * seekable stream (i.e., to a random access file). * *

For seekable streams, you don't need to calculate the CRC or * uncompressed size for {@link #STORED} entries before * invoking {@link #putNextEntry}. * @return true if seekable * @since 1.17 */ public boolean isSeekable() { return raf != null; } /** * The encoding to use for filenames and the file comment. * *

For a list of possible values see http://java.sun.com/j2se/1.5.0/docs/guide/intl/encoding.doc.html. * Defaults to the platform's default character encoding.

* @param encoding the encoding value * @since 1.3 */ public void setEncoding(final String encoding) { this.encoding = encoding; this.zipEncoding = ZipEncodingHelper.getZipEncoding(encoding); useUTF8Flag &= ZipEncodingHelper.isUTF8(encoding); } /** * The encoding to use for filenames and the file comment. * * @return null if using the platform's default character encoding. * * @since 1.3 */ public String getEncoding() { return encoding; } /** * Whether to set the language encoding flag if the file name * encoding is UTF-8. * *

Defaults to true.

*/ public void setUseLanguageEncodingFlag(boolean b) { useUTF8Flag = b && ZipEncodingHelper.isUTF8(encoding); } /** * Whether to create Unicode Extra Fields. * *

Defaults to NEVER.

*/ public void setCreateUnicodeExtraFields(UnicodeExtraFieldPolicy b) { createUnicodeExtraFields = b; } /** * Whether to fall back to UTF and the language encoding flag if * the file name cannot be encoded using the specified encoding. * *

Defaults to false.

*/ public void setFallbackToUTF8(boolean b) { fallbackToUTF8 = b; } /** * Finishs writing the contents and closes this as well as the * underlying stream. * * @since 1.1 * @throws IOException on error */ public void finish() throws IOException { closeEntry(); cdOffset = written; for (Iterator i = entries.iterator(); i.hasNext(); ) { writeCentralFileHeader((ZipEntry) i.next()); } cdLength = written - cdOffset; writeCentralDirectoryEnd(); offsets.clear(); entries.clear(); } /** * Writes all necessary data for this entry. * * @since 1.1 * @throws IOException on error */ public void closeEntry() throws IOException { if (entry == null) { return; } long realCrc = crc.getValue(); crc.reset(); if (entry.getMethod() == DEFLATED) { def.finish(); while (!def.finished()) { deflate(); } entry.setSize(adjustToLong(def.getTotalIn())); entry.setCompressedSize(adjustToLong(def.getTotalOut())); entry.setCrc(realCrc); def.reset(); written += entry.getCompressedSize(); } else if (raf == null) { if (entry.getCrc() != realCrc) { throw new ZipException("bad CRC checksum for entry " + entry.getName() + ": " + Long.toHexString(entry.getCrc()) + " instead of " + Long.toHexString(realCrc)); } if (entry.getSize() != written - dataStart) { throw new ZipException("bad size for entry " + entry.getName() + ": " + entry.getSize() + " instead of " + (written - dataStart)); } } else { /* method is STORED and we used RandomAccessFile */ long size = written - dataStart; entry.setSize(size); entry.setCompressedSize(size); entry.setCrc(realCrc); } // If random access output, write the local file header containing // the correct CRC and compressed/uncompressed sizes if (raf != null) { long save = raf.getFilePointer(); raf.seek(localDataStart); writeOut(ZipLong.getBytes(entry.getCrc())); writeOut(ZipLong.getBytes(entry.getCompressedSize())); writeOut(ZipLong.getBytes(entry.getSize())); raf.seek(save); } writeDataDescriptor(entry); entry = null; } /** * Begin writing next entry. * @param ze the entry to write * @since 1.1 * @throws IOException on error */ public void putNextEntry(ZipEntry ze) throws IOException { closeEntry(); entry = ze; entries.add(entry); if (entry.getMethod() == -1) { // not specified entry.setMethod(method); } if (entry.getTime() == -1) { // not specified entry.setTime(System.currentTimeMillis()); } // Size/CRC not required if RandomAccessFile is used if (entry.getMethod() == STORED && raf == null) { if (entry.getSize() == -1) { throw new ZipException("uncompressed size is required for" + " STORED method when not writing to a" + " file"); } if (entry.getCrc() == -1) { throw new ZipException("crc checksum is required for STORED" + " method when not writing to a file"); } entry.setCompressedSize(entry.getSize()); } if (entry.getMethod() == DEFLATED && hasCompressionLevelChanged) { def.setLevel(level); hasCompressionLevelChanged = false; } writeLocalFileHeader(entry); } /** * Set the file comment. * @param comment the comment * @since 1.1 */ public void setComment(String comment) { this.comment = comment; } /** * Sets the compression level for subsequent entries. * *

Default is Deflater.DEFAULT_COMPRESSION.

* @param level the compression level. * @throws IllegalArgumentException if an invalid compression * level is specified. * @since 1.1 */ public void setLevel(int level) { if (level < Deflater.DEFAULT_COMPRESSION || level > Deflater.BEST_COMPRESSION) { throw new IllegalArgumentException("Invalid compression level: " + level); } hasCompressionLevelChanged = (this.level != level); this.level = level; } /** * Sets the default compression method for subsequent entries. * *

Default is DEFLATED.

* @param method an int from java.util.zip.ZipEntry * @since 1.1 */ public void setMethod(int method) { this.method = method; } /** * Writes bytes to ZIP entry. * @param b the byte array to write * @param offset the start position to write from * @param length the number of bytes to write * @throws IOException on error */ public void write(byte[] b, int offset, int length) throws IOException { if (entry.getMethod() == DEFLATED) { if (length > 0) { if (!def.finished()) { if (length <= DEFLATER_BLOCK_SIZE) { def.setInput(b, offset, length); deflateUntilInputIsNeeded(); } else { final int fullblocks = length / DEFLATER_BLOCK_SIZE; for (int i = 0; i < fullblocks; i++) { def.setInput(b, offset + i * DEFLATER_BLOCK_SIZE, DEFLATER_BLOCK_SIZE); deflateUntilInputIsNeeded(); } final int done = fullblocks * DEFLATER_BLOCK_SIZE; if (done < length) { def.setInput(b, offset + done, length - done); deflateUntilInputIsNeeded(); } } } } } else { writeOut(b, offset, length); written += length; } crc.update(b, offset, length); } /** * Writes a single byte to ZIP entry. * *

Delegates to the three arg method.

* @param b the byte to write * @since 1.14 * @throws IOException on error */ public void write(int b) throws IOException { byte[] buff = new byte[1]; buff[0] = (byte) (b & BYTE_MASK); write(buff, 0, 1); } /** * Closes this output stream and releases any system resources * associated with the stream. * * @exception IOException if an I/O error occurs. * @since 1.14 */ public void close() throws IOException { finish(); if (raf != null) { raf.close(); } if (out != null) { out.close(); } } /** * Flushes this output stream and forces any buffered output bytes * to be written out to the stream. * * @exception IOException if an I/O error occurs. * @since 1.14 */ public void flush() throws IOException { if (out != null) { out.flush(); } } /* * Various ZIP constants */ /** * local file header signature * * @since 1.1 */ protected static final byte[] LFH_SIG = ZipLong.getBytes(0X04034B50L); /** * data descriptor signature * * @since 1.1 */ protected static final byte[] DD_SIG = ZipLong.getBytes(0X08074B50L); /** * central file header signature * * @since 1.1 */ protected static final byte[] CFH_SIG = ZipLong.getBytes(0X02014B50L); /** * end of central dir signature * * @since 1.1 */ protected static final byte[] EOCD_SIG = ZipLong.getBytes(0X06054B50L); /** * Writes next block of compressed data to the output stream. * @throws IOException on error * * @since 1.14 */ protected final void deflate() throws IOException { int len = def.deflate(buf, 0, buf.length); if (len > 0) { writeOut(buf, 0, len); } } /** * Writes the local file header entry * @param ze the entry to write * @throws IOException on error * * @since 1.1 */ protected void writeLocalFileHeader(ZipEntry ze) throws IOException { boolean encodable = zipEncoding.canEncode(ze.getName()); final ZipEncoding entryEncoding; if (!encodable && fallbackToUTF8) { entryEncoding = ZipEncodingHelper.UTF8_ZIP_ENCODING; } else { entryEncoding = zipEncoding; } ByteBuffer name = entryEncoding.encode(ze.getName()); if (createUnicodeExtraFields != UnicodeExtraFieldPolicy.NEVER) { if (createUnicodeExtraFields == UnicodeExtraFieldPolicy.ALWAYS || !encodable) { ze.addExtraField(new UnicodePathExtraField(ze.getName(), name.array(), name.arrayOffset(), name.limit())); } String comm = ze.getComment(); if (comm != null && !"".equals(comm)) { boolean commentEncodable = this.zipEncoding.canEncode(comm); if (createUnicodeExtraFields == UnicodeExtraFieldPolicy.ALWAYS || !commentEncodable) { ByteBuffer commentB = entryEncoding.encode(comm); ze.addExtraField(new UnicodeCommentExtraField(comm, commentB.array(), commentB.arrayOffset(), commentB.limit()) ); } } } offsets.put(ze, ZipLong.getBytes(written)); writeOut(LFH_SIG); written += WORD; //store method in local variable to prevent multiple method calls final int zipMethod = ze.getMethod(); writeVersionNeededToExtractAndGeneralPurposeBits(zipMethod, !encodable && fallbackToUTF8); written += WORD; // compression method writeOut(ZipShort.getBytes(zipMethod)); written += SHORT; // last mod. time and date writeOut(toDosTime(ze.getTime())); written += WORD; // CRC // compressed length // uncompressed length localDataStart = written; if (zipMethod == DEFLATED || raf != null) { writeOut(LZERO); writeOut(LZERO); writeOut(LZERO); } else { writeOut(ZipLong.getBytes(ze.getCrc())); writeOut(ZipLong.getBytes(ze.getSize())); writeOut(ZipLong.getBytes(ze.getSize())); } // CheckStyle:MagicNumber OFF written += 12; // CheckStyle:MagicNumber ON // file name length writeOut(ZipShort.getBytes(name.limit())); written += SHORT; // extra field length byte[] extra = ze.getLocalFileDataExtra(); writeOut(ZipShort.getBytes(extra.length)); written += SHORT; // file name writeOut(name.array(), name.arrayOffset(), name.limit()); written += name.limit(); // extra field writeOut(extra); written += extra.length; dataStart = written; } /** * Writes the data descriptor entry. * @param ze the entry to write * @throws IOException on error * * @since 1.1 */ protected void writeDataDescriptor(ZipEntry ze) throws IOException { if (ze.getMethod() != DEFLATED || raf != null) { return; } writeOut(DD_SIG); writeOut(ZipLong.getBytes(entry.getCrc())); writeOut(ZipLong.getBytes(entry.getCompressedSize())); writeOut(ZipLong.getBytes(entry.getSize())); // CheckStyle:MagicNumber OFF written += 16; // CheckStyle:MagicNumber ON } /** * Writes the central file header entry. * @param ze the entry to write * @throws IOException on error * * @since 1.1 */ protected void writeCentralFileHeader(ZipEntry ze) throws IOException { writeOut(CFH_SIG); written += WORD; // version made by // CheckStyle:MagicNumber OFF writeOut(ZipShort.getBytes((ze.getPlatform() << 8) | 20)); written += SHORT; final int zipMethod = ze.getMethod(); final boolean encodable = zipEncoding.canEncode(ze.getName()); writeVersionNeededToExtractAndGeneralPurposeBits(zipMethod, !encodable && fallbackToUTF8); written += WORD; // compression method writeOut(ZipShort.getBytes(zipMethod)); written += SHORT; // last mod. time and date writeOut(toDosTime(ze.getTime())); written += WORD; // CRC // compressed length // uncompressed length writeOut(ZipLong.getBytes(ze.getCrc())); writeOut(ZipLong.getBytes(ze.getCompressedSize())); writeOut(ZipLong.getBytes(ze.getSize())); // CheckStyle:MagicNumber OFF written += 12; // CheckStyle:MagicNumber ON // file name length final ZipEncoding entryEncoding; if (!encodable && fallbackToUTF8) { entryEncoding = ZipEncodingHelper.UTF8_ZIP_ENCODING; } else { entryEncoding = zipEncoding; } ByteBuffer name = entryEncoding.encode(ze.getName()); writeOut(ZipShort.getBytes(name.limit())); written += SHORT; // extra field length byte[] extra = ze.getCentralDirectoryExtra(); writeOut(ZipShort.getBytes(extra.length)); written += SHORT; // file comment length String comm = ze.getComment(); if (comm == null) { comm = ""; } ByteBuffer commentB = entryEncoding.encode(comm); writeOut(ZipShort.getBytes(commentB.limit())); written += SHORT; // disk number start writeOut(ZERO); written += SHORT; // internal file attributes writeOut(ZipShort.getBytes(ze.getInternalAttributes())); written += SHORT; // external file attributes writeOut(ZipLong.getBytes(ze.getExternalAttributes())); written += WORD; // relative offset of LFH writeOut((byte[]) offsets.get(ze)); written += WORD; // file name writeOut(name.array(), name.arrayOffset(), name.limit()); written += name.limit(); // extra field writeOut(extra); written += extra.length; // file comment writeOut(commentB.array(), commentB.arrayOffset(), commentB.limit()); written += commentB.limit(); } /** * Writes the "End of central dir record". * @throws IOException on error * * @since 1.1 */ protected void writeCentralDirectoryEnd() throws IOException { writeOut(EOCD_SIG); // disk numbers writeOut(ZERO); writeOut(ZERO); // number of entries byte[] num = ZipShort.getBytes(entries.size()); writeOut(num); writeOut(num); // length and location of CD writeOut(ZipLong.getBytes(cdLength)); writeOut(ZipLong.getBytes(cdOffset)); // ZIP file comment ByteBuffer data = this.zipEncoding.encode(comment); writeOut(ZipShort.getBytes(data.limit())); writeOut(data.array(), data.arrayOffset(), data.limit()); } /** * Smallest date/time ZIP can handle. * * @since 1.1 */ private static final byte[] DOS_TIME_MIN = ZipLong.getBytes(0x00002100L); /** * Convert a Date object to a DOS date/time field. * @param time the Date to convert * @return the date as a ZipLong * @since 1.1 */ protected static ZipLong toDosTime(Date time) { return new ZipLong(toDosTime(time.getTime())); } /** * Convert a Date object to a DOS date/time field. * *

Stolen from InfoZip's fileio.c

* @param t number of milliseconds since the epoch * @return the date as a byte array * @since 1.26 */ @SuppressWarnings("deprecation") protected static byte[] toDosTime(long t) { Date time = new Date(t); // CheckStyle:MagicNumberCheck OFF - I do not think that using constants // here will improve the readablity int year = time.getYear() + 1900; if (year < 1980) { return DOS_TIME_MIN; } int month = time.getMonth() + 1; long value = ((year - 1980) << 25) | (month << 21) | (time.getDate() << 16) | (time.getHours() << 11) | (time.getMinutes() << 5) | (time.getSeconds() >> 1); return ZipLong.getBytes(value); // CheckStyle:MagicNumberCheck ON } /** * Retrieve the bytes for the given String in the encoding set for * this Stream. * @param name the string to get bytes from * @return the bytes as a byte array * @throws ZipException on error * * @since 1.3 */ protected byte[] getBytes(String name) throws ZipException { try { ByteBuffer b = ZipEncodingHelper.getZipEncoding(encoding).encode(name); byte[] result = new byte[b.limit()]; System.arraycopy(b.array(), b.arrayOffset(), result, 0, result.length); return result; } catch (IOException ex) { throw new ZipException("Failed to encode name: " + ex.getMessage()); } } /** * Write bytes to output or random access file. * @param data the byte array to write * @throws IOException on error * * @since 1.14 */ protected final void writeOut(byte[] data) throws IOException { writeOut(data, 0, data.length); } /** * Write bytes to output or random access file. * @param data the byte array to write * @param offset the start position to write from * @param length the number of bytes to write * @throws IOException on error * * @since 1.14 */ protected final void writeOut(byte[] data, int offset, int length) throws IOException { if (raf != null) { raf.write(data, offset, length); } else { out.write(data, offset, length); } } /** * Assumes a negative integer really is a positive integer that * has wrapped around and re-creates the original value. * @param i the value to treat as unsigned int. * @return the unsigned int as a long. * @since 1.34 */ protected static long adjustToLong(int i) { if (i < 0) { return 2 * ((long) Integer.MAX_VALUE) + 2 + i; } else { return i; } } private void deflateUntilInputIsNeeded() throws IOException { while (!def.needsInput()) { deflate(); } } private void writeVersionNeededToExtractAndGeneralPurposeBits(final int zipMethod, final boolean utfFallback) throws IOException { // CheckStyle:MagicNumber OFF int versionNeededToExtract = 10; int generalPurposeFlag = (useUTF8Flag || utfFallback) ? UFT8_NAMES_FLAG : 0; if (zipMethod == DEFLATED && raf == null) { // requires version 2 as we are going to store length info // in the data descriptor versionNeededToExtract = 20; // bit3 set to signal, we use a data descriptor generalPurposeFlag |= 8; } // CheckStyle:MagicNumber ON // version needed to extract writeOut(ZipShort.getBytes(versionNeededToExtract)); // general purpose bit flag writeOut(ZipShort.getBytes(generalPurposeFlag)); } /** * enum that represents the possible policies for creating Unicode * extra fields. */ public static final class UnicodeExtraFieldPolicy { /** * Always create Unicode extra fields. */ public static final UnicodeExtraFieldPolicy ALWAYS = new UnicodeExtraFieldPolicy("always"); /** * Never create Unicode extra fields. */ public static final UnicodeExtraFieldPolicy NEVER = new UnicodeExtraFieldPolicy("never"); /** * Create Unicode extra fields for filenames that cannot be * encoded using the specified encoding. */ public static final UnicodeExtraFieldPolicy NOT_ENCODEABLE = new UnicodeExtraFieldPolicy("not encodeable"); private final String name; private UnicodeExtraFieldPolicy(String n) { name = n; } public String toString() { return name; } } } charactermanaj/src/org/apache/tools/zip/FallbackZipEncoding.java0000644000175000017500000000570211767047544025211 0ustar paulliupaulliu/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.tools.zip; import java.io.IOException; import java.nio.ByteBuffer; /** * A fallback ZipEncoding, which uses a java.io means to encode names. * *

This implementation is not favorable for encodings other than * utf-8, because java.io encodes unmappable character as question * marks leading to unreadable ZIP entries on some operating * systems.

* *

Furthermore this implementation is unable to tell, whether a * given name can be safely encoded or not.

* *

This implementation acts as a last resort implementation, when * neither {@link Simple8BitZipEnoding} nor {@link NioZipEncoding} is * available.

* *

The methods of this class are reentrant.

*/ class FallbackZipEncoding implements ZipEncoding { private final String charset; /** * Construct a fallback zip encoding, which uses the platform's * default charset. */ public FallbackZipEncoding() { this.charset = null; } /** * Construct a fallback zip encoding, which uses the given charset. * * @param charset The name of the charset or null for * the platform's default character set. */ public FallbackZipEncoding(String charset) { this.charset = charset; } /** * @see * org.apache.tools.zip.ZipEncoding#canEncode(java.lang.String) */ public boolean canEncode(String name) { return true; } /** * @see * org.apache.tools.zip.ZipEncoding#encode(java.lang.String) */ public ByteBuffer encode(String name) throws IOException { if (this.charset == null) { return ByteBuffer.wrap(name.getBytes()); } else { return ByteBuffer.wrap(name.getBytes(this.charset)); } } /** * @see * org.apache.tools.zip.ZipEncoding#decode(byte[]) */ public String decode(byte[] data) throws IOException { if (this.charset == null) { return new String(data); } else { return new String(data,this.charset); } } } charactermanaj/src/org/apache/tools/zip/ZipEncoding.java0000644000175000017500000000633211767047544023571 0ustar paulliupaulliu/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.tools.zip; import java.io.IOException; import java.nio.ByteBuffer; /** * An interface for encoders that do a pretty encoding of ZIP * filenames. * *

There are mostly two implementations, one that uses java.nio * {@link java.nio.charset.Charset Charset} and one implementation, * which copes with simple 8 bit charsets, because java-1.4 did not * support Cp437 in java.nio.

* *

The main reason for defining an own encoding layer comes from * the problems with {@link java.lang.String#getBytes(String) * String.getBytes}, which encodes unknown characters as ASCII * quotation marks ('?'). Quotation marks are per definition an * invalid filename on some operating systems like Windows, which * leads to ignored ZIP entries.

* *

All implementations should implement this interface in a * reentrant way.

*/ interface ZipEncoding { /** * Check, whether the given string may be losslessly encoded using this * encoding. * * @param name A filename or ZIP comment. * @return Whether the given name may be encoded with out any losses. */ boolean canEncode(String name); /** * Encode a filename or a comment to a byte array suitable for * storing it to a serialized zip entry. * *

Examples for CP 437 (in pseudo-notation, right hand side is * C-style notation):

*
     *  encode("\u20AC_for_Dollar.txt") = "%U20AC_for_Dollar.txt"
     *  encode("\u00D6lf\u00E4sser.txt") = "\231lf\204sser.txt"
     * 
* * @param name A filename or ZIP comment. * @return A byte buffer with a backing array containing the * encoded name. Unmappable characters or malformed * character sequences are mapped to a sequence of utf-16 * words encoded in the format %Uxxxx. It is * assumed, that the byte buffer is positioned at the * beinning of the encoded result, the byte buffer has a * backing array and the limit of the byte buffer points * to the end of the encoded result. * @throws IOException */ ByteBuffer encode(String name) throws IOException; /** * @param data The byte values to decode. * @return The decoded string. * @throws IOException */ String decode(byte [] data) throws IOException; } charactermanaj/src/org/apache/tools/zip/ExtraFieldUtils.java0000644000175000017500000003007311767047544024427 0ustar paulliupaulliu/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package org.apache.tools.zip; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.zip.ZipException; /** * ZipExtraField related methods * */ // CheckStyle:HideUtilityClassConstructorCheck OFF (bc) @SuppressWarnings({"unchecked", "rawtypes"}) public class ExtraFieldUtils { private static final int WORD = 4; /** * Static registry of known extra fields. * * @since 1.1 */ private static final Map implementations; static { implementations = new HashMap(); register(AsiExtraField.class); register(JarMarker.class); register(UnicodePathExtraField.class); register(UnicodeCommentExtraField.class); } /** * Register a ZipExtraField implementation. * *

The given class must have a no-arg constructor and implement * the {@link ZipExtraField ZipExtraField interface}.

* @param c the class to register * * @since 1.1 */ public static void register(Class c) { try { ZipExtraField ze = (ZipExtraField) c.newInstance(); implementations.put(ze.getHeaderId(), c); } catch (ClassCastException cc) { throw new RuntimeException(c + " doesn\'t implement ZipExtraField"); } catch (InstantiationException ie) { throw new RuntimeException(c + " is not a concrete class"); } catch (IllegalAccessException ie) { throw new RuntimeException(c + "\'s no-arg constructor is not public"); } } /** * Create an instance of the approriate ExtraField, falls back to * {@link UnrecognizedExtraField UnrecognizedExtraField}. * @param headerId the header identifier * @return an instance of the appropiate ExtraField * @exception InstantiationException if unable to instantiate the class * @exception IllegalAccessException if not allowed to instatiate the class * @since 1.1 */ public static ZipExtraField createExtraField(ZipShort headerId) throws InstantiationException, IllegalAccessException { Class c = (Class) implementations.get(headerId); if (c != null) { return (ZipExtraField) c.newInstance(); } UnrecognizedExtraField u = new UnrecognizedExtraField(); u.setHeaderId(headerId); return u; } /** * Split the array into ExtraFields and populate them with the * given data as local file data, throwing an exception if the * data cannot be parsed. * @param data an array of bytes as it appears in local file data * @return an array of ExtraFields * @throws ZipException on error */ public static ZipExtraField[] parse(byte[] data) throws ZipException { return parse(data, true, UnparseableExtraField.THROW); } /** * Split the array into ExtraFields and populate them with the * given data, throwing an exception if the data cannot be parsed. * @param data an array of bytes * @param local whether data originates from the local file data * or the central directory * @return an array of ExtraFields * @since 1.1 * @throws ZipException on error */ public static ZipExtraField[] parse(byte[] data, boolean local) throws ZipException { return parse(data, local, UnparseableExtraField.THROW); } /** * Split the array into ExtraFields and populate them with the * given data. * @param data an array of bytes * @param local whether data originates from the local file data * or the central directory * @param onUnparseableData what to do if the extra field data * cannot be parsed. * @return an array of ExtraFields * @throws ZipException on error * @since Ant 1.8.1 */ public static ZipExtraField[] parse(byte[] data, boolean local, UnparseableExtraField onUnparseableData) throws ZipException { List v = new ArrayList(); int start = 0; LOOP: while (start <= data.length - WORD) { ZipShort headerId = new ZipShort(data, start); int length = (new ZipShort(data, start + 2)).getValue(); if (start + WORD + length > data.length) { switch(onUnparseableData.getKey()) { case UnparseableExtraField.THROW_KEY: throw new ZipException("bad extra field starting at " + start + ". Block length of " + length + " bytes exceeds remaining" + " data of " + (data.length - start - WORD) + " bytes."); case UnparseableExtraField.READ_KEY: UnparseableExtraFieldData field = new UnparseableExtraFieldData(); if (local) { field.parseFromLocalFileData(data, start, data.length - start); } else { field.parseFromCentralDirectoryData(data, start, data.length - start); } v.add(field); /*FALLTHROUGH*/ case UnparseableExtraField.SKIP_KEY: // since we cannot parse the data we must assume // the extra field consumes the whole rest of the // available data break LOOP; default: throw new ZipException("unknown UnparseableExtraField key: " + onUnparseableData.getKey()); } } try { ZipExtraField ze = createExtraField(headerId); if (local || !(ze instanceof CentralDirectoryParsingZipExtraField)) { ze.parseFromLocalFileData(data, start + WORD, length); } else { ((CentralDirectoryParsingZipExtraField) ze) .parseFromCentralDirectoryData(data, start + WORD, length); } v.add(ze); } catch (InstantiationException ie) { throw new ZipException(ie.getMessage()); } catch (IllegalAccessException iae) { throw new ZipException(iae.getMessage()); } start += (length + WORD); } ZipExtraField[] result = new ZipExtraField[v.size()]; return (ZipExtraField[]) v.toArray(result); } /** * Merges the local file data fields of the given ZipExtraFields. * @param data an array of ExtraFiles * @return an array of bytes * @since 1.1 */ public static byte[] mergeLocalFileDataData(ZipExtraField[] data) { final boolean lastIsUnparseableHolder = data.length > 0 && data[data.length - 1] instanceof UnparseableExtraFieldData; int regularExtraFieldCount = lastIsUnparseableHolder ? data.length - 1 : data.length; int sum = WORD * regularExtraFieldCount; for (int i = 0; i < data.length; i++) { sum += data[i].getLocalFileDataLength().getValue(); } byte[] result = new byte[sum]; int start = 0; for (int i = 0; i < regularExtraFieldCount; i++) { System.arraycopy(data[i].getHeaderId().getBytes(), 0, result, start, 2); System.arraycopy(data[i].getLocalFileDataLength().getBytes(), 0, result, start + 2, 2); byte[] local = data[i].getLocalFileDataData(); System.arraycopy(local, 0, result, start + WORD, local.length); start += (local.length + WORD); } if (lastIsUnparseableHolder) { byte[] local = data[data.length - 1].getLocalFileDataData(); System.arraycopy(local, 0, result, start, local.length); } return result; } /** * Merges the central directory fields of the given ZipExtraFields. * @param data an array of ExtraFields * @return an array of bytes * @since 1.1 */ public static byte[] mergeCentralDirectoryData(ZipExtraField[] data) { final boolean lastIsUnparseableHolder = data.length > 0 && data[data.length - 1] instanceof UnparseableExtraFieldData; int regularExtraFieldCount = lastIsUnparseableHolder ? data.length - 1 : data.length; int sum = WORD * regularExtraFieldCount; for (int i = 0; i < data.length; i++) { sum += data[i].getCentralDirectoryLength().getValue(); } byte[] result = new byte[sum]; int start = 0; for (int i = 0; i < regularExtraFieldCount; i++) { System.arraycopy(data[i].getHeaderId().getBytes(), 0, result, start, 2); System.arraycopy(data[i].getCentralDirectoryLength().getBytes(), 0, result, start + 2, 2); byte[] local = data[i].getCentralDirectoryData(); System.arraycopy(local, 0, result, start + WORD, local.length); start += (local.length + WORD); } if (lastIsUnparseableHolder) { byte[] local = data[data.length - 1].getCentralDirectoryData(); System.arraycopy(local, 0, result, start, local.length); } return result; } /** * "enum" for the possible actions to take if the extra field * cannot be parsed. */ public static final class UnparseableExtraField { /** * Key for "throw an exception" action. */ public static final int THROW_KEY = 0; /** * Key for "skip" action. */ public static final int SKIP_KEY = 1; /** * Key for "read" action. */ public static final int READ_KEY = 2; /** * Throw an exception if field cannot be parsed. */ public static final UnparseableExtraField THROW = new UnparseableExtraField(THROW_KEY); /** * Skip the extra field entirely and don't make its data * available - effectively removing the extra field data. */ public static final UnparseableExtraField SKIP = new UnparseableExtraField(SKIP_KEY); /** * Read the extra field data into an instance of {@link * UnparseableExtraFieldData UnparseableExtraFieldData}. */ public static final UnparseableExtraField READ = new UnparseableExtraField(READ_KEY); private final int key; private UnparseableExtraField(int k) { key = k; } /** * Key of the action to take. */ public int getKey() { return key; } } } charactermanaj/src/org/apache/tools/zip/UnixStat.java0000644000175000017500000000410511767047544023133 0ustar paulliupaulliu/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package org.apache.tools.zip; /** * Constants from stat.h on Unix systems. * */ // CheckStyle:InterfaceIsTypeCheck OFF - backward compatible public interface UnixStat { /** * Bits used for permissions (and sticky bit) * * @since 1.1 */ int PERM_MASK = 07777; /** * Indicates symbolic links. * * @since 1.1 */ int LINK_FLAG = 0120000; /** * Indicates plain files. * * @since 1.1 */ int FILE_FLAG = 0100000; /** * Indicates directories. * * @since 1.1 */ int DIR_FLAG = 040000; // ---------------------------------------------------------- // somewhat arbitrary choices that are quite common for shared // installations // ----------------------------------------------------------- /** * Default permissions for symbolic links. * * @since 1.1 */ int DEFAULT_LINK_PERM = 0777; /** * Default permissions for directories. * * @since 1.1 */ int DEFAULT_DIR_PERM = 0755; /** * Default permissions for plain files. * * @since 1.1 */ int DEFAULT_FILE_PERM = 0644; } charactermanaj/src/org/apache/tools/zip/UnrecognizedExtraField.java0000644000175000017500000001066211767047544025765 0ustar paulliupaulliu/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package org.apache.tools.zip; /** * Simple placeholder for all those extra fields we don't want to deal * with. * *

Assumes local file data and central directory entries are * identical - unless told the opposite.

* */ public class UnrecognizedExtraField implements CentralDirectoryParsingZipExtraField { /** * The Header-ID. * * @since 1.1 */ private ZipShort headerId; /** * Set the header id. * @param headerId the header id to use */ public void setHeaderId(ZipShort headerId) { this.headerId = headerId; } /** * Get the header id. * @return the header id */ public ZipShort getHeaderId() { return headerId; } /** * Extra field data in local file data - without * Header-ID or length specifier. * * @since 1.1 */ private byte[] localData; /** * Set the extra field data in the local file data - * without Header-ID or length specifier. * @param data the field data to use */ public void setLocalFileDataData(byte[] data) { localData = ZipUtil.copy(data); } /** * Get the length of the local data. * @return the length of the local data */ public ZipShort getLocalFileDataLength() { return new ZipShort(localData.length); } /** * Get the local data. * @return the local data */ public byte[] getLocalFileDataData() { return ZipUtil.copy(localData); } /** * Extra field data in central directory - without * Header-ID or length specifier. * * @since 1.1 */ private byte[] centralData; /** * Set the extra field data in central directory. * @param data the data to use */ public void setCentralDirectoryData(byte[] data) { centralData = ZipUtil.copy(data); } /** * Get the central data length. * If there is no central data, get the local file data length. * @return the central data length */ public ZipShort getCentralDirectoryLength() { if (centralData != null) { return new ZipShort(centralData.length); } return getLocalFileDataLength(); } /** * Get the central data. * @return the central data if present, else return the local file data */ public byte[] getCentralDirectoryData() { if (centralData != null) { return ZipUtil.copy(centralData); } return getLocalFileDataData(); } /** * @param data the array of bytes. * @param offset the source location in the data array. * @param length the number of bytes to use in the data array. * @see ZipExtraField#parseFromLocalFileData(byte[], int, int) */ public void parseFromLocalFileData(byte[] data, int offset, int length) { byte[] tmp = new byte[length]; System.arraycopy(data, offset, tmp, 0, length); setLocalFileDataData(tmp); } /** * @param data the array of bytes. * @param offset the source location in the data array. * @param length the number of bytes to use in the data array. */ public void parseFromCentralDirectoryData(byte[] data, int offset, int length) { byte[] tmp = new byte[length]; System.arraycopy(data, offset, tmp, 0, length); setCentralDirectoryData(tmp); if (localData == null) { setLocalFileDataData(tmp); } } } charactermanaj/src/org/apache/tools/zip/JarMarker.java0000644000175000017500000000623411767047544023237 0ustar paulliupaulliu/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package org.apache.tools.zip; import java.util.zip.ZipException; /** * If this extra field is added as the very first extra field of the * archive, Solaris will consider it an executable jar file. * * @since Ant 1.6.3 */ public final class JarMarker implements ZipExtraField { private static final ZipShort ID = new ZipShort(0xCAFE); private static final ZipShort NULL = new ZipShort(0); private static final byte[] NO_BYTES = new byte[0]; private static final JarMarker DEFAULT = new JarMarker(); /** No-arg constructor */ public JarMarker() { // empty } /** * Since JarMarker is stateless we can always use the same instance. * @return the DEFAULT jarmaker. */ public static JarMarker getInstance() { return DEFAULT; } /** * The Header-ID. * @return the header id */ public ZipShort getHeaderId() { return ID; } /** * Length of the extra field in the local file data - without * Header-ID or length specifier. * @return 0 */ public ZipShort getLocalFileDataLength() { return NULL; } /** * Length of the extra field in the central directory - without * Header-ID or length specifier. * @return 0 */ public ZipShort getCentralDirectoryLength() { return NULL; } /** * The actual data to put into local file data - without Header-ID * or length specifier. * @return the data * @since 1.1 */ public byte[] getLocalFileDataData() { return NO_BYTES; } /** * The actual data to put central directory - without Header-ID or * length specifier. * @return the data */ public byte[] getCentralDirectoryData() { return NO_BYTES; } /** * Populate data from this array as if it was in local file data. * @param data an array of bytes * @param offset the start offset * @param length the number of bytes in the array from offset * * @throws ZipException on error */ public void parseFromLocalFileData(byte[] data, int offset, int length) throws ZipException { if (length != 0) { throw new ZipException("JarMarker doesn't expect any data"); } } } charactermanaj/src/org/apache/tools/zip/ZipExtraField.java0000644000175000017500000000526511767047544024076 0ustar paulliupaulliu/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package org.apache.tools.zip; import java.util.zip.ZipException; /** * General format of extra field data. * *

Extra fields usually appear twice per file, once in the local * file data and once in the central directory. Usually they are the * same, but they don't have to be. {@link * java.util.zip.ZipOutputStream java.util.zip.ZipOutputStream} will * only use the local file data in both places.

* */ public interface ZipExtraField { /** * The Header-ID. * @return the header id * @since 1.1 */ ZipShort getHeaderId(); /** * Length of the extra field in the local file data - without * Header-ID or length specifier. * @return the length of the field in the local file data * @since 1.1 */ ZipShort getLocalFileDataLength(); /** * Length of the extra field in the central directory - without * Header-ID or length specifier. * @return the length of the field in the central directory * @since 1.1 */ ZipShort getCentralDirectoryLength(); /** * The actual data to put into local file data - without Header-ID * or length specifier. * @return the data * @since 1.1 */ byte[] getLocalFileDataData(); /** * The actual data to put into central directory - without Header-ID or * length specifier. * @return the data * @since 1.1 */ byte[] getCentralDirectoryData(); /** * Populate data from this array as if it was in local file data. * @param data an array of bytes * @param offset the start offset * @param length the number of bytes in the array from offset * * @since 1.1 * @throws ZipException on error */ void parseFromLocalFileData(byte[] data, int offset, int length) throws ZipException; } charactermanaj/src/org/apache/tools/zip/UnicodeCommentExtraField.java0000644000175000017500000000533711767047544026245 0ustar paulliupaulliu/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package org.apache.tools.zip; /** * Info-ZIP Unicode Comment Extra Field (0x6375): * *

Stores the UTF-8 version of the file comment as stored in the * central directory header.

* *
 *         Value         Size        Description
 *         -----         ----        -----------
 *  (UCom) 0x6375        Short       tag for this extra block type ("uc")
 *         TSize         Short       total data size for this block
 *         Version       1 byte      version of this extra field, currently 1
 *         ComCRC32      4 bytes     Comment Field CRC32 Checksum
 *         UnicodeCom    Variable    UTF-8 version of the entry comment
 * 
*/ public class UnicodeCommentExtraField extends AbstractUnicodeExtraField { public static final ZipShort UCOM_ID = new ZipShort(0x6375); public UnicodeCommentExtraField () { } /** * Assemble as unicode comment extension from the name given as * text as well as the encoded bytes actually written to the archive. * * @param text The file name * @param bytes the bytes actually written to the archive * @param off The offset of the encoded comment in bytes. * @param len The length of the encoded comment or comment in * bytes. */ public UnicodeCommentExtraField(String text, byte[] bytes, int off, int len) { super(text, bytes, off, len); } /** * Assemble as unicode comment extension from the comment given as * text as well as the bytes actually written to the archive. * * @param comment The file comment * @param bytes the bytes actually written to the archive */ public UnicodeCommentExtraField(String comment, byte[] bytes) { super(comment, bytes); } public ZipShort getHeaderId() { return UCOM_ID; } } charactermanaj/src/org/apache/tools/zip/NioZipEncoding.java0000644000175000017500000000772711767047544024250 0ustar paulliupaulliu/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.tools.zip; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.CharBuffer; import java.nio.charset.Charset; import java.nio.charset.CharsetEncoder; import java.nio.charset.CoderResult; import java.nio.charset.CodingErrorAction; /** * A ZipEncoding, which uses a java.nio {@link * java.nio.charset.Charset Charset} to encode names. * *

This implementation works for all cases under java-1.5 or * later. However, in java-1.4, some charsets don't have a java.nio * implementation, most notably the default ZIP encoding Cp437.

* *

The methods of this class are reentrant.

*/ class NioZipEncoding implements ZipEncoding { private final Charset charset; /** * Construct an NIO based zip encoding, which wraps the given * charset. * * @param charset The NIO charset to wrap. */ public NioZipEncoding(Charset charset) { this.charset = charset; } /** * @see * org.apache.tools.zip.ZipEncoding#canEncode(java.lang.String) */ public boolean canEncode(String name) { CharsetEncoder enc = this.charset.newEncoder(); enc.onMalformedInput(CodingErrorAction.REPORT); enc.onUnmappableCharacter(CodingErrorAction.REPORT); return enc.canEncode(name); } /** * @see * org.apache.tools.zip.ZipEncoding#encode(java.lang.String) */ public ByteBuffer encode(String name) { CharsetEncoder enc = this.charset.newEncoder(); enc.onMalformedInput(CodingErrorAction.REPORT); enc.onUnmappableCharacter(CodingErrorAction.REPORT); CharBuffer cb = CharBuffer.wrap(name); ByteBuffer out = ByteBuffer.allocate(name.length() + (name.length() + 1) / 2); while (cb.remaining() > 0) { CoderResult res = enc.encode(cb, out,true); if (res.isUnmappable() || res.isMalformed()) { // write the unmappable characters in utf-16 // pseudo-URL encoding style to ByteBuffer. if (res.length() * 6 > out.remaining()) { out = ZipEncodingHelper.growBuffer(out, out.position() + res.length() * 6); } for (int i=0; ir with * r.capacity() = max(b.capacity()*2,newCapacity) and * all the data contained in b copied to the beginning * of r. * */ static ByteBuffer growBuffer(ByteBuffer b, int newCapacity) { b.limit(b.position()); b.rewind(); int c2 = b.capacity() * 2; ByteBuffer on = ByteBuffer.allocate(c2 < newCapacity ? newCapacity : c2); on.put(b); return on; } /** * The hexadecimal digits 0,...,9,A,...,F encoded as * ASCII bytes. */ private static final byte[] HEX_DIGITS = new byte [] { 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46 }; /** * Append %Uxxxx to the given byte buffer. * The caller must assure, that bb.remaining()>=6. * * @param bb The byte buffer to write to. * @param c The character to write. */ static void appendSurrogate(ByteBuffer bb, char c) { bb.put((byte) '%'); bb.put((byte) 'U'); bb.put(HEX_DIGITS[(c >> 12)&0x0f]); bb.put(HEX_DIGITS[(c >> 8)&0x0f]); bb.put(HEX_DIGITS[(c >> 4)&0x0f]); bb.put(HEX_DIGITS[c & 0x0f]); } /** * name of the encoding UTF-8 */ static final String UTF8 = "UTF8"; /** * variant name of the encoding UTF-8 used for comparisions. */ private static final String UTF_DASH_8 = "utf-8"; /** * name of the encoding UTF-8 */ static final ZipEncoding UTF8_ZIP_ENCODING = new FallbackZipEncoding(UTF8); /** * Instantiates a zip encoding. * * @param name The name of the zip encoding. Specify null for * the platform's default encoding. * @return A zip encoding for the given encoding name. */ static ZipEncoding getZipEncoding(String name) { // fallback encoding is good enough for utf-8. if (isUTF8(name)) { return UTF8_ZIP_ENCODING; } if (name == null) { return new FallbackZipEncoding(); } SimpleEncodingHolder h = (SimpleEncodingHolder) simpleEncodings.get(name); if (h!=null) { return h.getEncoding(); } try { Charset cs = Charset.forName(name); return new NioZipEncoding(cs); } catch (UnsupportedCharsetException e) { return new FallbackZipEncoding(name); } } /** * Whether a given encoding - or the platform's default encoding * if the parameter is null - is UTF-8. */ static boolean isUTF8(String encoding) { if (encoding == null) { // check platform's default encoding encoding = System.getProperty("file.encoding"); } return UTF8.equalsIgnoreCase(encoding) || UTF_DASH_8.equalsIgnoreCase(encoding); } } charactermanaj/src/org/apache/tools/zip/AsiExtraField.java0000644000175000017500000002363711767047544024053 0ustar paulliupaulliu/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package org.apache.tools.zip; import java.util.zip.CRC32; import java.util.zip.ZipException; /** * Adds Unix file permission and UID/GID fields as well as symbolic * link handling. * *

This class uses the ASi extra field in the format: *

 *         Value         Size            Description
 *         -----         ----            -----------
 * (Unix3) 0x756e        Short           tag for this extra block type
 *         TSize         Short           total data size for this block
 *         CRC           Long            CRC-32 of the remaining data
 *         Mode          Short           file permissions
 *         SizDev        Long            symlink'd size OR major/minor dev num
 *         UID           Short           user ID
 *         GID           Short           group ID
 *         (var.)        variable        symbolic link filename
 * 
* taken from appnote.iz (Info-ZIP note, 981119) found at ftp://ftp.uu.net/pub/archiving/zip/doc/

* *

Short is two bytes and Long is four bytes in big endian byte and * word order, device numbers are currently not supported.

* */ public class AsiExtraField implements ZipExtraField, UnixStat, Cloneable { private static final ZipShort HEADER_ID = new ZipShort(0x756E); private static final int WORD = 4; /** * Standard Unix stat(2) file mode. * * @since 1.1 */ private int mode = 0; /** * User ID. * * @since 1.1 */ private int uid = 0; /** * Group ID. * * @since 1.1 */ private int gid = 0; /** * File this entry points to, if it is a symbolic link. * *

empty string - if entry is not a symbolic link.

* * @since 1.1 */ private String link = ""; /** * Is this an entry for a directory? * * @since 1.1 */ private boolean dirFlag = false; /** * Instance used to calculate checksums. * * @since 1.1 */ private CRC32 crc = new CRC32(); /** Constructor for AsiExtraField. */ public AsiExtraField() { } /** * The Header-ID. * @return the value for the header id for this extrafield * @since 1.1 */ public ZipShort getHeaderId() { return HEADER_ID; } /** * Length of the extra field in the local file data - without * Header-ID or length specifier. * @return a ZipShort for the length of the data of this extra field * @since 1.1 */ public ZipShort getLocalFileDataLength() { return new ZipShort(WORD // CRC + 2 // Mode + WORD // SizDev + 2 // UID + 2 // GID + getLinkedFile().getBytes().length); } /** * Delegate to local file data. * @return the centralDirectory length * @since 1.1 */ public ZipShort getCentralDirectoryLength() { return getLocalFileDataLength(); } /** * The actual data to put into local file data - without Header-ID * or length specifier. * @return get the data * @since 1.1 */ public byte[] getLocalFileDataData() { // CRC will be added later byte[] data = new byte[getLocalFileDataLength().getValue() - WORD]; System.arraycopy(ZipShort.getBytes(getMode()), 0, data, 0, 2); byte[] linkArray = getLinkedFile().getBytes(); // CheckStyle:MagicNumber OFF System.arraycopy(ZipLong.getBytes(linkArray.length), 0, data, 2, WORD); System.arraycopy(ZipShort.getBytes(getUserId()), 0, data, 6, 2); System.arraycopy(ZipShort.getBytes(getGroupId()), 0, data, 8, 2); System.arraycopy(linkArray, 0, data, 10, linkArray.length); // CheckStyle:MagicNumber ON crc.reset(); crc.update(data); long checksum = crc.getValue(); byte[] result = new byte[data.length + WORD]; System.arraycopy(ZipLong.getBytes(checksum), 0, result, 0, WORD); System.arraycopy(data, 0, result, WORD, data.length); return result; } /** * Delegate to local file data. * @return the local file data * @since 1.1 */ public byte[] getCentralDirectoryData() { return getLocalFileDataData(); } /** * Set the user id. * @param uid the user id * @since 1.1 */ public void setUserId(int uid) { this.uid = uid; } /** * Get the user id. * @return the user id * @since 1.1 */ public int getUserId() { return uid; } /** * Set the group id. * @param gid the group id * @since 1.1 */ public void setGroupId(int gid) { this.gid = gid; } /** * Get the group id. * @return the group id * @since 1.1 */ public int getGroupId() { return gid; } /** * Indicate that this entry is a symbolic link to the given filename. * * @param name Name of the file this entry links to, empty String * if it is not a symbolic link. * * @since 1.1 */ public void setLinkedFile(String name) { link = name; mode = getMode(mode); } /** * Name of linked file * * @return name of the file this entry links to if it is a * symbolic link, the empty string otherwise. * * @since 1.1 */ public String getLinkedFile() { return link; } /** * Is this entry a symbolic link? * @return true if this is a symbolic link * @since 1.1 */ public boolean isLink() { return getLinkedFile().length() != 0; } /** * File mode of this file. * @param mode the file mode * @since 1.1 */ public void setMode(int mode) { this.mode = getMode(mode); } /** * File mode of this file. * @return the file mode * @since 1.1 */ public int getMode() { return mode; } /** * Indicate whether this entry is a directory. * @param dirFlag if true, this entry is a directory * @since 1.1 */ public void setDirectory(boolean dirFlag) { this.dirFlag = dirFlag; mode = getMode(mode); } /** * Is this entry a directory? * @return true if this entry is a directory * @since 1.1 */ public boolean isDirectory() { return dirFlag && !isLink(); } /** * Populate data from this array as if it was in local file data. * @param data an array of bytes * @param offset the start offset * @param length the number of bytes in the array from offset * @since 1.1 * @throws ZipException on error */ public void parseFromLocalFileData(byte[] data, int offset, int length) throws ZipException { long givenChecksum = ZipLong.getValue(data, offset); byte[] tmp = new byte[length - WORD]; System.arraycopy(data, offset + WORD, tmp, 0, length - WORD); crc.reset(); crc.update(tmp); long realChecksum = crc.getValue(); if (givenChecksum != realChecksum) { throw new ZipException("bad CRC checksum " + Long.toHexString(givenChecksum) + " instead of " + Long.toHexString(realChecksum)); } int newMode = ZipShort.getValue(tmp, 0); // CheckStyle:MagicNumber OFF byte[] linkArray = new byte[(int) ZipLong.getValue(tmp, 2)]; uid = ZipShort.getValue(tmp, 6); gid = ZipShort.getValue(tmp, 8); if (linkArray.length == 0) { link = ""; } else { System.arraycopy(tmp, 10, linkArray, 0, linkArray.length); link = new String(linkArray); } // CheckStyle:MagicNumber ON setDirectory((newMode & DIR_FLAG) != 0); setMode(newMode); } /** * Get the file mode for given permissions with the correct file type. * @param mode the mode * @return the type with the mode * @since 1.1 */ protected int getMode(int mode) { int type = FILE_FLAG; if (isLink()) { type = LINK_FLAG; } else if (isDirectory()) { type = DIR_FLAG; } return type | (mode & PERM_MASK); } public Object clone() { try { AsiExtraField cloned = (AsiExtraField) super.clone(); cloned.crc = new CRC32(); return cloned; } catch (CloneNotSupportedException cnfe) { // impossible throw new RuntimeException(cnfe); } } } charactermanaj/src/org/apache/tools/zip/AbstractUnicodeExtraField.java0000644000175000017500000001170711767047544026404 0ustar paulliupaulliu/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package org.apache.tools.zip; import java.io.UnsupportedEncodingException; import java.util.zip.CRC32; import java.util.zip.ZipException; /** * A common base class for Unicode extra information extra fields. */ public abstract class AbstractUnicodeExtraField implements ZipExtraField { private long nameCRC32; private byte[] unicodeName; private byte[] data; protected AbstractUnicodeExtraField() { } /** * Assemble as unicode extension from the name/comment and * encoding of the orginal zip entry. * * @param text The file name or comment. * @param bytes The encoded of the filename or comment in the zip * file. * @param off The offset of the encoded filename or comment in * bytes. * @param len The length of the encoded filename or commentin * bytes. */ protected AbstractUnicodeExtraField(String text, byte[] bytes, int off, int len) { CRC32 crc32 = new CRC32(); crc32.update(bytes, off, len); nameCRC32 = crc32.getValue(); try { unicodeName = text.getBytes("UTF-8"); } catch (UnsupportedEncodingException e) { throw new RuntimeException("FATAL: UTF-8 encoding not supported.", e); } } /** * Assemble as unicode extension from the name/comment and * encoding of the orginal zip entry. * * @param text The file name or comment. * @param bytes The encoded of the filename or comment in the zip * file. */ protected AbstractUnicodeExtraField(String text, byte[] bytes) { this(text, bytes, 0, bytes.length); } private void assembleData() { if (unicodeName == null) { return; } data = new byte[5 + unicodeName.length]; // version 1 data[0] = 0x01; System.arraycopy(ZipLong.getBytes(nameCRC32), 0, data, 1, 4); System.arraycopy(unicodeName, 0, data, 5, unicodeName.length); } /** * @return The CRC32 checksum of the filename or comment as * encoded in the central directory of the zip file. */ public long getNameCRC32() { return nameCRC32; } /** * @param nameCRC32 The CRC32 checksum of the filename as encoded * in the central directory of the zip file to set. */ public void setNameCRC32(long nameCRC32) { this.nameCRC32 = nameCRC32; data = null; } /** * @return The utf-8 encoded name. */ public byte[] getUnicodeName() { return unicodeName; } /** * @param unicodeName The utf-8 encoded name to set. */ public void setUnicodeName(byte[] unicodeName) { this.unicodeName = unicodeName; data = null; } public byte[] getCentralDirectoryData() { if (data == null) { this.assembleData(); } return data; } public ZipShort getCentralDirectoryLength() { if (data == null) { assembleData(); } return new ZipShort(data.length); } public byte[] getLocalFileDataData() { return getCentralDirectoryData(); } public ZipShort getLocalFileDataLength() { return getCentralDirectoryLength(); } public void parseFromLocalFileData(byte[] buffer, int offset, int length) throws ZipException { if (length < 5) { throw new ZipException("UniCode path extra data must have at least" + " 5 bytes."); } int version = buffer[offset]; if (version != 0x01) { throw new ZipException("Unsupported version [" + version + "] for UniCode path extra data."); } nameCRC32 = ZipLong.getValue(buffer, offset + 1); unicodeName = new byte[length - 5]; System.arraycopy(buffer, offset + 5, unicodeName, 0, length - 5); data = null; } } charactermanaj/src/charactermanaj/0000755000175000017500000000000011767047543017501 5ustar paulliupaulliucharactermanaj/src/charactermanaj/clipboardSupport/0000755000175000017500000000000011767047535023036 5ustar paulliupaulliucharactermanaj/src/charactermanaj/clipboardSupport/ClipboardUtil.java0000644000175000017500000000354111767047535026441 0ustar paulliupaulliupackage charactermanaj.clipboardSupport; import java.awt.Color; import java.awt.Toolkit; import java.awt.datatransfer.Clipboard; import java.awt.image.BufferedImage; import java.io.IOException; /** * クリップボード用ユーテリティクラス.
* @author seraphy */ public final class ClipboardUtil { private ClipboardUtil() { super(); } /** * クリップボードにイメージを設定する.
* JDKのクリップボード経由の画像転送では透過色を表現できないので、背景色を指定する必要がある.
* (ただし、このアプリケーション内であれば透過色を維持したままコピー可能.)
* @param img イメージ * @param bgColor 背景色 */ public static void setImage(BufferedImage img, Color bgColor) { if (img == null || bgColor == null) { throw new IllegalArgumentException(); } Toolkit tk = Toolkit.getDefaultToolkit(); Clipboard cb = tk.getSystemClipboard(); ImageSelection imageSelection = new ImageSelection(img, bgColor); cb.setContents(imageSelection, null); } /** * クリップボード内にイメージがあるか? * @return イメージがあればtrue */ public static boolean hasImage() { Toolkit tk = Toolkit.getDefaultToolkit(); Clipboard cb = tk.getSystemClipboard(); return ImageSelection.isSupprotedFlavorAvailable(cb); } /** * クリップボードからイメージを取得する.
* 取得できる形式がない場合はnullを返す.
* @return 画像、もしくはnull * @throws IOException 読み取り中に例外が発生した場合 */ public static BufferedImage getImage() throws IOException { Toolkit tk = Toolkit.getDefaultToolkit(); Clipboard cb = tk.getSystemClipboard(); return ImageSelection.getImage(cb); } } charactermanaj/src/charactermanaj/clipboardSupport/ImageSelection.java0000644000175000017500000002035111767047535026572 0ustar paulliupaulliupackage charactermanaj.clipboardSupport; import java.awt.Color; import java.awt.datatransfer.Clipboard; import java.awt.datatransfer.DataFlavor; import java.awt.datatransfer.FlavorMap; import java.awt.datatransfer.SystemFlavorMap; import java.awt.datatransfer.Transferable; import java.awt.datatransfer.UnsupportedFlavorException; import java.awt.image.BufferedImage; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.util.Arrays; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; import javax.imageio.ImageIO; import charactermanaj.graphics.io.ImageSaveHelper; import charactermanaj.model.AppConfig; /** * クリップボードに画像をコピーするためのセレクション.
* @author seraphy */ public class ImageSelection implements Transferable { /** * ロガー */ private static final Logger logger = Logger.getLogger(ImageSelection.class.getName()); /** * 実行環境がWindowsであるか? */ private static final boolean platformWindows; /** * 対象となるイメージ */ private BufferedImage img; /** * 背景色(jpeg画像変換時) */ private Color bgColor; /** * MIME汎用(PNG). */ private static final DataFlavor PNG_FLAVOR = new DataFlavor("image/png", "image/png"); /** * MIME汎用(JPEG). */ private static final DataFlavor JPEG_FLAVOR = new DataFlavor("image/jpeg", "image/jpeg"); /** * MIME汎用(BMP). */ private static final DataFlavor BMP_FLAVOR = new DataFlavor("image/bmp", "image/bmp"); /** * サポートされている形式.
* 順序は優先順.
*/ private static final List SUPPORTED_FLAVORS; /** * クラスイニシャライザ */ static { String lcOS = System.getProperty("os.name").toLowerCase(); platformWindows = lcOS.indexOf("windows") >= 0; if (platformWindows) { // Windowsの場合 SUPPORTED_FLAVORS = Arrays.asList(new DataFlavor[] { PNG_FLAVOR, DataFlavor.imageFlavor, }); } else { // Linux, Mac OS Xの場合を想定 SUPPORTED_FLAVORS = Arrays.asList(new DataFlavor[] { PNG_FLAVOR, JPEG_FLAVOR, BMP_FLAVOR, DataFlavor.imageFlavor, }); } } /** * システムのフレーバーマップを設定する.
* @return 正常にセットアップできた場合はtrue、そうでなければfalse */ public static boolean setupSystemFlavorMap() { try { AppConfig appConfig = AppConfig.getInstance(); if (appConfig.isEnablePNGSupportForWindows()) { // "PNG"へのマップを明示的に設定する. // (Windowsの場合、デフォルトでは、画像はDBI転送となり透過情報を持つことができないため.) FlavorMap defFlavorMap = SystemFlavorMap.getDefaultFlavorMap(); if (defFlavorMap instanceof SystemFlavorMap) { SystemFlavorMap sysFlavorMap = (SystemFlavorMap) defFlavorMap; sysFlavorMap.setNativesForFlavor(PNG_FLAVOR, new String[] {"PNG"}); sysFlavorMap.setNativesForFlavor(JPEG_FLAVOR, new String[] {"JFIF"}); } } return true; } catch (Exception ex) { logger.log(Level.SEVERE, "systemFlavorMap setup failed.", ex); } return false; } /** * セレクションを構築する. * @param img 対象となるイメージ */ public ImageSelection(BufferedImage img, Color bgColor) { if (img == null) { throw new IllegalArgumentException(); } this.img = img; this.bgColor = (bgColor == null) ? Color.white : bgColor; } public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException, IOException { if (flavor != null) { logger.log(Level.FINE, "getTransferData flavor=" + flavor); try { ImageSaveHelper imageSaveHelper = new ImageSaveHelper(); if (flavor.equals(PNG_FLAVOR) || flavor.equals(JPEG_FLAVOR) || flavor.equals(BMP_FLAVOR)) { // image/png, image/jpeg, image/bmpの場合は、 // そのファイル形式のデータを生成して、それを返す. ByteArrayOutputStream bos = new ByteArrayOutputStream(); try { imageSaveHelper.savePicture(img, bgColor, bos, flavor.getMimeType(), null); } finally { bos.close(); } return new ByteArrayInputStream(bos.toByteArray()); } if (flavor.equals(DataFlavor.imageFlavor)) { // "image/x-java-image"の場合 AppConfig appConfig = AppConfig.getInstance(); if (platformWindows || !appConfig.isEnablePNGSupportForWindows()) { // Windowsの場合は、背景色で塗りつぶしたBMP画像に変換して返す. // JDK5/6のシステムクリップボードへのコピーでは透過画像をサポートしておらず透過部分が黒色になるため. // ネイティブPNGとのマッピングが有効であれば、Windowsでは、そちらで対応する. return imageSaveHelper.createBMPFormatPicture(img, bgColor); } else { // Windows以外、且つ、透過サポートが有効の場合 return img; } } } catch (RuntimeException ex) { logger.log(Level.WARNING, "The exception occurred during the data transfer of a clipboard.", ex); throw ex; } catch (IOException ex) { logger.log(Level.WARNING, "The exception occurred during the data transfer of a clipboard.", ex); throw ex; } } throw new UnsupportedFlavorException(flavor); } public DataFlavor[] getTransferDataFlavors() { return SUPPORTED_FLAVORS.toArray(new DataFlavor[SUPPORTED_FLAVORS.size()]); } public boolean isDataFlavorSupported(DataFlavor flavor) { return flavor != null && SUPPORTED_FLAVORS.contains(flavor); } public static boolean isSupprotedFlavorAvailable(Clipboard cb) { if (cb != null) { for (DataFlavor flavor : SUPPORTED_FLAVORS) { if (cb.isDataFlavorAvailable(flavor)) { return true; } } } return false; } public static BufferedImage getImage(Clipboard cb) throws IOException { if (cb == null) { return null; } try { // サポートされている形式をチェックする. for (DataFlavor flavor : cb.getAvailableDataFlavors()) { logger.log(Level.FINE, "dataFlavor(in Clipboard)=" + flavor); } DataFlavor availableFlavor = null; for (DataFlavor flavor : SUPPORTED_FLAVORS) { // 優先順にチェックし最初に見つかったサポートされている形式を採用する. if (cb.isDataFlavorAvailable(flavor)) { availableFlavor = flavor; break; } } logger.log(Level.FINE, "selected flavor=" + availableFlavor); if (availableFlavor != null) { if (availableFlavor.equals(DataFlavor.imageFlavor)) { // 汎用の画像形式で取得を試みる。 // 透過画像は使えないため、ここで取得されるものは非透過画像である。 return (BufferedImage) cb.getData(DataFlavor.imageFlavor); } if (availableFlavor.equals(PNG_FLAVOR) || availableFlavor.equals(JPEG_FLAVOR) || availableFlavor.equals(BMP_FLAVOR)) { // image/png, image/bmp, image/jpegのいずれか InputStream is = (InputStream) cb.getData(availableFlavor); if (is != null) { BufferedImage img; try { img = ImageIO.read(is); } finally { is.close(); } return img; } } } } catch (IOException ex) { logger.log(Level.WARNING, "The exception occurred in access to a clipboard.", ex); throw ex; } catch (UnsupportedFlavorException ex) { // 直前にisDataFlavorAvailableで確認しているので、 // よほどタイミングが悪くなければエラーは発生しないはず。 logger.log(Level.WARNING, "The exception occurred in access to a clipboard.", ex); throw new IOException(ex.getMessage()); } // サポートしているものが無い場合. return null; } } charactermanaj/src/charactermanaj/Main.java0000644000175000017500000001676711767047543021251 0ustar paulliupaulliupackage charactermanaj; import java.awt.Font; import java.io.File; import java.lang.reflect.Method; import java.util.logging.Level; import java.util.logging.Logger; import javax.swing.SwingUtilities; import javax.swing.UIDefaults; import javax.swing.UIManager; import charactermanaj.clipboardSupport.ImageSelection; import charactermanaj.model.AppConfig; import charactermanaj.model.util.StartupSupport; import charactermanaj.ui.MainFrame; import charactermanaj.ui.ProfileListManager; import charactermanaj.ui.SelectCharatersDirDialog; import charactermanaj.util.AWTExceptionLoggingHandler; import charactermanaj.util.ConfigurationDirUtilities; import charactermanaj.util.DirectoryConfig; import charactermanaj.util.ErrorMessageHelper; import charactermanaj.util.ApplicationLoggerConfigurator; /** * エントリポイント用クラス * @author seraphy */ public final class Main implements Runnable { /** * ロガー.
*/ private static final Logger logger = Logger.getLogger(Main.class.getName()); /** * Mac OS Xであるか? */ private static final boolean isMacOSX; /** * Linuxであるか? */ private static final boolean isLinux; /** * クラスイニシャライザ.
* 実行環境に関する定数を取得・設定する.
*/ static { // Mac OS Xでの実行判定 // システムプロパティos.nameは、すべてのJVM実装に存在する. // 基本ディレクトリの位置の決定に使うため、 // なによりも、まず、これを判定しないとダメ.(順序が重要) String lcOS = System.getProperty("os.name").toLowerCase(); isMacOSX = lcOS.startsWith("mac os x"); isLinux = lcOS.indexOf("linux") >= 0; } /** * ロガーの初期化.
* 失敗しても継続する.
*/ private static void initLogger() { try { // ロガーの準備 // ローカルファイルシステム上のユーザ定義ディレクトリから // ログの設定を読み取る.(OSにより、設定ファイルの位置が異なることに注意) ApplicationLoggerConfigurator.configure(); // SwingのEDT内の例外ハンドラの設定 (ロギングするだけ) System.setProperty("sun.awt.exception.handler", AWTExceptionLoggingHandler.class.getName()); } catch (Throwable ex) { // ロガーの準備に失敗した場合はロガーがないかもなので // コンソールに出力する. ex.printStackTrace(); logger.log(Level.SEVERE, "logger initiation failed. " + ex, ex); } } /** * UIをセットアップする. * @throws Exception いろいろな失敗 */ private static void setupUIManager() throws Exception { //System.setProperty("swing.aatext", "true"); //System.setProperty("awt.useSystemAAFontSettings", "on"); // MacOSXであれば、スクリーンメニューを有効化 if (isMacOSX()) { System.setProperty("apple.laf.useScreenMenuBar", "true"); System.setProperty( "com.apple.mrj.application.apple.menu.about.name", "CharacterManaJ"); } // 実行プラットフォームのネイティブな外観にする. UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); // JSpliderのvalueを非表示 (GTKではデフォルトで有効のため) UIManager.put("Slider.paintValue", Boolean.FALSE); // JTextAreaの既定フォントを固定幅から、標準テキストと同じフォントに変更. // (Linuxなどで固定幅フォントでは日本語フォントを持っていないため。) Object textFieldFontUI = UIManager.get("TextField.font"); if (textFieldFontUI == null) { // もし無ければダイアログUIフォントを使う.(これは日本語をサポートするであろう。) textFieldFontUI = new UIDefaults.ProxyLazyValue( "javax.swing.plaf.FontUIResource", null, new Object[] { "dialog", Integer.valueOf(Font.PLAIN), Integer.valueOf(12)}); } UIManager.put("TextArea.font", textFieldFontUI); } /** * 初期処理およびメインフレームを構築する.
* SwingのUIスレッドで実行される.
*/ public void run() { try { // UIManagerのセットアップ. try { setupUIManager(); } catch (Exception ex) { // UIManagerの設定に失敗した場合はログに書いて継続する. ex.printStackTrace(); logger.log(Level.WARNING, "UIManager setup failed.", ex); } // アプリケーション設定の読み込み AppConfig appConfig = AppConfig.getInstance(); appConfig.loadConfig(); // クリップボードサポートの設定 if ( !ImageSelection.setupSystemFlavorMap()) { logger.log(Level.WARNING, "failed to set the clipboard-support."); } // キャラクターセットディレクトリの選択 File defaultCharacterDir = ConfigurationDirUtilities.getDefaultCharactersDir(); File currentCharacterDir = SelectCharatersDirDialog.getCharacterDir(defaultCharacterDir); if (currentCharacterDir == null) { // キャンセルされたので終了する. logger.info("luncher canceled."); return; } DirectoryConfig.getInstance().setCharactersDir(currentCharacterDir); // スタートアップ時の初期化 StartupSupport.getInstance().doStartup(); // デフォルトのプロファイルを開く. // (最後に使ったプロファイルがあれば、それが開かれる.) MainFrame mainFrame = ProfileListManager.openDefaultProfile(); if (isMacOSX()) { try { // MacOSXであればスクリーンメニューからのイベントをハンドルできるようにする. // OSXにしか存在しないクラスを利用するためリフレクションとしている. // ただしJDKによっては、Apple Java Extensionsがないことも予想されるので、 // その場合はエラーにしない。 Class clz = Class.forName("charactermanaj.ui.MainFramePartialForMacOSX"); Method mtd = clz.getMethod("setupScreenMenu", MainFrame.class); mtd.invoke(null, mainFrame); } catch (Throwable ex) { logger.log(Level.CONFIG, "The Apple Java Extensions is not found.", ex); } } // 表示(および位置あわせ) mainFrame.showMainFrame(); } catch (Throwable ex) { // なんらかの致命的な初期化エラーがあった場合、ログとコンソールに表示 // ダイアログが表示されるかどうかは状況次第. ex.printStackTrace(); logger.log(Level.SEVERE, "Application initiation failed.", ex); ErrorMessageHelper.showErrorDialog(null, ex); // メインフレームを破棄します. MainFrame.closeAllProfiles(); } } /** * エントリポイント.
* 最初のメインフレームを開いたときにMac OS Xであればスクリーンメニューの登録も行う.
* @param args 引数(未使用) */ public static void main(String[] args) { // ロガー等の初期化 initLogger(); // フレームの生成等は、SwingのEDTで実行する. SwingUtilities.invokeLater(new Main()); } /** * Mac OS Xで動作しているか? * @return Max OS X上であればtrue */ public static boolean isMacOSX() { return isMacOSX; } /** * Mac OS X、もしくはlinuxで動作しているか? * @return Mac OS X、もしくはlinuxで動作していればtrue */ public static boolean isLinuxOrMacOSX() { return isLinux || isMacOSX; } } charactermanaj/src/charactermanaj/util/0000755000175000017500000000000011767047543020456 5ustar paulliupaulliucharactermanaj/src/charactermanaj/util/ResourceNames.java0000644000175000017500000000202211767047543024070 0ustar paulliupaulliupackage charactermanaj.util; import java.util.AbstractList; import java.util.Arrays; /** * 関連もしくは類似するリソースをまとめて取り扱うためにグループ化するためのクラス.
* @author seraphy */ final class ResourceNames extends AbstractList { private final String[] resourceNames; ResourceNames(String[] resourceNames) { if (resourceNames == null) { throw new IllegalArgumentException(); } this.resourceNames = resourceNames; } @Override public int hashCode() { return Arrays.hashCode(resourceNames); } @Override public boolean equals(Object obj) { if (obj == this) { return true; } if (obj != null && obj instanceof ResourceNames) { ResourceNames o = (ResourceNames) obj; return Arrays.equals(resourceNames, o.resourceNames); } return false; } @Override public int size() { return resourceNames.length; } @Override public String get(int index) { return resourceNames[index]; } } charactermanaj/src/charactermanaj/util/ApplicationLoggerConfigurator.java0000644000175000017500000000704311767047543027313 0ustar paulliupaulliupackage charactermanaj.util; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.InputStream; import java.util.logging.ConsoleHandler; import java.util.logging.Level; import java.util.logging.LogManager; import java.util.logging.Logger; public final class ApplicationLoggerConfigurator { private static final String LOGGING_PROPERTIES = "logging.properties"; private ApplicationLoggerConfigurator() { super(); } public static void configure() { // ログマネージャ. // 初期時にJRE等にある初期設定がなされている. LogManager logManager = LogManager.getLogManager(); // 設定を一旦クリアする. // ルートロガーがInfoになり、ハンドラはすべてリセット。ルートロガー以外の設定は空にされる. logManager.reset(); Exception configurationError = null; try { // ユーザーごとのアプリケーション設定ディレクトリ上の設定ファイルを取得する. File appDataDir = ConfigurationDirUtilities.getUserDataDir(); File logConfig = new File(appDataDir, LOGGING_PROPERTIES); if ( !logConfig.exists()) { // ユーザ指定のロギングプロパティがない場合、リソースからコピーする copyDefaultLogProperty(logConfig); } InputStream is = null; if (logConfig.exists()) { // ユーザー指定のロギングプロパティがある場合 is = new FileInputStream(logConfig); } else { // リソース上のロギングプロパティ is = ApplicationLoggerConfigurator.class.getResourceAsStream("/" + LOGGING_PROPERTIES); } if (is != null) { try { // ログを再設定する. logManager.readConfiguration(is); } finally { is.close(); } } } catch (Exception ex) { // 初期化に失敗した場合はログに記録するために例外を保存するが、 // 処理は継続する. configurationError = ex; } // ロガーを取得 Logger logger = Logger.getLogger(ApplicationLoggerConfigurator.class.getName()); // 初期化時に失敗した場合、デフォルトのコンソールハンドラを設定し、ログに出力する. if (configurationError != null) { logger.addHandler(new ConsoleHandler()); logger.addHandler(new ApplicationLogHandler()); logger.log(Level.WARNING, "LogConfigurationFailed", configurationError); } // 初期化時のログ logger.info("open logger."); logger.info("application configuration: baseDir=" + ConfigurationDirUtilities.getApplicationBaseDir() + " appData=" + ConfigurationDirUtilities.getUserDataDir()); } /** * デフォルトのログプロパティをユーザディレクトリにコピーする. * @param logConfig ユーザディレクトリ上のログプロパティファイル位置 */ private static void copyDefaultLogProperty(File logConfig) { try { InputStream is = ApplicationLoggerConfigurator.class .getResourceAsStream("/" + LOGGING_PROPERTIES); if (is != null) { try { FileOutputStream fos = new FileOutputStream(logConfig); try { byte buf[] = new byte[4096]; for (;;) { int rd = is.read(buf); if (rd <= 0) { break; } fos.write(buf, 0, rd); } } finally { fos.close(); } } finally { is.close(); } } } catch (Exception ex) { ex.printStackTrace(); // 失敗しても継続する } } } charactermanaj/src/charactermanaj/util/FileMappedProperties.java0000644000175000017500000001506411767047543025412 0ustar paulliupaulliupackage charactermanaj.util; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.RandomAccessFile; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; import java.sql.Timestamp; import java.util.AbstractMap; import java.util.AbstractSet; import java.util.Iterator; import java.util.Map; import java.util.Properties; import java.util.Set; /** * ファイルに保存されるPropertiesアクセスの実装.
* ファイルの読み込みまたは書き込みがあると、それ以降、排他制御される.
* ファイルを閉じた後は読み込み・書き込みはできず、プロパティの更新もできなく、IllegalStateException例外となります.
* ただし、閉じた後でもプロパティの取得は可能です.
* @author seraphy */ public class FileMappedProperties extends AbstractMap { /** * データベースファイル */ private File file; /** * データベースファイルへのランダムアクセス */ private RandomAccessFile accessFile; /** * ランダムアクセスファイルへのファイルチャネル(排他制御のため) */ private FileChannel channel; /** * プロパティ実体 */ private Properties props = new Properties(); /** * 変更フラグ */ private boolean modified; /** * データベースとなるファイルを指定してプロパティを構築する.
* 呼び出された時点でファイルがなければ作成される.
* @param file ファイル * @throws IOException 失敗 */ public FileMappedProperties(File file) throws IOException { if (file == null) { throw new IllegalArgumentException(); } this.file = file; this.accessFile = new RandomAccessFile(file, "rw"); } /** * データベースとなるファイル * @return データベースとなるファイル */ public File getFile() { return file; } /** * 変更されているか?
* putまたはremoveによって設定される.
* save、load、clearのいずれかによりリセットされる.
* @return 変更されている場合はtrue */ public boolean isModified() { return modified; } /** * データベースとなるファイルからプロパティを読み取り排他制御をかける.
* 以降、closeされるまで、ファイルはロックされます.
* すでに読み込まれている場合は何もしません。(ロックされているので自身以外の書き込みは想定しないため).
* @throws IOException 失敗 */ public void load() throws IOException { checkOpen(); if (channel != null) { // すでにロード済みと見なす. return; } props.clear(); channel = accessFile.getChannel(); channel.lock(); // 全域に排他ロック int siz = (int) channel.size(); if (siz == 0) { // 空なので読み込まない. return; } byte[] data = new byte[siz]; ByteBuffer buf = ByteBuffer.wrap(data); channel.read(buf); InputStream bis = new ByteArrayInputStream(data); try { props.loadFromXML(bis); } finally { bis.close(); } } @Override public void clear() { super.clear(); modified = false; } /** * 現在のプロパティの内容をファイルに書き戻します.
* ファイルがロックされていない場合はロックされ、以降、closeされるまでロックを維持します.
* 呼び出される都度、ファイルを一括更新します.
* @throws IOException 失敗 */ public void save() throws IOException { checkOpen(); if (channel == null) { channel = accessFile.getChannel(); channel.lock(); // 全域に排他ロック } channel.position(0); // 先頭に戻す. ByteArrayOutputStream bos = new ByteArrayOutputStream(); try { String comment = file.getName().toString() + " " + new Timestamp(System.currentTimeMillis()); props.storeToXML(bos, comment); } finally { bos.close(); } byte[] data = bos.toByteArray(); ByteBuffer buf = ByteBuffer.wrap(data); channel.write(buf); channel.truncate(data.length); modified = false; } /** * ファイルを閉じます.
* ロックされている場合はロックが解除されます.
* 未保存のプロパティは保存されません.
* @throws IOException 失敗 */ public void close() throws IOException { if (channel != null) { channel.close(); channel = null; } if (accessFile != null) { accessFile.close(); accessFile = null; } } @Override protected void finalize() throws Throwable { close(); // 念のため } @Override public String put(String key, String value) { checkOpen(); String oldValue = (String) props.setProperty(key, value); // 変更チェック if (value != oldValue) { if (value == null || oldValue == null || !value.equals(oldValue)) { // 双方がnullでなく、いずれか一方がnullであるか、equalsが一致しない場合は変更あり. modified = true; } } return oldValue; } @Override public Set> entrySet() { final Set> entrySet = props.entrySet(); return new AbstractSet>() { @Override public Iterator> iterator() { final Iterator> ite = entrySet.iterator(); return new Iterator>() { public boolean hasNext() { return ite.hasNext(); } public Map.Entry next() { final Entry entry = ite.next(); return new Map.Entry() { public String getKey() { return (String) entry.getKey(); } public String getValue() { return (String) entry.getValue(); } public String setValue(String value) { checkOpen(); return (String) entry.setValue(value); } }; } public void remove() { checkOpen(); modified = true; ite.remove(); } }; } @Override public int size() { return entrySet.size(); } }; } protected void checkOpen() { if (accessFile == null) { throw new IllegalStateException("file is already closed." + file); } } } charactermanaj/src/charactermanaj/util/LocalizedResourceTextLoader.java0000644000175000017500000000603311767047543026735 0ustar paulliupaulliupackage charactermanaj.util; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.net.URL; import java.nio.charset.Charset; import java.util.Locale; /** * リソースからローカライズされたテキストを取得する.
* @author seraphy * */ public class LocalizedResourceTextLoader extends ResourceLoader { private static final LocalizedResourceTextLoader inst = new LocalizedResourceTextLoader(); private LocalizedResourceTextLoader() { super(); } public static LocalizedResourceTextLoader getInstance() { return inst; } /** * リソース名を指定して、テキストファイルを読み込んで、その文字列を返す.
* リソースは現在のデフォルトロケールを優先で検索されます.
* ファイルエンコーディングを引数csで指定する.
* @param name リソース名 * @param cs ファイルのエンコーディング * @return ファイルの内容(テキスト) */ public String getText(String name, Charset cs) { return getText(name, cs, Locale.getDefault()); } public String getText(String name, Charset cs, Locale locale) { if (name == null || name.length() == 0 || cs == null || locale == null) { throw new IllegalArgumentException(); } String language = locale.getLanguage(); String country = locale.getCountry(); String variant = locale.getVariant(); int extpos = name.lastIndexOf("."); int folderpos = name.lastIndexOf("/"); String basename; String ext; if (folderpos > extpos) { basename = name; ext = ""; } else { basename = name.substring(0, extpos); ext = name.substring(extpos); } String[] resourceNamesStr = { basename + "_" + language + "_" + country + "_" + variant + ext, basename + "_" + language + "_" + country + ext, basename + "_" + language + ext, basename + ext, }; ResourceNames resourceNames = new ResourceNames(resourceNamesStr); String text = loadText(resourceNames, cs); if (text == null) { throw new RuntimeException("resource not found: " + resourceNames); } return text; } protected String loadText(ResourceNames resourceNames, Charset cs) { if (resourceNames == null || cs == null) { throw new IllegalArgumentException(); } for (String resourceName : resourceNames) { URL url = getResource(resourceName); if (url == null) { continue; } StringBuilder buf = new StringBuilder(); try { InputStream is = url.openStream(); try { BufferedReader rd = new BufferedReader(new InputStreamReader(is, cs)); try { int ch; while ((ch = rd.read()) != -1) { buf.append((char) ch); } } finally { rd.close(); } } finally { is.close(); } } catch (IOException ex) { throw new RuntimeException("resource loading error: " + ex, ex); } return buf.toString(); } return null; } } charactermanaj/src/charactermanaj/util/LocalizedResourcePropertyLoader.java0000644000175000017500000001244711767047543027643 0ustar paulliupaulliupackage charactermanaj.util; import java.io.InputStream; import java.net.URL; import java.util.HashMap; import java.util.Locale; import java.util.Properties; /** * xml形式のリソース上のプロパティファイルのローカライズされた読み込みを行うためのクラス.
* リソースは、単純名、言語名を末尾に付与したもの、言語名と国を末尾に付与したもの、言語名と国とバリアントを末尾に付与したもの、の順で読み取られる.
* 順番に読み込んで重ね合わせる.
* 一度読み込んだものはキャッシュされ次回以降は読み取られない.
*/ public class LocalizedResourcePropertyLoader extends ResourceLoader { /** * プロパティファイル群と、それに対するキャッシュ */ private HashMap propCache = new HashMap(); /** * シングルトンインスタンス */ private static final LocalizedResourcePropertyLoader inst = new LocalizedResourcePropertyLoader(); /** * プライベートコンストラクタ */ private LocalizedResourcePropertyLoader() { super(); } /** * インスタンスを取得する * @return インスタンス */ public static LocalizedResourcePropertyLoader getInstance() { return inst; } /** * リソース名を指定してデフォルトのロケールでローカライズされたリソースプロパティを読み込む.
* リソースはxml形式である。リソース名には.xmlを付与しない.(自動的に内部で付与される.) * @param name リソース名 * @return プロパティ */ public Properties getLocalizedProperties(String name) { return getLocalizedProperties(name, null); } /** * リソース名を指定して指定したロケールでローカライズされたリソースプロパティを読み込む.
* リソースはxml形式である。リソース名には.xmlを付与しない.(自動的に内部で付与される.) * @param name リソース名 * @param locale ロケール、nullの場合はデフォルトのロケール * @return プロパティ */ public Properties getLocalizedProperties(String name, Locale locale) { if (name == null || name.length() == 0) { throw new IllegalArgumentException(); } if (locale == null) { locale = Locale.getDefault(); } String language = locale.getLanguage(); String country = locale.getCountry(); String variant = locale.getVariant(); String[] resourceNames = { name + ".xml", name + "_" + language + ".xml", name + "_" + language + "_" + country + ".xml", name + "_" + language + "_" + country + "_" + variant + ".xml", }; return getProperties(new ResourceNames(resourceNames)); } /** * リソース名群をもとにキャッシュもしくはプロパティをロードして返す.
* キャッシュされていない場合はプロパティをロードして、それをキャッシュに格納する.
* リソースが一つも存在しない場合は実行時例外を発生させる.
* @param resourceNames リソース名群 * @return プロパティ */ protected Properties getProperties(ResourceNames resourceNames) { if (resourceNames == null) { throw new IllegalArgumentException(); } Properties prop; synchronized (propCache) { prop = propCache.get(resourceNames); if (prop == null) { prop = loadProperties(resourceNames); propCache.put(resourceNames, prop); } } if (prop == null) { throw new RuntimeException("missing resource: " + resourceNames); } return prop; } /** * リソース名群からリソースプロパティをロードして返す.
* 一つも存在しない場合はnullを返す.
* @param resourceNames リソース群名 * @return プロパティ */ protected Properties loadProperties(ResourceNames resourceNames) { if (resourceNames == null) { throw new IllegalArgumentException(); } // システム埋め込みリソースでプロパティを取得したのちに、ユーザ指定のプロパティの内容で上書きする. // バージョンアップによりキーが増えて、既存のローカルファイル上のプロパティファイルにキーが存在しない場合でも // 安全なようにするためのもの。 ClassLoader[] loaders = new ClassLoader[] { getDefaultClassLoader(), getLocalizedClassLoader(null), }; boolean foundResource = false; Properties props = new Properties(); for (ClassLoader loader : loaders) { if (loader == null) { continue; } for (String resourceName : resourceNames) { URL resource = loader.getResource(resourceName); if (resource != null) { Properties org = new Properties(); try { InputStream is = resource.openStream(); try { org.loadFromXML(is); } finally { is.close(); } } catch (Exception ex) { throw new RuntimeException("resource loading error." + resource, ex); } foundResource = true; props.putAll(org); } } } if (foundResource) { return props; } return null; } } charactermanaj/src/charactermanaj/util/BeanPropertiesUtilities.java0000644000175000017500000001510411767047543026140 0ustar paulliupaulliupackage charactermanaj.util; import java.awt.Color; import java.beans.BeanInfo; import java.beans.IntrospectionException; import java.beans.Introspector; import java.beans.PropertyDescriptor; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.math.BigDecimal; import java.math.BigInteger; import java.util.HashSet; import java.util.Properties; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; /** * Setter/Getterのペアをもつビーンのプロパティを文字列化してプロパティに設定するか、 * プロパティからビーンに値を設定するためのユーテリティクラス.
* @author seraphy */ public final class BeanPropertiesUtilities { private static final Logger logger = Logger.getLogger(BeanPropertiesUtilities.class.getName()); private BeanPropertiesUtilities() { throw new RuntimeException("utilities class."); } /** * ビーンのSetter/Getterのペアをもつプロパティに対して、Propertiesより該当するプロパティの値を * 読み取り、プロパティに設定します.
* Propertiesに該当するプロパティ名が設定されていなければスキップされます.
* Propertiesにビーンにないプロパティ名があった場合、それは単に無視されます.
* Propertyの値が空文字の場合、Beanのプロパティの型が文字列以外であればnullが設定されます.
* (文字列の場合、空文字のまま設定されます.書き込み時、nullは空文字にされるため、文字列についてはnullを表現することはできません。)
* @param bean 設定されるビーン * @param props プロパティソース * @return 値の設定を拒否されたプロパティの名前、エラーがなければ空 */ public static Set loadFromProperties(Object bean, Properties props) { if (bean == null || props == null) { throw new IllegalArgumentException(); } HashSet rejectNames = new HashSet(); try { BeanInfo beanInfo = Introspector.getBeanInfo(bean.getClass()); for (PropertyDescriptor propDesc : beanInfo .getPropertyDescriptors()) { Class typ = propDesc.getPropertyType(); Method mtdReader = propDesc.getReadMethod(); Method mtdWriter = propDesc.getWriteMethod(); if (mtdReader != null && mtdWriter != null) { // 読み書き双方が可能なもののみ対象とする. String name = propDesc.getName(); String strVal = props.getProperty(name); if (strVal == null) { // 設定がないのでスキップ continue; } Object val; Throwable reject = null; try { if (String.class.equals(typ)) { val = strVal; } else if (strVal.length() == 0) { val = null; } else { if (Boolean.class.equals(typ) || boolean.class.equals(typ)) { val = Boolean.valueOf(strVal); } else if (Integer.class.equals(typ) || int.class.equals(typ)) { val = Integer.valueOf(strVal); } else if (Long.class.equals(typ) || long.class.equals(typ)) { val = Long.valueOf(strVal); } else if (Float.class.equals(typ) || float.class.equals(typ)) { val = Float.valueOf(strVal); } else if (Double.class.equals(typ) || double.class.equals(typ)) { val = Double.valueOf(strVal); } else if (BigInteger.class.equals(typ)) { val = new BigInteger(strVal); } else if (BigDecimal.class.equals(typ)) { val = new BigDecimal(strVal); } else if (Color.class.equals(typ)) { val = Color.decode(strVal); } else { rejectNames.add(name); logger.log(Level.WARNING, "unsupported propery type: " + typ + "/beanClass=" + bean.getClass() + " #" + name); continue; } } mtdWriter.invoke(bean, val); reject = null; } catch (InvocationTargetException ex) { reject = ex; } catch (IllegalAccessException ex) { reject = ex; } catch (RuntimeException ex) { reject = ex; } if (reject != null) { rejectNames.add(name); logger.log(Level.WARNING, "invalid propery: " + typ + "/beanClass=" + bean.getClass() + " #" + name + " /val=" + strVal , reject); } } } } catch (IntrospectionException ex) { throw new RuntimeException("bean intorospector failed. :" + bean.getClass(), ex); } return rejectNames; } /** * ビーンのSetter/Getterのペアをもつプロパティの各値をPropertiesに文字列として登録します.
* nullの場合は空文字が設定されます.
* @param bean プロパティに転送する元情報となるビーン * @param props 設定されるプロパティ */ public static void saveToProperties(Object bean, Properties props) { if (bean == null || props == null) { throw new IllegalArgumentException(); } try { BeanInfo beanInfo = Introspector.getBeanInfo(bean.getClass()); for (PropertyDescriptor propDesc : beanInfo.getPropertyDescriptors()) { Method mtdReader = propDesc.getReadMethod(); Method mtdWriter = propDesc.getWriteMethod(); if (mtdReader != null && mtdWriter != null) { // 読み書き双方が可能なもののみ対象とする. String name = propDesc.getName(); Object val = mtdReader.invoke(bean); String strVal; if (val == null) { strVal = ""; } else if (val instanceof String) { strVal = (String) val; } else if (val instanceof Number) { strVal = ((Number) val).toString(); } else if (val instanceof Boolean) { strVal = ((Boolean) val).booleanValue() ? "true" : "false"; } else if (val instanceof Color) { strVal = "#" + Integer.toHexString(((Color) val).getRGB() & 0xffffff); } else { logger.log(Level.WARNING, "unsupported propery type: " + val.getClass() + "/beanClass=" + bean.getClass() + " #" + name); continue; } props.setProperty(name, strVal); } } } catch (IllegalAccessException ex) { throw new RuntimeException("bean property read failed. :" + bean.getClass(), ex); } catch (InvocationTargetException ex) { throw new RuntimeException("bean property read failed. :" + bean.getClass(), ex); } catch (IntrospectionException ex) { throw new RuntimeException("bean intorospector failed. :" + bean.getClass(), ex); } } } charactermanaj/src/charactermanaj/util/LocalizedMessageAware.java0000644000175000017500000000065311767047543025520 0ustar paulliupaulliupackage charactermanaj.util; /** * ローカライズリソースを持っていることを示すインターフェイス.
* @author seraphy */ public interface LocalizedMessageAware { /** * ローカライズリソースのIDを取得する.
* 設定されていない場合はnullが返される.
* @return リソースID、もしくはnull */ String getLocalizedResourceId(); } charactermanaj/src/charactermanaj/util/AWTExceptionLoggingHandler.java0000644000175000017500000000233311767047543026441 0ustar paulliupaulliupackage charactermanaj.util; import java.util.logging.Level; import java.util.logging.Logger; import javax.swing.JFrame; import javax.swing.SwingUtilities; import charactermanaj.ui.MainFrame; /** * SwingのEDT内での例外をロギングするためのハンドラ. * @author seraphy */ public class AWTExceptionLoggingHandler { /** * ロガー */ private static final Logger logger = Logger.getLogger(AWTExceptionLoggingHandler.class.getName()); /** * 例外のハンドル.
* @param ex 例外 */ public void handle(final Throwable ex) { // まずはロギング logger.log(Level.SEVERE, "exception occurred on the event dispatch thread. " + ex, ex); // エラーダイアログを表示する.(非同期) SwingUtilities.invokeLater(new Runnable() { public void run() { JFrame currentFrame = MainFrame.getActivedMainFrame(); if (currentFrame == null || !currentFrame.isDisplayable() || !currentFrame.isVisible()) { // メインフレームがまだ無いか、表示されていないか破棄済みであれば無いとみなす. currentFrame = null; } ErrorMessageHelper.showErrorDialog(currentFrame, ex); } }); } } charactermanaj/src/charactermanaj/util/FileUserData.java0000644000175000017500000000443511767047543023637 0ustar paulliupaulliupackage charactermanaj.util; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.OutputStream; import java.util.logging.Level; import java.util.logging.Logger; /** * ファイルベースのユーザーデータの保存先の実装 * @author seraphy */ public class FileUserData implements UserData { /** * ロガー */ private static final Logger logger = Logger.getLogger(FileUserData.class.getName()); /** * 保存先ファイル */ private File file; public FileUserData(File file) { if (file == null) { throw new IllegalArgumentException(); } this.file = file; } public boolean exists() { return file.exists() && file.isFile(); } public long lastModified() { return file.lastModified(); } public InputStream openStream() throws IOException { return new BufferedInputStream(new FileInputStream(file)); } public OutputStream getOutputStream() throws IOException { return new BufferedOutputStream(new FileOutputStream(file)); } public boolean delete() { try { return file.delete(); } catch (Exception ex) { // セキュリティ例外ぐらい. logger.log(Level.WARNING, "file removing failed." + file, ex); return false; } } public void save(Object userData) throws IOException { ObjectOutputStream oos = new ObjectOutputStream(getOutputStream()); try { oos.writeObject(userData); oos.close(); } finally { oos.close(); } } public Object load() throws IOException { ObjectInputStream ois = new ObjectInputStream(openStream()); try { try { return ois.readObject(); } catch (ClassNotFoundException ex) { // 復元先クラスがみつからないということは、このアプリケーションの保存した形式としておかしい IOException ex2 = new IOException("invalid format."); ex2.initCause(ex2); throw ex2; } } finally { ois.close(); } } @Override public String toString() { return "FileUserData{file:" + file + "}"; } }charactermanaj/src/charactermanaj/util/DesktopUtilities.java0000644000175000017500000001427411767047543024636 0ustar paulliupaulliupackage charactermanaj.util; import java.awt.Component; import java.awt.Toolkit; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.io.File; import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.net.URI; import java.util.logging.Level; import java.util.logging.Logger; import javax.swing.BoxLayout; import javax.swing.JLabel; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JTextField; /** * デスクトップへのアクセスを提供するユーテリティ.
* JDK6の機能を使うため、JDK5以前では何もしない.(エラーにはならない) * @author seraphy */ public class DesktopUtilities { /** * ロガー */ private static final Logger logger = Logger.getLogger(DesktopUtilities.class.getName()); /** * デスクトップオブジェクト。JDK6以降でなければ、もしくはデスクトップをサポートしていなければnull */ private static Object desktopObj; /** * ブラウズメソッド */ private static Method methodBrowse; /** * 編集メソッド */ private static Method methodEdit; /** * 開くメソッド */ private static Method methodOpen; static { try { Class clz = Class.forName("java.awt.Desktop"); Method mtdGetDesktop = clz.getMethod("getDesktop"); methodBrowse = clz.getMethod("browse", URI.class); methodEdit = clz.getMethod("edit", File.class); methodOpen = clz.getMethod("open", File.class); desktopObj = mtdGetDesktop.invoke(null); } catch (ClassNotFoundException ex) { // JDK6以降でない場合 logger.log(Level.CONFIG, "AWT Desktop is not suuported."); desktopObj = null; } catch (Exception ex) { // その他の例外は基本的に発生しないが、発生したとしても // 単にサポートしていないと見なして継続する. logger.log(Level.SEVERE, "AWT Desktop failed.", ex); desktopObj = null; } } private DesktopUtilities() { throw new RuntimeException("utilities class."); } public static boolean isSupported() { return desktopObj != null; } protected static boolean callMethod(Method method, Object arg) throws IOException { if (desktopObj == null) { return false; } try { if (logger.isLoggable(Level.FINER)) { logger.log(Level.FINER, "invoke: " + method + "/arg=" + arg); } method.invoke(desktopObj, arg); return true; } catch (InvocationTargetException ex) { Throwable iex = ex.getCause(); if (iex != null && iex instanceof IOException) { throw (IOException) iex; } throw new RuntimeException(ex.getMessage(), ex); } catch (IllegalAccessException ex) { throw new RuntimeException(ex.getMessage(), ex); } } /** * ファイルを開く. * @param uri ファイル * @return サポートしていない場合はfalse、実行できればtrue。 * @throws IOException 実行できなかった場合 */ public static boolean browse(URI uri) throws IOException { return callMethod(methodBrowse, uri); } /** * 指定したdocBaseの親ディレクトリを開く. * @param docBase * @return サポートしていない場合はfalse、実行できればtrue。 * @throws IOException 実行できなかった場合 */ public static boolean browseBaseDir(URI docBase) throws IOException { File baseDir = null; try { if (docBase != null) { baseDir = new File(docBase).getParentFile(); } } catch (Exception ex) { baseDir = null; } if (baseDir == null) { Toolkit tk = Toolkit.getDefaultToolkit(); tk.beep(); return false; } return DesktopUtilities.open(baseDir); } /** * ファイルを編集する. * @param file ファイル * @return サポートしていない場合はfalse、実行できればtrue。 * @throws IOException 実行できなかった場合 */ public static boolean edit(File file) throws IOException { return callMethod(methodEdit, file); } /** * ファイルを編集する. * @param file ファイル * @return サポートしていない場合はfalse、実行できればtrue。 * @throws IOException 実行できなかった場合 */ public static boolean open(File file) throws IOException { return callMethod(methodOpen, file); } /** * ブラウザでURLを開きます.
* JDK1.6未満の場合はブラウザを開く代わりにURLと、それを説明するメッセージボックスが表示されます.
* @param parent 親フレーム、またはダイアログ * @param url URL * @param description ブラウズがサポートされていない場合に表示するダイアログでのURLの説明文 */ public static void browse(final Component parent, final String url, final String description) { try { URI helpURI = new URI(url); if (!DesktopUtilities.browse(helpURI) ){ // JDK5で実行中の場合 JPanel panel = new JPanel(); BoxLayout layout = new BoxLayout(panel, BoxLayout.Y_AXIS); panel.setLayout(layout); panel.add(new JLabel(description)); JTextField txtURL = new JTextField(url); panel.add(txtURL); JOptionPane.showMessageDialog(parent, panel); } } catch (Exception ex) { ErrorMessageHelper.showErrorDialog(parent, ex); } } /** * ブラウザでURLを開くアクションの生成.
* エラー時はエラーダイアログが開かれる.
* 返されるアクションがとるアクションイベントは無視されるため、nullを渡しても問題ありません.
* @param parent 親フレーム、またはダイアログ * @param url URLの文字列 * @param description ブラウズがサポートされていない場合に表示するダイアログでのURLの説明文 * @return アクション */ public static ActionListener createBrowseAction(final Component parent, final String url, final String description) { return new ActionListener() { public void actionPerformed(ActionEvent e) { browse(parent, url, description); } }; } } charactermanaj/src/charactermanaj/util/SetupLocalization.java0000644000175000017500000001016011767047543024770 0ustar paulliupaulliupackage charactermanaj.util; import java.io.BufferedReader; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.net.URL; import java.net.URLConnection; import java.util.ArrayList; import java.util.Collection; import java.util.logging.Level; import java.util.logging.Logger; import charactermanaj.Main; /** * 言語リソースを管理する. * @author seraphy */ public class SetupLocalization extends ResourceLoader { private final Logger logger = Logger.getLogger(getClass().getName()); public static final String DIRNAME_RESOURCES = "resources"; public static final String CATALOG_NAME = "/resource_list.txt"; private File baseDir; /** * アプリケーションデータ用ディレクトリを指定して構築する. * @param baseDir データディレクトリ */ public SetupLocalization(File baseDir) { if (baseDir == null || !baseDir.isDirectory()) { throw new IllegalArgumentException(); } this.baseDir = baseDir; } /** * リソース一覧を取得する.
* クラスパス上の一覧を取得する汎用的な方法はないため、カタログファイルを使用する.
* {@link #CATALOG_NAME}で示されるリソース上のファイルにリソース一覧が記録されているものとする.
* @return リソース一覧(言語関連リソースのみ) * @throws IOException 失敗 */ protected Collection getResourceList() throws IOException { ArrayList resources = new ArrayList(); URL listURL = Main.class.getResource(CATALOG_NAME); InputStream is = listURL.openStream(); try { BufferedReader rd = new BufferedReader(new InputStreamReader(is)); try { String line; while ((line = rd.readLine()) != null) { line = line.trim(); if (line.startsWith("#")) { continue; } resources.add(line); } } finally { rd.close(); } } finally { is.close(); } logger.log(Level.FINE, "resource list: " +resources); return resources; } /** * リソースをファイルにコピーする.
* @param fromURL * @param toFile * @throws IOException */ protected void copyResource(URL fromURL, File toFile) throws IOException { logger.log(Level.INFO, "copy resource '" + fromURL + "' to '" + toFile + "'"); File dir = toFile.getParentFile(); if ( !dir.exists()) { if ( !dir.mkdirs()) { throw new IOException("can't create directory. " + dir); } } URLConnection conn = fromURL.openConnection(); conn.setDoInput(true); InputStream is = conn.getInputStream(); try { long lastModified = conn.getLastModified(); OutputStream os = new FileOutputStream(toFile); try { byte[] buf = new byte[4096]; for (;;) { int rd = is.read(buf); if (rd <= 0) { break; } os.write(buf, 0, rd); } } finally { os.close(); } boolean result = toFile.setLastModified(lastModified); logger.log(Level.FINE, "setLastModified(" + toFile+ ") succeeded=" + result); } finally { is.close(); } } /** * リソースディレクトリを返す. * @return リソースディレクトリ */ public File getResourceDir() { try { return new File(baseDir, DIRNAME_RESOURCES).getCanonicalFile(); } catch (Exception ex) { throw new RuntimeException(ex); } } /** * ローカルシステム上のアプリケーションデータディレクトリに言語リソースをコピーする. * @throws IOException 失敗 */ public void setupToLocal() throws IOException { File toDir = getResourceDir(); ClassLoader cl = getDefaultClassLoader(); for (String resourceName : getResourceList()) { URL url = cl.getResource(resourceName); if (url != null) { File toFile = new File(toDir, resourceName).getCanonicalFile(); copyResource(url, toFile); } else { logger.log(Level.WARNING, "missing resource: " + resourceName); } } } } charactermanaj/src/charactermanaj/util/SystemUtil.java0000644000175000017500000000372511767047543023452 0ustar paulliupaulliupackage charactermanaj.util; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; /** * システムクラスのためのユーテリティ.
* Findbugsの警告がうっとおしいのでリフレクションで隠す.
* * @author seraphy */ public final class SystemUtil { private static final Class clsSystem; private static final Method garbageCollection; private static final Method exit; private static final int gcLoop = 3; private static final long sleepTime = 100; static { try { clsSystem = System.class; garbageCollection = clsSystem.getMethod("gc"); exit = clsSystem.getMethod("exit", int.class); } catch (NoSuchMethodException ex) { throw new RuntimeException(ex.getMessage(), ex); } catch (SecurityException ex) { throw new RuntimeException(ex.getMessage(), ex); } } private SystemUtil() { super(); } /** * 数回ガベージコレクションをかける.
*/ public static void gc() { try { for (int i = 0; i < gcLoop; i++) { if (i != 0) { Thread.sleep(sleepTime); } garbageCollection.invoke(null); } } catch (InterruptedException ex) { // 無視する. } catch (InvocationTargetException ex) { Throwable iex = ex.getCause(); if (iex == null) { iex = ex; } throw new RuntimeException(iex.getMessage(), iex); } catch (IllegalAccessException ex) { throw new RuntimeException(ex.getMessage(), ex); } } /** * JVMを終了する. * @param exitCode 終了コード */ public static void exit(int exitCode) { try { exit.invoke(null, exitCode); } catch (InvocationTargetException ex) { Throwable iex = ex.getCause(); if (iex == null) { iex = ex; } throw new RuntimeException(iex.getMessage(), iex); } catch (IllegalAccessException ex) { throw new RuntimeException(ex.getMessage(), ex); } } } charactermanaj/src/charactermanaj/util/ResourceLoader.java0000644000175000017500000000714711767047543024250 0ustar paulliupaulliupackage charactermanaj.util; import java.io.File; import java.net.MalformedURLException; import java.net.URL; import java.net.URLClassLoader; import java.security.AccessController; import java.security.PrivilegedAction; import java.security.PrivilegedExceptionAction; /** * リソースをロードするための抽象基底クラス. * @author seraphy */ public abstract class ResourceLoader { /** * クラスローダを取得する.
* まずローカルファイル上のリソースディレクトリがあれば、それを検索する.
* つぎにスレッドに関連づけられているコンテキストクラスローダか、もしなければ、このクラスをロードしたクラスローダを用いて検索する.
* @return クラスローダ */ protected ClassLoader getClassLoader() { return getLocalizedClassLoader(getDefaultClassLoader()); } /** * クラスローダを取得する.
* スレッドに関連づけられているコンテキストクラスローダか、もしなければ、このクラスをロードしたクラスローダを返す.
* @return クラスローダ */ protected ClassLoader getDefaultClassLoader() { return AccessController.doPrivileged(new PrivilegedAction() { public ClassLoader run() { ClassLoader cl = Thread.currentThread().getContextClassLoader(); if (cl == null) { cl = ResourceLoader.class.getClassLoader(); } return cl; } }); } /** * ローカルファイル上のリソースディレクトリにアクセスするクラスローダ取得する.
* 作成されていなければparentをそのまま返す.
* リソースはローカルファイル上のパスで検索されたのちにparentで検索されます.(標準のURLClassLoaderとは違う探索方法)
* @param parent 親クラスローダ、nullの場合は親の探索をしない. * @return ローカルシステム上のリソースディレクトリにアクセスするクラスローダ、なければparentのまま */ protected ClassLoader getLocalizedClassLoader(final ClassLoader parent) { try { File baseDir = ConfigurationDirUtilities.getUserDataDir(); SetupLocalization localize = new SetupLocalization(baseDir); final File resourceDir = localize.getResourceDir(); if (!resourceDir.exists() || !resourceDir.isDirectory()) { return parent; } URLClassLoader cl = AccessController.doPrivileged(new PrivilegedExceptionAction() { public URLClassLoader run() throws MalformedURLException { URL[] urls = new URL[] { resourceDir.toURI().toURL() }; return new URLClassLoader(urls, parent) { @Override public URL getResource(String name) { URL url = findResource(name); // 子が優先 (標準と逆) if (url == null) { ClassLoader parent = getParent(); if (parent != null) { url = parent.getResource(name); } } return url; } }; } }); return cl; } catch (Exception ex) { ex.printStackTrace(); return null; } } /** * クラスローダによりリソースをロードする.
* 該当するリソースが存在しない場合はnullを返す.
* リソース名がnullの場合もnullを返す.
* @param name リソース名またはnull * @return リソースがあれば、そのURL。なければnull */ protected URL getResource(String name) { if (name == null) { return null; } return getClassLoader().getResource(name); } } charactermanaj/src/charactermanaj/util/ApplicationLogHandler.java0000644000175000017500000001042211767047543025523 0ustar paulliupaulliupackage charactermanaj.util; import java.io.File; import java.io.FileOutputStream; import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.sql.Timestamp; import java.text.SimpleDateFormat; import java.util.logging.Handler; import java.util.logging.Level; import java.util.logging.LogRecord; import charactermanaj.model.AppConfig; /** * このアプリケーションの活動を記録するログハンドラ.
* アプリケーション用のディレクトリのlogsフォルダ下に開始日時のファイル名をもつログファイルを作成し、ログを記録する.
* ただし、終了時、警告以上のログが一度も書き込まれなかった場合はログファィルは自動的に削除される.
* * @author seraphy */ public class ApplicationLogHandler extends Handler { private static final String LOGS_DIR = "logs"; private final Object lock = new Object(); private final File logFile; private PrintWriter pw; private boolean notRemove; public ApplicationLogHandler() { File appDir = ConfigurationDirUtilities.getUserDataDir(); File logsDir = new File(appDir, LOGS_DIR); if (!logsDir.exists()) { if (!logsDir.mkdirs()) { // ログ記録場所が作成できていないのでコンソールに出すしかない. System.err.println("can't create the log directory. " + logsDir); } } String fname = getCurrentTimeForFileName() + ".log"; logFile = new File(logsDir, fname); PrintWriter tmp; try { tmp = new PrintWriter(new OutputStreamWriter(new FileOutputStream(logFile))); } catch (Exception ex) { ex.printStackTrace(); // ロガーが失敗しているので、この失敗はコンソールに出すしかない。 tmp = null; } this.pw = tmp; } @Override public void close() throws SecurityException { synchronized (lock) { // 終了時にAppConfigにアクセスする. // (アプリケーションの終了時にアクセスすることで初期化タイミングの問題を避ける.) try { AppConfig appConfig = AppConfig.getInstance(); if (appConfig.isNoRemoveLog()) { notRemove = true; } } catch (Exception ex) { // なんらかのアクセスに失敗した場合でも継続できるようにする. // ロガーが閉じられようとしているので、pwに直接出力する notRemove = true; try { if (pw != null) { ex.printStackTrace(pw); } } catch (Exception iex) { iex.printStackTrace(); // コンソールに出す他ない } } if (pw != null) { pw.close(); pw = null; } if (logFile != null && !notRemove) { // 警告未満のログしかない場合はログファイルは毎回削除する. if (!logFile.delete()) { System.err.println("can't delete file. " + logFile); } } } } @Override public void flush() { synchronized (lock) { if (pw != null) { pw.flush(); } } } @Override public void publish(LogRecord record) { if (record == null) { return; } // メッセージの記録 synchronized (lock) { if (pw == null) { return; } Level lv = record.getLevel(); String name = record.getLoggerName(); pw.println("#" + getCurrentTime() + " " + name + " " + lv.getLocalizedName() + " " + record.getMessage()); // 例外があれば、例外の記録 Throwable tw = record.getThrown(); if (tw != null) { tw.printStackTrace(pw); // 例外のコールスタックをロガーに出力 } // フラッシュする.(随時、ファイルの中身を見ることができるように.) pw.flush(); // 警告以上であれば終了時にファイルを消さない if (lv.intValue() >= Level.WARNING.intValue()) { notRemove = true; } } } public String getCurrentTime() { Timestamp tm = new Timestamp(System.currentTimeMillis()); SimpleDateFormat dt = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"); return dt.format(tm); } public String getCurrentTimeForFileName() { Timestamp tm = new Timestamp(System.currentTimeMillis()); SimpleDateFormat dt = new SimpleDateFormat("yyyy-MM-dd_HHmmssSSS"); return dt.format(tm); } } charactermanaj/src/charactermanaj/util/ConfigurationDirUtilities.java0000644000175000017500000001351511767047543026470 0ustar paulliupaulliupackage charactermanaj.util; import java.io.File; import java.io.IOException; import java.net.URL; import java.security.CodeSource; import java.security.ProtectionDomain; import java.util.logging.Level; import java.util.logging.Logger; import charactermanaj.Main; /** * アプリケーションの設定ファイル等の位置を取得するユーテリティクラス.
* Mainクラスのロード時に最も早くロードされるであろうクラスの一つである.
* @author seraphy */ public final class ConfigurationDirUtilities { public static final String CONFIGURATION_DIR_NAME = "CharacterManaJ"; private static File userDataDir; private static File applicationBaseDir; private ConfigurationDirUtilities() { throw new RuntimeException("utilities class."); } /** * ユーザーごとのアプリケーションデータ保存先を取得する.
* 環境変数「APPDATA」もしくはシステムプロパティ「appdata.dir」からベース位置を取得する.
* いずれも設定されておらず、Mac OS Xであれば「~/Library」をベース位置とする。 * Mac OS Xでなければ「~/」をベース位置とする.
* これに対してシステムプロパティ「characterdata.dirname」(デフォルトは「CharacterManaJ」)という * フォルダをユーザー毎のアプリケーションデータの保存先ディレクトリとする.
*/ public synchronized static File getUserDataDir() { if (userDataDir == null) { String appData = null; // システムプロパティ「appdata.dir」を探す appData = System.getProperty("appdata.dir"); if (appData == null) { // なければ環境変数APPDATAを探す // Windows2000/XP/Vista/Windows7には存在する. appData = System.getenv("APPDATA"); } if (appData == null && Main.isMacOSX()) { // システムプロパティも環境変数にも設定がなく、実行環境がMac OS Xであれば // ~/Libraryをベースにする.(Mac OS Xならば必ずある。) appData = new File(System.getProperty("user.home"), "Library").getPath(); } if (appData == null) { // なければシステムプロパティ「user.home」を使う // このプロパティは必ず存在する. appData = System.getProperty("user.home"); } // システムプロパティ「characterdata.dirname」のディレクトリ名、なければ「CharacterManaJ」を設定する. String characterDirName = System.getProperty("characterdata.dirname", CONFIGURATION_DIR_NAME); userDataDir = new File(appData, characterDirName).getAbsoluteFile(); // ディレクトリを準備する. if (!userDataDir.exists()) { if (!userDataDir.mkdirs()) { // ログ保管場所も設定されていないのでコンソールに出すしかない. System.err.println("can't create the user data directory. " + userDataDir); } } } return userDataDir; } /** * アプリケーションディレクトリを取得する.
* このクラスをコードソースから、ベースとなる位置を割り出す.
* クラスが格納されているクラスパスのフォルダか、JARに固められている場合は、そのJARファイルの、その親ディレクトリを指し示す.
* このクラスのプロテクションドメインのコードソースがnullでコードの位置が取得できないか、 * コードの位置を示すURLがファイルプロトコルでない場合は実行時例外が返される.
* ただし、システムプロパティ「appbase.dir」が明示的に設定されていれば、それが優先される.
*/ public synchronized static File getApplicationBaseDir() { if (applicationBaseDir == null) { String appbaseDir = System.getProperty("appbase.dir"); if (appbaseDir != null && appbaseDir.length() > 0) { // 明示的にアプリケーションベースディレクトリが指定されている場合. try { applicationBaseDir = new File(appbaseDir).getCanonicalFile(); } catch (IOException ex) { ex.printStackTrace(); // 継続する.まだログの準備ができていない可能性が高いので標準エラー出力へ. } } if (applicationBaseDir == null) { // 明示的に指定されていない場合はコードの実行位置から割り出す. ProtectionDomain pdomain = ConfigurationDirUtilities.class.getProtectionDomain(); CodeSource codeSource = pdomain.getCodeSource(); if (codeSource == null) { throw new RuntimeException("codeSource is null: domain=" + pdomain); } URL codeBaseUrl = codeSource.getLocation(); if (!codeBaseUrl.getProtocol().equals("file")) { throw new RuntimeException("codeLocation is not file protocol.: " + codeBaseUrl); } // クラスパスフォルダ、またはJARファイルの、その親 applicationBaseDir = new File(codeBaseUrl.getPath()).getParentFile(); } } return applicationBaseDir; } /** * デフォルトのユーザー固有のキャラクターデータディレクトリを取得する.
* ユーザー固有のキャラクターディレクトリがまだ存在しない場合は作成される.
* @return ユーザー固有のキャラクターデータディレクトリ */ public static File getDefaultCharactersDir() { Logger logger = Logger.getLogger(ConfigurationDirUtilities.class.getName()); File characterBaseDir = new File(ConfigurationDirUtilities.getUserDataDir(), "characters"); if (!characterBaseDir.exists()) { if (!characterBaseDir.mkdirs()) { logger.log(Level.WARNING, "can't create the charatcer base directory. " + characterBaseDir); } } return characterBaseDir; } } charactermanaj/src/charactermanaj/util/UserData.java0000644000175000017500000000256611767047543023042 0ustar paulliupaulliupackage charactermanaj.util; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; /** * ユーザーデータの保存先 * @author seraphy */ public interface UserData { /** * データを開く * @return 入力ストリーム * @throws IOException 開けなかった場合 */ InputStream openStream() throws IOException; /** * データを書き込む * @return 出力ストリーム * @throws IOException 開けなかった場合 */ OutputStream getOutputStream() throws IOException; /** * 更新日時(エポックタイム)、まだ存在しない場合は0 * @return 更新日時 */ long lastModified(); /** * 存在するか? * @return 存在すればtrue */ boolean exists(); /** * 削除する.
* すでに存在しない場合は何もしない.
* @return 削除された場合はtrue */ boolean delete(); /** * 引数に指定したオブジェクトをシリアライズします. * @param obj オブジェクト * @throws IOException 保存できなかった場合 */ void save(Object obj) throws IOException; /** * オブジェクトをデシリアライズして返します. * @return オブジェクト * @throws IOException 取得できなかった場合 */ Object load() throws IOException; } charactermanaj/src/charactermanaj/util/UIHelper.java0000644000175000017500000001351211767047543023000 0ustar paulliupaulliupackage charactermanaj.util; import java.awt.Component; import java.awt.Container; import java.awt.Graphics; import java.awt.image.BufferedImage; import java.io.IOException; import java.net.URL; import java.util.ArrayList; import java.util.Collection; import javax.imageio.ImageIO; import javax.swing.AbstractButton; import javax.swing.Icon; import javax.swing.ImageIcon; import javax.swing.JButton; import javax.swing.JMenu; public final class UIHelper extends ResourceLoader { private static final UIHelper singleton = new UIHelper(); private UIHelper() { super(); } public static final UIHelper getInstance() { return singleton; } /** * 指定したコンテナに含まれる指定したコンポーネント型のすべてのコンポーネントを返す.
* 一つも該当するものがなければ空を返す * @param 対象のコンポーネントのクラス型 * @param clz クラス * @param container 対象のコンテナ * @return コンポーネントのコレクション、もしくは空 */ @SuppressWarnings("unchecked") public Collection getDescendantOfClass(Class clz, Container container) { if (container == null || clz == null) { throw new IllegalArgumentException(); } Collection components = new ArrayList(); getDescendantOfClass(clz, container, components); return (Collection) components; } private void getDescendantOfClass(Class clz, Container container, Collection results) { if (container == null) { return; } Component[] children = (container instanceof JMenu) ? ((JMenu) container).getMenuComponents() : container.getComponents(); int mx = children.length; for (int idx = 0; idx < mx; idx++) { Component comp = children[idx]; if (clz.isInstance(comp)) { results.add(comp); } else if (comp instanceof Container) { getDescendantOfClass(clz, (Container) comp, results); } } } /** * 2つのステートをもつアイコンを作成します.
* このアイコンは、使用するコンポーネントがAbstractButton派生クラスであれば、isSelectedの結果が * trueである場合は2番目のアイコンイメージを表示します.
* isSelectedの結果がfalseであるか、もしくはAbstractButton派生クラスでなければ * 最初のアイコンイメージを表示します.
* @param iconName1 アイコン1 * @param iconName2 アイコン2 * @return アイコン */ public Icon createTwoStateIcon(String iconName1, String iconName2) { if (iconName1 == null || iconName2 == null || iconName1.length() == 0 || iconName2.length() == 0) { throw new IllegalArgumentException(); } final BufferedImage pinIcon1 = getImage(iconName1); final BufferedImage pinIcon2 = getImage(iconName2); Icon icon = new Icon() { public void paintIcon(Component c, Graphics g, int x, int y) { boolean selected = false; if (c instanceof AbstractButton) { AbstractButton btn = (AbstractButton) c; selected = btn.isSelected(); } BufferedImage iconImage; if ( !selected) { iconImage = pinIcon1; } else { iconImage = pinIcon2; } int w = iconImage.getWidth(); int h = iconImage.getHeight(); g.drawImage(iconImage, x, y, w, h, 0, 0, w, h, null); } public int getIconHeight() { return pinIcon1.getHeight(); } public int getIconWidth() { return pinIcon1.getWidth(); } }; return icon; } /** * アイコンボタン(非透過)を作成して返す.
* リソースが取得できない場合は実行時例外が返される.
* @param iconName 画像リソース名 * @return アイコンボタン */ public JButton createIconButton(String iconName) { if (iconName == null || iconName.length() == 0) { throw new IllegalArgumentException(); } JButton btn = new JButton(); btn.setIcon(new ImageIcon(getImage(iconName))); return btn; } /** * 通常時の画像のみをもつ透過ボタンを作成して返す.
* リソースが取得できない場合は実行時例外が返される.
* @param normal 通常時の画像リソース * @return 透過ボタン */ public JButton createTransparentButton(String normal) { return createTransparentButton(normal, null); } /** * リソースから通常とホバー時の画像をもつ透過ボタンを作成して返す.
* リソースが取得できない場合は実行時例外が返される.
* @param normal 通常時の画像リソース * @param rollover ホバー時の画像リソース * @return 透過ボタン */ public JButton createTransparentButton(String normal, String rollover) { if (normal == null || normal.length() == 0) { throw new IllegalArgumentException(); } ImageIcon normIcon = new ImageIcon(getImage(normal)); JButton btn = new JButton(normIcon); if (rollover != null && rollover.length() != 0) { ImageIcon rolloverIcon = new ImageIcon(getImage(rollover)); btn.setRolloverEnabled(true); btn.setRolloverIcon(rolloverIcon); btn.setPressedIcon(rolloverIcon); } btn.setOpaque(false); btn.setBorderPainted(false); btn.setContentAreaFilled(false); return btn; } /** * リソースから画像を取得する.
* 画像が取得できない場合は実行時例外を返す.
* @param name リソース * @return 画像 */ public BufferedImage getImage(String name) { URL url = getResource(name); if (url == null) { throw new RuntimeException("resource not found. " + name); } try { return ImageIO.read(url); } catch (IOException ex) { throw new RuntimeException("image load error." + ex.getMessage(), ex); } } } charactermanaj/src/charactermanaj/util/ErrorMessageHelper.java0000644000175000017500000000355611767047543025070 0ustar paulliupaulliupackage charactermanaj.util; import java.awt.Component; import java.awt.Dimension; import java.io.PrintWriter; import java.io.StringWriter; import java.util.logging.Level; import java.util.logging.Logger; import javax.swing.JOptionPane; import javax.swing.JScrollPane; import javax.swing.JTextArea; /** * 例外を表示するダイアログ.
* ログにも記録される.
* @author seraphy */ public final class ErrorMessageHelper { /** * ロガー */ private static final Logger logger = Logger.getLogger(ErrorMessageHelper.class.getName()); private ErrorMessageHelper() { super(); } /** * 例外が発生したことを示すダイアログを表示し、ログに記録する.
* @param parent ダイアログを表示する親、null可 * @param ex 例外、nullの場合はなにもせずに戻る. */ public static void showErrorDialog(Component parent, Throwable ex) { if (ex == null) { return; } // ログに記録する. logger.log(Level.SEVERE, ex.getLocalizedMessage(), ex); // 例外を表示するパネルの生成 JTextArea textArea = new JTextArea(); StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw); ex.printStackTrace(pw); // 例外のコールスタックをパネルに表示できるように出力 pw.close(); textArea.setText(sw.toString()); textArea.setSelectionStart(0); textArea.setSelectionEnd(0); textArea.setEditable(false); JScrollPane scr = new JScrollPane(textArea); scr.setPreferredSize(new Dimension(400, 150)); scr.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); scr.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); // ダイアログの表示 JOptionPane.showMessageDialog(parent, scr, "ERROR", JOptionPane.ERROR_MESSAGE); } } charactermanaj/src/charactermanaj/util/LocalizedMessageComboBoxRender.java0000644000175000017500000000361011767047543027325 0ustar paulliupaulliupackage charactermanaj.util; import java.awt.Component; import java.util.Properties; import javax.swing.JList; import javax.swing.plaf.basic.BasicComboBoxRenderer; /** * ローカライズリソースをサポートするオブジェクトをコンボボックスに表示するコンボボックスレンダー */ public class LocalizedMessageComboBoxRender extends BasicComboBoxRenderer { /** * シリアライズバージョンID */ private static final long serialVersionUID = 2148264299941543651L; /** * リソースホルダ */ private Properties strings; public LocalizedMessageComboBoxRender(Properties strings) { if (strings == null) { throw new IllegalArgumentException(); } this.strings = strings; } @Override public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) { Object localizedString = getLocalizedString(value); return super.getListCellRendererComponent(list, localizedString, index, isSelected, cellHasFocus); } /** * ローカライズリソースIDを取得する.
* サポートしていないか、該当がなければ、toString()の結果を返す.
* @param value オブジェクト * @return ローカライズされた文字列、もしくは通常の文字列 */ protected String getLocalizedString(Object value) { if (value == null) { return "(null)"; } if (value instanceof LocalizedMessageAware) { LocalizedMessageAware o = (LocalizedMessageAware) value; String id = o.getLocalizedResourceId(); String localizedString = null; if (id != null) { localizedString = strings.getProperty(id); if (localizedString == null) { return id; } } if (localizedString != null) { return localizedString; } } return value.toString(); } } charactermanaj/src/charactermanaj/util/DirectoryConfig.java0000644000175000017500000000116511767047543024416 0ustar paulliupaulliupackage charactermanaj.util; import java.io.File; public class DirectoryConfig { private static final DirectoryConfig inst = new DirectoryConfig(); private File charactersDir; private DirectoryConfig() { super(); } public File getCharactersDir() { if (charactersDir == null) { throw new IllegalStateException("キャラクターディレクトリが設定されていません."); } return charactersDir; } public void setCharactersDir(File charactersDir) { this.charactersDir = charactersDir; } public static DirectoryConfig getInstance() { return inst; } } charactermanaj/src/charactermanaj/util/UserDataFactory.java0000644000175000017500000003374411767047543024374 0ustar paulliupaulliupackage charactermanaj.util; import java.io.File; import java.io.IOException; import java.net.URI; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.UUID; import java.util.logging.Level; import java.util.logging.Logger; /** * ユーザーデータの保存先を生成するファクトリ * @author seraphy */ public class UserDataFactory { /** * ロガー */ private static final Logger logger = Logger.getLogger(UserDataFactory.class.getName()); /** * MANGLED管理ファイル名 */ private static final String META_FILE = "mangled_info.xml"; /** * シングルトン */ private static UserDataFactory inst = new UserDataFactory(); /** * MangledNameのキャッシュ */ private HashMap mangledNameMap = new HashMap(); /** * インスタンスを取得する. * @return インスタンス */ public static UserDataFactory getInstance() { return inst; } /** * プライベートコンストラクタ */ private UserDataFactory() { super(); } /** * 拡張子を含むファイル名を指定し、そのファイルが保存されるべきユーザディレクトリを判定して返す.
* nullまたは空の場合、もしくは拡張子がない場合はユーザディレクトリのルートを返します.
* フォルダがなければ作成されます.
* @param name ファイル名、もしくはnull * @return ファィルの拡張子に対応したデータ保存先フォルダ */ public File getSpecialDataDir(String name) { File userDataDir = ConfigurationDirUtilities.getUserDataDir(); if (name != null && name.length() > 0) { int pos = name.lastIndexOf('.'); if (pos >= 0) { String ext = name.substring(pos + 1); if (ext.length() > 0) { if ("ser".equals(ext)) { userDataDir = new File(userDataDir, "caches"); } else { userDataDir = new File(userDataDir, ext + "s"); } } } } // フォルダがなければ作成する. if (!userDataDir.exists()) { boolean result = userDataDir.mkdirs(); logger.log(Level.INFO, "makeDir: " + userDataDir + " /succeeded=" + result); } return userDataDir; } /** * 指定した名前のユーザーデータ保存先を作成する. * @param name ファイル名 * @return 保存先 */ public UserData getUserData(String name) { if (name == null || name.trim().length() == 0) { throw new IllegalArgumentException(); } return new FileUserData(new File(getSpecialDataDir(name), name)); } /** * docBaseごとにのハッシュ値を文字列表現化したプレフィックスをもつユーザーデータ保存先を作成する.
* docBaseのURIの圧縮を目的としており、等しいdocBaseは等しいプレフィックスによるようにしている.(暗号化が目的ではない).
* ハッシュ値はmd5の5バイトで生成されるため、既存のものと衝突した場合は末尾に数値が付与される.
* @param docBase URI、null可 * @param name ファイル名 * @return 保存先 */ public UserData getMangledNamedUserData(URI docBase, String name) { String mangledName = mangledNameMap.get(docBase); if (mangledName == null) { File storeDir = getSpecialDataDir(name); mangledName = registerMangledName(docBase, storeDir); mangledNameMap.put(docBase, mangledName); } return getUserData(mangledName + "-" + name); } /** * ハッシュ化文字列のデータベースファイルをオープンし、現在の登録をすべて読み込む.
* クローズされるまでデータベースはロックされた状態となる.
* @param storeDir データベースファイルの格納フォルダ * @return ハッシュ化文字列データベース * @throws IOException 失敗 */ protected FileMappedProperties openMetaFile(File storeDir) throws IOException { File metaFile = new File(storeDir, META_FILE); FileMappedProperties mangledProps = new FileMappedProperties(metaFile); // ロードされていなければロードする. // (ロードに失敗した場合は空の状態とみなして継続する.) try { mangledProps.load(); } catch (Exception ex) { logger.log(Level.WARNING, "mangled database is broken." + mangledProps.getFile(), ex); } return mangledProps; } private static final String URI_KEY_PREFIX = "uri."; private static final String MANGLED_NAME_PREFIX = "mangled."; /** * DocBaseのURIを辞書に登録し、そのハッシュ化文字列を返す.
* ハッシュの衝突を回避するための辞書ファイル「mangled_info.xml」を使用して、 * 衝突した場合は末尾に連番をふることで補正を行う.
* すでに登録済みの同じuriの場合は同じハッシュ化文字列を返す.
* @param docBase URI * @param mangledProps 辞書ファイル * @return ハッシュ化文字列(衝突した場合は連番が振られる) */ protected String registerMangledName(URI docBase, FileMappedProperties mangledProps) { final String noAdjustedMangledName = getNoAdjustedMangledName(docBase); String adjustedMangledName = noAdjustedMangledName; // ハッシュ化文字列に対応するURIのリストを取得する. // (通常は一個、まれにハッシュが衝突した場合に複数になる.) final String mangledLookupKey = "mangled_base." + noAdjustedMangledName; String sameMangledURIList = mangledProps.get(mangledLookupKey); List uris; if (sameMangledURIList != null && sameMangledURIList.length() > 0) { // ハッシュ化文字列に対するURIのリストは空白区切りであるとみなしてリストに変換する. uris = Arrays.asList(sameMangledURIList.split("\\s+")); } else { // 新規のハッシュ化文字列になる場合は空のリストとする. uris = Collections.emptyList(); sameMangledURIList = ""; } final String uri = docBase.toASCIIString(); final String registeredMangledName = mangledProps.get(URI_KEY_PREFIX + uri); if (!uris.contains(uri) || registeredMangledName == null || registeredMangledName.length() == 0) { // まだハッシュ化文字列に、そのURIが未登録の場合、 // もしくは、そのURIに対するハッシュ化文字列の索引がない場合 int pos = uris.indexOf(uri); if (pos < 0) { if (!uris.isEmpty()) { // まだ未登録である場合、同じUUIDであれば末尾に数値をつける. // (同一のUUIDがなければ末尾は付与しない) adjustedMangledName += "_" + uris.size(); } // 登録済みUUIDリストに追加する. if (sameMangledURIList.length() > 0) { sameMangledURIList += " "; // 空白区切り (URIの文字列表現では空白は含まれないため問題なし) } sameMangledURIList += uri; } else { // すでに登録済みであれば、再度、そのインデックスを使用する. if (pos > 0) { adjustedMangledName += "_" + pos; } } // 登録 mangledProps.put(MANGLED_NAME_PREFIX + adjustedMangledName, uri); // 補正後UUIDからURIへの索引 mangledProps.put(URI_KEY_PREFIX + uri, adjustedMangledName); // URIから補正語UUIDへの索引 mangledProps.put(mangledLookupKey, sameMangledURIList); // 補正前UUIDを使用するURIリスト } else { // 登録済みの場合 adjustedMangledName = registeredMangledName; } return adjustedMangledName; } /** * DocBaseのURIを辞書に登録し、そのハッシュ化文字列を返す.
* ハッシュの衝突を回避するための辞書ファイル「mangled_info.xml」を使用して、 * 衝突した場合は末尾に連番をふることで補正を行う.
* @param docBase URI * @param storeDir 格納先ディレクトリ(格納先単位で辞書ファイルが作成される) * @return ハッシュ化文字列(衝突した場合は連番が振られる) */ protected String registerMangledName(URI docBase, File storeDir) { final String noAdjustedMangledName = getNoAdjustedMangledName(docBase); String adjustedMangledName = noAdjustedMangledName; FileMappedProperties mangledProps = null; try { mangledProps = openMetaFile(storeDir); try { // 名前を登録する. adjustedMangledName = registerMangledName(docBase, mangledProps); // 追加・変更されていたら保存する. if (mangledProps.isModified()) { mangledProps.save(); } } finally { mangledProps.close(); } } catch (Exception ex) { logger.log( Level.WARNING, "mangled database is broken." + ((mangledProps == null) ? storeDir : mangledProps.getFile()), ex); } return adjustedMangledName; } /** * 登録されている、すべてのハッシュ化文字列に対するURIを取得する.
* @param mangledNames ハッシュ化文字列のコレクション * @param storeDir 格納先ディレクトリ(格納先単位で辞書ファイルが作成される) * @param ope ハッシュ化文字列に対応するURIが発見された場合のオペレーション */ public Map getMangledNameMap(File storeDir) { if (storeDir == null) { throw new IllegalArgumentException(); } HashMap uris = new HashMap(); FileMappedProperties mangledProps = null; try { mangledProps = openMetaFile(storeDir); try { for (Map.Entry propsEntry : mangledProps.entrySet()) { String key = propsEntry.getKey(); String value = propsEntry.getValue(); if (key.startsWith("mangled.")) { try { String mangledName = key.substring(8); URI uri = new URI(value); if (logger.isLoggable(Level.FINEST)) { logger.log(Level.FINEST, "registered mangled name: " + mangledName + "=" + uri); } uris.put(mangledName, uri); // 現在のすべての登録をキャッシュにも格納する. // (ただし変換できない場合は無視する) try { // URIはASCII変換されているため、一旦、ファイル位置に変換する. URI docBase = new File(uri).toURI(); mangledNameMap.put(docBase, mangledName); if (logger.isLoggable(Level.FINE)) { logger.log(Level.FINE, "mangled-name cached. " + docBase + "=" + mangledName); } } catch (Exception ex) { logger.log(Level.FINE, "mangled-name decode error." + uri, ex); } } catch (Exception ex) { logger.log(Level.WARNING, "mangled database is broken." + mangledProps.getFile(), ex); } } } } finally { mangledProps.close(); } } catch (IOException ex) { logger.log( Level.WARNING, "mangled database is broken." + ((mangledProps == null) ? storeDir : mangledProps.getFile()), ex); } catch (RuntimeException ex) { logger.log( Level.WARNING, "mangled database is broken." + ((mangledProps == null) ? storeDir : mangledProps.getFile()), ex); } return uris; } /** * URIのコレクションを指定して、それぞれの登録済みのハッシュ化文字列をマップとして返す.
* 登録フラグがfalseの場合、まだ登録されていないものはnullとなる.
* そうでない場合は新規に登録され、その値が設定される.
* @param uris URIのコレクション * @param storeDir 格納先ディレクトリ(格納先単位で辞書ファイルが作成される) * @param register 検索時に存在しなければ登録する場合はtrue * @return URIをキーとし、ハッシュ化文字列を値とするマップ。登録されていないURIはnullが値となる.
*/ public Map getMangledNameMap(Collection uris, File storeDir, boolean register) { if (storeDir == null) { throw new IllegalArgumentException(); } if (uris == null || uris.isEmpty()) { // nullまたは空の場合は空を返す. return Collections.emptyMap(); } HashMap results = new HashMap(); FileMappedProperties mangledProps = null; try { mangledProps = openMetaFile(storeDir); try { for (URI uri : uris) { if (uri == null) { continue; // 不正uri } String mangledName = mangledProps.get(uri.toASCIIString()); if (mangledName == null) { // 未登録の場合 if (register && uri != null) { // 登録する場合 mangledName = registerMangledName(uri, mangledProps); } } results.put(uri, mangledName); } // 登録する場合で変更があれば保存する. if (register && mangledProps.isModified()) { mangledProps.save(); } } finally { mangledProps.close(); } } catch (Exception ex) { logger.log( Level.WARNING, "mangled database is broken." + ((mangledProps == null) ? storeDir : mangledProps.getFile()), ex); } return results; } /** * docBaseをハッシュ値化文字列にした、補正前の文字列を返す.
* docBaseがnullの場合は空文字とみなして変換する.
* @param docBase URI、null可 * @return ハッシュ値の文字列表現 */ private String getNoAdjustedMangledName(URI docBase) { String docBaseStr; if (docBase == null) { docBaseStr = ""; } else { docBaseStr = docBase.toString(); } String mangledName = UUID.nameUUIDFromBytes(docBaseStr.getBytes()).toString(); if (logger.isLoggable(Level.FINEST)) { logger.log(Level.FINEST, "mangledName " + docBase + "=" + mangledName); } return mangledName; } } charactermanaj/src/charactermanaj/model/0000755000175000017500000000000011767047535020602 5ustar paulliupaulliucharactermanaj/src/charactermanaj/model/PartsColorInfo.java0000644000175000017500000001224011767047535024350 0ustar paulliupaulliupackage charactermanaj.model; import java.io.Serializable; import java.util.AbstractMap; import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; import charactermanaj.graphics.filters.ColorConvertParameter; /** * パーツ単位のカラーグループを含む色情報のコレクション.
* パーツは複数のレイヤーから構成されるため、レイヤーごとのカラーグループを含む色情報の集合を意味する.
* キーのセットはカテゴリに属するレイヤーに固定されており、追加・削除することはできない.
* @author seraphy */ public final class PartsColorInfo extends AbstractMap implements Serializable, Cloneable { /** * ロガー */ private static final Logger logger = Logger.getLogger(PartsColorInfo.class.getName()); private static final long serialVersionUID = -8639109147043912257L; /** * パーツが属するカテゴリのレイヤー構成に対する色情報のマップ.
*/ private HashMap colorInfoMap = new HashMap(); /** * カテゴリ */ private final PartsCategory partsCategory; /** * カテゴリを指定して色情報が未設定のインスタンスを構築する.
* カテゴリに属するレイヤーが初期化されている.
* @param partsCategory カテゴリ */ public PartsColorInfo(PartsCategory partsCategory) { if (partsCategory == null) { throw new IllegalArgumentException(); } this.partsCategory = partsCategory; init(); } @Override public PartsColorInfo clone() { try { PartsColorInfo inst = (PartsColorInfo) super.clone(); inst.colorInfoMap = new HashMap(); for (Map.Entry entry : colorInfoMap.entrySet()) { Layer layer = entry.getKey(); ColorInfo colorInfo = entry.getValue(); inst.colorInfoMap.put(layer, colorInfo.clone()); } return inst; } catch (CloneNotSupportedException ex) { throw new RuntimeException(ex); } } /** * パーツカラー情報を指定したパーツカテゴリに存在するレイヤーに正規化して返す.
* カテゴリに存在しないレイヤーの情報は破棄され、結果は有効なレイヤーのみの色情報となる.
* @param partsCategory パーツカテゴリ * @return 正規化されたパーツカラー情報 */ public PartsColorInfo createCompatible(PartsCategory partsCategory) { if (partsCategory == null) { throw new IllegalArgumentException(); } PartsColorInfo newInfo = new PartsColorInfo(partsCategory); newInfo.init(); for (Map.Entry entry : colorInfoMap.entrySet()) { Layer layer = entry.getKey(); ColorInfo colorInfo = entry.getValue(); if (partsCategory.hasLayer(layer)) { newInfo.put(layer, colorInfo.clone()); } else { logger.log(Level.INFO, "missing layer '" + layer + "' in " + partsCategory); } } return newInfo; } /** * 2つのパーツカラー情報が同じであるか判定する.
* 双方がnullである場合はtrueとなります.
* いずれか一方がnullである場合はfalseとなります.
* @param a 対象1、null可 * @param b 対象2, null可 * @return 同一であればtrue、そうでなければfalse */ public static boolean equals(PartsColorInfo a, PartsColorInfo b) { if (a == b) { return true; } if (a == null || b == null) { return false; } return a.equals(b); } private void init() { for (Layer layer : partsCategory.getLayers()) { colorInfoMap.put(layer, createColorInfo(layer)); } } protected ColorInfo createColorInfo(Layer layer) { ColorInfo colorInfo = new ColorInfo(); colorInfo.setColorGroup(layer.getColorGroup()); colorInfo.setSyncColorGroup(layer.getColorGroup().isEnabled()); colorInfo.setColorParameter(new ColorConvertParameter()); return colorInfo; } public PartsCategory getPartsCategory() { return partsCategory; } @Override public Set> entrySet() { return Collections.unmodifiableSet(colorInfoMap.entrySet()); } /** * カテゴリに属するレイヤーの色情報を設定する.
* カテゴリに該当しないレイヤーを指定した場合はIllegalArgumentException例外となる.
* @param key レイヤー * @param value 色情報 */ @Override public ColorInfo put(Layer key, ColorInfo value) { if (key == null || value == null) { throw new IllegalArgumentException(); } if (!colorInfoMap.containsKey(key)) { throw new IllegalArgumentException("invalid layer: " + key); } return colorInfoMap.put(key, value); } @Override public String toString() { StringBuilder buf = new StringBuilder(); buf.append(getClass().getSimpleName() + "@" + Integer.toHexString(System.identityHashCode(this))); buf.append("("); buf.append(colorInfoMap.toString()); buf.append(")"); return buf.toString(); } } charactermanaj/src/charactermanaj/model/PartsManageDataConverter.java0000644000175000017500000000425211767047535026334 0ustar paulliupaulliupackage charactermanaj.model; /** * パーツ設定情報から、パーツ管理情報に変換する.
* @author seraphy * */ public class PartsManageDataConverter { private PartsManageData partsManageData; /** * パーツ管理情報は自動作成される.
* @param partsSpecResolver */ public PartsManageDataConverter() { this(null); } /** * 書き込み先となるパーツ管理情報を指定して構築する.
* パーツ管理情報がnullの場合は自動作成される.
* @param partsManageData パーツ管理情報(書き込み先) */ public PartsManageDataConverter(PartsManageData partsManageData) { if (partsManageData == null) { this.partsManageData = new PartsManageData(); } else { this.partsManageData = partsManageData; } } /** * パーツ管理情報を取得する. * @return パーツ管理情報 */ public PartsManageData getPartsManageData() { return partsManageData; } /** * パーツ識別子とパーツ設定情報を指定して、パーツ管理情報に変換して登録する.
* @param partsIdentifier パーツ識別子 * @param partsSpec パーツ設定情報(null可) */ public void convert(PartsIdentifier partsIdentifier, PartsSpec partsSpec) { if (partsIdentifier == null) { throw new IllegalArgumentException(); } String localizedName = partsIdentifier.getLocalizedPartsName(); double version; String downloadURL; if (partsSpec != null) { version = partsSpec.getVersion(); downloadURL = partsSpec.getDownloadURL(); } else { version = 0; downloadURL = null; } PartsAuthorInfo partsAuthorInfo; if (partsSpec != null) { partsAuthorInfo = partsSpec.getAuthorInfo(); } else { partsAuthorInfo = null; } PartsManageData.PartsVersionInfo versionInfo = new PartsManageData.PartsVersionInfo(); versionInfo.setDownloadURL(downloadURL); versionInfo.setVersion(version); PartsManageData.PartsKey partsKey = new PartsManageData.PartsKey(partsIdentifier); partsManageData.putPartsInfo(partsKey, localizedName, partsAuthorInfo, versionInfo); } } charactermanaj/src/charactermanaj/model/PartsAuthorInfo.java0000644000175000017500000000110611767047535024533 0ustar paulliupaulliupackage charactermanaj.model; import java.io.Serializable; /** * パーツの作者情報 * @author seraphy */ public class PartsAuthorInfo implements Serializable { private static final long serialVersionUID = -5308326806956816225L; private String author; private String homePage; public String getAuthor() { return author; } public String getHomePage() { return homePage; } public void setAuthor(String author) { this.author = author; } public void setHomePage(String homePage) { this.homePage = homePage; } } charactermanaj/src/charactermanaj/model/Layer.java0000644000175000017500000001020111767047535022513 0ustar paulliupaulliupackage charactermanaj.model; import java.io.File; import java.io.Serializable; /** * レイヤー情報.
* 重ね合わせ順による比較が可能.
* immutableなクラスであり、構築された後に変更されることはない.
* 同値であるかは、ID、順序、Dirのみで判断され、それ以外の情報は無視される.
* @author seraphy */ public final class Layer implements Comparable, Serializable { private static final long serialVersionUID = -6437516046486547811L; /** * 重ね合わせ順 */ private final int order; /** * レイヤー識別名 */ private final String id; /** * レイヤー表示名 */ private final String localizedName; /** * カラーグループ */ private final ColorGroup colorGroup; /** * カラーグループ同期(初期) */ private final boolean initSync; /** * 対象ディレクトリ */ private final String dir; /** * レイヤー情報を構築する * @param id レイヤー識別名 * @param order 重ね合わせ順 * @param dir 対象ディレクトリ */ public Layer(String id, String localizedName, int order, ColorGroup colorGroup, boolean initSync, String dir) { if (id == null || id.length() == 0 || order < 0 || dir == null) { throw new IllegalArgumentException(); } if (localizedName == null || localizedName.length() == 0) { localizedName = id; } if (colorGroup == null) { colorGroup = ColorGroup.NA; } this.id = id; this.localizedName = localizedName; this.order = order; this.colorGroup = colorGroup; this.initSync = initSync; this.dir = dir; } /** * 重ね合わせ順に比較する. * 同順位の場合は名前・ディレクトリから順序を決める. */ public int compareTo(Layer o) { int ret = order - o.order; if (ret == 0) { ret = id.compareTo(o.id); } if (ret == 0) { File d1 = new File(dir); File d2 = new File(o.dir); ret = d1.compareTo(d2); } return ret; } /** * レイヤー識別名を取得する * @return レイヤー識別名 */ public String getId() { return id; } /** * レイヤー表示名を取得する * @return レイヤー表示名 */ public String getLocalizedName() { return localizedName; } /** * 重ね合わせ順を取得する. * @return 重ね合わせ順 */ public int getOrder() { return order; } /** * カラーグループを取得する. * @return カラーグループ */ public ColorGroup getColorGroup() { return colorGroup; } /** * カラーグループの同期フラグ(初期)を取得する. * @return 同期フラグ */ public boolean isInitSync() { return initSync; } /** * 対象ディレクトリを取得する. * @return 対象ディレクトリ */ public String getDir() { return dir; } /** * 同一レイヤーであるか判断する.
* ID、順序、Dirで判断する.
* (カラーグループ、カラーグループ同期、表示名、は無視される.)
*/ @Override public boolean equals(Object obj) { if (obj == this) { return true; } if (obj != null && obj instanceof Layer) { Layer o = (Layer) obj; File d1 = new File(dir); File d2 = new File(o.dir); return id.equals(o.id) && order == o.order && d1.equals(d2); } return false; } /** * 同一レイヤーであるか判断する.
* IDのみで判断する.
* (カラーグループ、カラーグループ同期、表示名、順序、Dirは無視される.)
* @param a 比較1 * @param b 比較2 * @return 等しければtrue、そうでなければfalse */ public static boolean equals(Layer a, Layer b) { if (a == b) { return true; } if (a == null || b == null) { return false; } return a.equals(b); } @Override public int hashCode() { return id.hashCode(); } @Override public String toString() { return "Layer(id=" + id + ", name=" + localizedName + ", order=" + order + ")"; } } charactermanaj/src/charactermanaj/model/PartsCategory.java0000644000175000017500000001354211767047535024241 0ustar paulliupaulliupackage charactermanaj.model; import java.io.Serializable; import java.util.Arrays; import java.util.Collections; import java.util.List; /** * パーツカテゴリ.
* 同値であるかはカテゴリIDが一致するかによってのみ判定します.
* それ以外の情報は無視されます.
* @author seraphy */ public final class PartsCategory implements Comparable, Serializable { /** * シリアライズバージョンID. */ private static final long serialVersionUID = -8652242530280056201L; /** * 順序 */ private final int order; /** * カテゴリ識別名 */ private final String categoryId; /** * カテゴリ表示名 */ private final String localizedCategoryName; /** * 複数選択可能? */ private final boolean multipleSelectable; /** * 表示行数 */ private final int visibleRows; /** * レイヤー情報 */ private final List layers; /** * カテゴリを構築する.
* @param order 順序 * @param categoryId カテゴリ識別名 * @param localizedCategoryName カテゴリ表示名 * @param multipleSelectable 複数選択可能? * @param visibleRows 表示行数 * @param layers レイヤー情報の配列、nullの場合は空とみなす */ public PartsCategory(final int order, final String categoryId, String localizedCategoryName, boolean multipleSelectable, int visibleRows, Layer[] layers) { if (categoryId == null || categoryId.trim().length() == 0) { throw new IllegalArgumentException(); } if (layers == null) { layers = new Layer[0]; } if (localizedCategoryName == null || localizedCategoryName.trim().length() == 0) { localizedCategoryName = categoryId; } this.order = order; this.categoryId = categoryId.trim(); this.localizedCategoryName = localizedCategoryName.trim(); this.multipleSelectable = multipleSelectable; this.layers = Collections.unmodifiableList(Arrays.asList(layers.clone())); this.visibleRows = visibleRows; } /** * カテゴリの順序を比較して返す.
* 順序で比較し、同一順序であれば表示名で比較し、それでも同一であれば識別子で比較します.
* @param o 比較対象. * @return 順序 */ public int compareTo(PartsCategory o) { if (o == this) { return 0; } int ret = order - o.order; if (ret == 0) { ret = localizedCategoryName.compareTo(o.localizedCategoryName); } if (ret == 0) { ret = categoryId.compareTo(o.categoryId); } return ret; } @Override public int hashCode() { return this.categoryId.hashCode(); } @Override public boolean equals(Object obj) { if (obj == this) { return true; } if (obj != null && obj instanceof PartsCategory) { // IDが等しいか? PartsCategory o = (PartsCategory) obj; if (categoryId.equals(o.getCategoryId())) { // それ以外の情報も等しいか? // (用法的に、異なるインスタンスで同じIDをもつことは希であり、 // その上、IDが同一で、それ以外の内容が一致しないことは更に希である。) if (order == o.order && localizedCategoryName .equals(o.localizedCategoryName) && multipleSelectable == o.multipleSelectable && visibleRows == o.visibleRows && layers.equals(o.layers)) { return true; } } } return false; } /** * 同一カテゴリであるか判定します.
* nullの場合は常にfalseを返します.
* @param obj パーツカテゴリ、またはnull * @return 同一のパーツカテゴリIDであればtrue、そうでなければfalse */ public boolean isSameCategoryID(PartsCategory obj) { if (obj == this) { return true; } if (obj != null) { return categoryId.equals(obj.categoryId); } return false; } public static boolean equals(PartsCategory o1, PartsCategory o2) { if (o1 == o2) { return true; } if (o1 == null || o2 == null) { return false; } return o1.equals(o2); } /** * 定義順を取得する * @return 定義順 */ public int getOrder() { return order; } /** * 複数選択可能であるか? * @return 複数選択可能であるか? */ public boolean isMultipleSelectable() { return multipleSelectable; } /** * 表示行数を取得する. * @return 表示行数 */ public int getVisibleRows() { return visibleRows; } /** * このカテゴリに指定したレイヤーが含まれるか検証する. * @param layer レイヤー * @return 含まれる場合はtrue、含まれない場合はfalse */ public boolean hasLayer(Layer layer) { if (layer == null) { return false; } for (Layer memberLayer : layers) { if (Layer.equals(memberLayer, layer)) { return true; } } return false; } /** * レイヤー情報 * @return レイヤー情報 */ public List getLayers() { return layers; } /** * レイヤーを取得する.
* 該当するレイヤーがなければnull * @param layerId レイヤー名 * @return レイヤーもしくはnull */ public Layer getLayer(String layerId) { if (layerId == null) { return null; } for (Layer layer : layers) { if (layer.getId().equals(layerId)) { return layer; } } return null; } /** * カテゴリ識別名を取得する. * @return カテゴリ識別名 */ public String getCategoryId() { return categoryId; } /** * カテゴリ表示名を取得する. * @return カテゴリ表示名 */ public String getLocalizedCategoryName() { return this.localizedCategoryName; } @Override public String toString() { return getLocalizedCategoryName(); } } charactermanaj/src/charactermanaj/model/PartsSpecResolver.java0000644000175000017500000000204511767047535025074 0ustar paulliupaulliupackage charactermanaj.model; import java.awt.Dimension; import java.util.Map; /** * パーツ設定を取得するためのインターフェイス * @author seraphy */ public interface PartsSpecResolver extends PartsCategoryResolver { /** * イメージのサイズを取得する * @return イメージサイズ、設定がなければnull */ Dimension getImageSize(); /** * 指定したパーツ識別子に対するパーツ設定を取得する.
* なければnull * @param partsIdentifier パーツ識別子 * @return パーツ設定、なければnull */ PartsSpec getPartsSpec(PartsIdentifier partsIdentifier); /** * 指定したカテゴリに該当するパーツ識別子とパーツ設定のマップを取得する.
* 該当するカテゴリがなければ空のマップを返す.
* @param category パーツカテゴリ * @return パーツ設定のマップ、なければ空 */ Map getPartsSpecMap(PartsCategory category); } charactermanaj/src/charactermanaj/model/PartsCategoryResolver.java0000644000175000017500000000101611767047535025754 0ustar paulliupaulliupackage charactermanaj.model; import java.util.List; public interface PartsCategoryResolver { /** * パーツカテゴリの一覧を取得する.
* @return パーツカテゴリの一覧。(表示順) */ List getPartsCategories(); /** * パーツカテゴリのIDを指定して該当のインスタンスを取得します.
* @param id パーツカテゴリID * @return インスタンス、なければnull */ PartsCategory getPartsCategory(String id); } charactermanaj/src/charactermanaj/model/AppConfig.java0000644000175000017500000006143611767047535023325 0ustar paulliupaulliupackage charactermanaj.model; import java.awt.Color; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.URI; import java.net.URL; import java.nio.charset.Charset; import java.util.Properties; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.Pattern; import charactermanaj.util.ApplicationLogHandler; import charactermanaj.util.BeanPropertiesUtilities; import charactermanaj.util.ConfigurationDirUtilities; /** * アプリケーションの全域にわたる設定.
* アプリケーション設定は、クラスパス上のリソース、コートベース直下のappConfig.xml、ユーザーごとのappConfig.xmlの順に読み込まれます.
* @author seraphy */ public final class AppConfig { /** * アプリケーション設定ファイルの名前 */ private static final String CONFIG_NAME = "appConfig.xml"; /** * 全ユーザー用キャラクターディレクトリのシステムプロパティのキー名.
*/ public static final String COMMON_CHARACTER_DIR_PROPERTY_NAME = "character.dir"; /** * 開発用仕様バージョン番号 */ private static final String DEFAULT_SPECIFICATION_VERSION = "1.0"; /** * ロガー */ private static final Logger logger = Logger.getLogger(AppConfig.class.getName()); /** * シングルトンインスタンス */ private static final AppConfig singleton = new AppConfig(); /** * インスタンスを取得する. * @return インスタンス */ public static AppConfig getInstance() { return singleton; } /** * プライベートコンストラクタ */ private AppConfig() { loadAppVersions(); } private String implementationVersion; private String specificationVersion; /** * 実装バージョンを取得する.
* ビルドされたjarパッケージからバージョン情報を取得する.
* クラスパスの実行からのバージョンは常に「develop」となる.
* @return 実装バージョン */ public String getImplementationVersion() { return implementationVersion; } /** * 仕様バージョンを取得する.
* ビルドされたjarパッケージからバージョン情報を取得する.
* クラスパスの実行からのバージョンは常に「develop」となる.
* @return 仕様バージョン */ public String getSpecificationVersion() { return specificationVersion; } /** * ビルドされたjarパッケージからバージョン情報を取得する.
* クラスパスの実行からのバージョンは常に「develop」となる.
*/ private void loadAppVersions() { Package pak = this.getClass().getPackage(); String implementationVersion = "develop"; String specificationVersion = DEFAULT_SPECIFICATION_VERSION; if (pak != null) { String vInfo = pak.getImplementationVersion(); if (vInfo != null && implementationVersion.trim().length() > 0) { implementationVersion = vInfo.trim(); } String specVInfo = pak.getSpecificationVersion(); if (specVInfo != null && specVInfo.trim().length() > 0) { specificationVersion = specVInfo.trim(); } } this.implementationVersion = implementationVersion; this.specificationVersion = specificationVersion; } /** * プロパティをロードする.
* リソース上の/appConfig.xml、コードベース下のappConfig.xml、アプリケーションデータ保存先のappConfig.xmlの順に読み取る.
* 存在しないか、読み取りに失敗した場合は、該当ファイルはスキップされる.
*/ public void loadConfig() { Properties config = new Properties(); try { File codeBase = ConfigurationDirUtilities.getApplicationBaseDir(); File userDataDir = ConfigurationDirUtilities.getUserDataDir(); URI[] uris = { new File(getClass().getResource("/" + CONFIG_NAME).getPath()).toURI(), new File(codeBase, CONFIG_NAME).getCanonicalFile().toURI(), new File(userDataDir, CONFIG_NAME).toURI(), }; for (URI uri : uris) { if (uri == null) { continue; // リソースがない場合はnullになる } // ファイルの実在チェック (チェックできる場合のみ) if ("file".equals(uri.getScheme())) { File file = new File(uri); if (!file.exists()) { logger.log(Level.CONFIG, "appConfig.xml is not found.:" + file); continue; } } // appConfig.xmlの読み込みを行う. // Properties#loadFromXML() はXMLからキーを読み取り、既存のキーに対して上書きする. // XMLに存在しないキーは読み込み前のままなので、繰り返し呼び出すことで「重ね合わせ」することができる. try { URL resourceURL = uri.toURL(); InputStream is = resourceURL.openStream(); try { config.loadFromXML(is); logger.log(Level.CONFIG, "appConfig.xml is loaded.:" + uri); } finally { is.close(); } } catch (FileNotFoundException ex) { logger.log(Level.CONFIG, "appConfig.xml is not found.: " + uri, ex); // 無視する (無い場合は十分にありえるので「情報」レベルでログ。) } catch (Exception ex) { logger.log(Level.WARNING, "appConfig.xml loading failed.: " + uri, ex); // 無視する } } } catch (IOException ex) { throw new RuntimeException("appConfig.xml loading failed.", ex); } catch (RuntimeException ex) { throw new RuntimeException("appConfig.xml loading failed.", ex); } BeanPropertiesUtilities.loadFromProperties(this, config); } /** * プロパティをアプリケーションデータの保存先に保存する. * @throws IOException 保存に失敗した場合 */ public void saveConfig() throws IOException { Properties config = getProperties(); File userDataDir = ConfigurationDirUtilities.getUserDataDir(); File configStore = new File(userDataDir, CONFIG_NAME); OutputStream os = new BufferedOutputStream(new FileOutputStream(configStore)); try { config.storeToXML(os, CONFIG_NAME, "UTF-8"); } finally { os.close(); } } /** * Propertiesの値を設定した場合に設定できない項目があるかチェックする.
* このメソッドを呼び出しても、アプリケーション設定自身は何も影響されない.
* @param props 適用するプロパティ * @return 設定できなかったプロパティキーのコレクション、問題なければ空が返される. */ public static Set checkProperties(Properties props) { if (props == null) { throw new IllegalArgumentException(); } AppConfig dummy = new AppConfig(); // アプリケーションから参照されないダミーのインスタンスを作成する. return BeanPropertiesUtilities.loadFromProperties(dummy, props); } /** * Propertiesの値で設定を更新する.
* @param props 適用するプロパティ * @return 設定できなかったプロパティキーのコレクション、問題なければ空が返される. */ public Set update(Properties props) { if (props == null) { throw new IllegalArgumentException(); } return BeanPropertiesUtilities.loadFromProperties(this, props); } /** * このアプリケーション設定をプロパティに書き出して返します.
* @return プロパティ */ public Properties getProperties() { Properties config = new Properties(); BeanPropertiesUtilities.saveToProperties(this, config); return config; } /** * プロファイル選択ダイアログのプロファイルのサンプルイメージの背景色 * @return サンプルイメージの背景色 */ public Color getSampleImageBgColor() { return sampleImageBgColor; } public void setSampleImageBgColor(Color sampleImageBgColor) { if (sampleImageBgColor == null) { throw new IllegalArgumentException(); } this.sampleImageBgColor = sampleImageBgColor; } private Color sampleImageBgColor = Color.white; /** * デフォルトのイメージ背景色を取得する. * @return デフォルトのイメージ背景色 */ public Color getDefaultImageBgColor() { return defaultImageBgColor; } public void setDefaultImageBgColor(Color defaultImageBgColor) { if (defaultImageBgColor == null) { throw new IllegalArgumentException(); } this.defaultImageBgColor = defaultImageBgColor; } private Color defaultImageBgColor = Color.white; /** * 使用中アイテムの背景色を取得する. * @return 使用中アイテムの背景色 */ public Color getCheckedItemBgColor() { return checkedItemBgColor; } public void setCheckedItemBgColor(Color checkedItemBgColor) { if (checkedItemBgColor == null) { throw new IllegalArgumentException(); } this.checkedItemBgColor = checkedItemBgColor; } private Color checkedItemBgColor = Color.cyan.brighter(); /** * 選択アイテムの背景色を取得する * @return 選択アイテムの背景色 */ public Color getSelectedItemBgColor() { return selectedItemBgColor; } public void setSelectedItemBgColor(Color selectedItemBgColor) { this.selectedItemBgColor = selectedItemBgColor; } private Color selectedItemBgColor = Color.orange; /** * 不備のあるデータ行の背景色を取得する. * @return 不備のあるデータ行の背景色 */ public Color getInvalidBgColor() { return invalidBgColor; } public void setInvalidBgColor(Color invalidBgColor) { if (invalidBgColor == null) { throw new IllegalArgumentException(); } this.invalidBgColor = invalidBgColor; } private Color invalidBgColor = Color.red.brighter().brighter(); /** * JPEG画像変換時の圧縮率を取得する. * @return 圧縮率 */ public float getCompressionQuality() { return compressionQuality; } public void setCompressionQuality(float compressionQuality) { if (compressionQuality < .1f || compressionQuality > 1f) { throw new IllegalArgumentException(); } this.compressionQuality = compressionQuality; } private float compressionQuality = .8f; /** * エクスポートウィザードのプリセットにパーツ不足時の警告色(前景色)を取得する. * @return エクスポートウィザードのプリセットにパーツ不足時の警告色(前景色) */ public Color getExportPresetWarningsForegroundColor() { return exportPresetWarningsForegroundColor; } public void setExportPresetWarningsForegroundColor( Color exportPresetWarningsForegroundColor) { this.exportPresetWarningsForegroundColor = exportPresetWarningsForegroundColor; } private Color exportPresetWarningsForegroundColor = Color.red; /** * JARファイル転送用バッファサイズ.
* @return JARファイル転送用バッファサイズ. */ public int getJarTransferBufferSize() { return jarTransferBufferSize; } public void setJarTransferBufferSize(int jarTransferBufferSize) { if (jarTransferBufferSize <= 0) { throw new IllegalArgumentException(); } this.jarTransferBufferSize = jarTransferBufferSize; } private int jarTransferBufferSize = 4096; /** * ZIPファイル名のエンコーディング.
* @return ZIPファイル名のエンコーディング.
*/ public String getZipNameEncoding() { return zipNameEncoding; } public void setZipNameEncoding(String zipNameEncoding) { if (zipNameEncoding == null) { throw new IllegalArgumentException(); } try { Charset.forName(zipNameEncoding); } catch (Exception ex) { throw new RuntimeException("unsupported charset: " + zipNameEncoding); } this.zipNameEncoding = zipNameEncoding; } private String zipNameEncoding = "csWindows31J"; /** * ディセーブルなテーブルのセルのフォアグラウンドカラーを取得する. * @return ディセーブルなテーブルのセルのフォアグラウンドカラー */ public Color getDisabledCellForgroundColor() { return disabledCellForegroundColor; } public void setDisabledCellForegroundColor(Color disabledCellForegroundColor) { if (disabledCellForegroundColor == null) { throw new IllegalArgumentException(); } this.disabledCellForegroundColor = disabledCellForegroundColor; } private Color disabledCellForegroundColor = Color.gray; /** * ディレクトリを監視する間隔(mSec)を取得する. * @return ディレクトリを監視する間隔(mSec) */ public int getDirWatchInterval() { return dirWatchInterval; } public void setDirWatchInterval(int dirWatchInterval) { if (dirWatchInterval <= 0) { throw new IllegalArgumentException(); } this.dirWatchInterval = dirWatchInterval; } private int dirWatchInterval = 7 * 1000; /** * ディレクトリの監視を有効にするか? * @return ディレクトリの監視を有効にする場合はtrue */ public boolean isEnableDirWatch() { return enableDirWatch; } public void setEnableDirWatch(boolean enableDirWatch) { this.enableDirWatch = enableDirWatch; } private boolean enableDirWatch = true; /** * ファイル転送に使うバッファサイズ.
* @return バッファサイズ */ public int getFileTransferBufferSize() { return fileTransferBufferSize; } public void setFileTransferBufferSize(int fileTransferBufferSize) { if (fileTransferBufferSize <= 0) { throw new IllegalArgumentException(); } this.fileTransferBufferSize = fileTransferBufferSize; } private int fileTransferBufferSize = 4096; /** * プレビューのインジケータを表示するまでのディレイ(mSec)を取得する. * @return プレビューのインジケータを表示するまでのディレイ(mSec) */ public long getPreviewIndicatorDelay() { return previewIndeicatorDelay; } public void setPreviewIndeicatorDelay(long previewIndeicatorDelay) { if (previewIndeicatorDelay < 0) { throw new IllegalArgumentException(); } this.previewIndeicatorDelay = previewIndeicatorDelay; } private long previewIndeicatorDelay = 300; /** * 情報ダイアログの編集ボタンを「開く」アクションにする場合はtrue、「編集」アクションにする場合はfalse * @return trueならばOpen、falseならばEdit */ public boolean isInformationDialogOpenMethod() { return informationDialogOpenMethod; } public void setInformationDialogOpenMethod( boolean informationDialogOpenMethod) { this.informationDialogOpenMethod = informationDialogOpenMethod; } private boolean informationDialogOpenMethod = true; /** * ログを常に残すか?
* falseの場合は{@link ApplicationLogHandler}の実装に従って終了時に * 必要なければログは削除される.
* @return 常に残す場合はtrue、そうでなければfalse */ public boolean isNoRemoveLog() { return noRemoveLog; } public void setNoRemoveLog(boolean noRemoveLog) { this.noRemoveLog = noRemoveLog; } private boolean noRemoveLog = false; /** * テーブルのグリッド色.
* @return テーブルのグリッド色 */ public Color getGridColor() { return gridColor; } public void setGridColor(Color gridColor) { if (gridColor == null) { throw new IllegalArgumentException(); } this.gridColor = gridColor; } private Color gridColor = Color.gray; /** * カラーダイアログの値が変更されたら、自動的にプレビューを更新するか? * @return カラーダイアログの値が変更されたら、自動的にプレビューを更新する場合はtrue (デフォルトはtrue) */ public boolean isEnableAutoColorChange() { return enableAutoColorChange; } public void setEnableAutoColorChange(boolean enableAutoColorChange) { this.enableAutoColorChange = enableAutoColorChange; } private boolean enableAutoColorChange = true; public void setAuthorEditConflictBgColor(Color authorEditConflictBgColor) { if (authorEditConflictBgColor == null) { throw new IllegalArgumentException(); } this.authorEditConflictBgColor = authorEditConflictBgColor; } /** * パーツの作者編集時に複数作者を選択した場合のに入力ボックスの背景色 * @return 背景色 */ public Color getAuthorEditConflictBgColor() { return authorEditConflictBgColor; } Color authorEditConflictBgColor = Color.yellow; public void setMainFrameMaxWidth(int width) { this.mainFrameMaxWidth = width; } /** * メインフレームの初期表示時の最大幅 * @return メインフレームの初期表示時の最大幅 */ public int getMainFrameMaxWidth() { return mainFrameMaxWidth; } private int mainFrameMaxWidth = 800; public void setMainFrameMaxHeight(int height) { this.mainFrameMaxHeight = height; } /** * メインフレームの初期表示時の最大高さ * @return メインフレームの初期表示時の最大高さ */ public int getMainFrameMaxHeight() { return mainFrameMaxHeight; } private int mainFrameMaxHeight = 600; /** * カラーダイアログで存在しないレイヤーをディセーブルにしない. * @return ディセーブルにしない場合はtrue */ public boolean isNotDisableLayerTab() { return notDisableLayerTab; } public void setNotDisableLayerTab(boolean notDisableLayerTab) { this.notDisableLayerTab = notDisableLayerTab; } private boolean notDisableLayerTab; /** * ログを消去する日数.
* この指定日を経過した古いログは削除される.
* 0の場合は削除されない. * @return */ public long getPurgeLogDays() { return purgeLogDays; } public void setPurgeLogDays(long purgeLogDays) { this.purgeLogDays = purgeLogDays; } private long purgeLogDays = 10; public String getPartsColorGroupPattern() { return partsColorGroupPattern; } public void setPartsColorGroupPattern(String pattern) { if (pattern != null && pattern.trim().length() > 0) { Pattern.compile(pattern); } partsColorGroupPattern = pattern; } private String partsColorGroupPattern = "^.*\\(@\\).*$"; private Color selectPanelTitleColor = Color.BLUE; public Color getSelectPanelTitleColor() { return selectPanelTitleColor; } public void setSelectPanelTitleColor(Color color) { if (color == null) { throw new IllegalArgumentException(); } selectPanelTitleColor = color; } private boolean enableAutoShrinkPanel; public boolean isEnableAutoShrinkPanel() { return enableAutoShrinkPanel; } public void setEnableAutoShrinkPanel(boolean enableAutoShrinkPanel) { this.enableAutoShrinkPanel = enableAutoShrinkPanel; } public boolean isDisableWatchDirIfNotWritable() { return disableWatchDirIfNotWritable; } public void setDisableWatchDirIfNotWritable(boolean disableWatchDirIfNotWritable) { this.disableWatchDirIfNotWritable = disableWatchDirIfNotWritable; } private boolean disableWatchDirIfNotWritable = true; public void setEnablePNGSupportForWindows(boolean enablePNGSupportForWindows) { this.enablePNGSupportForWindows = enablePNGSupportForWindows; } public boolean isEnablePNGSupportForWindows() { return enablePNGSupportForWindows; } private boolean enablePNGSupportForWindows = true; /** * 画像表示(通常モード)でオプティマイズを有効にする最大倍率. */ private double renderingOptimizeThresholdForNormal = 2.; public void setRenderingOptimizeThresholdForNormal( double renderingOptimizeThresholdForNormal) { this.renderingOptimizeThresholdForNormal = renderingOptimizeThresholdForNormal; } public double getRenderingOptimizeThresholdForNormal() { return renderingOptimizeThresholdForNormal; } /** * 画像表示(チェックモード)でオプティマイズを有効にする最大倍率. */ private double renderingOptimizeThresholdForCheck = 0.; public void setRenderingOptimizeThresholdForCheck( double renderingOptimizeThresholdForCheck) { this.renderingOptimizeThresholdForCheck = renderingOptimizeThresholdForCheck; } public double getRenderingOptimizeThresholdForCheck() { return renderingOptimizeThresholdForCheck; } /** * バイキュービックをサポートする場合 */ private boolean enableInterpolationBicubic = true; public void setEnableInterpolationBicubic(boolean enableInterpolationBicubic) { this.enableInterpolationBicubic = enableInterpolationBicubic; } public boolean isEnableInterpolationBicubic() { return enableInterpolationBicubic; } /** * 事前定義済みの倍率候補.
*/ private String predefinedZoomRanges = "20, 50, 80, 100, 120, 150, 200, 300, 400, 800"; public String getPredefinedZoomRanges() { return predefinedZoomRanges; } public void setPredefinedZoomRanges(String predefinedZoomRanges) { this.predefinedZoomRanges = predefinedZoomRanges; } /** * ズームパネルを初期状態で表示するか? */ private boolean enableZoomPanel = true; public boolean isEnableZoomPanel() { return enableZoomPanel; } public void setEnableZoomPanel(boolean enableZoomPanel) { this.enableZoomPanel = enableZoomPanel; } /** * ズームパネルをアクティブにする下部範囲 */ private int zoomPanelActivationArea = 30; public int getZoomPanelActivationArea() { return zoomPanelActivationArea; } public void setZoomPanelActivationArea(int zoomPanelActivationArea) { this.zoomPanelActivationArea = zoomPanelActivationArea; } /** * レンダリングヒントを使用するか? */ private boolean enableRenderingHints = true; public void setEnableRenderingHints(boolean enableRenderingHints) { this.enableRenderingHints = enableRenderingHints; } public boolean isEnableRenderingHints() { return enableRenderingHints; } /** * グリッド描画とマスク */ private int drawGridMask = 2; public int getDrawGridMask() { return drawGridMask; } public void setDrawGridMask(int drawGridMask) { this.drawGridMask = drawGridMask & 0x03; } private int previewGridColor = 0x7f7f0000; public int getPreviewGridColor() { return previewGridColor; } public void setPreviewGridColor(int previewGridColor) { this.previewGridColor = previewGridColor; } private int previewGridSize = 20; public int getPreviewGridSize() { return previewGridSize; } public void setPreviewGridSize(int previewGridSize) { this.previewGridSize = previewGridSize; } /** * チェックモード時の余白サイズ(片側) */ private int previewUnfilledSpaceForCheckMode = 0; public int getPreviewUnfilledSpaceForCheckMode() { return previewUnfilledSpaceForCheckMode; } public void setPreviewUnfilledSpaceForCheckMode( int previewUnfilledSpaceForCheckMode) { this.previewUnfilledSpaceForCheckMode = previewUnfilledSpaceForCheckMode; } /** * チェックモードでツールチップを表示するか? */ private boolean enableCheckInfoTooltip = true; public boolean isEnableCheckInfoTooltip() { return enableCheckInfoTooltip; } public void setEnableCheckInfoTooltip(boolean enableCheckInfoTooltip) { this.enableCheckInfoTooltip = enableCheckInfoTooltip; } /** * ホイールによるスクロールの単位.
*/ private int wheelScrollUnit = 10; public int getWheelScrollUnit() { return wheelScrollUnit; } public void setWheelScrollUnit(int wheelScrollUnit) { this.wheelScrollUnit = wheelScrollUnit; } /** * 壁紙にオフスクリーン描画を使用するか?.
* (あまり劇的なパフォーマンス効果はない.) */ private boolean enableOffscreenWallpaper = false; public boolean isEnableOffscreenWallpaper() { return enableOffscreenWallpaper; } public void setEnableOffscreenWallpaper(boolean enableOffscreenWallpaper) { this.enableOffscreenWallpaper = enableOffscreenWallpaper; } /** * 壁紙のオフスクリーンの既定サイズ. */ private int offscreenWallpaperSize = 300; public int getOffscreenWallpaperSize() { return offscreenWallpaperSize; } public void setOffscreenWallpaperSize(int offscreenWallpaperSize) { this.offscreenWallpaperSize = offscreenWallpaperSize; } } charactermanaj/src/charactermanaj/model/PartsSet.java0000644000175000017500000004063411767047535023221 0ustar paulliupaulliupackage charactermanaj.model; import java.awt.Color; import java.io.Serializable; import java.util.AbstractMap; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; /** * パーツセット.
* 各カテゴリの選択パーツと、そのパーツの色情報、および背景色をセットにしたもの.
* 保存する必要がなければIDおよび表示名は使用されないため、nullとなりえる.
* @author seraphy * */ public final class PartsSet extends AbstractMap> implements Serializable, Cloneable { /** * シリアライズバージョンID. */ private static final long serialVersionUID = 5972528889825451761L; /** * パーツセットID.
* 一時的な名前なしのパーツセットとして使われる場合はnull */ private String partsSetId; /** * パーツセットの表示名.
* 一時的な名前なしのパーツセットとして使われる場合はnull */ private String localizedName; /** * プリセットパーツフラグ */ private boolean presetParts; /** * パーツセットとともに使われる背景色.
*/ private Color bgColor; /** * アフィン変換用パラメータ.
* 変換しない場合はnull.
*/ private double[] affineTransformParameter; /** * パーツリスト */ private HashMap> parts = new HashMap>(); /** * パーツに対するカラー情報.
* かならずしも、パーツに対してカラー情報を設定する必要はない.
* カラー情報がない場合は空.
*/ private HashMap partsColorInfoMap = new HashMap(); /** * 無名、空のパーツセットを作成する. */ public PartsSet() { this(null, null, false); } /** * 名前つきパーツセットを作成する.
* @param partsSetId パーツセットID * @param localizedName 表示名 * @param presetParts プリセットフラグ */ public PartsSet(String partsSetId, String localizedName, boolean presetParts) { this.partsSetId = partsSetId; this.localizedName = localizedName; this.presetParts = presetParts; } /** * パーツセットをディープコピーする.
* 現在のキャラクターデータのカテゴリインスタンスに関連づけて再生させる場合は、 * resolverに現在のキャラクターデータのカテゴリリゾルバを指定します.
* @param org 元オブジェクト * @param resolver パーツセットのカテゴリを再生するためのリゾルバ、再生する必要がなければnull可 */ protected PartsSet(PartsSet org, PartsCategoryResolver resolver) { if (org == null) { throw new IllegalArgumentException(); } this.partsSetId = org.partsSetId; this.localizedName = org.localizedName; this.presetParts = org.presetParts; this.bgColor = org.bgColor; this.affineTransformParameter = org.affineTransformParameter == null ? null : org.affineTransformParameter.clone(); // ColorInfoMapの正規化 for (Map.Entry partsColorInfoEntry : org.partsColorInfoMap.entrySet()) { PartsIdentifier partsIdentifier = partsColorInfoEntry.getKey(); if (resolver != null) { PartsCategory orgPartsCategory = partsIdentifier.getPartsCategory(); PartsCategory repPartsCategory = resolver.getPartsCategory(orgPartsCategory.getCategoryId()); if (repPartsCategory == null) { // 同一IDのカテゴリがリゾルバになければ、このパーツは無かったことにする. continue; } if (orgPartsCategory != repPartsCategory) { // インスタンスが一致しなければリゾルバ側の結果を優先する. partsIdentifier = new PartsIdentifier( repPartsCategory, partsIdentifier.getPartsName(), partsIdentifier.getLocalizedPartsName()); } } PartsCategory repPartsCategory = partsIdentifier.getPartsCategory(); PartsColorInfo copiedPartsColorInfo = partsColorInfoEntry .getValue().createCompatible(repPartsCategory); partsColorInfoMap.put(partsIdentifier, copiedPartsColorInfo); } // PartsIdentifierの正規化 for (Map.Entry> partsEntry : org.parts.entrySet()) { PartsCategory orgPartsCategory = partsEntry.getKey(); PartsCategory partsCategory = orgPartsCategory; if (resolver != null) { PartsCategory repPartsCategory = resolver.getPartsCategory(orgPartsCategory.getCategoryId()); if (repPartsCategory == null) { // 同一IDのカテゴリがリゾルバになければ、このカテゴリはなかったことにする. continue; } if (repPartsCategory != orgPartsCategory) { // インスタンスが一致しなければリゾルバ側の結果を優先する. partsCategory = repPartsCategory; } } ArrayList partsIdentifiers = new ArrayList(partsEntry.getValue()); parts.put(partsCategory, partsIdentifiers); } } /** * リゾルバを使用してパーツカテゴリ(パーツ識別子のカテゴリも含む)のインスタンスを入れ替えます.
* パーツセットはキャラクターデータとは独立してロード・セーブされることがあるため、同じ情報でも異なるインスタンスとなる場合があり、 * これを是正するために、このメソッドを使用します.
* リゾルバから取得できないパーツカテゴリは除去されます.
* @param resolver リゾルバ * @return 互換性のあるパーツセット */ public PartsSet createCompatible(PartsCategoryResolver resolver) { if (resolver == null) { throw new IllegalArgumentException(); } // XXX: 本当は、こんなことはしたくない。 return new PartsSet(this, resolver); } /** * パーツセットをディープコピーする.
* @return コピーされたパーツセット */ @Override public PartsSet clone() { return new PartsSet(this, null); } @Override public int hashCode() { return super.hashCode() ^ (partsSetId == null ? 0 : partsSetId.hashCode()); } @Override public boolean equals(Object o) { if (o == this) { return true; } if (o != null && o instanceof PartsSet) { PartsSet obj = (PartsSet) o; // 双方のIDがnullもしくは、同一インスタンスであるか、ID文字列が等値である場合 if (partsSetId == obj.partsSetId || (partsSetId != null && partsSetId.equals(obj.partsSetId))) { // AbstractMapのequalsでパーツの構成物を比較する. if (super.equals(obj)) { // カラー定義が等しいか比較する. if (partsColorInfoMap.equals(obj.partsColorInfoMap)) { // 背景色がともにnullもしくは同一インスタンスであるか、背景色が等値である場合 if (bgColor == obj.bgColor || (bgColor != null && bgColor.equals(obj.bgColor))) { // アフィン変換パラメータがともにnullもしくは同一インスタンスであるか、同値である場合. if (affineTransformParameter == obj.affineTransformParameter || (affineTransformParameter != null && Arrays.equals(affineTransformParameter, obj.affineTransformParameter))) { return true; } } } } } } return false; } public void setPresetParts(boolean presetParts) { this.presetParts = presetParts; } /** * プリセット用パーツセットであるか? * これがfalseの場合は一時的なパーツセットか、もしくはお気に入り用である.
* @return プリセット用パーツセットである場合 */ public boolean isPresetParts() { return presetParts; } public void setBgColor(Color bgColor) { this.bgColor = bgColor; } /** * バックグラウンドカラーを取得する.
* 設定されていなければnull.
* @return バックグラウンドカラー、もしくはnull */ public Color getBgColor() { return bgColor; } /** * アフィン変換用パラメータを指定する.
* 配列は4または6でなければならない.
* アフィン変換しない場合はnull * @param affineTransformParameter 変換パラメータ(4または6個の要素)、もしくはnull */ public void setAffineTransformParameter(double[] affineTransformParameter) { if (affineTransformParameter != null && !(affineTransformParameter.length == 4 || affineTransformParameter.length == 6)) { throw new IllegalArgumentException("affineTransformParameter invalid length."); } this.affineTransformParameter = affineTransformParameter == null ? null : affineTransformParameter.clone(); } /** * アフィン変換用のパラメータを取得する.
* 変換しない場合はnull.
* @return アフィン変換用のパラメータ、またはnull */ public double[] getAffineTransformParameter() { return affineTransformParameter == null ? null : affineTransformParameter.clone(); } public void setPartsSetId(String partsSetId) { this.partsSetId = partsSetId; } public void setLocalizedName(String localizedName) { this.localizedName = localizedName; } /** * プリセットIDを取得する.
* 一時的なパーツセットである場合はnull * @return プリセットID、またはnull */ public String getPartsSetId() { return partsSetId; } /** * プリセット名を取得する.
* 一時的なパーツセットである場合はnull * @return プリセット名、またはnull */ public String getLocalizedName() { return localizedName; } /** * パーツセットのエントリセットを取得する. */ @Override public Set>> entrySet() { return parts.entrySet(); } /** * パーツカラー情報を取得する.
* カラー情報が関連づけられていない場合はnullが返される.
* @param partsIdentifier パーツ識別子 * @return カラー情報、もしくはnull */ public PartsColorInfo getColorInfo(PartsIdentifier partsIdentifier) { PartsColorInfo partsColorInfo = partsColorInfoMap.get(partsIdentifier); return partsColorInfo == null ? null : partsColorInfo.clone(); } /** * カテゴリ別にパーツを登録する.
* partsNameがnullまたは空文字の場合はカテゴリのみ登録する.
* これにより、そのカテゴリに選択がないことを示す.
* 複数選択可能なカテゴリの場合、複数回の呼び出しで登録する.(登録順)
* @param category カテゴリ * @param partsIdentifier パーツ識別子、またはnull * @param partsColorInfo パーツの色情報、なければnull可 */ public void appendParts(PartsCategory category, PartsIdentifier partsIdentifier, PartsColorInfo partsColorInfo) { if (category == null) { throw new IllegalArgumentException(); } List partsIdentifiers = parts.get(category); if (partsIdentifiers == null) { partsIdentifiers = new ArrayList(); parts.put(category, partsIdentifiers); } if (partsIdentifier != null) { partsIdentifiers.add(partsIdentifier); if (partsColorInfo != null) { partsColorInfoMap.put(partsIdentifier, partsColorInfo.clone()); } } } /** * すべてのパーツのカラー情報を除去する.
*/ public void removeColorInfo() { partsColorInfoMap.clear(); } /** * パーツセットが構造的に一致するか検証します.
* nullの場合は常にfalseとなります.
* @param other 比較対象、null可 * @return パーツ構成が一致すればtrue、そうでなければfalse */ public boolean isSameStructure(PartsSet other) { if (other != null && other.size() == this.size()) { // カテゴリが一致し、各カテゴリに登録されているパーツ識別子のリストも順序を含めて一致する場合、 // 構造的に一致すると判定する. for (Map.Entry> entry : entrySet()) { PartsCategory category = entry.getKey(); List ownList = entry.getValue(); List otherList = other.get(category); if (ownList == null || otherList == null || !ownList.equals(otherList)) { return false; } } return true; } return false; } /** * 2つのパーツセットが構造的に一致するか検証します.
* いずれか一方がnullであればfalseを返します.双方がnullであればtrueを返します.
* 双方がnullでなければ{@link #isSameStructure(PartsSet)}で判定されます.
* @param a 比較対象1、null可 * @param b 比較対象2、null可 * @return パーツ構成が一致すればtrue、そうでなければfalse */ public static boolean isSameStructure(PartsSet a, PartsSet b) { if (a == b) { return true; } if (a == null || b == null) { return false; } return a.isSameStructure(b); } /** * 保持しているパーツ識別子のカラー情報と同一のカラー情報をもっているか判定します.
* 相手側はカテゴリや順序を問わず、少なくとも自分と同じパーツ識別子をもっていれば足りるため、 * パーツ構成が同一であるかの判定は行いません.
* パーツ構造を含めて判定を行う場合は事前に{@link #isSameStructure(PartsSet)}を呼び出します.
* nullの場合は常にfalseとなります.
* @param other 判定先、null可 * @return 同一であればtrue、そうでなければfalse */ public boolean isSameColor(PartsSet other) { if (other != null && other.size() == size()) { for (List partsIdentifiers : values()) { for (PartsIdentifier partsIdentifier : partsIdentifiers) { PartsColorInfo ownColorInfo = getColorInfo(partsIdentifier); PartsColorInfo otherColorInfo = other.getColorInfo(partsIdentifier); if ( !PartsColorInfo.equals(ownColorInfo, otherColorInfo)) { return false; } } } return true; } return false; } /** * 引数aが保持しているパーツ識別子のカラー情報と同一のカラー情報を引数bがもっているか判定します.
* 引数b側はカテゴリや順序を問わず、少なくとも引数aと同じパーツ識別子をもっていれば足りるため、 * パーツ構成が同一であるかの判定は行いません.
* パーツ構造を含めて判定を行う場合は事前に{@link #isSameStructure(PartsSet, PartsSet)}を呼び出します.
* 双方がnullであればtrueとなります.
* いずれか一方がnullの場合はfalseとなります.
* @param a 対象1、null可 * @param b 対象2、null可 * @return 同一であればtrue、そうでなければfalse */ public static boolean isSameColor(PartsSet a, PartsSet b) { if (a == b) { return true; } if (a == null || b == null) { return false; } return a.isSameColor(b); } /** * このパーツセットが名前をもっているか? * @return 名前がある場合はtrue、設定されていないか空文字の場合はfalse */ public boolean hasName() { return localizedName != null && localizedName.length() > 0; } @Override public String toString() { StringBuilder buf = new StringBuilder(); buf.append(getClass().getSimpleName() + "@" + Integer.toHexString(System.identityHashCode(this))); buf.append("("); buf.append("partsSetId: " + partsSetId + ", "); buf.append("localizedName: " + localizedName + ", "); buf.append("presetFlg: " + presetParts + ", "); buf.append("background-color: " + bgColor + ", "); buf.append("affin-trans-param: " + Arrays.toString(affineTransformParameter) + ", "); buf.append("parts: " + parts + ", "); buf.append("partsColorMap: " + partsColorInfoMap); buf.append(")"); return buf.toString(); } } charactermanaj/src/charactermanaj/model/PartsSpecDecorater.java0000644000175000017500000000023711767047535025204 0ustar paulliupaulliupackage charactermanaj.model; public interface PartsSpecDecorater { void decoratePartsSpec(PartsIdentifier partsIdentifier, PartsSpec partsSpec); } charactermanaj/src/charactermanaj/model/RecommendationURL.java0000644000175000017500000000255111767047535024777 0ustar paulliupaulliupackage charactermanaj.model; import java.io.Serializable; /** * お勧めリンク * @author seraphy */ public class RecommendationURL implements Serializable, Cloneable { private static final long serialVersionUID = 3568122645473390201L; private String displayName; private String url; @Override public RecommendationURL clone() { try { return (RecommendationURL) super.clone(); } catch (CloneNotSupportedException ex) { throw new RuntimeException(ex); } } @Override public int hashCode() { if (url == null) { return 0; } return url.hashCode(); } @Override public boolean equals(Object obj) { if (obj == this) { return true; } if (obj != null && obj instanceof RecommendationURL) { RecommendationURL o = (RecommendationURL) obj; return (displayName == null ? (o.displayName == null) : displayName.equals(o.displayName)) && (url == null ? (o.url == null) : url.equals(o.url)); } return false; } public String getDisplayName() { return displayName; } public void setDisplayName(String displayName) { this.displayName = displayName; } public String getUrl() { return url; } public void setUrl(String url) { this.url = url; } @Override public String toString() { return "displayName=" + displayName + "/url=" + url; } } charactermanaj/src/charactermanaj/model/RecentData.java0000644000175000017500000000403611767047535023462 0ustar paulliupaulliupackage charactermanaj.model; import java.io.File; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectInputStream.GetField; import java.io.ObjectStreamClass; import java.io.Serializable; import java.net.URI; import java.net.URL; import java.util.logging.Level; import java.util.logging.Logger; /** * 最後に使用したデータの使用状況 * @author seraphy */ public class RecentData implements Serializable { /** * ロガー */ private static final Logger logger = Logger.getLogger(RecentData.class.getName()); private static final long serialVersionUID = 7232012934972862661L; private String appVersion; private URI docBase; /** * デシリアライズ * @param inp オブジェクトの復元ストリーム * @throws IOException 例外 * @throws ClassNotFoundException 例外 */ private void readObject(ObjectInputStream inp) throws IOException, ClassNotFoundException { GetField fields = inp.readFields(); ObjectStreamClass sig = fields.getObjectStreamClass(); if (logger.isLoggable(Level.FINE)) { logger.log(Level.FINE, "RecentDataのデシリアライズ name=" + sig.getName() + "/sid=" + sig.getSerialVersionUID()); } appVersion = (String) fields.get("appVersion", null); Object anyDocBase = fields.get("docBase", null); if (anyDocBase != null && anyDocBase instanceof URL) { // 旧形式のURLの場合はURIに変換. File file = new File(((URL) anyDocBase).getPath()); anyDocBase = file.toURI(); } docBase = (URI) anyDocBase; } public String getAppVersion() { return appVersion; } public void setAppVersion(String appVersion) { this.appVersion = appVersion; } public URI getDocBase() { return docBase; } public void setDocBase(URI docBase) { this.docBase = docBase; } @Override public String toString() { StringBuilder buf = new StringBuilder(); buf.append("appVersion=" + appVersion + "/docBase=" + docBase); return buf.toString(); } } charactermanaj/src/charactermanaj/model/WorkingSet.java0000644000175000017500000001152511767047535023545 0ustar paulliupaulliupackage charactermanaj.model; import java.io.File; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectStreamClass; import java.io.Serializable; import java.io.ObjectInputStream.GetField; import java.net.URI; import java.net.URL; import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; import charactermanaj.ui.model.WallpaperInfo; public class WorkingSet implements Serializable { /** * ロガー */ private static final Logger logger = Logger.getLogger(WorkingSet.class.getName()); private static final long serialVersionUID = -4728766140876842242L; private Map partsColorInfoMap; private String characterDataRev; private PartsSet partsSet; private URI characterDocBase; private File lastUsedSaveDir; private File lastUsedExportDir; // ver0.92 private PartsSet lastUsePresetParts; // ver0.94 private CharacterData characterData; // ver0.97 private WallpaperInfo wallpaperInfo; /** * デシリアライズ * @param inp オブジェクトの復元ストリーム * @throws IOException 例外 * @throws ClassNotFoundException 例外 */ @SuppressWarnings("unchecked") private void readObject(ObjectInputStream inp) throws IOException, ClassNotFoundException { GetField fields = inp.readFields(); ObjectStreamClass sig = fields.getObjectStreamClass(); if (logger.isLoggable(Level.FINE)) { logger.log(Level.FINE, "WorkingSetのデシリアライズ name=" + sig.getName() + "/sid=" + sig.getSerialVersionUID()); } partsColorInfoMap = (Map) fields.get("partsColorInfoMap", null); characterDataRev = (String) fields.get("characterDataRev", null); partsSet = (PartsSet) fields.get("partsSet", null); Object anyDocBase = fields.get("characterDocBase", null); if (anyDocBase != null && anyDocBase instanceof URL) { File file = new File(((URL) anyDocBase).getPath()); anyDocBase = file.toURI(); } // ver0.95からURI, それ以前はURL characterDocBase = (URI) anyDocBase; lastUsedSaveDir = (File) fields.get("lastUsedSaveDir", null); lastUsedExportDir = (File) fields.get("lastUsedExportDir", null); // ver0.92 lastUsePresetParts = (PartsSet) fields.get("lastUsePresetParts", null); // ver0.94 characterData = (CharacterData) fields.get("characterData", null); // ver0.97 wallpaperInfo = (WallpaperInfo) fields.get("wallpaperInfo", null); } public void setCharacterDataRev(String characterDataRev) { this.characterDataRev = characterDataRev; } public String getCharacterDataRev() { return characterDataRev; } public Map getPartsColorInfoMap() { return partsColorInfoMap; } public void setPartsColorInfoMap( Map partsColorInfoMap) { this.partsColorInfoMap = partsColorInfoMap; } public void setCharacterDocBase(URI characterDocBase) { this.characterDocBase = characterDocBase; } public void setPartsSet(PartsSet partsSet) { this.partsSet = partsSet; } public URI getCharacterDocBase() { return characterDocBase; } public PartsSet getPartsSet() { return partsSet; } public void setLastUsedSaveDir(File lastUsedSaveDir) { this.lastUsedSaveDir = lastUsedSaveDir; } public void setLastUsedExportDir(File lastUsedExportDir) { this.lastUsedExportDir = lastUsedExportDir; } public File getLastUsedSaveDir() { return lastUsedSaveDir; } public File getLastUsedExportDir() { return lastUsedExportDir; } /** * 最後に使用したお気に入りの情報.
* ver0.94以前には存在しなかったため、nullになりえます。 * @return */ public PartsSet getLastUsePresetParts() { return lastUsePresetParts; } public void setLastUsePresetParts(PartsSet lastUsePresetParts) { this.lastUsePresetParts = lastUsePresetParts; } public void setCharacterData(CharacterData characterData) { this.characterData = characterData; } /** * 使用していたキャラクター定義を取得します.
* ver0.95よりも以前には存在しないため、nullとなりえます.
* @return キャラクターデータ */ public CharacterData getCharacterData() { return characterData; } /** * 壁紙情報を取得します.
* ver0.97よりも以前には存在しないため、nullとなりえます.
* @return 壁紙情報 */ public WallpaperInfo getWallpaperInfo() { return wallpaperInfo; } public void setWallpaperInfo(WallpaperInfo wallpaperInfo) { this.wallpaperInfo = wallpaperInfo; } @Override public String toString() { return "docBase:" + characterDocBase + "/rev:" + characterDataRev; } } charactermanaj/src/charactermanaj/model/PartsSpec.java0000644000175000017500000000351711767047535023357 0ustar paulliupaulliupackage charactermanaj.model; import java.io.Serializable; /** * パーツの構成情報.
* @author seraphy */ public class PartsSpec implements Serializable { private static final long serialVersionUID = -5275967668710789314L; private PartsIdentifier partsIdentifier; private ColorGroup colorGroup = ColorGroup.NA; private PartsFiles partsFiles; /** * パーツの作者情報、指定がなければnull */ private PartsAuthorInfo authorInfo; /** * パーツのバージョン、指定がなければ0 */ private double version; /** * ダウンロードURL */ private String downloadURL; public PartsSpec(PartsIdentifier partsIdentifier) { if (partsIdentifier == null) { throw new IllegalArgumentException(); } this.partsIdentifier = partsIdentifier; this.partsFiles = new PartsFiles(partsIdentifier); } public PartsIdentifier getPartsIdentifier() { return partsIdentifier; } public PartsFiles getPartsFiles() { return partsFiles; } public void setAuthorInfo(PartsAuthorInfo authorInfo) { this.authorInfo = authorInfo; } public PartsAuthorInfo getAuthorInfo() { return authorInfo; } public String getAuthor() { if (authorInfo != null) { return authorInfo.getAuthor(); } return null; } public void setVersion(double version) { this.version = version; } public double getVersion() { return version; } public String getDownloadURL() { return downloadURL; } public void setDownloadURL(String downloadURL) { this.downloadURL = downloadURL; } public void setColorGroup(ColorGroup colorGroup) { if (colorGroup == null) { colorGroup = ColorGroup.NA; } this.colorGroup = colorGroup; } public ColorGroup getColorGroup() { return colorGroup; } } charactermanaj/src/charactermanaj/model/PartsIdentifier.java0000644000175000017500000000571611767047535024552 0ustar paulliupaulliupackage charactermanaj.model; import java.io.Serializable; /** * パーツ識別子.
* パーツ識別子の同値性は同一カテゴリID、且つ、同一のパーツ名(ID)であることによってのみ判定される.
* 表示名については不問.
* カテゴリIDのみ判定され、カテゴリの同値性についても問わない.
* @author seraphy * */ public final class PartsIdentifier implements Serializable, Comparable { private static final long serialVersionUID = 8943101890389091718L; private final PartsCategory partsCategory; private final String partsName; private final String localizedName; public PartsIdentifier(final PartsCategory partsCategory, final String partsName, final String localizedName) { if (partsName == null || partsCategory == null) { throw new IllegalArgumentException(); } this.partsCategory = partsCategory; this.partsName = partsName; this.localizedName = (localizedName == null || localizedName.trim().length() == 0) ? partsName : localizedName; } public PartsCategory getPartsCategory() { return partsCategory; } public boolean hasLayer(Layer layer) { return partsCategory.hasLayer(layer); } @Override public int hashCode() { return partsName.hashCode(); } @Override public boolean equals(Object obj) { if (obj == this) { return true; } if (obj != null && obj instanceof PartsIdentifier) { return partsName.equals(((PartsIdentifier) obj).partsName) && partsCategory.isSameCategoryID(((PartsIdentifier) obj).getPartsCategory()); } return false; } public static boolean equals(PartsIdentifier a, PartsIdentifier b) { if (a == b) { return true; } if (a == null || b == null) { return false; } return a.equals(b); } public int compareTo(PartsIdentifier o) { if (o == this) { return 0; } int ret = partsCategory.compareTo(o.partsCategory); if (ret == 0) { ret = localizedName.compareTo(o.localizedName); } if (ret == 0) { ret = partsName.compareTo(o.partsName); } return ret; } public String getPartsName() { return partsName; } public String getLocalizedPartsName() { return localizedName; } /** * ローカライズされた名前を変更する.
* [注意] このクラスは不変クラスなので、インスタンスを変更するのではなく、変更された状態の * 新しいインスタンスを返します.
* @param localizedName ローカライズされた名前 * @return 新しいインスタンス */ public PartsIdentifier setLocalizedPartsName(String localizedName) { if (localizedName == null || localizedName.trim().length() == 0) { throw new IllegalArgumentException(); } return new PartsIdentifier(partsCategory, partsName, localizedName); } @Override public String toString() { return getLocalizedPartsName(); } } charactermanaj/src/charactermanaj/model/CharacterData.java0000644000175000017500000007112411767047535024140 0ustar paulliupaulliupackage charactermanaj.model; import java.awt.Dimension; import java.io.File; import java.io.IOException; import java.io.Serializable; import java.net.URI; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.logging.Level; import java.util.logging.Logger; import charactermanaj.model.io.PartsDataLoader; /** * キャラクターデータ * @author seraphy */ public class CharacterData implements Serializable, PartsSpecResolver { /** * シリアライズバージョン */ private static final long serialVersionUID = -381763373314240953L; /** * ロガー */ private static final Logger logger = Logger.getLogger(CharacterData.class.getName()); /** * キャラクターデータを表示名順にソートするための比較器.
*/ public static final Comparator SORT_DISPLAYNAME = new Comparator() { public int compare(CharacterData o1, CharacterData o2) { if (!o1.isValid() || !o2.isValid()) { return o1.isValid() == o2.isValid() ? 0 : o1.isValid() ? 1 : -1; } int ret = o1.getName().compareTo(o2.getName()); if (ret == 0) { ret = o1.getId().compareTo(o2.getId()); } if (ret == 0) { ret = o1.getDocBase().toString().compareTo(o2.getDocBase().toString()); } return ret; } }; /** * キャラクターデータを定義しているXMLの位置.
* docBase自身はxml定義には含まれず、xmlをロードした位置を記憶するためにPersistentクラスによって設定される.
*/ private URI docBase; /** * キャラクターデータの内部用ID.
* キャラクターデータの構造を判定するために用いる.
*/ private String id; /** * キャラクターデータの更新番号.
* キャラクターデータの構造が変更されたことを識別するために用いる.
*/ private String rev; /** * 表示用名 */ private String localizedName; /** * 作成者 */ private String author; /** * 説明 */ private String description; /** * イメージサイズ */ private Dimension imageSize; /** * カテゴリ(定義順) */ private OrderedMap partsCategories = OrderedMap.emptyMap(); /** * 雑多なプロパティ.
*/ private Properties properties = new Properties(); /** * プリセットのマップ.
* キーはプリセット自身のID、値はプリセット自身.
*/ private HashMap presets = new HashMap(); /** * デフォルトのプリセットのID */ private String defaultPartsSetId; /** * カラーグループの定義.
*/ private OrderedMap colorGroups = OrderedMap.emptyMap(); /** * お勧めリンクリスト.
* Ver0.96以前には存在しないのでnullになり得る. */ private List recommendationURLList; /** * パーツカラーマネージャ.
* (非シリアライズデータ、デシリアライズ時には新規インスタンスが作成される).
*/ private transient PartsColorManager partsColorMrg = new PartsColorManager(this); /** * パーツデータローダー.
* パーツをロードしたときに設定され、リロードするときに使用する.
* パーツを一度もロードしていない場合はnull. * (非シリアライズデータ、デシリアライズ時はnullのまま).
*/ private transient PartsDataLoader partsDataLoader; /** * パーツイメージのセット.
* (キャラクターセットはパーツイメージをもったままシリアライズされることは想定していないが、可能ではある。) */ private Map> images = new HashMap>(); /** * シリアライズする * @param stream 出力先 * @throws IOException 失敗 */ private void writeObject(java.io.ObjectOutputStream stream) throws IOException { stream.defaultWriteObject(); } /** * 基本情報のみをコピーして返します.
* DocBase, ID, REV, Name, Author, Description, ImageSize、および, PartsCategory, ColorGroup, PartSetのコレクションがコピーされます.
* それ以外のものはコピーされません.
* @return 基本情報をコピーした新しいインスタンス */ public CharacterData duplicateBasicInfo() { return duplicateBasicInfo(true); } /** * 基本情報のみをコピーして返します.
* DocBase, ID, REV, Name, Author, Description, ImageSize、および, PartsCategory, ColorGroupがコピーされます.
* 引数のneedPartsSetがtrueの場合は,PartSetのコレクションもコピーされます.
* ディレクトリの監視状態、お勧めリンクもコピーされます.
* それ以外のものはコピーされません.
* @param needPartsSets パーツセットもコピーする場合、falseの場合はパーツセットは空となります. * @return 基本情報をコピーした新しいインスタンス */ public CharacterData duplicateBasicInfo(boolean needPartsSets) { CharacterData cd = new CharacterData(); cd.setId(this.id); cd.setRev(this.rev); cd.setDocBase(this.docBase); cd.setName(this.localizedName); cd.setAuthor(this.author); cd.setDescription(this.description); cd.setImageSize((Dimension)(this.imageSize == null ? null : this.imageSize.clone())); cd.setWatchDirectory(this.isWatchDirectory()); ArrayList recommendationURLList = null; if (this.recommendationURLList != null) { recommendationURLList = new ArrayList(); for (RecommendationURL recommendationUrl : this.recommendationURLList) { recommendationURLList.add(recommendationUrl.clone()); } } cd.setRecommendationURLList(recommendationURLList); ArrayList partsCategories = new ArrayList(); partsCategories.addAll(this.getPartsCategories()); cd.setPartsCategories(partsCategories.toArray(new PartsCategory[partsCategories.size()])); ArrayList colorGroups = new ArrayList(); colorGroups.addAll(this.getColorGroups()); cd.setColorGroups(colorGroups); if (needPartsSets) { for (PartsSet partsSet : this.getPartsSets().values()) { cd.addPartsSet(partsSet.clone()); } cd.setDefaultPartsSetId(this.defaultPartsSetId); } return cd; } /** * キャラクターデータが同じ構造であるか?
* カラーグループ、カテゴリ、レイヤーの各情報が等しければtrue、それ以外はfalse.
* 上記以外の項目(コメントや作者、プリセット等)については判定しない.
* サイズ、カラーグループの表示名や順序、カテゴリの順序や表示名、 * 複数アイテム可などの違いは構造の変更とみなさない.
* レイヤーはレイヤーID、重ね合わせ順、対象ディレクトリの3点が変更されている場合は構造の変更とみなす.
* いずれも個数そのものが変わっている場合は変更とみなす.
* 自分または相手がValidでなければ常にfalseを返す.
* @param other 比較対象, null可 * @return 同じ構造であればtrue、そうでなければfalse */ public boolean isSameStructure(CharacterData other) { if (!this.isValid() || other == null || !other.isValid()) { // 自分または相手がinvalidであれば構造的には常に不一致と見なす. return false; } // カラーグループが等しいか? (順序は問わない) // IDのみによって判定する ArrayList colorGroup1 = new ArrayList(getColorGroups()); ArrayList colorGroup2 = new ArrayList(other.getColorGroups()); if (colorGroup1.size() != colorGroup2.size()) { return false; } if (!colorGroup1.containsAll(colorGroup2)) { return false; } // カテゴリが等しいか? (順序は問わない) // IDによってのみ判定する. ArrayList categories1 = new ArrayList(getPartsCategories()); ArrayList categories2 = new ArrayList(other.getPartsCategories()); Comparator sortCategoryId = new Comparator() { public int compare(PartsCategory o1, PartsCategory o2) { int ret = o1.getCategoryId().compareTo(o2.getCategoryId()); if (ret == 0) { ret = o1.getOrder() - o2.getOrder(); } return ret; } }; // カテゴリID順に並び替えて, IDのみを比較する. Collections.sort(categories1, sortCategoryId); Collections.sort(categories2, sortCategoryId); int numOfCategories = categories1.size(); if (numOfCategories != categories2.size()) { // カテゴリ数不一致 return false; } for (int idx = 0; idx < numOfCategories; idx++) { PartsCategory category1 = categories1.get(idx); PartsCategory category2 = categories2.get(idx); String categoryId1 = category1.getCategoryId(); String categoryId2 = category2.getCategoryId(); if ( !categoryId1.equals(categoryId2)) { // カテゴリID不一致 return false; } } // レイヤーが等しいか? // ID、重ね順序、dirによってのみ判定する. int mx = categories1.size(); for (int idx = 0; idx < mx; idx++) { PartsCategory category1 = categories1.get(idx); PartsCategory category2 = categories2.get(idx); ArrayList layers1 = new ArrayList(category1.getLayers()); ArrayList layers2 = new ArrayList(category2.getLayers()); Comparator sortLayerId = new Comparator() { public int compare(Layer o1, Layer o2) { int ret = o1.getId().compareTo(o2.getId()); if (ret == 0) { ret = o1.getOrder() - o2.getOrder(); } return ret; } }; Collections.sort(layers1, sortLayerId); Collections.sort(layers2, sortLayerId); // ID、順序、Dirで判断する.(それ以外のレイヤー情報はequalsでは比較されない) if ( !layers1.equals(layers2)) { // レイヤー不一致 return false; } } return true; } /** * 引数で指定したキャラクター定義とアッパーコンパチブルであるか?
* 構造が同一であるか、サイズ違い、もしくはレイヤーの順序、カテゴリの順序、 * もしくはレイヤーまたはカテゴリが増えている場合で、減っていない場合はtrueとなる.
* 引数がnullの場合は常にfalseとなる. * @param other 前の状態のキャラクター定義、null可 * @return アッパーコンパチブルであればtrue、そうでなければfalse */ public boolean isUpperCompatibleStructure(CharacterData other) { if (!this.isValid() || other == null || !other.isValid()) { // 自分または相手がinvalidであれば構造的には常に互換性なしと見なす. return false; } // カラーグループが等しいか? (順序は問わない) // IDのみによって判定する ArrayList colorGroupNew = new ArrayList(getColorGroups()); ArrayList colorGroupOld = new ArrayList(other.getColorGroups()); if (!colorGroupNew.containsAll(colorGroupOld)) { // 自分が相手分のすべてのものを持っていなければ互換性なし. return false; } // カテゴリをすべて含むか? (順序は問わない) // IDによってのみ判定する. Map categoriesNew = new HashMap(); for (PartsCategory category : getPartsCategories()) { categoriesNew.put(category.getCategoryId(), category); } Map categoriesOld = new HashMap(); for (PartsCategory category : other.getPartsCategories()) { categoriesOld.put(category.getCategoryId(), category); } if ( !categoriesNew.keySet().containsAll(categoriesOld.keySet())) { // 自分が相手のすべてのカテゴリを持っていなければ互換性なし. return false; } // レイヤーをすべて含むか? // ID、Dirによってのみ判定する. for (Map.Entry categoryOldEntry : categoriesOld.entrySet()) { String categoryId = categoryOldEntry.getKey(); PartsCategory categoryOld = categoryOldEntry.getValue(); PartsCategory categoryNew = categoriesNew.get(categoryId); if (categoryNew == null) { return false; } Map layersNew = new HashMap(); for (Layer layer : categoryNew.getLayers()) { layersNew.put(layer.getId(), layer); } Map layersOld = new HashMap(); for (Layer layer : categoryOld.getLayers()) { layersOld.put(layer.getId(), layer); } if ( !layersNew.keySet().containsAll(layersOld.keySet())) { // 自分が相手のすべてのレイヤー(ID)を持っていなければ互換性なし. return false; } for (Map.Entry layerOldEntry : layersOld.entrySet()) { String layerId = layerOldEntry.getKey(); Layer layerOld = layerOldEntry.getValue(); Layer layerNew = layersNew.get(layerId); if (layerNew == null) { return false; } File dirOld = new File(layerOld.getDir()); File dirNew = new File(layerNew.getDir()); if ( !dirOld.equals(dirNew)) { // ディレクトリが一致しなければ互換性なし. return false; } } } return true; } /** * デシリアライズする. * @param stream 入力もと * @throws IOException 失敗 * @throws ClassNotFoundException 失敗 */ private void readObject(java.io.ObjectInputStream stream) throws IOException, ClassNotFoundException { stream.defaultReadObject(); partsColorMrg = new PartsColorManager(this); } /** * お勧めリンクのリストを取得する.
* 古いキャラクターデータで、お勧めリストノードが存在しない場合はnullとなる.
* @return お気に入りリンクのリスト、もしくはnull */ public List getRecommendationURLList() { return recommendationURLList; } /** * お勧めリンクリストを設定する.
* @param recommendationURLList、null可 */ public void setRecommendationURLList( List recommendationURLList) { this.recommendationURLList = recommendationURLList; } /** * 作者を設定する. * @param author 作者 */ public void setAuthor(String author) { this.author = author; } /** * 説明を設定する.
* 説明の改行コードはプラットフォーム固有の改行コードに変換される.
* @param description */ public void setDescription(String description) { if (description != null) { description = description.replace("\r\n", "\n"); description = description.replace("\r", "\n"); description = description.replace("\n", System.getProperty("line.separator")); } this.description = description; } public String getAuthor() { return author; } /** * 説明を取得する.
* 説明の改行コードはプラットフォーム固有の改行コードとなる.
* @return 説明 */ public String getDescription() { return description; } public String getId() { return id; } public void setId(String id) { this.id = id; } public String getRev() { return rev; } public void setRev(String rev) { this.rev = rev; } public void setDocBase(URI docBase) { this.docBase = docBase; } public URI getDocBase() { return docBase; } /** * ディレクトリを監視するか? (デフォルトは監視する) * @return ディレクトリを監視する場合はtrue */ public boolean isWatchDirectory() { try { String value = properties.getProperty("watch-dir"); if (value != null) { return Boolean.parseBoolean(value); } } catch (RuntimeException ex) { logger.log(Level.WARNING, "watch-dir property is invalid.", ex); } // デフォルトは監視する. return true; } /** * ディレクトリを監視するか指定する. * @param watchDir 監視する場合はtrue、しない場合はfalse */ public void setWatchDirectory(boolean watchDir) { properties.setProperty("watch-dir", Boolean.toString(watchDir)); } public String getProperty(String key) { if (key == null || key.trim().length() == 0) { throw new IllegalArgumentException(); } return properties.getProperty(key.trim()); } public void setProperty(String key, String value) { if (key == null || key.trim().length() == 0) { throw new IllegalArgumentException(); } properties.setProperty(key.trim(), value); } public Collection getPropertyNames() { ArrayList names = new ArrayList(); for (Object key : properties.keySet()) { names.add(key.toString()); } return names; } /** * 有効なキャラクターデータであるか? * ID, Name, DocBaseが存在するものが有効なキャラクターデータである.
* @return 有効であればtrue */ public boolean isValid() { return id != null && id.length() > 0 && localizedName != null && localizedName.length() > 0 && docBase != null; } /** * 編集可能か?
* まだdocbaseが指定されていない新しいインスタンスであるか、 * もしくはdocbaseが実在しファイルであり且つ読み込み可能であるか、 * もしくはdocbaseがまだ存在しない場合は、その親ディレクトリが読み書き可能であるか? * @return 編集可能であればtrue */ public boolean canWrite() { try { checkWritable(); return true; } catch (IOException ex) { return false; } } /** * 編集可能か?
* まだdocbaseが指定されていない新しいインスタンスであるか、 * もしくはdocbaseが実在しファイルであり且つ読み込み可能であるか、 * もしくはdocbaseがまだ存在しない場合は、その親ディレクトリが読み書き可能であるか? * @throws IOException 編集可能でなければIOException例外が発生する. */ public void checkWritable() throws IOException { if (docBase == null) { throw new IOException("invalid profile: " + this); } if ( !"file".equals(docBase.getScheme())) { throw new IOException("ファイルプロトコルではないため書き込みはできません。:" + docBase); } File xmlFile = new File(docBase); if (xmlFile.exists()) { // character.xmlファイルがある場合 if ( !xmlFile.canWrite() || !xmlFile.canRead()) { throw new IOException("書き込み、もしくは読み込みが禁止されているプロファイルです。" + docBase); } } else { // character.xmlファイルが、まだ存在していない場合 File parent = xmlFile.getParentFile(); if ( !parent.exists()) { throw new IOException("親ディレクトリがありません。" + docBase); } if ( !parent.canWrite() || !parent.canRead()) { throw new IOException("親ディレクトリは書き込み、もしくは読み込みが禁止されています。" + docBase); } } } /** * キャラクター名を設定する. * @param name */ public void setName(String name) { this.localizedName = name; } /** * キャラクター名を取得する. * @return */ public String getName() { return localizedName; } public void setImageSize(Dimension imageSize) { if (imageSize != null) { imageSize = (Dimension) imageSize.clone(); } this.imageSize = imageSize; } public Dimension getImageSize() { return imageSize != null ? (Dimension) imageSize.clone() : null; } public void setColorGroups(Collection colorGroups) { if (colorGroups == null) { throw new IllegalArgumentException(); } ArrayList colorGroupWithNA = new ArrayList(); colorGroupWithNA.add(ColorGroup.NA); for (ColorGroup colorGroup : colorGroups) { if (colorGroup.isEnabled()) { colorGroupWithNA.add(colorGroup); } } OrderedMap ret = new OrderedMap( colorGroupWithNA, new OrderedMap.KeyDetector() { public String getKey(ColorGroup data) { return data.getId(); } }); this.colorGroups = ret; } /** * カラーグループIDからカラーグループを取得する.
* 存在しない場合はN/Aを返す.
* @param colorGroupId カラーグループID * @return カラーグループ */ public ColorGroup getColorGroup(String colorGroupId) { ColorGroup cg = colorGroups.get(colorGroupId); if (cg != null) { return cg; } return ColorGroup.NA; } public Collection getColorGroups() { return colorGroups.values(); } public PartsCategory getPartsCategory(String categoryId) { if (partsCategories == null) { return null; } return partsCategories.get(categoryId); } public void setPartsCategories(PartsCategory[] partsCategories) { if (partsCategories == null) { partsCategories = new PartsCategory[0]; } this.partsCategories = new OrderedMap( Arrays.asList(partsCategories), new OrderedMap.KeyDetector() { public String getKey(PartsCategory data) { return data.getCategoryId(); } }); } public List getPartsCategories() { return partsCategories.asList(); } /** * パーツデータがロード済みであるか?
* 少なくとも{@link #loadPartsData(PartsDataLoader)}が一度呼び出されていればtrueとなる.
* falseの場合はパーツローダが設定されていないことを示す.
* @return パーツデータがロード済みであればtrue、そうでなければfalse */ public boolean isPartsLoaded() { return partsDataLoader != null; } /** * パーツデータをロードする.
* パーツローダを指定し、このローダはパーツの再ロード時に使用するため保持される.
* @param partsDataLoader ローダー */ public void loadPartsData(PartsDataLoader partsDataLoader) { if (partsDataLoader == null) { throw new IllegalArgumentException(); } this.partsDataLoader = partsDataLoader; reloadPartsData(); } /** * パーツデータをリロードする.
* ロード時に使用したローダーを使ってパーツを再ロードします.
* まだ一度もロードしていない場合はIllegalStateException例外が発生します.
* @return 変更があった場合はtrue、ない場合はfalse */ public boolean reloadPartsData() { if (partsDataLoader == null) { throw new IllegalStateException("partsDataLoader is not set."); } // パーツデータのロード images.clear(); for (PartsCategory category : partsCategories.asList()) { images.put(category, partsDataLoader.load(category)); } // NOTE: とりあえずパーツの変更を検査せず、常に変更ありにしておく。とりあえず実害ない。 return true; } /** * {@inheritDoc} */ public PartsSpec getPartsSpec(PartsIdentifier partsIdentifier) { if (partsIdentifier == null) { throw new IllegalArgumentException(); } PartsCategory partsCategory = partsIdentifier.getPartsCategory(); Map partsSpecMap = images.get(partsCategory); if (partsSpecMap != null) { PartsSpec partsSpec = partsSpecMap.get(partsIdentifier); if (partsSpec != null) { return partsSpec; } } return null; } /** * {@inheritDoc} */ public Map getPartsSpecMap(PartsCategory category) { Map partsImageMap = images.get(category); if (partsImageMap == null) { return Collections.emptyMap(); } return partsImageMap; } public PartsColorManager getPartsColorManager() { return this.partsColorMrg; } /** * パーツセットを登録します.
* お気に入りとプリセットの両方の共用です.
* IDおよび名前がないものは登録されず、falseを返します.
* パーツセットは、このキャラクター定義に定義されているカテゴリに正規化されます.
* 正規化された結果カテゴリが一つもなくなった場合は何も登録されず、falseを返します.
* 登録された場合はtrueを返します.
* 同一のIDは上書きされます.
* @param partsSet * @return 登録された場合はtrue、登録できない場合はfalse */ public boolean addPartsSet(PartsSet partsSet) { if (partsSet == null) { throw new IllegalArgumentException(); } if (partsSet.getPartsSetId() == null || partsSet.getPartsSetId().length() == 0 || partsSet.getLocalizedName() == null || partsSet.getLocalizedName().length() == 0) { return false; } PartsSet compatiblePartsSet = partsSet.createCompatible(this); if (compatiblePartsSet.isEmpty()) { return false; } presets.put(compatiblePartsSet.getPartsSetId(), compatiblePartsSet); return true; } /** * プリセットパーツおよびパーツセット(Favorites)のコレクション. * @return パーツセットのコレクション */ public Map getPartsSets() { return presets; } /** * プリセットパーツおよびパーツセットをリセットします.
* @param noRemovePreset プリセットは削除せず残し、プリセット以外のパーツセットをクリアする場合はtrue、falseの場合は全て削除される. */ public void clearPartsSets(boolean noRemovePreset) { if (!noRemovePreset) { // 全部消す presets.clear(); defaultPartsSetId = null; } else { // プリセット以外を消す Iterator> ite = presets.entrySet().iterator(); while (ite.hasNext()) { Map.Entry entry = ite.next(); if (!entry.getValue().isPresetParts()) { // デフォルトパーツセットであれば、デフォルトパーツセットもnullにする. // (ただし、デフォルトパーツセットはプリセットであることを想定しているので、この処理は安全策用。) if (entry.getKey().equals(defaultPartsSetId)) { defaultPartsSetId = null; } ite.remove(); } } } } /** * デフォルトのパーツセットを取得する.
* そのパーツセットIDが実在するか、あるいは、それがプリセットであるか、などは一切関知しない.
* 呼び出しもとで必要に応じてチェックすること.
* @return デフォルトとして指定されているパーツセットのID、なければnull */ public String getDefaultPartsSetId() { return defaultPartsSetId; } /** * デフォルトのパーツセットIDを指定する.
* nullの場合はデフォルトのパーツセットがないことを示す.
* パーツセットはプリセットであることが想定されるが、
* 実際に、その名前のパーツセットが存在するか、あるいは、そのパーツセットがプリセットであるか、などの判定は一切行わない.
* @param defaultPartsSetId パーツセットID、もしくはnull */ public void setDefaultPartsSetId(String defaultPartsSetId) { this.defaultPartsSetId = defaultPartsSetId; } @Override public String toString() { StringBuilder buf = new StringBuilder(); buf.append("character-id: " + id); buf.append("/rev:" + rev); buf.append("/name:" + localizedName); buf.append("/image-size:" + imageSize.width + "x" + imageSize.height); buf.append("/docBase:" + docBase); return buf.toString(); } } charactermanaj/src/charactermanaj/model/ColorInfo.java0000644000175000017500000000463311767047535023345 0ustar paulliupaulliupackage charactermanaj.model; import java.io.Serializable; import charactermanaj.graphics.filters.ColorConvertParameter; /** * カラーグループおよび連動フラグを含む色情報.
* @author seraphy */ public class ColorInfo implements Serializable, Cloneable { private static final long serialVersionUID = 2448550538711608223L; private ColorConvertParameter colorParameter = new ColorConvertParameter(); private boolean syncColorGroup = false; private ColorGroup colorGroup = ColorGroup.NA; @Override public ColorInfo clone() { ColorInfo colorInfo; try { colorInfo = (ColorInfo) super.clone(); } catch (CloneNotSupportedException ex) { throw new RuntimeException(ex.getMessage(), ex); } colorInfo.colorParameter = (ColorConvertParameter) this.colorParameter.clone(); return colorInfo; } @Override public int hashCode() { return colorParameter.hashCode() ^ colorGroup.hashCode(); } @Override public boolean equals(Object obj) { if (obj == this) { return true; } if (obj != null && obj instanceof ColorInfo) { ColorInfo o = (ColorInfo) obj; return colorGroup.equals(o.colorGroup) && syncColorGroup == o.syncColorGroup && colorParameter.equals(o.colorParameter); } return false; } public ColorConvertParameter getColorParameter() { return colorParameter; } public void setColorParameter(ColorConvertParameter colorParameter) { if (colorParameter == null) { this.colorParameter = new ColorConvertParameter(); } else { this.colorParameter = colorParameter; } } public boolean isSyncColorGroup() { return syncColorGroup; } public void setSyncColorGroup(boolean syncColorGroup) { this.syncColorGroup = syncColorGroup; } public ColorGroup getColorGroup() { return colorGroup; } public void setColorGroup(ColorGroup colorGroup) { if (colorGroup == null) { this.colorGroup = ColorGroup.NA; } else { this.colorGroup = colorGroup; } } @Override public String toString() { StringBuilder buf = new StringBuilder(); buf.append(getClass().getSimpleName() + "@" + Integer.toHexString(System.identityHashCode(this))); buf.append("("); buf.append("(colorGroup: " + colorGroup + "(sync: " + syncColorGroup + ")), "); buf.append("(colorParameter: " + colorParameter + ")"); buf.append(")"); return buf.toString(); } } charactermanaj/src/charactermanaj/model/io/0000755000175000017500000000000011767047535021211 5ustar paulliupaulliucharactermanaj/src/charactermanaj/model/io/PartsImageDirectoryWatchEvent.java0000644000175000017500000000070111767047535027764 0ustar paulliupaulliupackage charactermanaj.model.io; import java.util.EventObject; import charactermanaj.model.CharacterData; public class PartsImageDirectoryWatchEvent extends EventObject { private static final long serialVersionUID = 8090309437115158185L; public PartsImageDirectoryWatchEvent(CharacterData characterData) { super(characterData); } public CharacterData getCharacterData() { return (CharacterData) getSource(); } } charactermanaj/src/charactermanaj/model/io/RecentDataPersistent.java0000644000175000017500000001377411767047535026163 0ustar paulliupaulliupackage charactermanaj.model.io; import java.io.File; import java.io.IOException; import java.net.URI; import java.util.HashMap; import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; import charactermanaj.model.AppConfig; import charactermanaj.model.CharacterData; import charactermanaj.model.RecentData; import charactermanaj.util.DirectoryConfig; import charactermanaj.util.UserData; import charactermanaj.util.UserDataFactory; /** * 最後に使用したデータの使用状況を保存・復元するためのクラス * @author seraphy */ public final class RecentDataPersistent { /** * ロガー */ private static final Logger logger = Logger.getLogger(RecentDataPersistent.class.getName()); /** * シングルトン */ private static final RecentDataPersistent inst = new RecentDataPersistent(); /** * 最後に使用したプロファイルのコートベースを保存するファイル名 */ private static final String RECENT_CHARACTER_SER = "recent-character.ser"; /** * プライベートコンストラクタ */ private RecentDataPersistent() { super(); } /** * インスタンスを取得する * @return インスタンス */ public static RecentDataPersistent getInstance() { return inst; } /** * 最後に使用したキャラクターデータとして記録する. * @param characterData * @throws IOException 保存できなった場合 */ public void saveRecent(CharacterData characterData) throws IOException { if (characterData == null) { throw new IllegalArgumentException(); } if (!characterData.isValid()) { return; } AppConfig appConfig = AppConfig.getInstance(); RecentData recentData = new RecentData(); recentData.setAppVersion(appConfig.getSpecificationVersion()); recentData.setDocBase(characterData.getDocBase()); UserData recentCharacterStore = getRecentCharacterStore(); // 他のキャラクターディレクトリ上のデータを取得しマージする. Map recentDataMap = new HashMap(); try { if (recentCharacterStore.exists()) { Object rawRecentData = recentCharacterStore.load(); if (rawRecentData instanceof Map) { @SuppressWarnings("unchecked") Map prevRecentDataMap = (Map) rawRecentData; for (Map.Entry entry : prevRecentDataMap.entrySet()) { File dir = entry.getKey(); if (dir.exists() && dir.isDirectory()) { // 現在も有効なものだけを保存する. // (すでに存在しなくなっているものは除外する.) RecentData prevRecentData = entry.getValue(); recentDataMap.put(dir, prevRecentData); } } } } } catch (Exception ex) { logger.log(Level.WARNING, "old recentDataFile load failed.", ex); // 古い情報が取得できない場合でも最新のものだけは保存できるようにしておく。 } // 保存する. File currentCharactersDir = DirectoryConfig.getInstance().getCharactersDir(); recentDataMap.put(currentCharactersDir, recentData); recentCharacterStore.save(recentDataMap); } /** * 最後に使用したキャラクターデータを取得する. * @return キャラクターデータ。最後に使用したデータが存在しない場合はnull * @throws IOException 読み込みに失敗した場合 */ public CharacterData loadRecent() throws IOException { UserData recentCharacterStore = getRecentCharacterStore(); if (!recentCharacterStore.exists()) { return null; } RecentData recentData; try { File currentCharactersDir = DirectoryConfig.getInstance().getCharactersDir(); Object rawRecentData = recentCharacterStore.load(); if (rawRecentData instanceof RecentData) { // 旧形式 (単一) recentData = (RecentData) rawRecentData; logger.log(Level.INFO, "old-recentdata-type: " + recentData); // 旧形式で保存されているURIが、現在選択しているキャラクターデータと同じ親ディレクトリ // でなければ復元しない. URI uri = recentData.getDocBase(); File parentDir = new File(uri).getParentFile().getParentFile(); // 2段上 if (!currentCharactersDir.equals(parentDir)) { logger.log(Level.INFO, "unmatched characters-dir. current=" + currentCharactersDir + "/recent=" + parentDir); recentData = null; } } else if (rawRecentData instanceof Map) { // 新形式 (複数のキャラクターディレクトリに対応) @SuppressWarnings("unchecked") Map recentDataMap = (Map) rawRecentData; recentData = recentDataMap.get(currentCharactersDir); logger.log(Level.FINE, "recent-data: " + currentCharactersDir + "=" + recentData); } else { // 不明な形式 logger.log(Level.SEVERE, "invalid file format. " + recentCharacterStore + "/class=" + rawRecentData.getClass()); recentData = null; } } catch (Exception ex) { // RecentData情報の復元に失敗した場合は最後に使用したデータが存在しないものとみなす. logger.log(Level.WARNING, "recent data loading failed. " + recentCharacterStore, ex); recentData = null; } if (recentData != null) { CharacterDataPersistent persist = CharacterDataPersistent.getInstance(); return persist.loadProfile(recentData.getDocBase()); } // 履歴がない場合、もしくは読み取れなかった場合はnullを返す. return null; } /** * 最後に使用したキャラクタデータの保存先を取得する * @return 保存先 */ protected UserData getRecentCharacterStore() { UserDataFactory userDataFactory = UserDataFactory.getInstance(); UserData recentCharacterStore = userDataFactory.getUserData(RECENT_CHARACTER_SER); return recentCharacterStore; } } charactermanaj/src/charactermanaj/model/io/CharacterDataZipFileWriter.java0000644000175000017500000000727311767047535027233 0ustar paulliupaulliupackage charactermanaj.model.io; import java.io.File; import java.io.IOException; import java.io.OutputStream; import java.io.UnsupportedEncodingException; import java.nio.charset.Charset; import java.nio.charset.CharsetEncoder; import java.nio.charset.CodingErrorAction; import org.apache.tools.zip.ZipEntry; import org.apache.tools.zip.ZipOutputStream; import charactermanaj.model.AppConfig; public class CharacterDataZipFileWriter extends AbstractCharacterDataArchivedFileWriter { /** * Zipストリーム */ protected ZipOutputStream zipOutStm; /** * ファイル名のエンコーディング */ protected CharsetEncoder enc; /** * ルートコンテンツへのプレフィックス */ protected String rootPrefix = ""; public CharacterDataZipFileWriter(File outFile) throws IOException { super(outFile); AppConfig appConfig = AppConfig.getInstance(); String zipNameEncoding = appConfig.getZipNameEncoding(); // コンストラクタにストリームではなくファイル名を指定することで、 // 内部でランダムアクセスファイルを使うようになるためヘッダのCRCチェックの書き込み等で有利 this.zipOutStm = new ZipOutputStream(tmpFile); // ファイル名の文字コードを設定する. // (JDKの標準のZipOutputStreamはUTF-8になるが、一般的にはMS932が多いため、Apache Antのものを借用し指定する.) this.enc = Charset.forName(zipNameEncoding).newEncoder(); zipOutStm.setEncoding(zipNameEncoding); enc.onUnmappableCharacter(CodingErrorAction.REPORT); // zipの場合、根本に1つフォルダをたてておく. // 一般的なフォルダ圧縮したものと体裁をそろえるため. String fname = outFile.getName(); int extpos = fname.lastIndexOf('.'); if (extpos > 0) { // ドットで始まる名前の場合は無視 fname = fname.substring(0, extpos); } setRootPrefix(fname); } public void setRootPrefix(String rootPrefix) { if (rootPrefix == null || rootPrefix.trim().equals("/")) { rootPrefix = ""; } if (rootPrefix.length() > 0 && !rootPrefix.endsWith("/")) { rootPrefix += "/"; } this.rootPrefix = rootPrefix.trim(); } public String getRootPrefix() { return rootPrefix; } @Override protected void closeEntry() throws IOException { zipOutStm.closeEntry(); } @Override protected OutputStream getOutputStream() throws IOException { return zipOutStm; } @Override protected void putNextEntry(String name, long lastModified) throws IOException { // ルートプレフィックスをすべてのエントリの登録時に付与する. String fname = rootPrefix + name; // ファイル名がキャラクターセットに合致するか? checkName(fname); // Zipエントリの登録 ZipEntry entry = new ZipEntry(fname); if (lastModified > 0) { entry.setTime(lastModified); } zipOutStm.putNextEntry(entry); } protected void internalClose() throws IOException { zipOutStm.close(); } /** * ファイル名がエンコーディング可能であるかチェックする.
* @param name チェックする名前 * @throws IOException ファイル名が不正である場合 * @throws UnsupportedEncodingException ファイル名がエンコーディングできない場合 */ protected void checkName(String name) throws UnsupportedEncodingException, IOException { if (name == null || name.length() == 0) { throw new IOException("missing entry name"); } if (!enc.canEncode(name)) { throw new UnsupportedEncodingException("file name encoding error.: " + name); } } } charactermanaj/src/charactermanaj/model/io/CharacterDataDirectoryFile.java0000644000175000017500000001031411767047535027226 0ustar paulliupaulliupackage charactermanaj.model.io; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.net.URI; import java.util.Collection; import java.util.Collections; import java.util.logging.Level; import java.util.logging.Logger; import charactermanaj.model.CharacterData; /** * ディレクトリをアーカイブと見立てる * @author seraphy */ public class CharacterDataDirectoryFile extends AbstractCharacterDataArchiveFile { /** * ロガー */ private static final Logger logger = Logger.getLogger(CharacterDataDirectoryFile.class.getName()); /** * 対象ディレクトリ */ protected File baseDir; /** * ディレクトリ上のファイルコンテンツ * * @author seraphy */ protected static class DirFileContent implements FileContent { /** * ディレクトリ名 + ファイル名からなるエントリ名.
* エントリ名の区切り文字は常に「/」とする.
*/ private String entryName; /** * 実際のファイルへのパス */ private File entry; protected DirFileContent(File entry, String entryName) { this.entry = entry; this.entryName = entryName; } public String getEntryName() { return entryName; } public long lastModified() { return entry.lastModified(); } public InputStream openStream() throws IOException { return new FileInputStream(entry); } } /** * アーカイブファイルをベースとしたURIを返す.
* @param name コンテンツ名 * @return URI * @throws IOException URIを生成できない場合 */ protected URI getContentURI(String name) throws IOException { return new File(baseDir, name).toURI(); } @Override public Collection getPartsImageContents( CharacterData characterData, boolean newly) { if (!newly && isOverlapped(characterData)) { // 既存のプロファイルへのインポートで指定されたインポートもととなるディレクトリが // 既存のプロファイルのディレクトリと重なっていれば自分自身のインポートであるとして // 空のパーツリストを返す. return Collections.emptyList(); } return super.getPartsImageContents(characterData, newly); } /** * このディレクトリに対してターゲットプロファイルのディレクトリがかぶっているか? * つまり、ターゲット自身のディレクトリをソースとしていないか? * @param characterData ソースプロファイル * @return 被っている場合はtrue、被っていない場合はfalse */ protected boolean isOverlapped(CharacterData characterData) { if (characterData == null) { return false; } URI docBase = characterData.getDocBase(); if (docBase == null || !"file".equals(docBase.getScheme())) { return false; } String folderPlace = File.separator; String basePath = new File(docBase).getParent() + folderPlace; String sourcePath = baseDir.getPath() + folderPlace; // インポートもとディレクトリがインポート先のキャラクター定義のディレクトリを含むか? boolean result = basePath.contains(sourcePath); logger.log(Level.FINE, "checkOverlapped: " + basePath + " * " + sourcePath + " :" + result); return result; } public void close() throws IOException { // ディレクトリなのでクローズ処理は必要ない. } public CharacterDataDirectoryFile(File file) throws IOException { super(file); baseDir = file; load(baseDir, ""); searchRootPrefix(); } private void load(File dir, String prefix) { if (!dir.exists() || !dir.isDirectory()) { // ディレクトリでなければ何もせず戻る return; } for (File file : dir.listFiles()) { String name = file.getName(); String entryName = prefix + name; if (file.isDirectory()) { // エントリ名の区切り文字は常に「/」とする. (ZIP/JARのentry互換のため) load(file, entryName + "/"); } else { addEntry(new DirFileContent(file, entryName)); } } } } charactermanaj/src/charactermanaj/model/io/ExportInfoKeys.java0000644000175000017500000000054011767047535025004 0ustar paulliupaulliupackage charactermanaj.model.io; public interface ExportInfoKeys { String EXPORT_PRESETS = "EXPORT_PRESETS"; String EXPORT_SAMPLE_PICTURE = "EXPORT_SAMPLE_PICTURE"; String EXPORT_CHARACTER_DATA = "EXPORT_CHARACTER_DATA"; String EXPORT_PARTS_IMAGES = "EXPORT_PARTS_IMAGES"; String EXPORT_TIMESTAMP = "EXPORT_TIMESTAMP"; } charactermanaj/src/charactermanaj/model/io/CharacterDataJarArchiveFile.java0000644000175000017500000000224311767047535027302 0ustar paulliupaulliupackage charactermanaj.model.io; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.util.Enumeration; import java.util.jar.JarEntry; import java.util.jar.JarFile; public class CharacterDataJarArchiveFile extends AbstractCharacterDataArchiveFile { protected JarFile jarFile; protected class JarFileContent implements FileContent { private JarEntry entry; protected JarFileContent(JarEntry entry) { this.entry = entry; } public String getEntryName() { return entry.getName(); } public long lastModified() { return entry.getTime(); } public InputStream openStream() throws IOException { return jarFile.getInputStream(entry); } } public void close() throws IOException { jarFile.close(); } public CharacterDataJarArchiveFile(File file) throws IOException { super(file); jarFile = new JarFile(file); load(); } private void load() { Enumeration enm = jarFile.entries(); while (enm.hasMoreElements()) { JarEntry entry = enm.nextElement(); addEntry(new JarFileContent(entry)); } searchRootPrefix(); } } charactermanaj/src/charactermanaj/model/io/FilePartsDataLoader.java0000644000175000017500000000442711767047535025675 0ustar paulliupaulliupackage charactermanaj.model.io; import java.io.File; import java.io.FileFilter; import java.util.HashMap; import java.util.Map; import charactermanaj.graphics.io.FileImageResource; import charactermanaj.model.Layer; import charactermanaj.model.PartsCategory; import charactermanaj.model.PartsFiles; import charactermanaj.model.PartsIdentifier; import charactermanaj.model.PartsSpec; /** * ディレクトリを指定して、そこからキャラクターのパーツデータをロードするローダー.
* @author seraphy * */ public class FilePartsDataLoader implements PartsDataLoader { /** * ベースディレクトリ */ private File baseDir; public FilePartsDataLoader(File baseDir) { if (baseDir == null) { throw new IllegalArgumentException(); } this.baseDir = baseDir; } public File getBaseDir() { return baseDir; } public Map load(PartsCategory category) { if (category == null) { throw new IllegalArgumentException(); } final Map images = new HashMap(); for (Layer layer : category.getLayers()) { File searchDir = new File(baseDir, layer.getDir()); if (!searchDir.exists() || !searchDir.isDirectory()) { continue; } for (File imgFile : searchDir.listFiles(new FileFilter() { public boolean accept(File pathname) { if (pathname.isFile()) { String lcfname = pathname.getPath().toLowerCase(); return lcfname.endsWith(".png"); } return false; } })) { String partsName = imgFile.getName(); int extpos = partsName.lastIndexOf("."); if (extpos > 0) { partsName = partsName.substring(0, extpos); } PartsIdentifier partsIdentifier = new PartsIdentifier(category, partsName, partsName); PartsSpec partsSpec = images.get(partsIdentifier); if (partsSpec == null) { partsSpec = createPartsSpec(partsIdentifier); images.put(partsIdentifier, partsSpec); } PartsFiles parts = partsSpec.getPartsFiles(); parts.put(layer, new FileImageResource(imgFile)); } } return images; } protected PartsSpec createPartsSpec(PartsIdentifier partsIdentifier) { return new PartsSpec(partsIdentifier); } } charactermanaj/src/charactermanaj/model/io/PartsDataLoaderFactory.java0000644000175000017500000000243211767047535026417 0ustar paulliupaulliupackage charactermanaj.model.io; import java.io.File; import java.io.IOException; import java.net.URI; /** * パーツデータのローダーファクトリ.
* @author seraphy * */ public class PartsDataLoaderFactory { private static final PartsDataLoaderFactory singleton = new PartsDataLoaderFactory(); private PartsDataLoaderFactory() { super(); } public static PartsDataLoaderFactory getInstance() { return singleton; } /** * 設定ファイル(character.xml)のDocBaseをもとに、キャラクターデータのローダーを作成する.
* 現在のところ、fileプロトコルのみサポートする.
* @param docBase 設定ファイルのURI * @return ローダー * @throws IOException URIに対応するローダーが存在しないか、構築できない場合 */ public PartsDataLoader createPartsLoader(URI docBase) throws IOException { if (docBase == null) { throw new IllegalArgumentException(); } if (!"file".equals(docBase.getScheme())) { throw new IOException("ファイル以外はサポートしていません。:" + docBase); } File docbaseFile = new File(docBase); File baseDir = docbaseFile.getParentFile(); return new FilePartsDataLoader(baseDir); } } charactermanaj/src/charactermanaj/model/io/PartsSpecDecorateLoader.java0000644000175000017500000000545211767047535026564 0ustar paulliupaulliupackage charactermanaj.model.io; import java.util.Collection; import java.util.Collections; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; import charactermanaj.model.AppConfig; import charactermanaj.model.ColorGroup; import charactermanaj.model.PartsCategory; import charactermanaj.model.PartsIdentifier; import charactermanaj.model.PartsSpec; /** * パーツ名の末尾が、カラーグループの表記を括弧でくくったものと同じであれば、 * そのパーツ固有のカラーグループとして設定するためのデコレータ.
* このクラス自身はパーツのロードは行わず、コンストラクタで指定したローダーによりロードを行い、 * その結果に対してカラーグループの設定を行う.
* * @author seraphy * */ public class PartsSpecDecorateLoader implements PartsDataLoader { private PartsDataLoader parent; private Collection colorGroups; /** * パーツローダとカラーグループを指定して構築する. * @param parent 元パーツローダー * @param colorGroups カラーグループのコレクション、nullの場合は空とみなす. */ public PartsSpecDecorateLoader(PartsDataLoader parent, Collection colorGroups) { if (parent == null) { throw new IllegalArgumentException(); } if (colorGroups == null) { colorGroups = Collections.emptyList(); } this.parent = parent; this.colorGroups = colorGroups; } public Map load(PartsCategory category) { Map partsSpecs = parent.load(category); decolatePartsSpec(partsSpecs); return partsSpecs; } /** * パーツ識別子の表示名に、カラーグループの表示名により判定されるパターンに合致する場合、 * パーツ設定のカラーグループを、そのカラーグループとして設定する. * @param partsSpecs パーツマップ */ protected void decolatePartsSpec(Map partsSpecs) { String templ = AppConfig.getInstance().getPartsColorGroupPattern(); if (templ == null || templ.trim().length() == 0) { // パターンが設定されていない場合は無視する. return; } // パーツ名にカラーグループが含まれる場合、それを登録する. for (ColorGroup colorGroup : colorGroups) { String pattern = templ.replace("@", colorGroup.getLocalizedName()); Pattern pat = Pattern.compile(pattern); for (PartsSpec partsSpec : partsSpecs.values()) { Matcher mat = pat.matcher(partsSpec.getPartsIdentifier() .getLocalizedPartsName()); if (mat.matches()) { partsSpec.setColorGroup(colorGroup); } } } } } charactermanaj/src/charactermanaj/model/io/AbstractCharacterDataArchivedFileWriter.java0000644000175000017500000001730611767047535031700 0ustar paulliupaulliupackage charactermanaj.model.io; import java.awt.Color; import java.awt.Dimension; import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.Writer; import java.nio.charset.Charset; import java.sql.Timestamp; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Properties; import charactermanaj.graphics.io.ImageResource; import charactermanaj.graphics.io.ImageSaveHelper; import charactermanaj.model.AppConfig; import charactermanaj.model.CharacterData; import charactermanaj.model.Layer; import charactermanaj.model.PartsCategory; import charactermanaj.model.PartsFiles; import charactermanaj.model.PartsIdentifier; import charactermanaj.model.PartsManageData; import charactermanaj.model.PartsManageDataConverter; import charactermanaj.model.PartsSet; import charactermanaj.model.PartsSpec; public abstract class AbstractCharacterDataArchivedFileWriter extends AbstractCharacterDataFileWriter { protected AbstractCharacterDataArchivedFileWriter(File outFile) throws IOException { super(outFile); } protected abstract OutputStream getOutputStream() throws IOException; protected abstract void putNextEntry(String name, long lastModified) throws IOException; protected abstract void closeEntry() throws IOException; @Override protected void internalWriteExportProp(Properties prop) throws IOException { // export prop putNextEntry("export-info.xml", 0); prop.storeToXML(getOutputStream(), "exportProp"); } @Override protected void internalWriteCharacterData(CharacterData characterData) throws IOException { CharacterDataPersistent persist = CharacterDataPersistent.getInstance(); // character.xmlの出力 putNextEntry(CharacterDataPersistent.CONFIG_FILE, 0); persist.writeXMLCharacterData(characterData, getOutputStream()); closeEntry(); // character.iniの出力 internalWriteCharacterIni(characterData); } /** * character.iniを出力します.
* @param characterData キャラクターデータ * @throws IOException 出力に失敗した場合 */ protected void internalWriteCharacterIni(CharacterData characterData) throws IOException { StringBuilder buf = new StringBuilder(); buf.append("; created by charactermanaj " + new Timestamp(System.currentTimeMillis()) + "\r\n"); buf.append("[Size]\r\n"); Dimension dim = characterData.getImageSize(); if (dim == null) { dim = new Dimension(300, 400); } buf.append("size_x=" + dim.width + "\r\n"); buf.append("size_y=" + dim.height + "\r\n"); buf.append("\r\n"); buf.append("[Parts]\r\n"); Map partsMap = new HashMap(); for (PartsCategory partsCategory : characterData.getPartsCategories()) { String categoryId = partsCategory.getCategoryId(); partsMap.put(categoryId, ""); } Map partsSets = characterData.getPartsSets(); PartsSet partsSet = partsSets.get(characterData.getDefaultPartsSetId()); if (partsSet == null && !partsSets.isEmpty()) { // デフォルトのパーツセットが指定されていない場合は、どれか1つを選択する. partsSet = partsSets.values().iterator().next(); } if (partsSet != null) { for (Map.Entry> entry : partsSet .entrySet()) { PartsCategory partsCategory = entry.getKey(); StringBuilder partsNames = new StringBuilder(); for (PartsIdentifier partsIdentifier : entry.getValue()) { if (partsNames.length() > 0) { partsNames.append(","); } partsNames.append(partsIdentifier.getPartsName()); } String categoryId = partsCategory.getCategoryId(); partsMap.put(categoryId, partsNames.toString()); } } for (PartsCategory partsCategory : characterData.getPartsCategories()) { String categoryId = partsCategory.getCategoryId(); String partsNames = partsMap.get(categoryId); buf.append(categoryId + "=" + partsNames + "\r\n"); } // 色情報はすべてダミー(character.iniは色情報を省略しても問題ないようだが、一応) buf.append("\r\n"); buf.append("[Color]\r\n"); buf.append("hair_rgb=0\r\n"); buf.append("hair_gray=0\r\n"); buf.append("eye_rgb=0\r\n"); buf.append("eye_gray=0\r\n"); buf.append("skin_rgb=0\r\n"); buf.append("skin_gray=0\r\n"); buf.append("body_rgb=0\r\n"); buf.append("body_gray=0\r\n"); // UTF16LEで出力する. internalWriteTextUTF16LE(CharacterDataPersistent.COMPATIBLE_CONFIG_NAME, buf.toString()); } @Override protected void internalWriteTextUTF16LE(String name, String contents) throws IOException { if (contents == null) { contents = ""; } // LFまたはCR改行であればCR/LF改行に変換. contents = contents.replace("\r\n", "\n"); contents = contents.replace("\r", "\n"); contents = contents.replace("\n", "\r\n"); putNextEntry(name, 0); OutputStream os = getOutputStream(); os.write((byte) 0xff); os.write((byte) 0xfe); os.flush(); Writer wr = new OutputStreamWriter(os, Charset.forName("UTF-16LE")) { @Override public void close() throws IOException { // ZipのOutputStreamをクローズしてはならないため // OutputStreamWriter自身はクローズは呼び出さない. flush(); closeEntry(); } }; try { wr.append(contents); wr.flush(); } finally { wr.close(); } } @Override protected void internalWriteSamplePicture(BufferedImage samplePicture) throws IOException { putNextEntry("preview.png", 0); ImageSaveHelper imageSaveHelper = new ImageSaveHelper(); imageSaveHelper.savePicture(samplePicture, Color.white, getOutputStream(), "image/png", null); closeEntry(); } @Override protected void internalWritePartsImages( Map partsImages) throws IOException { AppConfig appConfig = AppConfig.getInstance(); byte[] buf = new byte[appConfig.getJarTransferBufferSize()]; for (Map.Entry entry : partsImages.entrySet()) { PartsIdentifier partsIdentifier = entry.getKey(); PartsSpec partsSpec = entry.getValue(); PartsFiles partsFiles = partsSpec.getPartsFiles(); for (Map.Entry imageEntry : partsFiles.entrySet()) { Layer layer = imageEntry.getKey(); ImageResource imageResource = imageEntry.getValue(); String name = layer.getDir() + "/" + partsIdentifier.getPartsName() + ".png"; name = name.replace("//", "/"); putNextEntry(name, imageResource.lastModified()); OutputStream os = getOutputStream(); InputStream is = imageResource.openStream(); try { int rd; while ((rd = is.read(buf)) >= 0) { os.write(buf, 0, rd); } } finally { is.close(); } closeEntry(); } } } @Override protected void internalWritePartsManageData( Map partsImages) throws IOException { PartsManageDataConverter partsManageDataConverter = new PartsManageDataConverter(); for (Map.Entry entry : partsImages.entrySet()) { PartsIdentifier partsIdentifier = entry.getKey(); PartsSpec partsSpec = entry.getValue(); partsManageDataConverter.convert(partsIdentifier, partsSpec); } PartsManageData partsManageData = partsManageDataConverter.getPartsManageData(); CharacterDataPersistent persist = CharacterDataPersistent.getInstance(); putNextEntry("parts-info.xml", 0); persist.savePartsManageData(partsManageData, getOutputStream()); closeEntry(); } } charactermanaj/src/charactermanaj/model/io/PartsImageDirectoryWatchAgentThread.java0000644000175000017500000002522511767047535031101 0ustar paulliupaulliupackage charactermanaj.model.io; import java.io.File; import java.io.FileFilter; import java.net.URI; import java.util.ArrayList; import java.util.Collections; import java.util.IdentityHashMap; import java.util.LinkedList; import java.util.logging.Level; import java.util.logging.Logger; import java.util.zip.CRC32; import charactermanaj.model.AppConfig; import charactermanaj.model.CharacterData; import charactermanaj.model.Layer; import charactermanaj.model.PartsCategory; public class PartsImageDirectoryWatchAgentThread implements Runnable { /** * ロガー */ private static final Logger logger = Logger.getLogger(PartsImageDirectoryWatchAgent.class.getName()); private final CharacterData characterData; private final File baseDir; /** * リスナーとサスペンド要求マップのロックオブジェクト */ private final Object lockListeners = new Object(); /** * 監視を通知されるリスナー. */ private LinkedList listeners = new LinkedList(); /** * サスペンド要求のマップ. */ private IdentityHashMap suspendStateMap = new IdentityHashMap(); /** * ロックオブジェクト */ private final Object lock = new Object(); /** * 停止フラグ */ private volatile boolean stopFlag; /** * 監視インターバル */ private int dirWatchInterval; /** * スレッド、生成されていなければnull */ private Thread thread; /** * 監視結果1、まだ監視されていなければnull */ private volatile Long signature; /** * 監視結果2、検出されたアイテムの個数 */ private volatile int itemCount; /** * 監視結果3、検出されたアイテムの最終更新日。ただし未来の日付は除外する。 */ private volatile long maxLastModified; public PartsImageDirectoryWatchAgentThread(CharacterData characterData) { if (characterData == null) { throw new IllegalArgumentException(); } URI docBase = characterData.getDocBase(); File baseDir = null; if (docBase != null && "file".equals(docBase.getScheme())) { baseDir = new File(docBase).getParentFile(); } this.characterData = characterData; this.baseDir = baseDir; this.dirWatchInterval = AppConfig.getInstance().getDirWatchInterval(); } public CharacterData getCharcterData() { return characterData; } /** * 監視を開始する.
* 一定時間待機後、フォルダを監視し、前回監視結果と比較して変更があれば通知を行う.
* その処理をstop指示が来るまで繰り返す.
* 開始しても、まず指定時間待機するため、すぐにはディレクトリの走査は行わない.
* 且つ、初回監視結果は変更通知されないため、二回以上走査しないかぎり通知は発生しない.
* これは意図したものである.
* 明示的に初回監視を行うには明示的に{@link #reset()}を呼び出す.
*/ private void start() { logger.log(Level.FINE, "watchAgent request start. " + this); synchronized (lock) { if (thread != null && thread.isAlive()) { // すでに開始済みであれば何もしない. return; } // 書き込み禁止キャラクター定義に対する監視を無効とするか? AppConfig appConfig = AppConfig.getInstance(); if (appConfig.isDisableWatchDirIfNotWritable()) { try { if (baseDir == null || !baseDir.exists() || !baseDir.isDirectory() || !baseDir.canWrite()) { logger.log(Level.INFO, "does not monitor the directory because it is not writable." + baseDir); return; } URI docBase = characterData.getDocBase(); if (docBase != null) { File docBaseFile = new File(docBase); if (!docBaseFile.exists() || !docBaseFile.canWrite()) { logger.log(Level.INFO, "does not monitor the directory because the character.xml is not writable." + characterData); return; } } } catch (Exception ex) { logger.log(Level.WARNING, "watch-dir start failed. " + characterData, ex); return; } } // スレッドを開始する. stopFlag = false; thread = new Thread(this); thread.setDaemon(true); thread.start(); } } /** * 監視を停止する.
* @return 停止した場合はtrue、すでに停止していたか開始されていない場合はfalse */ private boolean stop() { logger.log(Level.FINE, "watchAgent request stop. " + this); boolean stopped = false; synchronized (lock) { // スレッドが生きていれば停止する. if (thread != null && thread.isAlive()) { stopFlag = true; thread.interrupt(); try { thread.join(10000); // 10Secs } catch (InterruptedException ex) { logger.log(Level.FINE, "stop request interrupted.", ex); // 抜ける } stopped = true; } // スレッドは停止されていると見なす. thread = null; } return stopped; } /** * スレッドの停止フラグがたてられるまで、一定時間待機と監視とを永久に繰り返す.
* ただし、スレッド自身はデーモンとして動作させているので他の非デーモンスレッドが存在しなくなれば停止する.
*/ public void run() { logger.log(Level.FINE, "watch-dir thead started. " + this); // 初回スキャンは無視するためリセット状態とする. this.signature = null; // スキャンループ while (!stopFlag) { try { Thread.sleep(dirWatchInterval); watch(new Runnable() { public void run() { fireWatchEvent(); } }); } catch (InterruptedException ex) { logger.log(Level.FINE, "watch-dir thead interrupted."); // 何もしない } catch (Exception ex) { logger.log(Level.SEVERE, "PartsImageDirectoryWatchAgent failed.", ex); // 何もしない. } } logger.log(Level.FINE, "watch-dir thead stopped. " + this); } /** * 監視を行う.
* 停止フラグが設定されるか、割り込みされた場合は処理を中断してInterruptedException例外を返して終了する.
* @param notifier 通知するためのオブジェクト * @throws InterruptedException 割り込みされた場合 */ protected void watch(Runnable notifier) throws InterruptedException { if (baseDir == null || !baseDir.exists() || !baseDir.isDirectory()) { return; } int itemCount = 0; long maxLastModified = 0; long now = System.currentTimeMillis() + dirWatchInterval; CRC32 crc = new CRC32(); for (PartsCategory partsCategory : characterData.getPartsCategories()) { for (Layer layer : partsCategory.getLayers()) { File watchDir = new File(baseDir, layer.getDir()); ArrayList files = new ArrayList(); if (watchDir.exists() && watchDir.isDirectory()) { for (File file : watchDir.listFiles(new FileFilter() { public boolean accept(File pathname) { return pathname.isFile() && pathname.getName().toLowerCase().endsWith(".png"); } })) { if (Thread.interrupted() || stopFlag) { throw new InterruptedException(); } itemCount++; long lastModified = file.lastModified(); if (lastModified <= now) { // 未来の日付は除外する. // 未来の日付のファイルが一つでもあると他のファイルが実際に更新されても判定できなくなるため。 maxLastModified = Math.max(maxLastModified, lastModified); } files.add(file.getName() + ":" + lastModified); }; Collections.sort(files); } for (String file : files) { crc.update(file.getBytes()); } } } long signature = crc.getValue(); if (this.signature != null) { // 初回は無視される. if (this.signature.longValue() != signature || this.itemCount != itemCount || this.maxLastModified != maxLastModified) { // ハッシュ値が異なるか、アイテム数が異なるか、最大の最終更新日が異なる場合、変更されたとみなす. if (notifier != null) { notifier.run(); } } } this.signature = Long.valueOf(signature); this.maxLastModified = maxLastModified; this.itemCount = itemCount; } /** * イベントリスナを登録する * @param l リスナ */ public void addPartsImageDirectoryWatchListener(PartsImageDirectoryWatchListener l) { if (l != null) { synchronized (lockListeners) { listeners.add(l); } changeSuspendState(); } } /** * イベントリスナを登録解除する * @param l リスナ */ public void removePartsImageDirectoryWatchListener(PartsImageDirectoryWatchListener l) { if (l != null) { synchronized (lockListeners) { listeners.remove(l); suspendStateMap.remove(l); } changeSuspendState(); } } public void suspend(PartsImageDirectoryWatchListener l) { if (l != null) { synchronized (lockListeners) { Integer cnt = suspendStateMap.get(l); if (cnt == null) { cnt = Integer.valueOf(1); } else { cnt = cnt + 1; } suspendStateMap.put(l, cnt); } changeSuspendState(); } } public void resume(PartsImageDirectoryWatchListener l) { if (l != null) { synchronized (lockListeners) { Integer cnt = suspendStateMap.get(l); if (cnt != null) { cnt = cnt - 1; if (cnt > 0) { suspendStateMap.put(l, cnt); } else { suspendStateMap.remove(l); } } } changeSuspendState(); } } protected void changeSuspendState() { boolean active; synchronized (lockListeners) { // リスナが接続されており、且つ、サスペンド要求が一つもない場合はスレッド実行可 active = !listeners.isEmpty() && suspendStateMap.isEmpty(); } if (active) { start(); } else { stop(); } } /** * イベントを通知する. */ protected void fireWatchEvent() { PartsImageDirectoryWatchListener[] listeners; synchronized (this.listeners) { listeners = this.listeners.toArray(new PartsImageDirectoryWatchListener[this.listeners.size()]); } PartsImageDirectoryWatchEvent e = new PartsImageDirectoryWatchEvent(characterData); for (PartsImageDirectoryWatchListener listener : listeners) { listener.detectPartsImageChange(e); } } } charactermanaj/src/charactermanaj/model/io/PartsImageDirectoryWatchAgent.java0000644000175000017500000000234411767047535027746 0ustar paulliupaulliupackage charactermanaj.model.io; import charactermanaj.model.CharacterData; /** * パーツファイルのディレクトリの監視を行うエージェント.
* @author seraphy */ public interface PartsImageDirectoryWatchAgent { /** * 監視対象としているキャラクター定義を取得する. * @return キャラクター定義 */ CharacterData getCharcterData(); /** * 監視を接続する.
* 接続されるまでディレクトリの監視状態は通知されない.
*/ void connect(); /** * 監視を切断する.
* 接続されるまでディレクトリの監視状態は通知されない.
*/ void disconnect(); /** * 監視対象ディレクトリの監視を停止する.
*/ void suspend(); /** * 監視対象ディレクトリの監視再開を許可する.
*/ void resume(); /** * イベントリスナを登録する * @param l リスナ */ void addPartsImageDirectoryWatchListener(PartsImageDirectoryWatchListener l); /** * イベントリスナを登録解除する * @param l リスナ */ void removePartsImageDirectoryWatchListener(PartsImageDirectoryWatchListener l); } charactermanaj/src/charactermanaj/model/io/PartsImageDirectoryWatchListener.java0000644000175000017500000000024111767047535030467 0ustar paulliupaulliupackage charactermanaj.model.io; public interface PartsImageDirectoryWatchListener { void detectPartsImageChange(PartsImageDirectoryWatchEvent e); } charactermanaj/src/charactermanaj/model/io/ImportModel.java0000644000175000017500000002567611767047535024327 0ustar paulliupaulliupackage charactermanaj.model.io; import java.awt.image.BufferedImage; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.URI; import java.util.Collection; import java.util.logging.Level; import java.util.logging.Logger; import charactermanaj.model.AppConfig; import charactermanaj.model.CharacterData; import charactermanaj.model.PartsAuthorInfo; import charactermanaj.model.PartsCategory; import charactermanaj.model.PartsManageData; import charactermanaj.model.io.AbstractCharacterDataArchiveFile.CategoryLayerPair; import charactermanaj.model.io.AbstractCharacterDataArchiveFile.PartsImageContent; import charactermanaj.ui.progress.ProgressHandle; public class ImportModel { /** * ロガー.
*/ private static final Logger logger = Logger.getLogger(ImportModel.class.getName()); /** * インポートもとファイル */ private URI importSource; /** * プロファイル先のキャラクター定義、新規の場合はnull */ private CharacterData currentCharacterData; /** * インポートもとアーカイブ. * ロードされた場合に非nullとなる. */ private CharacterDataArchiveFile archiveFile; /** * インポートされたキャラクター定義、なければnull */ private CharacterData sourceCharacterData; /** * インポートされたサンプルピクチャ、なければnull */ private BufferedImage samplePicture; /** * インポートされたreadme */ private String readme; /** * インポート先のキャラクター定義、もしくは現在のキャラクター定義のディレクトリ構成から * 読み取ることのできるパーツのコレクション、なければ空.
*/ private Collection partsImageContentsMap; /** * パーツ管理データ、なければ空 */ private PartsManageData partsManageData; public void openImportSource(URI importSource, CharacterData currentCharacterData) throws IOException { if (archiveFile != null || importSource == null) { throw new IllegalStateException("既にアーカイブがオープンされています。"); } this.importSource = importSource; this.currentCharacterData = currentCharacterData; } public void closeImportSource() throws IOException { if (archiveFile != null) { try { archiveFile.close(); } finally { // クローズに失敗しても閉じたことにする. reset(); } } } public void loadContents(ProgressHandle progressHandle) throws IOException { if (archiveFile != null) { throw new IllegalStateException("既にアーカイブがオープンされています。"); } if (importSource == null) { throw new IllegalStateException("インポートファィルが指定されていません。"); } CharacterDataFileReaderWriterFactory factory = CharacterDataFileReaderWriterFactory.getInstance(); progressHandle.setCaption("open archive..."); archiveFile = factory.openArchive(importSource); readme = archiveFile.readReadMe(); progressHandle.setCaption("search the character definition..."); sourceCharacterData = archiveFile.readCharacterData(); if (sourceCharacterData == null) { // character.xmlがない場合は、character.iniで試行する. sourceCharacterData = archiveFile.readCharacterINI(); if (sourceCharacterData != null) { // readmeがあれば、それを説明として登録しておく if (readme != null && readme.trim().length() > 0) { sourceCharacterData.setDescription(readme); } } } else { // character.xmlがあった場合、favorites.xmlもあれば読み込む. archiveFile.readFavorites(sourceCharacterData); } // サンプルピクチャの読み込み、なければnull progressHandle.setCaption("load sample picture..."); samplePicture = archiveFile.readSamplePicture(); // パーツセットの読み込み、なければ空 progressHandle.setCaption("load partssets..."); if (currentCharacterData != null) { // 既存のキャラクターセットの定義をもとにパーツディレクトリを探索する場合 // (インポートもと・インポート先が同一であればパーツは除外される.) partsImageContentsMap = archiveFile.getPartsImageContents(currentCharacterData, false); } else { // インポート元にあるキャラクター定義をもとにパーツディレクトリを探索する場合 partsImageContentsMap = archiveFile.getPartsImageContents(sourceCharacterData, true); } // パーツ管理データの読み込み progressHandle.setCaption("load parts definitions..."); partsManageData = archiveFile.getPartsManageData(); } protected void reset() { importSource = null; archiveFile = null; sourceCharacterData = null; samplePicture = null; readme = null; partsImageContentsMap = null; partsManageData = null; } public URI getImportSource() { return importSource; } protected void checkArchiveOpened() { if (archiveFile == null) { throw new IllegalStateException("アーカイブはオープンされていません。"); } } public CharacterData getCharacterData() { checkArchiveOpened(); return sourceCharacterData; } public BufferedImage getSamplePicture() { checkArchiveOpened(); return samplePicture; } public String getReadme() { checkArchiveOpened(); return readme; } public Collection getPartsImageContents() { checkArchiveOpened(); return partsImageContentsMap; } public PartsManageData getPartsManageData() { checkArchiveOpened(); return partsManageData; } /** * パーツデータをプロファイルの画像ディレクトリに一括コピーする.
* @param partsImageContents コピー対象のパーツデータ * @param cd コピー先のプロファイル * @throws IOException 失敗 */ public void copyPartsImageContents( Collection partsImageContents, CharacterData cd) throws IOException { if (cd == null || cd.getDocBase() == null) { throw new IllegalArgumentException("invalid character data"); } // コピー先ディレクトリの確定 URI docbase = cd.getDocBase(); if ( !"file".equals(docbase.getScheme())) { throw new IOException("ファイル以外はサポートしていません: " + docbase); } File configFile = new File(docbase); File baseDir = configFile.getParentFile(); if (baseDir == null || !baseDir.isDirectory()) { throw new IOException("親フォルダがディレクトリではありません: " + baseDir); } AppConfig appConfig = AppConfig.getInstance(); byte[] stmbuf = new byte[appConfig.getFileTransferBufferSize()]; // ファイルコピー for (PartsImageContent content : partsImageContents) { InputStream is = new BufferedInputStream(content.openStream()); try { File outDir = new File(baseDir, content.getDirName()); if (!outDir.exists()) { if (!outDir.mkdirs()) { logger.log(Level.WARNING, "can't create the directory. " + outDir); } } File outFile = new File(outDir, content.getFileName()); OutputStream os = new BufferedOutputStream(new FileOutputStream(outFile)); try { for (;;) { int rd = is.read(stmbuf); if (rd < 0) { break; } os.write(stmbuf, 0, rd); } } finally { os.close(); } if (!outFile.setLastModified(content.lastModified())) { logger.log(Level.WARNING, "can't change the modified-date: " + outFile); } } finally { is.close(); } } } /** * パーツ管理情報を更新または作成する.
* パーツ管理情報がnullまたは空であれば何もしない.
* そうでなければインポートするパーツに該当するパーツ管理情報を、現在のプロファイルのパーツ管理情報に追記・更新し、 * それを書き出す.
* インポートもとにパーツ管理情報がなく、既存にある場合、インポートしても管理情報は削除されません.(上書きセマンティクスのため) * @param partsImageContents インポートするパーツ * @param partsManageData パーツ管理データ * @param current 現在のパーツ管理データを保持しているプロファイル、新規の場合はnull * @param target 書き込み先のプロファイル * @throws IOException 書き込みに失敗した場合 */ public void updatePartsManageData( Collection partsImageContents, PartsManageData partsManageData, CharacterData current, CharacterData target) throws IOException { if (target == null || !target.isValid()) { throw new IllegalArgumentException(); } if (partsImageContents == null || partsImageContents.isEmpty() || partsManageData == null || partsManageData.isEmpty()) { // インポートするパーツが存在しないか、管理情報がない場合は更新しようがないので何もしないで戻る. return; } CharacterDataPersistent persist = CharacterDataPersistent.getInstance(); PartsManageData mergedPartsManagedData; if (current != null && current.isValid()) { // 現在のプロファイルからパーツ管理情報を取得する. mergedPartsManagedData = persist.loadPartsManageData(current.getDocBase()); } else { // 新規の場合は空 mergedPartsManagedData = new PartsManageData(); } // インポート対象のパーツに該当するパーツ管理情報のみを取り出して追記する. for (PartsImageContent partsImageContent : partsImageContents) { String partsName = partsImageContent.getPartsName(); for (CategoryLayerPair catLayerPair : partsImageContent.getCategoryLayerPairs()) { PartsCategory partsCategory = catLayerPair.getPartsCategory(); String categoryId = partsCategory.getCategoryId(); PartsManageData.PartsKey partsKey = new PartsManageData.PartsKey(partsName, categoryId); PartsAuthorInfo partsAuthorInfo = partsManageData.getPartsAuthorInfo(partsKey); PartsManageData.PartsVersionInfo versionInfo = partsManageData.getVersion(partsKey); String localizedName = partsManageData.getLocalizedName(partsKey); if (partsAuthorInfo != null || versionInfo != null || localizedName != null) { // いずれかの情報の登録がある場合、パーツ管理情報として追記する. mergedPartsManagedData.putPartsInfo(partsKey, localizedName, partsAuthorInfo, versionInfo); } } } // パーツ管理情報を更新する. persist.savePartsManageData(target.getDocBase(), mergedPartsManagedData); } } charactermanaj/src/charactermanaj/model/io/AbstractCharacterDataFileWriter.java0000644000175000017500000001213011767047535030220 0ustar paulliupaulliupackage charactermanaj.model.io; import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; import java.util.Map; import java.util.Properties; import java.util.logging.Level; import java.util.logging.Logger; import charactermanaj.model.CharacterData; import charactermanaj.model.PartsIdentifier; import charactermanaj.model.PartsSpec; public abstract class AbstractCharacterDataFileWriter implements CharacterDataWriter { /** * ロガー */ private static final Logger logger = Logger.getLogger(AbstractCharacterDataFileWriter.class.getName()); protected File outFile; protected File tmpFile; protected Exception occureException; protected AbstractCharacterDataFileWriter(File outFile) throws IOException { if (outFile == null) { throw new IllegalArgumentException(); } if (outFile.exists()) { if (!outFile.canWrite()) { throw new IOException("not writable: " + outFile); } } File tmpFile = new File(outFile.getPath() + ".tmp"); this.tmpFile = tmpFile; this.outFile = outFile; } public void writeExportProp(Properties prop) throws IOException { if (prop == null) { throw new IllegalArgumentException(); } try { internalWriteExportProp(prop); } catch (IOException ex) { occureException = ex; throw ex; } catch (Exception ex) { occureException = ex; IOException ex2 = new IOException("write characterdata failed."); ex2.initCause(ex); throw ex2; } } protected abstract void internalWriteExportProp(Properties prop) throws IOException; public void writeCharacterData(CharacterData characterData) throws IOException { if (characterData == null) { throw new IllegalArgumentException(); } try { internalWriteCharacterData(characterData); } catch (IOException ex) { occureException = ex; throw ex; } catch (Exception ex) { occureException = ex; IOException ex2 = new IOException("write characterdata failed."); ex2.initCause(ex); throw ex2; } } protected abstract void internalWriteCharacterData(CharacterData characterData) throws IOException; public void writeTextUTF16LE(String name, String contents) throws IOException { if (name == null) { throw new IllegalArgumentException(); } try { internalWriteTextUTF16LE(name, contents); } catch (IOException ex) { occureException = ex; throw ex; } catch (Exception ex) { occureException = ex; IOException ex2 = new IOException("internalWriteTextUTF16 failed."); ex2.initCause(ex); throw ex2; } } protected abstract void internalWriteTextUTF16LE(String name, String contents) throws IOException; public void writeSamplePicture(BufferedImage samplePicture) throws IOException { if (samplePicture == null) { throw new IllegalArgumentException(); } try { internalWriteSamplePicture(samplePicture); } catch (IOException ex) { occureException = ex; throw ex; } catch (Exception ex) { occureException = ex; IOException ex2 = new IOException("write sample picture failed."); ex2.initCause(ex); throw ex2; } } protected abstract void internalWriteSamplePicture(BufferedImage samplePicture) throws IOException; public void writePartsImages(Map partsImages) throws IOException { if (partsImages == null) { throw new IllegalArgumentException(); } try { internalWritePartsImages(partsImages); } catch (IOException ex) { occureException = ex; throw ex; } catch (Exception ex) { occureException = ex; IOException ex2 = new IOException("write parts images failed."); ex2.initCause(ex); throw ex2; } } protected abstract void internalWritePartsImages(Map partsImages) throws IOException; public void writePartsManageData(Map partsImages) throws IOException { if (partsImages == null) { throw new IllegalArgumentException(); } try { internalWritePartsManageData(partsImages); } catch (IOException ex) { occureException = ex; throw ex; } catch (Exception ex) { occureException = ex; IOException ex2 = new IOException("write parts images failed."); ex2.initCause(ex); throw ex2; } } protected abstract void internalWritePartsManageData(Map partsImages) throws IOException; public void close() throws IOException { try { internalClose(); if (outFile.exists()) { if (!outFile.delete()) { throw new IOException("old file can't delete. " + outFile); } } } catch (Exception ex) { if (occureException == null) { occureException = ex; } } if (occureException != null) { if (!tmpFile.delete()) { logger.log(Level.WARNING, "temporary file can't delete. " + tmpFile); } return; } if (!tmpFile.renameTo(outFile)) { throw new IOException("rename failed. " + tmpFile); } } protected abstract void internalClose() throws IOException; } charactermanaj/src/charactermanaj/model/io/CharacterDataArchiveFile.java0000644000175000017500000001103411767047535026643 0ustar paulliupaulliupackage charactermanaj.model.io; import java.awt.image.BufferedImage; import java.io.IOException; import java.util.Collection; import charactermanaj.model.CharacterData; import charactermanaj.model.PartsManageData; import charactermanaj.model.io.AbstractCharacterDataArchiveFile.PartsImageContent; /** * アーカイブ形式のファイルを読み取るためのインターフェイス.
* @author seraphy */ public interface CharacterDataArchiveFile { /** * クローズする. * @throws IOException */ void close() throws IOException; /** * コンテンツルートへのプレフィックスを取得する.
* アーカイブの実際のルートに単一のフォルダしかない場合、そのフォルダがルートプレフィックスとなる.
* アーカイブの実際のルートに複数のフォルダがあるか、ファイルがある場合、ルートプレフィックスは空文字である.
* @return コンテンツルートへのプレフィックス */ String getRootPrefix(); /** * 指定したコンテンツが存在するか? * @param name コンテンツ名 * @return 存在すればtrue、存在しなければfalse */ boolean hasContent(String name); /** * キャラクター定義を読み込む.
* アーカイブに存在しなければnull * @return キャラクター定義、もしくはnull * @throws IOException 読み取りに失敗した場合 */ CharacterData readCharacterData() throws IOException; /** * お気に入りを読み込みキャラクター定義に追加する.
* アーカイブにお気に入りが存在しなければ何もしない.
* @param characterData キャラクター定義(お気に入りが追加される) * @throws IOException 読み取りに失敗した場合 */ void readFavorites(CharacterData characterData) throws IOException; /** * キャラクター定義をINIファイルより読み取る.
* character.iniが存在しなければnull.
* 「キャラクターなんとか機」の設定ファイルを想定している.
* @return キャラクター定義、もしくはnull * @throws IOException 読み取りに失敗した場合 */ CharacterData readCharacterINI() throws IOException; /** * サンプルピクチャを読み込む.
* アーカイブに存在しなければnull. * @return サンプルピクチャ、もしくはnull * @throws IOException 読み取りに失敗した場合 */ BufferedImage readSamplePicture() throws IOException; /** * アーカイブにある「readme.txt」、もしくは「readme」というファイルをテキストファイルとして読み込む.
* readme.txtが優先される。ともに存在しない場合はnull. * @return テキスト、もしくはnull * @throws 読み込みに失敗した場合 */ String readReadMe() throws IOException; /** * ファイルをテキストとして読み取り、返す.
* UTF-16LE/BE/UTF-8についてはBOMにより判定する.
* BOMがない場合はUTF-16/8ともに判定できない.
* BOMがなければMS932もしくはEUC_JPであると仮定して読み込む.
* @param name コンテンツ名 * @return テキスト、コンテンツが存在しない場合はnull * @throws IOException 読み込みに失敗した場合 */ String readTextFile(String name) throws IOException; /** * アーカイブに含まれる、キャラクター定義と同じフォルダをもつpngファイルからパーツイメージを取得する.
* パーツの探索は引数で指定したキャラクター定義によって行われます.
* @param characterData インポート先のキャラクターデータ、フォルダ名などを判別するため。nullの場合は空のマップを返す.
* @param newly 新規インポート用であるか?(新規でない場合は引数で指定したキャラクターセットと同じパーツは読み込まれない) * @return パーツイメージコンテンツのコレクション、なければ空 */ Collection getPartsImageContents(CharacterData characterData, boolean newly); /** * アーカイブに含まれるparts-info.xmlを読み込み返す.
* 存在しなければ空のインスタンスを返す.
* @return パーツ管理データ * @throws IOException */ PartsManageData getPartsManageData() throws IOException; } charactermanaj/src/charactermanaj/model/io/CharacterDataZipArchiveFile.java0000644000175000017500000000257311767047535027336 0ustar paulliupaulliupackage charactermanaj.model.io; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.util.Enumeration; import org.apache.tools.zip.ZipEntry; import org.apache.tools.zip.ZipFile; import charactermanaj.model.AppConfig; public class CharacterDataZipArchiveFile extends AbstractCharacterDataArchiveFile { protected ZipFile zipFile; protected class ZipFileContent implements FileContent { private ZipEntry entry; protected ZipFileContent(ZipEntry entry) { this.entry = entry; } public String getEntryName() { return entry.getName(); } public long lastModified() { return entry.getTime(); } public InputStream openStream() throws IOException { return zipFile.getInputStream(entry); } } public void close() throws IOException { zipFile.close(); } public CharacterDataZipArchiveFile(File file) throws IOException { super(file); AppConfig appConfig = AppConfig.getInstance(); String encoding = appConfig.getZipNameEncoding(); zipFile = new ZipFile(file, encoding); load(); } private void load() { @SuppressWarnings("unchecked") Enumeration enm = zipFile.getEntries(); while (enm.hasMoreElements()) { ZipEntry entry = enm.nextElement(); addEntry(new ZipFileContent(entry)); } searchRootPrefix(); } } charactermanaj/src/charactermanaj/model/io/PartsImageDirectoryWatchAgentFactory.java0000644000175000017500000001422411767047535031276 0ustar paulliupaulliupackage charactermanaj.model.io; import java.net.URI; import java.util.HashMap; import java.util.LinkedList; import java.util.logging.Level; import java.util.logging.Logger; import charactermanaj.model.CharacterData; /** * ディレクトリ監視エージェントのファクトリ.
* このファクトリ単位でディレクト利監視エージェントは保持されます.
* このファクトリ上に、すでに同一のキャラクターデータを監視するエージェントがいる場合、それらのエージェントの実体は * ハンドルによって共有されます.
* @author seraphy */ public final class PartsImageDirectoryWatchAgentFactory { private final Logger logger = Logger.getLogger(getClass().getName()); private static PartsImageDirectoryWatchAgentFactory inst = new PartsImageDirectoryWatchAgentFactory(); private HashMap agents = new HashMap(); private final Object lock = new Object(); private PartsImageDirectoryWatchAgentFactory() { super(); } public static PartsImageDirectoryWatchAgentFactory getFactory() { return inst; } /** * キャラクターデータを指定して、そのキャラクターデータに対する監視エージェントを作成し、そのハンドルを返します.
* すでに、そのキャラクターデータに対するエージェントが存在する場合は、そのハンドルを返します.
* 無効なキャラクターデータ、もしくはnullを指定した場合は、エージェントの実体を持たないダミーのハンドルが返されます.
* @param characterData キャラクターデータ * @return 作成された、もしくは既に作成されている監視エージェントのハンドル */ public PartsImageDirectoryWatchAgent getAgent(CharacterData characterData) { if (characterData == null || !characterData.isValid()) { // キャラクターデータがnullまたは無効である場合は、 // 何もしないダミーのディレクトリハンドルを返す. return new NullWatchAgentHandle(characterData); } URI docBase = characterData.getDocBase(); PartsImageDirectoryWatchAgentThread agentImpl; synchronized (lock) { if (agents.containsKey(docBase)) { agentImpl = agents.get(docBase); } else { agentImpl = new PartsImageDirectoryWatchAgentThread(characterData); agents.put(docBase, agentImpl); logger.log(Level.FINE, "watch agent is cached. " + agentImpl); } } return new PartsImageDirectoryWatchAgentHandle(agentImpl); } } /** * 監視スレッドの実体がない、何もしないハンドル. * @author seraphy */ class NullWatchAgentHandle implements PartsImageDirectoryWatchAgent, PartsImageDirectoryWatchListener { private CharacterData characterData; public NullWatchAgentHandle(CharacterData characterData) { this.characterData = characterData; } public void addPartsImageDirectoryWatchListener( PartsImageDirectoryWatchListener l) { } // なにもしない. public void connect() { // なにもしない. } public void detectPartsImageChange(PartsImageDirectoryWatchEvent e) { // なにもしない. } public void disconnect() { // なにもしない. } public CharacterData getCharcterData() { return characterData; } public void removePartsImageDirectoryWatchListener( PartsImageDirectoryWatchListener l) { // なにもしない. } public void resume() { // なにもしない. } public void suspend() { // なにもしない. } } /** * 監視ディレクトリに対する監視スレッドの実体を複数のメインフレームで共有するための、個々のフレーム用のハンドル.
* @author seraphy */ class PartsImageDirectoryWatchAgentHandle implements PartsImageDirectoryWatchAgent, PartsImageDirectoryWatchListener { /** * ロガー */ private final Logger logger = Logger.getLogger(getClass().getName()); /** * 監視スレッドの実体 */ private final PartsImageDirectoryWatchAgentThread agent; /** * 監視を通知されるリスナー */ private final LinkedList listeners = new LinkedList(); /** * このハンドルで開始要求されているか示すフラグ. */ private boolean connected; protected PartsImageDirectoryWatchAgentHandle(PartsImageDirectoryWatchAgentThread agent) { this.agent = agent; } public CharacterData getCharcterData() { return agent.getCharcterData(); } public void connect() { logger.log(Level.FINE, "agent connect request. " + this); if ( !connected) { agent.addPartsImageDirectoryWatchListener(this); connected = true; } } public void disconnect() { logger.log(Level.FINE, "agent disconnect request. " + this); if (connected) { connected = false; agent.removePartsImageDirectoryWatchListener(this); } } public void suspend() { logger.log(Level.FINE, "agent stop request. " + this); agent.suspend(this); } public void resume() { logger.log(Level.FINE, "agent resume request. " + this); agent.resume(this); } public void addPartsImageDirectoryWatchListener( PartsImageDirectoryWatchListener l) { if (l != null) { synchronized (listeners) { listeners.add(l); } } } public void removePartsImageDirectoryWatchListener( PartsImageDirectoryWatchListener l) { if (l != null) { synchronized (listeners) { listeners.remove(l); } } } public void detectPartsImageChange(PartsImageDirectoryWatchEvent e) { if (!connected) { logger.log(Level.FINE, "skip agent event. " + this); return; } logger.log(Level.FINE, "agent event occured. " + this); PartsImageDirectoryWatchListener[] ls; synchronized (listeners) { ls = listeners.toArray(new PartsImageDirectoryWatchListener[listeners.size()]); } for (PartsImageDirectoryWatchListener l : ls) { l.detectPartsImageChange(e); } } } charactermanaj/src/charactermanaj/model/io/CharacterDataJarFileWriter.java0000644000175000017500000000215011767047535027172 0ustar paulliupaulliupackage charactermanaj.model.io; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.util.jar.JarEntry; import java.util.jar.JarOutputStream; public class CharacterDataJarFileWriter extends AbstractCharacterDataArchivedFileWriter { protected JarOutputStream jarOutStm; public CharacterDataJarFileWriter(File outFile) throws IOException { super(outFile); this.jarOutStm = new JarOutputStream( new BufferedOutputStream(new FileOutputStream(tmpFile))); } @Override protected void closeEntry() throws IOException { jarOutStm.closeEntry(); } @Override protected OutputStream getOutputStream() throws IOException { return jarOutStm; } @Override protected void putNextEntry(String name, long lastModified) throws IOException { JarEntry entry = new JarEntry(name); if (lastModified > 0) { entry.setTime(lastModified); } jarOutStm.putNextEntry(entry); } protected void internalClose() throws IOException { jarOutStm.close(); } } charactermanaj/src/charactermanaj/model/io/CharacterDataFileReaderWriterFactory.java0000644000175000017500000000475511767047535031225 0ustar paulliupaulliupackage charactermanaj.model.io; import java.io.File; import java.io.IOException; import java.net.URI; public class CharacterDataFileReaderWriterFactory { private static final CharacterDataFileReaderWriterFactory singleton = new CharacterDataFileReaderWriterFactory(); private CharacterDataFileReaderWriterFactory() { super(); } public static CharacterDataFileReaderWriterFactory getInstance() { return singleton; } /** * ファイルの拡張子に応じてzip/cmj形式でのライターを構築して帰します.
* 拡張子がjarとcmjは同じ意味で、ともにjarファイル形式となります.
* zip/cmj/jar以外の拡張子はIOExceptionとなります.
* @param outfile 出力先ファイル名 * @return ライター * @throws IOException 該当するライターがみつからない場合 */ public CharacterDataWriter createWriter(File outfile) throws IOException { if (outfile == null) { throw new IllegalArgumentException(); } String name = outfile.getName().toLowerCase(); if (name.endsWith(".jar") || name.endsWith(".cmj")) { return new CharacterDataJarFileWriter(outfile); } else if (name.endsWith(".zip")) { return new CharacterDataZipFileWriter(outfile); } throw new IOException("unsupported file type: " + name); } public CharacterDataArchiveFile openArchive(URI archiveFile) throws IOException { if (archiveFile == null) { throw new IllegalArgumentException(); } if ("file".equals(archiveFile.getScheme())) { // ファイルまたはディレクトリの場合 File file = new File(archiveFile); return openArchive(file); } // file以外は現在のところサポートしない。 throw new UnsupportedOperationException(); } public CharacterDataArchiveFile openArchive(File archiveFile) throws IOException { if (archiveFile == null) { throw new IllegalArgumentException(); } if (archiveFile.exists() && archiveFile.isDirectory()) { // ディレクトリの場合 return new CharacterDataDirectoryFile(archiveFile); } // zipまたはcmjファイルの場合 String name = archiveFile.getName().toLowerCase(); if (name.endsWith(".jar") || name.endsWith(".cmj")) { return new CharacterDataJarArchiveFile(archiveFile); } else if (name.endsWith(".zip")) { return new CharacterDataZipArchiveFile(archiveFile); } throw new IOException("unsupported file type: " + name); } } charactermanaj/src/charactermanaj/model/io/PartsManageDataDecorateLoader.java0000644000175000017500000000767311767047535027663 0ustar paulliupaulliupackage charactermanaj.model.io; import java.util.HashMap; import java.util.Map; import charactermanaj.model.PartsAuthorInfo; import charactermanaj.model.PartsCategory; import charactermanaj.model.PartsIdentifier; import charactermanaj.model.PartsManageData; import charactermanaj.model.PartsSpec; /** * パーツ管理情報をパーツデータに補填するデコレータ・ローダー.
* ローダから読み込まれたパーツデータに対して、それに該当する管理情報があれば、その管理情報で更新し、 * 更新した結果を返す.
* @author seraphy */ public class PartsManageDataDecorateLoader implements PartsDataLoader { /** * パーツ管理情報を構築するファクトリ.
* ローダーが呼び出された段階で、最新のパーツ管理情報をロードするためのもの.
* @author seraphy * */ public interface PartsManageDataFactory { /** * パーツ管理情報をロードする.
* パーツ管理情報が存在しない場合、もしくは読み取りできなった場合は管理情報はないものとして空のインスタンスを返す.
* @return バーツ管理情報、存在しない場合は空を返す.
*/ PartsManageData createPartsManageData(); } private PartsDataLoader parent; private PartsManageDataFactory partsManageDataFactory; public PartsManageDataDecorateLoader(PartsDataLoader parent, PartsManageDataFactory partsManageDataFactory) { if (parent == null || partsManageDataFactory == null) { throw new IllegalArgumentException(); } this.parent = parent; this.partsManageDataFactory = partsManageDataFactory; } public Map load(PartsCategory category) { // 親よりパーツデータをロードする. Map partsSpecs = parent.load(category); // 最新のパーツ管理情報をロードする. PartsManageData partsManageData = partsManageDataFactory.createPartsManageData(); if (partsManageData == null || partsManageData.isEmpty()) { // パーツ管理情報がnullであるか空であれば、元のまま何もせず返す. return partsSpecs; } // 返却用 HashMap newPartsSpecs = new HashMap(); // パーツ管理データからのデータを補填する. for (Map.Entry entry : partsSpecs.entrySet()) { PartsIdentifier orgPartsId = entry.getKey(); PartsIdentifier newPartsId; PartsSpec partsSpec = entry.getValue(); PartsManageData.PartsKey partsKey = new PartsManageData.PartsKey(orgPartsId); // ローカライズ名が指定されているか? String localizedName = partsManageData.getLocalizedName(partsKey); if (localizedName != null && localizedName.length() > 0 && !localizedName.equals(orgPartsId.getLocalizedPartsName())) { // ローカライズ名が指定されており、且つ、元のローカライズ名と異なる場合 newPartsId = orgPartsId.setLocalizedPartsName(localizedName); } else { // ローカライズ名の変更なし newPartsId = orgPartsId; } // パーツ作者情報があれば設定する. PartsAuthorInfo partsAuthorInfo = partsManageData.getPartsAuthorInfo(partsKey); if (partsAuthorInfo != null) { partsSpec.setAuthorInfo(partsAuthorInfo); } // パーツのバージョンとダウンロード情報があれば設定する. PartsManageData.PartsVersionInfo versionInfo = partsManageData.getVersion(partsKey); if (versionInfo != null) { double version = versionInfo.getVersion(); String downloadURL = versionInfo.getDownloadURL(); partsSpec.setVersion(version); partsSpec.setDownloadURL(downloadURL); } newPartsSpecs.put(newPartsId, partsSpec); } return newPartsSpecs; } } charactermanaj/src/charactermanaj/model/io/CharacterDataPersistent.java0000644000175000017500000032327211767047535026634 0ustar paulliupaulliupackage charactermanaj.model.io; import java.awt.Color; import java.awt.Dimension; import java.awt.image.BufferedImage; import java.io.BufferedReader; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileFilter; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.ObjectInputStream; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.UnsupportedEncodingException; import java.net.URI; import java.net.URL; import java.net.URLConnection; import java.nio.charset.Charset; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.NoSuchElementException; import java.util.logging.Level; import java.util.logging.Logger; import javax.xml.XMLConstants; import javax.xml.namespace.NamespaceContext; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import javax.xml.parsers.SAXParser; import javax.xml.parsers.SAXParserFactory; import javax.xml.transform.OutputKeys; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerConfigurationException; import javax.xml.transform.TransformerException; import javax.xml.transform.TransformerFactory; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; import javax.xml.validation.Schema; import javax.xml.validation.SchemaFactory; import javax.xml.xpath.XPath; import javax.xml.xpath.XPathConstants; import javax.xml.xpath.XPathExpression; import javax.xml.xpath.XPathExpressionException; import javax.xml.xpath.XPathFactory; import org.w3c.dom.Attr; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.xml.sax.Attributes; import org.xml.sax.ErrorHandler; import org.xml.sax.SAXException; import org.xml.sax.SAXParseException; import org.xml.sax.helpers.DefaultHandler; import charactermanaj.graphics.filters.ColorConv; import charactermanaj.graphics.filters.ColorConvertParameter; import charactermanaj.graphics.io.FileImageResource; import charactermanaj.graphics.io.ImageLoader; import charactermanaj.graphics.io.ImageSaveHelper; import charactermanaj.graphics.io.LoadedImage; import charactermanaj.model.AppConfig; import charactermanaj.model.CharacterData; import charactermanaj.model.ColorGroup; import charactermanaj.model.ColorInfo; import charactermanaj.model.Layer; import charactermanaj.model.PartsAuthorInfo; import charactermanaj.model.PartsCategory; import charactermanaj.model.PartsColorInfo; import charactermanaj.model.PartsIdentifier; import charactermanaj.model.PartsManageData; import charactermanaj.model.PartsSet; import charactermanaj.model.PartsManageData.PartsKey; import charactermanaj.model.RecommendationURL; import charactermanaj.ui.MainFrame; import charactermanaj.util.DirectoryConfig; import charactermanaj.util.FileUserData; import charactermanaj.util.UserData; import charactermanaj.util.UserDataFactory; public class CharacterDataPersistent { /** * キャラクター定義ファイル名 */ public static final String CONFIG_FILE = "character.xml"; /** * キャラクターなんとか機用のiniファイル名 */ public static final String COMPATIBLE_CONFIG_NAME = "character.ini"; /** * ロガー */ private static final Logger logger = Logger.getLogger(CharacterDataPersistent.class.getName()); /** * サンプルイメージファイル名 */ public static final String SAMPLE_IMAGE_FILENAME = "preview.png"; /** * キャラクター定義バージョン */ public static final String VERSION_SIG_1_0 = "1.0"; /** * キャラクター定義XMLファイルの名前空間 */ public static final String NS = "http://charactermanaj.sourceforge.jp/schema/charactermanaj"; /** * パーツ定義XMLファイルの名前空間 */ public static final String NS_PARTSDEF = "http://charactermanaj.sourceforge.jp/schema/charactermanaj-partsdef"; /** * デフォルトのキャラクターデータのキャッシュ.
* リソース内にあるため、一度読み込んだら変更されることはないのでキャッシュしておく.
*/ private CharacterData defaultCharacterData; /** * キャラクター定義XML用のスキーマ定義リソース名 */ private static final String CHARACTER_XML_SCHEMA = "/schema/character.xsd"; /** * キャラクター定義XML用のスキーマ定義リソース名 */ private static final String CHARACTER_XML_SCHEMA_0_8 = "/schema/0.8/character.xsd"; /** * パーツセット定義XMLのスキーマ定義リソース名 */ private static final String PARTSSET_XML_SCHEMA = "/schema/partsset.xsd"; /** * パーツセット定義XMLのスキーマ定義リソース名 */ private static final String PARTSSET_XML_SCHEMA_0_8 = "/schema/0.8/partsset.xsd"; /** * リソースに格納されているデフォルトのキャラクター定義.
*/ public static final String DEFAULT_CHARACTER_XML = "/schema/character.xml"; /** * リソースに格納されているデフォルトのキャラクター定義のシリアライズデータ.
*/ public static final String DEFAULT_CHARACTER_XML_SER = "/schema/character.xml.ser"; /** * XMLのデータ形式.
* SchemaのバリデージョンチェックとDOMの解析を始める前にSAXで流し込んで、 * 最初のエレメント名や使用している名前空間、バージョンを読み込んで、 * スキーマをきりかえられるようにするためのもの. * @author seraphy */ public static class DocInfo { private String firstElementName; private String version; private String namespace; public void setFirstElementName(String firstElementName) { this.firstElementName = firstElementName; } /** * 最初の要素のqName * @return */ public String getFirstElementName() { return firstElementName; } public void setNamespace(String namespace) { this.namespace = namespace; } public void setVersion(String version) { this.version = version; } /** * 最初の要素に指定されているxmlns属性の値 * @param namespace */ public String getNamespace() { return namespace; } /** * 最初の要素に指定されているversion属性の値 * @return */ public String getVersion() { return version; } @Override public String toString() { return firstElementName + " /version: " + version + " /namespace:" + namespace; } } /** * プロファイルの列挙時のエラーハンドラ.
* @author seraphy */ public interface ProfileListErrorHandler { /** * エラーが発生したことを通知される * @param baseDir 読み込み対象のXMLのファイル * @param ex 例外 */ void occureException(File baseDir, Throwable ex); } /** * プロファイル列挙時のデフォルトのエラーハンドラ.
* 標準エラー出力にメッセージをプリントするだけで何もしない.
*/ public static final ProfileListErrorHandler DEFAULT_ERROR_HANDLER = new ProfileListErrorHandler() { public void occureException(File baseDir, Throwable ex) { logger.log(Level.WARNING, "invalid profile. :" + baseDir, ex); } }; /** * スキーマのキャッシュ. */ private HashMap schemaMap = new HashMap(); /** * JAXPで使用するデフォルトのエラーハンドラ */ private static final ErrorHandler errorHandler = new ErrorHandler() { public void error(SAXParseException exception) throws SAXException { throw exception; } public void fatalError(SAXParseException exception) throws SAXException { throw exception; } public void warning(SAXParseException exception) throws SAXException { throw exception; } }; /** * プライベートコンストラクタ.
* シングルトン実装であるため、一度だけ呼び出される. */ private CharacterDataPersistent() { super(); } /** * シングルトン */ private static final CharacterDataPersistent singleton = new CharacterDataPersistent(); /** * インスタンスを取得する * @return インスタンス */ public static CharacterDataPersistent getInstance() { return singleton; } /** * キャラクターデータを新規に保存する.
* REVがnullである場合は保存に先立ってランダムにREVが設定される.
* 保存先ディレクトリはユーザー固有のキャラクターデータ保存先のディレクトリにキャラクター定義のIDを基本とする * ディレクトリを作成して保存される.
* ただし、そのディレクトリがすでに存在する場合はランダムな名前で決定される.
* 実際のxmlの保存先にあわせてDocBaseが設定されて返される.
* @param characterData キャラクターデータ (IDは設定済みであること.それ以外はvalid, editableであること。) * @throws IOException 失敗 */ public void createProfile(CharacterData characterData) throws IOException { if (characterData == null) { throw new IllegalArgumentException(); } String id = characterData.getId(); if (id == null || id.trim().length() == 0) { throw new IOException("missing character-id:" + characterData); } // ユーザー個別のキャラクターデータ保存先ディレクトリを取得 DirectoryConfig dirConfig = DirectoryConfig.getInstance(); File charactersDir = dirConfig.getCharactersDir(); if (!charactersDir.exists()) { if (!charactersDir.mkdirs()) { throw new IOException("can't create the characters directory. " + charactersDir); } } if (logger.isLoggable(Level.FINE)) { logger.log(Level.FINE, "check characters-dir: " + charactersDir + ": exists=" + charactersDir.exists()); } // 新規に保存先ディレクトリを作成. // 同じ名前のディレクトリがある場合は日付+連番をつけて衝突を回避する File baseDir = null; String suffix = ""; String name = characterData.getName(); if (name == null) { // 表示名が定義されていなければIDで代用する.(IDは必須) name = characterData.getId(); } for (int retry = 0;;retry++) { baseDir = new File(charactersDir, name + suffix); if (!baseDir.exists()) { break; } if (retry > 100) { throw new IOException("character directory conflict.:" + baseDir); } // 衝突回避の末尾文字を設定 suffix = generateSuffix(retry); } if ( !baseDir.exists()) { if ( !baseDir.mkdirs()) { throw new IOException("can't create directory. " + baseDir); } logger.log(Level.INFO, "create character-dir: " + baseDir); } // 保存先を確認 File characterPropXML = new File(baseDir, CONFIG_FILE); if (characterPropXML.exists() && !characterPropXML.isFile()) { throw new IOException("character.xml is not a regular file.:" + characterPropXML); } if (characterPropXML.exists() && !characterPropXML.canWrite()) { throw new IOException("character.xml is not writable.:" + characterPropXML); } // DocBaseを実際の保存先に更新 URI docBase = characterPropXML.toURI(); characterData.setDocBase(docBase); // リビジョンが指定されてなければ新規にリビジョンを割り当てる。 if (characterData.getRev() == null) { characterData.setRev(generateRev()); } // 保存する. saveCharacterDataToXML(characterData); // シリアライズ形式でも保存する try { saveCharacterDataToSer(characterData); } catch (Exception ex) { // シリアライズに失敗しても無視する. } // ディレクトリを準備する preparePartsDir(characterData); } /** * リビジョンを生成して返す. * @return リビジョン用文字列 */ public String generateRev() { SimpleDateFormat fmt = new SimpleDateFormat("yyyy-MM-dd_HHmmss"); return fmt.format(new Date()); } /** * 衝突回避用の末尾文字を生成する. * @param retryCount リトライ回数 * @return 末尾文字 */ protected String generateSuffix(int retryCount) { SimpleDateFormat fmt = new SimpleDateFormat("yyyy-MM-dd_HHmmss"); String suffix = "_" + fmt.format(new Date()); if (retryCount > 0) { suffix = suffix + "_" + retryCount; } return suffix; } /** * キャラクターデータを更新する. * @param characterData キャラクターデータ(有効かつ編集可能であること) * @throws IOException 失敗 */ public void updateProfile(CharacterData characterData) throws IOException { if (characterData == null) { throw new IllegalArgumentException(); } characterData.checkWritable(); if (!characterData.isValid()) { throw new IOException("invalid profile: " + characterData); } // 保存する saveCharacterDataToXML(characterData); // シリアライズ形式でも保存する. try { saveCharacterDataToSer(characterData); } catch (Exception ex) { // シリアライズに失敗したも無視する. } // ディレクトリを準備する preparePartsDir(characterData); } /** * キャラクターデータのパーツイメージを保存するディレクトリを準備する * @param characterData キャラクターデータ * @param baseDir ベースディレクトリ * @throws IOException 失敗 */ protected void preparePartsDir(CharacterData characterData) throws IOException { if (characterData == null) { throw new IllegalArgumentException(); } characterData.checkWritable(); if (!characterData.isValid()) { throw new IOException("invalid profile: " + characterData); } URI docBase = characterData.getDocBase(); if (!"file".equals(docBase.getScheme())) { throw new IOException("ファイル以外はサポートしていません。:" + docBase); } File docBaseFile = new File(docBase); File baseDir = docBaseFile.getParentFile(); if (!baseDir.exists()) { if (!baseDir.mkdirs()) { throw new IOException("can't create directory. " + baseDir); } } for (PartsCategory partsCategory : characterData.getPartsCategories()) { for (Layer layer : partsCategory.getLayers()) { File layerDir = new File(baseDir, layer.getDir()); if ( !layerDir.exists()) { if ( !layerDir.mkdirs()) { throw new IOException("can't create directory. " + layerDir); } } } } } /** * プロファイルを列挙する.
* 読み取りに失敗した場合はエラーハンドラに通知されるが例外は返されない.
* 一つも正常なプロファイルがない場合は空のリストが返される.
* @param errorHandler エラーハンドラ、不要ならばnull * @return プロファイルのリスト(表示名順)、もしくは空 */ public List listProfiles(ProfileListErrorHandler errorHandler) { DirectoryConfig dirConfig = DirectoryConfig.getInstance(); File[] baseDirs = { dirConfig.getCharactersDir(), }; ArrayList profiles = new ArrayList(); for (File baseDir : baseDirs) { if (baseDir == null || !baseDir.exists() || !baseDir.isDirectory()) { continue; } for (File dir : baseDir.listFiles(new FileFilter() { public boolean accept(File pathname) { boolean accept = pathname.isDirectory() && !pathname.getName().startsWith("."); if (accept) { File configFile = new File(pathname, CONFIG_FILE); accept = configFile.exists() && configFile.canRead(); } return accept; } })) { File characterDataXml = new File(dir, CONFIG_FILE); if (characterDataXml.exists()) { try { File docBaseFile = new File(dir, CONFIG_FILE); URI docBase = docBaseFile.toURI(); CharacterData characterData = loadProfile(docBase); profiles.add(characterData); } catch (Exception ex) { if (errorHandler != null) { errorHandler.occureException(dir, ex); } } } } } Collections.sort(profiles, CharacterData.SORT_DISPLAYNAME); return Collections.unmodifiableList(profiles); } /** * character.xmlのキャッシュファイルの位置を取得する. * @param docBase * @return character.xmlのシリアライズデータを格納するUserData */ protected UserData getCharacterDataCacheUserFile(URI docBase) { if (docBase == null) { throw new IllegalArgumentException(); } String name = new File(docBase).getName(); UserDataFactory userDataFactory = UserDataFactory.getInstance(); return userDataFactory.getMangledNamedUserData(docBase, name + "-cache.ser"); } public CharacterData loadProfile(URI docBase) throws IOException { if (docBase == null) { throw new IllegalArgumentException(); } // docBaseの最終更新日を取得する. long lastModified; URL docBaseURL = docBase.toURL(); URLConnection conn = docBaseURL.openConnection(); try { lastModified = conn.getLastModified(); } finally { // コネクションを閉じる. conn.getInputStream().close(); conn = null; } // XML解析済みの中間ファイルがあれば、それをデシリアライズする. UserData serializedFile = getCharacterDataCacheUserFile(docBase); if (serializedFile.exists()) { // 更新日がxmlと等しいか、より新しい場合のみ有効とする. if (lastModified > 0 && serializedFile.lastModified() >= lastModified) { try { CharacterData deserializedCd = (CharacterData) serializedFile.load(); // DocBaseがデシリアライズ結果と一致しないかぎり、有効としない. URI deserializedDocBase = deserializedCd.getDocBase(); if (deserializedDocBase == null || !docBase.equals(deserializedDocBase)) { throw new IOException("docBase mismatch. actual=" + deserializedDocBase + "/expected=" + docBase); } // デシリアライズ結果を有効なキャラクターデータとして返す. return deserializedCd; } catch (Exception ex) { logger.log(Level.WARNING, "cached character.xml loading failed.: " + docBase, ex); // デシリアライズに失敗した場合は無視して継続 } } } // XMLから読み取る CharacterData characterData = loadCharacterDataFromXML(docBase); // XMLの読み取り結果をシリアライズする. try { serializedFile.save(characterData); } catch (Exception ex) { logger.log(Level.WARNING, "cached character.xml creation failed.: " + docBase, ex); // シリアライズに失敗しても処理は継続する. } return characterData; } /** * キャラクター定義(プロファイル)をロードする. * @param docBase 対象xml * @return キャラクターデータ * @throws IOException */ public CharacterData loadCharacterDataFromXML(URI docBase) throws IOException { if (docBase == null) { throw new IllegalArgumentException(); } DocInfo docInfo; URL docBaseURL = docBase.toURL(); InputStream is = docBaseURL.openStream(); try { docInfo = readDocumentType(is); logger.log(Level.FINE, "docinfo: " + docInfo); } finally { is.close(); } if (docInfo == null) { throw new IOException("unknown document type."); } CharacterData cd; is = docBaseURL.openStream(); try { cd = loadCharacterDataFromXML(is, docBase, docInfo); } finally { is.close(); } return cd; } /** * XMLの最初の要素と、その要素の属性にあるversionとxmlnsを取得して返す.
* XMLとして不正などの理由で取得できない場合はnullを返す.
* 読み込みそのものに失敗した場合は例外を返す.
* * @param is XMLコンテンツと想定される入力ストリーム * @return DocInfo情報、もしくはnull * @throws IOException 読み込みに失敗した場合 */ public DocInfo readDocumentType(InputStream is) throws IOException { SAXParserFactory saxParserFactory = SAXParserFactory.newInstance(); SAXParser saxParser; try { saxParser = saxParserFactory.newSAXParser(); } catch (ParserConfigurationException ex) { throw new RuntimeException("JAXP Configuration Exception.", ex); } catch (SAXException ex) { throw new RuntimeException("JAXP Configuration Exception.", ex); } try { final DocInfo[] result = new DocInfo[1]; saxParser.parse(is, new DefaultHandler() { private int elmCount = 0; @Override public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { if (elmCount == 0) { String version = attributes.getValue("version"); String namespace = attributes.getValue("xmlns"); DocInfo docInfo = new DocInfo(); docInfo.setFirstElementName(qName); docInfo.setVersion(version); docInfo.setNamespace(namespace); result[0] = docInfo; } elmCount++; } }); return result[0]; } catch (SAXException ex) { logger.log(Level.INFO, "character.xml check failed.", ex); } return null; } /** * デフォルトのキャラクター定義を生成して返す.
* 一度生成された場合はキャッシュされる.
* 生成されたキャラクター定義のdocBaseはnullであるため、docBaseをセットすること.
* @return キャラクター定義 */ public synchronized CharacterData createDefaultCharacterData() { try { if (defaultCharacterData == null) { CharacterData cd; try { // 埋め込みリソースからデフォルトキャラクターデータを構築する. cd = loadEmbeddedSerializedDefaultCharacterData(); } catch (Exception ex) { // 失敗した場合はXMLでの読み込みを試行する. logger.log(Level.WARNING, "can't de-serialize the embedded default character-data.", ex); cd = null; } if (cd == null) { // XMLリソースからデフォルトキャラクターデータを構築する. cd = loadEmbeddedXMLDefaultCharacterData(); } assert(cd != null); defaultCharacterData = cd; } // コピーを返す. return defaultCharacterData.duplicateBasicInfo(); } catch (IOException ex) { throw new RuntimeException("can not create the default profile from application's resource", ex); } } /** * XMLリソースファイルから、デフォルトキャラクターデータを生成して返す.
* (現在のロケールの言語に対応するデータを取得し、なければ最初の言語で代替する.)
* 生成されたキャラクター定義のdocBaseはnullであるため、使用する場合はdocBaseをセットすること.
* 都度、XMLファイルから読み込まれる.
* @return デフォルトキャラクターデータ * @throws IOException 失敗 */ public CharacterData loadEmbeddedXMLDefaultCharacterData() throws IOException { CharacterData cd; URL defaultCharacter = getEmbeddedResourceURL(DEFAULT_CHARACTER_XML); InputStream is = defaultCharacter.openStream(); try { DocInfo docInfo = new DocInfo(); docInfo.setFirstElementName("character"); docInfo.setNamespace(NS); docInfo.setVersion("1.0"); cd = loadCharacterDataFromXML(is, null, docInfo); } finally { is.close(); } return cd; } /** * シリアライズされたデフォルトキャラクターデータを埋め込みリソースより取得する.
* (現在のロケールの言語に対応するデータを取得し、なければenで代替する.)
* リソースがないか、読み込めない場合はnullを返す.
* 都度、リソースをデシリアライズする.
* @return キャラクター定義、もしくはnull */ public CharacterData loadEmbeddedSerializedDefaultCharacterData() throws IOException, ClassNotFoundException { URL defaultSerializedCharacter = getEmbeddedResourceURL(DEFAULT_CHARACTER_XML_SER); URL defaultCharacter = getEmbeddedResourceURL(DEFAULT_CHARACTER_XML); // 埋め込みリソースのシリアライズされたデフォルトキャラクターデータを復元する. if (defaultSerializedCharacter != null) { URLConnection connSer = defaultSerializedCharacter.openConnection(); URLConnection connXml = defaultCharacter.openConnection(); if (connXml.getLastModified() <= connSer.getLastModified()) { Object obj; InputStream is = connSer.getInputStream(); try { ObjectInputStream ois = new ObjectInputStream(is); try { obj = ois.readObject(); } finally { ois.close(); } } finally { is.close(); } @SuppressWarnings("unchecked") Map cdMap = (Map) obj; Locale locale = Locale.getDefault(); String lang = locale.getLanguage(); CharacterData cd = cdMap.get(lang); if (cd == null) { // 指定した言語が見つからなければenを代表とする. cd = cdMap.get("en"); if (cd == null && !cdMap.isEmpty()) { // それも見つからなければ、どれか1つを採用する. cd = cdMap.values().iterator().next(); } } return cd; } } // リソースがないか、リソースの読み込みに失敗した場合. return null; } /** * XMLコンテンツに対する入力ストリームからキャラクターデータを取り出す.
* docbaseはXMLファイルの位置を示すものであり、XMLデータ中には含まれず、キャラクターデータのロード時にセットされる.
* そのため引数としてdocbaseを引き渡す.
* 読み取りは現在のデフォルトロケールで行われる.
* @param is 入力ストリーム * @param docBase XMLファイルの位置を示すURI、nullの場合はnullが設定される。 * @param docInfo ドキュメントのタイプ * @return 読み取られたプロファイル * @throws IOException 読み取りに失敗 */ public CharacterData loadCharacterDataFromXML(InputStream is, URI docBase, DocInfo docInfo) throws IOException { return loadCharacterDataFromXML(is, docBase, docInfo, Locale.getDefault()); } /** * XMLコンテンツに対する入力ストリームからキャラクターデータを取り出す.
* docbaseはXMLファイルの位置を示すものであり、XMLデータ中には含まれず、キャラクターデータのロード時にセットされる.
* そのため引数としてdocbaseを引き渡す.
* 設定ファイル中の表示文字列にロケール指定がある場合、引数に指定したロケールに合致する言語の情報を取得する.
* 合致するものがなければ最初のものを使用する.
* @param is 入力ストリーム * @param docBase XMLファイルの位置を示すURI、nullの場合はnullが設定される。 * @param docInfo ドキュメントのタイプ * @param locale 読み取るロケール * @return 読み取られたプロファイル * @throws IOException 読み取りに失敗 */ public CharacterData loadCharacterDataFromXML(InputStream is, URI docBase, DocInfo docInfo, Locale locale) throws IOException { if (is == null || docInfo == null || locale == null) { throw new IllegalArgumentException(); } Schema schema = loadSchema(docInfo); DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); factory.setNamespaceAware(true); factory.setSchema(schema); Document doc; try { DocumentBuilder builder = factory.newDocumentBuilder(); final ArrayList errors = new ArrayList(); builder.setErrorHandler(new ErrorHandler() { public void error(SAXParseException exception) throws SAXException { errors.add(exception); } public void fatalError(SAXParseException exception) throws SAXException { errors.add(exception); } public void warning(SAXParseException exception) throws SAXException { errors.add(exception); } }); doc = builder.parse(is); if (errors.size() > 0) { throw errors.get(0); } } catch (ParserConfigurationException ex) { throw new RuntimeException("JAXP Configuration Exception.", ex); } catch (SAXException ex) { IOException ex2 = new IOException("CharacterData read failed."); ex2.initCause(ex); throw ex2; } XPath xpath = createXPath(docInfo); CharacterData characterData = new CharacterData(); characterData.setDocBase(docBase); try { // check version Node nodeVersion = (Node) xpath.evaluate("/pre:character/@version", doc, XPathConstants.NODE); if (!nodeVersion.getTextContent().equals(VERSION_SIG_1_0)) { throw new IOException("unsupported version: " + nodeVersion.toString()); } // character id Node nodeCharacterId = (Node) xpath.evaluate("/pre:character/@id", doc, XPathConstants.NODE); String characterId = nodeCharacterId.getTextContent().trim(); characterData.setId(characterId); // character rev Node nodeCharacterRev = (Node) xpath.evaluate("/pre:character/@rev", doc, XPathConstants.NODE); String characterRev = nodeCharacterRev.getTextContent().trim(); characterData.setRev(characterRev); // language String lang = locale.getLanguage(); // character name Node nodeCharacterName = (Node) xpath.evaluate( "/pre:character/pre:name[lang('" + lang + "')]", doc, XPathConstants.NODE); if (nodeCharacterName == null) { nodeCharacterName = (Node) xpath.evaluate( "/pre:character/pre:name[position() = 1]", doc, XPathConstants.NODE); } String characterName = nodeCharacterName.getTextContent(); characterData.setName(characterName); // author/description Node nodeAuthor = (Node) xpath.evaluate( "/pre:character/pre:information/pre:author[lang('" + lang + "')]", doc, XPathConstants.NODE); if (nodeAuthor == null) { nodeAuthor = (Node) xpath.evaluate( "/pre:character/pre:information/pre:author[position() = 1]", doc, XPathConstants.NODE); } characterData.setAuthor(nodeAuthor == null ? "" : nodeAuthor.getTextContent().trim()); Node nodeDescription = (Node) xpath.evaluate( "/pre:character/pre:information/pre:description[lang('" + lang + "')]", doc, XPathConstants.NODE); if (nodeDescription == null) { nodeDescription = (Node) xpath.evaluate( "/pre:character/pre:information/pre:description[position() = 1]", doc, XPathConstants.NODE); } // 説明の改行コードはXML上ではLF、キャラクターデータとしてインスタンス化された場合はプラットフォーム固有に変換される. String note = (nodeDescription == null) ? "" : nodeDescription.getTextContent().trim(); characterData.setDescription(note); // image size int width = Integer.parseInt(((Node) xpath.evaluate( "/pre:character/pre:image-size/pre:width", doc, XPathConstants.NODE)).getTextContent()); int height = Integer.parseInt(((Node) xpath.evaluate( "/pre:character/pre:image-size/pre:height", doc, XPathConstants.NODE)).getTextContent()); characterData.setImageSize(new Dimension(width, height)); // settings XPathExpression expSettingsEntry = xpath.compile("pre:character/pre:settings/pre:entry"); for (Node nodeSettingsEntry : iterable((NodeList) expSettingsEntry.evaluate(doc, XPathConstants.NODESET))) { Element elmSettingsEntry = (Element) nodeSettingsEntry; String key = elmSettingsEntry.getAttribute("key"); String value = elmSettingsEntry.getTextContent(); characterData.setProperty(key, value); } // color-group ArrayList colorGroups = new ArrayList(); XPathExpression expDisplayNameByLocale = xpath.compile("pre:display-name[lang('" + lang + "')]"); XPathExpression expDisplayNameByDefault = xpath.compile("pre:display-name[position() = 1]"); XPathExpression expColorGroupId = xpath.compile("@id"); for (Node nodeColorGroup : iterable((NodeList) xpath.evaluate( "/pre:character/pre:colorGroups/pre:colorGroup", doc, XPathConstants.NODESET))) { String id = ((Node) expColorGroupId.evaluate(nodeColorGroup, XPathConstants.NODE)).getTextContent().trim(); Node nodeDisplayName = (Node) expDisplayNameByLocale.evaluate(nodeColorGroup, XPathConstants.NODE); if (nodeDisplayName == null) { nodeDisplayName = (Node) expDisplayNameByDefault.evaluate(nodeColorGroup, XPathConstants.NODE); } String colorGroupDisplayName = nodeDisplayName.getTextContent(); ColorGroup colorGroup = new ColorGroup(id, colorGroupDisplayName); colorGroups.add(colorGroup); } characterData.setColorGroups(colorGroups); // category ArrayList categories = new ArrayList(); XPathExpression expCategoryId = xpath.compile("@id"); XPathExpression expMultipleSelectable = xpath.compile("@multipleSelectable"); XPathExpression expLayers = xpath.compile("pre:layers/pre:layer"); XPathExpression expVisibleRows = xpath.compile("pre:visible-rows"); XPathExpression expLayerOrder = xpath.compile("pre:order"); XPathExpression expLayerColorGroup = xpath.compile("pre:colorGroup"); XPathExpression expLayerDir = xpath.compile("pre:dir"); for (Node nodeCategory : iterable((NodeList) xpath.evaluate( "/pre:character/pre:categories/pre:category", doc, XPathConstants.NODESET))) { String categoryId = ((Node) expCategoryId.evaluate(nodeCategory, XPathConstants.NODE)).getTextContent().trim(); boolean multipleSelectable = Boolean.parseBoolean(((Node) expMultipleSelectable.evaluate( nodeCategory, XPathConstants.NODE)).getTextContent()); int visibleRows = Integer.parseInt(((Node) expVisibleRows.evaluate(nodeCategory, XPathConstants.NODE)).getTextContent()); Node nodeDisplayName = (Node) expDisplayNameByLocale.evaluate(nodeCategory, XPathConstants.NODE); if (nodeDisplayName == null) { nodeDisplayName = (Node) expDisplayNameByDefault.evaluate(nodeCategory, XPathConstants.NODE); } String categoryDisplayName = nodeDisplayName.getTextContent(); ArrayList layers = new ArrayList(); for (Node nodeLayer : iterable((NodeList) expLayers.evaluate(nodeCategory, XPathConstants.NODESET))) { NamedNodeMap attr = nodeLayer.getAttributes(); String layerId = attr.getNamedItem("id").getTextContent().trim(); String layerDir = ((Node) expLayerDir.evaluate(nodeLayer, XPathConstants.NODE)).getTextContent(); int order = Integer.valueOf(((Node) expLayerOrder.evaluate(nodeLayer, XPathConstants.NODE)).getTextContent()); Node nodeLayerDisplayName = (Node) expDisplayNameByLocale.evaluate(nodeLayer, XPathConstants.NODE); if (nodeLayerDisplayName == null) { nodeLayerDisplayName = (Node) expDisplayNameByDefault.evaluate(nodeLayer, XPathConstants.NODE); } String layerDisplayName = nodeLayerDisplayName.getTextContent(); Node nodeLayerColorGroup = (Node) expLayerColorGroup.evaluate(nodeLayer, XPathConstants.NODE); ColorGroup colorGroup = null; boolean initSync = false; if (nodeLayerColorGroup != null) { NamedNodeMap attrColorGroup = nodeLayerColorGroup.getAttributes(); String colorGroupRefId = attrColorGroup.getNamedItem("refid").getTextContent().trim(); colorGroup = characterData.getColorGroup(colorGroupRefId); initSync = Boolean.valueOf(attrColorGroup.getNamedItem("init-sync").getTextContent()); } Layer layer = new Layer(layerId, layerDisplayName, order, colorGroup, initSync, layerDir); layers.add(layer); } PartsCategory category = new PartsCategory(categories.size(), categoryId, categoryDisplayName, multipleSelectable, visibleRows, layers.toArray(new Layer[layers.size()])); categories.add(category); } characterData.setPartsCategories(categories.toArray(new PartsCategory[categories.size()])); // presets Node nodePresets = (Node) xpath.evaluate("/pre:character/pre:presets", doc, XPathConstants.NODE); if (nodePresets != null) { loadPartsSet(characterData, nodePresets, true, docInfo); } // recommendations Node recommendationsNode = (Node) xpath.evaluate("/pre:character/pre:recommendations", doc, XPathConstants.NODE); List recommendationURLList = null; // お勧めノードがない場合はnull if (recommendationsNode != null) { // お勧めノードがある場合 recommendationURLList = new ArrayList(); XPathExpression expRecommendDescriptionByLocale = xpath.compile("pre:description[lang('" + lang + "')]"); XPathExpression expRecommendDescriptionByDefault = xpath.compile("pre:description[position() = 1]"); XPathExpression expRecommendURLByLocale = xpath.compile("pre:URL[lang('" + lang + "')]"); XPathExpression expRecommendURLByDefault = xpath.compile("pre:URL[position() = 1]"); for (Node recomendationNode : iterable((NodeList) xpath.evaluate( "/pre:character/pre:recommendations/pre:recommendation", doc, XPathConstants.NODESET))) { Node nodeRecomendDescription = (Node) expRecommendDescriptionByLocale.evaluate(recomendationNode, XPathConstants.NODE); if (nodeRecomendDescription == null) { nodeRecomendDescription = (Node) expRecommendDescriptionByDefault.evaluate(recomendationNode, XPathConstants.NODE); } String recommentDescription = nodeRecomendDescription.getTextContent().trim(); Node nodeRecomendURL = (Node) expRecommendURLByLocale.evaluate(recomendationNode, XPathConstants.NODE); if (nodeRecomendURL == null) { nodeRecomendURL = (Node) expRecommendURLByDefault.evaluate(recomendationNode, XPathConstants.NODE); } String url = nodeRecomendURL.getTextContent().trim(); RecommendationURL recommendationURL = new RecommendationURL(); recommendationURL.setDisplayName(recommentDescription); recommendationURL.setUrl(url); recommendationURLList.add(recommendationURL); } } characterData.setRecommendationURLList(recommendationURLList); } catch (XPathExpressionException ex) { IOException ex2 = new IOException("CharacterData invalid format."); ex2.initCause(ex); throw ex2; } return characterData; } protected void saveCharacterDataToXML(CharacterData characterData) throws IOException { if (characterData == null) { throw new IllegalArgumentException(); } characterData.checkWritable(); if (!characterData.isValid()) { throw new IOException("invalid profile: " + characterData); } URI docBase = characterData.getDocBase(); if ( !"file".equals(docBase.getScheme())) { throw new IOException("ファイル以外はサポートしていません: " + docBase); } // XML形式で保存(メモリへ) ByteArrayOutputStream bos = new ByteArrayOutputStream(); try { writeXMLCharacterData(characterData, bos); } finally { bos.close(); } // 成功したら実際にファイルに出力 File characterPropXML = new File(docBase); File baseDir = characterPropXML.getParentFile(); if (!baseDir.exists()) { if (!baseDir.mkdirs()) { logger.log(Level.WARNING, "can't create directory. " + baseDir); } } FileOutputStream fos = new FileOutputStream(characterPropXML); try { fos.write(bos.toByteArray()); } finally { fos.close(); } } protected void saveCharacterDataToSer(CharacterData characterData) throws IOException { URI docBase = characterData.getDocBase(); if (docBase == null) { throw new IllegalArgumentException("docBaseがありません." + characterData); } try { UserData serializedFile = getCharacterDataCacheUserFile(docBase); serializedFile.save(characterData); } catch (IOException ex) { logger.log(Level.WARNING, "cached character.xml creation failed.: " + docBase, ex); throw ex; } } public void writeXMLCharacterData(CharacterData characterData, OutputStream outstm) throws IOException { if (outstm == null || characterData == null) { throw new IllegalArgumentException(); } Locale locale = Locale.getDefault(); String lang = locale.getLanguage(); Document doc; try { DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); factory.setNamespaceAware(true); DocumentBuilder builder = factory.newDocumentBuilder(); doc = builder.newDocument(); } catch (ParserConfigurationException ex) { throw new RuntimeException("JAXP Configuration failed.", ex); } Element root = doc.createElementNS(NS, "character"); root.setAttribute("version", VERSION_SIG_1_0); root.setAttribute("xmlns:xml", XMLConstants.XML_NS_URI); root.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance"); root.setAttribute("xsi:schemaLocation", NS + " character.xsd"); root.setAttribute("id", characterData.getId()); root.setAttribute("rev", characterData.getRev()); doc.appendChild(root); // name Element nodeName = doc.createElementNS(NS, "name"); Attr attrLang = doc.createAttributeNS(XMLConstants.XML_NS_URI, "lang"); attrLang.setValue(lang); nodeName.setAttributeNodeNS(attrLang); nodeName.setTextContent(characterData.getName()); root.appendChild(nodeName); // information String author = characterData.getAuthor(); String description = characterData.getDescription(); if ((author != null && author.length() > 0) || (description != null && description.length() > 0)) { Element nodeInfomation = doc.createElementNS(NS, "information"); if (author != null && author.length() > 0) { Element nodeAuthor = doc.createElementNS(NS, "author"); Attr attrNodeAuthorLang = doc.createAttributeNS(XMLConstants.XML_NS_URI, "lang"); attrNodeAuthorLang.setValue(lang); nodeAuthor.setAttributeNodeNS(attrNodeAuthorLang); nodeAuthor.setTextContent(author); nodeInfomation.appendChild(nodeAuthor); } if (description != null && description.length() > 0) { // 説明の改行コードはXML上ではLFとする. description = description.replace("\r\n", "\n"); description = description.replace("\r", "\n"); Element nodeDescription = doc.createElementNS(NS, "description"); Attr attrNodeDescriptionLang = doc.createAttributeNS(XMLConstants.XML_NS_URI, "lang"); attrNodeDescriptionLang.setValue(lang); nodeDescription.setAttributeNodeNS(attrNodeDescriptionLang); nodeDescription.setTextContent(description); nodeInfomation.appendChild(nodeDescription); } root.appendChild(nodeInfomation); } // size Element nodeSize = doc.createElementNS(NS, "image-size"); Element nodeWidth = doc.createElementNS(NS, "width"); nodeWidth.setTextContent(Integer.toString((int) characterData.getImageSize().getWidth())); Element nodeHeight = doc.createElementNS(NS, "height"); nodeHeight.setTextContent(Integer.toString((int) characterData.getImageSize().getHeight())); nodeSize.appendChild(nodeWidth); nodeSize.appendChild(nodeHeight); root.appendChild(nodeSize); // settings Element nodeSettings = doc.createElementNS(NS, "settings"); root.appendChild(nodeSettings); for (String settingsEntryName : characterData.getPropertyNames()) { String value = characterData.getProperty(settingsEntryName); if (value != null) { Element nodeEntry = doc.createElementNS(NS, "entry"); nodeEntry.setAttribute("key", settingsEntryName); nodeEntry.setTextContent(value); nodeSettings.appendChild(nodeEntry); } } // categories Element nodeCategories = doc.createElementNS(NS, "categories"); for (PartsCategory category : characterData.getPartsCategories()) { // category Element nodeCategory = doc.createElementNS(NS, "category"); nodeCategory.setAttribute("id", category.getCategoryId()); nodeCategory.setAttribute("multipleSelectable", category.isMultipleSelectable() ? "true" : "false"); // visible-rows Element nodeVisibleRows = doc.createElementNS(NS, "visible-rows"); nodeVisibleRows.setTextContent(Integer.toString(category.getVisibleRows())); nodeCategory.appendChild(nodeVisibleRows); // category name Element nodeCategoryName = doc.createElementNS(NS, "display-name"); Attr attrCategoryNameLang = doc.createAttributeNS(XMLConstants.XML_NS_URI, "lang"); attrCategoryNameLang.setValue(lang); nodeCategoryName.setAttributeNodeNS(attrCategoryNameLang); nodeCategoryName.setTextContent(category.getLocalizedCategoryName()); nodeCategory.appendChild(nodeCategoryName); // layers Element nodeLayers = doc.createElementNS(NS, "layers"); for (Layer layer : category.getLayers()) { // layer Element nodeLayer = doc.createElementNS(NS, "layer"); nodeLayer.setAttribute("id", layer.getId()); Element nodeLayerName = doc.createElementNS(NS, "display-name"); Attr attrLayerNameLang = doc.createAttributeNS(XMLConstants.XML_NS_URI, "lang"); attrLayerNameLang.setValue(lang); nodeLayerName.setAttributeNodeNS(attrLayerNameLang); nodeLayerName.setTextContent(layer.getLocalizedName()); nodeLayer.appendChild(nodeLayerName); Element nodeOrder = doc.createElementNS(NS, "order"); nodeOrder.setTextContent(Integer.toString(layer.getOrder())); nodeLayer.appendChild(nodeOrder); ColorGroup colorGroup = layer.getColorGroup(); if (colorGroup != null && colorGroup.isEnabled()) { Element nodeColorGroup = doc.createElementNS(NS, "colorGroup"); nodeColorGroup.setAttribute("refid", colorGroup.getId()); nodeColorGroup.setAttribute("init-sync", layer.isInitSync() ? "true" : "false"); nodeLayer.appendChild(nodeColorGroup); } Element nodeDir = doc.createElementNS(NS, "dir"); nodeDir.setTextContent(layer.getDir()); nodeLayer.appendChild(nodeDir); nodeLayers.appendChild(nodeLayer); } nodeCategory.appendChild(nodeLayers); nodeCategories.appendChild(nodeCategory); } root.appendChild(nodeCategories); // ColorGroupを構築する Collection colorGroups = characterData.getColorGroups(); if (colorGroups.size() > 0) { Element nodeColorGroups = doc.createElementNS(NS, "colorGroups"); int colorGroupCount = 0; for (ColorGroup colorGroup : colorGroups) { if (!colorGroup.isEnabled()) { continue; } Element nodeColorGroup = doc.createElementNS(NS, "colorGroup"); nodeColorGroup.setAttribute("id", colorGroup.getId()); Element nodeColorGroupName = doc.createElementNS(NS, "display-name"); Attr attrColorGroupNameLang = doc.createAttributeNS(XMLConstants.XML_NS_URI, "lang"); attrColorGroupNameLang.setValue(lang); nodeColorGroupName.setAttributeNodeNS(attrColorGroupNameLang); nodeColorGroupName.setTextContent(colorGroup.getLocalizedName()); nodeColorGroup.appendChild(nodeColorGroupName); nodeColorGroups.appendChild(nodeColorGroup); colorGroupCount++; } if (colorGroupCount > 0) { root.appendChild(nodeColorGroups); } } // Recommendations List recommendations = characterData.getRecommendationURLList(); if (recommendations != null) { Element nodeRecommendations = doc.createElementNS(NS, "recommendations"); for (RecommendationURL recommendation : recommendations) { Element nodeRecommendation = doc.createElementNS(NS, "recommendation"); String displayName = recommendation.getDisplayName(); String url = recommendation.getUrl(); Element nodeDescription = doc.createElementNS(NS, "description"); Attr attrRecommendationDescriptionLang = doc.createAttributeNS(XMLConstants.XML_NS_URI, "lang"); attrRecommendationDescriptionLang.setValue(lang); nodeDescription.setAttributeNodeNS(attrRecommendationDescriptionLang); nodeDescription.setTextContent(displayName); Element nodeURL = doc.createElementNS(NS, "URL"); Attr attrRecommendationURLLang = doc.createAttributeNS(XMLConstants.XML_NS_URI, "lang"); attrRecommendationURLLang.setValue(lang); nodeURL.setAttributeNodeNS(attrRecommendationURLLang); nodeURL.setTextContent(url); nodeRecommendation.appendChild(nodeDescription); nodeRecommendation.appendChild(nodeURL); nodeRecommendations.appendChild(nodeRecommendation); } root.appendChild(nodeRecommendations); } // presetsのelementを構築する. Element nodePresets = doc.createElementNS(NS, "presets"); if (writePartsSetElements(doc, nodePresets, characterData, true, false) > 0) { root.appendChild(nodePresets); } // output xml TransformerFactory txFactory = TransformerFactory.newInstance(); txFactory.setAttribute("indent-number", Integer.valueOf(4)); Transformer tfmr; try { tfmr = txFactory.newTransformer(); } catch (TransformerConfigurationException ex) { throw new RuntimeException("JAXP Configuration Failed.", ex); } tfmr.setOutputProperty(OutputKeys.INDENT, "yes"); // http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4504745 final String encoding = "UTF-8"; tfmr.setOutputProperty("encoding", encoding); try { tfmr.transform(new DOMSource(doc), new StreamResult(new OutputStreamWriter(outstm, Charset.forName(encoding)))); } catch (TransformerException ex) { IOException ex2 = new IOException("XML Convert failed."); ex2.initCause(ex); throw ex2; } } public void saveFavorites(CharacterData characterData) throws IOException { if (characterData == null) { throw new IllegalArgumentException(); } // xml形式 UserData favoritesData = getFavoritesUserData(characterData, "xml"); OutputStream os = favoritesData.getOutputStream(); try { saveFavorites(characterData, os); } finally { os.close(); } // serialize形式 UserData favoritesSer = getFavoritesUserData(characterData, "ser"); Collection partsSetsOrg = characterData.getPartsSets().values(); ArrayList partsSets = new ArrayList(partsSetsOrg); favoritesSer.save(partsSets); } protected UserData getFavoritesUserData(CharacterData characterData, String serializeType) { if (characterData == null) { throw new IllegalArgumentException(); } UserDataFactory userDataFactory = UserDataFactory.getInstance(); URI docBase = characterData.getDocBase(); try { if ("xml".equals(serializeType)) { // xml形式の場合、キャラクターディレクトリ上に設定する. File characterDir = new File(docBase).getParentFile(); return new FileUserData(new File(characterDir, "favorites.xml")); } if ("ser".equals(serializeType)) { // ser形式の場合はユーザデータディレクトリ上のキャッシュに設定する. String dataname = "favorites.ser"; UserData favoritesData = userDataFactory.getMangledNamedUserData(docBase, dataname); return favoritesData; } else { throw new UnsupportedOperationException("サポートされていない形式です: " + serializeType); } } catch (Exception ex) { logger.log(Level.SEVERE, "docBaseもしくはシリアライズタイプが不正です。" + docBase + "/type=" + serializeType, ex); throw new RuntimeException(ex); } } protected void saveFavorites(CharacterData characterData, OutputStream outstm) throws IOException { if (characterData == null || outstm == null) { throw new IllegalArgumentException(); } Document doc; try { DocumentBuilderFactory factory = DocumentBuilderFactory .newInstance(); factory.setNamespaceAware(true); DocumentBuilder builder = factory.newDocumentBuilder(); doc = builder.newDocument(); } catch (ParserConfigurationException ex) { throw new RuntimeException("JAXP Configuration Exception.", ex); } Element root = doc.createElementNS(NS, "partssets"); root.setAttribute("xmlns:xml", XMLConstants.XML_NS_URI); root.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance"); root.setAttribute("xsi:schemaLocation", NS + " partsset.xsd"); doc.appendChild(root); // presetsのelementを構築する.(Presetは除く) writePartsSetElements(doc, root, characterData, false, true); // output xml TransformerFactory txFactory = TransformerFactory.newInstance(); txFactory.setAttribute("indent-number", Integer.valueOf(4)); Transformer tfmr; try { tfmr = txFactory.newTransformer(); } catch (TransformerConfigurationException ex) { throw new RuntimeException("JAXP Configuration Failed.", ex); } tfmr.setOutputProperty(OutputKeys.INDENT, "yes"); // http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4504745 final String encoding = "UTF-8"; tfmr.setOutputProperty("encoding", encoding); try { tfmr.transform(new DOMSource(doc), new StreamResult(new OutputStreamWriter(outstm, Charset.forName(encoding)))); } catch (TransformerException ex) { IOException ex2 = new IOException("XML Convert failed."); ex2.initCause(ex); throw ex2; } } /** * キャラクターデータ内のPresetおよびFavotiesのPartssetの双方共通のパーツセット要素のリストを構築する. * @param doc ドキュメントオブジェクト(createElementNS用) * @param baseElement 親要素、キャラクターデータの場合はPreset、Favoritesの場合はPartssetを示す要素 * @param characterData キャラクターデータ * @param writePresets Preset属性のパーツセットを登録する場合はtrue、Preset属性時はデフォルトプリセット属性も(あれば)登録される * @param writeFavorites Preset属性のないパーツセットを登録する場合はtrue * @return 登録したパーツセットの個数 */ protected int writePartsSetElements(Document doc, Element baseElement, CharacterData characterData, boolean writePresets, boolean writeFavorites) { Map partsSetMap = characterData.getPartsSets(); Locale locale = Locale.getDefault(); String lang = locale.getLanguage(); HashMap registeredPartsSetMap = new HashMap(); for (Map.Entry partsSetsEntry : partsSetMap.entrySet()) { PartsSet partsSet = partsSetsEntry.getValue(); if (partsSet.isPresetParts() && !writePresets) { continue; } if (!partsSet.isPresetParts() && !writeFavorites) { continue; } if (partsSet.isEmpty()) { // 空のパーツセットは登録しない. continue; } String partsSetId = partsSet.getPartsSetId(); String localizedName = partsSet.getLocalizedName(); Element nodePreset = doc.createElementNS(NS, "preset"); nodePreset.setAttribute("id", partsSetId); baseElement.appendChild(nodePreset); registeredPartsSetMap.put(partsSet.getPartsSetId(), partsSet); // display-name Element nodeName = doc.createElementNS(NS, "display-name"); Attr attrLang = doc.createAttributeNS(XMLConstants.XML_NS_URI, "lang"); attrLang.setValue(lang); nodeName.setAttributeNode(attrLang); nodeName.setTextContent(localizedName); nodePreset.appendChild(nodeName); // bgColor Color bgColor = partsSet.getBgColor(); if (bgColor != null) { Element nodeBgColor = doc.createElementNS(NS, "background-color"); nodeBgColor.setAttribute("color", "#" + Integer.toHexString(bgColor.getRGB() & 0xffffff)); nodePreset.appendChild(nodeBgColor); } // affine transform parameter double[] affineTransformParameter = partsSet.getAffineTransformParameter(); if (affineTransformParameter != null) { Element nodeAffineTransform = doc.createElementNS(NS, "affine-transform-parameter"); StringBuilder tmp = new StringBuilder(); for (double affineItem : affineTransformParameter) { if (tmp.length() > 0) { tmp.append(" "); } tmp.append(Double.toString(affineItem)); } nodeAffineTransform.setTextContent(tmp.toString()); nodePreset.appendChild(nodeAffineTransform); } // categories for (Map.Entry> entry : partsSet.entrySet()) { PartsCategory partsCategory = entry.getKey(); // category Element nodeCategory = doc.createElementNS(NS, "category"); nodeCategory.setAttribute("refid", partsCategory.getCategoryId()); nodePreset.appendChild(nodeCategory); List partsIdentifiers = entry.getValue(); for (PartsIdentifier partsIdentifier : partsIdentifiers) { String partsName = partsIdentifier.getPartsName(); Element nodeParts = doc.createElementNS(NS, "parts"); nodeParts.setAttribute("name", partsName); nodeCategory.appendChild(nodeParts); PartsColorInfo partsColorInfo = partsSet.getColorInfo(partsIdentifier); if (partsColorInfo != null) { Element nodeColor = doc.createElementNS(NS, "color"); nodeParts.appendChild(nodeColor); for (Map.Entry colorInfoEntry : partsColorInfo.entrySet()) { Layer layer = colorInfoEntry.getKey(); ColorInfo colorInfo = colorInfoEntry.getValue(); Element nodeLayer = doc.createElementNS(NS, "layer"); nodeLayer.setAttribute("refid", layer.getId()); nodeColor.appendChild(nodeLayer); // ColorGroup ColorGroup colorGroup = colorInfo.getColorGroup(); boolean colorSync = colorInfo.isSyncColorGroup(); if (colorGroup.isEnabled()) { Element nodeColorGroup = doc.createElementNS(NS, "color-group"); nodeColorGroup.setAttribute("group", colorGroup.getId()); nodeColorGroup.setAttribute("synchronized", colorSync ? "true" : "false"); nodeLayer.appendChild(nodeColorGroup); } // RGB ColorConvertParameter param = colorInfo.getColorParameter(); Element nodeRGB = doc.createElementNS(NS, "rgb"); Object[][] rgbArgss = { {"red", param.getOffsetR(), param.getFactorR(), param.getGammaR()}, {"green", param.getOffsetG(), param.getFactorG(), param.getGammaG()}, {"blue", param.getOffsetB(), param.getFactorB(), param.getGammaB()}, {"alpha", param.getOffsetA(), param.getFactorA(), param.getGammaA()}, }; for (Object[] rgbArgs : rgbArgss) { Element nodeRGBItem = doc.createElementNS(NS, rgbArgs[0].toString()); nodeRGBItem.setAttribute("offset", rgbArgs[1].toString()); nodeRGBItem.setAttribute("factor", rgbArgs[2].toString()); nodeRGBItem.setAttribute("gamma", rgbArgs[3].toString()); nodeRGB.appendChild(nodeRGBItem); } nodeLayer.appendChild(nodeRGB); // HSB Element nodeHSB = doc.createElementNS(NS, "hsb"); nodeHSB.setAttribute("hue", Float.toString(param.getHue())); nodeHSB.setAttribute("saturation", Float.toString(param.getSaturation())); nodeHSB.setAttribute("brightness", Float.toString(param.getBrightness())); if (param.getContrast() != 0.f) { // ver0.96追加、optional // ぴったり0.0fだったら省略する. nodeHSB.setAttribute("contrast", Float.toString(param.getContrast())); } nodeLayer.appendChild(nodeHSB); // RGB Replace Element nodeRGBReplace = doc.createElementNS(NS, "rgb-replace"); ColorConv colorConv = param.getColorReplace(); if (colorConv == null) { colorConv = ColorConv.NONE; } nodeRGBReplace.setAttribute("replace-type", colorConv.name()); nodeRGBReplace.setAttribute("gray", Float.toString(param.getGrayLevel())); nodeLayer.appendChild(nodeRGBReplace); } } } } } // プリセット登録時はデフォルトのプリセットIDがあれば、それも登録する. // (ただし、該当パーツセットが書き込み済みである場合のみ) if (writePresets) { String defaultPresetId = characterData.getDefaultPartsSetId(); if (defaultPresetId != null && defaultPresetId.length() > 0) { PartsSet defaultPartsSet = registeredPartsSetMap.get(defaultPresetId); if (defaultPartsSet != null && defaultPartsSet.isPresetParts()) { baseElement.setAttribute("default-preset", defaultPresetId); } } } return registeredPartsSetMap.size(); } /** * CharacterDataのプリセットまたはFavoritesのパーツセットのXMLからパーツセットを読み取って登録する.
* @param characterData キャラクターデータ * @param nodePartssets パーツセットのノード、プリセットまたはパーツセットノード * @param presetParts ロードしたパーツセットにプリセットフラグをたてる場合はtrue * @param docInfo ドキュメントタイプ * @throws XPathExpressionException */ protected void loadPartsSet(CharacterData characterData, Node nodePartssets, boolean presetParts, DocInfo docInfo) throws XPathExpressionException { logger.log(Level.FINE, "loadPartsSet: " + characterData + " /presetParts=" + presetParts); XPath xpath = createXPath(docInfo); // language Locale locale = Locale.getDefault(); String lang = locale.getLanguage(); XPathExpression expPresets = xpath.compile("pre:preset"); XPathExpression expDisplayNameByLocale = xpath.compile("pre:display-name[lang('" + lang + "')]"); XPathExpression expDisplayNameByDefault = xpath.compile("pre:display-name[position() = 1]"); XPathExpression expId = xpath.compile("@id"); XPathExpression expRefId = xpath.compile("@refid"); XPathExpression expName = xpath.compile("@name"); XPathExpression expCategories = xpath.compile("pre:category"); XPathExpression expParts = xpath.compile("pre:parts"); XPathExpression expLayers = xpath.compile("pre:color/pre:layer"); XPathExpression expColorGroup = xpath.compile("pre:color-group"); XPathExpression expHsb = xpath.compile("pre:hsb"); XPathExpression expRgb = xpath.compile("pre:rgb/pre:red|pre:rgb/pre:green|pre:rgb/pre:blue|pre:rgb/pre:alpha"); XPathExpression expRgbReplace = xpath.compile("pre:rgb-replace"); XPathExpression expBgColor = xpath.compile("pre:background-color/@color"); XPathExpression expAffineTrns = xpath.compile("pre:affine-transform-parameter"); String defaultPresetId = null; Node nodeDefaultPreset = (Node) xpath.evaluate("@default-preset", nodePartssets, XPathConstants.NODE); if (nodeDefaultPreset != null) { defaultPresetId = nodeDefaultPreset.getTextContent().trim(); } for (Node nodePreset : iterable((NodeList) expPresets.evaluate(nodePartssets, XPathConstants.NODESET))) { String partsSetId = ((Node) expId.evaluate(nodePreset, XPathConstants.NODE)).getTextContent().trim(); if (defaultPresetId == null) { defaultPresetId = partsSetId; } Node nodeDisplayName = (Node) expDisplayNameByLocale.evaluate(nodePreset, XPathConstants.NODE); if (nodeDisplayName == null) { nodeDisplayName = (Node) expDisplayNameByDefault.evaluate(nodePreset, XPathConstants.NODE); } String displayName = nodeDisplayName.getTextContent(); PartsSet partsSet = new PartsSet(); partsSet.setPartsSetId(partsSetId); partsSet.setLocalizedName(displayName); partsSet.setPresetParts(presetParts); // bg-color Node nodeBgColor = (Node) expBgColor.evaluate(nodePreset, XPathConstants.NODE); if (nodeBgColor != null) { String bgColorStr = nodeBgColor.getTextContent().trim(); try { Color bgColor = Color.decode(bgColorStr); partsSet.setBgColor(bgColor); } catch (Exception ex) { logger.log(Level.WARNING, "bgColor parameter is invalid. :" + bgColorStr, ex); // 無視する } } // affine-transform-parameter Node nodeAffineTrans = (Node) expAffineTrns.evaluate(nodePreset, XPathConstants.NODE); if (nodeAffineTrans != null) { String strParams = nodeAffineTrans.getTextContent(); if (strParams != null && strParams.trim().length() > 0) { try { ArrayList affineTransformParameterArr = new ArrayList(); for (String strParam : strParams.split("\\s+")) { affineTransformParameterArr.add(Double.valueOf(strParam)); } double[] affineTransformParameter = new double[affineTransformParameterArr.size()]; int idx = 0; for (double aaffineItem : affineTransformParameterArr) { affineTransformParameter[idx++] = aaffineItem; } partsSet.setAffineTransformParameter(affineTransformParameter); } catch (Exception ex) { logger.log(Level.WARNING, "affine transform parameter is invalid. :" + strParams, ex); // 無視する. } } } // Category for (Node nodeCategory : iterable((NodeList) expCategories.evaluate(nodePreset, XPathConstants.NODESET))) { String categoryId = ((Node) expRefId.evaluate(nodeCategory, XPathConstants.NODE)).getTextContent().trim(); PartsCategory category = characterData.getPartsCategory(categoryId); if (category == null) { logger.log(Level.WARNING, "undefined category: " + categoryId); continue; } // Parts for (Node nodeParts : iterable((NodeList) expParts.evaluate(nodeCategory, XPathConstants.NODESET))) { String partsName = ((Node) expName.evaluate(nodeParts, XPathConstants.NODE)).getTextContent().trim(); PartsIdentifier partsIdentifier = new PartsIdentifier(category, partsName, partsName); // Color/Layer PartsColorInfo partsColorInfo = null; for (Node nodeLayer : iterable((NodeList) expLayers.evaluate(nodeParts, XPathConstants.NODESET))) { String layerId = ((Node) expRefId.evaluate(nodeLayer, XPathConstants.NODE)).getTextContent().trim(); Layer layer = category.getLayer(layerId); if (layer == null) { logger.log(Level.WARNING, "undefined layer: " + layerId); continue; } if (partsColorInfo == null) { partsColorInfo = new PartsColorInfo(category); } ColorInfo colorInfo = partsColorInfo.get(layer); // color-group Element elmColorGroup = (Element) expColorGroup.evaluate(nodeLayer, XPathConstants.NODE); if (elmColorGroup != null) { ColorGroup colorGroup = characterData.getColorGroup(elmColorGroup.getAttribute("group")); boolean syncColorGroup = Boolean.parseBoolean(elmColorGroup.getAttribute("synchronized")); colorInfo.setColorGroup(colorGroup); colorInfo.setSyncColorGroup(syncColorGroup); } // rgb ColorConvertParameter param = colorInfo.getColorParameter(); for (Node nodeRgb : iterable((NodeList) expRgb.evaluate(nodeLayer, XPathConstants.NODESET))) { Element elmRgb = (Element) nodeRgb; String rgbName = elmRgb.getNodeName(); int offset = Integer.parseInt(elmRgb.getAttribute("offset")); float factor = Float.parseFloat(elmRgb.getAttribute("factor")); float gamma = Float.parseFloat(elmRgb.getAttribute("gamma")); if ("red".equals(rgbName)) { param.setOffsetR(offset); param.setFactorR(factor); param.setGammaR(gamma); } else if ("green".equals(rgbName)) { param.setOffsetG(offset); param.setFactorG(factor); param.setGammaG(gamma); } else if ("blue".equals(rgbName)) { param.setOffsetB(offset); param.setFactorB(factor); param.setGammaB(gamma); } else if ("alpha".equals(rgbName)) { param.setOffsetA(offset); param.setFactorA(factor); param.setGammaA(gamma); } } // hsb Element elmHsb = (Element) expHsb.evaluate(nodeLayer, XPathConstants.NODE); if (elmHsb != null) { float hue = Float.parseFloat(elmHsb.getAttribute("hue")); float saturation = Float.parseFloat(elmHsb.getAttribute("saturation")); float brightness = Float.parseFloat(elmHsb.getAttribute("brightness")); String strContrast = elmHsb.getAttribute("contrast"); param.setHue(hue); param.setSaturation(saturation); param.setBrightness(brightness); if (strContrast != null && strContrast.length() > 0) { // ver0.96追加 optional float contrast = Float.parseFloat(strContrast); param.setContrast(contrast); } } // rgb-replace Element elmRgbReplace = (Element) expRgbReplace.evaluate(nodeLayer, XPathConstants.NODE); if (elmRgbReplace != null) { Float grayLevel = Float.parseFloat(elmRgbReplace.getAttribute("gray")); ColorConv colorType = ColorConv.valueOf(elmRgbReplace.getAttribute("replace-type")); param.setGrayLevel(grayLevel); param.setColorReplace(colorType); } } partsSet.appendParts(category, partsIdentifier, partsColorInfo); } } characterData.addPartsSet(partsSet); } if (presetParts) { characterData.setDefaultPartsSetId(defaultPresetId); } } /** * お気に入り(Favorites)を読み込む.
* 現在のパーツセットに追加する形で読み込まれ、同じパーツセットIDのものは上書きされます.
* * @param characterData キャラクターデータ * @throws IOException 読み込みに失敗した場合 */ public void loadFavorites(CharacterData characterData) throws IOException { if (characterData == null) { throw new IllegalArgumentException(); } // serialize形式 UserData favoritesSer = getFavoritesUserData(characterData, "ser"); // xml形式 UserData favoritesXml = getFavoritesUserData(characterData, "xml"); try { // serialize形式が存在し、更新日がxmlよりも等しいか新しければ、こちらを優先する. if (favoritesSer.exists() && favoritesSer.lastModified() >= favoritesXml.lastModified()) { // serialize形式の読み込み @SuppressWarnings("unchecked") Collection partsSets = (Collection) favoritesSer.load(); for (PartsSet partsSet : partsSets) { characterData.addPartsSet(partsSet); } return; } } catch (Exception ex) { logger.log(Level.WARNING, "cached favorites loading failed. :" + characterData, ex); // シリアライズ形式の読み込みに失敗した場合はxmlで継続する } // xml形式の読み込み if (favoritesXml.exists()) { DocInfo docInfo; InputStream is = favoritesXml.openStream(); try { docInfo = readDocumentType(is); } finally { is.close(); } if (docInfo == null) { throw new IOException("unknown document type :" + characterData); } is = favoritesXml.openStream(); try { loadPartsSet(characterData, is, docInfo); } finally { is.close(); } } // xml形式の読み込みに成功したらシリアライズする. try { Collection partsSetsOrg = characterData.getPartsSets().values(); ArrayList partsSets = new ArrayList(partsSetsOrg); favoritesSer.save(partsSets); } catch (Exception ex) { logger.log(Level.WARNING, "cached favorites creation failed. :" + characterData, ex); // シリアライズに失敗しても継続する. } } /** * 入力ストリームからパーツセット定義(Favorites.xml)を読み込んで、characterDataに追加登録する.
* @param characterData お気に入りを登録されるキャラクターデータ * @param inpstm お気に入りのxmlへの入力ストリーム * @param docInfo ドキュメントタイプ * @throws IOException 読み込みに失敗した場合 */ protected void loadPartsSet(CharacterData characterData, InputStream inpstm, DocInfo docInfo) throws IOException { if (characterData == null || inpstm == null || docInfo == null) { throw new IllegalArgumentException(); } Schema schema = loadSchema(docInfo); DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); factory.setNamespaceAware(true); factory.setSchema(schema); Document doc; try { DocumentBuilder builder = factory.newDocumentBuilder(); final ArrayList errors = new ArrayList(); builder.setErrorHandler(new ErrorHandler() { public void error(SAXParseException exception) throws SAXException { errors.add(exception); } public void fatalError(SAXParseException exception) throws SAXException { errors.add(exception); } public void warning(SAXParseException exception) throws SAXException { errors.add(exception); } }); doc = builder.parse(inpstm); if (errors.size() > 0) { throw errors.get(0); } } catch (ParserConfigurationException ex) { throw new RuntimeException("JAXP Configuration Failed.", ex); } catch (SAXException ex) { IOException ex2 = new IOException("PartsSet invalid format"); ex2.initCause(ex); throw ex2; } try { XPath xpath = createXPath(docInfo); Node nodePartssets = (Node) xpath.evaluate("/pre:partssets", doc, XPathConstants.NODE); if (nodePartssets != null) { loadPartsSet(characterData, nodePartssets, false, docInfo); } } catch (XPathExpressionException ex) { IOException ex2 = new IOException("PartsSet invalid format"); ex2.initCause(ex2); throw ex2; } } /** * 既存のキャラクター定義を削除する.
* 有効なdocBaseがあり、そのxmlファイルが存在するものについて、削除を行う.
* forceRemoveがtrueでない場合はキャラクター定義 character.xmlファイルの拡張子を * リネームすることでキャラクター定義として認識させなくする.
* forceRevmoeがtrueの場合は実際にファイルを削除する.
* character.xml、favorites、workingsetのキャッシュも削除される.
* @param cd キャラクター定義 * @param forceRemove ファイルを削除する場合はtrue、リネームして無効にするだけならfalse * @throws IOException 削除またはリネームできなかった場合 */ public void remove(CharacterData cd, boolean forceRemove) throws IOException { if (cd == null || cd.getDocBase() == null) { throw new IllegalArgumentException(); } URI docBase = cd.getDocBase(); File xmlFile = new File(docBase); if (!xmlFile.exists() || !xmlFile.isFile()) { // すでに存在しない場合 return; } // character.xml キャッシュの削除 UserData serializedFile = getCharacterDataCacheUserFile(docBase); if (serializedFile.exists()) { logger.log(Level.INFO, "remove file: " + serializedFile); serializedFile.delete(); } // favories.xml/serの削除 UserData[] favoritesDatas = new UserData[] { !forceRemove ? null : getFavoritesUserData(cd, "xml"), getFavoritesUserData(cd, "ser"), }; for (UserData favoriteData : favoritesDatas) { if (favoriteData != null && favoriteData.exists()) { logger.log(Level.INFO, "remove file: " + favoriteData); favoriteData.delete(); } } // ワーキングセットの削除 UserData workingSetSer = MainFrame.getWorkingSetUserData(cd, true); if (workingSetSer != null && workingSetSer.exists()) { logger.log(Level.INFO, "remove file: " + workingSetSer); workingSetSer.delete(); } // xmlファイルの拡張子を変更することでキャラクター定義として認識させない. // (削除に失敗するケースに備えて先にリネームする.) String suffix = "." + System.currentTimeMillis() + ".deleted"; File bakFile = new File(xmlFile.getPath() + suffix); if (!xmlFile.renameTo(bakFile)) { throw new IOException("can not rename configuration file.:" + xmlFile); } // ディレクトリ File baseDir = xmlFile.getParentFile(); if ( !forceRemove) { // 削除されたディレクトリであることを識別できるようにディレクトリ名も変更する. File parentBak = new File(baseDir.getPath() + suffix); if (!baseDir.renameTo(parentBak)) { throw new IOException("can't rename directory. " + baseDir); } } else { // 完全に削除する removeRecursive(baseDir); } } /** * 指定したファイルを削除します.
* 指定したファイルがディレクトリを示す場合、このディレクトリを含む配下のすべてのファイルとディレクトリを削除します.
* @param file ファイル、またはディレクトリ * @throws IOException 削除できない場合 */ protected void removeRecursive(File file) throws IOException { if (file == null) { throw new IllegalArgumentException(); } if ( !file.exists()) { return; } if (file.isDirectory()) { for (File child : file.listFiles()) { removeRecursive(child); } } if (!file.delete()) { throw new IOException("can't delete file. " + file); } } protected Iterable iterable(final NodeList nodeList) { final int mx; if (nodeList == null) { mx = 0; } else { mx = nodeList.getLength(); } return new Iterable() { public Iterator iterator() { return new Iterator() { private int idx = 0; public boolean hasNext() { return idx < mx; } public Node next() { if (idx >= mx) { throw new NoSuchElementException(); } return nodeList.item(idx++); } public void remove() { throw new UnsupportedOperationException(); } }; } }; } protected Schema loadSchema(DocInfo docInfo) throws IOException { if (docInfo == null) { throw new IllegalArgumentException(); } String schemaName = null; if ("character".equals(docInfo.getFirstElementName())) { if ("http://com.exmaple/charactermanaj".equals(docInfo.getNamespace())) { schemaName = CHARACTER_XML_SCHEMA_0_8; } else if ("http://charactermanaj.sourceforge.jp/schema/charactermanaj".equals(docInfo.getNamespace())) { schemaName = CHARACTER_XML_SCHEMA; } } else if ("partssets".equals(docInfo.getFirstElementName())) { if ("http://com.exmaple/charactermanaj".equals(docInfo.getNamespace())) { schemaName = PARTSSET_XML_SCHEMA_0_8; } else if ("http://charactermanaj.sourceforge.jp/schema/charactermanaj".equals(docInfo.getNamespace())) { schemaName = PARTSSET_XML_SCHEMA; } } if (schemaName == null) { throw new IOException("unsupported namespace: " + docInfo); } Schema schema = schemaMap.get(schemaName); if (schema != null) { return schema; } URL schemaURL = null; try { SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); schemaFactory.setErrorHandler(errorHandler); schemaURL = getEmbeddedResourceURL(schemaName); schema = schemaFactory.newSchema(schemaURL); schemaMap.put(schemaName, schema); return schema; } catch (Exception ex) { throw new RuntimeException("schema creation failed. :" + schemaURL, ex); } } protected URL getEmbeddedResourceURL(String schemaName) { return this.getClass().getResource(schemaName); } protected XPath createXPath(DocInfo docInfo) { if (docInfo == null) { throw new IllegalArgumentException(); } final String namespace; if (docInfo.getNamespace() != null && docInfo.getNamespace().length() > 0) { namespace = docInfo.getNamespace(); } else { namespace = NS; } XPathFactory xpathFactory = XPathFactory.newInstance(); XPath xpath = xpathFactory.newXPath(); xpath.setNamespaceContext(new NamespaceContext() { public String getNamespaceURI(String prefix) { if (prefix == null) { throw new IllegalArgumentException(); } if (prefix.equals("pre")) { return namespace; } if (prefix.equals("xml")) { return XMLConstants.XML_NS_URI; } return XMLConstants.NULL_NS_URI; } public Iterator getPrefixes(String namespaceURI) { throw new UnsupportedOperationException(); } public String getPrefix(String namespaceURI) { throw new UnsupportedOperationException(); } }); return xpath; } /** * サンプルピクチャを読み込む.
* ピクチャが存在しなければnullを返す. * キャラクター定義がValidでない場合は常にnullを返す.
* @param characterData キャラクター定義、null不可 * @param loader イメージのローダー、null不可 * @return ピクチャのイメージ、もしくはnull * @throws IOException ピクチャの読み取りに失敗した場合 */ public BufferedImage loadSamplePicture(CharacterData characterData, ImageLoader loader) throws IOException { if (characterData == null || loader == null) { throw new IllegalArgumentException(); } if (!characterData.isValid()) { return null; } File sampleImageFile = getSamplePictureFile(characterData); if (sampleImageFile != null && sampleImageFile.exists()) { LoadedImage loadedImage = loader.load(new FileImageResource(sampleImageFile)); return loadedImage.getImage(); } return null; } /** * キャラクターのサンプルピクチャが登録可能であるか?
* キャラクターデータが有効であり、且つ、ファイルの書き込みが可能であればtrueを返す.
* キャラクターデータがnullもしくは無効であるか、ファイルプロトコルでないか、ファイルが書き込み禁止であればfalseょ返す.
* @param characterData キャラクターデータ * @return 書き込み可能であればtrue、そうでなければfalse */ public boolean canSaveSamplePicture(CharacterData characterData) { if (characterData == null || !characterData.isValid()) { return false; } File sampleImageFile = getSamplePictureFile(characterData); if (sampleImageFile != null) { if (sampleImageFile.exists() && sampleImageFile.canWrite()) { return true; } if (!sampleImageFile.exists()) { File parentDir = sampleImageFile.getParentFile(); if (parentDir != null) { return parentDir.canWrite(); } } } return false; } /** * サンプルピクチャとして認識されるファイル位置を返す.
* ファイルが実在するかは問わない.
* DocBaseが未設定であるか、ファィルプロトコルとして返せない場合はnullを返す.
* @param characterData キャラクター定義 * @return サンプルピクチャの保存先のファイル位置、もしくはnull */ protected File getSamplePictureFile(CharacterData characterData) { if (characterData == null) { throw new IllegalArgumentException(); } URI docBase = characterData.getDocBase(); if (docBase != null && "file".endsWith(docBase.getScheme())) { File docBaseFile = new File(docBase); return new File(docBaseFile.getParentFile(), SAMPLE_IMAGE_FILENAME); } return null; } /** * サンプルピクチャを保存する. * @param characterData キャラクターデータ * @param samplePicture サンプルピクチャ * @throws IOException 保存に失敗した場合 */ public void saveSamplePicture(CharacterData characterData, BufferedImage samplePicture) throws IOException { if (!canSaveSamplePicture(characterData)) { throw new IOException("can not write a sample picture.:" + characterData); } File sampleImageFile = getSamplePictureFile(characterData); // canSaveSamplePictureで書き込み先検証済み if (samplePicture != null) { // 登録または更新 // pngで保存するので背景色は透過になるが、一応、コードとしては入れておく。 AppConfig appConfig = AppConfig.getInstance(); Color sampleImageBgColor = appConfig.getSampleImageBgColor(); ImageSaveHelper imageSaveHelper = new ImageSaveHelper(); imageSaveHelper.savePicture(samplePicture, sampleImageBgColor, sampleImageFile, null); } else { // 削除 if (sampleImageFile.exists()) { if (!sampleImageFile.delete()) { throw new IOException("sample pucture delete failed. :" + sampleImageFile); } } } } /** * パーツ管理情報をDocBaseと同じフォルダ上のparts-info.xmlに書き出す.
* XML生成中に失敗した場合は既存の管理情報は残される.
* (管理情報の書き込み中にI/O例外が発生した場合は管理情報は破壊される.)
* * @param docBase character.xmlの位置 * @param partsManageData パーツ管理情報 * @throws IOException 出力に失敗した場合 */ public void savePartsManageData(URI docBase, PartsManageData partsManageData) throws IOException { if (docBase == null || partsManageData == null) { throw new IllegalArgumentException(); } if ( !"file".equals(docBase.getScheme())) { throw new IOException("ファイル以外はサポートしていません: " + docBase); } File docBaseFile = new File(docBase); File baseDir = docBaseFile.getParentFile(); // データからXMLを構築してストリームに出力する. // 完全に成功したXMLのみ書き込むようにするため、一旦バッファする。 ByteArrayOutputStream bos = new ByteArrayOutputStream(); try { savePartsManageData(partsManageData, bos); } finally { bos.close(); } // バッファされたXMLデータを実際のファイルに書き込む File partsInfoXML = new File(baseDir, "parts-info.xml"); FileOutputStream os = new FileOutputStream(partsInfoXML); try { os.write(bos.toByteArray()); } finally { os.close(); } } /** * パーツ管理情報をXMLとしてストリームに書き出す.
* @param partsManageData パーツ管理データ * @param outstm 出力先ストリーム * @throws IOException 出力に失敗した場合 */ public void savePartsManageData(PartsManageData partsManageData, OutputStream outstm) throws IOException { if (partsManageData == null || outstm == null) { throw new IllegalArgumentException(); } Document doc; try { DocumentBuilderFactory factory = DocumentBuilderFactory .newInstance(); factory.setNamespaceAware(true); DocumentBuilder builder = factory.newDocumentBuilder(); doc = builder.newDocument(); } catch (ParserConfigurationException ex) { throw new RuntimeException("JAXP Configuration Exception.", ex); } Locale locale = Locale.getDefault(); String lang = locale.getLanguage(); Element root = doc.createElementNS(NS_PARTSDEF, "parts-definition"); root.setAttribute("xmlns:xml", XMLConstants.XML_NS_URI); root.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance"); root.setAttribute("xsi:schemaLocation", NS_PARTSDEF + " parts-definition.xsd"); doc.appendChild(root); // 作者情報を取得する Collection partsAuthors = partsManageData.getAuthorInfos(); for (PartsAuthorInfo partsAuthorInfo : partsAuthors) { String author = partsAuthorInfo.getAuthor(); if (author == null || author.length() == 0) { continue; } // 作者情報の登録 Element nodeAuthor = doc.createElementNS(NS_PARTSDEF, "author"); Element nodeAuthorName = doc.createElementNS(NS_PARTSDEF, "name"); Attr attrLang = doc.createAttributeNS(XMLConstants.XML_NS_URI, "lang"); attrLang.setValue(lang); nodeAuthorName.setAttributeNodeNS(attrLang); nodeAuthorName.setTextContent(author); nodeAuthor.appendChild(nodeAuthorName); String homepageURL = partsAuthorInfo.getHomePage(); if (homepageURL != null && homepageURL.length() > 0) { Element nodeHomepage = doc.createElementNS(NS_PARTSDEF, "home-page"); Attr attrHomepageLang = doc.createAttributeNS(XMLConstants.XML_NS_URI, "lang"); attrHomepageLang.setValue(lang); nodeHomepage.setAttributeNodeNS(attrHomepageLang); nodeHomepage.setTextContent(homepageURL); nodeAuthor.appendChild(nodeHomepage); } root.appendChild(nodeAuthor); Collection partsKeys = partsManageData.getPartsKeysByAuthor(author); // ダウンロード別にパーツキーの集約 HashMap> downloadMap = new HashMap>(); for (PartsKey partsKey : partsKeys) { PartsManageData.PartsVersionInfo versionInfo = partsManageData.getVersionStrict(partsKey); String downloadURL = versionInfo.getDownloadURL(); if (downloadURL == null) { downloadURL = ""; } List partsKeyGrp = downloadMap.get(downloadURL); if (partsKeyGrp == null) { partsKeyGrp = new ArrayList(); downloadMap.put(downloadURL, partsKeyGrp); } partsKeyGrp.add(partsKey); } // ダウンロード別にパーツ情報の登録 ArrayList downloadURLs = new ArrayList(downloadMap.keySet()); Collections.sort(downloadURLs); for (String downloadURL : downloadURLs) { List partsKeyGrp = downloadMap.get(downloadURL); Collections.sort(partsKeyGrp); Element nodeDownload = doc.createElementNS(NS_PARTSDEF, "download-url"); nodeDownload.setTextContent(downloadURL); root.appendChild(nodeDownload); for (PartsKey partsKey : partsKeyGrp) { PartsManageData.PartsVersionInfo versionInfo = partsManageData.getVersionStrict(partsKey); Element nodeParts = doc.createElementNS(NS_PARTSDEF, "parts"); nodeParts.setAttribute("name", partsKey.getPartsName()); if (partsKey.getCategoryId() != null) { nodeParts.setAttribute("category", partsKey.getCategoryId()); } if (versionInfo.getVersion() > 0) { nodeParts.setAttribute("version", Double.toString(versionInfo.getVersion())); } String localizedName = partsManageData.getLocalizedName(partsKey); if (localizedName != null && localizedName.trim().length() > 0) { Element nodeLocalizedName = doc.createElementNS(NS_PARTSDEF, "local-name"); Attr attrLocalizedNameLang = doc.createAttributeNS(XMLConstants.XML_NS_URI, "lang"); attrLocalizedNameLang.setValue(lang); nodeLocalizedName.setAttributeNodeNS(attrLocalizedNameLang); nodeLocalizedName.setTextContent(localizedName); nodeParts.appendChild(nodeLocalizedName); } root.appendChild(nodeParts); } } } // output xml TransformerFactory txFactory = TransformerFactory.newInstance(); txFactory.setAttribute("indent-number", Integer.valueOf(4)); Transformer tfmr; try { tfmr = txFactory.newTransformer(); } catch (TransformerConfigurationException ex) { throw new RuntimeException("JAXP Configuration Failed.", ex); } tfmr.setOutputProperty(OutputKeys.INDENT, "yes"); // http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4504745 final String encoding = "UTF-8"; tfmr.setOutputProperty("encoding", encoding); try { tfmr.transform(new DOMSource(doc), new StreamResult(new OutputStreamWriter(outstm, Charset.forName(encoding)))); } catch (TransformerException ex) { IOException ex2 = new IOException("XML Convert failed."); ex2.initCause(ex); throw ex2; } } /** * 指定したDocBaseと同じフォルダにあるparts-info.xmlからパーツ管理情報を取得して返す.
* ファイルが存在しない場合は空のインスタンスを返す.
* 返されるインスタンスは編集可能です.
* @param docBase character.xmlの位置 * @return パーツ管理情報、存在しない場合は空のインスタンス * @throws IOException 読み込み中に失敗した場合 */ public PartsManageData loadPartsManageData(URI docBase) throws IOException { if (docBase == null) { throw new IllegalArgumentException(); } if ( !"file".equals(docBase.getScheme())) { throw new IOException("ファイル以外はサポートしていません。:" + docBase); } File docBaseFile = new File(docBase); File baseDir = docBaseFile.getParentFile(); // パーツ管理情報ファイルの確認 final File partsInfoXML = new File(baseDir, "parts-info.xml"); if (!partsInfoXML.exists()) { // ファイルが存在しなければ空を返す. return new PartsManageData(); } PartsManageData partsManageData; InputStream is = new FileInputStream(partsInfoXML); try { partsManageData = loadPartsManageData(is); } finally { is.close(); } return partsManageData; } public PartsManageData loadPartsManageData(InputStream is) throws IOException { if (is == null) { throw new IllegalArgumentException(); } // パーツ管理情報 final PartsManageData partsManageData = new PartsManageData(); // SAXParserの準備 SAXParser saxParser; try { SAXParserFactory saxPartserFactory = SAXParserFactory.newInstance(); saxPartserFactory.setNamespaceAware(true); saxParser = saxPartserFactory.newSAXParser(); } catch (Exception ex) { throw new RuntimeException("JAXP Configuration failed.", ex); } // デフォルトのロケールから言語を取得 final Locale locale = Locale.getDefault(); final String lang = locale.getLanguage(); try { // 要素のスタック final LinkedList stack = new LinkedList(); // DOMではなくSAXで読み流す. saxParser.parse(is, new DefaultHandler() { private StringBuilder buf = new StringBuilder(); private PartsAuthorInfo partsAuthorInfo; private String authorName; private String homepageURL; private String authorNameLang; private String homepageLang; private String downloadURL; private String partsLocalNameLang; private String partsLocalName; private String partsCategoryId; private String partsName; private double partsVersion; @Override public void startDocument() throws SAXException { logger.log(Level.FINEST, "parts-info : start"); } @Override public void endDocument() throws SAXException { logger.log(Level.FINEST, "parts-info : end"); } @Override public void characters(char[] ch, int start, int length) throws SAXException { buf.append(ch, start, length); } @Override public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { stack.addFirst(qName); int mx = stack.size(); if (mx >= 2 && stack.get(1).equals("parts")) { if ("local-name".equals(qName)) { partsLocalNameLang = attributes.getValue(XMLConstants.XML_NS_URI, "lang"); } } else if (mx >= 2 && stack.get(1).equals("author")) { if ("name".equals(qName)) { authorNameLang = attributes.getValue(XMLConstants.XML_NS_URI, "lang"); } else if ("home-page".equals(qName)) { homepageLang = attributes.getValue(XMLConstants.XML_NS_URI, "lang"); } } else if ("author".equals(qName)) { partsAuthorInfo = null; authorName = null; authorNameLang = null; homepageURL = null; homepageLang = null; } else if ("download-url".equals(qName)) { downloadURL = null; } else if ("parts".equals(qName)) { partsLocalName = null; partsLocalNameLang = null; partsCategoryId = attributes.getValue("category"); partsName = attributes.getValue("name"); String strVersion = attributes.getValue("version"); try { if (strVersion == null || strVersion.length() == 0) { partsVersion = 0.; } else { partsVersion = Double.parseDouble(strVersion); if (partsVersion < 0) { partsVersion = 0; } } } catch (Exception ex) { logger.log(Level.INFO, "parts-info.xml: invalid version." + strVersion); partsVersion = 0; } } buf = new StringBuilder(); } @Override public void endElement(String uri, String localName, String qName) throws SAXException { int mx = stack.size(); if (mx >= 2 && "parts".equals(stack.get(1))) { if ("local-name".equals(qName)) { if (partsLocalName == null || lang.equals(partsLocalNameLang)) { partsLocalName = buf.toString(); } } } else if (mx >= 2 && "author".equals(stack.get(1))) { if ("name".equals(qName)) { if (authorName == null || lang.equals(authorNameLang)) { authorName = buf.toString(); } } else if ("home-page".equals(qName)) { if (homepageURL == null || lang.equals(homepageLang)) { homepageURL = buf.toString(); } } } else if ("author".equals(qName)) { logger.log(Level.FINE, "parts-info: author: " + authorName + " /homepage:" + homepageURL); if (authorName != null && authorName.length() > 0) { partsAuthorInfo = new PartsAuthorInfo(); partsAuthorInfo.setAuthor(authorName); partsAuthorInfo.setHomePage(homepageURL); } else { partsAuthorInfo = null; } } else if ("download-url".equals(qName)) { downloadURL = buf.toString(); logger.log(Level.FINE, "parts-info: download-url: " + downloadURL); } else if ("parts".equals(qName)) { if (logger.isLoggable(Level.FINE)) { logger.log(Level.FINE, "parts-info.xml: parts-name: " + partsName + " /category: " + partsCategoryId + " /parts-local-name: " + partsLocalName + " /version:" + partsVersion); } PartsManageData.PartsVersionInfo versionInfo = new PartsManageData.PartsVersionInfo(); versionInfo.setVersion(partsVersion); versionInfo.setDownloadURL(downloadURL); PartsManageData.PartsKey partsKey = new PartsManageData.PartsKey(partsName, partsCategoryId); partsManageData.putPartsInfo(partsKey, partsLocalName, partsAuthorInfo, versionInfo); } stack.removeFirst(); } }); } catch (SAXException ex) { IOException ex2 = new IOException("parts-info.xml read failed."); ex2.initCause(ex); throw ex2; } return partsManageData; } /** * character.iniファイルから、キャラクター定義を生成します.
* docBaseは設定されていないため、戻り値に設定する必要があります.
* @param is character.iniの入力ストリーム * @return キャラクターデータ * @throws IOException 読み取りに失敗した場合 */ public CharacterData readCharacterDataFromIni(InputStream is) throws IOException { if (is == null) { throw new IllegalArgumentException(); } CharacterData cd = createDefaultCharacterData(); // イメージサイズ int siz_x = 0; int siz_y = 0; // パーツセット HashMap plainPartsSet = new HashMap(); try { BufferedReader rd; try { rd = new BufferedReader(new InputStreamReader(is, "MS932")); // SJISで読み取る. } catch (UnsupportedEncodingException ex) { logger.log(Level.SEVERE, "SJIS encoded file cannot be read.", ex); rd = new BufferedReader(new InputStreamReader(is)); // システムデフォルトで読み込む } try { String line; int sectionMode = 0; while ((line = rd.readLine()) != null) { line = line.trim(); if (line.length() == 0) { continue; } if (line.startsWith("[")) { // セクションの判定 if (line.toLowerCase().equals("[size]")) { // Sizeセクション sectionMode = 1; } else if (line.toLowerCase().equals("[parts]")) { // Partsセクション sectionMode = 2; } else { // それ以外のセクションなのでモードをリセット. // 色情報のセクション「Color」は現在のところサポートしていない. sectionMode = 0; } } else { int eqpos = line.indexOf('='); String key, val; if (eqpos >= 0) { // キーは小文字に揃える (大小を無視して比較できるようにするため) key = line.substring(0, eqpos).toLowerCase().trim(); val = line.substring(eqpos + 1); } else { key = line.toLowerCase().trim(); val = ""; } if (sectionMode == 1) { // Sizeセクション try { if (key.equals("size_x")) { siz_x = Integer.parseInt(val); } else if (key.equals("size_y")) { siz_y = Integer.parseInt(val); } } catch (RuntimeException ex) { logger.log(Level.WARNING, "character.ini invalid. key=" + key + "/val=" + val, ex); // 変換できないものは無視する. } } else if (sectionMode == 2) { // Partsセクション if (key.length() > 0) { plainPartsSet.put(key, val); } } } } } finally { rd.close(); } } catch (IOException ex) { // エラーが発生したら、character.iniは無かったことにして続ける. logger.log(Level.WARNING, "character.ini invalid.", ex); return null; } finally { try { is.close(); } catch (IOException ex) { logger.log(Level.SEVERE, "can't close file.", ex); } } // イメージサイズの設定 if (siz_x > 0 && siz_y > 0) { cd.setImageSize(new Dimension(siz_x, siz_y)); } // パーツセットを構築する. boolean existsPartsetParts = false; if (!plainPartsSet.isEmpty()) { PartsSet partsSet = new PartsSet("default", "default", true); for (Map.Entry entry : plainPartsSet.entrySet()) { String categoryId = entry.getKey(); String partsName = entry.getValue(); PartsCategory partsCategory = cd.getPartsCategory(categoryId); if (partsCategory != null) { PartsIdentifier partsIdentifier; if (partsName == null || partsName.length() == 0) { partsIdentifier = null; } else { partsIdentifier = new PartsIdentifier(partsCategory, partsName, partsName); existsPartsetParts = true; } partsSet.appendParts(partsCategory, partsIdentifier, null); } } if (!partsSet.isEmpty() && existsPartsetParts) { // パーツセットが空でなく、 // なにかしらのパーツが登録されている場合のみパーツセットを登録する. cd.addPartsSet(partsSet); cd.setDefaultPartsSetId("default"); } } return cd; } /** * character.iniを読み取り、character.xmlを生成します.
* character.xmlのシリアライズされた中間ファイルも生成されます.
* すでにcharacter.xmlがある場合は上書きされます.
* 途中でエラーが発生した場合はcharacter.xmlは削除されます.
* @param characterIniFile 読み取るcharatcer.iniファイル * @param characterXmlFile 書き込まれるcharacter.xmlファイル * @throws IOException 失敗した場合 */ public void convertFromCharacterIni(File characterIniFile, File characterXmlFile) throws IOException { if (characterIniFile == null || characterXmlFile == null) { throw new IllegalArgumentException(); } // character.iniから、character.xmlの内容を構築する. FileInputStream is = new FileInputStream(characterIniFile); CharacterData characterData; try { characterData = readCharacterDataFromIni(is); } finally { is.close(); } // docBase URI docBase = characterXmlFile.toURI(); characterData.setDocBase(docBase); // character.xmlの書き込み boolean succeeded = false; try { FileOutputStream outstm = new FileOutputStream(characterXmlFile); try { writeXMLCharacterData(characterData, outstm); } finally { outstm.close(); } // キャッシュの生成 UserData serializedFile = getCharacterDataCacheUserFile(docBase); serializedFile.save(characterData); succeeded = true; } finally { if ( !succeeded) { // 途中で失敗した場合は生成ファイルを削除しておく. try { if (characterXmlFile.exists()) { characterXmlFile.delete(); } } catch (Exception ex) { logger.log(Level.WARNING, "ファイルの削除に失敗しました。:" + characterXmlFile, ex); } } } } /** * お勧めリンクリストが設定されていない場合(nullの場合)、デフォルトのお勧めリストを設定する.
* すでに設定されている場合(空を含む)は何もしない.
* @param characterData キャラクターデータ */ public void compensateRecommendationList(CharacterData characterData) { if (characterData == null) { throw new IllegalArgumentException(); } if (characterData.getRecommendationURLList() != null) { // 補填の必要なし return; } CharacterData defaultCd = createDefaultCharacterData(); characterData.setRecommendationURLList(defaultCd.getRecommendationURLList()); } } charactermanaj/src/charactermanaj/model/io/AbstractCharacterDataArchiveFile.java0000644000175000017500000007464411767047535030347 0ustar paulliupaulliupackage charactermanaj.model.io; import java.awt.image.BufferedImage; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.net.URI; import java.net.URISyntaxException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Locale; import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; import javax.imageio.ImageIO; import charactermanaj.graphics.io.PNGFileImageHeader; import charactermanaj.graphics.io.PNGFileImageHeaderReader; import charactermanaj.model.CharacterData; import charactermanaj.model.Layer; import charactermanaj.model.PartsCategory; import charactermanaj.model.PartsManageData; import charactermanaj.model.io.CharacterDataPersistent.DocInfo; public abstract class AbstractCharacterDataArchiveFile implements CharacterDataArchiveFile { private static final Logger logger = Logger.getLogger(AbstractCharacterDataArchiveFile.class.getName()); protected File archiveFile; protected String rootPrefix = ""; public interface FileContent { String getEntryName(); long lastModified(); InputStream openStream() throws IOException; } @Override public String toString() { return "CharacterDataArchiveFile(file=" + archiveFile + ")"; } public static final class CategoryLayerPair { private PartsCategory partsCategory; private Layer layer; public CategoryLayerPair(PartsCategory partsCategory, Layer layer) { if (partsCategory == null || layer == null) { throw new IllegalArgumentException(); } this.partsCategory = partsCategory; this.layer = layer; } @Override public int hashCode() { return partsCategory.hashCode() ^ layer.hashCode(); } @Override public boolean equals(Object obj) { if (obj == this) { return true; } if (obj != null && obj instanceof CategoryLayerPair) { CategoryLayerPair o = (CategoryLayerPair) obj; return partsCategory.equals(o.partsCategory) && layer.equals(o.layer); } return false; } public Layer getLayer() { return layer; } public PartsCategory getPartsCategory() { return partsCategory; } @Override public String toString() { return "(" + partsCategory + ":" + layer +")"; } } public static final class PartsImageContent implements FileContent { private final FileContent fileContent; private final Collection categoryLayerPairs; private final String dirName; private final String partsName; private final String fileName; private final PNGFileImageHeader pngFileImageHeader; /** * パーツイメージを示す.
* @param fileContent ファイルコンテンツ * @param categoryLayerPairs カテゴリとレイヤーのペア * @param partsName ファイル名(ファイル名のみ。拡張子を含まない。パーツ名の元として用いることを想定。) * @param pngFileImageHeader PNGファイルヘッダ */ protected PartsImageContent(FileContent fileContent, Collection categoryLayerPairs, String fileName, String partsName, PNGFileImageHeader pngFileImageHeader) { if (fileContent == null || categoryLayerPairs == null || categoryLayerPairs.isEmpty() || fileName == null || partsName == null || pngFileImageHeader == null) { throw new IllegalArgumentException(); } this.fileContent = fileContent; this.categoryLayerPairs = Collections.unmodifiableCollection(categoryLayerPairs); this.fileName = fileName; this.partsName = partsName; this.pngFileImageHeader = pngFileImageHeader; CategoryLayerPair categoryLayerPair = categoryLayerPairs.iterator().next(); dirName = categoryLayerPair.getLayer().getDir(); } @Override public int hashCode() { return getEntryName().hashCode(); } @Override public boolean equals(Object obj) { if (obj == this) { return true; } if (obj != null && obj instanceof PartsImageContent) { return getEntryName().equals(((PartsImageContent) obj).getEntryName()); } return false; } public Collection getCategoryLayerPairs() { return categoryLayerPairs; } public String getDirName() { return dirName; } public String getEntryName() { return fileContent.getEntryName(); } public String getFileName() { return fileName; } public String getPartsName() { return partsName; } public PNGFileImageHeader getPngFileImageHeader() { return pngFileImageHeader; } public long lastModified() { return fileContent.lastModified(); } public InputStream openStream() throws IOException { return fileContent.openStream(); } @Override public String toString() { return fileContent.getEntryName(); } } protected HashMap entries = new HashMap(); protected AbstractCharacterDataArchiveFile(File archiveFile) { if (archiveFile == null) { throw new IllegalArgumentException(); } this.archiveFile = archiveFile; } public File getArchiveFile() { return this.archiveFile; } protected void addEntry(FileContent fileContent) { if (fileContent == null) { throw new IllegalArgumentException(); } if (logger.isLoggable(Level.FINE)) { logger.log(Level.FINE, fileContent.getEntryName()); } entries.put(fileContent.getEntryName(), fileContent); } /** * アーカイブファイルをベースとしたURIを返す.
* @param name コンテンツ名 * @return URI * @throws IOException URIを生成できない場合 */ protected URI getContentURI(String name) throws IOException { try { URI baseURI = archiveFile.toURI(); return new URI("jar:" + baseURI.toString() + "/!" + name); } catch (URISyntaxException ex) { IOException iex = new IOException(ex.getMessage()); iex.initCause(ex); throw iex; } } /** * 指定したコンテンツが存在するか? * @param name コンテンツ名 * @return 存在すればtrue、存在しなければfalse */ public boolean hasContent(String name) { return entries.containsKey(name); } /** * 指定したコンテンツを取得する.
* 存在しない場合はnullを返す.
* @param name コンテンツ名 * @return 存在すればコンテンツ、存在しなければnull */ public FileContent getContent(String name) { return entries.get(name); } public String getRootPrefix() { return this.rootPrefix; } /** * アーカイブのルート上に単一のフォルダしかない場合、そのフォルダを真のルートとして設定する.
* 返されるルート名には末尾に「/」を含む.
* ルート上に複数のフォルダがあるかファイルが存在する場合は空文字を設定する.
*/ protected void searchRootPrefix() { HashSet dirs = new HashSet(); for (String name : entries.keySet()) { int pos = name.indexOf('/'); if (pos < 0) { // ルート上にファイルがあるので、ここがルート rootPrefix = ""; return; } if (pos >= 0) { String dir = name.substring(0, pos + 1); dirs.add(dir); } } if (dirs.size() == 1) { // ルート上に単一のフォルダしかないので、 // このフォルダが真のルート rootPrefix = dirs.iterator().next(); return; } // ルート上に複数のフォルダがあるので、ここがルート rootPrefix = ""; } /** * 指定したディレクトリ(フルパス指定)のファイルのみを取り出す.
* サブフォルダは含まない.
* ディレクトリに空文字またはnullまたは「/」を指定した場合はルートを意味する.
* @param dir ディレクトリ * @return ファイルへのフルパスのコレクション */ public Map getFiles(String dir) { if (dir == null) { dir = ""; } if (dir.length() > 0 && !dir.endsWith("/")) { dir += "/"; } if (dir.equals("/")) { dir = ""; // アーカイブ内コンテンツのパスは先頭は「/」ではないため。 } HashMap files = new HashMap(); int ep = dir.length(); for (Map.Entry entry : entries.entrySet()) { String name = entry.getKey(); FileContent fileContent = entry.getValue(); if (name.startsWith(dir)) { String suffix = name.substring(ep); int sep = suffix.indexOf('/'); if (sep < 0) { files.put(name, fileContent); } } } return files; } /** * キャラクター定義を読み込む.
* アーカイブに存在しないか、妥当性がない場合はnullを返す.
* @return キャラクター定義、もしくはnull */ public CharacterData readCharacterData() { FileContent characterFile = entries.get(rootPrefix + CharacterDataPersistent.CONFIG_FILE); if (characterFile == null) { return null; } CharacterDataPersistent persist = CharacterDataPersistent.getInstance(); try { // character.xmlとして妥当な文書であるか検査する. DocInfo docInfo; InputStream is = characterFile.openStream(); try { docInfo = persist.readDocumentType(is); logger.log(Level.INFO, docInfo == null ? "not xml document" : docInfo.toString()); } finally { is.close(); } if (docInfo == null || !"character".equals(docInfo.getFirstElementName())) { return null; } // character.xmlを読み込む CharacterData cd; is = characterFile.openStream(); try { URI docBase = getContentURI(rootPrefix + CharacterDataPersistent.CONFIG_FILE); cd = persist.loadCharacterDataFromXML(is, docBase, docInfo); } finally { is.close(); } return cd; } catch (Exception ex) { logger.log(Level.INFO, "character.xml load failed.", ex); return null; } } /** * キャラクター定義をINIファイルより読み取る.
* アーカイブのコンテンツルート上のcharacter.iniを探す.
* それがなければ、アーカイブ上のどこかにある/character.iniを探して、もっとも短い場所にある1つを採用する.
* character.iniが何処にも存在しないか、読み取り時にエラーとなった場合はnullを返す.
* 「キャラクターなんとか機 v2.2」の設定ファイルを想定している.
* @return キャラクター定義、もしくはnull */ public CharacterData readCharacterINI() { FileContent characterFile = null; characterFile = entries.get(rootPrefix + CharacterDataPersistent.COMPATIBLE_CONFIG_NAME); if (characterFile == null) { // どこかにあるcharacter.iniを探す ArrayList characterInis = new ArrayList(); for (Map.Entry entry : entries.entrySet()) { String entryName = entry.getKey(); if (entryName.endsWith("/" + CharacterDataPersistent.COMPATIBLE_CONFIG_NAME)) { characterInis.add(entryName); } } // もっとも短い名前のものを採用 Collections.sort(characterInis); if (characterInis.size() > 0) { characterFile = entries.get(characterInis.get(0)); } } if (characterFile == null) { // character.iniがないので何もしない. return null; } try { // デフォルトのキャラクター定義を構築する. CharacterDataPersistent persist = CharacterDataPersistent.getInstance(); CharacterData cd; InputStream is = characterFile.openStream(); try { cd = persist.readCharacterDataFromIni(is); } finally { is.close(); } // docBaseを設定する. URI docBase = getContentURI(rootPrefix + CharacterDataPersistent.COMPATIBLE_CONFIG_NAME); cd.setDocBase(docBase); return cd; } catch (Exception ex) { logger.log(Level.INFO, "character.ini load failed", ex); return null; } } /** * お気に入りを読み込みキャラクター定義に追加する.
* アーカイブにお気に入り(favorites.xml)が存在しなければ何もしない.
* 読み取り中に失敗した場合は、その時点で読み込みを止めて戻る.
* @param characterData キャラクター定義(お気に入りが追加される) */ public void readFavorites(CharacterData characterData) { if (characterData == null) { throw new IllegalArgumentException("characterDataにnullは指定できません。"); } FileContent favoritesXml = entries.get(rootPrefix + "favorites.xml"); if (favoritesXml == null) { // favorites.xmlがなければ何もしない return; } CharacterDataPersistent persist = CharacterDataPersistent.getInstance(); try { // favorites.xmlとして妥当な文書であるか検査する. DocInfo docInfo; InputStream is = favoritesXml.openStream(); try { docInfo = persist.readDocumentType(is); logger.log(Level.INFO, docInfo == null ? "not xml document" : docInfo.toString()); } finally { is.close(); } if (docInfo == null || !"partssets".equals(docInfo.getFirstElementName())) { // favorites.xmlの文書でない場合は何もしない. return; } // favorites.xmlを読み込む is = favoritesXml.openStream(); try { persist.loadPartsSet(characterData, is, docInfo); } catch (Exception ex) { logger.log(Level.INFO, "favorites.xml load failed.", ex); } finally { is.close(); } } catch (Exception ex) { logger.log(Level.INFO, "favorites.xml load failed", ex); } } /** * サンプルピクチャを読み込む.
* アーカイブに存在しないか、画像として読み取れなかった場合はnull * @return サンプルピクチャ、もしくはnull */ public BufferedImage readSamplePicture() { FileContent samplePictureFile = entries.get(rootPrefix + "preview.png"); if (samplePictureFile == null) { Map files = getFiles(rootPrefix); samplePictureFile = files.get("preview.jpg"); if (samplePictureFile == null) { samplePictureFile = files.get("preview.jpeg"); } if (samplePictureFile == null) { for (Map.Entry entry : files.entrySet()) { String name = entry.getKey(); if (name.endsWith(".jpg") || name.endsWith(".jpeg") || name.endsWith(".png")) { samplePictureFile = entry.getValue(); break; } } } } if (samplePictureFile == null) { return null; } try { BufferedImage pic; InputStream is = samplePictureFile.openStream(); try { pic = ImageIO.read(is); } finally { is.close(); } return pic; } catch (Exception ex) { logger.log(Level.INFO, "sample picture load failed.", ex); return null; } } /** * アーカイブにある「readme.txt」、もしくは「readme」というファイルをテキストファイルとして読み込む.
* readme.txtが優先される。ともに存在しない場合はnull.
* 改行コードはプラットフォーム固有の改行コードに変換して返される.
* 読み取れなかった場合はnull.
* @return テキスト、もしくはnull */ public String readReadMe() { Locale locale = Locale.getDefault(); String lang = locale.getLanguage(); ArrayList candiates = new ArrayList(); Map files = getFiles(rootPrefix); for (String findName : new String[] { "readme_" + lang + ".txt", "readme_" + lang, "readme.txt", "readme", null }) { for (Map.Entry entry : files.entrySet()) { String name = entry.getKey().toLowerCase(); if (findName == null && name.endsWith(".txt")) { candiates.add(entry.getValue()); break; } if (name.equals(findName)) { candiates.add(entry.getValue()); } } } if (candiates.isEmpty()) { return null; } // みつかったファイルについて読み込みを優先順で試行する. for (FileContent file : candiates) { try { return readTextUTF16(file); } catch (Exception ex) { logger.log(Level.WARNING, "read file failed. :" + file, ex); // 無視して次の候補を試行する. } } // すべて失敗したか、そもそもファイルがみつからなかった。 return null; } /** * ファイルをテキストとして読み取り、返す.
* UTF-16LE/BE/UTF-8についてはBOMにより判定する.
* BOMがない場合はUTF-16/8ともに判定できない.
* BOMがなければMS932もしくはEUC_JPであると仮定して読み込む.
* 改行コードはプラットフォーム固有の改行コードに変換して返される.
* @param name コンテンツ名 * @return テキスト、コンテンツが存在しない場合はnull * @throws IOException 読み込みに失敗した場合 */ public String readTextFile(String name) throws IOException { if (name == null) { throw new IllegalArgumentException(); } FileContent content = entries.get(name); if (content == null) { return null; } return readTextUTF16(content); } /** * ファイルをテキストとして読み取り、返す.
* UTF-16LE/BE/UTF-8についてはBOMにより判定する.
* BOMがない場合はUTF-16/8ともに判定できない.
* BOMがなければMS932もしくはEUC_JPであると仮定して読み込む.
* @param content コンテンツ * @return テキスト * @throws IOException 読み込みに失敗した場合 */ public String readTextUTF16(FileContent content) throws IOException { if (content == null) { throw new IllegalArgumentException(); } // 一旦メモリに取り込む ByteArrayOutputStream bos = new ByteArrayOutputStream(); InputStream is = content.openStream(); try { int ch; while ((ch = is.read()) != -1) { bos.write((byte) ch); } } finally { is.close(); } byte[] buf = bos.toByteArray(); String enc = null; if (buf.length >= 2) { // Windowsのメモ帳はUTF-16にBOMをつけるので、これで判定できる。 // 本アプリケーションのエクスポート時もUTF-16LEのBOM付きで出力する。 // 一般的なエディタはUTF-16BEにはBOMをつけないので、事前に判定することはできない。 if ((buf[0] & 0xff) == 0xff && (buf[1] & 0xff) == 0xfe) { enc = "UTF-16LE"; } else if ((buf[0] & 0xff) == 0xfe && (buf[1] & 0xff) == 0xff) { enc = "UTF-16BE"; } } if (enc == null && buf.length >= 3) { if ((buf[0] & 0xff) == 0xef && (buf[1] & 0xff) == 0xbb && (buf[1] & 0xff) == 0xbf) { // Windowsのメモ帳などはUTF-8にBOMをつけるので、これで判定できる。 // 一般的なエディタではUTF-8のBOMはつけないのでUTF-8であるかどうかを事前判定することはできない。 enc = "UTF-8"; } } if (enc == null) { // BOMがない場合はMS932かEUC_JPのいずれかであろう、と仮定する。 enc = "JISAutoDetect"; // SJIS/EUC_JPの自動判定 } // 文字列として変換 StringBuilder str = new StringBuilder(); InputStreamReader rd = new InputStreamReader(new ByteArrayInputStream(buf), enc); try { int ch; while ((ch = rd.read()) != -1) { str.append((char) ch); } } finally { rd.close(); } // 改行コードをプラットフォーム固有のものに変換 String line = str.toString(); line = line.replace("\r\n", "\n"); line = line.replace("\r", "\n"); line = line.replace("\n", System.getProperty("line.separator")); return line; } /** * キャラクター定義のカテゴリと、そのレイヤー情報から、画像のディレクトリの一覧をアーカイブ上のディレクトリの一覧として返す.
* ディレクトリの末尾は常にスラ付きとなる.
* enabledRootPefixがtrueの場合、ディレクトリの先頭はアーカイブのコンテンツルートとなる.
* 同一のディレクトリに対して複数のレイヤー(複数カテゴリを含む)が参照している場合、それらを含めて返す.
* 参照されているディレクトリがない場合は返される結果には含まれない.
* @param characterData キャラクター定義 * @param enabledRootPrefix ルートプレフィックス(アーカイブのコンテンツルート)を付与する場合 * @return パーツで使用する画像のディレクトリとして認識されるディレクトリの一覧、キーはアーカイブのディレクトリ位置、値は参照する1つ以上のレイヤー */ protected Map> getLayerDirs(CharacterData characterData, boolean enabledRootPrefix) { if (characterData == null) { return Collections.emptyMap(); } // イメージディレクトリの一覧 String rootPrefix = getRootPrefix(); HashMap> layerDirs = new HashMap>(); for (PartsCategory partsCategory : characterData.getPartsCategories()) { for (Layer layer : partsCategory.getLayers()) { String dir = layer.getDir(); if ( !dir.endsWith("/")) { dir += "/"; // スラ付きにする. } if (enabledRootPrefix) { // アーカイブのルートコンテキストからのディレクトリ位置とする. dir = rootPrefix + dir; } Collection sameDirLayers = layerDirs.get(dir); if (sameDirLayers == null) { sameDirLayers = new ArrayList(); layerDirs.put(dir, sameDirLayers); } sameDirLayers.add(new CategoryLayerPair(partsCategory, layer)); } } return layerDirs; } /** * アーカイブに含まれるフォルダをもつpngファイルからパーツイメージを取得する.
* * @param インポート先のキャラクターデータ * 、フォルダ名などを判別するため。nullの場合はディレクトリなしとみなす.
* @param newly * 新規インポート用であるか?(新規でない場合は引数で指定したキャラクターセットと同じパーツは読み込まれない). * (アーカイブファイルからの読み込みでは無視される) * @return パーツイメージコンテンツのコレクション、なければ空 */ public Collection getPartsImageContents(CharacterData characterData, boolean newly) { // コンテンツルートからの絶対位置指定でパーツイメージを取得する. Collection results = getPartsImageContentsStrict(characterData); if (results.isEmpty()) { // コンテンツルートからの絶対位置にパーツがない場合は、任意のディレクトリ位置からパーツイメージを推定する. results = getPartsImageContentsLazy(characterData); } return results; } /** * コンテンツルートからの絶対位置のフォルダからpngファイルからパーツイメージを取得する.
* @param インポート先のキャラクターデータ、フォルダ名などを判別するため。nullの場合はディレクトリなしとみなす.
* @return パーツイメージコンテンツのコレクション、なければ空 */ protected Collection getPartsImageContentsStrict(CharacterData characterData) { final Map> layerDirMap = getLayerDirs(characterData, true); CategoryLayerPairResolveStrategy strategy = new CategoryLayerPairResolveStrategy() { public Collection resolveCategoryLayerPairs(String dir) { Collection categoryLayerPairs = layerDirMap.get(dir); if (categoryLayerPairs == null || categoryLayerPairs.isEmpty()) { // ディレクトリ名に一致するものがないので、この画像は無視する return null; } return categoryLayerPairs; } }; return getPartsImageContents(strategy); } /** * アーカイブに含まれる任意のフォルダからpngファイルからパーツイメージを取得する.
* ディレクトリ名の大文字・小文字は区別されません.
* @param インポート先のキャラクターデータ、フォルダ名などを判別するため。nullの場合はディレクトリなしとみなす.
* @return パーツイメージコンテンツのコレクション、なければ空 */ protected Collection getPartsImageContentsLazy(CharacterData characterData) { final Map> layerDirMap = getLayerDirs(characterData, false); CategoryLayerPairResolveStrategy strategy = new CategoryLayerPairResolveStrategy() { public Collection resolveCategoryLayerPairs(String dir) { dir = (dir == null) ? "" : dir.toLowerCase(); for (Map.Entry> entry : layerDirMap.entrySet()) { String dirSuffix = entry.getKey().toLowerCase(); Collection categoryLayerPairs = entry.getValue(); if (dir.endsWith(dirSuffix)) { return categoryLayerPairs; } } return null; } }; return getPartsImageContents(strategy); } /** * ディレクトリ名からカテゴリとレイヤーを取得するためのインターフェイス.
* @author seraphy */ protected interface CategoryLayerPairResolveStrategy { /** * ディレクトリを指定して、それに該当するカテゴリとレイヤーペアのコレクションを返します.
* 同一のディレクトリに対して複数のレイヤーが割り当てられている可能性があるためコレクションで返されます.
* 空のコレクションにはなりません.
* レイヤーとして認識されていないディレクトリの場合はnullを返します.
* @param dir ディレクトリ * @return カテゴリ・レイヤーのペアのコレクション、またはnull (空のコレクションにはならない。) */ Collection resolveCategoryLayerPairs(String dir); } /** * アーカイブに含まれるフォルダをもつpngファイルからパーツイメージを取得する。 * @param strategy ディレクトリが売れ入れ可能であるか判断するストラテジー * @return パーツイメージコンテンツのコレクション、なければ空 */ protected Collection getPartsImageContents(CategoryLayerPairResolveStrategy strategy) { if (strategy == null) { throw new IllegalArgumentException(); } ArrayList results = new ArrayList(); for (Map.Entry entry : entries.entrySet()) { String name = entry.getKey(); FileContent fileContent = entry.getValue(); String[] split = name.split("/"); if (split.length < 2) { // 最低でもフォルダ下になければならない continue; } String lastName = split[split.length - 1]; if (!lastName.toLowerCase().endsWith(".png")) { // png拡張子でなければならない continue; } // ディレクトリ名 String dir = name.substring(0, name.length() - lastName.length()); // ディレクトリ名から対応するレイヤーを取得します. Collection categoryLayerPairs = strategy.resolveCategoryLayerPairs(dir); if (categoryLayerPairs == null) { // パーツイメージのディレクトリとして定義されていない場合は、この画像は無視される. continue; } // PNGファイルヘッダの取得と確認 PNGFileImageHeader pngFileHeader = readPNGFileHeader(fileContent); if (pngFileHeader == null) { // PNGファイルとして不正なものは無視する. logger.log(Level.WARNING, "invalid png: " + name); continue; } // パーツ名(拡張子を除いたもの) String partsName; int extpos = lastName.lastIndexOf('.'); partsName = lastName.substring(0, extpos); PartsImageContent partsImageContent = new PartsImageContent( fileContent, categoryLayerPairs, lastName, partsName, pngFileHeader); results.add(partsImageContent); } return results; } /** * PNGファイルとしてファイルを読み込みPNGヘッダ情報を返します.
* PNGでないか、ファイルの読み込みに失敗した場合はnullを返します.
* @param fileContent 画像ファイル * @return PNGヘッダ情報、またはnull */ protected PNGFileImageHeader readPNGFileHeader(FileContent fileContent) { PNGFileImageHeaderReader pngHeaderReader = PNGFileImageHeaderReader.getInstance(); PNGFileImageHeader pngFileHeader = null; try { InputStream is = fileContent.openStream(); try { pngFileHeader = pngHeaderReader.readHeader(is); } finally { is.close(); } } catch (IOException ex) { logger.log(Level.WARNING, "not png header. " + fileContent, ex); pngFileHeader = null; // 無視する. } return pngFileHeader; } /** * アーカイブに含まれるparts-info.xmlを読み込み返す.
* 存在しなければ空のインスタンスを返す.
* @return パーツ管理データ * @throws IOException */ public PartsManageData getPartsManageData() throws IOException { FileContent content = entries.get(rootPrefix + "parts-info.xml"); if (content == null) { return new PartsManageData(); } PartsManageData partsManageData; InputStream is = content.openStream(); try { CharacterDataPersistent persist = CharacterDataPersistent.getInstance(); partsManageData = persist.loadPartsManageData(is); } finally { is.close(); } return partsManageData; } } charactermanaj/src/charactermanaj/model/io/PartsImageCollectionParser.java0000644000175000017500000000664111767047535027310 0ustar paulliupaulliupackage charactermanaj.model.io; import java.util.List; import java.util.Map; import charactermanaj.graphics.filters.ColorConvertParameter; import charactermanaj.graphics.io.ImageResource; import charactermanaj.model.ColorInfo; import charactermanaj.model.Layer; import charactermanaj.model.PartsColorInfo; import charactermanaj.model.PartsFiles; import charactermanaj.model.PartsIdentifier; import charactermanaj.model.PartsSet; import charactermanaj.model.PartsSpec; import charactermanaj.model.PartsSpecResolver; /** * パーツセットから複合画像イメージを生成するために必要なイメージリソースを抽出する. * @author seraphy * */ public class PartsImageCollectionParser { /** * 抽出された複合画像イメージの個々のイメージソースと、カラー情報を受け取るハンドラ.
* @author seraphy */ public interface PartsImageCollectionHandler { /** * 個々のイメージリソースとカラー情報を受け取るハンドラ.
* * @param partsIdentifier 対象のパーツ識別子 * @param layer パーツのレイヤー * @param imageResource パーツのレイヤーの画像リソース * @param param カラー情報、設定されていない場合はnull */ void detectImageSource(PartsIdentifier partsIdentifier, Layer layer, ImageResource imageResource, ColorConvertParameter param); } /** * パーツ設定を解決するためのインターフェイス */ protected PartsSpecResolver partsSpecResolver; /** * パーツ設定のリゾルバを指定して構築する * @param partsSpecResolver パーツ設定のリゾルバ */ public PartsImageCollectionParser(PartsSpecResolver partsSpecResolver) { if (partsSpecResolver == null) { throw new IllegalArgumentException("resolver is null"); } this.partsSpecResolver = partsSpecResolver; } public PartsSpecResolver getPartsSpecResolver() { return this.partsSpecResolver; } /** * パーツセットを指定して複合画像を生成するために必要なイメージソースおよびカラー設定を解決する.
* protectedなので派生クラスで呼び出すか、publicに昇格させる.
* @param partsSet パーツセット */ public void parse(PartsSet partsSet, PartsImageCollectionHandler listener) { if (listener == null) { throw new IllegalArgumentException("listener is null"); } if (partsSet == null) { throw new IllegalArgumentException("PartsSet is null"); } for (List partsIdentifiers : partsSet.values()) { for (PartsIdentifier partsIdentifier : partsIdentifiers) { PartsColorInfo partsColorInfo = partsSet.getColorInfo(partsIdentifier); PartsSpec partsSpec = partsSpecResolver.getPartsSpec(partsIdentifier); if (partsSpec != null) { PartsFiles partsFiles = partsSpec.getPartsFiles(); for (Map.Entry entry : partsFiles.entrySet()) { Layer layer = entry.getKey(); ImageResource file = entry.getValue(); ColorConvertParameter param = null; if (partsColorInfo != null) { ColorInfo colorInfo = partsColorInfo.get(layer); if (colorInfo != null) { param = colorInfo.getColorParameter(); } } listener.detectImageSource(partsIdentifier, layer, file, param); } } } } } } charactermanaj/src/charactermanaj/model/io/CharacterDataWriter.java0000644000175000017500000000152511767047535025742 0ustar paulliupaulliupackage charactermanaj.model.io; import java.awt.image.BufferedImage; import java.io.IOException; import java.util.Map; import java.util.Properties; import charactermanaj.model.CharacterData; import charactermanaj.model.PartsIdentifier; import charactermanaj.model.PartsSpec; public interface CharacterDataWriter { void writeExportProp(Properties prop) throws IOException; void writeCharacterData(CharacterData characterData) throws IOException; void writeTextUTF16LE(String name, String contents) throws IOException; void writeSamplePicture(BufferedImage samplePicture) throws IOException; void writePartsImages(Map partsImages) throws IOException; void writePartsManageData(Map partsImages) throws IOException; void close() throws IOException; } charactermanaj/src/charactermanaj/model/io/PartsDataLoader.java0000644000175000017500000000114311767047535025065 0ustar paulliupaulliupackage charactermanaj.model.io; import java.util.Map; import charactermanaj.model.PartsCategory; import charactermanaj.model.PartsIdentifier; import charactermanaj.model.PartsSpec; /** * パーツデータのロードを行うインターフェイス * @author seraphy */ public interface PartsDataLoader { /** * カテゴリを指定してパーツデータをロードする.
* 該当がない場合は空が返される * @param category カテゴリ * @return パーツデータのマップ */ Map load(PartsCategory category); } charactermanaj/src/charactermanaj/model/PartsColorManager.java0000644000175000017500000003025311767047535025033 0ustar paulliupaulliupackage charactermanaj.model; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; import charactermanaj.graphics.filters.ColorConvertParameter; /** * パーツごとの色情報を設定・取得したり、最後に設定した色情報を記憶するなどの色情報アクセスのためのクラス.
* @author seraphy * */ public class PartsColorManager { private static final long serialVersionUID = 3246538139597289650L; private static final Logger logger = Logger.getLogger(PartsColorManager.class.getName()); /** * カテゴリごとのパーツカラー情報.
* @author seraphy */ public static final class CategoryColorInfo { private final PartsColorInfo partsColorInfo; private final boolean applyAll; public CategoryColorInfo(PartsColorInfo partsColorInfo, boolean applyAll) { this.partsColorInfo = partsColorInfo; this.applyAll = applyAll; } public PartsColorInfo getPartsColorInfo() { return partsColorInfo; } public boolean isApplyAll() { return applyAll; } } /** * パーツ単位でのカラーグループを含む色情報.
* カテゴリ全体に適用される場合はパーツ単位の色情報はリセットする.
*/ private HashMap partsColorInfoMap = new HashMap(); /** * カテゴリごとに共通となる場合のカラーグループを含む色情報.
* パーツ単位の色情報が定義されていない場合、カテゴリ単位での情報が使用される.
*/ private HashMap categoryColorInfoMap = new HashMap(); /** * カラーグループごとの色情報.
*/ private HashMap recentColorGroupMap = new HashMap(); /** * パーツ設定のリゾルバ */ private PartsSpecResolver partsSpecResolver; /** * パーツ設定リゾルバを指定して構築する. * @param partsSpecResolver リゾルバ */ public PartsColorManager(PartsSpecResolver partsSpecResolver) { if (partsSpecResolver == null) { throw new IllegalArgumentException(); } this.partsSpecResolver = partsSpecResolver; } /** * パーツ識別子ごとの色情報を取得します.
* まだ一度も登録されていない場合は、現在の状態から色情報を作成して返します.
* その場合、registered引数がtrueである場合は生成と同時に初期値として登録済みとする.
* そうでない場合は生成された色情報は一時的なものとなる. * @param partsIdentifier パーツ識別子 * @param registered 色情報を新規に作成した場合に登録する場合はtrue * @return 色情報 */ public PartsColorInfo getPartsColorInfo(PartsIdentifier partsIdentifier, boolean registered) { if (partsIdentifier == null) { throw new IllegalArgumentException(); } // パーツ識別子ごとのカラー情報を取得する. PartsColorInfo partsColorInfo = partsColorInfoMap.get(partsIdentifier); if (partsColorInfo == null) { // パーツ識別子ごとのカラー情報が設定されていない場合は // カテゴリ別情報からカラー情報を生成する. partsColorInfo = createDefaultColorInfo(partsIdentifier); if (registered) { // 生成されたカラー情報をパーツ識別子ごとのカラー情報に適用する. partsColorInfoMap.put(partsIdentifier, partsColorInfo); } } return partsColorInfo; } /** * パーツ識別子ごとのパーツ色情報を保存します.
* @param partsIdentifier パーツ識別子 * @param partsColorInfo パーツの色情報 * @param applyAll パーツ識別子ではなく、カテゴリに対して保存する場合 */ public void setPartsColorInfo(PartsIdentifier partsIdentifier, PartsColorInfo partsColorInfo, boolean applyAll) { if (partsIdentifier == null || partsColorInfo == null) { throw new IllegalArgumentException(); } partsColorInfo = partsColorInfo.clone(); PartsCategory partsCategory = partsIdentifier.getPartsCategory(); if (applyAll) { // カテゴリ指定の場合 // パーツ個別色をリセットすることでカテゴリを優先させる. resetPartsColorInfo(partsCategory); if (logger.isLoggable(Level.FINEST)) { logger.log(Level.FINEST, "setPartsColorInfo(Category): " + partsIdentifier + "=" + partsColorInfo); } } else { // パーツ個別指定の場合 partsColorInfoMap.put(partsIdentifier, partsColorInfo); if (logger.isLoggable(Level.FINEST)) { logger.log(Level.FINEST, "setPartsColorInfo(Parts): " + partsIdentifier + "=" + partsColorInfo); } } // カラーグループとしての最新のカラー情報を保存する.(有効なカラーグループで連動指定がある場合のみ) // ただし、「すべてに適用」でない場合は保存しない. if (applyAll) { setRecentColorGroup(partsColorInfo); } // カテゴリごとの最新の色情報を設定する. // (「すべてに適用」であるか、単数選択カテゴリで、まだ「すべてに適用」の色情報がない場合のみ.) // (複数選択カテゴリの場合は明示的に「すべてに適用」を選択していないかぎり保存されない.) CategoryColorInfo categoryColorInfo = categoryColorInfoMap.get(partsCategory); if (applyAll || (!partsCategory.isMultipleSelectable() && (categoryColorInfo == null || !categoryColorInfo.isApplyAll()))) { categoryColorInfo = new CategoryColorInfo(partsColorInfo, applyAll); categoryColorInfoMap.put(partsCategory, categoryColorInfo); } } /** * パーツの色情報を指定して、パーツ識別子の各レイーヤの色グループ情報を保存します.
* 連動指定が有効であり、有効なカラーグループである場合のみ保存されます.
* @param partsColorInfo パーツ識別子 */ protected void setRecentColorGroup(PartsColorInfo partsColorInfo) { if (partsColorInfo == null) { return; } for (Map.Entry entry : partsColorInfo.entrySet()) { ColorInfo colorInfo = entry.getValue(); ColorGroup colorGroup = colorInfo.getColorGroup(); if (colorInfo.isSyncColorGroup() && colorGroup != null && colorGroup.isEnabled()) { ColorConvertParameter colorParam = colorInfo.getColorParameter(); if (colorParam != null) { colorParam = colorParam.clone(); } ColorConvertParameter oldColorParam = recentColorGroupMap.put(colorGroup, colorParam); if (logger.isLoggable(Level.FINEST)) { if ( !ColorConvertParameter.equals(colorParam, oldColorParam)) { logger.log(Level.FINEST, "setRecentColorGroup(" + colorGroup + ")=" + colorParam); } } } } } /** * カラーグループごとのもっとも最近に設定した色情報を取得します.
* カラーグループが有効でないか、まだ一度も登録されていない場合はnullが返されます.
* @param colorGroup 色グループ、null可 * @return 色情報、もしくはnull */ protected ColorConvertParameter getRecentColorGroup(ColorGroup colorGroup) { if (colorGroup == null || !colorGroup.isEnabled()) { return null; } ColorConvertParameter colorParam = recentColorGroupMap.get(colorGroup); if (colorParam != null) { colorParam = colorParam.clone(); } return colorParam; } /** * 指定したパーツカテゴリの色情報を取得します.
* 登録がない場合はnullが返されます.
* @param partsCategory パーツカテゴリ * @return 指定したパーツカテゴリの色情報、またはnull */ public CategoryColorInfo getPartsColorInfo(PartsCategory partsCategory) { return categoryColorInfoMap.get(partsCategory); } /** * すべてのパーツ識別子の色情報をリセットします.
*/ public void resetPartsColorInfo() { resetPartsColorInfo(null); } /** * 指定したカテゴリと、カテゴリに属するパーツ識別子ごとの色情報をリセットします.
* 引数partsCategoryがnullの場合は全パーツ識別子、全カテゴリと、すべてのカラーグループをリセットします.
* @param partsCategory パーツカテゴリまたはnull */ public void resetPartsColorInfo(PartsCategory partsCategory) { if (partsCategory == null) { recentColorGroupMap.clear(); partsColorInfoMap.clear(); categoryColorInfoMap.clear(); return; } categoryColorInfoMap.remove(partsCategory); Iterator> ite = partsColorInfoMap.entrySet().iterator(); while (ite.hasNext()) { Map.Entry entry = ite.next(); PartsIdentifier partsIdentifier = entry.getKey(); if (partsIdentifier.getPartsCategory().equals(partsCategory)) { ite.remove(); } } } /** * パーツ識別子ごとの色情報を現在の状態から新たに構築する. * @param partsIdentifier パーツ識別子 * @return 色情報 */ protected PartsColorInfo createDefaultColorInfo(PartsIdentifier partsIdentifier) { PartsCategory category = partsIdentifier.getPartsCategory(); PartsColorInfo partsColorInfo = new PartsColorInfo(category); // パーツ固有のカラーグループの指定があるか? PartsSpec partsSpec = partsSpecResolver.getPartsSpec(partsIdentifier); ColorGroup partsSpecColorGroup = null; if (partsSpec != null) { partsSpecColorGroup = partsSpec.getColorGroup(); } if (partsSpecColorGroup != null && partsSpecColorGroup.isEnabled()) { // パーツ固定のカラーグループの指定があれば // 全レイヤーを該当カラーグループに設定する. for (Map.Entry entry : partsColorInfo.entrySet()) { ColorInfo colorInfo = entry.getValue(); colorInfo = colorInfo.clone(); colorInfo.setColorGroup(partsSpecColorGroup); colorInfo.setSyncColorGroup(true); entry.setValue(colorInfo); } } else { // パーツ固有のカラーグループがなければ // 同一カテゴリの最近設定されたカラー情報をもとに、パーツカラー情報を作成する. CategoryColorInfo categoryColorInfo = categoryColorInfoMap.get(category); if (categoryColorInfo != null) { PartsColorInfo categoryPartsColorInfo = categoryColorInfo.getPartsColorInfo(); for (Map.Entry entry : categoryPartsColorInfo.entrySet()) { Layer layer = entry.getKey(); ColorInfo colorInfo = entry.getValue(); if (colorInfo != null && partsColorInfo.containsKey(layer)) { colorInfo = colorInfo.clone(); // ただし、同一カテゴリに設定されたカラー情報が「すべてに適用」でない場合は、 // レイヤー固有のカラーグループを維持する. if ( !categoryColorInfo.isApplyAll()) { ColorGroup layerColorGroup = layer.getColorGroup(); if (layerColorGroup == null) { layerColorGroup = ColorGroup.NA; } colorInfo.setColorGroup(layerColorGroup); } partsColorInfo.put(layer, colorInfo); } } } } // カラーグループが指定されている場合、もっとも最近に設定されたカラーグループの色情報に設定し直す for (Map.Entry entry : partsColorInfo.entrySet()) { ColorInfo colorInfo = entry.getValue(); ColorGroup colorGroup = colorInfo.getColorGroup(); if (colorGroup != null && colorGroup.isEnabled() && colorInfo.isSyncColorGroup()) { ColorConvertParameter param = getRecentColorGroup(colorGroup); if (param != null) { colorInfo.setColorParameter(param); } } } return partsColorInfo; } public Map getPartsColorInfoMap() { return partsColorInfoMap; } } charactermanaj/src/charactermanaj/model/PartsManageData.java0000644000175000017500000002421411767047535024444 0ustar paulliupaulliupackage charactermanaj.model; import java.util.AbstractCollection; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.Map; /** * パーツ管理情報.
* パーツ識別子のかわりに、パーツキーを用いる.
* パーツキーは、CategoryIdがnullであることを許可している.
* カテゴリを省略し、パーツ名だけでパーツ管理情報を検索できるようにするためのもの.
* @author seraphy */ public class PartsManageData extends AbstractCollection { /** * パーツキー.
* パーツ識別子 {@link PartsIdentifier} とほぼ同等であるが、カテゴリがnullであることを許可している点が異なる.
* @author seraphy */ public static final class PartsKey implements Comparable { private final String partsName; private final String categoryId; public PartsKey(String partsName) { this(partsName, null); } public PartsKey(String partsName, String categoryId) { if (partsName == null || partsName.length() == 0) { throw new IllegalArgumentException(); } if (categoryId != null && categoryId.trim().length() == 0) { categoryId = null; } this.partsName = partsName; this.categoryId = categoryId; } public PartsKey(PartsIdentifier partsIdentifier) { if (partsIdentifier == null) { throw new IllegalArgumentException(); } this.partsName = partsIdentifier.getPartsName(); this.categoryId = partsIdentifier.getPartsCategory().getCategoryId(); } public int compareTo(PartsKey o) { int ret = partsName.compareTo(o.partsName); if (ret == 0) { if (categoryId == null || o.categoryId == null) { ret = (categoryId == o.categoryId) ? 0 : (categoryId == null ? -1 : 1); } } return ret; } public String getCategoryId() { return categoryId; } public String getPartsName() { return partsName; } @Override public int hashCode() { return partsName.hashCode(); } @Override public boolean equals(Object obj) { if (obj == this) { return true; } if (obj != null && obj instanceof PartsKey) { PartsKey o = (PartsKey) obj; if (partsName.equals(o.partsName)) { return categoryId == null ? (o.categoryId == null) : categoryId.equals(o.categoryId); } } return false; } } /** * パーツごとのバージョンとダウンロードURL情報を保持するホルダークラス.
* @author seraphy */ public static final class PartsVersionInfo { private double version; private String downloadURL; public PartsVersionInfo() { super(); } public PartsVersionInfo(double version, String downloadURL) { this.version = version; this.downloadURL = downloadURL; } public double getVersion() { return version; } public String getDownloadURL() { return downloadURL; } public void setVersion(double version) { this.version = version; } public void setDownloadURL(String downloadURL) { this.downloadURL = downloadURL; } } /** * パーツキーと、それに対する作者情報 */ private HashMap partsAuthorInfoMap = new HashMap(); /** * パーツキーと、それに対するローカライズ名 */ private HashMap partsLocalizedNameMap = new HashMap(); /** * パーツキーと、それに対するバージョン情報 */ private HashMap partsVersionInfoMap = new HashMap(); /** * すべてクリアする.
*/ @Override public void clear() { partsAuthorInfoMap.clear(); partsLocalizedNameMap.clear(); partsVersionInfoMap.clear(); } /** * パーツキーに結びつく、各種情報を登録する.
* @param partsKey パーツキー * @param localizedName ローカライズ名(なければnull) * @param partsAuthorInfo 作者情報 (なければnull) * @param versionInfo バージョン情報 (なければnull) */ public void putPartsInfo(PartsKey partsKey, String localizedName, PartsAuthorInfo partsAuthorInfo, PartsVersionInfo versionInfo) { if (partsKey == null) { throw new IllegalArgumentException(); } partsAuthorInfoMap.put(partsKey, partsAuthorInfo); partsLocalizedNameMap.put(partsKey, localizedName); partsVersionInfoMap.put(partsKey, versionInfo); } /** * パーツキーを指定して該当する作者情報を取得する.
* 完全に一致する作者情報がない場合は、カテゴリを無視して、パーツキーのパーツ名(ID)の一致する、いずれかの作者情報を返す.
* @param partsKey パーツキー * @return 作者情報、完全に一致するものがなく、且つ、パーツ名(ID)に一致する情報もない場合はnull */ public PartsAuthorInfo getPartsAuthorInfo(PartsKey partsKey) { if (partsKey == null) { return null; } PartsAuthorInfo authorInfo = partsAuthorInfoMap.get(partsKey); if (authorInfo == null) { for (Map.Entry entry : partsAuthorInfoMap.entrySet()) { PartsKey key = entry.getKey(); if (key.getPartsName().equals(partsKey.getPartsName())) { authorInfo = entry.getValue(); break; } } } return authorInfo; } /** * パーツキーと完全に一致する作者情報を取得する.
* 存在しない場合はnullを返す.
* @param partsKey パーツキー * @return 作者情報、もしくはnull */ public PartsAuthorInfo getPartsAuthorInfoStrict(PartsKey partsKey) { if (partsKey == null) { return null; } return partsAuthorInfoMap.get(partsKey); } /** * パーツキーを指定して該当するバージョン情報を取得する.
* 完全に一致するバージョン情報がない場合は、カテゴリを無視して、パーツキーのパーツ名(ID)の一致する、いずれかのバージョン情報を返す.
* @param partsKey パーツキー * @return バージョン情報、完全に一致するものがなく、且つ、パーツ名(ID)に一致する情報もない場合はnull */ public PartsVersionInfo getVersion(PartsKey partsKey) { if (partsKey == null) { return null; } PartsVersionInfo versionInfo = partsVersionInfoMap.get(partsKey); if (versionInfo == null) { for (Map.Entry entry : partsVersionInfoMap.entrySet()) { PartsKey key = entry.getKey(); if (key.getPartsName().equals(partsKey.getPartsName())) { versionInfo = entry.getValue(); break; } } } return versionInfo; } /** * パーツキーを指定して該当するローカライズ名を取得する.
* 完全に一致するバージョン情報がない場合は、カテゴリを無視して、パーツキーのパーツ名(ID)の一致する、いずれかのローカライズ名を返す.
* @param partsKey パーツキー * @return バージョン情報、完全に一致するものがなく、且つ、パーツ名(ID)に一致する情報もない場合はnull */ public String getLocalizedName(PartsKey partsKey) { if (partsKey == null) { return null; } String localizedName = partsLocalizedNameMap.get(partsKey); if (localizedName == null) { for (Map.Entry entry : partsLocalizedNameMap.entrySet()) { PartsKey key = entry.getKey(); if (key.getPartsName().equals(partsKey.getPartsName())) { localizedName = entry.getValue(); break; } } } return localizedName; } /** * パーツキーと完全に一致するバージョン情報を取得する.
* @param partsKey パーツキー * @return パーツキーに完全に一致するバージョン情報、該当がなければnull */ public PartsVersionInfo getVersionStrict(PartsKey partsKey) { if (partsKey == null) { return null; } return partsVersionInfoMap.get(partsKey); } /** * パーツキーと完全に一致するローカライズ名を取得する.
* @param partsKey パーツキー * @return パーツキーに完全に一致するローカライズ名、該当がなければnull */ public String getLocalizedNameStrict(PartsKey partsKey) { if (partsKey == null) { return null; } return partsLocalizedNameMap.get(partsKey); } /** * すべての作者情報を返す.
* @return 作者情報のコレクション */ public Collection getAuthorInfos() { HashMap authorInfos = new HashMap(); for (PartsAuthorInfo authorInfo : partsAuthorInfoMap.values()) { if (authorInfo != null) { String author = authorInfo.getAuthor(); if (author != null && author.length() > 0) { authorInfos.put(author, authorInfo); } } } return authorInfos.values(); } /** * 指定した作者に該当する登録されているパーツキーの一覧を返す.
* @param author 作者 * @return 作者に該当するパーツキー */ public Collection getPartsKeysByAuthor(String author) { if (author == null) { return Collections.emptyList(); } ArrayList partsKeys = new ArrayList(); for (Map.Entry entry : partsAuthorInfoMap.entrySet()) { PartsKey partsKey = entry.getKey(); PartsAuthorInfo partsAuthorInfo = entry.getValue(); if (partsAuthorInfo != null) { String author2 = partsAuthorInfo.getAuthor(); if (author2 == null) { author2 = ""; } if (author2.equals(author)) { partsKeys.add(partsKey); } } } return partsKeys; } @Override public Iterator iterator() { return partsAuthorInfoMap.keySet().iterator(); } @Override public int size() { return partsAuthorInfoMap.size(); } } charactermanaj/src/charactermanaj/model/PartsFiles.java0000644000175000017500000000331511767047535023523 0ustar paulliupaulliupackage charactermanaj.model; import java.io.Serializable; import java.util.AbstractMap; import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.Set; import charactermanaj.graphics.io.ImageResource; public class PartsFiles extends AbstractMap implements Serializable { private static final long serialVersionUID = 5799830380308843243L; private HashMap partsMap = new HashMap(); private final PartsIdentifier partsIdentifier; public PartsFiles(PartsIdentifier partsName) { if (partsName == null) { throw new IllegalArgumentException(); } this.partsIdentifier = partsName; } public PartsIdentifier getPartsIdentifier() { return partsIdentifier; } @Override public Set> entrySet() { return Collections.unmodifiableSet(partsMap.entrySet()); } @Override public ImageResource put(final Layer key, final ImageResource value) { if (key == null || value == null) { throw new IllegalArgumentException(); } if (!partsIdentifier.hasLayer(key)) { throw new IllegalArgumentException(key.toString()); } return partsMap.put(key, value); } @Override public ImageResource get(Object key) { return partsMap.get(key); } @Override public boolean containsKey(Object key) { return partsMap.containsKey(key); } public long lastModified() { long maxLastModified = 0; for (ImageResource imageResource : values()) { long lastModified = imageResource.lastModified(); if (lastModified > maxLastModified) { maxLastModified = lastModified; } } return maxLastModified; } } charactermanaj/src/charactermanaj/model/util/0000755000175000017500000000000011767047535021557 5ustar paulliupaulliucharactermanaj/src/charactermanaj/model/util/StartupSupport.java0000644000175000017500000003720611767047535025471 0ustar paulliupaulliupackage charactermanaj.model.util; import java.io.File; import java.net.URI; import java.net.URL; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.Map; import java.util.UUID; import java.util.logging.Level; import java.util.logging.Logger; import charactermanaj.model.AppConfig; import charactermanaj.model.io.CharacterDataPersistent; import charactermanaj.util.DirectoryConfig; import charactermanaj.util.UserDataFactory; /** * 開始前の事前準備するためのサポートクラス * @author seraphy */ public abstract class StartupSupport { private static StartupSupport inst; /** * インスタンスを取得する. * @return シングルトンインスタンス */ public static synchronized StartupSupport getInstance() { if (inst == null) { inst = new StartupSupport() { private final Logger logger = Logger.getLogger(StartupSupport.class.getName()); @Override public void doStartup() { StartupSupport[] startups = { new PurgeOldLogs(), new UpgradeCache(), new UpgradeFavoritesXml(), new PurgeUnusedCache(), }; for (StartupSupport startup : startups) { logger.log(Level.FINE, "startup operation start. class=" + startup.getClass().getSimpleName()); try { startup.doStartup(); logger.log(Level.FINE, "startup operation is done."); } catch (Exception ex) { logger.log(Level.WARNING, "startup operation failed.", ex); } } } }; } return inst; } /** * ユーザディレクトリをアップグレードします. */ public abstract void doStartup(); } /** * 古い形式のCacheを移動する. * @author seraphy * */ class UpgradeCache extends StartupSupport { /** * ロガー */ private final Logger logger = Logger.getLogger(getClass().getName()); @Override public void doStartup() { UserDataFactory userDataFactory = UserDataFactory.getInstance(); File appData = userDataFactory.getSpecialDataDir(null); // ver0.94まではユーザディレクトリ直下に*.serファイルを配置していたが、 // ver0.95以降はcachesに移動したため、旧ファイルがのこっていれば移動する. for (File file : appData.listFiles()) { try { String name = file.getName(); if (file.isFile() && name.endsWith(".ser")) { File toDir = userDataFactory.getSpecialDataDir(name); if ( !appData.equals(toDir)) { String convertedName = convertName(name); File toFile = new File(toDir, convertedName); boolean ret = file.renameTo(toFile); logger.log(Level.INFO, "move file " + file + " to " + toFile + " ;successed=" + ret); } } } catch (Exception ex) { logger.log(Level.WARNING, "file move failed. " + file, ex); } } } protected String convertName(String name) { if (name.endsWith("-workingset.ser") || name.endsWith("-favorites.ser")) { // {UUID}-CharacterID-workingset.ser もしくは、-favorites.serの場合は、 // {UUID}-workingset.ser / {UUID}-favorites.serに変更する. String[] splitedName = name.split("-"); if (splitedName.length >= 7) { StringBuilder buf = new StringBuilder(); for (int idx = 0; idx < 5; idx++) { if (idx > 0) { buf.append("-"); } buf.append(splitedName[idx]); } buf.append("-"); buf.append(splitedName[splitedName.length - 1]); return buf.toString(); } } return name; } } /** * DocBaseのハッシュ値をもとにしたデータをサポートする抽象クラス. * @author seraphy * */ abstract class StartupSupportForDocBasedData extends StartupSupport { /** * character.xmlのファイル位置を示すUUID表現を算定するためのアルゴリズムの選択肢.
* @author seraphy */ protected enum DocBaseSignatureStoratage { /** * 新形式のcharacter.xmlのUUIDを取得(もしくは生成)する. * charatcer.xmlファイルのURIを文字列にしたもののタイプ3-UUID表現.
*/ NEW_FORMAT() { @Override public Map getDocBaseSignature(Collection characterXmlFiles) { HashMap uris = new HashMap(); for (File characterXmlFile : characterXmlFiles) { uris.put(characterXmlFile.toURI(), characterXmlFile); } UserDataFactory userDataFactory = UserDataFactory.getInstance(); HashMap results = new HashMap(); File storeDir = userDataFactory.getSpecialDataDir("*.ser"); for (Map.Entry entry : userDataFactory .getMangledNameMap(uris.keySet(), storeDir, true).entrySet()) { File characterXmlFile = uris.get(entry.getKey()); String mangledName = entry.getValue(); results.put(characterXmlFile, mangledName); } return results; } }, /** * 旧形式のcharacter.xmlのUUIDを取得する.
* charatcer.xmlファイルのURLを文字列にしたもののタイプ3-UUID表現.
*/ OLD_FORMAT() { @Override public Map getDocBaseSignature(Collection characterXmlFiles) { HashMap results = new HashMap(); for (File characterXmlFile : characterXmlFiles) { String mangledName; try { @SuppressWarnings("deprecation") URL url = characterXmlFile.toURL(); mangledName = UUID.nameUUIDFromBytes(url.toString().getBytes()).toString(); } catch (Exception ex) { logger.log(Level.WARNING, "character.xmlのファイル位置をUUID化できません。:" + characterXmlFile, ex); mangledName = null; } results.put(characterXmlFile, mangledName); } return results; } }, ; /** * ロガー */ private static Logger logger = Logger.getLogger( DocBaseSignatureStoratage.class.getName()); /** * character.xmlからuuid表現のプレフィックスを算定する. * @param characterXmlFile character.xmlのファイルのコレクション * @return character.xmlファイルと、それに対応するUUIDのマップ、(UUIDは該当がなければnull) */ public abstract Map getDocBaseSignature(Collection characterXmlFiles); } /** * すべてのユーザおよびシステムのキャラクターデータのDocBaseをもととした、 * キャッシュディレクトリ上のハッシュ値(Prefix)の文字列をキーとし、 * そのキャラクターディレクトリを値とするマップを返す.
* (新タイプの場合は実在するcharacter.xmlに対するmangledNameが生成され登録される.)
* @param storatage ハッシュ値を生成する戦略(旧タイプ・新タイプのUUIDの区別のため) * @return DocBaseをもととしたハッシュ値の文字列表記をキー、キャラクターディレクトリを値とするマップ */ protected Map getDocBaseMapInCaches(DocBaseSignatureStoratage storatage) { if (storatage == null) { throw new IllegalArgumentException(); } // キャラクターデータフォルダ DirectoryConfig dirConfig = DirectoryConfig.getInstance(); File[] charactersDirs = { dirConfig.getCharactersDir() // 現在のキャラクターデータフォルダ }; // キャラクターデータディレクトリを走査し、character.xmlファイルを収集する. ArrayList characterXmlFiles = new ArrayList(); for (File charactersDir : charactersDirs) { if (charactersDir == null || !charactersDir.exists() || !charactersDir.isDirectory()) { continue; } for (File characterDir : charactersDir.listFiles()) { if ( !characterDir.isDirectory()) { continue; } File characterXml = new File(characterDir, CharacterDataPersistent.CONFIG_FILE); if ( !characterXml.exists()) { continue; } characterXmlFiles.add(characterXml); } } // character.xmlファイルに対するハッシュ化文字列を取得する. Map docBaseSigMap = storatage.getDocBaseSignature(characterXmlFiles); // ハッシュ化文字列をキーとし、そのcharacter.xmlファイルを値とするマップに変換する. HashMap docBaseSignatures = new HashMap(); for (Map.Entry entry : docBaseSigMap.entrySet()) { docBaseSignatures.put(entry.getValue(), entry.getKey()); } return docBaseSignatures; } /** * 指定したディレクトリ直下にあるDocBaseのUUIDと推定される文字列で始まり、suffixで終わる * ファイルのUUIDの部分文字列をキーとし、そのファイルを値とするマップを返す. * @param dataDir 対象ディレクトリ * @param suffix 対象となるファイル名の末尾文字列、nullまたは空文字の場合は全て * @return DocBaseのUUID表現と思われる部分文字列をキーとし、そのファイルを値とするマップ */ protected Map getUUIDMangledNamedMap(File dataDir, String suffix) { if (dataDir == null || !dataDir.exists() || !dataDir.isDirectory()) { throw new IllegalArgumentException(); } Map uuidMangledFiles = new HashMap(); for (File file : dataDir.listFiles()) { String name = file.getName(); if (file.isFile() && (suffix == null || suffix.length() == 0 || name.endsWith(suffix))) { String[] sigParts = name.split("-"); if (sigParts.length >= 5) { // UUIDはa-b-c-d-eの5パーツになり、更に末尾に何らかの文字列が付与されるので // 区切り文字は5つ以上になる. // UUIDの部分だけ結合し直して復元する. StringBuilder sig = new StringBuilder(); for (int idx = 0; idx < 5; idx++) { if (idx != 0) { sig.append("-"); } sig.append(sigParts[idx]); } uuidMangledFiles.put(sig.toString(), file); } } } return uuidMangledFiles; } } /** * 古い形式のFavorites.xmlを移動する. * @author seraphy */ class UpgradeFavoritesXml extends StartupSupportForDocBasedData { /** * ロガー */ private final Logger logger = Logger.getLogger(getClass().getName()); @Override public void doStartup() { UserDataFactory userDataFactory = UserDataFactory.getInstance(); File appData = userDataFactory.getSpecialDataDir(null); // キャラクターデータディレクトリを走査しdocBaseの識別子の一覧を取得する Map docBaseSignatures = getDocBaseMapInCaches( DocBaseSignatureStoratage.OLD_FORMAT); // ver0.94までは*.favorite.xmlはユーザディレクトリ直下に配備していたが // ver0.95以降は各キャラクターディレクトリに移動するため、旧docbase-id-favorites.xmlが残っていれば移動する // ユーザディレクトリ直下にある*-facotites.xmlを列挙する Map favorites = getUUIDMangledNamedMap(appData, "-favorites.xml"); // 旧式の*-favorites.xmlを各キャラクターディレクトリに移動する. for (Map.Entry favoritesEntry : favorites.entrySet()) { String sig = favoritesEntry.getKey(); File file = favoritesEntry.getValue(); try { File characterDir = docBaseSignatures.get(sig); if (characterDir != null) { File toFile = new File(characterDir, "favorites.xml"); boolean ret = file.renameTo(toFile); logger.log(Level.INFO, "move file " + file + " to " + toFile + " ;successed=" + ret); } } catch (Exception ex) { logger.log(Level.INFO, "move file failed." + file, ex); } } } } /** * 存在しないDocBaseを参照するキャッシュ(*.ser)を削除する.
* (キャラクターディレクトリを変更した場合は、以前のデータは削除される.)
* @author seraphy */ class PurgeUnusedCache extends StartupSupportForDocBasedData { /** * ロガー */ private final Logger logger = Logger.getLogger(getClass().getName()); @Override public void doStartup() { // キャッシュの保存先を取得する. UserDataFactory userDataFactory = UserDataFactory.getInstance(); File cacheDir = userDataFactory.getSpecialDataDir("*.ser"); // 現在選択されているキャラクターデータディレクトリ上のUUIDの登録を保証する. // (character.xmlに対するUUIDの問い合わせ時に登録がなければ登録される.) // (現在選択されていないディレクトリについてはUUIDの登録が行われないので、もしデータベースファイルが // 作成されていない場合はキャッシュは一旦削除されることになる.) getDocBaseMapInCaches(DocBaseSignatureStoratage.NEW_FORMAT); // キャッシュ上にあるDocBaseのUUID表現で始まる*.serを列挙 String[] suffixes = { "-character.xml-cache.ser", // character.xmlのキャッシュ "-workingset.ser", // 作業状態のキャッシュ "-favorites.ser" // お気に入りのキャッシュ }; // UUIDからURIの索引 final Map mangledURIMap = userDataFactory.getMangledNameMap(cacheDir); // キャッシュファイルで使用されているUUIDの示す実体のURIが実在しない場合は、そのキャッシュは不要と見なす. for (String suffix : suffixes) { Map caches = getUUIDMangledNamedMap(cacheDir, suffix); for (Map.Entry cacheEntry : caches.entrySet()) { String mangledUUID = cacheEntry.getKey(); File cacheFile = cacheEntry.getValue(); try { URI uri = mangledURIMap.get(mangledUUID); boolean remove = true; if (uri != null) { File characterXmlFile = new File(uri); if (characterXmlFile.exists() || characterXmlFile.isFile()) { // UUIDデータベースに登録があり、且つ、 // character.xmlが実在する場合のみ削除しない. remove = false; } } if (remove) { // キャッシュファイルを削除する. boolean result = cacheFile.delete(); logger.log(Level.INFO, "purge unused cache: " + cacheFile + "/succeeded=" + result); } } catch (Exception ex) { logger.log(Level.WARNING, "remove file failed. " + cacheFile, ex); } } } } } /** * 古いログファイルを消去する. * @author seraphy */ class PurgeOldLogs extends StartupSupport { /** * ロガー */ private final Logger logger = Logger.getLogger(getClass().getName()); @Override public void doStartup() { UserDataFactory userDataFactory = UserDataFactory.getInstance(); File logsDir = userDataFactory.getSpecialDataDir("*.log"); if (logsDir.exists()) { AppConfig appConfig = AppConfig.getInstance(); long purgeOldLogsMillSec = appConfig.getPurgeLogDays() * 24L * 3600L * 1000L; if (purgeOldLogsMillSec > 0) { long purgeThresold = System.currentTimeMillis() - purgeOldLogsMillSec; for (File file : logsDir.listFiles()) { try { String name = file.getName(); if (file.isFile() && file.canWrite() && name.endsWith(".log")) { long lastModified = file.lastModified(); if (lastModified > 0 && lastModified < purgeThresold) { boolean result = file.delete(); logger.log(Level.INFO, "remove file " + file + "/succeeded=" + result); } } } catch (Exception ex) { logger.log(Level.WARNING, "remove file failed. " + file, ex); } } } } } } charactermanaj/src/charactermanaj/model/util/MakeEmbeddedResource.java0000644000175000017500000001122011767047535026415 0ustar paulliupaulliupackage charactermanaj.model.util; import java.io.File; import java.io.FileOutputStream; import java.io.InputStream; import java.io.ObjectOutputStream; import java.io.OutputStreamWriter; import java.io.Writer; import java.net.URL; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.HashMap; import java.util.Locale; import charactermanaj.model.CharacterData; import charactermanaj.model.io.CharacterDataPersistent; import charactermanaj.model.io.CharacterDataPersistent.DocInfo; /** * 埋め込みリソースを生成するためのビルド時のヘルパ.
* @author seraphy */ public class MakeEmbeddedResource { public static void main(String[] args) throws Exception { // 出力するデフォルトのファイル名 String[] fileNames = { "resources/schema/character.xml.ser", "resources/resource_list.txt" }; // 引数があれば、そちらを優先 for (int idx = 0; idx < Math.min(args.length, fileNames.length); idx++) { fileNames[idx] = args[idx]; } // デフォルトのキャラクター定義の事前シリアライズバージョン作成 createDefaultCharacterSer(fileNames[0]); // 展開対象リソースファイル名の出力 createResourceList(fileNames[1]); } /** * 展開対象リソースファイル名の出力 * @param fileName ファィル名 * @throws Exception 失敗 */ public static void createResourceList(String fileName) throws Exception { File outiFile = new File(fileName); ArrayList resourceList = new ArrayList(); resourceList.add("_HOW_TO_LOCALIZE.txt"); File resourceDir = new File("resources"); for (String dirName : new String[] {"appinfo", "languages", "menu"}) { File dir = new File(resourceDir, dirName); for (File file : dir.listFiles()) { if ( !file.isFile()) { continue; } String resourceName = dirName + "/" + file.getName(); resourceList.add(resourceName); } } String br = "\r\n"; FileOutputStream fos = new FileOutputStream(outiFile); try { Writer wr = new OutputStreamWriter(fos, Charset.forName("UTF-8")); try { wr.write("# localized resources" + br); for (String resource : resourceList) { wr.write(resource + br); } } finally { wr.close(); } } finally { fos.close(); } System.out.println("make embedded resource : succeeded. file=" + outiFile); } /** * デフォルトのキャラクター定義の事前シリアライズバージョン作成 * @param fileName ファィル名 * @throws Exception 失敗 */ public static void createDefaultCharacterSer(String fileName) throws Exception { File outiFile = new File(fileName); // 読み込む言語データ(ENは標準) Locale[] locales = new Locale[] { Locale.ENGLISH, // デフォルト Locale.JAPANESE, // 日本語固有、(現状、2つしか定義さてれいない) }; // 各言語ごとのデフォルトキャラクターデータを読み込む HashMap characterDataMap = new HashMap(); for (Locale locale : locales) { String lang = locale.getLanguage(); if (characterDataMap.containsKey(lang)) { continue; // すでに読み込み済みなのでスキップする. } CharacterData cd = loadDefaultCharacterData(locale); characterDataMap.put(lang, cd); } // シリアライズする. FileOutputStream fos = new FileOutputStream(outiFile); try { ObjectOutputStream oos = new ObjectOutputStream(fos); try { oos.writeObject(characterDataMap); } finally { oos.close(); } } finally { fos.close(); } System.out.println("make embedded resource : succeeded. file=" + outiFile); } protected static CharacterData loadDefaultCharacterData(Locale locale) throws Exception { // 埋め込みリソースからデフォルトキャラクターデータを構築する. CharacterData cd; URL defaultCharacter = getEmbeddedResourceURL(CharacterDataPersistent.DEFAULT_CHARACTER_XML); InputStream is = defaultCharacter.openStream(); try { DocInfo docInfo = new DocInfo(); docInfo.setFirstElementName("character"); docInfo.setNamespace(CharacterDataPersistent.NS); docInfo.setVersion("1.0"); CharacterDataPersistent persist = CharacterDataPersistent.getInstance(); cd = persist.loadCharacterDataFromXML(is, null, docInfo, locale); } finally { is.close(); } return cd; } protected static URL getEmbeddedResourceURL(String schemaName) { return CharacterDataPersistent.class.getResource(schemaName); } } charactermanaj/src/charactermanaj/model/ColorGroup.java0000644000175000017500000000334511767047535023545 0ustar paulliupaulliupackage charactermanaj.model; import java.io.Serializable; /** * カラーグループ.
* カラーグループはimmutableであり、構築された値は変更されることはない.
* @author seraphy */ public final class ColorGroup implements Serializable { private static final long serialVersionUID = -2127943872189828172L; private final String id; private final boolean enabled; private final String localizedName; public static final ColorGroup NA = new ColorGroup("n/a", "", false); public ColorGroup(final String id, final String localizedName) { this(id, localizedName, true); } private ColorGroup(final String id, final String localizedName, final boolean enabled) { if (id == null || id.trim().length() == 0) { throw new IllegalArgumentException(); } this.id = id.trim(); this.localizedName = (localizedName == null || localizedName.trim().length() == 0) ? id : localizedName; this.enabled = enabled; } public boolean isEnabled() { return enabled; } public String getId() { return id; } public String getLocalizedName() { return localizedName; } @Override public int hashCode() { return id.hashCode(); } @Override public boolean equals(Object obj) { if (obj == this) { return true; } if (obj != null && obj instanceof ColorGroup) { ColorGroup o = (ColorGroup) obj; return id.equals(o.getId()); } return false; } public static boolean equals(ColorGroup v1, ColorGroup v2) { if (v1 == v2) { return true; } if (v1 == null || v2 == null) { return false; } return v1.equals(v2); } @Override public String toString() { return getLocalizedName(); } } charactermanaj/src/charactermanaj/model/OrderedMap.java0000644000175000017500000001235111767047535023471 0ustar paulliupaulliupackage charactermanaj.model; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; import java.util.AbstractList; import java.util.AbstractMap; import java.util.AbstractSet; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; /** * 定義順でのリストアクセスと、キーによる一意アクセスの双方を格納にするマップ兼リスト.
* 元となるコレクションからキーを分離し、キーでアクセス可能にする.
* マップへのvalues()等のコレクションアクセスでは元のコレクションと同じ定義順で返される.
* より明確には{{@link #asList()}を使うとリストとしてアクセスすることができる.
* 読み込み専用で、書き込みはできない.
* * @author seraphy * * @param データに含まれるキーの型 * @param データの型 */ public class OrderedMap extends AbstractMap implements Serializable { /** * シリアライズバージョンID. */ private static final long serialVersionUID = 5049488493598426918L; /** * 空を示す定数インスタンス */ public static final OrderedMap EMPTY_MAP = new OrderedMap(); @SuppressWarnings("unchecked") public static final OrderedMap emptyMap() { return (OrderedMap) EMPTY_MAP; } public interface KeyDetector { K getKey(V data); } protected static class OrderedMapEntry implements Map.Entry, Serializable { private static final long serialVersionUID = 5111249402034089224L; private final K key; private final V data; protected OrderedMapEntry(K key, V data) { this.key = key; this.data = data; } public K getKey() { return key; } public V getValue() { return data; } public V setValue(V arg0) { throw new UnsupportedOperationException(); } } /** * リストとしてのエントリ */ private ArrayList> entries = new ArrayList>(); /** * マップアクセス用.
* シリアライズ時はスキップされ、デシリアライズ時にリストから復元される.
*/ private transient HashMap entryMap = new HashMap(); /** * デシリアライズ.
* @param stream * @throws IOException * @throws ClassNotFoundException */ private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException { stream.defaultReadObject(); entryMap = new HashMap(); for (Map.Entry entry : entries) { entryMap.put(entry.getKey(), entry.getValue()); } } /** * シリアライズ * @param stream * @throws IOException */ private void writeObject(ObjectOutputStream stream) throws IOException { stream.defaultWriteObject(); } /** * 空のコレクションを作成する. */ protected OrderedMap() { super(); } /** * 元となるコレクションをコピーし、そのコレクションの各データのキーを分離しマップアクセス可能にする.
* @param datas 元となるコレクション * @param keyDetector コレクションの各要素からキーを分離するためのインターフェイス */ public OrderedMap(Collection datas, KeyDetector keyDetector) { if (datas == null || keyDetector == null) { throw new IllegalArgumentException(); } for (V data : datas) { K key = keyDetector.getKey(data); if (key == null) { throw new IllegalArgumentException("null key: " + data); } entries.add(new OrderedMapEntry(key, data)); entryMap.put(key, data); } if (entries.size() != entryMap.size()) { throw new IllegalArgumentException("duplicate-key"); } } @Override public Set> entrySet() { return new AbstractSet>() { @Override public int size() { return entries.size(); } @Override public Iterator> iterator() { final Iterator> ite = entries.iterator();; return new Iterator>() { public boolean hasNext() { return ite.hasNext(); } public java.util.Map.Entry next() { return ite.next(); } public void remove() { throw new UnsupportedOperationException(); } }; } }; } /** * マップの値を定義順のリストとして扱えるようにリストインターフェイスで返す.
* 読み込み専用です.
* @return マップの値のリスト */ public List asList() { return new AbstractList() { @Override public int size() { return entries.size(); } @Override public V get(int index) { return entries.get(index).getValue(); } }; } @Override public V get(Object key) { return entryMap.get(key); } @Override public int size() { return entries.size(); } } charactermanaj/src/charactermanaj/graphics/0000755000175000017500000000000011767047532021277 5ustar paulliupaulliucharactermanaj/src/charactermanaj/graphics/ColorConvertedImageLoader.java0000644000175000017500000000164311767047532027170 0ustar paulliupaulliupackage charactermanaj.graphics; import java.io.IOException; import charactermanaj.graphics.filters.ColorConvertParameter; import charactermanaj.graphics.io.ImageResource; import charactermanaj.graphics.io.LoadedImage; /** * 画像リソースを読み込み、カラー変換した画像を返す. * @author seraphy * */ public interface ColorConvertedImageLoader { /** * 画像リソースをロードし色変換した結果のBufferedImageを返します.
* 返される形式はARGBに変換されています.
* @param file 画像リソース * @param colorConvParam 色変換パラメータ、nullの場合はデフォルト * @return 画像イメージ * @throws IOException 形式が不明であるか、ファィルがないか読み取りに失敗した場合 */ LoadedImage load(ImageResource file, ColorConvertParameter colorConvParam) throws IOException; } charactermanaj/src/charactermanaj/graphics/filters/0000755000175000017500000000000011767047532022747 5ustar paulliupaulliucharactermanaj/src/charactermanaj/graphics/filters/AbstractFilter.java0000644000175000017500000001202311767047532026521 0ustar paulliupaulliupackage charactermanaj.graphics.filters; import java.awt.Rectangle; import java.awt.RenderingHints; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.awt.image.BufferedImage; import java.awt.image.BufferedImageOp; import java.awt.image.ColorModel; import java.awt.image.DataBufferInt; import java.awt.image.WritableRaster; /** * 色フィルタの抽象クラス.
* @author seraphy */ public abstract class AbstractFilter implements BufferedImageOp { /** * 色変換を行うルーチン.
* 派生クラスでオーバーライドする.
* @param pixcels ARGB形式のピクセルデータ */ protected abstract void filter(int[] pixcels); public BufferedImage createCompatibleDestImage(BufferedImage src, ColorModel destCM) { if (destCM == null) { destCM = src.getColorModel(); } int w = src.getWidth(); int h = src.getHeight(); return new BufferedImage(destCM, destCM.createCompatibleWritableRaster(w, h), destCM.isAlphaPremultiplied(), null); } public BufferedImage filter(BufferedImage src, BufferedImage dest) { if (dest == null) { dest = createCompatibleDestImage(src, null); } int w = src.getWidth(); int h = src.getHeight(); int imageType = src.getType(); int [] pixcels; boolean shared = false; if (src == dest && (imageType == BufferedImage.TYPE_INT_ARGB || imageType == BufferedImage.TYPE_INT_RGB)) { // 元イメージと出力先イメージが同一であり、且つ、 // イメージがARGB/RGB形式であればピクセルデータはイメージが持つバッファそのものを共有アクセスする. // したがって、setPixcelsの呼び出しは不要. pixcels = null; shared = true; } else { // 元イメージと出力先イメージが異なるか、もしくは、 // イメージがARGB/RGB形式以外であれば、RGB形式のint配列に変換して処理する. // イメージに書き戻すためにsetPixcelsの呼び出しが必要となる. int len = w * h; pixcels = new int[len]; } pixcels = getPixcels(src, 0, 0, w, h, pixcels); filter(pixcels); if (!shared) { setPixcels(dest, 0, 0, w, h, pixcels); } return dest; } public Rectangle2D getBounds2D(BufferedImage src) { int w = src.getWidth(); int h = src.getHeight(); return new Rectangle(0, 0, w, h); } public Point2D getPoint2D(Point2D srcPt, Point2D dstPt) { return (Point2D) srcPt.clone(); } public RenderingHints getRenderingHints() { return null; } /** * ピクセルデータを取得する.
* 画像のタイプがARGBもしくはRGBの場合はpixcelsにnullを指定して格納先を指定しない場合は * 格納先を自動的に構築する.
* そうでない場合はピクセルデータはイメージのピクセルバッファを、そのまま返す.(つまり、変更は即イメージの変更になる.)
* ARGB,RGB以外のイメージは常に幅x高さ分のRGB(もしくはARGB)を格納できるだけのバッファを指定しなければならない.
* @param img 対象のイメージ * @param x 位置x * @param y 位置y * @param w 幅 * @param h 高さ * @param pixcels 格納先バッファ、もしくはnull * @return ピクセルデータ */ protected int[] getPixcels(BufferedImage img, int x, int y, int w, int h, int[] pixcels) { if (w <= 0 || h <= 0) { return new int[0]; } int len = w * h; if (pixcels != null && pixcels.length < len) { throw new IllegalArgumentException("array too short."); } int imageType = img.getType(); if (imageType == BufferedImage.TYPE_INT_ARGB || imageType == BufferedImage.TYPE_INT_RGB) { WritableRaster raster = img.getRaster(); if (pixcels == null) { DataBufferInt buf = (DataBufferInt) raster.getDataBuffer(); return buf.getData(); } return (int[]) raster.getDataElements(x, y, w, h, pixcels); } if (pixcels == null) { throw new IllegalArgumentException("image type error."); } return img.getRGB(x, y, w, h, pixcels, 0, w); } /** * ピクセルデータをイメージに書き戻す.
* ピクセルデータがnullであるか幅または高さが0であれば何もしない.
* @param img 対象のイメージ * @param x 位置x * @param y 位置y * @param w 幅 * @param h 高さ * @param pixcels ピクセルデータ、nullの場合は何もしない. */ protected void setPixcels(BufferedImage img, int x, int y, int w, int h, int[] pixcels) { int len = w * h; if (pixcels == null || w == 0 || h == 0) { return; } if (pixcels.length < len) { throw new IllegalArgumentException("array too short."); } int imageType = img.getType(); if (imageType == BufferedImage.TYPE_INT_ARGB || imageType == BufferedImage.TYPE_INT_RGB) { WritableRaster raster = img.getRaster(); raster.setDataElements(x, y, w, h, pixcels); return; } img.setRGB(x, y, w, h, pixcels, 0, w); } } charactermanaj/src/charactermanaj/graphics/filters/ColorConv.java0000644000175000017500000000416311767047532025522 0ustar paulliupaulliupackage charactermanaj.graphics.filters; import charactermanaj.graphics.filters.ColorConvertFilter.ColorReplace; /** * 色置換オブジェクトの列挙子.
* 「キャラクターなんとか機」のカラー変更にあわせて、画像は青・白(黒)の濃淡だけで表現し、 * RGBのマッピングを変えることで色変換する.(色調変換ではない。)
* G成分はないがしろにされている。 * @author seraphy */ public enum ColorConv implements ColorReplace { /** * 変換なし */ NONE { @Override public void convert(int[] rgb) { // do nothing. } }, /** * 青系.
* RGB成分をRRBにする.
*/ BLUE { @Override public void convert(int[] rgb) { rgb[1] = rgb[0]; } }, /** * 紫系.
* RGB成分をBRBにする.
*/ VIOLET { @Override public void convert(int[] rgb) { rgb[1] = rgb[0]; rgb[0] = rgb[2]; } }, /** * 赤系.
* RGB成分をBRRにする.
*/ RED { @Override public void convert(int[] rgb) { rgb[1] = rgb[0]; rgb[0] = rgb[2]; rgb[2] = rgb[1]; } }, /** * 黄系.
* RGB成分をBBRにする.
*/ YELLOW { @Override public void convert(int[] rgb) { rgb[1] = rgb[2]; rgb[2] = rgb[0]; rgb[0] = rgb[1]; } }, /** * 緑系.
* RGB成分をRBRにする.
*/ GREEN { @Override public void convert(int[] rgb) { rgb[1] = rgb[2]; rgb[2] = rgb[0]; } }, /** * シアン系にする.
* RGB成分をRBBにする.
*/ CYAN { @Override public void convert(int[] rgb) { rgb[1] = rgb[2]; } }, /** * 黒系にする.
* RGB成分をRRRにする.
*/ BLACK { @Override public void convert(int[] rgb) { rgb[1] = rgb[0]; rgb[2] = rgb[0]; } }, /** * 白系にする.
* RGB成分をBBBにする.
*/ WHITE { @Override public void convert(int[] rgb) { rgb[0] = rgb[2]; rgb[1] = rgb[2]; } }; /** * 色置換する. */ public abstract void convert(int[] rgb); } charactermanaj/src/charactermanaj/graphics/filters/ColorConvertFilter.java0000644000175000017500000001171311767047532027402 0ustar paulliupaulliupackage charactermanaj.graphics.filters; import java.awt.Color; /** * 色変換フィルタ.
* @author seraphy */ public class ColorConvertFilter extends AbstractFilter { /** * 色置換用インターフェイス.
* @author seraphy */ public interface ColorReplace { /** * R,G,Bの順に格納されている色データに対して編集を行う.
* @param rgb 編集対象 */ void convert(int[] rgb); } /** * 色置換オブジェクト */ private final ColorReplace colorReplace; /** * HSB値の各オフセット(3要素) */ private final float[] hsbOffsets; /** * 淡色化率.
* 0に近づくほどグレースケールに近づく.
* 0で完全なグレースケール、1の場合は色の変更はなし。 */ private final float grayLevel; /** * ガンマ値補正用テーブル.
*/ private final int[][] gammaTbl; /** * コントラスト補正用テーブル */ private final int[][] contrastTbl; /** * 色変換フィルタを構築する.
* @param colorReplace 色置換オブジェクト、不要ならばnull * @param hsbOffsets HSBオフセット(3要素)、不要ならばnull * @param grayLevel 淡色化率、1でそのまま、0でグレースケール化。 * @param gammaTableFactory ガンマ補正値ファクトリ、不要ならばnull * @param contrastTableFactory コントラスト補正ファクトリ、不要ならばnull */ public ColorConvertFilter( ColorReplace colorReplace, float[] hsbOffsets, float grayLevel, GammaTableFactory gammaTableFactory, ContrastTableFactory contrastTableFactory) { if (gammaTableFactory == null) { gammaTableFactory = new GammaTableFactory(1.f); } if (contrastTableFactory == null) { contrastTableFactory = new ContrastTableFactory(1.f); } if (hsbOffsets != null && hsbOffsets.length < 3) { throw new IllegalArgumentException("hsbOffset too short."); } if (hsbOffsets != null) { if (hsbOffsets[0] == 0 && hsbOffsets[1] == 0 && hsbOffsets[2] == 0) { hsbOffsets = null; } else { hsbOffsets = (float[]) hsbOffsets.clone(); } } if (grayLevel < 0) { grayLevel = 0; } else if (grayLevel > 1) { grayLevel = 1.f; } this.grayLevel = grayLevel; this.gammaTbl = gammaTableFactory.createTable(); this.contrastTbl = contrastTableFactory.createTable(); this.hsbOffsets = hsbOffsets; this.colorReplace = colorReplace; } /** * ピクセルデータに対して色変換を行う.
* @param pixcels ピクセルデータ */ protected void filter(int[] pixcels) { final float grayLevel = this.grayLevel; final float negGrayLevel = 1.f - grayLevel; // グレースケール変換テーブル int[] precalc = new int[256]; int[] negPrecalc = new int[256]; for (int i = 0; i < 256; i++) { precalc[i] = (int)(i * grayLevel) & 0xff; negPrecalc[i] = (int)(i * negGrayLevel) & 0xff; } // 全ピクセルに対して計算を行う final ColorReplace colorReplace = this.colorReplace; int[] rgbvals = new int[3]; final float[] hsbOffsets = this.hsbOffsets; final float[] hsbvals = new float[3]; final int[][] gammaTbl = this.gammaTbl; final int mx = pixcels.length; for (int i = 0; i < mx; i++) { int argb = pixcels[i]; // ガンマ変換 int a = gammaTbl[0][(argb >> 24) & 0xff]; int r = gammaTbl[1][(argb >> 16) & 0xff]; int g = gammaTbl[2][(argb >> 8) & 0xff]; int b = gammaTbl[3][(argb) & 0xff]; // 色交換 if (colorReplace != null) { rgbvals[0] = r; rgbvals[1] = g; rgbvals[2] = b; colorReplace.convert(rgbvals); r = rgbvals[0]; g = rgbvals[1]; b = rgbvals[2]; } // 輝度 int br = ((77 * r + 150 * g + 29 * b) >> 8) & 0xff; // 輝度(グレースケール)に近づける r = ((int)(precalc[r] + negPrecalc[br])) & 0xff; g = ((int)(precalc[g] + negPrecalc[br])) & 0xff; b = ((int)(precalc[b] + negPrecalc[br])) & 0xff; // // コントラスト変換 // r = contrastTbl[0][r]; // g = contrastTbl[0][g]; // b = contrastTbl[0][b]; // 色調変換 if (hsbOffsets != null) { Color.RGBtoHSB(r, g, b, hsbvals); for (int l = 0; l < 3; l++) { hsbvals[l] += hsbOffsets[l]; } for (int l = 1; l < 3; l++) { if (hsbvals[l] < 0) { hsbvals[l] = 0; } else if (hsbvals[l] > 1.f) { hsbvals[l] = 1.f; } } int rgb = Color.HSBtoRGB(hsbvals[0], hsbvals[1], hsbvals[2]); argb = (a << 24) | (rgb & 0xffffff); } else { argb = (a << 24) | (r << 16) | (g << 8) | b; } // コントラスト変換 a = (argb >> 24) & 0xff; r = contrastTbl[0][(argb >> 16) & 0xff]; g = contrastTbl[1][(argb >> 8) & 0xff]; b = contrastTbl[2][(argb) & 0xff]; argb = (a << 24) | (r << 16) | (g << 8) | b; pixcels[i] = argb; } } } charactermanaj/src/charactermanaj/graphics/filters/TableFactory.java0000644000175000017500000000015611767047532026173 0ustar paulliupaulliupackage charactermanaj.graphics.filters; public interface TableFactory { int[][] createTable(); } charactermanaj/src/charactermanaj/graphics/filters/GammaTableFactory.java0000644000175000017500000000405311767047532027136 0ustar paulliupaulliupackage charactermanaj.graphics.filters; /** * ガンマ補正値を構築するためのファクトリ. * @author seraphy */ public class GammaTableFactory implements TableFactory { /** * ARGBの、それぞれのガンマ補正値の配列 */ private float[] gammas; /** * ARGBすべてが同一のガンマ値で構築する. * @param gamma ガンマ値 */ public GammaTableFactory(float gamma) { setGamma(gamma); } /** * ARGBそれぞれ異なるガンマ値で構築する.
* @param gammas ガンマ値 */ public GammaTableFactory(float[] gammas) { setGamma(gammas); } /** * ARGBすべてが同一のガンマ値を設定する. * @param gamma ガンマ値 */ public final void setGamma(float gamma) { setGamma(new float[] {gamma, gamma, gamma, gamma}); } /** * ARGBそれぞれ異なるガンマ値を設定する.
* @param gammas ガンマ値 */ public final void setGamma(float[] gammas) { if (gammas == null || gammas.length < 3) { throw new IllegalArgumentException(); } this.gammas = gammas; } /** * ARGB/RGBの、それぞれのガンマ値に対する0-255の範囲に対する補正後の値を格納する * 二次元配列を構築する.
* @return ガンマテーブル */ public int[][] createTable() { int mx = gammas.length; int[][] gammaTbls = new int[mx][]; for (int i = 0; i < 4; i++) { float gamma; if (i < mx) { gamma = gammas[i]; } else { gamma = 1.f; } gammaTbls[i] = createGamma(gamma); } return gammaTbls; } /** * ガンマ値に対する0-255の入力に対する、そのガンマ補正値の配列を返す.
* @param gamma ガンマ値 * @return ガンマテーブル */ private int[] createGamma(float gamma) { if (gamma < 0.01f) { gamma = 0.01f; } int gammaTbl[] = new int[256]; for (int gi = 0; gi <= 0xff; gi++) { gammaTbl[gi] = (int)(Math.pow(gi / 255.0, 1 / gamma) * 255) & 0xff; } return gammaTbl; } } charactermanaj/src/charactermanaj/graphics/filters/BackgroundColorFilter.java0000644000175000017500000001354211767047532030043 0ustar paulliupaulliupackage charactermanaj.graphics.filters; import java.awt.Color; public class BackgroundColorFilter extends AbstractFilter { /** * 背景モード. * @author seraphy */ public enum BackgroundColorMode { ALPHABREND(false, false) { @Override public void filter(BackgroundColorFilter me, int[] pixcels) { me.alphabrend(pixcels); } }, OPAQUE(false, true) { @Override public void filter(BackgroundColorFilter me, int[] pixcels) { me.opaque(pixcels); } }, GRAYSCALE(true, false) { @Override public void filter(BackgroundColorFilter me, int[] pixcels) { me.grayscale(pixcels); } }, DRAW_ALPHA(true, true) { @Override public void filter(BackgroundColorFilter me, int[] pixcels) { me.drawAlpha(pixcels); } }; private final boolean grayscale; private final boolean noAlphachanel; public abstract void filter(BackgroundColorFilter me, int[] pixcels); BackgroundColorMode(boolean grayscale, boolean noAlphachanel) { this.grayscale = grayscale; this.noAlphachanel = noAlphachanel; } public boolean isNoAlphaChannel() { return this.noAlphachanel; } public boolean isGrayscale() { return this.grayscale; } public static BackgroundColorMode valueOf(boolean noAlphachanel, boolean grayscale) { for (BackgroundColorMode mode : values()) { if (mode.isNoAlphaChannel() == noAlphachanel && mode.isGrayscale() == grayscale) { return mode; } } throw new RuntimeException("構成に誤りがあります."); } } /** * 背景モード */ private BackgroundColorMode mode; /** * 背景色、グレースケールまたはアルファ表示モードでは不要 */ private Color bgColor; /** * 背景モードと背景色を指定して背景色描画フィルタを構築する. * @param mode モード * @param bgColor 背景色、(グレースケールまたはアルファではnull化) */ public BackgroundColorFilter(BackgroundColorMode mode, Color bgColor) { if (mode == null) { // モードは必須. throw new IllegalArgumentException(); } if (!mode.isGrayscale() && bgColor == null) { // グレースケールもしくはアルファ表示モード以外は背景色は必須. throw new IllegalArgumentException(); } this.mode = mode; this.bgColor = bgColor; } @Override protected void filter(int[] pixcels) { mode.filter(this, pixcels); } /** * 普通のアルファブレンドします. * @param pixcels */ public void alphabrend(int[] pixcels) { int br = bgColor.getRed(); int bg = bgColor.getGreen(); int bb = bgColor.getBlue(); final int mx = pixcels.length; for (int idx = 0; idx < mx; idx++) { int argb = pixcels[idx]; int b = argb & 0xff; int g = (argb >>>= 8) & 0xff; int r = (argb >>>= 8) & 0xff; int a = (argb >>>= 8) & 0xff; if (a == 0) { // 完全透過ならば背景色まま b = bb; g = bg; r = br; } else if (a != 0xff) { // 完全非透過でなければアルファブレンド b = ((b * a) / 0xff + (bb * (0xff - a) / 0xff)) & 0xff; g = ((g * a) / 0xff + (bg * (0xff - a) / 0xff)) & 0xff; r = ((r * a) / 0xff + (br * (0xff - a) / 0xff)) & 0xff; } argb = 0xff000000 | (r << 16) | (g << 8) | b; pixcels[idx] = argb; } } /** * 完全透過の部分のみ背景色を設定し、それ以外は元の色のままアルファを取り除く.
* @param pixcels ピクセルデータ */ public void opaque(int[] pixcels) { int bgRgb = bgColor.getRGB(); final int mx = pixcels.length; for (int idx = 0; idx < mx; idx++) { int argb = pixcels[idx]; int a = (argb >>> 24) & 0xff; int rgb = (argb & 0xffffff); if (a == 0) { rgb = bgRgb; } argb = 0xff000000 | rgb; pixcels[idx] = argb; } } /** * アルファを取り除き、グレスケールで表現する.
* RGBチャネルのうち、RBはアルファ適用されたグレースケールで、 * Gチャネルはアルファ未適用のグレースケールで表現される.
* @param pixcels ピクセルデータ */ public void grayscale(int[] pixcels) { final int mx = pixcels.length; for (int idx = 0; idx < mx; idx++) { int argb = pixcels[idx]; int b = argb & 0xff; int g = (argb >>>= 8) & 0xff; int r = (argb >>>= 8) & 0xff; int a = (argb >>>= 8) & 0xff; int gray_brend = 0; int gray_plain = 0; if (a != 0) { // 平均化 gray_brend = ((r + g + b) / 3) & 0xff; gray_plain = gray_brend; if (a != 0xff) { // アルファによる調整 gray_brend = ((gray_brend * a) / 0xff) & 0xff; } } argb = 0xff000000 | (gray_brend << 16) | (gray_plain << 8) | gray_brend; pixcels[idx] = argb; } } /** * アルファチャネルとグレースケールを重ねて表示する.
* アルファ未適用のグレースケールをRチャネル、 * アルファを無し・半透明・透明の3段階にしたものをGチャネル、 * アルファをBチャネルで表現する.
* Gチャネルは、完全透過は濃緑(0x80)、完全非透過は黒(0xff)、半透明は明緑(0xff)となる.
* @param pixcels ピクセルデータ */ public void drawAlpha(int[] pixcels) { final int mx = pixcels.length; for (int idx = 0; idx < mx; idx++) { int argb = pixcels[idx]; int b = argb & 0xff; int g = (argb >>>= 8) & 0xff; int r = (argb >>>= 8) & 0xff; int a = (argb >>>= 8) & 0xff; int gray_plain = (r + g + b) / 3; int alpha_off = (a == 0) ? 0x80 : (a == 0xff) ? 0x00 : 0xff; argb = 0xff000000 | (gray_plain << 16) | (alpha_off << 8) | a; pixcels[idx] = argb; } } } charactermanaj/src/charactermanaj/graphics/filters/ContrastTableFactory.java0000644000175000017500000000143511767047532027712 0ustar paulliupaulliupackage charactermanaj.graphics.filters; public class ContrastTableFactory implements TableFactory { private float contrast = 1.f; public ContrastTableFactory() { this(1.f); } public ContrastTableFactory(float contrast) { this.contrast = contrast; } public int[][] createTable() { int[] table = new int[256]; for (int level = 0; level <= 255; level++) { float f = level / 255.f; f = getContrast(f); int c = (int)(f * 256); if (c > 255) { c = 255; } else if (c < 0) { c = 0; } table[level] = c; } int[][] tables = new int[3][]; for (int idx = 0; idx < 3; idx++) { tables[idx] = table; } return tables; } protected float getContrast(float f) { return (f - 0.5f) * contrast + 0.5f; } } charactermanaj/src/charactermanaj/graphics/filters/ColorConvertParameter.java0000644000175000017500000001613211767047532030075 0ustar paulliupaulliupackage charactermanaj.graphics.filters; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectInputStream.GetField; import java.io.Serializable; /** * カラー情報.
* RGB置換、GrayLevel、RGBオフセット各種、HSB調整値を持つ.
* @author seraphy */ public final class ColorConvertParameter implements Serializable, Cloneable { private static final long serialVersionUID = 3092895708547846162L; /** * 色置換パターン */ private ColorConv rgbChanelMixierPattern; /** * グレーレベル(0でモノトーン、1でノーマル、0.5で半分ほどモノトーン化) */ private float grayLevel = 1.f; private float hue = 0.f; private float saturation = 0.f; private float brightness = 0.f; private float contrast = 0.f; private int offsetR = 0; private int offsetG = 0; private int offsetB = 0; private int offsetA = 0; private float factorR = 1.f; private float factorG = 1.f; private float factorB = 1.f; private float factorA = 1.f; private float gammaR = 1.f; private float gammaG = 1.f; private float gammaB = 1.f; private float gammaA = 1.f; private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException { GetField fields = stream.readFields(); this.rgbChanelMixierPattern = (ColorConv) fields.get("rgbChanelMixierPattern", ColorConv.NONE); this.grayLevel = fields.get("grayLevel", 1.f); this.hue = fields.get("hue", 0.f); this.saturation = fields.get("saturation", 0.f); this.brightness = fields.get("brightness", 0.f); this.contrast = fields.get("contrast", 0.f); this.offsetR = fields.get("offsetR", 0); this.offsetG = fields.get("offsetG", 0); this.offsetB = fields.get("offsetB", 0); this.offsetA = fields.get("offsetA", 0); this.factorR = fields.get("factorR", 1.f); this.factorG = fields.get("factorG", 1.f); this.factorB = fields.get("factorB", 1.f); this.factorA = fields.get("factorA", 1.f); this.gammaR = fields.get("gammaR", 1.f); this.gammaG = fields.get("gammaG", 1.f); this.gammaB = fields.get("gammaB", 1.f); this.gammaA = fields.get("gammaA", 1.f); } @Override public int hashCode() { int ret = 0; if (rgbChanelMixierPattern != null) { ret = rgbChanelMixierPattern.ordinal(); } ret ^= (int)(grayLevel * 100); ret ^= (int)(hue * 100); ret ^= (int)(saturation * 100); ret ^= (int)(brightness * 100); ret ^= (int)(contrast * 100); ret ^= offsetR; ret ^= offsetG; ret ^= offsetB; ret ^= offsetA; ret ^= (int)(factorR * 100); ret ^= (int)(factorG * 100); ret ^= (int)(factorB * 100); ret ^= (int)(factorA * 100); ret ^= (int)(gammaR * 100); ret ^= (int)(gammaG * 100); ret ^= (int)(gammaB * 100); ret ^= (int)(gammaA * 100); return ret; } @Override public boolean equals(Object obj) { if (obj == this) { return true; } if (obj != null && obj instanceof ColorConvertParameter) { ColorConvertParameter o = (ColorConvertParameter) obj; return rgbChanelMixierPattern == o.rgbChanelMixierPattern && grayLevel == o.grayLevel && hue == o.hue && saturation == o.saturation && contrast == o.contrast && brightness == o.brightness && offsetR == o.offsetR && offsetG == o.offsetG && offsetB == o.offsetB && offsetA == o.offsetA && factorR == o.factorR && factorG == o.factorG && factorB == o.factorB && factorA == o.factorA && gammaR == o.gammaR && gammaG == o.gammaG && gammaB == o.gammaB && gammaA == o.gammaA; } return false; } public static boolean equals(ColorConvertParameter a, ColorConvertParameter b) { if (a == b) { return true; } if (a != null && b != null) { return a.equals(b); } return false; } @Override public ColorConvertParameter clone() { try { // シャローコピー. すべてimmutableな単純型だけのメンバなので問題なし. return (ColorConvertParameter) super.clone(); } catch (CloneNotSupportedException e) { throw new Error("internal error."); } } public ColorConv getColorReplace() { return rgbChanelMixierPattern; } public void setColorReplace(ColorConv colorReplace) { this.rgbChanelMixierPattern = colorReplace; } public float getGrayLevel() { return grayLevel; } public void setGrayLevel(float grayLevel) { this.grayLevel = grayLevel; } public float getHue() { return hue; } public void setHue(float hue) { this.hue = hue; } public float getSaturation() { return saturation; } public void setSaturation(float saturation) { this.saturation = saturation; } public float getBrightness() { return brightness; } public void setBrightness(float brightness) { this.brightness = brightness; } public float getContrast() { return contrast; } public void setContrast(float contrast) { this.contrast = contrast; } public int getOffsetR() { return offsetR; } public void setOffsetR(int offsetR) { this.offsetR = offsetR; } public int getOffsetG() { return offsetG; } public void setOffsetG(int offsetG) { this.offsetG = offsetG; } public int getOffsetB() { return offsetB; } public void setOffsetB(int offsetB) { this.offsetB = offsetB; } public int getOffsetA() { return offsetA; } public void setOffsetA(int offsetA) { this.offsetA = offsetA; } public float getFactorR() { return factorR; } public void setFactorR(float factorR) { this.factorR = factorR; } public float getFactorG() { return factorG; } public void setFactorG(float factorG) { this.factorG = factorG; } public float getFactorB() { return factorB; } public void setFactorB(float factorB) { this.factorB = factorB; } public float getFactorA() { return factorA; } public void setFactorA(float factorA) { this.factorA = factorA; } public float getGammaR() { return gammaR; } public void setGammaR(float gammaR) { this.gammaR = gammaR; } public float getGammaG() { return gammaG; } public void setGammaG(float gammaG) { this.gammaG = gammaG; } public float getGammaB() { return gammaB; } public void setGammaB(float gammaB) { this.gammaB = gammaB; } public float getGammaA() { return gammaA; } public void setGammaA(float gammaA) { this.gammaA = gammaA; } @Override public String toString() { StringBuilder buf = new StringBuilder(); buf.append(getClass().getSimpleName() + "@" + Integer.toHexString(System.identityHashCode(this)) + "("); buf.append("(replace:" + rgbChanelMixierPattern + ", grayLevel:" + grayLevel + ")"); buf.append("(H:" + hue + ", S:" + saturation + ", B:" + brightness + ", C:" + contrast +")"); buf.append("(Red:" + offsetR + "/" + factorR + "/" + gammaR + ", Green:" + offsetG + "/" + factorG + "/" + gammaG + ", Blue:" + offsetB + "/" + factorB + "/" + gammaB + ", Alpha:" + offsetA + "/" + factorA + "/" + gammaA + ")"); buf.append(")"); return buf.toString(); } } charactermanaj/src/charactermanaj/graphics/AsyncImageBuilder.java0000644000175000017500000001125511767047532025475 0ustar paulliupaulliupackage charactermanaj.graphics; import java.util.logging.Level; import java.util.logging.Logger; /** * 各パーツ情報をもとに非同期にイメージを合成する * @author seraphy */ public class AsyncImageBuilder extends ImageBuilder implements Runnable { /** * ロガー */ private static final Logger logger = Logger.getLogger(AsyncImageBuilder.class.getName()); /** * 非同期にイメージを構築するためのジョブ定義.
* リクエストを受け付けたことを示すイベントおよび、リクエストが放棄されたことを示すイベントを受け取ることができる.
* @author seraphy * */ public interface AsyncImageBuildJob extends ImageBuildJob { /** * リクエストを受け付けた場合に呼び出される.
* @param ticket このイメージビルダでリクエストを受け付けた通し番号 */ void onQueueing(long ticket); /** * リクエストを処理するまえに破棄された場合に呼び出される.
*/ void onAbandoned(); } /** * 同期オブジェクト */ private final Object lock = new Object(); /** * チケットのシリアルナンバー.
* リクエストがあるごとにインクリメントされる.
*/ private long ticketSerialNum = 0; /** * リクエストされているジョブ、なければnull */ private ImageBuildJob requestJob; /** * 停止フラグ(volatile) */ private volatile boolean stopFlag; /** * スレッド */ private Thread thread; /** * イメージローダを指定して構築する. * @param imageLoader イメージローダー */ public AsyncImageBuilder(ColorConvertedImageCachedLoader imageLoader) { super(imageLoader); thread = new Thread(this); thread.setDaemon(true); } /** * スレッドの実行部. */ public void run() { logger.log(Level.FINE, "AsyncImageBuilder thread started."); // 停止フラグがたてられるまで繰り返す. while (!stopFlag) { try { ImageBuildJob job; synchronized (lock) { while (!stopFlag && requestJob == null) { // ジョブリクエストがくるまで待機 lock.wait(1000); } if (stopFlag) { break; } // ジョブを一旦ローカル変数に保存 job = requestJob; // ジョブの受け付けを再開. requestJob = null; lock.notifyAll(); } // リクエストを処理する. AsyncImageBuilder.super.requestJob(job); } catch (InterruptedException ex) { logger.log(Level.FINE, "AsyncImageBuilder thead interrupted."); // 割り込みされた場合、単にループを再開する. } catch (Exception ex) { logger.log(Level.SEVERE, "AsyncImageBuilder failed.", ex); // ジョブ合成中の予期せぬ例外はログに記録するのみで // スレッドそのものは停止させない. // (Error系は、たぶんアプリ自身が続行不能な障害なので停止する.) } } logger.log(Level.FINE, "AsyncImageBuilder thread stopped."); } /** * イメージ作成ジョブをリクエストする.
* イメージ作成ジョブは非同期に実行される.
* 処理がはじまる前に新しいリクエストで上書きされた場合、前のリクエストは単に捨てられる.
*/ @Override public boolean requestJob(ImageBuildJob imageSource) { synchronized (lock) { // 現在処理待ちのリクエストがあれば、新しいリクエストで上書きする. if (this.requestJob != null && this.requestJob instanceof AsyncImageBuildJob) { ((AsyncImageBuildJob) this.requestJob).onAbandoned(); } // リクエストをセットして待機中のスレッドに通知を出す. this.requestJob = imageSource; if (imageSource != null && imageSource instanceof AsyncImageBuildJob) { ((AsyncImageBuildJob) imageSource).onQueueing(++ticketSerialNum); } lock.notifyAll(); } return false; } /** * スレッドが生きているか? * @return 生きていればtrue */ public boolean isAlive() { return thread.isAlive(); } /** * スレッドを開始する. */ public void start() { if (!thread.isAlive()) { stopFlag = false; thread.start(); } } /** * スレッドを停止する. */ public void stop() { if (thread.isAlive()) { stopFlag = true; thread.interrupt(); try { // スレッドの停止を待機する. thread.join(); } catch (InterruptedException ex) { // do nothing. } } } } charactermanaj/src/charactermanaj/graphics/ImageBuildJobAbstractAdaptor.java0000644000175000017500000000477711767047532027615 0ustar paulliupaulliupackage charactermanaj.graphics; import java.io.IOException; import charactermanaj.graphics.ImageBuilder.ImageOutput; import charactermanaj.graphics.ImageBuilder.ImageSourceCollector; import charactermanaj.graphics.filters.ColorConvertParameter; import charactermanaj.graphics.io.ImageResource; import charactermanaj.model.Layer; import charactermanaj.model.PartsIdentifier; import charactermanaj.model.PartsSet; import charactermanaj.model.PartsSpecResolver; import charactermanaj.model.io.PartsImageCollectionParser; /** * 非同期に複合画像を生成するイメージビルダに引き渡すジョブを生成するためのアダプタクラス.
* パーツセットとパーツ設定リゾルバからイメージビルダに引き渡す情報を解決するジョブを生成する.
* @author seraphy * */ public abstract class ImageBuildJobAbstractAdaptor implements AsyncImageBuilder.AsyncImageBuildJob { protected PartsImageCollectionParser partsImageCollectorParser; public ImageBuildJobAbstractAdaptor(PartsSpecResolver partsSpecResolver) { if (partsSpecResolver == null) { throw new IllegalArgumentException(); } this.partsImageCollectorParser = new PartsImageCollectionParser(partsSpecResolver); } public void loadParts(final ImageSourceCollector collector) throws IOException { if (collector == null) { throw new IllegalArgumentException("collector is null"); } PartsSet partsSet = getPartsSet(); if (partsSet == null) { throw new RuntimeException("PartsSet is null"); } collector.setSize(partsImageCollectorParser.getPartsSpecResolver().getImageSize()); collector.setImageBgColor(partsSet.getBgColor()); collector.setAffineTramsform(partsSet.getAffineTransformParameter()); partsImageCollectorParser.parse(partsSet, new PartsImageCollectionParser.PartsImageCollectionHandler() { public void detectImageSource(PartsIdentifier partsIdentifier, Layer layer, ImageResource imageResource, ColorConvertParameter param) { if (param == null) { param = new ColorConvertParameter(); } collector.setImageSource(layer, imageResource, param); } }); collector.setComplite(); } protected abstract PartsSet getPartsSet() throws IOException; public abstract void buildImage(ImageOutput output); public abstract void handleException(Throwable ex); public void onAbandoned() { // なにもしない } public void onQueueing(long ticket) { // なにもしない } } charactermanaj/src/charactermanaj/graphics/ColorConvertedImageCachedLoader.java0000644000175000017500000000565411767047532030266 0ustar paulliupaulliupackage charactermanaj.graphics; import java.io.IOException; import charactermanaj.graphics.filters.ColorConvertParameter; import charactermanaj.graphics.io.ImageCache; import charactermanaj.graphics.io.ImageCachedLoader; import charactermanaj.graphics.io.ImageLoader; import charactermanaj.graphics.io.ImageResource; import charactermanaj.graphics.io.LoadedImage; /** * 画像リソースに対する色変換後の画像イメージを返します.
* 一度読み込まれ色変換された画像は、画像ファイルの更新日が同一であり、且つ、色パラメータに変更がなければ * 読み込み済みの画像イメージを返します.
* @author seraphy * */ public class ColorConvertedImageCachedLoader extends ColorConvertedImageLoaderImpl { private ImageCache caches = new ImageCache(); public ColorConvertedImageCachedLoader() { this(new ImageCachedLoader()); } public ColorConvertedImageCachedLoader(ImageLoader imageLoader) { super(imageLoader); } @Override public LoadedImage load(ImageResource file, ColorConvertParameter colorConvParam) throws IOException { if (file == null) { throw new IllegalArgumentException(); } ColorConvertParameter param; if (colorConvParam == null) { param = new ColorConvertParameter(); } else { param = colorConvParam.clone(); } ColorConvertedImageKey key = new ColorConvertedImageKey(param, file); synchronized (caches) { LoadedImage loadedImage = caches.get(key); if (loadedImage == null) { loadedImage = super.load(file, param); caches.set(key, loadedImage); } return loadedImage; } } public void unlockImages() { caches.unlockImages(); } } final class ColorConvertedImageKey { private final ColorConvertParameter colorConvParameter; private final ImageResource imageResource; private final long lastModified; private final int hashCode; public ColorConvertedImageKey(ColorConvertParameter colorConvParameter, ImageResource imageResource) { if (colorConvParameter == null || imageResource == null) { throw new IllegalArgumentException(); } this.colorConvParameter = colorConvParameter; this.imageResource = imageResource; this.lastModified = imageResource.lastModified(); this.hashCode = imageResource.hashCode() ^ colorConvParameter.hashCode() ^ (int) this.lastModified; } @Override public int hashCode() { return this.hashCode; } @Override public boolean equals(Object obj) { if (obj == this) { return true; } if (obj != null && obj instanceof ColorConvertedImageKey) { ColorConvertedImageKey other = (ColorConvertedImageKey) obj; return lastModified == other.lastModified && imageResource.equals(other.imageResource) && colorConvParameter.equals(other.colorConvParameter); } return false; } } charactermanaj/src/charactermanaj/graphics/ColorConvertedImageLoaderImpl.java0000644000175000017500000000622111767047532030007 0ustar paulliupaulliupackage charactermanaj.graphics; import java.awt.image.BufferedImage; import java.awt.image.RescaleOp; import java.io.IOException; import charactermanaj.graphics.filters.ColorConvertFilter; import charactermanaj.graphics.filters.ColorConvertParameter; import charactermanaj.graphics.filters.ContrastTableFactory; import charactermanaj.graphics.filters.GammaTableFactory; import charactermanaj.graphics.io.ImageLoader; import charactermanaj.graphics.io.ImageResource; import charactermanaj.graphics.io.LoadedImage; /** * 画像リソースをロードし色変換された結果の画像イメージとして返す.
* @author seraphy */ public class ColorConvertedImageLoaderImpl implements ColorConvertedImageLoader { private static final ColorConvertParameter NULL_COLORCONVPARAM = new ColorConvertParameter(); private ImageLoader loader; public ColorConvertedImageLoaderImpl(ImageLoader loader) { if (loader == null) { throw new IllegalArgumentException(); } this.loader = loader; } /** * 画像リソースをロードし色変換した結果のBufferedImageを返します.
* 返される形式はARGBに変換されています.
* @param file 画像リソース * @param colorConvParam 色変換パラメータ、nullの場合はデフォルト * @return 画像イメージ * @throws IOException 形式が不明であるか、ファィルがないか読み取りに失敗した場合 */ public LoadedImage load(ImageResource file, ColorConvertParameter colorConvParam) throws IOException { if (file == null) { throw new IllegalArgumentException(); } if (colorConvParam == null) { colorConvParam = NULL_COLORCONVPARAM; } LoadedImage loadedImage = loader.load(file); BufferedImage originalImage = loadedImage.getImage(); BufferedImage image = colorConvert(originalImage, colorConvParam); return new LoadedImage(image, loadedImage.getLastModified()); } /** * 色変換ロジック. * @param img 元画像(ARGB形式) * @param param 変換パラメータ * @return 色変換後の画像 */ protected BufferedImage colorConvert(BufferedImage img, ColorConvertParameter param) { float[] factors = { param.getFactorR(), param.getFactorG(), param.getFactorB(), param.getFactorA(), }; float[] offsets = { param.getOffsetR(), param.getOffsetG(), param.getOffsetB(), param.getOffsetA(), }; RescaleOp rescale_op = new RescaleOp(factors, offsets, null); float[] gammas = { param.getGammaA(), param.getGammaR(), param.getGammaG(), param.getGammaB(), }; float[] hsbs = { param.getHue(), param.getSaturation(), param.getBrightness() }; float contrast = param.getContrast(); ColorConvertFilter colorConvert_op = new ColorConvertFilter( param.getColorReplace(), hsbs, param.getGrayLevel(), new GammaTableFactory(gammas), new ContrastTableFactory((float) Math.exp(contrast * 2.f)) // 対数補正 ); img = colorConvert_op.filter(img, null); img = rescale_op.filter(img, img); return img; } } charactermanaj/src/charactermanaj/graphics/io/0000755000175000017500000000000011767047532021706 5ustar paulliupaulliucharactermanaj/src/charactermanaj/graphics/io/EmbeddedImageResource.java0000644000175000017500000000445111767047532026721 0ustar paulliupaulliupackage charactermanaj.graphics.io; import java.io.IOException; import java.io.InputStream; import java.io.Serializable; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.util.logging.Level; import java.util.logging.Logger; import charactermanaj.util.ResourceLoader; /** * クラスローダからリソースを読み込むイメージリソース.
* @author seraphy */ public class EmbeddedImageResource extends ResourceLoader implements ImageResource, Serializable { /** * シリアライズバージョン */ private static final long serialVersionUID = 703707046457343373L; /** * ロガー */ private static final Logger logger = Logger.getLogger(EmbeddedImageResource.class.getName()); /** * ファイル */ private String resourceName; public EmbeddedImageResource(String resourceName) { if (resourceName == null) { throw new IllegalArgumentException(); } this.resourceName = resourceName; } public int compareTo(ImageResource o) { return getFullName().compareTo(o.getFullName()); } @Override public int hashCode() { return getFullName().hashCode(); } @Override public boolean equals(Object obj) { if (obj == this) { return true; } if (obj instanceof ImageResource) { ImageResource o = (ImageResource) obj; return getFullName().equals(o.getFullName()); } return false; } public String getFullName() { return resourceName; } public URI getURI() { URL url = getResource(resourceName); if (url != null) { try { return url.toURI(); } catch(URISyntaxException ex) { logger.log(Level.WARNING, "resource name is invalid. " + resourceName, ex); // 何もしない. } } return null; } /** * リソースが実在すれば日付は常に1を返す.
* リソースが存在しなければ0を返す.
*/ public long lastModified() { URL url = getResource(resourceName); if (url == null) { return 1; } return 0; } public InputStream openStream() throws IOException { URL url = getResource(resourceName); if (url == null) { return null; } return url.openStream(); } @Override public String toString() { return getFullName(); } } charactermanaj/src/charactermanaj/graphics/io/LoadedImage.java0000644000175000017500000000101211767047532024676 0ustar paulliupaulliupackage charactermanaj.graphics.io; import java.awt.image.BufferedImage; /** * ロードされたイメージ情報 * @author seraphy */ public final class LoadedImage { private final BufferedImage image; private final long lastModified; public LoadedImage(BufferedImage image, long lastModified) { this.image = image; this.lastModified = lastModified; } public BufferedImage getImage() { return image; } public long getLastModified() { return lastModified; } } charactermanaj/src/charactermanaj/graphics/io/OutputOption.java0000644000175000017500000000703411767047532025246 0ustar paulliupaulliupackage charactermanaj.graphics.io; import java.io.Serializable; import charactermanaj.util.LocalizedMessageAware; /** * 出力オプションモデル. * @author seraphy */ public class OutputOption implements Serializable, Cloneable { private static final long serialVersionUID = -879599688492364852L; /** * 拡大縮小時に使用するレンダリングオプション. * @author seraphy */ public enum ZoomRenderingType implements LocalizedMessageAware { NONE, BILINER, BICUBIC; public String getLocalizedResourceId() { return "outputOption.zoomRenderingType." + name(); } } /** * 出力する画像モード. * @author seraphy */ public enum PictureMode implements LocalizedMessageAware { NORMAL, OPAQUE, GRAY, ALPHA; public String getLocalizedResourceId() { return "outputOption.pictureMode." + name(); } } /** * JPEG品質 */ private double jpegQuality = 1.; /** * ズームの使用可否 */ private boolean enableZoom; /** * 拡大率 */ private double zoomFactor = 1.; /** * 拡大縮小に使うアルゴリズム */ private ZoomRenderingType zoomRenderingType = ZoomRenderingType.NONE; /** * 出力画像のタイプ */ private PictureMode pictureMode = PictureMode.NORMAL; /** * 背景色を強制するか? */ private boolean forceBgColor; @Override public OutputOption clone() { try { return (OutputOption) super.clone(); } catch (CloneNotSupportedException ex) { throw new RuntimeException(ex); } } public double getJpegQuality() { return jpegQuality; } public void setJpegQuality(double jpegQuality) { if (jpegQuality < 0.1) { jpegQuality = 0.1; } if (jpegQuality > 1) { jpegQuality = 1.; } this.jpegQuality = jpegQuality; } public double getZoomFactor() { return zoomFactor; } public void setZoomFactor(double zoomFactor) { if (zoomFactor < 0.1) { zoomFactor = 0.1; } if (zoomFactor > 10.) { zoomFactor = 10.; } this.zoomFactor = zoomFactor; } public ZoomRenderingType getZoomRenderingType() { return zoomRenderingType; } public void setZoomRenderingType(ZoomRenderingType zoomRenderingType) { if (zoomRenderingType == null) { zoomRenderingType = ZoomRenderingType.NONE; } this.zoomRenderingType = zoomRenderingType; } public PictureMode getPictureMode() { return pictureMode; } public void setPictureMode(PictureMode pictureMode) { if (pictureMode == null) { pictureMode = PictureMode.NORMAL; } this.pictureMode = pictureMode; } public boolean isForceBgColor() { return forceBgColor; } public void setForceBgColor(boolean forceBgColor) { this.forceBgColor = forceBgColor; } public void setEnableZoom(boolean enableZoom) { this.enableZoom = enableZoom; } public boolean isEnableZoom() { return enableZoom; } /** * 推奨値に変更する. */ public void changeRecommend() { if (zoomFactor > 1.) { zoomRenderingType = ZoomRenderingType.BICUBIC; } else if (zoomFactor < 1.) { zoomRenderingType = ZoomRenderingType.BILINER; } else { zoomRenderingType = ZoomRenderingType.NONE; } } @Override public String toString() { return "(OutputOption:(jpegQuality:" + jpegQuality + ")(enableZoom:" + enableZoom + ")(zoomFactor:" + zoomFactor + ")(renderingType:" + zoomRenderingType + ")(pictureMode:" + pictureMode + ")(forceBgColor:" + forceBgColor + "))"; } }charactermanaj/src/charactermanaj/graphics/io/PNGFileImageHeader.java0000644000175000017500000000610411767047532026052 0ustar paulliupaulliupackage charactermanaj.graphics.io; /** * PNGヘッダ情報.
* http://en.wikipedia.org/wiki/Portable_Network_Graphics#cite_note-4 , * http://www.libpng.org/pub/png/spec/1.2/PNG-Chunks.html#C.IHDR あたりを参照.
* @author seraphy */ public class PNGFileImageHeader { private int width; // 4bytes private int height; // 4bytes private int bitDepth; // 1byte private int colorType; // 1byte private int compressionMethod; // 1byte private int filterMethod; // 1byte private int interlaceMethod; // 1byte private boolean transparencyInformation; public int getWidth() { return width; } public void setWidth(int width) { this.width = width; } public int getHeight() { return height; } public void setHeight(int height) { this.height = height; } public int getBitDepth() { return bitDepth; } public void setBitDepth(int bitDepth) { this.bitDepth = bitDepth; } /** * カラータイプを取得する.
* Bit0がインデックス or グレースケール、Bit1がカラー、Bit2がアルファ.
* その組み合わせで8通りあるが、1、5、7はサポート外となる.
*
    *
  • 0: greyscale
  • *
  • 2: Truecolor (Color)
  • *
  • 3: Indexed (Color | Palette)
  • *
  • 4: greyscale alpha (Alpha)
  • *
  • 6: Alpha Color (Color | Alpha) (CharacterManaJでは、これを想定する.)
  • *
* @return */ public int getColorType() { return colorType; } public void setColorType(int colorType) { this.colorType = colorType; } public int getCompressionMethod() { return compressionMethod; } public void setCompressionMethod(int compressionMethod) { this.compressionMethod = compressionMethod; } public int getFilterMethod() { return filterMethod; } public void setFilterMethod(int filterMethod) { this.filterMethod = filterMethod; } public int getInterlaceMethod() { return interlaceMethod; } public void setInterlaceMethod(int interlaceMethod) { this.interlaceMethod = interlaceMethod; } public void setTransparencyInformation(boolean hasTransparencyInformation) { this.transparencyInformation = hasTransparencyInformation; } /** * 透過情報があるか?
* ColorTypeが3(Indexed Color), 2(TrueColor)で、透過情報がある場合は、透過情報つきカラーである.
* そうでなければ透過なしカラーである.
* @return */ public boolean hasTransparencyInformation() { return transparencyInformation; } @Override public String toString() { StringBuilder buf = new StringBuilder(); buf.append("PNG(widht:" + width + ", height:" + height); buf.append(", bitDepth:" + bitDepth + ", colorType: " + colorType); buf.append(", hasTransparency: " + transparencyInformation); buf.append(", compressionMethod:" + compressionMethod); buf.append(", filterMethod:" + filterMethod); buf.append(", interlaceMethod:" + interlaceMethod); buf.append(")"); return buf.toString(); } } charactermanaj/src/charactermanaj/graphics/io/ImageLoaderImpl.java0000644000175000017500000000446711767047532025557 0ustar paulliupaulliupackage charactermanaj.graphics.io; import java.awt.Graphics; import java.awt.image.BufferedImage; import java.io.IOException; import java.io.InputStream; import java.sql.Timestamp; import java.util.logging.Level; import java.util.logging.Logger; import javax.imageio.ImageIO; /** * 画像を読み取ります.
* @author seraphy */ public class ImageLoaderImpl implements ImageLoader { /** * ロガー */ private static final Logger logger = Logger.getLogger(ImageLoaderImpl.class.getName()); /** * 画像リソースからBufferedImageを返します.
* 返される形式はARGBに変換されています.
* @param imageResource 画像リソース * @throws IOException 読み取りに失敗した場合、もしくは画像の形式が不明な場合 */ public LoadedImage load(ImageResource imageResource) throws IOException { if (imageResource == null) { throw new IllegalArgumentException(); } BufferedImage img; InputStream is = imageResource.openStream(); try { img = ImageIO.read(is); } finally { is.close(); } if (img == null) { logger.log(Level.WARNING, "unsuppoted image: " + imageResource); throw new IOException("unsupported image"); } // ARGB形式でなければ変換する. img = convertARGB(img); long lastModified = imageResource.lastModified(); if (logger.isLoggable(Level.FINE)) { logger.log(Level.FINE, "load image: " + imageResource + " ;lastModified=" + new Timestamp(lastModified)); } return new LoadedImage(img, lastModified); } /** * イメージがARGB形式でなければ、ARGB形式に変換して返す.
* そうでなければ、そのまま返す. * @param image イメージ * @return ARGB形式のイメージ */ protected BufferedImage convertARGB(BufferedImage image) { if (image == null) { throw new IllegalArgumentException(); } int typ = image.getType(); if (typ == BufferedImage.TYPE_INT_ARGB) { return image; } // ARGB形式でなければ変換する. BufferedImage img2 = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_INT_ARGB); Graphics g = img2.getGraphics(); try { g.drawImage(image, 0, 0, null); } finally { g.dispose(); } return img2; } } charactermanaj/src/charactermanaj/graphics/io/ImageSaveHelper.java0000644000175000017500000005352511767047532025564 0ustar paulliupaulliupackage charactermanaj.graphics.io; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Component; import java.awt.Font; import java.awt.Graphics2D; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.Insets; import java.awt.RenderingHints; import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; import java.io.OutputStream; import java.util.Arrays; import java.util.Iterator; import java.util.List; import java.util.Properties; import java.util.logging.Level; import java.util.logging.Logger; import javax.imageio.IIOImage; import javax.imageio.ImageIO; import javax.imageio.ImageWriteParam; import javax.imageio.ImageWriter; import javax.imageio.event.IIOWriteWarningListener; import javax.imageio.stream.ImageOutputStream; import javax.swing.BorderFactory; import javax.swing.Box; import javax.swing.JCheckBox; import javax.swing.JComboBox; import javax.swing.JFileChooser; import javax.swing.JLabel; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JSpinner; import javax.swing.SpinnerNumberModel; import javax.swing.filechooser.FileFilter; import charactermanaj.graphics.io.OutputOption.PictureMode; import charactermanaj.graphics.io.OutputOption.ZoomRenderingType; import charactermanaj.util.LocalizedMessageComboBoxRender; import charactermanaj.util.LocalizedResourcePropertyLoader; /** * イメージを保存するためのヘルパークラス.
*/ public class ImageSaveHelper { /** * ロガー */ private static final Logger logger = Logger.getLogger(ImageSaveHelper.class.getName()); /** * リソース */ protected static final String STRINGS_RESOURCE = "languages/imageSaveHelper"; /** * このヘルパクラス用のファイルフィルタの抽象実装 * @author seraphy */ protected static abstract class ImageSaveHelperFilter extends FileFilter { @Override public boolean accept(File f) { if (f.isDirectory()) { // ディレクトリは選択可 return true; } return isSupported(f); } protected boolean isSupported(File f) { // サポートしている拡張子のいずれかにマッチするか? // (大文字・小文字は区別しない.) String lcName = f.getName().toLowerCase(); for (String ext : getSupprotedExtension()) { if (lcName.endsWith("." + ext.toLowerCase())) { return true; } } return false; } /** * 現在の選択されたファイル名を取得し、そのファイル名がデフォルトの拡張子で終端していなければ * デフォルトの拡張子を設定してファイルチューザに設定し直す.
* @param fileChooser ファイルチューザ * @return デフォルトの拡張子で終端されたファイル */ public File supplyDefaultExtension(JFileChooser fileChooser) { File outFile = fileChooser.getSelectedFile(); if (outFile == null) { return null; } if ( !isSupported(outFile)) { String extName = "." + getSupprotedExtension()[0]; outFile = new File(outFile.getPath() + extName); fileChooser.setSelectedFile(outFile); } return outFile; } /** * サポートするファイルの拡張子を取得する.
* 最初のものがデフォルトの拡張子として用いられる.
* @return ファイルの拡張子 */ protected abstract String[] getSupprotedExtension(); } /** * PNGファイルフィルタ */ protected static final FileFilter pngFilter = new ImageSaveHelperFilter() { @Override public String getDescription() { return "PNG(*.png)"; } @Override protected String[] getSupprotedExtension() { return new String[] {"png"}; } }; /** * JPEGファイルフィルタ */ protected static final FileFilter jpegFilter = new ImageSaveHelperFilter() { @Override public String getDescription() { return "JPEG(*.jpg;*.jpeg)"; } @Override protected String[] getSupprotedExtension() { return new String[] {"jpeg", "jpg"}; } }; /** * BMPファイルフィルタ */ protected static final FileFilter bmpFilter = new ImageSaveHelperFilter() { @Override public String getDescription() { return "Bitmap(*.bmp)"; } @Override protected String[] getSupprotedExtension() { return new String[] {"bmp"}; } }; /** * このヘルパクラスで定義されているファイルフィルタのリスト */ protected static final List fileFilters = Arrays.asList( pngFilter, jpegFilter, bmpFilter); /** * イメージビルダファクトリ */ protected OutputImageBuilderFactory imageBuilderFactory; /** * 最後に使用した出力オプション.
* 未使用であれば規定値.
*/ protected OutputOption outputOption; /** * 最後に使用したディレクトリ */ protected File lastUseSaveDir; /** * 最後に使用したフィルタ */ protected FileFilter lastUseFilter = pngFilter; /** * 最後に使用したディレクトリを設定する * @param lastUseSaveDir 最後に使用したディレクトリ、設定しない場合はnull */ public void setLastUseSaveDir(File lastUseSaveDir) { this.lastUseSaveDir = lastUseSaveDir; } /** * 最後に使用したディレクトリを取得する * @return 最後に使用したディレクトリ、なければnull */ public File getLastUsedSaveDir() { return lastUseSaveDir; } /** * コンストラクタ */ public ImageSaveHelper() { imageBuilderFactory = new OutputImageBuilderFactory(); outputOption = imageBuilderFactory.createDefaultOutputOption(); } /** * 画像ファイルの保存用ダイアログを表示する. * @param parent 親ウィンドウ * @return ファイル名 */ public File showSaveFileDialog(Component parent) { // 最後に使用したディレクトリを指定してファイルダイアログを構築する. JFileChooser fileChooser = new JFileChooser(lastUseSaveDir) { private static final long serialVersionUID = -9091369410030011886L; /** * OKボタン押下時の処理. */ @Override public void approveSelection() { File outFile = getSelectedFile(); if (outFile == null) { return; } // 選択したファイルフィルタに従ってデフォルトの拡張子を付与する. FileFilter selfilter = getFileFilter(); if (selfilter instanceof ImageSaveHelperFilter) { outFile = ((ImageSaveHelperFilter) selfilter).supplyDefaultExtension(this); } // ファイルが存在すれば上書き確認する. if (outFile.exists()) { Properties strings = LocalizedResourcePropertyLoader.getInstance() .getLocalizedProperties(STRINGS_RESOURCE); if (JOptionPane.showConfirmDialog(this, strings.getProperty("confirmOverwrite"), strings.getProperty("confirm"), JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE) != JOptionPane.YES_OPTION) { return; } } super.approveSelection(); } }; // // アクセサリパネルの追加˙ // final OutputOptionPanel accessoryPanel = new OutputOptionPanel(this.outputOption); // fileChooser.setAccessory(accessoryPanel); // ファイルフィルタ設定 fileChooser.setAcceptAllFileFilterUsed(false); for (FileFilter fileFilter : fileFilters) { fileChooser.addChoosableFileFilter(fileFilter); } // 最後に使用したファイルフィルタが既定のフィルタでないか未設定であればPNGにする. if (lastUseFilter == null || !fileFilters.contains(lastUseFilter)) { lastUseFilter = pngFilter; } // 最後に使用したフィルタをデフォルトのフィルタに設定する. fileChooser.setFileFilter(lastUseFilter); // ファイルダイアログを開く. int ret = fileChooser.showSaveDialog(parent); if (ret != JFileChooser.APPROVE_OPTION) { return null; } // // 出力オプションの保存 // OutputOption outputOption = accessoryPanel.getOutputOption(); // this.outputOption = outputOption; // 最後に使用したフィルタ、および選択したディレクトリを記憶する. File outFile = fileChooser.getSelectedFile(); lastUseSaveDir = outFile.getParentFile(); lastUseFilter = fileChooser.getFileFilter(); // 選択したファイルを返す. return outFile; } public OutputOption getOutputOption() { return outputOption.clone(); } public void setOutputOption(OutputOption outputOption) { if (outputOption == null) { throw new IllegalArgumentException(); } this.outputOption = outputOption.clone(); } /** * ファイル名を指定してイメージをファイルに出力します.
* 出力形式は拡張子より判定します.
* サポートされていない拡張子の場合はIOException例外が発生します.
* @param img イメージ * @param imgBgColor JPEGの場合の背景色 * @param outFile 出力先ファイル(拡張子が必須) * @param warnings 警告を記録するバッファ、必要なければnull * @throws IOException 失敗 */ public void savePicture(BufferedImage img, Color imgBgColor, File outFile, final StringBuilder warnings) throws IOException { if (img == null || outFile == null) { throw new IllegalArgumentException(); } // ファイル名から拡張子を取り出します. String fname = outFile.getName(); int extpos = fname.lastIndexOf("."); if (extpos < 0) { throw new IOException("missing file extension."); } String ext = fname.substring(extpos + 1).toLowerCase(); // 拡張子に対するImageIOのライタを取得します. Iterator ite = ImageIO.getImageWritersBySuffix(ext); if (!ite.hasNext()) { throw new IOException("unsupported file extension: " + ext); } ImageWriter iw = ite.next(); // ライタを使いイメージを書き込みます. savePicture(img, imgBgColor, iw, outFile, warnings); } /** * イメージをMIMEで指定された形式で出力します. * @param img イメージ * @param imgBgColor JPEGの場合の背景色 * @param outstm 出力先 * @param mime MIME * @param warnings 警告を書き込むバッファ、必要なければnull * @throws IOException 例外 */ public void savePicture(BufferedImage img, Color imgBgColor, OutputStream outstm, String mime, final StringBuilder warnings) throws IOException { if (img == null || outstm == null || mime == null) { throw new IllegalArgumentException(); } // mimeがパラメータ付きの場合は、パラメータを除去する. int pt = mime.indexOf(';'); if (pt >= 0) { mime = mime.substring(0, pt).trim(); } // サポートしているmimeタイプを検出. Iterator ite = ImageIO.getImageWritersByMIMEType(mime); if (!ite.hasNext()) { throw new IOException("unsupported mime: " + mime); } ImageWriter iw = ite.next(); savePicture(img, imgBgColor, iw, outstm, warnings); outstm.flush(); } protected void savePicture(BufferedImage img, Color imgBgColor, ImageWriter iw, Object output, final StringBuilder warnings) throws IOException { try { iw.addIIOWriteWarningListener(new IIOWriteWarningListener() { public void warningOccurred(ImageWriter source, int imageIndex, String warning) { if (warnings.length() > 0) { warnings.append(System.getProperty("line.separator")); } if (warnings != null) { warnings.append(warning); } logger.log(Level.WARNING, warning); } }); boolean jpeg = false; boolean bmp = false; for (String mime : iw.getOriginatingProvider().getMIMETypes()) { if (mime.contains("image/jpeg") || mime.contains("image/jpg")) { jpeg = true; break; } if (mime.contains("image/bmp") || mime.contains("image/x-bmp") || mime.contains("image/x-windows-bmp")) { bmp = true; break; } } ImageWriteParam iwp = iw.getDefaultWriteParam(); IIOImage ioimg; if (jpeg) { iwp.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); iwp.setCompressionQuality((float) outputOption.getJpegQuality()); // JPEGは透過色をサポートしていないので背景色を設定する. ioimg = new IIOImage(createJpegFormatPicture(img, imgBgColor), null, null); } else if (bmp) { // BMPは透過色をサポートしていないので背景色を設定する. ioimg = new IIOImage(createBMPFormatPicture(img, imgBgColor), null, null); } else if (outputOption.isForceBgColor()) { // 背景色強制 // JPEG, BMP以外(PNGを想定) ioimg = new IIOImage(createOpaquePNGFormatPicture(img, imgBgColor), null, null); } else { // 透過有効のまま // JPEG, BMP以外(PNGを想定) ioimg = new IIOImage(img, null, null); } ImageOutputStream imgstm = ImageIO.createImageOutputStream(output); try { iw.setOutput(imgstm); iw.write(null, ioimg, iwp); } finally { imgstm.close(); } } finally { iw.dispose(); } } /** * ARGB形式から、アルファチャンネルを削除し、かわりに背景色を設定したBGR形式画像を返します.
* JPEG画像として用いることを想定しています.
* @param img 変換するイメージ * @param imgBgColor 背景色 * @return 変換されたイメージ */ public BufferedImage createJpegFormatPicture(BufferedImage img, Color imgBgColor) { if (imgBgColor == null) { imgBgColor = Color.WHITE; } return createFormatPicture(img, imgBgColor, BufferedImage.TYPE_INT_BGR); } /** * ARGB形式から、アルファチャンネルを削除し、かわりに背景色を設定したBGR形式画像を返します.
* BMP画像として用いることを想定しています.
* @param img 変換するイメージ * @param imgBgColor 背景色 * @return 変換されたイメージ */ public BufferedImage createBMPFormatPicture(BufferedImage img, Color imgBgColor) { if (imgBgColor == null) { imgBgColor = Color.WHITE; } return createFormatPicture(img, imgBgColor, BufferedImage.TYPE_3BYTE_BGR); } /** * ARGB形式から、アルファチャンネルを削除し、かわりに背景色を設定したRGB形式画像を返します.
* 背景付きPNG画像として用いることを想定しています.
* @param img 変換するイメージ * @param imgBgColor 背景色 * @return 変換されたイメージ */ public BufferedImage createOpaquePNGFormatPicture(BufferedImage img, Color imgBgColor) { if (imgBgColor == null) { imgBgColor = Color.WHITE; } return createFormatPicture(img, imgBgColor, BufferedImage.TYPE_INT_ARGB); } /** * ARGB形式から、アルファチャンネルを削除し、かわりに背景色を設定したBGR形式画像を返します.
* JPEG画像として用いることを想定しています.
* @param img 変換するイメージ * @param imgBgColor 背景色 * @return 変換されたイメージ */ protected BufferedImage createFormatPicture(BufferedImage img, Color imgBgColor, int type) { if (img == null) { throw new IllegalArgumentException(); } int w = img.getWidth(); int h = img.getHeight(); BufferedImage tmpImg = new BufferedImage(w, h, type); Graphics2D g = tmpImg.createGraphics(); try { g.setRenderingHint( RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY); if (imgBgColor != null) { g.setColor(imgBgColor); g.fillRect(0, 0, w, h); } g.drawImage(img, 0, 0, w, h, 0, 0, w, h, null); } finally { g.dispose(); } return tmpImg; } } /** * 出力オプションパネル * @author seraphy */ class OutputOptionPanel extends JPanel { private static final long serialVersionUID = 1L; private JSpinner jpegQualitySpinner; private JCheckBox lblZoom; private JSpinner zoomSpinner; private JComboBox zoomAlgoCombo; private JComboBox pictureModeCombo; private JCheckBox checkForceBgColor; public OutputOptionPanel() { this(new OutputOption()); } public OutputOptionPanel(OutputOption outputOption) { Properties strings = LocalizedResourcePropertyLoader.getInstance() .getLocalizedProperties(ImageSaveHelper.STRINGS_RESOURCE); setBorder(BorderFactory.createCompoundBorder( BorderFactory.createEmptyBorder(3, 3, 3, 3), BorderFactory.createTitledBorder( strings.getProperty("ouputOption.caption")))); setLayout(new GridBagLayout()); GridBagConstraints gbc = new GridBagConstraints(); gbc.anchor = GridBagConstraints.EAST; gbc.fill = GridBagConstraints.BOTH; gbc.insets = new Insets(3, 3, 3, 3); gbc.gridheight = 1; gbc.ipadx = 0; gbc.ipady = 0; gbc.weighty = 0; // 左端余白 gbc.gridx = 0; gbc.gridy = 0; gbc.gridwidth = 1; gbc.weightx = 0.; add(Box.createHorizontalStrut(6), gbc); // JPEG gbc.gridx = 0; gbc.gridy = 0; gbc.gridwidth = 3; gbc.weightx = 1.; JLabel lblJpeg = new JLabel( strings.getProperty("outputOption.jpeg.caption")); lblJpeg.setFont(lblJpeg.getFont().deriveFont(Font.BOLD)); add(lblJpeg, gbc); gbc.gridx = 1; gbc.gridy = 1; gbc.gridwidth = 1; gbc.weightx = 0.; add(new JLabel( strings.getProperty("outputOption.jpeg.quality"), JLabel.RIGHT), gbc); SpinnerNumberModel spmodel = new SpinnerNumberModel(100, 10, 100, 1); this.jpegQualitySpinner = new JSpinner(spmodel); gbc.gridx = 2; gbc.gridy = 1; gbc.gridwidth = 1; add(jpegQualitySpinner, gbc); gbc.gridx = 3; gbc.gridy = 1; gbc.gridwidth = 1; add(new JLabel("%"), gbc); // ZOOM gbc.gridx = 0; gbc.gridy = 2; gbc.gridwidth = 3; gbc.weightx = 1.; gbc.insets = new Insets(10, 3, 3, 3); lblZoom = new JCheckBox(strings.getProperty("outputOption.zoom.caption")); lblZoom.setFont(lblJpeg.getFont().deriveFont(Font.BOLD)); add(lblZoom, gbc); gbc.gridx = 1; gbc.gridy = 3; gbc.gridwidth = 1; gbc.weightx = 0.; gbc.insets = new Insets(3, 3, 3, 3); add(new JLabel( strings.getProperty("outputOption.zoom.factor"), JLabel.RIGHT), gbc); SpinnerNumberModel zoomSpModel = new SpinnerNumberModel(100, 20, 800, 1); this.zoomSpinner = new JSpinner(zoomSpModel); gbc.gridx = 2; gbc.gridy = 3; gbc.gridwidth = 1; add(zoomSpinner, gbc); gbc.gridx = 3; gbc.gridy = 3; gbc.gridwidth = 1; add(new JLabel("%"), gbc); gbc.gridx = 1; gbc.gridy = 4; gbc.gridwidth = 1; gbc.weightx = 1.; add(new JLabel( strings.getProperty("outputOption.zoom.renderingMode"), JLabel.RIGHT), gbc); this.zoomAlgoCombo = new JComboBox(ZoomRenderingType.values()); this.zoomAlgoCombo.setRenderer(new LocalizedMessageComboBoxRender(strings)); gbc.gridx = 2; gbc.gridy = 4; gbc.gridwidth = 2; gbc.weightx = 0.; add(zoomAlgoCombo, gbc); // 画像モード gbc.gridx = 0; gbc.gridy = 5; gbc.gridwidth = 3; gbc.insets = new Insets(10, 3, 3, 3); JLabel lblPictureMode = new JLabel( strings.getProperty("outputOption.picture")); lblPictureMode.setFont(lblJpeg.getFont().deriveFont(Font.BOLD)); add(lblPictureMode, gbc); gbc.gridx = 1; gbc.gridy = 6; gbc.gridwidth = 1; gbc.insets = new Insets(3, 3, 3, 3); add(new JLabel( strings.getProperty("outputOption.picture.type"), JLabel.RIGHT), gbc); this.pictureModeCombo = new JComboBox(PictureMode.values()); this.pictureModeCombo.setRenderer( new LocalizedMessageComboBoxRender(strings)); gbc.gridx = 2; gbc.gridy = 6; gbc.gridwidth = 2; gbc.weightx = 1.; add(pictureModeCombo, gbc); gbc.gridx = 1; gbc.gridy = 7; gbc.gridwidth = 3; gbc.weightx = 0.; checkForceBgColor = new JCheckBox( strings.getProperty("outputOption.picture.forceBgColor")); add(checkForceBgColor, gbc); JPanel pnlBgAlpha = new JPanel(new BorderLayout(3, 3)); pnlBgAlpha.add(new JLabel("背景アルファ"), BorderLayout.WEST); SpinnerNumberModel bgAlphaModel = new SpinnerNumberModel(255, 0, 255, 1); JSpinner bgAlphaSpinner = new JSpinner(bgAlphaModel); pnlBgAlpha.add(bgAlphaSpinner, BorderLayout.CENTER); gbc.gridx = 1; gbc.gridy = 8; gbc.gridwidth = 3; gbc.weightx = 0.; add(pnlBgAlpha, gbc); // 余白 gbc.gridx = 0; gbc.gridy = 9; gbc.gridwidth = 4; gbc.weightx = 1.; gbc.weighty = 1.; add(Box.createGlue(), gbc); // update setOutputOption(outputOption); } public void setOutputOption(OutputOption outputOption) { if (outputOption == null) { outputOption = new OutputOption(); } jpegQualitySpinner.setValue((int) (outputOption.getJpegQuality() * 100)); lblZoom.setSelected(outputOption.isEnableZoom()); zoomSpinner.setValue((int) (outputOption.getZoomFactor() * 100)); zoomAlgoCombo.setSelectedItem(outputOption.getZoomRenderingType()); pictureModeCombo.setSelectedItem(outputOption.getPictureMode()); checkForceBgColor.setSelected(outputOption.isForceBgColor()); } public OutputOption getOutputOption() { OutputOption outputOption = new OutputOption(); outputOption.setJpegQuality(((Integer) jpegQualitySpinner.getValue() / 100.)); outputOption.setEnableZoom(lblZoom.isSelected()); outputOption.setZoomFactor(((Integer) zoomSpinner.getValue() / 100.)); outputOption.setZoomRenderingType((ZoomRenderingType) zoomAlgoCombo.getSelectedItem()); outputOption.setPictureMode((PictureMode) pictureModeCombo.getSelectedItem()); outputOption.setForceBgColor(checkForceBgColor.isSelected()); return outputOption; } } charactermanaj/src/charactermanaj/graphics/io/OutputImageBuilder.java0000644000175000017500000000075511767047532026332 0ustar paulliupaulliupackage charactermanaj.graphics.io; import java.awt.image.BufferedImage; public abstract class OutputImageBuilder { private OutputOption outputOption; public OutputImageBuilder(OutputOption outputOption) { if (outputOption == null) { throw new IllegalArgumentException(); } this.outputOption = outputOption; } public OutputOption getOutputOption() { return outputOption.clone(); } public abstract BufferedImage buildImage(BufferedImage src); } charactermanaj/src/charactermanaj/graphics/io/OutputImageBuilderFactory.java0000644000175000017500000000107311767047532027654 0ustar paulliupaulliupackage charactermanaj.graphics.io; import charactermanaj.model.AppConfig; public class OutputImageBuilderFactory { public OutputOption createDefaultOutputOption() { AppConfig appConfig = AppConfig.getInstance(); OutputOption outputOption = new OutputOption(); outputOption.setJpegQuality(appConfig.getCompressionQuality()); return outputOption; } public OutputImageBuilder createOutputImageBuilder(OutputOption outputOption) { if (outputOption == null) { outputOption = createDefaultOutputOption(); } return null; } } charactermanaj/src/charactermanaj/graphics/io/UkagakaImageConverter.java0000644000175000017500000001744611767047532026764 0ustar paulliupaulliupackage charactermanaj.graphics.io; import java.awt.Color; import java.awt.image.BufferedImage; import java.awt.image.Raster; import java.util.Arrays; import java.util.logging.Level; import java.util.logging.Logger; /** * 伺か用PNG変換クラス. * @author seraphy */ public class UkagakaImageConverter { /** * ロガー */ private static final Logger logger = Logger.getLogger(UkagakaImageConverter.class.getName()); /** * シングルトン */ private static final UkagakaImageConverter inst = new UkagakaImageConverter(); /** * シングルトンコンストラクタ */ protected UkagakaImageConverter() { super(); } public static UkagakaImageConverter getInstance() { return inst; } /** * 伺か用PNA(アルファチャネルのグレースケール表現)に変換する. * @param img 変換元の透過画像(TYPE_INT_ARGB専用) * @return 伺か用PNA画像(TYPE_INT_RGB, アルファチャネルのグレースケール表現) */ public BufferedImage createUkagakaPNA(BufferedImage img) { if (img == null) { throw new IllegalArgumentException("引数にnullは指定できません。"); } if (img.getType() != BufferedImage.TYPE_INT_ARGB) { throw new IllegalArgumentException("TYPE_INT_ARGB専用です."); } int w = img.getWidth(); int h = img.getHeight(); Raster raster = img.getData(); BufferedImage outimg = new BufferedImage(w, h, BufferedImage.TYPE_BYTE_GRAY); // アルファ値をグレースケール表現に変換 int[] argb = null; for (int y = 0; y < h; y++) { for (int x = 0; x < w; x++) { argb = raster.getPixel(x, y, argb); int a = argb[3]; // alpha int o = a << 16 | a << 8 | a; outimg.setRGB(x, y, o); } } return outimg; } /** * 伺か用PNGに変換するための透過色に設定できる色を選択する.
* 該当がない場合(選択できなかった場合)はnullを返す.
* @param img 変換元の透過画像(TYPE_INT_ARGB専用) * @return 伺か用PNG画像(非透過)の透過色キー、該当がない場合はnull */ public Color detectTransparentColorKey(BufferedImage img) { if (img == null) { throw new IllegalArgumentException("引数にnullは指定できません。"); } if (img.getType() != BufferedImage.TYPE_INT_ARGB) { throw new IllegalArgumentException("TYPE_INT_ARGB専用です."); } int w = img.getWidth(); int h = img.getHeight(); Raster raster = img.getData(); // 512色インデックスグループ化 final int colorMx = 512; int[] colorCounts = new int[colorMx]; // ピクセル単位の512色インデックスごとの使用数を計測 int[] argb = new int[4]; for (int y = 0; y < h; y++) { for (int x = 0; x < w; x++) { argb = raster.getPixel(x, y, argb); int a = argb[3]; if (a == 0) { continue; // 完全透過はノーカウント } // 上位3ビットのみ int r = (argb[0] >>> 5) & 0x07; int g = (argb[1] >>> 5) & 0x07; int b = (argb[2] >>> 5) & 0x07; // インデックス生成 int idx = r << 6 | g << 3 | b; colorCounts[idx]++; } } if (logger.isLoggable(Level.FINER)) { logger.log(Level.FINER, "counts=" + Arrays.toString(colorCounts)); } // 色インデックスの色相と選択候補の重み float[] colorHues = new float[colorMx]; float[] hsb = new float[3]; // 候補の重み float[] colorWeights = new float[colorMx]; // 上位3ビットインデックスから8ビットRGB値への変換マップ int[] colorMap = new int[colorMx]; for (int idx = 0; idx < colorMx; idx++) { int r = ((idx >>> 6) & 0x07); int g = ((idx >>> 3) & 0x07); int b = ((idx) & 0x07); r = r << 5 | 0x1f; g = g << 5 | 0x1f; b = b << 5 | 0x1f; hsb = Color.RGBtoHSB(r, g, b, hsb); colorHues[idx] = hsb[0]; colorWeights[idx] = hsb[1]; // 濃さを重みとする. colorMap[idx] = r << 16 | g << 8 | b; } if (logger.isLoggable(Level.FINER)) { logger.log(Level.FINER, "weight=" + Arrays.toString(colorWeights)); } // 色インデックスごとの未使用色相の個数カウント float[] unusedColorScore = new float[colorMx]; float tolerance = 0.0625f; for (int idx = 0; idx < colorMx; idx++) { if (colorCounts[idx] > 0) { // 使用していればスキップ. continue; } // 類似色相のスコア float hue = colorHues[idx]; float score = 0.f; for (int ref = 0; ref < colorMx; ref++) { if (colorCounts[ref] > 0) { // 使用していればスキップ continue; } float refHue = colorHues[ref]; // 色相が等しいものを1、色相がズレるほどに0に近づく float diff = (tolerance - Math.abs(hue - refHue)) / tolerance; if (diff < 0) { // 範囲を超えていればスキップ continue; } score += diff; } // 色による重み付け float weight = colorWeights[idx]; unusedColorScore[idx] = score * weight; } if (logger.isLoggable(Level.FINER)) { logger.log(Level.FINER, "scores=" + Arrays.toString(unusedColorScore)); } // もっとも重いスコアを選択 float maxScore = 0; int maxIdx = -1; for (int idx = 0; idx < colorMx; idx++) { float score = unusedColorScore[idx]; if (score > maxScore) { maxScore = score; maxIdx = idx; } } if (logger.isLoggable(Level.FINER)) { logger.log(Level.FINER, "selectedIdx=" + maxIdx + "/score=" + maxScore); } // 透過用色の取得 if (maxIdx >= 0) { return new Color(colorMap[maxIdx]); } // 候補を見つけられなかった場合. return null; } /** * 伺か用PNGに変換する.
* @param img 変換元の透過画像(TYPE_INT_ARGB専用) * @param transparentColorKey 透過色キー、nullの場合は自動選択 * @return 伺か用PNG画像(TYPE_INT_RGB, 左上に透過色指定あり) */ public BufferedImage createUkagakaPNG(BufferedImage img, Color transparentColorKey) { if (img == null) { throw new IllegalArgumentException("引数にnullは指定できません。"); } if (img.getType() != BufferedImage.TYPE_INT_ARGB) { throw new IllegalArgumentException("TYPE_INT_ARGB専用です."); } // 透過色に設定するカラーキーの取得 if (transparentColorKey == null) { transparentColorKey = detectTransparentColorKey(img); } int transparencyColor; if (transparentColorKey != null) { transparencyColor = transparentColorKey.getRGB() & 0xffffff; } else { // カラーキーの取得がでなければ、黒に限りなく近い非黒を透過色として代替する. logger.log(Level.INFO, "透過色の選択ができなかったため、0x010101で代用します."); transparencyColor = 0x010101; } int w = img.getWidth(); int h = img.getHeight(); Raster raster = img.getData(); // 完全な透過ピクセルに対して算定した透過色を割り当て、 // 画像の左上に透過色を設定する. int argb[] = new int[4]; BufferedImage outimg = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB); for (int y = 0; y < h; y++) { for (int x = 0; x < w; x++) { argb = raster.getPixel(x, y, argb); int a = argb[3]; // alpha int c; if (a == 0) { // 完全透過の場合のみ透過色を設定 c = transparencyColor; } else { // それ以外はアルファを無視してRGBのみ int r = argb[0]; int g = argb[1]; int b = argb[2]; c = r << 16 | g << 8 | b; } outimg.setRGB(x, y, c); } // 左上(0,0)に透過とする色を設定 outimg.setRGB(0, 0, transparencyColor); } return outimg; } } charactermanaj/src/charactermanaj/graphics/io/UkagakaImageSaveHelper.java0000644000175000017500000002664511767047532027054 0ustar paulliupaulliupackage charactermanaj.graphics.io; import java.awt.Color; import java.awt.Graphics2D; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.image.BufferedImage; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.util.Properties; import java.util.logging.Level; import java.util.logging.Logger; import javax.imageio.IIOImage; import javax.imageio.ImageIO; import javax.imageio.ImageReadParam; import javax.imageio.ImageReader; import javax.imageio.ImageWriteParam; import javax.imageio.ImageWriter; import javax.imageio.event.IIOWriteWarningListener; import javax.imageio.stream.ImageInputStream; import javax.imageio.stream.ImageOutputStream; import javax.swing.JFileChooser; import javax.swing.JFrame; import javax.swing.JOptionPane; import javax.swing.filechooser.FileFilter; import charactermanaj.ui.UkagakaConvertDialog; import charactermanaj.util.LocalizedResourcePropertyLoader; /** * 伺か用PNG/PNA出力ヘルパ. * @author seraphy * */ public class UkagakaImageSaveHelper { /** * ロガー */ private static final Logger logger = Logger.getLogger(UkagakaImageSaveHelper.class.getName()); /** * リソース */ protected static final String STRINGS_RESOURCE = "languages/ukagakaImageSaveHelper"; /** * PNGファイルフィルタ */ protected static final FileFilter pngFilter = new FileFilter() { @Override public boolean accept(File f) { return f.isDirectory() || f.getName().endsWith(".png"); } @Override public String getDescription() { return "PNG(*.png)"; } }; /** * 最後に開いたディレクトリ.
* まだ使用していなければnull.
*/ protected File lastUseOpenDir; /** * 最後に保存したディレクトリ.
* まだ使用していなければnull.
*/ protected File lastUseSaveDir; /** * 最後に保存したファイル名 */ protected String lastUseSaveName = "surface"; /** * 最後に使用した透過色キー.
* まだ使用していなければnull.
*/ protected Color transparentColorKey; /** * 最後に使用した透過色キーモード. */ protected boolean autoTransparentColor = true; /** * コンストラクタ */ public UkagakaImageSaveHelper() { Properties strings = LocalizedResourcePropertyLoader.getInstance() .getLocalizedProperties(STRINGS_RESOURCE); lastUseSaveName = strings.getProperty("default.lastUseSaveName"); } protected JFileChooser createFileChooser(final boolean save) { JFileChooser fileChooser = new JFileChooser() { private static final long serialVersionUID = 1L; @Override public void approveSelection() { File outFile = getSelectedFile(); if (outFile == null) { return; } String lcName = outFile.getName().toLowerCase(); FileFilter selfilter = getFileFilter(); if (selfilter == pngFilter) { if (!lcName.endsWith(".png")) { outFile = new File(outFile.getPath() + ".png"); setSelectedFile(outFile); } } if (save && outFile.exists()) { Properties strings = LocalizedResourcePropertyLoader.getInstance() .getLocalizedProperties(STRINGS_RESOURCE); if (JOptionPane.showConfirmDialog(this, strings.getProperty("confirmOverwrite"), strings.getProperty("confirm"), JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE) != JOptionPane.YES_OPTION) { return; } } super.approveSelection(); } }; if (lastUseSaveDir != null) { fileChooser.setCurrentDirectory(lastUseSaveDir); } fileChooser.setFileFilter(pngFilter); if ( !save) { fileChooser.setAcceptAllFileFilterUsed(false); } fileChooser.addChoosableFileFilter(pngFilter); return fileChooser; } /** * 透過画像(TYPE_INT_ARGB)から、伺か用のPNG/PNAファイルに出力する. * @param parent 親フレーム * @param img 対象イメージ * @param colorKey マニュアル指定時の候補(前回のものを優先) * @throws IOException 出力に失敗した場合 */ public void save(JFrame parent, BufferedImage img, Color colorKey) throws IOException { final UkagakaConvertDialog dlg = new UkagakaConvertDialog(parent); if (!autoTransparentColor && transparentColorKey != null) { // 前回マニュアル透過色キー指定であれば、それを使う. colorKey = transparentColorKey; } dlg.setExportImage(img, colorKey); dlg.setAutoTransparentColor(autoTransparentColor); dlg.setSaveActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { JFileChooser fileChooser = createFileChooser(true); fileChooser.setSelectedFile(new File(lastUseSaveDir, lastUseSaveName)); int ret = fileChooser.showSaveDialog(dlg); if (ret != JFileChooser.APPROVE_OPTION) { return; } // 結果 dlg.setResult(fileChooser.getSelectedFile()); dlg.dispose(); } }); dlg.setVisible(true); File selectedFile = (File) dlg.getResult(); if (selectedFile == null) { return; } lastUseSaveName = selectedFile.getName(); lastUseSaveDir = selectedFile.getParentFile(); File pngFile = selectedFile; File pnaFile = makePNAFileName(pngFile); File[] outfiles = {pngFile, pnaFile}; BufferedImage[] outimages = { dlg.getOpaqueImage(), dlg.getAlphaImage() }; savePNGImages(outfiles, outimages); } /** * 複数のファイルとイメージを指定して書き込みます.
* ファイルとイメージの個数は一致していなければなりません.
* 同じ添え字のファイルに対して、その添え字のイメージが出力されます.
* いずれかで失敗した場合、その時点で処理は打ち切られて例外が返されます.
* (すでに出力されたファイル、もしくは書き込み中のファイルは放置されます.)
* @param outfiles ファイルの配列 * @param outimages イメージの配列 * @throws IOException 失敗 */ protected void savePNGImages(File[] outfiles, BufferedImage[] outimages) throws IOException { if (outfiles == null || outimages == null) { throw new IllegalArgumentException("引数にnullは指定でまきせん。"); } if (outfiles.length != outimages.length) { throw new IllegalArgumentException("ファイルおよびイメージの個数は一致していなければなりません."); } ImageWriter iw = ImageIO.getImageWritersByFormatName("png").next(); try { iw.addIIOWriteWarningListener(new IIOWriteWarningListener() { public void warningOccurred(ImageWriter source, int imageIndex, String warning) { logger.log(Level.WARNING, warning); } }); for (int idx = 0; idx < outfiles.length; idx++) { File outfile = outfiles[idx]; BufferedImage outimage = outimages[idx]; BufferedOutputStream bos = new BufferedOutputStream( new FileOutputStream(outfile)); try { ImageWriteParam iwp = iw.getDefaultWriteParam(); IIOImage ioimg = new IIOImage(outimage, null, null); ImageOutputStream imgstm = ImageIO.createImageOutputStream(bos); try { iw.setOutput(imgstm); iw.write(null, ioimg, iwp); } finally { imgstm.close(); } } finally { bos.close(); } } } finally { iw.dispose(); } } /** * 複数ファイルを指定して既存のファイルから伺かPNG/PNAに変換して出力する.(ユーテリティ) * @param parent 親フレーム * @param colorKey 透過色キー(候補) * @throws IOException 失敗 */ public void convertChooseFiles(JFrame parent, Color colorKey) throws IOException { JFileChooser fileChooser = createFileChooser(false); fileChooser.setCurrentDirectory(lastUseOpenDir); fileChooser.setMultiSelectionEnabled(true); int ret = fileChooser.showOpenDialog(parent); if (ret != JFileChooser.APPROVE_OPTION) { return; } // 選択したディレクトリを記憶する. File[] files = fileChooser.getSelectedFiles(); if (files == null || files.length == 0) { return; } lastUseOpenDir = files[0].getParentFile(); final UkagakaConvertDialog dlg = new UkagakaConvertDialog(parent, null, true); if (!autoTransparentColor && transparentColorKey != null) { // 前回マニュアル透過色キー指定であれば、それを使う. colorKey = transparentColorKey; } dlg.setAutoTransparentColor(autoTransparentColor); ImageReader ir = ImageIO.getImageReadersByFormatName("png").next(); try { for (final File file : files) { String fname = file.getName(); ImageReadParam param = ir.getDefaultReadParam(); BufferedImage img; ImageInputStream iis = ImageIO.createImageInputStream(file); try { ir.setInput(iis); img = ir.read(0, param); } finally { iis.close(); } img = convertIntARGB(img); dlg.setCaption(fname); dlg.setExportImage(img, colorKey); dlg.setSaveActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { if ( !dlg.isOverwriteOriginalFile()) { JFileChooser fileChooser = createFileChooser(true); fileChooser.setCurrentDirectory(file.getParentFile()); fileChooser.setSelectedFile(file); int ret = fileChooser.showSaveDialog(dlg); if (ret != JFileChooser.APPROVE_OPTION) { return; } // 選択結果 dlg.setResult(fileChooser.getSelectedFile()); } else { // ソースと同じ (上書き) dlg.setResult(file); } dlg.dispose(); } }); dlg.setVisible(true); File selectedFile = (File) dlg.getResult(); if (selectedFile == null) { // キャンセルされた場合 break; } File pngFile = selectedFile; File pnaFile = makePNAFileName(pngFile); File[] outfiles = {pngFile, pnaFile}; BufferedImage[] outimages = { dlg.getOpaqueImage(), dlg.getAlphaImage() }; savePNGImages(outfiles, outimages); } } finally { ir.dispose(); } } /** * BufferedImageをINT_TYPE_ARGBに設定する.
* @param img イメージ * @return 形式をINT_ARGBに変換されたイメージ */ protected BufferedImage convertIntARGB(BufferedImage img) { if (img == null || img.getType() == BufferedImage.TYPE_INT_ARGB) { // nullであるか、変換不要であれば、そのまま返す. return img; } int w = img.getWidth(); int h = img.getHeight(); BufferedImage dst = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB); Graphics2D g = dst.createGraphics(); try { g.drawImage(img, 0, 0, w, h, 0, 0, w, h, null); } finally { g.dispose(); } return dst; } /** * 拡張子をPNAに変換して返す. * @param pngFile PNGファイル名 * @return PNAファイル名 */ protected File makePNAFileName(File pngFile) { if (pngFile == null) { return null; } String fname = pngFile.getName(); int extpos = fname.lastIndexOf('.'); if (extpos >= 0) { fname = fname.substring(0, extpos); } fname += ".pna"; return new File(pngFile.getParent(), fname); } } charactermanaj/src/charactermanaj/graphics/io/ImageCachedLoader.java0000644000175000017500000000415511767047532026017 0ustar paulliupaulliupackage charactermanaj.graphics.io; import java.io.IOException; /** * 一度読み込んだ画像をキャッシュする画像ローダ.
* すでに読み込まれており、ファイルの更新日に変更がなければ読み込み済みの画像をかえす.
* @author seraphy */ public class ImageCachedLoader extends ImageLoaderImpl { /** * リソースに対するイメージキャッシュ.
* リソースは複数のプロファイルで共有しえるのでstaticとしている。 */ private static ImageCache caches = new ImageCache(); public LoadedImage load(ImageResource imageResource) throws IOException { if (imageResource == null) { throw new IllegalArgumentException(); } ImageResourceCacheKey key = new ImageResourceCacheKey(imageResource); synchronized (caches) { LoadedImage loadedImage = caches.get(key); if (loadedImage != null) { long lastModified = loadedImage.getLastModified(); if (lastModified != imageResource.lastModified()) { // キャッシュされているが、すでに古い場合は破棄する. loadedImage = null; } } if (loadedImage == null) { loadedImage = super.load(imageResource); caches.set(key, loadedImage); caches.unlockImages(); // 即時解放許可 } return loadedImage; } } } final class ImageResourceCacheKey { private final ImageResource imageResource; private final int hashCode; public ImageResourceCacheKey(ImageResource imageResource) { if (imageResource == null) { throw new IllegalArgumentException(); } this.imageResource = imageResource; this.hashCode = imageResource.hashCode(); } @Override public int hashCode() { return this.hashCode; } @Override public boolean equals(Object obj) { if (obj == this) { return true; } if (obj != null && obj instanceof ImageResourceCacheKey) { ImageResourceCacheKey other = (ImageResourceCacheKey) obj; return imageResource.equals(other.imageResource); } return false; } } charactermanaj/src/charactermanaj/graphics/io/FileImageResource.java0000644000175000017500000000303211767047532026101 0ustar paulliupaulliupackage charactermanaj.graphics.io; import java.io.BufferedInputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.Serializable; import java.net.URI; /** * ファイルシステム上にある画像リソースを示す. * @author seraphy */ public class FileImageResource implements ImageResource, Serializable { /** * シリアライズバージョン */ private static final long serialVersionUID = 5397113740824387869L; /** * ファイル */ private File file; public FileImageResource(File file) { if (file == null) { throw new IllegalArgumentException(); } this.file = file; } public long lastModified() { return file.lastModified(); } public InputStream openStream() throws IOException { return new BufferedInputStream(new FileInputStream(file)); } @Override public int hashCode() { return file.hashCode(); } public int compareTo(ImageResource o) { return getFullName().compareTo(o.getFullName()); } @Override public boolean equals(Object obj) { if (obj == this) { return true; } if (obj != null && obj instanceof FileImageResource) { FileImageResource o = (FileImageResource) obj; return file.equals(o.file); } return false; } public String getFullName() { return file.getPath(); } public URI getURI() { return file.toURI(); } @Override public String toString() { return file.toString(); } } charactermanaj/src/charactermanaj/graphics/io/PNGFileImageHeaderReader.java0000644000175000017500000001131511767047532027175 0ustar paulliupaulliupackage charactermanaj.graphics.io; import java.io.BufferedInputStream; import java.io.DataInputStream; import java.io.EOFException; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.net.URI; import java.net.URL; import java.util.Arrays; /** * PNGファイルのヘッダ情報を読み取る * @author seraphy */ public class PNGFileImageHeaderReader { /** * PNGファイルのファイルヘッダ */ public static final byte[] FILE_HEADER = { (byte) 0x89, (byte) 0x50, (byte) 0x4e, (byte) 0x47, (byte) 0x0d, (byte) 0x0a, (byte) 0x1a, (byte) 0x0a }; private static final PNGFileImageHeaderReader singletion = new PNGFileImageHeaderReader(); private PNGFileImageHeaderReader() { super(); } public static PNGFileImageHeaderReader getInstance() { return singletion; } /** * URLを指定してヘッダ情報を取得する.
* @param url * @return ヘッダ、PNGでない場合はnull * @throws IOException 読み取りに失敗した場合 */ public PNGFileImageHeader readHeader(URI uri) throws IOException { if (uri == null) { throw new IllegalArgumentException(); } URL url = uri.toURL(); InputStream is = url.openStream(); try { return readHeader(is); } finally { is.close(); } } /** * ファイルを指定してヘッダ情報を取得する. * @param file * @return ヘッダ、PNGでない場合はnull * @throws IOException 読み取りに失敗した場合 */ public PNGFileImageHeader readHeader(File file) throws IOException { if (file == null) { throw new IllegalArgumentException(); } InputStream is = new BufferedInputStream(new FileInputStream(file)); try { return readHeader(is); } finally { is.close(); } } /** * ストリームを指定してヘッダ情報を読み取る.
* ストリームは読み取った分だけ消費された状態で返される.
* @param is ストリーム * @return ヘッダ、PNGでない場足はnull * @throws IOException 読み取りに失敗した場合 */ public PNGFileImageHeader readHeader(InputStream is) throws IOException { if (is == null) { throw new IllegalArgumentException(); } DataInputStream dis = new DataInputStream(is); try { // ファイルヘッダの読み取り byte[] fileHeader = new byte[FILE_HEADER.length]; dis.readFully(fileHeader); if (!Arrays.equals(fileHeader, FILE_HEADER)) { // ヘッダが一致しない return null; } PNGFileImageHeader imageHeader = null; boolean hasTransparencyInfomation = false; for (;;) { int chunkLen = dis.readInt(); // チャンクの長さ byte[] chunkType = new byte[4]; dis.readFully(chunkType); if (Arrays.equals(chunkType, "IHDR".getBytes())) { imageHeader = new PNGFileImageHeader(); imageHeader.setWidth(dis.readInt()); // 4bytes imageHeader.setHeight(dis.readInt()); // 4bytes imageHeader.setBitDepth(((int) dis.readByte()) & 0xff); // 1byte imageHeader.setColorType(((int) dis.readByte()) & 0xff); // 1byte imageHeader.setCompressionMethod(((int) dis.readByte()) & 0xff); // 1byte imageHeader.setFilterMethod(((int) dis.readByte()) & 0xff); // 1byte imageHeader.setInterlaceMethod(((int) dis.readByte()) & 0xff); // 1byte int zan = chunkLen - 13; if (zan < 0) { throw new EOFException("IHDR too short"); } if (dis.skipBytes(zan) != zan) { throw new IOException("チャンクのサイズが不正です."); } } else if (Arrays.equals(chunkType, "tRNS".getBytes())) { // カラータイプによりチャンクの中身の形式は異なる. // インデックス(ColorType=3)の場合は透過色のインデックス // グレースケールの場合は、透過色とするスケール値など. hasTransparencyInfomation = chunkLen > 0; if (dis.skipBytes(chunkLen) != chunkLen) { throw new IOException("チャンクのサイズが不正です."); } } else if (Arrays.equals(chunkType, "IEND".getBytes())) { // 終了チャンク break; } else { // IHDR以外のチャンクは読み飛ばす if (dis.skipBytes(chunkLen) != chunkLen) { throw new IOException("チャンクのサイズが不正です."); } } dis.readInt(); // CRC32を読み飛ばす } if (imageHeader != null) { imageHeader.setTransparencyInformation(hasTransparencyInfomation); } return imageHeader; } catch (EOFException e) { // 何もしない } return null; } } charactermanaj/src/charactermanaj/graphics/io/ImageLoader.java0000644000175000017500000000103111767047532024715 0ustar paulliupaulliupackage charactermanaj.graphics.io; import java.io.IOException; /** * 画像をロードします.
* @author seraphy */ public interface ImageLoader { /** * 画像リソースからBufferedImageを返します.
* 返される形式はARGBに変換されています.
* @param imageResource 画像リソース * @throws IOException 読み取りに失敗した場合、もしくは画像の形式が不明な場合 */ LoadedImage load(ImageResource imageResource) throws IOException; } charactermanaj/src/charactermanaj/graphics/io/ImagePreviewFileChooser.java0000644000175000017500000000633711767047532027271 0ustar paulliupaulliupackage charactermanaj.graphics.io; import java.awt.Image; import java.awt.Toolkit; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.io.File; import javax.swing.BorderFactory; import javax.swing.JFileChooser; import javax.swing.filechooser.FileFilter; import charactermanaj.Main; import charactermanaj.ui.MiniPictureBox; /** * 画像ファイルを選択するファイルチューザ.
* 選択したファイルの画像のサムネイルを表示する. * @author seraphy */ public class ImagePreviewFileChooser extends JFileChooser { private static final long serialVersionUID = -66951985128705674L; /*** * プレビューパネル */ private MiniPictureBox previewPanel; /** * デフォルトコンストラクタ */ public ImagePreviewFileChooser() { super(); initAccessory(); initFileFilter(); } /** * 初期ディレクトり指定コンストラクタ * @param initDir 初期ディレクトリ */ public ImagePreviewFileChooser(File initDir) { super(initDir); initAccessory(); initFileFilter(); } protected Image getSelectedImage() { return previewPanel.getImage(); } protected void setSelectedImage(Image selectedImage) { previewPanel.setImage(selectedImage); } /** * イメージをロードする.
* ファィルパスがnullまたはファイルを示さないか実在しない場合はnull.
* 既定ではイメージは非同期読み込みとなる.
* @param file ファイルパス * @return イメージ、もしくはnull */ protected Image loadImage(File file) { if (file == null || !file.exists() || !file.isFile()) { return null; } Toolkit tk = Toolkit.getDefaultToolkit(); return tk.createImage(file.getPath()); } protected void initAccessory() { previewPanel = createAccessory(); previewPanel.setVisible(true); setAccessory(previewPanel); addPropertyChangeListener( JFileChooser.SELECTED_FILE_CHANGED_PROPERTY, new PropertyChangeListener() { public void propertyChange(PropertyChangeEvent evt) { previewPanel.setImage(loadImage(getSelectedFile())); } }); } protected MiniPictureBox createAccessory() { MiniPictureBox pictureBox = new MiniPictureBox(); pictureBox.setBorder(BorderFactory.createCompoundBorder( BorderFactory.createEmptyBorder( 0, Main.isLinuxOrMacOSX() ? 5 : 10, 0, Main.isLinuxOrMacOSX() ? 5 : 0), pictureBox.getBorder())); return pictureBox; } protected void initFileFilter() { setAcceptAllFileFilterUsed(false); setFileFilter(createImageFileFilter()); } protected FileFilter createImageFileFilter() { return new FileFilter() { private final String[] acceptExts = {".png", ".jpeg", ".jpg", ".gif", ".bmp"}; @Override public boolean accept(File f) { if (f.isDirectory()) { return true; } String lcName = f.getName().toLowerCase(); for (String acceptExt : acceptExts) { if (lcName.endsWith(acceptExt)) { return true; } } return false; } public String getDescription() { return "Picture(*.jpeg;*.png;*.gif;*.bmp)"; }; }; } } charactermanaj/src/charactermanaj/graphics/io/ImageResource.java0000644000175000017500000000212011767047532025276 0ustar paulliupaulliupackage charactermanaj.graphics.io; import java.io.IOException; import java.io.InputStream; import java.net.URI; /** * 画像リソース * @author seraphy */ public interface ImageResource extends Comparable { /** * 画像リソースをストリームで取得します. * @return 入力ストリーム * @throws IOException 開けなかった場合 */ InputStream openStream() throws IOException; /** * 更新日時 * @return 更新日時を示すエポックタイム */ long lastModified(); /** * 同値用ハッシュ * @return ハッシュ */ int hashCode(); /** * 同値判定 * @param obj 比較対象 * @return 同一であればtrue */ boolean equals(Object obj); /** * ソート用比較 */ int compareTo(ImageResource o); /** * リソース位置を示すフルネーム * @return リソース位置を示すフルネーム */ String getFullName(); /** * リソース位置を示すURI * @return リソース位置を示すURI */ URI getURI(); } charactermanaj/src/charactermanaj/graphics/io/ImageCache.java0000644000175000017500000000613511767047532024524 0ustar paulliupaulliupackage charactermanaj.graphics.io; import java.awt.image.BufferedImage; import java.lang.ref.Reference; import java.lang.ref.ReferenceQueue; import java.lang.ref.SoftReference; import java.util.HashMap; import java.util.logging.Level; import java.util.logging.Logger; /** * 画像のキャッシュ.
* キャッシュは自動的にガベージコレクタにより回収されます.
* ただし、{@link #unlockImages()}が呼び出されるまで、{@link #set(Object, BufferedImage)}されたイメージは * ガベージコレクトの対象にはなりません。 * @author seraphy * * @param */ public class ImageCache { private static final Logger logger = Logger.getLogger(ImageCache.class.getName()); private HashMap> lockedImages = new HashMap>(); private ReferenceQueue queue = new ReferenceQueue(); private HashMap> caches = new HashMap>(); public LoadedImage get(K key) { if (key == null) { return null; } synchronized (caches) { BufferedImageWithKeyReference ref = caches.get(key); LoadedImage img = null; if (ref != null) { img = ref.get(); } sweep(); return img; } } public void set(K key, LoadedImage img) { if (key == null) { return; } synchronized (caches) { if (img == null) { if (logger.isLoggable(Level.FINE)) { logger.log(Level.FINE, "remove cache: " + key); } caches.remove(key); } else { BufferedImageWithKeyReference cacheData = new BufferedImageWithKeyReference(key, img, queue); lockedImages.put(key, cacheData); caches.put(key, cacheData); } sweep(); } } public void unlockImages() { synchronized (caches) { lockedImages.clear(); sweep(); } } public void sweep() { synchronized (caches) { // ガベージコレクト済みアイテムを除去する Reference ref = null; boolean removed = false; while ((ref = queue.poll()) != null) { @SuppressWarnings("unchecked") K key = ((BufferedImageWithKeyReference) ref).getKey(); if (key != null) { if (caches.get(key).get() == null) { if (logger.isLoggable(Level.FINE)) { logger.log(Level.FINE, "removed cache: " + key); } removed = true; caches.remove(key); } } } if (removed) { if (logger.isLoggable(Level.FINE)) { logger.log(Level.FINE, "cache[" + Integer.toHexString(this.hashCode()) + "] size:" + caches.size()); } } } } } /** * キー情報つきSoftReference * @author seraphy * * @param キー */ class BufferedImageWithKeyReference extends SoftReference { private final K key; public BufferedImageWithKeyReference(K key, LoadedImage img, ReferenceQueue queue) { super(img, queue); this.key = key; } public K getKey() { return key; } } charactermanaj/src/charactermanaj/graphics/ImageBuilder.java0000644000175000017500000004521411767047532024501 0ustar paulliupaulliupackage charactermanaj.graphics; import java.awt.Color; import java.awt.Dimension; import java.awt.Graphics2D; import java.awt.Rectangle; import java.awt.RenderingHints; import java.awt.geom.AffineTransform; import java.awt.image.AffineTransformOp; import java.awt.image.BufferedImage; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; import charactermanaj.graphics.filters.ColorConvertParameter; import charactermanaj.graphics.io.ImageResource; import charactermanaj.graphics.io.LoadedImage; import charactermanaj.model.AppConfig; import charactermanaj.model.Layer; /** * 各パーツの各レイヤーごとの画像を色変換したのちレイヤーの順序に従い重ね合わせ合成する。 * @author seraphy */ public class ImageBuilder { /** * ロガー */ private static final Logger logger = Logger.getLogger(ImageBuilder.class.getName()); /** * 各パーツ情報の読み取りタイムアウト */ private static final int MAX_TIMEOUT = 20; // Secs /** * 各パーツ情報を設定するためのインターフェイス.
* パーツ登録が完了したら、{@link #setComplite()}を呼び出す必要がある.
* @author seraphy * */ public interface ImageSourceCollector { /** * 画像サイズを設定する.
* @param size サイズ */ void setSize(Dimension size); /** * 画像の背景色を設定する.
* 画像生成処理そのものは背景色を使用しないが、画像の生成完了と同じタイミングで背景色を変えるために * ホルダーとして用いることを想定している.
* @param color */ void setImageBgColor(Color color); /** * アフィン変換処理のためのパラメータを指定する.
* 配列サイズは4または6でなければならない.
* @param param パラメータ、変換しない場合はnull */ void setAffineTramsform(double[] param); /** * 各パーツを登録する.
* 複数パーツある場合は、これを繰り返し呼び出す.
* すべて呼び出したらsetCompliteを呼び出す.
* @param layer レイヤー * @param imageResource イメージソース * @param param 色変換情報 */ void setImageSource(Layer layer, ImageResource imageResource, ColorConvertParameter param); /** * パーツの登録が完了したことを通知する。 */ void setComplite(); } /** * 合成が完了した画像を通知するインターフェイス * @author seraphy */ public interface ImageOutput { /** * 画像の背景色を取得する. * @return 背景色 */ Color getImageBgColor(); /** * 画像を取得する. * @return 画像 */ BufferedImage getImageOutput(); } /** * イメージを構築するためのジョブ定義.
* イメージを構築するためのパーツを登録するハンドラと、合成されたイメージを取り出すハンドラ、および * エラーハンドラからなる.
* @author seraphy */ public interface ImageBuildJob { /** * 合成する、各パーツを登録するためのハンドラ.
* @param collector 登録するためのインターフェイス */ void loadParts(ImageSourceCollector collector) throws IOException; /** * 合成されたイメージを取得するハンドラ * @param output イメージを取得するためのインターフェイス */ void buildImage(ImageOutput output); /** * 例外ハンドラ * @param ex 例外 */ void handleException(Throwable ex); } /** * イメージ構築に使用したパーツ情報 * @author seraphy */ private static final class BuildedPartsInfo { private final ImageBuildPartsInfo partsInfo; private final long lastModified; public BuildedPartsInfo(ImageBuildPartsInfo partsInfo, LoadedImage loadedImage) { this.partsInfo = partsInfo; this.lastModified = loadedImage.getLastModified(); } public ImageBuildPartsInfo getPartsInfo() { return partsInfo; } public long getLastModified() { return lastModified; } } /** * イメージ構築用情報.
* イメージ構築結果も格納される.
* @author seraphy */ private static final class ImageBuildInfo { private ArrayList partsInfos = new ArrayList(); private ArrayList buildPartsInfos = new ArrayList(); private BufferedImage canvas; private Rectangle rct = new Rectangle(0, 0, 0, 0); private Color imageBgColor; private double[] affineParamHolder; private boolean sorted; @Override public int hashCode() { return partsInfos.hashCode(); } @Override public boolean equals(Object obj) { if (obj != null && obj instanceof ImageBuildInfo) { ImageBuildInfo other = (ImageBuildInfo) obj; if (!getPartsInfos().equals(other.getPartsInfos())) { // パーツ情報を重ね順をあわせて比較している. return false; } if (!rct.equals(other.rct)) { return false; } if (!(imageBgColor == null ? other.imageBgColor == null : imageBgColor.equals(other.imageBgColor))) { return false; } if (!(affineParamHolder == null ? other.affineParamHolder == null : Arrays.equals(affineParamHolder, other.affineParamHolder))) { return false; } return true; } return false; } /** * このイメージ構築情報と、すでに構築したイメージ情報を比較し、 * 同一であるか判定する.
* 引数に指定したイメージ構築情報が、まだ構築されていない場合は常にfalseとなる.
* イメージリソースが更新されていれば同一構成であってもfalseとなる.
* @param usedInfo すでに構築済みのイメージ情報(結果が入っているもの) * @return 同一であればtrue、そうでなければfalse */ public boolean isAlreadyLoaded(ImageBuildInfo usedInfo) { if (usedInfo == null || usedInfo.getCanvas() == null) { return false; } if ( !usedInfo.equals(this)) { // 構成が違うのでfalse return false; } // 要求されているパーツ情報と、読み込み済みのパーツ情報が同一であるか判定する. int mx = partsInfos.size(); int mxUsed = usedInfo.buildPartsInfos.size(); if (mx != mxUsed) { return false; // 念のため } for (int idx = 0; idx < mx; idx++) { ImageBuildPartsInfo partsInfo = partsInfos.get(idx); BuildedPartsInfo buildedPartsInfo = usedInfo.buildPartsInfos.get(idx); if ( !partsInfo.equals(buildedPartsInfo.getPartsInfo())) { // パーツ構成が一致しない.(念のため) return false; } long lastModified = partsInfo.getFile().lastModified(); if (lastModified != buildedPartsInfo.getLastModified()) { // 画像ファイルが更新されている. return false; } } return true; } /** * イメージ構築に使用したパーツ情報を記録する. * @param partsInfo パーツ情報 * @param loadedImage イメージ */ public void addUsedPartsInfo(ImageBuildPartsInfo partsInfo, LoadedImage loadedImage) { buildPartsInfos.add(new BuildedPartsInfo(partsInfo, loadedImage)); } /** * イメージ構築結果を取得する. * @return イメージ構築結果 */ public BufferedImage getCanvas() { return canvas; } /** * イメージ構築結果を格納する. * @param canvas イメージ構築結果 */ public void setCanvas(BufferedImage canvas) { this.canvas = canvas; } public double[] getAffineParamHolder() { return affineParamHolder; } public void setAffineParamHolder(double[] affineParamHolder) { this.affineParamHolder = affineParamHolder; } public Color getImageBgColor() { return imageBgColor; } public void setImageBgColor(Color imageBgColor) { this.imageBgColor = imageBgColor; } public Rectangle getRct() { return rct; } public void setRect(int w, int h) { rct.width = w; rct.height = h; } /** * パーツのリストを取得する.
* 取得された時点で、パーツ情報は重ね合わせ順にソートされている.
* リストは変更不可です.
* @return パーツ情報のリスト */ public List getPartsInfos() { if ( !sorted) { Collections.sort(partsInfos); sorted = true; } return Collections.unmodifiableList(partsInfos); } public void add(ImageBuildPartsInfo imageBuildPartsInfo) { sorted = false; partsInfos.add(imageBuildPartsInfo); } public int getPartsCount() { return partsInfos.size(); } } /** * イメージのローダー */ private ColorConvertedImageCachedLoader imageLoader; /** * 最後に使用したイメージビルド情報.(初回ならばnull) */ private ImageBuildInfo lastUsedImageBuildInfo; /** * イメージのローダーを指定して構築します.
* @param imageLoader イメージローダー */ public ImageBuilder(ColorConvertedImageCachedLoader imageLoader) { if (imageLoader == null) { throw new IllegalArgumentException(); } this.imageLoader = imageLoader; } /** * イメージビルドジョブより、構築すべきイメージの情報を取得する. * @param imageBuildJob イメージビルドジョブ * @return 取得されたイメージ構築情報 * @throws IOException 失敗 * @throws InterruptedException 割り込み */ protected ImageBuildInfo getPartsInfo(ImageBuildJob imageBuildJob) throws IOException, InterruptedException { final ImageBuildInfo imageBuildInfo = new ImageBuildInfo(); final Semaphore compliteLock = new Semaphore(0); // ジョブリクエスト側に合成するイメージの情報を引き渡すように要求する. // loadPartsが非同期に行われる場合、すぐに制御を戻す. imageBuildJob.loadParts(new ImageSourceCollector() { // ジョブリクエスト側よりイメージサイズの設定として呼び出される public void setSize(Dimension size) { synchronized (imageBuildInfo) { imageBuildInfo.setRect(size.width, size.height); } } public void setImageBgColor(Color color) { synchronized (imageBuildInfo) { imageBuildInfo.setImageBgColor(color); } } public void setAffineTramsform(double[] param) { if (param != null && !(param.length == 4 || param.length == 6)) { throw new IllegalArgumentException("affineTransformParameter invalid length."); } synchronized (imageBuildInfo) { imageBuildInfo.setAffineParamHolder(param); } } // ジョブリクエスト側よりパーツの登録として呼び出される public void setImageSource(Layer layer, ImageResource imageResource, ColorConvertParameter param) { synchronized (imageBuildInfo) { imageBuildInfo.add(new ImageBuildPartsInfo( imageBuildInfo.getPartsCount(), layer, imageResource, param)); } } // ジョブリクエスト側よりイメージサイズとパーツの登録が完了したことを通知される. public void setComplite() { compliteLock.release(); } }); // ImageCollectorは非同期に呼び出されても良いことを想定している. // MAX_TIMEOUTを経過してもsetCompliteが呼び出されない場合、処理を放棄する. if (!compliteLock.tryAcquire(MAX_TIMEOUT, TimeUnit.SECONDS)) { throw new RuntimeException("ImageCollector Timeout."); } return imageBuildInfo; } /** * イメージビルド情報をもとにイメージを構築して返す. * @param imageBuildInfo イメージビルド情報と、その結果 * @throws IOException 失敗 */ protected void buildImage(ImageBuildInfo imageBuildInfo) throws IOException { // 出力画像のカンバスを作成 int w = imageBuildInfo.getRct().width; int h = imageBuildInfo.getRct().height; final BufferedImage canvas = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB); Graphics2D g = (Graphics2D) canvas.getGraphics(); try { // レンダリングヒント AppConfig appConfig = AppConfig.getInstance(); if (appConfig.isEnableRenderingHints()) { g.setRenderingHint( RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY); g.setRenderingHint( RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY); g.setRenderingHint( RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); } // 各パーツを重ね合わせ順にカンバスに描画する imageLoader.unlockImages(); boolean occureOutOfMemory = false; for (ImageBuildPartsInfo partsInfo : imageBuildInfo.getPartsInfos()) { ImageResource imageFile = partsInfo.getFile(); ColorConvertParameter colorConvParam = partsInfo.getColorParam(); for (;;) { try { if (occureOutOfMemory) { // 一度、OutOfMemoryエラーが発生した場合はキャッシュは常に // gc可能な状態に強制する。 imageLoader.unlockImages(); } LoadedImage loadedImage = imageLoader.load(imageFile, colorConvParam); // イメージ構築に使用した各パーツの結果を格納する. imageBuildInfo.addUsedPartsInfo(partsInfo, loadedImage); // イメージをキャンバスに重ねる. BufferedImage img = loadedImage.getImage(); g.drawImage(img, 0, 0, w, h, 0, 0, w, h, null); break; } catch (OutOfMemoryError ex) { if (occureOutOfMemory) { // すでに一度発生している場合は処理せずエラーとする. throw ex; } occureOutOfMemory = true; logger.log(Level.WARNING, "Out Of Memory!!", ex); } } } } finally { g.dispose(); } // アフィン処理を行う.(パラメータが指定されていれば) final BufferedImage affineTransformedCanvas; double[] affineTransformParameter = imageBuildInfo.getAffineParamHolder(); if (affineTransformParameter == null || affineTransformParameter.length != 6) { affineTransformedCanvas = canvas; } else { AffineTransform affineTransform = new AffineTransform(affineTransformParameter); AffineTransformOp affineTransformOp = new AffineTransformOp(affineTransform, AffineTransformOp.TYPE_BILINEAR); affineTransformedCanvas = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB); affineTransformOp.filter(canvas, affineTransformedCanvas); } // 最終的にできあがったキャンバスを結果として格納する. imageBuildInfo.setCanvas(affineTransformedCanvas); } /** * イメージ構築ジョブを要求します.
* 戻り値がtrueである場合は、ただちに完了したことを示します.
* @param imageBuildJob イメージを構築するジョブ * @return 画像がただちに得られた場合はtrue、そうでなければfalse */ public boolean requestJob(final ImageBuildJob imageBuildJob) { if (imageBuildJob == null) { throw new IllegalArgumentException(); } // 合成する画像パーツの取得処理 final ImageBuildInfo imageBuildInfo; try { imageBuildInfo = getPartsInfo(imageBuildJob); } catch (Throwable ex) { // 予期せぬ例外の通知 imageBuildJob.handleException(ex); return false; } try { synchronized (imageBuildInfo) { final BufferedImage canvas; // 前回構築したパーツと同じであれば再構築せず、以前のものを使う if (imageBuildInfo.isAlreadyLoaded(lastUsedImageBuildInfo)) { canvas = lastUsedImageBuildInfo.getCanvas(); } else { // パーツの合成処理 buildImage(imageBuildInfo); canvas = imageBuildInfo.getCanvas(); lastUsedImageBuildInfo = imageBuildInfo; } // 完成したカンバスを合成結果として通知する. imageBuildJob.buildImage(new ImageOutput() { public BufferedImage getImageOutput() { return canvas; } public Color getImageBgColor() { return imageBuildInfo.getImageBgColor(); } }); } } catch (Throwable ex) { // 予期せぬ例外の通知 imageBuildJob.handleException(ex); return false; } // 完了 return true; } } /** * 合成する個々のイメージ情報 .
* レイヤー順に順序づけられており、同一レイヤーであればOrder順に順序づけられます.
* @author seraphy */ final class ImageBuildPartsInfo implements Comparable { private int order; private Layer layer; private ImageResource imageResource; private ColorConvertParameter colorParam; public ImageBuildPartsInfo(int order, Layer layer, ImageResource imageResource, ColorConvertParameter colorParam) { this.order = order; this.layer = layer; this.imageResource = imageResource; this.colorParam = colorParam; } @Override public int hashCode() { return order ^ layer.hashCode() ^ imageResource.hashCode(); } @Override public boolean equals(Object obj) { if (obj == this) { return true; } if (obj != null && obj instanceof ImageBuildPartsInfo) { ImageBuildPartsInfo o = (ImageBuildPartsInfo) obj; return order == o.order && layer.equals(o.layer) && imageResource.equals(o.imageResource) && colorParam.equals(o.colorParam); } return false; } public int compareTo(ImageBuildPartsInfo o) { // レイヤー順 int ret = layer.compareTo(o.layer); if (ret == 0) { // 同一レイヤーであれば定義順 ret = order - o.order; } if (ret == 0) { // それでも同じであればイメージソースの固有の順序 ret = imageResource.compareTo(o.imageResource); } return ret; } public int getOrder() { return order; } public Layer getLayer() { return layer; } public ColorConvertParameter getColorParam() { return colorParam; } public ImageResource getFile() { return imageResource; } } charactermanaj/src/charactermanaj/ui/0000755000175000017500000000000011767047542020115 5ustar paulliupaulliucharactermanaj/src/charactermanaj/ui/MiniPictureBox.java0000644000175000017500000000664511767047542023674 0ustar paulliupaulliupackage charactermanaj.ui; import java.awt.Dimension; import java.awt.FontMetrics; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Image; import java.awt.Insets; import java.awt.RenderingHints; import java.awt.geom.Rectangle2D; import java.awt.image.ImageObserver; import javax.swing.BorderFactory; import javax.swing.JPanel; import javax.swing.border.BevelBorder; /** * サンプルイメージを表示する小さなピクチャボックス.
* 非同期イメージに対応している.
* @author seraphy */ public class MiniPictureBox extends JPanel { private static final long serialVersionUID = 3210952907784110605L; /** * 表示するイメージ.
* 非同期読み込みのイメージにも対応.
*/ private Image image; /** * 読み込みエラーが発生しているか? */ private boolean errorOccured; /** * コンストラクタ */ public MiniPictureBox() { setBorder(BorderFactory.createBevelBorder(BevelBorder.LOWERED)); setPreferredSize(new Dimension(150, 300)); } @Override protected void paintComponent(Graphics g0) { Graphics2D g = (Graphics2D) g0; super.paintComponent(g); Image img = getImage(); if (img != null) { if (errorOccured) { FontMetrics fm = g.getFontMetrics(); String message = "ERROR"; Rectangle2D rct = fm.getStringBounds(message, g); Insets insets = getInsets(); g.drawString(message, insets.left, insets.top + (int) rct.getHeight()); } else { // 画像の読み込みに失敗しているか? checkImage(img, -1, -1, new ImageObserver() { public boolean imageUpdate(Image img, int infoflags, int x, int y, int width, int height) { if ((infoflags & (ImageObserver.ERROR)) != 0) { errorOccured = true; repaint(); } return true; } }); if ( !prepareImage(img, this)) { // まだロードできていない場合は // ロードできるまで表示しない. return; } // イメージサイズの取得 int imgW = img.getWidth(this); int imgH = img.getHeight(this); // 表示エリア int x = 0; int y = 0; int w = getWidth(); int h = getHeight(); Insets insets = getInsets(); if (insets != null) { x = insets.left; y = insets.top; w -= insets.left + insets.right; h -= insets.top + insets.bottom; } // 倍率算定 double vx = (double) w / (double) imgW; double vy = (double) h / (double) imgH; double factor = Math.min(vx, vy); int scaledW = (int)(imgW * factor); int scaledH = (int)(imgH * factor); int offset_x = (w - scaledW) / 2; int offset_y = (h - scaledH) / 2; // 描画 g.setRenderingHint( RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); g.drawImage(img, x + offset_x, y + offset_y, x + offset_x + scaledW, y + offset_y + scaledH, 0, 0, imgW, imgH, this); } } } public Image getImage() { return image; } public void setImage(Image image) { Image oldimg = this.image; if ((oldimg != null && image == null) || (image != null && (oldimg == null || !oldimg.equals(image)))) { this.image = image; this.errorOccured = false; repaint(); firePropertyChange("image", oldimg, image); } } }charactermanaj/src/charactermanaj/ui/InformationDialog.java0000644000175000017500000004175111767047542024375 0ustar paulliupaulliupackage charactermanaj.ui; import static java.lang.Math.max; import java.awt.BorderLayout; import java.awt.Component; import java.awt.Container; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.Insets; import java.awt.Toolkit; import java.awt.datatransfer.Clipboard; import java.awt.datatransfer.StringSelection; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.KeyEvent; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.io.File; import java.io.IOException; import java.io.PrintWriter; import java.io.StringWriter; import java.net.URI; import java.util.Collections; import java.util.Properties; import java.util.logging.Level; import java.util.logging.Logger; import javax.swing.AbstractAction; import javax.swing.AbstractCellEditor; import javax.swing.ActionMap; import javax.swing.BorderFactory; import javax.swing.Box; import javax.swing.InputMap; import javax.swing.JButton; import javax.swing.JComponent; import javax.swing.JDialog; import javax.swing.JFrame; import javax.swing.JPanel; import javax.swing.JPopupMenu; import javax.swing.JRootPane; import javax.swing.JScrollPane; import javax.swing.JTable; import javax.swing.KeyStroke; import javax.swing.table.DefaultTableCellRenderer; import javax.swing.table.TableCellEditor; import javax.swing.table.TableCellRenderer; import javax.swing.table.TableColumn; import javax.swing.table.TableColumnModel; import charactermanaj.graphics.filters.ColorConvertParameter; import charactermanaj.graphics.io.ImageResource; import charactermanaj.graphics.io.PNGFileImageHeader; import charactermanaj.graphics.io.PNGFileImageHeaderReader; import charactermanaj.model.AppConfig; import charactermanaj.model.Layer; import charactermanaj.model.PartsIdentifier; import charactermanaj.model.PartsSet; import charactermanaj.model.PartsSpecResolver; import charactermanaj.model.io.PartsImageCollectionParser; import charactermanaj.model.io.PartsImageCollectionParser.PartsImageCollectionHandler; import charactermanaj.ui.model.AbstractTableModelWithComboBoxModel; import charactermanaj.util.DesktopUtilities; import charactermanaj.util.ErrorMessageHelper; import charactermanaj.util.LocalizedResourcePropertyLoader; /** * 情報ダイアログを開く * @author seraphy */ public class InformationDialog extends JDialog { private static final long serialVersionUID = 1L; private static final Logger logger = Logger.getLogger(InformationDialog.class.getName()); protected static final String STRINGS_RESOURCE = "languages/informationdialog"; private JTable informationTable; private InformationTableModel informationTableModel; private boolean modeOpen; public InformationDialog(JFrame parent, PartsSpecResolver partsSpecResolver, PartsSet partsSet) { super(parent, true); AppConfig appConfig = AppConfig.getInstance(); modeOpen = appConfig.isInformationDialogOpenMethod(); if (partsSpecResolver == null) { throw new IllegalArgumentException("partsSpecResolver is null"); } if (partsSet == null) { throw new IllegalArgumentException("partsSet is null"); } setDefaultCloseOperation(JDialog.DO_NOTHING_ON_CLOSE); addWindowListener(new WindowAdapter() { @Override public void windowClosing(WindowEvent e) { onClose(); } }); final Properties strings = LocalizedResourcePropertyLoader .getInstance().getLocalizedProperties(STRINGS_RESOURCE); setTitle(strings.getProperty("title")); informationTableModel = new InformationTableModel(); final PNGFileImageHeaderReader pngHeaderReader = PNGFileImageHeaderReader.getInstance(); PartsImageCollectionParser parser = new PartsImageCollectionParser(partsSpecResolver); parser.parse(partsSet, new PartsImageCollectionHandler() { public void detectImageSource(PartsIdentifier partsIdentifier, Layer layer, final ImageResource imageResource, ColorConvertParameter param) { AbstractAction act = new AbstractAction(strings.getProperty(modeOpen ? "btn.edit.open" : "btn.edit.edit")) { private static final long serialVersionUID = 1L; public void actionPerformed(ActionEvent e) { onOpen(imageResource); } }; URI uri = imageResource.getURI(); if (uri != null && "file".equals(uri.getScheme()) && DesktopUtilities.isSupported()) { act.setEnabled(true); } else { act.setEnabled(false); } PNGFileImageHeader pngHeader; try { pngHeader = pngHeaderReader.readHeader(uri); } catch (IOException ex) { logger.log(Level.WARNING, "PNG Header loading error.: " + uri, ex); pngHeader = null; } InformationModel information = new InformationModel(partsIdentifier, layer, imageResource, param, pngHeader, act); informationTableModel.addRow(information); } }); informationTableModel.sort(); informationTable = new JTable(informationTableModel) { private static final long serialVersionUID = 1L; // セルの幅を大きいものにあわせる public Component prepareRenderer(final TableCellRenderer renderer, final int row, final int column) { final Component prepareRenderer = super.prepareRenderer(renderer, row, column); final TableColumn tableColumn = getColumnModel().getColumn(column); int preferredWidth = max(prepareRenderer .getPreferredSize().width, tableColumn .getPreferredWidth()); // セルかヘッダのどちらか幅の大きいほう if (tableColumn.getPreferredWidth() != preferredWidth) { tableColumn.setPreferredWidth(preferredWidth); } return prepareRenderer; } }; informationTableModel.adjustColumnModel(informationTable.getColumnModel()); informationTable.setShowGrid(true); informationTable.setGridColor(appConfig.getGridColor()); informationTable.putClientProperty("terminateEditOnFocusLost", Boolean.TRUE); informationTable.setAutoResizeMode(JTable.AUTO_RESIZE_OFF); informationTable.setRowHeight(informationTable.getRowHeight() + 4); informationTable.setDefaultRenderer(JButton.class, new ButtonCellRender()); informationTable.setDefaultEditor(JButton.class, new ButtonCellEditor()); // セルデータの幅にあわせる(事前に) for (int row = 0; row < informationTable.getRowCount(); row++) { for (int col = 0; col < informationTable.getColumnCount(); col++) { TableCellRenderer renderer = informationTable.getCellRenderer(row, col); informationTable.prepareRenderer(renderer, row, col); } } final JPopupMenu popupMenu = new JPopupMenu(); popupMenu.add(new AbstractAction(strings.getProperty("popupmenu.copyPath")) { private static final long serialVersionUID = 1L; public void actionPerformed(ActionEvent e) { onCopyFilePath(); } }); informationTable.setComponentPopupMenu(popupMenu); Container contentPane = getContentPane(); contentPane.setLayout(new BorderLayout()); JScrollPane informationTableSP = new JScrollPane(informationTable); JPanel informationTableSPPabel = new JPanel(new BorderLayout()); informationTableSPPabel.setBorder(BorderFactory.createEmptyBorder(5, 5, 0, 5)); informationTableSPPabel.add(informationTableSP, BorderLayout.CENTER); contentPane.add(informationTableSPPabel, BorderLayout.CENTER); AbstractAction actClose = new AbstractAction(strings.getProperty("btnClose")) { private static final long serialVersionUID = 1L; public void actionPerformed(ActionEvent e) { onClose(); } }; JPanel btnPanel = new JPanel(); btnPanel.setBorder(BorderFactory.createEmptyBorder(3, 5, 3, 45)); GridBagLayout btnPanelLayout = new GridBagLayout(); btnPanel.setLayout(btnPanelLayout); GridBagConstraints gbc = new GridBagConstraints(); gbc.gridx = 0; gbc.gridy = 0; gbc.gridheight = 1; gbc.gridwidth = 1; gbc.anchor = GridBagConstraints.EAST; gbc.fill = GridBagConstraints.BOTH; gbc.insets = new Insets(3, 3, 3, 3); gbc.ipadx = 0; gbc.ipady = 0; gbc.weightx = 1.; gbc.weighty = 0.; btnPanel.add(Box.createHorizontalGlue(), gbc); gbc.gridx = 1; gbc.gridy = 0; gbc.weightx = 0.; gbc.weighty = 0.; JButton btnClose = new JButton(actClose); btnPanel.add(btnClose, gbc); contentPane.add(btnPanel, BorderLayout.SOUTH); Toolkit tk = Toolkit.getDefaultToolkit(); JRootPane rootPane = getRootPane(); rootPane.setDefaultButton(btnClose); InputMap im = rootPane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW); ActionMap am = rootPane.getActionMap(); im.put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE,0), "closeInformationDialog"); im.put(KeyStroke.getKeyStroke(KeyEvent.VK_W, tk.getMenuShortcutKeyMask()), "closeInformationDialog"); am.put("closeInformationDialog", actClose); pack(); setLocationRelativeTo(parent); } protected void onClose() { dispose(); } protected void onCopyFilePath() { StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw); for (int selRow : informationTable.getSelectedRows()) { InformationModel information = informationTableModel.getRow(selRow); pw.println(information.getImageResourceName()); } Toolkit tk = Toolkit.getDefaultToolkit(); String text = sw.toString(); if (text.length() == 0) { tk.beep(); return; } StringSelection textSelection = new StringSelection(sw.toString()); Clipboard cb = tk.getSystemClipboard(); cb.setContents(textSelection, null); } protected void onOpen(ImageResource imageResource) { try { URI uri = imageResource.getURI(); if (uri != null && "file".equals(uri.getScheme())) { File file = new File(uri); DesktopUtilities.open(file); } } catch (Exception ex) { ErrorMessageHelper.showErrorDialog(this, ex); } } } class InformationTableModel extends AbstractTableModelWithComboBoxModel { private static final long serialVersionUID = 1L; private static final String[] columnNames; private static final int[] columnWidths; public static final int COLUMN_BUTTON; static { final Properties strings = LocalizedResourcePropertyLoader .getInstance().getLocalizedProperties(InformationDialog.STRINGS_RESOURCE); columnNames = new String[] { strings.getProperty("column.partsName"), strings.getProperty("column.categoryName"), strings.getProperty("column.layerName"), strings.getProperty("column.layerOrder"), strings.getProperty("column.imagesize"), strings.getProperty("column.colortype"), strings.getProperty("column.imageName"), strings.getProperty("column.editbtn"), }; COLUMN_BUTTON = 7; columnWidths = new int[] { Integer.parseInt(strings.getProperty("column.partsName.width")), Integer.parseInt(strings.getProperty("column.categoryName.width")), Integer.parseInt(strings.getProperty("column.layerName.width")), Integer.parseInt(strings.getProperty("column.layerOrder.width")), Integer.parseInt(strings.getProperty("column.layerOrder.imagesize.width")), Integer.parseInt(strings.getProperty("column.layerOrder.colortype.width")), Integer.parseInt(strings.getProperty("column.imageName.width")), Integer.parseInt(strings.getProperty("column.editbtn.width")), }; } public void adjustColumnModel(TableColumnModel columnModel) { for (int idx = 0; idx < columnWidths.length; idx++) { columnModel.getColumn(idx).setPreferredWidth(columnWidths[idx]); } } public int getColumnCount() { return columnNames.length; } @Override public String getColumnName(int column) { return columnNames[column]; } @Override public Class getColumnClass(int columnIndex) { switch (columnIndex) { case 0: return String.class; case 1: return String.class; case 2: return String.class; case 3: return Integer.class; case 4: return String.class; case 5: return String.class; case 6: return String.class; case 7: return JButton.class; } return String.class; } public Object getValueAt(int rowIndex, int columnIndex) { InformationModel information = getRow(rowIndex); switch (columnIndex) { case 0: return information.getPartsName(); case 1: return information.getCategoryName(); case 2: return information.getLayerName(); case 3: return information.getLayerOrder(); case 4: return information.getImageSizeStr(); case 5: return information.getColorTypeStr(); case 6: return information.getImageResourceName(); case 7: return information.getButton(); } return ""; } public void sort() { Collections.sort(elements); fireTableDataChanged(); } @Override public boolean isCellEditable(int rowIndex, int columnIndex) { InformationModel information = getRow(rowIndex); if (columnIndex == COLUMN_BUTTON) { return information.getButton().isEnabled(); } return false; } } class InformationModel implements Comparable { private PartsIdentifier partsIdentifier; private Layer layer; private ImageResource imageResource; private JButton btnOpen; private PNGFileImageHeader pngHeader; public InformationModel(PartsIdentifier partsIdentifier, Layer layer, ImageResource imageResource, ColorConvertParameter colorConvertParameter, PNGFileImageHeader pngHeader, AbstractAction actOpen) { this.partsIdentifier = partsIdentifier; this.layer = layer; this.imageResource = imageResource; this.pngHeader = pngHeader; this.btnOpen = new JButton(actOpen) { private static final long serialVersionUID = 1L; @Override public String toString() { // JTableをクリップボードにコピーしたときに設定されるカラムの文字列表現 return "open"; } }; } @Override public int hashCode() { return partsIdentifier.hashCode(); } @Override public boolean equals(Object obj) { if (obj == this) { return true; } if (obj != null && obj instanceof InformationModel) { InformationModel o = (InformationModel) obj; return partsIdentifier.equals(o.partsIdentifier) && layer.equals(o.layer); } return false; } public int compareTo(InformationModel o) { int ret = partsIdentifier.compareTo(o.partsIdentifier); if (ret == 0) { ret = layer.compareTo(o.layer); } if (ret == 0) { ret = imageResource.compareTo(o.imageResource); } return ret; } public String getPartsName() { return this.partsIdentifier.getLocalizedPartsName(); } public String getCategoryName() { return this.partsIdentifier.getPartsCategory().getLocalizedCategoryName(); } public String getLayerName() { return this.layer.getLocalizedName(); } public int getLayerOrder() { return this.layer.getOrder(); } public String getImageResourceName() { return this.imageResource.getFullName(); } public JButton getButton() { return btnOpen; } public String getImageSizeStr() { if (pngHeader == null) { return "INVALID"; } return pngHeader.getWidth() + "x" + pngHeader.getHeight(); } public String getColorTypeStr() { if (pngHeader == null) { return "INVALID"; } StringBuilder buf = new StringBuilder(); int colorType = pngHeader.getColorType(); if ((colorType & 0x01) != 0) { buf.append("Indexed "); } if ((colorType & 0x02) != 0) { buf.append("Color "); } else { buf.append("Greyscale "); } if (colorType == 6 || pngHeader.hasTransparencyInformation()) { // 6:TrueColor または アルファ情報がある場合のみアルファ有りとする. buf.append("Alpha "); } buf.append(pngHeader.getBitDepth() + "bit"); return buf.toString().trim(); } } /** * ボタンレンダー.
* @author seraphy */ class ButtonCellRender extends DefaultTableCellRenderer { private static final long serialVersionUID = 1L; @Override public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); return (JButton) value; } } /** * ボタンエディタ.
* @author seraphy */ class ButtonCellEditor extends AbstractCellEditor implements TableCellEditor { private static final long serialVersionUID = 1L; public Component getTableCellEditorComponent(final JTable table, final Object value, final boolean isSelected, final int row, final int column) { final JButton orgBtn = (JButton) value; final JButton btn = new JButton(new AbstractAction(orgBtn.getText()) { private static final long serialVersionUID = 1L; public void actionPerformed(ActionEvent e) { fireEditingCanceled(); for (ActionListener listener : orgBtn.getActionListeners()) { listener.actionPerformed(e); } } }); return btn; } public Object getCellEditorValue() { return null; } } charactermanaj/src/charactermanaj/ui/AppConfigDialog.java0000644000175000017500000004000311767047542023743 0ustar paulliupaulliupackage charactermanaj.ui; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Component; import java.awt.Container; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.Insets; import java.awt.Toolkit; import java.awt.event.ActionEvent; import java.awt.event.KeyEvent; import java.awt.event.MouseEvent; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.io.File; import java.util.Collections; import java.util.Properties; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; import javax.swing.AbstractAction; import javax.swing.Action; import javax.swing.ActionMap; import javax.swing.BorderFactory; import javax.swing.Box; import javax.swing.InputMap; import javax.swing.JButton; import javax.swing.JCheckBox; import javax.swing.JComponent; import javax.swing.JDialog; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JRootPane; import javax.swing.JScrollPane; import javax.swing.JTable; import javax.swing.KeyStroke; import javax.swing.table.TableCellRenderer; import javax.swing.table.TableColumnModel; import charactermanaj.Main; import charactermanaj.model.AppConfig; import charactermanaj.ui.model.AbstractTableModelWithComboBoxModel; import charactermanaj.util.ConfigurationDirUtilities; import charactermanaj.util.DesktopUtilities; import charactermanaj.util.ErrorMessageHelper; import charactermanaj.util.LocalizedResourcePropertyLoader; import charactermanaj.util.SetupLocalization; /** * アプリケーション設定ダイアログ * @author seraphy */ public class AppConfigDialog extends JDialog { private static final long serialVersionUID = 1L; private static final Logger logger = Logger.getLogger(AppConfigDialog.class.getName()); private AppConfigTableModel appConfigTableModel; private JTable appConfigTable; private JCheckBox chkResetDoNotAskAgain; private RecentCharactersDir recentCharactersDir; public AppConfigDialog(JFrame parent) { super(parent, true); try { setDefaultCloseOperation(JDialog.DO_NOTHING_ON_CLOSE); addWindowListener(new WindowAdapter() { @Override public void windowClosing(WindowEvent e) { onClose(); } }); initComponent(); loadData(); } catch (RuntimeException ex) { logger.log(Level.SEVERE, "appConfig construct failed.", ex); dispose(); throw ex; } } private void initComponent() { Properties strings = LocalizedResourcePropertyLoader.getInstance() .getLocalizedProperties("languages/appconfigdialog"); setTitle(strings.getProperty("title")); Container contentPane = getContentPane(); contentPane.setLayout(new BorderLayout()); // buttons JPanel btnPanel = new JPanel(); btnPanel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 45)); GridBagLayout btnPanelLayout = new GridBagLayout(); btnPanel.setLayout(btnPanelLayout); GridBagConstraints gbc = new GridBagConstraints(); Action actApply = new AbstractAction(strings.getProperty("btn.apply")) { private static final long serialVersionUID = 1L; public void actionPerformed(ActionEvent e) { onUpdate(); } }; Action actCancel = new AbstractAction(strings.getProperty("btn.cancel")) { private static final long serialVersionUID = 1L; public void actionPerformed(ActionEvent e) { onClose(); } }; Action actLocalization = new AbstractAction(strings.getProperty("btn.setupLocalization")) { private static final long serialVersionUID = 1L; public void actionPerformed(ActionEvent e) { onSetupLocalization(); } }; chkResetDoNotAskAgain = new JCheckBox(strings.getProperty("chk.askForCharactersDir")); gbc.gridx = 0; gbc.gridy = 0; gbc.gridheight = 1; gbc.gridwidth = 3; gbc.anchor = GridBagConstraints.WEST; gbc.fill = GridBagConstraints.NONE; gbc.insets = new Insets(0, 0, 0, 0); gbc.ipadx = 0; gbc.ipady = 0; gbc.weightx = 1.; gbc.weighty = 0.; btnPanel.add(chkResetDoNotAskAgain, gbc); gbc.gridx = 0; gbc.gridy = 1; gbc.gridheight = 1; gbc.gridwidth = 3; gbc.anchor = GridBagConstraints.WEST; gbc.fill = GridBagConstraints.NONE; gbc.insets = new Insets(3, 3, 3, 3); gbc.ipadx = 0; gbc.ipady = 0; gbc.weightx = 1.; gbc.weighty = 0.; btnPanel.add(new JButton(actLocalization), gbc); gbc.gridx = 0; gbc.gridy = 2; gbc.gridheight = 1; gbc.gridwidth = 1; gbc.fill = GridBagConstraints.BOTH; gbc.weightx = 1.; gbc.weighty = 0.; btnPanel.add(Box.createHorizontalGlue(), gbc); gbc.gridx = Main.isLinuxOrMacOSX() ? 2 : 1; gbc.weightx = 0.; JButton btnApply = new JButton(actApply); btnPanel.add(btnApply, gbc); gbc.gridx = Main.isLinuxOrMacOSX() ? 1 : 2; gbc.weightx = 0.; JButton btnCancel = new JButton(actCancel); btnPanel.add(btnCancel, gbc); add(btnPanel, BorderLayout.SOUTH); setSize(350, 400); setLocationRelativeTo(getParent()); // Notes JLabel lblCaution = new JLabel(strings.getProperty("caution"), JLabel.CENTER); lblCaution.setBorder(BorderFactory.createEmptyBorder(3, 3, 3, 3)); lblCaution.setForeground(Color.red); contentPane.add(lblCaution, BorderLayout.NORTH); // Model appConfigTableModel = new AppConfigTableModel(); // JTable AppConfig appConfig = AppConfig.getInstance(); final Color invalidBgColor = appConfig.getInvalidBgColor(); appConfigTable = new JTable(appConfigTableModel) { private static final long serialVersionUID = 1L; @Override public Component prepareRenderer(TableCellRenderer renderer, int row, int column) { Component comp = super.prepareRenderer(renderer, row, column); AppConfigRowModel rowModel = appConfigTableModel.getRow(row); if (rowModel.isRejected()) { comp.setBackground(invalidBgColor); } else { if (isCellSelected(row, column)) { comp.setBackground(getSelectionBackground()); } else { comp.setBackground(getBackground()); } } return comp; } @Override public String getToolTipText(MouseEvent event) { int row = rowAtPoint(event.getPoint()); int col = columnAtPoint(event.getPoint()); if (col == 0) { AppConfigRowModel rowModel = appConfigTableModel.getRow(row); return rowModel.getDisplayName(); } return super.getToolTipText(event); } }; appConfigTable.setAutoResizeMode(JTable.AUTO_RESIZE_LAST_COLUMN); appConfigTable.setShowGrid(true); appConfigTable.setGridColor(AppConfig.getInstance().getGridColor()); appConfigTable.putClientProperty("terminateEditOnFocusLost", Boolean.TRUE); appConfigTableModel.adjustColumnModel(appConfigTable.getColumnModel()); JScrollPane appConfigTableSP = new JScrollPane(appConfigTable); appConfigTableSP.setBorder(BorderFactory.createCompoundBorder( BorderFactory.createEmptyBorder(0, 3, 0, 3), BorderFactory.createTitledBorder(strings.getProperty("table.caption"))) ); appConfigTableSP.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); contentPane.add(appConfigTableSP, BorderLayout.CENTER); // RootPane Toolkit tk = Toolkit.getDefaultToolkit(); JRootPane rootPane = getRootPane(); InputMap im = rootPane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW); ActionMap am = rootPane.getActionMap(); im.put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "closeAppConfigDialog"); im.put(KeyStroke.getKeyStroke(KeyEvent.VK_W, tk.getMenuShortcutKeyMask()), "closeAppConfigDialog"); am.put("closeAppConfigDialog", actCancel); } private void loadData() { Properties original = AppConfig.getInstance().getProperties(); appConfigTableModel.initModel(original); try { recentCharactersDir = RecentCharactersDir.load(); if (recentCharactersDir != null) { File lastUseCharactersDir = recentCharactersDir.getLastUseCharacterDir(); boolean enableLastUseCharacterDir = lastUseCharactersDir != null && lastUseCharactersDir.isDirectory(); boolean doNotAskAgain = enableLastUseCharacterDir && recentCharactersDir.isDoNotAskAgain(); chkResetDoNotAskAgain.setEnabled(enableLastUseCharacterDir); chkResetDoNotAskAgain.setSelected(!doNotAskAgain); } } catch (Exception ex) { recentCharactersDir = null; logger.log(Level.WARNING, "RecentCharactersDir load failed.", ex); } } protected void onSetupLocalization() { Properties strings = LocalizedResourcePropertyLoader.getInstance() .getLocalizedProperties("languages/appconfigdialog"); if (JOptionPane.showConfirmDialog(this, strings.getProperty("setupLocalization"), strings.getProperty("confirm.setupLocalization.caption"), JOptionPane.OK_CANCEL_OPTION, JOptionPane.WARNING_MESSAGE) != JOptionPane.OK_OPTION) { return; } try { File baseDir = ConfigurationDirUtilities.getUserDataDir(); SetupLocalization setup = new SetupLocalization(baseDir); setup.setupToLocal(); File resourceDir = setup.getResourceDir(); DesktopUtilities.open(resourceDir); } catch (Exception ex) { ErrorMessageHelper.showErrorDialog(this, ex); } } protected void onClose() { if (appConfigTableModel.isModified()) { Properties strings = LocalizedResourcePropertyLoader.getInstance() .getLocalizedProperties("languages/appconfigdialog"); if (JOptionPane.showConfirmDialog(this, strings.getProperty("confirm.close"), strings.getProperty("confirm.close.caption"), JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE) != JOptionPane.YES_OPTION) { return; } } dispose(); } protected void onUpdate() { if (appConfigTable.isEditing()) { // 編集中ならば許可しない. Toolkit tk = Toolkit.getDefaultToolkit(); tk.beep(); return; } Properties strings = LocalizedResourcePropertyLoader.getInstance() .getLocalizedProperties("languages/appconfigdialog"); // 編集されたプロパティを取得する. Properties props = appConfigTableModel.getProperties(); // 編集されたプロパティが適用可能か検証する. Set rejectNames = AppConfig.checkProperties(props); if (!rejectNames.isEmpty()) { // エラーがある場合 appConfigTableModel.setRejectNames(rejectNames); JOptionPane.showMessageDialog(this, strings.getProperty("error.message"), strings.getProperty("error.caption"), JOptionPane.ERROR_MESSAGE); return; } try { // アプリケーション設定を更新し、保存する. AppConfig appConfig = AppConfig.getInstance(); appConfig.update(props); appConfig.saveConfig(); // キャラクターデータディレクトリの起動時の選択 if (chkResetDoNotAskAgain.isEnabled()) { boolean doNotAskAgain = !chkResetDoNotAskAgain.isSelected(); if (doNotAskAgain != recentCharactersDir.isDoNotAskAgain()) { recentCharactersDir.setDoNotAskAgain(doNotAskAgain); recentCharactersDir.saveRecents(); } } } catch (Exception ex) { ErrorMessageHelper.showErrorDialog(this, ex); return; } // アプリケーションの再起動が必要なことを示すダイアログを表示する. String message = strings.getProperty("caution"); JOptionPane.showMessageDialog(this, message); dispose(); } } class AppConfigRowModel implements Comparable { private Properties target; private String key; private String displayName; private boolean rejected; public AppConfigRowModel(Properties target, String key, String displayName) { this.target = target; this.key = key; this.displayName = displayName; } @Override public int hashCode() { return key == null ? 0 : key.hashCode(); } @Override public boolean equals(Object obj) { if (obj == this) { return true; } if (obj != null && obj instanceof AppConfigRowModel) { AppConfigRowModel o = (AppConfigRowModel) obj; return key == null ? o.key == null : key.equals(o.key); } return false; } public int compareTo(AppConfigRowModel o) { if (o == this) { return 0; } int ret = displayName.compareTo(o.displayName); if (ret == 0) { ret = key.compareTo(o.key); } return ret; } public String getKey() { return key; } public void setValue(String value) { if (value == null) { value = ""; } target.setProperty(key, value); } public String getValue() { return target.getProperty(key); } public String getDisplayName() { int sep = displayName.indexOf(';'); if (sep >= 0) { return displayName.substring(sep + 1); } return displayName; } public boolean isRejected() { return rejected; } public void setRejected(boolean rejected) { this.rejected = rejected; } } class AppConfigTableModel extends AbstractTableModelWithComboBoxModel { private static final long serialVersionUID = 1L; private static final String[] COLUMN_NAMES; private static final int[] COLUMN_WIDTHS; private Properties target = new Properties(); private Properties original; static { Properties strings = LocalizedResourcePropertyLoader.getInstance() .getLocalizedProperties("languages/appconfigdialog"); COLUMN_NAMES = new String[] { strings.getProperty("column.key"), strings.getProperty("column.value"), }; COLUMN_WIDTHS = new int[] { Integer.parseInt(strings.getProperty("column.key.width")), Integer.parseInt(strings.getProperty("column.value.width")), }; } public void initModel(Properties original) { clear(); target.clear(); this.original = original; if (original != null) { target.putAll(original); Properties strings = LocalizedResourcePropertyLoader.getInstance() .getLocalizedProperties("languages/appconfigdialog"); for (Object key : target.keySet()) { String displayName = strings.getProperty((String) key); if (displayName == null || displayName.length() == 0) { displayName = (String) key; } AppConfigRowModel rowModel = new AppConfigRowModel(target, (String) key, displayName); addRow(rowModel); } } sort(); } public void sort() { Collections.sort(elements); fireTableDataChanged(); } public void setRejectNames(Set rejectNames) { for (AppConfigRowModel rowModel : elements) { String key = rowModel.getKey(); boolean rejected = (rejectNames != null && rejectNames.contains(key)); rowModel.setRejected(rejected); } fireTableDataChanged(); } /** * 編集されているか? * @return 編集されていればtrue、そうでなければfalse */ public boolean isModified() { if (original == null) { return true; } return !original.equals(target); } public Properties getProperties() { return target; } public int getColumnCount() { return COLUMN_NAMES.length; } @Override public Class getColumnClass(int columnIndex) { return String.class; } @Override public String getColumnName(int column) { return COLUMN_NAMES[column]; } @Override public boolean isCellEditable(int rowIndex, int columnIndex) { if (columnIndex == 1) { return true; } return false; } public Object getValueAt(int rowIndex, int columnIndex) { AppConfigRowModel rowModel = getRow(rowIndex); switch (columnIndex) { case 0: return rowModel.getDisplayName(); case 1: return rowModel.getValue(); } return ""; } @Override public void setValueAt(Object aValue, int rowIndex, int columnIndex) { AppConfigRowModel rowModel = getRow(rowIndex); if (columnIndex == 1) { rowModel.setValue((String) aValue); fireTableCellUpdated(rowIndex, columnIndex); } } public void adjustColumnModel(TableColumnModel columnModel) { int mx = columnModel.getColumnCount(); for (int idx = 0; idx < mx; idx++) { columnModel.getColumn(idx).setWidth(COLUMN_WIDTHS[idx]); } } } charactermanaj/src/charactermanaj/ui/ProfileSelectorDialog.java0000644000175000017500000007574411767047542025222 0ustar paulliupaulliupackage charactermanaj.ui; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Component; import java.awt.Container; import java.awt.Cursor; import java.awt.Dimension; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.GridLayout; import java.awt.Insets; import java.awt.Toolkit; import java.awt.dnd.DropTarget; import java.awt.event.ActionEvent; import java.awt.event.KeyEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; import java.io.PrintWriter; import java.io.StringWriter; import java.net.URI; import java.text.MessageFormat; import java.util.Collection; import java.util.List; import java.util.Properties; import java.util.logging.Level; import java.util.logging.Logger; import javax.swing.AbstractAction; import javax.swing.ActionMap; import javax.swing.BorderFactory; import javax.swing.Box; import javax.swing.BoxLayout; import javax.swing.ButtonGroup; import javax.swing.DefaultListCellRenderer; import javax.swing.DefaultListModel; import javax.swing.InputMap; import javax.swing.JButton; import javax.swing.JCheckBox; import javax.swing.JComponent; import javax.swing.JDialog; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JList; import javax.swing.JMenuItem; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JPopupMenu; import javax.swing.JRadioButton; import javax.swing.JRootPane; import javax.swing.JScrollPane; import javax.swing.JTextArea; import javax.swing.KeyStroke; import javax.swing.ListSelectionModel; import javax.swing.SwingUtilities; import javax.swing.UIManager; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; import charactermanaj.Main; import charactermanaj.clipboardSupport.ClipboardUtil; import charactermanaj.graphics.io.FileImageResource; import charactermanaj.graphics.io.ImageCachedLoader; import charactermanaj.graphics.io.LoadedImage; import charactermanaj.model.AppConfig; import charactermanaj.model.CharacterData; import charactermanaj.model.io.CharacterDataPersistent; import charactermanaj.model.io.PartsImageDirectoryWatchAgent; import charactermanaj.model.io.PartsImageDirectoryWatchAgentFactory; import charactermanaj.ui.util.FileDropTarget; import charactermanaj.util.DesktopUtilities; import charactermanaj.util.ErrorMessageHelper; import charactermanaj.util.LocalizedResourcePropertyLoader; import charactermanaj.util.UIHelper; /** * プロファイルを選択するためのダイアログ、およびデフォルトプロファイルを開く * @author seraphy */ public class ProfileSelectorDialog extends JDialog { private static final long serialVersionUID = -6883202891172975022L; private static final Logger logger = Logger.getLogger(ProfileSelectorDialog.class.getName()); protected static final String STRINGS_RESOURCE = "languages/profileselectordialog"; /** * サンプルイメージをロードするためのローダー */ private ImageCachedLoader imageLoader = new ImageCachedLoader(); /** * サンプルイメージファイルが保存可能であるか?
* 有効なキャラクターデータが選択されており、サンプルイメージの更新が許可されていればtrue.
*/ private boolean canWriteSamplePicture; /** * サンプルイメージを表示するパネル */ private SamplePicturePanel sampleImgPanel; private JButton btnOK; private JButton btnProfileNew; private JButton btnProfileEdit; private JButton btnProfileRemove; private JButton btnProfileBrowse; private JButton btnProfileImport; private JButton btnProfileExport; /** * プロファイル一覧を表示するリストコンポーネント */ private JList characterList; /** * プロファイル一覧のリストモデル */ private DefaultListModel characterListModel; /** * プロファイルの説明用テキストエリア */ private JTextArea descriptionArea; /** * ダイアログをOKで閉じた場合に選択していたキャラクターデータを示す.
* nullの場合はキャンセルを意味する. */ private CharacterData selectedCharacterData; /** * プロファイルの選択ダイアログを構築する. * @param parent 親フレーム、もしくはnull * @param characterDatas プロファイルのリスト */ public ProfileSelectorDialog(JFrame parent, List characterDatas) { super(parent, true); if (characterDatas == null) { throw new IllegalArgumentException(); } setDefaultCloseOperation(JDialog.DO_NOTHING_ON_CLOSE); addWindowListener(new WindowAdapter() { @Override public void windowClosing(WindowEvent e) { onClosing(); } }); final Properties strings = LocalizedResourcePropertyLoader .getInstance().getLocalizedProperties(STRINGS_RESOURCE); setTitle(strings.getProperty("title")); JPanel centerPanel = new JPanel(); GridLayout gridLayout = new GridLayout(2, 1); centerPanel.setLayout(gridLayout); JPanel pnlProfiles = new JPanel(new BorderLayout()); characterListModel = new DefaultListModel(); for (CharacterData characterData : characterDatas) { characterListModel.addElement(characterData); } characterList = new JList(characterListModel); characterList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); characterList.setCellRenderer(new DefaultListCellRenderer() { private static final long serialVersionUID = 1L; @Override public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) { CharacterData characterData = (CharacterData) value; StringBuilder displayNameBuf = new StringBuilder(); displayNameBuf.append(characterData.getName()); if (ProfileListManager.isUsingCharacterData(characterData)) { displayNameBuf.append(strings.getProperty("profile.opend")); } if (!characterData.canWrite()) { displayNameBuf.append(strings.getProperty("profile.noteditable")); } return super.getListCellRendererComponent(list, displayNameBuf .toString(), index, isSelected, cellHasFocus); } }); characterList.addListSelectionListener(new ListSelectionListener() { public void valueChanged(ListSelectionEvent e) { updateUIState(); } }); characterList.addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { if (e.getClickCount() == 2) { // 正確に2回 onOK(); } } }); pnlProfiles.add(characterList, BorderLayout.CENTER); JPanel pnlProfileEditButtons = new JPanel(); pnlProfileEditButtons.setBorder(BorderFactory.createEmptyBorder(0, 3, 0, 3)); GridBagLayout pnlProfileEditButtonsLayout = new GridBagLayout(); GridBagConstraints gbc = new GridBagConstraints(); pnlProfileEditButtons.setLayout(pnlProfileEditButtonsLayout); btnProfileNew = new JButton(new AbstractAction(strings.getProperty("profile.new")) { private static final long serialVersionUID = 1L; public void actionPerformed(ActionEvent e) { boolean makeDefault = ((e.getModifiers() & ActionEvent.SHIFT_MASK) != 0); onProfileNew(makeDefault); } }); btnProfileEdit = new JButton(new AbstractAction(strings.getProperty("profile.edit")) { private static final long serialVersionUID = 1L; public void actionPerformed(ActionEvent e) { onProfileEdit(); } }); btnProfileRemove = new JButton(new AbstractAction(strings.getProperty("profile.remove")) { private static final long serialVersionUID = 1L; public void actionPerformed(ActionEvent e) { onProfileRemove(); } }); btnProfileBrowse = new JButton(new AbstractAction(strings.getProperty("profile.browse")) { private static final long serialVersionUID = 1L; public void actionPerformed(ActionEvent e) { onProfileBrowse(); } }); btnProfileImport = new JButton(new AbstractAction(strings.getProperty("profile.import")) { private static final long serialVersionUID = 1L; public void actionPerformed(ActionEvent e) { onProfileImport(); } }); btnProfileExport = new JButton(new AbstractAction(strings.getProperty("profile.export")) { private static final long serialVersionUID = 1L; public void actionPerformed(ActionEvent e) { onProfileExport(); } }); btnProfileNew.setToolTipText(strings.getProperty("profile.new.tooltip")); gbc.gridx = 0; gbc.gridy = 0; gbc.gridwidth = 1; gbc.gridheight = 1; gbc.weightx = 0.; gbc.weighty = 0.; gbc.anchor = GridBagConstraints.WEST; gbc.fill = GridBagConstraints.BOTH; gbc.ipadx = 0; gbc.ipady = 0; gbc.insets = new Insets(0, 3, 0, 3); pnlProfileEditButtons.add(btnProfileNew,gbc); gbc.gridx = 0; gbc.gridy = 1; pnlProfileEditButtons.add(btnProfileEdit, gbc); gbc.gridx = 0; gbc.gridy = 2; pnlProfileEditButtons.add(btnProfileRemove, gbc); gbc.gridx = 0; gbc.gridy = 3; gbc.weighty = 1.; pnlProfileEditButtons.add(Box.createGlue(), gbc); gbc.gridx = 0; gbc.gridy = 4; gbc.weighty = 0.; pnlProfileEditButtons.add(btnProfileBrowse, gbc); gbc.gridx = 0; gbc.gridy = 5; gbc.weighty = 0.; pnlProfileEditButtons.add(btnProfileImport, gbc); gbc.gridx = 0; gbc.gridy = 6; pnlProfileEditButtons.add(btnProfileExport, gbc); JPanel pnlProfilesGroup = new JPanel(new BorderLayout()); pnlProfilesGroup.setBorder(BorderFactory.createCompoundBorder(BorderFactory .createEmptyBorder(3, 3, 3, 3), BorderFactory .createTitledBorder(strings.getProperty("profiles")))); pnlProfilesGroup.add(pnlProfiles, BorderLayout.CENTER); pnlProfilesGroup.add(pnlProfileEditButtons, BorderLayout.EAST); JPanel infoPanel = new JPanel(new GridLayout(1, 2)); JPanel descriptionPanel = new JPanel(new BorderLayout()); descriptionPanel.setBorder(BorderFactory.createCompoundBorder( BorderFactory.createEmptyBorder(3, 3, 3, 3), BorderFactory .createTitledBorder(strings.getProperty("description")))); descriptionArea = new JTextArea(); descriptionArea.setEditable(false); descriptionPanel.add(new JScrollPane(descriptionArea), BorderLayout.CENTER); // サンプルピクャパネル sampleImgPanel = new SamplePicturePanel(); // サンプルピクチャファイルのドラッグアンドドロップ new DropTarget(sampleImgPanel, new FileDropTarget() { @Override protected void onDropFiles(final List dropFiles) { if (dropFiles == null || dropFiles.isEmpty()) { return; } // インポートダイアログを開く. // ドロップソースの処理がブロッキングしないように、 // ドロップハンドラの処理を終了して呼び出す. SwingUtilities.invokeLater(new Runnable() { public void run() { onDrop(dropFiles); } }); } @Override protected void onException(Exception ex) { ErrorMessageHelper.showErrorDialog(ProfileSelectorDialog.this, ex); } }); // サンプルピクチャのコンテキストメニュー final JPopupMenu popupMenu = new JPopupMenu(); final JMenuItem popupMenuCut = popupMenu.add(new AbstractAction(strings.getProperty("samplepicture.cut")) { private static final long serialVersionUID = 1L; public void actionPerformed(ActionEvent e) { onSamplePictureCut(); } }); final JMenuItem popupMenuPaste = popupMenu.add(new AbstractAction(strings.getProperty("samplepicture.paste")) { private static final long serialVersionUID = 1L; public void actionPerformed(ActionEvent e) { onSamplePicturePaste(); } }); sampleImgPanel.addMouseListener(new MouseAdapter() { @Override public void mousePressed(MouseEvent e) { evaluatePopup(e); } @Override public void mouseReleased(MouseEvent e) { evaluatePopup(e); } private void evaluatePopup(MouseEvent e) { if (e.isPopupTrigger()) { popupMenuCut.setEnabled(sampleImgPanel.getSamplePictrue() != null && canWriteSamplePicture); popupMenuPaste.setEnabled(canWriteSamplePicture && ClipboardUtil.hasImage()); popupMenu.show(sampleImgPanel, e.getX(), e.getY()); } } }); JScrollPane sampleImgPanelSp = new JScrollPane(sampleImgPanel); sampleImgPanelSp.setBorder(null); JPanel sampleImgTitledPanel = new JPanel(new BorderLayout()); sampleImgTitledPanel.add(sampleImgPanelSp, BorderLayout.CENTER); sampleImgTitledPanel.setBorder(BorderFactory.createCompoundBorder( BorderFactory.createEmptyBorder(3, 3, 3, 3), BorderFactory .createTitledBorder(strings.getProperty("sample-image")))); infoPanel.add(descriptionPanel); infoPanel.add(sampleImgTitledPanel); centerPanel.add(pnlProfilesGroup); centerPanel.add(infoPanel); Container contentPane = getContentPane(); contentPane.setLayout(new BorderLayout()); contentPane.add(centerPanel, BorderLayout.CENTER); // OK/CANCEL ボタンパネル JPanel btnPanel = new JPanel(); BoxLayout boxLayout = new BoxLayout(btnPanel, BoxLayout.LINE_AXIS); btnPanel.setLayout(boxLayout); btnPanel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 42)); AbstractAction actOK = new AbstractAction(strings.getProperty("btn.select")) { private static final long serialVersionUID = 1L; public void actionPerformed(ActionEvent e) { onOK(); } }; AbstractAction actCancel = new AbstractAction(strings.getProperty("btn.cancel")) { private static final long serialVersionUID = 1L; public void actionPerformed(ActionEvent e) { onCancel(); } }; btnOK = new JButton(actOK); JButton btnCancel = new JButton(actCancel); if (Main.isLinuxOrMacOSX()) { btnPanel.add(btnCancel); btnPanel.add(btnOK); } else { btnPanel.add(btnOK); btnPanel.add(btnCancel); } JPanel btnPanel2 = new JPanel(new BorderLayout()); btnPanel2.add(btnPanel, BorderLayout.EAST); contentPane.add(btnPanel2, BorderLayout.SOUTH); Toolkit tk = Toolkit.getDefaultToolkit(); JRootPane rootPane = getRootPane(); rootPane.setDefaultButton(btnOK); InputMap im = rootPane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW); ActionMap am = rootPane.getActionMap(); im.put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "closeProfileSelectorDialog"); im.put(KeyStroke.getKeyStroke(KeyEvent.VK_W, tk.getMenuShortcutKeyMask()), "closeProfileSelectorDialog"); am.put("closeProfileSelectorDialog", actCancel); pack(); setSize(500, 500); setLocationRelativeTo(parent); characterList.requestFocus(); updateUIState(); } public CharacterData getSelectedCharacterData() { return selectedCharacterData; } /** * キャラクターデータの選択変更に伴い、ボタンやサンプルピクチャなどを切り替える. */ protected void updateUIState() { CharacterData characterData = (CharacterData) characterList.getSelectedValue(); final Properties strings = LocalizedResourcePropertyLoader .getInstance().getLocalizedProperties(STRINGS_RESOURCE); boolean selected = (characterData != null); boolean enableEdit = (characterData != null) && characterData.canWrite(); btnOK.setEnabled(selected); btnProfileNew.setEnabled(true); btnProfileEdit.setEnabled(selected); btnProfileRemove.setEnabled(selected && enableEdit); btnProfileImport.setEnabled(true); btnProfileExport.setEnabled(selected); btnProfileBrowse.setEnabled(selected); if (enableEdit) { btnProfileEdit.setText(strings.getProperty("profile.edit")); } else { btnProfileEdit.setText(strings.getProperty("profile.edit.readonly")); } // 有効なキャラクターデータであり、且つ、書き込み可能であり、且つ、使用中でなければ削除可能 boolean removable = characterData != null && characterData.isValid() && !ProfileListManager.isUsingCharacterData(characterData) && characterData.canWrite(); btnProfileRemove.setEnabled(removable); boolean canWriteSamplePicture = false; BufferedImage sampleImage = null; if (characterData != null && characterData.isValid()) { // description StringWriter sw = new StringWriter(); PrintWriter descriptionBuf = new PrintWriter(sw); URI docBase = characterData.getDocBase(); String author = characterData.getAuthor(); String description = characterData.getDescription(); if (docBase != null) { descriptionBuf.println("configuration: " + docBase); } if (author != null && author.length() > 0) { descriptionBuf.println("author: " + author); } Dimension imageSize = characterData.getImageSize(); if (imageSize != null) { descriptionBuf.println("size: " + imageSize.width + "x" + imageSize.height); } if (description != null) { description = description.replace("\r\n", "\n"); description = description.replace("\r", "\n"); description = description.replace("\n", System.getProperty("line.separator")); descriptionBuf.println(description); } descriptionArea.setText(sw.toString()); descriptionArea.setSelectionStart(0); descriptionArea.setSelectionEnd(0); // sample picture try { CharacterDataPersistent persist = CharacterDataPersistent.getInstance(); sampleImage = persist.loadSamplePicture(characterData, imageLoader); canWriteSamplePicture = persist.canSaveSamplePicture(characterData); } catch (Exception ex) { // サンプルピクチャの読み込みに失敗したら、サンプルピクチャを表示しないだけで処理は継続する. logger.log(Level.WARNING, "sample picture loading failed. " + characterData , ex); sampleImage = null; } } this.canWriteSamplePicture = canWriteSamplePicture; String dropHere = strings.getProperty("dropHere"); String noPicture = strings.getProperty("nopicture"); sampleImgPanel.setSamplePicture(sampleImage); sampleImgPanel.setAlternateText(canWriteSamplePicture ? dropHere : noPicture); } /** * サンプルピクチャのファイルを削除し、表示されている画像をクリップボードに保存する */ protected void onSamplePictureCut() { CharacterData characterData = (CharacterData) characterList.getSelectedValue(); BufferedImage img = sampleImgPanel.getSamplePictrue(); Toolkit tk = Toolkit.getDefaultToolkit(); if (characterData == null || !characterData.isValid() || !canWriteSamplePicture || img == null) { tk.beep(); return; } try { // クリップボードにイメージをセット Color bgColor = AppConfig.getInstance().getSampleImageBgColor(); ClipboardUtil.setImage(img, bgColor); // サンプルピクチャを削除 CharacterDataPersistent persist = CharacterDataPersistent.getInstance(); persist.saveSamplePicture(characterData, null); // プレビューを更新 sampleImgPanel.setSamplePicture(null); } catch (Exception ex) { ErrorMessageHelper.showErrorDialog(this, ex); } } /** * サンプルピクチャをクリップボードから貼り付け、それをファイルに保存する */ protected void onSamplePicturePaste() { CharacterData characterData = (CharacterData) characterList.getSelectedValue(); Toolkit tk = Toolkit.getDefaultToolkit(); if (characterData == null || !characterData.isValid() || !canWriteSamplePicture) { tk.beep(); return; } try { BufferedImage img = ClipboardUtil.getImage(); if (img != null) { // 画像が読み込まれた場合、それを保存する. CharacterDataPersistent persist = CharacterDataPersistent.getInstance(); persist.saveSamplePicture(characterData, img); sampleImgPanel.setSamplePicture(img); } else { // サンプルピクチャは更新されなかった。 tk.beep(); } } catch (Exception ex) { ErrorMessageHelper.showErrorDialog(this, ex); } } /** * サンプルピクチャパネルにドロップされた画像ファイルをサンプルピクチャとしてコピーします.
* @param dtde ドロップイベント */ protected void onDrop(Collection dropFiles) { Toolkit tk = Toolkit.getDefaultToolkit(); CharacterData characterData = (CharacterData) characterList.getSelectedValue(); if (dropFiles == null || dropFiles.isEmpty() || !canWriteSamplePicture || characterData == null || !characterData.isValid() || !canWriteSamplePicture) { tk.beep(); return; } try { // 最初のファィルのみ取得する. File dropFile = dropFiles.iterator().next(); // ドロップファイルがあれば、イメージとして読み込む BufferedImage img = null; if (dropFile != null && dropFile.isFile() && dropFile.canRead()) { try { LoadedImage loadedImage = imageLoader.load(new FileImageResource(dropFile)); img = loadedImage.getImage(); } catch (IOException ex) { // イメージのロードができない = 形式が不正であるなどの場合は // 読み込みせず、継続する. img = null; } } if (img != null) { // 画像が読み込まれた場合、それを保存する. CharacterDataPersistent persist = CharacterDataPersistent.getInstance(); persist.saveSamplePicture(characterData, img); sampleImgPanel.setSamplePicture(img); } else { // サンプルピクチャは更新されなかった。 tk.beep(); } } catch (Exception ex) { ErrorMessageHelper.showErrorDialog(this, ex); } } /** * 閉じる場合 */ protected void onClosing() { dispose(); } /** * OKボタン押下 */ protected void onOK() { selectedCharacterData = (CharacterData) characterList.getSelectedValue(); if (selectedCharacterData == null) { Toolkit tk = Toolkit.getDefaultToolkit(); tk.beep(); return; } dispose(); } /** * キャンセルボタン押下 */ protected void onCancel() { selectedCharacterData = null; onClosing(); } /** * プロファイルの作成 * @param makeDefault デフォルトのプロファイルで作成する場合 */ protected void onProfileNew(boolean makeDefault) { CharacterDataPersistent persist = CharacterDataPersistent.getInstance(); CharacterData cd = (CharacterData) characterList.getSelectedValue(); if (makeDefault || cd == null) { // デフォルトのプロファイルを使用します. cd = persist.createDefaultCharacterData(); } // 基本情報をコピーします。 CharacterData newCd = cd.duplicateBasicInfo(); // DocBaseはnullにする。これにより新規作成と判断される. newCd.setDocBase(null); // 新規なのでパーツセット情報はリセットする newCd.clearPartsSets(false); ProfileEditDialog editDlg = new ProfileEditDialog(this, newCd); editDlg.setVisible(true); newCd = editDlg.getResult(); if (newCd == null) { // キャンセルされた場合 return; } // 新規プロファイルを保存する. try { persist.createProfile(newCd); persist.saveFavorites(newCd); } catch (Exception ex) { ErrorMessageHelper.showErrorDialog(this, ex); return; } // 作成されたプロファイルを一覧に追加する. characterListModel.addElement(newCd); } /** * プロファィルの編集 */ protected void onProfileEdit() { CharacterData cd = (CharacterData) characterList.getSelectedValue(); int rowIndex = characterList.getSelectedIndex(); if (cd == null || !cd.isValid() || rowIndex < 0) { return; } try { // プロファイル編集ダイアログを開き、その結果を取得する. CharacterData newCd = ProfileListManager.editProfile(this, cd); if (newCd == null) { // キャンセルされた場合 return; } // 現在開いているメインフレームに対してキャラクター定義が変更されたことを通知する. MainFrame.notifyChangeCharacterData(cd, newCd, this); // プロファイル一覧画面も更新する. characterListModel.set(rowIndex, newCd); characterList.repaint(); } catch (Exception ex) { ErrorMessageHelper.showErrorDialog(this, ex); return; } } /** * プロファイルの削除 */ protected void onProfileRemove() { CharacterData cd = (CharacterData) characterList.getSelectedValue(); if (cd == null || !cd.isValid() || ProfileListManager.isUsingCharacterData(cd) || !cd.canWrite()) { // 無効なキャラクター定義であるか、使用中であるか、書き込み不可であれば削除は実行できない. Toolkit tk = Toolkit.getDefaultToolkit(); tk.beep(); return; } final Properties strings = LocalizedResourcePropertyLoader .getInstance().getLocalizedProperties(STRINGS_RESOURCE); String msgTempl = strings.getProperty("profile.remove.confirm"); MessageFormat fmt = new MessageFormat(msgTempl); String msg = fmt.format(new Object[]{cd.getName()}); JPanel msgPanel = new JPanel(new BorderLayout(5, 5)); msgPanel.add(new JLabel(msg), BorderLayout.CENTER); JCheckBox chkRemoveForce = new JCheckBox(strings.getProperty("profile.remove.force")); msgPanel.add(chkRemoveForce, BorderLayout.SOUTH); JOptionPane optionPane = new JOptionPane(msgPanel, JOptionPane.QUESTION_MESSAGE, JOptionPane.YES_NO_OPTION) { private static final long serialVersionUID = 1L; @Override public void selectInitialValue() { String noBtnCaption = UIManager.getString("OptionPane.noButtonText"); for (JButton btn : UIHelper.getInstance().getDescendantOfClass(JButton.class, this)) { if (btn.getText().equals(noBtnCaption)) { // 「いいえ」ボタンにフォーカスを設定 btn.requestFocus(); } } } }; JDialog dlg = optionPane.createDialog(this, strings.getProperty("confirm.remove")); dlg.setVisible(true); Object ret = optionPane.getValue(); if (ret == null || ((Number) ret).intValue() != JOptionPane.YES_OPTION) { return; } if (!cd.canWrite() || cd.getDocBase() == null) { JOptionPane.showMessageDialog(this, strings.getProperty("profile.remove.cannot")); return; } boolean forceRemove = chkRemoveForce.isSelected(); try { CharacterDataPersistent persiste = CharacterDataPersistent.getInstance(); persiste.remove(cd, forceRemove); } catch (Exception ex) { ErrorMessageHelper.showErrorDialog(this, ex); return; } // モデルから該当キャラクターを削除して再描画 characterListModel.removeElement(cd); characterList.repaint(); updateUIState(); } /** * 場所を開く */ protected void onProfileBrowse() { CharacterData cd = (CharacterData) characterList.getSelectedValue(); if (cd == null || !cd.isValid()) { Toolkit tk = Toolkit.getDefaultToolkit(); tk.beep(); return; } try { URI docBase = cd.getDocBase(); if (!DesktopUtilities.browseBaseDir(docBase)) { JOptionPane.showMessageDialog(this, docBase); } } catch (Exception ex) { ErrorMessageHelper.showErrorDialog(this, ex); } } /** * インポート */ protected void onProfileImport() { try { CharacterData selCd = (CharacterData) characterList.getSelectedValue(); // 選択したプロファイルを更新するか、新規にプロファイルを作成するか選択できるようにする if (selCd != null) { final Properties strings = LocalizedResourcePropertyLoader .getInstance().getLocalizedProperties(STRINGS_RESOURCE); JPanel radioPanel = new JPanel(new BorderLayout()); JRadioButton btnUpdate = new JRadioButton(strings.getProperty("importToUpdateProfile")); JRadioButton btnNew = new JRadioButton(strings.getProperty("importToCreateProfile")); ButtonGroup radios = new ButtonGroup(); radios.add(btnUpdate); radios.add(btnNew); btnUpdate.setSelected(true); radioPanel.add(btnUpdate, BorderLayout.NORTH); radioPanel.add(btnNew, BorderLayout.SOUTH); int ret = JOptionPane.showConfirmDialog(this, radioPanel, strings.getProperty("confirmUpdateProfile"), JOptionPane.OK_CANCEL_OPTION); if (ret != JOptionPane.OK_OPTION) { return; } if (btnNew.isSelected()) { // 選択されていないことにする. selCd = null; } } // キャラクターデータをロードし直す. CharacterData cd; if (selCd != null) { cd = selCd.duplicateBasicInfo(); try { ProfileListManager.loadCharacterData(cd); ProfileListManager.loadFavorites(cd); } catch (IOException ex) { ErrorMessageHelper.showErrorDialog(this, ex); // 継続する. } } else { cd = null; } // ディレクトリ監視エージェントの停止 PartsImageDirectoryWatchAgentFactory agentFactory = PartsImageDirectoryWatchAgentFactory.getFactory(); PartsImageDirectoryWatchAgent agent = agentFactory.getAgent(cd); agent.suspend(); try { // インポートウィザードの実行 ImportWizardDialog importWizDialog = new ImportWizardDialog(this, cd); importWizDialog.setVisible(true); CharacterData newCd = importWizDialog.getImportedCharacterData(); if (importWizDialog.getExitCode() == ImportWizardDialog.EXIT_PROFILE_CREATED) { // 作成されたプロファイルを一覧に追加する. characterListModel.addElement(newCd); } else if (importWizDialog.getExitCode() == ImportWizardDialog.EXIT_PROFILE_UPDATED) { // 更新されたプロファイルを通知する MainFrame.notifyImportedPartsOrFavorites(cd, newCd, this); } } finally { agent.resume(); } } catch (Exception ex) { ErrorMessageHelper.showErrorDialog(this, ex); } } /** * エクスポート */ protected void onProfileExport() { CharacterData cd = (CharacterData) characterList.getSelectedValue(); if (cd == null || !cd.isValid()) { Toolkit tk = Toolkit.getDefaultToolkit(); tk.beep(); return; } try { // コピーした情報に対してパーツデータをロードする. final CharacterData newCd = cd.duplicateBasicInfo(); setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); try { ProfileListManager.loadCharacterData(newCd); } finally { setCursor(Cursor.getDefaultCursor()); } // エクスポートウィザードを表示 BufferedImage sampleImage = sampleImgPanel.getSamplePictrue(); ExportWizardDialog exportWizDialog = new ExportWizardDialog(this, newCd, sampleImage); exportWizDialog.setVisible(true); } catch (Exception ex) { ErrorMessageHelper.showErrorDialog(this, ex); return; } } } charactermanaj/src/charactermanaj/ui/ProfileListManager.java0000644000175000017500000003405611767047542024517 0ustar paulliupaulliupackage charactermanaj.ui; import java.awt.Cursor; import java.io.IOException; import java.net.URI; import java.util.HashMap; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; import javax.swing.JDialog; import javax.swing.JFrame; import charactermanaj.model.CharacterData; import charactermanaj.model.PartsManageData; import charactermanaj.model.io.CharacterDataPersistent; import charactermanaj.model.io.PartsDataLoader; import charactermanaj.model.io.PartsDataLoaderFactory; import charactermanaj.model.io.PartsManageDataDecorateLoader; import charactermanaj.model.io.PartsSpecDecorateLoader; import charactermanaj.model.io.RecentDataPersistent; import charactermanaj.util.ErrorMessageHelper; /** * プロファイルの選択・管理を行うクラス. * @author seraphy */ public final class ProfileListManager { /** * ロガー */ private static final Logger logger = Logger.getLogger(ProfileListManager.class.getName()); /** * プライベートコンストラクタ */ private ProfileListManager() { super(); } /** * すべてのメインフレームで使用中のキャラクターデータのコレクション.
*/ private static final HashMap activeCharacterDatas = new HashMap(); /** * キャラクターデータが使用中であるか?
* キャラクターデータのDocBaseをもとに判断する.
* nullを指定した場合は常にfalseを返す.
* @param characterData キャラクターデータ、またはnull * @return 使用中であればtrue */ public static boolean isUsingCharacterData(CharacterData characterData) { URI characterDocBase = (characterData == null) ? null : characterData.getDocBase(); synchronized (activeCharacterDatas) { Integer cnt = (characterDocBase == null) ? null : activeCharacterDatas.get(characterDocBase); return cnt != null && cnt.intValue() > 0; } } /** * キャラクターデータが使用中であることを登録する。 * @param characterData キャラクターデータ */ public static void registerUsedCharacterData(CharacterData characterData) { if (characterData == null) { return; } synchronized (activeCharacterDatas) { URI characterDocBase = characterData.getDocBase(); if (characterDocBase != null) { Integer cnt = activeCharacterDatas.get(characterDocBase); if (cnt == null) { cnt = Integer.valueOf(1); } else { cnt = Integer.valueOf(cnt.intValue() + 1); } activeCharacterDatas.put(characterDocBase, cnt); } } } /** * キャラクターデータの使用中であることを登録解除する。 * @param characterData キャラクターデータ */ public static void unregisterUsedCharacterData(CharacterData characterData) { if (characterData == null) { return; } // 使用中のキャラクターデータとしてのカウントを減らす synchronized (activeCharacterDatas) { URI characterDocBase = characterData.getDocBase(); if (characterDocBase != null) { Integer cnt = activeCharacterDatas.get(characterDocBase); if (cnt != null) { cnt = Integer.valueOf(cnt.intValue() - 1); if (cnt.intValue() <= 0) { activeCharacterDatas.remove(characterDocBase); } else { activeCharacterDatas.put(characterDocBase, cnt); } } } } } /** * プロファイル選択ダイアログを表示し、選択されたプロファイルのメインフレームを作成して返す.
* プロファイルの選択をキャンセルした場合はnullを返す.
* @param parent 親フレーム * @return メインフレーム、もしくはnull * @throws IOException 読み込みに失敗した場合 */ public static MainFrame openProfile(JFrame parent) throws IOException { // キャラクタープロファイルのリストをロード CharacterDataPersistent persist = CharacterDataPersistent.getInstance(); List characterDatas = persist.listProfiles(CharacterDataPersistent.DEFAULT_ERROR_HANDLER); // 選択ダイアログを表示 ProfileSelectorDialog selDlg = new ProfileSelectorDialog(parent, characterDatas); selDlg.setVisible(true); CharacterData characterData = selDlg.getSelectedCharacterData(); if (characterData == null || !characterData.isValid()) { // キャンセルしたか、開くことのできないデータ return null; } // メインフレームを準備 MainFrame newFrame; parent.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); try { newFrame = openProfile(characterData); } finally { parent.setCursor(Cursor.getDefaultCursor()); } return newFrame; } /** * 指定したキャラクター定義で新しいメインフレームを作成して返す.
* @param characterData キャラクターデータ * @return 作成されたメインフレーム * @throws IOException 例外 */ public static MainFrame openProfile(CharacterData characterData) throws IOException { if (characterData == null || !characterData.isValid()) { throw new IOException("開くことのできないキャラクターデータです。:" + characterData); } // キャラクターデータのロード loadCharacterData(characterData); loadFavorites(characterData); // メインフレームを構築 MainFrame newFrame = new MainFrame(characterData); // 最後に使ったプロファイルとして登録 saveRecent(characterData); return newFrame; } /** * キャラクター定義編集用ダイアログを生成して返す. * @author seraphy */ private interface ProfileEditorDialogFactory { ProfileEditDialog create(CharacterData characterData); } /** * キャラクター定義を編集する.
* @param parent 親ダイアログ、もしくはnull * @param characterData キャラクター定義(参照のみ、変更されない.) * @return 編集されたキャラクター定義、もしくはキャンセルされた場合はnull * @throws IOException 失敗 */ public static CharacterData editProfile(final JDialog parent, CharacterData characterData) throws IOException { return internalEditProfile(characterData, new ProfileEditorDialogFactory() { public ProfileEditDialog create(CharacterData characterData) { return new ProfileEditDialog(parent, characterData); } }); } /** * キャラクター定義を編集する.
* @param parent 親フレーム、もしくはnull * @param characterData キャラクター定義(参照のみ、変更されない.) * @return 編集されたキャラクター定義、もしくはキャンセルされた場合はnull * @throws IOException 失敗 */ public static CharacterData editProfile(final JFrame parent, CharacterData characterData) throws IOException { return internalEditProfile(characterData, new ProfileEditorDialogFactory() { public ProfileEditDialog create(CharacterData characterData) { return new ProfileEditDialog(parent, characterData); } }); } /** * キャラクター定義を編集する.
* @param characterData キャラクター定義(参照のみ、変更されない.) * @param dialogFactory キャラクター定義編集ダイアログを生成するファクトリ * @return 編集されたキャラクター定義、もしくはキャンセルされた場合はnull * @throws IOException 失敗 */ private static CharacterData internalEditProfile(CharacterData characterData, ProfileEditorDialogFactory dialogFactory) throws IOException { if (characterData == null || !characterData.isValid()) { throw new IOException("開くことのできないキャラクターデータです。:" + characterData); } // キャラクターデータのコピーを作成する.(プリセットも含む) CharacterData original = characterData.duplicateBasicInfo(true); original.clearPartsSets(true); // プリセット以外のパーツセットはクリアする. try { loadFavorites(original); } catch (IOException ex) { ErrorMessageHelper.showErrorDialog(null, ex); // Favoritesの読み込みに失敗しても継続する. } // 編集用ダイアログを構築して開く ProfileEditDialog editDlg = dialogFactory.create(original); editDlg.setVisible(true); // 編集結果を得る. CharacterData newCd = editDlg.getResult(); if (newCd == null) { // キャンセルされた場合 return null; } // 保存する. CharacterDataPersistent persist = CharacterDataPersistent.getInstance(); persist.updateProfile(newCd); persist.saveFavorites(newCd); return newCd; } /** * 最後にしようしたプロファイル、それがなければデフォルトプロファイルを開いて、そのメインフレームを返す. * @return メインフレーム * @throws IOException 開けなかった場合 */ public static MainFrame openDefaultProfile() throws IOException { CharacterDataPersistent persistent = CharacterDataPersistent.getInstance(); CharacterData characterData; // 最後に使用したプロファイルのロードを試行する. try { characterData = loadRecent(); // キャラクターデータを読み込む loadCharacterData(characterData); loadFavorites(characterData); } catch (Exception ex) { ErrorMessageHelper.showErrorDialog(null, ex); characterData = null; } // 最後に使用したプロファイルの記録がないか、プロファイルのロードに失敗した場合は // プロファイル一覧から最初のプロファイルを選択する. if (characterData == null) { List profiles = persistent.listProfiles(CharacterDataPersistent.DEFAULT_ERROR_HANDLER); for (CharacterData tryCd : profiles) { try { characterData = tryCd; // キャラクターデータを読み込む loadCharacterData(tryCd); loadFavorites(tryCd); } catch (Exception ex) { logger.log(Level.SEVERE, "プロファイルのロードに失敗しました。" + tryCd, ex); // プロファイルの読み込みに失敗した場合は次を試行する. characterData = null; } } } // プロファイルが一個もなければ、デフォルトのプロファイルの生成を試行する. if (characterData == null) { logger.info("オープンできるプロファイルがないため、新規プロファイルを作成します。"); try { characterData = persistent.createDefaultCharacterData(); persistent.createProfile(characterData); } catch (IOException ex) { // デフォルトのプロファイルが作成できないことは致命的であるが、 // アプリケーションを起動させるために継続する. logger.log(Level.SEVERE, "default profile creation failed.", ex); // キャラクター定義として無効なダミーのインスタンスを生成して返す. // 何もできないが、メインフレームを空の状態で表示させることは可能. characterData = new CharacterData(); } } // 最後に使用したプロファイルとして記録 saveRecent(characterData); // メインフレームを生成して返す return new MainFrame(characterData); } /** * キャラクターデータに、パーツデータをロードする.
* お気に入りはロードされないので、必要ならば、このあとで{@link #loadFavorites(CharacterData)}を呼び出す.
* @param characterData * @throws IOException 開けなかった場合 */ public static void loadCharacterData(final CharacterData characterData) throws IOException { if (characterData != null && characterData.isValid()) { final CharacterDataPersistent persistent = CharacterDataPersistent.getInstance(); PartsDataLoaderFactory loaderFactory = PartsDataLoaderFactory.getInstance(); PartsDataLoader loader = loaderFactory.createPartsLoader(characterData.getDocBase()); PartsDataLoader colorGroupInfoDecorater = new PartsSpecDecorateLoader(loader, characterData.getColorGroups()); PartsManageDataDecorateLoader partsMngDecorater = new PartsManageDataDecorateLoader(colorGroupInfoDecorater, new PartsManageDataDecorateLoader.PartsManageDataFactory() { public PartsManageData createPartsManageData() { try { return persistent.loadPartsManageData(characterData.getDocBase()); } catch (Exception ex) { logger.log(Level.WARNING, "parts-info.xml loading failed.", ex); return new PartsManageData(); } } }); characterData.loadPartsData(partsMngDecorater); } } /** * キャラクターデータに、お気に入りをロードする.
* @param characterData * @throws IOException 開けなかった場合 */ public static void loadFavorites(final CharacterData characterData) throws IOException { if (characterData != null && characterData.isValid()) { final CharacterDataPersistent persistent = CharacterDataPersistent.getInstance(); persistent.loadFavorites(characterData); } } /** * 最後に使用したキャラクターデータとして記録する. * @param characterData * @throws IOException 保存できなった場合 */ public static void saveRecent(CharacterData characterData) throws IOException { RecentDataPersistent recentPersist = RecentDataPersistent.getInstance(); recentPersist.saveRecent(characterData); } /** * 最後に使用したキャラクターデータを取得する. * @return キャラクターデータ。最後に使用したデータが存在しない場合はnull * @throws IOException 読み込みに失敗した場合 */ public static CharacterData loadRecent() throws IOException { RecentDataPersistent recentPersist = RecentDataPersistent.getInstance(); return recentPersist.loadRecent(); } } charactermanaj/src/charactermanaj/ui/ImportWizardDialog.java0000644000175000017500000030565111767047542024545 0ustar paulliupaulliupackage charactermanaj.ui; import java.awt.BorderLayout; import java.awt.CardLayout; import java.awt.Color; import java.awt.Component; import java.awt.Container; import java.awt.Cursor; import java.awt.Dimension; import java.awt.Font; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.Insets; import java.awt.Rectangle; import java.awt.Toolkit; import java.awt.dnd.DropTarget; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.ComponentAdapter; import java.awt.event.ComponentEvent; import java.awt.event.ComponentListener; import java.awt.event.KeyEvent; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; import java.io.Serializable; import java.net.URI; import java.sql.Timestamp; import java.util.AbstractCollection; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.IdentityHashMap; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.Set; import javax.swing.AbstractAction; import javax.swing.Action; import javax.swing.ActionMap; import javax.swing.BorderFactory; import javax.swing.Box; import javax.swing.BoxLayout; import javax.swing.ButtonGroup; import javax.swing.InputMap; import javax.swing.JButton; import javax.swing.JCheckBox; import javax.swing.JComponent; import javax.swing.JDialog; import javax.swing.JFileChooser; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JPopupMenu; import javax.swing.JRadioButton; import javax.swing.JRootPane; import javax.swing.JScrollPane; import javax.swing.JTable; import javax.swing.JTextArea; import javax.swing.JTextField; import javax.swing.KeyStroke; import javax.swing.ListSelectionModel; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; import javax.swing.event.TableModelEvent; import javax.swing.event.TableModelListener; import javax.swing.table.TableCellRenderer; import javax.swing.table.TableColumnModel; import charactermanaj.Main; import charactermanaj.graphics.io.PNGFileImageHeader; import charactermanaj.model.AppConfig; import charactermanaj.model.CharacterData; import charactermanaj.model.PartsAuthorInfo; import charactermanaj.model.PartsCategory; import charactermanaj.model.PartsIdentifier; import charactermanaj.model.PartsManageData; import charactermanaj.model.PartsSet; import charactermanaj.model.PartsSpec; import charactermanaj.model.io.CharacterDataPersistent; import charactermanaj.model.io.AbstractCharacterDataArchiveFile.CategoryLayerPair; import charactermanaj.model.io.AbstractCharacterDataArchiveFile.PartsImageContent; import charactermanaj.model.io.ImportModel; import charactermanaj.ui.model.AbstractTableModelWithComboBoxModel; import charactermanaj.ui.progress.ProgressHandle; import charactermanaj.ui.progress.Worker; import charactermanaj.ui.progress.WorkerException; import charactermanaj.ui.progress.WorkerWithProgessDialog; import charactermanaj.ui.util.FileDropTarget; import charactermanaj.util.ErrorMessageHelper; import charactermanaj.util.LocalizedResourcePropertyLoader; /** * インポートウィザードダイアログ.
* * @author seraphy */ public class ImportWizardDialog extends JDialog { private static final long serialVersionUID = 1L; protected static final String STRINGS_RESOURCE = "languages/importwizdialog"; public static final int EXIT_PROFILE_UPDATED = 1; public static final int EXIT_PROFILE_CREATED = 2; public static final int EXIT_CANCELED = 0; /** * インポートウィザードの実行結果.
*/ private int exitCode = EXIT_CANCELED; /** * インポートされたキャラクターデータ.
*/ private CharacterData importedCharacterData; /** * 現在表示中もしくは選択中のプロファイル.
* 新規の場合はnull */ protected CharacterData current; private CardLayout mainPanelLayout; private ImportWizardCardPanel activePanel; private AbstractAction actNext; private AbstractAction actPrev; private AbstractAction actFinish; protected ImportFileSelectPanel importFileSelectPanel; protected ImportTypeSelectPanel importTypeSelectPanel; protected ImportPartsSelectPanel importPartsSelectPanel; protected ImportPresetSelectPanel importPresetSelectPanel; protected ImportModel importModel = new ImportModel(); /** * プロファイルにパーツデータ・プリセットデータをインポートします.
* @param parent 親フレーム * @param current 更新対象となる現在のプロファイル(新規インポートの場合はnull) * @param initFiles アーカイブファィルまたはディレクトリの初期選択、なければnullまたは空 */ public ImportWizardDialog(JFrame parent, CharacterData current, List initFiles) { super(parent, true); initComponent(parent, current); importFileSelectPanel.setSelectFile(initFiles); } /** * プロファイルにパーツデータ・プリセットデータをインポートします.
* @param parent 親ダイアログ * @param current 選択していてるプロファイル、新規インポートの場合はnull */ public ImportWizardDialog(JDialog parent, CharacterData current) { super(parent, true); initComponent(parent, current); } /** * ウィザードダイアログのコンポーネントを初期化します.
* currentがnullの場合は新規インポート、そうでない場合は更新インポートとります。 * @param parent 親コンテナ * @param current インポート対象プロファイル、新規の場合はnull */ private void initComponent(Component parent, CharacterData current) { this.current = current; setDefaultCloseOperation(JDialog.DO_NOTHING_ON_CLOSE); addWindowListener(new WindowAdapter() { @Override public void windowClosing(WindowEvent e) { onClose(); } }); Properties strings = LocalizedResourcePropertyLoader.getInstance() .getLocalizedProperties(STRINGS_RESOURCE); // タイトル if (current == null) { setTitle(strings.getProperty("title.new")); } else { setTitle(strings.getProperty("title.update")); } // メインパネル Container contentPane = getContentPane(); contentPane.setLayout(new BorderLayout()); final JPanel mainPanel = new JPanel(); mainPanel.setBorder(BorderFactory.createEtchedBorder()); this.mainPanelLayout = new CardLayout(5, 5); mainPanel.setLayout(mainPanelLayout); contentPane.add(mainPanel, BorderLayout.CENTER); ChangeListener changeListener = new ChangeListener() { public void stateChanged(ChangeEvent e) { updateBtnPanelState(); } }; ComponentListener componentListener = new ComponentAdapter() { public void componentShown(ComponentEvent e) { onComponentShown((ImportWizardCardPanel) e.getComponent()); } }; // アクション this.actNext = new AbstractAction(strings.getProperty("btn.next")) { private static final long serialVersionUID = 1L; public void actionPerformed(ActionEvent e) { setEnableButtons(false); String nextPanelName = doNext(); if (nextPanelName != null) { mainPanelLayout.show(mainPanel, nextPanelName); } else { // 移動先ページ名なければ、現在のページでボタン状態を再設定する. // 移動先ページ名がある場合、実際に移動し表示されるまでディセーブルのままとする. updateBtnPanelState(); } } }; this.actPrev = new AbstractAction(strings.getProperty("btn.prev")) { private static final long serialVersionUID = 1L; public void actionPerformed(ActionEvent e) { setEnableButtons(false); String prevPanelName = doPrevious(); if (prevPanelName != null) { mainPanelLayout.show(mainPanel, prevPanelName); } else { updateBtnPanelState(); } } }; this.actFinish = new AbstractAction(strings.getProperty("btn.finish")) { private static final long serialVersionUID = 1L; public void actionPerformed(ActionEvent e) { onFinish(); } }; AbstractAction actCancel = new AbstractAction(strings.getProperty("btn.cancel")) { private static final long serialVersionUID = 1L; public void actionPerformed(ActionEvent e) { onClose(); } }; // ImportFileSelectPanel this.importFileSelectPanel = new ImportFileSelectPanel(); this.importFileSelectPanel.addComponentListener(componentListener); this.importFileSelectPanel.addChangeListener(changeListener); mainPanel.add(this.importFileSelectPanel, ImportFileSelectPanel.PANEL_NAME); // ImportTypeSelectPanel this.importTypeSelectPanel = new ImportTypeSelectPanel(); this.importTypeSelectPanel.addComponentListener(componentListener); this.importTypeSelectPanel.addChangeListener(changeListener); mainPanel.add(this.importTypeSelectPanel, ImportTypeSelectPanel.PANEL_NAME); // ImportPartsSelectPanel this.importPartsSelectPanel = new ImportPartsSelectPanel(); this.importPartsSelectPanel.addComponentListener(componentListener); this.importPartsSelectPanel.addChangeListener(changeListener); mainPanel.add(this.importPartsSelectPanel, ImportPartsSelectPanel.PANEL_NAME); // ImportPresetSelectPanel this.importPresetSelectPanel = new ImportPresetSelectPanel(); this.importPresetSelectPanel.addComponentListener(componentListener); this.importPresetSelectPanel.addChangeListener(changeListener); mainPanel.add(this.importPresetSelectPanel, ImportPresetSelectPanel.PANEL_NAME); // button panel JPanel btnPanel = new JPanel(); btnPanel.setBorder(BorderFactory.createEmptyBorder(3, 3, 3, 45)); GridBagLayout btnPanelLayout = new GridBagLayout(); btnPanel.setLayout(btnPanelLayout); actPrev.setEnabled(false); actNext.setEnabled(false); actFinish.setEnabled(false); GridBagConstraints gbc = new GridBagConstraints(); gbc.gridx = 0; gbc.gridy = 0; gbc.gridheight = 1; gbc.gridwidth = 1; gbc.anchor = GridBagConstraints.EAST; gbc.fill = GridBagConstraints.BOTH; gbc.ipadx = 0; gbc.ipady = 0; gbc.insets = new Insets(3, 3, 3, 3); gbc.weightx = 1.; gbc.weighty = 0.; btnPanel.add(Box.createHorizontalGlue(), gbc); gbc.gridx = Main.isLinuxOrMacOSX() ? 2 : 1; gbc.gridy = 0; gbc.weightx = 0.; btnPanel.add(new JButton(this.actPrev), gbc); gbc.gridx = Main.isLinuxOrMacOSX() ? 3 : 2; gbc.gridy = 0; JButton btnNext = new JButton(this.actNext); btnPanel.add(btnNext, gbc); gbc.gridx = Main.isLinuxOrMacOSX() ? 4 : 3; gbc.gridy = 0; btnPanel.add(new JButton(this.actFinish), gbc); gbc.gridx = Main.isLinuxOrMacOSX() ? 1 : 4; gbc.gridy = 0; JButton btnCancel = new JButton(actCancel); btnPanel.add(btnCancel, gbc); contentPane.add(btnPanel, BorderLayout.SOUTH); // インプットマップ/アクションマップ Toolkit tk = Toolkit.getDefaultToolkit(); JRootPane rootPane = getRootPane(); rootPane.setDefaultButton(btnNext); InputMap im = rootPane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW); ActionMap am = rootPane.getActionMap(); im.put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "closeImportWizDialog"); im.put(KeyStroke.getKeyStroke(KeyEvent.VK_W, tk.getMenuShortcutKeyMask()), "closeImportWizDialog"); am.put("closeImportWizDialog", actCancel); // 表示 setSize(500, 550); setLocationRelativeTo(parent); mainPanelLayout.first(mainPanel); updateBtnPanelState(); } protected void onComponentShown(JPanel panel) { ImportWizardCardPanel activePanel = (ImportWizardCardPanel) panel; activePanel.onActive(this, this.activePanel); this.activePanel = activePanel; updateBtnPanelState(); } protected void updateBtnPanelState() { if (activePanel != null) { actPrev.setEnabled(activePanel.isReadyPrevious()); actNext.setEnabled(activePanel.isReadyNext()); actFinish.setEnabled(activePanel.isReadyFinish()); } else { setEnableButtons(false); } } public void setEnableButtons(boolean enabled) { actPrev.setEnabled(enabled); actNext.setEnabled(enabled); actFinish.setEnabled(enabled); } protected String doNext() { if (activePanel == null) { throw new IllegalStateException(); } String nextPanelName; setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); try { nextPanelName = activePanel.doNext(); } finally { setCursor(Cursor.getDefaultCursor()); } return nextPanelName; } protected String doPrevious() { if (activePanel == null) { throw new IllegalStateException(); } return activePanel.doPrevious(); } protected void onClose() { Properties strings = LocalizedResourcePropertyLoader.getInstance() .getLocalizedProperties(STRINGS_RESOURCE); if (JOptionPane.showConfirmDialog(this, strings.getProperty("confirm.close"), strings.getProperty("confirm"), JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE) != JOptionPane.YES_OPTION) { return; } // アーカイブを閉じる. importFileSelectPanel.closeArchive(); // キャンセル this.exitCode = EXIT_CANCELED; this.importedCharacterData = null; // ウィンドウを閉じる dispose(); } /** * インポートの実行.
*/ protected void onFinish() { Properties strings = LocalizedResourcePropertyLoader.getInstance() .getLocalizedProperties(ImportWizardDialog.STRINGS_RESOURCE); try { // 新規プロファイル作成、または更新の実行 setEnableButtons(false); setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); int exitCode; CharacterData importedCharacterData; try { if (current == null) { // 新規作成 importedCharacterData = createNewProfile(); exitCode = EXIT_PROFILE_CREATED; } else { // 更新 importedCharacterData = updateProfile(); exitCode = EXIT_PROFILE_UPDATED; } } finally { setCursor(Cursor.getDefaultCursor()); } // アーカイブを閉じる importFileSelectPanel.closeArchive(); // 完了メッセージ JOptionPane.showMessageDialog(this, strings.getProperty("complete")); // 完了後、ウィンドウを閉じる. this.exitCode = exitCode; this.importedCharacterData = importedCharacterData; dispose(); } catch (Exception ex) { ErrorMessageHelper.showErrorDialog(this, ex); // ディセーブルにしていたボタンをパネルの状態に戻す. updateBtnPanelState(); } } /** * ウィザードが閉じられた場合の終了コード. * {@link #EXIT_PROFILE_UPDATED}であればプロファイルが更新されており,
* {@link #EXIT_PROFILE_CREATED}であればプロファイルが作成されている.
* {@link #EXIT_CANCELED}であればキャンセルされている.
* @return 終了コード */ public int getExitCode() { return exitCode; } /** * 新規または更新されたプロファイル、キャンセルされた場合はnull * @return プロファイル */ public CharacterData getImportedCharacterData() { return importedCharacterData; } /** * アーカイブからの新規プロファイルの作成 * @return 作成された新規プロファイル * @throws IOException 失敗 */ protected CharacterData createNewProfile() throws IOException { CharacterData cd = importModel.getCharacterData(); if (cd == null || !cd.isValid()) { throw new IllegalStateException("imported caharcer data is invalid." + cd); } CharacterDataPersistent persist = CharacterDataPersistent.getInstance(); CharacterData characterData = cd.duplicateBasicInfo(); // キャラクターセット名と作者名を設定する characterData.setName(importTypeSelectPanel.getCharacterName()); characterData.setAuthor(importTypeSelectPanel.getAuthor()); // プリセットをインポートする場合 characterData.clearPartsSets(false); if (importTypeSelectPanel.isImportPreset()) { for (PartsSet partsSet : importPresetSelectPanel.getSelectedPartsSets()) { PartsSet ps = partsSet.clone(); ps.setPresetParts(true); characterData.addPartsSet(ps); } characterData.setDefaultPartsSetId(importPresetSelectPanel.getPrefferedDefaultPartsSetId()); } // プロファイルの新規作成 // docBaseが設定されて返される. persist.createProfile(characterData); // インポートするパーツの更新 if (importTypeSelectPanel.isImportPartsImages()) { // パーツのコピー Collection partsImageContents = importPartsSelectPanel.getSelectedPartsImageContents(); importModel.copyPartsImageContents(partsImageContents, characterData); // パーツ管理情報の登録 PartsManageData partsManageData = importModel.getPartsManageData(); importModel.updatePartsManageData(partsImageContents, partsManageData, null, characterData); } // インポートするピクチャの更新 if (importTypeSelectPanel.isImportSampleImage()) { BufferedImage samplePicture = importModel.getSamplePicture(); if (samplePicture != null) { persist.saveSamplePicture(characterData, samplePicture); } } return characterData; } /** * プロファイルの更新 * @return 更新されたプロファイル * @throws IOException 失敗 */ protected CharacterData updateProfile() throws IOException { if (current == null || !current.isValid()) { throw new IllegalStateException("current profile is not valid. :" + current); } CharacterDataPersistent persist = CharacterDataPersistent.getInstance(); CharacterData characterData = current.duplicateBasicInfo(); boolean imported = false; boolean modCharacterDef = false; boolean modFavories = false; // インポートするパーツの更新 if (importTypeSelectPanel.isImportPartsImages()) { // パーツのコピー Collection partsImageContents = importPartsSelectPanel.getSelectedPartsImageContents(); importModel.copyPartsImageContents(partsImageContents, characterData); // パーツ管理情報の追記・更新 PartsManageData partsManageData = importModel.getPartsManageData(); importModel.updatePartsManageData(partsImageContents, partsManageData, characterData, characterData); imported = true; } // インポートするピクチャの更新 if (importTypeSelectPanel.isImportSampleImage()) { BufferedImage samplePicture = importModel.getSamplePicture(); if (samplePicture != null) { persist.saveSamplePicture(characterData, samplePicture); imported = true; } } // インポートするパーツセットの更新 if (importTypeSelectPanel.isImportPreset()) { for (PartsSet partsSet : importPresetSelectPanel.getSelectedPartsSets()) { PartsSet ps = partsSet.clone(); ps.setPresetParts(false); characterData.addPartsSet(ps); } imported = true; modCharacterDef = true; modFavories = true; } // 説明の更新 if (importTypeSelectPanel.isAddDescription() && imported) { URI archivedFile = importModel.getImportSource(); String note = importTypeSelectPanel.getAdditionalDescription(); if (note != null && note.length() > 0) { String description = characterData.getDescription(); if (description == null) { description = ""; } String lf = System.getProperty("line.separator"); Timestamp tm = new Timestamp(System.currentTimeMillis()); description += lf + "--- import: " + tm + " : " + archivedFile + " ---" + lf; description += note + lf; characterData.setDescription(description); modCharacterDef = true; } } // キャラクター定義の更新 if (modCharacterDef) { persist.updateProfile(characterData); // キャラクター定義の構造に変化なし current.setDescription(characterData.getDescription()); } // お気に入りの更新 if (modFavories) { persist.saveFavorites(characterData); } return characterData; } } /** * タブの抽象基底クラス.
* @author seraphy */ abstract class ImportWizardCardPanel extends JPanel { private static final long serialVersionUID = 1L; private LinkedList listeners = new LinkedList(); public void addChangeListener(ChangeListener l) { if (l != null) { listeners.add(l); } } public void removeChangeListener(ChangeListener l) { if (l != null) { listeners.remove(l); } } public void fireChangeEvent() { ChangeEvent e = new ChangeEvent(this); for (ChangeListener l : listeners) { l.stateChanged(e); } } public void onActive(ImportWizardDialog parent, ImportWizardCardPanel previousPanel) { // なにもしない } public boolean isReadyPrevious() { return false; } public boolean isReadyNext() { return false; } public boolean isReadyFinish() { return false; } public String doNext() { throw new UnsupportedOperationException(); } public String doPrevious() { throw new UnsupportedOperationException(); } } /** * ファイル選択パネル * @author seraphy */ class ImportFileSelectPanel extends ImportWizardCardPanel { private static final long serialVersionUID = 1L; public static final String PANEL_NAME = "fileSelectPanel"; /** * アーカイブ用ファイルダイアログ */ private static ArchiveFileDialog archiveFileDialog = new ArchiveFileDialog(); private ImportWizardDialog parent; /** * ファイル名を指定してインポート */ private JRadioButton radioArchiveFile; /** * ファイル名入力ボックス */ private JTextField txtArchiveFile; /** * ファイル選択ボタン */ private Action actChooseFile; /** * ディレクトリを指定してインポート */ private JRadioButton radioDirectory; /** * ディレクトリ入力ボックス */ private JTextField txtDirectory; /** * ディレクトリ選択ボタン */ private Action actChooseDirectory; /* 以下、対象ファイルの読み取り結果 */ public ImportFileSelectPanel() { setLayout(new BorderLayout()); Properties strings = LocalizedResourcePropertyLoader.getInstance() .getLocalizedProperties(ImportWizardDialog.STRINGS_RESOURCE); DocumentListener documentListener = new DocumentListener() { public void removeUpdate(DocumentEvent e) { fireEvent(); } public void insertUpdate(DocumentEvent e) { fireEvent(); } public void changedUpdate(DocumentEvent e) { fireEvent(); } protected void fireEvent() { fireChangeEvent(); } }; txtArchiveFile = new JTextField(); txtDirectory = new JTextField(); txtArchiveFile.getDocument().addDocumentListener(documentListener); txtDirectory.getDocument().addDocumentListener(documentListener); actChooseFile = new AbstractAction(strings.getProperty("browse")) { private static final long serialVersionUID = 1L; public void actionPerformed(ActionEvent e) { onChooseFile(); } }; actChooseDirectory = new AbstractAction(strings.getProperty("browse")) { private static final long serialVersionUID = 1L; public void actionPerformed(ActionEvent e) { onChooseDirectory(); } }; JPanel fileChoosePanel = new JPanel(); GridBagLayout fileChoosePanelLayout = new GridBagLayout(); fileChoosePanel.setLayout(fileChoosePanelLayout); radioArchiveFile = new JRadioButton(strings.getProperty("importingArchiveFile")); radioDirectory = new JRadioButton(strings.getProperty("importingDirectory")); ChangeListener radioChangeListener = new ChangeListener() { public void stateChanged(ChangeEvent e) { updateUIState(); fireChangeEvent(); } }; radioArchiveFile.addChangeListener(radioChangeListener); radioDirectory.addChangeListener(radioChangeListener); ButtonGroup btnGroup = new ButtonGroup(); btnGroup.add(radioArchiveFile); btnGroup.add(radioDirectory); // アーカイブからのインポートをデフォルトとする radioArchiveFile.setSelected(true); GridBagConstraints gbc = new GridBagConstraints(); gbc.gridx = 0; gbc.gridy = 0; gbc.weightx = 1.; gbc.weighty = 0.; gbc.gridheight = 1; gbc.gridwidth = 3; gbc.ipadx = 0; gbc.ipady = 0; gbc.insets = new Insets(3, 3, 3, 3); gbc.anchor = GridBagConstraints.WEST; gbc.fill = GridBagConstraints.BOTH; fileChoosePanel.add(radioArchiveFile, gbc); gbc.gridx = 0; gbc.gridy = 1; gbc.gridwidth = 1; gbc.ipadx = 45; gbc.weightx = 0; fileChoosePanel.add(Box.createHorizontalGlue(), gbc); gbc.gridx = 1; gbc.gridy = 1; gbc.ipadx = 0; gbc.weightx = 1.; fileChoosePanel.add(txtArchiveFile, gbc); gbc.gridx = 2; gbc.gridy = 1; gbc.ipadx = 0; gbc.weightx = 0.; fileChoosePanel.add(new JButton(actChooseFile), gbc); gbc.gridx = 0; gbc.gridy = 2; gbc.ipadx = 0; gbc.gridwidth = 3; gbc.weightx = 1.; fileChoosePanel.add(radioDirectory, gbc); gbc.gridx = 0; gbc.gridy = 3; gbc.ipadx = 45; gbc.gridwidth = 1; gbc.weightx = 0; fileChoosePanel.add(Box.createHorizontalGlue(), gbc); gbc.gridx = 1; gbc.gridy = 3; gbc.ipadx = 0; gbc.weightx = 1.; fileChoosePanel.add(txtDirectory, gbc); gbc.gridx = 2; gbc.gridy = 3; gbc.ipadx = 0; gbc.weightx = 0.; fileChoosePanel.add(new JButton(actChooseDirectory), gbc); gbc.gridx = 0; gbc.gridy = 4; gbc.ipadx = 0; gbc.gridwidth = 3; gbc.weightx = 1.; gbc.weighty = 1.; fileChoosePanel.add(Box.createGlue(), gbc); add(fileChoosePanel, BorderLayout.CENTER); // ドロップターゲット new DropTarget(this, new FileDropTarget() { @Override protected void onDropFiles(List dropFiles) { if (dropFiles == null || dropFiles.isEmpty()) { return; } setSelectFile(dropFiles); } @Override protected void onException(Exception ex) { ErrorMessageHelper.showErrorDialog(ImportFileSelectPanel.this, ex); } }); updateUIState(); } /** * アーカイブファイルまたはディレクトリを選択状態とする.
* nullの場合は選択を解除する. * @param dropFile アーカイブファイルまたはディレクトリ、もしくはnull */ public void setSelectFile(List dropFiles) { File dropFile = null; if (dropFiles != null && dropFiles.size() > 0) { dropFile = dropFiles.get(0); } if (dropFile == null) { // 選択なしの場合 txtDirectory.setText(""); txtArchiveFile.setText(""); radioDirectory.setSelected(false); radioArchiveFile.setSelected(false); } else if (dropFile.isDirectory()) { // ディレクトリの場合 txtDirectory.setText(dropFile.getPath()); radioDirectory.setSelected(true); } else if (dropFile.isFile()) { // ファイルの場合 txtArchiveFile.setText(dropFile.getPath()); radioArchiveFile.setSelected(true); } } protected void updateUIState() { boolean enableArchiveFile = radioArchiveFile.isSelected(); boolean enableDirectory = radioDirectory.isSelected(); txtArchiveFile.setEnabled(enableArchiveFile); actChooseFile.setEnabled(enableArchiveFile); txtDirectory.setEnabled(enableDirectory); actChooseDirectory.setEnabled(enableDirectory); } protected void onChooseFile() { File initFile = null; if (txtArchiveFile.getText().trim().length() > 0) { initFile = new File(txtArchiveFile.getText()); } File file = archiveFileDialog.showOpenDialog(this, initFile); if (file != null) { txtArchiveFile.setText(file.getPath()); fireChangeEvent(); } } protected void onChooseDirectory() { String directoryTxt = txtDirectory.getText(); JFileChooser dirChooser = new JFileChooser(directoryTxt); dirChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); if (dirChooser.showOpenDialog(this) != JFileChooser.APPROVE_OPTION) { return; } File dir = dirChooser.getSelectedFile(); if (dir != null) { txtDirectory.setText(dir.getPath()); fireChangeEvent(); } } @Override public boolean isReadyNext() { if (radioArchiveFile.isSelected()) { String fileTxt = txtArchiveFile.getText(); if (fileTxt != null && fileTxt.trim().length() > 0) { return true; } } else if (radioDirectory.isSelected()) { String directoryTxt = txtDirectory.getText(); if (directoryTxt != null && directoryTxt.trim().length() > 0) { return true; } } return false; } @Override public void onActive(ImportWizardDialog parent, ImportWizardCardPanel previousPanel) { this.parent = parent; // 開いているアーカイブがあれば閉じる closeArchive(); } /** * 開いているアーカイブがあればクローズする. */ public void closeArchive() { try { parent.importModel.closeImportSource(); } catch (IOException ex) { ErrorMessageHelper.showErrorDialog(this, ex); // エラーが発生しても、とりあえず無視する. } } @Override public String doNext() { if (!isReadyNext()) { return null; } Properties strings = LocalizedResourcePropertyLoader.getInstance() .getLocalizedProperties(ImportWizardDialog.STRINGS_RESOURCE); URI importArchive; if (radioArchiveFile.isSelected()) { // ファイルによるインポート File file = new File(txtArchiveFile.getText()); if (!file.exists() || !file.isFile()) { JOptionPane.showMessageDialog(this, strings .getProperty("fileNotFound"), "ERROR", JOptionPane.ERROR_MESSAGE); return null; } importArchive = file.toURI(); } else if (radioDirectory.isSelected()) { // ディレクトリによるインポート File file = new File(txtDirectory.getText()); if ( !file.exists() || !file.isDirectory()) { JOptionPane.showMessageDialog(this, strings .getProperty("directoryNotFound"), "ERROR", JOptionPane.ERROR_MESSAGE); return null; } importArchive = file.toURI(); } else { // それ以外はサポートしていない. return null; } try { parent.importModel.openImportSource(importArchive, parent.current); // ワーカースレッドでアーカイブの読み込みを行う. Worker worker = new Worker() { public Void doWork(ProgressHandle progressHandle) throws IOException { parent.importModel.loadContents(progressHandle); return null; } }; WorkerWithProgessDialog dlg = new WorkerWithProgessDialog(parent, worker); dlg.startAndWait(); // 読み込めたら次ページへ return ImportTypeSelectPanel.PANEL_NAME; } catch (WorkerException ex) { ErrorMessageHelper.showErrorDialog(this, ex.getCause()); } catch (Exception ex) { ErrorMessageHelper.showErrorDialog(this, ex); } return null; } } class URLTableRow implements Serializable { private static final long serialVersionUID = 3452190266438145049L; private String downloadURL; private String author; public String getAuthor() { return author; } public String getDownloadURL() { return downloadURL; } public void setAuthor(String author) { this.author = author; } public void setDownloadURL(String downloadURL) { this.downloadURL = downloadURL; } } class URLTableModel extends AbstractTableModelWithComboBoxModel { private static final long serialVersionUID = 7075478118793390224L; private static final String[] COLUMN_NAMES; private static final int[] COLUMN_WIDTHS; static { COLUMN_NAMES = new String[] { "作者", "URL", }; COLUMN_WIDTHS = new int[] { 100, 300, }; } @Override public String getColumnName(int column) { return COLUMN_NAMES[column]; } public int getColumnCount() { return COLUMN_NAMES.length; } public Object getValueAt(int rowIndex, int columnIndex) { URLTableRow row = getRow(rowIndex); switch (columnIndex) { case 0: return row.getAuthor(); case 1: return row.getDownloadURL(); } return ""; } @Override public Class getColumnClass(int columnIndex) { return String.class; } public void adjustColumnModel(TableColumnModel columnModel) { for (int idx = 0; idx < COLUMN_WIDTHS.length; idx++) { columnModel.getColumn(idx).setPreferredWidth(COLUMN_WIDTHS[idx]); } } public void initModel(CharacterData characterData) { clear(); HashMap downloadUrlsMap = new HashMap(); if (characterData != null) { for (PartsCategory category : characterData.getPartsCategories()) { for (Map.Entry entry : characterData .getPartsSpecMap(category).entrySet()) { PartsSpec partsSpec = entry.getValue(); String author = partsSpec.getAuthor(); String downloadURL = partsSpec.getDownloadURL(); if (downloadURL != null && downloadURL.trim().length() > 0) { if (author == null || author.trim().length() == 0) { author = ""; } downloadUrlsMap.put(downloadURL, author); } } } } for (Map.Entry entry : downloadUrlsMap.entrySet()) { String downloadURL = entry.getKey(); String author = entry.getValue(); URLTableRow row = new URLTableRow(); row.setDownloadURL(downloadURL); row.setAuthor(author); addRow(row); } Collections.sort(elements, new Comparator() { public int compare(URLTableRow o1, URLTableRow o2) { int ret = o1.getAuthor().compareTo(o2.getAuthor()); if (ret == 0) { ret = o1.getDownloadURL().compareTo(o2.getDownloadURL()); } return ret; } }); fireTableDataChanged(); } } /** * ファイル選択パネル * @author seraphy */ class ImportTypeSelectPanel extends ImportWizardCardPanel { private static final long serialVersionUID = 1L; public static final String PANEL_NAME = "importTypeSelectPanel"; private SamplePicturePanel samplePicturePanel; private JTextField txtCharacterId; private JTextField txtCharacterRev; private JTextField txtCharacterName; private JTextField txtAuthor; private JTextArea txtDescription; private JCheckBox chkPartsImages; private JCheckBox chkPresets; private JCheckBox chkSampleImage; private JCheckBox chkAddDescription; private String additionalDescription; /* 以下、選択結果 */ public ImportTypeSelectPanel() { Properties strings = LocalizedResourcePropertyLoader.getInstance() .getLocalizedProperties(ImportWizardDialog.STRINGS_RESOURCE); GridBagLayout basicPanelLayout = new GridBagLayout(); GridBagConstraints gbc = new GridBagConstraints(); setLayout(basicPanelLayout); JPanel contentsSpecPanel = new JPanel(); contentsSpecPanel.setBorder(BorderFactory.createCompoundBorder( BorderFactory.createEmptyBorder(5, 5, 5, 5), BorderFactory .createTitledBorder(strings.getProperty("basic.contentsSpec")))); BoxLayout contentsSpecPanelLayout = new BoxLayout(contentsSpecPanel, BoxLayout.PAGE_AXIS); contentsSpecPanel.setLayout(contentsSpecPanelLayout); chkPartsImages = new JCheckBox(strings.getProperty("basic.chk.partsImages")); chkPresets = new JCheckBox(strings.getProperty("basic.chk.presets")); chkSampleImage = new JCheckBox(strings.getProperty("basic.chk.samplePicture")); contentsSpecPanel.add(chkPartsImages); contentsSpecPanel.add(chkPresets); contentsSpecPanel.add(chkSampleImage); // JPanel archiveInfoPanel = new JPanel(); archiveInfoPanel.setBorder(BorderFactory.createCompoundBorder(BorderFactory .createEmptyBorder(5, 5, 5, 5), BorderFactory .createTitledBorder(strings.getProperty("basic.archiveInfo")))); Dimension archiveInfoPanelMinSize = new Dimension(300, 200); archiveInfoPanel.setMinimumSize(archiveInfoPanelMinSize); archiveInfoPanel.setPreferredSize(archiveInfoPanelMinSize); GridBagLayout commentPanelLayout = new GridBagLayout(); archiveInfoPanel.setLayout(commentPanelLayout); gbc.gridx = 0; gbc.gridy = 0; gbc.gridheight = 1; gbc.gridwidth = 2; gbc.weightx = 0.; gbc.weighty = 0.; gbc.insets = new Insets(3, 3, 3, 3); gbc.anchor = GridBagConstraints.WEST; gbc.fill = GridBagConstraints.BOTH; gbc.gridx = 0; gbc.gridy = 1; gbc.gridheight = 1; gbc.gridwidth = 1; gbc.weightx = 0.; gbc.weighty = 0.; archiveInfoPanel.add(new JLabel(strings.getProperty("basic.profileId"), JLabel.RIGHT), gbc); gbc.gridx = 1; gbc.gridy = 1; gbc.gridheight = 1; gbc.gridwidth = 1; gbc.weightx = 0.; gbc.weighty = 0.; txtCharacterId = new JTextField(); txtCharacterId.setEditable(false); // 読み取り専用 txtCharacterId.setEnabled(false); archiveInfoPanel.add(txtCharacterId, gbc); gbc.gridx = 0; gbc.gridy = 2; gbc.gridheight = 1; gbc.gridwidth = 1; gbc.weightx = 0.; gbc.weighty = 0.; archiveInfoPanel.add(new JLabel(strings.getProperty("basic.profileRev"), JLabel.RIGHT), gbc); gbc.gridx = 1; gbc.gridy = 2; gbc.gridheight = 1; gbc.gridwidth = 1; gbc.weightx = 0.; gbc.weighty = 0.; txtCharacterRev = new JTextField(); txtCharacterRev.setEditable(false); // 読み取り専用 txtCharacterRev.setEnabled(false); archiveInfoPanel.add(txtCharacterRev, gbc); gbc.gridx = 0; gbc.gridy = 3; gbc.gridheight = 1; gbc.gridwidth = 1; gbc.weightx = 0.; gbc.weighty = 0.; archiveInfoPanel.add(new JLabel(strings.getProperty("basic.profileName"), JLabel.RIGHT), gbc); gbc.gridx = 1; gbc.gridy = 3; gbc.gridheight = 1; gbc.gridwidth = 1; gbc.weightx = 0.; gbc.weighty = 0.; txtCharacterName = new JTextField(); archiveInfoPanel.add(txtCharacterName, gbc); gbc.gridx = 0; gbc.gridy = 4; gbc.gridheight = 1; gbc.gridwidth = 1; gbc.weightx = 0.; gbc.weighty = 0.; archiveInfoPanel.add( new JLabel(strings.getProperty("author"), JLabel.RIGHT), gbc); gbc.gridx = 1; gbc.gridy = 4; gbc.gridwidth = 1; gbc.weightx = 1.; txtAuthor = new JTextField(); archiveInfoPanel.add(txtAuthor, gbc); gbc.gridx = 0; gbc.gridy = 5; gbc.gridwidth = 1; gbc.gridheight = 1; gbc.weightx = 0.; archiveInfoPanel.add(new JLabel(strings.getProperty("description"), JLabel.RIGHT), gbc); gbc.gridx = 1; gbc.gridy = 5; gbc.gridwidth = 1; gbc.gridheight = 5; gbc.weighty = 1.; gbc.weightx = 1.; txtDescription = new JTextArea(); // 説明は更新可能にしておく。 archiveInfoPanel.add(new JScrollPane(txtDescription), gbc); gbc.gridx = 0; gbc.gridy = 10; gbc.gridheight = 1; gbc.gridwidth = 2; gbc.weightx = 0.; gbc.weighty = 0.; gbc.weighty = 0.; gbc.weightx = 0.; chkAddDescription = new JCheckBox(strings.getProperty("appendDescription")); archiveInfoPanel.add(chkAddDescription, gbc); // / samplePicturePanel = new SamplePicturePanel(); JScrollPane samplePicturePanelSP = new JScrollPane(samplePicturePanel); samplePicturePanelSP.setBorder(null); JPanel samplePictureTitledPanel = new JPanel(new BorderLayout()); samplePictureTitledPanel.add(samplePicturePanelSP, BorderLayout.CENTER); samplePictureTitledPanel.setBorder(BorderFactory.createCompoundBorder( BorderFactory.createEmptyBorder(5, 5, 5, 5), BorderFactory .createTitledBorder(strings .getProperty("basic.sampleImage")))); // / gbc.gridx = 0; gbc.gridy = 0; gbc.gridheight = 1; gbc.gridwidth = 2; gbc.weightx = 1.; gbc.weighty = 0.; gbc.anchor = GridBagConstraints.WEST; gbc.fill = GridBagConstraints.BOTH; add(contentsSpecPanel, gbc); gbc.gridx = 0; gbc.gridy = 1; gbc.gridheight = 1; gbc.gridwidth = 1; gbc.weightx = 0.; gbc.weighty = 1.; gbc.anchor = GridBagConstraints.WEST; gbc.fill = GridBagConstraints.BOTH; add(archiveInfoPanel, gbc); gbc.gridx = 1; gbc.gridy = 1; gbc.gridheight = 1; gbc.gridwidth = 1; gbc.weightx = 1; gbc.weighty = 1.; gbc.anchor = GridBagConstraints.WEST; gbc.fill = GridBagConstraints.BOTH; add(samplePictureTitledPanel, gbc); // アクションリスナ ActionListener modListener = new ActionListener() { public void actionPerformed(ActionEvent e) { fireChangeEvent(); } }; chkPartsImages.addActionListener(modListener); chkPresets.addActionListener(modListener); chkSampleImage.addActionListener(modListener); chkAddDescription.addActionListener(modListener); } @Override public void onActive(ImportWizardDialog parent, ImportWizardCardPanel previousPanel) { if (previousPanel == parent.importPartsSelectPanel) { return; } Properties strings = LocalizedResourcePropertyLoader.getInstance() .getLocalizedProperties(ImportWizardDialog.STRINGS_RESOURCE); // 呼び出しもと情報 CharacterData current = parent.current; // キャラクター定義情報 CharacterData cd = parent.importModel.getCharacterData(); String readme; // 開いているか選択しているプロファイルが有効であれば更新可能 final boolean updatable = (current != null && current.isValid()); // 新規の場合でインポートもとが有効なキャラクターセットであれば作成可能 final boolean creatable = (current == null && cd != null && cd.isValid()); // 新規作成の場合はキャラクター定義名と作者名を更新可能とする txtCharacterName.setEnabled(current == null); txtCharacterName.setEditable(current == null); txtAuthor.setEditable(current == null); txtAuthor.setEnabled(current == null); // ID、REVが一致するか? boolean matchID = false; boolean matchREV = false; if (cd != null && cd.isValid()) { txtCharacterId.setText(cd.getId()); txtCharacterRev.setText(cd.getRev()); txtCharacterName.setText(cd.getName()); if (current != null) { // 既存のプロファイルを選択していてインポート結果のキャラクター定義がある場合はID, REVを比較する. matchID = current.getId() == null ? cd.getId() == null : current.getId().equals(cd.getId()); matchREV = current.getRev() == null ? cd.getRev() == null : current.getRev().equals(cd.getRev()); } else { // 既存のプロファイルが存在しない場合は、ID,REVの比較は成功とみなす matchID = true; matchREV = true; } AppConfig appConfig = AppConfig.getInstance(); Color invalidBgColor = appConfig.getInvalidBgColor(); txtCharacterId.setBackground(matchID ? getBackground() : invalidBgColor); txtCharacterRev.setBackground(matchREV ? getBackground() : invalidBgColor); txtAuthor.setText(cd.getAuthor()); readme = cd.getDescription(); } else { // ID, REV等は存在しないので空にする txtCharacterId.setText(""); txtCharacterRev.setText(""); txtCharacterName.setText(""); txtAuthor.setText(""); // readmeで代用 readme = parent.importModel.getReadme(); } // 説明を追記する. boolean existsReadme = (readme != null && readme.trim().length() > 0); additionalDescription = existsReadme ? readme : ""; txtDescription.setText(additionalDescription); chkAddDescription.setEnabled((updatable || creatable) && existsReadme); chkAddDescription.setSelected((updatable || creatable) && existsReadme); // プリセットまたはお気に入りが存在するか? boolean hasPresetOrFavorites = (cd == null) ? false : !cd.getPartsSets().isEmpty(); chkPresets.setEnabled(hasPresetOrFavorites); chkPresets.setSelected(hasPresetOrFavorites); // パーツイメージ Collection partsImageContentsMap = parent.importModel.getPartsImageContents(); // パーツが存在するか? boolean hasParts = !partsImageContentsMap.isEmpty(); chkPartsImages.setEnabled(hasParts); chkPartsImages.setSelected(hasParts); // サンプルピクチャ BufferedImage samplePicture = parent.importModel.getSamplePicture(); if (samplePicture != null && (updatable || creatable)) { // サンプルピクチャが存在し、インポートか新規作成が可能であれば有効にする. samplePicturePanel.setSamplePicture(samplePicture); chkSampleImage.setEnabled(true); chkSampleImage.setSelected(current == null); // 新規作成の場合のみデフォルトでON } else { samplePicturePanel.setSamplePicture(samplePicture); chkSampleImage.setEnabled(false); chkSampleImage.setSelected(false); } // パーツまたはお気に入り・プリセットが存在する場合、 // および、新規の場合はキャラクター定義が存在する場合はコンテンツ有り boolean hasContents = hasParts || hasPresetOrFavorites || (current == null && cd != null && cd.isValid()); if (!hasContents) { JOptionPane.showMessageDialog(this, strings.getProperty("noContents")); } else if (cd == null) { JOptionPane.showMessageDialog(this, strings.getProperty("notFormalArchive")); } else if (!matchID) { JOptionPane.showMessageDialog(this, strings.getProperty("unmatchedProfileId")); } else if (!matchREV) { JOptionPane.showMessageDialog(this, strings.getProperty("unmatchedProfileRev")); } } public boolean isImportPreset() { return chkPresets.isSelected(); } public boolean isImportPartsImages() { return chkPartsImages.isSelected(); } public boolean isImportSampleImage() { return chkSampleImage.isSelected(); } public boolean isAddDescription() { return chkAddDescription.isSelected(); } /** * 説明として追加するドキュメント.
* これはユーザーが編集可能であり、ユーザー編集後の値が取得される.
* @return 説明として追加するドキュメント */ public String getAdditionalDescription() { return txtDescription.getText(); } /** * キャラクター定義名を取得する. * @return キャラクター定義名 */ public String getCharacterName() { return txtCharacterName.getText(); } /** * 作者名を取得する. * @return 作者名 */ public String getAuthor() { return txtAuthor.getText(); } @Override public boolean isReadyPrevious() { return true; } @Override public String doPrevious() { return ImportFileSelectPanel.PANEL_NAME; } @Override public boolean isReadyNext() { if (isImportPartsImages() || isImportPreset()) { // パーツイメージの選択もしくはパーツセットの選択を指定している場合は次へ進む return true; } return false; } @Override public boolean isReadyFinish() { if (!isImportPartsImages() && !isImportPreset()) { if (isImportSampleImage()) { // 新規プロファイル作成かサンプルイメージの更新のみでイメージもパーツセットもいらなければ、ただちに作成可能. return true; } } return false; } @Override public String doNext() { return ImportPartsSelectPanel.PANEL_NAME; } } /** * パーツ選択パネル * @author seraphy */ class ImportPartsSelectPanel extends ImportWizardCardPanel { private static final long serialVersionUID = 1L; public static final String PANEL_NAME = "importPartsSelectPanel"; private ImportWizardDialog parent; private ImportPartsTableModel partsTableModel; private JPanel profileSizePanel; private JTextField txtProfileHeight; private int profileWidth; private int profileHeight; private JTextField txtProfileWidth; private JTable partsTable; private Action actSelectAll; private Action actDeselectAll; private Action actSort; private Action actSortByTimestamp; public ImportPartsSelectPanel() { Properties strings = LocalizedResourcePropertyLoader.getInstance() .getLocalizedProperties(ImportWizardDialog.STRINGS_RESOURCE); setLayout(new BorderLayout()); profileSizePanel = new JPanel(); GridBagLayout profileSizePanelLayout = new GridBagLayout(); profileSizePanel.setLayout(profileSizePanelLayout); profileSizePanel.setBorder(BorderFactory.createTitledBorder("プロファイルのサイズ")); GridBagConstraints gbc = new GridBagConstraints(); gbc.gridx = 0; gbc.gridy = 0; gbc.gridheight = 1; gbc.gridwidth = 1; gbc.anchor = GridBagConstraints.EAST; gbc.fill = GridBagConstraints.BOTH; gbc.insets = new Insets(3, 3, 3, 3); gbc.weightx = 0.; gbc.weighty = 0.; gbc.ipadx = 0; gbc.ipady = 0; profileSizePanel.add(new JLabel("幅:", JLabel.RIGHT), gbc); txtProfileWidth = new JTextField(); txtProfileWidth.setEditable(false); gbc.gridx = 1; gbc.gridy = 0; profileSizePanel.add(txtProfileWidth, gbc); gbc.gridx = 2; gbc.gridy = 0; profileSizePanel.add(new JLabel("高さ:", JLabel.RIGHT), gbc); txtProfileHeight = new JTextField(); txtProfileHeight.setEditable(false); gbc.gridx = 3; gbc.gridy = 0; profileSizePanel.add(txtProfileHeight, gbc); gbc.gridx = 4; gbc.gridy = 0; gbc.weightx = 1.; profileSizePanel.add(Box.createHorizontalGlue(), gbc); add(profileSizePanel, BorderLayout.NORTH); partsTableModel = new ImportPartsTableModel(); partsTableModel.addTableModelListener(new TableModelListener() { public void tableChanged(TableModelEvent e) { fireChangeEvent(); } }); AppConfig appConfig = AppConfig.getInstance(); final Color disabledForeground = appConfig.getDisabledCellForgroundColor(); partsTable = new JTable(partsTableModel) { private static final long serialVersionUID = 1L; @Override public Component prepareRenderer(TableCellRenderer renderer, int row, int column) { Component comp = super.prepareRenderer(renderer, row, column); if (comp instanceof JCheckBox) { // BooleanのデフォルトのレンダラーはJCheckBoxを継承したJTable$BooleanRenderer comp.setEnabled(isCellEditable(row, column) && isEnabled()); } // 行モデル取得 ImportPartsTableModel model = (ImportPartsTableModel) getModel(); ImportPartsModel rowModel = model.getRow(row); Long lastModifiedAtCur = rowModel.getLastModifiedAtCurrentProfile(); if (lastModifiedAtCur != null) { // 既存のパーツが存在すれば太字 comp.setFont(getFont().deriveFont(Font.BOLD)); } else { // 新規パーツであれば通常フォント comp.setFont(getFont()); } // 列ごとの警告の判定 boolean warnings = false; if (column == ImportPartsTableModel.COLUMN_LASTMODIFIED) { // 既存のほうが日付が新しければワーニング if (lastModifiedAtCur != null && rowModel.getLastModified() < lastModifiedAtCur.longValue()) { warnings = true; } } else if (column == ImportPartsTableModel.COLUMN_ALPHA) { // アルファ情報がない画像は警告 if (!rowModel.isAlphaColor()) { warnings = true; } } else if (column == ImportPartsTableModel.COLUMN_SIZE) { // プロファイルの画像サイズと一致しないか、不揃いな画像であれば警告 if (rowModel.isUnmatchedSize() || profileWidth != rowModel.getWidth() || profileHeight != rowModel.getHeight()) { warnings = true; } } // 前景色、ディセーブル時は灰色 Color foregroundColor = isCellSelected(row, column) ? getSelectionForeground() : getForeground(); comp.setForeground(isEnabled() ? foregroundColor : disabledForeground); // 背景色、警告行は赤色に if (warnings) { AppConfig appConfig = AppConfig.getInstance(); Color invalidBgColor = appConfig.getInvalidBgColor(); comp.setBackground(invalidBgColor); } else { if (isCellSelected(row, column)) { comp.setBackground(getSelectionBackground()); } else { comp.setBackground(getBackground()); } } return comp; } }; partsTable.setAutoResizeMode(JTable.AUTO_RESIZE_OFF); partsTable.setShowGrid(true); partsTable.setGridColor(appConfig.getGridColor()); partsTable.putClientProperty("terminateEditOnFocusLost", Boolean.TRUE); partsTableModel.adjustColumnModel(partsTable.getColumnModel()); partsTable.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); partsTable.setRowSelectionAllowed(true); Action actPartsSetCheck = new AbstractAction(strings.getProperty("parts.popup.check")) { private static final long serialVersionUID = 1L; public void actionPerformed(ActionEvent e) { int[] selRows = partsTable.getSelectedRows(); partsTableModel.setCheck(selRows, true); } }; Action actPartsUnsetCheck = new AbstractAction(strings.getProperty("parts.popup.uncheck")) { private static final long serialVersionUID = 1L; public void actionPerformed(ActionEvent e) { int[] selRows = partsTable.getSelectedRows(); partsTableModel.setCheck(selRows, false); } }; final JPopupMenu partsTablePopupMenu = new JPopupMenu(); partsTablePopupMenu.add(actPartsSetCheck); partsTablePopupMenu.add(actPartsUnsetCheck); partsTable.setComponentPopupMenu(partsTablePopupMenu); JScrollPane partsTableSP = new JScrollPane(partsTable); partsTableSP.setBorder(null); JPanel partsTableTitledPanel = new JPanel(new BorderLayout()); partsTableTitledPanel.add(partsTableSP, BorderLayout.CENTER); partsTableTitledPanel.setBorder(BorderFactory.createTitledBorder(strings .getProperty("parts.title"))); add(partsTableTitledPanel, BorderLayout.CENTER); actSelectAll = new AbstractAction(strings .getProperty("parts.btn.selectAll")) { private static final long serialVersionUID = 1L; public void actionPerformed(ActionEvent e) { onSelectAll(); } }; actDeselectAll = new AbstractAction(strings .getProperty("parts.btn.deselectAll")) { private static final long serialVersionUID = 1L; public void actionPerformed(ActionEvent e) { onDeselectAll(); } }; actSort = new AbstractAction(strings.getProperty("parts.btn.sort")) { private static final long serialVersionUID = 1L; public void actionPerformed(ActionEvent e) { onSort(); } }; actSortByTimestamp = new AbstractAction(strings .getProperty("parts.btn.sortByTimestamp")) { private static final long serialVersionUID = 1L; public void actionPerformed(ActionEvent e) { onSortByTimestamp(); } }; JPanel btnPanel = new JPanel(); GridBagLayout btnPanelLayout = new GridBagLayout(); btnPanel.setLayout(btnPanelLayout); gbc.gridx = 0; gbc.gridy = 0; gbc.gridheight = 1; gbc.gridwidth = 1; gbc.anchor = GridBagConstraints.EAST; gbc.fill = GridBagConstraints.BOTH; gbc.insets = new Insets(3, 3, 3, 3); gbc.ipadx = 0; gbc.ipady = 0; JButton btnSelectAll = new JButton(actSelectAll); btnPanel.add(btnSelectAll, gbc); gbc.gridx = 1; gbc.gridy = 0; JButton btnDeselectAll = new JButton(actDeselectAll); btnPanel.add(btnDeselectAll, gbc); gbc.gridx = 2; gbc.gridy = 0; JButton btnSort = new JButton(actSort); btnPanel.add(btnSort, gbc); gbc.gridx = 3; gbc.gridy = 0; JButton btnSortByTimestamp = new JButton(actSortByTimestamp); btnPanel.add(btnSortByTimestamp, gbc); gbc.gridx = 4; gbc.gridy = 0; gbc.weightx = 1.; btnPanel.add(Box.createHorizontalGlue(), gbc); add(btnPanel, BorderLayout.SOUTH); } @Override public void onActive(ImportWizardDialog parent, ImportWizardCardPanel previousPanel) { this.parent = parent; if (previousPanel == parent.importPresetSelectPanel) { return; } // インポート対象のプロファイルサイズ CharacterData characterData; if (parent.current == null) { // 新規インポート characterData = parent.importModel.getCharacterData(); } else { // 更新インポート characterData = parent.current; } int profileWidth = 0; int profileHeight = 0; if (characterData != null) { Dimension imageSize = characterData.getImageSize(); if (imageSize != null) { profileWidth = imageSize.width; profileHeight = imageSize.height; } } txtProfileWidth.setText(Integer.toString(profileWidth)); txtProfileHeight.setText(Integer.toString(profileHeight)); profileSizePanel.revalidate(); this.profileHeight = profileHeight; this.profileWidth = profileWidth; // パーツのインポート指定があれば編集可能に、そうでなければ表示のみ // (パーツセットのインポートの確認のため、パーツ一覧は表示できるようにしておく) boolean enabled = parent.importTypeSelectPanel.isImportPartsImages(); partsTable.setEnabled(enabled); actDeselectAll.setEnabled(enabled); actSelectAll.setEnabled(enabled); actSort.setEnabled(enabled); actSortByTimestamp.setEnabled(enabled); CharacterData currentProfile = parent.current; Collection partsImageContents = parent.importModel.getPartsImageContents(); PartsManageData partsManageData = parent.importModel.getPartsManageData(); partsTableModel.initModel(partsImageContents, partsManageData, currentProfile); // プリセットのモデルも更新する. Collection partsSets = null; if (parent.importTypeSelectPanel.isImportPreset()) { CharacterData cd = parent.importModel.getCharacterData(); if (cd != null && cd.isValid()) { partsSets = cd.getPartsSets().values(); } } String defaultPartsSetId; CharacterData presetImportTarget; if (parent.current == null) { presetImportTarget = null; CharacterData cd = parent.importModel.getCharacterData(); if (cd != null) { defaultPartsSetId = cd.getDefaultPartsSetId(); } else { defaultPartsSetId = null; } } else { presetImportTarget = parent.current; defaultPartsSetId = null; // 既存の場合はデフォルトのパーツセットであるかは表示する必要ないのでnullにする. } parent.importPresetSelectPanel.initModel(partsSets, defaultPartsSetId, presetImportTarget); } @Override public boolean isReadyPrevious() { return true; } @Override public String doPrevious() { this.partsTableModel.clear(); return ImportTypeSelectPanel.PANEL_NAME; } @Override public boolean isReadyNext() { if (this.parent != null) { if (this.parent.importTypeSelectPanel.isImportPreset()) { // パーツセットのインポート指定があれば次へ return true; } } return false; } @Override public boolean isReadyFinish() { if (this.parent != null) { if (this.parent.importTypeSelectPanel.isImportPartsImages() && !this.parent.importTypeSelectPanel.isImportPreset()) { // パーツセットのインポート指定がなければ可 return true; } } return false; } public String doNext() { return ImportPresetSelectPanel.PANEL_NAME; }; protected void onSelectAll() { partsTableModel.selectAll(); } protected void onDeselectAll() { partsTableModel.deselectAll(); } protected void onSort() { partsTableModel.sort(); if (partsTableModel.getRowCount() > 0) { Rectangle rct = partsTable.getCellRect(0, 0, true); partsTable.scrollRectToVisible(rct); } } protected void onSortByTimestamp() { partsTableModel.sortByTimestamp(); if (partsTableModel.getRowCount() > 0) { Rectangle rct = partsTable.getCellRect(0, 0, true); partsTable.scrollRectToVisible(rct); } } /** * 選択されたイメージコンテンツのコレクション.
* @return 選択されたイメージコンテンツのコレクション、なければ空 */ public Collection getSelectedPartsImageContents() { return partsTableModel.getSelectedPartsImageContents(); } /** * すでにプロファイルに登録済みのパーツ識別子、および、これからインポートする予定の選択されたパーツ識別子のコレクション.
* @return インポートされた、またはインポートするパーツ識別子のコレクション.なければ空. */ public Collection getImportedPartsIdentifiers() { HashSet partsIdentifiers = new HashSet(); partsIdentifiers.addAll(partsTableModel.getCurrentProfilePartsIdentifers()); partsIdentifiers.addAll(partsTableModel.getSelectedPartsIdentifiers()); return partsIdentifiers; } public void selectByPartsIdentifiers(Collection partsIdentifiers) { partsTableModel.selectByPartsIdentifiers(partsIdentifiers); } } /** * 同じパーツ名をもつイメージのコレクション.
* パーツの各レイヤーの集合を想定する.
* @author seraphy */ class ImportPartsImageSet extends AbstractCollection { /** * パーツ名 */ private String partsName; /** * 各レイヤー */ private ArrayList contentSet = new ArrayList(); private Long lastModified; private int width; private int height; private boolean unmatchedSize; private boolean alphaColor; private Collection partsCategories; private boolean checked; public ImportPartsImageSet(String partsName) { if (partsName == null || partsName.length() == 0) { throw new IllegalArgumentException(); } this.partsName = partsName; } public String getPartsName() { return partsName; } @Override public int size() { return contentSet.size(); } public Iterator iterator() { return contentSet.iterator(); } @Override public boolean add(PartsImageContent o) { if (o == null) { throw new IllegalArgumentException(); } if (!partsName.equals(o.getPartsName())) { throw new IllegalArgumentException(); } lastModified = null; // リセットする. return contentSet.add(o); } public int getWidth() { recheck(); return width; } public int getHeight() { recheck(); return height; } public boolean isUnmatchedSize() { recheck(); return unmatchedSize; } public boolean isAlphaColor() { recheck(); return alphaColor; } public long lastModified() { recheck(); return lastModified.longValue(); } public Collection getPartsCategories() { recheck(); return this.partsCategories; } protected void recheck() { if (lastModified != null) { return; } long lastModified = 0; int maxWidth = 0; int maxHeight = 0; int minWidth = 0; int minHeight = 0; boolean alphaColor = !this.contentSet.isEmpty(); HashSet partsCategories = new HashSet(); for (PartsImageContent partsImageContent : this.contentSet) { PNGFileImageHeader header = partsImageContent.getPngFileImageHeader(); maxWidth = Math.max(maxWidth, header.getWidth()); maxHeight = Math.max(maxHeight, header.getHeight()); minWidth = Math.max(minWidth, header.getWidth()); minHeight = Math.max(minHeight, header.getHeight()); if (header.getColorType() != 6 && !header.hasTransparencyInformation()) { // TrueColor + Alpha (6)か、アルファ情報があるもの以外はアルファなしとする. alphaColor = false; } for (CategoryLayerPair clPair : partsImageContent.getCategoryLayerPairs()) { partsCategories.add(clPair.getPartsCategory()); } long tm = partsImageContent.lastModified(); lastModified = Math.max(lastModified, tm); } this.lastModified = Long.valueOf(lastModified); this.width = maxWidth; this.height = maxHeight; this.unmatchedSize = (minWidth != maxWidth) || (minHeight != maxHeight); this.alphaColor = alphaColor; this.partsCategories = Collections.unmodifiableCollection(partsCategories); } public void setChecked(boolean checked) { this.checked = checked; } public boolean isChecked() { return checked; } } class ImportPartsModel { private PartsIdentifier partsIdentifier; private PartsAuthorInfo authorInfo; private PartsManageData.PartsVersionInfo versionInfo; private PartsSpec partsSpecAtCurrent; private ImportPartsImageSet imageSet; private int numOfLink; private Long lastModifiedAtCurrentProfile; /** * 行モデルを構築する * @param partsIdentifier パーツ識別子 * @param authorInfo 作者情報(なければnull) * @param versionInfo バージョン情報(なければnull) * @param imageSet イメージファイルのセット * @param numOfLink カテゴリの参照カウント数(複数カテゴリに参照される場合は2以上となる) */ public ImportPartsModel(PartsIdentifier partsIdentifier, PartsAuthorInfo authorInfo, PartsManageData.PartsVersionInfo versionInfo, PartsSpec partsSpecAtCurrent, ImportPartsImageSet imageSet, int numOfLink) { if (partsIdentifier == null || imageSet == null) { throw new IllegalArgumentException(); } this.partsIdentifier = partsIdentifier; this.authorInfo = authorInfo; this.versionInfo = versionInfo; this.partsSpecAtCurrent = partsSpecAtCurrent; this.imageSet = imageSet; this.numOfLink = numOfLink; if (partsSpecAtCurrent != null) { lastModifiedAtCurrentProfile = Long.valueOf(partsSpecAtCurrent .getPartsFiles().lastModified()); } else { lastModifiedAtCurrentProfile = null; } } public int getNumOfLink() { return numOfLink; } public PartsIdentifier getPartsIdentifier() { return partsIdentifier; } public ImportPartsImageSet getImageSet() { return imageSet; } public String getPartsName() { return partsIdentifier.getLocalizedPartsName(); } public String getAuthor() { if (authorInfo != null) { return authorInfo.getAuthor(); } return null; } public String getAuthorAtCurrent() { if (partsSpecAtCurrent != null) { PartsAuthorInfo partsAuthorInfo = partsSpecAtCurrent.getAuthorInfo(); if (partsAuthorInfo != null) { return partsAuthorInfo.getAuthor(); } } return null; } public double getVersion() { if (versionInfo != null) { return versionInfo.getVersion(); } return 0; } public double getVersionAtCurrent() { if (partsSpecAtCurrent != null) { return partsSpecAtCurrent.getVersion(); } return 0; } public PartsCategory getPartsCategory() { return partsIdentifier.getPartsCategory(); } public void setChecked(boolean checked) { imageSet.setChecked(checked); } public boolean isChecked() { return imageSet.isChecked(); } public int getWidth() { return imageSet.getWidth(); } public int getHeight() { return imageSet.getHeight(); } public boolean isUnmatchedSize() { return imageSet.isUnmatchedSize(); } public boolean isAlphaColor() { return imageSet.isAlphaColor(); } public long getLastModified() { return imageSet.lastModified(); } public Long getLastModifiedAtCurrentProfile() { return lastModifiedAtCurrentProfile; } } class ImportPartsTableModel extends AbstractTableModelWithComboBoxModel { private static final long serialVersionUID = 1L; private static final String[] COLUMN_NAMES; private static final int[] COLUMN_WIDTHS; public static final int COLUMN_LASTMODIFIED = 5; public static final int COLUMN_ALPHA = 4; public static final int COLUMN_SIZE = 3; private Set currentProfilePartsIdentifiers; static { Properties strings = LocalizedResourcePropertyLoader.getInstance() .getLocalizedProperties(ImportWizardDialog.STRINGS_RESOURCE); COLUMN_NAMES = new String[] { strings.getProperty("parts.column.check"), strings.getProperty("parts.column.partsname"), strings.getProperty("parts.column.category"), strings.getProperty("parts.column.imagesize"), strings.getProperty("parts.column.alpha"), strings.getProperty("parts.column.lastmodified"), strings.getProperty("parts.column.org-lastmodified"), strings.getProperty("parts.column.author"), strings.getProperty("parts.column.org-author"), strings.getProperty("parts.column.version"), strings.getProperty("parts.column.org-version"), }; COLUMN_WIDTHS = new int[] { Integer.parseInt(strings.getProperty("parts.column.check.size")), Integer.parseInt(strings.getProperty("parts.column.partsname.size")), Integer.parseInt(strings.getProperty("parts.column.category.size")), Integer.parseInt(strings.getProperty("parts.column.imagesize.size")), Integer.parseInt(strings.getProperty("parts.column.alpha.size")), Integer.parseInt(strings.getProperty("parts.column.lastmodified.size")), Integer.parseInt(strings.getProperty("parts.column.org-lastmodified.size")), Integer.parseInt(strings.getProperty("parts.column.author.size")), Integer.parseInt(strings.getProperty("parts.column.org-author.size")), Integer.parseInt(strings.getProperty("parts.column.version.size")), Integer.parseInt(strings.getProperty("parts.column.org-version.size")), }; } /** * モデルを初期化する.
* @param partsImageContents インポートもとアーカイブに含まれる、全パーツイメージコンテンツ * @param currentProfile インポート先のプロファイル、現在プロファイルが既に持っているパーツを取得するためのもの。 */ public void initModel(Collection partsImageContents, PartsManageData partsManageData, CharacterData currentProfile) { clear(); if (partsImageContents == null || partsManageData == null) { return; } // 現在のプロファイルが所有する全パーツ一覧を構築する. // 現在のプロファイルがなければ空. HashSet currentProfilePartsIdentifiers = new HashSet(); if (currentProfile != null) { for (PartsCategory partsCategory : currentProfile.getPartsCategories()) { currentProfilePartsIdentifiers.addAll(currentProfile.getPartsSpecMap(partsCategory).keySet()); } } this.currentProfilePartsIdentifiers = Collections.unmodifiableSet(currentProfilePartsIdentifiers); // 同じパーツ名をもつ各レイヤーを集める HashMap partsImageSets = new HashMap(); for (PartsImageContent content : partsImageContents) { String partsName = content.getPartsName(); ImportPartsImageSet partsImageSet = partsImageSets.get(partsName); if (partsImageSet == null) { partsImageSet = new ImportPartsImageSet(partsName); partsImageSets.put(partsName, partsImageSet); } partsImageSet.add(content); } // 名前順に並び替える ArrayList partsNames = new ArrayList(partsImageSets.keySet()); Collections.sort(partsNames); // 登録する for (String partsName : partsNames) { ImportPartsImageSet partsImageSet = partsImageSets.get(partsName); int numOfLink = partsImageSet.getPartsCategories().size(); for (PartsCategory partsCategory : partsImageSet.getPartsCategories()) { // パーツ管理情報の索引キー PartsManageData.PartsKey partsKey = new PartsManageData.PartsKey(partsName, partsCategory.getCategoryId()); // ローカライズされたパーツ名があれば取得する。なければオリジナルのまま String localizedPartsName = partsManageData.getLocalizedName(partsKey); if (localizedPartsName == null || localizedPartsName.length() == 0) { localizedPartsName = partsName; } // 作者情報・バージョン情報があれば取得する. PartsAuthorInfo partsAuthorInfo = partsManageData.getPartsAuthorInfo(partsKey); PartsManageData.PartsVersionInfo versionInfo = partsManageData.getVersion(partsKey); // パーツ識別子を構築する PartsIdentifier partsIdentifier = new PartsIdentifier(partsCategory, partsName, localizedPartsName); // 現在のプロファイル上のパーツ情報を取得する.(なければnull) PartsSpec partsSpec; if (currentProfile != null) { partsSpec = currentProfile.getPartsSpec(partsIdentifier); } else { partsSpec = null; } // 行モデルを構築する. ImportPartsModel rowModel = new ImportPartsModel( partsIdentifier, partsAuthorInfo, versionInfo, partsSpec, partsImageSet, numOfLink); addRow(rowModel); } } // 既存がないか、既存よりも新しい日付であれば自動的にチェックを設定する. // もしくはバージョンが上であれば自動的にチェックをつける. for (ImportPartsModel rowModel : elements) { // 現在のプロファイル上のファイル群の最終更新日 Long lastModifiedAtCurrent = rowModel.getLastModifiedAtCurrentProfile(); if (lastModifiedAtCurrent == null) { lastModifiedAtCurrent = Long.valueOf(0); } // インポートするファイル群の最終更新日 ImportPartsImageSet partsImageSet = rowModel.getImageSet(); // 新しければ自動的にチェックをつける. if (lastModifiedAtCurrent.longValue() < partsImageSet.lastModified()) { partsImageSet.setChecked(true); } // バージョンが新しければチェックをつける. (改変版や作者名改名もあるので、作者名が同一であるかは問わない.) double versionAtCurrent = rowModel.getVersionAtCurrent(); double version = rowModel.getVersion(); if (versionAtCurrent < version) { partsImageSet.setChecked(true); } } // 並び替え sort(); } /** * 選択されているパーツを構成するファイルのコレクションを返します.
* @return パーツイメージコンテンツのコレクション、選択がなければ空 */ public Collection getSelectedPartsImageContents() { IdentityHashMap partsImageSets = new IdentityHashMap(); for (ImportPartsModel rowModel : elements) { ImportPartsImageSet partsImageSet = rowModel.getImageSet(); if (partsImageSet.isChecked()) { partsImageSets.put(partsImageSet, partsImageSet); } } ArrayList partsImageContents = new ArrayList(); for (ImportPartsImageSet partsImageSet : partsImageSets.values()) { partsImageContents.addAll(partsImageSet); } return partsImageContents; } /** * 選択されているパーツ識別子のコレクションを返します.
* 返されるコレクションには同一のパーツ識別子が複数存在しないことが保証されます.
* 一つも選択がない場合は空が返されます.
* @return パーツ識別子のコレクション.
*/ public Collection getSelectedPartsIdentifiers() { HashSet partsIdentifiers = new HashSet(); for (ImportPartsModel rowModel : elements) { if (rowModel.isChecked()) { partsIdentifiers.add(rowModel.getPartsIdentifier()); } } return partsIdentifiers; } /** * 現在のプロファイルが所有している全パーツの識別子.
* 現在のプロファイルがないか、まったく所有していなければ空.
* @return 現在のプロファイルが所有するパーツの識別子のコレクション.(重複しない一意であることが保証される.) */ public Collection getCurrentProfilePartsIdentifers() { return currentProfilePartsIdentifiers; } public int getColumnCount() { return COLUMN_NAMES.length; } @Override public String getColumnName(int column) { return COLUMN_NAMES[column]; } public Object getValueAt(int rowIndex, int columnIndex) { ImportPartsModel rowModel = getRow(rowIndex); switch (columnIndex) { case 0: return rowModel.isChecked(); case 1: return rowModel.getPartsName(); case 2: return rowModel.getPartsCategory().getLocalizedCategoryName(); case 3: return rowModel.getWidth() + "x" + rowModel.getHeight() + (rowModel.isUnmatchedSize() ? "*" : ""); case 4: return rowModel.isAlphaColor(); case 5: long lastModified = rowModel.getLastModified(); if (lastModified > 0) { return new Timestamp(lastModified).toString(); } return ""; case 6: Long lastModifiedAtCur = rowModel.getLastModifiedAtCurrentProfile(); if (lastModifiedAtCur != null && lastModifiedAtCur.longValue() > 0) { return new Timestamp(lastModifiedAtCur.longValue()).toString(); } return ""; case 7: return rowModel.getAuthor(); case 8: return rowModel.getAuthorAtCurrent(); case 9: double version = rowModel.getVersion(); if (version > 0) { return Double.toString(version); } return ""; case 10: double versionAtCurrent = rowModel.getVersionAtCurrent(); if (versionAtCurrent > 0) { return Double.toString(versionAtCurrent); } return ""; } return ""; } @Override public void setValueAt(Object aValue, int rowIndex, int columnIndex) { ImportPartsModel rowModel = getRow(rowIndex); switch (columnIndex) { case 0: rowModel.setChecked(((Boolean) aValue).booleanValue()); break; default: return; } if (rowModel.getNumOfLink() > 1) { fireTableDataChanged(); } else { fireListUpdated(rowIndex, rowIndex); } } @Override public Class getColumnClass(int columnIndex) { switch (columnIndex) { case 0: return Boolean.class; case 1: return String.class; case 2: return String.class; case 3: return String.class; case 4: return Boolean.class; case 5: return String.class; case 6: return String.class; case 7: return String.class; case 8: return String.class; case 9: return String.class; case 10: return String.class; } return String.class; } @Override public boolean isCellEditable(int rowIndex, int columnIndex) { if (columnIndex == 0) { return true; } return false; } public void adjustColumnModel(TableColumnModel columnModel) { int mx = columnModel.getColumnCount(); for (int idx = 0; idx < mx; idx++) { columnModel.getColumn(idx).setWidth(COLUMN_WIDTHS[idx]); } } public void selectAll() { boolean modified = false; for (ImportPartsModel rowModel : elements) { if (!rowModel.isChecked()) { rowModel.setChecked(true); modified = true; } } if (modified) { fireTableDataChanged(); } } public void deselectAll() { boolean modified = false; for (ImportPartsModel rowModel : elements) { if (rowModel.isChecked()) { rowModel.setChecked(false); modified = true; } } if (modified) { fireTableDataChanged(); } } public void sort() { Collections.sort(elements, new Comparator () { public int compare(ImportPartsModel o1, ImportPartsModel o2) { int ret = (o1.isChecked() ? 0 : 1) - (o2.isChecked() ? 0 : 1); if (ret == 0) { ret = o1.getPartsIdentifier().compareTo(o2.getPartsIdentifier()); } return ret; } }); fireTableDataChanged(); } public void sortByTimestamp() { Collections.sort(elements, new Comparator () { public int compare(ImportPartsModel o1, ImportPartsModel o2) { long ret = (o1.isChecked() ? 0 : 1) - (o2.isChecked() ? 0 : 1); if (ret == 0) { Long tm1 = o1.getLastModifiedAtCurrentProfile(); Long tm2 = o2.getLastModifiedAtCurrentProfile(); long lastModified1 = Math.max(o1.getLastModified(), tm1 == null ? 0 : tm1.longValue()); long lastModified2 = Math.max(o2.getLastModified(), tm2 == null ? 0 : tm2.longValue()); ret = lastModified1 - lastModified2; } if (ret == 0) { ret = o1.getPartsIdentifier().compareTo(o2.getPartsIdentifier()); } return ret == 0 ? 0 : ret > 0 ? 1 : -1; } }); fireTableDataChanged(); } /** * 指定したパーツ識別子をチェック状態にする. * @param partsIdentifiers パーツ識別子のコレクション、nullの場合は何もしない. */ public void selectByPartsIdentifiers(Collection partsIdentifiers) { boolean modified = false; if (partsIdentifiers != null) { for (PartsIdentifier partsIdentifier : partsIdentifiers) { for (ImportPartsModel rowModel : elements) { if (rowModel.getPartsIdentifier().equals(partsIdentifier)) { if (!rowModel.isChecked()) { rowModel.setChecked(true); modified = true; } } } } } if (modified) { fireTableDataChanged(); } } public void setCheck(int[] selRows, boolean checked) { if (selRows == null || selRows.length == 0) { return; } Arrays.sort(selRows); for (int selRow : selRows) { ImportPartsModel rowModel = getRow(selRow); rowModel.setChecked(checked); } fireTableRowsUpdated(selRows[0], selRows[selRows.length - 1]); } } /** * プリセット選択パネル * @author seraphy */ class ImportPresetSelectPanel extends ImportWizardCardPanel { private static final long serialVersionUID = 1L; public static final String PANEL_NAME = "importPresetSelectPanel"; private ImportPresetTableModel presetTableModel; private ImportWizardDialog parent; private JTable presetTable; private Action actSelectAll; private Action actDeselectAll; private Action actSelectUsedParts; public ImportPresetSelectPanel() { Properties strings = LocalizedResourcePropertyLoader.getInstance() .getLocalizedProperties(ImportWizardDialog.STRINGS_RESOURCE); setBorder(BorderFactory.createTitledBorder(strings.getProperty("preset.title"))); setLayout(new BorderLayout()); presetTableModel = new ImportPresetTableModel(); presetTableModel.addTableModelListener(new TableModelListener() { public void tableChanged(TableModelEvent e) { if (e.getType() == TableModelEvent.UPDATE) { fireChangeEvent(); } } }); AppConfig appConfig = AppConfig.getInstance(); final Color warningForegroundColor = appConfig.getExportPresetWarningsForegroundColor(); final Color disabledForeground = appConfig.getDisabledCellForgroundColor(); presetTable = new JTable(presetTableModel) { private static final long serialVersionUID = 1L; @Override public Component prepareRenderer(TableCellRenderer renderer, int row, int column) { Component comp = super.prepareRenderer(renderer, row, column); if (comp instanceof JCheckBox) { // BooleanのデフォルトのレンダラーはJCheckBoxを継承したJTable$BooleanRenderer comp.setEnabled(isCellEditable(row, column) && isEnabled()); } ImportPresetModel presetModel = presetTableModel.getRow(row); // インポート先のプリセットを上書きする場合、もしくはデフォルトのパーツセットの場合は太字にする. if (presetModel.isOverwrite() || presetTableModel.isDefaultPartsSet(row)) { comp.setFont(getFont().deriveFont(Font.BOLD)); } else { comp.setFont(getFont()); } // インポートするプリセットのパーツが不足している場合、警告色にする. if (!isEnabled()) { comp.setForeground(disabledForeground); } else { if (presetModel.isCheched() && presetModel.getMissingPartsIdentifiers().size() > 0) { comp.setForeground(warningForegroundColor); } else { comp.setForeground(getForeground()); } } return comp; } }; presetTable.setShowGrid(true); presetTable.setGridColor(appConfig.getGridColor()); presetTable.putClientProperty("terminateEditOnFocusLost", Boolean.TRUE); actSelectUsedParts = new AbstractAction(strings.getProperty("preset.popup.selectUsedParts")) { private static final long serialVersionUID = 1L; public void actionPerformed(ActionEvent e) { exportUsedParts(); } }; final JPopupMenu popupMenu = new JPopupMenu(); popupMenu.add(actSelectUsedParts); presetTable.setComponentPopupMenu(popupMenu); presetTable.setAutoResizeMode(JTable.AUTO_RESIZE_OFF); presetTableModel.adjustColumnModel(presetTable.getColumnModel()); add(new JScrollPane(presetTable), BorderLayout.CENTER); actSelectAll = new AbstractAction(strings.getProperty("parts.btn.selectAll")) { private static final long serialVersionUID = 1L; public void actionPerformed(ActionEvent e) { onSelectAll(); } }; actDeselectAll = new AbstractAction(strings.getProperty("parts.btn.deselectAll")) { private static final long serialVersionUID = 1L; public void actionPerformed(ActionEvent e) { onDeselectAll(); } }; Action actSort = new AbstractAction(strings .getProperty("parts.btn.sort")) { private static final long serialVersionUID = 1L; public void actionPerformed(ActionEvent e) { onSort(); } }; JPanel btnPanel = new JPanel(); GridBagLayout btnPanelLayout = new GridBagLayout(); btnPanel.setLayout(btnPanelLayout); GridBagConstraints gbc = new GridBagConstraints(); gbc.gridx = 0; gbc.gridy = 0; gbc.gridheight = 1; gbc.gridwidth = 1; gbc.anchor = GridBagConstraints.EAST; gbc.fill = GridBagConstraints.BOTH; gbc.insets = new Insets(3, 3, 3, 3); gbc.ipadx = 0; gbc.ipady = 0; JButton btnSelectAll = new JButton(actSelectAll); btnPanel.add(btnSelectAll, gbc); gbc.gridx = 1; gbc.gridy = 0; JButton btnDeselectAll = new JButton(actDeselectAll); btnPanel.add(btnDeselectAll, gbc); gbc.gridx = 2; gbc.gridy = 0; JButton btnSort = new JButton(actSort); btnPanel.add(btnSort, gbc); gbc.gridx = 3; gbc.gridy = 0; gbc.weightx = 1.; btnPanel.add(Box.createHorizontalGlue(), gbc); add(btnPanel, BorderLayout.SOUTH); } @Override public void onActive(ImportWizardDialog parent, ImportWizardCardPanel previousPanel) { this.parent= parent; actSelectUsedParts.setEnabled(parent.importTypeSelectPanel.isImportPartsImages()); checkMissingParts(); } public void checkMissingParts() { Collection importedPartsIdentifiers = this.parent.importPartsSelectPanel.getImportedPartsIdentifiers(); presetTableModel.checkMissingParts(importedPartsIdentifiers); } protected void onSelectAll() { presetTableModel.selectAll(); } protected void onDeselectAll() { presetTableModel.deselectAll(); } protected void onSort() { presetTableModel.sort(); if (presetTableModel.getRowCount() > 0) { Rectangle rct = presetTable.getCellRect(0, 0, true); presetTable.scrollRectToVisible(rct); } } protected void exportUsedParts() { ArrayList requirePartsIdentifiers = new ArrayList(); int[] selRows = presetTable.getSelectedRows(); for (int selRow : selRows) { ImportPresetModel presetModel = presetTableModel.getRow(selRow); PartsSet partsSet = presetModel.getPartsSet(); for (List partsIdentifiers : partsSet.values()) { for (PartsIdentifier partsIdentifier : partsIdentifiers) { requirePartsIdentifiers.add(partsIdentifier); } } } this.parent.importPartsSelectPanel.selectByPartsIdentifiers(requirePartsIdentifiers); checkMissingParts(); } @Override public boolean isReadyPrevious() { return true; } @Override public boolean isReadyNext() { return false; } @Override public boolean isReadyFinish() { if (this.parent != null) { return true; } return false; } @Override public String doPrevious() { return ImportPartsSelectPanel.PANEL_NAME; } @Override public String doNext() { return null; } public Collection getSelectedPartsSets() { return presetTableModel.getSelectedPartsSets(); } /** * デフォルトのパーツセットIDとして使用されることが推奨されるパーツセットIDを取得する.
* 明示的なデフォルトのパーツセットIDがなければ、もしくは、 * 明示的に指定されているパーツセットIDが選択されているパーツセットの中になければ、 * 選択されているパーツセットの最初のアイテムを返す.
* 選択しているパーツセットが一つもなければnullを返す.
* @return デフォルトのパーツセット */ public String getPrefferedDefaultPartsSetId() { String defaultPartsSetId = presetTableModel.getDefaultPartsSetId(); String firstPartsSetId = null; boolean existsDefaultPartsSetId = false; for (PartsSet partsSet : getSelectedPartsSets()) { if (firstPartsSetId == null) { firstPartsSetId = partsSet.getPartsSetId(); } if (partsSet.getPartsSetId().equals(defaultPartsSetId)) { existsDefaultPartsSetId = true; } } if (!existsDefaultPartsSetId || defaultPartsSetId == null || defaultPartsSetId.length() == 0) { defaultPartsSetId = firstPartsSetId; } return defaultPartsSetId; } public void initModel(Collection partsSets, String defaultPartsSetId, CharacterData presetImportTarget) { presetTableModel.initModel(partsSets, defaultPartsSetId, presetImportTarget); } } class ImportPresetModel { private boolean cheched; private PartsSet partsSet; private boolean overwrite; private Collection missingPartsIdentifiers = Collections.emptySet(); public ImportPresetModel(PartsSet partsSet, boolean overwrite, boolean checked) { if (partsSet == null) { throw new IllegalArgumentException(); } this.partsSet = partsSet; this.cheched = checked; this.overwrite = overwrite; } public boolean isCheched() { return cheched; } public void setCheched(boolean cheched) { this.cheched = cheched; } public PartsSet getPartsSet() { return partsSet; } public String getPartsSetName() { return partsSet.getLocalizedName(); } public void setPartsSetName(String name) { if (name == null || name.trim().length() == 0) { throw new IllegalArgumentException(); } partsSet.setLocalizedName(name); } public Collection getMissingPartsIdentifiers() { return missingPartsIdentifiers; } public boolean hasMissingParts() { return true; } public boolean isOverwrite() { return overwrite; } public boolean checkMissingParts(Collection importedPartsIdentifiers) { HashSet missingPartsIdentifiers = new HashSet(); for (List partsIdentifiers : partsSet.values()) { for (PartsIdentifier partsIdentifier : partsIdentifiers) { boolean exists = false; if (importedPartsIdentifiers != null && importedPartsIdentifiers.contains(partsIdentifier)) { exists = true; } if (!exists) { missingPartsIdentifiers.add(partsIdentifier); } } } boolean modified = (!missingPartsIdentifiers.equals(this.missingPartsIdentifiers)); if (modified) { this.missingPartsIdentifiers = missingPartsIdentifiers; } return modified; } } class ImportPresetTableModel extends AbstractTableModelWithComboBoxModel { private static final long serialVersionUID = 1L; private static final String[] COLUMN_NAMES; // = {"選択", "プリセット名", "不足するパーツ"}; private static final int[] COLUMN_WIDTHS; // = {50, 100, 200}; static { Properties strings = LocalizedResourcePropertyLoader.getInstance() .getLocalizedProperties(ImportWizardDialog.STRINGS_RESOURCE); COLUMN_NAMES = new String[] { strings.getProperty("preset.column.check"), strings.getProperty("preset.column.name"), strings.getProperty("preset.column.missings"), }; COLUMN_WIDTHS = new int[] { Integer.parseInt(strings.getProperty("preset.column.check.size")), Integer.parseInt(strings.getProperty("preset.column.name.size")), Integer.parseInt(strings.getProperty("preset.column.missings.size")), }; } private String defaultPartsSetId; public String getDefaultPartsSetId() { return defaultPartsSetId; } public void setDefaultPartsSetId(String defaultPartsSetId) { this.defaultPartsSetId = defaultPartsSetId; } public int getColumnCount() { return COLUMN_NAMES.length; } @Override public String getColumnName(int column) { return COLUMN_NAMES[column]; } @Override public Class getColumnClass(int columnIndex) { switch (columnIndex) { case 0: return Boolean.class; case 1: return String.class; case 2: return String.class; } return String.class; } @Override public boolean isCellEditable(int rowIndex, int columnIndex) { if (columnIndex == 0 || columnIndex == 1) { return true; } return false; } public Object getValueAt(int rowIndex, int columnIndex) { ImportPresetModel rowModel = getRow(rowIndex); switch (columnIndex) { case 0: return rowModel.isCheched(); case 1: return rowModel.getPartsSetName(); case 2: return getMissingPartsIdentifiersString(rowModel); } return ""; } private String getMissingPartsIdentifiersString(ImportPresetModel rowModel) { StringBuilder buf = new StringBuilder(); for (PartsIdentifier partsIdentifier : rowModel.getMissingPartsIdentifiers()) { if (buf.length() > 0) { buf.append(", "); } buf.append(partsIdentifier.getLocalizedPartsName()); } return buf.toString(); } @Override public void setValueAt(Object aValue, int rowIndex, int columnIndex) { ImportPresetModel rowModel = getRow(rowIndex); switch (columnIndex) { case 0: rowModel.setCheched(((Boolean) aValue).booleanValue()); break; case 1: String name = (String) aValue; name = (name != null) ? name.trim() : ""; if (name.length() > 0) { rowModel.setPartsSetName(name); } default: return; } fireTableRowsUpdated(rowIndex, rowIndex); } /** * 指定した行のパーツセットがデフォルトパーツセットであるか? * @param rowIndex 行インデックス * @return デフォルトパーツセットであればtrue、そうでなければfalse */ public boolean isDefaultPartsSet(int rowIndex) { ImportPresetModel rowModel = getRow(rowIndex); return rowModel.getPartsSet().getPartsSetId().equals(defaultPartsSetId); } public void adjustColumnModel(TableColumnModel columnModel) { int mx = columnModel.getColumnCount(); for (int idx = 0; idx < mx; idx++) { columnModel.getColumn(idx).setWidth(COLUMN_WIDTHS[idx]); } } /** * パーツセットリストを構築する.
* @param partsSets 登録するパーツセット * @param defaultPartsSetId デフォルトのパーツセットID、なければnull * @param presetImportTarget インポート先、新規の場合はnull (上書き判定のため) */ public void initModel(Collection partsSets, String defaultPartsSetId, CharacterData presetImportTarget) { clear(); if (partsSets == null) { return; } // インポート先の既存のパーツセット Map currentProfilesPartsSet; if (presetImportTarget != null) { currentProfilesPartsSet = presetImportTarget.getPartsSets(); } else { // 新規の場合は既存パーツセットは空. currentProfilesPartsSet = Collections.emptyMap(); } // インポートもとのパーツセットをテープルモデルに登録する. for (PartsSet partsSet : partsSets) { String partsSetId = partsSet.getPartsSetId(); if (partsSetId == null || partsSetId.length() == 0) { continue; } PartsSet compatiblePartsSet; if (presetImportTarget != null) { // 既存のキャラクター定義へのインポート時は、パーツセットのカテゴリを合わせる. // 一つもカテゴリが合わない場合は空のパーツセットになる. compatiblePartsSet = partsSet.createCompatible(presetImportTarget); } else { compatiblePartsSet = partsSet; // 新規の場合はフィッティングの必要なし. } if (!compatiblePartsSet.isEmpty()) { // 空のパーツセットは登録対象にしない. boolean overwrite = currentProfilesPartsSet.containsKey(partsSetId); boolean checked = (presetImportTarget == null); // 新規の場合は既定で選択状態とする. ImportPresetModel rowModel = new ImportPresetModel(partsSet, overwrite, checked); addRow(rowModel); } } // デフォルトのパーツセットIDを設定、存在しない場合はnull this.defaultPartsSetId = defaultPartsSetId; sort(); } public Collection getSelectedPartsSets() { ArrayList partsSets = new ArrayList(); for (ImportPresetModel rowModel : elements) { if (rowModel.isCheched()) { partsSets.add(rowModel.getPartsSet()); } } return partsSets; } public void selectAll() { boolean modified = false; for (ImportPresetModel rowModel : elements) { if (!rowModel.isCheched()) { rowModel.setCheched(true); modified = true; } } if (modified) { fireTableDataChanged(); } } public void deselectAll() { boolean modified = false; for (ImportPresetModel rowModel : elements) { if (rowModel.isCheched()) { rowModel.setCheched(false); modified = true; } } if (modified) { fireTableDataChanged(); } } public void sort() { Collections.sort(elements, new Comparator() { public int compare(ImportPresetModel o1, ImportPresetModel o2) { int ret = (o1.isCheched() ? 0 : 1) - (o2.isCheched() ? 0 : 1); if (ret == 0) { ret = o1.getPartsSetName().compareTo(o2.getPartsSetName()); } return ret; } }); fireTableDataChanged(); } public void checkMissingParts(Collection importedPartsIdentifiers) { boolean changed = false; for (ImportPresetModel rowModel : elements) { if (rowModel.checkMissingParts(importedPartsIdentifiers)) { changed = true; } } if (changed) { fireTableDataChanged(); } } } charactermanaj/src/charactermanaj/ui/AboutBox.java0000644000175000017500000002501011767047542022501 0ustar paulliupaulliupackage charactermanaj.ui; import java.awt.BorderLayout; import java.awt.Dimension; import java.awt.event.ActionEvent; import java.io.File; import java.io.FileOutputStream; import java.io.OutputStreamWriter; import java.io.Writer; import java.net.URL; import java.nio.charset.Charset; import java.text.DecimalFormat; import java.util.Enumeration; import java.util.Map; import java.util.Properties; import java.util.TreeMap; import javax.swing.AbstractAction; import javax.swing.BorderFactory; import javax.swing.JButton; import javax.swing.JEditorPane; import javax.swing.JFileChooser; import javax.swing.JFrame; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JTabbedPane; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import javax.swing.event.HyperlinkEvent; import javax.swing.event.HyperlinkListener; import charactermanaj.model.AppConfig; import charactermanaj.util.DesktopUtilities; import charactermanaj.util.DirectoryConfig; import charactermanaj.util.ErrorMessageHelper; import charactermanaj.util.LocalizedResourceTextLoader; import charactermanaj.util.SystemUtil; /** * Aboutボックスを表示する. * @author seraphy */ public class AboutBox { private JFrame parent; public AboutBox(JFrame parent) { if (parent == null) { throw new IllegalArgumentException(); } this.parent = parent; } /** * Aboutボックスを表示する. */ public void showAboutBox() { final JTabbedPane tabs = new JTabbedPane(); tabs.setPreferredSize(new Dimension(500, 400)); final JPanel aboutPanel = createAboutPanel(); final JSysInfoPanel sysInfoPanel = new JSysInfoPanel() { private static final long serialVersionUID = 1L; @Override protected void onGc() { super.onGc(); setText(getSysInfoText()); } }; tabs.addTab("About", aboutPanel); tabs.addTab("System", sysInfoPanel); tabs.addChangeListener(new ChangeListener() { public void stateChanged(ChangeEvent e) { if (tabs.getSelectedIndex() == 1) { sysInfoPanel.setText(getSysInfoText()); } } }); JOptionPane.showMessageDialog(parent, tabs, "About", JOptionPane.INFORMATION_MESSAGE); } protected JPanel createAboutPanel() { String message = LocalizedResourceTextLoader.getInstance().getText("appinfo/about.html", Charset.forName("UTF-8")); AppConfig appConfig = AppConfig.getInstance(); String versionInfo = appConfig.getImplementationVersion(); String specificationVersionInfo = appConfig.getSpecificationVersion(); message = message.replace("@@IMPLEMENTS-VERSIONINFO@@", versionInfo); message = message.replace("@@SPECIFICATION-VERSIONINFO@@", specificationVersionInfo); JPanel aboutPanel = new JPanel(new BorderLayout()); JEditorPane editorPane = new JEditorPane(); editorPane.addHyperlinkListener(new HyperlinkListener() { public void hyperlinkUpdate(HyperlinkEvent e) { if (e.getEventType().equals(HyperlinkEvent.EventType.ACTIVATED)) { URL url = e.getURL(); if (url != null) { try { if (!DesktopUtilities.browse(url.toURI())) { JOptionPane.showMessageDialog(parent, url.toString()); } } catch (Exception ex) { ErrorMessageHelper.showErrorDialog(parent, ex); } } } } }); editorPane.setEditable(false); editorPane.putClientProperty(JEditorPane.HONOR_DISPLAY_PROPERTIES, Boolean.TRUE); editorPane.setContentType("text/html"); editorPane.setText(message); editorPane.setSelectionStart(0); editorPane.setSelectionEnd(0); aboutPanel.add(new JScrollPane(editorPane), BorderLayout.CENTER); return aboutPanel; } /** * システム情報を取得してHTML形式の文字列として返す.
* ランタイム情報、システムプロパティ情報、環境変数情報を取得する.
* @return システム情報のHTML文字列 */ protected String getSysInfoText() { // ランタイム情報の取得 long freeMem, totalMem, maxMem; Runtime rt = Runtime.getRuntime(); totalMem = rt.totalMemory() / 1024; freeMem = rt.freeMemory() / 1024; maxMem = rt.maxMemory() / 1024; DecimalFormat decimalFmt = new DecimalFormat("#,###,##0"); StringBuilder buf = new StringBuilder(); buf.append(""); buf.append("

Runtime Information

"); buf.append(""); buf.append(""); buf.append(""); buf.append(""); buf.append("
Max Memory:" + decimalFmt.format(maxMem) + " KiB
Total Memory:" + decimalFmt.format(totalMem) + " KiB
Free Memory:" + decimalFmt.format(freeMem) + " KiB
"); // キャラクターデータベースの取得 DirectoryConfig dirConfig = DirectoryConfig.getInstance(); String charactersDir = null; try { charactersDir = dirConfig.getCharactersDir().toString(); } catch (RuntimeException ex) { charactersDir = "**INVALID**"; } buf.append("

Character Database

"); buf.append(""); // buf.append(""); buf.append(""); buf.append("
System" + appConfig.getSystemCharactersDir() + "
User" + escape(charactersDir) + "
"); // サポートしているエンコーディングの列挙 buf.append("

Available Charsets

"); Charset defaultCharset = Charset.defaultCharset(); StringBuilder bufChars = new StringBuilder(); boolean foundWin31j = false; bufChars.append(""); for (Map.Entry entry : Charset.availableCharsets().entrySet()) { String name = entry.getKey(); Charset charset = entry.getValue(); boolean isDef = charset.equals(defaultCharset); boolean win31j = name.toLowerCase().equals("windows-31j"); StringBuilder aliasBuf = new StringBuilder(); for (String alias : charset.aliases()) { if (aliasBuf.length() > 0) { aliasBuf.append(", "); } aliasBuf.append(alias); if (alias.toLowerCase().equals("cswindows31j")) { win31j = true; } } foundWin31j = foundWin31j || win31j; bufChars.append(""); } bufChars.append("
"); if (isDef || win31j) { bufChars.append(""); } if (win31j) { bufChars.append(""); } bufChars.append(name); if (win31j) { bufChars.append(""); } if (isDef || win31j) { bufChars.append("*"); } bufChars.append("" + aliasBuf.toString() + "
"); if (!foundWin31j) { buf.append("

This system is not supporting Japanese.

"); } buf.append(bufChars.toString()); // システムプロパティの取得 buf.append("

System Properties

"); try { Properties sysprops = System.getProperties(); Enumeration enmKey = sysprops.keys(); TreeMap propMap = new TreeMap(); // プロパティキーのアルファベッド順にソート while (enmKey.hasMoreElements()) { String key = (String) enmKey.nextElement(); String value = sysprops.getProperty(key); propMap.put(key, value == null ? "null" : value); } for (Map.Entry entry : propMap.entrySet()) { buf.append(""); buf.append(""); buf.append(""); } buf.append("
" + escape(entry.getKey()) + "" + escape(entry.getValue()) + "
"); } catch (Exception ex) { buf.append(escape(ex.toString())); } // 環境変数の取得 buf.append("

System Environments

"); try { TreeMap envMap = new TreeMap(); // 環境変数名のアルファベット順にソート envMap.putAll(System.getenv()); buf.append(""); for (Map.Entry entry : envMap.entrySet()) { buf.append(""); buf.append(""); buf.append(""); } buf.append("
" + escape(entry.getKey()) + "" + escape(entry.getValue()) + "
"); } catch (Exception ex) { buf.append(escape(ex.toString())); } // HTMLとして文字列を返す. buf.append(""); return buf.toString(); } protected String escape(String value) { if (value == null) { return null; } value = value.replace("&", "&"); value = value.replace("<", "<"); value = value.replace(">", ">"); return value; } } class JSysInfoPanel extends JPanel { private static final long serialVersionUID = 1L; private JEditorPane editorPane; public JSysInfoPanel() { super(new BorderLayout()); editorPane = new JEditorPane(); editorPane.setEditable(false); editorPane.putClientProperty(JEditorPane.HONOR_DISPLAY_PROPERTIES, Boolean.TRUE); editorPane.setContentType("text/html"); editorPane.setText(""); JButton btnSave = new JButton(new AbstractAction("save") { private static final long serialVersionUID = 1L; public void actionPerformed(ActionEvent e) { onSave(); } }); JButton btnGc = new JButton(new AbstractAction("garbage collect") { private static final long serialVersionUID = 1L; public void actionPerformed(ActionEvent e) { onGc(); } }); JPanel btnPanel = new JPanel(new BorderLayout()); btnPanel.setBorder(BorderFactory.createEmptyBorder(3, 3, 3, 3)); btnPanel.add(btnSave, BorderLayout.EAST); btnPanel.add(btnGc, BorderLayout.WEST); add(new JScrollPane(editorPane), BorderLayout.CENTER); add(btnPanel, BorderLayout.SOUTH); } public void setText(String message) { editorPane.setText(message); editorPane.setSelectionStart(0); editorPane.setSelectionEnd(0); } protected void onGc() { SystemUtil.gc(); } protected void onSave() { JFileChooser chooser = new JFileChooser(); chooser.setSelectedFile(new File("sysinfo.html")); if (chooser.showSaveDialog(this) != JFileChooser.APPROVE_OPTION) { return; } File file = chooser.getSelectedFile(); try { FileOutputStream os = new FileOutputStream(file); try { Writer wr = new OutputStreamWriter(os, Charset.forName("UTF-8")); try { wr.write(editorPane.getText()); wr.flush(); } finally { wr.close(); } } finally { os.close(); } } catch (Exception ex) { ErrorMessageHelper.showErrorDialog(this, ex); } } } charactermanaj/src/charactermanaj/ui/ManageFavoriteDialog.java0000644000175000017500000001627411767047542025002 0ustar paulliupaulliupackage charactermanaj.ui; import java.awt.BorderLayout; import java.awt.Component; import java.awt.Container; import java.awt.Dimension; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.Toolkit; import java.awt.event.ActionEvent; import java.awt.event.KeyEvent; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.Iterator; import java.util.Map; import java.util.Properties; import javax.swing.AbstractAction; import javax.swing.Action; import javax.swing.ActionMap; import javax.swing.BorderFactory; import javax.swing.Box; import javax.swing.DefaultListCellRenderer; import javax.swing.DefaultListModel; import javax.swing.InputMap; import javax.swing.JButton; import javax.swing.JComponent; import javax.swing.JDialog; import javax.swing.JFrame; import javax.swing.JList; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JRootPane; import javax.swing.JScrollPane; import javax.swing.KeyStroke; import javax.swing.ListCellRenderer; import charactermanaj.model.CharacterData; import charactermanaj.model.PartsSet; import charactermanaj.util.LocalizedResourcePropertyLoader; /** * お気に入りの編集ダイアログ * @author seraphy */ public class ManageFavoriteDialog extends JDialog { private static final long serialVersionUID = 1L; protected static final String STRINGS_RESOURCE = "languages/managefavoritesdialog"; private CharacterData characterData; private DefaultListModel listModel; private JList list; private boolean dirty; public ManageFavoriteDialog(JFrame parent, CharacterData characterData) { super(parent, true); if (characterData == null) { throw new IllegalArgumentException(); } setDefaultCloseOperation(JDialog.DO_NOTHING_ON_CLOSE); addWindowListener(new WindowAdapter() { @Override public void windowClosing(WindowEvent e) { onClose(); } }); Properties strings = LocalizedResourcePropertyLoader.getInstance() .getLocalizedProperties(STRINGS_RESOURCE); setTitle(strings.getProperty("manageFavorites")); this.characterData = characterData; characterData.getPartsSets(); Container contentPane = getContentPane(); contentPane.setLayout(new BorderLayout()); listModel = new DefaultListModel(); list = new JList(listModel); ListCellRenderer listCellRenderer = new DefaultListCellRenderer() { private static final long serialVersionUID = 1L; @Override public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) { Object dispayValue = ((PartsSet) value).getLocalizedName(); return super.getListCellRendererComponent(list, dispayValue, index, isSelected, cellHasFocus); } }; list.setCellRenderer(listCellRenderer); AbstractAction actDelete = new AbstractAction(strings.getProperty("remove")) { private static final long serialVersionUID = 1L; public void actionPerformed(ActionEvent e) { onDelete(); } }; AbstractAction actRename = new AbstractAction(strings.getProperty("rename")) { private static final long serialVersionUID = 1L; public void actionPerformed(ActionEvent e) { onRename(); } }; JPanel buttonsPanel = new JPanel(); GridBagLayout gb = new GridBagLayout(); GridBagConstraints gbc = new GridBagConstraints(); buttonsPanel.setLayout(gb); gbc.gridx = 0; gbc.gridy = 0; gbc.anchor = GridBagConstraints.EAST; gbc.fill = GridBagConstraints.BOTH; buttonsPanel.add(new JButton(actDelete), gbc); gbc.gridx = 0; gbc.gridy = 1; buttonsPanel.add(new JButton(actRename), gbc); gbc.gridx = 0; gbc.gridy = 2; gbc.weighty = 1.; buttonsPanel.add(Box.createGlue(), gbc); JPanel panel2 = new JPanel(); panel2.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 42)); panel2.setLayout(new BorderLayout()); Action actCancel = new AbstractAction(strings.getProperty("close")) { private static final long serialVersionUID = 1L; public void actionPerformed(ActionEvent e) { onClose(); } }; JButton btnClose = new JButton(actCancel); panel2.add(btnClose, BorderLayout.EAST); JScrollPane scr = new JScrollPane(list); scr.setBorder(BorderFactory.createEtchedBorder()); scr.setPreferredSize(new Dimension(300, 150)); contentPane.add(scr, BorderLayout.CENTER); contentPane.add(buttonsPanel, BorderLayout.EAST); contentPane.add(panel2, BorderLayout.SOUTH); Toolkit tk = Toolkit.getDefaultToolkit(); JRootPane rootPane = getRootPane(); rootPane.setDefaultButton(btnClose); InputMap im = rootPane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW); ActionMap am = rootPane.getActionMap(); im.put(KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0), "deleteFav"); im.put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "closeManageFavoriteDialog"); im.put(KeyStroke.getKeyStroke(KeyEvent.VK_W, tk.getMenuShortcutKeyMask()), "closeManageFavoriteDialog"); am.put("deleteFav", actDelete); am.put("closeManageFavoriteDialog", actCancel); pack(); setLocationRelativeTo(parent); initListModel(); list.repaint(); } protected void initListModel() { ArrayList partssets = new ArrayList(); for (PartsSet partsset : characterData.getPartsSets().values()) { if (!partsset.isPresetParts()) { partssets.add(partsset); } } Collections.sort(partssets, new Comparator() { public int compare(PartsSet o1, PartsSet o2) { int ret = o1.getLocalizedName().compareTo(o2.getLocalizedName()); if (ret == 0) { ret = o1.getPartsSetId().compareTo(o2.getPartsSetId()); } if (ret == 0) { ret = o1.hashCode() - o2.hashCode(); } return ret; } }); list.setSelectedIndices(new int[0]); listModel.removeAllElements(); for (PartsSet partsset : partssets) { listModel.addElement(partsset); } } protected void onDelete() { Map partsSetMap = characterData.getPartsSets(); for (Object value : list.getSelectedValues()) { PartsSet partsSet = (PartsSet) value; Iterator> ite = partsSetMap.entrySet().iterator(); while (ite.hasNext()) { Map.Entry entry = ite.next(); PartsSet target = entry.getValue(); if (target == partsSet) { dirty = true; ite.remove(); } } } initListModel(); list.repaint(); } protected void onRename() { PartsSet partsSet = (PartsSet) list.getSelectedValue(); if (partsSet != null) { Properties strings = LocalizedResourcePropertyLoader.getInstance() .getLocalizedProperties(STRINGS_RESOURCE); String localizedName = JOptionPane.showInputDialog(this, strings .getProperty("inputName"), partsSet.getLocalizedName()); if (localizedName != null) { partsSet.setLocalizedName(localizedName); dirty = true; list.repaint(); } } } protected void onClose() { dispose(); } public boolean isModified() { return dirty; } } charactermanaj/src/charactermanaj/ui/ArchiveFileChooser.java0000644000175000017500000000576411767047542024500 0ustar paulliupaulliupackage charactermanaj.ui; import java.io.File; import java.util.Properties; import javax.swing.JFileChooser; import javax.swing.JOptionPane; import javax.swing.filechooser.FileFilter; import charactermanaj.model.AppConfig; import charactermanaj.util.LocalizedResourcePropertyLoader; /** * OK押下時に拡張子の補完とオーバーライドの警告を行うファイルチューザー.
* @author seraphy */ public class ArchiveFileChooser extends JFileChooser { private static final long serialVersionUID = -3908688762049311010L; protected static final String STRINGS_RESOURCE = "languages/exportwizdialog"; /** * Jarファイルフィルタ */ public static final FileFilter cmjFileFilter = new FileFilter() { @Override public String getDescription() { return "CharacterManaJ (*.cmj)"; } @Override public boolean accept(File f) { return f.isDirectory() || f.getName().endsWith(".cmj"); } }; /** * Zipファイルフィルタ */ public static final FileFilter zipFileFilter = new FileFilter() { @Override public String getDescription() { AppConfig appConfig = AppConfig.getInstance(); String zipNameEncoding = appConfig.getZipNameEncoding(); return "ZIP (" + zipNameEncoding + ") (*.zip)"; } @Override public boolean accept(File f) { return f.isDirectory() || f.getName().endsWith(".zip"); } }; protected boolean writeMode; protected ArchiveFileChooser(File initFile, boolean writeMode) { super(initFile); this.writeMode = writeMode; // フィルタの登録 addChoosableFileFilter(zipFileFilter); addChoosableFileFilter(cmjFileFilter); // デフォルトのフィルタ setFileFilter(zipFileFilter); } @Override public void approveSelection() { File file = getSelectedFile(); if (file == null) { return; } // ディレクトリ名を指定した場合は、そこに移動する. if (file.exists() && file.isDirectory()) { setCurrentDirectory(file); setSelectedFile(null); return; } String lcName = file.getName().toLowerCase(); FileFilter selfilter = getFileFilter(); if (selfilter == cmjFileFilter) { if (!lcName.endsWith(".cmj")) { file = new File(file.getPath() + ".cmj"); setSelectedFile(file); } } if (selfilter == zipFileFilter) { if (!lcName.endsWith(".zip")) { file = new File(file.getPath() + ".zip"); setSelectedFile(file); } } Properties strings = LocalizedResourcePropertyLoader.getInstance() .getLocalizedProperties(STRINGS_RESOURCE); if (writeMode && file.exists()) { if (JOptionPane.showConfirmDialog(this, strings.getProperty("confirm.overwrite"), strings.getProperty("confirm"), JOptionPane.YES_NO_OPTION) != JOptionPane.YES_OPTION) { return; } } if (!writeMode && !file.exists()) { JOptionPane.showMessageDialog(this, strings.getProperty("requiredExists")); return; } super.approveSelection(); } }; charactermanaj/src/charactermanaj/ui/PreviewPanel.java0000644000175000017500000017144711767047542023377 0ustar paulliupaulliupackage charactermanaj.ui; import static java.lang.Math.*; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Component; import java.awt.Cursor; import java.awt.Dimension; import java.awt.FontMetrics; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.Insets; import java.awt.Point; import java.awt.Rectangle; import java.awt.RenderingHints; import java.awt.Toolkit; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.event.MouseMotionAdapter; import java.awt.event.MouseMotionListener; import java.awt.event.MouseWheelEvent; import java.awt.event.MouseWheelListener; import java.awt.geom.Rectangle2D; import java.awt.image.BufferedImage; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.util.ArrayList; import java.util.EventObject; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.TreeSet; import java.util.concurrent.Semaphore; import java.util.logging.Level; import java.util.logging.Logger; import javax.swing.AbstractButton; import javax.swing.BorderFactory; import javax.swing.Box; import javax.swing.Icon; import javax.swing.JButton; import javax.swing.JCheckBox; import javax.swing.JComboBox; import javax.swing.JLabel; import javax.swing.JLayeredPane; import javax.swing.JPanel; import javax.swing.JScrollBar; import javax.swing.JScrollPane; import javax.swing.JSlider; import javax.swing.JTextField; import javax.swing.JToolBar; import javax.swing.JViewport; import javax.swing.OverlayLayout; import javax.swing.SwingUtilities; import javax.swing.Timer; import javax.swing.UIManager; import javax.swing.border.Border; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import javax.swing.plaf.basic.BasicComboBoxEditor; import charactermanaj.Main; import charactermanaj.graphics.filters.BackgroundColorFilter; import charactermanaj.graphics.filters.BackgroundColorFilter.BackgroundColorMode; import charactermanaj.model.AppConfig; import charactermanaj.ui.util.ScrollPaneDragScrollSupport; import charactermanaj.util.LocalizedResourcePropertyLoader; import charactermanaj.util.UIHelper; /** * プレビューパネル * @author seraphy */ public class PreviewPanel extends JPanel { private static final long serialVersionUID = 1L; protected static final String STRINGS_RESOURCE = "languages/previewpanel"; /** * プレビューパネルの上部ツールバーの通知を受けるリスナ * @author seraphy */ public interface PreviewPanelListener { /** * 保存 * @param e */ void savePicture(PreviewPanelEvent e); /** * コピー * @param e */ void copyPicture(PreviewPanelEvent e); /** * 背景色変更 * @param e */ void changeBackgroundColor(PreviewPanelEvent e); /** * 情報 * @param e */ void showInformation(PreviewPanelEvent e); /** * お気に入りに追加 * @param e */ void addFavorite(PreviewPanelEvent e); /** * 左右反転 * @param e */ void flipHorizontal(PreviewPanelEvent e); } /** * ロード中を示すインジケータ */ private final String indicatorText; /** * ロード中であるか判定するタイマー */ private final Timer timer; /** * インジケータを表示するまでのディレイ */ private long indicatorDelay; @Override public void addNotify() { super.addNotify(); if (!timer.isRunning()) { timer.start(); } } @Override public void removeNotify() { if (timer.isRunning()) { timer.stop(); } super.removeNotify(); } public static class PreviewPanelEvent extends EventObject { private static final long serialVersionUID = 1L; private int modifiers; public PreviewPanelEvent(Object src, ActionEvent e) { this(src, (e == null) ? 0 : e.getModifiers()); } public PreviewPanelEvent(Object src, int modifiers) { super(src); this.modifiers = modifiers; } public int getModifiers() { return modifiers; } public boolean isShiftKeyPressed() { return (modifiers & ActionEvent.SHIFT_MASK) != 0; } } private final Object lock = new Object(); private long loadingTicket; private long loadedTicket; private long firstWaitingTimestamp; private boolean indicatorShown; private String title; private JLabel lblTitle; private JLayeredPane layeredPane; private CheckInfoLayerPanel checkInfoLayerPanel; private PreviewImagePanel previewImgPanel; private JScrollPane previewImgScrollPane; private ScrollPaneDragScrollSupport scrollSupport; private PreviewControlPanel previewControlPanel; private double latestToggleZoom = 2.; private LinkedList listeners = new LinkedList(); public PreviewPanel() { setLayout(new BorderLayout()); final AppConfig appConfig = AppConfig.getInstance(); final Properties strings = LocalizedResourcePropertyLoader.getInstance() .getLocalizedProperties(STRINGS_RESOURCE); // 画像をロード中であることを示すインジケータの確認サイクル. timer = new Timer(100, new ActionListener() { public void actionPerformed(ActionEvent e) { onTimer(); } }); indicatorText = strings.getProperty("indicatorText"); indicatorDelay = appConfig.getPreviewIndicatorDelay(); UIHelper uiUtl = UIHelper.getInstance(); JButton saveBtn = uiUtl.createIconButton("icons/save.png"); JButton copyBtn = uiUtl.createIconButton("icons/copy.png"); JButton colorBtn = uiUtl.createIconButton("icons/color.png"); JButton informationBtn = uiUtl.createIconButton("icons/information.png"); JButton favoriteBtn = uiUtl.createIconButton("icons/favorite.png"); JButton flipHolizontalBtn = uiUtl.createIconButton("icons/flip.png"); saveBtn.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { savePicture(new PreviewPanelEvent(PreviewPanel.this, e)); } }); copyBtn.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { copyPicture(new PreviewPanelEvent(PreviewPanel.this, e)); } }); colorBtn.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { changeBackgroundColor(new PreviewPanelEvent(PreviewPanel.this, e)); } }); informationBtn.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { showInformation(new PreviewPanelEvent(PreviewPanel.this, e)); } }); favoriteBtn.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { addFavorite(new PreviewPanelEvent(PreviewPanel.this, e)); } }); flipHolizontalBtn.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { flipHolizontal(new PreviewPanelEvent(PreviewPanel.this, e)); } }); saveBtn.setToolTipText(strings.getProperty("tooltip.save")); copyBtn.setToolTipText(strings.getProperty("tooltip.copy")); colorBtn.setToolTipText(strings.getProperty("tooltip.changeBgColor")); informationBtn.setToolTipText(strings.getProperty("tooltip.showInformation")); favoriteBtn.setToolTipText(strings.getProperty("tooltip.registerFavorites")); flipHolizontalBtn.setToolTipText(strings.getProperty("tooltip.flipHorizontal")); final JToolBar toolBar = new JToolBar(); toolBar.setFloatable(false); toolBar.add(flipHolizontalBtn); toolBar.add(copyBtn); toolBar.add(saveBtn); toolBar.add(Box.createHorizontalStrut(8)); toolBar.add(colorBtn); toolBar.add(Box.createHorizontalStrut(4)); toolBar.add(favoriteBtn); toolBar.add(informationBtn); lblTitle = new JLabel() { private static final long serialVersionUID = 1L; public Dimension getPreferredSize() { Dimension dim = super.getPreferredSize(); int maxWidth = getParent().getWidth() - toolBar.getWidth(); if (dim.width > maxWidth) { dim.width = maxWidth; } return dim; }; public Dimension getMaximumSize() { return getPreferredSize(); }; public Dimension getMinimumSize() { Dimension dim = getPreferredSize(); dim.width = 50; return dim; }; }; lblTitle.setBorder(BorderFactory.createEmptyBorder(3, 10, 3, 3)); JPanel previewPaneHeader = new JPanel(); previewPaneHeader.setLayout(new BorderLayout()); previewPaneHeader.add(lblTitle, BorderLayout.WEST); previewPaneHeader.add(toolBar, BorderLayout.EAST); previewImgPanel = new PreviewImagePanel(); previewImgScrollPane = new JScrollPane(previewImgPanel); previewImgScrollPane.setAutoscrolls(false); previewImgScrollPane.setWheelScrollingEnabled(false); previewImgScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); previewImgScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); scrollSupport = new ScrollPaneDragScrollSupport(previewImgScrollPane) { @Override protected void setCursor(Cursor cursor) { PreviewPanel.this.setCursor(cursor); } }; add(previewPaneHeader, BorderLayout.NORTH); layeredPane = new JLayeredPane(); layeredPane.setLayout(new OverlayLayout(layeredPane)); layeredPane.add(previewImgScrollPane, JLayeredPane.DEFAULT_LAYER); checkInfoLayerPanel = new CheckInfoLayerPanel(); layeredPane.add(checkInfoLayerPanel, JLayeredPane.POPUP_LAYER); checkInfoLayerPanel.setVisible(false); add(layeredPane, BorderLayout.CENTER); previewControlPanel = new PreviewControlPanel(); Dimension dim = previewControlPanel.getPreferredSize(); Dimension prevDim = previewImgScrollPane.getPreferredSize(); dim.width = prevDim.width; previewControlPanel.setPreferredSize(dim); add(previewControlPanel, BorderLayout.SOUTH); previewControlPanel.setPinned(appConfig.isEnableZoomPanel()); // 倍率が変更された場合 previewControlPanel.addPropertyChangeListener("zoomFactorInt", new PropertyChangeListener() { public void propertyChange(PropertyChangeEvent evt) { Integer newValue = (Integer) evt.getNewValue(); zoomWithCenterPosition(newValue.doubleValue() / 100., null); } }); // 背景モードが切り替えられた場合 previewControlPanel.addPropertyChangeListener("backgroundColorMode", new PropertyChangeListener() { public void propertyChange(PropertyChangeEvent evt) { BackgroundColorMode bgColorMode = (BackgroundColorMode) evt.getNewValue(); previewImgPanel.setBackgroundColorMode(bgColorMode); if (bgColorMode != BackgroundColorMode.ALPHABREND && appConfig.isEnableCheckInfoTooltip() ) { // チェック情報ツールチップの表示 checkInfoLayerPanel.setMessage(null); checkInfoLayerPanel.setVisible(true); } else { // チェック情報ツールチップの非表示 checkInfoLayerPanel.setVisible(false); } } }); previewImgScrollPane.addMouseMotionListener(new MouseMotionAdapter() { @Override public void mouseMoved(MouseEvent e) { Rectangle rct = previewImgScrollPane.getBounds(); int y = e.getY(); if (y > rct.height - appConfig.getZoomPanelActivationArea()) { previewControlPanel.setVisible(true); } else { if ( !previewControlPanel.isPinned()) { previewControlPanel.setVisible(false); } } } }); // 標準のホイールリスナは削除する. for (final MouseWheelListener listener : previewImgScrollPane.getMouseWheelListeners()) { previewImgScrollPane.removeMouseWheelListener(listener); } previewImgScrollPane.addMouseWheelListener(new MouseWheelListener() { public void mouseWheelMoved(MouseWheelEvent e) { if ((Main.isMacOSX() && e.isAltDown()) || ( !Main.isMacOSX() && e.isControlDown())) { // Mac OS XならOptionキー、それ以外はコントロールキーとともにホイールスクロールの場合 zoomByWheel(e); } else { // ズーム以外のホイール操作はスクロールとする. scrollByWheel(e); } // 現在画像位置の情報の更新 updateCheckInfoMessage(e.getPoint()); } }); previewImgScrollPane.addMouseListener(new MouseAdapter() { @Override public void mousePressed(MouseEvent e) { if (e.getClickCount() == 2) { // ダブルクリック // (正確に2回目。3回目以降はダブルクリック + シングルクリック) toggleZoom(e.getPoint()); } else { scrollSupport.drag(true, e.getPoint()); } } @Override public void mouseReleased(MouseEvent e) { scrollSupport.drag(false, e.getPoint()); } }); previewImgScrollPane.addMouseMotionListener(new MouseMotionListener() { public void mouseMoved(MouseEvent e) { updateCheckInfoMessage(e.getPoint()); } public void mouseDragged(MouseEvent e) { scrollSupport.dragging(e.getPoint()); // 現在画像位置の情報の更新 updateCheckInfoMessage(e.getPoint()); } }); } /** * 倍率を切り替える. */ protected void toggleZoom(Point mousePos) { if (previewImgPanel.isDefaultZoom()) { // 等倍であれば以前の倍率を適用する. zoomWithCenterPosition(latestToggleZoom, mousePos); } else { // 等倍でなければ現在の倍率を記憶して等倍にする. double currentZoomFactor = previewImgPanel.getZoomFactor(); latestToggleZoom = currentZoomFactor; zoomWithCenterPosition(1., mousePos); } } /** * マウス位置に対して画像情報のツールチップを表示する * @param mousePosition マウス位置 */ protected void updateCheckInfoMessage(Point mousePosition) { if ( !checkInfoLayerPanel.isVisible()) { return; } // マウス位置から画像位置を割り出す Point imgPos = null; if (mousePosition != null) { Point panelPt = SwingUtilities.convertPoint(previewImgScrollPane, mousePosition, previewImgPanel); imgPos = previewImgPanel.getImagePosition(panelPt); } if (imgPos != null) { // 画像位置があれば、その位置の情報を取得する. int argb = previewImgPanel.getImageARGB(imgPos); checkInfoLayerPanel.setMessage(String.format("%3d,%3d¥n%08X", imgPos.x, imgPos.y, argb)); checkInfoLayerPanel.setPotision(mousePosition); } else { // 画像位置がなければツールチップは空にする. checkInfoLayerPanel.setMessage(null); } } /** * マウス座標単位で指定したオフセット分スクロールする. * @param diff_x 水平方向スクロール数 * @param diff_y 垂直方向スクロール数 */ protected void scroll(int diff_x, int diff_y) { scrollSupport.scroll(diff_x, diff_y); } /** * マウスホイールによる水平・垂直スクロール.
* シフトキーで水平、それ以外は垂直とする.
* @param e ホイールイベント */ protected void scrollByWheel(final MouseWheelEvent e) { scrollSupport.scrollByWheel(e); // イベントは処理済みとする. e.consume(); } /** * ホイールによる拡大縮小.
* ホイールの量は関係なく、方向だけで判定する.
* プラットフォームごとに修飾キーの判定が異なるので、 * 呼び出しもとであらかじめ切り分けて呼び出すこと.
* @param e ホイールイベント */ protected void zoomByWheel(final MouseWheelEvent e) { int wheelRotation = e.getWheelRotation(); double currentZoom = previewImgPanel.getZoomFactor(); double zoomFactor; if (wheelRotation < 0) { // ホイール上で拡大 zoomFactor = currentZoom * 1.1; } else if (wheelRotation > 0){ // ホイール下で縮小 zoomFactor = currentZoom * 0.9; } else { return; } // 倍率変更する zoomWithCenterPosition(zoomFactor, e.getPoint()); // イベント処理済み e.consume(); } /** * ズームスライダまたはコンボのいずれかの値を更新すると、他方からも更新通知があがるため * 二重処理を防ぐためのセマフォ.
*/ private Semaphore zoomLock = new Semaphore(1); /** * プレビューに表示する画像の倍率を更新する.
* 指定した座標が拡大縮小の中心点になるようにスクロールを試みる.
* 座標がnullの場合は現在表示されている中央を中心とするようにスクロールを試みる.
* (スクロールバーが表示されていない、もしくは十分にスクロールできない場合は必ずしも中心とはならない.)
* コントロールパネルの表示値も更新する.
* コントロールパネルからの更新通知をうけて再入しないように、 * 同時に一つしか実行されないようにしている.
* @param zoomFactor 倍率、範囲外のものは範囲内に補正される. * @param mousePos スクロールペイン上のマウス座標、もしくはnull(nullの場合は表示中央) */ protected void zoomWithCenterPosition(double zoomFactor, Point mousePos) { if ( !zoomLock.tryAcquire()) { return; } try { // 範囲制限. if (zoomFactor < 0.2) { zoomFactor = 0.2; } else if (zoomFactor > 8.) { zoomFactor = 8.; } JViewport vp = previewImgScrollPane.getViewport(); Point viewCenter; if (mousePos != null) { // スクロールペインのマウス座標を表示パネルの位置に換算する. viewCenter = SwingUtilities.convertPoint(this, mousePos, previewImgPanel); } else { // 表示パネル上の現在表示しているビューポートの中央の座標を求める Rectangle viewRect = vp.getViewRect(); viewCenter = new Point( (viewRect.x + viewRect.width / 2), (viewRect.y + viewRect.height / 2) ); } // 現在のビューサイズ(余白があれば余白も含む) Dimension viewSize = previewImgPanel.getScaledSize(true); // 倍率変更 previewControlPanel.setZoomFactor(zoomFactor); previewImgPanel.setZoomFactor(zoomFactor); // 新しいのビューサイズ(余白があれば余白も含む) Dimension viewSizeAfter = previewImgPanel.getScaledSize(true); Dimension visibleSize = vp.getExtentSize(); if (viewSize != null && viewSizeAfter != null && viewSizeAfter.width > 0 && viewSizeAfter.height > 0 && viewSizeAfter.width > visibleSize.width && viewSizeAfter.height > visibleSize.height) { // 新しいビューの大きさよりも表示可能領域が小さい場合のみ vp.setViewSize(viewSizeAfter); // スクロールペインに表示されている画面サイズを求める. // スクロールバーがある方向は、コンテンツの最大と等しいが // スクロールバーがない場合は画面サイズのほうが大きいため、 // 倍率変更による縦横の移動比は、それぞれ異なる. int visible_width = max(visibleSize.width, viewSize.width); int visible_height = max(visibleSize.height, viewSize.height); int visible_width_after = max(visibleSize.width, viewSizeAfter.width); int visible_height_after = max(visibleSize.height, viewSizeAfter.height); // 前回の倍率から今回の倍率の倍率. // オリジナルに対する倍率ではない. // また、画像は縦横同率であるが表示ウィンドウはスクロールバー有無により同率とは限らない. double zoomDiffX = (double) visible_width_after / (double) visible_width; double zoomDiffY = (double) visible_height_after / (double) visible_height; // 拡大後の座標の補正 Point viewCenterAfter = new Point(); viewCenterAfter.x = (int) round(viewCenter.x * zoomDiffX); viewCenterAfter.y = (int) round(viewCenter.y * zoomDiffY); // 倍率適用前後の座標の差分 int diff_x = viewCenterAfter.x - viewCenter.x; int diff_y = viewCenterAfter.y - viewCenter.y; // スクロール scroll(diff_x, diff_y); } // スクロールの単位を画像1ドットあたりの表示サイズに変更する. // (ただし1を下回らない) JScrollBar vsb = previewImgScrollPane.getVerticalScrollBar(); JScrollBar hsb = previewImgScrollPane.getHorizontalScrollBar(); vsb.setUnitIncrement(max(1, (int) ceil(zoomFactor))); hsb.setUnitIncrement(max(1, (int) ceil(zoomFactor))); } finally { zoomLock.release(); } } /** * プレビューに表示するタイトル.
* @param title タイトル */ public void setTitle(String title) { if (title == null) { title = ""; } if (!title.equals(this.title)) { this.title = title; lblTitle.setText(title + (indicatorShown ? indicatorText : "")); lblTitle.setToolTipText(title); } } public String getTitle() { return this.title; } /** * ロードに時間がかかっているか判定し、 * インジケータを表示するためのタイマーイベントハンドラ.
*/ protected void onTimer() { boolean waiting; long firstRequest; synchronized (lock) { waiting = isWaiting(); firstRequest = firstWaitingTimestamp; } boolean indicatorShown = (waiting && ((System.currentTimeMillis() - firstRequest) > indicatorDelay)); if (this.indicatorShown != indicatorShown) { this.indicatorShown = indicatorShown; lblTitle.setText(title + (indicatorShown ? indicatorText : "")); } } /** * チケットの状態が、ロード完了待ち状態であるか?
* ロード中のチケットが、ロード完了のチケットより新しければロード中と見なす.
* @return 完了待ちであればtrue、そうでなければfalse */ protected boolean isWaiting() { synchronized (lock) { return loadingTicket > loadedTicket; } } /** * ロード要求が出されるたびに、そのロード要求チケットを登録する.
* チケットは要求されるたびに増加するシーケンスとする.
* @param ticket ロード要求チケット */ public void setLoadingRequest(long ticket) { synchronized (lock) { if ( !isWaiting() && this.loadedTicket < ticket) { // 現在認識しているチケットの状態がロード完了であり、 // それよりも新しいチケットが要求されたならば、 // 今回のチケットから待ち時間の計測を開始する. this.firstWaitingTimestamp = System.currentTimeMillis(); } this.loadingTicket = ticket; } } /** * ロード完了するたびに呼び出される.
* @param ticket ロード要求チケット. */ public void setLoadingComplete(long ticket) { synchronized (lock) { this.loadedTicket = ticket; } } /** * 表示画像を設定する.
* @param previewImg 表示画像、もしくはnull */ public void setPreviewImage(BufferedImage previewImg) { previewImgPanel.setPreviewImage(previewImg); } /** * 表示されている画像を取得する.
* 表示画像が設定されていなければnull.
* @return 表示画像、もしくはnull */ public BufferedImage getPreviewImage() { return previewImgPanel.getPreviewImage(); } /** * 表示している画面イメージそのままを取得する. * @return 表示画像 */ public BufferedImage getScreenImage() { JViewport vp = previewImgScrollPane.getViewport(); Dimension dim = vp.getExtentSize(); BufferedImage img = new BufferedImage(dim.width, dim.height, BufferedImage.TYPE_INT_ARGB); Graphics2D g = img.createGraphics(); try { vp.paint(g); } finally { g.dispose(); } return img; } /** * 壁紙を設定する.
* @param wallpaperImg 壁紙、null不可 */ public void setWallpaper(Wallpaper wallpaper) { previewImgPanel.setWallpaper(wallpaper); } /** * 壁紙を取得する.
* 壁紙が未設定の場合は空の壁紙インスタンスが返される.
* @return 壁紙 */ public Wallpaper getWallpaper() { return previewImgPanel.getWallpaper(); } /** * 表示倍率を取得する. * @return 表示倍率 */ public double getZoomFactor() { return previewControlPanel.getZoomFactor(); } /** * 表示倍率を設定する * @param zoomFactor 表示倍率 */ public void setZoomFactor(double zoomFactor) { previewControlPanel.setZoomFactor(zoomFactor); } /** * ズームパネルのピン留め制御 * @param visible 表示する場合はtrue */ public void setVisibleZoomBox(boolean visible) { previewControlPanel.setPinned(visible); } /** * ズームパネルがピン留めされているか? * @return ピン留めされていればtrue */ public boolean isVisibleZoomBox() { return previewControlPanel.isPinned(); } public void addPreviewPanelListener(PreviewPanelListener listener) { if (listener == null) { throw new IllegalArgumentException(); } listeners.add(listener); } public void removePreviewPanelListener(PreviewPanelListener listener) { listeners.remove(listener); } protected void savePicture(PreviewPanelEvent e) { for (PreviewPanelListener listener : listeners) { listener.savePicture(e); } } protected void flipHolizontal(PreviewPanelEvent e) { for (PreviewPanelListener listener : listeners) { listener.flipHorizontal(e); } } protected void copyPicture(PreviewPanelEvent e) { for (PreviewPanelListener listener : listeners) { listener.copyPicture(e); } } protected void changeBackgroundColor(PreviewPanelEvent e) { for (PreviewPanelListener listener : listeners) { listener.changeBackgroundColor(e); } } protected void showInformation(PreviewPanelEvent e) { for (PreviewPanelListener listener : listeners) { listener.showInformation(e); } } protected void addFavorite(PreviewPanelEvent e) { for (PreviewPanelListener listener : listeners) { listener.addFavorite(e); } } } /** * チェック情報の表示用レイヤーパネル.
* @author seraphy */ class CheckInfoLayerPanel extends JPanel { private static final long serialVersionUID = 1L; /** * ロガー */ private static final Logger logger = Logger.getLogger(CheckInfoLayerPanel.class.getName()); /** * ボックスの余白 */ private Insets padding = new Insets(3, 3, 3, 3); /** * 表示位置プロパティ */ private Point pos = new Point(); /** * 表示メッセージプロパティ.
* ¥nで改行となる.
* 空文字ならば非表示.
*/ private String message = ""; /** * 解析済みメッセージ.
* 業に分割される.
* 空文字は空のリストとなる.
*/ private String[] messageLines; /** * 解析済みフォントの高さ.
*/ private int fontHeight; /** * 描画済みエリア.
* 次回描画前に消去する必要のある領域.
* まだ一度も描画してなければnull.
*/ private Rectangle eraseRect; /** * 現在、描画すべきエリア.
* なければnull.
*/ private Rectangle requestRect; /** * 画面に関連づけられていない状態でのテキスト表示サイズは * 計算できないため、画面追加時に再計算させるための * 予約フラグ.
*/ private boolean requestRecalcOnAdd; /** * フォントのためのデスクトップヒント.(あれば) */ @SuppressWarnings("rawtypes") private Map desktopHintsForFont; /** * 透明コンポーネントとして構築する.
*/ @SuppressWarnings("rawtypes") public CheckInfoLayerPanel() { setOpaque(false); Toolkit tk = Toolkit.getDefaultToolkit(); desktopHintsForFont = (Map) tk.getDesktopProperty("awt.font.desktophints"); logger.log(Level.CONFIG, "awt.font.desktophints=" + desktopHintsForFont); } /** * 指定エリアに情報を描画する.
*/ @Override protected void paintComponent(Graphics g0) { Graphics2D g = (Graphics2D) g0; super.paintComponent(g); // クリップ領域 Rectangle clip = g.getClipBounds(); // System.out.println("clip:" + clip + " /eraseRect:" + eraseRect + " /drawRect:" + requestRect); // 削除すべき領域が描画範囲に含まれているか? // (含まれていれば、その領域は消去済みである.) if (clip == null || (eraseRect != null && clip.contains(eraseRect))) { eraseRect = null; } // 表示領域の判定 if (requestRect == null || requestRect.isEmpty() || !(clip != null && clip.intersects(requestRect))) { // 表示すべき領域が存在しないか、描画要求範囲にない. return; } if (messageLines == null || messageLines.length == 0) { // 表示するものがなければ何もしない. return; } // フォントのレンダリングヒント if (desktopHintsForFont != null) { g.addRenderingHints(desktopHintsForFont); } // 箱の描画 g.setColor(new Color(255, 255, 255, 192)); g.fillRect(requestRect.x, requestRect.y, requestRect.width, requestRect.height); g.setColor(Color.GRAY); g.drawRect(requestRect.x, requestRect.y, requestRect.width - 1, requestRect.height - 1); // 情報の描画 g.setColor(Color.BLACK); int oy = fontHeight; for (String messageLine : messageLines) { g.drawString(messageLine, requestRect.x + padding.left, requestRect.y + padding.top - 1 + oy); oy += fontHeight; } // 描画された領域を次回消去領域として記憶する. if (eraseRect == null || eraseRect.isEmpty()) { // 消去済みであれば、今回分のみを次回消去領域とする. eraseRect = (Rectangle) requestRect.clone(); } else { // 消去済みエリアが未消去で残っている場合は // 今回領域を結合する. eraseRect.add(requestRect); } } /** * 画面にアタッチされた場合、描画領域の再計算が * 必要であれば計算する.
*/ @Override public void addNotify() { super.addNotify(); if (requestRecalcOnAdd) { requestRecalcOnAdd = false; calcRepaint(); } } /** * 要求されたプロパティから、フォント高さによる表示領域を計算し、 * その領域の再描画を要求する.(描画する内容がなれば、描画要求しない.)
* 前回表示領域があれば、消去するために、そのエリアも再描画を要求する.
* それ以外のエリアは描画要求しない.(描画の最適化による負荷軽減策)
* フォントサイズを求めるためにグラフィクスへのアクセスが必要となるが、 * まだ取得できない場合は{@link #addNotify()}の呼び出し時に * 再計算するようにフラグを立てておく.
*/ protected void calcRepaint() { Graphics2D g = (Graphics2D) getGraphics(); if (g == null) { requestRecalcOnAdd = true; return; } try { // 前回描画領域のクリアのために呼び出す. if (eraseRect != null && !eraseRect.isEmpty()) { repaint(eraseRect); } // 空であれば新たな描画なし. if (message.length() == 0) { requestRect = null; return; } FontMetrics fm = g.getFontMetrics(); String[] messageLines = message.split("¥n"); Rectangle2D rct = null; for (String messageLine : messageLines) { Rectangle2D tmp = fm.getStringBounds(messageLine, g); if (rct != null) { rct.add(tmp); } else { rct = tmp; } } int fw = (int) rct.getWidth(); int fh = (int) rct.getHeight(); int w = fw + padding.left + padding.right; int h = fh * messageLines.length + padding.top + padding.bottom; // 指定した位置の右上あたりにする int x = pos.x + 16; int y = pos.y - h; // サイズ int client_w = getWidth(); int client_h = getHeight(); if (x + w > client_w) { // 画面右の場合はカーソルの左に移動 x = pos.x - w - 10; } if (y < 0) { // 画面上の場合はカーソルの下に移動 y = pos.y + 10; } if (y + h > client_h) { y -= (y + h - client_h); } // 結果の格納 this.requestRect = new Rectangle(x, y, w, h); this.messageLines = messageLines; this.fontHeight = fh; // 再描画の要求 Rectangle paintRect = (Rectangle) requestRect.clone(); repaint(paintRect); } finally { g.dispose(); } } public void setPotision(Point requestPt) { if (requestPt == null) { throw new IllegalArgumentException(); } if ( !requestPt.equals(pos)) { Point oldpos = pos; pos = (Point) requestPt.clone(); calcRepaint(); firePropertyChange("position", oldpos, pos); } } public Point getPosition() { return (Point) pos.clone(); } public void setMessage(String message) { if (message == null) { message = ""; } message = message.replace("¥r¥n", "¥n"); if ( !message.equals(this.message)) { String oldmes = this.message; this.message = message; calcRepaint(); firePropertyChange("message", oldmes, message); } } public String getMessage() { return message; } } /** * 画像表示パネル * @author seraphy */ class PreviewImagePanel extends JPanel { private static final long serialVersionUID = 1L; /** * 背景モード.
*/ private BackgroundColorMode bgColorMode; /** * 壁紙.
*/ private Wallpaper wallpaper; /** * 壁紙変更イベントのリスナ */ private PropertyChangeListener wallpaperListener; /** * 透過オリジナル画像.
*/ private BufferedImage previewImg; /** * 表示用画像(背景モードによる調整あり).
* 事前に拡大縮小を適用済みの場合は、{@link #scaledZoomFactor}に * 適用している倍率が設定される.
* 表示用に改めてイメージを生成する必要がない場合は、 * 透過オリジナルと同じインスタンスとなりえる.
*/ private BufferedImage previewImgForDraw; /** * 表示用画像がスケール済みである場合、そのスケールが設定される.
* スケール済みでない場合はnullとなる.
*/ private Double scaledZoomFactor; /** * 倍率 */ private double zoomFactor = 1.; /** * 許容誤差 */ private static final double TOLERANT = 0.001; /** * コンストラクタ */ public PreviewImagePanel() { super(); // 通常モード bgColorMode = BackgroundColorMode.ALPHABREND; // 壁紙変更通知リスナ wallpaperListener = new PropertyChangeListener() { public void propertyChange(PropertyChangeEvent evt) { onChangeWallpaper(); } }; // 壁紙 wallpaper = new Wallpaper(); wallpaper.addPropertyChangeListener(wallpaperListener); } /** * 画像を表示する. */ @Override protected void paintComponent(Graphics g0) { Graphics2D g = (Graphics2D) g0; super.paintComponent(g); if (previewImgForDraw == null) { return; } // 倍率を適用した画像を画面の中央に配置できるように計算する. // (画像が倍率適用済みであれば1倍とする) Rectangle imgRct = adjustImageRectangle(); // 表示用画像がスケール済みでない場合はレンダリングオプションを適用する. if (scaledZoomFactor == null) { Object renderingOption = getRenderingOption(); g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, renderingOption); } // 背景処理 if (bgColorMode == BackgroundColorMode.ALPHABREND) { // 表示の最大範囲 (可視領域外も含む) int w = getWidth(); int h = getHeight(); wallpaper.drawWallpaper(g, w, h); } // レンダリング g.drawImage(previewImgForDraw, imgRct.x, imgRct.y, imgRct.x + imgRct.width, imgRct.y + imgRct.height, 0, 0, previewImgForDraw.getWidth(), previewImgForDraw.getHeight(), null); // 通常モード以外のグリッド描画に該当するモードはグリッドを前景に描く AppConfig appConfig = AppConfig.getInstance(); int drawGridMask = appConfig.getDrawGridMask(); if ((drawGridMask & bgColorMode.ordinal()) != 0) { Color oldc = g.getColor(); try { g.setColor(new Color(appConfig.getPreviewGridColor(), true)); drawGrid(g, imgRct.x, imgRct.y, appConfig.getPreviewGridSize()); } finally { g.setColor(oldc); } } } /** * グリッドを描画する.
* 開始位置の-1単位位置から画像サイズの+1単位までがグリッド範囲となる。 * @param g * @param offset_x 開始位置 * @param offset_y 開始位置 * @param unit グリッド単位(pixel) */ protected void drawGrid(Graphics2D g, int offset_x, int offset_y, int unit) { Rectangle clip = g.getClipBounds(); int src_w = previewImg.getWidth(); int src_h = previewImg.getHeight(); int my = src_h / unit; int mx = src_w / unit; int st_x = offset_x + (int)(-1 * unit * zoomFactor); int en_x = offset_x + (int)((mx + 1) * unit * zoomFactor); int w = en_x - st_x + 1; for (int y = -1; y <= my + 1; y++) { int y1 = y * unit; Rectangle rct = new Rectangle( st_x, offset_y + (int)(y1 * zoomFactor), w, 1); if (clip == null || clip.intersects(rct)) { g.drawLine(rct.x, rct.y, rct.x + rct.width, rct.y); } } int st_y = offset_y + (int)(-1 * unit * zoomFactor); int en_y = offset_y + (int)((my + 1) * unit * zoomFactor); int h = en_y - st_y + 1; for (int x = -1; x <= mx + 1; x++) { int x1 = x * unit; Rectangle rct = new Rectangle( offset_x + (int)(x1 * zoomFactor), st_y, 1, h); g.drawLine(rct.x, rct.y, rct.x, rct.y + rct.height); } } /** * 現在の倍率に応じたレンダリングオプションを取得する.
* @return レンダリングオプション */ protected Object getRenderingOption() { AppConfig appConfig = AppConfig.getInstance(); double rendringOptimizeThreshold; if (bgColorMode == BackgroundColorMode.ALPHABREND) { rendringOptimizeThreshold = appConfig.getRenderingOptimizeThresholdForNormal(); } else { rendringOptimizeThreshold = appConfig.getRenderingOptimizeThresholdForCheck(); } Object renderingHint; if (zoomFactor < rendringOptimizeThreshold) { // 補正を適用する最大倍率以内である場合 if (zoomFactor <= 1. || !appConfig.isEnableInterpolationBicubic()) { // 縮小する場合、もしくはバイキュービックをサポートしない場合 renderingHint = RenderingHints.VALUE_INTERPOLATION_BILINEAR; } else { // 拡大する場合でバイキュービックをサポートしている場合 renderingHint = RenderingHints.VALUE_INTERPOLATION_BICUBIC; } } else { // 補正を適用する最大倍率を超えている場合は補正なし. renderingHint = RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR; } return renderingHint; } /** * 倍率と、画面のサイズと、表示するオリジナルの画像サイズをもとに、 * 倍率を適用した画像サイズを、画面に収まる位置に補正して返す.
* 返される矩形の幅と高さ(width, height)は拡大後の画像サイズに等しい.
* 拡大後の画像が画面よりも小さければセンタリングするように矩形の開始位置(x, y)がオフセットされる.
* そうでなければ矩形の開始位置(x, y)は0となる.
* 画像が設定されていなければ幅と高さがゼロの矩形が返される.
* @return 画像を表示するオフセットと大きさ、もしくは空の矩形 */ public Rectangle adjustImageRectangle() { if (previewImg == null) { return new Rectangle(0, 0, 0, 0); // 幅・高さともにゼロ } int client_w = getWidth(); int client_h = getHeight(); int src_w = previewImg.getWidth(); int src_h = previewImg.getHeight(); int w = (int) round(src_w * zoomFactor); int h = (int) round(src_h * zoomFactor); int offset_x = 0; if (w < client_w) { offset_x = (client_w - w) / 2; } int offset_y = 0; if (h < client_h) { offset_y = (client_h - h) / 2; } return new Rectangle(offset_x, offset_y, w, h); } /** * パネルのマウス座標から、実寸の画像のピクセル位置を返す.
* 画像が表示されていないか、範囲外であればnullを返す.
* @param pt パネルの座標 * @return 画像の位置、もしくはnull */ public Point getImagePosition(Point pt) { if (pt == null || previewImg == null) { // プレビュー画像が設定されていなければnull return null; } Rectangle imgRct = adjustImageRectangle(); if ( !imgRct.contains(pt.x, pt.y)) { // 範囲外であればnull return null; } // オフセットを除去する. Point ret = (Point) pt.clone(); ret.x -= imgRct.x; ret.y -= imgRct.y; // 倍率を解除する. ret.x = (int) floor(ret.x / zoomFactor); ret.y = (int) floor(ret.y / zoomFactor); return ret; } /** * 画像の位置から画面の位置を割り出す.
* @param pt 画像の位置 * @return 画面の位置 */ public Point getMousePosition(Point pt) { if (pt == null || previewImg == null) { // プレビュー画像が設定されていなければnull return null; } Rectangle imgRct = adjustImageRectangle(); // 表示倍率を加える Point ret = (Point) pt.clone(); ret.x = (int) ceil(ret.x * zoomFactor); ret.y = (int) ceil(ret.y * zoomFactor); // オフセットを加える ret.x += imgRct.x; ret.y += imgRct.y; return ret; } /** * 指定した位置のRGB値を取得する.
* 範囲外の場合は0が返される.
* @param pt イメージの位置 * @return イメージのRGB値 */ public int getImageARGB(Point pt) { if (pt == null) { throw new IllegalArgumentException(); } try { return previewImg.getRGB(pt.x, pt.y); } catch (RuntimeException ex) { return 0; // 範囲外 } } /** * 倍率を適用した画像パネルのサイズを計算し適用する.
* モードにより余白が加えられる.
*/ protected void recalcScaledSize() { Dimension scaledSize = getScaledSize(true); if (scaledSize != null) { setPreferredSize(scaledSize); revalidate(); } } /** * 元画像の倍率適用後のサイズを返す.
* 元画像が設定されていなければnull.
* needOffsetがfalseであれば表示モードに関わらず、画像の拡大・縮小後の純粋なサイズ、 * trueであれば余白が必要な表示モードの場合の余白が付与された場合のサイズが返される.
* @param needOffset 余白を加味したサイズが必要な場合はtrue * @return 倍率適用後のサイズ、もしくはnull */ protected Dimension getScaledSize(boolean needOffset) { if (previewImg == null) { return null; } int src_w = previewImg.getWidth(); int src_h = previewImg.getHeight(); int w = (int) round(src_w * zoomFactor); int h = (int) round(src_h * zoomFactor); Dimension scaledSize = new Dimension(w, h); if (bgColorMode != BackgroundColorMode.ALPHABREND) { // 通常モード以外は画像よりも少し大きめにすることで // キャンバスに余白をつける AppConfig appConfig = AppConfig.getInstance(); int unfilledSpace = appConfig.getPreviewUnfilledSpaceForCheckMode(); scaledSize.width += max(0, unfilledSpace * 2); scaledSize.height += max(0, unfilledSpace * 2); } return scaledSize; } public void setPreviewImage(BufferedImage previewImg) { BufferedImage oldimg = this.previewImg; this.previewImg = previewImg; recalcScaledSize(); makeDrawImage(true); repaint(); firePropertyChange("previewImage", oldimg, previewImg); } public BufferedImage getPreviewImage() { return previewImg; } public void setWallpaper(Wallpaper wallpaper) { if (wallpaper == null) { throw new IllegalArgumentException(); } if ( !this.wallpaper.equals(wallpaper)) { Wallpaper wallpaperOld = this.wallpaper; if (wallpaperOld != null) { wallpaperOld.removePropertyChangeListener(wallpaperListener); } this.wallpaper = wallpaper; if (this.wallpaper != null) { this.wallpaper.addPropertyChangeListener(wallpaperListener); } firePropertyChange("wallpaper", wallpaperOld, this.wallpaper); onChangeWallpaper(); } } public Wallpaper getWallpaper() { return wallpaper; } protected void onChangeWallpaper() { repaint(); } /** * 背景モード調整済みの表示用画像を作成する. * @param changeImage 画像の変更あり */ protected void makeDrawImage(boolean changeImage) { if (previewImg == null) { // 画像が設定されていなければ空 this.previewImgForDraw = null; scaledZoomFactor = null; return; } BufferedImage img; if (changeImage || scaledZoomFactor != null) { // 画像が変更されているか、スケール済みであれば // 背景モードの再適用が必要. if (bgColorMode == BackgroundColorMode.ALPHABREND) { // アルファブレンド通常モードは背景用にあえて作成する必要はない. img = previewImg; } else { // アルファブレンド通常モード以外は背景に作成する Color bgColor = wallpaper.getBackgroundColor(); BackgroundColorFilter bgColorFilter = new BackgroundColorFilter(bgColorMode, bgColor); img = bgColorFilter.filter(previewImg, null); } } else { // 画像が変更されておらず、スケール済みでもなければ // すでに作成済みの画像が使用できる. img = previewImgForDraw; } // レンダリングオプション Object renderingOption = getRenderingOption(); // バイキュービックでなければ、事前の拡大縮小は行わずに、表示時に行う. if ( !renderingOption.equals(RenderingHints.VALUE_INTERPOLATION_BICUBIC)) { previewImgForDraw = img; scaledZoomFactor = null; return; } // バイキュービックの場合、倍率を適用したサイズに予め加工しておく. Dimension scaledSize = getScaledSize(false); BufferedImage offscreen = new BufferedImage( scaledSize.width, scaledSize.height, BufferedImage.TYPE_INT_ARGB); Graphics2D g = offscreen.createGraphics(); try { g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC); g.drawImage(img, 0, 0, scaledSize.width, scaledSize.height, 0, 0, img.getWidth(), img.getHeight(), null); } finally { g.dispose(); } previewImgForDraw = offscreen; scaledZoomFactor = Double.valueOf(zoomFactor); } public void setBackgroundColorMode(BackgroundColorMode bgColorMode) { if (bgColorMode == null) { throw new IllegalArgumentException(); } if (this.bgColorMode != bgColorMode) { BackgroundColorMode oldcm = bgColorMode; this.bgColorMode = bgColorMode; makeDrawImage(true); recalcScaledSize(); repaint(); firePropertyChange("backgroundColorMode", oldcm, bgColorMode); } } public BackgroundColorMode setBackgroundColorMode() { return bgColorMode; } public void setZoomFactor(double zoomFactor) { if (abs(zoomFactor - this.zoomFactor) > TOLERANT) { // 0.001未満の差異は誤差とみなして反映しない. double oldzoom = this.zoomFactor; this.zoomFactor = zoomFactor; recalcScaledSize(); makeDrawImage(false); repaint(); firePropertyChange("zoomFactor", oldzoom, zoomFactor); } } public double getZoomFactor() { return zoomFactor; } /** * 倍率が100%であるか? * @return 100%であればtrue */ public boolean isDefaultZoom() { return zoomFactor - 1 < TOLERANT; } } /** * 倍率・背景モードを操作するための下部パネル用 * @author seraphy */ class PreviewControlPanel extends JPanel { private static final long serialVersionUID = 1L; private static final Logger logger = Logger.getLogger(PreviewControlPanel.class.getName()); protected static final String STRINGS_RESOURCE = "languages/previewpanel"; /** * ピン留めチェックボックス */ private JCheckBox chkPinning; /** * アルファ確認チェックボックス */ private JCheckBox chkNoAlpha; /** * グレースケール確認チェックボックス */ private JCheckBox chkGrayscale; /** * 倍率用スライダ */ private JSlider zoomSlider; /** * 倍率入力用コンボボックス */ private JComboBox zoomCombo; /** * スライダの最小値 */ private static final int MIN_INDEX = -170; /** * スライダの最大値 */ private static final int MAX_INDEX = 219; /** * 最小倍率 */ private double minimumZoomFactor; /** * 最大倍率 */ private double maximumZoomFactor; /** * 現在の倍率(100倍済み) */ private int currentZoomFactorInt; /** * 現在の背景色モード */ private BackgroundColorMode backgroundColorMode; /** * 任意の底Aをもつ対数 logA(N)を計算して返す. * @param a 底 * @param x 引数 * @return logA(N) */ private static double logN(double a, double x) { return log(x) / log(a); } /** * 倍率(等倍を1とする)に対するスライダのインデックス値を返す.
* スライダは10ステップごとに前のステップの10%づつ増減する.(複利式)
* @param zoomFactor 倍率(1を等倍とする) * @return インデックス */ private static int zoomFactorToIndex(double zoomFactor) { return (int) round(logN((1. + 0.1), zoomFactor) * 10); } /** * スライダのインデックス値から倍率(等倍を1とする)を返す.
* 10ステップごとに10%づつ増減する.(複利式)
* @param index インデックス * @return 倍率(1を等倍とする) */ private static double zoomFactorFromIndex(int index) { return pow(1. + 0.1, index / 10.); } /** * コンストラクタ */ public PreviewControlPanel() { final Properties strings = LocalizedResourcePropertyLoader.getInstance() .getLocalizedProperties(STRINGS_RESOURCE); UIHelper uiHelper = UIHelper.getInstance(); // ピンアイコン Icon pinIcon = uiHelper.createTwoStateIcon( "icons/pin-icon1.png", "icons/pin-icon2.png"); // ピンチェックボックス chkPinning = new JCheckBox(pinIcon); chkPinning.setToolTipText(strings.getProperty("tooltip.zoompanel.pinning")); // 円ボタン型チェックボックス用アイコンの実装 final Icon stateIcon = new Icon() { public int getIconHeight() { return 12; } public int getIconWidth() { return 6; }; public void paintIcon(Component c, Graphics g, int x, int y) { boolean sw = false; if (c instanceof AbstractButton) { AbstractButton btn = (AbstractButton) c; sw = btn.isSelected(); } int w = getIconWidth(); int h = getIconHeight(); int s = min(w, h); int ox = 0; int oy = 0; if (w > s) { ox = (w - s) / 2; } if (h > s) { oy = (h - s) / 2; } if (sw) { AppConfig appConfig = AppConfig.getInstance(); Color fillColor = appConfig.getSelectedItemBgColor(); g.setColor(fillColor); g.fillOval(x + ox, y + oy, s, w); } g.setColor(Color.GRAY); g.drawOval(x + ox, y + oy, s, s); } }; // アルファ確認とグレースケール確認用のチェックボックス chkNoAlpha = new JCheckBox(stateIcon); chkGrayscale = new JCheckBox(stateIcon); chkNoAlpha.setToolTipText(strings.getProperty("tooltip.zoompanel.checkalpha")); chkGrayscale.setToolTipText(strings.getProperty("tooltip.zoompanel.checkgrayscale")); backgroundColorMode = BackgroundColorMode.ALPHABREND; final ChangeListener chkAlphaGrayChangeListener = new ChangeListener() { public void stateChanged(ChangeEvent e) { onChangeCheckAlphaGray(); } }; chkNoAlpha.addChangeListener(chkAlphaGrayChangeListener); chkGrayscale.addChangeListener(chkAlphaGrayChangeListener); // 倍率スライダ zoomSlider = new JSlider(JSlider.HORIZONTAL, MIN_INDEX, MAX_INDEX, 0); zoomSlider.setToolTipText(strings.getProperty("tooltip.zoompanel.zoomfactor_slider")); // 倍率コンボ zoomCombo = new JComboBox(); zoomCombo.setToolTipText(strings.getProperty("tooltip.zoompanel.zoomfactor_combo")); // 倍率の既定リストの設定と、最大・最小値の算定 minimumZoomFactor = zoomFactorFromIndex(zoomSlider.getMinimum()); maximumZoomFactor = zoomFactorFromIndex(zoomSlider.getMaximum()); int minZoomRange = (int) round(minimumZoomFactor * 100.); int maxZoomRange = (int) round(maximumZoomFactor * 100.); List predefinedZoomRanges = getPredefinedZoomRanges(); for (int zoomRange : predefinedZoomRanges) { if (zoomRange < minZoomRange) { minZoomRange = zoomRange; } if (zoomRange > maxZoomRange) { maxZoomRange = zoomRange; } zoomCombo.addItem(Integer.toString(zoomRange)); } final int[] zoomRanges = {minZoomRange, maxZoomRange}; currentZoomFactorInt = 100; zoomCombo.setSelectedItem(Integer.toString(currentZoomFactorInt)); zoomCombo.setEditable(true); if ( !Main.isMacOSX()) { // Windows環境だとデフォルトで9桁分のテキストフィールドが作成され // それがレイアウトの推奨サイズとして実際に使われてしまうため、 // 明示的に3桁にとどめておくようにオーバーライドする. // Mac OS Xならば問題ない. zoomCombo.setEditor(new BasicComboBoxEditor() { { editor = new JTextField(3) { private static final long serialVersionUID = 1L; @Override public void setBorder(Border border) { // 何もしない. } public void setText(String s) { if (getText().equals(s)) { return; } super.setText(s); } }; } }); } // スライダを変更することによりコンボボックスを変更する、 // もしくはコンボボックスを変更することでスライダを変更するが、 // 互いに通知を呼び合うことになるため他方を無視するためのセマフォ final Semaphore changeLock = new Semaphore(1); zoomCombo.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { boolean adjusted = false; String value = (String) zoomCombo.getSelectedItem(); int zoomFactorInt; try { zoomFactorInt = Integer.parseInt(value); if (zoomFactorInt < zoomRanges[0]) { zoomFactorInt = zoomRanges[0]; adjusted = true; } else if (zoomFactorInt > zoomRanges[1]) { zoomFactorInt = zoomRanges[1]; adjusted = true; } } catch (RuntimeException ex) { zoomFactorInt = 100; adjusted = true; } if (adjusted) { zoomCombo.setSelectedItem(Integer.toString(zoomFactorInt)); Toolkit tk = Toolkit.getDefaultToolkit(); tk.beep(); } if (changeLock.tryAcquire()) { try { zoomSlider.setValue(zoomFactorToIndex(zoomFactorInt / 100.)); } finally { changeLock.release(); } } fireZoomFactorChange(zoomFactorInt); } }); zoomSlider.addChangeListener(new ChangeListener() { public void stateChanged(ChangeEvent e) { int index = zoomSlider.getValue(); double zoomFactor = zoomFactorFromIndex(index); int zoomFactorInt = (int) round(zoomFactor * 100); if (changeLock.tryAcquire()) { try { zoomCombo.setSelectedItem(Integer.toString(zoomFactorInt)); } finally { changeLock.release(); } fireZoomFactorChange(zoomFactorInt); } } }); // パーツの配備 GridBagLayout gbl = new GridBagLayout(); setLayout(gbl); GridBagConstraints gbc = new GridBagConstraints(); gbc.gridx = 0; gbc.gridy = 0; gbc.ipadx = 0; gbc.ipady = 0; gbc.gridheight = 1; gbc.gridwidth = 1; gbc.fill = GridBagConstraints.NONE; gbc.anchor = GridBagConstraints.CENTER; gbc.insets = new Insets(0, 0, 0, 5); gbc.weightx = 0.; gbc.weighty = 0.; add(chkPinning, gbc); gbc.gridx = 1; gbc.weightx = 0.; gbc.insets = new Insets(0, 0, 0, 0); add(chkGrayscale, gbc); gbc.gridx = 2; gbc.weightx = 0.; gbc.insets = new Insets(0, 0, 0, 5); add(chkNoAlpha, gbc); gbc.gridx = 3; gbc.weightx = 1.; gbc.fill = GridBagConstraints.HORIZONTAL; add(zoomSlider, gbc); gbc.gridx = 4; gbc.weightx = 0.; gbc.insets = new Insets(3, 0, 3, 0); gbc.fill = GridBagConstraints.VERTICAL; add(zoomCombo, gbc); Integer scrollbarWidth = (Integer) UIManager.get("ScrollBar.width"); logger.log(Level.CONFIG, "ScrollBar.width=" + scrollbarWidth); if (scrollbarWidth == null) { scrollbarWidth = Integer.parseInt( strings.getProperty("uiconstraint.scrollbar.width")); } gbc.gridx = 5; gbc.weightx = 0.; gbc.anchor = GridBagConstraints.WEST; gbc.insets = new Insets(0, 0, 0, scrollbarWidth); add(new JLabel("%"), gbc); } /** * アプリケーション設定より事前定義済みの倍率候補を取得する * @return 倍率候補のリスト(順序あり) */ protected List getPredefinedZoomRanges() { AppConfig appConfig = AppConfig.getInstance(); String strs = appConfig.getPredefinedZoomRanges(); TreeSet ranges = new TreeSet(); for (String str : strs.split(",")) { str = str.trim(); if (str.length() > 0) { try { int zoomFactor = Integer.parseInt(str); ranges.add(Integer.valueOf(zoomFactor)); } catch (RuntimeException ex) { // 無視する. } } } ranges.add(Integer.valueOf(100)); // 等倍は常に設定する. return new ArrayList(ranges); } /** * 倍率が変更されたことを通知する. */ protected void fireZoomFactorChange(int newZoomFactor) { if (currentZoomFactorInt != newZoomFactor) { int oldValue = currentZoomFactorInt; currentZoomFactorInt = newZoomFactor; firePropertyChange("zoomFactorInt", oldValue, newZoomFactor); } } private Semaphore changeChkLock = new Semaphore(1); protected void onChangeCheckAlphaGray() { changeChkLock.tryAcquire(); try { BackgroundColorMode backgroundColorMode = BackgroundColorMode.valueOf( chkNoAlpha.isSelected(), chkGrayscale.isSelected() ); setBackgroundColorMode(backgroundColorMode); } finally { changeChkLock.release(); } } public BackgroundColorMode getBackgroundColorMode() { return this.backgroundColorMode; } public void setBackgroundColorMode(BackgroundColorMode backgroundColorMode) { if (backgroundColorMode == null) { throw new IllegalArgumentException(); } BackgroundColorMode oldcm = this.backgroundColorMode; if (oldcm != backgroundColorMode) { this.backgroundColorMode = backgroundColorMode; changeChkLock.tryAcquire(); try { chkNoAlpha.setSelected(backgroundColorMode.isNoAlphaChannel()); chkGrayscale.setSelected(backgroundColorMode.isGrayscale()); } finally { changeChkLock.release(); } firePropertyChange("backgroundColorMode", oldcm, backgroundColorMode); } } public boolean isPinned() { return chkPinning.isSelected(); } public void setPinned(boolean pinned) { chkPinning.setSelected(pinned); if (isDisplayable()) { setVisible(pinned); } } public double getZoomFactor() { return (double) currentZoomFactorInt / 100.; } public void setZoomFactor(double zoomFactor) { if (zoomFactor < minimumZoomFactor) { zoomFactor = minimumZoomFactor; } if (zoomFactor > maximumZoomFactor) { zoomFactor = maximumZoomFactor; } int zoomFactorInt = (int) round(zoomFactor * 100.); if (zoomFactorInt != currentZoomFactorInt) { zoomCombo.setSelectedItem(Integer.toString(zoomFactorInt)); } } } charactermanaj/src/charactermanaj/ui/ImageSelectPanelList.java0000644000175000017500000000265311767047542024764 0ustar paulliupaulliupackage charactermanaj.ui; import java.util.AbstractList; import java.util.ArrayList; import java.util.HashMap; import charactermanaj.model.PartsCategory; public class ImageSelectPanelList extends AbstractList { protected ArrayList imageSelectPanels = new ArrayList(); protected HashMap imageSelectPanelMap = new HashMap(); public ImageSelectPanelList() { } @Override public boolean add(ImageSelectPanel o) { if (o == null) { throw new IllegalArgumentException(); } PartsCategory partsCategory = o.getPartsCategory(); if (imageSelectPanelMap.containsKey(partsCategory)) { throw new IllegalArgumentException("duplicate category: " + partsCategory); } imageSelectPanelMap.put(partsCategory, o); return imageSelectPanels.add(o); } @Override public ImageSelectPanel get(int index) { return imageSelectPanels.get(index); } @Override public int size() { return imageSelectPanels.size(); } public ImageSelectPanel findByPartsCategory(PartsCategory partsCategory) { if (partsCategory == null) { throw new IllegalArgumentException(); } ImageSelectPanel panel = imageSelectPanelMap.get(partsCategory); if (panel == null) { throw new IllegalArgumentException("not registered: " + partsCategory); } return panel; } } charactermanaj/src/charactermanaj/ui/PartsManageDialog.java0000644000175000017500000007707711767047542024324 0ustar paulliupaulliupackage charactermanaj.ui; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Container; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.Insets; import java.awt.Toolkit; import java.awt.event.ActionEvent; import java.awt.event.KeyEvent; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.net.URI; import java.sql.Timestamp; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Properties; import java.util.concurrent.Semaphore; import java.util.logging.Level; import java.util.logging.Logger; import javax.swing.AbstractAction; import javax.swing.Action; import javax.swing.ActionMap; import javax.swing.BorderFactory; import javax.swing.Box; import javax.swing.InputMap; import javax.swing.JButton; import javax.swing.JComponent; import javax.swing.JDialog; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JPopupMenu; import javax.swing.JRootPane; import javax.swing.JScrollPane; import javax.swing.JTable; import javax.swing.JTextField; import javax.swing.KeyStroke; import javax.swing.UIManager; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; import javax.swing.event.TableModelEvent; import javax.swing.event.TableModelListener; import javax.swing.table.TableColumnModel; import charactermanaj.Main; import charactermanaj.model.AppConfig; import charactermanaj.model.CharacterData; import charactermanaj.model.PartsAuthorInfo; import charactermanaj.model.PartsCategory; import charactermanaj.model.PartsIdentifier; import charactermanaj.model.PartsManageData; import charactermanaj.model.PartsSpec; import charactermanaj.model.io.CharacterDataPersistent; import charactermanaj.ui.model.AbstractTableModelWithComboBoxModel; import charactermanaj.util.DesktopUtilities; import charactermanaj.util.ErrorMessageHelper; import charactermanaj.util.LocalizedResourcePropertyLoader; public class PartsManageDialog extends JDialog { private static final long serialVersionUID = 1L; protected static final String STRINGS_RESOURCE = "languages/partsmanagedialog"; private static final Logger logger = Logger.getLogger(PartsManageDialog.class.getName()); private CharacterData characterData; private PartsManageTableModel partsManageTableModel; private JTable partsManageTable; private JTextField txtHomepage; private JTextField txtAuthor; private boolean updated; public PartsManageDialog(JFrame parent, CharacterData characterData) { super(parent, true); if (characterData == null) { throw new IllegalArgumentException(); } this.characterData = characterData; setDefaultCloseOperation(JDialog.DO_NOTHING_ON_CLOSE); addWindowListener(new WindowAdapter() { @Override public void windowClosing(WindowEvent e) { onClose(); } }); Properties strings = LocalizedResourcePropertyLoader.getInstance() .getLocalizedProperties(STRINGS_RESOURCE); setTitle(strings.getProperty("title")); Container contentPane = getContentPane(); // パーツリストテーブル JPanel partsListPanel = new JPanel(); partsListPanel.setBorder(BorderFactory.createCompoundBorder( BorderFactory.createEmptyBorder(5, 5, 5, 5), BorderFactory .createTitledBorder(strings.getProperty("partslist")))); GridBagLayout partsListPanelLayout = new GridBagLayout(); partsListPanel.setLayout(partsListPanelLayout); partsManageTableModel = new PartsManageTableModel(); partsManageTable = new JTable(partsManageTableModel); partsManageTable.setShowGrid(true); partsManageTable.setGridColor(AppConfig.getInstance().getGridColor()); partsManageTableModel.adjustColumnModel(partsManageTable.getColumnModel()); partsManageTable.setAutoResizeMode(JTable.AUTO_RESIZE_OFF); partsManageTable.putClientProperty("terminateEditOnFocusLost", Boolean.TRUE); JScrollPane partsManageTableSP = new JScrollPane(partsManageTable); partsManageTable.getSelectionModel().addListSelectionListener(new ListSelectionListener() { public void valueChanged(ListSelectionEvent e) { if (!e.getValueIsAdjusting()) { onChangeSelection(); } } }); partsManageTableModel.addTableModelListener(new TableModelListener() { public void tableChanged(TableModelEvent e) { onTableDataChange(e.getFirstRow(), e.getLastRow()); } }); GridBagConstraints gbc = new GridBagConstraints(); gbc.gridx = 0; gbc.gridy = 0; gbc.gridheight = 1; gbc.gridwidth = 4; gbc.anchor = GridBagConstraints.EAST; gbc.fill = GridBagConstraints.BOTH; gbc.insets = new Insets(3, 3, 3, 3); gbc.ipadx = 0; gbc.ipady = 0; gbc.weightx = 1.; gbc.weighty = 1.; partsListPanel.add(partsManageTableSP, gbc); Action actSortByName = new AbstractAction(strings.getProperty("sortByName")) { private static final long serialVersionUID = 1L; public void actionPerformed(ActionEvent e) { onSortByName(); } }; Action actSortByAuthor = new AbstractAction(strings.getProperty("sortByAuthor")) { private static final long serialVersionUID = 1L; public void actionPerformed(ActionEvent e) { onSortByAuthor(); } }; Action actSortByTimestamp = new AbstractAction(strings.getProperty("sortByTimestamp")) { private static final long serialVersionUID = 1L; public void actionPerformed(ActionEvent e) { onSortByTimestamp(); } }; gbc.gridx = 0; gbc.gridy = 1; gbc.gridheight = 1; gbc.gridwidth = 1; gbc.weightx = 0.; gbc.weighty = 0.; partsListPanel.add(new JButton(actSortByName), gbc); gbc.gridx = 1; gbc.gridy = 1; gbc.weightx = 0.; gbc.weighty = 0.; partsListPanel.add(new JButton(actSortByAuthor), gbc); gbc.gridx = 2; gbc.gridy = 1; gbc.weightx = 0.; gbc.weighty = 0.; partsListPanel.add(new JButton(actSortByTimestamp), gbc); gbc.gridx = 3; gbc.gridy = 1; gbc.weightx = 1.; gbc.weighty = 0.; partsListPanel.add(Box.createHorizontalGlue(), gbc); contentPane.add(partsListPanel, BorderLayout.CENTER); // テーブルのコンテキストメニュー final JPopupMenu popupMenu = new JPopupMenu(); Action actApplyAllDownloadURL = new AbstractAction(strings.getProperty("applyAllDownloadURL")) { private static final long serialVersionUID = 1L; public void actionPerformed(ActionEvent e) { onApplyAllDownloadURL(); } }; Action actApplyAllVersion = new AbstractAction(strings.getProperty("applyAllVersion")) { private static final long serialVersionUID = 1L; public void actionPerformed(ActionEvent e) { onApplyAllVersion(); } }; popupMenu.add(actApplyAllVersion); popupMenu.add(actApplyAllDownloadURL); partsManageTable.setComponentPopupMenu(popupMenu); // 作者情報パネル JPanel authorPanel = new JPanel(); authorPanel.setBorder(BorderFactory.createCompoundBorder(BorderFactory .createEmptyBorder(5, 5, 5, 5), BorderFactory .createTitledBorder(strings.getProperty("author.info")))); GridBagLayout authorPanelLayout = new GridBagLayout(); authorPanel.setLayout(authorPanelLayout); gbc.gridx = 0; gbc.gridy = 0; gbc.gridheight = 1; gbc.gridwidth = 1; gbc.anchor = GridBagConstraints.EAST; gbc.fill = GridBagConstraints.BOTH; gbc.insets = new Insets(3, 3, 3, 3); gbc.ipadx = 0; gbc.ipady = 0; gbc.weightx = 0.; gbc.weighty = 0.; authorPanel.add(new JLabel(strings.getProperty("author"), JLabel.RIGHT), gbc); gbc.gridx = 1; gbc.gridy = 0; gbc.gridwidth = 2; gbc.weightx = 1.; txtAuthor = new JTextField(); authorPanel.add(txtAuthor, gbc); gbc.gridx = 0; gbc.gridy = 1; gbc.gridwidth = 1; gbc.weightx = 0.; authorPanel.add(new JLabel(strings.getProperty("homepage"), JLabel.RIGHT), gbc); gbc.gridx = 1; gbc.gridy = 1; gbc.gridwidth = 1; gbc.weightx = 1.; txtHomepage = new JTextField(); authorPanel.add(txtHomepage, gbc); gbc.gridx = 2; gbc.gridy = 1; gbc.gridwidth = 1; gbc.weightx = 0.; Action actBrowseHomepage = new AbstractAction(strings.getProperty("open")) { private static final long serialVersionUID = 1L; public void actionPerformed(ActionEvent e) { onBrosweHomepage(); } }; authorPanel.add(new JButton(actBrowseHomepage), gbc); if (!DesktopUtilities.isSupported()) { actBrowseHomepage.setEnabled(false); } txtAuthor.getDocument().addDocumentListener(new DocumentListener() { public void removeUpdate(DocumentEvent e) { onEditAuthor(); } public void insertUpdate(DocumentEvent e) { onEditAuthor(); } public void changedUpdate(DocumentEvent e) { onEditAuthor(); } }); txtHomepage.getDocument().addDocumentListener(new DocumentListener() { public void removeUpdate(DocumentEvent e) { onEditHomepage(); } public void insertUpdate(DocumentEvent e) { onEditHomepage(); } public void changedUpdate(DocumentEvent e) { onEditHomepage(); } }); // ボタンパネル JPanel btnPanel = new JPanel(); btnPanel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 45)); GridBagLayout btnPanelLayout = new GridBagLayout(); btnPanel.setLayout(btnPanelLayout); Action actClose = new AbstractAction(strings.getProperty("cancel")) { private static final long serialVersionUID = 1L; public void actionPerformed(ActionEvent e) { onClose(); } }; Action actOK = new AbstractAction(strings.getProperty("update")) { private static final long serialVersionUID = 1L; public void actionPerformed(ActionEvent e) { onOK(); } }; gbc.gridx = 0; gbc.gridy = 0; gbc.weightx = 1.; btnPanel.add(Box.createHorizontalGlue(), gbc); gbc.gridx = Main.isLinuxOrMacOSX() ? 2 : 1; gbc.gridy = 0; gbc.weightx = 0.; btnPanel.add(new JButton(actOK), gbc); gbc.gridx = Main.isLinuxOrMacOSX() ? 1 : 2; gbc.gridy = 0; gbc.weightx = 0.; btnPanel.add(new JButton(actClose), gbc); // ダイアログ下部 JPanel southPanel = new JPanel(new BorderLayout()); southPanel.add(authorPanel, BorderLayout.NORTH); southPanel.add(btnPanel, BorderLayout.SOUTH); contentPane.add(southPanel, BorderLayout.SOUTH); // キーボード Toolkit tk = Toolkit.getDefaultToolkit(); JRootPane rootPane = getRootPane(); InputMap im = rootPane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW); ActionMap am = rootPane.getActionMap(); im.put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "closePartsManageDialog"); im.put(KeyStroke.getKeyStroke(KeyEvent.VK_W, tk.getMenuShortcutKeyMask()), "closePartsManageDialog"); am.put("closePartsManageDialog", actClose); // モデル構築 partsManageTableModel.initModel(characterData); // ウィンドウ配置 setSize(500, 400); setLocationRelativeTo(parent); } private Semaphore authorEditSemaphore = new Semaphore(1); protected void onChangeSelection() { try { authorEditSemaphore.acquire(); try { int [] selRows = partsManageTable.getSelectedRows(); HashSet authors = new HashSet(); for (int selRow : selRows) { PartsManageTableRow row = partsManageTableModel.getRow(selRow); authors.add(row.getAuthor() == null ? "" : row.getAuthor()); } if (authors.size() > 1) { AppConfig appConfig = AppConfig.getInstance(); txtAuthor.setBackground(appConfig.getAuthorEditConflictBgColor()); txtHomepage.setBackground(appConfig.getAuthorEditConflictBgColor()); } else { Color bgColor = UIManager.getColor("TextField.background"); if (bgColor == null) { bgColor = Color.white; } txtAuthor.setBackground(bgColor); txtHomepage.setBackground(bgColor); } if (authors.isEmpty()) { // 選択されているauthorがない場合は全部編集不可 txtAuthor.setEditable(false); txtAuthor.setText(""); txtHomepage.setEditable(false); txtHomepage.setText(""); } else { // 選択されているAuthorが1つ以上あればAuthorは編集可 txtAuthor.setEditable(true); txtHomepage.setEditable(true); if (authors.size() == 1) { // 選択されているAuthorが一個であれば、それを表示 String author = authors.iterator().next(); String homepage = partsManageTableModel.getHomepage(author); txtAuthor.setText(author); txtHomepage.setText(homepage); } else { // 選択されているAuthorが二個以上あれば編集可能だがテキストには表示しない. txtAuthor.setText(""); txtHomepage.setText(""); } } } finally { authorEditSemaphore.release(); } } catch (InterruptedException ex) { ErrorMessageHelper.showErrorDialog(this, ex); } catch (RuntimeException ex) { ErrorMessageHelper.showErrorDialog(this, ex); } } protected void onTableDataChange(int firstRow, int lastRow) { onChangeSelection(); } protected void onApplyAllDownloadURL() { int[] selRows = partsManageTable.getSelectedRows(); if (selRows.length == 0) { Toolkit tk = Toolkit.getDefaultToolkit(); tk.beep(); return; } Arrays.sort(selRows); HashSet authors = new HashSet(); for (int selRow : selRows) { PartsManageTableRow row = partsManageTableModel.getRow(selRow); authors.add(row.getAuthor() == null ? "" : row.getAuthor()); } Properties strings = LocalizedResourcePropertyLoader.getInstance() .getLocalizedProperties(STRINGS_RESOURCE); if (authors.size() > 1) { if (JOptionPane.showConfirmDialog(this, strings.getProperty("confirm.authorConflict"), strings.getProperty("confirm"), JOptionPane.OK_CANCEL_OPTION) != JOptionPane.OK_OPTION) { return; } } PartsManageTableRow firstRow = partsManageTableModel.getRow(selRows[0]); String downloadURL = firstRow.getDownloadURL(); if (downloadURL == null) { downloadURL = ""; } String downloadURL_new = JOptionPane.showInputDialog(this, strings.getProperty("input.downloadURL"), downloadURL); if (downloadURL_new == null || downloadURL.equals(downloadURL_new)) { // キャンセルされたか、内容に変化ない場合は何もしない return; } for (int selRow : selRows) { PartsManageTableRow row = partsManageTableModel.getRow(selRow); row.setDownloadURL(downloadURL_new); } partsManageTableModel.fireTableRowsUpdated(selRows[0], selRows[selRows.length - 1]); } protected void onApplyAllVersion() { Toolkit tk = Toolkit.getDefaultToolkit(); int[] selRows = partsManageTable.getSelectedRows(); if (selRows.length == 0) { tk.beep(); return; } Arrays.sort(selRows); HashSet authors = new HashSet(); for (int selRow : selRows) { PartsManageTableRow row = partsManageTableModel.getRow(selRow); authors.add(row.getAuthor() == null ? "" : row.getAuthor()); } Properties strings = LocalizedResourcePropertyLoader.getInstance() .getLocalizedProperties(STRINGS_RESOURCE); if (authors.size() > 1) { if (JOptionPane.showConfirmDialog(this, strings.getProperty("confirm.authorConflict"), strings.getProperty("confirm"), JOptionPane.OK_CANCEL_OPTION) != JOptionPane.OK_OPTION) { return; } } PartsManageTableRow firstRow = partsManageTableModel.getRow(selRows[0]); double version = firstRow.getVersion(); String strVersion = (version < 0) ? "" : Double.toString(version); String strVersion_new = JOptionPane.showInputDialog(this, strings.getProperty("input.homepageURL"), strVersion); if (strVersion_new == null || strVersion.equals(strVersion_new)) { // キャンセルされたか、内容に変化ない場合は何もしない return; } double version_new; try { version_new = Double.parseDouble(strVersion_new); } catch (Exception ex) { // 数値として不正であれば何もしない. tk.beep(); return; } for (int selRow : selRows) { PartsManageTableRow row = partsManageTableModel.getRow(selRow); row.setVersion(version_new); } partsManageTableModel.fireTableRowsUpdated(selRows[0], selRows[selRows.length - 1]); } protected void onEditHomepage() { try { if (!authorEditSemaphore.tryAcquire()) { return; } try { String author = txtAuthor.getText(); String homepage = txtHomepage.getText(); partsManageTableModel.setHomepage(author, homepage); } finally { authorEditSemaphore.release(); } } catch (Exception ex) { ErrorMessageHelper.showErrorDialog(this, ex); } } protected void onEditAuthor() { try { if (!authorEditSemaphore.tryAcquire()) { return; } try { String author = txtAuthor.getText(); int[] selRows = partsManageTable.getSelectedRows(); int firstRow = -1; int lastRow = Integer.MAX_VALUE; for (int selRow : selRows) { PartsManageTableRow row = partsManageTableModel.getRow(selRow); String oldValue = row.getAuthor(); if (oldValue == null || !oldValue.equals(author)) { row.setAuthor(author); firstRow = Math.max(firstRow, selRow); lastRow = Math.min(lastRow, selRow); } } String homepage = partsManageTableModel.getHomepage(author); if (homepage == null) { homepage = ""; } txtHomepage.setText(homepage); if (firstRow >= 0) { partsManageTable.repaint(); } } finally { authorEditSemaphore.release(); } } catch (Exception ex) { ErrorMessageHelper.showErrorDialog(this, ex); } } protected void onClose() { Properties strings = LocalizedResourcePropertyLoader.getInstance() .getLocalizedProperties(STRINGS_RESOURCE); if (JOptionPane.showConfirmDialog(this, strings.getProperty("confirm.cancel"), strings.getProperty("confirm"), JOptionPane.YES_NO_OPTION) != JOptionPane.YES_OPTION) { return; } updated = false; dispose(); } protected void onBrosweHomepage() { Toolkit tk = Toolkit.getDefaultToolkit(); String homepage = txtHomepage.getText(); if (homepage == null || homepage.trim().length() == 0) { tk.beep(); return; } try { URI uri = new URI(homepage); DesktopUtilities.browse(uri); } catch (Exception ex) { tk.beep(); logger.log(Level.INFO, "browse failed. : " + homepage, ex); } } protected void onSortByAuthor() { partsManageTableModel.sortByAuthor(); } protected void onSortByName() { partsManageTableModel.sortByName(); } protected void onSortByTimestamp() { partsManageTableModel.sortByTimestamp(); } protected void onOK() { if (partsManageTable.isEditing()) { Toolkit tk = Toolkit.getDefaultToolkit(); tk.beep(); return; } int mx = partsManageTableModel.getRowCount(); // 作者ごとのホームページ情報の取得 // (同一作者につきホームページは一つ) HashMap authorInfoMap = new HashMap(); for (int idx = 0; idx < mx; idx++) { PartsManageTableRow row = partsManageTableModel.getRow(idx); String author = row.getAuthor(); String homepage = row.getHomepage(); if (author != null && author.trim().length() > 0) { PartsAuthorInfo authorInfo = authorInfoMap.get(author.trim()); if (authorInfo == null) { authorInfo = new PartsAuthorInfo(); authorInfo.setAuthor(author.trim()); authorInfoMap.put(authorInfo.getAuthor(), authorInfo); } authorInfo.setHomePage(homepage); } } PartsManageData partsManageData = new PartsManageData(); // パーツごとの作者とバージョン、ダウンロード先の取得 for (int idx = 0; idx < mx; idx++) { PartsManageTableRow row = partsManageTableModel.getRow(idx); String author = row.getAuthor(); PartsAuthorInfo partsAuthorInfo = null; if (author != null && author.trim().length() > 0) { partsAuthorInfo = authorInfoMap.get(author.trim()); } double version = row.getVersion(); String downloadURL = row.getDownloadURL(); String localizedName = row.getLocalizedName(); PartsIdentifier partsIdentifier = row.getPartsIdentifier(); PartsManageData.PartsKey partsKey = new PartsManageData.PartsKey(partsIdentifier); partsManageData.putPartsInfo(partsKey, localizedName, partsAuthorInfo, new PartsManageData.PartsVersionInfo(version, downloadURL)); } // パーツ管理情報を書き込む. CharacterDataPersistent persist = CharacterDataPersistent.getInstance(); try { URI docBase = characterData.getDocBase(); persist.savePartsManageData(docBase, partsManageData); } catch (Exception ex) { ErrorMessageHelper.showErrorDialog(this, ex); return; } updated = true; dispose(); } /** * パーツ管理情報が更新されたか? * @return 更新された場合はtrue、そうでなければfalse */ public boolean isUpdated() { return updated; } } class PartsManageTableRow { private PartsIdentifier partsIdentifier; private Timestamp timestamp; private String localizedName; private String author; private String homepage; private String downloadURL; private double version; public PartsManageTableRow(PartsIdentifier partsIdentifier, PartsSpec partsSpec) { if (partsIdentifier == null || partsSpec == null) { throw new IllegalArgumentException(); } this.partsIdentifier = partsIdentifier; this.localizedName = partsIdentifier.getLocalizedPartsName(); this.timestamp = new Timestamp(partsSpec.getPartsFiles().lastModified()); PartsAuthorInfo autherInfo = partsSpec.getAuthorInfo(); if (autherInfo != null) { author = autherInfo.getAuthor(); homepage = autherInfo.getHomePage(); } if (author == null) { author = ""; } if (homepage == null) { homepage = ""; } downloadURL = partsSpec.getDownloadURL(); version = partsSpec.getVersion(); } public PartsIdentifier getPartsIdentifier() { return partsIdentifier; } public Timestamp getTimestamp() { return timestamp; } public String getLocalizedName() { return localizedName; } public void setLocalizedName(String localizedName) { if (localizedName == null || localizedName.trim().length() == 0) { throw new IllegalArgumentException(); } this.localizedName = localizedName; } public String getAuthor() { return author; } public String getDownloadURL() { return downloadURL; } public double getVersion() { return version; } public void setAuthor(String author) { this.author = author; } public void setDownloadURL(String downloadURL) { this.downloadURL = downloadURL; } public void setVersion(double version) { this.version = version; } public String getHomepage() { return homepage; } public void setHomepage(String homepage) { this.homepage = homepage; } } class PartsManageTableModel extends AbstractTableModelWithComboBoxModel { private static final long serialVersionUID = 1L; private static final String[] COLUMN_NAMES; private static final int[] COLUMN_WIDTHS; static { Properties strings = LocalizedResourcePropertyLoader.getInstance() .getLocalizedProperties(PartsManageDialog.STRINGS_RESOURCE); COLUMN_NAMES = new String[] { strings.getProperty("column.partsid"), strings.getProperty("column.lastmodified"), strings.getProperty("column.category"), strings.getProperty("column.partsname"), strings.getProperty("column.author"), strings.getProperty("column.version"), strings.getProperty("column.downloadURL"), }; COLUMN_WIDTHS = new int[] { Integer.parseInt(strings.getProperty("column.partsid.width")), Integer.parseInt(strings.getProperty("column.lastmodified.width")), Integer.parseInt(strings.getProperty("column.category.width")), Integer.parseInt(strings.getProperty("column.partsname.width")), Integer.parseInt(strings.getProperty("column.author.width")), Integer.parseInt(strings.getProperty("column.version.width")), Integer.parseInt(strings.getProperty("column.downloadURL.width")), }; } public int getColumnCount() { return COLUMN_NAMES.length; } @Override public String getColumnName(int column) { return COLUMN_NAMES[column]; } public void adjustColumnModel(TableColumnModel columnModel) { for (int idx = 0; idx < COLUMN_WIDTHS.length; idx++) { columnModel.getColumn(idx).setPreferredWidth(COLUMN_WIDTHS[idx]); } } public Object getValueAt(int rowIndex, int columnIndex) { PartsManageTableRow row = getRow(rowIndex); switch (columnIndex) { case 0: return row.getPartsIdentifier().getPartsName(); case 1: return row.getTimestamp().toString(); case 2: return row.getPartsIdentifier().getPartsCategory().getLocalizedCategoryName(); case 3: return row.getLocalizedName(); case 4: return row.getAuthor(); case 5: return row.getVersion() > 0 ? row.getVersion() : null; case 6: return row.getDownloadURL(); } return ""; } @Override public void setValueAt(Object aValue, int rowIndex, int columnIndex) { PartsManageTableRow row = getRow(rowIndex); switch (columnIndex) { case 0: return; case 1: return; case 2: return; case 3: { String localizedName = (String) aValue; if (localizedName != null && localizedName.trim().length() > 0) { String oldValue = row.getLocalizedName(); if (oldValue != null && oldValue.equals(localizedName)) { return; // 変化なし } row.setLocalizedName(localizedName); } } break; case 4: { String author = (String) aValue; if (author == null) { author = ""; } String oldValue = row.getAuthor(); if (oldValue != null && oldValue.equals(author)) { return; // 変化なし } row.setAuthor(author); } break; case 5: { Double version = (Double) aValue; if (version == null || version.doubleValue() <= 0) { version = Double.valueOf(0.); } Double oldValue = row.getVersion(); if (oldValue != null && oldValue.equals(version)) { return; // 変化なし } row.setVersion(version); } break; case 6: { String downloadURL = (String) aValue; if (downloadURL == null) { downloadURL = ""; } String oldValue = row.getDownloadURL(); if (oldValue != null && oldValue.equals(downloadURL)) { return; // 変化なし } row.setDownloadURL(downloadURL); } break; default: return; } // 変更通知 fireTableRowsUpdated(rowIndex, columnIndex); } @Override public Class getColumnClass(int columnIndex) { switch (columnIndex) { case 0: return String.class; case 1: return String.class; case 2: return String.class; case 3: return String.class; case 4: return String.class; case 5: return Double.class; case 6: return String.class; } return String.class; } @Override public boolean isCellEditable(int rowIndex, int columnIndex) { if (columnIndex == 0 || columnIndex == 1 || columnIndex == 2) { // PARTS ID/Timetsmap/CATEGORYは変更不可 return false; } return true; } public void initModel(CharacterData characterData) { if (characterData == null) { throw new IllegalArgumentException(); } clear(); for (PartsCategory partsCategory : characterData.getPartsCategories()) { for (Map.Entry entry : characterData .getPartsSpecMap(partsCategory).entrySet()) { PartsIdentifier partsIdentifier = entry.getKey(); PartsSpec partsSpec = entry.getValue(); PartsManageTableRow row = new PartsManageTableRow(partsIdentifier, partsSpec); addRow(row); } } sortByAuthor(); } /** * ホームページを設定する.
* ホームページはAuthorに対して1つであるが、Authorが自由編集可能であるため便宜的にRowに持たせている.
* 結果として同じAuthorに対して同じ値を設定する必要がある.
* ホームページはテーブルに表示されないのでリスナーへの通知は行わない.
* @param author 作者、空またはnullは何もしない. * @param homepage ホームページ */ public void setHomepage(String author, String homepage) { if (author == null || author.length() == 0) { return; } for (PartsManageTableRow row : elements) { String targetAuthor = row.getAuthor(); if (targetAuthor == null) { targetAuthor = ""; } if (targetAuthor.equals(author)) { row.setHomepage(homepage); } } } /** * ホームページを取得する.
* 該当する作者がないか、作者がnullまたは空の場合は常にnullを返す.
* @param author 作者 * @return ホームページ、またはnull */ public String getHomepage(String author) { if (author == null || author.length() == 0) { return null; } for (PartsManageTableRow row : elements) { String targetAuthor = row.getAuthor(); if (targetAuthor == null) { targetAuthor = ""; } if (targetAuthor.equals(author)) { return row.getHomepage(); } } return null; } protected static final Comparator NAMED_SORTER = new Comparator() { public int compare(PartsManageTableRow o1, PartsManageTableRow o2) { // カテゴリで順序づけ int ret = o1.getPartsIdentifier().getPartsCategory().compareTo( o2.getPartsIdentifier().getPartsCategory()); if (ret == 0) { // 表示名で順序づけ String lnm1 = o1.getLocalizedName(); String lnm2 = o2.getLocalizedName(); if (lnm1 == null) { lnm1 = ""; } if (lnm2 == null) { lnm2 = ""; } ret = lnm1.compareTo(lnm2); } if (ret == 0) { // それでも判定できなければ元の識別子で判定する. ret = o1.getPartsIdentifier().compareTo(o2.getPartsIdentifier()); } return ret; } }; public void sortByName() { Collections.sort(elements, NAMED_SORTER); fireTableDataChanged(); } public void sortByTimestamp() { Collections.sort(elements, new Comparator() { public int compare(PartsManageTableRow o1, PartsManageTableRow o2) { // 更新日で順序づけ (新しいもの順) int ret = o1.getTimestamp().compareTo(o2.getTimestamp()) * -1; if (ret == 0) { // それでも判定できなければ名前順と同じ ret = NAMED_SORTER.compare(o1, o2); } return ret; } }); fireTableDataChanged(); } public void sortByAuthor() { Collections.sort(elements, new Comparator() { public int compare(PartsManageTableRow o1, PartsManageTableRow o2) { // 作者で順序づけ String author1 = o1.getAuthor(); String author2 = o2.getAuthor(); if (author1 == null) { author1 = ""; } if (author2 == null) { author2 = ""; } int ret = author1.compareTo(author2); if (ret == 0) { // それでも判定できなければ名前順と同じ ret = NAMED_SORTER.compare(o1, o2); } return ret; } }); fireTableDataChanged(); } } charactermanaj/src/charactermanaj/ui/MenuDataFactory.java0000644000175000017500000000577211767047542024021 0ustar paulliupaulliupackage charactermanaj.ui; import java.awt.event.ActionListener; import java.util.AbstractCollection; import java.util.Iterator; import java.util.NoSuchElementException; import java.util.Properties; public class MenuDataFactory extends AbstractCollection { private String name; private boolean checkbox; private ActionListener actionListener; private MenuDataFactory[] factories; public MenuDataFactory(String name) { this(name, false, null, null); } public MenuDataFactory(String name, MenuDataFactory[] factories) { this(name, false, null, factories); } public MenuDataFactory(String name, ActionListener actionListener) { this(name, false, actionListener, null); } public MenuDataFactory(String name, boolean checkbox, ActionListener actionListener) { this(name, checkbox, actionListener, null); } public String getName() { return name; } public boolean isCheckbox() { return checkbox; } public MenuDataFactory(String name, boolean checkbox, ActionListener actionListener, MenuDataFactory[] factories) { if (name == null || name.length() == 0) { throw new IllegalArgumentException(); } if (factories == null) { factories = new MenuDataFactory[0]; } this.name = name; this.checkbox = checkbox; this.actionListener = actionListener; this.factories = factories; } public MenuData createMenuData(Properties props) { if (props == null) { throw new IllegalArgumentException(); } String text = props.getProperty(name + ".text"); String mnemonic = props.getProperty(name + ".mnemonic"); String mnemonicDisp = props.getProperty(name + ".mnemonicDisp"); String ignoreMacOSX = props.getProperty(name + ".ignoreMacOSX"); String shortcutKey = props.getProperty(name + ".shortcut-key"); MenuData menuData = new MenuData(); menuData.setName(getName()); menuData.setCheckbox(isCheckbox()); menuData.setText(text); if (mnemonic != null && mnemonic.length() > 0) { menuData.setMnemonic(mnemonic.charAt(0)); menuData.setMnimonicDisp(mnemonicDisp); } menuData.setIgnoreMacOSX(ignoreMacOSX != null && Boolean.valueOf(ignoreMacOSX)); menuData.setActionListener(actionListener); menuData.setShortcutKey(shortcutKey); for (MenuDataFactory factory : factories) { if (factory != null) { menuData.add(factory.createMenuData(props)); } else { menuData.add(null); } } return menuData; } @Override public int size() { return factories.length; } @Override public Iterator iterator() { return new Iterator() { private int idx = 0; public boolean hasNext() { return idx < factories.length; } public MenuDataFactory next() { if (idx >= factories.length) { throw new NoSuchElementException(); } return factories[idx++]; } public void remove() { throw new UnsupportedOperationException(); } }; } } charactermanaj/src/charactermanaj/ui/Wallpaper.java0000644000175000017500000004556011767047542022721 0ustar paulliupaulliupackage charactermanaj.ui; import static java.lang.Math.ceil; import java.awt.AlphaComposite; import java.awt.Color; import java.awt.Composite; import java.awt.Graphics2D; import java.awt.GraphicsConfiguration; import java.awt.Image; import java.awt.Rectangle; import java.awt.Transparency; import java.awt.image.BufferedImage; import java.awt.image.VolatileImage; import java.beans.PropertyChangeListener; import java.beans.PropertyChangeSupport; import java.util.logging.Level; import java.util.logging.Logger; import charactermanaj.model.AppConfig; /** * 壁紙 * @author seraphy */ public class Wallpaper { /** * ロガー */ private static final Logger logger = Logger.getLogger(Wallpaper.class.getName()); /** * 壁紙の推奨されるブロックサイズ(幅).
* このサイズに満たない壁紙用画像は、このサイズに近い値まで敷き詰めて保持しておく.
*/ private static final int wallpaperPreferredWidth = 128; /** * 壁紙の推奨されるブロックサイズ(高さ).
* このサイズに満たない壁紙用画像は、このサイズに近い値まで敷き詰めて保持しておく.
*/ private static final int wallpaperPreferredHeight = 128; /** * プロパティ変更イベントのキー名 */ public static final String KEY_WALLPAPER_IMAGE = "wallpaperImage"; /** * プロパティ変更通知サポート. */ private PropertyChangeSupport propertyChangeSupport = new PropertyChangeSupport(this); /** * 背景色.
*/ private Color backgroundColor = Color.WHITE; /** * 壁紙画像.
*/ private BufferedImage wallpaperImg; /** * 壁紙画像のアルファ値.
*/ private float wallpaperAlpha = 1.f; /** * 壁紙用オフスクリーンサーフェイス.
* なければnull.
*/ private VolatileImage wallpaperVolatileImg; /** * 壁紙用オフスクリーンを生成したときに使用した背景色.
* なければnull */ private Color wallpaperVolatileBgColor; /** * 壁紙が表示されない状態で壁紙イメージを構築する.
*/ public Wallpaper() { this(null); } /** * 壁紙の元画像を指定して壁紙イメージを構築する.
* nullを指定した場合は壁紙は表示されない.
* @param wallpaperImg 壁紙イメージ、もしくはnull */ public Wallpaper(BufferedImage wallpaperImg) { this.wallpaperImg = makeExpandedWallpaper(makeExpandedWallpaper(wallpaperImg)); } /** * 壁紙画像を設定する.
* nullの場合は解除される.
* 壁紙が小さい場合は推奨されるブロックサイズまで敷き詰めなおした状態で * 保持する.(描画不可軽減のため.) * したがって、{@link #getWallpaperImage()}を呼び出したときには * 拡張されたサイズとなっている.
* @param wallpaperImg */ public void setWallpaperImage(BufferedImage wallpaperImg) { // 現在のオフスクリーンを破棄する. disposeOffscreen(); // 新しいイメージ BufferedImage wallpaperImgOld = makeExpandedWallpaper(this.wallpaperImg); this.wallpaperImg = makeExpandedWallpaper(wallpaperImg); propertyChangeSupport.firePropertyChange("wallpaperImage", wallpaperImgOld, this.wallpaperImg); } /** * 壁紙画像を取得する.
* 壁紙画像はブロックサイズまで拡張されたものとなっている.
* @return 壁紙画像、なければnull */ public BufferedImage getWallpaperImage() { return wallpaperImg; } public float getWallpaperAlpha() { return wallpaperAlpha; } /** * 壁紙画像を描画する場合のアルファ値を設定する.
* 負の値は0に、1以上は1に制限される.
* @param wallpaperAlpha アルファ値(0から1の範囲) */ public void setWallpaperAlpha(float wallpaperAlpha) { // 現在のオフスクリーンを破棄する. disposeOffscreen(); // 範囲無いに制限する. if (wallpaperAlpha < 0) { wallpaperAlpha = 0; } else if (wallpaperAlpha > 1.f) { wallpaperAlpha = 1.f; } // アルファ値を設定する. float oldalpha = this.wallpaperAlpha; if (oldalpha != wallpaperAlpha) { this.wallpaperAlpha = wallpaperAlpha; propertyChangeSupport.firePropertyChange("wallpaperAlpha", oldalpha, wallpaperAlpha); } } public Color getBackgroundColor() { return backgroundColor; } public void setBackgroundColor(Color backgroundColor) { // 現在のオフスクリーンを破棄する. disposeOffscreen(); // 色が省略された場合の補正. if (backgroundColor == null) { backgroundColor = Color.WHITE; } // 背景色を設定する. Color colorOld = this.backgroundColor; if ( !colorOld.equals(backgroundColor)) { this.backgroundColor = backgroundColor; propertyChangeSupport.firePropertyChange("backgroundColor", colorOld, backgroundColor); } } /** * 壁紙を左上(0,0)を原点に指定した幅・高さでタイル状に敷き詰めて描画します.
* 壁紙が設定されていなければ何もしません.
* アプリケーション設定でオフスクリーンの使用が有効である場合、グラフィクスコンテキストに * あわせてオフスクリーンイメージをあらかじめキャッシュとして作成して転送する.
* オフスクリーンは初回描画時に構築され、以降、必要に応じて再作成される.
* オフスクリーンを即座に破棄する場合には{@link #disposeOffscreen()}を呼び出す.
* @param g 描画先 * @param bgColor 背景色 * @param w 幅 (画面幅) * @param h 高さ (画面高) */ public void drawWallpaper(Graphics2D g, int w, int h) { drawWallpaper(g, w, h, false); } /** * 壁紙を左上(0,0)を原点に指定した幅・高さでタイル状に敷き詰めて描画します.
* 壁紙が設定されていなければ何もしません.
* アプリケーション設定でオフスクリーンの使用が有効である場合、グラフィクスコンテキストに * あわせてオフスクリーンイメージをあらかじめキャッシュとして作成して転送する.
* ただし、引数でオフスクリーンを使用しないと指定した場合はオフスクリーンには一切関知せず、 * 通常の画像による背景描画を行う.
* (この場合、オフスクリーンは作成されず、現在あるものを再作成も破棄もしない.) * @param g 描画先 * @param bgColor 背景色 * @param w 幅 (画面幅) * @param h 高さ (画面高) * @param noUseOffscreen オフスクリーンを使用しない.(たとえ利用可能であっても) */ public void drawWallpaper(Graphics2D g, int w, int h, boolean noUseOffscreen) { // 背景色 Color bgColor = getBackgroundColor(); // 背景色で塗りつぶす // (壁紙がある場合は不要) if (wallpaperImg == null) { fillBackgroundColor(g, bgColor, w, h); } // 壁紙を敷き詰める if (wallpaperImg != null) { // オフスクリーンによる背景描画が行われたか? boolean drawOffscreen = false; if ( !noUseOffscreen) { // オフクリーンサーフェイスのチェックまたは生成 AppConfig appConfig = AppConfig.getInstance(); if (appConfig.isEnableOffscreenWallpaper()) { checkOrCreateOffscreen(g, bgColor, w, h); } else { disposeOffscreen(); } // オフスクリーンフェイスが有効であれば、オフスクリーンで描画する. if (wallpaperVolatileImg != null) { int src_w = wallpaperVolatileImg.getWidth(); int src_h = wallpaperVolatileImg.getHeight(); drawWallpaper(g, w, h, wallpaperVolatileImg, src_w, src_h); // オフスクリーンがロストしていなければ // オフスクリーンで描画されたと判定する. drawOffscreen = !wallpaperVolatileImg.contentsLost(); } } // オフスクリーンサーフェイスで描画されていなければ通常の画像で描画する. if ( !drawOffscreen) { if (wallpaperVolatileImg != null && logger.isLoggable(Level.INFO)) { logger.log(Level.INFO, "offscreen is lost."); } fillBackgroundColor(g, bgColor, w, h); Composite oldcomp = g.getComposite(); try { float alpha = getWallpaperAlpha(); if (alpha < 1.f) { // アルファが100%の場合は、あえて設定しない. AlphaComposite comp = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, alpha); g.setComposite(comp); } int src_w = wallpaperImg.getWidth(); int src_h = wallpaperImg.getHeight(); drawWallpaper(g, w, h, wallpaperImg, src_w, src_h); } finally { g.setComposite(oldcomp); } } } } /** * 現在使用しているオフスクリーン用ネイティブリソースを破棄する.
*/ protected void disposeOffscreen() { if (wallpaperVolatileImg != null) { wallpaperVolatileImg.flush(); wallpaperVolatileImg = null; if (logger.isLoggable(Level.FINER)) { logger.log(Level.FINER, "オフスクリーンを破棄しました。"); } } } /** * オフスクリーンイメージが有効であるかチェックし、 * 有効でなければオフスクリーンを再作成、もしくは再描画する.
* オフスクリーンは背景画像と同じサイズで作成される.
* 背景画像が設定されていなければオフスクリーンも無効とする.
* @param g 実際のスクリーンデバイス(互換性あるオフスクリーンを作成するため) * @param bgColor 背景色 * @param offscreen_max_w オフスクリーンの最大サイズ(幅) * @param offscreen_max_h オフスクリーンの最大サイズ(高さ) */ protected void checkOrCreateOffscreen(Graphics2D g, Color bgColor, int offscreen_max_w, int offscreen_max_h) { if (wallpaperImg == null) { // 壁紙もと画像がなければ何もしない. disposeOffscreen(); return; } try { GraphicsConfiguration gConf = g.getDeviceConfiguration(); // オフスクリーンの状態を確認する. int validate = VolatileImage.IMAGE_INCOMPATIBLE; if (wallpaperVolatileImg != null) { validate = wallpaperVolatileImg.validate(gConf); if (logger.isLoggable(Level.FINEST)) { logger.log(Level.FINEST, "オフスクリーンの状態: " + validate); } } // 構築時の背景色と変更があるか? if (validate == VolatileImage.IMAGE_OK && (wallpaperVolatileBgColor != null && bgColor != null)) { if ( !wallpaperVolatileBgColor.equals(bgColor)) { validate = VolatileImage.IMAGE_RESTORED; } } // 壁紙元画像サイズ int src_w = wallpaperImg.getWidth(); int src_h = wallpaperImg.getHeight(); // オフスクリーンサイズの算定. // 要求された最大幅かアプリ設定の最大幅の小さいほうを最大幅とし、 // それが壁紙もと画像よりも小さければ壁紙サイズと同じとする. AppConfig appConfig = AppConfig.getInstance(); int offscreen_w = appConfig.getOffscreenWallpaperSize(); int offscreen_h = appConfig.getOffscreenWallpaperSize(); offscreen_w = Math.max(src_w, Math.min(offscreen_max_w, offscreen_w)); offscreen_h = Math.max(src_h, Math.min(offscreen_max_h, offscreen_h)); // ブロックサイズを満たすために必要な元サイズの繰り返し数 int nx = (int) ceil((double) offscreen_w / src_w); int ny = (int) ceil((double) offscreen_h / src_h); // 繰り返し数からブロックサイズに近い元サイズで割り切れるサイズを求める offscreen_w = src_w * nx; offscreen_h = src_h * ny; // オフスクリーンが有効、もしくは描画が必要である状態の場合にサイズのチェックを行う. if (validate == VolatileImage.IMAGE_OK || validate == VolatileImage.IMAGE_RESTORED) { int currentOffW = Math.max(1, wallpaperVolatileImg.getWidth()); int currentOffH = Math.max(1, wallpaperVolatileImg.getHeight()); double ratioW = ((double) offscreen_w / currentOffW); double ratioH = ((double) offscreen_h / currentOffH); // オフスクリーンの描画済みサイズが要求サイズの2割を超えるか割り込んだ場合は // 再作成が必要. if (ratioW < 0.8 || ratioW > 1.2 || ratioH < 0.8 || ratioH > 1.2) { validate = VolatileImage.IMAGE_INCOMPATIBLE; if (logger.isLoggable(Level.FINE)) { logger.log(Level.FINE, "オフスクリーンサイズの変更が必要です。: " + ratioW + "," + ratioH); } } } // オフスクリーンの状態が再構築または再描画が必要であるか? if (validate != VolatileImage.IMAGE_OK ) { // オフスクリーンがないか、コンパチでなくなっている場合はオフスクリーンを生成する. if (wallpaperVolatileImg == null || validate == VolatileImage.IMAGE_INCOMPATIBLE) { // 現在使用しているネイティブリソースを破棄する. disposeOffscreen(); // 新しいオフスクリーンサーフェイスを作成する. wallpaperVolatileImg = gConf.createCompatibleVolatileImage( offscreen_w, offscreen_h, Transparency.OPAQUE); if (wallpaperVolatileImg == null) { logger.log(Level.INFO, "オフスクリーンイメージは生成できません。"); } else { if (logger.isLoggable(Level.FINER)) { logger.log(Level.FINER, "オフスクリーンを構築しました。(サイズ:" + offscreen_w + "," + offscreen_h + ")"); } } } // 再描画する. if (wallpaperVolatileImg != null) { if (logger.isLoggable(Level.FINER)) { logger.log(Level.FINER, "オフスクリーンの描画 (サイズ:" + offscreen_w + "," + offscreen_h + ")"); } Graphics2D vg = wallpaperVolatileImg.createGraphics(); try { int ow = wallpaperVolatileImg.getWidth(); int oh = wallpaperVolatileImg.getHeight(); fillBackgroundColor(vg, bgColor, ow, oh); Composite oldcomp = vg.getComposite(); try { float alpha = getWallpaperAlpha(); if (alpha < 1.f) { // アルファが100%の場合は、あえて設定しない. AlphaComposite comp = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, alpha); vg.setComposite(comp); } drawWallpaper(vg, ow, oh, wallpaperImg, src_w, src_h); } finally { vg.setComposite(oldcomp); } } finally { vg.dispose(); } wallpaperVolatileBgColor = bgColor; } } } catch (RuntimeException ex) { logger.log(Level.SEVERE, "オフスクリーンイメージの生成中に例外が発生しました。", ex); // 現在使用しているネイティブリソースを破棄する. disposeOffscreen(); } } /** * 背景色で塗りつぶす * @param g 対象 * @param bgColor 背景色、nullの場合は何もしない. * @param w 幅 * @param h 高さ */ protected void fillBackgroundColor(Graphics2D g, Color bgColor, int w, int h) { if (bgColor == null) { return; } Color oldc = g.getColor(); try { Rectangle clip = g.getClipBounds(); if (clip == null) { clip = new Rectangle(0, 0, w, h); } g.setColor(bgColor); g.fill(clip); } finally { g.setColor(oldc); } } /** * 壁紙を指定の領域まで敷き詰めて描画する.
* @param g * @param w 敷き詰めるサイズ(幅) * @param h 敷き詰めるサイズ(高さ) * @param wallpaperImg 敷き詰める画像 * @param src_w 壁紙画像のサイズ * @param src_v 壁紙画像の高さ */ protected void drawWallpaper(Graphics2D g, int w, int h, Image wallpaperImg, int src_w, int src_h) { if (wallpaperImg == null) { return; } // 表示範囲で表示できる壁紙を表示できる個数 int nx = (int) ceil((double) w / src_w); int ny = (int) ceil((double) h / src_h); // 描画対象領域 Rectangle clip = g.getClipBounds(); // 描画対象領域にかかる壁紙を描画する. Rectangle rct = new Rectangle(0, 0, src_w, src_h); for (int iy = 0; iy <= ny; iy++) { for (int ix = 0; ix <= nx; ix++) { rct.x = ix * src_w; rct.y = iy * src_h; if (clip == null || clip.intersects(rct)) { g.drawImage( wallpaperImg, rct.x, rct.y, rct.x + rct.width, rct.y + rct.height, 0, 0, src_w, src_h, null ); } } } } /** * 壁紙を一定の大きさに敷き詰める.
* すでに十分大きい場合は何もしない.
* @param wallpaper 対象のイメージ * @return 拡張後のイメージ、もしくは同じイメージ */ protected BufferedImage makeExpandedWallpaper(BufferedImage wallpaper) { if (wallpaper == null) { return null; } // 敷き詰める画像の元サイズ int src_w = wallpaper.getWidth(); int src_h = wallpaper.getHeight(); // ブロックサイズよりも元サイズが大きければ何もしない. if (src_w > wallpaperPreferredWidth && src_h > wallpaperPreferredHeight) { return wallpaper; } // ブロックサイズを満たすために必要な元サイズの繰り返し数 int nx = (int) ceil((double) wallpaperPreferredWidth / src_w); int ny = (int) ceil((double) wallpaperPreferredHeight / src_h); // 繰り返し数からブロックサイズに近い元サイズで割り切れるサイズを求める int w = src_w * nx; int h = src_h * ny; // ブロックサイズまで元画像を敷き詰める BufferedImage wallpaperNew = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB); Graphics2D g = wallpaperNew.createGraphics(); try { drawWallpaper(g, w, h, wallpaper, src_w, src_h); } finally { g.dispose(); } return wallpaperNew; } public void addPropertyChangeListener(PropertyChangeListener listener) { propertyChangeSupport.addPropertyChangeListener(listener); } public void addPropertyChangeListener(String propertyName, PropertyChangeListener listener) { propertyChangeSupport.addPropertyChangeListener(propertyName, listener); } public void removePropertyChangeListener(PropertyChangeListener listener) { propertyChangeSupport.removePropertyChangeListener(listener); } public void removePropertyChangeListener(String propertyName, PropertyChangeListener listener) { propertyChangeSupport.removePropertyChangeListener(propertyName, listener); } } charactermanaj/src/charactermanaj/ui/SamplePicturePanel.java0000644000175000017500000002006511767047542024520 0ustar paulliupaulliupackage charactermanaj.ui; import static java.lang.Math.min; import java.awt.Color; import java.awt.Component; import java.awt.Container; import java.awt.Dimension; import java.awt.Font; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Insets; import java.awt.Rectangle; import java.awt.RenderingHints; import java.awt.Shape; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.font.FontRenderContext; import java.awt.image.BufferedImage; import java.util.Properties; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JViewport; import charactermanaj.model.AppConfig; import charactermanaj.ui.util.ScrollPaneDragScrollSupport; import charactermanaj.util.LocalizedResourcePropertyLoader; /** * サンプルピクチャ用パネル.
* ピクチャの自動縮小と等倍表示切り替えをサポートする.
* * @author seraphy */ public class SamplePicturePanel extends JPanel { private static final long serialVersionUID = 4026181978500938152L; protected BufferedImage samplePicture; protected boolean visiblePicture = true; protected Color sampleImageBgColor = AppConfig.getInstance().getSampleImageBgColor(); protected String alternateText = ""; protected boolean enableRealsize; /** * マウスドラッグによるスクロールのサポート */ protected ScrollPaneDragScrollSupport scrollSupport; { setMinimumSize(new Dimension(64, 64)); addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { if (e.getClickCount() == 2) { // 正確に2回 onDblClick(); e.consume(); } } }); } @Override public void addNotify() { super.addNotify(); // 親がJScrollPaneである場合、 // マウスのドラッグによるスクロールをサポートするようにマウスリスナをセットアップする. Component parent = getParent(); if (parent != null && (parent instanceof JViewport)) { Component gparent = parent.getParent(); if (gparent != null && (gparent instanceof JScrollPane)) { JScrollPane scrollPane = (JScrollPane) gparent; scrollSupport = new ScrollPaneDragScrollSupport(scrollPane); scrollSupport.installDraggingListener(this, null); } } } @Override public void removeNotify() { if (scrollSupport != null) { scrollSupport.uninstallDraggingListener(); scrollSupport = null; } super.removeNotify(); } public SamplePicturePanel() { super(); } public SamplePicturePanel(BufferedImage samplePicture) { super(); this.samplePicture = samplePicture; this.alternateText = ""; this.enableRealsize = false; adjustPreferrerdSize(false); } public SamplePicturePanel(BufferedImage samplePicture, String alternateText) { super(); if (alternateText == null) { alternateText = ""; } this.samplePicture = samplePicture; this.alternateText = alternateText; this.enableRealsize = false; adjustPreferrerdSize(false); } @Override protected void paintComponent(Graphics g0) { Graphics2D g = (Graphics2D) g0; super.paintComponent(g); if (samplePicture != null && isVisiblePicture()) { Rectangle rct = getBounds(); Insets insets = getInsets(); int x = insets.left; int y = insets.top; int w = rct.width - insets.left - insets.right; int h = rct.height - insets.top - insets.bottom; int imgW = samplePicture.getWidth(); int imgH = samplePicture.getHeight(); double factor1 = (double) h / (double) imgH; // 縦を納めた場合の、縦の縮小率 double factor2 = (double) w / (double) imgW; // 横を納めた場合の、横の縮小率 double factor = min(factor1, factor2); // 縦横を納めるのに最低必要な縮小率 int scaledW = (int)(imgW * factor); int scaledH = (int)(imgH * factor); int offset_x = (w - scaledW) / 2; int offset_y = (h - scaledH) / 2; Object renderingHint; if (factor <= 1.) { // 等倍未満 renderingHint = RenderingHints.VALUE_INTERPOLATION_BILINEAR; } else if (factor <= 2.) { // 2倍まで renderingHint = RenderingHints.VALUE_INTERPOLATION_BICUBIC; } else { // それ以上 renderingHint = RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR; } g.setRenderingHint( RenderingHints.KEY_INTERPOLATION, renderingHint); g.drawImage(samplePicture, x + offset_x, y + offset_y, x + offset_x + scaledW, y + offset_y + scaledH, 0, 0, imgW, imgH, sampleImageBgColor, null); } else if (alternateText.length() > 0) { Rectangle rct = getBounds(); Insets insets = getInsets(); int x = insets.left; int y = insets.top; int w = rct.width - insets.left - insets.right; int h = rct.height - insets.top - insets.bottom; Font font = g.getFont(); FontRenderContext frc = g.getFontRenderContext(); int textHeight = (int) font.getMaxCharBounds(frc).getHeight(); Shape clipOld = g.getClip(); g.setClip(x, y, w, h); g.drawString(alternateText, x, y + textHeight); g.setClip(clipOld); } } public void adjustPreferrerdSize(boolean fullsize) { Dimension minSize = getMinimumSize(); Dimension siz = minSize; String tooltip = null; if (samplePicture != null) { int div = fullsize ? 1 : 2; Insets insets = getInsets(); siz = new Dimension(samplePicture.getWidth() / div + insets.left + insets.right, samplePicture.getHeight() / div + insets.top + insets.bottom); Container parent = getParent(); if (parent != null && parent instanceof JViewport) { Properties strings = LocalizedResourcePropertyLoader .getInstance().getLocalizedProperties("languages/samplepicturepanel"); if (!fullsize) { tooltip = strings.getProperty("dblclick.fullsize"); } else { tooltip = strings.getProperty("dblclick.fit"); } } } setToolTipText(tooltip); siz.width = Math.max(minSize.width, siz.width); siz.height = Math.max(minSize.height, siz.height); Dimension ord = getPreferredSize(); if (ord == null || !ord.equals(siz)) { setPreferredSize(siz); revalidate(); } } @Override public Dimension getPreferredSize() { Container parent = getParent(); if (!enableRealsize && parent != null && parent instanceof JViewport) { JViewport viewport = (JViewport) parent; Dimension siz = viewport.getExtentSize(); Insets insets = viewport.getInsets(); Dimension preferredSize = new Dimension(siz.width - insets.left - insets.right, siz.height - insets.top - insets.bottom); return preferredSize; } return super.getPreferredSize(); } protected void onDblClick() { Container parent = getParent(); if (parent != null && parent instanceof JViewport) { enableRealsize = !enableRealsize; adjustPreferrerdSize(enableRealsize); } } public boolean isVisiblePicture() { return visiblePicture; } public void setVisiblePicture(boolean visiblePicture) { if (this.visiblePicture != visiblePicture) { this.visiblePicture = visiblePicture; repaint(); } } public void setSamplePicture(BufferedImage samplePicture) { if (this.samplePicture != samplePicture) { this.samplePicture = samplePicture; enableRealsize = false; adjustPreferrerdSize(false); repaint(); } } public BufferedImage getSamplePictrue() { return this.samplePicture; } public Color getSamplePictureBgColor() { return this.sampleImageBgColor; } public void setSamplePictureBgColor(Color color) { if (color == null) { throw new IllegalArgumentException(); } if (!sampleImageBgColor.equals(color)) { this.sampleImageBgColor = color; repaint(); } } public String getAlternateText() { return alternateText; } public void setAlternateText(String alternateText) { if (alternateText == null) { alternateText = ""; } if (!this.alternateText.equals(alternateText)) { this.alternateText = alternateText; repaint(); } } } charactermanaj/src/charactermanaj/ui/WallpaperDialog.java0000644000175000017500000003673511767047542024045 0ustar paulliupaulliupackage charactermanaj.ui; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Container; import java.awt.Dimension; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.Insets; import java.awt.Toolkit; import java.awt.event.ActionEvent; import java.awt.event.KeyEvent; import java.awt.event.WindowAdapter; import java.io.File; import java.util.ArrayList; import java.util.Properties; import javax.swing.AbstractAction; import javax.swing.ActionMap; import javax.swing.BorderFactory; import javax.swing.Box; import javax.swing.BoxLayout; import javax.swing.ButtonGroup; import javax.swing.InputMap; import javax.swing.JButton; import javax.swing.JComponent; import javax.swing.JDialog; import javax.swing.JFileChooser; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JList; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JRadioButton; import javax.swing.JRootPane; import javax.swing.JScrollPane; import javax.swing.JSpinner; import javax.swing.JTextField; import javax.swing.KeyStroke; import javax.swing.SpinnerNumberModel; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; import charactermanaj.Main; import charactermanaj.graphics.io.ImagePreviewFileChooser; import charactermanaj.ui.model.PredefinedWallpaper; import charactermanaj.ui.model.WallpaperInfo; import charactermanaj.ui.model.WallpaperInfo.WallpaperResourceType; import charactermanaj.util.LocalizedMessageComboBoxRender; import charactermanaj.util.LocalizedResourcePropertyLoader; /** * 壁紙選択ダイアログ * @author seraphy */ public class WallpaperDialog extends JDialog { private static final long serialVersionUID = 1L; /** * リソース */ protected static final String STRINGS_RESOURCE = "languages/wallpaperdialog"; /** * 定義済み壁紙のキャッシュ */ private static ArrayList predefinedWallpapers = new ArrayList(); /** * 壁紙情報 */ private WallpaperInfo wallpaperInfo = new WallpaperInfo(); /** * 選択なしラジオ */ private JRadioButton radioNone; /** * ファイルから選択ラジオ */ private JRadioButton radioFile; /** * 定義済みから選択ラジオ */ private JRadioButton radioPredefined; /** * 定義済み壁紙リスト */ private JList listPredefinedWallpapers; /** * 背景画像の透過率 */ private JSpinner spinnerAlpha; /** * 選択ファイルフィールド */ private JTextField txtFile; /** * ファイルを選択アクション */ private AbstractAction actChooseFile; /** * 背景色選択コンポーネント */ private ColorBox colorBox; /** * コンストラクタ * @param parent 親フレーム */ public WallpaperDialog(JFrame parent) { super(parent, true); try { setDefaultCloseOperation(JDialog.DO_NOTHING_ON_CLOSE); addWindowListener(new WindowAdapter() { public void windowClosing(java.awt.event.WindowEvent e) { onCancel(); }; }); initPredefinedWallpapers(); initComponent(); } catch (RuntimeException ex) { dispose(); throw ex; } } /** * 定義済み壁紙リストを初期化する.
* 既に初期化済みであれば何もしない.
*/ private static synchronized void initPredefinedWallpapers() { if ( !predefinedWallpapers.isEmpty()) { return; } predefinedWallpapers.addAll(PredefinedWallpaper.getPredefinedWallpapers()); } /** * このダイアログのコンポーネントを初期化します.
*/ private void initComponent() { Properties strings = LocalizedResourcePropertyLoader.getInstance() .getLocalizedProperties(WallpaperDialog.STRINGS_RESOURCE); setTitle(strings.getProperty("title")); Container contentPane = getContentPane(); contentPane.setLayout(new BorderLayout(3, 3)); getRootPane().setBorder(BorderFactory.createEmptyBorder(3, 3, 3, 3)); JPanel wallpaperPanel = createWallpaperChoosePanel(strings); JPanel bgcolorPanel = createBgColorPanel(strings); JPanel btnPanel = createButtonPanel(strings); contentPane.add(bgcolorPanel, BorderLayout.NORTH); contentPane.add(wallpaperPanel, BorderLayout.CENTER); contentPane.add(btnPanel, BorderLayout.SOUTH); setSize(400, 350); setLocationRelativeTo(getParent()); } private JPanel createButtonPanel(Properties strings) { JPanel btnPanel = new JPanel(); AbstractAction actOK = new AbstractAction(strings.getProperty("btn.ok")) { private static final long serialVersionUID = 1L; public void actionPerformed(ActionEvent e) { onOK(); } }; AbstractAction actCancel = new AbstractAction(strings.getProperty("btn.cancel")) { private static final long serialVersionUID = 1L; public void actionPerformed(ActionEvent e) { onCancel(); } }; JButton btnOK = new JButton(actOK); JButton btnCancel = new JButton(actCancel); BoxLayout bl = new BoxLayout(btnPanel, BoxLayout.LINE_AXIS); btnPanel.setLayout(bl); btnPanel.setBorder(BorderFactory.createEmptyBorder(3, 3, 3, 45)); btnPanel.add(Box.createHorizontalGlue()); if (Main.isLinuxOrMacOSX()) { btnPanel.add(btnCancel); btnPanel.add(btnOK); } else { btnPanel.add(btnOK); btnPanel.add(btnCancel); } // Enter/Returnキーを既定ボタンにする. JRootPane rootPane = getRootPane(); rootPane.setDefaultButton(btnOK); // CTRL-Wでウィンドウを非表示にする. (ESCでは非表示にしない) InputMap im = rootPane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW); ActionMap am = rootPane.getActionMap(); Toolkit tk = Toolkit.getDefaultToolkit(); im.put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "closeWallpaperDialog"); im.put(KeyStroke.getKeyStroke(KeyEvent.VK_W, tk.getMenuShortcutKeyMask()), "closeWallpaperDialog"); am.put("closeWallpaperDialog", actCancel); return btnPanel; } private JPanel createBgColorPanel(Properties strings) { JPanel bgcolorPanel = new JPanel(); bgcolorPanel.setBorder( BorderFactory.createCompoundBorder( BorderFactory.createTitledBorder(strings.getProperty("group.bgcolor")), BorderFactory.createEmptyBorder(3, 3, 3, 3))); GridBagLayout gbl = new GridBagLayout(); bgcolorPanel.setLayout(gbl); colorBox = new ColorBox(); colorBox.getColorDisplayPanel().setPreferredSize(new Dimension(48, 20)); GridBagConstraints gbc = new GridBagConstraints(); gbc.ipadx = 0; gbc.ipady = 0; gbc.insets = new Insets(0, 0, 0, 0); gbc.weighty = 0.; gbc.gridheight = 1; gbc.anchor = GridBagConstraints.CENTER; gbc.fill = GridBagConstraints.NONE; bgcolorPanel.add(colorBox, gbc); return bgcolorPanel; } private JPanel createWallpaperChoosePanel(Properties strings) { JPanel wallpaperPanel = new JPanel(); wallpaperPanel.setBorder(BorderFactory.createTitledBorder(strings.getProperty("group.wallpaper"))); GridBagLayout gbl = new GridBagLayout(); wallpaperPanel.setLayout(gbl); radioNone = new JRadioButton(strings.getProperty("radio.none")); radioFile = new JRadioButton(strings.getProperty("radio.file")); radioPredefined = new JRadioButton(strings.getProperty("radio.predefined")); ButtonGroup btnGroup = new ButtonGroup(); btnGroup.add(radioNone); btnGroup.add(radioFile); btnGroup.add(radioPredefined); radioNone.setSelected(true); txtFile = new JTextField(); actChooseFile = new AbstractAction(strings.getProperty("btn.chooseFile")) { private static final long serialVersionUID = 1L; public void actionPerformed(ActionEvent e) { onChooseFile(); } }; JButton btnChooseFile = new JButton(actChooseFile); listPredefinedWallpapers = new JList(predefinedWallpapers .toArray(new PredefinedWallpaper[predefinedWallpapers.size()])); listPredefinedWallpapers.setCellRenderer(new LocalizedMessageComboBoxRender(strings)); SpinnerNumberModel alphaSpModel = new SpinnerNumberModel(100, 0, 100, 1); spinnerAlpha = new JSpinner(alphaSpModel); GridBagConstraints gbc = new GridBagConstraints(); gbc.ipadx = 0; gbc.ipady = 0; gbc.insets = new Insets(0, 0, 0, 0); gbc.weighty = 0.; gbc.gridheight = 1; gbc.anchor = GridBagConstraints.WEST; gbc.fill = GridBagConstraints.BOTH; gbc.gridx = 0; gbc.gridy = 0; gbc.gridwidth = 3; gbc.weightx = 0.; wallpaperPanel.add(radioNone, gbc); gbc.gridx = 0; gbc.gridy = 1; gbc.gridwidth = 3; gbc.weightx = 0.; wallpaperPanel.add(radioFile, gbc); gbc.gridx = 0; gbc.gridy = 2; gbc.gridwidth = 1; gbc.weightx = 0.; wallpaperPanel.add(Box.createHorizontalStrut(20), gbc); gbc.gridx = 1; gbc.gridy = 2; gbc.gridwidth = 1; gbc.weightx = 1.; wallpaperPanel.add(txtFile, gbc); gbc.gridx = 2; gbc.gridy = 2; gbc.gridwidth = 1; gbc.weightx = 0.; wallpaperPanel.add(btnChooseFile, gbc); gbc.gridx = 0; gbc.gridy = 3; gbc.gridwidth = 3; gbc.weightx = 0.; wallpaperPanel.add(radioPredefined, gbc); gbc.gridx = 1; gbc.gridy = 4; gbc.gridwidth = 2; gbc.weightx = 1.; gbc.weighty = 1.; wallpaperPanel.add(new JScrollPane(listPredefinedWallpapers), gbc); gbc.gridx = 0; gbc.gridy = 5; gbc.gridwidth = 3; gbc.weightx = 0.; gbc.weighty = 0.; gbc.insets = new Insets(5, 0, 0, 0); wallpaperPanel.add(new JLabel( strings.getProperty("label.wallpaperImageAlpha")), gbc); JPanel alphaPanel = new JPanel(new BorderLayout(3, 3)); alphaPanel.setBorder(BorderFactory.createEmptyBorder(3, 0, 3, 0)); alphaPanel.add(spinnerAlpha, BorderLayout.CENTER); alphaPanel.add(new JLabel("%"), BorderLayout.EAST); gbc.gridx = 1; gbc.gridy = 6; gbc.gridwidth = 2; gbc.weightx = 0.; gbc.weighty = 0.; gbc.ipady = 0; gbc.insets = new Insets(0, 0, 0, 0); gbc.fill = GridBagConstraints.NONE; gbc.anchor = GridBagConstraints.WEST; wallpaperPanel.add(alphaPanel, gbc); // リスナ listPredefinedWallpapers.addListSelectionListener(new ListSelectionListener() { public void valueChanged(ListSelectionEvent e) { radioPredefined.setSelected(true); } }); return wallpaperPanel; } protected void onChooseFile() { File selectedFile = wallpaperInfo.getFile(); final JFileChooser fileChooser = new ImagePreviewFileChooser(); fileChooser.setSelectedFile(selectedFile); if (fileChooser.showOpenDialog(this) != JFileChooser.APPROVE_OPTION) { return; } selectedFile = fileChooser.getSelectedFile(); txtFile.setText(selectedFile == null ? "" : selectedFile.getPath()); radioFile.setSelected(selectedFile != null); } /** * 壁紙情報インスタンスの内容でコンポーネントを設定する. */ protected void applyByWallpaperInfo() { // ファイル File imageFile = wallpaperInfo.getFile(); txtFile.setText(imageFile == null ? "" : imageFile.getPath()); // リソース PredefinedWallpaper selectedPredefinedWp = null; String resource = wallpaperInfo.getResource(); for (PredefinedWallpaper predefinedWp : predefinedWallpapers) { if (predefinedWp.getResource().equals(resource)) { selectedPredefinedWp = predefinedWp; } } listPredefinedWallpapers.setSelectedValue(selectedPredefinedWp, true); // 透過率 int alphaInt = (int)(wallpaperInfo.getAlpha() * 100); spinnerAlpha.setValue(alphaInt); // 背景色 Color bgColor = wallpaperInfo.getBackgroundColor(); colorBox.setColorKey(bgColor); // タイプ WallpaperResourceType typ = wallpaperInfo.getType(); if (typ == WallpaperResourceType.FILE) { radioFile.setSelected(true); } else if (typ == WallpaperResourceType.PREDEFINED) { radioPredefined.setSelected(true); } else { radioNone.setSelected(true); } } /** * ダイアログのコンポーネントの状態から壁紙情報を構築して返す.
* 生成した壁紙情報が妥当であるかは検証しない.
* @return 新しい壁紙情報インスタンス */ public WallpaperInfo createWallpaperInfo() { WallpaperInfo wallpaperInfo = this.wallpaperInfo.clone(); // 選択したタイプ WallpaperResourceType typ; if (radioFile.isSelected()) { // 背景画像ファイル選択 typ = WallpaperResourceType.FILE; } else if (radioPredefined.isSelected()) { // リソース選択 typ = WallpaperResourceType.PREDEFINED; } else { // それ以外は選択なし typ = WallpaperResourceType.NONE; } // 画像ファイルの現在の選択 String strSelectedFile = txtFile.getText(); File selectedFile = null; if (strSelectedFile != null) { strSelectedFile = strSelectedFile.trim(); if (strSelectedFile.length() > 0) { selectedFile = new File(strSelectedFile); } } wallpaperInfo.setFile(selectedFile); // 定義済みリソースの現在の選択 PredefinedWallpaper predefinedWp = (PredefinedWallpaper) listPredefinedWallpapers.getSelectedValue(); wallpaperInfo.setResource(predefinedWp == null ? null : predefinedWp.getResource()); // タイプの設定 wallpaperInfo.setType(typ); // 背景画像の透過率 int alphaInt = (Integer) spinnerAlpha.getValue(); float alpha = (float) alphaInt / 100.f; wallpaperInfo.setAlpha(alpha); // 背景色 Color bgColor = colorBox.getColorKey(); wallpaperInfo.setBackgroundColor(bgColor); return wallpaperInfo; } protected void onOK() { WallpaperInfo wallpaperInfo = createWallpaperInfo(); if (!checkValidate(wallpaperInfo)) { return; } this.wallpaperInfo = wallpaperInfo; dispose(); } protected void onCancel() { wallpaperInfo = null; dispose(); } protected boolean checkValidate(WallpaperInfo wallpaperInfo) { String messageid = null; WallpaperResourceType typ = wallpaperInfo.getType(); if (typ == WallpaperResourceType.FILE) { File selectedFile = wallpaperInfo.getFile(); if (selectedFile == null) { messageid = "error.require.imageFile"; } else if (!selectedFile.exists() || !selectedFile.isFile() || !selectedFile.canRead()) { messageid = "error.invalid.imageFile"; } } else if (typ == WallpaperResourceType.PREDEFINED) { String resource = wallpaperInfo.getResource(); if (resource == null || resource.trim().length() == 0) { messageid = "error.require.resource"; } } if (messageid != null) { Properties strings = LocalizedResourcePropertyLoader.getInstance() .getLocalizedProperties(WallpaperDialog.STRINGS_RESOURCE); String message = strings.getProperty(messageid); JOptionPane.showMessageDialog(this, message, "ERROR", JOptionPane.ERROR_MESSAGE); } return messageid == null; } public void setWallpaperInfo(WallpaperInfo wallpaperInfo) { if (wallpaperInfo == null) { wallpaperInfo = new WallpaperInfo(); } this.wallpaperInfo = wallpaperInfo; } public WallpaperInfo getWallpaperInfo() { return wallpaperInfo; } /** * ダイアログを表示し、その結果を返す.
* キャンセルされた場合はnullが返される.
* @return 結果、もしくはnull */ public WallpaperInfo showDialog() { applyByWallpaperInfo(); setVisible(true); return wallpaperInfo; } } charactermanaj/src/charactermanaj/ui/ArchiveFileDialog.java0000644000175000017500000000615511767047542024270 0ustar paulliupaulliupackage charactermanaj.ui; import java.awt.Component; import java.io.File; import javax.swing.filechooser.FileFilter; /** * エクスポート・インポート用ファイルダイアログ * @author seraphy * */ public class ArchiveFileDialog { /** * 最後に使ったディレクトリ */ protected File lastUsedDir; /** * 最後に使用したフィルタ */ protected FileFilter lastUsedFileFiler; /** * 保存ダイアログを開く * @param parent 親 * @return 保存先ファイル、キャンセルした場合はnull */ public File showSaveDialog(Component parent) { ArchiveFileChooser fileChooser = new ArchiveFileChooser(lastUsedDir, true); int ret = fileChooser.showSaveDialog(parent); if (ret != ArchiveFileChooser.APPROVE_OPTION) { return null; } File outFile = fileChooser.getSelectedFile(); lastUsedDir = outFile.getParentFile(); return outFile; } /** * 開くダイアログを開く.
* initFileがnullの場合は前回選択したディレクトリが初期状態となる. * @param parent 親 * @param initFile 初期選択ファイル、もしくはディレクトリ、もしくはnull * @return 選択ファイル、キャンセルした場合はnull */ public File showOpenDialog(Component parent, File initFile) { // 初期ファイル名が指定されている場合、そのディレクトリをカレントにしてみる. // 指定されてなければ最後に使ったディレクトリを設定する. File initDir = null; if (initFile != null) { if (initFile.isDirectory()) { initDir = initFile; } else { initDir = initFile.getParentFile(); } } // 初期ファイルの指定がなければ前回の最後に使用したディレクトリを使用 if (initDir == null) { initDir = lastUsedDir; } ArchiveFileChooser fileChooser = new ArchiveFileChooser(initDir, false); // 最後に使用したフィルタがあれば、それを選択状態とする. if (lastUsedFileFiler != null) { fileChooser.setFileFilter(lastUsedFileFiler); } // 初期ファイル名が指定されていれば、それを選択状態としてみる. if (initFile != null) { fileChooser.setSelectedFile(initFile); } // ファイル選択ダイアログの表示 int ret = fileChooser.showOpenDialog(parent); if (ret != ArchiveFileChooser.APPROVE_OPTION) { // キャンセル return null; } // 選択ファイルの取得 File outFile = fileChooser.getSelectedFile(); // 最後に選択したディレクトリとフィルタを記憶する. lastUsedDir = outFile.getParentFile(); lastUsedFileFiler = fileChooser.getFileFilter(); return outFile; } /** * 最後に使用したディレクトリを取得する. * @return */ public File getLastUSedDir() { return lastUsedDir; } /** * 最後に使用したディレクトリを設定する. * @param lastUSedDir */ public void setLastUSedDir(File lastUSedDir) { this.lastUsedDir = lastUSedDir; } } charactermanaj/src/charactermanaj/ui/ExportWizardDialog.java0000644000175000017500000015471211767047542024554 0ustar paulliupaulliupackage charactermanaj.ui; import java.awt.BorderLayout; import java.awt.CardLayout; import java.awt.Color; import java.awt.Component; import java.awt.Container; import java.awt.Cursor; import java.awt.Dimension; import java.awt.Font; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.Insets; import java.awt.Rectangle; import java.awt.Toolkit; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.ComponentAdapter; import java.awt.event.ComponentEvent; import java.awt.event.ComponentListener; import java.awt.event.KeyEvent; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; import java.io.PrintWriter; import java.io.StringWriter; import java.sql.Timestamp; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Properties; import javax.swing.AbstractAction; import javax.swing.Action; import javax.swing.ActionMap; import javax.swing.BorderFactory; import javax.swing.Box; import javax.swing.BoxLayout; import javax.swing.InputMap; import javax.swing.JButton; import javax.swing.JCheckBox; import javax.swing.JComponent; import javax.swing.JDialog; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JPopupMenu; import javax.swing.JRootPane; import javax.swing.JScrollPane; import javax.swing.JTable; import javax.swing.JTextArea; import javax.swing.JTextField; import javax.swing.KeyStroke; import javax.swing.ListSelectionModel; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import javax.swing.event.TableModelEvent; import javax.swing.event.TableModelListener; import javax.swing.table.TableCellRenderer; import javax.swing.table.TableColumnModel; import charactermanaj.Main; import charactermanaj.model.AppConfig; import charactermanaj.model.CharacterData; import charactermanaj.model.PartsCategory; import charactermanaj.model.PartsIdentifier; import charactermanaj.model.PartsSet; import charactermanaj.model.PartsSpec; import charactermanaj.model.PartsSpecResolver; import charactermanaj.model.io.CharacterDataFileReaderWriterFactory; import charactermanaj.model.io.CharacterDataWriter; import charactermanaj.model.io.ExportInfoKeys; import charactermanaj.ui.model.AbstractTableModelWithComboBoxModel; import charactermanaj.ui.progress.ProgressHandle; import charactermanaj.ui.progress.Worker; import charactermanaj.ui.progress.WorkerException; import charactermanaj.ui.progress.WorkerWithProgessDialog; import charactermanaj.util.ErrorMessageHelper; import charactermanaj.util.LocalizedResourcePropertyLoader; public class ExportWizardDialog extends JDialog { private static final long serialVersionUID = 1L; protected static final String STRINGS_RESOURCE = "languages/exportwizdialog"; protected static ArchiveFileDialog archiveFileDialog = new ArchiveFileDialog(); private JPanel activePanel; private AbstractAction actNext; private AbstractAction actPrev; private AbstractAction actFinish; private ExportInformationPanel basicPanel; private ExportPartsSelectPanel partsSelectPanel; private ExportPresetSelectPanel presetSelectPanel; private CharacterData source; public static File getLastUsedDir() { return archiveFileDialog.getLastUSedDir(); } public static void setLastUsedDir(File lastUsedDir) { archiveFileDialog.setLastUSedDir(lastUsedDir); } public ExportWizardDialog(JFrame parent, CharacterData characterData, BufferedImage samplePicture) { super(parent, true); initComponent(parent, characterData, samplePicture); } public ExportWizardDialog(JDialog parent, CharacterData characterData, BufferedImage samplePicture) { super(parent, true); initComponent(parent, characterData, samplePicture); } private void initComponent(Component parent, CharacterData characterData, BufferedImage samplePicture) { if (characterData == null) { throw new IllegalArgumentException(); } this.source = characterData; setDefaultCloseOperation(JDialog.DO_NOTHING_ON_CLOSE); addWindowListener(new WindowAdapter() { @Override public void windowClosing(WindowEvent e) { onClose(); } }); Properties strings = LocalizedResourcePropertyLoader.getInstance() .getLocalizedProperties(STRINGS_RESOURCE); // タイトル setTitle(strings.getProperty("title")); // メインパネル Container contentPane = getContentPane(); contentPane.setLayout(new BorderLayout()); final JPanel mainPanel = new JPanel(); mainPanel.setBorder(BorderFactory.createEtchedBorder()); final CardLayout mainPanelLayout = new CardLayout(5, 5); mainPanel.setLayout(mainPanelLayout); contentPane.add(mainPanel, BorderLayout.CENTER); ComponentListener componentListener = new ComponentAdapter() { public void componentShown(ComponentEvent e) { onComponentShown((JPanel) e.getComponent()); } }; // アクション this.actNext = new AbstractAction(strings.getProperty("btn.next")) { private static final long serialVersionUID = 1L; public void actionPerformed(ActionEvent e) { mainPanelLayout.next(mainPanel); } }; this.actPrev = new AbstractAction(strings.getProperty("btn.prev")) { private static final long serialVersionUID = 1L; public void actionPerformed(ActionEvent e) { mainPanelLayout.previous(mainPanel); } }; this.actFinish = new AbstractAction(strings.getProperty("btn.finish")) { private static final long serialVersionUID = 1L; public void actionPerformed(ActionEvent e) { onFinish(); } }; AbstractAction actCancel = new AbstractAction(strings.getProperty("btn.cancel")) { private static final long serialVersionUID = 1L; public void actionPerformed(ActionEvent e) { onClose(); } }; ChangeListener actChangeValue = new ChangeListener() { public void stateChanged(ChangeEvent e) { updateBtnPanelState(); } }; ChangeListener actPanelEnabler = new ChangeListener() { public void stateChanged(ChangeEvent e) { updatePanelStatus(); } }; // panel1 : basic this.basicPanel = new ExportInformationPanel(characterData, samplePicture); this.basicPanel.addComponentListener(componentListener); this.basicPanel.addChangeListener(actChangeValue); this.basicPanel.addChangeListener(actPanelEnabler); mainPanel.add(this.basicPanel, "basicPanel"); // panel2 : panelSelectPanel this.partsSelectPanel = new ExportPartsSelectPanel(characterData); this.partsSelectPanel.addComponentListener(componentListener); this.partsSelectPanel.addChangeListener(actChangeValue); mainPanel.add(this.partsSelectPanel, "partsSelectPanel"); // panel3 : presetSelectPanel this.presetSelectPanel = new ExportPresetSelectPanel( this.partsSelectPanel, this.basicPanel, characterData.getPartsSets().values(), characterData.getDefaultPartsSetId()); this.presetSelectPanel.addComponentListener(componentListener); this.presetSelectPanel.addChangeListener(actChangeValue); mainPanel.add(this.presetSelectPanel, "presetSelectPanel"); // button panel JPanel btnPanel = new JPanel(); btnPanel.setBorder(BorderFactory.createEmptyBorder(3, 3, 3, 45)); GridBagLayout btnPanelLayout = new GridBagLayout(); btnPanel.setLayout(btnPanelLayout); actPrev.setEnabled(false); actNext.setEnabled(false); actFinish.setEnabled(false); GridBagConstraints gbc = new GridBagConstraints(); gbc.gridx = 0; gbc.gridy = 0; gbc.gridheight = 1; gbc.gridwidth = 1; gbc.anchor = GridBagConstraints.EAST; gbc.fill = GridBagConstraints.BOTH; gbc.ipadx = 0; gbc.ipady = 0; gbc.insets = new Insets(3, 3, 3, 3); gbc.weightx = 1.; gbc.weighty = 0.; btnPanel.add(Box.createHorizontalGlue(), gbc); gbc.gridx = Main.isLinuxOrMacOSX() ? 2 : 1; gbc.gridy = 0; gbc.weightx = 0.; btnPanel.add(new JButton(this.actPrev), gbc); gbc.gridx = Main.isLinuxOrMacOSX() ? 3 : 2; gbc.gridy = 0; JButton btnNext = new JButton(this.actNext); btnPanel.add(btnNext, gbc); gbc.gridx = Main.isLinuxOrMacOSX() ? 4 : 3; gbc.gridy = 0; btnPanel.add(new JButton(this.actFinish), gbc); gbc.gridx = Main.isLinuxOrMacOSX() ? 1 : 4; gbc.gridy = 0; JButton btnCancel = new JButton(actCancel); btnPanel.add(btnCancel, gbc); contentPane.add(btnPanel, BorderLayout.SOUTH); // インプットマップ/アクションマップ Toolkit tk = Toolkit.getDefaultToolkit(); JRootPane rootPane = getRootPane(); rootPane.setDefaultButton(btnNext); InputMap im = rootPane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW); ActionMap am = rootPane.getActionMap(); im.put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "closeExportWizDialog"); im.put(KeyStroke.getKeyStroke(KeyEvent.VK_W, tk.getMenuShortcutKeyMask()), "closeExportWizDialog"); am.put("closeExportWizDialog", actCancel); // 表示 setSize(500, 500); setLocationRelativeTo(parent); mainPanelLayout.first(mainPanel); updateBtnPanelState(); updatePanelStatus(); } protected void onComponentShown(JPanel panel) { activePanel = panel; updateBtnPanelState(); } protected void updatePanelStatus() { partsSelectPanel.setEnabled(basicPanel.isExportPartsImages()); presetSelectPanel.setEnabled(basicPanel.isExportPresets()); } protected void updateBtnPanelState() { actPrev.setEnabled(activePanel != null && activePanel != basicPanel); actNext.setEnabled(activePanel != null && activePanel != presetSelectPanel); actFinish.setEnabled(isComplete()); } protected void checkMissingParts(Collection partaSets) { if (partaSets == null) { partaSets = presetSelectPanel.getSelectedPresets(); } partsSelectPanel.checkMissingPartsList(partaSets); } protected boolean isComplete() { if (basicPanel.isExportPartsImages()) { if (partsSelectPanel.getSelectedCount() == 0) { // パーツイメージのエクスポートを指定した場合、エクスポートするパーツの選択は必須 return false; } } if (basicPanel.isExportPresets()) { if (presetSelectPanel.getSelectedCount() == 0) { // プリセットのエクスポートを指定した場合、エクスポートするプリセットの選択は必須 return false; } } return true; } protected void onClose() { Properties strings = LocalizedResourcePropertyLoader.getInstance() .getLocalizedProperties(ExportWizardDialog.STRINGS_RESOURCE); if (JOptionPane.showConfirmDialog(this, strings.getProperty("confirm.close"), strings.getProperty("confirm"), JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE) != JOptionPane.YES_OPTION) { return; } dispose(); } protected void onFinish() { if (!isComplete()) { Toolkit tk = Toolkit.getDefaultToolkit(); tk.beep(); return; } try { final File outFile = archiveFileDialog.showSaveDialog(this); if (outFile == null) { return; } // 出力 setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); try { Worker worker = new Worker() { public Object doWork(ProgressHandle progressHandle) throws IOException { doExport(outFile); return null; } }; WorkerWithProgessDialog dlg = new WorkerWithProgessDialog(this, worker); dlg.startAndWait(); } finally { setCursor(Cursor.getDefaultCursor()); } // 完了メッセージ Properties strings = LocalizedResourcePropertyLoader.getInstance() .getLocalizedProperties(ExportWizardDialog.STRINGS_RESOURCE); JOptionPane.showMessageDialog(this, strings.getProperty("complete")); // 完了後、ウィンドウを閉じる. dispose(); } catch (WorkerException ex) { ErrorMessageHelper.showErrorDialog(this, ex.getCause()); } catch (Exception ex) { ErrorMessageHelper.showErrorDialog(this, ex); } } protected void doExport(File outFile) throws IOException { CharacterDataFileReaderWriterFactory writerFactory = CharacterDataFileReaderWriterFactory.getInstance(); CharacterDataWriter exportWriter = writerFactory.createWriter(outFile); try { // 基本情報のみをコピーし、これをエクスポートするキャラクター定義のベースとする。 // (プリセットとパーツイメージはリセットされている状態。) CharacterData cd = source.duplicateBasicInfo(); cd.clearPartsSets(false); boolean exportPresets = basicPanel.isExportPresets(); boolean exportSamplePicture = basicPanel.isExportSamplePicture(); boolean exportCharacterData = true; boolean exportPartsImages = basicPanel.isExportPartsImages(); // 基本情報を設定する. cd.setAuthor(basicPanel.getAuthor()); cd.setDescription(basicPanel.getDescription()); // エクスポート情報を出力する. Properties exportProp = new Properties(); exportProp.setProperty(ExportInfoKeys.EXPORT_PRESETS, Boolean.toString(exportPresets)); exportProp.setProperty(ExportInfoKeys.EXPORT_SAMPLE_PICTURE, Boolean.toString(exportSamplePicture)); exportProp.setProperty(ExportInfoKeys.EXPORT_CHARACTER_DATA, Boolean.toString(exportCharacterData)); exportProp.setProperty(ExportInfoKeys.EXPORT_PARTS_IMAGES, Boolean.toString(exportPartsImages)); exportProp.setProperty(ExportInfoKeys.EXPORT_TIMESTAMP, Long.toString(System.currentTimeMillis())); exportWriter.writeExportProp(exportProp); // プリセットをエクスポートする場合、プリセット情報を登録する. if (exportPresets) { HashSet registered = new HashSet(); for (PartsSet partsSet : presetSelectPanel.getSelectedPresets()) { registered.add(partsSet.getPartsSetId()); cd.addPartsSet(partsSet); } // プリセットとして登録済みのもののみ既定に設定可能 String defaultPresetId = presetSelectPanel.getDefaultPresetId(); if (registered.contains(defaultPresetId)) { cd.setDefaultPartsSetId(defaultPresetId); } } // キャラクターデータを出力する. exportWriter.writeCharacterData(cd); // readme.txtを出力する. String readmeContents = cd.getDescription(); if (readmeContents != null && readmeContents.trim().length() > 0) { AppConfig appConfig = AppConfig.getInstance(); StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw); pw.println("exported by CharacterManaJ (version " + appConfig.getSpecificationVersion() + " " + appConfig.getImplementationVersion() + ")"); pw.println(); pw.println(readmeContents); pw.close(); exportWriter.writeTextUTF16LE("readme.txt", sw.toString()); } // サンプルピクチャをエクスポートする if (exportSamplePicture) { BufferedImage pic = null; pic = basicPanel.getSamplePicture(); if (pic != null) { exportWriter.writeSamplePicture(pic); } } if (exportPartsImages) { Map partsSpecMap = partsSelectPanel.getSelectedParts(); // パーツ管理情報を出力する exportWriter.writePartsManageData(partsSpecMap); // パーツイメージを出力する exportWriter.writePartsImages(partsSpecMap); } } finally { exportWriter.close(); } } } interface ExportResolverBase { void addChangeListener(ChangeListener l); void removeChangeListener(ChangeListener l); } /** * 基本情報 * @author seraphy */ interface ExportInformationResolver extends ExportResolverBase { BufferedImage getSamplePicture(); boolean isExportSamplePicture(); boolean isExportPartsImages(); boolean isExportPresets(); String getAuthor(); String getDescription(); } abstract class AbstractImportPanel extends JPanel implements ExportResolverBase { private static final long serialVersionUID = 1L; protected LinkedList listeners = new LinkedList(); public void addChangeListener(ChangeListener l) { if (l != null) { listeners.add(l); } } public void removeChangeListener(ChangeListener l) { if (l != null) { listeners.remove(l); } } protected void fireChangeEvent() { ChangeEvent e = new ChangeEvent(this); for (ChangeListener listener : listeners) { listener.stateChanged(e); } } } /** * 基本情報パネル * @author seraphy */ class ExportInformationPanel extends AbstractImportPanel implements ExportInformationResolver { private static final long serialVersionUID = 1L; private BufferedImage samplePicture; private SamplePicturePanel sampleImagePanel; private JTextField txtAuthor; private JTextArea txtDescription; private JCheckBox chkPartsImages; private JCheckBox chkPresets; private JCheckBox chkSampleImage; protected ExportInformationPanel(final CharacterData characterData, final BufferedImage samplePicture) { if (characterData == null) { throw new IllegalArgumentException(); } setName("basicPanel"); this.samplePicture = samplePicture; Properties strings = LocalizedResourcePropertyLoader.getInstance() .getLocalizedProperties(ExportWizardDialog.STRINGS_RESOURCE); GridBagLayout basicPanelLayout = new GridBagLayout(); GridBagConstraints gbc = new GridBagConstraints(); setLayout(basicPanelLayout); JPanel contentsSpecPanel = new JPanel(); contentsSpecPanel.setBorder(BorderFactory.createCompoundBorder( BorderFactory.createEmptyBorder(5, 5, 5, 5), BorderFactory.createTitledBorder(strings.getProperty("basic.contentsSpec")))); BoxLayout contentsSpecPanelLayout = new BoxLayout(contentsSpecPanel, BoxLayout.PAGE_AXIS); contentsSpecPanel.setLayout(contentsSpecPanelLayout); JCheckBox chkCharacterDef = new JCheckBox(strings.getProperty("characterdef")); chkPartsImages = new JCheckBox(strings.getProperty("basic.chk.partsImages")); chkPresets = new JCheckBox(strings.getProperty("basic.chk.presets")); chkSampleImage = new JCheckBox(strings.getProperty("basic.chk.samplePicture")); chkCharacterDef.setEnabled(false); // キャラクター定義は固定 chkCharacterDef.setSelected(true); contentsSpecPanel.add(chkCharacterDef); contentsSpecPanel.add(chkPartsImages); contentsSpecPanel.add(chkPresets); contentsSpecPanel.add(chkSampleImage); /// JPanel commentPanel = new JPanel(); Dimension archiveInfoPanelMinSize = new Dimension(300, 200); commentPanel.setMinimumSize(archiveInfoPanelMinSize); commentPanel.setPreferredSize(archiveInfoPanelMinSize); commentPanel.setBorder(BorderFactory.createCompoundBorder( BorderFactory.createEmptyBorder(5, 5, 5, 5), BorderFactory.createTitledBorder(strings.getProperty("basic.comment")))); GridBagLayout commentPanelLayout = new GridBagLayout(); commentPanel.setLayout(commentPanelLayout); gbc.gridx = 0; gbc.gridy = 0; gbc.gridheight = 1; gbc.gridwidth = 1; gbc.weightx = 0.; gbc.weighty = 0.; gbc.insets = new Insets(3, 3, 3, 3); gbc.anchor = GridBagConstraints.WEST; gbc.fill = GridBagConstraints.BOTH; commentPanel.add(new JLabel(strings.getProperty("author"), JLabel.RIGHT), gbc); gbc.gridx = 1; gbc.gridy = 0; gbc.gridwidth = 1; gbc.weightx = 1.; gbc.weighty = 0.; txtAuthor = new JTextField(); commentPanel.add(txtAuthor, gbc); gbc.gridx = 0; gbc.gridy = 1; gbc.gridwidth = 1; gbc.gridheight = 1; gbc.weightx = 0.; gbc.weighty = 0.; commentPanel.add(new JLabel(strings.getProperty("description"), JLabel.RIGHT), gbc); gbc.gridx = 1; gbc.gridy = 1; gbc.gridwidth = 1; gbc.gridheight = 2; gbc.weighty = 1.; gbc.weightx = 1.; txtDescription = new JTextArea(); commentPanel.add(new JScrollPane(txtDescription), gbc); /// sampleImagePanel = new SamplePicturePanel(samplePicture); sampleImagePanel.setBorder(BorderFactory.createCompoundBorder( BorderFactory.createEmptyBorder(5, 5, 5, 5), BorderFactory.createTitledBorder(strings.getProperty("basic.sampleImage")))); /// gbc.gridx = 0; gbc.gridy = 0; gbc.gridheight = 1; gbc.gridwidth = 2; gbc.weightx = 1.; gbc.weighty = 0.; gbc.anchor = GridBagConstraints.WEST; gbc.fill = GridBagConstraints.BOTH; add(contentsSpecPanel, gbc); gbc.gridx = 0; gbc.gridy = 1; gbc.gridheight = 1; gbc.gridwidth = 1; gbc.weightx = 0.; gbc.weighty = 1.; gbc.anchor = GridBagConstraints.WEST; gbc.fill = GridBagConstraints.BOTH; add(commentPanel, gbc); gbc.gridx = 1; gbc.gridy = 1; gbc.gridheight = 1; gbc.gridwidth = 1; gbc.weightx = 1.; gbc.weighty = 1.; gbc.anchor = GridBagConstraints.WEST; gbc.fill = GridBagConstraints.BOTH; add(sampleImagePanel, gbc); loadBasicInfo(characterData); // アクションリスナ ActionListener modListener = new ActionListener() { public void actionPerformed(ActionEvent e) { updateSamplePicture(); fireChangeEvent(); } }; chkPartsImages.addActionListener(modListener); chkPresets.addActionListener(modListener); chkSampleImage.addActionListener(modListener); } protected void updateSamplePicture() { sampleImagePanel.setVisiblePicture(chkSampleImage.isSelected()); } protected void loadBasicInfo(CharacterData characterData) { if (samplePicture == null) { // サンプルイメージがなければディセーブル chkSampleImage.setEnabled(false); chkSampleImage.setSelected(false); sampleImagePanel.setVisiblePicture(false); } else { chkSampleImage.setSelected(true); sampleImagePanel.setVisiblePicture(true); } chkPartsImages.setSelected(true); chkPresets.setSelected(true); String author = characterData.getAuthor(); String description = characterData.getDescription(); txtAuthor.setText(author == null ? "" : author); txtDescription.setText(description == null ? "" : description); } public BufferedImage getSamplePicture() { return samplePicture; } public boolean isExportSamplePicture() { return chkSampleImage.isSelected(); } public boolean isExportPartsImages() { return chkPartsImages.isSelected(); } public boolean isExportPresets() { return chkPresets.isSelected(); } public String getAuthor() { return txtAuthor.getText(); } public String getDescription() { return txtDescription.getText(); } } /** * エクスポート対象パーツ * @author seraphy */ interface ExportPartsResolver extends ExportResolverBase { int getSelectedCount(); void selectByPartsSet(Collection partsSet); Map getSelectedParts(); Map> checkMissingPartsList(Collection partsSets); } /** * エクスポート対象パーツ選択パネル * @author seraphy */ class ExportPartsSelectPanel extends AbstractImportPanel implements ExportPartsResolver { private static final long serialVersionUID = 1L; private ExportPartsTableModel partsTableModel; private JTable partsTable; private Action actSelectAll; private Action actDeselectAll; private Action actSort; private Action actSortByTimestamp; protected ExportPartsSelectPanel(PartsSpecResolver partsSpecResolver) { if (partsSpecResolver == null) { throw new IllegalArgumentException(); } Properties strings = LocalizedResourcePropertyLoader.getInstance() .getLocalizedProperties(ExportWizardDialog.STRINGS_RESOURCE); setName("choosePartsPanel"); setBorder(BorderFactory.createTitledBorder(strings.getProperty("parts.title"))); setLayout(new BorderLayout()); partsTableModel = new ExportPartsTableModel(); partsTableModel.addTableModelListener(new TableModelListener() { public void tableChanged(TableModelEvent e) { fireChangeEvent(); } }); loadPartsInfo(partsSpecResolver); AppConfig appConfig = AppConfig.getInstance(); final Color disabledForeground = appConfig.getDisabledCellForgroundColor(); partsTable = new JTable(partsTableModel) { private static final long serialVersionUID = 1L; @Override public Component prepareRenderer(TableCellRenderer renderer, int row, int column) { Component comp = super.prepareRenderer(renderer, row, column); if (comp instanceof JCheckBox) { // BooleanのデフォルトのレンダラーはKCheckBoxを継承したJTable$BooleanRenderer comp.setEnabled(isCellEditable(row, column) && isEnabled()); } comp.setForeground(isEnabled() ? (isCellSelected(row,column) ? getSelectionForeground() : getForeground()) : disabledForeground); return comp; } }; partsTable.setShowGrid(true); partsTable.setGridColor(appConfig.getGridColor()); partsTable.setAutoResizeMode(JTable.AUTO_RESIZE_OFF); partsTable.putClientProperty("terminateEditOnFocusLost", Boolean.TRUE); partsTableModel.adjustColumnModel(partsTable.getColumnModel()); partsTable.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); partsTable.setRowSelectionAllowed(true); Action actPartsSetCheck = new AbstractAction(strings.getProperty("parts.popup.check")) { private static final long serialVersionUID = 1L; public void actionPerformed(ActionEvent e) { int[] selRows = partsTable.getSelectedRows(); partsTableModel.setCheck(selRows, true); } }; Action actPartsUnsetCheck = new AbstractAction(strings.getProperty("parts.popup.uncheck")) { private static final long serialVersionUID = 1L; public void actionPerformed(ActionEvent e) { int[] selRows = partsTable.getSelectedRows(); partsTableModel.setCheck(selRows, false); } }; final JPopupMenu partsTablePopupMenu = new JPopupMenu(); partsTablePopupMenu.add(actPartsSetCheck); partsTablePopupMenu.add(actPartsUnsetCheck); partsTable.setComponentPopupMenu(partsTablePopupMenu); add(new JScrollPane(partsTable), BorderLayout.CENTER); actSelectAll = new AbstractAction(strings.getProperty("parts.btn.selectAll")) { private static final long serialVersionUID = 1L; public void actionPerformed(ActionEvent e) { onSelectAll(); } }; actDeselectAll = new AbstractAction(strings.getProperty("parts.btn.deselectAll")) { private static final long serialVersionUID = 1L; public void actionPerformed(ActionEvent e) { onDeselectAll(); } }; actSort = new AbstractAction(strings.getProperty("parts.btn.sort")) { private static final long serialVersionUID = 1L; public void actionPerformed(ActionEvent e) { onSort(); } }; actSortByTimestamp = new AbstractAction(strings.getProperty("parts.btn.sortByTimestamp")) { private static final long serialVersionUID = 1L; public void actionPerformed(ActionEvent e) { onSortByTimestamp(); } }; JPanel btnPanel = new JPanel(); GridBagLayout btnPanelLayout = new GridBagLayout(); btnPanel.setLayout(btnPanelLayout); GridBagConstraints gbc = new GridBagConstraints(); gbc.gridx = 0; gbc.gridy = 0; gbc.gridheight = 1; gbc.gridwidth = 1; gbc.anchor = GridBagConstraints.EAST; gbc.fill = GridBagConstraints.BOTH; gbc.insets = new Insets(3, 3, 3, 3); gbc.ipadx = 0; gbc.ipady = 0; JButton btnSelectAll = new JButton(actSelectAll); btnPanel.add(btnSelectAll, gbc); gbc.gridx = 1; gbc.gridy = 0; JButton btnDeselectAll = new JButton(actDeselectAll); btnPanel.add(btnDeselectAll, gbc); gbc.gridx = 2; gbc.gridy = 0; JButton btnSort = new JButton(actSort); btnPanel.add(btnSort, gbc); gbc.gridx = 3; gbc.gridy = 0; JButton btnSortByTimestamp = new JButton(actSortByTimestamp); btnPanel.add(btnSortByTimestamp, gbc); gbc.gridx = 4; gbc.gridy = 0; gbc.weightx = 1.; btnPanel.add(Box.createHorizontalGlue(), gbc); add(btnPanel, BorderLayout.SOUTH); } protected void loadPartsInfo(PartsSpecResolver partsSpecResolver) { partsTableModel.clear(); for (PartsCategory partsCategory : partsSpecResolver.getPartsCategories()) { Map partsSpecMap = partsSpecResolver.getPartsSpecMap(partsCategory); for (Map.Entry entry : partsSpecMap.entrySet()) { PartsIdentifier partsIdentifier = entry.getKey(); PartsSpec partsSpec = entry.getValue(); ExportPartsSelectModel model = new ExportPartsSelectModel(partsIdentifier, partsSpec, false); partsTableModel.addRow(model); } } partsTableModel.sort(); } protected void onSelectAll() { partsTableModel.selectAll(); } protected void onDeselectAll() { partsTableModel.deselectAll(); } protected void onSort() { partsTableModel.sort(); if (partsTableModel.getRowCount() > 0) { Rectangle rct = partsTable.getCellRect(0, 0, true); partsTable.scrollRectToVisible(rct); } } protected void onSortByTimestamp() { partsTableModel.sortByTimestamp(); if (partsTableModel.getRowCount() > 0) { Rectangle rct = partsTable.getCellRect(0, 0, true); partsTable.scrollRectToVisible(rct); } } public Map getSelectedParts() { return partsTableModel.getSelectedParts(); } public Map> checkMissingPartsList(Collection partsSets) { return partsTableModel.checkMissingPartsList(partsSets); } public void selectByPartsSet(Collection partsSets) { partsTableModel.selectByPartsSet(partsSets); } public int getSelectedCount() { return partsTableModel.getSelectedCount(); } @Override public void setEnabled(boolean enabled) { partsTable.setEnabled(enabled); partsTableModel.setEnabled(enabled); actSelectAll.setEnabled(enabled); actDeselectAll.setEnabled(enabled); super.setEnabled(enabled); } } interface ExportPresetResolve extends ExportResolverBase { int getSelectedCount(); List getSelectedPresets(); } class ExportPresetSelectPanel extends AbstractImportPanel implements ExportPresetResolve { private static final long serialVersionUID = 1L; private ExportPartsResolver exportPartsResolver; private ExportPresetTableModel presetTableModel; private JTable presetTable; private Action actSelectAll; private Action actDeselectAll; protected ExportPresetSelectPanel( final ExportPartsResolver exportPartsResolver, final ExportInformationResolver exportInfoResolver, Collection partsSets, String defaultPresetId) { this.exportPartsResolver = exportPartsResolver; setName("presetSelectPanel"); Properties strings = LocalizedResourcePropertyLoader.getInstance() .getLocalizedProperties(ExportWizardDialog.STRINGS_RESOURCE); setName("choosePartsPanel"); setBorder(BorderFactory.createTitledBorder(strings.getProperty("preset.title"))); setLayout(new BorderLayout()); presetTableModel = new ExportPresetTableModel(); presetTableModel.addTableModelListener(new TableModelListener() { public void tableChanged(TableModelEvent e) { if (e.getType() == TableModelEvent.UPDATE) { fireChangeEvent(); } } }); exportPartsResolver.addChangeListener(new ChangeListener() { public void stateChanged(ChangeEvent e) { checkMissingParts(); } }); loadPresetInfo(partsSets, defaultPresetId); AppConfig appConfig = AppConfig.getInstance(); final Color warningForegroundColor = appConfig.getExportPresetWarningsForegroundColor(); final Color disabledForeground = appConfig.getDisabledCellForgroundColor(); presetTable = new JTable(presetTableModel) { private static final long serialVersionUID = 1L; @Override public Component prepareRenderer(TableCellRenderer renderer, int row, int column) { Component comp = super.prepareRenderer(renderer, row, column); if (comp instanceof JCheckBox) { // BooleanのデフォルトのレンダラーはKCheckBoxを継承したJTable$BooleanRenderer comp.setEnabled(isCellEditable(row, column) && isEnabled()); } ExportPresetModel presetModel = presetTableModel.getRow(row); if (presetModel.isPresetParts()) { comp.setFont(getFont().deriveFont(Font.BOLD)); } else { comp.setFont(getFont()); } if (!isEnabled()) { comp.setForeground(disabledForeground); } else { if (presetModel.isSelected() && presetModel.getMissingPartsIdentifiers().size() > 0) { comp.setForeground(warningForegroundColor); } else { comp.setForeground(getForeground()); } } return comp; } }; presetTable.setShowGrid(true); presetTable.setGridColor(appConfig.getGridColor()); presetTable.putClientProperty("terminateEditOnFocusLost", Boolean.TRUE); final Action actSelectUsedParts = new AbstractAction( strings.getProperty("preset.popup.selectUsedParts")) { private static final long serialVersionUID = 1L; public void actionPerformed(ActionEvent e) { exportUsedParts(); } }; final JPopupMenu popupMenu = new JPopupMenu(); popupMenu.add(actSelectUsedParts); presetTable.setComponentPopupMenu(popupMenu); presetTable.setAutoResizeMode(JTable.AUTO_RESIZE_OFF); presetTableModel.adjustColumnModel(presetTable.getColumnModel()); add(new JScrollPane(presetTable), BorderLayout.CENTER); actSelectAll = new AbstractAction(strings.getProperty("parts.btn.selectAll")) { private static final long serialVersionUID = 1L; public void actionPerformed(ActionEvent e) { onSelectAll(); } }; actDeselectAll = new AbstractAction(strings.getProperty("parts.btn.deselectAll")) { private static final long serialVersionUID = 1L; public void actionPerformed(ActionEvent e) { onDeselectAll(); } }; Action actSort = new AbstractAction(strings.getProperty("parts.btn.sort")) { private static final long serialVersionUID = 1L; public void actionPerformed(ActionEvent e) { onSort(); } }; JPanel btnPanel = new JPanel(); GridBagLayout btnPanelLayout = new GridBagLayout(); btnPanel.setLayout(btnPanelLayout); GridBagConstraints gbc = new GridBagConstraints(); gbc.gridx = 0; gbc.gridy = 0; gbc.gridheight = 1; gbc.gridwidth = 1; gbc.anchor = GridBagConstraints.EAST; gbc.fill = GridBagConstraints.BOTH; gbc.insets = new Insets(3, 3, 3, 3); gbc.ipadx = 0; gbc.ipady = 0; JButton btnSelectAll = new JButton(actSelectAll); btnPanel.add(btnSelectAll, gbc); gbc.gridx = 1; gbc.gridy = 0; JButton btnDeselectAll = new JButton(actDeselectAll); btnPanel.add(btnDeselectAll, gbc); gbc.gridx = 2; gbc.gridy = 0; JButton btnSort = new JButton(actSort); btnPanel.add(btnSort, gbc); gbc.gridx = 3; gbc.gridy = 0; gbc.weightx = 1.; btnPanel.add(Box.createHorizontalGlue(), gbc); add(btnPanel, BorderLayout.SOUTH); } protected void loadPresetInfo(Collection partsSets, String defaultPresetId) { presetTableModel.clear(); for (PartsSet orgPartsSet : partsSets) { PartsSet partsSet = orgPartsSet.clone(); ExportPresetModel model = new ExportPresetModel(partsSet, partsSet.isPresetParts()); presetTableModel.addRow(model); } presetTableModel.setDefaultPresetId(defaultPresetId); presetTableModel.sort(); checkMissingParts(); } public void checkMissingParts() { ArrayList changedPartsSets = new ArrayList(); HashMap partsSetModelMap = new HashMap(); int mx = presetTableModel.getRowCount(); for (int idx = 0; idx < mx; idx++) { ExportPresetModel presetModel = presetTableModel.getRow(idx); PartsSet partsSet = presetModel.getPartsSet(); partsSetModelMap.put(partsSet, presetModel); changedPartsSets.add(partsSet); } Map> missingPartsIdentifiersMap = exportPartsResolver .checkMissingPartsList(changedPartsSets); for (Map.Entry> entry : missingPartsIdentifiersMap.entrySet()) { PartsSet partsSet = entry.getKey(); List missingPartsIdentifiers = entry.getValue(); ExportPresetModel presetModel = partsSetModelMap.get(partsSet); presetModel.setMissingPartsIdentifiers(missingPartsIdentifiers); } if (!missingPartsIdentifiersMap.isEmpty()) { presetTableModel.fireTableDataChanged(); } } protected void onSelectAll() { presetTableModel.selectAll(); } protected void onDeselectAll() { presetTableModel.deselectAll(); } protected void onSort() { presetTableModel.sort(); if (presetTableModel.getRowCount() > 0) { Rectangle rct = presetTable.getCellRect(0, 0, true); presetTable.scrollRectToVisible(rct); } } public List getSelectedPresets() { return presetTableModel.getSelectedPresets(); } protected void exportUsedParts() { ArrayList partsSets = new ArrayList(); int[] selRows = presetTable.getSelectedRows(); for (int selRow : selRows) { ExportPresetModel presetModel = presetTableModel.getRow(selRow); partsSets.add(presetModel.getPartsSet()); } exportPartsResolver.selectByPartsSet(partsSets); } public int getSelectedCount() { return presetTableModel.getSelectedCount(); } public String getDefaultPresetId() { return presetTableModel.getDefaultPresetId(); } @Override public void setEnabled(boolean enabled) { this.presetTable.setEnabled(enabled); this.presetTableModel.setEnabled(enabled); this.actSelectAll.setEnabled(enabled); this.actDeselectAll.setEnabled(enabled); super.setEnabled(enabled); } } class ExportPartsTableModel extends AbstractTableModelWithComboBoxModel { private static final long serialVersionUID = 1L; private static final String[] columnNames; private static final int[] columnWidths; private boolean enabled = true; static { Properties strings = LocalizedResourcePropertyLoader.getInstance() .getLocalizedProperties(ExportWizardDialog.STRINGS_RESOURCE); columnNames = new String[] { strings.getProperty("parts.column.selected"), strings.getProperty("parts.column.category"), strings.getProperty("parts.column.name"), strings.getProperty("parts.column.timestamp"), strings.getProperty("parts.column.author"), strings.getProperty("parts.column.version"), }; columnWidths = new int[] { Integer.parseInt(strings.getProperty("parts.column.selected.width")), Integer.parseInt(strings.getProperty("parts.column.category.width")), Integer.parseInt(strings.getProperty("parts.column.name.width")), Integer.parseInt(strings.getProperty("parts.column.timestamp.width")), Integer.parseInt(strings.getProperty("parts.column.author.width")), Integer.parseInt(strings.getProperty("parts.column.version.width")), }; } public void adjustColumnModel(TableColumnModel columnModel) { for (int idx = 0; idx < columnWidths.length; idx++) { columnModel.getColumn(idx).setPreferredWidth(columnWidths[idx]); } } public int getColumnCount() { return columnNames.length; } @Override public String getColumnName(int column) { return columnNames[column]; } public Object getValueAt(int rowIndex, int columnIndex) { ExportPartsSelectModel partsSelectModel = getRow(rowIndex); switch (columnIndex) { case 0: return Boolean.valueOf(partsSelectModel.isChecked() && enabled); case 1: return partsSelectModel.getPartsCategory().getLocalizedCategoryName(); case 2: return partsSelectModel.getPartsName(); case 3: Timestamp tm = partsSelectModel.getTimestamp(); if (tm != null) { return tm.toString(); } return ""; case 4: return partsSelectModel.getAuthor(); case 5: return partsSelectModel.getVersion(); default: } return ""; } @Override public void setValueAt(Object aValue, int rowIndex, int columnIndex) { ExportPartsSelectModel partsSelectModel = getRow(rowIndex); switch (columnIndex) { case 0: partsSelectModel.setChecked(((Boolean) aValue).booleanValue()); break; default: return; } fireTableRowsUpdated(rowIndex, rowIndex); } @Override public boolean isCellEditable(int rowIndex, int columnIndex) { if (columnIndex == 0) { return isEnabled(); } return false; } @Override public Class getColumnClass(int columnIndex) { switch (columnIndex) { case 0: return Boolean.class; case 1: return String.class; case 2: return String.class; case 3: return String.class; default: } return String.class; } public void sort() { Collections.sort(elements); fireTableDataChanged(); } public void sortByTimestamp() { Collections.sort(elements, new Comparator() { public int compare(ExportPartsSelectModel o1, ExportPartsSelectModel o2) { int ret = 0; Timestamp t1 = o1.getTimestamp(); Timestamp t2 = o2.getTimestamp(); if (t1 == null || t2 == null) { if (t1 == null && t2 == null) { ret = 0; } else if (t1 == null) { ret = 1; } else { ret = -1; } } else { ret = t2.compareTo(t1); // 逆順(日付の新しいもの順) } if (ret == 0) { ret = o1.compareTo(o2); } return ret; } }); fireTableDataChanged(); } public void selectAll() { for (ExportPartsSelectModel model : elements) { model.setChecked(true); } fireTableDataChanged(); } public void deselectAll() { for (ExportPartsSelectModel model : elements) { model.setChecked(false); } fireTableDataChanged(); } /** * 選択されているパーツイメージのマップを返す.
* @return 選択されているパーツイメージのマップ */ public Map getSelectedParts() { HashMap selectedPartsMap = new HashMap(); for (ExportPartsSelectModel partsSelectModel : elements) { if (partsSelectModel.isChecked() && isEnabled()) { selectedPartsMap.put(partsSelectModel.getPartsIdentifier(), partsSelectModel.getPartsSpec()); } } return selectedPartsMap; } /** * パーツセットのコレクションを指定し、パーツセットの各パーツがすべてエクスポート対象になっているものだけを返す.
* @param partsSets パーツセットのリスト * @return 不足しているパーツセットと、その不足しているパーツリストを返す. */ public Map> checkMissingPartsList(Collection partsSets) { if (partsSets == null) { throw new IllegalArgumentException(); } Map selectedPartsMap = getSelectedParts(); HashMap> missingPartsMap = new HashMap>(); for (PartsSet partsSet : partsSets) { ArrayList missingPartss = new ArrayList(); for (List partsIdentifiers : partsSet.values()) { for (PartsIdentifier requirePartsIdentifier : partsIdentifiers) { if (!selectedPartsMap.containsKey(requirePartsIdentifier)) { missingPartss.add(requirePartsIdentifier); } } } Collections.sort(missingPartss); missingPartsMap.put(partsSet, missingPartss); } return missingPartsMap; } /** * パーツセットで使用されているパーツを選択状態にする. * @param partsSet パーツセットのコレクション */ public void selectByPartsSet(Collection partsSets) { if (partsSets == null) { throw new IllegalArgumentException(); } HashSet requirePartsIdentifiers = new HashSet(); for (PartsSet partsSet : partsSets) { for (List partsIdentifiers : partsSet.values()) { for (PartsIdentifier partsIdentifier : partsIdentifiers) { requirePartsIdentifiers.add(partsIdentifier); } } } for (ExportPartsSelectModel partsSelectModel : elements) { if (requirePartsIdentifiers.contains(partsSelectModel.getPartsIdentifier())) { partsSelectModel.setChecked(true); } } fireTableDataChanged(); } /** * 選択されているパーツ数を返す. * @return パーツ数 */ public int getSelectedCount() { int count = 0; for (ExportPartsSelectModel partsSelectModel : elements) { if (partsSelectModel.isChecked() && isEnabled()) { count++; } } return count; } public void setEnabled(boolean enabled) { if (this.enabled != enabled) { this.enabled = enabled; fireTableDataChanged(); } } public boolean isEnabled() { return enabled; } public void setCheck(int[] selRows, boolean checked) { if (selRows == null || selRows.length == 0) { return; } Arrays.sort(selRows); for (int selRow : selRows) { ExportPartsSelectModel rowModel = getRow(selRow); rowModel.setChecked(checked); } fireTableRowsUpdated(selRows[0], selRows[selRows.length - 1]); } } class ExportPartsSelectModel implements Comparable { private boolean checked; private PartsIdentifier partsIdentifier; private PartsSpec partsSpec; private Timestamp timestamp; public ExportPartsSelectModel(PartsIdentifier partsIdentifier, PartsSpec partsSpec, boolean selected) { if (partsIdentifier == null || partsSpec == null) { throw new IllegalArgumentException(); } this.partsIdentifier = partsIdentifier; this.partsSpec = partsSpec; this.checked = selected; long maxLastModified = partsSpec.getPartsFiles().lastModified(); if (maxLastModified > 0) { timestamp = new Timestamp(maxLastModified); } else { timestamp = null; } } @Override public int hashCode() { return partsIdentifier.hashCode(); } @Override public boolean equals(Object obj) { if (obj == this) { return true; } if (obj != null && obj instanceof ExportPartsSelectModel) { ExportPartsSelectModel o = (ExportPartsSelectModel) obj; return partsIdentifier.equals(o.partsIdentifier); } return false; } public int compareTo(ExportPartsSelectModel o) { int ret = (checked ? 0 : 1) - (o.checked ? 0 : 1); // 逆順 if (ret == 0) { ret = partsIdentifier.compareTo(o.partsIdentifier); } return ret; } public PartsIdentifier getPartsIdentifier() { return this.partsIdentifier; } public PartsSpec getPartsSpec() { return this.partsSpec; } public boolean isChecked() { return checked; } public void setChecked(boolean checked) { this.checked = checked; } public PartsCategory getPartsCategory() { return this.partsIdentifier.getPartsCategory(); } public String getPartsName() { return this.partsIdentifier.getLocalizedPartsName(); } public Timestamp getTimestamp() { return timestamp == null ? null : (Timestamp) timestamp.clone(); } public String getAuthor() { return partsSpec.getAuthor(); } public String getVersion() { double version = partsSpec.getVersion(); if (version <= 0) { return ""; } return Double.toString(version); } } class ExportPresetTableModel extends AbstractTableModelWithComboBoxModel { private static final long serialVersionUID = 1L; private static final String[] columnNames; private static final int[] columnWidths; private boolean enabled = true; static { Properties strings = LocalizedResourcePropertyLoader.getInstance() .getLocalizedProperties(ExportWizardDialog.STRINGS_RESOURCE); columnNames = new String[] { strings.getProperty("preset.column.selected"), strings.getProperty("preset.column.default"), strings.getProperty("preset.column.name"), strings.getProperty("preset.column.missingparts"), }; columnWidths = new int[] { Integer.parseInt(strings.getProperty("preset.column.selected.width")), Integer.parseInt(strings.getProperty("preset.column.default.width")), Integer.parseInt(strings.getProperty("preset.column.name.width")), Integer.parseInt(strings.getProperty("preset.column.missingparts.width")), }; } private String defaultPresetId; public void adjustColumnModel(TableColumnModel columnModel) { for (int idx = 0; idx < columnWidths.length; idx++) { columnModel.getColumn(idx).setPreferredWidth(columnWidths[idx]); } } public int getColumnCount() { return columnNames.length; } @Override public String getColumnName(int column) { return columnNames[column]; } public Object getValueAt(int rowIndex, int columnIndex) { ExportPresetModel presetModel = getRow(rowIndex); switch (columnIndex) { case 0: return Boolean.valueOf(presetModel.isSelected() && isEnabled()); case 1: return Boolean.valueOf(presetModel.getPartsSet().getPartsSetId().equals(defaultPresetId) && isEnabled()); case 2: return presetModel.getPartsSetName(); case 3: StringBuilder buf = new StringBuilder(); for (PartsIdentifier partsIdentifier : presetModel.getMissingPartsIdentifiers()) { if (buf.length() > 0) { buf.append(", "); } buf.append(partsIdentifier.getLocalizedPartsName()); } return buf.toString(); default: } return ""; } @Override public void setValueAt(Object aValue, int rowIndex, int columnIndex) { ExportPresetModel presetModel = getRow(rowIndex); switch (columnIndex) { case 0: if (((Boolean) aValue).booleanValue()) { presetModel.setSelected(true); } else { presetModel.setSelected(false); if (presetModel.getPartsSet().getPartsSetId().equals(defaultPresetId)) { // 選択解除したものが既定のパーツセットであった場合、既定も解除する. defaultPresetId = null; fireTableRowsUpdated(rowIndex, rowIndex); return; } } break; case 1: if (((Boolean) aValue).booleanValue()) { defaultPresetId = presetModel.getPartsSet().getPartsSetId(); presetModel.setSelected(true); // 既定のパーツセットにした場合は自動的にエクスポート対象にもする。 fireTableDataChanged(); return; } default: return; } fireTableRowsUpdated(rowIndex, rowIndex); } @Override public Class getColumnClass(int columnIndex) { switch (columnIndex) { case 0: return Boolean.class; case 1: return Boolean.class; case 2: return String.class; case 3: return String.class; default: } return String.class; } @Override public boolean isCellEditable(int rowIndex, int columnIndex) { if (columnIndex == 0 || columnIndex == 1) { return isEnabled(); } return false; } public void sort() { Collections.sort(elements); fireTableDataChanged(); } public void selectAll() { for (ExportPresetModel model : elements) { model.setSelected(true); } fireTableDataChanged(); } public void deselectAll() { for (ExportPresetModel model : elements) { model.setSelected(false); } fireTableDataChanged(); } /** * 選択されているパーツセットのリストを返す.
* なにもなければ空.
* @return 選択されているパーツセットのリスト */ public List getSelectedPresets() { ArrayList partsSets = new ArrayList(); for (ExportPresetModel presetModel : elements) { if (presetModel.isSelected() && isEnabled()) { PartsSet partsSet = presetModel.getPartsSet().clone(); partsSet.setPresetParts(true); partsSets.add(partsSet); } } return partsSets; } public int getSelectedCount() { int count = 0; for (ExportPresetModel presetModel : elements) { if (presetModel.isSelected() && isEnabled()) { count++; } } return count; } public String getDefaultPresetId() { return defaultPresetId; } /** * デフォルトのプリセットを設定する.
* @param defaultPresetId */ public void setDefaultPresetId(String defaultPresetId) { this.defaultPresetId = defaultPresetId; } public void setEnabled(boolean enabled) { if (this.enabled != enabled) { this.enabled = enabled; fireTableDataChanged(); } } public boolean isEnabled() { return enabled; } } class ExportPresetModel implements Comparable { private boolean selected; private PartsSet partsSet; private List missingPartsIdentifiers; public ExportPresetModel(PartsSet partsSet, boolean selected) { if (partsSet == null) { throw new IllegalArgumentException(); } this.partsSet = partsSet; this.selected = selected; } @Override public int hashCode() { return partsSet.hashCode(); } @Override public boolean equals(Object obj) { if (obj == this) { return true; } if (obj != null && obj instanceof ExportPresetModel) { ExportPresetModel o = (ExportPresetModel) obj; return partsSet.equals(o.partsSet); } return false; } public int compareTo(ExportPresetModel o) { int ret = (selected ? 0 : 1) - (o.selected ? 0 : 1); if (ret == 0) { ret = getPartsSetName().compareTo(o.getPartsSetName()); } return ret; } public String getPartsSetName() { String name = partsSet.getLocalizedName(); return name == null ? "" : name; } public boolean isPresetParts() { return partsSet.isPresetParts(); } public boolean isSelected() { return selected; } public void setSelected(boolean selected) { this.selected = selected; } public PartsSet getPartsSet() { return partsSet; } public void setMissingPartsIdentifiers( List missingPartsIdentifiers) { this.missingPartsIdentifiers = Collections.unmodifiableList(missingPartsIdentifiers); } public List getMissingPartsIdentifiers() { if (missingPartsIdentifiers == null) { return Collections.emptyList(); } return missingPartsIdentifiers; } } charactermanaj/src/charactermanaj/ui/ColorDialog.java0000644000175000017500000011771011767047542023165 0ustar paulliupaulliupackage charactermanaj.ui; import java.awt.BorderLayout; import java.awt.Container; import java.awt.GraphicsEnvironment; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.GridLayout; import java.awt.Insets; import java.awt.Point; import java.awt.Rectangle; import java.awt.Toolkit; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.ComponentAdapter; import java.awt.event.ComponentEvent; import java.awt.event.KeyEvent; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.util.Collection; import java.util.HashMap; import java.util.LinkedList; import java.util.Map; import java.util.Properties; import java.util.concurrent.Semaphore; import java.util.concurrent.atomic.AtomicInteger; import java.util.logging.Level; import java.util.logging.Logger; import javax.swing.AbstractAction; import javax.swing.Action; import javax.swing.ActionMap; import javax.swing.BorderFactory; import javax.swing.Box; import javax.swing.BoxLayout; import javax.swing.InputMap; import javax.swing.JButton; import javax.swing.JCheckBox; import javax.swing.JComboBox; import javax.swing.JComponent; import javax.swing.JDialog; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JRootPane; import javax.swing.JSlider; import javax.swing.JSpinner; import javax.swing.JTabbedPane; import javax.swing.KeyStroke; import javax.swing.SpinnerNumberModel; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import charactermanaj.graphics.filters.ColorConv; import charactermanaj.graphics.filters.ColorConvertParameter; import charactermanaj.model.AppConfig; import charactermanaj.model.ColorGroup; import charactermanaj.model.Layer; import charactermanaj.model.PartsCategory; import charactermanaj.model.PartsIdentifier; import charactermanaj.ui.model.ColorChangeEvent; import charactermanaj.ui.model.ColorChangeListener; import charactermanaj.ui.util.SpinnerWheelSupportListener; import charactermanaj.util.LocalizedResourcePropertyLoader; /** * カラーダイアログ.
* カラーダイアログはカテゴリ別に関連づけられており、カテゴリ内の各レイヤーに対応するタブを持つ.
* @author seraphy */ public class ColorDialog extends JDialog { private static final long serialVersionUID = 1L; /** * ロガー */ private static final Logger logger = Logger.getLogger(ColorDialog.class.getName()); /** * このカラーダイアログが対応するカテゴリ */ private final PartsCategory partsCategory; /** * レイヤーごとのタブのマップ */ private HashMap tabs = new HashMap(); /** * タブペイン */ private JTabbedPane tabbedPane; /** * レイヤーに対するタブインデックスのマップ */ private HashMap tabbedPaneIndexMap = new HashMap(); /** * 色変更イベントのリスナのコレクション */ private LinkedList listeners = new LinkedList(); /** * キャプションのプレフィックス */ private String captionBase; /** * 現在表示しているカラー情報の対象としているパーツ識別子 */ private PartsIdentifier partsIdentifier; /** * カテゴリ全体に適用するチェックボックス */ private JCheckBox chkApplyAll; /** * リセットアクション */ private Action actReset; /** * コンストラクタ * @param parent 親フレーム * @param partsCategory カテゴリ * @param colorGroups 選択可能なカラーグループのコレクション */ public ColorDialog(JFrame parent, PartsCategory partsCategory, Collection colorGroups) { super(parent); this.partsCategory = partsCategory; final Properties strings = LocalizedResourcePropertyLoader.getInstance().getLocalizedProperties("languages/colordialog"); String caption = strings.getProperty("colordialog.caption"); String name = partsCategory.getLocalizedCategoryName(); captionBase = caption + name; setTitle(captionBase); // ダイアログを非表示にする. final AbstractAction actHide = new AbstractAction() { private static final long serialVersionUID = 1L; public void actionPerformed(ActionEvent e) { setVisible(false); } }; // 非表示アクション addWindowListener(new WindowAdapter() { @Override public void windowClosing(WindowEvent e) { // ウィンドウの閉じるボタン押下により、ダイアログを「非表示」にする. actHide.actionPerformed(new ActionEvent(ColorDialog.this, 0, "closing")); } }); Container container = getContentPane(); this.tabbedPane = new JTabbedPane(JTabbedPane.TOP, JTabbedPane.WRAP_TAB_LAYOUT); for (final Layer layer : partsCategory.getLayers()) { final ColorDialogTabPanel tabContainer = new ColorDialogTabPanel(this, layer, colorGroups); final ColorChangeListener innerListener = new ColorChangeListener() { private Semaphore semaphore = new Semaphore(1); // イベントが循環発生することを防ぐ public void onColorChange(ColorChangeEvent event) { if (semaphore.tryAcquire()) { try { ColorDialog.this.fireColorChangeEvent(layer, false); } finally { semaphore.release(); } } } public void onColorGroupChange(ColorChangeEvent event) { if (semaphore.tryAcquire()) { try { ColorDialog.this.fireColorGroupChangeEvent(layer); ColorDialog.this.fireColorChangeEvent(layer, false); } finally { semaphore.release(); } } } }; tabContainer.addColorChangeListener(innerListener); tabContainer.addComponentListener(new ComponentAdapter() { @Override public void componentShown(ComponentEvent e) { // レイヤータブが切り替えられるたびに、そのレイヤーの状況にあわせてリセットボタンの状態を更新する. updateResetButton(tabContainer); } }); tabContainer.addPropertyChangeListener("colorConvertParameter", new PropertyChangeListener() { public void propertyChange(PropertyChangeEvent evt) { // レイヤーの情報が変るたびにリセットボタンの状態を更新する updateResetButton(tabContainer); } }); tabbedPane.addTab(layer.getLocalizedName(), tabContainer); tabbedPaneIndexMap.put(layer, tabbedPane.getTabCount() - 1); tabs.put(layer, tabContainer); } // 適用アクション Action actApply = new AbstractAction(strings.getProperty("button.apply")) { private static final long serialVersionUID = 1L; public void actionPerformed(ActionEvent e) { apply(); } }; // リセットアクション actReset = new AbstractAction(strings.getProperty("button.reset")) { private static final long serialVersionUID = 1L; public void actionPerformed(ActionEvent e) { ColorDialogTabPanel tab = (ColorDialogTabPanel) tabbedPane .getSelectedComponent(); if (tab != null) { resetColor(tab); apply(); } } }; JPanel btnPanel = new JPanel(); btnPanel.setBorder(BorderFactory.createEmptyBorder(3, 3, 3, 3)); GridBagLayout gbl = new GridBagLayout(); btnPanel.setLayout(gbl); int colIdx = 0; GridBagConstraints gbc = new GridBagConstraints(); gbc.gridx = colIdx++; gbc.gridy = 0; gbc.ipadx = 0; gbc.ipady = 0; gbc.anchor = GridBagConstraints.EAST; gbc.fill = GridBagConstraints.BOTH; gbc.gridheight = 1; gbc.gridwidth = 1; gbc.weightx = 0.; gbc.weighty = 0.; chkApplyAll = new JCheckBox(strings.getProperty("checkbox.applyAllItems")); chkApplyAll.setSelected(!partsCategory.isMultipleSelectable()); chkApplyAll.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { // すべてに適用のチェックが変更された場合は全レイヤーの色の変更通知を出す. apply(); } }); btnPanel.add(chkApplyAll, gbc); gbc.gridx = colIdx++; gbc.gridy = 0; gbc.weightx = 1.; btnPanel.add(Box.createHorizontalGlue(), gbc); // 「適用」ボタン、アプリケーション設定により有無を選択できる. JButton btnApply = null; AppConfig appConfig = AppConfig.getInstance(); if ( !appConfig.isEnableAutoColorChange()) { gbc.gridx = colIdx++; gbc.gridy = 0; gbc.weightx = 0.; btnApply = new JButton(actApply); btnPanel.add(btnApply, gbc); } gbc.gridx = colIdx++; gbc.gridy = 0; gbc.weightx = 0.; JButton btnReset = new JButton(actReset); btnPanel.add(btnReset, gbc); container.setLayout(new BorderLayout()); container.add(tabbedPane, BorderLayout.CENTER); container.add(btnPanel, BorderLayout.SOUTH); Toolkit tk = Toolkit.getDefaultToolkit(); JRootPane rootPane = getRootPane(); // 「適用」ボタンがある場合は、デフォルトボタンに設定する. if (btnApply != null) { rootPane.setDefaultButton(btnApply); } // CTRL-Wでウィンドウを非表示にする. (ESCでは非表示にしない) InputMap im = rootPane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW); ActionMap am = rootPane.getActionMap(); im.put(KeyStroke.getKeyStroke(KeyEvent.VK_W, tk.getMenuShortcutKeyMask()), "hideColorDialog"); am.put("hideColorDialog", actHide); pack(); } /** * 各レイヤーのカラー情報のタブが開かれた場合、もしくはカラーの設定値が変更されるたびに * 呼び出されて、リセットボタンの状態を変更します.
* 現在のタブが選択しているパネルと異なるパネルからの要求については無視されます.
* @param panel 色情報が変更された、もしくは開かれた対象パネル */ protected void updateResetButton(ColorDialogTabPanel panel) { ColorDialogTabPanel currentPanel = (ColorDialogTabPanel) tabbedPane.getSelectedComponent(); if (currentPanel != null && currentPanel.equals(panel)) { actReset.setEnabled(panel.isColorConvertParameterModified()); } } /** * ダイアログの表示位置を調整する.
* 横位置Xはメインフレームの右側とし、縦位置Yはメインフレームの上位置からのoffset_yを加えた位置とする.
* @param offset_y オフセットY */ public void adjustLocation(int offset_y) { // メインウィンドウよりも左側に位置づけする. // 縦位置はメインウィンドウの上端からオフセットを加えたものとする. Point pt = getParent().getLocation(); Insets insets = getParent().getInsets(); pt.x += getParent().getWidth(); pt.y += (offset_y * insets.top); // メインスクリーンサイズを取得する. GraphicsEnvironment genv = GraphicsEnvironment.getLocalGraphicsEnvironment(); Rectangle desktopSize = genv.getMaximumWindowBounds(); // メインスクリーンのサイズ(デスクトップ領域のみ) // メインスクリーンサイズを超えた場合は、はみ出た分を移動する. if ((pt.x + getWidth()) > desktopSize.width) { pt.x -= ((pt.x + getWidth()) - desktopSize.width); } if ((pt.y + getHeight()) > desktopSize.height) { pt.y -= ((pt.y + getHeight()) - desktopSize.height); } setLocation(pt); } /** * このカラーダイアログが対応するパーツカテゴリを取得する.
* @return パーツカテゴリ */ public PartsCategory getPartsCategory() { return partsCategory; } /** * 指定したレイヤーのカラーグループが「連動」しているか?
* カテゴリに属していないレイヤーを指定した場合は常にfalseを返す.
* @param layer レイヤー * @return 連動している場合はtrue、そうでなければfalse */ public boolean isSyncColorGroup(Layer layer) { ColorDialogTabPanel tab = tabs.get(layer); if (tab == null) { return false; } return tab.isSyncColorGroup(); } /** * 指定したレイヤーのカラーグループの連動フラグを設定する.
* カテゴリに属していないレイヤーを指定した場合は何もしない.
* @param layer レイヤー * @param selected 連動フラグ */ public void setSyncColorGroup(Layer layer, boolean selected) { ColorDialogTabPanel tab = tabs.get(layer); if (tab != null) { tab.setSyncColorGroup(selected); } } /** * レイヤーごとの色情報のマップを指定して、各レイヤーに色情報を設定する.
* カテゴリに属していないレイヤーが含まれる場合は例外となる.
* @param params レイヤーと色情報のマップ */ public void setColorConvertParameters(Map params) { if (params == null) { throw new IllegalArgumentException(); } for (Layer layer : partsCategory.getLayers()) { ColorConvertParameter param = params.get(layer); if (param == null) { param = new ColorConvertParameter(); } setColorConvertParameter(layer, param); } } /** * 対象となるパーツ識別子を指定する。
* カラーダイアログのキャプションにパーツ名を設定される.
* nullを指定した場合はキャプションからパーツ名が除去される.
* @param partsIdentifier パーツ識別子、もしくはnull */ public void setPartsIdentifier(PartsIdentifier partsIdentifier) { this.partsIdentifier = partsIdentifier; if (partsIdentifier == null) { setTitle(captionBase); } else { setTitle(captionBase + "(" + partsIdentifier.getLocalizedPartsName() + ")"); } } /** * 対象となるパーツ識別子を取得する.
* 設定されていなければnullが返される.
* @return パーツ識別子、もしくはnull */ public PartsIdentifier getPartsIdentifier() { return partsIdentifier; } /** * 各レイヤーのタブの有効・無効状態を設定します.
* カテゴリに属さないレイヤーは無視されます.
* nullを指定した場合は、すべてのレイヤーが「有効」となります.
* @param layers 有効とするレイヤーのコレクション、もしくはnull */ public void setEnableLayers(Collection layers) { for (Map.Entry entry : tabs.entrySet()) { Layer layer = entry.getKey(); boolean enabled = (layers == null) || layers.contains(layer); Integer tabIndex = tabbedPaneIndexMap.get(layer); if (tabIndex != null) { tabbedPane.setEnabledAt(tabIndex, enabled); if (logger.isLoggable(Level.FINEST)) { logger.log(Level.FINEST, "setEnableLayers(" + layer + ")=" + enabled); } } } } /** * 「すべてに適用」フラグを取得する.
* @return すべてに適用フラグ */ public boolean isApplyAll() { return chkApplyAll.isSelected(); } /** * 各レイヤーと、その色情報をマップとして取得する.
* @return 各レイヤーと、その色情報のマップ */ public Map getColorConvertParameters() { HashMap params = new HashMap(); for (Layer layer : partsCategory.getLayers()) { ColorDialogTabPanel tab = tabs.get(layer); ColorConvertParameter param = tab.getColorConvertParameter(); params.put(layer, param); } return params; } /** * レイヤーを指定して、色情報を設定する.
* カテゴリに属していないレイヤーを指定した場合は例外となる.
* @param layer レイヤー * @param param 色情報 */ public void setColorConvertParameter(Layer layer, ColorConvertParameter param) { if (layer == null || param == null) { throw new IllegalArgumentException(); } ColorDialogTabPanel tab = tabs.get(layer); if (tab == null) { throw new IllegalArgumentException("layer not found. " + layer + "/tabs=" + tabs); } tab.setColorConvertParameter(param); } /** * 指定したレイヤーの色情報を取得する.
* カテゴリに属していないレイヤーを指定した場合は例外となる.
* @param layer レイヤー * @return 色情報 */ public ColorConvertParameter getColorConvertParameter(Layer layer) { if (layer == null) { throw new IllegalArgumentException(); } ColorDialogTabPanel tab = tabs.get(layer); if (tab == null) { throw new IllegalArgumentException("layer not found. " + layer); } return tab.getColorConvertParameter(); } /** * 指定したレイヤーのカラーグループを取得する.
* カテゴリに属さないレイヤーを指定した場合は例外となる.
* @param layer レイヤー * @return カラーグループ */ public ColorGroup getColorGroup(Layer layer) { if (layer == null) { throw new IllegalArgumentException(); } ColorDialogTabPanel tab = tabs.get(layer); if (tab == null) { throw new IllegalArgumentException("layer not found. " + layer); } return tab.getColorGroup(); } /** * 指定したレイヤーのカラーグループを設定する.
* カテゴリに属さないレイヤーを指定した場合は例外となる.
* @param layer レイヤー * @param colorGroup カラーグループ */ public void setColorGroup(Layer layer, ColorGroup colorGroup) { if (layer == null || colorGroup == null) { throw new IllegalArgumentException(); } ColorDialogTabPanel tab = tabs.get(layer); if (tab != null) { tab.setColorGroup(colorGroup); } } /** * 色ダイアログが変更された場合に通知を受けるリスナーを登録する.
* @param listener リスナー */ public void addColorChangeListener(ColorChangeListener listener) { if (listener == null) { throw new IllegalArgumentException(); } listeners.add(listener); } /** * 色ダイアログが変更された場合に通知を受けるリスナーを登録解除する.
* @param listener */ public void removeColorChangeListener(ColorChangeListener listener) { listeners.remove(listener); } /** * 全レイヤーに対するカラー変更イベントを明示的に送信する. */ protected void apply() { for (Layer layer : getPartsCategory().getLayers()) { ColorDialog.this.fireColorChangeEvent(layer, true); } } /** * カラーをリセットする. */ protected void resetColor(ColorDialogTabPanel tab) { tab.resetColor(); } /** * 指定したレイヤーに対するカラー変更イベントを通知する.
* ただし、force引数がfalseである場合、アプリケーション設定で即時プレビューが指定されていない場合は通知しない.
* @param layer レイヤー * @param force アプリケーション設定に関わらず送信する場合はtrue */ protected void fireColorChangeEvent(Layer layer, boolean force) { if (layer == null) { throw new IllegalArgumentException(); } if (!force) { AppConfig appConfig = AppConfig.getInstance(); if (!appConfig.isEnableAutoColorChange()) { return; } } ColorChangeEvent event = new ColorChangeEvent(this, layer); for (ColorChangeListener listener : listeners) { listener.onColorChange(event); } } /** * 色グループが変更されたことを通知する.
* @param layer レイヤー */ protected void fireColorGroupChangeEvent(Layer layer) { if (layer == null) { throw new IllegalArgumentException(); } ColorChangeEvent event = new ColorChangeEvent(this, layer); for (ColorChangeListener listener : listeners) { listener.onColorGroupChange(event); } } @Override public String toString() { return "ColorDialog(partsCategory:" + partsCategory + ")"; } } class ColorDialogTabPanel extends JPanel { private static final long serialVersionUID = 1L; private JComboBox cmbColorReplace; private JSpinner spinGray; private JSpinner spinOffsetR; private JSpinner spinOffsetG; private JSpinner spinOffsetB; private JSpinner spinOffsetA; private JSpinner spinFactorR; private JSpinner spinFactorG; private JSpinner spinFactorB; private JSpinner spinFactorA; private JSpinner spinHue; private JSpinner spinSaturation; private JSpinner spinBrightness; private JSpinner spinContrast; private JSpinner spinGammaR; private JSpinner spinGammaG; private JSpinner spinGammaB; private JSpinner spinGammaA; private JComboBox cmbColorGroup; private JCheckBox chkColorGroupSync; private final ColorDialog parent; /** * パラメータの明示的変更時に他のパラメータへの反映イベントを停止させるためのセマフォ */ private AtomicInteger changeEventDisableSemaphore = new AtomicInteger(); /** * 明示的に設定されたカラーパラメータを保存する.(リセットに使用するため) */ private ColorConvertParameter paramOrg = new ColorConvertParameter(); private ColorConvertParameter chachedParam; private LinkedList listeners = new LinkedList(); public void addColorChangeListener(ColorChangeListener listener) { if (listener == null) { throw new IllegalArgumentException(); } listeners.add(listener); } public void removeColorChangeListener(ColorChangeListener listener) { listeners.remove(listener); } protected void fireColorChangeEvent(Layer layer) { if (layer == null) { throw new IllegalArgumentException(); } chachedParam = null; if (changeEventDisableSemaphore.get() <= 0) { ColorChangeEvent event = new ColorChangeEvent(parent, layer); for (ColorChangeListener listener : listeners) { listener.onColorChange(event); } } } protected void fireColorGroupChangeEvent(Layer layer) { if (layer == null) { throw new IllegalArgumentException(); } chachedParam = null; if (changeEventDisableSemaphore.get() <= 0) { ColorChangeEvent event = new ColorChangeEvent(parent, layer); for (ColorChangeListener listener : listeners) { listener.onColorGroupChange(event); } } } public ColorDialogTabPanel(final ColorDialog parent, final Layer layer, Collection colorGroups) { if (parent == null || layer == null || colorGroups == null) { throw new IllegalArgumentException(); } this.parent = parent; final Properties strings = LocalizedResourcePropertyLoader.getInstance().getLocalizedProperties("languages/colordialog"); setLayout(new BorderLayout()); JPanel container = new JPanel(); BoxLayout boxlayout = new BoxLayout(container, BoxLayout.PAGE_AXIS); container.setLayout(boxlayout); add(container, BorderLayout.NORTH); // 変更イベントハンドラ final ChangeListener changeEventHandler = new ChangeListener() { public void stateChanged(ChangeEvent e) { fireColorChangeEvent(layer); firePropertyChange("colorConvertParameter", null, null); } }; // 色置換パネル JPanel colorReplacePanel = new JPanel(); colorReplacePanel.setBorder(BorderFactory.createCompoundBorder( BorderFactory.createEmptyBorder(3, 3, 3, 3), BorderFactory.createTitledBorder(strings.getProperty("group.replacergb.caption")))); GridBagLayout gbl = new GridBagLayout(); colorReplacePanel.setLayout(gbl); GridBagConstraints gbc = new GridBagConstraints(); JLabel lblColorReplace = new JLabel(strings.getProperty("replacergb"), JLabel.RIGHT); cmbColorReplace = new JComboBox(ColorConv.values()); JLabel lblGray = new JLabel(strings.getProperty("bright"), JLabel.RIGHT); SpinnerNumberModel grayModel = new SpinnerNumberModel(1., 0., 1., 0.01); grayModel.addChangeListener(changeEventHandler); cmbColorReplace.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { fireColorChangeEvent(layer); firePropertyChange("colorConvertParameter", null, null); } }); spinGray = new JSpinner(grayModel); spinGray.addMouseWheelListener(new SpinnerWheelSupportListener(grayModel)); gbc.gridx = 0; gbc.gridy = 0; gbc.gridwidth = 1; gbc.gridheight = 1; gbc.weightx = 0.; gbc.weighty = 0.; gbc.anchor = GridBagConstraints.WEST; gbc.fill = GridBagConstraints.BOTH; gbc.ipadx = 0; gbc.ipady = 0; gbc.insets = new Insets(3, 3, 3, 3); colorReplacePanel.add(lblColorReplace, gbc); gbc.gridx = 1; gbc.gridy = 0; gbc.gridwidth = 1; gbc.gridheight = 1; gbc.weightx = 1.; gbc.weighty = 0.; colorReplacePanel.add(cmbColorReplace, gbc); gbc.gridx = 2; gbc.gridy = 0; gbc.gridwidth = 1; gbc.gridheight = 1; gbc.weightx = 0.; gbc.weighty = 0.; colorReplacePanel.add(lblGray, gbc); gbc.gridx = 3; gbc.gridy = 0; gbc.weightx = 1.; gbc.weighty = 0.; gbc.gridwidth = 1; gbc.gridheight = 1; colorReplacePanel.add(spinGray, gbc); // RGB変更パネル JPanel colorLevelPanel = new JPanel(); colorLevelPanel.setBorder(BorderFactory.createCompoundBorder( BorderFactory.createEmptyBorder(3, 3, 3, 3), BorderFactory.createTitledBorder(strings.getProperty("group.rgb.caption")))); GridLayout gl = new GridLayout(4, 5); gl.setHgap(2); gl.setVgap(2); colorLevelPanel.setLayout(gl); colorLevelPanel.add(Box.createGlue()); colorLevelPanel.add(new JLabel(strings.getProperty("red"), JLabel.CENTER)); colorLevelPanel.add(new JLabel(strings.getProperty("green"), JLabel.CENTER)); colorLevelPanel.add(new JLabel(strings.getProperty("blue"), JLabel.CENTER)); colorLevelPanel.add(new JLabel(strings.getProperty("alpha"), JLabel.CENTER)); colorLevelPanel.add(new JLabel(strings.getProperty("offset"), JLabel.RIGHT)); SpinnerNumberModel offsetModelR = new SpinnerNumberModel(0, -255, 255, 1); SpinnerNumberModel offsetModelG = new SpinnerNumberModel(0, -255, 255, 1); SpinnerNumberModel offsetModelB = new SpinnerNumberModel(0, -255, 255, 1); SpinnerNumberModel offsetModelA = new SpinnerNumberModel(0, -255, 255, 1); offsetModelR.addChangeListener(changeEventHandler); offsetModelG.addChangeListener(changeEventHandler); offsetModelB.addChangeListener(changeEventHandler); offsetModelA.addChangeListener(changeEventHandler); spinOffsetR = new JSpinner(offsetModelR); spinOffsetG = new JSpinner(offsetModelG); spinOffsetB = new JSpinner(offsetModelB); spinOffsetA = new JSpinner(offsetModelA); spinOffsetR.addMouseWheelListener(new SpinnerWheelSupportListener(offsetModelR)); spinOffsetG.addMouseWheelListener(new SpinnerWheelSupportListener(offsetModelG)); spinOffsetB.addMouseWheelListener(new SpinnerWheelSupportListener(offsetModelB)); spinOffsetA.addMouseWheelListener(new SpinnerWheelSupportListener(offsetModelA)); colorLevelPanel.add(spinOffsetR); colorLevelPanel.add(spinOffsetG); colorLevelPanel.add(spinOffsetB); colorLevelPanel.add(spinOffsetA); colorLevelPanel.add(new JLabel(strings.getProperty("factor"), JLabel.RIGHT)); SpinnerNumberModel factorModelR = new SpinnerNumberModel(1., 0.01, 99, 0.01); SpinnerNumberModel factorModelG = new SpinnerNumberModel(1., 0.01, 99, 0.01); SpinnerNumberModel factorModelB = new SpinnerNumberModel(1., 0.01, 99, 0.01); SpinnerNumberModel factorModelA = new SpinnerNumberModel(1., 0.01, 99, 0.01); factorModelR.addChangeListener(changeEventHandler); factorModelG.addChangeListener(changeEventHandler); factorModelB.addChangeListener(changeEventHandler); factorModelA.addChangeListener(changeEventHandler); spinFactorR = new JSpinner(factorModelR); spinFactorG = new JSpinner(factorModelG); spinFactorB = new JSpinner(factorModelB); spinFactorA = new JSpinner(factorModelA); spinFactorR.addMouseWheelListener(new SpinnerWheelSupportListener(factorModelR)); spinFactorG.addMouseWheelListener(new SpinnerWheelSupportListener(factorModelG)); spinFactorB.addMouseWheelListener(new SpinnerWheelSupportListener(factorModelB)); spinFactorA.addMouseWheelListener(new SpinnerWheelSupportListener(factorModelA)); colorLevelPanel.add(spinFactorR); colorLevelPanel.add(spinFactorG); colorLevelPanel.add(spinFactorB); colorLevelPanel.add(spinFactorA); colorLevelPanel.add(new JLabel(strings.getProperty("gamma"), JLabel.RIGHT)); SpinnerNumberModel gammaModelR = new SpinnerNumberModel(1., 0.01, 99, 0.01); SpinnerNumberModel gammaModelG = new SpinnerNumberModel(1., 0.01, 99, 0.01); SpinnerNumberModel gammaModelB = new SpinnerNumberModel(1., 0.01, 99, 0.01); SpinnerNumberModel gammaModelA = new SpinnerNumberModel(1., 0.01, 99, 0.01); gammaModelR.addChangeListener(changeEventHandler); gammaModelG.addChangeListener(changeEventHandler); gammaModelB.addChangeListener(changeEventHandler); gammaModelA.addChangeListener(changeEventHandler); spinGammaR = new JSpinner(gammaModelR); spinGammaG = new JSpinner(gammaModelG); spinGammaB = new JSpinner(gammaModelB); spinGammaA = new JSpinner(gammaModelA); spinGammaR.addMouseWheelListener(new SpinnerWheelSupportListener(gammaModelR)); spinGammaG.addMouseWheelListener(new SpinnerWheelSupportListener(gammaModelG)); spinGammaB.addMouseWheelListener(new SpinnerWheelSupportListener(gammaModelB)); spinGammaA.addMouseWheelListener(new SpinnerWheelSupportListener(gammaModelA)); colorLevelPanel.add(spinGammaR); colorLevelPanel.add(spinGammaG); colorLevelPanel.add(spinGammaB); colorLevelPanel.add(spinGammaA); // 色調パネル JPanel colorTunePanel = new JPanel(); colorTunePanel.setBorder(BorderFactory.createCompoundBorder( BorderFactory.createEmptyBorder(3, 3, 3, 3), BorderFactory.createTitledBorder(strings.getProperty("group.hsb.caption")))); GridLayout gl2 = new GridLayout(3, 4); gl2.setHgap(3); gl2.setVgap(3); colorTunePanel.setLayout(gl2); colorTunePanel.add(new JLabel(strings.getProperty("hue"), JLabel.CENTER)); // Hue 色相 colorTunePanel.add(new JLabel(strings.getProperty("saturation"), JLabel.CENTER)); // Saturation 彩度 colorTunePanel.add(new JLabel(strings.getProperty("brightness"), JLabel.CENTER)); // Brightness 明度 colorTunePanel.add(new JLabel(strings.getProperty("contrast"), JLabel.CENTER)); // Contrast コントラスト SpinnerNumberModel hsbModelH = new SpinnerNumberModel(0., -1., 1., 0.001); SpinnerNumberModel hsbModelS = new SpinnerNumberModel(0., -1., 1., 0.001); SpinnerNumberModel hsbModelB = new SpinnerNumberModel(0., -1., 1., 0.001); SpinnerNumberModel hsbModelC = new SpinnerNumberModel(0., -1., 1., 0.001); hsbModelH.addChangeListener(changeEventHandler); hsbModelS.addChangeListener(changeEventHandler); hsbModelB.addChangeListener(changeEventHandler); hsbModelC.addChangeListener(changeEventHandler); spinHue = new JSpinner(hsbModelH); spinSaturation = new JSpinner(hsbModelS); spinBrightness = new JSpinner(hsbModelB); spinContrast = new JSpinner(hsbModelC); spinHue.addMouseWheelListener(new SpinnerWheelSupportListener(hsbModelH)); spinSaturation.addMouseWheelListener(new SpinnerWheelSupportListener(hsbModelS)); spinBrightness.addMouseWheelListener(new SpinnerWheelSupportListener(hsbModelB)); spinContrast.addMouseWheelListener(new SpinnerWheelSupportListener(hsbModelC)); colorTunePanel.add(spinHue); colorTunePanel.add(spinSaturation); colorTunePanel.add(spinBrightness); colorTunePanel.add(spinContrast); JSlider sliderHue = new JSlider(); JSlider sliderSaturation = new JSlider(); JSlider sliderBrightness = new JSlider(); JSlider sliderContrast = new JSlider(); sliderHue.setPreferredSize(spinHue.getPreferredSize()); sliderSaturation.setPreferredSize(spinSaturation.getPreferredSize()); sliderBrightness.setPreferredSize(spinBrightness.getPreferredSize()); sliderContrast.setPreferredSize(spinContrast.getPreferredSize()); colorTunePanel.add(sliderHue); colorTunePanel.add(sliderSaturation); colorTunePanel.add(sliderBrightness); colorTunePanel.add(sliderContrast); JSlider sliders[] = new JSlider[] {sliderHue, sliderSaturation, sliderBrightness, sliderContrast}; JSpinner spinners[] = new JSpinner[] {spinHue, spinSaturation, spinBrightness, spinContrast}; for (int idx = 0; idx < spinners.length; idx++) { final JSlider sl = sliders[idx]; final JSpinner sp = spinners[idx]; SpinnerNumberModel spModel = (SpinnerNumberModel) sp.getModel(); sl.setMinimum((int)(((Number)spModel.getMinimum()).floatValue() * 100)); sl.setMaximum((int)(((Number)spModel.getMaximum()).floatValue() * 100)); sl.setValue((int)(((Number) sp.getValue()).doubleValue() * 100.)); final Semaphore loopBlocker = new Semaphore(1); // イベントが循環発生することを防ぐ sl.addChangeListener(new ChangeListener() { public void stateChanged(ChangeEvent e) { if (loopBlocker.tryAcquire()) { try { double rate = sl.getValue() / 100.; sp.setValue(Double.valueOf(rate)); } finally { loopBlocker.release(); } } } }); sp.addChangeListener(new ChangeListener() { public void stateChanged(ChangeEvent e) { if (loopBlocker.tryAcquire()) { try { int rate = (int)(((Number) sp.getValue()).doubleValue() * 100.); sl.setValue(rate); } finally { loopBlocker.release(); } } } }); } // カラーグループ ColorGroup colorGroup = layer.getColorGroup(); JPanel colorGroupPanel = new JPanel(); colorGroupPanel.setBorder(BorderFactory.createCompoundBorder( BorderFactory.createEmptyBorder(3, 3, 3, 3), BorderFactory.createTitledBorder(strings.getProperty("colorgroup")))); GridBagLayout gbl2 = new GridBagLayout(); colorGroupPanel.setLayout(gbl2); GridBagConstraints gbc2 = new GridBagConstraints(); JLabel lblColorGroup = new JLabel(strings.getProperty("group"), JLabel.RIGHT); cmbColorGroup = new JComboBox(colorGroups.toArray(new ColorGroup[colorGroups.size()])); cmbColorGroup.setSelectedItem(colorGroup); cmbColorGroup.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { ColorGroup selColorGroup = (ColorGroup) cmbColorGroup.getSelectedItem(); chkColorGroupSync.setSelected(selColorGroup.isEnabled()); fireColorGroupChangeEvent(layer); } }); chkColorGroupSync = new JCheckBox(strings.getProperty("synchronized")); chkColorGroupSync.setSelected(layer.isInitSync()); gbc2.gridx = 0; gbc2.gridy = 0; gbc2.gridwidth = 1; gbc2.gridheight = 1; gbc2.weightx = 0.; gbc2.weighty = 0.; gbc2.anchor = GridBagConstraints.WEST; gbc2.fill = GridBagConstraints.BOTH; gbc2.ipadx = 0; gbc2.ipady = 0; gbc2.insets = new Insets(3, 3, 3, 3); colorGroupPanel.add(lblColorGroup, gbc2); gbc2.gridx = 1; gbc2.gridy = 0; gbc2.gridwidth = 1; gbc2.gridheight = 1; gbc2.weightx = 1.; gbc2.weighty = 0.; colorGroupPanel.add(cmbColorGroup, gbc2); gbc2.gridx = 2; gbc2.gridy = 0; gbc2.gridwidth = GridBagConstraints.REMAINDER; gbc2.gridheight = 1; gbc2.weightx = 0.; gbc2.weighty = 0.; colorGroupPanel.add(chkColorGroupSync, gbc2); if (colorGroupPanel != null) { container.add(colorGroupPanel); } container.add(colorLevelPanel); container.add(colorReplacePanel); container.add(colorTunePanel); } /** * このパネルで変更された色情報の状態をリセットする.
* 最後に{@link #setColorConvertParameter(ColorConvertParameter)}された値で * 設定し直す.
*/ public void resetColor() { setColorConvertParameter(paramOrg); } public void setColorConvertParameter(ColorConvertParameter param) { if (param == null) { throw new IllegalArgumentException(); } paramOrg = param.clone(); ColorConv colorReplace = param.getColorReplace(); if (colorReplace == null) { colorReplace = ColorConv.NONE; } changeEventDisableSemaphore.incrementAndGet(); try { cmbColorReplace.setSelectedItem(colorReplace); spinGray.setValue(Double.valueOf(param.getGrayLevel())); spinOffsetR.setValue(Integer.valueOf(param.getOffsetR())); spinOffsetG.setValue(Integer.valueOf(param.getOffsetG())); spinOffsetB.setValue(Integer.valueOf(param.getOffsetB())); spinOffsetA.setValue(Integer.valueOf(param.getOffsetA())); spinFactorR.setValue(Double.valueOf(param.getFactorR())); spinFactorG.setValue(Double.valueOf(param.getFactorG())); spinFactorB.setValue(Double.valueOf(param.getFactorB())); spinFactorA.setValue(Double.valueOf(param.getFactorA())); spinGammaR.setValue(Double.valueOf(param.getGammaR())); spinGammaG.setValue(Double.valueOf(param.getGammaG())); spinGammaB.setValue(Double.valueOf(param.getGammaB())); spinGammaA.setValue(Double.valueOf(param.getGammaA())); spinHue.setValue(Double.valueOf(param.getHue())); spinSaturation.setValue(Double.valueOf(param.getSaturation())); spinBrightness.setValue(Double.valueOf(param.getBrightness())); spinContrast.setValue(Double.valueOf(param.getContrast())); } finally { changeEventDisableSemaphore.decrementAndGet(); } chachedParam = param; firePropertyChange("colorConvertParameter", null, param); } public ColorConvertParameter getColorConvertParameter() { if (chachedParam != null) { return chachedParam; } ColorConvertParameter param = new ColorConvertParameter(); param.setColorReplace((ColorConv) cmbColorReplace.getSelectedItem()); param.setGrayLevel(((Number) spinGray.getValue()).floatValue()); param.setOffsetR(((Number) spinOffsetR.getValue()).intValue()); param.setOffsetG(((Number) spinOffsetG.getValue()).intValue()); param.setOffsetB(((Number) spinOffsetB.getValue()).intValue()); param.setOffsetA(((Number) spinOffsetA.getValue()).intValue()); param.setFactorR(((Number) spinFactorR.getValue()).floatValue()); param.setFactorG(((Number) spinFactorG.getValue()).floatValue()); param.setFactorB(((Number) spinFactorB.getValue()).floatValue()); param.setFactorA(((Number) spinFactorA.getValue()).floatValue()); param.setGammaR(((Number) spinGammaR.getValue()).floatValue()); param.setGammaG(((Number) spinGammaG.getValue()).floatValue()); param.setGammaB(((Number) spinGammaB.getValue()).floatValue()); param.setGammaA(((Number) spinGammaA.getValue()).floatValue()); param.setHue(((Number) spinHue.getValue()).floatValue()); param.setSaturation(((Number) spinSaturation.getValue()).floatValue()); param.setBrightness(((Number) spinBrightness.getValue()).floatValue()); param.setContrast(((Number) spinContrast.getValue()).floatValue()); chachedParam = param; return param; } /** * カラー設定が変更されているか? * @return 変更されている場合はtrue、そうでなければfalse */ public boolean isColorConvertParameterModified() { return !paramOrg.equals(getColorConvertParameter()); } public ColorGroup getColorGroup() { return (ColorGroup) cmbColorGroup.getSelectedItem(); } public void setColorGroup(ColorGroup colorGroup) { if (colorGroup == null) { colorGroup = ColorGroup.NA; } changeEventDisableSemaphore.incrementAndGet(); try { cmbColorGroup.setSelectedItem(colorGroup); } finally { changeEventDisableSemaphore.decrementAndGet(); } } public boolean isSyncColorGroup() { return chkColorGroupSync == null ? false : chkColorGroupSync.isSelected(); } public void setSyncColorGroup(boolean selected) { if (chkColorGroupSync != null) { changeEventDisableSemaphore.incrementAndGet(); try { chkColorGroupSync.setSelected(selected); } finally { changeEventDisableSemaphore.decrementAndGet(); } } } } charactermanaj/src/charactermanaj/ui/SelectCharatersDirDialog.java0000644000175000017500000003613111767047542025617 0ustar paulliupaulliupackage charactermanaj.ui; import java.awt.BorderLayout; import java.awt.Container; import java.awt.Dimension; import java.awt.Font; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.Insets; import java.awt.Toolkit; import java.awt.dnd.DropTarget; import java.awt.event.ActionEvent; import java.awt.event.FocusAdapter; import java.awt.event.FocusEvent; import java.awt.event.KeyEvent; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.io.File; import java.util.ArrayList; import java.util.List; import java.util.Properties; import java.util.logging.Level; import java.util.logging.Logger; import javax.swing.AbstractAction; import javax.swing.BorderFactory; import javax.swing.Box; import javax.swing.InputMap; import javax.swing.JButton; import javax.swing.JCheckBox; import javax.swing.JComboBox; import javax.swing.JDialog; import javax.swing.JFileChooser; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JRootPane; import javax.swing.KeyStroke; import charactermanaj.Main; import charactermanaj.model.io.CharacterDataPersistent; import charactermanaj.ui.util.FileDropTarget; import charactermanaj.util.ErrorMessageHelper; import charactermanaj.util.LocalizedResourcePropertyLoader; /** * 起動時にキャラクターデータディレクトリを選択するためのモーダルダイアログ.
* @author seraphy */ public class SelectCharatersDirDialog extends JDialog { private static final long serialVersionUID = -888834575856349442L; private static final Logger logger = Logger.getLogger(SelectCharatersDirDialog.class.getName()); /** * 最後に使用したキャラクターデータディレクトリと、その履歴情報. */ private final RecentCharactersDir recentCharactersDir; /** * 既定のディレクトリ */ private File defaultCharactersDir; /** * 選択されたディレクトリ */ private File selectedCharacterDir; /** * 次回起動時に問い合わせない */ private boolean doNotAskAgain; /** * ディレクトリ選択コンボ */ private JComboBox combDir; /** * 次回起動時に問い合わせないチェックボックス */ private JCheckBox chkDoNotAsk; public File getDefaultCharactersDir() { return defaultCharactersDir; } public void setDefaultCharactersDir(File defaultCharactersDir) { this.defaultCharactersDir = defaultCharactersDir; } public File getSelectedCharacterDir() { return selectedCharacterDir; } public boolean isDoNotAskAgain() { return doNotAskAgain; } /** * コンストラクタ * @param parent 親(通常は、null) * @param recentCharactersDir 最後に使用したキャラクターデータディレクトリと、その履歴情報. */ protected SelectCharatersDirDialog(JFrame parent, RecentCharactersDir recentCharactersDir) { super(parent, true); try { if (recentCharactersDir == null) { throw new IllegalArgumentException("recentCharactersDirにnullは指定できません。"); } this.recentCharactersDir = recentCharactersDir; setDefaultCloseOperation(DO_NOTHING_ON_CLOSE); addWindowListener(new WindowAdapter() { @Override public void windowClosing(WindowEvent e) { onClose(); } }); initComponent(); } catch (RuntimeException ex) { logger.log(Level.SEVERE, "キャラクターディレクトリ選択ダイアログの生成に失敗しました。", ex); dispose(); throw ex; } } private void initComponent() { Properties strings = LocalizedResourcePropertyLoader.getInstance() .getLocalizedProperties("languages/selectCharatersDirDialog"); Container contentPane = getContentPane(); contentPane.setLayout(new BorderLayout(3, 3)); AbstractAction actOk = new AbstractAction(strings.getProperty("btn.ok")) { private static final long serialVersionUID = 1L; public void actionPerformed(ActionEvent e) { onOK(); } }; AbstractAction actClose = new AbstractAction(strings.getProperty("btn.cancel")) { private static final long serialVersionUID = 1L; public void actionPerformed(ActionEvent e) { onClose(); } }; AbstractAction actBrowse = new AbstractAction(strings.getProperty("btn.chooseDir")) { private static final long serialVersionUID = 1L; public void actionPerformed(ActionEvent e) { onBrowse(); } }; AbstractAction actRemoveRecent = new AbstractAction(strings.getProperty("btn.clearRecentList")) { private static final long serialVersionUID = 1L; public void actionPerformed(ActionEvent e) { onRemoveRecent(); } }; final JButton btnRemoveRecent = new JButton(actRemoveRecent); final JButton btnOK = new JButton(actOk); final JButton btnCancel = new JButton(actClose); final JButton btnBroseForDir = new JButton(actBrowse); Toolkit tk = Toolkit.getDefaultToolkit(); final JRootPane rootPane = getRootPane(); FocusAdapter focusAdapter = new FocusAdapter() { @Override public void focusGained(FocusEvent e) { JButton btn = (JButton) e.getSource(); rootPane.setDefaultButton(btn); } @Override public void focusLost(FocusEvent e) { rootPane.setDefaultButton(btnOK); } }; rootPane.setDefaultButton(btnOK); InputMap im = rootPane.getInputMap(); im.put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "close"); im.put(KeyStroke.getKeyStroke(KeyEvent.VK_W, tk.getMenuShortcutKeyMask()), "close"); rootPane.getActionMap().put("close", actClose); btnRemoveRecent.addFocusListener(focusAdapter); btnOK.addFocusListener(focusAdapter); btnCancel.addFocusListener(focusAdapter); btnBroseForDir.addFocusListener(focusAdapter); JPanel dirPanel = new JPanel(new BorderLayout(3, 3)); dirPanel.setBorder(BorderFactory.createEmptyBorder(3, 10, 3, 3)); JLabel lbl = new JLabel(strings.getProperty("caption"), JLabel.CENTER); lbl.setFont(lbl.getFont().deriveFont(Font.BOLD)); lbl.setBorder(BorderFactory.createEmptyBorder(5, 0, 5, 0)); Dimension dim = lbl.getPreferredSize(); dim.width = Integer.parseInt(strings.getProperty("width")); lbl.setPreferredSize(dim); dirPanel.add(lbl, BorderLayout.NORTH); combDir = new JComboBox(); combDir.setEditable(true); dirPanel.add(combDir, BorderLayout.CENTER); dirPanel.add(new JLabel(strings.getProperty("lbl.dir")), BorderLayout.WEST); dirPanel.add(btnBroseForDir, BorderLayout.EAST); contentPane.add(dirPanel, BorderLayout.NORTH); JPanel btnPanel = new JPanel(); GridBagLayout gbl = new GridBagLayout(); btnPanel.setLayout(gbl); chkDoNotAsk = new JCheckBox(strings.getProperty("chk.doNotAskAgein")); chkDoNotAsk.setSelected(recentCharactersDir.isDoNotAskAgain()); GridBagConstraints gbc = new GridBagConstraints(); gbc.gridx = 0; gbc.gridy = 0; gbc.gridwidth = 5; gbc.gridheight = 1; gbc.weightx = 1.; gbc.weighty = 0.; gbc.fill = GridBagConstraints.BOTH; gbc.anchor = GridBagConstraints.WEST; gbc.ipadx = 0; gbc.ipady = 0; gbc.insets = new Insets(3, 3, 3, 3); btnPanel.add(chkDoNotAsk, gbc); gbc.gridx = 0; gbc.gridy = 1; gbc.gridwidth = 1; gbc.gridheight = 1; gbc.weightx = 0.; gbc.weighty = 0.; btnPanel.add(btnRemoveRecent, gbc); gbc.gridx = 1; gbc.gridy = 1; gbc.gridwidth = 1; gbc.gridheight = 1; gbc.weightx = 1.; gbc.weighty = 0.; btnPanel.add(Box.createGlue(), gbc); gbc.gridx = Main.isLinuxOrMacOSX() ? 3 : 2; gbc.gridy = 1; gbc.gridwidth = 1; gbc.gridheight = 1; gbc.weightx = 0.; gbc.weighty = 0.; btnPanel.add(btnOK, gbc); gbc.gridx = Main.isLinuxOrMacOSX() ? 2 : 3; gbc.gridy = 1; gbc.gridwidth = 1; gbc.gridheight = 1; gbc.weightx = 0.; gbc.weighty = 0.; btnPanel.add(btnCancel, gbc); gbc.gridx = 4; gbc.gridy = 1; gbc.gridwidth = 1; gbc.gridheight = 1; gbc.weightx = 0.; gbc.weighty = 0.; gbc.ipadx = 32; gbc.ipady = 0; btnPanel.add(Box.createGlue(), gbc); setTitle(strings.getProperty("title")); setResizable(false); contentPane.add(btnPanel, BorderLayout.SOUTH); // フォルダのドロップによる入力を許可 new DropTarget(this, new FileDropTarget() { @Override protected void onDropFiles(List dropFiles) { setSelectFile(dropFiles); } @Override protected void onException(Exception ex) { ErrorMessageHelper.showErrorDialog(SelectCharatersDirDialog.this, ex); } }); pack(); setLocationRelativeTo(null); } /** * ドロップによるファイル名の設定.
* 最初の1つだけを使用する.
* リストが空であるか、最初のファイルが、フォルダでなければ何もしない.
* @param dropFiles ドロップされたファイルリスト */ protected void setSelectFile(List dropFiles) { if (dropFiles.isEmpty()) { return; } File dropFile = dropFiles.get(0); if ( !dropFile.exists() || !dropFile.isDirectory()) { return; } combDir.setSelectedItem(dropFile); } /** * 指定したディレクトリの下のサブフォルダに、すでにcharacter.iniがある場合は、 * キャラクターなんとか機のディレクトリとして、標準のcharacter.xmlを生成する.
* ただし、書き込み禁止の場合は何もしない. * @param dirs 対象ディレクトリ */ protected void checkCharacterNantokaDirs(File dirs) { if (dirs == null || !dirs.isDirectory() || !dirs.canWrite()) { return; } CharacterDataPersistent persist = CharacterDataPersistent.getInstance(); for (File dir : dirs.listFiles()) { if (!dir.isDirectory()) { continue; } File characterXmlFile = new File(dir, CharacterDataPersistent.CONFIG_FILE); if (characterXmlFile.exists()) { // すでにキャラクター定義XMLが存在する. continue; } File characterIniFile = new File(dir, CharacterDataPersistent.COMPATIBLE_CONFIG_NAME); if (characterIniFile.exists() && characterIniFile.canWrite() && dir.canWrite()) { try { persist.convertFromCharacterIni(characterIniFile, characterXmlFile); } catch (Exception ex) { logger.log(Level.WARNING, "character.xmlの生成に失敗しました。:" + characterXmlFile, ex); } } } } protected void onClose() { selectedCharacterDir = null; dispose(); } protected void onOK() { try { Object value = combDir.getSelectedItem(); if (value != null && value instanceof String) { value = new File((String) value); } if (value != null && value instanceof File) { File file = (File) value; if (!file.exists()) { boolean result = file.mkdirs(); logger.log(Level.INFO, "mkdirs(" + file+ ") succeeded=" + result); } if (file.isDirectory()) { checkCharacterNantokaDirs(file); logger.log(Level.CONFIG, "selectedCharactersDir=" + file); selectedCharacterDir = file; doNotAskAgain = chkDoNotAsk.isSelected(); dispose(); return; } } // 選択されていないかファイルでない場合 Toolkit tk = Toolkit.getDefaultToolkit(); tk.beep(); } catch (Exception ex) { ErrorMessageHelper.showErrorDialog(this, ex); } } protected void onBrowse() { try { Object selectedItem = combDir.getSelectedItem(); String directoryTxt = null; if (selectedItem != null) { directoryTxt = selectedItem.toString(); } JFileChooser dirChooser = new JFileChooser(directoryTxt); dirChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); if (dirChooser.showOpenDialog(this) != JFileChooser.APPROVE_OPTION) { return; } File dir = dirChooser.getSelectedFile(); if (dir != null) { combDir.setSelectedItem(dir); } } catch (Exception ex) { ErrorMessageHelper.showErrorDialog(this, ex); } } protected void onRemoveRecent() { try { Object current = combDir.getSelectedItem(); recentCharactersDir.clrar(); setRecents(); if (current != null) { combDir.setSelectedItem(current); } } catch (Exception ex) { ErrorMessageHelper.showErrorDialog(this, ex); } } protected void setRecents() { // 現在の候補をクリア. while (combDir.getItemCount() > 0) { combDir.removeItemAt(0); } // 前回使用したディレクトリを最優先候補 ArrayList priorityDirs = new ArrayList(); File lastUseCatacterDir = recentCharactersDir.getLastUseCharacterDir(); if (lastUseCatacterDir != null) { if (defaultCharactersDir != null && !lastUseCatacterDir.equals(defaultCharactersDir)) { combDir.addItem(lastUseCatacterDir); priorityDirs.add(lastUseCatacterDir); } } // デフォルトのキャラクターデータを第2位に設定 if (defaultCharactersDir != null) { combDir.addItem(defaultCharactersDir); priorityDirs.add(defaultCharactersDir); } // それ以外の履歴を設定 for (File charactersDir : recentCharactersDir.getRecentCharacterDirs()) { if (charactersDir == null) { continue; } if (!priorityDirs.contains(charactersDir)) { combDir.addItem(charactersDir); } } // 第一候補を選択状態とする. if (combDir.getItemCount() > 0) { combDir.setSelectedIndex(0); } } public static File getCharacterDir(File defaultCharacterDir) { RecentCharactersDir recentChars; try { recentChars = RecentCharactersDir.load(); } catch (Exception ex) { logger.log(Level.WARNING, "最後に使用したキャラクターディレクトリ情報の読み込みに失敗しました。", ex); recentChars = null; } if (recentChars == null) { recentChars = new RecentCharactersDir(); } logger.log(Level.CONFIG, "RecentCharacterDirs.doNotAskAgain=" + recentChars.isDoNotAskAgain()); if (recentChars.isDoNotAskAgain()) { // 「再度問い合わせ無し」の場合で、過去のディレクトリが有効であれば、それを返す. File recentCharDir = recentChars.getLastUseCharacterDir(); if (recentCharDir != null && recentCharDir.exists() && recentCharDir.isDirectory()) { return recentCharDir; } recentChars.setDoNotAskAgain(false); // 不正である場合は「再度問い合わせ無し」をリセットする. } File selectedCharacterDir; SelectCharatersDirDialog dlg = new SelectCharatersDirDialog(null, recentChars); dlg.setDefaultCharactersDir(defaultCharacterDir); dlg.setRecents(); dlg.setVisible(true); selectedCharacterDir = dlg.getSelectedCharacterDir(); if (selectedCharacterDir != null) { recentChars.setLastUseCharacterDir(selectedCharacterDir); try { recentChars.setDoNotAskAgain(dlg.isDoNotAskAgain()); recentChars.saveRecents(); } catch (Exception ex) { logger.log(Level.WARNING, "最後に使用したキャラクターディレクトリ情報の保存に失敗しました。", ex); } } return selectedCharacterDir; } } charactermanaj/src/charactermanaj/ui/progress/0000755000175000017500000000000011767047541021760 5ustar paulliupaulliucharactermanaj/src/charactermanaj/ui/progress/Worker.java0000644000175000017500000000065111767047541024076 0ustar paulliupaulliupackage charactermanaj.ui.progress; /** * ワーカー.
* @author seraphy * * @param ワーカーの戻り型 */ public interface Worker { /** * ワーカーを実行する.
* * @param progressHandle 進行状態を通知するハンドル * @return 処理結果 * @throws Exception 何らかの失敗 */ T doWork(ProgressHandle progressHandle) throws Exception; } charactermanaj/src/charactermanaj/ui/progress/ProgressInfoHolder.java0000644000175000017500000000304011767047541026376 0ustar paulliupaulliupackage charactermanaj.ui.progress; /** * プログレスダイアログと、そのワーカースレッドの間で進行状態を通信するためのホルダ.
* @author seraphy */ public class ProgressInfoHolder implements ProgressHandle { /** * キャプション */ private String caption; /** * 進行状態不明フラグ. */ private Boolean indeterminate; /** * 進行状態の現在値 */ private Integer progressCurrent; /** * 進行状態の最大値 */ private Integer progressMaximum; public synchronized String getCaption() { return caption; } public synchronized void setCaption(String caption) { this.caption = caption; } public synchronized Boolean getIndeterminate() { return indeterminate; } public synchronized void setIndeterminate(boolean indeterminate) { this.indeterminate = indeterminate; } public synchronized Integer getProgressCurrent() { return progressCurrent; } public synchronized void setProgressCurrent(int progressCurrent) { this.progressCurrent = progressCurrent; } public synchronized Integer getProgressMaximum() { return progressMaximum; } public synchronized void setProgressMaximum(int progressMaximum) { this.progressMaximum = progressMaximum; } /** * 現在の状態で確定し、ただちに状態をリセットする.
*/ public synchronized void flush() { caption = null; indeterminate = null; progressCurrent = null; progressMaximum = null; } } charactermanaj/src/charactermanaj/ui/progress/WorkerWithProgessDialog.java0000644000175000017500000002245711767047541027425 0ustar paulliupaulliupackage charactermanaj.ui.progress; import java.awt.BorderLayout; import java.awt.Component; import java.awt.Container; import java.awt.Dimension; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.lang.Thread.UncaughtExceptionHandler; import javax.swing.BorderFactory; import javax.swing.JDialog; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JProgressBar; import javax.swing.JRootPane; import javax.swing.SwingUtilities; import javax.swing.Timer; import javax.swing.border.BevelBorder; /** * ワーカースレッドの実行中、プログレスを表示するモーダルダイアログ.
* ワーカースレッドの実行が完了するとダイアログは自動的に閉じられる.
* モーダルダイアログであるため、ワーカースレッドの実行中はユーザはUIを操作することはできない.
* @author seraphy * * @param ワーカーの処理結果の戻り値の型 */ public class WorkerWithProgessDialog extends JDialog { private static final long serialVersionUID = 1L; /** * ワーカースレッドが停止したことを示すフラグ */ private volatile boolean exitThread; /** * ワーカースレッドの戻り値 */ private volatile T result; /** * ワーカースレッドが例外により終了した場合の例外 */ private volatile Throwable occuredException; /** * ワーカースレッド */ private Thread thread; /** * ワーカースレッドの状態を監視しプログレスに反映させるタイマー */ private Timer timer; /** * プログレスの更新頻度(タイマーのインターバル) */ private static int interval = 200; /** * 親フレームとワーカーを指定して構築する.
* @param parent 親フレーム * @param worker ワーカー */ public WorkerWithProgessDialog(JFrame parent, Worker worker) { super(parent, true); try { if (worker == null) { throw new IllegalArgumentException(); } initComponent(parent, worker); } catch (RuntimeException ex) { dispose(); throw ex; } } /** * 親ダイアログとワーカーを指定して構築する.
* @param parent 親フレーム * @param worker ワーカー */ public WorkerWithProgessDialog(JDialog parent, Worker worker) { super(parent, true); try { if (worker == null) { throw new IllegalArgumentException(); } initComponent(parent, worker); } catch (RuntimeException ex) { dispose(); throw ex; } } /** * コンポーネントの初期化 * @param parent 親フレームまたは親ダイアログ * @param worker ワーカー */ private void initComponent(Component parent, Worker worker) { // 閉じるボタンは無効 setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE); // リサイズ不可 setResizable(false); // ウィンドウ装飾なし (閉じるボタンやタイトルバーなども無し) setUndecorated(true); Container container = getContentPane(); // プログレスバー final JProgressBar progressBar = new JProgressBar(); progressBar.setIndeterminate(true); progressBar.setStringPainted(false); container.add(progressBar, BorderLayout.SOUTH); // デフォルトのラベル表示 String title = "please wait for a while."; final JLabel lblCaption = new JLabel(title); container.add(lblCaption, BorderLayout.NORTH); // ウィンドウ枠 JRootPane rootPane = getRootPane(); rootPane.setBorder(BorderFactory.createCompoundBorder( BorderFactory.createBevelBorder(BevelBorder.RAISED), BorderFactory.createEmptyBorder(5, 5, 5, 5)) ); // 親ウィンドウの幅の70% Dimension dim = progressBar.getPreferredSize(); dim.width = (int)(parent.getWidth() * 0.7); progressBar.setPreferredSize(dim); // パックする. pack(); // 親の中央に表示 setLocationRelativeTo(parent); // ワーカースレッドとプログレスダイアログとの状態通信用オブジェクト final ProgressInfoHolder progressHandle = new ProgressInfoHolder() { @Override public synchronized void flush() { final String caption = getCaption(); final Boolean indeterminate = getIndeterminate(); final Integer progressMaximum = getProgressMaximum(); final Integer progressCurrent = getProgressCurrent(); if (caption != null || progressMaximum != null || progressCurrent != null || indeterminate != null) { SwingUtilities.invokeLater(new Runnable() { public void run() { // 設定されている値でプログレスダイアログに状態を反映する. if (caption != null) { lblCaption.setText(caption); } if (progressMaximum != null) { progressBar.setMaximum(progressMaximum.intValue()); } if (progressCurrent != null) { progressBar.setValue(progressCurrent.intValue()); } if (indeterminate != null) { progressBar.setIndeterminate(indeterminate.booleanValue()); progressBar.setStringPainted( !indeterminate.booleanValue()); } } }); } super.flush(); } }; // プログレスダイアログに状態を反映させるためのタイマー timer = new Timer(interval, new ActionListener() { public void actionPerformed(ActionEvent e) { if (exitThread || !thread.isAlive()) { // スレッドが終了していればスレッド停止を通知する. onExitWork(); } else { // スレッドが生きていれば、スレッドの進行状態を // プログレスダイアログに反映させる. progressHandle.flush(); } } }); // ワーカースレッドの構築. thread = new Thread(createJob(worker, progressHandle)); thread.setDaemon(true); // ワーカースレッドが予期せぬハンドルされていない例外により終了した場合のハンドラ. thread.setUncaughtExceptionHandler(new UncaughtExceptionHandler() { public void uncaughtException(Thread t, Throwable e) { occuredException = e; onExitWork(); } }); } /** * プログレスの表示間隔を取得する * @return 表示間隔 */ public static int getInterval() { return interval; } /** * プログレスの表示間隔を設定する * @param interval 表示間隔 */ public static void setInterval(int interval) { WorkerWithProgessDialog.interval = interval; } /** * ワーカーをラップするワーカースレッドのジョブを作成する.
* @param worker ワーカー * @param progressHandle 進行状態の通知ハンドル * @return ジョブ */ protected Runnable createJob(final Worker worker, final ProgressHandle progressHandle) { return new Runnable() { public void run() { try { try { worker.doWork(progressHandle); } catch (Throwable ex) { occuredException = ex; } } finally { onExitWork(); } } }; } /** * ワーカースレッドより、スレッドが終了したことを通知される.
* ワーカースレッド自身か、ワーカースレッドの例外ハンドラか、 * タイマーから呼び出されるため、2回以上呼び出される可能性がある.
*/ protected void onExitWork() { exitThread = true; SwingUtilities.invokeLater(new Runnable() { public void run() { // プログレスダイアログが表示されている場合、 // それを破棄する.(モーダルの解除) if (isDisplayable() && isVisible()) { dispose(); } } }); } /** * ワーカースレッドを開始し、プログレスダイアログを表示し、 * ワーカースレッドの完了まで待機する.
* @throws WorkerException ワーカースレッドが例外により終了した場合 */ public void startAndWait() throws WorkerException { // 初期化 result = null; occuredException = null; exitThread = false; // ワーカースレッドの開始 thread.start(); try { timer.start(); try { // モーダルダイアログの開始 // (モーダルダイアログが非表示されるまで制御を返さない.) setVisible(true); } finally { timer.stop(); } } finally { for (;;) { try { // ワーカースレッドの停止を待機する. thread.join(); break; } catch (InterruptedException ex) { // 割り込みされた場合は、ワーカースレッドを割り込みする. thread.interrupt(); } } } // ワーカースレッドが例外により終了した場合 // その例外を送出する. if (occuredException != null) { throw new WorkerException( "worker has failed." + occuredException.getMessage(), occuredException ); } } /** * ワーカースレッドの戻り値を取得する.
* 正常終了していない場合、または処理中の場合は意味を持たない.
* @return ワーカースレッドの戻り値 */ public T getResult() { return result; } } charactermanaj/src/charactermanaj/ui/progress/WorkerException.java0000644000175000017500000000077311767047541025762 0ustar paulliupaulliupackage charactermanaj.ui.progress; /** * プログレスダイアログのワーカースレッド実行中に例外が発生した場合 * @author seraphy */ public class WorkerException extends Exception { private static final long serialVersionUID = -8315947965963588713L; public WorkerException() { super(); } public WorkerException(String message) { super(message); } public WorkerException(String message, Throwable cause) { super(message, cause); } } charactermanaj/src/charactermanaj/ui/progress/ProgressHandle.java0000644000175000017500000000155311767047541025547 0ustar paulliupaulliupackage charactermanaj.ui.progress; /** * ワーカースレッドからプログレスダイアログに状態を通知するためのインターフェイス.
* 中途半端な状態で反映されないように、複数のプロパティを設定する場合は * 同期をとること.
* @author seraphy */ public interface ProgressHandle { /** * 進行状態の最大値を設定する. * @param maximum */ void setProgressMaximum(int maximum); /** * 進行状態の現在値を設定する. * @param current */ void setProgressCurrent(int current); /** * 進行状態が不明であることを設定する. * @param indeterminate */ void setIndeterminate(boolean indeterminate); /** * キャプションを設定する. * @param caption */ void setCaption(String caption); } charactermanaj/src/charactermanaj/ui/MenuData.java0000644000175000017500000000667211767047542022471 0ustar paulliupaulliupackage charactermanaj.ui; import java.awt.event.ActionListener; import java.util.AbstractCollection; import java.util.ArrayList; import java.util.Iterator; import javax.swing.JMenuItem; import javax.swing.KeyStroke; import charactermanaj.Main; public class MenuData extends AbstractCollection { private String name; private boolean checkbox; private String text; private Character mnemonic; private String mnemonicDisp; private boolean ignoreMacOSX; private String shortcutKey; private ActionListener actionListener; private ArrayList children = new ArrayList(); public MenuData() { this(null, false, null, null, false, null, null); } public MenuData(String text, boolean checkbox, Character mnemonic, String mnemonicDisp, boolean ignoreMacOSX, String shortcutKey, ActionListener actionListener) { this.text = text; this.checkbox = checkbox; this.mnemonic = mnemonic; this.mnemonicDisp = mnemonicDisp; this.ignoreMacOSX = ignoreMacOSX; this.shortcutKey = shortcutKey; this.actionListener = actionListener; } public void setName(String name) { this.name = name; } public String getName() { return name; } public void setCheckbox(boolean checkbox) { this.checkbox = checkbox; } public boolean isCheckbox() { return checkbox; } public String getText() { return text; } public void setText(String text) { this.text = text; } public Character getMnemonic() { return mnemonic; } public void setMnemonic(Character mnemonic) { this.mnemonic = mnemonic; } public String getMnemonicDisp() { return mnemonicDisp; } public void setMnimonicDisp(String mnemonicDisp) { this.mnemonicDisp = mnemonicDisp; } public boolean isIgnoreMacOSX() { return ignoreMacOSX; } public void setIgnoreMacOSX(boolean ignoreMacOSX) { this.ignoreMacOSX = ignoreMacOSX; } public ActionListener getActionListener() { return actionListener; } public void setActionListener(ActionListener actionListener) { this.actionListener = actionListener; } public String getShortcutKey() { return shortcutKey; } public void setShortcutKey(String shortcutKey) { this.shortcutKey = shortcutKey; } @Override public int size() { return children.size(); } @Override public Iterator iterator() { return children.iterator(); } @Override public boolean add(MenuData o) { return children.add(o); } public boolean makeMenu(JMenuItem menu) { if (! isIgnoreMacOSX() || ! Main.isMacOSX()) { if (Main.isMacOSX()) { menu.setText(getText()); } else { Character mnemonic = getMnemonic(); String mnemonicDisp =getMnemonicDisp(); if (mnemonicDisp == null) { mnemonicDisp = ""; } menu.setName(getName()); menu.setText(getText() + mnemonicDisp); if (mnemonic != null) { menu.setMnemonic(mnemonic); } } if (actionListener != null) { menu.addActionListener(actionListener); } if (shortcutKey != null && shortcutKey.length() > 0) { if (Main.isMacOSX()) { shortcutKey = shortcutKey.replace("?", "meta"); } else { shortcutKey = shortcutKey.replace("?", "control"); } KeyStroke ks = KeyStroke.getKeyStroke(shortcutKey); if (ks != null) { menu.setAccelerator(ks); } } return true; } return false; } } charactermanaj/src/charactermanaj/ui/ImageSelectPanel.java0000644000175000017500000012410511767047542024125 0ustar paulliupaulliupackage charactermanaj.ui; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Component; import java.awt.Dimension; import java.awt.Font; import java.awt.Insets; import java.awt.Rectangle; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.FocusAdapter; import java.awt.event.FocusEvent; import java.awt.event.KeyEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.io.Serializable; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.EventListener; import java.util.EventObject; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Properties; import javax.swing.AbstractAction; import javax.swing.Action; import javax.swing.ActionMap; import javax.swing.BorderFactory; import javax.swing.DefaultListSelectionModel; import javax.swing.InputMap; import javax.swing.JButton; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JPopupMenu; import javax.swing.JScrollPane; import javax.swing.JTable; import javax.swing.JToolBar; import javax.swing.KeyStroke; import javax.swing.ListSelectionModel; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; import javax.swing.event.TableModelEvent; import javax.swing.event.TableModelListener; import javax.swing.table.AbstractTableModel; import javax.swing.table.DefaultTableColumnModel; import javax.swing.table.TableCellRenderer; import javax.swing.table.TableColumn; import charactermanaj.model.AppConfig; import charactermanaj.model.PartsCategory; import charactermanaj.model.PartsIdentifier; import charactermanaj.model.PartsSpec; import charactermanaj.model.PartsSpecResolver; import charactermanaj.util.LocalizedResourcePropertyLoader; import charactermanaj.util.UIHelper; /** * 各パーツの選択パネル(カテゴリ別) * @author seraphy */ public class ImageSelectPanel extends JPanel { private static final long serialVersionUID = 1L; protected static final String STRINGS_RESOURCE = "languages/imageselectpanel"; /** * 変更通知を受けるリスナ * @author seraphy */ public interface ImageSelectPanelListener extends EventListener { /** * 選択が変更された場合 * @param event */ void onSelectChange(ImageSelectPanelEvent event); /** * アイテムが選択された場合 * @param event */ void onChange(ImageSelectPanelEvent event); /** * 色変更ボタンが押された場合 * @param event */ void onChangeColor(ImageSelectPanelEvent event); /** * 設定ボタンが押された場合 * @param event */ void onPreferences(ImageSelectPanelEvent event); /** * タイトルがクリックされた場合 * @param event */ void onTitleClick(ImageSelectPanelEvent event); /** * タイトルがクリックされた場合 * @param event */ void onTitleDblClick(ImageSelectPanelEvent event); }; /** * 変更通知イベント * @author seraphy */ public static class ImageSelectPanelEvent extends EventObject { private static final long serialVersionUID = 1L; public ImageSelectPanelEvent(ImageSelectPanel src) { super(src); } public ImageSelectPanel getImageSelectPanel() { return (ImageSelectPanel) getSource(); } } /** * 表示モード * @author seraphy */ public enum DisplayMode { /** * 最小化モード */ MINIMIZED, /** * 通常モード */ NORMAL, /** * 最大サイズフリーモード */ EXPANDED } /** * パネルノ拡大・縮小時のステップサイズ */ private static final int rowStep = 2; /** * 変更通知を受けるリスナー */ private final LinkedList listeners = new LinkedList(); /** * リストの一行の高さ */ private final int rowHeight; /** * パネルの最小高さ (ボーダー上限 + ヘッダ行の高さ) */ private final int minHeight; /** * 現在の表示行数 */ private int numOfVisibleRows; /** * 最小化モードであるか? */ private DisplayMode displayMode; /** * パーツ情報ソース */ private PartsSpecResolver partsSpecResolver; /** * パーツ選択テーブル */ private final JTable partsSelectTable; /** * パーツ選択テーブルモデル */ private final PartsSelectListModel partsSelectTableModel; /** * 選択中のアイテム(複数選択の場合はフォーカスされているもの)、もしくはnull */ private PartsIdentifier selectedPartsIdentifier; /** * 選択中のアイテムのリスト(順序あり)、もしくは空 */ private List selectedPartsIdentifiers = Collections.emptyList(); /** * このパネルが対象とするカテゴリ情報 */ private final PartsCategory partsCategory; /** * イメージ選択パネルを構築する * @param partsCategory パーツカテゴリ * @param partsSpecResolver キャラクターデータ */ public ImageSelectPanel(final PartsCategory partsCategory, final PartsSpecResolver partsSpecResolver) { if (partsCategory == null || partsSpecResolver == null) { throw new IllegalArgumentException(); } this.partsCategory = partsCategory; this.partsSpecResolver = partsSpecResolver; setLayout(new BorderLayout()); setBorder(BorderFactory.createCompoundBorder( BorderFactory.createEmptyBorder(3, 3, 3, 3), BorderFactory.createCompoundBorder( BorderFactory.createEtchedBorder(), BorderFactory.createEmptyBorder(3, 3, 3, 3)) ) ); partsSelectTableModel = new PartsSelectListModel(partsCategory); final DefaultTableColumnModel columnModel = new DefaultTableColumnModel(); TableColumn checkColumn = new TableColumn(0, 32); checkColumn.setMaxWidth(42); columnModel.addColumn(checkColumn); columnModel.addColumn(new TableColumn(1, 100)); final DefaultListSelectionModel selectionModel = new DefaultListSelectionModel(); selectionModel.addListSelectionListener(new ListSelectionListener() { public void valueChanged(ListSelectionEvent e) { if (!e.getValueIsAdjusting()) { onSelectChange(new ImageSelectPanelEvent(ImageSelectPanel.this)); } } }); AppConfig appConfig = AppConfig.getInstance(); final Properties strings = LocalizedResourcePropertyLoader.getInstance() .getLocalizedProperties(STRINGS_RESOURCE); final Color selectedItemColor = appConfig.getCheckedItemBgColor(); partsSelectTable = new JTable(partsSelectTableModel, columnModel, selectionModel) { private static final long serialVersionUID = 1L; @Override public Component prepareRenderer(TableCellRenderer renderer, int row, int column) { Component comp = super.prepareRenderer(renderer, row, column); if (isCellSelected(row, column) && hasFocus()) { // フォーカスのあるセル選択の背景色 comp.setBackground(getSelectionBackground()); } else { // フォーカスのないセル選択行 Boolean chk = (Boolean) getModel().getValueAt(row, 0); comp.setForeground(getForeground()); if (chk.booleanValue()) { // チェック済みの場合の背景色 comp.setBackground(selectedItemColor); } else { // 通常の背景色 comp.setBackground(getBackground()); } } return comp; } @Override public String getToolTipText(MouseEvent event) { // マウスが置かれている行のツールチップとしてパーツ名を表示する. int row = rowAtPoint(event.getPoint()); int mx = partsSelectTableModel.getRowCount(); if (row >= 0 && row < mx) { PartsSelectRow rowModel = partsSelectTableModel.getRow(row); PartsIdentifier partsIdentifier = rowModel.getPartsIdentifier(); PartsSpec partsSpec = partsSpecResolver.getPartsSpec(partsIdentifier); String suffix = ""; if (partsSpec != null) { // パーツの作者名とバージョンがあれば、それを末尾につけて表示する. String author = partsSpec.getAuthor(); double version = partsSpec.getVersion(); if (author != null) { if (version > 0) { suffix = " (" + author + " " + version + ")"; } else { suffix = " (" + author + ")"; } } } return partsIdentifier.getLocalizedPartsName() + suffix; } return null; } }; partsSelectTable.addFocusListener(new FocusAdapter() { @Override public void focusGained(FocusEvent e) { partsSelectTable.repaint(); } @Override public void focusLost(FocusEvent e) { partsSelectTable.repaint(); } }); final JPopupMenu partsSelectTablePopupMenu = new JPopupMenu(); Action actDeselectAll = new AbstractAction( strings.getProperty("popupmenu.deselectall")) { private static final long serialVersionUID = 9132032971228670868L; public void actionPerformed(ActionEvent e) { deselectAll(); } }; partsSelectTablePopupMenu.add(actDeselectAll); partsSelectTable.addMouseListener(new MouseAdapter() { @Override public void mousePressed(MouseEvent e) { evaluatePopup(e); } @Override public void mouseReleased(MouseEvent e) { evaluatePopup(e); } private void evaluatePopup(MouseEvent e) { if ((partsCategory.isMultipleSelectable() || isDeselectableSingleCategory()) && e.isPopupTrigger()) { partsSelectTablePopupMenu.show(partsSelectTable, e.getX(), e.getY()); } } }); partsSelectTableModel.addTableModelListener(new TableModelListener() { public void tableChanged(TableModelEvent e) { if (e.getType() == TableModelEvent.UPDATE) { onChange(new ImageSelectPanelEvent(ImageSelectPanel.this)); } } }); partsSelectTable.setSelectionBackground(appConfig.getSelectedItemBgColor()); if (partsCategory.isMultipleSelectable()) { partsSelectTable.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); } else { partsSelectTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); } partsSelectTable.setRowSelectionAllowed(true); partsSelectTable.setTableHeader(null); partsSelectTable.setAutoResizeMode(JTable.AUTO_RESIZE_LAST_COLUMN); partsSelectTable.setShowVerticalLines(false); partsSelectTable.setShowHorizontalLines(false); InputMap im = partsSelectTable.getInputMap(); im.put(KeyStroke.getKeyStroke(KeyEvent.VK_SPACE, 0), "toggleCheck"); im.put(KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0), "resetCheck"); ActionMap am = partsSelectTable.getActionMap(); am.put("toggleCheck", new AbstractAction() { private static final long serialVersionUID = 1L; public void actionPerformed(ActionEvent e) { int[] selectedRows = partsSelectTable.getSelectedRows(); boolean[] checks = partsSelectTableModel.getChecks(selectedRows); int checkedCount = 0; for (boolean checked : checks) { if (checked) { checkedCount++; } } if (checks.length == checkedCount) { // 選択しているアイテムのすべてがチェック済みである partsSelectTableModel.setChecks(false, selectedRows); } else { // 選択しているアイテムの一部もしくは全部がチェックされていない partsSelectTableModel.setChecks(true, selectedRows); } } }); am.put("resetCheck", new AbstractAction() { private static final long serialVersionUID = 1L; public void actionPerformed(ActionEvent e) { partsSelectTableModel.setChecks(false, partsSelectTable.getSelectedRows()); } }); JScrollPane scrollPane = new JScrollPane(partsSelectTable); scrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); UIHelper uiUtl = UIHelper.getInstance(); JButton leftBtn = uiUtl.createTransparentButton("icons/left.png", "icons/left2.png"); JButton rightBtn = uiUtl.createTransparentButton("icons/right.png", "icons/right2.png"); JButton colorBtn = uiUtl.createTransparentButton("icons/color.png", "icons/color2.png"); JButton configBtn = uiUtl.createTransparentButton("icons/config.png", "icons/config2.png"); leftBtn.setToolTipText(strings.getProperty("tooltip.shrink")); rightBtn.setToolTipText(strings.getProperty("tooltip.expand")); colorBtn.setToolTipText(strings.getProperty("tooltip.color")); configBtn.setToolTipText(strings.getProperty("tooltip.config")); leftBtn.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { if (isMinimizeMode()) { setMinimizeMode(false); } else { shrink(); } } }); rightBtn.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { if (isMinimizeMode()) { setMinimizeMode(false); } else { expand(); } } }); colorBtn.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { onChangeColor(); } }); configBtn.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { onPreferences(); } }); JPanel btnPanelGrp = new JPanel(new BorderLayout()); JToolBar toolBar = new JToolBar(); toolBar.setFloatable(false); toolBar.add(leftBtn); toolBar.add(rightBtn); toolBar.add(colorBtn); //toolBar.add(configBtn); // 設定ボタン (現在は非表示) btnPanelGrp.add(toolBar, BorderLayout.NORTH); if (partsCategory.isMultipleSelectable()) { UIHelper uiUty = UIHelper.getInstance(); JButton upBtn = uiUty.createTransparentButton("icons/arrow_up.png", "icons/arrow_up2.png"); JButton downBtn = uiUty.createTransparentButton("icons/arrow_down.png", "icons/arrow_down2.png"); JButton sortBtn = uiUty.createTransparentButton("icons/sort.png", "icons/sort2.png"); upBtn.setToolTipText(strings.getProperty("tooltip.up")); downBtn.setToolTipText(strings.getProperty("tooltip.down")); sortBtn.setToolTipText(strings.getProperty("tooltip.sort")); upBtn.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { onUp(); } }); downBtn.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { onDown(); } }); sortBtn.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { onSort(); } }); JToolBar toolBar2 = new JToolBar(); toolBar2.setFloatable(false); toolBar2.add(upBtn); toolBar2.add(downBtn); toolBar2.add(sortBtn); btnPanelGrp.add(toolBar2, BorderLayout.SOUTH); } JPanel header = new JPanel(new BorderLayout()); header.add(btnPanelGrp, BorderLayout.EAST); final JLabel title = new JLabel(" " + partsCategory.getLocalizedCategoryName() + " "); Font font = title.getFont(); title.setFont(font.deriveFont(Font.BOLD)); final Color defaultTitleColor = title.getForeground(); final Color hilightColor = appConfig.getSelectPanelTitleColor(); title.addMouseListener(new MouseAdapter() { @Override public void mousePressed(MouseEvent e) { if (e.getClickCount() == 2) { // 正確に2回 onTitleDblClick(); } else if (e.getClickCount() == 1) { // 正確に1回 onTitleClick(); } } @Override public void mouseEntered(MouseEvent e) { title.setForeground(hilightColor); } @Override public void mouseExited(MouseEvent e) { title.setForeground(defaultTitleColor); } }); header.add(title, BorderLayout.CENTER); add(header, BorderLayout.NORTH); add(scrollPane, BorderLayout.CENTER); rowHeight = partsSelectTable.getRowHeight(); // パネルの最小高さ (ボーダー上下 + ヘッダ行高さ) Insets insets = getInsets(); minHeight = header.getPreferredSize().height + insets.top + insets.bottom; // デフォルトのパネル幅を設定する. Dimension dim = new Dimension(200, 200); setPreferredSize(dim); // パネルの初期サイズ numOfVisibleRows = partsCategory.getVisibleRows(); setDisplayMode(DisplayMode.NORMAL); } /** * 表示行数から推奨のパネル高さを求める.
* パネル高さは1行の高さ x 表示行数 + ヘッダ + ボーダー上下である.
* @param numOfVisibleRows 表示行数 * @return 推奨のパネル高さ */ protected int calcPreferredHeight(int numOfVisibleRows) { return minHeight + Math.max(0, rowHeight * numOfVisibleRows); } /** * パーツをパネルにロードします.
* 既存の内容はリセットされたのち、現在の選択パーツ位置にスクロールします.
*/ public void loadParts() { partsSelectTableModel.load(partsSpecResolver.getPartsSpecMap(partsCategory).keySet()); scrollToSelectedRow(); } /** * このイメージ選択パネルの該当カテゴリを返します.
* @return カテゴリ */ public PartsCategory getPartsCategory() { return partsCategory; } /** * 現在選択している、すべてのパーツの選択を解除します.
* 単一選択カテゴリであるかどうかを問わず、常にすべて解除されます.
* 変更イベントが発生します.
*/ public void deselectAll() { PartsSelectListModel rowModelList = (PartsSelectListModel) partsSelectTable.getModel(); ArrayList rowModels = rowModelList.getRowModelList(); // すべての選択を解除する. for (PartsSelectRow rowModel : rowModels) { rowModel.setChecked(false); } // コンポーネントではなく、モデルに対する直接変更であるため、イベントは発生しません. // そのため再描画させる必要があります. partsSelectTable.repaint(); // アイテムの選択が変更されたことを通知する. onChange(new ImageSelectPanelEvent(ImageSelectPanel.this)); } /** * カテゴリのリストでパーツを選択しなおします.
* 変更イベントは発生しません.
* @param partsIdentifiers */ public void selectParts(Collection partsIdentifiers) { if (partsIdentifiers == null) { partsIdentifiers = Collections.emptyList(); } PartsSelectListModel rowModelList = (PartsSelectListModel) partsSelectTable.getModel(); ArrayList rowModels = rowModelList.getRowModelList(); for (PartsSelectRow rowModel : rowModels) { rowModel.setChecked(false); } ArrayList partsIdentifiersBuf = new ArrayList(partsIdentifiers); Collections.reverse(partsIdentifiersBuf); for (PartsIdentifier partsIdentifier : partsIdentifiersBuf) { Iterator ite = rowModels.iterator(); while (ite.hasNext()) { PartsSelectRow rowModel = ite.next(); if (rowModel.getPartsIdentifier().equals(partsIdentifier)) { rowModel.setChecked(true); if (partsIdentifiersBuf.size() >= 2 && partsCategory.isMultipleSelectable()) { ite.remove(); rowModels.add(0, rowModel); } break; } } } // 選択を保存する selectedPartsIdentifier = getSelectedPartsIdentifier(); selectedPartsIdentifiers = getSelectedPartsIdentifiers(); // コンポーネントではなく、モデルに対する直接変更であるため、イベントは発生しません. // そのため再描画させる必要があります. partsSelectTable.repaint(); // あたらしく選択されたアイテムが表示されるようにスクロールします. scrollToSelectedRow(); } /** * カテゴリのリストで選択中のアイテムが見えるようにスクロールする. */ public void scrollToSelectedRow() { PartsSelectListModel rowModelList = (PartsSelectListModel) partsSelectTable.getModel(); ArrayList rowModels = rowModelList.getRowModelList(); int mx = rowModels.size(); for (int row = 0; row < mx; row++) { if (rowModels.get(row).isChecked()) { Rectangle rct = partsSelectTable.getCellRect(row, 0, true); partsSelectTable.scrollRectToVisible(rct); break; } } } /** * カテゴリのパネルを最小表示.
* 最小化の場合は、高さは表示行数ゼロとなりタイトルとボーダーだけとなる.
* 最小化解除した場合は、標準高さは既定、最大サイズはフリーとなる.
* @param shrinkMode 最小化モードならばtrue、フリーモードならばfalse */ public void setMinimizeMode(boolean minimizeMode) { setDisplayMode(minimizeMode ? DisplayMode.MINIMIZED : DisplayMode.EXPANDED); } /** * 表示モードを切り替えパネルサイズを調整する.
* @param displayMode 表示モード */ public void setDisplayMode(DisplayMode displayMode) { if (displayMode == null) { displayMode = DisplayMode.NORMAL; } Dimension siz = getPreferredSize(); Dimension sizMax = getMaximumSize(); if (displayMode == DisplayMode.MINIMIZED) { int preferredHeight = calcPreferredHeight(0); siz.height = preferredHeight; sizMax.height = preferredHeight; } else if (displayMode == DisplayMode.EXPANDED) { int preferredHeight = calcPreferredHeight(numOfVisibleRows); siz.height = preferredHeight; sizMax.height = Integer.MAX_VALUE; } else { // DisplayMode.NORMALの場合 int preferredHeight = calcPreferredHeight(numOfVisibleRows); siz.height = preferredHeight; sizMax.height = preferredHeight; } setPreferredSize(siz); setMinimumSize(siz); setMaximumSize(sizMax); this.displayMode = displayMode; revalidate(); } public DisplayMode getDisplayMode() { return displayMode; } public boolean isMinimizeMode() { return displayMode == DisplayMode.MINIMIZED; } /** * カテゴリのパネルを縮小する.
* ただし、ヘッダ部よりは小さくならない.
* 現在の表示モードが標準でなければ縮小せず標準に戻す.
*/ public void shrink() { if (displayMode == DisplayMode.NORMAL) { // 表示行数を減ずる numOfVisibleRows = Math.max(0, numOfVisibleRows - rowStep); } // 通常モードの適用 setDisplayMode(DisplayMode.NORMAL); } /** * カテゴリのパネルを拡大する.
* 現在の表示モードが標準でなければ拡大前せず標準に戻す.
*/ public void expand() { if (displayMode == DisplayMode.NORMAL) { // 表示行数を加算する numOfVisibleRows += Math.max(0, rowStep); } // 通常モードの適用 setDisplayMode(DisplayMode.NORMAL); } public void addImageSelectListener(ImageSelectPanelListener listener) { if (listener == null) { throw new IllegalArgumentException(); } listeners.add(listener); } public void removeImageSelectListener(ImageSelectPanelListener listener) { listeners.remove(listener); } public void requestListFocus() { partsSelectTable.requestFocus(); } /** * 指定したパーツ識別子にフォーカスを当てます.
* 必要に応じてスクロールされます.
* 該当するパーツ識別子がなければ何もしません.
* @param partsIdentifier パーツ識別子 */ public void setSelection(PartsIdentifier partsIdentifier) { if (partsIdentifier == null) { return; } PartsCategory partsCategory = partsIdentifier.getPartsCategory(); if (!this.partsCategory.equals(partsCategory)) { return; } ArrayList rowModelList = ((PartsSelectListModel) partsSelectTable.getModel()).getRowModelList(); int mx = rowModelList.size(); for (int idx = 0; idx < mx; idx++) { PartsSelectRow partsSelectRow = rowModelList.get(idx); if (partsSelectRow.getPartsIdentifier().equals(partsIdentifier)) { partsSelectTable.getSelectionModel().setSelectionInterval(idx, idx); Rectangle rct = partsSelectTable.getCellRect(idx, 0, true); partsSelectTable.scrollRectToVisible(rct); partsSelectTable.requestFocus(); return; } } } /** * フォーカスのあるアイテムを1つ上に移動します. */ protected void onUp() { int selRow = partsSelectTable.getSelectedRow(); if (selRow < 0) { return; } if (selRow > 0) { ArrayList rowModelList = ((PartsSelectListModel) partsSelectTable.getModel()).getRowModelList(); PartsSelectRow rowModel = rowModelList.get(selRow); rowModelList.remove(selRow); rowModelList.add(selRow - 1, rowModel); partsSelectTable.setRowSelectionInterval(selRow - 1, selRow - 1); Rectangle rct = partsSelectTable.getCellRect(selRow - 1, 0, true); partsSelectTable.scrollRectToVisible(rct); onChange(new ImageSelectPanelEvent(this)); } partsSelectTable.repaint(); partsSelectTable.requestFocus(); } /** * フォーカスのあるアイテムを1つ下に移動します. */ protected void onDown() { int selRow = partsSelectTable.getSelectedRow(); if (selRow < 0) { return; } int mx = partsSelectTable.getRowCount(); if (selRow < mx - 1) { ArrayList rowModelList = ((PartsSelectListModel) partsSelectTable.getModel()).getRowModelList(); PartsSelectRow rowModel = rowModelList.get(selRow); rowModelList.remove(selRow); rowModelList.add(selRow + 1, rowModel); partsSelectTable.setRowSelectionInterval(selRow + 1, selRow + 1); Rectangle rct = partsSelectTable.getCellRect(selRow + 1, 0, true); partsSelectTable.scrollRectToVisible(rct); onChange(new ImageSelectPanelEvent(this)); } partsSelectTable.repaint(); partsSelectTable.requestFocus(); } /** * 選択中のアイテムを選択順序を維持したまま上側に、それ以外は名前順で下側に集めるようにソートします.
*/ protected void onSort() { if (partsSelectTable.getRowCount() > 0) { partsSelectTableModel.sort(); partsSelectTable.setRowSelectionInterval(0, 0); Rectangle rct = partsSelectTable.getCellRect(0, 0, true); partsSelectTable.scrollRectToVisible(rct); partsSelectTable.repaint(); } partsSelectTable.requestFocus(); } /** * タイトルがクリックされた場合 */ protected void onTitleClick() { ImageSelectPanelEvent event = new ImageSelectPanelEvent(this); for (ImageSelectPanelListener listener : listeners) { listener.onTitleClick(event); } } /** * タイトルがダブルクリックされた場合 */ protected void onTitleDblClick() { ImageSelectPanelEvent event = new ImageSelectPanelEvent(this); for (ImageSelectPanelListener listener : listeners) { listener.onTitleDblClick(event); } } /** * カラー変更ボタンが押下された場合 * @param event */ protected void onChangeColor() { ImageSelectPanelEvent event = new ImageSelectPanelEvent(this); for (ImageSelectPanelListener listener : listeners) { listener.onChangeColor(event); } } /** * 設定ボタンが押下された場合 * @param event */ protected void onPreferences() { ImageSelectPanelEvent event = new ImageSelectPanelEvent(this); for (ImageSelectPanelListener listener : listeners) { listener.onPreferences(event); } } /** * アイテムのチェック状態が変更された場合. * @param event */ protected void onChange(ImageSelectPanelEvent event) { List selectedNews = getSelectedPartsIdentifiers(); if (!selectedNews.equals(selectedPartsIdentifiers)) { selectedPartsIdentifiers = selectedNews; for (ImageSelectPanelListener listener : listeners) { listener.onChange(event); } onSelectChange(event); } } /** * アイテムの選択(フォーカス)が変更された場合. * @param event */ protected void onSelectChange(ImageSelectPanelEvent event) { PartsIdentifier selectedNew = getSelectedPartsIdentifier(); if (!PartsIdentifier.equals(selectedNew, selectedPartsIdentifier)) { selectedPartsIdentifier = selectedNew; for (ImageSelectPanelListener listener : listeners) { listener.onSelectChange(event); } } } /** * 使用中のアイテムの一覧を返す.(選択順)
* @return 使用中のアイテムの一覧.(選択順)、ひとつもなければ空 */ public List getSelectedPartsIdentifiers() { return partsSelectTableModel.getSelectedPartsIdentifiers(); } /** * 使用中のアイテムを返す.
* 複数選択可能である場合は、使用中のアイテムでフォーカスがある最初のアイテムを返す.
* 単一選択の場合は、最初の使用中アイテムを返す.
* 複数選択可能で、使用中のアイテムにひとつもフォーカスがあたってない場合は、 * 最初の使用中アイテムを返す.
* 使用中アイテムがなければnullを返す. * @return 使用中アイテム、もしくはnull */ public PartsIdentifier getSelectedPartsIdentifier() { // フォーカスがあたっていて、且つ、チェック状態のアイテムを上から順に走査し、 // 該当があれば、最初のものを返す. int[] selRows = partsSelectTable.getSelectedRows(); Arrays.sort(selRows); for (int selRow : selRows) { PartsSelectRow row = partsSelectTableModel.getRow(selRow); if (row.isChecked()) { return row.getPartsIdentifier(); } } // チェック状態のアイテムの最初のものを返す. List checkedRows = getSelectedPartsIdentifiers(); if (checkedRows.size() > 0) { return checkedRows.get(0); } // 該当なし return null; } /** * 選択選択パーツカテゴリの選択解除を許可するか?
* @return 許可する場合はtrue */ public boolean isDeselectableSingleCategory() { return partsSelectTableModel.isDeselectableSingleCategory(); } /** * 選択選択パーツカテゴリの選択解除を許可するか設定する.
* @param deselectable 許可する場合はtrue */ public void setDeselectableSingleCategory(boolean deselectable) { partsSelectTableModel.setDeselectableSingleCategory(deselectable); } } /** * リストの行モデル.
* パーツデータ、表示名と使用中フラグを管理する. * @author seraphy */ final class PartsSelectRow implements Serializable, Comparable { private static final long serialVersionUID = 5732273802364827L; private PartsIdentifier partsIdentifier; private boolean checked; private int displayOrder; public PartsSelectRow(final PartsIdentifier partsIdentifier, final boolean checked) { this.partsIdentifier = partsIdentifier; this.checked = checked; } /** * 選択されているものを上、そうでないものを下に振り分ける。 * 選択されているもの同士、選択されていないもの同士は、互いのディスプレイ順でソートされる.
* 選択されているもの同士、選択されていないもの同士で、且つ、同一のディスプレイ順序であればパーツの表示名順でソートされる.
* @param o 対象 * @return 比較結果 */ public int compareTo(PartsSelectRow o) { int ret = (checked == o.checked) ? 0 : (checked ? -1 : 1); if (ret == 0 && checked) { ret = displayOrder - o.displayOrder; } if (ret == 0) { ret = partsIdentifier.compareTo(o.partsIdentifier); } return ret; } public void setDisplayOrder(int displayOrder) { this.displayOrder = displayOrder; } public int getDisplayOrder() { return this.displayOrder; } @Override public boolean equals(Object obj) { if (obj == this) { return true; } if (obj != null && obj instanceof PartsSelectRow) { return this.compareTo((PartsSelectRow) obj) == 0; } return false; } public int hashCode() { return partsIdentifier.hashCode(); } public PartsIdentifier getPartsIdentifier() { return partsIdentifier; } /** * {@link PartsIdentifier#getLocalizedPartsName()}に委譲します. * @return パーツ名 */ public String getPartsName() { return partsIdentifier.getLocalizedPartsName(); } public boolean isChecked() { return checked; } public void setChecked(boolean checked) { this.checked = checked; } } /** * リストのモデル * @author seraphy */ class PartsSelectListModel extends AbstractTableModel { private static final long serialVersionUID = 7604828023134579608L; private PartsCategory partsCategory; private ArrayList partsSelectRowList; /** * カテゴリが複数パーツでない場合でも選択解除を許可するフラグ. */ private boolean deselectableSingleCategory; public PartsSelectListModel(PartsCategory partsCategory) { if (partsCategory == null) { throw new IllegalArgumentException(); } this.partsSelectRowList = new ArrayList(); this.partsCategory = partsCategory; } public void load(Collection partsIdentifiers) { if (partsIdentifiers == null) { throw new IllegalArgumentException(); } // 現在選択されているパーツを保存する HashMap selectedPartsIdentifiers = new HashMap(); for (PartsIdentifier partsIdentifier : getSelectedPartsIdentifiers()) { selectedPartsIdentifiers.put(partsIdentifier, selectedPartsIdentifiers.size()); } // パーツイメージマップからパーツ名を列挙する. ArrayList partsSelectList = new ArrayList(); for (PartsIdentifier partsIdentifier : partsIdentifiers) { Integer selIndex = selectedPartsIdentifiers.get(partsIdentifier); PartsSelectRow rowModel = new PartsSelectRow(partsIdentifier, selIndex != null); // 選択されているものは、選択されているものの順序を維持する.それ以外は名前順でソートされる. int order = (selIndex != null) ? selIndex.intValue() : 0; rowModel.setDisplayOrder(order); partsSelectList.add(rowModel); } if (partsCategory.isMultipleSelectable()) { // パーツを選択有無(順序維持)・名前順に並び替える. Collections.sort(partsSelectList); } else { // 単一選択モード時はパーツ識別子でソートする. Collections.sort(partsSelectList, new Comparator() { public int compare(PartsSelectRow o1, PartsSelectRow o2) { return o1.getPartsIdentifier().compareTo(o2.getPartsIdentifier()); } }); } this.partsSelectRowList = partsSelectList; fireTableDataChanged(); } /** * 選択選択パーツカテゴリの選択解除を許可するか?
* @return 許可する場合はtrue */ public boolean isDeselectableSingleCategory() { return deselectableSingleCategory; } /** * 選択選択パーツカテゴリの選択解除を許可するか設定する.
* @param deselectable 許可する場合はtrue */ public void setDeselectableSingleCategory(boolean deselectable) { this.deselectableSingleCategory = deselectable; } public PartsSelectRow getRow(int rowIndex) { return partsSelectRowList.get(rowIndex); } public ArrayList getRowModelList() { return this.partsSelectRowList; } public int getColumnCount() { // ヘッダは非表示のためヘッダ名は取得する必要なし. // col 0: 選択ボックス // col 1: パーツ表示名 return 2; } public int getRowCount() { return partsSelectRowList.size(); } public Object getValueAt(int rowIndex, int columnIndex) { PartsSelectRow rowModel = partsSelectRowList.get(rowIndex); switch (columnIndex) { case 0: return Boolean.valueOf(rowModel.isChecked()); case 1: return rowModel.getPartsName(); default: } return ""; } @Override public Class getColumnClass(int columnIndex) { switch (columnIndex) { case 0: return Boolean.class; case 1: return String.class; default: } return String.class; } @Override public boolean isCellEditable(int rowIndex, int columnIndex) { if (columnIndex == 0) { return true; } return false; } @Override public void setValueAt(Object aValue, int rowIndex, int columnIndex) { if (columnIndex != 0) { return; } PartsSelectRow rowModel = partsSelectRowList.get(rowIndex); boolean checked = ((Boolean) aValue).booleanValue(); if (!checked && rowModel.isChecked() && !deselectableSingleCategory && !partsCategory.isMultipleSelectable()) { // 複数選択が可能でない場合、現在選択中のチェックは一つしかないはずのため、これを外すことはしない。 // ただし単一選択パーツカテゴリでの選択解除が許可されている場合を除く。 return; } rowModel.setChecked(checked); // カテゴリが複数パーツ選択を許可しておらず、且つ、チェックをつけた場合、 // すでにチェックされている他の パーツ行のチェックを外す必要がある。 boolean unchecked = false; if (checked && !partsCategory.isMultipleSelectable()) { int mx = partsSelectRowList.size(); for (int idx = 0; idx < mx; idx++) { if (idx != rowIndex) { PartsSelectRow otherRow = partsSelectRowList.get(idx); if (otherRow.isChecked()) { otherRow.setChecked(false); unchecked = true; } } } } if (!unchecked) { // 指定されたセルの変更のみなので単一変更を通知する. fireTableCellUpdated(rowIndex, columnIndex); } else { // 他のセルも変更されたので一括変更を通知する. fireTableDataChanged(); } } /** * 選択されているパーツを上に、それ以外を下に振り分ける.
* それぞれはパーツの表示名順でソートされる.
*/ public void sort() { int mx = partsSelectRowList.size(); for (int idx = 0; idx < mx; idx++) { partsSelectRowList.get(idx).setDisplayOrder(idx); } Collections.sort(partsSelectRowList); fireTableDataChanged(); } /** * チェックされているパーツのパーツ識別子のリストを返す.
* リストの順序はパーツの表示されている順序と等しい.
* 選択がなければ空のリストが返される. * @return チェックされているパーツのパーツ識別子のリスト、もしくは空 */ public List getSelectedPartsIdentifiers() { ArrayList selectedRows = new ArrayList(); for (PartsSelectRow rowModel : partsSelectRowList) { if (rowModel.isChecked()) { selectedRows.add(rowModel.getPartsIdentifier()); } } return selectedRows; } /** * 指定したインデックスのパーツのチェック状態を返す. * @param rowIndexes 調べるインデックスの配列 * @return 引数に対応したインデックスのチェック状態、nullまたは空の場合は空を返す */ public boolean[] getChecks(int[] rowIndexes) { if (rowIndexes == null) { rowIndexes = new int[0]; } int mx = rowIndexes.length; boolean[] results = new boolean[mx]; for (int idx = 0; idx < mx; idx++) { int rowIndex = rowIndexes[idx]; PartsSelectRow row = partsSelectRowList.get(rowIndex); results[idx] = row.isChecked(); } return results; } /** * 指定したインデックスのチェック状態を設定する. * @param checked チェックする場合はtrue、チェックを解除する場合はfalse * @param selectedRows インデックスの配列、nullまたは空の場合は何もしない. */ public void setChecks(boolean checked, int[] selectedRows) { if (selectedRows == null || selectedRows.length == 0) { return; } ArrayList affectRows = new ArrayList(); if (!checked) { // 選択解除 if (!partsCategory.isMultipleSelectable()) { // 複数選択可能でない場合、選択はひとつしかないはずなので // クリアする必要はない。 return; } // 選択を解除する. for (int selRow : selectedRows) { PartsSelectRow row = partsSelectRowList.get(selRow); if (row.isChecked()) { row.setChecked(false); affectRows.add(selRow); } } } else { // 選択 if (partsCategory.isMultipleSelectable()) { // 複数選択可能であれば単純に選択を有効にする for (int selRow : selectedRows) { PartsSelectRow row = partsSelectRowList.get(selRow); if (!row.isChecked()) { row.setChecked(true); affectRows.add(selRow); } } } else { // 複数選択可能でない場合は最初のアイテムのみをチェックをつけ、 // それ以外のチェックを外す. int selRow = selectedRows[0]; PartsSelectRow row = partsSelectRowList.get(selRow); if (!row.isChecked()) { row.setChecked(true); affectRows.add(selRow); int mx = partsSelectRowList.size(); for (int idx = 0; idx < mx; idx++) { PartsSelectRow otherRow = partsSelectRowList.get(idx); if (idx != selRow) { if (otherRow.isChecked()) { otherRow.setChecked(false); affectRows.add(idx); } } } } } } if (affectRows.isEmpty()) { // なにも変わりないのでイベントも発生しない. return; } // 変更された最初の行から最後の行までの範囲で変更を通知する. // (変更されていない中間も含まれる) int minIdx = 0; int maxIdx = 0; for (int idx : affectRows) { minIdx = Math.min(minIdx, idx); maxIdx = Math.max(maxIdx, idx); } fireTableRowsUpdated(minIdx, maxIdx); } } charactermanaj/src/charactermanaj/ui/RecentCharactersDir.java0000644000175000017500000000463611767047542024650 0ustar paulliupaulliupackage charactermanaj.ui; import java.io.File; import java.io.IOException; import java.io.Serializable; import java.util.ArrayList; import java.util.Iterator; import charactermanaj.util.UserData; import charactermanaj.util.UserDataFactory; /** * 最後に使用したキャラクターデータディレクトリと、その履歴情報.
* @author seraphy */ public class RecentCharactersDir implements Serializable { private static final long serialVersionUID = -5274310741380875405L; /** * ファイル名 */ public static final String FILENAME = "recent-characterdirs.ser"; /** * 最後に使用したディレクトリ */ private File lastUseCharacterDir; /** * 過去に使用したディレクトリ情報 */ private ArrayList recentCharacterDirs = new ArrayList(); /** * ディレクトリの問い合わせ不要フラグ. */ private boolean doNotAskAgain; public ArrayList getRecentCharacterDirs() { return recentCharacterDirs; } public void setLastUseCharacterDir(File lastUseCharacterDir) { this.lastUseCharacterDir = lastUseCharacterDir; } public File getLastUseCharacterDir() { return lastUseCharacterDir; } public void clrar() { doNotAskAgain = false; lastUseCharacterDir = null; recentCharacterDirs.clear(); } public boolean isDoNotAskAgain() { return doNotAskAgain; } public void setDoNotAskAgain(boolean doNotAskAgain) { this.doNotAskAgain = doNotAskAgain; } public static RecentCharactersDir load() throws IOException { UserDataFactory factory = UserDataFactory.getInstance(); UserData recentCharDirs = factory.getUserData(FILENAME); if (recentCharDirs.exists()) { return (RecentCharactersDir) recentCharDirs.load(); } return null; } public void saveRecents() throws IOException { if (lastUseCharacterDir != null) { // 既存のリストに現在の選択があれば、一旦削除する. Iterator ite = recentCharacterDirs.iterator(); while (ite.hasNext()) { File file = ite.next(); if (lastUseCharacterDir.equals(file)) { ite.remove(); } } // 現在の選択を先頭にする. recentCharacterDirs.add(0, lastUseCharacterDir); } UserDataFactory factory = UserDataFactory.getInstance(); UserData recentCharDirs = factory.getUserData(FILENAME); recentCharDirs.save(this); } }charactermanaj/src/charactermanaj/ui/ColorBox.java0000644000175000017500000001616611767047542022521 0ustar paulliupaulliupackage charactermanaj.ui; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Dimension; import java.awt.Graphics; import java.awt.Insets; import java.awt.Rectangle; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.util.Properties; import javax.swing.AbstractAction; import javax.swing.BorderFactory; import javax.swing.JButton; import javax.swing.JColorChooser; import javax.swing.JPanel; import javax.swing.border.BevelBorder; import javax.swing.event.EventListenerList; import charactermanaj.util.LocalizedResourcePropertyLoader; /** * 色表示ボックス.
* 色が変更された場合はプロパティ「colorKey」に対するプロパティ変更リスナへの通知が行われます.
* ダブルクリックまたはボタンが押下され色を指定したことによるアクションリスナへの通知が行われます.
* 既定のコマンドは「colorKey」です.
* アクションは色選択ダイアログがOKされたことによるアクションであり、色が前後で変更されなくても通知されます.
* @author seraphy */ public class ColorBox extends JPanel { private static final long serialVersionUID = -8745278154296281466L; /** * リソース */ protected static final String STRINGS_RESOURCE = "languages/colorbox"; /** * コマンド */ private String actionCommand = "colorKey"; /** * 初期カラー */ private Color colorKey; /** * 色の表示パネル */ private JPanel colorDisplayPanel; /** * 色選択アクション */ private AbstractAction actChooseColor; /** * 色ボックスを構築します.
* 色選択ボタンが付与されます.
*/ public ColorBox() { this(null, true); } /** * 初期カラーと、色選択ボックスの表示有無を指定して構築します. * @param colorKey 初期カラー * @param colorPicker 色選択ボタンの表示有無 */ public ColorBox(Color colorKey, boolean colorPicker) { if (colorKey == null) { colorKey = Color.WHITE; } this.colorKey = colorKey; Properties strings = LocalizedResourcePropertyLoader.getInstance() .getLocalizedProperties(STRINGS_RESOURCE); colorDisplayPanel = createColorDiaplyPanel(); colorDisplayPanel.addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { if (actChooseColor.isEnabled() && e.getClickCount() == 2) { onChooseColor(new ActionEvent(this, 1, getActionCommand(), e.getWhen(), e.getModifiers())); } } }); colorDisplayPanel.setBorder(BorderFactory.createCompoundBorder( BorderFactory.createEmptyBorder(0, 0, 0, 3), BorderFactory.createBevelBorder(BevelBorder.LOWERED))); colorDisplayPanel.setPreferredSize(new Dimension(32, 24)); actChooseColor = new AbstractAction(strings.getProperty("btn.chooseColorKey")) { private static final long serialVersionUID = 1L; public void actionPerformed(ActionEvent e) { onChooseColor(e); } }; JButton btnChooseColor = new JButton(actChooseColor); btnChooseColor.setVisible(colorPicker); actChooseColor.setEnabled(colorPicker); setLayout(new BorderLayout()); add(colorDisplayPanel, BorderLayout.CENTER); add(btnChooseColor, BorderLayout.EAST); } protected JPanel createColorDiaplyPanel() { return new JPanel() { private static final long serialVersionUID = -8554046012311330274L; @Override protected void paintComponent(Graphics g) { super.paintComponent(g); Rectangle rct = getBounds(); Insets insets = getInsets(); int x = insets.left; int y = insets.top; int w = rct.width - insets.left - insets.right; int h = rct.height - insets.top - insets.bottom; g.setColor(getColorKey()); g.fillRect(x, y, w, h); } }; } protected JPanel getColorDisplayPanel() { return colorDisplayPanel; } @Override public void setEnabled(boolean enabled) { super.setEnabled(enabled); actChooseColor.setEnabled(enabled); } public void setColorKey(Color colorKey) { if (colorKey == null) { colorKey = Color.WHITE; } if ( !this.colorKey.equals(colorKey)) { Color oldc = this.colorKey; this.colorKey = colorKey; repaint(); firePropertyChange("colorKey", oldc, colorKey); } } /** * Adds an ActionListener to the button. * @param l the ActionListener to be added */ public void addActionListener(ActionListener l) { listenerList.add(ActionListener.class, l); } /** * Removes an ActionListener from the button. * If the listener is the currently set Action * for the button, then the Action * is set to null. * * @param l the listener to be removed */ public void removeActionListener(ActionListener l) { listenerList.remove(ActionListener.class, l); } /** * Notifies all listeners that have registered interest for * notification on this event type. The event instance * is lazily created using the event * parameter. * * @param event the ActionEvent object * @see EventListenerList */ protected void fireActionPerformed(ActionEvent event) { Object[] listeners = listenerList.getListenerList(); ActionEvent e = null; for (int i = listeners.length - 2; i >= 0; i -= 2) { if (listeners[i] == ActionListener.class) { if (e == null) { String actionCommand = event.getActionCommand(); if(actionCommand == null) { actionCommand = getActionCommand(); } e = new ActionEvent(this, ActionEvent.ACTION_PERFORMED, actionCommand, event.getWhen(), event.getModifiers()); } ((ActionListener) listeners[i + 1]).actionPerformed(e); } } } public void setActionCommand(String actionCommand) { this.actionCommand = actionCommand; } public String getActionCommand() { return actionCommand; } public Color getColorKey() { return colorKey; } protected String getColorDialogTitle() { Properties strings = LocalizedResourcePropertyLoader.getInstance() .getLocalizedProperties(STRINGS_RESOURCE); return strings.getProperty("caption.chooseColorKey"); } protected void onChooseColor(ActionEvent e) { Color colorKey = getColorKey(); colorKey = JColorChooser.showDialog( this, getColorDialogTitle(), colorKey); if (colorKey != null) { setColorKey(colorKey); fireActionPerformed(e); } } }charactermanaj/src/charactermanaj/ui/util/0000755000175000017500000000000011767047542021072 5ustar paulliupaulliucharactermanaj/src/charactermanaj/ui/util/ScrollPaneDragScrollSupport.java0000644000175000017500000002515211767047542027356 0ustar paulliupaulliupackage charactermanaj.ui.util; import java.awt.Cursor; import java.awt.Dimension; import java.awt.Point; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.awt.event.MouseMotionListener; import java.awt.event.MouseWheelEvent; import java.awt.event.MouseWheelListener; import javax.swing.JComponent; import javax.swing.JScrollPane; import javax.swing.JViewport; import javax.swing.SwingUtilities; import charactermanaj.model.AppConfig; /** * JScrollPaneでマウスによるドラッグスクロールをサポートするためのヘルパクラス.
* @author seraphy */ public class ScrollPaneDragScrollSupport { /** * 対象となるスクロールペイン */ private JScrollPane scrollPane; /** * ホイールによるスクロールで移動する分割単位.
* 表示されているエリアをn等分割したサイズごとにスクロールする.
*/ private int wheelDivider; /** * JScrollPaneを指定して構築する. * @param scrollPane */ public ScrollPaneDragScrollSupport(JScrollPane scrollPane) { if (scrollPane == null) { throw new IllegalArgumentException(); } this.scrollPane = scrollPane; AppConfig appConfig = AppConfig.getInstance(); wheelDivider = Math.max(2, appConfig.getWheelScrollUnit()); } /** * ドラッグ開始位置を示す.
* スクロールが調整されるたびに新しい座標にセットし直す.
* ドラッグ中であれば非nullとなる.
* ドラッグが完了した場合、もしくはドラッグが開始されていなければnullとなる.
*/ private Point dragPt; /** * ドラッグによるスクロールが可能か?
* 垂直・水平のいずれのスクロールバーがない状況ではドラッグは開始されない.
* @return ドラッグによるスクロールが可能である場合はtrue */ public boolean isDragScrollable() { JViewport vp = scrollPane.getViewport(); Dimension viewSize = vp.getViewSize(); Dimension visibleSize = vp.getExtentSize(); if (viewSize.width <= visibleSize.width && viewSize.height <= visibleSize.height) { // ビューポートにビューが全部表示されていればドラッグは開始されない. return false; } return true; } /** * 現在のドラッグ位置を取得する.
* ドラッグが開始されていなければnullとなる.
* @return ドラッグ位置 */ public Point getDragPt() { return dragPt; } /** * 現在ドラッグ中であるか? * @return ドラッグ中であればtrue */ public boolean isDragging() { return dragPt != null; } /** * カーソルを設定する. * @param cursor カーソル */ protected void setCursor(Cursor cursor) { scrollPane.setCursor(cursor); } /** * ドラッグの開始または終了を行う.
* すでに開始済みで開始要求するか、開始されておらず停止要求した場合は何もしない.
* @param start 開始する場合はtrue、終了する場合はfalse * @param mousePt 開始位置 */ public void drag(boolean start, Point mousePt) { if (start) { if (dragPt == null) { JViewport vp = scrollPane.getViewport(); Dimension viewSize = vp.getViewSize(); Dimension visibleSize = vp.getExtentSize(); if (viewSize.width <= visibleSize.width && viewSize.height <= visibleSize.height) { // ビューポートにビューが全部表示されていればドラッグは開始されない. dragPt = null; return; } // ドラッグ中であることを示す dragPt = mousePt; setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); } } else if (dragPt != null) { // ドラッグ中であれば解除する. // (ドラッグ解除済みであれば何もしない.) dragging(mousePt); setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); dragPt = null; } } /** * マウスによるドラッグによるスクロール.
* 前回位置(初回なら開始位置)との差分からスクロール量を判定する.
* @param mousePt 現在のマウス位置 */ public void dragging(Point mousePt) { if (dragPt == null || mousePt == null) { // 前回値がないか今回値がない場合は何もしない. return; } // 前回座標との差分を求める int diff_x = dragPt.x - mousePt.x; int diff_y = dragPt.y - mousePt.y; scroll(diff_x, diff_y); // 現在位置を記録 dragPt = mousePt; } /** * マウス座標単位で指定したオフセット分スクロールする. * @param diff_x 水平方向スクロール数 * @param diff_y 垂直方向スクロール数 */ public void scroll(int diff_x, int diff_y) { if (diff_x == 0 && diff_y == 0) { return; } JViewport vp = scrollPane.getViewport(); Dimension viewSize = vp.getViewSize(); Dimension visibleSize = vp.getExtentSize(); Point vpt = vp.getViewPosition(); vpt.x += diff_x; if (vpt.x < 0) { vpt.x = 0; } else if (vpt.x + visibleSize.width > viewSize.width) { // はみ出た分を引く vpt.x -= (vpt.x + visibleSize.width - viewSize.width); } vpt.y += diff_y; if (vpt.y < 0) { vpt.y = 0; } else if (vpt.y + visibleSize.height > viewSize.height) { // はみ出た分を引く vpt.y -= (vpt.y + visibleSize.height - viewSize.height); } vp.setViewPosition(vpt); } /** * ホイールによるスクロール量の分割数.
* 表示されている領域に対してn等分割したサイズを * 一回あたりのスクロール量とする.
* @return スクロール量の分割数 */ public int getWheelDivider() { return wheelDivider; } public void setWheelFactor(int wheelDivider) { this.wheelDivider = Math.max(2, wheelDivider); } /** * マウスホイールによる水平・垂直スクロールを行うためのコンビニエスとメソッド.
* シフトキーで水平、それ以外は垂直とする.
* @param e ホイールイベント */ public void scrollByWheel(final MouseWheelEvent e) { if (e == null) { return; } JViewport vp = scrollPane.getViewport(); Dimension visibleSize = vp.getExtentSize(); int rotation = e.getWheelRotation(); int diff_x = 0; int diff_y = 0; if (e.isShiftDown()) { // 水平スクロール int unit = visibleSize.width / getWheelDivider(); diff_x = rotation * unit; } else { // 垂直スクロール int unit = visibleSize.height / getWheelDivider(); diff_y = rotation * unit; } scroll(diff_x, diff_y); } /** * セットアップしたリスナを保存するもの */ private MouseListener mouseListener; /** * セットアップしたリスナを保存するもの */ private MouseMotionListener mouseMotionListener; /** * セットアップしたリスナを保存するもの */ private MouseWheelListener mouseWheelListener; /** * リスナをセットアップしたコンポーネント */ private JComponent installTarget; /** * ドラッグの開始に相応しいボタンプレスであるか判定するためのインターフェイス. * @author seraphy */ public interface DragPridicator { /** * このマウスイベントでドラッグ開始しても良いか? * @param e マウスイベント * @return ドラッグを開始しても良い場合はtrue */ boolean isDraggable(MouseEvent e); } /** * マウスによるドラッグをサポートする、一般的なマウスリスナとマウスモーションリスナをセットアップする.
* すでに登録済みであれば何もしない.
* このメソッドはマウスリスナに特別な処理が必要ない場合に定型的な処理を代行するコンビニエスメソッドです.
* @param comp マウスリスナをセットアップするターゲットのコンポーネント * @param predicator マウスによるドラッグの開始を行うか判定するためのオブジェクト、nullの場合は不問 */ public void installDraggingListener(final JComponent comp, final DragPridicator predicator) { if (comp == null) { throw new IllegalArgumentException(); } if (mouseListener == null) { mouseListener = new MouseAdapter() { @Override public void mousePressed(MouseEvent e) { if (predicator == null || predicator.isDraggable(e)) { Point pt = SwingUtilities.convertPoint( comp, e.getPoint(), scrollPane); drag(true, pt); } } @Override public void mouseReleased(MouseEvent e) { if (predicator == null || predicator.isDraggable(e)) { Point pt = SwingUtilities.convertPoint( comp, e.getPoint(), scrollPane); drag(false, pt); } } }; // リスナを登録する. comp.addMouseListener(mouseListener); } if (mouseMotionListener == null) { mouseMotionListener = new MouseMotionListener() { public void mouseMoved(MouseEvent e) { // 何もしない. } public void mouseDragged(MouseEvent e) { Point pt = SwingUtilities.convertPoint( comp, e.getPoint(), scrollPane); dragging(pt); } }; // リスナを登録する. comp.addMouseMotionListener(mouseMotionListener); } if (mouseWheelListener == null) { mouseWheelListener = new MouseWheelListener() { public void mouseWheelMoved(MouseWheelEvent e) { scrollByWheel(e); e.consume(); } }; // ホイールスクロールのデフォルト設定を解除する. scrollPane.setWheelScrollingEnabled(false); // リスナを登録する. comp.addMouseWheelListener(mouseWheelListener); } installTarget = comp; } /** * セットアップしたリスナを解除する.
* 登録されていない場合は何もしない.
*/ public void uninstallDraggingListener() { if (mouseListener != null && installTarget != null) { installTarget.removeMouseListener(mouseListener); mouseListener = null; } if (mouseMotionListener != null && installTarget != null) { installTarget.removeMouseMotionListener(mouseMotionListener); mouseMotionListener = null; } if (mouseWheelListener != null && installTarget != null) { installTarget.removeMouseWheelListener(mouseWheelListener); mouseWheelListener = null; } } } charactermanaj/src/charactermanaj/ui/util/FileDropTarget.java0000644000175000017500000000764111767047542024620 0ustar paulliupaulliupackage charactermanaj.ui.util; import java.awt.datatransfer.DataFlavor; import java.awt.datatransfer.UnsupportedFlavorException; import java.awt.dnd.DnDConstants; import java.awt.dnd.DropTargetAdapter; import java.awt.dnd.DropTargetDropEvent; import java.io.File; import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; /** * ファイルドロップターゲット.
* Windows/Macと、Linuxの両方のデスクトップのドロップをサポートする. * @author seraphy */ public class FileDropTarget extends DropTargetAdapter { /** * ロガー */ private final Logger logger = Logger.getLogger(getClass().getName()); protected FileDropListener fileDropListener; public FileDropTarget() { this(null); } public FileDropTarget(FileDropListener fileDropListener) { this.fileDropListener = fileDropListener; } public FileDropListener getFileDropListener() { return fileDropListener; } public void setFileDropListener(FileDropListener fileDropListener) { this.fileDropListener = fileDropListener; } protected void onDropFiles(List dropFiles) { if (fileDropListener != null) { if ( !dropFiles.isEmpty()) { fileDropListener.onDropFiles(dropFiles); } } } public void drop(DropTargetDropEvent dtde) { try { // urlListFlavor (RFC 2483 for the text/uri-list format) DataFlavor uriListFlavor; try { uriListFlavor = new DataFlavor("text/uri-list;class=java.lang.String"); } catch (ClassNotFoundException ex) { logger.log(Level.WARNING, "urlListFlavor is not supported.", ex); uriListFlavor = null; } final List dropFiles = new ArrayList(); // ドロップされたものが1つのファイルであれば受け入れる。 for (DataFlavor flavor : dtde.getCurrentDataFlavors()) { logger.log(Level.FINE, "flavor: " + flavor); if (DataFlavor.javaFileListFlavor.equals(flavor)) { dtde.acceptDrop(DnDConstants.ACTION_COPY); @SuppressWarnings({ "unchecked", "rawtypes" }) List files = (List) dtde.getTransferable().getTransferData(DataFlavor.javaFileListFlavor); logger.log(Level.FINER, "DragAndDrop files(javaFileListFlavor)=" + files); dropFiles.addAll(files); break; } if (uriListFlavor != null && uriListFlavor.equals(flavor)) { // LinuxではjavaFileListFlavorではなく、text/uri-listタイプで送信される. dtde.acceptDrop(DnDConstants.ACTION_COPY); String uriList = (String) dtde.getTransferable().getTransferData(uriListFlavor); logger.log(Level.FINER, "DragAndDrop files(text/uri-list)=" + uriList); for (String fileStr : uriList.split("\r\n")) { // RFC2483によると改行コードはCRLF fileStr = fileStr.trim(); if (fileStr.startsWith("#")) { continue; } try { URI uri = new URI(fileStr); File dropFile = new File(uri); dropFiles.add(dropFile); break; } catch (URISyntaxException ex) { logger.log(Level.WARNING, "invalid drop file: " + fileStr, ex); } } } } // 存在しないファイルを除去する. for (Iterator ite = dropFiles.iterator(); ite.hasNext();) { File dropFile = ite.next(); if (dropFile == null || !dropFile.exists()) { ite.remove(); } } // ドロップされたファイルを通知する. onDropFiles(dropFiles); } catch (UnsupportedFlavorException ex) { logger.log(Level.WARNING, "unsipported flovaor." , ex); onException(ex); } catch (IOException ex) { logger.log(Level.WARNING, "drop target failed." , ex); onException(ex); } } protected void onException(Exception ex) { // do nothing. } } charactermanaj/src/charactermanaj/ui/util/FileDropListener.java0000644000175000017500000000063711767047542025155 0ustar paulliupaulliupackage charactermanaj.ui.util; import java.io.File; import java.util.List; /** * ファイルがドロップされたことを通知されるリスナ.
* @author seraphy * */ public interface FileDropListener { /** * ファイルがドロップされたことを通知する. * @param dropFiles ドロップされたファイル */ void onDropFiles(List dropFiles); } charactermanaj/src/charactermanaj/ui/util/SpinnerWheelSupportListener.java0000644000175000017500000000420011767047542027437 0ustar paulliupaulliupackage charactermanaj.ui.util; import java.awt.event.MouseWheelEvent; import java.awt.event.MouseWheelListener; import javax.swing.SpinnerModel; import javax.swing.SpinnerNumberModel; /** * スピナーをホイールによって上下できるようにするためのホイールリスナ. * @author seraphy */ public class SpinnerWheelSupportListener implements MouseWheelListener { /** * 対象となるスピナーのモデル */ protected SpinnerModel model; /** * スピナーのモデルを指定して構築します. * @param model */ public SpinnerWheelSupportListener(SpinnerModel model) { if (model == null) { throw new IllegalArgumentException(); } this.model = model; } /** * ホイールによりスピナーモデルの現在値を上下させる.
* モデルが数値型である場合は範囲チェックをし、その範囲で適用します.
* 数値外であれば適用した結果エラーとなる場合は単に無視します.
* @param e マウスホイールイベント */ public void mouseWheelMoved(MouseWheelEvent e) { int rotate = e.getWheelRotation(); Object nextval = null; if (rotate < 0) { // 上スクロール(up) nextval = model.getNextValue(); } else if (rotate > 0) { // 下スクロール(down) nextval = model.getPreviousValue(); } if (nextval != null) { if (model instanceof SpinnerNumberModel) { SpinnerNumberModel nmodel = (SpinnerNumberModel) model; @SuppressWarnings("unchecked") Comparable max = nmodel.getMaximum(); @SuppressWarnings("unchecked") Comparable min = nmodel.getMinimum(); if (max.compareTo((Number) nextval) < 0) { nextval = null; } else if (min.compareTo((Number) nextval) > 0) { nextval = null; } } try { if (nextval != null) { model.setValue(nextval); } } catch (IllegalArgumentException ex) { // 範囲外になった場合はIllegalArgumentExceptionが発生するが、 // ユーザ操作によるものなので単に無視する } } } } charactermanaj/src/charactermanaj/ui/ProfileEditDialog.java0000644000175000017500000024273711767047542024325 0ustar paulliupaulliupackage charactermanaj.ui; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Component; import java.awt.Container; import java.awt.Dimension; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.Insets; import java.awt.Toolkit; import java.awt.event.ActionEvent; import java.awt.event.KeyEvent; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.net.URI; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Properties; import java.util.UUID; import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.Pattern; import javax.swing.AbstractAction; import javax.swing.Action; import javax.swing.BorderFactory; import javax.swing.Box; import javax.swing.ComboBoxModel; import javax.swing.DefaultCellEditor; import javax.swing.InputMap; import javax.swing.JButton; import javax.swing.JCheckBox; import javax.swing.JComboBox; import javax.swing.JComponent; import javax.swing.JDialog; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JRootPane; import javax.swing.JScrollPane; import javax.swing.JSpinner; import javax.swing.JTabbedPane; import javax.swing.JTable; import javax.swing.JTextArea; import javax.swing.JTextField; import javax.swing.KeyStroke; import javax.swing.ListSelectionModel; import javax.swing.SpinnerNumberModel; import javax.swing.SwingConstants; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; import javax.swing.event.ListDataEvent; import javax.swing.event.ListDataListener; import javax.swing.table.TableCellRenderer; import javax.swing.table.TableColumnModel; import charactermanaj.Main; import charactermanaj.model.AppConfig; import charactermanaj.model.CharacterData; import charactermanaj.model.ColorGroup; import charactermanaj.model.Layer; import charactermanaj.model.PartsCategory; import charactermanaj.model.PartsIdentifier; import charactermanaj.model.PartsSet; import charactermanaj.model.RecommendationURL; import charactermanaj.model.io.CharacterDataPersistent; import charactermanaj.ui.model.AbstractTableModelWithComboBoxModel; import charactermanaj.util.DesktopUtilities; import charactermanaj.util.ErrorMessageHelper; import charactermanaj.util.LocalizedResourcePropertyLoader; public class ProfileEditDialog extends JDialog { private static final long serialVersionUID = 8559918820826437849L; /** * ローカライズ文字列 */ protected static final String STRINGS_RESOURCE = "languages/profileditdialog"; protected static class JTextFieldEx extends JTextField { private static final long serialVersionUID = -8608404290439184405L; private boolean error; @Override public Color getBackground() { if (error) { AppConfig appConfig = AppConfig.getInstance(); return appConfig.getInvalidBgColor(); } return super.getBackground(); } public void setError(boolean error) { if (this.error != error) { this.error = error; repaint(); } } public boolean isError() { return error; } } /** * オリジナルのデータ. */ private CharacterData original; /** * キャラクターデータID */ private JTextFieldEx txtCharacterID; /** * キャラクターデータ Rev */ private JTextFieldEx txtCharacterRev; /** * キャラクターデータ DocBase(読み込み専用) */ private JTextField txtCharacterDocBase; /** * キャラクター名 */ private JTextFieldEx txtCharacterName; /** * イメージ幅 */ private JSpinner txtImageWidth; /** * イメージ高さ */ private JSpinner txtImageHeight; /** * 作者 */ private JTextField txtAuthor; /** * 説明 */ private JTextArea txtDescription; /** * ディレクトリの監視 */ private JCheckBox chkWatchDir; /** * カラーグループのモデル */ private ColorGroupsTableModel colorGroupsTableModel; /** * カテゴリのモデル */ private CategoriesTableModel categoriesTableModel; /** * レイヤーのモデル */ private LayersTableModel layersTableModel; /** * パーツセットのモデル */ private PartssetsTableModel partssetsTableModel; /** * お勧めリンクのモデル */ private RecommendationTableModel recommendationsTableModel; /** * 画面の内容から生成された新しいキャラクターデータ、もしくはnull */ private CharacterData result; /** * OKボタン */ private JButton btnOK; /** * キャラクターデータの編集画面を構築する.
* @param parent 親、もしくはnull * @param original オリジナルのキャラクターデータ(変更されない) */ public ProfileEditDialog(JFrame parent, CharacterData original) { super(parent, true); initDialog(parent, original); } /** * キャラクターデータの編集画面を構築する.
* @param parent 親、もしくはnull * @param original オリジナルのキャラクターデータ(変更されない) */ public ProfileEditDialog(JDialog parent, CharacterData original) { super(parent, true); initDialog(parent, original); } /** * 編集ダイアログを初期化する. * @param origianl 編集もとキャラクター定義 */ private void initDialog(Component parent, CharacterData original) { // 元情報 if (original == null) { throw new IllegalArgumentException(); } this.original = original; // ウィンドウイベントのハンドル setDefaultCloseOperation(JDialog.DO_NOTHING_ON_CLOSE); addWindowListener(new WindowAdapter() { @Override public void windowClosing(WindowEvent e) { onClose(); } }); // 設定 AppConfig appConfig = AppConfig.getInstance(); final Properties strings = LocalizedResourcePropertyLoader .getInstance().getLocalizedProperties(STRINGS_RESOURCE); // タイトル String title; if (original.isValid()) { title = strings.getProperty("title.edit"); } else { title = strings.getProperty("title.new"); } setTitle(title); // OK/CANCEL PANEL JPanel buttonsPanel = new JPanel(); buttonsPanel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 42)); GridBagLayout buttonsPanelLayout = new GridBagLayout(); buttonsPanel.setLayout(buttonsPanelLayout); GridBagConstraints gbc = new GridBagConstraints(); String okCaption; if (original.isValid()) { okCaption = strings.getProperty("button.ok.edit"); } else { okCaption = strings.getProperty("button.ok.new"); } btnOK = new JButton(new AbstractAction(okCaption) { private static final long serialVersionUID = 1L; public void actionPerformed(ActionEvent e) { onOK(); } }); btnOK.setEnabled(false); // 初期状態はディセーブル、updateUIStateで更新する. Action actOpenDir = new AbstractAction(strings.getProperty("button.openDir")) { private static final long serialVersionUID = 1L; public void actionPerformed(ActionEvent e) { onOpenDir(); } }; actOpenDir.setEnabled(original.isValid()); JButton btnOpenDir = new JButton(actOpenDir); Action actCancel = new AbstractAction(strings.getProperty("button.cancel")) { private static final long serialVersionUID = 1L; public void actionPerformed(ActionEvent e) { onClose(); } }; JButton btnCancel = new JButton(actCancel); gbc.gridx = 0; gbc.gridy = 0; gbc.weightx = 0.; gbc.anchor = GridBagConstraints.EAST; gbc.fill = GridBagConstraints.BOTH; buttonsPanel.add(btnOpenDir, gbc); gbc.gridx = 1; gbc.gridy = 0; gbc.weightx = 1.; gbc.anchor = GridBagConstraints.EAST; gbc.fill = GridBagConstraints.BOTH; buttonsPanel.add(Box.createGlue(), gbc); gbc.gridx = Main.isLinuxOrMacOSX() ? 3 : 2; gbc.gridy = 0; gbc.weightx = 0.; buttonsPanel.add(btnOK, gbc); gbc.gridx = Main.isLinuxOrMacOSX() ? 2 : 3; gbc.gridy = 0; buttonsPanel.add(btnCancel, gbc); Container contentPane = getContentPane(); contentPane.setLayout(new BorderLayout()); contentPane.add(buttonsPanel, BorderLayout.SOUTH); // InputMap/ActionMap Toolkit tk = Toolkit.getDefaultToolkit(); JRootPane rootPane = getRootPane(); rootPane.setDefaultButton(btnOK); InputMap im = rootPane.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT); im.put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "closeWindow"); im.put(KeyStroke.getKeyStroke(KeyEvent.VK_W, tk.getMenuShortcutKeyMask()), "closeWindow"); rootPane.getActionMap().put("closeWindow", actCancel); // Main JPanel mainPanel = new JPanel(); GridBagLayout mainPanelLayout = new GridBagLayout(); mainPanel.setLayout(mainPanelLayout); this.txtCharacterID = new JTextFieldEx(); this.txtCharacterRev = new JTextFieldEx(); this.txtCharacterDocBase = new JTextField(); this.txtCharacterID.setEditable(true); this.txtCharacterRev.setEditable(true); this.txtCharacterDocBase.setEditable(false); this.txtCharacterName = new JTextFieldEx(); this.txtImageWidth = new JSpinner(new SpinnerNumberModel(1, 1, 1024, 1)); this.txtImageHeight = new JSpinner(new SpinnerNumberModel(1, 1, 1024, 1)); this.txtAuthor = new JTextField(); this.txtDescription = new JTextArea(); gbc.gridx = 0; gbc.gridy = 0; gbc.gridwidth = 1; gbc.gridheight = 1; gbc.weightx = 0; gbc.weighty = 0; gbc.insets = new Insets(1, 3, 1, 5); gbc.anchor = GridBagConstraints.EAST; gbc.fill = GridBagConstraints.BOTH; mainPanel.add(new JLabel(strings.getProperty("docbase.caption"), SwingConstants.RIGHT), gbc); gbc.gridx = 1; gbc.gridy = 0; gbc.gridwidth = 3; gbc.gridheight = 1; gbc.weightx = 1.; gbc.weighty = 0; gbc.anchor = GridBagConstraints.EAST; gbc.fill = GridBagConstraints.BOTH; mainPanel.add(txtCharacterDocBase, gbc); gbc.gridx = 0; gbc.gridy = 1; gbc.gridwidth = 1; gbc.gridheight = 1; gbc.weightx = 0; gbc.weighty = 0; gbc.insets = new Insets(1, 3, 1, 5); gbc.anchor = GridBagConstraints.EAST; gbc.fill = GridBagConstraints.BOTH; mainPanel.add(new JLabel(strings.getProperty("id.caption"), SwingConstants.RIGHT), gbc); txtCharacterID.setToolTipText(strings.getProperty("id.caption.help")); txtCharacterRev.setToolTipText(strings.getProperty("rev.caption.help")); gbc.gridx = 1; gbc.gridy = 1; gbc.gridwidth = 3; gbc.gridheight = 1; gbc.weightx = 1.; gbc.weighty = 0; gbc.anchor = GridBagConstraints.EAST; gbc.fill = GridBagConstraints.BOTH; mainPanel.add(txtCharacterID, gbc); gbc.gridx = 0; gbc.gridy = 2; gbc.gridwidth = 1; gbc.gridheight = 1; gbc.weightx = 0; gbc.weighty = 0; gbc.insets = new Insets(1, 3, 1, 5); gbc.anchor = GridBagConstraints.EAST; gbc.fill = GridBagConstraints.BOTH; mainPanel.add(new JLabel(strings.getProperty("rev.caption"), SwingConstants.RIGHT), gbc); gbc.gridx = 1; gbc.gridy = 2; gbc.gridwidth = 3; gbc.gridheight = 1; gbc.weightx = 1.; gbc.weighty = 0; gbc.anchor = GridBagConstraints.EAST; gbc.fill = GridBagConstraints.BOTH; mainPanel.add(txtCharacterRev, gbc); gbc.gridx = 0; gbc.gridy = 3; gbc.gridwidth = 1; gbc.gridheight = 1; gbc.weightx = 0; gbc.weighty = 0; gbc.anchor = GridBagConstraints.EAST; gbc.fill = GridBagConstraints.BOTH; mainPanel.add(new JLabel(strings.getProperty("name.caption"), SwingConstants.RIGHT), gbc); gbc.gridx = 1; gbc.gridy = 3; gbc.gridwidth = 3; gbc.gridheight = 1; gbc.weightx = 1.; gbc.weighty = 0; gbc.anchor = GridBagConstraints.EAST; gbc.fill = GridBagConstraints.BOTH; mainPanel.add(txtCharacterName, gbc); gbc.gridx = 0; gbc.gridy = 4; gbc.gridwidth = 1; gbc.gridheight = 1; gbc.weightx = 0; gbc.weighty = 0; gbc.anchor = GridBagConstraints.EAST; gbc.fill = GridBagConstraints.BOTH; mainPanel.add(new JLabel(strings.getProperty("image-width.caption"), SwingConstants.RIGHT), gbc); gbc.gridx = 1; gbc.gridy = 4; gbc.gridwidth = 1; gbc.gridheight = 1; gbc.weightx = 0.5; gbc.weighty = 0; gbc.anchor = GridBagConstraints.EAST; gbc.fill = GridBagConstraints.BOTH; mainPanel.add(txtImageWidth, gbc); gbc.gridx = 2; gbc.gridy = 4; gbc.gridwidth = 1; gbc.gridheight = 1; gbc.weightx = 0; gbc.weighty = 0; gbc.anchor = GridBagConstraints.EAST; gbc.fill = GridBagConstraints.BOTH; mainPanel.add(new JLabel(strings.getProperty("image-height.caption"), JLabel.RIGHT), gbc); gbc.gridx = 3; gbc.gridy = 4; gbc.gridwidth = 1; gbc.gridheight = 1; gbc.weightx = 0.5; gbc.weighty = 0; gbc.anchor = GridBagConstraints.EAST; gbc.fill = GridBagConstraints.BOTH; mainPanel.add(txtImageHeight, gbc); gbc.gridx = 0; gbc.gridy = 5; gbc.gridwidth = 1; gbc.gridheight = 1; gbc.weightx = 0; gbc.weighty = 0; gbc.anchor = GridBagConstraints.EAST; gbc.fill = GridBagConstraints.BOTH; mainPanel.add(new JLabel(strings.getProperty("author.caption"), SwingConstants.RIGHT), gbc); gbc.gridx = 1; gbc.gridy = 5; gbc.gridwidth = 3; gbc.gridheight = 1; gbc.weightx = 1.; gbc.weighty = 0; gbc.anchor = GridBagConstraints.EAST; gbc.fill = GridBagConstraints.BOTH; mainPanel.add(txtAuthor, gbc); gbc.gridx = 0; gbc.gridy = 6; gbc.gridwidth = 1; gbc.gridheight = 1; gbc.weightx = 0; gbc.weighty = 0; gbc.anchor = GridBagConstraints.EAST; gbc.fill = GridBagConstraints.BOTH; mainPanel.add(new JLabel(strings.getProperty("description.caption"), SwingConstants.RIGHT), gbc); gbc.gridx = 1; gbc.gridy = 6; gbc.gridwidth = 3; gbc.gridheight = 2; gbc.weightx = 1.; gbc.weighty = 1.; gbc.anchor = GridBagConstraints.EAST; gbc.fill = GridBagConstraints.BOTH; mainPanel.add(new JScrollPane(txtDescription), gbc); // model this.colorGroupsTableModel = new ColorGroupsTableModel(); this.categoriesTableModel = new CategoriesTableModel(); this.layersTableModel = new LayersTableModel(); this.partssetsTableModel = new PartssetsTableModel(); this.recommendationsTableModel = new RecommendationTableModel(); this.colorGroupsTableModel.setEditable(true); this.categoriesTableModel.setEditable(true); this.layersTableModel.setEditable(true); this.partssetsTableModel.setEditable(true); this.recommendationsTableModel.setEditable(true); // colorGroup JPanel colorGroupPanel = new JPanel(new BorderLayout()); final JTable colorGroupTable = new JTable(colorGroupsTableModel); colorGroupTable.setShowGrid(true); colorGroupTable.setGridColor(appConfig.getGridColor()); colorGroupTable.putClientProperty("terminateEditOnFocusLost", Boolean.TRUE); colorGroupTable.setRowHeight(colorGroupTable.getRowHeight() + 4); colorGroupTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); colorGroupPanel.add(new JScrollPane(colorGroupTable), BorderLayout.CENTER); JPanel colorGroupBtnPanel = new JPanel(); GridBagLayout colorGroupBtnPanelLayout = new GridBagLayout(); colorGroupBtnPanel.setLayout(colorGroupBtnPanelLayout); AbstractAction actColorGroupAdd = new AbstractAction(strings.getProperty("colorgroup.add.caption")) { private static final long serialVersionUID = 1L; public void actionPerformed(ActionEvent e) { colorGroupsTableModel.addNewColorGroup(); } }; AbstractAction actColorGroupDel = new AbstractAction(strings.getProperty("colorgroup.delete.caption")) { private static final long serialVersionUID = 1L; public void actionPerformed(ActionEvent e) { int selRow = colorGroupTable.getSelectedRow(); if (selRow >= 0) { ColorGroupsTableRow colorGroup = colorGroupsTableModel.getRow(selRow); if (layersTableModel.isUsed(colorGroup)) { JOptionPane.showMessageDialog(ProfileEditDialog.this, strings.getProperty("warning.used-colorgroup")); } else { colorGroupsTableModel.removeRow(selRow); } } } }; AbstractAction actColorGroupMoveUp = new AbstractAction(strings.getProperty("colorgroup.moveup.caption")) { private static final long serialVersionUID = 1L; public void actionPerformed(ActionEvent e) { int rowIndex = colorGroupTable.getSelectedRow(); if (rowIndex >= 0) { int newSel = colorGroupsTableModel.moveUp(rowIndex); colorGroupTable.getSelectionModel().setSelectionInterval(newSel, newSel); } } }; AbstractAction actColorGroupMoveDown = new AbstractAction(strings.getProperty("colorgroup.movedown.caption")) { private static final long serialVersionUID = 1L; public void actionPerformed(ActionEvent e) { int rowIndex = colorGroupTable.getSelectedRow(); if (rowIndex >= 0) { int newSel = colorGroupsTableModel.moveDown(rowIndex); colorGroupTable.getSelectionModel().setSelectionInterval(newSel, newSel); } } }; gbc.gridx = 0; gbc.gridy = 0; gbc.gridwidth = 1; gbc.gridheight = 1; gbc.weightx = 0.; gbc.weighty = 0.; gbc.anchor = GridBagConstraints.EAST; gbc.fill = GridBagConstraints.BOTH; colorGroupBtnPanel.add(new JButton(actColorGroupAdd), gbc); gbc.gridx = 0; gbc.gridy = 1; colorGroupBtnPanel.add(new JButton(actColorGroupDel), gbc); gbc.gridx = 0; gbc.gridy = 2; colorGroupBtnPanel.add(new JButton(actColorGroupMoveUp), gbc); gbc.gridx = 0; gbc.gridy = 3; colorGroupBtnPanel.add(new JButton(actColorGroupMoveDown), gbc); gbc.gridx = 0; gbc.gridy = 4; gbc.weighty = 1.; colorGroupBtnPanel.add(Box.createGlue(), gbc); colorGroupPanel.add(colorGroupBtnPanel, BorderLayout.EAST); final Color disabledForeground = appConfig.getDisabledCellForgroundColor(); // categories JPanel categoriesPanel = new JPanel(new BorderLayout()); final JTable categoriesTable = new JTable(categoriesTableModel) { private static final long serialVersionUID = 1L; @Override public Component prepareRenderer(TableCellRenderer renderer, int row, int column) { Component comp = super.prepareRenderer(renderer, row, column); if (comp instanceof JCheckBox) { // BooleanのデフォルトのレンダラーはJCheckBoxを継承したJTable$BooleanRenderer comp.setEnabled(isCellEditable(row, column) && isEnabled()); } if (isCellSelected(row, column)) { comp.setForeground(getSelectionForeground()); comp.setBackground(getSelectionBackground()); } else { // 前景色、ディセーブル時は灰色 Color foregroundColor = getForeground(); comp.setForeground(isEnabled() ? foregroundColor : disabledForeground); comp.setBackground(getBackground()); } return comp; } }; categoriesTable.setShowGrid(true); categoriesTable.setGridColor(appConfig.getGridColor()); categoriesTable.putClientProperty("terminateEditOnFocusLost", Boolean.TRUE); categoriesTable.setRowHeight(categoriesTable.getRowHeight() + 4); categoriesTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); categoriesTable.setAutoResizeMode(JTable.AUTO_RESIZE_OFF); categoriesPanel.add(new JScrollPane(categoriesTable), BorderLayout.CENTER); categoriesTableModel.adjustColumnModel(categoriesTable.getColumnModel()); JPanel categoriesBtnPanel = new JPanel(); GridBagLayout categoryBtnPanelLayout = new GridBagLayout(); categoriesBtnPanel.setLayout(categoryBtnPanelLayout); AbstractAction actCategoryAdd = new AbstractAction(strings.getProperty("categories.add.caption")) { private static final long serialVersionUID = 1L; public void actionPerformed(ActionEvent e) { categoriesTableModel.addCategory(); } }; AbstractAction actCategoryDel = new AbstractAction(strings.getProperty("categories.delete.caption")) { private static final long serialVersionUID = 1L; public void actionPerformed(ActionEvent e) { int selRow = categoriesTable.getSelectedRow(); if (selRow >= 0) { CategoriesTableRow partsCategory = categoriesTableModel.getRow(selRow); if (layersTableModel.isUsed(partsCategory)) { JOptionPane.showMessageDialog(ProfileEditDialog.this, strings.getProperty("warning.used-category")); } else { categoriesTableModel.removeRow(selRow); } } } }; AbstractAction actCategoryMoveUp = new AbstractAction(strings.getProperty("categories.moveup.caption")) { private static final long serialVersionUID = 1L; public void actionPerformed(ActionEvent e) { int rowIndex = categoriesTable.getSelectedRow(); if (rowIndex >= 0) { int newSel = categoriesTableModel.moveUp(rowIndex); categoriesTable.getSelectionModel().setSelectionInterval(newSel, newSel); } } }; AbstractAction actCategoryMoveDown = new AbstractAction(strings.getProperty("categories.movedown.caption")) { private static final long serialVersionUID = 1L; public void actionPerformed(ActionEvent e) { int rowIndex = categoriesTable.getSelectedRow(); if (rowIndex >= 0) { int newSel = categoriesTableModel.moveDown(rowIndex); categoriesTable.getSelectionModel().setSelectionInterval(newSel, newSel); } } }; gbc.gridx = 0; gbc.gridy = 0; gbc.gridwidth = 1; gbc.gridheight = 1; gbc.weightx = 0.; gbc.weighty = 0.; gbc.anchor = GridBagConstraints.EAST; gbc.fill = GridBagConstraints.BOTH; categoriesBtnPanel.add(new JButton(actCategoryAdd), gbc); gbc.gridx = 0; gbc.gridy = 1; categoriesBtnPanel.add(new JButton(actCategoryDel), gbc); gbc.gridx = 0; gbc.gridy = 2; categoriesBtnPanel.add(new JButton(actCategoryMoveUp), gbc); gbc.gridx = 0; gbc.gridy = 3; categoriesBtnPanel.add(new JButton(actCategoryMoveDown), gbc); gbc.gridx = 0; gbc.gridy = 4; gbc.weighty = 1.; categoriesBtnPanel.add(Box.createGlue(), gbc); categoriesPanel.add(categoriesBtnPanel, BorderLayout.EAST); // layers JPanel layersPanel = new JPanel(new BorderLayout()); final Color invalidBgColor = appConfig.getInvalidBgColor(); final JTable layersTable = new JTable(layersTableModel) { private static final long serialVersionUID = 1L; @Override public Component prepareRenderer(TableCellRenderer renderer, int row, int column) { Component comp = super.prepareRenderer(renderer, row, column); if (comp instanceof JCheckBox) { // BooleanのデフォルトのレンダラーはJCheckBoxを継承したJTable$BooleanRenderer comp.setEnabled(isCellEditable(row, column) && isEnabled()); } LayersTableModel model = (LayersTableModel) getModel(); LayersTableRow layer = model.getRow(row); comp.setForeground(getForeground()); comp.setBackground(layer.isValid() ? getBackground() : invalidBgColor); // 前景色、ディセーブル時は灰色 Color foregroundColor = getForeground(); comp.setForeground(isEnabled() ? foregroundColor : disabledForeground); return comp; } }; layersTableModel.adjustColumnModel(layersTable.getColumnModel()); JComboBox colorGroupCombo = new JComboBox( new FirstItemInjectionComboBoxModelWrapper(colorGroupsTableModel, ColorGroupsTableRow.NA)); JComboBox categoriesCombo = new JComboBox(categoriesTableModel); layersTable.setShowGrid(true); layersTable.setGridColor(appConfig.getGridColor()); layersTable.putClientProperty("terminateEditOnFocusLost", Boolean.TRUE); layersTable.setRowHeight(layersTable.getRowHeight() + 4); layersTable.setDefaultEditor(ColorGroupsTableRow.class, new DefaultCellEditor(colorGroupCombo)); layersTable.setDefaultEditor(CategoriesTableRow.class, new DefaultCellEditor(categoriesCombo)); layersTable.setAutoResizeMode(JTable.AUTO_RESIZE_OFF); layersTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); layersPanel.add(new JScrollPane(layersTable), BorderLayout.CENTER); JPanel layersBtnPanel = new JPanel(); GridBagLayout layersBtnPanelLayout = new GridBagLayout(); layersBtnPanel.setLayout(layersBtnPanelLayout); AbstractAction actLayerAdd = new AbstractAction(strings.getProperty("layers.add.caption")) { private static final long serialVersionUID = 1L; public void actionPerformed(ActionEvent e) { layersTableModel.addNewLayer(); } }; AbstractAction actLayerDel = new AbstractAction(strings.getProperty("layers.delete.caption")) { private static final long serialVersionUID = 1L; public void actionPerformed(ActionEvent e) { int selRow = layersTable.getSelectedRow(); if (selRow >= 0) { layersTableModel.removeRow(selRow); } } }; AbstractAction actLayerSort = new AbstractAction(strings.getProperty("layers.sort.caption")) { private static final long serialVersionUID = 1L; public void actionPerformed(ActionEvent e) { layersTableModel.sort(); } }; AbstractAction actLayerMoveUp = new AbstractAction(strings.getProperty("layers.moveup.caption")) { private static final long serialVersionUID = 1L; public void actionPerformed(ActionEvent e) { int rowIndex = layersTable.getSelectedRow(); if (rowIndex >= 0) { int newSel = layersTableModel.moveUp(rowIndex); layersTable.getSelectionModel().setSelectionInterval(newSel, newSel); } } }; AbstractAction actLayerMoveDown = new AbstractAction(strings.getProperty("layers.movedown.caption")) { private static final long serialVersionUID = 1L; public void actionPerformed(ActionEvent e) { int rowIndex = layersTable.getSelectedRow(); if (rowIndex >= 0) { int newSel = layersTableModel.moveDown(rowIndex); layersTable.getSelectionModel().setSelectionInterval(newSel, newSel); } } }; gbc.gridx = 0; gbc.gridy = 0; gbc.gridwidth = 1; gbc.gridheight = 1; gbc.weightx = 0.; gbc.weighty = 0.; gbc.anchor = GridBagConstraints.EAST; gbc.fill = GridBagConstraints.BOTH; layersBtnPanel.add(new JButton(actLayerAdd), gbc); gbc.gridx = 0; gbc.gridy = 1; layersBtnPanel.add(new JButton(actLayerDel), gbc); gbc.gridx = 0; gbc.gridy = 2; layersBtnPanel.add(new JButton(actLayerMoveUp), gbc); gbc.gridx = 0; gbc.gridy = 3; layersBtnPanel.add(new JButton(actLayerMoveDown), gbc); gbc.gridx = 0; gbc.gridy = 4; layersBtnPanel.add(new JButton(actLayerSort), gbc); gbc.gridx = 0; gbc.gridy = 5; gbc.weighty = 1.; layersBtnPanel.add(Box.createGlue(), gbc); layersPanel.add(layersBtnPanel, BorderLayout.EAST); chkWatchDir = new JCheckBox(strings.getProperty("layers.watchdir")); layersPanel.add(chkWatchDir, BorderLayout.SOUTH); // Presets JPanel partssetsPanel = new JPanel(new BorderLayout()); JTable partssetsTable = new JTable(partssetsTableModel) { private static final long serialVersionUID = 1L; @Override public Component prepareRenderer(TableCellRenderer renderer, int row, int column) { Component comp = super.prepareRenderer(renderer, row, column); if (comp instanceof JCheckBox) { // BooleanのデフォルトのレンダラーはJCheckBoxを継承したJTable$BooleanRenderer comp.setEnabled(isCellEditable(row, column) && isEnabled()); } if (isCellSelected(row, column)) { comp.setForeground(getSelectionForeground()); comp.setBackground(getSelectionBackground()); } else { // 前景色、ディセーブル時は灰色 Color foregroundColor = getForeground(); comp.setForeground(isEnabled() ? foregroundColor : disabledForeground); comp.setBackground(getBackground()); } return comp; } }; partssetsTableModel.adjustColumnModel(partssetsTable.getColumnModel()); partssetsTable.setRowHeight(layersTable.getRowHeight() + 4); partssetsTable.setShowGrid(true); partssetsTable.setGridColor(appConfig.getGridColor()); partssetsTable.putClientProperty("terminateEditOnFocusLost", Boolean.TRUE); partssetsTable.setAutoResizeMode(JTable.AUTO_RESIZE_OFF); partssetsTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); partssetsPanel.add(new JScrollPane(partssetsTable), BorderLayout.CENTER); // Recommendations JPanel recommendationsPanel = new JPanel(new BorderLayout()); final JTable recommendationsTable = new JTable(recommendationsTableModel) { private static final long serialVersionUID = 1L; @Override public Component prepareRenderer(TableCellRenderer renderer, int row, int column) { Component comp = super.prepareRenderer(renderer, row, column); if (isCellSelected(row, column)) { comp.setForeground(getSelectionForeground()); comp.setBackground(getSelectionBackground()); } else { // 前景色、ディセーブル時は灰色 Color foregroundColor = getForeground(); comp.setForeground(isEnabled() ? foregroundColor : disabledForeground); comp.setBackground(getBackground()); } return comp; } }; recommendationsTableModel.adjustColumnModel(recommendationsTable.getColumnModel()); recommendationsTable.setRowHeight(layersTable.getRowHeight() + 4); recommendationsTable.setShowGrid(true); recommendationsTable.setGridColor(appConfig.getGridColor()); recommendationsTable.putClientProperty("terminateEditOnFocusLost", Boolean.TRUE); recommendationsTable.setAutoResizeMode(JTable.AUTO_RESIZE_OFF); recommendationsTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); recommendationsPanel.add(new JScrollPane(recommendationsTable), BorderLayout.CENTER); JPanel recommendationsBtnPanel = new JPanel(); GridBagLayout recommendationsBtnPanelLayout = new GridBagLayout(); recommendationsBtnPanel.setLayout(recommendationsBtnPanelLayout); AbstractAction actRecommendationAdd = new AbstractAction(strings.getProperty("recommendations.add.caption")) { private static final long serialVersionUID = 1L; public void actionPerformed(ActionEvent e) { recommendationsTableModel.addNew(); } }; AbstractAction actRecommendationDel = new AbstractAction(strings.getProperty("recommendations.delete.caption")) { private static final long serialVersionUID = 1L; public void actionPerformed(ActionEvent e) { int selRow = recommendationsTable.getSelectedRow(); if (selRow >= 0) { recommendationsTableModel.removeRow(selRow); } } }; AbstractAction actRecommendationMoveUp = new AbstractAction(strings.getProperty("recommendations.moveup.caption")) { private static final long serialVersionUID = 1L; public void actionPerformed(ActionEvent e) { int rowIndex = recommendationsTable.getSelectedRow(); if (rowIndex >= 0) { int newSel = recommendationsTableModel.moveUp(rowIndex); recommendationsTable.getSelectionModel().setSelectionInterval(newSel, newSel); } } }; AbstractAction actRecommendationMoveDown = new AbstractAction(strings.getProperty("recommendations.movedown.caption")) { private static final long serialVersionUID = 1L; public void actionPerformed(ActionEvent e) { int rowIndex = recommendationsTable.getSelectedRow(); if (rowIndex >= 0) { int newSel = recommendationsTableModel.moveDown(rowIndex); recommendationsTable.getSelectionModel().setSelectionInterval(newSel, newSel); } } }; gbc.gridx = 0; gbc.gridy = 0; gbc.gridwidth = 1; gbc.gridheight = 1; gbc.weightx = 0.; gbc.weighty = 0.; gbc.anchor = GridBagConstraints.EAST; gbc.fill = GridBagConstraints.BOTH; recommendationsBtnPanel.add(new JButton(actRecommendationAdd), gbc); gbc.gridx = 0; gbc.gridy = 1; recommendationsBtnPanel.add(new JButton(actRecommendationDel), gbc); gbc.gridx = 0; gbc.gridy = 2; recommendationsBtnPanel.add(new JButton(actRecommendationMoveUp), gbc); gbc.gridx = 0; gbc.gridy = 3; recommendationsBtnPanel.add(new JButton(actRecommendationMoveDown), gbc); gbc.gridx = 0; gbc.gridy = 4; gbc.weighty = 1.; recommendationsBtnPanel.add(Box.createGlue(), gbc); recommendationsPanel.add(recommendationsBtnPanel, BorderLayout.EAST); // データのロード loadCharacterData(original); // レイヤーのカテゴリ使用状態を監視するリスナ関連 final HashMap> usedLayerMap = new HashMap>(); final Runnable resetUsedLayers = new Runnable() { public void run() { usedLayerMap.clear(); } }; layersTableModel.addListDataListener(new ListDataListener() { public void contentsChanged(ListDataEvent e) { resetUsedLayers.run(); } public void intervalAdded(ListDataEvent e) { resetUsedLayers.run(); } public void intervalRemoved(ListDataEvent e) { resetUsedLayers.run(); } }); categoriesTableModel.setUsedCategoryDetector(new CategoriesTableModel.UsedCategoryDetector() { public List getLayers(CategoriesTableRow partsCategory) { if (usedLayerMap.isEmpty()) { int mx = layersTableModel.getRowCount(); for (int idx = 0; idx < mx; idx++) { LayersTableRow layer = layersTableModel.getRow(idx); CategoriesTableRow pc = layer.getPartsCategory(); List usedLayers = usedLayerMap.get(pc); if (usedLayers == null) { usedLayers = new ArrayList(); usedLayerMap.put(pc, usedLayers); } usedLayers.add(layer); } } return usedLayerMap.get(partsCategory); } }); // 生成可能であるかチェックするためのリスナ layersTableModel.addListDataListener(new ListDataListener() { public void contentsChanged(ListDataEvent e) { updateUIState(); layersTable.repaint(); // エラー有無表示を最新の状態で再判定・再描画するため } public void intervalAdded(ListDataEvent e) { updateUIState(); } public void intervalRemoved(ListDataEvent e) { updateUIState(); } }); // キャラクターID/REV/NAMEが変更されたことを通知され、OKボタンを判定するためのリスナ DocumentListener textChangeListener = new DocumentListener() { public void removeUpdate(DocumentEvent e) { updateUIState(); } public void insertUpdate(DocumentEvent e) { updateUIState(); } public void changedUpdate(DocumentEvent e) { updateUIState(); } }; txtCharacterID.getDocument().addDocumentListener(textChangeListener); txtCharacterRev.getDocument().addDocumentListener(textChangeListener); txtCharacterName.getDocument().addDocumentListener(textChangeListener); // TABS JTabbedPane tabbedPane = new JTabbedPane(); tabbedPane.add(strings.getProperty("panel.basicinfomation"), mainPanel); tabbedPane.add(strings.getProperty("panel.colorgroup"), colorGroupPanel); tabbedPane.add(strings.getProperty("panel.categories"), categoriesPanel); tabbedPane.add(strings.getProperty("panel.layers"), layersPanel); tabbedPane.add(strings.getProperty("panel.partssets"), partssetsPanel); tabbedPane.add(strings.getProperty("panel.recommendations"), recommendationsPanel); contentPane.add(tabbedPane, BorderLayout.CENTER); setSize(500, 500); setLocationRelativeTo(parent); updateUIState(); } /** * CharacterDataから画面へ転記する. * @param original オリジナル情報 */ protected void loadCharacterData(CharacterData original) { if (original == null) { throw new IllegalArgumentException(); } colorGroupsTableModel.clear(); categoriesTableModel.clear(); layersTableModel.clear(); partssetsTableModel.clear(); recommendationsTableModel.clear(); // 基本情報 txtCharacterID.setText(original.getId()); txtCharacterRev.setText(original.getRev()); txtCharacterDocBase.setText(original.getDocBase() == null ? "" : original.getDocBase().toString()); txtCharacterName.setText(original.getName()); txtAuthor.setText(original.getAuthor() != null ? original.getAuthor() : ""); txtDescription.setText(original.getDescription() != null ? original.getDescription() : ""); Dimension siz = original.getImageSize(); txtImageWidth.setValue(siz != null ? siz.width : 300); txtImageHeight.setValue(siz != null ? siz.height : 400); // カラーグループ HashMap colorGroupMap = new HashMap(); for (ColorGroup colorGroup : original.getColorGroups()) { if (colorGroup.isEnabled()) { ColorGroupsTableRow mutableColorGroup = ColorGroupsTableRow.valueOf(colorGroup); colorGroupsTableModel.addRow(mutableColorGroup); colorGroupMap.put(colorGroup, mutableColorGroup); } } // カテゴリとレイヤー for (PartsCategory partsCategory : original.getPartsCategories()) { categoriesTableModel.addRow(new CategoriesTableRow(partsCategory)); for (Layer layer : partsCategory.getLayers()) { LayersTableRow editableLayer = new LayersTableRow(); ColorGroupsTableRow mutableColorGroup = colorGroupMap.get(layer.getColorGroup()); if (mutableColorGroup == null) { mutableColorGroup = ColorGroupsTableRow.NA; } editableLayer.setColorGroup(mutableColorGroup); editableLayer.setPartsCategory(new CategoriesTableRow(partsCategory)); editableLayer.setDir(layer.getDir()); editableLayer.setOrder(layer.getOrder()); editableLayer.setLayerId(layer.getId()); editableLayer.setLayerName(layer.getLocalizedName()); layersTableModel.addRow(editableLayer); } } // ディレクトリ監視有無 chkWatchDir.setSelected(original.isWatchDirectory()); // パーツセット for (PartsSet partsSet : original.getPartsSets().values()) { partssetsTableModel.addRow(new PresetsTableRow(partsSet)); } partssetsTableModel.setDefaultPartsSetId(original.getDefaultPartsSetId()); // お勧めリンク List recommendationURLList = original.getRecommendationURLList(); if (recommendationURLList == null) { // キャラクターデータのお勧めリンクがnull(古い形式)の場合は、デフォルトのお勧めリンクで代替する. CharacterDataPersistent persist = CharacterDataPersistent.getInstance(); CharacterData defaultCd = persist.createDefaultCharacterData(); recommendationURLList = defaultCd.getRecommendationURLList(); } if (recommendationURLList != null) { for (RecommendationURL recommendationURL : recommendationURLList) { recommendationsTableModel.addRow(new RecommendationTableRow(recommendationURL)); } } } protected void onOpenDir() { try { URI docBase = original.getDocBase(); if (!DesktopUtilities.browseBaseDir(docBase)) { JOptionPane.showMessageDialog(this, docBase); } } catch (Exception ex) { ErrorMessageHelper.showErrorDialog(this, ex); } } /** * 画面を閉じる場合 */ protected void onClose() { result = null; boolean writable = !original.isValid() || original.canWrite(); // 新規または更新可能 if (writable) { final Properties strings = LocalizedResourcePropertyLoader .getInstance().getLocalizedProperties(STRINGS_RESOURCE); if (JOptionPane.showConfirmDialog(this, strings.get("confirm.close"), strings.getProperty("confirm"), JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE) != JOptionPane.YES_OPTION) { // YESでなければ継続しない. return; } } dispose(); } /** * 画面の状態を更新する */ protected void updateUIState() { boolean valid = isValidData(new ValidationReport() { public void validateReport(JComponent comp, boolean valid) { if (comp != null && comp instanceof JTextFieldEx) { ((JTextFieldEx) comp).setError(!valid); } } }); boolean writable = !original.isValid() || original.canWrite(); // 新規または更新可能 btnOK.setEnabled(valid && writable); } private interface ValidationReport { void validateReport(JComponent comp, boolean valid); } /** * 入力データが有効であるか判定する. * @return 有効であればtrue */ protected boolean isValidData(ValidationReport report) { // ID, REVが英数字であるか判定 Pattern pat = Pattern.compile("\\p{Graph}+"); String id = txtCharacterID.getText().trim(); String rev = txtCharacterRev.getText().trim(); boolean validId = pat.matcher(id).matches(); boolean validRev = pat.matcher(rev).matches(); boolean validName = txtCharacterName.getText().trim().length() > 0; // レイヤーの不備判定 boolean validLayers = true; int cnt = 0; int mx = layersTableModel.getRowCount(); for (int idx = 0; idx < mx; idx++) { LayersTableRow layer = layersTableModel.getRow(idx); if (!layer.isValid()) { // レイヤーに不備がある validLayers = false; break; } cnt++; } if (cnt == 0) { // レイヤーがない validLayers = false; } if (report != null) { report.validateReport(txtCharacterID, validId); report.validateReport(txtCharacterRev, validRev); report.validateReport(txtCharacterName, validName); } return validLayers && validId && validRev && validName; } /** * OKボタン押下 */ protected void onOK() { if ( !isValidData(null)) { // 編集可能でないか、まだ保存可能になっていない場合はビープ音を鳴らして警告 Toolkit tk = Toolkit.getDefaultToolkit(); tk.beep(); return; } // 編集可能であり、且つ、保存可能な状態であれば CharacterData newCd = createCharacterData(); final Properties strings = LocalizedResourcePropertyLoader .getInstance().getLocalizedProperties(STRINGS_RESOURCE); if (original.isValid() && !original.isSameStructure(newCd)) { if (original.getRev().equals(newCd.getRev())) { // 構造が変更されているがREVが変らない場合 int ret = JOptionPane.showConfirmDialog(this, strings.get("confirm.needchangerevision"), strings.getProperty("confirm"), JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.WARNING_MESSAGE); if (ret == JOptionPane.CANCEL_OPTION) { return; } if (ret == JOptionPane.YES_OPTION) { // リビジョンを生成して割り当てる CharacterDataPersistent persist = CharacterDataPersistent.getInstance(); newCd.setRev(persist.generateRev()); } } else if ( !newCd.isUpperCompatibleStructure(original)){ // 上位互換のない構造が変更されていることを通知する. if (JOptionPane.showConfirmDialog(this, strings.get("confirm.changestructre"), strings.getProperty("confirm"), JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE) != JOptionPane.YES_OPTION) { return; } } } result = newCd; dispose(); return; } /** * 画面の情報からキャラクターデータを構築して返します.
* @return キャラクターデータ */ protected CharacterData createCharacterData() { CharacterData cd = new CharacterData(); // オリジナルのDocBaseVを転記する. cd.setDocBase(original.getDocBase()); // ID, REV cd.setId(txtCharacterID.getText().trim()); cd.setRev(txtCharacterRev.getText().trim()); // キャラクターセット名 cd.setName(txtCharacterName.getText().trim()); // 情報 cd.setAuthor(txtAuthor.getText().trim()); cd.setDescription(txtDescription.getText()); // サイズ Dimension imageSize = new Dimension(); imageSize.width = ((Number)(txtImageWidth.getValue())).intValue(); imageSize.height = ((Number)(txtImageHeight.getValue())).intValue(); cd.setImageSize(imageSize); // カラーグループ int mxColorGroup = colorGroupsTableModel.getRowCount(); ArrayList colorGroups = new ArrayList(); for (int idx = 0; idx < mxColorGroup; idx++) { colorGroups.add(colorGroupsTableModel.getRow(idx).convert()); } cd.setColorGroups(colorGroups); // レイヤーの構築 HashMap> layerMap = new HashMap>(); int mxLayer = layersTableModel.getRowCount(); for (int idx = 0; idx < mxLayer; idx++) { LayersTableRow editableLayer = layersTableModel.getRow(idx); Layer layer = editableLayer.toLayer(); CategoriesTableRow partsCategory = editableLayer.getPartsCategory(); if (layer != null && partsCategory != null) { List layers = layerMap.get(partsCategory); if (layers == null) { layers = new ArrayList(); layerMap.put(partsCategory, layers); } layers.add(layer); } } // カテゴリおよびレイヤー ArrayList categories = new ArrayList(); int mxCategory = categoriesTableModel.getRowCount(); for (int idx = 0; idx < mxCategory; idx++) { CategoriesTableRow partsCategory = categoriesTableModel.getRow(idx); List layers = layerMap.get(partsCategory); if (layers != null) { partsCategory.setLayers(layers); categories.add(partsCategory.convert()); } } cd.setPartsCategories(categories.toArray(new PartsCategory[categories.size()])); // ディレクトリの監視 cd.setWatchDirectory(chkWatchDir.isSelected()); // パーツセット情報 int mxPartssets = partssetsTableModel.getRowCount(); for (int idx = 0; idx < mxPartssets; idx++) { PartsSet partsSet = partssetsTableModel.getRow(idx).convert(); cd.addPartsSet(partsSet); } cd.setDefaultPartsSetId(partssetsTableModel.getDefaultPartsSetId()); // お気に入りリンク情報 int mxRecommendations = recommendationsTableModel.getRowCount(); ArrayList recommendationURLList = new ArrayList(); for (int idx = 0; idx < mxRecommendations; idx++) { RecommendationTableRow row = recommendationsTableModel.getRow(idx); String displayName = row.getLocalizedName(); String url = row.getURL(); if ((displayName != null && displayName.trim().length() > 0) && (url != null && url.trim().length() > 0)) { RecommendationURL recommendationURL = new RecommendationURL(); recommendationURL.setDisplayName(displayName.trim()); recommendationURL.setUrl(url.trim()); recommendationURLList.add(recommendationURL); } } CharacterDataPersistent persist = CharacterDataPersistent.getInstance(); CharacterData defaultCd = persist.createDefaultCharacterData(); List defaultRecommendationURLList = defaultCd.getRecommendationURLList(); if (defaultRecommendationURLList != null && defaultRecommendationURLList.equals(recommendationURLList)) { // デフォルトのお勧めリストと内容が同じの場合は、明示的にリストを設定しない. recommendationURLList = null; } cd.setRecommendationURLList(recommendationURLList); return cd; } /** * 結果を取得する. * キャンセルされた場合はnullが返される.
* @return キャラクターデータ、またはnull */ public CharacterData getResult() { return result; } } /** * 編集用カラーグループ * @author seraphy */ class ColorGroupsTableRow { private final String id; private final boolean enabled; private String localizedName; public static final ColorGroupsTableRow NA = new ColorGroupsTableRow("n/a", "", false); public ColorGroupsTableRow(final String id, final String localizedName) { this(id, localizedName, true); } public static ColorGroupsTableRow valueOf(ColorGroup colorGroup) { if (colorGroup == null || !colorGroup.isEnabled()) { return NA; } return new ColorGroupsTableRow(colorGroup.getId(), colorGroup.getLocalizedName(), true); } public ColorGroup convert() { if (!isEnabled()) { return ColorGroup.NA; } return new ColorGroup(getId(), getLocalizedName()); } private ColorGroupsTableRow(final String id, final String localizedName, final boolean enabled) { if (id == null || id.trim().length() == 0) { throw new IllegalArgumentException(); } this.id = id.trim(); this.localizedName = (localizedName == null || localizedName.trim().length() == 0) ? id : localizedName; this.enabled = enabled; } public void setLocalizedName(String localizedName) { if (localizedName == null || localizedName.trim().length() == 0) { throw new IllegalArgumentException(); } if (!enabled) { throw new UnsupportedOperationException("unmodified object."); } this.localizedName = localizedName; } public boolean isEnabled() { return enabled; } public String getId() { return id; } public String getLocalizedName() { return localizedName; } @Override public int hashCode() { return id.hashCode(); } @Override public boolean equals(Object obj) { if (obj == this) { return true; } if (obj != null && obj instanceof ColorGroupsTableRow) { ColorGroupsTableRow o = (ColorGroupsTableRow) obj; return id.equals(o.getId()); } return false; } public static boolean equals(ColorGroupsTableRow v1, ColorGroupsTableRow v2) { if (v1 == v2) { return true; } if (v1 == null || v2 == null) { return false; } return v1.equals(v2); } @Override public String toString() { return getLocalizedName(); } } /** * カラーグループのテーブル編集モデル * @author seraphy */ class ColorGroupsTableModel extends AbstractTableModelWithComboBoxModel { private static final long serialVersionUID = 2952439955567262351L; private static final String[] colorGroupColumnNames; private static final Logger logger = Logger.getLogger(ColorGroupsTableModel.class.getName()); static { final Properties strings = LocalizedResourcePropertyLoader .getInstance().getLocalizedProperties(ProfileEditDialog.STRINGS_RESOURCE); colorGroupColumnNames = new String[] { strings.getProperty("colorgroup.column.colorgroupname"), }; } private int serialCounter = 1; public int getColumnCount() { return colorGroupColumnNames.length; } @Override public String getColumnName(int column) { return colorGroupColumnNames[column]; } public Object getValueAt(int rowIndex, int columnIndex) { ColorGroupsTableRow colorGroup = elements.get(rowIndex); switch (columnIndex) { case 0: return colorGroup.getLocalizedName(); } return "****"; } @Override public void setValueAt(Object aValue, int rowIndex, int columnIndex) { ColorGroupsTableRow colorGroup = elements.get(rowIndex); try { switch (columnIndex) { case 0: String localizedName = (String) aValue; if (localizedName != null && localizedName.trim().length() > 0) { colorGroup.setLocalizedName(localizedName.trim()); } break; default: return; } fireTableCellUpdated(rowIndex, columnIndex); } catch (Exception ex) { logger.log(Level.FINE, "value set failed. (" + rowIndex + ", " + columnIndex + "): " + aValue, ex); // 無視する } } @Override public Class getColumnClass(int columnIndex) { return String.class; } @Override public boolean isCellEditable(int rowIndex, int columnIndex) { return isEditable(); } public void addNewColorGroup() { String id = "cg" + UUID.randomUUID().toString(); String localizedName = "ColorGroup" + (serialCounter++); ColorGroupsTableRow colorGroup = new ColorGroupsTableRow(id, localizedName); addRow(colorGroup); } } /** * カラーグループ用のコンボボックスモデルに対して最初のアイテムとしてN/Aを常に追加するモデルに変換するラップクラス.
* @author seraphy */ class FirstItemInjectionComboBoxModelWrapper implements ComboBoxModel { private ComboBoxModel parent; private T selectedItem; private T firstItem; private LinkedList listDataListeners = new LinkedList(); public FirstItemInjectionComboBoxModelWrapper(ComboBoxModel parent, T firstItem) { if (parent == null || firstItem == null) { throw new IllegalArgumentException(); } this.parent = parent; this.firstItem = firstItem; parent.addListDataListener(new ListDataListener() { public void contentsChanged(ListDataEvent e) { fireListUpdated(convertRowIndex(e)); } public void intervalAdded(ListDataEvent e) { fireListAdded(convertRowIndex(e)); } public void intervalRemoved(ListDataEvent e) { fireListRemoved(convertRowIndex(e)); } /** * 親コンボボックスモデルのインデックスを+1したイベントに変換する. * @param e 元イベント * @return インデックス変換後のイベント */ protected ListDataEvent convertRowIndex(ListDataEvent e) { return new ListDataEvent(e.getSource(), e.getType(), e .getIndex0() + 1, e.getIndex1() + 1); } }); } protected void fireListUpdated(ListDataEvent e) { for (ListDataListener listener : listDataListeners) { listener.contentsChanged(e); } } protected void fireListAdded(ListDataEvent e) { for (ListDataListener listener : listDataListeners) { listener.intervalAdded(e); } } protected void fireListRemoved(ListDataEvent e) { for (ListDataListener listener : listDataListeners) { listener.intervalRemoved(e); } } public Object getSelectedItem() { return selectedItem; } @SuppressWarnings("unchecked") public void setSelectedItem(Object anItem) { selectedItem = (T) anItem; if (!firstItem.equals(anItem)) { parent.setSelectedItem(anItem); } } public void addListDataListener(ListDataListener l) { if (l != null) { listDataListeners.add(l); } } public void removeListDataListener(ListDataListener l) { if (l != null) { listDataListeners.remove(l); } } public Object getElementAt(int index) { if (index == 0) { return firstItem; } return parent.getElementAt(index - 1); } public int getSize() { return parent.getSize() + 1; } } class CategoriesTableRow implements Comparable { /** * 順序 */ private int order; /** * カテゴリ識別名 */ private String categoryId; /** * カテゴリ表示名 */ private String localizedCategoryName; /** * 複数選択可能? */ private boolean multipleSelectable; /** * 表示行数 */ private int visibleRows; /** * レイヤー情報 */ private ArrayList layers = new ArrayList(); /** * カテゴリを構築する.
* @param categoryId カテゴリ識別名 * @param localizedCategoryName カテゴリ表示名 * @param multipleSelectable 複数選択可能? * @param layers レイヤー情報の配列 */ public CategoriesTableRow(final int order, final String categoryId, String localizedCategoryName, boolean multipleSelectable, int visibleRows, Layer[] layers) { if (categoryId == null || categoryId.trim().length() == 0) { throw new IllegalArgumentException(); } if (layers == null) { layers = new Layer[0]; } if (localizedCategoryName == null || localizedCategoryName.trim().length() == 0) { localizedCategoryName = categoryId; } this.order = order; this.categoryId = categoryId.trim(); this.localizedCategoryName = localizedCategoryName.trim(); this.multipleSelectable = multipleSelectable; this.layers.addAll(Arrays.asList(layers)); this.visibleRows = visibleRows; } public CategoriesTableRow(PartsCategory partsCategory) { if (partsCategory == null) { throw new IllegalArgumentException(); } this.order = partsCategory.getOrder(); this.categoryId = partsCategory.getCategoryId(); this.localizedCategoryName = partsCategory.getLocalizedCategoryName(); this.multipleSelectable = partsCategory.isMultipleSelectable(); this.layers.addAll(partsCategory.getLayers()); this.visibleRows = partsCategory.getVisibleRows(); } public PartsCategory convert() { return new PartsCategory(order, categoryId, localizedCategoryName, multipleSelectable, visibleRows, layers .toArray(new Layer[layers.size()])); } public int compareTo(CategoriesTableRow o) { if (o == this) { return 0; } int ret = order - o.order; if (ret == 0) { ret = localizedCategoryName.compareTo(o.localizedCategoryName); } if (ret == 0) { ret = categoryId.compareTo(o.categoryId); } return ret; } @Override public boolean equals(Object obj) { if (obj == this) { return true; } if (obj != null && obj instanceof CategoriesTableRow) { return categoryId.equals(((CategoriesTableRow) obj).getCategoryId()); } return false; } public static boolean equals(CategoriesTableRow o1, CategoriesTableRow o2) { if (o1 == o2) { return true; } if (o1 == null || o2 == null) { return false; } return o1.equals(o2); } /** * 定義順を取得する * @return 定義順 */ public int getOrder() { return order; } /** * 定義順を設定する * @param order 定義順 */ public void setOrder(int order) { this.order = order; } /** * 複数選択可能であるか? * @return 複数選択可能であるか? */ public boolean isMultipleSelectable() { return multipleSelectable; } /** * 複数選択可能であるか設定する * @param multipleSelectable 複数選択可能であればtrue */ public void setMultipleSelectable(boolean multipleSelectable) { this.multipleSelectable = multipleSelectable; } /** * 表示行数を取得する. * @return 表示行数 */ public int getVisibleRows() { return visibleRows; } /** * 表示行数を設定する * @param visibleRows 表示行数 */ public void setVisibleRows(int visibleRows) { this.visibleRows = visibleRows; } /** * このカテゴリに指定したレイヤーが含まれるか検証する. * @param layer レイヤー * @return 含まれる場合はtrue、含まれない場合はfalse */ public boolean hasLayer(Layer layer) { if (layer != null) { for (Layer memberLayer : layers) { if (Layer.equals(memberLayer, layer)) { return true; } } } return false; } /** * レイヤー情報 * @return レイヤー情報 */ public Collection getLayers() { return Collections.unmodifiableCollection(layers); } /** * レイヤー情報 * @param layers */ public void setLayers(Collection layers) { this.layers.clear(); if (layers != null) { this.layers.addAll(layers); } } /** * レイヤーを取得する.
* 該当するレイヤーがなければnull * @param layerId レイヤー名 * @return レイヤーもしくはnull */ public Layer getLayer(String layerId) { if (layerId == null) { return null; } for (Layer layer : layers) { if (layer.getId().equals(layerId)) { return layer; } } return null; } /** * カテゴリ識別名を取得する. * @return カテゴリ識別名 */ public String getCategoryId() { return categoryId; } /** * カテゴリ表示名を取得する. * @return カテゴリ表示名 */ public String getLocalizedCategoryName() { return this.localizedCategoryName; } /** * カテゴリ表示名を設定する. * @param localizedCategoryName カテゴリ表示名 */ public void setLocalizedCategoryName(String localizedCategoryName) { if (localizedCategoryName == null || localizedCategoryName.trim().length() == 0) { throw new IllegalArgumentException(); } this.localizedCategoryName = localizedCategoryName.trim(); } @Override public int hashCode() { return this.categoryId.hashCode(); } @Override public String toString() { return getLocalizedCategoryName(); } } /** * カテゴリのテーブル編集モデル. * @author seraphy * */ class CategoriesTableModel extends AbstractTableModelWithComboBoxModel { private static final long serialVersionUID = 1L; private static final Logger logger = Logger.getLogger(CategoriesTableModel.class.getName()); public interface UsedCategoryDetector { List getLayers(CategoriesTableRow partsCategory); } private static final String[] categoriesColumnName; private static final int[] categoriesColumnWidths; static { final Properties strings = LocalizedResourcePropertyLoader .getInstance().getLocalizedProperties(ProfileEditDialog.STRINGS_RESOURCE); categoriesColumnName = new String[] { strings.getProperty("categories.column.categoryname"), strings.getProperty("categories.column.multipleselectable"), strings.getProperty("categories.column.displayrowcount"), strings.getProperty("categories.column.usedlayers"), }; categoriesColumnWidths = new int[] { Integer.parseInt(strings.getProperty("categories.column.categoryname.width")), Integer.parseInt(strings.getProperty("categories.column.multipleselectable.width")), Integer.parseInt(strings.getProperty("categories.column.displayrowcount.width")), Integer.parseInt(strings.getProperty("categories.column.usedlayers.width")), }; } private int serialCounter = 1; private UsedCategoryDetector usedCategoryDetector; public CategoriesTableModel() { } public void adjustColumnModel(TableColumnModel columnModel) { for (int idx = 0; idx < categoriesColumnWidths.length; idx++) { columnModel.getColumn(idx).setPreferredWidth(categoriesColumnWidths[idx]); } } public void setUsedCategoryDetector( UsedCategoryDetector usedCategoryDetector) { this.usedCategoryDetector = usedCategoryDetector; } public UsedCategoryDetector getUsedCategoryDetector() { return usedCategoryDetector; } public void addCategory() { String id = "cat" + UUID.randomUUID().toString(); String name = "Category" + (serialCounter++); CategoriesTableRow partsCategory = new CategoriesTableRow( serialCounter, id, name, false, 10, null); addRow(partsCategory); } /** * 定義順を振り直す. */ public void reorder() { int mx = elements.size(); for (int idx = 0; idx < mx; idx++) { CategoriesTableRow partsCategory = elements.get(idx); partsCategory.setOrder(idx + 1); } fireTableDataChanged(); } @Override public int moveDown(int rowIndex) { int ret = super.moveDown(rowIndex); reorder(); return ret; } @Override public int moveUp(int rowIndex) { int ret = super.moveUp(rowIndex); reorder(); return ret; } @Override public String getColumnName(int column) { return categoriesColumnName[column]; } public int getColumnCount() { return categoriesColumnName.length; } public Object getValueAt(int rowIndex, int columnIndex) { CategoriesTableRow partsCategory = elements.get(rowIndex); switch (columnIndex) { case 0: return partsCategory.getLocalizedCategoryName(); case 1: return Boolean.valueOf(partsCategory.isMultipleSelectable()); case 2: return partsCategory.getVisibleRows(); case 3: StringBuilder layerNames = new StringBuilder(); List layers = null; if (usedCategoryDetector != null) { layers = usedCategoryDetector.getLayers(partsCategory); } if (layers != null) { for (LayersTableRow layer : layers) { if (layerNames.length() > 0) { layerNames.append(", "); } layerNames.append(layer.getLayerName()); } } return layerNames.toString(); default: return "***"; } } @Override public void setValueAt(Object aValue, int rowIndex, int columnIndex) { CategoriesTableRow partsCategory = elements.get(rowIndex); try { switch (columnIndex) { case 0: partsCategory.setLocalizedCategoryName((String) aValue); break; case 1: partsCategory.setMultipleSelectable(((Boolean) aValue).booleanValue()); break; case 2: partsCategory.setVisibleRows(((Number) aValue).intValue()); break; default: return; } fireTableCellUpdated(rowIndex, columnIndex); } catch (RuntimeException ex) { logger.log(Level.FINE, "value set failed. (" + rowIndex + ", " + columnIndex + "): " + aValue, ex); // 無視する. } } @Override public Class getColumnClass(int columnIndex) { if (columnIndex == 1) { return Boolean.class; } if (columnIndex == 2) { return Integer.class; } return String.class; } @Override public boolean isCellEditable(int rowIndex, int columnIndex) { if (columnIndex >= categoriesColumnName.length - 1) { return false; } return isEditable(); } } /** * レイヤーのテーブル編集モデル * @author seraphy */ class LayersTableModel extends AbstractTableModelWithComboBoxModel { private static final long serialVersionUID = 1L; private static final Logger logger = Logger.getLogger(LayersTableModel.class.getName()); private static final String[] layerColumnNames; private static final int[] layersColumnWidths; static { final Properties strings = LocalizedResourcePropertyLoader .getInstance().getLocalizedProperties(ProfileEditDialog.STRINGS_RESOURCE); layerColumnNames = new String[] { strings.getProperty("layers.column.layername"), strings.getProperty("layers.column.category"), strings.getProperty("layers.column.colorgroup"), strings.getProperty("layers.column.order"), strings.getProperty("layers.column.directory"), }; layersColumnWidths = new int[] { Integer.parseInt(strings.getProperty("layers.column.layername.width")), Integer.parseInt(strings.getProperty("layers.column.category.width")), Integer.parseInt(strings.getProperty("layers.column.colorgroup.width")), Integer.parseInt(strings.getProperty("layers.column.order.width")), Integer.parseInt(strings.getProperty("layers.column.directory.width")), }; } private int serialCounter = 1; public LayersTableModel() { } public void adjustColumnModel(TableColumnModel columnModel) { for (int idx = 0; idx < layersColumnWidths.length; idx++) { columnModel.getColumn(idx).setPreferredWidth(layersColumnWidths[idx]); } } public void addNewLayer() { LayersTableRow layer = new LayersTableRow(); String layerId = "lay" + UUID.randomUUID().toString(); String layerName = "Layer" + (serialCounter++); layer.setLayerId(layerId); layer.setLayerName(layerName); addRow(layer); } public int getColumnCount() { return layerColumnNames.length; } @Override public String getColumnName(int column) { return layerColumnNames[column]; } public Object getValueAt(int rowIndex, int columnIndex) { LayersTableRow layer = elements.get(rowIndex); switch (columnIndex) { case 0: return layer.getLayerName(); case 1: return layer.getPartsCategory(); case 2: return layer.getColorGroup(); case 3: return layer.getOrder(); case 4: return layer.getDir(); default: return null; } } @Override public void setValueAt(Object aValue, int rowIndex, int columnIndex) { LayersTableRow layer = elements.get(rowIndex); try { switch (columnIndex) { case 0: layer.setLayerName((String) aValue); break; case 1: layer.setPartsCategory((CategoriesTableRow) aValue); break; case 2: layer.setColorGroup((ColorGroupsTableRow) aValue); break; case 3: layer.setOrder(((Number) aValue).intValue()); break; case 4: layer.setDir((String) aValue); break; default: return; } fireTableCellUpdated(rowIndex, columnIndex); } catch (Exception ex) { logger.log(Level.FINE, "value set failed. (" + rowIndex + ", " + columnIndex + "): " + aValue, ex); // 無視する. } } @Override public Class getColumnClass(int columnIndex) { switch (columnIndex) { case 0: return String.class; case 1: return CategoriesTableRow.class; case 2: return ColorGroupsTableRow.class; case 3: return Integer.class; case 4: return String.class; default: return String.class; } } @Override public boolean isCellEditable(int rowIndex, int columnIndex) { return isEditable(); } public void sort() { Collections.sort(elements, new Comparator() { public int compare(LayersTableRow o1, LayersTableRow o2) { int ret; CategoriesTableRow p1 = o1.getPartsCategory(); CategoriesTableRow p2 = o2.getPartsCategory(); if (p1 == p2) { ret = 0; } else if (p1 != null && p2 != null) { ret = p1.compareTo(p2); } else if (p1 == null) { ret = -1; } else { ret = 1; } if (ret == 0) { ret = o1.getOrder() - o2.getOrder(); } if (ret == 0) { ret = o1.getLayerId().compareTo(o2.getLayerId()); } return ret; } }); fireTableDataChanged(); } protected boolean isUsed(ColorGroupsTableRow colorGroup) { if (colorGroup != null) { for (LayersTableRow layer : elements) { if (ColorGroupsTableRow.equals(layer.getColorGroup(), colorGroup)) { return true; } } } return false; } protected boolean isUsed(CategoriesTableRow partsCategory) { if (partsCategory != null) { for (LayersTableRow layer : elements) { if (CategoriesTableRow.equals(layer.getPartsCategory(), partsCategory)) { return true; } } } return false; } } /** * レイヤーのテーブル編集モデルで使うレイヤー編集クラス * @author seraphy */ class LayersTableRow { private String layerId; private String layerName; private CategoriesTableRow partsCategory; private ColorGroupsTableRow colorGroup = ColorGroupsTableRow.NA; private int order; private String dir; public LayersTableRow() { super(); } public String getLayerId() { return layerId; } public void setLayerId(String layerId) { if (layerId == null || layerId.trim().length() == 0) { throw new IllegalArgumentException(); } this.layerId = layerId.trim(); } public String getLayerName() { return layerName; } public void setLayerName(String layerName) { if (layerName == null || layerName.trim().length() == 0) { throw new IllegalArgumentException(); } this.layerName = layerName.trim(); } public CategoriesTableRow getPartsCategory() { return partsCategory; } public void setPartsCategory(CategoriesTableRow partsCategory) { this.partsCategory = partsCategory; } public ColorGroupsTableRow getColorGroup() { return colorGroup; } public void setColorGroup(ColorGroupsTableRow colorGroup) { if (colorGroup == null) { throw new IllegalArgumentException(); } this.colorGroup = colorGroup; } public int getOrder() { return order; } public void setOrder(int order) { this.order = order; } public String getDir() { return dir; } public void setDir(String dir) { if (dir == null || dir.trim().length() == 0) { throw new IllegalArgumentException(); } dir = dir.trim(); if (dir.indexOf("/") >= 0 || dir.indexOf("\\") >= 0 || dir.indexOf("..") >= 0 || dir.endsWith(".")) { throw new IllegalArgumentException("not simple name: " + dir); } this.dir = dir; } public boolean isValid() { return layerName != null && layerName.trim().length() > 0 && dir != null && dir.trim().length() > 0 && partsCategory != null && colorGroup != null; } public Layer toLayer() { if (!isValid()) { return null; } ColorGroup colorGroup = getColorGroup().convert(); return new Layer( getLayerId(), getLayerName(), getOrder(), colorGroup, colorGroup.isEnabled(), getDir()); } } /** * パーツセットのテーブルの行編集モデル * @author seraphy */ class PresetsTableRow { private PartsSet partsSet; public PresetsTableRow(PartsSet partsSet) { if (partsSet == null) { throw new IllegalArgumentException(); } this.partsSet = partsSet.clone(); } public String getPartsSetId() { return partsSet.getPartsSetId(); } public String getLocalizedName() { return partsSet.getLocalizedName(); } public void setLocalizedName(String localizedName) { partsSet.setLocalizedName(localizedName); } public boolean isPresetParts() { return partsSet.isPresetParts(); } public void setPresetParts(boolean checked) { partsSet.setPresetParts(checked); } public PartsSet convert() { return partsSet.clone(); } } /** * パーツセットのテーブル編集モデル * @author seraphy */ class PartssetsTableModel extends AbstractTableModelWithComboBoxModel { private static final long serialVersionUID = 1L; private static final Logger logger = Logger.getLogger(PartssetsTableModel.class.getName()); private static final String[] partssetsColumnNames; private static final int[] partssetsColumnWidths; private String defaultPartsSetId; static { final Properties strings = LocalizedResourcePropertyLoader .getInstance().getLocalizedProperties(ProfileEditDialog.STRINGS_RESOURCE); partssetsColumnNames = new String[] { strings.getProperty("partssets.column.default"), strings.getProperty("partssets.column.preset"), strings.getProperty("partssets.column.partssetname"), strings.getProperty("partssets.column.usedpartsname"), }; partssetsColumnWidths = new int[] { Integer.parseInt(strings.getProperty("partssets.column.default.width")), Integer.parseInt(strings.getProperty("partssets.column.preset.width")), Integer.parseInt(strings.getProperty("partssets.column.partssetname.width")), Integer.parseInt(strings.getProperty("partssets.column.usedpartsname.width")), }; } public PartssetsTableModel() { } public void setDefaultPartsSetId(String defaultPartsSetId) { this.defaultPartsSetId = defaultPartsSetId; } public String getDefaultPartsSetId() { return defaultPartsSetId; } public void adjustColumnModel(TableColumnModel columnModel) { for (int idx = 0; idx < partssetsColumnWidths.length; idx++) { columnModel.getColumn(idx).setPreferredWidth(partssetsColumnWidths[idx]); } } public int getColumnCount() { return partssetsColumnNames.length; } @Override public String getColumnName(int column) { return partssetsColumnNames[column]; } public Object getValueAt(int rowIndex, int columnIndex) { PresetsTableRow rowModel = elements.get(rowIndex); switch (columnIndex) { case 0: return rowModel.getPartsSetId().equals(defaultPartsSetId); case 1: return Boolean.valueOf(rowModel.isPresetParts()); case 2: return rowModel.getLocalizedName(); case 3: return getUsedParts(rowModel); default: return null; } } private String getUsedParts(PresetsTableRow rowModel) { StringBuilder buf = new StringBuilder(); PartsSet partsSet = rowModel.convert(); ArrayList categories = new ArrayList(partsSet.keySet()); Collections.sort(categories); for (PartsCategory category : categories) { if (buf.length() > 0) { buf.append(", "); } buf.append("[" + category.getLocalizedCategoryName() + "] "); List partsIdentifiers = partsSet.get(category); if (partsIdentifiers.isEmpty()) { buf.append("empty"); } else { int mx = partsIdentifiers.size(); for (int idx = 0; idx < mx; idx++) { if (idx != 0) { buf.append(", "); } buf.append(partsIdentifiers.get(idx).getLocalizedPartsName()); } } } return buf.toString(); } @Override public void setValueAt(Object aValue, int rowIndex, int columnIndex) { PresetsTableRow rowModel = elements.get(rowIndex); try { switch (columnIndex) { case 0: if (((Boolean) aValue).booleanValue()) { // デフォルトのパーツセットに指定した場合、プリセットもOnとなる。 rowModel.setPresetParts(true); defaultPartsSetId = rowModel.getPartsSetId(); fireTableDataChanged(); return; } break; case 1: if (((Boolean) aValue).booleanValue()) { rowModel.setPresetParts(true); } else { // デフォルトのパーツセットをプリセットから外した場合、 // デフォルトのパーツセットは未設定となる. rowModel.setPresetParts(false); if (rowModel.getPartsSetId().equals(defaultPartsSetId)) { defaultPartsSetId = null; fireTableRowsUpdated(rowIndex, rowIndex); return; } } break; case 2: String localizedName = (String) aValue; if (localizedName != null && localizedName.trim().length() > 0) { rowModel.setLocalizedName(localizedName.trim()); } break; case 3: return; default: return; } fireTableCellUpdated(rowIndex, columnIndex); } catch (Exception ex) { logger.log(Level.FINE, "value set failed. (" + rowIndex + ", " + columnIndex + "): " + aValue, ex); // 無視する. } } @Override public Class getColumnClass(int columnIndex) { switch (columnIndex) { case 0: return Boolean.class; case 1: return Boolean.class; case 2: return String.class; case 3: return String.class; default: } return String.class; } @Override public boolean isCellEditable(int rowIndex, int columnIndex) { switch (columnIndex ) { case 0: return isEditable(); case 1: return isEditable(); case 2: return isEditable(); case 3: return false; default: } return false; } } /** * お勧めリンクのテーブルの行編集モデル * @author seraphy */ class RecommendationTableRow { private RecommendationURL recommendationURL; public RecommendationTableRow(RecommendationURL recommendationURL) { if (recommendationURL == null) { throw new IllegalArgumentException(); } this.recommendationURL = recommendationURL.clone(); } public String getLocalizedName() { return recommendationURL.getDisplayName(); } public void setLocalizedName(String localizedName) { recommendationURL.setDisplayName(localizedName); } public String getURL() { return recommendationURL.getUrl(); } public void setURL(String url) { recommendationURL.setUrl(url); } public RecommendationURL convert() { return recommendationURL.clone(); } } /** * お勧めリンクのテーブル編集モデル * @author seraphy */ class RecommendationTableModel extends AbstractTableModelWithComboBoxModel { private static final long serialVersionUID = 1L; private static final Logger logger = Logger.getLogger(PartssetsTableModel.class.getName()); private static final String[] partssetsColumnNames; private static final int[] partssetsColumnWidths; static { final Properties strings = LocalizedResourcePropertyLoader .getInstance().getLocalizedProperties(ProfileEditDialog.STRINGS_RESOURCE); partssetsColumnNames = new String[] { strings.getProperty("recommendations.column.displayName"), strings.getProperty("recommendations.column.url"), }; partssetsColumnWidths = new int[] { Integer.parseInt(strings.getProperty("recommendations.column.displayName.width")), Integer.parseInt(strings.getProperty("recommendations.column.url.width")), }; } public RecommendationTableModel() { } public void adjustColumnModel(TableColumnModel columnModel) { for (int idx = 0; idx < partssetsColumnWidths.length; idx++) { columnModel.getColumn(idx).setPreferredWidth(partssetsColumnWidths[idx]); } } public void addNew() { addRow(new RecommendationTableRow(new RecommendationURL())); } public int getColumnCount() { return partssetsColumnNames.length; } @Override public String getColumnName(int column) { return partssetsColumnNames[column]; } public Object getValueAt(int rowIndex, int columnIndex) { RecommendationTableRow rowModel = elements.get(rowIndex); switch (columnIndex) { case 0: return rowModel.getLocalizedName(); case 1: return rowModel.getURL(); default: return null; } } @Override public void setValueAt(Object aValue, int rowIndex, int columnIndex) { RecommendationTableRow rowModel = elements.get(rowIndex); try { switch (columnIndex) { case 0: if (aValue != null && ((String) aValue).trim().length() > 0) { rowModel.setLocalizedName((String) aValue); } break; case 1: if (aValue != null && ((String) aValue).trim().length() > 0) { rowModel.setURL((String) aValue); } break; default: return; } fireTableCellUpdated(rowIndex, columnIndex); } catch (Exception ex) { logger.log(Level.FINE, "value set failed. (" + rowIndex + ", " + columnIndex + "): " + aValue, ex); // 無視する. } } @Override public Class getColumnClass(int columnIndex) { switch (columnIndex) { case 0: return String.class; case 1: return String.class; default: } return String.class; } @Override public boolean isCellEditable(int rowIndex, int columnIndex) { switch (columnIndex ) { case 0: return isEditable(); case 1: return isEditable(); default: } return false; } } charactermanaj/src/charactermanaj/ui/UkagakaConvertDialog.java0000644000175000017500000003130411767047542025006 0ustar paulliupaulliupackage charactermanaj.ui; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Container; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.Insets; import java.awt.TextField; import java.awt.Toolkit; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.KeyEvent; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.awt.image.BufferedImage; import java.util.Properties; import javax.swing.AbstractAction; import javax.swing.ActionMap; import javax.swing.BorderFactory; import javax.swing.Box; import javax.swing.BoxLayout; import javax.swing.ButtonGroup; import javax.swing.InputMap; import javax.swing.JButton; import javax.swing.JCheckBox; import javax.swing.JComponent; import javax.swing.JDialog; import javax.swing.JFrame; import javax.swing.JPanel; import javax.swing.JRadioButton; import javax.swing.JRootPane; import javax.swing.JScrollPane; import javax.swing.KeyStroke; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import charactermanaj.Main; import charactermanaj.graphics.io.UkagakaImageConverter; import charactermanaj.util.LocalizedResourcePropertyLoader; /** * 伺か用PNG/PNA出力設定ダイアログ * @author seraphy */ public class UkagakaConvertDialog extends JDialog { private static final long serialVersionUID = 4189631881766588004L; /** * リソース */ protected static final String STRINGS_RESOURCE = "languages/ukagakaConvertDialog"; /** * キャプション */ private TextField caption = new TextField(); /** * キャンセル */ private AbstractAction actCancel; /** * 保存(デフォルトアクション) */ private AbstractAction actSave; /** * プレビュー(PNG) */ private SamplePicturePanel opaqueImagePanel = new SamplePicturePanel(); /** * プレビュー(PNA) */ private SamplePicturePanel alphaImagePanel = new SamplePicturePanel(); /** * 透過色キー表示ボックス */ private ColorBox colorBox = new ColorBox(); /** * エクスポート対象の元イメージ. */ private BufferedImage originalImage; /** * 透過色をマニュアルとするか? */ private boolean manualTransparentColorKey; /** * 透過キー自動モード */ private JRadioButton radioAuto; /** * 透過キー手動選択モード */ private JRadioButton radioManual; /** * 上書きモードチェックボックス */ private JCheckBox chkOverwriteOption; /** * 終了コード */ private Object result; /** * 保存ボタンアクションリスナ.
*/ private ActionListener saveActionListener; /** * 上書きオプションの表示フラグ */ private boolean showOverwriteOption; /** * 伺か用PNG/PNA出力設定ダイアログを構築する. * @param parent 親フレーム */ public UkagakaConvertDialog(JFrame parent) { this(parent, false); } /** * 伺か用PNG/PNA出力設定ダイアログを構築する. * @param parent 親フレーム * @param overwriteOption 上書きオプションの表示 */ public UkagakaConvertDialog(JFrame parent, boolean overwriteOption) { this(parent, null, overwriteOption); } /** * 伺か用PNG/PNA出力設定ダイアログを構築する. * @param parent 親フレーム * @param saveActionListener 保存ボタンアクション * @param overwriteOption 上書きオプションの表示 */ public UkagakaConvertDialog(JFrame parent, ActionListener saveActionListener, boolean overwriteOption) { super(parent, true); this.saveActionListener = saveActionListener; this.showOverwriteOption = overwriteOption; addWindowListener(new WindowAdapter() { @Override public void windowClosing(WindowEvent e) { onClose(); } @Override public void windowOpened(WindowEvent e) { result = null; } }); try { initComponent(); } catch (RuntimeException ex) { dispose(); throw ex; } } /** * コンポーネントの初期化 */ private void initComponent() { Properties strings = LocalizedResourcePropertyLoader.getInstance() .getLocalizedProperties(STRINGS_RESOURCE); setTitle(strings.getProperty("title")); Toolkit tk = Toolkit.getDefaultToolkit(); actCancel = new AbstractAction(strings.getProperty("btn.cancel")) { private static final long serialVersionUID = -1L; public void actionPerformed(ActionEvent e) { onClose(); } }; actSave = new AbstractAction(strings.getProperty("btn.save")) { private static final long serialVersionUID = -1L; public void actionPerformed(ActionEvent e) { onSave(); } }; JButton btnCancel = new JButton(actCancel); JButton btnSave = new JButton(actSave); Container contentPane = getContentPane(); contentPane.setLayout(new BorderLayout(3, 3)); contentPane.add(caption, BorderLayout.NORTH); caption.setEditable(false); caption.setVisible(false); JScrollPane opaqueSp = new JScrollPane(opaqueImagePanel); JScrollPane alphaSp = new JScrollPane(alphaImagePanel); JPanel previewSpPane = new JPanel(); BoxLayout boxlayout = new BoxLayout(previewSpPane, BoxLayout.LINE_AXIS); previewSpPane.setLayout(boxlayout); previewSpPane.add(opaqueSp); previewSpPane.add(Box.createHorizontalStrut(3)); previewSpPane.add(alphaSp); previewSpPane.setBorder(BorderFactory.createCompoundBorder( BorderFactory.createTitledBorder(strings.getProperty("preview")), BorderFactory.createEmptyBorder(3, 3, 3, 3) )); JPanel centerPane = new JPanel(new BorderLayout()); centerPane.add(previewSpPane, BorderLayout.CENTER); JPanel transparentColorPanel = new JPanel(); GridBagLayout tc_gbl = new GridBagLayout(); transparentColorPanel.setLayout(tc_gbl); transparentColorPanel.setBorder(BorderFactory.createCompoundBorder( BorderFactory.createTitledBorder(strings.getProperty("caption.chooseTransparentColorKey")), BorderFactory.createEmptyBorder(3, 3, 3, 3) )); centerPane.add(transparentColorPanel, BorderLayout.SOUTH); GridBagConstraints gbc = new GridBagConstraints(); radioAuto = new JRadioButton(strings.getProperty("radio.auto")); radioAuto.addChangeListener(new ChangeListener() { public void stateChanged(ChangeEvent e) { onChange(!radioAuto.isSelected()); } }); radioManual = new JRadioButton(strings.getProperty("radio.manual")); radioAuto.addChangeListener(new ChangeListener() { public void stateChanged(ChangeEvent e) { onChange(!radioAuto.isSelected()); } }); ButtonGroup btngroup = new ButtonGroup(); btngroup.add(radioAuto); btngroup.add(radioManual); radioAuto.setSelected(!manualTransparentColorKey); gbc.gridx = 0; gbc.gridy = 0; gbc.gridheight = 1; gbc.gridwidth = 1; gbc.weightx = 0.; gbc.weighty = 0.; gbc.insets = new Insets(3, 3, 3, 3); gbc.anchor = GridBagConstraints.WEST; gbc.fill = GridBagConstraints.BOTH; transparentColorPanel.add(radioAuto, gbc); gbc.gridx = 1; gbc.gridy = 0; transparentColorPanel.add(radioManual, gbc); gbc.gridx = 2; gbc.gridy = 0; transparentColorPanel.add(colorBox, gbc); contentPane.add(centerPane, BorderLayout.CENTER); JPanel btnPanel = new JPanel(); GridBagLayout gbl = new GridBagLayout(); btnPanel.setLayout(gbl); gbc.gridx = 0; gbc.gridy = 0; gbc.gridheight = 1; gbc.gridwidth = 1; gbc.weightx = 1.; gbc.weighty = 0.; gbc.insets = new Insets(3, 3, 3, 3); gbc.anchor = GridBagConstraints.WEST; gbc.fill = GridBagConstraints.BOTH; chkOverwriteOption = new JCheckBox(strings.getProperty("chk.overwriteOriginalFile")); if (showOverwriteOption) { btnPanel.add(chkOverwriteOption, gbc); } else { btnPanel.add(Box.createHorizontalGlue(), gbc); } gbc.gridx = Main.isLinuxOrMacOSX() ? 2 : 1; gbc.gridy = 0; gbc.weightx = 0.; btnPanel.add(btnSave, gbc); gbc.gridx = Main.isLinuxOrMacOSX() ? 1 : 2; gbc.gridy = 0; btnPanel.add(btnCancel, gbc); btnPanel.setBorder(BorderFactory.createEmptyBorder(3, 3, 3, 45)); contentPane.add(btnPanel, BorderLayout.SOUTH); JRootPane rootPane = getRootPane(); InputMap im = rootPane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW); ActionMap am = rootPane.getActionMap(); im.put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "closeExportWizDialog"); im.put(KeyStroke.getKeyStroke(KeyEvent.VK_W, tk.getMenuShortcutKeyMask()), "closeExportWizDialog"); am.put("closeExportWizDialog", actCancel); rootPane.setDefaultButton(btnSave); rootPane.setBorder(BorderFactory.createEmptyBorder(3, 5, 3, 5)); setSize(450, 450); setLocationRelativeTo(getParent()); // colorBoxの色変更イベントのハンドル colorBox.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { onChooseTransparentColorKey(); } }); } /** * 出力するイメージを設定する. * @param img イメージ(TYPE_INT_ARGBのみ) * @param colorKey 透過色に指定するカラーキー(候補)、nullの場合はデフォルト */ public void setExportImage(BufferedImage img, Color colorKey) { if (img == null) { throw new IllegalArgumentException(); } if (img.getType() != BufferedImage.TYPE_INT_ARGB) { throw new IllegalArgumentException("TYPE_INT_ARGB以外は指定できません。"); } if (colorKey == null) { colorKey = Color.GREEN; } this.originalImage = img; colorBox.setColorKey(colorKey); rebuildImage(); } public BufferedImage getOpaqueImage() { return opaqueImagePanel.getSamplePictrue(); } public BufferedImage getAlphaImage() { return alphaImagePanel.getSamplePictrue(); } public void setAutoTransparentColor(boolean mode) { if (mode) { radioAuto.setSelected(true); radioManual.setSelected(false); } else { radioManual.setSelected(true); radioAuto.setSelected(false); } } public Color getTransparentColorKey() { return colorBox.getColorKey(); } public void setTransparentColorKey(Color colorKey) { colorBox.setColorKey(colorKey); } public boolean isAutoTransparentColor() { return radioAuto.isSelected(); } public boolean isOverwriteOriginalFile() { return chkOverwriteOption.isSelected(); } public void setOverwriteOriginalFile(boolean overwriteOriginalFile) { chkOverwriteOption.setSelected(overwriteOriginalFile); } protected void onClose() { result = null; dispose(); } protected void onSave() { if (saveActionListener != null) { ActionEvent e = new ActionEvent(this, 0, "save"); saveActionListener.actionPerformed(e); } } public void setSaveActionListener(ActionListener saveActionListener) { this.saveActionListener = saveActionListener; } public ActionListener getSaveActionListener() { return saveActionListener; } public Object getResult() { return result; } public void setResult(Object result) { this.result = result; } public void setCaption(String text) { if (text == null || text.length() == 0) { caption.setText(""); caption.setVisible(false); } else { caption.setText(text); caption.setVisible(true); } } public String getCaption() { return caption.getText(); } /** * 透過色のマニュアル選択.
*/ protected void onChooseTransparentColorKey() { // モードを手動に切り替え setAutoTransparentColor(false); // プレビューを再構築 rebuildImage(); } /** * 伺か用PNGの透過色キーの自動・マニュアルの切り替えイベント.
* @param modeManual */ protected void onChange(boolean modeManual) { if (manualTransparentColorKey != modeManual) { manualTransparentColorKey = modeManual; rebuildImage(); } } /** * 伺か用のPNG/PNA画像を生成してプレビューに設定します.
*/ protected void rebuildImage() { if (originalImage == null) { return; } UkagakaImageConverter conv = UkagakaImageConverter.getInstance(); BufferedImage pna = conv.createUkagakaPNA(originalImage); Color transparentColorKey = null; if (manualTransparentColorKey) { transparentColorKey = colorBox.getColorKey(); } else { transparentColorKey = conv.detectTransparentColorKey(originalImage); } BufferedImage png = conv.createUkagakaPNG(originalImage, transparentColorKey); opaqueImagePanel.setSamplePicture(png); alphaImagePanel.setSamplePicture(pna); } } charactermanaj/src/charactermanaj/ui/MenuBuilder.java0000644000175000017500000001515611767047542023203 0ustar paulliupaulliupackage charactermanaj.ui; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.RenderingHints; import java.util.HashMap; import java.util.Properties; import javax.swing.JCheckBoxMenuItem; import javax.swing.JMenu; import javax.swing.JMenuBar; import javax.swing.JMenuItem; import javax.swing.JSeparator; import charactermanaj.util.LocalizedResourcePropertyLoader; /** * メニューを構築します. * @author seraphy */ public class MenuBuilder { /** * メニュー項目のリソース */ protected static final String MENU_STRINGS_RESOURCE = "menu/menu"; /** * アンチエイリアスの設定が必要か? */ private static final boolean needAntiAlias = isNeedAntialias(); /** * メニュー項目のアンチエイリアスが必要か判定する.
* java.specification.versionが1.5で始まる場合は必要とみなす.
* @return アンチエイリアスが必要であればtrue */ private static boolean isNeedAntialias() { return System.getProperty("java.specification.version").startsWith("1.5"); } /** * 生成したメニューのマップ */ private final HashMap menuMap = new HashMap(); /** * 生成したメニュー項目のマップ */ private final HashMap menuItemMap = new HashMap(); /** * 生成されたメニューを名前を指定して取得します.
* 存在しない場合は実行時例外が発生します.
* @param name メニュー名 * @return メニュー */ public JMenu getJMenu(String name) { JMenu menu = menuMap.get(name); if (menu == null) { throw new RuntimeException("登録されていないメニューです. " + name); } return menu; } /** * 生成されたメニュー項目を名前を指定して取得します.
* 存在しない場合は実行時例外が発生します.
* @param name メニュー項目名 * @return メニュー項目 */ public JMenuItem getJMenuItem(String name) { JMenuItem menuItem = menuItemMap.get(name); if (menuItem == null) { throw new RuntimeException("登録されていないメニュー項目です. " + name); } return menuItem; } /** * メニュー設定に従いメニューバーを構築して返します.
* 生成したメニューとメニュー項目は、{@link #getJMenu(String)}, * {@link #getJMenuItem(String)}で取得できます.
* @param menus メニュー設定 * @return 構築されたメニューバー */ public JMenuBar createMenuBar(MenuDataFactory[] menus) { // メニューリソース Properties menuProps = LocalizedResourcePropertyLoader.getInstance() .getLocalizedProperties(MENU_STRINGS_RESOURCE); // メニューバー final JMenuBar menuBar = createJMenuBar(); // 現在のメニュー設定をクリアする. menuMap.clear(); menuItemMap.clear(); // メニュー設定に従いメニューを構築する. for (MenuDataFactory menuDataFactory : menus) { MenuData menuData = menuDataFactory.createMenuData(menuProps); createMenu(new MenuAppender() { public void addMenu(JMenu menu) { menuBar.add(menu); } }, menuData, menuProps); } return menuBar; } private interface MenuAppender { void addMenu(JMenu menu); } protected void createMenu(MenuAppender parentMenu, MenuData menuData, Properties menuProps) { final JMenu menu = createJMenu(); if (menuData.makeMenu(menu)) { parentMenu.addMenu(menu); menuMap.put(menuData.getName(), menu); for (MenuData child : menuData) { if (child == null) { // セパレータ menu.add(new JSeparator()); } else if (child.getActionListener() == null) { // アクションリスナなしの場合はサブメニューと見なす createMenu(new MenuAppender() { public void addMenu(JMenu childMenu) { menu.add(childMenu); } }, child, menuProps); } else { // メニュー項目(チェックボックスつきメニュー項目を含む) JMenuItem menuItem; if (child.isCheckbox()) { menuItem = createJCheckBoxMenuItem(); } else { menuItem = createJMenuItem(); } if (child.makeMenu(menuItem)) { menu.add(menuItem); menuItemMap.put(child.getName(), menuItem); } } } } } /** * JMenuBarを構築します.
* アンチエイリアスが必要な場合はアンチエイリアスが設定されます.
* @return JMenuBar */ public JMenuBar createJMenuBar() { return new JMenuBar() { private static final long serialVersionUID = 1L; @Override public void paint(Graphics g) { if (needAntiAlias) { ((Graphics2D) g).setRenderingHint( RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); } super.paint(g); } }; } /** * JMenuを構築します.
* アンチエイリアスが必要な場合はアンチエイリアスが設定されます.
* @return JMenu */ public JMenu createJMenu() { return new JMenu() { private static final long serialVersionUID = 1L; @Override public void paint(Graphics g) { if (needAntiAlias) { ((Graphics2D) g).setRenderingHint( RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); } super.paint(g); } }; } /** * JCheckBoxMenuItemを構築します.
* アンチエイリアスが必要な場合はアンチエイリアスが設定されます.
* @return JCheckBoxMenuItem */ public JCheckBoxMenuItem createJCheckBoxMenuItem() { return new JCheckBoxMenuItem() { private static final long serialVersionUID = 1L; @Override public void paint(Graphics g) { if (needAntiAlias) { ((Graphics2D) g).setRenderingHint( RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); } super.paint(g); } }; } /** * JMenuItemを構築します.
* アンチエイリアスが必要な場合はアンチエイリアスが設定されます.
* @return JMenuItem */ public JMenuItem createJMenuItem() { return new JMenuItem() { private static final long serialVersionUID = 1L; @Override public void paint(Graphics g) { if (needAntiAlias) { ((Graphics2D) g).setRenderingHint( RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); } super.paint(g); } }; } } charactermanaj/src/charactermanaj/ui/MainFramePartialForMacOSX.java0000644000175000017500000000443311767047542025622 0ustar paulliupaulliupackage charactermanaj.ui; import java.awt.Image; import java.lang.reflect.Method; import java.util.logging.Level; import java.util.logging.Logger; import charactermanaj.util.SystemUtil; import com.apple.eawt.Application; import com.apple.eawt.ApplicationAdapter; import com.apple.eawt.ApplicationEvent; /** * Mac OS X用のメインフレームサポートクラス.
* スクリーンメニューのハンドラなどを接続している.
* @author seraphy * */ public class MainFramePartialForMacOSX { /** * ロガー */ private static final Logger logger = Logger.getLogger(MainFramePartialForMacOSX.class.getName()); private MainFramePartialForMacOSX() { super(); } public static void setupScreenMenu(final MainFrame mainFrame) { if (mainFrame == null) { throw new IllegalArgumentException(); } Application app = Application.getApplication(); app.setEnabledAboutMenu(true); app.setEnabledPreferencesMenu(true); ApplicationAdapter listener = new ApplicationAdapter() { public void handleAbout(ApplicationEvent arg0) { if (MainFrame.getActivedMainFrame() != null) { MainFrame.getActivedMainFrame().onAbout(); } arg0.setHandled(true); } public void handleQuit(ApplicationEvent arg0) { if (MainFrame.getActivedMainFrame() != null) { MainFrame.closeAllProfiles(); } arg0.setHandled(true); // JVMを明示的にシャットダウンする. (何もしないと強制終了になるため。) SystemUtil.exit(0); } public void handlePreferences(ApplicationEvent arg0) { if (MainFrame.getActivedMainFrame() != null) { MainFrame.getActivedMainFrame().onPreferences(); } arg0.setHandled(true); } }; app.addApplicationListener(listener); try { Class clz = app.getClass(); Method mtd = clz.getMethod("setDockIconImage", new Class[] {Image.class}); mtd.invoke(app, new Object[] {mainFrame.icon}); } catch (NoSuchMethodException ex) { // メソッドがない = Tiger以前の失敗であろうから、単に無視するだけで良い. logger.log(Level.CONFIG, "dockIcon not supported.", ex); } catch (Exception ex) { // 実行時の失敗だが、DockIconが設定できないだけなので継続する. logger.log(Level.WARNING, "dockIcon failed.", ex); } } } charactermanaj/src/charactermanaj/ui/model/0000755000175000017500000000000011767047540021213 5ustar paulliupaulliucharactermanaj/src/charactermanaj/ui/model/WallpaperFactory.java0000644000175000017500000001651411767047540025344 0ustar paulliupaulliupackage charactermanaj.ui.model; import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; import charactermanaj.graphics.io.EmbeddedImageResource; import charactermanaj.graphics.io.FileImageResource; import charactermanaj.graphics.io.ImageLoaderImpl; import charactermanaj.graphics.io.ImageResource; import charactermanaj.graphics.io.LoadedImage; import charactermanaj.ui.Wallpaper; import charactermanaj.ui.model.WallpaperInfo.WallpaperResourceType; /** * 壁紙情報から壁紙オブジェクトを作成するファクトリクラス.
* @author seraphy */ public class WallpaperFactory { /** * エラーが発生した場合にハンドリングするインターフェィス.
* ハンドラは、そのままエラーを送出するか、もしくは回復して続行させることができる.
* @author seraphy */ public interface ErrorHandler { /** * 指定された壁紙ファイルが、ファイルとして実在しない場合のエラー.
* 代わりのファイルを指定するかnullを返して画像なしとして扱うか、 * もしくは例外を送出することができる.
* @param wallpaperInfo 設定対象(回復時には更新可能) * @param file 対象となったファイル * @return 読み替えるファイル、nullの場合は画像なしとみなす. * @throws WallpaperFactoryException 例外とする場合 */ File missingImageFile(WallpaperInfo wallpaperInfo, File file) throws WallpaperFactoryException; /** * 指定した画像リソースの読み込みに失敗した場合のエラー.
* 代わりの画像を指定するかnullを返して画像なしとして扱うか、 * もしくは例外を送出することができる.
* @param wallpaperInfo 設定対象(回復時には更新可能) * @param imageResource 対象となった画像リソース * @param ex 失敗事由 * @return 代わりの画像、もしくは画像なしとするためにnullを返すことができる * @throws WallpaperFactoryException 例外とする場合 */ BufferedImage imageCreationFailed(WallpaperInfo wallpaperInfo, ImageResource imageResource, Throwable ex) throws WallpaperFactoryException; /** * その他の内部例外(RuntimeException)が発生した場合のハンドラ.
* 代わりの壁紙オブジェクトを返すか、もしくは例外を送出することができる.
* @param wallpaperInfo 壁紙情報 * @param wallpaper 構築中の壁紙オブジェクト * @param ex 発生した例外 * @return 代わりの壁紙オブジェクト (nullは返してはならない.) * @throws WallpaperFactoryException 例外とする場合 */ Wallpaper internalError(WallpaperInfo wallpaperInfo, Wallpaper wallpaper, Throwable ex) throws WallpaperFactoryException; } /** * 壁紙画像を読み取るためのイメージローダ.
*/ private ImageLoaderImpl imageLoader = new ImageLoaderImpl(); /** * シングルトン */ private static final WallpaperFactory inst = new WallpaperFactory(); /** * エラーを例外として送出する既定のハンドラ.
*/ public final ErrorHandler defaultErrorHandler = new WallpaperFactoryDefaultErrorHandler(); private WallpaperFactory() { super(); } public static WallpaperFactory getInstance() { return inst; } /** * 壁紙情報から壁紙オブジェクトを作成して返します.
* 壁紙情報に不備があるか何らかの問題により壁紙が作成できない場合はエラーハンドラが呼び出されます.
* エラーハンドラは例外を送出するか、もしくは回復して続行させることができます.
* 引数に渡される壁紙情報はエラーハンドラにより修復される可能性があります.
* @param wallpaperInfo 壁紙情報、nullの場合はデフォルト設定が用いられる. * @param errorHandler エラーハンドラ、省略した場合は{@link #DEFAULT_ERROR_HANDLER}が用いられる. * @return 生成された壁紙オブジェクト * @throws WallpaperFactoryException 壁紙オブジェクトの生成に失敗したことを通知する例外 */ public Wallpaper createWallpaper(WallpaperInfo wallpaperInfo, ErrorHandler errorHandler) throws WallpaperFactoryException { if (wallpaperInfo == null) { return new Wallpaper(); } if (errorHandler == null) { errorHandler = defaultErrorHandler; } Wallpaper wallpaper = new Wallpaper(); try { // 背景画像の設定. WallpaperResourceType typ = wallpaperInfo.getType(); ImageResource imageResource = null; if (typ == WallpaperResourceType.FILE) { // 選択ファイルから File imageFile = wallpaperInfo.getFile(); if (imageFile == null || !imageFile.exists() || !imageFile.isFile() || !imageFile.canRead()) { // ハンドラによってエラーを通知するか、もしくは回復する. imageFile = errorHandler.missingImageFile(wallpaperInfo, imageFile); } if (imageFile != null) { imageResource = new FileImageResource(imageFile); } } else if (typ == WallpaperResourceType.PREDEFINED) { // リソースファイルから String resource = wallpaperInfo.getResource(); if (resource != null && resource.trim().length() > 0) { imageResource = new EmbeddedImageResource(resource); } } BufferedImage wallpaperImg = null; if (imageResource != null) { try { LoadedImage wallpaperLoadedImage = imageLoader.load(imageResource); wallpaperImg = wallpaperLoadedImage.getImage(); } catch (IOException ex) { // ハンドラによってエラーを通知するか、もしくは回復する. wallpaperImg = errorHandler.imageCreationFailed(wallpaperInfo, imageResource, ex); } } wallpaper.setWallpaperImage(wallpaperImg); // アルファ値 wallpaper.setWallpaperAlpha(wallpaperInfo.getAlpha()); // 背景色 wallpaper.setBackgroundColor(wallpaperInfo.getBackgroundColor()); } catch (RuntimeException ex) { // ハンドラによってエラーを通知するか、もしくは回復する. wallpaper = errorHandler.internalError(wallpaperInfo, wallpaper, ex); if (wallpaper == null) { throw ex; } } return wallpaper; } } /** * 壁紙を構築時に問題があった場合に例外を送出するエラーハンドラ.
* @author seraphy */ class WallpaperFactoryDefaultErrorHandler implements WallpaperFactory.ErrorHandler { public BufferedImage imageCreationFailed(WallpaperInfo wallpaperInfo, ImageResource imageResource, Throwable ex) throws WallpaperFactoryException { throw new WallpaperFactoryException("image creation failed: " + imageResource, ex); } public Wallpaper internalError(WallpaperInfo wallpaperInfo, Wallpaper wallpaper, Throwable ex) throws WallpaperFactoryException { throw new WallpaperFactoryException("internal error: " + (ex == null ? "" : ex.getMessage()), ex); } public File missingImageFile(WallpaperInfo wallpaperInfo, File file) throws WallpaperFactoryException { throw new WallpaperFactoryException("missing image file: " + file); } } charactermanaj/src/charactermanaj/ui/model/WallpaperInfo.java0000644000175000017500000000560711767047540024631 0ustar paulliupaulliupackage charactermanaj.ui.model; import java.awt.Color; import java.io.File; import java.io.Serializable; /** * 壁紙情報.
* @author seraphy */ public class WallpaperInfo implements Serializable, Cloneable { /** * シリアライズバージョンID */ private static final long serialVersionUID = -3661482624140948074L; /** * 壁紙のリソースタイプ * @author seraphy */ public enum WallpaperResourceType { /** * なし */ NONE, /** * ファイル */ FILE, /** * 定義済み */ PREDEFINED } /** * 壁紙リソースのタイプ */ private WallpaperResourceType type = WallpaperResourceType.NONE; /** * ファイル */ private File file; /** * リソース */ private String resource; /** * 壁紙のアルファ値 */ private float alpha = 1.f; /** * 背景色 */ private Color backgroundColor = Color.WHITE; @Override public WallpaperInfo clone() { try { return (WallpaperInfo) super.clone(); } catch (CloneNotSupportedException ex) { throw new RuntimeException(ex); } } public WallpaperResourceType getType() { return type; } public void setType(WallpaperResourceType type) { if (type == null) { type = WallpaperResourceType.NONE; } this.type = type; } public File getFile() { return file; } public void setFile(File file) { this.file = file; } public String getResource() { return resource; } public void setResource(String resource) { if (resource != null) { resource = resource.trim(); if (resource.length() == 0) { resource = null; } } this.resource = resource; } /** * 背景画像のアルファ値を取得する. * @return アルファ値 */ public float getAlpha() { return alpha; } /** * 背景画像のアルファ値を設定する.
* 範囲は0から1の間であり、それを超えた場合は制限される.
* @param alpha アルファ値 */ public void setAlpha(float alpha) { if (alpha < 0) { alpha = 0; } else if (alpha > 1.f) { alpha = 1.f; } this.alpha = alpha; } /** * 背景色を取得する. * @return 背景色 */ public Color getBackgroundColor() { return backgroundColor; } /** * 背景色を設定する.
* nullを指定した場合は白とみなす.
* @param backgroundColor 背景色 */ public void setBackgroundColor(Color backgroundColor) { if (backgroundColor == null) { backgroundColor = Color.WHITE; } this.backgroundColor = backgroundColor; } @Override public String toString() { return "(WallpaperInfo:(type:" + type + ")(file:" + file + ")(resource:" + resource + ")(alpha:" + alpha + ")(bgColor:" + backgroundColor + "))"; } } charactermanaj/src/charactermanaj/ui/model/AbstractTableModelWithComboBoxModel.java0000644000175000017500000001162611767047540031026 0ustar paulliupaulliupackage charactermanaj.ui.model; import java.awt.Toolkit; import java.util.ArrayList; import java.util.LinkedList; import javax.swing.ComboBoxModel; import javax.swing.event.ListDataEvent; import javax.swing.event.ListDataListener; import javax.swing.table.AbstractTableModel; public /** * テーブルモデルをベースに、それをコンボボックスモデルとしても利用できるように拡張した抽象基底クラス.
* @author seraphy */ abstract class AbstractTableModelWithComboBoxModel extends AbstractTableModel implements ComboBoxModel { private static final long serialVersionUID = -6775939667002896930L; protected ArrayList elements = new ArrayList(); private boolean editable = true; public boolean removeRow(int rowIndex) { if (rowIndex < 0 || rowIndex >= elements.size()) { return false; } elements.remove(rowIndex); fireTableRowsDeleted(rowIndex, rowIndex); return true; } /** * 編集可能フラグ.
* モデル自身は、このフラグについて何ら関知せず、常に編集可能である.
* @param editable */ public void setEditable(boolean editable) { this.editable = editable; } /** * 編集可能フラグ.
* モデル自身は、このフラグについて何ら関知せず、常に編集可能である.
* 初期状態でtrueである.
* @return */ public boolean isEditable() { return editable; } public void clear() { elements.clear(); fireTableDataChanged(); } public T getRow(int index) { return elements.get(index); } public Object getElementAt(int index) { return getRow(index); } public boolean addRow(T obj) { if (obj == null) { throw new IllegalArgumentException(); } boolean ret = elements.add(obj); int row = elements.size() - 1; fireTableRowsInserted(row, row); return ret; } public int moveUp(int rowIndex) { if (rowIndex < 1 || rowIndex >= elements.size()) { Toolkit tk = Toolkit.getDefaultToolkit(); tk.beep(); return rowIndex; } T value = elements.get(rowIndex); elements.remove(rowIndex); elements.add(rowIndex - 1, value); fireTableRowsUpdated(rowIndex -1, rowIndex); return rowIndex - 1; } public int moveDown(int rowIndex) { if (rowIndex < 0 || rowIndex >= elements.size() - 1) { Toolkit tk = Toolkit.getDefaultToolkit(); tk.beep(); return rowIndex; } T value = elements.get(rowIndex); elements.remove(rowIndex); elements.add(rowIndex + 1, value); fireTableRowsUpdated(rowIndex, rowIndex + 1); return rowIndex + 1; } ///////////////////////////////////////// @Override public void fireTableCellUpdated(int row, int column) { super.fireTableCellUpdated(row, column); fireListUpdated(row, row); } @Override public void fireTableRowsDeleted(int firstRow, int lastRow) { super.fireTableRowsDeleted(firstRow, lastRow); fireListRemoved(firstRow, lastRow); } @Override public void fireTableRowsInserted(int firstRow, int lastRow) { super.fireTableRowsInserted(firstRow, lastRow); fireListAdded(firstRow, lastRow); } @Override public void fireTableRowsUpdated(int firstRow, int lastRow) { super.fireTableRowsUpdated(firstRow, lastRow); fireListUpdated(firstRow, lastRow); } @Override public void fireTableDataChanged() { super.fireTableDataChanged(); int siz = getRowCount(); if (siz > 0) { fireListUpdated(0, siz - 1); } } public int getRowCount() { return elements.size(); } ////////////////////////////// private Object selectedObject; private LinkedList listDataListeners = new LinkedList(); public Object getSelectedItem() { return selectedObject; } public void setSelectedItem(Object anItem) { selectedObject = anItem; } public void removeListDataListener(ListDataListener l) { if (l != null) { listDataListeners.remove(l); } } public void addListDataListener(ListDataListener l) { if (l != null) { listDataListeners.add(l); } } public void fireListUpdated(int firstRow, int lastRow) { ListDataEvent e = new ListDataEvent(this, ListDataEvent.CONTENTS_CHANGED, firstRow, lastRow); for (ListDataListener listener : listDataListeners) { listener.contentsChanged(e); } } public void fireListAdded(int firstRow, int lastRow) { ListDataEvent e = new ListDataEvent(this, ListDataEvent.INTERVAL_ADDED, firstRow, lastRow); for (ListDataListener listener : listDataListeners) { listener.intervalAdded(e); } } public void fireListRemoved(int firstRow, int lastRow) { ListDataEvent e = new ListDataEvent(this, ListDataEvent.INTERVAL_REMOVED, firstRow, lastRow); for (ListDataListener listener : listDataListeners) { listener.intervalRemoved(e); } } public int getSize() { return getRowCount(); } } charactermanaj/src/charactermanaj/ui/model/ColorChangeEvent.java0000644000175000017500000000166111767047540025250 0ustar paulliupaulliupackage charactermanaj.ui.model; import java.util.EventObject; import charactermanaj.model.Layer; import charactermanaj.ui.ColorDialog; public class ColorChangeEvent extends EventObject { private static final long serialVersionUID = -4185234778107466586L; private Layer layer; private boolean cascaded; public ColorChangeEvent(ColorDialog colorDialog, Layer layer) { this(colorDialog, layer, false); } public ColorChangeEvent(ColorChangeEvent src, boolean cascaded) { this((ColorDialog) src.getSource(), src.getLayer(), cascaded); } protected ColorChangeEvent(ColorDialog colorDialog, Layer layer, boolean cascaded) { super(colorDialog); if (layer == null) { throw new IllegalArgumentException("null layer"); } this.layer = layer; this.cascaded = cascaded; } public Layer getLayer() { return layer; } public boolean isCascaded() { return cascaded; } } charactermanaj/src/charactermanaj/ui/model/PartsSelectionManager.java0000644000175000017500000002650311767047540026316 0ustar paulliupaulliupackage charactermanaj.ui.model; import java.awt.Color; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import charactermanaj.model.PartsCategory; import charactermanaj.model.PartsColorInfo; import charactermanaj.model.PartsColorManager; import charactermanaj.model.PartsIdentifier; import charactermanaj.model.PartsSet; import charactermanaj.ui.ImageSelectPanel; /** * パーツ選択ペインの管理クラス. * @author seraphy */ public class PartsSelectionManager { /** * 背景色を管理するオブジェクトが実装するインターフェイス. * @author seraphy */ public interface ImageBgColorProvider { /** * 背景色を取得する. * @return 背景色 */ Color getImageBgColor(); /** * 背景色を設定する * @param imageBgColor 背景色 */ void setImageBgColor(Color imageBgColor); } /** * カテゴリ別パーツ選択パネル.
*/ private HashMap imageSelectPanels = new HashMap(); /** * パーツカラーマネージャ.
*/ private PartsColorManager partsColorMrg; /** * 背景色プロバイダ. */ private ImageBgColorProvider imageBgColorProvider; /** * アフィン変換用のパラメータ.
* 変換しない場合はnull.
*/ private double[] affineTransformParameter; /** * 単一選択カテゴリの選択解除の有効/無効.
* 有効にする場合はtrue.
*/ private boolean deselectableAllCategory; /** * コンストラクタ * @param partsColorMrg パーツカラーマネージャ * @param imageBgColorProvider 背景色プロバイダ */ public PartsSelectionManager( PartsColorManager partsColorMrg, ImageBgColorProvider imageBgColorProvider ) { if (partsColorMrg == null || imageBgColorProvider == null) { throw new IllegalArgumentException(); } this.partsColorMrg = partsColorMrg; this.imageBgColorProvider = imageBgColorProvider; } /** * パーツをロードする. */ public void loadParts() { for (ImageSelectPanel panel : imageSelectPanels.values()) { panel.loadParts(); } } public void register(ImageSelectPanel imageSelectPanel) { if (imageSelectPanel == null) { throw new IllegalArgumentException(); } imageSelectPanels.put(imageSelectPanel.getPartsCategory(), imageSelectPanel); } public List getSelectedPartsIdentifiers(PartsCategory partsCategory) { if (partsCategory == null) { throw new IllegalArgumentException(); } ImageSelectPanel panel = imageSelectPanels.get(partsCategory); if (panel != null) { return Collections.unmodifiableList(panel.getSelectedPartsIdentifiers()); } return Collections.emptyList(); } public PartsIdentifier getSelectedPartsIdentifier(PartsCategory partsCategory) { if (partsCategory == null) { throw new IllegalArgumentException(); } ImageSelectPanel panel = imageSelectPanels.get(partsCategory); if (panel != null) { return panel.getSelectedPartsIdentifier(); } return null; } public Collection getAllCategories() { ArrayList partsCategories = new ArrayList(); partsCategories.addAll(imageSelectPanels.keySet()); return partsCategories; } /** * 各カテゴリの選択状態と背景をパーツセットで指定されたものに設定します.
* @param partsSet パーツセット */ public void selectPartsSet(PartsSet partsSet) { if (partsSet == null) { throw new IllegalArgumentException(); } for (ImageSelectPanel panel : imageSelectPanels.values()) { PartsCategory partsCategory = panel.getPartsCategory(); List partsIdentifiers = partsSet.get(partsCategory); panel.selectParts(partsIdentifiers); if (partsIdentifiers != null) { for (PartsIdentifier partsIdentifier : partsIdentifiers) { PartsColorInfo partsColorInfo = partsSet.getColorInfo(partsIdentifier); if (partsColorInfo != null) { partsColorMrg.setPartsColorInfo(partsIdentifier, partsColorInfo, false); } } } } Color bgColor = partsSet.getBgColor(); if (bgColor != null) { setImageBgColor(bgColor); } affineTransformParameter = partsSet.getAffineTransformParameter(); // clone済み } /** * 現在の選択中のパーツと色設定からパーツセットを構築します. * 選択がなにもない場合は空のパーツセットとなります.
* @return パーツセット */ public PartsSet createPartsSet() { PartsSet presetParts = new PartsSet(); for (ImageSelectPanel imageSelectPanel : imageSelectPanels.values()) { PartsCategory category = imageSelectPanel.getPartsCategory(); for (PartsIdentifier partsIdentifier : imageSelectPanel.getSelectedPartsIdentifiers()) { PartsColorInfo partsColorInfo = partsColorMrg.getPartsColorInfo(partsIdentifier, false); presetParts.appendParts(category, partsIdentifier, partsColorInfo); } } presetParts.setBgColor(getImageBgColor()); presetParts.setAffineTransformParameter(affineTransformParameter); // 相手側でcloneする return presetParts; } /** * すべてのカテゴリのリストで選択中のアイテムが見えるようにスクロールする. */ public void scrollToSelectedParts() { for (ImageSelectPanel imageSelectPanel : imageSelectPanels.values()) { imageSelectPanel.scrollToSelectedRow(); } } /** * 指定したパーツ識別子にフォーカスを当てて、必要に応じてスクロールします.
* 該当するパーツ識別子がどこにもなければ何もしません.
* @param partsIdentifier パーツ識別子 */ public void setSelection(PartsIdentifier partsIdentifier) { if (partsIdentifier == null) { return; } PartsCategory partsCategory = partsIdentifier.getPartsCategory(); if (isMinimizeMode(partsCategory)) { setMinimizeModeIfOther(partsCategory, true); } ImageSelectPanel imageSelectPanel = imageSelectPanels.get(partsCategory); if (imageSelectPanel != null) { imageSelectPanel.setSelection(partsIdentifier); } } /** * 背景色を取得する. * @return 背景色 */ protected Color getImageBgColor() { return imageBgColorProvider.getImageBgColor(); } /** * 背景色を設定する * @param imageBgColor 背景色 */ protected void setImageBgColor(Color imageBgColor) { imageBgColorProvider.setImageBgColor(imageBgColor); } /** * アフィン変換用のパラメータを取得する.
* 変換しない場合はnull.
* @return アフィン変換用のパラメータ、もしくはnull */ public double[] getAffineTransformParameter() { return affineTransformParameter == null ? null : affineTransformParameter.clone(); } /** * アフィン変換用のパラメータを設定する.
* 変換しない場合はnull.
* 要素数は4または6でなければならない.
* @param affineTransformParameter アフィン変換用のパラメータ、もしくはnull */ public void setAffineTransformParameter(double[] affineTransformParameter) { if (affineTransformParameter != null && !(affineTransformParameter.length == 4 || affineTransformParameter.length == 6)) { throw new IllegalArgumentException("affineTransformParameter invalid length."); } this.affineTransformParameter = affineTransformParameter == null ? null : affineTransformParameter.clone(); } /** * 選択選択パーツカテゴリの選択解除を許可するか?
* @return 許可する場合はtrue */ public boolean isDeselectableSingleCategory() { return deselectableAllCategory; } /** * 選択選択パーツカテゴリの選択解除を許可するか設定する.
* @param deselectable 許可する場合はtrue */ public void setDeselectableSingleCategory(boolean deselectable) { this.deselectableAllCategory = deselectable; for (ImageSelectPanel imageSelectPanel : this.imageSelectPanels.values()) { imageSelectPanel.setDeselectableSingleCategory(deselectable); } } /** * パーツ選択をすべて解除する.
* 単一選択カテゴリが解除されるかどうかは、{@link #isDeselectableSingleCategory()}による.
*/ public void deselectAll() { for (ImageSelectPanel imageSelectPanel : this.imageSelectPanels.values()) { if (imageSelectPanel.getPartsCategory().isMultipleSelectable() || imageSelectPanel.isDeselectableSingleCategory()) { imageSelectPanel.deselectAll(); } } } /** * 指定したカテゴリ以外のパネルを最小化する.
* (指定したカテゴリがnullでなければ、そのカテゴリの最小化は解除される.)
* @param partsCategory 最小化の対象外のパネル、nullの場合は不問 * @param minimize 指定したカテゴリ以外を最小化する場合はtrue、falseの場合はすべてが最小化解除される. */ public void setMinimizeModeIfOther(PartsCategory partsCategory, boolean minimize) { for (Map.Entry entry : imageSelectPanels.entrySet()) { PartsCategory cat = entry.getKey(); ImageSelectPanel imageSelectPanel = entry.getValue(); if (partsCategory != null && cat.equals(partsCategory)) { imageSelectPanel.setMinimizeMode(false); } else { imageSelectPanel.setMinimizeMode(minimize); } } } /** * 指定したカテゴリが最小化モードであるか? * @param partsCategory カテゴリ * @return 指定したカテゴリが最小化モードであればtrue、該当するカテゴリがない場合は常にfalse */ public boolean isMinimizeMode(PartsCategory partsCategory) { ImageSelectPanel panel = imageSelectPanels.get(partsCategory); if (panel == null) { return false; } return panel.isMinimizeMode(); } /** * 指定したカテゴリが最小化モードでなく、且つ、他がすべて最小化モードであるか? * (指定カテゴリがない場合は全パネルが最小化モードである場合)
* @param partsCategory カテゴリ、もしくはnull * @return 指定したカテゴリが最小化モードでなく、且つ、他がすべて最小化モードである場合はtrue */ public boolean isNotMinimizeModeJust(PartsCategory partsCategory) { for (Map.Entry entry : imageSelectPanels.entrySet()) { PartsCategory cat = entry.getKey(); ImageSelectPanel imageSelectPanel = entry.getValue(); if (partsCategory != null) { if (cat.equals(partsCategory)) { // 指定したカテゴリが最小化モードであればfalse if (imageSelectPanel.isMinimizeMode()) { return false; } } else { // 指定したカテゴリ以外が最小化モードでなければfalse if ( !imageSelectPanel.isMinimizeMode()) { return false; } } } } // 指定カテゴリ以外がすべて最小化モードである場合 // (指定カテゴリがない場合は全パネルが最小化モードである場合) return true; } } charactermanaj/src/charactermanaj/ui/model/ColorChangeListener.java0000644000175000017500000000036111767047540025750 0ustar paulliupaulliupackage charactermanaj.ui.model; import java.util.EventListener; public interface ColorChangeListener extends EventListener { void onColorChange(ColorChangeEvent event); void onColorGroupChange(ColorChangeEvent event); } charactermanaj/src/charactermanaj/ui/model/PredefinedWallpaper.java0000644000175000017500000000466211767047540026003 0ustar paulliupaulliupackage charactermanaj.ui.model; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Properties; import charactermanaj.util.LocalizedMessageAware; import charactermanaj.util.LocalizedResourcePropertyLoader; /** * 定義済み壁紙 * @author seraphy */ public class PredefinedWallpaper implements Comparable, LocalizedMessageAware { private static final String PREDEFINED_WALLPAPER_RESOURCE = "images/wallpaper"; private final String key; private final String msgid; private final String resource; protected PredefinedWallpaper(String key, String msgid, String resource) { this.key = key; this.msgid = msgid; this.resource = resource; } public String getLocalizedResourceId() { return "predefinedWallpaper." + msgid; } public String getKey() { return key; } public String getMsgid() { return msgid; } public String getResource() { return resource; } @Override public boolean equals(Object obj) { if (obj == this) { return true; } if (obj instanceof PredefinedWallpaper) { PredefinedWallpaper o = (PredefinedWallpaper) obj; return key.equals(o.key); } return false; } @Override public int hashCode() { return key.hashCode(); } public int compareTo(PredefinedWallpaper o) { return key.compareTo(o.key); } @Override public String toString() { return msgid; } /** * 定義済み壁紙リソースのリストを取得する * @return 定義済み壁紙リソースリスト */ public static List getPredefinedWallpapers() { Properties predefinedWallpapers = LocalizedResourcePropertyLoader.getInstance() .getLocalizedProperties(PREDEFINED_WALLPAPER_RESOURCE); ArrayList results = new ArrayList(); for (Map.Entry entry : predefinedWallpapers.entrySet()) { String key = (String) entry.getKey(); String value = (String) entry.getValue(); String msgid; int pt = value.indexOf(';'); if (pt >= 0) { msgid = value.substring(pt + 1); } else { msgid = value; } PredefinedWallpaper predefinedWallpaper = new PredefinedWallpaper( key, msgid, "images/" + key); results.add(predefinedWallpaper); } Collections.sort(results); return results; } }charactermanaj/src/charactermanaj/ui/model/WallpaperFactoryErrorRecoverHandler.java0000644000175000017500000000502011767047540031170 0ustar paulliupaulliupackage charactermanaj.ui.model; import java.awt.image.BufferedImage; import java.io.File; import java.util.logging.Level; import java.util.logging.Logger; import charactermanaj.graphics.io.ImageResource; import charactermanaj.ui.Wallpaper; import charactermanaj.ui.model.WallpaperInfo.WallpaperResourceType; /** * 壁紙オブジェクトの構築に回復しながら継続するためのハンドラ.
* @author seraphy */ public class WallpaperFactoryErrorRecoverHandler implements WallpaperFactory.ErrorHandler { protected static final Logger logger = Logger.getLogger(WallpaperFactoryErrorRecoverHandler.class.getName()); private boolean errorOccured = false; private boolean recovered = false; /** * 何らかのエラーが発生し回復できなかった場合 * @return */ public boolean isErrorOccured() { return errorOccured; } /** * 何らかのエラーが発生したが回復された場合 * @return */ public boolean isRecovered() { return recovered; } public void setErrorOccured(boolean errorOccured) { this.errorOccured = errorOccured; } public void setRecovered(boolean recovered) { this.recovered = recovered; } public File missingImageFile(WallpaperInfo wallpaperInfo, File file) throws WallpaperFactoryException { if (file == null) { logger.log(Level.FINE, "壁紙ファイルの指定がありません."); } else { logger.log(Level.WARNING, "壁紙ファイルが存在しないか読み込みできません:" + file); } // ファイルは、もとより指定されていなかったものとして回復する. wallpaperInfo.setType(WallpaperResourceType.NONE); setRecovered(true); return null; } public BufferedImage imageCreationFailed(WallpaperInfo wallpaperInfo, ImageResource imageResource, Throwable ex) throws WallpaperFactoryException { logger.log(Level.WARNING, "壁紙ファイルの読み込みに失敗しました。:" + imageResource, ex); // ファイルは、もとより指定されていなかったものとして回復する. wallpaperInfo.setType(WallpaperResourceType.NONE); setRecovered(true); return null; } public Wallpaper internalError(WallpaperInfo wallpaperInfo, Wallpaper wallpaper, Throwable ex) throws WallpaperFactoryException { logger.log(Level.WARNING, "壁紙の構築に失敗しました。" + wallpaperInfo, ex); setErrorOccured(true); throw new WallpaperFactoryException("internal error: " + ex, ex); } } charactermanaj/src/charactermanaj/ui/model/PartsColorCoordinator.java0000644000175000017500000002654311767047540026364 0ustar paulliupaulliupackage charactermanaj.ui.model; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.IdentityHashMap; import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; import charactermanaj.graphics.filters.ColorConvertParameter; import charactermanaj.model.AppConfig; import charactermanaj.model.ColorGroup; import charactermanaj.model.ColorInfo; import charactermanaj.model.Layer; import charactermanaj.model.PartsColorInfo; import charactermanaj.model.PartsColorManager; import charactermanaj.model.PartsIdentifier; import charactermanaj.model.PartsSpec; import charactermanaj.model.PartsSpecResolver; import charactermanaj.ui.ColorDialog; import charactermanaj.ui.ImageSelectPanel; import charactermanaj.ui.ImageSelectPanel.ImageSelectPanelEvent; import charactermanaj.ui.ImageSelectPanel.ImageSelectPanelListener; /** * パーツの選択パネルとカラーダイアログを関連づけて調整するコーディネータオブジェクト.
* @author seraphy * */ public class PartsColorCoordinator { /** * ロガー */ private static final Logger logger = Logger.getLogger(PartsColorCoordinator.class.getName()); /** * パーツ選択パネルとカラーダイアログの関係を示すマップ.
*/ private IdentityHashMap colorDialogMap = new IdentityHashMap(); /** * パーツ識別子ごとのカラー情報を保存するパーツカラーマネージャ.
*/ private PartsColorManager partsColorMrg; /** * パーツ設定 */ private PartsSpecResolver partsSpecResolver; /** * 同一の色グループに設定値を同期させるためのコーディネータ.
*/ private ColorGroupCoordinator colorGroupCoordinator; /** * コンストラクタ.
* @param partsSpecResolver パーツ * @param partsColorMrg パーツ識別子ごとの色情報を管理するオブジェクト * @param colorGroupCoordinator 同一の色グループに設定値を同期させるためのコーディネータ. */ public PartsColorCoordinator(PartsSpecResolver partsSpecResolver, PartsColorManager partsColorMrg, ColorGroupCoordinator colorGroupCoordinator) { if (partsSpecResolver == null || partsColorMrg == null || colorGroupCoordinator == null) { throw new IllegalArgumentException(); } this.partsSpecResolver = partsSpecResolver; this.partsColorMrg = partsColorMrg; this.colorGroupCoordinator = colorGroupCoordinator; } /** * パーツ選択パネルと色ダイアログの関係を登録する.
* @param imageSelectPanel パーツ選択パネル * @param colorDialog 色ダイアログ */ public void register(final ImageSelectPanel imageSelectPanel, final ColorDialog colorDialog) { if (imageSelectPanel == null || colorDialog == null) { throw new IllegalArgumentException(); } if (colorDialogMap.containsKey(imageSelectPanel)) { throw new IllegalArgumentException("already registered: " + imageSelectPanel); } colorDialogMap.put(imageSelectPanel, colorDialog); imageSelectPanel.addImageSelectListener(new ImageSelectPanelListener() { public void onChangeColor(ImageSelectPanelEvent event) { // なにもしない } public void onPreferences(ImageSelectPanelEvent event) { // なにもしない } public void onChange(ImageSelectPanelEvent event) { PartsColorCoordinator.this.loadColorSettingToColorDialog(imageSelectPanel, colorDialog); } public void onSelectChange(ImageSelectPanelEvent event) { PartsColorCoordinator.this.loadColorSettingToColorDialog(imageSelectPanel, colorDialog); } public void onTitleClick(ImageSelectPanelEvent event) { // なにもしない } public void onTitleDblClick(ImageSelectPanelEvent event) { // なにもしない } }); colorDialog.addColorChangeListener(new ColorChangeListener() { public void onColorChange(ColorChangeEvent event) { saveColorSettingAll(); } public void onColorGroupChange(ColorChangeEvent event) { saveColorSettingAll(); } }); } /** * パーツ選択パネルの現在の選択に対する保存されているカラー情報を色ダイアログに設定する.
* @param imageSelectPanel パーツ選択パネル * @param colorDialog 色ダイアログ */ protected void loadColorSettingToColorDialog(ImageSelectPanel imageSelectPanel, ColorDialog colorDialog) { PartsIdentifier selectedParts = imageSelectPanel.getSelectedPartsIdentifier(); // 選択されているパーツのパーツ名と有効なレイヤーをカラーダイアログに設定する. // 選択されているパーツがない場合はデフォルトに戻す. colorDialog.setPartsIdentifier(selectedParts); colorDialog.setEnableLayers(getEnabledLayers(selectedParts)); if (selectedParts == null) { // 選択されているパーツがない場合は、ここまで。 return; } PartsColorInfo partsColorInfo = partsColorMrg.getPartsColorInfo(selectedParts, false); for (Map.Entry entry : partsColorInfo.entrySet()) { Layer layer = entry.getKey(); ColorInfo colorInfo = entry.getValue(); ColorGroup colorGroup = colorInfo.getColorGroup(); if (colorGroup == null) { colorGroup = ColorGroup.NA; } colorDialog.setColorGroup(layer, colorGroup); boolean syncColorGroup = colorInfo.isSyncColorGroup(); colorDialog.setSyncColorGroup(layer, syncColorGroup); ColorConvertParameter param = colorInfo.getColorParameter(); colorDialog.setColorConvertParameter(layer, param); if (syncColorGroup) { colorGroupCoordinator.syncColorGroup(colorDialog.getPartsCategory(), layer, colorDialog); } } } /** * 現在選択中のパーツの組み合わせに対応するカラーダイアログの設定情報をPartsColorManagerに保存する.
* (カラーダイアログの値が変更されるたびに呼び出される.)
*/ protected void saveColorSettingAll() { for (Map.Entry entry : colorDialogMap.entrySet()) { ImageSelectPanel imageSelectPanel = entry.getKey(); ColorDialog colorDialog = entry.getValue(); saveColorSettingFromColorDialog(imageSelectPanel, colorDialog); } } /** * カテゴリべつの現在選択中のパーツ識別子と、それに対応するカラーダイアログの設定値をPartsColorManagerに保存する.
* @param imageSelectPanel カテゴリ別のパーツ選択 * @param colorDialog 対応するカラーダイアログ */ protected void saveColorSettingFromColorDialog(ImageSelectPanel imageSelectPanel, ColorDialog colorDialog) { PartsIdentifier selectedParts = imageSelectPanel.getSelectedPartsIdentifier(); if (selectedParts == null) { return; } Map paramMap = colorDialog.getColorConvertParameters(); PartsColorInfo partsColorInfo = partsColorMrg.getPartsColorInfo(selectedParts, true); for (Map.Entry entry : paramMap.entrySet()) { Layer layer = entry.getKey(); ColorConvertParameter param = entry.getValue(); ColorInfo colorInfo = new ColorInfo(); ColorGroup colorGroup = colorDialog.getColorGroup(layer); colorInfo.setColorGroup(colorGroup); boolean syncColorGroup = colorDialog.isSyncColorGroup(layer); colorInfo.setSyncColorGroup(syncColorGroup); colorInfo.setColorParameter(param); partsColorInfo.put(layer, colorInfo); } boolean applyAll = colorDialog.isApplyAll(); partsColorMrg.setPartsColorInfo(selectedParts, partsColorInfo, applyAll); } /** * 全カラーダイアログで設定されている各レイヤーごとの色パラメータを全て取得する.
* @return 各レイヤーごとの色パラメータ */ public Map getColorConvertParameterMap() { final HashMap colorConvertParameterMap = new HashMap(); for (ColorDialog colorDlg : colorDialogMap.values()) { for (Map.Entry entry : colorDlg.getColorConvertParameters().entrySet()) { Layer layer = entry.getKey(); ColorConvertParameter colorConvertParameter = entry.getValue(); colorConvertParameterMap.put(layer, colorConvertParameter); } } return colorConvertParameterMap; } /** * 現在選択されている各カテゴリのパーツの組み合わせに対するカラー情報に各カテゴリの色ダイアログを設定する.
* (選択中のパーツ名も設定される.)
*/ public void initColorDialog() { for (Map.Entry entry : colorDialogMap.entrySet()) { ImageSelectPanel imageSelectPanel = entry.getKey(); ColorDialog colorDialog = entry.getValue(); PartsIdentifier partsIdentifier = imageSelectPanel.getSelectedPartsIdentifier(); // 選択されているパーツのパーツ名と有効なレイヤーをカラーダイアログに設定する. // 選択されているパーツがない場合はデフォルトに戻す. colorDialog.setPartsIdentifier(partsIdentifier); colorDialog.setEnableLayers(getEnabledLayers(partsIdentifier)); if (partsIdentifier != null) { PartsColorInfo partsColorInfo = partsColorMrg.getPartsColorInfo(partsIdentifier, false); for (Map.Entry colorInfoEntry : partsColorInfo.entrySet()) { Layer layer = colorInfoEntry.getKey(); ColorInfo colorInfo = colorInfoEntry.getValue(); if (logger.isLoggable(Level.FINE)) { logger.log(Level.FINE, layer + "=" + colorInfo); } colorDialog.setColorGroup(layer, colorInfo.getColorGroup()); colorDialog.setSyncColorGroup(layer, colorInfo.isSyncColorGroup()); colorDialog.setColorConvertParameter(layer, colorInfo.getColorParameter()); } } } } /** * 指定したパーツ識別子を構成する実際に存在するレイヤーを返します.
* (カテゴリが2つ以上のレイヤーをもっている場合でもパーツが、カテゴリのレイヤー数を全て使い切っていない場合は、使っているレイヤーのみが返されます.)
* 指定したパーツ識別子がnullの場合はnullを返します.
* 指定したパーツ識別子がnullではなく、且つ、パーツリゾルバから取得できない場合は空が返されます.
* ただし、アプリケーション設定で「存在しないレイヤーをディセーブルにしない」がtrueであれば、常にnullを返します.
* @param partsIdentifier パーツ識別子、もしくはnull * @return レイヤーのコレクション、もしくはnull */ protected Collection getEnabledLayers(PartsIdentifier partsIdentifier) { AppConfig appConfig = AppConfig.getInstance(); if (appConfig.isNotDisableLayerTab()) { return null; } if (partsIdentifier != null) { PartsSpec partsSpec = partsSpecResolver.getPartsSpec(partsIdentifier); ArrayList layers = new ArrayList(); if (partsSpec != null) { for (Layer layer : partsSpec.getPartsFiles().keySet()) { layers.add(layer); } } return layers; } return null; } } charactermanaj/src/charactermanaj/ui/model/WallpaperFactoryException.java0000644000175000017500000000073411767047540027220 0ustar paulliupaulliupackage charactermanaj.ui.model; /** * 壁紙の構築エラー.
* @author seraphy */ public class WallpaperFactoryException extends Exception { private static final long serialVersionUID = 6160297739997949904L; public WallpaperFactoryException() { super(); } public WallpaperFactoryException(String message) { super(message); } public WallpaperFactoryException(String message, Throwable cause) { super(message, cause); } } charactermanaj/src/charactermanaj/ui/model/ColorGroupCoordinator.java0000644000175000017500000002015211767047540026355 0ustar paulliupaulliupackage charactermanaj.ui.model; import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; import java.util.LinkedList; import java.util.Map; import charactermanaj.graphics.filters.ColorConvertParameter; import charactermanaj.model.ColorGroup; import charactermanaj.model.ColorInfo; import charactermanaj.model.Layer; import charactermanaj.model.PartsCategory; import charactermanaj.model.PartsColorInfo; import charactermanaj.model.PartsColorManager; import charactermanaj.model.PartsIdentifier; import charactermanaj.ui.ColorDialog; /** * 同一カラーグループでの連動をサポートするためのコーディネータオブジェクト.
* @author seraphy */ public class ColorGroupCoordinator { /** * パーツの選択状態を管理するマネージャ.
*/ private final PartsSelectionManager partsSelectionMrg; /** * 色ダイアログのコレクション */ private final LinkedList colorDialogs = new LinkedList(); /** * カラーグループの変更通知を受けるリスナーのコレクション.
*/ private final LinkedList listeners = new LinkedList(); /** * パーツ識別子ごとの色情報を管理するパーツカラーマネージャ */ private final PartsColorManager partsColorMrg; /** * 色ダイアログからの通知を受け取るリスナ.(自クラス内オブジェクト) */ protected final ColorChangeListener listener; /** * コンストラクタ * @param partsSelectionMrg パーツの選択を管理するマネージャ * @param partsColorMrg パーツ識別子ごとの色を管理するマネージャ */ public ColorGroupCoordinator(PartsSelectionManager partsSelectionMrg, PartsColorManager partsColorMrg) { if (partsSelectionMrg == null || partsColorMrg == null) { throw new IllegalArgumentException(); } this.partsSelectionMrg = partsSelectionMrg; this.partsColorMrg = partsColorMrg; listener = new ColorChangeListener() { public void onColorChange(ColorChangeEvent event) { Layer layer = event.getLayer(); ColorDialog colorDialog = (ColorDialog) event.getSource(); PartsCategory partsCategory = colorDialog.getPartsCategory(); ColorGroupCoordinator.this.syncColorGroup(partsCategory, layer, colorDialog); ColorGroupCoordinator.this.fireColorChangeEvent(event); } public void onColorGroupChange(ColorChangeEvent event) { Layer layer = event.getLayer(); ColorDialog colorDialog = (ColorDialog) event.getSource(); ColorGroup colorGroup = colorDialog.getColorGroup(layer); ColorGroupCoordinator.this.onChangeColorGroup(colorDialog, layer, colorGroup); ColorGroupCoordinator.this.fireColorGroupChangeEvent(event); } }; } /** * カラーダイアログを登録する.
* @param colorDialog カラーダイアログ */ public void registerColorDialog(ColorDialog colorDialog) { if (colorDialog == null) { throw new IllegalArgumentException(); } this.colorDialogs.add(colorDialog); colorDialog.addColorChangeListener(listener); } /** * カラーダイアログの登録を解除する.
* @param colorDialog カラーダイアログ */ public void unregisterColorDialog(ColorDialog colorDialog) { Iterator ite = colorDialogs.iterator(); while (ite.hasNext()) { ColorDialog dlg = ite.next(); if (dlg == colorDialog) { dlg.removeColorChangeListener(listener); ite.remove(); } } } /** * カラーグループが変更されたことを通知するリスナを登録する.
* @param listener リスナー */ public void addColorChangeListener(ColorChangeListener listener) { if (listener == null) { throw new IllegalArgumentException(); } listeners.add(listener); } /** * カラーグループが変更されたことを通知するリスナを登録解除する.
* @param listener リスナー */ public void removeColorChangeListener(ColorChangeListener listener) { listeners.remove(listener); } protected void fireColorChangeEvent(ColorChangeEvent e) { if (e == null) { throw new IllegalArgumentException(); } for (ColorChangeListener listener : listeners) { listener.onColorChange(e); } } protected void fireColorGroupChangeEvent(ColorChangeEvent e) { if (e == null) { throw new IllegalArgumentException(); } for (ColorChangeListener listener : listeners) { listener.onColorGroupChange(e); } } protected void onChangeColorGroup(ColorDialog destColorDialog, Layer layer, ColorGroup colorGroup) { if (destColorDialog == null || layer == null || colorGroup == null) { throw new IllegalArgumentException(); } for (ColorDialog colorDlg : colorDialogs) { for (Layer srcLayer : colorDlg.getPartsCategory().getLayers()) { if (!srcLayer.equals(layer)) { if (ColorGroup.equals(colorGroup, colorDlg.getColorGroup(srcLayer)) && colorDlg.isSyncColorGroup(srcLayer)) { ColorConvertParameter param = colorDlg.getColorConvertParameter(srcLayer); destColorDialog.setColorConvertParameter(layer, param); break; } } } } } /** * パーツの色ダイアログが変更されたことにより、同一の他のカラーグループのレイヤーのカラーダイアログの設定値をコピーする.
* (色ダイアログのパラメータ変更により呼び出される.)
* @param partsCategory パーツカテゴリ * @param eventSourceLayer 変更もとのレイヤー * @param sourceColorDialog 変更されたカラーダイアログ */ public void syncColorGroup(PartsCategory partsCategory, Layer eventSourceLayer, ColorDialog sourceColorDialog) { if (partsCategory == null || sourceColorDialog == null) { throw new IllegalArgumentException(); } // 変更もと ArrayList syncSources = new ArrayList(); if (eventSourceLayer != null) { if (sourceColorDialog.isSyncColorGroup(eventSourceLayer)) { syncSources.add(eventSourceLayer); } } else { for (Layer layer2 : partsCategory.getLayers()) { if (sourceColorDialog.isSyncColorGroup(layer2)) { syncSources.add(layer2); } } } // 変更もとのレイヤーのカラーグループを他のレイヤーにも適用する. for (Layer sourceLayer : syncSources) { ColorGroup sourceColorGroup = sourceColorDialog.getColorGroup(sourceLayer); if (sourceColorGroup != null && sourceColorGroup.isEnabled()) { ColorConvertParameter param = sourceColorDialog.getColorConvertParameter(sourceLayer); // 他のパネルに適用する for (ColorDialog targetColorDialog : colorDialogs) { for (Layer targetLayer : targetColorDialog.getPartsCategory().getLayers()) { if (!targetLayer.equals(sourceLayer)) { if (ColorGroup.equals(targetColorDialog.getColorGroup(targetLayer), sourceColorGroup)) { if (targetColorDialog.isSyncColorGroup(targetLayer)) { targetColorDialog.setColorConvertParameter(targetLayer, param); } } } } } // 色ダイアログで選択中でない有効なパーツも含めてパーツカラーを更新する. for (PartsCategory targetPartsCategory : partsSelectionMrg.getAllCategories()) { Collection selectedPartss = partsSelectionMrg.getSelectedPartsIdentifiers(targetPartsCategory); for (PartsIdentifier partsIdentifier : selectedPartss) { // カラーダイアログで選択されていない他のパーツも含めてパーツカラーを更新する. PartsColorInfo partsColorInfo = partsColorMrg.getPartsColorInfo(partsIdentifier, true); for (Map.Entry entry : partsColorInfo.entrySet()) { ColorInfo colorInfo = entry.getValue(); if (ColorGroup.equals(sourceColorGroup, colorInfo.getColorGroup())) { if (colorInfo.isSyncColorGroup()) { colorInfo.setColorParameter(param); } } } } } } } } } charactermanaj/src/charactermanaj/ui/SearchPartsDialog.java0000644000175000017500000004035611767047542024327 0ustar paulliupaulliupackage charactermanaj.ui; import java.awt.BorderLayout; import java.awt.Container; import java.awt.Dimension; import java.awt.GraphicsEnvironment; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.Insets; import java.awt.Point; import java.awt.Rectangle; import java.awt.Toolkit; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.KeyEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.Map.Entry; import java.util.WeakHashMap; import javax.swing.AbstractAction; import javax.swing.ActionMap; import javax.swing.BorderFactory; import javax.swing.Box; import javax.swing.InputMap; import javax.swing.JButton; import javax.swing.JComboBox; import javax.swing.JComponent; import javax.swing.JDialog; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JRootPane; import javax.swing.JScrollPane; import javax.swing.JTable; import javax.swing.JTextField; import javax.swing.KeyStroke; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; import javax.swing.table.TableColumnModel; import charactermanaj.model.AppConfig; import charactermanaj.model.PartsCategory; import charactermanaj.model.PartsIdentifier; import charactermanaj.model.PartsSpec; import charactermanaj.model.PartsSpecResolver; import charactermanaj.ui.model.AbstractTableModelWithComboBoxModel; import charactermanaj.ui.model.PartsSelectionManager; import charactermanaj.util.LocalizedResourcePropertyLoader; public class SearchPartsDialog extends JDialog { private static final long serialVersionUID = 1L; private static final WeakHashMap ALL_DIALOGS = new WeakHashMap(); protected static final String STRINGS_RESOURCE = "languages/searchpartsdialog"; private PartsSpecResolver partsSpecResolver; private PartsSelectionManager partsSelectionManager; private JTable searchPartsTable; private SearchPartsTableModel searchPartsTableModel; private JTextField txtPartsName; private JComboBox cmbAuthors; private JComboBox cmbCategories; public static SearchPartsDialog[] getDialogs() { return ALL_DIALOGS.keySet().toArray(new SearchPartsDialog[ALL_DIALOGS.size()]); } public SearchPartsDialog(JFrame parent, PartsSpecResolver partsSpecResolver, PartsSelectionManager partsSelectionManager) { super(parent, false); if (partsSpecResolver == null) { throw new IllegalArgumentException(); } this.partsSpecResolver = partsSpecResolver; this.partsSelectionManager = partsSelectionManager; setDefaultCloseOperation(JDialog.DO_NOTHING_ON_CLOSE); addWindowListener(new WindowAdapter() { @Override public void windowClosing(WindowEvent e) { onClose(); } }); Properties strings = LocalizedResourcePropertyLoader.getInstance() .getLocalizedProperties(STRINGS_RESOURCE); setTitle(strings.getProperty("title")); // モデル searchPartsTableModel = new SearchPartsTableModel(); // 検索条件パネル JPanel searchCondPanel = new JPanel(); GridBagLayout searchCondPanelLayout = new GridBagLayout(); searchCondPanel.setLayout(searchCondPanelLayout); GridBagConstraints gbc = new GridBagConstraints(); searchCondPanel.setBorder(BorderFactory.createCompoundBorder( BorderFactory.createEmptyBorder(5, 5, 5, 5), BorderFactory .createTitledBorder(strings.getProperty("search.condition")))); gbc.gridx = 0; gbc.gridy = 0; gbc.gridheight = 1; gbc.gridwidth = 1; gbc.anchor = GridBagConstraints.EAST; gbc.fill = GridBagConstraints.BOTH; gbc.ipadx = 0; gbc.ipady = 0; gbc.insets = new Insets(3, 3, 3, 3); gbc.weightx = 0.; gbc.weighty = 0.; searchCondPanel.add(new JLabel(strings.getProperty("partsname"), JLabel.RIGHT), gbc); gbc.gridx = 1; gbc.gridy = 0; gbc.weightx = 1.; txtPartsName = new JTextField(); searchCondPanel.add(txtPartsName, gbc); gbc.gridx = 0; gbc.gridy = 1; gbc.gridwidth = 1; gbc.weightx = 0.; searchCondPanel.add(new JLabel(strings.getProperty("author"), JLabel.RIGHT), gbc); gbc.gridx = 1; gbc.gridy = 1; gbc.gridwidth = 2; gbc.weightx = 1.; ArrayList authors = new ArrayList(); authors.add(""); authors.addAll(getAuthors(partsSpecResolver)); cmbAuthors = new JComboBox(authors.toArray(new String[authors.size()])); searchCondPanel.add(cmbAuthors, gbc); gbc.gridx = 0; gbc.gridy = 2; gbc.gridwidth = 1; gbc.weightx = 0.; searchCondPanel.add(new JLabel(strings.getProperty("partscategory"), JLabel.RIGHT), gbc); gbc.gridx = 1; gbc.gridy = 2; gbc.gridwidth = 2; gbc.weightx = 1.; ArrayList categories = new ArrayList(); categories.add(null); categories.addAll(partsSpecResolver.getPartsCategories()); cmbCategories = new JComboBox(categories.toArray(new PartsCategory[categories.size()])); searchCondPanel.add(cmbCategories, gbc); gbc.gridx = 2; gbc.gridy = 0; gbc.gridheight = 1; gbc.gridwidth = 1; gbc.weightx = 0.; gbc.weighty = 1.; gbc.anchor = GridBagConstraints.SOUTH; gbc.fill = GridBagConstraints.HORIZONTAL; JButton btnClear = new JButton(new AbstractAction(strings.getProperty("clear")) { private static final long serialVersionUID = 1L; public void actionPerformed(ActionEvent e) { txtPartsName.setText(""); } }); searchCondPanel.add(btnClear, gbc); // 検索結果 JPanel searchResult = new JPanel(new BorderLayout()); searchResult.setBorder(BorderFactory.createCompoundBorder( BorderFactory.createEmptyBorder(0, 5, 0, 5), BorderFactory .createTitledBorder(strings.getProperty("results")))); searchPartsTable = new JTable(searchPartsTableModel); searchPartsTable.setShowGrid(true); searchPartsTable.setGridColor(AppConfig.getInstance().getGridColor()); JScrollPane searchPartsTableSP = new JScrollPane(searchPartsTable); searchPartsTableModel.adjustColumnModel(searchPartsTable.getColumnModel()); searchResult.add(searchPartsTableSP, BorderLayout.CENTER); searchPartsTable.addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { if (e.getClickCount() == 2) { // ダブルクリック // 正確に2回 onSelect(); } } }); // テーブルのキーイベント ActionMap tblAm = searchPartsTable.getActionMap(); InputMap tblIm = searchPartsTable.getInputMap(); tblIm.put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), "onSelect"); tblAm.put("onSelect", new AbstractAction() { private static final long serialVersionUID = 1L; public void actionPerformed(ActionEvent e) { onSelect(); } }); JPanel btnPanel = new JPanel(); btnPanel.setBorder(BorderFactory.createEmptyBorder(0, 5, 0, 10)); GridBagLayout btnPanelLayout = new GridBagLayout(); btnPanel.setLayout(btnPanelLayout); gbc.gridx = 0; gbc.gridy = 0; gbc.gridheight = 1; gbc.gridwidth = 1; gbc.anchor = GridBagConstraints.EAST; gbc.fill = GridBagConstraints.BOTH; gbc.ipadx = 0; gbc.ipady = 0; gbc.insets = new Insets(3, 3, 3, 3); gbc.weightx = 1.; gbc.weighty = 0.; btnPanel.add(Box.createHorizontalGlue(), gbc); gbc.gridx = 1; gbc.gridy = 0; gbc.weightx = 0.; JButton btnSelect = new JButton(new AbstractAction(strings.getProperty("select")) { private static final long serialVersionUID = 1L; public void actionPerformed(ActionEvent e) { onSelect(); } }); btnPanel.add(btnSelect, gbc); searchResult.add(btnPanel, BorderLayout.SOUTH); // 検索条件の入力が変更されたことを検知するリスナの登録 txtPartsName.getDocument().addDocumentListener(new DocumentListener() { public void removeUpdate(DocumentEvent e) { onChangeCondition(); } public void insertUpdate(DocumentEvent e) { onChangeCondition(); } public void changedUpdate(DocumentEvent e) { onChangeCondition(); } }); ActionListener changeListener = new ActionListener() { public void actionPerformed(ActionEvent e) { onChangeCondition(); } }; cmbAuthors.addActionListener(changeListener); cmbCategories.addActionListener(changeListener); // ESCキーとCTRL-Wで閉じる. Toolkit tk = Toolkit.getDefaultToolkit(); JRootPane rootPane = getRootPane(); InputMap im = rootPane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW); ActionMap am = rootPane.getActionMap(); im.put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "closeSearchDialog"); im.put(KeyStroke.getKeyStroke(KeyEvent.VK_W, tk.getMenuShortcutKeyMask()), "closeSearchDialog"); am.put("closeSearchDialog", new AbstractAction() { private static final long serialVersionUID = 1L; public void actionPerformed(ActionEvent e) { onClose(); } }); // ウィンドウがアクティブになったときに検索フィールドにフォーカスをあてる addWindowFocusListener(new WindowAdapter() { public void windowGainedFocus(WindowEvent e) { txtPartsName.requestFocusInWindow(); } }); // 画面の設定 Container contentPane = getContentPane(); contentPane.setLayout(new BorderLayout()); contentPane.add(searchCondPanel, BorderLayout.NORTH); contentPane.add(searchResult, BorderLayout.CENTER); setSize(250, 300); setLocationRelativeTo(parent); // ダイアログの登録 ALL_DIALOGS.put(this, null); } public List getAuthors(PartsSpecResolver partsSpecResolver) { if (partsSpecResolver == null) { throw new IllegalArgumentException(); } HashSet authorsSet = new HashSet(); for (PartsCategory category : partsSpecResolver.getPartsCategories()) { for (Map.Entry entry : partsSpecResolver.getPartsSpecMap(category).entrySet()) { PartsSpec partsSpec = entry.getValue(); String author = partsSpec.getAuthor(); if (author != null) { authorsSet.add(author); } } } ArrayList authors = new ArrayList(authorsSet); Collections.sort(authors); return authors; } protected void onClose() { dispose(); } /** * ダイアログの表示位置を調整する.
* 横位置Xはメインフレームの右側とし、縦位置Yはメインフレームの上位置からのoffset_yを加えた位置とする.
* @param offset_y オフセットY */ public void adjustLocation(int offset_y) { // メインウィンドウよりも左側に位置づけする. // 縦位置はメインウィンドウの上端からオフセットを加えたものとする. Point pt = getParent().getLocation(); Insets insets = getParent().getInsets(); pt.x += getParent().getWidth(); pt.y += (offset_y * insets.top); // メインスクリーンサイズを取得する. GraphicsEnvironment genv = GraphicsEnvironment.getLocalGraphicsEnvironment(); Rectangle desktopSize = genv.getMaximumWindowBounds(); // メインスクリーンのサイズ(デスクトップ領域のみ) // メインスクリーンサイズを超えた場合は、はみ出た分を移動する. if ((pt.x + getWidth()) > desktopSize.width) { pt.x -= ((pt.x + getWidth()) - desktopSize.width); } if ((pt.y + getHeight()) > desktopSize.height) { pt.y -= ((pt.y + getHeight()) - desktopSize.height); } setLocation(pt); // 高さはメインフレームと同じにする. Dimension siz = getSize(); siz.height = getParent().getHeight() - offset_y; setSize(siz); } /** * 「選択」ボタンまたはテーブルのダブルクリックのハンドラ.
* 選択されている行のパーツ識別子をもとに、パーツにフォーカスをあてる.
*/ protected void onSelect() { int selRow = searchPartsTable.getSelectedRow(); if (selRow >= 0) { Map.Entry entry = searchPartsTableModel.getRow(selRow); PartsIdentifier partsIdentifier = entry.getKey(); partsSelectionManager.setSelection(partsIdentifier); } } protected void onChangeCondition() { String partsNamesRaw = txtPartsName.getText(); partsNamesRaw = partsNamesRaw.replace(" ", " "); // 全角空白を半角に変換 String[] condPartsNames = partsNamesRaw.split("\\s+"); PartsCategory condPartsCategory = (PartsCategory) cmbCategories.getSelectedItem(); String condAuthor = (String) cmbAuthors.getSelectedItem(); if (condAuthor != null && condAuthor.trim().length() == 0) { condAuthor = null; } ArrayList> partsIdentifiers = new ArrayList>(); for (PartsCategory partsCategory : partsSpecResolver.getPartsCategories()) { if (condPartsCategory != null && !condPartsCategory.equals(partsCategory)) { continue; } for (Map.Entry entry : partsSpecResolver.getPartsSpecMap(partsCategory).entrySet()) { PartsIdentifier partsIdentifier = entry.getKey(); PartsSpec partsSpec = entry.getValue(); if (condAuthor != null) { String author = partsSpec.getAuthor(); if (author == null || !author.equals(condAuthor)) { continue; } } String localizedPartsName = partsIdentifier.getLocalizedPartsName(); if (localizedPartsName != null) { for (String condPartsName : condPartsNames) { if (localizedPartsName.indexOf(condPartsName) >= 0) { partsIdentifiers.add(entry); continue; } } } } } Collections.sort(partsIdentifiers, new Comparator>() { public int compare(Entry o1, Entry o2) { PartsIdentifier partsIdentifier1 = o1.getKey(); PartsIdentifier partsIdentifier2 = o2.getKey(); return partsIdentifier1.compareTo(partsIdentifier2); } }); searchPartsTableModel.initModel(partsIdentifiers); } } class SearchPartsTableModel extends AbstractTableModelWithComboBoxModel> { private static final long serialVersionUID = 1L; private static final String[] COLUMN_NAMES; private static final int[] COLUMN_WIDTHS; static { Properties strings = LocalizedResourcePropertyLoader.getInstance() .getLocalizedProperties(SearchPartsDialog.STRINGS_RESOURCE); COLUMN_NAMES = new String[] { strings.getProperty("column.partsname"), strings.getProperty("column.category"), strings.getProperty("column.author"), }; COLUMN_WIDTHS = new int[] { Integer.parseInt(strings.getProperty("column.partsname.width")), Integer.parseInt(strings.getProperty("column.category.width")), Integer.parseInt(strings.getProperty("column.author.width")), }; } public void adjustColumnModel(TableColumnModel columnModel) { for (int idx = 0; idx < COLUMN_WIDTHS.length; idx++) { columnModel.getColumn(idx).setPreferredWidth(COLUMN_WIDTHS[idx]); } } public int getColumnCount() { return COLUMN_NAMES.length; } @Override public String getColumnName(int column) { return COLUMN_NAMES[column]; } public Object getValueAt(int rowIndex, int columnIndex) { Map.Entry row = getRow(rowIndex); PartsIdentifier partsIdentifier = row.getKey(); PartsSpec partsSpec = row.getValue(); switch (columnIndex) { case 0: return partsIdentifier.getLocalizedPartsName(); case 1: return partsIdentifier.getPartsCategory().getLocalizedCategoryName(); case 2: return partsSpec.getAuthor(); } return ""; } public void initModel(List> partsIdentifiers) { clear(); if (partsIdentifiers != null) { for (Map.Entry entry : partsIdentifiers) { addRow(entry); } } } } charactermanaj/src/charactermanaj/ui/MainFrame.java0000644000175000017500000024105711767047542022630 0ustar paulliupaulliupackage charactermanaj.ui; import static java.lang.Math.max; import static java.lang.Math.min; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Component; import java.awt.Container; import java.awt.Cursor; import java.awt.Dimension; import java.awt.Font; import java.awt.Frame; import java.awt.GraphicsEnvironment; import java.awt.Point; import java.awt.Rectangle; import java.awt.Toolkit; import java.awt.dnd.DropTarget; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.UUID; import java.util.logging.Level; import java.util.logging.Logger; import javax.swing.Box; import javax.swing.BoxLayout; import javax.swing.JCheckBox; import javax.swing.JColorChooser; import javax.swing.JFrame; import javax.swing.JMenu; import javax.swing.JMenuBar; import javax.swing.JMenuItem; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JScrollBar; import javax.swing.JScrollPane; import javax.swing.JSeparator; import javax.swing.JSplitPane; import javax.swing.JViewport; import javax.swing.SwingUtilities; import javax.swing.event.AncestorEvent; import javax.swing.event.AncestorListener; import javax.swing.event.MenuEvent; import javax.swing.event.MenuListener; import charactermanaj.Main; import charactermanaj.clipboardSupport.ClipboardUtil; import charactermanaj.graphics.AsyncImageBuilder; import charactermanaj.graphics.ColorConvertedImageCachedLoader; import charactermanaj.graphics.ImageBuildJobAbstractAdaptor; import charactermanaj.graphics.ImageBuilder.ImageOutput; import charactermanaj.graphics.io.ImageSaveHelper; import charactermanaj.graphics.io.OutputOption; import charactermanaj.graphics.io.UkagakaImageSaveHelper; import charactermanaj.model.AppConfig; import charactermanaj.model.CharacterData; import charactermanaj.model.ColorGroup; import charactermanaj.model.PartsCategory; import charactermanaj.model.PartsColorInfo; import charactermanaj.model.PartsColorManager; import charactermanaj.model.PartsIdentifier; import charactermanaj.model.PartsSet; import charactermanaj.model.RecommendationURL; import charactermanaj.model.WorkingSet; import charactermanaj.model.io.CharacterDataPersistent; import charactermanaj.model.io.PartsImageDirectoryWatchAgent; import charactermanaj.model.io.PartsImageDirectoryWatchAgentFactory; import charactermanaj.model.io.PartsImageDirectoryWatchEvent; import charactermanaj.model.io.PartsImageDirectoryWatchListener; import charactermanaj.model.io.RecentDataPersistent; import charactermanaj.ui.ImageSelectPanel.ImageSelectPanelEvent; import charactermanaj.ui.ImageSelectPanel.ImageSelectPanelListener; import charactermanaj.ui.PreviewPanel.PreviewPanelEvent; import charactermanaj.ui.PreviewPanel.PreviewPanelListener; import charactermanaj.ui.model.ColorChangeEvent; import charactermanaj.ui.model.ColorChangeListener; import charactermanaj.ui.model.ColorGroupCoordinator; import charactermanaj.ui.model.PartsColorCoordinator; import charactermanaj.ui.model.PartsSelectionManager; import charactermanaj.ui.model.WallpaperFactory; import charactermanaj.ui.model.WallpaperFactoryErrorRecoverHandler; import charactermanaj.ui.model.WallpaperFactoryException; import charactermanaj.ui.model.WallpaperInfo; import charactermanaj.ui.util.FileDropTarget; import charactermanaj.util.DesktopUtilities; import charactermanaj.util.ErrorMessageHelper; import charactermanaj.util.SystemUtil; import charactermanaj.util.LocalizedResourcePropertyLoader; import charactermanaj.util.UIHelper; import charactermanaj.util.UserData; import charactermanaj.util.UserDataFactory; /** * メインフレーム.
* アプリケーションがアクティブである場合は最低でも1つのメインフレームが表示されている.
* @author seraphy */ public class MainFrame extends JFrame { private static final long serialVersionUID = 1L; private static final Logger logger = Logger.getLogger(MainFrame.class.getName()); protected static final String STRINGS_RESOURCE = "languages/mainframe"; protected static final String MENU_STRINGS_RESOURCE = "menu/menu"; /** * メインフレームのアイコン.
*/ protected BufferedImage icon; /** * 現在アクティブなメインフレーム.
* フォーカスが切り替わるたびにアクティブフレームを追跡する.
* Mac OS XのAbout/Preferences/Quitのシステムメニューからよびだされた場合に * オーナーたるべきメインフレームを識別するためのもの.
*/ private static volatile MainFrame activedMainFrame; /** * このメインフレームが対象とするキャラクターデータ.
*/ protected CharacterData characterData; /** * プレビューペイン */ private PreviewPanel previewPane; /** * パーツ選択マネージャ */ protected PartsSelectionManager partsSelectionManager; /** * パネルの最小化モード */ private boolean minimizeMode; /** * パーツ選択パネルリスト */ protected ImageSelectPanelList imageSelectPanels; /** * パーツ選択パネルを納めるスクロールペイン */ protected JScrollPane imgSelectPanelsPanelSp; /** * カラーグループのマネージャ */ protected ColorGroupCoordinator colorGroupCoordinator; /** * パーツカラーのマネージャ */ protected PartsColorCoordinator partsColorCoordinator; /** * キャッシュつきのイメージローダ.
*/ private ColorConvertedImageCachedLoader imageLoader; /** * パーツを組み立てて1つのプレビュー可能なイメージを構築するためのビルダ */ private AsyncImageBuilder imageBuilder; /** * パーツイメージを画像として保存する場合のヘルパー.
* 最後に使ったディレクトリを保持するためのメンバ変数としている.
*/ private ImageSaveHelper imageSaveHelper = new ImageSaveHelper(); /** * 伺か用出力ヘルパ.
* 最後に使ったディレクトリ、ファイル名、モードなどを保持するためのメンバ変数としている.
*/ private UkagakaImageSaveHelper ukagakaImageSaveHelper = new UkagakaImageSaveHelper(); /** * パーツディレクトリを定期的にチェックし、パーツイメージが変更・追加・削除されている場合に * パーツリストを更新するためのウォッチャー */ private PartsImageDirectoryWatchAgent watchAgent; /** * デフォルトのパーツセット表示名 */ private String defaultPartsSetTitle; /** * 最後に使用したプリセット.
* (一度もプリセットを使用していなければnull). */ private PartsSet lastUsePresetParts; /** * 最後に使用した検索ダイアログ.
* nullであれば一度も使用していない.
* (nullでなくとも閉じられている可能性がある.)
*/ private SearchPartsDialog lastUseSearchPartsDialog; /** * 最後に使用した壁紙情報 */ private WallpaperInfo wallpaperInfo; /** * アクティブなメインフレームを設定する. * @param mainFrame メインフレーム */ public static void setActivedMainFrame(MainFrame mainFrame) { if (mainFrame == null) { throw new IllegalArgumentException(); } activedMainFrame = mainFrame; } /** * 現在アクティブなメインフレームを取得する. * まだメインフレームが開かれていない場合はnull.
* 最後のメインフレームが破棄中、もしくは破棄済みであれば破棄されたフレームを示すことに注意.
* @return メインフレーム、もしくはnull */ public static MainFrame getActivedMainFrame() { return activedMainFrame; } /** * インポートされパーツが増減した可能性がある場合に呼び出される. * @param cd 対象 * @param newCd インポートされたcd * @param caller 呼び出しもとメインフレーム * @throws IOException 例外 */ public static void notifyImportedPartsOrFavorites( final CharacterData cd, final CharacterData newCd, final Component caller ) throws IOException { if (cd == null || newCd == null || caller == null) { throw new IllegalArgumentException(); } if (!cd.isValid() || !newCd.isValid()) { // 変更前もしくは変更後が無効なキャラクターデータであれば // 反映する必要ない. return; } logger.log(Level.FINE, "parts imported for active profiles: " + newCd); if ( !cd.isSameStructure(newCd)) { // キャラクターデータそのものが変更されている場合 notifyChangeCharacterData(cd, newCd, caller); } else { // パーツ構成は変更されていない場合 // Frameのうち、ネイティブリソースと関連づけられているアクティブなフレームを調査 for (Frame frame : JFrame.getFrames()) { if (frame.isDisplayable() && frame instanceof MainFrame) { final MainFrame mainFrame = (MainFrame) frame; if (mainFrame.characterData == null || !mainFrame.characterData.isValid()) { // 無効なキャラクターデータを保持している場合は、そのメインフレームは処理対象外 continue; } SwingUtilities.invokeLater(new Runnable() { public void run() { // パーツ及びお気に入りを再取得する場合. try { Cursor oldCur = mainFrame.getCursor(); caller.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); try { mainFrame.reloadPartsAndFavorites(newCd, true); } finally { mainFrame.setCursor(oldCur != null ? oldCur : Cursor.getDefaultCursor()); } } catch (Exception ex) { ErrorMessageHelper.showErrorDialog(mainFrame, ex); } } }); } } } } /** * キャラクターデータが変更されたことを通知される.
* @param cd キャラクターデータ(変更前) * @param newCd キャラクターデータ(変更後) * @param caller 呼び出しもとコンポーネント(ウェイトカーソル表示用) * @param structureCompatible 構造が同一であるか? * @throws IOException 新しいキャラクターデータのパーツセットのロードに失敗した場合 (メインフレームは変更されていません.) */ public static void notifyChangeCharacterData( final CharacterData cd, final CharacterData newCd, final Component caller ) throws IOException { if (cd == null || newCd == null || caller == null) { throw new IllegalArgumentException(); } if (!cd.isValid() || !newCd.isValid()) { // 変更前もしくは変更後が無効なキャラクターデータであれば // 反映する必要ない. return; } logger.log(Level.FINE, "change active profile: " + newCd); if (!ProfileListManager.isUsingCharacterData(cd)) { // 使用中のプロファイルではないので何もしない. return; } caller.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); try { // キャラクターデータが、まだ読み込まれていなければ読み込む. if (!newCd.isPartsLoaded()) { ProfileListManager.loadCharacterData(newCd); ProfileListManager.loadFavorites(newCd); } // Frameのうち、ネイティブリソースと関連づけられているアクティブなフレームを調査 for (Frame frame : JFrame.getFrames()) { if (frame.isDisplayable() && frame instanceof MainFrame) { final MainFrame mainFrame = (MainFrame) frame; if (mainFrame.characterData == null || !mainFrame.characterData.isValid()) { // 無効なキャラクターデータを保持している場合は、そのメインフレームは処理対象外 continue; } // メインフレームが保持しているキャラクターデータのdocbaseと // 変更対象となったキャラクターデータのdocbaseが等しければ、メインフレームを更新する必要がある. String docbaseOrg = cd.getDocBase().toString(); String docbaseMine = mainFrame.characterData.getDocBase().toString(); if (docbaseOrg.equals(docbaseMine)) { SwingUtilities.invokeLater(new Runnable() { public void run() { try { Cursor oldCur = mainFrame.getCursor(); mainFrame.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); try { // 現在情報の保存 mainFrame.saveWorkingSet(); // 画面構成の再構築 mainFrame.initComponent(newCd); } finally { mainFrame.setCursor(oldCur != null ? oldCur : Cursor.getDefaultCursor()); } } catch (RuntimeException ex) { ErrorMessageHelper.showErrorDialog(mainFrame, ex); } } }); } } } } finally { caller.setCursor(Cursor.getDefaultCursor()); } } /** * お気に入りデータが変更されたことを通知される. * @param cd キャラクターデータ */ public static void notifyChangeFavorites(CharacterData cd) { if (cd == null) { throw new IllegalArgumentException(); } // Frameのうち、ネイティブリソースと関連づけられているアクティブなフレームを調査 for (Frame frame : JFrame.getFrames()) { if (frame.isDisplayable() && frame instanceof MainFrame) { MainFrame mainFrame = (MainFrame) frame; if (cd.getDocBase().equals(mainFrame.characterData.getDocBase())) { mainFrame.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); try { // お気に入りを最新の状態に読み直し mainFrame.refreshFavorites(); } finally { mainFrame.setCursor(Cursor.getDefaultCursor()); } } } } } /** * メインフレームを構築する. * @param characterData キャラクターデータ */ public MainFrame(CharacterData characterData) { try { if (characterData == null) { throw new IllegalArgumentException(); } setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE); addWindowListener(new WindowAdapter() { @Override public void windowClosing(WindowEvent e) { onCloseProfile(); } @Override public void windowClosed(WindowEvent e) { stopAgents(); } @Override public void windowActivated(WindowEvent e) { setActivedMainFrame(MainFrame.this); } @Override public void windowOpened(WindowEvent e) { // do nothing. } }); // アイコンの設定 icon = UIHelper.getInstance().getImage("icons/icon.png"); setIconImage(icon); // 画面コンポーネント作成 initComponent(characterData); JMenuBar menuBar = createMenuBar(); setJMenuBar(menuBar); // メインスクリーンサイズを取得する. GraphicsEnvironment genv = GraphicsEnvironment.getLocalGraphicsEnvironment(); Rectangle desktopSize = genv.getMaximumWindowBounds(); // メインスクリーンのサイズ(デスクトップ領域のみ) logger.log(Level.CONFIG, "desktopSize=" + desktopSize); Dimension imageSize = characterData.getImageSize(); // 画像サイズ300x400を基準サイズとして、それ以下にはならない. // アプリケーション設定の最大サイズ以上の場合はウィンドウサイズは固定してスクロールバーに任せる AppConfig appConfig = AppConfig.getInstance(); int maxWidth = min(desktopSize.width, appConfig.getMainFrameMaxWidth()); int maxHeight = min(desktopSize.height, appConfig.getMainFrameMaxHeight()); int imageWidth = min(maxWidth, max(300, imageSize != null ? imageSize.width : 0)); int imageHeight = min(maxHeight, max(400, imageSize != null ? imageSize.height : 0)); // 300x400の画像の場合にメインフレームが600x550だとちょうどいい感じ. // それ以上大きい画像の場合は増えた分だけフレームを大きくしておく. setSize(imageWidth - 300 + 600, imageHeight - 400 + 550); // 次回表示時にプラットフォーム固有位置に表示するように予約 setLocationByPlatform(true); } catch (RuntimeException ex) { logger.log(Level.SEVERE, "メインフレームの構築中に予期せぬ例外が発生しました。", ex); dispose(); // コンストラクタが呼ばれた時点でJFrameは構築済みなのでdisposeの必要がある. throw ex; } catch (Error ex) { logger.log(Level.SEVERE, "メインフレームの構築中に致命的な例外が発生しました。", ex); dispose(); // コンストラクタが呼ばれた時点でJFrameは構築済みなのでdisposeの必要がある. throw ex; } } /** * メインフレームを表示する.
* デスクトップ領域からはみ出した場合は位置を補正する.
*/ public void showMainFrame() { // メインスクリーンサイズを取得する. GraphicsEnvironment genv = GraphicsEnvironment.getLocalGraphicsEnvironment(); Rectangle desktopSize = genv.getMaximumWindowBounds(); // メインスクリーンのサイズ(デスクトップ領域のみ) logger.log(Level.CONFIG, "desktopSize=" + desktopSize); // プラットフォーム固有の位置あわせで表示する. // 表示した結果、はみ出している場合は0,0に補正する. setVisible(true); Point loc = getLocation(); logger.log(Level.CONFIG, "windowLocation=" + loc); Dimension windowSize = getSize(); if (loc.y + windowSize.height >= desktopSize.height) { loc.y = 0; } if (loc.x + windowSize.width >= desktopSize.width) { loc.x = 0; } if (loc.x == 0 || loc.y == 0) { setLocation(loc); } // デスクトップよりも大きい場合は小さくする. boolean resize = false; Dimension dim = getSize(); if (dim.height > desktopSize.height) { dim.height = desktopSize.height; resize = true; } if (dim.width > desktopSize.width) { dim.width = desktopSize.width; resize = true; } if (resize) { setSize(dim); } } /** * このメインフレームに関連づけられているエージェントスレッドを停止します.
* すでに停止している場合は何もしません。 */ protected void stopAgents() { // エージェントを停止 if (watchAgent != null) { try { watchAgent.disconnect(); } catch (Throwable ex) { logger.log(Level.SEVERE, "フォルダ監視スレッドの停止に失敗しました。", ex); } watchAgent = null; } // イメージビルダを停止 if (imageBuilder != null) { try { imageBuilder.stop(); } catch (Throwable ex) { logger.log(Level.SEVERE, "非同期イメージビルダスレッドの停止に失敗しました。", ex); } imageBuilder = null; } } /** * メインフレームを破棄します.
*/ @Override public void dispose() { stopAgents(); super.dispose(); } /** * 画面コンポーネントを設定します.
* すでに設定されている場合は一旦削除されたのちに再作成されます.
*/ private void initComponent(CharacterData characterData) { CharacterData oldCd; synchronized (this) { oldCd = this.characterData; if (oldCd != null) { // 使用中のキャラクターデータであることを登録解除する。 ProfileListManager.unregisterUsedCharacterData(oldCd); } this.characterData = characterData; // 使用中のキャラクターデータであることを登録する. ProfileListManager.registerUsedCharacterData(characterData); } // 設定まわり準備 AppConfig appConfig = AppConfig.getInstance(); Properties strings = LocalizedResourcePropertyLoader.getInstance() .getLocalizedProperties(STRINGS_RESOURCE); // タイトル表示 String title; if (Main.isMacOSX()) { // Mac OS Xの場合はウィンドウにタイトルはつけない。 title = ""; } else { title = strings.getProperty("title"); } setTitle(title + characterData.getName()); // デフォルトのパーツセット表示名 defaultPartsSetTitle = strings.getProperty("defaultPartsSetTitle"); // エージェントの停止 stopAgents(); // コンポーネント配置 Container contentPane = getContentPane(); // すでにあるコンポーネントを削除 for (Component comp : contentPane.getComponents()) { contentPane.remove(comp); } // 開いている検索ダイアログを閉じる closeSearchDialog(); PartsColorManager partsColorManager = characterData.getPartsColorManager(); // デフォルトの背景色の設定 Color bgColor = appConfig.getDefaultImageBgColor(); wallpaperInfo = new WallpaperInfo(); wallpaperInfo.setBackgroundColor(bgColor); imageLoader = new ColorConvertedImageCachedLoader(); imageBuilder = new AsyncImageBuilder(imageLoader); partsSelectionManager = new PartsSelectionManager(partsColorManager, new PartsSelectionManager.ImageBgColorProvider() { public Color getImageBgColor() { return wallpaperInfo.getBackgroundColor(); } public void setImageBgColor(Color imageBgColor) { applyBackgroundColorOnly(imageBgColor); } }); colorGroupCoordinator = new ColorGroupCoordinator(partsSelectionManager, partsColorManager); partsColorCoordinator = new PartsColorCoordinator(characterData, partsColorManager, colorGroupCoordinator); PartsImageDirectoryWatchAgentFactory agentFactory = PartsImageDirectoryWatchAgentFactory.getFactory(); watchAgent = agentFactory.getAgent(characterData); previewPane = new PreviewPanel(); previewPane.setTitle(defaultPartsSetTitle); previewPane.addPreviewPanelListener(new PreviewPanelListener() { public void addFavorite(PreviewPanelEvent e) { onRegisterFavorite(); } public void changeBackgroundColor(PreviewPanelEvent e) { if ( !e.isShiftKeyPressed()) { // 壁紙選択 onChangeWallpaper(); } else { // シフトキーにて背景色変更 onChangeBgColor(); } } public void copyPicture(PreviewPanelEvent e) { onCopy(e.isShiftKeyPressed()); } public void savePicture(PreviewPanelEvent e) { if ( !e.isShiftKeyPressed()) { // 画像出力 onSavePicture(); } else { // シフトキーにて「伺か」用出力 onSaveAsUkagaka(); } } public void showInformation(PreviewPanelEvent e) { onInformation(); } public void flipHorizontal(PreviewPanelEvent e) { onFlipHolizontal(); } }); imageSelectPanels = new ImageSelectPanelList(); JPanel imgSelectPanelsPanel = new JPanel(); BoxLayout bl = new BoxLayout(imgSelectPanelsPanel, BoxLayout.PAGE_AXIS); imgSelectPanelsPanel.setLayout(bl); for (PartsCategory category : characterData.getPartsCategories()) { final ImageSelectPanel imageSelectPanel = new ImageSelectPanel(category, characterData); imgSelectPanelsPanel.add(imageSelectPanel); imageSelectPanels.add(imageSelectPanel); partsSelectionManager.register(imageSelectPanel); } imgSelectPanelsPanelSp = new JScrollPane(imgSelectPanelsPanel) { private static final long serialVersionUID = 1L; @Override public JScrollBar createVerticalScrollBar() { JScrollBar sb = super.createVerticalScrollBar(); sb.setUnitIncrement(12); return sb; } }; imgSelectPanelsPanelSp.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); JSplitPane splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, true, imgSelectPanelsPanelSp, previewPane); contentPane.add(splitPane, BorderLayout.CENTER); imgSelectPanelsPanelSp.requestFocus(); ArrayList colorGroups = new ArrayList(); colorGroups.addAll(characterData.getColorGroups()); final ColorChangeListener colorChangeListener = new ColorChangeListener() { public void onColorGroupChange(ColorChangeEvent event) { // do nothing. } public void onColorChange(ColorChangeEvent event) { MainFrame.this.requestPreview(); } }; colorGroupCoordinator.addColorChangeListener(colorChangeListener); for (int idx = 0; idx < imageSelectPanels.size(); idx++) { ImageSelectPanel imageSelectPanel = imageSelectPanels.get(idx); final PartsCategory partsCategory = imageSelectPanel.getPartsCategory(); final ColorDialog colorDialog = new ColorDialog(this, partsCategory, colorGroups); colorGroupCoordinator.registerColorDialog(colorDialog); partsColorCoordinator.register(imageSelectPanel, colorDialog); final int curidx = idx; imageSelectPanel.addImageSelectListener(new ImageSelectPanelListener() { public void onChangeColor(ImageSelectPanelEvent event) { colorDialog.adjustLocation(curidx); colorDialog.setVisible(!colorDialog.isVisible()); } public void onPreferences(ImageSelectPanelEvent event) { // do nothing. (not supported) } public void onChange(ImageSelectPanelEvent event) { MainFrame.this.requestPreview(); } public void onSelectChange(ImageSelectPanelEvent event) { // do nothing. } public void onTitleClick(ImageSelectPanelEvent event) { PartsCategory partsCategory = (event != null) ? event.getImageSelectPanel().getPartsCategory() : null; MainFrame.this.onClickPartsCategoryTitle(partsCategory, false); } public void onTitleDblClick(ImageSelectPanelEvent event) { PartsCategory partsCategory = (event != null) ? event.getImageSelectPanel().getPartsCategory() : null; MainFrame.this.onClickPartsCategoryTitle(partsCategory, true); } }); imageSelectPanel.addAncestorListener(new AncestorListener() { public void ancestorAdded(AncestorEvent event) { } public void ancestorMoved(AncestorEvent event) { } public void ancestorRemoved(AncestorEvent event) { // パネルもしくは、その親が削除されたときにダイアログも非表示とする。 colorDialog.setVisible(false); } }); } // 全パーツのロード partsSelectionManager.loadParts(); // 保存されているワーキングセットを復元する. // 復元できなかった場合はパーツセットを初期選択する. if ( !loadWorkingSet()) { if (showDefaultParts(true)) { requestPreview(); } } // 選択されているパーツを見える状態にする scrollToSelectedParts(); // 非同期イメージローダの処理開始 if (!imageBuilder.isAlive()) { imageBuilder.start(); } // ドロップターゲットの設定 new DropTarget(imgSelectPanelsPanelSp, new FileDropTarget() { @Override protected void onDropFiles(final List dropFiles) { if (dropFiles == null || dropFiles.isEmpty()) { return; } // インポートダイアログを開く. // ドロップソースの処理がブロッキングしないように、 // ドロップハンドラの処理を終了してからインポートダイアログが開くようにする. SwingUtilities.invokeLater(new Runnable() { public void run() { onImport(dropFiles); } }); } @Override protected void onException(Exception ex) { ErrorMessageHelper.showErrorDialog(MainFrame.this, ex); } }); // ディレクトリを監視し変更があった場合にパーツをリロードするリスナ watchAgent.addPartsImageDirectoryWatchListener(new PartsImageDirectoryWatchListener() { public void detectPartsImageChange(PartsImageDirectoryWatchEvent e) { Runnable refreshJob = new Runnable() { public void run() { onDetectPartsImageChange(); } }; if (SwingUtilities.isEventDispatchThread()) { refreshJob.run(); } else { SwingUtilities.invokeLater(refreshJob); } } }); // 監視が有効であれば、ディレクトリの監視をスタートする if (appConfig.isEnableDirWatch() && characterData.isWatchDirectory()) { watchAgent.connect(); } // パーツカテゴリの自動縮小が設定されている場合 minimizeMode = false; if (appConfig.isEnableAutoShrinkPanel()) { onClickPartsCategoryTitle(null, true); } // コンポーネントの再構築の場合 if (oldCd != null) { validate(); } } /** * パーツが変更されたことを検知した場合.
* パーツデータをリロードし、各カテゴリのパーツ一覧を再表示させ、プレビューを更新する.
*/ protected void onDetectPartsImageChange() { try { reloadPartsAndFavorites(null, true); } catch (IOException ex) { logger.log(Level.SEVERE, "parts reload failed. " + characterData, ex); } } /** * すべてのカテゴリのリストで選択中のアイテムが見えるようにスクロールする. */ protected void scrollToSelectedParts() { partsSelectionManager.scrollToSelectedParts(); } /** * 指定したパーツカテゴリ以外のパーツ選択パネルを最小化する. * @param partsCategory パーツカテゴリ、nullの場合は全て最小化する. * @param dblClick ダブルクリック */ protected void onClickPartsCategoryTitle(PartsCategory partsCategory, boolean dblClick) { if (logger.isLoggable(Level.FINE)) { logger.log(Level.FINE, "onClickPartsCategoryTitle category=" + partsCategory + "/clickCount=" + dblClick); } if (dblClick) { minimizeMode = !minimizeMode; if (!minimizeMode) { partsSelectionManager.setMinimizeModeIfOther(null, false); return; } } if (minimizeMode) { if (partsSelectionManager.isNotMinimizeModeJust(partsCategory)) { partsSelectionManager.setMinimizeModeIfOther(null, true); // 全部縮小 } else { partsSelectionManager.setMinimizeModeIfOther(partsCategory, true); if (partsCategory != null) { // 対象のパネルがスクロールペイン内に見える用にスクロールする. // スクロールバーの位置指定などの座標系の操作は「要求」であり、実際に適用されるまで本当の位置は分らない。 // よって以下の処理は非同期に行い、先に座標を確定させたものに対して行う必要がある。 final ImageSelectPanel panel = imageSelectPanels.findByPartsCategory(partsCategory); SwingUtilities.invokeLater(new Runnable() { public void run() { final Point pt = panel.getLocation(); JViewport viewPort = imgSelectPanelsPanelSp.getViewport(); viewPort.setViewPosition(pt); viewPort.revalidate(); } }); } } } } /** * デフォルトパーツを選択する.
* デフォルトパーツがなければお気に入りの最初のものを選択する.
* それもなければ空として表示する.
* パーツの適用に失敗した場合はfalseを返します.(例外は返されません.)
* @param force すでに選択があっても選択しなおす場合はtrue、falseの場合は選択があれば何もしない. * @return パーツ選択された場合。force=trueの場合はエラーがなければ常にtrueとなります。 */ protected boolean showDefaultParts(boolean force) { try { if (!force) { // 現在選択中のパーツを取得する.(なければ空) PartsSet sel = partsSelectionManager.createPartsSet(); if (!sel.isEmpty()) { // 強制選択でない場合、すでに選択済みのパーツがあれば何もしない. return false; } } // デフォルトのパーツセットを取得する String defaultPresetId = characterData.getDefaultPartsSetId(); PartsSet partsSet = null; if (defaultPresetId != null) { partsSet = characterData.getPartsSets().get(defaultPresetId); } // デフォルトのパーツセットがなければ、お気に入りの最初を選択する. if (partsSet == null) { List partssets = getPartsSetList(); if (!partssets.isEmpty()) { partsSet = partssets.get(0); } } // パーツセットがあれば、それを表示要求する. // パーツセットがなければカラーダイアログを初期化するのみ if (partsSet == null) { partsColorCoordinator.initColorDialog(); } else { selectPresetParts(partsSet); } } catch (Exception ex) { logger.log(Level.WARNING, "パーツのデフォルト適用に失敗しました。", ex); return false; } return true; } /** * プリセットを適用しキャラクターイメージを再構築します.
* 実行時エラーは画面のレポートされます.
* @param presetParts パーツセット, nullの場合は何もしない. */ protected void selectPresetParts(PartsSet presetParts) { if (presetParts == null) { return; } try { // 最後に使用したプリセットとして記憶する. lastUsePresetParts = presetParts; // プリセットパーツで選択を変える partsSelectionManager.selectPartsSet(presetParts); // カラーパネルを選択されているアイテムをもとに再設定する partsColorCoordinator.initColorDialog(); // 再表示 requestPreview(); } catch (Exception ex) { ErrorMessageHelper.showErrorDialog(this, ex); } } /** * プリセットとお気に入りを表示順に並べて返す. * @return プリセットとお気に入りのリスト(表示順) */ protected List getPartsSetList() { ArrayList partssets = new ArrayList(); partssets.addAll(characterData.getPartsSets().values()); Collections.sort(partssets, new Comparator() { public int compare(PartsSet o1, PartsSet o2) { int ret = o1.getLocalizedName().compareTo(o2.getLocalizedName()); if (ret == 0) { ret = o1.getPartsSetId().compareTo(o2.getPartsSetId()); } if (ret == 0) { ret = o1.hashCode() - o2.hashCode(); } return ret; } }); return partssets; } /** * お気に入りメニューが開いたとき * @param menu */ protected void onSelectedFavoriteMenu(JMenu menu) { int mx = menu.getMenuComponentCount(); int separatorIdx = -1; for (int idx = 0; idx < mx; idx++) { Component item = menu.getMenuComponent(idx); if (item instanceof JSeparator) { separatorIdx = idx; break; } } // 既存メニューの削除 if (separatorIdx > 0) { while (menu.getMenuComponentCount() > separatorIdx + 1) { menu.remove(separatorIdx + 1); } } // 表示順にソート List partssets = getPartsSetList(); // メニューの再構築 MenuBuilder menuBuilder = new MenuBuilder(); for (final PartsSet presetParts : partssets) { JMenuItem favoriteMenu = menuBuilder.createJMenuItem(); favoriteMenu.setName(presetParts.getPartsSetId()); favoriteMenu.setText(presetParts.getLocalizedName()); if (presetParts.isPresetParts()) { Font font = favoriteMenu.getFont(); favoriteMenu.setFont(font.deriveFont(Font.BOLD)); } favoriteMenu.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { selectPresetParts(presetParts); } }); menu.add(favoriteMenu); } } /** * ヘルプメニューを開いたときにお勧めメニューを構築する. * @param menu */ protected void onSelectedRecommendationMenu(JMenu mnuRecomendation) { // 現在のお勧めメニューを一旦削除 while (mnuRecomendation.getMenuComponentCount() > 0) { mnuRecomendation.remove(0); } // お勧めリンクの定義がない場合はデフォルトを用いる.(明示的な空の場合は何もしない.) CharacterDataPersistent persist = CharacterDataPersistent.getInstance(); persist.compensateRecommendationList(characterData); // お勧めリンクメニューを作成する. List recommendations = characterData.getRecommendationURLList(); if (recommendations != null) { MenuBuilder menuBuilder = new MenuBuilder(); for (RecommendationURL recommendation : recommendations) { String displayName = recommendation.getDisplayName(); String url = recommendation.getUrl(); JMenuItem mnuItem = menuBuilder.createJMenuItem(); mnuItem.setText(displayName); mnuItem.addActionListener( DesktopUtilities.createBrowseAction(MainFrame.this, url, displayName) ); mnuRecomendation.add(mnuItem); } } // お勧めリンクメニューのリストがnullでなく空でもない場合は有効、そうでなければ無効にする. mnuRecomendation.setEnabled(recommendations != null && !recommendations.isEmpty()); } /** * 最後に選択されたお気に入りと同じ構成であれば、 * このお気に入りの名前をプレビューペインのタイトルに設定する.
* そうでなければデフォルトのパーツセット名(no titleとか)を表示する.
* 色情報が異なる場合に末尾に「*」マークがつけられる.
* @param requestPartsSet 表示するパーツセット(名前は設定されていなくて良い。お気に入り側を使うので。), nullの場合はデフォルトのパーツ名 */ protected void showPresetName(PartsSet requestPartsSet) { String title = getSuggestPartsSetName(requestPartsSet, true); if (title == null) { title = defaultPartsSetTitle; } previewPane.setTitle(title); } /** * パーツセット名を推定する.
* 最後に選択されたお気に入りと同じ構成であれば、 * このお気に入りの名前を返す.
* お気に入りが選択されていないか構成が異なる場合、お気に入りに名前がない場合はnullを返す.
* @param requestPartsSet 表示するパーツセット(名前は設定されていなくて良い。お気に入り側を使うので。) * @param markColorChange 色情報が異なる場合に末尾に「*」マークをつける場合はtrue */ private String getSuggestPartsSetName(PartsSet requestPartsSet, boolean markColorChange) { String partsSetTitle = null; if (lastUsePresetParts != null && PartsSet.isSameStructure(requestPartsSet, lastUsePresetParts)) { partsSetTitle = lastUsePresetParts.getLocalizedName(); if (markColorChange && !PartsSet.isSameColor(requestPartsSet, lastUsePresetParts)) { if (partsSetTitle != null) { partsSetTitle += "*"; } } } if (partsSetTitle != null && partsSetTitle.trim().length() > 0) { return partsSetTitle; } return null; } /** * プレビューの更新を要求する. * 更新は非同期に行われる. */ protected void requestPreview() { if (!characterData.isValid()) { return; } // 選択されているパーツの各イメージを取得しレイヤー順に並び替えて合成する. // 合成は別スレッドにて非同期に行われる. // リクエストは随時受け付けて、最新のリクエストだけが処理される. // (処理がはじまる前に新しいリクエストで上書きされた場合、前のリクエストは単に捨てられる.) imageBuilder.requestJob(new ImageBuildJobAbstractAdaptor(characterData) { /** * 構築するパーツセット情報 */ private PartsSet requestPartsSet; /** * 非同期のイメージ構築要求の番号.
*/ private long ticket; @Override public void onQueueing(long ticket) { this.ticket = ticket; previewPane.setLoadingRequest(ticket); } @Override public void buildImage(ImageOutput output) { // 合成結果のイメージを引数としてイメージビルダから呼び出される. final BufferedImage img = output.getImageOutput(); Runnable refreshJob = new Runnable() { public void run() { previewPane.setPreviewImage(img); previewPane.setLoadingComplete(ticket); showPresetName(requestPartsSet); } }; if (SwingUtilities.isEventDispatchThread()) { refreshJob.run(); } else { try { SwingUtilities.invokeAndWait(refreshJob); } catch (Exception ex) { logger.log(Level.WARNING, "build image failed.", ex); } } } @Override public void handleException(final Throwable ex) { // 合成中に例外が発生した場合、イメージビルダから呼び出される. Runnable showExceptionJob = new Runnable() { public void run() { ErrorMessageHelper.showErrorDialog(MainFrame.this, ex); } }; if (SwingUtilities.isEventDispatchThread()) { showExceptionJob.run(); } else { SwingUtilities.invokeLater(showExceptionJob); } } @Override protected PartsSet getPartsSet() { // 合成できる状態になった時点でイメージビルダから呼び出される. final PartsSet[] result = new PartsSet[1]; Runnable collectPartsSetJob = new Runnable() { public void run() { PartsSet partsSet = partsSelectionManager.createPartsSet(); result[0] = partsSet; } }; if (SwingUtilities.isEventDispatchThread()) { collectPartsSetJob.run(); } else { try { // スレッドによるSwingのイベントディスパッチスレッド以外からの呼び出しの場合、 // Swingディスパッチスレッドでパーツの選択状態を取得する. SwingUtilities.invokeAndWait(collectPartsSetJob); } catch (InvocationTargetException e) { throw new RuntimeException(e.getMessage(), e); } catch (InterruptedException e) { throw new RuntimeException("interrupted:" + e, e); } } if (logger.isLoggable(Level.FINE)) { logger.log(Level.FINE, "preview: " + result[0]); } requestPartsSet = result[0]; return requestPartsSet; } }); } /** * プロファイルを開く */ protected void onOpenProfile() { try { MainFrame main2 = ProfileListManager.openProfile(this); if (main2 != null) { main2.showMainFrame(); } } catch (Exception ex) { ErrorMessageHelper.showErrorDialog(this, ex); } } /** * 背景色を変更する. */ protected void onChangeBgColor() { getJMenuBar().setEnabled(false); try { Properties strings = LocalizedResourcePropertyLoader.getInstance() .getLocalizedProperties(STRINGS_RESOURCE); Color color = wallpaperInfo.getBackgroundColor(); color = JColorChooser.showDialog(this, strings.getProperty("chooseBgColor"), color); if (color != null) { applyBackgroundColorOnly(color); } } finally { getJMenuBar().setEnabled(true); } } /** * 壁紙を変更する. */ protected void onChangeWallpaper() { try { WallpaperDialog wallpaperDialog = new WallpaperDialog(this); // 最後に使用した壁紙情報でダイアログを設定する. wallpaperDialog.setWallpaperInfo(this.wallpaperInfo); // 壁紙情報を設定するモーダルダイアログを開く WallpaperInfo wallpaperInfo = wallpaperDialog.showDialog(); if (wallpaperInfo == null) { return; } // 壁紙情報を保存し、その情報をもとに背景を再描画する. applyWallpaperInfo(wallpaperInfo, false); } catch (WallpaperFactoryException ex) { ErrorMessageHelper.showErrorDialog(this, ex); } catch (RuntimeException ex) { ErrorMessageHelper.showErrorDialog(this, ex); } } /** * 背景色のみ変更し、背景を再描画する.
* 壁紙情報全体の更新よりも効率化するためのメソッドである.
* @param bgColor 背景色 */ protected void applyBackgroundColorOnly(Color bgColor) { wallpaperInfo.setBackgroundColor(bgColor); previewPane.getWallpaper() .setBackgroundColor(wallpaperInfo.getBackgroundColor()); } /** * 壁紙情報を保存し、その情報をもとに背景を再描画する.
* ignoreErrorがtrueである場合、適用に失敗した場合はログに記録するのみで、 * 壁紙情報は保存されず、壁紙も更新されない.
* @param wallpaperInfo 壁紙情報、null不可 * @param ignoreError 失敗を無視する場合 * @throws IOException 失敗 */ protected void applyWallpaperInfo(WallpaperInfo wallpaperInfo, boolean ignoreError) throws WallpaperFactoryException { if (wallpaperInfo == null) { throw new IllegalArgumentException(); } // 壁紙情報から壁紙インスタンスを生成する. WallpaperFactory wallpaperFactory = WallpaperFactory.getInstance(); Wallpaper wallpaper = null; try { // 壁紙情報の構築時に問題が発生した場合、 // 回復処理をして継続するかエラーとするか? WallpaperFactoryErrorRecoverHandler handler = null; if (ignoreError) { handler = new WallpaperFactoryErrorRecoverHandler(); } // 壁紙情報 wallpaper = wallpaperFactory.createWallpaper(wallpaperInfo, handler); } catch (WallpaperFactoryException ex) { logger.log(Level.WARNING, "壁紙情報の適用に失敗しました。", ex); if ( !ignoreError) { throw ex; } } catch (RuntimeException ex) { logger.log(Level.WARNING, "壁紙情報の適用に失敗しました。", ex); if ( !ignoreError) { throw ex; } } if (wallpaper == null) { return; } // 壁紙を更新する. previewPane.setWallpaper(wallpaper); // 壁紙情報として記憶する. this.wallpaperInfo = wallpaperInfo; } /** * プリビューしている画像をファイルに保存する。 * サポートしているのはPNG/JPEGのみ。 */ protected void onSavePicture() { Toolkit tk = Toolkit.getDefaultToolkit(); BufferedImage img = previewPane.getPreviewImage(); Color imgBgColor = wallpaperInfo.getBackgroundColor(); if (img == null) { tk.beep(); return; } try { // 出力オプションの調整 OutputOption outputOption = imageSaveHelper.getOutputOption(); outputOption.setZoomFactor(previewPane.getZoomFactor()); outputOption.changeRecommend(); imageSaveHelper.setOutputOption(outputOption); // ファイルダイアログ表示 File outFile = imageSaveHelper.showSaveFileDialog(this); if (outFile == null) { return; } logger.log(Level.FINE, "savePicture: " + outFile); logger.log(Level.FINE, "outputOption: " + outputOption); // 画像のファイルへの出力 StringBuilder warnings = new StringBuilder(); setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); try { imageSaveHelper.savePicture(img, imgBgColor, outFile, warnings); } finally { setCursor(Cursor.getDefaultCursor()); } if (warnings.length() > 0) { JOptionPane.showMessageDialog(this, warnings.toString(), "WARNINGS", JOptionPane.WARNING_MESSAGE); } } catch (Exception ex) { ErrorMessageHelper.showErrorDialog(this, ex); } } /** * 伺か用PNG/PNAの出力. */ protected void onSaveAsUkagaka() { BufferedImage img = previewPane.getPreviewImage(); Color bgColor = wallpaperInfo.getBackgroundColor(); if (img == null) { Toolkit tk = Toolkit.getDefaultToolkit(); tk.beep(); return; } try { ukagakaImageSaveHelper.save(this, img, bgColor); } catch (IOException ex) { ErrorMessageHelper.showErrorDialog(this, ex); } } /** * 伺か用PNG/PNAの変換 */ protected void onConvertUkagaka() { try { Color colorKey = wallpaperInfo.getBackgroundColor(); ukagakaImageSaveHelper.convertChooseFiles(this, colorKey); } catch (IOException ex) { ErrorMessageHelper.showErrorDialog(this, ex); } } /** * プロファイルの場所を開く */ protected void onBrowseProfileDir() { if (!characterData.isValid()) { Toolkit tk = Toolkit.getDefaultToolkit(); tk.beep(); return; } try { DesktopUtilities.browseBaseDir(characterData.getDocBase()); } catch (Exception ex) { ErrorMessageHelper.showErrorDialog(this, ex); } } /** * このプロファイルを編集する. */ protected void onEditProfile() { if (!characterData.isValid()) { Toolkit tk = Toolkit.getDefaultToolkit(); tk.beep(); return; } try { CharacterData cd = this.characterData; CharacterData newCd = ProfileListManager.editProfile(this, cd); if (newCd != null) { MainFrame.notifyChangeCharacterData(cd, newCd, this); } } catch (Exception ex) { ErrorMessageHelper.showErrorDialog(this, ex); } } /** * パーツの管理ダイアログを開く.
*/ protected void onManageParts() { if (!characterData.isValid()) { Toolkit tk = Toolkit.getDefaultToolkit(); tk.beep(); return; } PartsManageDialog mrgDlg = new PartsManageDialog(this, characterData); mrgDlg.setVisible(true); if (mrgDlg.isUpdated()) { // パーツ管理情報が更新された場合、 // パーツデータをリロードする. if (characterData.reloadPartsData()) { partsSelectionManager.loadParts(); requestPreview(); } } } /** * 「パーツ検索」ダイアログを開く.
* すでに開いているダイアログがあれば、それにフォーカスを当てる.
*/ protected void openSearchDialog() { if (!characterData.isValid()) { Toolkit tk = Toolkit.getDefaultToolkit(); tk.beep(); return; } if (lastUseSearchPartsDialog != null) { // 開いているダイアログがあれば、それにフォーカスを当てる. if (lastUseSearchPartsDialog.isDisplayable() && lastUseSearchPartsDialog.isVisible()) { lastUseSearchPartsDialog.requestFocus(); return; } } SearchPartsDialog searchPartsDlg = new SearchPartsDialog(this, characterData, partsSelectionManager); searchPartsDlg.adjustLocation(0); searchPartsDlg.setVisible(true); lastUseSearchPartsDialog = searchPartsDlg; } /** * 「パーツ検索」ダイアログを閉じる.
*/ protected void closeSearchDialog() { lastUsePresetParts = null; for (SearchPartsDialog dlg : SearchPartsDialog.getDialogs()) { if (dlg != null && dlg.isDisplayable() && dlg.getParent() == this) { dlg.dispose(); } } } /** * クリップボードにコピー * @param screenImage スクリーンイメージ */ protected void onCopy(boolean screenImage) { try { BufferedImage img = previewPane.getPreviewImage(); if (img == null) { Toolkit tk = Toolkit.getDefaultToolkit(); tk.beep(); return; } if (screenImage) { // 表示している内容をそのままコピーする. img = previewPane.getScreenImage(); } Color imgBgColor = wallpaperInfo.getBackgroundColor(); ClipboardUtil.setImage(img, imgBgColor); } catch (Exception ex) { ErrorMessageHelper.showErrorDialog(this, ex); } } /** * アプリケーションの設定ダイアログを開く */ protected void onPreferences() { AppConfigDialog appConfigDlg = new AppConfigDialog(this); appConfigDlg.setVisible(true); } /** * 新規モードでインポートウィザードを実行する.
*/ protected void onImportNew() { if (!characterData.isValid()) { Toolkit tk = Toolkit.getDefaultToolkit(); tk.beep(); return; } try { // インポートウィザードの実行(新規モード) ImportWizardDialog importWizDialog = new ImportWizardDialog(this, null, null); importWizDialog.setVisible(true); int exitCode = importWizDialog.getExitCode(); if (exitCode == ImportWizardDialog.EXIT_PROFILE_CREATED) { CharacterData cd = importWizDialog.getImportedCharacterData(); if (cd != null && cd.isValid()) { // インポートしたキャラクターデータのプロファイルを開く. setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); try { MainFrame mainFrame = ProfileListManager.openProfile(cd); mainFrame.setVisible(true); } finally { setCursor(Cursor.getDefaultCursor()); } } } } catch (Exception ex) { ErrorMessageHelper.showErrorDialog(this, ex); } } /** * 現在のプロファイルに対するインポートウィザードを実行する.
* インポートが実行された場合は、パーツをリロードする.
* インポートウィザード表示中は監視スレッドは停止される.
* @param initFile アーカイブファィルまたはディレクトリ、指定がなければnull */ protected void onImport(List initFiles) { if (!characterData.isValid()) { Toolkit tk = Toolkit.getDefaultToolkit(); tk.beep(); return; } try { watchAgent.suspend(); try { // インポートウィザードの実行 ImportWizardDialog importWizDialog = new ImportWizardDialog(this, characterData, initFiles); importWizDialog.setVisible(true); if (importWizDialog.getExitCode() == ImportWizardDialog.EXIT_PROFILE_UPDATED) { CharacterData importedCd = importWizDialog.getImportedCharacterData(); notifyImportedPartsOrFavorites(characterData, importedCd, this); } } finally { watchAgent.resume(); } } catch (Exception ex) { ErrorMessageHelper.showErrorDialog(this, ex); } } /** * パーツとお気に入りをリロードする.
* まだロードされていない場合はあらたにロードする.
* 引数newCdが指定されている場合は、現在のキャラクター定義の説明文を更新する.
* (説明文の更新以外には使用されない.)
* * @param newCd * 説明文更新のための更新されたキャラクターデータを指定する。null可 * @param forceRepaint * 必ず再描画する場合 * @throws IOException * 失敗 */ protected synchronized void reloadPartsAndFavorites(CharacterData newCd, boolean forceRepaint) throws IOException { if (newCd != null) { // (インポート画面では説明文のみ更新するので、それだけ取得) characterData.setDescription(newCd.getDescription()); } if ( !characterData.isPartsLoaded()) { // キャラクターデータが、まだ読み込まれていなければ読み込む. ProfileListManager.loadCharacterData(characterData); ProfileListManager.loadFavorites(characterData); partsSelectionManager.loadParts(); } else { // パーツデータをリロードする. if (characterData.reloadPartsData()) { partsSelectionManager.loadParts(); } // お気に入りをリロードする. CharacterDataPersistent persiste = CharacterDataPersistent.getInstance(); persiste.loadFavorites(characterData); notifyChangeFavorites(characterData); } // 現在選択されているパーツセットがない場合はデフォルトのパーツセットを選択する. if (showDefaultParts(false) || forceRepaint) { requestPreview(); } } protected void onExport() { if (!characterData.isValid()) { Toolkit tk = Toolkit.getDefaultToolkit(); tk.beep(); return; } ExportWizardDialog exportWizDlg = new ExportWizardDialog(this, characterData, previewPane.getPreviewImage()); exportWizDlg.setVisible(true); } protected void onResetColor() { if (!characterData.isValid()) { Toolkit tk = Toolkit.getDefaultToolkit(); tk.beep(); return; } Properties strings = LocalizedResourcePropertyLoader.getInstance() .getLocalizedProperties(STRINGS_RESOURCE); if (JOptionPane.showConfirmDialog(this, strings.get("confirm.resetcolors"), strings.getProperty("confirm"), JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE) != JOptionPane.YES_OPTION) { return; } characterData.getPartsColorManager().resetPartsColorInfo(); partsColorCoordinator.initColorDialog(); requestPreview(); } /** * プロファイルを閉じる. */ protected void onCloseProfile() { saveWorkingSet(); ProfileListManager.unregisterUsedCharacterData(characterData); if (characterData.isValid()) { // 最後に使用したキャラクターデータとして記憶する. try { RecentDataPersistent recentPersist = RecentDataPersistent.getInstance(); recentPersist.saveRecent(characterData); } catch (Exception ex) { logger.log(Level.WARNING, "recent data saving failed.", ex); // recent情報の記録に失敗しても致命的ではないので、これは無視する. } } // イメージビルダスレッド・ディレクトリ監視スレッドを停止する. stopAgents(); // フレームウィンドウを破棄する. dispose(); // 破棄されたことをロギングする. logger.log(Level.FINE, "dispose mainframe."); } /** * 開いている、すべてのプロファイルを閉じる.
* (Mac OS Xのcmd+Qで閉じる場合などで使用される.)
*/ public static void closeAllProfiles() { // ウィンドウが閉じられることでアクティブなフレームが切り替わる場合を想定し、 // 現在のアクティブなウィンドウをあらかじめ記憶しておく MainFrame mainFrame = activedMainFrame; // gcをかけてファイナライズを促進させる SystemUtil.gc(); // ファイナライズされていないFrameのうち、ネイティブリソースと関連づけられている // フレームについて、それがMainFrameのインスタンスであれば閉じる. // ただし、現在アクティブなものは除く for (Frame frame : JFrame.getFrames()) { try { if (frame.isDisplayable()) { // ネイティブリソースと関連づけられているフレーム if (frame instanceof MainFrame && frame != mainFrame) { // MainFrameのインスタンスであるので閉じる処理が可能. // (現在アクティブなメインフレームは最後に閉じるため、ここでは閉じない.) ((MainFrame) frame).onCloseProfile(); } } } catch (Throwable ex) { logger.log(Level.SEVERE, "mainframe closing failed.", ex); // フレームを閉じるときに失敗した場合、通常、致命的問題だが // クローズ処理は継続しなければならない. } } // 現在アクティブなフレームを閉じる. // 最後に閉じることで「最後に使ったプロファイル」として記憶させる. if (activedMainFrame != null && activedMainFrame.isDisplayable()) { try { activedMainFrame.onCloseProfile(); } catch (Throwable ex) { logger.log(Level.SEVERE, "mainframe closing failed.", ex); // フレームを閉じるときに失敗した場合、通常、致命的問題だが // クローズ処理は継続しなければならない. } } } /** * 画面の作業状態を保存するユーザーデータを取得する. * @param modeRead 読み込みモード * @return ユーザーデータ */ protected UserData getWorkingSetUserData(boolean modeRead) { return getWorkingSetUserData(characterData, modeRead); } /** * 画面の作業状態を保存するユーザーデータを取得する.
* キャラクターデータがnullまたは有効でない場合はnullを返す.
* @param cd キャラクターデータ * @param modeRead 読み込みモード * @return 作業状態を保存するユーザーデータ、もしくはnull */ public static UserData getWorkingSetUserData(CharacterData cd, boolean modeRead) { if (cd == null || !cd.isValid()) { return null; } UserDataFactory userDataFactory = UserDataFactory.getInstance(); return userDataFactory.getMangledNamedUserData(cd.getDocBase(), "workingset.ser"); } /** * 画面の作業状態を保存する. */ protected void saveWorkingSet() { if (!characterData.isValid()) { return; } try { WorkingSet workingSet = new WorkingSet(); workingSet.setCharacterDocBase(characterData.getDocBase()); workingSet.setCharacterDataRev(characterData.getRev()); PartsSet partsSet = partsSelectionManager.createPartsSet(); workingSet.setPartsSet(partsSet); workingSet.setPartsColorInfoMap(characterData.getPartsColorManager().getPartsColorInfoMap()); workingSet.setLastUsedSaveDir(imageSaveHelper.getLastUsedSaveDir()); workingSet.setLastUsedExportDir(ExportWizardDialog.getLastUsedDir()); workingSet.setLastUsePresetParts(lastUsePresetParts); workingSet.setCharacterData(characterData.duplicateBasicInfo(false)); // パーツセットは保存しない. workingSet.setWallpaperInfo(wallpaperInfo); UserData workingSetStore = getWorkingSetUserData(false); workingSetStore.save(workingSet); } catch (Exception ex) { ErrorMessageHelper.showErrorDialog(this, ex); } } /** * 画面の作業状態を復元する. * @return */ protected boolean loadWorkingSet() { if (!characterData.isValid()) { return false; } try { UserData workingSetStore = getWorkingSetUserData(true); if (workingSetStore != null && workingSetStore.exists()) { WorkingSet workingSet = (WorkingSet) workingSetStore.load(); if (!characterData.getDocBase().equals(workingSet.getCharacterDocBase())) { // ワーキングセットのキャラクターデータとキャラクターデータを定義しているDocBaseが異なる場合はワーキングセットを無視する logger.info("workingset data mismatch:" + characterData); return false; } CharacterData workingCd = workingSet.getCharacterData(); if (workingCd == null) { // ワーキングセットにキャラクターデータが設定されていない場合はREVで比較する.(ver0.96以前旧シリアライズデータ互換用) String docRev = characterData.getRev(); String workRev = workingSet.getCharacterDataRev(); if (docRev == null || workRev == null || !docRev.equals(workRev)) { // ワーキングセットが保存されてからrevisionが変更されていれば無視する. logger.info("workingset revision mismatch: actual=" + characterData + "/workingSet=" + workingSet); return false; } } else if ( !workingCd.isUpperCompatibleStructure(characterData)) { // ワーキングセットにキャラクターデータが設定されており、且つ、構造が一致しない場合は無視する.(ver0.96以降) logger.info("workingset cd-structure mismatch: actual=" + characterData + "/workingSet=" + workingSet); return false; } // 現在のパーツ色情報にワーキングセットで保存した内容を設定する. Map partsColorInfoMap = characterData.getPartsColorManager().getPartsColorInfoMap(); Map workingPartsColorInfoMap = workingSet.getPartsColorInfoMap(); if (workingPartsColorInfoMap != null) { for (Map.Entry entry : workingPartsColorInfoMap.entrySet()) { PartsIdentifier partsIdentifier = entry.getKey(); PartsColorInfo partsColorInfo = entry.getValue(); partsColorInfoMap.put(partsIdentifier, partsColorInfo); } } // 選択されているパーツの復元 PartsSet partsSet = workingSet.getPartsSet(); if (partsSet != null) { partsSet = partsSet.createCompatible(characterData); selectPresetParts(partsSet); // 最後に選択したお気に入り情報の復元 PartsSet lastUsePresetParts = workingSet.getLastUsePresetParts(); if (lastUsePresetParts != null && lastUsePresetParts.hasName() && lastUsePresetParts.isSameStructure(partsSet)) { this.lastUsePresetParts = lastUsePresetParts; showPresetName(lastUsePresetParts); } } // 最後に保存したディレクトリを復元する. imageSaveHelper.setLastUseSaveDir(workingSet.getLastUsedSaveDir()); ExportWizardDialog.setLastUsedDir(workingSet.getLastUsedExportDir()); // 壁紙情報を取得する. WallpaperInfo wallpaperInfo = workingSet.getWallpaperInfo(); if (wallpaperInfo != null) { // 壁紙情報を保存し、その情報をもとに背景を再描画する. // (適用に失敗した場合はエラーは無視し、壁紙情報は保存しない.) applyWallpaperInfo(wallpaperInfo, true); } return true; } } catch (Exception ex) { ErrorMessageHelper.showErrorDialog(this, ex); } return false; } protected void onAbout() { try { AboutBox aboutBox = new AboutBox(this); aboutBox.showAboutBox(); } catch (Exception ex) { ErrorMessageHelper.showErrorDialog(this, ex); } } protected void onHelp() { Properties strings = LocalizedResourcePropertyLoader.getInstance() .getLocalizedProperties(STRINGS_RESOURCE); String helpURL = strings.getProperty("help.url"); String helpDescription = strings.getProperty("help.show"); DesktopUtilities.browse(this, helpURL, helpDescription); } protected void onFlipHolizontal() { if (!characterData.isValid()) { Toolkit tk = Toolkit.getDefaultToolkit(); tk.beep(); return; } double[] affineTransformParameter = partsSelectionManager.getAffineTransformParameter(); if (affineTransformParameter == null) { // 左右フリップするアフィン変換パラメータを構築する. Dimension siz = characterData.getImageSize(); if (siz != null) { affineTransformParameter = new double[] {-1., 0, 0, 1., siz.width, 0}; } } else { // アフィン変換パラメータをクリアする. affineTransformParameter = null; } partsSelectionManager.setAffineTransformParameter(affineTransformParameter); requestPreview(); } protected void onSetDefaultPicture() { if (!characterData.isValid()) { Toolkit tk = Toolkit.getDefaultToolkit(); tk.beep(); return; } try { BufferedImage samplePicture = previewPane.getPreviewImage(); if (samplePicture != null) { CharacterDataPersistent persist = CharacterDataPersistent.getInstance(); persist.saveSamplePicture(characterData, samplePicture); } } catch (Exception ex) { ErrorMessageHelper.showErrorDialog(this, ex); } } protected void onInformation() { if (!characterData.isValid()) { Toolkit tk = Toolkit.getDefaultToolkit(); tk.beep(); return; } PartsSet partsSet = partsSelectionManager.createPartsSet(); InformationDialog infoDlg = new InformationDialog(this, characterData, partsSet); infoDlg.setVisible(true); } protected void onManageFavorites() { if (!characterData.isValid()) { Toolkit tk = Toolkit.getDefaultToolkit(); tk.beep(); return; } // お気に入りの状態を最新にリフレッシュする. refreshFavorites(); // お気に入り編集ダイアログを開く ManageFavoriteDialog dlg = new ManageFavoriteDialog(this, characterData); dlg.setVisible(true); if (!dlg.isModified()) { return; } // お気に入りを登録する. try { setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); try { CharacterDataPersistent persiste = CharacterDataPersistent.getInstance(); persiste.saveFavorites(characterData); notifyChangeFavorites(characterData); } finally { setCursor(Cursor.getDefaultCursor()); } } catch (Exception ex) { ErrorMessageHelper.showErrorDialog(this, ex); } } /** * 最新のお気に入りの状態を取り出す.
*/ protected void refreshFavorites() { logger.log(Level.FINE, "refresh Favorites.: " + characterData); try { setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); try { CharacterDataPersistent persiste = CharacterDataPersistent.getInstance(); characterData.clearPartsSets(true); persiste.loadFavorites(characterData); } finally { setCursor(Cursor.getDefaultCursor()); } } catch (Exception ex) { logger.log(Level.WARNING, "can't refresh favorites: " + characterData, ex); } } protected void onRegisterFavorite() { if (!characterData.isValid()) { Toolkit tk = Toolkit.getDefaultToolkit(); tk.beep(); return; } try { // パーツセットを生成 PartsSet partsSet = partsSelectionManager.createPartsSet(); if (partsSet.isEmpty()) { // 空のパーツセットは登録しない. return; } Properties strings = LocalizedResourcePropertyLoader.getInstance() .getLocalizedProperties(STRINGS_RESOURCE); // お気に入りに登録するパーツセットが最後に使用したお気に入りと同じ構成であれば、 // そのお気に入り名を使用する. String initName = getSuggestPartsSetName(partsSet, false); // カラー情報の有無のチェックボックス. JCheckBox chkColorInfo = new JCheckBox(strings.getProperty("input.favoritesColorInfo")); chkColorInfo.setSelected(true); String partsSetId = null; if (initName != null && lastUsePresetParts != null) { partsSetId = lastUsePresetParts.getPartsSetId(); } // 上書き保存の可否のチェックボックス JCheckBox chkOverwrite = new JCheckBox(strings.getProperty("input.favoritesOverwrite")); chkOverwrite.setSelected(partsSetId != null && partsSetId.length() > 0); chkOverwrite.setEnabled(partsSetId != null && partsSetId.length() > 0); // チェックボックスパネル Box checkboxsPanel = new Box(BoxLayout.PAGE_AXIS); checkboxsPanel.add(chkColorInfo); checkboxsPanel.add(chkOverwrite); // 入力ダイアログを開く String name = (String) JOptionPane.showInputDialog(this, checkboxsPanel, strings.getProperty("input.favorites"), JOptionPane.QUESTION_MESSAGE, null, null, initName == null ? "" : initName); if (name == null || name.trim().length() == 0) { return; } boolean includeColorInfo = chkColorInfo.isSelected(); if (!includeColorInfo) { // カラー情報を除去する. partsSet.removeColorInfo(); } // 新規の場合、もしくは上書きしない場合はIDを設定する. if (partsSetId == null || !chkOverwrite.isSelected()) { partsSetId = "ps" + UUID.randomUUID().toString(); } partsSet.setPartsSetId(partsSetId); // 名前を設定する. partsSet.setLocalizedName(name); // ファイルに保存 setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); try { CharacterDataPersistent persiste = CharacterDataPersistent.getInstance(); // 現在の最新情報を取り出す. characterData.clearPartsSets(true); persiste.loadFavorites(characterData); // お気に入りコレクションに登録 characterData.addPartsSet(partsSet); persiste.saveFavorites(characterData); notifyChangeFavorites(characterData); } finally { setCursor(Cursor.getDefaultCursor()); } // 最後に選択したお気に入りにする lastUsePresetParts = partsSet; showPresetName(partsSet); } catch (Exception ex) { ErrorMessageHelper.showErrorDialog(this, ex); } } /** * すべての解除可能なパーツの選択を解除する。 */ protected void onDeselectAll() { partsSelectionManager.deselectAll(); } /** * 単一選択カテゴリのパーツの解除を許可する。 */ protected void onDeselectableAllCategory() { partsSelectionManager .setDeselectableSingleCategory( !partsSelectionManager .isDeselectableSingleCategory()); } /** * プレビューのズームボックスの表示制御 */ protected void onEnableZoom() { previewPane.setVisibleZoomBox( !previewPane.isVisibleZoomBox()); } /** * メニューバーを構築します. * @return メニューバー */ protected JMenuBar createMenuBar() { final Properties strings = LocalizedResourcePropertyLoader .getInstance().getLocalizedProperties(STRINGS_RESOURCE); MenuDataFactory[] menus = new MenuDataFactory[] { new MenuDataFactory("menu.file", new MenuDataFactory[] { new MenuDataFactory("file.openProfile", new ActionListener() { public void actionPerformed(ActionEvent e) { onOpenProfile(); } }), new MenuDataFactory("file.savePicture", new ActionListener() { public void actionPerformed(ActionEvent e) { onSavePicture(); } }), new MenuDataFactory("file.ukagaka", new MenuDataFactory[] { new MenuDataFactory("file.saveAsUkagaka", new ActionListener() { public void actionPerformed(ActionEvent e) { onSaveAsUkagaka(); }; }), new MenuDataFactory("file.convertUkagaka", new ActionListener() { public void actionPerformed(ActionEvent e) { onConvertUkagaka(); }; }), }), null, new MenuDataFactory("file.editprofile", new ActionListener() { public void actionPerformed(ActionEvent e) { onEditProfile(); } }), new MenuDataFactory("file.opendir", new ActionListener() { public void actionPerformed(ActionEvent e) { onBrowseProfileDir(); } }), new MenuDataFactory("file.import", new MenuDataFactory[] { new MenuDataFactory("file.importMe", new ActionListener() { public void actionPerformed(ActionEvent e) { onImport(null); }; }), new MenuDataFactory("file.importNew", new ActionListener() { public void actionPerformed(ActionEvent e) { onImportNew(); }; }), }), new MenuDataFactory("file.export", new ActionListener() { public void actionPerformed(ActionEvent e) { onExport(); }; }), new MenuDataFactory("file.manageParts", new ActionListener() { public void actionPerformed(ActionEvent e) { onManageParts(); } }), new MenuDataFactory("file.preferences", new ActionListener() { public void actionPerformed(ActionEvent e) { onPreferences(); }; }), null, new MenuDataFactory("file.closeProfile", new ActionListener() { public void actionPerformed(ActionEvent e) { onCloseProfile(); } }), }), new MenuDataFactory("menu.edit", new MenuDataFactory[] { new MenuDataFactory("edit.search", new ActionListener() { public void actionPerformed(ActionEvent e) { openSearchDialog(); } }), new MenuDataFactory("edit.copy", new ActionListener() { public void actionPerformed(ActionEvent e) { onCopy((e.getModifiers() & ActionEvent.SHIFT_MASK) != 0); } }), new MenuDataFactory("edit.flipHorizontal", new ActionListener() { public void actionPerformed(ActionEvent e) { onFlipHolizontal(); } }), new MenuDataFactory("edit.resetcolor", new ActionListener() { public void actionPerformed(ActionEvent e) { onResetColor(); } }), null, new MenuDataFactory("edit.setDefaultPicture", new ActionListener() { public void actionPerformed(ActionEvent e) { onSetDefaultPicture(); } }), new MenuDataFactory("edit.information", new ActionListener() { public void actionPerformed(ActionEvent e) { onInformation(); } }), null, new MenuDataFactory("edit.deselectall", new ActionListener() { public void actionPerformed(ActionEvent e) { onDeselectAll(); } }), new MenuDataFactory("edit.deselectparts", true, new ActionListener() { public void actionPerformed(ActionEvent e) { onDeselectableAllCategory(); } }), new MenuDataFactory("edit.enableAutoShrink", true, new ActionListener() { public void actionPerformed(ActionEvent e) { onClickPartsCategoryTitle(null, true); } }), null, new MenuDataFactory("edit.enableZoomBox", true, new ActionListener() { public void actionPerformed(ActionEvent e) { onEnableZoom(); } }), null, new MenuDataFactory("edit.changeBgColor", new ActionListener() { public void actionPerformed(ActionEvent e) { onChangeBgColor(); } }), new MenuDataFactory("edit.changeWallpaper", new ActionListener() { public void actionPerformed(ActionEvent e) { onChangeWallpaper(); } }), }), new MenuDataFactory("menu.favorite", new MenuDataFactory[] { new MenuDataFactory("favorite.register", new ActionListener() { public void actionPerformed(ActionEvent e) { onRegisterFavorite(); } }), new MenuDataFactory("favorite.manage", new ActionListener() { public void actionPerformed(ActionEvent e) { onManageFavorites(); } }), null, }), new MenuDataFactory("menu.help", new MenuDataFactory[] { new MenuDataFactory("help.recommendations", (ActionListener) null), null, new MenuDataFactory("help.help", new ActionListener() { public void actionPerformed(ActionEvent e) { onHelp(); } }), new MenuDataFactory("help.forum", DesktopUtilities.createBrowseAction( MainFrame.this, strings.getProperty("help.forum.url"), strings.getProperty("help.forum.description")) ), new MenuDataFactory("help.bugreport", DesktopUtilities.createBrowseAction( MainFrame.this, strings.getProperty("help.reportbugs.url"), strings.getProperty("help.reportbugs.description")) ), new MenuDataFactory("help.about", new ActionListener() { public void actionPerformed(ActionEvent e) { onAbout(); } }), }), }; final MenuBuilder menuBuilder = new MenuBuilder(); JMenuBar menuBar = menuBuilder.createMenuBar(menus); menuBuilder.getJMenu("menu.edit").addMenuListener(new MenuListener() { public void menuCanceled(MenuEvent e) { // do nothing. } public void menuDeselected(MenuEvent e) { // do nothing. } public void menuSelected(MenuEvent e) { menuBuilder.getJMenuItem("edit.copy").setEnabled(previewPane.getPreviewImage() != null); menuBuilder.getJMenuItem("edit.deselectparts").setSelected( partsSelectionManager.isDeselectableSingleCategory()); menuBuilder.getJMenuItem("edit.enableAutoShrink").setSelected(minimizeMode); menuBuilder.getJMenuItem("edit.enableZoomBox").setSelected(previewPane.isVisibleZoomBox()); } }); final JMenu mnuFavorites = menuBuilder.getJMenu("menu.favorite"); mnuFavorites.addMenuListener(new MenuListener() { public void menuCanceled(MenuEvent e) { // do nothing. } public void menuDeselected(MenuEvent e) { // do nothing. } public void menuSelected(MenuEvent e) { onSelectedFavoriteMenu(mnuFavorites); } }); // J2SE5の場合は「パーツディレクトリを開く」コマンドは使用不可とする. if (System.getProperty("java.version").startsWith("1.5")) { menuBuilder.getJMenuItem("file.opendir").setEnabled(false); } // お勧めサイトメニュー構築 final JMenu mnuRecomendation = menuBuilder.getJMenu("help.recommendations"); JMenu mnuHelp = menuBuilder.getJMenu("menu.help"); mnuHelp.addMenuListener(new MenuListener() { public void menuCanceled(MenuEvent e) { // do nothing. } public void menuDeselected(MenuEvent e) { // do nothing. } public void menuSelected(MenuEvent e) { onSelectedRecommendationMenu(mnuRecomendation); } }); return menuBar; } } charactermanaj/unitTest/0000755000175000017500000000000011767047443015565 5ustar paulliupaulliucharactermanaj/unitTest/charactermanaj/0000755000175000017500000000000011767047443020530 5ustar paulliupaulliucharactermanaj/unitTest/charactermanaj/model/0000755000175000017500000000000011767047443021630 5ustar paulliupaulliucharactermanaj/unitTest/charactermanaj/model/prop1.xml0000644000175000017500000000022011767047443023405 0ustar paulliupaulliu 1 charactermanaj/unitTest/charactermanaj/model/AppConfigTest.java0000644000175000017500000000665111767047443025211 0ustar paulliupaulliupackage charactermanaj.model; import java.awt.Color; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; import java.net.URL; import java.util.Properties; import charactermanaj.util.BeanPropertiesUtilities; import junit.framework.TestCase; public class AppConfigTest extends TestCase { public static class Bean1 { private int val1; private String val2; private Color val3; private boolean val4; public int getVal1() { return val1; } public void setVal1(int val1) { this.val1 = val1; } public String getVal2() { return val2; } public void setVal2(String val2) { this.val2 = val2; } public Color getVal3() { return val3; } public void setVal3(Color val3) { this.val3 = val3; } public boolean isVal4() { return val4; } public void setVal4(boolean val4) { this.val4 = val4; } @Override public String toString() { return val1 + ":" + val2 + ":" + val3; } public String getX() { throw new UnsupportedOperationException(); } public void setY() { throw new UnsupportedOperationException(); } } public void test1() throws Exception { Properties prop = new Properties(); assertTrue(prop.isEmpty()); URL[] urls = new URL[] { getClass().getResource("prop1.xml"), getClass().getResource("prop2.xml"), }; for (URL url : urls) { assertTrue(url != null); InputStream is = url.openStream(); try { prop.loadFromXML(is); } finally { is.close(); } } assertTrue(prop.size() == 2); System.out.println(prop); } public void test2() throws Exception { Bean1 o = new Bean1(); o.setVal1(123); o.setVal2("abc"); o.setVal3(Color.blue); o.setVal4(true); Properties props = new Properties(); BeanPropertiesUtilities.saveToProperties(o, props); System.out.println(props); assertTrue(props.size() == 4); assertTrue(props.getProperty("val1").equals("123")); assertTrue(props.getProperty("val2").equals("abc")); assertTrue(props.getProperty("val3").equals("#ff")); assertTrue(props.getProperty("val4").equals("true")); Bean1 o2 = new Bean1(); BeanPropertiesUtilities.loadFromProperties(o2, props); System.out.println(o2); assertTrue(o2.getVal1() == 123); assertTrue(o2.getVal2().equals("abc")); assertTrue(o2.getVal3().equals(Color.blue)); assertTrue(o2.isVal4()); } public void test3() throws Exception { Properties props1 = new Properties(); AppConfig appConfig = AppConfig.getInstance(); BeanPropertiesUtilities.saveToProperties(appConfig, props1); String val1 = props1.toString(); System.out.println(val1); BeanPropertiesUtilities.loadFromProperties(appConfig, props1); Properties props2 = new Properties(); BeanPropertiesUtilities.saveToProperties(appConfig, props2); String val2 = props1.toString(); System.out.println(val2); assertTrue(val1.equals(val2)); ByteArrayOutputStream bos = new ByteArrayOutputStream(); props2.storeToXML(bos, "appConfig.xml"); bos.close(); Reader rd = new InputStreamReader(new ByteArrayInputStream(bos.toByteArray()), "UTF-8"); int ch; while ((ch = rd.read()) != -1) { System.out.print((char) ch); } rd.close(); appConfig.saveConfig(); } } charactermanaj/unitTest/charactermanaj/model/CharacterDataFactoryTest.java0000644000175000017500000000262511767047443027356 0ustar paulliupaulliupackage charactermanaj.model; import java.io.File; import junit.framework.TestCase; public class CharacterDataFactoryTest extends TestCase { public void testLoad() throws Exception { // CharacterDataPersistent cf = CharacterDataPersistent.getInstance(); // File baseDir = new File("./characters/default/character.xml"); // CharacterData cd = cf.loadProfile(baseDir.toURL()); // PartsDataLoader loader = new PartsDataLoader(baseDir); // cd.appendCharacterDataChangeListsner(new CharacterDataChangeListener() { // public void characterDataChange(CharacterDataChangeEvent e) { // System.out.println(e.getPartsIdentifier().getPartsCategory() // .getLocalizedCategoryName() // + ":" // + e.getPartsIdentifier().getLocalizedPartsName() // + ":" + e.getMode()); // } // }); // cd.loadPartsData(loader); // System.out.println("*2nd"); // cd.loadPartsData(loader); } public void testSave() throws Exception { // CharacterDataPersistent cf = CharacterDataPersistent.getInstance(); // CharacterData cd = cf.load(new File("./characters/default2/character.xml").toURL()); // // cf.save(cd, new File("./characters/default2")); } public void test1() throws Exception { File d = new File("a").getAbsoluteFile(); System.out.println(d); File f = new File(d, "b/c/d").getCanonicalFile(); System.out.println(f.isAbsolute() + ":" + f); } } charactermanaj/unitTest/charactermanaj/model/prop2.xml0000644000175000017500000000022011767047443023406 0ustar paulliupaulliu 2 charactermanaj/resources/0000755000175000017500000000000011767047556015765 5ustar paulliupaulliucharactermanaj/resources/_HOW_TO_LOCALIZE.txt0000644000175000017500000000137411767047556021213 0ustar paulliupaulliuローカライズしたい言語ごとにリソースファイルをコピーしてください。 Please copy the resource file for each language you want to localize. ファイル名の末尾は言語コードを指定する必要があります。 The end of the file name must be specified language code. http://www.loc.gov/standards/iso639-2/php/English_list.php 例 (Example) mainframe_ja.xml : 日本語 Japanese mainframe_en.xml : 英語 English mainframe_pt_BR : ブラジル Brazil mainframe_zh_TW : 台湾 Chinese mainframe.xml : デフォルト default * 作成したリソースファイルを募集しております。 We are looking for a resource that you created. http://sourceforge.jp/forum/forum.php?forum_id=23932 charactermanaj/resources/menu/0000755000175000017500000000000011767047553016726 5ustar paulliupaulliucharactermanaj/resources/menu/menu.xml0000644000175000017500000002063411767047553020421 0ustar paulliupaulliu File F false Edit E false Favorites A false Help H false Open Profile O false ? N Save the picture S false ? S Ukagaka U false Save as Ukagaka S false ? alt S Convert C false Browse this parts data B false ? D Edit this profile E false Manage the Parts Collection M false ? P Import I false Import to this profile I false Create New Profile N false Export E false Preference C true Close profile X false ? W Find F false ? F Copy to the clipboard C false control C Flip Horizontal H false Reset Colors R false ? R Set As Sample picture P false Information I false ? I Deselect all false All category is Deselectable false Auto shrink category panels false Zoom Z false Background color B false Background Setup W false Add Favorite A false ? B Manage Favorite M false ? M Help H false Report Bugs R false Forum F false About A true Recommended link false charactermanaj/resources/menu/menu_ja.xml0000644000175000017500000001560711767047553021077 0ustar paulliupaulliu ファイル F (F) 編集 E (E) お気に入り A (A) ヘルプ H (H) プロファイルを開く O (O) 画像を保存する S (S) 伺か用に保存する U (U) false 伺か用PNG/PNAの保存 S (S) false 32ビット透過PNGを伺か用PNG/PNAに変換する C (C) false パーツフォルダを開く D (D) false ? D このプロファイルを編集する E (E) false パーツの管理 M (M) インポート I (I) このプロファイルにインポートする I (I) 新規プロファイルを作成する N (N) エクスポート E (E) 設定 C (C) プロファイルを閉じる X (X) パーツの検索 F (F) 画像をクリップボードにコピーする C (C) 画像を左右反転する H (H) 色情報のリセット R (R) false サンプルピクチャに設定する P (P) false 情報 I (I) パーツの選択を解除する 単一選択カテゴリのパーツを解除可能にする カテゴリパネルを切替え時に縮小する false ズーム Z (Z) false 背景色の変更 B (B) false 背景の設定 W (W) false お気に入りに追加 A (A) お気に入りの管理 M (M) ヘルプ H (H) バグレポート R (R) フォーラム F (F) CharacterManaJについて A (A) お勧めサイト charactermanaj/resources/logging.properties0000644000175000017500000000040111767047556021524 0ustar paulliupaulliu# for Debug #handlers=java.util.logging.ConsoleHandler,charactermanaj.util.ApplicationLogHandler #java.util.logging.ConsoleHandler.level=FINEST #.level=FINE handlers=charactermanaj.util.ApplicationLogHandler .level=INFO charactermanaj.level=INFO charactermanaj/resources/preview.png0000644000175000017500000013240211767047556020156 0ustar paulliupaulliuPNG  IHDR,u]{ IDATxipdurV(^Ujfl!9,RԘBc٣#=xq1#Kqh!*n^@$g^xKebPj0{Oy7ۧǟy%CxO F"ǿxRV0w|u*?u8ֻ}n!<`939͗kF7]q'q4qB*7L3_p7OJ>"Э%a\!mwB+33?k r|cϥRB[׏ 'RT,|:/PB!Pm67VZXh_illm hx:O+Aeۆi6%IojwW*XZF9L >o嗟焠k "\T^#p!yȻ}!G,NLۆ"I͛+Wo-.^kZbpD. .|ٳgO PO, DBtwbp.9eTuDU|<ں@p8ȌBR(߸֟?^S)47PB Y^7V+9a}{' ANr2)!zx<>98O>s3 ˂8!TLbY,J̮zMJ a<8@rBQ]F>~rT{72(ȥt\6Lsg{PGDTu~3,eiۈG"0=}(t<D4x 1UiY0, n5 ĩ1$QRp/dQD볳xbz":~0B*ݲ0ɡ!mzh|vshW~C˄>ؗK?0_v;; h6\.1L`mv@)fWVl9w!K(j8;9|*öA:˹8aPjTYF2SƊ3? PWeoNf;K(X(O=ϟ:}zB-Jss fT kkA"JI|_MJ.n//cP3g`$a]Nb5jg''Q؎(H7ޅ,I ^~+3" m d0ͽך!yBz  pu~k[[%E1lƐK$PdIj6T /cx i8{NB n.-A7Mt{$Ir]b: UĐsNJb., `qRitzxsx ey@Bzpfb_z:ޮɍETmܸwDXߵ#c$db`f kk&HD`-oɠJ!ֈP% 'ܗOǷoy #ȏm1ivRL{%"GFůDU"Բ[,+~B'D4L2<{ر-EBX.\sM,!L"LgCL v>7d4+l{-%Oֻ#ϣ`Te;G*I(jkaVC\4[["I(;΍qb++0l3 )SCC \6 Ktݰ'!`_/wϝ=k@8,FB*C2#o͑h$R: Ƚl(R2#EK4V$j4p{i ##ȧR((ŏo7|$!cmk w߇$q"f4U%􁝢Xoԕ91:Ա_&%iUca#@ϟN( .PFs b^'xȢ<⚆Loy[2s*br[$zy. &We,85>X$Q b~m LEAʵ@$Ȣ\2k4KKHh$}~*d +Ό<53: C2ćAH@,.$?:)~ܹ.}&iWggvK$O7XX_eۤlb8w/oj Nb,Zgfplpps VVu}#(2^y h\2 q9,A*E*8ोA)>z zE-GP 0"cX\VVzukNfv1`?yW y,8htrPx s}d$DA Ӷ1R(ݔnRT]DU5砄 nvaY]l6R(>y ?CC-B,/ssSS2lX 書R&h$FU*8Lۆ$Hy95Rt s856s!|8wںO<}띞s(E&Prɱi,BK׹eF[k]hRJ}BL(X1HOsnv>!qx.cVs]4/UU"覉Jzb&Y1.>ێYprt.np!®%E0o_ UD0r,c0c Ytۘ_]E6Q00Sccz 9xɁW=s_Ɛń\2b(Xo{~턼sdgONNUM mk"p#a|ޏb%m]G8Hh/IP.tU#]:&H!_A݆8HDp,̭br`xKPıc`V ui)aYh:r#M&J%dIޅ88>< Q\.@TMXd!p>Z@8GD>f!&th^ͳݢ `qcí+V0X,Qr]q7Q9BQb1pNǭdSUHFKK>W b0B0^,$FyHKiQCр$Pх583+2"ny~H>+_el0, OD]ÈήĄֻ?u.:R&Jq Q#敨xП_^tVmASU 868D4t<RȢ"G`6\*d,;++_[{KL"~>@[vzn !0mk p\ƙ]C"9Ip6닋iٞ!", w PEN|Jj_ ?C6J)jJ4U& rw^g6TI`6&Ȳg?ýJ=@_hUo癑 rpCA@1F,,aXa6%ZcHFB>~nuĄ.]8}ݲ* bZ 6uȢz}J4>u Hma ]]\Ho޽Z'z5 vZ (bq}X [* w^`0N1hb*DY>D˷7 [^{n sXWN/>͘IZd0 i;n.-a mvp/B,|'qnb-|_vM94EA)٭?0(G"h:#J!"FUX V x]:9˒-0LNQ#xdh}Y!CKzF>~˻8|*E|v-%ossxcve@{ 1O=@Et{\{^< .;,dQH)//,I$v|n. ࡢ8rd&} ֻ@1~C33'j`!!(]\Fd43896b|y|\䥎?))~'D4b:-4;ȒZFfmKB08#Cz?ȥR=ցBv ֻ .~zFHضfi (fݺ!|w8ûU`$ǎAu h;wpocJ̓0ƐO&wml68}xQ!GѨCPa.|3 n$QJ++xk~wWWqaj ϝ= pQHqqz5>Da0J9DQD^Gײv}dCE f}6\ "Gp&L2Fj=ܿwfⅲDb(!iXt -/½B*w]!ht:HbzH;ϋs񁁏 !#wR&̓'Oo mm,j6 +v~yXrwWMǎF(x#cH㨷Zf1MH)\BQo". @6{ID}`-JARU,9"=̴mv񨁩> kkkmӗDZ ^_(5!"_,B:y۰HipޘzX:LJTd~\s5 ùZ-4;$YPk셵ABNXK񡙙/qY o7; MMi>4:.BF$EAs8ǶaFp, iYבAzDxp< Q(_v|Ȃ|o-,9\D[[H!bD~ d,e/|.&Z 812Weq,onD^п41]Tc,$wI&.|d}F]^ N"xeYzx: A("HOhV?q'`eI;mC4!2DoI['H((T: R[Q013d"E Z:*xT4FӗcA>ƋEL aesNz/WwA A>Š&xAöL~OB;|2YEnM(X?y|x8iQ9 !n7VH"q òmTewk]`o% 2Ţ bcyRH9׍ 0aYHC(Ѣ(!h6p8h܄QaMܣgAm4`Ƽ@m]F\H.?`wUaz ӄnE"HF(}[blF c&={1U}m {$<3noD"=cZ[.(4ܿQQDM%9eYhV6^~t'ȓyR lT*7vwSj5V^Ly;!L8,*L5ļxh6:^+\2yМc|>4`L ~̙ C/'0:2p;w@4$v)1hZAn%ĝ-8c R6n>zET(ERA}m u ̯"8!HhF 5?f Jퟏq]]*HbXTYXwL&x]O9t,v;`.wNc&`LJ-5q HN> 9Nss8s̎ ]MU[=B`[ltM0] IDATۆ,Ie1؆nfE߾;ظ{JX @$9?#r(HmX mc@.i۸I1l<97ODɊD UQ098<?5ǰ,wBmcht9$id30Ƃd7!RXEE޼ބ8[pl;hB(uRDIMs8u4;//ի(߾իW書:6fgQ[YAV%z*&*"խ-,nl@""k5ܽDvNCW*% D݃ ?HۺoڝNt_"M$ec ۈǟ<&2ΑFDhaXLoy״GaqR6{NŢm:HH(Xo'O:i( p{F5qm]w~S)US)C6Lscau`Yh{c,AZEۅ*I0L %Ǐ|_vMbQ0\.C7 >;jacۺLV oj>t(6IyO/kj&*F_6jULFӧ19+864/\˫%4 n] V$4 C. ahnyCFXoR[zj7Bƿ I/SS( Jc*&Z..UA{ { W! k7cP$ ,cPW> 4]?s,=8g<=y@9YFj~qDb@Ӈ>0z(e2gNM_LF2='w`Ge^kX@<QdzgR,#BSXzAT |W0R(`fdm) rf UnD10Ӳ`&aJ)tF&7ݶ/R00Z(@S~ `BPױl"Jkefqm~:q"WxOaYl40M ڎ(HD"tHܮwVVıc{>[KK}G/\8GR " d!HbAaX4]8>gYOSBh?7t`6n//c}+*Jt x7/"Qocnem]w=ch7~{' vs9h^+VE:wKUBe،!na1UUc1qn8PU.⚆JMo832|X.0mH(T,a[ p!"s0uHBzxw)[7~!^"p|x_öEUKPR{ D5 Ї+WQׯl"zkxmf|,LA!hwUt;x<UtQJFqcaA;7݆c $#Hm;++>iK/A$ Ap#RO}TULsb:}^ء&#&i񓣣?jJ܉6iB~˺]GT P>Eh{YjOO_4"^4A)< }r$MM:XVcIs;=巙ir ݺ!Mh4xuxF// m%u%H(&z D+{wS O?4 ޘC, ˿("GwB9B)=qu$4D4;Bv;Eoa}9ǩQHV+ o1a9[%)s`w%?98g`;~_/)u\EV67w]#17訷A0X4 Mpgq!I0Ơ*R޲v8w 8+Ya;.]B! n8s v.l<BPlr!{'4c6Nòl]ںPB Q??y| .J圣@eLMd ;{l7;:^ouBbA4ȢRJm6B_(mq=؞'৞8W{/$p>SOd!Օ #Hy+%yy~sE"(s>-q[${P(!ȧR) B)LL\ e)_!yBD&LrR1,AZ7!~BPkw0Y,`'Xp i'!V_kBdfs6-+8G(!*تn[cNJɢF벐gKvv}q_c('6~|@ (xմp/@*,I'ȱAG`?["dIB6 FsVERBp]ddP>$-'T!FEE({,y,O(xiyw@f'1gs Y"wSIPDYPv!E"}u~+-jN> `!#>'p/eǁ,InO)#z0aðL X-?I)u[lBR 㸻@('wc}w?ښ( rkgEI_- vf/aAFDG "6:{LE"{o;nTU4L^uL iv7s{Nj$9P$N۝b(2mODQv]5$Upj1Mq~tֵ`!:yB@(݁yr/E";Dݱt#gN|g^_b#0MX x48}geX2?8TE<ց>`uDL*ANx)?87;.9'h -n zl{ M@fQBY]ƃBdo-(ZV('b~m/j"K[l:>2;QQJ\ms c n+eJ!HMt{l_; ]w'ꋴ(g7,foFJINt`PE4;X?wl/~B&hT mw79z=[Ѵݿ?kf3 9q oݽX.SBAcR\91 w]#{Xm).!I$X:sΡJҎa̍ ›67N?@:rZ1hÎoim-TcF(XGDRFcތ<gϏ^tsg)^pΡ{!|zw/ ^wǡPh(nAsq_!ȧÝrU`+k:-4-&$Q ZK=t#v}"( {o  ('v`SCHۢ2J!LJ[(PE"[KK?F]`RIG?9HcAw7w?t#ZV67iAFΧ>܆*ː% LxύzEyb'T~Da=7$>D?yhR1(};5! DZXAa8> ȧRC~^A` 0-P9rYcGV HM09B0=r Y1>(9dQz$(BQ\:y kPI\M-,^_'M0RQ-F_iϮ~{~,=> uȒD'8̓}.!t I. 0h`zDԛp`p{sK/^<0ƐF1ц@)*",9n 'ʽ-Ywi; Vﱫp"bb(tӄY{MeathЉ"$!.zsP~R}HǣرJܥ~~t} c :Ё>u-e2#K"qN &cu]OUB׃Fu* 1(&>QZ-|L8JQL|8+4j!DguEEXY_$OQeyJvoTIbe Å*J0a (BnSO^IVQvmoRǔcYh&꺁mt e=``DX+GF/1d1dߏ~dB2M1:}@m; Dܮ> o70Z,f Cep$Bp|| vzĝe(8 rmo I?H: ò~bΟt4a&886޸;م%,nlhRin(!eXH&ޘC1,IHb0 F {/߻aFb񞏩in-o>"#ɒD?u/w h˳08ܝ[$z)ݸ{k={19GK7'EE"E6@BVGǶ0A DAp~fV068MX\x+o㉡1w]!ZGst:o?zn/`emGx{G"# LEU50xr<0x2r]8CPE ,;ymȳ^k,j((*89:=ays`~SS!3Oݯቩcι-jULN  tfW~gl}VϹI_?Ipbd?s+HQxQiw~ض^xR wP_4TX\rynu]|ӟ?yZ0`YmuwsdIw nzL<7֥t,w;T¤ŊtWV^o#ʲf9ΟzNdAȽf$ /=sbjpEFܸaIh",.ܭ~s|MB>. IDAT۳, BDP(F0EQnQ* O[??ùSR( ȧ%sȥr(eJ&Pp`}OiBdLMMr,p:QJ,!zU,$p(###J=0߯5> nt DA%AFqdq$Qtt[5pJAXM{ ʵ6?@.gqD(ũiz&<<ު}QKj"[[HbHr؁'@C #?H23$ B9rv;Mv[ڒz_Gu[i^TuۯGCZnR/=/^w=,\Nj!|WOQ@0™VinŮ 'ˈ׭ſ|\Jhu:"G9wH{D ӋPx!xqqN ͌zePUVZj V@E˔y1V,!PAq?n8Ln+F@)EQn--/;nA08!fȂ%Lz1y8.7 RlǃPdqFc?lGO) r^ۇb˿u;s߱kGS/`qB1 W=,jEPձ Gvv6 u/ؑc?@O_v7F A::Z Nl"ǐc~?##0b-x2<Յk-C0h`96T0ce,X9++/G rPK.M{pskM(bA&JuwRP0gg2.^|^7pJ2Bb[E?H\UQՓ0J8YFL&w?ǰVV*u8Ca(0\ @$EI\$ɺHɽ &5J0t#T(8NyY!ekt^ r&ocR#ŸNp hT^wAv6=bB!`c#-@ 7^ePAFkoǞ`pG jqgo1X9cq1?``iP(VhZ#OaըWr,ŽhaqbPp X-12i)T!E}.n_)6~GKJI_׷|cY E6=I.cd >-)P((Q%UxJ& ,%1jV+/@kfj ]q^}=h4pʨ`ra؉ շn U߿PsͭP˻M:<(1 1KV)999P0M(+*Vņu`Avqc钥(4kBTAA}%K `y51NJ3B\qR9+kَC. ;[αN;%u4 ڂ9<`TՅTPZ:z{O #FS kMV2 L#pTR<;p nm_܌PprF|rr=yf$X.̣~jtX!]/⟾r'7|kh!L mYYY(*.eǼy(.ÁV݀ J+Kac>sgɫ>[>?T f9G* |Edɒץg=ϋn&L%-<2l'Zb _ʵ\-.A4AܐVrqy62,b-IgpYZ;(uaA9++P2!JA#9˲5N`ʢGCA^zWnyd ĢEؽ! ]8݃u׭4`:65Ja (v:hE)& p{14UBP|?vpA,3j->6/$.#.4cҘm!fda%JCeF '8 0hJ[8p|0t R)wN,X@[Oqi J+[(üQE؜N(6J)YY #GG@O/ KKp_{-(+*BѭGX qYR\t9уN`O&XT7V{>$f]~\Aǭh=Ԋަ^|`| Tm؆'`X|&0\Nd}Mh/Ql<~ZaSp\ʲB˲,d[9ШT']v7 s%5?gUHx0)7 Ys 3Yœ638E)?8;a^|UUG( EAm!}ρEx׿BmrϿCAAn[R R ZP@V˳B^ AB 1VZy5, 82a眑!ݐ<>_J܆8W5&Pd*r:m} 'J1)eȂ5 6io?S^\.m YBAbmX@8^( i;~RңG/Rz'=6\,,6jSAM1KBPl &T 0k`9|_7z,rTI)RޯI lj5͒m-p# :~,;a8`M? T"~f!^?6 ?;}ÄJVpb߾@)ۄ‚Q$OݘCbK?1xT6Fz-!!vӯ7 /i%,.WO6|" $x>zȑIRZZl%u|tT; y梱,@P@)qQ^ ÓP:OIQ5J nupz}PH&ERjݞLJUeY ^J ikAxm٨6LR! i~s{.9$ Kr}^l^3`MQ` ::mbH I"~=xhHi9PI'  D~ӄݣ 0Zd + `M"v[}ソb)Z =4[G rJq0 r z4w.t`@P]K$\!H{#SaeEŇxOyH#bɒ`('ۛF}Ab_Ȃ5@_{{ZFBXZ˵|fRb=xGٌ^ l.s{>l] ?^_X"(^|\,(pe F)X*? bC?c]ŏR egJ)=6B(BPD@0rI Y{)`M2 }|͜Vc4VSmmgi53~Io4eq=⃿ x[a>!yR/CPLjI:!P(KBBCQ/9CA):0`y~vPj*rN$Fd&SmQ;!v{e;L QEA5A\ z# !Bkw  {E-(hg|H(ҿvXkHV$-:l@g * :H} ҭ؟Jż3r3bg$+ #띢PU㻇'$A&LF9;rcfvq2+&@)e:T(,aݓ'2D#KzPQ۩P(,Z[kR,+ NDɰ@V6jhY | fV$g*#^D |maP bJ`jơc@V'|Wrw01.?<|Q㱹|;`M1߽}>W@Kw}H \8.h@%V(qШsTJ1h ;7e^!<΋'jn/hjNͭ;~ =(&ta8fk |J0O0`d {q# sN)fxjmN+X¨բ '' aY ˟aD*@Z!JQ(5ڥ]d^beѳ*R p @yP>PG; 2%4S| CjSx)u:K2 <> *϶S S%]޹k8tl Z-֯~qx< e) * nj{{Oϳo^&ryE7 KÂ*TP@Y-_nxbh$eK0dIJH",5n[ZQ߄Lh +ZV.'QjyC}(ϏqT} G3.'DBR)h48uz}y+2 z^{q#[X3O;V.^ש7ftiZ-d ,-5aPk~3O@+ GŅ33 *Cg, @lo {z@– XSQ ൣxm4Dn]YbŨcǡ]H40<,|p̶]P(WK\,*'vt|H-%YfEEEn9 JtHEOFV6D^{==Xt 9y@IUL<&"XaVC48, ZBa^㒆].~ժ;0p|8 a >"R!u7[z]P^t ZZqb =l뽐k&90'Ջ`}Ja 0r" `Q @LV4h%/ ^A2@g_$k?c;$T mz)jP)Upy}xIWhaařY<sVFK: vtpĿ1"@T35Mn_| %RMMζW9aȂ5PJk/ߜo2RI5$R_DK, twN:1VJ4dHPG|Ǐ!xמW K& 3,#0U^̲,)js,.0,-±,n70csvF a.ȃkV,GqqV.IGbW_ IDATJФ;FN@ho`@k ,ݰ;0((ʂYM6:u !sƵ99Ufa~Nn|XȂ>@<҂Dv*ͷtu;'VY@TVs^/ٳ[3}e# ,aի_JT&mN}ՅNH"*[[1/?*b>C3ꑝgh+a(**FV,W Ŷݒ- lj0l.vlΟ= ^;б (4MAɲ87Hwr8ڊGy y:= ėd#@PTh~*TRRB+SXC^Y ssCPAXNo~PHb\A@P  dk+[Z`0u` e ˲[>C V?%円)2cBY0e }ߦM?^wA/ "Dz8ގaQXL1?&,ˋ&^ v{u`Xg[CN{ 0v++rmL!pjT dsP!ʤ8N!ˤ9?Y/vyԛ·JT~K_:|+}xA&؜Έ+ȱ,AW?vTΛ|'78dǵ˖c L Yf1%yyeWWV>~GU핥BE\8ӃbOVP`c#lv;Lz]U Dz}`3tZ}@;/TUU=Ȗ-[HIIId;/}%͂&QJAJ@vF*R* Amo5\WF$SZPXx/|p_~v6yhnFg8AT!t 87R} N\oZVn3 CTJg srζZ ./3Adt/|5?vy1 a@! !p8pE2t KvƀAxmZN-ܢz[)O=$///RFo/,pTktFX1K^  ;m\(wX6߿`zU_ mmhZDzFrFL,?pwNix3#5๦?=|ЙvRj3  p;0RBV(7 8ޘƟ Â*._}}LJq'\>u}{ & Zuj*uj+`.΃9SآhTb3!6!&?{Ur-\c…ؽ{mmm%kr A75N! (0ĥ&^/J4/<@gαJ ?㷬Y)k:rr`T)GmcBA46ln=ɺۑ;j[yy9?%Kvŋ SCTm*x`bu-A9}ӧ]c.:zZTI} 򲲐o2p|vnk{RHwMa@)E[[[ꡅ ~o>VӊZc0"YTVV&<.77w&O9Ba~)(+*61E-S (+hˌBKaXe'H0%,}v;i~)Oa^aTBo`޽{$%q]pU g `'?S4bھ};o:V*^2t㥔bݕW'۽'I,XKϿw~AA~0Y%aDm0dKK]"ڸq[n=pmE>CCC)Ѐ_~HNvkm5K QR"8k!%҉DBDq85:xYYzɒ8~|&dƎ,Xsa.],"UUǣl^ܬ!l/>JPٳ՟[jUYRh4`Zyx$cZ@Dr+P@t %!Bkܞ?1@POjs`(mܹnۓvb`qlZ\RK-%<ϣlԨT ^y*> $RbΝ;8R)\LLX[RTUU}' ܷ_X~JRfJKzYYY8,]zVͨ1k< U* e >WX$,VK_[cJ΄Cvu7;(]ͳ&*VS644`۶mZokZxDeN^h-_3'ZZ~GȌFT_dz88~bup#a !{eŪ GRło㖤&G3j_1􇗈u!V;w|YCk8ڱyF|qkR) x1$wqi X,Ǝx#Zx<8|c u [[P)ٮ'z>Xd kSQZNR]_|~?j%g xQPZJE]wFlڴ [lIYb ߠ" *80|'v4F d  l`o;qbz([T* r>5)+,\͆%ݤnWe& ޻7.oߎΤVkKrZA^0}8u08PPxAe#9Xo><0m;vߛт]%,ɋ`Y^ L欬2F 'šUEEEqa <'@>Owވplڴ ;v@mm-6l/b;wET׃1`+|<,,.%X\RBV^={"///朒;WSS3wdxBDwoBٸuhANQE>L `QWmN}f?tu)bj۶m `8xp^u;p_?1 \#7 cqF-[ }v\ 2I$kkk%ZW8=={^ױPjjqڝeƄ,XsNh4yt|QWTO~N$ 6o (// .oöm{Q5Q682Ki PB]R Vso@ʫe˖H.\jBpAp hhh=p:.JCuu5jkku3a Gf`Q*U^CXB@xy#444)ϑs;P[[RipX555J4k(]G/\<7N#X:XbӦM(**Žp^J*vs={q{NNgm5D0~E! dVP&cd?]::~رUUUDmmmZjŊ3x:t ֢Ũ@EEO} P]]?1  C<^o|6m›oZTVVF[WWSQa֦'NDFo]<QLw؁6K(` ?_U({5{O? svNHv9Ѕ+98qO>dxuu)fBn7xJoR[FզMp1lٲ%$Rh\<ӣ^'`Æ 8Ad0 #Wdd, 0[,,[V9xGee%xb ~a\(^ GDA-?ٳg/6mBmm-sQMu% ! ]ÎB٣^OGaK%N-[&beM:EY$J Xf ~6Պ_{iPJqw:.FQxeY[RX,,^YSWW͛7h4FYRɬ+IE &!&nq[Yt\e# cƍO<Y_eΝM"+{;w^011ڊEļGI${1?~ 7FJSiZu%QSSI?~6Lt,]*$͸ Q AdddC+իW؋H.Xvc>*xg ]'Z ~Dك'OFT)=xqX,>fÆ Ûo ue0SU9PJyo=eƂ,XsJ(UWSjߏy-ܵkxhڵk {18#5n{bΝ8yd$NEe9/F[WH !رcك;wٺ]ĉɐ2E p'VH&Y7nFmu߹O21Ȃ5 2ļl$-a]"ZPۥmҘV\~xBUuE):aǎZV5+9^***n 1 #[X,X jٝz#V;/Pɂ6iL0*+#0Z' f>ŲB0tB̢2# ,&@}c}IA*ڝK'x#L֜ i ѳaDe tOEeȂ5bDxwR T|,'Dccc$ؾzj< һۿ+_ N$!1 'KKpS`b()xȂ7=!m+'I%PbY?0xʒ9{]v!;6dLu򄋢Ju2/,#" ,%;wO{>JiZ@%eQJfYҘcRu+i,7DLu DaJB1ܑkF'644zg=WY+bEJ"Iˊc2Iw/43 E_&Buu5B }P(Ň2# ,%;]JB2.*V^=JT˒ćRaQJ#ǤyEW#l׮]udtoLfCDR ;P)GC1 ?p{'{2`J644o4蜒u_*XVݻ77Ee(*u͑vc> (dFT1$v8ҩV*# <1" ,%W Dcx3ZCH|AxgEcc#~H]*BRDzB"1,BuȲۼys4dXtc;te J)k4ry)`꿆dL{WWy衇o>8q?N23RZ/f_* 02ٺG/%Bsss`|طo_v… #yf޽_ߴD mnpMES)XDʆc|f\z]-ٿe0G'n TU4pD& sw ,2ڌ~晖 o)a6oN|oa4668cNÃ3HXC@SG=n)ڦu0`BA_J뮘PRfhKDBL ɨWLص񻂢u5%i ))//;p`Yar)@a28qT*w}ay$e#Gcıbk BSvšecϞ=N8e˪qԴo鬫n(>d kӟtc͈'y̙-æ۷*r # ,vE3%2MgM]Y m3< 1;WXfoьud. d kAJ!hu1g3=Yf!D#ǯD"Ø!vq9 $Lj&f2!e82YfRκzggb۷oaL3`?IkNIw"pv=tȂ5hiiUn&Wj"d:C{S<ن,XvYLw`(@(KR9yQ'jlܸ1}q} ! ,6=W㩖0\헬kLQZZ JQKYf B.9XM3%2sYf ҊTntH¡׏cx [Ȃ5GBI88N3-6e疫)\:k l IDATeb.K BL\ySjYY,9_BfF7Kfk3kUC]zMz|)p㱍K Ef [XJ) E]]]1AQ{zcty8uF |3w&Ӽq[]{Nx _9&Af2Ӄ,XD]]]1?1Q"x&C L Yf+(x`XKQ(yh+"dZQ\`"V^;w0f=-]fkqb0f-c!X,Y]fkظp¤n)fbÆ S; iEF%vZաaԾ_7TM4YUAfȂ5$ h]]ݨ}srTuX555X]Ȃ5$jM?00m۶.BžQbnW `0 Z*++ZY3Ưjkk/[X `0۶mCMMMk՗m+IF:nɳ` w vsee%q KKK/8NgIۃP+ٲtkSUU_q,Xb/'\8>uݧYf9۶m71+=̊r]GDZCM x#ϷmvYűR5Mmhh@MM yT23,X3DtѾL0ˑ{` F5AQQܮ2B"h_Fu]5 sy5MbV)Ċa2SG.((@HpA0hh<@^FMJe?C||*LZرc(3Ȃ5C$pOBSy/F^#b+3#lhR)Dfۀ`0 w$[*7bRZE&-,WZd!eBՎiM2ћ*~ݾ PvDF::Ȃ5PT())$/f!YN[w2 "ݰ!Wzd[YEcN唉8%b<rjkki}}}ƍ|=ٍ,X3@D7S1ъ1s*d2pGIsN|__Lu/3+荅Ͷ(QǼuȂ5$p Z3Lhqq83:K?8Tk*k,Lh͖sCpjR/Z3zLc$pO*ѥ,XX1h,3=KY!Xt阏TDk _Mvl+VTTRhđk kRL inŁ&B2`M? ':R`%b,EJ=n4)TsN׏n(sI _(OS[msELUOE9Bxѳ+5a%!N"FRL>`]̞VrOJC/ ;w9ƍCZd$H|)5{LAJÔg1EuavvFŢ %"yi4S__@둝 2SVkǩ1<93^b;80c-R>CCCcNi 悋8ٱc럚q\Ȃ53A .x4$! 6ĸӀ,X3L^08* /SJ0a .D۶m8p`ۤ^D&!``q8¥P)!g럢"J2z3EI.-=.^A z!PJA]Ufґ3 gm&lٲ%Ҿ+Qq Zj?:oƍX~=QySMMM"֎gOؾ};z9YVb! "TAWWרaZa6l\iK0O&h8x㍂$DՑ/ 1ٲ6<`06lĆ b[,G޽{gfaZ3Z[[cu3I,HsYfJ)0u8OUS9L5EtɚS(T+Sta2mm^]]'ND?x)W:fC ZW3)>da(Q_8| *CZ&=BHsssd =a։0"\ɶm@)A aӈÚax闿%@=cRs͛GyO>d80M0 EVn  T?_/40 Y `0 غu}ms 7ɲϵ~=O`6}Od&dzŎU#ss,xcLR5PjkVq1t ( ں Jc9qUJ$q,F*mϏuLX,_UTT$'[Յ_~9g_CY9T wfxQ폁t[P\l46fCԂR/;{(Xt: Uu>v̎MO~c>ӱB;xMܟVo?^t 2CY£>y|T?޹w>B 싈 jھtV[]ڪuˣV$"OkZbպ?lE)A!!$53w;?I-37_=3C3|w)˚8}4x\YTg1ً/w\(~tu ݧzGr\sz'l>Իp8 #^kn)1 I`'IIW=Bk&M*X}`@x+iQsB2F+\q383qK!XKy :*X) 0A 6QN $_%x17 hl2syܧ-yʪ̴4\?T~FEɤHC\vW{͗xlvbV a{_ qGJM :pn}E)ñScΓ'l i9<> ˲yd0vB`/Nt 5 p]&VA^u dyY,9ho7d*EWWSAhbư# \,t'Lm9WJ]]QcN͗ ?a0h']]3.^Lv 鏪~uñbB`xpB+EPyv@!f'wD=' 9޽q(Q(Rߥ9fLNL|+$56 $Ee0 P03(pgJB2 L_122X,p:OPzILm̰s4d9#bY΀ ѱ9n>4%)qBt邎Ra@NV$烵Z }Df;iJO/JqV1hjV!qg$,w/JIM&"re+7` 1W &G:shl߁s ?{I)Ap,9Xus&/^IFdUAifF墫@kz'TB opv{$tUC8pЧzߠ)"91 ֎N@VTLg] !vUL41'b apF'UW]޸rF _ˠCl[bi>t:ulB#GNE *XC LOK_U8{8:nٴ'M4٦µ6ñf?E;cڲV\}g3UB` HmR1 !&j9 tDC]%'Q啈eje)q;!u\t+ Gm ; I 6m(H7\nOc`=g8W IKV_=ȏEN) PJ!:7E+I?N B&iaZz/6â'<%,TR=;s:̑CXqY*FuUR}(&SIr[J{ B+E,Y2ybŴXb5^Ád*SkITkڻڥdflRSƒPN Vp\gM>Z܇ v; ȱ-n *X)M-Wlܗѽ.Drot+ {+ ;z* ʀ" alص_ z(ڶPZ|.m!!VnZ\UC`)0ڵg/pVDpomfoPu<8E)Mbn~PJ˖/חOepfKͤ6 ݽa u#BB8@UO9Qx +JKFw0,I:JDˑnu ͆g)'I.ʓ^g\eEYF$:+EIJS9 ~0~v?7}/f`ោ4nJdp'22j0R<ɱIm!x-m*%β`Z ZC`^q}! e;8 8sLrqma^Ao^H'y.n5 V L%$1 zrB?``&!aEZn:ܞ߉"hSdY\)5TUW3is>ou`pWP+!{dήZ>rpj,R[FPd$Q4EJr$}h ˎ;p8_1 UM<Ƈ*XLߟfGsMh `ȶvlYXx|B-H5Eֈ1r½!7jmhvIQΨd.)4_^~&;Aduww"y2 ƪQ4?'keMM@b]^_BjΫ_Z\4T+V9M:Ɔ{zj~IJc@fZ;j:vxdZ-\]i%AxSmcpS]8N'Nvf,Ǔ12^ߝ6?pAkh:|,"Rk6kg0qNJii uߠC3PQVqmsfϛaBDE-gl fc%Ԙ+V&ߔ38D%À'GEH;X$3DؓBB{ $YAՂ,iu@Kcw嗜?J+{őa4TF6aRE;{< ǝH<ހLG{= 8F19唎X8 azKK+ڛϿ` Gapٌnƹ <ʡ!r˔} \0$FXNjf^lp9%fyb`iYm&f^/+{FfsW 睰KߵxߝT@VvDȊgNn o0 !Kǝ%v)zUNͤ2zBT(u_?(Z5'H@0s\u_?(7eP;J̝=#@yS3#DKN i?;: {447_XHKؑ[^~JȠY@?#tQ,p3O65RYU fej 8i# =6D>V:.{ VW=E K'?l3>1cm؆tyA|~X#8'7,CqJzo\+C$TB`5Qj_UUE?QoFd&-o 8rkT10Z=F~#D`IDATM?\|IӁc`i@D\wS~<ñCk~ڙ*X`4EVu{H C>8nM̲/Re=ӏHx @WuMH%X*((Ƽr:7aG!8, 2 K~Ȳy{.\xE%oGmS*X2Pt 7Mp|1'   0 ~[z9zܳ7oh5Iݏep,_,)  096+[E}ZZst, e2 dUX-lAmX ʲ  ^e `Ɇ8r@Ac_`JdU@0Bj&-p:g&?;Zou/[nwks??g?paݭ 4')*DY!,PL|hq9Qt P^/PշO}V[VPV /PXuCaYp #|(*rAJ_&(V-)_ !Bl#ǟ|']㗤nȪŒt'3nV|}nߗm¨`1c9m\"dĤ|\;CVCR/~CK8@bRx[! ;R{)Ͷ#Bn)A,à[Qn>%h47vʤnQ'4}sk~:ciuo~A`5(PT9;}j]{͊8"GT4/Jm$XkuHP<QQ|_.)tEg̲ :=>z~O[:vG\eq, A^;0 3f+fgo׽|vpڊv_住~*!Vˢ; b4 L9g u%& S`ivIYFrtuv WYQg {lUต2 !? Lj,EU!JQqiisZmimDy%~sWaVYx]G; }@yi6vZwțwyxXzuS=Κ]_gLϻ_[?lklwIǃG\~ |nRo05@}ө0 fL;wc޼yoR?B<';l`&B/M ]قñǹImC&tArhpt[mM=k/ɋ=?y]m`_6~ٝӍ./񤲟^VbRT) 2te9;ol믻'},[\_)_  CGF^4Sy]bZN͆'[xSP#(r"lMz, =}qtu->cBXzte?J^}kW ??7B`uQO>>oѷj3.ȵs@*!^U%sfMn:++ keMM@Q3 }.ߞuUR dPUX3!m:‚SR'݇B' ^o; g<1ҤsEZ Sz(Ac(͂Q/a~gG~?{~Ḃ'j.-_Q[m;eWo3k,*d9we&2meEgg{m̓GԆzxH*M9| Lq )1)',ܩ $K)*˻{0 'mY̞äI_SorsvG5xE sι8/}ïzT4.lȇa =4e: IW +uix)ArœpiPtkC}|u$|Ŭ?5ټvY޾֪sN_4ߝ>Q:Ҡ+kj[ 1 $[qͼsUU ~qlZvV@tKk̟.GT4a{8.r{>:@^J a;Ԁ|N ݄c}K=q8 Q"c=n_ ('2/lzkn\XyǵܰeqK1 &#'sRr.#*X!r36|&P3Pf1uNbվOОWX ')tDUQVVb Q]Ul;2} _TN>kiCk+ "n[[ΆxZYSx_^x.ZkP˾ڵ2'}~Q0=,\^~ 1 @ezՈމǍpd^|/=umfWT:ŒoeÏJMvn# 9}'PȲ׽zdg@T5U)cәM;jVݞZ>l[nޯ,#/= WN)[~5Dtu:AlimsBIi+CO8x@H،x } Wث~~f7/~Ӎ~*Xtݢ(0LPD nўE_~e  #` F<|]o:~󗇎Cx~[9œn3h F,+nTQ!quUxo4̥e *r}wMusGBCw<~?ή(͚[ouNqBK#b l2p `YtP'GѓO=Ϭ#)*N\-;O/p'hq`[D\{S&{7k8,^oܤ ِee7 YmkoL壶8(cO7ok&d2 qݙ:x|BK#d1P:6t͖ZڱM6FRc-Zt`ĺ~޺8X zX.ǩt,AK#_3rB5 pmc_hӵIKk؟pΝnwiwo=ڲeQoqT#2C/^emnٻW#gxHT܁2YКc4}6Z,SL c*XNlbY;-Xi'Qƚ5kZ#%Ԉي[9L8((C: J/T4B0Ko׮]/v/ w2 ìyP-F]ez+y=VE~T((PҐGy43U@۠ eCPN@B9/>eNI=T(r!t? ELFvv$}Bkd@?< `Q(, 2bEPF CXCffIENDB`charactermanaj/resources/version.properties0000644000175000017500000000012011767047556021561 0ustar paulliupaulliuvendor=seraphy@seraphyware specification_version=1.0 implements_version=0.98 charactermanaj/resources/images/0000755000175000017500000000000011767047554017230 5ustar paulliupaulliucharactermanaj/resources/images/lattice_s.png0000644000175000017500000000030111767047554021677 0ustar paulliupaulliuPNG  IHDRw=sRGBbKGD pHYs  tIME% ;BgAIDATHc$p-|cccJh ,st8 Gh>|0F、&!Qy2—IENDB`charactermanaj/resources/images/lattice_l.png0000644000175000017500000000031611767047554021676 0ustar paulliupaulliuPNG  IHDR szzsRGBbKGD pHYs  tIME({NIDATX a6rm g6UD8$+G8o8| xp &qfiIENDB`charactermanaj/resources/images/wallpaper.xml0000644000175000017500000000035411767047554021743 0ustar paulliupaulliu wallpaper.xml 01;lattice_s 02;lattice_l charactermanaj/resources/schema/0000755000175000017500000000000011767047553017222 5ustar paulliupaulliucharactermanaj/resources/schema/parts-definition.xsd0000644000175000017500000001057711767047553023233 0ustar paulliupaulliu 空文字を許可しないトークンの定義 パーツリストの定義 パーツの作者 パーツのダウンロードURL パーツの定義 パーツのローカライズ名 パーツ名(ファイルのネームボディ) カテゴリ、省略時は任意のカテゴリ パーツのバージョン charactermanaj/resources/schema/0.8/0000755000175000017500000000000011767047552017526 5ustar paulliupaulliucharactermanaj/resources/schema/0.8/character_inc.xsd0000644000175000017500000002765311767047552023050 0ustar paulliupaulliu 空文字を許可しないトークンの定義 RGB変換パラメータ オフセット 倍率 ガンマ HSB変換パラメータ RGB置換タイプ RGB置換タイプ 淡色化(0でグレー化、1で淡色化なし) 定義済みパーツ組み合わせ例(プリセット)の定義リスト、定義順に表示される。 定義済みパーツ組み合わせ例(プリセット)の定義 表示名、該当するlangがない場合は最初をデフォルトとする。 背景色 アフィン変換用パラメータ、4または6つの要素からなるマトリックス カテゴリごとのパーツ定義、パーツが空の場合は該当カテゴリは選択なし パーツ 色定義(オプション) カラーグループ カラーグループの同期を行う RGB変換 HSB変換 RGB置換 レイヤー識別子 パーツ名 カテゴリの識別子 定義済みパーツ組み合わせ例(プリセット)の識別子 デフォルトのプリセットを示す識別子 charactermanaj/resources/schema/0.8/partsset.xsd0000644000175000017500000000100411767047552022106 0ustar paulliupaulliu charactermanaj/resources/schema/0.8/character.xsd0000644000175000017500000003575311767047552022217 0ustar paulliupaulliu キャラクターデータの定義 キャラクターデータ名。該当するlangがなければ最初の定義をデフォルト値とする。 備考 作者名 説明文 イメージのサイズ(幅と高さ) 雑多なプロパティのコレクション 雑多なプロパティ カテゴリの定義リスト、出現順で画面に表示される。 カテゴリの定義 表示するパーツの行数(初期値) カテゴリの表示名、該当するlangがない場合は最初のものをデフォルトとする。 パーツを構成するレイヤーの定義リスト パーツを構成するレイヤーの定義 レイヤーの表示名、該当するlangがない場合は最初のものをデフォルトとする。 パーツ全体でレイヤーを重ね合わせる順序。 色設定を連動させるグループの指定(省略可) カラーグループID 初期状態で連動させるか? このレイヤーの画像を格納しているディレクトリ名 レイヤーの識別子 カテゴリーの識別子 このカテゴリで複数のパーツが選択可能であるか? カラーグループの定義リスト カラーグループの定義 カラーグループの表示名。該当するlangがない場合は最初をデフォルトとする。 カラーグループの識別子 定義済みパーツ組み合わせ例(プリセット)の定義リスト、定義順に表示される。 このXMLのバージョン番号、1.0固定。 charactermanaj/resources/schema/character.xml0000644000175000017500000002314211767047553021702 0ustar paulliupaulliu Default デフォルト Unknown 名無し 300 400 true 6 Hair - Front 髪型 - 手前 Variable 可変色 12 hair_front Accessory アクセサリ 13 hair_front_accessory 6 Hair - Back 髪型 - 後ろ Variable 可変色 2 hair_back Accessory アクセサリ 3 hair_back_accessory 6 Head Head 9 head 6 Expression 表情 Face 表情 14 face_front Accessory アクセサリ 11 face_back 6 Eyes Eye 15 eye 6 Body 身体 Variable 可変色 7 body_front_color Clothes ドレス 6 body_front Skin 4 body_back 10 Accessory アクセサリー Top 最前面 16 accessory_front Middle(R) 中間(R) 10 accessory_middle_front Middle(L) 中間(L) 8 accessory_middle_back Underwear アンダーウェア 5 accessory_underwear Back 最背面 1 accessory_back Hair Eye Skin Dress Originator (K.Hmix 1st Edition) (キャラクターなんとか機本家) K.Hmix 1st Edition http://khmix.sakura.ne.jp/ The storage of an additional parts キャラクターなんとか機 追加パーツ保管庫 http://nantoka.main.jp/ charactermanaj/resources/schema/favorites.xml0000644000175000017500000014717611767047553021766 0ustar paulliupaulliu 秋にいり111 プリセット1 プリセット2 プリセット3 charactermanaj/resources/schema/character_inc.xsd0000644000175000017500000003007111767047553022530 0ustar paulliupaulliu 空文字を許可しないトークンの定義 RGB変換パラメータ オフセット 倍率 ガンマ HSB変換パラメータ RGB置換タイプ RGB置換タイプ 淡色化(0でグレー化、1で淡色化なし) 定義済みパーツ組み合わせ例(プリセット)の定義リスト、定義順に表示される。 定義済みパーツ組み合わせ例(プリセット)の定義 表示名、該当するlangがない場合は最初をデフォルトとする。 背景色 アフィン変換用パラメータ、4または6つの要素からなるマトリックス カテゴリごとのパーツ定義、パーツが空の場合は該当カテゴリは選択なし パーツ 色定義(オプション) カラーグループ カラーグループの同期を行う RGB変換 HSB変換 RGB置換 レイヤー識別子 パーツ名 カテゴリの識別子 定義済みパーツ組み合わせ例(プリセット)の識別子 デフォルトのプリセットを示す識別子 charactermanaj/resources/schema/xml.xsd0000644000175000017500000002164311767047553020550 0ustar paulliupaulliu

lang (as an attribute name)

denotes an attribute whose value is a language code for the natural language of the content of any element; its value is inherited. This name is reserved by virtue of its definition in the XML specification.

Notes

Attempting to install the relevant ISO 2- and 3-letter codes as the enumerated possible values is probably never going to be a realistic possibility.

See BCP 47 at http://www.rfc-editor.org/rfc/bcp/bcp47.txt and the IANA language subtag registry at http://www.iana.org/assignments/language-subtag-registry for further information.

The union allows for the 'un-declaration' of xml:lang with the empty string.

space (as an attribute name)

denotes an attribute whose value is a keyword indicating what whitespace processing discipline is intended for the content of the element; its value is inherited. This name is reserved by virtue of its definition in the XML specification.

base (as an attribute name)

denotes an attribute whose value provides a URI to be used as the base for interpreting any relative URIs in the scope of the element on which it appears; its value is inherited. This name is reserved by virtue of its definition in the XML Base specification.

See http://www.w3.org/TR/xmlbase/ for information about this attribute.

id (as an attribute name)

denotes an attribute whose value should be interpreted as if declared to be of type ID. This name is reserved by virtue of its definition in the xml:id specification.

See http://www.w3.org/TR/xml-id/ for information about this attribute.

Father (in any context at all)

denotes Jon Bosak, the chair of the original XML Working Group. This name is reserved by the following decision of the W3C XML Plenary and XML Coordination groups:

In appreciation for his vision, leadership and dedication the W3C XML Plenary on this 10th day of February, 2000, reserves for Jon Bosak in perpetuity the XML name "xml:Father".

About this schema document

This schema defines attributes and an attribute group suitable for use by schemas wishing to allow xml:base, xml:lang, xml:space or xml:id attributes on elements they define.

To enable this, such a schema must import this schema for the XML namespace, e.g. as follows:

          <schema . . .>
           . . .
           <import namespace="http://www.w3.org/XML/1998/namespace"
                      schemaLocation="http://www.w3.org/2001/xml.xsd"/>
     

or

           <import namespace="http://www.w3.org/XML/1998/namespace"
                      schemaLocation="http://www.w3.org/2009/01/xml.xsd"/>
     

Subsequently, qualified reference to any of the attributes or the group defined below will have the desired effect, e.g.

          <type . . .>
           . . .
           <attributeGroup ref="xml:specialAttrs"/>
     

will define a type which will schema-validate an instance element with any of those attributes.

Versioning policy for this schema document

In keeping with the XML Schema WG's standard versioning policy, this schema document will persist at http://www.w3.org/2009/01/xml.xsd.

At the date of issue it can also be found at http://www.w3.org/2001/xml.xsd.

The schema document at that URI may however change in the future, in order to remain compatible with the latest version of XML Schema itself, or with the XML namespace itself. In other words, if the XML Schema or XML namespaces change, the version of this document at http://www.w3.org/2001/xml.xsd will change accordingly; the version at http://www.w3.org/2009/01/xml.xsd will not change.

Previous dated (and unchanging) versions of this schema document are at:

charactermanaj/resources/schema/parts-definition.xml0000644000175000017500000000232311767047553023223 0ustar paulliupaulliu 緋龍華 麒麟 K.Hmix 1st Edition http://khmix.sakura.ne.jp/ http://khmix.sakura.ne.jp/file/character_02.08.zip Intake インテーク 緋龍華 麒麟 K.Hmix 1st Edition http://khmix.sakura.ne.jp/ Wave ウェーブ charactermanaj/resources/schema/character.xml.ser0000644000175000017500000001313111767047553022467 0ustar paulliupaulliusrjava.util.HashMap`F loadFactorI thresholdxp?@ wtjasr"charactermanaj.model.CharacterData<YGLauthortLjava/lang/String;L colorGroupst!Lcharactermanaj/model/OrderedMap;LdefaultPartsSetIdq~L descriptionq~LdocBasetLjava/net/URI;Lidq~L imageSizetLjava/awt/Dimension;LimagestLjava/util/Map;L localizedNameq~LpartsCategoriesq~LpresetstLjava/util/HashMap;L propertiestLjava/util/Properties;LrecommendationURLListtLjava/util/List;Lrevq~xpt 名無しsrcharactermanaj.model.OrderedMapFc[G&LentriestLjava/util/ArrayList;xpsrjava.util.ArrayListxaIsizexpw sr/charactermanaj.model.OrderedMap$OrderedMapEntryF@&mLdatatLjava/lang/Object;Lkeyq~xpsrcharactermanaj.model.ColorGroupxR'ZenabledLidq~L localizedNameq~xptn/aq~q~sq~sq~t color-hairt髪q~sq~sq~t color-eyet瞳q~sq~sq~t color-skint肌q~#sq~sq~t color-dresst服q~'xxpt$デフォルトのキャラクターptdefaultsrjava.awt.DimensionA׬_DIheightIwidthxp,sq~?@ wxtデフォルトsq~sq~w sq~sr"charactermanaj.model.PartsCategoryB!ZwZmultipleSelectableIorderI visibleRowsL categoryIdq~Llayersq~ LlocalizedCategoryNameq~xpt hair_frontsr&java.util.Collections$UnmodifiableList%1Llistq~ xr,java.util.Collections$UnmodifiableCollectionB^LctLjava/util/Collection;xpsrjava.util.Arrays$ArrayList٤<͈[at[Ljava/lang/Object;xpur[Lcharactermanaj.model.Layer;t8T\xpsrcharactermanaj.model.LayerYC]JҝZinitSyncIorderL colorGroupt!Lcharactermanaj/model/ColorGroup;Ldirq~Lidq~L localizedNameq~xp q~t hair_frontt hair_frontt 可変色sq~> q~thair_front_accessorythair_front_accessorytアクセサリq~;t髪型 - 手前q~4sq~sq~2t hair_backsq~5sq~9uq~<sq~>q~t hair_backt hair_backt 可変色sq~>q~thair_back_accessorythair_back_accessorytアクセサリq~Mt髪型 - 後ろq~Ksq~sq~2theadsq~5sq~9uq~<sq~> q~"theadtheadt頭q~\t頭q~Zsq~sq~2tfacesq~5sq~9uq~<sq~>q~t face_frontt face_frontt表情sq~> q~t face_backt face_backtアクセサリq~gt表情q~esq~sq~2teyesq~5sq~9uq~<sq~>q~teyeteyet目q~vt目q~tsq~sq~2tbodysq~5sq~9uq~<sq~>q~&tbody_front_colortbody_front_colort 可変色sq~>q~t body_frontt body_frontt ドレスsq~>q~"t body_backt body_backt肌q~t身体q~sq~sq~2 t accessorysq~5sq~9uq~<sq~>q~taccessory_fronttaccessory_frontt 最前面sq~> q~taccessory_middle_fronttaccessory_middle_frontt 中間(R)sq~>q~taccessory_middle_backtaccessory_middle_backt 中間(L)sq~>q~taccessory_underweartaccessory_underweartアンダーウェアsq~>q~taccessory_backtaccessory_backt 最背面q~tアクセサリーq~xxsq~?@ wxsrjava.util.Properties9zp6>Ldefaultsq~ xrjava.util.Hashtable%!JF loadFactorI thresholdxp?@w t watch-dirttruexpsq~w sr&charactermanaj.model.RecommendationURL1.wyL displayNameq~Lurlq~xpt<(キャラクターなんとか機本家) K.Hmix 1st Editionthttp://khmix.sakura.ne.jp/sq~t:キャラクターなんとか機 追加パーツ保管庫thttp://nantoka.main.jp/xtdefaultxtensq~tUnknownsq~sq~w sq~q~q~sq~sq~t color-hairtHairq~sq~sq~t color-eyetEyeq~sq~sq~t color-skintSkinq~sq~sq~t color-dresstDressq~xxptdefault characterptdefaultsq~+,sq~?@ wxtDefaultsq~sq~w sq~sq~2t hair_frontsq~5sq~9uq~<sq~> q~t hair_frontt hair_fronttVariablesq~> q~thair_front_accessorythair_front_accessoryt Accessoryq~t Hair - Frontq~sq~sq~2t hair_backsq~5sq~9uq~<sq~>q~t hair_backt hair_backtVariablesq~>q~thair_back_accessorythair_back_accessoryt Accessoryq~t Hair - Backq~sq~sq~2theadsq~5sq~9uq~<sq~> q~theadtheadtHeadq~tHeadq~sq~sq~2tfacesq~5sq~9uq~<sq~>q~t face_frontt face_fronttFacesq~> q~t face_backt face_backt Accessoryq~t Expressionq~sq~sq~2teyesq~5sq~9uq~<sq~>q~teyeteyetEyeq~tEyesq~sq~sq~2tbodysq~5sq~9uq~<sq~>q~tbody_front_colortbody_front_colortVariablesq~>q~t body_frontt body_fronttClothessq~>q~t body_backt body_backtSkinq~tBodyq~sq~sq~2 t accessorysq~5sq~9uq~<sq~>q~taccessory_fronttaccessory_fronttTopsq~> q~taccessory_middle_fronttaccessory_middle_frontt Middle(R)sq~>q~taccessory_middle_backtaccessory_middle_backt Middle(L)sq~>q~taccessory_underweartaccessory_underweart Underwearsq~>q~taccessory_backtaccessory_backtBackq~1t Accessoryq~/xxsq~?@ wxsq~?@w t watch-dirttruexpsq~w sq~tOriginator (K.Hmix 1st Edition)thttp://khmix.sakura.ne.jp/sq~t"The storage of an additional partsthttp://nantoka.main.jp/xtdefaultxxcharactermanaj/resources/schema/partsset.xsd0000644000175000017500000000103711767047553021610 0ustar paulliupaulliu charactermanaj/resources/schema/character.xsd0000644000175000017500000004276611767047553021715 0ustar paulliupaulliu キャラクターデータの定義 キャラクターデータ名。該当するlangがなければ最初の定義をデフォルト値とする。 備考 作者名 説明文 イメージのサイズ(幅と高さ) 雑多なプロパティのコレクション 雑多なプロパティ カテゴリの定義リスト、出現順で画面に表示される。 カテゴリの定義 表示するパーツの行数(初期値) カテゴリの表示名、該当するlangがない場合は最初のものをデフォルトとする。 パーツを構成するレイヤーの定義リスト パーツを構成するレイヤーの定義 レイヤーの表示名、該当するlangがない場合は最初のものをデフォルトとする。 パーツ全体でレイヤーを重ね合わせる順序。 色設定を連動させるグループの指定(省略可) カラーグループID 初期状態で連動させるか? このレイヤーの画像を格納しているディレクトリ名 レイヤーの識別子 カテゴリーの識別子 このカテゴリで複数のパーツが選択可能であるか? カラーグループの定義リスト カラーグループの定義 カラーグループの表示名。該当するlangがない場合は最初をデフォルトとする。 カラーグループの識別子 お薦めリンクのリスト お勧めリンク 説明文 言語 URL 言語 定義済みパーツ組み合わせ例(プリセット)の定義リスト、定義順に表示される。 このXMLのバージョン番号、1.0固定。 charactermanaj/resources/appinfo/0000755000175000017500000000000011767047555017420 5ustar paulliupaulliucharactermanaj/resources/appinfo/about.html0000644000175000017500000001705311767047555021426 0ustar paulliupaulliu

ChracterManaJ Version @@SPECIFICATION-VERSIONINFO@@

Implementation-Version: @@IMPLEMENTS-VERSIONINFO@@

[Software Requirements]

Use in the following environment is assumed.

  • Mac OS X
    • 10.4 Tiger (Intel/PPC) J2SE5
    • 10.5 Leopard (Intel/PPC) J2SE5
    • 10.5 Leopard (Intel) JavaSE6
    • 10.6 Snow Leopard (Intel) JavaSE6
    • 10.7 Lion (Intel) JavaSE6 (Java for Mac OS X 10.7 is required.)
  • Microsoft Windows
    • Windows 2000 SP4 (32Bit) JavaSE6 (Japanese Only)
    • Windows XP SP2 or later (32Bit) JavaSE6
    • Windows Vista SP1 or later (32/64Bit) JavaSE6
    • Windows 7 (32/64Bit) JavaSE6
    Installation of language support of East Asia is indispensable.
    (Control Panel / Regional and Language Options / Language Tab / Install files for East Asian languages)
  • Linux
    • Ubuntu 10.04 sun-java-jdk6
    • Fedora 14 Desktop openjdk-1.6.0 or sun-java-jdk6(recommended)

I think that it will operate fundamentally if there is an execution environment which supported the desktop of J2SE5 since J2SE5 has described.

JavaSE6 is recommended.

"lib/charsets.jar" is indispensable in order to treat a Japanese file name.

When "lib/charsets.jar" does not exist, it is necessary to re-install JRE.
(For example, when "East Asian Languages" is installed afterwards.)


Could someone please translate this japanese text.

JavaSE7の注意点

JavaSE7でも基本的には動作しますが、細かな点で互換性がなく、現時点では推奨されません。

また、Windows環境において、日本語を含むパス上にexeまたはjarファイルをおいてある場合はダブルクリックによる起動ができません。たとえば「デスクトップ」とか「マイドキュメント」の上に置くことはできません。

日本語を含むパスから起動したい場合は、コマンドプロンプトからカレントディレクトリに設定したのちに起動するようにすれば使用できますが、 単純に日本語を含まないフォルダに移動するほうが手っ取り早いと思います。(これは本アプリケーションにかぎらず、どのアプリでも起きるため、JavaSE7の不具合と思われますが、2012/3時点で修正されておりません。)

そのほか、色が劣化するなどの違いがあるようですが、基本的にはJDK7の完成度が低いことに起因するものと考えられます。

[About this software]

このアプリケーションは複数の画像を選択し重ね合わせることで一つの画像を作り出す、モンタージュを行うアプリケーションです。

用途としてパーツを組み合わせてキャラクターをデザインするという、一種の着せ替えを楽しむことを想定していますが、それに限りません。

モンタージュするための素材の画像データがないと、このアプリケーションは何もできません。ご自身で作成するか、既にある画像データを用意するなどしてください。

カテゴリ、レイヤー、カラーグループを増減させたりなど、画像の組み合わせ方については新しいプロファイルを作成することで自分好みの設定にすることができます。

このアプリケーションは、K.Hみっくす ふぁーすとえでぃしょん さんの
「キャラクターなんとか機」
を目標にJAVAによりフルスクラッチから書き起こしたものです。

初期状態で、「キャラクターなんとか機」と同じカテゴリ、レイヤー順序、ディレクトリ、カラーグループの設定がされたプロファイルが作成されますので、 「キャラクターなんとか機」のパーツデータをインポートして使うことができます。

[ライセンス/使用許諾条件]

「キャラクターなんとかJ」はオープンソースソフトウェアです。ソースコードは「The Apache License Version 2.0」です。

本ソフトウェアは完全に無保証です。本ソフトウェアは「現状のまま」、かつ明示か暗黙であるかを問わず、一切の保証を付けずに提供されます。

ここから最新リリース 、 および最新リリースのソースコードを取得することができます。

商用利用であるかを否かを問わず、どなたでも、自由にお使いいただけます。登録や費用は一切必要ありません。再配布も自由になさってかまいません。

このアプリケーション自身(javaコード)はseraphyによって書かれました。
人的リソースに限りがあるため、メンテナンスや改善に時間がかかることが予想されます。
このアプリケーションをより良くしようという有志がおられましたら、ぜひ、ご協力・ご連絡くださいますようお願いいたします。歓迎いたします。

また、バグレポート等ありましたら、どなた様も上記プロジェクトページにてご報告くださいますよう、お願いいたします。

[キャラクターデータについて]

このアプリケーションを動作させるためには、パーツの画像データが必要です。

初期状態のプロファイルでは「キャラクターなんとか機 Ver2」で使える画像を利用できるよう設定されます。 キャラクターデータのインポート方法はヘルプをご参照ください。

 「キャラクターなんとか機 Ver2」を目標としましたが、色調整等のパラメータを解析しきれなかったため完全には色を再現できていません。 そのかわり、各レイヤーごとに色の微調整ができるようにしているため、近い色を再現するはできると思います。 その他、いろいろ不備があると思いますがご容赦ください。

[パーツデータの作者様へ]

パーツデータをお持ちか、作成された方で、オープンソース系(たとえばクリエイティブコモンズなど) のライセンスで配布を許可くださる方は、ぜひ、ご連絡ください。

プロジェクトの配布ファイルに加えさせていただこうと思います。

[利用コンポーネントについて]

J2SE5の標準APIの他、Apache Antの一部(The Apache License Version 2.0)をライブラリとして使用しています。

charactermanaj/resources/appinfo/about_ja.html0000644000175000017500000002016311767047555022074 0ustar paulliupaulliu

ChracterManaJ Version @@SPECIFICATION-VERSIONINFO@@

実装バージョン: @@IMPLEMENTS-VERSIONINFO@@

[このアプリケーションについて]

このアプリケーションは複数の画像を選択し重ね合わせることで一つの画像を作り出す、モンタージュを行うアプリケーションです。

用途としてパーツを組み合わせてキャラクターをデザインするという、一種の着せ替えを楽しむことを想定していますが、それに限りません。

モンタージュするための素材の画像データがないと、このアプリケーションは何もできません。ご自身で作成するか、既にある画像データを用意するなどしてください。

カテゴリ、レイヤー、カラーグループを増減させたりなど、画像の組み合わせ方については新しいプロファイルを作成することで自分好みの設定にすることができます。

このアプリケーションは、K.Hみっくす ふぁーすとえでぃしょん さんの
「キャラクターなんとか機」
を目標にJAVAによりフルスクラッチから書き起こしたものです。

初期状態で、「キャラクターなんとか機」と同じカテゴリ、レイヤー順序、ディレクトリ、カラーグループの設定がされたプロファイルが作成されますので、 「キャラクターなんとか機」のパーツデータをインポートして使うことができます。

[ライセンス/使用許諾条件]

「キャラクターなんとかJ」はオープンソースソフトウェアです。ソースコードは「The Apache License Version 2.0」です。

本ソフトウェアは完全に無保証です。本ソフトウェアは「現状のまま」、かつ明示か暗黙であるかを問わず、一切の保証を付けずに提供されます。

ここから最新リリース 、 および最新リリースのソースコードを取得することができます。

商用利用であるかを否かを問わず、どなたでも、自由にお使いいただけます。登録や費用は一切必要ありません。再配布も自由になさってかまいません。

このアプリケーション自身(javaコード)はseraphyによって書かれました。
人的リソースに限りがあるため、メンテナンスや改善に時間がかかることが予想されます。
このアプリケーションをより良くしようという有志がおられましたら、ぜひ、ご協力・ご連絡くださいますようお願いいたします。歓迎いたします。

また、バグレポート等ありましたら、どなた様も上記プロジェクトページにてご報告くださいますよう、お願いいたします。

[キャラクターデータについて]

このアプリケーションを動作させるためには、パーツの画像データが必要です。

初期状態のプロファイルでは「キャラクターなんとか機 Ver2」で使える画像を利用できるよう設定されます。 キャラクターデータのインポート方法はヘルプをご参照ください。

 「キャラクターなんとか機 Ver2」を目標としましたが、色調整等のパラメータを解析しきれなかったため完全には色を再現できていません。 そのかわり、各レイヤーごとに色の微調整ができるようにしているため、近い色を再現するはできると思います。 その他、いろいろ不備があると思いますがご容赦ください。

[パーツデータの作者様へ]

パーツデータをお持ちか、作成された方で、オープンソース系(たとえばクリエイティブコモンズなど) のライセンスで配布を許可くださる方は、ぜひ、ご連絡ください。

プロジェクトの配布ファイルに加えさせていただこうと思います。

[動作環境]

以下の環境での利用を想定しています。

  • Mac OS X
    • 10.4 Tiger (Intel/PPC) J2SE5
    • 10.5 Leopard (Intel/PPC) J2SE5
    • 10.5 Leopard (Intel) JavaSE6
    • 10.6 Snow Leopard (Intel) JavaSE6
    • 10.7 Lion (Intel) JavaSE6 (Java for Mac OS X 10.7が必要)
  • Microsoft Windows
    • Windows 2000 SP4 (32Bit) JavaSE6 (日本語版のみ)
    • Windows XP SP2+ (32Bit) JavaSE6 (日本語版、それ以外はEast asian languagesをコンパネからインストール済みのこと)
    • Windows Vista SP1以降 (32/64Bit) JavaSE6
    • Windows 7 (32/64Bit) JavaSE6
  • Linux
    • Ubuntu 10.04 sun-java-jdk6
    • Fedora 14 Desktop openjdk-1.6.0/sun-java-jdk6 (sun-javaを推奨)

J2SE5で記述しているため、J2SE5のデスクトップをサポートした実行環境があれば基本的には動作すると思います。

Windows環境ではJ2SE5でも動作しますが、JavaSE6を推奨します。同様にMac OS XでもTigerやLeopard on PPCでないかぎりJavaSE6を推奨します。

メモリは、Mac OS Xのアプリケーションバンドル形式、またはWindowsのEXE形式での配布形式では、初期状態で96MB、最大で128MBの設定を行っています。

実行可能JAR形式ではJREのデフォルトのサイズが使われます。これは搭載している物理メモリによって割り当てられるサイズが64MBあるいは256MBのように変わります。

おそらく通常の利用方法において64MBの割り当てでは、やや不足する感があると思いますが、一応、動作はすると思います。

実際のメモリ使用状況については Aboutメニューからシステム情報タブの中で確認することができます。

Linuxをお使いの場合は、sun-javaを推奨します。(基本的にはopenjdkでも動作しますが、クリップボードが機能しません。)

JavaSE7の注意点

JavaSE7でも基本的には動作しますが、細かな点で互換性がなく、現時点では推奨されません。

また、Windows環境において、日本語を含むパス上にexeまたはjarファイルをおいてある場合はダブルクリックによる起動ができません。たとえば「デスクトップ」とか「マイドキュメント」の上に置くことはできません。

日本語を含むパスから起動したい場合は、コマンドプロンプトからカレントディレクトリに設定したのちに起動するようにすれば使用できますが、 単純に日本語を含まないフォルダに移動するほうが手っ取り早いと思います。(これは本アプリケーションにかぎらず、どのアプリでも起きるため、JavaSE7の不具合と思われますが、2012/3時点で修正されておりません。)

そのほか、色が劣化するなどの違いがあるようですが、基本的にはJDK7の完成度が低いことに起因するものと考えられます。

[利用コンポーネントについて]

J2SE5の標準APIの他、Apache Antの一部(The Apache License Version 2.0)をライブラリとして使用しています。

charactermanaj/resources/languages/0000755000175000017500000000000011767047555017732 5ustar paulliupaulliucharactermanaj/resources/languages/exportwizdialog_ja.xml0000644000175000017500000000621611767047555024366 0ustar paulliupaulliu 作者 説明 キャラクター定義 部分的なエクスポート(差分セット) エクスポート 完了しました。 確認 キャンセルしますか? ファイルが既に存在します。上書きしてもよろしいですか? ファイルがありません。 次へ 戻る 実行 キャンセル エクスポートするコンテンツ コメント サンプルイメージ パーツ プリセット/お気に入り サンプルイメージ パーツの選択 選択 カテゴリ パーツ名 更新日時 作者 バージョン 50 100 150 100 80 50 すべて選択 すべて選択解除 名前順で整列 日付順で整列 チェックする チェックを外す プリセット/お気に入りの選択 選択 既定 名前 不足するパーツ 50 50 150 300 すべて選択 すべて選択解除 整列 使用パーツをエクスポート対象にする charactermanaj/resources/languages/partsmanagedialog_ja.xml0000644000175000017500000000354711767047555024621 0ustar paulliupaulliu パーツの管理 パーツリスト 名前順に整列 作者順に整列 更新日順に整列 ダウンロードURLを一括指定 バージョンを一括指定 作者情報 作者名: ホームページ: 開く キャンセル 更新 複数の作者が選択されていますが、一括適用を行いますか? 確認 ダウンロードURLの入力 ホームページの入力 編集を破棄してもよろしいですか? パーツID 更新日 カテゴリ パーツ名 作者 バージョン ダウンロードURL 100 80 80 100 80 50 150 charactermanaj/resources/languages/mainframe.xml0000644000175000017500000000173111767047555022415 0ustar paulliupaulliu CharacterManaJ - no name http://charactermanaj.sourceforge.jp/help/0.9/ Help Document is here. CONFIRM AER YOU SURE? Choose the background-color Favorites Include colors Overwrite Report bugs http://sourceforge.jp/projects/charactermanaj/ticket/ Forum http://sourceforge.jp/projects/charactermanaj/forums/ charactermanaj/resources/languages/profileselectordialog_ja.xml0000644000175000017500000000355111767047555025513 0ustar paulliupaulliu プロファイルの選択 新規 シフトキーでデフォルトのプロファイルを作成します。 修正 照会 削除 場所を開く インポート エクスポート プロファイルの説明 プロファイル一覧 サンプルピクチャ プロファイルを開く キャンセル ここにピクチャをドロップします ピクチャはありません カット ペースト 「{0}」を削除してもよろしいですか? 完全に削除する 削除の確認 このプロファイルは削除できません (使用中) (編集不可) プロファイルの選択 選択されたプロファイルへのインポートを行う。 新規にプロファイルを作成してインポートを行う。 charactermanaj/resources/languages/ukagakaConvertDialog_ja.xml0000644000175000017500000000105311767047555025212 0ustar paulliupaulliu 伺か用PNG/PNAの出力 キャンセル 保存 プレビュー 自動 手動 既存のファイルに上書きする 透過とする色の選択 charactermanaj/resources/languages/wallpaperdialog_ja.xml0000644000175000017500000000212111767047555024271 0ustar paulliupaulliu 背景の設定 壁紙(タイル)の選択 背景色の選択 壁紙画像の不透明率 市松模様 (小) 市松模様 (大) なし ファイルから選択 既定から選択 選択 OK キャンセル 画像ファイルを指定してください。 定義済み壁紙を選択してください。 指定したファイルが存在しないか、読み取りできません。 charactermanaj/resources/languages/profileselectordialog.xml0000644000175000017500000000300011767047555025026 0ustar paulliupaulliu Profile Selector New If the shift key is pressed, the default profile is made. Edit View Remove Browse Import Export Description Profiles Sample Open Cancel dropHere No Picture Cut Paste ARE YOU SURE YOU WANT TO DELETE {0}? Delete completely. CONFIRM can not delete this profile. (Opened) (no edit) Select profile Import to the selected profile. Import to the new profile. charactermanaj/resources/languages/managefavoritesdialog.xml0000644000175000017500000000052211767047555025006 0ustar paulliupaulliu Manage favorites Remove Rename Close Input the Favorite's name charactermanaj/resources/languages/colorbox.xml0000644000175000017500000000035211767047555022303 0ustar paulliupaulliu Select the color Select the color charactermanaj/resources/languages/colorbox_ja.xml0000644000175000017500000000033411767047555022755 0ustar paulliupaulliu 色選択 色選択 charactermanaj/resources/languages/wallpaperdialog.xml0000644000175000017500000000165111767047555023626 0ustar paulliupaulliu Background Wallpaper Background color Opacity Lattice (small) Lattice (large) None File Predefined Choose OK Cancel The file is required. The predefined image is required. A file does not exist, or it cannot read. charactermanaj/resources/languages/ukagakaImageSaveHelper.xml0000644000175000017500000000046211767047555025004 0ustar paulliupaulliu surface Confirm charactermanaj/resources/languages/partsmanagedialog.xml0000644000175000017500000000340711767047555024142 0ustar paulliupaulliu Manage Parts's author Parts List Sort by name Sort by Author Sort by Last-modified Batch input for the download URL. Batch input for the version. Author Information Author: Homepage: Open Cancel Update Is the batch application done though two or more authors have been selected? Confirm Input the Download URL Input the Homepage URL May I annul the edit? Parts ID Last-Modified Category Localized name Author Version Download URL 100 80 80 100 80 50 150 charactermanaj/resources/languages/imageselectpanel_ja.xml0000644000175000017500000000074411767047555024435 0ustar paulliupaulliu 縮小する 拡大する 色ダイアログを開く 上へ 下へ 並び替え 全て選択解除する charactermanaj/resources/languages/profileditdialog.xml0000644000175000017500000001115611767047555024001 0ustar paulliupaulliu Profile(Edit) Profile(New) Update Create Cancel Show Directory Are you sure you want to change the structure? Structure is changed. Is revision updated? ID: The unique identifier to a character is specified here.
For example, "default" is the standard profile of the “character-nantoka-ki”.]]>
revision Rev: Config: Name: Picture Width: Picture Height: Author: Description: Add Remove Can not remove the color group because it is being used. Up Down Add Remove Can not remove the category because it is being used. Up Down Add Remove Sort Up Down Watch directories Basic Color Group Categories Layers Confirm Are you sure you want to cancel? Color Group Name Category Name Multiple selectable Display Row Count Used Layers 100 50 50 300 Layer Name Category Color Group Order Directory 100 100 100 50 300 Presets Default Preset Name Parts 50 50 150 200 Recommendations description URL 200 200 Add Remove Up Down
charactermanaj/resources/languages/selectCharatersDirDialog.xml0000644000175000017500000000103611767047555025347 0ustar paulliupaulliu 550 CharacterManaJ Select a workspace Workspace: OK Cancel Browse Clear recent list Use this as the default and do not ask again. charactermanaj/resources/languages/mainframe_ja.xml0000644000175000017500000000223711767047555023071 0ustar paulliupaulliu キャラクターなんとかJ - 無題 http://charactermanaj.sourceforge.jp/help/0.9/ ヘルプドキュメントは以下のURLにあります。 確認 パーツ固有の色情報をリセットしますか? 背景色の選択 お気に入りの名前 色情報を含める 上書きする バグレポートは以下のURLにあります。 http://sourceforge.jp/projects/charactermanaj/ticket/ フォーラムは以下のURLにあります。 http://sourceforge.jp/projects/charactermanaj/forums/ charactermanaj/resources/languages/ukagakaConvertDialog.xml0000644000175000017500000000100311767047555024533 0ustar paulliupaulliu Export for Ukagaka (PNG/PNA) Cancel Save Preview Auto Manual Overwrite original file Transparent color key charactermanaj/resources/languages/previewpanel.xml0000644000175000017500000000162411767047555023160 0ustar paulliupaulliu Loading... Save the image Copy to the clipboard Change the background Show the information Add to Favorites Flip Pin Check Alpha Check Brightness Zoom factor Zoom factor 30 charactermanaj/resources/languages/imageSaveHelper_ja.xml0000644000175000017500000000253411767047555024173 0ustar paulliupaulliu 確認 出力オプション JPEG 品質 拡大・縮小 倍率 モード 画像タイプ タイプ 常に背景色を使用する 補正なし バイリニア バイキュービック 通常 透過なし グレースケール アルファチャネル charactermanaj/resources/languages/appconfigdialog.xml0000644000175000017500000001036611767047555023610 0ustar paulliupaulliu appConfig.xml Application Configurations Apply Cancel Ask the data directory during startup. Key Value 200 100 Confirm Are you sure want to close? Error Please correct incompleteness. Change to property needs an application restart. Settings Custom Localization Confirm 01;Compression Quality 02;Use the transparency image in Clipboard.(Windows/OSX) 03;ZIP File Encoding 04;The judgment pattern of a color group.('@' is color group name) 05;Auto shrink category panels 10;Preview Max-Width 11;Preview Max-Height 12;Use rendering hints 13;Use Bicubic 14;Threshold of zoom factor of the rendering optimization.(normal) 15;Threshold of zoom factor of the rendering optimization.(check) 16;Predefined zoom factors 17;Enable zoom panel 18;Area where zoom is activated. 19;The unit of scrolling by a wheel 1A;Draw Wallpaper by offscreen. 1B;Offscreen default size. 20;Auto Color Refresh 21;The layer not existing is not disabled in the color dialog. 31;Enable Watch Directory 32;Disable Watch Directory If Not Writable 33;Directory Watch Interval (mSec) 34;No Remove Log 35;a number in days until purge log 36;Information Dialog Open Mode 50;Selected Item's Background Color 51;Export Preset's Warnings Foreground Color 52;Checked Item Background Color 53;Sample Picture Background Color 54;Preview Background Color 55;Invalid Cell's Background Color 56;Grid Color 57;Author Conflict Background Color 58;Parts Select Panel Hovering Color 90;Jar File Buffer 91;File Buffer A0;Draw grid on preview A1;Grid color (ARGB) A2;Grid size A3;Unfilled space (check-mode only) A4;Enable CheckInfo tooltip (check-mode only) charactermanaj/resources/languages/importwizdialog.xml0000644000175000017500000000745111767047555023707 0ustar paulliupaulliu Import Import (new profile) Complete Next Previous Finish Cancel Browse Import archived File (zip, cmj) Import from Directory File not found. Directory not found. Are you sure you want to cancel? Confirm Import contents Presets/Favorites Parts Sample picture Archive information ID Rev. Name Author Note Sample picture Append this description to the profile. No contents. This is not a formal archive, but may be containing some picture. Profile ID mismatch. Profile REV mismatch. Import parts select All deselect All Sort Sort by timestamp Check Uncheck Checked Parts name Category Image size Transparency Last-Modified Last-Modified on the current profile Author Author on the current profile Version Version on the current profile 50 100 80 50 50 80 80 80 80 50 50 Import favorites Import used parts Checked Name Missing parts 50 100 200 Complete charactermanaj/resources/languages/previewpanel_ja.xml0000644000175000017500000000237311767047555023634 0ustar paulliupaulliu Loading... 画像をファイルに保存する
(シフトキーで「伺か」用出力)]]>
画像をクリップボードにコピーする
(シフトキーでスクリーンイメージ取得)]]>
背景を設定する
(シフトキーで背景色のみ変更)]]>
情報を表示する お気に入りに追加する 画像を左右反転する 固定する 透過確認 輝度確認 表示倍率 表示倍率 30
charactermanaj/resources/languages/imageselectpanel.xml0000644000175000017500000000067011767047555023761 0ustar paulliupaulliu Shrink Expand Open the color dialog Up Down Sort Deselect all charactermanaj/resources/languages/searchpartsdialog.xml0000644000175000017500000000133511767047555024155 0ustar paulliupaulliu Search Search Condition Parts Name: Author: Category: Clear Results Select Parts Category Author 100 50 80 charactermanaj/resources/languages/colordialog_ja.xml0000644000175000017500000000221011767047555023417 0ustar paulliupaulliu 色 - 適用 リセット すべてのアイテム RGB置換 置換パターン 明るさ コントラスト RGB 透過 オフセット 倍率 ガンマ補正 HSB + コントラスト 色相 彩度 明度 色グループ 色グループ 連動する charactermanaj/resources/languages/importwizdialog_ja.xml0000644000175000017500000001056411767047555024360 0ustar paulliupaulliu インポート インポート (新規プロファイル作成) 次へ 戻る 実行 キャンセル Complete 参照... アーカイブファイル(zip,cmj)からインポート フォルダからインポート ファイル: ファイルがありません。 フォルダがありません。 キャンセルしますか? 確認 インポートするコンテンツ プリセット/お気に入り パーツ サンプルピクチャ アーカイブ情報 ID REV 名前 作者 説明 サンプルピクチャ キャラクター定義の説明に上記の説明を追記する。 インポートできる内容がありません。 CharacterManaJのエクスポート形式と異なりますが、使用できる可能性のあるパーツがあります。 プロファイルIDが一致しません。 プロファイルIDは一致しますが、リビジョンが一致しません。 インポートするパーツ 全て選択 全て解除 名前順で整列 日付順で整列 チェックする チェックを外す 選択 パーツ名 カテゴリ名 イメージサイズ 透過 最終更新日 現在の最終更新日 作者 現在の作者 バージョン 現在のバージョン 50 100 80 50 50 80 80 80 80 50 50 インポートするお気に入り 使用しているパーツのインポート 選択 プリセット名 不足するパーツ 50 100 200 インポートが完了しました。 charactermanaj/resources/languages/searchpartsdialog_ja.xml0000644000175000017500000000137611767047555024634 0ustar paulliupaulliu パーツの検索 検索条件 バーツ名: 作者: カテゴリ: クリア 該当結果 選択 パーツ名 カテゴリ 作者 100 50 80 charactermanaj/resources/languages/informationdialog.xml0000644000175000017500000000215511767047555024164 0ustar paulliupaulliu Information Close Parts Category Layer Order Size Color Mode Image Action 80 80 80 50 50 50 150 80 Copy the file path Edit Open charactermanaj/resources/languages/appconfigdialog_ja.xml0000644000175000017500000001304711767047555024261 0ustar paulliupaulliu appConfig.xml アプリケーションの設定 更新 キャンセル 起動時にデータフォルダを選択する. プロパティ名 設定値 200 100 確認 編集を破棄しますか? エラー 不備があります。修正してください。 設定を変更した場合、アプリケーションの再起動が必要です。 設定 言語のカスタマイズ 確認 01;JPEG圧縮時のクオリティ(1が最大、0.1が最小) 02;クリップボードの透過サポートを有効にする.(Windows/OSX) 03;ZIPファイルに格納されているファイル名のエンコーディング(csWindows31Jが標準) 04;パーツ名からカラーグループを判定するパターン(正規表現)(@がカラーグループ名の場所になります.) 05;自動的にパーツ選択パネルを縮小する. 10;プレビューの初期表示の最大幅 11;プレビューの初期表示の最大高さ 12;レンダリングヒントを使用する 13;拡大時にバイキュービック方式による最適化を使用する. 14;表示最適化を適用するズームの閾値(通常モード)(0は常に無効) 15;表示最適化を適用するズームの閾値(チェックモード)(0は常に無効) 16;ズーム倍率の選択候補(カンマ区切り) 17;ズームパネルを表示する. 18;ズームパネルをアクティブにする範囲(0で常に無効) 19;ホイールによるスクロールの単位 1A;壁紙をオフスクリーンで描画する. 1B;オフスクリーンの既定サイズ 20;カラー変更時、自動的にプレビューに適用する. 21;カラーダイアログで存在しないレイヤーをディセーブルにしない. 31;フォルダ監視の有効・無効 32;フォルダに書き込み権限がない場合は監視しない 33;フォルダの監視間隔(mSec) 34;正常時でもログを終了時に消去しない。 35;起動時に古いログを消去するまでの日数。(0の場合は削除しない) 36;情報ダイアログのアクションを「開く」にする。(false時は「編集」) 50;アイテム選択行(フォーカス行)の背景色 51;パーツセットのエクスポート時の警告色 52;アイテム選択(チェック行)の背景色 53;サンプルピクチャの背景色 54;プレビューの背景色(デフォルト) 55;不正行の背景色 56;グリッド(罫線)の色 57;パーツ作者入力で複数作者選択時の入力ボックスの背景色 58;パーツ選択パネルのホバー色 90;JARファイル用バッファサイズ 91;ファイル転送用バッファサイズ A0;グリッドを描画する確認モードのビットマスク(0-3, 0は無効にする場合) A1;プレビュー画面のグリッドカラー(ARGB) A2;プレビュー画面のグリッドサイズ A3;チェックモード時の余白 A4;チェックモードの情報ツールチップの表示有無 charactermanaj/resources/languages/informationdialog_ja.xml0000644000175000017500000000231511767047555024634 0ustar paulliupaulliu 情報 閉じる パーツ名 カテゴリ名 レイヤー 順序 サイズ カラーモード 画像ファイル アクション 80 80 80 50 50 50 150 80 ファイルパスをクリップボードにコピー 編集 開く charactermanaj/resources/languages/managefavoritesdialog_ja.xml0000644000175000017500000000060111767047555025456 0ustar paulliupaulliu お気に入りの管理 削除する 名前を変更する 閉じる お気に入りの名前を入力する charactermanaj/resources/languages/ukagakaImageSaveHelper_ja.xml0000644000175000017500000000052411767047555025455 0ustar paulliupaulliu surface 確認 charactermanaj/resources/languages/exportwizdialog.xml0000644000175000017500000000552311767047555023714 0ustar paulliupaulliu Author Description Character definition Export a subset Export Complete Confirm Are you sure you want to cancel? File already exists. Are you sure you want to overwrite? File not exists. Next Previous Finish Cancel Include contents Comments Sample picture Parts Preset/Favorites Sample picture Parts List Selected Category Parts name Last-Modified Author Version 50 100 150 100 80 50 Select All Deselect All Sort Sort By Timestamp Check Uncheck Preset/Favorite List Selected Name Default Missing parts 50 50 150 300 Select All Deselect All Sort Export used parts charactermanaj/resources/languages/samplepicturepanel_ja.xml0000644000175000017500000000051311767047555025022 0ustar paulliupaulliu ダブルクリックでピクチャサイズをフィットする ダブルクリックでピクチャサイズをフルサイズにする charactermanaj/resources/languages/selectCharatersDirDialog_ja.xml0000644000175000017500000000114411767047555026021 0ustar paulliupaulliu キャラクターなんとかJ キャラクターデータを格納するフォルダを選択してください。 フォルダ: 選択 キャンセル 参照 履歴のクリア 次回から、このフォルダを使用する. charactermanaj/resources/languages/profileditdialog_ja.xml0000644000175000017500000001225411767047555024453 0ustar paulliupaulliu プロファイルの編集 プロファイルの作成 更新 作成 キャンセル フォルダを表示 構造が変更されます。よろしいですか? 構造が変更されます。リビジョンを更新しますか? ID: キャラクターセットごとにユニークな名前をつけます。英数字および記号のみ。
たとえば、キャラクターなんとか機デフォルトパーツセット(Ver2)であれば「Default」とします。]]>
カテゴリやレイヤーの構造が変るたびに違う識別子を割り当てます。英数字および記号のみ。 Rev: 設定ファイル: 名前: キャンバスの幅: キャンバスの高さ: 作者: 説明: 追加 削除 このカラーグループは使用中のため削除できません。 上へ 下へ 追加 削除 このカテゴリは使用中のため削除できません。 上へ 下へ 追加 削除 整列 上へ 下へ フォルダを監視し、パーツ画像の変更を検知できるようにする。 基本 カラーグループ カテゴリ レイヤー 確認 編集をキャンセルしますか? カラーグループ名 カテゴリ名 複数選択可 表示行数 使用しているレイヤー 100 50 50 300 レイヤー名 カテゴリ カラーグループ 重ね順 フォルダ 100 100 100 50 300 プリセット デフォルト プリセット パーツセット名 使用パーツ 50 50 150 200 お勧めリンク 説明 URL 200 200 追加 削除 上へ 下へ
charactermanaj/resources/languages/imageSaveHelper.xml0000644000175000017500000000232011767047555023512 0ustar paulliupaulliu Confirm Output options JPEG Quality Zoom Factor Mode Picture type Type A background color is forced. NONE BILINER BICUBIC Normal Opaque Gray Alpha charactermanaj/resources/languages/samplepicturepanel.xml0000644000175000017500000000037611767047555024357 0ustar paulliupaulliu Double-click to fit. Double-click to view the image full size. charactermanaj/resources/languages/colordialog.xml0000644000175000017500000000207111767047555022752 0ustar paulliupaulliu Color - Apply Reset All items Replace RGB Replace Bright Contrast RGB Red Green Blue Alpha Offset Factor Gamma HSB + Contrast Hue Saturation Brightness Color Group Group Synchronized charactermanaj/resources/resource_list.txt0000644000175000017500000000267511767047556021422 0ustar paulliupaulliu# localized resources _HOW_TO_LOCALIZE.txt appinfo/about.html appinfo/about_ja.html languages/appconfigdialog.xml languages/appconfigdialog_ja.xml languages/colorbox.xml languages/colorbox_ja.xml languages/colordialog.xml languages/colordialog_ja.xml languages/exportwizdialog.xml languages/exportwizdialog_ja.xml languages/imageSaveHelper.xml languages/imageSaveHelper_ja.xml languages/imageselectpanel.xml languages/imageselectpanel_ja.xml languages/importwizdialog.xml languages/importwizdialog_ja.xml languages/informationdialog.xml languages/informationdialog_ja.xml languages/mainframe.xml languages/mainframe_ja.xml languages/managefavoritesdialog.xml languages/managefavoritesdialog_ja.xml languages/partsmanagedialog.xml languages/partsmanagedialog_ja.xml languages/previewpanel.xml languages/previewpanel_ja.xml languages/profileditdialog.xml languages/profileditdialog_ja.xml languages/profileselectordialog.xml languages/profileselectordialog_ja.xml languages/samplepicturepanel.xml languages/samplepicturepanel_ja.xml languages/searchpartsdialog.xml languages/searchpartsdialog_ja.xml languages/selectCharatersDirDialog.xml languages/selectCharatersDirDialog_ja.xml languages/ukagakaConvertDialog.xml languages/ukagakaConvertDialog_ja.xml languages/ukagakaImageSaveHelper.xml languages/ukagakaImageSaveHelper_ja.xml languages/wallpaperdialog.xml languages/wallpaperdialog_ja.xml menu/menu.xml menu/menu_ja.xml charactermanaj/resources/splash.png0000644000175000017500000013633511767047556020000 0ustar paulliupaulliuPNG  IHDR+miCCPICC ProfilexTkA6n"Zkx"IYhE6bk Ed3In6&*Ezd/JZE(ޫ(b-nL~7}ov r4 Ril|Bj A4%UN$As{z[V{wwҶ@G*q Y<ߡ)t9Nyx+=Y"|@5-MS%@H8qR>׋infObN~N>! ?F?aĆ=5`5_M'Tq. VJp8dasZHOLn}&wVQygE0  HPEaP@<14r?#{2u$jtbDA{6=Q<("qCA*Oy\V;噹sM^|vWGyz?W15s-_̗)UKuZ17ߟl;=..s7VgjHUO^gc)1&v!.K `m)m$``/]?[xF QT*d4o(/lșmSqens}nk~8X<R5 vz)Ӗ9R,bRPCRR%eKUbvؙn9BħJeRR~NցoEx pHYs   IDATxx\y`3(EUX,ˎ,;n&l/q6/uҞYr\e9bYT(Eb `zo %[Ypp{?o+G.Y.lh.$3ɬD7l6^3 p̌w.2uf %*ZJ|*QNI6PPe Cs\Dc^bYl*cp8NL&L'NS(KJ%JDgGlyEI>K(9JIY,EݡD<;Fܵ6A+N\զYTJM6Ӆ+^umedD"LtL&H$bX<OtB,~Fr'6׽ekNTR%J$:#&a4PZ> I|Q%%lT.+7>5P$OD u^Qj e΢1i Da:R8dɹp*;1rTV7̉Xd=#NVYѯ+rZ>P*UBV1|.0Dfѱ1^fmmZ-.-LioWjAkTrH4 'I,.)7O8/jp-h8I燦}Aޢo2)2r9CX_NJ}1#2x4'ghRiLT$oհS\YjԗT< jq0^Եy淺ii]<.t Fsl2kxr^Vk_.ƢLcN›ʄPPKA_,J(&iHl$`ˤ>V}ryC~3&ہH(6=f3f^fQjKŅ<(=ت"st:F(tRQkZj6K\|SU=^G*?l:1,6˴*UU8dGYBV1* !_LՖme.%0A. 9(LKT|wsB>j Ccse MppEI&?4D#BFU:ljjRa)I+ߓ#peҴ^mX.B (j 毅*)YTHpB%QR O yCO$c)QѪO |pm/g4+J #}De1e/`L ,1\C\t3Ͳĭ*g AkRxʇx=\t떵ެg/YG*?ږ&ɀ%S f=D D$| n2IDB^)lUֹƾ JRUJVkٟ7>BN\:a *>UTAĹwA4*OcĐwcdUԖ^r'f:BosOɬi---?P;w`44ohY^_''w{ey`{RJMu^T5olv׸50)K?WyՆFz:fYUEU-*eZ6Z/*=JhrmOZtfӂB@y6WKgAu!t"ɲ7T6h،7175LDܵd6s wvڶmڪ߼^{ŋn#Ȫu.W877_/W\fW4^~1LádmFV%1PԌwޖwd" įY!R3+6؅0R̊lA _X|^QQEHC!PR x1LyuuEfcZJDSY"4=Ó-;**fҊb3^~ݻw+_җ8pe7ډ;Kaju&k ߥBUNt Xj)MC*-p^Zq94\ +(J'3-+JƣԗED)PR(U5eQPfYj*gTFm}[[i0T%º:twbbog.O]bz S|/?dߎYMo Lޫ z+l0dy "-8¨ F@ v:֠V(̰S4I¡ϷdKEԉK78~T{-%8-U3fx< օFx<8ʼK7?!T/0JWOqkhXz mky=71a^ţ?}? 媽r(eAv_ZRfq)qVxG@=O}\VӄBqkP(-4I_X~_}G\-_sekpDX7_F^oqoݾ ɠꇽ} Ru.xF߸yX gƼS]cSLyEv8voLP>ӗu]:IwV2,ъ2Dw/}o5⏽ŕ?i8vwRcLSaӶZ<_\XSeZa¡~m1p:h:|7F='fq|*﬍UX +mYpFKA_RkZw7B؟]sOww/WW:G~qe=U+]c%^0tl|t˖-_ʗs8iwɠ!eaxWotxLp%dXݧեׂM"7}8:-jfq6v-l /js\QثoW g|dg>Yx7x#t{8}c3 ;>XI;7/kva@gMgY9\ÑrD~)_ԯxm])8췸B @,"*7fc;Pu*5KԬvCk,OCy^/x; %^¨RUֻ|~^Osfk!* s{:ܾ3csNtC I2a)msCPZ?_3‰Kp9<7?O@JmTͱTG|̚OZ$}A3=:˽ ##[>Tf}$!-Zav2s%`U. t+Dz5&M7lj'0>rg<4ۍԐYe'N{}ӖygfC5#p8>:SN:;g>G9DGUH > siQ&vzk](?0w~?FTKm\KC3rW-$geaɍ/YsGÀd"J(7^DA-3t-Mft |p|rm07! f~n=;TaiUZr$J]JXPY _SXYU VF*8xZTU^8YQYNiA%'*E78voqWuvuk};+ʴC<1Tj""!!yI4P\6]SF 97a_܊uKS}3IW+i9vTBӣMn82^d&Z IjԡtTXиҚDDPsvYgT'iNssuNxтW\_Q/>TdeR4iC9T&֪n=QJid{F'|q\dH@Fe}OR2۫g&0!z*a1! -h?rH  ٲ!GGhQ›\K;\לmwC(Nz>ݗK$IwZ ݲ{.z=AlVKrhPK|@+j\7nHNlv1X|¥:K[T?ˡIIDO4ḾQ>oͩܗ%E‚oGCOcOѽѺf*7"s˖&%0["76IeE S;=fjӧ+k,N i \Ha39֨'[[#z``#Ql{zճkp';p Fgzl;*/#*"M] F|5S^H_Y=X6/]8vAe*(:UNf4e%O*ЩR{GܚKيIcL -[ƻ|US$_6#Hh3#nZ*"’fp~R_2LM9v–#"XS էNؒcM"͕~H%*̳&D:X[ډ#?K} =5.wH$;q?7gv]!BOFaqs`5psSW, yp,ɱC Ql, cMrep4$$0y1gJNYIeοq "AYE^IOL}}G;;}3 j 2A*wرg_E۬/#d?xoCd E&;$Tɩy+2X/~M h~Sg[l#UPb;1_+843f?9JdðU!zd$*$uqמ]IJ(^f )7/bduu _nf;D|,XbtT@jNi'A7c3;LzPJXRO'KfK2t`&62;~z_\tvvj.Äv%"Rz@Z=LܾryJ: 3GyWd\ ^~ߤ;.ݓN =9HX80Hjl5|֫xef!S;~M.6_ɴFkNcQe F]sI YL%K6'wc:u:-9)4%6dqp)s;`)_h+60JMy%l\(pI1 ѽho2[,~Dro2!KF!BPf3yVM]h Jv IDAT`򤍞ĪܓCQ<Zulj>6XBHeF*Jǝ6ZoIAtcVV:bΡ#;v-#pɯySn˒# Tt8&bx\r&:gdC'F C3d&X3 v2R,'YOƅю0Wf=!PAԴKP-^>ft.% K.IK)mиv\:ߺ6U8/ ꓧ "b+J0,Lb'm"^#W#hhltzpϡ-B&X gѠ̈́kiW>U8.ߩ|5%?w y{kF[mr *N)c]Դ?s£r X1<8Ne14O Xێu.,e#" HzNfiQLNN>VP`fȐRPK,*H PDAhH&,iXub1BR$ɫ\(_!^ДeeUlfmz vH dRZkj,tj߼#B5xx7v7McOGjܼWxZ&&.? }<كkeɢd6sS,Hx09/Bډ7Bgw v S-ꏨL4c ̰OH,렺rWC,-+JBFf"[=c?[k/CXWGĺ>aK7x3`Q@}|`*J$ȼ+#Fx%n(QF.Pd.rB`ݞ1HƜsQLQwvx,}∹SYzǖbk!:Ow_׀xǐ|j+|,( VhyYBsxu-UժT C0s%Q`ɅQALL͈j1OK.QiC 5XZZ,m/-wۇІD> DoW\uS\}4[oG֝nܶ{ \*1;9b|31QA>|݀>_zy1苺k#B<#?AG'yi2@vYat TN c{;?~ݱgΊtP z=ˀ-a1sRR)[?ERt-hVg+hTzm{7WC~N!)`^fKDxDey\XftT LU: kfg2/U5[peF_^]bސ!:O+zC[-iΞ7OcmφH^B=N1ti<A/|S-c3|SaK@*|y=<( Fhךl3KԛwdshKd* `.2B-xB !d:0 PH]Ih ]sG/uU/!|EQQ&B:? ݦ9O _kg5S/B; ZY%o9]V%bLlb8`~{. mՆ)NH(Sdq1S&3i"Z ut<\Un07T'4.P9 8Lstj_ %X"1@4 P ?슉Xbm[L6hߴ|w*+-8ݐj0}gjIeOގc~΅*k]g/6hTdS~('C)7=" oڻ>Mpj9SS0A66v.CӮ vK$"E ^TeZ|M|$uِx_$<*1HGiҘAH7q?3L=%mZ E A`e{a (25. 6Ky)cc1`9d_u3 .bg~p~ڲ=]s{_|HX:x-65:ad[)K`$% VF O)B̷ 9% Yai$@"Rg:ǥۤbi2XJ ͝/\2 QoԢudLG& }f:[D68drr!MА*Jrse-aEFFFsICY o#bɐԅ;U)%}NZ"Ys/o432Kru&=}L꪿1D 6VjAq[b L@+k-$hXv-MpdJd`h&nMGP@/P2&*Ϥۂ $S!4BIjNuryB Bږ3 0rZ-A?Eq¾|9oXa'N:F2Sp `uv*8T|$%YC1+(-L(i ;dgg%Z{SΊb 5M|n~]c\1V$#{8y5{P["PXK"XoIR",@⸐W ת6Q'18M {4YODFLj-lU1R{'maQq$-Fүh<쮯~#'6 KHhFA|X&pF^ S" ^x˒3|I"唓LH/]F/}=Z^|k W>ץhb̑ c{{>)WJu9 DlcDc-%2 ɋAnqe$V"ɠdlr!WAb G%Bc܂2L ӑ}"mȪbX?@ iYq@R"\P&TWeiwF'ŪC݂gqY~y5!Z=߀|RǔZW%f&KXp 1,5;;3K /9>:Xy7zώ~O#}Q`[(gaCg0LpRj 0 wJh\LK|"&lD\gF!"ic$K5حft' G?F8BQv: #b#_^hTIOLQo2WN ԤLNV `p`wRZv Aegr7aGZK/vy2'j;RaԒpY`GaY|J=6yHRd12F)aB}#SѤbw#ARQauG m9XX:S\֍C/KMkI1q +*+Ӿ|WE ԀWsd`.|/ZtՎ֬?1ZG*,b*\^ʖBanګ nO_zvy䫯2.ϼ0z1, 3`x@Mx#`+ZN$6 lli4z87j2jJx"DMaT4fáHHԇC lȀ!f>?6 w !$jo !zɹ4; rQ uHd1 9il^! @x.i+-x~su4DO-{ia[tM $(ޘȶCHq_ c/?Zye]pur|4h-DwJVO0WV;2 fN|TQ܈&ZqyJU>&#,cP~qqkMBdҫR܌7(%}Rb~9$ᮻԡ T 0'qzB):QYӬS.YHYlwot $!92c?ܳrfec?› %U8Fy[>t/B= FcBZY2V҃?M߸Cnϕ IQPH0:93M 'B`U9« CL(EK&*"!|#s4y5'a 9BRsw&&^Tb]qgyC^e6M ׻^&t VOd6lglY[hS4LO}-3ŞR4}1gB) t.^(_$* R|1- ˉ7j2篿1Vp4J=ᙺJvêTp]lD7y(6uիz43H[o\T0 :/̖}wĠ TT CNx$F3/Anԇe G[ AP^fp_'R:]fJfpj!|Hւ9V{Zb8ݪRV)L%_4WH)p(߻u绐SqPefnWhL80mf77nyF~d0/T>#,ژJH*zRzʻbXC 2FebjBgǿގ-]\Yx9*O;?~Il3/KF1;_޼; QC? ,RWܷqإi(4Ņ_`@[6ϒvn2 Tq1-JZ!>rPH*?.I;r zJ!B}e4/QX h4UH̭BMTLƐ7#@'ue2+r0Bj.X~1iwUDzwt9M-8CnA[+eopp9矎 L^*M Dc F(bOnڿjsP[w'ΌY b(,QF lq{m=6vm _ u gJćSh!N[ΌYjD"PߤAz`eUg;64 `ӔiA{vi3Wiq_}m_Zjߴ%4dR>榅WK7y{bi^YE{[wȽ+s:9<uۉv[JD5y/aD 5?&!Q*EozN>y5Qn%Ju,{Dα8QTb: $\];1L@F9v,Dgr!o-[PbG`5:\E3ۚ7 jT.:@mRTbScls8: /l΍[kyhw oS, W,;5jF' IDATF{`JI(HY}~ﮏޣS[g'8_;:GxeW}s^4C !P,Q"vML ֺgwfcM g͠,KVHYAdFXEDWeN`ջ6 ɐP(ėfFsirj0U"¤CB$rQDdYYtnM:SEJ{.%/,)hc ˸:BUjdž©UwAܼoCյwF^-\GOt裷uW"7%iZiWexY3pA(ܻk'x$$FCYNd`|7xON@Xee"{2&CӜ/WNm*r,mo!U$ɒ>2$ T8zqHJ7&LΑiwpjO$$D$qE3mMF}8`-`/Ńd]7P*GFmi@ %a_8d*Չ55oD_ySKN_[vO`S/_8a ֫z8I+\YG~v&`̄}XAQbn|SlZ# l.9{ZG ;'xK@E*sd2ir3g,|W t!;T+w͍M04޶EӲdX, p˥`b  OfT*HM3;,.A$T08̫G$¡J; `łM7!z*kF;Wֹr 8H*~:K}'q%&9m.`&M:g4h !8>C  A!(loYS` +.xu޿| U~&F 0K%K۴?zC[yɴJS(a\·efԄvn±XHM&DR%Bݵ_}H4Y o\vEBrk:^Us;N~ X*B#oΛ3FRʮ| ŋy?xIEBRuyJk}o2Cino&2o1CdqQJe}UP-APQDqWCi4fi6k>wp e2T(O ͭ|9ooBmķ_:AYw4j|ϰ;k>c8kWQ .ZX~҂+QF713*c GDᶮT.}G4:وGDKݖӽnyŻ\mzˬrUD8HMAФVl <Ɨm['K+l9GEl,Z>#PV1|iLUY nWY=29MՈJFoXLZ\LR @X('E]׶Ppo$6xTEмyĀӪ UZ%l_6m̌Pߣ j"LTdvNI4:ϩCڬ5UV[݁h{d FMmYnuAFm#iM"+Ye{ Eh`')>ǎ{'y VirY.ŷYoi񁮘 7\5SPvֶ)<7R13D "kņT|4l׻/ {9+ z)bU\]T81Oڬ7z?'?~[?z_!["/p BJ?76QΠy zGhDP4 5{:$wmSkuUNgpxz!Yv1;::&''A]4hYYAf5&PH&"MMLseC9"8 ncM+j r`[΃'?zd)B ~8mO y!(A e&JI;]K](!7|_{wNAEѨK4:AlJCuaS'])@#jQG _2=?_n@r9Z5iuiFp8<00PWWW 2/DH7UmզEskUu.N9<\{Laeϲ<yeNɵN`,fˊKJ1x◽|&{Z&ʧ`M%@/|B7>G ~˚ #/~o?,/sߵaF}B>,>JR:*! |G#<95Íp5f٨54|PUd7 G立'$e J>s`Tj& Fa87FР f-$ʡKc,PR`9>ݶk8_x6t[IqQH]QZF_!wOC<߄o/ofb8y[`]N\G˹<>[c%pb S{ƀ0!.HE +(BeN =̏~?,)shTt؝UZpHFFF{7 D΁֍p Ts0{ +W..U-i!sP1Bl*l[;GG:Zfdj6?'>_~m=+4l|pwPCCEѣg;~?}0cس]Sc[Yrv/t̹P(,n./  Q.lXo -ՄBD8 V4V_멫z 􍣕oM A憇\W_dYM̀9oVw\$r~v^[ED| r'}cmݴ|M\rW7sv[/\,q\\ ⳿oͣ;ooU]^WVa\9ct*8 W9\]g&ooܳ+L75];}t\y~ɧ&&9GoT^A^yo[Rpb].Vl47/j}2D8M77TkV0zdZԎ0HtʂeY2E8陌S^?Mbr[͎>~<-:6Ӣ|^JVy"}~ Fɑ] ,!Y<r.ԕ?ٽm3ߘ捲2SdwZnj'aU"[;nS$~\"G*U tjaU'3|fŮvViUj;;90;z<ݮu֕mp+-i챱C0JL~?}Dl 8%=M<>o<ɔ'`&2}crzG`ItOU cG@@P,XVKܱGy]_S!vχ0UFWNM|es&o'i .T4jb{~Ž +bBn5*Yh 4|IE|7]M]:e`I{Iao8oP$5da7R' kGG|{ ctѨ8ߕQā475>];.M #~+TyHK/5M '8nR=w.imcޑxNUտ{q>al\8Xzu(|4J/ˮk}fb赧BJ2P:W۞FX/^?|Yp6`9LMf5_eϓ;- G{sh*RivZJک7B]֛Vߵr>ǵ)cڢ}xVECEB`& n1>at!^T3;:l oO߇i YK}BYGWΖ8@ߧ9x7Ô! i۫7l%c- &9;&Cϔ&%VSmS;=Ao,^O0u] ]^9#c";:ԹDVk8~Ua~p:%{:d|[8Ld>[>ubf9 .≳cp-S:Xeo(4rVG\YYsmsw4x>?ʐiƒi$8^3/|m@•; XOB89V)QM$/Mj.4# ϵml??W?,ix,eyBEfX8p<|WwF}Rvsuں==,HO |`d8=Ҵ~ɉxW%7ᩣKeMnd΍|'7y< J؛l{rRz-ϟORpJ!V0G0ʜ4IL{plL$Z`AI6uvj)) ;pY'fQdġw߂._SY?0xjWaO.ʺ_p:ҩ;WZahU8?#\]@]eRtBY_BT7-8^B}IEQEiW\pSbccT}py"ho}[x3DҞHH V!ɨ]yb jH[Lk)CYng?}nzk'/tStw⿋4jNhVdD-ؓ-N.2X8<4(4H|PuJ[R ,h .<=u7S frh"9NqˎN2_uXAVnXkp?߮}7߷7"9a$h(@"2쉢M#cR) @,Nve f?$xa_ T0>Ʉ7UWȅO:vZYڐl`r'iM+W[g' O?lkt\x dl :8DQ(p]O2﷒N@D$4a>ɋB:']i:y(;␇~ԉ-7pՋ[I˼-ja*8jG'VZc%뚘wu;Np4v:ZOOXQn~OՕ h+ZGFފmu+?'ޡc%7s\D9Qd4(Uy۪$d$@q0_WSg Ah 3'% =T)Rxho_4{iЊl)Ztu\|b[ot|6sb"1$k[\sm^xf߻Ѩ/瓩\2% [,KV \ψ!| Z"T;{ F0F'tRtS%b% !@1H>{{EJދ-[cjB |$^RHB yCfk%mӮgZ57lb4jvf{9sj|$W F\#!9 !eLON h4= a{ |s%SImpyNBEyXj,b}P<$A|FX aD=bI,L: Pf-9I7pH.|vάADxVmy˯BHbP v,0gpbHNeΌ[ \ `,)mOF@9/(+T8|!MPdHZQQ|8AΑe('Jr%ܭ p8Dt8_&xQIut0|p\_7=x / n(H(($G"I$C\[K4 ^)> hf́& vtRfHQo~n!ۗ)%UAd |pK>Y4E0=VLB~!Jb@|@'0ϣy8u. @#i4ukeBo;w p@tщw-YE'`( ο].TCL,O'}rDDVMjv&*sLVeU~96\NFvѣG8j5Y|>S,b )0%F@XI}Wjmt>_G.Iʆ3z}6HǕa j'z (!/4w&lY|п,+olA˾%u"6KEݠ&K8S@ yavF3&_ tH3 Tm IDAT<.m?"$DoH܅|~ٲ_/`RKv O8tj9͟nXP yߟm[&-Mn?.]h) .+'ojhtNd#0t& D1/4 ݂B0 ?|⥋c.w,$7xJ#^ ĝL&FxQӝ6Qg zr##±bv67qm,(qa{l}eWC* d1w΂ŕׁZE)boI5g0,lM 2H'PβK:/м {l/ p2LQr$ Dfq9ryf޾I4AȤ"!Tz^=|O>jy˳55 aV&Z&WP"i;Fo+C#'c" BG|~M.Px#kkk}y5k9x )4X/D= AqngSsj %\ 1`V^SC&͕+*H !Gc&n+f ׃~qu*kNJa;եmJ%0c29M唙6AW;"}Ͻ~O5kji5 H8No1؀(`uhÅ!&L~s^Co\k$Yl?&[mR3}; %YZ(~pPa*F2UY=j.x(9\`LBH+W"Boӧ8jkHاnY;#UJE٩53 ͒ŔA eᆌD,#V9W,>1ʻd,&H$ *+C,*i CbL4;`39 *sÆӢD_l۲bC(T1XpF'^0 c4/ =(UyaŚShnq(3Phpbq[F)*6-a?ģ0# w^_"GHWInn 8\^3)Nw .g O,da+hw#`M S*'CaPa ̨HbBd2Qy` -m|Aftx d: 8,;' ?޸a=`}ă/y~ҥR]ྤJR$ !ABoK>(y ra bIgIPP vJ8Lj%)DY2Y<4 f2EHЯ:[>S/t\S4m %$X[nu SvL%lK#Àdܩ[&m %I&"oDOK%0<5P i"hLO~!_Pĸ/`D.$^N4 ^M?AT:a=pQyAA< G֖K=с .0!1$|t3jgER|^ZoD(И1pEf.jZut5E cavY\>'I 8#p ld3d:7E[Ͷyㆽw]$S kP88,k5]2cLB%*AСHc>RXW[s֦c=>=moj"bnO QR pk F}? bPbs 0A3x,Jm_ 9|O>`>ѹB!aw .c1eW7 W{i%[q⫴]6No8\h~U?~m% "vh ".Wy}@(OɁPO_+uC͇CmzOrqK q4'!~ |S}}}bfAoP&ۥJ*]>Ckl G.SUy9YM'[urX(!4 h6 6] PGIdBv˾`UY5u9۳4ڧij,J|S-(RWU3= xC0Å HEcJ*DɌ T6n1w[FW"7D$|xjnor"f"+7sG~`͘.WX}#ݱW"!~HY.7kXJ}=OMW>A_~yT: :S[}(|MaQnI|U 05T:NW2 LYxg1ڈݩ[Lb֬X XY^S5ލ˗r ^;,nokmհJIP7AڞUm(bݙ~x3-Mg{v@ݎ\.("ˊ9" 1MV)oʲs8s/ơpD΂5 ԧO2OSV]wd֬؏~A#X(Jʊ@#<^AijH\\tJaFLu˖3wѾC˼"a NB,QΘ=XI Z'˖h3>\s =+qoT醩B]~hdX^i&NJxa#_8 EW hgg?~t+]WM?Yf]#L\bp(|LBgzukW f{@ !@9Aٺrr KQd 2uubwf3 Y,9C"7{`Ӈ3lU~E%! F -l|Pe05$' 3vL?_U3CmT`3;y;ۻoKiȮ{O>*RvXM[6"Jiyi%0{*<3' LaxE|$~<|ۺ o^ D*#!PͶ5e̤43w^#UIJKmi4|NL$sPҿ=_xviXlzUܑ74PCd/3%95]zk}P4Tתԙ:~$r 2u{h-x !;ofDq{Q?WLBg}YyꙹV\Q__aMS8(0@UVزdX,X!ʯBv:C?io2YKSNٲi.KDlCTˉ2[',糯\1^{~+7_ //W*S1}.Aұ)J@PN뷘o+gN7}H 0&!0L'xMND܊ʊcGOtM] 7[+t,]"&ҭ?믈 ʟܚP(qHºpJ29ήmvljv]`sͼJvpor@kd?ݽZ&[ףfpaP(~/uo}n.-&`r.j*p"=+.^Vœ83Nx.P4o|`{~h |Ji0^Ӯ;h?{ ;Z!0hZpK֛UkWר{Q`gW5lqY\tLQ^?L8AƽiyLӝ?50wb ESyNC\ux^]^;`zz8gNc_?V[cO}J 9ߴI =2|l% O2Jv+n{-|(!ZBdp ?2l߼]ΜK*/.+^b! Iu7, ^/qHxAAQ"Ii^T3pʢB:!ѹ<M'JbB0,JJY_W=.9,-;%rɈ4ӤrѴY8,.I߯;e=~suEƔprZtR(G"`ߠu\F6 -a(D縧wW3=8Y:\-VfJx"Ѫ7h M^V'VP4t_ {`bmk ټe"񌲢ESz9%P`] IDAT(UJYF@D, '\3M;}s1W40X<+|`[>k 7&3J ܁ T*z5 8cs/ZIxo *Lٶ+TOV]/I 79W(@kS-K/P+(麂ݺ7Yʌ\vZ1 p7kH[{z#@.wJ~?s9le]K ,SP`TvgΩ) MޫoqYuͷd+`Oc F< q7՝;Lº]{"sVUŃ{8[;XL֚+Vy ֽXR֬y`lH0Z":`S jEmaA4zߴm攪oNXV 8 L߸#PF5Qk3M8wtɎ˖HUFJ:}u}̂0.P%O`ꯜx j4暠)yء;|:[ 7-g ,lMFɁ<n˞xht> > aeҽ;xH-e25__bz- % p[$˫ -ho~4sکS yRZ|&@_K|f0D+$8'z>}~Jgۧ U; L=榆=cŚ1l`/5 hHCjq09tyz<~ӧm޾kGSs/J9u3Uj"&wm!D%w3H&JN3Te"L T6EVң "+Bd&`_ȧz۬ vzl-$WQYqf ޫ7XZtF;++*rVWVV@:ߴm'>߲h^ou0%!xH D%`0)** JX~El[M*3ޞƶx$Vf)I[,>e+Te/;_&ݑ0//l @Ę+c xoR=(l"C"PuDcirH^T|zEzzwm b8- a`?8EtD!BU"ZMTՈ ` ΔQGʪe-X2d4tW_qcm?t {/~0*K-[@ȅv&Ůā<:D ~IaWAa^aY NA^,km64 h]Œ1OrAKAXG\  0ED.R`s` %@x]R<|0@oZ8Un>ظNb YgP8fc  wS`P>𭃴ZUQJ\P. u] O ˔bª Ӱ1%DBX6x+ e{LDZfLSsFmnhR6h2á~r;:pĥTc<% zN#e6 rV_]rs<@$&ytNh8ڮь} Q">][`VxiC|w6ˀK ˜3; S]kkJVE$,,%%bXz|`Kÿ5KC?IڣC0BD2>JWrϦ7^lֿ_K"^\-ɀQZgryC]8+x&!ޙk2c.bp mnz /d9.I(O=ym9*ڐ 5Blo;z %S*:;l4W/*WKE! "ep,@{)&RA1 I^ (eX,jl;vnFӟ}ǹ|[ZTh2(vƆ 1P[dUhM9L& 1=.c^CM[N|C+`z:;?4ҪJHB֌I“-{2ɮsg.~5o2k\4X$F86#a)s)I 9Nod$k;j5sW{{јW3L4|0ڛPxh Yi wO=69@(Ȃ-P/ױB|9`sykV0\Fa6̱}VM#bi,1&SfMxA @Md!>qB-&ogjM8qV%|>Zg\ǥX4.&QBRHK(Zױ|_ΞqlR^- z=T " ~_0gAq]cyBÚ>|.vXXXdQDG3Y"zLnwn^<32K+]ř'M~o0$*fTIn)Τ538$Iegm~]Z )nϤK׸:IwMBBl%)TXK\M:R.D@'&s{템SZ_P$uVx,8`ןh8kVteԕ%YťB(! p]lc މB%O8D&PQ0Wh(?Yp,D{5,3Q)U_cQI6Jֱu1T{AcQg:|P%3̖-0d2,&鱸%X_SRT.ȁ@^Qfa8~Z>V >7h4%53fD0ZcQ+&3;G q'vp&y D7#d5}^-f#c tOP&Y=@.ǽv?y3'LƠ֜wGBg]?Ң2 ,S[F4k Î=G x\TUgi=>%0v)3rBg8#S$ƞLiK,BU[&!T(X3Npn6z`IM,} O-n_pA C3LRô~4b60IK|?&l,aAD!8_K t;:l&;7`FI%ĘΎ,jybI}w9rNn@=np_ʷvoM㢸Bxce˖@eDaW( BDC{茄&O*n#'9h(l vyG0a </9+H#g5 (3aL|h2%Z,l8,ļ ȱ.Oԃpnno,΀Ge)a_c,:*fU/oϾ{z%#+QHqS#q8}LE~#H˙ө˖n߼wʥYڑ4[2(v}!eDbV |Z|NmҐ\h)Ԋ o/E7_ nPΐEDN3* nM:pAY 6K*b :. HX"=ݺ%eIn^q&sf]%dy@Хsg/Q3A L_0j G~z/Ι[+ԙZ0~wLY% h*%ބp+QbdS7s-!0eX`to*_!*՞x[mل` xMNUwF!u~LOwԲGWg/O7}ퟨD[^|YB+$?Bu(Qex ! /}YIT@u\Ûr5h 'deaqj {vSา_ޱ@]-;zɬęUIv̚oR~Kb/,Zӧ4|%~V !jRbI+^w gߞvBbHuoygQ`%w5okyx' L"9z ,n;{A\HtJȱ!{G1ZɴeG + F 8ń:uٽgo}O[7jnE?Ĩ Psm[>OgVV/9^L'4n׽ol#ZZNn /~ݣ./2K95KуJ0@N+7`޲18mh GObbddpY[[:!,GfUL\ճc_>IK&G6K ]8l9oQ(}6I ȸPaH,eWRܚC>mӋS>og.Yu)B`@@]lp<"zE/s ! I$ve }p(5?> b b-֨*i~:ξkZÉc;zG] !od;_x`C]˕)>:D28/}><(AB=ÀxrBn;iJ WwW9Z͡oFmx4g jDag U_&jtp_?x7`gCFQc]sܟ/tefMz,$xMR?~6>m˦QWi=; RUNO?^H/ ,k| <_,u]"%]SԷĀgjidk6 Ά =yI_LӇ@Q=bgȨJ?-ZJ5Z)33f 弄JoT_%B|M| @OܦA0Cs__ȮX2Do* Rצ2msΊ&i5}s~"Ϯo-^,Ort?|gb'zx]ʇ\jL:9_hbE\Fz7%1#am߸1U}/H %֤)JF5޼y|[Dbڿhk\P"d O۩3h~+~aù_F:*kdIltȚ~g=nȝ5j-P ~pyDоuSټ}\Ӯ6#=rǝ7~sG&gTTB-J^7܎?L:}+oT@,q%24#Y?c#x0cësKpGO_[? Om<ʃ1_O5kŵ%JmtZ)UrN!ޅ[@㼸YIDATU Xw. G ~˗] VQtN 49CcП9f;H8 \P|E&c]#CyJ'L`*qؾeB]H:aR*VlGr3TrY6#۶,yR;a.}h4ԋ?mNwIB%"Ac(78TL+]K'BT^3U5UfNR?Ia n>؉BТGy5MV9'Q fP #1&gd,f_w 5rg.\t !N)\<ŕAdB0/ R !d||O}C#v)]VU`[?ű dD= {<1Y<NҒG P9j!gU@&y+ {֮AͰw(Eā! D̜R8{Ce{F^Uo=Fy}7Cp@oq=}[5 E<.RdJz cKNA;0tee,ui9~uYvJBP ү @Q9jR\Vg:zuF&L,UrVv7-HJO~hk={~ `i pҐM՞o*.YIp,#ŤI]X@<,0Hzby\A;jx]j&7p9aŨXt7*C`IDgw!'9]Ê5Wۚ1 yN(R1-?#[M` y{GA"50#Nñf[#Ll1))WN6}JJ1o~b>98g4ÌH]RƗJblU,G,K)DMz ,R(3 GPc[5{20[t9a|\zix]zs1#XBRHN_kݨy%#&HB4RL84UXP 6ͪ&ʍR| PBjN@LL `"wҍb0‰r2CR,}8HW8qL)*(L }PRWPOPwR-*K)eq)լu>m iI%8ETp*zɈj6tֶgXh#%+;H^zZ #"-4mz6@z vj;'[fI;!%"\v;1FڶP9:P;P -'mim 8 x`+m]`TzZe^opbyI)UUr!Ը IK::dT3'@YZ 6ܒ L,#C*AahPcuN*3?L N$[[CH@t|T fd. L:[Li=5FU)d/;7}响# *bޝɽ`!TFSVS풀[+4O39_h~N] jF:_m>e=6|7Ģٷ6t<Jv='a^dԂ5ZFkjjqe߳]O#nb6&d.%jvps2chܓp:LFnUbނh8t^-M]憮۩eW]+&ftd|^_ gI_/i`z]7 by3<AaeCK|~ taRn|]>gJSgLyIIͷU4 n{;LCb # `BZsh?5֧~FjKA47~o>Wپ6N< A)7${wMh65ڀDQEՕփǩIX*+`xOdP$HP&˗][>'B\k&Z 8a$'39v.O^59nyz/9D|Xxmf)]:A*,}5Hg"BC/~tC{x+F >$SAa lj,..Ѭ}8>"F|.0)ކryFm.^ysuTj LϔR,35 < /`%IT]} /#"- х~id >!6!?-?^JTnХB@sy)_-.b=*kHx屡T3^Xp)~BX$sE]? pRBG', !BfXHHS=e}i?5{f\{`Pn.T aۏ,)+Wl6?+wz$[SΛSd՜9J4U;Q&e0QӔCeAmAz{‡wϧ7px!_jr@.~y+I{6SW\-)&W3AlBLUO$|Q1Υ(ڠ|:= 4{bv( A}mœ?iY]G}KY v@+ +*4.A3J?1!M3?;tj;M43{;*) z5Cs?B!7a.rgϻef}WL(*eQIά!uUoCK_H|b~AVZŗKrIVUT#f^SJ59n7| (Su0H  :,gZ & eqji$D{]9p'w\(ZRX3:m^IQ]zG/.H*r@}!u_< I`Z_2'vg]``DV>".׫]dLJ[)\\]d1(jNP3WV(؀Nz$9SD1%w,P)N enr(k^ MJ@#O{I]͛ 8k71*Odꊳuφ gϖOyBNDb'ALN4ACx$WbI]z5EXp7F~ْ tcڎϯ͛q@W^v5K,`s;<+:%CVg}W:L=*|F?o[?OȞ5҅wރqώP0+>?~cҢH5RcϪpFmuD8T-]NQحlڸYfY)ٳCYA x)0C`ի\Ȯ(wB"{ۓ1fmujF*Vp\ ~$~3x^z4)laA=kQM30b:=6#j00kW>3glKKu8T+NcZN[RZ[0]eפZ'5ٔ(eBBT%Q<أJ7D{"mH||DV ]U 9=Gۂ!?,gzˁQ;0bjWCqv8XJP= _4k6ǪQ26mb8Y*׆'-x1ݣx͐:k#_uLin(Ғo{jwkBwkR|(}gGTTJE!ydn'X`FX'ZJjF'55bIaOl8iոhW{IaMĎ汦ZJ~NJG!9Mt ?'2P6++_ !HuQ+Ҡ*V' YF65·U28&SG^Tk=\ؗgKFȩ NbSȮAs^::*1HL-B%Bƨc>dcG8_l{3YS) [q "sŧpVX`qb\ NW-NjL 7000 #ffc800 false #ff0000 #ffff 0.8 true #808080 #ffffff #ffffff #ff0000 csWindows31J 4096 4096 true true #ffff00 charactermanaj/resources/icons/0000755000175000017500000000000011767047555017077 5ustar paulliupaulliucharactermanaj/resources/icons/information.png0000644000175000017500000000125611767047555022136 0ustar paulliupaulliuPNG  IHDRatEXtSoftwareAdobe ImageReadyqe<PIDATxb?2' /@MNo3"SXW]U\YTo_⍗=aKK-^;{dlT8^~_O2d8X~ܾvƶ^"j*MM9/$t{(/K_|d] 1od1>+H/˟q|X5b(@H/˟_4``/*tr/03*M3222?aP 䌻߿~cdc ;`_P ؼ=L@Sϟ6(JA]wNHمK9??IÓmoH&;#G?~y5"$gZ"&/ ||9*deT T!oe'*Q5@M IENDB`charactermanaj/resources/icons/left.png0000644000175000017500000000652311767047555020545 0ustar paulliupaulliuPNG  IHDRa pHYs   OiCCPPhotoshop ICC profilexڝSgTS=BKKoR RB&*! J!QEEȠQ, !{kּ> H3Q5 B.@ $pd!s#~<<+"x M0B\t8K@zB@F&S`cbP-`'{[! eDh;VEX0fK9-0IWfH  0Q){`##xFW<+*x<$9E[-qWW.(I+6aa@.y24x6_-"bbϫp@t~,/;m%h^ uf@Wp~<5j>{-]cK'Xto(hw?G%fIq^D$.Tʳ?D*A, `6B$BB dr`)B(Ͱ*`/@4Qhp.U=pa( Aa!ڈbX#!H$ ɈQ"K5H1RT UH=r9\F;2G1Q= C7F dt1r=6Ыhڏ>C03l0.B8, c˱" VcϱwE 6wB aAHXLXNH $4 7 Q'"K&b21XH,#/{C7$C2'ITFnR#,4H#dk9, +ȅ3![ b@qS(RjJ4e2AURݨT5ZBRQ4u9̓IKhhitݕNWGw Ljg(gwLӋT071oUX**| J&*/Tު UUT^S}FU3S ԖUPSSg;goT?~YYLOCQ_ cx,!k u5&|v*=9C3J3WRf?qtN (~))4L1e\kXHQG6EYAJ'\'GgSSݧ M=:.kDwn^Loy}/TmG X $ <5qo</QC]@Caaᄑ.ȽJtq]zۯ6iܟ4)Y3sCQ? 0k߬~OCOg#/c/Wװwa>>r><72Y_7ȷOo_C#dz%gA[z|!?:eAAA!h쐭!ΑiP~aa~ 'W?pX15wCsDDDޛg1O9-J5*>.j<74?.fYXXIlK9.*6nl {/]py.,:@LN8A*%w% yg"/6шC\*NH*Mz쑼5y$3,幄'L Lݛ:v m2=:1qB!Mggfvˬen/kY- BTZ(*geWf͉9+̳ې7ᒶKW-X潬j9(xoʿܔĹdff-[n ڴ VE/(ۻCɾUUMfeI?m]Nmq#׹=TR+Gw- 6 U#pDy  :v{vg/jBFS[b[O>zG499?rCd&ˮ/~јѡ򗓿m|x31^VwwO| (hSЧc3- cHRMz%u0`:o_F~IDATxڔ=eooo1Q%^ IبDNAENV4L"&mrWID j^BvoߏYT33]v<]ȜÊ +;JfW "W>kD"p6l{~ !v34ZGL#:C\tuMp ٱ 4l 0b0A όfINFp1-PCcyM;B4g8_n Fɓ@ɐHqfJ M6Mə[nNC0ԊkF4|jE 5#9M0> d8ȡI@ ,m[IMр8>R7YI;,vt~3aa+jk\r){in`{n H3Q5 B.@ $pd!s#~<<+"x M0B\t8K@zB@F&S`cbP-`'{[! eDh;VEX0fK9-0IWfH  0Q){`##xFW<+*x<$9E[-qWW.(I+6aa@.y24x6_-"bbϫp@t~,/;m%h^ uf@Wp~<5j>{-]cK'Xto(hw?G%fIq^D$.Tʳ?D*A, `6B$BB dr`)B(Ͱ*`/@4Qhp.U=pa( Aa!ڈbX#!H$ ɈQ"K5H1RT UH=r9\F;2G1Q= C7F dt1r=6Ыhڏ>C03l0.B8, c˱" VcϱwE 6wB aAHXLXNH $4 7 Q'"K&b21XH,#/{C7$C2'ITFnR#,4H#dk9, +ȅ3![ b@qS(RjJ4e2AURݨT5ZBRQ4u9̓IKhhitݕNWGw Ljg(gwLӋT071oUX**| J&*/Tު UUT^S}FU3S ԖUPSSg;goT?~YYLOCQ_ cx,!k u5&|v*=9C3J3WRf?qtN (~))4L1e\kXHQG6EYAJ'\'GgSSݧ M=:.kDwn^Loy}/TmG X $ <5qo</QC]@Caaᄑ.ȽJtq]zۯ6iܟ4)Y3sCQ? 0k߬~OCOg#/c/Wװwa>>r><72Y_7ȷOo_C#dz%gA[z|!?:eAAA!h쐭!ΑiP~aa~ 'W?pX15wCsDDDޛg1O9-J5*>.j<74?.fYXXIlK9.*6nl {/]py.,:@LN8A*%w% yg"/6шC\*NH*Mz쑼5y$3,幄'L Lݛ:v m2=:1qB!Mggfvˬen/kY- BTZ(*geWf͉9+̳ې7ᒶKW-X潬j9(xoʿܔĹdff-[n ڴ VE/(ۻCɾUUMfeI?m]Nmq#׹=TR+Gw- 6 U#pDy  :v{vg/jBFS[b[O>zG499?rCd&ˮ/~јѡ򗓿m|x31^VwwO| (hSЧc3- cHRMz%u0`:o_FSIDATxڔ?KAw9+?}/`P Y*rl AH ڈv[I#D {S͙?Y o7 Tx1 _,~|u$hZUU:v{|"竬uXކ&@#ЧKeeL nP:Q@!%'t|(sDǎ7 Xk0 `GCF.7IFf;9q~jV@ X3k04[1DHꆶNXwBkG( )&kK`u7s,.*`R"$m9S؃ů𵬶*/7ǭ$Ӡ]uv@Ղμ<ܕ_~Vx}IENDB`charactermanaj/resources/icons/favorite.png0000644000175000017500000000124611767047555021427 0ustar paulliupaulliuPNG  IHDRatEXtSoftwareAdobe ImageReadyqe<HIDATxڌSOhar`9K' js1%uƒP:A1jRYa\}s?sv9wغ|f~]Αy:'a{EvqUk}zbl8pM:p>RUetap5 TSk[m;H<=n%OqA/_ A+.4Bw+Ň~ kȧ=8楄þn:@SD`Y8J!%>j:M]/_WmJ Bl$0ҋ39dc7D*4Ab{ :{ ۭO\wNE8tDer!S@l:JCCŷ S|uFzRK0IMafHp(W{QV"Ѣ8.*(PrC*B& ipf>~ON ,7+B۵l_޼kk~+Gz1KF4qguG5ظaL\cI@/w[w"K$9%IENDB`charactermanaj/resources/icons/config.png0000644000175000017500000000670511767047555021062 0ustar paulliupaulliuPNG  IHDRa pHYs   OiCCPPhotoshop ICC profilexڝSgTS=BKKoR RB&*! J!QEEȠQ, !{kּ> H3Q5 B.@ $pd!s#~<<+"x M0B\t8K@zB@F&S`cbP-`'{[! eDh;VEX0fK9-0IWfH  0Q){`##xFW<+*x<$9E[-qWW.(I+6aa@.y24x6_-"bbϫp@t~,/;m%h^ uf@Wp~<5j>{-]cK'Xto(hw?G%fIq^D$.Tʳ?D*A, `6B$BB dr`)B(Ͱ*`/@4Qhp.U=pa( Aa!ڈbX#!H$ ɈQ"K5H1RT UH=r9\F;2G1Q= C7F dt1r=6Ыhڏ>C03l0.B8, c˱" VcϱwE 6wB aAHXLXNH $4 7 Q'"K&b21XH,#/{C7$C2'ITFnR#,4H#dk9, +ȅ3![ b@qS(RjJ4e2AURݨT5ZBRQ4u9̓IKhhitݕNWGw Ljg(gwLӋT071oUX**| J&*/Tު UUT^S}FU3S ԖUPSSg;goT?~YYLOCQ_ cx,!k u5&|v*=9C3J3WRf?qtN (~))4L1e\kXHQG6EYAJ'\'GgSSݧ M=:.kDwn^Loy}/TmG X $ <5qo</QC]@Caaᄑ.ȽJtq]zۯ6iܟ4)Y3sCQ? 0k߬~OCOg#/c/Wװwa>>r><72Y_7ȷOo_C#dz%gA[z|!?:eAAA!h쐭!ΑiP~aa~ 'W?pX15wCsDDDޛg1O9-J5*>.j<74?.fYXXIlK9.*6nl {/]py.,:@LN8A*%w% yg"/6шC\*NH*Mz쑼5y$3,幄'L Lݛ:v m2=:1qB!Mggfvˬen/kY- BTZ(*geWf͉9+̳ې7ᒶKW-X潬j9(xoʿܔĹdff-[n ڴ VE/(ۻCɾUUMfeI?m]Nmq#׹=TR+Gw- 6 U#pDy  :v{vg/jBFS[b[O>zG499?rCd&ˮ/~јѡ򗓿m|x31^VwwO| (hSЧc3- cHRMz%u0`:o_FIDATxڌ]hUGϽsgg6d15lk?-hG V "Ƣ`A}|QQ**$" ?؇ZSB+IKILMvwvfTPyS JC*x!ڰOs<{< mG  @Eh@ݯ釵pE PlqOo-T@k}7l9R< āCĥ.nM-$1RbbzniBIqlNqpa\ Yۋ98Q+`5w?w3[n VYkG"=~XN@#YP JM;1yd2OE)ݽptޞ#pW f`1Lde2mҍaZ EcV`vjB)lR\鮁+aM{\N-  hOoP]dfoY> wz_yql!L$""gIDDS"XK^)G[m;d:?>Es3ke/߆ޟXNZ0MjBCCg%[W%1Qy?ZR]vl5"Q3egm<枽~ȿ6yQT0̗SrPg?߷|!;7IENDB`charactermanaj/resources/icons/right.png0000644000175000017500000000652211767047555020727 0ustar paulliupaulliuPNG  IHDRa pHYs   OiCCPPhotoshop ICC profilexڝSgTS=BKKoR RB&*! J!QEEȠQ, !{kּ> H3Q5 B.@ $pd!s#~<<+"x M0B\t8K@zB@F&S`cbP-`'{[! eDh;VEX0fK9-0IWfH  0Q){`##xFW<+*x<$9E[-qWW.(I+6aa@.y24x6_-"bbϫp@t~,/;m%h^ uf@Wp~<5j>{-]cK'Xto(hw?G%fIq^D$.Tʳ?D*A, `6B$BB dr`)B(Ͱ*`/@4Qhp.U=pa( Aa!ڈbX#!H$ ɈQ"K5H1RT UH=r9\F;2G1Q= C7F dt1r=6Ыhڏ>C03l0.B8, c˱" VcϱwE 6wB aAHXLXNH $4 7 Q'"K&b21XH,#/{C7$C2'ITFnR#,4H#dk9, +ȅ3![ b@qS(RjJ4e2AURݨT5ZBRQ4u9̓IKhhitݕNWGw Ljg(gwLӋT071oUX**| J&*/Tު UUT^S}FU3S ԖUPSSg;goT?~YYLOCQ_ cx,!k u5&|v*=9C3J3WRf?qtN (~))4L1e\kXHQG6EYAJ'\'GgSSݧ M=:.kDwn^Loy}/TmG X $ <5qo</QC]@Caaᄑ.ȽJtq]zۯ6iܟ4)Y3sCQ? 0k߬~OCOg#/c/Wװwa>>r><72Y_7ȷOo_C#dz%gA[z|!?:eAAA!h쐭!ΑiP~aa~ 'W?pX15wCsDDDޛg1O9-J5*>.j<74?.fYXXIlK9.*6nl {/]py.,:@LN8A*%w% yg"/6шC\*NH*Mz쑼5y$3,幄'L Lݛ:v m2=:1qB!Mggfvˬen/kY- BTZ(*geWf͉9+̳ې7ᒶKW-X潬j9(xoʿܔĹdff-[n ڴ VE/(ۻCɾUUMfeI?m]Nmq#׹=TR+Gw- 6 U#pDy  :v{vg/jBFS[b[O>zG499?rCd&ˮ/~јѡ򗓿m|x31^VwwO| (hSЧc3- cHRMz%u0`:o_F}IDATxڔ;hVi;PVo,"V֢66ł6bŢM`XmZzQ`-.E ҷax0T` SOŘf%Yj %.vJ~*< K0s+6])̘t;#CO}5zm¬o#`252C pŌI8&* 7R8>'7o F*Y1v(1y\Z,&8@j@\LGUbBIg`z\Q1`_6_O[Fl #RHH2$92ge=0hcry-%%$$'4%NO'C.?kBqAb=RKS?-R E)$\~ 33hUpSv0ո׉aB%fxI 9W,L!`p/ŭ^Śkbpp#-8^`NYZ~f໴t]ץKAgZ<ĉ&r4y=r93V{V߯b{5kIENDB`charactermanaj/resources/icons/copy.png0000644000175000017500000000061111767047555020555 0ustar paulliupaulliuPNG  IHDRatEXtSoftwareAdobe ImageReadyqe<+IDATxڔD@r .@2TDm7JA-h]NWP?LX&gsNy'3uVqߟ++p00 yOןqh3emK˲LY'PEo-+Ik$!,y6Ms8e0e-]׷ra!i4*(_{0@UU<(8#A<$뎖>uIWv!g(gY.x>iE[;30 m )' DIENDB`charactermanaj/resources/icons/pin-icon1.png0000644000175000017500000000177411767047555021413 0ustar paulliupaulliuPNG  IHDR sRGBbKGD pHYs  tIME0oϫ|IDAT8ˍMlEƟB) ē 7c9P4"D9ΦR6Њ\͂AMfG7@3j9כX^Q]'1Iuo˺/^mh%Fwï`,+䩆g\dϟg^n^ӼK%)_ߦdwUN;SӘ1t-M9[eXWBr3p28RR|_wXKf*heLSYoY)G+u[m}Vn\ ChKs?8qth} 6dAtn߰1V:g 2O]3LPʂC/_¬qq SRhf=[5%J"*xXㄹCVqӥ~cz[iR{l~?"|x|+~ ̓Gfҗ 10vߚan'+씾ݘS-FGD@X߼qvV} n^Z P+JҲ Ւ;; Wľi($ $ Ѭv'J% !8K˓ ani˅l` v%|=#e?>Ș\{l * 0( '7-n&|g뷝}K?Msɉy?윎CScϾ@QxgcFJQiȸyDXk~~h685hny07ػQf%7:tE'. 1P֘)p«Oy3'׏~cgx^і\;Jv iދͧZHz@Ȁ!O݄[ +k%ӢIENDB`charactermanaj/resources/icons/arrow_down2.png0000644000175000017500000000613311767047555022053 0ustar paulliupaulliuPNG  IHDRa pHYs   OiCCPPhotoshop ICC profilexڝSgTS=BKKoR RB&*! J!QEEȠQ, !{kּ> H3Q5 B.@ $pd!s#~<<+"x M0B\t8K@zB@F&S`cbP-`'{[! eDh;VEX0fK9-0IWfH  0Q){`##xFW<+*x<$9E[-qWW.(I+6aa@.y24x6_-"bbϫp@t~,/;m%h^ uf@Wp~<5j>{-]cK'Xto(hw?G%fIq^D$.Tʳ?D*A, `6B$BB dr`)B(Ͱ*`/@4Qhp.U=pa( Aa!ڈbX#!H$ ɈQ"K5H1RT UH=r9\F;2G1Q= C7F dt1r=6Ыhڏ>C03l0.B8, c˱" VcϱwE 6wB aAHXLXNH $4 7 Q'"K&b21XH,#/{C7$C2'ITFnR#,4H#dk9, +ȅ3![ b@qS(RjJ4e2AURݨT5ZBRQ4u9̓IKhhitݕNWGw Ljg(gwLӋT071oUX**| J&*/Tު UUT^S}FU3S ԖUPSSg;goT?~YYLOCQ_ cx,!k u5&|v*=9C3J3WRf?qtN (~))4L1e\kXHQG6EYAJ'\'GgSSݧ M=:.kDwn^Loy}/TmG X $ <5qo</QC]@Caaᄑ.ȽJtq]zۯ6iܟ4)Y3sCQ? 0k߬~OCOg#/c/Wװwa>>r><72Y_7ȷOo_C#dz%gA[z|!?:eAAA!h쐭!ΑiP~aa~ 'W?pX15wCsDDDޛg1O9-J5*>.j<74?.fYXXIlK9.*6nl {/]py.,:@LN8A*%w% yg"/6шC\*NH*Mz쑼5y$3,幄'L Lݛ:v m2=:1qB!Mggfvˬen/kY- BTZ(*geWf͉9+̳ې7ᒶKW-X潬j9(xoʿܔĹdff-[n ڴ VE/(ۻCɾUUMfeI?m]Nmq#׹=TR+Gw- 6 U#pDy  :v{vg/jBFS[b[O>zG499?rCd&ˮ/~јѡ򗓿m|x31^VwwO| (hSЧc3- cHRMz%u0`:o_FIDATx;,Cq?PA, lH (ڨ4LFF`&6qS%M޺}{jka%g/|k}zEbId 2)^sWea(C@N@(p\tÝtgͮ3ۅ 9g?9}z<{S8,i!v]~Da_ ip{,3$k+mN>Jv](ޱ/ ǫvT =s;Y2Qka./+0^>n6R_cx6 t$ ,IENDB`charactermanaj/resources/icons/icon.png0000644000175000017500000001204111767047555020533 0ustar paulliupaulliuPNG  IHDR szz pHYs   OiCCPPhotoshop ICC profilexڝSgTS=BKKoR RB&*! J!QEEȠQ, !{kּ> H3Q5 B.@ $pd!s#~<<+"x M0B\t8K@zB@F&S`cbP-`'{[! eDh;VEX0fK9-0IWfH  0Q){`##xFW<+*x<$9E[-qWW.(I+6aa@.y24x6_-"bbϫp@t~,/;m%h^ uf@Wp~<5j>{-]cK'Xto(hw?G%fIq^D$.Tʳ?D*A, `6B$BB dr`)B(Ͱ*`/@4Qhp.U=pa( Aa!ڈbX#!H$ ɈQ"K5H1RT UH=r9\F;2G1Q= C7F dt1r=6Ыhڏ>C03l0.B8, c˱" VcϱwE 6wB aAHXLXNH $4 7 Q'"K&b21XH,#/{C7$C2'ITFnR#,4H#dk9, +ȅ3![ b@qS(RjJ4e2AURݨT5ZBRQ4u9̓IKhhitݕNWGw Ljg(gwLӋT071oUX**| J&*/Tު UUT^S}FU3S ԖUPSSg;goT?~YYLOCQ_ cx,!k u5&|v*=9C3J3WRf?qtN (~))4L1e\kXHQG6EYAJ'\'GgSSݧ M=:.kDwn^Loy}/TmG X $ <5qo</QC]@Caaᄑ.ȽJtq]zۯ6iܟ4)Y3sCQ? 0k߬~OCOg#/c/Wװwa>>r><72Y_7ȷOo_C#dz%gA[z|!?:eAAA!h쐭!ΑiP~aa~ 'W?pX15wCsDDDޛg1O9-J5*>.j<74?.fYXXIlK9.*6nl {/]py.,:@LN8A*%w% yg"/6шC\*NH*Mz쑼5y$3,幄'L Lݛ:v m2=:1qB!Mggfvˬen/kY- BTZ(*geWf͉9+̳ې7ᒶKW-X潬j9(xoʿܔĹdff-[n ڴ VE/(ۻCɾUUMfeI?m]Nmq#׹=TR+Gw- 6 U#pDy  :v{vg/jBFS[b[O>zG499?rCd&ˮ/~јѡ򗓿m|x31^VwwO| (hSЧc3- cHRMz%u0`:o_F LIDATxڌK\Wuu媮nw]b8zxD?JI^T$q,ւus<ZIBߧ[SY dd %- Qq$>)GiRUn%F)F $+"њBz ]+{(18)h EaQJaH!XZG+)flsդ5N+>EN*v!`9 Džd9cck~R0'f'.sXTuvZ<.#"z,/t)n!yaqSX+PPAb8ORDRǐF#= dyg|]e\:RS e z4ARdEAH!H)OfXO;!`$e}NpRBI$N:'@k͙Fn!$Kɲ  4{~ŋ}Nrc4XYY!N)xAH1ʭeXÀ,(M9s$i-I(|yJkλf4ۼ7IҴi> 8:>aIzvw?aZ-`{;,Zx`p Oi5iֻw6;<~EXqwtq9µRO;I7O#j`eG=H MGGG9 )%IEt[m"VS 4RF1NM,\trN )beDQ箳swm58H+%J)TՈ{]&ʵO^ }I `w]~ܲH')i1gB9RBe͗WVssTҋ$^ں0g_GOdqiFlc4'ܸ蹋qe]ZB(X_Zgvo">yͭMut]K}_>׹qG;l7_re!$"8n޹ɍgnp29a>sx3ݝ',7`>t[_BV#/l!`zB߾%~?$4M g(PJ_n~+\y 8S@vs/<${Y)U9rp^kwhrk8!Ilaq!m,9te71DNU)ٸÝز?Ya3BPd֖ߥ*ijʗ{.v2j H3Q5 B.@ $pd!s#~<<+"x M0B\t8K@zB@F&S`cbP-`'{[! eDh;VEX0fK9-0IWfH  0Q){`##xFW<+*x<$9E[-qWW.(I+6aa@.y24x6_-"bbϫp@t~,/;m%h^ uf@Wp~<5j>{-]cK'Xto(hw?G%fIq^D$.Tʳ?D*A, `6B$BB dr`)B(Ͱ*`/@4Qhp.U=pa( Aa!ڈbX#!H$ ɈQ"K5H1RT UH=r9\F;2G1Q= C7F dt1r=6Ыhڏ>C03l0.B8, c˱" VcϱwE 6wB aAHXLXNH $4 7 Q'"K&b21XH,#/{C7$C2'ITFnR#,4H#dk9, +ȅ3![ b@qS(RjJ4e2AURݨT5ZBRQ4u9̓IKhhitݕNWGw Ljg(gwLӋT071oUX**| J&*/Tު UUT^S}FU3S ԖUPSSg;goT?~YYLOCQ_ cx,!k u5&|v*=9C3J3WRf?qtN (~))4L1e\kXHQG6EYAJ'\'GgSSݧ M=:.kDwn^Loy}/TmG X $ <5qo</QC]@Caaᄑ.ȽJtq]zۯ6iܟ4)Y3sCQ? 0k߬~OCOg#/c/Wװwa>>r><72Y_7ȷOo_C#dz%gA[z|!?:eAAA!h쐭!ΑiP~aa~ 'W?pX15wCsDDDޛg1O9-J5*>.j<74?.fYXXIlK9.*6nl {/]py.,:@LN8A*%w% yg"/6шC\*NH*Mz쑼5y$3,幄'L Lݛ:v m2=:1qB!Mggfvˬen/kY- BTZ(*geWf͉9+̳ې7ᒶKW-X潬j9(xoʿܔĹdff-[n ڴ VE/(ۻCɾUUMfeI?m]Nmq#׹=TR+Gw- 6 U#pDy  :v{vg/jBFS[b[O>zG499?rCd&ˮ/~јѡ򗓿m|x31^VwwO| (hSЧc3- cHRMz%u0`:o_FyIDATxڤ;Vgs]oKRd$ nP$+Q@"b)*xiV\rYQ ^Iha {axG1hjOl ODfi @CoFgbw p ;]#% .JS禡>2ΣDLrI8t:%h C'@9.!a9 bw;Z- >k?U30X)1i2#S{ه p,I 2_LjEBúgH;q$/3ܝO} IENDB`charactermanaj/resources/icons/color2.png0000644000175000017500000000576711767047555021024 0ustar paulliupaulliuPNG  IHDRa pHYs   OiCCPPhotoshop ICC profilexڝSgTS=BKKoR RB&*! J!QEEȠQ, !{kּ> H3Q5 B.@ $pd!s#~<<+"x M0B\t8K@zB@F&S`cbP-`'{[! eDh;VEX0fK9-0IWfH  0Q){`##xFW<+*x<$9E[-qWW.(I+6aa@.y24x6_-"bbϫp@t~,/;m%h^ uf@Wp~<5j>{-]cK'Xto(hw?G%fIq^D$.Tʳ?D*A, `6B$BB dr`)B(Ͱ*`/@4Qhp.U=pa( Aa!ڈbX#!H$ ɈQ"K5H1RT UH=r9\F;2G1Q= C7F dt1r=6Ыhڏ>C03l0.B8, c˱" VcϱwE 6wB aAHXLXNH $4 7 Q'"K&b21XH,#/{C7$C2'ITFnR#,4H#dk9, +ȅ3![ b@qS(RjJ4e2AURݨT5ZBRQ4u9̓IKhhitݕNWGw Ljg(gwLӋT071oUX**| J&*/Tު UUT^S}FU3S ԖUPSSg;goT?~YYLOCQ_ cx,!k u5&|v*=9C3J3WRf?qtN (~))4L1e\kXHQG6EYAJ'\'GgSSݧ M=:.kDwn^Loy}/TmG X $ <5qo</QC]@Caaᄑ.ȽJtq]zۯ6iܟ4)Y3sCQ? 0k߬~OCOg#/c/Wװwa>>r><72Y_7ȷOo_C#dz%gA[z|!?:eAAA!h쐭!ΑiP~aa~ 'W?pX15wCsDDDޛg1O9-J5*>.j<74?.fYXXIlK9.*6nl {/]py.,:@LN8A*%w% yg"/6шC\*NH*Mz쑼5y$3,幄'L Lݛ:v m2=:1qB!Mggfvˬen/kY- BTZ(*geWf͉9+̳ې7ᒶKW-X潬j9(xoʿܔĹdff-[n ڴ VE/(ۻCɾUUMfeI?m]Nmq#׹=TR+Gw- 6 U#pDy  :v{vg/jBFS[b[O>zG499?rCd&ˮ/~јѡ򗓿m|x31^VwwO| (hSЧc3- cHRMz%u0`:o_F"IDATxڔJ@ώӥ X,`e#,N} H3Q5 B.@ $pd!s#~<<+"x M0B\t8K@zB@F&S`cbP-`'{[! eDh;VEX0fK9-0IWfH  0Q){`##xFW<+*x<$9E[-qWW.(I+6aa@.y24x6_-"bbϫp@t~,/;m%h^ uf@Wp~<5j>{-]cK'Xto(hw?G%fIq^D$.Tʳ?D*A, `6B$BB dr`)B(Ͱ*`/@4Qhp.U=pa( Aa!ڈbX#!H$ ɈQ"K5H1RT UH=r9\F;2G1Q= C7F dt1r=6Ыhڏ>C03l0.B8, c˱" VcϱwE 6wB aAHXLXNH $4 7 Q'"K&b21XH,#/{C7$C2'ITFnR#,4H#dk9, +ȅ3![ b@qS(RjJ4e2AURݨT5ZBRQ4u9̓IKhhitݕNWGw Ljg(gwLӋT071oUX**| J&*/Tު UUT^S}FU3S ԖUPSSg;goT?~YYLOCQ_ cx,!k u5&|v*=9C3J3WRf?qtN (~))4L1e\kXHQG6EYAJ'\'GgSSݧ M=:.kDwn^Loy}/TmG X $ <5qo</QC]@Caaᄑ.ȽJtq]zۯ6iܟ4)Y3sCQ? 0k߬~OCOg#/c/Wװwa>>r><72Y_7ȷOo_C#dz%gA[z|!?:eAAA!h쐭!ΑiP~aa~ 'W?pX15wCsDDDޛg1O9-J5*>.j<74?.fYXXIlK9.*6nl {/]py.,:@LN8A*%w% yg"/6шC\*NH*Mz쑼5y$3,幄'L Lݛ:v m2=:1qB!Mggfvˬen/kY- BTZ(*geWf͉9+̳ې7ᒶKW-X潬j9(xoʿܔĹdff-[n ڴ VE/(ۻCɾUUMfeI?m]Nmq#׹=TR+Gw- 6 U#pDy  :v{vg/jBFS[b[O>zG499?rCd&ˮ/~јѡ򗓿m|x31^VwwO| (hSЧc3- cHRMz%u0`:o_FIDATxڤkg?wrN1E!&^pZ(u)9D8(^(-.\KT.ZoKLN]p\N/;q*dB mHQ16ls ,- Z{ ˤ uق!MB6ϑ$ !Q7SB3O'! OJ?~hlaQ0ћ@ 8~oWf(:CM$[ .!]AU v7z!s᠖/>sg~  q@(/! nL nL5<)ժn} ) ?WX$xM9ix1"ٺP#~pp9=֟& B k*!\.)F d=`)r`(AR,v~8Sz#xYص&"aLTdǹ'ꥶ"%45Q -LG}@f?b͕o1B_LC&;}^[?l&}VKKÀbvmV1yLY;NIENDB`charactermanaj/resources/icons/color.png0000644000175000017500000000056311767047555020727 0ustar paulliupaulliuPNG  IHDRatEXtSoftwareAdobe ImageReadyqe<IDATxb\p4L,y߿ 5 @Ygx06t;H 6X~0D˖፜6 1pss3,Z?r @6CtDÿ𺈉aҥ.Ix /V/&L.@0uLa@j@j߿ "* \F X^.`&P6<}0&AR`I]+V˂)&7@PߵWIENDB`charactermanaj/resources/icons/flip.png0000644000175000017500000000613611767047555020545 0ustar paulliupaulliuPNG  IHDRa pHYs   OiCCPPhotoshop ICC profilexڝSgTS=BKKoR RB&*! J!QEEȠQ, !{kּ> H3Q5 B.@ $pd!s#~<<+"x M0B\t8K@zB@F&S`cbP-`'{[! eDh;VEX0fK9-0IWfH  0Q){`##xFW<+*x<$9E[-qWW.(I+6aa@.y24x6_-"bbϫp@t~,/;m%h^ uf@Wp~<5j>{-]cK'Xto(hw?G%fIq^D$.Tʳ?D*A, `6B$BB dr`)B(Ͱ*`/@4Qhp.U=pa( Aa!ڈbX#!H$ ɈQ"K5H1RT UH=r9\F;2G1Q= C7F dt1r=6Ыhڏ>C03l0.B8, c˱" VcϱwE 6wB aAHXLXNH $4 7 Q'"K&b21XH,#/{C7$C2'ITFnR#,4H#dk9, +ȅ3![ b@qS(RjJ4e2AURݨT5ZBRQ4u9̓IKhhitݕNWGw Ljg(gwLӋT071oUX**| J&*/Tު UUT^S}FU3S ԖUPSSg;goT?~YYLOCQ_ cx,!k u5&|v*=9C3J3WRf?qtN (~))4L1e\kXHQG6EYAJ'\'GgSSݧ M=:.kDwn^Loy}/TmG X $ <5qo</QC]@Caaᄑ.ȽJtq]zۯ6iܟ4)Y3sCQ? 0k߬~OCOg#/c/Wװwa>>r><72Y_7ȷOo_C#dz%gA[z|!?:eAAA!h쐭!ΑiP~aa~ 'W?pX15wCsDDDޛg1O9-J5*>.j<74?.fYXXIlK9.*6nl {/]py.,:@LN8A*%w% yg"/6шC\*NH*Mz쑼5y$3,幄'L Lݛ:v m2=:1qB!Mggfvˬen/kY- BTZ(*geWf͉9+̳ې7ᒶKW-X潬j9(xoʿܔĹdff-[n ڴ VE/(ۻCɾUUMfeI?m]Nmq#׹=TR+Gw- 6 U#pDy  :v{vg/jBFS[b[O>zG499?rCd&ˮ/~јѡ򗓿m|x31^VwwO| (hSЧc3- cHRMz%u0`:o_FIDATx;K#QI6f2hBBlN邐EP!XHXtAB "6Z,VֲZ lb$3s7謏U,{NjZxOx8PjWKo~՘vM{?_ V3ܮCuUN9bYL `Y_X[ ݱBMO\oNlE`A6B"a'OGr͐QN:fȵo#X+#y׳{?5x2'|J8%`6 i PC* ynĦ 64|EWbW 41\> X X DArl,Ekg{Ȋ?-onIENDB`charactermanaj/resources/icons/save.png0000644000175000017500000000635111767047555020550 0ustar paulliupaulliuPNG  IHDRa pHYs   OiCCPPhotoshop ICC profilexڝSgTS=BKKoR RB&*! J!QEEȠQ, !{kּ> H3Q5 B.@ $pd!s#~<<+"x M0B\t8K@zB@F&S`cbP-`'{[! eDh;VEX0fK9-0IWfH  0Q){`##xFW<+*x<$9E[-qWW.(I+6aa@.y24x6_-"bbϫp@t~,/;m%h^ uf@Wp~<5j>{-]cK'Xto(hw?G%fIq^D$.Tʳ?D*A, `6B$BB dr`)B(Ͱ*`/@4Qhp.U=pa( Aa!ڈbX#!H$ ɈQ"K5H1RT UH=r9\F;2G1Q= C7F dt1r=6Ыhڏ>C03l0.B8, c˱" VcϱwE 6wB aAHXLXNH $4 7 Q'"K&b21XH,#/{C7$C2'ITFnR#,4H#dk9, +ȅ3![ b@qS(RjJ4e2AURݨT5ZBRQ4u9̓IKhhitݕNWGw Ljg(gwLӋT071oUX**| J&*/Tު UUT^S}FU3S ԖUPSSg;goT?~YYLOCQ_ cx,!k u5&|v*=9C3J3WRf?qtN (~))4L1e\kXHQG6EYAJ'\'GgSSݧ M=:.kDwn^Loy}/TmG X $ <5qo</QC]@Caaᄑ.ȽJtq]zۯ6iܟ4)Y3sCQ? 0k߬~OCOg#/c/Wװwa>>r><72Y_7ȷOo_C#dz%gA[z|!?:eAAA!h쐭!ΑiP~aa~ 'W?pX15wCsDDDޛg1O9-J5*>.j<74?.fYXXIlK9.*6nl {/]py.,:@LN8A*%w% yg"/6шC\*NH*Mz쑼5y$3,幄'L Lݛ:v m2=:1qB!Mggfvˬen/kY- BTZ(*geWf͉9+̳ې7ᒶKW-X潬j9(xoʿܔĹdff-[n ڴ VE/(ۻCɾUUMfeI?m]Nmq#׹=TR+Gw- 6 U#pDy  :v{vg/jBFS[b[O>zG499?rCd&ˮ/~јѡ򗓿m|x31^VwwO| (hSЧc3- cHRMz%u0`:o_FIDATxڌ=kQ{n6Kb -$iҋ?R m?@ Bj,$H|mV63;sϽbAS^^cu7!JeRI-Y{pZN`v6.@ Jat -Le0`G XknA0= DcSt dfwAf"lӈ UqhWBpREB=zE|WHgpTnfa9 >+42Eyخ  /K$M'JiO"Bb @@ D*+rL R V7[x/Pgy<ǷuYEP}b4c?]~xn5mj%M;hef #?.Zvwz]~G'zj5JSxp]ՎF:mj#<;91$+RV{axzh KY҆ϷhYzmy$&QaIENDB`charactermanaj/resources/icons/config2.png0000644000175000017500000000670111767047555021140 0ustar paulliupaulliuPNG  IHDRa pHYs   OiCCPPhotoshop ICC profilexڝSgTS=BKKoR RB&*! J!QEEȠQ, !{kּ> H3Q5 B.@ $pd!s#~<<+"x M0B\t8K@zB@F&S`cbP-`'{[! eDh;VEX0fK9-0IWfH  0Q){`##xFW<+*x<$9E[-qWW.(I+6aa@.y24x6_-"bbϫp@t~,/;m%h^ uf@Wp~<5j>{-]cK'Xto(hw?G%fIq^D$.Tʳ?D*A, `6B$BB dr`)B(Ͱ*`/@4Qhp.U=pa( Aa!ڈbX#!H$ ɈQ"K5H1RT UH=r9\F;2G1Q= C7F dt1r=6Ыhڏ>C03l0.B8, c˱" VcϱwE 6wB aAHXLXNH $4 7 Q'"K&b21XH,#/{C7$C2'ITFnR#,4H#dk9, +ȅ3![ b@qS(RjJ4e2AURݨT5ZBRQ4u9̓IKhhitݕNWGw Ljg(gwLӋT071oUX**| J&*/Tު UUT^S}FU3S ԖUPSSg;goT?~YYLOCQ_ cx,!k u5&|v*=9C3J3WRf?qtN (~))4L1e\kXHQG6EYAJ'\'GgSSݧ M=:.kDwn^Loy}/TmG X $ <5qo</QC]@Caaᄑ.ȽJtq]zۯ6iܟ4)Y3sCQ? 0k߬~OCOg#/c/Wװwa>>r><72Y_7ȷOo_C#dz%gA[z|!?:eAAA!h쐭!ΑiP~aa~ 'W?pX15wCsDDDޛg1O9-J5*>.j<74?.fYXXIlK9.*6nl {/]py.,:@LN8A*%w% yg"/6шC\*NH*Mz쑼5y$3,幄'L Lݛ:v m2=:1qB!Mggfvˬen/kY- BTZ(*geWf͉9+̳ې7ᒶKW-X潬j9(xoʿܔĹdff-[n ڴ VE/(ۻCɾUUMfeI?m]Nmq#׹=TR+Gw- 6 U#pDy  :v{vg/jBFS[b[O>zG499?rCd&ˮ/~јѡ򗓿m|x31^VwwO| (hSЧc3- cHRMz%u0`:o_FIDATxڌ]hu?99b/u8Vi,4H&&H zAyh`$M%Zlvs BH{} _/? *$&x`&y9 g/ȇQ,WAm zZɞoc 84gT":TK%.7)b[\ {pft{<3~IT:˝Ht01"\ЇX bQ(,~638c+}?[TAfp)L_Od'psO#N2 1 -Cmvc85X5v.TƆk)wC8 @03\ 7Mōd]BRBr-0<4 ЊXV^n~V̪r! ѹhK?`৯ȒrrL.2CWUQ=yPUU_V.{[wvh>;Խڑ%V&zGp f7r'hN%=AS QaLCd IZRGЌbмFyT^J)G:: ;?[̥dya9 mog/ML/_[dv4\x蛫Pm6">]D[_"oߺ3?c=WۃkZD!gJ=e$cD3u; ?>ڤ[IENDB`charactermanaj/resources/icons/arrow_up.png0000644000175000017500000000614511767047555021451 0ustar paulliupaulliuPNG  IHDRa pHYs   OiCCPPhotoshop ICC profilexڝSgTS=BKKoR RB&*! J!QEEȠQ, !{kּ> H3Q5 B.@ $pd!s#~<<+"x M0B\t8K@zB@F&S`cbP-`'{[! eDh;VEX0fK9-0IWfH  0Q){`##xFW<+*x<$9E[-qWW.(I+6aa@.y24x6_-"bbϫp@t~,/;m%h^ uf@Wp~<5j>{-]cK'Xto(hw?G%fIq^D$.Tʳ?D*A, `6B$BB dr`)B(Ͱ*`/@4Qhp.U=pa( Aa!ڈbX#!H$ ɈQ"K5H1RT UH=r9\F;2G1Q= C7F dt1r=6Ыhڏ>C03l0.B8, c˱" VcϱwE 6wB aAHXLXNH $4 7 Q'"K&b21XH,#/{C7$C2'ITFnR#,4H#dk9, +ȅ3![ b@qS(RjJ4e2AURݨT5ZBRQ4u9̓IKhhitݕNWGw Ljg(gwLӋT071oUX**| J&*/Tު UUT^S}FU3S ԖUPSSg;goT?~YYLOCQ_ cx,!k u5&|v*=9C3J3WRf?qtN (~))4L1e\kXHQG6EYAJ'\'GgSSݧ M=:.kDwn^Loy}/TmG X $ <5qo</QC]@Caaᄑ.ȽJtq]zۯ6iܟ4)Y3sCQ? 0k߬~OCOg#/c/Wװwa>>r><72Y_7ȷOo_C#dz%gA[z|!?:eAAA!h쐭!ΑiP~aa~ 'W?pX15wCsDDDޛg1O9-J5*>.j<74?.fYXXIlK9.*6nl {/]py.,:@LN8A*%w% yg"/6шC\*NH*Mz쑼5y$3,幄'L Lݛ:v m2=:1qB!Mggfvˬen/kY- BTZ(*geWf͉9+̳ې7ᒶKW-X潬j9(xoʿܔĹdff-[n ڴ VE/(ۻCɾUUMfeI?m]Nmq#׹=TR+Gw- 6 U#pDy  :v{vg/jBFS[b[O>zG499?rCd&ˮ/~јѡ򗓿m|x31^VwwO| (hSЧc3- cHRMz%u0`:o_FIDATx=K[a&6~@"6iTit{KA.rEh!].ЭP! B-V`L?s9}t\V|;]lĈٻD4 >m;8}05J|4ugmG_WfTϵָZc\p|Wfn>_v>y!,7\ms@+ҹ̝,[{|cDQ``+e`T}-xH=LL==KxOH4xC&5/*??VO[[Z7+Z>xzi`sc %U~"ƥ XPUX!TςAq~VR"ORI P< 6}Z7IENDB`charactermanaj/resources/icons/arrow_up2.png0000644000175000017500000000614311767047555021531 0ustar paulliupaulliuPNG  IHDRa pHYs   OiCCPPhotoshop ICC profilexڝSgTS=BKKoR RB&*! J!QEEȠQ, !{kּ> H3Q5 B.@ $pd!s#~<<+"x M0B\t8K@zB@F&S`cbP-`'{[! eDh;VEX0fK9-0IWfH  0Q){`##xFW<+*x<$9E[-qWW.(I+6aa@.y24x6_-"bbϫp@t~,/;m%h^ uf@Wp~<5j>{-]cK'Xto(hw?G%fIq^D$.Tʳ?D*A, `6B$BB dr`)B(Ͱ*`/@4Qhp.U=pa( Aa!ڈbX#!H$ ɈQ"K5H1RT UH=r9\F;2G1Q= C7F dt1r=6Ыhڏ>C03l0.B8, c˱" VcϱwE 6wB aAHXLXNH $4 7 Q'"K&b21XH,#/{C7$C2'ITFnR#,4H#dk9, +ȅ3![ b@qS(RjJ4e2AURݨT5ZBRQ4u9̓IKhhitݕNWGw Ljg(gwLӋT071oUX**| J&*/Tު UUT^S}FU3S ԖUPSSg;goT?~YYLOCQ_ cx,!k u5&|v*=9C3J3WRf?qtN (~))4L1e\kXHQG6EYAJ'\'GgSSݧ M=:.kDwn^Loy}/TmG X $ <5qo</QC]@Caaᄑ.ȽJtq]zۯ6iܟ4)Y3sCQ? 0k߬~OCOg#/c/Wװwa>>r><72Y_7ȷOo_C#dz%gA[z|!?:eAAA!h쐭!ΑiP~aa~ 'W?pX15wCsDDDޛg1O9-J5*>.j<74?.fYXXIlK9.*6nl {/]py.,:@LN8A*%w% yg"/6шC\*NH*Mz쑼5y$3,幄'L Lݛ:v m2=:1qB!Mggfvˬen/kY- BTZ(*geWf͉9+̳ې7ᒶKW-X潬j9(xoʿܔĹdff-[n ڴ VE/(ۻCɾUUMfeI?m]Nmq#׹=TR+Gw- 6 U#pDy  :v{vg/jBFS[b[O>zG499?rCd&ˮ/~јѡ򗓿m|x31^VwwO| (hSЧc3- cHRMz%u0`:o_FIDATx쐽K[a_AG(th!(qPp(BC U3cBi 7cb4MyoK1YDz<;1P'ozjf_{BہnL m&"NzI+EReI])T#| s|vɷqrg˴LʀNiBκ2vCᩭ BXt;ƖI|Ft|- |/[%$gSQVQUi 0j{b w+UZ,!uD H3Q5 B.@ $pd!s#~<<+"x M0B\t8K@zB@F&S`cbP-`'{[! eDh;VEX0fK9-0IWfH  0Q){`##xFW<+*x<$9E[-qWW.(I+6aa@.y24x6_-"bbϫp@t~,/;m%h^ uf@Wp~<5j>{-]cK'Xto(hw?G%fIq^D$.Tʳ?D*A, `6B$BB dr`)B(Ͱ*`/@4Qhp.U=pa( Aa!ڈbX#!H$ ɈQ"K5H1RT UH=r9\F;2G1Q= C7F dt1r=6Ыhڏ>C03l0.B8, c˱" VcϱwE 6wB aAHXLXNH $4 7 Q'"K&b21XH,#/{C7$C2'ITFnR#,4H#dk9, +ȅ3![ b@qS(RjJ4e2AURݨT5ZBRQ4u9̓IKhhitݕNWGw Ljg(gwLӋT071oUX**| J&*/Tު UUT^S}FU3S ԖUPSSg;goT?~YYLOCQ_ cx,!k u5&|v*=9C3J3WRf?qtN (~))4L1e\kXHQG6EYAJ'\'GgSSݧ M=:.kDwn^Loy}/TmG X $ <5qo</QC]@Caaᄑ.ȽJtq]zۯ6iܟ4)Y3sCQ? 0k߬~OCOg#/c/Wװwa>>r><72Y_7ȷOo_C#dz%gA[z|!?:eAAA!h쐭!ΑiP~aa~ 'W?pX15wCsDDDޛg1O9-J5*>.j<74?.fYXXIlK9.*6nl {/]py.,:@LN8A*%w% yg"/6шC\*NH*Mz쑼5y$3,幄'L Lݛ:v m2=:1qB!Mggfvˬen/kY- BTZ(*geWf͉9+̳ې7ᒶKW-X潬j9(xoʿܔĹdff-[n ڴ VE/(ۻCɾUUMfeI?m]Nmq#׹=TR+Gw- 6 U#pDy  :v{vg/jBFS[b[O>zG499?rCd&ˮ/~јѡ򗓿m|x31^VwwO| (hSЧc3- cHRMz%u0`:o_FIDATxSKBq=>#hi#6 $jp #|D zM-YRBT5Ԩ}<(;pss5 !Nw`n0y1ܑBn>bu!petP& b:N/>H%@=IOLs;`粜<$kmqՂp, bb#d 3n <ğl[cu܇6H&s NAKskD|wVghi$}- DCIh4h/&NX*D8;L RGBUniwersalny profil RGBdescGeneric RGB ProfileGeneric RGB ProfileXYZ Zus4XYZ RXYZ tM=XYZ (6curvtextCopyright 2007 Apple Inc., all rights reserved.sf32 B&l8BIMHH8BIM+FFFF8BIMnorm:名称未設定 18BIMluniT yg*-[ 1- i i i i8BIMnormXNow Loading... コピー8BIMluni(Now Loading... 00018BIMnormDNow Loading...8BIMluni Now Loading...}}{}vovwwyyyyyywwxy|nymvzvwzzusy{yzv|rfs{wphj}yoozro{~|{mrot}{|zbajot||}to|y}||yqpno{{yxvttvy{wy~|zxwx{ubm~˝|lk{}`fbMxtghx鹉ƚь쵦g{y|eZUPP]yunƚЊ|h{~}tRNSaxfϚčtzlxհКѓxZr禝КѬ|OpǸǤК͚Ql俤КȋpK{КLj՗fSˑښКxcVڎКѴ~w~uldb晽}КѭgaawКѢvb~`WКўˈ|vddjdS茶Кѥαm\oZg_f[֟КѤŲSoYg~Wj|^au^ͫ֜Кџ×ǯJ\]djYkfcRoКѥݶfϺrKf^\h^[n]e]КѸzô{bPd\`\sv\[un_`{К}۹οq~^Smp`d_`\}b_[|^fIӔΎКѐۭİu`m_UXYl}[`^YųV`]q]bYКƂ٨Ÿ´uW``_KfYo^_i|dN_]^b_mКѢ٪~ԠتötWc\`cGy\Yy~qcKMa`q]fPʠКπ٫yݩߪp]^`Hs~]`Sy}Ԑt`^LjlCY_l_`iEКђܱÐНϑkg{zd\YU~رλsY_a^@VFa]qh_c\gК|߽yڟ׈Қ—rūԫɾhoZßᤘyDz]``bSInJPb`d``hU⾊КѤМ{߼ݬޯpˊɗ{ܥ䧌ݟ]e_``dF_]D]````aaȁКсޫ֝Òʕ}٢xӇڢ~ݦ嬈˺uJv\``a_@}lSIc````eSvКѿśx߹{߮Ϙ܇ඓᯫږbvN]```cQI`ETb```a`{zКч|w֜Ԝ”ߎ˗~͓qtMvO|e_```bDdlVBb````cRԉКoZM߻pÔ}ߴtzڢȽ]{LvRpw\```aXD`IKc````apϞКѮkW]Ӿۢu௓vިwަⷐiZ}LyUf^````cHUk]=^a```dRʺКv`Oĵr؞؟ڶuþe``RZ~M}{Y]h^```aYCr`RLc````c^}КatZVv_ZrV˘Ҝ~˷\˽h\`Zj_w|Pp[V~\````cISaaF\a```cQxКѥycSmrd]^VhĚs|gʯj~}}}tEk{\`_XdkvVe]Rt`````aWFko_TOc````b^ԊКp^Nl_WkFnY^]T[\E^]]_iOU^`bQkbo[Z`Qhq]````aIW\aK]a```cTΦКbrZY}qv[]hHh_`\^{^[8a`aasz}yYFd_aVore[QO7.3/)QcovYSr|m]````bSKe`aTIc````dWКѥdSqk{hTsFcb_aWhr`U#R`]`aOK;\zdafS׭z\adpa\`fI^_``aUKka?yaf:-^```aI\]u]```G_`````cZzԦКneaIk\fvpBG+P^`]`aQMT`afTۦ\`ejog|^`eSX_``a]Lu`I^m^o8A```bMUab_``cLSb`````fM̓КѨ~aZ_WtQQ7VT[`}]`aS`U]agW۠\`ejۊpkb_cUc`_``d[}^UH}ZkdK\``aSOb[```cO@d`````a`kyКeQW^gJbVaVRau]`aN`TXcgYۛ\`ejyx_h_cZq`_``elw^_=\cg_^``aXKb]pb_`a^:_``````cOКЃkRњTmYo_`bXOdk^`d@bSSef]۔Dž]`cns~Yo^bWaa_``ekbd?wa`e````a]Ia_ad_``eBLe``````bSКјqPoьlNzx]`bY]ib_`bHcOZef]ۏ]aayܫX~w]bYma_``ffykbKVj^a``````H_`_kd```cQ2aa`````a]gКѩvOyycDo^`a[sp]{b`cIudPidfZۘz]a^n[r{]bZd_``ednw^XEw\``````bI\```````ab=9f``````bS~КѦmQpvF^rl^`a\syZte_cJheQۀ`g\܎r]aZݥ_iy^bY|g_``eg۟\dG\``````bIYa```````g;GUe``````cLКѪsVqh{XZ`jh_`a[Zje_bO[fQܴTi\ݔ|r\cTncbq_bWwi^``ekp_fMda_`````bKWa```````g>SQe``````bMК̤tsleynd_cd``a_Ȁ\cd`bUNgQUj]ݜnt]eQ߰h_i`cVtj^`a`xgaWml^`````bMVa```````eJGRca````a^bКѽsdjxsb_`a``a`voa`b`aZMhQkf]׆ru]fS߄m]c`cT߀j^`a]ww\cqsy\`````bNVb```````cS>їNca````bYvКѠtysf^`````bpۮi^``a^WjQݔ^`qvu]f\ᾇu\``eSߚj^`cV|^fZ\`````cLVb```````b]9ѧT\c````dWКq}ll]`````dnuy\```d`xlPTX|w]dnုy\``fRḆj^`eSxiaUv_`````dDTb```````a`7pQbb```eMКr~eu\`````ejp^```fhtqPdYzu_[̈́|\``fUxk^`eS\a}c_````e@Mc```````ab3їVXcb``fRКu{]\`````e_ےh^``fdvOP_}vaR㏨\``f`pj^`fbu`fVtg_````f9Ce```````ad4łQ\ea`dOКуyW]`````gUo{\``ffOVB{rxdXၳ\`aazrg^a_ޗ|`Sn^````g6@f```````ad8ѽO[gddQКѕ]]b_````gSr^``ffqNHkqax暝\`cU}d_dRsa`|zx\````f5=f```````ab;ŀʼnRTejSКeWn]````eRݯ}g_`fcSQhnS~\`eS生a_gVuaRy\```ac8=f```````a_Jp̜lVYNК~w[````eRݐt]`feἅ\vfmZ飩VK`]飖`a_ࣀpTĀ]```bZ@Ae```````b[eҌƛgEКѻo^````dU|\`dkpodGb^Ό|>QVwȗz|QWIֈvi[u_```dNFHe```````dO˸Кыe_```bZl^abzysuyeV{`KVHթ}~V[do_N````gAPKd```````fEtКѷx\```a`zr`a^or_鶕[zx^g[ȻXtO:2fpdYSNLKMMS^gZ-4>]b_``g9MVb```````g>ԅКp]````eh⦍c`R䞋G믽~YߜA* $.ba_`a_CA_a``````abLͬКјe^```gWsLJ>`IdޓP(  K^ekotz"\```eNU;e```````eOqzКn}[```eSۓYfd`ЃDqܮɡϻ 9N(7=<3 ;?~`_`g>^=g```````g6}Кєa_``bVփaJh`aË26JQPF1B\_qa_a_?SMd``````cZ?ϝКs|[``aaޢG  e``````dV9srКы[``eR NcdSkmcQ3:ٌbdQVFJd`````ae6LԂКtl]`bZ@#  sP[stlV7#/!"}vڈ`g]^a`````gAE\ΥКn^_`fe픲+ FaӿF -9O@9[pvmV6AZ7; ڀbUVf`````ePK;~ŅКo|[`eN7sH:Xc0(G]c\F+gSY}b_ٴRd````eQGQ>zКxl\a`g#?zs#'8>:-/rorO|`a```dUFc4eԈКюc]fNN 11ȅ=./6@gvsrkWf```fVHcS8ͤКѮl``\SĨȲW&٦RٹQe``fJC`cCkÈК˃x]eR Ĺʿozrٌ[b`fRK`cPMuКѶz]`x B<glkcagLu`aC}КѲz}aS'9CA=3/29CCHH-;ٌcXhcVncNPњКѸ|vfQ\UjWc\De˷КɒqrYْQSTHf{Кѹbt|^ϭD@MyКѤċ[Z8ӊКѯTVeĒͥКӀ>ƕК˄9ɗzКgTtʖ|К\NW>YʖӋКq^`XTXXPIʘΤК~E```bXD˙ʿКv^]``a^Cz˜xК^M``aaDk˟wКiH```cHcˠԇКэZ```cLRˠϞКђۼߊL```cOIˡʸКѣ谠⼰t]```cTFˡ~КѧɬtEa```bWDˡ{КѥG[````bZD|ˡӆКѦŕڌOXa````a]DsˡіКѦuIXaa````a_Dvˡ̭КѨ“ՍRP]aa`````a`DqˡĈКѪΞҘ^HUaaa```````aFfˠzКѫ‚PIVaaaaa```````bH]ˡxКѮַ̠gMKX``````````````cJYˠӆКѯ߽ҥj{WV````````````````cMT̖՝КѱĤCQbx›zlx}W```````````````cQK}FKKGGGGS[^~КѱťTd`[DP```````a``````cVE߈Y```````faµКѳŇ`b`MJ``````_^`````a\Dɥxnhfgjr_X```````fVКѲfea^ٸD`````aVCc`````?G[HHKMPTY^```````_ZVQOPSX````````fRКѮUghB`````bP9a```cIN]````````````````````````````````dSКѹz}~{{ŴQj@`````bKDXa`cSIc`````````````````````````````````bZКсћi?`````_HQNba_D_````````^`````````````````````````acmКх򾶴kS````aVK]GbcJSb````````_``````````````````````````fZКёp~{Y````bOSbG`\Ha````````````````````````````````````gRКѠ˄e````bG[dKZNRb````````````````````````````````````fPКђ吨ѥfݯXLS_<`bSRI^`````````````````````````````````````dTКѝ좴ʢkټl=KY[FJa`````````````````````````````````````a]КсɧپĆM16\_````````````````````````````````````adkКĨ͊ꖃ٣n>\HLX``````````````````````````````````f\Књr뙠١<:UDBM[``````````````````````````````fTКѤΩتُ빞p_O[```````````````````````````eSКȭ縱ԗ0ʇp[NNPX_`````````````````````bWКѦ}tטܙ}Ӆ_էZRQT]````````````````aa|Кʹj%`޾ht "帓p^NPBS```````````edКήoMփӣﯥپwK```````````gWКϴv*DԦ걨걣٥"&ᠪQ```````````fRКǩxy_!Cۢݵר뺑6W˶ڀW```````````dSК}%!Tܢ⿁ұ+' ˜̩z[```````````b[КѤd)Cݢ઎̶ҹ !!/𦕺sq՝t_```````````acjКі {з|+TJ``````````````````cVКx.qlƙ[c-Q`````````````````a^КDŽڶ{ԩ߇}넡ꯠYͬ_eJ7``````````````````diКѢݶ߀nXyۭj㿡!$-_b_&``````````````````fYК೮׬џpɳa%!&,``dD V`````````````````fQКѷSƱj $ĞОb=]NR`_a\8`````````````````dQКɿwѬ뾓ϵwr /Neb___e3`````````````````bXКѕܸˆiDb____cS\````````````````abrКшޡ$tû~!,b`____`d A`````````````````f]КѱѮc $=h{yn71+gz>d______e@&`````````````````gTКъǡnZ@!"q[O 4~T S'!Rc______a\`````````````````eRК֎e B\*9 P)?!+a`_______h*N````````````````cTКђ|'c]S࿑ "$#&Ub_______dI3````````````````a]КѸ4 c Aq1eɼœ"n/h_______`a'`````````````````dpКўög2 ]ɾfj1*i________h-]````````````````f]Кь٧PR|W`zKZb_______eBD````````````````fUКуɿ =Ŗչǀ@e_______bV<^_``````````````eSКЂ˿ν Rƶĩ7d_______`a3][``````````````cTКyϟWmk.3<Ñķ@_________d=ZN``````````````a[КѴxUp *Ƕȟ¿YTa_______cJOKVa````````````abrКѫȤةPưȿŞݾɣxFd_______aZ@SLc`````````````e_Кќ絧ٱD˽ξŕ̿Ɔ0i________b:[Fa`````````````gXКګ܌3/ ̧ټĕƿƟ'j________dAWH\`````````````fRКѲî $= ˦Řʿ°@a`_______cLKMUa````````````fRКуԣǐA 驽š׾jMd_______`Z9TOb````````````dRКuž.=ĿIJ˘8g________c9[Lb````````````bWКlϷǺI/"{*;Ťܿəǽ+h________dCTC^````````````a]КuǶּŚͭ3g`_______bNM>Pb```````````abmКdzɞÿ\Xc_______`[?CEd````````````ehКLJɮ͜ZKd________c:L=d````````````e^КʩѠÿÀ)G_`________dAM9c````````````fWКѶˤդ̾ʞO%Kje`_________cJG7a````````````gUКѦӧ̡٢ɩc9A[le____________aY<9_````````````gUКћϿ隿̝٥ǿb.;Sdea______________`b1=\````````````gTКі΍˘٬װw98Yhfc`_________________d8>Za```````````gTКэ˕سʯgic_____________________dBCXa```````````gVКуʓֿz{_______________________bNETb```````````gSКсǑŲ_______________________aZ;Rd```````````gUКѿŏźb______________________`a6Pe```````````gUК2:CʏŽr_______________________d;Sf```````````fXКѣDkjSſ_______________________dCNg```````````ecКXW``yڹ}ša______________________cIHf``````````acmК=b_cö™p______________________bOIf``````````a^КѯCd_i×ޘ______________________aXBf``````````bVКэNc_oĖ`_____________________`];e``````````eSКpUb_yŚk_____________________`f/c``````````fSКrZe_šޚ____________________`dI`a`````````f\Кѧckťa__________________c`=[a````````a`wϚђ!/ťh________________`dU.\a````````cTƚЌħ______________`d]@""-[a````````fRƚkkkkkkkkkkkklmlbUOPSQJsimllllllllnkmkkkkkkkkklmldWPOQSKMhmlllllllmj{vskkkkkklmmkdWPOQSNH[gmlllllllnfkkklmmlg^UPPRSNJSngmlllllllnklmmje]VQPQSQJMetffmllllllmh~md[URQQQNJLYuxofmlllllloe{SIIHILOWe|sfnllllllltưƤƺƘļƳźĸƿ­ƷƪôƻƜƽƜęƷĵŞƠ~ٳƹ֟ƺִw~z_ٺ꺉ءԾ®}oiabsջڳךɺeagy΀ڮ량}ʘᆘڧհԉԹnڧ禝͓Ö֫aǸ֥֞do俤՞̋Y՘Ӡžֹ~bː۷˳џѪןz`ڎؐىНɑ΅{i晽Γўֲֹwv|ӡʰ֓y֜vk˞Ԟ՚||͂{a茶ĕԇտqַoֶt}d֟Žgӊm֝l՚sw⩆yͫ׺ßԫ[ɰ֪qr|̓m~zc򛁭įݶhdzӎ\ՠж~sr״spګr}h܈͙yc|Ѫ֡qvqՒrpԈuu~ьݤseԋwӫ{tvqytp۷s}XӔȧёwԇtil²֮mԪӼ֛pvsnנjurٍrydϑk۾vut\ǧȨ~nԨիvt|`vrsyuqŘѐk|١qvzUϠ֖qnդ֞٬{֭\_wwr~aʠƓ¿ŸԊrsvWԝֳrvgԸՖ֣㭎vt]τQnttvMʣ֯ۦԳ୦zך|qnhԸܘʒ܎muwsN׮kUxrtzri좸Ѳ绵ⷴյϷܢnڹΌծrvvygYψ[byv|uva۲ž߻ѯɊŲdzöܚ۵r|tvv{Vv׮rSrwvvvxwƽũ亱՚۴罴罴Ěב\ՓqvvwuNφfYzvvvv|cƄٻϵƱ߶̱vӓ_֯rvvvzdZ׫vTgyvvvwvʁϕ乵㹴ճ۵䭋Ԑ`ԓa|tvvvyR{υkPyvvvvzd¸ɉo_ϵyֳ˱z罴բsɘ]ԓe֓qvvvxmQׯwZ\zvvvwxzɼęУۄks龹~DZ³y뾔̮{֭o]ՕiӹȶsvvvvzWiσrKswvvv{dʺuaӿͦ~庴廴Έ|vvfշo^՘msǦ̀svvvxnRץue]zvvvvzeÎuoiեӓtnfܷ⺸xòrvotc֞pk՝qvvvvzZfwwVqwvvvzdɁƘyfΎ|rujŵⳍ|ȚmtȶSטqvvm|j}֨rfմuuvvvxlUӉuhbzvvvvyhɋ̫saոׯtlVԳՉnsrhգopSsstu\qȢahȺٱsvxdυypoֲudπrvvvvyYk֤qx\rwvvvzgÞxnn֛ՒptYթπtvqu՚so@wvwwbrΛnVϩ{twjʧ}qebC7=82e{nf֚rvvvvyg\}ԿwxhYzvvvv{aư|fЃɀhV{ԞyuwlӍve?zvwqrәwOզՈn\D5V]m|}bidFts]72Gպu}tvvvvuUpσsxUqwvvvzmƂ߱tbt۷roAәtvxh΄yXB}vxk{nՖ~{NճX16EO֞qxz]xZ>rvvvwsYwrܟqvxj]yvvvvvY˖v]SԻs|rZL*avrvxbշ]Hqf]|v~Wxmbqw{W~wqv}MdչuuvxgO΃xMw}G6svvvxYqrګrvvwWtvvvvvzouɞ}xXնq}QV4`nurvwd֭]gtgvw~Ttsh֨qw|Snsu|YMջuuvwrBԑvXuІsEOvvvy_iwyȺtvvz]fyvvvvv~YÂ˺ܝvnձtkddCjekurvwg֩uhykrxRqtnǫqw}SgԋyuzdLջvuvv{HբshY֛o|]qvvygbypvuvzaO{vvvvvwwp|c׮ְlvZyjxfXwrvx_֨vd{|gzRo{{qw|T֕ttzk[Ծvuvv}RfִsuIֱqzusvvxm\yrxuvwtGuvvvvvvz`Ãe֯hntvxhJ{sv{O֭x][}~UlrwzW`֝nsykiawuvv}Sky{Gxu|vvvvwrYwuw|uvv|P]|vvvvvvyeЌcڢ`rvxkJ́yuvyXְzVY}~Torwx[z֨mrynlXyuvv}R΄xVbςsxwvvvvvXuvt{uvvzdilzxvv~dۚu֠qvvvvv}Pm̀svv~RgԒUybbvx`vՠqvv}Wt΃sv}W~w}Ptvvvv~FR|vvvvvvvw{?cq}xv{`ܗlֱrvvvvvN_֙qvv~VݞW\Qq{V֥qvwweitxtkpubfЈsvvvvBM}vvvvvvvw{B`q{{btuytvvvvPxֳsvv}Yx\zYlwd~ܧqvzim|t|`exw^Օqvvvv}AK~vvvvvvvwyGǗdg}exkшrvvvv}U~tv~Xfٿd`mbqv}^{xtTxS`֡qvvvwzEK~vvvvvvvxuWxhn`方֟pvvvv|ZlԐrv~Yq|rYԮk\v\ھvwulzhpԭrvvvyoNO}vvvvvvvyquɊ|Sշsuvvw{d|k֤qw|]jԉloKrt{ðLdkgclRkRdոtvvv{`UW|vvvvvvv{b}tvvvyomjֲswyc{a}Zǀw[jT͜jpZcu]tqܻwuvvOb]|vvvvvvv}SǼǁ۟Քqvvvwv`ԾvxsrtWmćqst\skbG9W_TKEA>>>?EPXI&>3c`տyuvvF^jyvvvvvvvJֺɅձrvvvv|YyvbJqƕw\O2   %zxuvxuROtwvvvvvvwx\ʺ}svvvTgǡ\ZFzfVxˢeĂH&  K^ekotzNtvuv|`iH|vvvvvvv|bºË՛pvvv|[tg~{]pQbɼίοȻ 9N %((# 8+g[vuvKsJvvvvvvvBẺʂytvvyjsxZ[_WҿË'"054.!BXApxuxvLf_{vvvvvvzoJ巺ė֛pvvwwj; 4eˡWٸ|A%6::6* lmhzt}WmOtxvvvvvvHoиҕ{svv~\) 9эټ~u*8>?:0 R&czu~EvK}vvvvvv|jF~~צpvv}\— EaO3EF@5" :bfx{`FG[|vvvvvx}B]Ƀ΄rvyoů7   q;9JKF8$%}bdv~Q`txvvvvvOTmÞֶsuv}a& >s[? $=: %oD]ɂøm_ʍY|BNjЩ~R榌GrÝuˏcKιԥ}>ĈhÿºoNʁ]ürGbv{iMHiNJrQvmcclcZÜ}ǺļǝOvvvymSuŹļOrvvxsRȺÇcǻͺh^vvwwTz㸺ɀxùþºvTvvvzXq帺Ɇľl^vvvz]b为ęϱ̷cZvvvzbY帺뮸ۦձٿ_lvvvzgU帺簭뾣ᾏYOvvvvylT䷺džگbOovvvvyoRⷺȈŒÙdNjvvvvvxrR᷺Ɣ͡XNkvvvvvvxtR߷ÓѧgLZqvvvvvvvwvS~ܷΝˣnSQgvvvvvvvvvwxUu۷ĉɦdNThvvvvvvvvvvvvyXnַʁֶ̩kUOYlvvvvvvvvvvvvvvzZiηȆ߼ÕjQBH;jvvvvvvvvvvvvvvvv{_cǸʗԾݷP^izծuOAGR\`yZkvvvvvvvvvvvvvvvzdZyRWVQQPPW_bԽ踺g|vp9k|~zmwgmROZ`^]bsfbvvvvvvwwvvvvvvzjT҆nvvvvvvv~tƺּwyv>HYlvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv~f񑅤wС˲l볆XHGQpvvvvvvvvvvvvvvvvvvvvvvvvvvv|f̘}tz毯խv1„h\X[aluvvvvvvvvvvvvvvvvvvvvvykƥhg}ьo秊mY֪d^`frvvvvvvvvvvvvvvvvwwֶu#KВYY\nn} $幗wgX[Mgvvvvvvvvvvv}wϫj>^ö| p[vvvvvvvvvvviة7qxlzx}t ܜcvvvvvvvvvvv~dʣZ2ǏmzgВ}3W½~}kvvvvvvvvvvv{gAԒmX˞ն) —xpvvvvvvvvvvvyp^&jܕmdt࿬Ȭ0웎mkɔrtvvvvvvvvvvvwzCӐk}bοz І嫓Hbuvvvvvvvvvvvv~k| %XỌi[|εfcpRmĺXvvvvvvvvvvvvvdr'7zrr_UeNA  ľWvvvvvvvvvvvvv|eЕ #Vk˜~u.Jyi_lTvvvvvvvvvvvvvyjƥMPԹjޮR#_>-  JOvvvvvvvvvvvvvww칗UwqO8^ב_& Tvvvvvvvvvvvvvv}uXޘhfˮnp_tǴs zsvvvvvvvvvvvvvv~hΰrqD^젶ʦȿV 7|Yj^vvvvvvvvvvvvvv}eΟڕg5 *Nrڏǫw*% vvvvvvvvvvvvvvvvvvzi۠"PrVMq}ƽ(+ bvvvvvvvvvvvvvvvvvxsϬuʡѓ[WZm{kfgn椞V¢ )+! ?vvvvvvvvvvvvvvvvvw{}ѮؖWOd#@eݿ g,)))))))**$vvvvvvvvvvvvvvvvw{d/ zlcbbaad_ѼݷuƹI))))))))))))))))))))))*#C~vvvvvvvvvvxs+)Oⷺoƺj))))))))))))))))))))))*&7~vvvvvvvvvvyj"+)_ļ䷺kǺ/)))))))))))))))))))))*(,}vvvvvvvvvv|e%*)rƾ踺mɻC)))))))))))))))))))))),$zwvvvvvvvvv~e&+*ļ긺uʼm))))))))))))))))))))*+ uwvvvvvvvvv}o+6Ļvʼ2))))))))))))))))))+*nxvvvvvvvvww&%»𺹺xʼ>))))))))))))))))*+%qxvvvvvvvvzhſﻹ˽V+++++++++++++++-*5qywwwwwwww~eĶnnnnnnnnnnnnnonkeccddcij«snnnnnnnnnnonkfccddagĪĬ|nnnnnnnnonkfccddcbxĩĤŮnnnnoonlieccddbcnĬ¢½ưooomkifdcdddbg|Įľ¼ƲskheddddcberįįƵjaabbehp}ı̷ǽ̱º̖̺˾ƛ̽Ǿ̯ÿ̞ğĿ̘ȥ|}̽˥}ò̤z}nX[p~tnuwnklr~}TZVA^t\RS_|꺉õƆu쵆cUqjxlgjq{XNJDDNgcXõŁzkV}j~unlkoqwx{}~~}{cGDHTf|Vnŵƺnth~{wtrrpmooqrtryZ\հƵƍm~qLZ禝ƵƤmuzD[ǸǜƵÓqpmGk俤Ƶƾvl`Bx~Ƶƽ՗um~XI˒ړƵƷzkmunUOڎƵƫulld\V]晽yƵƥzlyY~SStƵƚoyeUlSKƵƗ{ljeUU[VJ茶ƵƝ^O`}NX|QXV֟ƵƝжH`MYlKZjPSfGͫ֕ƵƘÒԸu@zxuOPV[M\XVH_Ƶƞݶd¨{aAsnq|YPOZ{PO_uOXV|ƵƮtѻƳiUEoV|uoOSOadOOd_RRz|ƵwөamqPH\aSvVQSOlUQOj}PXAӔֈƵƉҚٷdS^QJKywM\ukOSPMqnJRPqaPUSƵƼ~ϕ׷ϼdKqSRQ?qsXM_suzQQZk{VCRPyPURkƵƚϗnɍТócKToOSUJ[P5PSSSSVGljƵp~RDp|wxiҼ͋xxΌexϦgĻVSSFn}NrlBkiMPqZPSSSTM:bqRGBUSSSSU]zƵWdNIpzdQMcL̶ܽqȇ~`}¿OZOSN[QekE`mOKlnOSSSSU?HySS=OSSSSUGzƵƝiUG]aVPRIYckoYnn]mkkmXiv;[iOSSLV\eJWsPH~|cRRSSSTK<\_RIDUSSSSTY␺ƵjvPC{}\wQK[=}yu]MPPHnNO;PPPRLhmmonDH~yPSTFt\U_OMzRGrZv`PSSSST?KnqOTAPSSSSUIܯƵYaMMk`eOPY>tzZQSORjPN/SSSSSjmikgM=rVQSK_sbWNFD/(,)$FU_dMHaj_rPSSSSUHAXSTI?USSSSVT΂ƵƝzWH`[iYIc=Tm}TRSKYaSH0VSSO`zq_i`S9zr~_MA1&=AKVairWEJF2P\O@'$2t~RrXQSSSSR#'18wmOTUBYS?+<=YYdePSSSSUBIVhOUHCUSSSSWPzƵƹldOHvNyqEeJ;czPSTFeXW>:XSTMnsrGhSV??m2?IESUDorOSSJJaQB|xN8MlSlRSSSSTK?PlvPSS=YSUKqstI|jPUI2]?DSRTERvOSSPAjR=^S7JMgYQSSSSS=PVSRUDGUSSSSYIܯƵrmQB_`PMfGEVsK;?O`kjFaK+&&*fSYBCYSUIwtsYklOTNApk`eqXQSMBxPSSU<}sRDIG%IaPSSSSU?IPqZPSOi}RM:M >SSSSSTIAS[ePSU?JTSSSSWHυƵc~QQ8TTkb?N2 bT~vPSTCoS8WQUSLXSXDrsgWpOSTV[}UkOSW5d|QRSU@RVR4|vH,,PSSSSP>SP|mOSTJAUSSSSSYU娛ƵƖyjSC<PWdO?6FSywPSTE}}@3O]SVSYBpvdUqOSVNwsS|uOSX;J~RRSTI<[T7gSX3&PSSST?OPdvPSSS=QSSSSSUNƵdWTA_|OXd`:=&DNRrwPSSG~vBIl\SSYBkwi]sOSWKVYk~PRWCARRSSP6dS>Q^P`19SSSUCJSUQSSUAHUSSSSSXG܈Ƶƛ~lSNpzQKcGG0JHLRkvPSSHsRIqbPTYCixlfvOSXJ]`\TRVHDzSRSSV?epPI>kN\VAOSSTHDUOuSRSUD8VSSSSSSShxƵƵn|WFwyKQX?UKTIBSduPSTBysSGsuJUYCfxqs~yOSWJcfQ~ZQULT]SRSSXJU|PR4yOVYRPSSTMAUP`TRSSP3RSSSSSSUCzƵu[G}ƑxI^M^QSTJoyUAEXXEcsvj~PSTSrtsMmfPUMeMTRSSXFd[T>G[PTSSSSSS>RSQ\VRSSUG,SSSSSSSTP\ƵƜxeE}phU;o`PSTNMh`PiURU?d{VAKVYDnk{b~PTP]RzNaiPUNgbzVQSSXFOfPK6eOSSSSSSU?OSSSSSSSSU52YSSSSSSUHtƵƜ{]JfleGUSSSSSTPYƵƳlZ`fbyUQSSSSSSPOSSUSTM;ZAOXJlZY}PYCc^PUSUIu\ZPSTP^`eOUMYgOSSSSSUCJTSSSSSSSUH7ƌDUTSSSSUMmƵƘjgctXPSSSSSULpnZPSSSP?v[BgPP\ZY|PXFodOSSWFlZPSUJjV}PYDYrOSSSSSUAIUSSSSSSSTP3ƜKOUSSSSVK}Ƶgk]s\PSSSSSVLRfOSSSVC_\AEKyft[yPVP^gOSSXDr[PSVEbZTGn^|QSSSSSU;IUSSSSSSSTS1ƺfGUUSSSXCƵhlWudOSSSSSWJ[|PSSSXJVaAKMhf[vQOjmjOSSYEb\PSXCVrPSS\URSSSSV9BUSSSSSSSSU.ƌKLUTSSXGƵmiQxnOSSSSSXDb~ZPSSYI_e@nDMi]nTFkmOSSXJY[PSXJ_SX@^YQSSSSY2;VSSSSSSSSV/ƹxGOXTSVEƵ{hK}zPSSSSSYAPiOSSXKwmBF9h[fVGhqOSSS\XYQTQbgkRG}[_PSSSSY/6XSSSSSSSSV3ƲvFOYVVFƵƍPQUQSSSSY@[|PSSXL[uCf?lVaS[vrOSUJyaVQVFUTSVdfOSSSSX/4YSSSSSSSSU6{ƺKIX[HƵƾ`K_PSSSSXBvhYQSXJ^G|EyU_FiuOSXFsTQYG^rT@UoOSSSSV04YSSSSSSSTRDpfLMCƵƶwhmOSSSSVCd|dPSXLpPtdqV^JwKASLSSRdql`Ig~ivPSSSUN89XSSSSSSSUOaߔƼa?ƵƱe}PRSSSVHt[qOSVPV_eT;QQswk6GK_xgjEL=be[FR}QSSSVC=>VSSSSSSSVEċƿƵƃXQSSSUNfUzPSUZsgcYeqWGeSAJ>jlJNPTRCmg}SRSSY9D@VSSSSSSSX;tƵƭofOSSSSSV_STPjZbKzh|Oy¾fgQYLmKbD2+SZPGA=;;;M8QTSSSSSSZ3X̄ƵƦjVPSSYN' 6Ɔϻ~u0I`kmdP/R#XURY1S5XSSSSSSVJ2qqƵƃ~qOSSXD Bdj^wyo[9:\\~TVD=7?VSSSSSTX.BㇾƵk[PSUNy4  |"uVfx`=%4%%}_ZySYBWQTSSSSSY8;RܰƵe}PRSXSy$;nR}<  2?UB?f}y_<D_>B ꗄWuUJ`>XSSSSSXC@3yҀƵfkOSWDľt/èЪd?Acm2+OgofN/k\cꕅYqUDqEVSSSSVG=F5zƵo\OTSsǡӻZF%+>EA02|䄇UnCkRRSSSSVIUH0ܯƵƤa}SSOqvQ ĿʬĻb&io@sCVSSX>;RU:cτƵ{huPXGܳ m텇SaaYNTSXF5SUDCtƵƫponPSi JؾCÂRKIVTY^xƵư[fujUےl~3g3D{ƵƛzONRs0ΜᑸƵƥtJݝ<[ќۯƵƷ޺qÇ[5ԠͅƵ|Ʋ̝u/֡xƵ`½g>nנ~ƵUýſi:Wmr`@3Oנ⑷ƵgżCSMGILF?ءܰƵrɿžř:SSSUM;ؤ˃ƵjʾƾCPSSTP:tئvƵW˷ƿ_BSSSS;bتxƵcŽýo=SSSU>[ج⌼ƵƆƾþeGSSSVBI٫ݨƵƌĿƪƱZASSSVD@ثʼnƵƜৰў̪ѷ}RNSSSUH<ث{ƵƠݩ൜صO:SSSSUK;ث{ƵƞѦX;NSSSSUN:uثƵƞŎܹ[>KSSSSSTP:lجƵƞØ|N;LSSSSSSTQ:pثڹƵƟ’Ŝ\>BPSSSSSSSSS;kثтƵƢΛcC;ISSSSSSSSSST<^ثyƵƣݽzX>=JSSSSSSSSSSSSU>U٫zƵƥֵ|dH=@LSSSSSSSSSSSSSSU?P٪⌺ƵƦ߼۸dMAI7KSSSSSSSSSSSSSSSSVBKڠ㧞ƵƧѪDEAABDPY_ƾƵƧҩIVSO1ixzuksdjQOY`^]ao\ESSSSSSSSSSSSSSUJKO(-MRSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSVcƵƹÃޖMHλji/B7>KSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSYUƵƑ[gނӽkB@l<46ALSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSXLƵƙ|oǜg볈WE??OSSSSSSSSSSSSSSSSSSSSSSSSSSSVHƵƾulrۧɢq#9ӊfTGFGLRSSSSSSSSSSSSSSSSSSSSSUKƵƞyc訂`}uȄiݟjf զ{VKHIPSSSSSSSSSSSSSSSSSStƵëg*NŇvSxUXhg|{ +ɩ嶑mYGJSSSSSSSSSSSSSSSSSXWƵƧէiC /GZa`XQPL*$!GLqֈ 1//////2!)SSSSSSSSSSSSSSSSSYLƵƃʺdDDB&$yf_  *OxW [-)0///////-SSSSSSSSSSSSSSSSSXHƵwYRKFa,; 6Y꼜 A/////////3GSSSSSSSSSSSSSSSSVJƵƇh)jb!XoTb͘ !*0///////1%3SSSSSSSSSSSSSSSSTPƵƮ8 i Do%@_{ʧџ! )z(3/////////(SSSSSSSSSSSSSSSSSViƵƖj4 OSSSSSSSSSSSSSXHƵƬ '= ײԡλN/////////0&GCITSSSSSSSSSSSSXGƵԦՙA Ԭ•t'2////////,2HDUSSSSSSSSSSSSVGƵqʜ.=ƦѾנ&3////////0%OATSSSSSSSSSSSSUKƵiǛJ1${-dzҮף,3////////2#P:PSSSSSSSSSSSSTPyƵržΨҡԶ?2////////0'J5EUSSSSSSSSSSSSUgƵ}վզЮg,0////////-;:;VSSSSSSSSSSSSV`Ƶƽ׺ڣ`&1////////0+A5VSSSSSSSSSSSSXXƵƷ׳ݩʆ0&//////////2&G2USSSSSSSSSSSSYOƵƬٯӢS$)32//////////1%E/TSSSSSSSSSSSSYLƵƞִڬӰg4%-42/////////////,:2RSSSSSSSSSSSSYMƵƔڨǠi2&)12/////////////////,4OSSSSSSSSSSSSYMƵƏѓڢݼr/",320//////////////////2'7MSSSSSSSSSSSSYLƵƇؠɩwqgbdI30/////////////////////2"?LTSSSSSSSSSSSYNƵ~ם̡e|ola_gqi///////////////////////0'AIUSSSSSSSSSSSYLƵ|֛ԢLgkpqw}////////////////////////,9GVSSSSSSSSSSSYMƵƵҘҭwƸ?////////////////////////1FVSSSSSSSSSSSYNƵ'$;יҸø`///////////////////////2,LXSSSSSSSSSSSXRƵƖ$43bac{Ĺ///////////////////////2'LYSSSSSSSSSSSX\ƵL,/?|h^eebdfd[ahVOwźB//////////////////////0$FYSSSSSSSSSSSVfƵƽ+0/TѸxjcaaa`c^tȻ]//////////////////////0(HYSSSSSSSSSSTPzƵƠ$2/gƷpȻx///////////////////////,AYSSSSSSSSSSUJƵƆ'1/xnɽ://////////////////////.7XSSSSSSSSSSWIƵm*0/»q˾V//////////////////////2-USSSSSSSSSSYJƵn-22zy/////////////////////1&RSSSSSSSSSSXUƵƞ1D}˿A//////////////////0/ OTSSSSSSSSSSoŵƋ%1ýҁP/////////////////2+PTSSSSSSSSUIõƇ¼ш³i11111111111111130"1PUTTTTTTTTZJõþɻxwwwwwwwwwwwwwwsmjklkkʻwwwwwwwwwwxwtnjjklhn˼wwwwwwwwxwtnkjkliiz¿̽wwwwwxwuqmjjkliksĿ̾xxwvtqmkkklkipĿtpmkkkkjio}ÿ¹¾tiijjnr{6;?%=@ >?@=& @OkaeH/41XkjeR*5XkjxeR:3Xkj)eR6Xkj&-j'9bR> ,u.Xkj- .j&'R7 x%*Xku_25 1.j5$/3:zR(>  S9XkvM_!9 .j+ '6JBkwiR4<. XW51XkcZؖ`Q$ 2j$ -''=hRaC( ]` "&Xkld00(y: IVj5 00/A<N.0 .li"#jR=/0-[p*0P!!XkT{k6&jE #I=B)<l MuR@ 7"-sj#*$Xk{{E3h\D.jC3BF1$ク8X;[qR@?>4&CY#&XkwK:!p jC1aM;_nnjR@ tF#;-r_kwKo+,YpjC1pGF8;_s9R@#5q#B 2]kwK  c'jC1}y#;_sKR@-u#9^kwKD/qw"jC1ksSB;_sm=R@ ")#V>R+kw>:&}}zuo jC1m{}LC;_i_R@`)T9Dd#kv6,cjD=5Z:T($R@!%>1.ww}Ewwy_wwwm#@kH.e:p\n Z#jHalllllllllktDa^rxoYb-DPBbllcRR5 cg^y9#jLk&ɐ^!kja6ggwRR?CR z }faUi*otӗU㔵/ #kR *eDR dv/_cpPfDxA[3K5 A7qouiON%+Y?#|CBN7u+=^#{mL)*w*+Q{:e4;.}! αN;WwP,UQЏ{ʕ+mц!ͤʿ+&U +ZzPw>͡"ʺIۋ< jiYN}Bh8Y`J`cap{q>pT3jUfOeQ Ox?Io96Cl3/JJJ9Bk3B.S5 k.S5 -IZN50&3H4By܎ ;`wky܎ NK3ܙ`ɇHm'ܰ:|B0Ee27k0E8F6p? 6AB1Nnzk1NlPppE;2B7$ BRk7$ Y$MR%mii&>thEBl Dkl kHWoxff[sFE BB%v_pkB%=V:ffbc E=1B&?r&k1IYcff MZEB%A nTk%AI$ff{G6|E }BNuh8Xj kNuhI\ffpGeE@'B=T8< yk=T֕gTBcBrffpG7EB)&Kdk)0?,ffpG]E B\ke ffpG^1EBBfKkf ffpG j/IC,_J8+tEEEEEEEEFJWm ,_J8+NggqHZ==#=#=O HtLo`%tLoh)Hyݠ{mmwN#=#=XLP ] *pvuVb:G#=#=ngcnQcnGf=M+j^Sfr#=#=N\\\-\\\\,dĖh U\\: \\\']e`]\\\\\\\\\]am dĖh QzpI\\\H(yb\\\J9\\\B9\\\=+\\\M;:\\\D \\\\ \\\\C$l C P*̾'iғ "4.DCCCDDCCCFDCCCFFCCCCECCCCDCCCCDDCCCDCCCCDECCCDCCCCCCCCCCDUDCCCDCCCCDECCCDCCCCCDCCCCCDCCCCFDCCCECCCCDCCCCCDCCCCCDGDCCCDCCCCDFCCCCFEEECDCCCCCCCICCCCFDCCCEFCCCCDCCCCCCCDCCCCFUHDCFDCCCEUHDCFKDFDEC`IECUFCCCCKJUIDDCCCCDCCCECCCCFCCCCCCCCDFCDCCCEECCDDCCCDDCCCECCCCCCCCDFECCCCCCCDDDCCDCCCDDDFCCCCECCCDECCCEJCCCCCCDICDDCCCDDHCCCDDCCCFDCCCDUCCCCFEDCCCCCCCCCCFCCCCDCCCCGCCCCEDCCCEEDCCCCCCCCCCFICCCCCCCCCCCCDECCCCCCCCCDGCCCCDCCCCDCCCDECCCCCCCCCINDCCCCCCCCDGCCCDDCCCGIDCCCDCCCCFDCCCCCCCDCCCCCECCCCCUCCCCENCCCCDCCCEDCCCCCCCDCCCCCEDCCCDCDDDDCCCC`FCCCCCCDCCCCCCCCCDCCCCDCCCCCCCCDCCCCCCfCCCCCCDDCCCDCCCDDCCCFEDCCCGCCCCFDCCCDEGCCCCCFGDCCDFCCCCCDCCCDDCCCEDCCCDEGCCCCCFECCCCGDCCCEIDCCCDDCEDCCCCCCCDCCCDDCCCCCDDCECCCCCFDCCCDEUPCCCCCCCDCCCFDCCCDUCCCCFDCCCDFUDCCCCCCCCJDCCCCCFCCCCCDCCCEDCCCDFUDCCCCEDDCCCCCCEDCCCDNJCCCCCCCCCCDCCCCCCCDCCCEECCCCECCCCCDDCCCFLDCCCECCCCFCCCCFEDCCDECCCCCCCCCCDCCCCfDCCCECCCCFEDCCDNGCCCCDCCCDFCCCCCCCCCDCCCCCDDCCCDFCCCDNCCCCDDCCCFGCCCCDCCCCFUDCCDCCCCNCCCCDECCCCCCDCCCCDDCCCEUDCCDCCCCNCDFEFDCCCEUDCCDDCCCCCCCCDCCCCDLECCCDECCCDDCCCDDCCCFDCCCCfCCCCFECCCCDCCCGCCCDCCCDHCCDGCCCCFDCCCEECCCCDCCCGFCCCCCCCCCCCEGCCCCDCCCCCCCCDCCCCDDCCCDCCCCGDCCCDDCCCFFDCCCCDCCCFECCCDCCCCECCCCDCCCECCCCCCCDUDCCCEECCCDCCCCEUDCCCCCCCCCCCCEFCCCDECCCCCCCCDCCCCDDCCCDCCCCLDCCCDDCCCFDCCCDHDCCCFGCCCCDCCCFDCCCDDCCCNCCCCIDCCDDCCCEGCCCCDCCCFCDCCCCDDEFFFCCCEFCCCDCCCCCCCCCDCCCCDDCCCDDCCCEDCCCDDCCCFCCCCDCCCCFCCCCDDCCCDCCCDDCCDCCCCDCCCEDCCCECCCCDDCCCDECCCDEECCCEFCCCDCCCCCCCCCDCCCCDDCCCDDCCCDDCCCDDCCCFUCCCCDCCCFCCCDIDCCCDCCCCCCEDCCCCCDNDCCCECCCDIDCCCDCCCFDCCCE`DCCCJFCCCCCCCCCDCCCCDDCCCDDCCCDDCCCCDDCCCFFCCCCCCCFDCCCDCCCCCDCCCCCDDDCCCCCDDCCCEDCCCDCCCCCFCCCDDCCCEDCCCDCCCCCCCCCCDCCCCDDCCCDNDCCCCFCCCCCDUfffUffffffffDCCCFCCCCCCCDFCCCCCECCCCDICCCCCDGCCCCCDDCCCDFFFFFFFFEECDUFCCCCCECCCCDCCCCGICCCCCDECCCCEfUDCCCCCDECCCCECCCDCCCCCDCCCCDGDGDCCCCCCDCCCCCICCCCGICCCCGCCCCDNCCCCCCCCCCCCCDCEDCCCCCDCCCCEDCCCCECCCCCCCCCCCCCCCCCLCCCCCCDCEDCCCCCCCCCDEDCCDCCCCCCDCCCCDEDCCCCDCCCDDCCCDDCCCDECCCDCCCCCCCCCCDDCCCCCCCCCICCCCGICCCCGDCCCECCCCCCFUDCCCCCCCCCCCCKDCCCDICCCCCCCCCCCCCCCCCCCCCCLUDCCCCCCCCCCCCKIDCCCCCCCCCDCCCENCCCCCCCCCCCDCCCCECCCDCCCCDDCCCDDCCCCCCCCFDCCCCCCCCCICCCCGICCCCGDCCCCDCCCCEFCCCCCCCCDCJCCCCECCCCDCCCCCCCCCCCCCCCCLFCCCCCCCCDCDDCCCCCCCCGCCCDJCDCCCCCCCEGCCCCDCCCDCCCCCCCCCDLDEDDCDLCCCCCCCCCCICCCCGICCCCGEEEEDJEEEEFJECDCDDEHEEEFUEEEHDDEDEEEEEEEEEDDDDFJECDCDDEHEECEDFfFEEECFCCDCCDKDEEEECEEEFCEEEGGEEEECCCCEFEEECUEEEEFUEEEEFDDGDDCCCIUECCCDfHDCCCCFIDCCCCDCCCCCCCCCHCCCCCCCCCCCCDGUDCCCCCCCDDDLCEHP;999:;999:;999:<999:9999999999::999::9999<999::999:99999:U9999::9999=9999:999::9999999999;:9999:999::999:999999:=:9999:999;<9999<;;;:99999999I:999:99999<9999999999999:999:U=::F99999U=::F<>;;;:@I::9999999:9999:999999:<;:9999:9999:;U@;99999:9999;99999U:999:;999:FU9999::999@:99999>9999<99999;999:FU9999:99:9<;999999999C::999999999:999999;:999:;:999::999999999;L9999;:999:9999:>:999:999;999999;999:f999999999:>:999C;999:9999;9999999999:99999:9999;;999:;999999999;;999:9:999:U:999:999C<999:=99::999<999:99999U:999:999CC9;9;9999:U999::99999999:9999:L<999::999;999999999;:9999f:999:>999::999;:999;99:@999=:999:99999>999::999;;:9999999999:@999::99999999:9999:9999:9999=:99999999;F:9999:999:;9999;999;:999;999:999:999:@99999;9999;999;U:999999999999:;999::99999999:9999;9999:9999B:99999999;<999:;:999:;9999:999:;999:;99:;;99:I999:99999;9999:999:C:99999:;::;999::9999:99999999:9999;9999::999<:99999999;;9999:999:C99999999>9999999:<99::999;99999C99999999>:9999:=999::9999999999999:9999;9999::999::99999999;@9999:999:9999I@:999@:99::99:999:999C999999999I@:999@:999>:999:@9999:F999999999:9999;9999:<99999999999999;:9999999::999:9999::999999D:99999999999:999:9999:F99999999::999:9999999999:9999;9999:;:999:<:99999UfffUffffffff9999;99999999::99:;;:999;I:9999:;999999:9999;;;;;;;;:;;:U::99:;;:999;9999;I99999:;9999:fU:99999:;999:;999;<999:;9999:;D;::999999:999::9999;:9999;:999:;999999<:9999:999999999:9999:99999>:9999999999999999@:9999:9999999999999:;><;:99999999999:9:9999:999::9999:999:;99999999999999999999:999::9999;:9999;:999:9999999U:99999999999:<:9999=;999:99999999999999999@U:99999999999:<=9999999999:9999C:999999999:;9999:999:;999:;999:9:999999999999::999::9999;:9999;:999:99999;;:99999999:@9999<999;99999999999999999@;:99999999:9999999999;999:@::999999:<;999:99999:9999:9999B<:999:@9999::999::9999;:9999;;:::>::::::@;::99:;@9::9@:::;:::::::::::::::::F@;::99:;@@<;9:;;f::::<9;::::9<D:::;:::::::::;;:::<:999<9:::<@:::::@:::::99@;9999IU:99:9f@999999=:999999::99999:;:99999999999:GU:::9999999;L<;=@}}{}vovwwyyyyyywwxy|nymvzvwzzusy{yzv|rfs{wphj}yoozro{~|{mrot}{|zbajot||}to|y}||yqpno{{yxvttvy{wy~|zxwx{ubm~˝|lk{}`fbMxtghx鹉ƚь쵦g{y|eZUPP]yunƚЊ|h{~}tRNSaxfϚčtzlxհКѓxZr禝КѬ|OpǸǤК͚Ql俤КȋpK{КLj՗fSˑښКxcVڎКѴ~w~uldb晽}КѭgaawКѢvb~`WКўˈ|vddjdS茶Кѥαm\oZg_f[֟КѤŲSoYg~Wj|^au^ͫ֜Кџ×ǯJ\]djYkfcRoКѥݶfϺrKf^\h^[n]e]КѸzô{bPd\`\sv\[un_`{К}۹οq~^Smp`d_`\}b_[|^fIӔΎКѐۭİu`m_UXYl}[`^YųV`]q]bYКƂ٨Ÿ´uW``_KfYo^_i|dN_]^b_mКѢ٪~ԠتötWc\`cGy\Yy~qcKMa`q]fPʠКπ٫yݩߪp]^`Hs~]`Sy}Ԑt`^LjlCY_l_`iEКђܱÐНϑkg{zd\YU~رλsY_a^@VFa]qh_c\gК|߽yڟ׈Қ—rūԫɾhoZßᤘyDz]``bSInJPb`d``hU⾊КѤМ{߼ݬޯpˊɗ{ܥ䧌ݟ]e_``dF_]D]````aaȁКсޫ֝Òʕ}٢xӇڢ~ݦ嬈˺uJv\``a_@}lSIc````eSvКѿśx߹{߮Ϙ܇ඓᯫږbvN]```cQI`ETb```a`{zКч|w֜Ԝ”ߎ˗~͓qtMvO|e_```bDdlVBb````cRԉКoZM߻pÔ}ߴtzڢȽ]{LvRpw\```aXD`IKc````apϞКѮkW]Ӿۢu௓vިwަⷐiZ}LyUf^````cHUk]=^a```dRʺКv`Oĵr؞؟ڶuþe``RZ~M}{Y]h^```aYCr`RLc````c^}КatZVv_ZrV˘Ҝ~˷\˽h\`Zj_w|Pp[V~\````cISaaF\a```cQxКѥycSmrd]^VhĚs|gʯj~}}}tEk{\`_XdkvVe]Rt`````aWFko_TOc````b^ԊКp^Nl_WkFnY^]T[\E^]]_iOU^`bQkbo[Z`Qhq]````aIW\aK]a```cTΦКbrZY}qv[]hHh_`\^{^[8a`aasz}yYFd_aVore[QO7.3/)QcovYSr|m]````bSKe`aTIc````dWКѥdSqk{hTsFcb_aWhr`U#R`]`aOK;\zdafS׭z\adpa\`fI^_``aUKka?yaf:-^```aI\]u]```G_`````cZzԦКneaIk\fvpBG+P^`]`aQMT`afTۦ\`ejog|^`eSX_``a]Lu`I^m^o8A```bMUab_``cLSb`````fM̓КѨ~aZ_WtQQ7VT[`}]`aS`U]agW۠\`ejۊpkb_cUc`_``d[}^UH}ZkdK\``aSOb[```cO@d`````a`kyКeQW^gJbVaVRau]`aN`TXcgYۛ\`ejyx_h_cZq`_``elw^_=\cg_^``aXKb]pb_`a^:_``````cOКЃkRњTmYo_`bXOdk^`d@bSSef]۔Dž]`cns~Yo^bWaa_``ekbd?wa`e````a]Ia_ad_``eBLe``````bSКјqPoьlNzx]`bY]ib_`bHcOZef]ۏ]aayܫX~w]bYma_``ffykbKVj^a``````H_`_kd```cQ2aa`````a]gКѩvOyycDo^`a[sp]{b`cIudPidfZۘz]a^n[r{]bZd_``ednw^XEw\``````bI\```````ab=9f``````bS~КѦmQpvF^rl^`a\syZte_cJheQۀ`g\܎r]aZݥ_iy^bY|g_``eg۟\dG\``````bIYa```````g;GUe``````cLКѪsVqh{XZ`jh_`a[Zje_bO[fQܴTi\ݔ|r\cTncbq_bWwi^``ekp_fMda_`````bKWa```````g>SQe``````bMК̤tsleynd_cd``a_Ȁ\cd`bUNgQUj]ݜnt]eQ߰h_i`cVtj^`a`xgaWml^`````bMVa```````eJGRca````a^bКѽsdjxsb_`a``a`voa`b`aZMhQkf]׆ru]fS߄m]c`cT߀j^`a]ww\cqsy\`````bNVb```````cS>їNca````bYvКѠtysf^`````bpۮi^``a^WjQݔ^`qvu]f\ᾇu\``eSߚj^`cV|^fZ\`````cLVb```````b]9ѧT\c````dWКq}ll]`````dnuy\```d`xlPTX|w]dnုy\``fRḆj^`eSxiaUv_`````dDTb```````a`7pQbb```eMКr~eu\`````ejp^```fhtqPdYzu_[̈́|\``fUxk^`eS\a}c_````e@Mc```````ab3їVXcb``fRКu{]\`````e_ےh^``fdvOP_}vaR㏨\``f`pj^`fbu`fVtg_````f9Ce```````ad4łQ\ea`dOКуyW]`````gUo{\``ffOVB{rxdXၳ\`aazrg^a_ޗ|`Sn^````g6@f```````ad8ѽO[gddQКѕ]]b_````gSr^``ffqNHkqax暝\`cU}d_dRsa`|zx\````f5=f```````ab;ŀʼnRTejSКeWn]````eRݯ}g_`fcSQhnS~\`eS生a_gVuaRy\```ac8=f```````a_Jp̜lVYNК~w[````eRݐt]`feἅ\vfmZ飩VK`]飖`a_ࣀpTĀ]```bZ@Ae```````b[eҌƛgEКѻo^````dU|\`dkpodGb^Ό|>QVwȗz|QWIֈvi[u_```dNFHe```````dO˸Кыe_```bZl^abzysuyeV{`KVHթ}~V[do_N````gAPKd```````fEtКѷx\```a`zr`a^or_鶕[zx^g[ȻXtO:2fpdYSNLKMMS^gZ-4>]b_``g9MVb```````g>ԅКp]````eh⦍c`R䞋G믽~YߜA* $.ba_`a_CA_a``````abLͬКјe^```gWsLJ>`IdޓP(  K^ekotz"\```eNU;e```````eOqzКn}[```eSۓYfd`ЃDqܮɡϻ 9N(7=<3 ;?~`_`g>^=g```````g6}Кєa_``bVփaJh`aË26JQPF1B\_qa_a_?SMd``````cZ?ϝКs|[``aaޢG  e``````dV9srКы[``eR NcdSkmcQ3:ٌbdQVFJd`````ae6LԂКtl]`bZ@#  sP[stlV7#/!"}vڈ`g]^a`````gAE\ΥКn^_`fe픲+ FaӿF -9O@9[pvmV6AZ7; ڀbUVf`````ePK;~ŅКo|[`eN7sH:Xc0(G]c\F+gSY}b_ٴRd````eQGQ>zКxl\a`g#?zs#'8>:-/rorO|`a```dUFc4eԈКюc]fNN 11ȅ=./6@gvsrkWf```fVHcS8ͤКѮl``\SĨȲW&٦RٹQe``fJC`cCkÈК˃x]eR Ĺʿozrٌ[b`fRK`cPMuКѶz]`x B<glkcagLu`aC}КѲz}aS'9CA=3/29CCHH-;ٌcXhcVncNPњКѸ|vfQ\UjWc\De˷КɒqrYْQSTHf{Кѹbt|^ϭD@MyКѤċ[Z8ӊКѯTVeĒͥКӀ>ƕК˄9ɗzКgTtʖ|К\NW>YʖӋКq^`XTXXPIʘΤК~E```bXD˙ʿКv^]``a^Cz˜xК^M``aaDk˟wКiH```cHcˠԇКэZ```cLRˠϞКђۼߊL```cOIˡʸКѣ谠⼰t]```cTFˡ~КѧɬtEa```bWDˡ{КѥG[````bZD|ˡӆКѦŕڌOXa````a]DsˡіКѦuIXaa````a_Dvˡ̭КѨ“ՍRP]aa`````a`DqˡĈКѪΞҘ^HUaaa```````aFfˠzКѫ‚PIVaaaaa```````bH]ˡxКѮַ̠gMKX``````````````cJYˠӆКѯ߽ҥj{WV````````````````cMT̖՝КѱĤCQbx›zlx}W```````````````cQK}FKKGGGGS[^~КѱťTd`[DP```````a``````cVE߈Y```````faµКѳŇ`b`MJ``````_^`````a\Dɥxnhfgjr_X```````fVКѲfea^ٸD`````aVCc`````?G[HHKMPTY^```````_ZVQOPSX````````fRКѮUghB`````bP9a```cIN]````````````````````````````````dSКѹz}~{{ŴQj@`````bKDXa`cSIc`````````````````````````````````bZКсћi?`````_HQNba_D_````````^`````````````````````````acmКх򾶴kS````aVK]GbcJSb````````_``````````````````````````fZКёp~{Y````bOSbG`\Ha````````````````````````````````````gRКѠ˄e````bG[dKZNRb````````````````````````````````````fPКђ吨ѥfݯXLS_<`bSRI^`````````````````````````````````````dTКѝ좴ʢkټl=KY[FJa`````````````````````````````````````a]КсɧپĆM16\_````````````````````````````````````adkКĨ͊ꖃ٣n>\HLX``````````````````````````````````f\Књr뙠١<:UDBM[``````````````````````````````fTКѤΩتُ빞p_O[```````````````````````````eSКȭ縱ԗ0ʇp[NNPX_`````````````````````bWКѦ}tטܙ}Ӆ_էZRQT]````````````````aa|Кʹj%`޾ht "帓p^NPBS```````````edКήoMփӣﯥپwK```````````gWКϴv*DԦ걨걣٥"&ᠪQ```````````fRКǩxy_!Cۢݵר뺑6W˶ڀW```````````dSК}%!Tܢ⿁ұ+' ˜̩z[```````````b[КѤd)Cݢ઎̶ҹ !!/𦕺sq՝t_```````````acjКі {з|+TJ``````````````````cVКx.qlƙ[c-Q`````````````````a^КDŽڶ{ԩ߇}넡ꯠYͬ_eJ7``````````````````diКѢݶ߀nXyۭj㿡!$-_b_&``````````````````fYК೮׬џpɳa%!&,``dD V`````````````````fQКѷSƱj $ĞОb=]NR`_a\8`````````````````dQКɿwѬ뾓ϵwr /Neb___e3`````````````````bXКѕܸˆiDb____cS\````````````````abrКшޡ$tû~!,b`____`d A`````````````````f]КѱѮc $=h{yn71+gz>d______e@&`````````````````gTКъǡnZ@!"q[O 4~T S'!Rc______a\`````````````````eRК֎e B\*9 P)?!+a`_______h*N````````````````cTКђ|'c]S࿑ "$#&Ub_______dI3````````````````a]КѸ4 c Aq1eɼœ"n/h_______`a'`````````````````dpКўög2 ]ɾfj1*i________h-]````````````````f]Кь٧PR|W`zKZb_______eBD````````````````fUКуɿ =Ŗչǀ@e_______cV<^_```a``````````eSКЂ˿ν Rƶĩ7d___kw`a4]a}```````````cTКyϟ Wmk.3<Ñķ@___ery=]sz`````````a[КɼɽѴxɞUp *Ƕȟ¿YTa_J`````````abrКѫȤةŌPưȿŞݾɣxFd_|``````````e_Кҩߩќ絧ٱ΂D˽ξŕ̿Ɔ0i_``````````gXКի᪖ګ܌҂3/ ̧ټĕƿƟ'j_~``````````fRКլᬖѲîx$= ˦Řʿ°@a`c{```````````fRКլ᫖уԣǐ)A 驽š׾jMd`Z9TOb````````````dRКլ᫖Ŀg_``r`aКլř᫖ΨϷإ\/"Ψ߭;斖_`ٻКլ踖᫖쨖쫖ּ㡖薖x˖gʖ柖λ閖Кլ՟᫖ݘ˖뙖ﮖȤߗ̀іkіЦКլƗ᫖묖𗖖䖖ޖ¯񱖖ٖ椖Ӝ_`ǖȖÖКլ᫖ǖ–ӖѲܖ̖ˑ󨖖_`}ưָКլ՟᫖ᗖ–ϫ𘖖斖X%KvÖ_``gUז׻ҠКլŗ᫖昖薖斖і𖖖ٺŖ陖N[le___```gUȖԽѴКլ᫖ܖ򯖖򟖖Ö򖖗Җʡ𖖖–٥ἢܖea____ʖ_````gTӴÖКլמ⭖ϖÖϘߖϖ٬______Ŗ_````gTԨ̖Кլė֯ՖȖ疖疖Ζӕ斖˖ٮז______Ȗ_````gVԵÖКլ󱖖㖖ǖ𖖘ᖖѓŖݖĖ_______````gSƖѭКլڞ蜖񞖖ɑܹҖݛ_`__`__````gUǖЗКլŗ樖ϖ˖ŏՖ񶖖x____v_````gUΠϟКլﰖ˗ߛʏꘖ᝖זӜ_`}```fXזѼ策ɖɖКժܝҹ쉖˜ڬǖҺΡݱ͹_````ec蹖峖ɖɖКӧ–񦖖}맖̀ϖkϖ``acm֜ږ沖ɖɖКᥖꪖBݖôם⠖斖쾗}ŖjƖ``a^ŲЖ߳ɖɖКβѯXIJ;ޘΪtg``bVКэ]`__t߹e`b```eSȦٸКpUb_yŚl__________``___`f/d```````a``fSбͲКrZe_šޚ_`______`___________`dI`a`````````f\ȠКѧckťa__________________c`=[a````````a`w騖Ϛђ!/ťh________________`dU.\a````````cT԰ƚЌħ______________`d]@""-[a````````fRƚkkkkkkkkkkkklmlbUOPSQJsimllllllllnk _mkkkkkkkkklmldWPOQSKMhmlllllllmj{vskkkkkklmmkdWPOQSNH[gmlllllllnfkkklmmlg^UPPRSNJSngmlllllllnklmmje]VQPQSQJMetffmllllllmh~md[URQQQNJLYuxofmlllllloe{SIIHILOWe|sfnllllllltưƤƺƘļƳźĸƿ­ƷƪôƻƜƽƜęƷĵŞƠ~ٳƹ֟ƺִw~z_ٺ꺉ءԾ®}oiabsջڳךɺeagy΀ڮ량}ʘᆘڧհԉԹnڧ禝͓Ö֫aǸ֥֞do俤՞̋Y՘Ӡžֹ~bː۷˳џѪןz`ڎؐىНɑ΅{i晽Γўֲֹwv|ӡʰ֓y֜vk˞Ԟ՚||͂{a茶ĕԇտqַoֶt}d֟Žgӊm֝l՚sw⩆yͫ׺ßԫ[ɰ֪qr|̓m~zc򛁭įݶhdzӎ\ՠж~sr״spګr}h܈͙yc|Ѫ֡qvqՒrpԈuu~ьݤseԋwӫ{tvqytp۷s}XӔȧёwԇtil²֮mԪӼ֛pvsnנjurٍrydϑk۾vut\ǧȨ~nԨիvt|`vrsyuqŘѐk|١qvzUϠ֖qnդ֞٬{֭\_wwr~aʠƓ¿ŸԊrsvWԝֳrvgԸՖ֣㭎vt]τQnttvMʣ֯ۦԳ୦zך|qnhԸܘʒ܎muwsN׮kUxrtzri좸Ѳ绵ⷴյϷܢnڹΌծrvvygYψ[byv|uva۲ž߻ѯɊŲdzöܚ۵r|tvv{Vv׮rSrwvvvxwƽũ亱՚۴罴罴Ěב\ՓqvvwuNφfYzvvvv|cƄٻϵƱ߶̱vӓ_֯rvvvzdZ׫vTgyvvvwvʁϕ乵㹴ճ۵䭋Ԑ`ԓa|tvvvyR{υkPyvvvvzd¸ɉo_ϵyֳ˱z罴բsɘ]ԓe֓qvvvxmQׯwZ\zvvvwxzɼęУۄks龹~DZ³y뾔̮{֭o]ՕiӹȶsvvvvzWiσrKswvvv{dʺuaӿͦ~庴廴Έ|vvfշo^՘msǦ̀svvvxnRץue]zvvvvzeÎuoiեӓtnfܷ⺸xòrvotc֞pk՝qvvvvzZfwwVqwvvvzdɁƘyfΎ|rujŵⳍ|ȚmtȶSטqvvm|j}֨rfմuuvvvxlUӉuhbzvvvvyhɋ̫saոׯtlVԳՉnsrhգopSsstu\qȢahȺٱsvxdυypoֲudπrvvvvyYk֤qx\rwvvvzgÞxnn֛ՒptYթπtvqu՚so@wvwwbrΛnVϩ{twjʧ}qebC7=82e{nf֚rvvvvyg\}ԿwxhYzvvvv{aư|fЃɀhV{ԞyuwlӍve?zvwqrәwOզՈn\D5V]m|}bidFts]72Gպu}tvvvvuUpσsxUqwvvvzmƂ߱tbt۷roAәtvxh΄yXB}vxk{nՖ~{NճX16EO֞qxz]xZ>rvvvwsYwrܟqvxj]yvvvvvY˖v]SԻs|rZL*avrvxbշ]Hqf]|v~Wxmbqw{W~wqv}MdչuuvxgO΃xMw}G6svvvxYqrګrvvwWtvvvvvzouɞ}xXնq}QV4`nurvwd֭]gtgvw~Ttsh֨qw|Snsu|YMջuuvwrBԑvXuІsEOvvvy_iwyȺtvvz]fyvvvvv~YÂ˺ܝvnձtkddCjekurvwg֩uhykrxRqtnǫqw}SgԋyuzdLջvuvv{HբshY֛o|]qvvygbypvuvzaO{vvvvvwwp|c׮ְlvZyjxfXwrvx_֨vd{|gzRo{{qw|T֕ttzk[Ծvuvv}RfִsuIֱqzusvvxm\yrxuvwtGuvvvvvvz`Ãe֯hntvxhJ{sv{O֭x][}~UlrwzW`֝nsykiawuvv}Sky{Gxu|vvvvwrYwuw|uvv|P]|vvvvvvyeЌcڢ`rvxkJ́yuvyXְzVY}~Torwx[z֨mrynlXyuvv}R΄xVbςsxwvvvvvXuvt{uvvzdilzxvv~dۚu֠qvvvvv}Pm̀svv~RgԒUybbvx`vՠqvv}Wt΃sv}W~w}Ptvvvv~FR|vvvvvvvw{?cq}xv{`ܗlֱrvvvvvN_֙qvv~VݞW\Qq{V֥qvwweitxtkpubfЈsvvvvBM}vvvvvvvw{B`q{{btuytvvvvPxֳsvv}Yx\zYlwd~ܧqvzim|t|`exw^Օqvvvv}AK~vvvvvvvwyGǗdg}exkшrvvvv}U~tv~Xfٿd`mbqv}^{xtTxS`֡qvvvwzEK~vvvvvvvxuWxhn`方֟pvvvv|ZlԐrv~Yq|rYԮk\v\ھvwulzhpԭrvvvyoNO}vvvvvvvyquɊ|Sշsuvvw{d|k֤qw|]jԉloKrt{ðLdkgclRkRdոtvvv{`UW|vvvvvvv{b}tvvvyomjֲswyc{a}Zǀw[jT͜jpZcu]tqܻwuvvOb]|vvvvvvv}SǼǁ۟Քqvvvwv`ԾvxsrtWmćqst\skbG9W_TKEA>>>?EPXI&>3c`տyuvvF^jyvvvvvvvJֺɅձrvvvv|YyvbJqƕw\O2   %zxuvxuROtwvvvvvvwx\ʺ}svvvTgǡ\ZFzfVxˢeĂH&  K^ekotzNtvuv|`iH|vvvvvvv|bºË՛pvvv|[tg~{]pQbɼίοȻ 9N %((# 8+g[vuvKsJvvvvvvvBẺʂytvvyjsxZ[_WҿË'"054.!BXApxuxvLf_{vvvvvvzoJ巺ė֛pvvwwj; 4eˡWٸ|A%6::6* lmhzt}WmOtxvvvvvvHoиҕ{svv~\) 9эټ~u*8>?:0 R&czu~EvK}vvvvvv|jF~~צpvv}\— EaO3EF@5" :bfx{`FG[|vvvvvx}B]Ƀ΄rvyoů7   q;9JKF8$%}bdv~Q`txvvvvvOTmÞֶsuv}a& >s[? $=: %oD]ɂøm_ʍY|BNjЩ~R榌GrÝuˏcKιԥ}>ĈhÿºoNʁ]ürGbv{iMHiNJrQvmcclcZÜ}ǺļǝOvvvymSuŹļOrvvxsRȺÇcǻͺh^vvwwTz㸺ɀxùþºvTvvvzXq帺Ɇľl^vvvz]b为ęϱ̷cZvvvzbY帺뮸ۦձٿ_lvvvzgU帺簭뾣ᾏYOvvvvylT䷺džگbOovvvvyoRⷺȈŒÙdNjvvvvvxrR᷺Ɣ͡XNkvvvvvvxtR߷ÓѧgLZqvvvvvvvwvS~ܷΝˣnSQgvvvvvvvvvwxUu۷ĉɦdNThvvvvvvvvvvvvyXnַʁֶ̩kUOYlvvvvvvvvvvvvvvzZiηȆ߼ÕjQBH;jvvvvvvvvvvvvvvvv{_cǸʗԾݷP^izծuOAGR\`yZkvvvvvvvvvvvvvvvzdZyRWVQQPPW_bԽ踺g|vp9k|~zmwgmROZ`^]bsfbvvvvvvwwvvvvvvzjT҆nvvvvvvv~tƺּwyv>HYlvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv~f񑅤wС˲l볆XHGQpvvvvvvvvvvvvvvvvvvvvvvvvvvv|f̘}tz毯խv1„h\X[aluvvvvvvvvvvvvvvvvvvvvvykƥhg}ьo秊mY֪d^`frvvvvvvvvvvvvvvvvwwֶu#KВYY\nn} $幗wgX[Mgvvvvvvvvvvv}wϫj>^ö| p[vvvvvvvvvvviة7qxlzx}t ܜcvvvvvvvvvvv~dʣZ2ǏmzgВ}3W½~}kvvvvvvvvvvv{gAԒmX˞ն) —xpvvvvvvvvvvvyp^&jܕmdt࿬Ȭ0웎mkɔrtvvvvvvvvvvvwzCӐk}bοz І嫓Hbuvvvvvvvvvvvv~k| %XỌi[|εfcpRmĺXvvvvvvvvvvvvvdr'7zrr_UeNA  ľWvvvvvvvvvvvvv|eЕ #Vk˜~u.Jyi_lTvvvvvvvvvvvvvyjƥMPԹjޮR#_>-  JOvvvvvvvvvvvvvww칗UwqO8^ב_& Tvvvvvvvvvvvvvv}uXޘhfˮnp_tǴs zsvvvvvvvvvvvvvv~hΰrqD^젶ʦȿV 7|Yj^vvvvvvvvvvvvvv}eΟڕg5 *Nrڏǫw*% vvvvvvvvvvvvvvvvvvzi۠"PrVMq}ƽ(+ bvvvvvvvvvvvvvvvvvxsϬuʡѓ[WZm{kfgn椞V¢ )+! ?vvvvvvvvvvvvvvvvvw{}ѮؖWOd#@eݿ g,)))))))**$vvvvvvvvvvvvvvvvw{d/ kCCCCT)TCCCvkCCCCveCCCCCCwVCCCCCCDCCFCCCICCbvCCCCCCCCMCCpCCCCCCICCbwCCCдʾCCCHCCS/'.+))xCCCT)TCCCvkCCCIvvfCCCCCCCCCCCCCCCfCCCCCC}CCCYCCQCCCCECCCCCCCCC~CCCМʇYCCCCCCCCCCCC[+*))))CCCT)TCCCvkCCCvvvfCCCyCCCCCCCCC\CCCCCCCCCCCCCCCVCChCCnCCICCCCCCCCCТCCCCCCCCCCCCCCCk))))))YCCCT)TCCCvkCCCvvvfCCCeCCCCCCCCCDCCICCCCCCCCCCCCCCICCtHCC{ɍط|CCCCCCCCCmCCCCaϷCCCCCCd))))))hCCCT)TCCCvkCCCvvvfCCC~CCCCCCCCCCCCuCCCCCCrCCCUCCsCC~CCFCCCȋzCCCCCCrCCCCCCWŞCCCCCCU))))))CCCT)TCCCvkCCCvvveCCCCCCtCCCCCCZCCClCCCTCCO\CCGCCLcCCeCCjCCEzCCCSCCO]CCGTCCC꾵CCCKCCE)*))*)wRCCCT)TCCCvkCCCvvvfCCCCCCnDCCCCCCDCCCCCC{CCCCCCnCCCCCHCCCCCoCCC̺wCCCCCCnCCC\oCCC|CCCK))))GCCCCT)TCCCvkCCCvvvgCCCWCCCSCCCCCCCCCCCCCFCCIPCCDACCCC|CCCCCʿCCCFCCIPCCDRCCCCCCCüCCCC{|yCCCCCT)OCCCvhCCCvvv~jCCCCCCCўPCCCCCCCCtCCCCCCCCCCCYCCCCCCCCCzǮDCCC=CCCCPCCCCˣCCCCCCCCCCCCCCCCCCCCzǮDCCCyCCCh׿WCCCCCƣsCCCC]VCCDCCCS)zCCCvCCCvvw}vCCCCCCCCCCCCHCCCCCCsCCCCCCCCzCCCCCCCCtCCCCCCCCCCCpXCCCEf^bcfCCC`]MCCCCCCCCCCCCCCCCuCCCCCCCCCCCpICCCCCCCCCiCCCƢ`CCCCCCCCCDTCCCPUCCC{ꀩCCCvvwzCCCMCCCCCCTCCCCCCsCCCCCCCCjCCCxCCCCOCCCCCCCK,CCCiCCCw_oCCCCCCCCCCCCCCCCOCCCCCCCKXCCCCCCRCCCuǢECCCCCCECCCYQCCCRCCCvvxsCCCn캘vxCCCCCCsCCCCCCCCƼ鼼tYo8իuYoĦ\j~oHJHE¼|üvvyjݼvCCCӼ󺼼󺼼5䷺ƭ/ؙ))Eմ}vxvvv|eqECCU%*)rƾ踺mɻD))))))))))+*)))),${wvvvvvvwvv~eDCSCCCC&+*ļ긺vʼm)*))))))*)))))))))))*+ uwvvvvvvvvv}oTCCCCHisPCCCCC+6Ļvʼ2))))))))))))))))))+*nxvvvvvvvvwwcCCCCCCCCCCG&%»𺹺xʼ>))))))))))))))))*+%qxvvvvvvvvzhpFCCCCNdſﻹ˽V+++++++++++++++-*5qywwwwwwww~eĶnnnnnnnnnnnnnonkeccddc)yij«snnnnnnnnnnonkfccddagĪĬ|nnnnnnnnonkfccddcbxĩĤŮnnnnoonlieccddbcnĬ¢½ưooomkifdcdddbg|Įľ¼ƲskheddddcberįįƵjaabbehp}ıĵ̷ǽ̱º̖̺˾ƛ̽Ǿ̯ÿ̞ğĿ̘ȥ|}̽˥}ò̤z}nX[p~tnuwnklr~}TZVA^t\RS_|꺉õƆu쵆cUqjxlgjq{XNJDDNgcXõŁzkV}j~unlkoqwx{}~~}{cGDHTf|Vnŵƺnth~{wtrrpmooqrtryZ\հƵƍm~qLZ禝ƵƤmuzD[ǸǜƵÓqpmGk俤Ƶƾvl`Bx~Ƶƽ՗um~XI˒ړƵƷzkmunUOڎƵƫulld\V]晽yƵƥzlyY~SStƵƚoyeUlSKƵƗ{ljeUU[VJ茶ƵƝ^O`}NX|QXV֟ƵƝжH`MYlKZjPSfGͫ֕ƵƘÒԸu@zxuOPV[M\XVH_Ƶƞݶd¨{aAsnq|YPOZ{PO_uOXV|ƵƮtѻƳiUEoV|uoOSOadOOd_RRz|ƵwөamqPH\aSvVQSOlUQOj}PXAӔֈƵƉҚٷdS^QJKywM\ukOSPMqnJRPqaPUSƵƼ~ϕ׷ϼdKqSRQ?qsXM_suzQQZk{VCRPyPURkƵƚϗnɍТócKToOSUJ[P5PSSSSVGljƵp~RDp|wxiҼ͋xxΌexϦgĻVSSFn}NrlBkiMPqZPSSSTM:bqRGBUSSSSU]zƵWdNIpzdQMcL̶ܽqȇ~`}¿OZOSN[QekE`mOKlnOSSSSU?HySS=OSSSSUGzƵƝiUG]aVPRIYckoYnn]mkkmXiv;[iOSSLV\eJWsPH~|cRRSSSTK<\_RIDUSSSSTY␺ƵjvPC{}\wQK[=}yu]MPPHnNO;PPPRLhmmonDH~yPSTFt\U_OMzRGrZv`PSSSST?KnqOTAPSSSSUIܯƵYaMMk`eOPY>tzZQSORjPN/SSSSSjmikgM=rVQSK_sbWNFD/(,)$FU_dMHaj_rPSSSSUHAXSTI?USSSSVT΂ƵƝzWH`[iYIc=Tm}TRSKYaSH0VSSO`zq_i`S9zr~_MA1&=AKVairWEJF2P\O@'$2t~RrXQSSSSR#'18wmOTUBYS?+<=YYdePSSSSUBIVhOUHCUSSSSWPzƵƹldOHvNyqEeJ;czPSTFeXW>:XSTMnsrGhSV??m2?IESUDorOSSJJaQB|xN8MlSlRSSSSTK?PlvPSS=YSUKqstI|jPUI2]?DSRTERvOSSPAjR=^S7JMgYQSSSSS=PVSRUDGUSSSSYIܯƵrmQB_`PMfGEVsK;?O`kjFaK+&&*fSYBCYSUIwtsYklOTNApk`eqXQSMBxPSSU<}sRDIG%IaPSSSSU?IPqZPSOi}RM:M >SSSSSTIAS[ePSU?JTSSSSWHυƵc~QQ8TTkb?N2 bT~vPSTCoS8WQUSLXSXDrsgWpOSTV[}UkOSW5d|QRSU@RVR4|vH,,PSSSSP>SP|mOSTJAUSSSSSYU娛ƵƖyjSC<PWdO?6FSywPSTE}}@3O]SVSYBpvdUqOSVNwsS|uOSX;J~RRSTI<[T7gSX3&PSSST?OPdvPSSS=QSSSSSUNƵdWTA_|OXd`:=&DNRrwPSSG~vBIl\SSYBkwi]sOSWKVYk~PRWCARRSSP6dS>Q^P`19SSSUCJSUQSSUAHUSSSSSXG܈Ƶƛ~lSNpzQKcGG0JHLRkvPSSHsRIqbPTYCixlfvOSXJ]`\TRVHDzSRSSV?epPI>kN\VAOSSTHDUOuSRSUD8VSSSSSSShxƵƵn|WFwyKQX?UKTIBSduPSTBysSGsuJUYCfxqs~yOSWJcfQ~ZQULT]SRSSXJU|PR4yOVYRPSSTMAUP`TRSSP3RSSSSSSUCzƵu[G}ƑxI^M^QSTJoyUAEXXEcsvj~PSTSrtsMmfPUMeMTRSSXFd[T>G[PTSSSSSS>RSQ\VRSSUG,SSSSSSSTP\ƵƜxeE}phU;o`PSTNMh`PiURU?d{VAKVYDnk{b~PTP]RzNaiPUNgbzVQSSXFOfPK6eOSSSSSSU?OSSSSSSSSU52YSSSSSSUHtƵƜ{]JfleGUSSSSSTPYƵƳlZ`fbyUQSSSSSSPOSSUSTM;ZAOXJlZY}PYCc^PUSUIu\ZPSTP^`eOUMYgOSSSSSUCJTSSSSSSSUH7ƌDUTSSSSUMmƵƘjgctXPSSSSSULpnZPSSSP?v[BgPP\ZY|PXFodOSSWFlZPSUJjV}PYDYrOSSSSSUAIUSSSSSSSTP3ƜKOUSSSSVK}Ƶgk]s\PSSSSSVLRfOSSSVC_\AEKyft[yPVP^gOSSXDr[PSVEbZTGn^|QSSSSSU;IUSSSSSSSTS1ƺfGUUSSSXCƵhlWudOSSSSSWJ[|PSSSXJVaAKMhf[vQOjmjOSSYEb\PSXCVrPSS\URSSSSV9BUSSSSSSSSU.ƌKLUTSSXGƵmiQxnOSSSSSXDb~ZPSSYI_e@nDMi]nTFkmOSSXJY[PSXJ_SX@^YQSSSSY2;VSSSSSSSSV/ƹxGOXTSVEƵ{hK}zPSSSSSYAPiOSSXKwmBF9h[fVGhqOSSS\XYQTQbgkRG}[_PSSSSY/6XSSSSSSSSV3ƲvFOYVVFƵƍPQUQSSSSY@[|PSSXL[uCf?lVaS[vrOSUJyaVQVFUTSVdfOSSSSX/4YSSSSSSSSU6{ƺKIX[HƵƾ`K_PSSSSXBvhYQSXJ^G|EyU_FiuOSXFsTQYG^rT@UoOSSSSV04YSSSSSSSTRDpfLMCƵƶwhmOSSSSVCd|dPSXLpPtdqV^JwKASLSSRdql`Ig~ivPSSSUN89XSSSSSSSUOaߔƼa?ƵƱe}PRSSSVHt[qOSVPV_eT;QQswk6GK_xgjEL=be[FR}QSSSVC=>VSSSSSSSVEċƿƵƃXQSSSUNfUzPSUZsgcYeqWGeSAJ>jlJNPTRCmg}SRSSY9D@VSSSSSSSX;tƵƭofOSSSSSV_STPjZbKzh|Oy¾fgQYLmKbD2+SZPGA=;;;M8QTSSSSSSZ3X̄ƵƦjVPSSYN' 6Ɔϻ~u0I`kmdP/R#XURY1S5XSSSSSSVJ2qqƵƃ~qOSSXD Bdj^wyo[9:\\~TVD=7?VSSSSSTX.BㇾƵk[PSUNy4  |"uVfx`=%4%%}_ZySYBWQTSSSSSY8;RܰƵe}PRSXSy$;nR}<  2?UB?f}y_<D_>B ꗄWuUJ`>XSSSSSXC@3yҀƵfkOSWDľt/èЪd?Acm2+OgofN/k\cꕅYqUDqEVSSSSVG=F5zƵo\OTSsǡӻZF%+>EA02|䄇UnCkRRSSSSVIUH0ܯƵƤa}SSOqvQ ĿʬĻb&io@sCVSSX>;RU:cτƵ{huPXGܳ m텇SaaYNTSXF5SUDCtƵƫponPSi JؾCÂRKIVTY^xƵư[fujUےl~3g3D{ƵƛzONRs0ΜᑸƵƥtJݝ<[ќۯƵƷ޺qÇ[5ԠͅƵ|Ʋ̝u/֡xƵ`½g>nנ~ƵUýſi:Wmr`@3Oנ⑷ƵgżCSMGILF?ءܰƵrɿžř:SSSUM;ؤ˃ƵjʾƾCPSSTP:tئvƵW˷ƿ_BSSSS;bتxƵcŽýo=SSSU>[ج⌼ƵƆƾþeGSSSVBI٫ݨƵƌĿƪƱZASSSVD@ثʼnƵƜৰў̪ѷ}RNSSSUH<ث{ƵƠݩ൜صO:SSSSUK;ث{ƵƞѦX;NSSSSUN:uثƵƞŎܹ[>KSSSSSTP:lجƵƞØ|N;LSSSSSSTQ:pثڹƵƟ’Ŝ\>BPSSSSSSSSS;kثтƵƢΛcC;ISSSSSSSSSST<^ثyƵƣݽzX>=JSSSSSSSSSSSSU>U٫zƵƥֵ|dH=@LSSSSSSSSSSSSSSU?P٪⌺ƵƦ߼۸dMAI7KSSSSSSSSSSSSSSSSVBKڠ㧞ƵƧѪDEAABDPY_ƾƵƧҩIVSO1ixzuksdjQOY`^]ao\ESSSSSSSSSSSSSSUJKO(-MRSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSVcƵƹÃޖMHλji/B7>KSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSYUƵƑ[gނӽkB@l<46ALSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSXLƵƙ|oǜg볈WE??OSSSSSSSSSSSSSSSSSSSSSSSSSSSVHƵƾulrۧɢq#9ӊfTGFGLRSSSSSSSSSSSSSSSSSSSSSUKƵƞyc訂`}uȄiݟjf զ{VKHIPSSSSSSSSSSSSSSSSSStƵëg*NŇvSxUXhg|{ +ɩ嶑mYGJSSSSSSSSSSSSSSSSSXWƵƧէiC /GZa`XQPL*$!GLqֈ 1//////2!)SSSSSSSSSSSSSSSSSYLƵƃʺdDDB&$yf_  *OxW [-)0///////-SSSSSSSSSSSSSSSSSXHƵwYRKFa,; 6Y꼜 A/////////3GSSSSSSSSSSSSSSSSVJƵƇh)jb!XoTb͘ !*0///////1%3SSSSSSSSSSSSSSSSTPƵƮ8 i Do%@_{ʧџ! )z(3/////////(SSSSSSSSSSSSSSSSSViƵƖj4 caN//-PT~{sSSSSSSSSSSSUJƵpƷ \ut06@іÙ:///6GN#ThoSSSSSSSSSTOƵƽƫnۓ\p -ĩ֦Ȝέ\*//z%uSSSSSSSSSSUkƵf9999d999ƣ~ǒtg999WԼԫ׮ڄ&2/k999jX999cSSSSSSSSSSWWƵf9999Ne999ƕ給xkq~yx999IʕҞՏ'3/p999JV999HSSSSSSSSSSYOƵk99999h999ƻ٦|yy}9998/ ۲ѝԪ13/q999OT999gSSSSSSSSSSXHƵm99999;l999Ƭ999x'= ײԡλN//s999O0RSSSSSSSSSSSXGƵm999999Rl999Ԧՙ999)A Ԭ•t'2s999O/,2HDUSSSSSSSSSSSSVGƵm999b999l999۾999ػǺƦ3r999O/bSSgSTƵm999?99\=99;c999O/LUUUSMUUUJ99@cƽ\=9RwUUUwƵm999҆999Tl999\999999999RZ999~999K999999`999999999RΨi9999999999JM99999999v999OP999O[999J9999999cƿ99999999v999aƵm999O999l999<9999C`F99999999G999999N999=9999CaH99999f99999K4-42//q999O/R999SU999<}SSYM999uʈ999u999mƵm999x999^l999999pt999Q99G99z99;999999999x999ǁP999999999999U2/////999O/R999SU999SSSYM999op999Ɣ999mƵm999L999o999999999999M99`99k99?ע999999999999999999999999f//////]999O/R999SU999SSSYL999o\999Ƥ999mƵm999:99?t999999}99999999@99m?99tޠz999999999e9999X̳999}999]//////l999O/R999SU999SSSYN999ou999ƒ999mƵm999v999d{999999dp999L99k99w99=999ܝx999999j999999NŞ999~999M//////999O/R999SU999SSSYL999oɕ999h999mƵm999K999X999F99CL99=99C[99\99d99;؛x999J99ET99=K999齵}999A99;/0//0/zI999O/R999SU999SSSYM999oˠ999^Ʒ:999mƵm999:999999d999999e99999>99999gҘ999r999999g999Sg999u999P////L~9999O/R999SU999SSSYN999oͿM999ƸI9999mƵk999u999999<99@E99:99999u99999י999=99@G99:I99999999999t99999P/N999SQ999SSSXR999oƶ9999߶F99999mo999mӳ99999999Ƶh999J99999}9999pĩ{:99989999J9999ε~99999999999999999999sĩ|:999r999`ѸN99999k9999T{M99:999O/w999Sw999SSSX\999k{999999999>999rv999g99999999Ƶc99999999`99999999999iV999;d^eea999X]Ox9999999999999999n99999999999i@999999999`999|X999999999;R999JZ999v_999SSSVf999gD999999K999}u999^99999999ƵZ999e9999C9999999Aƽ1999aѸ999v^g9999999999999999F9999999AQ999999I999m;999999<999SV999L^999SSTPz999]跒nh999u999b99999999Ƶø縸yaKaƠ=ӤnPgxjBDDJZSSUJe999ƵƵƆ::ٜ//JָXSUSSSWIƨ^Ћ;99GƵm*0/»q˾W//////////10////2-VSSSSSSSTSSYJx:9J˺y9999Ƶn-22{y/0//////0////////////1&RSSSSSSSSSSXUƯK9999>alG99999jƵƞ1D}˿A//////////////////0/ OTSSSSSSSSSSo[9999999999D8;L RGBUniwersalny profil RGBdescGeneric RGB ProfileGeneric RGB ProfileXYZ Zus4XYZ RXYZ tM=XYZ (6curvtextCopyright 2007 Apple Inc., all rights reserved.sf32 B&lLAYERSȒr1^1Now Loading...d'91535B6F2-E8E5-4153-B3A7-0B1C95F1727D-850-00000C448808E8B5 z@txV.{ bCA{ `{I?齘$1$7ӌIowy}o{~̼́yw?sۼ%7W֭|*5+<Ћ|RiM}fNBmO*U+=7c)Y'eS\:7Z#EeXg+oC/Ȓ[еo[?/ۚ9*iW7/c)W}M[M_'2Zcy;5~{q||b]׾g?w/>hiPVN")Ǝo~ç+ߨKjX*䷭g5v{|%7E{L32jGl ^_+1Om #K7#z0T+^/e6/qkbhiM]/Ӭޣ5J=B=ߎKHjoVnӒL -EG{L3B7F^-? _W7.nN; {VgV'FKӁ e5'!5'-;x܂γ3.>!yjcKHJ˨NI-jd7}RN:).>~x9sRt^9ym>c-9̪~So\_u靻GogBBz^?U29=trN]4sTUWV|I~Ñq}[k /yy󳆙 D<99S/~+)_rK+'&e6=0Y}5ώm_?{W(":_O[75wmxwI -%IȮ[^.}zo/~٣iv^ɺqU5{gQSY5C !+ 3|O_0a0_v^O[Q{kǧ-qTqRrN;IΨk襡~\F3eꙻʺwjf= /9o-ŏs ,'qf~̞x6_b,E{$לzY$䭸/\%+CK38:=#ۿOvmG?4o}?O(5k7 +'ƙǏ{LG?wLswPc: rtSc1=s,~YCTN=ᬓzmJja}[cꁟKM%rLz<f!æΫ!W7+(쐃܏$%VzO;yY Aˊo>۳ϗ}86y)/Ʃ/IfoV(/iï\^\#˺U˶H>8Gn2k|Տy7>>1UdP5׽苒_V|쯰d2^ܿL}l..&nx3zU AJX2sx_wDnvTX:J;co:oC^mل_NG%gN9{hنfAJNɫagi?X8wodeXeT$~%Yy_n.|+%_6/Աو_l0,9FFn'*y~U|  7Hyם't_șg7UFJV+J 57*(8HjHS(jsRrvYw F9 tУYĶHWT~o*ju>W']?F?B]r Ĺ12?)7BO&&nι]\3g6>}f~&헙_r/Ծ2ޯ;[ i:M}J~$o'7vi%-%}S>=Xklwrdfݮ؎QPGL~]?v#glmҿPoKJ}!k<"N.zR'$& mX5^[ 8Nj?-++ndCD_r9sy k`$ _*|oDfBGu)'Oë=gnwB9:x^<<9~FA7HY#­| Ncwy5sw;m7},nS/y|VqG!u{F(i{k;{wlSPx-w̎Ux L5chs+8~Fc? @Һw%g|%['AJAl9zڿFA?qs֯W<^ |kk.O?W:Č`_M_"ɩu:zl7cnqm#sA8\wo]P;*t>7_9+1 Yx w"Oc#3Oy՟α5jb;|Ş0[2ufw]C{ g㜩oƸ;YKU.뺜/pg^c`owpBbFS]{#Gߟa7HYz?|׸ٟΥ9:6/k. ꒫BȂ/2g-6~u< ~rJ?ǭ>1 r>˶nwN -D=Hۍעd#Wkbg[n!2G/g,؍BiE}p*|iǷ>-kB7 zpajkNm}Dk·)R0l+9#ۑ3~V߹'<Ǎݢ-r@?$BP'=Jl]|S:,-r^wfoye5Kilv߻|PBwB:?U35'uGA~1Q섏]JI&s1Q%| #G}`YkߊۉQb4=?OP3;>wgcwmBq넄h=F1x $wYrnIJέxc9HNɯ|co_E{Lo M streamtyped@NSMutableDictionary NSDictionaryNSObjecti:#-Now Loading... コピーd'9F5547121-1986-412D-B105-82239712D16E-850-00000C848ED4E1F5L@x ԶXc<9P"Se@*Q1(s(2ej2dnCQtd*Lu뾟{=_{pk_='p88Sᐪ:nlvc:Rµ g)7Qvႅ3ێKY0)l=ۆ*rrBs_͵ rF+ZX.^B\\oN*2*m c 7% ORru¦۾[(, S\XaS-E￸ U±æ?¦۷ 䄼Ha ~w)¦۾O,_{ _cAa n6 }FA_;.F;baG O/|' ҿT> } /L¦۾͘/;. n6`V΂6"; ʯ\*wZOs5Fneed/qF< y_kG(Ի9sn>_ϟe~9Nk 3Sg|jU⣛jRPf^=?׏sVWH)t\_簈+ZƳ97k/G#BxswލK>%2_Q啅A' Sݔ4_ ȴ*o/H5WQ/? / ϊ)~wA].A:sa̫o7rٱ{.ZpFW'\G .UT}͙CFJ]ئ#cm|xDA~o ?(R[ ʳ`/:s=Rsڝ`}3 ?p4|P=/IƩU1oAo2&7.i?<~AS ns ~+Oo $6 0lڹ z_!: {li{ ʮ[H]̉| >)Uu[7r1}B͗r<C;!ϑĵF.wKd|z?u.(GwR8y>(kOae\neS2%i2,}L*8׺YSp9ENAٳ;8L[ I^>u_i{Oٰ؉VطɊ9sk}F_sM>q0Hx yg lE1|M w[{>RNwUKyʼn[yc{,x/+G/Dgb^?tY&u-rg_-$m}z##.+>iG{Gop:C<|qHGi}gs Kޅ1?26gVFVAt]>6uP3g7wNpR:^W-̷gIZg yj!yoh>Ǝ^'G^?{"Vm;!v} Hte?C:~)ܰ16z+?V~?35HY7u*8#7i;9،V#rˇlQf;f|)/k_^M;)G|tt%ىSZ(xy]#~{:y?޳_l]M+lo>slZm6pW c#-/s}<yG_v>|ѵ"|ҋbwSvFwY^W`{6xa7r%XG'S;6y•ӯ̥XU- z!y|b0ܗ-9c3{_򷅿7Ӕ][~ K}g dƺO 9LSج_'J;{ [+sV|q@u>Mj:;oN[q e gj?GObrf0@ĵ dgĿqWob:߉_H\3ٓȬe9ک͑圭׵}aWxutYo\HOʟhYPo'?~U il_~'yR6_ks\$~>G37,K4oU].Ly2Wx,M_alHycA^]B3'dK'3̛7=N]9qn#Qg^G6oHY,J :*o,|v~?ם#$dz<u걟$6S_ ?삌ke9& ;&!8u5wȻ~kF; lKjsÍ{eT+;ROG';Ƴv*1 'rwW]{ciؑmgPHL8:Ss/tW YpVtׂ=m qP}Ȟ/9CH1dw7{09Ml}?3?=Nst~/>BҸF H9kW=y#h_Խ[l9iK[i'f7 ({/ l՚iDnɟ}K^D/mem=z̽:,sgMۋŸܓv2mkR/5ӓ&{-l~`[k=5s;5N/[oHlgX^k}b9qH-˕>mU{a㕽O@w42A21?>Ⱥys# {_B%;&zov?q^uT]|%/TrO$N^bīJgw4V:߬?'8E TلU|7{1.MA\gـtOl־)3to0>K]ds*߬GWI'q.8ygbI3>'Nwd#^co]Vsۆ>|VdKte[Іk ? =Gp| SP3)@ )pܼ_MݭXBǢ3uD7_S>q 먽[O1kPxƈx3ՏiW;]>92G^[fKYj?yߊcA#=];ۿ90>uQ?W.#kbI^?#w;DoSmmǻSm|0ueN?UO?9u 8ף,Y55'4%'7k e.}eG)w?xkXī4}Mr>+«~coo3?3ٲfIsBkg)evY6s&X4WزgDS[x ؾ$_$mY?h dξXU-I$3Y+1i'#/~v!1|;>}C & 8ya/E=ć;lqw6ι~ 9oa6# qS hM streamtyped@NSMutableDictionary NSDictionaryNSObjecti+名称未設定 1d'9DFD4605F-2882-4E44-A3EC-8C08D90F55F6-850-00000BFF954CD5BEDxw\U?77$7}{oMcDP4{JQ*"(EQT6:XfޒZ}<99|>s{lkL-V۞Қ(!'Ź.=Tj#*wDx5v"xOKңįB*GwC%cWOno@4d`oy6˕'m=(%Ǩ<(}Z?R}}kQڞ̴1U4 ҐnfM{0xauk,Z }g< OjSx8W")q2+) þ4߫3;:C'8ƾ"k-y /Ҍs… |yoy .>STVV lav@60%!;s~YWkqyR.gH9_J X>SZ_F/[U$oC'% Uh4x쉆}a l}`p7  ˑ| Iy@s1%b [B a['\(V q'|{ 9ɣȹrqj1]Lio( [t- 1Xi̗/55Zxq,bHf0.^WݻGxOWaYI0"Q0%C*> gWʧe ~Gp5?15;&ǸO=ϽUv©, >Sż3WK{ۿdI}~6Y@a VR XjZ.[a%|cDtlOvh{qQz^f9PL;ۑio7Ͻ{%tCub5`]{(8F[[x }|' HMM}$P j2oL~l-w:Y~HKK ܹ3}s2Fɢ{`QtV`9bgg3|wat~l A<:3+"dӫ6w=6?8qs RWBy6nݺgٳطoO?}}DT hX\U Hj3Cg`3#~Uܒ8CLq* +56'mߤ߉cQYJulIpf~㣏>RyIcݾ}׮]cFI|_g\VV}F'1'c~Xkߍ+ȿ ? ;K?!?YŞJGR6yðdn@]_>;["8bۿJ>U3tKζT׌oGxB$g_AoA!䏐_"(pEus7TɈ[ǔ)IQo%VhCyۋ$vӏ??>;>㘪~.\DJcU_5юH>Pf'&S3qr8/(fXB\ MGAyyBܒ~rKrKZMïv[?%GFy0x*5~IyhIn9-gŒlc;>=Ç0b~4z,oSg3>#ſb3~8_u (o ar%:q7ǴCq}ʅ!Q(G9,NMïβ!<.IJxW(:+%E^ye#,RܡePmsLFyQf z]]]įzn~=Vђ(?O~$2@tY@W$]ُ}yӝپHvt,&t}XQ]9G fd?rx>ʕ+D__>jR~sZ[[ͯXߋ34zl,iQ]N?~!"phއs_VѸa O s/\dhD/4{W4pfKRTېg =sC񪠠猥\w|dgSSS3-_ ιW4  ;Z~O8v޾ۗ1p".w!\-bJGfP={ _4'=N ʴkǸB M0Xc-BsQ~tm  y2 +sN:ݻwF0?X6ϹsHkU=9#Lïo ؿ ?C \߸ӷF0t߾M#{ET'*vhjxbCs<8%2zL:>UEz?$MF\Ru/dp2ʧ r3ێ۵qAɁqs=sK |C0 f N&? ;̭-q71%fkykgqQ =vz?*. tܷ2Y8/ ixg,2Z 0'ǩ$KmHyɘ(Ŷ {H?w6–@k[ -I^GCG3ӨhV1gwn&Q.rzF?/ߗ34R"ٯ ؊d&z޻.'p0Ό1ٱq`d_ˈ]4^wd0è' -۾?C`b4J%0,a,T K]h.M]&K"a:bI}1gnLyh>j7giؙGn-9 vys8z\=##Qhr3P,K8Sc4 vyH'R(k:Ÿ>3 q)=硱}10AXGJ%>]k`Nz!R^b&Vu~h=H9-wIܦ(E(O׃b6B[G3̙3ܳp;SCdyc~=VU^\|熜K}l+إhH]szu;߿iC']&i3U{ v yGԏyf2:Mxhl4رؒ-bNS-CJcA YC܅s׆k zk-{I97s~/IS/?-յ&ȗq8(Q~ʪ{X1 >\385^*g ߻Pvv'^BܡBt^(bS1ϼu 'oCǍ܁"y6nGĉ$_VQ-\<~8&%EhbCF9ij.T"7*8 ʷ>pŵJt\ H7f#&fN#ι`$'. mJy$&2 F,^n))xwù`ρ v|7  u񁬽$ݕyl7,SR?]2VKn+{ͧb-eΎgxdcA(~ ŗ9юi~ҁ'{,Fny=0w q[|W7.aޒvn˽XږXnCn:>S/CV{c~=7a#쇓w>ɡ8n>os :S@FYݛ:fFv^uC<,vx#o]G~IGYa!6mBKB<_'M# xTf%r?URw_&iUֺ铽FL=qkvv .ճN,Dh|1o.q-]p(F`k,k #ME|~Ek,cGhݷ6zB.&OSf// qg#XBZZ E}(ܾ~>v#)G1  \UbjS`'h'UFx" >'_FHb$w7&C$8 imXa)i3*K,r.L+uiĵc{rƘ_3o]|r &Ưu'j瀩2=1֧<5ԇ-H HgeFt`[ו~E6%Ւ @$砢i/^;`/;'}(ߣdIA<0kHf 5|_:t͐6X3̧%ZMCS!9`*pd zM˰j܏Td =<]>ζvcP뵞 V}9cLïm"]rئ2كc!-H{1Ngئ|^͹ GTV"voLʌ/AVhۻG IlW81d,h>T}2$-uV0+8ejdbu}`ja sr?znESޓ'$ Uqu9T!"~Z;s|vQPؚC6އbas?f˱Oi083Y?k7qh/:x'Ź7gi5"yo4$`:%bh#G!sFO!1fwA^ш-J/G9wN>yf 9֟|G;?N+p>"\a퍚# 8r޽̵? ٸoC☟JlN@dA2Dzn]@׍A4_F/ z ڑo WUІI2J84e9Zkn\\}F ~l;Y} y·>t G:]dGaOa~X+ p=Zhf Ů\;4)/5Y̏DT^ 0?۩Ql~O}{mg&"K?ZRE4^a t2˶Mk-Wj.51oM=;;'&_{'7*tClt) Rz\?qE95O,rHGfȥܶ~|=!qqLذҚy`JLh7`{+pHhhܒlpa~XssGT.;8 hƌ,Ɲ)CX[6|kW;QvH{'`3q[y1V(.Ga1-2ߑ,|̹>zY!S_Q gX֡D+Wv_?>dY_"x<# 8Fgycpߌݣ?Zqoi/Ԇ}Yw$H}R¹mc)cאC,75<9@q}pg'K{U{ 6Vb$ASU,bǷj&q 񚵇4f`cp=GXtzS(nCySۣR՗ZQw{y}о7ΏS\.{ /"15BW}뿻)'%_հFY{tL<Y(r/9*|Gs<ɧ+嵗3P(H OtMd}(?=B69ۇDٔv\FO_g\}$q/H#!%RHro?Nrl# /N ;S0 f+º^y.~k.S]33(P0e\~M v )Sp"Q<暿RgbND8]v  ABփεZ;9&Ï.\5тk!i(mDBw R^9:`ajyR7$=CZC9k`TNHq6kR9TDz[ȱe$-%*j0#M99X9՞S*MXj^emo:P~Q ?1&Zv-_3v;rOfYE>S-~}^GU]AFHy[uog߈>Ƿ4wG^/Ph#fO`\x]kN $g`&xܗSe+rn0PރHږklx8B:A-u޵2OYJ1}XT=?E1J:duըֈK8,u<̤RGjː{TSǭ36c6sfߏ806&uU4ed7hOfe)kiHxO`'I=HI5O%İg`LK:f.\=(ޑ8E*͋ω{d~:t,ΕG{ X#qq)Y3+-'C,z%/{3˯lmǦc!k8ҵBRM"yKVdC_k$ 6iN8Mkl_í'=jWn~8#{_,i}и".I1U#F\r&FD̢=9A_GOٳbA4ֳ9!4O_Jf+ų r>/^9 ޑpz8ܞE V5qA 7Ċ疾^ ߢkF/R@p2 XCg3Ӎm v|k+^e=GLU|iY04>L̘}H+ )i@'{&?cpvUvkƙ}2g_ i3IrO Ɨw ,S=EXRh̓^{]&0hL E>)Eŋy\A#P7/F}lx}7fk-q3_;S1d߼iq_ܲxE0|eU10Y|>P#h Z8<9eCAb<ѕuJ F޶@:7I[{1NGJQ=2%nU_3ʚtVWYp]}_-ńUos8vc.h{iυM&[8qO"Ճ}++.d^xa^]La[ p. C#⸜u7Su{\|PF Wa]G6QL_zGɘc1vҒkbzFzAyW/0\#6~}>V6"Ukţ۩c %&V=VkFY2Gqߡ3nž|_i9qF$ws0t'dzi~sZ^R ȷ[}#Y^ShlM!ZI?x1fc'_up-*}+ObJrR[Q[>ͻ.0Z-8GyF>2>]ܬQJy{JCe&_m?y/ ^^*/+[?҂9Oޏ鼉p=BM^)R=VkXvAȸZq(8QT'+,<{6֡q"|HYZrh[QN(Ű?>:COOq10)~H331_k~ߪ41r(Mi}L8Л,oc3bl\!?m0f.IdOKi){3PZ/D7&Ao5=dT!G<}qqNԱزH~/HX 7`_$G1˝=_aYC,50Yÿ1m<1rJE#KΙ,Dv>R%[`SHҜ"4Bt\In`bM'`_8)k9H +2kP;TG㚌/'k1@ y0Kpx4/˓\MZ͵I@PI˔I_1 Z]{y^/' {\Ja}WǢyS0˄8:'EZHKp)4GD~|b⁛BLևNhṃil|._=֚SV^^HG@F[_3#yls)_8W{*LESIիc _-5-m/0~I9NJ}xtYU\,޺sl]`d%/k&ĦFQ.jsKD\ߓ͏ұ&!?"/c<3]Y~IhhJgߜ$x=^|q7mұmv&xn+<1>PY rR$>"=7#~:]y AzSa~rz+> e p2x='F9W@!{v죬؄6N+4)#P:AۄcN^{af'ch3v8s_`\1,8>G\-1V,#a,E('ג߷\KnwmR(%W*4E?ut1qΉ?^>$]EJf7:c#DɏW/Q/iIkG =y}KVQ;(NCZr3Q5|uT4(>M^f ,x9R/C|bc--UDk+7g?|j3I _9'*xM;5nlW)#R ȈB{HZSSYjl{|57c6Eli::W,Y'NG.3tb?28??W* S};L/YtS&A4.LcڀaC+b9<:դh^y9BmLh,\A&g<#h+>J%~zDtZfEίe2;$p=S`gr^H=8w ٗ؜wu8~X쌴t,lnwɯ=Rtն$cXI5Qt 3=dݥZ"Z[g>dtͤϤ{Rﯿ}AݻppÓjwgi8^L~aJqLN*Y3Ygxq/Zoj%]"TNI`8:H]/=Gi#5|THcqZcjoa~هd&'>!{.,[/jQQh۫X_mP/0_ςd~T+*t\s<ǰ~,V)cvkl6c֊e)<~BE?z'KXG}˗Vr)n}R\5_1 v=^SnfU`($u4mƑTiQ=/Cqеq/ԃz&FoF0˾d(K-2EBjX+,ۈȤAfgp [1\RRa+!f#+߫,7ѫJkk?iQcBl 0{J; q5l)C0psZ)wmuXcd8B't\륵9\5Ä^[m6~ViX'] ,mQ}~m|,TCgH/X jg#bRCV5ؙȾ<}%Fb͒ߢzZҜ6"*4*:uK72CWle eωK[],/k5ވH++r 4% ,:Y,31I$'bVl/ū9 m]v `sm8/z* }ֱC@crAQ[U5vu E#Vќ|^kqM FLđEzVdp~V$6)[\+dnG8K6 ) s̲=Be@ұcϳrk{ͯ7( f)ÂcYb[<7 ..=)ӬZU_9zYhS[&a'vjj~}ygkyv&O [l2qBY1rJ & kC^R}!q:=ufg/Itc&L}/kHJ#+)"NEkFM[@Or^X> Ir Ml?2 fYE9E㐴]HGC=רUTϩra53b'Gi\zd =yKg.݀3r}~ ˌ6WnX&5.['C W~-BfϷ9\H=E7$\×zņ<k٧wfvb8uaVm~X_z|0=_2}ܒJ=rq(ǂ;NQbaD~Xr81/I}܄RQIsj ɂO'h0R8>5]_5W\ ǬH_߷F\6+午:]eڻqN钊k(>vSKdz^J:ie.pfm&)J:'zo9+KܕHu. 9`$|M:gڂtgDq^SlFo_@ӭ*؆ Zn1ǸF\YG~~׌58Yܑ탍n[^`P/l63(_풹_aobib*ms[E2Pw=bN}@┽Wm:3cCT!fVww^{Rh#" { %{tlUݸse{m \Y/%_ɏK #N뇱|3-pOa[+zI&J׋h/r.QgcO| y!TkNQYHd~ƒct"9 ?f|>W!=KĶ2ҩGbH`nI+,aŰNAZ1F]3x[Y0:`+*jXY5XG1;>cY7Jo׎r_=l"X阥1+ >.2yc9y#}3gaֺů :k/j_; ah{fvLïkqi. 1iVaHyQpEy!YX|/ = {@ه1ǹ^o KYDZ*$; V6%%gk~'YCsOTս[u)E8C^A>i;S;?>kH/,}Ho/燘ykdBO3^‹kMS?Z1w'#pZT5wWc4BߐV f/I='˛#Uȴ Fb]1fN zwXq,4ߦqNf)[ǃr-V7Y@J2K?^鬋*סkgǹ\hȤzm|5ʶ Vy`;b?-.m:/h;T'ũ$EbJǫ1:?Xdr7#mPG[Wk%פGʽ/S.{eZՂcj#+D\XblfkYGX'y I*j&Oh2dIG h6Q$΋>]<$~5`+4As v-dfIxE<NqS%{/@ْ6؂Cvలյdmo"$z v:F[uFs2Is|ys`ht쟊&vqɻ\JVYĎ4Y)J}ҤDZzWroCd=„hf+'N#5:oTˑܒS_+,Y ujRk~q62eHż I@ bwR! )nmhKOqy1:lz;yG96\,zi9YƫrK{ٌP.>)̜JtTfwGe,ǥ/6njUkKz.M^_(qAރvtŘoD}h[{uO,6n-$n^p GHL*&{cآ3Z:`;tlY0OV"< e5P4EDB%vfq=#K [9 4vU ߙ]rHDbVH<.cش݁uےZsSWèǡl]Gx#.udL//0t? W{i5mH# {3>4UEδvg?}Kl1vĆMX`"-̙Va0w:,_a6[a-S_濈0[bVmc}EWJ|X*q\ډ!Ą5Ʀ0 fY=bdyì33K[Ӓ#˸#^0ӆOH1\@ei|EYpMh]j3cAFYwqJ'3y׀yS粶udoGط;~?txդNQ=Ͱh-{3FOMjfi5-1Lտ~9>'mWbbů7 9i+ Ÿ/-WLDFpTbwc BRf/+r-Ut,̑Sˬtlu~`'Ȭ2|Sl sQ: yxޛ׌Ώan?53~j?}|FR k X H;[M0*;!xuR9 nF{8p2i+v*9]J|1fT}}w~Wl mÂ7ݏ"-=~g:--׿O#Oc/c߿o}4U*##| ܹv-\~/^OW{lJa$,bVA/cċkT;>xڮuaSk=g{oƛ_֦r>;î]gbdllZ3}x~8D48_9/XkޅTcJ>fm`5z'+Z݌6 +;]i^*F(*8炾`kkr$fIsM>;8?!f^Ԝ M$VMƩW>`;sssWen냿?ܔgX0yE6M`a]g~'~|6˿ۿA:?daikkO r~mkI=(>l6_뗄9U*To'ΛE&Q8;ѦceF?ekQ(h0CaܲSwe|abF=??,^]8r\&Jb)U|o>`C9ɾŬ(07ݎ}v7Ƕt^p;~?O~cGQGO;GNNO?ewW_Y]dF/bF[W,N&"MF->ff*K;1`%S}7cMïlb]yq|% & }Pc @c,Xe5x^M@1(&ؿ' NxȬ:]Sn|#rFF>wů 9zs6r!W t+<۲?ɟ>(1죏>k0b;Bbt[#U0mx# }PyI9g2h^%.ua_~)ڿ}k~H3klfϱFq04?ƽ\L-zmyW@*Rk0grػBщ%o9Š6`'Ƒ(!<.gڼRŨ7[w3g='_;o=?`a9~+Ɲ5?υZ:{Wl|} Kbq}2/}@➨*I1۪ε Xhc_^1*Ev?|C?4z,zY9pqk܏W(YynCg{uŠnػ_XU{ ./İWéK7>1scѓb+al;Bş4?Vsh9׈ '~#+DYE{%~}g̰wFZ>:?q(ZIs2cmBz+|#Ȫ*'ٵc4zl=ooi%?zS]ӸC]G}^)4Lc케-۬8=r_Y8ULio}[oœ}6~` U.K؍P`<ϿW`Wo$U 6 +܁c#石)dtw8s/%aR6<sO<ֹz˹_m.o>:%BlTk~MïvqW`Xx r$ǍnR=-tS X}CbՁ'|+9$VMd2k7}*"o|o^\oCMv#? ^7 x5Y[jͭCoZlk*BW?8wJ_ ;BbY"{a8gh#FmП;r45߼RV] r rd,v 5%,U}6#Mïk]p-K4Ԗk(Sc֨ KIWϠȩRd{}>Gxr[fW|O?lǿA} lwupuz:Q]Pͪ#xvӻwVK=f㩧b^gEUWwJyNeܻp6UΌ4cĮWs=$,%mb`M-T8#MïkMG>O~< Gao6:N]Sɨy Ų$u $.=g̚8<ɺ#>wkFsNµ~3U6K=8چ~6aسMlsg䠔Gȯ{?ǽc xgZguI, MxصT/-\#Ϸ_}l]U.)p vS8#Mï9})p(bZFǠ̗*9Jc=Zs'O,Yyx`ScޟfcճK%zi%Zxyi'XO?pkb/Uۅβ܎A\D:k=86g-5r h>Ȯt 85eZ ׿4_Hv5hw;8+ 6IY5y~8M7bde.X߻p\~Cwv8q2/`W?Z:NݧGqқzO|G8o5T/D&+,1[xcOc.p>Nߓrncv7ٴwZGӈ4K34X;'/Np* fFuY#wUJUãQחaSiSjDwdF%,J"@KƶnbM<qAai?Hyq7]n,]Vپ5݆o?l5UJ̱/Y|o> 4*Α6rc^I1+0.{v""zrxП"*鿰t!n]=(K?R Lh6`/734ZNG j1Xog--h>l3o/+"{*nI̒)MI_)a%F' ;?]dA16H˼&<'ErcS&Q/h4:fF_X+AIe-n׽g_VkoBmK7sil_!3i_V`t9uQkaX?^}`>bN]WמѼb}dB똻B8Q͝ȍI@s#kXJߥcU2 F Pⱥ q.(sWdX1=L} "#hC?s8u}~N;wctGDE. ]|$+nzRo'kw4qw'F wϝ̐Oӝsc"3f}ι瞋K]XEs< qw.)*/xA^ly_^Ү""mY-pl.;:U\EbƱb(S36n@lܮkBo?1r*<3i$ëK=xHL*4whW  aXLj/֗aɇ/Co#ͻNnW [fX5#0 6]PT+c*1l` I=V]$8W)eVbޮ]f!~^=Vԡ&JѮx?VS[Fۈڙ,U܍,8u]9:GUmpe9"( zb܇Ua_^RՓ$+ab+5ƱmdV ePP%2E Ws*YNg,~ \V0'(96BjȿQ켥H~="~8qV2Wx\;(>,"~2¾gs܅XĵDķ#y5b6zpmDL1L;#RN/b>-oMbv&$zx2ܧSEloV0* £0Zz0ʑS&n~}q~Es_ށ-5G˅J{S(ZԵ+bHca.?\M(}|'|C9k2P]zdƈl`1n~~R9"kgH¯V9Q9l #Z0W/Acȧ Eĕ/?k"_)Z ɭ PPZ9 bGQ+?.5 m۸f <|E"EwZka|5R?) ~BIPK^ *h;łxu,3e.!/ _k"f /*k b.ڙǼ™E9{R# %{Q ز 3ׯӞo-.nvD .=݂sFZ 2._!vCk|s42UدI! ji!1ɨϳxEO k]ܗ0vn1$6rʯ 2)MEp [%kNx4CIT'n9 ٥We2Qƴk~Y56FALwv \ d6n~Q?#JΚbv&$zeagg m8bCɿk▰vo=H=l. *' , ;ݟXSɹE*UTg-AYR' xEu%ˠc҈Sx!,صn@8Kv)~X; EʍJƵ]/7Ρ.ܭuBNh3)$C-/h n9S F0΀A*q~ׯvun8y-.>a0c>$!ΏSWX8xqٸrfcR A>4E"~+QZFWυԪC5Iݵh^@Vp,SNx":[;nGV@t?~X_w8!/%C-h;z74~ۯwcq~[̃)ka킽G .r R*QT*R<{ 8ص`j|x+uYu4`ʺ?ϯxGQ?2Tt+ j#;'ߜj[1aϬ}Du W27(Uw͸[m'k6Rc%:k׹H 䪂V4(h4UWndzMWnEb~. j#-#m섹%u j:W-cPlAm-uLZ+ p^x/B(j@èlj'/$b~l)Zthg׾s ōUuCFcYu?~ ~¨Tٹ0B׏%C-DG$&'9c}CcDgޝwۨdFܼ[븬6:TZU{ ׺jibL_ vKvޏԀHIuòF}q~8WITJ(/VڵظaSsşbap iY̏ 7x݃pOP9c1c\|.-5}L`Zb VmX !'. T5`Wk۹#3h"kH+(,xXQYȨcÍf 99aR,?Bj| li'mp p6˞ÎX,j c=֯źu6$퀽qF! Z XUI/6/\O]sao3vUKxWxI-ʠ!{V ЕbաMD~kw6!NꝅދU¯Zι> r 0d7^iYA[sr4Wa>6mĚ56KvMwxa֭-EAI2ԍKO="j!?3$ r R%-[46f(a\nX"҂Q\P$8*8,w;} g+db{Ք`jmcc#hLS&,6æxk刓̥%C-aW[[Cp[Aw[Vg\(ӁضÕ3km/֮ںj4lsS[jٔCèʺ0~A@|"oݢ_6?cڲXRs1c|i!fo[ʽ1pψrqXMRںkruj+fm^ p_^~=Ԣnܸ?jPڻwa,woEfWzp1)5 )oܘ#ʑՌ˥*FtZBa^ CRh0UuJ;oY B|bHZlظ/،YK`e%cK¯ZA8"J8- : 8}UNivΟ&gau{Up/~߲UBZon[3 b>&$h9;lz]AT դsza 6uJA(>cK/*^S  2ņgFbˋ͝m c.bb'$h18.~uw 7wHgz?s3!eaU\{3/ru4xKIqޒ<I39 (dj^~I4KgkhjC@ P6Tis8!Zg>a1[༥TMo} qnËŮϜ_^אݎfxʥ P7KQ5h`mi&)'/7'ƒI#Xk$I%0z{%zLT0 gvRI'+A}k`P湊˯Y˶ ʯWzRn|~ݰ3,{CT6Sf!CNjI%HQݷcZg4a!oo3,eW߳h*߁;ڲѰ7vT+eKp8+˂yKxD,#FܢP`q5BNjI%H 7nľ{a S''8{!-8}rsVnB[z/:E+y/'ZKPhXoͰv-ŷ?s1w*^O=qhdI~/ԒK p$l nAHXLEj{@WvE1X QykH@H|3kzEc#PPP{?aYid;{qQx7ٿ*x #akd1f[7oz5{ j+`oj +gwEc^y1-gRJ¯R&2 mqP(;3+sdffzFyy9bccE߾փͫ7'^ƔϟBΜЄf7ank`q?l5y|8Z:LOBbfTq'\P\sHMn# 68zJ F3T֡,2+scc]>ev끕@uGл[uH>׮vy../pX lesدI) X&BGO)a1fQ&_FA\X $d' O n {߼ أ`owʃ(9FxjAIKE `%8 CZD=ʼn}%y}^|Zk򞃾q0r:u6|:KjX _~>; +-8kFk$f -xV\Ř5 b3o=AyD-sG#K8m0_Ó>)ip=sX1g!Lla Cy ůR3/>zt끕_M4͈ 5֚P;KE- >/dPPQߊNĞ#ؾaWy/ÓiSHI-3<_BMy9|BZx7qM7vJil+!*s_ޤFVȋLE{ l4 o4>:QGf\,JC'ƘtL5~MM:Iꬹg.Cmm8T$\gBq}5ۖ#N2K¯V:Jb! D|R6b3q7jmSEk~yp t>y%vpeflPΆwX.\pu=S}*8sdPԊ+Ŏ!/9f32gARb"#"` ՐK>f?ab&$z e W xv\.Gl%$eUP$kwh_3^=WW Gǧ9 N`758녴M.Zs#c+# +XX + : -(JDK|-#'c@h|Yw^)Wͫ92}9~mM*I@QE$-_[+mbR'"31HైET0*#2+9˨LU[o~"i«Xp?dNXfcrkpGDRRr"_Uԉ܃kBFp3akib4~H[ĤjE+i_Gtr6]N.s YmP)5 }>p 1ǐ_x#ܛNm5$'Pky^:015EJv!V}4$J`g`ԷER_TE2,YqRR{pQ3Z g%P* Uþa%-8 jhHy"li`19c[xlGq_.kKNFa~A?65T!7&VDk[>;[=[6x|?O?*?ԫW^z,#/ (Թr鲘'u>zeP.I ,T&$\jåN_:_Z}L1f%:ꇞ=E&JZ'Q9<޻_5]wc7^8uBEm]*G$_茎+NyiOcrN'cG%Myf0e5 go?!߄ }ql~l]K_ԣGS8z6aPoDZ6+b.iqhcȓx2k\kh_|]qbRǿfXAgx_D%з,1'x#4 Ime}9Fٽ1'9GTEii"vip3?^g1~_?!]}6Ư0s[}Dۖ Rܤ_yMZo2_Wm'!H^&ۯν}qvlop3FCn)*{{XƧ#7*.˂uǾkX1.U%!͜ūc:%W"-U yxfV FĤ"5/Bz2w'DALz?ܼLә!^st}jsOӰw~g-ذ'.CI*P+g)hAӤ̗7 y[یRHY JdP8XX YAqPwx7kh>j8~0{ X#~qدI! {釘#!dBjUe_H ,}39@,;{<+O?ǭAl׿ۛw?>(IS~Xe+'3c~8)sjY8r rJ̧erNɇJ(\*n##E: ,3޴z0RĞ;> #7iMW|/>>ܚ9xY@31¡fX$VUFm~6w&2:Duc[ [a}oOŵKW@fFq&jxPe ?wWC:;OKf5HƯ. {;?}٣gVICCe3~+^PXY]dDEE ?yf '5YIH @/il wc;y'9x\#ݏp5By|ր3hmK^H)7jʫhADR\`c`_g"%=_T"?ۃef>^iRϱg zz$Eu@5[Z>ix~+嵓C0"""{!(r駞#H0޾yT\ԧ[L21zR|vI$bL?vl{0|B# W8z*zPTՁ5a~Rjk; 9 <T^ߡ3cm_k0a-+>z _w e_z uC% %Eg;)dR[ #֮0U҄>bQT\66)w+8Wơ-;Q6t/-a.['Tgg_r_;:R+u}1Fytdef!//eeez_FGc10RтY%>e ̼ڏXe5~ݰ`\gq د?K¯FaWّg.shϤګ,nAAo'Cy+1tiS~Vl5 ~Yp]CGE=.~ *ۇ$|UBGxOFbg *4uQ{Cai\FF`4R}#8(fLu ~kATCyND ^F jչRo0m\ٹhk^}~?ItL|,d wmhᴲRol"X|GQyMDBF> ph:TOk3Wmݑ \kq'&&_+.ĝ!fٲSLŗQcEbXad*yt>5vV#tvZvD"y-}),Gw#\PCaL:.8>r ISWǯ~K0m< ǃљ+n5)vIuH/Д͞8*w<3Sսb>K<ǜpV孠 %-/;&pL}')Iw ݗ_+{H^W{ ѩ\Y<KAq{]Ưˁ6 ֯O+fAqlbD'$o!4(GڊbDFNm01 Μ׀6 < yƒAȌ4绻c8oydPb㏢#خu U(Mx#9!qqfcY_?ZiR_PE~T: {!)6y\~>fo_6.Fqdzk($o$ٰAmu!sw׹/"V*-瑨0Uf"eGCO>{#b\f yWЀAYaĩx@[p_&gzVdqWɎuWçcc̛kݵ~]U~7~l?מFVZ"=#G|wzu^zhA^isfg}{}X9ˡw8q/"^|^?4}!#>Cg4&ớ"665y/(+^Cǜ8ftFץX%}gWk[+lFU@׌/Uuy/>nAS^9j/r៊G%\Bq\&jrz0~&_+ʁ fk@dǧ ZgD$_sFȁ} 3s>oOMfc0 ~]U~S r}:-T:$>OŏskAb9I745&>(UWRُ_(zMl`kkck'@vX"dWGSWߏ7{kg?sx>klԶ`mb6* &@^]A4zn噃 eE1ժ05"&}Hi3EWWs5481~m^Gg`՘á}wv`?_qܿ^צX%}(ȫ׍p5CEV#] K'8陣,҄{bMTwdl22bKP/=>1y #ƏקaoUO}t)VIu_ȵ*s{AQ"'/fnV(K~w dȽ UoߎkV $ 4AFF"#k]c g6*3OuxAί&& ~y9:QNu H)|~Ӧg&Fw>×s~#kدMJ¯BrVj:tlvTd ¯Xk"֝ 4Xr:ߟZׄWGߺysRE}@=Kx/ g] vNo ITG[{G#&?aռ&b_~RW i&cT8_T7'^qTZ_Ҟ&dq,=e)F}Q9Uڎ!~(TI+k3:M)8^ODX8nzk)r0)1Nv8W@VZ Y=%FzyƯks`į[$/t؀uϨ+tLE,f-(c}kЭM?Gߧ4Z}4eKSy-hkTOfzز844b=mWAO7'O8`^Mad`!]i/?p:iٲK\*/)VIu_!k2G3˯ܰw(3+4A^$gUVʉ=F /[eyܰsb|Ƒ~8%sPӎuJӀFx6ޫq%xy/րIY~'򫉀6) 3s~򃍦"*h ;mji@A\ȋn =ō=b@eVhY0c1ʯrVw1u?7c[T{J_K*χ?Ipg~7-ce%PTxB^< xO!B[e=*Rs!8(*M/\Q3fVP+:܏ݓ_o(J='~*Hį@0hkHtV-1_ٍN\.Ĥ"0Z/+/Sjl{/`1Oz 5[a*}#}_ |N^/)x 6I@FFJ 6P8o;SG8;\&Ӳ% mm_K'xn62r׾S˥b=ZӜuLZ?H8iљ9<Ưr :ފ&MbުXuZv:"?_b_-\|#Ν<;B<m]~H3Bz 6Xcfkl6ǚU:شA۶crH?[ƶ0/rvy輴Ke,l#G澋URׇ_)¥R2 ׀ZXVк z&埩! ~}6~3=6aql=/?!WXk2>?@HԱvFVbp?vrg̉y469b&lv H,@gs5zFgKߛI rd3ѷYw46BAU+ʚ󫠗_1~e^Ev7#֮0b2º:~󽷤0cup=)VIu_2ŎJm'ZAes?ۑQ3joRI'ⳮ ep.k1 abS]L,Age~!s&Wo 5`l(_P-W]~]&~]Gp=*9tKqt_C}D+1cZٱ>b6* [Zzk)n؋~6gsR0;UԉN擯@Tj;b3N 0CQ~O G\F!{q WWXB <)Y SݧSs?_{G|hE?޲^b6* >L?*8~NmY_)x>yu0ֆVZ=kE^fq3ᣵITϑr{޳HnGxR BaVmVq|O%O?zI~&@O۫vDץEH~Ŵŋ5=R;Sg| UE޼s_į.H^bǤmIoEXD7ѧ6:!o=!`W_g*,&f % ˯>2`0.~L5YO>2?׎_g޶Ab&-DK0(h¥ %=]BiCoǁEOw;NְeJ qbgD*9._nFQ+W wDà2qݣ^nڹ{`Z(`ckۦ#;.afw甶/08!~_OΪ u@d|D㽄"d_v_.Hq;s&R)u50?n,PB~ #{Շgn~Y3~嗹%Ok˭"n[e`,?!3ts#KQko[دA~M>*":y^~G#^v,vx/◣5mL[f΄* aF_c俼܍{1vo޽'~?[@ܠk%Ɖ vn^,+ܳ*{}֮892$%o=8h;2@/!,9LՑ ~L=M`Z E/aދǎ1=`/AL76dc泶0f{Y E^nK>'*]C8{q+YY痩`\;g1ƪۤpb (K@hU9Yuj ?j5x?i0~MASKSPREVIEW'+xw\U?77$7}{oMcDP4{JQ*"(EQT6:XfޒZ}<99|>s{lkL-V۞Қ(!'Ź.=Tj#*wDx5v"xOKңįB*GwC%cWOno@4d`oy6˕'m=(%Ǩ<(}Z?R}}kQڞ̴1U4 ҐnfM{0xauk,Z }g< OjSx8W")q2+) þ4߫3;:C'8ƾ"k-y /Ҍs… |yoy .>STVV lav@60%!;s~YWkqyR.gH9_J X>SZ_F/[U$oC'% Uh4x쉆}a l}`p7  ˑ| Iy@s1%b [B a['\(V q'|{ 9ɣȹrqj1]Lio( [t- 1Xi̗/55Zxq,bHf0.^WݻGxOWaYI0"Q0%C*> gWʧe ~Gp5?15;&ǸO=ϽUv©, >Sż3WK{ۿdI}~6Y@a VR XjZ.[a%|cDtlOvh{qQz^f9PL;ۑio7Ͻ{%tCub5`]{(8F[[x }|' HMM}$P j2oL~l-w:Y~HKK ܹ3}s2Fɢ{`QtV`9bgg3|wat~l A<:3+"dӫ6w=6?8qs RWBy6nݺgٳطoO?}}DT hX\U Hj3Cg`3#~Uܒ8CLq* +56'mߤ߉cQYJulIpf~㣏>RyIcݾ}׮]cFI|_g\VV}F'1'c~Xkߍ+ȿ ? ;K?!?YŞJGR6yðdn@]_>;["8bۿJ>U3tKζT׌oGxB$g_AoA!䏐_"(pEus7TɈ[ǔ)IQo%VhCyۋ$vӏ??>;>㘪~.\DJcU_5юH>Pf'&S3qr8/(fXB\ MGAyyBܒ~rKrKZMïv[?%GFy0x*5~IyhIn9-gŒlc;>=Ç0b~4z,oSg3>#ſb3~8_u (o ar%:q7ǴCq}ʅ!Q(G9,NMïβ!<.IJxW(:+%E^ye#,RܡePmsLFyQf z]]]įzn~=Vђ(?O~$2@tY@W$]ُ}yӝپHvt,&t}XQ]9G fd?rx>ʕ+D__>jR~sZ[[ͯXߋ34zl,iQ]N?~!"phއs_VѸa O s/\dhD/4{W4pfKRTېg =sC񪠠猥\w|dgSSS3-_ ιW4  ;Z~O8v޾ۗ1p".w!\-bJGfP={ _4'=N ʴkǸB M0Xc-BsQ~tm  y2 +sN:ݻwF0?X6ϹsHkU=9#Lïo ؿ ?C \߸ӷF0t߾M#{ET'*vhjxbCs<8%2zL:>UEz?$MF\Ru/dp2ʧ r3ێ۵qAɁqs=sK |C0 f N&? ;̭-q71%fkykgqQ =vz?*. tܷ2Y8/ ixg,2Z 0'ǩ$KmHyɘ(Ŷ {H?w6–@k[ -I^GCG3ӨhV1gwn&Q.rzF?/ߗ34R"ٯ ؊d&z޻.'p0Ό1ٱq`d_ˈ]4^wd0è' -۾?C`b4J%0,a,T K]h.M]&K"a:bI}1gnLyh>j7giؙGn-9 vys8z\=##Qhr3P,K8Sc4 vyH'R(k:Ÿ>3 q)=硱}10AXGJ%>]k`Nz!R^b&Vu~h=H9-wIܦ(E(O׃b6B[G3̙3ܳp;SCdyc~=VU^\|熜K}l+إhH]szu;߿iC']&i3U{ v yGԏyf2:Mxhl4رؒ-bNS-CJcA YC܅s׆k zk-{I97s~/IS/?-յ&ȗq8(Q~ʪ{X1 >\385^*g ߻Pvv'^BܡBt^(bS1ϼu 'oCǍ܁"y6nGĉ$_VQ-\<~8&%EhbCF9ij.T"7*8 ʷ>pŵJt\ H7f#&fN#ι`$'. mJy$&2 F,^n))xwù`ρ v|7  u񁬽$ݕyl7,SR?]2VKn+{ͧb-eΎgxdcA(~ ŗ9юi~ҁ'{,Fny=0w q[|W7.aޒvn˽XږXnCn:>S/CV{c~=7a#쇓w>ɡ8n>os :S@FYݛ:fFv^uC<,vx#o]G~IGYa!6mBKB<_'M# xTf%r?URw_&iUֺ铽FL=qkvv .ճN,Dh|1o.q-]p(F`k,k #ME|~Ek,cGhݷ6zB.&OSf// qg#XBZZ E}(ܾ~>v#)G1  \UbjS`'h'UFx" >'_FHb$w7&C$8 imXa)i3*K,r.L+uiĵc{rƘ_3o]|r &Ưu'j瀩2=1֧<5ԇ-H HgeFt`[ו~E6%Ւ @$砢i/^;`/;'}(ߣdIA<0kHf 5|_:t͐6X3̧%ZMCS!9`*pd zM˰j܏Td =<]>ζvcP뵞 V}9cLïm"]rئ2كc!-H{1Ngئ|^͹ GTV"voLʌ/AVhۻG IlW81d,h>T}2$-uV0+8ejdbu}`ja sr?znESޓ'$ Uqu9T!"~Z;s|vQPؚC6އbas?f˱Oi083Y?k7qh/:x'Ź7gi5"yo4$`:%bh#G!sFO!1fwA^ш-J/G9wN>yf 9֟|G;?N+p>"\a퍚# 8r޽̵? ٸoC☟JlN@dA2Dzn]@׍A4_F/ z ڑo WUІI2J84e9Zkn\\}F ~l;Y} y·>t G:]dGaOa~X+ p=Zhf Ů\;4)/5Y̏DT^ 0?۩Ql~O}{mg&"K?ZRE4^a t2˶Mk-Wj.51oM=;;'&_{'7*tClt) Rz\?qE95O,rHGfȥܶ~|=!qqLذҚy`JLh7`{+pHhhܒlpa~XssGT.;8 hƌ,Ɲ)CX[6|kW;QvH{'`3q[y1V(.Ga1-2ߑ,|̹>zY!S_Q gX֡D+Wv_?>dY_"x<# 8Fgycpߌݣ?Zqoi/Ԇ}Yw$H}R¹mc)cאC,75<9@q}pg'K{U{ 6Vb$ASU,bǷj&q 񚵇4f`cp=GXtzS(nCySۣR՗ZQw{y}о7ΏS\.{ /"15BW}뿻)'%_հFY{tL<Y(r/9*|Gs<ɧ+嵗3P(H OtMd}(?=B69ۇDٔv\FO_g\}$q/H#!%RHro?Nrl# /N ;S0 f+º^y.~k.S]33(P0e\~M v )Sp"Q<暿RgbND8]v  ABփεZ;9&Ï.\5тk!i(mDBw R^9:`ajyR7$=CZC9k`TNHq6kR9TDz[ȱe$-%*j0#M99X9՞S*MXj^emo:P~Q ?1&Zv-_3v;rOfYE>S-~}^GU]AFHy[uog߈>Ƿ4wG^/Ph#fO`\x]kN $g`&xܗSe+rn0PރHږklx8B:A-u޵2OYJ1}XT=?E1J:duըֈK8,u<̤RGjː{TSǭ36c6sfߏ806&uU4ed7hOfe)kiHxO`'I=HI5O%İg`LK:f.\=(ޑ8E*͋ω{d~:t,ΕG{ X#qq)Y3+-'C,z%/{3˯lmǦc!k8ҵBRM"yKVdC_k$ 6iN8Mkl_í'=jWn~8#{_,i}и".I1U#F\r&FD̢=9A_GOٳbA4ֳ9!4O_Jf+ų r>/^9 ޑpz8ܞE V5qA 7Ċ疾^ ߢkF/R@p2 XCg3Ӎm v|k+^e=GLU|iY04>L̘}H+ )i@'{&?cpvUvkƙ}2g_ i3IrO Ɨw ,S=EXRh̓^{]&0hL E>)Eŋy\A#P7/F}lx}7fk-q3_;S1d߼iq_ܲxE0|eU10Y|>P#h Z8<9eCAb<ѕuJ F޶@:7I[{1NGJQ=2%nU_3ʚtVWYp]}_-ńUos8vc.h{iυM&[8qO"Ճ}++.d^xa^]La[ p. C#⸜u7Su{\|PF Wa]G6QL_zGɘc1vҒkbzFzAyW/0\#6~}>V6"Ukţ۩c %&V=VkFY2Gqߡ3nž|_i9qF$ws0t'dzi~sZ^R ȷ[}#Y^ShlM!ZI?x1fc'_up-*}+ObJrR[Q[>ͻ.0Z-8GyF>2>]ܬQJy{JCe&_m?y/ ^^*/+[?҂9Oޏ鼉p=BM^)R=VkXvAȸZq(8QT'+,<{6֡q"|HYZrh[QN(Ű?>:COOq10)~H331_k~ߪ41r(Mi}L8Л,oc3bl\!?m0f.IdOKi){3PZ/D7&Ao5=dT!G<}qqNԱزH~/HX 7`_$G1˝=_aYC,50Yÿ1m<1rJE#KΙ,Dv>R%[`SHҜ"4Bt\In`bM'`_8)k9H +2kP;TG㚌/'k1@ y0Kpx4/˓\MZ͵I@PI˔I_1 Z]{y^/' {\Ja}WǢyS0˄8:'EZHKp)4GD~|b⁛BLևNhṃil|._=֚SV^^HG@F[_3#yls)_8W{*LESIիc _-5-m/0~I9NJ}xtYU\,޺sl]`d%/k&ĦFQ.jsKD\ߓ͏ұ&!?"/c<3]Y~IhhJgߜ$x=^|q7mұmv&xn+<1>PY rR$>"=7#~:]y AzSa~rz+> e p2x='F9W@!{v죬؄6N+4)#P:AۄcN^{af'ch3v8s_`\1,8>G\-1V,#a,E('ג߷\KnwmR(%W*4E?ut1qΉ?^>$]EJf7:c#DɏW/Q/iIkG =y}KVQ;(NCZr3Q5|uT4(>M^f ,x9R/C|bc--UDk+7g?|j3I _9'*xM;5nlW)#R ȈB{HZSSYjl{|57c6Eli::W,Y'NG.3tb?28??W* S};L/YtS&A4.LcڀaC+b9<:դh^y9BmLh,\A&g<#h+>J%~zDtZfEίe2;$p=S`gr^H=8w ٗ؜wu8~X쌴t,lnwɯ=Rtն$cXI5Qt 3=dݥZ"Z[g>dtͤϤ{Rﯿ}AݻppÓjwgi8^L~aJqLN*Y3Ygxq/Zoj%]"TNI`8:H]/=Gi#5|THcqZcjoa~هd&'>!{.,[/jQQh۫X_mP/0_ςd~T+*t\s<ǰ~,V)cvkl6c֊e)<~BE?z'KXG}˗Vr)n}R\5_1 v=^SnfU`($u4mƑTiQ=/Cqеq/ԃz&FoF0˾d(K-2EBjX+,ۈȤAfgp [1\RRa+!f#+߫,7ѫJkk?iQcBl 0{J; q5l)C0psZ)wmuXcd8B't\륵9\5Ä^[m6~ViX'] ,mQ}~m|,TCgH/X jg#bRCV5ؙȾ<}%Fb͒ߢzZҜ6"*4*:uK72CWle eωK[],/k5ވH++r 4% ,:Y,31I$'bVl/ū9 m]v `sm8/z* }ֱC@crAQ[U5vu E#Vќ|^kqM FLđEzVdp~V$6)[\+dnG8K6 ) s̲=Be@ұcϳrk{ͯ7( f)ÂcYb[<7 ..=)ӬZU_9zYhS[&a'vjj~}ygkyv&O [l2qBY1rJ & kC^R}!q:=ufg/Itc&L}/kHJ#+)"NEkFM[@Or^X> Ir Ml?2 fYE9E㐴]HGC=רUTϩra53b'Gi\zd =yKg.݀3r}~ ˌ6WnX&5.['C W~-BfϷ9\H=E7$\×zņ<k٧wfvb8uaVm~X_z|0=_2}ܒJ=rq(ǂ;NQbaD~Xr81/I}܄RQIsj ɂO'h0R8>5]_5W\ ǬH_߷F\6+午:]eڻqN钊k(>vSKdz^J:ie.pfm&)J:'zo9+KܕHu. 9`$|M:gڂtgDq^SlFo_@ӭ*؆ Zn1ǸF\YG~~׌58Yܑ탍n[^`P/l63(_풹_aobib*ms[E2Pw=bN}@┽Wm:3cCT!fVww^{Rh#" { %{tlUݸse{m \Y/%_ɏK #N뇱|3-pOa[+zI&J׋h/r.QgcO| y!TkNQYHd~ƒct"9 ?f|>W!=KĶ2ҩGbH`nI+,aŰNAZ1F]3x[Y0:`+*jXY5XG1;>cY7Jo׎r_=l"X阥1+ >.2yc9y#}3gaֺů :k/j_; ah{fvLïkqi. 1iVaHyQpEy!YX|/ = {@ه1ǹ^o KYDZ*$; V6%%gk~'YCsOTս[u)E8C^A>i;S;?>kH/,}Ho/燘ykdBO3^‹kMS?Z1w'#pZT5wWc4BߐV f/I='˛#Uȴ Fb]1fN zwXq,4ߦqNf)[ǃr-V7Y@J2K?^鬋*סkgǹ\hȤzm|5ʶ Vy`;b?-.m:/h;T'ũ$EbJǫ1:?Xdr7#mPG[Wk%פGʽ/S.{eZՂcj#+D\XblfkYGX'y I*j&Oh2dIG h6Q$΋>]<$~5`+4As v-dfIxE<NqS%{/@ْ6؂Cvలյdmo"$z v:F[uFs2Is|ys`ht쟊&vqɻ\JVYĎ4Y)J}ҤDZzWroCd=„hf+'N#5:oTˑܒS_+,Y ujRk~q62eHż I@ bwR! )nmhKOqy1:lz;yG96\,zi9YƫrK{ٌP.>)̜JtTfwGe,ǥ/6njUkKz.M^_(qAރvtŘoD}h[{uO,6n-$n^p GHL*&{cآ3Z:`;tlY0OV"< e5P4EDB%vfq=#K [9 4vU ߙ]rHDbVH<.cش݁uےZsSWèǡl]Gx#.udL//0t? W{i5mH# {3>4UEδvg?}Kl1vĆMX`"-̙Va0w:,_a6[a-S_濈0[bVmc}EWJ|X*q\ډ!Ą5Ʀ0 fY=bdyì33K[Ӓ#˸#^0ӆOH1\@ei|EYpMh]j3cAFYwqJ'3y׀yS粶udoGط;~?txդNQ=Ͱh-{3FOMjfi5-1Lտ~9>'mWbbů7 9i+ Ÿ/-WLDFpTbwc BRf/+r-Ut,̑Sˬtlu~`'Ȭ2|Sl sQ: yxޛ׌Ώan?53~j?}|FR k X H;[M0*;!xuR9 nF{8p2i+v*9]J|1fT}}w~Wl mÂ7ݏ"-=~g:--׿O#Oc/c߿o}4U*##| ܹv-\~/^OW{lJa$,bVA/cċkT;>xڮuaSk=g{oƛ_֦r>;î]gbdllZ3}x~8D48_9/XkޅTcJ>fm`5z'+Z݌6 +;]i^*F(*8炾`kkr$fIsM>;8?!f^Ԝ M$VMƩW>`;sssWen냿?ܔgX0yE6M`a]g~'~|6˿ۿA:?daikkO r~mkI=(>l6_뗄9U*To'ΛE&Q8;ѦceF?ekQ(h0CaܲSwe|abF=??,^]8r\&Jb)U|o>`C9ɾŬ(07ݎ}v7Ƕt^p;~?O~cGQGO;GNNO?ewW_Y]dF/bF[W,N&"MF->ff*K;1`%S}7cMïlb]yq|% & }Pc @c,Xe5x^M@1(&ؿ' NxȬ:]Sn|#rFF>wů 9zs6r!W t+<۲?ɟ>(1죏>k0b;Bbt[#U0mx# }PyI9g2h^%.ua_~)ڿ}k~H3klfϱFq04?ƽ\L-zmyW@*Rk0grػBщ%o9Š6`'Ƒ(!<.gڼRŨ7[w3g='_;o=?`a9~+Ɲ5?υZ:{Wl|} Kbq}2/}@➨*I1۪ε Xhc_^1*Ev?|C?4z,zY9pqk܏W(YynCg{uŠnػ_XU{ ./İWéK7>1scѓb+al;Bş4?Vsh9׈ '~#+DYE{%~}g̰wFZ>:?q(ZIs2cmBz+|#Ȫ*'ٵc4zl=ooi%?zS]ӸC]G}^)4Lc케-۬8=r_Y8ULio}[oœ}6~` U.K؍P`<ϿW`Wo$U 6 +܁c#石)dtw8s/%aR6<sO<ֹz˹_m.o>:%BlTk~MïvqW`Xx r$ǍnR=-tS X}CbՁ'|+9$VMd2k7}*"o|o^\oCMv#? ^7 x5Y[jͭCoZlk*BW?8wJ_ ;BbY"{a8gh#FmП;r45߼RV] r rd,v 5%,U}6#Mïk]p-K4Ԗk(Sc֨ KIWϠȩRd{}>Gxr[fW|O?lǿA} lwupuz:Q]Pͪ#xvӻwVK=f㩧b^gEUWwJyNeܻp6UΌ4cĮWs=$,%mb`M-T8#MïkMG>O~< Gao6:N]Sɨy Ų$u $.=g̚8<ɺ#>wkFsNµ~3U6K=8چ~6aسMlsg䠔Gȯ{?ǽc xgZguI, MxصT/-\#Ϸ_}l]U.)p vS8#Mï9})p(bZFǠ̗*9Jc=Zs'O,Yyx`ScޟfcճK%zi%Zxyi'XO?pkb/Uۅβ܎A\D:k=86g-5r h>Ȯt 85eZ ׿4_Hv5hw;8+ 6IY5y~8M7bde.X߻p\~Cwv8q2/`W?Z:NݧGqқzO|G8o5T/D&+,1[xcOc.p>Nߓrncv7ٴwZGӈ4K34X;'/Np* fFuY#wUJUãQחaSiSjDwdF%,J"@KƶnbM<qAai?Hyq7]n,]Vپ5݆o?l5UJ̱/Y|o> 4*Α6rc^I1+0.{v""zrxП"*鿰t!n]=(K?R Lh6`/734ZNG j1Xog--h>l3o/+"{*nI̒)MI_)a%F' ;?]dA16H˼&<'ErcS&Q/h4:fF_X+AIe-n׽g_VkoBmK7sil_!3i_V`t9uQkaX?^}`>bN]WמѼb}dB똻B8Q͝ȍI@s#kXJߥcU2 F Pⱥ q.(sWdX1=L} "#hC?s8u}~N;wctGDE. ]|$ Ut{wz%wq{+1bb51+& aq orn{+][ߞ~m1u(3CA}_R:~bk8pGc=`ў5vG̯hW?7*ɇ;Fdz|?Ac IdCG NnphcAlZ$Kb ?쿡{~Hx..V8αͰi`;ʡVƤ jİ6$Xah]Zcֶ嘳m5_-ƷktC/[c:x=ȇOy'NOܙ6"+~\m#)o|dg/ӷ ILocvoHB#;u0򑓽6}q~ "ľ/OV#ˉ镶 P% kWBEِ%~hlĦ70cQjRmֳYBV#?J/G}'SS?w ﷝ރv#-Zdz1,$">a#&1BZ*(~1镒D(ge\Z (C^ v_L⡓nzQkhSm#-a]A/gsػiSƽ| PW_۪AEzlAuYg@KD _Uc b/":™E>{R#Kbv14o6`~ڸVӞou)\D@AL*{E댴V < N_A6nfoh $);BdzEEJD}@g!. 8%}mljAl{౜'>8)6Xy0zO_M_34JJEzPQ.U*(a6e|v(vܺ߭k+!vE *}ݽ>#/' X<0ȇ%G);Bdz8̊Ϫt&Զr{ F$&ns'{ٹ3aj-}sxau`b]rN%QX7)=IT/^@Eى"^QĐEe1jąX80fk9Z %ʍHĭA1e94YWVp+2=);Bdz<ʎ|5Z7_و[w$udOvg@(oEȿ/녃{l]ĥm>L1r眕7g\0k"3AGu}y_j-QZWŢ5LJr$ʓ_ҼhΧxtuCZ] H,_w7^eXi %=ߍLLnp O_PcT?[K+SϘ[\W+"+DI PѭWYXxhݨg] Kgy'ΦQ\Y(gAEQvydEƯZLcl-bbb6t qL3KO$OaUu>D jb|*U|[ ]3n(6uUEw(Kutg+ǐ*UĠiLvhpcvC.E~S>\dz4)i)nCӽ8(ϕ,$}K0~2Q?D5~K1Mj%}Ihm5Tt Zۯ9{f݃]Czʭ_qpܼD좺̂IFMn1p ;{YV)-6 K@\Bܸ)f _?8zv.L?+d;k31sp0shn _K}昽b^߭Lħ EkЮ{M®ܽsg*K͕_/Gs;x("rq HL/+ U3shJEƯZl3:Wc# cKYCjPSׁ“(ϾHO#Q=Gjd܆TTu3} ,3bsUw3K[nflZyۖU.i ƥIW Óa[K^6Iylj~EĩLUOP*_vL594"c-v ,.)ALlYXRu5;Y?`%Xg+=Jp AJnKz>5׊j;SH:aYt*֯768q&K^}~H} f{k vqBf1}T<nqMP93ҟ0sTdz8XQYcfAh9cçĞ}&8} g/Ym;p Ni`+{@K^ƢE Xֈ7Zc+ljCqI!Z XS?n]k +#-2~=9ގO4zۻD WzD,؉vp]hњ 8:sfbu֜];w;Io(F:^UvU94 |7k-\:sL&Hb\mg +fa-f.1Y=37.O[c\w".=1>Eܲ녳w.X]؉ۗ»:tǴkD_oFhXبǥ< #C{#tĈH,ܾkva ˬYJOP1\ ?*FFQ/"STkJXBӨ_EP7ϢBm1lHQqfn^kŗ_ZVێٻ0bǴkq/ )򸦣+qVΪ]Is8s(6Q>+'W-^2VOoc ܋.B7V s(/us{׉xƭ窊_Kc2nkWF"x`S-Y 6vZ_2yrͷadی],a cاz =ơ} rױ>9 TGDb˟^޵%׉ 1ߚaai-;㟟oü=kx?ġh;^06deٻzvClzZ'8Ʀ0QсGuK;^c[~ 9m[@-̝*%~/Pb׹6n,""q<c4`@Nm:6}SNkK&  0GTo +#6;1z&XLz(G*jaٳ֩NsZ:w"5KĮ:XHuϋصF{'㳟f~=wP ršSYOkK& Z7u{M/M=vu-ϛ^9M`h_u}ؐ8ҙ˪ Z%;%Cgk0)huZW/rgFbvӻalረ<އrW~E/yľ]bٍk ''5x{x&/(ʛbqa|;k]Zy~5PTVd}rIvΟ6u8q{0ݩ!Gz0ޔ#ـ5_!sYt.>t|BN,]*$4]8rRc~u|'+2`iKK{hiaX~f^-Ĭ-y_ݷ SO[K&9h3*ob3f/n}1^uͻ pA!V.5Lz՟5J'$>T DTqf{W}nPȅt."v]@dv5o~O'1nhM;hAkaq)fo^9q~d"%kqM\7WƮ{[nn܁b(GLzKqR(R[R0d9<T#XdNflkvc}PJ*V1疨>٩8wA 'C+';o2F;wK>?ճ7Z:8p03x_]؁ O"c-ѽMs+!EEE"""oPv{> ~fsP8o^t\xYcwC6!1,*9 碡iTvs~} p Tʡ_ErZ(\| noFfh"^{x:~PG^^?X#*( rOaƺyۅE{VM<"c#TK uQ< m ]{{{Gsqqq<T<* /~6I=ξSƞ#8tz\HzI=e#+=kylfHn$]b00V8~!8p6n̎Dݷf=/}~O ٤5}nhAmqlkum} gm_GO"#)d<WqL4N]9 S|ΉFyy9%߹Տk7g^ D%r53;X2;G`.#\UˇW=υuD msWS~']U49/h~l?dFKe?}鎯bӪUcK1 C$%&}2ƴ~Ƨ^tuu,DIB&#x°$D9Bu;!."ٜFQyDƯGRNݸ*Wo~FXX$Wd_/>',~9r^EŜ_sWS°lef%ľsD o3-Ptc8S1Q rɊkUXr{jsO>ۼ/>{IǏ^:#Xw.vq -嵨)xe("4 EnpBΞ+jg>YWd |}`,?#e8Hq{F9Pcx?2|8$+ʺ%Cf,+^9;bvu? mjDr$%:w aɰviQZ#FNVV߆2֠x-(DsLz$'3q=2e6.b<kc8ILnFVچҹ {fgj_SƙpzXkDeBo঒&3+^gN;s81HS]nϩi)2~=7Z+]?80G*Սns <(ǫxsWʝ: Y̘sKuKW`[q]3-.|U+'8s?^|}GzUfW^|MuUNvSP%E[=4~?(^z^ۗ~iS=H%8z}tOF,Fq(m^|oV\={zঅXISX UJx ^qwz,]xOhb]b" KƳ*TgO[og;~_G"/?9iiix'-S/ _:ދorXב$k*#ڱ5iډ_P~QHWv&ܯLnr}fsCQvxg9>,Xy(o3ۙݨRD&eᆱ+N^tHN) 0V6'9so-۔\H*F&F3qiy012#'\&nO҂^#{5Ϊ߿ĺ_{6MfG&Mü _TGDݵ|>M;둖Wr%nqlMoYF1 k7ZZK˘:x}_颜2x#")wV5s!1:뙝Ŧ~nkз,,5aoEI'g:g4zZ:tN_O{K<,?g|/0ԓ/ap1 kz ~4DƯGV|j 5.܌`3`iK ̒l-˿={/ؾb *$xoEt@9b.4cjE Ͽk6CA#AMaCoraMcC^ PQ.b;<1QI[k+ -HWXiov^}E)sgq3}0~ɽ)SNdzd岩3̨ DZsS eF{+nDl]QPՄ^T`ncli{܄%v6k5x k[GWO J=t0|?s'nElx#(sxMm53Kh_]b!VԷvf;:"7` t#/1 ն7L`chʸ兄xD ӗWC\_^~cƯgcO`ɚeصwfm\3c*C|^M+Tg8Y5;:]L$MD ;Ts‘^m=]P-Ŧ]6O>O>sþNoj`L'EB'm1 ^Bc# +XX }G׀GWC+o3{~W_~Ka:9 sc_VcZ>Љ.EEȫu0zu=H !ͮMFH,♭sk|uv.n,NCYl&rB?">:m#y9z _FG$9mkxp/8KWamnl?-bR=rE+~Cdb6ӻ]Z7T3،v(a:酗jkB͠ *aքka =m̹l.Gt-CKcgKmۺ{އC\WI|W|fƮ3]+0{ y6mDƯGJ\xo0VrKhCԠaH|;RЮIp٬Scq__!`اttڴTe?,KZrI&%}I{@A^Y_z KU}#rƣ &弿iBN9'ԥ(=7)y]C6yzG:ZyVR1zii<~ۋrȝ+ʼ/xW|^"vm/7 h>U7L\6"#!vpE'~ _Ȏ{dΫU3+~ O<txZOk7Q5lCG=~Q/;"#!FG~e˨~KTӘQ m8lo=3[~[[oоf Ջ8vXM ɾԣ'ć ^<z#RSۤ9" ۡiLb,#L³!ZFu74zU˗=u}d'/x套uafo_ )sBdz$䔺lnZAGer7k^|ᏌWyWyY կ% $oY5DB%~߿ff6.)J:5m5nCϼj\&$dsIs~}g#mN9XEi))viz^[^E?|vu7ׇ bTG{9ͧ|M =mS^mL!\F/3/M$د~+Z!C= cSVd` cƛXb\ ʁػ}(݋B{l3'foۡެFy1H ૯6`je \RɄR*pY9CqE%N>| 7EGus;ز0T{ymzg";)H N?x!yc,> Wb"\VEycCEy Y0R Gʊy_W.~ Flt ?3cV/\g_5Sr05G?hGMby^۴zn;XANH/qn5~{{ZTE/_m6CY݀H%]R}Ñ^5g>~=mHC |lϏA2~-ص>soEƯrN}eO_]Z Ek|PxQNceu5w7_:WUPtdD' / pMk޴B;{q'/c3FQF>^1ظU数E|f$C6rzU",!>ҷB|u"8B}sO\Ÿi"~QwW/}pczG zzNܛry 9Xbd3ȼSك%HșBy@g5JZ\<X+"#..b #O̙{3O 5 rx.aSq%ʓsPR W%S;c <졵H1aػ[6 g@8` CSkhBQU PTք {;%cW[ cR9 S}YE:=w2KK˿s{"aO)|W)ZoTCBcRRc$y Es UuY=6~;`n+9gNV)f80]k,ߵ݅ pHcSS.K L$j|Oo<|qӢ~DcN8x%dRqm=(*~8/k=/rn*jZNm1]5`[謨Gy\&Z'dxH`]sq?VǑ|yOFۢ]|kk*4WS\lµ5{d0.i}9fE\W K9kxhQ.&=m!6."~' {]Aneuqϓî=XFLx|]gl?"8VEX;AHF^ 06uqQYi09f$"Qi?7O݈KˇREmj+}#Q[mc ~ qfռŸ%SW8Ak˾ H|1V$'s='3Wׇ.+scLK}לFDw*ˎLTѶXww ak3 {0}ᦩ=옍bYF˧t\R)ƙê<6NjAT *j:1Džk)>C'|"7wmu(KCNh<IgcY_d?ZhR f]SE~D*~f;&-+:*Q,]z -咑UҚa Fҭ>oɿ<";;VV<z U)܏D99$dWq|9 />to Eo{u|2>WWKg DGD<5ObW_?L-`{px|k>]1s"V!s):nl=m/ߨ_wD;QTڌb70}jwcŹcZU'K5UMx x L"ۇjn w !]w{[>XѨmKϥ>dKfv2R6%O@d;vن}=uYw%}SMQ)+Mv䗉e46[O e MvLZQ?rPC[]%Ɗن|"e硽5#~m_m_^Ԏk?/R\%p]Z`;}4jZsIܛ]oYD^! ScZ+',~aģNݔ2~ SyKc˷1;,;Yaq3ح,ޣZw`?\b +(5W~4-Ԭy@_Y<)YE*/{_Wv6;2ֵJLO;g7DOO?vԹH ٹñ\ UX9y9ycfOnߨʷW7b]0Hl\ZXcS7wc1,3Wԃ0ݴAؖv]N2~ ^,$#L_4 K,QFؼ?X|crx? .}p5Esh; ga;܋=_ YN(MY#گgyq쥦c^+үăr҆#Ӿbzi95|{af;=YWRPV23zl!75+.q ۫jҾ۷aO٣ V4h'Jdt_:s uwsnJ_Ax;)״WLryIH'l}lc  ; _ˢ5JC`9ߧ_H&jatӪ>23~G QՎ2]b0E׃{]?!oK-I~!9V7|qkЌdf?֗bz!-3AXd iTUskBQ4"_o kb5m4<3ʪv~]{{3n~ə"f/Xx&k0b6vԘM]Ư˯Ѽz -.#/wzS7ڴq].$I~=#'<~(Du%dCc.5QwK$*Zcw}44vqP75 93V!sm<g. |XFvOSޭkļkX9s Eyl4n:M`R"muMȈHPЅuF F,Ul']8u^1b yT}Z*^=Y9zER `\' )W/ScQM_ӗ_1=9+fhηǯڡϷn+^6ѣ^QY/ֱf1ےg<$ jkžˣΡ$y]o\߱^~y}o-oS;|Ob4?}XT{ws\RF'Ll9޹wnx<,~>a_b2),TUz"Q[[;s(B}#8Ȗ#'-qd!>~_*8r9Z 5D vm6<wQ@6Cp2*Noyy=5r/~>}zimo+6 &Cn'k J;z;dͰN8k)ZD iuɗ҃;6tdݝy/@#5⚡ERq ?㗧 CXE'ӌ%:Qld?6ey;MHuk4.TD o\s9#Rr4(!RD>Zmyyc)dE} My^2G`%#-&MM^/7P#PFzm}ᜢx?!G:VC-[;8^CvIeC/Jr&3lKd7oYMu-',iQPQу3޹O)ۏguÚ01t?_bXO~ecOITE}<ǛB_S/ '6%Z`G}UB]I_9mT}Mb~0a|Ui۳r) #k1-JqOܾ=ol|Pt.s5]9x,ylPjm~)N@k赜=)x-I)FrTʤ|aGUxoܤ#6攄F|`ǁVoFc]6ߟl#}G?E4֨on5/q:̄dUZs=u⢏`{r(!αK-dtGTcqRShGn?sW_mU:ݓ_s;|]t/4}!?ΑuQ5K'0QAGG}ĽOV+8aT4)N>)=[]=H C_J/jd.P+h}^6? WXS3IK94\-ض`=x͊?_ڱq}֭w;yPk~U0X!)݅VhG/ W%|Z> "cu>JѷEH }'mQL\:^\(GEнö#{1'u&3o/8ޟA=DC:E&hm`=N-SVRuұX! -O^ rKO=#()I%~u39/[pxmgOaRa'}sy6lÆ+H+e(_nlcK\_A1>kmH[ /?gA~c ZcQld ::&a=Cy} !{TΣWIf=zJ˒{ApcoO`+Bau^˯X m@iYNl\^4R?$.Zkb~Q2=oobs[ڇ_9c)W nMZWloK3)?/[O/(_1}#עN_׾B~O_OGrKλ.N9䛲w3Hyb|^'ɠla\4oG=?g#Giy똯1|C/Z_fbcsD!"< sOS|ݒ'X"(*jCӵ yot]+EO1R1U@dJبks]MTKtfڇӘ,ȝ3IΛ1 ) zu6dwKY~) E7dn?u67#i5m"a_nsܫ*a9Dܞp;51v_LrɗO#s׌;^0᳞wu*d0J1:^)ΣhoVu^H(\5=L$sLM*3\nZșͯ" n4=kujkE#3B5b)> RXؤb᪱k/+s6Ъ)%jH~ s |_ so1WuJJ>u4Nց\ryIs2 uQ5WI{O c.{STW}w푙"e_vVW<~x!Z+,zt!Q͢Dgkf'ꙐQ55>\-#Nظ;2f- 4hXk#΃0$ j%Kу'OQBÀl% `-# yg-_{f' e%"ӵ{mNyJ/G5B;N;}|Ɏl>}phti!?kƸѐD|-c@Fa+an e(dsx0F'Oyc&3]T9U>F~.ă^qNq ?ِIX|s=* /~81~XVH͸-Տ7 1n_FK.p}Kz"56a^^$]c)$}2T݄zՆvE;X`}|+L4t$ѼctFքHk[u$p-XŵKbb琘9>8-1o)rudԷ0ǹ/$Xu|m2&(̒n{k@Zr`T&(*]c3TQo=[9?j04MFo.4uNFK9#RL2"S h |qi;~T{k~ _f8/m⾵!ܸ;kM>wsM(\UjTvoz4r&wz25Hz<qIu7-OkƱ;Gڮ-\x_{{yJbXkKoi8Z:MݩByZk~m߇4%zxh uV+=jœi565c`07=;h{K7^Xun?vG=M- ToWs^^ flݱ#sۥ-k_F[1wW#20Wˠ .\7J/k{aBv(u!Sĥ:fi>U1۳3%v$Ư _eGU"eM8h_j/g9{95c!6c?mӗc;9MFhr&ڽ|f[̻PCqX8:)Ω1_أ9 4W;^iy6+//};so!OJSi3GOM쾝*dpΓG%v5Z:`<޾/<?F'ѸF=V.ˊ$~xOxk{xiX⿒G1MGw4t8\[">9͡ٸҶ-."ތ'Uz]~3!,<OWM׷l,>ԋs(_Hl\݌j'$`Y`AW` lb/9~]Ufů8ƯHY#O4"Zފ~uv1fsf?=;?[ܾ^ޟ! sߡғj˼bybGt.LDv7/a܇Q,9qW&!c34u} c>nXWQQ SU' t@ghZ߂d]$f4ØSχ_+ qW |jhr1kq~Y[]o~#ǫګ %PdE\DB[ gwKj 8׃Ř{a^}ƭw2~l羖yGƔClkNnkRmg{k_FckJ?N /NgԔɤEI&C3pPSjґBvZMԪTRLS@J3P=cq! N3Y,meU,[>Z"9I$$ʒ g@/v(mĮM~~nYbrz=^p0t"p$bSG#(ϬΰiLwvy_UMkI+~X$|5:tYJdiP+dRNFmU˙3P!34;B#79 4(*P;\q̭[}=﵂qk.,bڿ?GbG{4*RI񖫙3F.+s^_uZl<_/1QƇڱq=lJrz,徘NJ#,* 0+jt~J)~M,TCAec~8,,RSKW Jsj3gU&dA9Ᵽss9忄Q*16d|'c*y}.泖Z7/f'DlvB)*u8_z#!md&'e-'BU^bG;0~)Ґ&@Qrތ00~2 tړ

About the XML namespace

This schema document describes the XML namespace, in a form suitable for import by other schema documents.

See http://www.w3.org/XML/1998/namespace.html and http://www.w3.org/TR/REC-xml for information about this namespace.

Note that local names in this namespace are intended to be defined only by the World Wide Web Consortium or its subgroups. The names currently defined in this namespace are listed below. They should not be used with conflicting semantics by any Working Group, specification, or document instance.

See further below in this document for more information about how to refer to this schema document from your own XSD schema documents and about the namespace-versioning policy governing this schema document.