pax_global_header00006660000000000000000000000064144572150510014516gustar00rootroot0000000000000052 comment=a9a364306b99c480d1407f77f8a5fc9efc665a36 jdim-0.10.1/000077500000000000000000000000001445721505100125205ustar00rootroot00000000000000jdim-0.10.1/.clang-format000066400000000000000000000065741445721505100151070ustar00rootroot00000000000000--- Language: Cpp # BasedOnStyle: LLVM AccessModifierOffset: -2 AlignAfterOpenBracket: Align AlignConsecutiveAssignments: false AlignConsecutiveDeclarations: false AlignEscapedNewlines: DontAlign AlignOperands: true AlignTrailingComments: true AllowAllParametersOfDeclarationOnNextLine: true AllowShortBlocksOnASingleLine: false AllowShortCaseLabelsOnASingleLine: false AllowShortFunctionsOnASingleLine: Inline AllowShortIfStatementsOnASingleLine: true AllowShortLoopsOnASingleLine: false AlwaysBreakAfterDefinitionReturnType: None AlwaysBreakAfterReturnType: None AlwaysBreakBeforeMultilineStrings: false AlwaysBreakTemplateDeclarations: No BinPackArguments: true BinPackParameters: true BraceWrapping: AfterClass: true AfterControlStatement: false AfterEnum: true AfterFunction: true AfterNamespace: true AfterObjCDeclaration: false AfterStruct: true AfterUnion: true AfterExternBlock: false BeforeCatch: true BeforeElse: true IndentBraces: false SplitEmptyFunction: false SplitEmptyRecord: false SplitEmptyNamespace: false BreakBeforeBinaryOperators: NonAssignment BreakBeforeBraces: Custom BreakBeforeInheritanceComma: true BreakInheritanceList: BeforeComma BreakBeforeTernaryOperators: true BreakConstructorInitializersBeforeComma: false BreakConstructorInitializers: BeforeComma BreakAfterJavaFieldAnnotations: false BreakStringLiterals: false ColumnLimit: 0 CommentPragmas: '^ IWYU pragma:' CompactNamespaces: false ConstructorInitializerAllOnOneLineOrOnePerLine: false ConstructorInitializerIndentWidth: 4 ContinuationIndentWidth: 4 Cpp11BracedListStyle: false DerivePointerAlignment: false DisableFormat: false ExperimentalAutoDetectBinPacking: false FixNamespaceComments: false ForEachMacros: - foreach - Q_FOREACH - BOOST_FOREACH IncludeBlocks: Preserve IncludeCategories: - Regex: '^"(llvm|llvm-c|clang|clang-c)/' Priority: 2 - Regex: '^(<|"(gtest|gmock|isl|json)/)' Priority: 3 - Regex: '.*' Priority: 1 IncludeIsMainRegex: '(Test)?$' IndentCaseLabels: true IndentPPDirectives: None IndentWidth: 4 IndentWrappedFunctionNames: false JavaScriptQuotes: Leave JavaScriptWrapImports: true KeepEmptyLinesAtTheStartOfBlocks: true MacroBlockBegin: '' MacroBlockEnd: '' MaxEmptyLinesToKeep: 3 NamespaceIndentation: All ObjCBinPackProtocolList: Auto ObjCBlockIndentWidth: 2 ObjCSpaceAfterProperty: false ObjCSpaceBeforeProtocolList: true PenaltyBreakAssignment: 2 PenaltyBreakBeforeFirstCallParameter: 19 PenaltyBreakComment: 300 PenaltyBreakFirstLessLess: 120 PenaltyBreakString: 1000 PenaltyBreakTemplateDeclaration: 10 PenaltyExcessCharacter: 1000000 PenaltyReturnTypeOnItsOwnLine: 60 PointerAlignment: Left ReflowComments: true SortIncludes: true SortUsingDeclarations: true SpaceAfterCStyleCast: false SpaceAfterTemplateKeyword: true SpaceBeforeAssignmentOperators: true SpaceBeforeCpp11BracedList: false SpaceBeforeCtorInitializerColon: true SpaceBeforeInheritanceColon: true SpaceBeforeParens: Never SpaceBeforeRangeBasedForLoopColon: false SpaceInEmptyParentheses: false SpacesBeforeTrailingComments: 1 SpacesInAngles: true SpacesInContainerLiterals: true SpacesInCStyleCastParentheses: false SpacesInParentheses: true SpacesInSquareBrackets: true Standard: Cpp11 StatementMacros: - Q_UNUSED - QT_REQUIRE_VERSION TabWidth: 8 UseTab: Never ... jdim-0.10.1/.github/000077500000000000000000000000001445721505100140605ustar00rootroot00000000000000jdim-0.10.1/.github/ISSUE_TEMPLATE/000077500000000000000000000000001445721505100162435ustar00rootroot00000000000000jdim-0.10.1/.github/ISSUE_TEMPLATE/bug-report.md000066400000000000000000000017211445721505100206540ustar00rootroot00000000000000--- name: 不具合(バグ)の報告 about: 正常でない動作を見つけたらこちら title: '' labels: bug assignees: '' --- **バグの説明** **再現の方法** **やりたかったこと・期待する結果** **スクリーンショット** **動作環境** **追加の情報** jdim-0.10.1/.github/ISSUE_TEMPLATE/feature-request.md000066400000000000000000000006731445721505100217140ustar00rootroot00000000000000--- name: 機能の変更 about: 機能の追加や削除をしたいときはこちら title: '' labels: feature assignees: '' --- **背景や動機** **解決方法** **代替案** **追加の情報** jdim-0.10.1/.github/workflows/000077500000000000000000000000001445721505100161155ustar00rootroot00000000000000jdim-0.10.1/.github/workflows/ccpp.yml000066400000000000000000000163571445721505100176010ustar00rootroot00000000000000name: CI on: push: branches: - master pull_request: branches: - master jobs: ASan-22: runs-on: ubuntu-22.04 env: CC: gcc-11 CXX: g++-11 # Avoid crash for calling crypt_r() that is pointing invalid address. # https://github.com/JDimproved/JDim/issues/943 AVOID_CRASH: -Wl,--push-state,--no-as-needed -lcrypt -Wl,--pop-state steps: - uses: actions/checkout@v3 - name: dependencies installation run: | sudo apt update sudo apt install libgnutls28-dev libgtest-dev libgtkmm-3.0-dev meson zlib1g-dev g++-11 - name: meson setup builddir -Db_sanitize=address,undefined -Dbuildtype=debug -Dcpp_args="-D_DEBUG" -Dcpp_link_args="${AVOID_CRASH}" run: meson setup builddir -Db_sanitize=address,undefined -Dbuildtype=debug -Dcpp_args="-D_DEBUG" -Dcpp_link_args="${AVOID_CRASH}" # `compile` subcommand requires Meson 0.54 or later. - name: ninja -C builddir run: ninja -C builddir # Since Meson 0.57, `test` subcommand will only rebuild test program. - name: meson test -v -C builddir run: meson test -v -C builddir - name: ./builddir/src/jdim -V run: ./builddir/src/jdim -V compiler-20: runs-on: ubuntu-20.04 env: CC: ${{ matrix.sets.cc }} CXX: ${{ matrix.sets.cxx }} strategy: matrix: sets: - cc: gcc-8 cxx: g++-8 package: g++-8 - cc: gcc-9 cxx: g++-9 package: g++-9 - cc: gcc-10 cxx: g++-10 package: g++-10 - cc: clang-7 cxx: clang++-7 package: clang-7 - cc: clang-8 cxx: clang++-8 package: clang-8 - cc: clang-9 cxx: clang++-9 package: clang-9 - cc: clang-10 cxx: clang++-10 package: clang-10 steps: - uses: actions/checkout@v3 - name: dependencies installation run: | sudo apt update sudo apt install libgnutls28-dev libgtest-dev libgtkmm-3.0-dev meson zlib1g-dev ${{ matrix.sets.package }} - name: meson setup builddir -Dbuildtype=debug -Dcpp_args="-D_DEBUG" run: meson setup builddir -Dbuildtype=debug -Dcpp_args="-D_DEBUG" - name: ninja -C builddir run: ninja -C builddir - name: meson test -v -C builddir run: meson test -v -C builddir - name: ./builddir/src/jdim -V run: ./builddir/src/jdim -V compiler-22: runs-on: ubuntu-22.04 env: CC: ${{ matrix.sets.cc }} CXX: ${{ matrix.sets.cxx }} strategy: matrix: sets: - cc: gcc-11 cxx: g++-11 package: g++-11 # Omit gcc-12 due to jobs are too many. - cc: clang-11 cxx: clang++-11 package: clang-11 # Omit clang-12 due to jobs are too many. - cc: clang-13 cxx: clang++-13 package: clang-13 - cc: clang-14 cxx: clang++-14 package: clang-14 steps: - uses: actions/checkout@v3 - name: dependencies installation run: | sudo apt update sudo apt install libgnutls28-dev libgtest-dev libgtkmm-3.0-dev meson zlib1g-dev ${{ matrix.sets.package }} - name: meson setup builddir -Dbuildtype=debug -Dcpp_args="-D_DEBUG" run: meson setup builddir -Dbuildtype=debug -Dcpp_args="-D_DEBUG" - name: ninja -C builddir run: ninja -C builddir - name: meson test -v -C builddir run: meson test -v -C builddir - name: ./builddir/src/jdim -V run: ./builddir/src/jdim -V autotools: runs-on: ${{ matrix.sets.os }} env: CC: ${{ matrix.sets.cc }} CXX: ${{ matrix.sets.cxx }} CXXFLAGS: -Og -pipe GTEST_SRCDIR: /usr/src/googletest strategy: matrix: sets: - os: ubuntu-22.04 cc: gcc-11 cxx: g++-11 package: g++-11 steps: - uses: actions/checkout@v3 - name: dependencies installation run: | sudo apt update sudo apt install autoconf-archive libgnutls28-dev libgtest-dev libgtkmm-3.0-dev libltdl-dev libtool zlib1g-dev ${{ matrix.sets.package }} - name: autoreconf -i run: autoreconf -i - name: ./configure run: ./configure - name: make -j$(nproc) run: make -j$(nproc) - name: make check -j$(nproc) run: make check -j$(nproc) - name: ./src/jdim -V run: ./src/jdim -V library-20: runs-on: ubuntu-20.04 env: CC: gcc-9 CXX: g++-9 CXXFLAGS: -Og -pipe -D_DEBUG strategy: matrix: deps: - config_args: --with-tls=gnutls --with-sessionlib=xsmp --with-migemo --with-alsa --with-pangolayout packages: libgnutls28-dev libmigemo-dev libasound2-dev - config_args: --with-tls=openssl --with-sessionlib=no --with-migemo --disable-compat-cache-dir packages: libssl-dev libmigemo-dev - config_args: --with-tls=openssl --with-sessionlib=xsmp --with-alsa --with-pangolayout packages: libssl-dev libasound2-dev steps: - uses: actions/checkout@v3 - name: dependencies installation (${{ matrix.deps.packages }}) run: | sudo apt update sudo apt install autoconf-archive libgtest-dev libtool libltdl-dev libgtkmm-3.0-dev ${{ matrix.deps.packages }} g++-9 - name: autoreconf -i run: autoreconf -i - name: ./configure ${{ matrix.deps.config_args }} run: ./configure ${{ matrix.deps.config_args }} - name: make -j$(nproc) run: make -j$(nproc) - name: make check -j$(nproc) run: make check -j$(nproc) - name: ./src/jdim -V run: ./src/jdim -V library-22: runs-on: ubuntu-22.04 env: CC: gcc-11 CXX: g++-11 strategy: matrix: deps: - config_args: -Dtls=gnutls -Dsessionlib=xsmp -Dmigemo=enabled -Dalsa=enabled -Dpangolayout=enabled packages: libgnutls28-dev libmigemo-dev libasound2-dev - config_args: -Dtls=openssl -Dsessionlib=no -Dmigemo=enabled -Dcompat_cache_dir=disabled packages: libssl-dev libmigemo-dev - config_args: -Dtls=openssl -Dsessionlib=xsmp -Dalsa=enabled -Dpangolayout=enabled packages: libssl-dev libasound2-dev steps: - uses: actions/checkout@v3 - name: dependencies installation (${{ matrix.deps.packages }}) run: | sudo apt update sudo apt install meson libgtest-dev libgtkmm-3.0-dev ${{ matrix.deps.packages }} g++-11 - name: meson setup builddir -Dbuildtype=debug -Dcpp_args="-D_DEBUG" ${{ matrix.deps.config_args }} run: meson setup builddir -Dbuildtype=debug -Dcpp_args="-D_DEBUG" ${{ matrix.deps.config_args }} - name: ninja -C builddir run: ninja -C builddir - name: meson test -v -C builddir run: meson test -v -C builddir - name: ./builddir/src/jdim -V run: ./builddir/src/jdim -V manual-build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: actions/jekyll-build-pages@v1 with: source: ./docs destination: ./docs/_site jdim-0.10.1/.github/workflows/weekly.yml000066400000000000000000000054731445721505100201510ustar00rootroot00000000000000name: Weekly CI on: schedule: # Each Monday, on 00:00 UTC - cron: '0 0 * * 1' jobs: Unity-build-gcc12-Werror: runs-on: ubuntu-22.04 env: CC: gcc-12 CXX: g++-12 steps: - uses: actions/checkout@v3 - name: dependencies installation run: | sudo apt update sudo apt install libgnutls28-dev libgtest-dev libgtkmm-3.0-dev meson zlib1g-dev g++-12 - name: meson setup builddir -Dunity=on -Dunity_size=1000 -Dbuildtype=debug -Dcpp_args="-D_DEBUG" -Dwerror=true run: meson setup builddir -Dunity=on -Dunity_size=1000 -Dbuildtype=debug -Dcpp_args="-D_DEBUG" -Dwerror=true - name: ninja -C builddir -k0 run: ninja -C builddir -k0 - name: meson test -v -C builddir run: meson test -v -C builddir - name: ./builddir/src/jdim -V run: ./builddir/src/jdim -V Unity-build-clang15-Werror: runs-on: ubuntu-22.04 env: CC: clang-15 CXX: clang++-15 steps: - uses: actions/checkout@v3 - name: dependencies installation run: | sudo apt update sudo apt install libgnutls28-dev libgtest-dev libgtkmm-3.0-dev meson zlib1g-dev clang-15 - name: meson setup builddir -Dunity=on -Dunity_size=1000 -Dbuildtype=debug -Dcpp_args="-D_DEBUG" -Dwerror=true run: meson setup builddir -Dunity=on -Dunity_size=1000 -Dbuildtype=debug -Dcpp_args="-D_DEBUG" -Dwerror=true - name: ninja -C builddir -k0 run: ninja -C builddir -k0 - name: meson test -v -C builddir run: meson test -v -C builddir - name: ./builddir/src/jdim -V run: ./builddir/src/jdim -V muon-master: runs-on: ubuntu-22.04 env: CC: gcc-11 CXX: g++-11 steps: - uses: actions/checkout@v3 - name: dependencies installation run: | sudo apt update sudo apt install git libgnutls28-dev libpkgconf-dev libgtest-dev libgtkmm-3.0-dev meson zlib1g-dev g++-11 - name: git clone muon run: | git clone --depth 1 https://git.sr.ht/~lattis/muon muon-src # Skip bootstrapping muon to do meson setup instead. - name: git log -n1 muon run: git -C muon-src log -n1 - name: build muon run: | meson setup -Ddocs=disabled -Dlibarchive=disabled -Dlibcurl=disabled -Dlibpkgconf=enabled muon-build muon-src ninja -C muon-build - name: muon version run: ./muon-build/muon version - name: muon setup builddir run: ./muon-build/muon setup -Dcompat_cache_dir=disabled jdim-build - name: ninja -C jdim-build -k0 run: ninja -C jdim-build -k0 - name: muon test run: | cd jdim-build ../muon-build/muon test -v cd .. - name: ./jdim-build/src/jdim -V run: ./jdim-build/src/jdim -V jdim-0.10.1/.gitignore000066400000000000000000000013471445721505100145150ustar00rootroot00000000000000buildinfo.h src/jdim src/**/*.a src/icons/*.h !src/icons/iconmanager.h !src/icons/iconid.h !src/icons/iconfiles.h # generated by autoreconf -i Makefile Makefile.in aclocal.m4 autom4te.cache/ compile config.guess config.h.in config.sub configure depcomp install-sh ltmain.sh missing # generated by ./configure config.h* config.log config.status libtool src/.deps/ src/article/.deps/ src/bbslist/.deps/ src/board/.deps/ src/config/.deps/ src/control/.deps/ src/dbimg/.deps/ src/dbtree/.deps/ src/history/.deps/ src/icons/.deps/ src/image/.deps/ src/jdlib/.deps/ src/message/.deps/ src/skeleton/.deps/ src/sound/.deps/ src/xml/.deps/ stamp-h1 test-driver test/.deps # generated by make *.o # docs/ docs/.bundle/ docs/Gemfile.lock docs/_site/ jdim-0.10.1/AUTHORS000066400000000000000000000000741445721505100135710ustar00rootroot00000000000000https://jdimproved.github.io/JDim/ を見てください。 jdim-0.10.1/CONTRIBUTING.md000066400000000000000000000171451445721505100147610ustar00rootroot00000000000000# JDimへコントリビュートする はじめに、ガイドを読む時間を取っていただきありがとうございます。:+1: 私達はJDimを開発・保守してくためにパッチを歓迎しています!:revolving_hearts: JDimへのコントリビュート方法を案内します。 * :beginner: まずはJDimproved/JDimをforkしてリポジトリを用意し、ローカルにcloneしてJDimをビルドしてみましょう。 → [README.md][readme-md] * :mag: [Issues][issues]や[Pull requests][pull-requests]を確認すると興味関心のあるトピックが見つかるかもしれません。 * :earth_asia: [Linux板@5ちゃんねる][linux-5ch]のJD/JDimスレで質問やバグ報告をすることもできます。 * :heart_decoration: JDimはボランティアによって開発・保守されています。 そのため速やかに返信することが難しい場合がありますが予めご了承ください。 ## :question: 質問をしたい Discussionsを開いて質問をします。 → [New discussion][new-discussion] 使い方や設定方法の情報は[オンラインマニュアル][manual]や 2ch/5chのスレッド・過去ログにありますのでそちらも参照してください。 ## :beetle: バグを報告したい Issueを開いてバグを報告します。 → [Issue: 不具合(バグ)の報告][new-bug-report] 以下の情報は原因特定の手がかりになりますのでご報告くださいますようお願いいたします。 * 動作環境(クリップボードへのコピーを利用してください) * やりたいこと・期待する動作 * バグが現れるまでの手順・操作 * 実際の結果・エラーメッセージ バグなのかはっきりしない場合はDiscussionsで質問してみてください。 → [New discussion][new-discussion] ## :muscle: 機能のリクエストをしたい Issueを開いて欲しい機能の要望を出しフィードバックを集めます。 → [Issue: 機能の変更][new-feature-request] 可能ならコードを書き始めてください。実証用でもコードがあると具体的なフィードバックが集まりやすいです。 ## :mailbox_with_mail: Pull requestを提出する Pull requestは`master`ブランチに対してお願いいたします。 * 文書やソースコードのタイプミス修正、バグ修正、文書の改善、機能の改善などは直接PRを受け付けています。 * ユーザーインタフェースの変更や互換性に影響が出る修正は最初にissueを開いて意見・要望をお伝えいただければ幸いです。 * 影響が大きい変更は[RFCプロセス][rfcs]が必要になる場合があります。 * オンラインマニュアルの編集については [docs/README.md][docs-readme] を参照してください。 * テストケースを追加して動作をチェックすることもできます。詳細は [test/README.md][test-readme] を参照してください。 #### :pencil: C++ソースコードを修正するときの注意 * C++17の機能を使う。迷ったときは[C++ Core Guidelines][isocpp]を参考にする。 * CIのビルドで失敗する機能は使わなくてもビルドできるようにする。(下記参照) * コーディングスタイルは周囲のコードになるべく合わせる。 * ソースコードを修正したときはビルド可能なことチェックする。 * 修正前よりコンパイル時警告を増やさないように気をつける。 C++17で追加された標準ライブラリのうちg++ 8またはclang++ 7が[サポート][support]していないものに注意 | JDimの動作環境に合わない標準ライブラリ | ヘッダー | gcc | clang | | --- | --- | ---:| ---:| | [Standardization of Parallelism TS][cpp17exe] | `` | 9 | n/a | | [Hardware interference size][cpp17his] | | 12 | n/a | | ( [File system library][cpp17fs] ) | `` | 8 | 7 | | [Polymorphic memory resources][cpp17pmr] | `` | 9 | 16 | | [Mathematical special functions][cpp17math] | | 7 | n/a | | Splicing [Maps][cpp17maps] and [Sets][cpp17sets] | | 7 | 8 | | [Elementary string conversions][cpp17conv] (floating-point support) | `` | 11 | n/a | | `std::shared_ptr` and `std::weak_ptr` with array support | | 7 | 11 | | DR: [`std::hash`][cpp17fspathhash] | | 11.4 | n/a | * [gcc-8][gcc8fs] と [clang-7][clang7fs] の `` はライブラリが分かれている 使うときは別途リンクが必要なことをREADMEに注意書きすること [readme-md]: https://github.com/JDimproved/JDim/tree/master/README.md [issues]: https://github.com/JDimproved/JDim/issues [pull-requests]: https://github.com/JDimproved/JDim/pulls [linux-5ch]: https://mao.5ch.net/linux/ [new-discussion]: https://github.com/JDimproved/JDim/discussions/new [new-bug-report]: https://github.com/JDimproved/JDim/issues/new?assignees=&labels=bug&template=bug-report.md&title= [new-feature-request]: https://github.com/JDimproved/JDim/issues/new?assignees=&labels=feature&template=feature-request.md&title= [manual]: https://jdimproved.github.io/JDim/ [rfcs]: https://github.com/JDimproved/rfcs [docs-readme]: https://github.com/JDimproved/JDim/tree/master/docs/README.md [test-readme]: https://github.com/JDimproved/JDim/tree/master/test/README.md [isocpp]: https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines [support]: https://en.cppreference.com/w/cpp/compiler_support/17 [clang7fs]: https://releases.llvm.org/7.1.0/projects/libcxx/docs/UsingLibcxx.html [cpp17exe]: https://en.cppreference.com/w/cpp/header/execution [cpp17his]: https://en.cppreference.com/w/cpp/thread/hardware_destructive_interference_size [gcc8fs]: https://stackoverflow.com/questions/53201991/how-to-use-stdfilesystem-on-gcc-8 [cpp17fs]: https://en.cppreference.com/w/cpp/filesystem [cpp17pmr]: https://en.cppreference.com/w/cpp/header/memory_resource [cpp17math]: https://en.cppreference.com/w/cpp/numeric/special_functions [cpp17maps]: https://en.cppreference.com/w/cpp/container/map/merge [cpp17sets]: https://en.cppreference.com/w/cpp/container/set/merge [cpp17conv]: https://en.cppreference.com/w/cpp/header/charconv [cpp17fspathhash]: https://en.cppreference.com/w/cpp/filesystem/path/hash #### :chains: Unity buildの注意 ビルドツールMesonは複数のソースファイルを1つに結合してコンパイルする [Unity build][meson-unity-build] という機能があります(デフォルトは無効)。 機能を有効に設定してソースファイルを結合するとマクロや変数などの名前が衝突してコンパイルに失敗することがあります。 ビルドの問題を見つけたときはバグ報告や修正のPull requestをいただけると幸いです。 * 名前が衝突したときは名称を変更したり入れ子の名前空間の中に入れるなどで衝突を回避できます。 ```cpp // 入れ子の名前空間の例 namespace Outer::priv { constexpr int kBufferSize = 1024; } using namespace Outer; use_value( priv::kBufferSize ); ``` * `#define`マクロによる意図しない定義変更(上書き)に注意してください。 コンパイラーがマクロの警告を出したときはマクロ以外の方法が使えないかチェックしてみてください。 ##### Unity buildの使い方 setupサブコマンドで`-Dunity=on`を指定します。 ビルドオプション`-Dunity_size=N`で1つに結合するファイル数を変更できます。 ```sh meson setup builddir -Dunity=on -Dunity_size=10 ninja -C builddir ``` [meson-unity-build]: https://mesonbuild.com/Unity-builds.html jdim-0.10.1/COPYING000066400000000000000000000431101445721505100135520ustar00rootroot00000000000000 GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc. 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Library General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. , 1 April 1989 Ty Coon, President of Vice This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Library General Public License instead of this License. jdim-0.10.1/ChangeLog000066400000000000000000000013041445721505100142700ustar00rootroot00000000000000[ 更新履歴 ] 以前の更新履歴は"https://jdimproved.github.io/JDim/"を参照してください。 svnのコミットログを知りたい場合は svn log -r 1000:HEAD http://svn.sourceforge.jp/svnroot/jd4linux/jd/trunk などの様にしてsvnサーバから直接取得してください。 (1000のところは適当なリビジョン番号にしてください) ------------------------------------------------------------------------ 2.8.9-150226 - 2015/02/26(木) ------------------------------------------------------------------------ * リロード待ち状態でスレが更新されないままになる問題を修正した * タブ表示をさらに最適化した jdim-0.10.1/INSTALL000066400000000000000000000105601445721505100135530ustar00rootroot00000000000000[ make、実行方法について ] * 動作環境 必須環境 ・gtkmm 3.24.0 以上 ・glibmm 2.58.0 以上 ・zlib 1.2 以上 ・gnutls 3.6.7 以上 推奨環境 ・Linux Kernel 4.19.0 以上 ・UTF-8環境 ( EUC環境では LANG="ja_JP.UTF-8" を指定する必要がある ) ※ GTK2版はv0.4.0リリースをもって廃止 ( https://github.com/JDimproved/JDim/issues/229 を参照 ) * makeに必要なツール、ライブラリ 必須 ・meson 0.53.0 以上 ・g++ 8 以上、または clang++ 7 以上 ・gnutls ・gtkmm ・zlib オプション ・alsa-lib (-Dalsa=enabled) ・openssl 1.1.1 以上 (-Dtls=openssl) ・migemo (-Dmigemo=enabled) ・googletest 1.10.0 以上 (`test/RADME.md`を参照) WebPやAVIF画像の表示に必要なパッケージ インストールされていない環境では`.webp`や`.avif`で終わるURLは通常リンクになる。 ・libwebp, webp-pixbuf-loader (WebP) ・libavif (AVIF) GTKがデフォルトでサポートしていないWebPやAVIF形式の画像を表示する方法は https://github.com/JDimproved/JDim/discussions/737 を参照。(v0.5.0+からサポート) OSやディストリビューション別の解説は https://github.com/JDimproved/JDim/discussions/592 を参照。 * ビルド方法( meson ) 1. meson setup builddir 2. ninja -C builddir ( または meson compile -C builddir ) 3. 起動は ./builddir/src/jdim mesonのビルドオプション - meson setup builddir -Dpangolayout=enabled のように指定する。 - オプションの一覧は meson configure を実行してProject optionsの段落を参照する。 Autotools(./configure)のサポートは2023年7月のリリースをもって廃止されます。 詳しくは https://github.com/JDimproved/rfcs/blob/master/docs/0012-end-of-autotools-support.md を参照。 * ビルドオプション -Dsessionlib=[xsmp|no] XSMP を使ってセッション管理をするには「xsmp」を、セッション管理を無効にするには「no」を選択する。 デフォルトでは XSMP を使用する。 -Dpangolayout=enabled 描画にPangoLayoutを使う。デフォルトでは PangoGlyphString を使用する。 スレビューのテキスト表示に問題があるときはこのオプションを試してみてください。 -Dmigemo=enabled migemoによる検索が有効になる。migemoがUTF-8の辞書でインストールされている必要がある。 有効にすると正規表現のメタ文字が期待通りに動作しない場合があるので注意すること。 -Dmigemodict=PATH (-Dmigemo 限定) migemo の辞書ファイルの場所を設定する。 about:config で変更が可能、空欄にした場合は migemo が無効になる。(変更後は要再起動) -Dnative=enabled CPUに合わせた最適化 手動でCPUの種類を指定する場合は meson -Dcpp_args="-march=ARCH" を利用してください。 -Dtls=[gnutls|openssl] 使用するSSL/TLSライブラリを設定する。デフォルトでは GnuTLS を使用する。 -Dtls=openssl GnuTLS のかわりに OpenSSL を使用する。ライセンス上バイナリ配布が出来なくなることに注意すること。 -Dalsa=enabled ALSAによる効果音再生機能を有効にする。詳しくは"https://jdimproved.github.io/JDim/"の項を参照すること。 -Dgprof=enabled gprofによるプロファイリングを行う。コンパイルオプションに -pg が付き、JDimを実行すると gmon.out が出来るので gprof ./gmon.out で解析できる。CPUの最適化は効かなくなるので注意する。 -Dcompat_cache_dir=disabled JDのキャッシュディレクトリ ~/.jd を読み込む互換機能を無効化する。 -Dpackager=PACKAGER Meson限定: 動作環境にパッケージや作成者の情報を追加する。(v0.7.0+から追加) PACKAGER に改行やHTML文字参照を *含めない* ことを推奨する。 * メモ 実行するには直接 builddir/src/jdim を起動するか手動で /usr/bin あたりに builddir/src/jdim を cp する。 以上の操作で ninja が通らなかったり動作が変な時は meson のビルドオプションを変更する。 jdim-0.10.1/Makefile.am000066400000000000000000000046301445721505100145570ustar00rootroot00000000000000SUBDIRS = src test desktopdir= $(datadir)/applications dist_desktop_DATA = jdim.desktop icondir = $(datadir)/icons/hicolor/48x48/apps dist_icon_DATA = jdim.png iconsvgdir = $(datadir)/icons/hicolor/scalable/apps dist_iconsvg_DATA = jdim.svg metainfodir = $(datadir)/metainfo dist_metainfo_DATA = jdim.metainfo.xml EXTRA_DIST = AUTHORS TODO README configure jdim_BUILDINFO_HEADER = buildinfo.h jdim_CONFIGURE_ARGS = @ac_configure_args@ all: $(jdim_BUILDINFO_HEADER) test: all $(MAKE) -C $@ $(MAKECMDGOALS) .PHONY: test $(jdim_BUILDINFO_HEADER) $(jdim_BUILDINFO_HEADER): @-rm -f $@.new @echo '/* This file is generated from Makefile by make. */' >> $@.new @echo '#ifndef _BUILDINFO_H' >> $@.new @echo '#define _BUILDINFO_H' >> $@.new @echo '' >> $@.new @echo '// Build information.' >> $@.new @if test -n "$(jdim_CONFIGURE_ARGS)"; \ then \ echo "#define CONFIGURE_ARGS \"$(jdim_CONFIGURE_ARGS)\"" >> $@.new; \ fi @echo '' >> $@.new @echo '// Version information of GIT.' >> $@.new @GIT_HASH="" ; \ GIT_DATE="" ; \ GIT_DIRTY=0; \ GIT_STATUS="" ; \ if test -n "$(GIT)" ; \ then \ WORK_TREE="$(abs_top_srcdir)" ; \ GIT_HASH=`LANG=C.utf8 "$(GIT)" -C "$${WORK_TREE}" log --pretty=format:%h --abbrev=10 -n 1 2>/dev/null`; \ GIT_DATE=`LANG=C.utf8 "$(GIT)" -C "$${WORK_TREE}" log --pretty=format:%ad --date=format:%Y%m%d -n 1 2>/dev/null`; \ GIT_STATUS="`LANG=C.utf8 "$(GIT)" -C "$${WORK_TREE}" status -uno -s 2>/dev/null | head -n 1`" ; \ if test -n "$${GIT_HASH}" ; \ then \ echo "GIT: Hash = \"$${GIT_HASH}\"" ; \ fi ; \ if test -n "$${GIT_DATE}" ; \ then \ echo "GIT: Date = \"$${GIT_DATE}\"" ; \ fi ; \ if test -n "$${GIT_STATUS}" ; \ then \ echo "GIT: There are changes not committed yet." ; \ GIT_DIRTY=1 ; \ fi \ fi ; \ echo "#define GIT_HASH \"$${GIT_HASH}\"" >> $@.new ; \ echo "#define GIT_DATE \"$${GIT_DATE}\"" >> $@.new ; \ echo "#define GIT_DIRTY $${GIT_DIRTY}" >> $@.new @echo '' >> $@.new @echo '#endif' >> $@.new @if test ! -e $@; \ then \ echo '$@: Created.'; \ mv -f $@.new $@; \ elif test -z "$(XSUM)"; \ then \ echo '$@: Refreshed.'; \ mv -f $@.new $@; \ else \ HASH1=`cat $@ | $(XSUM)`; \ HASH2=`cat $@.new | $(XSUM)`; \ if test "$${HASH1}" = "$${HASH2}"; \ then \ echo '$@: Not modified.'; \ rm -f $@.new; \ else \ echo '$@: Modified.'; \ mv -f $@.new $@; \ fi \ fi jdim-0.10.1/NEWS000066400000000000000000000000741445721505100132200ustar00rootroot00000000000000https://jdimproved.github.io/JDim/ を見てください。 jdim-0.10.1/README000066400000000000000000000000421445721505100133740ustar00rootroot00000000000000README.mdを見てください。 jdim-0.10.1/README.md000066400000000000000000000370751445721505100140130ustar00rootroot00000000000000# JDim - 2ch browser for linux ![GitHub Actions CI](https://github.com/JDimproved/JDim/workflows/CI/badge.svg) [![Snap Store](https://snapcraft.io/jdim/badge.svg)](https://snapcraft.io/jdim) ここに書かれていない詳細は [オンラインマニュアル][manual] を、 開発に参加するための手順については [CONTRIBUTING][contrib] や [RFC][rfcs] を参照してください。 [manual]: https://jdimproved.github.io/JDim/ [repository]: https://github.com/JDimproved/JDim [rfcs]: https://github.com/JDimproved/rfcs "Request for Comments" * [概要](#概要) * [動作プラットフォーム](#動作プラットフォーム) * [導入方法](#導入方法) * [事前準備](#事前準備) * [ビルド](#ビルド) * [Snapパッケージ](#Snapパッケージ) * [通常の起動](#通常の起動) * [コマンドライン オプション](#コマンドライン-オプション) * [多重起動について](#多重起動について) * [実行時の注意事項](#precaution) * [JDとの互換性](#JDとの互換性) * [JDから移行する](#JDから移行する) * [著作権](#著作権) * [ライセンス](#ライセンス) * [連絡先](#連絡先) ## 概要 JDim (JD improved) は gtkmm/GTK+ を使用した"2ちゃんねる"型マルチスレッドBBSを閲覧するためのブラウザです。 JDim は GPLv2 の下で公開されている [JD][jd-project] からforkしたソフトウェアであり、 ルック・アンド・フィールや環境設定は JD と互換性があります。 **注意: 2023-07-11 からJDim本体で5chのスレ閲覧が可能になっています。** 5ch.netのDATファイルへのアクセスが[開放][5ch-924]されていますが今後の動向に注意してください。 また、デフォルト設定のユーザーエージェント(UA)のままでレスを書き込むとERRORになるため 事前にwebブラウザなどのUAに設定変更してください。 [jd-project]: https://jd4linux.osdn.jp/ [5ch-924]: https://agree.5ch.net/test/read.cgi/operate/9240230711/ ## 動作プラットフォーム LinuxなどのUnixライクなOS(FreeBSD,OpenBSD,Nexenta,MacOSXでも動作報告例があります)。 ##### サポートの最新情報 gccのバージョンが8未満のプラットフォームはサポートを終了しました。 Debian buster(2019年)より前にリリースされたディストリビューションを利用されている場合は更新をお願いいたします。 メンテナンスの都合によりWindows(MinGW)版のサポートは[終了][#445]しました。 Snap i386(32bit)版は2023年1月のリリースをもって[更新を終了][#890]しました。 i386版ディストロを利用されている場合は更新をお願いいたします。 [#1035]: https://github.com/JDimproved/JDim/issues/1035 [#445]: https://github.com/JDimproved/JDim/issues/445 [#890]: https://github.com/JDimproved/JDim/issues/890 ## 導入方法 ソースコードからJDimをビルドします。**GTK3版がビルド**されますのでご注意ください。 詳細は [INSTALL](./INSTALL) にも書いてあります。 **Autotools(./configure)のサポートは2023年7月のリリースをもって廃止されます。 かわりに[meson][mesonbuild]を利用してください。([RFC 0012][rfc0012])** [mesonbuild]: https://mesonbuild.com [dis556]: https://github.com/JDimproved/JDim/discussions/556 "Mesonを使ってJDimをビルドする方法 - Discussions #556" [rfc0012]: https://github.com/JDimproved/rfcs/blob/master/docs/0012-end-of-autotools-support.md ### 事前準備 ツールチェーンとライブラリをインストールします。一度インストールすれば次回から事前準備はいりません。 #### Redhat系 ```sh dnf install gtkmm30-devel gnutls-devel libSM-devel meson git ``` #### Debian (buster以降) ```sh sudo apt install libc6-dev meson gcc g++ git sudo vi /etc/apt/sources.list # ↑エディタは何でも良い。deb-src行でbuster以降を有効にする sudo apt update sudo apt build-dep jdim ``` #### Ubuntu (20.04以降) 開発環境が入っていない場合は、 ```sh sudo apt install build-essential git meson ``` 必要なライブラリを入れます。(抜けがあるかも) ```sh sudo apt install libgtkmm-3.0-dev libltdl-dev libgnutls28-dev ``` ### ビルド ```sh git clone -b master --depth 1 https://github.com/JDimproved/JDim.git jdim cd jdim meson setup builddir ninja -C builddir ``` 実行するには直接 builddir/src/jdim を起動するか手動で /usr/bin あたりに builddir/src/jdim を cp します。 #### Arch Linux ビルドファイルはAURで公開されています。(Thanks to @naniwaKun.) https://aur.archlinux.org/packages/jdim-git/ AUR Helper [yay](https://github.com/Jguer/yay) でインストール ```sh yay -S jdim-git ``` ### 参考 OSやディストリビューション別の解説は [GitHub Discussions][dis592] を参照してください。 [dis592]: https://github.com/JDimproved/JDim/discussions/592 "OS/ディストリビューション別インストール方法 - Discussions #592" ### Tips * **buildの高速化** Mesonは並列コンパイル(job数)を自動で設定します。メモリー不足などでビルドが中断するときは `ninja`(または`meson compile`)するときに `-j job数` を指定して実行数を調整してください。 Mesonの機能 [Unity build] を有効に設定するとビルドが高速化できる可能性があります。 機能を有効にするにはsetupコマンドでビルドオプションを指定します。`meson setup -Dunity=on builddir` [Unity build]: https://mesonbuild.com/Unity-builds.html * **CPUに合わせた最適化** `meson`を実行するときにCPUの種類(`-march=ARCH`や`-mcpu=CPU`)を`-Dcpp_args`に設定します。 ###### 例 (第2世代Coreプロセッサー) ```sh meson setup builddir -Dcpp_args="-march=sandybridge" -Doptimization=2 ``` マシンのCPUは下のコマンドで調べることができます。([GCCの最適化][gentoo-gcc] - Gentoo Wikiより) ```sh gcc -Q -c -march=native --help=target -o /dev/null | grep "march\|mtune\|mcpu" ``` [gentoo-gcc]: https://wiki.gentoo.org/wiki/GCC_optimization/ja#-march * **AddressSanitizer(ASan) を有効にするときの注意** gcc(バージョン10以降)を使いASanを有効にしてビルドすると 書き込みのプレビューで[トリップ][trip]を表示するときにクラッシュすることがある。 詳細は を参照。 #### ASanを有効にするときクラッシュを回避する方法 * 事前に環境変数 LDFLAGS を設定してビルドする ``` export LDFLAGS="$LDFLAGS -Wl,--push-state,--no-as-needed -lcrypt -Wl,--pop-state" meson setup asan -Db_sanitize=address ninja -C asan ``` * または、mesonのコマンドラインオプション`-Db_asneeded`でフラグを変更する ``` meson setup asan -Db_sanitize=address -Db_asneeded=false ninja -C asan ``` [trip]: https://ja.wikipedia.org/wiki/%E3%83%88%E3%83%AA%E3%83%83%E3%83%97_(%E9%9B%BB%E5%AD%90%E6%8E%B2%E7%A4%BA%E6%9D%BF) * **`sudo meson install`するとバージョンからgitコミット情報が消える場合** [CVE-2022-24765] の対策が入ったgitを使う場合、 rootユーザー(sudo)でmesonを実行するとバージョンからビルド時のコミット情報が消去されることがあります。 詳細は を見てください。 インストール時にコミット情報の消去を回避するには最新のJDimをビルドする、 または`--no-rebuild`を追加してinstallコマンドを実行します。([他の回避策][sudo-quickfix]) ```sh sudo meson install --no-rebuild -C _build ``` [CVE-2022-24765]: https://nvd.nist.gov/vuln/detail/CVE-2022-24765 [sudo-quickfix]: https://github.com/JDimproved/JDim/issues/965#issuecomment-1107459351 ### Snapパッケージ JDim はSnapパッケージとして[Snap Storeで公開][snapcraft]されています。 詳細は[マニュアル][manual-snap]を参照してください。 [snapcraft]: https://snapcraft.io/jdim [manual-snap]: https://jdimproved.github.io/JDim/start#snap ## 通常の起動 使い方は以下のとおりです。 ```sh $ jdim [OPTION] [URL,FILE] ``` 引数にURLを付けて起動する事も出来るので、他のアプリケーションから外部コマンドとしてURLを開く事などが出来ます。 (JDimが扱う事の出来るURLでない場合は設定されているWebブラウザに渡されます) ```sh $ jdim http://pc99.2ch.net/test/read.cgi/linux/1234567890/ ``` ローカルにあるdatファイルを指定して、一時的にスレビュー表示させることも出来ます。 ```sh $ jdim ./12345.dat ``` 環境変数 `JDIM_CACHE` でキャッシュディレクトリの位置を変更・指定することが可能です。 指定しなければ下記の[優先順位](#キャッシュディレクトリの優先順位)の通りに決まります。 ```sh $ JDIM_CACHE=~/.mycache jdim ``` 環境変数 `JDIM_LOCK` でロックファイルの位置を変更・指定することが可能です。 指定しなければ `<キャッシュディレクトリ>/JDLOCK` がロックファイルになります。 ```sh $ JDIM_LOCK=~/mylock jdim ``` #### キャッシュディレクトリの優先順位 | `~/.jd` | `$XDG_CACHE_HOME/jdim` | 使われるのは… | | --- | --- | --- | | 存在する | any | `~/.jd` | | 存在しない | any | `$XDG_CACHE_HOME/jdim` | | any (無効化) | any | `$XDG_CACHE_HOME/jdim` | NOTE: - 環境変数 `XDG_CACHE_HOME` が未設定または空のときはかわりに `$HOME/.cache/jdim` が使われます。 - `~/.jd` が無効化されている場合は `jdim --version` の出力に `--disable-compat-cache-dir` が追加されます。 - キャッシュディレクトリはJDimを起動すると作成されます。 ### コマンドライン オプション オプション | 説明 --- | --- -h, --help | ヘルプを表示 -m, --multi | 多重起動時のサブプロセスであっても終了しない -s, --skip-setup | 初回起動時の設定ダイアログを表示しない -l, --logfile | エラーなどのメッセージをファイル(キャッシュディレクトリのlog/msglog)に出力する -g, --geometry WxH-X+Y | 幅(W)高さ(H)横位置(X)縦位置(Y)の指定。WxHは省略可能(例: -g 100x40-10+30, -g -20+100 ) -V, --version | バージョン及びビルドオプションを全て表示 ## 多重起動について [マニュアル][manual-multiple]を参照してください。 [manual-multiple]: https://jdimproved.github.io/JDim/start/#multiple "起動について | JDim" ## 実行時の注意事項 廃止されたGTK2版同様のルック・アンド・フィールになるように実装していますが、 技術的な問題やテスト不足から完全な再現はできていません。 もしお気づきの点などがございましたらご指摘いただけると幸いです。 ### Wayland対応 JDim はWayland環境で起動しますが動作は安定していません。 GTKのバックエンドにWaylandを使うかわりに互換レイヤーのXWaylandをインストールして使うことをお薦めします。 環境変数 `GDK_BACKEND=x11` を設定してjdimを起動してください。 ```sh # シェルからJDimを起動する場合 GDK_BACKEND=x11 ./src/jdim ``` WaylandやXWaylandではX11限定の機能を使うことができないため注意してください。 ### GTK2版から変更/追加された部分 * GTK+ 3.14以上の環境でタッチスクリーンによる操作に対応した。 スレビューのタッチ操作については[マニュアル][manual-touch]を参照。 * 書き込みビューの配色にGTKテーマを使う設定が追加された。 1. メニューバーの`設定(C) > フォントと色(F) > 詳細設定(R)...`からフォントと色の詳細設定を開く 2. `色の設定`タブにある`書き込みビューの配色設定に GTKテーマ を用いる(W)`をチェックして適用する * GTK 3.16以上の環境で書き込みビューのダブルクリックによる単語の範囲選択、 トリプルクリックによる行の範囲選択に対応した。 [manual-touch]: https://jdimproved.github.io/JDim/operation/#threadview_touch "操作方法について | JDim" ### 既知の問題 * タブのドラッグ・アンド・ドロップの矢印ポップアップの背景が透過しない環境がある。 (アルファチャンネルが利用できない環境) * Wayland上で起動したときポップアップ内のアンカーからポップアップを出すとマウスポインターから離れた位置に表示される。 * Weston(Waylandコンポジタ)環境でXWaylandをバックエンドに指定して起動した場合、右クリックしながらポップアップ内に マウスポインターを動かすとポップアップ内容ではなくポップアップに隠れたスレビューに反応する。 * Wayland環境では画像ビュー(ウインドウ表示)のフォーカスが外れたら折りたたむ機能が正常に動作しない。 * gcc(バージョン10以降)を使いAddressSanitizer(ASan)を有効にしてビルドすると 書き込みのプレビューでトリップを表示するときにクラッシュすることがある。([上記](#crash-with-asan)を参照) ## JDとの互換性 JDimの環境設定はJDからフォーマットを継承しているので後方互換性があります。 また、ユーザーインタフェースの変更は今のところありません。 JDimで追加された不具合や機能の修正については[Pull requests][pr-merged]を見てください。 [pr-merged]: https://github.com/JDimproved/JDim/pulls?q=is%3Apr+is%3Amerged ### JDから移行する * 後方互換性としてJDのキャッシュディレクトリ(`~/.jd`)はそのまま使うことができます。 ただし、オプション`--disable-compat-cache-dir`が指定されたビルドでは互換機能は無効化されます。 * 互換機能が使えないときは `$XDG_CACHE_HOME/jdim`(`~/.cache/jdim`) にキャッシュディレクトリを移動してください。 ```bash $ mv ~/.jd ~/.cache/jdim ``` * 環境変数`JD_CACHE`でキャッシュディレクトリを設定している場合はかわりに`JDIM_CACHE`を使用してください。 * JDとJDimを併存させる(データや設定を共有しない)ためには環境変数によるキャッシュディレクトリの設定が必要です。 ([通常の起動](#通常の起動)を参照) ## 著作権 © 2017-2019 yama-natuki [https://github.com/yama-natuki/JD] © 2019-2023 JDimproved project [https://github.com/JDimproved/JDim] パッチやファイルを取り込んだ場合、それらのコピーライトは「JDimproved project」に統一します。 fork元の JD: © 2006-2015 JD project [https://ja.osdn.net/projects/jd4linux/] ## ライセンス JDim は GPLv2 の下で公開されています。 [GNU General Public License, version 2][lisence] [lisence]: https://github.com/JDimproved/JDim/blob/master/COPYING ## 連絡先 バグ報告その他は [Linux板@5ちゃんねる](http://mao.5ch.net/linux/) のJD/JDimスレ、 またはJDimのリポジトリにて行ってください。詳しい方法は[ガイド][contrib]をご覧ください。 [contrib]: https://github.com/JDimproved/JDim/tree/master/CONTRIBUTING.md jdim-0.10.1/TODO000066400000000000000000000000001445721505100131760ustar00rootroot00000000000000jdim-0.10.1/autogen.sh000077500000000000000000000037311445721505100145250ustar00rootroot00000000000000#!/bin/sh if test ! -f install-sh ; then touch install-sh ; fi MAKE=`which gnumake` if test ! -x "$MAKE" ; then MAKE=`which gmake` ; fi if test ! -x "$MAKE" ; then MAKE=`which make` ; fi HAVE_GNU_MAKE=`$MAKE --version|grep -c "Free Software Foundation"` if test "$HAVE_GNU_MAKE" != "1"; then echo Only non-GNU make found: $MAKE else echo `$MAKE --version | head -1` found fi if which autoconf2.50 >/dev/null 2>&1 then AC_POSTFIX=2.50 elif which autoconf259 >/dev/null 2>&1 then AC_POSTFIX=259 elif which autoconf >/dev/null 2>&1 then AC_POSTFIX="" else echo 'you need autoconfig (2.58+ recommended) to generate the Makefile' exit 1 fi echo checking autoconf$AC_POSTFIX ... echo `autoconf$AC_POSTFIX --version | head -1` found unset AM_POSTFIX for num in 10 9 8 7 ; do if which automake-1.$num > /dev/null 2>&1 ; then AM_POSTFIX=-1.$num break fi done if test -z "$AM_POSTFIX" ; then for num in 19 18 17 ; do if which automake$num > /dev/null 2>&1 ; then AM_POSTFIX=$num break fi done fi if test -z "$AM_POSTFIX" ; then if ! which automake > /dev/null 2>&1 ; then echo 'you need automake (1.8.3+ recommended) to generate the Makefile' exit 1 fi fi echo checking automake$AM_POSTFIX ... echo `automake$AM_POSTFIX --version | head -1` found if which libtoolize15 >/dev/null 2>&1 then LB_POSTFIX=15 elif which libtoolize >/dev/null 2>&1 then LB_POSTFIX="" else echo 'you need libtoolize to generate the Makefile' exit 1 fi echo checking libtoolize$LB_POSTFIX ... echo `libtoolize$LB_POSTFIX --version | head -1` found if test -d /usr/local/share/aclocal ; then ACLOCAL_INCLUDE="-I /usr/local/share/aclocal" fi echo This script runs configure and make... echo You did remember necessary arguments for configure, right? # autoreconf$AC_POSTFIX -fim _might_ do the trick, too. # chose to your taste aclocal$AM_POSTFIX $ACLOCAL_INCLUDE libtoolize$LB_POSTFIX --force --copy autoheader$AC_POSTFIX automake$AM_POSTFIX --add-missing --copy --gnu autoconf$AC_POSTFIX #./configure $* && $MAKE jdim-0.10.1/configure.ac000066400000000000000000000167521445721505100150210ustar00rootroot00000000000000dnl dnl JDim用 configure.ac dnl dnl Autotoolsのサポートは2023年7月のリリースをもって廃止されます。かわりにMesonを利用してください。 dnl AC_PREREQ([2.69]) AC_INIT([jdim],[1.0]) AM_INIT_AUTOMAKE AC_CONFIG_HEADERS(config.h) AC_PROG_CC AC_PROG_CPP AC_PROG_CXX AC_PROG_CXXCPP LT_INIT AC_PROG_MKDIR_P AC_C_BIGENDIAN AC_LANG([C++]) AX_CXX_COMPILE_STDCXX([17], [noext], [mandatory]) dnl dnl buildinfo.h dnl AC_DEFINE(HAVE_BUILDINFO_H, 1, Define to 1 if you have the 'buildinfo.h' header file.) AC_PATH_PROG(GIT, git) AC_PATH_PROG(XSUM, md5sum, [cksum]) AC_SUBST(ac_configure_args) dnl --------------------------------------------------- dnl --------------------------------------------------- dnl dnl ユーザー設定 dnl dnl 追加コンパイルオプション dnl -Wextraで有効になる-Wunused-parameterは修正方法の検討が必要なので暫定的に無効 CXXFLAGS="-ggdb -Wall -Wextra -Wno-unused-parameter -pedantic $CXXFLAGS" CPPFLAGS="-DGTK_DOMAIN='\"gtk30\"' $CPPFLAGS" dnl --------------------------------------------------- dnl --------------------------------------------------- LIBSM_CFLAGS="" LIBSM_LIBS="" dnl dnl パッケージのチェック dnl dnl dnl gtkmm dnl PKG_CHECK_MODULES(GTKMM, [gtkmm-3.0 >= 3.24.0]) AC_SUBST(GTKMM_CFLAGS) AC_SUBST(GTKMM_LIBS) dnl dnl crypt dnl AC_CHECK_LIB(crypt,crypt) AC_CHECK_LIB(crypt, crypt_r, [AC_DEFINE(HAVE_CRYPT_R, [1], [Define to 1 if you have the 'crypt_r' function])], ) dnl dnl zlib dnl AC_CHECK_HEADERS([zlib.h]) AC_CHECK_LIB(z,inflate) dnl dnl packages dependent on platform dnl dnl dnl any other POSIX systems dnl AC_CHECK_HEADERS([socket.h]) AC_CHECK_LIB(socket,socket) AC_CHECK_FUNC([timegm], [], [AC_DEFINE(NO_TIMEGM, 1, [Define to 1 if you do not have the 'timegm' function])]) dnl dnl X関連ライブラリ dnl PKG_CHECK_MODULES(X11, x11) AC_SUBST(X11_CFLAGS) AC_SUBST(X11_LIBS) dnl dnl セッション管理 dnl AC_MSG_CHECKING(for --with-sessionlib) AC_ARG_WITH(sessionlib, AS_HELP_STRING([--with-sessionlib=xsmp|no],[use session control library @<:@default=xsmp@:>@]), [with_sessionlib="$withval"], [with_sessionlib=xsmp]) AC_MSG_RESULT($with_sessionlib) AS_IF( [test "x$with_sessionlib" = xno], [], dnl XSMPを使ってセッション管理をする。libSMとlibICEが必要。無ければXSMPは無効になる [test "x$with_sessionlib" = xxsmp], [PKG_CHECK_MODULES(LIBSM, [sm >= 1.2], [], [ac_sm_ice_found=no]) PKG_CHECK_MODULES(LIBICE, [ice >= 1.0], [], [ac_sm_ice_found=no]) AS_IF( [test "x$ac_sm_ice_found" = xno], [AC_MSG_NOTICE([Disable XSMP due to LIBSM or LIBICE not found])], [LIBSM_CFLAGS="$LIBSM_CFLAGS $LIBICE_CFLAGS" LIBSM_LIBS="$LIBSM_LIBS $LIBICE_LIBS" AC_DEFINE(USE_XSMP, , [use xsmp]) AC_SUBST(LIBSM_CFLAGS) AC_SUBST(LIBSM_LIBS)] )], [AC_MSG_ERROR([session control library not found])] ) dnl dnl TLS dnl AC_MSG_CHECKING(for --with-tls) AC_ARG_WITH(tls, AS_HELP_STRING([--with-tls=@<:@gnutls|openssl@:>@],[use TLS library @<:@default=gnutls@:>@]), [with_tls="$withval"], [with_tls=gnutls]) AC_MSG_RESULT($with_tls) AS_IF( [test "x$with_tls" = xgnutls], [PKG_CHECK_MODULES(GNUTLS, [gnutls >= 3.6.7]) AC_DEFINE(USE_GNUTLS, , [use gnutls]) AC_SUBST(GNUTLS_CFLAGS) AC_SUBST(GNUTLS_LIBS)], [test "x$with_tls" = xopenssl], [PKG_CHECK_MODULES(OPENSSL, [openssl >= 1.1.1]) AC_DEFINE(USE_OPENSSL, , [use openssl]) AC_SUBST(OPENSSL_CFLAGS) AC_SUBST(OPENSSL_LIBS)], [AC_MSG_ERROR([TLS library not found])] ) dnl dnl enable gprof dnl AC_MSG_CHECKING(for --enable-gprof) AC_ARG_ENABLE(gprof, AS_HELP_STRING([--enable-gprof],[enable gprof]), [enable_gprof="$enableval"], [enable_gprof=no]) AC_MSG_RESULT($enable_gprof) AS_IF( [test "x$enable_gprof" = xyes], [CXXFLAGS="$CXXFLAGS -pg"] ) dnl dnl checking migemo dnl AC_MSG_CHECKING(for --with-migemo) AC_ARG_WITH(migemo, AS_HELP_STRING([--with-migemo],[enable migemo search]), [with_migemo="$withval"], [with_migemo=no]) AC_MSG_RESULT($with_migemo) AS_IF( [test "x$with_migemo" = xyes], [AC_CHECK_HEADERS([migemo.h]) AC_CHECK_LIB(migemo, migemo_open)] ) AC_MSG_CHECKING(for --with-migemodict) AC_ARG_WITH(migemodict, AS_HELP_STRING([--with-migemodict=PATH],[specifiy the path of migemo dictionary]), [with_migemodict="$withval"], [with_migemodict=]) AC_MSG_RESULT($with_migemodict) AS_IF( [test -n "$with_migemodict"], [AC_DEFINE_UNQUOTED(MIGEMODICT, "$with_migemodict", [migemodict])] ) dnl dnl checking alsa dnl case "${host_os}" in linux*|*linux) AC_MSG_CHECKING(for --with-alsa) AC_ARG_WITH(alsa, AS_HELP_STRING([--with-alsa],[enable alsa]), [with_alsa="$withval"], [with_alsa=no]) AC_MSG_RESULT($with_alsa) AS_IF( [test "x$with_alsa" = xyes], [PKG_CHECK_MODULES(ALSA, [alsa >= 1.0]) AC_DEFINE(USE_ALSA, , [use alsa]) AC_SUBST(ALSA_CFLAGS) AC_SUBST(ALSA_LIBS)] ) ;; esac dnl dnl checking googletest dnl dnl テストプログラムは明示的にコマンド(make test)を実行しなければビルドされない dnl 1. 環境変数 GTEST_SRCDIR が設定されていればgoogletestのソースコードをコンパイルして使う dnl 2. インストールされたパッケージがあればライブラリを使う dnl 3. それ以外のときはmake testが失敗する AM_CONDITIONAL([USE_GTEST_SOURCES], [test -d "$GTEST_SRCDIR"]) dnl PKG_CHECK_MODULES()の4番目の引数(action-if-not-found)を空欄にすると dnl パッケージが見つからないときエラーになるためダミーの処理を入れる AM_COND_IF([USE_GTEST_SOURCES], [], [PKG_CHECK_MODULES(GTEST, [gtest_main >= 1.10.0], [], [ac_gtest_main_found=no])]) AC_SUBST(GTEST_CFLAGS) AC_SUBST(GTEST_LIBS) dnl dnl checking pangolayout dnl AC_MSG_CHECKING(for --with-pangolayout) AC_ARG_WITH(pangolayout, AS_HELP_STRING([--with-pangolayout],[use pangolayout]), [with_pangolayout="$withval"], [with_pangolayout=no]) AC_MSG_RESULT($with_pangolayout) AS_IF( [test "x$with_pangolayout" = xyes], [AC_DEFINE(USE_PANGOLAYOUT, , [use pangolayout])] ) dnl dnl checking compatible cache directory dnl AC_MSG_CHECKING(for --disable-compat-cache-dir) AC_ARG_ENABLE(compat-cache-dir, AS_HELP_STRING([--disable-compat-cache-dir],[disable compatible cache directory]), [enable_compat_cache_dir="$enableval"], [enable_compat_cache_dir=yes]) AS_IF( [test "x$enable_compat_cache_dir" = xyes], [AC_MSG_RESULT(no) AC_DEFINE(ENABLE_COMPAT_CACHE_DIR, 1, [Support for compatible cache directory.])], [AC_MSG_RESULT(yes)] ) dnl dnl CPUの最適化オプション dnl AS_IF( dnl gprofを利用する場合最適化オプションは無視される [test "x$enable_gprof" = xno], [AC_MSG_CHECKING(for --with-native) AC_ARG_WITH(native, AS_HELP_STRING([--with-native],[produce code optimized for the local machine]), [with_native="$withval"], [with_native=no]) AC_MSG_RESULT($with_native) AS_IF([test "x$with_native" != xno], [CXXFLAGS="$CXXFLAGS -march=native"])] ) AC_ARG_VAR(GTEST_SRCDIR, [path for googletest framework source directory]) AC_CONFIG_FILES([Makefile src/Makefile src/skeleton/Makefile src/jdlib/Makefile src/dbtree/Makefile src/dbimg/Makefile src/bbslist/Makefile src/board/Makefile src/article/Makefile src/image/Makefile src/message/Makefile src/history/Makefile src/config/Makefile src/icons/Makefile src/sound/Makefile src/xml/Makefile src/control/Makefile test/Makefile]) AC_OUTPUT AC_MSG_WARN([JDim will drop ./configure support in the future release.]) AC_MSG_WARN([See https://github.com/JDimproved/JDim/issues/897 for details.]) jdim-0.10.1/docs/000077500000000000000000000000001445721505100134505ustar00rootroot00000000000000jdim-0.10.1/docs/Gemfile000066400000000000000000000002121445721505100147360ustar00rootroot00000000000000source 'https://rubygems.org' gem 'github-pages', group: :jekyll_plugins gem 'jekyll-redirect-from' gem 'jekyll-theme-cayman', '~> 0.1.1' jdim-0.10.1/docs/Makefile000066400000000000000000000010321445721505100151040ustar00rootroot00000000000000BUNDLE := bundle BUNDLE_DIR := .bundle/ GEMFILE_LOCK := Gemfile.lock JEKYLL := $(BUNDLE) exec jekyll DEST_DIR := _site/ .PHONY: all build clean distclean install run all: run build: $(BUNDLE_DIR) $(JEKYLL) build -d $(DEST_DIR) clean: $(JEKYLL) clean -d $(DEST_DIR) distclean: clean $(RM) -rf $(BUNDLE_DIR) $(RM) $(GEMFILE_LOCK) install: $(GEMFILE_LOCK) run: $(BUNDLE_DIR) $(JEKYLL) serve --watch -d $(DEST_DIR) $(GEMFILE_LOCK): $(BUNDLE) install --jobs $(shell nproc) --path $(BUNDLE_DIR) $(BUNDLE_DIR): $(GEMFILE_LOCK) jdim-0.10.1/docs/README.md000066400000000000000000000125441445721505100147350ustar00rootroot00000000000000# オンラインマニュアルのメンテナンスについて - [概要](#概要) - [ポリシー](#ポリシー) - [内容の更新について](#内容の更新について) - [JDimリリース時の作業について](#jdimリリース時の作業について) ## 概要 JDimのマニュアルは更新作業を簡素化するためMarkdownで記述し、 [GitHub pages][gh-pages]の機能([jekyll][jekyll])を利用してHTMLに変換する方法をとっています。 また、この文書は https://jd4linux.osdn.jp/maintenance_of_manual.html のコピーを起点としています。 ## ポリシー 一部のWebブラウザだけではなく、多くの環境で閲覧出来る事を目標にしています。 #### Markdownの利用 軽量マークアップ言語 [Markdown][gh-markdown] を使ってマニュアルを記述します。 HTMLのタグによるマークアップはなるべく使わないようにします。 #### スタイルについて スタイルはGitHub pagesで用意されているjekyllテーマを使用します。 ## 内容の更新について 1. gitリポジトリ `docs/` ディレクトリ以下にある `*.md` ファイルを修正します。 2. ローカルでjekyllを動かして修正したページの表示を確認してください。 jekyllの導入や実行は [Quickstart (jekyll)][jekyll-quickstart] を参照してください。
デフォルトの設定では `http://localhost:4000/JDim/` にサイトが公開されます。 3. Pull requestを提出します。([CONTRIBUTING.md][contributing]を参照) 4. マージされたらWebブラウザでアクセスして確認します。https://jdimproved.github.io/JDim/ #### 新規にファイルを追加する 他のファイルを参考に`docs/manual/`ディレクトリの中に作成して下さい。 #### 更新履歴を書く これ以降の`YYYY`は2011などの年数に読み替えて下さい。 `docs/manual/YYYY.md`のunreleasedの中に一行ずつ簡潔に書きます。 また、関連のIssueやPull requestへのリンクがあると参照しやすいです。例: ```markdown - Always include crypt.h header for crypt function ←PRのタイトル ([#1](https://github.com/JDimproved/JDim/pull/1)) ←PRの番号とURL ``` `api.github.com`からPull requestのデータを取得して変更履歴を作るコマンド (curl, jq, sed を使った例) ```sh # マージされた最新100件のPRから変更履歴を作る generate_changelogs () { API='https://api.github.com/repos/JDimproved/JDim/pulls?state=closed&base=master&per_page=100' QUERY='.[] | select(.merged_at != null) | .html_url, .title' curl "$API" | jq -r "$QUERY" | sed -e '1~2s%^.\+/\(.\+\)$%- ([#\1](&))%' -e '2~2s/^ */ /' } generate_changelogs ``` ```sh # 特定のPRから変更履歴を作る generate_changelog () { API="https://api.github.com/repos/JDimproved/JDim/pulls/$1" curl "$API" | jq -r '.html_url, .title' | sed -e '1~2s%^.\+/\(.\+\)$%- ([#\1](&))%' -e '2~2s/^ */ /' } generate_changelog 1 ``` #### 年が変わる場合 更新履歴は年ごとに分けているので、年が変わる場合は以下の作業をします。 1. 前年のフォーマットを参考に`docs/manual/YYYY.md`を作成する。 2. 前年のunreleasedを新年のファイルへ移動する。 2. `docs/manual/history.md`のリストに以下を追加する。 ```markdown - [YYYY年]({{ site.baseurl }}/YYYY/) ``` ## JDimリリース時の作業について JDimがリリースされた時には以下の作業を行います。 1. `docs/manual/YYYY.md`にある先頭の見出しをコピーします。例: ```markdown ### [0.2.0-unreleased](https://github.com/JDimproved/JDim/compare/JDim-v0.1.0...master) (unreleased) ### [0.2.0-unreleased](https://github.com/JDimproved/JDim/compare/JDim-v0.1.0...master) (unreleased) ... ``` 2. バージョン番号、日付、リンクを修正します。例: ```markdown ### [0.3.0-unreleased](https://github.com/JDimproved/JDim/compare/JDim-v0.2.0...master) (unreleased) ### [0.2.0-20190720](https://github.com/JDimproved/JDim/compare/JDim-v0.1.0...JDim-v0.2.0) (2019-07-20) ... ``` 3. リリースの見出しとリンクを追加します。例: ```markdown ### [0.3.0-unreleased](https://github.com/JDimproved/JDim/compare/JDim-v0.2.0...master) (unreleased) ### [**JDim-v0.2.0** Release](https://github.com/JDimproved/JDim/releases/tag/JDim-v0.2.0) (2019-07-20) ### [0.2.0-20190720](https://github.com/JDimproved/JDim/compare/JDim-v0.1.0...JDim-v0.2.0) (2019-07-20) ... ``` 4. 前のバージョンのマニュアルを更新します。
リンク集のファイル `link-YYYYMMDD` をコピーして日付やバージョン番号、URLなどを改めます。 そして `index.md` の「前のバージョンのマニュアル (GitHubリンク)」に新しいページへのリンクを追加します。 [gh-pages]: https://pages.github.com/ [jekyll]: https://jekyllrb.com/ [gh-markdown]: https://guides.github.com/features/mastering-markdown/ [jekyll-quickstart]: https://jekyllrb.com/docs/ [contributing]: https://github.com/JDimproved/JDim/tree/master/CONTRIBUTING.md jdim-0.10.1/docs/_config.yml000066400000000000000000000004611445721505100156000ustar00rootroot00000000000000theme: jekyll-theme-cayman plugins: - jekyll-redirect-from exclude: - Gemfile* - Makefile - README.md - backup_bund/ruby - vendor/bundle - vendor/cache - vendor/gems - vendor/ruby lang: ja baseurl: /JDim defaults: - scope: path: "manual" values: permalink: /:basename/ jdim-0.10.1/docs/_layouts/000077500000000000000000000000001445721505100153075ustar00rootroot00000000000000jdim-0.10.1/docs/_layouts/redirected.html000066400000000000000000000006431445721505100203120ustar00rootroot00000000000000

Redirecting...

Click here if you are not redirected. jdim-0.10.1/docs/assets/000077500000000000000000000000001445721505100147525ustar00rootroot00000000000000jdim-0.10.1/docs/assets/bkmark.png000066400000000000000000000002641445721505100167310ustar00rootroot00000000000000PNG  IHDRa{IDAT8 C鲏ڑOWu#d\TT@RmDH4?{9n)́q$ @o29B2HW%6^AT2 2bg8)dicncW(IENDB`jdim-0.10.1/docs/assets/bkmark_broken_subject.png000066400000000000000000000002671445721505100220130ustar00rootroot00000000000000PNG  IHDRa~IDAT8 C0 ONB( QC-P~!_.-A CL(m!1kV&y3 bD5O[X5O8^c GG^h~IENDB`jdim-0.10.1/docs/assets/bkmark_update.png000066400000000000000000000002531445721505100202710ustar00rootroot00000000000000PNG  IHDRarIDAT8Q0 BxzrZ\KoMxa$L$Z@ rTŔ p @܁, \HFRWҦz]s p 5 3PBfh=m]ݎ"X6qIENDB`jdim-0.10.1/docs/assets/board.png000066400000000000000000000003231445721505100165450ustar00rootroot00000000000000PNG  IHDRaIDAT8c?%HдЃ\&3z&7l a` !7o 9sv 12bP k0dg,/ p;xe w\p `DFe(ܡGVU$IENDB`jdim-0.10.1/docs/assets/board_update.png000066400000000000000000000004151445721505100201110ustar00rootroot00000000000000PNG  IHDRaIDAT8 0EL,$ұ 3`@C"IDG(+\c y}ޝ- ISѻ42'@DRoc樺"&_` !8벜(Wge|;0mj$d&NvEɡ*DP^?<ݓ3z(GP 4^N6#LGۋݕfڦ^; K'Djx#SmIENDB`jdim-0.10.1/docs/assets/board_updated.png000066400000000000000000000004521445721505100202560ustar00rootroot00000000000000PNG  IHDRaIDAT8S!@%hj*%4~@rcGR45\u%EЖ.4-vv'@d:՘5Db8U[dDmQ~Fd#wTm[TB /gD ̚YȊ;a5@.Q. (rEr^/U"6٤)4JIENDB`jdim-0.10.1/docs/assets/check.png000066400000000000000000000003271445721505100165370ustar00rootroot00000000000000PNG  IHDRaIDAT8 C_Sw̧sd_ k`TUZHıMINdf Xg- HH :⫱^q69K W}B.k\$af^%F [s C.>ܯ q XW7=+2,\7D^DIENDB`jdim-0.10.1/docs/assets/down.png000066400000000000000000000002631445721505100164300ustar00rootroot00000000000000PNG  IHDRazIDAT8 L 7dDl Ue7q  81ffc&gwVBZrv= ڦHZ!O`E )oIENDB`jdim-0.10.1/docs/assets/jd.css000066400000000000000000000041331445721505100160620ustar00rootroot00000000000000body{ background-color: #fdfdf5; padding-left: 1em; padding-right: 1em; padding-top: 1em; padding-bottom: 1.5em; } .res{ margin-bottom: 1.5em; padding-left: 2px; padding-right: 2px; padding-top: 2px; padding-bottom: 2px; border-style: solid; border-left-width: 2px; border-right-width: 2px; border-top-width: 2px; border-bottom-width: 2px; border-left-color: black; border-right-color: black; border-top-color: black; border-bottom-color: black; } .title{ background-color: #ccccff; padding-left: 4px; padding-right: 4px; padding-top: 4px; padding-bottom: 4px; border-style: solid; border-left-width: 2px; border-right-width: 2px; border-top-width: 2px; border-bottom-width: 2px; border-left-color: black; border-right-color: black; border-top-color: black; border-bottom-color: black; } .mes{ padding-left: 0.5em; padding-right: 0.5em; padding-top: 0.5em; padding-bottom: 0.5em; border-style: solid; border-left-width: 2px; border-right-width: 2px; border-top-width: 0px; border-bottom-width: 0px; border-left-color: black; border-right-color: black; border-top-color: black; border-bottom-color: black; } .id{ text-align: right; padding-left: 4px; padding-right: 4px; padding-top: 4px; padding-bottom: 4px; border-style: solid; border-left-width: 2px; border-right-width: 2px; border-left-color: black; border-right-color: black; } .date{ text-align: right; padding-left: 4px; padding-right: 4px; padding-top: 4px; padding-bottom: 4px; border-style: solid; border-left-width: 2px; border-right-width: 2px; border-bottom-width: 2px; border-left-color: black; border-right-color: black; border-bottom-color: black; } .separator{ text-align: center; color: white; background-color: #ff6666; padding-top: 4px; padding-bottom: 4px; margin-bottom: 1.5em; } imgpopup{ background-color: #fdfdf5; border-width: 2px; border-color: red; } jdim-0.10.1/docs/assets/jd.png000066400000000000000000000032561445721505100160630ustar00rootroot00000000000000PNG  IHDR``w8sBIT|d pHYstEXtSoftwarewww.inkscape.org<XtEXtCopyrightCC0 Public Domain Dedication http://creativecommons.org/publicdomain/zero/1.0/IDATxKiǿ3 P-IkĢD B)CN KR춋Ⱥe/{E"а @n^ V?ڄLy33S|nywyx?qי x/dH@%y!$I6-moohTsl%V+*++3I19K0VӧO4|| m^ةxAqZ%P J?jrD*y@2"!u P7޽{cH`٪' HP `dbb#YbΐΐTVVh4=#O͆҄6I%`~~?mrrMtx^ttt$swGoooSxpA^|gYqpp86$)wwwU,>}FW, ,+ρP(PH}R ⌡hݮطQ^^nH޽bߥڊ6ž@ ;wdHHfzihrѣGI/L.@Etuuw*q\I`ZOU>~FN'QTTo381jkkqg ̓ ,..*mnn㗛NSq1xsZz@ oPWW8nzz 1U S\+;?P^^gϞi c޽۷o+y_444p([ZZǏْDQLz!E`6mMMMiW¼2$3$3$3G΢Z ޿od.9j$app$ : &j5.F~,J)#rIDYD.#B_;\E  \]`!Ps~@Iu[xE&cW\AYYb6|)wss3~sUU ǭ9p\+@j%`f>r*s$-?g IENDB`jdim-0.10.1/docs/assets/newthread.png000066400000000000000000000002421445721505100174370ustar00rootroot00000000000000PNG  IHDRaiIDAT8 0 ψ[HmR?<7e;l2{ʱѴ 7C3_x pttRvŤ$yZ"(;!:'pE+ IENDB`jdim-0.10.1/docs/assets/newthread_hour.png000066400000000000000000000002351445721505100204760ustar00rootroot00000000000000PNG  IHDRadIDAT8͒A 0Ǽ|z,m4!`630p`X,%}pG {fk/s)GXx? V)W ܖf_vIENDB`jdim-0.10.1/docs/assets/skin_sample.png000066400000000000000000016470171445721505100200050ustar00rootroot00000000000000PNG  IHDR^sBIT|dtEXtSoftwaregnome-screenshot> IDATxwU> u P$aM-łclĈ7Hz>;}K~k-w=gOs9;ZB!B!B-o5[5}!tn~> !B!B!zjq(]b://'B!B!BEUUutJ!B!B!j|kվyO!B!B!Ըu(١mǦPRuهi]J2Z5xAyK, Zf_RG2}~+tB!B!b˒8>skEƢ8FG](gV!um @}gArm[[kq*+dggcyۥE&T%M8UԨAHPDsR)**HIq*,LXaK_{0us]\ iblc?h7IU$IR*D^U5t.>մCj0PN]!B!BLǞQSSvaϦSN5 VTFlmSo1ZUkwA׮E JJYv˗Ӡ U79o}ڵ޽{_P@Nv6h*-[k,Jrss0 C ϶T& Tl|qyɯz dc/};Ln=> A %Ig/*d ݃<õ@Z;TՐ4BמC6eۘuG&Akwk+WLYξ Q0Cahd"e;X7Π;_A<ߝ},{8nƿȇS 0HoئH$a5 'S%`5r[={pHVi^.[k09>M2aȱ28l4J 4h~n_P$oǀҿg/яERElX ɽY2m6+Ntۏl~v ҋ^ÌG_d"hgH9.Hxzp/1-5^p^t>,~{|3ٌvۦ׻B!B!Orft2ᐏyx.ZӒF+rme/88LpV]23UM6$3cN: (哲|vz|u̡P5y*lKu\ UAzJf=yr<9K0ebtˆMKXŴ 89Î)KIx>v6pmz 4mLXM LHg 4up]دYnꨋ?/0j#9%LҁڅCV2y ECPTZ]d#k ^2m{ y$b6R@;W.B!BSPj;tx^:RMv+#o}XP7әs[JBb!w2S D86~wfgfe~xb8~F8wޙm}'naPMَCo}v +3YLT$3>5vԥ1a/sۋIW5>[]bNWN9}O#8יN ]R/ f9Լw-Q{u!}xam]gv#t-5kQ*\.ʴgqZu vln>!Yi9A ßM{sT7unm ʧ)5 n{agYGK9xEpTzp"C<7泣=tNaO=]GM_ymu<<{M\jl3@nć][):ΘAa|2cvvm7Cs Vu &!0h ]L;e@J.f0]۶0֗9>LZ9)37ǯ w-׭0ܶ cG"A^Ņ˼iQ mߝ1 x!Cq\k^D7 |CQi`vAckCv~;u]3Kkp+DB>L6kkp1[̌=jV.Jclw'.B!BsaLVԌ֙Ax%.λ1\1 C,Hqח>]N/+1Jut)B"uap&nZjN[QTԅk$vjW=C3{{ض=vV}KZ7z8ۺnW;0h\zPv??5> 'hOM4i@a}i=jwN=mG㬱ӹrf`ўx\3Зѽ73Gάbci7J1u>HDIj0b xo ɘAXTMIMiK L?&`'\Rָ  \.RNij׎`fʣ} C^Dְv2F(ܛ0P}>Z̻iN2eEڂk~ƕ zghBbƝuywag h6(aeB/Aj-KTbׯK֖0{Mw3%xO别&I+k< jFk+g8g-<6Fzw%|<>]`4 /#ҡsAj,vL٤􄯐>T̢Q t7eg|Ia`k>5فhmul¬'D *o%<:-q|kUc]!B!DZ]a4h"XXVjr,?~-gz3NՔ"Yn~R>t[MזNPVgJxu@p` =4i0i;lb=096rv0 ?L7ik4k9y|yn{~ގn0B+ȡ}J䉷Kv™+\ƫOas8~D16=uMCuID&DN.ǿGi[Jgp;kСrP gC[\3m⇯7WӉgH;P^ CTg+1.B!B#m{5AEcѪ /c@y2 $4$Z*e0jȺ:ѵjg?' <~xb"^$)J܆QFTzIR7>XnEAWCy{҆aS㓭۴h2[NcOeTΖp)4nދv=7;w̒D87Dp_&(ݰ aprکHONS1&Cuyn }n]hFl1skUgp׳ } mgӵkX  |گ|Zͼ7kw|f5FizysX/ɣas޷W2yQ~1qose/63xh xt~+T]:swye<,:^ȣƒ6wjßi.i8i (ח2< t{id~9jaӮdڇ;-mS&F7)[ojw!B!B]oh`Iu=|s}R\|~ %71?7*Ȳ$SJ)***Bk0];: {J)3h àiq3(jؑZj2¨Ki-`b=ҁTj'5xб-xqΙŗf]C8n[_⣧M5'']u>e/_/g0}AgpӠڅqd nz4n-Rv@{g) cH+?*K3Q& W5 La݁%,0FsǃgнC 6wÏq_ce7s :ŹcjOc|>v"Ig%UEi\6X᣹9gpov54<DY;ѻ[xH< >0<3A]r)X*xlTF7 o=ͿeާAf¶vIB!B!ďd;h{tKcD\r`8kmWv;%h2bfݨFif¸RaF0uhǦMҴPh |ywulw5^@ZƗ߮'ݏSJ4't&alV8 76V\ {fcy˩ <Ǡqr@xkz*_̬ʶȣ5Ξ|Uv0I _W69zu66Oۭ(kVV.jv9Ǎa@| /sFp _=ʨ݋20u,Z)yXf1G_x}l~NQ #7^xBAC70e2a??+0sQ73@tO;Ͼ̷/3pa~5e˖le =}G>;$۹0Z)M͊Tӝ^<WgXyќ^•8 qh\ +^"=*2LיB<* m4ϘY4ړsԮ w*qөVzt92nú9/3ci.B!B!ďhU~7)6L<'AQ(85pu h%, _y5&sk|ęeeoO0OV6kfָҳ+*)/HϠa;k=1MnrztztOss)SFݾ']'YGL(մL9]v1GMgH9@(Cke2=*,@kE ;4 M8 Om;oNaɍ@b|iĢ]^ˀ @fsoUo?mCN6MOZR $ޙkfgq9sZF{6ʾ !B!B;VJ pڎ>}6xV.@Km`<*S}Mf!+mŋQTԙH$^sRģ5_G}[v,+Œ%K)*L8<\Q7ޞR˚5kIRvtF?ǭnrnf?ͼ yk2i  OS杜n-6ٿRea~lV9pC3Vc5X>\zbqfev@}Og^qp4(#{4+y)/u΅_Gay$4[l6&ۘ Vw|ݴ g x.m5eK.'1n[)|~_Qs4`~ LO/5c($)3eǨU f!]T IDAT3g]ҐC;)m/c%`B!B!5nh mC5?J fn<ϬF3 @ksB(eG2$Z<Ш/gkF+ҏm'??, hJcYhؔRp0ӎqi% @tIF>V;}&.qH13S)TָJmEsm*|-[CahѤ66J$h_4?4bk`hZ~'ݝxKsB!B!ّk՗ӀGU4M׫\Fi7mکKu}B)ڞJ-B!B!\vvdt4yke^5hn:Dm4n~tS!B!B!Nm!?pQB!B!B eYB!B!BrF !B!B!$B!B!BRSC!B!B!l"0:aԣB!B!B-)0P8SE!B!B!i0 )-B!B!b1 PRC!B!B!Įc(O蟔eؖ?'eضM /,h ";;@@B!B!v2 |?2HC6ŭ:#+rxun۳ߡlx zГL@oR4 Z;˷Dt}GtLC', j+l^VV4s9Z69wr?m.-;F)>|jT!%sj\?ֽyhl0* ,ǟD!B!,a2ZmyGu<Ƞڳr^ Fn^6lvZk**@)l᧳ʩ^mcnad2퇝!E 񔖖ca[^?ǜu$o}!K+R>(ϫ{ 3g6VR{Jow]1~wAeXy$]I_ey_^H,~^{xG"$HRXXPG, :]>v;a"7[D'yDWړ48 ' +#~AN:{^xS☦npCP{9#;/_};N<'!&O/4gJe9!4Z;'@3ʊ i*DvWb ߇+Cڝ5 XgHf/ LY$S)ټ" dlIښ)!0i"n jXvF_6cJeVS8㸶9?jj*){n.{D>X"aNym23FmE߇@!CF"lrԤuK&FcqRT]PkL6=GZbY6l/Wx'I5&f馁aG<3p~/Kd}4//Q) QȀN4USlq?EaOzxUԇўƳc Mǟ~S3 GEy f,70{\z: (ՎCN8y% ?E㞲5^\r ߞT߱ ǭ{xRfFojeت3~&^pwxkvuQG<7.齥O0Im|ݎѵ-^7O,?5?=V X"F<'ɡ_s؍̙WNw:1>}g>f^Zt$g.b8p_P&Rfir¨iRl^ y)fmʪ= [B  m;8[9— tGK+Cy+G8/hqG'癋c殸!B!e:Ff;:8֐L䎽{2L)l"~~χ ˿fYKI8.1r&͜nf34a㵾~8'I/R P=(`F{[jM[ő'KWa6nkݭA;,_`~yjJA*eLP%7 ]cT}^­lيcF>5O,H. ɗacd{.{s]Y^4k X[!@3&qdu@)m*>dҮs{B^)3МtnV/<k9]z#j>( rR}-?³w 7;jp lV#fXܚe1)s'{ڏv6O9x/@G4=UF3e2Li#d?5qϴ~'~7[w}w7 |Ha$66#@]˒5*/b._ǪߞLgY:tԍFS4$ļx}uX6xˠB&EK>=^ٸ9|ݟaG #R1vDfj7x98<$: cS|5;^6ZNWY.A#kf_1L~L>u/,&BLaG;J2Z<.{1 `Ï2cuP;>۟m7nr&ʡEJE%HAdo_&qmuQsugu_*@[\2A)J)YUPAtB!B!~e:Tjn7gONH)E.HK )WQf2q#)W)HHtD&\|"}6|΂0kֺa:< )*{wEfh(X4FyƴG(& *A{bl_W:l7}` Y0]z!h8^ \7~za%, *<0p}]9"Vc;ڌ 8f+^%ƌ[>_o?d") =*gĕ"aϡPʌQYJLZZJe3CPduwwܷ1gtdSj.=o=7x d5aa<|?BG)L/#rټ%IJs%\rŒ{ݬҳ-r5+ytrMwظh1Ϟmc+n?9|&ΞZ7^1{]L+wsIy:l^ܸu!BoE"|a; |5^[\gb7cXUNd5K[^WB!B!e1ƌmI$'зl6O4y}crċ_VTT(7_ Nw,w?mYXT:-?)y)ۮz/BؒRM>̴?ܯ<*$ʪJUD"".{p1{cK2ld60MpqS͒O?dTǠ %fBC!]X#Mfm4Rߙ͆#aܠb8ba]221t_Wߐɷ]Hͽ̹wpzJJFn,|0aF48RC9?܇=<o{fTt.Nf޻1mlJR^ruI}|0q!L}pՏ.=AVn^̛ ~ЦV~ۘrRw/;7 G}F'U1Fo؞MufTWdegt}9u߬$T^Um?r酛x0)\W}7_uj lx#/;kҟxd9Getll2UyZhqMݫB!Bb,ӱ%ɤx.="jc 8mTi R4?xe1EBuhǁv}.Mef Ǔ'1Ay_lTAדX w<>XfgUAejY6 ?%~g-\$)̮#1rw%y4_ĄAsmRjN$l c',kP5bFy5Gz?=:/?_0nL+9?ߣ:4celT fM/ύym}.~/הǦjVnd x اQ=ðXRS$_Ɔ2g:Zvs2 *}am>~Ccas_`S8HhVWkMu"c?Kc38b}^[ehFl'I9*:nλ/خI\qy5c8it gg3@'\z6Ǟ%v)a8(:+tl?onl+{!TnF%n?,ZdgHox''6:[LZ}#sŭ'k2eTZu?[`{m|] !B!ct$)6Glrؽækin]5eB/x/Ю9j{x1ͧJ5Kc-79s2.QLB %ˉtYwkjjXGP;4N݃WeŒ2Rz=O;?%-m}. ZÊgU*Bywg]TKG]4I[}"~Y4nqܱK^f/@B~XIř6c է3=y;h>n*])M׏?g A7o>[OɒFѤQ'B!B!E1=e:֤Re1\ޫc?3J,Nn _MQW,K4]Nz3n?p0wi 3fonKc/?dn{n-seY uΦ8*VWT}S_q\3P6O}UC"ijVa^ {-be5ɺYE"8o9?Yx-lÌ}cKk9gHT2q&Lޏq 爓t*Ư-^}֚J*K G*d~qO;f:H&STǃ $8/z3ֵ]m}}lK۝Ey s_B@fy[GSѸi~?+&cjqըif-L;-hg|4;گH VK*oά|j9.A=Y erhq̙*UzF{4ΒyϙG|6g;%tCۋS#IԚS¦iYV}50ï3ec,\9cm#{~MI|[_whfrZjK48/O\}e|H_}3#ς/>`܄ili,Z˖}蝘O- `u͓㞟vU]iu$;sfd`QC+>,4?5q_ȡ4L믻YlA {xl?vܵi$Ț; T&MW;rMO%) ñ#*'I#zI0>G] |;}N?yϞarbk-cnqRI\}_7MRtzwFS<&sxo7P2.5cꋓYm=nn{^lW~2 u˱}ӭE|Ic 7{{+OHR$'Sو 8!@څlRCXC0g,68fCEI|8;nrզqrvmwd8o+qǮ8<4nך P;)pLvQH&:ğ=~""""""""E60n*ge% ♑DZ0v9=e)(p}|FF9yy9̕5)q[qn_T s]2We:#T9T 00'T0plpЇNp(!"""""""""R[ɜ9-_.$8WlR4 w+U(+sg8 |}2o=0qpmVSZ l''ͦcSLBq*M'*Ɣ)S3pgv}:ig1x_D/x^^ܟp+6~7USԑ}5Tۼ1X\y\]ې0c>/õOy'm_=NrĦ`<65o Uj72 I8Έ2K$q)& <2n) `Z-%KsJ%Ĵ]qK !!WBBh[ǔNRGj/0`rڴiQºX;G|βsg+޾~}4ͳr܇_ (.8_ȊFwnUN5 }Ȕ%| Z 7暎`p?UDDDDDع3&W'`~V~v~&~jiD{FڎLGqwaSUV1*8! ѡCgp O=„a;04C+#\p|qBz.{7#{à=H4X57f4u4IrV0}^~L[c&؋KxV6l I?v̕P8}ʃ+?͋]1O=3VG,͎rz%ed[H^îTwGzߗcp4ieC?S!w9^zw-'p۸SNGkKDDDDDЗX@YeqM 'IB:lxabFLTWb#.8S+x2/X IUJOF`TʊM0:&=?.Q J9cڑ[=Ȇoɉkig2:qïrDVf/ݖO ?̫zܾXv;m&bXl|bgzXH85+ZE 2"޷UU՘dn }?vXv hU46ccN G^!==Z)lۺ9Y5N% s^}^[,xSsɲ Q\|=\ٳVI2c;W2W+gIF?̘j{"q ?\һ6n1Wž3n1$,Ư_tO,5{n/,S1[usrmҗsokWff'/޸f&.k5^uLq=ハ1=GGu!ܵw6"Xۙs}.~h=Ou<=A6L~'$sa==5vwEoy88w<?}L8/a$.A9.Mp{R[+O2-:&:dt3\yn644%o_9\[""""""r5iSԿV `WPm},>Qq-|20qѸnܕn)ٶx\n׋F]v&N_W_ƿ[(2/{hV} {}?om]d|37/mv?S|_xOQ͔Sj5˖~\ Z(`[w8{k ζmG]\#m1o!_Cu"pƏ޵Rŷ r~401S%ɫc&?pԯ掵l[{bɴ٬{#ݤ01 HvQ9KHI06CzQF`:9{GpjkN4(w#*:,y?螊g{sokVQ,a^r]#ûO}G7oű׷xx<3w|9)\,^{nz//01^V}ggOI &]i,^W@з?wiۯ5Fi?nO)N6R.ͳ>в.|=k6/iF(m5=z_ȡOt~wv$V /}p Na!KJm鐪)Y]\'p۶0h_C aȐ!7n222h޼yƴ fQ(k vV^^Tb[cB7+'Bw ݄-[J|(ӤG ȎdHNϣ_M#ͳ&ۋǺ&J}ン̙_\/Ч[s\1TRn'G1[SM~W4ck'bo~ň[}w]f/1Mzq7maBk+oNC_榁@3~sL+}ʞJΊ9&K>tiAPb޻V;;gT<~ 4;Ц6k{s?%_ym4]ZyJҿWbΥ oO\E1<6#Qs)|^Nkَ +psӤ'}{u&4=3^NfY'sd<9Sna{Ɠ:Y,wÌc;]D~-wbظ|,`&KR+S lC0^+[ Utnc`[vIhrHLVWt\nJԌzwL@Ϝ;Gݳ#A>j+t[3iYG\יrl/JTo{ =KdT81sb:䕸G/Й Zv;݉dg-F<]v%.ΥcqvU1G[E2>xe1]0p2(Ck]FK{iw"[x!l߸) QX@L6ۦttԌ6B5 l)0י)ZQh%#]] h*`ڴwtntja·o355Hv)g{a33^"㻱YȉOMmq,8Ci_$t+5rS׾\{A#?Gco挶&S:BPmג:5n љe{f~xll{C mЎHMgpյd?-*jg4ܭr˄{x;{4W 8$7Os*7]*'ft>բyX< bk㪠b(f1;Ҁﻓj̭)5W}&\mލ[Lz}:G1x@wgcnb` d%Q1H9s$rlWEN,8_XʻñwQY~-i G߳<3HM =^Ԋɯ?-o4kZ]Xj1dOJ}Ϟ9qY,EI[?*""""""7äY=FRx, ;`s8msx IGJX`R_iUcWmgҝDi۶1\fhpvO/NVv"|ڴh46kѷiGL!8kw(*׷ N9n雈Iч|K[:P7^~Lh?Ez۸/kzZ `Fk(oǿRWXysڱg:;;y*~qy1td^}cWx2q};`&8$˞fVr^[T؏oR~~C^L[!Tݡ1Tu-.㑡[xQE/4y!E^y0IhЉ~-s eۆyh~7_ɖMwrHb(3Í w9yœ _/i6ru9}+>_#sW}C^ĽPPar-K7$A=>Mxn,o ~wιan*ϫ| t8BZX`aN.:g]Q{i`hK(w$ϿJw>̲CT_jLt}iDsdeZ>y:{]C1q lm0་WGgddмys֬J\PgrҩW)S0`Zv-˖-c_lM4jܔ½??/Ӧr)nmVU1+<-@;Wrvpzņ.`׃!\W__/vgr45C*"""""rdvֹasv6|)D1L֭* GH>̈w×YaUY;I0_-?w/2qlhmü9*ݚmc;..vJWuꖙ>h5ub#.`׃!}`iV/#+&#Dnk+c+i IDAT˜fh^EDDDDD Z9#77ϓطډLǾ&y+kVcQTj#AJ~Ȩ|NQ'G$Ȕ)S"E5ͺ1l}s0>Xj1[bit}F~> ͫÌo_ƘHF0 x9Kj [~^.s~M.]tض =hjBXL2qX0o'?u1 x֯[d~a:X.4MHtHnRa a%If01Mf&0\Eߛi1 3taZOm`nZ6jmx.M7e5dL#DDDDDDDDDa,_Va00q=%;+hШQQ)y 끄şNFgVFC_51 ͚ 2^/72o:$Bll[ni*QSL$)9}XUm5~Æ4n 0ݤi35o Z/""""""""8`V0ȁa&n9a`.ZnGmǶ,IDbYu2VXDDDDDDDDD:)-""""""""""54f&DHS2ZDDDDDDDDDDj;6n`úxD0Cz ߰сEDDDDDDDD ;vlg˦ؙ)(˲Xn-k2EE\;@$""""""""AlݚTCIM04%EDDDDDDD֌&N=E884hԘܜ'akF\.la4TId4NїHua%l2:VHDUDDDDDDDD/WFS4=/lL?;qu9mى+%Vf6fiEIx/\3rBHN@rҙ&ڝԗb=ٲ`b*I{R ;bVvEܾQ:ZDDDDDDDpR2}Pn0 z} cB%ْɏX5aA=Þ&&[~|W\˷u½;!oCn5$ЯW*ұįXqQJe՟O\F oF;UKnچ=_OcVxReLp.RUE י<37<=S0 ?^:FU~wy[VDyNO|*)n/,XUq&Im6)0:1a4CH^XEV\#Y*==GaaAگxᙙS\S=i4<o¼ƃxh9{?Eu=I*_n;_tJ_}^ݒ[N og>N(_{< {Um? zZy-ګUDDDDDDDD+(fMvo3fw-޼y=*+S`[A,``6S~ܿr % ~{I+{0]ٛ]Zq-&`q=nMԮ]RmVLG]!C0dƍ׾4o޼nw!Gr9-{;ޟ̦ ,w6;O>xtwC־Or+iOg;|uC+^3;^эii_WA=_vbW Fqc` # 1\%M+'Mx 7deKK0bZ|4).v~l]fnɶFgpڐ;XĒ~#萿O>F^sCh_s 3y~jrx⫣pEANOMц-x.~8 :Tkij`mkbx7GqT_JE5kJ=ojՊ( IKK㨣t"LGU2Ieg]5VZ+-RVn`g|#M=\p"h>02&'m6}گp=ߐZݔc̈́\yGw_ll3[Mam[<}Qz<0N?aWF=88k]?uw a8 mPn,~pG{٭f2j)|)i:T=P3Ǟv9`dθwXq \.:z\ *1UDDDDDDDD.gq:[Fnz4ω'P"KF!}k2P^iM^^6&ҁR-:bߌ#Nϊ-p 7|'l=yv/;B>_ݯf^&ތ5%V~gV\x&{z`N)>-^RtEb(xWLmY8TS}6?7^5Mb~qv|V.G ڜ|H-}nG{wss-y?`QgR;˅gt/_ǟ7Qjầ$t'/Bw.-{=w2L=YD jaDէs lmz*bϥ\[oMZJ8VM#/Q1ɋU ˄vK'򿿏N1՗p1sܷĝ6:6m5w ]=1NM]1/ *4QgH9/fܳ8T Ngqx̤vt&IGrVG9lߘOL=NIbc虓o3Jv81fL;Gq`uѤ nOi6Y[H~-O?'Εz'Q=OOj/)CsdZ@ao㯛8PN7[ٰ yswvL}!/?]i:d.jD^EpRʺ`|l[Qe_PUDDDDDDDDUڵ9O`Р*(6sX^w3i6*w,~<~&cuSG~@?sr:@J>}yAf'/nȣ&3.~as{нy[5LN42^Ks&л!%=M8tJ]Zx N[mA䔔20ݱԪiɪz Olˢ7pOykg.?-5Yl2V"-V'm;pl}[an\e`.&S/2b`THqvO OԍGq~âb;#׆\1Q  Oĉa+1|5{ݮۘ]PV} ;gا """""""""SN*)#Ue[|='d؞wm]C.B`90&,ji]?W1\@M$hzp;OmT`& 57tI~CEdeX`&#_6ѥ5𦶡sj#fqc27xAu(H@taZ~=&3o';k_w^t5?&-[Ѣ}zz&ƾ+n쳨JH'$`eU&4Kޕ|mp`/=3G˭gሆGU6 3 Gۯܚٓ&jFW}C)V.HF}*~tDxapU7aLyoaeo8 ь+xyxbb].B&JYtɥ ˊ9|+ t | `hzi~*ODoEW9n%.yw-5bZ0COX83hu_ҙ leq|4/=/Q'q#HN6ZCssKgmb׷zs|9ML}.Kzs{dt[5#5r7'|_jv^gzٹ#> 71 JPPf2a>;nu{µ\B]s^@jgysfUSd*_i!wΎ%6ٳv\)vVGnl.Hhz ,g0鍗`sF6kxf n]A~Q Co`99RHnՊi#߹ n`}[!w׼5vtx8ӔQ-ccQ|`}[;cp~fÈ"пiwqoq@lzH2qsرr*bYM>y3hVj~<3tNhz8\9r8'zeuʪ6~E}zT] F}2as禗?XEDDDDDDD䟯5222/J3t-ύv?kc#V:DGc!hӣ>G_yP17>ČUs|W?)k=z^ ,K_ Kb^/;>`i%Ɓ4iqm DdYL{?1ǣ]~cՎt| Zz7xLgW ՚`A֊_j'PJ׎99"I~pww>/PڌOL&S&9GMͳycy*T# o㏑ko9<;=~d)PX՜_\5 F#O>1o?8eoXr7a:dYroO_*iۖ_`k zHPý^V^s`y9J4v8g l@$ISt@(ҔzMӬO4H%ϧs5zH6 IDATu4B} TUM;;;-WWWzM(+zY [7+)aGdxƓpcLrY4,=V5ժ&H`0`ww -q-IP5i-_j~J%HQ!M=:yZ;E/K~~̪.1sr|'rc^bji IN5Gc|}{ԋ?O""-^/$I gX^7/LkITN+Y.~E,8{K<-G Ȳ֫%M+r$Ax^G4+uVJȋ*I*?Z@ӮY׀E+'A0&UB۶GKIߧΑ954MCϙm Ie89$If8?OǧiJ{T" A`A"|<шZbښ1uӟ^+ Z`M, @mݑЀ 5H{@!U nǫ_D@:GLc|H$?8>fg^}hYm%G:ں%^cc=mP !@Y4MC@@? E&Sn+.^ r3-Ō,Q,iG󧜟ӄ,+PYE?pw`E)1?r[ҝ`E׫+&7W^͹ߐ90Ә@ף-iQ`lqO{{o<>$ô /3[hZ+k#(JbM'$4MȲ } >XK;c/^0 N [3z0\cZ2%7 %I4yBpqug {jb>C1cZ95Usú5X)y9<ܥ1 ,K@*.ί)!,^ U@" :aujjTK\2_F3~w|?AL=BHYVT>C93,laLg>{W\BRB$b4ݒEP55[6"")kL$i4{zk 2*L4UUm[38@%w!ZDRYv{ϡkH#B/eYsmR%^#!lE{/{f7t(1B I-w$ɖo:NWiK)$ɺEʪihGf"pXX.x"eYIF A[щ"U>H-cr#b @ KdN>\sS{-Iuz&){#>>~B"ĭmC0nٛ/rq_H\H!"AE3+DP>%-J0h֚bx/&5BE A 8+1l@n"tCI;ruD@#5`I"edI)m]E5 ֫ZQ:@[im0:`褠`mh BQ]%#҅D^E$MK!ZLo|{; Q:Kx[hHĭ(A*!;❘ȁ#aѵ ^ *`{WHPwqŶDPTQ!RDrxkt7H8o~%7w*wgr+<7>U6,yǿ9Dp|J0}LS^?S-Պ{`0b0\hcn)c p2ӗ\]] UxAX MWfiDuQUضTh2^y|79_OXU7\MHLY. {;XujD[ QE+D؉x H<"'IXq$:c4=>RkϟU$y_t,6Ui3HXk )$ˣzԴ$Yi"Qvȳb1#iAG!N`X0^36" ݶ>,-]b7؉݆ޓz+Qƴ`"%U7Zb:19#puuWqt!7SUsQl`+,4O(>5AEUvkں!MHuE%QZGc!wRzI{\Zޥ%ȕ Aw(BK,E6{DMayQDŽUJ2yHcmɲ$we<3mhۆ,KY+ i-BБ,I`(iҶ,+[ZS5"Ϣjg9Jb"~d N2Q cY@~S)̾@ fr:Њ')" oSI2))^Ƞrvv8;y^9ۄƓI2^S`Ro/|v,( 6q-TnB|s,LX!Bw $7D$I+<J(>B3xt9ZlT_A}}}U|(ɍ L,ےZ*%EZ)"P |޶!=^RkzE׿5^_4b\L[H'K8U=p.EI El^:P]3n\l$k JD[ö-"7Hۻj(bט6ViE6,oIRA?Nak֍-I»ö́Ut>nU-D[5]5l{19 Ru֦>~.㬡552xwxv- {b&X)RUBmب".ng d?ߒkx%oɒ􊦍j۶:>D{ƶݢLk>.̦WH=|ϟ229@ǟ?frN@C8?TRe?Z)Isb[/ϧL^1?ͳg F='T)Z vv{eRirp (ŵ2, ={Q9|zzrdX0ypz?P5kx$It| )JƇ( lX,q6Ƒ)e*VUE/`sNWr ;c{DgAeEȀ6?z/I+Y!䃏>&}|w@qOv[q/(}NNH %ق\'dYhP7X4k}.?ϱ!5V3~LI~﷿kxzw=ZS i4Kx٧?Q!ß[m18'PW+z=ʲd>3R5f>cԌv||ŋemZI`8/J>zJ;Zp. hgw)eYru~bd6]P j_Uܗ('IBjE*feGYAɀJ4o[ԫ0g\ϧ$*e[od9_1_.BvJUƘɄTkTs 㰦EG`Ib/.X^jQ B1ܐ="g杯. v/m͔ވa'OѬh)~4XBJ$mpg]RشQNe*P*! 0:OHT& e YƋ4[nuÚܶ(kjV9s۟t B8ƽt\Ӵ$U$9u~rk"yR'5W]qqk΃ "7@'t*1n`:O>RPZ(Bpttİ7穪5uB7b̗+VֵHۢBTYEk)cY3QQbQIhA1i \ ;jPXEwmA"&xg گ-Ć@4i =AXa4 \ Uk{w^{Ȼ7 ͊i BGbΰ?mkDrESߪ@ QM$EGHK"@tTu>lx$xT5}+وmocܬ+V96,;iVf%"#Eڝ;~ü?+Pj]N|Ѯ#ܪ~gFv?_ 28f+9<<䣏>mT;dd]-Y-mkUnnf3^^\qhJֵŇ$볷7ŋsmrPrtt:e2ږ4YjѰ-ISWK ZzmHku 98Lx/'c'?&Ղ"OxGٓU/%YPdfkvvGqrr_lzC^#@S5$=RKKmqTy; m /H˄΀WKZ yb@xGB!pITQZKjx\/AAq%Ϟ}#|rq}A] ~G?O?o Lϧ<T%'#NO 찿;䳟~@P}%Np!A|9Zx_jjlAdgcɇ胏НrtV$dog[w˨;{'{oc cP:z2jxiQmCs}sdq'yLqttSmMcw4YiL!b\b]C\Of,SwRrt|;ۄ'4%R*uMT ҠTBYX 0)!6xl7_8msT +O´4^-˼jÑ7E$RIt=7‘hn[IhH"ڂ)!lQUu&I ]Q<7Du%1qSOI"Bd>kEk!VY<ː^ңt){{'$YrZ#`aZm=H%lQQi2xx- ֎#MSF-}°kFu*i qуQe;|7S(U{>>(lȘ)!%ͺ2!W^HnЕc!ׇ[RiSBlh5 WH l۴ {&@)l$qlK11q_TxG6!$R'HQZGn@u뚭-toLHB[:viI4ڦ) V=nyqZ)wT+d`r=CeZKcMv3?IΤƈߩEqP )-ek,:k!+_ ">u !rf m ^<{u"su9i^F^,IDk\s1 }_/vyn~=(x3}ʽ%7~&G yQ͚XqFk :ͶޝMVu-ł<>{{; GktΖSMMJ-xg`oq]GOUᡳ\-nW./7Ϟ>O?`woN<Ýh?g*-9b\ՀT+& ْǟ<ƺ;C&S?t:omXxGMLfsoH2Z EV4MCR{2ԁ2z%M$MdUЩ om݀"]K݉HvmDeJ{ .6Ӌ @~UYWW7opgA_ ӊQdxt#2/q5G!dr~qޠGZd9;{}Vp맧mt>磏>y>ARqv|GoP55/=Ox%/_7`988 K5"-yEQp~s.z+0_L.g|)=yL>(|H~$4шZacZƵmKQU577xJ"/AB IDATD{EcY-HY訄wcI-rG &?,Kί8UeΈlr1cS"5ez9:;4k?gY(z=QӔ$ .ۦ Hސ)+Қ% WF7FPi}ֳ[> ,xazYss5Yfӊ8_1/9:C֋5\>㗌5=-Bg!$Qq_xIIBn'VIˮvEj#qrzrBhuUl$.ł 8XGiv (! [ҒCT{qO΃YmcCZ ^(|7wS|O?hLC irobQm-3 [3\GUQ$YXvWpIo!1_fѩ4RWwj! 햅e_BLE3f & %QedPb׈9uP f԰{_g20"DTj9Z)2UU1LYLg~;mϰ\8mwחe#xħ?ea%o`0/?sN1SN=j[~/sV9r ;qm8=<|GrҲ3dxԫՔԍ%Yo~GCAi.npB{pJ)?/|_k[L2NNn ,E% dYPxuSf'O3~:%kOc TՊb|5Z,Ȳ -yQ5qfPI=zIJOizKFi, YV5LuKVRJj ϸ9?|D^\Oy0>~W]p%nbAX̘\k_\M.i놛M/-%U]oeY"f6u+A$uB(ƼN;L45T~.z+OYwbg-G';|p)b^uTm [!VĽ[eZca>jiZS)ۜ"+,o Yͯ+˲{/=p;o;ⱌTyY {Tg4_z}e2Zq5P!EEeyI7rR(k,ZEK 6TlMf ' &7H-Ƴl|^=jكo!UxwpG$Iڶ-^dIwUU[Dg[=Idݒћų^`{4͖>[xSؼG 0*():}!AƉ۪s8/P_&bejw.DWPQ^]g:!qػ $Q$l[4Һ#[m[ Y-)Th|"i8X0WlHўi)slV muα^1ޑudt@ AbA %Rm-ND׽tp9 E@p!ljƃ>YNv{o*aRUzjxvqb9RJ2, ZSiӋ=BH$:C 6~E!uKΟ>,)IHEq 1Nb7ʭTIT #n1}o Ώւ(Nt!ЉB k !zbm鮲+{> dmYdZ' uDcEISyJa'O3EkG"V <+88ܡ{OruuӧO)YVPRj&^kc Ƹ(`8hΨ 9./_|ócgHM˼5,VK4eo"E 0d>b䂛%ŀhXj`r'}JL$2u/P>;CeJ˗d7@ ɪ]C* c<Y^?(2Hl3!@UUmQGՉu`"ณ}|h|s'&xK4MC۶BgzAIiV 0\D5R*Ty/QJam7v-lrh D1p}zH:{ztw&^`#snfg-iq7W&z.tC,X`ҌjR,J8T"qFw:)Y/Vd2h}}~N=kHL"^43y|tZ jgJ@ԐeEQ@75ϺOpVш,lrkBM)*.F|6 چmh㰣Z* SٶM *':Ͼ_hZ "+h!-!E􊖑\pdsD#vL ZiYKfOy[Ξke-͂ 7S-WfVRBvH+hM4`fzz%+/YՍ( Ω<|{ʒk F8P uՠTvǣLWz{#:ZIӴH/%҃{b q"EDQrpp$I HPWeilћazW$0ny1QB#=Q /,Rx:bX =Ǝ,ńoos`֫Ϟjǟ?_wh1,֚ޚQ8P|_g,΅M@ /aWТط^n[ܛ!<2p1GRP,MR .0W, 8aB "| #Rޞ^{w\JU1se6x{\ƞUeJBF#F k$I?s7l8+!QLs߯3hw ȉ5j^A㗭߯_I~s)219kʮ(V{Ѵ=.%܎lՒ/_)mUs||D]!E eNO#RZFԍaY#DC#P^qtt9i2ϸY0GX ݆=fueE\ih6.TITh#XVuE',Nȳ٭ȋ Yqst4#=]א9yʚ>hD|Һyoa}sLAKN'~dŞjst-ZIP*:#UNӌ4Y5?\{8" 1i?:Ο{似^\]7?S5[:!BOP5?zE䛔oYarɓ=z"X(c6* 1rCAn[6][O>)"pJӔV錪LG9/] |rċ eۑ%kK^n Ng'-)j/! 3mI%<9 8F AUxEQ`\OS8iL&c"JFc/%xA9 I^9Ɉf\6o|pDE JHꦥ7o&{rxM[:4)%i:8kߛIDͧ uӡtwt/hl̻I~qMx'x{,ס J~}'6eGwF i@:?_gGx̶j1X/p.mwe5U?ӣ9o?ۿ+O/o8}xͦȠܔ4wh[oE<{lPJ28}ﰶ cITՎ(>leNs028" ӎ:s7xrm!Cp^3,9UՀ%b%)n隶iA)EEc>GCk[a4U%g/^$qCC&pn`.oqR,D!ex_z箐7zGdf3NNNᴿLbt&xo{p.r)4jfӳp4Y$z۾m5"{qdQvt%)[QHta18sq䋌*EE%<0AXi3ԍ͊Ŝ4Ei"J ܬKe1:.p^ƘEhIGs/Ҙa˙jд=E6uQ8})yӯWl֐|pW+9 $Js^䂫bDW<}}b29ӣ)xaq|'?=y|LOXH|HyF ےIRa8'''O"Eg dJW;>3|er4/)9jkDŒ]W4K;FnW>w шdBUUmM1(˒򊲩I15=~ gܻwJ>xjsN%q)d8I:S=Id2E(XT͒< \AێmÔC4ure2>B$)G8Nԕ5gL)i,黚jph .闩wY`}=~<|xfEHnOORruiwX22Ֆq1!-rRm$s`G|)˚~|gdT AUU\]]9햶1%J<cmȗ#*:@t= Hqo i p-Vrp2o  yH`QD5Z>ӝC[uݝ);z>=]WS:lR#wq{v41W-m¦hKk;=!R!dA ݐI`[D1^o嚮3IA:Z0<~)$*&TPv~ËO‘:1}2V[G̝Qk-Άagk UPZwz(źզ"y>] Tu-JKРܔ^E{1Da5kv-1] oM7Vpi @ rh#\ka7rbh V1/Ơuh %RGsFdT{7Żo qZA$Q@8^\R8N!aTh[bPm:\j!D k d%" g`ƺ!Z7*7\ yDu-v o J] ̄j( g} R#ɲ(>0]T5kñ4'"Вd oƲ-Ye?ŋL䓔S_`l(_Ȑ:f>Ͼ`4q#fGL6gvr+G?Dt5i8]P5IsyQ0 me 1Pȋ̊ϟ?e\r!IVc֛k /6-G t:zg:b^uӗ<-؎<(& B qUڎfMVEbfYNי p 脫ͺe2;K./ʎuB`6TH4N-cQ0&tm4()鍥[CS,!{29u|'#!qszw [Dc\hR)B(vk?cAH5K֫G1miZ .%K $o33՚Y6a~ 7\Q_\g{{XTxo # ɀ·qT!Q(p'::ѣ8pyKZeIg2"61’NR;A~>>经{aUsWJ1LFfV 咲,V?_]y6 BE8qU&IXy2g!Es,6_Zk[8c㈶l7%]gMFy>ΏO_\(F"$ȷ7,+5=/y) vh􀯴suDM 2NpXu-[n+qE <!e41咼(lnJmG_fE'T Hb F9BII:"#)9y`j+NIRg BcZXYh;X%XB(!4EG-%b$-zFYNUU$f:0ѯ[/.x%I4N)VՆrt嚑|YS<'HҌO*fͮi*be͆xmۦ˫sTGG$y;,˸Yɗ46Go6g_rz4f}䭇YMHId̃{GlK./jw~w8:^е,ˈ !}.Y'³Oy}w~ /I)Uaèa4g N^kC@48$Q$[v(Ԏ~9Jf8jX-uԛzSoMr\iPʳ.4EJKƢujXb (ᚸGv}7!8cݲږtV0̎͘Mf{0 ؾކg1 8𝡮{SJƤiY7n|<ݮI93Q1c)EQ ,L=H\Eb7L&Gww%|y}w^]Zͳ%f$!~cBA淧! Pk"%cKY$, U{H3Fy1 B(LS{1?^pyu9 MӰݖށWX#NG._/WXqOn+T'!cY.8 aJFw HΈ$CmnxqHȦ)=4mMF,<􆾳""2G7m8פ oQB'јŽ9*Uęu)<[ z8EZ1t( >#ٴ$Vް! ֚4m~!$MU9uS~sfW,=Bvmv_>cgv3,ί6\\ZPWc~Uop]E5t;iL,>,9;d;[g]K94T+,8وqs~ao-䊗 /+{|wD9RATU*)Pj fX"{D2w}nn0" q!NJur<=m F*x^9sqB~xL&أ~ou](hږ(0}#Vz5 ݚ7H,brs[>,.vH8)%:R(#؏c(q^a· [G#)/T-7$ћq7!EM4uR.k^\\3+~Oy ]p^$?)ms_ 2o}`wS%dĺHq߭yӏYĢCFYTaG8<1LyFoZvGY4S>Pih-qӌhĮ,ɳlb`4^BzM4jrWRhGHpyq_89;cv4G( rc=:]i߲5!$ASlwKFYE!ZPgGلHtØjMFJa}$A Mն" AQ5h~1%Y&qER6mzCkPЙmpHtDjϞ?F&O>{LQd<=÷(MYL@uhA ziRא {;ugmu.D^D 4 }gU5BEi> u~8J%)q cIV&ash\}]!X0">mKeWx۶$^t5rw673nC 3l p8 b_[toǫHqx   U:ߡp^XF@UdA[ȣA +JiƂrh%pc}sE1:1a$2uxO$ױ;8Áj1Fw8 [y4~+s{p?`\ρoo& KP7ԛzSdޥFiG Հ xGttF:tA"pZ$%؊!B 7%)!պb(*cEA8r" )t2cG/[u=Q|lbzch:eBZ%mO_%i:G|4և$IH4-U}S)ۊI,[ۭW_4,Zm1V9Mc%s4UKߴXI~I - T"IJ_Ԧ,#99B+4!sPag (Г_!B_ 8GD@[H0|Sa3BlٽSvXcZ:aLF`cX߁mUUp̳Ԭ^py~ͷ~لſ243OU%J V- -DqfG|dzDy^`떛/G=bM?BX$xQ%MYe4,yg|!ۆzA"0=ґx8%Kb+o{NCH'x!8|qNԛzSoM_$l*%PrPFHO4Rko5~6(50DFDG2\ǜ003զx(cׄqv!pz8"y[Kc4wtmI$OYL߷i4bh MU ,EqP[M%Z)̎r"QI#*n,jD,uoJKUtMӚFJBAha+TmM]{H-&Dx& fkua#uYHӔ(J¿6-|XStA t6u%XMCGg8N֔M9˗L&aSS4MCi zyI4t]za4q^#gr~uŮae>3NO0ag\;(&p|<{k0=omlYU4[+)]ex NO)1r@9;;cOmKrq + }q<[פ#e´BMy]a{cn›rG$f1"Nٶ-YSd1. ;V5irpAHF 1WK8(Y"?SLQ52O_>gOhs49#}RfGgA5#O"H$>]gx<;xMl|oq8⣟|D%aJcT_p4 >jB;Cb1 (sVM@WtG}]<qqFNJcޓ1q:aW(9::xKW G'HeBYxxA]t+>|/ϩ떺ji iZ`"\O8F {rG۔H\p^Yl7UO>oNj3lCd9J("c|`' .m %Q64n횚lDb됃ᴵHҔ<;v5J(IC_X*Z/haqzF]ݶ4 oޛ=qei~{ AVY[kl$ld&ӳLOeYkU$Q$@@"÷zD@Hp`s6 eg  @wZkđMΜfeZ֫0on͵tUcX-נ4qR?iDExt33vDn RDv%uq,Bn& FVZf$kr(%-' B-ɽw=7]U0ˍڂi#87i5l(BGQȳ27$Ήnj_-6r^pBښH%1"1(U oh#zJVyJQ˺g,1(,׬&.JRbRFMoU$yE8Y ޲~%DMg&Y&a:aV[UDAsȔ+H uCP 0֜HH␄8ׂNĶBmom670%JKR1:Rm9(%tDD$!MqH?VW|o8ߣk"ې VvXN%1[^0\\-8 8ZQkڲdt~#kv_ܟާ}o߾i!_ûf*DG9ٔX"Ҙ$6C۵j J(em .(3)8foo`@DIB$Vcsm)|E l7n_iF2oS[kjF{ܺup5R(fQB7QDԤ!EzNgu]2{E5/7rJsB5ih*2ϿhiC{Gyڥl"rjk(%ꇶrEcb8}>G)%a$s] a1_,Kf%7bz>aq5(V{=,we:Ywy|0D)^Kt}1?o;)ٽ|?#ꂇ]Pg"S,\̘\s>D ^pqF݉I >1>b/23~N2K%]Vݻ9v3t?'OAjJ 'u]qzzGcޘY\-jT\N)ʆ$[2;I3vb}~aDS,*\sx 2{ItFi9eU1WWr3HSYeI :u`=Xc:I(cT|CH ;KCDq#ʺb(mX,/%6ܻwĸ[qJy{Rs`'NBJ@DY(:.u]ij4a-Zic3Z 8 :nD ڳ? j nμX%8%; [vHᄠ(+MĨX%80 +)_?B8nM דc Ȑ6k/'!Z+w93b@ xg1Ƒ%qˆvxoZtspB\"U{YmсeV~DD|omC - &uG C² l5urVp89?9B(Gg)u}8KHℬ*"d&V n`hr|gToab>;N(bEN ' hGA'z(nXNwx~rƣOy KfdILUX,':&R3$ {cf5KoXW]NuStXA8lJS6hkeK8,Ni *J il[H1~ ƦUzì`XWu`H?ulw/0=͌ou]ϩk\uuB"ut<ܺss.w:k%ggg[ƻ~5:8c4<{{叏d2CNጡZW8cwWXm8$IAץ۽ 4KSo1-Q78j@Gf N_ErVqvzs0}<|tǟ3~AxkiHXLWg[ "&8k,DщӰ6 &t`, O=nhj z<&bh@{tHDw3aX')'K:#Nc^0])CQ5–^i*>@G'NtJ/bjJ4F:lyN:}޺ŰQ .4E5'OPT%g Ƒ1>ge7g- s5Ypq1T.vw)%狒x~:eYV8gpJZ\ΖbS=zJE썹ܽ.^F} `0x>]6Aϟ?'֊Uz e~F*./).U #1uS]5L&VE7;q&7o&s 2 Z*^x8'"Ι\<~7n<QXi?RGHTW8C 4h-}J4DDRR5eY\$1eASUEhQἡ,˖r"i`gNq; ?+|ˈyL+)Na. wDZ2 9x[In79Gd0255e}Ù`@Xc rdJvQ F>#κ8ohLMTPao٠ F¸>h( Xnc:@Q[Y.Q*~vk;""Y DmZ6/o_lK Fkr cq':-*ziu6 AlN#MlKܶpR`hݐ; lYDl"ync->@+u;FmRXV2'qTqȬyom6B%#DU wu`DlC\K78|Vc}k|h-?f1b~L;bE46. -ϐc7,X*qX24`vⅤ1xQNY;NN_SF}wFik:b|7o0&$oDQ JF0N{{ܿ}~H'U|1%cSEZ=8j|NCa!Ϻ$*/R dDS:& {"%ьYRZBx!I5JKÛ0z\78^۠eY\okt5k ^X"!q^fNZ@Ƞ4 I֚|ꊢ(H〷 Z\M.(˒jIiiB]!4֒&9Bl9؝(#QZ8u-S%6i/-qZmE((V-64**~~CFt; iQ!"Itme18b6lrceU4$Il$ ֌F10%{{#(r]f0ZTGI"t n%[ \|s5Kc]#m U7r!Ƽm--r F~#.aL ioJLQ+4hPm+A[Y0hḘEݴK#/P VTJ8`\d#m J(q [c=qH.SfUFnBBQ[gl!-OH+b{XܱX,\?hp;I-:߂om6]m0繭^\`r2)mm(:gq.7fN!Ł5ϧ(!(K _p9]?6 ye 2i 󤩩5s!~1p$ B1\ajTp61$Z Ea1 Hv<Ѝ<9B5n3O)eY+jj*-:JĚ'XiHB;YmaY z/B%Ik ˠpW¿H|Kob!6ۍ? oJ"F*M1gX쥖!ֻ>k@[m_ׁTUv-S)C릤vJ 8<1s8F)la018tuwg*\],I9 H/PM1-᪍+MbZ&jJ=|#.Y^tDzAM15I^Ҭ 9PhØ:YiE눺9e)N?3NrxD*,S-aV:$8c!Z-&)WSqBEe:1iVbbjF]Nc:f(-Η9zUR5 F25JJʺIm`e>"x0g"hEΰ8RX9߰E`Q{p^`nn|ӂϊob8 M^Ɇ۲ދ-M pRJ#>MP$ƺj,5 C: nXK`] Nӂ|KX.H7x*at}a˻ϼʌ~:z+ٱR_wj@NR=xuZJ.E`̗LRP$Lrmq6?-MC{;9gg|SzGGGdT `f3q~6Ř`4+D(R^ ??Ͽ׿|#&+""??qrrB咺ly&s`t:Xk;?/?;\S9ZIxp!`r5ΐạ?tx@>gs2z}]\.iL:ܑXy(A(oq^pܹ{%i.LĽ6xǏx|}T̗k8<%ɄlJմ#O{2kj*s-E]i*@gL"H4a^}xe|^\`֚~C:M1u`FQDk{ ]z=|\\\S~;Cg4))=Bm7%4EjGee$~K, &WWupp8`XMYW%քΦU}x:aMqYrk[bk#, J{ zB(V+(y/ZdY*:(\[cLCUΐ9nB*,3t:pfz5 -usAk&pc-I;mJJנ0wy1c@ McX dk~g8 6?@B)n!Uc:ҡpZa D6C۷kX矈 F:ow5k0%H!`hp^#̻Gx yJt !ce00Z.8?ag9qㅠ .(@14M[B U=P r![<0(+)ٜjE:%{!J$i2zc}aXi>+Ŀv[xomo|7t44yc!Z(MDgU@Ƈ܃ !Rm<;?1:TUI]AD%<D*t`TDS(HBgҼ$J:elD4 WsS"xA/!J9J;KESy״Fqb]p4 KrFq71c#f%|%'+Reɴgpd6-m2P$$ -K,|ez<#5EKZQܿ&Za.aZ0\{ՎjMx o~B-/,?u@u!T.v6dhCVih5"X)Բh,%)V~py~EQT` 4a1/$Abp(Z.]pX@yכg۽waKv ^pcwڻ>r[ܼM8bfx}M41BTUAUU|gGܸq-5łbEc-!$Kz@[]z9=ɣOXd݄?z| 3C8źVO,VQ:{? -i'NO,{1?|= łN}VˆrRʢ揟>a؛ȇ )%_ɧs~qh<;L/Ԁ1AwU+7nb0S /\+QfKbɒdZ3qq9aoi͚^YUyF,M\r$pX\qiwWsguI}w?~O8yA֞j;tƫuMf|<{;ܸqA7b:r>!i/gupoQcXL1} $ܿw3$rTLX,hbZ816BOk>XT6>1N/< +NF,24!txkEZ㣣Cn8;쏲ic"^wٴ <'75,?w>׿ )眜$Qah% U]yOh mijmG1;sj! X|RCi}i򌪮yy^Wsx0$M&330|'MHMD"!IIA4\]I*UI`TY, Պ,KR*!|>VM5oePLjK[S5փl}ӤI&axLYŊ~?y-3-hm}ܠ.KlSiI7 Cf(% z 9tw:koT8FE b\$˫ ?Jpvq:D'&Oc$b81Li\Aaw>UJ,5₦iō,x;*!-]_o+R7Iц:{ZsM]lX {p6,xCE1:N MXN.Q*1:XBPmiIyY^dDn2B Zh2O @n'B _Y _KQ20p%MYkfL(WklZ;PDJ.֬l9 vr #mۗFm6v (2d :ă# \a66ABజ^qvyA'O1EH6cPՎ7A$ :MB֊~3 "HiC5s")b Ϟ}A$bDHb-Z=vw) m bSKLUSقrISuXU`#˹sأý.Q}NL*ث-:mhJya1g|h?˒vS#1J#Ef5")p8a.[fۢ ADe΃uJ1\r?]^_  'Y߷y޶Զә a FD9~<$q1uXQTT-Z\JEۅrrB+EQaYp)w%fA[73\HӴ],۵ӆ @3_gDڼhpl˯w=\% t_Wfv,ybm)eY/++L=ϯGEYʭiJC.+NNNf@4EQP%q`K+͍7*F E4t:9wrrB9:0kELouv; G}fgkTu#!7D"+@IVb2H#Kb-X1'g34>~{Y./PZ" ƶ?!J28%: c}{.J)iU:shZ8;j2㽇R͟$i*HVqJ4R%6-8jGTa KeTd.0O(lU 6M(ƮV+tԡ9I(/=Co۷r@46?xOXkլ Ee zn TgVosq(Ycd]ckr2vu`}ЌtBaPEvH )5isppsnMcQQ(t~fٔ9p(/ItLGip4b0$Ip"đ1!2ҐV&&ҏܺ}xg>ജGG)|9*JY IaIDQTKr#9$ŵ~=dp XwLyN Le;Ɛ*[3 Xj_oAn5 AlP{aFueBo^$TiQ &Y Hoj-BjN1^R֎8NCŶL* 1 "IyA>AKBε::Jtp>Ce4I، F>xײ7ZW $Ck˂byɋ3&b) jቴX}XtZh,Z^6h﷊!Az맷6xNcW,>!HE+ 2j@e]0*XU%'ܿy_[x)8IAt':hC$< utĔ+Sh4"snv`0.I4q"Y;0",.ФqDUZơ1)˚,JX( i*={@,!჈\5/.XM+=OdݺOFKh-H2c 9??%$$xo[2 ܰdW @Ynbk5_+aoJ7Ǜϫh:ܫo>-1$qAƘSŊb\/Y}>X_p .(Ok-Euoha6:J Fǃ[:s372#Q~u}^Acw|kv_lcM[.c.S+l|3dY1I4,ضeExlXGDw;ω۷oSR,ևy %$Q!/Y.5{Oeܺu/Nq9`ѓO9r1W3G]OhrœOF#dXO;97o~M'ǏeW9\|d&$]]5@-?V A'6HvfPӭ.uaѦ ]{/,fWI`G2=drEIQw=')qӿylV Dјi2h>K /^$MCv:J憛 zC\`bAvV+B^2e<=}X.il%t.xr3McZ2 W7<:}Ȥquy]SA&ii"] 7/ל> 愇LJh!sq2& lo5Ry"ir!z_vHUS]jH*!'HG:ۆ$MP#&2ӃEx'i*k%)Nnku]_bw (!㈮iN僵(I/㽠k0GXzy (%'%шc,,Kslˊ[f1LHbb>fg!u|8#w75Y^_qqX8K&STUS '{]C1&xX 삅5qƳHGqwvL$; .=2c FO7vew"ZY;#8HD[,'O:S-O,vLwvK%ıHUb : "N|')N.'wjLh/tɻ X%$al\T€uT\™Q0€5&p]nӈddQn7Σ olq!deY=`BCȰp.̫ֈD7_?q2ft`2[)U)ۚdrp|@ՔivEIJXlb$7לΩ_}g~[kETUR#$ù&&H1Q(f,yԁ5}sݴ&Y>h9^Sps5]oh4M8pv~EB]RQrRw.ru48g0V`B{ѣXod,H(؜y,\߰cv IDATwŴKBվMyz?zjo8w׉_Cx_K]Z|] Zu nk͚)MCYmz[Pu]6xfA= ^Kmɿ>AĜ!0cH44wYiϵo۰gI9tV+T5]6.@q_}*7< {;F9hsqA99MCY7Q:BﳷP ! -!t-E11-ox1'G'<}_~%Q`uPlT|L3׬6K -^X,i_%$q<g1&fq0T|q8i[Me8{פiʓ'?F OOYoZ֛R~/x?C>3$ӧ|knnnd:໖G'#Ƌ 횗_9*XaYQg<}Y6W?//"_ ӧ(J0Qok=|W^o+/_W|)?g\<=EP[yZP{&x{oIcH5#/H)/^]PFc=<%Iڪ$N?3淿$Nxt>a(1ٌ$G wQ"I#E.Η,f[YbG$ST|?盋%x](5Ҋ]p+Z,F6kudiN4$&,~~="c>s}{1h-3tzzSu]Smi"v8xx|̃Ð$XKp6444ӧ}5 jCܖ+,5۰F,\/G\@H ҃𞮮9n(B1] g,IPB\; L4XiM4<!j\/N#H*4C,O5]>f ˶D ߃aQb)V8q]@(tTlKb4=k1䎆BF ][ҖK(&$D.5-^<dIJAxIG΃RclpT -eϭ&%AEK6 Pw8Lb)g/$I8iH.K*YO$j{W]wB!/02Ԏ a#[0B wu]6 ΂Tw[!VL>K>cڲb8>ydzr{OOɧc~eY2!Q[*<<[5ۛk7L8VW8_1NxSsRbچÃ9εX#ѩØ5SlK &q&Rq~*l² Ice1&lnY&P#g8XL%7ezyA1Zpx$ID$%IU}MF:NA`e5f@S:) &AQ`FCoe5{f@yx{6|w}Zt i^gA[ݾƀ⼥kZڶZ*lΛ-vK];6TѨ Ft `i׳4= u=c_}yٮWc?Æzi6=Q$riJ;jYbCgL߯c>9ۘq|TmBt7g g#VM9742vvM4Vm!zdYSm;Fil6puqŤ Lgc/ftFS7?x˗Qx"%6k,pnul Y,),ukn>s6((i5iHY?WLQUQ!{O!Q{/*2BxO"9GG Y`Oh'I"&G k5Yd4E YV ݾ3A%g Bz(ɒGyzzijg@kHp.鈺ޒf.CeY6u+NNNH ۶Wm iITw0v:$xzkc{![/s֫-J)~]#]Dmϵ?`乷h6BXOKC#@5 W%#::'Kdq,lz)6f2$dz (t!4A3!Q#p>$^Ƥ2xee BxHŲBtydqH~5vHkqք)#w uUFZ ozW]wt%qF@@h4Mhf>x&R\8 ligI8 ~R>XPO c_OA̶ii~h2csFX]Π]|Ͳ"?A ^w,bs|)O{v_?_YLVuamjU+nZCڰ@Fp}DJL&ĉb:$ u]Q%]q8UtBx(U<~0C:.+:%ǧ83m "Mm JGg-y6%дր5/{x<ˎ !GX/#dkPc2PKǷ24}' Oު޼^{>!m(f+(poBa e*꺦*B%]؜5 4 ]6w"161H VϜ2Q5m[u;o#[v{ss7_}:Yuuߌ 88$eX,v!g0kZx@>yh Oٿ[cz{K^L9((kb1_qX:㸾YT+%$MaE5DՊՌ5R TjUYsxq~W&L]AfA"К C!tu h:X8'*[f O?d<uCuLT@#| uۄR56 Q2h[,mHui1]k8>9?THc|M2z݃|qg $I8bA|##t:钪No} -,Q|eTH2n8ϑ~Mהº VJtv. v^5Ȇ ,ێJ$JKE> ` T+tTJ`@5SBX%$yr4[WdRBۚޠ8Hڦ폺z4{(ϩ;d `@D8p}}Ƀ#nf:=hs{jE"@8A ]Y0j;L=$ z i fpΑqٯ5}o3V_"ICHu{7ɡbFqB Mۆ~J^$0H(O H`Mk#He#d.x 0u]M6!b0*FwJC WT i. ^XE8bӃcbqji倁r_ϡ*m]wտed/s ˗^Pcpqr)BH!b0/خW\ޮox1.ʆWW䣂>NJ8d )I޳lجrjrNr8f^#0[J]/9X˗/mpyq̓cnoLc$*,Mx,K<#T$jR2:ӇȲbjT$u 1yʇO0M8/7b g na11<}4MhJDjL.RWzGYS55I^<~!i6)<~!1ϧ`ֆlF$Z: *SA?n<1;Zn.Ï)7[^cqTiM,$Vx"MSR!yuqƶՊK &LwۿC.1mSVH8Mm,G&1޾=#2HvCY ( 5lkxrr7Ͽm4T$ y2!%TĸHPѵ<ަDĚ((^8EfMv"& ^u]qq~knהơlk-o>m5b&I i3rp)R,g4C)ޙ}z!aV@'k-ggg׿f\(;&i\__3(¡Pq"R?~ tj[G?am48"M%Fהۖ GmW5~ `p()%mqVY$Yƃ`<>}@<~tʶ\CḹGYXtyoۯk$I>; 4MS g6Ll6;O4J`Iq`;hSP`F;:ۡHV͖"ɤDXG5-7_KmWc\RE*F!}pOX^p{i:{G\ܬ JmK^LݴX'Kl-Ӵ-ӓUKܳ#JFZWq>X@*ĢH#Z-Qo/}ObOhq{-k-XHzbi'#,Kr",::Mɝcb!PQDgH%FNw]MU !+&8BD4$D(!8yIcBp iB#3`E!d2]wwW$;P}b" ɖ!7F6fR6Qz3Ikt`&A$@(!bWQ57k_~5WKy斋˗m? \LHN]e`;K"=ޙy`V+6 eY供͆je]n6%UU6I{~&(c@s-B(aC;FCkK2aCDam<Y,f3<c;}g:r||xłXzyKUAk: GOQ3N=bq4a<ϩ#~A5Mkxu~=ǣ#Fc1xI!Fttln$Il>h(My1<}ǧ:<{-Bm9CE0=eY՞<F+/o*£rE\(LQ(vSRҲI2F#RcMVLVl2R*&"8i5UEcr↫K6xAEH@.\`(;rߑC0~s^)%BbP%e E,-xn KwbD'mDžj AՁToSyE1I$ G|귿A[o?5G'\s"ǜ1vK۶<+- IbFj{I-pm<&&[EQKGڮ IDAT{oLXӑ!7n:-0㳯b-"&$FH<‰6n٬8H7w3J+yԝgPbwdWo[ `IL)(z2k;did\0ʳ>@Pb`nЛmnޱLG$¢뒛[m{{[ш9sS&zzOMSS1/.OUC)dsq~l>G{ "Rn!gTU1>H ېQI(AEY (!-ur;p&]Ir[ FgYFɷuŇk-@ Q #L:)n˫s(Xƚ9OHiu{D$FH:F*Jvx( IRxtWEE^DI$ }`URЮ R ?F@w>\]]4QD"0H ;ӎ(VH!xlx~1hڒdYa B$w`zW]} >~{Pa>S*,F{?bǒ /(re>;_rqvI*#{r}sb6ռtZn0J iinZ ͆z#8%Β066u7f3Di\(6wygBCg4RFd$IqC;&n$tI\{>ZA2ԈCiw?iFfo>>'w5;yQ-b׎ӛmk.ꖺŻ5Qd2UUqqqݱnooٞI`eŊ;`67+lh:"IÐ&xffs>K[yEH(pŢfIΫ!`$" /*ض`θ s$g[8 ҶMi5D0ব'V6Nczo(ٓ ߼&ܖcMv3$\V(lj][Y0~D$K nݴdIB*C#.$۪fYҵ Ib;@[G$D޲e_.ɋYל|HNTe<3 E1MFX"IgT%))F4,dB9IRL+0ZcL7הIR6 JCUb9J@b S\5Bvkݹә 8F㓇1LH0 -~zXֶ-/(v?ֽY9b SW)y6ǟ0O~‡>m[.ί|wrАKq;d:1[Bߦlt-$ [5Ǽ<{grMA~- ?Nݏ"|7Gm$e).TjmBsZ!Ƹ5ѡi"A&y୵W[y6T"Guw(}HY5tp>c6[1i,"ITM5P1$N)%]"v*u֐mCxfZcRӋbwU pRGpt_\ 7Be`q=sC:Œ,v!w ]z{KM)e/pA/ ,7K Rc0C*ADILU76H4, 'tuM+ d1IӜm\w!Ƕ-U,HF!V X&3D*4tOeZm-uJ|bTpջzW]}v1BJlՒ0K*oj4H\`%fR%H"jxS%dPng?c2S H$LS6-%@Bؖ[sH4NLfSa2J9Oe]xh4""l6|/Eش cr`V,MY9 ()/ho-g~)Sw;>3e|RP8S^j(94Qqƃ+ndBuTH7%Mc1H8G(rR4]^bdZ,#0ʾazOQDQLx"ﺝoлw\߶+\__nq3S 'acM]הeId{Xk<`qxHٖK,$=a>_<|?!BIbb|u.c !!̂t=D1I" 2 Sg (ܕWHeT5/UF?8XL_\^d)Y R0"dJ\Rmn M31(,KȲȩʰͦ,3nʱ$R:f4XӡB[y`:# M8lɓtҵu,AH6kGZp4c$}?*/cl1L&j`X>p.1{@fFHMLen_ejn>N ?.yX?!} {n/"潕r7mϕץ7nh6쁫Cf8|?Эgh:|ŜC@,ǬM-y.0yŦn@$UD|vWS%eYPcFxd:-G(uƨbK?%Nl¢YnY^mX7,Xۡ$Ii$M a)0PUը@Mېmۖ C66+<]ӲbAڶ{saLFTӌ]e\O&rG?d4M (-4^%y۱ޭe[6׼'3>3, 9;9NעW}.R^oJ^3O t}3;o~!/!+2)"vb>EK<Ϩf#bZ)Ѷ\?{8OƸN[tISoIeM=7W+]܂sTѴ@ \'!3nXmZmh@^Ty f>um|vpMBO!Zhm b'GdU?wd]F $Mdߏ_ѴDH.a+U5ƻ9>>@"Ī wy3cMak yt-u$&HxFgs5JIۆnjsvMk&gni-5xv;>S=|HCI?EON%W )v!b<Մߟ0:RW+Chj|7{XhrcxO(\=}Nj9͊%Ũ 4Du((rb1x4JPEj]SnG9n7&I&xLsE$-tծf<jzf;\&g@smֱmmnl놶nn)u{NP.vubJmKUbMGYQ[K Cdۡ_i DP(%'(c@)k{I7eɔbTdǼzc ]Ӑ*|3cDaIl]Al153z9I y_pit~}( Bz۳ɤLzV+dGxLkS0lLM2*B xk^"%U?5~ϡ 2B(Tud")K~p=%!H. IE𖫫+&)l҇Od "JH0~(Fѳ(- eybH9A4ml/gdyxcC#I%ˊ*5&*_%d?`}Ƕ[5;_S|MJ ]B6횪(Gcb+VL#JYv;(cZF\C^ 4Aۦfagmʈц}~||9>t:-\v\?\{=ym( <12hv50D ͎4Tl%O/,/0:G ,s)6xOþԠr e4.g{U#NyWtRi+hZKt>bx' ! a ?:1jup]dE+1vex eYoaw?%1(s4;jY_^27M!BcLz ۺ4mz-F $!'DK9"R<}((TPLfc%ˋ征jrMcyj<#/GXklkG;*Ig"fEe4 *SLFc)(TdŘݶյJ2Jr"|~1.X~ 6t]Pu$5(a(.9Vpu;hB3lv;V7Z +J!TE G5!wn[&umYo8T)6d(e@ M[Fi\L+f&QZMMw_`TC'lxr 5JРB vm khָw-RHrchBӈs\-Wɧs씮gDPJ0_LEN"&IFyRt.Xh;\a]wT-*K]h-Y1vm2w9"*4&ҠdZh]n,GDQ*O.bkh\/Rm(G3c,nPr<:3Mx[SݵH]ӢSNl?gqpmb8s6.s lg_|ruPwcZvݫkz )ˋ%= JbV+پ^F&?*b4Pp}f-dݮeܬ.qy~>o.w85~1!sΟRnEl3[={GRXQV+^i$d^syz7>0ڃ;؇ua-T>ҶR7>P"İ~ D1RHUH.z\ha#P"jR83TM'HOfD$M1 $M2lhTѶ5 9ctN95[uE@6I9yxʛK)F,sOM(5b]k1EQl6lv([{nj6 <(2c0Rb=@vA5,g􋜁%b໎\TLmv khw[UJƮf>bur\Ҵ;l4qdDSgx(1٘rT UbhC9D׵cI)kY^Ld~mN,ȽoNdUI&%ˋgܬ6rt`<勩уvi ^zY/[5&b -n9*)Mb z%KfCJ։=eyclyNT* r!Qa`ܢa״tC'q]7z>~0X&dJid:F炶تT)B*y\AhvHצ"y g9@0,9::B)ֵ&3{p6˾>+1L>mkO15o<{['FݗHAڂq_&u}~Vd?/}Wb]R;3T<~k89^0eLjJ52&=~y(@_2ex?%TZĀG'+E)=;^RD d%Df9^ Z^w=v|mC$ʈL's"z``6Tecqz|bn#ׂQQ2i8vt!mԽvnQ Q(ڶU/ijd#X^^/jyQZ&ս)L)B֛cn5k;֦bnZKY,Rtʣ'y/)z,|oXp|ze|Л |K^{oGrn} Q;#ao.W|njQ||pIO\+ȳ1cyuţ'l izOhqVϯs'D!KÍ.D'HeM&q||gwɲda%?z7yoA*fKv\@4~%efxɌY4'I'l62JJlYQ3mׂ R^e\.)d"<#:w5$[7cpҝsd2;TaPExGtlmKLIv$ yY,Y]ߠa1MNT1Z} z\gf3| E"+QRE&#JM,ȝ% |A irH9fk[B 42 !Au69YRYeZſ(,) >>@&ٰͲl?~<.^zY/e.}Y3F1uBl~{-ƨJj|!"-=xZzZۦ+ $;;~_e$׊I1)KÇ?\^߸ǽWNʜffȴ1KG<nv+zoΝ{l+FIƌn:2) FCG hQdYAg-2SLg qzhEzÆhTYج!]b~lW[Ԝs|., J|lh Mь%99qy?Nl# "5vM-'pZHH!(F*vh <5,MЬFHv͎!ª4X&h#>,#118v5WOzvAY %m躆ٱ^YoV4\j*i9N`߰]gͧ{%toUEQ,S󺟓ZH[%Frӫ#eG.z}Z͘ ޶Qt: wb`8i__#x 6x^{w|:)gmv,&S|.O%`y)m:$gn-w^dz7{~Rg C)rl;W=x&Z R^!n H R=w d) l?!?я1 lT!$LO?o~jD «5X' !],d9Ft./_}!q> nnR#ɓ'im)R'1(uvot,ӽ(KSr)&y:o6_zG9+Grv׵Տ~rïlh6Nl&lW,og3kUu?cNOO{.Ϟ=a|;'?YCn;XVTU0&S2uhtxwe}X%KB3K HLyokp#/x(G nU}*uO`ud2xBV9۵dDe^l)t-njD)h[K& "G9JlY(wk$`EM)XNmx2_ӡ:е"]< C&X51z##NNZbHYQ2&,DŽ,}W``TZ'x1I"CL0@}z[K@dx4MCtmY_߰^]etfYF92=:BW۶#ownKmq"BzLb{lb;k;ڶUyy.|t>:OS?"B$s[9:*r";\W`t4 0rMy.D)%hƘlCv-^\Rq11 iv&+@}Qf'U"H.5U~(cr}?|l>d^wK< _M@@蹺x/\K״(iRxdG!d BU}%>$B*HJЃEk}RDDiER+Z+YMLt_%Dd$^bpx!>2Rٺ%:y~}lĶ v}:G˲Ĩ4& r%yoV\]%fp~QkӱUu6[\gUY(Ğ62/6_ i8Jb\Gc$797˛6$E Œ,whL`hֳ.WY >©(=%s|_,7ܽ{o}[}m>䣯=f<^E2My8>>1YJb|c g}t qru7]jKmXw9 )кul7 0:G+62p=?X$6 wi&|FDꌬLÇYoF p}8I~ӟ#.| _<|bF}<߄zrJquuBm TܳvF(G3,H&S+dHI )77K.hոdo3L?%JP{"GOz\p(+1YFS7 6D|m7K^}k;~ ?/yW_e=l]<}ʧ?kшWϊ}g<u ]%Pz6)Q lл#N8(CkM}AI'5b!zODg&UoS%`G6`K&/:뛿ZKM$WgϞq}} Bل-3<Ĉ'&ecL)b0ێg)k2#ɳ2Y{*5#Ԙ, P]mK>s B܂<d8?@]L#Eʚjl7O{|'ӟɖ ( ]:Ory&½WfwL_%f374/vpuuEQ#W^ym`lsVNOO=67mX xC3i=`!z D g@ +Jƣ2W\iږ|T6[vMi=LqcWL&FUbP\!.h۶'8zMUmǝ;wȮh-mbc涙UCˢR(c0&VvMڧ !}h)k`I C 7au NɲA*24n7R{8B;DRԶ"1x%e^3Rz,R s1"urJ+FezlL-St])@j7kơMkQҠAJXIY(1-H !l9!iUҀ|^pxq=h2_b^zY(),J'b$8Egqm!7& RMA=cщe6 pmz]sڰY7OtΣݵw5ZgHkƔՄdS;mx݊x>zomS3Mȝwٵ &@@PIгiPуE2 ݳvkmѣ %ضHI锧.Xid2n[:Wz"SSVcd&*QK@dB$rf''FyD@0ZPe^&OK(As*@zEI<ъ҆'ٖb kq]µmj4tMKpP I4uR-jp!ҹ$Y1Ӛy $R}XkalyQ UҼ'8~jf'Z3޸&ggg˂_Bp(QZ{`  4^dH/֡E!9VzE}>|/lkUVϫa#HՇ.9YnRW-o F#jkEI^. 5L(w|plҿ%[H!8Kݲ^^qu՚jqٽW !)kl[(t 1Mt=o%~l B#K"`aZa'O/nBk@bbuf^'vvYX`srrF J޲^ռfO$ȣg%DJfھHOzdL&$$%q鮻="3\[=ll2#3"<{9{>O|f]Ѷ!*}C GeNk^xzf>)}5[',&E'k3f9e)2|YC?٧L3Knwrp EI cտ6\<ᏸE˰nŚhm68|ۿOOK<1͆DƧ ycoM=;#h`D~c_Y".0:PMLAkLb웚fJv tW?u;_ @tb\}dޒeYLK,,gDȳݮX (natX&+l6;vۄ1NpmuRY0O)6uv,Q u]b$`t4oL 5=,,x.i]š&R<( w(j}GHldZRLKdf%PiK%'MI08`2őE"j .DZYv_ܛ$$>ψγi=]7!21$ 4c_W}Gbn_vA&lF9Jb&1S-I&D7^0R1( Ȗ~޵w]{޵ߤ)HM2w%ƽߘJ:< vjAKWu ]b:H1T'QV<o3!1)аH[LHv &y@X1TeYb1g`qd}TZv-D={L4Vtb'UhL$L4=m,dʓޤLB:pӴݎ 2'TamJu4^R>#̒49{򔿻gmƲ@놪d#e4тS HS泒٤N2;:G`섶כjacl=J+:@ Į!N趦6um8Mu=c#XTMulw[FcA?h,8~s`1ЃH~XpCNy'I")ZvʚxX/K,#u]DWm~}7o& 148::b:)%( Ց=)gd쓾 }ۃo/6z/mW]Y@l4Vc'{.IӔU2>zX-0ĪuR*>0@AJHb t=wߡ\`elksBR 9_3췔2Mþ=`9azQ>Y-Iގ<fP~s t>GT=]ȬP2v!>4L 2fO㔦s45DD>,l6#@=A!{W+Jrd?>{#||}RDB0f̦%WWW{tbNmQqowZmNJLL&Se^Ӻj%j՞_aH$91Z%4c0EZ>:gu{j&YFI7d唠 gyu m,ڤlwfzSQGb0P)U1pC<ƈPFV?Y>⊬ܣk^|)| 9/_~tS~~8B[C/i;Q- EU+$A]> ~Hmg/^k <tlἣ\ِX,:~ӹ[{,#&bPsVeѶIbP|BF&E6>n6GEW) =Z폿)%#w3? p,k2w>T]ףm Zq||%|G?dJ);gٰX,PJk=r:oFQ)d W_K`2=u[ޱKCpea&$#6Akc: 2vK[ռ1Is\z"P LNK212EA"7lYvIWNԍcۉ,@cmSN!*Mt~l// >c6 2ڻkoܬ5EJA*YQ9a1l@VTL!8SiaZ#9ԒDŮq"u]anRw$ ^ Zڮѓ\*1"lɣ#`l9i_0|heAe9-QHYP#IlҳL1T[UF4/h6.؞q==;[^~by#>} K[XmLSnL>S ZfY( l/ȣc!?!K Gt1L Ħ)RtIa1&ufT?{Ȳ{l]7~ȏ+v!և!v_?d?m;@g09_|Y9!Ms Ҽi=3tI7ǩۺ/:p=Hd2\m$OP¹٣f:puuEf4FA\\\p~~֐%MG$ ڱ7}>!UV c4nOY5*y.B>L9ŝm9G7|}Wsswf>b4$Zm"xIi2%b h"gk!-RODm1Ƣ=T׶ֲln6̦bZk4ɓ'zmj5]t/ķGga("zt|4PZ !*9izC{3\( B(4<,F)_:+hWJAW|N3 F%ZhMmGLhBŖs_\b2h w[V5AEY>a-'`l1#Rv-wL~ٜ$Fz&X;oo/b3&z!zb6ec_5LV5ݎHG1DT,#VW6(&:ms0=a>]0N87 &1H턳'<:{B۶|(nnĺbZtkϦXEA秧&פy1:|vwwųXٔo->{Gqq'6ۖ|.riއ; !]綋 ./_͏qTܐ9Zk=zxwb13|{?(iT lVAeTUä2}hu'\_ݓk3j:v_|AQdgKNN/kgXً L؀BBKbJ炐Z\qOKEQi|LtαX,xEolܡuYd=2u#tD㛁Q5ERx&)fѶgI»Fy+`$@VZdE\)d,k3ڦl$8"d:3#D5~ЅB6-Z%F&g ݆D8G"mTdDmda:< 䤮B7gڻkڻ+6V2B HSKOTE h1DL1]K iSJa]$+nm Xv~1$Khqj҄5Y('ˣϗ=:hw[L0VDӳx*:Ll7JgQ¶?dLmE&R5Qkpa6?b1# m2nV5"ggi] ^jzdǧrB7*Pf)2'K$Stb006# Sگ_pk0q8^(LR[ kk ͊5ݎݶl6uD|M"'K !?k-&tlP Xn94xzB"$=yF\o׃D\zfKA/AU۬,i{%EQ6 GĤ(~}|9{`+{~@yYɇ@5 u=!(}F >W >H'Ox٧b8>#&YN6` VM+hгx䙱֠LmYbGu BFI@bXw\6J0=fyt{ϞQgm5Q)MKVL'? d(-H]G4#+srhnV\Ĥ$َ̏{7״Z3YN{? jȴtbQ1}ձZ} T(m?v=֊5($.5Rf#{%mݙd 'i+fG͊vW/_p}}M&#D H~I_+#@8N9::GatYk6HLFb 眜;-?{_ Aj]7ɳ b?*NNN7;jvhHaVXYl]x(.ƈx%?~L& Z0r?79e!٤W_>_PI-tl1t 7|7ړ0r^H@k݃G bd6eYOӄodY%FE-݃0 a1xP8nu ]`Mm(i$$iN ma3h IDAT[nP=\^i$T:*  XVZ 0εEJQ-bdG T<կvQ ]'_Pؔj?~۶eXR%Z*l6d'!c IZ0-MԪtZKb 6&F1)2 -Fc Nn$lf(*+mU(&rêz'KRMK\ۍd2aX?c%H[P&I낀cYRQ#]Ls|;Ʉs D-f8,24'Ё!s(no|kڻkگkj% v$jII #i¤Qh#^I/gYY3LV` ͞ߡ`J)&t=o:-w]g9$aVXjN0D N4F|(JdXR(t`y$'ƈE`Z%X\Jiiښ蚖lVP()%Nj9V$Ikn5sIɺȧ $'AknYo7=fOoIbJt Sǧ̏)%Kw(ߢ!K Fyb hmʠE+)Y_3@ԢARxxt?0W*/@ 5*:m5͊;n.|InW 8݇Y5ll@8༧nI)x%"Ԣz@UU 0ƠJ@؍ĺC(Y”ڮΏ$z@G++BpY!@a]\GZLp^Q#>}>T{]oE/86Q-oCᾅ_q,x`Fm Cex!{m0m܏`4:D ѣ||2Ië㞸ceɲU|@hJ_uB%XX'Q۬VhLBͦD8r_=pfȼ^9a/A\]ۑ6J;h[ 3|!:kuT:GY:EE{tt O-H!6u4]Zaf٭w,Ku{lawssC&6; ,mNZM@֫1 ܭ6,O<=a?Zˣ >EuŒ>^TY%~xŋ&%SQcvEZTd֒& ݯYKTT$yJX< zevnvI֡/N|G%7zջ ߣRukWWٟ9'')Ϟ=Er*[㢡V8{:PbtTf?6\ެvZWfz noﹸ$ϦopsG2 :XUGZXNTMͫ bA}A0?"(CTZ fC0 Rw]/̲tJ$~kI޵w]{޵$ 0Aޏ3 $& (]hNmBVd} hQE6i$HH'%LH3w5VE PQ6⚛9U>:+֛ԑ3̶ І]U7~kʲ$oHӜ|[ozF͟_3ч?<a`$ 8"DgF(gKBoAi5ZlX#DĪPE0 ָ(b@9RQiw;ba"hQ੪(3:(7d"|6< /7/`2B3Fۗ<쏽iZw#+Q6R#EŜi,q}f^wBJiԲo](Oddt}{`1ey2mێ3Mqf'''o(3\ 4]Kjm$NłzY۴#p4B`Z1}pq}}M鄺s嫶z' ֤$BP>$) +s4$ϩ[VIHyQ"RvOgLo肢r[oE-?7 F5 >qt2)2^OsN&SQ6Jh+~|9;;c]\^-t:a2YVg/9?;zG[.qt9;;J-sV4 X,9??HӜϟ}wD8LR6m8Ƈ;/??%/^ zMt|̙J={FfsvՆV#O-g~e㓂Ǐn*֝ؾieFjuG*Dc3Zy" CF1sZhQJt:xq)ijyc꾨)wo\\\4#vGՕY}?5■T$ƀ-ElfSo~ ;yN[%9H식H W{AA Q:st4g1d}>eR(ɂrP-m㸻[Zm%3ć<+YoWD&FZa-$iF!M36i1t$q]1 `plqrzz:VE+`dPDHfxmҵkepQ2dA')M+Ҧ$4i:$w{Cť:unAa^%%I !/ u8׎1n'|45=-ݯm$9.D( eQ)1vVN%17I6MJ:$ݷ2X4Md2帇w]{޵w]u-+Qttƫ !,j tta!I4ס"&)LˌY9UФ "&ft}UԘHPW- "m4Y'D 6T%"lv=-2#=cPWmKݵQK,#v M=e[q@a2ok%oQIXI1a13) eD-];Iw.i12fhjlzw cmǰ,X,P*t- 1t1#[B%Ey ʰ@'#7=OsoY|ԭGM5c "¬6L򒦪y g{G<~+6`,+h[$xSWwDqV;!j4&>|Sc\Ql+)Nwcf&(, C":*"L}_xmhڊ˘\WXj66!x92Aƺnv1n,V3b7ݲuEZl&ˤ )QsPX|+^3Ѵܼ^֔1- ΉMQO:G{b_u4#[8#d,NMOL9 FI햫+&I= CiwF%5y)>k;QF*M"$ K{/V@)I2䉾ûn4ֳ,jQQުDkZߨ|(!Gm0x ),5{*LbXMLZ{j J~  °>>>+ڻkڻO5c Fh;GI_sa ƪs-$R,ǀR,feπ4h$й=MȌ& ćMvYՁMyrzIJ̘ G kl9;srr2#K5DH(J)5F迯V` ]tѓsBHʅU+ :.TRc|$Q7l&p>j!{,+(3o%_،r`&+ィ,+X.O8{tryBQH y*TphzK=$yFxcbٽ}%ƯxC0bn8CHۯx|)ڈm 5kn/ju`V0H1& 2FIR(jmF@ƱwQ=bT;{`\rP؁tx55sN6N>眜.Jr!pmܓw y8~2>W|rz ~[v"wJmA kL)I9^S|gz/_Q M#6b9Qi N0Cԧy{X\cƹh늦G L:_Uf~{,u]Q觫~? mzTN kTԲyUQXHSkfiApE sZ=m#sפIm-ަZMJ-ǰֲX,g<ˤ8]bЇeƬ7ƒ̦cFm[d>czٙ'~*ŷ}Տo裏v~={*4e۱8JP<(_6}PKĈ۫Nb ZܛVb1AKa -z+ V>-)%FþX*$H/1#y>ޓOfݭP'ߣ,Kеl6|N-u%V?G' OA ^|Ij놺 #3DvMfkʲ1V%KQ1>8::lv|kM1}\pttDY/cyɫW׼)Lg1k&y ^~zu !XI€bs#xE~&Or?:ev4M0i®ݳ_Ht {gSܮvyړ +݇ۄ }duݸ$9pgOXklDx|ٳgH>(n sl hB;Aa`݃R@???kf7wժ!㥟c{ZKk7*mI@yzng:-jl&@4ٔlAKѴ}Mɪ@)E%d ;32g?AͲlwk0\Ak +#QݶAňQ"I%'\k yȊ4HSer] '7y@ cRKn4!̵¾ E,>:I߇"YQJzul7+#I5a!hADy޵w]{޵ߤ9.:a}H׵uM P۞yLjIPޛTiP9e9a6:'9׫]q@7n " :RDgϞEʼL5|S?:b>p]v\\D'$0E.>AẆ:,`Fh}1FF^* s@4/x+>u샲R(]Me~}'>$h:rquzT=U`lfOvD%4GctC/x-IQku.P38??xI>)Hcvs@G:F,d9mRT $WgF! w%p6F?8)cت^k>~lJ,Rj>{3Ύg$}j抺n'h ٤Ħb |^ aFv/&8G [Ev=E^N'G=xFq_vXJE+}cbn[c$IMͮp{X7iὄs| ^\x D ճ!tUDŎ|D0H $瞽k=9gggdyT:2jg,Wf3"n=u~~sY\2,=߻k<ۺB #j@UO_h⺢dt5JEm >pZr~89'x[>!aS֤%o>ٵYцX0*a:#}FĚfܹ۷oV5u>S/0R1O'`bgTPqyqY\kȾ!.) A'_^oXAw9<<8Qۛ1żq$&Ԡ"iژ"%U]Ò$'w~oÃ}./QܳwD63=.H#zm[ X|󟡅 bBG~CPi\1 8lGHPD>4mGl:]b`]_\Y )M!ڶɳ(@#%IDծ0M ڎ²zm;bYj$Iu:@^^^"ܦVU(k mⴎ"冮ItPIu6TMmRLb$)`ҤqRImZd۶aMZ:ʲd0LJ:7x[\C {/DuWwZZCMͦmPbz((خE` P/lMȰCvRdy=^x=^kTg(|Qu#8RhRC Sghu(!I"7,4>)%(-88ܣZ^,k{ap^xux$t.uH \KjsZE8O"FDzaY!x6*`uౄ RdIE0jVIl| T .hlG>fݰؔi #&e )&j숫u˲r4~ɺj) YzbYkGOyw,&*TA4 Q" '8 "U]bb#n -7kzaQ.mzNxs_|/D~-~ 6Q~;D%ExتYKP$iA?deru-W>m0 C zXl輧ڔ&"u|#ϸ(؛FEz\7}1 ȻQ.bFҀ!k7s)sxxmF`3Zgc}E$[y{{{M5d:-Hz$5@ۘa!&c6f ܺu 9ʦ޺#8f36M& 2!fX,*>+ڶelO]\'8OѭK[GG,K\g1Zo*u!8suzb6'`41Xo*\__GHdVƂq *Z#(FtJ fԁ-Y,Z(rU24}gKt(着( :!Ismc4 ۵<~\݂QJ&)(L[H3FYj~_b G4)ӽܰZnNʲ瞏1}AX8 P |>yw9=m[)~sncBBg#%Mc'X4@SiFغ?DOSl6%'%bI3L:hB(Ɠ_}of2.o~%{{HUٰZIiTVYq|ry%"ŀf !MGU}oK؍ p6:Ű?JvɺYcдy ^_$]B@?џdTTAkbC]YqzŪ6$۰ZE [$$IS]MSl6ʦd7!MUeB(}q#:!0nL+úqt& ]c1&~?4W=SE@mkJmAABn7u)%F(hrU:!!$KRNɲ$-.Q}(@$$੫`qGIJ]iMve4W߽K2 aRZpnլ׬F#2ITBj O:-m0;?*Wx@ylgAkBx b^nzz#tf6uh, ,3tȂLb!pqâC'sٛbb`Z!WmYN  F Ġ bajHrh֌M8ѡ!15Bxlp|toKOhpE HMt)Wd H$'hMp ua6GKQ"!d9K/EEyG$0\M6ZC$m$V1Ubaw8cZ2aEIG$}Fh#hd]rdqyA]wp~;:emsAS[{XpquS >$,OFށC%$. }JHo-2@8rնu>]jwΓu$J`>8MtTAm{ڦ cցmbWEhۊ<eMIk=Mpptw^,iۖ>{=$7Di1G`]\ѭC(v}4쌧GSZlP*v9縸̣e 0*.fs$aX[&wo;Wv64|g)x1E1|Tt7tCXR%ur~~k_[x3Ү$MɳiBhԚZD6,E@ݶcȐ e`(3")r\0غ 'G(1&vcpmٻRT%e1Oi;R 7F(\6"Gu­YǏID(sFعvqyƧeO :Pú\]\Rߺ(98~Z(Ҹ!RBn]DNH4MvMq謡uo=-YbPJl $] 4&z ֡Q\<ą+9U.̨鰭# |Z'M{jLYkN] P}H1 i8aۆ\SnVI )z%xӁ%uѥ1}Su4młuY.+ AmkR ST6bKw2ڦ锃%֭1iBʺ&o[tbMe$$j]rqrqEQybMjD:LsѬȶ&u1Mt]t=omK:zhz,mi+$(K6Fi@J0*jw]4ڔx7GGG6W rM9$%ISLI" Rj)8mɍb\ܽ}$+fz"I4ZŤhOt鈑P/.Y.. r6Dgg1>)ARC!b ݻ?CzGp8lifݐI^[d$DeBMej$NK% 2͒ggԚIeNF] KA0#jM\+...xvzRy=|_~%HrA#{;P/zrf6\~S\g)˒íK1Mm1T'2N/OlBY;vEh}nK A4H#(Տ8,OG,[yw0]m z]fkb6|}q>thpE)E3߾ ]l!^o"zѥN%BnLTMjS')dH0OްLEv V!5VDkW\X\Q 2-IM02Zmؔdbrkhb)p 1e`A]Zmige=8wZTAD$҅.ŽS;}xGp.:0>zr$,כ̮2LZ䱻Xbϒ,yS,|4BM&s#mY\]m9Ym?׈) ItIT$XBY6CT%oQL,-46vw\__X,{33f:^BLtᄫ42(7-e hHqAQV-uEU5xYѮamBʈ(7{cL9|>ߺ˲)7)&1,;ss\_Ŏ$E)ݻ;M|>g:S2~%?rqqA۶$:\7 ѮMӰٴH["$ztOsw4]":wڶ}aVZspp@X=/Yv]3B_(hȧ}x<=;6t>M ><mc PKBhN!IVDto=6wːa?MIFclJp]Uq(bp =w8n%|!8W*2ILHʪeS5am xK@ M;,DOd:Ÿ'X\c.ISmzSQ5-D$rku߭'#Z`zx_qӶ=Q @lJ$J*k-U..Ĵf硬Ymjk0t?}N7U<|P7 !\cC@cElq\/pFRR65~[jq3E\Xq>I#َ6b&ISC|dBD9%4` k@{=^x=q]]KBM{^Eժo{H8uDqdꖦ흵RMnR: BLJjϲڀq!P;%5֮QRKL2ai(TCd<9GewxRt-ͺ!If hY ,(Md4d:fI4?t`Ayt8Iq n#7??}%{|7[L[.[͛oI&2F9O>e2p>}ܷr1`q;E.0%ؖsdE~{Kb Ey"&W,>p[Յ<쩇K !Eo IDATˈ_?F a\rp=&C~~k|t!ȐےB⽢(z^d ]łbA۶]P-={%6s0Vn9y>BGAJݻwٛzGQ4RѴU, ;݊z޼Ki>8|*Mb=A|~vQ w4uE׵ƭ|Bet6łXenJud@(r8g SĖdt0!L=ڶoZ㽺.{WsB,w>/50Ӝo=6yt$ٌGY$i]Bꊫ~Wi.B{5!Rs~n ؊ZwP-B(b5$KZ6.b[~SwX+ENŰ& Ai!lYt+˹D8fhXxzk;>o'}A >NPb:7ƶ-587g]먚զj$/TjM>uUY~W<{16fC-y0'x2$h .hb[\ѶEGp<<-/Y +O JKB B|aS&Ǯb egzz6÷58;#ɵafhkq]Ntd'3CD)ƭTU'>ZH\pLqt됳% <;orrrkɊbᜥ(@>U]d>'\]qyy)}&IX,,lʶ^Be u] Ŗ\oX6:P%Bj (to{,k5xDsD˲왑k "* f<}rJsp( LKjU>Ak;"z_X,8:8$2Ɠ,7Ҵ~ wNg[W߮V@A/t5MCD0^&2t߽BAཽ-L[u:?c+*:Y^x:QiJ1u, .uhRd9/{)w3Y\/ &{}NOxW|~[qd:cz7bI6*8;;㏾|DŽXV|<~ӭH~tt6xn`-uMc  V . Q7;G$dTv>> j!U8GkQëKfw ޔ@ ܍+o)Zro^w|+R V rp6,*&7o˜~3=d7\sp[qzp("5xyGhcJ!1\7fMU I /E?$]SZ/9;; MT-:jg[wl6`IzDk[o3z{{aFyFdzzZ yUUm]C0cXx{Ot9r Jћk{J<ֶTeI8Ed$E c1 T γXl<[s@kd2^@Ef+0RmJ)ڶeջՇupw]e1"cLJו(݃ME&j8;;OR &-r~Q99y|Γ'O>vO^Y,5);<_s| B!=T:6mk|!(4AEE|֖UKZ0GΗk"p?$q_Q_]j[Qt{=qttW_=|!q1qq"dtC5dI׿\5@CQN㏹urxt~ b11:v p?;;//x{֒MUEAdZGm/?ł!^ t"Jg2l۱^]qfwY_qxCwnGUU(,(+C:/u;w4G>#~:ijͶp^Dm= Hx߇@68Bd=Ƨi ."-H1D\ghw8St xj/ f2'b{Zle)d;/]] Av7BqCѺCPA-ζخM)tMEn&v`Ʉl W Wk&=nܡ>>zUc2A</^ÿXuXM @"G$&)5ud٬`<"XpuMdRl׻GDLCBL>>TEݱ˔ݸ az I>V1i$$!c[l0-rD H`Nn4F &(mbNUE 2q0Lg4]<(KA}kSfEItFEju1">Ed K &H!of:Ҷ-W%t B2{Ht "^nVqcp}qߜd'}EQ5oܽǦ^-Y.H)y =y"7˨ Xӹ"/ Byl*!H>}f]y&I`@JCIFL-","#^ ^72wh-*rB5V@-/LZi-Fرߞ<$(Ig#4"C yEYmdyQ R,rd+Ѷ58K%t]zmzuzSp)& Նjպ;Rjv y! "O~)Z%clV4!ɢW3mg&>c5ދAg; mׂhEQHL㈏MUqyq5M!eNkInpI+Zx#uM$:ڋ?grޱHRHUctf<{B7&\dM"w,αz;뵸hLr~WX2 sY o.d<C_ǟ>>.]NRʺd9kI@ gU B|l,Ij躎ztG,J&`<2c T%X$uGՒ#Vˋ( C)"FMH@r)QJEǷ/i QAU5QjF=.>Р꣔Aϰx=~K)~ t]sH;<{FyNk9DPZFZQw, He5Yc'td:Dzb6)FqhE.cbgg1'i۸o"P7 m[w$]D.*XSVbь|)n鮗$٘>SNNN8urՊOxۼ;נtb cbL.T͚EhSwԭ#&q~)mx\*A&nlMp]+69-]ܤjR¦whc7{oXЫx=^x=~Q$"ТP:oYAe$»{Q[Ql<\zuXWk22$a(:AJA; DRRTv$HQ1XD A״4UKg>5W1}2SUzZ"auiחW,()>\.YK+PtxGՒ/~0Aeu_}7X-RQdy$=[cPeJRqtAc0,'Ks\pVƴ,U5ury!c oo]_i"MV[.[{vڦXV,5]^z18 ſƋ] JDTC^m265CJba>Sv,+ـqZnݺEul+={O(}%cz?97ý{p6O1 } <Ç?f<Dl4Mw>eYZHt,+FMs{.gggTUE]G@e[mW~s/׋&Gmcpmu VG+QxOuw sPGcEtC݋e){vrGJ`"w ]/׭ˮkxx#C};`}@ޜxB6!ߢk;ꦣɜ!dHS:f 1"ίtwds9] dXKR\`hjEUUu+ϧZ`FQ^J1$yAl64M=cv݀hغwü۴ZSm6CɄiXV@QiVlDbbtuE6 :^%F<]FWZ<%/br=akuh};uau,8;;Z˺*qLDl֔e8s.HO>gI?yA[Wb嫌}18y0=CoX4h!Q( :?hfڦdجhR{4n=>u:z jc$€2Y-7ܽstOZ>C6{ @## <zMngk-k>#b2?dXggóG!DTf\ k[:J3ۧn[=~Be:ú#j苏>l^'tՆrsŷWϙc̓,9oVVגݥ^ jd$#"flƌCa0m!`†c،1F,Ai-{UumYU9g8|UFbDddUv |~豱,s7 Ðm>O"sH)Y]g&MsW{|~Gyg!&ClޡԖl>cM.]JMH]>Gf!X9{I2ccc7V}:@..áS톮ŚV#[ӖEy1V!,g>wd2h|=E2IUޟvL10feR`1 1]({񸞋E^sbQ8vT=HZkU &oJkg-Bz֢p\a7yٌD"4m9u4w qV8FEY4 & n4 dϜxA@ y^8cnR (̳mf/^79<'Z Ky$3'Z^H?&׊t*5Yn=qyL'Okl ,H3 я28} Ʉ8v0 {1`}}11gΜ<^z%w6@G + ?%ܰu@"U5A/]Pn:Vk)yr!`B R86r \ @iJ2K8.UؠPBk, IDATt3XE( v{Q,n|pl>w6Cx [jbE0m6\v:w:ө z}a$ľz[Rζ u'@WnޜKF 7\=0U+xÄpz^X#U$N'Pq}lncLI&fsoЈKx<S"uN9[sÐ4!2ȍMΪ5b@Uewn ¢eѝN:$sBU12&jxj(*mSE<%/jtIm+ pRn_B^~qbchJtEzm>8–ɘdgϞ;;yͽ[Pk׸c=?Qx nghp}1 lƫ_j8駟f^$-֕Q1:_z!/"f1u(gd$ c&RWп,ɋp{d.5TDZcV^Âc1K8kK畝I&1f )vm3R3>)`ih] kr`L ,6,EǼB1P\_Z9>ua(ogE2ku 5ǟPfVBRKE!BH@6 ]fZc2"AW2;8<: Vũg0ݳ43Mi ._#fLDfYYRn3:|YLͦFfEs9\ ղ֖8-+Wli!/3ʴ$Me';Ww;!reIĉ9sVILIG!v\^NyJj>GC'h4vzZ- 9WWH)ݏ]E7})xCy ]8jMp>g ?l&kȍ|鹰ߢtjA!3)[-$Qv?Dɓ' m|>cb=_[q@vK|A&/nݪ +FXJVkm\de4ce%",*\xZ$cK1႐dq," kWuR|*2YYp&3^*x Vfa{u([Eش,?f=;pj X!Aey]@=D^Y>ezضboՌ . K:YXWk aD a&̔& +W ]J+0GUgSi)Ҕd6A(bǂxat`;νfɄȉ6Q$Mnp`kvϪ1O1^s+ku|OmR:8.;q賹JipŌV='!qt0Og:<kO1zEGA.~i,aDaE {41ŋyIf)J/pE"[yN)%'ˠ+%^HhV%If_˳<'/Jb,fB/^԰ygvlg:.j,3j+ tʀp!F;`UW1R3GXZ<+9Jy䪉64c^};7LGHp) ][4I@#0i- c|/DD " 43avw[ Ƌ ܺ~&4#jwhf)nӧ7|C1FW4=g.JSγW P/kO,[Zh}%{Ϟ/.=?ط?FK鬀0NW;G0_7{6&64[zġ߽fg\돟6!Jvd3~~qI{qm"8x| »%xV¿{~薓-^U&L/o[*ɨMl,2.ƿC_o|_~ FO}I[9}C:$X=~mv?}+&|KjK-><??Z >}AK}q[˻~|q¿'o%?6Q  |IġOTm  A<ܱ:+=|%ۛB3;ҥ+vk҂vVkR |<}>V~2ҚҀ_aP73iDwo6U@S_U9;,Z6ʊZnjiӄy^PhDk'vXcy{8ًcFW.]FGGGloof{N|<<1W?O~^8?5ma {x*/]A:av}<ϸ t< /^GEsjt],L&4-679u ш .8&䎽@u_i!"`jE̾sAܚ ]51-cFZ?SQe9nWۈ0q k,+g\^l]>ޚhTuvbx;mi?ǖ$^~ҁBRa'@.B\{uZe ^~N/l <QA|+{!iT{!=")Je.r~uuc4!yYR7h6 &tyGV]JFfX7N>pF`/h>̲R_][.l$f~OOn. B,s 7wd6G+9= 8qfeme0ֺk9e.EI9,ed`0@JaX9<"s\Tr˪8(Jfp8dmmVmC&a5U!'w~@FA C^FC7EhS#|Jʚ6 Ig4:]օ*CdPj ^v‹ms l#v}gyvOY8q]U.^!n}??ï=|੏b *aA&ۢØJQ5~hvW~ՍM*e@f%hg"ڽP&S% %! 9}^ʹ{L&3vljV"\ #A*ڝHoaձu]ٌy:c=awh}t:mn:g_jnN6Ƹ"K^8%@]|(+vs@ۤ ;9gVyLS2~R8 = pǗ%`2`* [-*IYg[|FGE` b^/BjܚrQJQ%ƚMإEWJ*,0%:/Q:f ?;yFӧӊ>>1Y2㑇G[HFF!}li6 F ϙ>qӥZřCHE2Ogl*^fIY(%[j{ M oQ)7{ *[XUcϷzQoXp[PiK3xY~~_qiBb%-a_|7J I[GW?"_}3?|iS>Ç>2Gin|ׯ>Ώ[Vw_]?[|ɏ(t^Z_Ml}?x;O*,n|rFoOoq@?"w=<n~9x _E;`Y[8Hok[Sx;~iJcʠw|7_%Ow?=woYފ?0.1li{1 :?>ǿjd<|ׄG?InIg_דxJ IDAT{W'06ցB S D)|#'G`||E *X,+H͜d2MG$ɜ,W(nf(JK9ʥ]#EQM n#4L %td+d!(,P޽@sc No5k{ h! GJKZV$]aepc'?/ ([BzQ)ZxRNQd%Irq+[[[hힹ5h1dZx=:>^F^8w >{ExRR)(rPYfZ:ubay|BkXzօb,kٌf* z* |_amCm`' y0z6/^Brp7)CQX& y^.RJZZ[|_Q9EW +]6,x\w*'midPdZ;G#ަwՕ) .)KrBC )R>R`68{@*IzsM:5zsۗ F/tH)VH6ˡzpk||ET?)MK B7)gMQdbJѨ b|*i'Vw;\=(A[t㐵lnXSȈ( 4 0$ #|d* h#\7HӔk)BHNqbsFKW1bVh(D[CQ*h€[6Y,{w/yW6ŧ^=9ϴ^[3-ww|@=K*&o\eS{,=Wuo</׊_B'y%<.X~?~y?[;y_ϳ>ků7,6=de`p@f;}I9{Ц{~J=><zzX\s^Y`Zs6v7͞3m14y+osm3>~3oKկ}gop?⾸>%?;)w;??[\2?˂ۼO6+|sz]h>V^?y[s}fL`(‚Յ+z ܆1NnMlnn"GmV#=cF3E4q^Jx n ZrNƄuPunxdEQ&3^HwijVR)绵k% F֠),#Z[ VMQ=cJ0OҨm8=?PeLZoJ.:^^ÁcΫ(عz|.L) <ß713(f~;^!~]O9w)dB9TW*֖ ueZL{ʧyX59壱Q@£tmc[BPnRE8[0tc|xJѴ |%ٝy@$e:MlnhF#ݼt6g8%ݕ[q.5cIGHk\&S)^UNsdBgaƪY^rz p8ҥK4y!Z-FgVV7$bٌ^d 䙳u!l[.-x,kAl'+j qRa|;rpJ,w-'OVW:ܼ~N1(? ttcYQB lZo7[.T^y7?nq+O-g?h3=o> oO"7x8k h萣IFt^.t$dퟛ &k` PfեPk|{k+!PQ"E`Ѻ,sg @dyBn98 O$ix?j1фe6,c)#uV fZ$iJ4„gS2'h"+F%z1b(%;!,.|WQڝ:neVYXo`X:%/g82K31R \ (bl<0 ?;ZOshQEŲe05/^!$Kݸ!`0! 6ݖmٖ-ilf޹yIZF}ˀr^XWH], YMe> T ,E!ye9|h(#S= (кF)G(8 c8R8^{78'}vaAK VBM9G닟;`7MA]yt>,N% ̀GkR{G){gNʒp61rpGJ/m첣ydY­>}O$B_TTu`|??6'>{Ke1":iOqa0b<+pʂ>EQ[nb0aksǯa20_Z9p n}'Il-X +!aרjƸ/:g;N/Z;ckە +K cNk .9|'O#ܨcTmp{gL% K t8.C4"v!ƹ4*|q,@Jyt%y4 ˂E`#auAUNuTzV d Kˇ?Q7tŅ~);a2nkЛf={Sgbj{{+*X qT$dn@lՂ,T fkkL)U]a!!RV9RX qcL++l0,̥1G Tsdcp z=zK iHaeuJdI8:.W^q %d2W#xۡyf^^F$ýjB׷b6QA IWZȽfkGTZtҔq^`o2!bf%|#= ܇2č2j|ʦhg1R5}9D!ȳD'Cg, F69Ua[2u,-_:̴eh8d3D8_fK(%> {|¼YT!Rk"%US!$樇8t`믹n'(]hsYZkWa$rOyڍ%\A}{­X1>1ED\rtmr/y#7_ Vf|cA/\J-8xW߿GW>:sW5|͖c`>\o~|p;|wG_!^㷱$$>^lWr3#:tln敯{cw|%ljsc4_𛂌yN":" /qkx3A`p[O{#|_y뿑Mg]䑪fGx;?KzCl*^]֧/ؕp_|;׽ gZ%NkYOfʕ\ ,Ũ"*~¶)Co{^]oɎv{{ڛӟ"jSnRJT1Ə4>iZϞ* 08 7vqR`ed;lnM+dOeybl#2b6iReay]eǎ27#%a@D E#xLn3IӔ$ R$T 1I9z2$:DY'#B`g|: u)2MZ!єŃIF(.9tԙL<0E}9z.x+"GjBdQb.Y^,ǯb:芪STL(ᅭ0hoOH#Xva$D#5cZPǂē2AZԉևoYq@^ʍknjcVJ27`d`S4ogkcSNF7밼6p0 X;2g ԃ><Q䙐֍9%=run뮻{=ٳgg˂gLOٳt:8&$\wqZ >s']w~F3 X+o7f*3pfնD6 >X(_,QԞu-cvo<[&`' })swc:SU)P `L뺾dsg IDAT{"0IzG]rEsj~4(mm}[R{oB Wq(-Nxd (=pو ͽ?jk]Pc/Yǵ3_hhmR彄gL}-:$ 'Cz'f#z`y@ Q, _U`0`{l}ąBfV< T%QL糢k1~MDʈ͍ì="I0 fAPŌ忿'D!a %,KVW2/.1 X[[ܹs Ni~-d%q\)(fG-C),-?DUk毩Pe>K?~89q+8@8 f,ͳ4,Bx+ړ.] 4mTeY=9GDiJ N8ɴːMфqhZ`QUV>uW" řLҕԥV/f% ab^4"7Pđ0ƚk={49a!F*koRd$rm(fC։,.aennn'jj,ٜˌfOf8tx8c) s,/-vzYRz')%'ODCX;I|2++PB"jTVA@GIJuHcEQL~X=xBQQ|W6 7ˉ)r~BxߛmҳR ܞĢ]p]m[hk  o$z& F:G8]cĩe3I'/ QI:,FO`21ݦ]Ft%CJf/FZC7X2 C/[dek$&mϑv9T*)*@+Thnm `gX?=V?dMONv /ɯ/џ֛yOKc/[P|_''V ^9{1x !Ę?+x3=7s_W^cO?uٓe|N?Q8=.z]n;x֫pF*qzQN6;R?3=7!<9w7 })_I֜˟uo$2=p} .%G)O7w|߳{ Ќ=pa> p089^ ѿoqO4yǟC_>8'Λ u_7K|;_{< R[y_oكT}قcD J\Ϳoa/Mf <^+y_voq? eRzEo?]8<# UH!gN1m=ð보iz2 FcƓ~>Jqa8FҮg0RZ>m{r=W\BS$BȊ@=D(bzŝTSΟ[㑇O1ݥ\ #=V8ei0ց ]FytH5tB"bRԕ&;t!ۄIpZbEQADUi}(i2٦*;2rѕT$2 $EfaqGeuu]UlnlPOh8aP|:a:P9:~S@^HˌaĭemYOb) TlG;~n^یP3 ׄu>*O|A̙:B(>s'g .BvL i$D "yIJ_ۣ]=b$܏G/=LcGbKe)E!X,z}lϾW meD3NCFF#68Ӱr;«`h(ڳ@ 뵮}xy\#[PI=-DDO >Wbvoձzp@3FvV0Ơ"l21?س_6Y`ȕ{ 30=gX!v{Q]8cg]k$tP*&06-l%Rx4Q׆6hU!1߹k$h)1QF0bE'׀װ.=H60v/.A@}`0 I#,,hj1u5w ʈ BEyV ʇѰi0#CQjЮ2L5.]q a΢M3d0u = 2<V^sKc%:&IJU9D V6[{A~?H=D"`)v$IHӔi^RIܥ6kـ̠ {<)-*NVA(Цn@hGU@kVy0Ei@6`8)N"E`5yY^d;M`.v:a:2[U67רtveavEEG?{j:+\~)4e4 $Y&JXN!(+ 9 )|u͸`4 u\߶=ڷXmLd<뾋_AQso|?zU^RkDgJ^3ny_uhJ7w|Oӽл BaSƏPPQԞr֢ɸ o:OK8={I&a}>|E3;GcsZGm=o}'ۏy|\t/D3g)'8ʨ䁷6w_+^z)\hP0/u5gm ~p[k,ӵிm>/i{G G9q GTI[xx ;8%/[Ĝ}`|pOP:~?Ə}3Xx37s_k:/F'C>o/Hy)=`uuI.;z9yI^UL7jM SYEA>W^x8ܹSlmmquO{gϞcc<nίuٳk0(.VWgr}3Rџcw0d:-HZ׮l7@4M*h4Ac=nbhK,5 eY50Aƀ +Қ1'0sR]Ս'x"aʂp1Iq@UT$!Pabho{~7;M:͸pK{|Fri h{~h|VM~l2׎Y@ZZ|1>Zo`l=x`ku]7$&JY }6tn߷WPOi7xtZ`Y[c(Pt:>@u1MQ\GI9뚳^Y^35ְs@2 BPkM>-)z]T27 vC$СUVVV|^AضF܃mi«NJ]fYӬW]Ǯ ICƓ]6c2ynnosS( c_`&'Ŕ$ )zM`o[n s|`0`4˲G)%E1%˺^6EGgO(6668txkƳ5d:J$Ex@,|A t vv/9~ÇV<`H v~!Ec Ȁ(J5JI1/B|Xs ]K]Uݬya l{%,71 {RNFC?A PB[CY䓜`Z_#$ \{*V;⮻#M$q6G!y|%:>e F!ZF4fyq@/(/TSO|α@P!ESO ɘ^U!0hC97BK3t2"(@`>FA;M5JxEB뺱XJS¾!bISkIK#s8x8 EllO4!J:x X?wCCg_{vFhdc[NQgU 8U,KX]^ /*k!3`5R("˧ Fwp&Ek㘍 >2kYBI]*Jc#I1c:FUXs}F݆?HA5X_u(Eju2wň]S1PlJM|3Q gL+K7j<OqŔHטɀN~y]678?ي,4X]I&&1*!L:L+Kx=IfL0=Z~jIY{ŶǓ ufDt`6߀*0 7{/Mץ#>{}/a9CK%Dq^^Gǭ]] ۱cǸ>UW]E >bk'l9om'ZOw]qV 90yM/b堖/!k7/)ԇo?T y&ټ18 {{皟Mrǹi-w+:7%;٭O{|ۮ^دwo?Z~8?һ;x-Q 4|!}֋e`])!˾۸_e_Jt}u܇x*?7,v;<_xcyisSmi@k]9!5H q¢?8U0%BN#b/K& 3,40N20̋=GmUiϸvH:TUc.劫.',t{rYiaJWLw'8ma{{(Ht2⑇Oj@P5:b;Kz=q 3LK$i=&h8M" CO&e.[7KK.iIW^ɴXA$)E>&ut9K5E !,(ɭ! `0qQҳfO>MS>,- Q5[{BTDznX]ls6`tn,`Fu΃~=@(/-By 6g=s 4/{yիW_餠pOe0 'Ocq0;cјYxw6ln$qNf Iu#4 (c@\cs>Rq᣼r-*:ulm6(`0&ˤ 9@ѦV*_m+tk[a0ȓI D x{0 g0[}e뭉0$i0NPU%e1ֳk]%)ք310ݟqC*/ɞ9Uq^ɘh$$/h wBF!kWktЃujVT՞-Ǟ(>H@v,tr')lZ i9tA|%q[D9! C/nYIU1|Mö`ƘR2O7g(9uYeRҿքRy^pL(D@@~7THʢ&FR]S%. "h2%I<[%:y bLu+^mSꊺ|֌t1 ֹҐ|LguYsR ̴h@z}U) cDž} 2vvvY^';sNFH=|~& +sDԨ4׷(򚺂CbzsȠaox/ز ?T$"`#y.u8vQjSP1;;;lmmx8F jn%aatmr,28TX]QTEDNLmgMp:`neǯpǝe}k vwT&v}VWPAN@^DqLQ{lll\tqxVR8MB ڇ(-̂áϸmV(kC3-j Z[jklo k z6~8,(R!j5n/c2Z4ELc[hmnв IDATRh )6XY^呇5%Hř3YW[RDhcFUmQVR?NO10reQ5Q#e}Ɠ!-N;CC>l ۋmq'dD+#*wDH5DZ(Gܧ('.N&$I-%oOUU,,, {NR9AKl PJi\gO>LUUp@/FSo`Qar3ɏ|Dvٝh+=/Z%/& :ivևP !E \>A(|0gQ9`2aiX ;,ڐ4(ScFSTCH")6YHAcu9#э20 $?Qv{'+@Qך!:gӤQfPfcȠ 0N2 TcB6(8Um a=x3޼"(/X8@A?7+c&Q"D,u5Qbܟl]MLuY]Edx!)SjHnY`:@|7J G}( uQ`? S"iM(EYQ; (e-/C>O>~lx_ޫX#|y8Y"v/D0h I.ܜ46`\%5.<Ԍ ;y/[~X_Wl}8o&){gtǭ>s;O>=1]oA }[#v ePmcv2W!, tQ1y:~ #iIm-HIj% G(+/y_[YYYT#Aimvtd.3BQN looD1iAdUTIܥ"/D Ӽd{8aOIO#T:Lc=`=탸.iGᄭ Nג8NӟQ bQP`';LΒtb2% @d>s ,8BQL]PC\"U 9[OVd]vwu TQ%քFlëSpg2m?btR&.րuÜ4vZx&sq>"KRT>(= +t:Iz\}sӌFYk,˚z: 5! )thMG$H_bB#gzڹMlnnSNg+n:. ʢbksg<'xeEU,#z,ϱecs]6Q?l=VCznA+}~RD'*B|pXS h%{ag_LkmtBFԍf0 AI` tmtX]g=XP9ƉCᤗ+͘iùZs}Zk8 H^V_8q,w/f1%[6v˄Zg?;ٽ77Y BuY[/xpELŬ9?ZC_+zJmj~*poYeY{ִj$֮CD1KKP\tE1 h-Q|pn'im5ݴ4i$}<MEر4F 53^/)% vvv8NY^^}-X;wѐ$I1׃ "Γu &w{H!y*BP9&a:1y:&BMѡK^5͘(NM֘51t^ ?ω}㷵BݳjtkA5-]k1gI'|e\{x1[xvӞ-J5 3[l j8|?#G*|F ӵWXFDm YxKY48 BS8a]pRA?m VSnP 769rhhCfY2y !ȫxLQQ\~Ubq n8#|}1<çϐfsh[sYDW%#Fɸ֨8gY4\ Aj*m*$|:eán^DFiD?JX3@LF7ES8F0lVfF jW^׬5g87plGFH)1 {¸Y}':gwg@lll%uQ`~HG4y%~|+s"q kjL=jP=](mRŊ\@Y/Gh$Jz~A%hJJDEf}rd!eSÀ8K^M~ffGkJ {]N ,̧3RYHeIbJ|EB% "*3@5j!}ezCM_TW˻o{[w>77C7#(\Gx/%7~ywp9{=8fu)1i|m?eԿ+.?-կ~zb)4! ~ -?grɧ?~}&&9P W 5nC>X^< R 5_wn]yʮo;s0A`J`,R(ND1 62L\0!HIh( 4Nwn9m=%<{󞫑4w$gny~-nh i&spwwȃ z5{_=|&__C?ci'`au1x?9=G3#g} 4:USRWچnHp6Z-hl)gyMfy%BX@@zȇ0>O~7p@cB"$1Ȗg20w ᢲGLd>ԍ1w޳=Kڠ,&AYiOcAD x[ Q7),:Jб@jNf8'ѱ LaPZ5FLvA'ny1avR-(,Q% $Z]5bmcuŜÃD.VZOc %l2o'#"p%Jts^Jn.4&nD`  y` !n;|H&\,Xe<}G*td(x޳6ƚc 4Q+ Xi Ds<198:&l G9{ɫ^*T$S˂-n(UދeQ3[v)<{$kIԏp\)@ ] y1S!D>fRnc0-IYDJ k'$ԁ,ӔY6~886MI28"P#QRyx+,u)w}PVm6>=WU:^{Nw^{#e1b3<ßO89>ŋlooy 6 _BLm8:::YK8gU8bG&"bM TMli@4,)<~JXĚG{ywJAey8{ l˽>ci(PpK3л>!|I%(Il3PRPׁm)`mm `>ʈ, p9lJ], ^!0`quR@XX+Co}mfY\ΙS,w}:x HҴUy(HjӰH] < F4e2pt<7Br\舃Mʦ.jJT>.^<bm2M D)gNfvѭ@JM6˝e<,§n$2I0cH)|"(uRrOl1z3 ?&$ՙqmBlU](c y{fcXO'jeA'2BtST5q~N"Dxxp|XaoxFwr"MJƘ^yu~A'''cև1PRiHAV*ѥRKlm{w=eYSc THL]sh)OV#_ua<ʢњ'DiF> mU6pE!$UUb]yb6gz:6DR$Itk)Y>(C4a]ڬwl!>MMX ̅({Re_D'_+yP;w=1[Ks1 ~swȏ1o~?/ahWD`o⟼{/+K[ \|ek__+WWuꅶ_?Ǥ66 //{xmTɭ?Y)4q"yԘx/G.=KL}A[mA2O0<;? ?ɵJY̤<?]яI5JK|ƗލP][yw_Ҟ[?tz߈.dwfm{s;NrZ֭|ÿo ?'lƿz==孿._'@{\/hC* fpG=􅏁÷3Wn}GッWpA=/Uֵq<(%kֲ,I ,tE]\.9:9ڍ\z,^i=.JCPSAsᵯ}-/}KCȚ(Rh!72($YN()O>sshD"Mw.ဣ)F' zfG3@oXcBE$F#!lhͦs 9I9yQ*L6ݣ gXHbGϒR%[ۛ$A#dfG",#˲p* w$qJD_ ~ >^|t`X "EhҚ!L$L @z N9gta߅CػplFXnܸy{1Xt7͗YBe&i_s4x :Ϣ,B(y1$* ]T{݌ N&`=7 锓E_wlg<@W^#3.*FKE$aZpk(zX"%JCbU`OrZJ+)'MwΪ΀ĵSރ(!jNݦ#su~] %u`'wq8f:> kEZ+AUO3t{[~ 8wʈ^\ <ݻt?ߝUDZ"oxwߺ3!Ї"2;cTp<W;3$Mw7n2MiLEQ-Zqq&kPrXR 38Ps}ϙOtlo\DQ¥;`pA3mϲ Uviw=5 3m.#Msi;;k=U]f9ɄxLi4 ~2ۦipP59!Jt@&l0XM6pΝc8jqV6s1!%?8kpNIJ:HC5g> F}k uw8NIfnx4EwN8J0'K>RkOjT {cNgDbTmçiN@^,(p*f@ߎ%1lH$x'Dst8Wz!DӽξKŒ W{fp[ QFIϼY0Qjvq(0FT tZ}enJb OEԕaw\(x:uZ^鋃G{Q+94R")pxa@BxSSp̪9gٿy$VhNq.\,zKEf`c?8 1NUU'lh@2a^r/i{q\ɂËUH!|S;>w. (K #H<$p{HwvM٣sNjf4 ;-`,[63! *b>dx!JnBgڄ=T!c 6$I vYzwcg8::b5O )5kkkT }?%]2kt ^4ǟ8ƙ %"e/""fGbv8^-T_!¿6l2i -BQ" %ZHHG)eSc1Ɔ %$2JBR48Қa! %t*aq$Jra8<մ,/=ʃkl̲hd8 5,& |Cݔ`v( IDAT׹6Ci"H9VxH)xƵw8L"meU;0_S=?%~_ S^3 Wy{ ;os?\:~d"_=?W'υ[k8*Gx#^7?^,> ߼vh7Ac> jzhI!Ʀo>nafs^H}>< ~g~? 74?68*1UQsg{/wO9|駿|&_Ι=g[刏۾:w})ozP Z?}nĹz@{~'߽)&oF?qoKL-lqk;ww'Ǜ?>dK*y?fT?z=1MϧaW9+tBq[GY?piqcAR6*-"'ڏF6KlnDٲi~*>$>~'sRx*#X8牤 s&s/^!ɥKw, n2[aߢs58KBB EeB؍2Flm@VfxX騼1 Q)ZzG i42yx< 3\yq({e~F$yp2fm~)iM L㰶@IMed)CrQ0-ϗh@%I@z ld!D*×A6ۊyR0>-$2j^c$);?B)u]PD~9Ų'!(Sb~WXriZ&qw"*!^Jtm]߯|`NNҹ^Q p/d5Z߆/غa,Z^wH뒃7|2!lS:c\[8:#t95|kU6R -cFAG^6$0ڰ%/eݔs۬mp˗A&ulP0EQQ |x}n1Btmsܹd逪E>ż`Y,P4[,Qw`ơTvo}x;m<l:˔eI5@U/a6-%MӞMAQ;]0j$iҺ  FC<eYN'$Yփ tu9üdL g{ln[ ; L۰(X0i *iZ:=5ILѻdc* y UmKZb"!$8P(R0c:rxp4qZRAjS ǘF-eI?,Mx[V{BԆ$9,Ø_ʪƴףJLmB8g\TU u˲ Mhv^VXkCgw!LP+9R>U^Ekyo-Y@҃N\wMQטΧԦBXq}bUϾ.k <Ӝ=|t_Tm(e?WӲ[[W޷Jk@z(yPQ q[/H#D3=ؚ, }}=llnb!Dz0蘽= P -ꘇ8³b苣{.R;%:j!#|irN!+rm5- 6_tx'yQ"{lldOOdN:цno F@#}V*:S `t$\)3c|}={m-g;*w(ᶛ'T\/_,KְF0-(8r8kZkX E+HE^ \VR)nJ$$qù-Gˏ-r-?*`t'8*d01V!(3c U *xx$Bj(8hBe3I%&,CGߴ8<۴Xk\S BTA ZG)q:XU4MIHIH#EimESS6(A wv"V4V( iB\ :7Bpq00xn !M.QTwrR-;m+[o|!D8ʗl?^qle~-?| D+f>ov"Ueѷ> _ Rwm'ǿ|?~җu?p77 q_S߮{wI,z?? -\g$*ٟgӿ_po_篪dw;_|׷}K>(K&zk#?I_wYuV Ѳ;ζP@ B,c^Uo'W6I_~Ϗu+HoN_C6oowZ9uODCv_ͼ{?btnI8yqW>ꅅz ttHc[`9@84`Y i`tL>3d F$ TU8DزĶ,|=\p}7{{{y]lmm1Ilm*!TDjBiqKiF, [jn8wEƱl.u􀞔ƆC'(d64X#*+  Y> /Qbl4b0\c0d<&IŲY0dng$!h%O3sȽo}-֞z^5*dYOi"ޠC8AX,$(Z#+|;U[7y%Ha#D"#BFHKqa[,*YP( W^MNfS$x;ɵW v-MåKݻ.{/}yvT%i::34-k.0иƚVYu$iobQ0MlArr<^yf#\kDz(J .< ׭S%I[*Hwi3<{gEJȞ!wk[Uump'0?$hl%֭aX,8::iCIDhzl.0O C<';moϲg &wlZtj!VVyάw^ӱ؍U=XlLeXxꌧt=%sj]П$ s0bY,5gLp|ͲI1_pqhqOf<گOJVV,P8Uuceݳ;:x|1eYj RhP C26 #77H(UGq Sߪ\ OUMi-ol3 w4i2xHBr@H:X8 F8JTkuVm٩Ɔ5/`6_2_\\#Sʪ X,,3)X,&"E um;G]Wj۳JYUުX~;"clMłɌ/&~9:ާ 68۾pm k%$իW984 'P|i3@Eȥ譙Rؾ]-8,H  #-I'NSӠ"``3Qx 89H)~:Q:ٌx|xAX;wpk,Kr:6?q %#6CI BIij@;yYH$XkDzIü'MR4ƃM6vҗrzG]Vm*"ؚTUuhUDJ*dWݺ Aaq~NhkBzW|'888@)ō73[I<(܍31!-FPj8&Y OBش$iE(Ъ\'M~ws矿EG Fm]gW }raws.mpR"ƴ yHjm(ωtBB1Qb$@$K(NP`%qNRTUC:Qum0B:K(o Aiemm ߔxB8Km#mk)%Rwh, P'/N}2;fI0iH oĵ7hɄmJǘ# D+*.lc'[D10Z?G:^x9d(+,GDg}}k" '  i3_`{ 5D Z Xxk*cij/O3],9wŜw]svϗ_A1MXz5~>Zk677ecc4NXP`}C,gld U hf}>֚999a8 ^>X>~rX'''Ub6=2U8͙- p֚OqELmW|d}AO:٪ghǒ Vl)8ʿ{V!D`y'D{8 /|*Nb[H&UЭ4a(yUEʔ ʲd6 9H)+k|yՊ.g{2u7=z;{Э%ѫuwX}U}Q i')4(: t)eoA%Is H<2_xqLgid؂ Kkbam<5eؒ>;_DQx'?7 #x\ly]4)%,[`)McY,Bh[YVX۰Ic$YF&%ɄdB60>KS[wvĭԻ2 uQD(r)8e9B-K`,2uEQ4,CE 1E)ISVKU->wzkC I!Xsrrl> yEzt iI!o=oNpF0 9YBjm\ oۑ",c!;3ciiP8q>)%WqS$դii<5$śӣ+O{ǿ>Ο?>b0ҥKn+˒d6Ux<ӯ,#M" %TXS/Κ4R:uc) qj6iJR77J$iXT ^w@S̩k&.l__JNuB5蒡WmX@Q2DJVW8$ F4 E5j+ijWVr)K&,By"n-id lIΤw$I>"0;er`UfUn`R i-)H>o&2)@1m>W^|#̨Hu-yJ0b_99>0(^hz TއItzA[,ju|(B!)r9y;]wxmq" -#Hq6B X iSDٜɈ7Ҙ,0&qp?y7{3=9f:=$Rc,7h'I$bz|BQˊi4x $RD˒!fΩEJ ɆMFTsNNNώ#"q.Znj7wٟWDNqŻ׷)^㐔nJŜڈzBy666XH_d`톃Dxv"pv/`^0s}+'>mm>lm x2;jYx$ kkklooJn>ׯ_ҥKhyCr@ǝQQT GocI3XWGsL89/Xf8vB!XN<07 ulVj=N]$I(˒|ޓfk+ˊݭ> mY[llXTDqt?AND"L]I{+s4 'hPLlv%FBi- Bi`0,pDOYm/ k|[Rք> d|>gN& UB, s QA0'J IDAT+b$⮻b4Y. 9-d K rHH%)Iv!E`4 $.أѫU]S%EYK& EyHXb%hPs=FC۞݂u%9}tՙ 8w\֍|P5;w=$Ν穧bv|2~Bٜ54Mj+Z]>oP ۞caqY"lQUaԘJ' ֔rw vɀQA6(˲߳prr:ZkiX;/uNyTR'looizG/?.{\z32]IjU@6$ Έ"3&ZPq`ae ziL`OY,]{{唨jXW ;/R{|Y2nVUU<-ֆ܀P6z)XV}ᾳDvMcퟵBUբpFk'a_݆&ԦiQ<(k <3J@ -5NxL&TƔ-ȵe7K")Y>w_U!x\RJvbANHBlIݔ눢cmHo'1J6[*BHI%V(^cC8ND2T%1`,^;): ڴ>g(dR99l?DX8Od1 B;XaHC'ZKax$CS{(EQe< 2[c(,m&[:pOݡvŽ1cccccl0R5]G!а4 mmCLU5hXlm,jOm *Sa `'BpUNFlm6V^ !h '| jSF),gDf80Mc*YX^2\vXz*)xMLcg~u|Ebe2X,:f0L>[m*5]Q-*B*K,)e) f9Ӡu}l~d0JX8,1>g[vmU8Y.>tk:`cO~x7MC,"<_9eY LュדO?b/llro$MX__15Y6`}=Fi̢lHag3x4 ?g6 {$Fc+5ku$Ӻ:c [[qL7AoBZG !Y\.C2׏ŮI60GJ)BSsHHgS!u,jNSbccC`0k6J vv3fGhLZ4MT->de͛7|2AN"gw@pP ,ˢhz [W87k$9_b=kUM$L g86+Lw21ڬ!9$ 4U]{n'/ȓ.Uk˪Bs"<"$ zG[m5S0%e9I\rsRsvvx:'EA0 <^4X%[?1I(,Wl6\2?:DkESoTQv>e4k9 苗~kJt]N*EM-kD1-IbqM%2ͩcY€E4 ia6/v`xܱ{)wK;ABZkH+jc9??򊓓3?zB|@Yu!?%)ß"BBz0VJs~~xcA YxO&#r ʡfyA-1$s7n@ _dyDD6}HD6Ja#dhibT`9 Ƣ#M'!,(IqRnT݆Vŷ )a\xM)%Q1qFw[5mCZ_禭)-+4up5M(jzWEzw$S^|ɓ/2g(i=K.<{1 ţG98:({p~qEQdYt:gܱnhT UU1Ayo Vq5M&q=/N mR5 1&t/_C͒d\c$X׼~!nЃU%\G>ֈcXtA * ®p!Z;D7nǁ'"N}~geFg?$7D]pֶP5jO|EW^q}}ͽ{trZD쾸һ֘ bsH0βlq& U]v͆ nxcQY]^6d 0g4zͦR R%>wi1шh4uMt@qКZwMs>(J4MuJ1턓"Q{<ֹsQ}t1 qvquuE+8A ^_3 |YknB'TUxRE8.43/_GѽAp5iqu{1P5 UU#cfCc(}5O>,HD'w! E鹹sF)Ň~t:e<Se]@N.y$IȲiadJʺFYzj>a440キ-ZWՇٔi$4MfA]m&M?1Պ.Nǜ8<8%&օn)-eztpLkjCNOիE0Yxy7uh;!8K3T|pJPxJJU܉!-uK芊 [C Ix$w |T&dUˢ( *??!Rk#~w=lޤD*Fkc"C7 ΅MiX")İN[#K3ځYm6D"pzTM`j}!Fr\߻پZt:e:ݰX@H$KնDm-t_b!O\n"tx@:Ϲb+^bHUe"BkD.45%5>5QQjio,Cn>VZ)`\s.{IQ5]VGYw~\`=G{^1\E~q?Vt߳޺Wn< !3}~|$$|ge狢v5Q$C!z(vш;Z Wo9[>k |IiHSMSbkVU7q)%EհUddbySÐCB) 4Y3)gog]O?00D:\MS!dYP0(Xk :7(²l(Fa={S I 1: kZ "fǡfrxÇI㌟'x g'W,8be0u Dg@*tP-ݒ,98:n=Ozu]srr\-s<{GGANd<5m֯jp%ue?yEP/;zy8ٕŝyYuJEEvݶDJES5шѲ,g!lp;+9겠mþ( aL&Yƒ.1ZuA~Ł[(5;.7pm[3W{E$, }7h%o} y^b˚ ƛEM$CGv1p=yunDIٱKW4Ֆik&d㘪2J8[ұZm|Ō;Y/tt@x t<Ez~n;z_10'#D2;95[_W k-uk(l.9_}s6;k DyZS+Ny;Yn8N0M+4[yrw}%/_駟VZO@-c ϙf!wHtjyP2ġtLUHCɀjƅ8R_{΄XX4>?c1u>]PUU4e7N)8998f@quܔNs;}-At_+8{Űmܓr=͢d@E{4!bI 펿⻿cCOIf_ P ؾ0BJŰy#>p{Pv6qR q!x눤x,rpA΂8ѷIQsغj5{TG?n-,{z<Ǜ!pۅҏa$n'A\\]rxxwO7\g-yx7G!hˆ /#j@D81=fdL>+nk5 h 4tT9B%N&rIbTo 1 4l+=[SW4uE],lϾqBYDvecyŧ|ͫW?S6뗤IXĎ0=#E갡j3:/1;pj]1 !Ȏ!y|̻sbEuU&)R!B"!~;8?%UۀL.|Gq|zGt?358MY/QQ ߸mv_G3O`'R`h%8I7X7^E9ٌwy2eϏ~NOOǁ :e|g懴m}>g?.CQT\o:F#:QMXʛƈ]n~&st߅׋pop#;,;H /m,31Ʊ^t<)ZE$i-m76|.pkcpBH ;c}j8J4%[A g>o1\WWc̝0۶x1~ڡg ]F^OxЖ}𞵖a)B`ڃnuK+Q2#RbGS>RH!5jɓ4"q6\/Y\^!<#CSRm-mkǝTi-,D(Wb ξ[tAO XM]Z5S:$֚ fwj-QB\j-$҃p4Cqn kPQ1%%gǴd2AJ9ojJcֳJcq1N(fE8*D1QvgϞrJqzzJeDq28iPQjpYbX{L$` ?g,WpgQNI4B(lLUĤIBwWrW֠%T"|5gH;KZb\2qϟs !D8"65'v7p5ŗ7}"v֓f;m(?NY7,O26xsuyrqc4|V/PUaC(b50쬠BQ@)vss>k ZJS,ܭ(5EaEZ"%Jquϟ/&ce\3Lt>cǏd4qztfa<^/ZPgR7 B)<|Oh:?i5BxQ`E]]Km.hq:/0Clj/tws0<{ &M)C7/1><"Œ(dYpg#rQmꂲPXC]g_P/qbopwa_bs~i X].K5.//yш]rTuN-$~eB:ݻ(U.Xv˟-_ybt_ _N{p iP;G$%Yۦj "Ul\UKk,8۠gy*ж[iqΠT5NQP:*&he<$b)+6[$iw79#xʦhTƣa`uyQ0>8vLkG4ڲ@NXE6{QѷExpDE1psssvovhc=.dKbt6G9Uxh*z|sR{ Eg1C"uG""X#U51N kT`|UW5VHt i@M`@'DQOۀNnjeLzXKxp1Un:f8IBq dI, ֋KV7W\, "dTgu-rLJsfłzrUppNF9?ɿjd<*ŷ?&W(a4ȳDGZdh@=Pqw Ba 2HnBd@.ĝ댢h42$%VBqW19s+ڶ'Bn N{=ayyMmZl[ 8±|MhfGl[#?Vٔb}?|{_ bA@`:P*l,[6-ƃ0pqb1i7?>Gz s|;%Ib >ÇQmp,Vןqn["uBfmAZ֒Q\a n8`s׋M_wwk~N_u$g]dوhBQ\\\G O&U~jN,q#ԡhex1 o:{q;H:h+z@sw>GMlۆnޭ[AwWGQԱ)þc^SEJX,Pꢤ,^#<qzrMM|yM4Ι1?:4oB$O2pCO*6jI*ґ1q'OHeͮ| sVP2 ,# B mwFbC/a~YkΝ^S鄶5mԪ$5hPeߝ` #=#:[XQEsv椱DnîBqY7$qF;\k:<뺡W^ӟ'_|Ǐc Y`֧Yޗ1M#B)֒zkDQLiUSfatzIg%ZǤijj ۉE[\Y+B<.=݆j ғd,kBJ ]Cg< A@ӠȍGu_>iݳO{VP[ǧl6y&b:Ҕ{@,.IݮD[~tAQֹq>?d4cLxt5 Y(EҌ8Iu*k!R8Ez%Y<{(NZ|S_"} \\\FpO-Y?޿Z0Ʉ*;vz}xv0%>2!I'~U$]!M)8?Q] 1 fgiG*)Cb('IB,#CH*ᇏp+ϵXAb.Xݷ*D'{"I 4 MJSJG1 o ʺ"4#N8IV UUP;LӢ\[k; lC 9(f~p7WxW?9k~[o}{1Uw,$e+"kdVbW=$IC%/N30unWfx'P: lPJ( jC_~Q^*v<%ޜovo gZu'88Pi4lLH-p>,"c-Ib|GdӖ̠֘WW\^-֑9gf34]ggg8o d-!WB*m ojLUE z"n‘%N"zs6m&1d>?a.<,#GhѹuyST];C`*babIްZ[泄X%a^kNO6jp˟a4 W!֒qzTEeI[|d72\-WkiZMK׉dL'DIlC& N~$ߦ[W[As6w"ޣ3X^/Ynxڌ錣vKOSd|c 9/h?$qI\g|[_NNςa` no!KAc,dh U‡dmY / ?_g PI"qrzI`Z8nFa6)Pj7gonU뿧7W \? -u( ֚`qՋQ'!mkԃn?5IR6;55 i'l[Bx:^?k3)oxpފ@wÌnnB=t!؛$nWcЋQycR'I2|>FAY}Qv\zIXNfTm8{. U5md}O>eI<oss 90 9JQErwIZ=au&rIUUz?͆$s(Dc.,ˆs#98Ǔ;`:;8IZ1\#|L,͈f\_u|x ݜ@w"Ƴt4bm,ζKZ?9ÕRyNQwv5{{ rP="jLE8k),8<">yzzW_!uRDZ9hBy.,ԇpLr}}gEZYC͞fd18Meu-RBg_nlfNO'( nnVdو+=puy}oog˿<}<׳ e%)EQAHm xmgLsRGpz%^$^vY0>ܓ o`޻ł8QBg*l(Hi% I!Rq!{bdoM_h E<@JY@n;#=)~pO'͆o}[rO_\f+*bn҉Nt(qxw]RI2XߘvIimCGlk_'1I{RY6uIW_ybtDQ4!H٥ڇNg[ /$"ux4!&$hǣ 3Hq8.Ir6 7^$:NPQX5dQD|a-ӗ8Yqt8ev!V)kSӔ[bB8o+"sl w| <&Oc Ud4ɒnhi€aww1bYiNx;ގ*cX92D)9::`FjڶY F1*jQڂx݂I,GoodZ ^pO&ܻ=ш5bbIGTH SATaƅ0tQsK$O>?ÿbkvSʲ |s3#.//F9Zl[낦5(4A\T:&R*T o:íznH:wTEр)I[1Ϳ_wgkZETu3`zQikF.rdڠ Bҡ f ~?- e/h & qiPP8"&t(q6rWptk-?C\\]?'x/Ӷ-b3`(&NvJʲ MSF`F٘t.Wʮ!:W>:B V!@Y~-Z|(eCaBDy N%GGGUKU 4 +)ё"u!:~UPFgi6(K-itd2g %mp |oΌn[Lےe90m)U]3͆ dB4IaNj#!ǵbm H$)h~>hcr="*%1*vTnX8L瀤Ι`Ƥ)Z'}$O8z[^,dZp4DqLuS IDAT` &ӌ6# B*.68.W<$\]]2EAہBt/8: \N+voQT;J)6yQd1P%5Bj-Ys4tsW@kj6Ś4`HOK3AaB >1: ]Ɗh%/%!ms슊ϞݮzM]VC*f BZ4VApk]hܵ-(t>#iG'J2P6K67 b%͏ ծ4mjV52͚CvWW-`mSc}Dak+ GTdy2y8!bE\nuDƬցwJXp5&0IeBxv,kV)=X=m s%Rm[PRryH'2*yCߵA VkAZkM뮬>@KPA"k\c"Mt5"\iI/<^y{ MӐĚ$Qס$I'WWoL`Qbu3} ߈cmkozT^'ܕ4eCg$s=[4')«Aɒrò{x`>)5͛$ZʺU'O=l8N#(50p8,B9lDht-ZkfIP%8HJ$4+'ToR{skp-qv}z}n+_g~T _8%Uݐ&1kFp~25ڲAhفq PeP,YP*$ !=}/Q̲9o)˒rR@, V\#IB8/X.7u`m+k!t>SU咇ݻw3T)Jnoocx q!U4X}Y!;Z_HA$%I}V'<8 kFK&5,0BGAg󔶭тi[Ǿr~~N6$y(Hѱ~xg|/[fk҆:l:nnn.[6c1} @ۃO'4{ UH'iL'85Xx<̴, $kS}QMjb{SW sJr<NNN@%Tuh`d9C#T`4RCv,g'kQ,v: ]5Q,:7yI~ X i; .Xq|aKڴ!D%!DIr XICg-cVd^qBNg]XzQ$I6#Y`3h{%\1B~`v ©( ;a'JkM4v[`#9c<{~bYk~ObNul6w8ѣ `LK5DiHbi?W_ao?Je޹Z_l.9lkŚ7/)oX_s巿$pș%17 ttmEQmW[xC(lT $S=YG:nӠY0G,&\;0T"$fgYLV ]gl|f@be`rNu]SWAlS,gϞ4 ?o_r{{?,tm˳gx-'k-yX-}juBe?O(ꚫEȓ>k\ %RJnnnnh ~QN&ky\wVthP@׬!RJI%jGOɃC8pqF2tCs*񞣔BMLO3( !Qښ ;F5&sH XʮA'9USGa^*2ޝsH!0Ybq }}0KEDU|=tx( n!XOxk:Bsv C`1;L7 }a\׉X"ݖF8ҪD *OШ0E#$4bǮ,Hg`P{lm@IP(7u#hᠰ.GjAhRHңTD7^/Au˂pt%bg=><R } >#fP}pe~gs3K$*hrX8FpH@IIc8@"7I"g۰.fY: LtF7IҌ(QQT%Z,&97Ej^#qd{Y{l1cִeZk(Zꢤ( ϾPь,ABH M-&N4Ifnpp.}6MIN>}'-˓yZ߳\.RۛkZ1WWW#%E Yj۞'ny{}r,kvŊ:Mw`c/X!~F(HQ4o CoGo7oǀ1h +#Qi$$iJ]Ut]`- NR؛:|{%h$Ih.BL!K ; 0X:ף#1IGvX^f!( ;5>}KgZǚ7hy,E= &at7Zsww՛p&ܫ"h=ncZkmv,ĈVBb,aB"ZʦFiP8GuxiJ >05HOk` <=nk4T";鱤c>8⒛;8eMSQԘ *&CK(pR4nJnoCWP i aO'TUr<ϹvCݔHy\O9;;GɄ`_\. [5^}%5ۡ MX'yoo=U!&VG[Ib6D06AAs/ϠԚrM},UCcՠ#6-t14^u V3pX [RˣmqN]~>!uPFa϶gݰnK<'5SNO9{h ERDJ qeTyN]a;QPE:Aio'e؛dcͳO͛ ՜kt䨺;<j`8㉉ཧ ?f6NEHr5IIB[wh c\hq5CC#:u/Qf)2NpN[G5HZ:Yә7(ˣY#Q;mmNELrw{}K֒VR"o@( !R I$ȇMq#KqpX Ap)#eItDQD]\\Vxx}?2=b{6iS8>h4=HmOBDzNҔ<iALyEQ "bC2(.~Zk4ȫlqcߏKloěwA<*,O3oQfl>gO!WѣG Sv*]%$^u|>7햢(ϗo_/+RJ֫f Ȳׯ^b鸹d nZQ55?3)uLdM:amGhf{?d`g=/֚޳Oj˱}bZo|NjbI}(v=QLF$Xt]7݁ |*JwWW˩ax%k ~G_t0M@-FgW7M`h]#4P#p{Ȉs\ lL7Ν`EGHbdwD&_YWd">8;=,n)IӔ7k 4S{8H|V3!CMǀpq_}&Q#K0q9apd$quK4ZkVA q~'ٟ9G'D`($R1#D(Eo(Z 8c>pMSe]q>i+a\ӿ[vUU>|+fu]Pifeߢf{A7又3 8j# eE[wxʺc$z08K*RPzjn¡a y y>,g8< JEl#&i4aoLÚPJ, JtvlaF=N` a:)Cڞ恛k}5Yx)˚vs ʐ*wC t8dy3Xcu> $<)+)f$IQF[?'1~ ؗݖ(3oo\h#s1TU5&8JKTuA&@J7m }ISUx$uл.$ lki~qn`G!ȇ8 KR}w|$$ثe};Nڪ2.HylO/`!Cj|9Q<Ͱ]BI6w1,:7^sNNNlYNp~dkOQ(Fq|qI$%#dġq2TH6w$)I"MFdNTN3grh*)G\>zc˙<_8mp:BYHTP:ɏIp-傶mY,| ̯Ǐq} _5Ji4!JvJB*EQP weNR  IDATQ_z-gKV딦t1痴m5_s87.22ᑮ+4mf^)JUMXT2g@g9:Mx`f$Z~`mVx(MX,, Y\Sl(~sA∲Q'Ϟ~64ωD43_,'F'8$EP-}Kۿ%cf9nYV|7!S):svq|>۷\^^R7qejBE1wdy//yWCCt-?["0#*\0w~wn@5d|\]P%c8;;#"&FERt2;A$Y:={4,jn?}ق {X׶g!v\;.JizLv;VJ9n@1m90dA?J]Dː1f gKXxɜO> HqΓduI"ƁR Az΅0ZguPIt$Q)3=w{k1w싯FL k;K΃NMB<:CT`7Iٹ7y@4!M<ڻ4`. r%hG׌džsɭMQ`}pGI2M;);1!'"vl `||.(ullnP8o 9-w.XU91\swC۾/UPEPe|6cZP7=uk1])e8g0Z*CC=M-T!4Y"-AcaFjeɓ/=~k0zDC7Pθ)3naS&7uMGeY#HvL20,pYBHJx,Kt&P Kc>vh]r9#uP`4C}Ced{|q|2p9 9<<((P !2 !Uw\y ?&U.nb...(8 |M6Mv{u]S5MӐ/Te3:қ SclGރyFDZ1{]S~,ڏcVob1_ezL`|$|x`%s팣j:&0[iKpۋY>gZgdi۞}QFjc{"lDZ#/K4}a?=RY3}9޳,M|~5MC?H,%ZvDi^cx^9g LEQPTf9y<'1'lL,}Z<dzgl2ƹ82?>ǜ?0]óДJCHSf8|: ~'o<+(b8EF恵u˓s,A P2k]Kے,t䳈Gk-={NDqr$S`AHaEQL4Mvs.../>$IX,(suu6Ellui x$TɊww|w׷uA;"uP1:0Ŵbڧg,qX2f"볾:P2*?{ L`no8dfG9Ŋ<#=S>TXbKJB$Z b-#i[޽믿G;vۂnjgˋOx5} 2es0#;7]D UՄ@J\ gPW+H)R{0*֓f9im,R,'ϸ{nDMկagkqCmp`a؀ݕe]htlvۛsTU$IE!"cN(4ECf"9C6,)LV1~_KoՂKZcLu2s|wFYf$ve%t !%#OR$%{bKE,yauH'NM -Z8-eQ \ЄCl貄Y`/pnp%(l 98Sv8Y,Tj YJn J@96-"tr(;|R)8>8*H('PV{dD.y`aF J"4I~;cc>!=V^Vu8z*z$mb2i>yHx<~Bacpظšxͣ&n.^ Aben ^ 8l$ ,)ދv%mK b1BURkx9urss7wG@18.Ƭ60.D黎^Il}-Glon(ZN̎ }Z( :ذ00Ewvvk9(nG!Pn;Xv;}34$e) &tʲi @R*CSKx"H6 ^`8`5 !]y8ov]G׶Cs,CGr0pRz>GbX)I+GE P c^+ʢ"f8SNOOŋE1y'IA >24&j)YFȁխxRlʊ߼7 Ŏv (;gAeh15FbsHJk~bcc[(I9cSj ySX,(;|[6wl7l7$l6 쌓-sPb>y )0#t9o0m ? | v³|}fȑ˚Y}.))s(lƺ4sz~BQ0gg}Sҡ1@l-yBU`u}P s'|hTUÞyxu)D@ FzaZ%1Y$ٜ(J(QAomߣn:ڦ mgsegە4Mǩc" EH~?[,͐sw{C3"B8YCwsyȒ'8k{wMӶOQ/Y,fH)cKX),Јi;fCI$HȒpȲ8=x  S4隊'''̲+D' :MR*l|b3/+IӜ#U2J~OoXn1vz?8>lFPDi4]o9RQ#F8;O8cX/p^b#pl>fǟ 8)%p㘢(O,1,=w̍}O̜ /{ eY45r{ܱ5.N8;9 o8V:=9露5(%I%2h:7!c*hLEq4ȉ#M&,9V% ]#t&|.(+=H'/ֆ1еۇ8p^X _ǓqJ)GHv)J nK["g{ ?}J/g!Pkކ(+}B8/..(˒%i'~< peYruu?~ 4g>_E`ajӧOϟt)O|Bݴ|||Sޡ?~f|on,1I6CD i}MBQK1= ab5>Azh>}ǯjk-Qc cLMcZ͂|AyOV5дuж=M[Xu]O_g@exbwC3Cqbb%3˽yߓ q9'xjXk^  J :4ȡq -]LZ")%jZ@Ugzڮ {vj]d6ՅSyooo w eG(IӵӞ^g3~LGl۶t]wdkgSǍ s(;f3%BFQd$)9@JG<qEQbʊx%Xkoؗ5\/~۫WdtAQT{iUǏtkx-Xd*;v=iRu`Fi, ĉaR{T3gggݹ?> )%͆|W{nl3fm??}/,Uې"u "/ۿyMhKMU? w󸷎j㳕cۀc€1%d~ԡxPdyvlpޫ85Ғ,͛WprkXO>Y&Zck. ??~^kpI;(Xk%av[8^nx(Z(B0A YW*me[UJSa&DQBpMbpX$AcdI3aDNU}mO&냌*#U~mJ6{R7R]u>5X/)nV+v;YPJ7;no쫚?o.9?iZi>kڶ&fql)tݶ87؍ JJBp]+&a2LԔky ǖOՎo7Ͽ7k1?)1]qqqA\^^ٳ{Sr{{bx"FQd9Z, AX֟}/^ۈX^28rF:I1Q,ɪ.XCۻZ[1=IFѻ}9浖nzb3\aӧTUkj8Zr(p|| A:8&{-X1'(JX qa}wU4ev+,O$7OQJ6AhaXpss3Իݎ& #"z_}2EV=Ҥ!4'`,6lbz@&BMrS m :4"5#2YZ-kDmK B=A ji>(vĩߢ(0yx#0-Ȳ _aggX,朝Dқi>dn- *g싒i1F.~/_j^|-?'Ijo~Y61$<<;{lٳgq-/?x%W7w9GQnRQ >ֻ]APjEUȾ*8/M5h+>hEf':lI&LuA|zm7ߵʲ4MGb>F\)8(7[6,GXm1߶ IDAT晘jh5hi*91Epu{fb61?:f2wΫ,ޜ0tEk7n ҁ<)ʝȹ|u5I"/LG:LA D4:ph;MYl64e% p))fQ Ym7 ^?մmC %I"Qv!lS 6P݉;qP eY289;vuU),k5M' ZT֗\6@Y6!OEYc) qbN3|niF-0XZme0VAA(7/_)_P5b_ƇrAU..ްۯ \ې/f}3@ޥ ukL4Mд-VHe:bNtxhJ{f~4HjNy m'V5QDD$qbb-UUVY+BgqD,G4Mv|*@pY~ޒ'vc([kZg:OCfE-v:@d.WcܠHҘ4KhjaY_|5Œv?-=zz٨OhGS6[퉌du"r&^ްX,zES,sNs{nn/z_|Ax/y<8_1}zzFeMӔD_~Meݰ^__-Ϟ=CpQߗTeCe̤m4)χ2U { ;!p5/"lϐ{Nk8A鴗bj?~U}G'r^R4͹c2[%(VA[7\]:Ϯ,ZGQմN<'{>*qg9>Yrq<"SKuA؆׸Nc0XC?/nբG@|(b6|t`_U`bp~d ~h k%DqNlu|;C+߳pw^k66I, ޶5|ߥJ@CH4Mz`WbprvjeH)b z=iBn>>? x̕#.ϖ1[5.@6ݯy֎l!=3{`vޏmKA{g<'Zī(o;,ϰVS]߀=ӎ1Y@UzQ9jd6%w/AZUu-!9Iy:l-G5@}`\[ Y <-}҉Gaq>(qm+~^!Wc4٭oy51Ӭ-痗e`>mK2 `,{uY&$6hpgiy-www$Yp}uۯyBRXuMdk-ht]C')M[58WW7dǏ&9͎bEvQARY2/{I kd"!A4ctU)MSQlwyR)777<8>A)\TO)\<(4Q8ʹj=sZa8Lm]NJ2C;%Vy=~'''EAFĉm[v8G}(ǀ&Is&)t$?/yCɄ'qww <~T99PȠ(Nώu<|􈲻m 5Mͧ?ɗt-T?eS (c(y-GËpY;t@LYt$bY'$}`-x:!&hAiae*%t}w7B9^WAajp A|2KWztlH7˹p4MãGx1-~kc"֫[f)77r&l6|\]]) oL&|{~mY:WTUyژX'Dq|:a9q\bMLCƠhMxNjڢmw56e9_\qvG̗$IFj5-MYQ v lq4g4har5.xi75G,3M]__p}}M~GX咦ǀ#k-iSPBPq5Ʉhe 4ud:g\r}}ɾV^uGy[V$&R>y5MΗ\݉Լh@s||>"ͧs^k%=MSЖ[LdY8VVbS罀86Auo UOUZPzjע4Hf yis>ޖ$Ѕ- ihmw#<+OA0itV1bAZa@C4MKb)j+/!vË Vx8=$!<.<d!Rw7'\oDo#=mQ\4Zc#M ~B xGK7 wtuCSVVQbYA%DŌ,q N HF\ =3[kCɝsՊ}*цi'M?zrGS^~s͚(꒠dES:dZpB~b}B}}9nGux9;묫+9!~񳟳8Zx\\\gK%ZRMGPHPZ k'PhA+Ȱ<>,(eV+֑wI,JŨ4ź@-lYqX<:#αQ%$n/$kǞۯ+cq0Nf$q6KF ,$ZC/H&9?%eYM'(H՚fVW'2N?>'c~_nnnHӔ,Kmٳ03^>>euղ{|Cc^s~~_4'J2ZIlyL,ʢx1"R|{ܽ%EKPj] uX,pn@뻠!;< O-0 ɿ9lB&Htݎn7՝sAbqb{zVagX0=7kfnCi<%{٬3^xס"M\KUdF{ 1{v]G[7Jx뙗^ݏCÇV1Vibc!ґO"8g8IIf|O2{z;>14/?ӵ),kL&ۭm8y>R򄾱ne@\]MώNP *"N]vgRig36*(88mD"(Oqج.m$I~e61Peݠ{.!Y+ElqodY";mk3p]lw-uUMC(SR]AEp~S2M"еyxP>F^lC=y=~rq ǢyiZGlv|Ͼ]rr_#B=;e.?(H&-y~Jb`ݒ9Y:_9 kŇr!ϋ7+|?c77,2bo%IBhe˃D◔9h6,v5nAk4Z=Њdsxv0Lf9˓cAD{S5uۑXBSwm ^++5qsssf"K#>̄n\Уp1өbWtE=Jf;Z9$eY?ţL, ...g5.sMz_}_i#%6:PF.>q=pbu5xO*T${)G F3ԡ]a 3:C!64ne#=h9mD2lh{*c|fj_61eQpss$:NNp;_ko.IӔnn08'T,'֊lhF2ح$Ig,HebBP(0&"hHҜ b[b"(GǞV|DPXbcI 6QT͖݆fOSN<ߵ>Ghɲtl 0G#a<_~ɦ.n-\ʓX.?$+-e]#./ & ͎8m[nV+l"Lqް^KHյuonYm$Y={7P,4@F|'heձ\;GH}6JWq^Fwzପ zz*.|pmsҜoKW!"4g]KkzI=8c=mIMB%(`#k;ꦠ ifqmG𖧏N3j/ csbZ1 ''Dqdj0&fKqN9a|f3?=x&FVy宪(NX#^}wuXEfc+v*J=}φ3 %)ᙧ^~o5[<׆iRDS=83]wJة1wMY,˒IN{1 %1ٔ$ɰ6(vM+ ,XoRHFr) i\$i7_Bl(v%rz}ۂtAUD\XE]64UK[wep_ jQ./GInur1k,s] :Ϳľm3 M&8 Ӫ~ aۊu+wO0q46#Hִs*$`&W!9IѴ9LW8L8hPs]Ni^| W-FFf#uw(fE)ݞ)^`_'~sJljWh$B횚4||̓'8:^O>fq;ʢ%Rv |KYwęf[acGS>0h/nX%wM;XmG#{*LaѼ㇞gSnp.l tZ,mۆY)cVw;QuXq3IL hnwyFݵyJ]yd2Fu+hcZZ/AS6 ]/bNkj(' D7(RL&!MmZlH."zf80>nIFz_}K ?6#R; Ҡ#HxPr/6+6جؐx 믓KNp>nI=>^]E4QAsqq1Ȫ>-]^?G?opuq\א FxfK)+@S6XuDIqt|DYR! <(aFIJEkF)6(:+b :0 VHؗҽx8Pw-Um;ʢ`lDK?e("aM:O]% 3\Q[G6(kkBdL&N&czd??063k=Cj (v i&i{S[=eQ5^>s:ƭ}+vE땪g෯ZZ{5J& z(Oɺ>LJ}!yD+v3>F8ŊCy>={Q   ^iGQ)E|pG<|}A509_XTܮIbKi"ck@$M8^Ֆu`"xKw%VxgST`[f!r(kU e,nb nTDM&%&ª-jEUU3ϹDi?'@{v=UHb}'^ٌ-FkN3BT7Tl6\'a9i:=z5uΓYHx=LA5ȷ !9xz_߯4`&HI7 L[a>+#`A059cFMx:ofw >t;ip^\ס$FSki(7%ꚣ>^x)횋 SI\%_}kMfBr>ils.qK J1q QW栙Q8Q`ȁTatcDkt8>9&`ig\md2Σh,/??H#?}PK:"nook0OdmOb\۱츾4/_|=fk3jNMD=z㰪ꦢm:9ǡpE}Yw.PǾuz-[.oOﹸd[J)=)E1 =(5Zg|u]BbGǽv0/`-zQJɕE)BJJf(lz+bhێ&QDHγ<9aZ{zOsss -m[dIN,[$fWȲzALq0,mt8papyynVB Q1ι~;(OS$jCa3%Ko7,S<$0J3Ι.LgG#y 33XXa{>\6Bb<.&z/67ZC:/K%(큚Cv\ ps#ߗ4 &8Z,]mxUQ3.$(͇|?!ݎ'?{veXb:Ջo,GB=}||4J_~݃Bk5x34. s?Nv; =3p{w|:u fG'tγe"hv~  %ކv\Zq蒷=qh `pssa '$jwGJY۲/ WuTgSz<ՆbDŽZl|>ٗ Z :6t]U ҲX1[.=\Ŋ"0] +vl 0Ri%|Ha20&z Gm$qZ^~L2(z$H;8(^6S hʯm ibvº6B5# Q1Ҷ-M4wwwHIWdP;t fȳZlp("L#Ȏ kh}͍Q9G@9m+*(Oxw{5C&9rn*ŎjEpœӌiSqͫ7 V"@`1uc74;B`dFp}go`5H)]!RB mQ:|M_U XC ]Ln_uSrj1\ ߉ QEBV)׷Av*'@Qdu(V+"Ac~ǿoeZ{8|>nهnخ7lݚ}Y1MۢO?4xpr5WWWdIsk?ެٲ$Mz}{>Sĉ!ʬ.u[@CH Ӎ+0;fLUU̬33yy;kU]UWf'Oyzp(# # =ˠmMGJyN׵(`y*Jʪ&Hb0"KDD * CEpEd`dFMú '1LQaLjwZ]Z2wi|>F :Rwxq\6v]W$)RvCO>csʲ`ݒ9yq%c.VH_wg?Pz8K!suk(Y̦˰Z4n]mjWTX'R)'LFO4θ|qI]מk_8Ew\_0$˗Ard2PU튢CFH8x։&KFb^;C',KґC2H)yqqIߑlʺa4f$*uI-9sN]!_%RJ% #G50eK9ט]yi Am&]'B;B+( (tm]A#^;6k,ʎ P5VH$E_=ƿO'DAB %\?'E;=~p_ќ]r8*l3N#W7׀?Z2/x%m <}] n sY_\-ֲIˆ1L(҄@VK`Ocf2850HSn6;(F7 Z{1T8aYemHX@aŗ(鼃38 37cwqEۻloӱ7mIL!dzxnjTYʙ/ uM1qP9~ Y1ϙNqL UB JcFiJYt![z^4E ta{]R9ٔ눼E}`bJ[7X "~DPW 阮i=8#Gp$݊Mu ;CRw5yy g&0X, Ea#K焾HJK"4Vk("JeA[W(ġjhr$N3T#> 8HiX눢 LTMV::>fX }[ˆAv"kwk9#1\wDiWXl1 <ӄIԚ0Tp!e@YL6HӘ0 "|g|3hO)@ԵⰥ)vضBPYpO>,K1R9vvҹ4cuhDUY[Ok-!'?LlhٟW_a<~uՅkX,٭7<~/^0?ܵ uQNv C/lמq;l4uK]NCBT(C.4ڒJ?pB8) 5nxӶr)Ԉpp?"7ŀڐ"@=mn;RcarXGNf!"PC'$BAٳ`ޠZ\<( dꂐmkڭ=i&5})%c ϠjE$ܻJ$ yɗO7>.VW|\g\ZA$lK0 S$k: vA6oҔuV,: L]KT0#ִ.*whm hw@+ EQpss*k0L0F۞@ŌGCCQ6uh OŝfCp4eY9]qqqJZooƛfoƯ=sd/ph±)F .c¡4"]aBѰ!¿9A =l4- EI{cz&u|5}-|]Q9. ux"GHfq%eYC+?4 GCruyu mאdEmНs9,5Vt6@-ƅQ꙾n/w0!{w¹_w7^>s mN@I(0hֻ=JA{)kO9m!\5I%ohhNnt 87Mzw [n m9(Jm4Ha\eSqA!S5Y91Tv%Wt^!y^ D/T<~wykOd{n! !LY2 hςY|kgpBuBsUn'R'pX6s+eP\^;g]Se؇+@!Bwv0[d])'д/?BPT% 3dƒybxzA |tpIB?> ݻ 9ݺ+lP': bsg[Wml AQXw滹#eR== jyϴA6J^ QE|39Ž8+յktGZ G)*ts:WJ9O)i { 5IIfX·uح]vnKTs<=k0tnu$ubȝ# .eKH$eK5ͮb8f~t!/R2O٬|W7d:z#rYecQ E;G`d:<_zg:䍢ծ~=nCgxmC>+CYhбGri:9XǠI"SB׀Ju7ݱ "/|Ÿ'vjN3@CILSV%{ls|r֟ǿOG.Lg#L9}l[c<;Cj)GQJtJp8 J:mԅ ,tC{N#a mgR( nJz6Z\!;yS6yοɟ?/>w%7gfZ)g€ rEvCQ̢$fXpy}Eh4@;G R~[xw677kOâIi4i*qf@ Ћ>Zˡɦ#=s>'_Zڶӯ|w)9}5Q0UՖoqd(s~_{btUUih4΢(Ayxo*|h( .//p!EQpuu5p C!eH! 3tmwcDWMK[WX!i͚ Kg]ʽ>5u2mcF +-00@eep$KPMǘ΂Hl :d͖xDYeN2B{6Pu|ru8瞭vfoƛfj,PaLBfHM/ZzIxd~wۭc_.C@/_h/߲ŰpbsY .4C[Zc("6~Okѻ#D[k GGoѵb΋/{0A[AW]Kq:F#h@Cnh:4B#3JB0ࡻnϤL~|Q  n_5-ej@pz:?/sR5 ٔzS0?:&J Ieszٌ ž:ʶvQ 5d}unnYNvPݡ JnnVH)\Fct881ЅFuCFEQquux~.3C0^1ύ0N4zj(/|B pQ8qzA-m=#gV3Ůz 0=÷c˲59Jn[o ֿMsEayN(與,Wyڬ Ht( <^om1m-CuLߛѷl0n߯k?xp../Hׂ56uR E7)M&sdކ@,yb GCp}}a=i ;%1Qt:%I}7 j޹o}Y*ne۱X\mMbD -mB?cLn{ɈxnyQkFI;}6`Y.|>f6 wm #\y2$++J]|x<>>nKQ+(gv;h7(AnoNsu<$WX :~Ԯhێm Ӿ=6-]~ bT1>y^2M-mIF<|ԹWH{qpB ! $5"p*zfp{ˆ8Mh4ve!]@ 2R(-@w{O|gL]/thnE1t2 g6r)%z(^__8d%u]a#$gG$Iⴷ|ikW;uMP5#3%\^Uwm榠;N"hH]f$ %8Z7*4 H$J@ =__VcMB025h#p?+;v֐]s`MɳϹ|8y9??x50"Dk͆/~AcC4PNeY}*vf6S`Md2;|wIIƉ|r8 k5BZ8d:7?~}e~rÝ-X#☯O?cf htũ%liZMAqn-Ψ @zGplœ|ܣ궛:Anϙe,oM'^,E)uӐe5li;ӌfuniL{ic<:>+^ 8\0cOs:|z)8ĩީ~qqM2P@_C~CTkMedY1v;|B;p-q;wde}wѱÄ.tBxQ81ON(ުw;H Mz0LWǒENo.ۈu3ƝYq]u?;kL\xhײkZ!CX:mHDEh# 2(b4r~!T8W֣m1CZ Jb`pm)b85;֋сowwIh }4M;n]븾s-1F$qDqD⯙\vuHŲjjuC(ۺ.!+༎( w |X^ԔAlt9˛F[d11;'Pн3|<h"谝 JNШd½{|kq0`xߓ)qhC&=gd6(Z,"oQ- $G6 m[ulI54UMSXmDnj2 `U<ꦣ[V#d@es6-a!8M [glJ'?w狯?@戮 ,X7Bɘdf:įm[>UeH)QaVlvXb@ ( BaMiڌ;@XH{ɐ TUit۰=̧c1d\6+f9''̧c& ~GQ^y₋kV$Q5ERu(n;F *0%$unx>G|𝷹w2AI hVyY\myq 7;^S8k5AdD&P[0`E.B@%LN$*vþauUc=~k1:Mӡz ЇJ)Ե9to=|a{Kz=x0;x]+^R M(HEL `[ʪc=1Vv4Z:c\ءH1+ծ:*1JTUE9A>&pUeKBϞq3`F;Xݡۊ~ "d6h#)Mu ] \y;JG|>r8v_bz3ތ7x3ތoC@9NXaskWz"[%&0}1 |n dUU (pP됖!Ȧs0N09e:MyoXX#8Ke8b#ˆ}^DU%0BwEv^4M~S%d V|ׁwϜ}z{ﺁ-\&"_я!BR%uQblGDI&I IDAT{\-x1.'<8G6+ Pε&l[ϙ-l6#KE~p-Zd)!'MF$IB۽;c]{;,C8Zkd \ݡE^)] WO!@ NOOp8M6CRj,zÇ]f64 I30;&á8>avcnb,9QZ9. ,.tpJcgCVbq`i - ʁpWuǃCf0=8fMPz\^]Ŝ -P)eY x6vH E͆>>Ra#%xttt:ꚶ"%C^:Q{4Jhg AuQw0Zsׯw؇$}INfr}+u]v; Mc7}7t:fϝ\;7x>'C;k*sA=o$IHt*Yp!%ڣnscz45 r0m tYvHشvnrM;\U](0J"Bp=SHd~qh;0_zrCM?z B󯸸ѣG|kxǤYa >ap8T4saHs1 +:ʢŔrb-z35ÜNw}wr#R&uQ6Fnt]G>Kq;dv8gn}Gyb!}6̬H |ޮH ѐݱFܸ < 8?9&I30f:na~]d 5v0 999ϟn}Ar#JRHu8#vu ҁq(;Q+|m!ʯypR眏^G3X~{<{o{QcW?~ 9\7d2XVudѺ>cnV8\?Bj|fPX2bZm>?O认*az)i"64Æ&1Z[QM`L6VBWF`CR0LsFmoخ$r2?`.WNz~ *0,UnAȈN],!a\.9%///j=Belu AݔT !ݐkHXu-I_]~;oBw@ 8%Ϟ=z,5W7>~O_ 1F ?}77[>٧Nٮ)|iir㔏>/[os,i$( $뺁ܶ/a䵍$>ofoVCKx: [gκ~29VH np.MS VoVoktƆq2F x< D2JCFYgÇV+^<JQxǜ/Xh,KKr~k׶9iƤiJ @ E(&Ա,+1ё'1*\aw7Puհ2&[a -|`ょÂc u]#=֥:X1YAi'Xш0 ]6˘d+-шdфӓ3>!BywyO<i1mGu_4]Ƃ~mgCXG6B|~ԣh"n1Y!;JH3/_zr@];,Mc/7{DQ͝#D{}^l6b@<I5g(ж~/ B_haiDȀOW|[o%S5iQ%PHDhMSNys;g)㌶ј]d(w㝇_{btvև7髵Rul_i}Xa_)_DQ4T)\h(IǴuqBmV$)HlDOYެA떲MPoPA2Fҧj ͆P*2$ɈC ]laCPe{3ތ7x3ތ_56h554H1&C(B4#S,R nM}EVrݟA[vbymXBݨ?A8XVm{hMQ !HۭN?y/_e۳UʒUYMK~SiPAvx% $ [QcL+|wrkkO_a8"zhqzG7v((,ΘLfqHLn?vy}nc bd2R7;6 펳Ouع[j D!wԹ>R:F]q"ufrHkX,Ts'K;߿ֆ#8??磏>{|Y.yJeʊoƽ.aQʢ"/:Ytg{6QxFﶴNs6v^hf՚^vE3HkqK+׆[yҿlz`ʽӟIz`UkF#. (azO+'`pۦ[mow*!c՝ y1(IKry5ggg$I68G#W EY84vu>uGy [׳֔xvmQXM_㫢YtO[ yghl>P.ah;UUqqqp J50+fy5tc[kK\^^X,PaHrs&7$>㮹]SdsEI(D#ʒ=c۰Wg6YX _$C#Q")I-v\n5nčUlj_~@!*ǒ8q8omz!OY rΫ$qsvJ7t. -9骟zn;ְ MJթ 3N9r}jǜQix4iL?t,{{29& эe6%ᐺ)FGYEwSz8rd%~W^ƭM\TElaزfƾo3 Ӟ̙$ F7DA!څ8 [)Ʃυ#Kk7{A DY%eVNi逨iJxʭo4)QoPزY,kBMZ8d0R#Yxki!'cw1߽r%NQ {鐯~گ()̞?o6?GgK*$ҋʣi4~iVK[Wy_syG}eS嚪j^P,_S|SыŢAIQ(gfYևtQ]J*-wZiRW;[8r\\\Z\)CvRTv`DGʏ4>]˭ϓHXR֖hLX2׿NNX.슋~c,i6#P$uW(@Α [(15p4M18s𣘤}BI߱(H`1#M,VK]垸U,WNNY6&{,+pr⳵DɄǧg-1EU2_- CH )ш_/_9Fㄺ*Phymw׿fq1ϰ2|ј $hMSn\jp.OU9X#0,;|v'\0h4`8B5|_1 mg& -A1MvJڎoDUW fe$mx|(/IP4:sr:c<Q~3+:yK'Oy=G)!hbZ( l$5 ck RYctգF-,t <0KW_g7_ hy^LjςXcv(˲ a0`24ƭ_;0=\"m;%}t6 |#P(k%|,p5d5)ghpikUO??O'/ Z~<}eɍ[7IҔzMek=^V̗>-zM^q׮1 ) ǒͲ*{oŪ]>{uT˒xdJ+QT%UUkĵ7uFHAL$c5X +\IJzZ/0. )s CRWǭ{908ׇ=/2ݻh4_tRݦ}7o50 IDAT֚/h4BˀY7(Ȋ)F|w LF#0+ց5aN)%oٌ޿.5O0w]u޳ 6@P)y*۸k4b8 Uv B{ #L,w֏3k4EwvssكON3Ny&lfRN!ij ӏM)F7M&o]V^ lH%WYv\ͩA 0߲Ƙ'fކfܰXzdc R^y% j5QJl dClkMwN =sNN)[x̧^ hڑDee5h3XT MUc |vjWd7YNI2]VE=5˲D͗np֢!37o$\yZW+ :DZv%M"Awᗬwzf|7]tXy}ELS^H%8e<,݄xt:e8NBRYO,˲- x3mHln+rb*jitk㳿߃FWxJ0Ϳ|/G6hDq>&;UV b`pttRdGPZ|_!lxIRA]J$A)]u8&ʶqmtUSIAdٺ_"Q #}lM嬖[(X߿{S999fWH)zhm?:<__Ww}ݻ&gOcꆋ~s<|t xrFaş y_o{(oauk͆o~o_Eޡ Eĩ ?O Fw J', Vp0l.q^SUSapy"k>t@!|ySG+0hQ/-rBu?(iNyΟR*gwJխTxkb>EB2/9==EF4yԬK4P*,~]G>{S\;&MSNRfdG޻>&o8L9H' &x^@5yAUA65lZ7,+⋷wIo1pj<,οk$x,r,#U<X7;@i `Ya& C&Aҏ=Ub-{{ʒ8N(lwAzIY44*Zo|o};4V#{fZ#ۆA+Ȟ*E(@I6(8rXcc+!d1B"<׸q$RT?Mh4iUWu@8c:F>c?yȽ{8?pi' >}^OUU}0{nu+V>5hpv:ݻ|;⵾JEa{Z2<9kG|3oZ,YV,uհYg}XUk5B h8lzMm>ku uߗAߎQ>XZD2 CL ʆF!`5JR^y67n\;? ]!e^QyӖ#ah?(yWèoZcUaNR5mki&QT=9vxs]pq~yL B0Yg (.?-^K0׊PA#0Nx NN8] 8vȭIҘN9w0~~g?z_!Àl{ǩP E}l €}'[}xzʛo?OAzܸydBQzz]|fQ{ٌ7nqc~()t{DMggؽxݛmVYA',HZ8==ŝ944 uU|bx)r1)‚H=+c bSLzZ%. ?lv_#a-B7.>T qSJE:U8>>rP7|iQ䀭rIQı [;fldg2w{,:Vy^yŗO#u4"2b2pM'o )M=ͫtCϘvc4uk7&A]~о>A|u]OS!qk0+W*|<Befe0rrr;x"*[)5F)ofZQU%q#=_R0٬sle0#\]?narTF~~jNQTylznyYͺWktci]c eQQ7 'OZ7rxzPqroUՎ}skS1#Q9@)8<< ÐS9.8In7L'6]ݻ?o2ʚQ)+ʲ;ʪ"l=ݎ$I/}{m^xUTehp޻jR܅]}xhh,12 |'l ۬xJWUy!_K,Wk׿| -qB+Mu,[ƽ=lE7ƯUe~ǭr4$iBٌ݂$^00L&ā;vU̝/>>&B꺤*Y].jA6FMdB]l(jlq21׮cݿT/ /q;l|jAY!.@=rnm]5)8$MCdoui౷7q*Qk7ʺ}6@1ч!(.+Q95Ӎ7x7Y/X.ιwR A ']7IStXܻceP l 9>: ] ѵk^^0B7ITSFEc'8a-~T~dY,( r ~4+wg4[CG( |zRD!kG'PǮ<;9kGG3Og\~Sh_%{ӱSHpUnT纵o:54m!׮]^ni =yα*ggy e0dWl)]8~0  Dk0qRV Ŷ HO5VFhH} 58E+/& #V%uCYoкJKkuϲZc&wzmnVN9ePrx )-jɓF\O FOSRggktUܸquEN`0`4q~~΃ܢv1ij&u<)1m0Baʺvxwc?PJQk1>OXϢ|kz )45Ø|;ܺ"ł'l y饗69јl7SeYQ%G{{c8DylOi4n$eeU; 6['=;S5g>t,Ur΋ vAvMDz^Ϫ w(#BH |ֆݶpiIJ_ZH!i++ B<뀒KgpɒvmpTsGڱZQ>VKv_vr'ꁀ8v|΍7XhI[{עc^eF.h*ݰ]CP^@Uk\xJppxbD ^!\Mx)z=?ka}?|Z7sFsp|DDiWlKd`9xՒX}>ISFS 'gٟk.%*pnRQ{t'_hT?8cq~؞1h܍Scrڵy¿'Bn]HgpYE)Ղ4M"*tʍ7`\29cwR}7nA{vp>D\r~~w^֭[qLQx:bZqTKH=VW=h;֗@MCf\m(t#DۧaZˠqF(ykmZ׸|\6Da Q%R80ZY6'M]sFXNԻmJX-s$W^[1^%36 UkI!qVVC<9mDtrgqORyv,xwvAj|ײwwl[m(!"t׻S'; r>R>7n?ytI8`Ph U{> T9ƌ AMcD߾u+G7n{69׎|$B(ovd8tQoF\v1VPc5/QV>ASyY%z䧩{拏=f:Yf1ծp]T$ FWZYq^}NtT]@h7w>eY" IDAT8 Юn3@"EU$QE `8Ŏ9t]!gbz(i)v  x &)5k_;`o:lvNU<|{ɜui4MMz-p{hO$1_y1}-4[nq~~Ϋ֫Q͛.кfcl.~v./zP=s||YCيW_1?8 Zz-ֻli BhHFFa֎$5VE}J^aB//Bnݦ:J߄uy iƺl壔XV98d8p hvk4ID4Fs1_"rBhɈ6R"Tz=w]IU6-P%劇O8>bee "MD4Z4ZyɤS9vَ8v!Ɠk_᭷BH'S $ w~7ϼ)A]d=|%'9<'MB]k~DͿ'??Xp躩U|>vp!.@䤷FR1pSh'O{q(j}6CJ$(v6j\hn^yA!*EnLׯvU7Ly=8vRoSt54j%ROBYM<$q ɋ+2DH9~5lGc$]oCt$=?N Mel6c'y=wU$;vJ~x5xڃZNҶ r8ٰPĊKyޯ,Kǎb'iAKr"Iss׮IQ׺ew}8c5Mb8zWe|C0" <$oS;*)XqD"Y`'ѽܺ: t~SwL[)::[S7%rxŌdUp2ƪ1\.-}:}96f|cE`P6P5hEH٦P7WzDohɳ:5J;fS[,il$ITg, MӖ_PV5q#G>yxbDgXoUu՞A>{ { t2iѦm4`uu{r+e%]˃3/q೰)ŶvjJ̲ށ}> xGXk]K:=<~D-Bwϳ,I}|NS߿ϼdu%AuT=qk@Φ؆rBy\feYc tØpD]VԻ%u݆kJw ?,KNO }}@UNx}3vvqGtJr7l ݜBO_4j ֢xHc,Hȱb^J2Hbc:`XHqsx睷Ӕ4M>PO%j֦p/C{[/Z"Mٌ 8=}BL^r5 ͺ, &M8>>vQkkga|_~'OflMq_|rJ!]!ƠoI? evARp1;ɓ9 BG cQΩuݻ9ƍeZ\ͩ+f! 5 Cs0$" Ӽg޲fT(y|r |<|ٌmV쳔TpN96h4"Lb( Q֚ihfʆ:S!|p_hsSAQ-*ƈ膺]׌C\BɊ۷nG'g LG=$\?>O/9:8rt0;om4nH!o| gI^}UF>S,p4Ƀ~zM\<~j0wΗg8ċ vɄ7nL\jaoo(sd3ch+(*8bW5HF1e΋+–$yy>Ҵ2zMb`45{ܹs=\JJ]-Q#e Xoz~rd̵GMS@{x{D#w)˂hx_"`2{cgP[,הŀx|-{VIg)cW%2j01 =8ۜg}{eFws%3?N }Fg`*0NǽfS4s< paQcJ؁SkNyNY\ySYm[na:`:X,Ȳ#^|Vq>n08Gl\fpE!\@^{)YuoYk\7 ]6@'۾jѱ=s5hυ:j!J4t %$ޛX睿8r%ʬZIjR -Y,ق<f|`f``0Mز40dQ&[fUwג{{g?!9U"e6o.f<'n}㺺ا0O-].V PMd8nYl&=aGOvt(vE[>_y>ERulٌKIVѬsH0)UAh~Bme@XR$ɆLa:r(YZ-ؖE;laR .8a)ʌ(NN{tvS͙{EuG}j,3R&Ҕ$RQ%-hц!ިBk[=#^;gâP,"LUGbS]M'0Yp]mry%ingle9XbaEt:=?~5Y%PK}=Lv) }n;lca`/*iđ!]y!Dqjmw[-,Sn/J]4Za(KEi(Ĵ~$R\&97( Z~dXY/y>'ICV >dq3e٠\\bJ+沋Z<.~fiVP{+fAS*mlҲ,czuE9~#ǯso}{7΀_u ۴UjmOㄗ':t{z6Eigci;84`z%P6K~#NSϱ,j{c]h1iS=A1775r}aj~\{|AeYhK1 U@;N8j~Ǐ}SBu°ͳ/9=yb( J0IJ,6wvxmU0+o":i"BtBgҭ`T)5&@TGB),ǁЮKӬB=@%ѭv6IS}h65±$yՎm2G"(ƃpö Ͱ-ӶIzWu9Gm5փJ]խ¶h!wv96BIJ71۟~E iN1eu@is1fDuFOnΰO)j2_pzrjaդ˂2MڎWqQ *yBɯJi.]m-)TR(rŭм!f9HU (DsW8:hLgyh [)+׎Dv\KYF)%Ih܆$Y:*Ψvt:Pw7f4)"|c>4x}!*Nn0L$o zMYj9Vzʲ JR6 ZX\׍݃j9v`@uD8e+?U֬vGjHDQR*NQyS8Z^vLdI6rxxGBhuyZY@8Gq[k,¨XZEqlz;];J5l4cܽ{8,t~v?SF#U]4znPA00MYj1Ja#mZ||V+pNuQMs^2y'jْ|ց lSU7b{ԬIQWƔVgض:x%k3mBl]%e#$ C.//uia%]Ϸz{G+)E5A幄aHНMc7,5GZpvL4NBVåfaYn7PMq۱ }YĔ2%Is,竟wv@Sz.Y1Lh!劽**0 N^8b7`_՚]Ep%S\aaz 4bdYtrŝ;w8;=\4ps3Bmx[6TZ2M(6J)jMY.?V8$!4sy!N4 % }Ϟ=cZnwq](}$2Bw`<t%s`sO97 F,9!YF7Mq%8&t$RpsLO -Z=6ӳ)p* |'rZ3 %Q7K;>Us {cC۔mnF<9m e|u*Gh p6?RJ (m"q,VV)JEQf\\b2?Ǵ$͒A_ G8HvT3菸'8r + ϠӂqWKsuqJ0 7?ۿ-j,ӋjtbQe+wjuvͫkwBќ?OkYѾoo,*,KAP:Jʲ,&x'Zt:~eYx> I/'d$SD_w5_ zVնy[ET3n8NŵFa.$IQb07BơXa^R9 v i(Q4{joyLԨ:utZ=`n;:ʨo?NLq_S\$,"R07HC맟 TZ*w4,ڡ<ϖ& $VX9(ux eAc44\IT(aP:^5jB tp!QdgƟTnkYBYJ!喗) aRHE8g`(BbnD1QaZVj1{t40}{EYF5@Zf5J)JS2s9Xo N*1\ N}n;T6^uHyXu"akݕaY:46qژzrhzYquuU1# guhZ067 I4f~Z괱VwNG݊*xZ&a9vQdф^kfiIkRI[s=eӮwҡWg_r _hNi8A l:QEl];MOZۘv[ٕ>aLk]4zT;5hB65.߰-3ϙL& q@CG:ҐN_|^32@%T9B;ju,o5;C~\6ZLěSf)ǜ0:e80Bp~q;KX,gw=7~g/D%=̦svvv-4-vL86Y"K2өzo^mI+o=F9b&ytϾ|װ݀)f`2ٮ&DL >~,Z9„>_|CJ ^-Q" |CIAiClnVkzJ`Zr (OwCwKiD&ə<%^J ߧ9 `ggm[Dъ|R9y.q ۶,K6x((TF\ls`dY,'M+#Mk vH{-^m9~u³?#Qjܻ3OhwwMvq"XLWqִ,e@A޸:|3͌`տC˗3z~ź\QْzܻG2l)O^> s-F J-} ܿO?gl66VIb۠խ锣#uZiVTkV謓+K֛˫k֛-V0]\EÂL-V GcCXɫ36S!ռVMr)իGGGo :jEa:b6v(ttlqk899a1_a54Il8n7ؓۛ`]Q鮞:x7ލwx7~q=z&aM8A{D-P:Ɠ |]_Y)f%\T\d :Iha(04ղ0͢q"ikY~`4abviyպ #ǴMY!*'B`ff!NSLƲJTkTͨ <ϣ0tx +m!m!'}}# vvv\w9d<%-K݀UōbAsrbyOH,Qf_+- D3Y]- J CUH !Lj*00M0upB)Uz%4BJIV乢(v E+vq,,qU-BK *ϟxR d%+} f7U)J4;%Bo΅ZX9"Yzn:=0)ͦ9.d9ҫAm€N|NaY뿵,./<a-Hvhhq-N~)m7GeRiG2iMD =V|vͷ~fD?00U(Y{;xOܬYDxk;d5Ͼ lj+u`ڄ8 %ٔ4D[eJN2_-Y,np]Ã}]4I3'%(EQ"V-Q" ?la9687I%%СbQ iB.a` B O6qLۦ q=z`XW3yq͖dx8 | b??aQ~7Yoxq>9.ysu$bJ˓W/eT2ѣئɠa6;U'ITESC96JY8CSoYaϟ?c>O?ŰB?ao:\rLG]aG)X.)᷾ɰ ' /; ,f1{ϟ?i<~DpqqɌ;{ $Ղh;#$az3mS?'"9d<R% FiT8~q]qtkܹG _u/[tzc>ǟ1jt{l6 d>lt!X(%[lVfNӔuv閛J#fWr>Ylsۄ8X,7,YRxA"bLl\iS 9A۶Hݽ1Gv{C{Xa( #9="K8l7{<9nsiseŗ/*c Yh8??_-~;H?$JɒǨ%7'+AVSA(w0`l|c2S 2b_[nMroj8enuSn6}~_#(v7۶0 ?oVÇY,ijwzNo1;J =lڞ?x<{ 0-%ق]:EQ!qLixp$ )u-TIjt4vl6k|>00l6ܻw,˘fmvvvp² y"+ZLMfmywx7ލwO8vi0 9??'^,vB5'0Gf)7E‘# qSZ:_fuWm~} "7!^3Jo<4i۬V9qWmY㶮[TRa` ƲlLF ARidk0a\bUoԮQЬѰhH`)a"noO4l}5޾_uLgKB1[IҔ(+Y" ΐ(K׋Yoc%ta,u8y}lǤ(2hKQZD201 i e%E'K@ -i,KM.),ekm LD S(\pjL%oHMm긹Y eAa4z>o㘬G!rMWVt%~.eQDJ!406@`Yk:Chǩx)dY90k_fWsQ`@ CJIEt:FQ"]x^,k 3a2Ŷm.Nxni8H)]Bu,s\c>Se-YwCI I#E =hځzmZk14MH $SJ3.VoXUFsٖEӡnm1slD3. IDAT ^iRl#(B,^;5(=g'^,nr'jyb rѣGmz #޽{rӧ|g8K5ݶ*1Lx>O;tC lk*S,S?'ygUScvwwG0Ca2L,O.j=goSpG?s0}Ĵ%-+7,'x&8~_#gt[- Qw߬l`U2 z],$4q (*Ua[tܐrver5e4A5ma-Ҽ$t," *h={vh!b$! dxgيEIg1O&醯6qTp|6Qvz@)n?F)ד3!B(Z\r<9&r c>*'-v{=6ypGTL?\kEQ4hv UZQ\]] GreYMjt9p]`,SE^Xo -uڎj')E!KJRՁfU7MzŽ{8=9na4,뽽ꊝ\%Ilf6np; _hXxYMvSP%EQ^ofdjɊ(C-o-wiwGsɄ /Y/W}7ލwx7ލ7|wy I\tprr%(i2!MS^za 1ݢo~t@Zɲ̊k>++V-V:~ͺ=s݊ jBŭ4#ϋ]W`Y6IVqԭeYR(l~z!/rxGD Т3>J%,a_&{[دn$o:.yuzF%SQ̖ :1,61Hh\QF 錋S, ^yԑ}4jڑ/u4v-( I& rB*a!1մURUTvrZi1h_bqiYB¯ >F40$M$S7-NOO[J,-==9R !chJ)@:Z45c4jniaזBuΏsGQh"C0zojjK-Ԏh2hZUfAcuHzfqcnY&ݠR! 0-lF'X.6VT]nեYo27ǿfcc.|1/Ķm1&I677i۽IBjZF,x Sex="E\9*&#e{wKw„hW"=f8Kݦ^3nrxxvww<$:Pc'xN[o1TkJǴ[.s毿5o u7@oNt 8Ų2H}P>rX,Sﱹ߳N1 7R WN$Tm[g2yztlooTBvj?gevJgr:e<>kp||zbѬn[# jB^Hݡ4؇?o T LJϱZ*EɄFeZ5jD0_\sؾw!0dޡӳ8=zMn!sy~ƒ;,Ws,ˠөହ<>9Z#`6sy~qAès26yݷ_qocxo-FaMJa|>'I666e䄍赵50d20}xaufY@t]DQp8dXU30p*:LhԨV뜞1-8:>NzfÇloo3LmͰ*[ors@qHwUQT h6mLZF:_܊rt:-ӷ4-7vv] }nnnÇeEQ%^0 mzh!V>ë,*jhaW7Ğ ?Kw~ػ׻_ޔ[qu:gy2z+fB(: gaQ / q4Ơ^sssSbFQ*pCUH2Q,Lʴch{Pg(VvU0CRiJcB?2$(TR _2Ld,sro_=__&@T)czkk,V3Ͽ@I-gJ?UP:&N8&c$Bp5լc)k4rAMJ'R"ec2af$UqEDaBf b 2eeؖvQ4@I?HLAH Qidń;;;:Aim TTf<{dVu }nrs@_rX`LrhHS.D8Ig vwMFQ۹Y{0R,! 0 EB$qoF=^RJR+-1ShcD)-"9ò򞤯O}/VT5(RSA)q;,WL4Haxm"k&e2{c&7 zTPԪv⬢21$syq^;v񂐛?p HfޡZsyyd|ZD14Lm*;iSkIH*?Ԣub"wHK٘fB7aջ+ ш!N @XX.DQDfscÓg_\Q;'ST Uj3YbJg9J|EV "T67ңdFh4b<ss3);;Ni}+aZ0vgNXB~?_rȲ _f>k?~)$ ۜjHZql嚋vaW\???ϻbHߋ:0 Y)5:ZZ;ַiڥDN^w4x۶ }Y.Rϣirvv]䍦_xqxDe>WW_y$4,cNFdd:7#-lg? o5&7co|~zd2VmbX{=~tlLB%+L` Ŝ/~3s5CT*UZ V%b0A:U6Pr|b oHˆ4GG',s,b}}^ﯸVk4?Jr~~Jlo0X_d:[K&q`e_p34Vi7DӖ<PFסjY򛯮hیF#|gkKW<ϣR] ʅy\y^n(ځ^yQ^=ɓ'z=9oZȓh<^x=^K$I)|B☦nN2)aIT UmJ$J,i I22E,P$"ADpW4$i8T60ETIabJ0HvZ5-i$B`-D` lڍ}=Z0~eiDU⸈\ϧLS|?nhҐ(/B3L2Yv]+k!RL*C@dxLIOrW~U9fY{LC"-7vF !Jq 1ZD_z=+sft}Cd}c p*IOfTU}0JX[ CZq魑f!G?&LJx eV9<1pU8ecIomgQWC ַ6hA8ƑtpS! B,+uL77?h_g!D u=m@XGՑ 2&ʹ[E$a"c9ҹ+iJ3*i74a>g1vxgL&jtjHx:g{s/V*|_a0\po>M']>x#83 ~ 3 C `0pvvF4I"VQ+#S\'0m~a8Ug}soARc4qv~˕v.K2]AV^Z.( U]sv>袞 fήfX4Mi4t]?x-.^Hf1]`H~?޿cˡ^m&`[TsvzAŭ3-5Z^HyAHݦrrrFA}_2X.d"tE^Vh4^^+ZVZvRH=w4 VAH?`Nu봙NxGZNO裏כ ڕ^0 yajDSV@t:kgJ3: Tp8ԎA/B6?z]Ֆeѻ|c./mG+ av! **qqq|14eI *b>/z1 5.Ύ i:w1|7ZDQéq9tef:hpog˿W;\.;w""WxUX*Ua%=fIJ@۴>Q! z]uNK%| Jh|bd>_hF8Tj5p+k>?qyT5HM Ჳy*b/bX.a:Y\b귉U͍F vk0\q-o SxI(%>ftqD!Lh5x9[[ⓟ暍yS:^ }*5獃7x ZkLIZpIJzuu\ FTTX__' C%یc.//Y,z=l.yˎN O?c:u)q%ɔƅq'|NmqH臧ݶ~7(Ӱ1œOiZ\^^b1T*uDQ۝Ff\]_!aN:! Rz.R 8b2\.!5|?qy;==dZ?y7xrZ~=^x=~ea`J2Yb '$QȽvU)Ӣ`mB,[Zқ` C̤+ 4yMܘxp.6~gva-ߐa]Tv+js $z*gs’V.$(s)قnŨUg4a6 .o.V*)P[E1(K#30~|wbUh/uկ㫌4aZagx,=aA@; ab;lˢװI%$w&[d9:B(K +II=iHL)IB(AKKb-Js\D1LCj&4 6$BR$aiYv4>,{Ue 8/gQm+E;e*7|"w+%1ڠZ2/TMt,-f3-Nj jU#L "kwp]7/VCk_v|:4BbBgю`K܅^B>蛦)"\kC޺_xuUF/᥊gU_*g0 kM du>? 5abW\ZZש0O!MrA:(KЅB4M v Y3 d[XMFd v0j4d\b4JiDR!40DZ'\Gj5&x%J%8cuӰlC:[d2Lg*EIr\ Pd*}>! EIL=l6#>[5)Ne1-Q<+8NòH8J Xf r88 ИFɉ. 5RJhH$In7ӟ| ף[$^CfTO:OBᐭ>WS&Y#Zu>)[aZ^jҿfs~,gd2 {~Z |IŴ[Uk1I"9La0;a0_νmא =x9K?/%IdțT6~ o9a<!O~#"z&7kQɊpcRj,aYL+2u1d&S=vLL8/9##ͣit,fSnnnXVG= Q咣#*|1nm8 /x܌_K܃XyȺt9>{99~k1,"ٷnSl}_/6E6MSnnnNpzzd2)+nqfyaɷCjb`6aYѨt:-4jAR38&\\\3qYSJ^4)qq㺮meoofOx~l+z :n0 q;>>ցzk(bV]0ϱlR?ض|>GGGdB[/ / )_(+;Q{ŦpwN=W 0T P~.B,UU@+^ݎv:onP4r<s~~c}/Ej 5AlP!M$%[Ex^%ş:|eNw_<ݝe`魈Zx:l7$*iT\[c/0PnIO,MQ魳7-^ 0C7LU&E8H&},A_VJh \ )P6)L\ )Cd)(M)̌Vl̜1*IӴ1ܙR$e,䟩<(0BKn_4o9 ?B%}Y鷈i-KТ49Js%c}vCxrJa&QSVN cQ`00T˲X,4Z8^nw0L|I$,Zs3m{#͠N.˯<D^8 /s'ǣ{m@g{Mw8 AW!Im*.NLj~oaX-j-H%F%}VAf[>|Hc*1W|_0^X+pŽxpô,m+*# xFm. DR)K}~ַ-)WWMQr,NO 2C6Uv:d V^EYqqy)tR cs}x#-Ѯcs7v|^X t:g޽s#HwoZ/Qd}}ZvK$BK47'C?|.0+]獇z~Ə?q _[EO42h,E~6a\.ːBhNuuARVeYyBJq]Fy@F0MEN/لmj> bAuX juw9n7͹l P[( 00^,YWlP  kz \r,oqJWO<~)3 X}\ Uѹ`!s^lɔ+*.ws"/xs/`~.sG)a8mh6a`Q>QkxQ`(thiZF\9؎Q"YDHQ@ 40C(ҭH-: KH+"Q4F #b(HHP*B4Ɍ$U$X0,l_$YR;ɱiIA3+ H;gF|iDzBH'\!ǖH-,+!=" [ݞ_B*L0Pf/ D6^˲@,c>dR)i8??g}}Csr[-666} |Ow<:@y1lr~q%kؖvdFEVKjZNf?s~XKR"w Z`J3У^c.}CӢѨjA MSm\m[F96_ |iQҰTl%!f |4Bɔ`T4M媜n`8Ȓ[1:24!N:5RN@~"LW)@ 3|:%jVd2ASޮ/wg+,d0mggԛ5:Fd2|'i[ӣҪ7SS9I h 5(%nZf9JdDIH "SȃAekU4hIQm% g}|@]X,HK b%yq]w9qm;kyvM^0.<|Z֣78??5-666 IJl}~ӟL'2NhuZ̗s#d?࣏>"LtlQ[s%P#lq=awwEZ4 Z붩7.O:`6"dt:'UIQo4ːk b:#(e\zQis|9d\37,"hѧ{%e{{3 N! ecfLd\Oh4Ql3mjnce\ assg&)FglRi™M^8>aZ)uk }]7x''={ZJ$I.}1?ODVlcMFv!Njc'oL8>{J],Q_fԍ&1`MM1KK~,M{L / 1?j7 L'Kf;UO_Faj6qEDztqӷAD, WWbLM$K8{rO__8/Y&X17syO~͹8</70NKUQ}SJ,gϞb *f3^ b%N(bz+h71шV".{{{\\\2qY___/.iNhrvvQVFmZ-Oo^c JTH{*x=^x=~PJ% QU{Y^V[i5Ak1\XuX.x+MU *nQ~ş(;͵e/ /T93Ng:[ N41M-i"@j|Z޶m֫ع1ɴ8ZX-5@I.P)0$3M 0*yJhwT3T\ϝL;sM0DHcHB!HH"hUe !0M`S*E$CZP9D gH ,ZdIhyXKk PR!0+JV٭P-^nSLU\`(~D )1F̵24Mȅg(hGJ 0>rvߠ)Dz= M. Pw}N]@V(z-]w~u*W+:n|IĐ(x/D`IR4l"#BHya U"XNlV[A[2d8b`$Um%>rI;M$j AbZZxGcWPYlB.V!$>7Ou"c+U,%#P'ma uͦ6%H^uaXiDxJ;סY#X_y?]U.j J@l%c1 X!ݭ]%oG׸Ǐuk^eQ)&}IQe UU瞙y9`B]EĜ׌k55`5-%ar?AAtszϏ?.%%aX̒lV; V ņZ Dc Z|PXXHIA͆|$nwcsl},8D"1JܴF<VORbD(z+p;TUh 5cp F B }|UJ8F0D4F0ew qe ;N4v291¡ uVjRѣ;EdeRN u:ha_}U ͊a''˅JO@ U5&pgyYdݺu$"Qu$6\^ fBQv.+Ay|8?%=z-XmNkeA[or SCc s' SP0ws}FqAihhYaW-,q9E.GmAjժp8L^^~:CSSh"*U*kUf56OFF/.+]F8mm-Ap'봵41[(--M|՗Df{FŨ*6ؔ[V3sEvY,1a\. .RWWGn())j=ӣGd|innN׹F kzt؞SSSc~IPͮ4776l ##\B6lvGϞ=YhB!*N/iJXy XzL7%,_AKs---dff3$)m)(ecH5]3utP0 u-ܼGa$:͗q gW՚^n%B,eCd(kWSZZJCCu吙fʕfCC4H7ME1B,@ N]]& '^J $gۼmĮ477 .'YlV+ 79yJ[K;~3@8Xђ%f:^ qCGÙ&éajX1 4C!nѱ,8cf2J  *C1@Uf5KoXTbuUPU܅ › Aj&˲h n=F"$tp,I-(̰+Ar0ЎX@ГJ*#:9 bIyjt//-K =fO30*=>7]IUb1np8҉8@f3 466i%%%(Buu 6MSI$KV{466)**T ø\f}frhBQQSYY.; hmm, hjjJv PVVFAQazH$BYY UUU8N\'Uߟ딇B!sf]u5YZPTdOdlC''f+e(:暦@GSG\墴Kcs36Hz#Ԙ`$Le±0e奸3s= ߏ ì֊neSN  Q k.rsDfيxbSJxU6;,hG<AK^KMӒ݊99jP$-ÕlPҍpp`̪WUX $A/m'EEEdzXVzMff&֭7ϹNaA1,֭$;;'pڵYt)=VaÇS]]McKzbʕx^Z1P "(RXion6Z[p;mx<=YՓKcٽ1H wK,';;6X,ĕIYY9ٓ2Sg%G:V%d{Yov._D=_>IyiEd8!i afwb5=evIc{3V#Xq6TVjr['"ikm#ظ;,3zEkinnNx -(V' ,g`+441#3#bB>b"rJxgSҽ?mAƍ=}{SW[@NN7no†UX,*̢r}\>ah͊?Ux]6VWQ(Q hhh>0Xvm/93,$ZOfhll$/ 6 -ԫV2Hŋ:!C?GJbV\i~8L'!+MOnn.H h#_J0ڂҥK),CX.mmm~.g:xzT&(YjV5yM51Sd3NMNYljFtf?lXN$SPPBs};6J0$?/%F>%df Z^M E[>D  ( م:|wZfw557P\\n#Ѐߊrq:,Xm 16TW[himdd8?K^d՛Wldee`Zٳ;{#͚{m?RÕzn}u⧥nӛJi,=hjBPrL:7NFe`ex=9QUC!orغ=fq׬2/ Gq/pvE9;{\`e?mxX[s.u_1FP=ɣ||nWnA='\y r=ǭܼǏCN) awwy}82~Lde}A7k7䩸Oy3ڍb!ʱ +ɹ n> Oo{xq?W>F3pO'rg#p>[>dBhgeݸl^`[>QB!B!~]nT6Ǧ~dLOPq# uD@T.7([u)_ƷpǷy/ 7\x/^_ 78`ҭpj/sޟ\~{Zռs2y<<>ajQZ|Q 'hckobk#w3\N' _xR^U;KO+`0{)w fS>gr G{`3ӗ3}y(>_h}4 ԅdŝ,@%+l.{/@6ێ;h}:ASbtlZi=_k;u%`傧i% z)K,Cз72!}-gNs/5wrwv:{F]Ob[)쫆38s  L^^7w}w練_~3|ε>dwxbCQ@_nߍr=VЂ, iZ1|c*(+6Fkp=×KN4cϽKv}+(.5 5N>Czx$oR]~PA_ȵ<) rGx 9aM3n̽gp̙wE;U!B!Y{EEŔwfinn={i#;]V׽ W M3ÃB$q.巙"/[w?IQaQyAI])ٖS3q {NӤXRʦauƷ Y<vn(-vև9b _߀$8+ƜKlp ui k'dՒS"bMf%|4v[@=K?KgczZbq{J>BX#)Ԟfox>ZF,s?s43]]mrځ?p ‘ZB~T[!N/Z91t v 7[)9{cPilh7Q0öTǾ0f&'Iwp]mA 9әĝI^~!kWrziye+ӌʿ|66]Nnkwߡla&=@c]+R2VZ|*YYz6Ÿ~2g2fWMdl=Co9CW=d;@<8p[4 AT *_6kE.&iUAU|:`%86gv/D^Ƥsxq? IDATap>=0GFͧ'_bBz<󯸚?ݞh~XO{Ε2B!B!t\ kptDo.dr9+3z bV'+V/9V]msv+nɪ\wWejԭ>ŎPŔxun ?8\=ؖ[L`?OqӲ[VC10-8l+;ţoѷoj/] \I#vB!B!vXEy?Zm܄˵)*!wǟsOɿ^!B!B:?%e[ŰZ;zsrgB!B!Btn<@WqÍ7guznc4K0Z!B!B!6ѴM+Oc)ʖ#e:RB!B!BpBޝ1xѯr +=(9$K(p0ߥ&q'ո8=,pT$8cvQ9n!B!B!ԙm~_6RŴXwQw7H%:.A| ?Axym==N uKp:[DB!B!:[ V 'sp#7C J`ƼMe3$ XDhUX *}on/oUWnK!B!B!v`3~j wsTmNַ(̻A[xo6fg[NEo6Հ^ʨB c A6oV{%B!B!d;u0:E9Ίgچm+9:#0_bpFo.4xyord;04PU@]͌U!wޖB!B!B 6,Sz!d)sot3üya( \f>Nhʏ> W,ΰB0a# AܖB!B!BY~~_w\k]=MEx|)s;R]*/h#ω9毁966xteT8laqiN7)\=B!B!B?:3:^cʱv9S0柛KXB3\`޺R1i4&Kb?PHReE Fom !B!B!NhQЎq:O'5v=iuFy5|{B' /\eCuΔnQCʫUa074m !B!B!Nv s_>mGlV{x Scv]dl+oZEϳ#G8zN'X geC{J xw#B!B!tJywdVeX &=fd9:LK(40WV' !B!B!C;u0ZSfa+B!B!B۩|):m˾f6}2Ӄ6꾍T' (Z׽h[+l"޴J7zlW_6{.Oߋk!B!B?0KxG=+{/b]'p=fyuGg70Q(nGraS7{و% (J4oM{+dBjϓzݣgpGuvS4_ejY漁 ,өZc}s~7Gxr^]wO>Kݡ24y>5“6z[;j__eg5r),J6 vz\:W5~̥ǽͫ`|V/2q3Vlz纃ryٶ=+B!BC@:߿/-'8)]ץ!ko7o*~9pY9υhO JGRSh M ٟݸG13}[<^˫'Wj~6yN8DT`HyYO\^?YX۫8vzAۏO6Kt-4_ix8e1\~^M_{KUܿ a=z1k8/^ϾKo4}g>QY? }(cMs阜i)yGAxle}o2캮7 ]L*DB1ּ]#@j;\}92)UWgٜ&B|?!I͒}ü'gٗ1ѡtMQfuq_!r5Jf\Ѳ8IQÜ:[i2ھ# yVh&x\-SW?" 8xYsnb^Mn&ax`C~ 'W;z~|^3=XŢ!%_%sc3>ZRr9+׮eJ֮Y˚5kYu7R1m'<>z, ;P ZW"T'mn{_9-B!B;y0 \yǷNdybC*Q2Zp {7d2 8!b8vGumcӗxW,h Pqc<7e'>ᤡ'^A'x_nfO{>.AS}$ZzڛlDW'i8rU0|,y] 9p,(7C\+G9<1Zp=sk+yW8k8ۭ]W0,EVYYYc^C{wmd7w׳}ߙ|5NhpsJ+~3Y;oxtCg>>9.26֯pz=ռdx0S,P] ǣWC|Tr\Μ+Sӣ˙>?&_fCY˥n7D/9gѷhӼNޱr«y\<^T΂0!x+o}y8ay[\K}_vF!} ` '<ur h|T&p[G3XH%jO05B!B!_ln+"[ Z(?4XN<2."J֞\~~}%Wv=FrP7:G<g%w:Ny*/X@J?sg]"ryoۙIcXsO=xwr]ռt"\̪ń?1hO*a;3&"qŢ\,eCF7Z ?}1&~a]r8~|9Bn /McFzZˆBe-.i%G[6u[q'S\={:X?tdWs1" 9 9OpT|2ɹ]Ŭ~^e [>AÇpZ{~ +}_d܌iųS޽{\¨#|%1VaZr 'i+ 2YsŔ4r.X*&p7r1]f 1{v\'o\~\ Y(p5Zy -9T@h΃/ݜq?xx5ow|/ξ %rij&w] 1V3#ʼvX޼Gz!ݫuS|&2~*YŤfpRj f3xMV7KW螮\3;?87yk2|Ocr?ree^=HSKB$C8rԯ~@oVַw(ruXs's^Y ?< z*:9F={\}gq^!+=#٧l+E\9Ω?Y3绨k`EJQZU@OnagPz_|9cS,^g˜{L_^.jCGq~!Ӿx6z取Skda* ;b(Àa{q8s 00('ϹNwb;?ړN9`P50h|/Jw[0K>/|Pv|tG? GNa WLmІeD`Tj*89c0o-]BV[{_N⋱u Xwߋ,\CӅӝ v%J'_;p}6s˦-N7sXA*4^Ghd]{.v`Yv9-B!B;y0l}ɋʦxTtOq׏&3\ k{!y?=ʻd*c݂nYE;s_ۻ<~^)GhNj)g/}q>i ~㿽6gq^7517۳C`<ʪGrpbs/1/ҋ>.?+au:ޏ[!>2:-%W3}"x]jø͂nJ8ws]qNi]F7O\Ϭ&B^I7,{uOֽzOa醮gw9-B!B;}0Q14si/+ib}R!-7sh3xl8 *4{y<{9[9u\<889Df޻?%׮cׄ,Z!J8Hs~.9[ vޏ+f.J&O!?gW2"Ə'/ќg /]ˉsp͠\2z?ts#ڶ"3$XoY3H-KO'9Tu,6dFkh,U߼xjnp XAe u)8N]Q7.K{4k'9051>Na]8yA?>] WuJ_ !~_*W<ʄ)[@񮎪ɼqGfbŃpuF; `yсFQ=\sְoY:nA%{Jj-#m2k]ɹٖ~K{?%ߟ;X=;sB뭠YT@g62#u %<agg;vjuYǩd1f2Juz|+ۢĬ/㼂2m?2=B!B?cFqHA{Wc6ezrmW {g_SOs/xK }m9z6YHڏ9㝦:>^2=!C J[αD7bgn(<P+8Kxh|?^^iѶc>y\]/pč8w1]v0K]z&tϱBIW.՝!'k_bh\Q~9>8fv[C17+t& Й"o<|OGz_0fTqjmvaw]1_p&]K׎[Q8o v@Xڿh kyɝǁX%}.H?CʵꂏXqF|˟ qgQ3[[KUyPskrO8^2țfcbh^blAsԆӑ! IDATۯ_*WqظS*G$_Ɓd>,sT9\1e+D0JOlٸv`,]Q7 J@p0r OjڜP~凷#u'o Su b/3?JB7Nu f1ncCskaBY'S"N쑇Y> JgkO:Eπu0=>ѝlN,H3FnCTDr5oK|5Mb_?2:N@uZE\ ,ZTFZtӉFbDil~dMH͏SS漶127;~a,uݸ:]cAyߠq Ku%eF۲ф%1tM]Huؿ~h 6rd$ ,Gݐ.?zרoŽNJnO08Xwq`bpȃ}g?.HѸPDYIe)Yg27yQY#dh("e=OfX#t!lʓowC!sqGzDW4{Op9 :{A썘8yRqᇡ<'L\.f XxpGNĉ4^.>5EZÿl`,W/ y|_:ə*LRWiwC{v,;-c=r85$fNV})>ڶE+d{(u_cz?4a%Rn9'*pf썾y8NOZCYCUԂxcv,Kw9iUz(Tawd5v潱/й]zvFN*|dxy*2xcT&6o{Q^|u:عq2o.Ƶ5+|Ͷ% E߯ܔl5q] vswѧ'm*Rԩڒ T{4.q|7gm{v1C1>E;)^Mδη/u_[}S<"=eLo;o;P,>mt uJP~+\z\G^#_oV:?.H% Sq2f7~o~p*+V'%W$}|S@AvMjOUJ993'4Y7m$\Rf0Zz],O~T5KR%t O=M w/95,LHڝ)s=M'<Ԗ5OѦZ%BQmԖ?mPџ浍5e6C.tsvcy,d0|˶i/]#埡Qښ猤U3֌׶#wql"ckgQ4ALxplЙc+"D;F\Sy*Ҟjmgs"7ӑZuppi~^L~xx-TG<ْrݬ8^p T $gnӟÜ$\9έm& kWf+X'v.,~'7cw3L(RqyYg1{ V2l5y)=WBqKcɯ(όgtOv2'bbD̳qzn'jδ'(f^""֊iyyZ Obk/оw{_exoQ⫖=գ(f43T%+ls:Ri=Xt rjD@"NNCEDDDDDDDD{Le4`LܪgE7!QTOD;ʢGбNYr[~ _å[2ucNppۏ|7# EJOžeoӽvA\wN"y4L٩3z=S!nD7/ JՠU^4uMq\\ 1!v`&q7jF͡ ˸dXcrQtY*V@xKZëߥG06Bhԣ4 `q:e8l;i&G4m4jW"qjR+Pdmv v{4,mgKvW)jU]?E{vL"""""""""2`$ы4|99 Nՠq]BҾs?C"و>.У>Vω3^>wn|t^Bۘ&s>n܉ݽ]ha buP`=إ>v Go+baIapj V}WbN〛=ػ2;k5]Mi֏*C^WUu?9Ьe;^hD¾P*]RtǺWR35%8'(ﺂj>y N9wfi5`$b$w+Eֲ},k62ϼʑNؽdglY:~9L9R`|8#cw{Ϲb.IZDDDDDDDDDoym̢wYћ$!y}+_2RQCM gwpj b34 kyfj$d gYm |ƅ,lY?)ca|sɧu-@2 tl,{g/&Hp#Ĵfʛ;/;?Σy Dw^ћ;Y9O慸-Υ{c8;3q X.]"-hvXQ/eI}K. 9CyjXʩ_H?ЍkXZv5yё@lT.p9l% ZOϰh?JR7` ŧ?:%CCRKx= T`>_{rr L_AWKlB&2䋺t*]<'zf*ze`?XO}FQt؈*% Fʅyeόotp_rs?`3v_/25ӨӶ iC8~Jņ^X6#d4y`mgZ'ˎg| )Xn# AhTx_bCGO{ݱguNf^搥!ӗORoՈ7Y9^fZS q$0c6FEuɲ -ߨinٴM[pd{rZLn [z2W#Itq/ l*t?䮯dWԊ~ߙ}cGNrյ8UJjE[C-PvKH}~2עႇAAxas`4?/ :Z+)Y phLgN@!o WF`"(Obh쓋bOAtctntǕMH縇DpcC/P-""""""""""Nad:""""""""""FHS-""""""""""Nad:""""""""""FHS-""""""""""Nad:""""""""""FHS-""""""""""NaxwloHf?8/e;ż_UTo[2i!Z=*zex>V7ԢOiW5F2rml~eC77=N"3C~[Y9^7chT_nɡIoY$Ue d1hLי*YY3kgp9vk Ge\1>8wzgE3kgOsZ;-ټk,Xޫ8.ob_!tV/<)ݽ3y,b(" v;u#OQƂ rX^Z˳P+G`M>w{s,zm˼̗n;ofSxg`YhY7uqDBxeY/KH ݯ lMi;1ҹk23ɲt5'dѳޔ E䴹0=-\Nt<4)|S6s8"2m.lEi ~~S|΀3~~JY z+4| ZZy}+ ;xgK'35F; 9P-"""""""""PJrrP8;uCvjl4E-f=Kn[B<^ۧ[V-ޠV 1Sʎ 9w1|G[E,^!dKKQvp-‹_LUm(ZYULz~gB1foć0#3V/eG$?OT ”"|ڃKy)Jtx:پ͎.Q:{ N|w3,{Ge2{QJj"^y{',QY8| uT3XŤ݋3h@roM/`k/RB(/U/Yw/5xaSqӈF]sVރEDDDDDDDDDa+o! {ܬ~5⛯00c&WO>Abz۱&ēH*C6d@W3z(m*cHv>jo ?IdK1`Zdp͂ Oڂt˛=yҽyuƒ}f_9[2kavoKeה?fApm{dvV5Hktן錏3A+Oq q{?g')3'r|,{e;== eݵ{uK:{kʯ^B镼ձJWA)v5=\)"""""""""ƩYvZ=OggGTt9ż_UTo KsQ#;緥7Ý[/tOo̟DT/{*[ۇ||{:!UzP@roGG)m?mOa5LUi:^Zނ CgWYRc\؝fpvGuIqh{zs=FC6jCf$E&J" 8q} ӌxI;u!װt gѻw\NmN2S?sG鎷qt Fԕ-ds/wiF"{ӗ{}H1{Ux7$f@lݐEF0eYȗjnkiƗ{DV~{cZP GvUj|ԒЛ1eTޞcǘh^ &{#k~Rq;9m.L|nZb.v:n!>}_b4xɛꃯ͟xgW/Ⱥvރ R1-)=mQMxn>H66دU.EJ^l b\NnG;%bw0$k08ʡ6겑e`3W}tFdӥPZ7pt<"[ROR5x/;W0ZDDDDDDDDqq0Zu&s{R b.b ![KQ?pvmg`vT `qɺ;47lift#,W䡠p Ng=;cdns0\iise EDDDDDDDD[d'seVrJJ.``02SgS/P[bgHf1 :3? fBP7`+p)Е O?Y_d)T@ >'0}AÞߦ6"""""""""Y9_O.sIϭ0P}g4O>0۹:ڇϣl EDDDDDDDD+~ٲNW7Uy(s:1sCJ2tHS-""""""""""GoSedGRm[iEDDDDDDDDDQed2Z=EDDDDDDDDDRed2Z=EDDDDDDDDDSed:""""""""""FHS-""""""""""Nad:""""""""""FHS-""""""""""Nad:""""""""""FHS-""""""""""Nad:""""""""""FHS-""""""""""Nad:""""""""""FHS-""""""""""Nad:""""""""""FH3'1ӈHv#=*EDDDDDDDDD$+ѷ<]DDDDDDDDDǥs*EDDDDDDDDD$ѷ<.I֩B""""""""""Ie-;w:EDDDDDDDDDsʗ7<%J""""""""""FHS-""""""""""Na8|!}vZr5֞]+OFCrIFc1Iƍ5l88rQ>b47|akּ7[et8/-9LqO0h".7gu!nQ6~ L?[DCIuXIS:=~XJ:g}8w|/e|F>. ; 8 s"Vsz 9͘#KǸ`~0]PP|R%њq7p3_QqQq78 qGa& W.3qM&-Ada]m<y ;p#83| np+rE.B $TC &[Ḻ o~Xb=yF2E}uΗ,FvZi}.n33Zwpn^]dp9g*+Ñi+c3nLEp Ym/Q1_4waRxRĩeP땣p@G\ipoźmF-H<}+P#aw{6ϞDEw}k9liI Q`S$9E\iI0ZK1,^{?dU Fs=鸄)h\k D +'siHyОnfNoS/VkE dE\ }yΟU;na?J9Һ׮TQX5b؟ OVMl˷$+jCg1gBNNgRk0<(8!r>$8sS5 . `B97{ aC_!.֪hOY+aN EDDDDDDDe_})88;{B|HtT% Õ5 88m;MI[b >71gn z#>fOUQb]*}˖f1xE+t%o?=:Fb ̮;spd^{W&oCFF_[.;&E0ϗ/b76=)lpsa(/mلWJUΔ'$FEϳ-z2ei82|l)ׂ?}rb?՛~Ƿu+||g8c' ֓Dq ??B|!LD̍@~zWmEP:=z.u7kmgLwQUkLO@BBIAH`굠ر.b 6A@.^C%g&3sGL2^|9e;;tl!ۍ!ĺVeMd¦n[hzGN-ak>'ͨC<ߢeILcx@v=Eæ^KeFBk=ś3mu'ffPBk̳c01Яg'?bk%z>;>^5  Q] kXOdlaY_f8#gu4sd}ĩ07NVԗ~\yk@ߡtX{c^0ƴmVSfP0̀&FjACK\1$F gDj*pp"%ڕh>{t{)Lkt` iFA5)^!B!Bq:-jjdHTP @!0{D Ⱦr㫮~p2OM۹o@S\.<>fml[9 S&=DcJ`mL~IPi@Z7 %}U\)O ̟v_`tNz?Eeɼiy\׳|iG;5Ϝb:ƦmtgɘsV=n>wx¼̍S^ 6b(FHx,MeQ5k0@%,^]KgS,XМIt&ztHŌI^Gi}m:{/"cyH,}4km35bܡ<ۀyw5I7K<0Jmj[;tmt7:Kό;gʞ̽sdܗ1>[q.;-~)AU5Z o {r^bnAL|hDXa.T*hۜ:i٩Q1DzCO1Hw'?$lp[k<;a\I ;}Z3L#l@Sp{6QOc6*. Ǎ%O9؛=y84?@BVP!j y/oy͹ihhGؑlG:h w|w$Z>@'#IFN5uJX;_WHbɌyL6cyS'jw^4 ̜x<5?{F@K%& CkRcb¢G=ZжnkB!B! Fk@0EChHOPEȜ)7B!A5iS(-y 3ɗφB{ S{;L v ,}9 /o!VVGs7աg}_ Ͱ$4AϞ/3ޞQ W F* EdǨ0`dAQ ?sViN<(+u!VEQ1(x:vHNvIY==MCk2=ô\'{݃G|{ufxxok&T4\ΣU4>5M i9qݡc C)6DR,=43۞a'BG(pP oxܸ ؂oի|BJtehBJ@ ~N_ ePשgMa ֊_$2+2iin\US XsT8R+:B!B!>.`|gJ*d/Nv'`{'0.UWh*xtȸ\p O\PX{՚=˿?d<4d}k^ϑ2*K/ĵ扙Jnra+qd2_m63$%'uE 1L3&7YiKXR/p?ҞMO]'qguDSWtK;K<8ɥW4^U]G 8(ˑ$Ԯ= W)ٸ4p9#Mչ}q~I2wZ ss(GW.I $X9cg 䱙b2u0cӶWHO7Ojwi:3.ߎj beQ!E ʮ!o1}|MӫQЁʗ(;rRA֊_͇)t XgԹ ] {pTR7dP:#tMACG*/B!B!۸X̺'B88p@}6Hl~}8xVKxVݨIuLQKܱt vCg X'> I?Ry.ޝ$ńܒAxFr3|x4S逎)tgaeGdEQPpq Lݍ=&Ӊ z~غ8ڀA$Tl\>nE#Ň Ndf ]5SZl&~yeB!B!+W1;zQ`HXp7P@o 0Ȏ9 )^9m^C׼{&o. H‚U:w0ه@_z qթAQ0ؿ]/`?w-)$iQtkځSNtgUߦ("uʽm$LkΜϏt]:[JBIfNQxfyx!3/S\2>^9sQ>=MM׸ݼa3ҝzx&\FNGx᷉fqͧĉv s$pѿdOO Vaj_<i}'-`zwURKZ'oyh:F%PAa3c+cj 6zu}&gAmSݰvi:y%]3 ½11t}ē-f+ y,F+ ؛}l6k*͎=x}[?M^!B!Bqٻ68PgkaWW8x`A@<^u#@Ϡƫ%:‘Gs!i\G!Aם|mhjSWnPlGcW"&̙gph^c_mˆh',0uN 3my)p+{ f5utZN{ib#C^LfevoY3}8⑵cF-k@%(N6JCW\zUWPUIh֟hzY'?SuSY+2]6`h8~q쵭; (Z%+|}cO7Og1Z=mFR;1|qBHμ\{MFǟWݶ6=&"c1Xn0O^XW>20S'}*"?ǂj_Odu?8ZDc@1Cׅ:VQ+"rr%1DWڽWV>q6|B!B!+ htHm)j!(:h~X' _]o 4{;kV\/8YJ}E;/(d?ƽ`l˧VnІO OW{+meӁrbƱlG $x(t<<~:"<$#F6'aᑞw:|lZ~ `[Etkrbd ¦zV ^0-z_d\n,~u +,ݖ0)ǎɿeOYM|5N؝uנ9/ddQ5`"Pjp[vWPճٵe6g&'m 3MDdtf*GO,q\#!R]Rn٨j\k4j=P VVA/ =C@t-L{L-\0 "M *,.Jta!jRϟGCaa2.a,ϸEn̶8jX,?}䮵d>]ȯyB;c}-/2ݍB!B!mQx/ւ_мEKEEQ2?Z$kk(((VFW+PߠQ\)rU?kUp :?A l?FE !?Qkըn(E5":WbK\u@ԛ1&nŦ ?}㤵Yyv!B!B񗹬Bg#vWRGqYsB!B! B!B!B $-B!B!/?Mq6B!B!BdeB!B!BteS^!B!B!1MJVF !B!B!I0Z!B!B!_NB!B!B!rB!B!B`B!B!BB!B!B$-B!B!/ F!B!B!Pu.+svاy\gݤ,Ng¸FX'zˬ W饋zF_)&rg8`]}~LR +@hKn2vm⠹0Tָ|~ >/ ~!B!B!o;GL<|[hד~ORMy8. $ƚ iR9Kr='6>Sm k>45mɒ FD踲OqL!nw^5;Av<;>QPELq% D !B!B!f-Z9a/*?Y8xsi+jZ!)pQ&hQr4I'9U:m!`gㄮ j"߱>DTTM~`MVØנQ&4mژ&KO@F{yꡱ MSZ7K[tܹ8z^K[ߠH˯Τx-ǐ`ppt~ƣIy+݀5Ȋ,04wվ5Qm<ں0`68 qw5 &wcHz!B!B!˧נ1Yh|ϧ⨸wI nIhFԧO }cn1Pcyn$ߎUwaW y:B!B!7HӡԠ;HGF2(7Ynen!sz۴tz< s+3f.؊y+'yQ\>ͣ7h΃;&-7|Du\Qvv 7/J['AbϽrj~3GMufI薵0 ɜw'y]kt6uK w*q9z~Ȑg>z+B&wDtZ3fB[&xW˺P&m$Ewt 0e3Y,k:+o(9[?IMgє9??7ZgikcѮu3p)3 !B!B!Ii:+:/}_n&'fm-C{Ql cӎS6b، ϓ,pշd\z>Y~<Jo_B*(JfɒTM̹4ua! jSBHD˞}c(,exs50.lz-U m&Ң-W.,(B7[1yK9ł/vPsP[zCJ:fS:LaXUB!B!B"6kѺEE|` u}l<"Y }  Z%b 1N֬C͚uؘ݀׌nDǧb )s=}ϸwC?Z޹IUb& k3Ou;w̤,Eٶhn UB!B!ĹHesojLdL?m_P*1e.~}0wv )Hu,w̓ml>fڴ3^wFj$MsLoi4p0`m+p8\~eRS%Jx3pWYwAy1G ˥dcc:-_N hyli^^[qcI!B!B!(^jկ~ WnhNk3ٵk> o!c;\xyou'[* /Ԡ\7Y ׊嵕hޮFghLUPġ/_,S=}-SG`:sMG"m%Eg z_@0}}̀)Lp B!B!Bpyk|=\-|dM֞= =UiYqE2𞯘< 7{5d % PpW~\|W5Z(Hae6Ot'F>˾Tr7I z‷LGF857fR6ffҽ͡)AB!B!$U.qu+9xzYq賬 ;w'myW'}?@-9ef=GJ";o9F ,9֝pl<Gmۿىa̯IL^RzXh0z=K۳µ\}<5o&|߱O^]M% y nlv^B!B!?KYGȺM&1uW_s]+T|._3ݐ Oܹ]S攙\: )歽$^|3~w~KusV1Tu;+,YKy3{HN 3frB8;׋رg æϠrSpO.vZN)_ɏݟ_/&B!B.`aYb<~\lu krQ/ܟ߇RWr/ z2xng4_ 1Kc~ vU@ľ%t]|-xY$~S%|Fq,p5A'ɮV~]Ǹͭ c~OU?W3xoFf'puCa\![qGB&2Rχk}?;y5=&'19O _ݬx+ cO_S_@FHLq?c~vU @whˆ~"Ŧn\fcDWni"xw@:~Ŧ15(ԯUvo]/~MSTЛ8ßQ5wC#Z3:[ 5hoձЀLMkZ.߭ڇz?u?MǪaǕhj/Sl h^%ۄB!Be2^G݃mXN.G88Vĩ<ɔ$~:j. NOÞftT|5]y>ti?4>NV f=4NVX&57zgr8@c:\iS|:yi8IZɪFG~:+O{lKfEzIKU4S>atc )aSlwS~g8|Cg;wSȶ|˳(0=@sT* ?;d ׹̳*<ŏaWbZI l $X+®1m90Y ]^STO;V28y:6W{=:`ە-b`l+ IIONDv+_s9|OA"rs*53lD0$xmY>" O2==E_r捉8BwX֪/8AXQ.Z5瓎k/3+Εr"mޙB!B!82^bܲ?{ #gWi7t/53N2ahJ`֍Bqnv5уFnR] X3~"K*x9Qˍ3WRlXȓ#BZIߺfצyt]M0̹&/5Μ߾+o%A"uvn+\;lޟ5bI~37ZYܡ!?9KnFMΩ i;gBñrH`\^n;|( Ƙ\Dy;ٮiw(&Y{3k9 09?⹘Ƽ,3{?ZL{M4gѶPtAPZPRkԌu[Xqǰ#]J:jgnRZq =CsMNɨ6(Ŏγ氯 mKia{lU9Ul^ɪ@Fԧo\>'e3god5W6{S͹Ut|Z6&).Z@UԊ;eT!t :LX6zZu.YGsگ> \L^GD7Z1T+ZxUB!B!yq0D~ױ,hWiŦRl.SpyW9cJઁ 6qwB0yG{WY' V)~ X'z-ƈ+ olԆUW`rfN:@0LGZb.=tG}l;T/sw[L!B!BwYʄ 7b Nq%` Pո>me"qCzEW~\9G2-hߪ5075L&ݩbkYFpG`6SvSHqO9hм)-}Šj׎kU ~5٤-P- 2rLo}<݂fNl*իWHbk:D8\Ess;^ѤFm"q椳1y.uJtf\/0_EU}Nuo5bB>'۫yRنc0K (6k;Eѵn Yyث1V#U.g^',S,szW=1gvUh#4mUO.kwKZ|g/6 {k sq?JpO fъZ9Ti,1 ۋjE)&@1Q/\Q)3?ӦpZ_1!ʹLx(|WsB GH4W~w4[#)tZ;}ϡjo|{:q3O\L~[_(ZR;`K ̸عe>=f޸^T]>^XШ.JG9E `LxbT-hRyW`4:J+s0TQɃyސɶġk;dY 3iZVl'ڐi{6`b8(,-"m ¤U_x\DN~+})dl=[&c sŇ*MߙB!B!8]0]GGXД.|.EU1cA *RPU*lQv|pt[SOⓍD\7jKWTd?xh7J(j0wj~ɾHrRIΪ|yS_UŠ{ʎa*|ݧ tJzӟU̕l* Vz.J&ZhejWzŚ,g)]e,fc9YIe5B6J%yq~ 1 ƻVJ5斯9PyL'3'~Sv)wSy+a(XТ+3[+8["H*UA-~5LP2( FFvCiyqjç>6Wʺw3B!B?M?Ůqݝ NqAט@vиEm">_U]hi: UCFW7(^۔`rTSRP[,7NJe<~\wx/6Yc,ts;&#CR||S>[p \gX>wSxyVFي˘/ 4q{4%.vI栫]՚CP\t~U t>Yե hθU HӊS0CG}XWO8VU^sRŜ4G0$'lp椰WGv2+]5xs#7!L>N˷[7Ҧu$9ϭƳ7aU5G';+t|[&㎩Gsb~G?**_aʲxoh_U.VwB!BwU&ܹ)leLrZ6o.Lyde 6.\3*(S|]-^W:_U]z<KyT!{Wk fkSȸofsߞTR:΂LBxc6[Ң x9Tѻe=[poK5G⿚n^T58T"_3Ekт5T636&F/Zăi$el_OjvCUΕ*>#U=Šsb /9ۋW&>T#}%e1c**1iYōUժT9'Faߊ?x5)$^X-nd2=1lW!h'o_s:UUfP m5Yzu";rsXkq C}cwdeƖĭܶ2mqQ~Q!ཱི?[Ou)*ޙB!B!82Oӡ] 7na_$:=skb ML+k z0CVtR,Z12PcxM?¼ͯ6I?Qw:}pdj1S`mÉtnzP Dk>VQ U- (SXgZQY|n *'.ҍ ku 3.ӞjJTp0RL4 U,D]k[P|u}i)MhKɿ |?9Oo7+?K$^oػ㮱e?+N#>O?]~9wGsOk,ΟRǏh_o61o9|yivw'VAAAAxPFg~?IIbx[#B򊵽+O~[DŽ;ֹֿ)VyȆa'昼_}w~WrpOg/7ה?o<;|Ko?T'?>6rb?~q`Uڣ6+M{x/7^P\55e0#wtlaux_?n[Gu+wز8Z~ +,GA|/} n    \c5w~4J?i> 7l6om_+ ?J   CT1b'U 鏞/|?l-   gh1pV GG}l_AAA3/WAAAAA^AAAAA/AAAAA/AAAAA/AAAAA/AAAAA/AAAAA/AAAAA/AAAAA/AAAAA/AAAAA/AAAAA/AAAAA/AAAAA/nmMAMAAAAFOPJ.Zη>Nofs/(" KCp>t*Bf֚ 5ھًK.^n֏*zՖ/_b*-j{ET"nC /?aٰn1;S/}, R>V,i\_T1FQ`Msn#>8XeQJa!HĘ91FQ:A0 8l6}''O0P~!aj:V] ]ὧ:(>{vbvZzgϞ1 1x{OXkSz(hE0 t]7M>3V?eЩOB4n"Ez(,ڤ6MBk}H!)^Q*g'` KUUXkDkzˋc MӤbӽtϽ/p.EA]:>&?9@dp}if{?t.Ҷ-O>ZK#gsιkIa֚~h!t݋5apc>~)Z[M}Jτ, N' ]O8:ٟ#   ec'RY|㶣(7nv$V mG1 ĝ5`>`kA)(eʂ'$u#_k= sH c%eYH9GG)C,FAnQY %ƈIBe](hf&_oa3|8>(0lk-_i?w7Io}[|'<~eYN}]ƘI˜k !x?~ѳ3ryy/PXpzzwhiv,vg]kr0tTUb0]z:NxzmLyLXk'y~q?˙$xm=m޾<8{AT|j֚vHk !0 (PZ5g~q8&   6Ƌǘ Kߴuvbu( T-1"0L֨@ĨpCro6}]JzEW IDAT9mcC8ƀI$ڏH"Z'ha+\ 8:Z5ĈR;3M"z4]ف)BOQl6,K>s4M?ˋ/XY.e98uɥ=*9jG9f-kפ1}zU{c{{I&1,lnvyc',υᛥñ#߇gcz(η0k\o?3: 3}q< LjmL;mOpɇ{%b    o'oRy]acbԾX.8u\c?7n"FPa0}Nfs)i(*F3H5aav~@qzm>$-->smYSUG]1^}H.Z0Yb>sn؝2={F], קu͓'O[D]Xb,lP<)9GUUc~Lns:|g}AX) aъc(u=:$0ў]0 } _3! ǣ=~ (*mv~!aTRŽ>H,Ru"<AAAAb1o ĺyUEWq-Zk k4ک)WzlY')gyWmXI9,H"1me 0 ~'Tj3<@ΏWqv?r\ i(0 80#3 9zKN"0 SVb`?NQmz)GQ CEA:ʲĹ5Պahc ?r FZkʐ$"'a@Vdz2vO㮉{WiM繭ߑ뺍C1=}><\R6/ AAAAxyh"9Ww4.;2;/!;2!~89=(ꝨrqcRs(xygEŅwbtr: h* wS}ק\^m E?@frg8]cE_7*s 푅aesEc4z F{Ҋ $P-={JEA׵<6EeIQ,I.˒/^PZNNNh-(0FM|ڶk%咮xӘj H91p((lj}|N9wD~N.Y NFqƱ39B~p`yƝK@0#NW#:4'PL}~I9yLAAAA7^䰼͙8wZ|U(˒;B18* A\x7ED(/_n޹o?&*bP)(g-hMJ@C;Fg(Bx|S=]MoێQ ;/T\oPe ԋ#w{7sCUU,}GquuEY_'?Ǐ8VO'''c˱>{ݏ8v{_䜋͇89@o ?'v$vyr$*cibEb>l\dM(8}{9    | CQfw~Q평wEdx^orLONI`&51BYpI)ZEh1( |(TӶ,98.FO>}Rrzz6h< jrY_hۖx9MLǏsrڹ_#;N↋ł !lPm2+EHd'n|&w}41b.ޛ&qvj1OZc_υYϲuڋ0N_aSS)=/}CA|ޗYܞAAAAVx1&98ڡsݫ>7m*gZC(HJq;BhAiV C{;V|$͋)FN u$zd0&M)5zT8ϘcTq={rl1ijQ,j./6XS2 i4MC]cuOߏq% ʲ{aHױ\.(5t]C]ef!Ç(]IL}bi6Xk)R/5^<">N.>'?#y{b_=p]DkK8|.w ~pG9z>ncY7    /Fs94}wqL`;zApcF ufyFYX0@!4vh!Z.(1Q8QMlwDЅ&x~HΠ0F$fwJ0cH"Q/՘Fk Y)(dZbv¶RAD0EmK yRF .k =XlZV""RlBQѵ-18Rێmbg8'qu,K=];Z-h TP{S~wyIQB3 *`L9xSvhcdDyNrbmeGk(\pƜoBrNS4 _Me 0+?%Z+"Z;GhS@{ *>PZ;劣St]Z͋jD]tG` #ךӵygn1xȶ09 OFHQFӴ[AAAAƋsEtsߡ}s19-FgQ0NG֚Z8{OQXk5XwSFs@ CN'HnFL·4 ~LŪFQ> 9c_SQ Às'i kb먫s5v /^^܆*Qa*k|sua<\]׍B"929Y!Ǖ$xvW税°19SZkyyy7Ӏs=MӤh?ar 3_w{1}i[zp[wlAs[[/t<|ozo&Gr:>tx?n8AAAAxRs1u/#>sѷoIıZ*fCQ-j =̮yl1frZk)| &7t:0n_SP~|rƨ@5#~eE$!aع(;rDGt5eYu0pWEruz}ʏ}|k_ùwѣ'<}~pPOW,W+b9G۶ePc˻ =*aЬŇ|dd('ڱ' K?|6eFo‚8ޏM5ߗb7 0N6,)vg&F`Iv    o#o}SL|/t6?1,Wy-bT˜ҁaعxa}WubOXBs;f1;?@Q,h/Ykcfq h dY67mr4'}sz+b<|ϟ{7 ќX|yyAwx^5Ð@[}ʝ,J$>zG7o=l,&MwzR>#!Ʃm=#;v&ƒr؊<+ٞOg#].>^ôL$ԟ)Fd_ο HWy=!   6Ƌp]չj0;#7Puܗu#}'HU[naH꺦Zb@V c cX/]ΣU* zzjrIǰ(L\ns~UsAgހ9w?}jr?"Lb*>}J4|Q=ejo4(jaNEr =mWO{g c b]c]al;9REjhbwIx(4}Iat~eٶ-HQTXS2 JiU*WVzo y}b\}M89&Nu!{|؏Oöř`}ώsNN;A:Oθ0 =8Ue$u\tqAAAA/_ 1n#8߸}qxu$r1oLkM])f# n1csSf.;obm@95~fLϷKch2?Zϊ&3{茾AAAAtUj\m??֮j5 3DM˗/i.%QZkH.L}D~l=Cgisž |skn."V+ڶ9r,KV+:4Kmuѥ4MvMY3r-5mmж^q1ZRtq~~γghۖ(ŋusX.|v;m[*Yt߿C~>6rLd=}[ ymp츯_YsV><4woW    x7Ẉu]Nnosl,6sL+!tBoapKPJᜣ(Kmujcmڋ=*C̝Ix~C}M1Z2IgruZk b9W:1Fڶrc 1]}/zݻkrIm M0tg/.Bk{B'Z)Xj)lw~Jפx/. 9Ң=y)_z*6M.]cًu햫6Nq;EӧOyηy1GH\i' r$J˼XDiYvcµkMa:ncknos69fǹ v{ eYN(*?X; Z9֣,Kg\t,@' esvRdhZ=(l=M2ƂD=eLvMӰ^.0 '%{./.Ea C*ؘ0WW[?IUUXb9mI$a1UUJU磏>K 6, cCBv,'suur}`^b{֊=, BpX|_RQ? 3s|(Rv&q[nܸwlk:_vS^۱bHDAAAmL$f?aXN,gűPb2Ma=Pʐ$Zf4HQt}jj#Mkcrs1<m:!FS30LQk0( apԫQ4m,FAіXm| R0ɋhBTb4eU%״frNOPkѣ@\.wM<"PUEr$ƯW#;8ǹ$7:<rSV N\.Z6{Zqu\߹YϞ}BYF>=BӇH7EQrݠ+)$Dv`%FY3 媦1݇?L.EM^Upqt{cVͲѤ :=QG">eHJToO4v*hyJ@1d\sуBCkޑP! muJbtTwԊ"MbVT4>C(@>. L%iϩQ!Rt_QJC:#Svk֌RE   6ƋљWɂfߡ3_fmMk((wy:RK]<~a EŞ IDAT!,H].30psV0 M!b(=99>UDu?Q֒=ZKQb&sx&vnvO !ZKUULoItna$j7MCc5VQ߿,3^ Q:^_]nPJZ/meYEQLsH/%!%1 ~rrM|yA4Vˠ&ܵ^A=s|X\L| ۜ7q8M:?دڪcb    o+o}=_z1nE'a2/Z~']Cc½Hs|c{~|yAAAAxxcl>&" lǖt웎y~OߥT}n]֬;kXa VXS\L5ƠԜ }OaNf➂(Ejp $izNq!?;'i~*IP$A5  ˳gπzZUl[Զbrзl[l6 SL5u]r{oz)jWeYNBkQü:5?;?tΏ>I66n-]a~w-IC^t=oov뻽I>vl"    o;o}LDmN0{?~&W AZ??o,[x|?;oʢU$!6%V%΍NexhݦaF2: M7 E4BA=r1 IM`_2zu1mCv7+)B|a?`ж!QNl۶aB~e]^qvvDih{|駬O8==O>!8 !ж-UU4 ,K\Y΂`֓v[–i0 ԋB`ʲ}9;7@ $*p㻖"} 咶ml6hj<4\,c|ދ(!x̕B : űd{{2od|<p<&䉇\57F\qU~qḍ4b=SA/Ey#   qwe1? >su(Snw~{B`tM^f "L}߳X,jtxvf2SL8*"$oXLq)&#TTDk5%aw} [퉧or#3&\=֖ۘo{3}3oOzABĠvm5%j1.d]s~e    _vxgk^L\>&"u-B3:BH˒[&g3\Iܢ}߳^Qu]S\AEV):9dM$FAjߡ|rq΅='a.pX OOLj1(u=a)'ٹH9^+!GhM4򒓓 0ExnxГЯ4ڳS60t#M"B?3?pzz87P0hP&Nn̓14 l#Fœ'/(!vL͢tB$FQ׎3߇9F$UERZkXwΏ&%?y'@ q*θ{/ 5{rk^dqMz!gyǘrqf_?󵥱s    -bM$_X!Uڱ/&Az\n*GHyI .NbJk>}z|uuEUU }0o{q"mAw'B}Y2SCPsC+rfn(%wu)#%btl6\^^0Vtc̔}4Ctj^V=ڂXܻwy">I[!>躎(Rłf3:;ׅ裟]{dž16!cj5?82{懼m(:sQ|btɭ4It1-r<*HAAAmK!Fj<.8&k35(Vɝ}J HڒI֓(ѭk'a8-g14mQI !<}Al~=M駏x޻4M3ƞI ʰ'Ybmjtn?c;8O$;.mV5eQg{JqA. 섫 +t]C/^RU%}ܭnK@۶(E0|⪪om5=g:!rgbϠ&Altr{?O=<<(9w aJq AO"֚@v7ObIݳѳp,_0x '=IQ1B לƓszo)_iSQgk2Ap(f    btZQ@˅!e:Eg09-1nʾ*kL̵&+3 QcpTu-Z[%/0:bBLU:at]*W1s1$7=F)!b.V\;BϢ^ǂvYx|H)J!QQT~,֧!@h5FMs([C *1e9p> n#@S|GY-hE xa_^,99]=%~nrA۶wvV'-EZoFLyuV,꒪nQVkX,lxyq{@yzw0Apj$ jw1 PX3w'UtJB*0JZ[\ר18 \(Te|7K#Xc E\رMA'bN"{@~_#>x<asbD~SOs|hPFG規!`ƾQ6O"Rs=}/R֤pEKAAAmK Fsmwۤ9Y:<盖cծv~,c bȁ,i. YSMP$eɦRU}0ڢPnk-Fiً`z.tA¶J f]$v]7D~ػ>?~VZKacARDPV5[ұemV&E0 Xk9??SU$V+a H]\]]am{a n6\u)jOԟ{bb,(,mpr.+)9Gsw,K'??F_E)t8P'9OX%0Ʀ$Tųzǜ]!cCDt<5V%4x7:3`S~HbQфpBQ)!<0r}*Bӟ`T:jO.iI;j 31 +L[Q&(k0*]wxw8ҽKZ F+AAAA/-o:XU9tD޶u,|&ρl6$a(˒af:NOOǂz6eXL.§ugc w%T۔ :,o:_{reEQ%于UkMe ]&Q{?n\,,K,Ku]'qZ|y^uYM"s) $4hŒ+R,2f}hi[Bkfa?~rd۵ԛ OGqrrk gkv==nz3ǬV8sJDxtP}<8Qq6a!ľW!9#7G[+[<3z4NH7F%]T̑YQRٳg,a:f%8=Zk8ѣ"w!Oٯ7ϭ    /F$. ݴ<ƱbzQ}`YDS5J$9?ˢkQy,.]`m9i n`nƘP͐Ƀlr'AZKNNc؉|ى퇞Cb,1&$G?tD?ĹaIMI&pg^OmZӶ-kSt͆R$r>}:]ׁJ9V+-ާTerDc:(ƻ͆ %/_vTUy![6 |VﱨKm/p~b> VM$<ǘ,* Vk~ h!sZ'w 8PCEV vt1(Ӥx?X|GWrD]hUsݧ1F :HQ#zQBv$qRtct@فcrP])O!&1@Xpni"F3FtIQ CD^LkRLJc@5'ѩtn?3ZAAA/\9#ujjqiۻ]qscS%<!^7BoU/Yל;;ڶm[lۆ~c6?(=F,Řn)qCrf'V7]|fcPBL^@b$z]Qv7&v~(}t%|΃VEATU5f7h3FSf\^n("E(){=zXz5 Ur:#3MP?$lv_m2jӧOw?{o+K={Lg;T5")J"Z5АӪz#@H Aк-! @@REʬw8c n s8ܬ${7yfxk5="lpq e>ʛV``$q<}6Ft͑'R5Crيxc<06BaD6PY Yd@&uR̀)QY76R3$s+976)b%b@;H)?Yİ\)***************zKoF0|? y&CӘmT/6̍1 Iʿfx7JWG~sa]ld&s~VrH"Q`lc9R;hkL~pjl$dsbuT^w]i,'"3Ϙus>}:'s\-MCJ O("D tm W\|ٳgt}O__7џġo F99YҶ-W߂{5Jpq,ǀ|Z4bȦ)L=Ԍ󸎊"`G‥P`~rD#.jZ`Țc$CΦ؇wb:D3.cDf~6/=Vf=RNa#9oE %P,4>ODOUgZ8y) VX٣nR&$Z&E5BĘ|?nJModžO?1~ZU" [HzV\.8!HNOF1&y RUմ}VOks":na۱\1gg4Mve>sww~?/^>g^c斺iӧO1DC崶7S L#zd7yI"#od ['81d|۾vǔíE Ɇ}zy}72y<>=/ܯ3.ρD}w왹޴֒b6G#:)e}h}QCӹ%37TqFS6SpovA-|FS(!'"> R$IG;n'"@Z;$m{| wox)l[6~>Lim[crxZMf|1Vt:~C k픐>4Л #dn#Ȼ>$'?5}9@p|WWWY3~?G?>|}~aCOJ̱!8?1hwH*y"I/G㚌C2&016i|X*{xg8th@Ϥ%3>ɏxP0eb]c0Po "]K.޺`?ic̞wE'Jқ|y), 7HGSk4\(Z:EǤoÄUkQBwLƭhj>+BkتtǺ_ IDAT@LnI)P5ulVZ!|SU4DRڷdloUb d 1BifI9%񱝒)%_ʇ~'$Z hແ6Z.l6}>c '''}Ou{*9K6OD%}@Hˡndu:H.⧊C4aBBI*$NQY!' bs&df*$9l-ȋWdCOp ùZktD$~;˅-Q{K: l MSvŊ.**************:KoF`Ie  2Dۗ6SoKSXS ?)<`c&1f3*kQr4_JTUfm\eQwwkiRkqs`H)1~co6~̈́Sp̦"cX\4}d$-{bPU>E9BdN)1Ql6wu=ݣ | ۉy,.G&5Ϟ={1!^_]\.quD3X>yBߵ9ݎ3NOOO~B䂌ZvMSJ6ǏNpPN}S}oΡ!INeLj\ qxޏ37覚\0~8cO!/ }DtHpJraM92yϒF cAM^_Q;45ɂ/***************ZKoFq4,4`8,foL '|4̒U"cLt@ zC=hjT,V,%ۄb+0ZD m\ڜVCL$|G05A!f``lFTv1֣md('`D0bLxq6 C05$bɓh;" JؔSC2ohdIܗ8:4uϷ`tHrEPH'FJDDGPcahI`@p Q$97$KBx7gFX1t`j7'BGϢq||3릯}V3߆쾩:oM7+}ʾ_ա?y?fɐcA0CZ8: ]M,xXoLIh^Veƈhcȉv9ILNf35)%nnnj ZkvK]bIi4:nw\\ç|z}(}fV[5I4BE&g?G<ɓ'y ד8ω1c4 /_ٳgsfο'^w?1wwq#H.鈙|_,$o{ [ 꺚aEb ^k 1"TXš41nw}KJa8?!ĈEQl|5PR%xM) ⽫s5ʔV#{ZfX#fZ:9b h]0+uMME2}xFqHɩY,gܭo0}#'OU5>#:fFRl{|ck3d^ǘ\PU3BC*|>uU5~ӧkrF48==chیX,<~)bL)r9?!slJ[lFrRw@^dyPUfчfQ_S֐ Ћ"My WTGƴa*7HfbEUZ#5Zs S-1񹈠FuĮυQҀ )f0e0Ӑ??X&;rG3>H7牛3hQ3EEEEEEEEEEEEEEEE_7}%/\m8ۗ>#CGyLg>I̮MݪИ2Yc.\gR"=˕RI܀Xgivby%b"]ױl4fSUts1bf-WYD\.ƌ1rE}k$>q{R{F ^,ԲXGUUuɴ_Vv;Bhd`7??~ng+.//1BU:0vm60 iSϩϟ\.nYV4qm6ԯyYV@6{?$%+2CjtݖeJ[D.guL$!>xr` N 7&D,1ϣd} uC̩8uUNmv1Đ4OARd1o:Akۙ a!͘ MJ@-[~FɁv>4GM6cc<~m9;;m[5Ջ, `Zf3.//P_})g|;ssBy]#\ v1P]Z.899m{v((ӓS~Q5!%nG]88;;:~4?;9'hL͈Rmese>ϼ".0?FAej{d AMJ#ﴊ[Ы;uIʙ@I$>e^J 3RF}O.z,gWHcsbK?B۞qV+̬Fj TSSu iߵ DhF DULFugةOmEdJFO9#!ght}0ߦ{АoK(mcC 1>!7qjYQRUܶ׀rqXS93qeqךc] P1n#&& >y\` ԪTǼ%<9,{LNoG()35b+pvײ JH Y3O-j+c,gHؙD0\X15h 55T htFe aJ{~m'V&0z|q6yVTTTTTTTTTTTTTTTUחތ>dQo+vٛivT4Nq<<4'S9 Hx1l]i0kbmwTf*7e*\82gc̆k4G3-sܶu;8puE{%"x~>4JI;cbmGz{|Gt]G*MжM]B8G]EN5v;._:fvΛ]G]ͰvC.nɻ:l2k<}W^Q5;vNV nhoQt|u>9}Oif5jpαl&;n_B߱ќY\w{ODD:=ewT!bRj6 YAGlbGdzl>cUBDK1vj?%V,O4Od( u'kv|3"AIg vçT3bJfI%.D`R)DRT4Qy%M DZc̓@81*b_ϒ6?mg}AEEEEEEEEEEEEEEE_?}L凒)h%3!;,蜣l=S8 bR컻;fm3o].^ 9j29l^iۖ{...&՞-|d 23| 1)B"iN'g6s %Ek8F4[o%41Tx$%Գvlwk拆 3SFk-clY:9O<1<9C*l1zbh $6\4sfNC1]6g͔vu]e tn|m1z^qP"}OmsSWt]vCTM[GCo{bRdOp/o8cNS xz-U3JU5ga8#6yiGz\'ѣxQUHPI c*N# =V̭CCĥq$It޲B}{FtD@ȈrQ–̏6XDs0?!d  =ɠ"^hSҩmv`2V#7Mo;vvCqlG`waZ>~>N%_hD!{1FbG~8&RX[/ NXY- 旟w̌N ba\\\p۰52aH',T|nU\1g4oyyT`+:m3)zG/`Oh">$߳sAAsό?v?4cp'?҇(1]TTTTTTTTTTTTTTTuӗތ&obN-؎l1"f{sNv]j13k\d&z_Mh}FOٸ G2v}6"&rnGK1Ҁron#$0 Eěǹn-F40B ~j7Y= *~%gggf3Mþ&J]bA54 ]gAטڶVsl@ NNOhwkVgg֜hu9M_/<Tnl>Gu::$՜{d娂Ư'AtnCu~ zHyA0:kX0$p;2xJUesnqM鸚7M,Yfž%Ŝd#ɬjwPtP1sQc6orazھˌ߁ jp7m6a>d^3x'SWS./sz!E$%x֬N.\gg |07*)"l6 ꘩e4hn0@Lc,#۷kRh5H\}w.65 J zLex|_򗄾H)nIB6S]2 ɓ̚9q#͎| @uh Xz1HfG޶<~ᖎ(1c ="S邧&I\M}Фg!AS58 nooquzH\^.6&8Kr1A[9z"RV_FP7[+m\\6JGm+Eo }Jȳ;JLyfa ~u͉K\^HjZ}$:Kc,vbuu!jA073y3Ç\$E1D6w7C.ܗNl:#:1ǂ]25) R R$rRu[S7PW =$lr3^-im B= 8Fp6Klrbzhl6Lok*I)쐴lprs}MUU3S׎v4Mj0t?Bs*v777sz~joX#'tvjzsǏcdQ$vNNxSRJ94q63Ew8KkLfg""X0 3gE7&oV )O~B~A%`PR"3/}h ?m ^22a!NpqCÕfJ o[IucdaDryB߷m)͆nGH=Fl \NtҜ=! #cd\$n71ʦæHa0|SNFXGm[1,<8*FsX[e{\I)%sc3 ףIw5m?b!Fvin7kєѷe\5%hJ\z>gՀqWS>-fs4&R z k-mgMSqw&gn}X,nVܭԫ̭S*Ue6n1VuM=DcĩM!!(q/P)ZDȱv`>aSJֹ $ڹM ~l!-1yDrf1gx1ݎ;UMT|}{1== ~B/h TnQ73υnG:DMu70Q9??gٰlX1c[P2O@Ll6c -N/ޡm[0A.P D枏t> )ceՂi9͏W?]S'k:aCo!*MFI lcs6D$ )@/jيtk*[IT@Zl-:U brO$ $Z|BEF!x,8b3Fvd *'xl'%=09jC$xi4udpMs[AC^u]ׄzH0}P @=jM`C&7U` nf3 0͸] Y3F)b*mS |liR ƈZׯ[srr2];mAɈ ))Ir[%gϞ1ϙfqӄ8!LE7{JW$ 3I &48|N!~{owWHḼ̌%0Ho.8cє"dܺM_6B%8;͜vKE铒D䂛ێ#Z c'CqCJq7LV+c6&=qc(J1,G6994|—<S_VU'39QQ!䢃Z.:WU Fv1 }*y4!Ud cf9-]Uyz`1q57&USf\2sX#؃:2ma0c:7S$i1o'"'S?oI*Cj{?;lͶk5|2GU7 ڔ|Hݑ0`rQRk,Ex<}c+b Z5i(sWlL梉n#!\Ff3vGJLsEΈX'g|oXĂjD:{(}06OtԍC>%0%{ RLhȸ k*32j&}}3inw]loo8?}LZf]mo Q=FYp{sG_B4`&j`jӬC'.%YylC ɫ+.>Ue0V){Me0ƢF!̀\L<1%m K0Ӑ4äިaUBϝf_`_`Ȝַz}#0%|\0'!Vcb9ƹ'ݛ*j2]=v6ojE6 ŁKS֙E,)C9(9%C @}ﺁqnjxvLD?:ai6f3kyL3 ܎؏DL;"&"jjH9;0 Yn۱\.nue. طHR*c0uC:;C5iJnHE7hoy6y#1S"Be͐PC$ SJxsb-,<bV̖ \s.:i% E'gu1)~"Z?"S!D71 bP &e~qJ׵oY8=NO?b{yGϹc憓9}LNJǡ$%`J zI&*Wb$MTãMbIF )_szql6Tz>\&3}/IݿA!XrR~,bJ }?xL\Q͚a/=!CFL˧am/߆ȦSL8g-|sqxwLq۶̇@68~Jlt\/e`-mEs F}c:om2`BO8&C6Ƹ2Ә֞a!Ds L1k5Ʌ! rWU6X,mlJ>\.\\\з[\Sqw}=έde-|dF5khw놢|}NVlX4ML1#7{@zAhqZsٚw?&gyr® 뻜<6?~Lvxlvl6 rEy9y${\=c<5&Cr`ssCHl &h{?O}Qb Nt(pzzjnoiBkƊir2zXN߳ZCFR q:nrmN<"w$X:Uǧ|G^ܾlhq*+[s% g,I2Fnh3 @OOlo1IS©C5!Dvc vhQe|s[9h[Fs:=i"iٸ1fEEEEEEEEEEEEEEEE_v}T)_|N:pnd0D< U7$"0 \;^~ӧO?|laP3G*i7{>g@P)"őe)f4#iN(Ep &{]xoA8_S^l}?./oo= e!-j:Ug@<@VP>%l}lA8"X6%b1Qbg0H)%R$ii*QAaź γ7o$ Y,yrr/G'  eY>YYxwmFwBOˮ7¯):u:7q \ ڡq$QA }mwN/):OV;"p:$Ral4z]~pǶlmeYSvo&/[J/X^YBlnאR^6hFBV>Ϧ\X,Α h Նb{g#&E>;/ǝ;q8d$ y6p|ԯT㬍 d}g}\'{wV"U"xqAPso/o;FiNY7a75E#<"b= ri=^>&M .E ܯACWu>sL1:tu```````````````g" y;Ros9Ito*B8;rIv]֑jh gOI9>})vF}" X]Zhzt6חVlF =BDHm'NcvJ%:^ikf xsfN3BP Eq~~㇏(FǏ2 e!URlD*5 \qzrdTU_)H ob`>W9<<Td2iK4!I4.8ѿ##95|8YOm?(xH*}W8Vi Y$n0HHEtw gq!`wJ#=Z c|MXA!Mσoor!l-ō$\~rCg[3G>au~\y)}M\&?y 1l62 h(Wk֫Uk7o gTU\;aS8W$ 7oގZ>~=o nݺd2#PȪ d88R@7rˋܞ3篶$Z((UY/Ji>?(Q , zjg5|(rB-㚚9<>j2XB M\گkn}퀁0Y'>]x5}׃ gp)2N,n.T@$ĕ+b J<mn笔B I]h 끹1ȩ4+jÓs޿S84=2Ǐ3͊=|>ۚ$Iz[ Λw/j'nlW?漻|McR ,uFlڠ>.)%kȲIXq^)!`2q&{&*!ԌcLS7]Q5 M]JfVRJϱW^ú?|,QZj)ҚhJǦ4jTDH <!"5(7+lS7L ?P5U]Bbrycc6+/[s/S9!I>%:%m4ew $-Z<ѷv#Ox0/9:ʯ5b>GiDE|9cQ}K^blhYs ь''A~k$o ]̹=,֯J྽24>|hx5R,O3pjWwŠ0).HL^B@ZjƘ^хU!c409MiIlF@Thhdk-pSj|xGw\,OUUhi4yND,r !}I1"KnBg, &LJ^-!7G%$ґ&QI =>X1g8yᬡ6qle/.mv IDAT"l}Ό,ٙ4ME4e4<9 |:a\dl+9Gk1nCQ(=?^z%oD'u>b:!EwRMO^"ͽ0F'1eE8d+Ŧ2<u!pJm LFN# R~\i%& B!e>\iv#lsh{sl2Rϡ0zI]q%5R$ɘdB1C GYh%q/£?@5^]~px|>g,-HGcjӐdc>>5,j;ᅢ=t*]I1Lβ^/PJ /a~?$x!#}]>gKvG˭[oއ|t6ƍ[@Jm}Cyp`EV"u '8"7? ۰HnTrC z #|@ EeZ]?ѬKx*m?YkյϝJN{\J3z```````````````̧>~gg5QtWj /@^)  j˯pof1mPm@W,ج'; zˏgӛdWuk=>|z;y[V}MpVt#[M` g}ZjU{vwFcaDtAd&O41Qr%cy<Z-$l. s b5ͷ:5cx܉Vk߾܅aa(aej\*11B@Ef)YQmJOOz`$|x!)jуeK(8@RS7Bl*0Oɟ_?gӿϛ?xf,{LSQ/O.B0_~tktg u2 % pM ,@HPBmyJqb}k qEt>Ɯ>i*>!dl9B(<$!yqڜ806xcٟY/p!*c5YpH,B* $#IelxCm7n@yzÓp.}?9}1hK_<<~LҤ`gwb,˘g\\\PȆ(Ӝ.}<ͪd;A(% 9yZw1ֱ6 ܺ}꯾k /@/J"'Q)Bx'e_DR“%H' A.y/<2I0HRHuA@T e( =URJ&e4ݠE! km6Ԑ?>by%O3h/mVWB R>d4!a]9.s$Gǐ['JZ+=zHl y1Jf|vwvΦ, ,Ezcoܸؼ~5 RJw<\?_ת\?'% kϫ;Iiߪ^׭aÅ@T`Y,."-e9vyN\?a6ZFݾy7pkZB;L16wߞS!\n+x4ƂR (SʲbW_ZKys7oa|돹}ߞ3Ň@kvf#gOhLMe!ݤn)?_BrI ]sIӔzʄlwatu3穧4\46//[]ؤ`2Og}H{~"zkj}E?Vo$}+8؈v9i7k:6)%$PWg$ehEHd"0 } e!8w{aw.LN?p{K8 Z3PEԎTyGE@^e\3:Uc si MS[1шٌfI1Ǐy)iJ^qrFIB[g󜍮#<U(+53} k`.g3d5%{xϟ><GScVZ ^{o+ŖO.y!yl<9;/PUZuC>_o=ʗ(1iOUopֶ B!MdgzvGF,꒽۷XOT̋v^bTJ`/82'*ctf$0 H[}ضlA]nh.>PA h3&y>a<\չƟvw2|66u=(ف6TJ݅3>;j&v~;DM a<4 b@w4u5₦i3N89چ.YPMSOe1 X) l'kj\iT8 DJ }ba^bѪQ4 wH )E 8x;Z%$yixZPB)[ʍC8' Ij 4nNkR3JSF$:C\\\ pwQ;qxx\<_2G>_8}߹Hs_f\!iBdhGGGhl$[^ud !:S2ߙ0e ;!tYl lPU%z__2pttk-Y\ :Om >xP-`&k+<ϾlcpO57B60S='IdKR`C&"oK\q?TR=uo:^y%5Я㦩^# aH>|&m{f(ŏt۷<=E7$bzuTF9 1;m9&]@>Koh,9y߷?oD@koku!vTئϢ:0zcϟp?u/vv`kORkj0.CmqTLwmh}EGwt7G:i yl>c Yld}4Mُ=Zkyp.~ /v{ZH m{Mord::CO !s19cj,q4IHdY?,]S}Q;^Dȫ~쪮j^elo,DrSf#gOع A֚4p\G>\WO|&~pVGk$pY{}\G A,oZ }T~l^ y%LI H qPoPJ!qB6 ٬Q;d1h3&:}6M# -lݐb d.]^OpN]x|tAtЅ]t;?5]ׇ11m|?nRT'RmmjBLSƐ9S..x)jlzKg k?C$hBQV(qXw.~!Xw'_Gz˿~uW;ob%LSdM2Jx-$ W%տ#0u@+E#-N)ֵ?|>GD'Irevy4[ ueW7>|&OjC?_ϺݳB< L+h >tiAb:2b ^;iPh9{])/B^iKbl4Oȳѕ.ןg70<ݚ>c?x_h(1޴y{`[`̗U='8|8(F u]cI1ܰ;9Er! ː1XGP2:Ń$׆p!uV۰@%%dyҫX3xrrsśdk o<`oor6 ʟr`2-8;;6Z9.qQơ,g%?|-% ϸX}Yptbic2St·?η=/2E>[Dl6& `G(1O_ӂ\~9&{m!xdھݾ| k@2fo7 e%cLdLk]EAZ"t hN3OۛQX5xBޔW#%$ H3fYlևIl@]0DT2Ns`<2LMßGz6[E1k(ql 4P7q;w@ bwYޥ$ER |X#=$Bb4Z _)u{{om(}~4QL&w- D m4G׹& !$)GhI)?>ajc[ '/Ͼ- jM?7Z&/N !.sbiZBb sK% mf pDc%>( !!C.Lv4_ IDAT~B.|>߶^!\mjo;v7?I^+ۯw 4#PZ#BLgcaSmXJsqqA11#9Y\2o.XzŃ1&zŲJ)*Zs)7of`҂4͹y6ǤYN3Ř$I888`<ߣ*kO+_ Mr_x6S,c6!QcK<2X/ $Y^p $F%)p||cg#q||啗HUmȲ_'Rz@"C A#l6%gO+%@h,A "u[GRd`vKR)!<4ͰQ}molo]Ap}w4zO}\zVY3Ǐssl,{U m\{|50Ok6 :t=uEQ HtF8oHr8`P{T!GTEwiHR|s__p>ۡjUao⪲ l˺SAiڪG2\R)RiJYkiWUM䪪x| T$:/#iKcJ뚽y<*oyQnjphZRU  zX Bd\f@ł Fh$ >+X-V$=ƣԜ,)#%+&]KE6QLckIc^-Hb>ۡLSu;[7 5$ {[Mcg r[Gqxc?|-Fb78OBrA Ie IQp:e6.wϊبKtDLPYʹ[)*H$ [(sq3,)0k!eELc6|+DFO}."vf$zk/D^!#%ZmåDruhaN.V$"&G3Hd2A ȲjHb$%Ȑ%q@1.+0MqMz XJHxL0M%AW!$QKa[-H". ^gⱵ.z m96dY,R3cXmښDvmRA$TP5:0~\l6H)BA8FiFU&…t ֭[gh[oKbl ZKB@KY˜:E%ɳ1ITΐ 5I+dJ \$%YZ` 5 MJC9@T#rB!H!AI0qAhu6xQ={mϧV~4U+ibڂ4*p.7>o|h fdo7k!~uಡ+o᫚ <2]LsL HՆ2G@PǮB m3Sva agղdAJٷŵރ,8S!j T^to,2M6#zD/nJ[tio/ԽjC^ĿEu<։քo|h&':E58 E@A Apwۮ =)c+hCos xCA[C ChG*7bPkk_4Ėb#i,~mawJ)ixk[v՘:YMP%MP9g#!'ُ,iz|[DZUL ͡8LҔ,2uO ɶ  $833>[թ%_DfU_4i4NV.e^_>/c?>W>| ؇{ܷN3Z9 RfX4 O<9(2\K·3\MΉBaeRUu]ӷnӢU9ΙP FM6 M$e3m{0皃Dku*bt] -@k=JiFfܬ쳱Z"љ)̲ʶ{߱ZH)KЙKwZR/EmvHgs{-Me.& }Hq"%O~)۷|j۷?<UU{r]9_|b>ZK] bl-!r~ >>*|Vmqf:^WMӰQ59GLXW1o@c JkLw>{B!S@b )fB.z)c ׇ!w'aj6J6(p'.a/mUw#U!UFiAe4J@%d28a*BiM@I)$ j.ٱR<+HʤL3QU ʐ!Ѝ1b6Ԏ]؞.:쵇XB8I=)~*K=o po _΂%#qKfu +Fp8 OzVTUEYo.%g֜_+'lWcŒ(zNԾF)ŢQ""7[\ ) ~]ZqkBW// MI1%CHBY/*?: E~K>'λ2TY q&V2.st9j| d4 bikRP*tszM9~K@DmGΑwe%D|iꆪs\RUi;R\]ho,LLLLLLLLLLLLLLLL|_·џ_ -; J[yMAQ:8KpɈʥez7=`;uJ %z[#9"8LJU1Њm+k+j?$@訪8m>4(RRxl6Eycy}ιy|ܸOk}0ۤ6(C;<9s:zkS;k˱NGe]ZfnYO9-劾x:*C)eb tݞvBN+~Cg$[TʴW_< .i|_Cj1ZYs7qr|#* WAW|>g\Wn;O7^x/7;?}3~<\Zps if͂'Oqy M?FISQ9Z[f}L,O_|MI%;{Z @UYZ)L`}߳kVfKp* Jeff-Ғ>~1y1kN׆f>g>c\sk&;Hkn~NCZ'&&&&&&&&&&&&&&&'|hY;;L"xI|ۿ*ʔkYY@r8?ɝ <^2;h&(:l5 jtYo؊!D\3k䎮c]7)FsހØB 0o ^a\2bGlowoW"o^ #K&3Ԯ&P_sz򀧏a]U̕'sHgxlv{RJf v;5gY̖N/bX 1|Ï1G۔u1_R927[p BJ۶ر-09afm;k)#@UT>"KJisXI)Cc0RFFuLCl`X e,4KP7-f{K;ಾI111111111111111};Fx=G-o:MB c3:TB[wK sh.|ϭTFjCQ%@ȃc@^sxF* FP, oCu׿ %pעn.1l}n;65AaqhQqh8תyTAyp>E2R%&p0ر (Т119+N7\\}mz5cÀ#]f59Gc"iS|aEno޲tqC^D%Hޑ@-.ʰs{k=Fe!]}Nn/Q8 (ʂD9{Dn˯/ל=|3g;^]b+SBiwjw7_2_-+K3[\zM~O=,szb+~_/57X,f5 øwnO۶Y,gK_aFA|1R=M`^ ^_IZjx?> Jp\J)2" h)2:@mz{nPȐT!l`(QF㱍]y0aX;OxkU\\)F&}ߗtUu1#xu >&Bm#-U4uJƺ WHBT uĸ9=9YbHC1 @a-k*6z14R_,Rʢa{Af\1v;1̗sW/!B;x|S4Kbe\qz~^`>!ȳr>+^x7_?/1pα"b]@cqP)xpg̛2o SJ2 빺G`f5Z[TDc`DPʐs]aqI"BXnEIFcFѠձa'H}}uZdMQT ڀhU<0ZI8N[y8 MJG EJ jBZFM:Nd!.g !aCJ)abL%2-E ڷl![c0s> 8m\c )QR%*LceQCP`ǣVceXZv ŵHʄezcd8:c83tAD J#SU裋m[<}ij'^.1qQzj-sgsssؖi[Xey5nCe؞1,#,68@8ybCbϢQ9}`p/+, آTahCԓ{r|1;yɒ|726'{-4|%ggls./yW |1_[}YuNvf<}ǏKۃ'l{cf9'%}_iZU £4MCh7٬,d߷eE)x xbɭ,Oq̠P6p>*^#vnn0J]. d7὚Iv_~{Cul67l[BJPYqsu]Ω}lzJ'&&&&&&&&&&&&&&|h1! -tVߺ8Ę8mjr [Pj D/D: AQ PjDYFui:5Tڙ$bNv{l]uQ:Fހ-a't-ɇz9֥Smsiʔ1ʇZc=h8r(8$%z8nV%cQJ;r6xJB1b91iI6HIh{vǼBSnϮAri_k#s{AѲ|oo ݆JS7C X=n"Xx$'*1Y4BNh\&G[E DYp2$E{t5+zk֏af;Dif {vڶ{޾}l`STJ|WO-}3ͮ\gruu΂s5o/j|(- r^nq!%lh9_?u8mpT Gܣ6Fk` 1 E'h*gbjRvb,gN,XmH5\kv$RWc IDAT3(}PäDb7Z{Xdu{Մ蹽%~w=MUDh=]raI:zOve߽|hAr5[]?s 3Cs7i;&grC+1=>H1poшyF)yhE̙<?̐d}^\)↦'qWy| +j}gn②ًCW/NaQ:I Rd2% rwF;D,N/{%"!%\f~DŽ&y0c-VzrK8 .O?)/|;T #h3Nn9y10 tͬ(]uĀʪ $FY!cjтX!Ƣ@J@gt%1hф6uQ_/ui$Վk>X q5nO{63W+fKfz o]b0캖szzNOOIga~z8G*'0ui,8=]RfsBR77v ,h7EY2[cQ\\_4.}d\b#RgM+S]& r_}uSq c 7Wuͼ^z :N tfo/"MLLLLLLLLLLLLLLL|·2 ۺc<=U86eXyġHQw(soAKw_˦w19a|eDY+HilZ[cc82(!(()Aő]⻞C[f־?qI)cDX}$DH >RUJ肧:NaP+$Uļ~O!E.riݞ'B'-B; vْ &{BڀR.cF˾; <4cи/!Vv* 1!þ1-QkfhD\c0ܲm}9~}9Z5(8??uJ=/_ϱT<~7'hb4\_0P)3, fCUUx\f9KNC>y{suETieBP4)er&-R.pc_rm33C*SJMN̪˛|s$%o.9-m3vQ Y(kg|9 xP;݀4!8MUB+)j1z\ZfW߹{asVt6~HCn3JȒȱ(՚H]B:sHdU$>e$e֖B-jP ˸cT ǰ.S3]ǹciզ:wx m# `=MX"h~sA8U!E B "=obޡGqrVs:sK6i"%C0) X;[ Y@J]&c41;LHk*HB އHH4EMfQ;o1,DJeXJf3K<|]?{O//1z\ci"2US,gs قf1?l՚ݶ-zTUE{>z)M%.׷HT밥"e5`6)t͆Dg7/}3ɢ}!fV=ͦh9\MTĘ",hm9=;GkMԡh3gVW}o$߳|ZV2v7W~3{}/^3;Rܠ+G],hE8nð1P6AipB8pt-dMi'j?﹗˟J;KzENefT㢵>;r\,[mmͭ:<ݿapl[{Oʶ{ﹹfË_'ݮKUUeؤ@NU-N4*{$ztERBiP$Lhv1F%DVz~0GKΞ9wn@[&g~{M軁U1J#C{:LN~>)tlȏW B rȹ4~rb"hE#a81T]=cδ-\ FC=*/Cռ$Ԯ7/)Z޼~z믿ɓg}k[՜OE0F1αȩMh0z87*N)PU{|X-xߡdg;V^.I\bs}C}_|Z}97o{ܿx񂫫+yX2\O!'aOh~S1j4F׳huP-!1Q1|Ion8rz9% wB߻)DߡA(kȒ$D!|K0*$4lAt 9`uQ9$ Ml(aUUZ! &[oC裢\3Vs~|z/h4zx3BM@ YWhUSCR\^ߔ(ت_~#fƇ~D7o.Os<ʹɧ?&HKC,,4tūǹ> -v)8YqS(RΈނR4USZ].UnHVse!J+\ t["USTз|syG}O"_}+)VӵjW?3@B>4ۖ/1V}ݕ_} _'~JNLLLLLLLLLLLLLLLn/o[c`O:vyv(O[ Գ:'&&&&&&&&&&&&&&&O|?6 <6KV0C{h&cHRBSҢ80ro)-CHԡ1˝`00 =4U;o&@vC1 hQsDhusJXRlfN@4}ߣ#Y!A!ɣ'%;M$@Ctij_ːa܆_s~}ʏ> U5GR, "!mdߗ7Ͽd{}>NunGN-VVl|)7{(8ZCia!I sv~E$m"Dk5ZJSYk@ݢFc9.wcopxk1 z4Lu]W_pr˗V+nn1RYK eł-JJ9ݵ{_[3)zξ߳ -!iljnXt ]>:<~l3#7 r F;W)am9vGHdmowx#ͬBkMr=|Lݱ^_\]B ; [\9o_];Fy?UG@fjA( A*OFYtCA-`5m5k)MԻc<c؍!baPR6vi*kI)uZҠ7l[-Aa$uYU )tb)C yܾ6KqoGД\zf)hEjb*ƭ/s.wD =] }K9O>s h1eҡ6wc+قm 1%0Tkw|ODdHBHhm7{ hRD&xA Ax[q+@~8.t} H"|H֪"/L}NCUUz!=h?UB&( Զ(/]EXjG_kk}ʏ}O\Mc9*{̨Q0&g-24HC2!LNC͝&r9J Ja.s䡙=sC|;{ )oࢆҶK-heP.aXUwXSk{$ DEžyUT"VeȞ=! 4EAQpcJ*6EȎEQq !*NOOQY:8Q,hYDH\\!&="m'ŀcW$ *tVꚔ5T<}f4˭2h,Lwꆐִ=֦EAAr( 9 "99}DLJrDхM=??Et}B9IdGdh}(XfkJb+Rߕ#yH;cϨҘNg^uU]aa\ׁf1b&HZMߦ~b""86*(mI& VA'v9S-K v>'gpJ"{|v)6Czĕ-9R;4 b1_S&ӳ5=yCjLc6=~Sabbbbbbbbbbbbbbb{w>剀$Ѓa$.])JÒcXý9DZG:*nK 0 }BT8g껚g)^蔰Q^ۋsY0d ipۡ>nnWe;SXWB܏4HOw|{v0Ø5xZ4^Ȟńh*E#buFuŃ3;,/Z &R9$l˙"WhSAi.FJXVDd,vf!1+iKJBq%vb4g )%J<%8,,0( qh4'@e)ERᜅC1; X/_}qn7|UsY$Wm>-.]sx$FPEw} njq.$rǁ}V{7 `Tʠ.ܘmk* 1!)CG.CpRF(IG[BqاT^PzO2m!d~2j6$ 2HKhUġŜ6ùs-$(|KARa?QCi[Bc]MJaPP1m2Pdxqo[~O޶(Wԗ82)eReaHwvJvhV'`+|M)[l hB!%b$h늶# Z6()9K4!F1z'gw;bl6͎ahr]80 jLWh@ N[svP!UYHdB(>o/nx!)?YeYz|k=!"rΪ=l[4P)dAdC 'ޗ  BH%&lMv*3c:kQلnl  qŻ|SG,Ge.//D̉n9sttDƟٟwޡW>Z45sd7 ԗ'Ԍ`;A[ mzS ̙}_ f+ˣc]qݨ\yva` HˈS)}[Gd2Ef-ı[UcԶBN=u]ӯΟ?a~%#cs8|p?ˑXS7j ٬ l}kMusTgД u*8)NqeCwH)h ߫\>!] ϸ41{$|ee6x5jHcX,)q0KT#ylOYwi0@z`&jz^jh+lVk|K;gmh&XI*1gG%aR IDATI`YyK\3\TJWW. iN3) ~ELvJPg<}!gϞCF]:N8Z3Uy3_Rf{N+6SF*Qû zDcK//s]f9g+>|Uu۷3YOf8;i988tlbbbbbbbbbbbbbbbg>AھÏ!j%5Cۺv,_J#r_׃C#_(>jiH EJaRQ.ry8>jaLJAM[˳OG+_&bޑ4!כo9lg~BHP\iP׾7"@ժJ hBSE":y%6lJҽYDahb!`)TsKX/(OAc\K VA^Im8p8}_.\'TmPZwʗS@<-;zP9gNnq"$UthTkLᜧmg:ÂM];o(U"|1ْWWϕ9ѯJQLjVqCk6 ms_#jl&8)MFڅVCy6uDsRfF68 p{ƶ{n0-pHG'$gx3kgrm7l{&=3[<{7˰:y}΢Ċ3Y!}.[qb4vS UW jp.eeE]P<҆N9sƲawV g8qK8} T8%#O.I}wZlh}gPC;(0rѦ@nJTփgT$8)K<+i''P/GMTeqm;G]אf2w[bס>뚦mqU;g6[8go\Q9ݻwRU8HN@hBC~aOɱn3*f.zxD}GϸGqjZ\Qpd&xB zǨ2ԱlVXk\mQaj(GUs]Oڌxi|mF0zv N\ -U1KibrIUU.J[?ܶ]nV#E=j1%zK'.[s݆ܗfǬr!Qx)D?Fe<9l] 5}A^O76'6xfsvă]9h폛lP LsD4uVR9!x wl[gėv[ӥL# FKn&TD+BqRUvFn*ԑrt mqd!t5iBsb;ئ[mq/׶s#TH1ux %ќsx9"D+ 7zxvzέ7hf-fGjvw:?hyKEl؏ꒄYǬiyhM70 LIK[B,96[ׇZQb8o{p*?r}Dij )baܻlJ=lVF=}öef]|FKNUuqdׁuJȍ2$g˸-AsiqjK...ϸ[h1 #09 %gcdhєp *F2Ox)B퓛y|fmSfuqAV|Q[<~f9ZsVΛgܹy, DIC0Qg#'&bW>cas^6111111111111111yF; Eeh6(7eJX,"(WҮ:GyhЖ+[s%(7B=d"R5S6 f' T3:}>J=_g!{HQp.+ڶkM]K5-~KU{m9G9zQYɺXTA7Tf$au#닯)U9^qBNjz~Gvlj36]rMĻlOsa\橪Byk֫g\|ᇼxWI&'#r.ws4&|| 8!^_s@EÅ+>'>auA u] !AE1:ˠ:g vk#UUSvᣳkы("tw5> pa> }J߭Ο~eܺ@ .⠣`à265m4u9NF lˈI rUPjW ʉ=j D/s>H!mic~*.ۓrF%kB7,$4* bN`Жw@=O\{GG,+f3?Dk 9Fj_ $C\ίs.+&fy D= TBP*Sfl;/|Zmc Y1q )Le"#27Ox1q~[G7iLJ I=?k{|+nYfR*˰I+5͌NCc±?'c1 lP:;Y@v,36 zIO"> \:.uM׀n{fEgT3oqzqʏ~>UhO~W)O^J8zɍvyx#>^~v,q+׽*CQ O_pA?I񢁭>a4FJw=# !‡jZE!MFcp{l2l6͖wOH"52uãxLJ{-҇?~}80EѬS,!F,r+ w::S)h0!kI&嵟in~klSƷ-(qC{w=%T=_fxS-[ּӖ竜CBzM=?!e߸[_"7.՚Gl6CG3RRG5R{4uRs?{x>r.8?;p4Y.XRLAܴl8683;-e%,xWqYh0|.-{=1$U>AQ $+y7P T8nΖ{^nkV<}ʫʭ[ER$u6oHmGܺsmnC<`v /ι:9{'9oA'&&&&&&&&&&&&&&&>o|h3+[؅D@Nc+Ӷm^٠pWCƫPÆo@~Kcz>]S#9˿̏e)-wxE͑|u܁֗xGhp&kq0?lgS'e\J=f~A3)o13.i H._R[iktJ Y%LREci+o=ID앪o7Ooa[P^ 0  =B"3]Vz ( k'=(v0هc(ˊ]@hj=:L{y_s6D!$ 4b$X51tM[`2@y%9F箆UȻТEJ T]vVL#Y<#R6Ŝ kN?~č%CvAE1n[UK ^H1%EL1Z|~ gKRœYIlx@17h;jIfBe0x-@25/}8*6 bd+l>_ngO?a6}i"9 òҭ7ł34wšēDPLJ3.8?=# K (dYOJ5܃MxBJD4ԁFQC_)m`*A]Wd5ꪢc&8vLj,@2%i}s* /oxV+JSzIȭKfwo\[MV=%pqybqDlqL3:p ~n|>'(r\W[{>\?)M|}ȡ.PkctJc Rn஄=cH]p0<\Z{ϫLvW o>3+_ڗa|4!ZY T1FO4f6u{Wlx<*W7\ѓb5nmWȕw(mޔ;拻N\>S%i[ 00CNi7,/#H9'aeŞ[˗Ӝo3HVX?~ޣ->BH`**$qOdy,p)^m^} vF Ϟ!"AC~b޿ϼmx [;8Y,I >7o57os1Pq|tv& #w*xvuR!\<< q$0Oi&t7+_;A+bFe5.-Q#xO-S"KظRT,? ̫ZaU3"~ќ>xmG4 UQ n9*OUQX 飯jY瀪?U~ %y]WۻECk?P޿^ǀuO!c~}k_R^3a>3US8P30ˑUǝ;wXЫAEƸ~X^;/pU16Cw@]-T!hǢG?$WPk7pޠiÝ{yk|-}!H9jHN=5qnКp:{SNe@;6HF4mM@ho%x BUO<JYeUs\K }VҦR!fض\f22,`Ƭ \m88wΠR̘@+y*`(Y!ae1#+Nr oU%ĨŨ7J_)s%~%C+$04yG k[T< ͬ%i>rmOJCu`0B;r9xb\l9ݟ4|}qBqWcS jyW; Qaׇ1:à0PO\ v;CH%šC @+ٰ)g3V'"|E'$_51&D"sp:6t΅q=ԫ@i0Dŋr3zh9 [i۠ƶ8P=1zw>"RnߌՁ*d|1{b#:3B%DTlzS\,a&*6 #l!Uz]lĜ8_ĸ;w3l^{rrai#`ݐ.6<~JmrM% 煷 ^yufΣ'sNn#+DSU4KxQѣmj %U!TC5[/mQUsXoHp$*MNO-KW_7EXMC#;>?/~􈋋vLTij햓gOyG9\.ygxwY O?✓7r$۔sDtX?9ߦDc+M$|̊0" )U WDոL$FJ9GƓH6:O\8$'1#G379~W8uX8W.N?C%_X<]Şjֲ=Nn 58+Ҫ]SWȸPCk^yUWCT'g|o#mݝkU 1@C~n2ԐO??zJSW/Kcz脾B7(4er-躾(,mDox .ԄMGcbvVcbp~[w^c~;3DXCq_J}u_u%kp%v.L61u IDATρHfu;r*%;^=m)aK-m;Pi+\~E-<]|>,!-@l7e"e[")4%θqX$g)vY6 \9Ov@4M נGRgp# eGjry{ԎE=}^Uk4}i{ALMH)!& 7PeL` k*LΑJmpj53<'YP 9gNؚ-tsqN/!3y޳JKŷɋ98a8}_s'6ÇܹsolV4UѬy|>m[rJ/BpAcBϛm%xEas8|"cbbbbbbbbbbbbbbbg>:pwߒ=MJpz/ XK^-];χM s'7@FapL?RGJs5J'. GsWfxZ=>;%e,e\S˥go8kiVmG E=BFsƒQ8K_sTZnw3r+7̶8ŻGf-O?_Y,ԡyw| sr6GӶsB gsx+K3Dsc<OY0O'K[grFOLLLLLLLLLLLLLL|̇c\d]fJiH M݊·A"n #9A`j]9V?0rnha:3PDo^r|?d>fb6bgWB@qJR&KF)aa0,) ;..G 6Ps 0 _596)*KH{"*@xn̈8*lؾ֣Qa8`+Ae:jRlc֡} ) ɵ_x1G5cr^s|;o8&.ּtc< "%;f9v*>'j860_sm C}pN  Aql;3.鏙k,+g03nS:q$`M16[,'9','` %Vu*23yn1c>B`}x1}{7%L]yRO&*:;n|Y[UŽ0$zSKDul u q<Q@\Λf: ٰlX* =,xrpаDZԈF*0LhB(SĔhJք e6GdΣpw8W%q<<.hd#3q)'k?k84X@x %L rNw>_%?/s?8bYᝲ/gښrNhs2Lҍ KxQf|5d|}Y䉩̣ǡ)F-!ݠ)Avubbbbbbbbbbbbbbbw>a3zu>{PɣП}MV޷GMay2ۡ倫v3>?1 !*c AmGG̙P /pֳ]_Cqܿy¯/}dh1(TāAɞ]AXQƬJtpWX/q m ԁ[H\ӌH.\9ccWԙ'% ̍[',3跬?bCN5!xA#9073*JQ D_T9<{WSES" Cs䨻wugmFcJ4Gb/ΛJwqJܬज़'mB;+AT&fLPaܼe9C,ZI\ex~@{IH1ȱK*:ybRLjrxAԎX.<"焙b9⭴gJRC3jt1LjC(+y@m̸_Eo:gC=o6w믿^ZY J췈 0$B =K.5#8b0coPlɜ '`45'@W&8ъ箟q݌xɨsO=r2W8%-: J#VɌ(BJUU+4sGTA=_y[oW dqVҴ)ܞ-9i[?W^_x;@RL1 uP͑$y-\Z{w^g,6l0  xƣ)dK[,gyDCiy3##v>#Du;eIpB.\p)lNl./x]׍|2x"L5U}1mE ap!x26 r1Lr@EW)n)"bwcǚ:4J!p΃~d@Hb]14i=ᩛ"{t {l N" іE &ٗ\.x[Ȓ! US$䁂e9Snm"zC¨ɣ{(%k,h5(OxMB||RJhes\cspާ#A) r[adJ'>΋JP =sWRC"o~t>vs<$!R$ C`{D"Ղ=SŠՑĀׂWB5[ E=(!.Bf "Ƞ` y hQP1bEcG)GR)LVZL>?וkGJIHDA!*APhE-!dGӿ ´`BiĈڞ5tNhMK/ȫeIQh=: >RU3>}޽rrxQ!ཧ,>zcLE蜏}vHoC {]*n_0 λqMy*kD -O.p)CS8PudddddddddddddddƗ>~"< rHm6޶-!;vr:sn_ldoͨp2IYnTi()͙^QXkW$͒mcu`>G+{0p "#UȍQ;xUmQ(Q! #)RCC{9Z5D %7pg'&U14Fqv.&(]C[kC"}臖Gk͐>*( "i>o=iwXkI K C|ocY٬b!kMYg5ҬV(:lV(b >+}O=hChrS]r !^4;/k [mNm!Ni`88"LT tcLqEԙ!ڐN7BDHJ3c!<YS)SjůUUV5jrn{ yRfw4%mHl:~]q\9#7PXC}̗C\bC GFFFFFFFFFFFFFF|A/2 zr&bx^Ex-bkzیn@矵hn>Bsdk0 [HKڱ7u8؛R}h1  ^UCG<=>^(ytrYQW-{BI>s`-G:+S ǴMt:,K 킮pDEQaP4MCY ;gAB@Bu-|H.tH@}GD)CJ)ެ(P%< tu!"f>b>3k4(Œb[m]fY=:'6{3V}_t)ˍ"FRuEmC8tja"R[i˵h),) c)JA.ZFIFf( #F @iKP.zT] - -u6Zk~?|p/̯̓BuuLAm ,V+&唾O$eQ>S$i}yu{$D s"ߺJ}@X"iXjPJEI[J3תFFFFFFFFFFFFFFFFsKFoÚ_7c9XŶ]p̠RēuasU_ɕh1 "D-ҭgMO1k0b"QyAF(c1Z|Ķ4PDlZ.pՌMHGiE uCtJ߳PZ}$ Vw48=߰Zm( nܼB!"\r}^|E& >OY)(BKbkPVW'v86[& %[wj4 %[$ϘkB5NCYC"B'jF PT3WV9?}f6{{hMR͚( 'ۖ=Ib-`vo;Hf35 h!zub+EJɶ;E=[`Ώ:Lf4Q1׎`,Jan #opDf%B- $=t0yACR.EP1zޕ#"XǹT儿|=.XlV+I]c "TrCUMI!q\Ϩ&&l8=& w;z>pXraM PhP(mAaYE_|.ӱ=22222222222222UKF1ݲ xMhE.{U^'4Cs'mgمH*gRV,.gYU!rCSyB>\BDQhG)a\] !knPxHnf6xa?_k7\=g}xꅧB(m>$ =lI@e%GRIAfpH'UbbbϔSrTQ(cBP=BՉ.}<2)(!8'i5K4`3xdo):tUPSN  .o /(y E@4X$!!%5b'?{>pUܸqv |~YU-}J(4Y 8Y ]3bX1!"г\y03;B{1fwO*{bF|҇d#";sFH6 rٜ(\< ?9n_Dxk 9u v6`TV3iA"7 K!"FhgI!~?zCdmhIM|24EbjӲ?ۧų\7]au( }icci0d]ĔoX5XATVPزfE 5XkV!78Y IYa ū<ˆ+i ye!VY1VP*3ca&S0l։(>7C'VgrH_KPZuD /=hЦbj2Cs}#f{{ptJ;uō+G4| ]qj;?nrxw}W]E]'*%-E7 B•8CN?lք}i瞾|b˅hddddddddddddddd—> qmԈ`&аœ^\O%Sj@;RJ١k o1c,Q r*p:}4 DqV /oHAh+R{}˯cyx$$MKG@qp嘄i{fa6(hzج{AH{f)]`mMRP5Z HˈO ʧGeY2=4>aZW,M$e7lR)EuYi.PjmKe501(oH 1&J>#mߓŎm~_ztB` gVOH 5$4! (j8^oOqΐ&!$T HD= Tȃ#S5?P^g,Er{AA"_K;p"^1Qg ʐ45O1ٛsx)0 w6 7&yɂ%eˋ/'w0st|?/ssqZ$aCb8y[xx7988`:G}BWmS&%^o() Q }c["ϹeYR<<"ICeY]DCڣicdddddddddddddddƗ>QcI)ڂ"9rֲm';Y1}(BE^)A )ڡaӧeosp- b}T$T6`Th^R(iI mC @aXo6X"J@ӆ@.IWW#9\m{?>>g{R$z|A#}L45 a\,rt6!-]1BTC8l4ﯙ}k_Y1( Ƣ{$y@ZDath)pͺ( ք퀿_alr+9 >mGDRfC1BS/4(I pL^Wd2eh쓏я~Ȧi,ܾy*QpA&)E-6q o[ol$[`z$Ebi5&&9nV}O ~ ׇSDF6}ƐPPxƀsiS8BϘQ%{G QJP%?Xs3'o+7O~cfo7PQ!) Kw$%B|={7oLYgopSMz*U P%!BHtǹ:K&u7Z߳Zvt9?8>4}Y9Ɛ|^bC>_2j6*T9%]*Ai,5kh??ehح\bEh<8RzMEt(rZe^Mm݀¾σ 1DdoVCIsxta V,hKZZ NѤ(<׈Q06+R|rJb?%ܿ+NOOyxAP8X#I$QU5auC-yƵ+ߢ kf+|A9*E&bO~OcJ되EQwyG?)~kOK/Ywc~7Ž_⅛CZ=ޛQ)M詊IO?/Y *Y٬ K 9V͆]$q|||Rמ9C{r!%8yh>U;;6 MP4}|}Tu91sd smtd煾O\j 0 5( (c4F M{BۢP8#!D z~ Ù!TcUA=ʒ.(% .A0TX&D}MftFk6DjC?ΝO? BT|&)!$us8 DIx :uI=X(gY74]Yaåƻ- O|J)TUEO\)-KC14Kܸ,V7֠00JP60QWFW(hkCj(Tڲ^9;?x -Vhpᩧn⌢{.!PClG6L|_1| qJO蛖 Os!g!G]bp$IʠaQ$*+B"d1{W?p7;Ʉ&I]5~$$!H|K"?ˣ8CBD$bS,K^=ggg c z"&O1T8sᗯ*+X!<R/R81(v!tJ鉡EQgSb }Ƹ"z`822222222222222UKFxR110BQhABg,rR~=1Xc}zEnckc?@'-1F#HQEAutME2fC=b:E$e4"=@+bJEvQ g-ԣiYwu4'MHCޯZsS{Ǖ09(!%N7L]Ф sjb ,M۱Zj>IEuC''1Ź n<B҄ȟs=J]8UHr#ZDRkd:&<_SLֈhRT=%9b4Zi(c(U,9\nlViLܰW| -ggsҊ(LfyYw9u{<ܳ| s}^`uZ3MJc:ڳշjDTȱ54V_c !zhkv\ْf 1~o|bI`07gVyWHy5|YHԉbo'J{gp]{9|ȵ+y 8<=u]Cs8=}L KV+{ȵ[81•EUc>Gr~vJnPZe?葑 _0l>+.y6hܶ/"="(uqDJC }ʀh:AZaup6YYJ#S@E:۴XkLfX[&ܒ*r4y 'Zl놇nR*ʘ=Z F[zQV&?#4@]N">z(@Ω^{:qu8{+SKB@ph]D17]z:AB2;(1Wf0Ρ$0PֻY=N4 b:[[L's\]RMj"1%|恇@q׮BYCk8J .'GP6;˃2ZKaJ)"NNxOQW1NIhT3>CfӒ^|gGgserĹ%j2 eC)"JfP_آb>/iA{wk|U҇.XVʓ0F X=Bdsg}YalZY͡.KZX5V$gR \"@EH{uܬ^./M "Hyh]QPO' * Y:鄮*ꚺ1C] ]GQTGO gKq 8ka|@@_ňqu{HL }{{Ah?ʤk,뚪ʋDrsa;0ЭY#'#yE|wQ( g#ߓ_V쌌|҇"BLy +a6$۶/Ƙ](dAhL_tJG!5hRs<ߣ4P:N#v-Ai SyYw(rsrۆjUl.}nJ )w uYP9V̪{QTvêmmC9qd k{Ơ|~ @,Jkbis`{·$xk/Ҷxs\0a-~[?wr^(!/$ԮyV:L*,zlRYGTzA808p}~,d8I(,+c2 \mڶE[u.9G ռZBL)r8ۣi; q|](+_}D@ 4a ,C P:֐4 i6횲,ٿzWbn6VY* B"]k>3#>\a^{^}vvw¥ˍ!Jӫujvcu>(KJ(Mg?f05, RkjbZ 㟲X^j=m7|;s~[7q=&MYtk=9_F]Lg{EvxKRe=׬jHBq気BJՄY-ax|2}ldddddddddddddddė?fZM/zZE$/NA6Lihvd]:q00SgP1$R䖮DBڂJ| UcQ(R 8 ޳^-s3f-1%aDtvhflu<і!%1RveAJml]>>ŚtRPVGXk)|J(z8NaE Mdtkhba:)蚆7nphηxӇb 1 ͆zRrttbm{AE^hKBR:%G!nkX|: &ӊRNj[t3&Y6ͺ˺[#ZcCO߶HO%7ccXPڊ!YPLtoQ~/~11t3 DRBL')CDC Gis@v=촞PXlqդmqF#ʠ!M]YAD\Yz4BѬIf>?$5__W_m}sR s||ȭG\=tVcjNkQyq(rPY˦kM0(F*P Osw}k9h?!d=4y QfG:@6 #Px|_fٴ:>d61bB#SXmc t/g+ܹÇ_!ׯ?3hձY#ϋ(r|:ٜP5&%5Lws(bΆBO|G^Tz1w:65###############_=a֚rKp Y;8v3 "(4-))h F(, GJ59{J*M*sX% (("P0}ߣA"PEìpvv͛7qQ5<\{1I)tm+S%M}$Vy>CDHYY዇{ZXrA!@QX>#]+!}۷x뭟uA(ehjpFQlH1(@!Faj(!knYTc"xIh2P5s%<ڸy"[3RaPޛ=[vy7igy& "-Q@Xh9خ(E.RTJ+7[CE2hF$A@w>}=rn%S}{k=y9e8kY bzϽRkx Np||½;3Iys&1$1L4BL@gdYm:tOԄN>ꀐプ 4yO9ܿB93k|U.FhZiso5 I ɨW+fz >i.ih|w|E &('"o6Z+ڶ<F.r>/"F97_Ziq&\F&=ʲ˜<-*ň0aCCNE5FlԻo؜:JN!t<$ x5)%J] !vIYlR!!F AâdƂQ!4_42ĻHQS  I=JI rqXbL ezFMh:['zufyA&ŒdC HS7dm@HBeA#K\T#2\j0bꖻp~rGnm29cq\0X^\v dfEEe cn]0hԆ֦sZi@<#/(a&%^ n[t!>Ϣ4HpC{)"">1c6nhK9dYm"&PJY5"4K64meYdǍsfeEQ9{۷?o5>"' cz=bP&G \@(t"BT1DBjEkfkgxcO>J䣏/ Fd \kgjPjMi~Gܭ/8| !;PY k"7~7X͈!gCUc?'g|RgO׆"9_-"PgW~rBl)Q:5:H69x39GٟRRAsl!4Χԩ_N0A Kl繠 -7]!4n3V Cp)2Ie`it,.ΐc!yt!%R\~t웶)|Rux$u6\hPB kkТ4h+pR^$"]ېR Q$Rv6f!P  e%@A)dZcF :Jh!UmB  %kAf/D*E u{Ne*LJB G݂(sr IXqvv*ܸy^|~j>˧z)Al2TJTJHBm b)/7i|h`m[a㰥.KHVӇI󡒮ùSMe685DOk-REL*Z&C b 4m!D A}u,&`Z!`>#6$g,BLJ DٖnxMNOO g8Ʉp}@jB34ƽ0 c OO;G mlGYXq~~t:e<$́Nxn|N9(<,Sc[謤 Ό28M0Fe[G<T""FwtC,eY" TuVä``\]dJ=1"Cu+OQQr`koDS)n˦e^-mh5g\r{o;Q شj'D>o1;>.k~YV[ǿF5ʸX-)-=8:6Mήi: -+,c6Ϧܻ޽Kt]7㟾 ?Axrb!pH$yY|bV#[\/! x'?\?ʏ i̫1_|3+s8癕#(GyvFmBbR(N\~(6D%/{HLo~yH nX`ے܁Y<&Nj;CWӵL{6>.K0p``````````````߉06C(.M0y%J |0Yj.Ch#[m~j {AЮUUN4u;Ve4^ڤP &L'c5?!sD? /KbXP*u[rT1CͪzU9k[d:%DR4xY1!$5 Zږ3O G?<zx4E*uSdՈjd2buj\:eo:cݶT˰.tv:֡3MtD"ZK`d#mȍHbץ}VL`n25-$u]3hۚU:ʲUU6, L"9Ĥq0Yg/޿Gck)$b@gJgIuuD1 b@!f5OB㘜mkoK/qH+Bf{,VK{yC ͦ,WgL΀ PhZ\+>~_zTQryݷ%¾|lanɂ"Ǵ>f2*15qo\Bk;_7= 3&{D,HP`/G|#Zet68Gm-<"Z-Vѱ>úA;#Q b45h_?ɗe 񘋋5?Ъ?z)!R[?iR>Y|;i .r6mDDD(bV0-!t!R7^6MCHd笚 U rѶ-m"D6c(IшSRTUw^Ye4;0BL4:y6. \S,|jMhDӴD1M"I=g[FSJOS{m]FckZkYk)28::"gBl۞9 xcN.Zv&3Z"޽ :&0+ vMQhkJ(|t !SYf07MA`f&iK,h鈺Yg]+jEUVIb}ǩIk7n>FQdBۆcxpr!iޱP+s6w~n>&1:yy)f o3Sd?UyI]{{wso**I6*g_"XV/qtZ*bv= *gXk~j1͞7v`Yo֧&pyw;~k7 RQ_Kf```````````````>6y8& bga=#}&W`a=(D.R֒mVt]diRa~JBv谮e2ҬkRYuӒG@t2gh-$Fں\+=iKweZQ傦hv-RRT%zcئ]RX:è"5#Պ88h2ȪnX65G7'+&ƨzDꂢ3Mj]K/RHDX-ZHޫ֚st %2{SUL9c4*1bbҩ;p^0ҋwt7-tMs*7t"E(s`' GJX@ b @r( /ox_|}ez9n~':ӟ(-qӟL?TgV:CT%0yFD8h6[=sSr{Fq]G=i5ϿWbR'otmC,H-A,B(bHC(mPʠUvn &fxU}|a/ p'nl8000000000000000A?oj!nB. 2 S2Ɉ:pD<'bCEAY(1AxO@Y(%?X*6;b2aLLdY1:+(1yB`?V* |Ugse|t-$yjޮV+^uFU^q<-nK k^{5}yT&np+奣 )%3O>9ȕ+W mq10MMnm>T:3wx>F\t%l>+˲א7w5 !l5rwl^SټxG HH IDAT$oFK!6@iCdl&"];5twKnUc]c M dƨQȘ128RᅇKar& r:\({DRR%J)"&8O[#Y.nZ gtm˺n1yh:>8Y>:lBKE)$9K!|NzgU4, FH :r |) 7\) eX40yFV@W7hhZ*g{frJmN@kT,.p=bI!$yQ8?mS d2Aj VGw4B ?'YS2-"LSCkI]RdYWHMS@*iE@ BƀBD! "FACһ!CB")_!Z!%igt 2^7vbt\;O!uu!Ȳ\ |_ LCc?Hnqcl |7~e}xU )Fr?t0S*d"Gp<) %!ӒYs~r "l5RJf9Zk5r!\HJ(.l[֦ۗ|mdPQ*, ]Yl^-hkBbX/h*ѝ4y=p@5px*X5X s||LA}qqBۮ,s2! seʂn.DiP]1888@gj4"J8==/y@>1;wcڵk4Mcjuf3), n}'\A)Ce:5HޥVt orū rӪ}ԽlJbHo1L1Y\4\|]n1{qRޝx !k| , IrvS)PR#3/dvhw'gyo/Jk5" ,[%<i}qyѪ{mc)y=000000000000000IcߌNNրso6*BxZ# D;2 ,">Juvxk^1kk|gb@ &,K}Ktivn +/.N(%0" k=ˮeu@b#r.w{'x.Dڶ"<f{{{t=.`ޫ۵ IjvMt:8:c>w֢3ŭ[hۆO4|/ _4//U&cm,#} k7_g<8;E (G:k/:szn^>]b6ZrcȴA|jz(VJuѭI-t֢Lݼ ]#ԢͻlzMȮvvq;u~v끁Oftet?0YHRiWtTdRd""Eۏqѧ8ར(&s[A`;RNHջ#D IN<:=[FE-; ޡI[r~vBQTT >W+ =de40>k-YK׶("YFe Ec`O'~=4uMۥz=ƒ,Y_,9OVkj2"%Ӑŋ n_c m[Z#h`:)R'RRv4EYVv,K %ʘm{/ka^X.Y.[C/5RJʲ)H)麎d#r+ܪR8y' x(v!C_9/vl;~y:?>PW 𳟿9zg?6Wa$n3lyA=ܽ˲^cmK]h%xoģq#()Q"| y* tMK4۠8;.g|ҝwυD@g HPz{ǼYr=qvܕ!D| ׫}TY,uQ_') Ra0iaZP,NdL!юj)*AFirQ7i ]Vike2)x bK8LAVƠ"/KFzLg-11ޑ)o[Bp Zh=9JU<89CHS/00O1Z Mt?FKx:mZT:~~u]Sʲ4nJl_#%ֶE{wx9;;{p-3Q, 3O{RdEڶwxd\?~65fe12(\gxYQ)1`Qx %$Y)$UUq=ƓɬKڶZۀ\x,Y-WhQ%9DE4 Ԓ%:@hɧ{EkoWnCj!kZ*s؄iY$b]1W57>"_d^QY">l__/|<7)2ߧ )_|sKr 1rPB2Ft]GUi !\ 80JqG#Ls HPV65J۶I^.ufy6}?8茛7wa69.xbyCfsxu͕+W8_.ϦHV\J^rɭYf}|ԌL;҆9tf QK|W4L2$˨t[-ܪN!R'...(r;P^a2w)3rDQ8??5>\kIH2ȓf$gH+b]ȲLK Gff\=E$y3ӇxEbAmK|*_oy^k899SzK}/Ӻm")qC`RVUop}|hq% eFa$w8t?Ԋ]R?tNYu_zMaP5!D=hE>7I%-DO+ )S[>41h-{FyNr$'TUt>Y\,V(0YZLh5~1YR(ڥBPefym3tfPBrt||{G(vMA4Yӽ9Ũ"AXFSgt!Ei"F*id2bqFZszqέ[X_A(Vy W+Pw1ɈJ5>uhMmʫՊ,Kno{@1cۂNejj=䓎 ؜-t@@ɀDEHkv:¶\2DbpEYQnC%vuRUUDΣY1JmXQB)"mS,1Xp5]Pd:X &/eE4mV[Ьk$ZKWe+ Ҡ7R sp5I~p"/ߧ3NNN8<ʭ#2y]9%e*w!ϓG8+#7yp| Z۰h[XTE к@D_M4y1!=AI#F9]R ʦ1A8Ƴ)wݥNx鑛D7#H]׸F,VKF1eYbNmޥֳq.T ܿ,Quc2]:eRGL&SڶeyOQ ZeMMӀVh#Xpe^x'%os. )JهI  ?JbLxWz'0?3执nOp4L=C@ksє/"[$H,//+)lQ ih#t]hM{2n1H1!0¢(^mu"B#BDL~ބЛ]\6j͝ctE-Ȳl;iu۶f_mM0{:000000000000000>ޓ9wDۡrŨ*%F;ʼHzB XB*3D,KM8 <ԢRy |Yh] V,{qQThc6ت.fIp,u.5K_\>SI_ެWtz)Z{jW:Uv>`a-PmqF538 5k)w⍈̵ }(ⷕkAmN>fk-#n'b- -7k]uv!XV(@5.DXw]K t1ΰ>"8-5B9G' -l4rQ0;op`֢p8lp,WdYׯ}=F'R"#8 VKtN ݎ,1L-rYv=lK/. 7~yKzޔ=Wv9OF=1ŢBHv#1H~~??{f[JbNƽ=A!$>)ɹ9]a "]ץM@[xs]78wJ?suyj]˻bTY1gΜ9s̙3gΜ9svG ϋeh]C߶,e@$~@H jQQ%^)Hjl@ t~7FdRH}҃{'B[ۂH__W i1 {V ۳X!xUj>mǻ{N`,K2M:ڙUUY88ѵ=cSuy{wv嚢(cp8u]CjnzflZȲ,A^h)o2XKe yYus,:GuyP/.s2c1 sC^!|kիW<yY*3 WԐ{4lpQpٞФMNk- ^䪪Rh^zsk{ņDנL˗w>`ɧs pUqsv/T Rxۣ7{^'>w~_#J%wRFi"%Pʐ`dh>$3@U1l"aXt5u fQ*0p;L ENuZP=t(Q3OBRjc;z\ `eEQN C L|\)A#q\03z~Hv=Xy:gΜ9s̙3gΜ9s̙g0Gr2ʀ=?z|(R. L *xȊԠZH0*F@a b$:G칸Lзuk%ѻԔUV#Hp)gl[|p9_{?3t$5 (% !"JQ! }cWc)%18ेOTldi(jh> mZ>`m>PzM~pN-] b7"ӞRFBpk;2S{Z;804Scz! S_!N <–l()q'kbƜ9s̙3gΜ9s̙3gη)xmPѢDDƀ .i0.ZE)p40Ԕhɦ˲m[vyrmVGԔ sDVclۖc)jdZ[7 0u;0`B;!hN<9s̙3gΜ9s̙3gΜoK0zir%!ж=H 2Bhۧa .?1BS{q_v hL*IYB Q2Lb#!Mhٰjd8lhc^$K Nf1s>qI)Ӏ;s5'5&sRH<ܠ !LmT}!utEivz̜ :s̙3gΜ9s̙3gΜoc0}=@ b0Z{tkbHPں E4uɳ9%@X2)"FH&;o>6A+bk}LhF\zQ, LaqLbk;;$ v;>1,7k䋷(\^ ٷ&pH q8G=ELz)%W״}:MӐe9R) ׯ^ry8psuuEk8xۧ=MI`ʂ㡞gQ\ju+:SDQ$ qj 0wǤvJ^"ah.7)"wes8l.i#s7+ 53YjXh= =3=Ȱ0m'GE>p8 :@;]z4h2jIߦag-4Z*VBzMϟkX@gBy}Μ9s̙3gΜ9s̙3ەo<-P1:O۶ͩ=&iz[2=<,ʊdvaUfERym&p|lHc2-|yrGF\.yxx G TS]˗o+v{(hmɊm]'[B߱^-Q,"֯$2lyQrC}Hq%2ȓ7䈶eeYr[>xCKmYQ zǺn:6 yqjjn..{0.Nϡ{sfPuG*_ҴBLQ8ďb ߹ q y7AjQ|)( )5ޟ   )ɀ "9Wczjy?Ozp Y7S9F?}:1 YSǤ(jyJG$0cN*27̙3gΜ9s̙3gΜ9߾|at>+IG(Ej.8<6rֈ&Bu0]ZOH}M#'GseD<1e1';ixIyjkv1l-I13Z!}U8 NՒWxய:cɋ4лbA.ac&4 dT%1 꺦*=t]R(21|qK{ֱ.2yN&asBno-OO;|$7YB>?F_|@\E!www9jEQS{^__,m ktL{3頻zUUM|VI.Tk{?%.d$ $XOȳ;ϻw}Dy M;y"fwAq6η/@> =|~<3˒:pZ ;ӏvB@}N9s̙3gΜ9s̙3gη1x [ޓ4LLgJD\o|6 ,"kԐv EZ5-ۖN 8Y7 QmrrIm'wXX٠qq{  [6՚%)~"Œ|f\q84mK^4^~=rn9bG.//BprIVv{, :ϸZ,tDx&5_KAԡ >(sz}S IXV%!2Hy q}K^zEAFZ׵X~n OOOE5e4%XWqssC紶g4erƒk풻{rgԚVJѶ=:K r9)BR"d$+*2S! m#?я.Fd@$Ll@ Jɘ\F eA,ڼ1 Kuyam7p>5!w1s.6x]|L*seV c-"ikWL$ŅZXJZ.$WtpnhJBvO^dĨy||dZ{4(J DD) EX&p Zt-ZuA^5yd@b4|MδR`"QJ6jl],jbaQU- AjUNe[TUB@9^>)9.+Zd Y! \IQ:S,Ă\u wH08nooY*i Rl6n,}ovC VB &Us,˒#xeZEO,P{HiMSGz?HN>(z'}8 B!Rߣ3EDȤ!J($h3*#ʯ~'x=1&<#9;$/Ze3-B:AK{<$K{8{=\>R{~t̙3gΜ9s̙3gΜ9s}2ϩ[G82; @@QD)ab 'N JBĵ=Yd b՝wUi4Ef>i(=&1w,Amh-kRc]"'N{ȫ$1=)躖}ksicxwo岢 oޱlJO ,a l>/8QR AK>9-=-,lRm4BurP#PBٞ\\Qixs4p{HMk4: [f.h#QRVC(4o o>qv:"9Pyv{^N?Hu "ղ.TԴv ER۳ZmRq?QU^hrVXJI ZED&LM[k!+J|bQ!JMS Mz| UY=u}( bZ^|#r9ܯaH'ot ,]ߦ8neAQyNs !lG^\_ DAvX/D_4Rpc|Z@ B %bcFp]r#iz.Ow[NE }) ~5>DB rdbiԮ .3]ۣن͢k{b{B^^/5H]b'usJ~%WTɴ" k;te86-{E{ ㏱\`^!eRxEA :Oն' 1۞Ñrɧw"eҦ"Td,1P%wo`ꆧ5EDIU&9-ϯ_7l{)?|dX2o?fUUDhEi>dY.vl),Kڶ`QPEDA(w/2y9HEt"z  D30;OpUJsj, V+@bm h=qjќ4 Qq,8l JR5;LC@0U] o{; >gw8 ' \Ɏ 1FE%o=OGITU" (z@v^ὣ's̙3gΜ9s̙3gΜ9ߦ|UD PIMX68@(!"BT3vlHhfZeH.:)!|p"jh8[k5۶gMZlZam=w1 C&h2=5A{ ZeGOu-mWvuj\z%á)o5GO>k'\5&/>Vv:\^&X|<R CmDϔnqAߍwBu 2zc2d۱^Й?|y#gY-ղ:Q`xm5|VIM6 eXaX,qmC4oO_Uh)SИ1z5Om@۶9)xGsb|"$]JDaQbࠥ@ m$&S(qGz?̙3gΜ9s̙3gΜ9s} ȱeG2#iqv%24m=Dc006X"mَ|; h ifFTPf!T]S%yT&k=?qjA{Yry>W_C i[KQ6-H-M0ojU*JEΒ}Rx1%OO[.^ ҵ-ixMбujeU"5QϿ^\.Iy6'v'i\]1"Œ'˗/yzzJz]{MY{ H4,ח4|=^_ǯo%˿ (a˲@n{d|4+J=B(ŤX,>563.H ϛi+ e;}O :FŤǯϵ=}Қ@>lRAVBő!o(@>6 M$"G HcjGz1AnigU*\LʄGtQi{nnnr~"R 8M @<ǻbzM$bFxKe\^]:_\^q<Y,M$<ۂ1YʓŋI/^ZЂϲ(Á+:4 W/q۷4MCc \W<>>RnɊcѻ.-*ti{֊܇K D8ȴ5uګqM߉9i9s֨IM?!yc[*9<'McA .9@?{ۚu&YHu)t|hEqu>X躎vW/hi1H4hdEI]UEu85 Zk^~=m_)69? >CΨ떗p8`4tE(I^MǗ;>|Պ yGRdRd,423;I^zӱ̲ Acŋ86}Xk~Ek9!g5G;с>6\=al%rŸ cF؞ͤHPqiڟI3kiq{OQD6I 5鵕#љ`h%Erv8{(y9s̙3gΜ9s̙3ۗő  I33 NFbv߷ýxnIBr !&1 &qnj+ A=k25 *dقx8$hEc;{1'X>$m6hBӖ_wqαZzKtyzhcj 9I[r{{;cXmO]B# yOYU]Z$1{בi[51X`\r1$_&D\\\?nnnЙ|ET.\ap\< Po۩,]M[inCICeym'( ІP*A<ݎrIӿ|j(*" $2yFE5@Ӱ> 𸪪䠎f?RUWWWH)fz:w==~}<c鱜w6}ϟ1?YTtޏ*!I% x <.-͙3gΜ9s̙3gΜ9s|"Њ JaI;Np7g(aNQ*OU i~GQp9xOuؾG*gcjؒhn*1kDO."xAV^`ko°,'Q;T2fHb "PeœWRkCzsw3LY j&m{GeV+=~)}[jAm@XXXn. sHѣFJzEeUUccLFDR-Vy-e\^LF[V* ZbtHɊ = \.״}O%}B6h6޾}ͫsG6_l@s+\OO;V(RsZ:;W P(kBKʤwMQHA"i1#gRKzl5[kQF㜧*...&~zoL>C:mƸ~3 rz~ibΘ!JIP&]!cDkP%e m'cZ{)Bqosw^Ꮜ0>@|Μ9s̙3gΜ9s̙3۔o<6RFoiȟu=L^!B @PІ*䧍""Ɩ}zj8lp@uHhT(mۦa BHBJ%6m:r{ȾG8amwlvEzG:dZҶ-6x(`yxx"f&t# inGY4ȯmSpH>W^ C: yxx5ON A%8NAzp=yޠ(h5jvc2<92岨nq}}M^m<,Vk.G=C\^4 uZt-yS.:s}nI0*B"B)2C[UKV `rCB(p10$On:_F=ϳqMNsssIc 9@:n$ih nS?"ǗߞϷyΏ˻ ,h۴`8YaҥL #&z?#"} @顑->|!J1>c8gΜ9s̙3gΜ9s̙m7F eMFz6FDޛ4IiZϙU2oޛwnz`ņ O`6%HCumL,GTI d`jxΫgf%U#mk .\Zb(Y~ǁׯ_w8;0#R}:="a=Ue v;'\o* : 9K{r(mTp"O\XgiFb\O^! AH,JT$A[HAд2OO'Ҍ~Brl1Fv0ygaqӧLo?^3o^@ ';4tM?=ଢi3H_p=I B<<|mL :?6giZNnw??W_X&v] psXs-O嘥)jg9ȉ'~r:4yr"5Y'tݫ25l ĔZ#r& 6S4~o M)sQ8ٹvz4쒮E~D9 4ߕ-} Eܪ3恤)/.T?n7grDL΢ j9Z{PYDN %K:ҒN9@mIn(|~Ix?ŬviV5 Bh7Cv[ri~mhZua1p [{rQ䘈9?JMn˅J)qZ4;kZ>v: \7@隖)$^_ȿLi,SNX3,C$Yˑ1d9ZKk3w%i BU=wB  FePx1G 3V "17ZS ۭ.qqt⧟~BJIk~躎ϟ?cmYxv{>~nCW〵s`|S-4M0 0êuG[s-Bi@hE^as$$1mkwm,hgm`n U,weyy.y"M#x޾~u~;6mԆs$(wQ(Ȕ]$4ቖ#z%/?˚CyEq3HRHkMoc3p9Lj$3>=t A4-i"L̓ g74sS0Ҹ4Yc*WRi 6#m[/ ,g+]!zI' yo{w5q&Zя+APni_9,=yw}zxD $!L%9w+m.C t Z*Ϥ09G1~3Պ}y eTĘ8vsww˧k9 |,-@jkFU><<9 }qsI?NeeۢUK{P Ahlt{Ҟdu 4`#R͆V7yQBJ! no5 W Iy1$T{&lRʫ\|-Ʋb@_܉ "2P2>r4DN❏bٲi9ǰ@s9曇RJb.`PI )QnτQeyn BClZNL^X]7ӊVC4DmJkrSl2) 43|Ҫ4q읦)=MzZS8]1Nҟж #\9G08Am> ()B/~$@-ǧvrvky<)rwؗ6g;ڠYW.db#ak2)DQ(:#@KSm nfjsǜx5>|uYR&aXkiy "9ΖR"D2qQqBfcb p=h64j'e1΋Eۗ.D NJ~wu{t瘾/E˂ھl]/6kK$r'n[S"ȹm= y9?/=J)!zy\_Cyvtʙ&qdqBSnBz_gRH)\RJ Eq^Weih7q^`uȝ2g[z-OW AO>M7Goÿ8GB&#AIH!jA׻1"3M=YH2COx8_hOOŽ,^ϳ1(`Gәai:$?a¹%τcB ȱ83wԗt:vLB"3Mi躎0EN  20|qQ0 1 D04$!4 Ō DB+U@t(H˄ w=KvXwDe٢1+R (eoPz^?_re[x,m[']J3Ji)'fιgpxM|o%x_iyݳ4 (O)$J< cas.-I9`Y N)M:Ͽ]b9Ob"ju|9'ZlF.J^@0g/1⽟wq<93MB{_j[/o_fAhWCU<==Rt]ǔ"1% ka_G|t tNj*cj$')ʾ7McdG0Ҵ-M5!4sǸ!ˠ>ema`>_]q3Sd)ֶmXZgZ}q<GYW3D2B+w{]8_W-}3 >|`&_A8ky04 l\%2J!)31 xi=a=Ch3x?ǐŰO4ԗc_s oFq*J8cnϰ8\B) $z]Afi*%1xJ)<8& 9l)_B;\1'BzmRе:l&tͥs Rp =^>0#hzÇ3Yh%"o5?+߼{q` ~x=FIN B(rNt MJC<~_`d#^<#?1 #!Џ,eDki ( !)h;pw\.N'L8t:_\|R+&3ǻ;B{?w<>>/s qae,,x+a{RJ|Vz1;=(p|-./a˟,lv;7["#Ӽ5D=/UiKq]K.mcUJ1N-dYr1n-10i.$εe!&R Ԋ$` 1)^5j1e^밽y VzH 4>3Dnv{TG,MXkR7WyXl)e,vKx<^luR(3 {i+ٖv1k_kv#J/K~/..4iCe<23TeyILJ?#aIqypjI$)=0/-@拆@U83kŚat @f)R"sBI̐|XARJ % ̋+6D~aS,P4Hc.M@m dfgq䇽Z 'NW}|ti:IBL8dR;s-EπҘC s8-Ogm|pXT1H"z~ ??rm)ae{~p~Z=߿G+>|?DZH.*-!$s%8cs - w(w?b=WߣݎmZk@2$9aVpMzѫ,\SҠfpsw]Wqi>}D`G ~-mJ[Yg/.,Vua/{Vg97aɱCc?c,C>%Fpa.*ώʹ2F!dhyo 05Z=Z[_Ky-s Ȗ[@6 Pmsnm:@A۶@i>m]z:V R,)4% E ! + )~T#nۖaʱq$Z<]F:k8=\x!e4â2cf"m $ù\58WtÎcmr *DvÇ'rqG͘&{&#rrl4Yi1~u6<A˕G\2d2%.pٿ?O\WݎJΙ?~Ķ+fg5zea4.BO9_&4G7;qM5AK Yb<04bpk=kW m9[ȼ`ؽڭb>N7}?q:= 6_bV󾔁}|nڎLE;ex`^c c@BFqDP4GƱ_hPZbH gʖs ^f"ĭfOη蚚cyZY[3κ<mД ߬E#B+BNMȁaЪG(i")1;`%4M+.-|iIG!zm.Ƕ ]\ u !b$C% Z}dNhegqFͧϟg0 sέ@l9|cעqY4Bd~,PD % 9@i@Ȍ5 +3ӼCZ!b^K]׭ sq7Ot]a@J2!5JiNk@L[c;ȖJPs7 hcs30lC.- ظ4XlgR5+/cN(t]pJ•z-Ƚ9=i\p~F'HL~~!&bscگ? HYX씤%?;39G>1-4ͳE[^SSSSSSSSSSSSSSSs7}Nh$0CޯަB"$LL0N9 G_fW/8 %L#b\<ξl'\!,e=t]G m ~ h9qxqH:X#Mcv;@̥ihh Ƴ?n~nӇ?amCr"qezZc^a'] R!^|wz"KEBK(k wcSBi:={5'2o w8gci3 WBNXte(kidm8NHH"I#Y%R_~ \C*ʗkɢ sfLiDO}ba:lǵ=b⇎B"gPRdL !(:v]xWA2%1Ő0B5/28p;Ҝnc >(]\뇻= n߫VCo8}"#Yuُm{iGpKgRhļS$QDHH-ILY>/w-H)I3`j]reE))ٱ2qݟ綺/=Ϳ1FjYÚ_yB@i 4!(֌LstGx]snC!Bt6tKyy[C4 ~xVsz*#7g^ȧimy@cJ moZRra1 ZmI_[ =LqB;K$ϠW! s-jB uHʥ jB'^4 &ʠ?myhM&H%0B!{ ^I)cY4 À !Lˈ6 $ʂqr鯴rGw<<<`w|D[C4"S F!$2Tr9aN䜘BJ6f<1he6U_6l7oۺ/A\lz(u[xٶFآؾ?<|_aL%XE"sZC/4if,硁RjOd)YWo^ݻҦ E\4Ⱦ=Rjci|~nCBNwϓBrѣT$~G93ECg#=p<W@ ^W+ !~G쪍1ӟGy! -,u(OOOc;'m˟'޼yzErm,8|/>ߚa(MҖf3 XAn q ϟ9|ftd@(z-b}0Bxhۢs%D|\w8%>}\ v}weX`Xόcs)KxF 8tn/ۇ޼y1?r5?&r>v݁yg6}0 ǻM傱ea4ϗ20E,]Wqp|>cMi-ET4 ubb?⇞':9H":H6EYDHKPF9}w` ,jܛO4 (ߓC`w<ǏD q򹺆,s^KZq"&mHʤqJ┤u rk4F ^1W*"e:rC)Cv} quHwhm2JK9G5~%I)5!@4<&|VtfG0vȍBEe) b<#BIاgֽ9`zrΗPҬmE|!mS 7ݮMk-!@U)nki/Ѷ5^_ꗭK:QXsSY$X2ض%fЯ G󚚚a2PeZF44)ivݬȘ&\!`|}M([^g `"KvkXP [/Yes98, 1-X\/eU2ZnC膤 ! RISiݿ5~eJh1-,zj+HBЏӕOO}lL\Jƶe rQZ#O<3/r ^KqmP/^ťK}z/ZX4|=N_ݎ궖߲_vTJTt"}{0]!LH@i)BY2S@JW3H~9|r}*;+\ϯ[yBAJRZ<4Lti t]L~`Gvw0am~4 >Nx8h뚢Әg 41\v3epv;.}OZ>ۭ`lqj 1@ǵ={^֪a]'&?v|x8s8C-)ft!3vrܡ[LvBqS$EOĀ-W ~xq ɇJ+T|z|bL:/g~|ŇLx+>$45:ޢQ*f }SQHaur]n=KkxU{ݾ%ۆ6[XzucqIopW}GUi]=uAg>q&2h3G.g2Z*Q\r'Pr>)rsj E&Eg _N&u)E_.c7a2| q7}i )1OimC^A@ƺ`1X[HQ^":@jUږCO@b7;iB @BpS2q1F(6p^i5K ,aPF 6i"H AȌ뜛yiVX fd>?<0LAkr8זvtӐZ+k r V; A ɀ)RP9kOHr`'W"@d>Oci#! kA΂FrmBH4y253Zq߰sۜ9e}5555555555555555?|0z7]Rj*z%LSn{IEIIL~ǥ꧉PnߟFs(44V6CiV@ڣFkA S^Ai8Nl$G.3Gi2# ym9~Wr4P 1r8{!ꎖRR)YROH!$/<{s9@c,!'h{laź=mpCi2nC_Z9bՔ)S"# c"Yk}kv K*N)`r13 ^T[Ӌfdu IDAT SJLQOˀO>Z+~?"8+Yv)Z)BȐnPv;T1F.,S9GZ**6H4mWDK;ĩ ]f2roiTO!)mQL1YS}oN8H1e!ʂ8M$2!555555555555555?|0Z[})Ƶc8<)ܚܼf8кi2 imGPF>kZC~◾ \K)fv|cp82p0#mWr@)4 -x~}?7/XsӴ}O皵Mm E)4yMӬM?}4M4f_Zxk4N(t77GeMӠԏɺ}$ Ѫg: QnNG2\#R*y:]Bljӹ'$hYZJgywwww0ۗg"vπrͯfPbA$ E)'f74RXrK'Ecy,߳f07]Lo6 5+m[r^i#9å 8pyh"Bv]iiRvו8^n]!{nc{v#P\8d9@̄B"7op8yM0cS +c b# 9R̭Rx(}M8N2LSHh=9H̴ ś9swwG;д-ƺPjȲA$B(槙IY\ԠIm Mrg2 F^S 8=cJA2TP+nc?imi?7 4h֢A-FGwPxꬹȹfTj"\TtJcyg>3حnwl!G <ӁY)Fn)mcgr+t9XBA)4Brl%3X)LRR,!IޗŞ'ym6۹_%zP>SW)_$\ZaZaO\xjjjjjjjjjjjjjjjj~natyV5($ũLJ#5G8Ҷ-燧u~/p)FsքIcbn:V)%4bD3gY] [G*3t0X> F,{pɲL]Þ" YP#PN-4WDQD+ڨ\dER̓yN {Xkk;N%XdsĉرcǷx%78΅Q9R4{988(YN*Q b! <8X0VD) ɓ, )M1v#G+\+fy*[ޡ-y\P[L V֓3ҙ [NqFmAjE+(+CB:r+N ˶X \{O]s='N՘,gQEA^FebUcuFf [[[\tхL& e &ӨȊ^Ǒe,-ikɊ:Pc"7TcHRVt7sׁ.m|>ԩSb( 3:qѼ=5ۦe:uU5ĩ:tEY 'SVذ~=dS븾Lz>9nh7MGl) Bi蚺WstCYIʏ<& Z_+K^ӹH=8rU/0:U ۨpg    @0փrk#cZ J)v&;C{?)mKYC朣bpe4ip[10B6;c-,]EBr1OSBV+fY|=ZW%u|M&s"0W\{NfB!nvpMvxE30L>ZU?=ɄZ]6u;]UU tMMi FLf]p>jQLɄŜCiAiln( u \EnkrT,SI Zۜ6ǏQxㅇرc;vGω&YEQ₉3͆&1ũ;]<\>9u 7dBQa?txOn.`m>\ȲiY'Os {{"ͪoY PPЙc'=p(9#>}1Bא0C|w cB7VlQ=u]No(g    @0͗KLߞ@*CiM0B+2vιsn5gܦ:bv#gsԜkQ,s0Y]#FH.&U\O&j$@aZ`c۪:'j'C[2 :C4};7n2cx640)!u1L8<{Gr>Kɑt4I?T7z9w<˘ͪFfq/Nk?shۖl,( $1/38\pM{OfZ(qvh/;)?Dw]7\J VeYR0q2ZUqmc"&): CXV(/6]AAAYFwcA~QǠۖUPTt.`ml;e ,&?h6hmzl1mk>Hr77P5y^`og 6i:1Xk/-zіMvd5_0LhcQYzɲ8Oۍ(04$5xϩͪiT XChCIX,(ˊz>)ٚr9AMlE (Y*(t+kG+¤r}@!EjvP8ؖ50Q2++:>6R')Ayc cs1]ϘlZQVct &BK`*EMqk:5{O1fC1m`Þ걗:;&i)!3%1zJn3\ײZ4edôCkz K bq.޷j~m&s)rǻܺŎAkhnã^k:FIeXc`.] (zCѵQ| ʕPg:jfZP(>    ->YA)ݮXsvh#eY4-KVԋmyTi,W%Z m8xN"2,˚Bl Q M2Աau4G[N;!،VL(4aJ0zA|x Dd Gp-^y{ew1t|65z(QJedeFUU䯆U1@Mfvx7TIWX,ueEQCTrIt} ΅jP ni;߲XB|$FibtEyGZ2ӂcl^.5sBT-O4/@TUsLL4kд)],7!5!2mihȧoWOje.&ȧ ӅcܦAAAApևH+ݖf>Zct*jDU`Z0 nb1 9KRy8'&Zk၆{E{9udReEp_Zܐ冲ʇc4t`y1D? F@yBۍ6tHnbSyY\.)CH>}F~:95vޯ.x a    <0!w>t|Bnmm4C;ЙVK[ں{ǯ?AU;zf`Gf,`t6*ңqX*pn1ɲ1=ڬDNZ-hۆ6EQ(WKrJj*~Z<О\Vhe\!(E^FE}d m Q!`krk!뚲,9ujlFasn}V-N3$9t0(V}h;;ȳU,2p:ʼD+b(՜jVcb(mt>EghChAGW}sT ֢槔A)>`d&댠<.x|;IEj7R} kGru+֞eh4릻c/s"ǡ]Ʉ(X.Cӳ*)_=Ӷ-;;;c@ ~00!tXβl|>gcǎł'N5^wgS;TA!,in-:(cTd]bsɊ)eTsܙCZ> jmMJ%tR >jJ*67hCBlmUcF  A" ͠\Xk-d2o Sn֯GxϠH1Va{gF۸84֮Wu1'nގx}.J)C9g:qmê^6khV5Ǐ_|>g2)T%usBdCf>[C1AAAAn at `,3u$zd~a`aQlj+˲&G]F RuMuloo_OmUr䢭$I;0N@9@B?j=+RP8P+\.鼣Lp/)<8oq'50;,+ f2Yljz|JȊUi䤳~s\ x( Q]ϛo+z\~J'R7N;} aC^ù99մ=J9o5溎^\suX.pMY8/8'n:~}'Uڕ5a]'Nt:ni]9&Bw1Jz`dy5|EyvY,fd劮@VtCnvTE1ih:GӴ @<\=n WFw}Z,98r5k>C,Y#/ Iɹe20T岎-e\iV[[SڶC \.i6a/}E!HQfq` nc@"Uc|w ɤ IDATAE%Jn7z=z㢋    ->vtHnت휣sKߐ4똙r6MSk|VuuiwC4uX[>(VuK* {yXan,`j\^QN&tmε<0\/5HSej3͆vz2u2:cCf!5?ڂd;V[bJ9kNbHk=Cb|bܻO_pfrL"03zqh? o6иl9Jqcey,5dp^ :? roE|6>9:߁j'?vU^0LMkB 'N`8kaspp EYٌ͑ k5ViE:ideku4{09ԍx@cj§p#zh2vx̌YT8bȫ50Uqx`VSsFg 9u((5:?A rZ?akF7zrG+P0R    R8h`l{ɤ)Za:t-ƿMf~hX 6McVm ˬqJ!GYމy{{vUC g"j1zd2|q\KQB}v>t|V輢S ؀Q1k:OڵrALh& K^ A@bpY:Ò`{hF3 ,zď9)|P}uXBо"JtoR [k|JTڛ` )=nlmm.}VW:f[S,Σ cX-kuCoi `rPdYև:sT)nfo/F=C HH{%{M\S7#~S&3X*0a]RjJ u%    09rdvq(4lvƮ92i1zܰL6qQgYphߓ9vM 5@T!aUU4nX;=Go 8GC[zFTzLjtڼIB)’MȫmL>dU TltkkbWy wc:ax_i86b}o aN|NL=У@u|u0i#?sthCKpc b[ׁU=npQ̶Zk9z02YZ;L>H Z)6Z\8tͰ^LIG J=yM9^Ji)Z>KAAAA%sևu]suע‹.#jjѐe9KyzPQ<*]&ZEʁ81 aKacǎ rdVA)JM t{ Eu][Nb]xX ]jU`9G8KӴ7jޱfBapީ{ڮcRY|ص <t6P% )'[mLQ06L;  @QUb3;~aDᦱF]t%>saNt V" MߨŠxjF)0Ǎ:vƠ˜ߥ6uZȎw̰CUhښ:;ZfVEYbmNUUx:>(Dkjxo<χk.Vmn/}}Q~snc8a:|(m C8lX *6:Xt9|bЫslAկF|P@teY֌nuz!    7g09;_㰶vEńB(}wFjmzUJB #1Z$ijEQTUշDۍK knYei^1ZW0p{{X/i,#8?4@q ʚ>6-M_<.xtfSly؂2n`:zC?ۤ75ߓjad: Lx5] T&7A۫fz̸‡[٧{ b°Z£p3Y,2ׂQ'z%Lf)j)s=r~<դZG|Bf~۴kUJt]3,(u [t)י!HŬߘYVu50l)?S["Rh k>C+BΣF[M?GM?w^m)H   pK9UU9 e4RQEV _O_VMa>P!&he*?}T =:~`͍±~$m]E{ 70L|t]u c ySNaa\Ɔh,ѽ7@,cYtY,}\΅dx)s),+ fBV] Sma(mƠ]Ǫo60908m3c|NGmtD a6Ǿ %qzxX/Vq0cu) >P@TP(|I/inEݶyEν >w;7V5щu0/(BKacF800)j ZѱL;9cH !B#uO0ߵW7RC`ѹs.65ÅFa7/~yB!9TrmbQC,'AAAA=g}ݵ0¾;uї۶X*F%m .2k >I놶q+Ő)[)4=vA}_VlooZe\!hYFee𶷷lFuM6MӇٖ8؟v1Ghkq Nh[thm*{?IBlj~j0T`#鼌à ;;=n`7 'ckn㟃jڶ*s4Mu-8-1ЫChca~-8˲$3qHgnͺmrӯlQu_}mZuk/ZiŢiٙUm[Őkkpq8iێmNZ}<(CB?0~=&}+24 ;;wpm7kk-T\Vxlmmu<ރeb NZ-yR7KzAie0BAtmMGޘ96L\kp"ˢsжPSde,ʙyl 3KJsȳ}Q Z)+,zĒh :}C;U T ׁEt{̅ D%HHǏuؙ~JQdq:5_Hgt::ond@S/YVE/Wo]K,mMUek2l?,/w.JCc3|K    ܂8bzPYjnwhcPaY>l#d)Du6j*:T{Bf#:hwf[ ZZ 0.66ۨjhe6rf>3mj^_Gj&W$Uꊠ%h|j@뢿6W-΃dy9 ~-| =8|q{A|1Mk@ӍTJ[_ڊY {ۖjʼn'ù *ٺU\3O5Eo3;,@vq>@ի0BtW O7<ʨvөi߇ոܸfSث/Jāumo9\B|)ta#u&8׿3dد!ZGӵAAAAnIatQe6zN[T66)=TU9S|> !izY!_ f`ڮ+:O9EQ1LGEǮ4Xʲַ>'˜ Yqp? rKAtJjڊz8ԮwcӺahu{e FcAgEW9Zm0yQa e \oC{6Fz 6U8,vlo~f zFA1bja R>x>iCL7<پVx\WjڏMC7%AAAYFU`6ABAx),JY C{ҹ8#'s4r.0n)l0jm<ϙQ\zI,L&ַ#cc%ϣ7i2ǘlxm)[.m;mE)6zRȚ!yGPxc(4A &C,LQh469cP၀ x8܆NFo;]B =w6Sy8馡m[Ov5[pZ7 ڊ"IZxV+P:>WSP7F}3;^l>かM ;n܆#9Z3BλLq<h8m|.!kҏezO.tz_EGE O3%=N{jAAAAnNat:ѣxb aCpLuvصs2, BC"5*#t:N,ys#G1f6xJ)jb`wwrɑ#Gb9v0a,)k |q0?Mw(Ki״dtqh<{A4 ipg:w#p-aO~bx2_+|jb|G]/ ʲJUѶ喣G2L躎;,.8F-GZ\/{DZ6դ)!`& ǫkC`B@jc M6Zk _ ÚIꑨt]G?pXgni`]ף &OۏjZ1YuE^ߢPtzp; Ӵ+sEAAAA89={>߻p8x ' Xq-ؽoA,Oś߷>/{!    ;sVIAAAAA/EAAAAAmH-     q$AAAAA8F     g AAAAA3т     GhAAAAA#a     pƑ0ZAAAAA8H-     q$AAAAA8F     g AAAAA3т     GhAAAAA#a     pƑ0ZAAAAA8H-     q$AAAAA8F     g AAAAA3т     !VןƏ4,x{"_. Oi?|^7~̈́9z3xȽ}h7}?ǟ;/懟s*^̇rK.Ko~ko}M<ⵟ8p#1oxB9bV˞b>H5|u?.Jʏ?BhAAAAA Wyᕟ斫żmװw'y=o]>F'^q9~))Oz G~7~ÍoJ>C_wѭ7豜Gq N;;׼7^ۮ a44˙y^G>2s/# &q{T.{9yQռaeJ{AAAAA S9_B @ux7|}-G8~çou[v,}`y|?Q>Y+¡=^;u^_М|:ܜO_Q>W4[>y5W}fGu['Wy9g9|$>_.ᏼ).;7_{Swт     |ќ{8~Jv/I\~>Ox3÷~盟_a })U<.nm{ pm_:nwwE'Mӯ~7pz[|sgK1 3RA|j{GÖ|%wήWYͤ-    -6\z~ccTχ?7G,W݇K^.۹v_⢤-W0w׾//%Mp/!y˿TP|z\0SYт     |Q;|_Ξ:>=pѭ܊hEyBvu-Lryryǯku~.Vuz[/Tp%O~yoK(s8RZxW5:n> AAAAA8 h iCK5+x?/}S 4? IDAT~ZλxֳKEN'?G0o`.r:_spWC=x[z^GpĽo C Yy)w<_|(S7cTXKnu˂s7gOAAAAAŲʓs?/>wG7#~ 9rc7 3?cw4f\U{s+x/gW;-o0?|˹zcF>q~Q/eOw-6o>y0暕et}&MVGsp3A|6=F4     8WVrO|-+U;X}8C⑷-8盿~(w?>㩼،co=W}-x͝Tn᫖I? ^_=ר/;"g#Ϸ<«'/y69/v۫_ΣL׸mvʽ~.s_x w{x}_}_,wO]]xo>< {[lyx+O,Ͼx oO{_p+:?ɽ=.'0g/Ǎ+a     gwUA};r^ͯx/rIdGSh}=g h@s#_K{y h^r,oy=}> L3\ ͝c,}įy7{𣯸; +/y|r݌?%to_*.y㿀^>?( ;l]v7.x».nsхUU/Ou|/M;h]AAAA_¯9l7{_s򳛵T5U7>Te2ޣ.wW}tF    pF3-m~!@)Oa LD!&GAoF.dƐ %ItjH_cފ;<5~'.LQ}k~_]Z1CvAG~wlo=v:ׇPЍgPzg~u};FTΙehc@+4ٖ`h{0I|Fmwɡt;v}O0>;ؓCy}Y,}uց_^izpEƺ>5Dy%8~kom\aY鳩S5'37~ʈgRm>rC9z,5% EDDDDDDDD# @!;z?l=qha{ TJf{K^)5,Pz1&[ۏ1f]ÛJޖZ}<*,zfD_mZemnNgkC sǗ11uk"!%ELVXss?s nVe?w$j6M ܜܦGQaTH+9nNfǠq-!/+=7[-bsٟv{gN%L=[3㫿z{8LbFo;Aihna-`.z lTfF0)s9v8: (5ai]^`(""""""""r<<6MNKbw~] +ggXYL\*[EcCF+W~'fD?hfW{I6q'~vB8ɾ9o.P }k;5_cr7-`5G65mjI^r t;*&ejW<ؖ}Ȯh L_6"""""""""Ps#="Ak]z n-yO^3O6zn[{|7.~1ڡs|V>w-x']B|o Әg&%e]:DnZ!2 ݸrHJJ.Cïu#(޻ώ_wԾ; j* O&1u\$Ϧ[SB_RMpH0YYp-0 , XJ0ZDDDDDDDDLӏ\e<-ҏf+^L$#3sT,5kDw}wsp[3.!>2J9Y&͝Nw1Ã?g$LbCpb9gX=_y9i>P9 \;|=. p*\{'q}'*_jƮ;hݶ-GZCf ̣/5X-ܾR/?<^'_ ;/igrzȿ p|;Yw x ęE%wYgݫI88 )u/гJiWM_}599 mM7ۍrr:q|?oqqqtؙ"""""""""5zAtE㝉r6 o` +DeexMŻJI򱱜kR?6'++ b(Xpatb/5^a? |3s$M| o9?F(&M⭳ך2ܷeNM7x ~֭YMVVfqtiF#۴-7V=EDDDDDDDDI ?}_mºO\FC>L%>tڝHb=lc~~]i8?֭.5VaTf}DOjAaX Vp^;ؓ\Տ~LlƜ0[EDDDDDDDDDM/0wJvF\""""""""""rgTˢŋ;w.yyyGsՐ!k똓FG?_=)**bΜ9ˠA* A;EDDDDDDDDDL0 svٳgsWеKRוuRhg3 \C;W/ ^"GIRDDDDDDDDDDHN:Px  #- RFO 4dUz]Qa!S];q""""""""""S\xBBXj%9^),,dP;atnIEDDDDDDDDD0 ujaܱͪ,\.$?b( nJ_`4m_=?:vDRSSؗłn#0(DFօfffJnnnatF۳bvVIY}E֭Kdݺ^a&999$8@\RR+#"")*( i>vnZ#EȿOPp0 ]6M@tL,15^{|sV+0ZDDDDDDDDDDjhq EDDDDDDDDD)˭"""""Rҳ&ۻT[ԪU am1 '""""""""""|9Yط۷>脦$""""""""""Rc{0ZDDDDDDDDDDjhq EDDDDDDDDD)0ZDDDDDDDDDDjhq EDDDDDDDDDƝ0,H%q2扚qgmru{i9[<ɼfgאn6嶎;?Y6.WRX0S}֟/!^5ǜĒٌz~_ܙ1Xb9=s5sW[6$yX+p,@ٕCTpC0nNfaB_3o+_X@NKse=}Oyt{+?}w}k6 X=|@)0^O.4|E4QƜUDѮ̭[|(潉Xwߔ&[oXϴ{y4 )ԝ|#&%Q3y[tpoA<#nڧqU0⚆>[Nӡc{j ì[ ѪvZWZ~ZE.5_hI|͉:ʯ'#سN;^yزڜן6ZMtן&e wm@yQ9r<Ily|F6֚1kE&\ua$6Zk/s>^B}՝ŎkȲD X˂6N&.qaF qFHx\5v PUK7$\šntgEZBJYhՔcպҷ%J|F%= ⻐`]ĖMi8ǔ[[ek2 ?HsQs/0G4cVXwb ;v&Ȳ/v{&5b[Ty\ܧVdHdP#v瓑Za/JTGx F&z ':ڙIuAWy]0wk ZDDDDDDDyI IDATDjіΌ`?,JU ]N uqS s&!AxA}zGٱkEB[ZD[Ѩ·H_~Epֈt\asҪ}mAY2"hw@.WuH܏iыa#{PS-HlLCEϠvWaGkIMA_ թ OȧKAWѦÂfQG:3qNr2shd[p-Bӹ6xuGE|{MFv\L6ozx Ƶ<{P;8\vvtV6hA~pfJa'{G6*]͂|3m)x81q庅G¹M/}nΥ# s| 3/+}|Y9{:u~NB\V89kPdFӼetk0s7&`MяrY:_5N_ƌo4[t]YQq e"""""""""mCy䝭9#ɻ@&r{g/;D}qV?IfWpP +ﱫqEi$M**~O˛5kw}BʶX:~p3 *]9^SBBi9Nd,Rs!nH*H];~YDM0Pιz.C@\t aX,X~ EXyY=s>ɝcyFea^waQZ|k.fGäaL{+!ը\;ay?LGճ_y;ȿqea]ŮǎRc 2tj0Pnşz8-/vn:zKpeUj ohTIbVʞ nTq0Jg[oa"wl3n!=$ Y_ޘ̌ĮyK(M{nEقVMyќfNV- $oJv8C2\* Lh#  ~3. Ӧ=G8L RDdN#g֒s] nfͻO?q7t&M&[?c;;h3qnY&u/cxI᱿g$<:KN'ZxEϑ0?1nV&"""""""""nn+]HfZOϗS5v u/i[Jb w]B&֓D,UEJeů{ l*ھJwgsz<#kʒ7V܍΢nŜ)8+w=xoL%D@ g1pήw.$ HJyx,N%}z;!nkϑklD^=8@5ٹb Wn0jz)E3qMAKyt@UcܝҏPe\E⮬㆝X"l>y]5WT/K.j 쵢ŝVzʲɚ+/L! \ܰ&""""""""")B/Ugo" bف>m}헤0wĽ\L@!>4h؜WPT }W*fh[+ ')m_nJE)~W+F?sg` 6F7%%~Q՛esHmiyv {_ݏiN닸4(L&l'ie}?85P{0gtmɯp9q_`%iJfdmXՈݣH^_K6hqZ<5I0J.Jf^7y=NԢϣ/pc{UZ_|Ht7v?'/|I'4s0+4gQ\=e6cؔV*EDDDDDDDD* Mg./cAHBa$:G2&aM+|D"m13q-=䩎>4eӶ<ƅ^]UV58%t܄Npcn0t+ Vhßͻ3ywV{ToEAVuF׫8!oGc}͑)!t)aX;ҍDҧ-~TجW+*[Eǵf`:]s (Jb^FcOm5"""""""""'jԨ o,IENDB`jdim-0.10.1/docs/assets/skin_sample_thumb.png000066400000000000000000000403471445721505100211740ustar00rootroot00000000000000PNG  IHDRs΂ bKGDԂ pHYsHHFk> vpAgs(?IDATxidu˼{UhlM$ M-)J,,E8&b'43c{P##˖ek(qDE@Xڷg·zh~U]ܥ*+ϽI}CR)$ER 41 Uho;4B _kpkǽ RB1b[Vo|˴L㡙/-vlo0??ǵkL19Vlpsm$}~i~|?H5?,#ob~ⳟg<9 zy,Bɩ1\YБe>°c`M:&8n$t$t{xaCfEt*^Z֚^.0 )DqBǔ%._AY"c<;5 mЬF ++غ.xhĉOB!l=(AHp}wM[m}5b[EJx샏hmC;CFx R"D!m(<fJd` TB*0¶ (q=qc&4@H(! iRbJƁi0´~L6GG۾eY$qChq2!x3 fan ͰI!Rbcőv ^~7JXqOg}cbyWNSۼN/6!r:°&($Ҵyiaz96kxF1ez& }^ 40㗏uϢҘNf#G8$CmL)$A{h♣{/>./ylq N.259bfNE,.-$]x qjpxaqD೴H4)ѣ\r0 T*lml0;7M0]J<:Cl FGe:Qح>p>igbzvIg&D~-,;Tf3*bVDz=\/éϐuߢx#@E!rpjc! y+ȤMƓ5nJ>Mo/px9~]Z}\zLa[^òB8qhmr&!S>A:F~0 *h:XΒ13h%,2 IJ)r$WV s SG $&~C0-{I:loL&) 7$-È6.!AS+M$oakYq9yk[:_Gķ]kĈ5bܸqÐ8v?J)k]榀,j-& C.]vYcJi4DqLmg!j+,PŴliJRHmNe|nSjy>%@ݡZbHv>_w~PBmΝ&/\hh_oj6߯REsos>;[-ڝnvNCfbb{ ߡa֚N/Lq 0S)ŊN$12"QiR8)f95XO_< 7G&°@ipseFy(uz5mvI&Fѣ5E$Y6BYD@lH&DzjmzAB磏|O*WujR%89?A1Rx$:AH@'Z061ý}?f:?" S-!H:t؇WR"ĉ<#1}y#5BԎ?Fui`C(B'"$S&'V@ ADgS3g. bLKb;Yz<ɫ}U6^-5f)A'x;xCG>diJ mpM˸T) 4So-`,8_Z3gq̐Vs-T*(\ Lpt&6:IxqW~ȏyxNOaHOpsEmG&c#A~DT@tD3gp}_ />ǸDT&1jC{;Wij4) ?LBFJاo7۶qMK䤑̏#0Qp =~)P;t#UQJa|{f-05ً;\hR~O-88F1ɓE܅AG|i0#E:Ǐ##zN RW#IaN5HT*b>ՙ*.OښW֯&{XEy>N 67ㄢ1+sS* 8$Y&s/#?^xElP[H)XX(2V)P% YcbMͳWo"kH!񌔏~A6i)24HPdKR6hvC*yw_06O/]RtH}t"^RȘes\Zߣ\ܥXq( lի;^sd e_ z-4a T~g"@5)QiBiڬ\ȋ?/DAB&C}|G> (N0?K })l C ,0Q5~A0F?Qs4}A Jr1Ak p /a9,/_^ܽX0ĖIƶ^NP*!_,{>WΟPȗ'˗_K4 @"Sz~~QQo94Im/D)w՛4;J{>c2o30HXC&cjx!MDocF`Ne}(P)ͫ?c{?1  vΣmuÇ8+}}2}١rFɃ{0ë/<˯̅WM@$qd$*pw6i7j<}lBH 0  Rdr" Oa&R^q 2qICc0uT=v6p,I; HnKQp`vj!ILKu"K& L,p ǎWHTT ?ͯ>_}>a0W6W,f?`kiH$b0ıL.b E;`bl17WVds+ ?Z&$:¬Mm$7:XKa<6:4߇AdY6YO7/cYQDBnCH,PPG¶3a8u1V)E)$M/񭧿]$GDIJ*d(n4jX~ })%aƲm,}8Im:vSwxOSoQjTۧ<J^<6) Nd dvwkM3C!$R&Wnf LMϲr3gO5=sbOk0b U10db*G`24q]~O[# lmKk{' ñ* R&ι__,O,s)0<&QǏ3hnX~$WoxbKS{w/&!Ia^nhuR>#\_&Q!J zKjd֢{u=ResmD)al XC6)Vm^HC)aW7.NDZ P"R)1a4=>$V('&hA@6$"ut""egsN=>)/,[O255^m.>> dܰ+37燩R< ~/6=Rĵ ҔZnٱvF6/0" "F-CgO1hYkj1YWg:P&/;ɓgv2Nˋm]r 7WuCA+$Wu,s\>ѣN{=?j}N>ND.]abbJ>\Ƭos;>>=By+G 0k#Eerv+;8v{9ŷ_')z9JSt0wHvɗx_ƕ< GO> .'EiA? %XId 6tUim`@6$iJh$)3GQSNb``mcT |3UJܸy\>ÇgӲ(ln2=90Ha$ uth7€AW,Ǚ`兗脊 ;7&?'>͏ǟ}(sGO333OI?3que eZbl9fi 塱DZY:&"ELn D5R $c fggX]YZ M">Pn_frxUډ# ?!LӲml1RthDC(S, Au|J !B A;s<[~'>gn~An{;z|G?Hb&=~o\~ΰ6M:{ %~rU^|5Ijcƶ,>Sȗy ױ'm: S$Ę4;=8F2 Q_}ϙcw/rQ"Du\Ťm~1LOX-3.ve\yAqGXؖc]RJiaډ5WeMC CY8|m÷N>c;>u^mzz /~4,ff^Cx^MrXFӟQDb.6,03_S!OZ)FoՓ8 *+ MWw^667˥+t=Rĉ4i#PZӏbry\.Ghnb:r"a  llΡXQۭ{T* }Dq9HR!}S=|ɱziiy %F(L#at V4% |ÜFsOwVV%t n~g;%R}o ȏC(׀N+ SAhu7 42)LO\vz߬3pthwXOu$XY! 6^FC#odmӓ?gׯqȸDaJ߷zaI6.Sk057Ojdl)Ao0Jc$N~0XM{!5hKH sF2S0 'N-WvxqHtRäl{h&Bm( {LNFAFg_`glcGgpl xz$IB+Ln{l<ܷY]]٘8Ah'XwaƆ6!@)N, 0uē|_g\wQ*S^~rAg^::N}4ZNM4UIJTĉ"IslÐZáz.;"i>i>>q rH !q,քS-[IHXʑ(&ׯApL* Mvvv)W9Ji(UGÛcEDqD5lGOaM8>0GxV@4>d{cN&^s[uvwwX[_|?%4Io} hivCN.bH Ďܷq.ooXڧ'3yJa0uC/W`g~K_kPohm Ε,g~g vuv]taQZS噚fww^R&$P1 2{(= bVǰLzɍW9Уŗ7d\_Y!xS3s8GDJCmQ(0M,BW_KY*#<V|B?Tkׯ/LU9$>r/SSLNN1?ٳg À /p ̹i^N`ET ĸ~i.]~|]eF^vJa8y9׹)P)o7|J%i:TYYOrd=ı-z_?TݢP x7^1E a`&|}?D6_eeuO<'H0 c;=|9v1 Hb a լp!n޼6TQ 634:ef(KLOi3uHN.q!8 "nPﳹg8CH7}?K= `zEYVo(@sm;5넁bzrGRK{ s3=ʱco?$Sc =G a9`a2Kd31VsW(yZ%KYgvʵk+ s5* :[옷 \lq`:NeJ\@?g9qQZ݀*?VGQX:TƑRpCh!8+09(MP:5ezRfoF>|([bzr.= g]JEn,0f3$Sl&%*Dl|xǔ'$F!e@ a\_ӥ0%v(!I4U?ITDFr8RSZRN5ld@fD͠g)m iq1RDfLx%֊0Q( qB>LKc>NpW0-$I-Rifvo~Sp1 DDu7(8eaLF$M>t9\Sa+@L?)R4iZ47)4cw]GG¿W~f( x-4_xcn6;{3aD^[^g@7;{[[OM}gðunnN_K˴ a )"HR(݄4dsNs=}$I^D)jݚ{~NF|ZV bw' M{Qdp6׌dX)~X)$ҰHca\8y,͑(ET4;LMO~I>%IL;ˡYAzHCVֶX:|~4W ϣiX2q m26Vawgm kkt;Q) 719C_~yS_&]?GβK5iri TJE?#&X/p16,LO07?_:K\< waj7ֶ}<(3K lrZXk؅*gO:1_巸"7wLTMqN#GF bs}MO0M'>qL } wOC(T>irJ5rݍur-g#_b( \xCfޠP,Ʋᙚ(W1&s <) b 6)T&ZZbYb#U~ʯ#z~w'@Vʥ2곹g K90:E Rabbs \.3cmN=y%:u CX:73sw~e@uب^ϼ>] X. BƢ41C15=++K9Xej~՛a c>ybE߅FGxw40eμaG|vFZ9z6+Zkvhwhlܼ+`529j;4Zvv^ڵ8dry]ƊRi3f&g1*byd#s'&m]ezvg"M$ H:B!w;K(\#b#!1$Qϸ 0 )KQJ(m|!{b@;th'Au722*.ϼp9VodƋy:6΀i666H$nr4DAK׶X>|jp:GF0]Bs 3UcS 6dl6GXĶmF/Γ&1B9No0mA bp>C?B.;,EdCٽ1ǖ&K17ˉ{'!Ye{Ǐ ϩ i&(aǹ8|8yKF$B[=o0  v* Fޔn0 ym)@u^t~ܔ@0|ӿ>Lҭ*ˤf 0.;^#ޣ ;n7G?0rqXEav$ RZ&ZiLI 3 .x1Lo ]x'9Ǐ<(e5iUfq>ǎUeȕ9}(㼕4b;iޡ{cbvLdNNP{lnטP7_\`yMRq]RIF|T)blCbjMaj}kTVvc>(M)KREfGx;W^ث1<7Ta}چ+6l=qB+%(j&1}i? :6f't[ PtngBSieln6[-MA'Q+>QH).Q24r\f=n߰F߸Jэ""E8gV96$f/Nv1-QLl )82BbbJ0$(l0A4#I4Ji#d]VLDB;`ыcʮc %J+^m4#jd#lLM=ᾉ_ɉp#w4V}1F|rGw- ӲLiH(%*5A{"RTM$83\J)Mn 2V" CwOIBY׵iu d2.*I'ɢ%~;݆#%bKp%G WJ4`D`i Ҙ4JOW f:S+4 5V؀#%FbCZ#[klŨBRJ0 4R A9i@Civ2aZi7w{5b`0>VRhkqB!$MF0p R1vZe9T* FP:AJp8 `'Hsd1M)ҸuׇaPK ba(p@ۦmS,m4IxxLX0Mqt5ZHR 4BHր Q"#&w Zѷ a (1 ש{QòI˔d8<$D[BT#GHb'BBc!)"D@nq^ h$)M,"MB_+&:w ! t>:˴X\\buaekI ~OF l7&k&r[ , 8X[bAGxtS h!cSh4& 4J,"&ƒ6iSD%1ڰ۠UۡT'҉,fx8| /جn3:H!У?mr9^W(" CHgȲm`8dg )PQ˶p vSLEMRK8/"*EW7RrL.N߈8oGAt~tBqa(=\J* $pwô4 l7G..13H0ؾsO_~ݠ8c?*zr6]]HTC|.ǔa}\/K~4#!JD|К !]AReopmuiGm:nũ2τx>amR(xo{;q@I1o8BS NZ m$qJ WVXNn} ǒ $RcY]R߾p+NTO *k؃}*!MF:f7qGXy4_z)4R 1P!sEf>L$ aZC0q=!$;>f%4]+Mbw9N<~/SOqzvҔ4%W,vVN߈8wByd!ah4rщ"|t" vݝu\DcXEwaƏ.qlpzǨomro͗Y_Wl5e}rS34뻴Z-6ַأo;2^:L\PJ!A@'J++S(XX:F!eP&RXA_A'Йlvs%T$pt B;qYEJIXZ"-7=ΝaR`Zz9 !r׈} à׬QMR2>J|gh4z~c<(/[Q# 67/yW$%ԚdP`&KEhґnMԠз/AU @@ vm,gH)1ML6G^33CjHVb +,] Hi BM̢GT0$*5Fo7C˃J CQPu > ,#eqjcέoS۸]![(!!U =G|O)6rE>z4!]µ\ 1A!Qo7B d5 # 5c ]מxd!q3pL0az$),A׋Qsz?|7"T '0m0㰵gwM)#QJ`XY MGbo7 YH4$2 N!ǣ vVϓ7yj|V%,Hu(=?5g#DUB'MbΜ9Ήo=[;~#㼉 TJXC#c H 蔨IJ-^51%Y T3. cyNoi\qhnC{mͱYVjI@"gT3K, `&IH 4q!ShGE:MU:mekD6TtI<~BARLCdÍB0ɡ1Lfo3Xza195ih4Mq1x{,4xQB#DEZA5:I0$Q6L!VҘ H̀apr. T؞ 'H)lR,UdC vcY!}b=x{*(@.PmTYK0B~g)JsiM$hR,,L-/ӴPZÊsqt ˠ" txs!PSE40M6Z <}}Xؘ;tl &19E&Aa9(e!Q58^nS,um)BA Iצn İ1vhIz]d1\T$?Nf>yv %${:WZRP{hFH4QLh̳truӲ>e:Hv,!fsTk$qDX(MoRjH"JcyA.S)/-0lf/d̵)rBE RQ9g(N Jk0Ķ4-lnX+5~V !RJ5Fxܹ:tخy>Ҋ(I]B>=:PjhV爥u0N+,sc3)1 BEfHA@ߥاA%>abvs@Hmc a )&Z&)IS1]paG1΍\ꑩsS#}lE+MEXE&A[댍W1L0 moҭoY$UT"#6w4TQiY0" HMNBG!Պ4N.8<G˓8(&Nbyq B je{7^F6HQɀ$ID%iJ22o;FHІ9t$/SHJpi#R4^>=?{KefJtZMz&/= ^ɉ1Lĵ%I*Ew*HGF=h]-Q5ML$S԰^a$% -m\$"Ji4xY8!b_U2ﲵ`kli' Jh$NBJKH`h#F ؃OHջ:;rGYz;#?xj?5FI.]5ˬ_HLbd>rJ.ߡ0Vv3w<F'IUJѤPbadaie|!(rNj>L~z?2MN}Sw|;tnNblln=2.j{;<9ffdL7Z]^kAѣGz2(:Y_Y:)T ;ƒ$A3xѨl;09I6WkEa.m1İ|>(,/|gw!OReY|[qڍwF| V^>wUsU vw˲EXaY7ŋ8ΰNp%:}$N#Fp*)/ Gu=zA LBlojyPp.Zkkw#!qc 刿] >fCĸz^kXkb&UVue`@&sĈ[SO=βBՠ06CKכ<裼ͧ8~vJy|0 Y~(]c)s~ cizQӨeib X\\xQ(ː&"籽$GNq9r(%}h*K1s|.CbR;x*N`0`NB]G%tEXtdate:create2010-12-28T22:42:29+09:00S.%tEXtdate:modify2010-12-28T22:42:29+09:00"s 1tEXtSoftwaregnome-screenshot>IENDB`jdim-0.10.1/docs/assets/thread.png000066400000000000000000000003411445721505100167250ustar00rootroot00000000000000PNG  IHDRaIDAT8͓A E<G16޴;[nM<߅BHJZčxJ/i M$feZ7$af  C/j9@-RHTݟ8 ཏuܒ& Jcfz_UP9ͨ`mqV]"S *y& Sߘ^CWK` RjIENDB`jdim-0.10.1/docs/assets/thread_old.png000066400000000000000000000004351445721505100175670ustar00rootroot00000000000000PNG  IHDRaIDAT80Dgܨ%RK\ DjID `c~DJkIv3{%3y 1-/L!|gۙ3z IOL9)2N`FıGpIENDB`jdim-0.10.1/docs/index.md000066400000000000000000000042161445721505100151040ustar00rootroot00000000000000--- title: JDim オンラインマニュアル layout: default --- # JDim オンラインマニュアル - [JDimについて]({{ site.baseurl }}/about/) - [make、実行方法について]({{ site.baseurl }}/make/) - [起動について]({{ site.baseurl }}/start/) - [OS/ディストリビューション別インストール方法][dis592] (GitHub discussions) - [datファイルのインポート、エクスポートについて]({{ site.baseurl }}/dat/) - [バックアップ、アンインストールについて]({{ site.baseurl }}/backup/) - [操作方法について]({{ site.baseurl }}/operation/) - [マウスジェスチャについて]({{ site.baseurl }}/mouse/) - [お気に入りについて]({{ site.baseurl }}/favorite/) - [外部板について]({{ site.baseurl }}/external/) - [実況モードについて]({{ site.baseurl }}/live/) - [ユーザーコマンド、リンクフィルタについて]({{ site.baseurl }}/usrcmd/) - [アスキーアート(AA)の入力について]({{ site.baseurl }}/asciiart/) - [次スレ検索について]({{ site.baseurl }}/next/) - [FAQ][wiki-faq] (JD wiki) - [Tips][wiki-tips] (JD wiki) - [開発ドキュメント][wiki-rfcs] (GitHub wiki) - [その他]({{ site.baseurl }}/info/) - [更新履歴]({{ site.baseurl }}/history/) ### 前のバージョンのマニュアル (GitHubリンク) - [JDim 0.10.1-20230723](./link-20230723) - [JDim 0.10.0-20230708](./link-20230708) - [JDim 0.9.0-20230107](./link-20230107) - [JDim 0.8.0-20220716](./link-20220716) - [JDim 0.7.0-20220115](./link-20220115) - [JDim 0.6.0-20210710](./link-20210710) - [JDim 0.5.0-20210109](./link-20210109) - [JDim 0.4.0-20200718](./link-20200718) - [JDim 0.3.0-20200118](./link-20200118) - [JDim 0.2.0-20190720](./link-20190720) - [JDim 0.1.0-20190122](./link-20190122) - [JD 2.8.9-150226][jd-289] (jd4linux) © 2019-2023 JDimproved project [dis592]: https://github.com/JDimproved/JDim/discussions/592 [wiki-faq]: https://ja.osdn.net/projects/jd4linux/wiki/FAQ [wiki-tips]: https://ja.osdn.net/projects/jd4linux/wiki/Tips [wiki-rfcs]: https://github.com/JDimproved/rfcs/wiki/rfc-index "Request for Comments" [jd-289]: https://jd4linux.osdn.jp/manual/289/ jdim-0.10.1/docs/manual/000077500000000000000000000000001445721505100147255ustar00rootroot00000000000000jdim-0.10.1/docs/manual/2006.md000066400000000000000000000743471445721505100156550ustar00rootroot00000000000000--- title: 更新履歴(2006年) layout: default --- > [Top](../) > [更新履歴]({{ site.baseurl }}/history/) > {{ page.title }} ## {{ page.title }} ### [1.8.5-beta061227](https://github.com/JDimproved/JDim/compare/b5f77c9735...f8b1874884) (2006-12-27) - 設定ファイルをjd.confに移行 - キャッシュフォルダの指定を環境変数 JD_CACHE で行うようにした - スレビューでBSキーでジャンプ元のレスに戻れるようにした - 書き込みビューでundo実装 - 書き込みビューでC-npfbdaeのショートカット実装 - スレ一覧でのマルチキーソーティング実装 - スレ一覧でのスレのブックマーク機能実装 - スレ一覧で規定のレス数を越えていて全レスを読んだスレを下にする機能実装 - 設定メニューに「フォント幅の近似計算を厳密におこなう」を追加 - クリックでポップアップウィンドウにマウスポインタを動かせるようにした - 設定メニューに「シングルクリックで多重ポップアップモードに移行する」を追加 - 板別にプロキシの設定をできるようにした - レス番号指定であぼーんできるようにした ### [1.8.1-061217](https://github.com/JDimproved/JDim/compare/1240fb4fb3...b5f77c9735) (2006-12-17) - &nbsp; をスペース(0x20)に置き換えた ### [1.8.1-rc061213](https://github.com/JDimproved/JDim/compare/f1d5ecf597...1240fb4fb3) (2006-12-13) - gtkmm-24環境でリンクの下線がずれるバグを修正 - スレビューで折り返しがあるときに描画されない時があるバグを修正 - .jdrcでhistory_size = 0とすると落ちるバグを修正 - スレ表示でボールド文字の右側が欠けるバグを修正 - 暫定的にチルトスクロールに対応 - .jdrcでtab_min_str より少ない文字数のタブの最後の文字が消えるバグを修正 - タッチパッドからの数字入力でジャンプ出来なかったバグを修正 - 折り返しの禁則処理の問題を修正 - etc.txtの一番上が表示されなかったバグを修正 - フォントによってスレの文字がずれるバグをある程度修正( strict_char_width ) - サイドバーの挙動が変だったバグを修正 - 書き込みビューに長い文字列をコピーすると落ちるバグを修正 ### [1.8.1-beta061202](https://github.com/JDimproved/JDim/compare/a2abcf37f4...f1d5ecf597) (2006-12-02) - ツリービューで選択範囲外の行をクリックしても選択が解除されないバグを修正 - margin_popup = 0 とすると画像ポップアップが表示されないバグを修正 - スレ一覧で複数(30〜40以上)のスレを選択して同時に開くと落ちるバグを修正 - ・スレ一覧で選択した行数をステータスに表示するようにした - 文字参照処理を作りなおした( spchar_tbl.h 追加) - お気に入りで列を削除した時にフォーカスを次の列に移すようにした - ツリービューで複数選択したときに選択数をステータスに表示するようにした - お気に入りでディレクトリを移動する時にフォーカスが変になるバグを修正 - ビューのフォーカスの切り替え処理部をまじめに書き直した - MISC::iconv を MISC::Iconv に変更(FreeBSDのiconv.h対策) - スレ一覧を表示するまでの時間を短縮した - 右クリックメニューや設定メニューを見直してシンプルにした - ホイールマウスジェスチャが正常に動作しない時があるバグを修正 - スレ一覧で新着スレにマークを付けるようにした( netthread.png 追加 ) - 画像ビューで強制再読み込みをしたときにアイコンの表示が変わらないバグを修正 - 起動を若干高速化 - フォーカス状態をきちんとセッション管理するようにした - アンカークリックで他スレを開くときに開いているタブの左に開くようにした - 新着があるとログを削除してもマークが消えないバグを修正 - datファイル保存を右クリックメニューに追加 - automake-1.10に対応 - configureのオプションに `--with-core2duo` 追加 - 書き込み中ダイアログを出さないようにする設定追加( hide_writing_dialog ) - 横幅が16よりも小さい画像を開くと落ちるバグを修正 - 文字の描画でPangoLayoutではなくてPangoGlyphStringを使うようにした - スレ一覧で並び替えをするとフォーカスが外れるバグを修正 - スレ一覧とスレビューで数字入力でジャンプする機能を追加 ### [1.8.0-061114](https://github.com/JDimproved/JDim/compare/6d937eb7d4...a2abcf37f4) (2006-11-14) - アクセス規制などで書き込めないときにエラーメッセージが表示されないバグを修正 - 書き込みエラー時にサーバーからのメッセージをコンソールに表示するようにした - セットアップダイアログをタスクトレイに表示するようにした - 画像の保存ダイアログでEnterキーを押して保存できるようにした ### [1.8.0-rc061108](https://github.com/JDimproved/JDim/compare/23cac32e93...6d937eb7d4) (2006-11-08) - 適当に処理していたDAT落ち判定をまじめにするようにした - HTTP304が返ると壊れているスレでも壊れているという表示がでないバグを修正 - 書き込みプレビューでレス番号をクリックしてもポップアップメニューなどが出ないようにした - 板の移転があったときにお気に入りのURLも変更するようにした - 表示しているスレを閉じるときにひとつ前のページに一瞬だけフォーカスが移るバグを修正 - スレあぼーんでNGワードでスレを全てあぼーんした後に解除すると表示が戻らないバグを修正 - 画像のドラッグスクロールが効かなくなる時があるバグを修正 ### [1.8.0-beta061103](https://github.com/JDimproved/JDim/compare/8e1022d0bf...23cac32e93) (2006-11-03) - 画像のキャッシュをまとめて消すときにビューが閉じなかったバグを修正 - ユニコード文字参照で#と;の間が数字でないときに変な文字が出てくるバグを修正 - configure.inのlibSMのサーチパスに/usr/X11R6/lib64 /usr/lib64を追加 - 書き込みウインドウにスレのタイトルを表示するようにした。 - desktop-file-utilsの使用変更に対応するためjd.specとjd.desktopを更新 - ユーザコマンドと外部板設定ファイルで空白行とコメント行を入れられるようにした - 互換板でもSETTING.TXTをダウンロードするようにした - デフォルト名無しで無いときにレスの「名前」のところをクリックしてNG名前登録できるようにした - あぼーんしたレスをクリックして透明あぼーん設定が出来るようにした - 書き込み時に名前欄に<>が含まれていると書き込みプレビューが壊れるバグを修正 - メール欄でsageの前に文字列が入っているとageとして処理されるバグを修正 - 名前の抽出とポップアップを可能にした - jd.desktopにName[ja]とComment[ja]の行を追加 - IDや名前で抽出したときに荒らし報告ように抽出したレスのURLを表示できるようにした。 - 書き込みの時に行数、バイト数をオーバーしてたら警告を出すようにした ### [1.8.0-beta061023](https://github.com/JDimproved/JDim/compare/de645ea11c...8e1022d0bf) (2006-10-23) - 書き込みウィンドウに現在の行数や最大行数などを表示した - リンクにime.nu/などが含まれているときに削除するようにした - ユーザーコマンドを設定出来るようにした - webブラウザ設定のリンク置換文字を%sから %LINKに変更(一応 %sでも置換可能 ) - 期間を指定して画像キャッシュを消せるようにした - レス番号クリックの「レスをコピー」でコピーするときにで板名やスレ名も含めるようにした - 参照コピー時に先頭に付ける文字列を変更できるようにした(ref_prefix) - タブメニューにタイトルとURLのコピーを追加 - 開く画像の最大サイズを設定できるようにした( max_img_size ) - 改行直後の空白の取扱いが変だったのを修正 - ageているレスのメール欄に色を付けられるようにした( `color_char_age_{RGB}` ) - レスあぼーんでNGワードや正規表現に改行が入っていても無視されるバグを修正 - 暫定的に書き込みログ保存機能を実装 ### [1.8.0-beta061009](https://github.com/JDimproved/JDim/compare/f610f899d5...de645ea11c) (2006-10-09) - ログイン機能実装 - 右クリック+ホイール回転ででタブ切り替え出来るようにした - 新着が透明あぼーんされているときスクロールがずれるバグを修正 - 表示メニューにサイドバー表示を追加 - サイドバーを消したまま閉じて起動したときも閉じた状態のままにするようにした - F9キーでサイドバー表示/非表示切り替え出来るようにした - 書き込みプレビューでトリップを表示出来るようにした ### [1.7.0-060927](https://github.com/JDimproved/JDim/compare/42f1cf28ec...f610f899d5) (2006-09-27) - 書き込み中表示のダイアログのラベルがフォーカスされているバグを修正 ### [1.7.0-rc060921](https://github.com/JDimproved/JDim/compare/5bd8d05ddb...42f1cf28ec) (2006-09-21) - specファイルのバージョンの付け方をFedora風にした - Fedora向けにjd.specとjd.desktop改良 - レス内容のコピーをしたときにIDの後ろに0000000という文字列が入るバグを修正 - ウィンドウサイズを変更したときに現在見ているレスがずれるバグを修正 - 板一覧をクリックすると左ペーンが閉じる時があるバグを修正 - 新規の先頭レスが透明あぼーんされていると新着セパレータが表示されないバグを修正 - 抽出結果などのタブをお気に入りにドロップすると空白行が出来るバグを修正 - オフラインのときに複数選択で板谷スレを開くとリロードアイコンが表示されるバグを修正 - 板を複数選択を開いたときに現在開いている板のフォーカスが奪われるバグを修正 - タブメニューに更新キャンセルを追加 - オートリロードモード中にオフラインモードにしてもアイコンが変わらないバグを修正 - スレ一覧をスクロールしている時に列幅を変更するとスレが開くバグを一応修正 - 画像の右クリックメニューで変だったところを修正 - 連鎖あぼーんのアルゴリズム変更(全てのアンカーがあぼーんしていたらあぼーんする) - 透明/連鎖あぼーんを切り替えると新着セパレータが消えるバグを修正 ### [1.70b.060914](https://github.com/JDimproved/JDim/compare/5718b45cb8...5bd8d05ddb) (2006-09-14) - 更新した点が多いのでバージョンを1.52ではなくて1.70とした - スクロールモードになったときにタブのスクロールボタンが効かないバグを修正 - スクロールモードになったときにタブのクリック動作が変だった問題を修正 - マウスボタン設定にDragStartButton,TreeRowSelectionButtonを追加 - 環境によってはお気に入りでフォルダとその中のアイテムを範囲選択して移動すると落ちるバグを修正 - ウィンドウサイズを変更するとタブの動作が変になるバグを(とりあえず)修正 - 板一覧でディレクトリのアイコンやラベルをクリックして開けるようにした - ツリービューで複数選択しているときに選択範囲をクリックすると全てをタブで開くようにした - ツリービューで複数選択しているときに選択範囲外をクリックすると選択解除するようにした - ツリービューで再起動したときに変な場所にスクロールするときがあるバグを修正 - ツリービューでフォーカスを外したまま再起動すると変な所がフォーカスされるバグを修正 - ステータスバーのの文字の色が薄い問題を修正(gtkmm2.6以降) - タブアイコンを非表示にできるようにした(.jdrcの show_tab_icon ) - 板一覧のメニューに「ディレクトリ内全選択」項目を追加 - スクロールモードでタブをクリックしたときタブ番号の取得に失敗するバグを修正 - タブ関係の処理の大幅なリファクタリング - タブメニューに「移動」の項目を追加 ### [1.52b.060903](https://github.com/JDimproved/JDim/compare/ca5ce41577...5718b45cb8) (2006-09-03) - 書き込みウィンドウのフォント設定を実装 - 2ch互換板のBASIC認証対応 - migemoパッチ取り込み - 株が<a href="http://2ch.se/">株</a>と表示されるバグを修正 - 名前欄の先頭に数字があるときだけアンカーにするようにアルゴリズムを変更 - 50=10のようなアンカーで=の後ろのレスが表示されなかったバグを修正 - スレビューにスレの速度と最終書き込み日時を表示 - ウインドウタイトルに開いているビューの名前を表示 - xgl環境でポップアップをクリックするとポップアップが閉じるバグを修正 - タブに表示する文字列の最小文字数設定追加( .jdrcの tab_min_str ) ### [1.52b.060827](https://github.com/JDimproved/JDim/compare/e6df057476...ca5ce41577) (2006-08-27) - gccのバージョンによってmake出来なかった問題を解決 - 履歴の文字数が多い時はカットして「...」を付けるようにした - ポップアップや抽出表示からのアンカージャンプが効かないバグを修正 - ヘルプに「2chスレ過去ログ」表示を追加 - 右クリックメニューからタブでビューを開いたときにフォーカスが移らないバグを修正 - スレ一覧のアイコンがGNOMEのテーマによっては変な形になっていたので自作して統一した - スレタイが空白のときタイトルが「壊れています」になるバグの修正 - 板一覧と右ペーンの間のセパレータをクリックして板一覧を開いたり閉じたりする機能を追加 - ftp://のリンクがhttp://として扱われるバグを修正 - URLとして認識する文字をRFCに合わせた( `[-a-zA-Z0-9!#$%&'()~=@;+:*,./?_]` ) - リンクの下線を消せるようにした( .jdrc の draw_underline ) - 板とスレの履歴を分離した - タブにアイコンを表示した - 最適化してスレ一覧からスレを開くスピードを上げた - IDの横に発言数を表示した - 全てのタブの更新機能を実装 - メニュー表示のショートカット追加( Shift+F10 及び Ctrl+m ) ### [1.52b.060803](https://github.com/JDimproved/JDim/compare/16924dcfd3...e6df057476) (2006-08-03) - あぼーんされたレスからの参照は参照数としてカウントしないようにした - 発言数にあぼーんされたレスをカウントしないようにした - 板一覧の列名を日本語に直した - 透明あぼーん実装 - 連鎖あぼーん実装 - ポップアップの最終行を範囲選択していると再描画がかからないバグを修正 - 範囲選択で画面の上下にカーソルを動かしてもスクロールしないときがあるバグを修正 - 1つのレスの中で同じレスに何回もレスしていると対象のレス番号が赤くなるバグを修正 - サーバ/スレッド/xxx,xxx,xxx という形式のアンカーをポップアップできるようにした - xxx+xxx+xxx という形式のアンカーを連結してポップアップできるようにした - treeviewでpageupとpagedownキーを有効にした。 - スレッドあぼーん実装 - スレビューのの「板を開く」ボタンをクリックしたときにリロードしないで開く機能を実装 - 「右、左のタブを閉じる」をタブメニューに追加 - 再起動時にスレ一覧やスレの復元をしてもフォーカスが移らなかったバグを修正 - リンクの最後に"がくると"と表示されてリンクされるバグを修正 - リンクの最後が()で終わっている場合はリンクにしないようにした ### [1.51.060717](https://github.com/JDimproved/JDim/compare/0d64104dd5...16924dcfd3) (2006-07-17) - スレのキャッシュを削除したときにスレ一覧のアイコンにごみが残るバグを修正 - READMEを更新した ### [1.51rc.060712](https://github.com/JDimproved/JDim/compare/efb432b073...0d64104dd5) (2006-07-12) - https://〜ではじまるリンクを開けなかったバグを修正 - あぼーんしたレスをポップアップすると assertion が出る問題を修正 - ロードした画像を削除してもリンクが赤いままになるバグを修正 - お気に入りビューで板、スレ、画像のプロパティを開けるようにした - スレ一覧で画像のプロパティを開けるようにした - スペースキーを押しっぱなしにすると再描画を繰り返して負荷が高くなるバグを修正 - 長い数列を範囲選択すると暴走するバグを修正 - タブで開くボタンの入れ替えをするとツリービューでドラッグ複数選択が出来ないバグを修正 - FreeBSDでautogen出来ない問題を応急処置で修正 - 日付のフォーマットを 01/01 01:01 みたいに2桁で揃えて0で埋めるようにした ### [1.51b.060707](https://github.com/JDimproved/JDim/compare/83119ad334...efb432b073) (2006-07-07) - 内部的にオートリロード機能を実装 - スレ一覧で「選択した行を開く」機能を右クリックメニューに追加 - スレ一覧などのツリービューでshiftクリックしても複数選択されないバグを修正 - 板、スレ一覧で中ボタンドラッグで範囲選択出来るようにした - 板一覧とお気に入りで「選択した行を開く」機能を右クリックメニューに追加 - 板一覧とスレ一覧のツリーでフォントを別々に設定出来るようにした - 設定メニューに「デフォルトでタブで開く」設定を追加 ### [1.51b.060628](https://github.com/JDimproved/JDim/compare/6e64fc528e...83119ad334) (2006-06-28) - NG Wordによるあぼ〜ん実装 - 正規表現によるあぼ〜ん実装 - あぼ〜ん関係の処理を最適化 - PrefDiagFactory追加 - スレ情報が保存されなかった時があったバグを修正 - bbsreleaseのタブが縮んだままになるバグを修正 - スレ一覧からスペースでスレを開くとWIDGET_REALIZED_FOR_EVENT assertが出るバグを修正 - ブラウザ設定をコンボボックスを使うように改良 - メニューなどの英語を日本語に直した - httpsで始まる文字列がリンクされなかったバグを修正 ### [1.51b.060617](https://github.com/JDimproved/JDim/compare/e8e9260646...6e64fc528e) (2006-06-17) - 名無しがfusianasanの板の時は名前欄が空白だと書き込めないようにした - FreeBSD対応 - NOUSE_MS932 を使わないようにした - 板のプロパティに「名前欄が空白の時は書き込まない」チェックボタンを追加 - 画像キャッシュ削除ダイアログに現在のキャッシュサイズを表示するようにした - 大昔(2000年頃)に建てられたスレを開いたときスレ一覧の既読マークが表示されないバグを修正 ### [1.51b.060612](https://github.com/JDimproved/JDim/compare/4089941b80...e8e9260646) (2006-06-12) - gtkmm24でコンパイル出来なくなったバグを修正 - GTK_CHECK_VERSION マクロを取り除いた - スレのプロパティにサイズや更新日時などを表示するようにした ### [1.51b.060611](https://github.com/JDimproved/JDim/compare/ad1b13b564...4089941b80) (2006-06-11) - 64bit対応パッチを当てた - articleviewで検索したときに再描画がかからない時があるバグを修正 - articleviewで検索対象の文字列が最後のレスに含まれているとリンク下線がずれるバグを修正 - 一度articleviewを閉じないと最初にブックマークしたレスにジャンプ出来ないバグを修正 - ダブルクリックで板やスレを開けないバグを修正 - articleviewの行間幅を真面目に計算で求めるようにした - 手動で下線や改行位置を調節できるようにした( adjust_underline_pos, adjust_line_space ) - 画像をオリジナルサイズで最初から開けるようにした( zoom_to_fit ) - 板のプロパティでhanaの確認と削除を出来るようにした - cookieを真面目に解析して作成するようにした(PON,NAME,MAILを取得) - ヘルプメニューからサポートBBSにアクセス出来るようにした ### [1.50.060601](https://github.com/JDimproved/JDim/tree/ad1b13b564) (2006-06-01) - 誤った正規表現を入力すると落ちるバグを修正 - 正式版リリース ### 1.5rc.060528 (2006-05-28) - hana仕様変更対応 ### 1.5rc.060527 (2006-05-27) - タブを中クリックで全て閉じると落ちるバグを修正 - hanaに暫定対応 - テスト項目消化 ### 1.5b.060522 (2006-05-22) - フォーカスアウト周りの処理をKDEなどの環境に合わせて変えるのを止めた
→GdkEventCrossingのmodeをみてフォーカスアウトするかどうか決定 ### 1.5b.060518 (2006-05-18) - KDE環境でリンクをクリックしても反応が無い問題を修正 - KDE環境で多重ポップアップ出来ない問題を修正 - まちBBSでレスの引用書き込みが出来なかったバグを修正 - gtk2-2.9のツリービューのパス取得の仕様が変わったせいで固まるバグを修正 ### 1.5b.060514 フィーチャーフリーズ (2006-05-14) - キーワードやIDなどを抽出するときは抽出元のスレの隣にタブを開くようにした - マウスジェスチャやショートカットキーボタンの設定を細かく出来るようにした - 名前がデフォルトの名無しの時は数字をアンカーにしないようにした - sprintfになっていた部分をsnprintfに直した - FreeBSDでhistoryがセグフォ出していた問題を修正 - 書き込み確認のダイアログを表示するようにした( always_write_ok ) - 書き込み中に「書き込み中」と表示するようにした - 書き込みがタイムアウトしたら再読みしろというダイアログを出すようにした - FreeBSDでもautogen.shが通るようにした - 履歴を板とスレで分けた - 履歴の保持数を設定できるようにした( history_size ) - 板一覧でカテゴリ全体をまとめてお気に入りに登録できるようにした - (追加変更した操作) - タブをダブルクリック : 再読み込み - ダブルクリック : 画像再読み込み - alt + q : 書き込みをキャンセル ( alt + c から変更 ) ### 1.5b.060415 (2006-04-15) - configure.inを修正 - libgnomeuiでは無くてXSMPをデフォルトで使用するようにした - ポップアップ中にマウスホイールを回したらポップアップの方をスクロールさせるようにした - ポップアップとカーソルの間のマージンを設定出来るようにした( margin_popup ) - 板一覧でカテゴリをひとつだけしか開かないように設定可能にした( open_one_category ) - メニューにショートカットキーを表示するようにした - 設定ファイル(keyconf.xml)でキーバインディングを設定出来るようにした - (追加変更したショートカットキー) - alt + c : 書き込みをキャンセル - alt + w : 書き込み実行 - F3 : 新着へ移動が次検索と被っていたのでF4に変更 ### 1.5b.060401 (2006-04-01) - gnomeセッションを終了時にセッション情報を保存するようにした(要libgnomeui-devel) - configure.in.vineを消してconfigure.inひとつに統一した - コンパイルオプションに-Wallを付けて出てきた警告を全部消した ### 1.5b.060319 (2006-03-19) - 自前のツールチップの表示部を作りなおした(背景色をテーマに合わせるようにした) - タブの色がテーマによっては変になるバグを修正 - 「123-124」みたいなアンカーがあるとき、半角の「-」も認識するようにした - いつの間にかポップアップでスクロールバーが出なくなっていたので修正 - SIGINTやSIGHUPを受け取ったときにお気に入りをバックアップするようにした(テスト不十分) - ローダがget_addrinfoに失敗すると落ちるバグを修正 ### 1.5b.060311 (2006-03-11) - 他のスレをポップアップした直後にそのスレを消して再ポップアップすると落ちるバグを修正 - ロード開始直後にすぐスレを閉じると落ちるバグを修正 - キャッシュのルートディレクトリがうまく設定できないバグを修正 - 正規表現クラス実装 - rawモード読み込み対応(?) - まちBBSの読み書きに対応(スレ立てはテスト出来ないので未実装) - bbsrelease,board,articleビューで正規表現による抽出・検索機能実装 - 範囲指定のみでXのバッファに文字列をコピーするようにした - 多重起動防止実装( ロックファイルは (キャッシュルート)/JDLOCK ) - ファイルローダでsendしたときのエラーチェックがいい加減だった問題を修正 - zlib1.1系に対応(-DUSE_OLDZLIB) - その他ファイルローダの安定化 - articleビューでスクロール時の負荷を大幅に減らした - HTMLパーサを少しだけ最適化 - リンクの上を範囲選択してマウスを上げると開いてしまうバグを修正 - オートスクロール中にキーを押すとスクロールが止まるバグを修正 - 画像インジケータのアイコン画像の比率が変だったバグを修正 - 板別に最後にソートした列を記録するようにした - windowを最大化/最大化解除したときにタブの幅を調整するようにした - jd.spec, jd.png、jd.desktopを付属( thanks to 189, 190氏 ) - その他細かい修正 ### 1.5b.060219 (2006-02-19) - ファイルロード時にconnectに失敗していてもsendしていた問題を修正 ### 1.5b.060214 (2006-02-14) - アイコンバーのアイコンを並び替えるとスクロールが変になるバグを修正 - 画像のロード中に画像保存ダイアログを開くとロードが停止するバグを修正 - 名前欄のトリップの中の数字をリンクしないようにした - Fileメニューにお気に入り保存を追加 - (追加、変更したショートカットキー、マウスジェスチャ) - F3 : 新着へ移動 - →↓→ : 新着へ移動 - z: 元の画像サイズ - x: 画像を画面にフィットさせる - +: 画像の拡大 - -: 画像の縮小 - c: 画像のモザイク解除 ( x から変更、gthumbに合わせるため ) ### 1.5b.060207 (2006-02-07) - ファイルローダで一度タイムアウトすると再読み込みしなくなるバグを修正 - 縦3paneモード実装 ### 1.5b.060205 (2006-02-05) - subject.txtが壊れていると落ちるバグを修正 - ファイルローダでsend時にタイムアウトしているのにエラーチェックを素通りしていたバグを修正 - ファイルローダをhttps対応にした - ファイルロード中にロード停止したときの反応を良くした - Gtk::Stock::MEDIA_RECORDを使うのを止めた ### 1.5b.060204 (2006-02-04) - コードを大幅整理(特にスレ標示部) - ユニコード文字参照に対応 - 書き込み中にarticleビューの書き込みボタンを押してもmessageを閉じないようにした ### 1.5b.060115 (2006-01-15) - 履歴をクリアした後板を開くと落ちるバグを修正 - ツリービューのスクロール処理を自前でするようにした( .jdrcのtree_scroll_sizeで調整 ) - ダブルクリックで文字列を範囲選択出来るようにした ### 1.5b.060113 (2006-01-13) - GLIBのスレッド初期化忘れを修正 - コンパイルオプションに -DNOUSE_MS932 を与えるととCP932で文字コード変換するようにした - iconvでMS932からUTF8にマッピング出来なかった文字を□に変換するようにした - 起動時のimageの復元機能実装 - SETTING.TXTのローダ作成 - 操作性向上(主にタブ操作、画像操作まわり) ### 1.5b.060106 (2006-01-06) - favoriteで名前が変更出来ないバグを修正 - 画像を保存した後に画像をダウンロードすると固まるバグを修正 - 起動時のboard,articleの復元機能実装 - history機能実装 - 文字参照の一部を実装(thinsp, ensp, emsp, etc.) ### 1.0-060101 (2006-01-01) - リリース jdim-0.10.1/docs/manual/2007.md000066400000000000000000000561451445721505100156520ustar00rootroot00000000000000--- title: 更新履歴(2007年) layout: default --- > [Top](../) > [更新履歴]({{ site.baseurl }}/history/) > {{ page.title }} ## {{ page.title }} ### [1.9.8-071228](https://github.com/JDimproved/JDim/compare/27d140cab7...4c35cda400) (2007-12-28) - お気に入りでアイテムが消えない時があったバグを修正 ### [1.9.8-rc071223](https://github.com/JDimproved/JDim/compare/23c7db918f...27d140cab7) (2007-12-23) - テーマによってはLabelの背景色が変になるバグを修正 - バージョン2.4のgtkmmでビルド出来なかった問題を修正 - 旧設定ファイル(.jdrc)が存在するとキャッシュディレクトリが出来ない時があるバグを修正 ### [1.9.8-beta071218](https://github.com/JDimproved/JDim/compare/5f25d7d12a...23c7db918f) (2007-12-18) - テーマによってはLabelEntryの背景色が変になるバグを修正 - dat落ちしたスレを開いたとき、タブに「???」と表示するようにした - ウィンドウのリサイズ直後にスレの範囲選択の位置がズレる時があったバグを修正 - レスにUTF-8直書きされているときにスレが壊れていると判定されるバグを修正 - スレの連鎖あぼーん設定のチェックが再起動すると外れるバグを修正 - テーマによってはタブの色が変になるバグを修正 ### [1.9.8-beta071210](https://github.com/JDimproved/JDim/compare/c48d699a18...5f25d7d12a) (2007-12-10) - RH系のブラウザをhtmlviewからxdg-openに変更した - メッセージダイアログでtabでラベルにフォーカスが移るバグを修正 - リロード後に新着に自動的に移動する設定追加( jump_new_after_reload ) - key.confに Scroll\*Board と Scroll\*Image を追加 - 起動時に画像ウィンドウも復元するとスレビューが表示されなかったバグを修正 - スレビューでn,pで前後のレスに移動できるようにした - 書き込みビューのファイル選択画面でキャンセルを押すとビューが閉じるバグを修正 - ログ検索中に他の検索ビューを閉じると検索が終了するバグを修正 - タブを中クリックで閉じる動作をキャンセルできるようにした - ポップアップフレームとオートスクロールマーカの色を設定できるようにした - お気に入りの最終行がディレクトリの時に最終行へのジャンプが正常に動作しないバグを修正 - リンクの上でコンテキストメニューを開いてからマウスを移動してクリックするとリンクが開くバグを修正 - デフォルトではOpenSSLではなくてGNUTLSを使用するようにした( `--with-openssl` でOpenSSL使用 ) - テーマを変更したときにスレビューのラベルの背景色が変わらなかったバグを修正 ### [1.9.7-071122](https://github.com/JDimproved/JDim/compare/0b62eec74e...c48d699a18) (2007-11-22) - configureに `--with-athlon64` オプションを追加 - $LANGが取得出来なかった場合に落ちるバグを修正 - SVN版で作業コピーでない場合にコンパイル出来なくなっていたバグを修正 ### [1.9.7-rc071115](https://github.com/JDimproved/JDim/compare/e80cdb065c...0b62eec74e) (2007-11-15) - 画像ビューのメニュー項目の一部にマウスジェスチャが表示されていなかったバグを修正 - 板一覧にフォーカスがあるときに板一覧再読み込みするとサイドバーが閉じるバグを修正 - 起動時にjd.confが読み込めない時はjd.confが存在するか確認するようにした - スレビューで他スレをポップアップ表示したあとキャッシュ削除してもポップアップ表示されるバグを修正 - 「画像を全て保存」でモザイクを外していない画像まで保存していた問題を修正 - お気に入りで ディレクトリを削除するとキーボードフォーカスが外れるバグを修正 ### [1.9.7-beta071109](https://github.com/JDimproved/JDim/compare/bbafc44f4d...e80cdb065c) (2007-11-09) - 「画像ポップアップを表示する」設定を追加 - スレビューでリロード後に末尾に飛ぶ設定を追加( jump_after_reload ) - スレビューの検索に「WEB検索」を追加( menu_search_web, url_search_web ) - 画像ビューを開いたまま閉じて再起動するとビューが展開されずに落ちるバグを修正 - 起動中やスレ表示前にスレビューの上をマウス移動すると落ちるバグを修正 - ツールバー(リスト)項目設定のダイアログを充実させた - 画像関係の表示メニューを設定メニューから表示メニューに移した ### [1.9.7-beta071101](https://github.com/JDimproved/JDim/compare/447c79aaeb...bbafc44f4d) (2007-11-01) - BMP画像表示に対応 - スレタイ検索機能実装( menu_search_title, url_search_title, regex_search_title ) - BEログイン実装 - BEのマーク表示をするようにした - メインツールバーと各ビューのツールバーの項目を編集出来るようにした - 起動、シャットダウンを高速化した - 板一覧を開くときにリダイレクトされたら移転のチェックをするようにした - メインメニューにキーアクセラレータとショートカットを追加 - 「JDの動作環境」の一部に「/etc/issue.net」値を取得して表示するようにした - 書き込みプレビューでURLに含まれる「&」が「&amp;」と表示されてしまう問題を修正 - 「IPv6使用」設定をメニューに追加 ### [1.9.6-071005](https://github.com/JDimproved/JDim/compare/3edeb2457e...447c79aaeb) (2007-10-05) - 書き込みメッセージを保存した際にダイアログが再び出てしまうバグを修正 - ログ検索中にビューを閉じても検索スレッドが停止しないバグを修正 ### [1.9.6-rc070930](https://github.com/JDimproved/JDim/compare/790ee740da...3edeb2457e) (2007-09-30) - お気に入りで新規ディレクトリの追加が出来なかったバグを修正 - 板一覧とお気に入りで前検索すると固まることがあるバグを修正 ### [1.9.6-beta070918](https://github.com/JDimproved/JDim/compare/b8b13b78ac...790ee740da) (2007-09-18) - スレ一覧の表示項目を設定できるようにした - 非アクティブのときに書き込みウィンドウを折り畳むようにした - GNOME環境でウィンドウを閉じたときに最大化が時々勝手に解除されるバグを修正 - 各ビューで全て選択(Ctrl+a)実装 - 書き込みビューのコンテキストメニューにAA入力の項目を追加 - 書き込みビューを閉じるときに編集中の文章を保存できるようにした - 画像あぼーんの情報を削除できるようにした - 画像ウィンドウでお気に入り追加ダイアログを開くとウィンドウが畳まれるバグを修正 - アンカーのコンテキストメニューでジャンプが動作しなくなっていたバグを修正 - 全お気に入り更新チェックを追加 - メインメニューに「ツール」の項目を追加 - 履歴メニューの項目もEnterキーを押してアクティブに出来るようにした - aboutダイアログ追加( パッチスレ71 ) - button.confでDbl\*の設定が効かなかったバグを修正 - スレ一覧のログ検索ボタンを削除してツールメニューにログ検索移動 - 発言数のチェックを外すオプション追加( check_id = 1 ) - スレをお気に入りに登録したときに自動でブックマーク設定するようにした - レス参照で色を変える回数を設定できるようにした( num_reference_high=3, num_reference_low=1 ) - 発言数で色を変える回数を変更できるようにした( num_id_high=4, num_id_low=2 ) - 画像のピクセル数が大きいときは警告するようにした( max_img_pixel = 20 ) - タブのロック機能を実装 - メインメニューからJDホームページの項目を削除(aboutダイアログから可能) - 書き込みビューのコンテキストメニューに「クリップボードから引用」の項目を追加 - 書き込みビューのコンテキストメニューに「JDの動作環境を記入」の項目を追加 - バージョン表示でSVN版のリビジョン番号を表示できるようにした - スレの再取得をするときにあぼーんなどのスレ情報を削除しないようにした - スレがDAT落ちしているときにラベルの色を青くするようにした - 設定メニューに板、スレ、画像プロパティ項目追加 - タブメニューに「プロパティ」追加 - 表示設定からタブの非表示が出来るようにした ### [1.9.6-beta070804](https://github.com/JDimproved/JDim/compare/e8886a7b89...b8b13b78ac) (2007-08-04) - 更新チェック機能実装 - 更新チェックのタイムアウト値の設定追加(loader_timeout_checkupdate) - スレが壊れているときにラベルの色を赤くするようにした - スレ一覧のポップアップが表示されないときがあるバグを修正 - お気に入りでディレクトリ以外の行でも左キーで開いているフォルダを畳めるようにした - migemo検索の高速化パッチを適用 - スレ一覧でインクリメンタル検索が出来るようにした ( inc_search_board = 1 ) - メニュー項目でダイアログを開く行に「...」を付けるパッチを当てた - 板を開いたまま再起動時するとSETTING.TXTを勝手にロードするバグを修正 - SETTING.TXTの取得時に更新時刻を送るようにした - 板のプロパティにローカルルールを表示 - 画像ビュアーで302の画像もダウンロードするようにした - 画像のボタン設定追加(CloseImageButton,ScrollImageButton,CancelMosaicButton) - 画像の拡張子と実際の形式が違うときはモザイク解除の時に警告するようにした ### [1.9.5-070630](https://github.com/JDimproved/JDim/compare/123e99532a...e8886a7b89) (2007-06-30) - AAメニューでカーソル上下で誤動作するバグを修正 ### [1.9.5-rc070625](https://github.com/JDimproved/JDim/compare/a77ef76cdc...123e99532a) (2007-06-25) - メール指定が無いときの名前欄の文字色を選択できるようにした - 画像まわりの細かいバグをまとめて修正した ### [1.9.5-beta070616](https://github.com/JDimproved/JDim/compare/24bb552ecd...a77ef76cdc) (2007-06-16) - bbsmenu.html内にあるリンクは全て板とみなす設定追加( use_link_as_board ) - アニメーションGIFの表示処理を軽減化 ### [1.9.5-beta070611](https://github.com/JDimproved/JDim/compare/fe2fcf21a3...24bb552ecd) (2007-06-11) - XML処理の変更に伴い、\*.xmlファイルの書式をXMLの仕様に従うように変更 - BackspEditコントロール設定を追加 - 書き込みビューに最終書き込み日を表示 - 連続書き込み規制(書き込みビューに再書き込み可能になるまでの時間を表示) - スレビューで再読込ボタンで全タブを更新する機能追加( reload_allthreads ) - JD ホームページのアドレスを設定できようにした( url_jdhp ) - 画像の横幅や縦幅が長い場合に落ちるバグを修正 - 画像のファイルサイズが0byteの時に画像ビューが落ちるバグを修正 - AAメニューの履歴数を変更できるようにした( aahistory_size ) ### [1.9.5-beta070528](https://github.com/JDimproved/JDim/compare/91febcb109...fe2fcf21a3) (2007-05-28) - 右ペーンが空の時にサイドバーを最大化する設定(expand_sidebar)追加 - ポップアップと画像カーソルの間のマージン設定(margin_imgpopup)追加 - スレビューの削除メニューなどに「削除して再読み込み」を追加 - スレを再読み込みして再起動するとスレ一覧の新着がマイナスになる問題を修正 - 水平タブ(0x09)が文字化けする問題を修正 - スレビューで上下キーのスクロール速度を調整可能にした( key_scroll_size ) - スレ内で検索をしたときにヒット数を表示するようにした - configureのオプションに `--with-migemodict` を追加 - 検索用補完Entryクラス実装 - 最近閉じたスレの履歴を実装 - 設定にプライバシーメニュー追加 ### [1.9.5-beta070516](https://github.com/JDimproved/JDim/compare/96442f1e96...91febcb109) (2007-05-16) - スレビューの描画エンジンを最適化 - 新着しおり(ここまで読んだ)をブロック要素化 - css対応( jd.css ) - 画像表示関係を高速化 - レススキン対応( Res.html ) - インライン画像実装 - インライン画像のon/off設定をメニューに追加 - お気に入りが空のとき最後に移動すると異常終了するバグを修正 - 板レベルのあぼーん設定を追加 - 画像キャッシュクリアを高速化 - 画像キャッシュ削除中にダイアログを出してキャンセル可能にした - 画像プロパティにサイズやキャッシュ保護のチェックなどを追加 - スレッドのデフォルトスタックサイズを減らした - 画像あぼーん実装 - 履歴右クリックでポップアップ表示して特定の履歴を削除出来るようにした - Res.htmlにNAMELINKを追加(「名前」に置換する) - IDあぼーんしたら板の方にも自動で登録するようにした - タブ右クリックメニューの複数のタブを閉じる機能をまとめた - スレ一覧とスレビューのツールバー表示/非表示設定追加 - 板一覧とスレ一覧のテキストカラー変更設定追加 - 板一覧とスレ一覧の偶数行の色設定追加 - 画像ポップアップのマージン設定追加( jd.css の .imgpopup の margin ) - サイドバーの表示切り替えの挙動がおかしかった問題を修正 - bbsmenu.htmlの解釈がおかしかった問題を修正 - スレのプロバティの番号あぼーんでマイナスの数を入れると落ちるバグを修正 - スレのプロバティの番号あぼーんで12-34のような書式に対応した ### [1.8.8-070403](https://github.com/JDimproved/JDim/compare/fb2b7e66c7...96442f1e96) (2007-04-03) - スレタイトルに特殊文字が入っているときにデコードされてなかったバグを応急処置で修正 ### [1.8.8-rc070330](https://github.com/JDimproved/JDim/compare/cb3339cf9c...fb2b7e66c7) (2007-03-30) - 移転処理時にスレビューに対象板内のスレを開いていると再読み込み出来なくなるバグを修正 - 書き込みプレビュー画面でCtrl+g(検索)を押すと落ちるバグを修正 - フォントの詳細設定ダイアログに行高さ調整の項目追加 - フォントの詳細設定ダイアログに下線位置調整の項目追加 - 書き込みエディタの文字や背景色を変更できるようにした - スレビューでShift+キーにスクロール機能を割り当てると2回スクロールするバグを修正 - 細かいバグの修正 - その他微調整 ### [1.8.8-beta070324](https://github.com/JDimproved/JDim/compare/8726987d2c...cb3339cf9c) (2007-03-24) - 画像ウィンドウ表示時に画像の参照元レスを開いてもウィンドウが閉じないバグを修正 - 画像ウィンドウ表示時にマウスジェスチャが表示されないバグを修正 - 画像ウインドウが表示される前にqを連打してウィンドウを閉じると落ちるバグを修正 - KDE環境で画像ウィンドウ上でダイアログを閉じるとフォーカスが外れるバグを修正 - GIF画像の読み込みに失敗すると落ちるバグを修正 - 強制画像再読み込みをしてもアイコン表示が変わらないバグを修正 - 埋め込み書き込みビューがフォーカスされてもURLとタイトルが変わらないバグを修正 - 埋め込み書き込みビューを開いたまま再起動するとフォーカスが外れるバグを修正 - スレ一覧の読み込み中にスレを読んでいると読み込み終了時にスレ一覧に戻るバグを修正 - スレビューにフォーカスがあるときはスレビューをクリックしても再描画をかけないようにした - AAポップアップメニューにAAが表示されないときがあるバグを修正 - セットアップウィザード追加 - 初起動直後の初期フォントをシステムで利用可能な物から選ぶようにした ### [1.8.8-beta070317](https://github.com/JDimproved/JDim/compare/ea2c522ed6...8726987d2c) (2007-03-17) - ヘルプファイルをXML化した( thank to tamagodake氏) - フォント、色詳細設定ダイアログ追加( thank to tamagodake氏) - スレ一覧の速度の計算アルゴリズムを変えた - Debian GNU/kFreeBSD に configure.inを対応した - ダイアログボックスのフォーカス処理をきちんとするようにした - 書き込みビューを開いているときにスレビューの「レス」「参照してレス」、および「引用してレス」 - を選択すると書き込みビューに引用できるようにした( thank to tamagodake氏) - 書き込みビューに「書き込み後に閉じない」ボタン追加 - メニュー項目の見直しと整理 - utf8環境で起動していないときは警告を出すようにした( thank to tamagodake氏) - メニューバーの表示設定追加(F8) - スレビューでのホイールスクロール速度設定追加( jd.confのscroll_size ) - RFC規定外の文字(^など)もURL判定に用いる設定追加( jd.confのloose_url) - ロックファイルにPIDを保存して起動済み判定を厳密にした - 書き込みビューのタブを取り除いてプレビューボタンを追加した - ローダ実行中にあるタイミングで別のローダを起動すると落ちるバグを修正( class Dispatchable 追加 ) - ツリービューの行間スペース設定追加( jd.confのtree_ypad ) - 書き込み先がスレビューで開いているスレと異なるときは警告を出すようにした - ツリービューのハイライト色の設定追加 - make時にx86_64で警告が出る問題を修正( thanks to 180氏) - ipv6使用オプション追加( jd.confのuse_ipv6) - 書き込みビューにファイル挿入ボタン追加 ### [1.8.8-beta070218](https://github.com/JDimproved/JDim/compare/62b73373b8...ea2c522ed6) (2007-02-18) - 表示されていないビューの切り替えボタンやメニューは非アクティブにした - 「選択範囲を引用してレス」を右クリックメニューに追加 - 2pane, 3paneモードの切り替えなどを再起動無しでできるようにした - ツールバーの位置を再起動無しで切り替えられるようにした - 複数のスレを表示しているときにウィンドウのリサイズを高速化 - 3pane表示のときに開いていないビューを隠すようにした - 埋め込み書き込みビュー実装 - 画像ウィンドウ分離表示機能実装 - 板移転時にメッセージのjavascriptを解析して移転情報を更新する機能を実装 - ユーザコマンドに$DATURLと$LOCALDATを追加 - http://info.2ch.net を板として認識していたバグを修正 - idpopup_by_mo, refpopup_by_mo,namepopup_by_mo 設定追加 - 名前欄のbタグで囲まれたトリップなどの文字列に色を付けられるようにした( thank to tamagodake氏) ### [1.8.5-070203](https://github.com/JDimproved/JDim/compare/a2f7992fe5...62b73373b8) (2007-02-03) - 参照文字に&yen;追加 - スレ一覧を全部閉じてから板一覧を閉じてもスレにフォーカスが移らないバグを修正 - 初回起動時に板一覧ではなくてお気に入りにフォーカスがあるバグを修正 - ビューの切り替え方法を説明したダイアログを表示するようにした - 移転情報テーブルが循環しているとお気に入りのアドレスが空白になるバグを修正 ### [1.8.5-rc070121](https://github.com/JDimproved/JDim/compare/8fc07bac6a...a2f7992fe5) (2007-01-21) - 書き込みビューでCtrl+n,pでカーソルを移動した時にスクロールしないバグを修正 - 書き込みプレビューで画像をダウンロードしてもリンクの色が変わらないバグを修正 - 書き込みプレビューで画像を削除してもリンクの色が変わらないバグを修正 - 設定メニューに「書き込みビューでEmacs風のキーバインドにする」の項目を追加 - 2.4以下のgtkmmでスピンボックスの値が変わらなかったバグを修正 - 板一覧とお気に入りをコンボボックスで切り替えられるようにした - ツリービューの背景色設定にgtkrcを用いる設定を追加( use_tree_gtkrc ) - 画像ビューを開かないようにする設定を追加( use_image_view ) - 選択不可のユーザコマンドを非表示にする設定追加( hide_usrcmd ) - 指定した数( max_show_usrcmd )以上の時はサブメニュー表示にする設定追加 ### [1.8.5-beta070114](https://github.com/JDimproved/JDim/compare/f8b1874884...8fc07bac6a) (2007-01-14) - 生のdatファイルに\0などの制御文字が含まれていると表示が壊れるバグを修正 - スレ一覧のマークでのソーティングアルゴリズムを変更 - キャッシュのログ検索を暫定的に実装 - ツールバーにサイドバーの切り替えボタンを付けた - 移転処理した時に移転した板を表示するダイアログでTextViewを使用した - 移転処理をしたときにSETTING.TXTをロードしなくなる時があるバグを修正 - 移転したときに履歴メニューのURLが変更されていなかったバグを修正 - 外部板ビューを廃止して外部板は板一覧の先頭に表示するようにした - 板一覧とお気に入りをサイドバー形式に変更 - スレ一覧、スレ表示、画像タブを廃止 - ビュー切り替えボタンをツールバーに追加 - ツールバーの位置を切り替えられるようにした - 書き込み時のデフォルトの名前とメールアドレスを板別に設定できるようにした - 書き込みビューにundoボタンを付けた - 画像ビューでキーボードでスクロール出来るようにした - チルトボタンの設定を button.conf に追加 jdim-0.10.1/docs/manual/2008.md000066400000000000000000000426611445721505100156510ustar00rootroot00000000000000--- title: 更新履歴(2008年) layout: default --- > [Top](../) > [更新履歴]({{ site.baseurl }}/history/) > {{ page.title }} ## {{ page.title }} ### [2.1.0-081228](https://github.com/JDimproved/JDim/compare/d078f67593...65130226e7) (2008-12-28) - 動作環境にconfigureオプションを表示するようにした - 「JDについて」のダイアログに動作環境を追加した - 鬼車を使用可能にした( `./configure --with-oniguruma` ) - ユーザープロフィールの様に途中でhttp://が入っているアドレスが板として認識されるバグを修正 - 次スレ検索時にしおりが付いているスレの次スレにもしおりを付けるようにした - about:configに「スレをお気に入りに追加した時にしおりをセットする」設定を追加 - お気に入りに登録されているスレのキャッシュを削除した時にお気に入りも削除するようにした - ログアウト時にタイミングによってjd.confが壊れるバグを修正 - プロキシのホスト名にhttp://などが付いているとロード出来ないバグを修正 - 実況等で書き込みと読み込みのタイミングによって書き込み判定に失敗するバグを修正 - 一行目の改行前にスペースを沢山入れた書き込みの書き込み判定に失敗するバグを修正 ### [2.1.0-beta081216](https://github.com/JDimproved/JDim/compare/2c244310aa...d078f67593) (2008-12-16) - 次スレ検索機能を実装 - 次スレ移行時にお気に入りの名前とアドレスも更新するようにした - お気に入りビューの更新マークを再起動しても消えないようにした - お気に入り項目の編集直後に板かスレを開いたときにお気に入りの状態を保存するようにした - proxyのbasic認証に対応した - お気に入り挿入先の選択ダイアログを作り直した - お気に入りのショートカット(Ctrl+d)追加 - スレビューで適当なダイアログを開いて閉じた後にスクロールが変になるバグを修正 - 書き込みログ表示で再読み込みをすると空白のビューが表示されるバグを修正 - 板一覧の読み込みに失敗した時にダイアログを表示するようにした - お気に入りで空白行にもコメントを入れられるようにした - お気に入り編集ウィンドウを作成( ツールメニューに「お気に入りの編集」を追加 ) - 適当にやっていたDnD処理をきちんと作り直した - ダイアログ上でスレッドからのメッセージがDispatchされない問題をタイマーを使って解決した - squid3経由でスレを差分ダウンロードするとtimeoutする問題を解決(Content-Lengthを考慮した) - スレの差分ダウンロード時にプロキシがrangeを無視して先頭からdatを送って来る場合に対処 ### [2.0.3-081124](https://github.com/JDimproved/JDim/compare/dda4a740a5...2c244310aa) (2008-11-24) - 書き込み履歴を削除したときに、固定書き込み名とメールも消去するようにした。 ### [2.0.3-rc081117](https://github.com/JDimproved/JDim/compare/eef78753fb...dda4a740a5) (2008-11-17) - 「ここまで読んだ」が表示されていると「リロード後に新着レスに自動的に移動」が効かない問題を修正 - gcc4.4対応 - 更新チェック時にスレビューのタブのアイコンが正しく更新されない時があるバグを修正 ### [2.0.3-beta081110](https://github.com/JDimproved/JDim/compare/7062bd297d...eef78753fb) (2008-11-10) - 書き込みビューでTabキーで書き込みボタンがフォーカスされなかったバグを修正 - configureのオプションに「`--with-ppc74x0`」オプションを追加 - gtkmm-2.4 でコンパイルが通るようにした - uimの文字変換中にCtrl+qを押すとキーアクセレートが優先されてJDが終了するバグを修正 - boards.xmlの中に同じ板のアドレスが多重登録されていると起動時に落ちるバグを修正 - FreeBSDの場合はtimegmの代わりにmktimeを使うようにした。 - board.infoが空ファイルの時に落ちるバグを修正 - 復元しない設定で画像ビューを開いたまま閉じると再起動時に画像ビューが表示されるバグを修正 - モザイクのかけ具合をabout:configで調整できるようにした。 - vip2chの読み書きに対応した - ショートカットキー設定をGUI化した - マウスジェスチャ設定をGUI化した - ボタン設定をGUI化した - Beログイン時にBeアイコンが表示されるため自分の書き込みの判定に失敗するバグを修正 - Beアイコンを表示出来るようにした - アクセシビリティをonにすると画像削除でフリーズするバグを修正 - navi2ch互換スレ情報ファイルが壊れているとJDが落ちるバグを修正 - スレのプロパティに書き込み履歴クリアボタンを追加 - 板のプロパティに板内の全スレの書き込み履歴クリアボタンを追加 - プライバシー情報の削除に書き込みログの削除と書き込み履歴削除を追加 - スレビューが縦に非常に長い時に下の方にあるリンクがクリック不可になるバグを修正 - 設定メニューに書き込み履歴(鉛筆マーク)を保存しない設定を追加 - サイドバーのツールバーに「お気に入り更新チェックボタン」を追加出きるようにした - 色の詳細設定を表形式にした - 画像ビューで画像サイズをウィンドウの横幅のみに合わせられるモードを追加(xキーでモード切り替え) - BASIC認証有りの外部板のSETTING.txtとhead.txtのロード時も認証するようにした ### [2.0.2-080919](https://github.com/JDimproved/JDim/compare/d020f01212...7062bd297d) (2008-09-19) - 書き込み時にhana=mogeraやsuka=pontanの様なキーワードをきちんと取得するようにした - レス中の「<hr>」にスキンが適用されていなかったバグを修正 ### [2.0.1-080914](https://github.com/JDimproved/JDim/compare/65dc5275dd...d020f01212) (2008-09-14) - デフォルトフォントにMona-VLGothicを追加 - セットアップウィザードにペイン設定を追加 - スレ削除、板移転確認ダイアログに「表示しない」のチェックボタンを追加 - configureのオプションに「`--with-xdgopen`」を追加 - configureのオプションに「`--with-atom`」を追加 ### [2.0.1-beta080901](https://github.com/JDimproved/JDim/compare/55d6e4b5a7...65dc5275dd) (2008-09-01) - ユーザコマンドのGUI設定と階層化を実装 - jd.confのget_max_show_usrcmdを廃止 - 板移転時に落ちるときがあるバグを修正 - 他スレポップアップ時に落ちる時があるバグを修正 - ダウンロードしたファイルが画像でないときに Content-Typeを表示するようにした - 起動中にスレビューの余白の上でマウスを動かすと落ちるときがあるバグを修正 - Auroraなどの特定のテーマでスレ一覧のスクロールバーが描画されない時があるバグを修正 - リンクフィルタ機能実装 - スレ一覧で前回開いた時点以降に立てられたスレをアイコン表示するようにした - about:configに「お気に入りでカテゴリを常にひとつだけ開く」を追加 - スレのプロパティに更新日時のクリアボタンを追加 - ubuntuなど環境によってはプロパティの更新日時表示が正しくなかったバグを修正 - レスの引用コピーでアドレスもコピーするようにした。 - 書き込みビューに長い文章をコピペすると落ちるバグを修正 - 書き込みビューで改行や特殊文字の文字数を考慮するようにした - key.confでExecWrite = Shift+Enterとして書き込みをすると改行が入るバグを修正 - スレビューの検索、スレ一覧、板一覧の検索の補完記録を分離した - about:configに「3ペーン時にスレ一覧やスレビューを最大化する」を追加 - ユーザコマンドにBBSNAME,DATNAME,HOST,HOSTL,TITLE,BOARDNAME,LOGPATH,NUMBER 追加 - ユーザコマンドにCACHEDIMG,DIALOG,ONLY 追加 - ユーザコマンドにTEXTI, TEXTIU, TEXTIX, TEXTIE, INPUT, INPUTU, INPUTX, INPUTE 追加 - 外部ブラウザを用いてp2経由で書き込む機能を実装 - 画像ダウンロード時にリダイレクトが起きると反応が無くなる時があるバグを修正 - 2chログイン中は書き込み秒数規制をしないようにした - スレビューや画像ビューでエラー画像のエラーコードも削除できるようにした - まちBBSでread.cgi形式のアドレス表記に対応した - 多重ポップアップに移行するときフォーカスが外れるとポップアップが閉じるバグを修正 - BEにログインしているときは書き込み規制秒数ダイアログを表示しないようにした - solarisでもソースの修正無しでコンパイルが通るようにした ### [2.0.0-080722](https://github.com/JDimproved/JDim/compare/d6de55cff3...55d6e4b5a7) (2008-07-22) - 閉じるボタンを押してビューを閉じた後に再び開くとボタンの枠が表示されたままになるバグを修正 - テンキーの「Home」や「Pg Dn」などでも操作出来るようにした - about:configに「自前でウィンドウ配置を管理する」を追加 ### [2.0.0-rc080714](https://github.com/JDimproved/JDim/compare/affc455059...d6de55cff3) (2008-07-14) - 検索バーをescapeで閉じるようにした。 - 非表示の状態からサイドバーをドラッグして表示させるとXの閉じるボタンを押しても閉じないバグを修正 - 画像と書き込みビューを埋め込み表示にすると画像を表示した時に書き込みツールバーが現れるバグを修正 - 書き込みプレビューボタンをトグルキーにした - 自動板移転処理後に板一覧を更新すると自動移転処理できなくなるバグを修正 - 板のプロパティに更新日時の表示とリセットを追加 - 書き込みビューが画面の下側にあるときにAAポップアップの表示領域を広げた - お気に入り登録ダイアログをqやESCキーで閉じられるようにした - テーマによってはツールバーが描画されないバグを修正 - compiz環境で起動する度にウィンドウが右下にずれていくバグを修正 - gtkrcを利用してツリービューの背景色を描画する時に偶数行が描画されないバグを修正 ### [2.0.0-beta080702](https://github.com/JDimproved/JDim/compare/ccf7bf5f92...affc455059) (2008-07-02) - 「前回開いていた各ビューを起動時に復元する」のチェックを外してもロックしていたタブを復元 - about:config 実装 - キャッシュが無く、かつdat落ちしているスレを2回開いたときにタブが空白になるバグを修正 - 自分の書き込みを抽出できるようにした。 - HRタグ実装 - スレビューの埋め込み画像の枠の下部が表示されないときがあるバグを修正 - スクロール時の埋め込み画像の表示を高速化 - 書き込みログを一覧表示できるようにした(ツールメニューから) - 書き込みログが最大サイズ(バイト)を越えたら分割するようにした(about:configでバイト設定) - 自分の書き込んだレスにマークをつけることができるようにした(表示の詳細設定から) - 自分の書き込みに対するレスにマークをつけることができるようにした(表示の詳細設定から) - スレビューのしおりをアイコン表示に変更 - 板一覧のコメントの文字色を設定できるようにした(色の詳細設定から) - 色の詳細設定にリセットボタンを追加 ### [2.0.0-beta080601](https://github.com/JDimproved/JDim/compare/7b70d7f0da...ccf7bf5f92) (2008-06-01) - 簡易実況モード追加( ショートカット = F6 ) - スムーススクロール時のカクカクを軽減(描画タイマーを通常描画と別にした) - ツールバーとしてGtk::Toolbarをカスタマイズして使用する事にした - 各ビューと枠との間の余白を設定できるようにした( view_margin ) - 高速スクロールの速度調整設定追加( key_fastscroll_size ) - 書き込みメッセージのスペース/改行チェックの実装を取りやめた - 画像ビューの拡大縮小でテンキーの「+-」も使えるようにした - 多重ポップアップの際にポップアップの最大幅が1つ前の参照元の幅になっていたのを修正 - スレタイ検索で数字選択してポップアップさせると落ちるバグを修正 - スレタイ検索でキーワード抽出などをすると落ちるバグを修正 - 効果音再生機能を実装( `./configure --with-alsa` ) - スレ読み込み中にビューを閉じると落ちる時があるバグを修正 - スレビューのダブル、トリプルクリックによる範囲選択機能を強化 ### [2.0.0-beta080418](https://github.com/JDimproved/JDim/compare/4c35cda400...7b70d7f0da) (2008-04-18) - お気に入りの名前変更でスペースが入力できないバグを修正 - $TEXTでURLエンコードされていたバグを修正 - 書き込み中止のショートカットをAlt+q->Alt+wからAlt+q->Alt+qに変更 - jump_new_after_reloadを有効にするとスレ再取得後に元のレスにジャンプしないバグを修正 - 右ボタン+ホイール回転のタブ移動で2つおきに移動するバグを修正 - URLのリンクにマウスポインタを乗せた時にURLをステータスバーに表示するようにした - スレビューのテキスト上ではカーソルを I に変えるようにした - 起動時にログやスレタイ検索ビューも復元するようにした(検索は再実行しない) - スレ一覧やスレビューの進むと戻るを実装 - ツールバーを各ビューから独立分離した - スレ一覧、スレビューのツールバーの項目設定にロックボタンを追加 - gnutlsの必須バージョンを1.2以降に変更した - インストール直後の書き込みウィンドウのフォントがスレフォントと異なるバグを修正 - g++4.3対応 - 名前欄に b タグが二つ以上含まれたときに後ろのタグが無視されるバグを修正 - ツールバー項目選択ダイアログでアイテムを追加したときに出るassertion表示を無くした - タブをドラッグ移動するときに矢印を表示するようにした - サイズの大きいスレを開こうとすると落ちる時があるバグを修正 - リンクをクリックして他板のスレを開くと壊れていると判定されるバグを修正 - 画像ポップアップのメモリリークを修正 - 巨大サイズ画像を表示する時に固まったようになる問題を修正 - 横に細長い画像をモザイク表示すると落ちるバグを修正 - jump_after_reload、jump_new_after_reloadを有効にしたとき挙動が変だったバグを修正 - 誤爆注意ダイアログで×ボタンでウィンドウを閉じると書き込みするバグを修正 - お気に入りと混同すると指摘があったのでスレやレスの「ブックマーク」を「しおり」に改名 - 書き込みビューでAA入力するときに出ていたWARNINGが出ないようにした - 書き込みプレビューでスペースキーなどでスクロールできない問題を修正 - 文字が折り返していないレスのポップアップ表示で必ずスクロールバーが付くバグを修正 - 埋め込み画像があるレスのポップアップ表示で必ずスクロールバーが付くバグを修正 - 書き込みビューをemacs風操作にするとuim環境などで誤動作する問題を修正 - 最大化を解除する時に時々勝手にリサイズするバグを修正 - 外部板を板一覧から追加/編集できるようにした - a hrefの解釈でURLが "" に囲まれている場合とそうでない場合で処理を分けた - 起動時の引数処理を追加(既に動作中のJDにURLを渡して開ける機能など) - ボタンなど、できるだけwidgetの枠を除いて画面をすっきりさせた - サイドバーのコンボボックスを無くしてすっきりさせた - 書き込みメッセージの最後がスペースか改行で終わっている場合は確認するようにした - 板一覧の外部板の項目が空になった場合、etc.txtに保存されないバグを修正 - 一時的にDAT落ちになったスレが復活しても、お気に入りの表示のアイコンが戻らないバグを修正 - ツールバーに埋め込んでいたScrolledWindowをHBoxに変更した - レスポップアップの最大幅をスレビューの幅に制限するようにした - 「表示→詳細設定→一般→ボタンをフラット表示」を追加 jdim-0.10.1/docs/manual/2009.md000066400000000000000000000346571445721505100156600ustar00rootroot00000000000000--- title: 更新履歴(2009年) layout: default --- > [Top](../) > [更新履歴]({{ site.baseurl }}/history/) > {{ page.title }} ## {{ page.title }} ### [2.5.5-091228](https://github.com/JDimproved/JDim/compare/54f56db3d7...c3d339be60) (2009-12-28) - 書き込み履歴のあるスレを削除する時の確認ダイアログを非表示に出来るようにした ### [2.5.5-rc091225](https://github.com/JDimproved/JDim/compare/324e52e0ff...54f56db3d7) (2009-12-25) - about:configに「スレ一覧をロードする前にキャッシュにある一覧を表示する」を追加 - about:configに「お知らせスレ(924)のアイコンを表示する」を追加 - 色の詳細設定でデフォルトと異なる設定の行の背景色を変えるようにした - about:configの「true」「false」を「はい」「いいえ」表記に変更 - 書き込み履歴のあるスレを削除する時はダイアログで問い合わせるようにした ### [2.5.5-beta091220](https://github.com/JDimproved/JDim/compare/4877e5bd42...324e52e0ff) (2009-12-20) - スレ一覧でスレッド924(お知らせスレ)をアイコン表示するようにした - スレビューでIDの重複数のカウント処理を高速化した - スレビューで描画処理を少しだけ最適化した - スレ一覧の読み込みと表示処理を改良した - HTTP201の画像を開けないバグを修正 - about:configに「ビューを閉じても書き込み欄の日本語のON/OFF状態を保つ」を追加 - p2のログイン処理の仕様変更に対応 ( submit_member -> submit_userlogin ) - キャッシュの画像の読み込みに失敗すると画像表示が乱れるバグを修正 ### [2.5.0-091206](https://github.com/JDimproved/JDim/compare/2da6142f86...4877e5bd42) (2009-12-06) - 初回起動時のデフォルトフォント候補に小夏フォントを追加 - エラーなどのメッセージがファイルに出力される機能を起動オプションとして追加 ### [2.5.0-beta091123](https://github.com/JDimproved/JDim/compare/fd71704179...2da6142f86) (2009-11-23) - 更新チェック中に対象スレを手動でロードすると更新チェックが停止する時があるバグを修正 - お気に入り追加ダイアログで名前を変更してもお気に入りに反映されないバグを修正 - 板一覧再読み込みで更新量が多いと固まったようになるバグを修正 - 画像ローダのメモリキャッシュ機能を実装(デフォルトキャッシュ数 3 ) ### [2.5.0-beta091103](https://github.com/JDimproved/JDim/compare/7a0a639930...fd71704179) (2009-11-03) - 書き込みビューが空のときに対応するスレを閉じたら書き込みビューも閉じるようにした - スレ一覧の since、最終書込の表示形式を表示メニューから変更できるようにした - 画像ポップアップ上でショートカットキーを使えるようにした - 画像ポップアップ上でマウスジェスチャを使えるようにした - 画像ポップアップ上でコンテキストメニューを表示できるようにした - フォントと色の詳細設定をタブで分けた - 色の詳細設定に「スレビューの選択範囲の色設定に gtkrc を用いる」を追加 - 色の詳細設定にabout:configから「ツリービューの背景色設定に gtkrc を用いる」を移動 - 色の詳細設定で複数行を選択可能にした - about:configに「起動時にお気に入りを自動でチェックする」を追加 - HTML化された過去ログの読み込みに対応 - コンテキストメニューの表示がキャンセルされるとポップアップが消えなくなるバグを修正 - 画像ビューでタブスクロールボタンを押すとキーボードフォーカスが外れるバグを修正 - スレを閉じるときに無駄なディスクアクセスが生じていた問題を修正 - スレ一覧の列幅を調整するときに時々一番上のスレを開くバグを修正 - ログ一覧の表示機能を実装 ### [2.4.2-090927](https://github.com/JDimproved/JDim/compare/83458b61e8...7a0a639930) (2009-09-27) - 画像ビューが埋め込み表示の時にビューを開くとスレビューのスクロール位置がズレる問題を修正 ### [2.4.2-beta090914](https://github.com/JDimproved/JDim/compare/d9316e69d7...83458b61e8) (2009-09-14) - 書き込みビューにスペースと&nbsp;を相互に変換する機能を追加 - スレ一覧でツールバー非表示の時にCtrl+fや/で検索バーを表示できるようにした - 画像ビューにユーザコマンドメニューを追加 - スレビューのコンテキストメニューに「選択範囲のレスをあぼ〜ん」を追加 - DAT落ちしたスレを開いた時にダイアログを表示するようにした - タブメニューの移動の項目にアイコンを表示するようにした - スレ一覧でスレを複数指定してからDatの保存が出来るようにした - subject.txtのレス数よりも実際のレス数が多い時スレ一覧にアイコンを表示するようにした - スレ一覧で!を押したときに現在のソートのモードをステータスバーに表示するようにした - 更新済タブへ移動のショートカットキーとマウスジェスチャ(デフォルト未設定)を追加 ### [2.4.2-beta090806](https://github.com/JDimproved/JDim/compare/3ce0d1ebdf...d9316e69d7) (2009-08-06) - 板一覧の更新時にエラー等で新規データが空になった場合でも更新されてしまう問題を修正 - サイドバーのアイコン表示切り替えアルゴリズムを効率化 - サイドバーの項目をクリックしてもアイコン表示が戻らない時があったバグを修正 - サイドバーの更新チェックのキャンセルが効かなかったバグを修正 - サイドバーに履歴ビューを実装 - サイドバーの更新チェック機能を改良 - 履歴メニューを改良 - スレ一覧の!のソートモードにモード3を追加 - スレビューのコンテキストメニューの項目を編集できるようにした - スレビューのコンテキストメニューに「再読み込み」を追加 - スレビューのコンテキストメニューに「タイトルとURLをコピー」を追加 - スレビューのコンテキストメニューに「お気に入りに追加」を追加 - ツールバー項目設定の「元に戻す」ボタンを押してもデフォルト状態に戻らない問題を修正 - レスポップアップで行数の少ないレスでもスクロールさせないと全文が読めないバグを修正 ### [2.4.1-090712](https://github.com/JDimproved/JDim/compare/ded5cd0461...3ce0d1ebdf) (2009-07-12) - タブ切り替えメニューにタブのアイコンを表示 - お気に入りの次スレ自動更新時に確認ダイアログを表示するようにした - 表示メニュー→詳細→一般に「ツールバーの背景を描画する」を追加 ### [2.4.1-beta090628](https://github.com/JDimproved/JDim/compare/99a5bf2a46...ded5cd0461) (2009-06-28) - 水平線(HR)の左端をテキストの字下げ位置に合わせた - スレ一覧でキャッシュが無いスレは「dat保存」メニューを選択できないようにした - datファイルのサイズ表記を k から K に変更 - 画像メニューに「エラー画像を閉じる」を追加(パッチ124) - jd.confの設定値が異常な場合の安全策を追加 - 現在表示中のタブの表示が変(右端が切れる時がある)な問題を修正 - タブ上でのホイール回転でタブを循環表示するようにした - タブ切り替えボタンの枠がフォーカス時にはみ出るバグを修正 - ローダの仕様を変更(詳しくはJDスレ7-613参照) - 表示位置でタブを選択出来るキーバインドを追加 - win32対応 - スレビューの画像ポップアップメニューに「モザイクで開く」を追加 - 書き込みプレビューで新方式のトリップに対応 - 画像ポップアップ表示時にポインタがポップアップと重なるとポップアップが消えるバグを修正 ### [2.4.0-090522](https://github.com/JDimproved/JDim/compare/f2c2da668c...99a5bf2a46) (2009-05-22) - 細かいバグをいくつか修正 ### [2.4.0-rc090516](https://github.com/JDimproved/JDim/compare/6a4ae3ea31...f2c2da668c) (2009-05-16) - スレにフォントに含まれない文字がある時にコンソールに大量のエラーが表示される問題を修正 - Dat落ちしたスレの更新可能アイコン表示が消えないバグを修正 - 3ペーン表示時にタブをクリックしてもスレ一覧とスレビュー間のフォーカスが移らないバグを修正 - 3ペーン表示時にタブ上でホイールを回転してもスレ一覧とスレビュー間のフォーカスが移らないバグを修正 - タブ切り替えボタンでタブを切り替えたときにスレタイやステータスが変わらないバグを修正 - タブメニューの移動サブメニューでタブを切り替えたときにスレタイやステータスが変わらないバグを修正 ### [2.4.0-beta090510](https://github.com/JDimproved/JDim/compare/f151889baf...6a4ae3ea31) (2009-05-10) - gtk+-2.4辺りのバージョンのgtkを使用するとスレビューのツールバーのボタンが押せないバグを修正 - ハッシュの導入によるDBTREE::BoardBase::get_article関数の高速化 - configureオプションに「`--with-native`」を追加(gcc>=4.2, x86 or x86_64) - スレビューの描画領域の高さが1以下の時にスレの読み込みをすると落ちるバグを修正 - `-s` ( `--skip-setup` ) 起動オプション追加 ### [2.4.0-beta090429](https://github.com/JDimproved/JDim/compare/366a66a2c1...f151889baf) (2009-04-29) - スレのプロパティのあぼーん設定に「sage以外あぼーん」を追加 - まちBBSのサブジェクトをtitleタグから取得するようにした( offlawモードでない時 ) - まちBBSのofflaw.cgi読み込みモードを実装( 設定メニューの一般項目で設定) - 動作環境のconfigureオプションとして表示する内容を一部に限定した - p2にログインして直接書き込みできるようにした - 鬼車使用時に改行が入ると正規表現のマッチングに失敗する問題を修正 - about:configに画像のスムージングレベルの設定を追加 - datファイルのインポート機能を実装 - datファイルの一時表示機能を実装 - JBBSの文字コードをEUC-JPからEUCJP-WINに変更(丸付き数字の文字化け対策) - 次スレ移行時に古いスレタイが新スレタイに変わらない時があるバグを修正 - 16進数の文字参照を実装 - スレ一覧更新時にDat落ちしたスレをスレッドあぼーんのリストから取り除ける様にした(ダイアログ表示) - まちBBSのトップページをWebブラウザで開く事が出来ない問題を修正 - フォント幅の計算時にメモリの割り当て領域外にアクセスする場合があったバグを修正 - 板(スレ一覧)の更新チェック機能を実装 - タブ切り替えボタンをタブの右側に追加 ### [2.3.0-090305](https://github.com/JDimproved/JDim/compare/626f220688...366a66a2c1) (2009-03-05) - したらばに書き込めなくなった問題に対応( application/x-www-form-urlencoded ) - まちBBSの仕様変更に対応 ( read.pl -> read.cgi ) - スレタイの先頭がスペースで始まっている場合にスレ一覧で先頭のスペースが省略表示されるバグを修正 - タブが含まれる書き込みで鉛筆マークが付かないバグを修正 - gtkrcでnotebookのythicknessを0にしてスレ一覧を開くとCPUが100%になるバグを修正 - 他のビューにタブをドロップするとタブ移動の矢印が消えなかったバグを修正 - 画像ダウンロード中のツールチップの文字色をスレビューの文字色と合わせた - サイドバーに「お気に入り更新チェックしてタブで開く」ボタンを追加出きるようにした - web検索のショートカットキーを追加 (Ctrl+k) - スレタイ検索のショートカットキーを追加( Ctrl+t) - ローカルログ検索のショートカットキーを追加( Ctrl+Enter ) - 全ログ検索のショートカットキーを設定出きるようにした( デフォルト無効 ) - about:configに「Ctrl+qでウィンドウを閉じない」を追加 ### [2.2.0-090212](https://github.com/JDimproved/JDim/compare/46775b83b2...626f220688) (2009-02-12) - キャッシュのsubject.txtが古いと次スレのリンクをクリックした時にdat落ちになるバグを修正 - キャッシュのsubject.txtが無いと次スレのリンクをクリックした時にloading表示が消えないバグを修正 - キャッシュのsubject.txtが無い時に次スレのリンクをクリックした時のスレタイ類似度計算に - レーベンシュタイン距離を使うようにした - 次スレ移行時にお気に入りの名前が編集されているときは新スレの名前にしないようにした - 書き込み時にWAVE DASH 問題のため書き込みマークが付かない問題を修正 - 「スレ情報を消さずにスレ再取得」でタブのロックが外れるバグを修正 - 「スレ情報を消さずにスレ再取得」でタブの位置がズレるバグを修正 ### [2.1.1-beta090128](https://github.com/JDimproved/JDim/compare/65130226e7...46775b83b2) (2009-01-28) - お気に入りの編集に対するUNDOとREDOを実装した - お気に入り編集ウィンドウに検索窓を付けた - 書き込みしたレスに"が含まれていると鉛筆マークが付かないバグを修正 - p2経由で書き込みをチェックしてもbbspink.comではブラウザが開かないバグを修正 jdim-0.10.1/docs/manual/2010.md000066400000000000000000000433111445721505100156330ustar00rootroot00000000000000--- title: 更新履歴(2010年) layout: default --- > [Top](../) > [更新履歴]({{ site.baseurl }}/history/) > {{ page.title }} ## {{ page.title }} ### [2.7.5-101228](https://github.com/JDimproved/JDim/compare/5d1534c773...ba564b8d78) (2010-12-28) - キーワード抽出やログ検索で()をエスケープする様にした - 検索テキストボックスで漢字変換中にEscを押すとボックスが閉じるバグを修正 - 検索テキストボックスで補完候補ポップアップの選択行をCtrl+n, Ctrl+pで上下移動出来るようにした - 「選択範囲の画像を開く」で画像を開くとあぼーんしている画像も開いてしまうバグを修正 ### [2.7.5-beta101213](https://github.com/JDimproved/JDim/compare/51d1ac67d0...5d1534c773) (2010-12-13) - 512Kを越えたスレに書き込みしようとした時、書き込みビューを開く前に警告ダイアログを出すようにした - マウスボタン設定に「画像保存」を追加 - マウスボタン設定に「画像サイズ調整」を追加 - about:configのウィンドウの欄に「メニューバーを非表示にした時にダイアログを表示」を追加 - about:configのスレビューの欄に「画像ポップアップとカーソルの間の水平間隔(ピクセル)」を追加 - about:configのお気に入りの欄に「登録時に挿入先ダイアログ表示」を追加 - about:configの画像の欄に「deleteを押したときに確認ダイアログを表示する」を追加 - about:configの画像の欄に「インライン画像の最大幅」を追加 - about:configの画像の欄に「インライン画像の最大高さ」を追加 - about:configの書き込みビューの欄に「編集中のメッセージの保存確認ダイアログを表示する」を追加 - about:configのその他の欄に「数字入力ジャンプの待ち時間(ミリ秒)」を追加 - マウスジェスチャに「メニューバー表示」を追加 - マウスジェスチャに「メインツールバー表示」を追加 - マウスジェスチャに「JD終了」を追加 - マウスジェスチャに「全てのタブを閉じる」を追加 - マウスジェスチャに「他のタブを閉じる」を追加 - マウスジェスチャに「全ての更新されたタブを再読み込み」を追加 - マウスジェスチャに「最大化 / 最大化解除」を追加 - マウスジェスチャに「最小化」を追加 - ショートカットキーに「前のディレクトリに移動」を追加(デフォルト } ) - ショートカットキーに「次のディレクトリに移動」を追加(デフォルト { ) - ビューが表示されてない領域でもマウスジェスチャを認識するようにした - ポップアップのマージンを0に設定できないバグを修正 - 書き込み時に書込秒数規制の残り秒数をステータスバーの一番左に表示 - 書き込み時に書込秒数規制の残り秒数をウィンドウタイトルに表示 - スレが壊れている時などに、ラベルだけでなくステータスバーの色も変えるようにした - 書き込み規制中や行数オーバー時に書き込みウィンドウのラベルやステータスバーの色を変えるようにした - 全画面モードを暫定実装 (表示メニュー → 「全画面」 で切り替え) ### [2.7.5-beta101104](https://github.com/JDimproved/JDim/compare/4398cdccbc...51d1ac67d0) (2010-11-04) - p2と●を同時ログインしているとdat落ちした過去ログを取得できないバグを修正 - 画像ビューのコンテキストメニューの「複数の画像を閉じる」にエラー関係の設定を追加(404のみを閉じる等) - IDの末尾を判定してIDポップアップやID抽出に端末の種類を表示するようにした - ファイルメニューの「お気に入り保存」を廃止して「セッション保存」を追加 - SIGHUP、SIGINT、SIGQUITの割り込み時にバックアップファイルを作成しないでセッションを保存するようにした - about:configのその他に「指定した分ごとにセッションを自動保存」を追加 - about:configのお気に入りの所に「重複項目を登録する」を追加 - about:configのツリービューの行間スペースを0にしても板一覧に適用されないバグを修正 - キーワードやIDなどの抽出ビューの表示を更新ボタンやショートカット(F5, s)で更新出来るようにした - 過去のレスに対してレス参照をカウントするようにした - 表示メニューの詳細の一般に「ステータスバー表示」を追加 - 「ブラウザで開く」で画像を開くとき、キャッシュではなくて元のアドレスを開くようにした - 「(画像の)キャッシュをブラウザで開く」をコンテキストメニューに追加 - ショートカットキーに「元のスレを開く」を追加(デフォルト Shift+F5,S) - IDの代わりにIPが表示される板ではIPをカウントするようにした - セットアップウィザードで3ペインを選択してもメニューの表示では2ペインのままになっているバグを修正 - ユーザーコマンドに $OLDHOSTNAME, $OLDHOST, $OLDHOSTNAMEL, $OLDHOSTL, $BBSNAMEL, $DATNAMEL, $LOCALDATL 追加 - 「リロード後に新着レスに移動する」時に実況モードにするとリロードする度に新着セパレータに戻るバグを修正 - レスポップアップを使ってAAの騙し絵を表示できるようにした - 画像の拡張子がgifでないのにリダイレクト先の拡張子がgifになっていると偽装判定に失敗するバグを修正 - SETTING.txtのBBS_UNICODEがpassである板でのunicode文字書き込みに対応した - スレ一覧に「増分」の列を追加(前回スレ一覧を開いた時からのスレ数の増分) - 表示→詳細設定→書き込み設定→テキストを折り返し表示を追加(デフォルトON) ### [2.7.0-100823](https://github.com/JDimproved/JDim/compare/0ea7d7a3db...4398cdccbc) (2010-08-23) - p2ログイン時のIDとパスワードをURLエンコードしていなかったバグを修正 - 書き込みビューやサイドバーを閉じている時に境界をドラッグしたら境界を元の位置に戻すようにした ### [2.7.0-beta100808](https://github.com/JDimproved/JDim/compare/abf810d790...0ea7d7a3db) (2010-08-08) - スレビューで範囲選択していないときにCtrl+tで空のスレタイ検索ビューを開けない問題を修正 - about:configのその他に「マウスジェスチャを有効にする」を追加 - about:configのツリービューに「ツリービューのエクスパンダを表示する」を追加 - about:configのツリービューに「ツリービューのレベルインデント調整量(ピクセル)」を追加 - about:configのスレビューに「ポップアップが消えるまでの時間(ミリ秒)」を追加 - about:configのネットワークに「同一ホストに対する最大コネクション数」を追加 - about:configの履歴ビューに「ペーン境界をクリックしてサイドバーを開け閉めする」を追加 - about:configのスレ一覧の所に「dat落ちしたスレを表示する」を追加 - 画像ビューをクリックする度に元サイズ画像とウィンドウに合わせた画像を切り替えるようにした。 - 実況時にスレの読み込みのタイミングに合わせて書き込みビューを開くとビューが正しく開かないバグを修正 - 「次検索」と「次のレスへ移動」のショートカットキー(n)が被っていたので「次検索」から除いた - 表示メニューの詳細設定のツールバー表示のメインツールバーの表示切り替え項目をチェック形式に変更した - ショートカットキーの設定ダイアログに「メインツールバー表示」を追加(デフォルトキーは未設定) - 設定から「スレ一覧に過去ログも表示する」の設定を廃止して板別に表示設定出来るようにした - スレビューのレスポップアップの抽出に「ジャンプ」を追加 - スレビューから板レベルのNGワードやNGネームを追加した時、その板を開かないと情報が保存されないバグを修正 - ホイールスクロール直後にポインタの下がリンクの時はマウスを少し動かさないとポップアップしないようにした - ログ検索時に正規表現メタ文字が含まれていたらエスケープするようにした - スレビューで選択中の文字をキーワード抽出する時に正規表現メタ文字が含まれていたらエスケープするようにした - 拡張子が偽装されている画像のモザイクを外すとき、既に外してあっても警告ダイアログが表示されるバグを修正 - スレを削除する時に画像キャッシュも削除するかダイアログを表示するようにした - IDの後ろに●が付いているレスが発言数にカウントされないバグを修正 - 起動オプションに `-n/--norestore` を追加 ### [2.7.0-beta100627](https://github.com/JDimproved/JDim/compare/6fd0027336...abf810d790) (2010-06-27) - お気に入りのコンテキストメニューに「並び替え」項目を追加 - お気に入り登録時に同一スレや板が登録されている場合はダイアログを出すようにした - ショートカットキーとマウスジェスチャの設定項目にサイドバーの更新チェックを追加 - ローカルルールのHTTPコードの戻り値が302の時にプロパティに内容が表示されないバグを修正 - 板ボタンの▼をクリックした時のメニューにローカルルール表示と板のプロパティ表示の項目を追加 - 投稿確認ダイアログにローカルルールを表示するようにした - about:configに「画像ビューのフォーカスが外れたら折りたたむ」を追加 - about:configに「スレビューのスクロールバーを左に配置する」を追加 - JD再起動時に全キャッシュ検索や書き込みログのタブが復元されないバグを修正 - アンカーやリンクを含んだ行をトリプルクリックで行全体選択出来ないバグを修正 - 書き込んでもスレ一覧の「最終書込」が更新されないバグを修正 - あぼーんしたレスの ID も同一 ID の発言数にカウントするようにした - スレ一覧のコンテキストメニューに「スレ情報を消さずに再取得」を追加 - スレ削除時の警告ダイアログで close ボタンや esc キーで閉じてもスレを削除するバグを修正 - 画像ロード等でHTTP301に対応 - ログ検索ツールバーに「しおり」チェックボックスを追加 - スレ一覧、スレビューの検索やキーワード抽出で全角英数字や半角カナを区別しない様にした - キーワード抽出で抽出したURLのレス番号が細かく分かれるバグを修正 - スレビューの検索でDOMノードを越えて検索出来るようにした - フォントサイズが大きいとスレ一覧のポップアップが一瞬で消えるバグを修正 - 「サイドバー表示」のマウスジェスチャ設定を追加 - 書き込みウィンドウの右クリックメニューの選択時の挙動を微調整 - お気に入りの更新チェックが行われない時があるバグを修正 ### [2.6.5-100425](https://github.com/JDimproved/JDim/compare/dbca691403...6fd0027336) (2010-04-25) - 板のプロパティのNGタイトルのページに「dat落ちしたスレのタイトルを削除する」ボタンを追加 - 環境変数 JD_LOCK でロックファイルのパスを指定出来るようにした - ローカルにあるdatファイルを起動時に開いた後で再起動すると落ちるバグを修正 - 書き込みビューを開いてからスレビューを全て閉じてプレビューでポップアップ表示すると落ちるバグを修正 - スレビューのコンテキストメニューに「選択範囲の画像をあぼ〜んする」を追加 ### [2.6.5-beta100411](https://github.com/JDimproved/JDim/compare/1b0bf46c99...dbca691403) (2010-04-11) - 次/前の書き込みへの移動を追加(Shift+F2/Ctrl+Shift+F2) - スレ一覧のコンテキストメニュー項目を編集可能にした - スレビューのコンテキスメニューに「選択範囲の画像を開く」を追加 - スレビューのコンテキスメニューに「選択範囲の画像を削除」を追加 - ツールバー/リスト項目設定における不適切な配色のしかたを修正 - JBBSのスレは更新チェックを行えないようにした(Last-Modifiedを取得できないため) - まちBBSでofflawモードでない時は更新チェックを行えないようにした(HTTP500が返るため) - まちBBSでロード時にdat落ち判定を可能にした
(→ 更新チェック時の判定は常にHTTP200が返るので無理。JBBSは常にHTTP200、0バイトが返るので無理) - 板一覧やスレ一覧のマウスボタンの設定を変えるとお気に入りのディレクトリがクリックで開かないバグを修正 - Webブラウザ設定にchromeを追加 - 板のプロパティで最大レス数を指定可能にした - 実況モードでスレビューのフォーカスを外している時にビューの再描画が実行されない時があるバグを修正 - URL末尾の文字参照が正しく認識されないバグを修正 - FIFOにエラーがあった場合のダイアログに「今後表示しない」を追加 - お気に入りの仮想板機能を実装 - お気に入りに登録しているスレの次スレを開いた時に表示されるダイアログに「お気に入り追加」を追加 - about:configに「dat落ちしたスレをNGスレタイトルから除く」を追加 - 実況のスクロール速度を0に設定出きるようにした。 - `>>111`の様に途中に全角文字を含む数字を書き込むと正しくアンカー処理されないバグを修正 - スレ一覧でスレをあぼーんする度にスレ一覧を再描画していた問題を修正 - スレのプロパティのNGスレ番号に取得レス数以上の数字を入れると落ちるバグを修正 - 書き込みビューを閉じたときにUNDOのバッファが残ったままになっていたバグを修正 - 設定ダイアログのテキストボックスに文字を入れてエンターキーを押すとOKで閉じるようにした - 外部板の書き込み時にクッキーを送らない時がある問題を修正 - オートスクロールのマークがスレビューのスクロールバーにかかっていると落ちるバグを修正 ### [2.6.0-100208](https://github.com/JDimproved/JDim/compare/61155e314d...1b0bf46c99) (2010-02-08) - 画像ビューでお気に入り追加ダイアログを閉じると画像ビューのフォーカスが外れるバグを修正 - gtk2.18以降でタブにスクロールボタンの矢印が表示されたままになる時があるバグを修正 - WAVEDASH問題を修正 ### [2.6.0-rc100130](https://github.com/JDimproved/JDim/compare/606c2fbb11...61155e314d) (2010-01-30) - 2chにログインしているとHTML化された過去ログを読めなくなるバグを修正 - スレ一覧のコンテキストメニューに「次スレ検索」を追加 - 書き込み欄を埋め込み表示してる時、スレビューを開いていないと新スレ作成欄が表示されないバグを修正 - 板一覧やスレ一覧を表示した状態でCompizのスケールを起動してJDを選択するとスレ一覧やスレが開くバグを修正 ### [2.6.0-beta100123](https://github.com/JDimproved/JDim/compare/c3d339be60...606c2fbb11) (2010-01-23) - スレビューのスクロール処理を高速化 - キー押しっぱなしでスレビューをスクロールすると止まらなくなる事があるバグを修正 - スレ一覧を復元した時にdat落ちしたスレがスレ一覧に表示されないバグを修正 - 画像読み込み中に画像ビューの他の画像を削除すると読み込み中の画像が閉じるバグを修正 - 画像の強制再読み込みをしても埋め込み画像の表示が変わらないバグを修正 - 状況によってはArticleHash::it_get()でセグフォで落ちる場合があるバグを修正 - dat落ちスレのロード中にタブを切り替えて、タブを閉じますかのダイアログで切り替えたスレが閉じるバグを修正 - 画像ポップアップの表示サイズを大きくしたときに常に画面内に表示されるようにした(クリックかESCで閉じる) - subject.txtが正しいフォーマットでないと落ちる時があるバグを修正 - スレビューのあぼーんをクリックしたときに出てくるポップアップメニューにあぼーん設定を追加 - gtkでEmacsのキーバインドをしているときAAメニューの項目が誤動作するバグを修正 - about:config → ツリービュー → "カテゴリを開いたときにスクロールする"を追加 - マウスボタン詳細設定 → スレビュー → "アンカーをクリックでジャンプ" を追加 - 「URLを開く」ダイアログを追加 ( Ctrl+o ) jdim-0.10.1/docs/manual/2011.md000066400000000000000000000150031445721505100156310ustar00rootroot00000000000000--- title: 更新履歴(2011年) layout: default --- > [Top](../) > [更新履歴]({{ site.baseurl }}/history/) > {{ page.title }} ## {{ page.title }} ### [2.8.2-110808](https://github.com/JDimproved/JDim/compare/122e58e792...a7df368213) (2011-08-08) - スレタイ検索(Ctrl+t)のショートカットをスレビュー以外でも効くようにした - 画像ビューの「複数の画像を閉じる」でステータスの[m/n]表示が正しく更新されないバグを修正 - datファイルのmtimeをサーバ上のmtimeに一致させるようにした - Youtube動画のサムネイル画像をインライン表示出来るようにした - about:configの「ウィンドウ」に「タブ上でマウスホイールを回転してタブを切り替える」を追加 - スレのプロパティのあぼーん項目に「板/全体レベルでのあぼ〜んを有効にする」のチェックを追加 ### [2.8.2-beta110724](https://github.com/JDimproved/JDim/compare/2b40cc4b7e...122e58e792) (2011-07-24) - 画像を削除すると画像ビューのキャッシュに無い画像が全て閉じるバグを修正 - プレビュー画面で画像をポップアップ表示させてる時にdeleteを押してもポップアップが消えないバグを修正 - 色の設定に「複数発言したIDの文字色」を追加 - 色の設定に「多く発言したIDの文字色」を追加 - 色の設定に「参照されていないレス番号の文字色」を追加 - 画像ビューのステータスバーに[タブ番号/タブ数]と[画像ファイル名]を表示 - アルファベットの大文字とマウスパッドのキーコードがぶつかっていたバグを修正 (例: K と KP_left) - スレタイ検索でタイトル中に<が入っているとその後の文字が消えるバグを修正 - ファイルダイアログで日付の列が日本語ロケールにならないバグを修正 - about:configの「画像」の「埋め込み画像ビューを閉じたときにタブも閉じる」を追加 - スレビューのコンテキスメニューに「削除」を追加 - 起動オプションに `-g/--geometry WxH-X+Y` を追加 - 複数のスレビューを閉じる時、タブの再描画をしないようにして高速化 - スレ一覧ツールバーに「ハイライト解除」ボタンを追加出来るようにした - 埋め込み画像ビューのタブ上のサムネイルの間をクリックするとサイドバーが閉じるバグを修正 - ログ/スレタイ検索のツールバーの並びを編集出来るようにした - 全画面表示のショートカットキーを追加(F11) - あぼーんしたスレがdat落ちするとスレ一覧のNGスレタイトルのリスト順が並び替えられるバグを修正 - 複数画像の保存時にファイル名が重複した時、画像キャッシュ内のファイル名で保存出来るようにした - スレビューでdeleteを押したときの確認ダイアログに「今後表示しない」チェックを追加 - マウスジェスチャに「削除」を追加 (デフォルト↓→↓) - ubuntu11.04でポップアップ表示時にスレビューをクリックするとポップアップが消えるバグを修正 - リンクの末尾から"()[]"を特別に除外する仕様を廃止 - 閉じた板の履歴を履歴メニューとサイドバーに追加 - サイドバーで複数選択した複数画像も開けるようにした ### [2.8.1-110312](https://github.com/JDimproved/JDim/compare/22e977fd5f...2b40cc4b7e) (2011-03-12) - スレ内にあるリンクの文字列が丁度256文字の時にバッファオーバフローしていた問題を修正 - about:configの書き込みビューの欄に「デフォルトの書き込み名」を追加 - about:configの書き込みビューの欄に「デフォルトのメールアドレス」を追加 - 設定メニューのあぼーん項目に「NG正規表現で大小と全半角文字の違いを無視する」を追加 - 範囲選択した文字列を抽出する時に、文字列に空白が含まれているなら前後に""を付けるようにした - 書き込み失敗時に出るダイアログで詳細を表示出来るようにした。 - スレ一覧のある行を長押ししてから素早くクリックし直すとそのスレが2度読みされるバグを修正 - オフラインモードなのにキャッシュの無い画像が「範囲選択の画像を開く」でダウンロードされるバグを修正 - ショートカットキー詳細設定の「編集」に「改行」を追加(デフォルト 無し) ### [2.8.1-beta110214](https://github.com/JDimproved/JDim/compare/819fd49060...22e977fd5f) (2011-02-14) - スレ一覧でスレタイが512文字を越えると端末にPango-WARNINGで表示されて落ちるバグを修正(1024文字まで拡張) - 2chでクッキーHAPを保存して送るようにした(いわゆる忍法帖対応) ### [2.8.0-110203](https://github.com/JDimproved/JDim/compare/e9f3eeb83e...819fd49060) (2011-02-03) - タブ幅を縮める際にタブ幅の計算を正確に行うようにした - 新しくスレを開いたときのタブ幅の計算回数を減らした - 機能してなかった $NUMBER ユーザコマンドをきちんと作った。 ### [2.8.0-beta110118](https://github.com/JDimproved/JDim/compare/ba564b8d78...e9f3eeb83e) (2011-01-18) - BEの新ログイン形式に対応した - アイコンテーマを実装 - スキンファイルのデフォルトディレクトリを~/.jd 直下から ~/.jd/theme に変更 - サイドバーと履歴メニューに「最近閉じた画像」を追加した - 設定 → プロパティ → 板一覧のプロパティを追加 - ショートカットキーに「最後に閉じたタブを復元」を追加(デフォルト Ctrl+T) - ショートカットキーに「sageのON/OFF切り替え」を追加(デフォルト Alt+s) - マウスジェスチャに「最後に閉じたタブを復元」を追加(デフォルト →←) - スレ一覧とスレビューのタブメニューの「複数のタブを閉じる」に「同じアイコンのタブ」を追加 - about:configのウィンドウ欄に「状態変更時にメインステータスバーの色を変える」を追加 - 排他処理をきちんとして、まれにリロード時に反応が無くなるバグを修正 jdim-0.10.1/docs/manual/2012.md000066400000000000000000000031441445721505100156350ustar00rootroot00000000000000--- title: 更新履歴(2012年) layout: default --- > [Top](../) > [更新履歴]({{ site.baseurl }}/history/) > {{ page.title }} ## {{ page.title }} ### [2.8.5-120826](https://github.com/JDimproved/JDim/compare/35818ddd51...591ad36134) (2012-08-26) - リンクフィルタのコマンドでの置換文字に\0〜\9を追加 - Youtubeのサムネイルが表示されない問題を修正 - 正規表現ライブラリとしてPOSIX regex の代わりにPCREを使用できるようにした - スレ一覧の時刻表示オプション「年/月/日 時:分:秒」を追加 - スレ一覧に、スレを最後にロードした最終取得時刻を表示できるようにした - 数値文字参照(&#169;など)は「;」なしでも表示できるようにした ### [2.8.5-beta120206](https://github.com/JDimproved/JDim/compare/a7df368213...35818ddd51) (2012-02-06) - svn版のabout:config に「ビュー内から他のビューを開いたときのタブの位置」を追加 - HTTPステータスコードが301のリダイレクトに対応 - cssでTABの行が無視されていたバグを修正 - 書き込みビューのプレビューアイコンを変更しても反映されないバグを修正 - 環境に依存しやすいシステムコール(utimensat)を使用していた問題を修正 - 非2chにアクセスする際のデフォルトUAを暫定的に「Monazilla/1.00 JD」にした - p2.2ch.net→w2.p2.2ch.netのリダイレクトに対応 - gnutls>=2.12.0においてSSL接続が出来なくなっていたバグを修正 jdim-0.10.1/docs/manual/2013.md000066400000000000000000000052061445721505100156370ustar00rootroot00000000000000--- title: 更新履歴(2013年) layout: default --- > [Top](../) > [更新履歴]({{ site.baseurl }}/history/) > {{ page.title }} ## {{ page.title }} ### [2.8.6-130518](https://github.com/JDimproved/JDim/compare/9ac7c2079b...5cc6041855) (2013-05-18) - タブを閉じたとき、タイトルとURLが変更されないことがあるバグを修正 ### [2.8.6-rc130414](https://github.com/JDimproved/JDim/compare/22d5584e97...9ac7c2079b) (2013-04-14) - URL変換に$THUMBNAILオプションを追加した - youtubeのサムネイル表示を統合し、urlreplace.confファイルに設定するようにした - 連続投稿が忍法帖で規制された場合にも、Samba24の規制時間を取得するようにした - 正規表現ライブラリとしてPCREを使用しているとき、「.」が改行に一致するよう変更 - WAVEDASH問題を修正 - アスキーアート用のフォントを設定したときのレイアウトの崩れを修正 ### [2.8.6-beta130304](https://github.com/JDimproved/JDim/compare/591ad36134...22d5584e97) (2013-03-04) - &が&amp;に変換されているdatファイルに対応した - automake-1.13に対応した - アスキーアート用のフォントを設定できるようにした - URLを変換(urlreplace.conf)できるようにした - 実況モードでポップアップ表示中はスクロールを一時停止するようにした - 書き込みマークを設定/解除できるようにした - 次スレ検索を開くときのタブの位置を変更できるようにした - 複数のスレや画像をロードするときの待ち間隔を短く変更した - datを保存に、名前を付けて保存するショートカットキーを割り当てた - 表示中のビューのプロパティを開くショートカットキー(Ctrl+P(大文字))を追加 - 選択範囲の画像を開くショートカットキー(Ctrl+I(大文字))を追加 - 選択範囲のレスをあぼーん、画像を消す・あぼ〜んにショートカットキー設定を追加 - 保護した画像は、選択範囲の画像を消す・あぼ〜んの対象外に変更 - 画像のエラー情報(404エラーなど)は、選択範囲の画像を消す対象に変更 - ショートカットキーは、CapsLock状態に影響しないように変更 - レスに長いURLを含むスレを開くと、落ちるバグを修正 - subject.txtのレス数が読み込めない場合などに、落ちるバグを修正 - スレビューの描画で高負荷時に、落ちることがあるバグを修正 jdim-0.10.1/docs/manual/2014.md000066400000000000000000000051411445721505100156360ustar00rootroot00000000000000--- title: 更新履歴(2014年) layout: default --- > [Top](../) > [更新履歴]({{ site.baseurl }}/history/) > {{ page.title }} ## {{ page.title }} ### [2.8.8-140601](https://github.com/JDimproved/JDim/compare/0cc6c39b70...90b6d7e325) (2014-06-01) - 2chからのエラーコードに追加対応した - 2ch、2ch互換、まちBBSなど、板の種類が異なるときに移転処理を行わないようにした - 「about:config」メニューを上位に移動した - バージョン2.8.8-betaでのスレタイ検索のURL変更を取り消し、find.2ch.netに対応した - p2のURLをp2.2ch.scに変更 - 過去ログの取得時に、「壊れています」と誤判定することがあるバグを修正 - 同じスレを複数登録した場合などに、サイドバーの選択がずれることがあるバグを修正 - subject.txtが不当な形式の時に、落ちるバグを修正 ### [2.8.8-beta140329](https://github.com/JDimproved/JDim/compare/4a759065ae...0cc6c39b70) (2014-03-29) - 2chにログインした場合、過去ログをrokkaシステムで取得するようにした - 2chの過去ログを、offlaw2.soで取得できるように設定を追加した - 改行文字がない通知スレッドの表示に対応した - サイドバー/スレ一覧の選択を表示中のビューと同期するメニューを追加 - スレ一覧の最終取得の時刻形式を個別に設定するオプションを追加 - 「ツリービューで選択したビューを開くときのタブの位置」を追加 - 「他のビューを開くときのタブの位置」を適用する操作を追加 - findおよびp2のドメインをmoritapo.jpに変更 - 2chにログインするパスワードを、浪人にあわせ「秘密鍵」に表記変更 - バージョン2.8.7で、画像ビューをオフに設定できなくなっていたバグを修正 ### [2.8.7-140104](https://github.com/JDimproved/JDim/compare/5cc6041855...4a759065ae) (2014-01-04) - したらば(JBBS)のドメイン変更に対応した - 表示中のサイドバーやスレ一覧の選択を、表示中のビューと同期するようにした - 「終了」「オンラインマニュアル」のショートカットキーを変更できるようにした - Ctrl+qでウィンドウを閉じない設定は、ショートカットキー設定に統合した - 画像ビューメニューをラジオボタンに変更 - 「AAレスと判定する正規表現」を空に設定したり、リセットできないバグを修正 jdim-0.10.1/docs/manual/2015.md000066400000000000000000000015101445721505100156330ustar00rootroot00000000000000--- title: 更新履歴(2015年) layout: default --- > [Top](../) > [更新履歴]({{ site.baseurl }}/history/) > {{ page.title }} ## {{ page.title }} ### [2.8.9-150226](https://github.com/JDimproved/JDim/compare/3ed63d419b...5c05922bb5) (2015-02-26) - リロード待ち状態でスレが更新されないままになる問題を修正した - タブ表示をさらに最適化した ### [2.8.9-rc150201](https://github.com/JDimproved/JDim/compare/90b6d7e325...3ed63d419b) (2015-02-01) - エモーションの表示に対応した - スレタイ検索のURLをdig.2ch.netに変更した - いわゆる一行スキン用に<MESSAGE br="no"/>の指定を追加した - タブ表示を最適化した - 特定のスレタイで落ちるバグを修正 jdim-0.10.1/docs/manual/2017.md000066400000000000000000000066111445721505100156440ustar00rootroot00000000000000--- title: 更新履歴(2017年) layout: default --- > [Top](../) > [更新履歴]({{ site.baseurl }}/history/) > {{ page.title }} ## {{ page.title }} ### [2.8.9-171124](https://github.com/JDimproved/JDim/compare/ab77d02541...be3d051dc5) (2017-11-24) - 絵文字(> U+FFFF)をサポートする ([5f1ba8dd76](https://github.com/JDimproved/JDim/commit/5f1ba8dd76f50f2b8943507d7a84843739267f96)) - README.mdにビルドに関するTipsを追加する ([4363a7560b](https://github.com/JDimproved/JDim/commit/4363a7560b726fa814823a1897ece54ea70288e7)) ### [2.8.9-171003](https://github.com/JDimproved/JDim/compare/c4efac5bd2...ab77d02541) (2017-10-03) - 5ch.netに対応する ([5367fc361d](https://github.com/JDimproved/JDim/commit/5367fc361d77cad50c6a63ac16696ab2e8d69ed1)) - README.mdを作成してインストール方法の説明を追加する ([07e581f115](https://github.com/JDimproved/JDim/commit/07e581f11530970c20ffbd540cd58ff8f45bab6b), [af1ac02d26](https://github.com/JDimproved/JDim/commit/af1ac02d269c09830814725134a8d6d39a0dd8bc), [abe2fb78f3](https://github.com/JDimproved/JDim/commit/abe2fb78f3d15c55c398ce8bec0d83e7c2a94141), [aa2b0edc1a](https://github.com/JDimproved/JDim/commit/aa2b0edc1a48802503c733c802e7ab93c047dd0a), [f8d91689b5](https://github.com/JDimproved/JDim/commit/f8d91689b5d253a427820fbd082c99c9c8dd2851)) ### [2.8.9-170418](https://github.com/JDimproved/JDim/compare/57b5f412b1...c4efac5bd2) (2017-04-18) - 一部HTTPSサイト(SNI拡張を使ってるサイト)への接続が失敗するのを修正する ([d8712b97c7](https://github.com/JDimproved/JDim/commit/d8712b97c7fa8c434ad12a8621d98b3c41e8f139)) - C++11のコンパイラオプションの自動追加 (autoconf-archive パッケージが必要) ([5462869964](https://github.com/JDimproved/JDim/commit/54628699641f8595ffb96c70987a8d7e3dbf28f4)) ### [2.8.9-170416](https://github.com/JDimproved/JDim/compare/66f0eea81c...57b5f412b1) (2017-04-16) - proxyを介した2ちゃんねる系サイトへのhttpsアクセスを修正する ([fd5e01d7ef](https://github.com/JDimproved/JDim/commit/fd5e01d7efe643915c963c663d633799db9b5293), [e94065247d](https://github.com/JDimproved/JDim/commit/e94065247db8be67ba4a3b19bab830fb5f363f5e), [470d1e42bb](https://github.com/JDimproved/JDim/commit/470d1e42bbaeae8132270014c6d353de8e2b5521), [dc7d540a9b](https://github.com/JDimproved/JDim/commit/dc7d540a9bf072c7d1f934a7f5541d19f3f0c4d1)) ### [2.8.9-170411](https://github.com/JDimproved/JDim/compare/5c05922bb5...66f0eea81c) (2017-04-11) - `https`の実験的なサポート ([a191ea4a06](https://github.com/JDimproved/JDim/commit/a191ea4a069f22ca904835f514f5a1360d37f08f)) - ビルドの問題を修正する ([169f9932a4](https://github.com/JDimproved/JDim/commit/169f9932a4b58c83a26fa32e5d0427c778510dda)) - 暗黙的なboolへの変換によるコンパイルエラーを修正する ([6b79cd4a8c](https://github.com/JDimproved/JDim/commit/6b79cd4a8c9e398b781021b9bb54c9a98c0ee77e)) - 文字列のDOM解析を修正する ([518d74f06c](https://github.com/JDimproved/JDim/commit/518d74f06cd8fc4a95674da4f866a09e6b858d19)) - ローダーの無限ループ発生を修正する ([d4aa5f1cc9](https://github.com/JDimproved/JDim/commit/d4aa5f1cc9869bc98b8be6e939599eecf54d6a52)) jdim-0.10.1/docs/manual/2018.md000066400000000000000000000040031445721505100156360ustar00rootroot00000000000000--- title: 更新履歴(2018年) layout: default --- > [Top](../) > [更新履歴]({{ site.baseurl }}/history/) > {{ page.title }} ## {{ page.title }} ### [2.8.9-20181023](https://github.com/JDimproved/JDim/compare/c1c9d64a85...bb608f24b1) (2018-10-23) バージョンの日付に[コミット作成日][envnote]を使うようになった。 [envnote]: {{ site.baseurl }}/environment/#note "動作環境の記入について | JDim" - Fix hash length for git revision information ([#14](https://github.com/JDimproved/JDim/pull/14)) - Fix behaviors ([#13](https://github.com/JDimproved/JDim/pull/13)) - Add support for std::thread ([#12](https://github.com/JDimproved/JDim/pull/12)) - Add git revision information to env info ([#11](https://github.com/JDimproved/JDim/pull/11)) - Replace incompatible features ([#10](https://github.com/JDimproved/JDim/pull/10)) - Use new features ([#9](https://github.com/JDimproved/JDim/pull/9)) - Replace deprecated features ([#8](https://github.com/JDimproved/JDim/pull/8)) - Add: ubuntu 18.04 install section. ([5fc7b8b7fd](https://github.com/JDimproved/JDim/commit/5fc7b8b7fd840659900e443872b1c50149638f39)) - Deprecate usage of GLib memory profiler on 2.46 and above ([#6](https://github.com/JDimproved/JDim/pull/6)) ### [2.8.9-180424](https://github.com/JDimproved/JDim/compare/9e692ad2ba...c1c9d64a85) (2018-04-24) - Fix Loader::skip_chunk. ([#5](https://github.com/JDimproved/JDim/pull/5)) ### [2.8.9-180217](https://github.com/JDimproved/JDim/compare/be3d051dc5...9e692ad2ba) (2018-02-17) - std:stringアクセスの別修正 ([#4](https://github.com/JDimproved/JDim/pull/4)) - std:stringアクセスのチェックの修正 ([#3](https://github.com/JDimproved/JDim/pull/3)) - Check array member access more strictly ([#2](https://github.com/JDimproved/JDim/pull/2)) - Always include crypt.h header for crypt function ([#1](https://github.com/JDimproved/JDim/pull/1)) jdim-0.10.1/docs/manual/2019.md000066400000000000000000000251001445721505100156400ustar00rootroot00000000000000--- title: 更新履歴(2019年) layout: default --- > [Top](../) > [更新履歴]({{ site.baseurl }}/history/) > {{ page.title }} ## {{ page.title }} ### [0.2.0-20191027](https://github.com/JDimproved/JDim/compare/JDim-v0.2.0...362b797d53f) (2019-10-27) - Remove deprecated cpu optimization options ([#140](https://github.com/JDimproved/JDim/pull/140)) - Fix scrollbar behaviors for thread view on GTK3 ([#136](https://github.com/JDimproved/JDim/pull/136)) - Set ellipsize to dialog info label ([#135](https://github.com/JDimproved/JDim/pull/135)) - Use Gtk::FontChooserDialog instead of Gtk::FileSelectionDialog on GTK3 ([#134](https://github.com/JDimproved/JDim/pull/134)) - Improve displaying tooltip for bbs list/thread list ([#133](https://github.com/JDimproved/JDim/pull/133)) - Improve displaying manual on github.com ([#132](https://github.com/JDimproved/JDim/pull/132)) - Fix GTKMM_CHECK_VERSION macro ([#131](https://github.com/JDimproved/JDim/pull/131)) - Fix tooltips which are not near mouse pointer on bbs list/thread list for GTK3 ([#128](https://github.com/JDimproved/JDim/pull/128)) - Add github links for previous version manuals ([#130](https://github.com/JDimproved/JDim/pull/130)) - Revert "[WORKAROUND] Fix build error for manual" ([#129](https://github.com/JDimproved/JDim/pull/129)) - Use range-based for ([#126](https://github.com/JDimproved/JDim/pull/126)) - Refactor string operation ([#125](https://github.com/JDimproved/JDim/pull/125)) - Update requirements for dependencies (gtkmm >= 2.18) ([#124](https://github.com/JDimproved/JDim/pull/124)) - [WORKAROUND] Fix build error for manual ([#127](https://github.com/JDimproved/JDim/pull/127)) - Fix mouse button config to set Button5 correctly ([#123](https://github.com/JDimproved/JDim/pull/123)) - Implement tilt wheel for DragTreeView on GTK3 ([#122](https://github.com/JDimproved/JDim/pull/122)) - Fix bug which is applied ascii art font falsely on GTK3 ([#121](https://github.com/JDimproved/JDim/pull/121)) - Replace `gcry_md_hash_buffer` with `gnutls_hash_fast` ([#120](https://github.com/JDimproved/JDim/pull/120)) - Add a note of bug which is applied AA font falsely to thread view ([#119](https://github.com/JDimproved/JDim/pull/119)) - Add snapcraft.yaml ([#118](https://github.com/JDimproved/JDim/pull/118)) - Improve tab switch by mouse wheel for image view ([#117](https://github.com/JDimproved/JDim/pull/117)) - Implement tab switch by mouse wheel for GTK3 ([#114](https://github.com/JDimproved/JDim/pull/114)) - Add smooth scroll event to enable mouse wheel on GTK3 ([#113](https://github.com/JDimproved/JDim/pull/113)) - Fix configure args info to include "without" and "disable" options ([#112](https://github.com/JDimproved/JDim/pull/112)) - Fix #109: Remove cache root config providing compatible with old version ([#111](https://github.com/JDimproved/JDim/pull/111)) - Fix markdown format for the table of cache directory priority ([#115](https://github.com/JDimproved/JDim/pull/115)) - Fix #104: Implement XDG Base Directory support for cache ([#108](https://github.com/JDimproved/JDim/pull/108)) - Fix #103: Add jdim.metainfo.xml ([#107](https://github.com/JDimproved/JDim/pull/107)) - Fix #102: Update application icon installation ([#106](https://github.com/JDimproved/JDim/pull/106)) - Fix #101 Remove subversion support ([#105](https://github.com/JDimproved/JDim/pull/105)) - add CPPFLAGS, CXXFLAGS and LDFLAGS for test ([#100](https://github.com/JDimproved/JDim/pull/100)) - Fix #98: Add travis-ci.com configuration ([#99](https://github.com/JDimproved/JDim/pull/99)) ### [**JDim-v0.2.0** Release](https://github.com/JDimproved/JDim/releases/tag/JDim-v0.2.0) (2019-07-20) 主な変更点 - GTK3版をビルドするオプションを追加した(デフォルトはGTK2版) - 最大表示可能レス数を固定(11000)から変更可能にした - スレタイ検索のデフォルト設定を変更した(ff5ch.syoboi.jp) - 動作環境の表示にTLSライブラリを追加した - 旧設定ファイル(version < 1.9.5)のサポートを削除した ### [0.2.0-20190720](https://github.com/JDimproved/JDim/compare/79e90c8b2d...JDim-v0.2.0) (2019-07-20) - Release 0.2.0 ([#97](https://github.com/JDimproved/JDim/pull/97)) - Minor fixes ([#96](https://github.com/JDimproved/JDim/pull/96)) - Fix #90: Implement ENVIRONMENT::get_tlslib_version ([#94](https://github.com/JDimproved/JDim/pull/94)) - Tweak UI and configuration ([#93](https://github.com/JDimproved/JDim/pull/93)) - Add const qualifier to member functions ([#92](https://github.com/JDimproved/JDim/pull/92)) - Refactor SKELETON namespace ([#89](https://github.com/JDimproved/JDim/pull/89)) - Update constractor and destructor ([#88](https://github.com/JDimproved/JDim/pull/88)) - Improve ENVIRONMENT::get_wm() and ENVIRONMENT::get_wm_str() ([#87](https://github.com/JDimproved/JDim/pull/87)) - Fix test program build ([#85](https://github.com/JDimproved/JDim/pull/85)) - Fix #81: Add grouped configure option ([#84](https://github.com/JDimproved/JDim/pull/84)) - Fix #80: Deprecate configure option for CPU optimization ([#83](https://github.com/JDimproved/JDim/pull/83)) - Fix #79: Remove support for old version config files ([#82](https://github.com/JDimproved/JDim/pull/82)) - [DRAFT] Fix #45: Add unittest (Google Test) ([#78](https://github.com/JDimproved/JDim/pull/78)) - README.mdの整理 ([#77](https://github.com/JDimproved/JDim/pull/77)) - Update manual to remove login and simplify history ([#75](https://github.com/JDimproved/JDim/pull/75)) - Fix to save thread list column width for GTK3 ([#74](https://github.com/JDimproved/JDim/pull/74)) - Fix manual link (a.k.a. Create online manual [2/2]) ([#72](https://github.com/JDimproved/JDim/pull/72)) - Create online manual [1/2] ([#69](https://github.com/JDimproved/JDim/pull/69)) ### [0.1.0-20190430](https://github.com/JDimproved/JDim/compare/JDim-v0.1.0...79e90c8b2d) (2019-04-30) - Fix #71: Deprecate gtkmm version less than 2.18 ([#73](https://github.com/JDimproved/JDim/pull/73)) - Fix check button for usrcmdpref ([#70](https://github.com/JDimproved/JDim/pull/70)) - Fix \*BSD compile error ([#68](https://github.com/JDimproved/JDim/pull/68)) - Fix #60: Change default thread title search site (ff5ch.syoboi.jp) ([#65](https://github.com/JDimproved/JDim/pull/65)) - Tweak config dialog layout for GTK+3 ([#64](https://github.com/JDimproved/JDim/pull/64)) - Use range-based for instead of MISC::count_chr() ([#63](https://github.com/JDimproved/JDim/pull/63)) - Fix #53: 長いスレッド(>11000レス?)を開くとクラッシュする不具合の修正 ([#62](https://github.com/JDimproved/JDim/pull/62)) - Use std::unordered_set instead of std::vector for res information ([#61](https://github.com/JDimproved/JDim/pull/61)) - Fix released-event misfire for GtkGestureMultiPress ([#58](https://github.com/JDimproved/JDim/pull/58)) - Update function parameters for MISC::asc() ([#57](https://github.com/JDimproved/JDim/pull/57)) - Sanitize numeric character reference ([#56](https://github.com/JDimproved/JDim/pull/56)) - Fix #40: Make --with-gthread deprecated ([#55](https://github.com/JDimproved/JDim/pull/55)) - Add downloading dat from old URL ([#54](https://github.com/JDimproved/JDim/pull/54)) - Fix runtime warning for opening a thread since GTK+ 3.20 ([#52](https://github.com/JDimproved/JDim/pull/52)) - Fix oniguruma detection for configure script ([#51](https://github.com/JDimproved/JDim/pull/51)) - font.cpp内のmallocをnewに置き換える ([#49](https://github.com/JDimproved/JDim/pull/49)) - Fix #46: キーワード抽出を行うとクラッシュする不具合の修正 ([#48](https://github.com/JDimproved/JDim/pull/48)) - Avoid crash on extraction ([#47](https://github.com/JDimproved/JDim/pull/47)) - Add MATE and Cinnamon to ENVIRONMENT::get_wm() ([#44](https://github.com/JDimproved/JDim/pull/44)) - Replace JDLIB::hash_set_thread with std::unordered_set ([#43](https://github.com/JDimproved/JDim/pull/43)) - Fix variable type for assigning from std::string::find() ([#42](https://github.com/JDimproved/JDim/pull/42)) - Add touchscreen support (a.k.a. Add gtk3 support [2/2]) ([#39](https://github.com/JDimproved/JDim/pull/39)) - Tweak JDLIB::JDSSL::connect() ([#38](https://github.com/JDimproved/JDim/pull/38)) - Set ellipsize to window status label since GTK+ 2.6 ([#37](https://github.com/JDimproved/JDim/pull/37)) - Create CONTRIBUTING.md ([#36](https://github.com/JDimproved/JDim/pull/36)) - Fix gnutls reception ([#35](https://github.com/JDimproved/JDim/pull/35)) - Fix return types ([#34](https://github.com/JDimproved/JDim/pull/34)) - Debianでの開発環境について更新 ([#33](https://github.com/JDimproved/JDim/pull/33)) - Fix JBBS board URL for setting board info ([#31](https://github.com/JDimproved/JDim/pull/31)) - Add gtk3 support [1/2] ([#30](https://github.com/JDimproved/JDim/pull/30)) ### [**JDim-v0.1.0** Release](https://github.com/JDimproved/JDim/releases/tag/JDim-v0.1.0) (2019-01-31) ### [0.1.0-20190129](https://github.com/JDimproved/JDim/compare/f6390b7f97...JDim-v0.1.0) (2019-01-29) - Fix progname and docs ([#29](https://github.com/JDimproved/JDim/pull/29)) - Merge test branch into master ([#28](https://github.com/JDimproved/JDim/pull/28)) - Fix undefined behavior in JDLIB::hash_set_thread::get_key ([#27](https://github.com/JDimproved/JDim/pull/27)) - Add progname info ([#26](https://github.com/JDimproved/JDim/pull/26)) - Fix enabling digitlink in NodeTreeBase::parse_name ([#25](https://github.com/JDimproved/JDim/pull/25)) ### [0.1.0-20190123](https://github.com/JDimproved/JDim/compare/bb608f24b1...f6390b7f97) (2019-01-23) JDimへの名称変更に合わせてバージョン番号のリセットを行った。 - Update project ([#24](https://github.com/JDimproved/JDim/pull/24)) - Use -Wextra option ([#23](https://github.com/JDimproved/JDim/pull/23)) - Use override keyword ([#22](https://github.com/JDimproved/JDim/pull/22)) - Use compiler option "-pedantic" instead of "-pedantic-errors" ([#21](https://github.com/JDimproved/JDim/pull/21)) - Use flag pedantic errors ([#20](https://github.com/JDimproved/JDim/pull/20)) - Replace container type ([#19](https://github.com/JDimproved/JDim/pull/19)) - Fix compile warning ([#18](https://github.com/JDimproved/JDim/pull/18)) - Fix access violation in DBTREE::NodeTreeBase::receive_data() ([#17](https://github.com/JDimproved/JDim/pull/17)) jdim-0.10.1/docs/manual/2020.md000066400000000000000000001013771445721505100156430ustar00rootroot00000000000000--- title: 更新履歴(2020年) layout: default --- > [Top](../) > [更新履歴]({{ site.baseurl }}/history/) > {{ page.title }} ## {{ page.title }} ### [0.4.0-20201003](https://github.com/JDimproved/JDim/compare/JDim-v0.4.0...1fee8b4327) (2020-10-03) - Fix posting to JD support BBS ([#478](https://github.com/JDimproved/JDim/pull/478)) - Use `Gtk::IconTheme` instead of `Gtk::Widget::render_icon_pixbuf()` ([#477](https://github.com/JDimproved/JDim/pull/477)) - Use `Gtk::ColorChooserDialog` instead of `Gtk::ColorSelectionDialog` ([#476](https://github.com/JDimproved/JDim/pull/476)) - Use `GIConv` instead of the underlying iconv implementation ([#475](https://github.com/JDimproved/JDim/pull/475)) - `DrawAreaBase`: Remove deprecated `Gtk::Widget::signal_visibility_notify_event()` ([#473](https://github.com/JDimproved/JDim/pull/473)) - setupwizard: Use Gtk::Grid instead of `Gtk::Table` ([#472](https://github.com/JDimproved/JDim/pull/472)) - Remove unused macros for windows support ([#468](https://github.com/JDimproved/JDim/pull/468)) - Use `Gtk::Dialog::get_content_area()` instead of `get_vbox()` part3 ([#467](https://github.com/JDimproved/JDim/pull/467)) - Use `Gtk::Dialog::get_content_area()` instead of `get_vbox()` part2 ([#466](https://github.com/JDimproved/JDim/pull/466)) - Use `Gtk::Dialog::get_content_area()` instead of `get_vbox()` part1 ([#465](https://github.com/JDimproved/JDim/pull/465)) - Remove Windows support ([#464](https://github.com/JDimproved/JDim/pull/464)) - Fix posting to 2ch ([#463](https://github.com/JDimproved/JDim/pull/463)) - Use `Gtk::Widget::set_[hv]align()` instead of `Gtk::Misc::set_alignment()` ([#462](https://github.com/JDimproved/JDim/pull/462)) - Use `Gtk::Label::set_xalign()` instead of `Gtk::Misc::set_alignment()` ([#461](https://github.com/JDimproved/JDim/pull/461)) - Use `Gtk::Widget::property_margin()` instead of `Gtk::Misc::set_padding()` ([#460](https://github.com/JDimproved/JDim/pull/460)) - `Board2chCompati`: Cut html source for analyzing form data ([#459](https://github.com/JDimproved/JDim/pull/459)) - `Board2ch`: Fix clearing keyword for new article ([#458](https://github.com/JDimproved/JDim/pull/458)) - Get rid of snap build badge from README.md ([#457](https://github.com/JDimproved/JDim/pull/457)) - Update histories ([#456](https://github.com/JDimproved/JDim/pull/456)) - Use `Gtk::Paned` instead of `Gtk::VPaned` ([#455](https://github.com/JDimproved/JDim/pull/455)) - Use `Gtk::Paned` instead of `Gtk::HPaned` ([#454](https://github.com/JDimproved/JDim/pull/454)) - Use `Gtk::GestureMultiPress` instead of `Gtk::Button::signal_pressed/released` ([#453](https://github.com/JDimproved/JDim/pull/453)) - Use icon theme instead of `Gtk::Arrow` ([#452](https://github.com/JDimproved/JDim/pull/452)) - Change default regex library to Glib Regex ([#451](https://github.com/JDimproved/JDim/pull/451)) - Deprecate gtkmm version less than 3.22 ([#450](https://github.com/JDimproved/JDim/pull/450)) - Use cursor name instead of `Gdk::CursorType` ([#449](https://github.com/JDimproved/JDim/pull/449)) - Use `Gdk::RGBA` instead of `Gdk::Color` ([#448](https://github.com/JDimproved/JDim/pull/448)) - Use `Gtk::Scrollbar` instead of `Gtk::VScrollbar` ([#447](https://github.com/JDimproved/JDim/pull/447)) - message: Add missing headers for Makefile.am ([#446](https://github.com/JDimproved/JDim/pull/446)) - `DrawAreaBase`: Remove compile condition for touchscreen ([#442](https://github.com/JDimproved/JDim/pull/442)) - `DrawAreaBase`: Remove compile conditions for drawing thread view ([#441](https://github.com/JDimproved/JDim/pull/441)) - `DrawAreaBase`: Remove double bufferd processing for gtk < 3.9.2 ([#440](https://github.com/JDimproved/JDim/pull/440)) - `EditTextView`: Remove compile condition for word/line selection ([#439](https://github.com/JDimproved/JDim/pull/439)) - `Core`: Remove wrapper class which is no longer needed ([#438](https://github.com/JDimproved/JDim/pull/438)) - `ICON_Manager`: Remove compile condition for rendering icon ([#437](https://github.com/JDimproved/JDim/pull/437)) - Remove compile conditions for style setting ([#436](https://github.com/JDimproved/JDim/pull/436)) - Improve compile conditions for `MISC::WarpPointer()` ([#435](https://github.com/JDimproved/JDim/pull/435)) - Remove compile conditions for value getting/setting part5 ([#434](https://github.com/JDimproved/JDim/pull/434)) - Remove compile conditions for value getting/setting part4 ([#433](https://github.com/JDimproved/JDim/pull/433)) - Remove compile conditions for value getting/setting part3 ([#432](https://github.com/JDimproved/JDim/pull/432)) - Remove compile conditions for value getting/setting part2 ([#431](https://github.com/JDimproved/JDim/pull/431)) - Remove compile conditions for value getting/setting part1 ([#430](https://github.com/JDimproved/JDim/pull/430)) - prefdiag: Remove compile conditions for adding member function ([#429](https://github.com/JDimproved/JDim/pull/429)) - `DelImgCacheDiag`: Remove compile conditions for member function ([#428](https://github.com/JDimproved/JDim/pull/428)) - control: Remove compile conditions for iterating elements ([#427](https://github.com/JDimproved/JDim/pull/427)) - cache: Remove compile conditions for local variables ([#426](https://github.com/JDimproved/JDim/pull/426)) - Remove compile conditions for supporting smooth scroll ([#425](https://github.com/JDimproved/JDim/pull/425)) - `ViewNotebook`: Remove member functions which are no longer in use ([#424](https://github.com/JDimproved/JDim/pull/424)) - `ToolBarNotebook`: Remove member function which is no longer in use ([#423](https://github.com/JDimproved/JDim/pull/423)) - Add note in the case of running on old MATE desktop ([#422](https://github.com/JDimproved/JDim/pull/422)) - Add note for configure option `--with-pangolayout` ([#421](https://github.com/JDimproved/JDim/pull/421)) - `TabSwtichButton`: Remove member function which are no longer in use ([#420](https://github.com/JDimproved/JDim/pull/420)) - `TabNotebook`: Remove member functions which are no longer in use ([#419](https://github.com/JDimproved/JDim/pull/419)) - `PopupWinBase`: Remove member functions which are no longer in use ([#418](https://github.com/JDimproved/JDim/pull/418)) - `JDToolbar`: Remove member function which is no longer in use ([#417](https://github.com/JDimproved/JDim/pull/417)) - `DragableNoteBook`: Remove member functions which are no longer in use ([#416](https://github.com/JDimproved/JDim/pull/416)) - `Core`: Remove member functions which are no longer in use ([#415](https://github.com/JDimproved/JDim/pull/415)) - Replace `GtkNotebookPage*` with `Gtk::Widget*` ([#414](https://github.com/JDimproved/JDim/pull/414)) - `CookieManager`: Accept empty value Cookie ([#413](https://github.com/JDimproved/JDim/pull/413)) - Remove `JDLIB::Thread` implementations for pthread and gthread ([#411](https://github.com/JDimproved/JDim/pull/411)) - Change buildinfo header generation to do every time ([#410](https://github.com/JDimproved/JDim/pull/410)) - Update version requirement for GnuTLS (>= 3.4.10) ([#409](https://github.com/JDimproved/JDim/pull/409)) - Remove deprecated configure option `--with-regex=pcre` ([#408](https://github.com/JDimproved/JDim/pull/408)) - Remove deprecated configure option `--with-xdgopen` ([#407](https://github.com/JDimproved/JDim/pull/407)) - Remove configure option `--with-gtkmm3` ([#406](https://github.com/JDimproved/JDim/pull/406)) - Remove deprecated configure options for thread library ([#405](https://github.com/JDimproved/JDim/pull/405)) - Remove deprecated configure options `--with-[oniguruma|pcre]` ([#404](https://github.com/JDimproved/JDim/pull/404)) - Remove deprecated configure option `--with-openssl` ([#403](https://github.com/JDimproved/JDim/pull/403)) ### [**JDim-v0.4.0** Release](https://github.com/JDimproved/JDim/releases/tag/JDim-v0.4.0) (2020-07-18) 主な変更点 - GTK2版は廃止されGTK3版がデフォルトになる - スレビューのメール欄フォント設定を追加した - 高参照レス抽出(赤レス抽出)を追加した - 掲示板サイトのhttps化に対する対応を進めた - 正規表現ライブラリGlib Regexのサポートを追加した - Python製のビルドツール [meson](https://mesonbuild.com) のサポートを追加した(実験的な機能) ### [0.4.0-20200718](https://github.com/JDimproved/JDim/compare/e6e4e5bc37...JDim-v0.4.0) (2020-07-18) - Release 0.4.0 ([#402](https://github.com/JDimproved/JDim/pull/402)) - Add more specific description for building by meson ([#401](https://github.com/JDimproved/JDim/pull/401)) - `BBSListViewBase`: Use const reference to make alias for local variable ([#400](https://github.com/JDimproved/JDim/pull/400)) - Replace `snprintf` with string format function ([#399](https://github.com/JDimproved/JDim/pull/399)) - `ArticleBase`: Fix integer overflow ([#398](https://github.com/JDimproved/JDim/pull/398)) - Snap: Fix wrong keyword "runs-on" to "run-on" for snapcraft.yaml ([#397](https://github.com/JDimproved/JDim/pull/397)) - Snap: Fix architectures settings ([#396](https://github.com/JDimproved/JDim/pull/396)) - Snap: Drop architectures i386, ppc64el and s390x ([#395](https://github.com/JDimproved/JDim/pull/395)) - Snap: Update dependencies to use GNOME 3.34 snapcraft extension ([#394](https://github.com/JDimproved/JDim/pull/394)) - Tweak meson.build ([#393](https://github.com/JDimproved/JDim/pull/393)) - `JDSSL`: Fix dead assignment ([#392](https://github.com/JDimproved/JDim/pull/392)) - `environment`: Move local variable to inner scope ([#391](https://github.com/JDimproved/JDim/pull/391)) - `post`: Fix compiler waring for `-Wnon-virtual-dtor` ([#390](https://github.com/JDimproved/JDim/pull/390)) - `EditTextView`: Fix bounds check ([#389](https://github.com/JDimproved/JDim/pull/389)) - Add meson build for experimental support ([#388](https://github.com/JDimproved/JDim/pull/388)) - `DrawAreaBase`: Add null check before dereferencing pointer ([#387](https://github.com/JDimproved/JDim/pull/387)) - `DrawAreaBase`: Fix dead increment ([#386](https://github.com/JDimproved/JDim/pull/386)) - `Loader`: Fix dead assignment ([#385](https://github.com/JDimproved/JDim/pull/385)) - `ImageAdmin`: Add null check before dereferencing pointer ([#384](https://github.com/JDimproved/JDim/pull/384)) - `MessageViewBase`: Remove redundant null check ([#383](https://github.com/JDimproved/JDim/pull/383)) - `JDWinMain`: Unite if-statement blocks for same condition ([#382](https://github.com/JDimproved/JDim/pull/382)) - `JDWinMain`: Fix calling virtual function from the ctor and dtor ([#381](https://github.com/JDimproved/JDim/pull/381)) - `EditTextView`: Fix redundant initialization ([#380](https://github.com/JDimproved/JDim/pull/380)) - Revert "Update snapcraft.yaml to bundle the gnome-3-34 extension" ([#379](https://github.com/JDimproved/JDim/pull/379)) - Update snapcraft.yaml to bundle the gnome-3-34 extension ([#378](https://github.com/JDimproved/JDim/pull/378)) - `Loader`: Fix dead assignment ([#377](https://github.com/JDimproved/JDim/pull/377)) - `Core`: Fix member initialization ([#376](https://github.com/JDimproved/JDim/pull/376)) - `Core`: Add const qualifier to function parameters ([#375](https://github.com/JDimproved/JDim/pull/375)) - Rename local variables to avoid shadowing ([#374](https://github.com/JDimproved/JDim/pull/374)) - Add explicit keyword to constructors which have one argument ([#373](https://github.com/JDimproved/JDim/pull/373)) - Root: Use `std::find_if()` instead of iterator loop ([#372](https://github.com/JDimproved/JDim/pull/372)) - `mousekeypref`: Modify loop statements ([#371](https://github.com/JDimproved/JDim/pull/371)) - Remove local variable to avoid unread value ([#370](https://github.com/JDimproved/JDim/pull/370)) - Move local variables to inner scope to avoid unused value ([#369](https://github.com/JDimproved/JDim/pull/369)) - Add void cast for unread variables ([#368](https://github.com/JDimproved/JDim/pull/368)) - Bump version to 0.4.0-beta ([#367](https://github.com/JDimproved/JDim/pull/367)) - Fix make rule to not recompile all test codes ([#365](https://github.com/JDimproved/JDim/pull/365)) - Add description for migemo build option ([#366](https://github.com/JDimproved/JDim/pull/366)) - Remove preprocessor for zlib version check ([#364](https://github.com/JDimproved/JDim/pull/364)) - Deprecate configure option for PCRE ([#363](https://github.com/JDimproved/JDim/pull/363)) - Add configure option for Glib Regex ([#362](https://github.com/JDimproved/JDim/pull/362)) - `MouseKeyDiag`: Use range-based for statements to avoid shadowing ([#360](https://github.com/JDimproved/JDim/pull/360)) - `Dom`: Move local variable to inner scope ([#359](https://github.com/JDimproved/JDim/pull/359)) - `JDTreeViewBase`: Move local variable to inner scope ([#358](https://github.com/JDimproved/JDim/pull/358)) - misc: Move local variable to inner scope ([#357](https://github.com/JDimproved/JDim/pull/357)) - `NodeTreeBase`: Move local variable to inner scope ([#356](https://github.com/JDimproved/JDim/pull/356)) - `ImgLoader`: Move local variable to inner scope ([#355](https://github.com/JDimproved/JDim/pull/355)) - `ImageAdmin`: Move local variable to inner scope ([#354](https://github.com/JDimproved/JDim/pull/354)) - `Root`: Modify local variable to avoid shadowing ([#353](https://github.com/JDimproved/JDim/pull/353)) - `DrawAreaBase`: Move local variable to inner scope ([#352](https://github.com/JDimproved/JDim/pull/352)) - `ImageViewBase`: Move local variables to inner scope to avoid shadowing ([#351](https://github.com/JDimproved/JDim/pull/351)) - Remove local var declaration to avoid shadowing ([#350](https://github.com/JDimproved/JDim/pull/350)) - Rename local variable to avoid shadowing ([#349](https://github.com/JDimproved/JDim/pull/349)) - Modify default bbsmenu URL to https ([#348](https://github.com/JDimproved/JDim/pull/348)) - Support processing for moving bbsmenu by 301 Moved Permanently ([#347](https://github.com/JDimproved/JDim/pull/347)) - Support processing for moving board by 301 Moved Permanently ([#346](https://github.com/JDimproved/JDim/pull/346)) - `Root`: Change parameters to pass by value ([#345](https://github.com/JDimproved/JDim/pull/345)) - Modify parameter type from reference to value ([#344](https://github.com/JDimproved/JDim/pull/344)) - Add const qualifier to function parameters ([#343](https://github.com/JDimproved/JDim/pull/343)) - Unite if-statement blocks for same condition ([#342](https://github.com/JDimproved/JDim/pull/342)) - Improve https hack for proxy connection ([#341](https://github.com/JDimproved/JDim/pull/341)) - Apply proxy settings to loading bbslist ([#340](https://github.com/JDimproved/JDim/pull/340)) - Remove unused local variables ([#339](https://github.com/JDimproved/JDim/pull/339)) - Remove unnecessary condition for assigning value ([#338](https://github.com/JDimproved/JDim/pull/338)) - Fix re-assign value before the old one is used ([#337](https://github.com/JDimproved/JDim/pull/337)) - Remove invariable condition expression ([#336](https://github.com/JDimproved/JDim/pull/336)) - Use STL Algorithm for raw loops ([#335](https://github.com/JDimproved/JDim/pull/335)) - `DrawAreaBase`: Use standard library math functions ([#334](https://github.com/JDimproved/JDim/pull/334)) - Deprecate version less then 3.18 for gtkmm ([#333](https://github.com/JDimproved/JDim/pull/333)) - `IOMonitor`: Add compile condition to private member function ([#331](https://github.com/JDimproved/JDim/pull/331)) - `Dom`: Fix comparison condition for unsigned integer ([#330](https://github.com/JDimproved/JDim/pull/330)) - Rename local variable names to avoid shadowing ([#329](https://github.com/JDimproved/JDim/pull/329)) - misc: Remove redundant condition for NUL character check ([#328](https://github.com/JDimproved/JDim/pull/328)) - `JDEntry`: Use member initializer ([#327](https://github.com/JDimproved/JDim/pull/327)) - image: Replace printf format for image size with `std::to_string()` ([#326](https://github.com/JDimproved/JDim/pull/326)) - `Iconv`: Fix printf format for print debug ([#325](https://github.com/JDimproved/JDim/pull/325)) - Update HTTP request for new article creation ([#324](https://github.com/JDimproved/JDim/pull/324)) - Improve analyzation of keywords for writing and newarticle ([#323](https://github.com/JDimproved/JDim/pull/323)) - main: Fix C-style pointer casting ([#322](https://github.com/JDimproved/JDim/pull/322)) - `DrawAreaBase`: Fix bitwise OR operation to logical OR for if condition ([#321](https://github.com/JDimproved/JDim/pull/321)) - Remove unnecessary using declaration for member function ([#320](https://github.com/JDimproved/JDim/pull/320)) - Separate cookie generation for posting ([#319](https://github.com/JDimproved/JDim/pull/319)) - Rename cookie functions ([#318](https://github.com/JDimproved/JDim/pull/318)) - Fix thread title search for intial setting (2020-06) ([#316](https://github.com/JDimproved/JDim/pull/316)) - Fix function parameter to const reference ([#312](https://github.com/JDimproved/JDim/pull/312)) - Fix constructor parameter to const reference ([#311](https://github.com/JDimproved/JDim/pull/311)) - Fix to use member initializer lists ([#310](https://github.com/JDimproved/JDim/pull/310)) - Fix std::set insertation to avoid double searching ([#309](https://github.com/JDimproved/JDim/pull/309)) - `ImgProvider`: Fix iterator loop to use `std::find_if()` ([#308](https://github.com/JDimproved/JDim/pull/308)) - Fix array bounds checking ([#307](https://github.com/JDimproved/JDim/pull/307)) - Update documents ([#306](https://github.com/JDimproved/JDim/pull/306)) - `Dom`: Fix accessing first character for `std::string` ([#305](https://github.com/JDimproved/JDim/pull/305)) - `ToolMenuButton`: Fix mutable member function in assert macro ([#304](https://github.com/JDimproved/JDim/pull/304)) - `Admin`: Fix the class to be compliant with rule of three ([#303](https://github.com/JDimproved/JDim/pull/303)) - Implement `SimpleCookieManager` ([#302](https://github.com/JDimproved/JDim/pull/302)) - Fix loading local dat file specified by command line arguments ([#301](https://github.com/JDimproved/JDim/pull/301)) - Fix displaying tab icon for extraction tab ([#300](https://github.com/JDimproved/JDim/pull/300)) - Fix mouse wheel scrolling for thread list ([#299](https://github.com/JDimproved/JDim/pull/299)) - Fix unexpected jump by mouse over url ([#298](https://github.com/JDimproved/JDim/pull/298)) - Fix unmatched paren for `MISC::getenv_limited()` ([#293](https://github.com/JDimproved/JDim/pull/293)) - `MessageAdmin`: Fix if-statement which is always true ([#292](https://github.com/JDimproved/JDim/pull/292)) - Fix dangling pointer for logging file ([#291](https://github.com/JDimproved/JDim/pull/291)) - Fix ignored return value for `MISC::get_hostname()` ([#290](https://github.com/JDimproved/JDim/pull/290)) - Replace char buffer with `std::string` for `DBTREE::Board` classes ([#289](https://github.com/JDimproved/JDim/pull/289)) - `JDWindow`: Fix calling virtual function from the ctor and dtor ([#288](https://github.com/JDimproved/JDim/pull/288)) - `JDWindow`: Fix calling virtual function from the destructor ([#287](https://github.com/JDimproved/JDim/pull/287)) - `Admin`: Fix calling virtual function from the destructor ([#286](https://github.com/JDimproved/JDim/pull/286)) - `RuleLoader`: Fix calling virtual function from the ctor and dtor ([#285](https://github.com/JDimproved/JDim/pull/285)) - `ArticleBase`: Fix calling virtual function from the destructor ([#284](https://github.com/JDimproved/JDim/pull/284)) - `ArticleViewMain`: Fix calling virtual function from the destructor ([#283](https://github.com/JDimproved/JDim/pull/283)) - `View`: Fix calling virtual function from the destructor ([#282](https://github.com/JDimproved/JDim/pull/282)) - `ToolBar`: Fix calling virtual function from the constructor ([#281](https://github.com/JDimproved/JDim/pull/281)) - `ArticleViewSearch`: Fix calling virtual function from the destructor ([#280](https://github.com/JDimproved/JDim/pull/280)) - `NodeTreeBase`: Fix calling virtual function from the destructor ([#279](https://github.com/JDimproved/JDim/pull/279)) - `ViewHistory`: Fix `ArticleBase::get_current_url()` to const member ([#278](https://github.com/JDimproved/JDim/pull/278)) - Fix memory leak for `JDLIB::Timeout::connect()` ([#277](https://github.com/JDimproved/JDim/pull/277)) - Fix thread title search for the initial setting ([#275](https://github.com/JDimproved/JDim/pull/275)) - `ArticleBase`: Fix `ArticleBase::empty()` to const member ([#274](https://github.com/JDimproved/JDim/pull/274)) - `ArticleHash`: Fix the class to be compliant with rule of three ([#273](https://github.com/JDimproved/JDim/pull/273)) - `DrawAreaBase`: Fix redundant condition ([#272](https://github.com/JDimproved/JDim/pull/272)) - `ConfigItems`: Fix member initialization ([#271](https://github.com/JDimproved/JDim/pull/271)) - `Iconv`: Fix member initialization ([#270](https://github.com/JDimproved/JDim/pull/270)) - `ArticleHash`: Fix member initialization ([#269](https://github.com/JDimproved/JDim/pull/269)) - Add known issues for Wayland to documents ([#268](https://github.com/JDimproved/JDim/pull/268)) - Remove p2.2ch.sc login ([#266](https://github.com/JDimproved/JDim/pull/266)) - Fix crash by click anchor for GTK3 on Wayland ([#265](https://github.com/JDimproved/JDim/pull/265)) - Deprecate `--with-xdgopen` ([#264](https://github.com/JDimproved/JDim/pull/264)) - `Search_Manager`: Fix member initialization ([#263](https://github.com/JDimproved/JDim/pull/263)) - `CheckUpdate_Manager`: Fix member initialization ([#262](https://github.com/JDimproved/JDim/pull/262)) - `JDWindow`: Fix member initialization ([#261](https://github.com/JDimproved/JDim/pull/261)) - `SKELETON::View`: Fix member initialization ([#260](https://github.com/JDimproved/JDim/pull/260)) - `Play_Sound`: Fix member initialization ([#259](https://github.com/JDimproved/JDim/pull/259)) - Split history sections ([#258](https://github.com/JDimproved/JDim/pull/258)) ### [0.3.0-20200426](https://github.com/JDimproved/JDim/compare/JDim-v0.3.0...e6e4e5bc37) (2020-04-26) - Update histories ([#255](https://github.com/JDimproved/JDim/pull/255)) - `TabLabel`: Fix member initialization ([#254](https://github.com/JDimproved/JDim/pull/254)) - Pre-notify updating recommended requirements to GTK 3.22+ ([#251](https://github.com/JDimproved/JDim/pull/251)) - Remove deprecated `g_mem_set_vtable()` ([#250](https://github.com/JDimproved/JDim/pull/250)) - Fix compiler warning for `MISC::recover_path()` ([#249](https://github.com/JDimproved/JDim/pull/249)) - `PaneControl`: Fix member initialization ([#248](https://github.com/JDimproved/JDim/pull/248)) - `ImgLoader`: Fix member initialization ([#247](https://github.com/JDimproved/JDim/pull/247)) - Add RFC repository link to documents ([#246](https://github.com/JDimproved/JDim/pull/246)) - Remove the description about external board machi bbs ([#245](https://github.com/JDimproved/JDim/pull/245)) - Deprecate gtk2 version ([#244](https://github.com/JDimproved/JDim/pull/244)) - Remove the option to fetch kako logs using offlaw2 ([#243](https://github.com/JDimproved/JDim/pull/243)) - `ImageAreaIcon`: Fix member initialization ([#241](https://github.com/JDimproved/JDim/pull/241)) - Refactor migemo functions ([#240](https://github.com/JDimproved/JDim/pull/240)) - `EditTreeView`: Fix member initialization ([#239](https://github.com/JDimproved/JDim/pull/239)) - `MsgDiag`: Fix member initialization ([#238](https://github.com/JDimproved/JDim/pull/238)) - Fix buffer overrun for `NodeTreeBase::parse_html()` ([#237](https://github.com/JDimproved/JDim/pull/237)) - Deprecate thread configure options except for std ([#236](https://github.com/JDimproved/JDim/pull/236)) - Fix checking sssp scheme ([#235](https://github.com/JDimproved/JDim/pull/235)) - Add const qualifier to `XML::Dom*` variables ([#234](https://github.com/JDimproved/JDim/pull/234)) - `ImageViewBase`: Fix member initialization ([#233](https://github.com/JDimproved/JDim/pull/233)) - Change button config name "ブックマーク" with "しおりの設定/解除" ([#232](https://github.com/JDimproved/JDim/pull/232)) - Add the toggle command for posted mark to mouse button config ([#231](https://github.com/JDimproved/JDim/pull/231)) - `ImageViewMain`: Fix member initialization ([#227](https://github.com/JDimproved/JDim/pull/227)) - Replace char buffer with `std::string` for `SKELETON::TextLoader` ([#226](https://github.com/JDimproved/JDim/pull/226)) - `XML::Dom`: Change behavior for `insertBefore()` ([#225](https://github.com/JDimproved/JDim/pull/225)) - Refactor `XML::Dom` part2 ([#224](https://github.com/JDimproved/JDim/pull/224)) - Replace char buffer with `std::string` for `CACHE::jdcopy()` ([#223](https://github.com/JDimproved/JDim/pull/223)) - `ArticleViewBase`: Fix member initialization ([#222](https://github.com/JDimproved/JDim/pull/222)) - Refactor `XML::Dom` ([#221](https://github.com/JDimproved/JDim/pull/221)) - Replace char buffer with `std::string` for `DBTREE::NodeTreeBase` [2/2] ([#220](https://github.com/JDimproved/JDim/pull/220)) - `ArticleViewMain`: Fix member initialization ([#219](https://github.com/JDimproved/JDim/pull/219)) - Fix thread view font configuration for GTK2 ([#218](https://github.com/JDimproved/JDim/pull/218)) - snap: Remove dbus slot from snapcraft.yaml ([#217](https://github.com/JDimproved/JDim/pull/217)) - `Iconv::convert`: handle emoji subdivision flags sequence ([#216](https://github.com/JDimproved/JDim/pull/216)) - Implement high reference extraction ([#215](https://github.com/JDimproved/JDim/pull/215)) - Replace char buffer with `std::string` for `DBTREE::NodeTreeBase` [1/2] ([#212](https://github.com/JDimproved/JDim/pull/212)) - Fix compiler warning -Wformat-truncation= for `MISC::timettostr()` ([#211](https://github.com/JDimproved/JDim/pull/211)) - Fix wordings for about:config favorite category to directory ([#213](https://github.com/JDimproved/JDim/pull/213)) - Replace `XMLDomList` with `std::list` ([#210](https://github.com/JDimproved/JDim/pull/210)) - `DrawAreaBase`: Fix member initialization ([#208](https://github.com/JDimproved/JDim/pull/208)) - Initialize some `bool` members for search mode ([#207](https://github.com/JDimproved/JDim/pull/207)) - Update manual ([#205](https://github.com/JDimproved/JDim/pull/205)) - `layout_one_text_node`: `reset br_size` after writing characters ([#206](https://github.com/JDimproved/JDim/pull/206)) - Reduce memory usage for font width caches ([#204](https://github.com/JDimproved/JDim/pull/204)) - Implement mouse gesture wheel for GTK3 ([#203](https://github.com/JDimproved/JDim/pull/203)) - Fix thread view popup window scrolling for GTK3 ([#202](https://github.com/JDimproved/JDim/pull/202)) - Add autoconf-archive to build requirement ([#201](https://github.com/JDimproved/JDim/pull/201)) - Fix out-of-range for `JDLIB::Regex::exec()` ([#199](https://github.com/JDimproved/JDim/pull/199)) - `MISC::asc`: Fix crash when searching title on Machi BBS ([#197](https://github.com/JDimproved/JDim/pull/197)) - Root: Fix parsing bbsmenu for 2ch.sc and next2ch.net ([#196](https://github.com/JDimproved/JDim/pull/196)) - Separate mail font ([#195](https://github.com/JDimproved/JDim/pull/195)) - Remove legacy gtk2 codes for less than version 2.24 ([#194](https://github.com/JDimproved/JDim/pull/194)) - Remove unused `NodeTreeBase::get_raw_res_str()` ([#193](https://github.com/JDimproved/JDim/pull/193)) - Improve `JDLIB::HEAP` to return pointer aligned by specified type ([#192](https://github.com/JDimproved/JDim/pull/192)) - Fix not setting result to the out parameter of `NodeTreeMachi::raw2dat()` ([#191](https://github.com/JDimproved/JDim/pull/191)) - Replace char buffer with `std::string` for `DBTREE::NodeTreeMachi` ([#190](https://github.com/JDimproved/JDim/pull/190)) - Remove zlib client codes for version < 1.2.0 ([#189](https://github.com/JDimproved/JDim/pull/189)) - Fix buffer overrun for `MISC::is_url_scheme_impl()` test ([#188](https://github.com/JDimproved/JDim/pull/188)) - Fix a `-Wstringop-overflow` compiler warning in `create_trip_newtype()` ([#187](https://github.com/JDimproved/JDim/pull/187)) - Replace char buffer with `std::string` for `DBTREE::NodeTreeJBBS` ([#186](https://github.com/JDimproved/JDim/pull/186)) - Fix number of bytes for `strncpy` argument of `Css_Manager::create_textnode()` ([#185](https://github.com/JDimproved/JDim/pull/185)) - `HEAP::heap_alloc()`: adjust alignment ([#184](https://github.com/JDimproved/JDim/pull/184)) - `CONTROL::get_keyconfig` use `snprintf` and set copying buffer size correctly ([#181](https://github.com/JDimproved/JDim/pull/181)) - Replace dynamic allocation with local variable for `struct utsname` ([#179](https://github.com/JDimproved/JDim/pull/179)) - Remove deprecated `--with-sessionlib=gnomeui` option ([#178](https://github.com/JDimproved/JDim/pull/178)) - Update requirements for dependencies (gtkmm >= 2.24) ([#177](https://github.com/JDimproved/JDim/pull/177)) - `MISC::remove_space`: handle the case str consists of just spaces ([#176](https://github.com/JDimproved/JDim/pull/176)) - Migrate snap package from legacy helper to gnome extension ([#174](https://github.com/JDimproved/JDim/pull/174)) - Set std::thread as default for configure script ([#173](https://github.com/JDimproved/JDim/pull/173)) - Set gtkmm3 as default for configure script ([#172](https://github.com/JDimproved/JDim/pull/172)) ### [**JDim-v0.3.0** Release](https://github.com/JDimproved/JDim/releases/tag/JDim-v0.3.0) (2020-01-18) 主な変更点 - GTK3版の安定性が向上した - freedesktop.org規格の対応を進めた - JDのキャッシュディレクトリ(`~/.jd`)を使わないようにするconfigureオプションを追加した - ロゴを更新した - Snapパッケージを公開した (GTK3版) - 古いCPUに合わせて最適化するconfigureオプションを削除した ### [0.3.0-20200118](https://github.com/JDimproved/JDim/compare/362b797d53f...JDim-v0.3.0) (2020-01-18) - Release 0.3.0 ([#169](https://github.com/JDimproved/JDim/pull/169)) - Refactor `ARTICLE::DrawAreaBase` ([#168](https://github.com/JDimproved/JDim/pull/168)) - Replace char buffer with `std::string` for `DBTREE::BoardBase` ([#167](https://github.com/JDimproved/JDim/pull/167)) - Update snapcraft grade to stable ([#165](https://github.com/JDimproved/JDim/pull/165)) - Replace char buffer with `std::string` for `MISC::Iconv()` ([#166](https://github.com/JDimproved/JDim/pull/166)) - Update year to 2020 ([#164](https://github.com/JDimproved/JDim/pull/164)) - Add a description for undocumented operation to the manual ([#163](https://github.com/JDimproved/JDim/pull/163)) - Replace char buffer with `std::string` part2 ([#162](https://github.com/JDimproved/JDim/pull/162)) - Unset 404 link on the GitHub Actions CI badge ([#161](https://github.com/JDimproved/JDim/pull/161)) - Add GitHub Actions configuration for CI ([#160](https://github.com/JDimproved/JDim/pull/160)) - Replace char buffer with `std::string` ([#159](https://github.com/JDimproved/JDim/pull/159)) - Implement MessageView word/line selection by mouse click for gtk3.16+ ([#158](https://github.com/JDimproved/JDim/pull/158)) - Fix logo color for "im" ([#156](https://github.com/JDimproved/JDim/pull/156)) - Remove legacy gtk2 codes for less than version 2.18 ([#155](https://github.com/JDimproved/JDim/pull/155)) - Fix icon background to transparency ([#154](https://github.com/JDimproved/JDim/pull/154)) - Fix deprecated warning for including `` ([#153](https://github.com/JDimproved/JDim/pull/153)) - Update logo ([#152](https://github.com/JDimproved/JDim/pull/152)) - Fix menu item labels for toolbar overflow menu ([#151](https://github.com/JDimproved/JDim/pull/151)) - Replace `NULL` and `0` for being assigned to pointer with `nullptr` ([#150](https://github.com/JDimproved/JDim/pull/150)) - Remove make rule which generates unused compiler info ([#149](https://github.com/JDimproved/JDim/pull/149)) - Use `std::to_string` instead of `MISC::itostr` ([#148](https://github.com/JDimproved/JDim/pull/148)) - Fix unable for initialization by `JDIM_CACHE` on compat mode ([#147](https://github.com/JDimproved/JDim/pull/147)) - Update history ([#143](https://github.com/JDimproved/JDim/pull/143)) - Deprecate `--with-sessionlib=gnomeui` option ([#142](https://github.com/JDimproved/JDim/pull/142)) - Deprecate gtkmm version less than 2.24 ([#141](https://github.com/JDimproved/JDim/pull/141)) jdim-0.10.1/docs/manual/2021.md000066400000000000000000001110411445721505100156310ustar00rootroot00000000000000--- title: 更新履歴(2021年) layout: default --- > [Top](../) > [更新履歴]({{ site.baseurl }}/history/) > {{ page.title }} ## {{ page.title }} ### [0.6.0-20211106](https://github.com/JDimproved/JDim/compare/JDim-v0.6.0...344da3bdc1) (2021-11-06) - Update histories ([#850](https://github.com/JDimproved/JDim/pull/850)) - Fix URL redirection for bbsmenu and board to handle 302 Found ([#849](https://github.com/JDimproved/JDim/pull/849)) - `LoginBe`: Update iterator for loop ([#847](https://github.com/JDimproved/JDim/pull/847)) - `LinkFilterPref`: Update iterator for loop ([#846](https://github.com/JDimproved/JDim/pull/846)) - tfidf: Update iterator for loop ([#845](https://github.com/JDimproved/JDim/pull/845)) - Improve named color support for `jd.css` ([#844](https://github.com/JDimproved/JDim/pull/844)) - `Loader`: Fix assignment in loop ([#843](https://github.com/JDimproved/JDim/pull/843)) - miscutil: Update iterator for loop ([#842](https://github.com/JDimproved/JDim/pull/842)) - miscgtk: Update iterator for loop ([#841](https://github.com/JDimproved/JDim/pull/841)) - `Loader`: Update iterator for loop ([#840](https://github.com/JDimproved/JDim/pull/840)) - Autotools: Update generation of test/Makefile ([#839](https://github.com/JDimproved/JDim/pull/839)) - `SettingLoader`: Set max res number by `BBS_THREAD_STOP` from SETTING.TXT ([#838](https://github.com/JDimproved/JDim/pull/838)) - `ImageViewBase`: Update iterator for loop ([#837](https://github.com/JDimproved/JDim/pull/837)) - `ICON_Manager`: Update iterator for loop ([#836](https://github.com/JDimproved/JDim/pull/836)) - `History_Manager`: Update iterator for loop ([#835](https://github.com/JDimproved/JDim/pull/835)) - `NodeTreeBase::parse_html` / `check_link_impl`: modify length check ([#834](https://github.com/JDimproved/JDim/pull/834)) - `GlobalAbonePref`: Update iterator for loop ([#832](https://github.com/JDimproved/JDim/pull/832)) - `FontColorPref`: Update iterator for loop ([#831](https://github.com/JDimproved/JDim/pull/831)) - environment: Update iterator for loop ([#830](https://github.com/JDimproved/JDim/pull/830)) - Add abone config for default name and no id ([#829](https://github.com/JDimproved/JDim/pull/829)) - `DispatchManager`: Update iterator for loop ([#828](https://github.com/JDimproved/JDim/pull/828)) - dbtree: Update iterator for loop part2 ([#827](https://github.com/JDimproved/JDim/pull/827)) - `Root`: Update iterator for loop part5 ([#826](https://github.com/JDimproved/JDim/pull/826)) - `Root`: Update iterator for loop part4 ([#825](https://github.com/JDimproved/JDim/pull/825)) - `Root`: Update iterator for loop part3 ([#824](https://github.com/JDimproved/JDim/pull/824)) - Add thread abone config for low number of res ([#823](https://github.com/JDimproved/JDim/pull/823)) - `Root`: Update iterator for loop part2 ([#822](https://github.com/JDimproved/JDim/pull/822)) - `Root`: Update iterator for loop part1 ([#821](https://github.com/JDimproved/JDim/pull/821)) - `BoardViewBase`: Improve update of thread speed ([#819](https://github.com/JDimproved/JDim/pull/819)) - `NodeTreeMachi`: Update iterator for loop ([#818](https://github.com/JDimproved/JDim/pull/818)) - `NodeTreeBase`: Update iterator for loop part3 ([#817](https://github.com/JDimproved/JDim/pull/817)) - meson: Change current directory to get build information ([#816](https://github.com/JDimproved/JDim/pull/816)) - `NodeTreeBase`: Update iterator for loop part2 ([#815](https://github.com/JDimproved/JDim/pull/815)) - `NodeTreeBase`: Update `NodeTreeBase::get_imglinks()` ([#814](https://github.com/JDimproved/JDim/pull/814)) - `Root`: Correct board URLs which are missing scheme ([#813](https://github.com/JDimproved/JDim/pull/813)) - `Root`: Update regex for URL ([#812](https://github.com/JDimproved/JDim/pull/812)) - `NodeTreeBase`: Update iterator for loop part1 ([#811](https://github.com/JDimproved/JDim/pull/811)) - `BoardBase`: Update iterator for loop ([#810](https://github.com/JDimproved/JDim/pull/810)) - `Admin`: Drop virtual function from `SKELETON::Admin::close_view(View*)` ([#809](https://github.com/JDimproved/JDim/pull/809)) - `Root`: Fix check for URL scheme ([#808](https://github.com/JDimproved/JDim/pull/808)) - Improve miscellaneous codes part1 ([#807](https://github.com/JDimproved/JDim/pull/807)) - dbtree: Update iterator for loop part1 ([#806](https://github.com/JDimproved/JDim/pull/806)) - `ArticleBase`: Update iterator for loop ([#805](https://github.com/JDimproved/JDim/pull/805)) - dbimg: Update iterator for loop ([#804](https://github.com/JDimproved/JDim/pull/804)) - `DelImgCacheDiag`: Update iterator for loop ([#803](https://github.com/JDimproved/JDim/pull/803)) - `Css_Manager`: Update iterator for loop ([#802](https://github.com/JDimproved/JDim/pull/802)) - `Core`: update iter for loop ([#801](https://github.com/JDimproved/JDim/pull/801)) - `MouseKeyPref`: Update iterator for loop ([#800](https://github.com/JDimproved/JDim/pull/800)) - `MouseKeyConf`: Update iterator for loop ([#799](https://github.com/JDimproved/JDim/pull/799)) - controlutil: Update iterator for loop ([#798](https://github.com/JDimproved/JDim/pull/798)) - `Control`: Update iterator for loop ([#797](https://github.com/JDimproved/JDim/pull/797)) - board: Update iterator for loop ([#796](https://github.com/JDimproved/JDim/pull/796)) - `Loader`: Fix HTTP header analyzing to ignoring case ([#795](https://github.com/JDimproved/JDim/pull/795)) - `BoardViewBase`: Update iterator for loop ([#794](https://github.com/JDimproved/JDim/pull/794)) - bbslist: Update iterator for loop ([#793](https://github.com/JDimproved/JDim/pull/793)) - `BBSListViewMain`: Update iterator for loop ([#792](https://github.com/JDimproved/JDim/pull/792)) - `ARTICLE::Preferences`: Update iterator for loop ([#790](https://github.com/JDimproved/JDim/pull/790)) - `LayoutTree`: Update iterator for loop ([#789](https://github.com/JDimproved/JDim/pull/789)) - font: Update iterator for loop ([#788](https://github.com/JDimproved/JDim/pull/788)) - `DrawAreaBase`: Update iterator for loop ([#787](https://github.com/JDimproved/JDim/pull/787)) - `ArticleViewSearch`: Update iterator for loop ([#786](https://github.com/JDimproved/JDim/pull/786)) - `ArticleViewBase`: Update iterator for loop ([#785](https://github.com/JDimproved/JDim/pull/785)) - `ArticleAdmin`: Update iterator for loop ([#784](https://github.com/JDimproved/JDim/pull/784)) - `AAManager`: Update iterator for loop ([#783](https://github.com/JDimproved/JDim/pull/783)) - Deprecate platforms where gcc version less than 7 ([#781](https://github.com/JDimproved/JDim/pull/781)) - Deprecate oniguruma regex option ([#780](https://github.com/JDimproved/JDim/pull/780)) - Remove deprecated command-line option `--norestore` ([#779](https://github.com/JDimproved/JDim/pull/779)) - Remove obsolete `--with-regex=posix` for ./configure ([#778](https://github.com/JDimproved/JDim/pull/778)) ### [**JDim-v0.6.0** Release](https://github.com/JDimproved/JDim/releases/tag/JDim-v0.6.0) (2021-07-10) 主な変更点 - ダイアログにClient-Side Decoration(CSD)のサポートを追加した - スレ一覧にソートの優先順を変更するショートカットキーを追加した (設定が必要) - 画像フォーマット _WebP_ と _AVIF_ に対応した (対応するローダーのインストールが必要) - 正規表現ライブラリPOSIX regex(`--with-regex=posix`)のサポートを廃止した ### [0.6.0-20210710](https://github.com/JDimproved/JDim/compare/948a10e9b44...JDim-v0.6.0) (2021-07-10) - Release 0.6.0 ([#775](https://github.com/JDimproved/JDim/pull/775)) - Deprecate command-line option `--norestore` ([#774](https://github.com/JDimproved/JDim/pull/774)) - Restore snapcraft configuration (2021-06) ([#773](https://github.com/JDimproved/JDim/pull/773)) - Set snapcraft config for i386 (2021-06) ([#771](https://github.com/JDimproved/JDim/pull/771)) - Optimize finding start string part11 ([#770](https://github.com/JDimproved/JDim/pull/770)) - skeleton: Optimize finding start string ([#769](https://github.com/JDimproved/JDim/pull/769)) - message: Optimize finding start string ([#768](https://github.com/JDimproved/JDim/pull/768)) - jdlib: Optimize finding start string ([#767](https://github.com/JDimproved/JDim/pull/767)) - Bump version to 0.6.0-beta ([#766](https://github.com/JDimproved/JDim/pull/766)) - message: Improve error messages for incomplete input ([#765](https://github.com/JDimproved/JDim/pull/765)) - environment: Optimize finding start string ([#764](https://github.com/JDimproved/JDim/pull/764)) - dbtree: Optimize finding start string ([#763](https://github.com/JDimproved/JDim/pull/763)) - `Css_Manager`: Optimize finding start string ([#762](https://github.com/JDimproved/JDim/pull/762)) - `Core`: Optimize finding start string ([#760](https://github.com/JDimproved/JDim/pull/760)) - Optimize finding start string part3 ([#759](https://github.com/JDimproved/JDim/pull/759)) - `DrawAreaBase`: Optimize finding start string ([#758](https://github.com/JDimproved/JDim/pull/758)) - `ArticleViewBase`: Optimize finding start string ([#757](https://github.com/JDimproved/JDim/pull/757)) - `AAManager`: Optimize finding start string ([#756](https://github.com/JDimproved/JDim/pull/756)) - `EditTextView`: Replace space conversion ` ` with ` ` ([#755](https://github.com/JDimproved/JDim/pull/755)) - Update documents ([#754](https://github.com/JDimproved/JDim/pull/754)) - Convert line separator to whitespace for drawing thread view tidily ([#753](https://github.com/JDimproved/JDim/pull/753)) - `Img`: Add error message for unsupported image with fake extension ([#752](https://github.com/JDimproved/JDim/pull/752)) - `Img`: Add image/webp, image/avif to Accept header for genuine URL if supported ([#751](https://github.com/JDimproved/JDim/pull/751)) - `MouseKeyConf`: Use member variables to backup instead of global vars ([#750](https://github.com/JDimproved/JDim/pull/750)) - `SKELETON::Toolbar`: Update close button flat style ([#747](https://github.com/JDimproved/JDim/pull/747)) - `Img`: Get rid of image/webp from Accept header for requesting image ([#746](https://github.com/JDimproved/JDim/pull/746)) - `ArticleBase`: Add noexcept qualifier to member function ([#745](https://github.com/JDimproved/JDim/pull/745)) - `ArticleBase`: Add const qualifier to member function part7 ([#744](https://github.com/JDimproved/JDim/pull/744)) - `ArticleBase`: Add const qualifier to member function part6 ([#743](https://github.com/JDimproved/JDim/pull/743)) - `ArticleBase`: Add const qualifier to member function part5 ([#742](https://github.com/JDimproved/JDim/pull/742)) - `ArticleBase`: Add const qualifier to member function part4 ([#741](https://github.com/JDimproved/JDim/pull/741)) - `ArticleBase`: Add const qualifier to member function part3 ([#740](https://github.com/JDimproved/JDim/pull/740)) - `ArticleBase`: Add const qualifier to member function part2 ([#739](https://github.com/JDimproved/JDim/pull/739)) - `ArticleBase`: Add const qualifier to member function part1 ([#738](https://github.com/JDimproved/JDim/pull/738)) - Add WebP and AVIF support ([#736](https://github.com/JDimproved/JDim/pull/736)) - `NodeTreeBase`: Add const qualifier to member function part5 ([#735](https://github.com/JDimproved/JDim/pull/735)) - `NodeTreeBase`: Add const qualifier to member function part4 ([#734](https://github.com/JDimproved/JDim/pull/734)) - `NodeTreeBase`: Add const qualifier to member function part3 ([#733](https://github.com/JDimproved/JDim/pull/733)) - `NodeTreeBase`: Add const qualifier to member function part2 ([#732](https://github.com/JDimproved/JDim/pull/732)) - `BBSListViewBase`: Add const qualifier to member function argument ([#731](https://github.com/JDimproved/JDim/pull/731)) - `NodeTreeBase`: Add const qualifier to member function part1 ([#730](https://github.com/JDimproved/JDim/pull/730)) - `NodeTreeBase`: Add static keyword to member function ([#729](https://github.com/JDimproved/JDim/pull/729)) - `NodeTreeBase`: Add delete declaration to unimplemented member function ([#728](https://github.com/JDimproved/JDim/pull/728)) - `NodeTreeBase`: Add noexcept qualifier to member function ([#726](https://github.com/JDimproved/JDim/pull/726)) - `Play_Sound`: Fix loading WAV file on big endian machine ([#725](https://github.com/JDimproved/JDim/pull/725)) - misctrip: Add configure check for thread-safe crypt_r ([#724](https://github.com/JDimproved/JDim/pull/724)) - image: Add const qualifier member function ([#723](https://github.com/JDimproved/JDim/pull/723)) - message: Add const qualifier member function ([#722](https://github.com/JDimproved/JDim/pull/722)) - `SKELETON::ToolBar`: Remove unused member function ([#721](https://github.com/JDimproved/JDim/pull/721)) - `TreeViewBase`: Add const qualifier to member function ([#720](https://github.com/JDimproved/JDim/pull/720)) - `PaneControl`: Add const qualifier to member function ([#719](https://github.com/JDimproved/JDim/pull/719)) - `DragableNoteBook`: Add const qualifier to member function ([#718](https://github.com/JDimproved/JDim/pull/718)) - `AboutDiag`: Add const qualifier to member function ([#717](https://github.com/JDimproved/JDim/pull/717)) - `MessageAdmin`: Fix unexpected mouse cursor on text selection ([#716](https://github.com/JDimproved/JDim/pull/716)) - skeleton: Add const qualifier member function part1 ([#715](https://github.com/JDimproved/JDim/pull/715)) - `Loader`: Get rid of DNT: 1 from HTTP request header ([#713](https://github.com/JDimproved/JDim/pull/713)) - `JDWindow`: Add const qualifier to member function ([#712](https://github.com/JDimproved/JDim/pull/712)) - `MISC::get_pointer_at_window()`: Add const qualifier to argument ([#711](https://github.com/JDimproved/JDim/pull/711)) - `ArticleHash`: Add const qualifier to member function ([#710](https://github.com/JDimproved/JDim/pull/710)) - filtering: Add const qualifier member function ([#709](https://github.com/JDimproved/JDim/pull/709)) - jdlib: Add const qualifier member function ([#708](https://github.com/JDimproved/JDim/pull/708)) - `IOMonitor`: Add const qualifier to member function ([#707](https://github.com/JDimproved/JDim/pull/707)) - `Root`: Add const qualifier to member function ([#706](https://github.com/JDimproved/JDim/pull/706)) - `TextLoader`: Add const qualifier to member function ([#705](https://github.com/JDimproved/JDim/pull/705)) - `ImgRoot`: Add const qualifier to member function ([#704](https://github.com/JDimproved/JDim/pull/704)) - `Img`: Add const qualifier to member function ([#703](https://github.com/JDimproved/JDim/pull/703)) - `DelImgCacheDiag`: Add static keyword to member function ([#702](https://github.com/JDimproved/JDim/pull/702)) - `Core`: Add const qualifier to member function ([#701](https://github.com/JDimproved/JDim/pull/701)) - `InputDiag`: Add const qualifier to member function ([#700](https://github.com/JDimproved/JDim/pull/700)) - `KeyConfig`: Add const qualifier to member function ([#699](https://github.com/JDimproved/JDim/pull/699)) - `ButtonConfig`: Add const qualifier to member function ([#698](https://github.com/JDimproved/JDim/pull/698)) - Add shortcut key configurations for switching board view column sort ([#696](https://github.com/JDimproved/JDim/pull/696)) - `MouseKeyConf`: Add const qualifier to member function part2 ([#695](https://github.com/JDimproved/JDim/pull/695)) - `MouseKeyConf`: Add const qualifier to member function part1 ([#694](https://github.com/JDimproved/JDim/pull/694)) - `LayoutTree`: Set font IDs for abone layout nodes ([#693](https://github.com/JDimproved/JDim/pull/693)) - `MouseKeyItem`: Add const qualifier to member function ([#692](https://github.com/JDimproved/JDim/pull/692)) - `BoardViewBase`: Add const qualifier to member function ([#691](https://github.com/JDimproved/JDim/pull/691)) - `BoardViewBase`: Remove not implemented member function ([#690](https://github.com/JDimproved/JDim/pull/690)) - `SelectListDialog`: Add const qualifier to member function ([#689](https://github.com/JDimproved/JDim/pull/689)) - `BBSListViewBase`: Add const qualifier to member function ([#688](https://github.com/JDimproved/JDim/pull/688)) - `ArticleToolBar`: Remove not implemented member functions ([#687](https://github.com/JDimproved/JDim/pull/687)) - `DrawAreaBase`: Add const qualifier to member function ([#686](https://github.com/JDimproved/JDim/pull/686)) - `LayoutTree`: Add const qualifier to member function ([#685](https://github.com/JDimproved/JDim/pull/685)) - `CARET_POSITION`: Add const qualifier to member function ([#684](https://github.com/JDimproved/JDim/pull/684)) - `ArticleViewBase`: Add const qualifier to member function ([#683](https://github.com/JDimproved/JDim/pull/683)) - `AAManager`: Add const qualifier to member function ([#682](https://github.com/JDimproved/JDim/pull/682)) - Fix initial settings for thread title search (2021-04) ([#680](https://github.com/JDimproved/JDim/pull/680)) - `DragableNoteBook`: Fix DnD destination mark position on Wayland ([#678](https://github.com/JDimproved/JDim/pull/678)) - `Post`: Fix error message for HTTP response ([#677](https://github.com/JDimproved/JDim/pull/677)) - Add several missing headers to HTTP request ([#676](https://github.com/JDimproved/JDim/pull/676)) - `BoardViewBase`: Simplify if statement condition ([#673](https://github.com/JDimproved/JDim/pull/673)) - Replace `Gtk::Menu::popup()` with new API ([#672](https://github.com/JDimproved/JDim/pull/672)) - Refactor `Dom` part3 ([#671](https://github.com/JDimproved/JDim/pull/671)) - `Dom`: Implement member function `size()` ([#670](https://github.com/JDimproved/JDim/pull/670)) - dialog: Add Client-Side Decoration support ([#668](https://github.com/JDimproved/JDim/pull/668)) - Remove `ConstPtr` which represents unowned pointer ([#667](https://github.com/JDimproved/JDim/pull/667)) - `BoardFactory`: Use `std::unique_ptr` instead of raw pointer ([#666](https://github.com/JDimproved/JDim/pull/666)) - `Root`: Use `std::find_if()` instead of range based for statement ([#665](https://github.com/JDimproved/JDim/pull/665)) - `Root`: Use `std::unique_ptr` instead of raw pointer ([#664](https://github.com/JDimproved/JDim/pull/664)) - `ImageAdmin`: Use `std::unique_ptr` instead of raw pointer ([#663](https://github.com/JDimproved/JDim/pull/663)) - `ArticleHash`: Use `std::unique_ptr` instead of raw pointer ([#662](https://github.com/JDimproved/JDim/pull/662)) - `BoardBase`: Use concrete type member instead of dynamic allocation ([#661](https://github.com/JDimproved/JDim/pull/661)) - notebook: Use `Gtk::manage()` instead of operator delete ([#660](https://github.com/JDimproved/JDim/pull/660)) - `PopupWinBase`: Uss css setting instead of drawing border lines ([#659](https://github.com/JDimproved/JDim/pull/659)) - Switch css class name instead of reload css ([#658](https://github.com/JDimproved/JDim/pull/658)) ### [0.5.0-20210404](https://github.com/JDimproved/JDim/compare/JDim-v0.5.0...948a10e9b44) (2021-04-04) - Update histories ([#656](https://github.com/JDimproved/JDim/pull/656)) - `DrawAreaBase`: Use incomplete type support for `std::list` ([#655](https://github.com/JDimproved/JDim/pull/655)) - `History_Manager`: Use incomplete type support for `std::list` ([#654](https://github.com/JDimproved/JDim/pull/654)) - `Log_Manager`: Use incomplete type support for `std::list` ([#653](https://github.com/JDimproved/JDim/pull/653)) - `Completion_Manager`: Simplify memory allocation for `std::vector` elements ([#652](https://github.com/JDimproved/JDim/pull/652)) - `ViewHistory`: Use `std::unique_ptr` instead of new/delete ([#651](https://github.com/JDimproved/JDim/pull/651)) - `ImgRoot`: Use `std::unique_ptr` instead of new/delete ([#650](https://github.com/JDimproved/JDim/pull/650)) - `ArticleHash`: Remove unnecessary member variable ([#649](https://github.com/JDimproved/JDim/pull/649)) - skeleton: Use `std::unique_ptr` instead of new/delete part2 ([#648](https://github.com/JDimproved/JDim/pull/648)) - `JDWindow`: Fix expanding image view window by focus-in ([#647](https://github.com/JDimproved/JDim/pull/647)) - `JDWindow`: remove dummy window for transient setting ([#646](https://github.com/JDimproved/JDim/pull/646)) - Use static allocation for local dialog var instead of new/delete ([#644](https://github.com/JDimproved/JDim/pull/644)) - Use static allocation for local iconv var instead of new/delete ([#643](https://github.com/JDimproved/JDim/pull/643)) - `Admin`: Remove unnecessary if-statements ([#642](https://github.com/JDimproved/JDim/pull/642)) - skeleton: Use delegating constructors instead of setup function ([#641](https://github.com/JDimproved/JDim/pull/641)) - `EditListWin`: Set window display position to center ([#640](https://github.com/JDimproved/JDim/pull/640)) - skeleton: Use `std::unique_ptr` instead of new/delete ([#639](https://github.com/JDimproved/JDim/pull/639)) - `MessageViewBase`: Use std::unique_ptr instead of new/delete ([#638](https://github.com/JDimproved/JDim/pull/638)) - `ImageViewPopup`: Use `std::unique_ptr` instead of new/delete ([#637](https://github.com/JDimproved/JDim/pull/637)) - `HistoryManager`: Use `std::unique_ptr` instead of new/delete ([#636](https://github.com/JDimproved/JDim/pull/636)) - `MouseKeyPref`: Use `std::unique_ptr` instead of new/delete ([#635](https://github.com/JDimproved/JDim/pull/635)) - board: Use `std::unique_ptr` instead of new/delete ([#634](https://github.com/JDimproved/JDim/pull/634)) - bbslist: Use `std::unique_ptr` instead of new/delete ([#633](https://github.com/JDimproved/JDim/pull/633)) - `Core`: Use `std::unique_ptr` instead of new/delete ([#632](https://github.com/JDimproved/JDim/pull/632)) - `JDWinMain`: Use `std::unique_ptr` instead of new/delete ([#631](https://github.com/JDimproved/JDim/pull/631)) - article: Use `std::unique_ptr` instead of new/delete ([#630](https://github.com/JDimproved/JDim/pull/630)) - `ArticleBase`: Use `std::time_t` instead of `struct timeval` ([#629](https://github.com/JDimproved/JDim/pull/629)) - Fix compile error on FreeBSD ([#628](https://github.com/JDimproved/JDim/pull/628)) - misctime: Use `std::chrono::steady_clock` instead of `gettimeofday()` ([#627](https://github.com/JDimproved/JDim/pull/627)) - `DrawAreaBase`: Use `std::chrono::steady_clock` instead of `gettimeofday()` ([#626](https://github.com/JDimproved/JDim/pull/626)) - `BoardBase`: Use `std::time_t` instead of `struct timeval` ([#624](https://github.com/JDimproved/JDim/pull/624)) - Use `std::time()` instead of `gettimeofday()` ([#623](https://github.com/JDimproved/JDim/pull/623)) - `PrefDiagFactory`: Use `std::unique_ptr` instead of new/delete ([#622](https://github.com/JDimproved/JDim/pull/622)) - `Search_Manager`: Use `std::unique_ptr` instead of new/delete ([#621](https://github.com/JDimproved/JDim/pull/621)) - message: Use `std::unique_ptr` instead of new/delete ([#620](https://github.com/JDimproved/JDim/pull/620)) - dbtree: Use `std::unique_ptr` instead of new/delete ([#619](https://github.com/JDimproved/JDim/pull/619)) - Replace char buffer with `std::string` for `CORE::Login2ch/Be` ([#618](https://github.com/JDimproved/JDim/pull/618)) - Remove unused `MISC::get_sec_str()` ([#617](https://github.com/JDimproved/JDim/pull/617)) - `Play_Sound`: Use std::vector instead of new/delete ([#616](https://github.com/JDimproved/JDim/pull/616)) - Update `MISC::datetotime()` to remove conditional compilation ([#615](https://github.com/JDimproved/JDim/pull/615)) - Fix build error for meson 0.57 ([#614](https://github.com/JDimproved/JDim/pull/614)) - skeleton: Fix member initialization ([#612](https://github.com/JDimproved/JDim/pull/612)) - `AboutDiag`: Remove empty member function ([#610](https://github.com/JDimproved/JDim/pull/610)) - `LogItem`: Fix off-by-one error for buffer copy ([#609](https://github.com/JDimproved/JDim/pull/609)) - `JDWinMain`: Fix member initialization ([#608](https://github.com/JDimproved/JDim/pull/608)) - `Dom`: Fix member initialization ([#607](https://github.com/JDimproved/JDim/pull/607)) - `AAMenu`: Fix member initialization ([#606](https://github.com/JDimproved/JDim/pull/606)) - message: Fix member initialization ([#605](https://github.com/JDimproved/JDim/pull/605)) - jdlib: Fix member initialization ([#604](https://github.com/JDimproved/JDim/pull/604)) - image: Fix member initialization ([#603](https://github.com/JDimproved/JDim/pull/603)) - `IOMonitor`: Fix member initialization ([#601](https://github.com/JDimproved/JDim/pull/601)) - history: Fix member initialization ([#600](https://github.com/JDimproved/JDim/pull/600)) - `DND_Manager`: Fix member initialization ([#599](https://github.com/JDimproved/JDim/pull/599)) - dbtree: Fix member initialization ([#598](https://github.com/JDimproved/JDim/pull/598)) - dbimg: Fix member initialization ([#597](https://github.com/JDimproved/JDim/pull/597)) - `Css_Manager`: Fix member initialization ([#596](https://github.com/JDimproved/JDim/pull/596)) - `MouseKeyDiag`: Fix member initialization ([#595](https://github.com/JDimproved/JDim/pull/595)) - board: Fix member initialization ([#594](https://github.com/JDimproved/JDim/pull/594)) - bbslist: Fix member initialization ([#593](https://github.com/JDimproved/JDim/pull/593)) - `SearchToolBar`: Fix member initialization ([#590](https://github.com/JDimproved/JDim/pull/590)) - `ArticleToolBar`: Fix member initialization ([#589](https://github.com/JDimproved/JDim/pull/589)) - `LayoutTree`: Fix member initialization ([#588](https://github.com/JDimproved/JDim/pull/588)) - `CARET_POSITION`: Fix member initialization ([#587](https://github.com/JDimproved/JDim/pull/587)) - `ArticleViewSearch`: Fix member initialization ([#586](https://github.com/JDimproved/JDim/pull/586)) - `ArticleAdmin`: Fix member initialization ([#585](https://github.com/JDimproved/JDim/pull/585)) - `MessageViewBase`: Fix member initialization ([#584](https://github.com/JDimproved/JDim/pull/584)) - `ArticleBase`: Fix member initialization ([#583](https://github.com/JDimproved/JDim/pull/583)) - `Loader`: Fix member initialization ([#582](https://github.com/JDimproved/JDim/pull/582)) - `BoardBase`: Fix member initialization ([#581](https://github.com/JDimproved/JDim/pull/581)) - Change size setting for dialog box to use child natural size ([#580](https://github.com/JDimproved/JDim/pull/580)) - `SetupWidzard`: Delegate dialog size and position to desktop environment ([#579](https://github.com/JDimproved/JDim/pull/579)) - `AAMenu`: Fix display position for ascii art preview on Wayland ([#578](https://github.com/JDimproved/JDim/pull/578)) - `TabNotebook`: Fix tab click ([#577](https://github.com/JDimproved/JDim/pull/577)) - Use `Gdk::Window::get_device_position()` instead of `Gtk::Widget::get_pointer()` ([#574](https://github.com/JDimproved/JDim/pull/574)) - `PopupWin`: Use `Gdk::Seat` instead of `Gdk::DeviceManager` ([#573](https://github.com/JDimproved/JDim/pull/573)) - Get rid of calling deprecated `Gtk::TreeView::set_rules_hint()` ([#572](https://github.com/JDimproved/JDim/pull/572)) - Use `__has_include()` instead of config macro ([#571](https://github.com/JDimproved/JDim/pull/571)) - Migrate cpp\_std to c++17 ([#570](https://github.com/JDimproved/JDim/pull/570)) - Remove obsolete gtk codes for less than version 3.22 ([#569](https://github.com/JDimproved/JDim/pull/569)) - Remove deprecated build option `--with-regex=posix` ([#568](https://github.com/JDimproved/JDim/pull/568)) - Remove obsolete configure options ([#567](https://github.com/JDimproved/JDim/pull/567)) - Update requirements for dependencies (gtkmm >= 3.22) ([#566](https://github.com/JDimproved/JDim/pull/566)) - Update CI settings ([#565](https://github.com/JDimproved/JDim/pull/565)) ### [**JDim-v0.5.0** Release](https://github.com/JDimproved/JDim/releases/tag/JDim-v0.5.0) (2021-01-09) 主な変更点 - GTK2版のサポートを廃止した - Windows(MinGW)のサポートを終了した - 正規表現PCREの対応を廃止した - デフォルトの正規表現ライブラリをGlib Regexに変更した - したらば掲示板のデフォルト名無しに対応した - [文字列置換](https://jdimproved.github.io/JDim/replacestr/)の機能と設定ダイアログを実装した ### [0.5.0-20210109](https://github.com/JDimproved/JDim/compare/1fee8b4327...JDim-v0.5.0) (2021-01-09) - Release 0.5.0 ([#564](https://github.com/JDimproved/JDim/pull/564)) - Update issue templates for feature request ([#563](https://github.com/JDimproved/JDim/pull/563)) - Fix bug report link for CONTRIBUTING.md ([#562](https://github.com/JDimproved/JDim/pull/562)) - Update issue templates for bug report ([#561](https://github.com/JDimproved/JDim/pull/561)) - metadata: Fix AppStream component-ID to lowercase ([#560](https://github.com/JDimproved/JDim/pull/560)) - Fix updating thread title for replacing favorite ([#559](https://github.com/JDimproved/JDim/pull/559)) - Change tips link for building by Meson ([#558](https://github.com/JDimproved/JDim/pull/558)) - Remove travis-ci badge from README.md ([#557](https://github.com/JDimproved/JDim/pull/557)) - Update year to 2021 ([#555](https://github.com/JDimproved/JDim/pull/555)) - `DrawAreaBase`: Fix warning while closing thread view ([#554](https://github.com/JDimproved/JDim/pull/554)) - `Root`: Use `std::find_if()` instead of range based for statement ([#553](https://github.com/JDimproved/JDim/pull/553)) - Fix compiler warning for unused const variable ([#552](https://github.com/JDimproved/JDim/pull/552)) - Fix compiler warning for null possibility ([#551](https://github.com/JDimproved/JDim/pull/551)) - Restore snapcraft configuration ([#550](https://github.com/JDimproved/JDim/pull/550)) - Set snapcraft config for i386 ([#549](https://github.com/JDimproved/JDim/pull/549)) - Remove travis.yml ([#548](https://github.com/JDimproved/JDim/pull/548)) - board: Add const qualifier to member function ([#547](https://github.com/JDimproved/JDim/pull/547)) - Use `Gtk::Dialog::get_content_area()` instead of `get_vbox()` part4 ([#546](https://github.com/JDimproved/JDim/pull/546)) - Remove out-of-date topics for known issues ([#545](https://github.com/JDimproved/JDim/pull/545)) - Update contributing guide for using discussions ([#544](https://github.com/JDimproved/JDim/pull/544)) - Fix button label text without language pack ([#542](https://github.com/JDimproved/JDim/pull/542)) - Add meson description to test/README.md ([#541](https://github.com/JDimproved/JDim/pull/541)) - Bump version to 0.5.0-beta ([#540](https://github.com/JDimproved/JDim/pull/540)) - Modify dialog message for multiple start ([#539](https://github.com/JDimproved/JDim/pull/539)) - board: Fix cache filename checking ([#538](https://github.com/JDimproved/JDim/pull/538)) - `NodeTreeBase`, `DragTreeView`: Fix member initialization ([#537](https://github.com/JDimproved/JDim/pull/537)) - `NodeTreeBase`: Fix out of bounds error ([#536](https://github.com/JDimproved/JDim/pull/536)) - Revert "Use `Gtk::ColorChooserDialog` instead of `Gtk::ColorSelectionDialog`" ([#535](https://github.com/JDimproved/JDim/pull/535)) - Update compiler requirement to clang-5.0 since version 0.5.0 ([#533](https://github.com/JDimproved/JDim/pull/533)) - Implement replacing string feature for thread ([#532](https://github.com/JDimproved/JDim/pull/532)) - meson: Update support to provisional ([#531](https://github.com/JDimproved/JDim/pull/531)) - `NodeTreeBase`: Update DAT parsing to try the old format if failure ([#530](https://github.com/JDimproved/JDim/pull/530)) - Improve regex class ([#529](https://github.com/JDimproved/JDim/pull/529)) - Deprecate posix regex option ([#528](https://github.com/JDimproved/JDim/pull/528)) - Implement loading local rule and settings for JBBS ([#526](https://github.com/JDimproved/JDim/pull/526)) - `BBSListViewBase`: Get URL from `TreeView` instead of `DBTREE` interfaces ([#525](https://github.com/JDimproved/JDim/pull/525)) - Use boardbase URL instead of subject.txt URL for identifier ([#524](https://github.com/JDimproved/JDim/pull/524)) - meson: Fall back requirement to version 0.49.0 ([#523](https://github.com/JDimproved/JDim/pull/523)) - `TabLabel`: Fix null pointer check for debug print ([#522](https://github.com/JDimproved/JDim/pull/522)) - meson: Add summary for build configuration ([#520](https://github.com/JDimproved/JDim/pull/520)) - meson: Add `build_tests` option ([#519](https://github.com/JDimproved/JDim/pull/519)) - `Loader`: Fix typo for error message ([#518](https://github.com/JDimproved/JDim/pull/518)) - setupwizard: Use `Gtk::Grid` instead of `Gtk::VBox` for `PageEnd` ([#517](https://github.com/JDimproved/JDim/pull/517)) - setupwizard: Use `Gtk::Grid` instead of `Gtk::VBox` for `PagePane` ([#516](https://github.com/JDimproved/JDim/pull/516)) - setupwizard: Fix setting mnemonic widget for `PageFont` ([#515](https://github.com/JDimproved/JDim/pull/515)) - setupwizard: Use `Gtk::Grid` instead of `Gtk::VBox` for `PageFont` ([#514](https://github.com/JDimproved/JDim/pull/514)) - setupwizard: Set parent window to pref dialogs for `PageNet` ([#513](https://github.com/JDimproved/JDim/pull/513)) - setupwizard: Use `Gtk::Grid` instead of `Gtk::VBox` for `PageNet` ([#512](https://github.com/JDimproved/JDim/pull/512)) - setupwizard: Use `Gtk::Grid` instead of `Gtk::VBox` for `PageStart` ([#511](https://github.com/JDimproved/JDim/pull/511)) - Remove unused `ImgButton` class ([#510](https://github.com/JDimproved/JDim/pull/510)) - Remove unused `ImgToggleButton` class ([#509](https://github.com/JDimproved/JDim/pull/509)) - Remove unused `MsgDiag::add_default_button(const Gtk::StockID&, const int)` ([#508](https://github.com/JDimproved/JDim/pull/508)) - manual: Update histories ([#507](https://github.com/JDimproved/JDim/pull/507)) - `Root`: Fix null pointer redundant check ([#505](https://github.com/JDimproved/JDim/pull/505)) - miscutil: Fix known condition true/false ([#504](https://github.com/JDimproved/JDim/pull/504)) - miscutil: Fix return dangling pointer ([#503](https://github.com/JDimproved/JDim/pull/503)) - `ImageAdmin`: Fix null pointer redundant check ([#502](https://github.com/JDimproved/JDim/pull/502)) - Use i18n text and nemed icon instead of `Gtk::Stock` for buttons ([#501](https://github.com/JDimproved/JDim/pull/501)) - Use named icons instead of `Gtk::Stock::GO_(FORWARD|BACK)` for button ([#500](https://github.com/JDimproved/JDim/pull/500)) - Remove `Gtk::Stock::REFRESH` icon from button ([#499](https://github.com/JDimproved/JDim/pull/499)) - Use i18n text instead of `Gtk::Stock::REVERT_TO_SAVED` for button label ([#498](https://github.com/JDimproved/JDim/pull/498)) - Use i18n text instead of `Gtk::Stock::SAVE` for button label ([#497](https://github.com/JDimproved/JDim/pull/497)) - Use i18n text instead of `Gtk::Stock::OPEN` for button label ([#496](https://github.com/JDimproved/JDim/pull/496)) - Use i18n text instead of `Gtk::Stock::(ADD|DELETE)` for button label ([#495](https://github.com/JDimproved/JDim/pull/495)) - Use i18n text instead of `Gtk::Stock::REMOVE` for button label ([#494](https://github.com/JDimproved/JDim/pull/494)) - Use i18n text instead of `Gtk::Stock::CLOSE` for button label ([#493](https://github.com/JDimproved/JDim/pull/493)) - Use i18n text instead of `Gtk::Stock::APPLY` for button label ([#492](https://github.com/JDimproved/JDim/pull/492)) - manual: Add supplement for environment input ([#491](https://github.com/JDimproved/JDim/pull/491)) - Fix documents for meson command example ([#489](https://github.com/JDimproved/JDim/pull/489)) - Use i18n text instead of `Gtk::Stock::CANCEL` for button label ([#488](https://github.com/JDimproved/JDim/pull/488)) - Use i18n text instead of `Gtk::Stock::OK` for button label ([#487](https://github.com/JDimproved/JDim/pull/487)) - Use i18n text instead of `Gtk::Stock::NO` for button label ([#486](https://github.com/JDimproved/JDim/pull/486)) - Use i18n text instead of `Gtk::Stock::YES` for button label ([#485](https://github.com/JDimproved/JDim/pull/485)) - Add `JDLIB::Iconv` test cases ([#484](https://github.com/JDimproved/JDim/pull/484)) - snapcraft: Get rid of `libsigc++-2.0` from stage-packages ([#483](https://github.com/JDimproved/JDim/pull/483)) - Fix argument order for `JDLIB::Iconv` constructor ([#481](https://github.com/JDimproved/JDim/pull/481)) - Remove outdated rpm spec file ([#480](https://github.com/JDimproved/JDim/pull/480)) - Update histories ([#479](https://github.com/JDimproved/JDim/pull/479)) jdim-0.10.1/docs/manual/2022.md000066400000000000000000000520421445721505100156370ustar00rootroot00000000000000--- title: 更新履歴(2022年) layout: default --- > [Top](../) > [更新履歴]({{ site.baseurl }}/history/) > {{ page.title }} ## {{ page.title }} ### [0.8.0-20221001](https://github.com/JDimproved/JDim/compare/JDim-v0.8.0...660f4f6755) (2022-10-01) - ([#1048](https://github.com/JDimproved/JDim/pull/1048)) snap: Fix build error - ([#1047](https://github.com/JDimproved/JDim/pull/1047)) snap: Migrate base to core20 - ([#1046](https://github.com/JDimproved/JDim/pull/1046)) docs: Get rid of the note for running on old MATE desktop - ([#1045](https://github.com/JDimproved/JDim/pull/1045)) `ReplaceStr`: Add ignore full/half width option - ([#1044](https://github.com/JDimproved/JDim/pull/1044)) `Dom`: Distinguish letter cases for tag name on XML parse mode - ([#1043](https://github.com/JDimproved/JDim/pull/1043)) Use `std::string_view` for `MISC::url_decode()` - ([#1042](https://github.com/JDimproved/JDim/pull/1042)) statusbar: Reset default color if setting turned off - ([#1041](https://github.com/JDimproved/JDim/pull/1041)) toolbar: Add setting that changes color for thread subject per state - ([#1040](https://github.com/JDimproved/JDim/pull/1040)) `ReplaceStrPref`: Add ignore case option without regex - ([#1039](https://github.com/JDimproved/JDim/pull/1039)) Use Pango Markup to display thread subject - ([#1038](https://github.com/JDimproved/JDim/pull/1038)) `ArticleBase`: Skip loading article if already read - ([#1037](https://github.com/JDimproved/JDim/pull/1037)) Convert thread subject to plain text - ([#1036](https://github.com/JDimproved/JDim/pull/1036)) Abandon Ubuntu 18.04 support - ([#1034](https://github.com/JDimproved/JDim/pull/1034)) Revert "`BoardViewBase`: Use PANGO markup to display thread subject (#1033)" - ([#1033](https://github.com/JDimproved/JDim/pull/1033)) `BoardViewBase`: Use PANGO markup to display thread subject - ([#1032](https://github.com/JDimproved/JDim/pull/1032)) GnuTLS: Set `GNUTLS_NO_SIGNAL` to ignore `SIGPIPE` for preventing crash - ([#1031](https://github.com/JDimproved/JDim/pull/1031)) Implement ReplaceStr for thread subject - ([#1030](https://github.com/JDimproved/JDim/pull/1030)) `Post`: Update error message extraction to support samba24 - ([#1029](https://github.com/JDimproved/JDim/pull/1029)) `LayoutTree`: Show HTAB as single space if not multispace mode - ([#1028](https://github.com/JDimproved/JDim/pull/1028)) `ArticleBiewBase`: Update BE user link - ([#1027](https://github.com/JDimproved/JDim/pull/1027)) Update `MISC::html_unescape()` - ([#1026](https://github.com/JDimproved/JDim/pull/1026)) message: Change showing preview in the case of `BBS_UNICODE=change` - ([#1025](https://github.com/JDimproved/JDim/pull/1025)) article: Escape board name for extraction view - ([#1024](https://github.com/JDimproved/JDim/pull/1024)) Update misc html escape - ([#1023](https://github.com/JDimproved/JDim/pull/1023)) `Board2ch`: Change return value of `get_unicode()` to empty string - ([#1022](https://github.com/JDimproved/JDim/pull/1022)) `NodeTreeBase`: Improve `remove_imenu()` - ([#1021](https://github.com/JDimproved/JDim/pull/1021)) `NodeTreeBase`: Refactor `convert_amp()` - ([#1019](https://github.com/JDimproved/JDim/pull/1019)) miscutil: Fix `std::string` construction from pointer - ([#1018](https://github.com/JDimproved/JDim/pull/1018)) Snap: Fix typo for packager information ### [**JDim-v0.8.0** Release](https://github.com/JDimproved/JDim/releases/tag/JDim-v0.8.0) (2022-07-16) 主な変更点 - タブの右クリックメニューに「お気に入りに追加」を追加した - GTK4対応の下準備としてタブ、スレ一覧、サイドバーの右クリックメニューからアクセラレータキーやマウスジェスチャーの表示が無くなった - OpenSSL 3.0に対応した (ライセンス上バイナリ配布はできない) - about:configの`スレタイ検索時にアドレスとスレタイを取得する正規表現`に名前付きキャプチャ対応を暫定的に追加した - コマンドラインのフラグオプションに引数を指定すると今まではエラーになっていたが無視して動作するようになった (一部を除く) - 廃止予定の正規表現ライブラリのビルドオプション`--with-regex`を削除した ### [0.8.0-20220716](https://github.com/JDimproved/JDim/compare/b06a6a6fe71...JDim-v0.8.0) (2022-07-16) - ([#1014](https://github.com/JDimproved/JDim/pull/1014)) Release 0.8.0 - ([#1013](https://github.com/JDimproved/JDim/pull/1013)) Revert "Set snapcraft config for i386 (2022-07) (#1012)" - ([#1012](https://github.com/JDimproved/JDim/pull/1012)) Set snapcraft config for i386 (2022-07) - ([#1011](https://github.com/JDimproved/JDim/pull/1011)) Rename function `MISC::remove_space()` to `MISC::utf8_trim()` - ([#1010](https://github.com/JDimproved/JDim/pull/1010)) `ToolBar`: Remove signal handler `slot_focusout_write_button()` - ([#1009](https://github.com/JDimproved/JDim/pull/1009)) `ToolBar`: Use CSS styling for switching flat buttons - ([#1008](https://github.com/JDimproved/JDim/pull/1008)) `BBSListViewBase`: Fix duplicate if-coindition - ([#1007](https://github.com/JDimproved/JDim/pull/1007)) Prenotice end of update for snap i386 after 2023 - ([#1006](https://github.com/JDimproved/JDim/pull/1006)) Bump version to 0.8.0-beta - ([#1005](https://github.com/JDimproved/JDim/pull/1005)) `ImageViewPopup`: Use CSS styling instead of depreacted function - ([#1004](https://github.com/JDimproved/JDim/pull/1004)) `ImageViewIcon`: Use CSS styling instead of depreacted function - ([#1003](https://github.com/JDimproved/JDim/pull/1003)) `BBSListViewBase`: Update context menu construction to use `Gio::Menu` - ([#1002](https://github.com/JDimproved/JDim/pull/1002)) Snap: Add packager information to operating environment - ([#1001](https://github.com/JDimproved/JDim/pull/1001)) meson: Add `-Dpackager=PACKAGER` build option - ([#999](https://github.com/JDimproved/JDim/pull/999)) `BoardViewBase`: Update context menu construction to use `Gio::Menu` - ([#998](https://github.com/JDimproved/JDim/pull/998)) `History_Manager`: Move static variable to outside of fucntion - ([#997](https://github.com/JDimproved/JDim/pull/997)) `DrawAreaBase`: Get rid of variable assignments which never used - ([#996](https://github.com/JDimproved/JDim/pull/996)) `Loader`: Fix C-style pointer cast part3 - ([#995](https://github.com/JDimproved/JDim/pull/995)) `Iconv`: Fix C-style pointer cast part2 - ([#994](https://github.com/JDimproved/JDim/pull/994)) Fix C-style pointer cast part1 - ([#993](https://github.com/JDimproved/JDim/pull/993)) `Admin`: Update context menu construction to use `Gio::Menu` - ([#992](https://github.com/JDimproved/JDim/pull/992)) `Board2chCompati`: Modify default subbbs.cgi path to /test/bbs.cgi - ([#990](https://github.com/JDimproved/JDim/pull/990)) `JDWindow`: Fix known condition true/false - ([#989](https://github.com/JDimproved/JDim/pull/989)) Fix local variable names that shadow outer function part2 - ([#988](https://github.com/JDimproved/JDim/pull/988)) `ArticleViewBase`: Fix local variable names that shadow outer function - ([#987](https://github.com/JDimproved/JDim/pull/987)) Revert `Gtk::Menu` API to fix menu scrolling on older GTK - ([#986](https://github.com/JDimproved/JDim/pull/986)) `ChunkedDecoder`: Fix condition for breaking loop - ([#984](https://github.com/JDimproved/JDim/pull/984)) `SelectItemPref`: Improve while loop - ([#983](https://github.com/JDimproved/JDim/pull/983)) `EditTreeView`: Improve while loop - ([#982](https://github.com/JDimproved/JDim/pull/982)) Update OpenSSL client codes to support version 3.0 - ([#981](https://github.com/JDimproved/JDim/pull/981)) `Loader`: Improve proccesing for Chunked transfer encording - ([#979](https://github.com/JDimproved/JDim/pull/979)) `LinkFilterPref`: Improve while loop - ([#978](https://github.com/JDimproved/JDim/pull/978)) environment: Improve while loop - ([#976](https://github.com/JDimproved/JDim/pull/976)) `DrawAreaBase`: Modify loop to return the variable value just as it is - ([#974](https://github.com/JDimproved/JDim/pull/974)) Update function `MISC::is_utf8()` - ([#973](https://github.com/JDimproved/JDim/pull/973)) Update function `MISC::is_sjis()` - ([#972](https://github.com/JDimproved/JDim/pull/972)) Update function `MISC::is_jis()` - ([#971](https://github.com/JDimproved/JDim/pull/971)) Update function `MISC::is_euc()` to `MISC::is_eucjp()` - ([#970](https://github.com/JDimproved/JDim/pull/970)) `Css_Manager`: Improve while loop - ([#969](https://github.com/JDimproved/JDim/pull/969)) `Core`: Improve while loop - ([#968](https://github.com/JDimproved/JDim/pull/968)) bbslist: Improve while loop - ([#967](https://github.com/JDimproved/JDim/pull/967)) meson: Improve `buildinfo.h` generation - ([#966](https://github.com/JDimproved/JDim/pull/966)) Move `MISC::utf8_fix_wavedash()` from miscutil to misccharcode - ([#964](https://github.com/JDimproved/JDim/pull/964)) `UsrCmdPref`: Use `Gio::SimpleActionGroup` instead of `Gtk::ActionGroup` - ([#963](https://github.com/JDimproved/JDim/pull/963)) Rename function `MISC::remove_spaces()` to `MISC::ascii_trim()` - ([#962](https://github.com/JDimproved/JDim/pull/962)) Implement `MISC::utf32toutf8()` - ([#961](https://github.com/JDimproved/JDim/pull/961)) Implement App class to use `GtkApplication` features - ([#960](https://github.com/JDimproved/JDim/pull/960)) Use `std::string_view` for `MISC::replace_newlines_to_str()` - ([#959](https://github.com/JDimproved/JDim/pull/959)) Use `std::string_view` for `MISC::cut_str()` - ([#958](https://github.com/JDimproved/JDim/pull/958)) Use `std::string_view` for `MISC::remove_str(str, pattern)` - ([#957](https://github.com/JDimproved/JDim/pull/957)) Remove unused `MISC::count_str()` - ([#956](https://github.com/JDimproved/JDim/pull/956)) meson: Fix check for `crypt(3)` to improve OpenBSD support - ([#955](https://github.com/JDimproved/JDim/pull/955)) `ICON_Manager`: Fix icon loading to prevent crash on start - ([#953](https://github.com/JDimproved/JDim/pull/953)) Implement `MISC::get_unicodeblock()` ### [0.7.0-20220402](https://github.com/JDimproved/JDim/compare/JDim-v0.7.0...b06a6a6fe71) (2022-04-02) - Update histories ([#952](https://github.com/JDimproved/JDim/pull/952)) - Use `std::string_view` for `MISC::replace_str_list()` ([#950](https://github.com/JDimproved/JDim/pull/950)) - Use `std::string_view` for `MISC::replace_str()` ([#949](https://github.com/JDimproved/JDim/pull/949)) - Use `std::string_view` for `MISC::remove_str(str, start, end)` ([#948](https://github.com/JDimproved/JDim/pull/948)) - Implement `MISC::utf8toutf32()` ([#947](https://github.com/JDimproved/JDim/pull/947)) - Use `std::string_view` for `Loader::analyze_header_option_list()` ([#946](https://github.com/JDimproved/JDim/pull/946)) - Use `std::string_view` for `Loader::analyze_header_option()` ([#945](https://github.com/JDimproved/JDim/pull/945)) - readme: Add information about crash with asan ([#944](https://github.com/JDimproved/JDim/pull/944)) - Update function parameters for font width caching ([#942](https://github.com/JDimproved/JDim/pull/942)) - `DrawAreaBase`: Fix known condition true/false ([#941](https://github.com/JDimproved/JDim/pull/941)) - Use `std::string_view` for `MISC::ascii_ignore_case_find()` ([#940](https://github.com/JDimproved/JDim/pull/940)) - Use `std::string_view` for `ReplaceStr_Manager::replace()` ([#939](https://github.com/JDimproved/JDim/pull/939)) - Use `std::string_view` for `MISC::chref_decode()` ([#938](https://github.com/JDimproved/JDim/pull/938)) - `IOMonitor`: Refactor parameter type of member function ([#937](https://github.com/JDimproved/JDim/pull/937)) - Set signal action to ignore SIGPIPE for preventing crash ([#936](https://github.com/JDimproved/JDim/pull/936)) - Use `std::string_view` for `NodeTreeBase::parse_html()` ([#935](https://github.com/JDimproved/JDim/pull/935)) - Use `std::string_view` for `NodeTreeBase::parse_write()` ([#934](https://github.com/JDimproved/JDim/pull/934)) - Use `std::string_view` for `NodeTreeBase::parse_date_id()` ([#933](https://github.com/JDimproved/JDim/pull/933)) - Use `std::string_view` for `NodeTreeBase::parse_mail()` ([#932](https://github.com/JDimproved/JDim/pull/932)) - Use `std::string_view` for `NodeTreeBase::parse_name()` ([#931](https://github.com/JDimproved/JDim/pull/931)) - Implement `MISC::utf8bytes()` ([#930](https://github.com/JDimproved/JDim/pull/930)) - `IOMonitor`: Remove quit command for debug build ([#929](https://github.com/JDimproved/JDim/pull/929)) - `NodeTreeBase`: Use `std::string_view` instead of const pointer and size ([#928](https://github.com/JDimproved/JDim/pull/928)) - Use `std::string_view` for `NodeTreeBase::create_node_text()` ([#927](https://github.com/JDimproved/JDim/pull/927)) - Use `std::string_view` for `NodeTreeBase::create_node_link()` part2 ([#926](https://github.com/JDimproved/JDim/pull/926)) - `AboutDiag`: Use `Pango::Attribute` to show version number bigger ([#925](https://github.com/JDimproved/JDim/pull/925)) - Merge implementation of `NodeTreeBase::check_link()` ([#924](https://github.com/JDimproved/JDim/pull/924)) - Use `std::string_view` for `NodeTreeBase::create_node_anc()` part2 ([#923](https://github.com/JDimproved/JDim/pull/923)) - Use `std::string_view` for `NodeTreeBase::create_node_img()` part2 ([#922](https://github.com/JDimproved/JDim/pull/922)) - Use `std::string_view` for `NodeTreeBase::create_node_sssp()` ([#921](https://github.com/JDimproved/JDim/pull/921)) - Core: Disable virtual board for non-thread's histories ([#920](https://github.com/JDimproved/JDim/pull/920)) - `ICON_Manager`: Adjust size of loaded icon from file ([#919](https://github.com/JDimproved/JDim/pull/919)) - Use `std::string_view` for `NodeTreeBase::create_node_thumbnail()` part2 part3 ([#918](https://github.com/JDimproved/JDim/pull/918)) - `NodeTreeBase`: Refactor local variable for urlreplace ([#917](https://github.com/JDimproved/JDim/pull/917)) - `Admin`: Add append-favorite to tab context menu ([#915](https://github.com/JDimproved/JDim/pull/915)) - `ICON_Manager`: Change APPENDFAVIRITE icon (retake) ([#914](https://github.com/JDimproved/JDim/pull/914)) - Use `std::string_view` for `NodeTreeBase::create_node_link()` ([#913](https://github.com/JDimproved/JDim/pull/913)) - Use `std::string_view` for `NodeTreeBase::create_node_img()` ([#912](https://github.com/JDimproved/JDim/pull/912)) - Implement `JDLIB::span` class ([#911](https://github.com/JDimproved/JDim/pull/911)) - `BoardViewBase`: Add accelerator keys to dialog ([#909](https://github.com/JDimproved/JDim/pull/909)) - `ICON_Manager`: Change APPENDFAVORITE icon ([#908](https://github.com/JDimproved/JDim/pull/908)) - Use `std::string_view` for `NodeTreeBase::create_node_thumbnail()` ([#907](https://github.com/JDimproved/JDim/pull/907)) - Use `std::string_view` for `NodeTreeBase::create_node_anc()` ([#906](https://github.com/JDimproved/JDim/pull/906)) - Use `std::string_view` for `NodeTreeBase::create_node_multispace()` ([#905](https://github.com/JDimproved/JDim/pull/905)) - `Regex`: Implement named captures ([#904](https://github.com/JDimproved/JDim/pull/904)) - Replace lambda with `sigc::mem_fun()` for signal connection ([#903](https://github.com/JDimproved/JDim/pull/903)) - Change first argument of `sigc::mem_fun()` to reference value `*this` ([#902](https://github.com/JDimproved/JDim/pull/902)) - `Admin`: Add null pointer check ([#901](https://github.com/JDimproved/JDim/pull/901)) - Replace fallthrough comments with C++17 attribute ([#900](https://github.com/JDimproved/JDim/pull/900)) - Remove deprecated build option for regex ([#899](https://github.com/JDimproved/JDim/pull/899)) - Update requirements for dependencies (gcc >= 7) ([#898](https://github.com/JDimproved/JDim/pull/898)) ### [**JDim-v0.7.0** Release](https://github.com/JDimproved/JDim/releases/tag/JDim-v0.7.0) (2022-01-15) 主な変更点 - 板のプロパティにUser-Agent(UA)の設定を追加した - 板にある未取得のスレッドのうち指定レス以下をあぼ〜んにする設定を板のプロパティに追加した - デフォルト名無しとID無しのレスをあぼ〜んにする設定をスレのプロパティに追加した - 以前の修正で動作しなくなったコマンドラインオプション`--norestore`を廃止した ### [0.7.0-20220115](https://github.com/JDimproved/JDim/compare/344da3bdc1...JDim-v0.7.0) (2022-01-15) - Release 0.7.0 ([#895](https://github.com/JDimproved/JDim/pull/895)) - Revert "Set snapcraft config for i386 (2022-01) (#893)" ([#894](https://github.com/JDimproved/JDim/pull/894)) - Set snapcraft config for i386 (2022-01) ([#893](https://github.com/JDimproved/JDim/pull/893)) - `EditColumns`: Remove unused member function `copy_row()` ([#892](https://github.com/JDimproved/JDim/pull/892)) - Add error message for XSMP initialization failure to console log ([#891](https://github.com/JDimproved/JDim/pull/891)) - Update year to 2022 ([#889](https://github.com/JDimproved/JDim/pull/889)) - Bump version to 0.7.0-beta ([#888](https://github.com/JDimproved/JDim/pull/888)) - readme: Update example cpu for compiler optimization ([#887](https://github.com/JDimproved/JDim/pull/887)) - `Post`: Fix cookie check for posting ([#886](https://github.com/JDimproved/JDim/pull/886)) - miscutil: Fix percent encoding to use uppercase ([#885](https://github.com/JDimproved/JDim/pull/885)) - `Loader`: Correct HTTP header position in request ([#884](https://github.com/JDimproved/JDim/pull/884)) - `History_Manager`: Remove unused `replace_current_url_viewhistory()` ([#883](https://github.com/JDimproved/JDim/pull/883)) - `BoardViewBase`: Remove unused member function `redraw_scrollbar()` ([#882](https://github.com/JDimproved/JDim/pull/882)) - `DrawAreaBase`: Remove unused member function `is_separator_on_screen()` ([#881](https://github.com/JDimproved/JDim/pull/881)) - `Loader`: Use `reinterpret_cast` instead of C-style pointer casting ([#880](https://github.com/JDimproved/JDim/pull/880)) - `Root`: Rename local variable to avoid shadow function ([#879](https://github.com/JDimproved/JDim/pull/879)) - miscutil: Fix dereference invalid iterator ([#878](https://github.com/JDimproved/JDim/pull/878)) - `BBSListViewBase`: Use `std::find_if()` instead of range based for ([#877](https://github.com/JDimproved/JDim/pull/877)) - manual: Add note for cache directory creation ([#876](https://github.com/JDimproved/JDim/pull/876)) - board: Improve dialog layout for displaying cookie and keyword ([#875](https://github.com/JDimproved/JDim/pull/875)) - Implement User-Agent configuration for board ([#874](https://github.com/JDimproved/JDim/pull/874)) - `EditTreeView`: Add const qualifier to function parameter ([#873](https://github.com/JDimproved/JDim/pull/873)) - Add const qualifier to local variable found by cppcheck ([#871](https://github.com/JDimproved/JDim/pull/871)) - Use `set_value` function instead of `operator[]` part2 ([#870](https://github.com/JDimproved/JDim/pull/870)) - miscutil: Fix heap buffer overflow ([#869](https://github.com/JDimproved/JDim/pull/869)) - `MouseKeyPref`: Use `set_value` function instead of `operator[]` ([#868](https://github.com/JDimproved/JDim/pull/868)) - `Root`: Fix known condition true/false ([#867](https://github.com/JDimproved/JDim/pull/867)) - `BoardViewLog`: Fix known condition true/false ([#866](https://github.com/JDimproved/JDim/pull/866)) - font: Fix known condition true/false ([#865](https://github.com/JDimproved/JDim/pull/865)) - `CheckUpdate_Manager`: Update iterator for loop ([#864](https://github.com/JDimproved/JDim/pull/864)) - `JDTreeViewBase`: Update iterator for loop ([#863](https://github.com/JDimproved/JDim/pull/863)) - `SKELETON::ToolBar`: Update iterator for loop ([#862](https://github.com/JDimproved/JDim/pull/862)) - `EditTreeView`: Update iterator for loop ([#861](https://github.com/JDimproved/JDim/pull/861)) - `CompletionEntry`: Update iterator for loop ([#860](https://github.com/JDimproved/JDim/pull/860)) - `BackForwardButton`: Update iterator for loop ([#859](https://github.com/JDimproved/JDim/pull/859)) - `Admin`: Update iterator for loop ([#858](https://github.com/JDimproved/JDim/pull/858)) - `DrawAreaBase`: Improve performance for drawing `Pango::GlyphString` ([#857](https://github.com/JDimproved/JDim/pull/857)) - session: Update iterator for loop ([#856](https://github.com/JDimproved/JDim/pull/856)) - `Search_Manager`: Update iterator for loop ([#855](https://github.com/JDimproved/JDim/pull/855)) - `Post`: Update iterator for loop ([#854](https://github.com/JDimproved/JDim/pull/854)) - `Log_Manager`: Fix iterator loop ([#853](https://github.com/JDimproved/JDim/pull/853)) - menuslots: Update iterator for loop ([#852](https://github.com/JDimproved/JDim/pull/852)) - playsound: Remove `__has_include` macro for `` ([#851](https://github.com/JDimproved/JDim/pull/851)) jdim-0.10.1/docs/manual/2023.md000066400000000000000000000447531445721505100156520ustar00rootroot00000000000000--- title: 更新履歴(2023年) layout: default --- > [Top](../) > [更新履歴]({{ site.baseurl }}/history/) > {{ page.title }} ## {{ page.title }} ### [0.11.0-unreleased](https://github.com/JDimproved/JDim/compare/JDim-v0.10.1...master) (unreleased) ### [**JDim-v0.10.1** Release](https://github.com/JDimproved/JDim/releases/tag/JDim-v0.10.1) (2023-07-23) 主な変更点 - [2023-07-11][20230711] から5ch.netのDATファイルへのアクセスが開放されたため過去ログ読み込みに対応した [20230711]: https://agree.5ch.net/test/read.cgi/operate/9240230711/3 ### [0.10.1-20230723](https://github.com/JDimproved/JDim/compare/JDim-v0.10.0...JDim-v0.10.1) (2023-07-23) - ([#1206](https://github.com/JDimproved/JDim/pull/1206)) Release 0.10.1 - ([#1205](https://github.com/JDimproved/JDim/pull/1205)) `NodeTree2ch`: Fix loading of past log DATs to organize DAT URLs - ([#1204](https://github.com/JDimproved/JDim/pull/1204)) `NodeTree2ch`: Update loading of past log DATs to enable resume mode - ([#1201](https://github.com/JDimproved/JDim/pull/1201)) readme: Add Unity build tips for how to faster builds - ([#1200](https://github.com/JDimproved/JDim/pull/1200)) Weekly CI: Add logging for cloned muon repository and built version - ([#1199](https://github.com/JDimproved/JDim/pull/1199)) Fix crash on `NodeTreeBase::parse_date_id()` - ([#1196](https://github.com/JDimproved/JDim/pull/1196)) Add notice to change UA settings before posting to prevent error - ([#1195](https://github.com/JDimproved/JDim/pull/1195)) Implement reading past logs with access to 5ch.net DAT files - ([#1193](https://github.com/JDimproved/JDim/pull/1193)) docs: Add notice about 5ch thread access in JDim ### [**JDim-v0.10.0** Release](https://github.com/JDimproved/JDim/releases/tag/JDim-v0.10.0) (2023-07-08) 主な変更点 - 不正なShift\_JIS文字列をUTF-8と見なすオプションを about:config に追加した - スレビューで太文字、文字色、背景色などを表示する機能を強化した - Emoji ZWJ Sequences (複数の絵文字を連結して1文字として見せる絵文字) の表示に対応した - スレビューでURLのパーセントエンコーディングをデコードして表示するオプションを about:config に追加した - HTTPの応答から文字エンコーディングを検出する仕組みを追加した ### [0.10.0-20230708](https://github.com/JDimproved/JDim/compare/2d85091f13...JDim-v0.10.0) (2023-07-08) - ([#1191](https://github.com/JDimproved/JDim/pull/1191)) Release 0.10.0 - ([#1190](https://github.com/JDimproved/JDim/pull/1190)) docs: Fix doxygen warnings - ([#1189](https://github.com/JDimproved/JDim/pull/1189)) Add weekly CI for Unity build and muon - ([#1188](https://github.com/JDimproved/JDim/pull/1188)) `Login2ch`: Fix compile error about unused const variable - ([#1187](https://github.com/JDimproved/JDim/pull/1187)) `Core`: Get rid of unused private field - ([#1186](https://github.com/JDimproved/JDim/pull/1186)) Fix compile errors on Unity build - ([#1185](https://github.com/JDimproved/JDim/pull/1185)) Bump version to 0.10.0-beta - ([#1184](https://github.com/JDimproved/JDim/pull/1184)) Fix comparing floating point with '==' or '!=' - ([#1183](https://github.com/JDimproved/JDim/pull/1183)) Fix implicit narrowing conversion for floating point literal - ([#1182](https://github.com/JDimproved/JDim/pull/1182)) Fix snprintf() format mismatch by changing param type to unsigned int - ([#1181](https://github.com/JDimproved/JDim/pull/1181)) `Socket`: Fix casting of pointers to types with different alignments - ([#1180](https://github.com/JDimproved/JDim/pull/1180)) `DrawAreaBase`: Fix memory leak for `PangoFontMetrics` - ([#1179](https://github.com/JDimproved/JDim/pull/1179)) `NodeTreeBase`: Fix bug missing reply anchors in 5ch threads - ([#1175](https://github.com/JDimproved/JDim/pull/1175)) Fix compiler warnings for useless cast - ([#1174](https://github.com/JDimproved/JDim/pull/1174)) `NodeTreeBase`: Fix parameter name shadowing - ([#1173](https://github.com/JDimproved/JDim/pull/1173)) `NodeTreeBase`: Suppress code analysis report about invalid lifetime - ([#1172](https://github.com/JDimproved/JDim/pull/1172)) `NodeTreeBase`: Modify parsing HTML to unconditionally parse `` elements - ([#1171](https://github.com/JDimproved/JDim/pull/1171)) Fix resource leak for logging - ([#1170](https://github.com/JDimproved/JDim/pull/1170)) `NodeTree2chCompati`: Get rid of raw mode processing - ([#1169](https://github.com/JDimproved/JDim/pull/1169)) Remove Rokka support - ([#1168](https://github.com/JDimproved/JDim/pull/1168)) Add blank lines between replies which are concatenated - ([#1167](https://github.com/JDimproved/JDim/pull/1167)) `NodeTreeBase`: Do not mark thread as broken on HTTP 416 error - ([#1166](https://github.com/JDimproved/JDim/pull/1166)) `Root`: Treat manually added 2ch/5ch boards as native boards - ([#1165](https://github.com/JDimproved/JDim/pull/1165)) `NodeTreeBase`: Update fucntion to parse date, ID, and BE link - ([#1164](https://github.com/JDimproved/JDim/pull/1164)) NodeTreeBase::parse_name(): Update loop condition and text copy - ([#1163](https://github.com/JDimproved/JDim/pull/1163)) `NodeTreeBase`: Update DAT/HTML parsing to add newline after `.*(.*).*.*(.*).* list_cookies = SKELETON::Loadable::cookies(); #ifdef _DEBUG std::cout << "TITLE: [" << title << "]\n"; std::cout << "2ch_X: [" << tag_2ch << "]\n"; std::cout << "CONF: [" << conf << "]\n"; std::cout << "MSG: [" << msg << "]\n"; std::cout << "ERR: [" << m_errmsg << "]\n"; for( const std::string& cookie : list_cookies ) std::cout << "cookie : [" << cookie << "]\n"; std::cout << "location: [" << location() << "]\n"; #endif // クッキーのセット const bool empty_cookies = DBTREE::board_cookie_for_post( m_url ).empty(); if( list_cookies.size() ) DBTREE::board_set_list_cookies( m_url, list_cookies ); // 成功 if( title.find( "書きこみました" ) != std::string::npos || tag_2ch.find( "true" ) != std::string::npos || ( ( get_code() == HTTP_MOVED_PERM || get_code() == HTTP_REDIRECT ) && ! location().empty() ) // リダイレクトされた場合 ){ #ifdef _DEBUG std::cout << "write ok" << std::endl; #endif DBTREE::article_update_writetime( m_url ); if( m_new_article ) { // 板のフロントページをダウンロードしてスレ立てに使うキーワードを更新する DBTREE::board_download_front( m_url ); } emit_sigfin(); return; } // クッキー確認 else if( m_count < 1 && // 永久ループ防止 ( title.find( "書き込み確認" ) != std::string::npos || tag_2ch.find( "cookie" ) != std::string::npos || ( empty_cookies && list_cookies.size() ) ) ){ clear(); // 書き込み確認表示 if( ! CONFIG::get_always_write_ok() ){ std::string diagmsg = MISC::replace_str( msg, "
", "\n" ); if( diagmsg.empty() ){ if( m_errmsg.empty() ) diagmsg = "クッキーを有功にして書き込みますか?"; else diagmsg = m_errmsg; } ConfirmDiag mdiag( m_url, diagmsg ); const int response = mdiag.run(); mdiag.hide(); if( response != Gtk::RESPONSE_OK ){ set_code( HTTP_CANCEL ); emit_sigfin(); return; } if( mdiag.get_chkbutton().get_active() ) CONFIG::set_always_write_ok( true ); } // HTMLからフォームデータを取得できたらメッセージボディを更新する std::string msg_body = DBTREE::board_parse_form_data( m_url, m_return_html ); if( ! msg_body.empty() ) m_msg = std::move( msg_body ); // subbbs.cgi にポスト先を変更してもう一回ポスト m_subbbs = true; ++m_count; // 永久ループ防止 post_msg(); return; } // スレ立て時の書き込み確認 else if( m_count < 1 // 永久ループ防止 && ! m_subbbs && conf.find( "書き込み確認" ) != std::string::npos ){ // HTMLからフォームデータを取得できたらメッセージボディを更新する std::string msg_body = DBTREE::board_parse_form_data( m_url, m_return_html ); if( ! msg_body.empty() ) m_msg = std::move( msg_body ); // subbbs.cgi にポスト先を変更してもう一回ポスト m_subbbs = true; ++m_count; // 永久ループ防止 post_msg(); return; } // その他のエラー #ifdef _DEBUG std::cout << "Error" << std::endl; std::cout << m_return_html << std::endl; #endif MISC::ERRMSG( m_errmsg ); set_code( HTTP_ERR ); emit_sigfin(); } jdim-0.10.1/src/message/post.h000066400000000000000000000043141445721505100160730ustar00rootroot00000000000000// ライセンス: GPL2 // // 記事投稿クラス // #ifndef _POST_H #define _POST_H #include "skeleton/loadable.h" #include #include #include #include namespace SKELETON { class MsgDiag; } namespace MESSAGE { // 投稿の種類で異なるインターフェース部分を抽象クラスにしてStrategyパターンを使う // 実行時にコンストラクタでインターフェースを選択する struct PostStrategy { virtual ~PostStrategy() noexcept = default; virtual std::string url_bbscgi( const std::string& url ) = 0; // 1回目の投稿先 virtual std::string url_subbbscgi( const std::string& url ) = 0; // 2回目の投稿先 virtual std::string get_referer( const std::string& url ) const = 0; // リファラを取得 }; class Post : public SKELETON::Loadable { // ポスト終了シグナル typedef sigc::signal< void > SIG_FIN; SIG_FIN m_sig_fin; // 親widget Gtk::Widget* m_parent; std::string m_url; std::string m_msg; std::string m_return_html; std::string m_errmsg; std::string m_rawdata; int m_count{}; // 書き込み確認時の永久ループ防止用 bool m_subbbs{}; // true なら subbbs.cgiにpostする bool m_new_article; // 新スレ作成 // 書き込んでいますのダイアログ std::unique_ptr m_writingdiag; PostStrategy* m_post_strategy; public: Post( Gtk::Widget* parent, const std::string& url, const std::string& msg, bool new_article ); ~Post(); SIG_FIN sig_fin() const { return m_sig_fin; } const std::string& get_return_html() const { return m_return_html;} const std::string& get_errmsg() const { return m_errmsg; } void post_msg(); // 書き込み中ダイアログ表示 void show_writingdiag( const bool show_buttons ); private: void clear(); void emit_sigfin(); void slot_response( int id ); void receive_data( std::string_view buf ) override; void receive_finish() override; }; } #endif jdim-0.10.1/src/message/toolbar.cpp000066400000000000000000000202621445721505100171030ustar00rootroot00000000000000// ライセンス: GPL2 //#define _DEBUG #include "jddebug.h" #include "toolbar.h" #include "messageadmin.h" #include "skeleton/imgtoolbutton.h" #include "icons/iconmanager.h" #include "control/controlutil.h" #include "control/controlid.h" #include "session.h" #include "global.h" using namespace MESSAGE; MessageToolBarBase::MessageToolBarBase() : SKELETON::ToolBar( MESSAGE::get_admin() ) , m_enable_slot( true ) {} Gtk::ToggleToolButton* MessageToolBarBase::get_button_preview() { #ifdef _DEBUG std::cout << "MessageToolBarBase::get_button_preview\n"; #endif if( ! m_button_preview ){ m_button_preview = Gtk::manage( new SKELETON::ImgToggleToolButton( ICON::PREVIEW ) ); m_button_preview->signal_clicked().connect( sigc::mem_fun( *this, &MessageToolBarBase::slot_toggle_preview ) ); } return m_button_preview; } // プレビュー切り替え void MessageToolBarBase::slot_toggle_preview() { if( ! m_enable_slot ) return; #ifdef _DEBUG std::cout << "MessageToolBarBase::slot_toggle_preview\n"; #endif MESSAGE::get_admin()->set_command( "toggle_preview" ); } // previewボタンのトグル void MessageToolBarBase::set_active_previewbutton( const bool active ) { m_enable_slot = false; m_button_preview->set_active( active ); m_enable_slot = true; } ////////////////////////////////// // 通常のツールバー MessageToolBar::MessageToolBar() : MessageToolBarBase() { MessageToolBar::pack_buttons(); } MessageToolBar::~MessageToolBar() noexcept = default; // 新規スレ名entry表示切り替え void MessageToolBar::show_entry_new_subject( bool show ) { if( m_show_entry_new_subject == show ) return; m_show_entry_new_subject = show; update_button(); } std::string MessageToolBar::get_new_subject() const { if( m_show_entry_new_subject && m_entry_new_subject ) return m_entry_new_subject->get_text(); return std::string(); } void MessageToolBar::clear_new_subject() { if( m_entry_new_subject ) m_entry_new_subject->set_text( "" ); } // ボタンのパッキング // virtual void MessageToolBar::pack_buttons() { #ifdef _DEBUG std::cout << "MessageToolBar::pack_buttons\n"; #endif int num = 0; for(;;){ int item = SESSION::get_item_msg_toolbar( num ); if( item == ITEM_END ) break; switch( item ){ case ITEM_PREVIEW: if( auto button = get_button_preview() ) { get_buttonbar().append( *button ); button->set_label( CONTROL::get_label( CONTROL::Preview ) ); set_tooltip( *button, CONTROL::get_label_motions( CONTROL::Preview ) + "\n\nタブ移動のショートカットでも表示の切り替えが可能\n\n" + CONTROL::get_label_motions( CONTROL::TabRight ) + "\n\n" + CONTROL::get_label_motions( CONTROL::TabLeft ) ); } break; case ITEM_WRITEMSG: get_buttonbar().append( *get_button_write() ); set_tooltip( *get_button_write(), CONTROL::get_label_motions( CONTROL::ExecWrite ) + "\n\nTabキーで書き込みボタンにフォーカスを移すことも可能" ); break; case ITEM_OPENBOARD: get_buttonbar().append( *get_button_board() ); break; case ITEM_NAME: pack_transparent_separator(); // スレ名ラベルを表示 if( ! m_show_entry_new_subject ) get_buttonbar().append( *get_label() ); // 新規スレ名入力entry表示 else{ if( ! m_tool_new_subject ){ m_tool_new_subject = Gtk::manage( new Gtk::ToolItem ); m_entry_new_subject = Gtk::manage( new Gtk::Entry ); m_entry_new_subject->set_size_request( 0 ); m_tool_new_subject->add( *m_entry_new_subject ); m_tool_new_subject->set_expand( true ); } get_buttonbar().append( *m_tool_new_subject ); } pack_transparent_separator(); break; case ITEM_UNDO: if( ! m_button_undo ){ m_button_undo = Gtk::manage( new SKELETON::ImgToolButton( ICON::UNDO, CONTROL::UndoEdit ) ); m_button_undo->signal_clicked().connect( sigc::mem_fun( *this, &MessageToolBar::slot_undo_clicked ) ); } get_buttonbar().append( *m_button_undo ); set_tooltip( *m_button_undo, CONTROL::get_label_motions( CONTROL::UndoEdit ) ); break; case ITEM_INSERTTEXT: if( ! m_button_insert_draft ){ m_button_insert_draft = Gtk::manage( new SKELETON::ImgToolButton( ICON::INSERTTEXT, CONTROL::InsertText ) ); m_button_insert_draft->signal_clicked().connect( sigc::mem_fun( *this, &MessageToolBar::slot_insert_draft_clicked ) ); } get_buttonbar().append( *m_button_insert_draft ); set_tooltip( *m_button_insert_draft, CONTROL::get_label_motions( CONTROL::InsertText ) ); break; case ITEM_LOCK_MESSAGE: if( auto button = get_button_lock() ) { get_buttonbar().append( *button ); button->set_label( CONTROL::get_label( CONTROL::LockMessage ) ); set_tooltip( *button, CONTROL::get_label_motions( CONTROL::LockMessage ) ); } break; case ITEM_QUIT: get_buttonbar().append( *get_button_close() ); set_tooltip( *get_button_close(), CONTROL::get_label_motions( CONTROL::CancelWrite ) ); break; case ITEM_SEPARATOR: pack_separator(); break; } ++num; } set_relief(); show_all_children(); } // undo ボタン void MessageToolBar::slot_undo_clicked() { MESSAGE::get_admin()->set_command( "undo_text" ); } // 下書き挿入ボタン void MessageToolBar::slot_insert_draft_clicked() { MESSAGE::get_admin()->set_command( "insert_draft" ); } /////////////////////// // プレビュー用のツールバー MessageToolBarPreview::MessageToolBarPreview() : MessageToolBarBase() { MessageToolBarPreview::pack_buttons(); } MessageToolBarPreview::~MessageToolBarPreview() noexcept = default; // ボタンのパッキング // virtual void MessageToolBarPreview::pack_buttons() { #ifdef _DEBUG std::cout << "MessageToolBarPreview::pack_buttons\n"; #endif int num = 0; for(;;){ int item = SESSION::get_item_msg_toolbar( num ); if( item == ITEM_END ) break; switch( item ){ case ITEM_PREVIEW: if( auto button = get_button_preview() ) { get_buttonbar().append( *button ); button->set_label( "プレビューを閉じる" ); set_tooltip( *button, "プレビューを閉じる" ); } break; case ITEM_OPENBOARD: get_buttonbar().append( *get_button_board() ); break; case ITEM_NAME: pack_transparent_separator(); get_buttonbar().append( *get_label() ); pack_transparent_separator(); break; case ITEM_WRITEMSG: get_buttonbar().append( *get_button_write() ); set_tooltip( *get_button_write(), CONTROL::get_label_motions( CONTROL::ExecWrite ) + "\n\nTabキーで書き込みボタンにフォーカスを移すことも可能" ); break; case ITEM_QUIT: get_buttonbar().append( *get_button_close() ); set_tooltip( *get_button_close(), CONTROL::get_label_motions( CONTROL::CancelWrite ) ); break; } ++num; } set_relief(); show_all_children(); } jdim-0.10.1/src/message/toolbar.h000066400000000000000000000033141445721505100165470ustar00rootroot00000000000000// ライセンス: GPL2 // ツールバーのクラス #ifndef _MESSAGE_TOOLBAR_H #define _MESSAGE_TOOLBAR_H #include #include "skeleton/toolbar.h" namespace MESSAGE { class MessageToolBarBase : public SKELETON::ToolBar { bool m_enable_slot; Gtk::ToggleToolButton* m_button_preview{}; public: MessageToolBarBase(); ~MessageToolBarBase() noexcept = default; // previewボタンのトグル void set_active_previewbutton( const bool active ); protected: Gtk::ToggleToolButton* get_button_preview(); private: void slot_toggle_preview(); }; /////////////////////////////////// // 通常 class MessageToolBar : public MessageToolBarBase { Gtk::ToolButton* m_button_insert_draft{}; Gtk::ToolButton* m_button_undo{}; // false ならスレ名ラベル、trueなら新規レス名entry表示 bool m_show_entry_new_subject{}; Gtk::ToolItem* m_tool_new_subject{}; Gtk::Entry* m_entry_new_subject{}; public: MessageToolBar(); ~MessageToolBar() noexcept; void show_entry_new_subject( bool show ); std::string get_new_subject() const; void clear_new_subject(); protected: void pack_buttons() override; private: void slot_undo_clicked(); void slot_insert_draft_clicked(); }; /////////////////////////////////// // プレビュー用 class MessageToolBarPreview : public MessageToolBarBase { public: MessageToolBarPreview(); ~MessageToolBarPreview() noexcept; protected: void pack_buttons() override; }; } #endif jdim-0.10.1/src/msgitempref.cpp000066400000000000000000000031021445721505100163310ustar00rootroot00000000000000// ライセンス: GPL2 //#define _DEBUG #include "jddebug.h" #include "msgitempref.h" #include "icons/iconmanager.h" #include "jdlib/miscutil.h" #include "global.h" #include "session.h" #include "command.h" using namespace CORE; MsgItemPref::MsgItemPref( Gtk::Window* parent, const std::string& url ) : SKELETON::SelectItemPref( parent, url ) { // デフォルトの項目を設定 append_default_pair( ITEM_NAME_PREVIEW, ICON::get_icon( ICON::PREVIEW ) ); append_default_pair( ITEM_NAME_WRITEMSG, ICON::get_icon( ICON::WRITE ) ); append_default_pair( ITEM_NAME_OPENBOARD, ICON::get_icon( ICON::TRANSPARENT ) ); append_default_pair( ITEM_NAME_NAME, ICON::get_icon( ICON::TRANSPARENT ) ); append_default_pair( ITEM_NAME_UNDO, ICON::get_icon( ICON::UNDO ) ); append_default_pair( ITEM_NAME_INSERTTEXT, ICON::get_icon( ICON::INSERTTEXT ) ); append_default_pair( ITEM_NAME_LOCK_MESSAGE, ICON::get_icon( ICON::LOCK) ); append_default_pair( ITEM_NAME_QUIT, ICON::get_icon( ICON::QUIT ) ); append_default_pair( ITEM_NAME_SEPARATOR, ICON::get_icon( ICON::TRANSPARENT ) ); // 文字列を元に行を追加 append_rows( SESSION::get_items_msg_toolbar_str() ); set_title( "ツールバー項目設定(書き込みビュー)" ); } // OKを押した void MsgItemPref::slot_ok_clicked() { SESSION::set_items_msg_toolbar_str( get_items() ); CORE::core_set_command( "update_message_toolbar_button" ); } // // デフォルトボタン // void MsgItemPref::slot_default() { append_rows( SESSION::get_items_msg_toolbar_default_str() ); } jdim-0.10.1/src/msgitempref.h000066400000000000000000000010521445721505100160000ustar00rootroot00000000000000// ライセンス: GPL2 // 書き込みビューのツールバーの表示項目設定 #ifndef _MSGITEMPREF_H #define _MSGITEMPREF_H #include "skeleton/selectitempref.h" namespace CORE { class MsgItemPref : public SKELETON::SelectItemPref { public: MsgItemPref( Gtk::Window* parent, const std::string& url ); ~MsgItemPref() noexcept = default; private: // OK押した void slot_ok_clicked() override; // デフォルトボタン void slot_default() override; }; } #endif jdim-0.10.1/src/openurldiag.cpp000066400000000000000000000012771445721505100163330ustar00rootroot00000000000000// ライセンス: GPL2 //#define _DEBUG #include "jddebug.h" #include "openurldiag.h" #include "command.h" using namespace CORE; OpenURLDialog::OpenURLDialog( const std::string& url ) : SKELETON::PrefDiag( nullptr, url, true, false, true ), m_label_url( true, "URL :" ) { m_label_url.set_text( url ); set_activate_entry( m_label_url ); get_content_area()->pack_start( m_label_url, Gtk::PACK_SHRINK ); set_title( "URLを開く" ); resize( 600, 1 ); set_default_response( Gtk::RESPONSE_OK ); show_all_children(); m_label_url.grab_focus(); } void OpenURLDialog::slot_ok_clicked() { CORE::core_set_command( "open_url", m_label_url.get_text() ); } jdim-0.10.1/src/openurldiag.h000066400000000000000000000007251445721505100157750ustar00rootroot00000000000000// ライセンス: GPL2 // // URL を開く // #ifndef _OPENURL_H #define _OPENURL_H #include "skeleton/prefdiag.h" #include "skeleton/label_entry.h" namespace CORE { class OpenURLDialog : public SKELETON::PrefDiag { SKELETON::LabelEntry m_label_url; public: explicit OpenURLDialog( const std::string& url ); ~OpenURLDialog() noexcept = default; protected: void slot_ok_clicked() override; }; } #endif jdim-0.10.1/src/passwdpref.h000066400000000000000000000100631445721505100156360ustar00rootroot00000000000000// ライセンス: GPL2 // パスワード設定ダイアログ #ifndef _PASSWDPREF_H #define _PASSWDPREF_H #include "skeleton/prefdiag.h" #include "skeleton/label_entry.h" #include "login2ch.h" #include "loginbe.h" #include "jdlib/miscutil.h" enum { BOXSPACING = 8 }; namespace CORE { // 2chログイン用 class PasswdFrame2ch : public Gtk::VBox { SKELETON::LabelEntry m_label_sid_2ch; public: SKELETON::LabelEntry entry_id; SKELETON::LabelEntry entry_passwd; PasswdFrame2ch() : m_label_sid_2ch( false, "SID: ", "2chのログインは現在サポート中止しています。" ) , entry_id( true, "ユーザID(_I): " ) , entry_passwd( true, "パスワード(_P): " ) { const int mrg = 8; set_border_width( BOXSPACING ); entry_id.set_border_width( BOXSPACING ); pack_start( entry_id ); entry_passwd.set_border_width( BOXSPACING ); entry_passwd.set_visibility( false ); pack_start( entry_passwd, Gtk::PACK_SHRINK ); m_label_sid_2ch.set_border_width( BOXSPACING ); pack_start( m_label_sid_2ch ); set_border_width( mrg ); } }; // BEログイン用 class PasswdFrameBe : public Gtk::VBox { SKELETON::LabelEntry m_label_dmdm; SKELETON::LabelEntry m_label_mdmd; public: SKELETON::LabelEntry entry_id; SKELETON::LabelEntry entry_passwd; PasswdFrameBe() : m_label_dmdm( false, "DMDM: ", CORE::get_loginbe()->get_sessionid() ), m_label_mdmd( false, "MDMD: ", CORE::get_loginbe()->get_sessiondata() ), entry_id( true, "メールアドレス(_I): " ), entry_passwd( true, "パスワード(_P): " ) { set_border_width( BOXSPACING ); entry_id.set_border_width( BOXSPACING ); pack_start( entry_id ); entry_passwd.set_border_width( BOXSPACING ); entry_passwd.set_visibility( false ); pack_start( entry_passwd, Gtk::PACK_SHRINK ); pack_start( m_label_dmdm ); pack_start( m_label_mdmd ); set_border_width( BOXSPACING ); } }; class PasswdPref : public SKELETON::PrefDiag { Gtk::Notebook m_notebook; PasswdFrame2ch m_frame_2ch; PasswdFrameBe m_frame_be; // OK押した void slot_ok_clicked() override { // 2ch CORE::get_login2ch()->set_username( MISC::utf8_trim( m_frame_2ch.entry_id.get_text().raw() ) ); CORE::get_login2ch()->set_passwd( MISC::utf8_trim( m_frame_2ch.entry_passwd.get_text().raw() ) ); // BE CORE::get_loginbe()->set_username( MISC::utf8_trim( m_frame_be.entry_id.get_text().raw() ) ); CORE::get_loginbe()->set_passwd( MISC::utf8_trim( m_frame_be.entry_passwd.get_text().raw() ) ); } public: PasswdPref( Gtk::Window* parent, const std::string& url ) : SKELETON::PrefDiag( parent, url ) , m_frame_2ch(), m_frame_be() { // 2chログイン用 m_frame_2ch.entry_id.set_text( CORE::get_login2ch()->get_username() ); m_frame_2ch.entry_passwd.set_text( CORE::get_login2ch()->get_passwd() ); set_activate_entry( m_frame_2ch.entry_id ); set_activate_entry( m_frame_2ch.entry_passwd ); // beログイン用 m_frame_be.entry_id.set_text( CORE::get_loginbe()->get_username() ); m_frame_be.entry_passwd.set_text( CORE::get_loginbe()->get_passwd() ); set_activate_entry( m_frame_be.entry_id ); set_activate_entry( m_frame_be.entry_passwd ); m_notebook.append_page( m_frame_2ch, "2ch" ); m_notebook.append_page( m_frame_be, "BE" ); get_content_area()->pack_start( m_notebook ); set_title( "パスワード設定" ); show_all_children(); } ~PasswdPref() noexcept = default; }; } #endif jdim-0.10.1/src/prefdiagfactory.cpp000066400000000000000000000102161445721505100171640ustar00rootroot00000000000000// ライセンス: GPL2 #include "prefdiagfactory.h" #include "passwdpref.h" #include "privacypref.h" #include "browserpref.h" #include "linkfilterpref.h" #include "replacestrpref.h" #include "usrcmdpref.h" #include "proxypref.h" #include "globalabonepref.h" #include "globalabonethreadpref.h" #include "fontcolorpref.h" #include "livepref.h" #include "openurldiag.h" #include "mainitempref.h" #include "sidebaritempref.h" #include "boarditempref.h" #include "boarditemmenupref.h" #include "articleitempref.h" #include "articleitemmenupref.h" #include "searchitempref.h" #include "msgitempref.h" #include "dbimg/delimgdiag.h" #include "board/preference.h" #include "article/preference.h" #include "image/preference.h" #include "control/keypref.h" #include "control/mousepref.h" #include "control/buttonpref.h" #include "config/aboutconfig.h" std::unique_ptr CORE::PrefDiagFactory( Gtk::Window* parent, const int type, const std::string& url, const std::string& command ) { switch( type ) { case PREFDIAG_PASSWD: return std::make_unique( parent, url ); case PREFDIAG_PRIVACY: return std::make_unique( parent, url ); case PREFDIAG_BROWSER: return std::make_unique( parent, url ); case PREFDIAG_LINKFILTER: return std::make_unique( parent, url ); case PREFDIAG_REPLACESTR: return std::make_unique( parent, url ); case PREFDIAG_USRCMD: return std::make_unique( parent, url ); case PREFDIAG_PROXY: return std::make_unique( parent, url ); case PREFDIAG_GLOBALABONE: return std::make_unique( parent, url ); case PREFDIAG_GLOBALABONETHREAD: return std::make_unique( parent, url ); case PREFDIAG_FONTCOLOR: return std::make_unique( parent, url ); case PREFDIAG_LIVE: return std::make_unique( parent, url ); case PREFDIAG_MAINITEM: return std::make_unique( parent, url ); case PREFDIAG_SIDEBARITEM: return std::make_unique( parent, url ); case PREFDIAG_BOARDITEM_COLUM: return std::make_unique( parent, url ); case PREFDIAG_BOARDITEM: return std::make_unique( parent, url ); case PREFDIAG_BOARDITEM_MENU: return std::make_unique( parent, url ); case PREFDIAG_ARTICLEITEM: return std::make_unique( parent, url ); case PREFDIAG_ARTICLEITEM_MENU: return std::make_unique( parent, url ); case PREFDIAG_SEARCHITEM: return std::make_unique( parent, url ); case PREFDIAG_MSGITEM: return std::make_unique( parent, url ); case PREFDIAG_DELIMG: return std::make_unique( parent, url ); case PREFDIAG_BOARD: return std::make_unique( parent, url, command ); case PREFDIAG_ARTICLE: return std::make_unique( parent, url, command ); case PREFDIAG_IMAGE: return std::make_unique( parent, url ); case PREFDIAG_KEY: return std::make_unique( parent, url ); case PREFDIAG_MOUSE: return std::make_unique( parent, url ); case PREFDIAG_BUTTON: return std::make_unique( parent, url ); case PREFDIAG_ABOUTCONFIG: return std::make_unique( parent ); case PREFDIAG_OPENURL: return std::make_unique( url ); default: return nullptr; } } jdim-0.10.1/src/prefdiagfactory.h000066400000000000000000000024351445721505100166350ustar00rootroot00000000000000// ライセンス: GPL2 // // SKELETON::PrefDiagのファクトリー // #ifndef _PREFDIAGFACTORY_H #define _PREFDIAGFACTORY_H #include "skeleton/prefdiag.h" #include #include #include namespace CORE { enum { PREFDIAG_PASSWD, PREFDIAG_PRIVACY, PREFDIAG_BROWSER, PREFDIAG_LINKFILTER, PREFDIAG_REPLACESTR, PREFDIAG_USRCMD, PREFDIAG_PROXY, PREFDIAG_GLOBALABONETHREAD, PREFDIAG_GLOBALABONE, PREFDIAG_FONTCOLOR, PREFDIAG_LIVE, PREFDIAG_MAINITEM, PREFDIAG_SIDEBARITEM, PREFDIAG_BOARDITEM, PREFDIAG_BOARDITEM_MENU, PREFDIAG_BOARDITEM_COLUM, PREFDIAG_ARTICLEITEM, PREFDIAG_ARTICLEITEM_MENU, PREFDIAG_SEARCHITEM, PREFDIAG_MSGITEM, PREFDIAG_DELIMG, PREFDIAG_BOARD, PREFDIAG_ARTICLE, PREFDIAG_IMAGE, PREFDIAG_KEY, PREFDIAG_MOUSE, PREFDIAG_BUTTON, PREFDIAG_ABOUTCONFIG, PREFDIAG_OPENURL, PREFDIAG_NONE }; std::unique_ptr PrefDiagFactory( Gtk::Window* parent, const int type, const std::string& url, const std::string& command = {} ); } #endif jdim-0.10.1/src/privacypref.h000066400000000000000000000055651445721505100160250ustar00rootroot00000000000000// ライセンス: GPL2 // プライバシー設定ダイアログ #ifndef _PRIVACYPREF_H #define _PRIVACYPREF_H #include "skeleton/prefdiag.h" #include "skeleton/msgdiag.h" #include "command.h" namespace CORE { class PrivacyPref : public SKELETON::PrefDiag { Gtk::VBox m_vbox; Gtk::CheckButton m_bt_board; Gtk::CheckButton m_bt_thread; Gtk::CheckButton m_bt_close; Gtk::CheckButton m_bt_search; Gtk::CheckButton m_bt_name; Gtk::CheckButton m_bt_mail; Gtk::HBox m_hbox_selectall; Gtk::Button m_bt_selectall; void slot_selectall() { m_bt_board.set_active( true ); m_bt_thread.set_active( true ); m_bt_close.set_active( true ); m_bt_search.set_active( true ); m_bt_name.set_active( true ); m_bt_mail.set_active( true ); } // OK押した void slot_ok_clicked() override { if( m_bt_board.get_active() ) CORE::core_set_command( "clear_board" ); if( m_bt_thread.get_active() ) CORE::core_set_command( "clear_thread" ); if( m_bt_close.get_active() ) CORE::core_set_command( "clear_closed_thread" ); if( m_bt_search.get_active() ) CORE::core_set_command( "clear_search" ); if( m_bt_name.get_active() ) CORE::core_set_command( "clear_name" ); if( m_bt_mail.get_active() ) CORE::core_set_command( "clear_mail" ); } public: PrivacyPref( Gtk::Window* parent, const std::string& url ) : SKELETON::PrefDiag( parent, url ), m_bt_board( "板履歴(_B)", true ), m_bt_thread( "スレ履歴(_T)", true ), m_bt_close( "最近閉じたスレの履歴(_R)", true ), m_bt_search( "検索履歴(_F)", true ), m_bt_name( "書き込みビューの名前履歴(_N)", true ), m_bt_mail( "書き込みビューのメール履歴(_E)", true ), m_bt_selectall( "全て選択(_A)", true ) { m_vbox.set_spacing( 8 ); m_vbox.set_border_width( 8 ); m_vbox.pack_start( m_bt_thread ); m_vbox.pack_start( m_bt_board ); m_vbox.pack_start( m_bt_close ); m_vbox.pack_start( m_bt_search ); m_vbox.pack_start( m_bt_name ); m_vbox.pack_start( m_bt_mail ); m_bt_selectall.signal_clicked().connect( sigc::mem_fun( *this, &PrivacyPref::slot_selectall ) ); m_hbox_selectall.pack_start( m_bt_selectall, Gtk::PACK_SHRINK ); m_vbox.pack_start( m_hbox_selectall, Gtk::PACK_SHRINK ); get_content_area()->set_spacing( 8 ); get_content_area()->pack_start( m_vbox, Gtk::PACK_SHRINK ); set_title( "プライバシー情報の消去" ); show_all_children(); } ~PrivacyPref() noexcept = default; }; } #endif jdim-0.10.1/src/proxypref.h000066400000000000000000000150401445721505100155160ustar00rootroot00000000000000// ライセンス: GPL2 // プロキシ設定ダイアログ #ifndef _PROXYPREF_H #define _PROXYPREF_H #include "skeleton/prefdiag.h" #include "skeleton/label_entry.h" #include "config/globalconf.h" #include "jdlib/miscutil.h" namespace CORE { constexpr const char kSendCookieTooltip[] = "JDimからプロキシへサイトのクッキーを送信します。\n" "プロキシのクッキー処理にあわせて設定してください。"; class ProxyFrame : public Gtk::Frame { Gtk::VBox m_vbox; Gtk::HBox m_hbox; public: Gtk::CheckButton ckbt; Gtk::CheckButton send_cookie_check; SKELETON::LabelEntry entry_host; SKELETON::LabelEntry entry_port; ProxyFrame( const std::string& title, const Glib::ustring& ckbt_label, const Glib::ustring& send_label, const Glib::ustring& host_label, const Glib::ustring& port_label ) : ckbt( ckbt_label, true ) , send_cookie_check( send_label, true ) , entry_host( true, host_label) , entry_port( true, port_label ) { send_cookie_check.set_tooltip_text( kSendCookieTooltip ); m_hbox.set_spacing( 8 ); m_hbox.pack_start( ckbt, Gtk::PACK_SHRINK ); m_hbox.pack_start( send_cookie_check, Gtk::PACK_SHRINK ); m_hbox.pack_start( entry_host ); m_hbox.pack_start( entry_port, Gtk::PACK_SHRINK ); m_hbox.set_border_width( 8 ); m_vbox.set_spacing( 8 ); m_vbox.pack_start( m_hbox, Gtk::PACK_SHRINK ); set_label( title ); set_border_width( 8 ); add( m_vbox ); } }; class ProxyPref : public SKELETON::PrefDiag { Gtk::Label m_label; // 2ch読み込み用 ProxyFrame m_frame_2ch; // 2ch書き込み用 ProxyFrame m_frame_2ch_w; // 一般用 ProxyFrame m_frame_data; // OK押した void slot_ok_clicked() override { // 2ch CONFIG::set_use_proxy_for2ch( m_frame_2ch.ckbt.get_active() ); CONFIG::set_send_cookie_to_proxy_for2ch( m_frame_2ch.send_cookie_check.get_active() ); CONFIG::set_proxy_for2ch( MISC::utf8_trim( m_frame_2ch.entry_host.get_text().raw() ) ); CONFIG::set_proxy_port_for2ch( atoi( m_frame_2ch.entry_port.get_text().c_str() ) ); // 2ch書き込み用 CONFIG::set_use_proxy_for2ch_w( m_frame_2ch_w.ckbt.get_active() ); CONFIG::set_send_cookie_to_proxy_for2ch_w( m_frame_2ch_w.send_cookie_check.get_active() ); CONFIG::set_proxy_for2ch_w( MISC::utf8_trim( m_frame_2ch_w.entry_host.get_text().raw() ) ); CONFIG::set_proxy_port_for2ch_w( atoi( m_frame_2ch_w.entry_port.get_text().c_str() ) ); // 一般 CONFIG::set_use_proxy_for_data( m_frame_data.ckbt.get_active() ); CONFIG::set_send_cookie_to_proxy_for_data( m_frame_data.send_cookie_check.get_active() ); CONFIG::set_proxy_for_data( MISC::utf8_trim( m_frame_data.entry_host.get_text().raw() ) ); CONFIG::set_proxy_port_for_data( atoi( m_frame_data.entry_port.get_text().c_str() ) ); } public: ProxyPref( Gtk::Window* parent, const std::string& url ) : SKELETON::PrefDiag( parent, url ) , m_label( "認証を行う場合はホスト名を「ユーザID:パスワード@ホスト名」としてください。" ) , m_frame_2ch( "2ch読み込み用", "使用する(_U)", "クッキーを送る(_I)", "ホスト名(_H): ", "ポート番号(_P): " ) , m_frame_2ch_w( "2ch書き込み用", "使用する(_S)", "クッキーを送る(_J)", "ホスト名(_N): ", "ポート番号(_R): " ) , m_frame_data( "その他のサーバ用(板一覧、外部板、画像など)", "使用する(_E)", "クッキーを送る(_K)", "ホスト名(_A): ", "ポート番号(_T): " ) { std::string host; // 2ch用 m_frame_2ch.ckbt.set_active( CONFIG::get_use_proxy_for2ch() ); m_frame_2ch.send_cookie_check.set_active( CONFIG::get_send_cookie_to_proxy_for2ch() ); if( CONFIG::get_proxy_basicauth_for2ch().empty() ) host = CONFIG::get_proxy_for2ch(); else host = CONFIG::get_proxy_basicauth_for2ch() + "@" + CONFIG::get_proxy_for2ch(); m_frame_2ch.entry_host.set_text( host ); m_frame_2ch.entry_port.set_text( std::to_string( CONFIG::get_proxy_port_for2ch() ) ); set_activate_entry( m_frame_2ch.entry_host ); set_activate_entry( m_frame_2ch.entry_port ); // 2ch書き込み用 m_frame_2ch_w.ckbt.set_active( CONFIG::get_use_proxy_for2ch_w() ); m_frame_2ch_w.send_cookie_check.set_active( CONFIG::get_send_cookie_to_proxy_for2ch_w() ); if( CONFIG::get_proxy_basicauth_for2ch_w().empty() ) host = CONFIG::get_proxy_for2ch_w(); else host = CONFIG::get_proxy_basicauth_for2ch_w() + "@" + CONFIG::get_proxy_for2ch_w(); m_frame_2ch_w.entry_host.set_text( host ); m_frame_2ch_w.entry_port.set_text( std::to_string( CONFIG::get_proxy_port_for2ch_w() ) ); set_activate_entry( m_frame_2ch_w.entry_host ); set_activate_entry( m_frame_2ch_w.entry_port ); // 一般用 m_frame_data.ckbt.set_active( CONFIG::get_use_proxy_for_data() ); m_frame_data.send_cookie_check.set_active( CONFIG::get_send_cookie_to_proxy_for_data() ); if( CONFIG::get_proxy_basicauth_for_data().empty() ) host = CONFIG::get_proxy_for_data(); else host = CONFIG::get_proxy_basicauth_for_data() + "@" + CONFIG::get_proxy_for_data(); m_frame_data.entry_host.set_text( host ); m_frame_data.entry_port.set_text( std::to_string( CONFIG::get_proxy_port_for_data() ) ); set_activate_entry( m_frame_data.entry_host ); set_activate_entry( m_frame_data.entry_port ); get_content_area()->set_spacing( 4 ); get_content_area()->pack_start( m_label ); get_content_area()->pack_start( m_frame_2ch ); get_content_area()->pack_start( m_frame_2ch_w ); get_content_area()->pack_start( m_frame_data ); set_title( "プロキシ設定" ); show_all_children(); } ~ProxyPref() noexcept = default; }; } #endif jdim-0.10.1/src/replacestrmanager.cpp000066400000000000000000000231051445721505100175130ustar00rootroot00000000000000// ライセンス: GPL2 //#define _DEBUG #include "jddebug.h" #include "replacestrmanager.h" #include "cache.h" #include "type.h" #include "jdlib/miscmsg.h" #include "jdlib/miscutil.h" #include "xml/document.h" #include "xml/tools.h" #include // std::any_of(), std::find_if() #include #include #include // std::distance() constexpr const char* kRootNodeNameReplaceStr = "replacestrlist"; static CORE::ReplaceStr_Manager* instance_replacestr_manager = nullptr; CORE::ReplaceStr_Manager* CORE::get_replacestr_manager() { if( ! instance_replacestr_manager ) instance_replacestr_manager = new ReplaceStr_Manager(); assert( instance_replacestr_manager ); return instance_replacestr_manager; } void CORE::delete_replacestr_manager() { if( instance_replacestr_manager ) delete instance_replacestr_manager; instance_replacestr_manager = nullptr; } /////////////////////////////////////////////// using namespace CORE; unsigned long ReplaceStrCondition::to_ulong() const { // エンディアンの問題を回避するため std::bitset を使って変換する // データ互換性のため削除したフラグのビット位置を再利用しないこと std::bitset<31> out; out[0] = active; out[1] = icase; out[2] = regex; out[3] = wchar; out[4] = norm; return out.to_ulong(); } ReplaceStrCondition ReplaceStrCondition::from_ulong( unsigned long condition ) noexcept { const std::bitset<31> input( condition ); ReplaceStrCondition out{}; out.active = input[0]; out.icase = input[1]; out.regex = input[2]; out.wchar = input[3]; out.norm = input[4]; return out; } ReplaceStr_Manager::ReplaceStr_Manager() { std::string xml; if( CACHE::load_rawdata( CACHE::path_replacestr(), xml ) ) xml2list( xml ); else { // デフォルトの設定を行う ReplaceStrCondition condition{}; list_append( REPLACETARGET_MESSAGE, condition, "ダブルクリックすると編集出来ます", "(この項目は削除して構いません)" ); condition.regex = true; list_append( REPLACETARGET_MESSAGE, condition, "(?= REPLACETARGET_MAX ) return; JDLIB::RegexPattern creg; if( condition.regex ) { constexpr bool newline = true; constexpr bool migemo = false; if( ! creg.set( pattern, condition.icase, newline, migemo, condition.wchar, condition.norm ) ) { std::string msg = "invlid replacestr pattern: "; msg.append( creg.errstr() + ": " + pattern ); MISC::ERRMSG( msg ); } } #ifdef _DEBUG std::cout << "ReplaceStr_Manager::list_append id=" << id << " conditnon=" << condition.to_ulong() << " pattern=" << pattern << " replace=" << replace << std::endl; #endif m_list[id].push_back( ReplaceStrItem{ condition, pattern, replace, std::move( creg ) } ); } // // xml -> リスト // void ReplaceStr_Manager::xml2list( const std::string& xml ) { for( auto& item : m_list ) { item.clear(); } if( xml.empty() ) return; const XML::Document document( xml ); const XML::Dom* const root = document.get_root_element( kRootNodeNameReplaceStr ); if( ! root ) return; for( const XML::Dom* dom : *root ) { if( dom->nodeType() != XML::NODE_TYPE_ELEMENT ) continue; const int type = XML::get_type( dom->nodeName() ); if( type != TYPE_DIR ) continue; const std::size_t id = target_id( dom->getAttribute( "name" ) ); if( id >= REPLACETARGET_MAX ) continue; m_chref[id] = dom->getAttribute( "chref" ) == "true"; for( const XML::Dom* query : *dom ) { if( query->nodeType() != XML::NODE_TYPE_ELEMENT ) continue; const int element_type = XML::get_type( query->nodeName() ); if( element_type != TYPE_REPLACESTR ) continue; const std::string cond_str = query->getAttribute( "condition" ); // 解析に失敗した時は初期値(0)にする const unsigned long cond_num = std::strtoul( cond_str.c_str(), nullptr, 10 ); list_append( id, ReplaceStrCondition::from_ulong( cond_num ), query->getAttribute( "pattern" ), query->getAttribute( "replace" ) ); } } } // // XML 保存 // void ReplaceStr_Manager::save_xml() { XML::Document document; XML::Dom* root = document.appendChild( XML::NODE_TYPE_ELEMENT, kRootNodeNameReplaceStr ); if( ! root ) return; for( int i = 0; i < REPLACETARGET_MAX; ++i ) { XML::Dom* node = dom_append( root, i, m_chref[i] ); for( const auto& item : m_list[i] ) { dom_append( node, item.condition, item.pattern, item.replace ); } } #ifdef _DEBUG std::cout << "ReplaceStr_Manager::save_xml" << std::endl; std::cout << document.get_xml() << std::endl; #endif CACHE::save_rawdata( CACHE::path_replacestr(), document.get_xml() ); } // // 置換対象の要素名からid取得 // int ReplaceStr_Manager::target_id( const std::string& name ) { auto it = std::find_if( kReplStrTargetNames.begin(), kReplStrTargetNames.end(), [&name] ( const char* id ) { return name == id; } ); return std::distance( kReplStrTargetNames.begin(), it ); } // // 置換対象idから要素名取得 // std::string ReplaceStr_Manager::target_name( const int id ) { if( id >= REPLACETARGET_MAX ) return {}; return kReplStrTargetNames[id]; } // // 実行 // std::string ReplaceStr_Manager::replace( std::string_view str, const int id ) const { if( id >= REPLACETARGET_MAX || ( m_list[id].empty() && ! m_chref[id] ) ) return std::string{ str }; std::string buffer; if( m_chref[id] ) buffer = MISC::chref_decode( str, false ); else buffer.assign( str ); #ifdef _DEBUG std::cout << "ReplaceStr_Manager::replace str=" << buffer << std::endl; #endif // bufferに対してリストに登録された変換処理を実行していく for( const auto& item : m_list[id] ) { const ReplaceStrCondition condition = item.condition; if( ! condition.active || item.pattern.empty() ) continue; if( condition.regex ) { const int lng_buf = buffer.size(); // 文字列が空の時は"^$"などにマッチするように改行を追加 if( lng_buf == 0 ) buffer.push_back( '\n' ); JDLIB::Regex regex; bool match = false; int offset = 0; std::string replaced; while( regex.match( item.creg, buffer, offset, match ) ) { match = true; const int p0 = regex.pos( 0 ); if( p0 != offset ) replaced.append( buffer.substr( offset, p0 - offset ) ); // \0 ... \9 の文字列を置換 replaced.append( regex.replace( item.replace ) ); offset = p0 + regex.length( 0 ); if( offset >= lng_buf ) break; // 0文字にマッチするパターンは繰り返さない if( regex.length( 0 ) == 0 ) break; } if( match ) { if( lng_buf > offset ) { replaced.append( buffer.substr( offset, lng_buf - offset ) ); } buffer = std::move( replaced ); } } // 正規表現を使わない置換処理 else if( condition.icase ) buffer = MISC::replace_casestr( buffer, item.pattern, item.replace ); else buffer = MISC::replace_str( buffer, item.pattern, item.replace ); } #ifdef _DEBUG std::cout << "ReplaceStr_Manager::replace replaced str=" << buffer << std::endl; #endif return buffer; } // // 置換対象のXMLノード作成 // XML::Dom* ReplaceStr_Manager::dom_append( XML::Dom* node, const int id, const bool chref ) { XML::Dom* const dir = node->appendChild( XML::NODE_TYPE_ELEMENT, XML::get_name( TYPE_DIR ) ); dir->setAttribute( "name", target_name( id ) ); dir->setAttribute( "chref", chref ? "true" : "false" ); return dir; } // // 置換条件のXMLノード作成 // XML::Dom* ReplaceStr_Manager::dom_append( XML::Dom* node, ReplaceStrCondition condition, const std::string& pattern, const std::string& replace ) { XML::Dom* const query = node->appendChild( XML::NODE_TYPE_ELEMENT, XML::get_name( TYPE_REPLACESTR ) ); query->setAttribute( "condition", std::to_string( condition.to_ulong() ) ); query->setAttribute( "pattern", pattern ); query->setAttribute( "replace", replace ); return query; } jdim-0.10.1/src/replacestrmanager.h000066400000000000000000000055231445721505100171640ustar00rootroot00000000000000// ライセンス: GPL2 // // 文字列置換の管理クラス // #ifndef REPLACESTRMANAGER_H #define REPLACESTRMANAGER_H #include "jdlib/jdregex.h" #include #include #include #include namespace XML { class Dom; } namespace CORE { enum { REPLACETARGET_SUBJECT = 0, REPLACETARGET_NAME, REPLACETARGET_MAIL, REPLACETARGET_DATE, REPLACETARGET_MESSAGE, REPLACETARGET_MAX }; constexpr std::array kReplStrTargetNames = { "subject", "name", "mail", "date", "msg" }; constexpr std::array kReplStrTargetLabels = { "スレタイトル", "名前", "メール", "日付/ID", "本文" }; struct ReplaceStrCondition { // データ互換性のためフラグの並び(ビット位置)は変更しない unsigned char active : 1; // 0 unsigned char icase : 1; // 1 unsigned char regex : 1; // 2 unsigned char wchar : 1; // 3 unsigned char norm : 1; // 4 static ReplaceStrCondition from_ulong( unsigned long condition ) noexcept; unsigned long to_ulong() const; }; struct ReplaceStrItem { ReplaceStrCondition condition; std::string pattern; std::string replace; JDLIB::RegexPattern creg; }; class ReplaceStr_Manager { std::list m_list[ REPLACETARGET_MAX ]{}; bool m_chref[ REPLACETARGET_MAX ]{}; public: ReplaceStr_Manager(); ~ReplaceStr_Manager() noexcept = default; const std::list& get_list( int id ) const noexcept { return m_list[id]; } bool list_get_active( const int id ) const; void list_clear( const int id ); void list_append( const int id, ReplaceStrCondition condition, const std::string& pattern, const std::string& replace ); bool get_chref( int id ) const noexcept { return m_chref[id]; } void set_chref( int id, bool chref ) { m_chref[id] = chref; } void save_xml(); // 文字列を置換 std::string replace( std::string_view str, const int id ) const; static XML::Dom* dom_append( XML::Dom* node, const int id, const bool chref ); static XML::Dom* dom_append( XML::Dom* node, ReplaceStrCondition condition, const std::string& pattern, const std::string& replace ); private: void xml2list( const std::string& xml ); static int target_id( const std::string& name ); static std::string target_name( const int id ); }; /////////////////////////////////////// // インターフェース ReplaceStr_Manager* get_replacestr_manager(); void delete_replacestr_manager(); } #endif jdim-0.10.1/src/replacestrpref.cpp000066400000000000000000000455521445721505100170470ustar00rootroot00000000000000// ライセンス: GPL2 //#define _DEBUG #include "jddebug.h" #include "replacestrpref.h" #include "replacestrmanager.h" #include "control/controlid.h" #include "jdlib/miscgtk.h" #include "skeleton/msgdiag.h" #include "xml/document.h" #include "command.h" #include "environment.h" #include #include using namespace CORE; ReplaceStrDiag::ReplaceStrDiag( Gtk::Window* parent, ReplaceStrCondition condition, const Glib::ustring& pattern, const Glib::ustring& replace ) : SKELETON::PrefDiag( parent, "" ) , m_button_copy( "この設定をクリップボードにコピー(_L)", true ) , m_check_active( "有効(_E)", true ) , m_check_icase( "大文字小文字(_I)", true ) , m_check_regex( "正規表現(_R)", true ) , m_check_wchar( "全角半角(_W)", true ) , m_check_norm( "互換文字(_N)", true ) , m_label_pattern( "置換パターン(_P):", true ) , m_label_replace( "置換文字列(_S):", true ) { resize( 600, 1 ); m_button_copy.signal_clicked().connect( sigc::mem_fun( *this, &ReplaceStrDiag::slot_copy ) ); m_check_regex.signal_clicked().connect( sigc::mem_fun( *this, &ReplaceStrDiag::slot_sens ) ); m_check_wchar.signal_clicked().connect( sigc::mem_fun( *this, &ReplaceStrDiag::slot_sens ) ); m_check_norm.signal_clicked().connect( sigc::mem_fun( *this, &ReplaceStrDiag::slot_sens ) ); m_check_active.set_active( condition.active ); m_check_icase.set_active( condition.icase ); m_check_regex.set_active( condition.regex ); m_check_wchar.set_active( condition.wchar ); m_check_wchar.set_sensitive( condition.regex && ! condition.norm ); m_check_norm.set_active( condition.norm ); m_check_norm.set_sensitive( condition.regex && condition.wchar ); m_check_active.set_tooltip_text( "この条件の置換を有効にする" ); m_check_icase.set_tooltip_text( "大文字小文字を区別しない" ); m_check_regex.set_tooltip_text( "正規表現を使用する" ); m_check_wchar.set_tooltip_text( "英数字とカナの文字幅(いわゆる全角半角)を区別しない" ); m_check_norm.set_tooltip_text( "Unicodeの互換文字を区別しない" ); m_hbox_regex.pack_start( m_check_regex, Gtk::PACK_SHRINK ); m_hbox_regex.pack_start( m_check_icase, Gtk::PACK_SHRINK ); m_hbox_regex.pack_start( m_check_wchar, Gtk::PACK_SHRINK ); m_hbox_regex.pack_start( m_check_norm, Gtk::PACK_SHRINK ); m_entry_pattern.set_text( pattern ); m_entry_replace.set_text( replace ); m_entry_pattern.set_hexpand( true ); m_entry_replace.set_hexpand( true ); m_label_pattern.set_mnemonic_widget( m_entry_pattern ); m_label_replace.set_mnemonic_widget( m_entry_replace ); m_label_pattern.set_xalign( 0 ); m_label_replace.set_xalign( 0 ); m_grid_entry.attach( m_label_pattern, 0, 0, 1, 1 ); m_grid_entry.attach( m_entry_pattern, 1, 0, 1, 1 ); m_grid_entry.attach( m_label_replace, 0, 1, 1, 1 ); m_grid_entry.attach( m_entry_replace, 1, 1, 1, 1 ); m_grid_entry.set_row_spacing( 8 ); m_hbox_active.pack_start( m_check_active ); m_hbox_active.pack_start( m_button_copy, Gtk::PACK_SHRINK ); get_content_area()->set_spacing( 8 ); get_content_area()->pack_start( m_hbox_active, Gtk::PACK_SHRINK ); get_content_area()->pack_start( m_hbox_regex, Gtk::PACK_SHRINK ); get_content_area()->pack_start( m_grid_entry, Gtk::PACK_SHRINK ); set_title( "置換条件設定" ); show_all_children(); } // // 条件フラグ取得 // ReplaceStrCondition ReplaceStrDiag::get_condition() const { ReplaceStrCondition condition{}; condition.active = get_active(); condition.icase = get_icase(); condition.regex = get_regex(); condition.wchar = get_wchar(); condition.norm = get_norm(); return condition; } // // クリップボードにコピー // void ReplaceStrDiag::slot_copy() { XML::Document document; const XML::Dom* node = ReplaceStr_Manager::dom_append( &document, get_condition(), get_pattern(), get_replace() ); MISC::CopyClipboard( node->get_xml() ); } // // チェックボックスのsensitive切り替え // void ReplaceStrDiag::slot_sens() { if( ! get_regex() ) { m_check_wchar.set_active( false ); m_check_norm.set_active( false ); } m_check_wchar.set_sensitive( get_regex() && ! get_norm() ); m_check_norm.set_sensitive( get_regex() && get_wchar() ); } /////////////////////////////////////////////// ReplaceStrPref::ReplaceStrPref( Gtk::Window* parent, const std::string& url ) : SKELETON::PrefDiag( parent, url ) , m_id_target( REPLACETARGET_MAX - 1 ) // 初期設定は本文 , m_label_target( "置換対象(_R):", true ) , m_link_manual( ENVIRONMENT::get_jdhelpreplstr(), "オンラインマニュアル(_M)" ) , m_check_chref( "対象の置換前に文字参照をデコード(_E)", true ) , m_chref( REPLACETARGET_MAX ) , m_store( REPLACETARGET_MAX ) , m_button_top( g_dpgettext( GTK_DOMAIN, "Stock label, navigation\x04_Top", 24 ), true ) , m_button_up( g_dpgettext( GTK_DOMAIN, "Stock label, navigation\x04_Up", 24 ), true ) , m_button_down( g_dpgettext( GTK_DOMAIN, "Stock label, navigation\x04_Down", 24 ), true ) , m_button_bottom( g_dpgettext( GTK_DOMAIN, "Stock label, navigation\x04_Bottom", 24 ), true ) , m_button_delete( g_dpgettext( GTK_DOMAIN, "Stock label\x04_Delete", 12 ), true ) , m_button_add( g_dpgettext( GTK_DOMAIN, "Stock label\x04_Add", 12 ), true ) , m_vbuttonbox{ Gtk::ORIENTATION_VERTICAL } { m_button_top.set_image_from_icon_name( "go-top" ); m_button_up.set_image_from_icon_name( "go-up" ); m_button_down.set_image_from_icon_name( "go-down" ); m_button_bottom.set_image_from_icon_name( "go-bottom" ); m_button_top.signal_clicked().connect( sigc::mem_fun( *this, &ReplaceStrPref::slot_top ) ); m_button_up.signal_clicked().connect( sigc::mem_fun( *this, &ReplaceStrPref::slot_up ) ); m_button_down.signal_clicked().connect( sigc::mem_fun( *this, &ReplaceStrPref::slot_down ) ); m_button_bottom.signal_clicked().connect( sigc::mem_fun( *this, &ReplaceStrPref::slot_bottom ) ); m_button_delete.signal_clicked().connect( sigc::mem_fun( *this, &ReplaceStrPref::slot_delete ) ); m_button_add.signal_clicked().connect( sigc::mem_fun( *this, &ReplaceStrPref::slot_add ) ); std::generate( m_store.begin(), m_store.end(), [this] { return Gtk::ListStore::create( m_columns ); } ); m_current_store = m_store.back(); m_treeview.set_model( m_current_store ); m_treeview.set_size_request( 640, 400 ); m_treeview.signal_row_activated().connect( sigc::mem_fun( *this, &ReplaceStrPref::slot_row_activated ) ); m_treeview.sig_key_press().connect( sigc::mem_fun( *this, &ReplaceStrPref::slot_key_press ) ); m_treeview.sig_key_release().connect( sigc::mem_fun( *this, &ReplaceStrPref::slot_key_release ) ); std::vector columns( m_columns.size() ); columns[0] = Gtk::manage( new Gtk::TreeViewColumn( "有効", m_columns.m_col_active ) ); columns[0]->set_fixed_width( 35 ); columns[0]->set_alignment( Gtk::ALIGN_CENTER ); // 正規と大小は設定の関係性から元になったパッチから変更して列を入れ替えた columns[1] = Gtk::manage( new Gtk::TreeViewColumn( "正規", m_columns.m_col_regex ) ); columns[1]->set_fixed_width( 35 ); columns[1]->set_alignment( Gtk::ALIGN_CENTER ); columns[2] = Gtk::manage( new Gtk::TreeViewColumn( "大小", m_columns.m_col_icase ) ); columns[2]->set_fixed_width( 35 ); columns[2]->set_alignment( Gtk::ALIGN_CENTER ); columns[3] = Gtk::manage( new Gtk::TreeViewColumn( "全角", m_columns.m_col_wchar ) ); columns[3]->set_fixed_width( 35 ); columns[3]->set_alignment( Gtk::ALIGN_CENTER ); columns[4] = Gtk::manage( new Gtk::TreeViewColumn( "互換", m_columns.m_col_norm ) ); columns[4]->set_fixed_width( 35 ); columns[4]->set_alignment( Gtk::ALIGN_CENTER ); columns[5] = Gtk::manage( new Gtk::TreeViewColumn( "置換パターン", m_columns.m_col_pattern ) ); columns[5]->set_fixed_width( 220 ); columns[6] = Gtk::manage( new Gtk::TreeViewColumn( "置換文字列", m_columns.m_col_replace ) ); columns[6]->set_fixed_width( 200 ); for( Gtk::TreeViewColumn* col : columns ) { col->set_sizing( Gtk::TREE_VIEW_COLUMN_FIXED ); col->set_resizable( true ); m_treeview.append_column( *col ); } m_scrollwin.add( m_treeview ); m_scrollwin.set_policy( Gtk::POLICY_AUTOMATIC, Gtk::POLICY_ALWAYS ); m_scrollwin.set_size_request( 640, 400 ); m_vbuttonbox.pack_start( m_button_top, Gtk::PACK_SHRINK ); m_vbuttonbox.pack_start( m_button_up, Gtk::PACK_SHRINK ); m_vbuttonbox.pack_start( m_button_down, Gtk::PACK_SHRINK ); m_vbuttonbox.pack_start( m_button_bottom, Gtk::PACK_SHRINK ); m_vbuttonbox.pack_start( m_button_delete, Gtk::PACK_SHRINK ); m_vbuttonbox.pack_start( m_button_add, Gtk::PACK_SHRINK ); m_vbuttonbox.set_spacing( 4 ); m_hbox.pack_start( m_scrollwin, Gtk::PACK_EXPAND_WIDGET ); m_hbox.pack_start( m_vbuttonbox, Gtk::PACK_SHRINK ); for( const char* target : kReplStrTargetLabels ) { m_menu_target.append( target ); } m_menu_target.signal_changed().connect( sigc::mem_fun( *this, &ReplaceStrPref::slot_target_changed ) ); m_menu_target.set_active_text( kReplStrTargetLabels.back() ); m_label_target.set_mnemonic_widget( m_menu_target ); m_grid_head.attach( m_label_target, 0, 0, 1, 1 ); m_grid_head.attach( m_menu_target, 1, 0, 1, 1 ); m_grid_head.attach( m_link_manual, 2, 0, 1, 1 ); m_grid_head.attach( m_check_chref, 0, 1, 2, 1 ); m_grid_head.set_hexpand( true ); m_link_manual.set_halign( Gtk::ALIGN_END ); m_link_manual.set_hexpand( true ); m_link_manual.set_use_underline( true ); m_check_chref.set_tooltip_markup( "ダブルクォーテション\", アンパサント" "&, 少なり記号<, 大なり記号>除く" "文字参照をデコードしてから置換を行います。" ); get_content_area()->set_spacing( 8 ); get_content_area()->pack_start( m_grid_head, Gtk::PACK_SHRINK ); get_content_area()->pack_start( m_hbox ); show_all_children(); m_treeview.grab_focus(); set_title( "文字列置換設定" ); append_rows(); } void ReplaceStrPref::append_rows() { const ReplaceStr_Manager* const mgr = CORE::get_replacestr_manager(); for( int i = 0; i < REPLACETARGET_MAX; ++i ) { m_chref[i] = mgr->get_chref( i ); for( const auto& item : mgr->get_list( i ) ) { append_row( m_store[i], item.condition, item.pattern, item.replace ); } } const int id = m_menu_target.get_active_row_number(); m_check_chref.set_active( m_chref[id] ); select_row( get_top_row() ); } void ReplaceStrPref::append_row( const Glib::RefPtr& store, ReplaceStrCondition condition, const std::string& pattern, const std::string& replace ) { Gtk::TreeModel::Row row = *store->append(); if( row ) { row[ m_columns.m_col_active ] = condition.active; row[ m_columns.m_col_icase ] = condition.icase; row[ m_columns.m_col_regex ] = condition.regex; row[ m_columns.m_col_wchar ] = condition.wchar; row[ m_columns.m_col_norm ] = condition.norm; row[ m_columns.m_col_pattern ] = pattern; row[ m_columns.m_col_replace ] = replace; select_row( row ); } } Gtk::TreeModel::const_iterator ReplaceStrPref::get_selected_row() const { std::vector paths = m_treeview.get_selection()->get_selected_rows(); if( paths.empty() ) return Gtk::TreeModel::const_iterator(); return *( m_current_store->get_iter( paths.front() ) ); } Gtk::TreeModel::const_iterator ReplaceStrPref::get_top_row() const { Gtk::TreeModel::Children children = m_current_store->children(); if( children.empty() ) return Gtk::TreeModel::const_iterator(); return children.begin(); } Gtk::TreeModel::const_iterator ReplaceStrPref::get_bottom_row() const { Gtk::TreeModel::Children children = m_current_store->children(); if( children.empty() ) return Gtk::TreeModel::const_iterator(); return std::prev( children.end() ); } void ReplaceStrPref::select_row( const Gtk::TreeModel::const_iterator& it ) { if( ! it ) return; const Gtk::TreePath path( it ); m_treeview.get_selection()->select( path ); } // // OK ボタンを押した // void ReplaceStrPref::slot_ok_clicked() { ReplaceStr_Manager* const mgr = CORE::get_replacestr_manager(); m_chref[ m_id_target ] = m_check_chref.get_active(); for( int i = 0; i < REPLACETARGET_MAX; ++i ) { mgr->list_clear( i ); mgr->set_chref( i, m_chref[i] ); for( const Gtk::TreeModel::Row& row : m_store[i]->children() ) { const bool active = row[ m_columns.m_col_active ]; const bool icase = row[ m_columns.m_col_icase ]; const bool regex = row[ m_columns.m_col_regex ]; const bool wchar = row[ m_columns.m_col_wchar ]; const bool norm = row[ m_columns.m_col_norm ]; const ReplaceStrCondition condition{ active, icase, regex, wchar, norm }; const Glib::ustring& pattern = row[ m_columns.m_col_pattern ]; const Glib::ustring& replace = row[ m_columns.m_col_replace ]; mgr->list_append( i, condition, pattern.raw(), replace.raw() ); } } mgr->save_xml(); //DBTREE::update_abone_thread(); CORE::core_set_command( "relayout_all_board" ); CORE::core_set_command( "relayout_all_article", "", "completely" ); } void ReplaceStrPref::slot_row_activated( const Gtk::TreeModel::Path& path, Gtk::TreeViewColumn* ) { #ifdef _DEBUG std::cout << "ReplaceStrPref::slot_row_activated path = " << path.to_string() << std::endl; #endif Gtk::TreeModel::Row row = *( m_current_store->get_iter( path ) ); if( ! row ) return; const bool active = row[ m_columns.m_col_active ]; const bool icase = row[ m_columns.m_col_icase ]; const bool regex = row[ m_columns.m_col_regex ]; const bool wchar = row[ m_columns.m_col_wchar ]; const bool norm = row[ m_columns.m_col_norm ]; const ReplaceStrCondition condition{ active, icase, regex, wchar, norm }; ReplaceStrDiag dlg( this, condition, row[ m_columns.m_col_pattern ], row[ m_columns.m_col_replace ] ); if( dlg.run() == Gtk::RESPONSE_OK ) { row.set_value( m_columns.m_col_active, dlg.get_active() ); row.set_value( m_columns.m_col_icase, dlg.get_icase() ); row.set_value( m_columns.m_col_regex, dlg.get_regex() ); row.set_value( m_columns.m_col_wchar, dlg.get_wchar() ); row.set_value( m_columns.m_col_norm, dlg.get_norm() ); row.set_value( m_columns.m_col_pattern, dlg.get_pattern() ); row.set_value( m_columns.m_col_replace, dlg.get_replace() ); if( dlg.get_regex() ) { JDLIB::RegexPattern ptn; constexpr bool newline = true; constexpr bool migemo = false; if( ! ptn.set( dlg.get_pattern(), dlg.get_icase(), newline, migemo, dlg.get_wchar(), dlg.get_norm() ) ) { const std::string msg = ptn.errstr() + "\n\n" + dlg.get_pattern(); SKELETON::MsgDiag mdlg( *this, msg, false, Gtk::MESSAGE_WARNING, Gtk::BUTTONS_OK ); mdlg.run(); } } } } bool ReplaceStrPref::slot_key_press( GdkEventKey* event ) { const int id = m_control.key_press( event ); #ifdef _DEBUG std::cout << "ReplaceStrPref::slot_key_press id = " << id << std::endl; #endif if( id == CONTROL::Up ) { if( auto it = get_selected_row() ) { if( --it ) { m_treeview.get_selection()->select( it ); return true; } } } else if( id == CONTROL::Down ) { if( auto it = get_selected_row() ) { if( ++it ) { m_treeview.get_selection()->select( it ); return true; } } } else if( event->keyval == GDK_KEY_Return || event->keyval == GDK_KEY_space ) { if( auto it = get_selected_row() ) { slot_row_activated( Gtk::TreePath( it ), nullptr ); } } return false; } bool ReplaceStrPref::slot_key_release( GdkEventKey* event ) { const int id = m_control.key_press( event ); #ifdef _DEBUG std::cout << "ReplaceStrPref::slot_key_release id = " << id << std::endl; #endif if( id == CONTROL::Delete ) slot_delete(); return true; } // // 一番上へ移動 // void ReplaceStrPref::slot_top() { Gtk::TreeModel::iterator it = get_selected_row(); Gtk::TreeModel::iterator it_top = get_top_row(); if( it && it != it_top ) m_current_store->move( it, it_top ); } // // 上へ移動 // void ReplaceStrPref::slot_up() { if( auto it = get_selected_row() ) { if( auto it_dest = std::prev( it ) ) { m_current_store->iter_swap( it, it_dest ); } } } // // 下へ移動 // void ReplaceStrPref::slot_down() { if( auto it = get_selected_row() ) { if( auto it_dest = std::next( it ) ) { m_current_store->iter_swap( it, it_dest ); } } } // // 一番下へ移動 // void ReplaceStrPref::slot_bottom() { Gtk::TreeModel::iterator it = get_selected_row(); Gtk::TreeModel::iterator it_bottom = get_bottom_row(); if( it && it != it_bottom ) { m_current_store->move( it, m_current_store->children().end() ); } } // // 削除ボタン // void ReplaceStrPref::slot_delete() { Gtk::TreeModel::iterator it = get_selected_row(); if( ! it ) return; SKELETON::MsgDiag mdiag( *this, "削除しますか?", false, Gtk::MESSAGE_QUESTION, Gtk::BUTTONS_YES_NO ); if( mdiag.run() != Gtk::RESPONSE_YES ) return; auto it_next = std::next( it ); m_current_store->erase( it ); if( it_next ) select_row( it_next ); else { Gtk::TreeModel::iterator it_bottom = get_bottom_row(); if( it_bottom ) select_row( it_bottom ); } } // // 追加ボタン // void ReplaceStrPref::slot_add() { constexpr ReplaceStrCondition condition{ true, false, false, false, false }; ReplaceStrDiag dlg( this, condition, "", "" ); if( dlg.run() == Gtk::RESPONSE_OK ) { append_row( m_current_store, dlg.get_condition(), dlg.get_pattern(), dlg.get_replace() ); } } // // 置換対象変更 // void ReplaceStrPref::slot_target_changed() { const int id = m_menu_target.get_active_row_number(); #ifdef _DEBUG std::cout << "ReplaceStrPref::slot_target_changed target=" << id << std::endl; #endif m_chref[ m_id_target ] = m_check_chref.get_active(); m_check_chref.set_active( m_chref[ id ] ); m_current_store = m_store[ id ]; m_treeview.set_model( m_current_store ); select_row( get_top_row() ); m_id_target = id; } jdim-0.10.1/src/replacestrpref.h000066400000000000000000000104451445721505100165050ustar00rootroot00000000000000// ライセンス: GPL2 // // 文字列置換設定ダイアログ // #ifndef REPLACESTRPREF_H #define REPLACESTRPREF_H #include "skeleton/prefdiag.h" #include "skeleton/treeviewbase.h" #include "control/control.h" #include namespace CORE { struct ReplaceStrCondition; class ReplaceStrDiag : public SKELETON::PrefDiag { Gtk::Box m_hbox_active; Gtk::Box m_hbox_regex; Gtk::Button m_button_copy; Gtk::CheckButton m_check_active; Gtk::CheckButton m_check_icase; Gtk::CheckButton m_check_regex; Gtk::CheckButton m_check_wchar; Gtk::CheckButton m_check_norm; Gtk::Grid m_grid_entry; Gtk::Label m_label_pattern; Gtk::Entry m_entry_pattern; Gtk::Label m_label_replace; Gtk::Entry m_entry_replace; public: ReplaceStrDiag( Gtk::Window* parent, ReplaceStrCondition condition, const Glib::ustring& pattern, const Glib::ustring& replace ); ~ReplaceStrDiag() noexcept = default; bool get_active() const { return m_check_active.get_active(); } bool get_icase() const { return m_check_icase.get_active(); } bool get_regex() const { return m_check_regex.get_active(); } bool get_wchar() const { return m_check_wchar.get_active(); } bool get_norm() const { return m_check_norm.get_active(); } ReplaceStrCondition get_condition() const; Glib::ustring get_pattern() const { return m_entry_pattern.get_text(); } Glib::ustring get_replace() const { return m_entry_replace.get_text(); } private: void slot_copy(); void slot_sens(); }; class ReplaceRecord : public Gtk::TreeModel::ColumnRecord { public: Gtk::TreeModelColumn m_col_active; Gtk::TreeModelColumn m_col_icase; Gtk::TreeModelColumn m_col_regex; Gtk::TreeModelColumn m_col_wchar; Gtk::TreeModelColumn m_col_norm; Gtk::TreeModelColumn m_col_pattern; Gtk::TreeModelColumn m_col_replace; ReplaceRecord() { add( m_col_active ); add( m_col_icase ); add( m_col_regex ); add( m_col_wchar ); add( m_col_norm ); add( m_col_pattern ); add( m_col_replace ); } ~ReplaceRecord() noexcept = default; }; class ReplaceStrPref : public SKELETON::PrefDiag { int m_id_target; Gtk::Grid m_grid_head; Gtk::Label m_label_target; Gtk::ComboBoxText m_menu_target; Gtk::LinkButton m_link_manual; Gtk::CheckButton m_check_chref; std::vector m_chref; SKELETON::JDTreeViewBase m_treeview; CONTROL::Control m_control; std::vector> m_store; Glib::RefPtr m_current_store; ReplaceRecord m_columns; Gtk::ScrolledWindow m_scrollwin; Gtk::Button m_button_top; Gtk::Button m_button_up; Gtk::Button m_button_down; Gtk::Button m_button_bottom; Gtk::Button m_button_delete; Gtk::Button m_button_add; Gtk::Box m_vbuttonbox; Gtk::Box m_hbox; public: ReplaceStrPref( Gtk::Window* parent, const std::string& url ); ~ReplaceStrPref() noexcept = default; private: void append_rows(); void append_row( const Glib::RefPtr& store, ReplaceStrCondition condition, const std::string& pattern, const std::string& replace ); Gtk::TreeModel::const_iterator get_selected_row() const; Gtk::TreeModel::const_iterator get_top_row() const; Gtk::TreeModel::const_iterator get_bottom_row() const; void select_row( const Gtk::TreeModel::const_iterator& it ); // OK押した void slot_ok_clicked() override; void slot_row_activated( const Gtk::TreeModel::Path& path, Gtk::TreeViewColumn* ); bool slot_key_press( GdkEventKey* event ); bool slot_key_release( GdkEventKey* event ); void slot_top(); void slot_up(); void slot_down(); void slot_bottom(); void slot_delete(); void slot_add(); void slot_target_changed(); }; } #endif jdim-0.10.1/src/searchitempref.cpp000066400000000000000000000025321445721505100170160ustar00rootroot00000000000000// ライセンス: GPL2 //#define _DEBUG #include "jddebug.h" #include "searchitempref.h" #include "icons/iconmanager.h" #include "global.h" #include "session.h" #include "command.h" using namespace CORE; SearchItemPref::SearchItemPref( Gtk::Window* parent, const std::string& url ) : SKELETON::SelectItemPref( parent, url ) { // デフォルトの項目を設定 append_default_pair( ITEM_NAME_NAME, ICON::get_icon( ICON::TRANSPARENT ) ); append_default_pair( ITEM_NAME_SEARCH, ICON::get_icon( ICON::SEARCH ) ); append_default_pair( ITEM_NAME_RELOAD, ICON::get_icon( ICON::RELOAD ) ); append_default_pair( ITEM_NAME_STOPLOADING, ICON::get_icon( ICON::STOPLOADING ) ); append_default_pair( ITEM_NAME_QUIT, ICON::get_icon( ICON::QUIT ) ); append_default_pair( ITEM_NAME_SEPARATOR, ICON::get_icon( ICON::TRANSPARENT ) ); // 文字列を元に行を追加 append_rows( SESSION::get_items_search_toolbar_str() ); set_title( "ツールバー項目設定(ログ/スレタイ検索)" ); } // // OKを押した // void SearchItemPref::slot_ok_clicked() { SESSION::set_items_search_toolbar_str( get_items() ); CORE::core_set_command( "update_article_toolbar_button" ); } // // デフォルトボタン // void SearchItemPref::slot_default() { append_rows( SESSION::get_items_search_toolbar_default_str() ); } jdim-0.10.1/src/searchitempref.h000066400000000000000000000010751445721505100164640ustar00rootroot00000000000000// ライセンス: GPL2 // ログ/スレタイ検索のツールバーの表示項目設定 #ifndef _SEARCHITEMPREF_H #define _SEARCHITEMPREF_H #include "skeleton/selectitempref.h" namespace CORE { class SearchItemPref : public SKELETON::SelectItemPref { public: SearchItemPref( Gtk::Window* parent, const std::string& url ); ~SearchItemPref() noexcept = default; private: // OKボタン void slot_ok_clicked() override; // デフォルトボタン void slot_default() override; }; } #endif jdim-0.10.1/src/searchloader.cpp000066400000000000000000000042431445721505100164520ustar00rootroot00000000000000// ライセンス: GPL2 //#define _DEBUG #include "jddebug.h" #include "searchloader.h" #include "jdencoding.h" #include "usrcmdmanager.h" #include "jdlib/loaderdata.h" #ifdef _DEBUG #include "jdlib/misccharcode.h" #endif #include "config/globalconf.h" using namespace CORE; SearchLoader::SearchLoader() : SKELETON::TextLoader() { std::string url = CONFIG::get_url_search_title(); Encoding enc = Encoding::utf8; // 結果のエンコード指定を、検索結果のエンコードに設定する if( url.find( "$OUTU" ) != std::string::npos ) enc = Encoding::utf8; else if( url.find( "$OUTX" ) != std::string::npos ) enc = Encoding::eucjp; else if( url.find( "$OUTE" ) != std::string::npos ) enc = Encoding::sjis; // 結果のエンコード指定がない場合は、検索クエリのエンコードを検索結果のエンコードに設定する else if( url.find( "$TEXTX" ) != std::string::npos ) enc = Encoding::eucjp; else if( url.find( "$TEXTE" ) != std::string::npos ) enc = Encoding::sjis; set_encoding( enc ); #ifdef _DEBUG std::cout << "SearchLoader::SearchLoader encoding = " << MISC::encoding_to_cstr( enc ) << std::endl;; #endif } SearchLoader::~SearchLoader() { #ifdef _DEBUG std::cout << "SearchLoader::~SearchLoader\n"; #endif } std::string SearchLoader::get_url() const { std::string url = get_usrcmd_manager()->replace_cmd( CONFIG::get_url_search_title(), "", "", m_query, 0 ); #ifdef _DEBUG std::cout << "SearchLoader::get_url url = " << url << std::endl; #endif return url; } void SearchLoader::search( const std::string& query ) { #ifdef _DEBUG std::cout << "SearchLoader::search query = " << query << std::endl; #endif m_query = query; reset(); download_text( get_encoding() ); } // ロード用データ作成 void SearchLoader::create_loaderdata( JDLIB::LOADERDATA& data ) { #ifdef _DEBUG std::cout << "SearchLoader::create_loaderdata\n"; #endif data.init_for_data(); data.url = get_url(); } // ロード後に呼び出される void SearchLoader::parse_data() { #ifdef _DEBUG std::cout << "SearchLoader::parse_data\n"; #endif m_sig_search_fin.emit(); } jdim-0.10.1/src/searchloader.h000066400000000000000000000016441445721505100161210ustar00rootroot00000000000000// ライセンス: GPL2 // // スレタイ検索ローダ // #ifndef _SEARCHLOADER_H #define _SEARCHLOADER_H #include "skeleton/textloader.h" #include namespace CORE { class SearchLoader : public SKELETON::TextLoader { // 検索が終了するとemitされる typedef sigc::signal< void > SIG_SEARCH_FIN; SIG_SEARCH_FIN m_sig_search_fin; std::string m_query; public: SearchLoader(); ~SearchLoader(); SIG_SEARCH_FIN sig_search_fin(){ return m_sig_search_fin; } void search( const std::string& query ); protected: std::string get_url() const override; std::string get_path() const override { return {}; } // ロード用データ作成 void create_loaderdata( JDLIB::LOADERDATA& data ) override; // ロード後に呼び出される void parse_data() override; }; } #endif jdim-0.10.1/src/searchmanager.cpp000066400000000000000000000176111445721505100166210ustar00rootroot00000000000000// ライセンス: GPL2 //#define _DEBUG #include "jddebug.h" #include "searchmanager.h" #include "searchloader.h" #include "jdlib/miscutil.h" #include "jdlib/miscmsg.h" #include "jdlib/jdregex.h" #include "dbtree/interface.h" #include "dbtree/articlebase.h" #include "config/globalconf.h" #include CORE::Search_Manager* instance_search_manager = nullptr; CORE::Search_Manager* CORE::get_search_manager() { if( ! instance_search_manager ) instance_search_manager = new Search_Manager(); assert( instance_search_manager ); return instance_search_manager; } void CORE::delete_search_manager() { if( instance_search_manager ) delete instance_search_manager; instance_search_manager = nullptr; } /////////////////////////////////////////////// using namespace CORE; Search_Manager::Search_Manager() = default; Search_Manager::~Search_Manager() { // デストラクタの中からdispatchを呼ぶと落ちるので dispatch不可にする set_dispatchable( false ); stop( std::string() ); // m_thread.joinable() == true のときスレッドを破棄すると強制終了するため待機処理を入れる wait(); } // // ログ検索 // // 結果は m_list_article と m_list_data に入る。 // // id : 呼び出し元の ID。 検索終了時に SIG_SEARCH_FIN シグナルで送る // searchmode : 検索モード // url: ログ検索先の板のアドレス // query : 検索文字列、空文字ならキャッシュにあるスレを全て選択 // mode_or : false なら AND、true なら OR で検索する // bm : trueの時、しおりが付いている(スレ一覧でしおりを付けた or レスに一つでもしおりが付いている)スレのみを対象に検索する // calc_data : 検索終了時に m_list_data を求める // bool Search_Manager::search( const std::string& id, const int searchmode, const std::string& url, const std::string& query, const bool mode_or, const bool bm, const bool calc_data ) { #ifdef _DEBUG std::cout << "Search_Manager::search url = " << url << " query = " << query << std::endl; #endif if( m_searching ) return false; if( m_thread.joinable() ) return false; m_searchmode = searchmode; m_id = id; m_url = url; m_query = query; m_mode_or = mode_or; m_bm = bm; m_calc_data = calc_data; m_list_article.clear(); m_list_data.clear(); // 全ログ検索のとき、スレッドを起動する前にメインスレッドで板情報ファイルを // 読み込んでおかないと大量の warning が出る if( m_searchmode == SEARCHMODE_ALLLOG ) DBTREE::read_boardinfo_all(); try { m_thread = std::thread( &Search_Manager::thread_search, this ); } catch( std::system_error& ) { MISC::ERRMSG( "Search_Manager::search : could not start thread" ); return false; } m_searching = true; return true; } // // ログ検索実行スレッド // void Search_Manager::thread_search() { #ifdef _DEBUG std::cout << "Search_Manager::thread_search\n"; #endif m_stop = false; if( m_searchmode == SEARCHMODE_LOG ) DBTREE::search_cache( m_url, m_list_article, m_query, m_mode_or, m_bm, m_stop ); else if( m_searchmode == SEARCHMODE_ALLLOG ) DBTREE::search_cache_all( m_list_article, m_query, m_mode_or, m_bm, m_stop ); if( m_calc_data ){ for( const DBTREE::ArticleBase* article : m_list_article ) { SEARCHDATA data; data.url_readcgi = DBTREE::url_readcgi( article->get_url(), 0, 0 ); data.subject = article->get_subject(); data.num = article->get_number_load(); data.bookmarked = article->is_bookmarked_thread(); data.num_bookmarked = article->get_num_bookmark(); data.boardname = DBTREE::board_name( data.url_readcgi ); #ifdef _DEBUG std::cout << "url = " << data.url_readcgi << std::endl << "board = " << data.boardname << std::endl << "subject = " << data.subject << std::endl << "num = " << data.num << std::endl << std::endl; #endif m_list_data.push_back( data ); } } dispatch(); } // // ディスパッチャのコールバック関数 // void Search_Manager::callback_dispatch() { wait(); search_fin(); } // // 検索終了 // void Search_Manager::search_fin() { #ifdef _DEBUG std::cout << "Search_Manager::search_fin\n"; #endif m_sig_search_fin.emit( m_id ); m_searching = false; } // // 検索中止 // void Search_Manager::stop( const std::string& id ) { if( ! m_searching ) return; if( ! id.empty() && id != m_id ) return; #ifdef _DEBUG std::cout << "Search_Manager::stop\n"; #endif m_stop = true; // スレタイ検索停止 if( m_searchmode == SEARCHMODE_TITLE && m_searchloader ) m_searchloader->stop_load(); } void Search_Manager::wait() { if( m_thread.joinable() ) m_thread.join(); } ///////////////////////////////////////////////////////////////////////// // // スレタイ検索 // bool Search_Manager::search_title( const std::string& id, const std::string& query ) { if( m_searching ) return false; if( m_searchloader && m_searchloader->is_loading() ) return false; #ifdef _DEBUG std::cout << "Search_Manager::search_title query = " << query << std::endl; #endif m_searchmode = SEARCHMODE_TITLE; m_id = id; m_query = query; m_list_data.clear(); // スレタイの検索が終わったら search_fin_title が呼び出される if( ! m_searchloader ){ m_searchloader = std::make_unique(); m_searchloader->sig_search_fin().connect( sigc::mem_fun( *this, &Search_Manager::search_fin_title ) ); } m_searching = true; m_searchloader->search( m_query ); return true; } // // スレタイ検索ロード終了 // void Search_Manager::search_fin_title() { #ifdef _DEBUG std::cout << "Search_Manager::search_fin_title\n"; #endif if( ! m_searchloader ) return; // 正規表現を使ってURLを抜き出していく const std::string& source = m_searchloader->get_data(); if( ! source.empty() ){ const std::string pattern = CONFIG::get_regex_search_title(); JDLIB::Regex regex; const bool icase = false; const bool newline = true; const bool usemigemo = false; const bool wchar = false; const JDLIB::RegexPattern regexptn( pattern, icase, newline, usemigemo, wchar ); const std::string named_caps[3] = { "url", "subject", "number" }; std::size_t offset = 0; while( regex.match( regexptn, source, offset, false, false, named_caps ) ){ SEARCHDATA data; // パターン中にグループ名が有れば名前付きキャプチャ、無ければグループ番号で取得する data.url_readcgi = DBTREE::url_readcgi( regex.named_or_num( named_caps[0], 1 ), 0, 0 ); data.subject = MISC::html_unescape( regex.named_or_num( named_caps[1], 2 ) ); data.num = std::atoi( regex.named_or_num( named_caps[2], 3 ).c_str() ); // マッチしていなければ 0 になる data.bookmarked = false; data.num_bookmarked = 0; if( ! data.url_readcgi.empty() ){ data.boardname = DBTREE::board_name( data.url_readcgi ); #ifdef _DEBUG std::cout << "url = " << data.url_readcgi << std::endl << "board = " << data.boardname << std::endl << "subject = " << data.subject << std::endl << "num = " << data.num << std::endl << std::endl; #endif m_list_data.push_back( data ); } // オフセットを設定して再検索する offset = regex.pos( 0 ) + regex.length( 0 ); } } m_searchloader->reset(); // call SKELETON::TextLoader::reset() search_fin(); } jdim-0.10.1/src/searchmanager.h000066400000000000000000000065621445721505100162710ustar00rootroot00000000000000// ライセンス: GPL2 // // ログ、スレタイ検索クラス // #ifndef _SEARCHMANAGER_H #define _SEARCHMANAGER_H #include "skeleton/dispatchable.h" #include #include #include #include #include namespace DBTREE { class ArticleBase; } namespace CORE { class SearchLoader; // 検索モード enum { SEARCHMODE_LOG = 0, // ログ検索 SEARCHMODE_ALLLOG, // 全ログ検索 SEARCHMODE_TITLE // スレタイ検索 }; struct SEARCHDATA { std::string url_readcgi; std::string boardname; std::string subject; int num; // 読み込み数 bool bookmarked; // スレ全体にしおりがついているか int num_bookmarked; // レスにつけられたしおりの数 }; class Search_Manager : public SKELETON::Dispatchable { typedef sigc::signal< void, const std::string& > SIG_SEARCH_FIN; SIG_SEARCH_FIN m_sig_search_fin; std::thread m_thread; int m_searchmode{}; std::string m_id; std::string m_url; std::string m_query; bool m_mode_or{}; bool m_bm{}; bool m_calc_data{}; std::vector< DBTREE::ArticleBase* > m_list_article; std::list< SEARCHDATA > m_list_data; // 検索実行中 bool m_searching{}; bool m_stop{}; // スレタイ検索ローダ std::unique_ptr m_searchloader; public: Search_Manager(); ~Search_Manager(); SIG_SEARCH_FIN sig_search_fin(){ return m_sig_search_fin; } const std::vector< DBTREE::ArticleBase* >& get_list_article() const{ return m_list_article; } const std::list< SEARCHDATA >& get_list_data() const { return m_list_data; } bool is_searching() const { return m_searching; } bool is_searching( const std::string& id ) const { if( id == m_id ) return m_searching; else return false; } void stop( const std::string& id ); // ログ検索 // // 結果は m_list_article と m_list_data に入る。 // // id : 呼び出し元の ID。 検索終了時に SIG_SEARCH_FIN シグナルで送る // searchmode : 検索モード // url: ログ検索先の板のアドレス // query : 検索文字列、空文字ならキャッシュにあるスレを全て選択 // mode_or : false なら AND、true なら OR で検索する // bm : trueの時、しおりが付いている(スレ一覧でしおりを付けた or レスに一つでもしおりが付いている)スレのみを対象に検索する // calc_data : 検索終了時に m_list_data を求める bool search( const std::string& id, const int searchmode, const std::string& url, const std::string& query, const bool mode_or, const bool bm, const bool calc_data ); // スレタイ検索 bool search_title( const std::string& id, const std::string& query ); private: void wait(); void thread_search(); void callback_dispatch() override; void search_fin(); void search_fin_title(); }; /////////////////////////////////////// // インターフェース Search_Manager* get_search_manager(); void delete_search_manager(); } #endif jdim-0.10.1/src/session.cpp000066400000000000000000001467651445721505100155210ustar00rootroot00000000000000// ライセンス: GPL2 //#define _DEBUG #include "jddebug.h" #include "session.h" #include "cache.h" #include "global.h" #include "jdlib/confloader.h" #include "jdlib/miscutil.h" #include "jdlib/misctime.h" #include "bbslist/bbslistadmin.h" #include "article/articleadmin.h" #include "article/articleviewbase.h" #include #include bool booting = true; bool quitting = false; bool tab_operating_article = false; bool tab_operating_image = false; int mode_pane; bool mode_online; bool mode_login2ch; bool mode_loginbe; enum { mode_loginp2 }; // Removed in v0.3.0 (2020-05) int win_hpane_main_pos; int win_vpane_main_pos; int win_hpane_main_r_pos; int win_vpane_main_mes_pos; int win_notebook_main_page; int win_bbslist_page; int win_board_page; int win_article_page; int win_image_page; std::list< std::string > board_urls; std::list< bool > board_locked; std::list< std::string > board_switchhistory; std::list< std::string > article_urls; std::list< bool > article_locked; std::list< std::string > article_switchhistory; std::list< std::string > image_urls; std::list< bool > image_locked; std::string items_sidebar_toolbar_str; std::vector< int > items_sidebar_toolbar; std::string items_main_toolbar_str; std::vector< int > items_main_toolbar; std::string items_article_toolbar_str; std::vector< int > items_article_toolbar; std::string items_search_toolbar_str; std::vector< int > items_search_toolbar; std::string items_board_toolbar_str; std::vector< int > items_board_toolbar; std::string items_board_col_str; std::vector< int > items_board_col; std::string items_msg_toolbar_str; std::vector< int > items_msg_toolbar; std::string items_board_menu_str; std::vector< int > items_board_menu; std::string items_article_menu_str; std::vector< int > items_article_menu; int board_col_mark; int board_col_id; int board_col_board; int board_col_subject; int board_col_number; int board_col_load; int board_col_new; int board_col_since; int board_col_write; int board_col_access; int board_col_speed; int board_col_diff; int board_col_since_time; int board_col_write_time; int board_col_access_time; bool win_show_sidebar; bool win_show_menubar; bool show_main_toolbar; int win_toolbar_pos; bool show_bbslist_toolbar; bool show_board_toolbar; bool show_article_toolbar; bool show_board_tab; bool show_article_tab; bool show_main_statbar; int win_focused_admin; int win_focused_admin_sidebar; int x_win_main; int y_win_main; int x_win_img; int y_win_img; int x_win_mes; int y_win_mes; int width_win_main; int height_win_main; int width_win_img; int height_win_img; int width_win_mes; int height_win_mes; bool focus_win_main = false; bool focus_win_img = false; bool focus_win_mes = false; bool maximized_win_main = false; bool maximized_win_img = false; bool maximized_win_mes = false; bool iconified_win_main = false; bool iconified_win_img = false; bool iconified_win_mes = false; bool shown_win_main = false; bool shown_win_img = false; bool shown_win_mes = false; bool full_win_main = false; bool dialog_shown = false; bool embedded_img; bool embedded_mes; bool close_mes; std::string dir_dat; std::string img_dir_img_save; std::string dir_draft; bool popupmenu_shown; std::vector< std::string > delete_list; std::vector< std::string > live_urls; int img_fit_mode; std::string dir_select_favorite; ///////////////////////////////////// // ツールバー等の項目名 -> ID 変換 int SESSION::parse_item( const std::string& item_name ) { int item = ITEM_END; if( item_name == ITEM_NAME_BBSLISTVIEW ) item = ITEM_BBSLISTVIEW; else if( item_name == ITEM_NAME_FAVORITEVIEW ) item = ITEM_FAVORITEVIEW; else if( item_name == ITEM_NAME_BOARDVIEW ) item = ITEM_BOARDVIEW; else if( item_name == ITEM_NAME_HISTVIEW ) item = ITEM_HISTVIEW; else if( item_name == ITEM_NAME_HIST_BOARDVIEW ) item = ITEM_HIST_BOARDVIEW; else if( item_name == ITEM_NAME_HIST_CLOSEVIEW ) item = ITEM_HIST_CLOSEVIEW; else if( item_name == ITEM_NAME_HIST_CLOSEBOARDVIEW ) item = ITEM_HIST_CLOSEBOARDVIEW; else if( item_name == ITEM_NAME_HIST_CLOSEIMGVIEW ) item = ITEM_HIST_CLOSEIMGVIEW; else if( item_name == ITEM_NAME_ARTICLEVIEW ) item = ITEM_ARTICLEVIEW; else if( item_name == ITEM_NAME_IMAGEVIEW ) item = ITEM_IMAGEVIEW; else if( item_name == ITEM_NAME_URL ) item = ITEM_URL; else if( item_name == ITEM_NAME_GO ) item = ITEM_GO; else if( item_name == ITEM_NAME_SEPARATOR ) item = ITEM_SEPARATOR; else if( item_name == ITEM_NAME_MARK ) item = ITEM_MARK; else if( item_name == ITEM_NAME_ID ) item = ITEM_ID; else if( item_name == ITEM_NAME_BOARD ) item = ITEM_BOARD; else if( item_name == ITEM_NAME_NAME ) item = ITEM_NAME; else if( item_name == ITEM_NAME_RES ) item = ITEM_RES; else if( item_name == ITEM_NAME_LOAD ) item = ITEM_LOAD; else if( item_name == ITEM_NAME_NEW ) item = ITEM_NEW; else if( item_name == ITEM_NAME_SINCE ) item = ITEM_SINCE; else if( item_name == ITEM_NAME_LASTWRITE ) item = ITEM_LASTWRITE; else if( item_name == ITEM_NAME_ACCESS ) item = ITEM_ACCESS; else if( item_name == ITEM_NAME_SPEED ) item = ITEM_SPEED; else if( item_name == ITEM_NAME_DIFF ) item = ITEM_DIFF; else if( item_name == ITEM_NAME_WRITEMSG ) item = ITEM_WRITEMSG; else if( item_name == ITEM_NAME_OPENBOARD ) item = ITEM_OPENBOARD; else if( item_name == ITEM_NAME_OPENARTICLETAB ) item = ITEM_OPENARTICLETAB; else if( item_name == ITEM_NAME_REGETARTICLE ) item = ITEM_REGETARTICLE; else if( item_name == ITEM_NAME_BOOKMARK ) item = ITEM_BOOKMARK; else if( item_name == ITEM_NAME_SEARCH ) item = ITEM_SEARCH; else if( item_name == ITEM_NAME_DRAWOUT ) item = ITEM_DRAWOUT; else if( item_name == ITEM_NAME_RELOAD ) item = ITEM_RELOAD; else if( item_name == ITEM_NAME_STOPLOADING ) item = ITEM_STOPLOADING; else if( item_name == ITEM_NAME_APPENDFAVORITE ) item = ITEM_APPENDFAVORITE; else if( item_name == ITEM_NAME_FAVORITE_ARTICLE ) item = ITEM_FAVORITE_ARTICLE; else if( item_name == ITEM_NAME_CHECK_UPDATE_ROOT ) item = ITEM_CHECK_UPDATE_ROOT; else if( item_name == ITEM_NAME_CHECK_UPDATE_OPEN_ROOT ) item = ITEM_CHECK_UPDATE_OPEN_ROOT; else if( item_name == ITEM_NAME_COPY ) item = ITEM_COPY; else if( item_name == ITEM_NAME_COPY_URL ) item = ITEM_COPY_URL; else if( item_name == ITEM_NAME_COPY_TITLE_URL ) item = ITEM_COPY_TITLE_URL; else if( item_name == ITEM_NAME_COPY_TITLE_URL_THREAD ) item = ITEM_COPY_TITLE_URL_THREAD; else if( item_name == ITEM_NAME_COPY_THREAD_INFO ) item = ITEM_COPY_THREAD_INFO; else if( item_name == ITEM_NAME_DELETE ) item = ITEM_DELETE; else if( item_name == ITEM_NAME_QUIT ) item = ITEM_QUIT; else if( item_name == ITEM_NAME_BACK ) item = ITEM_BACK; else if( item_name == ITEM_NAME_FORWARD ) item = ITEM_FORWARD; else if( item_name == ITEM_NAME_LOCK ) item = ITEM_LOCK; else if( item_name == ITEM_NAME_LIVE ) item = ITEM_LIVE; else if( item_name == ITEM_NAME_NEWARTICLE ) item = ITEM_NEWARTICLE; else if( item_name == ITEM_NAME_SEARCHBOX ) item = ITEM_SEARCHBOX; else if( item_name == ITEM_NAME_SEARCH_NEXT ) item = ITEM_SEARCH_NEXT; else if( item_name == ITEM_NAME_SEARCH_PREV ) item = ITEM_SEARCH_PREV; else if( item_name == ITEM_NAME_NEXTARTICLE ) item = ITEM_NEXTARTICLE; else if( item_name == ITEM_NAME_CLEAR_HIGHLIGHT ) item = ITEM_CLEAR_HIGHLIGHT; else if( item_name == ITEM_NAME_INSERTTEXT ) item = ITEM_INSERTTEXT; else if( item_name == ITEM_NAME_LOCK_MESSAGE ) item = ITEM_LOCK_MESSAGE; else if( item_name == ITEM_NAME_PREVIEW ) item = ITEM_PREVIEW; else if( item_name == ITEM_NAME_UNDO ) item = ITEM_UNDO; else if( item_name == ITEM_NAME_REDO ) item = ITEM_REDO; else if( item_name == ITEM_NAME_NGWORD ) item = ITEM_NGWORD; else if( item_name == ITEM_NAME_ABONE_SELECTION ) item = ITEM_ABONE_SELECTION; else if( item_name == ITEM_NAME_ABONE_ARTICLE ) item = ITEM_ABONE_ARTICLE; else if( item_name == ITEM_NAME_QUOTE_SELECTION ) item = ITEM_QUOTE_SELECTION; else if( item_name == ITEM_NAME_OPEN_BROWSER ) item = ITEM_OPEN_BROWSER; else if( item_name == ITEM_NAME_OPEN_CACHE_BROWSER ) item = ITEM_OPEN_CACHE_BROWSER; else if( item_name == ITEM_NAME_USER_COMMAND ) item = ITEM_USER_COMMAND; else if( item_name == ITEM_NAME_ETC ) item = ITEM_ETC; else if( item_name == ITEM_NAME_SAVE_DAT ) item = ITEM_SAVE_DAT; else if( item_name == ITEM_NAME_SELECTIMG ) item = ITEM_SELECTIMG; else if( item_name == ITEM_NAME_SELECTDELIMG ) item = ITEM_SELECTDELIMG; else if( item_name == ITEM_NAME_SELECTABONEIMG ) item = ITEM_SELECTABONEIMG; else if( item_name == ITEM_NAME_PREF_BOARD ) item = ITEM_PREF_BOARD; else if( item_name == ITEM_NAME_PREF_THREAD ) item = ITEM_PREF_THREAD; return item; } std::vector< int > parse_items( const std::string& items_str ) { std::vector< int > items; const std::list< std::string > list_order = MISC::split_line( items_str ); for( const std::string& item_str : list_order ) { const int item = SESSION::parse_item( item_str ); if( item != ITEM_END ) items.push_back( item ); } items.push_back( ITEM_END ); return items; } void read_list_urls( JDLIB::ConfLoader& cf, const std::string& id_urls, std::list< std::string >& list_urls ) { list_urls.clear(); const std::string str_tmp = cf.get_option_str( id_urls, "" ); if( ! str_tmp.empty() ){ std::list list_tmp = MISC::split_line( str_tmp ); for( std::string& url : list_tmp ) if( ! url.empty() ) list_urls.push_back( std::move( url ) ); } } void read_list_locked( JDLIB::ConfLoader& cf, const std::string& id_locked, std::list< bool >& list_locked ) { list_locked.clear(); const std::string str_tmp = cf.get_option_str( id_locked, "" ); if( ! str_tmp.empty() ){ const std::list list_tmp = MISC::split_line( str_tmp ); std::transform( list_tmp.cbegin(), list_tmp.cend(), std::back_inserter( list_locked ), []( const auto& lock_str ) { return lock_str == "1"; } ); } } // セッション情報読み込み void SESSION::init_session() { #ifdef _DEBUG std::cout << "SESSION::init_session\n"; #endif JDLIB::ConfLoader cf( CACHE::path_session(), std::string() ); // オンライン mode_online = cf.get_option_bool( "mode_online", true ); // 2chログイン mode_login2ch = cf.get_option_bool( "mode_login2ch", false ); // beログイン mode_loginbe = cf.get_option_bool( "mode_loginbe", false ); // paneのモード mode_pane = cf.get_option_int( "mode_pane", MODE_2PANE, 0, MODE_PANE_NUM -1 ); x_win_main = cf.get_option_int( "x", 0, 0, 8192 ); y_win_main = cf.get_option_int( "y", 0, 0, 8192 ); width_win_main = cf.get_option_int( "width", 800, 80, 8192 ); height_win_main = cf.get_option_int( "height", 600, 60, 8192 ); maximized_win_main = cf.get_option_bool( "maximized", false ); full_win_main = cf.get_option_bool( "full_win_main", false ); win_show_sidebar = cf.get_option_bool( "show_sidebar", true ); win_show_menubar = cf.get_option_bool( "show_menubar", true ); show_main_toolbar = cf.get_option_bool( "show_main_toolbar", true ); win_toolbar_pos = cf.get_option_int( "toolbar_pos", TOOLBAR_POS_NORMAL, 0, TOOLBAR_POS_NUM -1 ); show_bbslist_toolbar = cf.get_option_bool( "show_bbslist_toolbar", true ); show_board_toolbar = cf.get_option_bool( "show_board_toolbar", true ); show_article_toolbar = cf.get_option_bool( "show_article_toolbar", true ); show_board_tab = cf.get_option_bool( "show_board_tab", true ); show_article_tab = cf.get_option_bool( "show_article_tab", true ); show_main_statbar = cf.get_option_bool( "show_main_statbar", true ); win_focused_admin = cf.get_option_int( "focused_admin", FOCUS_NOT, 0, FOCUS_NUM -1 ); win_focused_admin_sidebar = cf.get_option_int( "focused_admin_sidebar", FOCUS_NOT, 0, FOCUS_NUM -1 ); win_hpane_main_pos = cf.get_option_int( "hpane_main_pos", 190, 0, 8192 ); win_vpane_main_pos = cf.get_option_int( "vpane_main_pos", 200, 0, 8192 ); win_hpane_main_r_pos = cf.get_option_int( "hpane_main_r_pos", 300, 0, 8192 ); win_vpane_main_mes_pos = cf.get_option_int( "vpane_main_mes_pos", 400, 0, 8192 ); // メインnotebookのページ番号 win_notebook_main_page = cf.get_option_int( "notebook_main_page", PAGE_ARTICLE, 0, PAGE_NUM -1 ); // 各ビューの開いてるページ番号 win_bbslist_page = cf.get_option_int( "bbslist_page", 0, 0, 1024 ); win_board_page = cf.get_option_int( "board_page", 0, 0, 1024 ); win_article_page = cf.get_option_int( "article_page", 0, 0, 1024 ); win_image_page = cf.get_option_int( "image_page", 0, 0, 1024 ); // スレ一覧のURLとロック状態と切り替え履歴 read_list_urls( cf, "board_urls", board_urls ); read_list_locked( cf, "board_locked", board_locked ); read_list_urls( cf, "board_switchhistory", board_switchhistory ); // スレタブのURLとロック状態と切り替え履歴 read_list_urls( cf, "article_urls", article_urls ); read_list_locked( cf, "article_locked", article_locked ); read_list_urls( cf, "article_switchhistory", article_switchhistory ); // 画像タブのURLとロック状態 read_list_urls( cf, "image_urls", image_urls ); read_list_locked( cf, "image_locked", image_locked ); // サイドバーのツールバー項目 items_sidebar_toolbar_str = cf.get_option_str( "items_sidebar", get_items_sidebar_toolbar_default_str() ); items_sidebar_toolbar = parse_items( items_sidebar_toolbar_str ); // メインツールバーの項目 items_main_toolbar_str = cf.get_option_str( "items_main_toolbar", get_items_main_toolbar_default_str() ); items_main_toolbar = parse_items( items_main_toolbar_str ); // スレビューのツールバーの項目 items_article_toolbar_str = cf.get_option_str( "items_article_toolbar", get_items_article_toolbar_default_str() ); items_article_toolbar = parse_items( items_article_toolbar_str ); // 検索ビューのツールバーの項目 items_search_toolbar_str = cf.get_option_str( "items_search_toolbar", get_items_search_toolbar_default_str() ); items_search_toolbar = parse_items( items_search_toolbar_str ); // スレ一覧のツールバー項目 items_board_toolbar_str = cf.get_option_str( "items_board_toolbar", get_items_board_toolbar_default_str() ); items_board_toolbar = parse_items( items_board_toolbar_str ); // 書き込みビューのツールバー項目 items_msg_toolbar_str = cf.get_option_str( "items_msg_toolbar", get_items_msg_toolbar_default_str() ); items_msg_toolbar = parse_items( items_msg_toolbar_str ); // スレ一覧の列項目 items_board_col_str = cf.get_option_str( "items_board", get_items_board_col_default_str() ); items_board_col = parse_items( items_board_col_str ); // スレ一覧のコンテキストメニュー項目 items_board_menu_str = cf.get_option_str( "items_board_menu", get_items_board_menu_default_str() ); items_board_menu = parse_items( items_board_menu_str ); // スレビューのコンテキストメニュー項目 items_article_menu_str = cf.get_option_str( "items_article_menu", get_items_article_menu_default_str() ); items_article_menu = parse_items( items_article_menu_str ); // board ビューの列幅 board_col_mark = cf.get_option_int( "col_mark", 30, 4, 8192 ); board_col_id = cf.get_option_int( "col_id", 45, 4, 8192 ); board_col_board = cf.get_option_int( "col_board", 70, 4, 8192 ); board_col_subject = cf.get_option_int( "col_subject", 190, 4, 8192 ); board_col_number = cf.get_option_int( "col_number", 45, 4, 8192 ); board_col_load = cf.get_option_int( "col_load", 45, 4, 8192 ); board_col_new = cf.get_option_int( "col_new", 45, 4, 8192 ); board_col_since = cf.get_option_int( "col_since", 70, 4, 8192 ); board_col_write = cf.get_option_int( "col_write", 70, 4, 8192 ); board_col_access = cf.get_option_int( "col_access", 70, 4, 8192 ); board_col_speed = cf.get_option_int( "col_speed", 45, 4, 8192 ); board_col_diff = cf.get_option_int( "col_diff", 45, 4, 8192 ); // スレ一覧の since の表示モード board_col_since_time = cf.get_option_int( "col_since_time", MISC::TIME_NORMAL, 0, MISC::TIME_NUM-1 ); // スレ一覧の 最終書込 の表示モード board_col_write_time = cf.get_option_int( "col_write_time", MISC::TIME_NORMAL, 0, MISC::TIME_NUM-1 ); // スレ一覧の 最終取得 の表示モード board_col_access_time = cf.get_option_int( "col_access_time", MISC::TIME_NORMAL, 0, MISC::TIME_NUM-1 ); embedded_img = cf.get_option_bool( "embedded_img", true ); x_win_img = cf.get_option_int( "x_win_img", 0, 0, 8192 ); y_win_img = cf.get_option_int( "y_win_img", 0, 0, 8192 ); width_win_img = cf.get_option_int( "width_win_img", 600, 60, 8192 ); height_win_img = cf.get_option_int( "height_win_img", 400, 40, 8192 ); embedded_mes = cf.get_option_bool( "embedded_mes", false ); // 書き込み後にmessageを閉じる close_mes = cf.get_option_bool( "close_mes", true ); x_win_mes = cf.get_option_int( "x_win_mes", 0, 0, 8192 ); y_win_mes = cf.get_option_int( "y_win_mes", 0, 0, 8192 ); width_win_mes = cf.get_option_int( "width_win_mes", 600, 60, 8192 ); height_win_mes = cf.get_option_int( "height_win_mes", 400, 40, 8192 ); dir_dat = cf.get_option_str( "dir_dat", "" ); img_dir_img_save = cf.get_option_str( "img_dir_img_save", "" ); dir_draft = cf.get_option_str( "dir_draft", "" ); popupmenu_shown = false; // 画像のfitモード img_fit_mode = cf.get_option_int( "img_fit_mode", IMG_FIT_NORMAL, 0, IMG_FIT_NUM -1 ); dir_select_favorite = cf.get_option_str( "dir_select_favorite", "" ); #ifdef _DEBUG std::cout << "x=" << x_win_main << std::endl << "y=" << y_win_main << std::endl << "w=" << width_win_main << std::endl << "h=" << height_win_main << std::endl << "max=" << maximized_win_main << std::endl << "toolbar_pos=" << win_toolbar_pos << std::endl << "sidebar=" << win_show_sidebar << std::endl << "menubar=" << win_show_menubar << std::endl << "focused_admin=" << win_focused_admin << std::endl << "focused_admin_sidebar=" << win_focused_admin_sidebar << std::endl << "hpane=" << win_hpane_main_pos << std::endl << "vpane=" << win_vpane_main_pos << std::endl << "hpane_r=" << win_hpane_main_r_pos << std::endl << "vpane_mes=" << win_vpane_main_mes_pos << std::endl << "notebook_main_page=" << win_notebook_main_page << std::endl << "bbslist_page=" << win_bbslist_page << std::endl << "board_page=" << win_board_page << std::endl << "article_page=" << win_article_page << std::endl << "image_page=" << win_image_page << std::endl; std::cout << "board_urls\n"; for( const std::string& url : board_urls ) if( ! url.empty() ) std::cout << url; std::cout << "article_urls\n"; for( const std::string& url : article_urls ) if( ! url.empty() ) std::cout << url; std::cout << "image_urls\n"; for( const std::string& url : image_urls ) if( ! url.empty() ) std::cout << url; std::cout << "columns\n" << board_col_mark << std::endl << board_col_id << std::endl << board_col_board << std::endl << board_col_subject << std::endl << board_col_number << std::endl << board_col_load << std::endl << board_col_new << std::endl << board_col_since << std::endl << board_col_write << std::endl << board_col_speed << std::endl << board_col_diff << std::endl << "embedded_mes = " << embedded_mes << std::endl << "close_mes = " << close_mes << std::endl << "wx=" << x_win_mes << std::endl << "wy=" << y_win_mes << std::endl << "ww=" << width_win_mes << std::endl << "wh=" << height_win_mes << std::endl; #endif } // セッション情報保存 void SESSION::save_session() { // 開いているタブのURL std::string str_board_urls; std::string str_article_urls; std::string str_image_urls; for( const std::string& url : board_urls ) { if( ! url.empty() ) str_board_urls.append( " \"" + url + "\"" ); } for( const std::string& url : article_urls ) { if( ! url.empty() ) str_article_urls.append( " \"" + url + "\"" ); } for( const std::string& url : image_urls ) { if( ! url.empty() ) str_image_urls.append( " \"" + url + "\"" ); } // 開いているタブのロック状態 std::string str_board_locked; std::string str_article_locked; std::string str_image_locked; for( const bool lock : board_locked ) { str_board_locked.append( lock ? " 1" : " 0" ); } for( const bool lock : article_locked ) { str_article_locked.append( lock ? " 1" : " 0" ); } for( const bool lock : image_locked ) { str_image_locked.append( lock ? " 1" : " 0" ); } // タブの切り替え履歴 std::string str_board_switchhistory; std::string str_article_switchhistory; for( const std::string& url : board_switchhistory ) { if( ! url.empty() ) str_board_switchhistory.append( " \"" + url + "\"" ); } for( const std::string& url : article_switchhistory ) { if( ! url.empty() ) str_article_switchhistory.append( " \"" + url + "\"" ); } // 保存内容作成 std::ostringstream oss; oss << "mode_pane = " << mode_pane << std::endl << "mode_online = " << mode_online << std::endl << "mode_login2ch = " << mode_login2ch << std::endl << "mode_loginbe = " << mode_loginbe << std::endl << "x = " << x_win_main << std::endl << "y = " << y_win_main << std::endl << "width = " << width_win_main << std::endl << "height = " << height_win_main << std::endl << "maximized = " << maximized_win_main << std::endl << "full_win_main = " << full_win_main << std::endl << "show_main_toolbar = " << show_main_toolbar << std::endl << "toolbar_pos = " << win_toolbar_pos << std::endl << "show_bbslist_toolbar = " << show_bbslist_toolbar << std::endl << "show_board_toolbar = " << show_board_toolbar << std::endl << "show_article_toolbar = " << show_article_toolbar << std::endl << "show_board_tab = " << show_board_tab << std::endl << "show_article_tab = " << show_article_tab << std::endl << "show_sidebar = " << win_show_sidebar << std::endl << "show_menubar = " << win_show_menubar << std::endl << "show_main_statbar = " << show_main_statbar << std::endl << "focused_admin = " << win_focused_admin << std::endl << "focused_admin_sidebar = " << win_focused_admin_sidebar << std::endl << "hpane_main_pos = " << win_hpane_main_pos << std::endl << "vpane_main_pos = " << win_vpane_main_pos << std::endl << "hpane_main_r_pos = " << win_hpane_main_r_pos << std::endl << "vpane_main_mes_pos = " << win_vpane_main_mes_pos << std::endl << "notebook_main_page = " << win_notebook_main_page << std::endl << "bbslist_page = " << win_bbslist_page << std::endl << "board_page = " << win_board_page << std::endl << "board_urls = " << str_board_urls << std::endl << "board_locked = " << str_board_locked << std::endl << "board_switchhistory = " << str_board_switchhistory << std::endl << "article_page = " << win_article_page << std::endl << "article_urls = " << str_article_urls << std::endl << "article_locked = " << str_article_locked << std::endl << "article_switchhistory = " << str_article_switchhistory << std::endl << "image_page = " << win_image_page << std::endl << "image_urls = " << str_image_urls << std::endl << "image_locked = " << str_image_locked << std::endl << "items_sidebar = " << items_sidebar_toolbar_str << std::endl << "items_main_toolbar = " << items_main_toolbar_str << std::endl << "items_article_toolbar = " << items_article_toolbar_str << std::endl << "items_search_toolbar = " << items_search_toolbar_str << std::endl << "items_board_toolbar = " << items_board_toolbar_str << std::endl << "items_msg_toolbar = " << items_msg_toolbar_str << std::endl << "items_board = " << items_board_col_str << std::endl << "items_board_menu = " << items_board_menu_str << std::endl << "items_article_menu = " << items_article_menu_str << std::endl << "col_mark = " << board_col_mark << std::endl << "col_id = " << board_col_id << std::endl << "col_board = " << board_col_board << std::endl << "col_subject = " << board_col_subject << std::endl << "col_number = " << board_col_number << std::endl << "col_load = " << board_col_load << std::endl << "col_new = " << board_col_new << std::endl << "col_since = " << board_col_since << std::endl << "col_write = " << board_col_write << std::endl << "col_access = " << board_col_access << std::endl << "col_speed = " << board_col_speed << std::endl << "col_diff = " << board_col_diff << std::endl << "col_since_time = " << board_col_since_time << std::endl << "col_write_time = " << board_col_write_time << std::endl << "col_access_time = " << board_col_access_time << std::endl << "embedded_img = " << embedded_img << std::endl << "x_win_img = " << x_win_img << std::endl << "y_win_img = " << y_win_img << std::endl << "width_win_img = " << width_win_img << std::endl << "height_win_img = " << height_win_img << std::endl << "embedded_mes = " << embedded_mes << std::endl << "close_mes = " << close_mes << std::endl << "x_win_mes = " << x_win_mes << std::endl << "y_win_mes = " << y_win_mes << std::endl << "width_win_mes = " << width_win_mes << std::endl << "height_win_mes = " << height_win_mes << std::endl << "dir_dat = " << dir_dat << std::endl << "img_dir_img_save = " << img_dir_img_save << std::endl << "dir_draft = " << dir_draft << std::endl << "img_fit_mode = " << img_fit_mode << std::endl << "dir_select_favorite = " << dir_select_favorite << std::endl; CACHE::save_rawdata( CACHE::path_session(), oss.str() ); #ifdef _DEBUG std::cout << "SESSION::save_session\n" << oss.str() << std::endl; #endif } // ブート中 bool SESSION::is_booting(){ return booting; } void SESSION::set_booting( const bool boot ){ booting = boot; } // 終了中 bool SESSION::is_quitting(){ return quitting; } void SESSION::set_quitting( const bool quit ){ quitting = quit; } // 入れ替えなどのタブ操作中 // ビューの再描画などを禁止する bool SESSION::is_tab_operating( const std::string& url_admin ) { if( url_admin == URL_ARTICLEADMIN ) return tab_operating_article; if( url_admin == URL_IMAGEADMIN ) return tab_operating_image; return false; } void SESSION::set_tab_operating( const std::string& url_admin, const bool operating ) { if( url_admin == URL_ARTICLEADMIN ) tab_operating_article = operating; if( url_admin == URL_IMAGEADMIN ) tab_operating_image = operating; } int SESSION::get_mode_pane() { return mode_pane; } void SESSION::set_mode_pane( const int mode ){ mode_pane = mode; } bool SESSION::is_online(){ return mode_online; } void SESSION::set_online( const bool mode ){ mode_online = mode; } bool SESSION::login2ch(){ return mode_login2ch; } void SESSION::set_login2ch( const bool login ){ mode_login2ch = login; } bool SESSION::loginbe(){ return mode_loginbe; } void SESSION::set_loginbe( const bool login ){ mode_loginbe = login; } bool SESSION::show_sidebar(){ return win_show_sidebar; } bool SESSION::show_menubar(){ return win_show_menubar; } void SESSION::set_show_menubar( const bool show ){ win_show_menubar = show; } bool SESSION::get_show_main_toolbar(){ return show_main_toolbar; } void SESSION::set_show_main_toolbar( const bool show ){ show_main_toolbar = show; } int SESSION::get_toolbar_pos(){ return win_toolbar_pos; } void SESSION::set_toolbar_pos( const int pos ){ win_toolbar_pos = pos; } bool SESSION::get_show_bbslist_toolbar(){ return show_bbslist_toolbar; } void SESSION::set_show_bbslist_toolbar( const bool show ){ show_bbslist_toolbar = show; } bool SESSION::get_show_board_toolbar(){ return show_board_toolbar; } void SESSION::set_show_board_toolbar( const bool show ){ show_board_toolbar = show; } bool SESSION::get_show_article_toolbar(){ return show_article_toolbar; } void SESSION::set_show_article_toolbar( const bool show ){ show_article_toolbar = show; } bool SESSION::get_show_board_tab(){ return show_board_tab; } void SESSION::set_show_board_tab( const bool show ){ show_board_tab = show; } bool SESSION::get_show_article_tab(){ return show_article_tab; } void SESSION::set_show_article_tab( const bool show ){ show_article_tab = show; } bool SESSION::get_show_main_statbar(){ return show_main_statbar; } void SESSION::set_show_main_statbar( const bool show ){ show_main_statbar = show; } int SESSION::focused_admin(){ return win_focused_admin; } void SESSION::set_focused_admin( const int admin ){ win_focused_admin = admin; } int SESSION::focused_admin_sidebar(){ return win_focused_admin_sidebar; } void SESSION::set_focused_admin_sidebar( const int admin ){ win_focused_admin_sidebar = admin; } // 各windowの座標 int SESSION::get_x_win_main(){ return x_win_main; } int SESSION::get_y_win_main(){ return y_win_main; } void SESSION::set_x_win_main( const int x ){ x_win_main = x; } void SESSION::set_y_win_main( const int y ){ y_win_main = y; } int SESSION::get_x_win_img(){ return x_win_img; } int SESSION::get_y_win_img(){ return y_win_img; } void SESSION::set_x_win_img( const int x ){ x_win_img = x; } void SESSION::set_y_win_img( const int y ){ y_win_img = y; } int SESSION::get_x_win_mes(){ return x_win_mes; } int SESSION::get_y_win_mes(){ return y_win_mes; } void SESSION::set_x_win_mes( const int x ){ x_win_mes = x; } void SESSION::set_y_win_mes( const int y ){ y_win_mes = y; } // 各windowのサイズ int SESSION::get_width_win_main(){ return width_win_main; } int SESSION::get_height_win_main(){ return height_win_main; } void SESSION::set_width_win_main( const int width ){ width_win_main = width; } void SESSION::set_height_win_main( const int height ){ height_win_main = height; } int SESSION::get_width_win_img(){ return width_win_img; } int SESSION::get_height_win_img(){ return height_win_img; } void SESSION::set_width_win_img( const int width ){ width_win_img = width; } void SESSION::set_height_win_img( const int height ){ height_win_img = height; } int SESSION::get_width_win_mes(){ return width_win_mes; } int SESSION::get_height_win_mes(){ return height_win_mes; } void SESSION::set_width_win_mes( const int width ){ width_win_mes = width; } void SESSION::set_height_win_mes( const int height ){ height_win_mes = height; } // 各window がフォーカスされているか bool SESSION::is_focus_win_main(){ return focus_win_main; } void SESSION::set_focus_win_main( const bool set ){ focus_win_main = set; } bool SESSION::is_focus_win_img(){ return focus_win_img; } void SESSION::set_focus_win_img( const bool set ){ focus_win_img = set; } bool SESSION::is_focus_win_mes(){ return focus_win_mes; } void SESSION::set_focus_win_mes( const bool set ){ focus_win_mes = set; } // 各window が最大化されているか bool SESSION::is_maximized_win_main(){ return maximized_win_main; } void SESSION::set_maximized_win_main( const bool set ){ maximized_win_main = set; } bool SESSION::is_maximized_win_img(){ return maximized_win_img; } void SESSION::set_maximized_win_img( const bool set ){ maximized_win_img = set; } bool SESSION::is_maximized_win_mes(){ return maximized_win_mes; } void SESSION::set_maximized_win_mes( const bool set ){ maximized_win_mes = set; } // 各window が最小化されているか bool SESSION::is_iconified_win_main(){ return iconified_win_main; } void SESSION::set_iconified_win_main( const bool set ){ iconified_win_main = set; } bool SESSION::is_iconified_win_img(){ return iconified_win_img; } void SESSION::set_iconified_win_img( const bool set ){ iconified_win_img = set; } bool SESSION::is_iconified_win_mes(){ return iconified_win_mes; } void SESSION::set_iconified_win_mes( const bool set ){ iconified_win_mes = set; } // 各window が画面に表示されているか bool SESSION::is_shown_win_main(){ return shown_win_main; } void SESSION::set_shown_win_main( const bool set ){ shown_win_main = set; } bool SESSION::is_shown_win_img(){ return shown_win_img; } void SESSION::set_shown_win_img( const bool set ){ shown_win_img = set; } bool SESSION::is_shown_win_mes(){ return shown_win_mes; } void SESSION::set_shown_win_mes( const bool set ){ shown_win_mes = set; } // windowがフルスクリーンか bool SESSION::is_full_win_main(){ return full_win_main; } void SESSION::set_full_win_main( const bool set ){ full_win_main = set; } // ダイアログ表示中 bool SESSION::is_dialog_shown(){ return dialog_shown; } void SESSION::set_dialog_shown( const bool set ){ dialog_shown = set; } void SESSION::set_show_sidebar( const bool showurl ){ win_show_sidebar = showurl; } // メインウィンドウのペインの敷居の位置 int SESSION::hpane_main_pos(){ return win_hpane_main_pos; } int SESSION::vpane_main_pos(){ return win_vpane_main_pos; } int SESSION::hpane_main_r_pos(){ return win_hpane_main_r_pos; } int SESSION::vpane_main_mes_pos(){ return win_vpane_main_mes_pos; } void SESSION::set_hpane_main_pos( const int pos ){ win_hpane_main_pos = pos; } void SESSION::set_vpane_main_pos( const int pos ){ win_vpane_main_pos = pos; } void SESSION::set_hpane_main_r_pos( const int pos ){ win_hpane_main_r_pos = pos; } void SESSION::set_vpane_main_mes_pos( const int pos ){ win_vpane_main_mes_pos = pos; } // メインnotebookのページ番号 int SESSION::notebook_main_page(){ return win_notebook_main_page; } void SESSION::set_notebook_main_page( const int page ){ win_notebook_main_page = page; } // bbslistの開いてるページ番号 int SESSION::bbslist_page(){ return win_bbslist_page; } void SESSION::set_bbslist_page( const int page ){ win_bbslist_page = page; } // 前回閉じたときに開いていたboardのページ番号とURL int SESSION::board_page(){ return win_board_page; } void SESSION::set_board_page( const int page ){ win_board_page = page; } const std::list< std::string >& SESSION::get_board_URLs(){ return board_urls; } void SESSION::set_board_URLs( const std::list< std::string >& urls ){ board_urls = urls; } // スレ一覧のロック状態 const std::list< bool >& SESSION::get_board_locked(){ return board_locked; } void SESSION::set_board_locked( const std::list< bool >& locked ){ board_locked = locked; } // スレ一覧の切り替え履歴 const std::list< std::string >& SESSION::get_board_switchhistory(){ return board_switchhistory; } void SESSION::set_board_switchhistory( const std::list< std::string >& hist ){ board_switchhistory = hist; } // 前回閉じたときに開いていたスレタブのページ番号とURL int SESSION::article_page(){ return win_article_page; } void SESSION::set_article_page( const int page ){ win_article_page = page; } const std::list< std::string >& SESSION::get_article_URLs(){ return article_urls; } void SESSION::set_article_URLs( const std::list< std::string >& urls ){ article_urls = urls; } // スレタブのロック状態 const std::list< bool >& SESSION::get_article_locked(){ return article_locked; } void SESSION::set_article_locked( const std::list< bool >& locked ){ article_locked = locked; } // スレタブの切り替え履歴 const std::list< std::string >& SESSION::get_article_switchhistory(){ return article_switchhistory; } void SESSION::set_article_switchhistory( const std::list< std::string >& hist ){ article_switchhistory = hist; } // 前回閉じたときに開いていたimageのページ番号とURL int SESSION::image_page(){ return win_image_page; } void SESSION::set_image_page( const int page ){ win_image_page = page; } const std::list< std::string >& SESSION::image_URLs(){ return image_urls; } void SESSION::set_image_URLs( const std::list< std::string >& urls ){ image_urls = urls; } // 画像タブのロック状態 const std::list< bool >& SESSION::get_image_locked(){ return image_locked; } void SESSION::set_image_locked( const std::list< bool >& locked ){ image_locked = locked; } // サイドバーのツールバーの項目 const std::string& SESSION::get_items_sidebar_toolbar_str(){ return items_sidebar_toolbar_str; } std::string SESSION::get_items_sidebar_toolbar_default_str() { return ITEM_NAME_SEARCHBOX + std::string ( " " ) + ITEM_NAME_SEARCH_NEXT + std::string ( " " ) + ITEM_NAME_SEARCH_PREV; } void SESSION::set_items_sidebar_toolbar_str( const std::string& items_str ) { items_sidebar_toolbar_str = items_str; items_sidebar_toolbar = parse_items( items_sidebar_toolbar_str ); } int SESSION::get_item_sidebar_toolbar( const int num ){ return items_sidebar_toolbar[ num ]; } // メインツールバーの項目 const std::string& SESSION::get_items_main_toolbar_str(){ return items_main_toolbar_str; } std::string SESSION::get_items_main_toolbar_default_str() { return ITEM_NAME_BBSLISTVIEW + std::string ( " " ) + ITEM_NAME_FAVORITEVIEW + std::string ( " " ) + ITEM_NAME_BOARDVIEW + std::string ( " " ) + ITEM_NAME_ARTICLEVIEW + std::string ( " " ) + ITEM_NAME_IMAGEVIEW + std::string ( " " ) + ITEM_NAME_SEPARATOR + std::string ( " " ) + ITEM_NAME_URL + std::string ( " " ) + ITEM_NAME_GO; } void SESSION::set_items_main_toolbar_str( const std::string& items_str ) { items_main_toolbar_str = items_str; items_main_toolbar = parse_items( items_main_toolbar_str ); } int SESSION::get_item_main_toolbar( const int num ){ return items_main_toolbar[ num ]; } // スレビューのツールバーの項目 const std::string& SESSION::get_items_article_toolbar_str(){ return items_article_toolbar_str; } std::string SESSION::get_items_article_toolbar_default_str() { return ITEM_NAME_WRITEMSG + std::string ( " " ) + ITEM_NAME_OPENBOARD + std::string ( " " ) + ITEM_NAME_NAME + std::string ( " " ) + ITEM_NAME_SEARCH + std::string ( " " ) + ITEM_NAME_RELOAD + std::string ( " " ) + ITEM_NAME_STOPLOADING + std::string ( " " ) + ITEM_NAME_APPENDFAVORITE + std::string ( " " ) + ITEM_NAME_DELETE + std::string ( " " ) + ITEM_NAME_QUIT; } void SESSION::set_items_article_toolbar_str( const std::string& items_str ) { items_article_toolbar_str = items_str; items_article_toolbar = parse_items( items_article_toolbar_str ); } int SESSION::get_item_article_toolbar( const int num ){ return items_article_toolbar[ num ]; } // 検索ビューのツールバーの項目 const std::string& SESSION::get_items_search_toolbar_str(){ return items_search_toolbar_str; } std::string SESSION::get_items_search_toolbar_default_str() { return ITEM_NAME_NAME + std::string ( " " ) + ITEM_NAME_SEARCH + std::string ( " " ) + ITEM_NAME_RELOAD + std::string ( " " ) + ITEM_NAME_STOPLOADING + std::string ( " " ) + ITEM_NAME_QUIT; } void SESSION::set_items_search_toolbar_str( const std::string& items_str ) { items_search_toolbar_str = items_str; items_search_toolbar = parse_items( items_search_toolbar_str ); } int SESSION::get_item_search_toolbar( const int num ){ return items_search_toolbar[ num ]; } // スレ一覧のツールバー項目 const std::string& SESSION::get_items_board_toolbar_str(){ return items_board_toolbar_str; } std::string SESSION::get_items_board_toolbar_default_str() { return ITEM_NAME_NEWARTICLE + std::string ( " " ) + ITEM_NAME_SEARCHBOX + std::string ( " " ) + ITEM_NAME_SEARCH_NEXT + std::string ( " " ) + ITEM_NAME_SEARCH_PREV + std::string ( " " ) + ITEM_NAME_RELOAD + std::string ( " " ) + ITEM_NAME_STOPLOADING + std::string ( " " ) + ITEM_NAME_APPENDFAVORITE + std::string ( " " ) + ITEM_NAME_DELETE + std::string ( " " ) + ITEM_NAME_QUIT; } void SESSION::set_items_board_toolbar_str( const std::string& items_str ) { items_board_toolbar_str = items_str; items_board_toolbar = parse_items( items_board_toolbar_str ); } int SESSION::get_item_board_toolbar( const int num ){ return items_board_toolbar[ num ]; } // 書き込みビューのツールバー項目 const std::string& SESSION::get_items_msg_toolbar_str(){ return items_msg_toolbar_str; } std::string SESSION::get_items_msg_toolbar_default_str() { return ITEM_NAME_PREVIEW + std::string ( " " ) + ITEM_NAME_WRITEMSG+ std::string ( " " ) + ITEM_NAME_NAME + std::string ( " " ) + ITEM_NAME_UNDO + std::string ( " " ) + ITEM_NAME_INSERTTEXT + std::string ( " " ) + ITEM_NAME_LOCK_MESSAGE + std::string ( " " ) + ITEM_NAME_QUIT; } void SESSION::set_items_msg_toolbar_str( const std::string& items_str ) { items_msg_toolbar_str = items_str; items_msg_toolbar = parse_items( items_msg_toolbar_str ); } int SESSION::get_item_msg_toolbar( const int num ){ return items_msg_toolbar[ num ]; } // スレ一覧の列項目 const std::string& SESSION::get_items_board_col_str(){ return items_board_col_str; } std::string SESSION::get_items_board_col_default_str() { return ITEM_NAME_MARK + std::string ( " " ) + ITEM_NAME_ID + std::string ( " " ) + ITEM_NAME_NAME + std::string ( " " ) + ITEM_NAME_RES + std::string ( " " ) + ITEM_NAME_LOAD + std::string ( " " ) + ITEM_NAME_NEW + std::string ( " " ) + ITEM_NAME_SINCE + std::string ( " " ) + ITEM_NAME_LASTWRITE + std::string ( " " ) + ITEM_NAME_SPEED; } void SESSION::set_items_board_col_str( const std::string& items_str ) { items_board_col_str = items_str; items_board_col = parse_items( items_board_col_str ); } int SESSION::get_item_board_col( const int num ){ return items_board_col[ num ]; } // スレ一覧のコンテキストメニュー項目 const std::string& SESSION::get_items_board_menu_str(){ return items_board_menu_str; } std::string SESSION::get_items_board_menu_default_str() { return ITEM_NAME_BOOKMARK + std::string ( " " ) + ITEM_NAME_SEPARATOR + std::string ( " " ) + ITEM_NAME_OPENARTICLETAB + std::string ( " " ) + ITEM_NAME_OPEN_BROWSER + std::string ( " " ) + ITEM_NAME_SEPARATOR + std::string ( " " ) + ITEM_NAME_COPY_URL + std::string ( " " ) + ITEM_NAME_COPY_TITLE_URL_THREAD + std::string ( " " ) + ITEM_NAME_SEPARATOR + std::string ( " " ) + ITEM_NAME_ABONE_ARTICLE + std::string ( " " ) + ITEM_NAME_SEPARATOR + std::string ( " " ) + ITEM_NAME_DELETE + std::string ( " " ) + ITEM_NAME_SEPARATOR + std::string ( " " ) + ITEM_NAME_ETC + std::string ( " " ) + ITEM_NAME_SEPARATOR + std::string ( " " ) + ITEM_NAME_PREF_THREAD + std::string ( " " ) + ITEM_NAME_PREF_BOARD; } void SESSION::set_items_board_menu_str( const std::string& items_str ) { items_board_menu_str = items_str; items_board_menu = parse_items( items_board_menu_str ); } int SESSION::get_item_board_menu( const int num ){ return items_board_menu[ num ]; } // スレビューのコンテキストメニュー項目 const std::string& SESSION::get_items_article_menu_str(){ return items_article_menu_str; } std::string SESSION::get_items_article_menu_default_str() { return ITEM_NAME_DRAWOUT + std::string ( " " ) + ITEM_NAME_GO + std::string ( " " ) + ITEM_NAME_SEARCH + std::string ( " " ) + ITEM_NAME_NGWORD + std::string ( " " ) + ITEM_NAME_SEPARATOR + std::string ( " " ) + ITEM_NAME_QUOTE_SELECTION + std::string ( " " ) + ITEM_NAME_SEPARATOR + std::string ( " " ) + ITEM_NAME_OPEN_BROWSER + std::string ( " " ) + ITEM_NAME_USER_COMMAND + std::string ( " " ) + ITEM_NAME_SEPARATOR + std::string ( " " ) + ITEM_NAME_COPY_URL + std::string ( " " ) + ITEM_NAME_COPY + std::string ( " " ) + ITEM_NAME_SEPARATOR + std::string ( " " ) + ITEM_NAME_ETC + std::string ( " " ) + ITEM_NAME_SEPARATOR + std::string ( " " ) + ITEM_NAME_PREF_THREAD; } void SESSION::set_items_article_menu_str( const std::string& items_str ) { items_article_menu_str = items_str; items_article_menu = parse_items( items_article_menu_str ); } int SESSION::get_item_article_menu( const int num ){ return items_article_menu[ num ]; } // board ビューの列幅 int SESSION::col_mark(){ return board_col_mark; } int SESSION::col_id(){ return board_col_id; } int SESSION::col_board(){ return board_col_board; } int SESSION::col_subject(){ return board_col_subject; } int SESSION::col_number(){ return board_col_number; } int SESSION::col_load(){ return board_col_load; } int SESSION::col_new(){ return board_col_new; } int SESSION::col_since(){ return board_col_since; } int SESSION::col_write(){ return board_col_write; } int SESSION::col_access(){ return board_col_access; } int SESSION::col_speed(){ return board_col_speed; } int SESSION::col_diff(){ return board_col_diff; } // スレ一覧の since の表示モード int SESSION::get_col_since_time() { return board_col_since_time; } void SESSION::set_col_since_time( const int mode ){ board_col_since_time = mode; } // スレ一覧の 最終書込 の表示モード int SESSION::get_col_write_time() { return board_col_write_time; } void SESSION::set_col_write_time( const int mode ){ board_col_write_time = mode; } // スレ一覧の 最終取得 の表示モード int SESSION::get_col_access_time() { return board_col_access_time; } void SESSION::set_col_access_time( const int mode ){ board_col_access_time = mode; } // 現在開いているサイドバーのページ int SESSION::get_sidebar_current_page() { return BBSLIST::get_admin()->get_current_page(); } // 現在開いているサイドバーのurl std::string SESSION::get_sidebar_current_url() { return BBSLIST::get_admin()->get_current_url(); } void SESSION::set_col_mark( const int width ){ board_col_mark = width; } void SESSION::set_col_id( const int width ){ board_col_id = width; } void SESSION::set_col_board( const int width ){ board_col_board = width; } void SESSION::set_col_subject( const int width ){ board_col_subject = width; } void SESSION::set_col_number( const int width ){ board_col_number = width; } void SESSION::set_col_load( const int width ){ board_col_load = width; } void SESSION::set_col_new( const int width ){ board_col_new = width; } void SESSION::set_col_since( const int width ){ board_col_since = width; } void SESSION::set_col_write( const int width ){ board_col_write = width; } void SESSION::set_col_access( const int width ){ board_col_access = width; } void SESSION::set_col_speed( const int width ){ board_col_speed = width; } void SESSION::set_col_diff( const int width ){ board_col_diff = width; } // 現在開いているarticle の ARTICLE::DrawAreaBase ARTICLE::DrawAreaBase* SESSION::get_base_drawarea() { ARTICLE::ArticleViewBase* base_view = nullptr; base_view = dynamic_cast< ARTICLE::ArticleViewBase* >( ARTICLE::get_admin()->get_current_view() ); if( base_view == nullptr ) return nullptr; ARTICLE::DrawAreaBase* base_drawarea = nullptr; base_drawarea = base_view->drawarea(); return base_drawarea; } // 現在開いているarticle のurl std::string SESSION::get_article_current_url() { return ARTICLE::get_admin()->get_current_url(); } // 埋め込みimage使用 bool SESSION::get_embedded_img(){ return embedded_img; } void SESSION::set_embedded_img( const bool set ){ embedded_img = set; } // 埋め込みmessageを使用 bool SESSION::get_embedded_mes(){ return embedded_mes; } void SESSION::set_embedded_mes( const bool set ){ embedded_mes = set; } // 書き込み後にmessageを閉じる bool SESSION::get_close_mes(){ return close_mes; } void SESSION::set_close_mes( const bool set ){ close_mes = set; } // 最後にdatを読み書きしたディレクトリ const std::string& SESSION::get_dir_dat(){ return dir_dat; } void SESSION::set_dir_dat( const std::string& dir ){ dir_dat = dir; } // 最後に画像を保存したディレクトリ const std::string& SESSION::dir_img_save(){ return img_dir_img_save; } void SESSION::set_dir_img_save( const std::string& dir ){ img_dir_img_save = dir; } // 下書きファイルのディレクトリ const std::string& SESSION::get_dir_draft(){ return dir_draft; } void SESSION::set_dir_draft( const std::string& dir ){ dir_draft = dir; } // ポップアップメニュー表示中 bool SESSION::is_popupmenu_shown(){ return popupmenu_shown; } void SESSION::set_popupmenu_shown( const bool shown ){ popupmenu_shown = shown; } // JD終了時に削除するスレのリスト // 実況などしたスレは削除する。 Core::~Core()を参照 const std::vector< std::string >& SESSION::get_delete_list() { return delete_list; } void SESSION::append_delete_list( const std::string& url ) { if( std::find( delete_list.cbegin(), delete_list.cend(), url ) != delete_list.cend() ) { return; } delete_list.push_back( url ); #ifdef _DEBUG std::cout << "SESSION::append_delete_list urls == " << delete_list.size() << " url = " << url << std::endl; #endif } void SESSION::remove_delete_list( const std::string& url ) { if( delete_list.empty() ) return; const auto it = std::find( delete_list.cbegin(), delete_list.cend(), url ); if( it != delete_list.cend() ) delete_list.erase( it ); #ifdef _DEBUG std::cout << "SESSION::remove_delete_list urls == " << delete_list.size() << " url = " << url << std::endl; #endif } // 実況実行中のスレか bool SESSION::is_live( const std::string& url ) { #ifdef _DEBUG std::cout << "SESSION::is_live live_urls == " << live_urls.size() << " url = " << url << std::endl; #endif if( live_urls.empty() ) return false; const auto it = std::find( live_urls.cbegin(), live_urls.cend(), url ); return it != live_urls.cend(); } void SESSION::append_live( const std::string& url ) { if( ! is_live( url ) ) live_urls.push_back( url ); #ifdef _DEBUG std::cout << "SESSION::append_live live_urls == " << live_urls.size() << " url = " << url << std::endl; #endif } void SESSION::remove_live( const std::string& url ) { if( live_urls.empty() ) return; const auto it = std::find( live_urls.cbegin(), live_urls.cend(), url ); if( it != live_urls.cend() ) live_urls.erase( it ); #ifdef _DEBUG std::cout << "SESSION::remove_live live_urls == " << live_urls.size() << " url = " << url << std::endl; #endif } // 画像のfitモード int SESSION::get_img_fit_mode() { return img_fit_mode; } void SESSION::toggle_img_fit_mode() { if( img_fit_mode == IMG_FIT_NORMAL ) img_fit_mode = IMG_FIT_WIDTH; else img_fit_mode = IMG_FIT_NORMAL; } // お気に入り挿入ダイアログで最後に保存したディレクトリ名 const std::string& SESSION::get_dir_select_favorite() { return dir_select_favorite; } void SESSION::set_dir_select_favorite( const std::string& dir ) { dir_select_favorite = dir; } // 各履歴を取得 void SESSION::get_history( const std::string& url, CORE::DATA_INFO_LIST& info_list ) { return BBSLIST::get_admin()->get_history( url, info_list ); } // サイドバーの指定したidのディレクトリに含まれるスレのアドレスを取得 void SESSION::get_sidebar_threads( const std::string& url, const int dirid, std::vector< std::string >& list_url ) { BBSLIST::get_admin()->get_threads( url, dirid, list_url ); } // サイドバーの指定したidのディレクトリの名前を取得 std::string SESSION::get_sidebar_dirname( const std::string& url, const int dirid ) { return BBSLIST::get_admin()->get_dirname( url, dirid ); } jdim-0.10.1/src/session.h000066400000000000000000000355651445721505100151610ustar00rootroot00000000000000// ライセンス: GPL2 // // 座標などのウィンドウ情報とかのセッション情報 // #ifndef _SESSION_H #define _SESSION_H #include "type.h" #include "data_info.h" #include #include #include namespace ARTICLE { class DrawAreaBase; } namespace SESSION { // focused_admin の値。どこにフォーカスしているか // Core::slot_focus_in_event, Core::slot_focus_out_event などを参照 enum { FOCUS_SIDEBAR = 0, FOCUS_BOARD, FOCUS_ARTICLE, FOCUS_IMAGE, FOCUS_MESSAGE, FOCUS_NOT, // どこもフォーカスされていない FOCUS_NUM }; // ペーンモード enum { MODE_2PANE = 0, MODE_3PANE, MODE_V3PANE, MODE_PANE_NUM }; // メインウィンドウの右ペーンに表示中のnotebook enum { PAGE_ARTICLE = 0, PAGE_IMAGE, PAGE_BOARD, PAGE_NUM }; // メインツールバーの位置 enum { TOOLBAR_POS_NORMAL = 0, // メニューバーの下に表示 TOOLBAR_POS_RIGHT, // サイドバーの右に表示 TOOLBAR_POS_NUM }; // 画像のfitモード enum { IMG_FIT_NORMAL = 0, // 縦と横で小さい方をウィンドウに合わせる IMG_FIT_WIDTH, // 常に横をウィンドウに合わせる IMG_FIT_NUM }; void init_session(); void save_session(); // ブート中 bool is_booting(); void set_booting( const bool boot ); // 終了中 bool is_quitting(); void set_quitting( const bool quit ); // 入れ替えなどのタブ操作中 // ビューの再描画などを禁止する bool is_tab_operating( const std::string& url_admin ); void set_tab_operating( const std::string& url_admin, const bool operating ); int get_mode_pane(); void set_mode_pane( const int mode ); bool is_online(); void set_online( const bool mode ); // 2chログイン中 bool login2ch(); void set_login2ch( const bool login ); // BEログイン中 bool loginbe(); void set_loginbe( const bool login ); bool loginp2() = delete; // Removed in v0.3.0 (2020-05) void set_loginp2( const bool login ) = delete; // Removed in v0.3.0 (2020-05) // サイドバー表示中 bool show_sidebar(); void set_show_sidebar( const bool showbar ); // メニューバー bool show_menubar(); void set_show_menubar( const bool show ); // メインツールバー表示 bool get_show_main_toolbar(); void set_show_main_toolbar( const bool show ); // メインツールバー位置 int get_toolbar_pos(); void set_toolbar_pos( const int pos ); // 板一覧のツールバー表示 bool get_show_bbslist_toolbar(); void set_show_bbslist_toolbar( const bool show ); // スレ一覧のツールバー表示 bool get_show_board_toolbar(); void set_show_board_toolbar( const bool show ); // スレビューのツールバー表示 bool get_show_article_toolbar(); void set_show_article_toolbar( const bool show ); // スレ一覧のタブ表示 bool get_show_board_tab(); void set_show_board_tab( const bool show ); // スレビューのタブ bool get_show_article_tab(); void set_show_article_tab( const bool show ); // メインステータスバー表示 bool get_show_main_statbar(); void set_show_main_statbar( const bool show ); // フォーカスされているadmin int focused_admin(); void set_focused_admin( const int admin ); // 各windowの座標 int get_x_win_main(); // メインウィンドウ int get_y_win_main(); void set_x_win_main( const int x ); void set_y_win_main( const int y ); int get_x_win_img(); // 画像ウィンドウ int get_y_win_img(); void set_x_win_img( const int x ); void set_y_win_img( const int y ); int get_x_win_mes(); // 書き込みウィンドウ int get_y_win_mes(); void set_x_win_mes( const int x ); void set_y_win_mes( const int y ); // 各windowのサイズ int get_width_win_main(); // メインウィンドウ int get_height_win_main(); void set_width_win_main( const int width ); void set_height_win_main( const int height ); int get_width_win_img(); // 画像ウィンドウ int get_height_win_img(); void set_width_win_img( const int width ); void set_height_win_img( const int height ); int get_width_win_mes(); // 書き込みウィンドウ int get_height_win_mes(); void set_width_win_mes( const int width ); void set_height_win_mes( const int height ); // 各window がフォーカスされているか bool is_focus_win_main(); // メインウィンドウ void set_focus_win_main( const bool set ); bool is_focus_win_img(); // 画像ウィンドウ void set_focus_win_img( const bool set ); bool is_focus_win_mes(); // 書き込みウィンドウ void set_focus_win_mes( const bool set ); // 各window が最大化されているか bool is_maximized_win_main(); // メインウィンドウ void set_maximized_win_main( const bool maximized ); bool is_maximized_win_img(); // 画像ウィンドウ void set_maximized_win_img( const bool set ); bool is_maximized_win_mes(); // 書き込みウィンドウ void set_maximized_win_mes( const bool maximized ); // 各window が最小化されているか bool is_iconified_win_main(); // メインウィンドウ void set_iconified_win_main( const bool set ); bool is_iconified_win_img(); // 画像ウィンドウ void set_iconified_win_img( const bool set ); bool is_iconified_win_mes(); // 書き込みウィンドウ void set_iconified_win_mes( const bool set ); // 各window が画面に表示されているか bool is_shown_win_main(); // メインウィンドウ void set_shown_win_main( const bool set ); bool is_shown_win_img(); // 画像ウィンドウ void set_shown_win_img( const bool set ); bool is_shown_win_mes(); // 書き込みウィンドウ void set_shown_win_mes( const bool set ); // windowがフルスクリーンか bool is_full_win_main(); // メインウィンドウ void set_full_win_main( const bool set ); // ダイアログ表示中 bool is_dialog_shown(); void set_dialog_shown( const bool set ); // サイドバーを閉じる前にフォーカスされていたadmin int focused_admin_sidebar(); void set_focused_admin_sidebar( const int admin ); /// メインウィンドウのペインの敷居の位置 int hpane_main_pos(); int vpane_main_pos(); int hpane_main_r_pos(); int vpane_main_mes_pos(); void set_hpane_main_pos( const int pos ); void set_vpane_main_pos( const int pos ); void set_hpane_main_r_pos( const int pos ); void set_vpane_main_mes_pos( const int pos ); // 前回閉じたときに開いていたメインnotebookのページ番号 int notebook_main_page(); void set_notebook_main_page( const int page ); // 前回閉じたときに開いていたbbslistの開いてるページ番号 int bbslist_page(); void set_bbslist_page( const int page ); // 前回閉じたときに開いていたスレ一覧のページ番号とURL int board_page(); void set_board_page( const int page ); const std::list< std::string >& get_board_URLs(); void set_board_URLs( const std::list< std::string >& urls ); // スレ一覧のロック状態 const std::list< bool >& get_board_locked(); void set_board_locked( const std::list< bool >& locked ); // スレ一覧の切り替え履歴 const std::list< std::string >& get_board_switchhistory(); void set_board_switchhistory( const std::list< std::string >& hist ); // 前回閉じたときに開いていたスレタブのページ番号とURL int article_page(); void set_article_page( const int page ); const std::list< std::string >& get_article_URLs(); void set_article_URLs( const std::list< std::string >& urls ); // スレタブのロック状態 const std::list< bool >& get_article_locked(); void set_article_locked( const std::list< bool >& locked ); // スレタブの切り替え履歴 const std::list< std::string >& get_article_switchhistory(); void set_article_switchhistory( const std::list< std::string >& hist ); // 前回閉じたときに開いていたimageのページ番号とURL int image_page(); void set_image_page( const int page ); const std::list< std::string >& image_URLs(); void set_image_URLs( const std::list< std::string >& urls ); // 画像タブのロック状態 const std::list< bool >& get_image_locked(); void set_image_locked( const std::list< bool >& locked ); // 現在開いているサイドバーのページ int get_sidebar_current_page(); // 現在開いているサイドバーのurl std::string get_sidebar_current_url(); // ツールバー等の項目名 -> ID 変換 int parse_item( const std::string& item_name ); // サイドバーのツールバー項目 const std::string& get_items_sidebar_toolbar_str(); std::string get_items_sidebar_toolbar_default_str(); void set_items_sidebar_toolbar_str( const std::string& items ); int get_item_sidebar_toolbar( const int num ); // メインツールバーの項目 const std::string& get_items_main_toolbar_str(); std::string get_items_main_toolbar_default_str(); void set_items_main_toolbar_str( const std::string& items_str ); int get_item_main_toolbar( const int num ); // スレビューのツールバーの項目 const std::string& get_items_article_toolbar_str(); std::string get_items_article_toolbar_default_str(); void set_items_article_toolbar_str( const std::string& items_str ); int get_item_article_toolbar( const int num ); // 検索ビューのツールバーの項目 const std::string& get_items_search_toolbar_str(); std::string get_items_search_toolbar_default_str(); void set_items_search_toolbar_str( const std::string& items_str ); int get_item_search_toolbar( const int num ); // スレ一覧のツールバー項目 const std::string& get_items_board_toolbar_str(); std::string get_items_board_toolbar_default_str(); void set_items_board_toolbar_str( const std::string& items ); int get_item_board_toolbar( const int num ); // 書き込みビューのツールバー項目 const std::string& get_items_msg_toolbar_str(); std::string get_items_msg_toolbar_default_str(); void set_items_msg_toolbar_str( const std::string& items ); int get_item_msg_toolbar( const int num ); // スレ一覧の列項目 const std::string& get_items_board_col_str(); std::string get_items_board_col_default_str(); void set_items_board_col_str( const std::string& items ); int get_item_board_col( const int num ); // スレ一覧のコンテキストメニュー項目 const std::string& get_items_board_menu_str(); std::string get_items_board_menu_default_str(); void set_items_board_menu_str( const std::string& items_str ); int get_item_board_menu( const int num ); // スレビューのコンテキストメニュー項目 const std::string& get_items_article_menu_str(); std::string get_items_article_menu_default_str(); void set_items_article_menu_str( const std::string& items_str ); int get_item_article_menu( const int num ); // スレ一覧の列幅 int col_mark(); int col_id(); int col_board(); int col_subject(); int col_number(); int col_load(); int col_new(); int col_since(); int col_write(); int col_access(); int col_speed(); int col_diff(); void set_col_mark( const int width ); void set_col_id( const int width ); void set_col_board( const int width ); void set_col_subject( const int width ); void set_col_number( const int width ); void set_col_load( const int width ); void set_col_new( const int width ); void set_col_since( const int width ); void set_col_write( const int width ); void set_col_access( const int width ); void set_col_speed( const int width ); void set_col_diff( const int width ); // スレ一覧の since の表示モード int get_col_since_time(); void set_col_since_time( const int mode ); // スレ一覧の 最終書込 の表示モード int get_col_write_time(); void set_col_write_time( const int mode ); // スレ一覧の 最終取得 の表示モード int get_col_access_time(); void set_col_access_time( const int mode ); // 現在開いているarticle の ARTICLE::DrawAreaBase ARTICLE::DrawAreaBase* get_base_drawarea(); // 現在開いているarticle のurl std::string get_article_current_url(); // 埋め込みimage使用 bool get_embedded_img(); void set_embedded_img( const bool set ); // 埋め込みmessageを使用 bool get_embedded_mes(); void set_embedded_mes( const bool set ); // 書き込み後にmessageを閉じる bool get_close_mes(); void set_close_mes( const bool set ); // 最後にdatを読み書きしたディレクトリ const std::string& get_dir_dat(); void set_dir_dat( const std::string& dir ); // 最後に画像を保存したディレクトリ const std::string& dir_img_save(); void set_dir_img_save( const std::string& dir ); // 下書きファイルのディレクトリ const std::string& get_dir_draft(); void set_dir_draft( const std::string& dir ); // ポップアップメニュー表示中 bool is_popupmenu_shown(); void set_popupmenu_shown( const bool shown ); // JD終了時に削除するスレのリスト const std::vector< std::string >& get_delete_list(); void append_delete_list( const std::string& url ); void remove_delete_list( const std::string& url ); // 実況実行中のスレ bool is_live( const std::string& url ); void append_live( const std::string& url ); void remove_live( const std::string& url ); // 画像のfitモード int get_img_fit_mode(); void toggle_img_fit_mode(); // お気に入り挿入ダイアログで最後に保存したディレクトリ名 const std::string& get_dir_select_favorite(); void set_dir_select_favorite( const std::string& dir ); // 各履歴を取得 void get_history( const std::string& url, CORE::DATA_INFO_LIST& info_list ); // サイドバーの指定したidのディレクトリに含まれるスレのアドレスを取得 void get_sidebar_threads( const std::string& url, const int dirid, std::vector< std::string >& list_url ); // サイドバーの指定したidのディレクトリの名前を取得 std::string get_sidebar_dirname( const std::string& url, const int dirid ); } #endif jdim-0.10.1/src/setupwizard.cpp000066400000000000000000000262651445721505100164070ustar00rootroot00000000000000// ライセンス: GPL2 //#define _DEBUG #include "jddebug.h" #include "setupwizard.h" #include "prefdiagfactory.h" #include "fontid.h" #include "session.h" #include "config/globalconf.h" #include "icons/iconmanager.h" using namespace CORE; enum { SPACING_SIZE = 8 }; PageStart::PageStart() : Gtk::Grid() , m_icon( ICON::get_icon_manager()->get_icon( ICON::JD48 ) ) , m_label( "1/5.JDim セットアップ開始", Gtk::ALIGN_START ) , m_label2( "JDimセットアップウィザードへようこそ\n\n" "このウィザードでネットワークとフォント等の設定をおこないます\n\n" "設定を始めるには[次へ]を押してください", Gtk::ALIGN_START ) { set_column_spacing( SPACING_SIZE ); set_row_spacing( SPACING_SIZE ); // Gtk::Grid::attach( child, column, row, width, height ) attach( m_icon, 0, 0, 1, 1 ); attach( m_label, 1, 0, 1, 1 ); attach( m_label2, 0, 1, 2, 1 ); m_label.set_hexpand( true ); } ///////////////////////////////////////////// PageNet::PageNet( Gtk::Window* parent ) : Gtk::Grid() , m_parent{ parent } , m_icon( ICON::get_icon_manager()->get_icon( ICON::JD48 ) ) , m_label( "2/5.ネットワークの設定をします", Gtk::ALIGN_START ) , m_proxy( "プロキシ設定(_P)", true ) , m_browser( "ブラウザ設定(_W)", true ) , m_frame( "ブラウザ起動コマンド" ) // フレーム , m_label_browser( CONFIG::get_command_openurl(), Gtk::ALIGN_START ) { m_label_browser.property_margin() = SPACING_SIZE; m_frame.add( m_label_browser ); m_proxy.signal_clicked().connect( sigc::mem_fun( *this, &PageNet::slot_setup_proxy ) ); m_browser.signal_clicked().connect( sigc::mem_fun( *this, &PageNet::slot_setup_browser ) ); set_column_spacing( SPACING_SIZE ); set_row_spacing( SPACING_SIZE ); // Gtk::Grid::attach( child, column, row, width, height ) attach( m_icon, 0, 0, 1, 1 ); attach( m_label, 1, 0, 1, 1 ); attach( m_proxy, 0, 1, 2, 1 ); attach( m_browser, 0, 2, 2, 1 ); attach( m_frame, 0, 3, 2, 1 ); // フレームの追加 m_label.set_hexpand( true ); } // // プロキシ設定 // void PageNet::slot_setup_proxy() { auto pref = CORE::PrefDiagFactory( m_parent, CORE::PREFDIAG_PROXY, "" ); pref->run(); } // // ブラウザ設定 // void PageNet::slot_setup_browser() { auto pref = CORE::PrefDiagFactory( m_parent, CORE::PREFDIAG_BROWSER, "" ); pref->run(); m_label_browser.set_text( CONFIG::get_command_openurl() ); } ///////////////////////////////////////////// PageFont::PageFont() : Gtk::Grid() , m_icon( ICON::get_icon_manager()->get_icon( ICON::JD48 ) ) , m_label( "3/5.フォントの設定をします", Gtk::ALIGN_START ) , m_label_res( "スレ(_T)", Gtk::ALIGN_START, Gtk::ALIGN_CENTER, true ) , m_label_mail( "メール(_U)", Gtk::ALIGN_START, Gtk::ALIGN_CENTER, true ) , m_label_popup( "ポップアップ(_P)", Gtk::ALIGN_START, Gtk::ALIGN_CENTER, true ) , m_label_tree( "板/スレ一覧(_O)", Gtk::ALIGN_START, Gtk::ALIGN_CENTER, true ) , m_font_res( "スレフォント" ) , m_font_mail( "メール欄フォント" ) , m_font_popup( "ポップアップフォント" ) , m_font_tree( "板/スレ一覧フォント" ) { m_label_res.set_mnemonic_widget( m_font_res ); m_label_mail.set_mnemonic_widget( m_font_mail ); m_label_popup.set_mnemonic_widget( m_font_popup ); m_label_tree.set_mnemonic_widget( m_font_tree ); m_font_res.set_font_name( CONFIG::get_fontname( FONT_MAIN ) ); m_font_mail.set_font_name( CONFIG::get_fontname( FONT_MAIL ) ); m_font_popup.set_font_name( CONFIG::get_fontname( FONT_POPUP ) ); m_font_tree.set_font_name( CONFIG::get_fontname( FONT_BBS ) ); m_font_res.signal_font_set().connect( sigc::mem_fun( *this, &PageFont::slot_font_res ) ); m_font_mail.signal_font_set().connect( sigc::mem_fun( *this, &PageFont::slot_font_mail ) ); m_font_popup.signal_font_set().connect( sigc::mem_fun( *this, &PageFont::slot_font_popup ) ); m_font_tree.signal_font_set().connect( sigc::mem_fun( *this, &PageFont::slot_font_tree ) ); m_font_res.set_hexpand( true ); m_font_mail.set_hexpand( true ); m_font_popup.set_hexpand( true ); m_font_tree.set_hexpand( true ); constexpr int width = 1; constexpr int height = 1; m_table.attach( m_label_res, 0, 0, width, height ); m_table.attach( m_label_mail, 0, 1, width, height ); m_table.attach( m_label_popup, 0, 2, width, height ); m_table.attach( m_label_tree, 0, 3, width, height ); m_table.attach( m_font_res, 1, 0, width, height ); m_table.attach( m_font_mail, 1, 1, width, height ); m_table.attach( m_font_popup, 1, 2, width, height ); m_table.attach( m_font_tree, 1, 3, width, height ); set_column_spacing( SPACING_SIZE ); set_row_spacing( SPACING_SIZE ); // Gtk::Grid::attach( child, column, row, width, height ) attach( m_icon, 0, 0, 1, 1 ); attach( m_label, 1, 0, 1, 1 ); attach( m_table, 0, 1, 2, 1 ); m_label.set_hexpand( true ); m_table.set_hexpand( true ); m_table.set_row_spacing( 4 ); m_table.set_column_spacing( 4 ); } void PageFont::slot_font_res() { CONFIG::set_fontname( FONT_MAIN, m_font_res.get_font_name() ); CONFIG::set_fontname( FONT_MESSAGE, m_font_res.get_font_name() ); } void PageFont::slot_font_mail() { CONFIG::set_fontname( FONT_MAIL, m_font_mail.get_font_name() ); } void PageFont::slot_font_popup() { CONFIG::set_fontname( FONT_POPUP, m_font_popup.get_font_name() ); } void PageFont::slot_font_tree() { CONFIG::set_fontname( FONT_BBS, m_font_tree.get_font_name() ); CONFIG::set_fontname( FONT_BOARD, m_font_tree.get_font_name() ); } ///////////////////////////////////////////// PagePane::PagePane() : Gtk::Grid() , m_icon( ICON::get_icon_manager()->get_icon( ICON::JD48 ) ) , m_label( "4/5.ペイン表示設定をします", Gtk::ALIGN_START ) , m_2pane( m_radiogroup, "2ペイン表示(_2)", true ) , m_3pane( m_radiogroup, "3ペイン表示(_3)", true ) , m_v3pane( m_radiogroup, "縦3ペイン表示(_V)", true ) , m_label_inst( "", Gtk::ALIGN_START ) { switch( SESSION::get_mode_pane() ){ case SESSION::MODE_2PANE: m_2pane.set_active( true ); slot_2pane(); break; case SESSION::MODE_3PANE: m_3pane.set_active( true ); slot_3pane(); break; case SESSION::MODE_V3PANE: m_v3pane.set_active( true ); slot_v3pane(); break; } m_2pane.signal_toggled().connect( sigc::mem_fun( *this, &PagePane::slot_2pane ) ); m_3pane.signal_toggled().connect( sigc::mem_fun( *this, &PagePane::slot_3pane ) ); m_v3pane.signal_toggled().connect( sigc::mem_fun( *this, &PagePane::slot_v3pane ) ); set_column_spacing( SPACING_SIZE ); set_row_spacing( SPACING_SIZE ); // Gtk::Grid::attach( child, column, row, width, height ) attach( m_icon, 0, 0, 1, 1 ); attach( m_label, 1, 0, 1, 1 ); attach( m_2pane, 0, 1, 2, 1 ); attach( m_3pane, 0, 2, 2, 1 ); attach( m_v3pane, 0, 3, 2, 1 ); attach( m_label_inst, 0, 4, 2, 1 ); m_label.set_hexpand( true ); } void PagePane::slot_2pane() { SESSION::set_mode_pane( SESSION::MODE_2PANE ); m_label_inst.set_text( "ウィンドウの左に板一覧、右にスレ一覧とスレビューを切り替え表示" ); } void PagePane::slot_3pane() { SESSION::set_mode_pane( SESSION::MODE_3PANE ); m_label_inst.set_text( "ウィンドウの左に板一覧、右上にスレ一覧、右下にスレビューを表示" ); } void PagePane::slot_v3pane() { SESSION::set_mode_pane( SESSION::MODE_V3PANE ); // ウインドウの幅を固定するため空白で長さを揃える m_label_inst.set_text( "ウィンドウの左に板一覧、中央にスレ一覧、右にスレビューを表示 " ); } ///////////////////////////////////////////// PageEnd::PageEnd() : Gtk::Grid() , m_icon( ICON::get_icon_manager()->get_icon( ICON::JD48 ) ) , m_label( "5/5.JDim セットアップ完了", Gtk::ALIGN_START ) , m_label2( "その他の設定は起動後に設定及び表示メニューからおこなって下さい\n\n" "完了を押すとJDimを起動して板一覧のリストをロードします\n" "板一覧が表示されるまでしばらくお待ち下さい", Gtk::ALIGN_START ) { set_column_spacing( SPACING_SIZE ); set_row_spacing( SPACING_SIZE ); // Gtk::Grid::attach( child, column, row, width, height ) attach( m_icon, 0, 0, 1, 1 ); attach( m_label, 1, 0, 1, 1 ); attach( m_label2, 0, 1, 2, 1 ); m_label.set_hexpand( true ); } ///////////////////////////////////////////// SetupWizard::SetupWizard() : Gtk::Dialog() , m_page_network{ this } , m_back( "<< 戻る(_B)", true ) , m_next( "次へ(_N) >>", true ) { set_title( "JDim セットアップウィザード" ); set_keep_above( true ); set_resizable( false ); set_position( Gtk::WIN_POS_CENTER ); // 配置はデスクトップ環境次第 // ボタン get_action_area()->set_spacing( SPACING_SIZE / 2 ); get_action_area()->pack_start( m_back, Gtk::PACK_SHRINK ); get_action_area()->pack_start( m_next, Gtk::PACK_SHRINK ); m_fin = add_button( "完了(_C)", Gtk::RESPONSE_OK ); m_back.set_sensitive( false ); m_next.set_sensitive( true ); m_fin->set_sensitive( false ); m_back.signal_clicked().connect( sigc::mem_fun( *this, &SetupWizard::slot_back ) ); m_next.signal_clicked().connect( sigc::mem_fun( *this, &SetupWizard::slot_next ) ); // ページ m_notebook.append_page( m_page_start ); m_notebook.append_page( m_page_network ); m_notebook.append_page( m_page_font ); m_notebook.append_page( m_page_pane ); m_notebook.append_page( m_page_end ); m_notebook.set_border_width( SPACING_SIZE ); m_notebook.set_show_border( false ); m_notebook.set_show_tabs( false ); m_sigc_switch_page = m_notebook.signal_switch_page().connect( sigc::mem_fun( *this, &SetupWizard::slot_switch_page ) ); get_content_area()->pack_start( m_notebook, Gtk::PACK_EXPAND_PADDING, SPACING_SIZE ); show_all_children(); } SetupWizard::~SetupWizard() { if( m_sigc_switch_page.connected() ) m_sigc_switch_page.disconnect(); } void SetupWizard::slot_switch_page( Gtk::Widget* notebookpage, guint page ) { switch( page ){ case 0: m_back.set_sensitive( false ); m_next.set_sensitive( true ); m_fin->set_sensitive( false ); break; case 1: case 2: case 3: m_back.set_sensitive( true ); m_next.set_sensitive( true ); m_fin->set_sensitive( false ); break; case 4: m_back.set_sensitive( true ); m_next.set_sensitive( false ); m_fin->set_sensitive( true ); m_fin->grab_focus(); break; } } void SetupWizard::slot_back() { int page = m_notebook.get_current_page(); m_notebook.set_current_page( page - 1 ); } void SetupWizard::slot_next() { int page = m_notebook.get_current_page(); m_notebook.set_current_page( page + 1 ); } jdim-0.10.1/src/setupwizard.h000066400000000000000000000054521445721505100160470ustar00rootroot00000000000000// ライセンス: GPL2 // // セットアップウィザード // #ifndef _SETUPWIZARD_H #define _SETUPWIZARD_H #include "gtkmmversion.h" #include namespace CORE { class PageStart : public Gtk::Grid { Gtk::Image m_icon; Gtk::Label m_label; Gtk::Label m_label2; public: PageStart(); }; //////////////////////////////////////////// class PageNet : public Gtk::Grid { Gtk::Window* m_parent{}; Gtk::Image m_icon; Gtk::Label m_label; Gtk::Button m_proxy; Gtk::Button m_browser; // フレームの追加 Gtk::Frame m_frame; Gtk::Label m_label_browser; public: explicit PageNet( Gtk::Window* parent ); private: void slot_setup_proxy(); void slot_setup_browser(); }; ///////////////////////////////////////////// class PageFont : public Gtk::Grid { Gtk::Image m_icon; Gtk::Label m_label; Gtk::Grid m_table; Gtk::Label m_label_res; Gtk::Label m_label_mail; Gtk::Label m_label_popup; Gtk::Label m_label_tree; Gtk::FontButton m_font_res; Gtk::FontButton m_font_mail; Gtk::FontButton m_font_popup; Gtk::FontButton m_font_tree; public: PageFont(); private: void slot_font_res(); void slot_font_mail(); void slot_font_popup(); void slot_font_tree(); }; ///////////////////////////////////////////// class PagePane : public Gtk::Grid { Gtk::Image m_icon; Gtk::Label m_label; Gtk::RadioButtonGroup m_radiogroup; Gtk::RadioButton m_2pane; Gtk::RadioButton m_3pane; Gtk::RadioButton m_v3pane; Gtk::Label m_label_inst; public: PagePane(); private: void slot_2pane(); void slot_3pane(); void slot_v3pane(); }; ///////////////////////////////////////////// class PageEnd : public Gtk::Grid { Gtk::Image m_icon; Gtk::Label m_label; Gtk::Label m_label2; public: PageEnd(); }; ///////////////////////////////////////////// class SetupWizard : public Gtk::Dialog { sigc::connection m_sigc_switch_page; Gtk::Notebook m_notebook; PageStart m_page_start; PageNet m_page_network; PageFont m_page_font; PagePane m_page_pane; PageEnd m_page_end; Gtk::Button* m_fin; Gtk::Button m_back; Gtk::Button m_next; public: SetupWizard(); ~SetupWizard(); private: void slot_switch_page( Gtk::Widget* notebookpage, guint page ); void slot_back(); void slot_next(); void slot_fin(); }; } #endif jdim-0.10.1/src/sharedbuffer.cpp000066400000000000000000000006461445721505100164610ustar00rootroot00000000000000// ライセンス: GPL2 #include "sharedbuffer.h" using namespace CORE; CORE::DATA_INFO_LIST shared_infolist; int CORE::SBUF_size() { return shared_infolist.size(); } void CORE:: SBUF_clear_info() { shared_infolist.clear(); } void CORE::SBUF_set_list( const CORE::DATA_INFO_LIST& list_info ) { shared_infolist = list_info; } CORE::DATA_INFO_LIST& CORE::SBUF_list_info() { return shared_infolist; } jdim-0.10.1/src/sharedbuffer.h000066400000000000000000000006131445721505100161200ustar00rootroot00000000000000// ライセンス: GPL2 // // 共有バッファ // // クラス間で情報をやり取りするときに使う // #ifndef _SHAREDBUFFER_H #define _SHAREDBUFFER_H #include "type.h" #include "data_info.h" namespace CORE { int SBUF_size(); void SBUF_clear_info(); void SBUF_set_list( const CORE::DATA_INFO_LIST& list_info ); CORE::DATA_INFO_LIST& SBUF_list_info(); } #endif jdim-0.10.1/src/sidebaritempref.cpp000066400000000000000000000030161445721505100171600ustar00rootroot00000000000000// ライセンス: GPL2 //#define _DEBUG #include "jddebug.h" #include "sidebaritempref.h" #include "icons/iconmanager.h" #include "jdlib/miscutil.h" #include "global.h" #include "session.h" #include "command.h" using namespace CORE; SidebarItemPref::SidebarItemPref( Gtk::Window* parent, const std::string& url ) : SKELETON::SelectItemPref( parent, url ) { // デフォルトの項目を設定 append_default_pair( ITEM_NAME_SEARCHBOX, ICON::get_icon( ICON::TRANSPARENT ) ); append_default_pair( ITEM_NAME_SEARCH_NEXT, ICON::get_icon( ICON::SEARCH_NEXT ) ); append_default_pair( ITEM_NAME_SEARCH_PREV, ICON::get_icon( ICON::SEARCH_PREV ) ); append_default_pair( ITEM_NAME_CHECK_UPDATE_ROOT, ICON::get_icon( ICON::CHECK_UPDATE_ROOT ) ); append_default_pair( ITEM_NAME_CHECK_UPDATE_OPEN_ROOT, ICON::get_icon( ICON::CHECK_UPDATE_OPEN_ROOT ) ); append_default_pair( ITEM_NAME_STOPLOADING, ICON::get_icon( ICON::STOPLOADING ) ); append_default_pair( ITEM_NAME_SEPARATOR, ICON::get_icon( ICON::TRANSPARENT ) ); // 文字列を元に列を追加 append_rows( SESSION::get_items_sidebar_toolbar_str() ); set_title( "ツールバー項目設定(サイドバー)" ); } // OKを押した void SidebarItemPref::slot_ok_clicked() { SESSION::set_items_sidebar_toolbar_str( get_items() ); CORE::core_set_command( "update_bbslist_toolbar_button" ); } // // デフォルトボタン // void SidebarItemPref::slot_default() { append_rows( SESSION::get_items_sidebar_toolbar_default_str() ); } jdim-0.10.1/src/sidebaritempref.h000066400000000000000000000010711445721505100166240ustar00rootroot00000000000000// ライセンス: GPL2 // サイドバーのツールバーの表示項目設定 #ifndef _SIDEBARITEMPREF_H #define _SIDEBARITEMPREF_H #include "skeleton/selectitempref.h" namespace CORE { class SidebarItemPref : public SKELETON::SelectItemPref { public: SidebarItemPref( Gtk::Window* parent, const std::string& url ); ~SidebarItemPref() noexcept = default; private: // OK押した void slot_ok_clicked() override; // デフォルトボタン void slot_default() override; }; } #endif jdim-0.10.1/src/sign.h000066400000000000000000000012671445721505100144260ustar00rootroot00000000000000// ビューのアドレスで使用するサイン #ifndef _SIGN_H #define _SIGN_H #define ARTICLE_SIGN "_ARTICLE_" #define NEXT_SIGN "_NEXT_" #define LOG_SIGN "_LOG_" #define SIDEBAR_SIGN "_SIDEBAR_" #define BOARD_SIGN "_BOARD_" #define RES_SIGN "_RES_" #define NAME_SIGN "_NAME_" #define ID_SIGN "_ID_" #define BOOKMK_SIGN "_BM_" #define POST_SIGN "_POST_" #define HIGHREFRES_SIGN \ "_HIGH_REFRES_" #define URL_SIGN "_URL_" #define REFER_SIGN "_REF_" #define KEYWORD_SIGN "_KW_" #define POSTLOG_SIGN "_POST_" #define ORMODE_SIGN "_OR_" #define CENTER_SIGN "_CENTER_" #define TIME_SIGN "_TIME_" #define TITLE_SIGN "_TITLE_" #endif jdim-0.10.1/src/skeleton/000077500000000000000000000000001445721505100151335ustar00rootroot00000000000000jdim-0.10.1/src/skeleton/Makefile.am000066400000000000000000000030401445721505100171640ustar00rootroot00000000000000noinst_LIBRARIES = libskeleton.a libskeleton_a_SOURCES = \ window.cpp \ aamenu.cpp \ admin.cpp \ dispatchable.cpp \ loadable.cpp \ treeviewbase.cpp \ dragtreeview.cpp \ edittreeview.cpp \ editcolumns.cpp \ view.cpp \ editview.cpp \ dragnote.cpp \ tabnote.cpp \ toolbarnote.cpp \ viewnote.cpp \ tablabel.cpp \ tabswitchbutton.cpp \ tabswitchmenu.cpp \ menubutton.cpp \ toolmenubutton.cpp \ backforwardbutton.cpp \ popupwinbase.cpp \ popupwin.cpp \ entry.cpp \ compentry.cpp \ label_entry.cpp \ login.cpp \ prefdiag.cpp \ selectitempref.cpp \ msgdiag.cpp \ aboutdiag.cpp \ detaildiag.cpp \ panecontrol.cpp \ hpaned.cpp \ vpaned.cpp \ notebook.cpp \ vbox.cpp \ jdtoolbar.cpp \ toolbar.cpp \ textloader.cpp \ undobuffer.cpp noinst_HEADERS = \ window.h \ aamenu.h \ admin.h \ dispatchable.h \ loadable.h \ lockable.h \ treeviewbase.h \ dragtreeview.h \ edittreeview.h \ editcolumns.h \ view.h \ editview.h \ editviewdialog.h \ dragnote.h \ tabnote.h \ toolbarnote.h \ viewnote.h \ tablabel.h \ tabswitchbutton.h \ tabswitchmenu.h \ imgtoolbutton.h \ menubutton.h \ toolmenubutton.h \ backforwardbutton.h \ popupwinbase.h \ popupwin.h \ iconpopup.h \ entry.h \ compentry.h \ label_entry.h \ prefdiag.h \ selectitempref.h \ msgdiag.h \ aboutdiag.h \ detaildiag.h \ editviewdialog.h \ filediag.h \ login.h \ panecontrol.h \ hpaned.h \ vpaned.h \ notebook.h \ vbox.h \ jdtoolbar.h \ toolbar.h \ textloader.h \ undobuffer.h AM_CXXFLAGS = @GTKMM_CFLAGS@ AM_CPPFLAGS = -I$(top_srcdir)/src jdim-0.10.1/src/skeleton/aamenu.cpp000066400000000000000000000170331445721505100171110ustar00rootroot00000000000000// AA 選択ポップアップメニュークラス //#define _DEBUG #include "gtkmmversion.h" #include "jddebug.h" #include "aamenu.h" #include "config/globalconf.h" #include "fontid.h" #include "colorid.h" #include "aamanager.h" #include "cache.h" using namespace SKELETON; AAMenu::AAMenu( Gtk::Window& parent ) : Gtk::Menu() , m_parent( parent ), m_popup( SKELETON::POPUPWIN_NOFRAME ) { #ifdef _DEBUG std::cout << "AAMenu::AAMenu\n"; #endif Pango::FontDescription pfd( CONFIG::get_fontname( FONT_MESSAGE ) ); pfd.set_weight( Pango::WEIGHT_NORMAL ); m_textview.override_font( pfd ); m_textview.override_color( Gdk::RGBA( CONFIG::get_color( COLOR_CHAR_SELECTION ) ), Gtk::STATE_FLAG_NORMAL ); m_textview.override_background_color( Gdk::RGBA( CONFIG::get_color( COLOR_BACK_SELECTION ) ), Gtk::STATE_FLAG_NORMAL ); m_popup.set_transient_for( m_parent ); m_popup.sig_configured().connect( sigc::mem_fun( *this, &AAMenu::slot_configured_popup ) ); m_popup.add( m_textview ); m_popup.show_all_children(); create_popupmenu(); } AAMenu::~AAMenu() { #ifdef _DEBUG std::cout << "AAMenu::~AAMenu\n"; #endif } int AAMenu::get_size() const { return static_cast< int >( get_children().size() ); } void AAMenu::set_text( const std::string& text ) { m_popup.show(); m_popup.resize( 1, 1 ); m_textview.get_buffer()->set_text( text ); } // メニュー項目作成 void AAMenu::create_menuitem( Glib::RefPtr< Gtk::ActionGroup > actiongroup, Gtk::Menu* menu, const int id ) { const int maxchar = 20; Glib::ustring aa_label = CORE::get_aamanager()->get_label( id ); std::string shortcut = CORE::get_aamanager()->id2shortcut( id ); if( ! shortcut.empty() ) aa_label = "[" + shortcut + "] " + aa_label; std::string actname = "aa" + std::to_string( id ); if( actiongroup->get_action( actname ) ) return; // 登録済み #ifdef _DEBUG std::cout << actname << " label = " << aa_label << std::endl; #endif Glib::RefPtr< Gtk::Action > action = Gtk::Action::create( actname, aa_label.substr( 0, maxchar ) ); action->set_accel_group( m_parent.get_accel_group() ); Gtk::MenuItem* item = Gtk::manage( action->create_menu_item() ); actiongroup->add( action, sigc::bind< Gtk::MenuItem* >( sigc::mem_fun( *this, &AAMenu::slot_aainput_menu_clicked ), item ) ); item->signal_select().connect( sigc::bind< Gtk::MenuItem* >( sigc::mem_fun( *this, &AAMenu::slot_select_item ), item ) ); menu->append( *item ); m_map_items.insert( std::make_pair( item, id ) ); } // メニュー作成 void AAMenu::create_popupmenu() { std::string aa_lines; if( ! CACHE::load_rawdata( CACHE::path_aalist(), aa_lines ) ) return; Glib::RefPtr< Gtk::ActionGroup > actiongroup = Gtk::ActionGroup::create(); // 履歴 for( int i = 0 ; i < CORE::get_aamanager()->get_historysize() ; ++i ){ int org_id = CORE::get_aamanager()->history2id( i ); create_menuitem( actiongroup, this, org_id ); } if( CORE::get_aamanager()->get_historysize() ){ Gtk::MenuItem* item = Gtk::manage( new Gtk::SeparatorMenuItem() ); append( *item ); m_map_items.insert( std::make_pair( item, -1 ) ); } for( int i = 0 ; i < CORE::get_aamanager()->get_size() ; ++i ) create_menuitem( actiongroup, this, i ); show_all_children(); } void AAMenu::on_map() { #ifdef _DEBUG std::cout << "AAMenu::on_realize\n"; #endif Gtk::Menu::on_map(); select_item( *dynamic_cast< Gtk::MenuItem* >( *get_children().begin() ) ); } void AAMenu::on_hide() { #ifdef _DEBUG std::cout << "AAMenu::on_hide\n"; #endif m_popup.hide(); Gtk::Menu::on_hide(); } // 下移動 bool AAMenu::move_down() { #ifdef _DEBUG std::cout << "AAMenu::move_down\n"; #endif const auto items = get_children(); auto it = std::find( items.begin(), items.end(), static_cast< Gtk::Widget* >( m_activeitem ) ); ++it; if( m_map_items[ dynamic_cast< Gtk::MenuItem* >( *it ) ] == -1 ) { ++it; // セパレータをスキップする } if( it == items.end() ) { it = items.begin(); // 一番下まで行ったら上に戻る } select_item( *dynamic_cast< Gtk::MenuItem* >( *it ) ); return true; } // 上移動 bool AAMenu::move_up() { #ifdef _DEBUG std::cout << "AAMenu::move_up\n"; #endif // gtk2のGlib::ListHandleのイテレーターはデクリメント不可なので型変換する const std::vector< Gtk::Widget* > items = get_children(); auto it = std::find( items.begin(), items.end(), static_cast< Gtk::Widget* >( m_activeitem ) ); if( it != items.begin() ) { --it; if( m_map_items[ dynamic_cast< Gtk::MenuItem* >( *it ) ] == -1 && it != items.begin() ) { --it; // セパレータをスキップする } select_item( *dynamic_cast< Gtk::MenuItem* >( *it ) ); } else { // 一番上に行ったら下に戻る select_item( *dynamic_cast< Gtk::MenuItem* >( items.back() ) ); } return true; } // キー入力のフック bool AAMenu::on_key_press_event( GdkEventKey* event ) { #ifdef _DEBUG std::cout << "AAMenu::on_key_press_event key = " << event->keyval << std::endl; #endif // 下移動 if( event->keyval == GDK_KEY_j || ( ( event->state & GDK_CONTROL_MASK ) && event->keyval == GDK_KEY_n ) || event->keyval == GDK_KEY_space ){ move_down(); return true; } // 上移動 else if( event->keyval == GDK_KEY_k || ( ( event->state & GDK_CONTROL_MASK ) && event->keyval == GDK_KEY_p ) ){ move_up(); return true; } // ショートカット else{ int id = CORE::get_aamanager()->shortcut2id( event->string[ 0 ] ); #ifdef _DEBUG std::cout << "id = " << id << " key = " << event->string[ 0 ] << std::endl; #endif if( id >= 0 ){ std::string aa = CORE::get_aamanager()->get_aa( id ); if( ! aa.empty() ){ m_sig_selected.emit( aa ); CORE::get_aamanager()->append_history( id ); hide(); } } } return Gtk::Menu::on_key_press_event( event ); } // // ポップアップウィンドウのサイズが変わった // void AAMenu::slot_configured_popup( int width, int height ) { int sw = get_screen()->get_width(); int x, y; get_window()->get_root_coords( 0, 0, x, y ); #ifdef _DEBUG std::cout << " AAMenu::slot_configured_popup w = " << width << " h = " << height << " x = " << x << " screen width = " << sw << std::endl; #endif y -= height; if( x + width > sw ) x = sw - width; m_popup.move( x, y ); } // // メニューの行を選択 // void AAMenu::slot_select_item( Gtk::MenuItem* item ) { if( ! get_active() ) return; int id = m_map_items[ item ]; m_activeitem = item; #ifdef _DEBUG std::cout << "AAMenu::slot_select_item id = " << id << std::endl; #endif set_text( CORE::get_aamanager()->get_aa( id ) ); } // // アスキーアート入力 // void AAMenu::slot_aainput_menu_clicked( Gtk::MenuItem* item ) { if( ! get_active() ) return; int id = m_map_items[ item ]; m_activeitem = item; #ifdef _DEBUG std::cout << "AAMenu::slot_aainput_menu_clicked id = " << id << std::endl; std::cout << CORE::get_aamanager()->get_aa( id ) << std::endl; #endif m_sig_selected.emit( CORE::get_aamanager()->get_aa( id ) ); CORE::get_aamanager()->append_history( id ); } jdim-0.10.1/src/skeleton/aamenu.h000066400000000000000000000025651445721505100165620ustar00rootroot00000000000000// ライセンス: GPL2 // // AA 選択ポップアップメニュークラス // #ifndef _AAMENU_H #define _AAMENU_H #include #include #include "popupwinbase.h" namespace SKELETON { typedef sigc::signal< void, const std::string& > SIG_AAMENU_SELECTED; class AAMenu : public Gtk::Menu { SIG_AAMENU_SELECTED m_sig_selected; Gtk::Window& m_parent; SKELETON::PopupWinBase m_popup; Gtk::TextView m_textview; std::map< Gtk::MenuItem*, int > m_map_items; Gtk::MenuItem* m_activeitem{}; public: explicit AAMenu( Gtk::Window& parent ); ~AAMenu(); // 選択されたらemitされる SIG_AAMENU_SELECTED sig_selected() { return m_sig_selected; } protected: void on_map() override; void on_hide() override; bool on_key_press_event (GdkEventKey* event) override; private: int get_size() const; void set_text( const std::string& text ); void create_menuitem( Glib::RefPtr< Gtk::ActionGroup > actiongroup, Gtk::Menu* menu, const int id ); void create_popupmenu(); bool move_down(); bool move_up(); void slot_select_item( Gtk::MenuItem* item ); void slot_configured_popup( int width, int height ); void slot_aainput_menu_clicked( Gtk::MenuItem* item ); }; } #endif jdim-0.10.1/src/skeleton/aboutdiag.cpp000066400000000000000000000211631445721505100176010ustar00rootroot00000000000000// ライセンス: GPL2 //#define _DEBUG #include "jddebug.h" #include "aboutdiag.h" #include "command.h" #include "environment.h" #include "session.h" #include "config/globalconf.h" #include "icons/iconmanager.h" #include "jdlib/miscgtk.h" #include using namespace SKELETON; enum { MARGIN = 8 }; AboutDiag::AboutDiag( const Glib::ustring& title ) : Gtk::Dialog( title, ENVIRONMENT::get_dialog_use_header_bar() ? Gtk::DIALOG_USE_HEADER_BAR : Gtk::DialogFlags{} ) , m_button_copy_environment( "クリップボードへコピー" ) { set_transient_for( *CORE::get_mainwindow() ); set_resizable( false ); add_button( g_dgettext( GTK_DOMAIN, "_Close" ), Gtk::RESPONSE_CLOSE ); set_default_response( Gtk::RESPONSE_CLOSE ); set_logo( ICON::get_icon( ICON::JD96 ) ); set_version( ENVIRONMENT::get_jdversion() ); set_comments( ENVIRONMENT::get_jdcomments() ); set_website( CONFIG::get_url_jdimhp() ); set_copyright( ENVIRONMENT::get_jdcopyright() ); set_license( ENVIRONMENT::get_jdlicense() ); set_environment_list(); } AboutDiag::~AboutDiag() noexcept = default; // // run() // int AboutDiag::run() { #ifdef _DEBUG std::cout << "AboutDiag::run start\n"; #endif init(); SESSION::set_dialog_shown( true ); CORE::core_set_command( "dialog_shown" ); int ret = Gtk::Dialog::run(); SESSION::set_dialog_shown( false ); CORE::core_set_command( "dialog_hidden" ); #ifdef _DEBUG std::cout << "AboutDiag::run fin\n"; #endif return ret; } // // 各ウィジェットを配置して初期化 // void AboutDiag::init() { // 情報タブの追加 m_label_tab_info.set_label( "情報" ); m_vbox_info.set_spacing( MARGIN ); m_vbox_info.set_border_width( MARGIN ); // ロゴ if( get_logo() ) { m_vbox_info.pack_start( m_image_logo, Gtk::PACK_SHRINK ); } // バージョン if( ! get_version().empty() ) { m_vbox_info.pack_start( m_label_version, Gtk::PACK_EXPAND_WIDGET, MARGIN ); } // コメント if( ! get_comments().empty() ) { m_vbox_info.pack_start( m_label_comments, Gtk::PACK_SHRINK ); } // コピーライト if( ! get_copyright().empty() ) { m_label_copyright.set_justify( Gtk::JUSTIFY_CENTER ); m_vbox_info.pack_start( m_label_copyright, Gtk::PACK_SHRINK ); } // Webサイト if( ! get_website_label().empty() ) { m_hbox_url.pack_start( m_button_website, Gtk::PACK_EXPAND_PADDING ); m_vbox_info.pack_start( m_hbox_url, Gtk::PACK_SHRINK ); } m_notebook.append_page( m_vbox_info, m_label_tab_info ); // ライセンスタブの追加 if( ! get_license().empty() ) { m_label_tab_license.set_label( "ライセンス" ); m_notebook.append_page( m_scrollwindow_license, m_label_tab_license ); } // 動作環境タブの追加 m_label_tab_environment.set_label( "動作環境" ); m_notebook.append_page( m_vbox_environment, m_label_tab_environment ); // 動作環境一覧 m_vbox_environment.pack_start( m_scrollwindow_environment, Gtk::PACK_EXPAND_WIDGET ); // クリップボードへコピーのボタン m_button_copy_environment.signal_clicked().connect( sigc::mem_fun( *this, &AboutDiag::slot_copy_environment ) ); m_hbuttonbox_environment.set_layout( Gtk::BUTTONBOX_END ); m_hbuttonbox_environment.pack_start( m_button_copy_environment, Gtk::PACK_SHRINK ); m_vbox_environment.pack_start( m_hbuttonbox_environment, Gtk::PACK_SHRINK ); get_content_area()->pack_start( m_notebook, Gtk::PACK_EXPAND_WIDGET, MARGIN ); show_all_children(); // バージョンの文字列の長さが短い( x.x.x-YYMMDD )と // ウィンドウの幅が狭すぎるので"幅/高さ"いずれかの大 // きい方に合わせて4:3を保持するようにする。 int win_w, win_h; get_size( win_w, win_h ); if( win_w * 3 / 4 < win_h ) set_size_request( win_h * 4 / 3, -1 ); else set_size_request( -1, win_w * 3 / 4 ); } // // ロゴ // void AboutDiag::set_logo( const Glib::RefPtr< Gdk::Pixbuf >& logo ) { m_image_logo.set( logo ); } Glib::RefPtr< Gdk::Pixbuf > AboutDiag::get_logo() { return m_image_logo.get_pixbuf(); } // // バージョン表示 // void AboutDiag::set_version( const Glib::ustring& version ) { m_label_version.set_label( version ); auto scale = Pango::Attribute::create_attr_scale( 5.0 / 3.0 ); auto weight = Pango::Attribute::create_attr_weight( Pango::WEIGHT_BOLD ); Pango::AttrList list; list.insert( scale ); list.insert( weight ); m_label_version.set_attributes( list ); } Glib::ustring AboutDiag::get_version() const { return m_label_version.get_label(); } // // コメント // void AboutDiag::set_comments( const Glib::ustring& comments ) { m_label_comments.set_label( comments ); } Glib::ustring AboutDiag::get_comments() const { return m_label_comments.get_label(); } // // コピーライト // void AboutDiag::set_copyright( const Glib::ustring& copyright ) { m_label_copyright.set_label( copyright ); } Glib::ustring AboutDiag::get_copyright() const { return m_label_copyright.get_label(); } // // Webサイト // void AboutDiag::set_website( const Glib::ustring& website ) { if( m_button_website.get_label().empty() ) m_button_website.set_label( website ); m_button_website.set_relief( Gtk::RELIEF_NONE ); m_button_website.signal_clicked().connect( sigc::mem_fun( *this, &AboutDiag::slot_button_website_clicked ) ); m_website_url = website; } Glib::ustring AboutDiag::get_website() const { return m_website_url; } // // Webサイトラベル // void AboutDiag::set_website_label( const Glib::ustring& website_label ) { m_button_website.set_label( website_label ); } Glib::ustring AboutDiag::get_website_label() const { return m_button_website.get_label(); } // // Webサイトのボタンがクリックされた // void AboutDiag::slot_button_website_clicked() { CORE::core_set_command( "open_url_browser", m_website_url ); } // // ライセンス // void AboutDiag::set_license( const Glib::ustring& license ) { m_textview_license.get_buffer()->set_text( license ); m_textview_license.set_editable( false ); m_textview_license.set_cursor_visible( false ); m_textview_license.set_accepts_tab( false ); m_textview_license.set_wrap_mode( Gtk::WRAP_WORD_CHAR ); m_scrollwindow_license.set_policy( Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC ); m_scrollwindow_license.add( m_textview_license ); } Glib::ustring AboutDiag::get_license() const { return m_textview_license.get_buffer()->get_text(); } // // 動作環境一覧 // void AboutDiag::set_environment_list() { Gtk::TreeModelColumn< Glib::ustring > column_name; Gtk::TreeModelColumn< Glib::ustring > column_value; Gtk::TreeModel::ColumnRecord record; Glib::RefPtr< Gtk::ListStore > liststore; record.add( column_name ); record.add( column_value ); liststore = Gtk::ListStore::create( record ); m_treeview_environment.set_model( liststore ); Gtk::TreeModel::Row row; m_treeview_environment.append_column( "項目", column_name ); m_treeview_environment.append_column( "内容", column_value ); row = *( liststore->append() ); row[ column_name ] = "JDimのバージョン"; row[ column_value ] = ENVIRONMENT::get_jdversion(); row = *( liststore->append() ); row[ column_name ] = "ディストリビューション"; row[ column_value ] = ENVIRONMENT::get_distname(); row = *( liststore->append() ); row[ column_name ] = "デスクトップ環境等"; row[ column_value ] = ENVIRONMENT::get_wm_str(); row = *( liststore->append() ); row[ column_name ] = "gtkmmのバージョン"; row[ column_value ] = ENVIRONMENT::get_gtkmm_version(); row = *( liststore->append() ); row[ column_name ] = "glibmmのバージョン"; row[ column_value ] = ENVIRONMENT::get_glibmm_version(); row = *( liststore->append() ); row[ column_name ] = "TLSライブラリ"; row[ column_value ] = ENVIRONMENT::get_tlslib_version(); row = *( liststore->append() ); row[ column_name ] = "主なオプション"; row[ column_value ] = ENVIRONMENT::get_configure_args( ENVIRONMENT::CONFIGURE_OMITTED ); static_cast( row ); // cppcheck: unreadVariable m_scrollwindow_environment.add( m_treeview_environment ); } // // 動作環境をクリップボードにコピー // void AboutDiag::slot_copy_environment() { std::string jdinfo = ENVIRONMENT::get_jdinfo(); if( ! jdinfo.empty() ) MISC::CopyClipboard( jdinfo ); } jdim-0.10.1/src/skeleton/aboutdiag.h000066400000000000000000000041341445721505100172450ustar00rootroot00000000000000// ライセンス: GPL2 // Gtk::AboutDialog( gtkmm >= 2.6 )の代わりのクラス #ifndef _ABOUTDIAG_H #define _ABOUTDIAG_H #include namespace SKELETON { class AboutDiag : public Gtk::Dialog { Gtk::Notebook m_notebook; // 情報タブ Gtk::Label m_label_tab_info; Gtk::Image m_image_logo; Gtk::Label m_label_version; Gtk::VBox m_vbox_info; Gtk::Label m_label_info; Gtk::Label m_label_comments; Gtk::HBox m_hbox_url; Gtk::Button m_button_website; Gtk::Label m_label_copyright; // ライセンスタブ Gtk::Label m_label_tab_license; Gtk::ScrolledWindow m_scrollwindow_license; Gtk::TextView m_textview_license; // 動作環境タブ Gtk::Label m_label_tab_environment; Gtk::VBox m_vbox_environment; Gtk::HButtonBox m_hbuttonbox_environment; Gtk::Button m_button_copy_environment; Gtk::ScrolledWindow m_scrollwindow_environment; Gtk::TreeView m_treeview_environment; void set_environment_list(); Glib::ustring m_website_url; void init(); void slot_button_website_clicked(); void slot_copy_environment(); public: explicit AboutDiag( const Glib::ustring& title ); ~AboutDiag() noexcept; int run(); void set_logo( const Glib::RefPtr< Gdk::Pixbuf >& logo ); Glib::RefPtr< Gdk::Pixbuf > get_logo(); void set_version( const Glib::ustring& version ); Glib::ustring get_version() const; void set_comments( const Glib::ustring& comments ); Glib::ustring get_comments() const; void set_website( const Glib::ustring& website ); Glib::ustring get_website() const; void set_website_label( const Glib::ustring& website_label ); Glib::ustring get_website_label() const; void set_copyright( const Glib::ustring& copyright ); Glib::ustring get_copyright() const; void set_license( const Glib::ustring& license ); Glib::ustring get_license() const; }; } #endif jdim-0.10.1/src/skeleton/admin.cpp000066400000000000000000002460101445721505100167320ustar00rootroot00000000000000// ライセンス: GPL2 //#define _DEBUG #include "jddebug.h" #include "admin.h" #include "window.h" #include "view.h" #include "dragnote.h" #include "msgdiag.h" #include "tabswitchmenu.h" #include "dbtree/interface.h" #include "jdlib/miscutil.h" #include "jdlib/miscgtk.h" #include "history/historymanager.h" #include "history/viewhistoryitem.h" #include "control/controlutil.h" #include "control/controlid.h" #include "config/globalconf.h" #include "command.h" #include "global.h" #include "session.h" #include "sign.h" #include "updatemanager.h" #include // open_view で使うモード enum { OPEN_MODE_AUTO = 1, OPEN_MODE_NOSWITCH = 2, OPEN_MODE_LOCK = 4, OPEN_MODE_OFFLINE = 8, OPEN_MODE_REGET = 16 }; using namespace SKELETON; Admin::Admin( const std::string& url ) : m_url( url ) , m_notebook{ std::make_unique() } , m_prev_n_pages{ -1 } { m_notebook->signal_switch_page().connect( sigc::mem_fun( *this, &Admin::slot_switch_page ) ); m_notebook->set_scrollable( true ); m_notebook->sig_tab_clicked().connect( sigc::mem_fun( *this, &Admin::slot_tab_clicked ) ); m_notebook->sig_tab_scrolled().connect( sigc::mem_fun( *this, &Admin::slot_tab_scrolled ) ); m_notebook->sig_tab_close().connect( sigc::mem_fun( *this, &Admin::slot_tab_close ) ); m_notebook->sig_tab_reload().connect( sigc::mem_fun( *this, &Admin::slot_tab_reload ) ); m_notebook->sig_tab_menu().connect( sigc::mem_fun( *this, &Admin::slot_tab_menu ) ); m_notebook->get_tabswitch_button().signal_clicked().connect( sigc::mem_fun(*this, &Admin::slot_show_tabswitchmenu ) ); // D&D m_notebook->sig_drag_data_get().connect( sigc::mem_fun(*this, &Admin::slot_drag_data_get ) ); m_list_command.clear(); } Admin::~Admin() { #ifdef _DEBUG std::cout << "Admin::~Admin " << m_url << std::endl; #endif // デストラクタの中からdispatchを呼ぶと落ちるので dispatch不可にする set_dispatchable( false ); const int pages = m_notebook->get_n_pages(); #ifdef _DEBUG std::cout << "pages = " << pages << std::endl; #endif if( pages ){ for( int i = 0; i < pages; ++i ){ SKELETON::View* view = dynamic_cast< View* >( m_notebook->get_nth_page( 0 ) ); m_notebook->remove_page( 0, false ); if( view ) delete view; } } m_list_command.clear(); Admin::close_window(); } void Admin::save_session() { std::list< SKELETON::View* > list_view = get_list_view(); for( SKELETON::View* view : list_view ) { if( view ) view->save_session(); } } // // メニューのセットアップ // void Admin::setup_menu() { // 右クリックメニュー m_action_group = Gio::SimpleActionGroup::create(); m_action_group->add_action( "Quit", sigc::mem_fun( *this, &Admin::slot_close_tab ) ); m_action_group->add_action_bool( "LockTab", sigc::mem_fun( *this, &Admin::slot_lock ), false ); m_action_group->add_action( "CloseOther", sigc::mem_fun( *this, &Admin::slot_close_other_tabs ) ); m_action_group->add_action( "CloseLeft", sigc::mem_fun( *this, &Admin::slot_close_left_tabs ) ); m_action_group->add_action( "CloseRight", sigc::mem_fun( *this, &Admin::slot_close_right_tabs ) ); m_action_group->add_action( "CloseAll", sigc::mem_fun( *this, &Admin::slot_close_all_tabs ) ); m_action_group->add_action( "CloseSameIcon", sigc::mem_fun( *this, &Admin::slot_close_same_icon_tabs ) ); m_action_group->add_action( "CheckUpdateAll", sigc::mem_fun( *this, &Admin::slot_check_update_all_tabs ) ); m_action_group->add_action( "CheckUpdateReloadAll", sigc::mem_fun( *this, &Admin::slot_check_update_reload_all_tabs ) ); m_action_group->add_action( "ReloadAll", sigc::mem_fun( *this, &Admin::slot_reload_all_tabs ) ); m_action_group->add_action( "CancelReloadAll", sigc::mem_fun( *this, &Admin::slot_cancel_reload_all_tabs ) ); m_action_group->add_action( "OpenBrowser", sigc::mem_fun( *this, &Admin::slot_open_by_browser ) ); m_action_group->add_action( "CopyURL", sigc::mem_fun( *this, &Admin::slot_copy_url ) ); m_action_group->add_action( "CopyTitleURL", sigc::mem_fun( *this, &Admin::slot_copy_title_url ) ); m_action_group->add_action( "AppendFavorite", sigc::mem_fun( *this, &Admin::slot_append_favorite ) ); m_action_group->add_action( "Preference", sigc::mem_fun( *this, &Admin::show_preference ) ); // 戻る、進む m_action_group->add_action( "PrevView", sigc::bind( sigc::mem_fun( *this, &Admin::back_clicked_viewhistory ), 1 ) ); m_action_group->add_action( "NextView", sigc::bind( sigc::mem_fun( *this, &Admin::forward_clicked_viewhistory ), 1 ) ); m_action_group->add_action( "tab-head-focus", sigc::mem_fun( *this, &Admin::tab_head_focus ) ); m_action_group->add_action( "tab-tail-focus", sigc::mem_fun( *this, &Admin::tab_tail_focus ) ); m_action_group->add_action_with_parameter( "tab-switch", Glib::Variant::variant_type(), sigc::mem_fun( *this, &Admin::set_current_page_focus ) ); m_notebook->insert_action_group( "admin", m_action_group ); // ポップアップメニューのレイアウト Glib::ustring str_ui = R"(
タブをロックする(_K) admin.LockTab
)" ITEM_NAME_QUIT R"((_C) admin.Quit
複数のタブを閉じる(_T) 全てのタブ(_A) admin.CloseAll 他のタブ(_O) admin.CloseOther 左←のタブ(_L) admin.CloseLeft 右→のタブ(_R) admin.CloseRight 同じアイコンのタブ(_I) admin.CloseSameIcon
全てのタブの再読み込み(_A)
再読み込み(_R) admin.ReloadAll 更新チェックのみ(_U) admin.CheckUpdateAll 更新されたタブを再読み込み(_A) admin.CheckUpdateReloadAll
更新キャンセル(_C) admin.CancelReloadAll
)" ITEM_NAME_OPEN_BROWSER R"((_W) admin.OpenBrowser
)" ITEM_NAME_COPY_URL R"((_U) admin.CopyURL )" ITEM_NAME_COPY_TITLE_URL R"((_L) admin.CopyTitleURL
お気に入りに追加(_F)... admin.AppendFavorite
プロパティ(_P)... admin.Preference
)" ITEM_NAME_BACK R"((_P) admin.PrevView )" ITEM_NAME_FORWARD R"((_N) admin.NextView
先頭のタブに移動(_H) admin.tab-head-focus 最後のタブに移動(_T) admin.tab-tail-focus
)"; // アクセラレーターキーやマウスジェスチャーの追加は行わない // CONTROL::set_menu_motion() はGTK4で廃止されるGtk::MenuのAPIを使っている auto builder = Gtk::Builder::create_from_string( str_ui ); m_model_popup = Glib::RefPtr::cast_dynamic( builder->get_object( "popup_menu" ) ); // タブの切り替えメニュー m_model_tabswitch = SKELETON::TabSwitchMenu::create( m_notebook.get() ); // 移動サブメニュー auto submenu = Glib::RefPtr::cast_dynamic( builder->get_object( "move_submenu" ) ); submenu->append_section( m_model_tabswitch ); m_item_sub = Gio::MenuItem::create( ITEM_NAME_GO, submenu ); m_model_popup->prepend_item( m_item_sub ); } void Admin::slot_popupmenu_deactivate() { if( m_model_tabswitch ) m_model_tabswitch->deactivate(); } Gtk::Widget* Admin::get_widget() { return static_cast( m_notebook.get() ); } Gtk::Window* Admin::get_win() { return static_cast( m_win.get() ); } void Admin::set_jdwin( std::unique_ptr win ) { m_win = std::move( win ); } void Admin::delete_jdwin() { m_win.reset(); } bool Admin::is_booting() { if( get_jdwin() && get_jdwin()->is_booting() ) return true; return ( has_commands() ); } // // ページが含まれていないか // bool Admin::empty() const { return ( m_notebook->get_n_pages() == 0 ); } // タブの数 int Admin::get_tab_nums() { return m_notebook->get_n_pages(); } // // 含まれているページのURLのリスト取得 // std::list Admin::get_URLs() { std::list urls; const int pages = m_notebook->get_n_pages(); if( pages ){ for( int i = 0; i < pages; ++i ){ SKELETON::View* view = dynamic_cast< SKELETON::View* >( m_notebook->get_nth_page( i ) ); if( view ) urls.push_back( view->get_url() ); } } return urls; } // // クロック入力 // void Admin::clock_in() { // アクティブなビューにクロックを送る SKELETON::View* view = get_current_view(); if( view ) view->clock_in(); // 全てのビューにクロックを送る // clock_in_always()には軽い処理だけを含めること const int pages = m_notebook->get_n_pages(); if( pages ){ for( int i = 0; i < pages; ++i ){ view = dynamic_cast< SKELETON::View* >( m_notebook->get_nth_page( i ) ); if( view ) view->clock_in_always(); } } m_notebook->clock_in(); if( m_win ) m_win->clock_in(); } // // コマンド受付(通常) // void Admin::set_command( const std::string& command, const std::string& url, const std::string& arg1, const std::string& arg2, const std::string& arg3, const std::string& arg4, const std::string& arg5, const std::string& arg6, const std::string& arg7, const std::string& arg8 ) { COMMAND_ARGS command_arg; command_arg.command = command; command_arg.url = url; command_arg.arg1 = arg1; command_arg.arg2 = arg2; command_arg.arg3 = arg3; command_arg.arg4 = arg4; command_arg.arg5 = arg5; command_arg.arg6 = arg6; command_arg.arg7 = arg7; command_arg.arg8 = arg8; set_command_impl( false, command_arg ); } // // コマンド受付(即実行) // void Admin::set_command_immediately( const std::string& command, const std::string& url, const std::string& arg1, const std::string& arg2, const std::string& arg3, const std::string& arg4, const std::string& arg5, const std::string& arg6, const std::string& arg7, const std::string& arg8 ) { COMMAND_ARGS command_arg; command_arg.command = command; command_arg.url = url; command_arg.arg1 = arg1; command_arg.arg2 = arg2; command_arg.arg3 = arg3; command_arg.arg4 = arg4; command_arg.arg5 = arg5; command_arg.arg6 = arg6; command_arg.arg7 = arg7; command_arg.arg8 = arg8; set_command_impl( true, command_arg ); } // // コマンド受付 // // immediately = false の場合はすぐにコマンドを実行しないで一旦Dispatcherで // メインスレッドにコマンドを渡してからメインスレッドで実行する。通常は // immediately = false で呼び出して、緊急にコマンドを実行させたい場合は // immediately = true とすること。 // void Admin::set_command_impl( const bool immediately, const COMMAND_ARGS& command_arg ) { #ifdef _DEBUG std::cout << "Admin::set_command : immediately = " << immediately << " command = " << command_arg.command << " url = " << command_arg.url << std::endl << command_arg.arg1 << " " << command_arg.arg2 << std::endl << command_arg.arg3 << " " << command_arg.arg4 << std::endl << command_arg.arg5 << " " << command_arg.arg6 << std::endl << command_arg.arg7 << " " << command_arg.arg8 << std::endl; #endif if( immediately ){ m_list_command.push_front( command_arg ); exec_command(); } else{ m_list_command.push_back( command_arg ); dispatch(); // 一度メインループに戻った後にcallback_dispatch() が呼び戻される } } // // ディスパッチャのコールバック関数 // void Admin::callback_dispatch() { while( m_list_command.size() ) exec_command(); } // // コマンド実行 // void Admin::exec_command() { if( m_list_command.size() == 0 ) return; COMMAND_ARGS command = m_list_command.front(); m_list_command.pop_front(); // コマンドリストが空になったことをcoreに知らせる if( m_list_command.size() == 0 ) CORE::core_set_command( "empty_command", m_url ); #ifdef _DEBUG std::cout << "Admin::exec_command " << m_url << " : " << command.command << " " << command.url << " " << std::endl << command.arg1 << " " << command.arg2 << std::endl << command.arg3 << " " << command.arg4 << std::endl << command.arg5 << " " << command.arg6 << std::endl; #endif // 前回終了時の状態を回復 if( command.command == "restore" ){ restore( ( command.arg1 == "only_locked" ) ); } // 移転などでホストの更新 else if( command.command == "update_url" ){ update_url( command.url, command.arg1 ); } // 板名更新 else if( command.command == "update_boardname" ){ update_boardname( command.url ); } // viewを開く else if( command.command == "open_view" ){ open_view( command ); } // リストで開く // arg1 にはdatファイルを空白で区切って指定する // else if( command.command == "open_list" ){ open_list( command ); } else if( command.command == "switch_view" ){ switch_view( command.url ); } else if( command.command == "reload_view" ){ reload_view( command.url ); } else if( command.command == "tab_left" ){ tab_left( false ); } else if( command.command == "tab_right" ){ tab_right( false ); } else if( command.command == "tab_left_updated" ){ tab_left( true ); } else if( command.command == "tab_right_updated" ){ tab_right( true ); } else if( command.command == "tab_num" ){ tab_num( command.arg1 ); } else if( command.command == "tab_head" ){ tab_head(); } else if( command.command == "tab_tail" ){ tab_tail(); } else if( command.command == "set_tab_operating" ){ #ifdef _DEBUG std::cout << command.command << " " << m_url << " " << command.arg1 << std::endl; #endif if( command.arg1 == "true" ) SESSION::set_tab_operating( m_url, true ); else{ SESSION::set_tab_operating( m_url, false ); if( ! empty() ){ // 現在開いているタブへの switch 処理 const bool focus_tmp = m_focus; m_focus = true; slot_switch_page( nullptr, m_notebook->get_current_page() ); m_focus = focus_tmp; } } } else if( command.command == "redraw" ){ redraw_view( command.url ); } else if( command.command == "redraw_current_view" ){ redraw_current_view(); } else if( command.command == "relayout_current_view" ){ relayout_current_view(); } // command.url を含むview全てを検索して表示中なら再描画 else if( command.command == "redraw_views" ){ redraw_views( command.url ); } else if( command.command == "update_view" ){ // ビュー全体を更新 update_view( command.url ); } else if( command.command == "update_item" ){ // ビューの一部を更新 update_item( command.url, command.arg1 ); } else if( command.command == "update_finish" ){ update_finish( command.url ); } // 全ロック解除 else if( command.command == "unlock_views" ){ unlock_all_view( command.url ); } // ビューを閉じる else if( command.command == "close_view" ){ if( command.arg1 == "closeall" ) close_all_view( command.url ); // command.url を含むタブを全て閉じる else if( command.arg1 == "closealltabs" ) slot_close_all_tabs(); // 全て閉じる else if( command.arg1 == "closeother" ) close_other_views( command.url ); else close_view( command.url ); } else if( command.command == "close_currentview" ){ close_current_view(); } // 最後に閉じたタブを復元する else if( command.command == "restore_lasttab" ){ restore_lasttab(); } else if( command.command == "set_page" ){ set_current_page( atoi( command.arg1.c_str() ) ); } // フォーカスイン、アウト else if( command.command == "focus_current_view" ){ m_focus = true; focus_current_view(); } else if( command.command == "focus_out" ){ m_focus = false; focus_out(); } else if( command.command == "restore_focus" ){ m_focus = true; restore_focus(); } // adminクラスを前面に出す else if( command.command == "switch_admin" ){ switch_admin(); } // タブに文字をセット、タブ幅調整 else if( command.command == "set_tablabel" ){ set_tablabel( command.url, command.arg1 ); } else if( command.command == "adjust_tabwidth" ){ m_notebook->adjust_tabwidth(); } // 全てのビューを再描画 else if( command.command == "relayout_all" ){ const bool completely = ( command.arg1 == "completely" ); relayout_all( completely ); } // タイトル表示 // アクティブなviewから依頼が来たらコアに渡す else if( command.command == "set_title" ){ const bool force = ( command.arg2 == "force" ); set_title( command.url, command.arg1, force ); } // ステータス表示 // アクティブなviewから依頼が来たらコアに渡す else if( command.command == "set_status" ){ const bool force = ( command.arg2 == "force" ); set_status( command.url, command.arg1, force ); } // ステータスの色を変える else if( command.command == "set_status_color" ){ const bool force = ( command.arg2 == "force" ); set_status_color( command.url, command.arg1, force ); } // マウスジェスチャ else if( command.command == "set_mginfo" ){ if( m_win ) m_win->set_mginfo( command.arg1 ); } // 全タブオートリロード else if( command.command == "reload_all_tabs" ){ reload_all_tabs(); } // 全タブ更新チェックして開く else if( command.command == "check_update_reload_all_tabs" ){ check_update_all_tabs( true ); } // オートリロードのキャンセル else if( command.command == "cancel_reload" ){ slot_cancel_reload_all_tabs(); } // タブを隠す else if( command.command == "hide_tabs" ) m_notebook->set_show_tabs( false ); // window 開け閉じ else if( command.command == "open_window" ){ open_window(); return; } else if( command.command == "close_window" ){ close_window(); return; } // ツールバーの検索ボックスをフォーカス else if( command.command == "focus_toolbar_search" ){ focus_toolbar_search(); } // ツールバー表示/非表示切り替え else if( command.command == "toggle_toolbar" ){ toggle_toolbar(); } // ツールバーのアドレスを更新 else if( command.command == "update_toolbar_url" ){ m_notebook->update_toolbar_url( command.url, command.arg1 ); } // ツールバーのラベルを更新 else if( command.command == "redraw_toolbar" ){ redraw_toolbar(); } // ツールバーボタン表示更新 else if( command.command == "update_toolbar_button" ){ update_toolbar_button(); } // 検索バー表示 else if( command.command == "open_searchbar" ){ open_searchbar(); } // 検索バー非表示 else if( command.command == "close_searchbar" ){ close_searchbar(); } // タブ表示切り替え else if( command.command == "toggle_tab" ){ toggle_tab(); } // アイコン表示切り替え else if( command.command == "toggle_icon" ){ toggle_icon( command.url ); } // window 開け閉じ可能/不可 else if( command.command == "enable_fold_win" ){ if( get_jdwin() ) get_jdwin()->set_enable_fold( true ); } else if( command.command == "disable_fold_win" ){ if( get_jdwin() ) get_jdwin()->set_enable_fold( false ); } // プロパティ表示 else if( command.command == "show_preferences" ){ SKELETON::View* view = get_view( command.url ); if( view ) view->show_preference(); } else if( command.command == "show_current_preferences" ){ SKELETON::View* view = get_current_view(); if( view ) view->show_preference(); } // View履歴:戻る else if( command.command == "back_viewhistory" ){ back_viewhistory( command.url, atoi( command.arg1.c_str() ) ); } // View履歴:進む else if( command.command == "forward_viewhistory" ){ forward_viewhistory( command.url, atoi( command.arg1.c_str() ) ); } // View履歴削除 else if( command.command == "clear_viewhistory" ){ clear_viewhistory(); } // オートリロード開始 else if( command.command == "start_autoreload" ){ int mode = AUTORELOAD_ON; if( command.arg1 == "once" ) mode = AUTORELOAD_ONCE; int sec = atoi( command.arg2.c_str() ); set_autoreload_mode( command.url, mode, sec ); } // オートリロード停止 else if( command.command == "stop_autoreload" ){ set_autoreload_mode( command.url, AUTORELOAD_NOT, 0 ); } // ポップアップを隠す(インスタンスは削除しない) else if( command.command == "hide_popup" ) hide_popup(); else{ // ツールバー関係 SKELETON::View* view = get_view( command.url ); if( view && command.command == "toolbar_exec_search" ) view->exec_search(); else if( view && command.command == "toolbar_operate_search" ) view->operate_search( command.arg1 ); else if( view && command.command == "toolbar_set_search_query" ) view->set_search_query( command.arg1 ); if( view && command.command == "toolbar_up_search" ) view->up_search(); if( view && command.command == "toolbar_down_search" ) view->down_search(); else if( view && command.command == "toolbar_write" ) view->write(); else if( view && command.command == "toolbar_reload" ) view->reload(); else if( view && command.command == "toolbar_stop" ) view->stop(); else if( view && command.command == "toolbar_close_view" ) view->close_view(); else if( view && command.command == "toolbar_delete_view" ) view->delete_view(); else if( view && command.command == "toolbar_set_favorite" ) view->set_favorite(); // ロック/アンロック else if( view && command.command == "toolbar_lock_view" ){ if( view->is_locked() ) unlock( get_current_page() ); else lock( get_current_page() ); } // 子クラス別のコマンド処理 else command_local( command ); } } // リストで与えられたページをタブで連続して開く // // 連続してリロードかけるとサーバに負担をかけるので、オフラインで開いて // タイミングをずらしながらリロードする // void Admin::open_list( const COMMAND_ARGS& command_list ) { #ifdef _DEBUG std::cout << "Admin::open_list " << m_url << std::endl; #endif const std::string& str_list = command_list.arg1; std::list< std::string > list_url = MISC::split_line( str_list ); if( list_url.empty() ) return; int waittime = 0; const bool online = SESSION::is_online(); SESSION::set_tab_operating( m_url, true ); std::list< std::string >::iterator it = list_url.begin(); for( ; it != list_url.end(); ++it, waittime += AUTORELOAD_MINSEC ){ // 各admin別の引数をセット COMMAND_ARGS command_arg = get_open_list_args( ( *it ), command_list ); // 共通の引数をセット command_arg.command = "open_view"; command_arg.url = (*it); command_arg.arg1 = "true"; // タブで開く command_arg.arg2 = "false"; // 既に開いているかチェック command_arg.arg3 = "noswitch"; // タブを切り替えない if( ! command_list.arg2.empty() ) command_arg.arg3 += " " + command_list.arg2; #ifdef _DEBUG std::cout << "url = " << command_arg.url << std::endl; #endif open_view( command_arg ); // 一番最初のページは普通にオンラインで開く // 二番目からは ウェイトを入れてリロード if( online ){ if( !waittime ) SESSION::set_online( false ); else set_autoreload_mode( command_arg.url, AUTORELOAD_ONCE, waittime ); } } SESSION::set_online( online ); set_command( "set_tab_operating", "", "false" ); switch_admin(); switch_view( *( list_url.begin() ) ); } // // 移転などでviewのurlを更新 // void Admin::update_url( const std::string& url_old, const std::string& url_new ) { #ifdef _DEBUG std::cout << "Admin::update_url " << url_old << " -> " << url_new << std::endl; #endif const int pages = m_notebook->get_n_pages(); if( pages ){ for( int i = 0; i < pages; ++i ){ SKELETON::View* view = dynamic_cast< View* >( m_notebook->get_nth_page( i ) ); if( view && view->get_url().rfind( url_old, 0 ) == 0 ){ std::list< std::string >::iterator it = std::find( m_list_switchhistory.begin(), m_list_switchhistory.end(), view->get_url() ); view->update_url( url_old, url_new ); if( it != m_list_switchhistory.end() ) (*it) = view->get_url(); } } } } // // urlを含むviewの板名を更新 // void Admin::update_boardname( const std::string& url ) { #ifdef _DEBUG std::cout << "Admin::update_boardname url = " << url << std::endl; #endif const int pages = m_notebook->get_n_pages(); if( pages ){ for( int i = 0; i < pages; ++i ){ SKELETON::View* view = dynamic_cast< View* >( m_notebook->get_nth_page( i ) ); if( view && view->get_url().rfind( url, 0 ) == 0 ) view->update_boardname(); } redraw_toolbar(); } } // // URLやステータスを更新 // void Admin::update_status( View* view, const bool force ) { if( view ){ set_title( view->get_url(), view->get_title(), force ); set_url( view->get_url(), view->url_for_copy(), force ); set_status( view->get_url(), view->get_status(), force ); set_status_color( view->get_url(), view->get_color(), force ); } } // // ビューを開く // // command.arg1: 開く位置 // // "false" ならアクティブなタブを置き換える ( デフォルト ) // "true" なら一番右側に新しいタブを開く // "right" ならアクティブなタブの右に開く // "newtab" "opentab" なら設定によってアクティブなタブの右に開くか一番右側に開くか切り替える // "left" ならアクティブなタブの左に開く // "page数字" なら指定した位置にタブで開く // "replace" なら arg3 に指定したタブがあれば置き換え、なければ "newtab" で動作する // // command.arg2: "true" なら既に command.url を開いているかチェックしない // // command.arg3: モード ( 複数指定する場合は空白で空ける ) // // "auto"なら表示されていればリロードせずに切替える // されていなければarg1で指定した場所に新しいタブで開いてロード // スレ番号ジャンプなどで使用する // "noswitch"ならタブを切り替えない(連続して開くときに使用) // "lock" なら開いてからロックする // "offline" なら オフラインで開く // "reget" なら読み込み時にキャッシュ等を消してから再読み込みする // "boardnext" など、 "replace" で置き換えるタブの種類を指定する // // その他のargは各ビュー別の設定 // void Admin::open_view( const COMMAND_ARGS& command ) { #ifdef _DEBUG std::cout << "Admin::open_view : " << command.url << std::endl << "arg1 = " << command.arg1 << std::endl << "arg2 = " << command.arg2 << std::endl << "arg3 = " << command.arg3 << std::endl; #endif SKELETON::View* view; SKELETON::View* current_view = get_current_view(); const bool online = SESSION::is_online(); const bool nocheck_opened = ( command.arg2 == "true" ); int mode = 0; if( ! command.arg3.empty() ){ if( command.arg3.find( "auto" ) != std::string::npos ) mode |= OPEN_MODE_AUTO; if( command.arg3.find( "noswitch" ) != std::string::npos ) mode |= OPEN_MODE_NOSWITCH; if( command.arg3.find( "lock" ) != std::string::npos ) mode |= OPEN_MODE_LOCK; if( command.arg3.find( "offline" ) != std::string::npos ) mode |= OPEN_MODE_OFFLINE; if( command.arg3.find( "reget" ) != std::string::npos ) mode |= OPEN_MODE_REGET; } if( current_view ) current_view->focus_out(); // urlを既に開いていたら表示してリロード if( ! nocheck_opened ){ view = get_view( command_to_url( command ) ); if( view ){ // タブの切り替え if( ! ( mode & OPEN_MODE_NOSWITCH ) ){ int page = m_notebook->page_num( *view ); #ifdef _DEBUG std::cout << "page = " << page << std::endl; #endif set_current_page( page ); switch_admin(); } // オートモードは切り替えのみ if( mode & OPEN_MODE_AUTO ) return; // ロック if( mode & OPEN_MODE_LOCK ) lock( m_notebook->page_num( *view ) ); // オフラインで開く if( mode & OPEN_MODE_OFFLINE ) SESSION::set_online( false ); // ロード時にキャッシュを削除してからスレを再読み込みする if( mode & OPEN_MODE_REGET ) view->set_reget( true ); view->show_view(); SESSION::set_online( online ); return; } } // 開く位置の基準を、アクティブなタブに仮定 int page = m_notebook->get_current_page(); std::string open_method = command.arg1; if( open_method.empty() ){ open_method = "false"; } // 置き替えるページを探す if( open_method == "replace" ){ // 該当するタブが見つからない場合のために、新しいタブで開くモードに仮設定 open_method = "newtab"; // タブの種類 (command.arg3) に該当するタブを探す int find_page = find_view( command.arg3 ); #ifdef _DEBUG std::cout << "replace mode = " << command.arg3 << " page = " << page << " find = " << find_page << std::endl; #endif if( find_page >= 0 ){ SKELETON::View* found_view = dynamic_cast< View* >( m_notebook->get_nth_page( page ) ); if( found_view ){ // 指定したタブを置き換えるモードに設定 open_method = "false"; // 開く位置の基準を、見つかったタブに設定 page = find_page; current_view = found_view; } } } view = create_view( command ); if( !view ) return; const bool open_tab = ( page == -1 || open_method != "false" || ( mode & OPEN_MODE_AUTO ) // オートモードの時もタブで開く || is_locked( page ) ); // ツールバー表示 if( page == -1 ) show_toolbar(); // タブで表示 if( open_tab ){ // 一番右側に新しいタブを開くに仮定 int openpage = -1; // 現在のページの右に表示 if( open_method == "right" ) openpage = page +1; // 設定によって切り替える else if( open_method == "newtab" ){ switch( CONFIG::get_newtab_pos() ){ case 1: openpage = page +1; break; case 2: openpage = page; break; } } else if( open_method == "opentab" ){ switch( CONFIG::get_opentab_pos() ){ case 1: openpage = page +1; break; case 2: openpage = page; break; } } // 現在のページの左に表示 else if( open_method == "left" ) openpage = page; // 指定した位置に表示 else if( open_method.rfind( "page", 0 ) == 0 ) openpage = atoi( open_method.c_str() + 4 ); // ロックされていたら右に表示 else if( open_method == "false" && page != -1 && is_locked( page ) ) openpage = page +1; #ifdef _DEBUG std::cout << "append openpage = " << openpage << " / page = " << page << std::endl; #endif if( page != -1 ) m_notebook->insert_page( command.url, *view, openpage ); // 最後に表示 else m_notebook->append_page( command.url, *view ); if( m_use_viewhistory ){ if( m_last_closed_url.empty() ) HISTORY::get_history_manager()->create_viewhistory( view->get_url() ); // 直前に閉じたViewの履歴を次に開いたViewに引き継ぐ else{ HISTORY::get_history_manager()->append_viewhistory( m_last_closed_url, view->get_url() ); m_last_closed_url = std::string(); } } } // 開いてるviewを消してその場所に表示 else{ #ifdef _DEBUG std::cout << "replace page\n"; #endif // タブを入れ替えたときに隣のタブの再描画を防ぐ set_command_immediately( "set_tab_operating", "", "true" ); // タブ入れ替え m_notebook->insert_page( command.url, *view, page ); m_notebook->remove_page( page + 1, false ); if( current_view ){ const std::string url_current = current_view->get_url(); delete current_view; remove_switchhistory( url_current ); if( m_use_viewhistory ){ // Aを開く -> B をタブで開く -> A を閉じる -> A を開いて B と置き換える(*) -> B をタブで開く // とすると、History_Manager::get_viewhistory()のキャッシュが誤作動して // ビューの戻る進むが変になるので、(*)の所で進む戻るの履歴を引き継ぐのをキャンセルする if( ! m_last_closed_url.empty() && view->get_url() == m_last_closed_url ){ HISTORY::get_history_manager()->delete_viewhistory( m_last_closed_url ); m_last_closed_url = std::string(); } HISTORY::get_history_manager()->append_viewhistory( url_current, view->get_url() ); } } #ifdef _DEBUG std::cout << "replace done\n"; #endif set_command( "set_tab_operating", "", "false" ); } m_notebook->show_all(); view->show(); // オフラインで開く if( mode & OPEN_MODE_OFFLINE ) SESSION::set_online( false ); // ロード時にキャッシュを削除してからスレを再読み込みする if( mode & OPEN_MODE_REGET ) view->set_reget( true ); view->show_view(); SESSION::set_online( online ); // ツールバーの情報更新 m_notebook->set_current_toolbar( view->get_id_toolbar(), view ); // タブの切り替え if( ! ( mode & OPEN_MODE_NOSWITCH ) ){ switch_admin(); set_current_page( m_notebook->page_num( *view ) ); } // ロック if( mode & OPEN_MODE_LOCK ) lock( m_notebook->page_num( *view ) ); } // // ツールバーの検索ボックスをフォーカス // void Admin::focus_toolbar_search() { m_notebook->focus_toolbar_search(); } // // ツールバー表示更新 // void Admin::redraw_toolbar() { SKELETON::View* view = get_current_view(); if( view ) m_notebook->set_current_toolbar( view->get_id_toolbar(), view ); } // // ツールバーのボタン表示更新 // void Admin::update_toolbar_button() { m_notebook->update_toolbar_button(); redraw_toolbar(); } // // ビュー切り替え // // 指定したURLのビューに切り替える // void Admin::switch_view( const std::string& url ) { SKELETON::View* view = get_view( url ); if( view ){ int page = m_notebook->page_num( *view ); set_current_page( page ); } } // // 指定したURLのビューを再読み込み // void Admin::reload_view( const std::string& url ) { SKELETON::View* view = get_view( url ); if( view ) view->reload(); } // // タブ左移動 // // updated == true の時は更新されたタブに移動 // void Admin::tab_left( const bool updated ) { const int pages = m_notebook->get_n_pages(); if( pages == 1 ) return; int page = m_notebook->get_current_page(); if( page == -1 ) return; #ifdef _DEBUG std::cout << "Admin::tab_left updated << " << updated << " page = " << page; #endif for( int i = 0; i < pages ; ++i ){ --page; if( page < 0 ) page = pages -1; if( ! updated ) break; SKELETON::View* view = dynamic_cast< View* >( m_notebook->get_nth_page( page ) ); if( ! view ) return; // updated アイコンが表示されているタブを見つける if( get_notebook()->get_tabicon( page ) == view->get_icon( "updated" ) ) break; } #ifdef _DEBUG std::cout << " -> " << page << std::endl; #endif if( page != m_notebook->get_current_page() ) set_current_page( page ); } // // タブ右移動 // // updated == true の時は更新されたタブに移動 // void Admin::tab_right( const bool updated ) { const int pages = m_notebook->get_n_pages(); if( pages == 1 ) return; int page = m_notebook->get_current_page(); if( page == -1 ) return; #ifdef _DEBUG std::cout << "Admin::tab_right updated << " << updated << " page = " << page; #endif for( int i = 0; i < pages; ++i ){ ++page; if( page >= pages ) page = 0; if( ! updated ) break; SKELETON::View* view = dynamic_cast< View* >( m_notebook->get_nth_page( page ) ); if( ! view ) return; // updated アイコンが表示されているタブを見つける if( get_notebook()->get_tabicon( page ) == view->get_icon( "updated" ) ) break; } #ifdef _DEBUG std::cout << " -> " << page << std::endl; #endif if( page != m_notebook->get_current_page() ) set_current_page( page ); } // // タブ位置(1-9)で移動 // void Admin::tab_num( const std::string& str_num ) { if( str_num.empty() ) return; const int num = strtol( str_num.c_str(), nullptr, 10 ); // Firefoxの動作に合わせた // 0 → 無視 // 8以下で存在する数より多い → 無視 // 9(存在しない) → 最後のタブ if ( num < 1 || num > 9 || ( num < 9 && num > get_tab_nums() ) ) return; // 9は存在しなくてもそのまま渡してしまう set_current_page( num - 1 ); } // // タブ先頭移動 // void Admin::tab_head() { const int pages = m_notebook->get_n_pages(); if( pages == 1 ) return; set_current_page( 0 ); } // // フォーカスしてタブ先頭移動 // void Admin::tab_head_focus() { if( ! m_focus ) switch_admin(); tab_head(); } // // タブ最後に移動 // void Admin::tab_tail() { const int pages = m_notebook->get_n_pages(); if( pages == 1 ) return; set_current_page( pages-1 ); } // // タブ最後に移動 // void Admin::tab_tail_focus() { if( ! m_focus ) switch_admin(); tab_tail(); } // // ビューを再描画 // void Admin::redraw_view( const std::string& url ) { #ifdef _DEBUG std::cout << "Admin::redraw_view : " << m_url << " : url = " << url << std::endl; #endif SKELETON::View* view = get_view( url ); if( view ) view->redraw_view(); } // // 現在のビューを再描画 // void Admin::redraw_current_view() { #ifdef _DEBUG std::cout << "Admin::redraw_current_view : " << m_url << std::endl; #endif SKELETON::View* view = get_current_view(); if( view ) view->redraw_view(); } // // 現在のビューを再レイアウト // void Admin::relayout_current_view() { #ifdef _DEBUG std::cout << "Admin::relayout_current_view : " << m_url << std::endl; #endif SKELETON::View* view = get_current_view(); if( view ) view->relayout(); } // // urlを含むビューを検索してそれがカレントならば再描画 // void Admin::redraw_views( const std::string& url ) { #ifdef _DEBUG std::cout << "Admin::redraw_view : " << m_url << " : url = " << url << std::endl; #endif SKELETON::View* current_view = get_current_view(); std::list< SKELETON::View* > list_view = get_list_view( url ); for( SKELETON::View* view : list_view ) { if( view && view == current_view ) view->redraw_view(); } } // // ビューを閉じる // void Admin::close_view( const std::string& url ) { #ifdef _DEBUG std::cout << "Admin::close_view : " << url << std::endl; #endif SKELETON::View* view = get_view( url ); close_view( view ); } void Admin::close_view( SKELETON::View* view ) { if( !view ) return; int page = m_notebook->page_num( *view ); int current_page = m_notebook->get_current_page(); if( is_locked( page ) ) return; remove_switchhistory( view->get_url() ); if( page == current_page ){ // その前に開いていたビューに切り替える const std::string hist_url = get_valid_switchhistory(); if( ! hist_url.empty() ) switch_view( hist_url ); // もし現在表示中のビューを消すときは予めひとつ右のビューにスイッチしておく // そうしないと左のビューを一度表示してしまうので遅くなる else{ SKELETON::View* newview = dynamic_cast< View* >( m_notebook->get_nth_page( page + 1 ) ); if( newview ) switch_view( newview->get_url() ); } } m_notebook->remove_page( page, true ); if( m_use_viewhistory ){ // 直前に閉じたViewの履歴を次に開いたViewに引き継ぐ if( ! m_last_closed_url.empty() ) HISTORY::get_history_manager()->delete_viewhistory( m_last_closed_url ); m_last_closed_url = view->get_url(); } delete view; #ifdef _DEBUG std::cout << "Admin::close_view : delete page = " << page << std::endl; #endif // 全てのビューが無くなったらコアに知らせる if( empty() ){ #ifdef _DEBUG std::cout << "empty\n"; #endif m_notebook->hide_toolbar(); CORE::core_set_command( "empty_page", m_url ); } } // // url を含むビューを全てアンロック // void Admin::unlock_all_view( const std::string& url ) { #ifdef _DEBUG std::cout << "Admin::unlock_all_view : " << url << std::endl; #endif std::list< View* > list_view = get_list_view( url ); for( SKELETON::View* view : list_view ) { if( view && view->is_locked() ){ view->unlock(); redraw_toolbar(); } } } // // url を含むビューを全て閉じる // void Admin::close_all_view( const std::string& url ) { #ifdef _DEBUG std::cout << "Admin::close_all_view : " << url << std::endl; #endif std::list< View* > list_view = get_list_view( url ); for( SKELETON::View* view : list_view ) { close_view( view ); } } // // url 以外のビューを全て閉じる // void Admin::close_other_views( const std::string& url ) { const int pages = m_notebook->get_n_pages(); for( int i = 0; i < pages; ++i ){ SKELETON::View* view = dynamic_cast< View* >( m_notebook->get_nth_page( i ) ); if( view && view->get_url() != url ) set_command( "close_view", view->get_url() ); } } // // 現在のビューを閉じる // void Admin::close_current_view() { #ifdef _DEBUG std::cout << "Admin::close_current_view : " << m_url << std::endl; #endif SKELETON::View* view = get_current_view(); if( view ) close_view( view->get_url() ); } // // ビューを更新する // void Admin::update_view( const std::string& url ) { #ifdef _DEBUG std::cout << "Admin::update_view : " << url << std::endl; #endif SKELETON::View* view = get_view( url ); if( view ) view->update_view(); } // // ビューの一部(例えばBoardViewなら行など)を更新 // void Admin::update_item( const std::string& url, const std::string& id ) { #ifdef _DEBUG std::cout << "Admin::update_item : " << url << " " << id << std::endl; #endif std::list< SKELETON::View* > list_view = get_list_view(); for( SKELETON::View* view : list_view ) { if( view ) view->update_item( url, id ); } } // // ビューに更新終了を知らせる // // update_view はデータロード時などの更新途中でも呼び出されるが // update_finish は最後に一度だけ呼び出される // void Admin::update_finish( const std::string& url ) { #ifdef _DEBUG std::cout << "Admin::update_finish : " << url << std::endl; #endif SKELETON::View* view = get_view( url ); if( view ) view->update_finish(); } // // タイトル表示 // void Admin::set_title( const std::string& url, const std::string& title, const bool force ) { if( m_win ) m_win->set_title( "JDim - " + title ); else{ SKELETON::View* view = get_current_view(); if( view ){ // アクティブなviewからコマンドが来たら表示する if( force || ( m_focus && view->get_url() == url ) ) CORE::core_set_command( "set_title", url, title ); } } } // // URLバーにアドレス表示 // void Admin::set_url( const std::string& url, const std::string& url_show, const bool force ) { if( m_win ){} else{ SKELETON::View* view = get_current_view(); if( view ){ // アクティブなviewからコマンドが来たら表示する if( force || ( m_focus && view->get_url() == url ) ){ CORE::core_set_command( "set_url", url_show ); // ツリービューのURLを選択 if( CONFIG::get_select_item_sync() != 0 ){ CORE::core_set_command( "select_sidebar_item", url ); CORE::core_set_command( "select_board_item", url ); } } } } } // // ステータス表示 // // virtual void Admin::set_status( const std::string& url, const std::string& stat, const bool force ) { if( m_win ) m_win->set_status( stat ); else{ SKELETON::View* view = get_current_view(); if( view ){ // アクティブなviewからコマンドが来たら表示する if( force || ( m_focus && view->get_url() == url ) ){ CORE::core_set_command( "set_status", url, stat ); CORE::core_set_command( "set_mginfo", "", "" ); } } } } // // ステータスの色を変える // void Admin::set_status_color( const std::string& url, const std::string& color, const bool force ) { if( m_win ) m_win->set_status_color( color ); else{ SKELETON::View* view = get_current_view(); if( view ){ // アクティブなviewからコマンドが来たら色を変える if( force || ( m_focus && view->get_url() == url ) ){ CORE::core_set_command( "set_status_color", url, color ); } } } } // // フォーカスする // // タイトルやURLバーやステータス表示も更新する // void Admin::focus_view( int page ) { #ifdef _DEBUG std::cout << "Admin::focus_view : " << m_url << " page = " << page << std::endl; #endif if( m_win ) m_win->focus_in(); SKELETON::View* view = dynamic_cast< View* >( m_notebook->get_nth_page( page ) ); if( view ) { view->focus_view(); update_status( view, false ); } } // // 現在のviewをフォーカスする // void Admin::focus_current_view() { if( ! m_focus ) return; #ifdef _DEBUG std::cout << "Admin::focus_current_view : " << m_url << std::endl; #endif int page = m_notebook->get_current_page(); focus_view( page ); } // // フォーカスアウトしたあとにフォーカス状態を回復する // // focus_current_view()と違ってURLバーやステータスを再描画しない // void Admin::restore_focus() { #ifdef _DEBUG std::cout << "Admin::restore_focus : " << m_url << std::endl; #endif int page = m_notebook->get_current_page(); SKELETON::View* view = dynamic_cast< View* >( m_notebook->get_nth_page( page ) ); if( view ) view->focus_view(); } // // 現在のviewをフォーカスアウトする // メインウィンドウがフォーカスアウトしたときなどに呼ばれる // void Admin::focus_out() { #ifdef _DEBUG std::cout << "Admin::focus_out : " << m_url << std::endl; #endif // ウィンドウ表示の時、ビューが隠れないようにフォーカスアウトする前に transient 指定をしておく if( get_jdwin() ) get_jdwin()->set_transient( true ); SKELETON::View* view = get_current_view(); if( view ) view->focus_out(); } // // タブラベル更新 // void Admin::set_tablabel( const std::string& url, const std::string& str_label ) { #ifdef _DEBUG std::cout << "Admin::set_tablabel : " << url << std::endl; #endif SKELETON::View* view = get_view( url ); if( view ){ const std::string label = MISC::to_plain( str_label ); m_notebook->set_tab_fulltext( label, m_notebook->page_num( *view ) ); // View履歴のタイトルも更新 if( m_use_viewhistory ){ HISTORY::get_history_manager()->replace_current_title_viewhistory( view->get_url(), label ); } } } // // 再レイアウト実行 // void Admin::relayout_all( const bool completely ) { std::list< SKELETON::View* > list_view = get_list_view(); for( SKELETON::View* view : list_view ) { if( view ) view->relayout( completely ); } } // // タブ表示切り替え // void Admin::toggle_tab() { if( m_notebook->get_show_tabs() ) m_notebook->set_show_tabs( false ); else m_notebook->set_show_tabs( true ); } // // アイコン表示切り替え // void Admin::toggle_icon( const std::string& url ) { #ifdef _DEBUG std::cout << "Admin::toggle_icon url = " << url << std::endl; #endif SKELETON::View* view = get_view( url ); if( view ){ std::string iconname = "default"; // まだロード中 if( view->is_loading() ) iconname = "loading"; // オートリロードモードでロード待ち else if( view->get_autoreload_mode() != AUTORELOAD_NOT ) iconname = "loading_stop"; // 古い else if( view->is_old() ) iconname = "old"; // 更新チェックして更新可能 ( 更新はしていない ) else if( view->is_check_update() ) iconname = "update"; // 更新済み ( HTTP_OK 又は HTTP_PARTIAL_CONTENT ) else if( view->is_updated() ){ // タブがアクティブの時は通常アイコンを表示 if( get_notebook()->page_num( *view ) == get_notebook()->get_current_page() ) iconname = "default"; else iconname = "updated"; } #ifdef _DEBUG std::cout << "name = " << iconname << std::endl; #endif const int id = view->get_icon( iconname ); get_notebook()->set_tabicon( iconname, get_notebook()->page_num( *view ), id ); if( m_model_tabswitch ) m_model_tabswitch->update_icons(); } } // // オートリロードのモード設定 // // 成功したらtrueを返す // // mode : モード (global.h 参照) // sec : リロードまでの秒数 // bool Admin::set_autoreload_mode( const std::string& url, int mode, int sec ) { SKELETON::View* view = get_view( url ); if( view ){ if( mode == AUTORELOAD_NOT && view->get_autoreload_mode() == AUTORELOAD_NOT ) return false; view->set_autoreload_mode( mode, sec ); // モード設定が成功したらアイコン変更 if( view->get_autoreload_mode() == mode ){ toggle_icon( view->get_url() ); return true; } } return false; } // ポップアップを隠す(インスタンスは削除しない) void Admin::hide_popup() { SKELETON::View* view = get_current_view(); if( view ) view->set_command( "hide_popup" ); } // // ビュークラス取得 // View* Admin::get_view( const std::string& url ) { SKELETON::View* view = get_current_view(); if( view && view->get_url() == url ) return view; const int pages = m_notebook->get_n_pages(); if( pages ){ for( int i = 0; i < pages; ++i ){ view = dynamic_cast< View* >( m_notebook->get_nth_page( i ) ); if( view && view->get_url() == url ) return view; } } return nullptr; } // // ビュークラスのリスト取得 // // url を含むビューのリストを返す // std::list< View* > Admin::get_list_view( const std::string& url ) { std::list< View* > list_view; const int pages = m_notebook->get_n_pages(); if( pages ){ for( int i = 0; i < pages; ++i ){ SKELETON::View* view = dynamic_cast< View* >( m_notebook->get_nth_page( i ) ); if( view && view->get_url().find ( url ) != std::string::npos ) list_view.push_back( view ); } } return list_view; } // // 全てのビュークラスのリスト取得 // // std::list< View* > Admin::get_list_view() { std::list< View* > list_view; const int pages = m_notebook->get_n_pages(); if( pages ){ for( int i = 0; i < pages; ++i ){ SKELETON::View* view = dynamic_cast< View* >( m_notebook->get_nth_page( i ) ); if( view ) list_view.push_back( view ); } } return list_view; } // // 現在表示されているビュークラス取得 // View* Admin::get_current_view() { int page = m_notebook->get_current_page(); if( page == -1 ) return nullptr; SKELETON::View* view = dynamic_cast< View* >( m_notebook->get_nth_page( page ) ); return view; } // // 指定したページに表示切替え // void Admin::set_current_page( const int page ) { m_notebook->set_current_page( page ); } // // フォーカスしてから指定したページに表示切替え // void Admin::set_current_page_focus( const Glib::VariantBase& page ) { if( ! m_focus ) switch_admin(); auto v = Glib::VariantBase::cast_dynamic>( page ); m_notebook->set_current_page( v.get() ); } // // 現在表示されているページ番号 // int Admin::get_current_page() { return m_notebook->get_current_page(); } // // 現在表示されているページのURL // std::string Admin::get_current_url() { SKELETON::View* view = get_current_view(); if( ! view ) return std::string(); return view->get_url(); } // // notebookのタブのページが切り替わったら呼ばれるslot // void Admin::slot_switch_page( Gtk::Widget*, guint page ) { // 起動中とシャットダウン中は処理しない if( SESSION::is_booting() ) return; if( SESSION::is_quitting() ) return; // タブ操作中 if( SESSION::is_tab_operating( m_url ) ) return; #ifdef _DEBUG std::cout << "Admin::slot_switch_page : " << m_url << " page = " << page << " focus = " << m_focus << std::endl; #endif SKELETON::View* view = dynamic_cast< View* >( m_notebook->get_nth_page( page ) ); if( view ){ #ifdef _DEBUG std::cout << "url = " << view->get_url() << std::endl; #endif // ツールバー表示切り替え m_notebook->set_current_toolbar( view->get_id_toolbar(), view ); // タブのアイコンを通常に戻して再描画 toggle_icon( view->get_url() ); view->redraw_view(); if( m_focus ){ update_status( view, false ); focus_current_view(); } append_switchhistory( view->get_url() ); CORE::core_set_command( "page_switched", m_url, view->get_url() ); } } // // タブをクリックした // void Admin::slot_tab_clicked( const int page ) { #ifdef _DEBUG std::cout << "Admin::slot_tab_clicked " << page << std::endl; #endif if( ! m_focus ) switch_admin(); } // タブの上でホイールを回した void Admin::slot_tab_scrolled( GdkEventScroll* event ) { #ifdef _DEBUG std::cout << "Admin::slot_tab_scrolled\n"; #endif if( ! m_focus ) switch_admin(); } // // タブを閉じる // void Admin::slot_tab_close( const int page ) { #ifdef _DEBUG std::cout << "Admin::slot_tab_close " << page << std::endl; #endif // 閉じる SKELETON::View* view = dynamic_cast< View* >( m_notebook->get_nth_page( page ) ); if( view ) close_view( view->get_url() ); } // // タブ再読み込み // void Admin::slot_tab_reload( const int page ) { #ifdef _DEBUG std::cout << "Admin::slot_tab_reload " << page << std::endl; #endif SKELETON::View* view = dynamic_cast< View* >( m_notebook->get_nth_page( page ) ); if( view ) view->reload(); } // // タブメニュー表示 // void Admin::slot_tab_menu( int page, int x, int y ) { if( ! m_model_popup ) return; #ifdef _DEBUG std::cout << "Admin::slot_tab_menu " << page << std::endl; #endif Glib::RefPtr act; m_clicked_page = -1; // メニューのactive状態を変えたときにslot関数が呼び出されるのをキャンセル auto get_action = [this]( const char* name ) { return Glib::RefPtr::cast_dynamic( m_action_group->lookup_action( name ) ); }; // ロック act = get_action( "LockTab" ); if( page >= 0 && act ){ if( ! is_lockable( page ) ) act->set_enabled( false ); else { act->set_enabled( true ); auto state = Glib::Variant::create( is_locked( page ) ); act->set_state( state ); } } // 閉じる if( act = get_action( "Quit" ); act ) { act->set_enabled( ! is_locked( page ) ); } SKELETON::View* view = dynamic_cast( m_notebook->get_nth_page( page ) ); // 進む、戻る if( view ){ if( act = get_action( "PrevView" ); act ) { const bool enabled = HISTORY::get_history_manager()->can_back_viewhistory( view->get_url(), 1 ); act->set_enabled( enabled ); } if( act = get_action( "NextView" ); act ) { const bool enabled = HISTORY::get_history_manager()->can_forward_viewhistory( view->get_url(), 1 ); act->set_enabled( enabled ); } // 仮想板や抽出ビューなどはお気に入りに追加を選択不可にする if( act = get_action( "AppendFavorite" ); act ) { const std::string& url = view->get_url(); // 仮想板 global.h を参照 if( url.rfind( "jdview://", 0 ) == 0 ) { act->set_enabled( false ); } // 抽出ビュー sign.h と ArticleAdmin::command_to_url() を参照 // XXX: 判定に使うマクロ文字列がurl部分にマッチすると誤って選択不可になってしまう可能性がある else if( const auto i = url.find( '_' ); i != std::string::npos && ( url.find( ARTICLE_SIGN, i ) != std::string::npos || url.find( POSTLOG_SIGN, i ) != std::string::npos || url.find( BOARD_SIGN KEYWORD_SIGN, i ) != std::string::npos || url.find( TITLE_SIGN KEYWORD_SIGN, i ) != std::string::npos ) ) { act->set_enabled( false ); } else { act->set_enabled( true ); } } } m_clicked_page = page; if( m_model_tabswitch ){ m_model_tabswitch->update_labels_and_icons(); // 起動時間を短縮するためメニューを表示するタイミングでGio::Menuをバインドする if( ! m_tablabel_menu.get_attach_widget() ) { m_tablabel_menu.bind_model( m_model_popup, true ); m_tablabel_menu.attach_to_widget( *m_notebook ); m_tablabel_menu.signal_deactivate().connect( sigc::mem_fun( *this, &Admin::slot_popupmenu_deactivate ) ); } // メニュー項目ラベルの更新は重いためタブ数が変わらないときは更新しない const int n_pages = m_notebook->get_n_pages(); if( n_pages != m_prev_n_pages ) { // Gio::Menuを使うとメニューに追加された項目のラベルは変更できない // 動的なメニュー項目を実現するには一旦メニューから項目を取り除く必要がある m_model_popup->remove( 0 ); auto label = Glib::ustring::compose( ITEM_NAME_GO " [ タブ数 %1 ](_M)", n_pages ); m_item_sub->set_label( label ); m_model_popup->prepend_item( m_item_sub ); m_prev_n_pages = n_pages; } #if GTK_CHECK_VERSION(3,24,6) m_tablabel_menu.popup_at_pointer( nullptr ); // current event #else // GTK 3.24.5 以下のバージョンではメニューのスクロールが出来なくなることがあるため廃止予定APIを使う m_tablabel_menu.popup( 0, gtk_get_current_event_time() ); #endif } } // タブ切り替えメニュー表示 void Admin::slot_show_tabswitchmenu() { #ifdef _DEBUG std::cout << "Admin::slot_show_tabswitchmenu\n"; #endif if( ! m_model_tabswitch ) { m_model_tabswitch = SKELETON::TabSwitchMenu::create( m_notebook.get() ); } m_model_tabswitch->update_labels_and_icons(); // 起動時間を短縮するためメニューを表示するタイミングでGio::Menuをバインドする if( ! m_tabswitchmenu.get_attach_widget() ) { m_tabswitchmenu.bind_model( m_model_tabswitch, true ); m_tabswitchmenu.attach_to_widget( *m_notebook ); m_tabswitchmenu.signal_deactivate().connect( sigc::mem_fun( *this, &Admin::slot_popupmenu_deactivate ) ); } #if GTK_CHECK_VERSION(3,24,6) // Specify the current event by nullptr. m_tabswitchmenu.popup_at_widget( &( m_notebook->get_tabswitch_button() ), Gdk::GRAVITY_SOUTH_EAST, Gdk::GRAVITY_NORTH_EAST, nullptr ); #else // GTK 3.24.5 以下のバージョンではメニューのスクロールが出来なくなることがあるため廃止予定APIを使う m_tabswitchmenu.popup( Gtk::Menu::SlotPositionCalc( sigc::mem_fun( *this, &Admin::slot_popup_pos ) ), 0, gtk_get_current_event_time() ); #endif } #if ! GTK_CHECK_VERSION(3,24,6) /** * @brief タブ切り替えメニューの位置決め */ void Admin::slot_popup_pos( int& x, int& y, bool& push_in ) { if( ! m_model_tabswitch ) return; const int mrg = 16; m_notebook->get_tabswitch_button().get_pointer( x, y ); int ox, oy; m_notebook->get_tabswitch_button().get_window()->get_origin( ox, oy ); const Gdk::Rectangle rect = m_notebook->get_tabswitch_button().get_allocation(); x += ox + rect.get_x() - mrg; y = oy + rect.get_y() + rect.get_height(); push_in = false; } #endif // // 右クリックメニューの閉じる // void Admin::slot_close_tab() { if( m_clicked_page < 0 ) return; #ifdef _DEBUG std::cout << "Admin::slot_close_tab " << m_clicked_page << std::endl; #endif SKELETON::View* view = dynamic_cast< View* >( m_notebook->get_nth_page( m_clicked_page ) ); if( view ) close_view( view->get_url() ); } // // ロック // void Admin::slot_lock() { if( m_clicked_page < 0 ) return; if( is_locked( m_clicked_page ) ) unlock( m_clicked_page ); else lock( m_clicked_page ); } // // 右クリックメニューの他を閉じる // void Admin::slot_close_other_tabs() { #ifdef _DEBUG std::cout << "Admin::slot_close_other_tabs " << m_clicked_page << std::endl; #endif set_command( "set_tab_operating", "", "true" ); std::string url; SKELETON::View* view = dynamic_cast< View* >( m_notebook->get_nth_page( m_clicked_page ) ); if( view ) url = view->get_url(); close_other_views( url ); set_command( "set_tab_operating", "", "false" ); } // // 右クリックメニューの左を閉じる // void Admin::slot_close_left_tabs() { #ifdef _DEBUG std::cout << "Admin::slot_close_left_tabs " << m_clicked_page << std::endl; #endif set_command( "set_tab_operating", "", "true" ); for( int i = 0; i < m_clicked_page; ++i ){ SKELETON::View* view = dynamic_cast< View* >( m_notebook->get_nth_page( i ) ); if( view ) set_command( "close_view", view->get_url() ); } set_command( "set_tab_operating", "", "false" ); } // // 右クリックメニューの右を閉じる // void Admin::slot_close_right_tabs() { #ifdef _DEBUG std::cout << "Admin::slot_close_right_tabs " << m_clicked_page << std::endl; #endif set_command( "set_tab_operating", "", "true" ); const int pages = m_notebook->get_n_pages(); for( int i = m_clicked_page +1; i < pages; ++i ){ SKELETON::View* view = dynamic_cast< View* >( m_notebook->get_nth_page( i ) ); if( view ) set_command( "close_view", view->get_url() ); } set_command( "set_tab_operating", "", "false" ); } // // 右クリックメニューの全てを閉じる // void Admin::slot_close_all_tabs() { #ifdef _DEBUG std::cout << "Admin::slot_close_all_tabs " << m_clicked_page << std::endl; #endif set_command( "set_tab_operating", "", "true" ); const int pages = m_notebook->get_n_pages(); for( int i = 0; i < pages; ++i ){ SKELETON::View* view = dynamic_cast< View* >( m_notebook->get_nth_page( i ) ); if( view ) set_command( "close_view", view->get_url() ); } set_command( "set_tab_operating", "", "false" ); } // // 右クリックメニューの同じアイコンのタブを閉じる // void Admin::slot_close_same_icon_tabs() { const int id = get_notebook()->get_tabicon( m_clicked_page ); #ifdef _DEBUG std::cout << "Admin::slot_close_same_icon_tabs page = " << m_clicked_page << " id = " << id << std::endl; #endif set_command( "set_tab_operating", "", "true" ); const int pages = m_notebook->get_n_pages(); for( int i = 0; i < pages; ++i ){ if( get_notebook()->get_tabicon( i ) == id ){ SKELETON::View* view = dynamic_cast< View* >( m_notebook->get_nth_page( i ) ); if( view ) set_command( "close_view", view->get_url() ); } } set_command( "set_tab_operating", "", "false" ); } // // 右クリックメニューの全てのタブの更新チェック // void Admin::slot_check_update_all_tabs() { check_update_all_tabs( m_clicked_page, false ); } // // 右クリックメニューの全てのタブの更新チェックと再読み込み // void Admin::slot_check_update_reload_all_tabs() { check_update_all_tabs( m_clicked_page, true ); } // // 開いているタブから全てのタブの更新チェックと再読み込み // void Admin::check_update_all_tabs( const bool open ) { check_update_all_tabs( m_notebook->get_current_page(), open ); } // // from_page から全てのタブを更新チェック // void Admin::check_update_all_tabs( const int from_page, const bool open ) { #ifdef _DEBUG std::cout << "Admin::check_update_all_tabs from = " << from_page << std::endl; #endif if( ! SESSION::is_online() ) return; const int pages = m_notebook->get_n_pages(); // 開始タブから右側 for( int i = from_page ; i < pages; ++i ){ SKELETON::View* view = dynamic_cast< View* >( m_notebook->get_nth_page( i ) ); if( view && view->get_enable_autoreload() ) CORE::get_checkupdate_manager()->push_back( view->get_url(), open ); } // 開始タブから左側 for( int i = 0 ; i < from_page; ++i ){ SKELETON::View* view = dynamic_cast< View* >( m_notebook->get_nth_page( i ) ); if( view && view->get_enable_autoreload() ) CORE::get_checkupdate_manager()->push_back( view->get_url(), open ); } CORE::get_checkupdate_manager()->run(); } // // 右クリックメニューの全ての再読み込み // void Admin::slot_reload_all_tabs() { #ifdef _DEBUG std::cout << "Admin::slot_reload_all_tabs " << m_clicked_page << std::endl; #endif reload_all_tabs( m_clicked_page ); } // // 開いているタブから全てのタブを再読み込み // void Admin::reload_all_tabs() { reload_all_tabs( m_notebook->get_current_page() ); } // // from_page から全てのタブを再読み込み // void Admin::reload_all_tabs( const int from_page ) { #ifdef _DEBUG std::cout << "Admin::reload_all_tabs from = " << from_page << std::endl; #endif if( ! SESSION::is_online() ) return; int waittime = 0; const int pages = m_notebook->get_n_pages(); // クリックしたタブから右側 for( int i = from_page ; i < pages; ++i ){ SKELETON::View* view = dynamic_cast< View* >( m_notebook->get_nth_page( i ) ); if( view ){ if( set_autoreload_mode( view->get_url(), AUTORELOAD_ONCE, waittime ) ) waittime += AUTORELOAD_MINSEC; } } // クリックしたタブから左側 for( int i = 0 ; i < from_page; ++i ){ SKELETON::View* view = dynamic_cast< View* >( m_notebook->get_nth_page( i ) ); if( view ){ if( set_autoreload_mode( view->get_url(), AUTORELOAD_ONCE, waittime ) ) waittime += AUTORELOAD_MINSEC; } } } // // 右クリックメニューの更新キャンセル // void Admin::slot_cancel_reload_all_tabs() { for( int i = 0 ; i < m_notebook->get_n_pages(); ++i ){ SKELETON::View* view = dynamic_cast< View* >( m_notebook->get_nth_page( i ) ); if( view ) set_autoreload_mode( view->get_url(), AUTORELOAD_NOT, 0 ); } CORE::get_checkupdate_manager()->stop(); } // // 右クリックメニューのブラウザで開く // void Admin::slot_open_by_browser() { #ifdef _DEBUG std::cout << "Admin::slot_open_by_browser " << m_clicked_page << std::endl; #endif SKELETON::View* view = dynamic_cast< View* >( m_notebook->get_nth_page( m_clicked_page ) ); if( view ) CORE::core_set_command( "open_url_browser", view->url_for_copy() ); } // // 右クリックメニューのURLコピー // void Admin::slot_copy_url() { SKELETON::View* view = dynamic_cast< View* >( m_notebook->get_nth_page( m_clicked_page ) ); if( view ) MISC::CopyClipboard( view->url_for_copy() ); } // // 右クリックメニューのタイトルとURLコピー // void Admin::slot_copy_title_url() { std::string str = m_notebook->get_tab_fulltext( m_clicked_page ); SKELETON::View* view = dynamic_cast< View* >( m_notebook->get_nth_page( m_clicked_page ) ); if( view ) str += "\n" + view->url_for_copy(); MISC::CopyClipboard( str ); } /** * @brief 右クリックメニューのお気に入りに追加 */ void Admin::slot_append_favorite() { if( m_clicked_page < 0 ) return; const SKELETON::View* view = dynamic_cast( get_notebook()->get_nth_page( m_clicked_page ) ); if( ! view ) return; const std::string url = view->get_url(); append_favorite_impl( url ); } // ページがロックされているかリストで取得 std::list< bool > Admin::get_locked() { std::list< bool > locked; const int pages = m_notebook->get_n_pages(); if( pages ){ for( int i = 0; i < pages; ++i ) locked.push_back( is_locked( i ) ); } return locked; } // タブのロック/アンロック bool Admin::is_lockable( const int page ) { SKELETON::View* view = dynamic_cast< View* >( m_notebook->get_nth_page( page ) ); if( view ) return view->is_lockable(); return false; } bool Admin::is_locked( const int page ) { SKELETON::View* view = dynamic_cast< View* >( m_notebook->get_nth_page( page ) ); if( view ) return view->is_locked(); return false; } bool Admin::is_locked( const std::string& url ) { SKELETON::View* view = get_view( url ); if( view ) return view->is_locked(); return false; } void Admin::lock( const int page ) { SKELETON::View* view = dynamic_cast< View* >( m_notebook->get_nth_page( page ) ); if( view ){ view->lock(); redraw_toolbar(); } } void Admin::unlock( const int page ) { SKELETON::View* view = dynamic_cast< View* >( m_notebook->get_nth_page( page ) ); if( view ){ view->unlock(); redraw_toolbar(); } } // urlで指定されるタブが存在するか bool Admin::exist_tab( const std::string& url ) { SKELETON::View* view = get_view( url ); if( view ) return true; return false; } // プロパティ表示 void Admin::show_preference() { SKELETON::View* view = dynamic_cast< View* >( m_notebook->get_nth_page( m_clicked_page ) ); if( view ) view->show_preference(); } // // View履歴:戻る // bool Admin::back_viewhistory( const std::string& url, const int count ) { return back_forward_viewhistory( url, true, count ); } void Admin::back_clicked_viewhistory( const int count ) { if( m_clicked_page < 0 ) return; SKELETON::View* view = dynamic_cast< View* >( m_notebook->get_nth_page( m_clicked_page ) ); if( ! view ) return; set_current_page( m_clicked_page ); back_forward_viewhistory( view->get_url(), true, count ); } // // View履歴進む // bool Admin::forward_viewhistory( const std::string& url, const int count ) { return back_forward_viewhistory( url, false, count ); } void Admin::forward_clicked_viewhistory( const int count ) { if( m_clicked_page < 0 ) return; SKELETON::View* view = dynamic_cast< View* >( m_notebook->get_nth_page( m_clicked_page ) ); if( ! view ) return; set_current_page( m_clicked_page ); back_forward_viewhistory( view->get_url(), false, count ); } // // 戻る、進む // bool Admin::back_forward_viewhistory( const std::string& url, const bool back, const int count ) { if( ! m_use_viewhistory ) return false; #ifdef _DEBUG std::cout << "Admin::back_forward_viewhistory back = " << back << "count = " << count << " tab = " << url << std::endl; #endif SKELETON::View* view = get_view( url ); if( view ){ if( view->is_locked() ) return false; const HISTORY::ViewHistoryItem* historyitem; if( back ) historyitem = HISTORY::get_history_manager()->back_viewhistory( url, count, false ); else historyitem = HISTORY::get_history_manager()->forward_viewhistory( url, count, false ); if( historyitem && ! historyitem->url.empty() ){ #ifdef _DEBUG std::cout << "open : " << historyitem->url << std::endl; #endif // 既にタブで開いている場合 if( get_view( historyitem->url) ){ // 次のviewを開けるかチェック bool enable_next = false; if( back ) enable_next = HISTORY::get_history_manager()->can_back_viewhistory( url, count +1 ); else enable_next = HISTORY::get_history_manager()->can_forward_viewhistory( url, count +1 ); SKELETON::MsgDiag mdiag( get_win(), historyitem->title + "\n\nは既にタブで開いています", false, Gtk::MESSAGE_QUESTION, Gtk::BUTTONS_NONE ); mdiag.add_button( "タブを開く(_T)", Gtk::RESPONSE_YES ); if( enable_next ) mdiag.add_button( "次を開く(_N)", Gtk::RESPONSE_NO ); mdiag.add_button( g_dgettext( GTK_DOMAIN, "_Cancel" ), Gtk::RESPONSE_CANCEL ); int ret = mdiag.run(); mdiag.hide(); switch( ret ){ case Gtk::RESPONSE_YES: switch_view( historyitem->url ); return false; case Gtk::RESPONSE_NO: return back_forward_viewhistory( url, back, count + 1 ); default: break; } return false; } // Admin::open_view() 中の append_viewhistory() を実行しないでここで View履歴の現在位置を変更する // Admin::Open_view()も参照すること const bool use_history = get_use_viewhistory(); set_use_viewhistory( false ); if( use_history ){ if( back ) HISTORY::get_history_manager()->back_viewhistory( url, count, true ); else HISTORY::get_history_manager()->forward_viewhistory( url, count, true ); } const COMMAND_ARGS command_arg = url_to_openarg( historyitem->url, false, false ); open_view( command_arg ); set_use_viewhistory( use_history ); return true; } } return false; } // // View履歴削除 // void Admin::clear_viewhistory() { if( ! m_use_viewhistory ) return; std::list< SKELETON::View* > list_view = get_list_view(); for( const SKELETON::View* view : list_view ) { if( view ){ HISTORY::get_history_manager()->delete_viewhistory( view->get_url() ); HISTORY::get_history_manager()->create_viewhistory( view->get_url() ); const int page = m_notebook->page_num( *view ); const std::string& str_label = m_notebook->get_tab_fulltext( page ); HISTORY::get_history_manager()->replace_current_title_viewhistory( view->get_url(), str_label ); } } if( ! m_last_closed_url.empty() ) HISTORY::get_history_manager()->delete_viewhistory( m_last_closed_url ); m_last_closed_url = std::string(); update_toolbar_button(); } // // タブの切り替え履歴を更新 // void Admin::append_switchhistory( const std::string& url ) { if( ! m_use_switchhistory ) return; // 起動中とシャットダウン中は処理しない if( SESSION::is_booting() ) return; if( SESSION::is_quitting() ) return; #ifdef _DEBUG std::cout << "Admin::append_switchhistory url = " << url << std::endl; #endif m_list_switchhistory.remove( url ); m_list_switchhistory.push_back( url ); #ifdef _DEBUG for( const std::string& hist_url : m_list_switchhistory ) { std::cout << hist_url << std::endl; } #endif } void Admin::remove_switchhistory( const std::string& url ) { if( ! m_use_switchhistory ) return; // 起動中とシャットダウン中は処理しない if( SESSION::is_booting() ) return; if( SESSION::is_quitting() ) return; #ifdef _DEBUG std::cout << "Admin::remove_switchhistory url = " << url << std::endl; #endif m_list_switchhistory.remove( url ); #ifdef _DEBUG for( const std::string& hist_url : m_list_switchhistory ) { std::cout << hist_url << std::endl; } #endif } // // タブの切り替え履歴から、有効な最後のURLを返す // // 表示されているビューに存在しないURLは履歴から削除して、表示されているビューのURLを返す。 // 見つからないときは空文字列を返す。 // // NOTE: // タブの切り替え履歴にあるビューは、表示されているビューのうちのどれかであるはずだが、 // 検索ビューなどでURLが変更になる場合があり、 ( View::set_url()で変更できてしまう。 ) // タブの切り替え履歴にあるURLと、表示されているビューのURLが不一致となることがある。 // また、その不一致なタブの切り替え履歴を、セッション情報 ( article_switchhistoryなど ) に // 保存してしまっていたため、ここで不一致な履歴の削除を行うことで、履歴を修復する。 // std::string Admin::get_valid_switchhistory() { if( ! m_use_switchhistory ) return std::string(); while( m_list_switchhistory.size() > 0 ){ // タブの切り替え履歴から、最後のURLを取り出す const std::string url = m_list_switchhistory.back(); // 表示されているビューであれば、そのURLを返す SKELETON::View* view = get_view( url ); if( view ) return url; // 表示されていないので、最後のURLを削除する m_list_switchhistory.pop_back(); #ifdef _DEBUG std::cout << "Admin::get_valid_switchhistory remove = " << url << std::endl; #endif } return std::string(); } jdim-0.10.1/src/skeleton/admin.h000066400000000000000000000330111445721505100163720ustar00rootroot00000000000000// ライセンス: GPL2 // Viewを管理するクラス // 派生クラスはcreate_view()をオーバロードすること // Adminへの命令はすべてset_command()を通じておこなうこと #ifndef _ADMIN_H #define _ADMIN_H #include "dispatchable.h" #include "command_args.h" #include #include #include #include namespace SKELETON { class JDWindow; class View; class DragableNoteBook; class TabSwitchMenu; class Admin : public Dispatchable { std::string m_url; std::unique_ptr m_win; protected: std::unique_ptr m_notebook; private: bool m_focus{}; std::list< COMMAND_ARGS > m_list_command; // 右クリックメニュー用 Glib::RefPtr m_action_group; Glib::RefPtr m_model_popup; Gtk::Menu m_tablabel_menu; int m_prev_n_pages; ///< 前回表示したときのタブ数 int m_clicked_page; // 移動サブメニュー Glib::RefPtr m_item_sub; Glib::RefPtr m_model_tabswitch; // タブ切り替えメニュー Gtk::Menu m_tabswitchmenu; // view履歴使用 bool m_use_viewhistory{}; std::string m_last_closed_url; // タブの切り替え履歴 bool m_use_switchhistory{}; std::list< std::string > m_list_switchhistory; public: explicit Admin( const std::string& url ); ~Admin(); // コピー禁止 Admin( const Admin& ) = delete; Admin& operator=( const Admin& ) = delete; virtual void save_session(); void setup_menu(); virtual bool empty() const; const std::string& get_url() const{ return m_url; } virtual Gtk::Widget* get_widget(); virtual Gtk::Window* get_win(); // 起動中 bool is_booting(); // フォーカスされているか virtual bool has_focus() const { return m_focus; } // タブの数 virtual int get_tab_nums(); // 含まれているページのURLのリスト取得 virtual std::list< std::string > get_URLs(); // Core からのクロック入力。 // Coreでタイマーをひとつ動かして全体の同期を取るようにしているので // 一定時間毎に clock_in() が Core から呼び出される virtual void clock_in(); // コマンド入力(通常) void set_command( const std::string& command, const std::string& url = std::string(), const std::string& arg1 = std::string(), const std::string& arg2 = std::string(), const std::string& arg3 = std::string(), const std::string& arg4 = std::string(), const std::string& arg5 = std::string(), const std::string& arg6 = std::string(), const std::string& arg7 = std::string(), const std::string& arg8 = std::string() ); // コマンド入力(即時実行) void set_command_immediately( const std::string& command, const std::string& url = std::string(), const std::string& arg1 = std::string(), const std::string& arg2 = std::string(), const std::string& arg3 = std::string(), const std::string& arg4 = std::string(), const std::string& arg5 = std::string(), const std::string& arg6 = std::string(), const std::string& arg7 = std::string(), const std::string& arg8 = std::string() ); // コマンドがセットされているか bool has_commands() const { return m_list_command.size(); } // 現在表示してるページ番号およびURL // 表示ページを指定したいときは "set_page" コマンドを使う virtual int get_current_page(); std::string get_current_url(); // urlで指定されるタブがロックされているか bool is_locked( const std::string& url ); // urlで指定されるタブが存在するか bool exist_tab( const std::string& url ); // 指定したページに表示切替え void set_current_page( const int page ); // フォーカスしてから指定したページに表示切替え void set_current_page_focus( const Glib::VariantBase& page ); virtual View* get_current_view(); protected: void set_use_viewhistory( const bool use ){ m_use_viewhistory = use; } bool get_use_viewhistory() const { return m_use_viewhistory; } void set_use_switchhistory( const bool use ){ m_use_switchhistory = use; } JDWindow* get_jdwin(){ return m_win.get(); } void set_jdwin( std::unique_ptr win ); void delete_jdwin(); // URLやステータスを更新 void update_status( View* view, const bool force ); DragableNoteBook* get_notebook(){ return m_notebook.get(); } // コマンド入力 // immediately = true のときディスパッチャを呼ばずにすぐさま実行 void set_command_impl( const bool immediately, const COMMAND_ARGS& command_arg ); void callback_dispatch() override; // admin共通コマンド実行 virtual void exec_command(); // 派生クラス固有のコマンドを実行 virtual void command_local( const COMMAND_ARGS& command ) = 0; // 起動時に前回の状態を回復 virtual void restore( const bool only_locked ) = 0; // url から Viewを開くための COMMAND_ARGS を取得する // restore() などで使用する virtual COMMAND_ARGS url_to_openarg( const std::string& url, const bool tab, const bool lock ) = 0; // COMMAND_ARGS からビューの URL を取得する virtual std::string command_to_url( const COMMAND_ARGS& command ){ return command.url; } // view_modeに該当するページを探す virtual int find_view( const std::string& view_mode ){ return -1; }; virtual void open_view( const COMMAND_ARGS& command ); virtual void switch_admin() = 0; // CORE::core_set_command( "switch_*" ) を送る virtual void switch_view( const std::string& url ); void reload_view( const std::string& url ); // タブ左右移動 // updated == true の時は更新されたタブに移動 virtual void tab_left( const bool updated ); virtual void tab_right( const bool updated ); virtual void tab_num( const std::string& str_num ); virtual void tab_head(); void tab_head_focus(); virtual void tab_tail(); void tab_tail_focus(); virtual void redraw_view( const std::string& url ); virtual void redraw_current_view(); virtual void relayout_current_view(); virtual void redraw_views( const std::string& url ); virtual void update_view( const std::string& url ); virtual void update_item( const std::string& url, const std::string& id ); virtual void update_finish( const std::string& url ); virtual void close_view( const std::string& url ); virtual void unlock_all_view( const std::string& url ); virtual void close_all_view( const std::string& url ); virtual void close_other_views( const std::string& url ); virtual void close_current_view(); virtual void restore_lasttab(){} virtual void set_title( const std::string& url, const std::string& title, const bool force ); virtual void set_url( const std::string& url, const std::string& url_show, const bool force ); virtual void set_status( const std::string& url, const std::string& stat, const bool force ); void set_status_color( const std::string& url, const std::string& color, const bool force ); virtual void focus_view( int page ); virtual void focus_current_view(); virtual void restore_focus(); virtual void focus_out(); virtual void set_tablabel( const std::string& url, const std::string& str_label ); virtual void relayout_all( const bool completely = false ); virtual void open_window(){} virtual void close_window(){} virtual void toggle_tab(); virtual void toggle_icon( const std::string& url ); // オートリロードのモード設定 virtual bool set_autoreload_mode( const std::string& url, int mode, int sec ); // ポップアップを隠す(インスタンスは削除しない) void hide_popup(); // タブをお気に入りにドロップした時にお気に入りがデータ送信を要求してきた virtual void slot_drag_data_get( Gtk::SelectionData& selection_data, const int page ) = 0; void open_list( const COMMAND_ARGS& command_list ); virtual COMMAND_ARGS get_open_list_args( const std::string& url, const COMMAND_ARGS& command_list ){ return COMMAND_ARGS(); } virtual View* create_view( const COMMAND_ARGS& command ){ return nullptr; }; virtual View* get_view( const std::string& url ); // url を含むビュークラスをリストで取得 ( url と一致するビューでは無い ) std::list< View* > get_list_view( const std::string& url ); // 全てのビュークラスをリストで取得 std::list< View* > get_list_view(); // ツールバー virtual void show_toolbar(){} virtual void toggle_toolbar(){} void focus_toolbar_search(); void redraw_toolbar(); void update_toolbar_button(); virtual void open_searchbar(){} virtual void close_searchbar(){} // タブの更新チェック void check_update_all_tabs( const bool open ); void check_update_all_tabs( const int from_page, const bool open ); // タブの再読み込み void reload_all_tabs(); void reload_all_tabs( const int from_page ); // notebookのタブが切り替わったときに呼ばれるslot void slot_switch_page( Gtk::Widget*, guint page ); // タブをクリックした void slot_tab_clicked( const int page ); // タブの上でホイールを回した void slot_tab_scrolled( GdkEventScroll* event ); // タブを閉じる void slot_tab_close( const int page ); // タブ再読み込み void slot_tab_reload( const int page ); // タブメニュー表示 virtual void slot_tab_menu( int page, int x, int y ); // タブ切り替えメニュー表示 void slot_show_tabswitchmenu(); #if ! GTK_CHECK_VERSION(3,24,6) /// タブ切り替えメニューの位置決め void slot_popup_pos( int& x, int& y, bool& push_in ); #endif // 右クリックメニュー virtual void slot_close_tab(); virtual void slot_lock(); virtual void slot_close_other_tabs(); virtual void slot_close_left_tabs(); virtual void slot_close_right_tabs(); virtual void slot_close_all_tabs(); virtual void slot_close_same_icon_tabs(); virtual void slot_check_update_all_tabs(); virtual void slot_check_update_reload_all_tabs(); virtual void slot_reload_all_tabs(); virtual void slot_cancel_reload_all_tabs(); virtual void slot_open_by_browser(); virtual void slot_copy_url(); virtual void slot_copy_title_url(); void slot_append_favorite(); virtual void append_favorite_impl( const std::string& url ) {} // ページがロックされているかリストで取得 virtual std::list< bool > get_locked(); // タブのロック/アンロック virtual bool is_lockable( const int page ); virtual bool is_locked( const int page ); virtual void lock( const int page ); virtual void unlock( const int page ); // プロパティ表示 void show_preference(); // View履歴戻る / 進む bool back_viewhistory( const std::string& url, const int count ); void back_clicked_viewhistory( const int count ); bool forward_viewhistory( const std::string& url, const int count ); void forward_clicked_viewhistory( const int count ); // View履歴削除 void clear_viewhistory(); // タブの切り替え履歴 const std::list< std::string >& get_switchhistory() const { return m_list_switchhistory; } void set_switchhistory( const std::list< std::string >& hist ){ m_list_switchhistory = hist; } private: void slot_popupmenu_deactivate(); bool back_forward_viewhistory( const std::string& url, const bool back, const int count ); // 移転などでviewのurlを更新 void update_url( const std::string& url_old, const std::string& url_new ); // urlを含むviewの板名を更新 void update_boardname( const std::string& url ); // タブの切り替え履歴を更新 void append_switchhistory( const std::string& url ); void remove_switchhistory( const std::string& url ); std::string get_valid_switchhistory(); void close_view( View* view ); }; } #endif jdim-0.10.1/src/skeleton/backforwardbutton.cpp000066400000000000000000000037241445721505100213660ustar00rootroot00000000000000// ライセンス: GPL2 //#define _DEBUG #include "jddebug.h" #include "backforwardbutton.h" #include "history/historymanager.h" #include "history/viewhistoryitem.h" #include "icons/iconmanager.h" #include using namespace SKELETON; BackForwardButton::BackForwardButton( const std::string& url, const bool back ) : SKELETON::MenuButton( true, ( back ? ICON::BACK : ICON::FORWARD ) ), m_url( url ), m_back( back ) {} void BackForwardButton::set_url( const std::string& url ) { m_url = url; #ifdef _DEBUG std::cout << "BackForwardButton::set_url back = " << m_back << " url = " << m_url << std::endl; #endif if( m_back ){ if( HISTORY::get_history_manager()->can_back_viewhistory( m_url, 1 ) ) set_sensitive( true ); else set_sensitive( false ); } else{ if( HISTORY::get_history_manager()->can_forward_viewhistory( m_url, 1 ) ) set_sensitive( true ); else set_sensitive( false ); } } // // ポップアップメニュー表示 // // virtual void BackForwardButton::show_popupmenu() { #ifdef _DEBUG std::cout << "BackForwardButton::show_popupmenu back = " << m_back << " url = " << m_url << std::endl; #endif std::vector< std::string > items; auto inserter = std::back_inserter( items ); const auto get_title = []( const HISTORY::ViewHistoryItem* item ) { return item->title; }; // 戻る更新 if( m_back ){ std::vector< HISTORY::ViewHistoryItem* >& histitems = HISTORY::get_history_manager()->get_items_back_viewhistory( m_url, MAX_MENU_SIZE ); std::transform( histitems.cbegin(), histitems.cend(), inserter, get_title ); } else{ std::vector< HISTORY::ViewHistoryItem* >& histitems = HISTORY::get_history_manager()->get_items_forward_viewhistory( m_url, MAX_MENU_SIZE ); std::transform( histitems.cbegin(), histitems.cend(), inserter, get_title ); } append_menu( items ); SKELETON::MenuButton::show_popupmenu(); } jdim-0.10.1/src/skeleton/backforwardbutton.h000066400000000000000000000010441445721505100210240ustar00rootroot00000000000000// ライセンス: GPL2 // 「戻る」「進む」履歴付きボタン #ifndef _BACKFORWARDBUTTON_H #define _BACKFORWARDBUTTON_H #include "menubutton.h" namespace SKELETON { class BackForwardButton : public SKELETON::MenuButton { std::string m_url; bool m_back; public: BackForwardButton( const std::string& url, const bool back ); void set_url( const std::string& url ); protected: // ポップアップメニュー表示 void show_popupmenu() override; }; } #endif jdim-0.10.1/src/skeleton/compentry.cpp000066400000000000000000000160671445721505100176710ustar00rootroot00000000000000// ライセンス: GPL2 //#define _DEBUG #include "jddebug.h" #include "compentry.h" #include "control/controlid.h" #include "compmanager.h" using namespace SKELETON; enum { POPUP_SIZE = 5 }; CompletionEntry::CompletionEntry( const int mode ) : m_mode( mode ) , m_enable_changed( true ) , m_popup_win( SKELETON::POPUPWIN_DRAWFRAME ) { m_entry.signal_key_press().connect( sigc::mem_fun( *this, &CompletionEntry::slot_entry_key_press ) ); m_entry.signal_button_press().connect( sigc::mem_fun(*this, &CompletionEntry::slot_entry_button_press ) ); m_entry.signal_operate().connect( sigc::mem_fun( *this, &CompletionEntry::slot_entry_operate ) ); m_entry.signal_activate().connect( sigc::mem_fun( *this, &CompletionEntry::slot_entry_acivate ) ); m_entry.signal_changed().connect( sigc::mem_fun( *this, &CompletionEntry::slot_entry_changed ) ); m_entry.signal_focus_out_event().connect( sigc::mem_fun(*this, &CompletionEntry::slot_entry_focus_out ) ); m_entry.set_max_width_chars( 1 ); m_entry.set_width_chars( 1 ); m_entry.set_hexpand( true ); pack_start( m_entry ); // ポップアップ m_column_record.add( m_column ); m_liststore = Gtk::ListStore::create( m_column_record ); m_treeview.set_model( m_liststore ); m_treeview.append_column( "", m_column ); m_treeview.set_headers_visible( false ); m_treeview.sig_motion_notify().connect( sigc::mem_fun(*this, &CompletionEntry::slot_treeview_motion ) ); m_treeview.sig_button_release().connect( sigc::mem_fun(*this, &CompletionEntry::slot_treeview_button_release ) ); m_scr_win.add( m_treeview ); m_scr_win.set_policy( Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC ); m_scr_win.set_size_request( 1, 1 ); m_popup_win.add( m_scr_win ); set_size_request( 0 ); } CompletionEntry::~CompletionEntry() noexcept = default; // 補完実行 bool CompletionEntry::completion() { bool ret = false; if( m_show_popup ){ Gtk::TreeModel::Row row = m_treeview.get_current_row(); if( row ){ set_text( row[ m_column ] ); ret = true; } } hide_popup(); return ret; } void CompletionEntry::set_text( const Glib::ustring& text ) { m_enable_changed = false; m_entry.set_text( text ); m_entry.set_position( text.length() ); m_enable_changed = true; } void CompletionEntry::grab_focus() { #ifdef _DEBUG std::cout << "CompletionEntry::grab_focus\n"; #endif m_focused = true; m_entry.grab_focus(); } // // ポップアップ表示 // // show_all == true なら候補を全て表示する // void CompletionEntry::show_popup( const bool show_all ) { const int mrg = 2; std::string query = m_entry.get_text(); if( ! show_all && query.empty() ){ hide_popup(); return; } if( show_all ) query = std::string(); CORE::COMPLIST complist = CORE::get_completion_manager()->get_list( m_mode, query ); if( ! complist.size() ) hide_popup(); m_liststore->clear(); Gtk::TreeModel::Row row; for( const std::string& comp : complist ) { if( comp != m_entry.get_text() ) { row = *( m_liststore->append() ); row[ m_column ] = comp; } } const int size = m_liststore->children().size(); if( ! size ){ hide_popup(); return; } // 座標と大きさを計算してポップアップ表示 const int cell_h = m_treeview.get_row_height() + mrg; int x, y; get_window()->get_origin( x, y ); Gdk::Rectangle rect = get_allocation(); m_popup_win.move( x + rect.get_x(), y + rect.get_y() + rect.get_height() ); m_popup_win.resize( get_width(), cell_h * MIN( POPUP_SIZE, size ) + mrg ); m_popup_win.show_all(); m_show_popup = true; m_treeview.unset_cursor(); m_treeview.scroll_to_point( -1, 0 ); Gdk::RGBA rgba; if( !get_style_context()->lookup_color( "theme_bg_color", rgba ) ) { #ifdef _DEBUG std::cout << "ERROR:CompletionEntry::show_popup() " << "lookup theme_bg_color faild." << std::endl; #endif } m_treeview.get_column_cell_renderer( 0 )->property_cell_background_rgba() = rgba; } // ポップアップ閉じる void CompletionEntry::hide_popup() { if( m_show_popup ){ m_popup_win.hide(); m_show_popup = false; } } // entryでキーが押された void CompletionEntry::slot_entry_key_press( int keyval ) { #ifdef _DEBUG std::cout << "CompletionEntry::slot_entry_key_press keyval = " << keyval << std::endl; #endif } // entryでボタンを押した void CompletionEntry::slot_entry_button_press( GdkEventButton* event ) { if( event->type != GDK_BUTTON_PRESS ) return; #ifdef _DEBUG std::cout << "CompletionEntry::slot_entry_button_press button = " << event->button << " focused = " << m_focused << std::endl; #endif if( m_show_popup ) hide_popup(); // 右クリックならコンテキストメニューを優先して補完候補は表示しない else if( m_focused && event->button != 3 ) { show_popup( m_entry.get_text().empty() ); } m_focused = true; } // entry操作 void CompletionEntry::slot_entry_operate( int controlid ) { #ifdef _DEBUG std::cout << "CompletionEntry::slot_entry_operate id = " << controlid << std::endl; #endif switch( controlid ){ case CONTROL::Up: if( ! m_treeview.row_up() ) m_treeview.goto_bottom(); break; case CONTROL::Down: if( ! m_treeview.row_down() ) m_treeview.goto_top(); break; case CONTROL::Cancel: if( m_show_popup ) hide_popup(); else m_sig_operate.emit( controlid ); break; default: m_sig_operate.emit( controlid ); } } // entry からsignal_activateを受け取った void CompletionEntry::slot_entry_acivate() { #ifdef _DEBUG std::cout << "CompletionEntry::slot_entry_acivate\n"; #endif if( ! completion() ) m_sig_activate.emit(); } // entry からsignal_changedを受け取った void CompletionEntry::slot_entry_changed() { if( m_enable_changed ){ if( m_entry.get_text().empty() ) hide_popup(); else show_popup( false ); } m_sig_changed.emit(); } // entryのフォーカスが外れた bool CompletionEntry::slot_entry_focus_out( GdkEventFocus* ) { #ifdef _DEBUG std::cout << "CompletionEntry::slot_entry_focus_out\n"; #endif hide_popup(); m_focused = false; return true; } // ポップアップ内をマウスを動かした bool CompletionEntry::slot_treeview_motion( GdkEventMotion* ) { Gtk::TreeModel::Path path = m_treeview.get_path_under_mouse(); if( path.size() > 0 ) m_treeview.set_cursor( path ); return true; } // ポップアップクリック bool CompletionEntry::slot_treeview_button_release( GdkEventButton* ) { #ifdef _DEBUG std::cout << "CompletionEntry::slot_treeview_button_release\n"; #endif hide_popup(); Gtk::TreeModel::Row row = m_treeview.get_current_row(); if( row ){ set_text( row[ m_column ] ); hide_popup(); } return true; } jdim-0.10.1/src/skeleton/compentry.h000066400000000000000000000054471445721505100173360ustar00rootroot00000000000000// ライセンス: GPL2 // // 補完 Entryクラス // #ifndef _COMPENTRY_H #define _COMPENTRY_H #include "entry.h" #include "popupwinbase.h" #include "treeviewbase.h" #include namespace SKELETON { class CompletionEntry : public Gtk::HBox { typedef sigc::signal< void, int > SIG_OPERATE; typedef sigc::signal< void > SIG_ACTIVATE; typedef sigc::signal< void > SIG_CHANGED; SIG_OPERATE m_sig_operate; SIG_ACTIVATE m_sig_activate; SIG_CHANGED m_sig_changed; int m_mode; JDEntry m_entry; bool m_enable_changed; bool m_focused{}; // ポップアップ bool m_show_popup{}; PopupWinBase m_popup_win; Gtk::ScrolledWindow m_scr_win; JDTreeViewBase m_treeview; Gtk::TreeModelColumn< Glib::ustring > m_column; Gtk::TreeModel::ColumnRecord m_column_record; Glib::RefPtr< Gtk::ListStore > m_liststore; public: // mode は補完モード ( compmanager.h 参照 ) explicit CompletionEntry( const int mode ); ~CompletionEntry() noexcept; SIG_OPERATE signal_operate(){ return m_sig_operate; } SIG_ACTIVATE signal_activate(){ return m_sig_activate; } SIG_CHANGED signal_changed(){ return m_sig_changed; } // m_entry の入力コントローラのモード設定 // 補完モード(m_mode)とは異なる void add_control_mode( int mode ){ m_entry.add_mode( mode ); } // 補完実行 bool completion(); Glib::ustring get_text() const { return m_entry.get_text(); } void set_text( const Glib::ustring& text ); void grab_focus(); void modify_font( const Pango::FontDescription& pfd ) { m_entry.override_font( pfd ); } private: // ポップアップ表示 // show_all == true なら候補を全て表示する void show_popup( const bool show_all ); // ポップアップ閉じる void hide_popup(); // entryでキーが押された void slot_entry_key_press( int keyval ); // entryでボタンを押した void slot_entry_button_press( GdkEventButton* event ); // entry操作 void slot_entry_operate( int controlid ); // entry からsignal_activateを受け取った void slot_entry_acivate(); // entry からsignal_changedを受け取った void slot_entry_changed(); // entryのフォーカスが外れた bool slot_entry_focus_out( GdkEventFocus* ); // ポップアップ内をマウスを動かした bool slot_treeview_motion( GdkEventMotion* ); // ポップアップクリック bool slot_treeview_button_release( GdkEventButton* ); }; } #endif jdim-0.10.1/src/skeleton/detaildiag.cpp000066400000000000000000000031161445721505100177270ustar00rootroot00000000000000// ライセンス: GPL2 //#define _DEBUG #include "jddebug.h" #include "gtkmmversion.h" #include "detaildiag.h" #include "view.h" #include "viewfactory.h" using namespace SKELETON; DetailDiag::DetailDiag( Gtk::Window* parent, const std::string& url, const bool add_cancel, const std::string& message, const std::string& tab_message, const std::string& detail_html, const std::string& tab_detail ) : SKELETON::PrefDiag( parent, url, add_cancel ) , m_message( message ) { m_message.set_width_chars( 60 ); m_message.set_line_wrap( true ); m_message.property_margin() = 8; m_message.set_selectable( true ); m_message.property_can_focus() = false; m_detail.reset( CORE::ViewFactory( CORE::VIEW_ARTICLEINFO, get_url() ) ); if( ! detail_html.empty() ) m_detail->set_command( "append_html", detail_html ); m_notebook.append_page( m_message, tab_message ); m_notebook.append_page( *m_detail, tab_detail ); m_notebook.signal_switch_page().connect( sigc::mem_fun( *this, &DetailDiag::slot_switch_page ) ); get_content_area()->pack_start( m_notebook ); show_all_children(); grab_ok(); } // メンバーに不完全型のスマートポインターがあるためデストラクタはinlineにできない DetailDiag::~DetailDiag() noexcept = default; void DetailDiag::timeout() { if( m_detail ) m_detail->clock_in(); } void DetailDiag::slot_switch_page( Gtk::Widget*, guint page ) { if( get_notebook().get_nth_page( page ) == m_detail.get() ) m_detail->redraw_view(); } jdim-0.10.1/src/skeleton/detaildiag.h000066400000000000000000000017031445721505100173740ustar00rootroot00000000000000// ライセンス: GPL2 // 詳細表示ダイアログ #ifndef _DETAILDIAG_H #define _DETAILDIAG_H #include "prefdiag.h" #include namespace SKELETON { class View; class DetailDiag : public SKELETON::PrefDiag { Gtk::Notebook m_notebook; Gtk::Label m_message; std::unique_ptr m_detail; public: DetailDiag( Gtk::Window* parent, const std::string& url, const bool add_cancel, const std::string& message, const std::string& tab_message, const std::string& detail_html, const std::string& tab_detail ); ~DetailDiag() noexcept; protected: Gtk::Notebook& get_notebook(){ return m_notebook; } SKELETON::View* get_detail(){ return m_detail.get(); } private: virtual void slot_switch_page( Gtk::Widget*, guint page ); void timeout() override; }; } #endif jdim-0.10.1/src/skeleton/dispatchable.cpp000066400000000000000000000011551445721505100202640ustar00rootroot00000000000000// ライセンス: GPL2 //#define _DEBUG #include "jddebug.h" #include "dispatchable.h" #include "dispatchmanager.h" using namespace SKELETON; Dispatchable::Dispatchable() : m_dispatchable( true ) {} Dispatchable::~Dispatchable() { #ifdef _DEBUG std::cout << "Dispatchable::~Dispatchable\n"; #endif set_dispatchable( false ); } void Dispatchable::set_dispatchable( const bool dispatchable ) { m_dispatchable = dispatchable; if( ! m_dispatchable ) CORE::get_dispmanager()->remove( this ); } void Dispatchable::dispatch() { if( m_dispatchable ) CORE::get_dispmanager()->add( this ); } jdim-0.10.1/src/skeleton/dispatchable.h000066400000000000000000000020121445721505100177220ustar00rootroot00000000000000// ライセンス: GPL2 // // Dispatchクラス // // Core::DispatchManagerと組み合わせて使う。詳しくはCore::DispatchManagerの説明を見ること // // なお派生クラスのデストラクタの中からdispatchを呼ぶと落ちるので、特にスレッドを使用している // 派生クラスのデストラクタの先頭に set_dispatchable( false ) を入れてdispatch不可にすること // #ifndef _DISPATCHABLE_H #define _DISPATCHABLE_H #include namespace CORE { class DispatchManager; } namespace SKELETON { class Dispatchable { friend class CORE::DispatchManager; bool m_dispatchable; public: Dispatchable(); virtual ~Dispatchable(); protected: void dispatch(); void set_dispatchable( const bool dispatchable ); // dispacth() で DispatchManager に登録されて // DispatchManager が callback_dispatch() を呼び戻す virtual void callback_dispatch() = 0; }; } #endif jdim-0.10.1/src/skeleton/dragnote.cpp000066400000000000000000000410651445721505100174500ustar00rootroot00000000000000// ライセンス: GPL2 //#define _DEBUG #include "jddebug.h" #include "dragnote.h" #include "tablabel.h" #include "toolbar.h" #include "view.h" #include "iconpopup.h" #include "icons/iconmanager.h" #include "control/controlid.h" #include "command.h" #include "dndmanager.h" #include "session.h" #include using namespace SKELETON; DragableNoteBook::DragableNoteBook() : Gtk::VBox() , m_notebook_tab( this ) , m_notebook_toolbar( this ) , m_notebook_view( this ) , m_bt_tabswitch( this ) , m_show_tabs{ true } , m_show_toolbar{ true } , m_page( -1 ) { set_spacing( 0 ); m_notebook_tab.signal_switch_page().connect( sigc::mem_fun( *this, &DragableNoteBook::slot_switch_page_tab ) ); m_notebook_tab.sig_button_press().connect( sigc::mem_fun( *this, &DragableNoteBook::slot_button_press_event ) ); m_notebook_tab.sig_button_release().connect( sigc::mem_fun( *this, &DragableNoteBook::slot_button_release_event ) ); m_notebook_tab.sig_tab_drag_motion().connect( sigc::mem_fun(*this, &DragableNoteBook::slot_drag_motion ) ); m_notebook_tab.sig_scroll_event().connect( sigc::mem_fun(*this, &DragableNoteBook::slot_scroll_event ) ); m_hbox_tab.pack_start( m_notebook_tab ); m_hbox_tab.pack_start( m_bt_tabswitch, Gtk::PACK_SHRINK ); m_bt_tabswitch.set_tooltip_text( "タブの一覧表示" ); pack_start( m_hbox_tab, Gtk::PACK_SHRINK ); pack_start( m_notebook_toolbar, Gtk::PACK_SHRINK ); pack_start( m_notebook_view ); show_all_children(); } // メンバーに不完全型のスマートポインターがあるためデストラクタはinlineにできない DragableNoteBook::~DragableNoteBook() noexcept = default; // // クロック入力 // void DragableNoteBook::clock_in() { m_notebook_tab.clock_in(); } void DragableNoteBook::set_show_tabs( bool show_tabs ) { #ifdef _DEBUG std::cout << "DragableNoteBook::set_show_tabs show_tabs = " << show_tabs << std::endl; #endif if( ! show_tabs && m_show_tabs ){ remove( m_hbox_tab ); m_show_tabs = false; } else if( show_tabs && ! m_show_tabs ){ if( m_show_toolbar && m_notebook_toolbar.get_n_pages() ) remove( m_notebook_toolbar ); remove( m_notebook_view ); pack_start( m_hbox_tab, Gtk::PACK_SHRINK ); if( m_show_toolbar && m_notebook_toolbar.get_n_pages() ) pack_start( m_notebook_toolbar, Gtk::PACK_SHRINK ); pack_start( m_notebook_view ); m_show_tabs = true; } } void DragableNoteBook::set_scrollable( bool scrollable ) { m_notebook_tab.set_scrollable( scrollable ); } int DragableNoteBook::get_n_pages() const { return m_notebook_view.get_n_pages(); } Gtk::Widget* DragableNoteBook::get_nth_page( int page_num ) { return m_notebook_view.get_nth_page( page_num ); } int DragableNoteBook::page_num( const Gtk::Widget& child ) const { return m_notebook_view.page_num( child ); } int DragableNoteBook::get_current_page() const { return m_notebook_view.get_current_page(); } void DragableNoteBook::set_current_page( int page_num ) { if( get_current_page() == page_num ) return; if( page_num >= get_n_pages() ) page_num = get_n_pages()-1; m_notebook_tab.set_current_page( page_num ); m_notebook_view.set_current_page( page_num ); SKELETON::View* view = dynamic_cast< View* >( get_nth_page( page_num ) ); if( view ) set_current_toolbar( view->get_id_toolbar(), view ); } // // ページのappendとinsert // int DragableNoteBook::append_page( const std::string& url, Gtk::Widget& child ) { m_notebook_view.append_page( child ); m_bt_tabswitch.show_button(); SKELETON::TabLabel* tablabel = Gtk::manage( create_tablabel( url ) ); return m_notebook_tab.append_tab( *tablabel ); } int DragableNoteBook::insert_page( const std::string& url, Gtk::Widget& child, int page ) { m_notebook_view.insert_page( child, page ); m_bt_tabswitch.show_button(); SKELETON::TabLabel* tablabel = Gtk::manage( create_tablabel( url ) ); return m_notebook_tab.insert_tab( *tablabel, page ); } // // タブの文字列取得 // const std::string& DragableNoteBook::get_tab_fulltext( const int page ) { return m_notebook_tab.get_tab_fulltext( page ); } // // タブに文字列をセットとタブ幅調整 // void DragableNoteBook::set_tab_fulltext( const std::string& str, const int page ) { m_notebook_tab.set_tab_fulltext( str, page ); } // // ページ取り除き // void DragableNoteBook::remove_page( const int page, const bool adjust_tab ) { // タブはGtk::manage()の効果でdeleteされる m_notebook_tab.remove_tab( page, adjust_tab ); m_notebook_view.remove_page( page ); if( ! get_n_pages() ) m_bt_tabswitch.hide_button(); } // // ツールバー取得 // SKELETON::ToolBar* DragableNoteBook::get_toolbar( int page ) { return dynamic_cast< SKELETON::ToolBar* >( m_notebook_toolbar.get_nth_page( page ) ); } // // ツールバー全体を表示 // // タブにビューが表示されたら admin から呼び出される // void DragableNoteBook::show_toolbar() { #ifdef _DEBUG std::cout << "DragableNoteBook::show_toolbar\n"; #endif if( ! m_show_toolbar && m_notebook_toolbar.get_n_pages() ){ remove( m_notebook_view ); pack_start( m_notebook_toolbar, Gtk::PACK_SHRINK ); pack_start( m_notebook_view ); m_show_toolbar = true; } } // // ツールバー全体を非表示 // // タブに全てのビューが無くなったら admin から呼び出される // void DragableNoteBook::hide_toolbar() { #ifdef _DEBUG std::cout << "DragableNoteBook::hide_toolbar\n"; #endif if( m_show_toolbar && m_notebook_toolbar.get_n_pages() ){ remove( m_notebook_toolbar ); m_show_toolbar = false; } } // // ツールバーセット // // 各Adminクラスの virtual void show_toolbar() でセットされる // void DragableNoteBook::append_toolbar( Gtk::Widget& toolbar ) { #ifdef _DEBUG std::cout << "DragableNoteBook::append_toolbar\n"; #endif m_notebook_toolbar.append_page( toolbar ); } // // ツールバー切り替え // void DragableNoteBook::set_current_toolbar( const int id_toolbar, SKELETON::View* view ) { // タブ操作中 if( SESSION::is_tab_operating( view->get_url_admin() ) ) return; #ifdef _DEBUG std::cout << "DragableNoteBook::set_current_toolbar id = " << id_toolbar << " / " << m_notebook_toolbar.get_n_pages() << std::endl << "url = " << view->get_url() << std::endl; #endif if( m_notebook_toolbar.get_n_pages() <= id_toolbar ) return; m_notebook_toolbar.set_current_page( id_toolbar ); // ツールバーのラベルなどの情報を更新 SKELETON::ToolBar* toolbar = get_toolbar( id_toolbar ); if( toolbar ){ toolbar->set_view( view ); toolbar->show_toolbar(); } } int DragableNoteBook::get_current_toolbar() const { return m_notebook_toolbar.get_current_page(); } // // ツールバー内の検索ボックスにフォーカスを移す // void DragableNoteBook::focus_toolbar_search() { SKELETON::ToolBar* toolbar = get_toolbar( m_notebook_toolbar.get_current_page() ); if( toolbar ) toolbar->focus_entry_search(); } // // ツールバーURL更新 // void DragableNoteBook::update_toolbar_url( const std::string& url_old, const std::string& url_new ) { for( int i = 0; i < m_notebook_toolbar.get_n_pages(); ++i ){ SKELETON::ToolBar* toolbar = get_toolbar( i ); if( toolbar && toolbar->get_url() == url_old ) toolbar->set_url( url_new ); } } // // ツールバーボタン表示更新 // void DragableNoteBook::update_toolbar_button() { for( int i = 0; i < m_notebook_toolbar.get_n_pages(); ++i ){ SKELETON::ToolBar* toolbar = get_toolbar( i ); if( toolbar ) toolbar->update_button(); } } // // タブのアイコンを取得する // int DragableNoteBook::get_tabicon( const int page ) { SKELETON::TabLabel* tablabel = m_notebook_tab.get_tablabel( page ); if( tablabel ) return tablabel->get_id_icon(); return ICON::NONE; } // // タブにアイコンをセットする // void DragableNoteBook::set_tabicon( const std::string& iconname, const int page, const int id ) { SKELETON::TabLabel* tablabel = m_notebook_tab.get_tablabel( page ); if( tablabel && id != ICON::NONE ) tablabel->set_id_icon( id ); } // // タブ作成 // SKELETON::TabLabel* DragableNoteBook::create_tablabel( const std::string& url ) { SKELETON::TabLabel *tablabel = new SKELETON::TabLabel( url ); // ドラッグ設定 GdkEventButton event; m_control.get_eventbutton( CONTROL::DragStartButton, event ); tablabel->set_dragable( m_dragable, event.button ); tablabel->sig_tab_drag_begin().connect( sigc::mem_fun(*this, &DragableNoteBook::slot_drag_begin ) ); tablabel->sig_tab_drag_data_get().connect( sigc::mem_fun(*this, &DragableNoteBook::slot_drag_data_get ) ); tablabel->sig_tab_drag_end().connect( sigc::mem_fun(*this, &DragableNoteBook::slot_drag_end ) ); return tablabel; } // // タブの幅を固定するか // void DragableNoteBook::set_fixtab( bool fix ) { m_notebook_tab.set_fixtab( fix ); } // // タブ幅調整 // bool DragableNoteBook::adjust_tabwidth() { return m_notebook_tab.adjust_tabwidth(); } // // notebook_tabのタブが切り替わったときに呼ばれるslot // void DragableNoteBook::slot_switch_page_tab( Gtk::Widget* bookpage, guint page ) { // view も切り替える m_notebook_view.set_current_page( page ); // ツールバー(枠) 再描画 m_notebook_toolbar.queue_draw(); m_sig_switch_page.emit( bookpage, page ); } // // タブの上でボタンを押した // bool DragableNoteBook::slot_button_press_event( GdkEventButton* event ) { // ボタンを押した時点でのページ番号を記録しておく m_page = m_notebook_tab.get_page_under_mouse(); m_dragging_tab = false; // ダブルクリック // button_release_eventでは event->type に必ず GDK_BUTTON_RELEASE が入る m_dblclick = false; if( event->type == GDK_2BUTTON_PRESS ) m_dblclick = true; #ifdef _DEBUG std::cout << "DragableNoteBook::on_button_press_event page = " << m_page << std::endl; std::cout << "x = " << (int)event->x_root << " y = " << (int)event->y_root << " dblclick = " << m_dblclick << std::endl; #endif if( m_page >= 0 && m_page < get_n_pages() ){ // ページ切替え if( m_control.button_alloted( event, CONTROL::ClickButton ) ){ set_current_page( m_page ); m_sig_tab_clicked.emit( m_page ); } return true; } else m_page = -1; return false; } // // タブの上でボタンを離した // bool DragableNoteBook::slot_button_release_event( GdkEventButton* event ) { const int page = m_notebook_tab.get_page_under_mouse(); const int x = (int)event->x_root; const int y = (int)event->y_root; #ifdef _DEBUG std::cout << "DragableNoteBook::on_button_release_event\n"; std::cout << "x = " << (int)event->x_root << " y = " << (int)event->y_root << std::endl; #endif if( ! m_dragging_tab && m_page >= 0 && m_page < get_n_pages() ){ // ダブルクリックの処理のため一時的にtypeを切替える GdkEventType type_copy = event->type; if( m_dblclick ) event->type = GDK_2BUTTON_PRESS; // タブを閉じる if( page == m_page && m_control.button_alloted( event, CONTROL::CloseTabButton ) ){ m_sig_tab_close.emit( m_page ); // タブにページが残ってなかったらtrueをreturnしないと落ちる // TabNotebook::on_button_release_event() も参照せよ // // (注意) なぜか m_notebook_tab.get_n_pages() < m_notebook_view.get_n_pages() の時があって // 以前の様に // if( get_n_pages() == 0 ) // という条件では m_notebook_tab.get_n_pages() = 0 でも get_n_pages() != 0 になって落ちることがある if( m_notebook_tab.get_n_pages() == 0 ){ m_page = -1; return true; } } // タブを再読み込み else if( m_control.button_alloted( event, CONTROL::ReloadTabButton ) ) m_sig_tab_reload.emit( m_page ); // ポップアップメニュー else if( m_control.button_alloted( event, CONTROL::PopupmenuButton ) ) m_sig_tab_menu.emit( m_page, x, y ); m_page = -1; event->type = type_copy; } return false; } // notebook_tab の上でホイールを回した bool DragableNoteBook::slot_scroll_event( GdkEventScroll* event ) { #ifdef _DEBUG std::cout << "DragableNoteBook::slot_scroll_event direction = " << event->direction << " page = " << get_current_page() << std::endl; #endif bool ret = false; int next_page; if( event->direction == GDK_SCROLL_UP ) { next_page = get_current_page() - 1; // If negative, the last page will be used. ret = true; } else if( event->direction == GDK_SCROLL_DOWN ) { next_page = ( get_current_page() + 1 ) % get_n_pages(); ret = true; } else if( event->direction == GDK_SCROLL_SMOOTH ) { constexpr double smooth_scroll_factor{ 4.0 }; m_smooth_dy += smooth_scroll_factor * event->delta_y; if( m_smooth_dy < -1.0 ) { next_page = get_current_page() - 1; // If negative, the last page will be used. ret = true; m_smooth_dy = 0.0; } else if( m_smooth_dy > 1.0 ) { next_page = ( get_current_page() + 1 ) % get_n_pages(); ret = true; m_smooth_dy = 0.0; } } if( ret ) { set_current_page( next_page ); } m_sig_tab_scrolled.emit( event ); return ret; } // // タブのドラッグを開始 // void DragableNoteBook::slot_drag_begin() { #ifdef _DEBUG std::cout << "DragableNoteBook::slot_drag_begin page = " << m_page << std::endl; #endif CORE::DND_Begin(); m_dragging_tab = true; } // // タブをドラッグ中 // // 矢印アイコンをタブの上に表示する // void DragableNoteBook::slot_drag_motion( const int page, const int tab_x, const int tab_y, const int tab_width ) { #ifdef _DEBUG std::cout << "DragableNoteBook::slot_drag_motion page = " << page << " tab_x = " << tab_x << " tab_y = " << tab_y << " tab_w " << tab_width << std::endl; #endif if( page < 0 || page == m_page ){ if( m_down_arrow ) m_down_arrow->hide(); } else if( m_dragging_tab ){ if( ! m_down_arrow ) m_down_arrow = std::make_unique( ICON::DOWN ); // HACK: サブウインドウにタブ機能がないためメインウインドウに決め打ちしている m_down_arrow->set_transient_for( *CORE::get_mainwindow() ); m_down_arrow->show(); const int space = 4; int x, y; m_notebook_tab.get_window()->get_origin( x, y ); x += tab_x - m_down_arrow->get_img_width()/2; y += tab_y - m_down_arrow->get_img_height() - space; if( page > m_page ) x += tab_width; m_down_arrow->move( x , y ); } } // // D&Dで受信側がデータ送信を要求してきた // void DragableNoteBook::slot_drag_data_get( Gtk::SelectionData& selection_data ) { #ifdef _DEBUG std::cout << "DragableNoteBook::slot_drag_data_get target = " << selection_data.get_target() << " page = " << m_page << std::endl; #endif // お気に入りがデータ送信を要求してきた if( selection_data.get_target() == DNDTARGET_FAVORITE ) m_sig_drag_data_get.emit( selection_data, m_page ); // タブの入れ替え処理 else if( selection_data.get_target() == DNDTARGET_TAB ){ int page = m_notebook_tab.get_page_under_mouse(); if( page >= m_notebook_tab.get_n_pages() ) page = m_notebook_tab.get_n_pages()-1; // ドラッグ前とページが変わっていたら入れ替え if( m_page != -1 && page != -1 && m_page != page ){ #ifdef _DEBUG std::cout << "reorder_chiled " << m_page << " -> " << page << std::endl; #endif m_notebook_tab.reorder_child( m_page, page ); m_notebook_tab.queue_draw(); m_notebook_view.reorder_child( *m_notebook_view.get_nth_page( m_page ), page ); m_page = -1; } if( m_down_arrow ) m_down_arrow->hide(); } } // // タブのドラッグを終了 // void DragableNoteBook::slot_drag_end() { #ifdef _DEBUG std::cout << "DragableNoteBook::slot_drag_end\n"; #endif m_dragging_tab = false; if( m_down_arrow ) m_down_arrow->hide(); CORE::DND_End(); } jdim-0.10.1/src/skeleton/dragnote.h000066400000000000000000000141461445721505100171150ustar00rootroot00000000000000// ライセンス: GPL2 // // タブをドラッグしてページを入れ替え可能なNoteBook // // DragableNoteBook は 下の3つの notebook から構成されている // // タブ : TabNotebook // ツールバー : ToolBarNotebook // ビュー : ViewNotebook #ifndef _DRAGNOTE_H #define _DRAGNOTE_H #include "tabnote.h" #include "toolbarnote.h" #include "viewnote.h" #include "tabswitchbutton.h" #include "control/control.h" #include #include namespace SKELETON { class View; class ToolBar; class TabLabel; class IconPopup; typedef sigc::signal< void, Gtk::Widget*, int > SIG_SWITCH_PAGE; typedef sigc::signal< void, int > SIG_TAB_CLICKED; typedef sigc::signal< void, int > SIG_TAB_CLOSE; typedef sigc::signal< void, int > SIG_TAB_RELOAD; typedef sigc::signal< void, int, int , int > SIG_TAB_MENU; typedef sigc::signal< void, GdkEventScroll* > SIG_TAB_SCROLLED; typedef sigc::signal< void, Gtk::SelectionData&, const int > SIG_DRAG_DATA_GET; // DragableNoteBook を構成している各Notebookの高さ // 及びタブの高さや位置の情報 struct Alloc_NoteBook { int y_tab; int x_tab; int height_tab; int width_tab; int y_tabbar; int height_tabbar; int y_toolbar; int height_toolbar; int x_box; int y_box; int width_box; int height_box; }; class DragableNoteBook : public Gtk::VBox { SIG_SWITCH_PAGE m_sig_switch_page; SIG_TAB_CLICKED m_sig_tab_clicked; SIG_TAB_CLOSE m_sig_tab_close; SIG_TAB_RELOAD m_sig_tab_reload; SIG_TAB_MENU m_sig_tab_menu; SIG_TAB_SCROLLED m_sig_tab_scrolled; SIG_DRAG_DATA_GET m_sig_drag_data_get; // DragableNoteBook は 下の3つのノートブックから出来ている TabNotebook m_notebook_tab; // タブ ToolBarNotebook m_notebook_toolbar; // ツールバー ViewNotebook m_notebook_view; // ビュー Gtk::HBox m_hbox_tab; TabSwitchButton m_bt_tabswitch; // タブの切り替えボタン bool m_show_tabs; bool m_show_toolbar; int m_page; // タブをドラッグ中 bool m_dragging_tab{}; bool m_dblclick{}; // 入力コントローラ CONTROL::Control m_control; bool m_dragable{}; std::unique_ptr m_down_arrow; double m_smooth_dy{}; // GDK_SCROLL_SMOOTH のスクロール変化量 public: SIG_SWITCH_PAGE signal_switch_page(){ return m_sig_switch_page; } SIG_TAB_CLICKED sig_tab_clicked() { return m_sig_tab_clicked; } SIG_TAB_CLOSE sig_tab_close() { return m_sig_tab_close; } SIG_TAB_RELOAD sig_tab_reload(){ return m_sig_tab_reload; } SIG_TAB_MENU sig_tab_menu() { return m_sig_tab_menu; } SIG_TAB_SCROLLED sig_tab_scrolled(){ return m_sig_tab_scrolled; } SIG_DRAG_DATA_GET sig_drag_data_get() { return m_sig_drag_data_get; } DragableNoteBook(); ~DragableNoteBook() noexcept; void clock_in(); bool get_show_tabs() const { return m_show_tabs; } void set_show_tabs( bool show_tabs ); void set_scrollable( bool scrollable ); int get_n_pages() const; Gtk::Widget* get_nth_page( int page_num ); int page_num( const Gtk::Widget& child ) const; int get_current_page() const; void set_current_page( int page_num ); int append_page( const std::string& url, Gtk::Widget& child ); int insert_page( const std::string& url, Gtk::Widget& child, int page ); void remove_page( const int page, const bool adust_tab ); // ツールバー関係 // 各Adminクラスの virtual void show_toolbar()でツールバーを作成してappend_toolbar()で登録する void show_toolbar(); void hide_toolbar(); void append_toolbar( Gtk::Widget& toolbar ); void set_current_toolbar( const int id_toolbar, SKELETON::View* view ); int get_current_toolbar() const; void focus_toolbar_search(); // ツールバー内の検索entryにフォーカスを移す void update_toolbar_url( const std::string& url_old, const std::string& url_new ); void update_toolbar_button(); // タブの文字列取得/セット const std::string& get_tab_fulltext( const int page ); void set_tab_fulltext( const std::string& str, const int page ); // タブのアイコン取得/セット int get_tabicon( const int page ); void set_tabicon( const std::string& iconname, const int page, const int icon ); // ドラッグ可/不可切り替え(デフォルト false ); void set_dragable( bool dragable ){ m_dragable = dragable; } // タブの幅を固定するか void set_fixtab( bool fix ); // タブ幅調整 bool adjust_tabwidth(); // タブ切り替えボタン Gtk::Button& get_tabswitch_button(){ return m_bt_tabswitch.get_button(); } private: // ツールバー取得 SKELETON::ToolBar* get_toolbar( int page ); // タブ作成 SKELETON::TabLabel* create_tablabel( const std::string& url ); // notebook_tabのタブが切り替わったときに呼び出されるslot void slot_switch_page_tab( Gtk::Widget*, guint page ); // notebook_tab の上でボタンを押した/離した bool slot_button_press_event( GdkEventButton* event ); bool slot_button_release_event( GdkEventButton* event ); // notebook_tab の上でホイールを回した bool slot_scroll_event( GdkEventScroll* event ); protected: // コントローラ CONTROL::Control& get_control(){ return m_control; } // タブのドラッグ・アンド・ドロップ処理 void slot_drag_begin(); void slot_drag_motion( const int page, const int tab_x, const int tab_y, const int tab_width ); void slot_drag_data_get( Gtk::SelectionData& selection_data ); void slot_drag_end(); }; } #endif jdim-0.10.1/src/skeleton/dragtreeview.cpp000066400000000000000000000407131445721505100203340ustar00rootroot00000000000000// ライセンス: GPL2 //#define _DEBUG #include "jddebug.h" #include "gtkmmversion.h" #include "dragtreeview.h" #include "view.h" #include "popupwin.h" #include "config/globalconf.h" #include "jdlib/miscutil.h" #include "control/controlid.h" #include "command.h" #include "colorid.h" #include "dndmanager.h" #include "session.h" #ifndef MAX #define MAX( a, b ) ( a > b ? a : b ) #endif #ifndef MIN #define MIN( a, b ) ( a < b ? a : b ) #endif using namespace SKELETON; constexpr const char* DragTreeView::s_css_classname; DragTreeView::DragTreeView( const std::string& url, const std::string& dndtarget, const bool use_usr_fontcolor, const std::string& fontname, const int colorid_text, const int colorid_bg, const int colorid_bg_even ) : JDTreeViewBase() , m_url( url ) , m_dndtarget( dndtarget ) { #ifdef _DEBUG std::cout << "DragTreeView::DragTreeView\n"; #endif set_enable_search( false ); add_events( Gdk::LEAVE_NOTIFY_MASK ); auto context = get_style_context(); context->add_class( s_css_classname ); context->add_provider( m_provider, GTK_STYLE_PROVIDER_PRIORITY_APPLICATION ); if( use_usr_fontcolor ){ init_color( colorid_text, colorid_bg, colorid_bg_even ); init_font( fontname ); } get_selection()->set_mode( Gtk::SELECTION_MULTIPLE ); get_selection()->signal_changed().connect( sigc::mem_fun( *this, &DragTreeView::slot_selection_changed ) ); // D&D 設定 std::vector< Gtk::TargetEntry > targets; targets.push_back( Gtk::TargetEntry( get_dndtarget(), Gtk::TARGET_SAME_APP, 0 ) ); // ドラッグ開始ボタン設定 Gdk::ModifierType type = Gdk::BUTTON1_MASK; GdkEventButton event; m_control.get_eventbutton( CONTROL::DragStartButton, event ); switch( event.button ){ case 1: type = Gdk::BUTTON1_MASK; break; case 2: type = Gdk::BUTTON2_MASK; break; case 3: type = Gdk::BUTTON3_MASK; break; case 4: type = Gdk::BUTTON4_MASK; break; case 5: type = Gdk::BUTTON5_MASK; break; } drag_source_set( targets, type ); } // メンバーに不完全型のスマートポインターがあるためデストラクタはinlineにできない DragTreeView::~DragTreeView() noexcept = default; // // 他のアプリからの text/url-list のドロップを有効にする // ドロップされるとSIG_DROPPED_URI_LIST を発行する // void DragTreeView::set_enable_drop_uri_list() { std::vector< Gtk::TargetEntry > targets_drop; targets_drop.push_back( Gtk::TargetEntry( "text/uri-list" ) ); // ドロップされると on_drag_data_received() が呼び出される drag_dest_set( targets_drop, Gtk::DEST_DEFAULT_ALL, Gdk::ACTION_COPY ); } // // 再描画 // void DragTreeView::redraw_view() { // 今のところ特に何もしない // ポップアップが表示されていたらポップアップを再描画 if( is_popup_shown() && m_popup_win->view() ) m_popup_win->view()->redraw_view(); } // // 色初期化 // void DragTreeView::init_color( const int colorid_text, const int colorid_bg, const int colorid_bg_even ) { if( CONFIG::get_use_tree_gtkrc() ) { m_use_bg_even = false; m_provider->load_from_data( u8"" ); return; } // 文字色 m_color_text.set( CONFIG::get_color( colorid_text ) ); // 背景色 m_color_bg.set( CONFIG::get_color( colorid_bg ) ); m_use_bg_even = ! ( CONFIG::get_color( colorid_bg ) == CONFIG::get_color( colorid_bg_even ) ); m_color_bg_even.set( CONFIG::get_color( colorid_bg_even ) ); // TreeViewの偶数/奇数行を指定するCSSセレクタは動作しないバージョンが // 存在するため偶数行の背景色設定は既存のコードをそのまま使う // https://gitlab.gnome.org/GNOME/gtk/issues/581 try { m_provider->load_from_data( Glib::ustring::compose( u8".%1.view:not(:selected) { color: %2; background-color: %3; }", s_css_classname, m_color_text.to_string(), m_color_bg.to_string() ) ); } catch( Gtk::CssProviderError& err ) { #ifdef _DEBUG std::cout << "ERROR:DragTreeView::init_color load from data failed: " << err.what() << std::endl; #endif } } // // フォント初期化 // void DragTreeView::init_font( const std::string& fontname ) { Pango::FontDescription pfd( fontname ); pfd.set_weight( Pango::WEIGHT_NORMAL ); override_font( pfd ); } // // クロック入力 // void DragTreeView::clock_in() { // ポップアップ表示中はそちらにクロックを回す if( is_popup_shown() && m_popup_win->view() ){ m_popup_win->view()->clock_in(); return; } } // // ポップアップが表示されていてかつマウスがその上にあるか // bool DragTreeView::is_mouse_on_popup() { if( ! is_popup_shown() ) return false; if( ! m_popup_win->view() ) return false; return m_popup_win->view()->is_mouse_on_view(); } // // ポップアップウィンドウ表示 // void DragTreeView::show_popup( const std::string& url, View* view ) { const int mrg_x = 10; const int mrg_y = 10; #ifdef _DEBUG std::cout << "DragTreeView::show_popup url = " << url << std::endl; #endif delete_popup(); m_popup_win = std::make_unique( this, view, mrg_x, mrg_y ); m_popup_win->signal_leave_notify_event().connect( sigc::mem_fun( *this, &DragTreeView::slot_popup_leave_notify_event ) ); m_popup_win->sig_hide_popup().connect( sigc::mem_fun( *this, &DragTreeView::hide_popup ) ); m_pre_popup_url = url; m_popup_shown = true; } // // popup windowの外にポインタが出た // bool DragTreeView::slot_popup_leave_notify_event( GdkEventCrossing* event ) { #ifdef _DEBUG std::cout << "DragTreeView::slot_popup_leave_notify_event\n"; #endif // クリックしたときやホイールを回すと event->mode に GDK_CROSSING_GRAB // か GDK_CROSSING_UNGRAB がセットされてイベントが発生する場合がある if( event->mode == GDK_CROSSING_GRAB ) return false; if( event->mode == GDK_CROSSING_UNGRAB ) return false; // ポインタがポップアップ上にある場合はポップアップは消さない // 時々ポインタがポップアップの上にあってもイベントが発生する場合がある if( is_mouse_on_popup() ) return false; hide_popup(); return true; } // // ポップアップを隠す // void DragTreeView::hide_popup() { if( ! is_popup_shown() ) return; // ポップアップメニューが表示されてたらhideしない if( SESSION::is_popupmenu_shown() ) return; #ifdef _DEBUG std::cout << "DragTreeView::hide_popup\n"; #endif m_popup_win->hide(); if( m_popup_win->view() ) m_popup_win->view()->stop(); if( get_path_under_mouse().empty() ) m_pre_popup_url = std::string(); m_popup_shown = false; } // // ポップアップウィンドウ削除 // void DragTreeView::delete_popup() { if( m_popup_win ){ #ifdef _DEBUG std::cout << "DragTreeView::delete_popup\n"; #endif m_popup_win.reset(); m_pre_popup_url.clear(); m_popup_shown = false; } } // // マウスボタンを押した // bool DragTreeView::on_button_press_event( GdkEventButton* event ) { Gtk::TreeModel::Path path = get_path_under_xy( (int)event->x, (int)event->y ); m_dragging = false; m_selection_canceled = false; sig_button_press().emit( event ); // ドラッグして範囲選択 // m_path_dragstart が empty でない時に範囲選択を行う // on_motion_notify_event()も参照せよ if( m_control.button_alloted( event, CONTROL::TreeRowSelectionButton ) ) m_path_dragstart = m_path_dragpre = path; else m_path_dragstart = m_path_dragpre = Gtk::TreeModel::Path(); // 複数行選択時の動作 if( get_selection()->get_selected_rows().size() >= 2 ){ // D&Dのため、ctrlやshiftなしで普通にクリックしたときも選択解除しない if( !( event->state & GDK_CONTROL_MASK ) && !( event->state & GDK_SHIFT_MASK ) ){ // ただし範囲選択外をクリックしたとき、または範囲選択開始ボタンを押したときは選択解除 if( ! get_selection()->is_selected( path ) || ! m_path_dragstart.empty() ){ get_selection()->unselect_all(); if( get_row( path ) ) set_cursor( path ); m_selection_canceled = true; // ボタンを離したときにシグナルをemitしないようにする } else return true; } } return Gtk::TreeView::on_button_press_event( event ); } // // マウスボタンを離した // bool DragTreeView::on_button_release_event( GdkEventButton* event ) { bool emit_sig = false; // true なら m_sig_button_release をemitする Gtk::TreeModel::Path path = get_path_under_xy( (int)event->x, (int)event->y ); if( ! m_dragging // ドラッグ中ではない && !m_selection_canceled // on_button_press_event()で選択状態を解除していない && !( event->state & GDK_CONTROL_MASK ) && !( event->state & GDK_SHIFT_MASK ) // ctrl/shift + クリックで複数行選択操作をしてない場合 && !( !m_path_dragstart.empty() && ! path.empty() && path != m_path_dragstart ) // 範囲選択操作をしていない場合 ){ // 以上の場合はシグナルをemitする emit_sig = true; // 範囲選択状態でポップアップメニュー以外のボタンを離したら選択解除 if( get_selection()->get_selected_rows().size() >= 2 && ! m_control.button_alloted( event, CONTROL::PopupmenuButton ) ){ get_selection()->unselect_all(); if( get_row( path ) ) set_cursor( path ); emit_sig = false; } } // ポップアップメニューボタンを押したら必ずシグナルをemit if( m_control.button_alloted( event, CONTROL::PopupmenuButton ) ) emit_sig = true; m_path_dragstart = m_path_dragpre = Gtk::TreeModel::Path(); const bool expanded = row_expanded( path ); const bool ret = Gtk::TreeView::on_button_release_event( event ); // 左の△ボタンを押してディレクトリを開け閉めした場合は信号のemitをキャンセル if( expanded != row_expanded( path ) ) emit_sig = false; if( emit_sig ) sig_button_release().emit( event ); return ret; } // // D&D開始 // // このtreeがソースで無い時は呼ばれないのに注意 // void DragTreeView::on_drag_begin( const Glib::RefPtr< Gdk::DragContext >& context ) { #ifdef _DEBUG Gtk::TreeModel::Path path = get_path_under_mouse(); std::cout << "DragTreeView::on_drag_begin path = " << path.to_string() << std::endl; #endif m_dragging = true; CORE::DND_Begin(); Gtk::TreeView::on_drag_begin( context ); } // // D&D 終了 // // このtreeがソースでない時は呼び出されない // void DragTreeView::on_drag_end( const Glib::RefPtr< Gdk::DragContext >& context ) { #ifdef _DEBUG std::cout << "DragTreeView::on_drag_end\n"; #endif m_dragging = false; CORE::DND_End(); Gtk::TreeView::on_drag_end( context ); } // // ドロップされた void DragTreeView::on_drag_data_received( const Glib::RefPtr< Gdk::DragContext >& context, int x, int y, const Gtk::SelectionData& selection, guint info, guint time ) { #ifdef _DEBUG std::cout << "DragTreeView::on_drag_data_received type = " << selection.get_data_type() << std::endl; #endif if( selection.get_data_type() == "text/uri-list" ){ #ifdef _DEBUG std::cout << selection.get_data_as_string() << std::endl; #endif std::list< std::string > uri_list = MISC::get_lines( MISC::url_decode( selection.get_data_as_string() ) ); m_sig_dropped_url_list.emit( uri_list ); } context->drag_finish( true, false, time ); } // // キーボードのキーを押した // bool DragTreeView::on_key_press_event( GdkEventKey* event ) { #ifdef _DEBUG std::cout << "DragTreeView::on_key_press_event\n"; #endif // ポップアップ表示中はそちらにクロックを回す if( is_popup_shown() && m_popup_win->view() ) return m_popup_win->view()->slot_key_press( event ); return JDTreeViewBase::on_key_press_event( event ); } // // マウスを動かした // bool DragTreeView::on_motion_notify_event( GdkEventMotion* event ) { #ifdef _DEBUG // std::cout << "DragTreeView::on_motion_notify_event x = " << event->x << " y = " << event->y << std::endl; #endif // drag_source_set() でセットしたボタン以外でドラッグして範囲選択 // m_path_dragstart が empty で無いときに実行 // DragTreeView::on_button_press_event() も参照せよ Gtk::TreeModel::Path path = get_path_under_xy( (int)event->x, (int)event->y ); if( ! m_path_dragstart.empty() && !path.empty() && path != m_path_dragpre ){ get_selection()->unselect_all(); get_selection()->select( path, m_path_dragstart ); m_path_dragpre = path; } sig_motion_notify().emit( event ); return Gtk::TreeView::on_motion_notify_event( event ); } // マウスのwheelを回した bool DragTreeView::on_scroll_event( GdkEventScroll* event ) { sig_scroll_event().emit( event ); return true; } // // マウスホイールの処理 // void DragTreeView::wheelscroll( GdkEventScroll* event ) { auto adj = get_vadjustment(); double val = adj->get_value(); int scr_inc = get_row_height() * CONFIG::get_tree_scroll_size(); if( event->direction == GDK_SCROLL_UP ) val = MAX( 0, val - scr_inc ); else if( event->direction == GDK_SCROLL_DOWN ) val = MIN( adj->get_upper() - adj->get_page_size(), val + scr_inc ); else if( event->direction == GDK_SCROLL_SMOOTH ) { constexpr double smooth_scroll_factor = 4.0; m_smooth_dy += smooth_scroll_factor * event->delta_y; if( std::abs( m_smooth_dy ) > 1.0 ) { val += std::copysign( scr_inc, m_smooth_dy ); m_smooth_dy = 0.0; } // チルトホイール(横スクロール)対応 constexpr double smooth_scroll_factor_x = 0.5; auto hadj = get_hadjustment(); hadj->set_value( hadj->get_value() + scr_inc * smooth_scroll_factor_x * event->delta_x ); } adj->set_value( val ); #ifdef _DEBUG std::cout << "DragTreeView::on_scroll_event\n"; std::cout << "scr_inc = " << scr_inc << std::endl; std::cout << "lower = " << adj->get_lower() << std::endl; std::cout << "upper = " << adj->get_upper() << std::endl; std::cout << "value = " << val << std::endl; std::cout << "step = " << adj->get_step_increment() << std::endl; std::cout << "page = " << adj->get_page_increment() << std::endl; std::cout << "page_size = " << adj->get_page_size() << std::endl; #endif } bool DragTreeView::on_leave_notify_event( GdkEventCrossing* event ) { if( ! is_mouse_on_popup() ){ hide_popup(); m_pre_popup_url = std::string(); } return Gtk::TreeView::on_leave_notify_event( event ); } // // 範囲選択更新 // void DragTreeView::slot_selection_changed() { int size = get_selection()->get_selected_rows().size(); std::string str; if( size >= 2 ) str = "選択数 " + std::to_string( size ); CORE::core_set_command( "set_info" ,"", str ); } // // 実際の描画の際に cellrenderer のプロパティをセットするスロット関数 // void DragTreeView::slot_cell_data( Gtk::CellRenderer* cell, const Gtk::TreeModel::iterator& it ) { if( ! m_use_bg_even ){ cell->property_cell_background_set() = false; return; } Gtk::TreeModel::Row row = *it; Gtk::TreePath path = get_model()->get_path( row ); std::string path_str = path.to_string(); #ifdef _DEBUG std::cout << "DragTreeView::slot_cell_data path = " << path_str << std::endl; #endif bool even = false; int rownum = atoi( path_str.c_str() ); if( rownum %2 ) even = true; size_t pos = path_str.find( ':' ); while( pos != std::string::npos ){ path_str = path_str.substr( pos+1 ); rownum = atoi( path_str.c_str() ); if( ( even && ( rownum %2 ) ) || ( ! even && !( rownum %2 ) ) ) even = true; else even = false; pos = path_str.find( ':' ); } // 偶数行に色を塗る if( even ){ cell->property_cell_background_rgba() = m_color_bg_even; cell->property_cell_background_set() = true; } else cell->property_cell_background_set() = false; } jdim-0.10.1/src/skeleton/dragtreeview.h000066400000000000000000000124111445721505100177730ustar00rootroot00000000000000// ライセンス: GPL2 // // ドラッグ開始可能なtreeviewクラス // // set_enable_drop_uri_list() で他のアプリから text/uri-list のドロップを受け付ける // // フォント変更、偶数、奇数行別に色分けも可能。コンストラクタの use_usr_fontcolor を trueにしてフォントや色を指定する // 複数行選択、ツールチップ、ポップアップの表示も可能 // #ifndef _DRAGTREEVIEW_H #define _DRAGTREEVIEW_H #include "gtkmmversion.h" #include "treeviewbase.h" #include "control/control.h" #include #include namespace SKELETON { class View; class PopupWin; typedef sigc::signal< void, const std::list< std::string >& > SIG_DROPPED_URI_LIST; class DragTreeView : public JDTreeViewBase { std::string m_url; std::string m_dndtarget; bool m_dragging{}; // ドラッグ中 // 範囲選択に使用 bool m_selection_canceled{}; // 範囲選択を解除したときにsig_button_release()を発行しないようにする Gtk::TreeModel::Path m_path_dragstart; Gtk::TreeModel::Path m_path_dragpre; static constexpr const char* s_css_classname = u8"jd-dragtreeview"; Glib::RefPtr< Gtk::CssProvider > m_provider = Gtk::CssProvider::create(); // 色 bool m_use_bg_even{}; Gdk::RGBA m_color_text; Gdk::RGBA m_color_bg; Gdk::RGBA m_color_bg_even; // ポップアップウィンドウ用 std::unique_ptr m_popup_win; std::string m_pre_popup_url; bool m_popup_shown{}; // 入力コントローラ CONTROL::Control m_control; // text/uri-list をドロップされた SIG_DROPPED_URI_LIST m_sig_dropped_url_list; double m_smooth_dy{ 0.0 }; // GDK_SCROLL_SMOOTH のスクロール変化量 public: // use_usr_fontcolor が true の時はフォントや色を指定する DragTreeView( const std::string& url, const std::string& dndtarget, const bool use_usr_fontcolor, const std::string& fontname, const int colorid_text, const int colorid_bg, const int colorid_bg_even ); ~DragTreeView() noexcept; virtual void clock_in(); SIG_DROPPED_URI_LIST sig_dropped_uri_list(){ return m_sig_dropped_url_list; } const std::string& get_dndtarget() const { return m_dndtarget; } // 他のアプリからの text/url-list のドロップを有効にする // ドロップされるとSIG_DROPPED_URI_LIST を発行する void set_enable_drop_uri_list(); void redraw_view(); // 色初期化 void init_color( const int colorid_text, const int colorid_bg, const int colorid_bg_even ); // フォント初期化 void init_font( const std::string& fontname ); // ポップアップウィンドウ表示 const std::string& pre_popup_url() const { return m_pre_popup_url; } void reset_pre_popupurl( const std::string& url ){ if( ! m_pre_popup_url.empty() && m_pre_popup_url != url ) m_pre_popup_url = std::string(); } void show_popup( const std::string& url, View* view ); void hide_popup(); // マウスホイールの処理 void wheelscroll( GdkEventScroll* event ); // 実際の描画の際に cellrenderer のプロパティをセットするスロット関数 void slot_cell_data( Gtk::CellRenderer* cell, const Gtk::TreeModel::iterator& it ); protected: // drag_source_set() でセットしたボタンでドラッグした時に呼び出される順番は // // (1) on_button_press_event // (2) on_drag_begin // (3) on_drag_motion // (4) on_button_release_event // (5) on_drag_drop // (6) on_drag_end // // drag_source_set() でセットしたボタン以外でドラッグしたときは on_drag_motion() // ではなくて普通に on_motion_notify_event() が呼ばれるのに注意 // bool on_button_press_event( GdkEventButton* event ) override; bool on_button_release_event( GdkEventButton* event ) override; void on_drag_begin( const Glib::RefPtr< Gdk::DragContext>& context ) override; void on_drag_end( const Glib::RefPtr< Gdk::DragContext>& context ) override; void on_drag_data_received( const Glib::RefPtr< Gdk::DragContext >& context, int x, int y, const Gtk::SelectionData& selection, guint info, guint time ) override; bool on_key_press_event( GdkEventKey* event ) override; bool on_motion_notify_event( GdkEventMotion* event ) override; bool on_scroll_event( GdkEventScroll* event ) override; bool on_leave_notify_event( GdkEventCrossing* event ) override; private: // ポップアップが表示されているか bool is_popup_shown() const { return ( m_popup_win && m_popup_shown ); } // ポップアップ削除 void delete_popup(); // ポップアップが表示されていてかつマウスがその上にあるか bool is_mouse_on_popup(); bool slot_popup_leave_notify_event( GdkEventCrossing* event ); void slot_selection_changed(); }; } #endif jdim-0.10.1/src/skeleton/editcolumns.cpp000066400000000000000000000015431445721505100201700ustar00rootroot00000000000000// ライセンス: GPL2 //#define _DEBUG #include "jddebug.h" #include "editcolumns.h" #include "xml/tools.h" #include "type.h" using namespace SKELETON; EditColumns::EditColumns() { add( m_name ); add( m_image ); add( m_type ); add( m_url ); add( m_data ); add( m_underline ); add( m_expand ); add( m_fgcolor ); add( m_dirid ); } EditColumns::~EditColumns() noexcept = default; void EditColumns::setup_row( Gtk::TreeModel::Row& row, const Glib::ustring url, const Glib::ustring name, const Glib::ustring data, const int type, const size_t dirid ) { row[ m_name ] = name; row[ m_image ] = XML::get_icon( type ); row[ m_type ] = type; row[ m_url ] = url; row[ m_data ] = data; row[ m_expand ] = false; row[ m_underline ] = false; row[ m_dirid ] = dirid; } jdim-0.10.1/src/skeleton/editcolumns.h000066400000000000000000000030221445721505100176270ustar00rootroot00000000000000// ライセンス: GPL2 // // 編集可能な ColumnRecord クラス // // SKELETON::EditTreeView と組み合わせて使う // #ifndef _EDITCOLUMNS_H #define _EDITCOLUMNS_H #include namespace SKELETON { // 列ID enum { EDITCOL_NAME = 0, EDITCOL_IMAGE, // 以下不可視 EDITCOL_TYPE, EDITCOL_URL, EDITCOL_DATA, EDITCOL_UNDERLINE, EDITCOL_EXPAND, EDITCOL_FGCOLOR, EDITCOL_DIRID, EDITCOL_NUM_COL }; class EditColumns : public Gtk::TreeModel::ColumnRecord { public: Gtk::TreeModelColumn< Glib::ustring > m_name; // サブジェクト Gtk::TreeModelColumn< Glib::RefPtr< Gdk::Pixbuf > > m_image; // アイコン画像 Gtk::TreeModelColumn< int > m_type; // 行のタイプ Gtk::TreeModelColumn< Glib::ustring > m_url; // アドレス Gtk::TreeModelColumn< Glib::ustring > m_data; // ユーザデータ Gtk::TreeModelColumn< bool > m_underline; // 行に下線を引く Gtk::TreeModelColumn< bool > m_expand; // Dom::parse() で使用 Gtk::TreeModelColumn< Gdk::RGBA > m_fgcolor; // 文字色 Gtk::TreeModelColumn< size_t > m_dirid; // ディレクトリID EditColumns(); ~EditColumns() noexcept; virtual void setup_row( Gtk::TreeModel::Row& row, const Glib::ustring url, const Glib::ustring name, const Glib::ustring data, const int type, const size_t dirid ); }; } #endif jdim-0.10.1/src/skeleton/edittreeview.cpp000066400000000000000000001476451445721505100203600ustar00rootroot00000000000000// ライセンス: GPL2 //#define _DEBUG #include "jddebug.h" #include "gtkmmversion.h" #include "edittreeview.h" #include "editcolumns.h" #include "msgdiag.h" #include "undobuffer.h" #include "config/globalconf.h" #include "dbtree/interface.h" #include "jdlib/miscgtk.h" #include "xml/document.h" #include "sharedbuffer.h" #include "global.h" #ifndef MAX #define MAX( a, b ) ( a > b ? a : b ) #endif #ifndef MIN #define MIN( a, b ) ( a < b ? a : b ) #endif enum { DNDSCROLLSPEED = 250 // D&D 時のスクロール速度 }; // // 行の最新アドレスを取得 // const std::string get_uptodate_url( const std::string& url_org, const int type ) { std::string url = url_org; if( type == TYPE_BOARD || type == TYPE_BOARD_UPDATE ){ url = DBTREE::url_boardbase( url ); } else if( type == TYPE_THREAD || type == TYPE_THREAD_UPDATE || type == TYPE_THREAD_OLD ){ url = DBTREE::url_dat( url ); } return url; } // // 行の最新状態を取得 // int get_uptodate_type( const std::string& url, const int type_org ) { int type = type_org; if( type == TYPE_BOARD || type == TYPE_BOARD_UPDATE ){ if( DBTREE::board_status( url ) & STATUS_UPDATE ) type = TYPE_BOARD_UPDATE; else type = TYPE_BOARD; } else if( type == TYPE_THREAD || type == TYPE_THREAD_UPDATE || type == TYPE_THREAD_OLD ){ if( DBTREE::article_status( url ) & STATUS_OLD ) type = TYPE_THREAD_OLD; else if( DBTREE::article_status( url ) & STATUS_UPDATE ) type = TYPE_THREAD_UPDATE; else type = TYPE_THREAD; } return type; } // // ソート用比較クラス // // EditTreeView::sort()で使用 // class compare_path { SKELETON::JDTreeViewBase& m_treeview; SKELETON::EditColumns& m_columns; int m_mode; int type_to_order( const int type ) const { constexpr const int order[] = { TYPE_DIR, TYPE_VBOARD, TYPE_BOARD_UPDATE, TYPE_BOARD, TYPE_THREAD_UPDATE, TYPE_THREAD, TYPE_IMAGE, TYPE_COMMENT, TYPE_THREAD_OLD, TYPE_UNKNOWN }; int i = 0; for( ; order[ i ] != TYPE_UNKNOWN; ++i ){ if( type == order[ i ] ) return i; } return i; } public: compare_path( SKELETON::JDTreeViewBase& treeview, SKELETON::EditColumns& columns, const int mode ) : m_treeview( treeview ), m_columns( columns ), m_mode( mode ){} // path_a が上ならtrue path_b が上ならfalse bool operator () ( const Gtk::TreePath& path_a, const Gtk::TreePath& path_b ) { const Gtk::TreeRow row_a = m_treeview.get_row( path_a ); const Gtk::TreeRow row_b = m_treeview.get_row( path_b ); if( ! row_a || ! row_b ) return false; // 名前でソート if( m_mode == SKELETON::SORT_BY_NAME ){ const Glib::ustring name_a = row_a[ m_columns.m_name ]; const Glib::ustring name_b = row_b[ m_columns.m_name ]; if( name_a > name_b ) return false; else if( name_a < name_b ) return true; } // タイプでソート const Glib::ustring url_a = row_a[ m_columns.m_url ]; const Glib::ustring url_b = row_b[ m_columns.m_url ]; const int type_a = get_uptodate_type( url_a.raw(), row_a[ m_columns.m_type ] ); const int type_b = get_uptodate_type( url_b.raw(), row_b[ m_columns.m_type ] ); const int order_a = type_to_order( type_a ); const int order_b = type_to_order( type_b ); if( order_a == order_b ) return false; return ( order_a < order_b ); } }; ///////////////////////////////////////////////// using namespace SKELETON; EditTreeView::EditTreeView( const std::string& url, const std::string& dndtarget, EditColumns& columns, const bool use_usr_fontcolor, const std::string& fontname, const int colorid_text, const int colorid_bg, const int colorid_bg_even ) : DragTreeView( url, dndtarget, use_usr_fontcolor, fontname, colorid_text, colorid_bg, colorid_bg_even ), m_url( url ), m_columns( columns ) {} EditTreeView::EditTreeView( const std::string& url, const std::string& dndtarget, EditColumns& columns ) : DragTreeView( url, dndtarget, false, "", 0, 0, 0 ), m_url( url ), m_columns( columns ) {} EditTreeView::~EditTreeView() { #ifdef _DEBUG std::cout << "EditTreeView::~EditTreeView url = " << m_url << std::endl; #endif } // // 編集可能にする // void EditTreeView::set_editable_view( const bool editable ) { m_editable = editable; if( m_editable ){ // D&D のドロップを可能にする std::vector< Gtk::TargetEntry > targets; targets.push_back( Gtk::TargetEntry( get_dndtarget(), Gtk::TARGET_SAME_APP, 0 ) ); drag_dest_set( targets ); } } // // クロック入力 // void EditTreeView::clock_in() { DragTreeView::clock_in(); // D&D 中にカーソルが画面の上か下の方にある場合はスクロールさせる if( m_editable && m_dragging_on_tree ){ ++m_dnd_counter; if( m_dnd_counter >= DNDSCROLLSPEED / TIMER_TIMEOUT ){ m_dnd_counter = 0; Gtk::TreePath path = get_path_under_mouse(); auto adjust = get_vadjustment(); if( get_row( path ) && adjust ){ const int height = get_height(); const int step = (int)adjust->get_step_increment() / 2; int val = -1; int x,y; MISC::get_pointer_at_window( get_window(), x, y ); if( y < step * 2 ){ val = MAX( 0, (int)adjust->get_value() - step ); } else if( y > height - step * 2 ){ val = MIN( (int)adjust->get_value() + step, (int)( adjust->get_upper() - adjust->get_page_size() ) ); } if( val != -1 ){ adjust->set_value( val ); path = get_path_under_mouse(); draw_underline_while_dragging( path ); } } } } // 行を追加した直後に scroll_to_row() を呼んでもまだツリーが更新されてなくて // 正しくスクロールしないので更新されてからスクロールする else if( m_pre_adjust_upper && ! m_jump_path.empty() ){ const int upper = ( int )( get_vadjustment()->get_upper() ); #ifdef _DEBUG std::cout << "adjust_upper : " << m_pre_adjust_upper << " -> " << upper << " count = " << m_jump_count << std::endl; #endif // 時間切れ const int max_jump_count = 200 / TIMER_TIMEOUT; ++m_jump_count; if( m_pre_adjust_upper != upper || m_jump_count >= max_jump_count ){ #ifdef _DEBUG std::cout << "scroll to " << m_jump_path.to_string() << std::endl; #endif Gtk::TreeRow row = get_row( m_jump_path ); if( row ) scroll_to_row( m_jump_path, 0.5f ); m_pre_adjust_upper = 0; m_jump_path = Gtk::TreePath(); m_jump_count = 0; } } } // // path にスクロール // // 遅延させてツリー構造が変わってからスクロールする // clock_in()を参照 // void EditTreeView::set_scroll( const Gtk::TreePath& path ) { m_pre_adjust_upper = ( int )( get_vadjustment()->get_upper() ); m_jump_path = path; m_jump_count = 0; } // // treestoreのセット // void EditTreeView::set_treestore( const Glib::RefPtr< Gtk::TreeStore >& treestore ) { set_model( treestore ); set_headers_visible( false ); } // // xml -> tree 展開して treestore をセットする // void EditTreeView::xml2tree( const XML::Document& document, Glib::RefPtr< Gtk::TreeStore >& treestore, const std::string& root_name ) { #ifdef _DEBUG std::cout << "EditTreeView::xml2tree "; std::cout << " root = " << root_name; std::cout << " size = " << document.size() << std::endl; #endif treestore->clear(); unset_model(); // 開いてるツリーの格納用 std::list< Gtk::TreePath > list_path_expand; // Domノードから Gtk::TreeStore をセット document.set_treestore( treestore, m_columns, root_name, list_path_expand ); set_treestore( treestore ); // ディレクトリIDのセット(まだセットされていない場合) get_max_dirid(); set_dirid(); // ディレクトリオープン for( const Gtk::TreePath& path : list_path_expand ) { expand_parents( path ); expand_row( path, false ); } } // // tree -> XML 変換 // void EditTreeView::tree2xml( XML::Document& document, const std::string& root_name ) { Glib::RefPtr< Gtk::TreeStore > treestore = Glib::RefPtr< Gtk::TreeStore >::cast_dynamic( get_model() ); if( ! treestore || treestore->children().empty() ){ document.clear(); return; } // 全てのツリーに row[ m_columns.expand ] の値をセットする set_expanded_row( treestore, treestore->children() ); // m_treestore からノードツリーを作成 document.init( treestore, m_columns, root_name ); #ifdef _DEBUG std::cout << "EditTreeView::tree2xml "; std::cout << " root = " << root_name; std::cout << " size = " << document.size() << std::endl; #endif } // ディレクトリIDの最大値を取得 void EditTreeView::get_max_dirid() { m_max_dirid = 0; SKELETON::EditTreeViewIterator it( *this, m_columns, Gtk::TreePath() ); for( ; ! it.end(); ++it ){ Gtk::TreeModel::Row row = *it; if( row[ m_columns.m_type ] == TYPE_DIR ){ const size_t dirid = row[ m_columns.m_dirid ]; if( dirid > m_max_dirid ) m_max_dirid = dirid; } } ++m_max_dirid; #ifdef _DEBUG std::cout << "EditTreeView::get_max_dirid id = " << m_max_dirid << std::endl; #endif } // IDがついていないディレクトリにIDをセットする void EditTreeView::set_dirid() { #ifdef _DEBUG std::cout << "EditTreeView::set_dirid\n"; #endif SKELETON::EditTreeViewIterator it( *this, m_columns, Gtk::TreePath() ); for( ; ! it.end(); ++it ){ Gtk::TreeModel::Row row = *it; if( row[ m_columns.m_type ] == TYPE_DIR ){ const size_t dirid = row[ m_columns.m_dirid ]; if( ! dirid ){ row[ m_columns.m_dirid ] = m_max_dirid++; #ifdef _DEBUG std::cout << row[ m_columns.m_name ] << " id = " << row[ m_columns.m_dirid ] << std::endl; #endif } } } } // // 全てのツリーに m_columns.m_expand の値をセットする( tree2xml()で使用 ) // void EditTreeView::set_expanded_row( Glib::RefPtr< Gtk::TreeStore >& treestore, const Gtk::TreeModel::Children& children ) { for( const Gtk::TreeRow& row : children ) { const Gtk::TreePath path = treestore->get_path( row ); // ツリーが開いているか row[ m_columns.m_expand ] = row_expanded( path ); // 再帰 if( const auto chldn = row.children(); ! chldn.empty() ) { set_expanded_row( treestore, chldn ); } } } // // 列の作成 // // ypad : 行間スペース // Gtk::TreeViewColumn* EditTreeView::create_column( const int ypad ) { // Gtk::mange してるのでdeleteしなくてもよい Gtk::TreeViewColumn* col = Gtk::manage( new Gtk::TreeViewColumn( "name" ) ); col->pack_start( m_columns.m_image, Gtk::PACK_SHRINK ); m_ren_text = Gtk::manage( new Gtk::CellRendererText() ); m_ren_text->signal_edited().connect( sigc::mem_fun( *this, &EditTreeView::slot_ren_text_on_edited ) ); m_ren_text->signal_editing_canceled().connect( sigc::mem_fun( *this, &EditTreeView::slot_ren_text_on_canceled ) ); m_ren_text->property_underline() = Pango::UNDERLINE_SINGLE; // 行間スペース if( ypad >= 0 ) m_ren_text->property_ypad() = ypad; col->pack_start( *m_ren_text, true ); col->add_attribute( *m_ren_text, "text", EDITCOL_NAME ); col->add_attribute( *m_ren_text, "underline", EDITCOL_UNDERLINE ); col->add_attribute( *m_ren_text, "foreground_rgba", EDITCOL_FGCOLOR ); col->set_sizing( Gtk::TREE_VIEW_COLUMN_FIXED ); // 実際の描画時に偶数行に色を塗る col->set_cell_data_func( *col->get_first_cell(), sigc::mem_fun( *this, &DragTreeView::slot_cell_data ) ); col->set_cell_data_func( *m_ren_text, sigc::mem_fun( *this, &DragTreeView::slot_cell_data ) ); append_column( *col ); return col; } // // 新規ディレクトリ作成 // Gtk::TreePath EditTreeView::create_newdir( const Gtk::TreePath& path ) { CORE::DATA_INFO_LIST list_info; CORE::DATA_INFO info; info.type = TYPE_DIR; info.name = "新規ディレクトリ"; info.path = path.to_string(); while( ! dirid_to_path( m_max_dirid ).empty() ) ++m_max_dirid; info.dirid = m_max_dirid; list_info.push_back( info ); const bool before = false; const bool scroll = false; const bool force = false; // append_info 内でundoのコミットをしないで名前を変更してからslot_ren_text_on_canceled()でコミットする const bool cancel_undo_commit = true; const int check_dup = 0; // 項目の重複チェックをしない append_info( list_info, path, before, scroll, force, cancel_undo_commit, check_dup ); Gtk::TreePath path_new = get_current_path(); set_cursor( path_new ); rename_row( path_new ); return path_new; } // ディレクトリIDとパスを相互変換 Gtk::TreePath EditTreeView::dirid_to_path( const size_t dirid ) { if( ! dirid ) return Gtk::TreePath(); SKELETON::EditTreeViewIterator it( *this, m_columns, Gtk::TreePath() ); for( ; ! it.end(); ++it ){ Gtk::TreeModel::Row row = *it; if( row[ m_columns.m_type ] == TYPE_DIR ){ if( row[ m_columns.m_dirid ] == dirid ) return get_model()->get_path( row ); } } return Gtk::TreePath(); } size_t EditTreeView::path_to_dirid( const Gtk::TreePath path ) { Gtk::TreeModel::Row row = get_row( Gtk::TreePath( path ) ); if( row ) return row[ m_columns.m_dirid ]; return 0; } // // コメント挿入 // Gtk::TreePath EditTreeView::create_newcomment( const Gtk::TreePath& path ) { CORE::DATA_INFO_LIST list_info; CORE::DATA_INFO info; info.type = TYPE_COMMENT; info.name = "コメント"; info.path = path.to_string(); list_info.push_back( info ); const bool before = false; const bool scroll = false; const bool force = false; // append_info 内でundoのコミットをしないで名前を変更してからslot_ren_text_on_canceled()でコミットする const bool cancel_undo_commit = true; const int check_dup = 0; // 項目の重複チェックをしない append_info( list_info, path, before, scroll, force, cancel_undo_commit, check_dup ); Gtk::TreePath path_new = get_current_path(); set_cursor( path_new ); rename_row( path_new ); return path_new; } // // pathで指定した行の名前変更 // void EditTreeView::rename_row( const Gtk::TreePath& path ) { if( path.empty() ) return; // edit可 slot_ren_text_on_edited() と slot_ren_text_on_canceled で false にする m_ren_text->property_editable() = true; set_cursor( path, *get_column( 0 ), true ); } // // 行の名前を変更したときにCellRendererTextから呼び出される // void EditTreeView::slot_ren_text_on_edited( const Glib::ustring& path, const Glib::ustring& text ) { #ifdef _DEBUG std::cout << "EditTreeView::slot_ren_text_on_edited\n" << "path = " << path << std::endl << "text = " << text << std::endl; #endif Gtk::TreeRow row = get_row( Gtk::TreePath( path ) ); if( row ){ const Glib::ustring text_before = row[ m_columns.m_name ]; row.set_value( m_columns.m_name, text ); if( m_editable && m_undo_buffer ) m_undo_buffer->set_name( Gtk::TreePath( path ), text, text_before ); } slot_ren_text_on_canceled(); } // // 行の名前変更をキャンセルしたときにCellRendererTextから呼び出される // void EditTreeView::slot_ren_text_on_canceled() { #ifdef _DEBUG std::cout << "EditTreeView::slot_ren_text_on_canceld\n"; #endif m_ren_text->property_editable() = false; if( m_editable && m_undo_buffer ) m_undo_buffer->commit(); } // // D&D中の受信側上でマウスを動かした // // 編集可能の時は下線を引く // 他のwidgetがソースの時も呼び出される。ドラッグ中は on_motion_notify_event() は呼び出されない // bool EditTreeView::on_drag_motion( const Glib::RefPtr& context, int x, int y, guint time ) { const Gtk::TreePath path = get_path_under_mouse(); #ifdef _DEBUG if( ! m_dragging_on_tree ) std::cout << "EditTreeView::on_drag_enter"; else std::cout << "EditTreeView::on_drag_motion"; std::cout << " x = " << x << " y = " << y; if( ! path.empty() ) std::cout << " path = " << path.to_string(); std::cout << std::endl; #endif const bool ret = DragTreeView::on_drag_motion( context, x, y, time ); m_dragging_on_tree = true; draw_underline_while_dragging( path ); return ret; } // // D&D中に受信側からマウスが出た // void EditTreeView::on_drag_leave( const Glib::RefPtr& context, guint time ) { #ifdef _DEBUG std::cout << "EditTreeView::on_drag_leave\n"; #endif DragTreeView::on_drag_leave( context, time ); m_dragging_on_tree = false; draw_underline( m_drag_path_uline, false ); } // // D&Dで受信側がデータ送信を要求してきた // void EditTreeView::on_drag_data_get( const Glib::RefPtr& context, Gtk::SelectionData& selection_data, guint info, guint time ) { #ifdef _DEBUG std::cout << "EditTreeView::on_drag_data_get target = " << selection_data.get_target() << std::endl; #endif DragTreeView::on_drag_data_get( context, selection_data, info, time ); // 範囲選択行を共有バッファに入れる if( selection_data.get_target() == get_dndtarget() ){ CORE::DATA_INFO_LIST list_info; const bool dir = true; get_info_in_selection( list_info, dir ); if( list_info.size() ){ CORE::SBUF_set_list( list_info ); // 受信側の on_drag_data_received() を呼び出す selection_data.set( get_dndtarget(), m_url ); } } } // // D&Dの受信側がデータを取得 // void EditTreeView::on_drag_data_received( const Glib::RefPtr& context, int x, int y, const Gtk::SelectionData& selection_data, guint info, guint time ) { const std::string url_from = selection_data.get_data_as_string(); #ifdef _DEBUG std::cout << "EditTreeView::on_drag_data_received target = " << selection_data.get_target() << " url = " << m_url << " url_from = " << url_from << std::endl; #endif DragTreeView::on_drag_data_received( context, x, y, selection_data, info, time ); draw_underline( m_drag_path_uline, false ); m_exec_drop = false; m_row_dest = Gtk::TreeRow(); m_row_dest_before = false; m_dropped_from_other = false; // 挿入先のrowを保存 if( m_editable && selection_data.get_target() == get_dndtarget() ){ CORE::DATA_INFO_LIST list_info = CORE::SBUF_list_info(); if( ! list_info.size() ) return; const Gtk::TreePath path_dest = get_path_under_mouse(); m_row_dest = get_row( path_dest ); // セル内の座標を見て真ん中より上だったら上に挿入 if( m_row_dest ){ int cell_x, cell_y, cell_w, cell_h; get_cell_xy_wh( cell_x, cell_y, cell_w, cell_h ); if( cell_y < cell_h / 2 ) m_row_dest_before = true; } if( url_from != m_url ) m_dropped_from_other = true; #ifdef _DEBUG std::cout << "x = " << x << " y = " << y << " path_dest = " << path_dest.to_string() << " before = " << m_row_dest_before << " other = " << m_dropped_from_other << std::endl; #endif // 同じ widget からドロップされた場合は上書きになっていないかチェックする if( m_row_dest && ! m_dropped_from_other ){ for( const CORE::DATA_INFO& data : list_info ) { #ifdef _DEBUG std::cout << data.name << " path = " << data.path << std::endl; #endif if( data.path.empty() ) continue; // 移動先と送り側が同じならキャンセル if( data.path == path_dest.to_string() ) return; // 移動先がサブディレクトリに含まれないかチェック if( Gtk::TreePath( data.path ).is_ancestor( path_dest ) ){ SKELETON::MsgDiag mdiag( get_parent_win(), "移動先は送り側のディレクトリのサブディレクトリです", false, Gtk::MESSAGE_ERROR ); mdiag.run(); return; } } } m_exec_drop = true; // 送信側の on_drag_data_delete()を呼び出す context->drag_finish( true, true, time ); } } // // D&Dの送信側がデータを削除 // void EditTreeView::on_drag_data_delete( const Glib::RefPtr& context ) { #ifdef _DEBUG std::cout << "EditTreeView::on_drag_data_delete\n"; #endif DragTreeView::on_drag_data_delete( context ); // 選択行の削除 if( m_editable ){ CORE::DATA_INFO_LIST list_info = CORE::SBUF_list_info(); delete_rows( list_info, Gtk::TreePath() ); if( m_editable && m_undo_buffer ){ m_undo_buffer->set_list_info_selected( list_info ); // undo したときに選択する列 m_undo_buffer->set_list_info( CORE::DATA_INFO_LIST(), list_info ); } } } // // D&Dで受信側にデータがドロップされた // // 他のwidgetがソースの時も呼ばれるのに注意 // bool EditTreeView::on_drag_drop( const Glib::RefPtr& context, int x, int y, guint time ) { #ifdef _DEBUG std::cout << "EditTreeView::on_drag_drop\n"; #endif const bool ret = DragTreeView::on_drag_drop( context, x, y, time ); if( ! get_model() ) return ret; if( ! m_editable ) return ret; if( ! m_exec_drop ) return ret; Gtk::TreePath path_dest = Gtk::TreePath(); bool before = false; if( m_row_dest ){ path_dest = get_model()->get_path( m_row_dest ); before = m_row_dest_before; } #ifdef _DEBUG std::cout << "path_dest = " << path_dest.to_string() << " before = " << before << std::endl; #endif // 共有バッファ内の行を追加 const bool scroll = false; const bool force = false; const bool cancel_undo_commit = false; const int check_dup = m_dropped_from_other ? CONFIG::get_check_favorite_dup() : 0; const CORE::DATA_INFO_LIST list_info = append_info( CORE::SBUF_list_info(), path_dest, before, scroll, force, cancel_undo_commit, check_dup ); CORE::SBUF_clear_info(); if( m_dropped_from_other ) m_sig_dropped_from_other.emit( list_info ); return ret; } // // D&Dで受信側に終了を知らせる // // この widget がソースでない時は呼び出されない // void EditTreeView::on_drag_end( const Glib::RefPtr< Gdk::DragContext >& context ) { #ifdef _DEBUG std::cout << "EditTreeView::on_drag_end\n"; #endif draw_underline( m_drag_path_uline, false ); DragTreeView::on_drag_end( context ); } // // ドラッグ中にマウスカーソルの下に下線を引く // void EditTreeView::draw_underline_while_dragging( Gtk::TreePath path ) { if( ! m_editable ) return; if( path.empty() ) return; bool draw = true; int cell_x, cell_y, cell_w, cell_h; get_cell_xy_wh( cell_x, cell_y, cell_w, cell_h ); // 真ん中より上の場合 if( cell_y < cell_h / 2 ){ path.prev(); if( is_dir( path ) || path == Gtk::TreePath( "0" ) // 先頭行 ) draw = false; } draw_underline( m_drag_path_uline, false ); if( draw ) draw_underline( path, true ); m_drag_path_uline = path; } // // draw == true なら pathに下線を引く // void EditTreeView::draw_underline( const Gtk::TreePath& path, const bool draw ) { if( ! m_editable ) return; Gtk::TreeRow row = get_row( path ); if( ! row ) return; row[ m_columns.m_underline ] = draw; static_cast( row ); // cppcheck: unreadVariable } // // path は ディレクトリか // bool EditTreeView::is_dir( const Gtk::TreeModel::iterator& it ) const { const Gtk::TreeRow row = ( *it ); if( ! row ) return false; if( row[ m_columns.m_type ] == TYPE_DIR ) return true; return false; } bool EditTreeView::is_dir( const Gtk::TreePath& path ) { if( path.size() <= 0 ) return false; Gtk::TreeModel::iterator it = get_model()->get_iter( path ); return is_dir( it ); } // 前のディレクトリに移動 void EditTreeView::prev_dir() { Gtk::TreePath path = get_current_path(); for(;;){ Gtk::TreePath new_path = prev_path( path ); if( path == new_path ){ goto_top(); return; } path = new_path; if( is_dir( path ) ) break; } set_cursor( path ); } // 次のディレクトリに移動 void EditTreeView::next_dir() { Gtk::TreePath path = get_current_path(); for(;;){ path = next_path( path ); if( ! path.size() || ! get_row( path ) ){ goto_bottom(); return; } if( is_dir( path ) ) break; } set_cursor( path ); } // // 指定したアドレスの行が含まれているか // bool EditTreeView::exist_row( const std::string& url, const int type ) { if( url.empty() ) return false; const std::string url_target = get_uptodate_url( url, type ); SKELETON::EditTreeViewIterator it( *this, m_columns, Gtk::TreePath() ); for( ; ! it.end(); ++it ){ Gtk::TreeModel::Row row = *it; const Glib::ustring url_row = row[ m_columns.m_url ]; if( ! url_row.empty() ){ if( url_target == get_uptodate_url( url_row.raw(), row[ m_columns.m_type ] ) ) return true; } } return false; } // // ディレクトリ内を全選択 // void EditTreeView::select_all_dir( Gtk::TreePath path_dir ) { if( ! is_dir( path_dir ) ) return; get_selection()->select( path_dir ); path_dir.down(); while( get_row( path_dir ) ){ get_selection()->select( path_dir ); select_all_dir( path_dir ); path_dir.next(); } } // // list_info を path_dest 以下に追加 // // list_info の各path にあらかじめ値をセットしておくこと // // scroll = true なら追加した行にスクロールする // // force = true なら m_editable が false でも追加 // // cancel_undo_commit = true なら undo バッファをコミットしない // // check_dup == 0 ならチェックせず追加 1 なら重複チェックをして重複してたらダイアログ表示、2なら重複チェックして重複してたら追加しない // // (1) path_dest が empty なら一番最後 // // (2) before = true なら path_dest の前 // // (3) path_destがディレクトリなら path_dest の下 // // (4) そうでなければ path_dest の後 // CORE::DATA_INFO_LIST EditTreeView::append_info( const CORE::DATA_INFO_LIST& list_info, const Gtk::TreePath& path_dest, const bool before, const bool scroll, const bool force, const bool cancel_undo_commit, int check_dup ) { CORE::DATA_INFO_LIST list_info_src; if( ! force && ! m_editable ) return list_info_src; if( ! list_info.size() ) return list_info_src; #ifdef _DEBUG std::cout << "EditTreeView::append_info" << " path_dest = " << path_dest.to_string() << " before = " << before << " check_dup = " << check_dup << std::endl; #endif if( ! check_dup ) list_info_src = list_info; // 重複がないかチェック else{ #ifdef _DEBUG std::cout << "checking duplicatiion\n"; #endif for( const CORE::DATA_INFO& info : list_info ) { if( exist_row( info.url, info.type ) ){ if( check_dup == 2 ) continue; SKELETON::MsgCheckDiag mdiag( get_parent_win(), info.name + "\n\nは既に含まれています。追加しますか?", "今後表示しない(常に追加しない) (_D)", Gtk::MESSAGE_QUESTION, Gtk::BUTTONS_YES_NO, Gtk::RESPONSE_NO ); mdiag.set_title( "お気に入り追加確認" ); const int ret = mdiag.run(); if( mdiag.get_chkbutton().get_active() ){ CONFIG::set_check_favorite_dup( 2 ); check_dup = 2; } if( ret != Gtk::RESPONSE_YES ) continue; } list_info_src.push_back( info ); } } if( ! list_info_src.size() ) return list_info_src; if( m_editable && m_undo_buffer ){ CORE::DATA_INFO_LIST list_info_selected; const bool dir = false; get_info_in_selection( list_info_selected, dir ); m_undo_buffer->set_list_info_selected( list_info_selected ); // undo したときに選択する列 } replace_infopath( list_info_src, path_dest, before ); expand_parents( path_dest ); append_rows( list_info_src ); select_info( list_info_src ); if( m_editable && m_undo_buffer ){ m_undo_buffer->set_list_info( list_info_src, CORE::DATA_INFO_LIST() ); m_undo_buffer->set_list_info_selected( list_info_src ); // redo したときに選択する列 if( ! cancel_undo_commit ) m_undo_buffer->commit(); } // 遅延させてツリー構造が変わってからスクロールする // clock_in()を参照 if( scroll ) set_scroll( Gtk::TreePath( list_info_src.front().path ) ); return list_info_src; } // // pathをまとめて削除 // // force = true なら m_editable が false でも削除 // void EditTreeView::delete_path( const std::list& list_path, const bool force ) { if( ! list_path.size() ) return; if( ! force && ! m_editable ) return; #ifdef _DEBUG std::cout << "EditTreeView::delete_path\n"; #endif // 削除範囲に現在のカーソルがある時はカーソルの位置を変更する bool selected = false; const Gtk::TreePath path_selected = get_current_path(); CORE::DATA_INFO_LIST list_info; for( const Gtk::TreePath& path : list_path ) { if( path_selected == path ) selected = true; #ifdef _DEBUG std::cout << "path = " << path.to_string() << std::endl; #endif CORE::DATA_INFO info; path2info( info, path ); list_info.push_back( info ); } // カーソルを最後の行の次の行に移動するため、あらかじめ削除範囲の最後の行に移動しておく Gtk::TreePath next = path_selected; if( selected ) next = next_path( Gtk::TreePath( ( list_info.back() ).path ), true ); delete_rows( list_info, next ); if( m_editable && m_undo_buffer ){ m_undo_buffer->set_list_info_selected( list_info ); // undo したときに選択する列 m_undo_buffer->set_list_info( CORE::DATA_INFO_LIST(), list_info ); CORE::DATA_INFO_LIST list_info_selected; const bool dir = false; get_info_in_selection( list_info_selected, dir ); m_undo_buffer->set_list_info_selected( list_info_selected ); // redo したときに選択する列 m_undo_buffer->commit(); } } // // 選択した行をまとめて削除 // // force = true なら m_editable が false でも削除 // void EditTreeView::delete_selected_rows( const bool force ) { if( ! force && ! m_editable ) return; std::list< Gtk::TreeModel::iterator > list_selected = get_selected_iterators(); if( ! list_selected.size() ) return; // ディレクトリが含まれていないか無いか確認 for( const Gtk::TreeModel::iterator& iter : list_selected ) { if( is_dir( iter ) ){ SKELETON::MsgDiag mdiag( get_parent_win(), "ディレクトリを削除するとディレクトリ内の行も全て削除されます。削除しますか?", false, Gtk::MESSAGE_QUESTION, Gtk::BUTTONS_YES_NO ); if( mdiag.run() != Gtk::RESPONSE_YES ) return; break; } } #ifdef _DEBUG std::cout << "EditTreeView::delete_selected_rows" << " path = " << get_model()->get_path( *list_selected.begin() ).to_string() << std::endl; #endif // 削除する行を取得 CORE::DATA_INFO_LIST list_info; constexpr bool scan_in_dir = true; get_info_in_selection( list_info, scan_in_dir ); // カーソルを最後の行の次の行に移動するため、あらかじめ削除範囲の最後の行に移動しておく const Gtk::TreePath next = next_path( Gtk::TreePath( ( list_info.back() ).path ), true ); delete_rows( list_info, next ); if( m_editable && m_undo_buffer ){ m_undo_buffer->set_list_info_selected( list_info ); // undo したときに選択する列 m_undo_buffer->set_list_info( CORE::DATA_INFO_LIST(), list_info ); CORE::DATA_INFO_LIST list_info_selected; constexpr bool not_scan_in_dir = false; get_info_in_selection( list_info_selected, not_scan_in_dir ); m_undo_buffer->set_list_info_selected( list_info_selected ); // redo したときに選択する列 m_undo_buffer->commit(); } } // // UNDO // void EditTreeView::undo() { if( ! m_undo_buffer ) return; if( ! m_undo_buffer->get_enable_undo() ) return; m_undo_buffer->undo(); const UNDO_DATA& data = m_undo_buffer->get_undo_data(); const int size = data.size(); #ifdef _DEBUG std::cout << "EditTreeView::undo size = " << size << std::endl; #endif for( int i = size-1; i >=0; --i ){ if( ! data[ i ].list_info_selected.empty() ){ set_scroll( Gtk::TreePath( data[ i ].list_info_selected.front().path ) ); select_info( data[ i ].list_info_selected ); } // 追加キャンセル if( ! data[ i ].list_info_append.empty() ) delete_rows( data[ i ].list_info_append, Gtk::TreePath() ); // 削除キャンセル if( ! data[ i ].list_info_delete.empty() ){ expand_rows( data[ i ].list_info_delete ); append_rows( data[ i ].list_info_delete ); } // 名前変更キャンセル Gtk::TreeRow row = get_row( data[ i ].path_renamed ); if( row ){ scroll_to_row( data[ i ].path_renamed, 0.5f ); // ツリー構造に変化は無いのですぐにスクロールする set_cursor( data[ i ].path_renamed ); if( ! data[ i ].name_before.empty() ) row[ m_columns.m_name ] = data[ i ].name_before; } } } // // REDO // void EditTreeView::redo() { #ifdef _DEBUG std::cout << "EditTreeView::redo\n"; #endif if( ! m_undo_buffer ) return; if( ! m_undo_buffer->get_enable_redo() ) return; const UNDO_DATA& data = m_undo_buffer->get_undo_data(); #ifdef _DEBUG std::cout << "EditTreeView::redo size = " << data.size() << std::endl; #endif for( const UNDO_ITEM& item : data ) { if( ! item.list_info_selected.empty() ){ set_scroll( Gtk::TreePath( item.list_info_selected.front().path ) ); select_info( item.list_info_selected ); } // 削除 if( ! item.list_info_delete.empty() ) delete_rows( item.list_info_delete, Gtk::TreePath() ); // 追加 if( ! item.list_info_append.empty() ){ expand_rows( item.list_info_append ); append_rows( item.list_info_append ); } // 名前変更 Gtk::TreeRow row = get_row( item.path_renamed ); if( row ){ scroll_to_row( item.path_renamed, 0.5f ); // ツリー構造に変化は無いのですぐにスクロールする set_cursor( item.path_renamed ); if( ! item.name_new.empty() ) row[ m_columns.m_name ] = item.name_new; } } m_undo_buffer->redo(); } // // list_info の各要素の path を path_dest 以下に変更 // // list_info の各要素の path にあらかじめ値をセットしておくこと // // (1) path_dest が empty なら一番最後 // // (2) before = true なら path_dest の前 // // (3) path_destがディレクトリなら path_dest の下 // // (4) そうでなければ path_dest の後 // void EditTreeView::replace_infopath( CORE::DATA_INFO_LIST& list_info, const Gtk::TreePath& path_dest, const bool before ) { if( ! list_info.size() ) return; if( ! get_model() ) return; Gtk::TreePath path = path_dest; // path の初期値を求める if( path.empty() ){ Gtk::TreeModel::Children children = get_model()->children(); if( children.empty() ) path = Gtk::TreePath( "0" ); else{ path = get_model()->get_path( *( std::prev( children.end() ) ) ); path.next(); } } else if( before ); else if( is_dir( path ) ) path.down(); else path.next(); // list_infoの先頭に path をセット CORE::DATA_INFO_LIST::iterator it = list_info.begin(); Gtk::TreePath path_prev( ( *it ).path ); ( *it ).path = path.to_string(); const size_t max_pathsize_org = path_prev.size(); const size_t max_pathsize = path.size(); #ifdef _DEBUG std::cout << "EditTreeView::replace_infopath\n" << "max_pathsize_org = " << max_pathsize_org << " max_pathsize = " << max_pathsize << std::endl << "path_prev = " << path_prev.to_string() << " -> " << ( *it ).path << " type = " << ( *it ).type << " name = " << ( *it ).name << std::endl; #endif for( ++it; it != list_info.end() ; ++it ){ CORE::DATA_INFO& info = ( *it ); // list_infoの構造と合わせて path を更新していく do{ // 次 Gtk::TreePath path_tmp = path_prev; path_tmp.next(); if( path_tmp.to_string() == info.path ){ path.next(); break; } // ディレクトリ下がる path_tmp = path_prev; path_tmp.down(); if( path_tmp.to_string() == info.path ){ path.down(); break; } // ディレクトリ上がる bool reset = true; int count_up = 0; path_tmp = path_prev; while( path_tmp.size() != max_pathsize_org ){ ++count_up; path_tmp.up(); path_tmp.next(); if( path_tmp.to_string() == info.path ){ while( count_up-- ) path.up(); path.next(); reset = false; break; } } // どれにも該当しない場合には一番上のレベルまで戻る if( reset ){ while( path.size() != max_pathsize ) path.up(); path.next(); } } while(0); path_prev = Gtk::TreePath( info.path ); info.path = path.to_string(); #ifdef _DEBUG std::cout << "path_prev = " << path_prev.to_string() << " -> " << info.path << " type = " << info.type << " name = " << info.name << " data = " << info.data << std::endl; #endif } } // // 選択行をlist_infoにセット // // dir : trueの時はディレクトリが選択されているときはディレクトリ内の行もlist_infoに再帰的にセットする // void EditTreeView::get_info_in_selection( CORE::DATA_INFO_LIST& list_info, const bool dir ) { list_info.clear(); if( ! get_model() ) return; std::list< Gtk::TreeModel::iterator > list_selected = get_selected_iterators(); if( ! list_selected.size() ) return; for( const Gtk::TreeModel::iterator& iter : list_selected ) { Gtk::TreePath path = get_model()->get_path( iter ); CORE::DATA_INFO info; path2info( info, path ); // 既に path が list_info に登録されていたら登録しない const auto equal_to = [&info]( const CORE::DATA_INFO& i ) { return i.path == info.path; }; if( std::any_of( list_info.cbegin(), list_info.cend(), equal_to ) ) continue; list_info.push_back( info ); if( dir ) get_info_in_dir( list_info, path ); } } // // ディレクトリ(path_dir)内の行を全てlist_infoにセットする // // path_dir が empty() ならルートの行を全てセット // void EditTreeView::get_info_in_dir( CORE::DATA_INFO_LIST& list_info, const Gtk::TreePath& path_dir ) { if( ! path_dir.empty() && ! is_dir( path_dir ) ) return; CORE::DATA_INFO info; Gtk::TreePath path = path_dir; if( path_dir.empty() ) path = Gtk::TreePath( "0" ); else path.down(); while( get_row( path ) ){ path2info( info, path ); list_info.push_back( info ); get_info_in_dir( list_info, path ); path.next(); } } // // path から info を取得 // void EditTreeView::path2info( CORE::DATA_INFO& info, const Gtk::TreePath& path ) { Glib::ustring tmp_str; Gtk::TreeRow row = get_row( path ); info.type = row[ m_columns.m_type ]; info.parent = nullptr; tmp_str = row[ m_columns.m_url ]; info.url = tmp_str.raw(); tmp_str = row[ m_columns.m_name ]; info.name = tmp_str.raw(); tmp_str = row[ m_columns.m_data ]; info.data = tmp_str.raw(); info.path = path.to_string(); info.dirid = row[ m_columns.m_dirid ]; info.expanded = row_expanded( path ); } // // 一行追加 // // 戻り値は追加した行のpath // // (1) path_dest が empty なら一番最後に作る // // (2) before = true なら前に作る // // (3) path_dest がディレクトリかつ sudir == true なら path_dest の下に追加。 // // (4) そうでなければ path_dest の後に追加 // Gtk::TreePath EditTreeView::append_one_row( const std::string& url, const std::string& name, const int type, const size_t dirid, const std::string& data, const Gtk::TreePath& path_dest, const bool before, const bool subdir ) { Glib::RefPtr< Gtk::TreeStore > treestore = Glib::RefPtr< Gtk::TreeStore >::cast_dynamic( get_model() ); if( ! treestore ) return Gtk::TreePath(); #ifdef _DEBUG std::cout << "EditTreeView::append_one_row : " << name << " path = " << path_dest.to_string() << " before = " << before << " subdir = " << subdir << std::endl; #endif Gtk::TreeRow row_dest = get_row( path_dest ); Gtk::TreeRow row_new; // 一番下に追加 if( ! row_dest ) row_new = *( treestore->append() ); // 前に追加 else if( before ) row_new = *( treestore->insert( row_dest ) ); // ディレクトリの下に追加 else if( subdir && row_dest[ m_columns.m_type ] == TYPE_DIR ){ row_new = *( treestore->prepend( row_dest.children() ) ); } // 後ろに追加 else row_new = *( treestore->insert_after( row_dest ) ); // 行のアドレスや状態を最新にする const std::string url_new = get_uptodate_url( url, type ); const int type_new = get_uptodate_type( url_new, type ); m_columns.setup_row( row_new, url_new, name, data, type_new, dirid ); return treestore->get_path( row_new ); } // // list_info に示した行の親を再起的にexpandする // list_info の各要素の path にあらかじめ値をセットしておくこと // void EditTreeView::expand_rows( const CORE::DATA_INFO_LIST& list_info ) { #ifdef _DEBUG std::cout << "EditTreeView::expand_rows\n"; #endif if( ! list_info.size() ) return; for( const CORE::DATA_INFO& info : list_info ) { #ifdef _DEBUG std::cout << "path = " << info.path << std::endl; #endif expand_parents( Gtk::TreePath( info.path ) ); } } // // list_info に示した行を追加 // // list_info の各要素の path にあらかじめ値をセットしておくこと // void EditTreeView::append_rows( const CORE::DATA_INFO_LIST& list_info ) { #ifdef _DEBUG std::cout << "EditTreeView::append_rows\n"; #endif if( ! list_info.size() ) return; Gtk::TreePath path_parent; bool expand_parent = false; bool head_info = true; for( const CORE::DATA_INFO& info : list_info ) { Gtk::TreePath path( info.path ); #ifdef _DEBUG std::cout << "path = " << path.to_string() << " type = " << info.type << " name = " << info.name << " data = " << info.data << std::endl; #endif bool before = false; bool subdir = false; const bool prev = path.prev(); if( ! prev ){ // 先頭行 if( path == Gtk::TreePath( "0" ) ) before = true; // ディレクトリの先頭 else { path.up(); subdir = true; // 最初の info なら親のディレクトリを開く if( head_info ){ #ifdef _DEBUG std::cout << "expand parent dir\n"; #endif path_parent = path; expand_parent = true; } } } append_one_row( info.url, info.name, info.type, info.dirid, info.data, path, before, subdir ); head_info = false; } // ディレクトリを開く if( expand_parent ) expand_row( path_parent, false ); for( const CORE::DATA_INFO& info : list_info ) { if( info.expanded ){ const Gtk::TreePath path( info.path ); expand_row( path, false ); } } m_updated = true; } // // list_info に示した行を削除 // // list_info の各要素の path にあらかじめ値をセットしておくこと // // 削除した後、path_select にカーソルを移動する(emptyの場合は移動しない) // void EditTreeView::delete_rows( const CORE::DATA_INFO_LIST& list_info, const Gtk::TreePath& path_select ) { #ifdef _DEBUG std::cout << "EditTreeView::delete_rows"; if( ! path_select.empty() ) std::cout << " path_select = " << path_select.to_string(); std::cout << std::endl; #endif if( ! list_info.size() ) return; Glib::RefPtr< Gtk::TreeStore > treestore = Glib::RefPtr< Gtk::TreeStore >::cast_dynamic( get_model() ); if( ! treestore ) return; // あらかじめ path_select にカーソルを移動しておく // もし path_select が存在しなかったら削除してから一番下に移動 bool gotobottom = false; if( ! path_select.empty() ){ gotobottom = ( ! get_row( path_select ) ); if( ! gotobottom ) set_cursor( path_select ); } CORE::DATA_INFO_LIST::const_iterator it = list_info.end(); do{ --it; const CORE::DATA_INFO& info = ( *it ); Gtk::TreePath path( info.path ); #ifdef _DEBUG std::cout << path.to_string() << " type = " << info.type << " name = " << info.name << " url = " << info.url << " data = " << info.data << std::endl; #endif Gtk::TreeRow row = get_row( path ); treestore->erase( row ); } while( it != list_info.begin() ); m_updated = true; if( gotobottom ) goto_bottom(); } // // list_infoに示した行を選択 // void EditTreeView::select_info( const CORE::DATA_INFO_LIST& list_info ) { get_selection()->unselect_all(); for( const CORE::DATA_INFO& info : list_info ) { Gtk::TreePath path( info.path ); get_selection()->select( path ); } } // // ソート実行 // // path : ディレクトリなら中をソート、そうでなければそのレベルでソート // mode : ソートのモード。詳しくは class compare_path を参照 // void EditTreeView::sort( const Gtk::TreePath& path, const int mode ) { if( ! m_editable ) return; if( ! get_row( path ) ) return; CORE::DATA_INFO_LIST list_info; Gtk::TreePath path_head = path; if( is_dir( path_head ) ) path_head.down(); else while( path_head.prev() ); if( ! get_row( path_head ) ) return; Gtk::TreePath path_parent = path_head; if( path_parent.size() >= 2 ) path_parent.up(); else path_parent = Gtk::TreePath(); #ifdef _DEBUG std::cout << "EditTreeView::sort head = " << path_head.to_string() << " parent = " << path_parent.to_string() << std::endl; #endif // ソート std::list< Gtk::TreePath > list_path; Gtk::TreePath path_tmp = path_head; while( get_row( path_tmp ) ){ list_path.push_back( path_tmp ); path_tmp.next(); } list_path.sort( compare_path( *this, m_columns, mode ) ); for( const Gtk::TreePath& p : list_path ) { const Gtk::TreeRow row = get_row( p ); if( row ){ #ifdef _DEBUG std::cout << p.to_string() << " type = " << row[ m_columns.m_type ] << " name = " << row[ m_columns.m_name ] << std::endl; #endif CORE::DATA_INFO info; path2info( info, p ); list_info.push_back( info ); get_info_in_dir( list_info, p ); } } // path_parent の中を全て削除 if( m_undo_buffer ){ CORE::DATA_INFO_LIST list_info_selected; const bool dir = false; get_info_in_selection( list_info_selected, dir ); m_undo_buffer->set_list_info_selected( list_info_selected ); // undo したときに選択する列 } CORE::DATA_INFO_LIST list_info_delete; get_info_in_dir( list_info_delete, path_parent ); delete_rows( list_info_delete, Gtk::TreePath() ); if( m_undo_buffer ){ m_undo_buffer->set_list_info( CORE::DATA_INFO_LIST(), list_info_delete ); } // list_info を path_parent の中に追加 const bool before = false; const bool scroll = false; const bool force = false; const bool cancel_undo_commit = false; const int check_dup = 0; // 項目の重複チェックをしない append_info( list_info, path_parent, before, scroll, force, cancel_undo_commit, check_dup ); set_scroll( path_parent ); } //////////////////////////////// // // EditTreeViewの項目の反復子 // // path から反復開始 // path が empty の時はルートから反復する // EditTreeViewIterator::EditTreeViewIterator( EditTreeView& treeview, EditColumns& columns, const Gtk::TreePath path ) : m_treeview( treeview ) , m_columns( columns ) , m_path( path ) { const bool root = ( m_path.empty() ); if( root ){ Gtk::TreeModel::Children children = m_treeview.get_model()->children(); if( ! children.empty() ) m_path = m_treeview.get_model()->get_path( children.begin() ); else{ m_end = true; return; } } Gtk::TreeModel::Row row = m_treeview.get_row( m_path ); if( ! row ) m_end = true; else{ m_depth = m_path.size(); if( ! root ) ++m_depth; } } Gtk::TreeModel::Row EditTreeViewIterator::operator * () { return m_treeview.get_row( m_path ); } void EditTreeViewIterator::operator ++ () { if( m_end ) return; Gtk::TreeModel::Row row = m_treeview.get_row( m_path ); while( 1 ){ if( row ){ switch( row[ m_columns.m_type ] ){ case TYPE_DIR: m_path.down(); break; default: m_path.next(); break; } } else{ if( m_path.size() > m_depth ){ m_path.up(); m_path.next(); } else{ m_end = true; break; } } row = m_treeview.get_row( m_path ); if( row ) break; } } jdim-0.10.1/src/skeleton/edittreeview.h000066400000000000000000000266371445721505100200220ustar00rootroot00000000000000// ライセンス: GPL2 // // D&Dによって行の編集が可能なtreeviewクラス // // set_editable_view() で true を指定すると編集可能になる // #ifndef _EDITTREEVIEW_H #define _EDITTREEVIEW_H #include "dragtreeview.h" #include "type.h" #include "data_info.h" #include #include namespace XML { class Document; } namespace SKELETON { // ソートのモード enum { SORT_BY_TYPE = 0, SORT_BY_NAME }; // 他のwidgetからドロップされた typedef sigc::signal< void, const CORE::DATA_INFO_LIST& > SIG_DROPPED_FROM_OTHER; class EditColumns; class UNDO_BUFFER; class EditTreeView : public DragTreeView { SIG_DROPPED_FROM_OTHER m_sig_dropped_from_other; std::string m_url; Gtk::Window* m_parent_win{}; EditColumns& m_columns; Gtk::CellRendererText* m_ren_text{}; // Gtk managed // 編集可能 bool m_editable{}; // UNDO 用のバッファ UNDO_BUFFER* m_undo_buffer{}; // D&D用変数 Gtk::TreePath m_drag_path_uline; // D&D時に下線を引いている行 int m_dnd_counter{}; // D&D時のスクロールのタイミング用 bool m_exec_drop{}; // D&D時のドロップ処理で挿入するか Gtk::TreeRow m_row_dest{}; // ドロップ先 bool m_row_dest_before{}; // m_row_dest の前に挿入するか bool m_dropped_from_other{}; // 他のwidgetからドロップされた // スクロール用変数 // 詳しくは EditTreeView::clock_in() を参照 int m_pre_adjust_upper{}; Gtk::TreePath m_jump_path; int m_jump_count{}; // 更新された bool m_updated{}; // ドラッグがこのツリー上で行われている bool m_dragging_on_tree{}; // ディレクトリIDの最大値 size_t m_max_dirid{}; public: // ColumnRecord として SKELETON::EditColumns を派生したものを使用すること EditTreeView( const std::string& url, const std::string& dndtarget, EditColumns& columns, const bool use_usr_fontcolor, const std::string& fontname, const int colorid_text, const int colorid_bg, const int colorid_bg_even ); EditTreeView( const std::string& url, const std::string& dndtarget, EditColumns& columns ); ~EditTreeView(); void clock_in() override; SIG_DROPPED_FROM_OTHER sig_dropped_from_other(){ return m_sig_dropped_from_other; } void set_parent_win( Gtk::Window* parent_win ){ m_parent_win = parent_win; } Gtk::Window* get_parent_win(){ return m_parent_win; } void set_undo_buffer( UNDO_BUFFER* undo_buffer ){ m_undo_buffer = undo_buffer; } bool is_updated() const { return m_updated; }; void set_updated( const bool set ){ m_updated = set; } // treestoreのセット void set_treestore( const Glib::RefPtr< Gtk::TreeStore >& treestore ); // xml -> tree 展開して treestore をセットする void xml2tree( const XML::Document& document, Glib::RefPtr< Gtk::TreeStore >& treestore, const std::string& root_name ); // tree -> XML 変換 void tree2xml( XML::Document& document, const std::string& root_name ); // 列の作成 // ypad : 行間スペース Gtk::TreeViewColumn* create_column( const int ypad ); // 編集可能にする void set_editable_view( const bool editable ); // 指定した path のタイプは ディレクトリか bool is_dir( const Gtk::TreeModel::iterator& it ) const; bool is_dir( const Gtk::TreePath& path ); // 次のディレクトリに移動 void prev_dir(); void next_dir(); // 指定したアドレスの行が含まれているか bool exist_row( const std::string& url, const int type ); // ディレクトリ内を全選択 void select_all_dir( Gtk::TreePath path_dir ); // 新規ディレクトリ作成 Gtk::TreePath create_newdir( const Gtk::TreePath& path ); // ディレクトリIDとパスを相互変換 Gtk::TreePath dirid_to_path( const size_t dirid ); size_t path_to_dirid( const Gtk::TreePath path ); // コメント挿入 Gtk::TreePath create_newcomment( const Gtk::TreePath& path ); // pathで指定した行の名前の変更 void rename_row( const Gtk::TreePath& path ); bool is_renaming_row() const { return m_ren_text->property_editable(); } // list_info を path_dest 以下に追加 // // list_info の各path にあらかじめ値をセットしておくこと // scroll = true なら追加した行にスクロールする // force = true なら m_editable が false でも追加 // cancel_undo_commit = true なら undo バッファをコミットしない // check_dup == 0 ならチェックせず追加 1 なら重複チェックをして重複してたらダイアログ表示、2なら重複チェックして重複してたら追加しない // // (1) path_dest が empty なら一番最後 // (2) before = true なら path_dest の前 // (3) path_destがディレクトリなら path_dest の下 // (4) そうでなければ path_dest の後 CORE::DATA_INFO_LIST append_info( const CORE::DATA_INFO_LIST& list_info, const Gtk::TreePath& path_dest, const bool before, const bool scroll, const bool force, const bool cancel_undo_commit, int check_dup ); // pathをまとめて削除 // force = true なら m_editable が false でも削除 void delete_path( const std::list& list_path, const bool force ); // 選択した行をまとめて削除 // force = true なら m_editable が false でも削除 void delete_selected_rows( const bool force ) override; void undo(); void redo(); // 選択行をlist_infoにセットする // dir : true の時はディレクトリが選択されているときはディレクトリ内の行もlist_infoに再帰的にセットする void get_info_in_selection( CORE::DATA_INFO_LIST& list_info, const bool dir ); // 一行追加 // 戻り値は追加した行のpath // (1) path_dest が empty なら一番最後に作る // (2) before = true なら前に作る // (3) path_dest がディレクトリかつ sudir == true なら path_dest の下に追加。 // (4) そうでなければ path_dest の後に追加 Gtk::TreePath append_one_row( const std::string& url, const std::string& name, const int type, const size_t dirid, const std::string& data, const Gtk::TreePath& path_dest, const bool before, const bool subdir ); // ソート実行 void sort( const Gtk::TreePath& path, const int mode ); protected: bool on_drag_motion( const Glib::RefPtr& context, int x, int y, guint time ) override; void on_drag_leave( const Glib::RefPtr< Gdk::DragContext >& context, guint time ) override; bool on_drag_drop( const Glib::RefPtr& context, int x, int y, guint time ) override; void on_drag_data_get( const Glib::RefPtr< Gdk::DragContext >& context, Gtk::SelectionData& selection_data, guint info, guint time ) override; void on_drag_data_received( const Glib::RefPtr< Gdk::DragContext >& context, int x, int y, const Gtk::SelectionData& selection_data, guint info, guint time ) override; void on_drag_data_delete( const Glib::RefPtr& context ) override; void on_drag_end( const Glib::RefPtr< Gdk::DragContext>& context ) override; private: // path にスクロール void set_scroll( const Gtk::TreePath& path ); // set_model()をprivate化して使用不可にする。代わりにset_treestore()を使用すること void set_model( const Glib::RefPtr< Gtk::TreeModel >& model ){ Gtk::TreeView::set_model( model ); } // ディレクトリIDの最大値を取得 void get_max_dirid(); // ディレクトリにIDをセットする void set_dirid(); // 全てのツリーに m_columns.m_expand の値をセットする( tree2xml()で使用 ) void set_expanded_row( Glib::RefPtr< Gtk::TreeStore >& treestore, const Gtk::TreeModel::Children& children ); void slot_ren_text_on_edited( const Glib::ustring& path, const Glib::ustring& text ); void slot_ren_text_on_canceled(); // ドラッグ中にマウスカーソルの下に下線を引く void draw_underline_while_dragging( Gtk::TreePath path ); // draw == true なら pathに下線を引く void draw_underline( const Gtk::TreePath& path, bool const draw ); // list_info の各要素の path を path_dest 以下に変更 // list_info の各様の path にあらかじめ値をセットしておくこと // (1) path_dest が empty なら一番最後 // (2) before = true なら path_dest の前 // (3) path_destがディレクトリなら path_dest の下 // (4) そうでなければ path_dest の後 void replace_infopath( CORE::DATA_INFO_LIST& list_info, const Gtk::TreePath& path_dest, const bool before ); // ディレクトリ(path_dir)内の行を全てlist_infoにセットする void get_info_in_dir( CORE::DATA_INFO_LIST& list_info, const Gtk::TreePath& path_dir ); // path から info を取得 void path2info( CORE::DATA_INFO& info, const Gtk::TreePath& path ); // list_info に示した行の親を再起的にexpandする // list_info の各要素の path にあらかじめ値をセットしておくこと void expand_rows( const CORE::DATA_INFO_LIST& list_info ); // list_info に示した行を追加 // list_info の各要素の path にあらかじめ値をセットしておくこと void append_rows( const CORE::DATA_INFO_LIST& list_info ); // list_info に示した行を削除 // list_info の各要素の path にあらかじめ値をセットしておくこと // 削除した後、path_select にカーソルを移動する(emptyの場合は移動しない) void delete_rows( const CORE::DATA_INFO_LIST& list_info, const Gtk::TreePath& path_select ); // list_infoに示した行を選択 void select_info( const CORE::DATA_INFO_LIST& list_info ); }; //////////////////////////////// // EditTreeViewの項目の反復子 class EditTreeViewIterator { EditTreeView& m_treeview; EditColumns& m_columns; Gtk::TreePath::size_type m_depth; bool m_end{}; Gtk::TreePath m_path; public: // path から反復開始 // path が empty の時はルートから反復する EditTreeViewIterator( EditTreeView& treeview, EditColumns& columns, const Gtk::TreePath path ); Gtk::TreeModel::Row operator * (); Gtk::TreePath get_path() const { return m_path; } void operator ++ (); bool end() const { return m_end; } }; } #endif jdim-0.10.1/src/skeleton/editview.cpp000066400000000000000000000524621445721505100174700ustar00rootroot00000000000000// ライセンス: GPL2 //#define _DEBUG #include "gtkmmversion.h" #include "jddebug.h" #include "editview.h" #include "aamenu.h" #include "control/controlid.h" #include "control/controlutil.h" #include "aamanager.h" #include "environment.h" #include "session.h" #include "jdlib/misccharcode.h" #include "jdlib/miscutil.h" #include "config/globalconf.h" using namespace SKELETON; enum { MIN_AAMENU_LINES = 12 }; // extend-selectionのハンドラーはC APIを利用するためヘッダーには公開しない static gboolean EditTextView_slot_extend_selection( GtkTextView*, GtkTextExtendSelection granularity, GtkTextIter* location, GtkTextIter* start, GtkTextIter* end, gpointer ); // 区切り文字の判定 // FIXME: src/article/drawareabase.cpp にある同名の関数と統合する static inline bool is_separate_char( char32_t c ) { return g_unichar_isspace( c ) || g_unichar_ispunct( c ); } EditTextView::EditTextView() : Gtk::TextView() , m_pre_offset{ -1 } , m_pre_line{ -1 } , m_line_offset{ -1 } { // コントロールモード設定 m_control.add_mode( CONTROL::MODE_EDIT ); m_control.add_mode( CONTROL::MODE_MESSAGE ); get_buffer()->signal_changed().connect( sigc::mem_fun( *this, &EditTextView::slot_buffer_changed ) ); // NOTE: gtkmmでextend-selectionシグナルが公開されていなかった g_signal_connect( G_OBJECT( gobj() ), "extend-selection", G_CALLBACK( &EditTextView_slot_extend_selection ), nullptr ); } // メンバーに不完全型のスマートポインターがあるためデストラクタはinlineにできない EditTextView::~EditTextView() noexcept = default; // // カーソルの位置に挿入 // // use_br == true なら改行を入れる // void EditTextView::insert_str( const std::string& str, bool use_br ) { std::string br; if( use_br ){ Gtk::TextIter it = get_buffer()->get_insert()->get_iter(); if( it.get_chars_in_line() > 1 ) br = "\n\n"; else if( it.backward_char() && it.get_chars_in_line() > 1 ) br = "\n"; } get_buffer()->insert_at_cursor( br + str ); scroll_to( get_buffer()->get_insert(), 0.0 ); } void EditTextView::cursor_up_down( bool up ) { Gtk::TextIter it = get_buffer()->get_insert()->get_iter(); int line = it.get_line(); int offset = it.get_line_offset(); it.set_line_offset( 0 ); // 上 if( up ){ if( ! it.backward_line() ) return; } // 下 else{ if( ! it.forward_line() ){ // 最終行に文字が無い場合 if( get_buffer()->get_line_count() -1 == it.get_line() ) it.forward_to_end(); else return; } } if( m_line_offset >= 0 && offset == m_pre_offset && line == m_pre_line ) offset = m_line_offset; if( offset < it.get_chars_in_line() ){ it.set_line_offset( offset ); m_line_offset = -1; } // 文字数オーバー else{ m_line_offset = offset; if( it.get_chars_in_line() > 1 && ! it.forward_to_line_end() ) it.forward_to_end(); } get_buffer()->place_cursor( it ); scroll_to( get_buffer()->get_insert(), 0.0 ); m_pre_line = get_buffer()->get_insert()->get_iter().get_line(); m_pre_offset = get_buffer()->get_insert()->get_iter().get_line_offset(); } void EditTextView::cursor_up() { cursor_up_down( true ); } void EditTextView::cursor_down() { cursor_up_down( false ); } void EditTextView::cursor_left() { Gtk::TextIter it = get_buffer()->get_insert()->get_iter(); if( it.backward_char() ) get_buffer()->place_cursor( it ); } void EditTextView::cursor_right() { Gtk::TextIter it = get_buffer()->get_insert()->get_iter(); if( ! it.forward_char() ) it.forward_to_end(); get_buffer()->place_cursor( it ); } void EditTextView::cursor_home() { Gtk::TextIter it = get_buffer()->get_insert()->get_iter(); it.set_line_offset( 0 ); get_buffer()->place_cursor( it ); } void EditTextView::cursor_end() { Gtk::TextIter it = get_buffer()->get_insert()->get_iter(); if( ! it.forward_to_line_end() ) it.forward_to_end(); get_buffer()->place_cursor( it ); } void EditTextView::delete_char() { // 範囲選択消去 if( get_buffer()->erase_selection() ) return; Gtk::TextIter it = get_buffer()->get_insert()->get_iter(); Gtk::TextIter it2 = it; if( ! it2.forward_char() ) it2.forward_to_end(); m_delete_pushed = true; get_buffer()->erase( it, it2 ); } void EditTextView::backsp_char() { // 範囲選択消去 if( get_buffer()->erase_selection() ) return; Gtk::TextIter it = get_buffer()->get_insert()->get_iter(); Gtk::TextIter it2 = it; if( it2.backward_char() ) get_buffer()->erase( it2, it ); } // // バッファの文字列が変化したときに UNDO バッファを変更する // void EditTextView::slot_buffer_changed() { if( m_cancel_change ) return; #ifdef _DEBUG std::cout << "EditTextView::slot_buffer_changed\n"; #endif Glib::ustring text = get_buffer()->get_text(); unsigned int lng = text.length(); unsigned int pre_lng = m_pre_text.length(); unsigned int size = lng > pre_lng ? lng - pre_lng : pre_lng - lng; if( size ){ UndoDatum udata; udata.pos = 0; udata.append = false; // diff を取る for( ; udata.pos < MIN( lng, pre_lng ) && text.at( udata.pos ) == m_pre_text.at( udata.pos ); ++udata.pos ); // 追加 if( lng > pre_lng ){ udata.append = true; udata.str_diff = text.substr( udata.pos, size ); } // 削除 else udata.str_diff = m_pre_text.substr( udata.pos, size ); // カーソルの位置を取得 udata.pos_cursor = get_buffer()->get_insert()->get_iter().get_offset(); if( udata.append ) udata.pos_cursor -= size; else { if( size > 1 // 範囲選択で消した場合 || ! m_delete_pushed // backspaceで消した場合 ) udata.pos_cursor += size; } #ifdef _DEBUG std::cout << "size = " << size << " offset = " << udata.pos << " cursor = " << udata.pos_cursor; if( udata.append ) std::cout << " + "; else std::cout << " - "; std::cout << "pre = " << m_pre_text << " text = " << text << " diff = " << udata.str_diff << std::endl; #endif m_undo_tree.push_back( udata ); m_undo_pos = m_undo_tree.size() -1; } m_pre_text = text; } // // undo // void EditTextView::undo() { if( ! m_undo_tree.size() ) return; if( m_undo_pos < 0 ){ // 根元まで来たらツリーの先頭に戻る m_undo_pos = m_undo_tree.size() -1; return; } UndoDatum udata = m_undo_tree[ m_undo_pos-- ]; #ifdef _DEBUG std::cout << "EditTextView::undo pos = " << m_undo_pos << " size = " << m_undo_tree.size() << std::endl; std::cout << "offset = " << udata.pos << " cursor = " << udata.pos_cursor; if( udata.append ) std::cout << " - "; else std::cout << " + "; std::cout << udata.str_diff << std::endl; #endif m_cancel_change = true; // slot_buffer_changed() の呼出をキャンセル // 追加と削除を逆転 udata.append = ! udata.append; // 追加 if( udata.append ) get_buffer()->insert( get_buffer()->get_iter_at_offset( udata.pos ), udata.str_diff ); // 削除 else get_buffer()->erase( get_buffer()->get_iter_at_offset( udata.pos ), get_buffer()->get_iter_at_offset( udata.pos + udata.str_diff.length() ) ); // カーソル移動 Gtk::TextIter it = get_buffer()->get_iter_at_offset( udata.pos_cursor ); get_buffer()->place_cursor( it ); m_cancel_change = false; m_pre_text = get_buffer()->get_text(); // 逆方向にツリーを延ばす if( udata.append ) udata.pos_cursor += udata.str_diff.length(); m_undo_tree.push_back( udata ); } void EditTextView::clear_undo() { m_undo_tree.clear(); } // // マウスボタン入力のフック // bool EditTextView::on_button_press_event( GdkEventButton* event ) { m_sig_button_press.emit( event ); return Gtk::TextView::on_button_press_event( event ); } // // キー入力のフック // bool EditTextView::on_key_press_event( GdkEventKey* event ) { #ifdef _DEBUG std::cout << "EditTextView::on_key_press_event key = " << event->keyval << std::endl; #endif bool cancel_event = false; m_delete_pushed = false; if( event->keyval == GDK_KEY_Delete ) m_delete_pushed = true; const int controlid = m_control.key_press( event ); switch( controlid ){ case CONTROL::ExecWrite: case CONTROL::CancelWrite: case CONTROL::FocusWrite: case CONTROL::TabLeft: case CONTROL::TabRight: case CONTROL::TabLeftUpdated: case CONTROL::TabRightUpdated: case CONTROL::ToggleSage: case CONTROL::HomeEdit: case CONTROL::EndEdit: case CONTROL::UpEdit: case CONTROL::DownEdit: case CONTROL::RightEdit: case CONTROL::LeftEdit: case CONTROL::DeleteEdit: case CONTROL::BackspEdit: case CONTROL::UndoEdit: case CONTROL::InputAA: { if( im_context_filter_keypress( event ) ) { #ifdef _DEBUG std::cout << "gtk_im_context_filter_keypress\n"; #endif reset_im_context(); return true; } } } switch( controlid ){ // MessageViewでショートカットで書き込むと文字が挿入されてしまうので // キャンセルする case CONTROL::ExecWrite: case CONTROL::CancelWrite: case CONTROL::FocusWrite: case CONTROL::TabLeft: case CONTROL::TabRight: case CONTROL::ToggleSage: cancel_event = true; break; case CONTROL::HomeEdit: cursor_home(); return true; case CONTROL::EndEdit: cursor_end(); return true; case CONTROL::UpEdit: cursor_up(); return true; case CONTROL::DownEdit: cursor_down(); return true; case CONTROL::RightEdit: cursor_right(); return true; case CONTROL::LeftEdit: cursor_left(); return true; case CONTROL::DeleteEdit: delete_char(); return true; case CONTROL::BackspEdit: backsp_char(); return true; case CONTROL::UndoEdit: undo(); return true; case CONTROL::EnterEdit: event->keyval = GDK_KEY_Return; event->state &= ~GDK_CONTROL_MASK; event->state &= ~GDK_SHIFT_MASK; event->state &= ~GDK_MOD1_MASK; break; case CONTROL::InputAA: show_aalist_popup(); return true; } m_sig_key_press.emit( event ); if( cancel_event ) return true; return Gtk::TextView::on_key_press_event( event ); } bool EditTextView::on_key_release_event( GdkEventKey* event ) { #ifdef _DEBUG std::cout << "EditTextView::on_key_release_event key = " << event->keyval << std::endl; #endif bool cancel_event = false; switch( m_control.key_press( event ) ){ // MessageViewでショートカットで書き込むと文字が挿入されてしまうので // キャンセルする case CONTROL::ExecWrite: case CONTROL::CancelWrite: case CONTROL::FocusWrite: case CONTROL::TabLeft: case CONTROL::TabRight: case CONTROL::ToggleSage: cancel_event = true; break; case CONTROL::HomeEdit: case CONTROL::EndEdit: case CONTROL::UpEdit: case CONTROL::DownEdit: case CONTROL::RightEdit: case CONTROL::LeftEdit: case CONTROL::DeleteEdit: case CONTROL::BackspEdit: case CONTROL::UndoEdit: case CONTROL::InputAA: return true; } m_sig_key_release.emit( event ); if( cancel_event ) return true; return Gtk::TextView::on_key_release_event( event ); } // // コンテキストメニュー表示 // void EditTextView::on_populate_popup( Gtk::Menu* menu ) { #ifdef _DEBUG std::cout << "EditTextView::on_populate_popup\n"; #endif m_context_menu = menu; menu->signal_map().connect( sigc::mem_fun( *this, &EditTextView::slot_map_popupmenu ) ); menu->signal_hide().connect( sigc::mem_fun( *this, &EditTextView::slot_hide_popupmenu ) ); // セパレータ Gtk::MenuItem* menuitem = Gtk::manage( new Gtk::SeparatorMenuItem() ); menu->prepend( *menuitem ); // JDimの動作環境を記入 menuitem = Gtk::manage( new Gtk::MenuItem( "JDimの動作環境を記入" ) ); menuitem->signal_activate().connect( sigc::mem_fun( *this, &EditTextView::slot_write_jdinfo ) ); menu->prepend( *menuitem ); // 変換(スペース⇔ ) menuitem = Gtk::manage( new Gtk::MenuItem( "変換(スペース⇔ )" ) ); menuitem->signal_activate().connect( sigc::mem_fun( *this, &EditTextView::slot_convert_space ) ); menu->prepend( *menuitem ); // クリップボードから引用 menuitem = Gtk::manage( new Gtk::MenuItem( "クリップボードから引用" ) ); menuitem->signal_activate().connect( sigc::mem_fun( *this, &EditTextView::slot_quote_clipboard ) ); Glib::RefPtr< Gtk::Clipboard > clip = Gtk::Clipboard::get(); if( clip->wait_is_text_available() ) menuitem->set_sensitive( true ); else menuitem->set_sensitive( false ); menu->prepend( *menuitem ); // AA入力メニュー追加 if( CORE::get_aamanager()->get_size() ){ menuitem = Gtk::manage( new Gtk::MenuItem( CONTROL::get_label_motions( CONTROL::InputAA ) ) ); menuitem->signal_activate().connect( sigc::mem_fun( *this, &EditTextView::slot_select_aamenu ) ); menu->prepend( *menuitem ); } menu->show_all_children(); Gtk::TextView::on_populate_popup( menu ); } // // AA追加メニュー // void EditTextView::slot_select_aamenu() { if( m_context_menu ) m_context_menu->hide(); show_aalist_popup(); } // // クリップボードから引用して貼り付け // void EditTextView::slot_quote_clipboard() { if( m_context_menu ) m_context_menu->hide(); Glib::RefPtr< Gtk::Clipboard > clip = Gtk::Clipboard::get(); std::string text = clip->wait_for_text(); std::string str_res = CONFIG::get_ref_prefix(); text = MISC::replace_str( text, "\n", "\n" + str_res ); insert_str( str_res + text, false ); } // // 変換(スペース⇔ ) // void EditTextView::slot_convert_space() { if( m_context_menu ) m_context_menu->hide(); Glib::RefPtr< Gtk::TextBuffer > buffer = get_buffer(); std::string text = buffer->get_text(); std::string converted; //  が含まれていたらスペースに変換する if( text.find( " " ) != std::string::npos ) { converted = MISC::replace_str( text, " ", " " ); } else { std::list lines = MISC::get_lines( text ); for( std::string& line : lines ) { if( ! line.empty() ) { // 行末のスペースを取り除く line.erase( line.find_last_not_of( ' ' ) + 1 ); // 行頭のスペースを に変換する if( line.front() == ' ' ) { line.replace( 0, 1, " " ); } // 連続スペースを に変換する converted.append( MISC::replace_str( line, " ", "  " ) ); } converted.push_back( '\n' ); } // 最後の改行を取り除く converted.pop_back(); } buffer->set_text( converted ); } // // JDの動作環境を記入 // void EditTextView::slot_write_jdinfo() { if( m_context_menu ) m_context_menu->hide(); std::string jdinfo = ENVIRONMENT::get_jdinfo(); insert_str( jdinfo, false ); } // // ポップアップメニューがmapしたときに呼ばれるslot // void EditTextView::slot_map_popupmenu() { #ifdef _DEBUG std::cout << "EditTextView::slot_map_popupmenu\n"; #endif SESSION::set_popupmenu_shown( true ); } // // コンテキストメニューが閉じた // void EditTextView::slot_hide_popupmenu() { #ifdef _DEBUG std::cout << "EditTextView::slot_hide_popupmenu\n"; #endif m_context_menu = nullptr; SESSION::set_popupmenu_shown( false ); } // // カーソルの画面上の座標 // Gdk::Rectangle EditTextView::get_cursor_root_origin() { Gdk::Rectangle rect; int wx, wy; int x, y; get_iter_location( get_buffer()->get_insert()->get_iter(), rect ); buffer_to_window_coords( Gtk::TEXT_WINDOW_TEXT, rect.get_x(), rect.get_y(), wx, wy ); get_window( Gtk::TEXT_WINDOW_TEXT )->get_origin( x, y ); rect.set_x( x + wx ); rect.set_y( y + wy ); return rect; } // // AA ポップアップメニュー表示 // void EditTextView::show_aalist_popup() { if( CORE::get_aamanager()->get_size() ) { m_aapopupmenu = std::make_unique( *dynamic_cast( get_toplevel() ) ); m_aapopupmenu->sig_selected().connect( sigc::mem_fun( *this, &EditTextView::slot_aamenu_selected ) ); m_aapopupmenu->signal_map().connect( sigc::mem_fun( *this, &EditTextView::slot_map_aamenu ) ); m_aapopupmenu->signal_hide().connect( sigc::mem_fun( *this, &EditTextView::slot_hide_aamenu ) ); m_aapopupmenu->popup( Gtk::Menu::SlotPositionCalc( sigc::mem_fun( *this, &EditTextView::slot_popup_aamenu_pos ) ), 0, gtk_get_current_event_time() ); } } // // AA ポップアップの表示位置を決定 // void EditTextView::slot_popup_aamenu_pos( int& x, int& y, bool& push_in ) { push_in = false; const Gdk::Rectangle rect = get_cursor_root_origin(); const int line_height = rect.get_height(); const int sh = get_screen()->get_height(); const int min_height = MIN( CORE::get_aamanager()->get_size(), MIN_AAMENU_LINES ) * line_height; x = rect.get_x(); y = rect.get_y() + line_height; // 最低でも MIN_AAMENU_LINES 行よりも表示領域が高くなるようにする if( y + min_height > sh ) y = sh - min_height; } // // AAポップアップで選択された // void EditTextView::slot_aamenu_selected( const std::string& aa ) { insert_str( aa, false ); } // // AAポップアップメニューがmapしたときに呼ばれるslot // void EditTextView::slot_map_aamenu() { #ifdef _DEBUG std::cout << "EditTextView::slot_map_aamenu\n"; #endif SESSION::set_popupmenu_shown( true ); } // // AAポップアップが閉じた void EditTextView::slot_hide_aamenu() { #ifdef _DEBUG std::cout << "EditTextView::slot_hide_aamenu\n"; #endif SESSION::set_popupmenu_shown( false ); } // // 範囲選択を実行する // static gboolean EditTextView_slot_extend_selection( GtkTextView*, GtkTextExtendSelection granularity, GtkTextIter* location, GtkTextIter* start, GtkTextIter* end, gpointer ) { const char32_t loc_char = gtk_text_iter_get_char( location ); if( loc_char == 0 ) return GDK_EVENT_PROPAGATE; gtk_text_iter_assign( start, location ); gtk_text_iter_assign( end, location ); Gtk::TextIter& start_iter = Glib::wrap( start ); Gtk::TextIter& end_iter = Glib::wrap( end ); if( granularity == GTK_TEXT_EXTEND_SELECTION_WORD ) { const auto block = MISC::get_unicodeblock( loc_char ); const bool sep = is_separate_char( loc_char ); const auto find_char = [block, sep]( char32_t c ) { return block != MISC::get_unicodeblock( c ) || sep != is_separate_char( c ); }; if( start_iter.backward_find_char( find_char ) ) { start_iter.forward_char(); } end_iter.forward_find_char( find_char ); return GDK_EVENT_STOP; } else if( granularity == GTK_TEXT_EXTEND_SELECTION_LINE ) { while( start_iter.backward_char() ) { if( start_iter.ends_line() ) { start_iter.forward_char(); break; } } if( !end_iter.ends_line() ) { end_iter.forward_to_line_end(); } return GDK_EVENT_STOP; } return GDK_EVENT_PROPAGATE; } ////////////////////////////////////////////// EditView::EditView() : Gtk::ScrolledWindow() { set_policy( Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC ); add( m_textview ); auto context = m_textview.get_style_context(); context->add_class( s_css_classname ); context->add_provider( m_provider, GTK_STYLE_PROVIDER_PRIORITY_APPLICATION ); show_all_children(); } EditView::~EditView() noexcept = default; constexpr const char* EditView::s_css_classname; // // EditTextViewのスタイルを更新する // void EditView::update_style( const Glib::ustring& custom_css ) { #ifdef _DEBUG std::cout << "EditView::update_style custom css: " << custom_css << std::endl; #endif try { m_provider->load_from_data( custom_css ); } catch( Gtk::CssProviderError& err ) { #ifdef _DEBUG std::cout << "ERROR:EditView::update_style fail " << err.what() << std::endl; #endif } } jdim-0.10.1/src/skeleton/editview.h000066400000000000000000000114201445721505100171220ustar00rootroot00000000000000// ライセンス: GPL2 #ifndef _EDITVIEW_H #define _EDITVIEW_H #include "control/control.h" #include #include namespace SKELETON { class AAMenu; typedef sigc::signal< bool, GdkEventKey* > SIG_KEY_PRESS; typedef sigc::signal< bool, GdkEventKey* > SIG_KEY_RELEASE; typedef sigc::signal< bool, GdkEventButton* > SIG_BUTTON_PRESS; // undo 用のバッファ struct UndoDatum { Glib::ustring str_diff; unsigned int pos; unsigned int pos_cursor; bool append; }; // キーのプレスとリリースをフックする class EditTextView : public Gtk::TextView { SIG_KEY_PRESS m_sig_key_press; SIG_KEY_RELEASE m_sig_key_release; SIG_BUTTON_PRESS m_sig_button_press; // 入力コントローラ CONTROL::Control m_control; // undo 用 std::vector m_undo_tree; int m_undo_pos{}; Glib::ustring m_pre_text; bool m_cancel_change{}; bool m_delete_pushed{}; // カーソル移動用 int m_pre_offset; int m_pre_line; int m_line_offset; // コンテキストメニュー Gtk::Menu* m_context_menu{}; // AAポップアップ std::unique_ptr m_aapopupmenu; public: SIG_KEY_PRESS sig_key_press(){ return m_sig_key_press; } SIG_KEY_RELEASE sig_key_release(){ return m_sig_key_release; } SIG_BUTTON_PRESS sig_button_press() { return m_sig_button_press; } EditTextView(); ~EditTextView() noexcept; void insert_str( const std::string& str, bool use_br ); void cursor_up(); void cursor_down(); void cursor_left(); void cursor_right(); void cursor_home(); void cursor_end(); void delete_char(); void backsp_char(); void undo(); void clear_undo(); // カーソルの画面上の座標 Gdk::Rectangle get_cursor_root_origin(); protected: bool on_button_press_event( GdkEventButton* event ) override; bool on_key_press_event( GdkEventKey* event ) override; bool on_key_release_event( GdkEventKey* event ) override; void on_populate_popup( Gtk::Menu* menu ) override; void slot_buffer_changed(); private: void cursor_up_down( bool up ); void slot_select_aamenu(); void slot_map_popupmenu(); void slot_hide_popupmenu(); // クリップボードから引用 void slot_quote_clipboard(); // 変換(スペース⇔ ) void slot_convert_space(); // JDの動作環境を記入 void slot_write_jdinfo(); // AA ポップアップ void slot_popup_aamenu_pos( int& x, int& y, bool& push_in ); void show_aalist_popup(); void slot_aamenu_selected( const std::string& aa ); void slot_map_aamenu(); void slot_hide_aamenu(); }; class EditView : public Gtk::ScrolledWindow { EditTextView m_textview; static constexpr const char* s_css_classname = u8"jd-editview"; Glib::RefPtr< Gtk::CssProvider > m_provider = Gtk::CssProvider::create(); public: EditView(); ~EditView() noexcept; SIG_BUTTON_PRESS sig_button_press(){ return m_textview.sig_button_press(); } SIG_KEY_PRESS sig_key_press(){ return m_textview.sig_key_press(); } SIG_KEY_RELEASE sig_key_release(){ return m_textview.sig_key_release(); } Glib::RefPtr< Gtk::TextBuffer > get_buffer(){ return m_textview.get_buffer(); } void set_text( const Glib::ustring& text ){ m_textview.get_buffer()->set_text( text ); } Glib::ustring get_text() const { return m_textview.get_buffer()->get_text(); } void set_wrap_mode( Gtk::WrapMode wrap_mode ){ m_textview.set_wrap_mode( wrap_mode ); } const char* get_css_classname() const noexcept { return s_css_classname; } // EditTextViewのスタイルを更新する void update_style( const Glib::ustring& custom_css ); void insert_str( const std::string& str, bool use_br ){ m_textview.insert_str( str, use_br ); } void set_editable( bool editable ){ m_textview.set_editable( editable ); } void set_accepts_tab( bool accept ){ m_textview.set_accepts_tab( accept ); } void modify_font( const Pango::FontDescription& font_desc ) { m_textview.override_font( font_desc ); } void focus_view(){ m_textview.grab_focus(); } void undo(){ m_textview.undo(); } void clear_undo(){ m_textview.clear_undo(); } // カーソルの画面上の座標 Gdk::Rectangle get_cursor_root_origin(){ return m_textview.get_cursor_root_origin(); } }; } #endif jdim-0.10.1/src/skeleton/editviewdialog.h000066400000000000000000000013011445721505100202770ustar00rootroot00000000000000// ライセンス: GPL2 #ifndef _EDITVIEWDIALOG_H #define _EDITVIEWDIALOG_H #include #include #include "editview.h" namespace SKELETON { class EditViewDialog : public Gtk::Dialog { SKELETON::EditView m_edit; public: EditViewDialog( const std::string& str, const std::string& title, bool editable ){ m_edit.set_text( str ); m_edit.set_editable( editable ); add_button( g_dgettext( GTK_DOMAIN, "_OK" ), Gtk::RESPONSE_OK ); get_content_area()->pack_start( m_edit ); set_title( title ); show_all_children(); } }; } #endif jdim-0.10.1/src/skeleton/entry.cpp000066400000000000000000000042101445721505100167750ustar00rootroot00000000000000// ライセンス: GPL2 //#define _DEBUG #include "jddebug.h" #include "entry.h" #include "control/controlid.h" using namespace SKELETON; JDEntry::~JDEntry() noexcept = default; // ボタン入力のフック bool JDEntry::on_button_press_event( GdkEventButton* event ) { #ifdef _DEBUG std::cout << "JDEntry::on_button_press_event\n"; #endif m_sig_button_press.emit( event ); return Gtk::Entry::on_button_press_event( event ); } // キー入力のフック bool JDEntry::on_key_press_event( GdkEventKey* event ) { #ifdef _DEBUG std::cout << "JDEntry::on_key_press_event key = " << event->keyval << std::endl; #endif m_controlid = CONTROL::None; const guint key = event->keyval; const bool ctrl = ( event->state ) & GDK_CONTROL_MASK; const bool up = ( key == GDK_KEY_Up || ( ctrl && key == 'p' ) ); const bool down = ( key == GDK_KEY_Down || ( ctrl && key == 'n' ) ); const bool esc = ( key == GDK_KEY_Escape ); // 上下をキャンセル // gtkentry.cpp からのハック。環境やバージョンによっては問題が出るかもしれないので注意 if( up || down || esc ){ if( im_context_filter_keypress( event ) ) { #ifdef _DEBUG std::cout << "gtk_im_context_filter_keypress\n"; #endif reset_im_context(); return TRUE; } else if( up || down ){ if( up ) m_sig_operate.emit( CONTROL::Up ); else m_sig_operate.emit( CONTROL::Down ); return TRUE; } } m_sig_key_press.emit( event->keyval ); m_controlid = m_control.key_press( event ); return Gtk::Entry::on_key_press_event( event ); } bool JDEntry::on_key_release_event( GdkEventKey* event ) { const bool ret = Gtk::Entry::on_key_release_event( event ); #ifdef _DEBUG std::cout << "JDEntry::on_key_release_event id = " << m_controlid << std::endl; #endif switch( m_controlid ){ case CONTROL::DrawOutAnd: case CONTROL::SearchCache: case CONTROL::Cancel: m_sig_operate.emit( m_controlid ); break; } m_controlid = CONTROL::None; return ret; } jdim-0.10.1/src/skeleton/entry.h000066400000000000000000000027131445721505100164500ustar00rootroot00000000000000// ライセンス: GPL2 // // キーボードフックしたentryクラス // #ifndef _ENTRY_H #define _ENTRY_H #include "control/control.h" #include namespace SKELETON { class JDEntry : public Gtk::Entry { typedef sigc::signal< void, GdkEventButton* > SIG_BUTTON_PRESS; typedef sigc::signal< void, int > SIG_KEY_PRESS; typedef sigc::signal< void, int > SIG_OPERATE; SIG_BUTTON_PRESS m_sig_button_press; SIG_KEY_PRESS m_sig_key_press; SIG_OPERATE m_sig_operate; // 入力コントローラ CONTROL::Control m_control; int m_controlid{}; public: SIG_BUTTON_PRESS signal_button_press(){ return m_sig_button_press; } SIG_KEY_PRESS signal_key_press(){ return m_sig_key_press; } SIG_OPERATE signal_operate(){ return m_sig_operate; } using Gtk::Entry::Entry; ~JDEntry() noexcept; // CONTROL::Control のモード設定( controlid.h 参照 ) // キー入力をフックして JDEntry::on_key_release_event() で SIG_OPERATE をemitする void add_mode( const int mode ){ m_control.add_mode( mode ); } protected: // マウスクリックのフック bool on_button_press_event( GdkEventButton* event ) override; // キー入力のフック bool on_key_press_event( GdkEventKey* event ) override; bool on_key_release_event( GdkEventKey* event ) override; }; } #endif jdim-0.10.1/src/skeleton/filediag.h000066400000000000000000000037051445721505100170550ustar00rootroot00000000000000// ライセンス: GPL2 // ファイル選択ダイアログの基底クラス #ifndef _FILEDIAG_H #define _FILEDIAG_H #include #include #include "command.h" #include "session.h" namespace SKELETON { class FileDiag : public Gtk::FileChooserDialog { Gtk::FileChooserAction m_action; public: // ボタン追加 + saveボタンをデフォルトボタンにセット void add_buttons(){ add_button( g_dgettext( GTK_DOMAIN, "_Cancel" ), Gtk::RESPONSE_CANCEL ); if( m_action == Gtk::FILE_CHOOSER_ACTION_OPEN ) { add_button( g_dpgettext( GTK_DOMAIN, "Stock label\x04_Open", 12 ), Gtk::RESPONSE_ACCEPT ); } else add_button( g_dpgettext( GTK_DOMAIN, "Stock label\x04_Save", 12 ), Gtk::RESPONSE_ACCEPT ); set_default_response( Gtk::RESPONSE_ACCEPT ); } FileDiag( Gtk::Window& parent, const Glib::ustring& title, Gtk::FileChooserAction action = Gtk::FILE_CHOOSER_ACTION_OPEN ) : Gtk::FileChooserDialog( parent, title, action ), m_action( action ) { add_buttons(); } // parent がポインタの時は nullptr かどうかで場合分け FileDiag( Gtk::Window* parent, const Glib::ustring& title, Gtk::FileChooserAction action = Gtk::FILE_CHOOSER_ACTION_OPEN ) : Gtk::FileChooserDialog( title, action ), m_action( action ) { add_buttons(); if( parent ) set_transient_for( *parent ); else set_transient_for( *CORE::get_mainwindow() ); } ~FileDiag() noexcept = default; virtual int run(){ SESSION::set_dialog_shown( true ); CORE::core_set_command( "dialog_shown" ); int ret = Gtk::FileChooserDialog::run(); SESSION::set_dialog_shown( false ); CORE::core_set_command( "dialog_hidden" ); return ret; } }; } #endif jdim-0.10.1/src/skeleton/hpaned.cpp000066400000000000000000000022731445721505100171020ustar00rootroot00000000000000// ライセンス: GPL2 //#define _DEBUG #include "jddebug.h" #include "hpaned.h" using namespace SKELETON; JDHPaned::JDHPaned( const int fixmode ) : Gtk::Paned( Gtk::ORIENTATION_HORIZONTAL ) , m_pctrl( *this, fixmode ) {} JDHPaned::~JDHPaned() noexcept = default; void JDHPaned::on_realize() { Gtk::Paned::on_realize(); m_pctrl.update_position(); } bool JDHPaned::on_button_press_event( GdkEventButton* event ) { m_pctrl.button_press_event( event ); return Gtk::Paned::on_button_press_event( event ); } bool JDHPaned::on_button_release_event( GdkEventButton* event ) { m_pctrl.button_release_event( event ); return Gtk::Paned::on_button_release_event( event ); } bool JDHPaned::on_motion_notify_event( GdkEventMotion* event ) { m_pctrl.motion_notify_event( event ); return Gtk::Paned::on_motion_notify_event( event ); } bool JDHPaned::on_enter_notify_event( GdkEventCrossing* event ) { m_pctrl.enter_notify_event( event ); return Gtk::Paned::on_enter_notify_event( event ); } bool JDHPaned::on_leave_notify_event( GdkEventCrossing* event ) { m_pctrl.leave_notify_event( event ); return Gtk::Paned::on_leave_notify_event( event ); } jdim-0.10.1/src/skeleton/hpaned.h000066400000000000000000000014461445721505100165500ustar00rootroot00000000000000// ライセンス: GPL2 // // HPaneeクラス // #ifndef _HPANED_H #define _HPANED_H #include #include "panecontrol.h" namespace SKELETON { class JDHPaned : public Gtk::Paned { HPaneControl m_pctrl; public: explicit JDHPaned( const int fixmode ); ~JDHPaned() noexcept; HPaneControl& get_ctrl(){ return m_pctrl; } protected: void on_realize() override; bool on_button_press_event( GdkEventButton* event ) override; bool on_button_release_event( GdkEventButton* event ) override; bool on_motion_notify_event( GdkEventMotion* event ) override; bool on_enter_notify_event( GdkEventCrossing* event ) override; bool on_leave_notify_event( GdkEventCrossing* event ) override; }; } #endif jdim-0.10.1/src/skeleton/iconpopup.h000066400000000000000000000021411445721505100173160ustar00rootroot00000000000000// ライセンス: GPL2 #ifndef _ICONPOPUP_H #define _ICONPOPUP_H #include #include "icons/iconmanager.h" namespace SKELETON { class IconPopup : public Gtk::Window { Glib::RefPtr< Gdk::Pixbuf > m_pixbuf; Gtk::Image m_img; public: explicit IconPopup( const int icon_id ) : Gtk::Window( Gtk::WINDOW_POPUP ) , m_pixbuf{ ICON::get_icon( icon_id ) } { m_img.set( m_pixbuf ); // NOTE: アルファチャンネルが利用できない環境では背景を透過できない set_decorated( false ); set_app_paintable( true ); auto screen = get_screen(); auto visual = screen->get_rgba_visual(); if( visual && screen->is_composited() ) { gtk_widget_set_visual( GTK_WIDGET( gobj() ), visual->gobj() ); } add( m_img ); show_all_children(); } int get_img_width() const { return m_pixbuf->get_width(); } int get_img_height() const { return m_pixbuf->get_height(); } }; } #endif jdim-0.10.1/src/skeleton/imgtoolbutton.h000066400000000000000000000022671445721505100202210ustar00rootroot00000000000000// ライセンス: GPL2 // 画像つきツールボタン #ifndef _IMGTOOLBUTTON_H #define _IMGTOOLBUTTON_H #include #include #include #include "control/controlutil.h" #include "icons/iconmanager.h" namespace SKELETON { template class ToolButtonExtension : public Base { static_assert( std::is_base_of::value, "Base must inherit Gtk::ToolButton" ); public: explicit ToolButtonExtension( const int iconid ) : Base( *Gtk::manage( new Gtk::Image( ICON::get_icon( iconid ) ) ) ) {} explicit ToolButtonExtension( const int iconid, const int controlid ) : Base( *Gtk::manage( new Gtk::Image( ICON::get_icon( iconid ) ) ), CONTROL::get_label( controlid ) ) {} explicit ToolButtonExtension( const int iconid, const Glib::ustring& label ) : Base( *Gtk::manage( new Gtk::Image( ICON::get_icon( iconid ) ) ), label ) {} ~ToolButtonExtension() noexcept = default; }; using ImgToolButton = ToolButtonExtension; using ImgToggleToolButton = ToolButtonExtension; } #endif jdim-0.10.1/src/skeleton/jdtoolbar.cpp000066400000000000000000000006601445721505100176210ustar00rootroot00000000000000// ライセンス: GPL2 //#define _DEBUG #include "jddebug.h" #include "jdtoolbar.h" using namespace SKELETON; JDToolbar::JDToolbar() : Gtk::Toolbar() { // 子ウィジェットの配色がGTKテーマと違うことがある。 // ツールバーのcssクラスを削除し配色を修正する。 get_style_context()->remove_class( GTK_STYLE_CLASS_TOOLBAR ); } JDToolbar::~JDToolbar() noexcept = default; jdim-0.10.1/src/skeleton/jdtoolbar.h000066400000000000000000000007751445721505100172750ustar00rootroot00000000000000// ライセンス: GPL2 // // カスタマイズした Gtk::Toolbar ( 背景を描画しない ) // // (注) DragableNoteBookにappendするのは SKELETON::ToolBar // // TODO: クライアント側で Gtk::Toolbar に置き換える // TODO: CONFIG::(set|get)_draw_toolbarback() を削除する #ifndef JDTOOLBAR_H #define JDTOOLBAR_H #include namespace SKELETON { class JDToolbar : public Gtk::Toolbar { public: JDToolbar(); ~JDToolbar() noexcept; }; } #endif jdim-0.10.1/src/skeleton/label_entry.cpp000066400000000000000000000037311445721505100201430ustar00rootroot00000000000000// ライセンス: GPL2 //#define _DEBUG #include "jddebug.h" #include "label_entry.h" #include "view.h" using namespace SKELETON; LabelEntry::LabelEntry( const bool editable, const std::string& label, const std::string& text ) : m_editable( editable ) { set_label( label ); m_label.set_mnemonic_widget ( m_entry ); m_entry.signal_activate().connect( sigc::mem_fun( *this, &LabelEntry::slot_entry_acivate ) ); pack_start( m_label, Gtk::PACK_SHRINK ); m_info.set_size_request( 0, 0 ); m_info.set_xalign( 0 ); m_info.set_selectable( true ); m_info.set_ellipsize( Pango::ELLIPSIZE_END ); setup(); set_text( text ); } LabelEntry::~LabelEntry() noexcept = default; void LabelEntry::setup() { if( m_editable ) pack_start( m_entry ); else pack_start( m_info ); show_all_children(); } void LabelEntry::set_editable( const bool editable ) { if( m_editable == editable ) return; #ifdef _DEBUG std::cout << "LabelEntry::set_editable editable = " << editable << std::endl; #endif if( m_editable ) remove( m_entry ); else remove( m_info ); m_editable = editable; setup(); } void LabelEntry::set_visibility( bool visibility ) { if( m_editable ) m_entry.set_visibility( visibility ); } void LabelEntry::set_label( const std::string& label ) { m_label.set_text_with_mnemonic( label ); } void LabelEntry::set_text( const std::string& text ) { m_entry.set_text( text ); m_info.set_text( text ); } Glib::ustring LabelEntry::get_text() const { if( m_editable ) return m_entry.get_text(); return m_info.get_text(); } void LabelEntry::grab_focus() { if( m_editable ) m_entry.grab_focus(); } bool LabelEntry::has_grab() const { if( m_editable ) return m_entry.has_grab(); return false; } // entry からsignal_activateを受け取った void LabelEntry::slot_entry_acivate() { #ifdef _DEBUG std::cout << "LabelEntry::slot_entry_acivate\n"; #endif m_sig_activate.emit(); } jdim-0.10.1/src/skeleton/label_entry.h000066400000000000000000000021431445721505100176040ustar00rootroot00000000000000// ライセンス: GPL2 // // ラベル + ラベル / エントリー // // プロパティの表示用 // #ifndef _LABEL_ENTRY_H #define _LABEL_ENTRY_H #include namespace SKELETON { class LabelEntry : public Gtk::HBox { typedef sigc::signal< void > SIG_ACTIVATE; SIG_ACTIVATE m_sig_activate; bool m_editable; Gtk::Label m_label; Gtk::Label m_info; Gtk::Entry m_entry; public: LabelEntry( const bool editable, const std::string& label, const std::string& text = std::string() ); ~LabelEntry() noexcept; SIG_ACTIVATE signal_activate(){ return m_sig_activate; } void set_editable( const bool editable ); void set_visibility( const bool visibility ); void set_label( const std::string& label ); void set_text( const std::string& text ); Glib::ustring get_text() const; void grab_focus(); bool has_grab() const; private: void setup(); // entry からsignal_activateを受け取った void slot_entry_acivate(); }; } #endif jdim-0.10.1/src/skeleton/loadable.cpp000066400000000000000000000205531445721505100174070ustar00rootroot00000000000000// ライセンス: GPL2 //#define _DEBUG #include "jddebug.h" #include "loadable.h" #include "jdlib/loader.h" #include "jdlib/misccharcode.h" #include "jdlib/misctime.h" #include "jdlib/miscutil.h" #include "httpcode.h" using namespace SKELETON; /** @brief HTTPやHTMLからテキストの文字エンコーディングを検出する処理の状態 */ enum class Loadable::CharsetDetection { parse_header, ///< HTTP header を解析する parse_meta, ///< HTML meta 要素を解析する finished, ///< 検出を終えた }; Loadable::Loadable() : m_charset_det{ CharsetDetection::parse_header } , m_encoding{ Encoding::unknown } { clear_load_data(); } Loadable::~Loadable() { terminate_load(); // 一応デストラクタ内部でも実行しておく } // ロード強制停止 void Loadable::terminate_load() { #ifdef _DEBUG std::cout << "Loadable::terminate_load\n"; #endif // loadableを delete する前に terminate_load() を呼び出さないと // スレッド実行中にメモリなどが初期化されてしまうため落ちる時がある // デストラクタ内から terminate_load() しても落ちる時があるので // デストラクタの外から呼び出すこと set_dispatchable( false ); m_loader.reset(); } // 完全クリア void Loadable::clear_load_data() { m_charset_det = CharsetDetection::parse_header; m_code = HTTP_INIT; m_str_code = std::string(); m_date_modified = std::string(); m_cookies.clear(); m_location = std::string(); m_total_length = 0; m_current_length = 0; } bool Loadable::is_loading() const { // m_loader != nullptr ならローダ起動中ってこと return static_cast( m_loader ); } // // 更新時刻 // time_t Loadable::get_time_modified() { time_t time_out; time_out = MISC::datetotime( m_date_modified ); if( time_out == 0 ) time_out = time( nullptr ) - 600; #ifdef _DEBUG std::cout << "Loadable::get_time_modified " << m_date_modified << std::endl << " -> " << time_out << std::endl; #endif return time_out; } // // ロード開始 // bool Loadable::start_load( const JDLIB::LOADERDATA& data ) { if( is_loading() ) return false; #ifdef _DEBUG std::cout << "Loadable::start_load url = " << data.url << std::endl; #endif assert( m_loader == nullptr ); m_loader = std::make_unique( m_low_priority ); // 情報初期化 // m_date_modified, m_cookie は初期化しない m_charset_det = CharsetDetection::parse_header; m_code = HTTP_INIT; m_str_code = std::string(); m_location = std::string(); m_total_length = 0; m_current_length = 0; if( !m_loader->run( this, data ) ){ m_code = get_loader_code(); m_str_code = get_loader_str_code(); m_loader.reset(); return false; } return true; } // // ロード中止 // void Loadable::stop_load() { if( m_loader ) m_loader->stop(); } // ローダーからコールバックされてコードなどを取得してから // receive_data() を呼び出す void Loadable::receive( const char* data, size_t size ) { m_code = get_loader_code(); if( ! m_total_length && m_code != HTTP_INIT ) m_total_length = get_loader_length(); m_current_length += size; if( m_charset_det == CharsetDetection::parse_header ) { const Encoding enc = get_loader_content_charset(); if( enc != Encoding::unknown ) { set_encoding( enc ); // The HTTP header has a higher precedence than the in-HTML elements. m_charset_det = CharsetDetection::finished; } else if( m_loader && MISC::ascii_ignore_case_find( m_loader->data().contenttype, "html" ) != std::string::npos ) { // If the content MIME type is text/html or application/xhtml+xml, // find charset from elements. m_charset_det = CharsetDetection::parse_meta; } else { m_charset_det = CharsetDetection::finished; } } if( m_charset_det == CharsetDetection::parse_meta ) { std::string buf( data, size ); const std::string charset = MISC::parse_charset_from_html_meta( buf ); if( ! charset.empty() ) { const Encoding enc = MISC::encoding_from_web_charset( charset ); if( enc != Encoding::unknown ) set_encoding( enc ); m_charset_det = CharsetDetection::finished; } else if( m_current_length >= 1024 ) { // elements which declare a character encoding must be located // entirely within the first 1024 bytes of the document. m_charset_det = CharsetDetection::finished; } } receive_data( std::string_view{ data, size } ); } // 別スレッドで動いているローダからfinish()を呼ばれたらディスパッチして // メインスレッドに制御を戻してから callback_dispatch()を呼び出す。 // そうしないと色々不具合が生じる void Loadable::finish() { #ifdef _DEBUG std::cout << "Loadable::finish\n"; #endif dispatch(); } // // ローダを削除してreceive_finish()をコール // void Loadable::callback_dispatch() { #ifdef _DEBUG std::cout << "Loadable::callback_dispatch\n"; #endif // ローダを削除する前に情報保存 m_code = get_loader_code(); if( ! get_loader_str_code().empty() ) m_str_code = get_loader_str_code(); if( ! get_loader_contenttype().empty() ) m_contenttype = get_loader_contenttype(); if( ! get_loader_modified().empty() ) m_date_modified = get_loader_modified(); if( ! get_loader_cookies().empty() ) m_cookies = get_loader_cookies(); if( ! get_loader_location().empty() ) m_location = get_loader_location(); if( m_charset_det == CharsetDetection::parse_header ) { const Encoding enc = get_loader_content_charset(); if( enc != Encoding::unknown ) set_encoding( enc ); m_charset_det = CharsetDetection::finished; } #ifdef _DEBUG std::cout << "delete loader\n"; #endif m_loader.reset(); #ifdef _DEBUG std::cout << "code = " << m_code << std::endl; std::cout << "str_code = " << m_str_code << std::endl; std::cout << "contenttype = " << m_contenttype << std::endl; std::cout << "modified = " << m_date_modified << std::endl; std::cout << "location = " << m_location << std::endl; std::cout << "total_length = " << m_total_length << std::endl; std::cout << "current length = " << m_current_length << std::endl; std::cout << "charset = " << MISC::encoding_to_cstr( get_encoding() ) << std::endl; #endif receive_finish(); } // ローダから各種情報の取得 int Loadable::get_loader_code() const { if( ! m_loader ) return HTTP_INIT; return m_loader->data().code; } std::string Loadable::get_loader_str_code() const { if( ! m_loader ) return std::string(); return m_loader->data().str_code; } std::string Loadable::get_loader_contenttype() const { if( ! m_loader ) return std::string(); return m_loader->data().contenttype; } std::string Loadable::get_loader_modified() const { if( ! m_loader ) return std::string(); return m_loader->data().modified; } std::list< std::string > Loadable::get_loader_cookies() const { if( ! m_loader ) return std::list< std::string >(); return m_loader->data().list_cookies; } std::string Loadable::get_loader_location() const { if( ! m_loader ) return std::string(); return m_loader->data().location; } size_t Loadable::get_loader_length() const { if( ! m_loader ) return 0; return m_loader->data().length; } /** @brief HTTP header Content-Type から文字エンコーディング情報を取得する * * @return 文字エンコーディングの列挙型 * @retval Encoding::unknown エンコーディング情報が見つからない、またはJDimでは処理しないエンコーディングだった */ Encoding Loadable::get_loader_content_charset() const { if( m_loader ) { const std::string& contenttype = m_loader->data().contenttype; const std::size_t pos = contenttype.find( "charset=" ); if( pos != std::string::npos ){ const std::string raw_charset = MISC::utf8_trim( std::string_view{ contenttype }.substr( pos + 8 ) ); return MISC::encoding_from_web_charset( raw_charset ); } } return Encoding::unknown; } jdim-0.10.1/src/skeleton/loadable.h000066400000000000000000000130671445721505100170560ustar00rootroot00000000000000// ライセンス: GPL2 // // ロード可能クラスの基底クラス // JDLIB::Loaderを使ってデータを受信する // // ・start_load() でロード開始。JDLIB::LOADERDATA data を引数に渡す // ・stop_load() で停止 // // ロード中はreceive_data()がコールバックされてデータが送られる // ロードが終わると receive_finish() がコールバックされる // // ・ delete する前に terminate_load() を明示的に呼び出すこと(terminate_load()のコメントを参照) // // 詳しい流れは次の通り /* (1) start_load() の JDLIB::create_loader() でローダが作成される (2) m_loader->run() でロード開始 --------ここから別スレッド内で実行 (3) ローダがデータを受け取るとreceive()がコールバックされてhttpコードやサイズを取得する (4) receive()からreceive_data()が呼び出される (5) ロードが終了したらfinish()が呼ばれて Dispatchable::dispatch()により ディスパッチャを使ってメインスレッドに制御を戻す --------ここからメインスレッドに戻る (6) ディスパッチャ経由で Dispatchable::callback_dispatch() が呼び出される (7) クッキー、更新時刻などを取得する (8) m_loader.reset()でローダの停止を待って削除する (9) receive_finish()を呼び出す 注意点 ・receive_data()は別スレッド内で実行される ・receive_finish() はメインスレッド内で実行される ・receive_finish()が呼ばれた時点で既にローダは削除されているので明示的に削除する必要はない */ #ifndef _LOADABLE_H #define _LOADABLE_H #include "jdencoding.h" #include "dispatchable.h" #include #include #include #include #include namespace JDLIB { class Loader; class LOADERDATA; } namespace SKELETON { class Loadable : public Dispatchable { enum class CharsetDetection; std::unique_ptr m_loader; bool m_low_priority{}; CharsetDetection m_charset_det; ///< HTTPやHTMLからテキストの文字エンコーディングを検出する処理の状態 Encoding m_encoding; // ローダからコピーしたデータ int m_code; std::string m_str_code; std::string m_contenttype; std::string m_date_modified; std::list< std::string > m_cookies; std::string m_location; size_t m_total_length; size_t m_current_length; public: Loadable(); ~Loadable(); // HTTPコードなどの完全クリア void clear_load_data(); // ロード中かどうか bool is_loading() const; Encoding get_encoding() const { return m_encoding; } void set_encoding( const Encoding encoding ){ m_encoding = encoding; } int get_code() const { return m_code; } void set_code( int code ) { m_code = code; } const std::string& get_str_code() const { return m_str_code; } void set_str_code( const std::string& str_code ){ m_str_code = str_code; } const std::string& get_contenttype() const { return m_contenttype; } const std::list< std::string >& cookies() { return m_cookies; } const std::string& location() const { return m_location; } size_t total_length() const { return m_total_length; } void set_total_length( int length ){ m_total_length = length; } size_t current_length() const { return m_current_length; } void set_current_length( int length ){ m_current_length = length; } // 更新時刻関係 time_t get_time_modified(); const std::string& get_date_modified() const { return m_date_modified; } void set_date_modified( const std::string& date ){ m_date_modified = date; } // ローダーからコールバックされてコードなどを取得してから // receive_data() を呼び出す void receive( const char* data, size_t size ); // ディスパッチャ経由で Dispatchable::callback_dispatch()、 receive_finish()を呼ぶ // ロード直後に直接 receive_finish() は呼ばないこと void finish(); // ロード開始 // パラメータは Loader::run()の説明をみること bool start_load( const JDLIB::LOADERDATA& data ); // ロード停止 virtual void stop_load(); // ロード強制停止 // loadableを delete する前に terminate_load() を呼び出さないと // スレッド実行中にメモリなどが初期化されてしまうため落ちる時がある // デストラクタ内から terminate_load() しても落ちる時があるので // デストラクタの外から呼び出すこと void terminate_load(); // ローダがスレッド起動待ち状態になった時に、起動順のプライオリティを下げる void set_priority_low(){ m_low_priority = true; } private: virtual void receive_data( std::string_view ) {} virtual void receive_finish(){}; void callback_dispatch() override; int get_loader_code() const; std::string get_loader_str_code() const; std::string get_loader_contenttype() const; std::string get_loader_modified() const; std::list< std::string > get_loader_cookies() const; std::string get_loader_location() const; size_t get_loader_length() const; Encoding get_loader_content_charset() const; }; } #endif jdim-0.10.1/src/skeleton/lockable.h000066400000000000000000000013331445721505100170600ustar00rootroot00000000000000// ライセンス: GPL2 // // ロック可能クラス ( RefPtr と組み合わせて使う ) // // Lockable を継承したクラスを JDLIB::RefPtr_Lock 経由で呼ぶことによってロックを掛ける // ロックが外れたら unlook_impl が呼ばれる // #ifndef _LOCKABLE_H #define _LOCKABLE_H namespace SKELETON { class Lockable { int m_lock{}; public: Lockable() = default; virtual ~Lockable() noexcept = default; int get_lock() const { return m_lock; } void lock(){ ++m_lock; } void unlock(){ --m_lock; if( m_lock == 0 ) unlock_impl(); } private: virtual void unlock_impl(){} }; } #endif jdim-0.10.1/src/skeleton/login.cpp000066400000000000000000000035301445721505100167500ustar00rootroot00000000000000// ライセンス: GPL2 //#define _DEBUG #include "jddebug.h" #include "login.h" #include "cache.h" #include "jdlib/confloader.h" #include #include #include // chmod #include using namespace SKELETON; Login::Login( const std::string& url ) : m_url( url ) { #ifdef _DEBUG std::cout << "Login::Login " << get_url() << std::endl; #endif read_info(); } Login::~Login() { #ifdef _DEBUG std::cout << "Login::~Login " << get_url() << std::endl; #endif if( m_save_info ) save_info(); } void Login::set_username( const std::string& username ) { if( username != m_username ){ m_username = username; m_save_info = true; } } void Login::set_passwd( const std::string& passwd ) { if( passwd != m_passwd ){ m_passwd = passwd; m_save_info = true; } } // // パスワードやユーザー名読み込み // void Login::read_info() { std::string path = CACHE::path_passwd( get_url().substr( strlen( "jdlogin://" ) ) ); #ifdef _DEBUG std::cout << "Login::read_info path = " << path << std::endl; #endif JDLIB::ConfLoader cf( path, std::string() ); m_username = cf.get_option_str( "username", "" ); m_passwd = cf.get_option_str( "passwd", "" ); #ifdef _DEBUG std::cout << "user = " << m_username << " ,passwd = " << m_passwd << std::endl; #endif } // // パスワードやユーザー名書き込み // void Login::save_info() { std::string path = CACHE::path_passwd( get_url().substr( strlen( "jdlogin://" ) ) ); #ifdef _DEBUG std::cout << "Login::save_info path = " << path << std::endl; #endif std::ostringstream oss; oss << "username = " << m_username<< std::endl << "passwd = " << m_passwd << std::endl; CACHE::save_rawdata( path, oss.str() ); chmod( path.c_str(), S_IWUSR | S_IRUSR ); } jdim-0.10.1/src/skeleton/login.h000066400000000000000000000030171445721505100164150ustar00rootroot00000000000000// ライセンス: GPL2 // // 2chなどへのログイン管理クラス // // セッション管理やログイン、パスワードの保存などを行う // #ifndef _LOGIN_H #define _LOGIN_H #include "loadable.h" namespace SKELETON { class Login : public SKELETON::Loadable { std::string m_url; bool m_login_now{}; bool m_save_info{}; std::string m_username; std::string m_passwd; std::string m_sessionid; // セッションID std::string m_sessiondata; // セッションデータ public: explicit Login( const std::string& url ); ~Login(); bool login_now() const { return m_login_now; } void set_login_now( bool login_now ){ m_login_now = login_now; } const std::string& get_url() const { return m_url; } const std::string& get_username() const { return m_username; } void set_username( const std::string& username ); const std::string& get_passwd() const { return m_passwd; } void set_passwd( const std::string& passwd ); const std::string& get_sessionid() const { return m_sessionid; } void set_sessionid( const std::string& id ){ m_sessionid = id; } const std::string& get_sessiondata() const { return m_sessiondata; } void set_sessiondata( const std::string& data ){ m_sessiondata = data; } virtual void start_login()=0; virtual void logout()=0; private: void read_info(); void save_info(); }; } #endif jdim-0.10.1/src/skeleton/menubutton.cpp000066400000000000000000000140021445721505100200340ustar00rootroot00000000000000// ライセンス: GPL2 //#define _DEBUG #include "jddebug.h" #include "gtkmmversion.h" #include "menubutton.h" #include "jdlib/miscutil.h" #include "icons/iconmanager.h" #include "command.h" using namespace SKELETON; enum { MENU_MAX_LNG = 50 // メニューに表示する文字数(半角) }; MenuButton::MenuButton( const bool show_arrow, Gtk::Widget* label, Gtk::PackOptions options ) : m_label{ label } , m_enable_sig_clicked{ true } { Gtk::Box* hbox = Gtk::manage( new Gtk::Box( Gtk::ORIENTATION_HORIZONTAL ) ); hbox->set_spacing( 4 ); if( m_label ) hbox->pack_start( *m_label, options ); if( show_arrow ){ m_arrow = Gtk::manage( new Gtk::Image() ); m_arrow->set_from_icon_name( "pan-down-symbolic", Gtk::ICON_SIZE_SMALL_TOOLBAR ); hbox->pack_start( *m_arrow, Gtk::PACK_SHRINK ); } else m_enable_sig_clicked = false; add_events( Gdk::ENTER_NOTIFY_MASK ); add_events( Gdk::POINTER_MOTION_MASK ); add_events( Gdk::LEAVE_NOTIFY_MASK ); add_events( Gdk::SCROLL_MASK ); signal_enter_notify_event().connect( sigc::mem_fun( *this, &MenuButton::slot_enter ) ); signal_leave_notify_event().connect( sigc::mem_fun( *this, &MenuButton::slot_leave ) ); signal_motion_notify_event().connect( sigc::mem_fun( *this, &MenuButton::slot_motion ) ); add( *hbox ); show_all_children(); // メニュー項目作成 Glib::RefPtr< Gtk::ActionGroup > actiongroup = Gtk::ActionGroup::create(); Glib::RefPtr< Gtk::AccelGroup > agroup = CORE::get_mainwindow()->get_accel_group(); for( size_t i = 0 ; i < MAX_MENU_SIZE; ++i ){ Glib::RefPtr< Gtk::Action > action = Gtk::Action::create( "menu" + std::to_string( i ), "dummy" ); action->set_accel_group( agroup ); Gtk::MenuItem* item = Gtk::manage( action->create_menu_item() ); actiongroup->add( action, sigc::bind( sigc::mem_fun( *this, &MenuButton::slot_menu_selected ), i ) ); m_menuitems.push_back( item ); } set_focus_on_click( false ); } MenuButton::MenuButton( const bool show_arrow, Gtk::Widget& label ) : MenuButton( show_arrow, &label ) { } MenuButton::MenuButton( const bool show_arrow, const int id ) : MenuButton( show_arrow, Gtk::manage( new Gtk::Image( ICON::get_icon( id ) ) ), Gtk::PACK_SHRINK ) { } void MenuButton::set_tooltip_arrow( const std::string& tooltip ) { if( m_arrow ) { m_arrow->set_tooltip_text( tooltip ); } } // // メニュー項目追加 // void MenuButton::append_menu( std::vector< std::string >& items ) { // 古いメニューからメニュー項目を取り除いてdelete if( m_popupmenu ){ const int menusize = m_popupmenu->get_children().size(); for( int i = 0; i < menusize; ++i ) m_popupmenu->remove( *m_menuitems[ i ] ); m_popupmenu.reset(); } if( ! items.size() ) return; // 新しくメニューを作成して項目追加 m_popupmenu = std::make_unique(); const size_t size = MIN( items.size(), MAX_MENU_SIZE ); for( size_t i = 0 ; i < size; ++i ){ Gtk::MenuItem* item = nullptr; if( items[ i ] == "separator" ){ item = Gtk::manage( new Gtk::SeparatorMenuItem() ); } else{ item = m_menuitems[ i ]; if( auto label = dynamic_cast( item->get_child() ) ) { label->set_text( MISC::cut_str( items[i], MENU_MAX_LNG ) ); } } if( item ) m_popupmenu->append( *item ); } m_popupmenu->show_all(); } // // ポップアップメニュー表示 // // virtual void MenuButton::show_popupmenu() { if( ! m_popupmenu ) return; #if GTK_CHECK_VERSION(3,24,6) // Specify the current event by nullptr. m_popupmenu->popup_at_widget( this, Gdk::GRAVITY_SOUTH_WEST, Gdk::GRAVITY_NORTH_WEST, nullptr ); #else // GTK 3.24.5 以下のバージョンではメニューのスクロールが出来なくなることがあるため廃止予定APIを使う m_popupmenu->popup( Gtk::Menu::SlotPositionCalc( sigc::mem_fun( *this, &MenuButton::slot_popup_pos ) ), 0, gtk_get_current_event_time() ); #endif } // // メニューが選ばれた // void MenuButton::slot_menu_selected( int i ) { #ifdef _DEBUG std::cout << "MenuButton::slot_menu_selected i = " << i << std::endl; #endif m_sig_selected.emit( i ); } // // ボタンがクリックされたらクリックした位置で処理を変える // void MenuButton::on_clicked() { #ifdef _DEBUG std::cout << "MenuButton::on_clicked\n"; #endif if( ! m_enable_sig_clicked || m_on_arrow ) show_popupmenu(); else m_sig_clicked.emit(); } #if ! GTK_CHECK_VERSION(3,24,6) /** * @brief ポップアップメニューの位置決め */ void MenuButton::slot_popup_pos( int& x, int& y, bool& push_in ) { int ox, oy; get_window()->get_origin( ox, oy ); Gdk::Rectangle rect = get_allocation(); x = ox + rect.get_x(); y = oy + rect.get_y() + rect.get_height(); push_in = false; } #endif bool MenuButton::slot_enter( GdkEventCrossing* event ) { #ifdef _DEBUG std::cout << "MenuButton::slot_enter\n"; #endif check_on_arrow( (int)event->x ); return true; } bool MenuButton::slot_leave( GdkEventCrossing* event ) { #ifdef _DEBUG std::cout << "MenuButton::slot_leave\n"; #endif m_on_arrow = false; return true; } bool MenuButton::slot_motion( GdkEventMotion* event ) { check_on_arrow( (int)event->x ); return true; } // // ポインタが矢印の下にあるかチェック // void MenuButton::check_on_arrow( int ex ) { if( ! m_arrow ){ m_on_arrow = false; return; } if( ! m_label ){ m_on_arrow = true; return; } Gdk::Rectangle rect = m_arrow->get_allocation(); int x = rect.get_x(); rect = get_allocation(); x -= rect.get_x(); if( ex >= x ) m_on_arrow = true; else m_on_arrow = false; #ifdef _DEBUG std::cout << "MenuButton::check_on_arrow x = " << ex << " / " << x << " on = " << m_on_arrow << std::endl; #endif } jdim-0.10.1/src/skeleton/menubutton.h000066400000000000000000000043121445721505100175040ustar00rootroot00000000000000// ライセンス: GPL2 // メニュー付きボタン #ifndef _MENUBUTTON_H #define _MENUBUTTON_H #include #include #include #include constexpr size_t MAX_MENU_SIZE = 20; namespace SKELETON { class MenuButton : public Gtk::Button { typedef sigc::signal< void > SIG_BUTTON_CLICKED; typedef sigc::signal< void, const int > SIG_SELECTED; SIG_BUTTON_CLICKED m_sig_clicked; SIG_SELECTED m_sig_selected; std::unique_ptr m_popupmenu; std::vector< Gtk::MenuItem* > m_menuitems; Gtk::Widget* m_label{}; Gtk::Image* m_arrow{}; bool m_on_arrow{}; bool m_enable_sig_clicked; MenuButton( const bool show_arrow, Gtk::Widget* label, Gtk::PackOptions options = Gtk::PACK_EXPAND_WIDGET ); public: MenuButton( const bool show_arrow, Gtk::Widget& label ); MenuButton( const bool show_arrow , const int id ); ~MenuButton() noexcept = default; Gtk::Widget* get_label_widget(){ return m_label; } void set_tooltip_arrow( const std::string& tooltip ); SIG_BUTTON_CLICKED signal_button_clicked(){ return m_sig_clicked; } // メニューが選択されたらemitされる SIG_SELECTED signal_selected(){ return m_sig_selected; } // メニュー項目追加 void append_menu( std::vector< std::string >& items ); // 矢印ボタン以外をクリックしたときにSIG_BUTTON_CLICKEDをemitする // false の時はボタンのどこを押してもメニューを表示する void set_enable_sig_clicked( const bool enable ){ m_enable_sig_clicked = enable; } void on_clicked() override; protected: // ポップアップメニュー表示 virtual void show_popupmenu(); private: void slot_menu_selected( int i ); #if ! GTK_CHECK_VERSION(3,24,6) /// ポップアップメニューの位置決め void slot_popup_pos( int& x, int& y, bool& push_in ); #endif bool slot_enter( GdkEventCrossing* event ); bool slot_leave( GdkEventCrossing* event ); bool slot_motion( GdkEventMotion* event ); void check_on_arrow( int ex ); }; } #endif jdim-0.10.1/src/skeleton/meson.build000066400000000000000000000016511445721505100173000ustar00rootroot00000000000000sources = [ 'aamenu.cpp', 'aboutdiag.cpp', 'admin.cpp', 'backforwardbutton.cpp', 'compentry.cpp', 'detaildiag.cpp', 'dispatchable.cpp', 'dragnote.cpp', 'dragtreeview.cpp', 'editcolumns.cpp', 'edittreeview.cpp', 'editview.cpp', 'entry.cpp', 'hpaned.cpp', 'jdtoolbar.cpp', 'label_entry.cpp', 'loadable.cpp', 'login.cpp', 'menubutton.cpp', 'msgdiag.cpp', 'notebook.cpp', 'panecontrol.cpp', 'popupwin.cpp', 'popupwinbase.cpp', 'prefdiag.cpp', 'selectitempref.cpp', 'tablabel.cpp', 'tabnote.cpp', 'tabswitchbutton.cpp', 'tabswitchmenu.cpp', 'textloader.cpp', 'toolbar.cpp', 'toolbarnote.cpp', 'toolmenubutton.cpp', 'treeviewbase.cpp', 'undobuffer.cpp', 'vbox.cpp', 'view.cpp', 'viewnote.cpp', 'vpaned.cpp', 'window.cpp', ] skeleton_lib = static_library( 'skeleton', sources, dependencies : gtkmm_dep, include_directories : include_directories('..'), ) jdim-0.10.1/src/skeleton/msgdiag.cpp000066400000000000000000000123371445721505100172600ustar00rootroot00000000000000// ライセンス: GPL2 //#define _DEBUG #include "jddebug.h" #include "msgdiag.h" #include "command.h" #include "session.h" #include "dispatchmanager.h" #include "global.h" #include using namespace SKELETON; MsgDiag::MsgDiag( Gtk::Window& parent, const Glib::ustring& message, bool use_markup, Gtk::MessageType type, Gtk::ButtonsType buttons, bool modal ) : Gtk::MessageDialog( parent, message, use_markup, type, buttons, modal ) {} // parent がポインタの時は nullptr かどうかで場合分け MsgDiag::MsgDiag( Gtk::Window* parent, const Glib::ustring& message, bool use_markup, Gtk::MessageType type, Gtk::ButtonsType buttons, bool modal ) : Gtk::MessageDialog( message, use_markup, type, buttons, modal ) { if( parent ) set_transient_for( *parent ); else set_transient_for( *CORE::get_mainwindow() ); // tab でラベルにフォーカスが移らないようにする const std::vector< Gtk::Widget* > area = get_message_area()->get_children(); Gtk::Label* const primary_label = dynamic_cast< Gtk::Label* >( area.front() ); if( primary_label ) { primary_label->set_can_focus( false ); } } MsgDiag::~MsgDiag() noexcept = default; void MsgDiag::add_default_button( const Glib::ustring& label, const int id ) { Gtk::Button* button = Gtk::manage( new Gtk::Button( label, true ) ); add_default_button( button, id ); } void MsgDiag::add_default_button( Gtk::Widget* button, const int id ) { if( ! button ) return; add_action_widget( *button, id ); button->show(); button->set_can_default( true ); button->grab_default(); button->grab_focus(); } int MsgDiag::run() { #ifdef _DEBUG std::cout << "MsgDiag::run start\n"; #endif SESSION::set_dialog_shown( true ); CORE::core_set_command( "dialog_shown" ); // タイマーセット sigc::slot< bool > slot_tmout = sigc::bind( sigc::mem_fun(*this, &MsgDiag::slot_timeout), 0 ); m_conn_timer = JDLIB::Timeout::connect( slot_tmout, TIMER_TIMEOUT ); int ret = Gtk::MessageDialog::run(); SESSION::set_dialog_shown( false ); CORE::core_set_command( "dialog_hidden" ); #ifdef _DEBUG std::cout << "MsgDiag::run fin\n"; #endif return ret; } void MsgDiag::show() { #ifdef _DEBUG std::cout << "MsgDiag::show\n"; #endif SESSION::set_dialog_shown( true ); CORE::core_set_command( "dialog_shown" ); Gtk::MessageDialog::show(); } void MsgDiag::hide() { #ifdef _DEBUG std::cout << "MsgDiag::hide\n"; #endif SESSION::set_dialog_shown( false ); CORE::core_set_command( "dialog_hidden" ); Gtk::MessageDialog::hide(); } // タイマーのslot関数 bool MsgDiag::slot_timeout( int timer_number ) { // メインループが停止していて dispatcher が働かないため // タイマーから強制的に実行させる CORE::get_dispmanager()->slot_dispatch(); return true; } ///////////////////////////////////// MsgCheckDiag::MsgCheckDiag( Gtk::Window* parent, const Glib::ustring& message, const Glib::ustring& message_check, Gtk::MessageType type, Gtk::ButtonsType buttons, const int default_response ) : SKELETON::MsgDiag( parent, message, false, type, Gtk::BUTTONS_NONE, false ) , m_chkbutton( message_check, true ) { const int mrg = 16; Gtk::HBox* hbox = Gtk::manage( new Gtk::HBox ); hbox->pack_start( m_chkbutton, Gtk::PACK_EXPAND_WIDGET, mrg ); get_content_area()->pack_start( *hbox, Gtk::PACK_SHRINK ); if( buttons == Gtk::BUTTONS_OK ){ add_default_button( g_dgettext( GTK_DOMAIN, "_OK" ), Gtk::RESPONSE_OK ); } else if( buttons == Gtk::BUTTONS_OK_CANCEL ){ add_button( g_dgettext( GTK_DOMAIN, "_No" ), Gtk::RESPONSE_CANCEL ); add_default_button( g_dgettext( GTK_DOMAIN, "_OK" ), Gtk::RESPONSE_OK ); } else if( buttons == Gtk::BUTTONS_YES_NO ){ if( default_response == Gtk::RESPONSE_NO ){ add_default_button( g_dgettext( GTK_DOMAIN, "_No" ), Gtk::RESPONSE_NO ); add_button( g_dgettext( GTK_DOMAIN, "_Yes" ), Gtk::RESPONSE_YES ); } else{ add_button( g_dgettext( GTK_DOMAIN, "_No" ), Gtk::RESPONSE_NO ); add_default_button( g_dgettext( GTK_DOMAIN, "_Yes" ), Gtk::RESPONSE_YES ); } } show_all_children(); } MsgCheckDiag::~MsgCheckDiag() noexcept = default; ///////////////////////////////////// MsgOverwriteDiag::MsgOverwriteDiag( Gtk::Window* parent ) : SKELETON::MsgDiag( parent, "ファイルが存在します。ファイル名を変更して保存しますか?", false, Gtk::MESSAGE_QUESTION, Gtk::BUTTONS_NONE ) { add_button( g_dgettext( GTK_DOMAIN, "_No" ), Gtk::RESPONSE_NO ); add_button( g_dgettext( GTK_DOMAIN, "_Yes" ), Gtk::RESPONSE_YES ); add_button( "上書き", OVERWRITE_YES ); add_button( "すべていいえ", OVERWRITE_NO_ALL ); add_button( "すべて上書き", OVERWRITE_YES_ALL ); } MsgOverwriteDiag::~MsgOverwriteDiag() noexcept = default; jdim-0.10.1/src/skeleton/msgdiag.h000066400000000000000000000045651445721505100167310ustar00rootroot00000000000000// ライセンス: GPL2 // メッセージダイアログの基底クラス #ifndef _MSGDIAG_H #define _MSGDIAG_H #include #include "jdlib/timeout.h" namespace SKELETON { class MsgDiag : public Gtk::MessageDialog { std::unique_ptr m_conn_timer; public: MsgDiag( Gtk::Window& parent, const Glib::ustring& message, bool use_markup = false, Gtk::MessageType type = Gtk::MESSAGE_INFO, Gtk::ButtonsType buttons = Gtk::BUTTONS_OK, bool modal = false); // parent がポインタの時は nullptr かどうかで場合分け MsgDiag( Gtk::Window* parent, const Glib::ustring& message, bool use_markup = false, Gtk::MessageType type = Gtk::MESSAGE_INFO, Gtk::ButtonsType buttons = Gtk::BUTTONS_OK, bool modal = false); ~MsgDiag() noexcept; void add_default_button( const Glib::ustring& label, const int id ); void add_default_button( Gtk::Widget* button, const int id ); virtual int run(); void show(); void hide(); private: // タイマーのslot関数 bool slot_timeout( int timer_number ); }; ///////////////////////////////////// // チェックボタン付き class MsgCheckDiag : public SKELETON::MsgDiag { Gtk::CheckButton m_chkbutton; public: MsgCheckDiag( Gtk::Window* parent, const Glib::ustring& message, const Glib::ustring& message_check, Gtk::MessageType type = Gtk::MESSAGE_INFO, Gtk::ButtonsType buttons = Gtk::BUTTONS_OK, const int default_response = -1 ); ~MsgCheckDiag() noexcept; Gtk::CheckButton& get_chkbutton(){ return m_chkbutton; } }; ///////////////////////////////////// // 上書きチェックダイアログ enum { OVERWRITE_YES = Gtk::RESPONSE_YES + 100, OVERWRITE_YES_ALL = Gtk::RESPONSE_YES + 200, OVERWRITE_NO_ALL = Gtk::RESPONSE_NO + 200 }; class MsgOverwriteDiag : public SKELETON::MsgDiag { public: explicit MsgOverwriteDiag( Gtk::Window* parent ); ~MsgOverwriteDiag() noexcept; }; } #endif jdim-0.10.1/src/skeleton/notebook.cpp000066400000000000000000000006761445721505100174700ustar00rootroot00000000000000// ライセンス: GPL2 //#define _DEBUG #include "jddebug.h" #include "notebook.h" using namespace SKELETON; JDNotebook::~JDNotebook() noexcept = default; // unpack = true の時取り除く int JDNotebook::append_remove_page( bool unpack, Widget& child, const Glib::ustring& tab_label, bool use_mnemonic ) { if( unpack ){ remove( child ); return 0; } return append_page( child, tab_label, use_mnemonic ); } jdim-0.10.1/src/skeleton/notebook.h000066400000000000000000000007071445721505100171300ustar00rootroot00000000000000// ライセンス: GPL2 // // Notebook クラス // #ifndef _NOTEBOOK_H #define _NOTEBOOK_H #include namespace SKELETON { class JDNotebook : public Gtk::Notebook { public: using Gtk::Notebook::Notebook; ~JDNotebook() noexcept; // unpack = true の時取り除く int append_remove_page( bool unpack, Widget& child, const Glib::ustring& tab_label, bool use_mnemonic = false ); }; } #endif jdim-0.10.1/src/skeleton/panecontrol.cpp000066400000000000000000000141631445721505100201700ustar00rootroot00000000000000// ライセンス: GPL2 //#define _DEBUG #include "jddebug.h" #include "panecontrol.h" using namespace SKELETON; enum { PANE_PAGE_MINSIZE = 32 }; PaneControl::PaneControl( Gtk::Paned& paned, int fixmode ) : m_paned( paned ) , m_click_fold( PANE_CLICK_NORMAL ) , m_fixmode( fixmode ) , m_mode( PANE_NORMAL ) , m_pre_size{ -1 } {} PaneControl::~PaneControl() noexcept = default; // // クロック入力 // void PaneControl::clock_in() { // Gtk::Paned は configure_event()をキャッチ出来ないので // 応急処置としてタイマーの中でサイズが変更したか調べて // 変わっていたら仕切りの位置を調整する if( m_pre_size != get_size() ){ #ifdef _DEBUG int pos = m_paned.get_position(); std::cout << "PaneControl::clock_in resize pos = " << pos << " presize = " << m_pre_size << " size = " << get_size() << std::endl; #endif update_position(); m_pre_size = get_size(); } } // // モードにしたがってセパレータの位置を更新 // void PaneControl::update_position() { #ifdef _DEBUG std::cout << "PaneControl::update_position fixmode = " << m_fixmode << " mode = " << m_mode << " pos = " << m_pos << " size = " << get_size() << std::endl; #endif int pos = m_paned.get_position(); // PAGE1を最大化 if( m_mode == PANE_MAX_PAGE1 ) m_paned.set_position( get_size() ); // PAGE2を最大化 else if( m_mode == PANE_MAX_PAGE2 && pos > 0 ) m_paned.set_position( 0 ); // 通常状態 else if( m_mode == PANE_NORMAL ){ // ウィンドウをリサイズしたときの処理 // m_fixmode の値によりPAGE1とPAGE2のどちらのサイズを固定するか判断する int newpos = 0; if( m_fixmode == PANE_FIXSIZE_PAGE1 && pos != m_pos ){ newpos = MIN( m_pos, get_size() - PANE_PAGE_MINSIZE ); } else if( m_fixmode == PANE_FIXSIZE_PAGE2 && pos != get_size() - m_pos ){ newpos = MAX( get_size() - m_pos, PANE_PAGE_MINSIZE ); } if( newpos ) m_paned.set_position( newpos ); #ifdef _DEBUG std::cout << "newpos = " << newpos << " pos = " << m_paned.get_position() << std::endl; #endif } } int PaneControl::get_position() { if( ! m_pos ){ if( m_fixmode == PANE_FIXSIZE_PAGE1 ) m_pos = m_paned.get_position(); else if( m_fixmode == PANE_FIXSIZE_PAGE2 ) m_pos = get_size() - m_paned.get_position(); } #ifdef _DEBUG std::cout << "PaneControl::get_position " << m_pos << std::endl; #endif return m_pos; } void PaneControl::set_position( int position ) { if( position == m_pos ) return; #ifdef _DEBUG std::cout << "PaneControl::set_position position = " << position << " size = " << get_size() << std::endl; #endif m_pos = position; update_position(); } // // ページ最大化切り替え // void PaneControl::set_mode( int mode ) { if( mode == m_mode ) return; #ifdef _DEBUG int pos = m_paned.get_position(); int size = get_size(); std::cout << "PaneControl::set_mode = " << mode << " size = " << size << " current_pos = " << pos << " pos = " << get_position() << std::endl; #endif m_mode = mode; update_position(); m_sig_pane_modechanged.emit( m_mode ); } // unpack = true の時取り除く void PaneControl::add_remove1( bool unpack, Gtk::Widget& child ) { if( unpack ) m_paned.remove( child ); // paned.ccg Revision 2 , Tue Jan 21 13:41:59 2003 UTC をハック // pack1 を SHRINK、pack2 を EXPAND にしないとリサイズしたときに // 仕切りがバタつく else m_paned.pack1( child, Gtk::SHRINK ); } // unpack = true の時取り除く void PaneControl::add_remove2( bool unpack, Gtk::Widget& child ) { if( unpack ) m_paned.remove( child ); else m_paned.pack2( child, Gtk::EXPAND ); } void PaneControl::button_press_event( GdkEventButton* event ) { m_clicked = is_separater_clicked( event ); // 仕切りをクリックしたかチェック m_drag = false; #ifdef _DEBUG std::cout << "PaneControl::button_press_event" << " x = " << event->x << " y = " << event->y << " pos = " << m_pos << " current_pos = " << m_paned.get_position() << " click = " << m_clicked << std::endl; #endif } void PaneControl::button_release_event( GdkEventButton* event ) { #ifdef _DEBUG std::cout << "PaneControl::butoon_release_event clicked = " << m_clicked << " drag = " << m_drag << std::endl; #endif // 仕切りをクリックしたら折りたたむ if( m_click_fold != PANE_CLICK_NORMAL && m_clicked && ! m_drag ){ if( m_click_fold == PANE_CLICK_FOLD_PAGE1 ){ if( m_mode == PANE_NORMAL ) set_mode( PANE_MAX_PAGE2 ); else set_mode( PANE_NORMAL ); } else if( m_click_fold == PANE_CLICK_FOLD_PAGE2 ){ if( m_mode == PANE_NORMAL ) set_mode( PANE_MAX_PAGE1 ); else set_mode( PANE_NORMAL ); } } // 仕切りをドラッグした場合 else if( m_clicked && m_drag ){ if( m_mode == PANE_NORMAL ){ if( m_fixmode == PANE_FIXSIZE_PAGE1 ) m_pos = m_paned.get_position(); else if( m_fixmode == PANE_FIXSIZE_PAGE2 ) m_pos = get_size() - m_paned.get_position(); #ifdef _DEBUG std::cout << "new pos = " << m_pos << std::endl; #endif set_mode( PANE_NORMAL ); } else if( m_mode == PANE_MAX_PAGE1 ) m_paned.set_position( get_size() ); else if( m_mode == PANE_MAX_PAGE2 ) m_paned.set_position( 0 ); } m_clicked = false; m_drag = false; } void PaneControl::motion_notify_event( GdkEventMotion* event ) { #ifdef _DEBUG // std::cout << "PaneControl::motion_notify_event\n"; #endif m_drag = true; } void PaneControl::enter_notify_event( GdkEventCrossing* event ) { #ifdef _DEBUG std::cout << "PaneControl::enter_notify_event\n"; #endif m_on_paned = true; } void PaneControl::leave_notify_event( GdkEventCrossing* event ) { #ifdef _DEBUG std::cout << "PaneControl::leave_notify_event\n"; #endif m_on_paned = false; } jdim-0.10.1/src/skeleton/panecontrol.h000066400000000000000000000076211445721505100176360ustar00rootroot00000000000000// ライセンス: GPL2 // // Paned widget の制御クラス // #ifndef _PANECONTROL_H #define _PANECONTROL_H #include namespace SKELETON { // コンストラクタで指定する m_fixmode の値 enum { PANE_FIXSIZE_PAGE1, // ウィンドウリサイズ時にPAGE1のサイズを固定する PANE_FIXSIZE_PAGE2 // ウィンドウリサイズ時にPAGE2のサイズを固定する }; // set_mode()で指定する m_mode の値 enum { PANE_NORMAL = 0, // どちらのページも折り畳んでいない通常状態 PANE_MAX_PAGE1, // PAGE1 最大化中 PANE_MAX_PAGE2 // PAGE2 最大化中 }; // set_click_fold()で指定する m_click_fold の値 enum { PANE_CLICK_NORMAL, // セパレータをクリックしても折り畳まない PANE_CLICK_FOLD_PAGE1, // セパレータをクリックするとPAGE1を折り畳む PANE_CLICK_FOLD_PAGE2 // セパレータをクリックするとPAGE2を折り畳む }; // ペーン表示が切り替えられた typedef sigc::signal< void, int > SIG_PANE_MODECHANGED; class PaneControl { SIG_PANE_MODECHANGED m_sig_pane_modechanged; Gtk::Paned& m_paned; int m_click_fold; bool m_clicked{}; bool m_drag{}; bool m_on_paned{}; int m_fixmode; int m_mode; int m_pos{}; int m_pre_size; public: PaneControl( Gtk::Paned& paned, int fixmode ); virtual ~PaneControl() noexcept; SIG_PANE_MODECHANGED& sig_pane_modechanged() { return m_sig_pane_modechanged; } void clock_in(); // セパレータの位置の取得やセット void update_position(); int get_position(); void set_position( int position ); // PANE_MAX_PAGE1 などを指定 void set_mode( int mode ); int get_mode() const { return m_mode; } // PANE_CLICK_FOLD_PAGE1 などを指定 void set_click_fold( int mode ){ m_click_fold = mode; } int get_click_fold() const { return m_click_fold; } void add_remove1( bool unpack, Gtk::Widget& child ); void add_remove2( bool unpack, Gtk::Widget& child ); void button_press_event( GdkEventButton* event ); void button_release_event( GdkEventButton* event ); void motion_notify_event( GdkEventMotion* event ); void enter_notify_event( GdkEventCrossing* event ); void leave_notify_event( GdkEventCrossing* event ); protected: bool is_on_paned() const { return m_on_paned; } const Gtk::Paned& get_paned() const { return m_paned; } virtual int get_size() const = 0; virtual bool is_separater_clicked( GdkEventButton* event ) const = 0; }; ///////////////////////////////////// class HPaneControl : public PaneControl { public: using PaneControl::PaneControl; ~HPaneControl() noexcept = default; protected: int get_size() const override { return get_paned().get_width(); } bool is_separater_clicked( GdkEventButton* event ) const override { if( is_on_paned() && event->type == GDK_BUTTON_PRESS && event->button == 1 && event->x >= 0 && event->x <= 8 ) return true; return false; } }; ///////////////////////////////////// class VPaneControl : public PaneControl { public: using PaneControl::PaneControl; ~VPaneControl() noexcept = default; protected: int get_size() const override { return get_paned().get_height(); } bool is_separater_clicked( GdkEventButton* event ) const override { if( is_on_paned() && event->type == GDK_BUTTON_PRESS && event->button == 1 && event->y >= 0 && event->y <= 8 ) return true; return false; } }; } #endif jdim-0.10.1/src/skeleton/popupwin.cpp000066400000000000000000000053121445721505100175210ustar00rootroot00000000000000// ライセンス: GPL2 //#define _DEBUG #include "jddebug.h" #include "popupwin.h" using namespace SKELETON; PopupWin::PopupWin( Gtk::Widget* parent, SKELETON::View* view, const int mrg_x, const int mrg_y ) : PopupWinBase( POPUPWIN_NOFRAME ) , m_parent{ parent } , m_view{ view } , m_mrg_x{ mrg_x } , m_mrg_y{ mrg_y } { #ifdef _DEBUG std::cout << "PopupWin::PopupWin\n"; #endif m_view->sig_resize_popup().connect( sigc::mem_fun( *this, &PopupWin::slot_resize_popup ) ); add( *m_view ); m_view->sig_hide_popup().connect( sigc::mem_fun( *this, &PopupWin::slot_hide_popup ) ); m_view->show_view(); Gtk::Widget* toplevel = m_parent->get_toplevel(); if( toplevel->get_is_toplevel() ) { set_transient_for( *dynamic_cast< Gtk::Window* >( toplevel ) ); } slot_resize_popup(); } // // ポップアップウィンドウの座標と幅と高さを計算して移動とリサイズ // void PopupWin::slot_resize_popup() { if( ! m_view ) return; // マウス座標 int x_mouse, y_mouse; Gdk::Display::get_default()->get_default_seat()->get_pointer()->get_position( x_mouse, y_mouse ); // クライアントのサイズを取得 const int width_client = m_view->width_client(); const int height_client = m_view->height_client(); const int width_desktop = m_parent->get_screen()->get_width(); const int height_desktop = m_parent->get_screen()->get_height(); // x 座標と幅 const int width_popup = width_client; int x_popup; if( x_mouse + m_mrg_x + width_popup <= width_desktop ) x_popup = x_mouse + m_mrg_x; else x_popup = MAX( 0, width_desktop - width_popup ); // y 座標と高さ int y_popup; int height_popup; if( y_mouse - ( height_client + m_mrg_y ) >= 0 ){ // 上にスペースがある y_popup = y_mouse - height_client - m_mrg_y; height_popup = height_client; } else if( y_mouse + m_mrg_y + height_client <= height_desktop ){ // 下にスペースがある y_popup = y_mouse + m_mrg_y; height_popup = height_client; } else if( m_view->get_popup_upside() || y_mouse > height_desktop/2 ){ // スペースは無いが上に表示 y_popup = MAX( 0, y_mouse - ( height_client + m_mrg_y ) ); height_popup = y_mouse - ( y_popup + m_mrg_y ); } else{ // 下 y_popup = y_mouse + m_mrg_y; height_popup = height_desktop - y_popup; } #ifdef _DEBUG std::cout << "PopupWin::slot_resize_popup : x = " << x_popup << " y = " << y_popup << " w = " << width_popup << " h = " << height_popup << std::endl; #endif move( x_popup, y_popup ); resize( width_popup, height_popup ); show_all(); } jdim-0.10.1/src/skeleton/popupwin.h000066400000000000000000000024271445721505100171720ustar00rootroot00000000000000// ライセンス: GPL2 // // ポップアップウィンドウ // // SKELETON::Viewをポップアップ表示する。 // 表示するSKELETON::Viewはデストラクタでdeleteするので呼出元でdeleteしなくても良い // #ifndef _POPUPWIN_H #define _POPUPWIN_H #include "popupwinbase.h" #include "view.h" #include namespace SKELETON { class PopupWin : public PopupWinBase { SIG_HIDE_POPUP m_sig_hide_popup; Gtk::Widget* m_parent; std::unique_ptr m_view; int m_mrg_x; // ポップアップとマウスカーソルの間のマージン(水平方向) int m_mrg_y; // ポップアップとマウスカーソルの間のマージン(垂直方向) public: PopupWin( Gtk::Widget* parent, SKELETON::View* view, const int mrg_x, const int mrg_y ); ~PopupWin() noexcept = default; // m_view からの hide シグナルをブリッジする SIG_HIDE_POPUP& sig_hide_popup() { return m_sig_hide_popup; } void slot_hide_popup(){ m_sig_hide_popup.emit(); } SKELETON::View* view(){ return m_view.get(); } // ポップアップウィンドウの座標と幅と高さを計算して移動とリサイズ void slot_resize_popup(); }; } #endif jdim-0.10.1/src/skeleton/popupwinbase.cpp000066400000000000000000000015401445721505100203530ustar00rootroot00000000000000// ライセンス: GPL2 //#define _DEBUG #include "jddebug.h" #include "popupwinbase.h" #include "command.h" using namespace SKELETON; PopupWinBase::PopupWinBase( bool draw_frame ) : Gtk::Window( Gtk::WINDOW_POPUP ) , m_draw_frame( draw_frame ) { if( m_draw_frame ) { set_border_width( 1 ); auto provider = Gtk::CssProvider::create(); provider->load_from_data( "window { border: 1px solid black; }" ); get_style_context()->add_provider( provider, GTK_STYLE_PROVIDER_PRIORITY_APPLICATION ); } if ( auto main_window = CORE::get_mainwindow() ) { set_transient_for( *main_window ); } } bool PopupWinBase::on_configure_event( GdkEventConfigure* event ) { const bool ret = Gtk::Window::on_configure_event( event ); m_sig_configured.emit( get_width(), get_height() ); return ret; } jdim-0.10.1/src/skeleton/popupwinbase.h000066400000000000000000000017361445721505100200270ustar00rootroot00000000000000// ライセンス: GPL2 // // ポップアップウィンドウの基底クラス // #ifndef POPUPWINBASE_H #define POPUPWINBASE_H #include namespace SKELETON { constexpr bool POPUPWIN_DRAWFRAME = true; constexpr bool POPUPWIN_NOFRAME = false; // // Gtk::Windows は signal_configure_event()を発行しないようなので // 自前でconfigureイベントをフックしてシグナルを発行する // using SIG_CONFIGURED_POPUP = sigc::signal< void, int, int >; class PopupWinBase : public Gtk::Window { SIG_CONFIGURED_POPUP m_sig_configured; const bool m_draw_frame; public: // draw_frame == true なら枠を描画する explicit PopupWinBase( bool draw_frame ); ~PopupWinBase() noexcept = default; SIG_CONFIGURED_POPUP sig_configured(){ return m_sig_configured; } protected: bool on_configure_event( GdkEventConfigure* event ) override; }; } #endif jdim-0.10.1/src/skeleton/prefdiag.cpp000066400000000000000000000061501445721505100174220ustar00rootroot00000000000000// ライセンス: GPL2 //#define _DEBUG #include "jddebug.h" #include "prefdiag.h" #include "label_entry.h" #include "command.h" #include "dispatchmanager.h" #include "environment.h" #include "global.h" #include "session.h" #include using namespace SKELETON; PrefDiag::PrefDiag( Gtk::Window* parent, const std::string& url, const bool add_cancel, const bool add_apply, const bool add_open ) : Gtk::Dialog( "", ENVIRONMENT::get_dialog_use_header_bar() ? Gtk::DIALOG_USE_HEADER_BAR : Gtk::DialogFlags{} ) , m_url( url ) , m_bt_apply( g_dgettext( GTK_DOMAIN, "_Apply" ), true ) { if( add_apply ){ m_bt_apply.signal_clicked().connect( sigc::mem_fun(*this, &PrefDiag::slot_apply_clicked ) ); if( property_use_header_bar() ) { get_header_bar()->pack_start( m_bt_apply ); } else { get_action_area()->pack_start( m_bt_apply ); } } if( add_cancel ){ add_button( g_dgettext( GTK_DOMAIN, "_Cancel" ), Gtk::RESPONSE_CANCEL ) ->signal_clicked().connect( sigc::mem_fun(*this, &PrefDiag::slot_cancel_clicked ) ); } if( add_open ) m_bt_ok = add_button( g_dpgettext( GTK_DOMAIN, "Stock label\x04_Open", 12 ), Gtk::RESPONSE_OK ); else m_bt_ok = add_button( g_dgettext( GTK_DOMAIN, "_OK" ), Gtk::RESPONSE_OK ); m_bt_ok->signal_clicked().connect( sigc::mem_fun(*this, &PrefDiag::slot_ok_clicked ) ); if( parent ) set_transient_for( *parent ); else set_transient_for( *CORE::get_mainwindow() ); } PrefDiag::~PrefDiag() = default; // // okボタンをフォーカス // void PrefDiag::grab_ok() { if( ! m_bt_ok ) return; m_bt_ok->set_can_default( true ); m_bt_ok->grab_default(); m_bt_ok->grab_focus(); } // // Entry、LabelEntryがactiveになったときにOKでダイアログを終了させる // void PrefDiag::set_activate_entry( Gtk::Entry& entry ) { entry.signal_activate().connect( sigc::mem_fun( *this, &PrefDiag::slot_activate_entry ) ); } void PrefDiag::set_activate_entry( LabelEntry& entry ) { entry.signal_activate().connect( sigc::mem_fun( *this, &PrefDiag::slot_activate_entry ) ); } void PrefDiag::slot_activate_entry() { #ifdef _DEBUG std::cout << "PrefDiag::slot_activate_entry\n"; #endif slot_ok_clicked(); response( Gtk::RESPONSE_OK ); } int PrefDiag::run(){ SESSION::set_dialog_shown( true ); CORE::core_set_command( "dialog_shown" ); // タイマーセット sigc::slot< bool > slot_tmout = sigc::bind( sigc::mem_fun(*this, &PrefDiag::slot_timeout), 0 ); m_conn_timer = JDLIB::Timeout::connect( slot_tmout, TIMER_TIMEOUT ); int ret = Gtk::Dialog::run(); SESSION::set_dialog_shown( false ); CORE::core_set_command( "dialog_hidden" ); return ret; } // タイマーのslot関数 bool PrefDiag::slot_timeout( int timer_number ) { // メインループが停止していて dispatcher が働かないため // タイマーから強制的に実行させる CORE::get_dispmanager()->slot_dispatch(); timeout(); return true; } jdim-0.10.1/src/skeleton/prefdiag.h000066400000000000000000000027121445721505100170670ustar00rootroot00000000000000// ライセンス: GPL2 // 設定ダイアログの基底クラス #ifndef _PREFDIAG_H #define _PREFDIAG_H #include #include "jdlib/timeout.h" namespace SKELETON { class LabelEntry; class PrefDiag : public Gtk::Dialog { std::string m_url; Gtk::Button* m_bt_ok{}; Gtk::Button m_bt_apply; std::unique_ptr m_conn_timer; public: // parent == nullptr のときはメインウィンドウをparentにする PrefDiag( Gtk::Window* parent, const std::string& url, const bool add_cancel = true, const bool add_apply = false, const bool add_open = false ); ~PrefDiag(); const std::string& get_url() const { return m_url; } // okボタンをフォーカス void grab_ok(); // Entry、LabelEntryがactiveになったときにOKでダイアログを終了させる void set_activate_entry( Gtk::Entry& entry ); void set_activate_entry( LabelEntry& entry ); virtual int run(); protected: virtual void slot_ok_clicked(){} virtual void slot_cancel_clicked(){} virtual void slot_apply_clicked(){} private: // タイマーのslot関数 bool slot_timeout( int timer_number ); // 各設定ダイアログ別のタイムアウト処理 ( slot_timeout()から呼び出される ) virtual void timeout(){} void slot_activate_entry(); }; } #endif jdim-0.10.1/src/skeleton/selectitempref.cpp000066400000000000000000000426501445721505100206610ustar00rootroot00000000000000// ライセンス: GPL2 //#define _DEBUG #include "jddebug.h" #include "selectitempref.h" #include "config/globalconf.h" #include "jdlib/miscutil.h" #include "global.h" #include "session.h" #include using namespace SKELETON; #define ROW_COLOR "WhiteSmoke" SelectItemPref::SelectItemPref( Gtk::Window* parent, const std::string& url ) : SKELETON::PrefDiag( parent, url, true, true ) , m_button_top( g_dpgettext( GTK_DOMAIN, "Stock label, navigation\x04_Top", 24 ), true ) , m_button_up( g_dpgettext( GTK_DOMAIN, "Stock label, navigation\x04_Up", 24 ), true ) , m_button_down( g_dpgettext( GTK_DOMAIN, "Stock label, navigation\x04_Down", 24 ), true ) , m_button_bottom( g_dpgettext( GTK_DOMAIN, "Stock label, navigation\x04_Bottom", 24 ), true ) , m_button_default( g_dpgettext( GTK_DOMAIN, "Stock label\x04_Revert", 12 ), true ) { m_list_default_data.clear(); m_button_top.set_image_from_icon_name( "go-top" ); m_button_up.set_image_from_icon_name( "go-up" ); m_button_down.set_image_from_icon_name( "go-down" ); m_button_bottom.set_image_from_icon_name( "go-bottom" ); m_button_delete.set_image_from_icon_name( "go-next" ); m_button_add.set_image_from_icon_name( "go-previous" ); pack_widgets(); } // // widgetのパック // void SelectItemPref::pack_widgets() { // 同一カラムにアイコンと項目名の両方を表示するので、手動で列を作成する // 表示項目リスト m_tree_shown.get_selection()->set_mode( Gtk::SELECTION_MULTIPLE ); m_tree_shown.signal_focus_in_event().connect( sigc::mem_fun( *this, &SelectItemPref::slot_focus_in_shown ) ); m_store_shown = Gtk::ListStore::create( m_columns_shown ); m_tree_shown.set_model( m_store_shown ); // "表示"の列を作成 Gtk::TreeViewColumn* view_column_shown = Gtk::manage( new Gtk::TreeViewColumn( "表示" ) ); m_tree_shown.append_column( *view_column_shown ); // アイコン Gtk::CellRendererPixbuf* render_pixbuf_shown = Gtk::manage( new Gtk::CellRendererPixbuf() ); view_column_shown->pack_start( *render_pixbuf_shown, false ); view_column_shown->add_attribute( *render_pixbuf_shown, "pixbuf", 0 ); // 項目名 Gtk::CellRendererText* render_text_shown = Gtk::manage( new Gtk::CellRendererText() ); view_column_shown->pack_start( *render_text_shown, true ); view_column_shown->add_attribute( *render_text_shown, "text", 1 ); // ボタン(縦移動) m_vbuttonbox_v.pack_start( m_button_top, Gtk::PACK_SHRINK ); m_vbuttonbox_v.pack_start( m_button_up, Gtk::PACK_SHRINK ); m_vbuttonbox_v.pack_start( m_button_down, Gtk::PACK_SHRINK ); m_vbuttonbox_v.pack_start( m_button_bottom, Gtk::PACK_SHRINK ); // ボタン(横移動) m_vbuttonbox_h.pack_start( m_button_delete, Gtk::PACK_SHRINK ); m_vbuttonbox_h.pack_start( m_button_add, Gtk::PACK_SHRINK ); // ボタン(アクション) m_vbuttonbox_action.pack_start( m_button_default, Gtk::PACK_SHRINK ); // ボタン(スロット関数) m_button_top.signal_clicked().connect( sigc::mem_fun( *this, &SelectItemPref::slot_top ) ); m_button_up.signal_clicked().connect( sigc::mem_fun( *this, &SelectItemPref::slot_up ) ); m_button_down.signal_clicked().connect( sigc::mem_fun( *this, &SelectItemPref::slot_down ) ); m_button_bottom.signal_clicked().connect( sigc::mem_fun( *this, &SelectItemPref::slot_bottom ) ); m_button_delete.signal_clicked().connect( sigc::mem_fun( *this, &SelectItemPref::slot_delete ) ); m_button_add.signal_clicked().connect( sigc::mem_fun( *this, &SelectItemPref::slot_add ) ); m_button_default.signal_clicked().connect( sigc::mem_fun( *this, &SelectItemPref::slot_default ) ); // 非表示項目リスト m_tree_hidden.get_selection()->set_mode( Gtk::SELECTION_MULTIPLE ); m_tree_hidden.signal_focus_in_event().connect( sigc::mem_fun( *this, &SelectItemPref::slot_focus_in_hidden ) ); m_store_hidden = Gtk::ListStore::create( m_columns_hidden ); m_tree_hidden.set_model( m_store_hidden ); // "非表示"の列を作成 Gtk::TreeViewColumn* view_column_hidden = Gtk::manage( new Gtk::TreeViewColumn( "非表示" ) ); m_tree_hidden.append_column( *view_column_hidden ); // アイコン Gtk::CellRendererPixbuf* render_pixbuf_hidden = Gtk::manage( new Gtk::CellRendererPixbuf() ); view_column_hidden->pack_start( *render_pixbuf_hidden, false ); view_column_hidden->add_attribute( *render_pixbuf_hidden, "pixbuf", 0 ); // 項目名 Gtk::CellRendererText* render_text_hidden = Gtk::manage( new Gtk::CellRendererText() ); view_column_hidden->pack_start( *render_text_hidden, true ); view_column_hidden->add_attribute( *render_text_hidden, "text", 1 ); // 全体のパッキング m_scroll_shown.add( m_tree_shown ); m_scroll_shown.set_size_request( 250, 300 ); m_scroll_shown.set_policy( Gtk::POLICY_NEVER, Gtk::POLICY_ALWAYS ); m_hbox.pack_start( m_scroll_shown, Gtk::PACK_EXPAND_WIDGET ); m_vbuttonbox_v.set_layout( Gtk::BUTTONBOX_START ); m_vbuttonbox_v.set_spacing( 4 ); m_vbox.pack_start( m_vbuttonbox_v, Gtk::PACK_EXPAND_WIDGET ); m_vbuttonbox_h.set_layout( Gtk::BUTTONBOX_EDGE ); m_vbuttonbox_h.set_spacing( 4 ); m_vbox.pack_start( m_vbuttonbox_h, Gtk::PACK_SHRINK ); m_vbuttonbox_action.set_layout( Gtk::BUTTONBOX_END ); m_vbuttonbox_action.set_spacing( 4 ); m_vbox.pack_start( m_vbuttonbox_action, Gtk::PACK_EXPAND_WIDGET ); m_hbox.pack_start( m_vbox, Gtk::PACK_SHRINK, 4 ); m_scroll_hidden.add( m_tree_hidden ); m_scroll_hidden.set_size_request( 250, 300 ); m_scroll_hidden.set_policy( Gtk::POLICY_NEVER, Gtk::POLICY_ALWAYS ); m_hbox.pack_start( m_scroll_hidden, Gtk::PACK_EXPAND_WIDGET ); get_content_area()->set_spacing( 8 ); get_content_area()->pack_start( m_hbox ); show_all_children(); } // // フォーカスが外れたTreeViewから項目の選択をなくす // bool SelectItemPref::slot_focus_in_shown( GdkEventFocus* event ) { m_tree_hidden.get_selection()->unselect_all(); return true; } bool SelectItemPref::slot_focus_in_hidden( GdkEventFocus* event ) { m_tree_shown.get_selection()->unselect_all(); return true; } // // 項目名でデフォルトデータからアイコンを取得 // Glib::RefPtr< Gdk::Pixbuf > SelectItemPref::get_icon( const Glib::ustring& name ) { Glib::RefPtr< Gdk::Pixbuf > icon; auto it = std::find_if( m_list_default_data.cbegin(), m_list_default_data.cend(), [&name]( const DEFAULT_DATA& data ) { return data.name == name; } ); if( it != m_list_default_data.cend() ) { icon = it->icon; } return icon; } // // 表示項目のクリア // void SelectItemPref::clear() { m_store_shown->clear(); m_store_hidden->clear(); } // // デフォルトデータを追加( 項目名、アイコンID ) // void SelectItemPref::append_default_pair( const Glib::ustring& name, const Glib::RefPtr< Gdk::Pixbuf > icon ) { if( name.empty() ) return; DEFAULT_DATA default_data; default_data.name = name; default_data.icon = icon; m_list_default_data.push_back( default_data ); } // // 文字列を元に行を作成 // void SelectItemPref::append_rows( const std::string& str ) { clear(); bool found_separator = false; // "非表示"にデフォルトデータを全て追加 for( const DEFAULT_DATA& data : m_list_default_data ) { // 2つ以上セパレータを追加しない if( data.name == ITEM_NAME_SEPARATOR ) { if( ! found_separator ) append_hidden( data.name, false ); found_separator = true; } else append_hidden( data.name, false ); } if( str.empty() ) return; // "表示"に追加と不要な"非表示"を削除 std::list< std::string > items = MISC::split_line( str ); for( const std::string& name : items ) { if( append_shown( name, false ) ) erase_hidden( name ); } } // // 全ての有効な項目を文字列で取得 // std::string SelectItemPref::get_items() const { std::string items; const Gtk::TreeModel::Children children = m_store_shown->children(); for( const Gtk::TreeRow& row : children ) { items.append( row[ m_columns_shown.m_column_text ] + " " ); } return items; } // // 表示項目に指定した項目を追加 // Gtk::TreeRow SelectItemPref::append_shown( const std::string& name, const bool set_cursor ) { Gtk::TreeModel::Row row; if( SESSION::parse_item( name ) == ITEM_END ) return row; row = *( m_store_shown->append() ); Glib::RefPtr< Gdk::Pixbuf > icon = get_icon( name ); if( icon ) row[ m_columns_shown.m_column_icon ] = icon; row[ m_columns_shown.m_column_text ] = name; if( set_cursor ){ m_tree_shown.get_selection()->select( row ); m_tree_shown.set_cursor( m_store_shown->get_path( row ) ); } return row; } // // 非表示項目に指定した項目を追加 // Gtk::TreeRow SelectItemPref::append_hidden( const std::string& name, const bool set_cursor ) { Gtk::TreeModel::Row row = *( m_store_hidden->append() ); Glib::RefPtr< Gdk::Pixbuf > icon = get_icon( name ); if( icon ) row[ m_columns_hidden.m_column_icon ] = icon; row[ m_columns_hidden.m_column_text ] = name; if( set_cursor ){ m_tree_hidden.get_selection()->select( row ); m_tree_hidden.set_cursor( m_store_hidden->get_path( row ) ); } return row; } // // 非表示項目から指定した項目を削除 // void SelectItemPref::erase_hidden( const std::string& name ) { if( name == ITEM_NAME_SEPARATOR ) return; const Gtk::TreeModel::Children children = m_store_hidden->children(); const auto& col = m_columns_hidden.m_column_text; auto it = std::find_if( children.begin(), children.end(), [&col, &name]( const Gtk::TreeRow& row ) { return row[ col ] == name; } ); if( it != children.end() ) { m_store_hidden->erase( *it ); } } // // KeyPressのフック // bool SelectItemPref::on_key_press_event( GdkEventKey* event ) { bool hook = false; // "Gtk::Dialog"は"Esc"を押すと閉じられてしまうので // "GDK_KEY_Escape"をキャンセルする switch( event->keyval ) { case GDK_KEY_Escape : hook = true; break; } m_sig_key_press.emit( event ); if( hook ) return true; return Gtk::Dialog::on_key_press_event( event ); } // // KeyReleaseのフック // bool SelectItemPref::on_key_release_event( GdkEventKey* event ) { bool hook = false; switch( event->keyval ) { case GDK_KEY_Escape : hook = true; m_tree_shown.get_selection()->unselect_all(); m_tree_hidden.get_selection()->unselect_all(); break; case GDK_KEY_Right : slot_delete(); break; case GDK_KEY_Left : slot_add(); break; } m_sig_key_release.emit( event ); if( hook ) return true; return Gtk::Dialog::on_key_release_event( event ); } // // 上端へ移動 // void SelectItemPref::slot_top() { Gtk::TreeModel::Children children = m_store_shown->children(); if( children.empty() ) return; // 移動先のイテレータ Gtk::TreeIter upper_it = children.begin(); std::vector< Gtk::TreePath > selection_path = m_tree_shown.get_selection()->get_selected_rows(); for( const Gtk::TreePath& path : selection_path ) { Gtk::TreeIter src_it = m_store_shown->get_iter( path ); Gtk::TreeIter dst_it = upper_it; // 元と先が同じでない if( src_it != dst_it ) { // 参照渡しなので書き換えられてしまう m_store_shown->move( src_it, dst_it ); upper_it = src_it; } // 移動先の位置を下げる if( upper_it != children.end() ) ++upper_it; } // フォーカスを移す set_focus( m_tree_shown ); } // // 上へ移動 // void SelectItemPref::slot_up() { Gtk::TreeModel::Children children = m_store_shown->children(); if( children.empty() ) return; // 上限のイテレータ Gtk::TreeIter upper_it = children.begin(); std::vector< Gtk::TreePath > selection_path = m_tree_shown.get_selection()->get_selected_rows(); for( const Gtk::TreePath& src : selection_path ) { Gtk::TreeIter src_it = m_store_shown->get_iter( src ); // 限度位置に達していなければ入れ替え if( src_it != upper_it ) { Gtk::TreePath dst( src ); if( dst.prev() ) { Gtk::TreeIter dst_it = m_store_shown->get_iter( dst ); m_store_shown->iter_swap( src_it, dst_it ); } } // 上限に達していたら次の限度位置を下げる else if( upper_it != children.end() ) ++upper_it; } // フォーカスを移す set_focus( m_tree_shown ); } // // 下へ移動 // void SelectItemPref::slot_down() { Gtk::TreeModel::Children children = m_store_shown->children(); if( children.empty() ) return; // 下限のイテレータ Gtk::TreeIter bottom_it = --children.end(); std::vector< Gtk::TreePath > selection_path = m_tree_shown.get_selection()->get_selected_rows(); for( auto it = selection_path.rbegin(); it != selection_path.rend(); ++it ) { Gtk::TreePath src = *it; Gtk::TreeIter src_it = m_store_shown->get_iter( src ); // 限度位置に達していなければ入れ替え if( src_it != bottom_it ) { Gtk::TreePath dst( src ); dst.next(); Gtk::TreeIter dst_it = m_store_shown->get_iter( dst ); if( dst_it ) { m_store_shown->iter_swap( src_it, dst_it ); } } // 下限に達していたら次の限度位置を上げる else if( bottom_it != children.begin() ) --bottom_it; } // フォーカスを移す set_focus( m_tree_shown ); } // // 下端へ移動 // void SelectItemPref::slot_bottom() { Gtk::TreeModel::Children children = m_store_shown->children(); if( children.empty() ) return; // 移動先のイテレータ Gtk::TreeIter bottom_it = children.end(); std::vector< Gtk::TreePath > selection_path = m_tree_shown.get_selection()->get_selected_rows(); for( auto it = selection_path.rbegin(); it != selection_path.rend(); ++it ) { Gtk::TreeIter src_it = m_store_shown->get_iter( *it ); Gtk::TreeIter dst_it = bottom_it; // 元と先が同じでない if( src_it != dst_it ) { // 参照渡しなので書き換えられてしまう m_store_shown->move( src_it, dst_it ); bottom_it = dst_it; } // 移動先の位置を上げる if( bottom_it != children.begin() ) --bottom_it; } // フォーカスを移す set_focus( m_tree_shown ); } // // 削除ボタン // void SelectItemPref::slot_delete() { std::vector< Gtk::TreePath > selection_path = m_tree_shown.get_selection()->get_selected_rows(); // 選択したアイテムが無い場合はフォーカスだけ移して出る if( selection_path.empty() ) { set_focus( m_tree_hidden ); return; } std::list< Gtk::TreeRow > erase_rows; bool set_cursor = true; // 一番上の選択項目にカーソルをセット for( const Gtk::TreePath& path : selection_path ) { Gtk::TreeRow row = *m_store_shown->get_iter( path ); if( row ) { Glib::ustring name = row[ m_columns_shown.m_column_text ]; // 非表示項目に追加 if( name != ITEM_NAME_SEPARATOR ) { append_hidden( name, set_cursor ); set_cursor = false; } // 表示項目から削除するリストに追加 erase_rows.push_back( row ); } } // 追加と削除を同時にすると滅茶苦茶になるので、分けて削除する for( const Gtk::TreeRow& row : erase_rows ) { m_store_shown->erase( row ); } // フォーカスを移す set_focus( m_tree_hidden ); } // // 追加ボタン // void SelectItemPref::slot_add() { std::vector< Gtk::TreePath > selection_path = m_tree_hidden.get_selection()->get_selected_rows(); // 選択したアイテムが無い場合はフォーカスだけ移して出る if( selection_path.empty() ) { set_focus( m_tree_shown ); return; } std::list< Gtk::TreeRow > erase_rows; bool set_cursor = true; // 一番上の選択項目にカーソルをセット // 選択したアイテムを追加 for( const Gtk::TreePath& path : selection_path ) { Gtk::TreeRow row = *m_store_hidden->get_iter( path ); if( row ) { Glib::ustring name = row[ m_columns_hidden.m_column_text ]; // 表示項目に追加 append_shown( name, set_cursor ); set_cursor = false; // 非表示項目から削除するリストに追加 if( name != ITEM_NAME_SEPARATOR ) erase_rows.push_back( row ); } } // 追加と削除を同時にすると滅茶苦茶になるので、分けて削除する for( const Gtk::TreeRow& row : erase_rows ) { m_store_hidden->erase( row ); } // フォーカスを移す set_focus( m_tree_shown ); } jdim-0.10.1/src/skeleton/selectitempref.h000066400000000000000000000103731445721505100203230ustar00rootroot00000000000000// ライセンス: GPL2 // 表示項目設定 #ifndef _SELECTITEMPREF_H #define _SELECTITEMPREF_H #include "prefdiag.h" #include #include namespace SKELETON { typedef struct { std::string name; Glib::RefPtr< Gdk::Pixbuf > icon; } DEFAULT_DATA; class SelectItemPref : public SKELETON::PrefDiag { // デフォルトの値を格納( 項目名, アイコンID, 有効/無効 ) std::list< DEFAULT_DATA > m_list_default_data; // ColumnRecord class TreeModelColumns : public Gtk::TreeModel::ColumnRecord { public: TreeModelColumns() { add( m_column_icon ); add( m_column_text ); } Gtk::TreeModelColumn< Glib::RefPtr< Gdk::Pixbuf > > m_column_icon; Gtk::TreeModelColumn< Glib::ustring > m_column_text; }; // 表示項目 Gtk::TreeView m_tree_shown; Glib::RefPtr< Gtk::ListStore > m_store_shown; TreeModelColumns m_columns_shown; Gtk::ScrolledWindow m_scroll_shown; // ボタン(縦移動) Gtk::Button m_button_top; Gtk::Button m_button_up; Gtk::Button m_button_down; Gtk::Button m_button_bottom; Gtk::VButtonBox m_vbuttonbox_v; // ボタン(横移動) Gtk::Button m_button_delete; Gtk::Button m_button_add; Gtk::VButtonBox m_vbuttonbox_h; // ボタン(アクション) Gtk::Button m_button_default; Gtk::VButtonBox m_vbuttonbox_action; // まとめ( m_vbuttonbox_* ) Gtk::VBox m_vbox; // 非表示項目 Gtk::TreeView m_tree_hidden; Glib::RefPtr< Gtk::ListStore > m_store_hidden; TreeModelColumns m_columns_hidden; Gtk::ScrolledWindow m_scroll_hidden; // まとめ( m_tree.shown, m_vbox, m_tree_hidden ) Gtk::HBox m_hbox; // キーフック用 typedef sigc::signal< bool, GdkEventKey* > SIG_KEY_PRESS; typedef sigc::signal< bool, GdkEventKey* > SIG_KEY_RELEASE; SIG_KEY_PRESS m_sig_key_press; SIG_KEY_RELEASE m_sig_key_release; public: SelectItemPref( Gtk::Window* parent, const std::string& url ); ~SelectItemPref() noexcept = default; private: // widgetのパック void pack_widgets(); // フォーカスが外れたTreeViewから項目の選択をなくす bool slot_focus_in_shown( GdkEventFocus* event ); bool slot_focus_in_hidden( GdkEventFocus* event ); // 項目名でデフォルトデータからアイコンを取得 Glib::RefPtr< Gdk::Pixbuf > get_icon( const Glib::ustring& name ); protected: // 表示項目のクリア void clear(); // デフォルトデータを追加 void append_default_pair( const Glib::ustring& name, const Glib::RefPtr< Gdk::Pixbuf > icon = Glib::RefPtr< Gdk::Pixbuf >() ); // 文字列を元に行を作成 void append_rows( const std::string& str ); // 全ての有効な項目を文字列で取得 std::string get_items() const; // 表示項目に指定した項目を追加 Gtk::TreeRow append_shown( const std::string& name, const bool set_cursor ); // 非表示項目に指定した項目を追加 Gtk::TreeRow append_hidden( const std::string& name, const bool set_cursor ); // 非表示項目から指定した項目を削除 void erase_hidden( const std::string& name ); // KeyPressのフック bool on_key_press_event( GdkEventKey* event ) override; // KeyReleaseのフック bool on_key_release_event( GdkEventKey* event ) override; // 最上位へ移動 void slot_top(); // 上へ移動 void slot_up(); // 下へ移動 void slot_down(); // 最下位へ移動 void slot_bottom(); // 削除ボタン void slot_delete(); // 追加ボタン void slot_add(); // デフォルトボタン virtual void slot_default() = 0; // 適用ボタン void slot_apply_clicked() override { slot_ok_clicked(); } }; } #endif jdim-0.10.1/src/skeleton/tablabel.cpp000066400000000000000000000114601445721505100174070ustar00rootroot00000000000000// ライセンス: GPL2 //#define _DEBUG #include "jddebug.h" #include "tablabel.h" #include "icons/iconmanager.h" #include "config/globalconf.h" #include "dndmanager.h" using namespace SKELETON; enum { SPACING_LABEL = 3 // アイコンとラベルの間のスペース }; TabLabel::TabLabel( const std::string& url ) : m_url( url ) , m_id_icon( ICON::NUM_ICONS ) { #ifdef _DEBUG std::cout << "TabLabel::TabLabel " << m_url << std::endl; #endif // 背景透過 set_visible_window( false ); add_events( Gdk::POINTER_MOTION_MASK ); add_events( Gdk::LEAVE_NOTIFY_MASK ); add_events( Gdk::SMOOTH_SCROLL_MASK ); // マウスホイールによるタブの切り替え add( m_hbox ); m_hbox.pack_start( m_label, Gtk::PACK_SHRINK ); show_all_children(); } // アイコンセット void TabLabel::set_id_icon( const int id ) { if( ! CONFIG::get_show_tab_icon() ) return; if( m_id_icon == id ) return; if( id == ICON::NONE ) return; if( !m_image ){ m_image = std::make_unique(); m_hbox.remove( m_label ); m_hbox.set_spacing( SPACING_LABEL ); m_hbox.pack_start( *m_image, Gtk::PACK_SHRINK ); m_hbox.pack_start( m_label, Gtk::PACK_SHRINK ); show_all_children(); } #ifdef _DEBUG std::cout << "TabLabel::set_icon " << m_fulltext << " id = " << id << std::endl; #endif m_id_icon = id; m_image->set( ICON::get_icon( id ) ); } // タブの文字列の文字数がlngになるようにリサイズする void TabLabel::resize_tab( const unsigned int lng ) { Glib::ustring ulabel( m_fulltext ); const unsigned int lng_org = ulabel.length(); ulabel.resize( lng ); if( lng < lng_org ) ulabel += "..."; m_label.set_text( ulabel ); } // // D&D設定 // void TabLabel::set_dragable( bool dragable, int button ) { if( dragable ){ std::vector< Gtk::TargetEntry > targets; targets.push_back( Gtk::TargetEntry( DNDTARGET_FAVORITE, Gtk::TARGET_SAME_APP, 0 ) ); targets.push_back( Gtk::TargetEntry( DNDTARGET_TAB, Gtk::TARGET_SAME_APP, 0 ) ); // ドラッグ開始側にする switch( button ){ case 1: drag_source_set( targets, Gdk::BUTTON1_MASK ); break; case 2: drag_source_set( targets, Gdk::BUTTON2_MASK ); break; case 3: drag_source_set( targets, Gdk::BUTTON3_MASK ); break; default: return; } } else{ drag_source_unset(); drag_dest_unset(); } } // // 本体の横幅 - ラベルの横幅 // int TabLabel::get_label_margin() const { int label_margin; int x_pad, y_pad; m_label.get_padding( x_pad, y_pad ); label_margin = x_pad*2 + m_hbox.get_spacing() + m_hbox.get_border_width()*2 + get_border_width()*2; if( CONFIG::get_show_tab_icon() && m_id_icon < ICON::NUM_ICONS ) label_margin += ICON::get_icon( m_id_icon )->get_width(); #ifdef _DEBUG std::cout << "image_w = " << ( m_image ? m_image->get_allocation().get_width() : 0 ) << " label_w = " << m_label.get_allocation().get_width() << " alloc_w = " << get_allocation().get_width() << " label_margine = " << label_margin << std::endl; #endif return label_margin; } // // マウスが動いた // bool TabLabel::on_motion_notify_event( GdkEventMotion* event ) { const bool ret = Gtk::EventBox::on_motion_notify_event( event ); m_sig_tab_motion_event.emit(); return ret; } // // マウスが出た // bool TabLabel::on_leave_notify_event( GdkEventCrossing* event ) { #ifdef _DEBUG std::cout << "TabLabel::leave\n"; #endif const bool ret = Gtk::EventBox::on_leave_notify_event( event ); m_sig_tab_leave_event.emit(); return ret; } // // ドラッグ開始 // void TabLabel::on_drag_begin( const Glib::RefPtr< Gdk::DragContext >& context ) { #ifdef _DEBUG std::cout << "TabLabel::on_drag_begin " << m_fulltext << std::endl; #endif m_sig_tab_drag_begin.emit(); Gtk::EventBox::on_drag_begin( context ); } // // D&Dで受信側がデータ送信を要求してきた // void TabLabel::on_drag_data_get( const Glib::RefPtr& context, Gtk::SelectionData& selection_data, guint info, guint time ) { #ifdef _DEBUG std::cout << "TabLabel::on_drag_data_get target = " << selection_data.get_target() << " " << m_fulltext << std::endl;; #endif Gtk::EventBox::on_drag_data_get( context, selection_data, info, time ); m_sig_tab_drag_data_get( selection_data ); } // // ドラッグ終了 // void TabLabel::on_drag_end( const Glib::RefPtr< Gdk::DragContext >& context ) { #ifdef _DEBUG std::cout << "TabLabel::on_drag_end " << m_fulltext << std::endl;; #endif Gtk::EventBox::on_drag_end( context ); m_sig_tab_drag_end.emit(); } jdim-0.10.1/src/skeleton/tablabel.h000066400000000000000000000065231445721505100170600ustar00rootroot00000000000000// ライセンス: GPL2 #ifndef _TABLABEL_H #define _TABLABEL_H #include #include #include namespace SKELETON { // マウス typedef sigc::signal< void > SIG_TAB_MOTION_EVENT; typedef sigc::signal< void > SIG_TAB_LEAVE_EVENT; // D&D typedef sigc::signal< void > SIG_TAB_DRAG_BEGIN; typedef sigc::signal< void, Gtk::SelectionData& > SIG_TAB_DRAG_DATA_GET; typedef sigc::signal< void > SIG_TAB_DRAG_END; class TabLabel : public Gtk::EventBox { SIG_TAB_MOTION_EVENT m_sig_tab_motion_event; SIG_TAB_LEAVE_EVENT m_sig_tab_leave_event; SIG_TAB_DRAG_BEGIN m_sig_tab_drag_begin; SIG_TAB_DRAG_DATA_GET m_sig_tab_drag_data_get; SIG_TAB_DRAG_END m_sig_tab_drag_end; int m_x{}; int m_y{}; int m_width{}; int m_height{}; std::string m_url; Gtk::HBox m_hbox; int m_id_icon; Gtk::Label m_label; // アイコン画像 std::unique_ptr m_image; // ラベルに表示する文字列の全体 std::string m_fulltext; public: explicit TabLabel( const std::string& url ); ~TabLabel() noexcept = default; SIG_TAB_MOTION_EVENT sig_tab_motion_event(){ return m_sig_tab_motion_event; } SIG_TAB_LEAVE_EVENT sig_tab_leave_event(){ return m_sig_tab_leave_event; } SIG_TAB_DRAG_BEGIN sig_tab_drag_begin() { return m_sig_tab_drag_begin; } SIG_TAB_DRAG_DATA_GET sig_tab_drag_data_get() { return m_sig_tab_drag_data_get; } SIG_TAB_DRAG_END sig_tab_drag_end() { return m_sig_tab_drag_end; } int get_tab_x() const { return m_x; } int get_tab_y() const { return m_y; } int get_tab_width() const { return m_width; } int get_tab_height() const { return m_height; } void set_tab_x( const int x ){ m_x = x; } void set_tab_y( const int y ){ m_y = y; } void set_tab_width( const int width ){ m_width = width; } void set_tab_height( const int height ){ m_height = height; } Pango::FontDescription get_label_font_description(){ return m_label.get_pango_context()->get_font_description(); } const std::string& get_url() const { return m_url; } void set_dragable( bool dragable, int button ); // 本体の横幅 - ラベルの横幅 int get_label_margin() const; // カットしていない全体の文字列 const std::string& get_fulltext() const { return m_fulltext; } void set_fulltext( const std::string& label ){ m_fulltext = label; } // アイコンセット void set_id_icon( const int id ); int get_id_icon() const { return m_id_icon; } // タブの文字列の文字数をlngにセット void resize_tab( const unsigned int lng ); private: bool on_motion_notify_event( GdkEventMotion* event ) override; bool on_leave_notify_event( GdkEventCrossing* event ) override; void on_drag_begin( const Glib::RefPtr< Gdk::DragContext >& context ) override; void on_drag_data_get( const Glib::RefPtr< Gdk::DragContext >& context, Gtk::SelectionData& selection_data, guint info, guint time ) override; void on_drag_end( const Glib::RefPtr< Gdk::DragContext>& context ) override; }; } #endif jdim-0.10.1/src/skeleton/tabnote.cpp000066400000000000000000000371671445721505100173110ustar00rootroot00000000000000// ライセンス: GPL2 //#define _DEBUG //#define _DEBUG_RESIZE_TAB #include "jddebug.h" #include "gtkmmversion.h" #include "dragnote.h" #include "tabnote.h" #include "tablabel.h" #include "config/globalconf.h" #include "jdlib/miscgtk.h" #include "session.h" #include "dndmanager.h" using namespace SKELETON; // ダミーWidgetを作成してtabにappend ( 表示はされない ) class DummyWidget : public Gtk::Widget { public: DummyWidget() : Gtk::Widget() { set_has_window( false ); } ~DummyWidget() noexcept = default; }; ////////////////////////////////////////////// TabNotebook::TabNotebook( DragableNoteBook* parent ) : Gtk::Notebook() , m_parent( parent ) , m_layout_tab{ create_pango_layout( "" ) } { set_border_width( 0 ); set_size_request( 1, -1 ); // これが無いと最大化を解除したときにウィンドウが勝手にリサイズする add_events( Gdk::POINTER_MOTION_MASK ); add_events( Gdk::LEAVE_NOTIFY_MASK ); add_events( Gdk::SCROLL_MASK ); // DnD設定 // ドロップ側に設定する drag_source_unset(); drag_dest_unset(); std::vector< Gtk::TargetEntry > targets; targets.push_back( Gtk::TargetEntry( DNDTARGET_TAB, Gtk::TARGET_SAME_APP, 0 ) ); drag_dest_set( targets, Gtk::DEST_DEFAULT_MOTION | Gtk::DEST_DEFAULT_DROP ); m_tab_mrg = get_margin_start(); if( m_tab_mrg <= 0 ) { m_tab_mrg = get_style_context()->get_margin().get_left(); } m_pre_width = get_width(); } TabNotebook::~TabNotebook() noexcept = default; // // クロック入力 // void TabNotebook::clock_in() { // Gtk::NoteBook は on_configure_event() と on_window_state_event() をキャッチ出来ない // かつ、 on_size_allocate() は最大化、最小化の時に無反応なので // 応急処置としてタイマーの中でサイズが変更したか調べて // 変わっていたらタブ幅を調整する if( ! m_fixtab && get_n_pages() && m_pre_width != get_width() && ! SESSION::is_booting() && ! SESSION::is_quitting() ){ calc_tabsize(); adjust_tabwidth(); } } // // サイズ変更 // void TabNotebook::on_size_allocate( Gtk::Allocation& allocation ) { clock_in(); // タブ再描画の反応を良くする Gtk::Notebook::on_size_allocate( allocation ); } int TabNotebook::append_tab( Widget& tab ) { #ifdef _DEBUG std::cout << "TabNotebook::append_tab\n"; #endif // ダミーWidgetを作成してtabにappend (表示はされない ) DummyWidget* dummypage = Gtk::manage( new DummyWidget ); return append_page( *dummypage , tab ); } int TabNotebook::insert_tab( Widget& tab, int page ) { #ifdef _DEBUG std::cout << "TabNotebook::insert_tab position = " << page << std::endl; #endif // ダミーWidgetを作成してtabにappend (表示はされない ) DummyWidget* dummypage = Gtk::manage( new DummyWidget ); return insert_page( *dummypage, tab, page ); } void TabNotebook::remove_tab( const int page, const bool adjust_tab ) { #ifdef _DEBUG std::cout << "TabNotebook::remove_tab page = " << page << std::endl; #endif // ダミーWidgetはGtk::manage()の効果でdeleteされる remove_page( page ); if( adjust_tab ) adjust_tabwidth(); } void TabNotebook::reorder_child( int page1, int page2 ) { Gtk::Notebook::reorder_child( *get_nth_page( page1 ), page2 ); } // タブ取得 SKELETON::TabLabel* TabNotebook::get_tablabel( int page ) { return dynamic_cast< SKELETON::TabLabel* >( get_tab_label( *get_nth_page( page ) ) ); } // // マウスの下にあるタブの番号を取得 // // タブ上では無いときは-1を返す // マウスがタブの右側にある場合はページ数( get_n_pages() )を返す // int TabNotebook::get_page_under_mouse() { int x, y; Gdk::Rectangle rect = get_allocation(); MISC::get_pointer_at_window( get_window(), x, y ); #ifdef _DEBUG std::cout << "TabNotebook::get_page_under_mouse x = " << x << " y = " << y << std::endl; #endif if( y < rect.get_y() || y > rect.get_y() + rect.get_height() ) return -1; calc_tabsize(); const int pages = get_n_pages(); int ret = pages; for( int i = 0; i < pages; ++i ){ SKELETON::TabLabel* tab = get_tablabel( i ); if( tab ){ int tab_x = tab->get_tab_x(); int tab_w = tab->get_tab_width(); if( tab_x < 0 ) continue; #ifdef _DEBUG std::cout << "TabNotebook::get_page_under_mouse page = " << i << " x = " << tab_x << " w = " << tab_w << std::endl; #endif if( x < tab_x ){ ret = -1; break; } else if( x <= tab_x + tab_w ){ ret = i; break; } } } #ifdef _DEBUG std::cout << "TabNotebook::get_page_under_mouse ret = " << ret << std::endl; #endif return ret; } // // タブの文字列取得 // const std::string& TabNotebook::get_tab_fulltext( int page ) { SKELETON::TabLabel* tablabel = get_tablabel( page ); if( ! tablabel ) return m_str_empty; return tablabel->get_fulltext(); } // // タブに文字列をセットとタブ幅調整 // void TabNotebook::set_tab_fulltext( const std::string& str, int page ) { #ifdef _DEBUG std::cout << "TabNotebook::set_tab_fulltext page = " << page << " " << str << std::endl; #endif SKELETON::TabLabel* tablabel = get_tablabel( page ); if( tablabel && str != tablabel->get_fulltext() ){ tablabel->set_fulltext( str ); tablabel->set_tooltip_text( str ); if( m_fixtab ) tablabel->resize_tab( str.length() ); else adjust_tabwidth(); } } // // 各タブのサイズと座標を取得 // void TabNotebook::calc_tabsize() { #ifdef _DEBUG std::cout << "TabNotebook::calc_tabsize\n"; #endif // gtk3は実装の詳細がバージョンによって異なるためタブの代わりにラベルの領域を取得する const int n_pages = get_n_pages(); // ラベルの領域とタブの領域のオフセットを概算する // GTKテーマが変更されるとオフセットが変わる可能性があるので毎回計算する // XXX: この修正はラベルの左右の余白の大きさが同じであることを前提とする int offset = 0; if( n_pages > 1 ) { const auto* tab1 = get_tablabel( 0 ); for( int i = 1; i < n_pages; ++i ) { const auto* const tab2 = get_tablabel( i ); if( tab1 && tab2 && tab1->get_mapped() && tab2->get_mapped() ) { const auto alloc1 = tab1->get_allocation(); const auto alloc2 = tab2->get_allocation(); offset = alloc2.get_x() - ( alloc1.get_x() + alloc1.get_width() ); #ifdef _DEBUG std::cout << "computed offset = " << offset << std::endl; #endif break; } tab1 = tab2; } } for( int i = 0; i < n_pages; ++i ) { auto* const tab_label = get_tablabel( i ); if( tab_label ) { int tab_x = -1; int tab_y = -1; int tab_w = -1; int tab_h = -1; if( tab_label->get_mapped() ) { Gdk::Rectangle rect = tab_label->get_allocation(); tab_x = rect.get_x() - ( offset / 2 ); tab_y = rect.get_y(); tab_w = rect.get_width() + offset; tab_h = rect.get_height(); m_tab_mrg = 0; } #ifdef _DEBUG std::cout << "page = " << i << " x = " << tab_x << " w = " << tab_w << " mrg = " << m_tab_mrg << std::endl; #endif tab_label->set_tab_x( tab_x ); tab_label->set_tab_y( tab_y ); tab_label->set_tab_width( tab_w ); tab_label->set_tab_height( tab_h ); } } } // // タブ幅調整 // bool TabNotebook::adjust_tabwidth() { // 起動中とシャットダウン中は処理しない if( SESSION::is_booting() ) return false; if( SESSION::is_quitting() ) return false; const int mrg_notebook = 30; const int pages = get_n_pages(); if( ! pages ) return false; // layoutにラベルのフォントをセットする SKELETON::TabLabel* tab = get_tablabel( 0 ); if( ! tab ) return false; m_layout_tab->set_font_description( tab->get_label_font_description() ); const int label_margin = tab->get_label_margin() + m_tab_mrg; m_pre_width = get_width(); const int width_notebook = m_pre_width - mrg_notebook; int avg_width_tab = (int)( (double)width_notebook / MAX( 3, pages ) ); // タブ幅の平均値 if( avg_width_tab < label_margin ){ avg_width_tab = label_margin; } #ifdef _DEBUG_RESIZE_TAB std::cout << "TabNotebook::adjust_tabwidth\n" << "width_notebook = " << width_notebook << " page = " << pages << std::endl << "avg_width_tab = " << avg_width_tab << " tab_mrg = " << m_tab_mrg << " label_margin " << label_margin << std::endl; #endif int label_width_org; // 変更前のタブの文字数 int label_width; // 変更後のタブの文字数 int width_total = 0; int tab_width, max_width; for( int i = 0; i < pages; ++i ){ tab = get_tablabel( i ); if( tab ){ Glib::ustring ulabel( tab->get_fulltext() ); Glib::ustring ulabel_org( ulabel ); bool cut_suffix = false; label_width_org = label_width = ulabel.length(); tab_width = -1; // タブ幅を未計算状態に初期化 max_width = avg_width_tab * ( i + 1 ) - width_total; // 最大値 ( 収まらないときマイナスになる ) // 長すぎるタブの文字列は表示しないよう、あらかじめ最大値を256に制限する if( label_width > 256 ){ label_width = 256; ulabel.resize( label_width ); } // タブの幅を最大値以下に縮める while( label_width > CONFIG::get_tab_min_str() ){ if( ! cut_suffix && label_width < label_width_org ){ cut_suffix = true; // 文字列を切り詰めたら "..." を付与する ulabel.append( "..." ); } m_layout_tab->set_text( ulabel ); int width = m_layout_tab->get_pixel_logical_extents().get_width() + label_margin; #ifdef _DEBUG_RESIZE_TAB std::cout << "s " << i << " " << width << " / " << max_width << " + " << width_total << " lng = " << label_width << " : " << ulabel << std::endl; #endif if( width < max_width ){ // 最大値以下 tab_width = width; // タブ幅を保存 break; } // 最大値を越えていたら、概算で収まるように縮める int n = label_width - (int)( (double)label_width * max_width / width ); if( n < 1 ) n = 1; if( label_width - n < CONFIG::get_tab_min_str() ){ n = label_width - CONFIG::get_tab_min_str(); } label_width -= n; ulabel.erase( label_width, n ); // 末尾のn文字を消す } // 横をはみださないようにタブ幅を延ばしていく for(;;){ if( label_width >= label_width_org ) break; // 全て収まった if( max_width < 0 ) break; // 確実に収まらない ulabel.insert( label_width, 1, ulabel_org[ label_width ] ); // 末尾の1文字を戻す ++label_width; m_layout_tab->set_text( ulabel ); int width = m_layout_tab->get_pixel_logical_extents().get_width() + label_margin; #ifdef _DEBUG_RESIZE_TAB std::cout << "w " << i << " " << width << " / " << max_width << " + " << width_total << " lng = " << label_width << " : " << ulabel << std::endl; #endif // 最大値を越えたらひとつ戻してbreak; if( width > max_width ){ --label_width; ulabel.erase( label_width, 1 ); // 末尾の1文字を消す break; } tab_width = width; // 最大値を超えていないタブ幅を保存 } // タブ幅を確定する if( tab_width < 0 ){ m_layout_tab->set_text( ulabel ); tab_width = m_layout_tab->get_pixel_logical_extents().get_width() + label_margin; #ifdef _DEBUG_RESIZE_TAB std::cout << "f " << i << " " << tab_width << " / " << max_width << " + " << width_total << " lng = " << label_width << " : " << ulabel << std::endl; #endif } width_total += tab_width; tab->resize_tab( label_width ); } } // 枠の再描画 m_parent->queue_draw(); return true; } // signal_button_press_event と signal_button_release_event は emit されない // ときがあるので自前でemitする bool TabNotebook::on_button_press_event( GdkEventButton* event ) { #ifdef _DEBUG std::cout << "TabNotebook::on_button_press_event\n"; #endif if( m_sig_button_press.emit( event ) ) return true; #ifdef _DEBUG std::cout << "Gtk::Notebook::on_button_press_event\n"; #endif return Gtk::Notebook::on_button_press_event( event ); } bool TabNotebook::on_button_release_event( GdkEventButton* event ) { #ifdef _DEBUG std::cout << "TabNotebook::on_button_release_event\n"; #endif if( m_sig_button_release.emit( event ) ) return true; #ifdef _DEBUG std::cout << "Gtk::Notebook::on_button_release_event\n"; #endif return Gtk::Notebook::on_button_release_event( event ); } // // マウスが動いた // bool TabNotebook::on_motion_notify_event( GdkEventMotion* event ) { #ifdef _DEBUG std::cout << "TabNotebook::on_motion_notify_event\n"; #endif m_sig_tab_motion_event.emit(); return Gtk::Notebook::on_motion_notify_event( event ); } // // マウスが出た // bool TabNotebook::on_leave_notify_event( GdkEventCrossing* event ) { #ifdef _DEBUG std::cout << "TabNotebook::leave\n"; #endif m_sig_tab_leave_event.emit(); return Gtk::Notebook::on_leave_notify_event( event ); } // // マウスホイールを回した // bool TabNotebook::on_scroll_event( GdkEventScroll* event ) { if( ! CONFIG::get_switchtab_wheel() ) return true; #ifdef _DEBUG std::cout << "TabNotebook::scroll\n"; #endif if( m_sig_scroll_event.emit( event ) ) return true; return Gtk::Notebook::on_scroll_event( event ); } // // ドラッグ中にマウスを動かした // bool TabNotebook::on_drag_motion( const Glib::RefPtr& context, int x, int y, guint time) { #ifdef _DEBUG std::cout << "Gtk::Notebook::on_drag_motion x = " << x << " y = " << y << std::endl; #endif int tab_x = -1; int tab_y = -1; int tab_w = -1; int page = get_page_under_mouse(); if( page >= 0 ){ if( page >= get_n_pages() ) page = get_n_pages() -1; SKELETON::TabLabel* tab = get_tablabel( page ); if( tab ){ tab_x = tab->get_tab_x(); tab_y = tab->get_tab_y(); tab_w = tab->get_tab_width(); #ifdef _DEBUG std::cout << "page = " << page << " tab_x = " << tab_x << " tab_y = " << tab_y << " tab_w " << tab_w << std::endl; #endif } } m_sig_tab_drag_motion( page, tab_x, tab_y, tab_w ); // on_drag_motion をキャンセルしないとDnD中にタブが勝手に切り替わる( gtknotebook.c をハック ) return true; } jdim-0.10.1/src/skeleton/tabnote.h000066400000000000000000000071341445721505100167450ustar00rootroot00000000000000// ライセンス: GPL2 // // DragableNoteBookを構成するタブ表示用の Notebook // #ifndef _TABNOTE_H #define _TABNOTE_H #include namespace SKELETON { class DragableNoteBook; class TabLabel; struct Alloc_NoteBook; // タブのクリック typedef sigc::signal< bool, GdkEventButton* > SIG_BUTTON_PRESS; typedef sigc::signal< bool, GdkEventButton* > SIG_BUTTON_RELEASE; // マウス移動 typedef sigc::signal< void > SIG_TAB_MOTION_EVENT; typedef sigc::signal< void > SIG_TAB_LEAVE_EVENT; // ホイール回転 typedef sigc::signal< bool, GdkEventScroll* > SIG_SCROLL_EVENT; // D&D typedef sigc::signal< void, const int, const int, const int, const int > SIG_TAB_DRAG_MOTION; // タブ用の Notebook class TabNotebook : public Gtk::Notebook { SIG_BUTTON_PRESS m_sig_button_press; SIG_BUTTON_RELEASE m_sig_button_release; SIG_TAB_MOTION_EVENT m_sig_tab_motion_event; SIG_TAB_LEAVE_EVENT m_sig_tab_leave_event; SIG_TAB_DRAG_MOTION m_sig_tab_drag_motion; SIG_SCROLL_EVENT m_sig_scroll_event; DragableNoteBook* m_parent; // タブ幅計算用layout Glib::RefPtr< Pango::Layout > m_layout_tab; int m_pre_width; bool m_fixtab{}; int m_tab_mrg; const std::string m_str_empty; public: SIG_BUTTON_PRESS sig_button_press(){ return m_sig_button_press; } SIG_BUTTON_RELEASE sig_button_release(){ return m_sig_button_release; } SIG_TAB_MOTION_EVENT sig_tab_motion_event(){ return m_sig_tab_motion_event; } SIG_TAB_LEAVE_EVENT sig_tab_leave_event(){ return m_sig_tab_leave_event; } SIG_TAB_DRAG_MOTION sig_tab_drag_motion(){ return m_sig_tab_drag_motion; } SIG_SCROLL_EVENT sig_scroll_event(){ return m_sig_scroll_event; } explicit TabNotebook( DragableNoteBook* parent ); ~TabNotebook() noexcept; void clock_in(); // タブの幅を固定するか(デフォルト false ); void set_fixtab( bool fix ){ m_fixtab = fix; } int append_tab( Widget& tab ); int insert_tab( Widget& tab, int page ); void remove_tab( const int page, const bool adjust_tab ); void reorder_child( int page1, int page2 ); // タブ取得 SKELETON::TabLabel* get_tablabel( int page ); // タブの文字列取得/セット const std::string& get_tab_fulltext( int page ); void set_tab_fulltext( const std::string& str, int page ); // タブ幅調整 bool adjust_tabwidth(); // マウスの下にあるタブの番号を取得 // タブ上では無いときは-1を返す // マウスがタブの右側にある場合はページ数の値を返す int get_page_under_mouse(); private: // 各タブのサイズと座標を取得 void calc_tabsize(); protected: void on_size_allocate( Gtk::Allocation& allocation ) override; // signal_button_press_event と signal_button_release_event は emit されない // ときがあるので自前でemitする bool on_button_press_event( GdkEventButton* event ) override; bool on_button_release_event( GdkEventButton* event ) override; bool on_motion_notify_event( GdkEventMotion* event ) override; bool on_leave_notify_event( GdkEventCrossing* event ) override; bool on_scroll_event( GdkEventScroll* event ) override; bool on_drag_motion( const Glib::RefPtr& context, int x, int y, guint time) override; }; } #endif jdim-0.10.1/src/skeleton/tabswitchbutton.cpp000066400000000000000000000017721445721505100210720ustar00rootroot00000000000000// ライセンス: GPL2 //#define _DEBUG #include "jddebug.h" #include "tabswitchbutton.h" using namespace SKELETON; TabSwitchButton::TabSwitchButton( DragableNoteBook* ) : Gtk::Notebook() { set_border_width( 0 ); m_arrow.set_from_icon_name( "pan-down-symbolic", Gtk::ICON_SIZE_SMALL_TOOLBAR ); m_button.add( m_arrow ); m_button.show_all_children(); m_button.set_relief( Gtk::RELIEF_NONE ); m_button.set_focus_on_click( false ); // フォーカス時にボタンの枠がはみ出さないようにする m_button.set_margin_top( 0 ); m_button.set_margin_bottom( 0 ); m_vbox.pack_start( m_button, Gtk::PACK_SHRINK ); set_show_tabs( false ); } TabSwitchButton::~TabSwitchButton() noexcept = default; void TabSwitchButton::show_button() { if( m_shown ) return; append_page( m_vbox ); show_all_children(); m_shown = true; } void TabSwitchButton::hide_button() { if( ! m_shown ) return; remove_page( m_vbox ); m_shown = false; } jdim-0.10.1/src/skeleton/tabswitchbutton.h000066400000000000000000000012371445721505100205330ustar00rootroot00000000000000// ライセンス: GPL2 // // タブの切り替えボタン // // TODO: 使われなくなったコンストラクタの引数を整理する #ifndef TABSWITCHBUTON_H #define TABSWITCHBUTON_H #include namespace SKELETON { class DragableNoteBook; class TabSwitchButton: public Gtk::Notebook { Gtk::VBox m_vbox; Gtk::Button m_button; Gtk::Image m_arrow; bool m_shown = false; public: explicit TabSwitchButton( DragableNoteBook* ); ~TabSwitchButton() noexcept; Gtk::Button& get_button(){ return m_button; } void show_button(); void hide_button(); }; } #endif jdim-0.10.1/src/skeleton/tabswitchmenu.cpp000066400000000000000000000054471445721505100205260ustar00rootroot00000000000000// ライセンス: GPL2 //#define _DEBUG #include "jddebug.h" #include "tabswitchmenu.h" #include "dragnote.h" #include "icons/iconmanager.h" #include "jdlib/miscutil.h" using namespace SKELETON; Glib::RefPtr TabSwitchMenu::create( DragableNoteBook* notebook ) { return Glib::RefPtr( new TabSwitchMenu( notebook ) ); } TabSwitchMenu::TabSwitchMenu( DragableNoteBook* notebook ) : Gio::Menu() , m_parentnote( notebook ) , m_deactivated{ true } { } /** * @brief メニュー項目を作り直してラベルとアイコンを更新する */ void TabSwitchMenu::update_labels_and_icons() { // Gio::Menu に追加した Gio::MenuItem の属性は変更できないため // メニューを更新するときは item を全て削除して追加し直す remove_all(); alloc_items(); const int n_pages = m_parentnote->get_n_pages(); for( int i = 0; i < n_pages; ++i ) { // ラベルの更新 std::string label = m_parentnote->get_tab_fulltext( i ); if( label.empty() ) label = "\?\?\?"; const unsigned int maxsize = 50; m_items[i]->set_label( MISC::cut_str( label, maxsize ) ); // アイコンの更新 const int icon = m_parentnote->get_tabicon( i ); if( icon != ICON::NONE && icon != ICON::NUM_ICONS ){ m_items[i]->set_icon( ICON::get_icon( icon ) ); } append_item( m_items[i] ); } m_deactivated = false; } /** * @brief メニュー項目を作り直してアイコンを更新する */ void TabSwitchMenu::update_icons() { // メニューが表示されてないときはアイコンの更新を行わない if( m_deactivated ) return; // Gio::Menu に追加した Gio::MenuItem の属性は変更できないため // メニューを更新するときは item を全て削除して追加し直す remove_all(); alloc_items(); const int n_pages = m_parentnote->get_n_pages(); for( int i = 0; i < n_pages; ++i ) { // アイコンの更新 const int icon = m_parentnote->get_tabicon( i ); if( icon != ICON::NONE && icon != ICON::NUM_ICONS ){ m_items[i]->set_icon( ICON::get_icon( icon ) ); } append_item( m_items[i] ); } } /** * @brief メニューがスクリーンから消されるときに呼び出す */ void TabSwitchMenu::deactivate() { m_deactivated = true; } /** * @brief メニュー項目を必要な分だけ確保しておき使い回す */ void TabSwitchMenu::alloc_items() { const Glib::ustring empty_label; const int n_pages = m_parentnote->get_n_pages(); for( int i = m_items.size(); i < n_pages; ++i ) { m_items.push_back( Gio::MenuItem::create( empty_label, Glib::ustring::compose( "admin.tab-switch(%1)", i ) ) ); } } jdim-0.10.1/src/skeleton/tabswitchmenu.h000066400000000000000000000024471445721505100201700ustar00rootroot00000000000000// ライセンス: GPL2 // // タブの切り替えメニュー // #ifndef SKELETON_TABSWITCHMENU_H #define SKELETON_TABSWITCHMENU_H #include #include namespace SKELETON { class DragableNoteBook; /** * @brief タブを切り替えるメニューのモデル * * SKELETON::Admin でメニュー項目の部品として使われる */ class TabSwitchMenu : public Gio::Menu { /// Admin メンバー変数への参照で所有しない DragableNoteBook* m_parentnote; std::vector> m_items; bool m_deactivated; public: static Glib::RefPtr create( DragableNoteBook* notebook ); explicit TabSwitchMenu( DragableNoteBook* notebook ); ~TabSwitchMenu() noexcept = default; /// メニュー項目を作り直してラベルとアイコンを更新する void update_labels_and_icons(); /// メニュー項目を作り直してアイコンを更新する void update_icons(); /// メニューがスクリーンから消されるときに呼び出す void deactivate(); private: /// メニュー項目を必要な分だけ確保しておき使い回す void alloc_items(); }; } #endif jdim-0.10.1/src/skeleton/textloader.cpp000066400000000000000000000100221445721505100200050ustar00rootroot00000000000000// ライセンス: GPL2 //#define _DEBUG #include "jddebug.h" #include "textloader.h" #include "jdlib/jdiconv.h" #include "jdlib/loaderdata.h" #include "jdlib/miscmsg.h" #include "httpcode.h" #include "session.h" #include "cache.h" #include namespace SKELETON::tl { constexpr std::size_t kSizeOfRawData = 256 * 1024; } using namespace SKELETON; TextLoader::TextLoader() : SKELETON::Loadable() { #ifdef _DEBUG std::cout << "TextLoader::TextLoader\n"; #endif } TextLoader::~TextLoader() { #ifdef _DEBUG std::cout << "TextLoader::~TextLoader\n"; #endif clear(); } void TextLoader::init() { clear(); if( m_rawdata.capacity() < tl::kSizeOfRawData ) { m_rawdata.reserve( tl::kSizeOfRawData ); } } void TextLoader::clear() { m_rawdata.clear(); m_rawdata.shrink_to_fit(); } void TextLoader::reset() { m_loaded = false; m_data = std::string(); clear(); } /** @brief キャッシュからロード * * @details 読み込んだキャッシュはUTF-8に変換する。 * @param[in] encoding キャッシュの文字エンコーディング */ void TextLoader::load_text( const Encoding encoding ) { if( get_path().empty() ) return; init(); set_code( HTTP_INIT ); set_encoding( encoding ); receive_finish(); } /** @brief ダウンロードを開始する * * @details ダウンロードしたテキストはUTF-8に変換する。 * HTTP 304 Not Modified の時はキャッシュから読み込む。 * @param[in] encoding ダウンロードしたテキストの文字エンコーディング */ void TextLoader::download_text( const Encoding encoding ) { #ifdef _DEBUG std::cout << "TextLoader::download_text url = " << get_url() << std::endl; #endif if( get_url().empty() ) return; if( is_loading() ) return; if( m_loaded ) return; // 読み込み済み if( ! SESSION::is_online() ){ load_text( encoding ); return; } #ifdef _DEBUG std::cout << "start loading...\n"; #endif JDLIB::LOADERDATA data; init(); set_encoding( encoding ); create_loaderdata( data ); if( data.url.empty() ) return; if( ! start_load( data ) ) clear(); } // // ローダよりデータ受信 // void TextLoader::receive_data( std::string_view buf ) { m_rawdata.append( buf ); } // // ロード完了 // void TextLoader::receive_finish() { #ifdef _DEBUG std::cout << "TextLoader::receive_finish code = " << get_str_code() << std::endl << "lng = " << m_rawdata.size() << " modified = " << get_date_modified() << std::endl; #endif // 初期化時やnot modifiedの時はキャッシュから読み込み if( ! get_path().empty() && ( get_code() == HTTP_INIT || get_code() == HTTP_NOT_MODIFIED ) ){ m_rawdata.resize( tl::kSizeOfRawData ); const std::size_t read_size = CACHE::load_rawdata( get_path(), m_rawdata.data(), tl::kSizeOfRawData ); m_rawdata.resize( read_size ); #ifdef _DEBUG std::cout << "read from " << get_path() << std::endl; if( ! read_size ) std::cout << "no data in cache!\n"; #endif if( ! read_size && get_code() == HTTP_INIT ) set_date_modified( std::string() ); } // 読み込みエラー if( get_code() != HTTP_OK && get_code() != HTTP_INIT && get_code() != HTTP_NOT_MODIFIED ){ if( get_code() == HTTP_NOT_FOUND ) m_loaded = true; clear(); set_date_modified( std::string() ); parse_data(); return; } // キャッシュに保存 if( ! get_path().empty() && get_code() == HTTP_OK && ! m_rawdata.empty() ){ CACHE::save_rawdata( get_path(), m_rawdata ); #ifdef _DEBUG std::cout << "save to " << get_path() << std::endl; #endif } if( get_code() != HTTP_INIT ) m_loaded = true; set_code( HTTP_OK ); set_str_code( std::string() ); // UTF-8に変換しておく JDLIB::Iconv libiconv( Encoding::utf8, get_encoding() ); libiconv.convert( m_rawdata.data(), m_rawdata.size(), m_data ); clear(); receive_cookies(); parse_data(); } jdim-0.10.1/src/skeleton/textloader.h000066400000000000000000000032711445721505100174620ustar00rootroot00000000000000// ライセンス: GPL2 // // テキストファイルの簡易ローダ // // ロードしたファイルはget_path()で示されたパスに保存される // get_path() が empty() ならば保存しない // #ifndef _TEXTLODER_H #define _TEXTLODER_H #include "jdencoding.h" #include "loadable.h" #include #include namespace JDLIB { class LOADERDATA; } namespace SKELETON { class TextLoader : public SKELETON::Loadable { bool m_loaded{}; // 読み込み済みか std::string m_rawdata; std::string m_data; public: TextLoader(); ~TextLoader(); const std::string& get_data() const { return m_data; } // 一度ロードしたらreset()を呼ばない限りリロードしない void reset(); // キャッシュからロード void load_text( const Encoding encoding ); // ダウンロードを開始する // HTTP 304 Not Modified の時はキャッシュから読み込む void download_text( const Encoding encoding ); protected: virtual std::string get_url() const = 0; virtual std::string get_path() const = 0; // ロード用データ作成 virtual void create_loaderdata( JDLIB::LOADERDATA& data ) = 0; // ロード後に呼び出される virtual void parse_data() = 0; private: void init(); void clear(); void receive_data( std::string_view buf ) override; void receive_finish() override; // HTTP応答ヘッダーのクッキーを取り扱う場合は派生クラスでoverrideする virtual void receive_cookies() {} }; } #endif jdim-0.10.1/src/skeleton/toolbar.cpp000066400000000000000000000630571445721505100173140ustar00rootroot00000000000000// ライセンス: GPL2 //#define _DEBUG #include "jddebug.h" #include "toolbar.h" #include "admin.h" #include "view.h" #include "imgtoolbutton.h" #include "menubutton.h" #include "toolmenubutton.h" #include "backforwardbutton.h" #include "compentry.h" #include "dbtree/interface.h" #include "jdlib/miscutil.h" #include "icons/iconmanager.h" #include "config/globalconf.h" #include "control/controlutil.h" #include "control/controlid.h" #include "command.h" #include "prefdiagfactory.h" using namespace SKELETON; constexpr const char* ToolBar::s_css_label; ToolBar::ToolBar( Admin* admin ) : m_admin( admin ) , m_enable_slot{ true } { m_buttonbar.set_border_width( 0 ); m_buttonbar.set_icon_size( Gtk::ICON_SIZE_MENU ); m_buttonbar.set_toolbar_style( Gtk::TOOLBAR_ICONS ); // ツールバーの子ウィジェットのコンテキストメニューの配色がGTKテーマと違うことがある。 // ツールバーのcssクラスを削除しコンテキストメニューの配色を修正する。 m_buttonbar.get_style_context()->remove_class( GTK_STYLE_CLASS_TOOLBAR ); } ToolBar::~ToolBar() noexcept = default; void ToolBar::set_url( const std::string& url ) { m_url = url; if( m_button_back ) m_button_back->get_backforward_button()->set_url( m_url ); if( m_button_forward ) m_button_forward->get_backforward_button()->set_url( m_url ); } // // タブが切り替わった時にDragableNoteBook::set_current_toolbar()から呼び出される( Viewの情報を取得する ) // // virtual void ToolBar::set_view( SKELETON::View* view ) { if( ! view ) return; // slot関数を実行しない m_enable_slot = false; set_url( view->get_url() ); // ラベル表示更新 set_label( view->get_label(), view->get_label_use_markup() ); if( m_tool_label ) { const std::string& tooltip{ view->get_tooltip_label() }; set_tooltip( *m_tool_label, tooltip.empty() ? view->get_label() : tooltip, view->get_label_use_markup() ); } if( CONFIG::get_change_statitle_color() && ( view->is_broken() || view->is_old() || view->is_overflow() ) ) { set_color( view->get_color() ); } // 閉じるボタンの表示更新 if( m_button_close ){ if( view->is_locked() ) m_button_close->set_sensitive( false ); else m_button_close->set_sensitive( true ); if( m_button_lock ) m_button_lock->set_active( view->is_locked() ); } if( m_button_write ) m_button_write->set_sensitive( view->is_writeable() ); if( m_entry_search ) m_entry_search->set_text( view->get_search_query() ); if( m_label_board ) m_label_board->set_text( DBTREE::board_name( get_url() ) ); m_enable_slot = true; } // // タブが切り替わった時にDragableNoteBookから呼び出される( ツールバーを表示する ) // // gtk+-2.4 辺りの古い gtk では、スレビューなどツールバーの複数が複数ある場合に // 最初からボタンバーを pack するとボタンが押せなくなる症状があったので、実際に // ツールバーが表示されるまでpackしないようにした // void ToolBar::show_toolbar() { // ボタンバーのpack if( m_buttonbar_shown && ! m_buttonbar_packed ){ if( m_searchbar_packed ) remove( *m_searchbar ); pack_start( m_buttonbar, Gtk::PACK_SHRINK ); if( m_searchbar_packed ) pack_start( *m_searchbar, Gtk::PACK_SHRINK ); show_all_children(); set_relief(); m_buttonbar_packed = true; } // 検索バーのpack if( m_searchbar_shown && ! m_searchbar_packed ){ pack_start( *m_searchbar, Gtk::PACK_SHRINK ); show_all_children(); set_relief(); m_searchbar_packed = true; } } // ボタンバー表示 void ToolBar::open_buttonbar() { // フラグだけ立てて実際に pack するのは show_toolbar() の中 // 何故そんな面倒な事をしているかは show_toolbar() の説明を参照 m_buttonbar_shown = true; } // ボタンバーを隠す void ToolBar::close_buttonbar() { m_buttonbar_shown = false; if( m_buttonbar_packed ){ remove( m_buttonbar ); show_all_children(); m_buttonbar_packed = false; } } // ボタン表示更新 void ToolBar::update_button() { unpack_buttons(); pack_buttons(); if( ! m_buttonbar_shown ) close_buttonbar(); // 進む、戻るボタンのsensitive状態を更新する set_url( m_url ); } // ボタンのアンパック void ToolBar::unpack_buttons() { std::vector< Gtk::Widget* > lists = m_buttonbar.get_children(); for( Gtk::Widget* widget : lists ) { m_buttonbar.remove( *widget ); if( dynamic_cast( widget ) ) delete widget; } } // 検索ツールバー上のボタンのアンパック void ToolBar::unpack_search_buttons() { if( ! m_searchbar ) return; std::vector< Gtk::Widget* > lists = m_searchbar->get_children(); for( Gtk::Widget* widget : lists ) { m_searchbar->remove( *widget ); if( dynamic_cast( widget ) ) delete widget; } } // ボタンのrelief指定 void ToolBar::set_relief() { std::vector< Gtk::Widget* > lists_toolbar = get_children(); for( Gtk::Widget* bar_widget : lists_toolbar ) { Gtk::Toolbar* toolbar = dynamic_cast( bar_widget ); if( ! toolbar ) continue; std::vector< Gtk::Widget* > lists = toolbar->get_children(); for( Gtk::Widget* btn_widget : lists ) { Gtk::Button* button = nullptr; if( auto toolitem = dynamic_cast( btn_widget ); toolitem ) { button = dynamic_cast( toolitem->get_child() ); } if( button ){ auto context = button->get_style_context(); if( CONFIG::get_flat_button() ) context->add_class( GTK_STYLE_CLASS_FLAT ); else context->remove_class( GTK_STYLE_CLASS_FLAT ); } } } } // 区切り追加 void ToolBar::pack_separator() { Gtk::SeparatorToolItem *sep = Gtk::manage( new Gtk::SeparatorToolItem() ); // delete は unpack_buttons() で行う m_buttonbar.append( *sep ); } // 透明区切り追加 void ToolBar::pack_transparent_separator() { Gtk::SeparatorToolItem *sep = Gtk::manage( new Gtk::SeparatorToolItem() ); // delete は unpack_buttons() で行う sep->set_draw( false ); m_buttonbar.append( *sep ); } // // ツールチップ // void ToolBar::set_tooltip( Gtk::ToolItem& toolitem, const std::string& tip, const bool use_markup ) { if( use_markup ) toolitem.set_tooltip_markup( tip ); else toolitem.set_tooltip_text( tip ); } // // ラベル // Gtk::ToolItem* ToolBar::get_label() { if( ! m_tool_label ){ m_tool_label = Gtk::manage( new Gtk::ToolItem ); m_ebox_label = Gtk::manage( new Gtk::EventBox ); m_label = Gtk::manage( new Gtk::Label ); m_label->set_ellipsize( Pango::ELLIPSIZE_END ); m_label->set_xalign( 0 ); m_label->set_selectable( true ); m_ebox_label->add( *m_label ); m_ebox_label->set_visible_window( false ); m_tool_label->add( *m_ebox_label ); m_tool_label->set_expand( true ); auto context = m_label->get_style_context(); context->add_class( s_css_label ); context->add_provider( m_label_provider, GTK_STYLE_PROVIDER_PRIORITY_APPLICATION ); try { m_label_provider->load_from_data( ".red:not(:selected), .red:active:not(:selected) { color: white; background-color: red; }" ".green:not(:selected), .green:active:not(:selected) { color: white; background-color: green; }" ".blue:not(:selected), .blue:active:not(:selected) { color: white; background-color: blue; }" ); } catch( Gtk::CssProviderError& err ) { #ifdef _DEBUG std::cout << "ERROR:ToolBar::get_label css fail " << err.what() << std::endl; #endif } } return m_tool_label; } void ToolBar::set_label( const std::string& label, const bool use_markup ) { if( ! m_ebox_label ) return; m_label->set_text( label ); m_label->set_use_markup( use_markup ); set_color( "" ); } void ToolBar::set_color( const std::string& color ) { if( ! m_ebox_label ) return; auto context = m_label->get_style_context(); context->remove_class( "red" ); context->remove_class( "green" ); context->remove_class( "blue" ); if( ! color.empty() ) context->add_class( color ); } // 検索バー Gtk::Toolbar* ToolBar::get_searchbar() { if( ! m_searchbar ){ m_searchbar = Gtk::manage( new SKELETON::JDToolbar() ); m_searchbar->set_icon_size( Gtk::ICON_SIZE_MENU ); m_searchbar->set_toolbar_style( Gtk::TOOLBAR_ICONS ); } return m_searchbar; } // 検索バー表示 void ToolBar::open_searchbar() { if( ! m_searchbar ) return; #ifdef _DEBUG std::cout << "ToolBar::open_searchbar\n"; #endif // フラグだけ立てて実際に pack するのは show_toolbar() の中 // 何故そんな面倒な事をしているかは show_toolbar() の説明を参照 m_searchbar_shown = true; } // 検索バー非表示 void ToolBar::close_searchbar() { if( ! m_searchbar ) return; #ifdef _DEBUG std::cout << "ToolBar::close_searchbar\n"; #endif m_searchbar_shown = false; if( m_searchbar_packed ){ remove( *m_searchbar ); show_all_children(); m_searchbar_packed = false; m_admin->set_command( "focus_current_view" ); } } // // 検索バーを開く/閉じるボタン // Gtk::ToolItem* ToolBar::get_button_open_searchbar() { if( ! m_button_open_searchbar ){ m_button_open_searchbar = Gtk::manage( new ImgToolButton( ICON::SEARCH, CONTROL::Search ) ); std::string tooltip = "検索バーを開く " + CONTROL::get_str_motions( CONTROL::Search ); set_tooltip( *m_button_open_searchbar, tooltip ); m_button_open_searchbar->signal_clicked().connect( sigc::mem_fun(*this, &ToolBar::slot_toggle_searchbar ) ); } return m_button_open_searchbar; } Gtk::ToolItem* ToolBar::get_button_close_searchbar() { if( ! m_button_close_searchbar ){ m_button_close_searchbar = Gtk::manage( new ImgToolButton( ICON::CLOSE_SEARCH, CONTROL::CloseSearchBar ) ); set_tooltip( *m_button_close_searchbar, CONTROL::get_label_motions( CONTROL::CloseSearchBar ) ); m_button_close_searchbar->signal_clicked().connect( sigc::mem_fun(*this, &ToolBar::slot_toggle_searchbar ) ); } return m_button_close_searchbar; } // 検索バー表示/非表示切り替え void ToolBar::slot_toggle_searchbar() { if( ! m_enable_slot ) return; if( ! m_searchbar ) return; #ifdef _DEBUG std::cout << "ToolBar::slot_toggle_searchbar shown = " << m_searchbar_shown << std::endl; #endif if( ! m_searchbar_shown ) m_admin->set_command( "open_searchbar", m_url ); else m_admin->set_command( "close_searchbar", m_url ); } // // 検索 entry // Gtk::ToolItem* ToolBar::get_tool_search( const int mode ) { if( ! m_tool_search ){ m_tool_search = Gtk::manage( new Gtk::ToolItem ); m_entry_search = Gtk::manage( new CompletionEntry( mode ) ); m_entry_search->signal_changed().connect( sigc::mem_fun( *this, &ToolBar::slot_changed_search ) ); m_entry_search->signal_activate().connect( sigc::mem_fun( *this, &ToolBar::slot_active_search ) ); m_entry_search->signal_operate().connect( sigc::mem_fun( *this, &ToolBar::slot_operate_search ) ); m_tool_search->add( *m_entry_search ); m_tool_search->set_expand( true ); } return m_tool_search; } SKELETON::CompletionEntry* ToolBar::get_entry_search() { return m_entry_search; } void ToolBar::add_search_control_mode( const int mode ) { if( m_entry_search ) m_entry_search->add_control_mode( mode ); } std::string ToolBar::get_search_text() const { if( ! m_entry_search ) return std::string(); return m_entry_search->get_text(); } void ToolBar::slot_changed_search() { if( ! m_enable_slot ) return; if( m_url.empty() || ! m_admin ) return; std::string query = m_entry_search->get_text(); #ifdef _DEBUG std::cout << "ToolBar::slot_changed_search query = " << query << std::endl; #endif m_admin->set_command( "toolbar_set_search_query", m_url, query ); } void ToolBar::slot_active_search() { if( ! m_enable_slot ) return; if( m_url.empty() || ! m_admin ) return; #ifdef _DEBUG std::cout << "ToolBar::slot_active_search\n"; #endif m_admin->set_command( "toolbar_exec_search", m_url ); } void ToolBar::slot_operate_search( const int controlid ) { if( ! m_enable_slot ) return; if( m_url.empty() || ! m_admin ) return; #ifdef _DEBUG std::cout << "ToolBar::slot_operate_search id = " << controlid << std::endl; #endif m_admin->set_command( "toolbar_operate_search", m_url, std::to_string( controlid ) ); } // 検索 entry をフォーカス void ToolBar::focus_entry_search() { if( m_entry_search ) m_entry_search->grab_focus(); } // // 上検索 // Gtk::ToolButton* ToolBar::get_button_up_search() { if( ! m_button_up_search ){ m_button_up_search = Gtk::manage( new ImgToolButton( ICON::SEARCH_PREV, CONTROL::SearchPrev ) ); set_tooltip( *m_button_up_search, CONTROL::get_label_motions( CONTROL::SearchPrev ) ); m_button_up_search->signal_clicked().connect( sigc::mem_fun(*this, &ToolBar::slot_clicked_up_search ) ); } return m_button_up_search; } void ToolBar::slot_clicked_up_search() { if( ! m_enable_slot ) return; if( m_url.empty() || ! m_admin ) return; #ifdef _DEBUG std::cout << "ToolBar::slot_clicked_up_search\n"; #endif m_admin->set_command( "toolbar_up_search", m_url ); } // // 下検索 // Gtk::ToolButton* ToolBar::get_button_down_search() { if( ! m_button_down_search ){ m_button_down_search = Gtk::manage( new ImgToolButton( ICON::SEARCH_NEXT, CONTROL::SearchNext ) ); set_tooltip( *m_button_down_search, CONTROL::get_label_motions( CONTROL::SearchNext ) ); m_button_down_search->signal_clicked().connect( sigc::mem_fun(*this, &ToolBar::slot_clicked_down_search ) ); } return m_button_down_search; } void ToolBar::slot_clicked_down_search() { if( ! m_enable_slot ) return; if( m_url.empty() || ! m_admin ) return; #ifdef _DEBUG std::cout << "ToolBar::slot_clicked_down_search\n"; #endif m_admin->set_command( "toolbar_down_search", m_url ); } // // ハイライト解除 // Gtk::ToolButton* ToolBar::get_button_clear_highlight() { if( ! m_button_clear_highlight ){ m_button_clear_highlight = Gtk::manage( new ImgToolButton( ICON::CLEAR_SEARCH, CONTROL::HiLightOff ) ); set_tooltip( *m_button_clear_highlight, CONTROL::get_label_motions( CONTROL::HiLightOff ) ); m_button_clear_highlight->signal_clicked().connect( sigc::mem_fun(*this, &ToolBar::slot_clear_highlight ) ); } return m_button_clear_highlight; } void ToolBar::slot_clear_highlight() { if( ! m_enable_slot ) return; if( m_url.empty() || ! m_admin ) return; m_admin->set_command( "clear_highlight", m_url ); } // 板を開くボタン Gtk::ToolItem* ToolBar::get_button_board() { if( ! m_button_board ){ m_label_board = Gtk::manage( new Gtk::Label ); m_label_board->set_xalign( 0 ); m_button_board = Gtk::manage( new SKELETON::ToolMenuButton( CONTROL::get_label( CONTROL::OpenParentBoard ), false, true, *m_label_board ) ); std::vector< std::string > menu; menu.push_back( "開く" ); menu.push_back( "再読み込みして開く" ); menu.push_back( "separator" ); menu.push_back( "ローカルルールを表示" ); menu.push_back( "板のプロパティを表示" ); m_button_board->get_button()->append_menu( menu ); m_button_board->get_button()->signal_selected().connect( sigc::mem_fun(*this, &ToolBar::slot_menu_board ) ); m_button_board->get_button()->signal_button_clicked().connect( sigc::mem_fun(*this, &ToolBar::slot_open_board ) ); set_tooltip( *m_button_board, CONTROL::get_label_motions( CONTROL::OpenParentBoard ) ); } return m_button_board; } void ToolBar::slot_open_board() { if( ! m_enable_slot ) return; CORE::core_set_command( "open_board", DBTREE::url_boardbase( get_url() ), "true", "auto" // オートモードで開く ); } void ToolBar::slot_menu_board( int i ) { if( ! m_enable_slot ) return; std::unique_ptr pref; // ToolBar::get_button_board()で作成したメニューの順番に内容を合わせる if( i == 0 ) slot_open_board(); else if( i == 1 ) CORE::core_set_command( "open_board", DBTREE::url_boardbase( get_url() ), "true" ); else if( i == 3 ) pref = CORE::PrefDiagFactory( CORE::get_mainwindow(), CORE::PREFDIAG_BOARD, DBTREE::url_boardbase( get_url() ), "show_localrule" ); else if( i == 4 ) pref = CORE::PrefDiagFactory( CORE::get_mainwindow(), CORE::PREFDIAG_BOARD, DBTREE::url_boardbase( get_url() ) ); if( pref ){ pref->run(); } } // // 書き込みボタン // Gtk::ToolButton* ToolBar::get_button_write() { if( ! m_button_write ){ m_button_write = Gtk::manage( new ImgToolButton( ICON::WRITE, CONTROL::WriteMessage ) ); set_tooltip( *m_button_write, CONTROL::get_label_motions( CONTROL::WriteMessage ) ); m_button_write->signal_clicked().connect( sigc::mem_fun(*this, &ToolBar::slot_clicked_write ) ); } return m_button_write; } void ToolBar::slot_clicked_write() { if( ! m_enable_slot ) return; if( m_url.empty() || ! m_admin ) return; #ifdef _DEBUG std::cout << "ToolBar::slot_clicked_write\n"; #endif m_admin->set_command( "toolbar_write", m_url ); } // 書き込みボタンをフォーカス void ToolBar::focus_button_write() { get_button_write()->get_child()->grab_focus(); } // // 再読み込みボタン // Gtk::ToolButton* ToolBar::get_button_reload() { if( ! m_button_reload ){ m_button_reload = Gtk::manage( new ImgToolButton( ICON::RELOAD, CONTROL::Reload ) ); set_tooltip( *m_button_reload, CONTROL::get_label_motions( CONTROL::Reload ) ); m_button_reload->signal_clicked().connect( sigc::mem_fun(*this, &ToolBar::slot_clicked_reload ) ); } return m_button_reload; } void ToolBar::slot_clicked_reload() { if( ! m_enable_slot ) return; if( m_url.empty() || ! m_admin ) return; #ifdef _DEBUG std::cout << "ToolBar::slot_clicked_reload\n"; #endif m_admin->set_command( "toolbar_reload", m_url ); } // // 読み込み停止ボタン // Gtk::ToolButton* ToolBar::get_button_stop() { if( ! m_button_stop ){ m_button_stop = Gtk::manage( new ImgToolButton( ICON::STOPLOADING, CONTROL::StopLoading ) ); set_tooltip( *m_button_stop, CONTROL::get_label_motions( CONTROL::StopLoading ) ); m_button_stop->signal_clicked().connect( sigc::mem_fun(*this, &ToolBar::slot_clicked_stop ) ); } return m_button_stop; } void ToolBar::slot_clicked_stop() { if( ! m_enable_slot ) return; if( m_url.empty() || ! m_admin ) return; #ifdef _DEBUG std::cout << "ToolBar::slot_clicked_stop\n"; #endif m_admin->set_command( "toolbar_stop", m_url ); } // // 閉じるボタン // Gtk::ToolButton* ToolBar::get_button_close() { if( ! m_button_close ){ m_button_close = Gtk::manage( new ImgToolButton( ICON::QUIT, CONTROL::Quit ) ); set_tooltip( *m_button_close, CONTROL::get_label_motions( CONTROL::Quit ) ); m_button_close->signal_clicked().connect( sigc::mem_fun(*this, &ToolBar::slot_clicked_close ) ); } return m_button_close; } void ToolBar::slot_clicked_close() { if( ! m_enable_slot ) return; if( m_url.empty() || ! m_admin ) return; #ifdef _DEBUG std::cout << "ToolBar::slot_clicked_close\n"; #endif // relief が Gtk:: RELIEF_NONE のときにタブの最後のビューを閉じると、 // ボタンに leave_notify イベントが送られないため、次にビューを開いたときに // 枠が残ったままになる if( m_admin->get_tab_nums() == 1 ){ Gtk::Button* button = dynamic_cast< Gtk::Button* >( m_button_close->get_child() ); // ボタンを一旦非表示にして描画状態をリセットする button->unmap(); button->map(); } m_admin->set_command( "toolbar_close_view", m_url ); } // // 削除ボタン // Gtk::ToolItem* ToolBar::get_button_delete() { if( ! m_button_delete ){ m_button_delete = Gtk::manage( new ImgToolButton( ICON::DELETE, CONTROL::Delete ) ); set_tooltip( *m_button_delete, CONTROL::get_label_motions( CONTROL::Delete ) ); m_button_delete->signal_clicked().connect( sigc::mem_fun(*this, &ToolBar::slot_clicked_delete ) ); } return m_button_delete; } void ToolBar::slot_clicked_delete() { if( ! m_enable_slot ) return; if( m_url.empty() || ! m_admin ) return; #ifdef _DEBUG std::cout << "ToolBar::slot_clicked_delete\n"; #endif m_admin->set_command( "toolbar_delete_view", m_url ); } // // お気に入りボタン // Gtk::ToolItem* ToolBar::get_button_favorite() { if( ! m_button_favorite ){ m_button_favorite = Gtk::manage( new ImgToolButton( ICON::APPENDFAVORITE, CONTROL::AppendFavorite ) ); set_tooltip( *m_button_favorite, CONTROL::get_label_motions( CONTROL::AppendFavorite ) ); m_button_favorite->signal_clicked().connect( sigc::mem_fun(*this, &ToolBar::slot_clicked_favorite ) ); } return m_button_favorite; } void ToolBar::slot_clicked_favorite() { if( ! m_enable_slot ) return; if( m_url.empty() || ! m_admin ) return; #ifdef _DEBUG std::cout << "ToolBar::slot_clicked_favorite\n"; #endif m_admin->set_command( "toolbar_set_favorite", m_url ); } // // UNDO ボタン // Gtk::ToolButton* ToolBar::get_button_undo() { if( ! m_button_undo ){ m_button_undo = Gtk::manage( new ImgToolButton( ICON::UNDO, CONTROL::Undo ) ); m_button_undo->set_sensitive( false ); set_tooltip( *m_button_undo, CONTROL::get_label_motions( CONTROL::Undo ) ); } return m_button_undo; } // // REDO ボタン // Gtk::ToolButton* ToolBar::get_button_redo() { if( ! m_button_redo ){ m_button_redo = Gtk::manage( new ImgToolButton( ICON::REDO, CONTROL::Redo ) ); m_button_redo->set_sensitive( false ); set_tooltip( *m_button_redo, CONTROL::get_label_motions( CONTROL::Redo ) ); } return m_button_redo; } // // 戻るボタン // Gtk::ToolItem* ToolBar::get_button_back() { if( ! m_button_back ){ const std::string label = CONTROL::get_label( CONTROL::PrevView ); m_button_back = Gtk::manage( new SKELETON::ToolBackForwardButton( label, false, m_url, true ) ); m_button_back->get_button()->signal_button_clicked().connect( sigc::mem_fun(*this, &ToolBar::slot_clicked_back ) ); m_button_back->get_button()->signal_selected().connect( sigc::mem_fun(*this, &ToolBar::slot_selected_back ) ); set_tooltip( *m_button_back, CONTROL::get_label_motions( CONTROL::PrevView ) ); } return m_button_back; } void ToolBar::slot_clicked_back() { if( ! m_enable_slot ) return; if( m_url.empty() || ! m_admin ) return; #ifdef _DEBUG std::cout << "ToolBar::slot_clicked_back : " << m_url << std::endl; #endif m_admin->set_command( "back_viewhistory", m_url, "1" ); } void ToolBar::slot_selected_back( const int i ) { if( ! m_enable_slot ) return; if( m_url.empty() || ! m_admin ) return; #ifdef _DEBUG std::cout << "ToolBar::slot_selected_back : " << i << " url = " << m_url << std::endl; #endif m_admin->set_command( "back_viewhistory", m_url, std::to_string( i+1 ) ); } // // 進むボタン // Gtk::ToolItem* ToolBar::get_button_forward() { if( ! m_button_forward ){ const std::string label = CONTROL::get_label( CONTROL::NextView ); m_button_forward = Gtk::manage( new SKELETON::ToolBackForwardButton( label, false, m_url, false ) ); m_button_forward->get_button()->signal_button_clicked().connect( sigc::mem_fun(*this, &ToolBar::slot_clicked_forward ) ); m_button_forward->get_button()->signal_selected().connect( sigc::mem_fun(*this, &ToolBar::slot_selected_forward ) ); set_tooltip( *m_button_forward, CONTROL::get_label_motions( CONTROL::NextView ) ); } return m_button_forward; } void ToolBar::slot_clicked_forward() { if( ! m_enable_slot ) return; if( m_url.empty() || ! m_admin ) return; #ifdef _DEBUG std::cout << "ToolBar::slot_clicked_forward : " << m_url << std::endl; #endif m_admin->set_command( "forward_viewhistory", m_url, "1" ); } void ToolBar::slot_selected_forward( const int i ) { if( ! m_enable_slot ) return; if( m_url.empty() || ! m_admin ) return; #ifdef _DEBUG std::cout << "ToolBar::slot_selected_forward : " << i << " url = " << m_url << std::endl; #endif m_admin->set_command( "forward_viewhistory", m_url, std::to_string( i+1 ) ); } // // ロックボタン // Gtk::ToolButton* ToolBar::get_button_lock() { if( ! m_button_lock ){ m_button_lock = Gtk::manage( new ImgToggleToolButton( ICON::LOCK, CONTROL::Lock ) ); set_tooltip( *m_button_lock, CONTROL::get_label_motions( CONTROL::Lock ) ); m_button_lock->signal_clicked().connect( sigc::mem_fun( *this, &ToolBar::slot_lock_clicked ) ); } return m_button_lock; } void ToolBar::slot_lock_clicked() { if( ! m_enable_slot ) return; m_admin->set_command( "toolbar_lock_view", get_url() ); } jdim-0.10.1/src/skeleton/toolbar.h000066400000000000000000000136061445721505100167540ustar00rootroot00000000000000// ライセンス: GPL2 // // ツールバーの基底クラス // #ifndef _TOOLBAR_H #define _TOOLBAR_H #include "jdtoolbar.h" #include namespace SKELETON { class Admin; class View; class ToolMenuButton; class ToolBackForwardButton; class BackForwardButton; class CompletionEntry; class ToolBar : public Gtk::VBox { SKELETON::Admin* m_admin; std::string m_url; bool m_enable_slot; // ボタンバー SKELETON::JDToolbar m_buttonbar; bool m_buttonbar_shown{}; bool m_buttonbar_packed{}; // ラベル Gtk::ToolItem* m_tool_label{}; Gtk::EventBox* m_ebox_label{}; Gtk::Label* m_label{}; // 検索関係 Gtk::Toolbar* m_searchbar{}; // 検索バー bool m_searchbar_shown{}; bool m_searchbar_packed{}; Gtk::ToolButton* m_button_open_searchbar{}; Gtk::ToolButton* m_button_close_searchbar{}; Gtk::ToolButton* m_button_up_search{}; Gtk::ToolButton* m_button_down_search{}; Gtk::ToolButton* m_button_clear_highlight{}; Gtk::ToolItem* m_tool_search{}; SKELETON::CompletionEntry* m_entry_search{}; // 板を開く Gtk::Label* m_label_board{}; SKELETON::ToolMenuButton* m_button_board{}; // その他ボタン Gtk::ToolButton* m_button_write{}; Gtk::ToolButton* m_button_reload{}; Gtk::ToolButton* m_button_stop{}; Gtk::ToolButton* m_button_close{}; Gtk::ToolButton* m_button_delete{}; Gtk::ToolButton* m_button_favorite{}; Gtk::ToolButton* m_button_undo{}; Gtk::ToolButton* m_button_redo{}; Gtk::ToggleToolButton* m_button_lock{}; // 進む、戻るボタン SKELETON::ToolBackForwardButton* m_button_back{}; SKELETON::ToolBackForwardButton* m_button_forward{}; static constexpr const char* s_css_label = u8"jd-toolbar-label"; Glib::RefPtr< Gtk::CssProvider > m_label_provider = Gtk::CssProvider::create(); public: explicit ToolBar( Admin* admin ); ~ToolBar() noexcept; void set_url( const std::string& url ); const std::string& get_url() const { return m_url; } // タブが切り替わった時にDragableNoteBookから呼び出される( Viewの情報を取得する ) virtual void set_view( SKELETON::View * view ); // タブが切り替わった時にDragableNoteBookから呼び出される( ツールバーを表示する ) void show_toolbar(); // ボタンバー表示/非表示 void open_buttonbar(); void close_buttonbar(); // 検索バー表示/非表示 void open_searchbar(); void close_searchbar(); // 検索entryをフォーカス void focus_entry_search(); // 書き込みボタンをフォーカス void focus_button_write(); // ボタン表示更新 void update_button(); protected: SKELETON::Admin* get_admin(){ return m_admin; } // ボタンのパッキング virtual void pack_buttons() = 0; void unpack_buttons(); void unpack_search_buttons(); // ボタンのrelief指定 void set_relief(); Gtk::Toolbar& get_buttonbar(){ return m_buttonbar; } // ラベル Gtk::ToolItem* get_label(); // 検索関係 Gtk::Toolbar* get_searchbar(); Gtk::ToolItem* get_button_open_searchbar(); Gtk::ToolItem* get_button_close_searchbar(); // mode は補完モード ( compmanager.h 参照 ) Gtk::ToolItem* get_tool_search( const int mode ); SKELETON::CompletionEntry* get_entry_search(); // CompletionEntry の入力コントローラのモード設定 void add_search_control_mode( const int mode ); std::string get_search_text() const; // 上検索 Gtk::ToolButton* get_button_up_search(); // 下検索 Gtk::ToolButton* get_button_down_search(); // ハイライト解除 Gtk::ToolButton* get_button_clear_highlight(); // 板を開く Gtk::ToolItem* get_button_board(); // その他ボタン Gtk::ToolButton* get_button_write(); Gtk::ToolButton* get_button_reload(); Gtk::ToolButton* get_button_stop(); Gtk::ToolButton* get_button_close(); Gtk::ToolItem* get_button_delete(); Gtk::ToolItem* get_button_favorite(); Gtk::ToolButton* get_button_undo(); Gtk::ToolButton* get_button_redo(); Gtk::ToolButton* get_button_lock(); Gtk::ToolItem* get_button_back(); Gtk::ToolItem* get_button_forward(); void pack_separator(); void pack_transparent_separator(); void set_tooltip( Gtk::ToolItem& toolitem, const std::string& tip, const bool use_markup = false ); private: // ラベル関係 void set_label( const std::string& label, const bool use_markup = false ); void set_color( const std::string& color ); // 検索関係 void slot_toggle_searchbar(); void slot_changed_search(); void slot_active_search(); void slot_operate_search( const int controlid ); void slot_clicked_up_search(); void slot_clicked_down_search(); void slot_clear_highlight(); // 板を開く void slot_open_board(); void slot_menu_board( int i ); // その他ボタン void slot_clicked_write(); void slot_clicked_reload(); void slot_clicked_stop(); void slot_clicked_close(); void slot_clicked_delete(); void slot_clicked_favorite(); void slot_lock_clicked(); void slot_clicked_back(); void slot_selected_back( const int i ); void slot_clicked_forward(); void slot_selected_forward( const int i ); }; } #endif jdim-0.10.1/src/skeleton/toolbarnote.cpp000066400000000000000000000006141445721505100201700ustar00rootroot00000000000000// ライセンス: GPL2 //#define _DEBUG #include "jddebug.h" #include "toolbarnote.h" using namespace SKELETON; ToolBarNotebook::ToolBarNotebook( DragableNoteBook* ) : Gtk::Notebook() { set_show_border( true ); set_show_tabs( false ); set_border_width( 0 ); set_margin_top( 1 ); set_margin_bottom( 1 ); } ToolBarNotebook::~ToolBarNotebook() noexcept = default; jdim-0.10.1/src/skeleton/toolbarnote.h000066400000000000000000000007361445721505100176420ustar00rootroot00000000000000// ライセンス: GPL2 // // DragableNoteBookを構成するツールバー表示用の Notebook // // TODO: 使われなくなったコンストラクタの引数を整理する #ifndef _TOOLBARNOTE_H #define _TOOLBARNOTE_H #include namespace SKELETON { class DragableNoteBook; class ToolBarNotebook : public Gtk::Notebook { public: explicit ToolBarNotebook( DragableNoteBook* ); ~ToolBarNotebook() noexcept; }; } #endif jdim-0.10.1/src/skeleton/toolmenubutton.cpp000066400000000000000000000051141445721505100207360ustar00rootroot00000000000000// ライセンス: GPL2 //#define _DEBUG #include "jddebug.h" #include "toolmenubutton.h" #include "menubutton.h" #include "backforwardbutton.h" using namespace SKELETON; ToolMenuButton::ToolMenuButton() = default; ToolMenuButton::ToolMenuButton( const std::string& label, const bool expand, const bool show_arrow, Gtk::Widget& widget ) : ToolMenuButton( Gtk::manage( new SKELETON::MenuButton( show_arrow, widget ) ), label, expand ) { } ToolMenuButton::ToolMenuButton( const std::string& label, const bool expand, const bool show_arrow, const int id ) : ToolMenuButton( Gtk::manage( new SKELETON::MenuButton( show_arrow, id ) ), label, expand ) { } ToolMenuButton::~ToolMenuButton() noexcept = default; ToolMenuButton::ToolMenuButton( SKELETON::MenuButton* button, const std::string& label, const bool expand ) : m_button{ button } { assert( m_button != nullptr ); Gtk::Widget* label_widget = m_button->get_label_widget(); assert( label_widget ); Gtk::MenuItem* item = nullptr; // アイコンの場合はアイコン表示 Gtk::Image* image = dynamic_cast< Gtk::Image* >( label_widget ); if( image ){ const Gtk::ImageType type = image->get_storage_type(); if( type == Gtk::IMAGE_STOCK ) { Gtk::StockID id; Gtk::IconSize size; image->get_stock( id, size ); item = Gtk::manage( new Gtk::ImageMenuItem( *Gtk::manage( new Gtk::Image( id, size ) ), label ) ); } else if( type == Gtk::IMAGE_PIXBUF ) { auto pixbuf = image->get_pixbuf(); item = Gtk::manage( new Gtk::ImageMenuItem( *Gtk::manage( new Gtk::Image( pixbuf ) ), label ) ); } } if( !item ) { item = Gtk::manage( new Gtk::MenuItem( label ) ); } if( item ){ item->signal_activate().connect( sigc::mem_fun( *m_button, &MenuButton::on_clicked ) ); set_proxy_menu_item( label, *item ); } set_expand( expand ); add( *m_button ); } /////////////////////////// ToolBackForwardButton::ToolBackForwardButton( const std::string& label, const bool expand, const std::string& url, const bool back ) : ToolMenuButton( Gtk::manage( new SKELETON::BackForwardButton( url, back ) ), label, expand ) { } SKELETON::BackForwardButton* ToolBackForwardButton::get_backforward_button() { return dynamic_cast< SKELETON::BackForwardButton* >( get_button() ); } jdim-0.10.1/src/skeleton/toolmenubutton.h000066400000000000000000000026751445721505100204140ustar00rootroot00000000000000// ライセンス: GPL2 // ツールバーに表示する SKELETON::MenuButton、および SKELETON::BackForwardButton // メニューがオーバーフローしたときにアイコン、または label を表示して // 選択したら MenuButton::on_clicked() を呼び出す #ifndef _TOOLMENUBUTTON_H #define _TOOLMENUBUTTON_H #include namespace SKELETON { class MenuButton; class BackForwardButton; class ToolMenuButton : public Gtk::ToolItem { SKELETON::MenuButton* m_button{}; public: ToolMenuButton(); ToolMenuButton( const std::string& label, const bool expand, const bool show_arrow, Gtk::Widget& widget ); ToolMenuButton( const std::string& label, const bool expand, const bool show_arrow , const int id ); ~ToolMenuButton() noexcept; SKELETON::MenuButton* get_button(){ return m_button; } protected: ToolMenuButton( SKELETON::MenuButton* button, const std::string& label, const bool expand ); }; ////////////////////////// class ToolBackForwardButton : public SKELETON::ToolMenuButton { public: ToolBackForwardButton( const std::string& label, const bool expand, const std::string& url, const bool back ); SKELETON::BackForwardButton* get_backforward_button(); }; } #endif jdim-0.10.1/src/skeleton/treeviewbase.cpp000066400000000000000000000242521445721505100203310ustar00rootroot00000000000000// ライセンス: GPL2 //#define _DEBUG #include "jddebug.h" #include "treeviewbase.h" #include "jdlib/miscgtk.h" #include using namespace SKELETON; JDTreeViewBase::JDTreeViewBase() { add_events( Gdk::KEY_PRESS_MASK ); add_events( Gdk::KEY_RELEASE_MASK ); add_events( Gdk::SCROLL_MASK ); add_events( Gdk::BUTTON_PRESS_MASK ); add_events( Gdk::POINTER_MOTION_MASK ); } JDTreeViewBase::~JDTreeViewBase() noexcept = default; // // 行数 // int JDTreeViewBase::get_row_size() const { if( ! get_model() ) return 0; return get_model()->children().size(); } // // 現在フォーカスしてる行の最初のパスを取得 // Gtk::TreeModel::Path JDTreeViewBase::get_current_path() const { Gtk::TreeModel::Path path; std::vector< Gtk::TreeModel::Path > paths = get_selection()->get_selected_rows(); if( paths.size() ){ std::vector< Gtk::TreeModel::Path >::iterator it = paths.begin(); path = ( *it ); } return path; } // 現在フォーカスしてる行の最初のrowを取得 Gtk::TreeModel::Row JDTreeViewBase::get_current_row() { Gtk::TreePath path = get_current_path(); return get_row( path ); } // // x, y 座標の下のパスを取得 // Gtk::TreeModel::Path JDTreeViewBase::get_path_under_xy( int x, int y ) const { Gtk::TreeModel::Path path; Gtk::TreeViewColumn* column; int cell_x, cell_y; if( !get_path_at_pos( x, y, path, column, cell_x, cell_y ) ) return Gtk::TreeModel::Path(); return path; } // // 現在のマウスポインタの下のパスを取得 // Gtk::TreeModel::Path JDTreeViewBase::get_path_under_mouse() const { int x, y; MISC::get_pointer_at_window( get_window(), x, y ); return get_path_under_xy( x, y ); } // // 現在のマウスポインタの下のセルの幅高さとセル内での座標を取得 // void JDTreeViewBase::get_cell_xy_wh( int& cell_x, int& cell_y, int& cell_w, int& cell_h ) const { cell_x = cell_y = cell_w = cell_h = -1; Gtk::TreeModel::Path path; Gtk::TreeViewColumn* column; int x, y; Gdk::Rectangle rect; MISC::get_pointer_at_window( get_window(), x, y ); get_path_at_pos( x, y, path, column, cell_x, cell_y ); if( column ) { int o_x, o_y; column->cell_get_size( rect, o_x, o_y, cell_w, cell_h ); } } // // 選択中の Gtk::TreeModel::iterator のリストを取得 // // 削除などを実行してから get_model()->get_iter() するとパスが変わってエラーが出るので // 先に iterator だけ取得しておく // std::list< Gtk::TreeModel::iterator > JDTreeViewBase::get_selected_iterators() { std::list< Gtk::TreeModel::iterator > list_it; if( get_model() ){ std::vector< Gtk::TreeModel::Path > paths = get_selection()->get_selected_rows(); std::transform( paths.begin(), paths.end(), std::back_inserter( list_it ), [this]( Gtk::TreeModel::Path& p ) { return get_model()->get_iter( p ); } ); } return list_it; } // // 選択行の削除 // void JDTreeViewBase::delete_selected_rows( const bool force ) { std::vector< Gtk::TreeModel::Path > list_path = get_selection()->get_selected_rows(); if( ! list_path.size() ) return; Glib::RefPtr< Gtk::ListStore > liststore; Glib::RefPtr< Gtk::TreeStore > treestore = Glib::RefPtr< Gtk::TreeStore >::cast_dynamic( get_model() ); if( ! treestore ){ liststore = Glib::RefPtr< Gtk::ListStore >::cast_dynamic( get_model() ); if( ! liststore ) return; } // カーソルを選択範囲の最後の行の次の行に移動 const Gtk::TreePath next = next_path( list_path.back(), true ); const bool gotobottom = ( ! get_row( next ) ); if( ! gotobottom ) set_cursor( next ); for( const Gtk::TreePath& path : list_path ) { Gtk::TreeRow row = get_row( path ); if( treestore ) treestore->erase( row ); else liststore->erase( row ); } if( gotobottom ) goto_bottom(); } // // 先頭へ // void JDTreeViewBase::goto_top() { if( ! get_row_size() ) return; Gtk::TreePath path = get_model()->get_path( *( get_model()->children().begin() ) ); scroll_to_row( path, 0 ); set_cursor( path ); } // // 一番最後へ // void JDTreeViewBase::goto_bottom() { if( ! get_row_size() ) return; Gtk::TreePath path = get_model()->get_path( *std::prev( get_model()->children().end() ) ); scroll_to_row( path, 0 ); set_cursor( path ); } // // 選択行を上へ移動 // bool JDTreeViewBase::row_up() { Gtk::TreePath path = get_current_path(); if( !get_row( path ) ) return false; Gtk::TreePath new_path = prev_path( path ); if( path != new_path ) set_cursor( new_path ); else return false; return true; } // // 選択行を下へ移動 // bool JDTreeViewBase::row_down() { Gtk::TreePath path = get_current_path(); if( !get_row( path ) ) return false; Gtk::TreePath new_path = next_path( path ); if( new_path.size() && get_row( new_path ) ) set_cursor( new_path ); else return false; return true; } // // page up // void JDTreeViewBase::page_up() { bool set_top = false; // スクロール auto adj = get_vadjustment(); double val = adj->get_value(); if( val > adj->get_page_size()/2 ) set_top = true; val = MAX( 0, val - adj->get_page_size() ); adj->set_value( val ); // 選択行移動 Gtk::TreePath path; if( set_top ) path = get_path_under_xy( 0, (int)adj->get_page_size() - 4 ); else path = get_path_under_xy( 0, 0 ); if( path.size() && get_row( path ) )set_cursor( path ); } // // page down // void JDTreeViewBase::page_down() { bool set_bottom = false; // スクロール auto adj = get_vadjustment(); double val = adj->get_value(); if( val < adj->get_upper() - adj->get_page_size() - adj->get_page_size()/2 ) set_bottom = true; val = MIN( adj->get_upper() - adj->get_page_size(), val + adj->get_page_size() ); adj->set_value( val ); // 選択行移動 Gtk::TreePath path; if( set_bottom ) path = get_path_under_xy( 0, 0 ); else path = get_path_under_xy( 0, (int)adj->get_page_size() - 4 ); if( path.size() && get_row( path ) ) set_cursor( path ); } // // path の前の path を取得 // // check_expand = true なら行が開いてるかチェックして開いて無い時はdown()しない // Gtk::TreePath JDTreeViewBase::prev_path( const Gtk::TreePath& path, bool check_expand ) { Gtk::TreePath path_out( path ); // 前に移動 if( path_out.prev() && ( row_expanded( path_out ) || ! check_expand ) ){ Gtk::TreePath path_tmp = path_out; while( get_row( path_out ) && ( path_out = next_path( path_out, check_expand ) ) != path ) path_tmp = path_out; if( get_row( path_tmp ) ) return path_tmp; } // 一番上まで到達したらup path_out = path; if( ! path_out.prev() && path_out.size() >= 2 ) path_out.up(); return path_out;; } // // path の次の path を取得 // // check_expand = true なら行が開いてるかチェックして開いて無い時はdown()しない // Gtk::TreePath JDTreeViewBase::next_path( const Gtk::TreePath& path, bool check_expand ) { if( !get_row( path ) ) return path; Gtk::TreePath path_out( path ); if( row_expanded( path_out ) || ! check_expand ){ path_out.down(); if( get_row( path_out ) ) return path_out; } // next()してレベルの一番下まで到達したら上のレベルに移動 path_out = path; while( path_out.next(), ( ! get_row( path_out ) && path_out.size() >=2 ) ) path_out.up(); return path_out; } // // path->row 変換 // Gtk::TreeModel::Row JDTreeViewBase::get_row( const Gtk::TreePath& path ) { if( path.empty() || ! get_model() ) return Gtk::TreeModel::Row(); Gtk::TreeModel::Row row = *( get_model()->get_iter( path ) ); if( !row ) return Gtk::TreeModel::Row(); if( path != get_model()->get_path( row ) ) return Gtk::TreeModel::Row(); return row; } // // pathの親を再起的に開く // void JDTreeViewBase::expand_parents( const Gtk::TreePath& path ) { if( ! get_model() ) return; for( Gtk::TreePath::size_type level = 1; level < path.size(); ++level ){ Gtk::TreeModel::Row row_tmp = get_row( path ); if( ! row_tmp ) return; for( Gtk::TreePath::size_type i = 0; i < path.size() - level; ++i ){ if( row_tmp.parent() ) row_tmp = *( row_tmp.parent() ); } Gtk::TreePath path_tmp = get_model()->get_path( row_tmp ); expand_row( path_tmp, false ); } } // // pathが開かれているか // bool JDTreeViewBase::is_expand( const Gtk::TreePath& path ) { Gtk::TreePath parent( path ); if( path.size() < 2 ) return true; if( parent.up() && row_expanded( parent ) ) return true; return false; } // // 行のセルの高さ // int JDTreeViewBase::get_row_height() const { const Gtk::TreeViewColumn* column = get_column( m_column_for_height ); if( !column ) return 0; int x,y,w,h; Gdk::Rectangle rect; column->cell_get_size( rect, x, y, w, h ); return h; } // // キーボードのキーを押した // bool JDTreeViewBase::on_key_press_event( GdkEventKey* event ) { return m_sig_key_press.emit( event ); } // // キーボードのキーを離した // bool JDTreeViewBase::on_key_release_event( GdkEventKey* event ) { m_sig_key_release.emit( event ); return true; } // マウスのwheelを回した bool JDTreeViewBase::on_scroll_event( GdkEventScroll* event ) { m_sig_scroll_event.emit( event ); return Gtk::TreeView::on_scroll_event( event ); } // // マウスボタンを押した // bool JDTreeViewBase::on_button_press_event( GdkEventButton* event ) { m_sig_button_press.emit( event ); return Gtk::TreeView::on_button_press_event( event ); } // // マウスボタンを離した // bool JDTreeViewBase::on_button_release_event( GdkEventButton* event ) { m_sig_button_release.emit( event ); return Gtk::TreeView::on_button_release_event( event ); } // // マウスを動かした // bool JDTreeViewBase::on_motion_notify_event( GdkEventMotion* event ) { m_sig_motion_notify.emit( event ); return Gtk::TreeView::on_motion_notify_event( event ); } jdim-0.10.1/src/skeleton/treeviewbase.h000066400000000000000000000075341445721505100200020ustar00rootroot00000000000000// ライセンス: GPL2 // // treeviewクラスの基底クラス // #ifndef _TREEVIEWBASE_H #define _TREEVIEWBASE_H #include #include namespace SKELETON { class JDTreeViewBase : public Gtk::TreeView { typedef sigc::signal< bool, GdkEventKey* > SIG_KEY_PRESS; typedef sigc::signal< bool, GdkEventKey* > SIG_KEY_RELEASE; typedef sigc::signal< bool, GdkEventScroll* > SIG_SCROLL_EVENT; typedef sigc::signal< bool, GdkEventButton* > SIG_BUTTON_PRESS; typedef sigc::signal< bool, GdkEventButton* > SIG_BUTTON_RELEASE; typedef sigc::signal< bool, GdkEventMotion* > SIG_MOTION_NOTIFY; SIG_KEY_PRESS m_sig_key_press; SIG_KEY_RELEASE m_sig_key_release; SIG_SCROLL_EVENT m_sig_scroll_event; SIG_BUTTON_PRESS m_sig_button_press; SIG_BUTTON_RELEASE m_sig_button_release; SIG_MOTION_NOTIFY m_sig_motion_notify; // get_row_height() で高さを取得するためのcolumn番号 int m_column_for_height{}; public: SIG_KEY_PRESS& sig_key_press() { return m_sig_key_press; } SIG_KEY_RELEASE& sig_key_release() { return m_sig_key_release; } SIG_SCROLL_EVENT& sig_scroll_event(){ return m_sig_scroll_event; } SIG_BUTTON_PRESS& sig_button_press() { return m_sig_button_press; } SIG_BUTTON_RELEASE& sig_button_release() { return m_sig_button_release; } SIG_MOTION_NOTIFY& sig_motion_notify() { return m_sig_motion_notify; } JDTreeViewBase(); ~JDTreeViewBase() noexcept; // 行数 int get_row_size() const; // カーソル解除 void unset_cursor(){ get_selection()->unselect_all(); } // 現在フォーカスしてる行の最初のパスを取得 Gtk::TreeModel::Path get_current_path() const; // 現在フォーカスしてる行の最初のrowを取得 Gtk::TreeModel::Row get_current_row(); //x, y 座標の下のパスを取得 Gtk::TreeModel::Path get_path_under_xy( int x, int y ) const; // 現在のマウスポインタの下のパスを取得 Gtk::TreeModel::Path get_path_under_mouse() const; // 現在のマウスポインタの下のセルの幅高さとセル内での座標を取得 void get_cell_xy_wh( int& cell_x, int& cell_y, int& cell_w, int& cell_h ) const; // 選択中の Gtk::TreeModel::iterator のリストを取得 std::list< Gtk::TreeModel::iterator > get_selected_iterators(); // 選択行の削除 virtual void delete_selected_rows( const bool force ); // 選択行の移動 void goto_top(); void goto_bottom(); bool row_up(); bool row_down(); void page_up(); void page_down(); // path の前後のpathを取得 Gtk::TreePath prev_path( const Gtk::TreePath& path, bool check_expand = true ); Gtk::TreePath next_path( const Gtk::TreePath& path, bool check_expand = true ); // path -> row 変換 Gtk::TreeModel::Row get_row( const Gtk::TreePath& path ); // pathの親を再起的にexpandする void expand_parents( const Gtk::TreePath& path ); // pathが開かれているか bool is_expand( const Gtk::TreePath& path ); // 行のセルの高さ int get_row_height() const; void set_column_for_height( int column ){ m_column_for_height = column; } protected: bool on_key_press_event( GdkEventKey* event ) override; bool on_key_release_event( GdkEventKey* event ) override; bool on_scroll_event( GdkEventScroll* event ) override; bool on_button_press_event( GdkEventButton* event ) override; bool on_button_release_event( GdkEventButton* event ) override; bool on_motion_notify_event( GdkEventMotion* event ) override; }; } #endif jdim-0.10.1/src/skeleton/undobuffer.cpp000066400000000000000000000054611445721505100200040ustar00rootroot00000000000000// ライセンス: GPL2 //#define _DEBUG #include "jddebug.h" #include "undobuffer.h" using namespace SKELETON; UNDO_BUFFER::UNDO_BUFFER() : m_first{ true } { m_vec_undo.push_back( UNDO_DATA() ); } void UNDO_BUFFER::set_item( UNDO_ITEM& item ) { #ifdef _DEBUG std::cout << "UNDO_BUFFER::set_item\n"; #endif if( m_first ){ #ifdef _DEBUG std::cout << "reset_data\n"; #endif get_undo_data().clear(); m_first = false; } get_undo_data().push_back( item ); #ifdef _DEBUG std::cout << "size = " << get_undo_data().size() << std::endl; #endif } void UNDO_BUFFER::undo() { if( get_enable_undo() ){ #ifdef _DEBUG std::cout << "UNDO_BUFFER::undo pos = " << m_pos << " max = " << m_max << std::endl; #endif --m_pos; m_first = true; m_sig_undo.emit(); } } void UNDO_BUFFER::redo() { if( get_enable_redo() ){ #ifdef _DEBUG std::cout << "SKELETON::UNDO_BUFFER::redo pos = " << m_pos << " max = " << m_max << std::endl; #endif ++m_pos; m_first = true; m_sig_redo.emit(); } } // // 選択中の行 // void UNDO_BUFFER::set_list_info_selected( const CORE::DATA_INFO_LIST& list_info_selected ) { #ifdef _DEBUG std::cout << "UNDO_BUFFER::set_list_info_selected\n"; #endif UNDO_ITEM item; item.list_info_selected = list_info_selected; set_item( item ); } // // 削除や追加した行 // void UNDO_BUFFER::set_list_info( const CORE::DATA_INFO_LIST& list_info_append, const CORE::DATA_INFO_LIST& list_info_delete ) { #ifdef _DEBUG std::cout << "UNDO_BUFFER::set_list_info pos = " << m_pos << " max = " << m_max << std::endl; #endif UNDO_ITEM item; item.list_info_append = list_info_append; item.list_info_delete = list_info_delete; set_item( item ); } // // 名前を変更した行 // void UNDO_BUFFER::set_name( const Gtk::TreePath& path_renamed, const Glib::ustring& name_new, const Glib::ustring& name_before ) { #ifdef _DEBUG std::cout << "UNDO_BUFFER::set_name pos = " << m_pos << " max = " << m_max << std::endl << "path = " << path_renamed.to_string() << " new = " << name_new << " before = " << name_before << std::endl; #endif UNDO_ITEM item; item.path_renamed = path_renamed; item.name_new = name_new; item.name_before = name_before; set_item( item ); } // // set_* でデータをセットしたら最後にcommitする // void UNDO_BUFFER::commit() { if( m_first ) return; #ifdef _DEBUG std::cout << "UNDO_BUFFER::commit pos = " << m_pos << " max = " << m_pos << std::endl; #endif ++m_pos; m_max = m_pos; if( (int)m_vec_undo.size() == m_pos ) m_vec_undo.push_back( UNDO_DATA() ); m_first = true; m_sig_commit.emit(); #ifdef _DEBUG std::cout << "-> pos = max = " << m_pos << std::endl; #endif } jdim-0.10.1/src/skeleton/undobuffer.h000066400000000000000000000043241445721505100174460ustar00rootroot00000000000000// ライセンス: GPL2 // UNDO用バッファ #ifndef SKELETON_UNDOBUFFER_H #define SKELETON_UNDOBUFFER_H #include "sharedbuffer.h" #include namespace SKELETON { class UNDO_ITEM { public: CORE::DATA_INFO_LIST list_info_selected; CORE::DATA_INFO_LIST list_info_append; CORE::DATA_INFO_LIST list_info_delete; Gtk::TreePath path_renamed; Glib::ustring name_new; Glib::ustring name_before; UNDO_ITEM() { list_info_selected.clear(); list_info_append.clear(); list_info_delete.clear(); path_renamed = Gtk::TreePath(); name_new = std::string(); name_before = std::string(); } }; typedef std::vector< UNDO_ITEM > UNDO_DATA; typedef sigc::signal< void > SIG_UNDO; typedef sigc::signal< void > SIG_REDO; typedef sigc::signal< void > SIG_COMMIT; class UNDO_BUFFER { std::vector< UNDO_DATA > m_vec_undo; int m_pos{}; int m_max{}; bool m_first; SIG_UNDO m_sig_undo; SIG_REDO m_sig_redo; SIG_COMMIT m_sig_commit; public: UNDO_BUFFER(); virtual ~UNDO_BUFFER() noexcept = default; SIG_UNDO sig_undo(){ return m_sig_undo; } SIG_REDO sig_redo(){ return m_sig_redo; } SIG_COMMIT sig_commit(){ return m_sig_commit; } UNDO_DATA& get_undo_data(){ return m_vec_undo[ m_pos ]; } bool get_enable_undo() const { return ( m_pos ); } bool get_enable_redo() const { return ( m_pos < m_max ); } void undo(); void redo(); // 選択中の行 void set_list_info_selected( const CORE::DATA_INFO_LIST& list_info_selected ); // 削除や追加した行 void set_list_info( const CORE::DATA_INFO_LIST& list_info_append, const CORE::DATA_INFO_LIST& list_info_delete ); // 名前を変更した行 void set_name( const Gtk::TreePath& path_renamed, const Glib::ustring& name_new, const Glib::ustring& name_before ); // set_* でデータをセットしたら最後にcommitする void commit(); private: void set_item( UNDO_ITEM& item ); }; } #endif jdim-0.10.1/src/skeleton/vbox.cpp000066400000000000000000000011241445721505100166130ustar00rootroot00000000000000// ライセンス: GPL2 //#define _DEBUG #include "jddebug.h" #include "vbox.h" using namespace SKELETON; JDVBox::~JDVBox() noexcept = default; // unpack = true の時取り除く void JDVBox::pack_remove_start( bool unpack, Widget& child, Gtk::PackOptions options, guint padding ) { if( unpack ) remove( child ); else pack_start( child, options, padding ); } // unpack = true の時取り除く void JDVBox::pack_remove_end( bool unpack, Widget& child, Gtk::PackOptions options, guint padding ) { if( unpack ) remove( child ); else pack_end( child, options, padding ); } jdim-0.10.1/src/skeleton/vbox.h000066400000000000000000000010651445721505100162640ustar00rootroot00000000000000// ライセンス: GPL2 // // VBoxクラス // #ifndef _VBOX_H #define _VBOX_H #include namespace SKELETON { class JDVBox : public Gtk::VBox { public: using Gtk::VBox::VBox; ~JDVBox() noexcept; // unpack = true の時取り除く void pack_remove_start( bool unpack, Widget& child, Gtk::PackOptions options = Gtk::PACK_EXPAND_WIDGET, guint padding = 0 ); void pack_remove_end( bool unpack, Widget& child, Gtk::PackOptions options = Gtk::PACK_EXPAND_WIDGET, guint padding = 0 ); }; } #endif jdim-0.10.1/src/skeleton/view.cpp000066400000000000000000000150311445721505100166110ustar00rootroot00000000000000// ライセンス: GPL2 //#define _DEBUG #include "jddebug.h" #include "admin.h" #include "view.h" #include "config/globalconf.h" #include "history/historymanager.h" #include "jdlib/miscgtk.h" #include "global.h" #include "session.h" #include "command.h" using namespace SKELETON; View::View( const std::string& url, const std::string& arg1 ,const std::string& arg2 ) : m_url( url ) , m_autoreload_mode( AUTORELOAD_NOT ) , m_lockable( true ) , m_writeable( true ) , m_id_toolbar( 0 ) // 派生クラスでコンテキストが異なる {} const std::string& View::get_url_admin() { return get_admin()->get_url(); } // // url_new に URL を変更 // void View::set_url( const std::string& url_new ) { if( ! m_url.empty() && ! url_new.empty() && m_url != url_new ){ // View履歴のURLを更新 HISTORY::get_history_manager()->replace_url_viewhistory( m_url, url_new ); // ツールバーのURLを更新 get_admin()->set_command( "update_toolbar_url", m_url, url_new ); m_url = url_new; } } // // url 更新 // // 移転があったときなどにadminから呼び出される // void View::update_url( const std::string& url_old, const std::string& url_new ) { if( m_url.rfind( url_old, 0 ) != 0 ) return; std::string url = url_new + m_url.substr( url_old.length() ); #ifdef _DEBUG std::cout << "View::update_url\n"; std::cout << m_url << " -> " << url << std::endl; #endif set_url( url ); } // クロック入力 // clock_in_always()はviewの種類に依らず常に呼び出されるので重い処理を含めてはいけない void View::clock_in_always() { // タブ単位でのオートリロードモード if( m_autoreload_mode == AUTORELOAD_ONCE && inc_autoreload_counter() ) reload(); // キーボード数字入力ジャンプ if( inc_keyjump_counter() ){ goto_num( m_keyjump_num, 0 ); reset_keyjump_counter(); } } // オートリロードのカウンタをインクリメント // 指定秒数を越えたら true を返す bool View::inc_autoreload_counter() { if( m_autoreload_mode == AUTORELOAD_NOT ) return false; ++m_autoreload_counter; if( m_autoreload_counter > m_autoreload_sec * 1000/TIMER_TIMEOUT ){ reset_autoreload_counter(); return true; } return false; } // オートリロードのモードを設定 void View::set_autoreload_mode( int mode, int sec ) { if( ! m_enable_autoreload ) return; if( m_autoreload_mode != AUTORELOAD_NOT && mode != AUTORELOAD_NOT ) return; m_autoreload_mode = mode; m_autoreload_sec = sec; m_autoreload_counter = 0; } // オートリロードのカウンタをリセット void View::reset_autoreload_counter() { m_autoreload_counter = 0; // オートリロードのモードがAUTORELOAD_ONCEの時はオートリロード停止 if( m_autoreload_mode == AUTORELOAD_ONCE ) m_autoreload_mode = AUTORELOAD_NOT; } // 数字入力ジャンプカウンタのインクリメント // 指定秒数を越えたら true を返す bool View::inc_keyjump_counter() { if( ! m_keyjump_counter ) return false; ++m_keyjump_counter; if( m_keyjump_counter > CONFIG::get_numberjmp_msec() / TIMER_TIMEOUT ) return true; return false; } // 数字入力ジャンプカウンタのリセット void View::reset_keyjump_counter() { m_keyjump_counter = 0; m_keyjump_num = 0; } // 数字入力ジャンプ用に sig_key_press() から呼び出す bool View::release_keyjump_key( int key ) { // キーパッド対応 if( key >= GDK_KEY_KP_0 && key <= GDK_KEY_KP_9 ) key = key - GDK_KEY_KP_0 + GDK_KEY_0; if( key >= GDK_KEY_0 && key <= GDK_KEY_9 ){ m_keyjump_counter = 1; m_keyjump_num *= 10; m_keyjump_num += key - '0'; CORE::core_set_command( "set_info", "", std::to_string( m_keyjump_num ) ); return true; } return false; } // view 上にマウスポインタがあれば true bool View::is_mouse_on_view() const { bool ret = false; int x,y; MISC::get_pointer_at_window( get_window(), x, y ); if( x < get_width() && x >= 0 && y < get_height() && y >= 0 ) ret = true; #ifdef _DEBUG std::cout << "View::is_mouse_on_view ret = " << ret << " x= " << x << " y= " << y << " w= " << get_width() << " h= " << get_height() << std::endl; #endif return ret; } // ポップアップメニュー表示 void View::show_popupmenu( const std::string& url, bool use_slot ) { // ポップアップメニューを表示する前にメニューのアクティブ状態を切り替える activate_act_before_popupmenu( url ); Gtk::Menu* popupmenu = get_popupmenu( url ); if( popupmenu ){ #ifdef _DEBUG std::cout << "View::show_popupmenu\n"; #endif const auto result = m_url_popup.insert( url ); if( result.second ) { popupmenu->signal_map().connect( sigc::mem_fun( *this, &View::slot_map_popupmenu ) ); popupmenu->signal_hide().connect( sigc::mem_fun( *this, &View::slot_hide_popupmenu ) ); } if( use_slot ) popupmenu->popup( sigc::mem_fun( *this, &View::slot_popup_menu_position ), 0, gtk_get_current_event_time() ); else popupmenu->popup( 0, gtk_get_current_event_time() ); } } // ポップアップメニュー表示時に表示位置を決めるスロット void View::slot_popup_menu_position( int& x, int& y, bool& push_in) { // viewの左上の座標をセットする int x2, y2; get_window()->get_position( x, y ); translate_coordinates( *dynamic_cast< Gtk::Widget* >( get_toplevel() ), 0, 0, x2, y2 ); x += x2; y += y2; push_in = false; } // // ポップアップメニューがmapしたときに呼ばれるslot // void View::slot_map_popupmenu() { #ifdef _DEBUG std::cout << "View::slot_map_popupmenu\n"; #endif SESSION::set_popupmenu_shown( true ); } // // ポップアップメニューがhideしたときに呼ばれるslot // void View::slot_hide_popupmenu() { #ifdef _DEBUG std::cout << "View::slot_hide_popupmenu\n"; #endif SESSION::set_popupmenu_shown( false ); // もしviewがポップアップウィンドウ上にあって、かつ // メニューを消したときにマウスポインタが領域外にあれば自分自身をhide if( ! is_mouse_on_view() ) sig_hide_popup().emit(); } // // ラベルやステータスバーの色 // std::string View::get_color() const { if( is_broken() ) return "red"; else if( is_old() ) return "blue"; else if( is_overflow() ) return "green"; return ""; } jdim-0.10.1/src/skeleton/view.h000066400000000000000000000316111445721505100162600ustar00rootroot00000000000000// ライセンス: GPL2 #ifndef _VIEW_H #define _VIEW_H #include #include #include #include "control/control.h" namespace SKELETON { class Admin; // 自分がポップアップviewの時に(ポップアップウィンドウ( SKELETON::PopupWin ) 経由で) // 親widgetにhideを依頼するシグナル。PopupWin::PopupWin()でPopupWin::slot_hide_popup()にコネクトされる。 typedef sigc::signal< void > SIG_HIDE_POPUP; // 自分がポップアップviewでリサイズしたときに、明示的にポップアップウィンドウ( SKELETON::PopupWin ) // にリサイズを依頼するシグナル。PopupWin::PopupWin()でPopupWin::slot_resize_popup()にコネクトされる。 typedef sigc::signal< void > SIG_RESIZE_POPUP; class View : public Gtk::VBox { SIG_HIDE_POPUP m_sig_hide_popup; SIG_RESIZE_POPUP m_sig_resize_popup; std::string m_url; Gtk::Window* m_parent_win{}; // クライアント領域の幅、高さ int m_width_client{}; int m_height_client{}; // 入力コントローラ CONTROL::Control m_control; // ポップアップメニュー Glib::RefPtr< Gtk::ActionGroup > m_action_group; Glib::RefPtr< Gtk::UIManager > m_ui_manager; std::set< std::string > m_url_popup; // ツールバーに表示する文字列 std::string m_label; /// ツールバーのツールチップに表示する文字列 std::string m_tooltip_label; /// ツールバーに表示する文字列にmarkupを使用するか bool m_label_use_markup{}; // メインウィンドウのタイトルに表示する文字 std::string m_title; // メインウィンドウのステータスバーに表示する文字 std::string m_status; // true ならマウスジェスチャ使用 bool m_enable_mg{}; // オートリロード bool m_enable_autoreload{}; // true ならオートリロード可能(デフォルト:off) int m_autoreload_mode; // モード int m_autoreload_sec{}; // 何秒おきにリロードするか int m_autoreload_counter{}; // オートリロード用のカウンタ // キーボード数字入力ジャンプ用 int m_keyjump_counter{}; int m_keyjump_num{}; // ロック可能か bool m_lockable; // ロック状態 bool m_locked{}; // 書き込み可能か bool m_writeable; // ツールバーのID (派生クラスでコンテキストが異なる) int m_id_toolbar; // 検索文字列 std::string m_search_query; // ポップアップ時に全ての領域を表示できないならカーソルの上に表示 bool m_popup_upside{}; // ロード時にキャッシュを削除してからviewを再読み込みする bool m_reget{}; protected: // url_new に URL を変更 void set_url( const std::string& url_new ); // Viewが所属するAdminクラス virtual Admin* get_admin() = 0; // UI Glib::RefPtr< Gtk::ActionGroup >& action_group(){ return m_action_group; } Glib::RefPtr< Gtk::UIManager >& ui_manager(){ return m_ui_manager; } // コントローラ CONTROL::Control& get_control(){ return m_control; } /// ツールバーに表示するラベルをセット void set_label( const std::string& label, const bool use_markup = false ) { m_label = label; m_label_use_markup = use_markup; } /// ツールバーに表示するラベルのツールチップをセット void set_tooltip_label( const std::string& label ) { m_tooltip_label = label; } // メインウィンドウのタイトルに表示する文字列 void set_title( const std::string& title ){ m_title = title; } // メインウィンドウのステータスバーに表示する文字 void set_status( const std::string& status ){ m_status = status; } // マウスジェスチャ void set_enable_mg( bool mg ){ m_enable_mg = mg; } bool enable_mg() const { return m_enable_mg; } // オートリロードのカウンタをインクリメント // 指定秒数を越えたら true を返す bool inc_autoreload_counter(); // オートリロード可能/不可能切替え void set_enable_autoreload( bool autoreload ){ m_enable_autoreload = autoreload; } // オートリロードのカウンタをリセット void reset_autoreload_counter(); // オートリロード間隔設定 int get_autoreload_sec() const { return m_autoreload_sec; } void set_autoreload_sec( const int sec ){ m_autoreload_sec = sec; } // オートリロード用のカウンタ int get_autoreload_counter() const { return m_autoreload_counter; } void set_autoreload_counter( const int counter ) { m_autoreload_counter = counter; } // 数字入力ジャンプカウンタのインクリメント // 指定秒数を越えたら true を返す bool inc_keyjump_counter(); // 数字入力ジャンプカウンタのリセット void reset_keyjump_counter(); // 数字入力ジャンプ用に sig_key_press() から呼び出す bool release_keyjump_key( int key ); // ポップアップメニュー表示 void show_popupmenu( const std::string& url, bool use_slot = false ); // ポップアップメニュー表示時に表示位置を決めるスロット void slot_popup_menu_position( int& x, int& y, bool& push_in ); // ポップアップメニューがmapした時に呼び出されるスロット void slot_map_popupmenu(); // ポップアップメニューがhideした時に呼び出されるスロット void slot_hide_popupmenu(); // ポップアップメニューを表示する前にメニューのアクティブ状態を切り替える virtual void activate_act_before_popupmenu( const std::string& url ){} // ポップアップメニュー取得 virtual Gtk::Menu* get_popupmenu( const std::string& url ){ return nullptr; } public: SIG_HIDE_POPUP sig_hide_popup(){ return m_sig_hide_popup; } SIG_RESIZE_POPUP sig_resize_popup(){ return m_sig_resize_popup; } explicit View( const std::string& url, const std::string& arg1 = {}, const std::string& arg2 = {} ); ~View() noexcept = default; virtual void save_session() = 0; virtual const std::string& get_url() const { return m_url; } const std::string& get_url_admin(); virtual void set_parent_win( Gtk::Window* parent_win ){ m_parent_win = parent_win; } virtual Gtk::Window* get_parent_win(){ return m_parent_win; } // 移転があったときなどにadminから呼び出される virtual void update_url( const std::string& url_old, const std::string& url_new ); // 検索文字列 const std::string& get_search_query() const { return m_search_query; } // ツールバーのID // タブの切り替えのときに Admin から参照される // コンストラクタであらかじめ指定しておくこと void set_id_toolbar( const int id ) { m_id_toolbar = id; } int get_id_toolbar() const { return m_id_toolbar; } // ロック/アンロック bool is_lockable() const { return m_lockable; } void set_lockable( const bool lockable ){ m_lockable = lockable; } bool is_locked() const { return m_locked; } virtual void lock(){ m_locked = true; } virtual void unlock(){ m_locked = false; } // 書き込み可能/不可能 bool is_writeable() const { return m_writeable; } void set_writeable( const bool writeable ){ m_writeable = writeable; } // ポップアップ時に全ての領域を表示できないならカーソルの上に表示 bool get_popup_upside() const { return m_popup_upside; } void set_popup_upside( const bool upside ){ m_popup_upside = upside; } // view 上にマウスポインタがあれば true bool is_mouse_on_view() const; // 各view個別のコマンド virtual bool set_command( const std::string& command, const std::string& arg1 = {}, const std::string& arg2 = {} ) { return true; } // コピー用のURL virtual std::string url_for_copy() const { return m_url; } // ツールバーのラベルに表示する文字列 const std::string& get_label() const { return m_label; } /// ツールバーのラベルのツールチップに表示する文字列 const std::string& get_tooltip_label() const { return m_tooltip_label; } /// ツールバーのラベルに表示する文字列にmarkupを使用するか bool get_label_use_markup() const { return m_label_use_markup; } // メインウィンドウのタイトルバーに表示する文字列 virtual const std::string& get_title() const { return m_title; } // メインウィンドウのステータスバーに表示する文字列 virtual const std::string& get_status() const { return m_status; } // クライアント領域の幅、高さ virtual int width_client() const { return m_width_client; } virtual int height_client() const { return m_height_client; } void set_width_client( int val ){ m_width_client = val; } void set_height_client( int val ){ m_height_client = val; } // オートリロード可能か bool get_enable_autoreload() const { return m_enable_autoreload; } // オートリロードのモード設定 void set_autoreload_mode( int mode, int sec ); // 現在のオートリロードのモード取得 int get_autoreload_mode() const { return m_autoreload_mode; } // ロード時にキャッシュを削除してからviewを再読み込みする bool get_reget() const{ return m_reget; } void set_reget( const bool reget ){ m_reget = reget; } // アイコンのID取得 virtual int get_icon( const std::string& iconname ) const { return -1; } // ロード中 virtual bool is_loading() const { return false; } // 更新した virtual bool is_updated() const { return false; } // 更新チェックして更新可能か virtual bool is_check_update() const { return false; } // 古いデータか virtual bool is_old() const { return false; } // 壊れているか virtual bool is_broken() const { return false; } // レス数が最大表示可能数以上か virtual bool is_overflow() const noexcept { return false; } // ラベルやステータスバーの色 std::string get_color() const; // キーを押した virtual bool slot_key_press( GdkEventKey* event ){ return false; } // クロック入力 // clock_in()はビューがアクティブのときに呼び出される // clock_in_always()はviewの種類に依らず常に呼び出されるので重い処理を含めてはいけない virtual void clock_in(){}; virtual void clock_in_always(); virtual void write(){} virtual void reload(){} virtual void stop(){} virtual void show_view(){} virtual void redraw_view(){} virtual void relayout( const bool completely = false ){} virtual void update_view(){} virtual void update_finish(){} virtual void focus_view(){} virtual void focus_out(){} virtual void close_view(){} virtual void delete_view(){} virtual void set_favorite(){} virtual void update_item( const std::string& url, const std::string& id ){} virtual bool operate_view( const int ) = 0; virtual void goto_top(){} virtual void goto_bottom(){} virtual void goto_num( const int num_to, const int num_from ){} virtual void scroll_up(){} virtual void scroll_down(){} virtual void scroll_left(){} virtual void scroll_right(){} virtual void show_preference(){} virtual void update_boardname(){} // 進む、戻る virtual void back_viewhistory( const int count ){} virtual void forward_viewhistory( const int count ){} // 検索 virtual void exec_search(){} virtual void up_search(){} virtual void down_search(){} virtual void operate_search( const std::string& controlid ){} virtual void set_search_query( const std::string& query ){ m_search_query = query; } }; } #endif jdim-0.10.1/src/skeleton/viewnote.cpp000066400000000000000000000005771445721505100175100ustar00rootroot00000000000000// ライセンス: GPL2 //#define _DEBUG #include "jddebug.h" #include "viewnote.h" #include "config/globalconf.h" using namespace SKELETON; ViewNotebook::ViewNotebook( DragableNoteBook* ) : Gtk::Notebook() { set_show_border( true ); set_show_tabs( false ); set_border_width( CONFIG::get_view_margin() ); } ViewNotebook::~ViewNotebook() noexcept = default; jdim-0.10.1/src/skeleton/viewnote.h000066400000000000000000000007041445721505100171450ustar00rootroot00000000000000// ライセンス: GPL2 // // DragableNoteBookを構成するview表示用の Notebook // // TODO: 使われなくなったコンストラクタの引数を整理する #ifndef _VIEWNOTE_H #define _VIEWNOTE_H #include namespace SKELETON { class DragableNoteBook; class ViewNotebook : public Gtk::Notebook { public: explicit ViewNotebook( DragableNoteBook* ); ~ViewNotebook() noexcept; }; } #endif jdim-0.10.1/src/skeleton/vpaned.cpp000066400000000000000000000022711445721505100171160ustar00rootroot00000000000000// ライセンス: GPL2 //#define _DEBUG #include "jddebug.h" #include "vpaned.h" using namespace SKELETON; JDVPaned::JDVPaned( const int fixmode ) : Gtk::Paned( Gtk::ORIENTATION_VERTICAL ) , m_pctrl( *this, fixmode ) {} JDVPaned::~JDVPaned() noexcept = default; void JDVPaned::on_realize() { Gtk::Paned::on_realize(); m_pctrl.update_position(); } bool JDVPaned::on_button_press_event( GdkEventButton* event ) { m_pctrl.button_press_event( event ); return Gtk::Paned::on_button_press_event( event ); } bool JDVPaned::on_button_release_event( GdkEventButton* event ) { m_pctrl.button_release_event( event ); return Gtk::Paned::on_button_release_event( event ); } bool JDVPaned::on_motion_notify_event( GdkEventMotion* event ) { m_pctrl.motion_notify_event( event ); return Gtk::Paned::on_motion_notify_event( event ); } bool JDVPaned::on_enter_notify_event( GdkEventCrossing* event ) { m_pctrl.enter_notify_event( event ); return Gtk::Paned::on_enter_notify_event( event ); } bool JDVPaned::on_leave_notify_event( GdkEventCrossing* event ) { m_pctrl.leave_notify_event( event ); return Gtk::Paned::on_leave_notify_event( event ); } jdim-0.10.1/src/skeleton/vpaned.h000066400000000000000000000014461445721505100165660ustar00rootroot00000000000000// ライセンス: GPL2 // // VPanedクラス // #ifndef _VPANED_H #define _VPANED_H #include #include "panecontrol.h" namespace SKELETON { class JDVPaned : public Gtk::Paned { VPaneControl m_pctrl; public: explicit JDVPaned( const int fixmode ); ~JDVPaned() noexcept; VPaneControl& get_ctrl(){ return m_pctrl; } protected: void on_realize() override; bool on_button_press_event( GdkEventButton* event ) override; bool on_button_release_event( GdkEventButton* event ) override; bool on_motion_notify_event( GdkEventMotion* event ) override; bool on_enter_notify_event( GdkEventCrossing* event ) override; bool on_leave_notify_event( GdkEventCrossing* event ) override; }; } #endif jdim-0.10.1/src/skeleton/window.cpp000066400000000000000000000530401445721505100171500ustar00rootroot00000000000000// ライセンス: GPL2 //#define _DEBUG #include "jddebug.h" #include "window.h" #include "config/globalconf.h" #include "environment.h" #include "global.h" #include "session.h" #include "dndmanager.h" #include "command.h" #include enum { MGINFO_CHARS = MAX_MG_LNG * 2 + 16, // マウスジェスチャ表示欄の文字数 FOCUSOUT_TIMEOUT = 250, // GNOME環境でフォーカスを外すまでの時間 ( msec ) FOCUS_TIME = 100, // JDWindowにフォーカスを移してウィンドウサイズを復元するまでの時間 ( msec ) UNGRAB_TIME = 100, // ブート直後にフォーカスをメインウィンドウに戻すまでの時間 ( msec ) JDWIN_FOLDSIZE = 10 // 折りたたみ時に指定するウィンドウ高さ }; // ウィンドウ状態 enum { JDWIN_INIT = 0, JDWIN_NORMAL, // 開いている JDWIN_FOLD, // 折り畳んでいる JDWIN_EXPANDING, // 展開中( clock_in() 参照 ) JDWIN_UNMAX, // 最大化 -> 通常 JDWIN_UNMAX_FOLD, // 最大化 -> 折り畳み JDWIN_HIDE, // hide 中 JDWIN_UNGRAB // ブート直後にungrabする( clock_in() 参照 ) }; ////////////////////////////////////////////// using namespace SKELETON; constexpr const char* JDWindow::s_css_stat_label; // メッセージウィンドウでは m_mginfo が不要なので need_mginfo = false になる JDWindow::JDWindow( const bool fold_when_focusout, const bool need_mginfo ) : Gtk::Window( Gtk::WINDOW_TOPLEVEL ) , m_fold_when_focusout( fold_when_focusout ) , m_boot( true ) , m_enable_fold( m_fold_when_focusout ) , m_mode( JDWIN_INIT ) { // ステータスバー m_label_stat.set_size_request( 0, -1 ); m_label_stat.set_xalign( 0 ); m_label_stat.set_selectable( true ); m_label_stat.set_single_line_mode( true ); m_label_stat.set_ellipsize( Pango::ELLIPSIZE_END ); m_label_stat_ebox.add( m_label_stat ); m_label_stat_ebox.set_visible_window( false ); m_statbar.pack_start( m_label_stat_ebox ); if( need_mginfo ){ m_mginfo_ebox.add( m_mginfo ); m_mginfo_ebox.set_visible_window( false ); m_statbar.pack_start( m_mginfo_ebox, Gtk::PACK_SHRINK ); } m_mginfo.set_width_chars( MGINFO_CHARS ); m_mginfo.set_xalign( 0 ); m_statbar.show_all_children(); add( m_vbox ); m_gtkwidget = GTK_WIDGET( gobj() ); m_gtkwindow = GTK_WINDOW( gobj() ); gpointer parent_class = g_type_class_peek_parent( G_OBJECT_GET_CLASS( gobj() ) ); m_grand_parent_class = g_type_class_peek_parent( parent_class ); auto context = m_label_stat.get_style_context(); context->add_class( s_css_stat_label ); context->add_provider( m_stat_provider, GTK_STYLE_PROVIDER_PRIORITY_APPLICATION ); if( need_mginfo ) { context = m_mginfo.get_style_context(); context->add_class( s_css_stat_label ); context->add_provider( m_stat_provider, GTK_STYLE_PROVIDER_PRIORITY_APPLICATION ); } try { m_stat_provider->load_from_data( ".red:not(:selected), .red:active:not(:selected) { color: white; background-color: red; }" ".green:not(:selected), .green:active:not(:selected) { color: white; background-color: green; }" ".blue:not(:selected), .blue:active:not(:selected) { color: white; background-color: blue; }" ); } catch( Gtk::CssProviderError& err ) { #ifdef _DEBUG std::cout << "ERROR:JDWindow::JDWindow css fail " << err.what() << std::endl; #endif } } // windowの初期設定(サイズ変更や移動など) void JDWindow::init_win() { // フォーカスアウトで折り畳む場合 if( m_fold_when_focusout ){ m_scrwin = std::make_unique(); m_vbox_view = std::make_unique(); m_scrwin->set_size_request( 0, 0 ); m_scrwin->set_policy( Gtk::POLICY_EXTERNAL, Gtk::POLICY_EXTERNAL ); m_scrwin->add( *m_vbox_view ); m_vbox.pack_remove_end( false, *m_scrwin, Gtk::PACK_EXPAND_WIDGET ); set_skip_taskbar_hint( true ); resize( get_width_win(), 1 ); move_win( get_x_win(), get_y_win() ); focus_out(); property_window_position().set_value( Gtk::WIN_POS_NONE ); set_transient( true ); Glib::signal_idle().connect( sigc::mem_fun( *this, &JDWindow::slot_idle ) ); } // 通常のウィンドウ else{ resize( get_width_win(), get_height_win() ); move_win( get_x_win(), get_y_win() ); if( is_maximized_win() ) maximize_win(); property_window_position().set_value( Gtk::WIN_POS_NONE ); set_shown_win( true ); Glib::signal_idle().connect( sigc::mem_fun( *this, &JDWindow::slot_idle ) ); } } bool JDWindow::slot_idle() { // ブート完了 if( m_boot ){ #ifdef _DEBUG std::cout << "----------------\nJDWinow::slot_idle boot end mode = " << m_mode << std::endl; #endif m_boot = false; move_win( get_x_win(), get_y_win() ); if( m_fold_when_focusout ){ // 遅延させて clock_in()の中でフォーカスをメインウィンドウに戻す if( m_mode == JDWIN_FOLD ){ m_mode = JDWIN_UNGRAB; m_counter = 0; } } CORE::core_set_command( "window_boot_fin" ); } return false; } // クロック入力 void JDWindow::clock_in() { // 折りたたみ処理 if( m_fold_when_focusout ){ // 遅延リサイズ( focus_in()にある説明を参照 ) if( m_mode == JDWIN_EXPANDING ){ constexpr int waitcount = FOCUS_TIME / TIMER_TIMEOUT; ++m_counter; if( m_counter > waitcount && ! ( m_counter % waitcount ) ){ if( get_height() < get_height_win() ) resize( get_width_win(), get_height_win() ); else{ #ifdef _DEBUG std::cout << "JDWindow::clock_in resize\n"; #endif m_mode = JDWIN_NORMAL; set_shown_win( true ); present(); } } } // ブート直後にフォーカスをメインウィンドウに戻す else if( m_mode == JDWIN_UNGRAB ){ ++m_counter; if( m_counter > UNGRAB_TIME / TIMER_TIMEOUT ){ #ifdef _DEBUG std::cout << "JDWindow::clock_in ungrab\n"; #endif // WMによってはフォーカスが外れない時があるのでlower()して // 無理矢理フォーカスを外す set_transient( false ); get_window()->lower(); set_transient( true ); CORE::core_set_command( "restore_focus", "", "present" ); m_mode = JDWIN_FOLD; } } // GNOME環境ではタスクトレイなどで切り替えたときに画像windowがフォーカスされてしまうので // メインウィンドウと画像ウィンドウが同時にフォーカスアウトしたら // 一時的に transient 指定を外す。メインウィンドウがフォーカスインしたときに // Admin::focus_out() で transient 指定を戻す using ENVIRONMENT::DesktopType; const DesktopType wm = ENVIRONMENT::get_wm(); if( ( wm == DesktopType::gnome || wm == DesktopType::mate || wm == DesktopType::cinnamon ) && ! SESSION::is_iconified_win_main() // メインウィンドウが最小化しているときに transient を外すとウィンドウが表示されなくなる && ! SESSION::is_focus_win_main() && ! is_focus_win() ){ constexpr int waitcount = FOCUSOUT_TIMEOUT / TIMER_TIMEOUT; if( m_count_focusout < waitcount ) ++m_count_focusout; if( m_count_focusout == waitcount ){ #ifdef _DEBUG std::cout << "JDWindow::clock_in focus timeout\n"; #endif set_transient( false ); ++m_count_focusout; } } else m_count_focusout = 0; } } void JDWindow::set_spacing( int space ) { m_vbox.set_spacing( space ); } void JDWindow::maximize_win() { set_maximized_win( true ); maximize(); } void JDWindow::unmaximize_win() { set_maximized_win( false ); unmaximize(); } void JDWindow::iconify_win() { set_iconified_win( true ); iconify(); } // // ウィンドウ移動 // void JDWindow::move_win( const int x, const int y ) { if( ! CONFIG::get_manage_winpos() ) return; #ifdef _DEBUG std::cout << "JDWindow::move_win " << "x = " << x << " y = " << y << std::endl; #endif move( x, y ); set_x_win( x ); set_y_win( y ); // compiz 環境などでは move() で指定した座標がズレるので補正する m_win_moved = true; } // // ウィンドウ座標取得 // void JDWindow::set_win_pos() { if( ! get_window() ) return; int x,y; get_window()->get_root_origin( x, y ); #ifdef _DEBUG std::cout << "JDWindow::set_win_pos " << "x = " << x << " / " << get_x_win() << ", y = " << y << " / " << get_y_win() << std::endl; #endif // compiz 環境などでは move() で指定した座標がズレるので補正する if( m_win_moved ){ if( x != get_x_win() || y != get_y_win() ){ // 補正量がmrgを越えたら補正を諦める const int mrg = 64; const int delta_x = x - get_x_win(); const int delta_y = y - get_y_win(); x = get_x_win(); y = get_y_win(); m_win_moved = false; if( abs( delta_x ) <= mrg && abs( delta_y ) <= mrg ){ move( get_x_win() - delta_x, get_y_win() - delta_y ); #ifdef _DEBUG std::cout << "!!! moved x = " << get_x_win() << " y = " << get_y_win() << " dx = " << delta_x << " dy = " << delta_y << std::endl; #endif } } } set_x_win( x ); set_y_win( y ); } // hide 中 bool JDWindow::is_hide() const { return ( m_mode == JDWIN_HIDE ); } void JDWindow::pack_remove_start( bool unpack, Widget& child, Gtk::PackOptions options, guint padding ) { if( m_fold_when_focusout ){ m_vbox_view->pack_remove_start( unpack, child, options, padding ); if( ! unpack ) m_vbox_view->show_all_children(); } else{ m_vbox.pack_remove_start( unpack, child, options, padding ); if( ! unpack ) m_vbox.show_all_children(); } } void JDWindow::pack_remove_end( bool unpack, Widget& child, Gtk::PackOptions options, guint padding ) { if( m_fold_when_focusout ){ m_vbox_view->pack_remove_end( unpack, child, options, padding ); if( ! unpack ) m_vbox_view->show_all_children(); } else{ m_vbox.pack_remove_end( unpack, child, options, padding ); if( ! unpack ) m_vbox.show_all_children(); } } // ステータスバー表示 void JDWindow::set_status( const std::string& stat ) { if( stat == m_status ) return; m_status = stat; m_label_stat.set_text( stat ); m_label_stat_ebox.set_tooltip_text( stat ); } // 一時的にステータスバーの表示を変える( マウスオーバーでのURL表示用 ) // // 恒久的に変えてしまうと、マウススオ-バー中に ArticleViewBase では // ないクラスから表示を変更された場合に本来の表示に戻せなくなる。 void JDWindow::set_status_temporary( const std::string& stat ) { if( stat == m_status ) return; m_label_stat.set_text( stat ); } // 一時的に変えたステータスバーの表示を戻す void JDWindow::restore_status() { m_label_stat.set_text( m_status ); } // マウスジェスチャ表示 void JDWindow::set_mginfo( const std::string& mginfo ) { if( m_mginfo.get_realized() ) { m_mginfo.set_text( mginfo ); } } // ステータスの色を変える void JDWindow::set_status_color( const std::string& color ) { #ifdef _DEBUG std::cout << "JDWindow::set_status_color " << color << std::endl; #endif auto context = m_label_stat.get_style_context(); context->remove_class( "red" ); context->remove_class( "green" ); context->remove_class( "blue" ); if( ! color.empty() ) context->add_class( color ); if( m_mginfo.get_realized() ) { context = m_mginfo.get_style_context(); context->remove_class( "red" ); context->remove_class( "green" ); context->remove_class( "blue" ); if( ! color.empty() ) context->add_class( color ); } } // メインウィンドウに対して transient 設定 void JDWindow::set_transient( bool set ) { if( m_fold_when_focusout ){ #ifdef _DEBUG std::cout << "JDWindow::set_transient set = " << set << " " << m_transient << std::endl; #endif if( set && ! m_transient && CORE::get_mainwindow() ){ set_transient_for( *CORE::get_mainwindow() ); m_transient = true; } // transientを外す else if( ! set && m_transient ){ unset_transient_for(); m_transient = false; } } } // // ダイアログ表示などでフォーカスが外れてもウインドウを畳まないようにする // void JDWindow::set_enable_fold( bool enable ) { if( m_fold_when_focusout && m_enable_fold != enable ){ #ifdef _DEBUG std::cout << "JDWindow::set_enable_fold " << enable << std::endl; #endif m_enable_fold = enable; // XFCE 環境の場合はここでpresent()しておかないとフォーカスが外れる if( m_mode == JDWIN_NORMAL && m_enable_fold ){ if( ENVIRONMENT::get_wm() == ENVIRONMENT::DesktopType::kde ) switch_admin(); else present(); } } } // フォーカスイン void JDWindow::focus_in() { // 折りたたみ処理 if( m_fold_when_focusout ){ // メインウィンドウが最小化しているときはメインウィンドウを開く if( SESSION::is_iconified_win_main() ){ m_mode = JDWIN_UNGRAB; m_counter = 0; return; } if( ! m_enable_fold ) return; show(); if( is_iconified_win() ) deiconify(); if( ! is_maximized_win() && get_window() ){ int x, y; get_window()->get_root_origin( x, y ); if( x != get_x_win() || y != get_y_win() ) move_win( get_x_win(), get_y_win() ); } // 開く // // GNOME環境では focus in 動作中に resize() が失敗する時が // あるので、遅延させて clock_in() の中でリサイズとpresentする if( ! is_maximized_win() && m_mode != JDWIN_EXPANDING ){ m_mode = JDWIN_EXPANDING; m_counter = 0; } #ifdef _DEBUG std::cout << "JDWindow::focus_in mode = " << m_mode << " maximized = " << is_maximized_win() << " iconified = " << is_iconified_win() << std::endl; #endif } else{ show(); if( is_iconified_win() ) deiconify(); present(); } } // フォーカスアウト void JDWindow::focus_out() { // 折りたたみ処理 if( m_fold_when_focusout && m_enable_fold ){ // ポップアップメニューを表示しているかD&D中はfocus_outしない if( SESSION::is_popupmenu_shown() ) return; if( CORE::DND_Now_dnd() ) return; // 最大化している時は通常状態に戻しておかないと表示されなくなる if( is_maximized_win() ) unmaximize(); // 折り畳み if( m_mode != JDWIN_FOLD ){ resize( get_width_win(), JDWIN_FOLDSIZE ); m_mode = JDWIN_FOLD; set_shown_win( false ); CORE::core_set_command( "restore_focus" ); } #ifdef _DEBUG std::cout << "JDWindow::focus_out mode = " << m_mode << " maximized = " << is_maximized_win() << " iconified = " << is_iconified_win() << std::endl; #endif } } // フォーカスインイベント bool JDWindow::on_focus_in_event( GdkEventFocus* event ) { set_focus_win( true ); if( ! m_boot ){ // 折りたたみ処理 if( m_fold_when_focusout ){ #ifdef _DEBUG std::cout << "JDWindow::on_focus_in_event\n"; #endif if( m_mode != JDWIN_UNGRAB ) switch_admin(); } } return Gtk::Window::on_focus_in_event( event ); } // フォーカスアウトイベント bool JDWindow::on_focus_out_event( GdkEventFocus* event ) { set_focus_win( false ); if( ! m_boot ){ // 折りたたみ処理 if( m_fold_when_focusout ){ #ifdef _DEBUG std::cout << "JDWindow::on_focus_out_event\n"; #endif focus_out(); } } return Gtk::Window::on_focus_out_event( event ); } // Xボタンを押した bool JDWindow::on_delete_event( GdkEventAny* event ) { #ifdef _DEBUG std::cout << "JDWindow::on_delete_event\n"; #endif // 折りたたみ処理 if( m_fold_when_focusout ){ if( is_maximized_win() ) unmaximize(); else{ // hideする前に座標保存 if( ! is_iconified_win() && get_window() ) set_win_pos(); hide(); m_mode = JDWIN_HIDE; set_shown_win( false ); } return true; } return Gtk::Window::on_delete_event( event ); } // 最大、最小化 bool JDWindow::on_window_state_event( GdkEventWindowState* event ) { const bool maximized = event->new_window_state & GDK_WINDOW_STATE_MAXIMIZED; const bool iconified = event->new_window_state & GDK_WINDOW_STATE_ICONIFIED; const bool fullscreen = event->new_window_state & GDK_WINDOW_STATE_FULLSCREEN; #ifdef _DEBUG std::cout << "JDWindow::on_window_state_event : " << "maximized = " << is_maximized_win() << " -> " << maximized << " / iconified = " << is_iconified_win() << " -> " << iconified << " / full = " << is_full_win() << " -> " << fullscreen << std::endl; #endif if( ! m_boot ){ // 通常 -> アイコン化 if( ! is_iconified_win() && iconified ) set_shown_win( false ); // アイコン -> 通常 else if( is_iconified_win() && ! iconified && m_mode != JDWIN_FOLD ) set_shown_win( true ); // 通常 -> 最大化 if( ! is_maximized_win() && maximized ){ m_mode = JDWIN_NORMAL; set_shown_win( true ); } // 最大 -> 折り畳み or 通常 else if( is_maximized_win() && ! maximized ){ // 最大 -> 折り畳み if( m_fold_when_focusout && m_mode == JDWIN_FOLD ) m_mode = JDWIN_UNMAX_FOLD; // 最大 -> 通常 else m_mode = JDWIN_UNMAX; } // 通常 -> 全画面 else if( ! is_full_win() && fullscreen ) set_full_win( true ); // 全画面 -> 通常 else if( is_full_win() && ! fullscreen ) set_full_win( false ); #ifdef _DEBUG std::cout << " mode = " << m_mode << std::endl; #endif } set_maximized_win( maximized ); set_iconified_win( iconified ); return Gtk::Window::on_window_state_event( event ); } // 移動、サイズ変更イベント bool JDWindow::on_configure_event( GdkEventConfigure* event ) { const int mrg = 16; const int width_new = event->width; const int height_new = event->height; int min_height = 0; if( m_scrwin ) min_height = m_vbox.get_height() - m_scrwin->get_height() + mrg; if( ! m_boot ){ #ifdef _DEBUG std::cout << "JDWindow::on_configure_event" << " boot = " << m_boot << " mode = " << m_mode << " x = " << event->x << " y = " << event->y << " w = " << width_new << " h = " << height_new << " min_height = " << min_height << std::endl; #endif // 最大 -> 通常に戻る時はリサイズをキャンセル if( m_mode == JDWIN_UNMAX ) m_mode = JDWIN_NORMAL; else if( m_mode == JDWIN_UNMAX_FOLD ) m_mode = JDWIN_FOLD; // 最大、最小化しているときは除く else if( ! is_maximized_win() && ! is_iconified_win() && ! is_full_win() ){ set_win_pos(); // サイズ変更 if( ( ! m_fold_when_focusout || m_mode == JDWIN_NORMAL || m_mode == JDWIN_FOLD ) && height_new > min_height ) { set_width_win( width_new ); set_height_win( height_new ); } } #ifdef _DEBUG std::cout << "configure fin --> mode = " << m_mode << " show = " << is_shown_win() << " maximized = " << is_maximized_win() << " iconified = " << is_iconified_win() << " x = " << get_x_win() << " y = " << get_y_win() << " w = " << get_width_win() << " height = " << get_height_win() << std::endl; #endif } return Gtk::Window::on_configure_event( event ); } // uimなど、漢字変換モードの途中でctrl+qを押すとキーアクセレータが // 優先されてJDが終了する問題があった。 // // gedit-window.c の gedit_window_key_press_event を見ると // gtk_window_propagate_key_event() を実行した後でキーアクセレータ // の処理をするようにしていたのでJDもそうした。 bool JDWindow::on_key_press_event( GdkEventKey* event ) { if( gtk_window_propagate_key_event( m_gtkwindow, event ) ) return true; if( gtk_window_activate_key( m_gtkwindow, event ) ) return true; #ifdef _DEBUG std::cout << "JDWindow::on_key_press_event key = " << event->keyval << std::endl; std::cout << m_grand_parent_class << " - " << m_gtkwidget << std::endl; #endif return GTK_WIDGET_CLASS( m_grand_parent_class )->key_press_event( m_gtkwidget, event ); } jdim-0.10.1/src/skeleton/window.h000066400000000000000000000107301445721505100166140ustar00rootroot00000000000000// ライセンス: GPL2 // // Window クラス // #ifndef _JDWINDOW_H #define _JDWINDOW_H #include "gtkmmversion.h" #include "vbox.h" #include #include namespace SKELETON { class JDWindow : public Gtk::Window { GtkWidget* m_gtkwidget{}; GtkWindow* m_gtkwindow{}; gpointer m_grand_parent_class{}; bool m_win_moved{}; // フォーカスアウト時の折りたたみ処理で用いるメンバ変数 bool m_fold_when_focusout; // フォーカスアウトしたときにウィンドウを畳むか bool m_boot; bool m_enable_fold; // 「一時的に」折りたたみ可能かどうか切り替える bool m_transient{}; int m_mode; int m_counter{}; int m_count_focusout{}; // フォーカス制御用カウンタ SKELETON::JDVBox m_vbox; std::unique_ptr m_scrwin; std::unique_ptr m_vbox_view; // ステータスバー std::string m_status; Gtk::HBox m_statbar; Gtk::Label m_label_stat; Gtk::EventBox m_label_stat_ebox; Gtk::EventBox m_mginfo_ebox; Gtk::Label m_mginfo; static constexpr const char* s_css_stat_label = u8"jd-stat-label"; Glib::RefPtr< Gtk::CssProvider > m_stat_provider = Gtk::CssProvider::create(); public: explicit JDWindow( const bool fold_when_focusout, const bool need_mginfo = true ); ~JDWindow() noexcept = default; Gtk::HBox& get_statbar(){ return m_statbar; } virtual void clock_in(); // 最大、最小化 void maximize_win(); void unmaximize_win(); void iconify_win(); void set_spacing( int space ); // hide 中 bool is_hide() const; // 起動中 bool is_booting() const { return m_boot; } void pack_remove_start( bool unpack, Widget& child, Gtk::PackOptions options = Gtk::PACK_EXPAND_WIDGET, guint padding = 0 ); void pack_remove_end( bool unpack, Widget& child, Gtk::PackOptions options = Gtk::PACK_EXPAND_WIDGET, guint padding = 0 ); void set_status( const std::string& stat ); void set_status_temporary( const std::string& stat ); void restore_status(); std::string get_status() const { return m_status; } void set_mginfo( const std::string& mginfo ); // ステータスの色を変える void set_status_color( const std::string& color ); // メインウィンドウに対して transient 設定 void set_transient( bool set ); // ダイアログ表示などでフォーカスが外れてもウインドウを畳まないようにする void set_enable_fold( bool enable ); virtual void focus_in(); virtual void focus_out(); protected: SKELETON::JDVBox& get_vbox(){ return m_vbox;} // windowの初期設定(サイズ変更や移動など) void init_win(); virtual void switch_admin(){} virtual int get_x_win() const = 0; virtual int get_y_win() const = 0; virtual void set_x_win( const int x ) = 0; virtual void set_y_win( const int y ) = 0; virtual int get_width_win() const = 0; virtual int get_height_win() const = 0; virtual void set_width_win( const int width ) = 0; virtual void set_height_win( const int height ) = 0; virtual bool is_focus_win() const = 0; virtual void set_focus_win( const bool set ) = 0; virtual bool is_maximized_win() const = 0; virtual void set_maximized_win( const bool set ) = 0; virtual bool is_iconified_win() const = 0; virtual void set_iconified_win( const bool set ) = 0; virtual bool is_full_win() const = 0; virtual void set_full_win( const bool set ) = 0; virtual bool is_shown_win() const = 0; virtual void set_shown_win( const bool set ) = 0; bool on_focus_in_event( GdkEventFocus* event ) override; bool on_focus_out_event( GdkEventFocus* event ) override; bool on_delete_event( GdkEventAny* event ) override; bool on_window_state_event( GdkEventWindowState* event ) override; bool on_configure_event( GdkEventConfigure* event ) override; bool on_key_press_event( GdkEventKey* event ) override; private: bool slot_idle(); void move_win( const int x, const int y ); void set_win_pos(); }; } #endif jdim-0.10.1/src/sound/000077500000000000000000000000001445721505100144375ustar00rootroot00000000000000jdim-0.10.1/src/sound/Makefile.am000066400000000000000000000003321445721505100164710ustar00rootroot00000000000000noinst_LIBRARIES = libsound.a libsound_a_SOURCES = \ playsound.cpp \ soundmanager.cpp noinst_HEADERS = \ playsound.h \ soundmanager.h AM_CXXFLAGS = @GTKMM_CFLAGS@ @ALSA_CFLAGS@ AM_CPPFLAGS = -I$(top_srcdir)/src jdim-0.10.1/src/sound/meson.build000066400000000000000000000003521445721505100166010ustar00rootroot00000000000000sources = [ 'playsound.cpp', 'soundmanager.cpp', ] deps = [ alsa_dep, config_h_dep, gtkmm_dep, ] sound_lib = static_library( 'sound', sources, dependencies : deps, include_directories : include_directories('..'), ) jdim-0.10.1/src/sound/playsound.cpp000066400000000000000000000145431445721505100171700ustar00rootroot00000000000000// ライセンス: GPL2 //#define _DEBUG #include "jddebug.h" #include "playsound.h" #ifdef USE_ALSA #include "jdlib/miscmsg.h" #include #include #include #include #include #include using namespace SOUND; #ifdef WORDS_BIGENDIAN namespace { std::uint16_t byteswap16( std::uint16_t us ) { return ((us & 0xFF00) >> 8) | ((us & 0x00FF) << 8); } std::uint32_t byteswap32( std::uint32_t ui ) { return ((ui & 0xFF00'0000) >> 24) | ((ui & 0x00FF'0000) >> 8) | ((ui & 0x0000'FF00) << 8) | ((ui & 0x0000'00FF) << 24); } } // namespace #endif // WORDS_BIGENDIAN Play_Sound::Play_Sound() { #ifdef _DEBUG std::cout << "Play_Sound::Play_Sound\n"; #endif } Play_Sound::~Play_Sound() { #ifdef _DEBUG std::cout << "Play_Sound::~Play_Sound\n"; #endif // デストラクタの中からdispatchを呼ぶと落ちるので dispatch不可にする set_dispatchable( false ); stop(); // m_thread.joinable() == true のときスレッドを破棄すると強制終了するため待機処理を入れる wait(); } void Play_Sound::stop() { if( ! is_playing() ) return; #ifdef _DEBUG std::cout << "Play_Sound::stop\n"; #endif m_stop = true; wait(); } void Play_Sound::wait() { if( m_thread.joinable() ) m_thread.join(); } // // wav 再生スレッド起動 // void Play_Sound::play( const std::string& wavfile ) { if( wavfile.empty() ) return; if( m_thread.joinable() ){ MISC::ERRMSG( "Play_Sound::play : thread has been running" ); return; } m_wavfile = wavfile; m_stop = false; try { m_thread = std::thread( &Play_Sound::play_wavfile, this ); m_playing = true; } catch( std::system_error& ) { MISC::ERRMSG( "Play_Sound::play : could not start thread" ); } } // // wav 再生 スレッド // void Play_Sound::play_wavfile() { if( m_wavfile.empty() ) return; #ifdef _DEBUG std::cout << "Play_Sound::play_wavfile file = " << m_wavfile << std::endl; #endif FILE* fin = nullptr; snd_pcm_t *handle = nullptr; try{ fin = fopen( m_wavfile.c_str(), "rb" ); if( ! fin ) throw "cannot open " + m_wavfile; // フォーマット取得 size_t filepos = 0; RIFFCHK riffchk; size_t chksize = 8; filepos += fread( &riffchk, 1, chksize, fin ); #ifdef WORDS_BIGENDIAN // RIFFはリトルエンディアンで保存されるためビッグエンディアンの環境ではバイトオーダーを逆にする riffchk.size = byteswap32( riffchk.size ); #endif if( strncmp( riffchk.id, "RIFF", 4 ) != 0 ) throw m_wavfile + " is not a wave file"; WAVEFMTCHK wavefmt; chksize = 28; filepos += fread( &wavefmt, 1, chksize, fin ); #ifdef WORDS_BIGENDIAN wavefmt.size = byteswap32( wavefmt.size ); wavefmt.fmt = byteswap16( wavefmt.fmt ); wavefmt.chn = byteswap16( wavefmt.chn ); wavefmt.rate = byteswap32( wavefmt.rate ); wavefmt.average = byteswap32( wavefmt.average ); wavefmt.block = byteswap16( wavefmt.block ); wavefmt.bit = byteswap16( wavefmt.bit ); #endif if( strncmp( wavefmt.id, "WAVEfmt ", 8 ) != 0 ) throw m_wavfile + " is broken"; if( wavefmt.fmt != 1 ) throw m_wavfile + " is not a PCM format"; // データチャンクまでseek DATACHK datachk; chksize = 8; for(;;){ fseek( fin, filepos, SEEK_SET ); if( ! fread( &datachk, 1, chksize, fin ) ) throw m_wavfile + " is broken"; if( strncmp( datachk.id, "data", 4 ) == 0 ) break; ++filepos; } #ifdef WORDS_BIGENDIAN datachk.size = byteswap32( datachk.size ); #endif #ifdef _DEBUG std::cout << "rate = " << wavefmt.rate << std::endl << "chn = " << wavefmt.chn << std::endl << "bit = " << wavefmt.bit << std::endl << "sec = " << (double)datachk.size/wavefmt.average << std::endl; #endif // デバイスオープン const char *device = "default"; int err = snd_pcm_open( &handle, device, SND_PCM_STREAM_PLAYBACK, 0 ); if( err < 0 || ! handle ) throw std::string( "cannot open sound device : " ) + snd_strerror( err ); // パラメータ設定 const int msec = 100; snd_pcm_format_t pcmfm = ( wavefmt.bit == 8 ? SND_PCM_FORMAT_U8 : SND_PCM_FORMAT_S16_LE ); err = snd_pcm_set_params( handle, pcmfm, SND_PCM_ACCESS_RW_INTERLEAVED, wavefmt.chn, wavefmt.rate, 1, // リサンプリングする msec * 1000 // period の秒数( msec * 1000 ) ); if( err < 0 ) throw std::string( "failed to set parameter : " ) + snd_strerror( err ); // バッファのメモリ確保 snd_pcm_uframes_t buffer_size; snd_pcm_uframes_t period_size; snd_pcm_get_params( handle, &buffer_size, &period_size ); size_t bufsize = period_size * wavefmt.block; std::vector buffer( bufsize + 64 ); #ifdef _DEBUG std::cout << "period = " << period_size << ", bufsize = " << bufsize << std::endl; size_t totalsize = 0; #endif while( ! m_stop ){ std::fill( buffer.begin(), buffer.end(), 0 ); const std::size_t readsize = fread( buffer.data(), 1, bufsize , fin ); if( ! readsize ) break; #ifdef _DEBUG totalsize += readsize; std::cout << totalsize << " / " << datachk.size << std::endl; #endif snd_pcm_sframes_t frames = snd_pcm_writei( handle, buffer.data(), readsize / wavefmt.block ); if( frames < 0 ) frames = snd_pcm_recover( handle, frames, 0 ); // レジューム if( frames < 0 || frames < ( snd_pcm_sframes_t ) ( readsize / wavefmt.block ) ) throw std::string( "failed to snd_pcm_write : " ) + snd_strerror( err ); } } catch( const std::string& err ){ MISC::ERRMSG( err ); } if( handle ) snd_pcm_close( handle ); if( fin ) fclose( fin ); #ifdef _DEBUG std::cout << "fin\n"; #endif dispatch(); } // // ディスパッチャのコールバック関数 // void Play_Sound::callback_dispatch() { #ifdef _DEBUG std::cout << "Play_Sound::callback_dispatch\n"; #endif wait(); m_playing = false; } #endif jdim-0.10.1/src/sound/playsound.h000066400000000000000000000027151445721505100166330ustar00rootroot00000000000000// ライセンス: GPL2 // サウンド再生クラス #ifndef _PLAYSOUND_H #define _PLAYSOUND_H #ifdef HAVE_CONFIG_H #include "config.h" #endif #ifdef USE_ALSA #include "skeleton/dispatchable.h" #include #include namespace SOUND { // RIFF ( 8 byte ) struct RIFFCHK { char id[ 4 ]; // = "RIFF" std::uint32_t size; // 全体サイズ }; // WAVEfmt ( 28 byte ) struct WAVEFMTCHK { char id[ 8 ]; // "WAVEfmt " std::uint32_t size; // チャンクサイズ std::uint16_t fmt; // 種類( PCMは1 ) std::uint16_t chn; std::uint32_t rate; std::uint32_t average; // = rate * block ( byte ) std::uint16_t block; // = chn * bit / 8 ( byte ) std::uint16_t bit; }; // data ( 8 byte ) struct DATACHK { char id[ 4 ]; // = "data" std::uint32_t size; // チャンクサイズ = PCMデータサイズ }; class Play_Sound : public SKELETON::Dispatchable { std::thread m_thread; std::string m_wavfile; bool m_stop{}; bool m_playing{}; public: Play_Sound(); ~Play_Sound(); bool is_playing() const { return m_playing; } void play( const std::string& wavfile ); void stop(); private: void wait(); void play_wavfile(); void callback_dispatch() override; }; } #endif #endif jdim-0.10.1/src/sound/soundmanager.cpp000066400000000000000000000040631445721505100176310ustar00rootroot00000000000000// ライセンス: GPL2 //#define _DEBUG #include "jddebug.h" #include "soundmanager.h" #include "cache.h" SOUND::SOUND_Manager* instance_sound_manager = nullptr; SOUND::SOUND_Manager* SOUND::get_sound_manager() { #ifdef USE_ALSA if( ! instance_sound_manager ) instance_sound_manager = new SOUND::SOUND_Manager(); assert( instance_sound_manager ); #endif return instance_sound_manager; } void SOUND::delete_sound_manager() { #ifdef USE_ALSA if( instance_sound_manager ) delete instance_sound_manager; instance_sound_manager = nullptr; #endif } void SOUND::play( const int sound ) { #ifdef USE_ALSA SOUND::get_sound_manager()->play( sound ); #endif } /////////////////////////////////////////////// #ifdef USE_ALSA using namespace SOUND; SOUND_Manager::SOUND_Manager() { #ifdef _DEBUG std::cout << "SOUND_Manager::SOUND_Manager\n"; #endif for( int i = 0; i < NUM_SOUNDS; ++i ){ std::string path_sound = get_file( i ); bool exist = false; if( CACHE::file_exists( path_sound ) == CACHE::EXIST_FILE ) exist = true; #ifdef _DEBUG std::cout << path_sound << " " << exist << std::endl; #endif m_playable.push_back( exist ); } } SOUND_Manager::~SOUND_Manager() { #ifdef _DEBUG std::cout << "SOUND_Manager::~SOUND_Manager\n"; #endif m_playsound.stop(); } void SOUND_Manager::play( const int sound ) { if( ! m_playable[ sound ] ) return; #ifdef _DEBUG std::cout << "SOUND_Manager::play sound = " << sound << std::endl; #endif std::string path_sound = get_file( sound ); if( ! path_sound.empty() ) m_playsound.play( path_sound ); } std::string SOUND_Manager::get_file( const int sound ) { std::string path_sound = CACHE::path_sound_root(); switch( sound ){ case SOUND_RES: path_sound += "res.wav"; break; case SOUND_NO: path_sound += "no.wav"; break; case SOUND_NEW: path_sound += "new.wav"; break; case SOUND_ERR: path_sound += "err.wav"; break; default: return std::string(); } return path_sound; } #endif jdim-0.10.1/src/sound/soundmanager.h000066400000000000000000000017071445721505100173000ustar00rootroot00000000000000// ライセンス: GPL2 // サウンド管理クラス #ifndef _SOUNDMANAGER_H #define _SOUNDMANAGER_H #ifdef HAVE_CONFIG_H #include "config.h" #endif #include "playsound.h" #include namespace SOUND { enum{ SOUND_RES = 0, SOUND_NO, SOUND_NEW, SOUND_ERR, NUM_SOUNDS }; #ifdef USE_ALSA class SOUND_Manager { std::vector< bool > m_playable; Play_Sound m_playsound; public: SOUND_Manager(); virtual ~SOUND_Manager(); void play( const int sound ); private: std::string get_file( const int sound ); }; #else class SOUND_Manager { public: SOUND_Manager(){} virtual ~SOUND_Manager(){} }; #endif /////////////////////////////////////// // インターフェース SOUND_Manager* get_sound_manager(); void delete_sound_manager(); void play( const int sound ); } #endif jdim-0.10.1/src/type.h000066400000000000000000000014771445721505100144520ustar00rootroot00000000000000// タイプ #ifndef _TYPE_H #define _TYPE_H enum { // 板のタイプ TYPE_BOARD_2CH = 0, TYPE_BOARD_2CH_COMPATI, // 2ch 互換 TYPE_BOARD_LOCAL, // ローカルファイル TYPE_BOARD_JBBS, // したらば TYPE_BOARD_MACHI, // まち TYPE_BOARD_UNKNOWN, // その他一般的なデータタイプ TYPE_BOARD, TYPE_BOARD_UPDATE, TYPE_THREAD, TYPE_THREAD_UPDATE, TYPE_THREAD_OLD, TYPE_IMAGE, TYPE_DIR, TYPE_DIR_END, // お気に入りの追加の時にサブディレクトリの終了の意味で使う TYPE_COMMENT, TYPE_LINK, TYPE_VBOARD, // お気に入りの仮想板 TYPE_AA, TYPE_HISTITEM, TYPE_USRCMD, TYPE_LINKFILTER, TYPE_REPLACESTR, TYPE_SEPARATOR, TYPE_FILE, TYPE_UNKNOWN }; #endif jdim-0.10.1/src/updatemanager.cpp000066400000000000000000000154121445721505100166330ustar00rootroot00000000000000// ライセンス: GPL2 //#define _DEBUG #include "jddebug.h" #include "updatemanager.h" #include "dbtree/interface.h" #include "command.h" #include "global.h" #include CORE::CheckUpdate_Manager* instance_checkupdate_manager = nullptr; CORE::CheckUpdate_Manager* CORE::get_checkupdate_manager() { if( ! instance_checkupdate_manager ) instance_checkupdate_manager = new CheckUpdate_Manager(); assert( instance_checkupdate_manager ); return instance_checkupdate_manager; } void CORE::delete_checkupdate_manager() { if( instance_checkupdate_manager ) delete instance_checkupdate_manager; instance_checkupdate_manager = nullptr; } /////////////////////////////////////////////// using namespace CORE; CheckUpdate_Manager::CheckUpdate_Manager() = default; CheckUpdate_Manager::~CheckUpdate_Manager() noexcept { assert( ! m_list_item.size() ); } void CheckUpdate_Manager::run() { if( m_running ){ #ifdef _DEBUG std::cout << "CheckUpdate_Manager::run failed items = " << m_list_item.size() << std::endl; #endif return; } m_total = m_list_item.size(); if( ! m_total ) return; #ifdef _DEBUG std::cout << "CheckUpdate_Manager::run total = " << m_total << std::endl; #endif m_running = true; m_url_checking = std::string(); pop_front(); } void CheckUpdate_Manager::stop() { if( ! m_running ) return; m_list_item.clear(); #ifdef _DEBUG std::cout << "CheckUpdate_Manager::stop running = " << m_url_checking << std::endl; #endif // ArticleBase::slot_load_finished()経由でpop_front()が呼び出されて m_running が false になる DBTREE::article_stop_load( m_url_checking ); } // // 更新チェックする板やスレをセットする // // open == true なら更新チェック終了時に url を開く( url が更新可能状態なら ) // void CheckUpdate_Manager::push_back( const std::string& url, const bool open ) { if( m_running ) return; #ifdef _DEBUG std::cout << "CheckUpdate_Manager::push_back " << " open = " << open << " url = " << url << std::endl; #endif std::list< std::string > urllist; // urlが板のアドレスかスレのアドレスか判断する int num_from, num_to; std::string num_str; const std::string url_dat = DBTREE::url_dat( url, num_from, num_to, num_str ); const std::string boardbase = DBTREE::url_boardbase( url ); // スレ if( ! url_dat.empty() ){ #ifdef _DEBUG std::cout << "type = dat\n" << DBTREE::article_subject( url ) << std::endl; #endif urllist.push_back( url ); } // 板 else if( ! boardbase.empty() ){ #ifdef _DEBUG std::cout << "type = board\n" << boardbase << std::endl; #endif urllist = DBTREE::board_get_check_update_articles( url ); } else return; if( open ) m_list_open.push_back( url ); if( ! urllist.size() ) return; // リストの先頭にあるスレから更新チェックをしていき、もし更新されていたらグループに属する // 残りのスレの更新チェックをキャンセルする CheckItem item; item.urllist = urllist; m_list_item.push_back( item ); #ifdef _DEBUG std::cout << "size = " << m_list_item.size() << std::endl; for( const std::string& u : urllist ) std::cout << u << std::endl; #endif } // 次のスレをチェック void CheckUpdate_Manager::pop_front() { if( ! m_running ) return; // チェック完了 if( ! m_list_item.size() ){ m_running = false; #ifdef _DEBUG std::cout << "CheckUpdate_Manager::pop_front end size = " << m_list_item.size() << std::endl; #endif // 更新したスレを開く if( m_list_open.size() ){ std::string urls_article = std::string(); std::string urls_board = std::string(); for( const std::string& url : m_list_open ) { // urlが板のアドレスかスレのアドレスか判断する int num_from, num_to; std::string num_str; const std::string url_dat = DBTREE::url_dat( url, num_from, num_to, num_str ); const std::string boardbase = DBTREE::url_boardbase( url ); // スレ if( ! url_dat.empty() ){ if( DBTREE::article_status( url ) & STATUS_UPDATE ) urls_article += url + " "; } // 板 else if( ! boardbase.empty() ){ if( DBTREE::board_status( url ) & STATUS_UPDATE ) urls_board += url + " "; } } #ifdef _DEBUG std::cout << "open urls_article = " << urls_article << std::endl; std::cout << "open urls_board = " << urls_board << std::endl; #endif if( ! urls_article.empty() ) CORE::core_set_command( "open_article_list", std::string(), urls_article ); if( ! urls_board.empty() ) CORE::core_set_command( "open_board_list", std::string(), urls_board ); m_list_open.clear(); } CORE::core_set_command( "set_info", "", "更新チェック完了"); } // チェック実行 else{ CORE::core_set_command( "set_info", "", "更新チェック中 (" + std::to_string( m_total - m_list_item.size() ) + "/" + std::to_string( m_total ) +")" ); // チェックした結果、更新されていたら // グループの残りのスレのチェックをキャンセルして次のグループへ if( ! m_url_checking.empty() && ( DBTREE::article_status( m_url_checking ) & STATUS_UPDATE ) ){ m_list_item.pop_front(); m_url_checking = std::string(); pop_front(); return; } CheckItem& item = m_list_item.front(); if( ! item.urllist.size() ){ // 次のグループへ m_list_item.pop_front(); m_url_checking = std::string(); pop_front(); return; } m_url_checking = item.urllist.front(); item.urllist.pop_front(); #ifdef _DEBUG std::cout << "CheckUpdate_Manager::pop_front download url = " << m_url_checking << " size = " << m_list_item.size() << std::endl; #endif // 更新チェックが終わったらローダからpop_front()がコールバックされる const bool check_update = true; DBTREE::article_download_dat( m_url_checking, check_update ); if( ! DBTREE::article_is_checking_update( m_url_checking ) ){ #ifdef _DEBUG std::cout << "skipped\n"; #endif m_url_checking = std::string(); pop_front(); return; } #ifdef _DEBUG std::cout << "started\n"; #endif } } jdim-0.10.1/src/updatemanager.h000066400000000000000000000025431445721505100163010ustar00rootroot00000000000000// ライセンス: GPL2 // // 更新チェッククラス // // push_back() 更新チェックするスレのグループを指定してからrun()を呼び出すと更新チェックを開始する。 // stop()で停止する。 // #ifndef _UPDATEMANAGER_H #define _UPDATEMANAGER_H #include #include namespace CORE { class CheckItem { public: std::list< std::string > urllist; // 更新チェックするスレのアドレスのリスト CheckItem(){ urllist.clear(); } }; class CheckUpdate_Manager { std::list< CheckItem > m_list_item; std::list< std::string > m_list_open; bool m_running{}; int m_total{}; std::string m_url_checking; public: CheckUpdate_Manager(); virtual ~CheckUpdate_Manager() noexcept; void run(); void stop(); // 更新チェックする板やスレをセットする // open == true なら更新チェック終了時に url を開く( url が更新可能状態なら ) void push_back( const std::string& url, const bool open ); // 次のスレをチェック void pop_front(); }; /////////////////////////////////////// // インターフェース CheckUpdate_Manager* get_checkupdate_manager(); void delete_checkupdate_manager(); } #endif jdim-0.10.1/src/urlreplacemanager.cpp000066400000000000000000000155021445721505100175070ustar00rootroot00000000000000// ライセンス: GPL2 //#define _DEBUG #include "jddebug.h" #include "urlreplacemanager.h" #include "cache.h" #include "type.h" #include "jdlib/miscutil.h" #include "jdlib/miscmsg.h" #include CORE::Urlreplace_Manager* instance_urlreplace_manager = nullptr; CORE::Urlreplace_Manager* CORE::get_urlreplace_manager() { if( ! instance_urlreplace_manager ) instance_urlreplace_manager = new Urlreplace_Manager(); assert( instance_urlreplace_manager ); return instance_urlreplace_manager; } void CORE::delete_urlreplace_manager() { if( instance_urlreplace_manager ) delete instance_urlreplace_manager; instance_urlreplace_manager = nullptr; } /////////////////////////////////////////////// using namespace CORE; #define DEFALUT_CONFIG \ "#\n" \ "# UrlReplace設定ファイル (urlreplace.conf)\n" \ "#\n" \ "# 設定ファイルの書式:\n" \ "# 正規表現<タブ>変換後URL<タブ>リファラURL<タブ>制御文字\n" \ "#\n" \ "# 設定例:\n" \ "# http://www\\.foobar\\.com/(view/|img\\.php\\?id=)([0-9]+) http://www.foobar.com/view/$2 $0 $IMAGE\n" \ "#\n" \ "# 詳細な書式はマニュアルを参照してください。\n" \ "# この機能を無効にする場合は、このファイルの内容を空にして保存してください。\n" \ "#\n" \ "https?://www\\.youtube\\.com/watch\\?(|[^#]+&)v=([^&#/]+) http://img.youtube.com/vi/$2/0.jpg\n" \ "https?://youtu\\.be/([^#&=/]+) http://img.youtube.com/vi/$1/0.jpg\n" \ "https?://img\\.youtube\\.com/vi/[^/]+/0.jpg $0 $THUMBNAIL\n" \ "\n" Urlreplace_Manager::Urlreplace_Manager() { std::string conf; const std::string path = CACHE::path_urlreplace(); if( CACHE::load_rawdata( path, conf ) ){ conf2list( conf ); } else { // 読み込みエラー、または空ファイル if( CACHE::file_exists( path ) == CACHE::EXIST_ERROR ){ // ファイルが存在しないとき、デフォルト設定ファイルを作成する conf = DEFALUT_CONFIG; if( CACHE::save_rawdata( path, conf ) ){ conf2list( conf ); } } } } Urlreplace_Manager::~Urlreplace_Manager() noexcept = default; // // conf -> リスト // void Urlreplace_Manager::conf2list( const std::string& conf ) { m_list_cmd.clear(); if( conf.empty() ) return; std::list< std::string > lines = MISC::get_lines( conf ); if( lines.size() == 0 ) return; for( const std::string& conf_str : lines ) { if( conf_str.empty() ) continue; if( conf_str[0] == '#' ) continue; // コメント行 std::list tokens = MISC::StringTokenizer( conf_str, '\t' ); if( tokens.size() < 2 ) continue; UrlreplaceItem item; std::list::iterator str = tokens.begin(); // 1: 検索URL constexpr bool icase = false; constexpr bool newline = true; constexpr bool migemo = false; constexpr bool wchar = false; if( (*str).empty() || ! item.creg.set( *str, icase, newline, migemo, wchar ) ) { continue; } // 2: 置換URL if( (*++str).empty() ) { continue; } item.replace = std::move( *str ); std::string tgt_text = "$0"; std::string rep_text = "\\0"; for( int n = '0'; n <= '9'; ++n ) { tgt_text[1] = rep_text[1] = n; item.replace = MISC::replace_str( item.replace, tgt_text, rep_text ); } // 3: リファラURL if( (++str) != tokens.end() ) item.referer = std::move( *str ); // 4: コントロール item.imgctrl = IMGCTRL_NONE; item.match_break = false; if( (++str) != tokens.end() ) { const std::string& ctrl = *str; int imgctrl = IMGCTRL_INIT; // 拡張子がない場合でも画像として扱う if( ctrl.find( "$IMAGE" ) != std::string::npos ){ imgctrl += IMGCTRL_FORCEIMAGE; } // 拡張子があっても画像として扱わない else if( ctrl.find( "$BROWSER" ) != std::string::npos ){ imgctrl += IMGCTRL_FORCEBROWSER; // $IMAGEを優先 } // 拡張子の偽装をチェックしない if( ctrl.find( "$GENUINE" ) != std::string::npos ){ imgctrl += IMGCTRL_GENUINE; } // サムネイル画像 if( ctrl.find( "$THUMBNAIL" ) != std::string::npos ){ imgctrl += IMGCTRL_THUMBNAIL; } if( imgctrl != IMGCTRL_INIT ) item.imgctrl = imgctrl; // 正規表現に一致したら以降の判定を行わない item.match_break = ( ctrl.find( "$BREAK" ) != std::string::npos ); } m_list_cmd.push_back( std::move( item ) ); } } // // URLを任意の正規表現で変換する // bool Urlreplace_Manager::exec( std::string &url ) const { if( m_list_cmd.empty() ) return false; JDLIB::Regex regex; constexpr std::size_t offset = 0; // いずれかの正規表現に一致するか bool matched = false; for( const auto& cmd : m_list_cmd ) { if( regex.match( cmd.creg, url, offset ) ) { matched = true; // 置換URLの変換 url = regex.replace( cmd.replace ); // URLが空になったか、以降の判定を行わない if( url.empty() || cmd.match_break ) break; } } return matched; } // // URLからリファラを求める // bool Urlreplace_Manager::referer( const std::string &url, std::string &refstr ) const { if( m_list_cmd.empty() ) return false; refstr = url; JDLIB::Regex regex; constexpr std::size_t offset = 0; // いずれかの正規表現に一致するか bool matched = false; for( const auto& cmd : m_list_cmd ) { if( regex.match( cmd.creg, refstr, offset ) ) { matched = true; // リファラURLの変換 refstr = regex.replace( cmd.referer ); // URLが空になったか、以降の判定を行わない if( refstr.empty() || cmd.match_break ) break; } } return matched; } // // URLの画像コントロールを取得する // int Urlreplace_Manager::get_imgctrl( const std::string &url ) const { if( m_list_cmd.empty() ) return IMGCTRL_NONE; int imgctrl = IMGCTRL_NONE; JDLIB::Regex regex; constexpr std::size_t offset = 0; // いずれかの正規表現に一致するか for( const auto& cmd : m_list_cmd ) { if( regex.match( cmd.creg, url, offset ) ) { // 画像コントロールを取得 imgctrl = cmd.imgctrl; // 以降の判定を行わない if( cmd.match_break ) break; } } return imgctrl; } jdim-0.10.1/src/urlreplacemanager.h000066400000000000000000000024431445721505100171540ustar00rootroot00000000000000// ライセンス: GPL2 #ifndef _URLREPLACEMANAGER_H #define _URLREPLACEMANAGER_H #include "jdlib/jdregex.h" #include #include namespace CORE { enum { IMGCTRL_INIT = 0, IMGCTRL_NONE = 1, IMGCTRL_FORCEIMAGE = 2, IMGCTRL_FORCEBROWSER = 4, IMGCTRL_GENUINE = 8, IMGCTRL_THUMBNAIL = 16, }; struct UrlreplaceItem { JDLIB::RegexPattern creg; std::string replace; std::string referer; int imgctrl; bool match_break; }; class Urlreplace_Manager { std::vector m_list_cmd; public: Urlreplace_Manager(); virtual ~Urlreplace_Manager() noexcept; // URLを任意の正規表現で変換する bool exec( std::string& url ) const; // URLからリファラを求める bool referer( const std::string& url, std::string& refstr ) const; // URLの画像コントロールを取得する int get_imgctrl( const std::string& url ) const; private: void conf2list( const std::string& conf ); }; /////////////////////////////////////// // インターフェース Urlreplace_Manager* get_urlreplace_manager(); void delete_urlreplace_manager(); } #endif jdim-0.10.1/src/usrcmdmanager.cpp000066400000000000000000000417621445721505100166550ustar00rootroot00000000000000// ライセンス: GPL2 //#define _DEBUG #include "jddebug.h" #include "gtkmmversion.h" #include "usrcmdmanager.h" #include "command.h" #include "cache.h" #include "type.h" #include "skeleton/msgdiag.h" #include "skeleton/prefdiag.h" #include "xml/tools.h" #include "jdlib/miscutil.h" #include "jdlib/jdregex.h" #include "dbtree/interface.h" #include "dbimg/imginterface.h" #include "config/globalconf.h" CORE::Usrcmd_Manager* instance_usrcmd_manager = nullptr; CORE::Usrcmd_Manager* CORE::get_usrcmd_manager() { if( ! instance_usrcmd_manager ) instance_usrcmd_manager = new Usrcmd_Manager(); assert( instance_usrcmd_manager ); return instance_usrcmd_manager; } void CORE::delete_usrcmd_manager() { if( instance_usrcmd_manager ) delete instance_usrcmd_manager; instance_usrcmd_manager = nullptr; } /////////////////////////////////////////////// class ReplaceTextDiag : public SKELETON::PrefDiag { Gtk::VBox m_vbox; Gtk::Entry m_entry; Gtk::Label m_label; public: ReplaceTextDiag( Gtk::Window* parent, const std::string& title ) : SKELETON::PrefDiag( parent, "" ), m_label( title + "を置き換えるテキストを入力してください。" ) { resize( 640, 1 ); m_vbox.pack_start( m_label, Gtk::PACK_SHRINK ); m_vbox.pack_start( m_entry, Gtk::PACK_SHRINK ); get_content_area()->set_spacing( 8 ); get_content_area()->pack_start( m_vbox ); set_title( "テキスト入力" ); show_all_children(); } Glib::ustring get_text() const { return m_entry.get_text(); } }; /////////////////////////////////////////////// using namespace CORE; Usrcmd_Manager::Usrcmd_Manager() { std::string xml; if( CACHE::load_rawdata( CACHE::path_usrcmd(), xml ) ) m_document.init( xml ); analyze_xml(); } // // XML に含まれるコマンド情報を取り出してデータベースを更新 // void Usrcmd_Manager::analyze_xml() { #ifdef _DEBUG std::cout << "Usrcmd_Manager::analyze_xml\n"; #endif m_list_cmd.clear(); const std::list usrcmds = m_document.getElementsByTagName( XML::get_name( TYPE_USRCMD ) ); for( const XML::Dom* usrcmd : usrcmds ) { const std::string cmd = usrcmd->getAttribute( "data" ); #ifdef _DEBUG const std::string name = usrcmd->getAttribute( "name" ); std::cout << usrcmd->nodeName() << " " << name << " " << cmd << std::endl; #endif set_cmd( cmd ); } m_size = m_list_cmd.size(); } void Usrcmd_Manager::set_cmd( const std::string& cmd ) { std::string cmd2 = MISC::utf8_trim( cmd ); m_list_cmd.push_back( cmd2 ); #ifdef _DEBUG std::cout << "Usrcmd_Manager::set_cmd "; std::cout << "[" << m_list_cmd.size()-1 << "] " << cmd2 << std::endl; #endif } // // XML 保存 // void Usrcmd_Manager::save_xml() { #ifdef _DEBUG std::cout << "Usrcmd_Manager::save_xml\n"; std::cout << m_document.get_xml() << std::endl; #endif CACHE::save_rawdata( CACHE::path_usrcmd(), m_document.get_xml() ); } // // 実行 // void Usrcmd_Manager::exec( const int comnum, // コマンド番号 const std::string& url, const std::string& link, const std::string& selection, // 選択文字 const int number // レス番号 ) { if( comnum >= m_size ) return; exec( m_list_cmd[ comnum ], url, link, selection, number ); } void Usrcmd_Manager::exec( const std::string& command, // コマンド const std::string& url, const std::string& link, const std::string& selection, // 選択文字 const int number // レス番号 ) { std::string cmd = command; if( cmd.find( "$ONLY" ) != std::string::npos ){ std::list< std::string > lines = MISC::split_line( cmd ); if( lines.size() >= 2 ){ std::list< std::string >::iterator it = lines.begin(); ++it; ++it; cmd = *it; ++it; for( ; it != lines.end(); ++it ) cmd += " " + ( *it ); } } #ifdef _DEBUG std::cout << "Usrcmd_Manager::exec\n" << "command = " << cmd << std::endl << "url = " << url << std::endl << "link = " << link << std::endl << "selection = " << selection << std::endl << "number = " << number << std::endl; #endif bool use_browser = false; if( cmd.rfind( "$VIEW", 0 ) == 0 ){ use_browser = true; cmd = cmd.substr( 5 ); cmd = MISC::utf8_trim( cmd ); } bool show_dialog = false; if( cmd.rfind( "$DIALOG", 0 ) == 0 ){ show_dialog = true; cmd = cmd.substr( 7 ); cmd = MISC::utf8_trim( cmd ); } cmd = replace_cmd( cmd, url, link, selection, number ); #ifdef _DEBUG std::cout << "exec " << cmd << std::endl; #endif if( cmd.empty() ) return; if( use_browser ) CORE::core_set_command( "open_url_browser", cmd ); else if( show_dialog ){ SKELETON::MsgDiag mdiag( nullptr, cmd ); mdiag.run(); } else Glib::spawn_command_line_async( cmd ); } // // 文字列置換用テキストダイアログ表示 // bool Usrcmd_Manager::show_replacetextdiag( std::string& texti, const std::string& title ) const { if( ! texti.empty() ) return true; ReplaceTextDiag textdiag( nullptr, title ); if( textdiag.run() != Gtk::RESPONSE_OK ) return false; texti = textdiag.get_text(); return true; } // コマンド置換 // cmdの$URLをurl, $LINKをlink, $TEXT*をtext, $NUMBERをnumberで置き換えて出力 // text は UTF-8 であること std::string Usrcmd_Manager::replace_cmd( const std::string& cmd, const std::string& url, const std::string& link, const std::string& text, const int number ) const { std::string cmd_out = cmd; const std::string& oldhostl = DBTREE::article_org_host( link ); const std::string& oldhost = DBTREE::article_org_host( url ); cmd_out = MISC::replace_str( cmd_out, "$URL", DBTREE::url_readcgi( url, 0, 0 ) ); cmd_out = MISC::replace_str( cmd_out, "$DATURL", DBTREE::url_dat( url ) ); cmd_out = MISC::replace_str( cmd_out, "$LOGPATH", CACHE::path_root() ); cmd_out = MISC::replace_str( cmd_out, "$LOCALDATL", CACHE::path_dat( link ) ); cmd_out = MISC::replace_str( cmd_out, "$LOCALDAT", CACHE::path_dat( url ) ); cmd_out = MISC::replace_str( cmd_out, "$LINK", link ); cmd_out = MISC::replace_str( cmd_out, "$NUMBER", std::to_string( number ) ); if( cmd_out.find( "$CACHEDIMG" ) != std::string::npos ){ cmd_out = MISC::replace_str( cmd_out, "$CACHEDIMG", DBIMG::get_cache_path( link ) ); } // ホスト名(http://含む) cmd_out = MISC::replace_str( cmd_out, "$SERVERL", MISC::get_hostname( link ) ); cmd_out = MISC::replace_str( cmd_out, "$SERVER", MISC::get_hostname( url ) ); // ホスト名(http://含まない) cmd_out = MISC::replace_str( cmd_out, "$OLDHOSTNAMEL", MISC::get_hostname( oldhostl, false ) ); cmd_out = MISC::replace_str( cmd_out, "$OLDHOSTNAME", MISC::get_hostname( oldhost, false ) ); cmd_out = MISC::replace_str( cmd_out, "$OLDHOSTL", MISC::get_hostname( oldhostl, false ) ); cmd_out = MISC::replace_str( cmd_out, "$OLDHOST", MISC::get_hostname( oldhost, false ) ); cmd_out = MISC::replace_str( cmd_out, "$HOSTNAMEL", MISC::get_hostname( link, false ) ); cmd_out = MISC::replace_str( cmd_out, "$HOSTNAME", MISC::get_hostname( url, false ) ); cmd_out = MISC::replace_str( cmd_out, "$HOSTL", MISC::get_hostname( link, false ) ); cmd_out = MISC::replace_str( cmd_out, "$HOST", MISC::get_hostname( url, false ) ); // スレが属する板のID cmd_out = MISC::replace_str( cmd_out, "$BBSNAMEL", DBTREE::board_id( link ) ); cmd_out = MISC::replace_str( cmd_out, "$BBSNAME", DBTREE::board_id( url ) ); // スレのID cmd_out = MISC::replace_str( cmd_out, "$DATNAMEL", DBTREE::article_key( link ) ); cmd_out = MISC::replace_str( cmd_out, "$DATNAME", DBTREE::article_key( url ) ); cmd_out = MISC::replace_str( cmd_out, "$TITLE", MISC::to_plain( DBTREE::article_subject( url ) ) ); cmd_out = MISC::replace_str( cmd_out, "$BOARDNAME", DBTREE::board_name( url ) ); // 範囲選択した文字列 std::string texti = text; if( cmd_out.find( "$TEXTIU" ) != std::string::npos ){ if( ! show_replacetextdiag( texti, "$TEXTIU" ) ) return std::string(); cmd_out = MISC::replace_str( cmd_out, "$TEXTIU", MISC::url_encode_plus( texti, Encoding::utf8 ) ); } if( cmd_out.find( "$TEXTIX" ) != std::string::npos ){ if( ! show_replacetextdiag( texti, "$TEXTIX" ) ) return std::string(); cmd_out = MISC::replace_str( cmd_out, "$TEXTIX", MISC::url_encode_plus( texti, Encoding::eucjp ) ); } if( cmd_out.find( "$TEXTIE" ) != std::string::npos ){ if( ! show_replacetextdiag( texti, "$TEXTIE" ) ) return std::string(); cmd_out = MISC::replace_str( cmd_out, "$TEXTIE", MISC::url_encode_plus( texti, Encoding::sjis ) ); } if( cmd_out.find( "$TEXTI" ) != std::string::npos ){ if( ! show_replacetextdiag( texti, "$TEXTI" ) ) return std::string(); cmd_out = MISC::replace_str( cmd_out, "$TEXTI", texti ); } if( cmd_out.find( "$TEXTU" ) != std::string::npos ){ cmd_out = MISC::replace_str( cmd_out, "$TEXTU", MISC::url_encode_plus( texti, Encoding::utf8 ) ); } if( cmd_out.find( "$TEXTX" ) != std::string::npos ){ cmd_out = MISC::replace_str( cmd_out, "$TEXTX", MISC::url_encode_plus( texti, Encoding::eucjp ) ); } if( cmd_out.find( "$TEXTE" ) != std::string::npos ){ cmd_out = MISC::replace_str( cmd_out, "$TEXTE", MISC::url_encode_plus( texti, Encoding::sjis ) ); } if( cmd_out.find( "$TEXT" ) != std::string::npos ){ cmd_out = MISC::replace_str( cmd_out, "$TEXT", texti ); } // 入力ダイアログを表示 std::string input; if( cmd_out.find( "$INPUTU" ) != std::string::npos ){ if( ! show_replacetextdiag( input, "$INPUTU" ) ) return std::string(); cmd_out = MISC::replace_str( cmd_out, "$INPUTU", MISC::url_encode_plus( input, Encoding::utf8 ) ); } if( cmd_out.find( "$INPUTX" ) != std::string::npos ){ if( ! show_replacetextdiag( input, "$INPUTX" ) ) return std::string(); cmd_out = MISC::replace_str( cmd_out, "$INPUTX", MISC::url_encode_plus( input, Encoding::eucjp ) ); } if( cmd_out.find( "$INPUTE" ) != std::string::npos ){ if( ! show_replacetextdiag( input, "$INPUTE" ) ) return std::string(); cmd_out = MISC::replace_str( cmd_out, "$INPUTE", MISC::url_encode_plus( input, Encoding::sjis ) ); } if( cmd_out.find( "$INPUT" ) != std::string::npos ){ if( ! show_replacetextdiag( input, "$INPUT" ) ) return std::string(); cmd_out = MISC::replace_str( cmd_out, "$INPUT", input ); } // 結果のエンコード cmd_out = MISC::replace_str( cmd_out, "$OUTU", "" ); cmd_out = MISC::replace_str( cmd_out, "$OUTX", "" ); cmd_out = MISC::replace_str( cmd_out, "$OUTE", "" ); #ifdef _DEBUG std::cout << "Usrcmd_Manager::replace_cmd\n"; std::cout << "cmd = " << cmd << std::endl; std::cout << "out = " << cmd_out << std::endl; #endif return cmd_out; } // // メニューをアクティブにするか // bool Usrcmd_Manager::is_sensitive( int num, const std::string& link, const std::string& selection ) const { const unsigned int max_selection_str = 1024; if( num >= m_size ) return false; const std::string cmd = m_list_cmd[ num ]; if( cmd.find( "$LINK" ) != std::string::npos || cmd.find( "$SERVERL" ) != std::string::npos || cmd.find( "$HOSTNAMEL" ) != std::string::npos || cmd.find( "$HOSTL" ) != std::string::npos || cmd.find( "$OLDHOSTNAMEL" ) != std::string::npos || cmd.find( "$OLDHOSTL" ) != std::string::npos || cmd.find( "$BBSNAMEL" ) != std::string::npos || cmd.find( "$DATNAMEL" ) != std::string::npos || cmd.find( "$LOCALDATL" ) != std::string::npos ){ if( link.empty() ) return false; } if( cmd.find( "$TEXT" ) != std::string::npos && cmd.find( "$TEXTI" ) == std::string::npos ){ if( selection.empty() || selection.length() > max_selection_str ) return false; } if( cmd.find( "$CACHEDIMG" ) != std::string::npos ){ if( link.empty() ) return false; if( DBIMG::get_type_ext( link ) == DBIMG::T_UNKNOWN ) return false; if( ! DBIMG::is_cached( link ) ) return false; } return true; } // // メニューを隠すか // bool Usrcmd_Manager::is_hide( int num, const std::string& url ) const { if( num >= m_size ) return false; const std::string cmd = m_list_cmd[ num ]; #ifdef _DEBUG std::cout << "Usrcmd_manager::is_hide cmd = " << cmd << std::endl << "url = " << url << std::endl; #endif if( cmd.find( "$ONLY" ) != std::string::npos ){ std::list< std::string > lines = MISC::split_line( cmd ); if( lines.size() >= 2 ){ JDLIB::Regex regex; const size_t offset = 0; const bool icase = false; const bool newline = true; const bool usemigemo = false; const bool wchar = false; std::list< std::string >::iterator it = lines.begin(); ++it; const std::string query = *it; #ifdef _DEBUG std::cout << "query = " << query << std::endl; #endif if( ! regex.exec( query, url, offset, icase, newline, usemigemo, wchar ) ) return true; } } return false; } // // ユーザコマンドの登録とメニュー作成 // std::string Usrcmd_Manager::create_usrcmd_menu( Glib::RefPtr< Gtk::ActionGroup >& action_group ) { int dirno = 0; int cmdno = 0; return create_usrcmd_menu( action_group, &m_document, dirno, cmdno ); } // ユーザコマンドの登録とメニュー作成(再帰用) std::string Usrcmd_Manager::create_usrcmd_menu( Glib::RefPtr< Gtk::ActionGroup >& action_group, const XML::Dom* dom, int& dirno, int& cmdno ) { std::string menu; if( ! dom ) return menu; for( const XML::Dom* child : *dom ) { if( child->nodeType() == XML::NODE_TYPE_ELEMENT ) { #ifdef _DEBUG std::cout << "name = " << child->nodeName() << std::endl; #endif const int type = XML::get_type( child->nodeName() ); if( type == TYPE_DIR ){ const std::string name = child->getAttribute( "name" ); #ifdef _DEBUG std::cout << "[" << dirno << "] " << name << std::endl; #endif const std::string dirname = "usrcmd_dir" + std::to_string( dirno ); action_group->add( Gtk::Action::create( dirname, name ) ); ++dirno; menu += ""; menu += create_usrcmd_menu( action_group, child, dirno, cmdno ); menu += ""; } else if( type == TYPE_SEPARATOR ){ menu += ""; } else if( type == TYPE_USRCMD ){ const std::string name = child->getAttribute( "name" ); #ifdef _DEBUG std::cout << "[" << cmdno << "] " << name << std::endl; #endif const std::string cmdname = "usrcmd" + std::to_string( cmdno ); action_group->add( Gtk::Action::create( cmdname, name ) ); ++cmdno; menu += ""; } else if( child->hasChildNodes() ) menu += create_usrcmd_menu( action_group, child, dirno, cmdno ); } } return menu; } Glib::RefPtr< Gtk::Action > Usrcmd_Manager::get_action( Glib::RefPtr< Gtk::ActionGroup >& action_group, const int num ) { const std::string str_cmd = "usrcmd" + std::to_string( num ); return action_group->get_action( str_cmd ); } // // 選択不可かどうか判断して visible や sensitive を切り替える // void Usrcmd_Manager::toggle_sensitive( Glib::RefPtr< Gtk::ActionGroup >& action_group, const std::string& url_article, const std::string& url_link, const std::string& str_select ) { for( int i = 0; i < m_size; ++i ){ Glib::RefPtr< Gtk::Action > act = get_action( action_group, i ); if( act ){ if( is_hide( i, url_article ) ) act->set_visible( false ); else{ act->set_visible( true ); if( is_sensitive( i, url_link, str_select ) ) act->set_sensitive( true ); else{ act->set_sensitive( false ); if( CONFIG::get_hide_usrcmd() ) act->set_visible( false ); } } } } } jdim-0.10.1/src/usrcmdmanager.h000066400000000000000000000056271445721505100163220ustar00rootroot00000000000000// ライセンス: GPL2 // // ユーザーコマンドの管理クラス // #ifndef _USRCMDMANAGER_H #define _USRCMDMANAGER_H #include "xml/document.h" #include #include #define ROOT_NODE_NAME_USRCMD "usrcmdlist" namespace CORE { class Usrcmd_Manager { XML::Document m_document; int m_size; std::vector< std::string > m_list_cmd; public: Usrcmd_Manager(); virtual ~Usrcmd_Manager() noexcept = default; XML::Document& xml_document() { return m_document; } void analyze_xml(); void save_xml(); int get_size() const { return m_size; } // 実行 void exec( const int comnum, // コマンド番号 const std::string& url, const std::string& link, const std::string& selection, // 選択文字 const int number // レス番号 ); void exec( const std::string& command, // コマンド const std::string& url, const std::string& link, const std::string& selection, // 選択文字 const int number // レス番号 ); // コマンド置換 // cmdの$URLをurl, $LINKをlink, $TEXT*をtext, $NUMBERをnumberで置き換えて出力 // text は UTF-8 であること std::string replace_cmd( const std::string& cmd, const std::string& url, const std::string& link, const std::string& text, const int number ) const; // ユーザコマンドメニューの作成 std::string create_usrcmd_menu( Glib::RefPtr< Gtk::ActionGroup >& action_group ); std::string create_usrcmd_menu( Glib::RefPtr< Gtk::ActionGroup >& action_group, const XML::Dom* dom, int& dirno, int& cmdno ); Glib::RefPtr< Gtk::Action > get_action( Glib::RefPtr< Gtk::ActionGroup >& action_group, const int num ); // 選択不可かどうか判断して visible や sensitive を切り替える void toggle_sensitive( Glib::RefPtr< Gtk::ActionGroup >& action_group, const std::string& url_article, const std::string& url_link, const std::string& str_select ); private: bool show_replacetextdiag( std::string& texti, const std::string& title ) const; void set_cmd( const std::string& cmd ); bool is_sensitive( int num, const std::string& link, const std::string& selection ) const; bool is_hide( int num, const std::string& url ) const; }; /////////////////////////////////////// // インターフェース Usrcmd_Manager* get_usrcmd_manager(); void delete_usrcmd_manager(); } #endif jdim-0.10.1/src/usrcmdpref.cpp000066400000000000000000000257121445721505100161740ustar00rootroot00000000000000// ライセンス: GPL2 //#define _DEBUG #include "jddebug.h" #include "gtkmmversion.h" #include "usrcmdpref.h" #include "usrcmdmanager.h" #include "command.h" #include "dndmanager.h" #include "environment.h" #include "type.h" #include "control/controlid.h" #include "control/controlutil.h" #include "skeleton/msgdiag.h" #include "config/globalconf.h" #include "jdlib/miscutil.h" using namespace CORE; UsrCmdDiag::UsrCmdDiag( Gtk::Window* parent, const Glib::ustring& name, const Glib::ustring& cmd ) : SKELETON::PrefDiag( parent, "" ), m_label_name( "コマンド名", Gtk::ALIGN_START ), m_label_cmd( "実行するコマンド", Gtk::ALIGN_START ), m_button_manual( "オンラインマニュアルの置換文字一覧を表示" ) { resize( 640, 1 ); m_entry_name.set_text( name ); m_entry_cmd.set_text( cmd ); m_button_manual.signal_clicked().connect( sigc::mem_fun( *this, &UsrCmdDiag::slot_show_manual ) ); m_vbox.set_spacing( 8 ); m_vbox.pack_start( m_label_name, Gtk::PACK_SHRINK ); m_vbox.pack_start( m_entry_name, Gtk::PACK_SHRINK ); m_hbox_cmd.pack_start( m_label_cmd, Gtk::PACK_SHRINK ); m_hbox_cmd.pack_end( m_button_manual, Gtk::PACK_SHRINK ); m_vbox.pack_start( m_hbox_cmd, Gtk::PACK_SHRINK ); m_vbox.pack_start( m_entry_cmd, Gtk::PACK_SHRINK ); get_content_area()->set_spacing( 8 ); get_content_area()->pack_start( m_vbox ); set_activate_entry( m_entry_name ); set_activate_entry( m_entry_cmd ); set_title( "ユーザコマンド設定" ); show_all_children(); } void UsrCmdDiag::slot_show_manual() { CORE::core_set_command( "open_url_browser", ENVIRONMENT::get_jdhelpcmd() ); } /////////////////////////////////////////// UsrCmdPref::UsrCmdPref( Gtk::Window* parent, const std::string& url ) : SKELETON::PrefDiag( parent, url ), m_label( "右クリックしてコンテキストメニューからコマンドの追加と削除が出来ます。編集するにはダブルクリックします。" ), m_treeview( url, DNDTARGET_USRCMD, m_columns ), m_ckbt_hide_usrcmd( "選択不可のユーザコマンドを非表示にする", true ) { m_treestore = Gtk::TreeStore::create( m_columns ); m_treeview.create_column( 0 ); m_treeview.set_editable_view( true ); m_treeview.set_size_request( 640, 400 ); m_treeview.signal_row_activated().connect( sigc::mem_fun( *this, &UsrCmdPref::slot_row_activated ) ); m_treeview.sig_button_press().connect( sigc::mem_fun(*this, &UsrCmdPref::slot_button_press ) ); m_treeview.sig_button_release().connect( sigc::mem_fun(*this, &UsrCmdPref::slot_button_release ) ); m_treeview.sig_key_press().connect( sigc::mem_fun(*this, &UsrCmdPref::slot_key_press ) ); m_treeview.sig_key_release().connect( sigc::mem_fun(*this, &UsrCmdPref::slot_key_release ) ); m_scrollwin.add( m_treeview ); m_scrollwin.set_policy( Gtk::POLICY_AUTOMATIC, Gtk::POLICY_ALWAYS ); m_scrollwin.set_size_request( 640, 400 ); get_content_area()->set_spacing( 8 ); get_content_area()->pack_start( m_label, Gtk::PACK_SHRINK ); get_content_area()->pack_start( m_scrollwin ); m_ckbt_hide_usrcmd.set_active( CONFIG::get_hide_usrcmd() ); get_content_area()->pack_start( m_ckbt_hide_usrcmd, Gtk::PACK_SHRINK ); // ポップアップメニュー m_action_group = Gio::SimpleActionGroup::create(); m_action_group->add_action( "NewCmd", sigc::mem_fun( *this, &UsrCmdPref::slot_newcmd ) ); m_action_group->add_action( "NewDir", sigc::mem_fun( *this, &UsrCmdPref::slot_newdir ) ); m_action_group->add_action( "NewSepa", sigc::mem_fun( *this, &UsrCmdPref::slot_newsepa ) ); m_action_group->add_action( "Rename", sigc::mem_fun( *this, &UsrCmdPref::slot_rename ) ); m_action_group->add_action( "Delete", sigc::mem_fun( *this, &UsrCmdPref::slot_delete ) ); m_treeview.insert_action_group( "usrcmd", m_action_group ); auto gmenu = Gio::Menu::create(); gmenu->append( "新規コマンド(_C)", "usrcmd.NewCmd" ); gmenu->append( "名前変更(_R)", "usrcmd.Rename" ); gmenu->append( "新規ディレクトリ(_N)", "usrcmd.NewDir" ); gmenu->append( "区切り(_S)", "usrcmd.NewSepa" ); auto submenu = Gio::Menu::create(); submenu->append( "削除する(_D)", "usrcmd.Delete" ); gmenu->append_submenu( "削除(_D)", submenu ); m_treeview_menu.bind_model( gmenu, true ); m_treeview_menu.attach_to_widget( m_treeview ); m_treeview.xml2tree( CORE::get_usrcmd_manager()->xml_document(), m_treestore, ROOT_NODE_NAME_USRCMD ); show_all_children(); set_title( "ユーザコマンド設定" ); } void UsrCmdPref::timeout() { m_treeview.clock_in(); } void UsrCmdPref::slot_ok_clicked() { CONFIG::set_hide_usrcmd( m_ckbt_hide_usrcmd.get_active() ); m_treeview.tree2xml( CORE::get_usrcmd_manager()->xml_document(), ROOT_NODE_NAME_USRCMD ); CORE::get_usrcmd_manager()->analyze_xml(); CORE::get_usrcmd_manager()->save_xml(); CORE::core_set_command( "reset_article_popupmenu" ); } // // マウスボタン押した // bool UsrCmdPref::slot_button_press( GdkEventButton* event ) { #ifdef _DEBUG std::cout << "UsrCmdPref::slot_button_press\n"; #endif return true; } // // マウスボタン離した // bool UsrCmdPref::slot_button_release( GdkEventButton* event ) { m_path_selected = m_treeview.get_path_under_xy( (int)event->x, (int)event->y ); #ifdef _DEBUG std::cout << "UsrCmdPref::slot_button_release path = " << m_path_selected.to_string() << std::endl;; #endif if( m_control.button_alloted( event, CONTROL::PopupmenuButton ) ) show_popupmenu(); // ディレクトリの開閉 else if( ! m_path_selected.empty() && m_control.button_alloted( event, CONTROL::ClickButton ) ){ Gtk::TreeModel::Row row = *( m_treestore->get_iter( m_path_selected ) ); if( row && row[ m_columns.m_type ] == TYPE_DIR ){ if( ! m_treeview.row_expanded( m_path_selected ) ) m_treeview.expand_row( m_path_selected, false ); else m_treeview.collapse_row( m_path_selected ); } } return true; } // // キーを押した // bool UsrCmdPref::slot_key_press( GdkEventKey* event ) { // 行の名前を編集中なら何もしない if( m_treeview.is_renaming_row() ) return false; const int control = m_control.key_press( event ); #ifdef _DEBUG std::cout << "UsrCmdPref::slot_key_press key = " << control << std::endl; #endif if( control == CONTROL::Delete ) delete_row(); return true; } // // キー上げた // bool UsrCmdPref::slot_key_release( GdkEventKey* event ) { // 行の名前を編集中なら何もしない if( m_treeview.is_renaming_row() ) return false; #ifdef _DEBUG std::cout << "UsrCmdPref::slot_key_release\n"; #endif return true; } // ポップアップメニュー表示 void UsrCmdPref::show_popupmenu() { std::list< Gtk::TreeModel::iterator > list_it = m_treeview.get_selected_iterators(); const bool multi_selected{ list_it.size() > 1 }; auto get_action = [this]( const char* name ) { return Glib::RefPtr::cast_dynamic( m_action_group->lookup_action( name ) ); }; get_action( "NewCmd" )->set_enabled( ! multi_selected ); get_action( "NewDir" )->set_enabled( ! multi_selected ); get_action( "NewSepa" )->set_enabled( ! multi_selected ); auto act_rename = get_action( "Rename" ); auto act_delete = get_action( "Delete" ); if( m_path_selected.empty() ){ act_rename->set_enabled( false ); act_delete->set_enabled( false ); } else{ Gtk::TreeModel::Row row = *( m_treestore->get_iter( m_path_selected ) ); int type = row[ m_columns.m_type ]; act_rename->set_enabled( type != TYPE_SEPARATOR && ! multi_selected ); act_delete->set_enabled( true ); } #if GTK_CHECK_VERSION(3,24,6) // Specify the current event by nullptr. m_treeview_menu.popup_at_pointer( nullptr ); #else m_treeview_menu.popup( 0, gtk_get_current_event_time() ); #endif } // コマンド作成 void UsrCmdPref::slot_newcmd() { UsrCmdDiag diag( this, "", "" ); if( diag.run() == Gtk::RESPONSE_OK ){ if( diag.get_name().empty() || diag.get_cmd().empty() ) return; CORE::DATA_INFO_LIST list_info; CORE::DATA_INFO info; info.type = TYPE_USRCMD; info.parent = nullptr; info.url = std::string(); info.name = diag.get_name(); info.data = diag.get_cmd(); if( m_path_selected.empty() ) info.path = Gtk::TreePath( "0" ).to_string(); else info.path = m_path_selected.to_string(); list_info.push_back( info ); const bool subdir = true; const bool scroll = false; const bool force = false; const bool cancel_undo_commit = false; const int check_dup = 0; // 項目の重複チェックをしない m_treeview.append_info( list_info, m_path_selected, subdir, scroll, force, cancel_undo_commit, check_dup ); m_path_selected = m_treeview.get_current_path(); } } // ディレクトリ作成 void UsrCmdPref::slot_newdir() { m_path_selected = m_treeview.create_newdir( m_path_selected ); } // 区切り作成 void UsrCmdPref::slot_newsepa() { CORE::DATA_INFO_LIST list_info; CORE::DATA_INFO info; info.type = TYPE_SEPARATOR; info.parent = nullptr; info.url = std::string(); info.name = "--- 区切り ---"; info.data = std::string(); if( m_path_selected.empty() ) info.path = Gtk::TreePath( "0" ).to_string(); else info.path = m_path_selected.to_string(); list_info.push_back( info ); const bool subdir = true; const bool scroll = false; const bool force = false; const bool cancel_undo_commit = false; const int check_dup = 0; // 項目の重複チェックをしない m_treeview.append_info( list_info, m_path_selected, subdir, scroll, force, cancel_undo_commit, check_dup ); m_path_selected = m_treeview.get_current_path(); } // // 削除 // void UsrCmdPref::delete_row() { SKELETON::MsgDiag mdiag( nullptr, "削除しますか?", false, Gtk::MESSAGE_QUESTION, Gtk::BUTTONS_YES_NO ); if( mdiag.run() != Gtk::RESPONSE_YES ) return; slot_delete(); } void UsrCmdPref::slot_delete() { const bool force = false; m_treeview.delete_selected_rows( force ); } // 名前変更 void UsrCmdPref::slot_rename() { m_treeview.rename_row( m_path_selected ); } void UsrCmdPref::slot_row_activated( const Gtk::TreeModel::Path& path, Gtk::TreeViewColumn* column ) { #ifdef _DEBUG std::cout << "UsrCmdPref::slot_row_activated path = " << path.to_string() << std::endl; #endif Gtk::TreeModel::Row row = *( m_treestore->get_iter( path ) ); if( ! row ) return; if( row[ m_columns.m_type ] != TYPE_USRCMD ) return; UsrCmdDiag diag( this, row[ m_columns.m_name ], row[ m_columns.m_data ] ); if( diag.run() == Gtk::RESPONSE_OK ){ row[ m_columns.m_name ] = diag.get_name(); row[ m_columns.m_data ] = diag.get_cmd(); static_cast( row ); // cppcheck: unreadVariable } } jdim-0.10.1/src/usrcmdpref.h000066400000000000000000000041211445721505100156300ustar00rootroot00000000000000// ライセンス: GPL2 // ユーザコマンド設定ダイアログ #ifndef _USRCMDPREF_H #define _USRCMDPREF_H #include "skeleton/prefdiag.h" #include "skeleton/edittreeview.h" #include "skeleton/editcolumns.h" #include "control/control.h" namespace CORE { class UsrCmdDiag : public SKELETON::PrefDiag { Gtk::VBox m_vbox; Gtk::Entry m_entry_name; Gtk::Entry m_entry_cmd; Gtk::Label m_label_name; Gtk::HBox m_hbox_cmd; Gtk::Label m_label_cmd; Gtk::Button m_button_manual; public: UsrCmdDiag( Gtk::Window* parent, const Glib::ustring& name, const Glib::ustring& cmd ); Glib::ustring get_name() const { return m_entry_name.get_text(); } Glib::ustring get_cmd() const { return m_entry_cmd.get_text(); } private: void slot_show_manual(); }; class UsrCmdPref : public SKELETON::PrefDiag { Gtk::Label m_label; SKELETON::EditTreeView m_treeview; SKELETON::EditColumns m_columns; Glib::RefPtr< Gtk::TreeStore > m_treestore; Gtk::ScrolledWindow m_scrollwin; Gtk::CheckButton m_ckbt_hide_usrcmd; Gtk::TreeModel::Path m_path_selected; // ポップアップメニュー Glib::RefPtr m_action_group; Gtk::Menu m_treeview_menu; CONTROL::Control m_control; public: UsrCmdPref( Gtk::Window* parent, const std::string& url ); private: void timeout() override; // OK押した void slot_ok_clicked() override; bool slot_button_press( GdkEventButton* event ); bool slot_button_release( GdkEventButton* event ); bool slot_key_press( GdkEventKey* event ); bool slot_key_release( GdkEventKey* event ); void show_popupmenu(); void delete_row(); void slot_newcmd(); void slot_newdir(); void slot_newsepa(); void slot_delete(); void slot_rename(); void slot_row_activated( const Gtk::TreeModel::Path& path, Gtk::TreeViewColumn* column ); }; } #endif jdim-0.10.1/src/viewfactory.cpp000066400000000000000000000122041445721505100163540ustar00rootroot00000000000000// ライセンス: GPL2 #include "viewfactory.h" #include "searchmanager.h" #include "bbslist/bbslistview.h" #include "bbslist/favoriteview.h" #include "bbslist/selectlistview.h" #include "bbslist/historyview.h" #include "board/boardview.h" #include "board/boardviewnext.h" #include "board/boardviewlog.h" #include "board/boardviewsidebar.h" #include "article/articleview.h" #include "article/articleviewsearch.h" #include "article/articleviewpreview.h" #include "article/articleviewinfo.h" #include "article/articleviewpopup.h" #include "article/articleviewetc.h" #include "image/imageview.h" #include "image/imageviewicon.h" #include "image/imageviewpopup.h" #include "message/messageview.h" SKELETON::View* CORE::ViewFactory( int type, const std::string& url, VIEWFACTORY_ARGS view_args ) { switch( type ) { case VIEW_BBSLISTVIEW: return new BBSLIST::BBSListViewMain( url, view_args.arg1, view_args.arg2 ); case VIEW_FAVORITELIST: return new BBSLIST::FavoriteListView( url, view_args.arg1, view_args.arg2 ); case VIEW_SELECTLIST: return new BBSLIST::SelectListView( url, view_args.arg1, view_args.arg2 ); case VIEW_HISTTHREAD: return new BBSLIST::HistoryThreadView( url, view_args.arg1, view_args.arg2 ); case VIEW_HISTCLOSE: return new BBSLIST::HistoryCloseView( url, view_args.arg1, view_args.arg2 ); case VIEW_HISTBOARD: return new BBSLIST::HistoryBoardView( url, view_args.arg1, view_args.arg2 ); case VIEW_HISTCLOSEBOARD: return new BBSLIST::HistoryCloseBoardView( url, view_args.arg1, view_args.arg2 ); case VIEW_HISTCLOSEIMG: return new BBSLIST::HistoryCloseImgView( url, view_args.arg1, view_args.arg2 ); ////////////////// case VIEW_BOARDVIEW: return new BOARD::BoardView( url ); case VIEW_BOARDNEXT: return new BOARD::BoardViewNext( url, view_args.arg1 ); case VIEW_BOARDLOG: return new BOARD::BoardViewLog( url ); case VIEW_BOARDSIDEBAR: return new BOARD::BoardViewSidebar( url, ( view_args.arg1 == "set_history" ) ); ///////////////// case VIEW_ARTICLEVIEW: return new ARTICLE::ArticleViewMain( url ); case VIEW_ARTICLERES: return new ARTICLE::ArticleViewRes( url ); case VIEW_ARTICLENAME: return new ARTICLE::ArticleViewName( url ); case VIEW_ARTICLEID: return new ARTICLE::ArticleViewID( url ); case VIEW_ARTICLEBM: return new ARTICLE::ArticleViewBM( url ); case VIEW_ARTICLEPOST: return new ARTICLE::ArticleViewPost( url ); case VIEW_ARTICLEHIGHREFRES: return new ARTICLE::ArticleViewHighRefRes( url ); case VIEW_ARTICLEURL: return new ARTICLE::ArticleViewURL( url ); case VIEW_ARTICLEREFER: return new ARTICLE::ArticleViewRefer( url ); case VIEW_ARTICLEDRAWOUT: return new ARTICLE::ArticleViewDrawout( url ); case VIEW_ARTICLEPOSTLOG: return new ARTICLE::ArticleViewPostlog( url ); case VIEW_ARTICLESEARCHLOG: return new ARTICLE::ArticleViewSearch( url, ( view_args.arg1 == "exec" ) ); case VIEW_ARTICLESEARCHALLLOG: return new ARTICLE::ArticleViewSearch( url, ( view_args.arg1 == "exec" ) ); case VIEW_ARTICLESEARCHTITLE: return new ARTICLE::ArticleViewSearch( url, ( view_args.arg1 == "exec" ) ); case VIEW_ARTICLEPREVIEW: return new ARTICLE::ArticleViewPreview( url ); case VIEW_ARTICLEINFO: return new ARTICLE::ArticleViewInfo( url ); ///////////////// case VIEW_ARTICLEPOPUPHTML: return new ARTICLE::ArticleViewPopupHTML( url, view_args.arg1 ); case VIEW_ARTICLEPOPUPRES: return new ARTICLE::ArticleViewPopupRes( url, view_args.arg1, ( view_args.arg2 == "true" ), ( view_args.arg3 == "true" ) ); case VIEW_ARTICLEPOPUPNAME: return new ARTICLE::ArticleViewPopupName( url, view_args.arg1 ); case VIEW_ARTICLEPOPUPID: return new ARTICLE::ArticleViewPopupID( url, view_args.arg1 ); case VIEW_ARTICLEPOPUPREFER: return new ARTICLE::ArticleViewPopupRefer( url, view_args.arg1 ); case VIEW_ARTICLEPOPUPDRAWOUT: return new ARTICLE::ArticleViewPopupDrawout( url, view_args.arg1, ( view_args.arg2 == "OR" ) ); case VIEW_ARTICLEPOPUPBM: return new ARTICLE::ArticleViewPopupBM( url ); ///////////////// case VIEW_IMAGEVIEW: return new IMAGE::ImageViewMain( url ); case VIEW_IMAGEICON: return new IMAGE::ImageViewIcon( url ); case VIEW_IMAGEPOPUP: return new IMAGE::ImageViewPopup( url ); ///////////////// case VIEW_MESSAGE: return new MESSAGE::MessageViewMain( url, view_args.arg1 ); case VIEW_NEWTHREAD: return new MESSAGE::MessageViewNew( url, view_args.arg1 ); default: return nullptr; } } jdim-0.10.1/src/viewfactory.h000066400000000000000000000031311445721505100160200ustar00rootroot00000000000000// ライセンス: GPL2 // // SKELETON::VIEWのファクトリー // #ifndef _VIEWFACTORY_H #define _VIEWFACTORY_H #include #include namespace SKELETON { class View; } namespace CORE { enum { VIEW_BBSLISTVIEW, VIEW_FAVORITELIST, VIEW_SELECTLIST, VIEW_HISTTHREAD, VIEW_HISTCLOSE, VIEW_HISTBOARD, VIEW_HISTCLOSEBOARD, VIEW_HISTCLOSEIMG, VIEW_BOARDVIEW, VIEW_BOARDNEXT, VIEW_BOARDLOG, VIEW_BOARDSIDEBAR, VIEW_ARTICLEVIEW, VIEW_ARTICLERES, VIEW_ARTICLENAME, VIEW_ARTICLEID, VIEW_ARTICLEBM, VIEW_ARTICLEPOST, VIEW_ARTICLEHIGHREFRES, VIEW_ARTICLEURL, VIEW_ARTICLEREFER, VIEW_ARTICLEDRAWOUT, VIEW_ARTICLEPREVIEW, VIEW_ARTICLEINFO, VIEW_ARTICLESEARCHLOG, VIEW_ARTICLESEARCHALLLOG, VIEW_ARTICLESEARCHTITLE, VIEW_ARTICLEPOSTLOG, VIEW_ARTICLEPOPUPHTML, VIEW_ARTICLEPOPUPRES, VIEW_ARTICLEPOPUPNAME, VIEW_ARTICLEPOPUPID, VIEW_ARTICLEPOPUPREFER, VIEW_ARTICLEPOPUPDRAWOUT, VIEW_ARTICLEPOPUPBM, VIEW_IMAGEVIEW, VIEW_IMAGEICON, VIEW_IMAGEPOPUP, VIEW_MESSAGE, VIEW_NEWTHREAD, VIEW_NONE }; struct VIEWFACTORY_ARGS { std::string arg1; std::string arg2; std::string arg3; std::string arg4; }; SKELETON::View* ViewFactory( int type, const std::string& url, VIEWFACTORY_ARGS view_args = VIEWFACTORY_ARGS() ); } #endif jdim-0.10.1/src/winmain.cpp000066400000000000000000000165171445721505100154670ustar00rootroot00000000000000// ライセンス: GPL2 #ifdef HAVE_CONFIG_H #include "config.h" #endif //#define _DEBUG #include "jddebug.h" #include "winmain.h" #include "command.h" #include "core.h" #include "session.h" #include "icons/iconmanager.h" #include "jdlib/loader.h" #include "control/controlutil.h" #include "control/controlid.h" #ifdef HAVE_MIGEMO_H #include "jdlib/jdmigemo.h" #include "config/globalconf.h" #endif JDWinMain::JDWinMain( const bool init, const bool skip_setupdiag, const int init_w, const int init_h, const int init_x, const int init_y ) : SKELETON::JDWindow( false ) { #ifdef _DEBUG std::cout << "JDWinMain::JDWinMain init = " << init << std::endl << "x y w h = " << JDWinMain::get_x_win() << " " << JDWinMain::get_y_win() << " " << JDWinMain::get_width_win() << " " << JDWinMain::get_height_win() << std::endl; #endif setlocale( LC_ALL, "ja_JP.UTF-8" ); // アイコンをセット std::vector< Glib::RefPtr< Gdk::Pixbuf > > list_icons; list_icons.push_back( ICON::get_icon( ICON::JD16 ) ); list_icons.push_back( ICON::get_icon( ICON::JD32 ) ); list_icons.push_back( ICON::get_icon( ICON::JD48 ) ); list_icons.push_back( ICON::get_icon( ICON::JD96 ) ); Gtk::Window::set_default_icon_list( list_icons ); // セッション回復 SESSION::init_session(); bool cancel_maximize = false; if( init_w >= 0 ){ cancel_maximize = true; JDWinMain::set_width_win( init_w ); } if( init_h >= 0 ){ cancel_maximize = true; JDWinMain::set_height_win( init_h ); } if( init_x >= 0 ){ cancel_maximize = true; JDWinMain::set_x_win( init_x ); } if( init_y >= 0 ){ cancel_maximize = true; JDWinMain::set_y_win( init_y ); } if( cancel_maximize ){ JDWinMain::set_maximized_win( false ); JDWinMain::set_full_win( false ); } // サイズ変更 init_win(); if( SESSION::is_full_win_main() ) fullscreen(); set_spacing( 4 ); set_modal( false ); property_window_position().set_value( Gtk::WIN_POS_NONE ); set_resizable( true ); property_destroy_with_parent().set_value( false ); add_events( Gdk::BUTTON_PRESS_MASK ); add_events( Gdk::BUTTON_RELEASE_MASK ); add_events( Gdk::POINTER_MOTION_MASK ); // migemo 初期化 #ifdef HAVE_MIGEMO_H jdmigemo::init( CONFIG::get_migemodict_path() ); #endif // 後はcoreを作って任せる m_core = std::make_unique( *this ); m_core->run( init, skip_setupdiag ); } JDWinMain::~JDWinMain() { #ifdef _DEBUG std::cout << "JDWinMain::~JDWinMain window size : x = " << JDWinMain::get_x_win() << " y = " << JDWinMain::get_y_win() << " w = " << JDWinMain::get_width_win() << " h = " << JDWinMain::get_height_win() << " max = " << JDWinMain::is_maximized_win() << std::endl; #endif m_core.reset(); JDLIB::check_loader_alive(); // migemo のクローズ #ifdef HAVE_MIGEMO_H jdmigemo::close(); #endif // アイコンマネージャ削除 ICON::delete_icon_manager(); } // 通常のセッション保存 void JDWinMain::save_session() { #ifdef _DEBUG std::cout << "JDWinMain::save_session\n"; #endif m_core->save_session(); } void JDWinMain::hide() { #ifdef _DEBUG std::cout << "JDWinMain::hide\n"; #endif // GNOME環境の時に閉じるとウィンドウが最小化する時があるので // state_eventをキャンセルする m_cancel_state_event = true; Gtk::Widget::hide(); } int JDWinMain::get_x_win() const { return SESSION::get_x_win_main(); } int JDWinMain::get_y_win() const { return SESSION::get_y_win_main(); } void JDWinMain::set_x_win( const int x ) { SESSION::set_x_win_main( x ); } void JDWinMain::set_y_win( const int y ) { SESSION::set_y_win_main( y ); } int JDWinMain::get_width_win() const { return SESSION::get_width_win_main(); } int JDWinMain::get_height_win() const { return SESSION::get_height_win_main(); } void JDWinMain::set_width_win( const int width ) { SESSION::set_width_win_main( width ); } void JDWinMain::set_height_win( const int height ) { SESSION::set_height_win_main( height ); } bool JDWinMain::is_focus_win() const { return SESSION::is_focus_win_main(); } void JDWinMain::set_focus_win( const bool set ) { SESSION::set_focus_win_main( set ); } bool JDWinMain::is_maximized_win() const { return SESSION::is_maximized_win_main(); } void JDWinMain::set_maximized_win( const bool set ) { SESSION::set_maximized_win_main( set ); } bool JDWinMain::is_iconified_win() const { return SESSION::is_iconified_win_main(); } void JDWinMain::set_iconified_win( const bool set ) { SESSION::set_iconified_win_main( set ); } bool JDWinMain::is_full_win() const { return SESSION::is_full_win_main(); } void JDWinMain::set_full_win( const bool set ) { SESSION::set_full_win_main( set ); } bool JDWinMain::is_shown_win() const { return SESSION::is_shown_win_main(); } void JDWinMain::set_shown_win( const bool set ) { SESSION::set_shown_win_main( set ); } bool JDWinMain::on_delete_event( GdkEventAny* event ) { #ifdef _DEBUG std::cout << "JDWinMain::on_delete_event\n"; #endif // GNOME環境の時に閉じるとウィンドウが最小化する時があるので // state_eventをキャンセルする m_cancel_state_event = true; return SKELETON::JDWindow::on_delete_event( event ); } // 最大、最小化 bool JDWinMain::on_window_state_event( GdkEventWindowState* event ) { // キャンセル ( JDWinMain::on_delete_even() の説明を参照せよ ) if( m_cancel_state_event ) { #ifdef _DEBUG std::cout << "JDWinMain::on_window_state_event cancel" << std::endl; #endif return Gtk::Window::on_window_state_event( event ); } return SKELETON::JDWindow::on_window_state_event( event ); } bool JDWinMain::on_configure_event( GdkEventConfigure* event ) { // キャンセル ( JDWinMain::on_delete_event() の説明を参照せよ ) if( m_cancel_state_event ) { #ifdef _DEBUG std::cout << "JDWinMain::on_configure_event cancel" << std::endl; #endif return Gtk::Window::on_configure_event( event ); } return SKELETON::JDWindow::on_configure_event( event ); } bool JDWinMain::on_button_press_event( GdkEventButton* event ) { #ifdef _DEBUG std::cout << "JDWinMain::on_button_press_event\n"; #endif // マウスジェスチャ m_control.MG_start( event ); return SKELETON::JDWindow::on_button_press_event( event ); } bool JDWinMain::on_button_release_event( GdkEventButton* event ) { #ifdef _DEBUG std::cout << "JDWinMain::on_button_release_event\n"; #endif const bool ret = SKELETON::JDWindow::on_button_release_event( event ); /// マウスジェスチャ const int mg = m_control.MG_end( event ); // マウスジェスチャ if( mg != CONTROL::None ) operate_win( mg ); return ret; } // // マウスが動いた // bool JDWinMain::on_motion_notify_event( GdkEventMotion* event ) { #ifdef _DEBUG std::cout << "JDWinMain::on_motion_notify_event\n"; #endif /// マウスジェスチャ m_control.MG_motion( event ); return SKELETON::JDWindow::on_motion_notify_event( event ); } bool JDWinMain::operate_win( const int control ) { return CONTROL::operate_common( control, std::string(), nullptr ); } jdim-0.10.1/src/winmain.h000066400000000000000000000036451445721505100151320ustar00rootroot00000000000000// ライセンス: GPL2 // // メインウィンドウとmain関数 // #ifndef _MAINWIN_H #define _MAINWIN_H #include "skeleton/window.h" #include "control/control.h" #include namespace CORE { class Core; } class JDWinMain : public SKELETON::JDWindow { std::unique_ptr m_core; bool m_cancel_state_event{}; // 入力コントローラ CONTROL::Control m_control; public: JDWinMain( const bool init, const bool skip_setupdiag, const int init_w, const int init_h, const int init_x, const int init_y ); ~JDWinMain(); // 通常のセッション保存 void save_session(); void hide(); protected: int get_x_win() const override; int get_y_win() const override; void set_x_win( const int x ) override; void set_y_win( const int y ) override; int get_width_win() const override; int get_height_win() const override; void set_width_win( const int width ) override; void set_height_win( const int height ) override; bool is_focus_win() const override; void set_focus_win( const bool set ) override; bool is_maximized_win() const override; void set_maximized_win( const bool set ) override; bool is_iconified_win() const override; void set_iconified_win( const bool set ) override; bool is_full_win() const override; void set_full_win( const bool set ) override; bool is_shown_win() const override; void set_shown_win( const bool set ) override; bool on_delete_event( GdkEventAny* event ) override; bool on_window_state_event( GdkEventWindowState* event ) override; bool on_configure_event( GdkEventConfigure* event ) override; bool on_button_press_event( GdkEventButton* event ) override; bool on_button_release_event( GdkEventButton* event ) override; bool on_motion_notify_event( GdkEventMotion* event ) override; private: bool operate_win( const int control ); }; #endif jdim-0.10.1/src/xml/000077500000000000000000000000001445721505100141075ustar00rootroot00000000000000jdim-0.10.1/src/xml/Makefile.am000066400000000000000000000003141445721505100161410ustar00rootroot00000000000000noinst_LIBRARIES = libxml.a libxml_a_SOURCES = \ document.cpp \ dom.cpp \ tools.cpp noinst_HEADERS = \ document.h \ dom.h \ tools.h AM_CXXFLAGS = @GTKMM_CFLAGS@ AM_CPPFLAGS = -I$(top_srcdir)/src jdim-0.10.1/src/xml/document.cpp000066400000000000000000000073021445721505100164330ustar00rootroot00000000000000// License GPL2 //#define _DEBUG #include "jddebug.h" #include "document.h" #include "jdlib/miscutil.h" namespace XML::dc { constexpr std::size_t kSizeOfRawData = 2 * 1024 * 1024; } using namespace XML; // 文字列を元にノードツリーを作る場合( htmlはHTMLモード。デフォルト = false ) Document::Document( const std::string& str, const bool html ) : Dom( NODE_TYPE_DOCUMENT, "#document", html ) { init( str ); } // Gtk::TreeStore を元にノードツリーを作る場合 Document::Document( Glib::RefPtr< Gtk::TreeStore > treestore, SKELETON::EditColumns& columns, const std::string& root_name ) : Dom( NODE_TYPE_DOCUMENT, "#document" ) { init( treestore, columns, root_name ); } // 何も無い状態からノードツリーを作る場合 Document::Document() : Dom( NODE_TYPE_DOCUMENT, "#document" ) {} // // このクラスは代入可能 // Document& Document::operator=( const Document& document ) { if( this == &document ) return *this; copy_childNodes( document ); return *this; } // // 初期化 // void Document::init( const std::string& str ) { clear(); if( ! str.empty() ) parse( remove_comments( str ) ); } void Document::init( Glib::RefPtr< Gtk::TreeStore > treestore, SKELETON::EditColumns& columns, const std::string& root_name ) { clear(); // XMLとして必要なのでルート要素を作成 Dom* root = appendChild( NODE_TYPE_ELEMENT, root_name ); // ルート以下に追加 root->parse( treestore->children(), columns ); } // // 予めコメントやXML宣言などを取り除いておく( 順番に注意 ) // std::string Document::remove_comments( const std::string& str ) { if( str.size() > dc::kSizeOfRawData ) return std::string(); std::string out_str = str; out_str = MISC::remove_str( out_str, "" ); // out_str = MISC::remove_str( out_str, "" ); // out_str = MISC::remove_str( out_str, "" ); // * ]]> out_str = MISC::remove_str( out_str, "" ); // return out_str; } // // Gtk::TreeStore をセット // // list_path_expand = 後で Gtk::TreeView::expand_row() をするためのリスト // void Document::set_treestore( Glib::RefPtr< Gtk::TreeStore >& treestore, SKELETON::EditColumns& columns, const std::string& root_name, std::list< Gtk::TreePath >& list_path_expand ) const { treestore->clear(); if( size() ) { // ルートの子要素以下が対象 const Dom* root = get_root_element( root_name ); // ルート要素の有無で処理を分ける( 旧様式=無, 新様式=有 ) if( root ) root->append_treestore( treestore, columns, list_path_expand ); else append_treestore( treestore, columns, list_path_expand ); } } // // ルート要素を取得 // // node_name : 要素名で限定、空の時は要素名問わず取得 // static Dom* get_root_element_impl( const std::list& children, const std::string& node_name ) { auto it = std::find_if( children.cbegin(), children.cend(), []( const Dom* c ) { return c->nodeType() == NODE_TYPE_ELEMENT; } ); if( it != children.cend() && ( node_name.empty() || (*it)->nodeName() == node_name ) ) return *it; return nullptr; } Dom* Document::get_root_element( const std::string& node_name ) { // Domのfriend classなのでアクセス可能 return get_root_element_impl( m_childNodes, node_name ); } const Dom* Document::get_root_element( const std::string& node_name ) const { return get_root_element_impl( m_childNodes, node_name ); } jdim-0.10.1/src/xml/document.h000066400000000000000000000035001445721505100160740ustar00rootroot00000000000000// License GPL2 // Document( Dom派生 )クラス #ifndef _DOCUMENT_H #define _DOCUMENT_H #include "dom.h" namespace XML { class Document : public Dom { // コメントなどを取り除く std::string remove_comments( const std::string& str ); // コピーコンストラクタは使わない Document( const Document& ) = delete; public: // 文字列を元にノードツリーを作る場合( html = HTMLモード ) explicit Document( const std::string& str, const bool html = false ); // Gtk::TreeStore を元にノードツリーを作る場合 // ただし列は SKELETON::EditColumns を継承したものであること Document( Glib::RefPtr< Gtk::TreeStore > treestore, SKELETON::EditColumns& columns, const std::string& root_name ); // 何も無い状態からノードツリーを作る場合 Document(); ~Document() noexcept = default; // このクラスは代入可能 Document& operator=( const Document& document ); // 初期化 void init( const std::string& str ); void init( Glib::RefPtr< Gtk::TreeStore > treestore, SKELETON::EditColumns& columns, const std::string& root_name ); // Gtk::TreeStore をセットする // ただし列は SKELETON::EditColumns を継承したものであること void set_treestore( Glib::RefPtr< Gtk::TreeStore >& treestore, SKELETON::EditColumns& columns, const std::string& root_name, std::list< Gtk::TreePath >& list_path_expand ) const; // ルート要素を取得する Dom* get_root_element( const std::string& node_name = {} ); const Dom* get_root_element( const std::string& node_name = {} ) const; }; } #endif jdim-0.10.1/src/xml/dom.cpp000066400000000000000000000465741445721505100154120ustar00rootroot00000000000000// License: GPL2 //#define _DEBUG #include "jddebug.h" #include "dom.h" #include "jdlib/miscutil.h" #include "type.h" #include "tools.h" #include "skeleton/editcolumns.h" #include namespace XML::dm { constexpr std::size_t kSizeOfRawData = 2 * 1024 * 1024; } using namespace XML; // staticなHTMLの要素名リストを再定義 std::set< std::string > Dom::m_static_html_elements; // コンストラクタ Dom::Dom( const int type, const std::string& name, const bool html ) : m_html( html ) , m_nodeType( type ) , m_nodeName( name ) { // HTMLの場合に空要素として扱う物の内で、使われていそうな物( 小文字で統一 ) if( html && m_static_html_elements.empty() ) { m_static_html_elements.insert( "base" ); m_static_html_elements.insert( "br" ); m_static_html_elements.insert( "hr" ); m_static_html_elements.insert( "img" ); m_static_html_elements.insert( "input" ); m_static_html_elements.insert( "link" ); m_static_html_elements.insert( "meta" ); } } // デストラクタ Dom::~Dom() noexcept { #ifdef _DEBUG std::cout << "~Dom() : " << m_nodeName << ", " << m_childNodes.size() << std::endl; #endif clear(); } // コピーコンストラクタ Dom::Dom( const Dom& dom ) : m_html( dom.m_html ) , m_nodeType( dom.m_nodeType ) , m_nodeName( dom.m_nodeName ) , m_nodeValue( dom.m_nodeValue ) , m_attributes( dom.m_attributes ) { copy_childNodes( dom ); } // // 子ノードのクリア // void Dom::clear() noexcept { for( Dom* child : m_childNodes ) { if( child ) delete child; } m_childNodes.clear(); } // // XMLの文字列をパースして子ノードを追加する // void Dom::parse( const std::string& str ) { if( str.empty() || str.size() > dm::kSizeOfRawData ) return; size_t current_pos = 0; const size_t str_length = str.length(); while( current_pos < str_length ) { //プロパティなどに使う変数 int type = NODE_TYPE_UNKNOWN; std::string name; std::string value; std::map< std::string, std::string > attributes_pair; std::string next_source; // "<"を探す const std::size_t tag_lt_pos = str.find( '<', current_pos ); // タグの前のテキストノード if( current_pos < tag_lt_pos ) { type = NODE_TYPE_TEXT; name = "#text"; value = str.substr( current_pos, tag_lt_pos - current_pos ); current_pos = tag_lt_pos; } // 全てがテキストノード else if( tag_lt_pos == std::string::npos ) { type = NODE_TYPE_TEXT; name = "#text"; value = str; current_pos = str.length(); } // 要素ノード else if( current_pos == tag_lt_pos ) { const std::size_t tag_gt_pos = str.find( '>', tag_lt_pos + 1 ); current_pos = tag_gt_pos + 1; // 開始タグの中身( )を取り出す std::string open_tag = str.substr( tag_lt_pos + 1, tag_gt_pos - tag_lt_pos - 1 ); if( open_tag.empty() ) continue; // タグの中身がアルファベットで始まっているかチェック if( !( open_tag[0] >= 'A' && open_tag[0] <= 'Z' ) && !( open_tag[0] >= 'a' && open_tag[0] <= 'z' ) ) { continue; } // タグ構造が壊れてる場合 size_t broken_pos = 0; if( ( broken_pos = open_tag.find( '<' ) ) != std::string::npos ) { current_pos += broken_pos; continue; } // XMLの場合に空要素として扱う要素( )かどうか bool empty_element = false; if( ! m_html && open_tag.compare( open_tag.length() - 1, 1, "/" ) == 0 ) { empty_element = true; open_tag.erase( open_tag.length() - 1 ); } // 要素名を取り出す std::string element_name; size_t i = 0; const size_t open_tag_length = open_tag.length(); while( i < open_tag_length && open_tag[i] != '\n' && open_tag[i] != '\t' && open_tag[i] != ' ' ) { element_name += open_tag[i]; ++i; } if( element_name.empty() ) continue; // 要素ノードのタイプを設定 type = NODE_TYPE_ELEMENT; // 要素名は小文字に統一 name = MISC::tolower_str( element_name ); // 属性ペアのリストを作成 attributes_pair = create_attribute( open_tag.substr( i ) ); // HTMLの場合に空要素として扱う要素かどうか if( m_html && m_static_html_elements.find( name ) != m_static_html_elements.end() ) empty_element = true; // 終了タグがある要素からの中身の取り出し( 中身 ) if( ! empty_element ) { // count は見つける必要がある終了タグの数 size_t close_tag_lt_pos = 0, close_tag_gt_pos = 0, count = 1; while( ( close_tag_lt_pos = str.find( '<', current_pos ) ) != std::string::npos && ( close_tag_gt_pos = str.find( '>', close_tag_lt_pos + 1 ) ) != std::string::npos ) { current_pos = close_tag_gt_pos + 1; // タグの中身を取り出す std::string close_tag = str.substr( close_tag_lt_pos + 1, close_tag_gt_pos - close_tag_lt_pos - 1 ); if( m_html ) close_tag = MISC::tolower_str( close_tag ); // タグ構造が壊れてる場合 if( close_tag.empty() ) continue; else if( ( broken_pos = close_tag.find( '<' ) ) != std::string::npos ) { current_pos += broken_pos; continue; } const std::string& tag_name{ m_html ? name : element_name }; // 空要素でない同名の開始タグを見つけたらカウントを増やす if( close_tag.back() != '/' && close_tag.rfind( tag_name, 0 ) == 0 ) ++count; // 終了タグを見つけたらカウントを減らす else if( close_tag.front() == '/' && close_tag.compare( 1, tag_name.size(), tag_name ) == 0 ) --count; // 終了タグを見つける必要数が 0 になったらループを抜ける if( count == 0 ) break; } // 必要な終了タグが見付からないまま上記のループを抜けた場合は、全体のループを抜ける if( count > 0 ) break; // 次の再帰で使う"子ノード"の素材になる文字列 next_source = str.substr( tag_gt_pos + 1, close_tag_lt_pos - tag_gt_pos - 1 ); } } #ifdef _DEBUG std::cout << "Dom:parse():---------------------------------------\n"; std::cout << "nodeName : " << name << "\n"; std::cout << "nodeType : " << type << "\n"; for( const auto& attr : attributes_pair ) { std::cout << "Attribute: " << "name=" << attr.first << ", value=" << attr.second << std::endl; } std::cout << "nodeValue: " << value << std::endl; #endif // ノードを追加 Dom* node = appendChild( type, name ); node->m_attributes = std::move( attributes_pair ); node->m_nodeValue = std::move( value ); // 再帰 if( ! next_source.empty() ) node->parse( next_source ); } } // // 属性ペアのリストを作成 // std::map< std::string, std::string > Dom::create_attribute( const std::string& str ) const { std::map< std::string, std::string > attributes_pair; if( str.empty() ) return attributes_pair; size_t i = 0, str_length = str.length(); while( i < str_length ) { std::string attr_key; std::string attr_value; // 余分なスペース等を詰める while( i < str_length && ( str[i] == '\n' || str[i] == '\t' || str[i] == ' ' ) ) ++i; // "="かスペース等でなければ属性名とする while( i < str_length && str[i] != '\n' && str[i] != '\t' && str[i] != ' ' && str[i] != '=' ) { attr_key += str[i]; ++i; } // 余分なスペース等を詰める while( i < str_length && ( str[i] == '\n' || str[i] == '\t' || str[i] == ' ' ) ) ++i; // "="の区切りがある場合のみ属性の値をセットする if( str[i] == '=' ) { // "="の分進める ++i; // 余分なスペース等を詰める while( i < str_length && ( str[i] == '\n' || str[i] == '\t' || str[i] == ' ' ) ) ++i; // 属性の値 "〜" if( str[i] == '"' ) { ++i; while( i < str_length && str[i] != '"' ) { attr_value += str[i]; ++i; } if( str[i] == '"' ) ++i; } // 属性の値 '〜' else if( str[i] == '\'' ) { ++i; while( i < str_length && str[i] != '\'' ) { attr_value += str[i]; ++i; } if( str[i] == '\'' ) ++i; } // 属性の値 その他( 本来ならXMLとしてエラー ) else { while( i < str_length && str[i] != '\n' && str[i] != '\t' && str[i] != ' ' ) { attr_value += str[i]; ++i; } } } // 属性を追加 if( ! attr_key.empty() && ! attr_value.empty() ) { // 属性名は小文字に統一する attributes_pair.insert( make_pair( MISC::tolower_str( attr_key ), attr_value ) ); } } return attributes_pair; } // // XMLタグ構造の文字列を生成 // std::string Dom::get_xml( const int n ) const { std::stringstream xml; // インデント std::string indent; // テキストノードの文字列 std::string text; // ノードの種類別に処理 switch( m_nodeType ) { // 要素 case NODE_TYPE_ELEMENT: // インデントを追加 for( int i = 0; i < n; ++i ) indent += " "; // タグの始まり xml << indent << "<" << m_nodeName; // 属性を追加 for( const auto& attr : m_attributes ) { xml << " " << attr.first << "=\"" << attr.second << "\""; } // 子要素がある場合 if( ! m_childNodes.empty() ) { xml << ">\n"; for( const Dom* child : m_childNodes ) { // 子要素をたどる xml << child->get_xml( n + 2 ); } xml << indent << "\n"; } // 空要素の場合 else xml << " />\n"; break; // テキストノード case NODE_TYPE_TEXT: text = MISC::ascii_trim( m_nodeValue ); if( ! text.empty() ) { // インデントを追加 for( int i = 0; i < n; ++i ) indent += " "; xml << indent << text << "\n"; } break; // ドキュメントノード case NODE_TYPE_DOCUMENT: if( ! m_childNodes.empty() ) xml << "\n"; for( const Dom* child : m_childNodes ) { // 子要素をたどる xml << child->get_xml(); } break; } return xml.str(); } // // Gtk::TreeModel::Children からノードツリーを生成 // // ただし列は SKELETON::EditColumns を継承したものであること // void Dom::parse( const Gtk::TreeModel::Children& children, SKELETON::EditColumns& columns ) { if( children.empty() ) return; // Gtk::TreeModel::Children を走査 for( const Gtk::TreeModel::Row& row : children ) { // 各値を取得( skeleton/editcolumns.h を参照 ) const int type = row[ columns.m_type ]; const Glib::ustring& url = row[ columns.m_url ]; const Glib::ustring& data = row[ columns.m_data ]; const Glib::ustring& name = row[ columns.m_name ]; const size_t dirid = row[ columns.m_dirid ]; const bool expand = row[ columns.m_expand ]; if( type != TYPE_UNKNOWN ) { // タイプにより要素名を決定( board や link など) const std::string node_name = XML::get_name( type ); if( ! node_name.empty() ) { Dom* node = appendChild( NODE_TYPE_ELEMENT, node_name ); if( type == TYPE_DIR && expand ) node->setAttribute( "open", "y" ); if( ! name.empty() ) node->setAttribute( "name", name ); if( ! url.empty() ) node->setAttribute( "url", url ); if( ! data.empty() ) node->setAttribute( "data", data ); if( dirid ) node->setAttribute( "dirid", dirid ); // 再帰 const Gtk::TreeModel::Children sub_children = row.children(); if( ! sub_children.empty() ) node->parse( sub_children, columns ); } } } } // // ノードを分解して Gtk::TreeStore へ Gtk::TreeModel::Row を追加 // // ただし列は SKELETON::EditColumns を継承したものであること // // list_path_expand は開いてるツリーを格納するための参照渡し // void Dom::append_treestore( Glib::RefPtr< Gtk::TreeStore >& treestore, SKELETON::EditColumns& columns, std::list< Gtk::TreePath >& list_path_expand, const Gtk::TreeModel::Row& parent ) const { // ノードの子要素を走査 for( const Dom* child : m_childNodes ) { const int node_type = child->nodeType(); if( node_type == NODE_TYPE_ELEMENT ) { const int type = XML::get_type( child->nodeName() ); if( type != TYPE_UNKNOWN ) { Gtk::TreeModel::Row row; // Gtk::TreeStore::append() の追加ポイント if( parent ) row = *( treestore->append( parent.children() ) ); else row = *( treestore->append() ); // 各値をセット columns.setup_row( row, child->getAttribute( "url" ), child->getAttribute( "name" ), child->getAttribute( "data" ), type, 0 ); if( type == TYPE_DIR ){ row[ columns.m_dirid ] = atoi( child->getAttribute( "dirid" ).c_str() ); // 開いているツリーを追加 if( child->getAttribute( "open" ) == "y" ) list_path_expand.push_back( treestore->get_path( row ) ); } // 再帰 if( child->hasChildNodes() ) child->append_treestore( treestore, columns, list_path_expand, row ); } } } } // // getElementById() // Dom* Dom::getElementById( const std::string& id ) const { Dom* node = nullptr; for( Dom* child : m_childNodes ) { if( child->nodeType() == NODE_TYPE_ELEMENT ) { if( child->getAttribute( "id" ) == id ) node = child; // 再帰 else if( child->hasChildNodes() ) node = child->getElementById( id ); if( node ) break; } } return node; } // // getElementsByTagName() // std::list Dom::getElementsByTagName( const std::string& name ) const { std::list domlist; for( Dom* child : m_childNodes ) { if( child->nodeType() == NODE_TYPE_ELEMENT ) { if( child->nodeName() == name ) domlist.push_back( child ); // 再帰 std::list sub_nodes = child->getElementsByTagName( name ); domlist.splice( domlist.end(), std::move( sub_nodes ) ); } } return domlist; } // // ノード:hasChildNodes // bool Dom::hasChildNodes() const noexcept { return ! m_childNodes.empty(); } // // dom の子ノードをコピーする // void Dom::copy_childNodes( const Dom& dom ) { clear(); for( const Dom* child : dom.m_childNodes ) { Dom* node = new Dom( child->m_nodeType, child->m_nodeName ); node->m_nodeValue = child->m_nodeValue; node->m_parentNode = this; node->m_attributes = child->m_attributes; node->copy_childNodes( *child ); m_childNodes.push_back( node ); } } // // ノード:firstChild // Dom* Dom::firstChild() const { if( m_childNodes.empty() ) return nullptr; return m_childNodes.front(); } // // ノード:appendChild() // Dom* Dom::appendChild( const int node_type, const std::string& node_name ) { Dom* node = new Dom( node_type, node_name, m_html ); node->m_parentNode = this; m_childNodes.push_back( node ); return node; } // // ノード:removeChild() // bool Dom::removeChild( Dom* node ) { if( ! node ) return false; m_childNodes.remove( node ); delete node; return true; } // // insertBefore() の機能をフルに使っていなかったためシンプルにした関数を導入する // Dom* Dom::emplace_front( const int node_type, const std::string& node_name ) { Dom* node = new Dom( node_type, node_name ); node->m_parentNode = this; m_childNodes.push_front( node ); return node; } // // 属性:getAttribute() // std::string Dom::getAttribute( const std::string& name ) const { std::string value; if( name.empty() ) return value; std::map< std::string, std::string >::const_iterator it = m_attributes.find( name ); if( it != m_attributes.cend() ) value = MISC::html_unescape( (*it).second ); return value; } // // 属性:setAttribute( std::string ) // bool Dom::setAttribute( const std::string& name, const std::string& value ) { if( name.empty() || value.empty() || m_nodeType != NODE_TYPE_ELEMENT ) return false; // 属性名は小文字に統一 const std::string attr_name = MISC::tolower_str( name ); m_attributes.insert( make_pair( attr_name, MISC::html_escape( value ) ) ); return true; } // // 属性:setAttribute( int ) // bool Dom::setAttribute( const std::string& name, const int value ) { if( name.empty() || m_nodeType != NODE_TYPE_ELEMENT ) return false; // 属性名は小文字に統一 const std::string attr_name = MISC::tolower_str( name ); m_attributes.insert( make_pair( attr_name, std::to_string( value ) ) ); return true; } jdim-0.10.1/src/xml/dom.h000066400000000000000000000103631445721505100150420ustar00rootroot00000000000000// License: GPL2 // DOM基本クラス #ifndef _DOM_H #define _DOM_H #include #include #include #include #include namespace SKELETON { class EditColumns; } namespace XML { // ノードタイプ( m_nodeType ) enum { NODE_TYPE_UNKNOWN = 0, NODE_TYPE_ELEMENT, NODE_TYPE_TEXT, NODE_TYPE_DOCUMENT }; class Dom { // HTMLの時に空要素として扱う要素を格納する static std::set< std::string > m_static_html_elements; // HTMLモード有/無 bool m_html; // プロパティ int m_nodeType; std::string m_nodeName; std::string m_nodeValue; Dom* m_parentNode{}; // 属性ペアのリスト std::map< std::string, std::string > m_attributes; // 子要素リスト std::list< Dom* > m_childNodes; private: // このクラスの代入演算子は使わない Dom& operator=( const Dom& ) = delete; // 属性ペアのリストを作成 std::map< std::string, std::string > create_attribute( const std::string& str ) const; protected: // Document をフレンドクラスとする( append_treestore() を public にしたくない ) friend class Document; // コピーコンストラクタ Dom( const Dom& dom ); // パースして子ノードを追加 void parse( const std::string& str ); void parse( const Gtk::TreeModel::Children& children, SKELETON::EditColumns& columns ); // プロパティをセットするアクセッサ void copy_childNodes( const Dom& dom ); // dom の子ノードをコピーする // ノードを分解して Gtk::TreeStore へ Gtk::TreeModel::Row を追加 // ただし列は SKELETON::EditColumns を継承したものであること void append_treestore( Glib::RefPtr< Gtk::TreeStore >& treestore, SKELETON::EditColumns& columns, std::list< Gtk::TreePath >& list_path_expand, const Gtk::TreeModel::Row& parnet = Gtk::TreeModel::Row() ) const; public: // コンストラクタ、デストラクタ Dom( const int type, const std::string& name, const bool html = false ); virtual ~Dom() noexcept; // クリア void clear() noexcept; // XMLタグ構造の文字列を生成 std::string get_xml( const int n = 0 ) const; // getElement(s)By*() Dom* getElementById( const std::string& id ) const; std::list getElementsByTagName( const std::string& name ) const; // プロパティを扱うアクセッサ int nodeType() const noexcept { return m_nodeType; } std::string nodeName() const { return m_nodeName; } std::string nodeValue() const { return m_nodeValue; } void nodeValue( const std::string& value ) { m_nodeValue = value; } // ノード // 注意:appendChild() は // 戻り値と引数がJavascriptなどのDOMとは異なります。 // // delete忘れを防ぐために外部でnewしない方が良いだろうという // 事で、メンバ関数の内部でnewして追加されたノードのポインタ // を返すようにしてあります。 // // クラス外で使用していないメンバ関数は削除してあります。 bool hasChildNodes() const noexcept; std::list childNodes() const = delete; Dom* firstChild() const; Dom* appendChild( const int node_type, const std::string& node_name ); bool removeChild( Dom* node ); Dom* emplace_front( int node_type, const std::string& node_name ); // 属性 std::string getAttribute( const std::string& name ) const; bool setAttribute( const std::string& name, const std::string& value ); bool setAttribute( const std::string& name, const int value ); std::size_t size() const noexcept { return m_childNodes.size(); } auto begin() const noexcept { return m_childNodes.cbegin(); } auto end() const noexcept { return m_childNodes.cend(); } }; } #endif jdim-0.10.1/src/xml/meson.build000066400000000000000000000002731445721505100162530ustar00rootroot00000000000000sources = [ 'document.cpp', 'dom.cpp', 'tools.cpp', ] xml_lib = static_library( 'xml', sources, dependencies : gtkmm_dep, include_directories : include_directories('..'), ) jdim-0.10.1/src/xml/tools.cpp000066400000000000000000000105171445721505100157570ustar00rootroot00000000000000// License: GPL2 #include "tools.h" #include "icons/iconmanager.h" #include "type.h" // // TYPE_ID から要素名を取得( TYPE_ID は global.h を参照 ) // std::string XML::get_name( const int type_id ) { std::string name; switch( type_id ) { case TYPE_DIR: // サブディレクトリ name = "subdir"; break; case TYPE_BOARD: // 板 name = "board"; break; case TYPE_BOARD_UPDATE: // 更新可能板 name = "board_update"; break; case TYPE_THREAD: // スレ name = "thread"; break; case TYPE_THREAD_UPDATE: // 更新可能スレ name = "thread_update"; break; case TYPE_THREAD_OLD: // dat落ちスレ name = "thread_old"; break; case TYPE_IMAGE: // 画像 name = "image"; break; case TYPE_COMMENT: // コメント name = "comment"; break; case TYPE_LINK: // リンク name = "link"; break; case TYPE_VBOARD: // 仮想板 name = "vboard"; break; case TYPE_AA: // AA name = "aa"; break; case TYPE_HISTITEM: // HISTORY::ViewHistoryItem name = "histitem"; break; case TYPE_USRCMD: name = "usrcmd"; break; case TYPE_LINKFILTER: name = "linkfilter"; break; case TYPE_REPLACESTR: name = "replacestr"; break; case TYPE_SEPARATOR: name = "separator"; break; } return name; } // // 要素名から TYPE_ID を取得 // int XML::get_type( const std::string& node_name ) { int type = TYPE_UNKNOWN; if( node_name == "board" ) { type = TYPE_BOARD; } else if( node_name == "board_update" ) { type = TYPE_BOARD_UPDATE; } else if( node_name == "comment" ) { type = TYPE_COMMENT; } else if( node_name == "image" ) { type = TYPE_IMAGE; } else if( node_name == "link" ) { type = TYPE_LINK; } else if( node_name == "vboard" ) { type = TYPE_VBOARD; } else if( node_name == "subdir" ) { type = TYPE_DIR; } else if( node_name == "thread" ) { type = TYPE_THREAD; } else if( node_name == "thread_update" ) { type = TYPE_THREAD_UPDATE; } else if( node_name == "thread_old" ) { type = TYPE_THREAD_OLD; } else if( node_name == "aa" ) { type = TYPE_AA; } else if( node_name == "histitem" ) { type = TYPE_HISTITEM; } else if( node_name == "usrcmd" ) { type = TYPE_USRCMD; } else if( node_name == "linkfilter" ) { type = TYPE_LINKFILTER; } else if( node_name == "replacestr" ) { type = TYPE_REPLACESTR; } else if( node_name == "separator" ) { type = TYPE_SEPARATOR; } return type; } // // TYPE_ID からアイコンを取得 ( アイコンは icons/iconmanager.h を参照 ) // Glib::RefPtr< Gdk::Pixbuf > XML::get_icon( const int type_id ) { Glib::RefPtr< Gdk::Pixbuf > icon = ICON::get_icon( ICON::TRANSPARENT ); switch( type_id ) { case TYPE_DIR: icon = ICON::get_icon( ICON::DIR ); break; case TYPE_BOARD: icon = ICON::get_icon( ICON::BOARD ); break; case TYPE_BOARD_UPDATE: icon = ICON::get_icon( ICON::BOARD_UPDATE ); break; case TYPE_THREAD: icon = ICON::get_icon( ICON::THREAD ); break; case TYPE_THREAD_UPDATE: icon = ICON::get_icon( ICON::THREAD_UPDATE ); break; case TYPE_THREAD_OLD: icon = ICON::get_icon( ICON::THREAD_OLD ); break; case TYPE_IMAGE: icon = ICON::get_icon( ICON::IMAGE ); break; case TYPE_LINK: icon = ICON::get_icon( ICON::LINK ); break; case TYPE_VBOARD: icon = ICON::get_icon( ICON::BOARD ); break; case TYPE_USRCMD: icon = ICON::get_icon( ICON::THREAD ); break; } return icon; } jdim-0.10.1/src/xml/tools.h000066400000000000000000000007451445721505100154260ustar00rootroot00000000000000// License: GPL2 #ifndef _TOOLS_H #define _TOOLS_H #include #include namespace XML { // TYPE_ID から要素名を取得( TYPE_ID は global.h を参照 ) std::string get_name( const int type_id ); // 要素名から TYPE_ID を取得 int get_type( const std::string& node_name ); // TYPE_ID からアイコンを取得 ( アイコンは icons/iconmanager.h を参照 ) Glib::RefPtr< Gdk::Pixbuf > get_icon( const int type_id ); } #endif jdim-0.10.1/test/000077500000000000000000000000001445721505100134775ustar00rootroot00000000000000jdim-0.10.1/test/Makefile.am000066400000000000000000000073631445721505100155440ustar00rootroot00000000000000AUTOMAKE_OPTIONS = subdir-objects GTEST_SRCDIR = @GTEST_SRCDIR@ .PHONY: test # no-op all: ; test: check if USE_GTEST_SOURCES GTEST_DIR = $(GTEST_SRCDIR)/googletest GTEST_INCS = -isystem $(GTEST_DIR)/include GTEST_MAIN_LIB = gtest_main.a # For simplicity and to avoid depending on Google Test's # implementation details, the dependencies specified below are # conservative and not optimized. This is fine as Google Test # compiles fast and for ordinary users its source rarely changes. gtest-all.o: $(GTEST_DIR)/src/gtest-all.cc $(CXX) $(CPPFLAGS) $(GTEST_INCS) -I$(GTEST_DIR) $(CXXFLAGS) -c -o $@ $< gtest_main.o: $(GTEST_DIR)/src/gtest_main.cc $(CXX) $(CPPFLAGS) $(GTEST_INCS) -I$(GTEST_DIR) $(CXXFLAGS) -c -o $@ $< $(GTEST_MAIN_LIB): gtest-all.o gtest_main.o $(AR) $(ARFLAGS) $@ $^ endif check_PROGRAMS = gtest_jdim TESTS = gtest_jdim AM_CXXFLAGS = @GTKMM_CFLAGS@ @GNUTLS_CFLAGS@ @OPENSSL_CFLAGS@ @LIBSM_CFLAGS@ @GTEST_CFLAGS@ \ $(GTEST_INCS) -I$(top_srcdir)/src gtest_jdim_LDADD = \ $(top_builddir)/src/article/libarticle.a \ $(top_builddir)/src/bbslist/libbbslist.a \ $(top_builddir)/src/board/libboard.a \ $(top_builddir)/src/config/libconfig.a \ $(top_builddir)/src/control/libcontrol.a \ $(top_builddir)/src/dbimg/libdbimg.a \ $(top_builddir)/src/dbtree/libdbtree.a \ $(top_builddir)/src/history/libhistory.a \ $(top_builddir)/src/icons/libicon.a \ $(top_builddir)/src/image/libimage.a \ $(top_builddir)/src/jdlib/libjdlib.a \ $(top_builddir)/src/message/libmessage.a \ $(top_builddir)/src/skeleton/libskeleton.a \ $(top_builddir)/src/sound/libsound.a \ $(top_builddir)/src/xml/libxml.a \ \ $(top_builddir)/src/aamanager.o \ $(top_builddir)/src/articleitemmenupref.o \ $(top_builddir)/src/articleitempref.o \ $(top_builddir)/src/boarditemmenupref.o \ $(top_builddir)/src/boarditempref.o \ $(top_builddir)/src/browsers.o \ $(top_builddir)/src/cache.o \ $(top_builddir)/src/command.o \ $(top_builddir)/src/compmanager.o \ $(top_builddir)/src/core.o \ $(top_builddir)/src/cssmanager.o \ $(top_builddir)/src/dispatchmanager.o \ $(top_builddir)/src/dndmanager.o \ $(top_builddir)/src/environment.o \ $(top_builddir)/src/fontcolorpref.o \ $(top_builddir)/src/iomonitor.o \ $(top_builddir)/src/linkfiltermanager.o \ $(top_builddir)/src/linkfilterpref.o \ $(top_builddir)/src/livepref.o \ $(top_builddir)/src/login2ch.o \ $(top_builddir)/src/loginbe.o \ $(top_builddir)/src/mainitempref.o \ $(top_builddir)/src/maintoolbar.o \ $(top_builddir)/src/menuslots.o \ $(top_builddir)/src/msgitempref.o \ $(top_builddir)/src/openurldiag.o \ $(top_builddir)/src/prefdiagfactory.o \ $(top_builddir)/src/replacestrmanager.o \ $(top_builddir)/src/replacestrpref.o \ $(top_builddir)/src/searchitempref.o \ $(top_builddir)/src/searchloader.o \ $(top_builddir)/src/searchmanager.o \ $(top_builddir)/src/session.o \ $(top_builddir)/src/setupwizard.o \ $(top_builddir)/src/sharedbuffer.o \ $(top_builddir)/src/sidebaritempref.o \ $(top_builddir)/src/updatemanager.o \ $(top_builddir)/src/urlreplacemanager.o \ $(top_builddir)/src/usrcmdmanager.o \ $(top_builddir)/src/usrcmdpref.o \ $(top_builddir)/src/viewfactory.o \ $(top_builddir)/src/winmain.o \ \ $(GTEST_MAIN_LIB) \ @LIBS@ @GTKMM_LIBS@ @GNUTLS_LIBS@ @OPENSSL_LIBS@ @LIBSM_LIBS@ @ALSA_LIBS@ @X11_LIBS@ @GTEST_LIBS@ # テストコードのファイルを追加したらここにリストします # 行末のバックスラッシュに注意 gtest_jdim_SOURCES = \ gtest_dbtree_nodetreebase.cpp \ gtest_dbtree_spchar_decoder.cpp \ gtest_jdlib_cookiemanager.cpp \ gtest_jdlib_jdiconv.cpp \ gtest_jdlib_jdregex.cpp \ gtest_jdlib_loader.cpp \ gtest_jdlib_misccharcode.cpp \ gtest_jdlib_misctime.cpp \ gtest_jdlib_misctrip.cpp \ gtest_jdlib_miscutil.cpp \ gtest_jdlib_span.cpp \ gtest_xml_dom.cpp jdim-0.10.1/test/README.md000066400000000000000000000056741445721505100147720ustar00rootroot00000000000000# JDim テストガイド JDimは [Google Test][google_test] (gtest)を使って機能をテストします。 テストはmesonコマンドでビルド、実行が行えます。 ## セットアップ [事前準備][readme-prepare]に加えて googletest をインストールします。 (a)ディストリビューションのパッケージをインストールするか、 (b)[meson wraptool][meson-wraptool]を利用してください。 ### (a) ディストリビューションのパッケージを利用する場合 ディストロのパッケージ管理ツールを使って googletest のパッケージをインストールします。 #### Debian系 ```sh sudo apt install libgtest-dev ``` ### (b) Meson wraptool を利用する場合 [Wrap DB][wrapdb]を利用してローカルの`subprojects`ディレクトリにインストールします。 googletest のソースコードは構成時にダウンロードされます。 ```sh mkdir subprojects meson wrap install gtest ``` ## テストのビルドと実行 mesonの **test** サブコマンドでテストプログラムのビルドと実行を行います。 結果表示など詳細はMesonの[リファレンス][meson-reference]を参照してください。 ```sh meson setup builddir meson test -C builddir ``` ### 特定のテストケースだけ実行する `meson test`のオプション引数 `--test-args` にgtestのテストケースを絞り込むオプションを渡します。 ```sh meson test -C builddir --test-args='--gtest_filter=Iconv_*' ``` > #### `--gtest_filter=POSITIVE_PATTERNS[-NEGATIVE_PATTERNS]` > Run only the tests whose name matches one of the positive patterns but none of the negative patterns. > '?' matches any single character; '*' matches any substring; ':' separates two patterns. ## テストコードのファイルを追加する テストコードのファイル名は gtest\_\*.cpp の形式にする必要があります。 ファイル名は小文字で `gtest_サブディレクトリ名_ソースファイル名.cpp` を推奨します。 テストの記述方法はテストコードを見るかwebを参照してください。 例 * `src/jdlib/miscutil.cpp` → `test/gtest_jdlib_miscutil.cpp` * `src/core.cpp` → `test/gtest_core.cpp` テストコードのファイルを追加したときは **test/meson.build** の `sources` にリストします。 ## 制限 以下は今のところサポートしておりません。 * JDim全体ではなくテスト対象のソースコードだけをビルドする * `src/main.cpp` をテストする (ファイル名やエントリーポイントが衝突する) [google_test]: https://github.com/google/googletest [readme-prepare]: https://github.com/JDimproved/JDim/blob/master/README.md#%E4%BA%8B%E5%89%8D%E6%BA%96%E5%82%99 [meson-wraptool]: https://mesonbuild.com/Using-wraptool.html [wrapdb]: https://wrapdb.mesonbuild.com/gtest [meson-reference]: https://mesonbuild.com/Unit-tests.html#other-test-options jdim-0.10.1/test/gtest_dbtree_nodetreebase.cpp000066400000000000000000000105011445721505100213730ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-2.0-only #include "dbtree/nodetreebase.h" #include "gtest/gtest.h" #include namespace { class NodeTreeBase_RemoveImenuTest : public ::testing::Test {}; TEST_F(NodeTreeBase_RemoveImenuTest, empty_string) { std::string inout; EXPECT_FALSE( DBTREE::NodeTreeBase::remove_imenu( inout ) ); EXPECT_EQ( "", inout ); } TEST_F(NodeTreeBase_RemoveImenuTest, not_remove) { constexpr const char* test_data[][2] = { { "ftp://ime.nu/foobar.baz", "ftp://ime.nu/foobar.baz" }, { "http://example.test/foobar.baz", "http://example.test/foobar.baz" }, { "https://ime.nu/", "https://ime.nu/" }, { "https://ime.st/", "https://ime.st/" }, { "https://nun.nu/", "https://nun.nu/" }, { "https://jump.5ch.net/?", "https://jump.5ch.net/?" }, { "https://jump.2ch.net/?", "https://jump.2ch.net/?" }, { "https://pinktower.com/", "https://pinktower.com/" }, }; std::string buffer; for( auto [input, expect] : test_data ) { buffer.assign( input ); EXPECT_FALSE( DBTREE::NodeTreeBase::remove_imenu( buffer ) ); EXPECT_EQ( expect, buffer ); } } TEST_F(NodeTreeBase_RemoveImenuTest, single_ime_nu) { constexpr const char* test_data[][2] = { { "http://ime.nu/foobar.baz", "http://foobar.baz" }, { "https://ime.nu/foobar.baz", "https://foobar.baz" }, { "https://ime.nu/http://foobar.baz", "http://foobar.baz" }, }; std::string buffer; for( auto [input, expect] : test_data ) { buffer.assign( input ); EXPECT_TRUE( DBTREE::NodeTreeBase::remove_imenu( buffer ) ); EXPECT_EQ( expect, buffer ); } } TEST_F(NodeTreeBase_RemoveImenuTest, single_ime_st) { constexpr const char* test_data[][2] = { { "http://ime.st/foobar.baz", "http://foobar.baz" }, { "https://ime.st/foobar.baz", "https://foobar.baz" }, { "http://ime.nu/https://foobar.baz", "https://foobar.baz" }, }; std::string buffer; for( auto [input, expect] : test_data ) { buffer.assign( input ); EXPECT_TRUE( DBTREE::NodeTreeBase::remove_imenu( buffer ) ); EXPECT_EQ( expect, buffer ); } } TEST_F(NodeTreeBase_RemoveImenuTest, single_nun_nu) { constexpr const char* test_data[][2] = { { "http://nun.nu/foobar.baz", "http://foobar.baz" }, { "https://nun.nu/foobar.baz", "https://foobar.baz" }, { "https://nun.nu/http://foobar.baz", "http://foobar.baz" }, }; std::string buffer; for( auto [input, expect] : test_data ) { buffer.assign( input ); EXPECT_TRUE( DBTREE::NodeTreeBase::remove_imenu( buffer ) ); EXPECT_EQ( expect, buffer ); } } TEST_F(NodeTreeBase_RemoveImenuTest, single_pinktower_com) { constexpr const char* test_data[][2] = { { "http://pinktower.com/foobar.baz", "http://foobar.baz" }, { "https://pinktower.com/foobar.baz", "https://foobar.baz" }, { "http://pinktower.com/https://foobar.baz", "https://foobar.baz" }, }; std::string buffer; for( auto [input, expect] : test_data ) { buffer.assign( input ); EXPECT_TRUE( DBTREE::NodeTreeBase::remove_imenu( buffer ) ); EXPECT_EQ( expect, buffer ); } } TEST_F(NodeTreeBase_RemoveImenuTest, single_jump_5ch_net) { constexpr const char* test_data[][2] = { { "http://jump.5ch.net/?http://foobar.baz", "http://foobar.baz" }, { "https://jump.5ch.net/?https://foobar.baz", "https://foobar.baz" }, { "http://jump.5ch.net/?https://foobar.baz", "https://foobar.baz" }, }; std::string buffer; for( auto [input, expect] : test_data ) { buffer.assign( input ); EXPECT_TRUE( DBTREE::NodeTreeBase::remove_imenu( buffer ) ); EXPECT_EQ( expect, buffer ); } } TEST_F(NodeTreeBase_RemoveImenuTest, single_jump_2ch_net) { constexpr const char* test_data[][2] = { { "http://jump.2ch.net/?http://foobar.baz", "http://foobar.baz" }, { "https://jump.2ch.net/?https://foobar.baz", "https://foobar.baz" }, { "http://jump.2ch.net/?https://foobar.baz", "https://foobar.baz" }, }; std::string buffer; for( auto [input, expect] : test_data ) { buffer.assign( input ); EXPECT_TRUE( DBTREE::NodeTreeBase::remove_imenu( buffer ) ); EXPECT_EQ( expect, buffer ); } } } // namespace jdim-0.10.1/test/gtest_dbtree_spchar_decoder.cpp000066400000000000000000000412101445721505100217010ustar00rootroot00000000000000#include "dbtree/node.h" #include "dbtree/spchar_decoder.h" #include "dbtree/spchar_tbl.h" #include "gtest/gtest.h" namespace { class DBTREE_DecodeCharNumberTest : public ::testing::Test {}; TEST_F(DBTREE_DecodeCharNumberTest, non_charref) { char out_char[16]{}; int n_in; int n_out; EXPECT_EQ( DBTREE::NODE_NONE, DBTREE::decode_char_number( "hello world", n_in, out_char, n_out, false ) ); EXPECT_EQ( 0, n_in ); EXPECT_STREQ( "", out_char ); EXPECT_EQ( 0, n_out ); } TEST_F(DBTREE_DecodeCharNumberTest, prefix_only) { char out_char[16]{}; int n_in; int n_out; // 数値文字参照の3文字目をチェックするのでヌル終端を除いて長さ2未満の文字列は未定義動作になる EXPECT_EQ( DBTREE::NODE_NONE, DBTREE::decode_char_number( "&#", n_in, out_char, n_out, false ) ); EXPECT_EQ( 0, n_in ); EXPECT_STREQ( "", out_char ); EXPECT_EQ( 0, n_out ); } TEST_F(DBTREE_DecodeCharNumberTest, decimal_hiragana_a) { char out_char[16]{}; int n_in; int n_out; EXPECT_EQ( DBTREE::NODE_TEXT, DBTREE::decode_char_number( "あ", n_in, out_char, n_out, false ) ); EXPECT_EQ( 8, n_in ); EXPECT_STREQ( "\xE3\x81\x82", out_char ); EXPECT_EQ( 3, n_out ); } TEST_F(DBTREE_DecodeCharNumberTest, hexadecimal_hiragana_a) { char out_char[16]{}; int n_in; int n_out; EXPECT_EQ( DBTREE::NODE_TEXT, DBTREE::decode_char_number( "あ", n_in, out_char, n_out, false ) ); EXPECT_EQ( 8, n_in ); EXPECT_STREQ( "\xE3\x81\x82", out_char ); EXPECT_EQ( 3, n_out ); } TEST_F(DBTREE_DecodeCharNumberTest, non_digits) { char out_char[16]{}; int n_in; int n_out; EXPECT_EQ( DBTREE::NODE_NONE, DBTREE::decode_char_number( "&#qux;", n_in, out_char, n_out, false ) ); EXPECT_EQ( 0, n_in ); EXPECT_STREQ( "", out_char ); EXPECT_EQ( 0, n_out ); } TEST_F(DBTREE_DecodeCharNumberTest, invalid_sequence) { // '#' の後に続く数字のみ考慮する char out_char[16]{}; int n_in; int n_out; EXPECT_EQ( DBTREE::NODE_TEXT, DBTREE::decode_char_number( "あhoge7;", n_in, out_char, n_out, false ) ); EXPECT_EQ( 7, n_in ); EXPECT_STREQ( "\xE3\x81\x82", out_char ); EXPECT_EQ( 3, n_out ); EXPECT_EQ( DBTREE::NODE_TEXT, DBTREE::decode_char_number( "あhoge9;", n_in, out_char, n_out, false ) ); EXPECT_EQ( 7, n_in ); EXPECT_STREQ( "\xE3\x81\x82", out_char ); EXPECT_EQ( 3, n_out ); } TEST_F(DBTREE_DecodeCharNumberTest, without_semicoron) { // '#' の後に続く数字のみ考慮する char out_char[16]{}; int n_in; int n_out; EXPECT_EQ( DBTREE::NODE_TEXT, DBTREE::decode_char_number( "{", n_in, out_char, n_out, false ) ); EXPECT_EQ( 5, n_in ); EXPECT_STREQ( "{", out_char ); EXPECT_EQ( 1, n_out ); EXPECT_EQ( DBTREE::NODE_TEXT, DBTREE::decode_char_number( "{", n_in, out_char, n_out, false ) ); EXPECT_EQ( 5, n_in ); EXPECT_STREQ( "{", out_char ); EXPECT_EQ( 1, n_out ); } TEST_F(DBTREE_DecodeCharNumberTest, padding_zeros) { char out_char[16]{}; int n_in; int n_out; EXPECT_EQ( DBTREE::NODE_TEXT, DBTREE::decode_char_number( "a", n_in, out_char, n_out, false ) ); EXPECT_EQ( 7, n_in ); EXPECT_STREQ( "a", out_char ); EXPECT_EQ( 1, n_out ); EXPECT_EQ( DBTREE::NODE_TEXT, DBTREE::decode_char_number( "A", n_in, out_char, n_out, false ) ); EXPECT_EQ( 8, n_in ); EXPECT_STREQ( "A", out_char ); EXPECT_EQ( 1, n_out ); } TEST_F(DBTREE_DecodeCharNumberTest, zwsp_u200B) { char out_char[16]{}; int n_in; int n_out; // zwsp(U+200B) は今のところ空文字列にする EXPECT_EQ( DBTREE::NODE_ZWSP, DBTREE::decode_char_number( "​", n_in, out_char, n_out, false ) ); EXPECT_EQ( 8, n_in ); EXPECT_STREQ( "", out_char ); EXPECT_EQ( 0, n_out ); } TEST_F(DBTREE_DecodeCharNumberTest, zwnj_zwj_lrm_rlm) { char out_char[16]{}; int n_in; int n_out; EXPECT_EQ( DBTREE::NODE_TEXT, DBTREE::decode_char_number( "‌", n_in, out_char, n_out, false ) ); EXPECT_EQ( 8, n_in ); EXPECT_STREQ( "\xE2\x80\x8C", out_char ); EXPECT_EQ( 3, n_out ); EXPECT_EQ( DBTREE::NODE_TEXT, DBTREE::decode_char_number( "‍", n_in, out_char, n_out, false ) ); EXPECT_EQ( 8, n_in ); EXPECT_STREQ( "\xE2\x80\x8D", out_char ); EXPECT_EQ( 3, n_out ); EXPECT_EQ( DBTREE::NODE_TEXT, DBTREE::decode_char_number( "‎", n_in, out_char, n_out, false ) ); EXPECT_EQ( 8, n_in ); EXPECT_STREQ( "\xE2\x80\x8E", out_char ); EXPECT_EQ( 3, n_out ); EXPECT_EQ( DBTREE::NODE_TEXT, DBTREE::decode_char_number( "‏", n_in, out_char, n_out, false ) ); EXPECT_EQ( 8, n_in ); EXPECT_STREQ( "\xE2\x80\x8F", out_char ); EXPECT_EQ( 3, n_out ); } TEST_F(DBTREE_DecodeCharNumberTest, line_separator_u2028) { char out_char[16]{}; int n_in; int n_out; // U+2028 LINE SEPARATOR を描画処理に渡すと改行が乱れるため空白に置き換える (webブラウザと同じ挙動) EXPECT_EQ( DBTREE::NODE_TEXT, DBTREE::decode_char_number( "
", n_in, out_char, n_out, false ) ); EXPECT_EQ( 8, n_in ); EXPECT_STREQ( " ", out_char ); EXPECT_EQ( 1, n_out ); } TEST_F(DBTREE_DecodeCharNumberTest, do_not_correct_surrogate_pair) { char out_char[16]{}; int n_in; int n_out; // U+1F600 GRINNING FACE constexpr const char* emoji = "��"; EXPECT_EQ( DBTREE::NODE_TEXT, DBTREE::decode_char_number( emoji, n_in, out_char, n_out, false ) ); EXPECT_EQ( 8, n_in ); // サロゲートペアをデコードしないときは1つ目の数値文字参照だけ U+FFFD REPLACEMENT CHARACTER に変換する EXPECT_STREQ( "\xEF\xBF\xBD", out_char ); EXPECT_EQ( 3, n_out ); } TEST_F(DBTREE_DecodeCharNumberTest, correct_surrogate_pair) { char out_char[16]{}; int n_in; int n_out; constexpr bool correct_surroagete = true; // U+1F600 GRINNING FACE constexpr const char* emoji = "��"; EXPECT_EQ( DBTREE::NODE_TEXT, DBTREE::decode_char_number( emoji, n_in, out_char, n_out, correct_surroagete ) ); EXPECT_EQ( 16, n_in ); EXPECT_STREQ( "\xf0\x9f\x98\x80", out_char ); EXPECT_EQ( 4, n_out ); } TEST_F(DBTREE_DecodeCharNumberTest, only_high_surrogate) { char out_char[16]{}; int n_in; int n_out; constexpr bool correct_surroagete = true; constexpr const char* chrefs = "�"; EXPECT_EQ( DBTREE::NODE_TEXT, DBTREE::decode_char_number( chrefs, n_in, out_char, n_out, correct_surroagete ) ); EXPECT_EQ( 8, n_in ); EXPECT_STREQ( "\xEF\xBF\xBD", out_char ); EXPECT_EQ( 3, n_out ); } TEST_F(DBTREE_DecodeCharNumberTest, following_no_low_surrogate) { char out_char[16]{}; int n_in; int n_out; constexpr bool correct_surroagete = true; constexpr const char* chrefs = "�あ"; EXPECT_EQ( DBTREE::NODE_TEXT, DBTREE::decode_char_number( chrefs, n_in, out_char, n_out, correct_surroagete ) ); EXPECT_EQ( 8, n_in ); EXPECT_STREQ( "\xEF\xBF\xBD", out_char ); EXPECT_EQ( 3, n_out ); } class DBTREE_DecodeCharNameTest : public ::testing::Test {}; TEST_F(DBTREE_DecodeCharNameTest, ampersand_only) { char out_char[16]{}; int n_in; int n_out; // 文字実体参照の2文字目をチェックするのでヌル終端を除いて長さ0の文字列は未定義動作になる EXPECT_EQ( DBTREE::NODE_NONE, DBTREE::decode_char_name( "&", n_in, out_char, n_out ) ); EXPECT_EQ( 0, n_in ); EXPECT_STREQ( "", out_char ); EXPECT_EQ( 0, n_out ); } TEST_F(DBTREE_DecodeCharNameTest, non_charref) { char out_char[16]{}; int n_in; int n_out; EXPECT_EQ( DBTREE::NODE_NONE, DBTREE::decode_char_name( "hello world", n_in, out_char, n_out ) ); EXPECT_EQ( 0, n_in ); EXPECT_STREQ( "", out_char ); EXPECT_EQ( 0, n_out ); } TEST_F(DBTREE_DecodeCharNameTest, invalid_name) { char out_char[16]{}; int n_in; int n_out; EXPECT_EQ( DBTREE::NODE_NONE, DBTREE::decode_char_name( "&foobar;", n_in, out_char, n_out ) ); EXPECT_EQ( 0, n_in ); EXPECT_STREQ( "", out_char ); EXPECT_EQ( 0, n_out ); } TEST_F(DBTREE_DecodeCharNameTest, without_semicoron) { char out_char[16]{}; int n_in; int n_out; EXPECT_EQ( DBTREE::NODE_NONE, DBTREE::decode_char_name( "&hearts", n_in, out_char, n_out ) ); EXPECT_EQ( 0, n_in ); EXPECT_STREQ( "", out_char ); EXPECT_EQ( 0, n_out ); } TEST_F(DBTREE_DecodeCharNameTest, u0022) { char out_char[16]{}; int n_in; int n_out; EXPECT_EQ( DBTREE::NODE_TEXT, DBTREE::decode_char_name( """, n_in, out_char, n_out ) ); EXPECT_EQ( 6, n_in ); EXPECT_STREQ( "\x22", out_char ); EXPECT_EQ( 1, n_out ); EXPECT_EQ( DBTREE::NODE_TEXT, DBTREE::decode_char_name( """, n_in, out_char, n_out ) ); EXPECT_EQ( 6, n_in ); EXPECT_STREQ( "\x22", out_char ); EXPECT_EQ( 1, n_out ); } TEST_F(DBTREE_DecodeCharNameTest, u0026) { char out_char[16]{}; int n_in; int n_out; EXPECT_EQ( DBTREE::NODE_TEXT, DBTREE::decode_char_name( "&", n_in, out_char, n_out ) ); EXPECT_EQ( 5, n_in ); EXPECT_STREQ( "\x26", out_char ); EXPECT_EQ( 1, n_out ); EXPECT_EQ( DBTREE::NODE_TEXT, DBTREE::decode_char_name( "&", n_in, out_char, n_out ) ); EXPECT_EQ( 5, n_in ); EXPECT_STREQ( "\x26", out_char ); EXPECT_EQ( 1, n_out ); } TEST_F(DBTREE_DecodeCharNameTest, u0391) { char out_char[16]{}; int n_in; int n_out; EXPECT_EQ( DBTREE::NODE_TEXT, DBTREE::decode_char_name( "Α", n_in, out_char, n_out ) ); EXPECT_EQ( 7, n_in ); EXPECT_STREQ( "\xCE\x91", out_char ); EXPECT_EQ( 2, n_out ); } TEST_F(DBTREE_DecodeCharNameTest, u2233) { char out_char[16]{}; int n_in; int n_out; EXPECT_EQ( DBTREE::NODE_TEXT, DBTREE::decode_char_name( "∳", n_in, out_char, n_out ) ); EXPECT_EQ( 10, n_in ); EXPECT_STREQ( "\xE2\x88\xB3", out_char ); EXPECT_EQ( 3, n_out ); // 一番長い名前付き文字参照 EXPECT_EQ( DBTREE::NODE_TEXT, DBTREE::decode_char_name( "∳", n_in, out_char, n_out ) ); EXPECT_EQ( 33, n_in ); EXPECT_STREQ( "\xE2\x88\xB3", out_char ); EXPECT_EQ( 3, n_out ); } TEST_F(DBTREE_DecodeCharNameTest, lt_cases) { char out_char[16]{}; int n_in; int n_out; EXPECT_EQ( DBTREE::NODE_TEXT, DBTREE::decode_char_name( "<", n_in, out_char, n_out ) ); EXPECT_EQ( 4, n_in ); EXPECT_STREQ( "\x3C", out_char ); EXPECT_EQ( 1, n_out ); EXPECT_EQ( DBTREE::NODE_TEXT, DBTREE::decode_char_name( "<", n_in, out_char, n_out ) ); EXPECT_EQ( 4, n_in ); EXPECT_STREQ( "\x3C", out_char ); EXPECT_EQ( 1, n_out ); // < や < と異なる文字 EXPECT_EQ( DBTREE::NODE_TEXT, DBTREE::decode_char_name( "≪", n_in, out_char, n_out ) ); EXPECT_EQ( 4, n_in ); EXPECT_STREQ( "\xE2\x89\xAA", out_char ); EXPECT_EQ( 3, n_out ); // 存在しない EXPECT_EQ( DBTREE::NODE_NONE, DBTREE::decode_char_name( "&lT;", n_in, out_char, n_out ) ); EXPECT_EQ( 0, n_in ); EXPECT_STREQ( "", out_char ); EXPECT_EQ( 0, n_out ); } TEST_F(DBTREE_DecodeCharNameTest, gt_cases) { char out_char[16]{}; int n_in; int n_out; EXPECT_EQ( DBTREE::NODE_TEXT, DBTREE::decode_char_name( ">", n_in, out_char, n_out ) ); EXPECT_EQ( 4, n_in ); EXPECT_STREQ( "\x3E", out_char ); EXPECT_EQ( 1, n_out ); EXPECT_EQ( DBTREE::NODE_TEXT, DBTREE::decode_char_name( ">", n_in, out_char, n_out ) ); EXPECT_EQ( 4, n_in ); EXPECT_STREQ( "\x3E", out_char ); EXPECT_EQ( 1, n_out ); // > や > と異なる文字 EXPECT_EQ( DBTREE::NODE_TEXT, DBTREE::decode_char_name( "≫", n_in, out_char, n_out ) ); EXPECT_EQ( 4, n_in ); EXPECT_STREQ( "\xE2\x89\xAB", out_char ); EXPECT_EQ( 3, n_out ); // 存在しない EXPECT_EQ( DBTREE::NODE_NONE, DBTREE::decode_char_name( "&gT;", n_in, out_char, n_out ) ); EXPECT_EQ( 0, n_in ); EXPECT_STREQ( "", out_char ); EXPECT_EQ( 0, n_out ); } TEST_F(DBTREE_DecodeCharNameTest, u1D504) { char out_char[16]{}; int n_in; int n_out; EXPECT_EQ( DBTREE::NODE_TEXT, DBTREE::decode_char_name( "𝔄", n_in, out_char, n_out ) ); EXPECT_EQ( 5, n_in ); EXPECT_STREQ( "\xF0\x9D\x94\x84", out_char ); EXPECT_EQ( 4, n_out ); } TEST_F(DBTREE_DecodeCharNameTest, u003D_u20E5) { char out_char[16]{}; int n_in; int n_out; EXPECT_EQ( DBTREE::NODE_TEXT, DBTREE::decode_char_name( "=⃥", n_in, out_char, n_out ) ); EXPECT_EQ( 5, n_in ); EXPECT_STREQ( "\x3D\xE2\x83\xA5", out_char ); EXPECT_EQ( 4, n_out ); } TEST_F(DBTREE_DecodeCharNameTest, u0066_u006A) { char out_char[16]{}; int n_in; int n_out; EXPECT_EQ( DBTREE::NODE_TEXT, DBTREE::decode_char_name( "fj", n_in, out_char, n_out ) ); EXPECT_EQ( 7, n_in ); EXPECT_STREQ( "\x66\x6A", out_char ); EXPECT_EQ( 2, n_out ); } TEST_F(DBTREE_DecodeCharNameTest, u222D_u0331) { char out_char[16]{}; int n_in; int n_out; EXPECT_EQ( DBTREE::NODE_TEXT, DBTREE::decode_char_name( "∽̱", n_in, out_char, n_out ) ); EXPECT_EQ( 6, n_in ); EXPECT_STREQ( "\xE2\x88\xBD\xCC\xB1", out_char ); EXPECT_EQ( 5, n_out ); } TEST_F(DBTREE_DecodeCharNameTest, u228B_uFE00) { char out_char[16]{}; int n_in; int n_out; EXPECT_EQ( DBTREE::NODE_TEXT, DBTREE::decode_char_name( "⊋︀", n_in, out_char, n_out ) ); EXPECT_EQ( 8, n_in ); EXPECT_STREQ( "\xE2\x8A\x8B\xEF\xB8\x80", out_char ); EXPECT_EQ( 6, n_out ); } TEST_F(DBTREE_DecodeCharNameTest, convert_to_zwsp) { char out_char[16]{}; int n_in; int n_out; // ZWSP(U+200B)に変換される文字参照は今のところ空文字列にする EXPECT_EQ( DBTREE::NODE_ZWSP, DBTREE::decode_char_name( "​", n_in, out_char, n_out ) ); EXPECT_EQ( 21, n_in ); EXPECT_STREQ( "", out_char ); EXPECT_EQ( 0, n_out ); EXPECT_EQ( DBTREE::NODE_ZWSP, DBTREE::decode_char_name( "​", n_in, out_char, n_out ) ); EXPECT_EQ( 20, n_in ); EXPECT_STREQ( "", out_char ); EXPECT_EQ( 0, n_out ); EXPECT_EQ( DBTREE::NODE_ZWSP, DBTREE::decode_char_name( "​", n_in, out_char, n_out ) ); EXPECT_EQ( 19, n_in ); EXPECT_STREQ( "", out_char ); EXPECT_EQ( 0, n_out ); EXPECT_EQ( DBTREE::NODE_ZWSP, DBTREE::decode_char_name( "​", n_in, out_char, n_out ) ); EXPECT_EQ( 23, n_in ); EXPECT_STREQ( "", out_char ); EXPECT_EQ( 0, n_out ); EXPECT_EQ( DBTREE::NODE_ZWSP, DBTREE::decode_char_name( "​", n_in, out_char, n_out ) ); EXPECT_EQ( 16, n_in ); EXPECT_STREQ( "", out_char ); EXPECT_EQ( 0, n_out ); } TEST_F(DBTREE_DecodeCharNameTest, zwnj_zwj_lrm_rlm) { char out_char[16]{}; int n_in; int n_out; EXPECT_EQ( DBTREE::NODE_TEXT, DBTREE::decode_char_name( "‌", n_in, out_char, n_out ) ); EXPECT_EQ( 6, n_in ); EXPECT_STREQ( "\xE2\x80\x8C", out_char ); EXPECT_EQ( 3, n_out ); EXPECT_EQ( DBTREE::NODE_TEXT, DBTREE::decode_char_name( "‍", n_in, out_char, n_out ) ); EXPECT_EQ( 5, n_in ); EXPECT_STREQ( "\xE2\x80\x8D", out_char ); EXPECT_EQ( 3, n_out ); EXPECT_EQ( DBTREE::NODE_TEXT, DBTREE::decode_char_name( "‎", n_in, out_char, n_out ) ); EXPECT_EQ( 5, n_in ); EXPECT_STREQ( "\xE2\x80\x8E", out_char ); EXPECT_EQ( 3, n_out ); EXPECT_EQ( DBTREE::NODE_TEXT, DBTREE::decode_char_name( "‏", n_in, out_char, n_out ) ); EXPECT_EQ( 5, n_in ); EXPECT_STREQ( "\xE2\x80\x8F", out_char ); EXPECT_EQ( 3, n_out ); } TEST_F(DBTREE_DecodeCharNameTest, u200A) { // ゼロ幅文字 ZWSP(U+200B) の処理がはみ出していないか境界チェック char out_char[16]{}; int n_in; int n_out; EXPECT_EQ( DBTREE::NODE_TEXT, DBTREE::decode_char_name( " ", n_in, out_char, n_out ) ); EXPECT_EQ( 15, n_in ); EXPECT_STREQ( "\xE2\x80\x8A", out_char ); EXPECT_EQ( 3, n_out ); EXPECT_EQ( DBTREE::NODE_TEXT, DBTREE::decode_char_name( " ", n_in, out_char, n_out ) ); EXPECT_EQ( 8, n_in ); EXPECT_STREQ( "\xE2\x80\x8A", out_char ); EXPECT_EQ( 3, n_out ); } TEST_F(DBTREE_DecodeCharNameTest, u2010) { // ゼロ幅文字 rlm(U+200F) の処理がはみ出していないか境界チェック char out_char[16]{}; int n_in; int n_out; EXPECT_EQ( DBTREE::NODE_TEXT, DBTREE::decode_char_name( "‐", n_in, out_char, n_out ) ); EXPECT_EQ( 8, n_in ); EXPECT_STREQ( "\xE2\x80\x90", out_char ); EXPECT_EQ( 3, n_out ); EXPECT_EQ( DBTREE::NODE_TEXT, DBTREE::decode_char_name( "‐", n_in, out_char, n_out ) ); EXPECT_EQ( 6, n_in ); EXPECT_STREQ( "\xE2\x80\x90", out_char ); EXPECT_EQ( 3, n_out ); } } // namespace jdim-0.10.1/test/gtest_jdlib_cookiemanager.cpp000066400000000000000000000054641445721505100213720ustar00rootroot00000000000000// License: GPL2 #include "jdlib/cookiemanager.h" #include "gtest/gtest.h" namespace { class CookieManager_TestBase : public ::testing::Test { public: JDLIB::CookieManager* the_cookie_manager{}; void SetUp() override { the_cookie_manager = JDLIB::get_cookie_manager(); } void TearDown() override { JDLIB::delete_cookie_manager(); the_cookie_manager = nullptr; } }; class CookieManager_GetCookieByHost : public CookieManager_TestBase {}; TEST_F(CookieManager_GetCookieByHost, no_data) { const std::string expect = ""; EXPECT_EQ( expect, the_cookie_manager->get_cookie_by_host( "example.com" ) ); } TEST_F(CookieManager_GetCookieByHost, single_value) { the_cookie_manager->feed( "example.com", "foo=bar" ); const std::string expect = "foo=bar"; EXPECT_EQ( expect, the_cookie_manager->get_cookie_by_host( "example.com" ) ); } TEST_F(CookieManager_GetCookieByHost, multiple_values) { the_cookie_manager->feed( "example.com", "foo=bar" ); the_cookie_manager->feed( "example.com", "baz=qux" ); // 現在の実装ではクッキー名を辞書順でソートする const std::string expect = "baz=qux; foo=bar"; EXPECT_EQ( expect, the_cookie_manager->get_cookie_by_host( "example.com" ) ); } TEST_F(CookieManager_GetCookieByHost, empty_values) { the_cookie_manager->feed( "example.com", "foo=" ); the_cookie_manager->feed( "example.com", "bar=" ); const std::string expect = "bar=; foo="; EXPECT_EQ( expect, the_cookie_manager->get_cookie_by_host( "example.com" ) ); } TEST_F(CookieManager_GetCookieByHost, parsing_domain) { the_cookie_manager->feed( "first.hello.world.test", "foo=bar; domain=.world.test" ); the_cookie_manager->feed( "second.hello.world.test", "baz=qux" ); const std::string expect = "baz=qux; foo=bar"; EXPECT_EQ( expect, the_cookie_manager->get_cookie_by_host( "second.hello.world.test" ) ); } TEST_F(CookieManager_GetCookieByHost, parsing_path) { // ルート(/)以外のpathは無視する the_cookie_manager->feed( "example.com", "foo=bar; path=/" ); the_cookie_manager->feed( "example.com", "baz=qux; path=/user" ); const std::string expect = "foo=bar"; EXPECT_EQ( expect, the_cookie_manager->get_cookie_by_host( "example.com" ) ); } class CookieManager_DeleteCookieByHost : public CookieManager_TestBase {}; TEST_F(CookieManager_DeleteCookieByHost, delete_toplevel) { the_cookie_manager->feed( "hello.world.test", "foo=bar; domain=.world.test" ); the_cookie_manager->feed( "hello.world.test", "baz=qux;" ); // トップレベルのドメインまで削除される the_cookie_manager->delete_cookie_by_host( "hello.world.test" ); const std::string expect = ""; EXPECT_EQ( expect, the_cookie_manager->get_cookie_by_host( "hello.world.test" ) ); } } // namespace jdim-0.10.1/test/gtest_jdlib_jdiconv.cpp000066400000000000000000000151551445721505100202200ustar00rootroot00000000000000// License: GPL2 #include "jdencoding.h" #include "jdlib/jdiconv.h" #include "gtest/gtest.h" #include namespace { // エンコーディング変換は無数の組み合わせがあるためテストケースを網羅できない // JDim側で特別な処理をするパターンについてテストする class Iconv_ToAsciiFromUtf8 : public ::testing::Test {}; TEST_F(Iconv_ToAsciiFromUtf8, empty) { char input[] = ""; constexpr bool broken_sjis_be_utf8 = false; JDLIB::Iconv icv( Encoding::ascii, Encoding::utf8, broken_sjis_be_utf8 ); const std::string& result = icv.convert( input, std::strlen(input) ); EXPECT_EQ( "", result ); EXPECT_EQ( 0, result.size() ); } TEST_F(Iconv_ToAsciiFromUtf8, helloworld) { char input[] = "hello world!\n"; constexpr bool broken_sjis_be_utf8 = false; JDLIB::Iconv icv( Encoding::ascii, Encoding::utf8, broken_sjis_be_utf8 ); const std::string& result = icv.convert( input, std::strlen(input) ); EXPECT_EQ( "hello world!\n", result ); EXPECT_EQ( 13, result.size() ); } TEST_F(Iconv_ToAsciiFromUtf8, hiragana) { char input[] = "あいうえお"; constexpr bool broken_sjis_be_utf8 = false; JDLIB::Iconv icv( Encoding::ascii, Encoding::utf8, broken_sjis_be_utf8 ); const std::string& result = icv.convert( input, std::strlen(input) ); EXPECT_EQ( "あいうえお", result ); EXPECT_EQ( 40, result.size() ); } TEST_F(Iconv_ToAsciiFromUtf8, subdivision_flag) { // :england: JDLIB::Iconv::convert()のコメントを参照 char input[] = "\U0001F3F4\U000E0067\U000E0062\U000E0065\U000E006E\U000E0067\U000E007F"; constexpr bool broken_sjis_be_utf8 = false; JDLIB::Iconv icv( Encoding::ascii, Encoding::utf8, broken_sjis_be_utf8 ); const std::string& result = icv.convert( input, std::strlen(input) ); EXPECT_EQ( "🏴󠁧󠁢󠁥󠁮󠁧󠁿", result ); EXPECT_EQ( 63, result.size() ); } class Iconv_ToUtf8FromAscii : public ::testing::Test {}; TEST_F(Iconv_ToUtf8FromAscii, replacement_character_to_utf8_is_uFFFD) { // テストは網羅してない char input[] = "\x80\x91\xA2\xB3\xC4\xD5\xE6\xF7"; constexpr bool broken_sjis_be_utf8 = false; JDLIB::Iconv icv( Encoding::utf8, Encoding::ascii, broken_sjis_be_utf8 ); const std::string& result = icv.convert( input, std::strlen(input) ); // UTF-8へ変換するとき入力エンコーディングで無効なバイト列は U+FFFD に置き換える EXPECT_EQ( "\xEF\xBF\xBD\xEF\xBF\xBD\xEF\xBF\xBD\xEF\xBF\xBD" "\xEF\xBF\xBD\xEF\xBF\xBD\xEF\xBF\xBD\xEF\xBF\xBD", result ); EXPECT_EQ( 24, result.size() ); } class Iconv_ToUtf8FromMs932 : public ::testing::Test {}; TEST_F(Iconv_ToUtf8FromMs932, empty) { char input[] = ""; constexpr bool broken_sjis_be_utf8 = false; JDLIB::Iconv icv( Encoding::utf8, Encoding::sjis, broken_sjis_be_utf8 ); const std::string& result = icv.convert( input, std::strlen(input) ); EXPECT_EQ( "", result ); EXPECT_EQ( 0, result.size() ); } TEST_F(Iconv_ToUtf8FromMs932, helloworld) { char input[] = "hello world!\n"; constexpr bool broken_sjis_be_utf8 = false; JDLIB::Iconv icv( Encoding::utf8, Encoding::sjis, broken_sjis_be_utf8 ); const std::string& result = icv.convert( input, std::strlen(input) ); EXPECT_EQ( "hello world!\n", result ); EXPECT_EQ( 13, result.size() ); } TEST_F(Iconv_ToUtf8FromMs932, hiragana) { char input[] = "\x82\xA0\x82\xA2\x82\xA4\x82\xA6\x82\xA8"; constexpr bool broken_sjis_be_utf8 = false; JDLIB::Iconv icv( Encoding::utf8, Encoding::sjis, broken_sjis_be_utf8 ); const std::string& result = icv.convert( input, std::strlen(input) ); EXPECT_EQ( "あいうえお", result ); EXPECT_EQ( 15, result.size() ); } TEST_F(Iconv_ToUtf8FromMs932, hex_a0) { char input[] = "hello\xa0world!\n"; constexpr bool broken_sjis_be_utf8 = false; JDLIB::Iconv icv( Encoding::utf8, Encoding::sjis, broken_sjis_be_utf8 ); const std::string& result = icv.convert( input, std::strlen(input) ); EXPECT_EQ( "hello world!\n", result ); EXPECT_EQ( 13, result.size() ); } TEST_F(Iconv_ToUtf8FromMs932, mojibake_fix_inequality_sign_pattern1) { // DATのデータ区切り <> が文字化けするとスレが壊れるため変換を修正する // エンコーディングがMS932のスレにUTF-8で書き込み文字化けした場合をテスト char input[] = "<>test テスト<>"; constexpr bool broken_sjis_be_utf8 = false; JDLIB::Iconv icv( Encoding::utf8, Encoding::sjis, broken_sjis_be_utf8 ); const std::string& result = icv.convert( input, std::strlen(input) ); EXPECT_EQ( "<>test \xE7\xB9\x9D\xE2\x96\xA1\xE3\x81\x9B\xE7\xB9\x9D?<>", result ); EXPECT_EQ( 22, result.size() ); } TEST_F(Iconv_ToUtf8FromMs932, mojibake_fix_inequality_sign_pattern2) { // DATのデータ区切り <> が文字化けするとスレが壊れるため変換を修正する // エンコーディングがMS932のスレにUTF-8で書き込み文字化けした場合をテスト char input[] = "<> test テスト <>"; constexpr bool broken_sjis_be_utf8 = false; JDLIB::Iconv icv( Encoding::utf8, Encoding::sjis, broken_sjis_be_utf8 ); const std::string& result = icv.convert( input, std::strlen(input) ); EXPECT_EQ( "<> test \xE7\xB9\x9D\xE2\x96\xA1\xE3\x81\x9B\xE7\xB9\x9D\xE2\x96\xA1<>", result ); EXPECT_EQ( 25, result.size() ); } TEST_F(Iconv_ToUtf8FromMs932, mapping_error) { // MS932の符号として不正な2バイトコードは白四角(\x81\A0 == U+25A1)として処理する // テストは網羅してない char input[] = "\x81\xAD\x82\x40\x88\x90\x98\x90"; constexpr bool broken_sjis_be_utf8 = false; JDLIB::Iconv icv( Encoding::utf8, Encoding::sjis, broken_sjis_be_utf8 ); const std::string& result = icv.convert( input, std::strlen(input) ); EXPECT_EQ( "\u25A1\u25A1\u25A1\u25A1", result ); EXPECT_EQ( 12, result.size() ); } TEST_F(Iconv_ToUtf8FromMs932, broken_sjis_be_utf8) { char input[] = "\x82\xa0\x82\xa2\x82\xa4 " "\xe5\x85\xa5\xe3\x82\x8c\xe6\x9b\xbf\xe3\x82\x8f" "\xe3\x81\xa3\xe3\x81\xa6\xe3\x82\x8b\xe3\x80\x9c?" " \x82\xa6\x82\xa8"; constexpr bool broken_sjis_be_utf8 = true; JDLIB::Iconv icv( Encoding::utf8, Encoding::sjis, broken_sjis_be_utf8 ); const std::string& result = icv.convert( input, std::strlen(input) ); EXPECT_EQ( "あいう 入れ替わってる〜? えお", result ); EXPECT_EQ( 28 - 2 + 7 + 9 * 3 + 5 * 3, result.size() ); } } // namespace jdim-0.10.1/test/gtest_jdlib_jdregex.cpp000066400000000000000000000036541445721505100202150ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-2.0-only #include "jdlib/jdregex.h" #include "gtest/gtest.h" namespace { constexpr bool icase = false; constexpr bool newline = false; constexpr bool notbol = false; constexpr bool noteol = false; class Regex_NamedOrNumTest : public ::testing::Test {}; TEST_F(Regex_NamedOrNumTest, invalid_both_arguments) { const JDLIB::RegexPattern pattern( "(?foobar)", icase, newline ); JDLIB::Regex regex; const std::vector named_caps = { "name" }; EXPECT_TRUE( regex.match( pattern, "foobar", 0, notbol, noteol, named_caps ) ); constexpr std::size_t invalid_group = 10; EXPECT_EQ( regex.named_or_num( "invalid_name", invalid_group ), "" ); } TEST_F(Regex_NamedOrNumTest, prioritize_named_capture) { const JDLIB::RegexPattern pattern( "(?foobar) (fallback)", icase, newline ); JDLIB::Regex regex; const std::vector named_caps = { "name" }; EXPECT_TRUE( regex.match( pattern, "foobar fallback", 0, notbol, noteol, named_caps ) ); constexpr std::size_t valid_group = 2; EXPECT_EQ( regex.named_or_num( "name", valid_group ), "foobar" ); } TEST_F(Regex_NamedOrNumTest, unregistered_name) { const JDLIB::RegexPattern pattern( "(?foobar) (fallback)", icase, newline ); JDLIB::Regex regex; EXPECT_TRUE( regex.match( pattern, "foobar fallback", 0, notbol, noteol ) ); constexpr std::size_t valid_group = 2; EXPECT_EQ( regex.named_or_num( "name", valid_group ), "fallback" ); } TEST_F(Regex_NamedOrNumTest, register_invalid_name) { JDLIB::RegexPattern pattern( "(fallback) (?foobar)", icase, newline ); JDLIB::Regex regex; const std::vector named_caps = { "invalid_name" }; EXPECT_TRUE( regex.match( pattern, "fallback foobar", 0, notbol, noteol, named_caps ) ); constexpr std::size_t valid_group = 1; EXPECT_EQ( regex.named_or_num( "name", valid_group ), "fallback" ); } } // namespace jdim-0.10.1/test/gtest_jdlib_loader.cpp000066400000000000000000000227451445721505100200350ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-2.0-only #include "jdlib/loader.h" #include "gtest/gtest.h" #include #include namespace { struct ChunkedDecoderDataSet { const char* input{}; const char* output{}; std::size_t output_size{}; bool is_completed{}; }; class JDLIB_ChunkedDecoder_Decode : public ::testing::Test {}; TEST_F(JDLIB_ChunkedDecoder_Decode, empty) { JDLIB::ChunkedDecoder decoder; char buf[] = ""; std::size_t size = 0; EXPECT_TRUE( decoder.decode( buf, size ) ); EXPECT_EQ( std::string_view( buf, size ), "" ); EXPECT_EQ( size, 0 ); EXPECT_FALSE( decoder.is_completed() ); } TEST_F(JDLIB_ChunkedDecoder_Decode, last_chunk_only) { JDLIB::ChunkedDecoder decoder; char buf[] = "0\r\n"; std::size_t size = 3; EXPECT_TRUE( decoder.decode( buf, size ) ); EXPECT_EQ( std::string_view( buf, size ), "" ); EXPECT_EQ( size, 0 ); EXPECT_TRUE( decoder.is_completed() ); } TEST_F(JDLIB_ChunkedDecoder_Decode, one_chunk) { JDLIB::ChunkedDecoder decoder; char buf[] = "a\r\nhelloworld\r\n0\r\n\r\n"; std::size_t size = std::strlen( buf ); EXPECT_TRUE( decoder.decode( buf, size ) ); EXPECT_EQ( std::string_view( buf, size ), "helloworld" ); EXPECT_EQ( size, 10 ); EXPECT_TRUE( decoder.is_completed() ); } TEST_F(JDLIB_ChunkedDecoder_Decode, one_chunk_including_crlf) { JDLIB::ChunkedDecoder decoder; char buf[] = "C\r\nhello\r\nworld\r\n0\r\n\r\n"; std::size_t size = std::strlen( buf ); EXPECT_TRUE( decoder.decode( buf, size ) ); EXPECT_EQ( std::string_view( buf, size ), "hello\r\nworld" ); EXPECT_EQ( size, 12 ); EXPECT_TRUE( decoder.is_completed() ); } TEST_F(JDLIB_ChunkedDecoder_Decode, multiple_chunks) { JDLIB::ChunkedDecoder decoder; char buf[] = "5\r\nhello\r\n5\r\nworld\r\n0\r\n\r\n"; std::size_t size = std::strlen( buf ); EXPECT_TRUE( decoder.decode( buf, size ) ); EXPECT_EQ( std::string_view( buf, size ), "helloworld" ); EXPECT_EQ( size, 10 ); EXPECT_TRUE( decoder.is_completed() ); } TEST_F(JDLIB_ChunkedDecoder_Decode, chunk_ext) { JDLIB::ChunkedDecoder decoder; char buf[] = "5;foo=bar\r\nhello\r\n5;baz\r\nworld\r\n0\r\n\r\n"; std::size_t size = std::strlen( buf ); EXPECT_TRUE( decoder.decode( buf, size ) ); EXPECT_EQ( std::string_view( buf, size ), "helloworld" ); EXPECT_EQ( size, 10 ); EXPECT_TRUE( decoder.is_completed() ); } TEST_F(JDLIB_ChunkedDecoder_Decode, traier_part) { JDLIB::ChunkedDecoder decoder; char buf[] = "5\r\nhello\r\n5\r\nworld\r\n0\r\nAdditional: Data\r\n\r\n"; std::size_t size = std::strlen( buf ); EXPECT_TRUE( decoder.decode( buf, size ) ); EXPECT_EQ( std::string_view( buf, size ), "helloworld" ); EXPECT_EQ( size, 10 ); EXPECT_TRUE( decoder.is_completed() ); } TEST_F(JDLIB_ChunkedDecoder_Decode, multiple_time_feed_chunks) { constexpr const ChunkedDecoderDataSet chunks[] = { { "5\r\nhello\r\n", "hello", 5, false }, { "5\r\nworld\r\n", "world", 5, false }, { "0\r\n\r\n", "", 0, true }, }; JDLIB::ChunkedDecoder decoder; char buf[64]; std::size_t size; for( auto [input, output, output_size, is_completed] : chunks ) { std::strcpy( buf, input ); size = std::strlen( buf ); EXPECT_TRUE( decoder.decode( buf, size ) ); EXPECT_EQ( std::string_view( buf, size ), output ); EXPECT_EQ( size, output_size ); EXPECT_EQ( decoder.is_completed(), is_completed ); } } TEST_F(JDLIB_ChunkedDecoder_Decode, multiple_time_feed_crlf_fragmentation) { constexpr const ChunkedDecoderDataSet chunks[] = { { "5\r\nQuick", "Quick", 5, false }, { "\r\n5\r\nBrown\r", "Brown", 5, false }, { "\n3\r\nFox\r\n", "Fox", 3, false }, { "0\r\n\r\n", "", 0, true }, }; JDLIB::ChunkedDecoder decoder; char buf[64]; std::size_t size; for( auto [input, output, output_size, is_completed] : chunks ) { std::strcpy( buf, input ); size = std::strlen( buf ); EXPECT_TRUE( decoder.decode( buf, size ) ); EXPECT_EQ( std::string_view( buf, size ), output ); EXPECT_EQ( size, output_size ); EXPECT_EQ( decoder.is_completed(), is_completed ); } } TEST_F(JDLIB_ChunkedDecoder_Decode, multiple_time_feed_body_fragmentation) { constexpr const ChunkedDecoderDataSet chunks[] = { { "5\r", "", 0, false }, { "\nQuick\r\n5\r\nB", "QuickB", 6, false }, { "rown\r\n3\r\nFox\r\n5", "rownFox", 7, false }, { "\r\nJumps\r\n4\r\nOve", "JumpsOve", 8, false }, { "r\r\n0\r", "r", 1, false }, { "\n\r\n", "", 0, true }, }; JDLIB::ChunkedDecoder decoder; char buf[64]; std::size_t size; for( auto [input, output, output_size, is_completed] : chunks ) { std::strcpy( buf, input ); size = std::strlen( buf ); EXPECT_TRUE( decoder.decode( buf, size ) ); EXPECT_EQ( std::string_view( buf, size ), output ); EXPECT_EQ( size, output_size ); EXPECT_EQ( decoder.is_completed(), is_completed ); } } TEST_F(JDLIB_ChunkedDecoder_Decode, fail_pase_size) { constexpr const ChunkedDecoderDataSet chunks[] = { { "5\r\nQuick\r\n\r\n" }, { "5\r\nQuick\r\nZ\r\nBrown\r\n0\r\n\r\n" }, }; JDLIB::ChunkedDecoder decoder; char buf[64]; std::size_t size; for( auto [input, unused_1, unused_2, is_completed] : chunks ) { std::strcpy( buf, input ); size = std::strlen( buf ); EXPECT_FALSE( decoder.decode( buf, size ) ); EXPECT_FALSE( decoder.is_completed() ); decoder.clear(); } } TEST_F(JDLIB_ChunkedDecoder_Decode, fail_pase_body_cr_lf) { constexpr const ChunkedDecoderDataSet chunks[] = { { "5\r\nQuick\n0\r\n" }, { "5\r\nQuick\r\n5\r\nBrown\r0\r\n\r\n" }, }; JDLIB::ChunkedDecoder decoder; char buf[64]; std::size_t size; for( auto [input, unused_1, unused_2, is_completed] : chunks ) { std::strcpy( buf, input ); size = std::strlen( buf ); EXPECT_FALSE( decoder.decode( buf, size ) ); EXPECT_FALSE( decoder.is_completed() ); decoder.clear(); } } TEST_F(JDLIB_ChunkedDecoder_Decode, call_again_after_completed) { constexpr const ChunkedDecoderDataSet chunks[] = { { "F\r\nQuick Brown Fox\r\n0\r\n", "Quick Brown Fox", 15, true }, { "F\r\nQuick Brown Fox\r\n0\r\n", "", 0, true }, }; JDLIB::ChunkedDecoder decoder; char buf[64]; std::size_t size; for( auto [input, output, output_size, is_completed] : chunks ) { std::strcpy( buf, input ); size = std::strlen( buf ); EXPECT_TRUE( decoder.decode( buf, size ) ); EXPECT_EQ( std::string_view( buf, size ), output ); EXPECT_EQ( size, output_size ); EXPECT_TRUE( decoder.is_completed() ); } } class JDLIB_GzipDecoderTest : public ::testing::Test {}; TEST_F(JDLIB_GzipDecoderTest, default_construction) { JDLIB::GzipDecoder decoder; EXPECT_FALSE( decoder.is_decoding() ); } TEST_F(JDLIB_GzipDecoderTest, call_once_clear) { JDLIB::GzipDecoder decoder; decoder.clear(); EXPECT_FALSE( decoder.is_decoding() ); } TEST_F(JDLIB_GzipDecoderTest, setup_with_nullptr) { JDLIB::GzipDecoder decoder; EXPECT_TRUE( decoder.setup( 0, nullptr ) ); EXPECT_TRUE( decoder.is_decoding() ); decoder.clear(); EXPECT_FALSE( decoder.is_decoding() ); } TEST_F(JDLIB_GzipDecoderTest, setup_with_noop_lambda) { JDLIB::GzipDecoder decoder; EXPECT_TRUE( decoder.setup( 0, []( const char*, std::size_t ) {} ) ); EXPECT_TRUE( decoder.is_decoding() ); decoder.clear(); EXPECT_FALSE( decoder.is_decoding() ); } TEST_F(JDLIB_GzipDecoderTest, feed_empty_data) { JDLIB::GzipDecoder decoder; EXPECT_TRUE( decoder.setup( 16, nullptr ) ); EXPECT_TRUE( decoder.is_decoding() ); auto size = decoder.feed( "", 0 ); EXPECT_TRUE( size.has_value() ); EXPECT_EQ( *size, 0 ); decoder.clear(); EXPECT_FALSE( decoder.is_decoding() ); } TEST_F(JDLIB_GzipDecoderTest, feed_hello_world) { constexpr char encoded_data[] = "\x1f\x8b\x08\x00{\x00\x00\x00\x00\x03\x01\x0c\x00\xf3\xff" "Hello World!\xa3\x1c)\x1c\x0c\x00\x00\x00"; char output[128]{}; auto callback = [p = output]( const char* buf, std::size_t buf_size ) { std::strncpy( p, buf, buf_size ); }; JDLIB::GzipDecoder decoder; EXPECT_TRUE( decoder.setup( sizeof(encoded_data), callback ) ); EXPECT_TRUE( decoder.is_decoding() ); auto size = decoder.feed( encoded_data, sizeof(encoded_data) ); EXPECT_TRUE( size.has_value() ); EXPECT_EQ( *size, 12 ); EXPECT_STREQ( output, "Hello World!" ); decoder.clear(); EXPECT_FALSE( decoder.is_decoding() ); } TEST_F(JDLIB_GzipDecoderTest, feed_buffer_size_is_short) { constexpr char encoded_data[] = "\x1f\x8b\x08\x00{\x00\x00\x00\x00\x03\x01\x0c\x00\xf3\xff" "Hello World!\xa3\x1c)\x1c\x0c\x00\x00\x00"; char output[128]{}; auto callback = [p = output]( const char* buf, std::size_t buf_size ) { std::strncpy( p, buf, buf_size ); }; constexpr std::size_t too_short = 1; JDLIB::GzipDecoder decoder; EXPECT_TRUE( decoder.setup( too_short, callback ) ); EXPECT_TRUE( decoder.is_decoding() ); auto size = decoder.feed( encoded_data, sizeof(encoded_data) ); EXPECT_FALSE( size.has_value() ); EXPECT_STREQ( output, "" ); decoder.clear(); EXPECT_FALSE( decoder.is_decoding() ); } } // namespace jdim-0.10.1/test/gtest_jdlib_misccharcode.cpp000066400000000000000000000664571445721505100212230ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-2.0-only #include "jdlib/misccharcode.h" #include "gtest/gtest.h" namespace { class MISC_EncodingToCstrTest : public ::testing::Test {}; TEST_F(MISC_EncodingToCstrTest, unknown) { EXPECT_STREQ( "ISO-8859-1", MISC::encoding_to_cstr( Encoding::unknown ) ); } TEST_F(MISC_EncodingToCstrTest, ascii) { EXPECT_STREQ( "ASCII", MISC::encoding_to_cstr( Encoding::ascii ) ); } TEST_F(MISC_EncodingToCstrTest, eucjp) { EXPECT_STREQ( "EUC-JP", MISC::encoding_to_cstr( Encoding::eucjp ) ); } TEST_F(MISC_EncodingToCstrTest, jis) { EXPECT_STREQ( "ISO-2022-JP", MISC::encoding_to_cstr( Encoding::jis ) ); } TEST_F(MISC_EncodingToCstrTest, sjis) { EXPECT_STREQ( "Shift_JIS", MISC::encoding_to_cstr( Encoding::sjis ) ); } TEST_F(MISC_EncodingToCstrTest, utf8) { EXPECT_STREQ( "UTF-8", MISC::encoding_to_cstr( Encoding::utf8 ) ); } TEST_F(MISC_EncodingToCstrTest, invalid_enum) { EXPECT_STREQ( "ISO-8859-1", MISC::encoding_to_cstr( static_cast( -200 ) ) ); EXPECT_STREQ( "ISO-8859-1", MISC::encoding_to_cstr( static_cast( 200 ) ) ); } class MISC_EncodingToIconvCstrTest : public ::testing::Test {}; TEST_F(MISC_EncodingToIconvCstrTest, unknown) { EXPECT_STREQ( "ISO-8859-1", MISC::encoding_to_iconv_cstr( Encoding::unknown ) ); } TEST_F(MISC_EncodingToIconvCstrTest, ascii) { EXPECT_STREQ( "ASCII", MISC::encoding_to_iconv_cstr( Encoding::ascii ) ); } TEST_F(MISC_EncodingToIconvCstrTest, eucjp) { EXPECT_STREQ( "EUCJP-MS", MISC::encoding_to_iconv_cstr( Encoding::eucjp ) ); } TEST_F(MISC_EncodingToIconvCstrTest, jis) { EXPECT_STREQ( "ISO-2022-JP", MISC::encoding_to_iconv_cstr( Encoding::jis ) ); } TEST_F(MISC_EncodingToIconvCstrTest, sjis) { EXPECT_STREQ( "MS932", MISC::encoding_to_iconv_cstr( Encoding::sjis ) ); } TEST_F(MISC_EncodingToIconvCstrTest, utf8) { EXPECT_STREQ( "UTF-8", MISC::encoding_to_iconv_cstr( Encoding::utf8 ) ); } TEST_F(MISC_EncodingToIconvCstrTest, invalid_enum) { EXPECT_STREQ( "ISO-8859-1", MISC::encoding_to_iconv_cstr( static_cast( -200 ) ) ); EXPECT_STREQ( "ISO-8859-1", MISC::encoding_to_iconv_cstr( static_cast( 200 ) ) ); } class MISC_EncodingFromSvTest : public ::testing::Test {}; TEST_F(MISC_EncodingFromSvTest, iso_8859_1) { EXPECT_EQ( Encoding::unknown, MISC::encoding_from_sv( "ISO-8859-1" ) ); } TEST_F(MISC_EncodingFromSvTest, ascii) { EXPECT_EQ( Encoding::ascii, MISC::encoding_from_sv( "ASCII" ) ); } TEST_F(MISC_EncodingFromSvTest, eucjp_ms) { EXPECT_EQ( Encoding::eucjp, MISC::encoding_from_sv( "EUCJP-MS" ) ); EXPECT_EQ( Encoding::eucjp, MISC::encoding_from_sv( "EUC-JP" ) ); } TEST_F(MISC_EncodingFromSvTest, iso_2022_jp) { EXPECT_EQ( Encoding::jis, MISC::encoding_from_sv( "ISO-2022-JP" ) ); } TEST_F(MISC_EncodingFromSvTest, ms932) { EXPECT_EQ( Encoding::sjis, MISC::encoding_from_sv( "MS932" ) ); EXPECT_EQ( Encoding::sjis, MISC::encoding_from_sv( "Shift_JIS" ) ); } TEST_F(MISC_EncodingFromSvTest, utf8) { EXPECT_EQ( Encoding::utf8, MISC::encoding_from_sv( "UTF-8" ) ); } TEST_F(MISC_EncodingFromSvTest, invalid_encoding_name) { EXPECT_EQ( Encoding::unknown, MISC::encoding_from_sv( "" ) ); EXPECT_EQ( Encoding::unknown, MISC::encoding_from_sv( "INVALID-ENCODING-NAME" ) ); } TEST_F(MISC_EncodingFromSvTest, small_case_is_invalid) { EXPECT_EQ( Encoding::unknown, MISC::encoding_from_sv( "iso-8859-1" ) ); EXPECT_EQ( Encoding::unknown, MISC::encoding_from_sv( "ascii" ) ); EXPECT_EQ( Encoding::unknown, MISC::encoding_from_sv( "eucjp-ms" ) ); EXPECT_EQ( Encoding::unknown, MISC::encoding_from_sv( "iso-2022-jp" ) ); EXPECT_EQ( Encoding::unknown, MISC::encoding_from_sv( "ms932" ) ); EXPECT_EQ( Encoding::unknown, MISC::encoding_from_sv( "utf-8" ) ); } class MISC_EncodingFromWebCharsetTest : public ::testing::Test {}; TEST_F(MISC_EncodingFromWebCharsetTest, iso_8859_1) { EXPECT_EQ( Encoding::unknown, MISC::encoding_from_web_charset( "ISO-8859-1" ) ); EXPECT_EQ( Encoding::unknown, MISC::encoding_from_web_charset( "latin1" ) ); } TEST_F(MISC_EncodingFromWebCharsetTest, us_ascii) { EXPECT_EQ( Encoding::unknown, MISC::encoding_from_web_charset( "ASCII" ) ); EXPECT_EQ( Encoding::unknown, MISC::encoding_from_web_charset( "US-ASCII" ) ); } TEST_F(MISC_EncodingFromWebCharsetTest, euc_jp) { EXPECT_EQ( Encoding::eucjp, MISC::encoding_from_web_charset( "EUC-JP" ) ); EXPECT_EQ( Encoding::eucjp, MISC::encoding_from_web_charset( "x-euc-jp" ) ); } TEST_F(MISC_EncodingFromWebCharsetTest, iso_2022_jp) { EXPECT_EQ( Encoding::unknown, MISC::encoding_from_web_charset( "ISO-2022-JP" ) ); } TEST_F(MISC_EncodingFromWebCharsetTest, shift_jis) { EXPECT_EQ( Encoding::sjis, MISC::encoding_from_web_charset( "Shift_JIS" ) ); EXPECT_EQ( Encoding::sjis, MISC::encoding_from_web_charset( "Shift-JIS" ) ); EXPECT_EQ( Encoding::sjis, MISC::encoding_from_web_charset( "Windows-31J" ) ); EXPECT_EQ( Encoding::sjis, MISC::encoding_from_web_charset( "sjis" ) ); EXPECT_EQ( Encoding::sjis, MISC::encoding_from_web_charset( "x-sjis" ) ); EXPECT_EQ( Encoding::unknown, MISC::encoding_from_web_charset( "MS_Kanji" ) ); EXPECT_EQ( Encoding::unknown, MISC::encoding_from_web_charset( "MS932" ) ); } TEST_F(MISC_EncodingFromWebCharsetTest, utf8) { EXPECT_EQ( Encoding::utf8, MISC::encoding_from_web_charset( "UTF-8" ) ); EXPECT_EQ( Encoding::utf8, MISC::encoding_from_web_charset( "utf8" ) ); } class IsEucjpTest : public ::testing::Test {}; TEST_F(IsEucjpTest, null_data) { EXPECT_TRUE( MISC::is_eucjp( "", 0 ) ); } TEST_F(IsEucjpTest, ascii_only) { EXPECT_TRUE( MISC::is_eucjp( "!\"#$%&'()*+,-./ :;<=>?@ [\\]^_` {|}~", 0 ) ); EXPECT_TRUE( MISC::is_eucjp( "0123456789 abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ", 0 ) ); } TEST_F(IsEucjpTest, hiragana_katakana) { EXPECT_TRUE( MISC::is_eucjp( "\xA4\xA2\xA4\xA4\xA4\xA6\xA4\xA8\xA4\xAA", 0 ) ); // あいうえお EXPECT_TRUE( MISC::is_eucjp( "\xA5\xA2\xA5\xA4\xA5\xA6\xA5\xA8\xA5\xAA", 0 ) ); // アイウエオ } TEST_F(IsEucjpTest, fullwidth_alnum) { EXPECT_TRUE( MISC::is_eucjp( "\xA3\xB1\xA3\xB2\xA3\xB3\xA3\xB4\xA3\xB5", 0 ) ); // 12345 EXPECT_TRUE( MISC::is_eucjp( "\xA3\xC1\xA3\xC2\xA3\xC3\xA3\xC4\xA3\xC5", 0 ) ); // ABCDE EXPECT_TRUE( MISC::is_eucjp( "\xA3\xE1\xA3\xE2\xA3\xE3\xA3\xE4\xA3\xE5", 0 ) ); // abcde } TEST_F(IsEucjpTest, halfwidth_katakana) { EXPECT_TRUE( MISC::is_eucjp( "\x8E\xA7\x8E\xA8\x8E\xA9\x8E\xAA\x8E\xAB", 0 ) ); // アイウエオ } TEST_F(IsEucjpTest, three_byte_sub_kanji) { EXPECT_TRUE( MISC::is_eucjp( "\x8F\xB0\xD0\x8F\xB0\xD1\x8F\xB0\xD2\x8F\xB0\xD2\x8F\xB0\xD3", 0 ) ); // 仾仿伀 } TEST_F(IsEucjpTest, jis) { EXPECT_TRUE( MISC::is_eucjp( "\x1B$B$\"$$$&$($*\x1B(B", 0 ) ); } TEST_F(IsEucjpTest, sjis) { EXPECT_FALSE( MISC::is_eucjp( "\x82\xA0\x82\xA2\x82\xA4\x82\xA6\x82\xA8", 0 ) ); } TEST_F(IsEucjpTest, utf8) { EXPECT_FALSE( MISC::is_eucjp( "\u3042", 0 ) ); } TEST_F(IsEucjpTest, skip_data) { EXPECT_TRUE( MISC::is_eucjp( "\u3042\xA4\xA2\xA3\xB1", 3 ) ); } TEST_F(IsEucjpTest, lack_head_byte) { // hiragana EXPECT_FALSE( MISC::is_eucjp( "\xA2", 0 ) ); EXPECT_FALSE( MISC::is_eucjp( "\xA2\xA4\xA4", 0 ) ); // fullwidth EXPECT_FALSE( MISC::is_eucjp( "\xB1\xA3\xB2", 0 ) ); EXPECT_FALSE( MISC::is_eucjp( "\xC1\xA3\xC2", 0 ) ); EXPECT_FALSE( MISC::is_eucjp( "\xE1\xA3\xE2", 0 ) ); // halfwidth katakana EXPECT_FALSE( MISC::is_eucjp( "\xA7\x8E\xA8", 0 ) ); // three byte sub kanji EXPECT_FALSE( MISC::is_eucjp( "\xB0", 0 ) ); EXPECT_TRUE( MISC::is_eucjp( "\xB0\xD0", 0 ) ); EXPECT_TRUE( MISC::is_eucjp( "\xB0\xD0\x8F\xB0\xD1", 0 ) ); EXPECT_FALSE( MISC::is_eucjp( "\xD0", 0 ) ); EXPECT_FALSE( MISC::is_eucjp( "\xD0\x8F\xB0\xD1", 0 ) ); } TEST_F(IsEucjpTest, lack_following_bytes) { // hiragana EXPECT_FALSE( MISC::is_eucjp( "\xA4", 0 ) ); EXPECT_FALSE( MISC::is_eucjp( "\xA4\xA2\xA4", 0 ) ); // fullwidth EXPECT_FALSE( MISC::is_eucjp( "\xA3\xB1\xA3", 0 ) ); EXPECT_FALSE( MISC::is_eucjp( "\xA3\xC1\xA3", 0 ) ); EXPECT_FALSE( MISC::is_eucjp( "\xA3\xE1\xA3", 0 ) ); // halfwidth katakana EXPECT_FALSE( MISC::is_eucjp( "\x8E", 0 ) ); EXPECT_FALSE( MISC::is_eucjp( "\x8E\xA7\x8E", 0 ) ); // three byte sub kanji EXPECT_FALSE( MISC::is_eucjp( "\x8F", 0 ) ); EXPECT_FALSE( MISC::is_eucjp( "\x8F\xB0", 0 ) ); EXPECT_FALSE( MISC::is_eucjp( "\x8F\xB0\xD0\x8F", 0 ) ); EXPECT_FALSE( MISC::is_eucjp( "\x8F\xB0\xD0\x8F\xB0", 0 ) ); } class IsJisTest : public ::testing::Test {}; TEST_F(IsJisTest, null_data) { std::size_t byte = 0; EXPECT_FALSE( MISC::is_jis( "", byte ) ); } TEST_F(IsJisTest, ascii_only) { std::size_t byte = 0; EXPECT_FALSE( MISC::is_jis( "!\"#$%&'()*+,-./ :;<=>?@ [\\]^_` {|}~", byte ) ); EXPECT_EQ( 35, byte ); byte = 0; EXPECT_FALSE( MISC::is_jis( "0123456789 abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ", byte ) ); EXPECT_EQ( 64, byte ); } TEST_F(IsJisTest, hiragana_katakana) { std::size_t byte = 0; EXPECT_TRUE( MISC::is_jis( "\x1B$B$\"$$$&$($*\x1B(B", byte ) ); // あいうえお EXPECT_EQ( 0, byte ); EXPECT_TRUE( MISC::is_jis( "\x1B$B%\"%$%&%(%*\x1B(B", byte ) ); // アイウエオ EXPECT_EQ( 0, byte ); } TEST_F(IsJisTest, fullwidth_alnum) { std::size_t byte = 0; EXPECT_TRUE( MISC::is_jis( "\x1B$B!*!I!t!p!s\x1B(B", byte ) ); // !”#$% EXPECT_EQ( 0, byte ); EXPECT_TRUE( MISC::is_jis( "\x1B$B#1#2#3#4#5\x1B(B", byte ) ); // 12345 EXPECT_EQ( 0, byte ); EXPECT_TRUE( MISC::is_jis( "\x1B$B#A#B#C#D#E\x1B(B", byte ) ); // ABCDE EXPECT_EQ( 0, byte ); EXPECT_TRUE( MISC::is_jis( "\x1B$B#a#b#c#d#e\x1B(B", byte ) ); // abcde EXPECT_EQ( 0, byte ); } TEST_F(IsJisTest, eucjp) { std::size_t byte = 0; EXPECT_FALSE( MISC::is_jis( "\xA4\xA2\xA4\xA4\xA4\xA6\xA4\xA8\xA4\xAA", byte ) ); EXPECT_EQ( 0, byte ); } TEST_F(IsJisTest, sjis) { std::size_t byte = 0; EXPECT_FALSE( MISC::is_jis( "\x82\xA0\x82\xA2\x82\xA4\x82\xA6\x82\xA8", byte ) ); EXPECT_EQ( 0, byte ); } TEST_F(IsJisTest, utf8) { std::size_t byte = 0; EXPECT_FALSE( MISC::is_jis( "\u3042", byte ) ); // U+3042 EXPECT_EQ( 0, byte ); } TEST_F(IsJisTest, skip_data) { std::size_t byte = 3; EXPECT_TRUE( MISC::is_jis( "\u3042\x1B$B#A#B#C#D#E\x1B(B", byte ) ); // U+3042ABCDE EXPECT_EQ( 3, byte ); } class IsSjisTest : public ::testing::Test {}; TEST_F(IsSjisTest, null_data) { EXPECT_TRUE( MISC::is_sjis( "", 0 ) ); } TEST_F(IsSjisTest, ascii_only) { EXPECT_TRUE( MISC::is_sjis( "!\"#$%&'()*+,-./ :;<=>?@ [\\]^_` {|}~", 0 ) ); EXPECT_TRUE( MISC::is_sjis( "0123456789 abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ", 0 ) ); } TEST_F(IsSjisTest, hiragana_katakana) { EXPECT_TRUE( MISC::is_sjis( "\x82\xA0\x82\xA2\x82\xA4\x82\xA6\x82\xA8", 0 ) ); // あいうえお EXPECT_TRUE( MISC::is_sjis( "\x83\x41\x83\x43\x83\x45\x83\x47\x83\x49", 0 ) ); // アイウエオ } TEST_F(IsSjisTest, fullwidth_alnum) { EXPECT_TRUE( MISC::is_sjis( "\x81\x41\x81\x42\x81\x43\x81\x44\x81\x45", 0 ) ); // 、 。 , . ・ EXPECT_TRUE( MISC::is_sjis( "\x82\x50\x82\x51\x82\x52\x82\x53\x82\x54", 0 ) ); // 12345 EXPECT_TRUE( MISC::is_sjis( "\x82\x60\x82\x61\x82\x62\x82\x63\x82\x64", 0 ) ); // ABCDE EXPECT_TRUE( MISC::is_sjis( "\x82\x81\x82\x82\x82\x83\x82\x84\x82\x85", 0 ) ); // abcde } TEST_F(IsSjisTest, kanji) { EXPECT_TRUE( MISC::is_sjis( "\x89\x40\x89\x41\x89\x42\x89\x43\x89\x45", 0 ) ); // 院陰隠韻吋 } TEST_F(IsSjisTest, halfwidth_katakana) { EXPECT_TRUE( MISC::is_sjis( "\xB1\xB2\xB3\xB4\xB5", 0 ) ); // アイウエオ } TEST_F(IsSjisTest, eucjp) { EXPECT_TRUE( MISC::is_sjis( "\xA4\xA2\xA4\xA4\xA4\xA6\xA4\xA8\xA4\xAA", 0 ) ); } TEST_F(IsSjisTest, jis) { EXPECT_TRUE( MISC::is_sjis( "\x1B$B$\"$$$&$($*\x1B(B", 0 ) ); } TEST_F(IsSjisTest, utf8) { EXPECT_FALSE( MISC::is_sjis( "\u3042", 0 ) ); } TEST_F(IsSjisTest, skip_data) { EXPECT_TRUE( MISC::is_sjis( "\u3042\x82\xA0\x83\x41", 0 ) ); } TEST_F(IsSjisTest, lack_head_byte) { // 後続バイトがASCIIの値と同じ範囲の場合はASCII扱いでエラーにならない // hiragana EXPECT_FALSE( MISC::is_sjis( "\xA0", 0 ) ); EXPECT_TRUE( MISC::is_sjis( "\xA0\x82\xA2", 0 ) ); // fullwidth EXPECT_TRUE( MISC::is_sjis( "\x41", 0 ) ); EXPECT_TRUE( MISC::is_sjis( "\x41\x81\x42", 0 ) ); EXPECT_TRUE( MISC::is_sjis( "\x50\x82\x51", 0 ) ); EXPECT_TRUE( MISC::is_sjis( "\x60\x82\x61", 0 ) ); EXPECT_FALSE( MISC::is_sjis( "\x81\x82\x82", 0 ) ); // kanji EXPECT_TRUE( MISC::is_sjis( "\x40", 0 ) ); EXPECT_TRUE( MISC::is_sjis( "\x40\x89\x41", 0 ) ); } TEST_F(IsSjisTest, lack_following_bytes) { // hiragana EXPECT_FALSE( MISC::is_sjis( "\x82", 0 ) ); EXPECT_FALSE( MISC::is_sjis( "\x82\xA0\x82", 0 ) ); // fullwidth EXPECT_FALSE( MISC::is_sjis( "\x81\x41\x81", 0 ) ); EXPECT_FALSE( MISC::is_sjis( "\x82\x50\x82", 0 ) ); EXPECT_FALSE( MISC::is_sjis( "\x82\x60\x82", 0 ) ); EXPECT_FALSE( MISC::is_sjis( "\x82\x81\x82", 0 ) ); // kanji EXPECT_FALSE( MISC::is_sjis( "\x89", 0 ) ); EXPECT_FALSE( MISC::is_sjis( "\x89\x40\x89", 0 ) ); } class IsUtf8Test : public ::testing::Test {}; TEST_F(IsUtf8Test, null_data) { EXPECT_TRUE( MISC::is_utf8( "", 0 ) ); } TEST_F(IsUtf8Test, ascii_only) { EXPECT_TRUE( MISC::is_utf8( "!\"#$%&'()*+,-./ :;<=>?@ [\\]^_` {|}~", 0 ) ); EXPECT_TRUE( MISC::is_utf8( "0123456789 abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ", 0 ) ); } TEST_F(IsUtf8Test, two_bytes) { EXPECT_TRUE( MISC::is_utf8( "\xC2\x80", 0 ) ); // U+0080 EXPECT_TRUE( MISC::is_utf8( "\xDF\xBF", 0 ) ); // U+07FF } TEST_F(IsUtf8Test, three_bytes) { EXPECT_TRUE( MISC::is_utf8( "\xE0\xA0\x80", 0 ) ); // U+0800 EXPECT_TRUE( MISC::is_utf8( "\xEF\xBF\xBF", 0 ) ); // U+FFFF } TEST_F(IsUtf8Test, surrogate_pair) { // surrogate pair の universal character names はコンパイルエラーになった // error: \uD800 is not a valid universal character EXPECT_TRUE( MISC::is_utf8( "\xED\xA0\x80\xED\xAF\xBF", 0 ) ); // U+D800 U+DBFF EXPECT_TRUE( MISC::is_utf8( "\xED\xB0\x80\xED\xBF\xBF", 0 ) ); // U+DC00 U+DFFF } TEST_F(IsUtf8Test, four_bytes) { EXPECT_TRUE( MISC::is_utf8( "\xF0\x90\x80\x80", 0 ) ); // U+10000 EXPECT_TRUE( MISC::is_utf8( "\xF4\x8F\xBF\xBF", 0 ) ); // U+10FFFF } TEST_F(IsUtf8Test, obsolete_four_bytes) { // 廃止された RFC2279 // 簡易的なチェックのため先頭が F4 のシーケンスはtrueが返る EXPECT_TRUE( MISC::is_utf8( "\xF4\x90\x80\x80", 0 ) ); // U+110000 EXPECT_TRUE( MISC::is_utf8( "\xF4\xBF\xBF\xBF", 0 ) ); // U+13FFFF EXPECT_FALSE( MISC::is_utf8( "\xF5\x80\x80\x80", 0 ) ); // U+140000 EXPECT_FALSE( MISC::is_utf8( "\xF6\x80\x80\x80", 0 ) ); // U+180000 EXPECT_FALSE( MISC::is_utf8( "\xF7\x80\x80\x80", 0 ) ); // U+1C0000 EXPECT_FALSE( MISC::is_utf8( "\xF7\xBF\xBF\xBF", 0 ) ); // U+1FFFFF } TEST_F(IsUtf8Test, obsolete_five_bytes) { // 廃止された RFC2279 EXPECT_FALSE( MISC::is_utf8( "\xF8\x88\x80\x80\x80", 0 ) ); // U+200000 EXPECT_FALSE( MISC::is_utf8( "\xF9\x80\x80\x80\x80", 0 ) ); // U+1000000 EXPECT_FALSE( MISC::is_utf8( "\xFA\x80\x80\x80\x80", 0 ) ); // U+2000000 EXPECT_FALSE( MISC::is_utf8( "\xFB\x80\x80\x80\x80", 0 ) ); // U+3000000 EXPECT_FALSE( MISC::is_utf8( "\xFB\xBF\xBF\xBF\xBF", 0 ) ); // U+3FFFFFF } TEST_F(IsUtf8Test, obsolete_six_bytes) { // 廃止された RFC2279 EXPECT_FALSE( MISC::is_utf8( "\xFC\x84\x80\x80\x80\x80", 0 ) ); // U+4000000 EXPECT_FALSE( MISC::is_utf8( "\xFD\x80\x80\x80\x80\x80", 0 ) ); // U+40000000 EXPECT_FALSE( MISC::is_utf8( "\xFD\xBF\xBF\xBF\xBF\xBF", 0 ) ); // U+7FFFFFFF } TEST_F(IsUtf8Test, non_characters) { EXPECT_TRUE( MISC::is_utf8( "\uFDD0\uFDEF", 0 ) ); EXPECT_TRUE( MISC::is_utf8( "\uFFFE\uFFFF", 0 ) ); EXPECT_TRUE( MISC::is_utf8( "\U0001FF80\U0001FFFF", 0 ) ); } TEST_F(IsUtf8Test, invalid_byte) { // マルチバイトの後続部分 char invalid_seq[2]{}; for( int i = 0x80; i < 0xC0; ++i ) { invalid_seq[0] = i; EXPECT_FALSE( MISC::is_utf8( invalid_seq, 0 ) ); } // C0 C1 は禁止 EXPECT_FALSE( MISC::is_utf8( "\xC0", 0 ) ); EXPECT_FALSE( MISC::is_utf8( "\xC1", 0 ) ); // UTF-16, UTF-32 で使われるバイトオーダーマーク EXPECT_FALSE( MISC::is_utf8( "\xFE", 0 ) ); EXPECT_FALSE( MISC::is_utf8( "\xFF", 0 ) ); } TEST_F(IsUtf8Test, invalid_seq) { EXPECT_FALSE( MISC::is_utf8( "\xC2\x7F", 0 ) ); EXPECT_FALSE( MISC::is_utf8( "\xC2\xC0", 0 ) ); // 簡易的なチェックのためtrueが返る (右コメントは正しい範囲) EXPECT_TRUE( MISC::is_utf8( "\xE0\x9F\x80", 0 ) ); // E0 A0-BF 80-BF EXPECT_TRUE( MISC::is_utf8( "\xED\xA0\x80", 0 ) ); // ED 80-9F 80-BF EXPECT_TRUE( MISC::is_utf8( "\xF0\x8F\x80\x80", 0 ) ); // F0 90-BF 80-BF 80-BF } TEST_F(IsUtf8Test, eucjp) { EXPECT_FALSE( MISC::is_utf8( "\xA4\xA2\xA4\xA4\xA4\xA6\xA4\xA8\xA4\xAA", 0 ) ); } TEST_F(IsUtf8Test, jis) { EXPECT_TRUE( MISC::is_utf8( "\x1B$B$\"$$$&$($*\x1B(B", 0 ) ); } TEST_F(IsUtf8Test, sjis) { EXPECT_FALSE( MISC::is_utf8( "\x82\xA0\x82\xA2\x82\xA4\x82\xA6\x82\xA8", 0 ) ); } TEST_F(IsUtf8Test, skip_data) { EXPECT_TRUE( MISC::is_utf8( "\x82\xA0\u3042", 2 ) ); } TEST_F(IsUtf8Test, lack_following_bytes) { constexpr char one_byte[1] = { '\xC2' }; // C2 80 EXPECT_FALSE( MISC::is_utf8( std::string_view{ one_byte, 1 }, 0 ) ); constexpr char two_bytes[2] = { '\xE0', '\xA0' }; // E0 A0 80 EXPECT_FALSE( MISC::is_utf8( std::string_view{ two_bytes, 2 }, 0 ) ); constexpr char three_bytes[3] = { '\xF0', '\x90', '\x80' }; // F0 90 80 80 EXPECT_FALSE( MISC::is_utf8( std::string_view{ three_bytes, 3 }, 0 ) ); } class Utf8BytesTest : public ::testing::Test {}; TEST_F(Utf8BytesTest, null_data) { EXPECT_EQ( 0, MISC::utf8bytes( nullptr ) ); EXPECT_EQ( 0, MISC::utf8bytes( "" ) ); } TEST_F(Utf8BytesTest, ascii) { char ascii_seq[2]{}; for( int i = 1; i < 0x80; ++i ) { ascii_seq[0] = i; EXPECT_EQ( 1, MISC::utf8bytes( ascii_seq ) ); } } TEST_F(Utf8BytesTest, two_bytes) { EXPECT_EQ( 2, MISC::utf8bytes( "\xC2\x80" ) ); // U+0080 EXPECT_EQ( 2, MISC::utf8bytes( "\xDF\xBF" ) ); // U+07FF } TEST_F(Utf8BytesTest, three_bytes) { EXPECT_EQ( 3, MISC::utf8bytes( "\xE0\xA0\x80" ) ); // U+0800 EXPECT_EQ( 3, MISC::utf8bytes( "\xEF\xBF\xBF" ) ); // U+FFFF } TEST_F(Utf8BytesTest, four_bytes) { EXPECT_EQ( 4, MISC::utf8bytes( "\xF0\x90\x80\x80" ) ); // U+10000 EXPECT_EQ( 4, MISC::utf8bytes( "\xF4\x8F\xBF\xBF" ) ); // U+10FFFF } TEST_F(Utf8BytesTest, obsolete_four_bytes) { // 廃止された RFC2279 // 簡易的なチェックのため先頭が F4 のシーケンスは4(バイト)が返る EXPECT_EQ( 4, MISC::utf8bytes( "\xF4\x90\x80\x80" ) ); // U+110000 EXPECT_EQ( 4, MISC::utf8bytes( "\xF4\xBF\xBF\xBF" ) ); // U+13FFFF EXPECT_EQ( 0, MISC::utf8bytes( "\xF5\x80\x80\x80" ) ); // U+140000 EXPECT_EQ( 0, MISC::utf8bytes( "\xF6\x80\x80\x80" ) ); // U+180000 EXPECT_EQ( 0, MISC::utf8bytes( "\xF7\x80\x80\x80" ) ); // U+1C0000 EXPECT_EQ( 0, MISC::utf8bytes( "\xF7\xBF\xBF\xBF" ) ); // U+1FFFFF } TEST_F(Utf8BytesTest, obsolete_five_bytes) { // 廃止された RFC2279 EXPECT_EQ( 0, MISC::utf8bytes( "\xF8\x88\x80\x80\x80" ) ); // U+200000 EXPECT_EQ( 0, MISC::utf8bytes( "\xF9\x80\x80\x80\x80" ) ); // U+1000000 EXPECT_EQ( 0, MISC::utf8bytes( "\xFA\x80\x80\x80\x80" ) ); // U+2000000 EXPECT_EQ( 0, MISC::utf8bytes( "\xFB\x80\x80\x80\x80" ) ); // U+3000000 EXPECT_EQ( 0, MISC::utf8bytes( "\xFB\xBF\xBF\xBF\xBF" ) ); // U+3FFFFFF } TEST_F(Utf8BytesTest, obsolete_six_bytes) { // 廃止された RFC2279 EXPECT_EQ( 0, MISC::utf8bytes( "\xFC\x84\x80\x80\x80\x80" ) ); // U+4000000 EXPECT_EQ( 0, MISC::utf8bytes( "\xFD\x80\x80\x80\x80\x80" ) ); // U+40000000 EXPECT_EQ( 0, MISC::utf8bytes( "\xFD\xBF\xBF\xBF\xBF\xBF" ) ); // U+7FFFFFFF } TEST_F(Utf8BytesTest, invalid_byte) { // マルチバイトの後続部分 char invalid_seq[2]{}; for( int i = 0x80; i < 0xC0; ++i ) { invalid_seq[0] = i; EXPECT_EQ( 0, MISC::utf8bytes( invalid_seq ) ); } // 最少のバイト数による表現以外は不正 EXPECT_EQ( 0, MISC::utf8bytes( "\xC0" ) ); EXPECT_EQ( 0, MISC::utf8bytes( "\xC1" ) ); // UTF-16, UTF-32 で使われるバイトオーダーマーク EXPECT_EQ( 0, MISC::utf8bytes( "\xFE" ) ); EXPECT_EQ( 0, MISC::utf8bytes( "\xFF" ) ); } TEST_F(Utf8BytesTest, invalid_seq) { EXPECT_EQ( 0, MISC::utf8bytes( "\xC2\x7F" ) ); EXPECT_EQ( 0, MISC::utf8bytes( "\xC2\xC0" ) ); // 簡易的なチェックのため非ゼロが返る (右コメントは正しい範囲) EXPECT_EQ( 3, MISC::utf8bytes( "\xE0\x9F\x80" ) ); // E0 A0-BF 80-BF EXPECT_EQ( 3, MISC::utf8bytes( "\xED\xA0\x80" ) ); // ED 80-9F 80-BF EXPECT_EQ( 4, MISC::utf8bytes( "\xF0\x8F\x80\x80" ) ); // F0 90-BF 80-BF 80-BF } class Utf8ToUtf32Test : public ::testing::Test {}; TEST_F(Utf8ToUtf32Test, null_data) { int byte; EXPECT_EQ( 0, MISC::utf8toutf32( nullptr, byte ) ); EXPECT_EQ( 0, byte ); EXPECT_EQ( 0, MISC::utf8toutf32( "", byte ) ); EXPECT_EQ( 0, byte ); } TEST_F(Utf8ToUtf32Test, ascii) { int byte; EXPECT_EQ( 0x0001, MISC::utf8toutf32( "\x01", byte ) ); EXPECT_EQ( 1, byte ); EXPECT_EQ( 0x007F, MISC::utf8toutf32( "\x7F", byte ) ); EXPECT_EQ( 1, byte ); } TEST_F(Utf8ToUtf32Test, two_bytes) { int byte; EXPECT_EQ( 0x0080, MISC::utf8toutf32( "\xC2\x80", byte ) ); EXPECT_EQ( 2, byte ); EXPECT_EQ( 0x07FF, MISC::utf8toutf32( "\xDF\xBF", byte ) ); EXPECT_EQ( 2, byte ); } TEST_F(Utf8ToUtf32Test, three_bytes) { int byte; EXPECT_EQ( 0x0800, MISC::utf8toutf32( "\xE0\xA0\x80", byte ) ); EXPECT_EQ( 3, byte ); EXPECT_EQ( 0xFFFF, MISC::utf8toutf32( "\xEF\xBF\xBF", byte ) ); EXPECT_EQ( 3, byte ); } TEST_F(Utf8ToUtf32Test, four_bytes) { int byte; EXPECT_EQ( 0x00010000, MISC::utf8toutf32( "\xF0\x90\x80\x80", byte ) ); EXPECT_EQ( 4, byte ); EXPECT_EQ( 0x0010FFFF, MISC::utf8toutf32( "\xF4\x8F\xBF\xBF", byte ) ); EXPECT_EQ( 4, byte ); } TEST_F(Utf8ToUtf32Test, invalid_bytes) { // 範囲外のうち一部の4バイトシーケンスは 0 にならない // Utf8BytesTest::obsolete_four_bytes を参照 int byte; EXPECT_EQ( 0x00110000, MISC::utf8toutf32( "\xF4\x90\x80\x80", byte ) ); EXPECT_EQ( 4, byte ); EXPECT_EQ( 0x0013FFFF, MISC::utf8toutf32( "\xF4\xBF\xBF\xBF", byte ) ); EXPECT_EQ( 4, byte ); EXPECT_EQ( 0, MISC::utf8toutf32( "\xF5\x80\x80\x80", byte ) ); // U+140000 EXPECT_EQ( 0, byte ); } class Utf32ToUtf8Test : public ::testing::Test {}; TEST_F(Utf32ToUtf8Test, null_data) { EXPECT_EQ( 0, MISC::utf32toutf8( 0x1000, nullptr ) ); } TEST_F(Utf32ToUtf8Test, ascii) { char out_char[8]; EXPECT_EQ( 1, MISC::utf32toutf8( 0x0000, out_char ) ); EXPECT_STREQ( "\u0000", out_char ); EXPECT_EQ( 1, MISC::utf32toutf8( 0x007F, out_char ) ); EXPECT_STREQ( "\u007F", out_char ); } TEST_F(Utf32ToUtf8Test, two_bytes) { char out_char[8]; EXPECT_EQ( 2, MISC::utf32toutf8( 0x0080, out_char ) ); EXPECT_STREQ( "\u0080", out_char ); EXPECT_EQ( 2, MISC::utf32toutf8( 0x07FF, out_char ) ); EXPECT_STREQ( "\u07FF", out_char ); } TEST_F(Utf32ToUtf8Test, three_bytes) { char out_char[8]; EXPECT_EQ( 3, MISC::utf32toutf8( 0x0800, out_char ) ); EXPECT_STREQ( "\u0800", out_char ); EXPECT_EQ( 3, MISC::utf32toutf8( 0xFFFF, out_char ) ); EXPECT_STREQ( "\uFFFF", out_char ); } TEST_F(Utf32ToUtf8Test, four_bytes) { char out_char[8]; EXPECT_EQ( 4, MISC::utf32toutf8( 0x00010000, out_char ) ); EXPECT_STREQ( "\U00010000", out_char ); EXPECT_EQ( 4, MISC::utf32toutf8( 0x0010FFFF, out_char ) ); EXPECT_STREQ( "\U0010FFFF", out_char ); } TEST_F(Utf32ToUtf8Test, out_of_range) { char out_char[8]; EXPECT_EQ( 0, MISC::utf32toutf8( 0x00110000, out_char ) ); EXPECT_STREQ( "", out_char ); } class GetUnicodeBlockTest : public ::testing::Test {}; TEST_F(GetUnicodeBlockTest, basic_latin) { EXPECT_EQ( MISC::UnicodeBlock::BasicLatin, MISC::get_unicodeblock( 0x0000 ) ); EXPECT_EQ( MISC::UnicodeBlock::BasicLatin, MISC::get_unicodeblock( 0x007F ) ); } TEST_F(GetUnicodeBlockTest, hiragana) { EXPECT_EQ( MISC::UnicodeBlock::Hira, MISC::get_unicodeblock( 0x3040 ) ); EXPECT_EQ( MISC::UnicodeBlock::Hira, MISC::get_unicodeblock( 0x309F ) ); } TEST_F(GetUnicodeBlockTest, katanaka) { EXPECT_EQ( MISC::UnicodeBlock::Kata, MISC::get_unicodeblock( 0x30A0 ) ); EXPECT_EQ( MISC::UnicodeBlock::Kata, MISC::get_unicodeblock( 0x30FF ) ); } TEST_F(GetUnicodeBlockTest, other) { EXPECT_EQ( MISC::UnicodeBlock::Other, MISC::get_unicodeblock( 0x0080 ) ); EXPECT_EQ( MISC::UnicodeBlock::Other, MISC::get_unicodeblock( 0x303F ) ); EXPECT_EQ( MISC::UnicodeBlock::Other, MISC::get_unicodeblock( 0x3100 ) ); EXPECT_EQ( MISC::UnicodeBlock::Other, MISC::get_unicodeblock( 0x10FFFF ) ); EXPECT_EQ( MISC::UnicodeBlock::Other, MISC::get_unicodeblock( 0x110000 ) ); } class Utf8FixWaveDashTest : public ::testing::Test {}; TEST_F(Utf8FixWaveDashTest, empty_data) { EXPECT_EQ( "", MISC::utf8_fix_wavedash( "", MISC::WaveDashFix::UnixToWin ) ); EXPECT_EQ( "", MISC::utf8_fix_wavedash( "", MISC::WaveDashFix::WinToUnix ) ); } TEST_F(Utf8FixWaveDashTest, not_fix) { EXPECT_EQ( "Hello World", MISC::utf8_fix_wavedash( "Hello World", MISC::WaveDashFix::UnixToWin ) ); EXPECT_EQ( "いろはにほへ", MISC::utf8_fix_wavedash( "いろはにほへ", MISC::WaveDashFix::WinToUnix ) ); } TEST_F(Utf8FixWaveDashTest, fix_unix_to_win) { EXPECT_EQ( "\uFF5E+a", MISC::utf8_fix_wavedash( "\u301C+a", MISC::WaveDashFix::UnixToWin ) ); EXPECT_EQ( "\u2015-b", MISC::utf8_fix_wavedash( "\u2014-b", MISC::WaveDashFix::UnixToWin ) ); EXPECT_EQ( "\u2225*c", MISC::utf8_fix_wavedash( "\u2016*c", MISC::WaveDashFix::UnixToWin ) ); EXPECT_EQ( "\uFF0D/d", MISC::utf8_fix_wavedash( "\u2212/d", MISC::WaveDashFix::UnixToWin ) ); } TEST_F(Utf8FixWaveDashTest, fix_win_to_unix) { EXPECT_EQ( "a+\u301C", MISC::utf8_fix_wavedash( "a+\uFF5E", MISC::WaveDashFix::WinToUnix ) ); EXPECT_EQ( "b-\u2014", MISC::utf8_fix_wavedash( "b-\u2015", MISC::WaveDashFix::WinToUnix ) ); EXPECT_EQ( "c*\u2016", MISC::utf8_fix_wavedash( "c*\u2225", MISC::WaveDashFix::WinToUnix ) ); EXPECT_EQ( "d/\u2212", MISC::utf8_fix_wavedash( "d/\uFF0D", MISC::WaveDashFix::WinToUnix ) ); } TEST_F(Utf8FixWaveDashTest, not_fix_unix_to_win) { EXPECT_EQ( "a+\uFF5E", MISC::utf8_fix_wavedash( "a+\uFF5E", MISC::WaveDashFix::UnixToWin ) ); EXPECT_EQ( "b-\u2015", MISC::utf8_fix_wavedash( "b-\u2015", MISC::WaveDashFix::UnixToWin ) ); EXPECT_EQ( "c*\u2225", MISC::utf8_fix_wavedash( "c*\u2225", MISC::WaveDashFix::UnixToWin ) ); EXPECT_EQ( "d/\uFF0D", MISC::utf8_fix_wavedash( "d/\uFF0D", MISC::WaveDashFix::UnixToWin ) ); } TEST_F(Utf8FixWaveDashTest, not_fix_win_to_unix) { EXPECT_EQ( "\u301C+a", MISC::utf8_fix_wavedash( "\u301C+a", MISC::WaveDashFix::WinToUnix ) ); EXPECT_EQ( "\u2014-b", MISC::utf8_fix_wavedash( "\u2014-b", MISC::WaveDashFix::WinToUnix ) ); EXPECT_EQ( "\u2016*c", MISC::utf8_fix_wavedash( "\u2016*c", MISC::WaveDashFix::WinToUnix ) ); EXPECT_EQ( "\u2212/d", MISC::utf8_fix_wavedash( "\u2212/d", MISC::WaveDashFix::WinToUnix ) ); } } // namespace jdim-0.10.1/test/gtest_jdlib_misctime.cpp000066400000000000000000000114021445721505100203650ustar00rootroot00000000000000// License: GPL-2.0 #include "jdlib/misctime.h" #include "gtest/gtest.h" #include #include // tzset namespace { // NOTE: time_tのエポックはUnix epoch (1970-01-01T00:00:00 UTC)を想定している class MISC_DateToTimeTest : public ::testing::Test {}; TEST_F(MISC_DateToTimeTest, empty_input) { const std::time_t result = MISC::datetotime( "" ); EXPECT_EQ( result, 0 ); } TEST_F(MISC_DateToTimeTest, invalid_format) { const std::time_t result = MISC::datetotime( "hello world" ); EXPECT_EQ( result, 0 ); } TEST_F(MISC_DateToTimeTest, unix_epoch) { const std::time_t result = MISC::datetotime( "Thu, 01 Jan 1970 00:00:00 GMT" ); EXPECT_EQ( result, 0 ); } TEST_F(MISC_DateToTimeTest, non_gmt_wont_be_parsed) { const std::time_t result = MISC::datetotime( "Wed, 01 Jan 2020 12:34:56 JST" ); EXPECT_EQ( result, 0 ); } TEST_F(MISC_DateToTimeTest, iso8601_wont_be_parsed) { const std::time_t result = MISC::datetotime( "2006-01-27T03:48:59Z" ); EXPECT_EQ( result, 0 ); } TEST_F(MISC_DateToTimeTest, one_hundred_million) { const std::time_t result = MISC::datetotime( "Sat, 03 Mar 1973 09:46:40 GMT" ); EXPECT_EQ( result, 100'000'000 ); } TEST_F(MISC_DateToTimeTest, wrong_day_will_be_parsed) { // Although correct day of 2001-09-09 is Sunday, parsed time is right. const std::time_t result = MISC::datetotime( "Mon, 09 Sep 2001 01:46:40 GMT" ); EXPECT_EQ( result, 1'000'000'000 ); } } // namespace #ifdef _POSIX_C_SOURCE namespace { static_assert( sizeof(time_t) >= 4, "MISC_TimetToStrTest requires sizeof(time_t) >= 4." ); // NOTE: 関数の戻り値はUTF-8文字列であるのが前提 class MISC_TimetToStrTest : public ::testing::Test { std::string m_save_tz; void SetUp() override { m_save_tz = Glib::getenv( "TZ" ); Glib::setenv( "TZ", "GMT" ); ::tzset(); } void TearDown() override { Glib::setenv( "TZ", m_save_tz ); ::tzset(); } }; TEST_F(MISC_TimetToStrTest, time_normal) { EXPECT_EQ( "1970/01/01 00:00", MISC::timettostr( 0, MISC::TIME_NORMAL ) ); EXPECT_EQ( "2000/10/02 15:20", MISC::timettostr( 970500000, MISC::TIME_NORMAL ) ); EXPECT_EQ( "2009/02/13 23:31", MISC::timettostr( 1234567890, MISC::TIME_NORMAL ) ); EXPECT_EQ( "2038/01/19 03:14", MISC::timettostr( 2147483647, MISC::TIME_NORMAL ) ); } TEST_F(MISC_TimetToStrTest, time_no_year) { EXPECT_EQ( "01/01 00:00", MISC::timettostr( 0, MISC::TIME_NO_YEAR ) ); EXPECT_EQ( "10/02 15:20", MISC::timettostr( 970500000, MISC::TIME_NO_YEAR ) ); EXPECT_EQ( "02/13 23:31", MISC::timettostr( 1234567890, MISC::TIME_NO_YEAR ) ); EXPECT_EQ( "01/19 03:14", MISC::timettostr( 2147483647, MISC::TIME_NO_YEAR ) ); } TEST_F(MISC_TimetToStrTest, time_week) { EXPECT_EQ( "1970/01/01(木) 00:00:00", MISC::timettostr( 0, MISC::TIME_WEEK ) ); EXPECT_EQ( "2000/10/02(月) 15:20:00", MISC::timettostr( 970500000, MISC::TIME_WEEK ) ); EXPECT_EQ( "2000/10/04(水) 22:53:20", MISC::timettostr( 970700000, MISC::TIME_WEEK ) ); EXPECT_EQ( "2000/10/07(土) 06:26:40", MISC::timettostr( 970900000, MISC::TIME_WEEK ) ); EXPECT_EQ( "2000/10/08(日) 10:13:20", MISC::timettostr( 971000000, MISC::TIME_WEEK ) ); EXPECT_EQ( "2009/02/13(金) 23:31:30", MISC::timettostr( 1234567890, MISC::TIME_WEEK ) ); EXPECT_EQ( "2038/01/19(火) 03:14:07", MISC::timettostr( 2147483647, MISC::TIME_WEEK ) ); } TEST_F(MISC_TimetToStrTest, time_passed) { // NOTE: テストケースは60秒以内に関数の実行が完了すること&算術オーバフローしないことが前提 // 〜秒後はタイミングがシビアなので数値はテストしない const std::string sec = MISC::timettostr( time( nullptr ), MISC::TIME_PASSED ); // 現時刻 EXPECT_EQ( " 秒前", sec.substr( sec.size() - 7, std::string::npos ) ); EXPECT_EQ( "1 分前", MISC::timettostr( time( nullptr ) - 60, MISC::TIME_PASSED ) ); // 60秒前 EXPECT_EQ( "1 時間前", MISC::timettostr( time( nullptr ) - 3600, MISC::TIME_PASSED ) ); // 60分前 EXPECT_EQ( "1 日前", MISC::timettostr( time( nullptr ) - 86400, MISC::TIME_PASSED ) ); // 24時間前 EXPECT_EQ( "1 年前", MISC::timettostr( time( nullptr ) - 31622400, MISC::TIME_PASSED ) ); // 366日前 EXPECT_EQ( "未来", MISC::timettostr( time( nullptr ) + 60, MISC::TIME_PASSED ) ); // 60秒後 } TEST_F(MISC_TimetToStrTest, time_second) { EXPECT_EQ( "1970/01/01 00:00:00", MISC::timettostr( 0, MISC::TIME_SECOND ) ); EXPECT_EQ( "2000/10/02 15:20:00", MISC::timettostr( 970500000, MISC::TIME_SECOND ) ); EXPECT_EQ( "2009/02/13 23:31:30", MISC::timettostr( 1234567890, MISC::TIME_SECOND ) ); EXPECT_EQ( "2038/01/19 03:14:07", MISC::timettostr( 2147483647, MISC::TIME_SECOND ) ); } } // namespace #endif // _POSIX_C_SOURCE jdim-0.10.1/test/gtest_jdlib_misctrip.cpp000066400000000000000000000046411445721505100204140ustar00rootroot00000000000000// License: GPL-2.0 // Copyright (C) 2019, JDimproved project #include "jdlib/misctrip.h" #include "gtest/gtest.h" namespace { class GetTripTest : public ::testing::Test {}; // 実際に運用されているShift_JISエンコーディングでテスト // ASCII以外の文字はShift_JISに変換されるのでバイト数に注意すること // 戻り値のテストデータはWikipediaのperlスクリプトを使って計算した // ヘルパー関数 inline static std::string get_trip_sjis( std::string u8key ) { return MISC::get_trip( u8key, Encoding::sjis ); } TEST_F(GetTripTest, trip8_sjis_empty) { EXPECT_EQ( "", get_trip_sjis( u8"" ) ); } TEST_F(GetTripTest, trip8_sjis_A) { EXPECT_EQ( "hRJ9Ya./t.", get_trip_sjis( u8"A" ) ); } TEST_F(GetTripTest, trip8_sjis_hello7) { EXPECT_EQ( "/wfpxFEFeQ", get_trip_sjis( u8"hellowo" ) ); } TEST_F(GetTripTest, trip8_sjis_hello8) { EXPECT_EQ( "d75etXAowg", get_trip_sjis( u8"hellowor" ) ); } TEST_F(GetTripTest, trip8_sjis_hello11) { // 11バイトまでは従来の方式(9〜11バイト目は無視される) EXPECT_EQ( "d75etXAowg", get_trip_sjis( u8"helloworld!" ) ); } TEST_F(GetTripTest, trip8_sjis_yojijukugo) { EXPECT_EQ( "sX.SlNvMe.", get_trip_sjis( u8"四字熟語" ) ); } TEST_F(GetTripTest, trip8_sjis_hex_less_12) { // シャープで始まる16進数キーでも12バイト未満なら従来の方式 EXPECT_EQ( "RTDIJZhD3g", get_trip_sjis( u8"#0123456789" ) ); } TEST_F(GetTripTest, trip8_sjis_dollar_less_12) { // ドル記号で始まるキーでも12バイト未満なら従来の方式 EXPECT_EQ( "46g6cHndYk", get_trip_sjis( u8"$0123456789" ) ); } TEST_F(GetTripTest, trip12_sjis_hello12) { // 12バイト以上は新方式 EXPECT_EQ( "xwumaTFfu1OK", get_trip_sjis( u8"helloworld!?" ) ); } TEST_F(GetTripTest, trip12_sjis_jdimprovedproject) { // 新方式の+は.に変換される EXPECT_EQ( "Y7G9gYfXrr6.", get_trip_sjis( u8"jdimprovedproject" ) ); } TEST_F(GetTripTest, trip12_sjis_hex) { // 新方式 16進数のキー EXPECT_EQ( "ClNHFHdYIw", get_trip_sjis( u8"#0123456789abcdef" ) ); } TEST_F(GetTripTest, trip12_sjis_non_hex) { // 念の為トライグラフ対策としてエスケープ EXPECT_EQ( "\?\?\?", get_trip_sjis( u8"#あいうえおか" ) ); } TEST_F(GetTripTest, trip12_sjis_dollar) { EXPECT_EQ( "\?\?\?", get_trip_sjis( u8"$将来の拡張用" ) ); } } // namespace jdim-0.10.1/test/gtest_jdlib_miscutil.cpp000066400000000000000000002726541445721505100204260ustar00rootroot00000000000000// License: GPL2 #include "jdlib/miscutil.h" #include "gtest/gtest.h" #include // std::iota() namespace { class SplitLineTest : public ::testing::Test {}; TEST_F(SplitLineTest, split_empty) { std::list< std::string > expect = {}; EXPECT_EQ( expect, MISC::split_line( u8"" ) ); } TEST_F(SplitLineTest, split_U_0020) { std::list< std::string > expect = {}; EXPECT_EQ( expect, MISC::split_line( u8" " ) ); expect.assign( { u8"the", u8"quick", u8"brown", u8"fox" } ); EXPECT_EQ( expect, MISC::split_line( u8" the quick brown fox " ) ); } TEST_F(SplitLineTest, split_U_3000) { std::list< std::string > expect = {}; EXPECT_EQ( expect, MISC::split_line( u8"\u3000 \u3000 " ) ); expect.assign( { u8"the", u8"quick", u8"brown", u8"fox" } ); EXPECT_EQ( expect, MISC::split_line( u8"\u3000the\u3000quick \u3000brown\u3000 \u3000fox\u3000 " ) ); } TEST_F(SplitLineTest, split_doublequote_U_0020) { std::list< std::string > expect = {}; EXPECT_EQ( expect, MISC::split_line( u8" \"\" " ) ); expect.assign( { u8"the quick", u8" ", u8" brown fox " } ); EXPECT_EQ( expect, MISC::split_line( u8" \"the quick\" \" \" \" brown fox \" " ) ); } TEST_F(SplitLineTest, split_doublequote_U_3000) { std::list< std::string > expect = { u8"the\u3000quick", u8"\u3000", u8"\u3000brown \u3000fox\u3000" }; EXPECT_EQ( expect, MISC::split_line( u8"\u3000\"the\u3000quick\" \"\u3000\" \"\u3000brown \u3000fox\u3000\"" ) ); } class ConcatWithSuffixTest : public ::testing::Test {}; TEST_F(ConcatWithSuffixTest, empty_list) { std::list list_in; EXPECT_EQ( "", MISC::concat_with_suffix( list_in, '!' ) ); } TEST_F(ConcatWithSuffixTest, one_element) { std::list list_in = { "hello" }; EXPECT_EQ( "hello!", MISC::concat_with_suffix( list_in, '!' ) ); } TEST_F(ConcatWithSuffixTest, hello_world) { std::list list_in = { "hello", "world" }; EXPECT_EQ( "hello!world!", MISC::concat_with_suffix( list_in, '!' ) ); } TEST_F(ConcatWithSuffixTest, ignore_empty_string) { std::list list_in = { "", "hello", "", "", "world", "" }; EXPECT_EQ( "hello!world!", MISC::concat_with_suffix( list_in, '!' ) ); } class Utf8TrimTest : public ::testing::Test {}; TEST_F(Utf8TrimTest, remove_empty) { std::string expect = {}; EXPECT_EQ( expect, MISC::utf8_trim( "" ) ); } TEST_F(Utf8TrimTest, remove_U_0020) { std::string expect = {}; EXPECT_EQ( expect, MISC::utf8_trim( " " ) ); expect.assign( "the quick brown fox" ); EXPECT_EQ( expect, MISC::utf8_trim( " the quick brown fox " ) ); } TEST_F(Utf8TrimTest, remove_mixed_U_2000_U_3000) { std::string expect = {}; EXPECT_EQ( expect, MISC::utf8_trim( "\u3000 \u3000 " ) ); expect.assign( "the quick\u3000brown\u3000 fox" ); EXPECT_EQ( expect, MISC::utf8_trim( "\u3000the quick\u3000brown\u3000 fox\u3000 " ) ); } TEST_F(Utf8TrimTest, not_remove_U_3000_only) { // 半角スペースが含まれてないときはU+3000が先頭末尾にあってもトリミングしない std::string expect = "\u3000\u3000"; EXPECT_EQ( expect, MISC::utf8_trim( "\u3000\u3000" ) ); expect.assign( "\u3000the\u3000quick\u3000brown\u3000fox\u3000" ); EXPECT_EQ( expect, MISC::utf8_trim( expect ) ); } TEST_F(Utf8TrimTest, remove_doublequote) { std::string expect = "\"\""; EXPECT_EQ( expect, MISC::utf8_trim( "\u3000 \"\"\u3000 " ) ); } TEST_F(Utf8TrimTest, input_length_is_shorter_than_u3000) { std::string expect = "a"; EXPECT_EQ( expect, MISC::utf8_trim( " a" ) ); expect = "b"; EXPECT_EQ( expect, MISC::utf8_trim( "b " ) ); } class AsciiTrimTest : public ::testing::Test {}; TEST_F(AsciiTrimTest, empty_data) { EXPECT_EQ( "", MISC::ascii_trim( "" ) ); } TEST_F(AsciiTrimTest, no_space_chars_at_start_and_end) { EXPECT_EQ( "Hello \n \r \t World", MISC::ascii_trim( "Hello \n \r \t World" ) ); EXPECT_EQ( "あいうえお", MISC::ascii_trim( "あいうえお" ) ); } TEST_F(AsciiTrimTest, trim_front) { EXPECT_EQ( "Hello", MISC::ascii_trim( " Hello" ) ); EXPECT_EQ( "Hello", MISC::ascii_trim( "\nHello" ) ); EXPECT_EQ( "Hello", MISC::ascii_trim( "\rHello" ) ); EXPECT_EQ( "Hello", MISC::ascii_trim( "\tHello" ) ); EXPECT_EQ( "Hello", MISC::ascii_trim( "\n \r \t Hello" ) ); } TEST_F(AsciiTrimTest, trim_back) { EXPECT_EQ( "World", MISC::ascii_trim( "World " ) ); EXPECT_EQ( "World", MISC::ascii_trim( "World\n" ) ); EXPECT_EQ( "World", MISC::ascii_trim( "World\r" ) ); EXPECT_EQ( "World", MISC::ascii_trim( "World\t" ) ); EXPECT_EQ( "World", MISC::ascii_trim( "World\n \r \t" ) ); } TEST_F(AsciiTrimTest, trim_both_side) { EXPECT_EQ( "Hello\t \n \rWorld", MISC::ascii_trim( "\n \r \t Hello\t \n \rWorld \n \r \t" ) ); } TEST_F(AsciiTrimTest, not_trim_ascii) { EXPECT_EQ( "\vHello\v", MISC::ascii_trim( "\vHello\v" ) ); // VERTICAL TAB EXPECT_EQ( "\fHello\f", MISC::ascii_trim( "\fHello\f" ) ); // FORM FEED } TEST_F(AsciiTrimTest, not_trim_unicode) { EXPECT_EQ( "\u00A0Hello\u00A0", MISC::ascii_trim( "\u00A0Hello\u00A0" ) ); // NO-BREAK SPACE EXPECT_EQ( "\u3000Hello\u3000", MISC::ascii_trim( "\u3000Hello\u3000" ) ); // IDEOGRAPHIC SPACE } class RemoveStrStartEndTest : public ::testing::Test {}; TEST_F(RemoveStrStartEndTest, empty_data) { EXPECT_EQ( "", MISC::remove_str( "", "", "" ) ); EXPECT_EQ( "", MISC::remove_str( "", "<<", "" ) ); EXPECT_EQ( "", MISC::remove_str( "", "<<", ">>" ) ); EXPECT_EQ( "", MISC::remove_str( "", "", ">>" ) ); } TEST_F(RemoveStrStartEndTest, empty_start) { EXPECT_EQ( "Quick<>Fox", MISC::remove_str( "Quick<>Fox", "", ">>" ) ); } TEST_F(RemoveStrStartEndTest, empty_end) { EXPECT_EQ( "Quick<>Fox", MISC::remove_str( "Quick<>Fox", "<<", "" ) ); } TEST_F(RemoveStrStartEndTest, different_marks) { EXPECT_EQ( "QuickFox", MISC::remove_str( "Quick<>Fox", "<<", ">>" ) ); } TEST_F(RemoveStrStartEndTest, same_marks) { EXPECT_EQ( "QuickFox", MISC::remove_str( "Quick!!Brown!!Fox", "!!", "!!" ) ); } TEST_F(RemoveStrStartEndTest, much_start_marks) { EXPECT_EQ( "TheFox", MISC::remove_str( "The(Quick(Brown)Fox", "(", ")" ) ); } TEST_F(RemoveStrStartEndTest, much_end_marks) { EXPECT_EQ( "TheBrown)Fox", MISC::remove_str( "The(Quick)Brown)Fox", "(", ")" ) ); } class CutStrFrontBackTest : public ::testing::Test {}; TEST_F(CutStrFrontBackTest, empty_data) { EXPECT_EQ( "", MISC::cut_str( "", "", "" ) ); EXPECT_EQ( "", MISC::cut_str( "", "AA", "" ) ); EXPECT_EQ( "", MISC::cut_str( "", "AA", "BB" ) ); EXPECT_EQ( "", MISC::cut_str( "", "", "BB" ) ); } TEST_F(CutStrFrontBackTest, empty_front_separator) { EXPECT_EQ( "", MISC::cut_str( "Quick<>Fox", "", ">>" ) ); } TEST_F(CutStrFrontBackTest, empty_back_separator) { EXPECT_EQ( "", MISC::cut_str( "Quick<>Fox", "<<", "" ) ); } TEST_F(CutStrFrontBackTest, different_separators) { EXPECT_EQ( "Brown", MISC::cut_str( "Quick<>Fox", "<<", ">>" ) ); } TEST_F(CutStrFrontBackTest, same_separators) { EXPECT_EQ( "Brown", MISC::cut_str( "Quick!!Brown!!Fox", "!!", "!!" ) ); } TEST_F(CutStrFrontBackTest, much_front_separators) { EXPECT_EQ( "Quick(Brown", MISC::cut_str( "The(Quick(Brown)Fox", "(", ")" ) ); } TEST_F(CutStrFrontBackTest, much_back_separators) { EXPECT_EQ( "Quick", MISC::cut_str( "The(Quick)Brown)Fox", "(", ")" ) ); } class ReplaceStrTest : public ::testing::Test {}; TEST_F(ReplaceStrTest, empty_data) { EXPECT_EQ( "", MISC::replace_str( "", "", "" ) ); EXPECT_EQ( "", MISC::replace_str( "", "AA", "" ) ); EXPECT_EQ( "", MISC::replace_str( "", "AA", "BB" ) ); EXPECT_EQ( "", MISC::replace_str( "", "", "BB" ) ); } TEST_F(ReplaceStrTest, empty_match) { EXPECT_EQ( "Quick Brown Fox", MISC::replace_str( "Quick Brown Fox", "", "Red" ) ); } TEST_F(ReplaceStrTest, replace_with_empty) { EXPECT_EQ( "Quick//Fox", MISC::replace_str( "Quick/Brown/Fox", "Brown", "" ) ); } TEST_F(ReplaceStrTest, not_match) { EXPECT_EQ( "Quick Brown Fox", MISC::replace_str( "Quick Brown Fox", "Red", "Blue" ) ); } TEST_F(ReplaceStrTest, multi_match) { EXPECT_EQ( "Quick Red Red Fox", MISC::replace_str( "Quick Brown Brown Fox", "Brown", "Red" ) ); } class ReplaceCaseStrTest : public ::testing::Test {}; TEST_F(ReplaceCaseStrTest, empty_data) { EXPECT_EQ( "", MISC::replace_casestr( "", "", "" ) ); EXPECT_EQ( "", MISC::replace_casestr( "", "AA", "" ) ); EXPECT_EQ( "", MISC::replace_casestr( "", "AA", "BB" ) ); EXPECT_EQ( "", MISC::replace_casestr( "", "", "BB" ) ); } TEST_F(ReplaceCaseStrTest, empty_match) { EXPECT_EQ( "Quick Brown Fox", MISC::replace_casestr( "Quick Brown Fox", "", "Red" ) ); } TEST_F(ReplaceCaseStrTest, replace_with_empty) { EXPECT_EQ( "Quick//Fox", MISC::replace_casestr( "Quick/Brown/Fox", "Brown", "" ) ); } TEST_F(ReplaceCaseStrTest, replace_with_empty_ignore_case) { EXPECT_EQ( "Quick//Fox", MISC::replace_casestr( "Quick/BrOwN/Fox", "bRoWn", "" ) ); } TEST_F(ReplaceCaseStrTest, not_match) { EXPECT_EQ( "Quick Brown Fox", MISC::replace_casestr( "Quick Brown Fox", "Red", "Blue" ) ); } TEST_F(ReplaceCaseStrTest, multi_match) { EXPECT_EQ( "Quick Red Red Fox", MISC::replace_casestr( "Quick Brown Brown Fox", "Brown", "Red" ) ); } TEST_F(ReplaceCaseStrTest, multi_match_ignore_case) { EXPECT_EQ( "Quick Red Red Fox", MISC::replace_casestr( "Quick BrOwN bRoWn Fox", "BRowN", "Red" ) ); } class ReplaceStrListTest : public ::testing::Test {}; TEST_F(ReplaceStrListTest, empty_data) { std::list empty; std::list expect; EXPECT_EQ( expect, MISC::replace_str_list( empty, "AA", "BB" ) ); } TEST_F(ReplaceStrListTest, sample_match) { std::list input = { "hello", "world", "sample" }; std::list expect = { "hell123", "w123rld", "sample" }; EXPECT_EQ( expect, MISC::replace_str_list( input, "o", "123" ) ); } class ReplaceNewlinesToStrTest : public ::testing::Test {}; TEST_F(ReplaceNewlinesToStrTest, empty_data) { EXPECT_EQ( "", MISC::replace_newlines_to_str( "", "" ) ); EXPECT_EQ( "", MISC::replace_newlines_to_str( "", "A\nA" ) ); } TEST_F(ReplaceNewlinesToStrTest, empty_replacement) { EXPECT_EQ( "\nBrown\nFox\n", MISC::replace_newlines_to_str( "\nBrown\nFox\n", "" ) ); EXPECT_EQ( "\rBrown\rFox\r", MISC::replace_newlines_to_str( "\rBrown\rFox\r", "" ) ); EXPECT_EQ( "\r\nBrown\r\nFox\r\n", MISC::replace_newlines_to_str( "\r\nBrown\r\nFox\r\n", "" ) ); } TEST_F(ReplaceNewlinesToStrTest, replace_cr) { EXPECT_EQ( "!!Brown!!Fox!!", MISC::replace_newlines_to_str( "\rBrown\rFox\r", "!!" ) ); } TEST_F(ReplaceNewlinesToStrTest, replace_lf) { EXPECT_EQ( "!!Brown!!Fox!!", MISC::replace_newlines_to_str( "\nBrown\nFox\n", "!!" ) ); } TEST_F(ReplaceNewlinesToStrTest, replace_crlf) { EXPECT_EQ( "!!Brown!!Fox!!", MISC::replace_newlines_to_str( "\r\nBrown\r\nFox\r\n", "!!" ) ); } class ChrToBinTest : public ::testing::Test {}; TEST_F(ChrToBinTest, empty_input) { char output[4]{}; EXPECT_EQ( 0, MISC::chrtobin( "", output ) ); } TEST_F(ChrToBinTest, fullwidth_input) { char output[4]{}; EXPECT_EQ( 0, MISC::chrtobin( "AB", output ) ); EXPECT_EQ( 0, MISC::chrtobin( "12", output ) ); } TEST_F(ChrToBinTest, non_ascii_input) { char output[4]{}; EXPECT_EQ( 0, MISC::chrtobin( "あい", output ) ); EXPECT_EQ( 0, MISC::chrtobin( "アイ", output ) ); } TEST_F(ChrToBinTest, non_hexadecimal_input) { char output[4]{}; EXPECT_EQ( 0, MISC::chrtobin( "GH", output ) ); EXPECT_EQ( 0, MISC::chrtobin( "gh", output ) ); EXPECT_EQ( 0, MISC::chrtobin( " !", output ) ); } TEST_F(ChrToBinTest, hexadecimal_input) { char output[16]; std::memset( output, '\0', 16 ); EXPECT_EQ( 10, MISC::chrtobin( "0123456789", output ) ); EXPECT_STREQ( "\x01\x23\x45\x67\x89", output ); std::memset( output, '\0', 16 ); EXPECT_EQ( 6, MISC::chrtobin( "ABCDEF", output ) ); EXPECT_STREQ( "\xAB\xCD\xEF", output ); } TEST_F(ChrToBinTest, hexadecimal_incomplete_input) { char output[16]; std::memset( output, '\0', 16 ); EXPECT_EQ( 3, MISC::chrtobin( "123", output ) ); EXPECT_STREQ( "\x12\x03", output ); std::memset( output, '\0', 16 ); EXPECT_EQ( 3, MISC::chrtobin( "ABC", output ) ); EXPECT_STREQ( "\xAB\x0C", output ); } TEST_F(ChrToBinTest, break_at_non_hexadecimal) { char output[16]; std::memset( output, '\0', 16 ); EXPECT_EQ( 4, MISC::chrtobin( "1234あFFFF", output ) ); EXPECT_STREQ( "\x12\x34", output ); std::memset( output, '\0', 16 ); EXPECT_EQ( 3, MISC::chrtobin( "ABC FFFF", output ) ); EXPECT_STREQ( "\xAB\xC0", output ); } class HtmlEscapeTest : public ::testing::Test {}; TEST_F(HtmlEscapeTest, empty_data) { EXPECT_EQ( "", MISC::html_escape( "", false ) ); EXPECT_EQ( "", MISC::html_escape( "", true ) ); } TEST_F(HtmlEscapeTest, not_escape) { EXPECT_EQ( "hello world", MISC::html_escape( "hello world", false ) ); EXPECT_EQ( "hello world", MISC::html_escape( "hello world", true ) ); } TEST_F(HtmlEscapeTest, escape_amp) { EXPECT_EQ( "hello&world", MISC::html_escape( "hello&world", false ) ); EXPECT_EQ( "hello&world", MISC::html_escape( "hello&world", true ) ); } TEST_F(HtmlEscapeTest, escape_quot) { EXPECT_EQ( "hello"world", MISC::html_escape( "hello\"world", false ) ); EXPECT_EQ( "hello"world", MISC::html_escape( "hello\"world", true ) ); } TEST_F(HtmlEscapeTest, escape_lt) { EXPECT_EQ( "hello<world", MISC::html_escape( "helloworld", false ) ); EXPECT_EQ( "hello>world", MISC::html_escape( "hello>world", true ) ); } TEST_F(HtmlEscapeTest, completely) { // URLを含むテキスト const std::string input = "& https://foobar.test/?a=b&c=d& &"; EXPECT_EQ( "& https://foobar.test/?a=b&c=d& &", MISC::html_escape( input, true ) ); EXPECT_EQ( "& https://foobar.test/?a=b&c=d& &", MISC::html_escape( input, false ) ); } class HtmlUnescapeTest : public ::testing::Test {}; TEST_F(HtmlUnescapeTest, empty_data) { EXPECT_EQ( "", MISC::html_unescape( "" ) ); EXPECT_EQ( "", MISC::html_unescape( "" ) ); } TEST_F(HtmlUnescapeTest, not_escape) { EXPECT_EQ( "quick brown fox", MISC::html_unescape( "quick brown fox" ) ); EXPECT_EQ( "quick brown fox", MISC::html_unescape( "quick brown fox" ) ); } TEST_F(HtmlUnescapeTest, escape_amp) { EXPECT_EQ( "quick&brown&fox", MISC::html_unescape( "quick&brown&fox" ) ); } TEST_F(HtmlUnescapeTest, escape_quot) { EXPECT_EQ( "quick\"brown\"fox", MISC::html_unescape( "quick"brown"fox" ) ); } TEST_F(HtmlUnescapeTest, escape_lt) { EXPECT_EQ( "quickbrown>fox", MISC::html_unescape( "quick>brown>fox" ) ); } TEST_F(HtmlUnescapeTest, numeric_char_reference) { EXPECT_EQ( "quick{brownêfox", MISC::html_unescape( "quick{brownêfox" ) ); EXPECT_EQ( "quickꯍbrownfox", MISC::html_unescape( "quickꯍbrownfox" ) ); } TEST_F(HtmlUnescapeTest, named_char_reference) { EXPECT_EQ( "quickäbrownÜfox", MISC::html_unescape( "quickäbrownÜfox" ) ); EXPECT_EQ( "quick↠brown⇓fox", MISC::html_unescape( "quick↠brown⇓fox" ) ); } class IsUrlSchemeTest : public ::testing::Test {}; TEST_F(IsUrlSchemeTest, url_none) { EXPECT_EQ( MISC::SCHEME_NONE, MISC::is_url_scheme( "foo" ) ); EXPECT_EQ( MISC::SCHEME_NONE, MISC::is_url_scheme( "http:/" ) ); EXPECT_EQ( MISC::SCHEME_NONE, MISC::is_url_scheme( "ttp:/" ) ); EXPECT_EQ( MISC::SCHEME_NONE, MISC::is_url_scheme( "tp:/" ) ); EXPECT_EQ( MISC::SCHEME_NONE, MISC::is_url_scheme( "ftp:/" ) ); // "sssp:/" はバッファオーバーランを起こす } TEST_F(IsUrlSchemeTest, url_http) { int length; EXPECT_EQ( MISC::SCHEME_HTTP, MISC::is_url_scheme( "http://", &length ) ); EXPECT_EQ( 7, length ); EXPECT_EQ( MISC::SCHEME_HTTP, MISC::is_url_scheme( "http://foobar", &length ) ); EXPECT_EQ( 7, length ); } TEST_F(IsUrlSchemeTest, url_ttp) { int length; EXPECT_EQ( MISC::SCHEME_TTP, MISC::is_url_scheme( "ttp://", &length ) ); EXPECT_EQ( 6, length ); EXPECT_EQ( MISC::SCHEME_TTP, MISC::is_url_scheme( "ttp://foobar", &length ) ); EXPECT_EQ( 6, length ); } TEST_F(IsUrlSchemeTest, url_tp) { int length; EXPECT_EQ( MISC::SCHEME_TP, MISC::is_url_scheme( "tp://", &length ) ); EXPECT_EQ( 5, length ); EXPECT_EQ( MISC::SCHEME_TP, MISC::is_url_scheme( "tp://foobar", &length ) ); EXPECT_EQ( 5, length ); } TEST_F(IsUrlSchemeTest, url_ftp) { int length; EXPECT_EQ( MISC::SCHEME_FTP, MISC::is_url_scheme( "ftp://", &length ) ); EXPECT_EQ( 6, length ); EXPECT_EQ( MISC::SCHEME_FTP, MISC::is_url_scheme( "ftp://foobar", &length ) ); EXPECT_EQ( 6, length ); } TEST_F(IsUrlSchemeTest, url_sssp) { int length; EXPECT_EQ( MISC::SCHEME_HTTP, MISC::is_url_scheme( "sssp://", &length ) ); EXPECT_EQ( 7, length ); EXPECT_EQ( MISC::SCHEME_HTTP, MISC::is_url_scheme( "sssp://foobar", &length ) ); EXPECT_EQ( 7, length ); EXPECT_EQ( MISC::SCHEME_SSSP, MISC::is_url_scheme( "sssp://img.2ch", &length ) ); EXPECT_EQ( 7, length ); EXPECT_EQ( MISC::SCHEME_SSSP, MISC::is_url_scheme( "sssp://img.5ch", &length ) ); EXPECT_EQ( 7, length ); } class MISC_AscTest : public ::testing::Test {}; TEST_F(MISC_AscTest, empty_input) { std::string output; std::vector table; // 入力はヌル終端文字列 MISC::asc( u8"", output, table ); EXPECT_EQ( u8"", output ); EXPECT_EQ( 0, output.size() ); // 文字列の終端(ヌル文字)の位置が追加されるためtableのサイズが+1大きくなる EXPECT_EQ( 1, table.size() ); EXPECT_EQ( 0, table.at( 0 ) ); } TEST_F(MISC_AscTest, halfwidth_latin_capital_letter) { std::string output; std::vector table; MISC::asc( u8"THE QUICK BROWN FOX JUMPS OVER THE LAZY DOG.", output, table ); EXPECT_EQ( u8"THE QUICK BROWN FOX JUMPS OVER THE LAZY DOG.", output ); EXPECT_EQ( output.size(), table.size() - 1 ); for( int i = 0, size = table.size(); i < size; ++i ) { EXPECT_EQ( i, table.at( i ) ); } } TEST_F(MISC_AscTest, halfwidth_latin_small_letter) { std::string output; std::vector table; MISC::asc( u8"the quick brown fox jumps over the lazy dog.", output, table ); EXPECT_EQ( u8"the quick brown fox jumps over the lazy dog.", output ); EXPECT_EQ( output.size(), table.size() - 1 ); for( int i = 0, size = table.size(); i < size; ++i ) { EXPECT_EQ( i, table.at( i ) ); } } TEST_F(MISC_AscTest, halfwidth_digit_sign) { std::string output; std::vector table; MISC::asc( u8"1234567890+-*/", output, table ); EXPECT_EQ( u8"1234567890+-*/", output ); EXPECT_EQ( output.size(), table.size() - 1 ); for( int i = 0, size = table.size(); i < size; ++i ) { EXPECT_EQ( i, table.at( i ) ); } } TEST_F(MISC_AscTest, halfwidth_append_data) { std::string output = "123"; std::vector table = { 0, 1, 2 }; // アウトプット引数は初期化せずデータを追加する MISC::asc( u8"hello", output, table ); EXPECT_EQ( u8"123hello", output ); EXPECT_EQ( output.size(), table.size() - 1 ); const std::vector expected_table = { 0, 1, 2, 0, 1, 2, 3, 4, 5 }; EXPECT_EQ( expected_table, table ); } std::vector expected_table_fullwidth_quick_brown_fox() { // 全角英数字から半角英数字に変換したときの文字列の位置を保存しておくテーブルのテストデータ return { // TH E U+3000 Q U I C K U+3000 B R O W N U+3000 F O X U+3000 0, 3, 6, 9, 10, 11, 12, 15, 18, 21, 24, 27, 28, 29, 30, 33, 36, 39, 42, 45, 46, 47, 48, 51, 54, 57, 58, 59, // JU M P S U+3000 O V E R U+3000 T H E U+3000 L A Z Y 60, 63, 66, 69, 72, 75, 76, 77, 78, 81, 84, 87, 90, 91, 92, 93, 96, 99, 102, 103, 104, 105, 108, 111, 114, // U+3000 D O G U+FF0E U+0000 117, 118, 119, 120, 123, 126, 129, 130, 131, 132, }; } TEST_F(MISC_AscTest, fullwidth_latin_capital_letter) { std::string output; std::vector table; MISC::asc( u8"THE QUICK BROWN FOX JUMPS OVER THE LAZY DOG.", output, table ); // 和字間隔(U+3000)は半角スペースに変換されない EXPECT_EQ( u8"THE QUICK BROWN FOX JUMPS OVER THE LAZY DOG.", output ); EXPECT_EQ( output.size(), table.size() - 1 ); EXPECT_EQ( expected_table_fullwidth_quick_brown_fox(), table ); } TEST_F(MISC_AscTest, fullwidth_latin_small_letter) { std::string output; std::vector table; MISC::asc( u8"the quick brown fox jumps over the lazy dog.", output, table ); EXPECT_EQ( u8"the quick brown fox jumps over the lazy dog.", output ); EXPECT_EQ( output.size(), table.size() - 1 ); EXPECT_EQ( expected_table_fullwidth_quick_brown_fox(), table ); } TEST_F(MISC_AscTest, fullwidth_digit_sign) { std::string output; std::vector table; MISC::asc( u8"1234567890+−*/", output, table ); // 全角数字は半角に変換されるが、全角記号は半角に変換されない EXPECT_EQ( u8"1234567890+−*/", output ); EXPECT_EQ( output.size(), table.size() - 1 ); const std::vector expected_table = { 0, 3, 6, 9, 12, 15, 18, 21, 24, 27, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42 }; EXPECT_EQ( expected_table, table ); } TEST_F(MISC_AscTest, halfwidth_katakana_without_voiced_sound_mark) { std::string output; std::vector table; constexpr const char halfwidth[] { u8"\uFF61\uFF62\uFF63\uFF64\uFF65" u8"\uFF66" u8"\uFF67\uFF68\uFF69\uFF6A\uFF6B" u8"\uFF6C\uFF6D\uFF6E\uFF6F\uFF70" u8"\uFF71\uFF72\uFF73\uFF74\uFF75" u8"\uFF76\uFF77\uFF78\uFF79\uFF7A" u8"\uFF7B\uFF7C\uFF7D\uFF7E\uFF7F" u8"\uFF80\uFF81\uFF82\uFF83\uFF84" u8"\uFF85\uFF86\uFF87\uFF88\uFF89" u8"\uFF8A\uFF8B\uFF8C\uFF8D\uFF8E" u8"\uFF8F\uFF90\uFF91\uFF92\uFF93" u8"\uFF94\uFF95\uFF96" u8"\uFF97\uFF98\uFF99\uFF9A\uFF9B" u8"\uFF9C\uFF9D" }; constexpr const char fullwidth[] { u8"。「」、・" u8"ヲ" u8"ァィゥェォ" u8"ャュョッー" u8"アイウエオ" u8"カキクケコ" u8"サシスセソ" u8"タチツテト" u8"ナニヌネノ" u8"ハヒフヘホ" u8"マミムメモ" u8"ヤユヨ" u8"ラリルレロ" u8"ワン" }; // 半角片仮名から全角片仮名へ一対一の変換 MISC::asc( halfwidth, output, table ); EXPECT_EQ( fullwidth, output ); EXPECT_EQ( output.size(), table.size() - 1 ); for( int i = 0, size = table.size(); i < size; ++i ) { EXPECT_EQ( i, table.at( i ) ); } } TEST_F(MISC_AscTest, halfwidth_katakana_only_voiced_sound_mark) { std::string output; std::vector table; // 半角の濁点と半濁点は単独では全角に変換されない MISC::asc( u8"\uFF9E\uFF9F", output, table ); EXPECT_EQ( u8"\uFF9E\uFF9F", output ); EXPECT_EQ( output.size(), table.size() - 1 ); for( int i = 0, size = table.size(); i < size; ++i ) { EXPECT_EQ( i, table.at( i ) ); } } TEST_F(MISC_AscTest, halfwidth_katakana_combining_voiced_sound_mark_to_precomposed) { std::string output; std::vector table; constexpr const char halfwidth[] = { u8"\uFF76\uFF9E\uFF77\uFF9E\uFF78\uFF9E\uFF79\uFF9E\uFF7A\uFF9E" u8"\uFF7B\uFF9E\uFF7C\uFF9E\uFF7D\uFF9E\uFF7E\uFF9E\uFF7F\uFF9E" u8"\uFF80\uFF9E\uFF81\uFF9E\uFF82\uFF9E\uFF83\uFF9E\uFF84\uFF9E" u8"\uFF8A\uFF9E\uFF8B\uFF9E\uFF8C\uFF9E\uFF8D\uFF9E\uFF8E\uFF9E" u8"\uFF8A\uFF9F\uFF8B\uFF9F\uFF8C\uFF9F\uFF8D\uFF9F\uFF8E\uFF9F" }; constexpr const char fullwidth[] { u8"\u30AC\u30AE\u30B0\u30B2\u30B4" // ガギグゲゴ u8"\u30B6\u30B8\u30BA\u30BC\u30BE" // ザジズゼゾ u8"\u30C0\u30C2\u30C5\u30C7\u30C9" // ダヂヅデド u8"\u30D0\u30D3\u30D6\u30D9\u30DC" // バビブベボ u8"\u30D1\u30D4\u30D7\u30DA\u30DD" // パピプペポ }; // 合成済み文字が存在する(半)濁点付き半角片仮名は全角へ変換される MISC::asc( halfwidth, output, table ); EXPECT_EQ( fullwidth, output ); EXPECT_EQ( output.size(), table.size() - 1 ); const std::vector expected_table = { 0, 1, 2, 6, 7, 8, 12, 13, 14, 18, 19, 20, 24, 25, 26, 30, 31, 32, 36, 37, 38, 42, 43, 44, 48, 49, 50, 54, 55, 56, 60, 61, 62, 66, 67, 68, 72, 73, 74, 78, 79, 80, 84, 85, 86, 90, 91, 92, 96, 97, 98, 102, 103, 104, 108, 109, 110, 114, 115, 116, 120, 121, 122, 126, 127, 128, 132, 133, 134, 138, 139, 140, 144, 145, 146, 150, }; EXPECT_EQ( expected_table, table ); } TEST_F(MISC_AscTest, halfwidth_katakana_combining_voiced_sound_mark_wagyo) { std::string output; std::vector table; // 濁点付き半角片仮名のウは全角の合成済み文字に変換される:ヴ // ワ行の濁点付き半角片仮名は全角に変換されない(変換表が未実装) : ヷヺ MISC::asc( u8"\uFF73\uFF9E\uFF9C\uFF9E\uFF66\uFF9E", output, table ); EXPECT_EQ( u8"\u30F4\uFF9C\uFF9E\uFF66\uFF9E", output ); EXPECT_EQ( output.size(), table.size() - 1 ); const std::vector expected_table = { 0, 1, 2, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18 }; EXPECT_EQ( expected_table, table ); } TEST_F(MISC_AscTest, halfwidth_katakana_combining_voiced_sound_mark_through) { std::string output; std::vector table; // 合成済み文字が存在しない(半)濁点付き半角片仮名は全角に変換されない : カ゚キ゚ク゚ケ゚コ゚ // NOTE: 組み合わせが多いのでテストは網羅していない MISC::asc( u8"\uFF76\uFF9F\uFF77\uFF9F\uFF78\uFF9F\uFF79\uFF9F\uFF7A\uFF9F", output, table ); EXPECT_EQ( u8"\uFF76\uFF9F\uFF77\uFF9F\uFF78\uFF9F\uFF79\uFF9F\uFF7A\uFF9F", output ); EXPECT_EQ( output.size(), table.size() - 1 ); for( int i = 0, size = table.size(); i < size; ++i ) { EXPECT_EQ( i, table.at( i ) ); } } TEST_F(MISC_AscTest, fullwidth_katakana_without_voiced_sound_mark) { std::string output; std::vector table; constexpr const char fullwidth[] { u8"\u30A1\u30A2\u30A3\u30A4\u30A5\u30A6\u30A7\u30A8\u30A9\u30AA" // ァ - オ u8"\u30AB\u30AD\u30AF\u30B1\u30B3\u30B5\u30B7\u30B9\u30BB\u30BD" // カ - ソ u8"\u30BF\u30C1\u30C3\u30C4\u30C6\u30C8\u30CA\u30CB\u30CC\u30CD\u30CE" // タ - ノ u8"\u30CF\u30D2\u30D5\u30D8\u30DB\u30DE\u30DF\u30E0\u30E1\u30E2" // ハ - モ u8"\u30E3\u30E4\u30E5\u30E6\u30E7\u30E8\u30E9\u30EA\u30EB\u30EC\u30ED" // ャ - ロ u8"\u30EE\u30EF\u30F0\u30F1\u30F2\u30F3\u30F5\u30F6" // ヮ - ヶ }; // (半)濁点の付いていない全角片仮名はそのまま MISC::asc( fullwidth, output, table ); EXPECT_EQ( fullwidth, output ); EXPECT_EQ( output.size(), table.size() - 1 ); for( int i = 0, size = table.size(); i < size; ++i ) { EXPECT_EQ( i, table.at( i ) ); } } TEST_F(MISC_AscTest, fullwidth_katakana_precomposed_voiced_sound_mark) { std::string output; std::vector table; constexpr const char fullwidth[] { u8"\u30AC\u30AE\u30B0\u30B2\u30B4\u30B6\u30B8\u30BA\u30BC\u30BE" // ガ - ゾ u8"\u30C0\u30C2\u30C5\u30C7\u30C9\u30D0\u30D3\u30D6\u30D9\u30DC" // ダ - ボ u8"\u30F4\u30F7\u30F8\u30F9\u30FA" // ヴ - ヺ u8"\u30D1\u30D4\u30D7\u30DA0x30DD" // パ - ポ }; // 合成済み文字の(半)濁点付き全角片仮名はそのまま MISC::asc( fullwidth, output, table ); EXPECT_EQ( fullwidth, output ); EXPECT_EQ( output.size(), table.size() - 1 ); for( int i = 0, size = table.size(); i < size; ++i ) { EXPECT_EQ( i, table.at( i ) ); } } TEST_F(MISC_AscTest, fullwidth_katakana_combining_voiced_sound_mark) { std::string output; std::vector table; constexpr const char fullwidth[] { u8"\u30AB\u3099\u30AD\u3099\u30AF\u3099\u30B1\u3099\u30B3\u3099" // ガギグゲゴ u8"\u30AB\u309A\u30AD\u309A\u30AF\u309A\u30B1\u309A\u30B3\u309A" // カ゚キ゚ク゚ケ゚コ゚ }; // 合成済み文字の有無に関係なく(半)濁点の結合文字が付いている全角片仮名は変換されない // NOTE: 組み合わせが多いのでテストは網羅していない MISC::asc( fullwidth, output, table ); EXPECT_EQ( fullwidth, output ); EXPECT_EQ( output.size(), table.size() - 1 ); for( int i = 0, size = table.size(); i < size; ++i ) { EXPECT_EQ( i, table.at( i ) ); } } TEST_F(MISC_AscTest, fullwidth_hiragana_without_voiced_sound_mark) { std::string output; std::vector table; constexpr const char fullwidth[] { u8"\u3041\u3042\u3043\u3044\u3045\u3046\u3047\u3048\u3049\u304A" // ぁ - お u8"\u304B\u304D\u304F\u3051\u3053\u3055\u3057\u3059\u305B\u305D" // か - そ u8"\u305F\u3061\u3063\u3064\u3066\u3068\u306A\u306B\u306C\u306D\u306E" // た - の u8"\u306F\u3072\u3075\u3078\u307B\u307E\u307F\u3080\u3081\u3082" // は - も u8"\u3083\u3084\u3085\u3086\u3087\u3088\u3089\u308A\u308B\u308C\u308D" // ゃ - ろ u8"\u308E\u308F\u3090\u3091\u3092\u3093\u3095\u3096" // ゎ - ゖ }; // (半)濁点の付いていない全角平仮名はそのまま MISC::asc( fullwidth, output, table ); EXPECT_EQ( fullwidth, output ); EXPECT_EQ( output.size(), table.size() - 1 ); for( int i = 0, size = table.size(); i < size; ++i ) { EXPECT_EQ( i, table.at( i ) ); } } TEST_F(MISC_AscTest, fullwidth_hiragana_precomposed_voiced_sound_mark) { std::string output; std::vector table; constexpr const char fullwidth[] { u8"\u304C\u304E\u3050\u3052\u3054\u3056\u3058\u305A\u305C\u305E" // が - ぞ u8"\u3060\u3062\u3065\u3067\u3069\u3070\u3073\u3076\u3079\u307C" // だ - ぼ u8"\u3094" // ゔ u8"\u3071\u3074\u3077\u307A0x307D" // ぱ - ぽ }; // 合成済み文字の(半)濁点付き全角平仮名はそのまま MISC::asc( fullwidth, output, table ); EXPECT_EQ( fullwidth, output ); EXPECT_EQ( output.size(), table.size() - 1 ); for( int i = 0, size = table.size(); i < size; ++i ) { EXPECT_EQ( i, table.at( i ) ); } } TEST_F(MISC_AscTest, fullwidth_hiragana_combining_voiced_sound_mark) { std::string output; std::vector table; constexpr const char fullwidth[] { u8"\u304B\u3099\u304D\u3099\u304F\u3099\u3051\u3099\u3053\u3099" // がぎぐげご u8"\u304B\u309A\u304D\u309A\u304F\u309A\u3051\u309A\u3053\u309A" // か゚き゚く゚け゚こ゚ }; // 合成済み文字の有無に関係なく(半)濁点の結合文字が付いている全角平仮名は変換されない // NOTE: 組み合わせが多いのでテストは網羅していない MISC::asc( fullwidth, output, table ); EXPECT_EQ( fullwidth, output ); EXPECT_EQ( output.size(), table.size() - 1 ); for( int i = 0, size = table.size(); i < size; ++i ) { EXPECT_EQ( i, table.at( i ) ); } } class MISC_NormTest : public ::testing::Test {}; TEST_F(MISC_NormTest, empty_input) { std::string output; std::vector table; // 入力はヌル終端文字列 MISC::norm( "", output, &table ); EXPECT_EQ( "", output ); EXPECT_EQ( 0, output.size() ); // 文字列の終端(ヌル文字)の位置が追加されるためtableのサイズが+1大きくなる EXPECT_EQ( 1, table.size() ); EXPECT_EQ( 0, table.at( 0 ) ); } TEST_F(MISC_NormTest, halfwidth_latin_capital_letter) { std::string output; std::vector table; MISC::norm( "THE QUICK BROWN FOX JUMPS OVER THE LAZY DOG.", output, &table ); EXPECT_EQ( "THE QUICK BROWN FOX JUMPS OVER THE LAZY DOG.", output ); EXPECT_EQ( output.size(), table.size() - 1 ); for( int i = 0, size = table.size(); i < size; ++i ) { EXPECT_EQ( i, table.at( i ) ); } } TEST_F(MISC_NormTest, halfwidth_latin_small_letter) { std::string output; std::vector table; MISC::norm( "the quick brown fox jumps over the lazy dog.", output, &table ); EXPECT_EQ( "the quick brown fox jumps over the lazy dog.", output ); EXPECT_EQ( output.size(), table.size() - 1 ); for( int i = 0, size = table.size(); i < size; ++i ) { EXPECT_EQ( i, table.at( i ) ); } } TEST_F(MISC_NormTest, halfwidth_digit_sign) { std::string output; std::vector table; MISC::norm( "1234567890+-*/", output, &table ); EXPECT_EQ( "1234567890+-*/", output ); EXPECT_EQ( output.size(), table.size() - 1 ); for( int i = 0, size = table.size(); i < size; ++i ) { EXPECT_EQ( i, table.at( i ) ); } } TEST_F(MISC_NormTest, halfwidth_append_data) { std::string output = "123"; std::vector table = { 0, 1, 2 }; // アウトプット引数は初期化せずデータを追加する MISC::norm( "hello", output, &table ); EXPECT_EQ( "123hello", output ); EXPECT_EQ( output.size(), table.size() - 1 ); const std::vector expected_table = { 0, 1, 2, 0, 1, 2, 3, 4, 5 }; EXPECT_EQ( expected_table, table ); } std::vector norm_expected_table_fullwidth_quick_brown_fox() { // 全角英数字から半角英数字に変換したときの文字列の位置を保存しておくテーブルのテストデータ return { // TH E _ Q U I C K _0 B R O W N _ F O X _ 0, 3, 6, 9, 12, 15, 18, 21, 24, 27, 30, 33, 36, 39, 42, 45, 48, 51, 54, 57, // JU M P S _ O V E R _ T H E _ L A Z Y 60, 63, 66, 69, 72, 75, 78, 81, 84, 87, 90, 93, 96, 99, 102, 105, 108, 111, 114, // _ D O G . U+0000 117, 120, 123, 126, 129, 132, }; } TEST_F(MISC_NormTest, fullwidth_latin_capital_letter) { std::string output; std::vector table; MISC::norm( "THE QUICK BROWN FOX JUMPS OVER THE LAZY DOG.", output, &table ); // 和字間隔(U+3000)と全角ピリオド(U+FF0E)も半角スペースに変換される EXPECT_EQ( "THE QUICK BROWN FOX JUMPS OVER THE LAZY DOG.", output ); EXPECT_EQ( output.size(), table.size() - 1 ); EXPECT_EQ( norm_expected_table_fullwidth_quick_brown_fox(), table ); } TEST_F(MISC_NormTest, fullwidth_latin_small_letter) { std::string output; std::vector table; MISC::norm( "the quick brown fox jumps over the lazy dog.", output, &table ); EXPECT_EQ( "the quick brown fox jumps over the lazy dog.", output ); EXPECT_EQ( output.size(), table.size() - 1 ); EXPECT_EQ( norm_expected_table_fullwidth_quick_brown_fox(), table ); } TEST_F(MISC_NormTest, fullwidth_digit_sign) { std::string output; std::vector table; MISC::norm( "1234567890+−*/", output, &table ); // 全角Minus Sign(U+2212)以外は半角に変換される EXPECT_EQ( "1234567890+−*/", output ); EXPECT_EQ( output.size(), table.size() - 1 ); const std::vector expected_table = { 0, 3, 6, 9, 12, 15, 18, 21, 24, 27, 30, 33, 34, 35, 36, 39, 42 }; EXPECT_EQ( expected_table, table ); } TEST_F(MISC_NormTest, halfwidth_katakana_without_voiced_sound_mark) { std::string output; std::vector table; constexpr const char halfwidth[] { "\uFF61\uFF62\uFF63\uFF64\uFF65" "\uFF66" "\uFF67\uFF68\uFF69\uFF6A\uFF6B" "\uFF6C\uFF6D\uFF6E\uFF6F\uFF70" "\uFF71\uFF72\uFF73\uFF74\uFF75" "\uFF76\uFF77\uFF78\uFF79\uFF7A" "\uFF7B\uFF7C\uFF7D\uFF7E\uFF7F" "\uFF80\uFF81\uFF82\uFF83\uFF84" "\uFF85\uFF86\uFF87\uFF88\uFF89" "\uFF8A\uFF8B\uFF8C\uFF8D\uFF8E" "\uFF8F\uFF90\uFF91\uFF92\uFF93" "\uFF94\uFF95\uFF96" "\uFF97\uFF98\uFF99\uFF9A\uFF9B" "\uFF9C\uFF9D" }; constexpr const char fullwidth[] { "。「」、・" "ヲ" "ァィゥェォ" "ャュョッー" "アイウエオ" "カキクケコ" "サシスセソ" "タチツテト" "ナニヌネノ" "ハヒフヘホ" "マミムメモ" "ヤユヨ" "ラリルレロ" "ワン" }; // 半角片仮名から全角片仮名へ一対一の変換 MISC::norm( halfwidth, output, &table ); EXPECT_EQ( fullwidth, output ); EXPECT_EQ( output.size(), table.size() - 1 ); for( int i = 0, size = table.size(); i < size; ++i ) { EXPECT_EQ( i, table.at( i ) ); } } TEST_F(MISC_NormTest, halfwidth_katakana_only_voiced_sound_mark) { std::string output; std::vector table; // 半角の濁点と半濁点は全角結合文字に変換される MISC::norm( "\uFF9E\uFF9F", output, &table ); EXPECT_EQ( "\u3099\u309A", output ); EXPECT_EQ( output.size(), table.size() - 1 ); for( int i = 0, size = table.size(); i < size; ++i ) { EXPECT_EQ( i, table.at( i ) ); } } TEST_F(MISC_NormTest, halfwidth_katakana_combining_voiced_sound_mark_to_decomposed) { std::string output; std::vector table; constexpr const char halfwidth[] = { "\uFF76\uFF9E\uFF77\uFF9E\uFF78\uFF9E\uFF79\uFF9E\uFF7A\uFF9E" "\uFF7B\uFF9E\uFF7C\uFF9E\uFF7D\uFF9E\uFF7E\uFF9E\uFF7F\uFF9E" "\uFF80\uFF9E\uFF81\uFF9E\uFF82\uFF9E\uFF83\uFF9E\uFF84\uFF9E" "\uFF8A\uFF9E\uFF8B\uFF9E\uFF8C\uFF9E\uFF8D\uFF9E\uFF8E\uFF9E" "\uFF8A\uFF9F\uFF8B\uFF9F\uFF8C\uFF9F\uFF8D\uFF9F\uFF8E\uFF9F" }; constexpr const char fullwidth[] { "\u30AB\u3099\u30AD\u3099\u30AF\u3099\u30B1\u3099\u30B3\u3099" // ガギグゲゴ "\u30B5\u3099\u30B7\u3099\u30B9\u3099\u30BB\u3099\u30BD\u3099" // ザジズゼゾ "\u30BF\u3099\u30C1\u3099\u30C4\u3099\u30C6\u3099\u30C8\u3099" // ダヂヅデド "\u30CF\u3099\u30D2\u3099\u30D5\u3099\u30D8\u3099\u30DB\u3099" // バビブベボ "\u30CF\u309A\u30D2\u309A\u30D5\u309A\u30D8\u309A\u30DB\u309A" // パピプペポ }; // 合成済み文字が存在する(半)濁点付き半角片仮名は全角片仮名と結合文字へ変換される MISC::norm( halfwidth, output, &table ); EXPECT_EQ( fullwidth, output ); EXPECT_EQ( output.size(), table.size() - 1 ); std::vector expected_table( table.size() ); std::iota( expected_table.begin(), expected_table.end(), 0 ); EXPECT_EQ( expected_table, table ); } TEST_F(MISC_NormTest, halfwidth_katakana_combining_voiced_sound_mark_wagyo) { std::string output; std::vector table; // 濁点付き半角片仮名のウやワ行は全角片仮名と結合文字へ変換される MISC::norm( "\uFF73\uFF9E\uFF9C\uFF9E\uFF66\uFF9E", output, &table ); EXPECT_EQ( "\u30A6\u3099\u30EF\u3099\u30F2\u3099", output ); EXPECT_EQ( output.size(), table.size() - 1 ); std::vector expected_table( table.size() ); std::iota( expected_table.begin(), expected_table.end(), 0 ); EXPECT_EQ( expected_table, table ); } TEST_F(MISC_NormTest, halfwidth_katakana_combining_voiced_sound_mark_through) { std::string output; std::vector table; // 合成済み文字が存在しない(半)濁点付き半角片仮名は全角片仮名と結合文字に変換される : カ゚キ゚ク゚ケ゚コ゚ // NOTE: 組み合わせが多いのでテストは網羅していない MISC::norm( "\uFF76\uFF9F\uFF77\uFF9F\uFF78\uFF9F\uFF79\uFF9F\uFF7A\uFF9F", output, &table ); EXPECT_EQ( "\u30AB\u309A\u30AD\u309A\u30AF\u309A\u30B1\u309A\u30B3\u309A", output ); EXPECT_EQ( output.size(), table.size() - 1 ); for( int i = 0, size = table.size(); i < size; ++i ) { EXPECT_EQ( i, table.at( i ) ); } } TEST_F(MISC_NormTest, fullwidth_katakana_without_voiced_sound_mark) { std::string output; std::vector table; constexpr const char fullwidth[] { "\u30A1\u30A2\u30A3\u30A4\u30A5\u30A6\u30A7\u30A8\u30A9\u30AA" // ァ - オ "\u30AB\u30AD\u30AF\u30B1\u30B3\u30B5\u30B7\u30B9\u30BB\u30BD" // カ - ソ "\u30BF\u30C1\u30C3\u30C4\u30C6\u30C8\u30CA\u30CB\u30CC\u30CD\u30CE" // タ - ノ "\u30CF\u30D2\u30D5\u30D8\u30DB\u30DE\u30DF\u30E0\u30E1\u30E2" // ハ - モ "\u30E3\u30E4\u30E5\u30E6\u30E7\u30E8\u30E9\u30EA\u30EB\u30EC\u30ED" // ャ - ロ "\u30EE\u30EF\u30F0\u30F1\u30F2\u30F3\u30F5\u30F6" // ヮ - ヶ }; // (半)濁点の付いていない全角片仮名はそのまま MISC::norm( fullwidth, output, &table ); EXPECT_EQ( fullwidth, output ); EXPECT_EQ( output.size(), table.size() - 1 ); for( int i = 0, size = table.size(); i < size; ++i ) { EXPECT_EQ( i, table.at( i ) ); } } TEST_F(MISC_NormTest, fullwidth_katakana_precomposed_voiced_sound_mark) { std::string output; std::vector table; constexpr const char fullwidth[] { "\u30AC\u30AE\u30B0\u30B2\u30B4\u30B6\u30B8\u30BA\u30BC\u30BE" // ガ - ゾ "\u30C0\u30C2\u30C5\u30C7\u30C9\u30D0\u30D3\u30D6\u30D9\u30DC" // ダ - ボ "\u30F4\u30F7\u30F8\u30F9\u30FA" // ヴ - ヺ "\u30D1\u30D4\u30D7\u30DA\u30DD" // パ - ポ }; constexpr const char fullwidth_combining[] { "\u30AB\u3099\u30AD\u3099\u30AF\u3099\u30B1\u3099\u30B3\u3099" // ガギグゲゴ "\u30B5\u3099\u30B7\u3099\u30B9\u3099\u30BB\u3099\u30BD\u3099" // ザジズゼゾ "\u30BF\u3099\u30C1\u3099\u30C4\u3099\u30C6\u3099\u30C8\u3099" // ダヂヅデド "\u30CF\u3099\u30D2\u3099\u30D5\u3099\u30D8\u3099\u30DB\u3099" // バビブベボ "\u30A6\u3099\u30EF\u3099\u30F0\u3099\u30F1\u3099\u30F2\u3099" // ヴヷヸヹヺ "\u30CF\u309A\u30D2\u309A\u30D5\u309A\u30D8\u309A\u30DB\u309A" // パピプペポ }; // 合成済み文字の(半)濁点付き全角片仮名は結合文字が分解される MISC::norm( fullwidth, output, &table ); EXPECT_EQ( fullwidth_combining, output ); EXPECT_EQ( output.size(), table.size() - 1 ); for( std::size_t i = 0; i < table.size() - 6; i += 6 ) { EXPECT_EQ( i / 2 + 0, table.at(i + 0) ); EXPECT_EQ( i / 2 + 1, table.at(i + 1) ); EXPECT_EQ( i / 2 + 2, table.at(i + 2) ); EXPECT_EQ( i / 2 + 3, table.at(i + 3) ); EXPECT_EQ( i / 2 + 4, table.at(i + 4) ); EXPECT_EQ( i / 2 + 5, table.at(i + 5) ); } } TEST_F(MISC_NormTest, fullwidth_katakana_combining_voiced_sound_mark) { std::string output; std::vector table; constexpr const char fullwidth[] { "\u30AB\u3099\u30AD\u3099\u30AF\u3099\u30B1\u3099\u30B3\u3099" // ガギグゲゴ "\u30AB\u309A\u30AD\u309A\u30AF\u309A\u30B1\u309A\u30B3\u309A" // カ゚キ゚ク゚ケ゚コ゚ }; // 合成済み文字の有無に関係なく(半)濁点の結合文字が付いている全角片仮名は変換されない // NOTE: 組み合わせが多いのでテストは網羅していない MISC::norm( fullwidth, output, &table ); EXPECT_EQ( fullwidth, output ); EXPECT_EQ( output.size(), table.size() - 1 ); for( int i = 0, size = table.size(); i < size; ++i ) { EXPECT_EQ( i, table.at( i ) ); } } TEST_F(MISC_NormTest, fullwidth_hiragana_without_voiced_sound_mark) { std::string output; std::vector table; constexpr const char fullwidth[] { "\u3041\u3042\u3043\u3044\u3045\u3046\u3047\u3048\u3049\u304A" // ぁ - お "\u304B\u304D\u304F\u3051\u3053\u3055\u3057\u3059\u305B\u305D" // か - そ "\u305F\u3061\u3063\u3064\u3066\u3068\u306A\u306B\u306C\u306D\u306E" // た - の "\u306F\u3072\u3075\u3078\u307B\u307E\u307F\u3080\u3081\u3082" // は - も "\u3083\u3084\u3085\u3086\u3087\u3088\u3089\u308A\u308B\u308C\u308D" // ゃ - ろ "\u308E\u308F\u3090\u3091\u3092\u3093\u3095\u3096" // ゎ - ゖ }; // (半)濁点の付いていない全角平仮名はそのまま MISC::norm( fullwidth, output, &table ); EXPECT_EQ( fullwidth, output ); EXPECT_EQ( output.size(), table.size() - 1 ); for( int i = 0, size = table.size(); i < size; ++i ) { EXPECT_EQ( i, table.at( i ) ); } } TEST_F(MISC_NormTest, fullwidth_hiragana_precomposed_voiced_sound_mark) { std::string output; std::vector table; constexpr const char fullwidth[] { "\u304C\u304E\u3050\u3052\u3054\u3056\u3058\u305A\u305C\u305E" // が - ぞ "\u3060\u3062\u3065\u3067\u3069\u3070\u3073\u3076\u3079\u307C" // だ - ぼ "\u3094" // ゔ "\u3071\u3074\u3077\u307A\u307D" // ぱ - ぽ }; constexpr const char fullwidth_combining[] { "\u304B\u3099\u304D\u3099\u304F\u3099\u3051\u3099\u3053\u3099" "\u3055\u3099\u3057\u3099\u3059\u3099\u305B\u3099\u305D\u3099" "\u305F\u3099\u3061\u3099\u3064\u3099\u3066\u3099\u3068\u3099" "\u306F\u3099\u3072\u3099\u3075\u3099\u3078\u3099\u307B\u3099" "\u3046\u3099" "\u306F\u309A\u3072\u309A\u3075\u309A\u3078\u309A\u307B\u309A" }; // 合成済み文字の(半)濁点付き全角平仮名は結合文字が分解される MISC::norm( fullwidth, output, &table ); EXPECT_EQ( fullwidth_combining, output ); EXPECT_EQ( output.size(), table.size() - 1 ); for( std::size_t i = 0; i < table.size() - 6; i += 6 ) { EXPECT_EQ( i / 2 + 0, table.at(i + 0) ); EXPECT_EQ( i / 2 + 1, table.at(i + 1) ); EXPECT_EQ( i / 2 + 2, table.at(i + 2) ); EXPECT_EQ( i / 2 + 3, table.at(i + 3) ); EXPECT_EQ( i / 2 + 4, table.at(i + 4) ); EXPECT_EQ( i / 2 + 5, table.at(i + 5) ); } } TEST_F(MISC_NormTest, fullwidth_hiragana_combining_voiced_sound_mark) { std::string output; std::vector table; constexpr const char fullwidth[] { "\u304B\u3099\u304D\u3099\u304F\u3099\u3051\u3099\u3053\u3099" // がぎぐげご "\u304B\u309A\u304D\u309A\u304F\u309A\u3051\u309A\u3053\u309A" // か゚き゚く゚け゚こ゚ }; // 合成済み文字の有無に関係なく(半)濁点の結合文字が付いている全角平仮名は変換されない // NOTE: 組み合わせが多いのでテストは網羅していない MISC::norm( fullwidth, output, &table ); EXPECT_EQ( fullwidth, output ); EXPECT_EQ( output.size(), table.size() - 1 ); for( int i = 0, size = table.size(); i < size; ++i ) { EXPECT_EQ( i, table.at( i ) ); } } class MISC_StartsWith : public ::testing::Test {}; TEST_F(MISC_StartsWith, null_terminated_string_with_zero_length) { EXPECT_TRUE( MISC::starts_with( "", "" ) ); EXPECT_TRUE( MISC::starts_with( "helloworld", "" ) ); EXPECT_FALSE( MISC::starts_with( "", "helloworld" ) ); } TEST_F(MISC_StartsWith, null_terminated_string) { EXPECT_TRUE( MISC::starts_with( "hello", "hello" ) ); EXPECT_TRUE( MISC::starts_with( "helloworld", "hello" ) ); EXPECT_FALSE( MISC::starts_with( "hello", "helloworld" ) ); } class MISC_ParseHtmlFormData : public ::testing::Test {}; TEST_F(MISC_ParseHtmlFormData, empty_html) { auto result = MISC::parse_html_form_data( std::string{} ); EXPECT_TRUE( result.empty() ); } TEST_F(MISC_ParseHtmlFormData, hidden_datum) { auto result = MISC::parse_html_form_data( "" ); decltype(result) expect{ { "FOO", "100" } }; EXPECT_EQ( result, expect ); } TEST_F(MISC_ParseHtmlFormData, hidden_datum_uppercase) { auto result = MISC::parse_html_form_data( "" ); decltype(result) expect{ { "FOO", "100" } }; EXPECT_EQ( result, expect ); } TEST_F(MISC_ParseHtmlFormData, hidden_datum_with_double_quotes) { auto result = MISC::parse_html_form_data( R"()" ); decltype(result) expect{ { "FOO", "100" } }; EXPECT_EQ( result, expect ); } TEST_F(MISC_ParseHtmlFormData, submit_datum) { auto result = MISC::parse_html_form_data( "" ); decltype(result) expect{ { "submit", "書き込む" } }; EXPECT_EQ( result, expect ); } TEST_F(MISC_ParseHtmlFormData, submit_datum_with_double_quotes) { auto result = MISC::parse_html_form_data( R"()" ); decltype(result) expect{ { "submit", "書き込む" } }; EXPECT_EQ( result, expect ); } TEST_F(MISC_ParseHtmlFormData, textarea_datum) { auto result = MISC::parse_html_form_data( "" ); decltype(result) expect{ { "MESSAGE", "あかさたな" } }; EXPECT_EQ( result, expect ); } TEST_F(MISC_ParseHtmlFormData, textarea_have_tag) { auto result = MISC::parse_html_form_data( "" ); decltype(result) expect{ { "MESSAGE", "あか
さたな" } }; EXPECT_EQ( result, expect ); } TEST_F(MISC_ParseHtmlFormData, textarea_datum_uppercase) { auto result = MISC::parse_html_form_data( "" ); decltype(result) expect{ { "MESSAGE", "あかさたな" } }; EXPECT_EQ( result, expect ); } TEST_F(MISC_ParseHtmlFormData, textarea_datum_with_double_quotes) { auto result = MISC::parse_html_form_data( R"()" ); decltype(result) expect{ { "MESSAGE", "あかさたな" } }; EXPECT_EQ( result, expect ); } TEST_F(MISC_ParseHtmlFormData, multi_data_1) { auto result = MISC::parse_html_form_data( R"()" "" R"()" ); decltype(result) expect{ { "MESSAGE", "あかさたな" }, { "submit", "書き込む" }, { "FOO", "100" } }; EXPECT_EQ( result, expect ); } TEST_F(MISC_ParseHtmlFormData, multi_data_2) { auto result = MISC::parse_html_form_data( "" R"()" R"()" ); decltype(result) expect{ { "submit", "書き込む" }, { "FOO", "100" }, { "MESSAGE", "あかさたな" } }; EXPECT_EQ( result, expect ); } TEST_F(MISC_ParseHtmlFormData, multi_data_3) { auto result = MISC::parse_html_form_data( R"()" R"()" "" ); decltype(result) expect{ { "FOO", "100" }, { "MESSAGE", "あかさたな" }, { "submit", "書き込む" } }; EXPECT_EQ( result, expect ); } TEST_F(MISC_ParseHtmlFormData, hidden_empty_name_by_double_quotes) { auto result = MISC::parse_html_form_data( R"()" ); decltype(result) expect{ { std::string{}, "100" } }; EXPECT_EQ( result, expect ); } TEST_F(MISC_ParseHtmlFormData, hidden_empty_value_by_double_quotes) { auto result = MISC::parse_html_form_data( R"()" ); decltype(result) expect{ { "FOO", std::string{} } }; EXPECT_EQ( result, expect ); } TEST_F(MISC_ParseHtmlFormData, submit_empty_name_by_double_quotes) { auto result = MISC::parse_html_form_data( R"()" ); decltype(result) expect{ { std::string{}, "書き込む" } }; EXPECT_EQ( result, expect ); } TEST_F(MISC_ParseHtmlFormData, submit_empty_value_by_double_quotes) { auto result = MISC::parse_html_form_data( R"()" ); decltype(result) expect{ { "submit", std::string{} } }; EXPECT_EQ( result, expect ); } TEST_F(MISC_ParseHtmlFormData, textarea_empty_name_by_double_quotes) { auto result = MISC::parse_html_form_data( R"()" ); decltype(result) expect{ { std::string{}, "あかさたな" } }; EXPECT_EQ( result, expect ); } TEST_F(MISC_ParseHtmlFormData, textarea_empty_value_by_double_quotes) { auto result = MISC::parse_html_form_data( R"()" ); decltype(result) expect{ { "MESSAGE", std::string{} } }; EXPECT_EQ( result, expect ); } class MISC_ParseHtmlFormActionTest : public ::testing::Test {}; TEST_F(MISC_ParseHtmlFormActionTest, empty_html) { std::string result = MISC::parse_html_form_action( std::string{} ); EXPECT_EQ( result, std::string{} ); } TEST_F(MISC_ParseHtmlFormActionTest, unquote_post) { const std::string html = R"(")"; std::string result = MISC::parse_html_form_action( html ); EXPECT_EQ( result, "/test/bbs.cgi" ); } TEST_F(MISC_ParseHtmlFormActionTest, quote_post) { const std::string html = R"(")"; std::string result = MISC::parse_html_form_action( html ); EXPECT_EQ( result, "/test/bbs.cgi" ); } TEST_F(MISC_ParseHtmlFormActionTest, subbbscgi) { const std::string html = R"(")"; std::string result = MISC::parse_html_form_action( html ); EXPECT_EQ( result, "/test/subbbs.cgi" ); } TEST_F(MISC_ParseHtmlFormActionTest, with_parameter) { const std::string html = R"(")"; std::string result = MISC::parse_html_form_action( html ); EXPECT_EQ( result, "/test/bbs.cgi?foo=bar" ); } TEST_F(MISC_ParseHtmlFormActionTest, ignore_attributes) { const std::string html = R"(")"; std::string result = MISC::parse_html_form_action( html ); EXPECT_EQ( result, "/test/bbs.cgi" ); } TEST_F(MISC_ParseHtmlFormActionTest, relative_path_an_upper_order) { const std::string html = R"(")"; std::string result = MISC::parse_html_form_action( html ); EXPECT_EQ( result, "/test/bbs.cgi" ); } TEST_F(MISC_ParseHtmlFormActionTest, relative_path_same_hierarchy) { std::string html = R"(")"; std::string result = MISC::parse_html_form_action( html ); EXPECT_EQ( result, "/test/bbs.cgi" ); html = R"(")"; result = MISC::parse_html_form_action( html ); EXPECT_EQ( result, "/test/subbbs.cgi" ); } TEST_F(MISC_ParseHtmlFormActionTest, relative_path_double_upper_orders) { const std::string html = R"(")"; std::string result = MISC::parse_html_form_action( html ); EXPECT_EQ( result, std::string{} ); } TEST_F(MISC_ParseHtmlFormActionTest, relative_path_middle_upper_order) { const std::string html = R"(")"; std::string result = MISC::parse_html_form_action( html ); EXPECT_EQ( result, std::string{} ); } TEST_F(MISC_ParseHtmlFormActionTest, absolute_path_without_scheme) { const std::string html = R"(")"; std::string result = MISC::parse_html_form_action( html ); EXPECT_EQ( result, std::string{} ); } TEST_F(MISC_ParseHtmlFormActionTest, http_url) { const std::string html = R"(")"; std::string result = MISC::parse_html_form_action( html ); EXPECT_EQ( result, std::string{} ); } TEST_F(MISC_ParseHtmlFormActionTest, https_url) { const std::string html = R"(")"; std::string result = MISC::parse_html_form_action( html ); EXPECT_EQ( result, std::string{} ); } class MISC_ParseCharsetFromHtmlMetaTest : public ::testing::Test {}; TEST_F(MISC_ParseCharsetFromHtmlMetaTest, empty_html) { std::string result = MISC::parse_charset_from_html_meta( std::string{} ); EXPECT_EQ( result, std::string{} ); } TEST_F(MISC_ParseCharsetFromHtmlMetaTest, http_equiv) { const std::string html = R"()"; std::string result = MISC::parse_charset_from_html_meta( html ); EXPECT_EQ( result, "UTF-8" ); } TEST_F(MISC_ParseCharsetFromHtmlMetaTest, http_equiv_uppercase) { const std::string html = R"()"; std::string result = MISC::parse_charset_from_html_meta( html ); EXPECT_EQ( result, "Shift_JIS" ); } TEST_F(MISC_ParseCharsetFromHtmlMetaTest, http_equiv_trim) { const std::string html = R"()"; std::string result = MISC::parse_charset_from_html_meta( html ); EXPECT_EQ( result, "EUC-JP" ); } TEST_F(MISC_ParseCharsetFromHtmlMetaTest, charset) { const std::string html = R"()"; std::string result = MISC::parse_charset_from_html_meta( html ); EXPECT_EQ( result, "UTF-8" ); } TEST_F(MISC_ParseCharsetFromHtmlMetaTest, charset_uppercase) { const std::string html = R"()"; std::string result = MISC::parse_charset_from_html_meta( html ); EXPECT_EQ( result, "Shift_JIS" ); } TEST_F(MISC_ParseCharsetFromHtmlMetaTest, charset_trim) { const std::string html = R"()"; std::string result = MISC::parse_charset_from_html_meta( html ); EXPECT_EQ( result, "EUC-JP" ); } TEST_F(MISC_ParseCharsetFromHtmlMetaTest, charset_no_quote) { const std::string html = R"()"; std::string result = MISC::parse_charset_from_html_meta( html ); EXPECT_EQ( result, "latin1" ); } TEST_F(MISC_ParseCharsetFromHtmlMetaTest, choose_first_meta_tag) { const std::string html = R"()" R"()"; std::string result = MISC::parse_charset_from_html_meta( html ); EXPECT_EQ( result, "utf8" ); } class ToPlainTest : public ::testing::Test {}; TEST_F(ToPlainTest, empty) { const std::string result = MISC::to_plain( std::string{} ); EXPECT_EQ( result, std::string{} ); } TEST_F(ToPlainTest, no_conversion) { const std::string result = MISC::to_plain( "hello 世界" ); EXPECT_EQ( result, "hello 世界" ); } TEST_F(ToPlainTest, decimal_hello) { const std::string result = MISC::to_plain( "hello" ); EXPECT_EQ( result, "hello" ); } TEST_F(ToPlainTest, hexadecimal_hello) { const std::string result = MISC::to_plain( "hello" ); EXPECT_EQ( result, "hello" ); } TEST_F(ToPlainTest, escape_html_char_completely) { const std::string input = "<>&" <>&" <>&""; const std::string result = MISC::to_plain( input ); EXPECT_EQ( result, R"(<>&" <>&" <>&")" ); } TEST_F(ToPlainTest, flatten_tags) { const std::string input = "Hello世界QuickBrown Fox"; const std::string result = MISC::to_plain( input ); EXPECT_EQ( result, "Hello世界QuickBrown Fox" ); } TEST_F(ToPlainTest, broken_tags) { std::string input = "Hello世界oo>Quick Brown Fox"; std::string result = MISC::to_plain( input ); EXPECT_EQ( result, "Hello世界oo>Quick Brown Fox" ); input = "Hello世界>QuickQuick" ); } class ToMarkupTest : public ::testing::Test {}; TEST_F(ToMarkupTest, empty) { const std::string result = MISC::to_markup( std::string{} ); EXPECT_EQ( result, std::string{} ); } TEST_F(ToMarkupTest, no_conversion) { const std::string result = MISC::to_markup( "hello 世界" ); EXPECT_EQ( result, "hello 世界" ); } TEST_F(ToMarkupTest, decimal_hello) { const std::string result = MISC::to_markup( "hello" ); EXPECT_EQ( result, "hello" ); } TEST_F(ToMarkupTest, hexadecimal_hello) { const std::string result = MISC::to_markup( "hello" ); EXPECT_EQ( result, "hello" ); } TEST_F(ToMarkupTest, escape_html_char_completely) { // 動作の根拠がはっきりしていないが " は " に変換される const std::string input = "<>&" <>&" <>&""; const std::string result = MISC::to_markup( input ); EXPECT_EQ( result, R"(<>&" <>&" <>&")" ); } TEST_F(ToMarkupTest, flatten_tags) { const std::string input = "Hello世界QuickBrown Fox"; const std::string result = MISC::to_markup( input ); EXPECT_EQ( result, "Hello世界QuickBrown Fox" ); } TEST_F(ToMarkupTest, broken_tags) { std::string input = "Hello世界oo>Quick Brown Fox"; std::string result = MISC::to_markup( input ); EXPECT_EQ( result, "Hello世界oo>Quick Brown Fox" ); input = "Hello世界>QuickQuick" ); } TEST_F(ToMarkupTest, span_tags) { std::string input = "Hello世界Quick Brown Fox"; std::string result = MISC::to_markup( input ); EXPECT_EQ( result, "Hello世界Quick Brown Fox" ); input = R"(Hello 世界QuickBrown Fox)"; result = MISC::to_markup( input ); EXPECT_EQ( result, R"(Hello 世界QuickBrown Fox)" ); } TEST_F(ToMarkupTest, mark_tags) { std::string input = "Hello世界Quick Brown Fox"; std::string result = MISC::to_markup( input ); EXPECT_EQ( result, R"(Hello世界Quick Brown Fox)" ); input = R"(Hello世界Quick Brown Fox)"; result = MISC::to_markup( input ); EXPECT_EQ( result, "Hello世界Quick Brown Fox" ); } class ChrefDecodeTest : public ::testing::Test {}; TEST_F(ChrefDecodeTest, empty) { const std::string result = MISC::chref_decode( std::string{}, true ); EXPECT_EQ( result, std::string{} ); } TEST_F(ChrefDecodeTest, decimal_hello) { const std::string result = MISC::chref_decode( std::string{ "hello" }, true ); EXPECT_EQ( result, "hello" ); } TEST_F(ChrefDecodeTest, hexadecimal_hello) { const std::string result = MISC::chref_decode( std::string{ "hello" }, true ); EXPECT_EQ( result, "hello" ); } TEST_F(ChrefDecodeTest, escape_html_char_completely) { const std::string input = "<>&" <>&" <>&""; const std::string result = MISC::chref_decode( input, true ); EXPECT_EQ( result, R"(<>&" <>&" <>&")" ); } TEST_F(ChrefDecodeTest, escape_html_char_keeping) { const std::string input = "<>&" <>&" <>&""; const std::string result = MISC::chref_decode( input, false ); EXPECT_EQ( result, "<>&" <>&" <>&"" ); } class UrlDecodeTest : public ::testing::Test {}; TEST_F(UrlDecodeTest, empty) { EXPECT_EQ( "", MISC::url_decode( "" ) ); } TEST_F(UrlDecodeTest, not_decode) { EXPECT_EQ( "http://foobar.test?a=1&b=c", MISC::url_decode( "http://foobar.test?a=1&b=c" ) ); } TEST_F(UrlDecodeTest, decode_hiragana) { constexpr const char* url = "http://hira.test/%E3%81%82%E3%81%84%E3%81%86%E3%81%88%E3%81%8A"; EXPECT_EQ( "http://hira.test/あいうえお", MISC::url_decode( url ) ); } TEST_F(UrlDecodeTest, decode_plus_sign) { constexpr const char* url = "http://plus.test/Quick+Brown+Fox"; EXPECT_EQ( "http://plus.test/Quick Brown Fox", MISC::url_decode( url ) ); } TEST_F(UrlDecodeTest, out_of_range_segments) { constexpr const char* url = "http://out.test/%41%4G%61%G1%%a"; EXPECT_EQ( "http://out.test/A%4Ga%G1%%a", MISC::url_decode( url ) ); } class MISC_UrlEncodeTest : public ::testing::Test {}; TEST_F(MISC_UrlEncodeTest, empty_string) { std::string_view input = ""; EXPECT_EQ( "", MISC::url_encode( input ) ); } TEST_F(MISC_UrlEncodeTest, unencoded_ascii_characters) { std::string_view input = "0123456789_ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz*-._"; EXPECT_EQ( input, MISC::url_encode( input ) ); } TEST_F(MISC_UrlEncodeTest, u0020) { std::string_view input = " "; EXPECT_EQ( "%20", MISC::url_encode( input ) ); } TEST_F(MISC_UrlEncodeTest, u000A) { std::string_view input = "quick\nbrown\n\nfox"; EXPECT_EQ( "quick%0Abrown%0A%0Afox", MISC::url_encode( input ) ); } TEST_F(MISC_UrlEncodeTest, u000D) { std::string_view input = "quick\rbrown\r\rfox"; EXPECT_EQ( "quick%0Dbrown%0D%0Dfox", MISC::url_encode( input ) ); } TEST_F(MISC_UrlEncodeTest, u000D_u000A) { std::string_view input = "quick\r\nbrown\r\n\r\nfox"; EXPECT_EQ( "quick%0D%0Abrown%0D%0A%0D%0Afox", MISC::url_encode( input ) ); } TEST_F(MISC_UrlEncodeTest, encoded_ascii_characters) { std::string_view input = "!\"#$%&\'()_+,_/_:;<=>?@_[\\]^_`_{|}~"; std::string_view result = "%21%22%23%24%25%26%27%28%29_%2B%2C_%2F_" "%3A%3B%3C%3D%3E%3F%40_%5B%5C%5D%5E_%60_%7B%7C%7D%7E"; EXPECT_EQ( result, MISC::url_encode( input ) ); } TEST_F(MISC_UrlEncodeTest, encoded_utf8) { std::string_view input = "\xE3\x81\x82"; // U+3042 std::string_view result = "%E3%81%82"; EXPECT_EQ( result, MISC::url_encode( input ) ); } TEST_F(MISC_UrlEncodeTest, encoded_shift_jis) { std::string_view input = "\x82\xA0"; // HIRAGANA A std::string_view result = "%82%A0"; EXPECT_EQ( result, MISC::url_encode( input ) ); } TEST_F(MISC_UrlEncodeTest, url_utf8) { std::string_view input = "https://jdim.test/い ろ/は?に=ほ&へ=と z"; std::string_view result = "https%3A%2F%2Fjdim.test%2F%E3%81%84%20%E3%82%8D%2F" "%E3%81%AF%3F%E3%81%AB%3D%E3%81%BB%26%E3%81%B8%3D%E3%81%A8%20z"; EXPECT_EQ( result, MISC::url_encode( input ) ); } class MISC_UrlEncodeWithEncodingTest : public ::testing::Test {}; TEST_F(MISC_UrlEncodeWithEncodingTest, empty_string) { std::string input = ""; EXPECT_EQ( "", MISC::url_encode( input, Encoding::sjis ) ); } TEST_F(MISC_UrlEncodeWithEncodingTest, unencoded_ascii_characters) { std::string input = "0123456789_ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz*-._"; EXPECT_EQ( input, MISC::url_encode( input, Encoding::sjis ) ); } TEST_F(MISC_UrlEncodeWithEncodingTest, u0020) { std::string input = " "; EXPECT_EQ( "%20", MISC::url_encode( input, Encoding::sjis ) ); } TEST_F(MISC_UrlEncodeWithEncodingTest, u000A) { std::string input = "quick\nbrown\n\nfox"; EXPECT_EQ( "quick%0Abrown%0A%0Afox", MISC::url_encode( input, Encoding::sjis ) ); } TEST_F(MISC_UrlEncodeWithEncodingTest, u000D) { std::string input = "quick\rbrown\r\rfox"; EXPECT_EQ( "quick%0Dbrown%0D%0Dfox", MISC::url_encode( input, Encoding::sjis ) ); } TEST_F(MISC_UrlEncodeWithEncodingTest, u000D_u000A) { std::string input = "quick\r\nbrown\r\n\r\nfox"; EXPECT_EQ( "quick%0D%0Abrown%0D%0A%0D%0Afox", MISC::url_encode( input, Encoding::sjis ) ); } TEST_F(MISC_UrlEncodeWithEncodingTest, encoded_ascii_characters) { std::string input = "!\"#$%&\'()_+,_/_:;<=>?@_[\\]^_`_{|}~"; std::string_view result = "%21%22%23%24%25%26%27%28%29_%2B%2C_%2F_" "%3A%3B%3C%3D%3E%3F%40_%5B%5C%5D%5E_%60_%7B%7C%7D%7E"; EXPECT_EQ( result, MISC::url_encode( input, Encoding::sjis ) ); } TEST_F(MISC_UrlEncodeWithEncodingTest, encoded_to_ms932) { std::string input = "\xE3\x81\x82"; // U+3042 std::string_view result = "%82%A0"; EXPECT_EQ( result, MISC::url_encode( input, Encoding::sjis ) ); } TEST_F(MISC_UrlEncodeWithEncodingTest, encoded_to_eucjp) { std::string input = "\xE3\x81\x82"; // U+3042 std::string_view result = "%A4%A2"; EXPECT_EQ( result, MISC::url_encode( input, Encoding::eucjp ) ); } TEST_F(MISC_UrlEncodeWithEncodingTest, url_to_ms932) { std::string input = "https://jdim.test/い ろ/は?に=ほ&へ=と z"; std::string_view result = "https%3A%2F%2Fjdim.test%2F%82%A2%20%82%EB%2F" "%82%CD%3F%82%C9%3D%82%D9%26%82%D6%3D%82%C6%20z"; EXPECT_EQ( result, MISC::url_encode( input, Encoding::sjis ) ); } class MISC_UrlEncodePlusTest : public ::testing::Test {}; TEST_F(MISC_UrlEncodePlusTest, empty_string) { std::string_view input = ""; EXPECT_EQ( "", MISC::url_encode_plus( input ) ); } TEST_F(MISC_UrlEncodePlusTest, unencoded_ascii_characters) { std::string_view input = "0123456789_ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz*-._"; EXPECT_EQ( input, MISC::url_encode_plus( input ) ); } TEST_F(MISC_UrlEncodePlusTest, u0020) { std::string_view input = " "; EXPECT_EQ( "+", MISC::url_encode_plus( input ) ); } TEST_F(MISC_UrlEncodePlusTest, u000A) { std::string_view input = "quick\nbrown\n\nfox"; EXPECT_EQ( "quick%0D%0Abrown%0D%0A%0D%0Afox", MISC::url_encode_plus( input ) ); } TEST_F(MISC_UrlEncodePlusTest, u000D) { std::string_view input = "quick\rbrown\r\rfox"; EXPECT_EQ( "quickbrownfox", MISC::url_encode_plus( input ) ); } TEST_F(MISC_UrlEncodePlusTest, u000D_u000A) { std::string_view input = "quick\r\nbrown\r\n\r\nfox"; EXPECT_EQ( "quick%0D%0Abrown%0D%0A%0D%0Afox", MISC::url_encode_plus( input ) ); } TEST_F(MISC_UrlEncodePlusTest, encoded_ascii_characters) { std::string_view input = "!\"#$%&\'()_+,_/_:;<=>?@_[\\]^_`_{|}~"; std::string_view result = "%21%22%23%24%25%26%27%28%29_%2B%2C_%2F_" "%3A%3B%3C%3D%3E%3F%40_%5B%5C%5D%5E_%60_%7B%7C%7D%7E"; EXPECT_EQ( result, MISC::url_encode_plus( input ) ); } TEST_F(MISC_UrlEncodePlusTest, encoded_utf8) { std::string_view input = "\xE3\x81\x82"; // U+3042 std::string_view result = "%E3%81%82"; EXPECT_EQ( result, MISC::url_encode_plus( input ) ); } TEST_F(MISC_UrlEncodePlusTest, encoded_shift_jis) { std::string_view input = "\x82\xA0"; // HIRAGANA A std::string_view result = "%82%A0"; EXPECT_EQ( result, MISC::url_encode_plus( input ) ); } TEST_F(MISC_UrlEncodePlusTest, url_utf8) { std::string_view input = "https://jdim.test/い ろ/は?に=ほ&へ=と z"; std::string_view result = "https%3A%2F%2Fjdim.test%2F%E3%81%84+%E3%82%8D%2F" "%E3%81%AF%3F%E3%81%AB%3D%E3%81%BB%26%E3%81%B8%3D%E3%81%A8+z"; EXPECT_EQ( result, MISC::url_encode_plus( input ) ); } class MISC_UrlEncodePlusWithEncodingTest : public ::testing::Test {}; TEST_F(MISC_UrlEncodePlusWithEncodingTest, empty_string) { std::string input = ""; EXPECT_EQ( "", MISC::url_encode_plus( input, Encoding::sjis ) ); } TEST_F(MISC_UrlEncodePlusWithEncodingTest, unencoded_ascii_characters) { std::string input = "0123456789_ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz*-._"; EXPECT_EQ( input, MISC::url_encode_plus( input, Encoding::sjis ) ); } TEST_F(MISC_UrlEncodePlusWithEncodingTest, single_u0020) { std::string input = " "; EXPECT_EQ( "+", MISC::url_encode_plus( input, Encoding::sjis ) ); } TEST_F(MISC_UrlEncodePlusWithEncodingTest, u000A) { std::string input = "quick\nbrown\n\nfox"; EXPECT_EQ( "quick%0D%0Abrown%0D%0A%0D%0Afox", MISC::url_encode_plus( input, Encoding::sjis ) ); } TEST_F(MISC_UrlEncodePlusWithEncodingTest, u000D) { std::string input = "quick\rbrown\r\rfox"; EXPECT_EQ( "quickbrownfox", MISC::url_encode_plus( input, Encoding::sjis ) ); } TEST_F(MISC_UrlEncodePlusWithEncodingTest, u000D_u000A) { std::string input = "quick\r\nbrown\r\n\r\nfox"; EXPECT_EQ( "quick%0D%0Abrown%0D%0A%0D%0Afox", MISC::url_encode_plus( input, Encoding::sjis ) ); } TEST_F(MISC_UrlEncodePlusWithEncodingTest, words_separated_by_u0020) { // U+3000 won't be converted to '+' std::string input = "Quick Brown Fox い ろ は"; std::string_view result = "Quick+Brown%81%40Fox+%82%A2+%82%EB%81%40%82%CD"; EXPECT_EQ( result, MISC::url_encode_plus( input, Encoding::sjis ) ); } TEST_F(MISC_UrlEncodePlusWithEncodingTest, encoded_ascii_characters) { std::string input = "!\"#$%&\'()_+,_/_:;<=>?@_[\\]^_`_{|}~"; std::string_view result = "%21%22%23%24%25%26%27%28%29_%2B%2C_%2F_" "%3A%3B%3C%3D%3E%3F%40_%5B%5C%5D%5E_%60_%7B%7C%7D%7E"; EXPECT_EQ( result, MISC::url_encode_plus( input, Encoding::sjis ) ); } TEST_F(MISC_UrlEncodePlusWithEncodingTest, encoded_to_ms932) { std::string input = "\xE3\x81\x82"; // U+3042 std::string_view result = "%82%A0"; EXPECT_EQ( result, MISC::url_encode_plus( input, Encoding::sjis ) ); } TEST_F(MISC_UrlEncodePlusWithEncodingTest, encoded_to_eucjp) { std::string input = "\xE3\x81\x82"; // U+3042 std::string_view result = "%A4%A2"; EXPECT_EQ( result, MISC::url_encode_plus( input, Encoding::eucjp ) ); } TEST_F(MISC_UrlEncodePlusWithEncodingTest, url_to_ms932) { std::string input = "https://jdim.test/い ろ/は?に=ほ&へ=と z"; std::string_view result = "https%3A%2F%2Fjdim.test%2F%82%A2+%82%EB%2F" "%82%CD%3F%82%C9%3D%82%D9%26%82%D6%3D%82%C6+z"; EXPECT_EQ( result, MISC::url_encode_plus( input, Encoding::sjis ) ); } class MISC_DecodeSpcharNumberRawTest : public ::testing::Test {}; TEST_F(MISC_DecodeSpcharNumberRawTest, empty_string) { EXPECT_EQ( 0, MISC::decode_spchar_number_raw( "", 0, 0 ) ); } TEST_F(MISC_DecodeSpcharNumberRawTest, padding_zeros) { EXPECT_EQ( 0, MISC::decode_spchar_number_raw( "�", 2, 4 ) ); EXPECT_EQ( 0, MISC::decode_spchar_number_raw( "�", 3, 4 ) ); EXPECT_EQ( 0, MISC::decode_spchar_number_raw( "�", 3, 4 ) ); EXPECT_EQ( 0x000D, MISC::decode_spchar_number_raw( " ", 2, 4 ) ); EXPECT_EQ( 0x000D, MISC::decode_spchar_number_raw( " ", 3, 4 ) ); EXPECT_EQ( 0x000D, MISC::decode_spchar_number_raw( " ", 3, 4 ) ); } TEST_F(MISC_DecodeSpcharNumberRawTest, null_character) { EXPECT_EQ( 0, MISC::decode_spchar_number_raw( "�", 2, 1 ) ); EXPECT_EQ( 0, MISC::decode_spchar_number_raw( "�", 3, 1 ) ); EXPECT_EQ( 0, MISC::decode_spchar_number_raw( "�", 3, 1 ) ); } TEST_F(MISC_DecodeSpcharNumberRawTest, carriage_return) { EXPECT_EQ( 0x000D, MISC::decode_spchar_number_raw( " ", 2, 2 ) ); EXPECT_EQ( 0x000D, MISC::decode_spchar_number_raw( " ", 3, 1 ) ); EXPECT_EQ( 0x000D, MISC::decode_spchar_number_raw( " ", 3, 1 ) ); } TEST_F(MISC_DecodeSpcharNumberRawTest, ascii_whitespace) { EXPECT_EQ( 0x0009, MISC::decode_spchar_number_raw( " ", 2, 1 ) ); EXPECT_EQ( 0x0009, MISC::decode_spchar_number_raw( " ", 3, 1 ) ); EXPECT_EQ( 0x000A, MISC::decode_spchar_number_raw( " ", 2, 2 ) ); EXPECT_EQ( 0x000A, MISC::decode_spchar_number_raw( " ", 3, 1 ) ); EXPECT_EQ( 0x000C, MISC::decode_spchar_number_raw( " ", 2, 2 ) ); EXPECT_EQ( 0x000C, MISC::decode_spchar_number_raw( " ", 3, 1 ) ); EXPECT_EQ( 0x0020, MISC::decode_spchar_number_raw( " ", 2, 2 ) ); EXPECT_EQ( 0x0020, MISC::decode_spchar_number_raw( " ", 3, 2 ) ); } TEST_F(MISC_DecodeSpcharNumberRawTest, out_of_range) { EXPECT_EQ( 0x110000, MISC::decode_spchar_number_raw( "�", 2, 7 ) ); EXPECT_EQ( 0x110000, MISC::decode_spchar_number_raw( "�", 3, 6 ) ); EXPECT_EQ( 0x1000000, MISC::decode_spchar_number_raw( "�", 2, 8 ) ); EXPECT_EQ( 0x1000000, MISC::decode_spchar_number_raw( "�", 3, 7 ) ); } TEST_F(MISC_DecodeSpcharNumberRawTest, high_surrogate) { EXPECT_EQ( 0xD800, MISC::decode_spchar_number_raw( "�", 2, 5 ) ); EXPECT_EQ( 0xD800, MISC::decode_spchar_number_raw( "�", 3, 4 ) ); EXPECT_EQ( 0xDBFF, MISC::decode_spchar_number_raw( "�", 2, 5 ) ); EXPECT_EQ( 0xDBFF, MISC::decode_spchar_number_raw( "�", 3, 4 ) ); } TEST_F(MISC_DecodeSpcharNumberRawTest, low_surrogate) { EXPECT_EQ( 0xDC00, MISC::decode_spchar_number_raw( "�", 2, 5 ) ); EXPECT_EQ( 0xDC00, MISC::decode_spchar_number_raw( "�", 3, 4 ) ); EXPECT_EQ( 0xDFFF, MISC::decode_spchar_number_raw( "�", 2, 5 ) ); EXPECT_EQ( 0xDFFF, MISC::decode_spchar_number_raw( "�", 3, 4 ) ); } TEST_F(MISC_DecodeSpcharNumberRawTest, noncharacter) { EXPECT_EQ( 0xFDD0, MISC::decode_spchar_number_raw( "﷐", 2, 5 ) ); EXPECT_EQ( 0xFDD0, MISC::decode_spchar_number_raw( "﷐", 3, 4 ) ); EXPECT_EQ( 0xFDEF, MISC::decode_spchar_number_raw( "﷯", 2, 5 ) ); EXPECT_EQ( 0xFDEF, MISC::decode_spchar_number_raw( "﷯", 3, 4 ) ); EXPECT_EQ( 0xFFFE, MISC::decode_spchar_number_raw( "￾", 2, 5 ) ); EXPECT_EQ( 0xFFFE, MISC::decode_spchar_number_raw( "￾", 3, 4 ) ); EXPECT_EQ( 0xFFFF, MISC::decode_spchar_number_raw( "￿", 2, 5 ) ); EXPECT_EQ( 0xFFFF, MISC::decode_spchar_number_raw( "￿", 3, 4 ) ); EXPECT_EQ( 0x1FFFE, MISC::decode_spchar_number_raw( "🿾", 2, 6 ) ); EXPECT_EQ( 0x1FFFE, MISC::decode_spchar_number_raw( "🿾", 3, 5 ) ); EXPECT_EQ( 0x1FFFF, MISC::decode_spchar_number_raw( "🿿", 2, 6 ) ); EXPECT_EQ( 0x1FFFF, MISC::decode_spchar_number_raw( "🿿", 3, 5 ) ); EXPECT_EQ( 0x10FFFE, MISC::decode_spchar_number_raw( "􏿾", 2, 7 ) ); EXPECT_EQ( 0x10FFFE, MISC::decode_spchar_number_raw( "􏿾", 3, 6 ) ); EXPECT_EQ( 0x10FFFF, MISC::decode_spchar_number_raw( "􏿿", 2, 7 ) ); EXPECT_EQ( 0x10FFFF, MISC::decode_spchar_number_raw( "􏿿", 3, 6 ) ); } TEST_F(MISC_DecodeSpcharNumberRawTest, between_u007F_and_u009F_decimal) { EXPECT_EQ( 0x007F, MISC::decode_spchar_number_raw( "", 2, 3 ) ); EXPECT_EQ( 0x0080, MISC::decode_spchar_number_raw( "€", 2, 3 ) ); EXPECT_EQ( 0x0081, MISC::decode_spchar_number_raw( "", 2, 3 ) ); EXPECT_EQ( 0x0082, MISC::decode_spchar_number_raw( "‚", 2, 3 ) ); EXPECT_EQ( 0x0083, MISC::decode_spchar_number_raw( "ƒ", 2, 3 ) ); EXPECT_EQ( 0x0084, MISC::decode_spchar_number_raw( "„", 2, 3 ) ); EXPECT_EQ( 0x0085, MISC::decode_spchar_number_raw( "…", 2, 3 ) ); EXPECT_EQ( 0x0086, MISC::decode_spchar_number_raw( "†", 2, 3 ) ); EXPECT_EQ( 0x0087, MISC::decode_spchar_number_raw( "‡", 2, 3 ) ); EXPECT_EQ( 0x0088, MISC::decode_spchar_number_raw( "ˆ", 2, 3 ) ); EXPECT_EQ( 0x0089, MISC::decode_spchar_number_raw( "‰", 2, 3 ) ); EXPECT_EQ( 0x008A, MISC::decode_spchar_number_raw( "Š", 2, 3 ) ); EXPECT_EQ( 0x008B, MISC::decode_spchar_number_raw( "‹", 2, 3 ) ); EXPECT_EQ( 0x008C, MISC::decode_spchar_number_raw( "Œ", 2, 3 ) ); EXPECT_EQ( 0x008D, MISC::decode_spchar_number_raw( "", 2, 3 ) ); EXPECT_EQ( 0x008E, MISC::decode_spchar_number_raw( "Ž", 2, 3 ) ); EXPECT_EQ( 0x008F, MISC::decode_spchar_number_raw( "", 2, 3 ) ); EXPECT_EQ( 0x0090, MISC::decode_spchar_number_raw( "", 2, 3 ) ); EXPECT_EQ( 0x0091, MISC::decode_spchar_number_raw( "‘", 2, 3 ) ); EXPECT_EQ( 0x0092, MISC::decode_spchar_number_raw( "’", 2, 3 ) ); EXPECT_EQ( 0x0093, MISC::decode_spchar_number_raw( "“", 2, 3 ) ); EXPECT_EQ( 0x0094, MISC::decode_spchar_number_raw( "”", 2, 3 ) ); EXPECT_EQ( 0x0095, MISC::decode_spchar_number_raw( "•", 2, 3 ) ); EXPECT_EQ( 0x0096, MISC::decode_spchar_number_raw( "–", 2, 3 ) ); EXPECT_EQ( 0x0097, MISC::decode_spchar_number_raw( "—", 2, 3 ) ); EXPECT_EQ( 0x0098, MISC::decode_spchar_number_raw( "˜", 2, 3 ) ); EXPECT_EQ( 0x0099, MISC::decode_spchar_number_raw( "™", 2, 3 ) ); EXPECT_EQ( 0x009A, MISC::decode_spchar_number_raw( "š", 2, 3 ) ); EXPECT_EQ( 0x009B, MISC::decode_spchar_number_raw( "›", 2, 3 ) ); EXPECT_EQ( 0x009C, MISC::decode_spchar_number_raw( "œ", 2, 3 ) ); EXPECT_EQ( 0x009D, MISC::decode_spchar_number_raw( "", 2, 3 ) ); EXPECT_EQ( 0x009E, MISC::decode_spchar_number_raw( "ž", 2, 3 ) ); EXPECT_EQ( 0x009F, MISC::decode_spchar_number_raw( "Ÿ", 2, 3 ) ); } TEST_F(MISC_DecodeSpcharNumberRawTest, between_u007F_and_u009F_hexdecimal) { EXPECT_EQ( 0x007F, MISC::decode_spchar_number_raw( "", 3, 2 ) ); EXPECT_EQ( 0x0080, MISC::decode_spchar_number_raw( "€", 3, 2 ) ); EXPECT_EQ( 0x0081, MISC::decode_spchar_number_raw( "", 3, 2 ) ); EXPECT_EQ( 0x0082, MISC::decode_spchar_number_raw( "‚", 3, 2 ) ); EXPECT_EQ( 0x0083, MISC::decode_spchar_number_raw( "ƒ", 3, 2 ) ); EXPECT_EQ( 0x0084, MISC::decode_spchar_number_raw( "„", 3, 2 ) ); EXPECT_EQ( 0x0085, MISC::decode_spchar_number_raw( "…", 3, 2 ) ); EXPECT_EQ( 0x0086, MISC::decode_spchar_number_raw( "†", 3, 2 ) ); EXPECT_EQ( 0x0087, MISC::decode_spchar_number_raw( "‡", 3, 2 ) ); EXPECT_EQ( 0x0088, MISC::decode_spchar_number_raw( "ˆ", 3, 2 ) ); EXPECT_EQ( 0x0089, MISC::decode_spchar_number_raw( "‰", 3, 2 ) ); EXPECT_EQ( 0x008A, MISC::decode_spchar_number_raw( "Š", 3, 2 ) ); EXPECT_EQ( 0x008B, MISC::decode_spchar_number_raw( "‹", 3, 2 ) ); EXPECT_EQ( 0x008C, MISC::decode_spchar_number_raw( "Œ", 3, 2 ) ); EXPECT_EQ( 0x008D, MISC::decode_spchar_number_raw( "", 3, 2 ) ); EXPECT_EQ( 0x008E, MISC::decode_spchar_number_raw( "Ž", 3, 2 ) ); EXPECT_EQ( 0x008F, MISC::decode_spchar_number_raw( "", 3, 2 ) ); EXPECT_EQ( 0x0090, MISC::decode_spchar_number_raw( "", 3, 2 ) ); EXPECT_EQ( 0x0091, MISC::decode_spchar_number_raw( "‘", 3, 2 ) ); EXPECT_EQ( 0x0092, MISC::decode_spchar_number_raw( "’", 3, 2 ) ); EXPECT_EQ( 0x0093, MISC::decode_spchar_number_raw( "“", 3, 2 ) ); EXPECT_EQ( 0x0094, MISC::decode_spchar_number_raw( "”", 3, 2 ) ); EXPECT_EQ( 0x0095, MISC::decode_spchar_number_raw( "•", 3, 2 ) ); EXPECT_EQ( 0x0096, MISC::decode_spchar_number_raw( "–", 3, 2 ) ); EXPECT_EQ( 0x0097, MISC::decode_spchar_number_raw( "—", 3, 2 ) ); EXPECT_EQ( 0x0098, MISC::decode_spchar_number_raw( "˜", 3, 2 ) ); EXPECT_EQ( 0x0099, MISC::decode_spchar_number_raw( "™", 3, 2 ) ); EXPECT_EQ( 0x009A, MISC::decode_spchar_number_raw( "š", 3, 2 ) ); EXPECT_EQ( 0x009B, MISC::decode_spchar_number_raw( "›", 3, 2 ) ); EXPECT_EQ( 0x009C, MISC::decode_spchar_number_raw( "œ", 3, 2 ) ); EXPECT_EQ( 0x009D, MISC::decode_spchar_number_raw( "", 3, 2 ) ); EXPECT_EQ( 0x009E, MISC::decode_spchar_number_raw( "ž", 3, 2 ) ); EXPECT_EQ( 0x009F, MISC::decode_spchar_number_raw( "Ÿ", 3, 2 ) ); } class MISC_DecodeSpcharNumberTest : public ::testing::Test {}; TEST_F(MISC_DecodeSpcharNumberTest, result_ok) { EXPECT_EQ( 0xC, MISC::decode_spchar_number( " ", 3, 1 ) ); EXPECT_EQ( 32, MISC::decode_spchar_number( " ", 2, 2 ) ); EXPECT_EQ( 0x20, MISC::decode_spchar_number( " ", 3, 2 ) ); EXPECT_EQ( 0xA0, MISC::decode_spchar_number( " ", 3, 2 ) ); EXPECT_EQ( 1234, MISC::decode_spchar_number( "Ӓ", 2, 4 ) ); EXPECT_EQ( 0x1234, MISC::decode_spchar_number( "ሴ", 3, 4 ) ); EXPECT_EQ( 0xD7FF, MISC::decode_spchar_number( "퟿", 3, 4 ) ); EXPECT_EQ( 0xE000, MISC::decode_spchar_number( "", 3, 4 ) ); EXPECT_EQ( 0xFDCF, MISC::decode_spchar_number( "﷏", 3, 4 ) ); EXPECT_EQ( 0xFDF0, MISC::decode_spchar_number( "ﷰ", 3, 4 ) ); EXPECT_EQ( 1114109, MISC::decode_spchar_number( "􏿽", 2, 7 ) ); EXPECT_EQ( 0x10FFFD, MISC::decode_spchar_number( "􏿽", 3, 6 ) ); EXPECT_EQ( 0x0abcde, MISC::decode_spchar_number( "򫳞", 3, 5 ) ); } TEST_F(MISC_DecodeSpcharNumberTest, result_error) { EXPECT_EQ( 0xFFFD, MISC::decode_spchar_number( "�", 2, 1 ) ); EXPECT_EQ( 0xFFFD, MISC::decode_spchar_number( "", 2, 1 ) ); EXPECT_EQ( 0xFFFD, MISC::decode_spchar_number( " ", 3, 1 ) ); EXPECT_EQ( 0xFFFD, MISC::decode_spchar_number( " ", 3, 1 ) ); EXPECT_EQ( 0xFFFD, MISC::decode_spchar_number( "", 2, 2 ) ); EXPECT_EQ( 0xFFFD, MISC::decode_spchar_number( "�", 3, 4 ) ); EXPECT_EQ( 0xFFFD, MISC::decode_spchar_number( "�", 3, 4 ) ); EXPECT_EQ( 0xFFFD, MISC::decode_spchar_number( "﷐", 3, 4 ) ); EXPECT_EQ( 0xFFFD, MISC::decode_spchar_number( "﷯", 3, 4 ) ); EXPECT_EQ( 0xFFFD, MISC::decode_spchar_number( "�", 3, 6 ) ); EXPECT_EQ( 0xFFFD, MISC::decode_spchar_number( "�", 2, 7 ) ); } TEST_F(MISC_DecodeSpcharNumberTest, result_transform) { EXPECT_EQ( 0xFFFD, MISC::decode_spchar_number( "", 3, 2 ) ); EXPECT_EQ( 0x20AC, MISC::decode_spchar_number( "€", 3, 2 ) ); EXPECT_EQ( 0xFFFD, MISC::decode_spchar_number( "", 3, 2 ) ); EXPECT_EQ( 0x201A, MISC::decode_spchar_number( "‚", 3, 2 ) ); EXPECT_EQ( 0x0192, MISC::decode_spchar_number( "ƒ", 3, 2 ) ); EXPECT_EQ( 0x201E, MISC::decode_spchar_number( "„", 3, 2 ) ); EXPECT_EQ( 0x2026, MISC::decode_spchar_number( "…", 3, 2 ) ); EXPECT_EQ( 0x2020, MISC::decode_spchar_number( "†", 3, 2 ) ); EXPECT_EQ( 0x2021, MISC::decode_spchar_number( "‡", 3, 2 ) ); EXPECT_EQ( 0x02C6, MISC::decode_spchar_number( "ˆ", 3, 2 ) ); EXPECT_EQ( 0x2030, MISC::decode_spchar_number( "‰", 3, 2 ) ); EXPECT_EQ( 0x0160, MISC::decode_spchar_number( "Š", 3, 2 ) ); EXPECT_EQ( 0x2039, MISC::decode_spchar_number( "‹", 3, 2 ) ); EXPECT_EQ( 0x0152, MISC::decode_spchar_number( "Œ", 3, 2 ) ); EXPECT_EQ( 0xFFFD, MISC::decode_spchar_number( "", 3, 2 ) ); EXPECT_EQ( 0x017D, MISC::decode_spchar_number( "Ž", 3, 2 ) ); EXPECT_EQ( 0xFFFD, MISC::decode_spchar_number( "", 3, 2 ) ); EXPECT_EQ( 0xFFFD, MISC::decode_spchar_number( "", 3, 2 ) ); EXPECT_EQ( 0x2018, MISC::decode_spchar_number( "‘", 3, 2 ) ); EXPECT_EQ( 0x2019, MISC::decode_spchar_number( "’", 3, 2 ) ); EXPECT_EQ( 0x201C, MISC::decode_spchar_number( "“", 3, 2 ) ); EXPECT_EQ( 0x201D, MISC::decode_spchar_number( "”", 3, 2 ) ); EXPECT_EQ( 0x2022, MISC::decode_spchar_number( "•", 3, 2 ) ); EXPECT_EQ( 0x2013, MISC::decode_spchar_number( "–", 3, 2 ) ); EXPECT_EQ( 0x2014, MISC::decode_spchar_number( "—", 3, 2 ) ); EXPECT_EQ( 0x02DC, MISC::decode_spchar_number( "˜", 3, 2 ) ); EXPECT_EQ( 0x2122, MISC::decode_spchar_number( "™", 3, 2 ) ); EXPECT_EQ( 0x0161, MISC::decode_spchar_number( "š", 3, 2 ) ); EXPECT_EQ( 0x203A, MISC::decode_spchar_number( "›", 3, 2 ) ); EXPECT_EQ( 0x0153, MISC::decode_spchar_number( "œ", 3, 2 ) ); EXPECT_EQ( 0xFFFD, MISC::decode_spchar_number( "", 3, 2 ) ); EXPECT_EQ( 0x017E, MISC::decode_spchar_number( "ž", 3, 2 ) ); EXPECT_EQ( 0x0178, MISC::decode_spchar_number( "Ÿ", 3, 2 ) ); } class MISC_SanitizeNumericCharrefTest : public ::testing::Test {}; TEST_F(MISC_SanitizeNumericCharrefTest, null_character) { char32_t out = 0; EXPECT_EQ( 0xFFFD, MISC::sanitize_numeric_charref( 0, &out ) ); EXPECT_EQ( 0, out ); } TEST_F(MISC_SanitizeNumericCharrefTest, carriage_return) { char32_t out = 0; EXPECT_EQ( 0xFFFD, MISC::sanitize_numeric_charref( 0x000D, &out ) ); EXPECT_EQ( 0, out ); } TEST_F(MISC_SanitizeNumericCharrefTest, ascii_whitespace) { char32_t out = 0; EXPECT_EQ( 0x0009, MISC::sanitize_numeric_charref( 0x0009, &out ) ); EXPECT_EQ( 0, out ); out = 0; EXPECT_EQ( 0x000A, MISC::sanitize_numeric_charref( 0x000A, &out ) ); EXPECT_EQ( 0, out ); out = 0; EXPECT_EQ( 0x000C, MISC::sanitize_numeric_charref( 0x000C, &out ) ); EXPECT_EQ( 0, out ); out = 0; EXPECT_EQ( 0x0020, MISC::sanitize_numeric_charref( 0x0020, &out ) ); EXPECT_EQ( 0, out ); } TEST_F(MISC_SanitizeNumericCharrefTest, out_of_range) { char32_t out = 0; EXPECT_EQ( 0xFFFD, MISC::sanitize_numeric_charref( 0x110000, &out ) ); EXPECT_EQ( 0, out ); EXPECT_EQ( 0xFFFD, MISC::sanitize_numeric_charref( 0x1000000, nullptr ) ); } TEST_F(MISC_SanitizeNumericCharrefTest, high_surrogate) { // 数が多いため網羅していない char32_t out = 0; EXPECT_EQ( 0xFFFD, MISC::sanitize_numeric_charref( 0xD800, &out ) ); EXPECT_EQ( 0xD800, out ); out = 0; EXPECT_EQ( 0xFFFD, MISC::sanitize_numeric_charref( 0xDBFF, &out ) ); EXPECT_EQ( 0xDBFF, out ); } TEST_F(MISC_SanitizeNumericCharrefTest, low_surrogate) { // 数が多いため網羅していない char32_t out = 0; EXPECT_EQ( 0xFFFD, MISC::sanitize_numeric_charref( 0xDC00, &out ) ); EXPECT_EQ( 0, out ); out = 0; EXPECT_EQ( 0xFFFD, MISC::sanitize_numeric_charref( 0xDFFF, &out ) ); EXPECT_EQ( 0, out ); } TEST_F(MISC_SanitizeNumericCharrefTest, noncharacter) { // 数が多いため網羅していない constexpr char32_t nonchars[] = { 0xFDD0, 0xFDEF, 0xFFFE, 0xFFFF, 0x1FFFE, 0x1FFFF, 0x10FFFE, 0x10FFFF }; for( const char32_t uch : nonchars ) { char32_t out = 0; EXPECT_EQ( 0xFFFD, MISC::sanitize_numeric_charref( uch, &out ) ); EXPECT_EQ( 0, out ); } } TEST_F(MISC_SanitizeNumericCharrefTest, between_u007F_and_u009F) { EXPECT_EQ( 0xFFFD, MISC::sanitize_numeric_charref( 0x007F, nullptr ) ); EXPECT_EQ( 0x20AC, MISC::sanitize_numeric_charref( 0x0080, nullptr ) ); EXPECT_EQ( 0xFFFD, MISC::sanitize_numeric_charref( 0x0081, nullptr ) ); EXPECT_EQ( 0x201A, MISC::sanitize_numeric_charref( 0x0082, nullptr ) ); EXPECT_EQ( 0x0192, MISC::sanitize_numeric_charref( 0x0083, nullptr ) ); EXPECT_EQ( 0x201E, MISC::sanitize_numeric_charref( 0x0084, nullptr ) ); EXPECT_EQ( 0x2026, MISC::sanitize_numeric_charref( 0x0085, nullptr ) ); EXPECT_EQ( 0x2020, MISC::sanitize_numeric_charref( 0x0086, nullptr ) ); EXPECT_EQ( 0x2021, MISC::sanitize_numeric_charref( 0x0087, nullptr ) ); EXPECT_EQ( 0x02C6, MISC::sanitize_numeric_charref( 0x0088, nullptr ) ); EXPECT_EQ( 0x2030, MISC::sanitize_numeric_charref( 0x0089, nullptr ) ); EXPECT_EQ( 0x0160, MISC::sanitize_numeric_charref( 0x008A, nullptr ) ); EXPECT_EQ( 0x2039, MISC::sanitize_numeric_charref( 0x008B, nullptr ) ); EXPECT_EQ( 0x0152, MISC::sanitize_numeric_charref( 0x008C, nullptr ) ); EXPECT_EQ( 0x017D, MISC::sanitize_numeric_charref( 0x008E, nullptr ) ); EXPECT_EQ( 0xFFFD, MISC::sanitize_numeric_charref( 0x008F, nullptr ) ); EXPECT_EQ( 0xFFFD, MISC::sanitize_numeric_charref( 0x0090, nullptr ) ); EXPECT_EQ( 0x2018, MISC::sanitize_numeric_charref( 0x0091, nullptr ) ); EXPECT_EQ( 0x2019, MISC::sanitize_numeric_charref( 0x0092, nullptr ) ); EXPECT_EQ( 0x201C, MISC::sanitize_numeric_charref( 0x0093, nullptr ) ); EXPECT_EQ( 0x201D, MISC::sanitize_numeric_charref( 0x0094, nullptr ) ); EXPECT_EQ( 0x2022, MISC::sanitize_numeric_charref( 0x0095, nullptr ) ); EXPECT_EQ( 0x2013, MISC::sanitize_numeric_charref( 0x0096, nullptr ) ); EXPECT_EQ( 0x2014, MISC::sanitize_numeric_charref( 0x0097, nullptr ) ); EXPECT_EQ( 0x02DC, MISC::sanitize_numeric_charref( 0x0098, nullptr ) ); EXPECT_EQ( 0x2122, MISC::sanitize_numeric_charref( 0x0099, nullptr ) ); EXPECT_EQ( 0x0161, MISC::sanitize_numeric_charref( 0x009A, nullptr ) ); EXPECT_EQ( 0x203A, MISC::sanitize_numeric_charref( 0x009B, nullptr ) ); EXPECT_EQ( 0x0153, MISC::sanitize_numeric_charref( 0x009C, nullptr ) ); EXPECT_EQ( 0xFFFD, MISC::sanitize_numeric_charref( 0x009D, nullptr ) ); EXPECT_EQ( 0x017E, MISC::sanitize_numeric_charref( 0x009E, nullptr ) ); EXPECT_EQ( 0x0178, MISC::sanitize_numeric_charref( 0x009F, nullptr ) ); } class AsciiIgnoreCaseFindTest : public ::testing::Test {}; TEST_F(AsciiIgnoreCaseFindTest, empty) { EXPECT_EQ( 0, MISC::ascii_ignore_case_find( std::string{}, std::string{} ) ); EXPECT_EQ( std::string::npos, MISC::ascii_ignore_case_find( std::string{}, std::string{}, 10 ) ); } TEST_F(AsciiIgnoreCaseFindTest, not_match) { const std::string haystack = "spam ham eggs"; EXPECT_EQ( std::string::npos, MISC::ascii_ignore_case_find( haystack, "foo bar" ) ); } TEST_F(AsciiIgnoreCaseFindTest, out_of_bounds) { const std::string haystack = "out of bounds"; EXPECT_EQ( std::string::npos, MISC::ascii_ignore_case_find( haystack, "needle", haystack.size() + 1 ) ); } TEST_F(AsciiIgnoreCaseFindTest, match_same_case) { const std::string haystack = "helloworld"; EXPECT_EQ( 0, MISC::ascii_ignore_case_find( haystack, "hello" ) ); EXPECT_EQ( 5, MISC::ascii_ignore_case_find( haystack, "world" ) ); } TEST_F(AsciiIgnoreCaseFindTest, match_lowercase) { const std::string haystack_lower = "helloworld"; EXPECT_EQ( 0, MISC::ascii_ignore_case_find( haystack_lower, "HELLO" ) ); EXPECT_EQ( 5, MISC::ascii_ignore_case_find( haystack_lower, "WORLD" ) ); } TEST_F(AsciiIgnoreCaseFindTest, match_uppercase) { const std::string haystack_upper = "HELLOWORLD"; EXPECT_EQ( 0, MISC::ascii_ignore_case_find( haystack_upper, "hello" ) ); EXPECT_EQ( 5, MISC::ascii_ignore_case_find( haystack_upper, "world" ) ); } TEST_F(AsciiIgnoreCaseFindTest, match_mix_case) { const std::string haystack_mix = "HeLlOwOrLd"; EXPECT_EQ( 0, MISC::ascii_ignore_case_find( haystack_mix, "hElLo" ) ); EXPECT_EQ( 5, MISC::ascii_ignore_case_find( haystack_mix, "WoRlD" ) ); } TEST_F(AsciiIgnoreCaseFindTest, match_lead_word) { const std::string haystack = "foo bar baz bar"; EXPECT_EQ( 4, MISC::ascii_ignore_case_find( haystack, "bar" ) ); EXPECT_EQ( 12, MISC::ascii_ignore_case_find( haystack, "BAR", 5 ) ); } } // namespace jdim-0.10.1/test/gtest_jdlib_span.cpp000066400000000000000000000317641445721505100175310ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-2.0-only #include "gtest/gtest.h" #include "jdlib/span.h" #include #include #include #include namespace { class SpanTest : public ::testing::Test { protected: // test fixtures int bounded_array5[5] = { 1, 2, 3, 4, 5 }; std::array std_array = { "abc", "def", "ghi", "jkl" }; std::string std_string = "helloworld"; std::string_view std_string_view = "lorem ipsum"; std::vector std_vector = { "quick", "brown", "fox", "jumps", "over", "the", "lazy", "dog" }; }; TEST_F(SpanTest, construct_const) { JDLIB::span arr_span{ bounded_array5 }; EXPECT_EQ( 1, *arr_span.begin() ); JDLIB::span stdarr_span{ std_array }; EXPECT_EQ( std_array[0], stdarr_span[0] ); const std::array const_std_array = { "qrs", "tuv", "wxyz" }; JDLIB::span const_stdarr_span{ const_std_array }; EXPECT_EQ( const_std_array.back(), const_stdarr_span.back() ); JDLIB::span const_std_string_span{ std_string }; EXPECT_EQ( std_string.data(), const_std_string_span.data() ); JDLIB::span const_std_string_view_span{ std_string_view }; EXPECT_EQ( std_string_view.data(), const_std_string_view_span.data() ); JDLIB::span vec_span{ std_vector }; EXPECT_EQ( "quick", vec_span.front() ); static std::size_t static_arr[] = { 123, 234, 345, 456, 567, 678, 789 }; constexpr JDLIB::span static_arr_span{ static_arr }; EXPECT_EQ( 7, static_arr_span.size() ); static const std::array static_stdarr = { 111, 222, 333 }; constexpr JDLIB::span static_stdarr_span{ static_stdarr }; EXPECT_EQ( 3, static_stdarr_span.size() ); } TEST_F(SpanTest, operator_equal) { JDLIB::span arr_span{ bounded_array5 }; JDLIB::span mutable_copy = arr_span; EXPECT_EQ( 1, *mutable_copy.begin() ); JDLIB::span stdarr_span{ std_array }; JDLIB::span const_copy = stdarr_span; EXPECT_EQ( std_array[0], const_copy[0] ); static std::size_t static_arr[] = { 123, 234, 345, 456, 567, 678, 789 }; constexpr JDLIB::span static_arr_span{ static_arr }; constexpr JDLIB::span constexpr_copy = static_arr_span; EXPECT_EQ( 123, constexpr_copy.front() ); } TEST_F(SpanTest, begin) { JDLIB::span arr_span{ bounded_array5 }; EXPECT_EQ( 1, *arr_span.begin() ); JDLIB::span arr_subspan{ bounded_array5 + 1, 3 }; EXPECT_EQ( 2, *arr_subspan.begin() ); JDLIB::span stdarr_span{ std_array }; EXPECT_EQ( *std_array.begin(), *stdarr_span.begin() ); JDLIB::span vec_span{ std_vector }; *vec_span.begin() = "moge"; EXPECT_EQ( *std_vector.begin(), "moge" ); static constexpr std::size_t static_arr[] = { 123, 234, 345, 456, 567, 678, 789 }; constexpr JDLIB::span static_arr_span{ static_arr }; constexpr std::size_t value = *static_arr_span.begin(); EXPECT_EQ( 123, value ); } TEST_F(SpanTest, end) { JDLIB::span arr_span{ bounded_array5 }; EXPECT_EQ( bounded_array5 + 5, arr_span.end() ); JDLIB::span arr_subspan{ bounded_array5 + 1, 3 }; EXPECT_EQ( bounded_array5 + 4, arr_subspan.end() ); JDLIB::span stdarr_span{ std_array }; EXPECT_EQ( std_array.data() + std_array.size(), stdarr_span.end() ); JDLIB::span vec_span{ std_vector }; EXPECT_EQ( std_vector.data() + std_vector.size(), vec_span.end() ); static constexpr std::size_t static_arr[] = { 123, 234, 345, 456, 567, 678, 789 }; constexpr JDLIB::span static_arr_span{ static_arr }; constexpr auto end = static_arr_span.end(); EXPECT_EQ( static_arr + 7, end ); } TEST_F(SpanTest, rbegin) { JDLIB::span arr_span{ bounded_array5 }; EXPECT_EQ( 5, *arr_span.rbegin() ); JDLIB::span arr_subspan{ bounded_array5 + 1, 3 }; EXPECT_EQ( 4, *arr_subspan.rbegin() ); JDLIB::span stdarr_span{ std_array }; EXPECT_EQ( *std_array.rbegin(), *stdarr_span.rbegin() ); JDLIB::span vec_span{ std_vector }; *vec_span.rbegin() = "cat"; EXPECT_EQ( *std_vector.rbegin(), "cat" ); static constexpr std::size_t static_arr[] = { 123, 234, 345, 456, 567, 678, 789 }; constexpr JDLIB::span static_arr_span{ static_arr }; constexpr auto value = *static_arr_span.rbegin(); EXPECT_EQ( 789, value ); } TEST_F(SpanTest, rend) { JDLIB::span arr_span{ bounded_array5 }; EXPECT_EQ( bounded_array5, arr_span.rend().base() ); JDLIB::span arr_subspan{ bounded_array5 + 1, 3 }; EXPECT_EQ( bounded_array5 + 1, arr_subspan.rend().base() ); JDLIB::span stdarr_span{ std_array }; EXPECT_EQ( std_array.data(), stdarr_span.rend().base() ); JDLIB::span vec_span{ std_vector }; EXPECT_EQ( std_vector.data(), vec_span.rend().base() ); static constexpr std::size_t static_arr[] = { 123, 234, 345, 456, 567, 678, 789 }; constexpr JDLIB::span static_arr_span{ static_arr }; constexpr auto it = static_arr_span.rend().base(); EXPECT_EQ( static_arr, it ); } TEST_F(SpanTest, front) { JDLIB::span arr_span{ bounded_array5 }; EXPECT_EQ( bounded_array5[0], arr_span.front() ); JDLIB::span arr_subspan{ bounded_array5 + 1, 3 }; EXPECT_EQ( bounded_array5[1], arr_subspan.front() ); JDLIB::span stdarr_span{ std_array }; EXPECT_EQ( std_array[0], stdarr_span.front() ); JDLIB::span const_std_string_span{ std_string }; EXPECT_EQ( std_string.front(), const_std_string_span.front() ); JDLIB::span const_std_string_view_span{ std_string_view }; EXPECT_EQ( std_string_view.front(), const_std_string_view_span.front() ); JDLIB::span vec_span{ std_vector }; vec_span.front() = "foobar"; EXPECT_EQ( std_vector[0], "foobar" ); static constexpr std::size_t static_arr[] = { 123, 234, 345, 456, 567, 678, 789 }; constexpr JDLIB::span static_arr_span{ static_arr }; constexpr auto value = static_arr_span.front(); EXPECT_EQ( static_arr[0], value ); } TEST_F(SpanTest, back) { JDLIB::span arr_span{ bounded_array5 }; EXPECT_EQ( bounded_array5[4], arr_span.back() ); JDLIB::span arr_subspan{ bounded_array5 + 1, 3 }; EXPECT_EQ( bounded_array5[3], arr_subspan.back() ); JDLIB::span stdarr_span{ std_array }; EXPECT_EQ( std_array[3], stdarr_span.back() ); JDLIB::span const_std_string_span{ std_string }; EXPECT_EQ( std_string.back(), const_std_string_span.back() ); JDLIB::span const_std_string_view_span{ std_string_view }; EXPECT_EQ( std_string_view.back(), const_std_string_view_span.back() ); JDLIB::span vec_span{ std_vector }; vec_span.back() = "bazqux"; EXPECT_EQ( std_vector[7], "bazqux" ); static constexpr std::size_t static_arr[] = { 123, 234, 345, 456, 567, 678, 789 }; constexpr JDLIB::span static_arr_span{ static_arr }; constexpr auto value = static_arr_span.back(); EXPECT_EQ( static_arr[6], value ); } TEST_F(SpanTest, operator_brackets) { JDLIB::span arr_span{ bounded_array5 }; EXPECT_EQ( bounded_array5[1], arr_span[1] ); JDLIB::span arr_subspan{ bounded_array5 + 1, 3 }; EXPECT_EQ( bounded_array5[2], arr_subspan[1] ); JDLIB::span stdarr_span{ std_array }; EXPECT_EQ( std_array[1], stdarr_span[1] ); JDLIB::span const_std_string_span{ std_string }; EXPECT_EQ( std_string[1], const_std_string_span[1] ); JDLIB::span const_std_string_view_span{ std_string_view }; EXPECT_EQ( std_string_view[1], const_std_string_view_span[1] ); JDLIB::span vec_span{ std_vector }; vec_span[1] = "hogefuga"; EXPECT_EQ( std_vector[1], "hogefuga" ); static constexpr std::size_t static_arr[] = { 123, 234, 345, 456, 567, 678, 789 }; constexpr JDLIB::span static_arr_span{ static_arr }; constexpr auto value = static_arr_span[1]; EXPECT_EQ( static_arr[1], value ); } TEST_F(SpanTest, data) { JDLIB::span empty_span; EXPECT_EQ( nullptr, empty_span.data() ); JDLIB::span arr_span{ bounded_array5 }; EXPECT_EQ( bounded_array5, arr_span.data() ); JDLIB::span arr_subspan{ bounded_array5 + 1, 3 }; EXPECT_EQ( bounded_array5 + 1, arr_subspan.data() ); JDLIB::span stdarr_span{ std_array }; EXPECT_EQ( std_array.data(), stdarr_span.data() ); JDLIB::span vec_span{ std_vector }; EXPECT_EQ( std_vector.data(), vec_span.data() ); static std::size_t static_arr[] = { 123, 234, 345, 456, 567, 678, 789 }; constexpr JDLIB::span static_arr_span{ static_arr }; constexpr auto data = static_arr_span.data(); EXPECT_EQ( static_arr, data ); } TEST_F(SpanTest, size) { JDLIB::span empty_span; EXPECT_EQ( 0, empty_span.size() ); JDLIB::span arr_span{ bounded_array5 }; EXPECT_EQ( 5, arr_span.size() ); JDLIB::span arr_subspan{ bounded_array5 + 1, 3 }; EXPECT_EQ( 3, arr_subspan.size() ); JDLIB::span stdarr_span{ std_array }; EXPECT_EQ( 4, stdarr_span.size() ); JDLIB::span const_std_string_span{ std_string }; EXPECT_EQ( std_string.size(), const_std_string_span.size() ); JDLIB::span const_std_string_view_span{ std_string_view }; EXPECT_EQ( std_string_view.size(), const_std_string_view_span.size() ); JDLIB::span vec_span{ std_vector }; EXPECT_EQ( 8, vec_span.size() ); static std::size_t static_arr[] = { 123, 234, 345, 456, 567, 678, 789 }; constexpr JDLIB::span static_arr_span{ static_arr }; constexpr auto arr_size = static_arr_span.size(); EXPECT_EQ( 7, arr_size ); static std::array static_stdarr = { 111, 222, 333 }; constexpr JDLIB::span static_stdarr_span{ static_stdarr }; constexpr auto stdarr_size = static_stdarr_span.size(); EXPECT_EQ( 3, stdarr_size ); } TEST_F(SpanTest, empty) { JDLIB::span empty_span; EXPECT_TRUE( empty_span.empty() ); JDLIB::span arr_span{ bounded_array5 }; EXPECT_FALSE( arr_span.empty() ); JDLIB::span arr_subspan{ bounded_array5 + 1, 3 }; EXPECT_FALSE( arr_subspan.empty() ); JDLIB::span stdarr_span{ std_array }; EXPECT_FALSE( stdarr_span.empty() ); JDLIB::span vec_span{ std_vector }; EXPECT_FALSE( vec_span.empty() ); static std::size_t static_arr[] = { 123, 234, 345, 456, 567, 678, 789 }; constexpr JDLIB::span static_arr_span{ static_arr }; constexpr auto empty = static_arr_span.empty(); EXPECT_FALSE( empty ); } TEST_F(SpanTest, first) { JDLIB::span arr_span{ bounded_array5 }; EXPECT_EQ( 1, *arr_span.first(3).begin() ); JDLIB::span arr_subspan{ bounded_array5 + 1, 3 }; EXPECT_EQ( bounded_array5 + 3, arr_subspan.first(2).end() ); JDLIB::span stdarr_span{ std_array }; EXPECT_EQ( 4, stdarr_span.first(4).size() ); JDLIB::span vec_span{ std_vector }; EXPECT_EQ( "jumps", vec_span.first(6)[3] ); static std::size_t static_arr[] = { 123, 234, 345, 456, 567, 678, 789 }; constexpr JDLIB::span static_arr_span{ static_arr }; constexpr auto constexpr_first = static_arr_span.first(5); EXPECT_EQ( static_arr + 5, constexpr_first.end() ); } TEST_F(SpanTest, last) { JDLIB::span arr_span{ bounded_array5 }; EXPECT_EQ( 3, *arr_span.last(3).begin() ); JDLIB::span arr_subspan{ bounded_array5 + 1, 3 }; EXPECT_EQ( bounded_array5 + 4, arr_subspan.last(1).end() ); JDLIB::span stdarr_span{ std_array }; EXPECT_EQ( 4, stdarr_span.last(4).size() ); JDLIB::span vec_span{ std_vector }; EXPECT_EQ( "the", vec_span.last(6)[3] ); static std::size_t static_arr[] = { 123, 234, 345, 456, 567, 678, 789 }; constexpr JDLIB::span static_arr_span{ static_arr }; constexpr auto constexpr_last = static_arr_span.last(5); EXPECT_EQ( static_arr + 2, constexpr_last.begin() ); } TEST_F(SpanTest, subspan) { JDLIB::span arr_span{ bounded_array5 }; EXPECT_EQ( 2, *arr_span.subspan(1, 4).begin() ); JDLIB::span arr_subspan{ bounded_array5 + 1, 3 }; EXPECT_EQ( bounded_array5 + 4, arr_subspan.subspan(2, 1).end() ); JDLIB::span stdarr_span{ std_array }; EXPECT_EQ( 2, stdarr_span.subspan(2, 2).size() ); JDLIB::span vec_span{ std_vector }; EXPECT_EQ( "lazy", vec_span.subspan(3, 4)[3] ); static std::size_t static_arr[] = { 123, 234, 345, 456, 567, 678, 789 }; constexpr JDLIB::span static_arr_span{ static_arr }; constexpr auto constexpr_sub = static_arr_span.subspan(1, 5); EXPECT_EQ( static_arr + 1, constexpr_sub.data() ); } } // namespace jdim-0.10.1/test/gtest_xml_dom.cpp000066400000000000000000000260651445721505100170610ustar00rootroot00000000000000// License: GPL-2.0 #include "xml/dom.h" #include "gtest/gtest.h" namespace { class XML_DomGetXml : public ::testing::Test {}; TEST_F(XML_DomGetXml, xml_empty) { XML::Dom dom{ XML::NODE_TYPE_UNKNOWN, "test" }; std::string xml = dom.get_xml(); EXPECT_EQ( "", xml ); } TEST_F(XML_DomGetXml, xml_empty_document) { XML::Dom dom{ XML::NODE_TYPE_DOCUMENT, "document" }; std::string xml = dom.get_xml(); EXPECT_EQ( "", xml ); } TEST_F(XML_DomGetXml, xml_single_element) { XML::Dom dom{ XML::NODE_TYPE_DOCUMENT, "document" }; XML::Dom* root = dom.appendChild( XML::NODE_TYPE_ELEMENT, "root" ); root->setAttribute( "name", "value" ); std::string xml = dom.get_xml(); EXPECT_EQ( "\n\n", xml ); } TEST_F(XML_DomGetXml, xml_single_child_element) { XML::Dom dom{ XML::NODE_TYPE_DOCUMENT, "document" }; XML::Dom* root = dom.appendChild( XML::NODE_TYPE_ELEMENT, "root" ); root->appendChild( XML::NODE_TYPE_ELEMENT, "child" ); std::string xml = dom.get_xml(); EXPECT_EQ( "\n\n \n\n", xml ); } TEST_F(XML_DomGetXml, xml_single_child_text) { XML::Dom dom{ XML::NODE_TYPE_DOCUMENT, "document" }; XML::Dom* root = dom.appendChild( XML::NODE_TYPE_ELEMENT, "root" ); XML::Dom* child = root->appendChild( XML::NODE_TYPE_TEXT, "child" ); child->nodeValue( "hello world" ); std::string xml = dom.get_xml(); EXPECT_EQ( "\n\n hello world\n\n", xml ); } TEST_F(XML_DomGetXml, xml_nesting_child_element) { XML::Dom dom{ XML::NODE_TYPE_DOCUMENT, "document" }; XML::Dom* root = dom.appendChild( XML::NODE_TYPE_ELEMENT, "root" ); XML::Dom* child = root->appendChild( XML::NODE_TYPE_ELEMENT, "first" ); child->appendChild( XML::NODE_TYPE_ELEMENT, "second" ); std::string xml = dom.get_xml(); std::string expect = R"=( )="; EXPECT_EQ( expect, xml ); } class XML_DomProperty : public ::testing::Test {}; TEST_F(XML_DomProperty, node_type) { const auto types = { XML::NODE_TYPE_UNKNOWN, XML::NODE_TYPE_ELEMENT, XML::NODE_TYPE_TEXT, XML::NODE_TYPE_DOCUMENT, }; for( const auto t : types ) { XML::Dom dom{ t, "test" }; int result = dom.nodeType(); EXPECT_EQ( t, result ); } } TEST_F(XML_DomProperty, node_name) { XML::Dom dom{ XML::NODE_TYPE_UNKNOWN, "test" }; std::string name = dom.nodeName(); EXPECT_EQ( "test", name ); } TEST_F(XML_DomProperty, node_value) { XML::Dom dom{ XML::NODE_TYPE_UNKNOWN, "test" }; std::string value = dom.nodeValue(); EXPECT_EQ( "", value ); } class XML_DomGetElement : public ::testing::Test {}; TEST_F(XML_DomGetElement, empty_id) { XML::Dom dom{ XML::NODE_TYPE_UNKNOWN, "test" }; XML::Dom* result = dom.getElementById( "" ); EXPECT_EQ( nullptr, result ); } TEST_F(XML_DomGetElement, not_found_id) { XML::Dom dom{ XML::NODE_TYPE_UNKNOWN, "test" }; XML::Dom* child = dom.appendChild( XML::NODE_TYPE_UNKNOWN, "unknown" ); child->setAttribute( "id", "the-id" ); child = dom.appendChild( XML::NODE_TYPE_TEXT, "text" ); child->setAttribute( "id", "the-id" ); child = dom.appendChild( XML::NODE_TYPE_DOCUMENT, "document" ); child->setAttribute( "id", "the-id" ); XML::Dom* result = dom.getElementById( "the-id" ); EXPECT_EQ( nullptr, result ); } TEST_F(XML_DomGetElement, found_first_id) { XML::Dom dom{ XML::NODE_TYPE_UNKNOWN, "test" }; XML::Dom* child = dom.appendChild( XML::NODE_TYPE_ELEMENT, "first" ); child->setAttribute( "id", "the-id" ); XML::Dom* expect = child; child = dom.appendChild( XML::NODE_TYPE_ELEMENT, "second" ); child->setAttribute( "id", "the-id" ); XML::Dom* result = dom.getElementById( "the-id" ); EXPECT_EQ( expect, result ); } TEST_F(XML_DomGetElement, empty_tag_name) { XML::Dom dom{ XML::NODE_TYPE_UNKNOWN, "test" }; const std::list result = dom.getElementsByTagName( "" ); EXPECT_TRUE( result.empty() ); } TEST_F(XML_DomGetElement, not_found_tag_name) { XML::Dom dom{ XML::NODE_TYPE_UNKNOWN, "test" }; dom.appendChild( XML::NODE_TYPE_UNKNOWN, "the-tag" ); dom.appendChild( XML::NODE_TYPE_TEXT, "the-tag" ); dom.appendChild( XML::NODE_TYPE_DOCUMENT, "the-tag" ); const std::list result = dom.getElementsByTagName( "" ); EXPECT_TRUE( result.empty() ); } TEST_F(XML_DomGetElement, found_tag_name) { XML::Dom dom{ XML::NODE_TYPE_UNKNOWN, "test" }; XML::Dom* first = dom.appendChild( XML::NODE_TYPE_ELEMENT, "the-tag" ); dom.appendChild( XML::NODE_TYPE_ELEMENT, "another-tag" ); XML::Dom* second = dom.appendChild( XML::NODE_TYPE_ELEMENT, "the-tag" ); const std::list expect = { first, second }; const std::list result = dom.getElementsByTagName( "the-tag" ); EXPECT_EQ( expect, result ); } class XML_DomChildren : public ::testing::Test {}; TEST_F(XML_DomChildren, has_child_nodes) { XML::Dom dom{ XML::NODE_TYPE_UNKNOWN, "test" }; bool result = dom.hasChildNodes(); EXPECT_FALSE( result ); dom.appendChild( XML::NODE_TYPE_ELEMENT, "child" ); result = dom.hasChildNodes(); EXPECT_TRUE( result ); } TEST_F(XML_DomChildren, child_nodes) { XML::Dom dom{ XML::NODE_TYPE_UNKNOWN, "unknown" }; XML::Dom* first = dom.appendChild( XML::NODE_TYPE_ELEMENT, "element" ); XML::Dom* second = dom.appendChild( XML::NODE_TYPE_TEXT, "text" ); XML::Dom* third = dom.appendChild( XML::NODE_TYPE_DOCUMENT, "document" ); const std::list expect = { first, second, third }; const std::list result{ dom.begin(), dom.end() }; EXPECT_EQ( expect, result ); } TEST_F(XML_DomChildren, append_child) { XML::Dom dom{ XML::NODE_TYPE_UNKNOWN, "test" }; XML::Dom* child = dom.appendChild( XML::NODE_TYPE_ELEMENT, "child" ); EXPECT_NE( nullptr, child ); EXPECT_EQ( XML::NODE_TYPE_ELEMENT, child->nodeType() ); EXPECT_EQ( "child", child->nodeName() ); } TEST_F(XML_DomChildren, first_child) { XML::Dom dom{ XML::NODE_TYPE_UNKNOWN, "test" }; XML::Dom* child = dom.firstChild(); EXPECT_EQ( nullptr, child ); dom.appendChild( XML::NODE_TYPE_ELEMENT, "child" ); child = dom.firstChild(); EXPECT_NE( nullptr, child ); EXPECT_EQ( XML::NODE_TYPE_ELEMENT, child->nodeType() ); EXPECT_EQ( "child", child->nodeName() ); } TEST_F(XML_DomChildren, remove_child) { XML::Dom dom{ XML::NODE_TYPE_UNKNOWN, "test" }; bool result = dom.removeChild( nullptr ); EXPECT_FALSE( result ); XML::Dom* child = dom.appendChild( XML::NODE_TYPE_ELEMENT, "child" ); result = dom.removeChild( child ); EXPECT_TRUE( result ); EXPECT_FALSE( dom.hasChildNodes() ); } TEST_F(XML_DomChildren, emplace_front) { XML::Dom dom{ XML::NODE_TYPE_UNKNOWN, "test" }; dom.appendChild( XML::NODE_TYPE_ELEMENT, "child1" ); XML::Dom* second_child = dom.emplace_front( XML::NODE_TYPE_ELEMENT, "child2" ); EXPECT_EQ( dom.firstChild(), second_child ); XML::Dom* third_child = dom.emplace_front( XML::NODE_TYPE_ELEMENT, "child2" ); EXPECT_EQ( dom.firstChild(), third_child ); } TEST_F(XML_DomChildren, children_clear) { XML::Dom dom{ XML::NODE_TYPE_UNKNOWN, "test" }; dom.appendChild( XML::NODE_TYPE_ELEMENT, "child1" ); dom.appendChild( XML::NODE_TYPE_ELEMENT, "child2" ); dom.clear(); EXPECT_FALSE( dom.hasChildNodes() ); } class XML_DomAttribute : public ::testing::Test {}; TEST_F(XML_DomAttribute, set_not_element_type) { XML::Dom dom_unknown{ XML::NODE_TYPE_UNKNOWN, "unknown" }; bool result = dom_unknown.setAttribute( "name", "value" ); EXPECT_FALSE( result ); XML::Dom dom_text{ XML::NODE_TYPE_TEXT, "text" }; result = dom_text.setAttribute( "name", "value" ); EXPECT_FALSE( result ); XML::Dom dom_doc{ XML::NODE_TYPE_DOCUMENT, "document" }; result = dom_doc.setAttribute( "name", "value" ); EXPECT_FALSE( result ); } TEST_F(XML_DomAttribute, set_empty_name_with_string_value) { XML::Dom dom{ XML::NODE_TYPE_ELEMENT, "test" }; bool result = dom.setAttribute( "", "value" ); EXPECT_FALSE( result ); } TEST_F(XML_DomAttribute, set_empty_name_with_integer_value) { XML::Dom dom{ XML::NODE_TYPE_ELEMENT, "test" }; bool result = dom.setAttribute( "", 123 ); EXPECT_FALSE( result ); } TEST_F(XML_DomAttribute, set_empty_string_value) { XML::Dom dom{ XML::NODE_TYPE_ELEMENT, "test" }; bool result = dom.setAttribute( "name", "" ); EXPECT_FALSE( result ); } TEST_F(XML_DomAttribute, get_empty) { XML::Dom dom{ XML::NODE_TYPE_ELEMENT, "test" }; std::string value = dom.getAttribute( "" ); EXPECT_EQ( "", value ); } TEST_F(XML_DomAttribute, get_not_found) { XML::Dom dom{ XML::NODE_TYPE_ELEMENT, "test" }; std::string value = dom.getAttribute( "not-found" ); EXPECT_EQ( "", value ); } TEST_F(XML_DomAttribute, set_string_value) { XML::Dom dom{ XML::NODE_TYPE_ELEMENT, "test" }; bool result = dom.setAttribute( "hello", "world" ); std::string value = dom.getAttribute( "hello" ); EXPECT_TRUE( result ); EXPECT_EQ( "world", value ); } TEST_F(XML_DomAttribute, set_string_value_twice) { XML::Dom dom{ XML::NODE_TYPE_ELEMENT, "test" }; dom.setAttribute( "hello", "world" ); bool result = dom.setAttribute( "hello", "jdim" ); std::string value = dom.getAttribute( "hello" ); EXPECT_TRUE( result ); EXPECT_EQ( "world", value ); } TEST_F(XML_DomAttribute, set_positive_integer_value) { XML::Dom dom{ XML::NODE_TYPE_ELEMENT, "test" }; bool result = dom.setAttribute( "width", 32767 ); std::string value = dom.getAttribute( "width" ); EXPECT_TRUE( result ); EXPECT_EQ( "32767", value ); } TEST_F(XML_DomAttribute, set_negative_integer_value) { XML::Dom dom{ XML::NODE_TYPE_ELEMENT, "test" }; bool result = dom.setAttribute( "width", -32768 ); std::string value = dom.getAttribute( "width" ); EXPECT_TRUE( result ); EXPECT_EQ( "-32768", value ); } TEST_F(XML_DomAttribute, set_integer_value_twice) { XML::Dom dom{ XML::NODE_TYPE_ELEMENT, "test" }; dom.setAttribute( "width", 12321 ); bool result = dom.setAttribute( "width", 0 ); std::string value = dom.getAttribute( "width" ); EXPECT_TRUE( result ); EXPECT_EQ( "12321", value ); } TEST_F(XML_DomAttribute, set_uppercase_name_with_string_value) { XML::Dom dom{ XML::NODE_TYPE_ELEMENT, "test" }; bool result = dom.setAttribute( "UPPER", "hello world" ); EXPECT_TRUE( result ); std::string value = dom.getAttribute( "UPPER" ); EXPECT_EQ( "", value ); value = dom.getAttribute( "upper" ); EXPECT_EQ( "hello world", value ); } TEST_F(XML_DomAttribute, set_uppercase_name_with_integer_value) { XML::Dom dom{ XML::NODE_TYPE_ELEMENT, "test" }; bool result = dom.setAttribute( "UPPER", 256 ); EXPECT_TRUE( result ); std::string value = dom.getAttribute( "UPPER" ); EXPECT_EQ( "", value ); value = dom.getAttribute( "upper" ); EXPECT_EQ( "256", value ); } } // namespace jdim-0.10.1/test/meson.build000066400000000000000000000012511445721505100156400ustar00rootroot00000000000000# Add test code source files to following list. sources = [ 'gtest_dbtree_nodetreebase.cpp', 'gtest_dbtree_spchar_decoder.cpp', 'gtest_jdlib_cookiemanager.cpp', 'gtest_jdlib_jdiconv.cpp', 'gtest_jdlib_jdregex.cpp', 'gtest_jdlib_loader.cpp', 'gtest_jdlib_misccharcode.cpp', 'gtest_jdlib_misctime.cpp', 'gtest_jdlib_misctrip.cpp', 'gtest_jdlib_miscutil.cpp', 'gtest_jdlib_span.cpp', 'gtest_xml_dom.cpp', ] deps = [ config_h_dep, gtest_main_dep, jdim_deps, ] test_exe = executable( 'gtest_jdim', [buildinfo_h, core_sources, sources], dependencies : deps, include_directories : jdim_incs, link_with : jdim_libs, ) test('gtest tests', test_exe)