typo 5.2.98 → 5.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/app/controllers/admin/sidebar_controller.rb +15 -3
- data/app/helpers/sidebar_helper.rb +9 -4
- data/app/models/feedback.rb +10 -2
- data/app/models/page_cache.rb +5 -1
- data/app/views/admin/blacklist/_blacklist_patterns.html.erb +2 -0
- data/app/views/admin/categories/new.html.erb +1 -1
- data/app/views/admin/content/_form.html.erb +3 -3
- data/app/views/admin/content/_simple_editor.html.erb +2 -0
- data/app/views/admin/content/_visual_editor.html.erb +2 -0
- data/app/views/admin/pages/_form.html.erb +5 -5
- data/app/views/admin/pages/_simple_editor.html.erb +2 -0
- data/app/views/admin/pages/_visual_editor.html.erb +2 -0
- data/app/views/articles/_comment_box.html.erb +1 -1
- data/app/views/articles/_comment_failed.html.erb +1 -1
- data/app/views/articles/read.html.erb +1 -1
- data/config/environment.rb +3 -2
- data/lib/tasks/release.rake +3 -4
- data/lib/typo_version.rb +1 -1
- data/public/stylesheets/administration.css +37 -2
- data/public/stylesheets/administration_rtl.css +316 -437
- data/vendor/actionwebservice/CHANGELOG +320 -0
- data/vendor/actionwebservice/MIT-LICENSE +21 -0
- data/vendor/actionwebservice/README +381 -0
- data/vendor/actionwebservice/Rakefile +173 -0
- data/vendor/actionwebservice/TODO +32 -0
- data/vendor/actionwebservice/examples/googlesearch/README +143 -0
- data/vendor/actionwebservice/examples/googlesearch/autoloading/google_search_api.rb +50 -0
- data/vendor/actionwebservice/examples/googlesearch/autoloading/google_search_controller.rb +57 -0
- data/vendor/actionwebservice/examples/googlesearch/delegated/google_search_service.rb +108 -0
- data/vendor/actionwebservice/examples/googlesearch/delegated/search_controller.rb +7 -0
- data/vendor/actionwebservice/examples/googlesearch/direct/google_search_api.rb +50 -0
- data/vendor/actionwebservice/examples/googlesearch/direct/search_controller.rb +58 -0
- data/vendor/actionwebservice/examples/metaWeblog/README +17 -0
- data/vendor/actionwebservice/examples/metaWeblog/apis/blogger_api.rb +60 -0
- data/vendor/actionwebservice/examples/metaWeblog/apis/blogger_service.rb +34 -0
- data/vendor/actionwebservice/examples/metaWeblog/apis/meta_weblog_api.rb +67 -0
- data/vendor/actionwebservice/examples/metaWeblog/apis/meta_weblog_service.rb +48 -0
- data/vendor/actionwebservice/examples/metaWeblog/controllers/xmlrpc_controller.rb +16 -0
- data/vendor/actionwebservice/generators/web_service/USAGE +28 -0
- data/vendor/actionwebservice/generators/web_service/templates/api_definition.rb +5 -0
- data/vendor/actionwebservice/generators/web_service/templates/controller.rb +8 -0
- data/vendor/actionwebservice/generators/web_service/templates/functional_test.rb +19 -0
- data/vendor/actionwebservice/generators/web_service/web_service_generator.rb +29 -0
- data/vendor/actionwebservice/lib/action_web_service.rb +66 -0
- data/vendor/actionwebservice/lib/action_web_service/api.rb +297 -0
- data/vendor/actionwebservice/lib/action_web_service/base.rb +38 -0
- data/vendor/actionwebservice/lib/action_web_service/casting.rb +144 -0
- data/vendor/actionwebservice/lib/action_web_service/client.rb +3 -0
- data/vendor/actionwebservice/lib/action_web_service/client/base.rb +28 -0
- data/vendor/actionwebservice/lib/action_web_service/client/soap_client.rb +113 -0
- data/vendor/actionwebservice/lib/action_web_service/client/xmlrpc_client.rb +58 -0
- data/vendor/actionwebservice/lib/action_web_service/container.rb +3 -0
- data/vendor/actionwebservice/lib/action_web_service/container/action_controller_container.rb +93 -0
- data/vendor/actionwebservice/lib/action_web_service/container/delegated_container.rb +86 -0
- data/vendor/actionwebservice/lib/action_web_service/container/direct_container.rb +69 -0
- data/vendor/actionwebservice/lib/action_web_service/dispatcher.rb +2 -0
- data/vendor/actionwebservice/lib/action_web_service/dispatcher/abstract.rb +207 -0
- data/vendor/actionwebservice/lib/action_web_service/dispatcher/action_controller_dispatcher.rb +379 -0
- data/vendor/actionwebservice/lib/action_web_service/invocation.rb +202 -0
- data/vendor/actionwebservice/lib/action_web_service/protocol.rb +4 -0
- data/vendor/actionwebservice/lib/action_web_service/protocol/abstract.rb +112 -0
- data/vendor/actionwebservice/lib/action_web_service/protocol/discovery.rb +37 -0
- data/vendor/actionwebservice/lib/action_web_service/protocol/soap_protocol.rb +176 -0
- data/vendor/actionwebservice/lib/action_web_service/protocol/soap_protocol/marshaler.rb +242 -0
- data/vendor/actionwebservice/lib/action_web_service/protocol/xmlrpc_protocol.rb +122 -0
- data/vendor/actionwebservice/lib/action_web_service/scaffolding.rb +281 -0
- data/vendor/actionwebservice/lib/action_web_service/struct.rb +64 -0
- data/vendor/actionwebservice/lib/action_web_service/support/class_inheritable_options.rb +26 -0
- data/vendor/actionwebservice/lib/action_web_service/support/signature_types.rb +226 -0
- data/vendor/actionwebservice/lib/action_web_service/templates/scaffolds/layout.html.erb +65 -0
- data/vendor/actionwebservice/lib/action_web_service/templates/scaffolds/methods.html.erb +6 -0
- data/vendor/actionwebservice/lib/action_web_service/templates/scaffolds/parameters.html.erb +29 -0
- data/vendor/actionwebservice/lib/action_web_service/templates/scaffolds/result.html.erb +30 -0
- data/vendor/actionwebservice/lib/action_web_service/test_invoke.rb +110 -0
- data/vendor/actionwebservice/lib/action_web_service/version.rb +9 -0
- data/vendor/actionwebservice/lib/actionwebservice.rb +1 -0
- data/vendor/actionwebservice/setup.rb +1379 -0
- data/vendor/actionwebservice/test/abstract_client.rb +183 -0
- data/vendor/actionwebservice/test/abstract_dispatcher.rb +547 -0
- data/vendor/actionwebservice/test/abstract_unit.rb +33 -0
- data/vendor/actionwebservice/test/api_test.rb +102 -0
- data/vendor/actionwebservice/test/apis/auto_load_api.rb +3 -0
- data/vendor/actionwebservice/test/apis/broken_auto_load_api.rb +2 -0
- data/vendor/actionwebservice/test/base_test.rb +42 -0
- data/vendor/actionwebservice/test/casting_test.rb +86 -0
- data/vendor/actionwebservice/test/client_soap_test.rb +155 -0
- data/vendor/actionwebservice/test/client_xmlrpc_test.rb +153 -0
- data/vendor/actionwebservice/test/container_test.rb +73 -0
- data/vendor/actionwebservice/test/dispatcher_action_controller_soap_test.rb +137 -0
- data/vendor/actionwebservice/test/dispatcher_action_controller_xmlrpc_test.rb +59 -0
- data/vendor/actionwebservice/test/fixtures/db_definitions/mysql.sql +8 -0
- data/vendor/actionwebservice/test/fixtures/users.yml +12 -0
- data/vendor/actionwebservice/test/gencov +3 -0
- data/vendor/actionwebservice/test/invocation_test.rb +185 -0
- data/vendor/actionwebservice/test/run +6 -0
- data/vendor/actionwebservice/test/scaffolded_controller_test.rb +146 -0
- data/vendor/actionwebservice/test/struct_test.rb +52 -0
- data/vendor/actionwebservice/test/test_invoke_test.rb +112 -0
- data/vendor/gems/calendar_date_select-1.15/.specification +56 -0
- data/vendor/gems/calendar_date_select-1.15/History.txt +237 -0
- data/vendor/gems/calendar_date_select-1.15/MIT-LICENSE +20 -0
- data/vendor/gems/calendar_date_select-1.15/Manifest.txt +42 -0
- data/vendor/gems/calendar_date_select-1.15/Rakefile +31 -0
- data/vendor/gems/calendar_date_select-1.15/Readme.txt +16 -0
- data/vendor/gems/calendar_date_select-1.15/init.rb +1 -0
- data/vendor/gems/calendar_date_select-1.15/js_test/functional/cds_test.html +334 -0
- data/vendor/gems/calendar_date_select-1.15/js_test/prototype.js +4184 -0
- data/vendor/gems/calendar_date_select-1.15/js_test/test.css +40 -0
- data/vendor/gems/calendar_date_select-1.15/js_test/unit/cds_helper_methods.html +46 -0
- data/vendor/gems/calendar_date_select-1.15/js_test/unittest.js +564 -0
- data/vendor/gems/calendar_date_select-1.15/lib/calendar_date_select.rb +33 -0
- data/vendor/gems/calendar_date_select-1.15/lib/calendar_date_select/calendar_date_select.rb +116 -0
- data/vendor/gems/calendar_date_select-1.15/lib/calendar_date_select/form_helpers.rb +225 -0
- data/vendor/gems/calendar_date_select-1.15/lib/calendar_date_select/includes_helper.rb +29 -0
- data/vendor/gems/calendar_date_select-1.15/public/blank_iframe.html +2 -0
- data/vendor/gems/calendar_date_select-1.15/public/images/calendar_date_select/calendar.gif +0 -0
- data/vendor/gems/calendar_date_select-1.15/public/javascripts/calendar_date_select/calendar_date_select.js +443 -0
- data/vendor/gems/calendar_date_select-1.15/public/javascripts/calendar_date_select/format_american.js +34 -0
- data/vendor/gems/calendar_date_select-1.15/public/javascripts/calendar_date_select/format_db.js +27 -0
- data/vendor/gems/calendar_date_select-1.15/public/javascripts/calendar_date_select/format_euro_24hr.js +7 -0
- data/vendor/gems/calendar_date_select-1.15/public/javascripts/calendar_date_select/format_euro_24hr_ymd.js +7 -0
- data/vendor/gems/calendar_date_select-1.15/public/javascripts/calendar_date_select/format_finnish.js +32 -0
- data/vendor/gems/calendar_date_select-1.15/public/javascripts/calendar_date_select/format_hyphen_ampm.js +37 -0
- data/vendor/gems/calendar_date_select-1.15/public/javascripts/calendar_date_select/format_iso_date.js +46 -0
- data/vendor/gems/calendar_date_select-1.15/public/javascripts/calendar_date_select/format_italian.js +24 -0
- data/vendor/gems/calendar_date_select-1.15/public/javascripts/calendar_date_select/locale/de.js +11 -0
- data/vendor/gems/calendar_date_select-1.15/public/javascripts/calendar_date_select/locale/fi.js +10 -0
- data/vendor/gems/calendar_date_select-1.15/public/javascripts/calendar_date_select/locale/fr.js +10 -0
- data/vendor/gems/calendar_date_select-1.15/public/javascripts/calendar_date_select/locale/pl.js +10 -0
- data/vendor/gems/calendar_date_select-1.15/public/javascripts/calendar_date_select/locale/pt.js +11 -0
- data/vendor/gems/calendar_date_select-1.15/public/javascripts/calendar_date_select/locale/ru.js +10 -0
- data/vendor/gems/calendar_date_select-1.15/public/stylesheets/calendar_date_select/blue.css +130 -0
- data/vendor/gems/calendar_date_select-1.15/public/stylesheets/calendar_date_select/default.css +135 -0
- data/vendor/gems/calendar_date_select-1.15/public/stylesheets/calendar_date_select/plain.css +128 -0
- data/vendor/gems/calendar_date_select-1.15/public/stylesheets/calendar_date_select/red.css +135 -0
- data/vendor/gems/calendar_date_select-1.15/public/stylesheets/calendar_date_select/silver.css +133 -0
- data/vendor/gems/calendar_date_select-1.15/spec/calendar_date_select/calendar_date_select_spec.rb +14 -0
- data/vendor/gems/calendar_date_select-1.15/spec/calendar_date_select/form_helpers_spec.rb +166 -0
- data/vendor/gems/calendar_date_select-1.15/spec/spec_helper.rb +26 -0
- data/vendor/gems/coderay-0.8.260/.specification +2 -1
- data/vendor/gems/htmlentities-4.0.0/.specification +68 -0
- data/vendor/gems/htmlentities-4.0.0/COPYING.txt +21 -0
- data/vendor/gems/htmlentities-4.0.0/History.txt +47 -0
- data/vendor/gems/htmlentities-4.0.0/README.txt +44 -0
- data/vendor/gems/htmlentities-4.0.0/lib/htmlentities.rb +165 -0
- data/vendor/gems/htmlentities-4.0.0/lib/htmlentities/html4.rb +257 -0
- data/vendor/gems/htmlentities-4.0.0/lib/htmlentities/legacy.rb +27 -0
- data/vendor/gems/htmlentities-4.0.0/lib/htmlentities/string.rb +26 -0
- data/vendor/gems/htmlentities-4.0.0/lib/htmlentities/xhtml1.rb +258 -0
- data/vendor/gems/htmlentities-4.0.0/test/entities_test.rb +206 -0
- data/vendor/gems/htmlentities-4.0.0/test/html4_test.rb +24 -0
- data/vendor/gems/htmlentities-4.0.0/test/legacy_test.rb +34 -0
- data/vendor/gems/htmlentities-4.0.0/test/roundtrip_test.rb +94 -0
- data/vendor/gems/htmlentities-4.0.0/test/string_test.rb +24 -0
- data/vendor/gems/htmlentities-4.0.0/test/test_all.rb +3 -0
- data/vendor/gems/htmlentities-4.0.0/test/xhtml1_test.rb +23 -0
- data/vendor/gems/json-1.1.3/.require_paths +4 -0
- data/vendor/gems/json-1.1.3/.specification +58 -0
- data/vendor/gems/json-1.1.3/CHANGES +93 -0
- data/vendor/gems/json-1.1.3/GPL +340 -0
- data/vendor/gems/json-1.1.3/README +78 -0
- data/vendor/gems/json-1.1.3/RUBY +58 -0
- data/vendor/gems/json-1.1.3/Rakefile +309 -0
- data/vendor/gems/json-1.1.3/TODO +1 -0
- data/vendor/gems/json-1.1.3/VERSION +1 -0
- data/vendor/gems/json-1.1.3/benchmarks/benchmark.txt +133 -0
- data/vendor/gems/json-1.1.3/benchmarks/benchmark_generator.rb +48 -0
- data/vendor/gems/json-1.1.3/benchmarks/benchmark_parser.rb +26 -0
- data/vendor/gems/json-1.1.3/benchmarks/benchmark_rails.rb +26 -0
- data/vendor/gems/json-1.1.3/bin/edit_json.rb +10 -0
- data/vendor/gems/json-1.1.3/bin/prettify_json.rb +76 -0
- data/vendor/gems/json-1.1.3/data/example.json +1 -0
- data/vendor/gems/json-1.1.3/data/index.html +38 -0
- data/vendor/gems/json-1.1.3/data/prototype.js +4184 -0
- data/vendor/gems/json-1.1.3/ext/json/ext/generator.bundle +0 -0
- data/vendor/gems/json-1.1.3/ext/json/ext/generator/Makefile +149 -0
- data/vendor/gems/json-1.1.3/ext/json/ext/generator/extconf.rb +9 -0
- data/vendor/gems/json-1.1.3/ext/json/ext/generator/generator.bundle +0 -0
- data/vendor/gems/json-1.1.3/ext/json/ext/generator/generator.c +875 -0
- data/vendor/gems/json-1.1.3/ext/json/ext/generator/generator.o +0 -0
- data/vendor/gems/json-1.1.3/ext/json/ext/generator/unicode.c +182 -0
- data/vendor/gems/json-1.1.3/ext/json/ext/generator/unicode.h +53 -0
- data/vendor/gems/json-1.1.3/ext/json/ext/generator/unicode.o +0 -0
- data/vendor/gems/json-1.1.3/ext/json/ext/parser.bundle +0 -0
- data/vendor/gems/json-1.1.3/ext/json/ext/parser/Makefile +149 -0
- data/vendor/gems/json-1.1.3/ext/json/ext/parser/extconf.rb +9 -0
- data/vendor/gems/json-1.1.3/ext/json/ext/parser/parser.bundle +0 -0
- data/vendor/gems/json-1.1.3/ext/json/ext/parser/parser.c +1758 -0
- data/vendor/gems/json-1.1.3/ext/json/ext/parser/parser.o +0 -0
- data/vendor/gems/json-1.1.3/ext/json/ext/parser/parser.rl +638 -0
- data/vendor/gems/json-1.1.3/ext/json/ext/parser/unicode.c +154 -0
- data/vendor/gems/json-1.1.3/ext/json/ext/parser/unicode.h +58 -0
- data/vendor/gems/json-1.1.3/ext/json/ext/parser/unicode.o +0 -0
- data/vendor/gems/json-1.1.3/install.rb +26 -0
- data/vendor/gems/json-1.1.3/lib/json.rb +235 -0
- data/vendor/gems/json-1.1.3/lib/json/Array.xpm +21 -0
- data/vendor/gems/json-1.1.3/lib/json/FalseClass.xpm +21 -0
- data/vendor/gems/json-1.1.3/lib/json/Hash.xpm +21 -0
- data/vendor/gems/json-1.1.3/lib/json/Key.xpm +73 -0
- data/vendor/gems/json-1.1.3/lib/json/NilClass.xpm +21 -0
- data/vendor/gems/json-1.1.3/lib/json/Numeric.xpm +28 -0
- data/vendor/gems/json-1.1.3/lib/json/String.xpm +96 -0
- data/vendor/gems/json-1.1.3/lib/json/TrueClass.xpm +21 -0
- data/vendor/gems/json-1.1.3/lib/json/add/core.rb +135 -0
- data/vendor/gems/json-1.1.3/lib/json/add/rails.rb +58 -0
- data/vendor/gems/json-1.1.3/lib/json/common.rb +354 -0
- data/vendor/gems/json-1.1.3/lib/json/editor.rb +1362 -0
- data/vendor/gems/json-1.1.3/lib/json/ext.rb +13 -0
- data/vendor/gems/json-1.1.3/lib/json/json.xpm +1499 -0
- data/vendor/gems/json-1.1.3/lib/json/pure.rb +75 -0
- data/vendor/gems/json-1.1.3/lib/json/pure/generator.rb +394 -0
- data/vendor/gems/json-1.1.3/lib/json/pure/parser.rb +259 -0
- data/vendor/gems/json-1.1.3/lib/json/version.rb +9 -0
- data/vendor/gems/json-1.1.3/tests/fixtures/fail1.json +1 -0
- data/vendor/gems/json-1.1.3/tests/fixtures/fail10.json +1 -0
- data/vendor/gems/json-1.1.3/tests/fixtures/fail11.json +1 -0
- data/vendor/gems/json-1.1.3/tests/fixtures/fail12.json +1 -0
- data/vendor/gems/json-1.1.3/tests/fixtures/fail13.json +1 -0
- data/vendor/gems/json-1.1.3/tests/fixtures/fail14.json +1 -0
- data/vendor/gems/json-1.1.3/tests/fixtures/fail18.json +1 -0
- data/vendor/gems/json-1.1.3/tests/fixtures/fail19.json +1 -0
- data/vendor/gems/json-1.1.3/tests/fixtures/fail2.json +1 -0
- data/vendor/gems/json-1.1.3/tests/fixtures/fail20.json +1 -0
- data/vendor/gems/json-1.1.3/tests/fixtures/fail21.json +1 -0
- data/vendor/gems/json-1.1.3/tests/fixtures/fail22.json +1 -0
- data/vendor/gems/json-1.1.3/tests/fixtures/fail23.json +1 -0
- data/vendor/gems/json-1.1.3/tests/fixtures/fail24.json +1 -0
- data/vendor/gems/json-1.1.3/tests/fixtures/fail25.json +1 -0
- data/vendor/gems/json-1.1.3/tests/fixtures/fail27.json +2 -0
- data/vendor/gems/json-1.1.3/tests/fixtures/fail28.json +2 -0
- data/vendor/gems/json-1.1.3/tests/fixtures/fail3.json +1 -0
- data/vendor/gems/json-1.1.3/tests/fixtures/fail4.json +1 -0
- data/vendor/gems/json-1.1.3/tests/fixtures/fail5.json +1 -0
- data/vendor/gems/json-1.1.3/tests/fixtures/fail6.json +1 -0
- data/vendor/gems/json-1.1.3/tests/fixtures/fail7.json +1 -0
- data/vendor/gems/json-1.1.3/tests/fixtures/fail8.json +1 -0
- data/vendor/gems/json-1.1.3/tests/fixtures/fail9.json +1 -0
- data/vendor/gems/json-1.1.3/tests/fixtures/pass1.json +56 -0
- data/vendor/gems/json-1.1.3/tests/fixtures/pass15.json +1 -0
- data/vendor/gems/json-1.1.3/tests/fixtures/pass16.json +1 -0
- data/vendor/gems/json-1.1.3/tests/fixtures/pass17.json +1 -0
- data/vendor/gems/json-1.1.3/tests/fixtures/pass2.json +1 -0
- data/vendor/gems/json-1.1.3/tests/fixtures/pass26.json +1 -0
- data/vendor/gems/json-1.1.3/tests/fixtures/pass3.json +6 -0
- data/vendor/gems/json-1.1.3/tests/runner.rb +25 -0
- data/vendor/gems/json-1.1.3/tests/test_json.rb +293 -0
- data/vendor/gems/json-1.1.3/tests/test_json_addition.rb +161 -0
- data/vendor/gems/json-1.1.3/tests/test_json_fixtures.rb +30 -0
- data/vendor/gems/json-1.1.3/tests/test_json_generate.rb +100 -0
- data/vendor/gems/json-1.1.3/tests/test_json_rails.rb +118 -0
- data/vendor/gems/json-1.1.3/tests/test_json_unicode.rb +61 -0
- data/vendor/gems/json-1.1.3/tools/fuzz.rb +140 -0
- data/vendor/gems/json-1.1.3/tools/server.rb +62 -0
- data/vendor/plugins/localization/bin/make_language.rb +2 -2
- data/vendor/plugins/localization/lib/localization.rb +2 -2
- data/vendor/plugins/will_paginate/CHANGELOG.rdoc +110 -0
- data/vendor/plugins/will_paginate/LICENSE +18 -0
- data/vendor/plugins/will_paginate/README.rdoc +107 -0
- data/vendor/plugins/will_paginate/Rakefile +62 -0
- data/vendor/plugins/will_paginate/examples/apple-circle.gif +0 -0
- data/vendor/plugins/will_paginate/examples/index.haml +69 -0
- data/vendor/plugins/will_paginate/examples/index.html +92 -0
- data/vendor/plugins/will_paginate/examples/pagination.css +90 -0
- data/vendor/plugins/will_paginate/examples/pagination.sass +91 -0
- data/vendor/plugins/will_paginate/init.rb +1 -0
- data/vendor/plugins/will_paginate/lib/will_paginate.rb +82 -0
- data/vendor/plugins/will_paginate/lib/will_paginate/array.rb +16 -0
- data/vendor/plugins/will_paginate/lib/will_paginate/collection.rb +146 -0
- data/vendor/plugins/will_paginate/lib/will_paginate/core_ext.rb +32 -0
- data/vendor/plugins/will_paginate/lib/will_paginate/finder.rb +260 -0
- data/vendor/plugins/will_paginate/lib/will_paginate/named_scope.rb +170 -0
- data/vendor/plugins/will_paginate/lib/will_paginate/named_scope_patch.rb +37 -0
- data/vendor/plugins/will_paginate/lib/will_paginate/version.rb +9 -0
- data/vendor/plugins/will_paginate/lib/will_paginate/view_helpers.rb +383 -0
- data/vendor/plugins/will_paginate/test/boot.rb +21 -0
- data/vendor/plugins/will_paginate/test/collection_test.rb +143 -0
- data/vendor/plugins/will_paginate/test/console +8 -0
- data/vendor/plugins/will_paginate/test/database.yml +22 -0
- data/vendor/plugins/will_paginate/test/finder_test.rb +476 -0
- data/vendor/plugins/will_paginate/test/fixtures/admin.rb +3 -0
- data/vendor/plugins/will_paginate/test/fixtures/developer.rb +14 -0
- data/vendor/plugins/will_paginate/test/fixtures/developers_projects.yml +13 -0
- data/vendor/plugins/will_paginate/test/fixtures/project.rb +15 -0
- data/vendor/plugins/will_paginate/test/fixtures/projects.yml +6 -0
- data/vendor/plugins/will_paginate/test/fixtures/replies.yml +29 -0
- data/vendor/plugins/will_paginate/test/fixtures/reply.rb +7 -0
- data/vendor/plugins/will_paginate/test/fixtures/schema.rb +38 -0
- data/vendor/plugins/will_paginate/test/fixtures/topic.rb +10 -0
- data/vendor/plugins/will_paginate/test/fixtures/topics.yml +30 -0
- data/vendor/plugins/will_paginate/test/fixtures/user.rb +2 -0
- data/vendor/plugins/will_paginate/test/fixtures/users.yml +35 -0
- data/vendor/plugins/will_paginate/test/helper.rb +37 -0
- data/vendor/plugins/will_paginate/test/lib/activerecord_test_case.rb +36 -0
- data/vendor/plugins/will_paginate/test/lib/activerecord_test_connector.rb +73 -0
- data/vendor/plugins/will_paginate/test/lib/load_fixtures.rb +11 -0
- data/vendor/plugins/will_paginate/test/lib/view_test_process.rb +165 -0
- data/vendor/plugins/will_paginate/test/tasks.rake +59 -0
- data/vendor/plugins/will_paginate/test/view_test.rb +363 -0
- data/vendor/plugins/will_paginate/will_paginate.gemspec +22 -0
- data/vendor/plugins/xml_sidebar/lib/xml_sidebar.rb +1 -1
- metadata +349 -16
- data/migrate.txt +0 -142
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# This file contains implementations of rails custom objects for
|
|
2
|
+
# serialisation/deserialisation.
|
|
3
|
+
|
|
4
|
+
unless Object.const_defined?(:JSON) and ::JSON.const_defined?(:JSON_LOADED) and
|
|
5
|
+
::JSON::JSON_LOADED
|
|
6
|
+
require 'json'
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
class Object
|
|
10
|
+
def self.json_create(object)
|
|
11
|
+
obj = new
|
|
12
|
+
for key, value in object
|
|
13
|
+
next if key == 'json_class'
|
|
14
|
+
instance_variable_set "@#{key}", value
|
|
15
|
+
end
|
|
16
|
+
obj
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def to_json(*a)
|
|
20
|
+
result = {
|
|
21
|
+
'json_class' => self.class.name
|
|
22
|
+
}
|
|
23
|
+
instance_variables.inject(result) do |r, name|
|
|
24
|
+
r[name[1..-1]] = instance_variable_get name
|
|
25
|
+
r
|
|
26
|
+
end
|
|
27
|
+
result.to_json(*a)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
class Symbol
|
|
32
|
+
def to_json(*a)
|
|
33
|
+
to_s.to_json(*a)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
module Enumerable
|
|
38
|
+
def to_json(*a)
|
|
39
|
+
to_a.to_json(*a)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# class Regexp
|
|
44
|
+
# def to_json(*)
|
|
45
|
+
# inspect
|
|
46
|
+
# end
|
|
47
|
+
# end
|
|
48
|
+
#
|
|
49
|
+
# The above rails definition has some problems:
|
|
50
|
+
#
|
|
51
|
+
# 1. { 'foo' => /bar/ }.to_json # => "{foo: /bar/}"
|
|
52
|
+
# This isn't valid JSON, because the regular expression syntax is not
|
|
53
|
+
# defined in RFC 4627. (And unquoted strings are disallowed there, too.)
|
|
54
|
+
# Though it is valid Javascript.
|
|
55
|
+
#
|
|
56
|
+
# 2. { 'foo' => /bar/mix }.to_json # => "{foo: /bar/mix}"
|
|
57
|
+
# This isn't even valid Javascript.
|
|
58
|
+
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
require 'json/version'
|
|
2
|
+
|
|
3
|
+
module JSON
|
|
4
|
+
class << self
|
|
5
|
+
# If _object_ is string-like parse the string and return the parsed result
|
|
6
|
+
# as a Ruby data structure. Otherwise generate a JSON text from the Ruby
|
|
7
|
+
# data structure object and return it.
|
|
8
|
+
#
|
|
9
|
+
# The _opts_ argument is passed through to generate/parse respectively, see
|
|
10
|
+
# generate and parse for their documentation.
|
|
11
|
+
def [](object, opts = {})
|
|
12
|
+
if object.respond_to? :to_str
|
|
13
|
+
JSON.parse(object.to_str, opts => {})
|
|
14
|
+
else
|
|
15
|
+
JSON.generate(object, opts => {})
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Returns the JSON parser class, that is used by JSON. This might be either
|
|
20
|
+
# JSON::Ext::Parser or JSON::Pure::Parser.
|
|
21
|
+
attr_reader :parser
|
|
22
|
+
|
|
23
|
+
# Set the JSON parser class _parser_ to be used by JSON.
|
|
24
|
+
def parser=(parser) # :nodoc:
|
|
25
|
+
@parser = parser
|
|
26
|
+
remove_const :Parser if const_defined? :Parser
|
|
27
|
+
const_set :Parser, parser
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Return the constant located at _path_. The format of _path_ has to be
|
|
31
|
+
# either ::A::B::C or A::B::C. In any case A has to be located at the top
|
|
32
|
+
# level (absolute namespace path?). If there doesn't exist a constant at
|
|
33
|
+
# the given path, an ArgumentError is raised.
|
|
34
|
+
def deep_const_get(path) # :nodoc:
|
|
35
|
+
path = path.to_s
|
|
36
|
+
path.split(/::/).inject(Object) do |p, c|
|
|
37
|
+
case
|
|
38
|
+
when c.empty? then p
|
|
39
|
+
when p.const_defined?(c) then p.const_get(c)
|
|
40
|
+
else raise ArgumentError, "can't find const #{path}"
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Set the module _generator_ to be used by JSON.
|
|
46
|
+
def generator=(generator) # :nodoc:
|
|
47
|
+
@generator = generator
|
|
48
|
+
generator_methods = generator::GeneratorMethods
|
|
49
|
+
for const in generator_methods.constants
|
|
50
|
+
klass = deep_const_get(const)
|
|
51
|
+
modul = generator_methods.const_get(const)
|
|
52
|
+
klass.class_eval do
|
|
53
|
+
instance_methods(false).each do |m|
|
|
54
|
+
m.to_s == 'to_json' and remove_method m
|
|
55
|
+
end
|
|
56
|
+
include modul
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
self.state = generator::State
|
|
60
|
+
const_set :State, self.state
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Returns the JSON generator modul, that is used by JSON. This might be
|
|
64
|
+
# either JSON::Ext::Generator or JSON::Pure::Generator.
|
|
65
|
+
attr_reader :generator
|
|
66
|
+
|
|
67
|
+
# Returns the JSON generator state class, that is used by JSON. This might
|
|
68
|
+
# be either JSON::Ext::Generator::State or JSON::Pure::Generator::State.
|
|
69
|
+
attr_accessor :state
|
|
70
|
+
|
|
71
|
+
# This is create identifier, that is used to decide, if the _json_create_
|
|
72
|
+
# hook of a class should be called. It defaults to 'json_class'.
|
|
73
|
+
attr_accessor :create_id
|
|
74
|
+
end
|
|
75
|
+
self.create_id = 'json_class'
|
|
76
|
+
|
|
77
|
+
NaN = (-1.0) ** 0.5
|
|
78
|
+
|
|
79
|
+
Infinity = 1.0/0
|
|
80
|
+
|
|
81
|
+
MinusInfinity = -Infinity
|
|
82
|
+
|
|
83
|
+
# The base exception for JSON errors.
|
|
84
|
+
class JSONError < StandardError; end
|
|
85
|
+
|
|
86
|
+
# This exception is raised, if a parser error occurs.
|
|
87
|
+
class ParserError < JSONError; end
|
|
88
|
+
|
|
89
|
+
# This exception is raised, if the nesting of parsed datastructures is too
|
|
90
|
+
# deep.
|
|
91
|
+
class NestingError < ParserError; end
|
|
92
|
+
|
|
93
|
+
# This exception is raised, if a generator or unparser error occurs.
|
|
94
|
+
class GeneratorError < JSONError; end
|
|
95
|
+
# For backwards compatibility
|
|
96
|
+
UnparserError = GeneratorError
|
|
97
|
+
|
|
98
|
+
# If a circular data structure is encountered while unparsing
|
|
99
|
+
# this exception is raised.
|
|
100
|
+
class CircularDatastructure < GeneratorError; end
|
|
101
|
+
|
|
102
|
+
# This exception is raised, if the required unicode support is missing on the
|
|
103
|
+
# system. Usually this means, that the iconv library is not installed.
|
|
104
|
+
class MissingUnicodeSupport < JSONError; end
|
|
105
|
+
|
|
106
|
+
module_function
|
|
107
|
+
|
|
108
|
+
# Parse the JSON string _source_ into a Ruby data structure and return it.
|
|
109
|
+
#
|
|
110
|
+
# _opts_ can have the following
|
|
111
|
+
# keys:
|
|
112
|
+
# * *max_nesting*: The maximum depth of nesting allowed in the parsed data
|
|
113
|
+
# structures. Disable depth checking with :max_nesting => false, it defaults
|
|
114
|
+
# to 19.
|
|
115
|
+
# * *allow_nan*: If set to true, allow NaN, Infinity and -Infinity in
|
|
116
|
+
# defiance of RFC 4627 to be parsed by the Parser. This option defaults
|
|
117
|
+
# to false.
|
|
118
|
+
# * *create_additions*: If set to false, the Parser doesn't create
|
|
119
|
+
# additions even if a matchin class and create_id was found. This option
|
|
120
|
+
# defaults to true.
|
|
121
|
+
def parse(source, opts = {})
|
|
122
|
+
JSON.parser.new(source, opts).parse
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Parse the JSON string _source_ into a Ruby data structure and return it.
|
|
126
|
+
# The bang version of the parse method, defaults to the more dangerous values
|
|
127
|
+
# for the _opts_ hash, so be sure only to parse trusted _source_ strings.
|
|
128
|
+
#
|
|
129
|
+
# _opts_ can have the following keys:
|
|
130
|
+
# * *max_nesting*: The maximum depth of nesting allowed in the parsed data
|
|
131
|
+
# structures. Enable depth checking with :max_nesting => anInteger. The parse!
|
|
132
|
+
# methods defaults to not doing max depth checking: This can be dangerous,
|
|
133
|
+
# if someone wants to fill up your stack.
|
|
134
|
+
# * *allow_nan*: If set to true, allow NaN, Infinity, and -Infinity in
|
|
135
|
+
# defiance of RFC 4627 to be parsed by the Parser. This option defaults
|
|
136
|
+
# to true.
|
|
137
|
+
# * *create_additions*: If set to false, the Parser doesn't create
|
|
138
|
+
# additions even if a matchin class and create_id was found. This option
|
|
139
|
+
# defaults to true.
|
|
140
|
+
def parse!(source, opts = {})
|
|
141
|
+
opts = {
|
|
142
|
+
:max_nesting => false,
|
|
143
|
+
:allow_nan => true
|
|
144
|
+
}.update(opts)
|
|
145
|
+
JSON.parser.new(source, opts).parse
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Unparse the Ruby data structure _obj_ into a single line JSON string and
|
|
149
|
+
# return it. _state_ is
|
|
150
|
+
# * a JSON::State object,
|
|
151
|
+
# * or a Hash like object (responding to to_hash),
|
|
152
|
+
# * an object convertible into a hash by a to_h method,
|
|
153
|
+
# that is used as or to configure a State object.
|
|
154
|
+
#
|
|
155
|
+
# It defaults to a state object, that creates the shortest possible JSON text
|
|
156
|
+
# in one line, checks for circular data structures and doesn't allow NaN,
|
|
157
|
+
# Infinity, and -Infinity.
|
|
158
|
+
#
|
|
159
|
+
# A _state_ hash can have the following keys:
|
|
160
|
+
# * *indent*: a string used to indent levels (default: ''),
|
|
161
|
+
# * *space*: a string that is put after, a : or , delimiter (default: ''),
|
|
162
|
+
# * *space_before*: a string that is put before a : pair delimiter (default: ''),
|
|
163
|
+
# * *object_nl*: a string that is put at the end of a JSON object (default: ''),
|
|
164
|
+
# * *array_nl*: a string that is put at the end of a JSON array (default: ''),
|
|
165
|
+
# * *check_circular*: true if checking for circular data structures
|
|
166
|
+
# should be done (the default), false otherwise.
|
|
167
|
+
# * *allow_nan*: true if NaN, Infinity, and -Infinity should be
|
|
168
|
+
# generated, otherwise an exception is thrown, if these values are
|
|
169
|
+
# encountered. This options defaults to false.
|
|
170
|
+
# * *max_nesting*: The maximum depth of nesting allowed in the data
|
|
171
|
+
# structures from which JSON is to be generated. Disable depth checking
|
|
172
|
+
# with :max_nesting => false, it defaults to 19.
|
|
173
|
+
#
|
|
174
|
+
# See also the fast_generate for the fastest creation method with the least
|
|
175
|
+
# amount of sanity checks, and the pretty_generate method for some
|
|
176
|
+
# defaults for a pretty output.
|
|
177
|
+
def generate(obj, state = nil)
|
|
178
|
+
if state
|
|
179
|
+
state = State.from_state(state)
|
|
180
|
+
else
|
|
181
|
+
state = State.new
|
|
182
|
+
end
|
|
183
|
+
obj.to_json(state)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# :stopdoc:
|
|
187
|
+
# I want to deprecate these later, so I'll first be silent about them, and
|
|
188
|
+
# later delete them.
|
|
189
|
+
alias unparse generate
|
|
190
|
+
module_function :unparse
|
|
191
|
+
# :startdoc:
|
|
192
|
+
|
|
193
|
+
# Unparse the Ruby data structure _obj_ into a single line JSON string and
|
|
194
|
+
# return it. This method disables the checks for circles in Ruby objects, and
|
|
195
|
+
# also generates NaN, Infinity, and, -Infinity float values.
|
|
196
|
+
#
|
|
197
|
+
# *WARNING*: Be careful not to pass any Ruby data structures with circles as
|
|
198
|
+
# _obj_ argument, because this will cause JSON to go into an infinite loop.
|
|
199
|
+
def fast_generate(obj)
|
|
200
|
+
obj.to_json(nil)
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# :stopdoc:
|
|
204
|
+
# I want to deprecate these later, so I'll first be silent about them, and later delete them.
|
|
205
|
+
alias fast_unparse fast_generate
|
|
206
|
+
module_function :fast_unparse
|
|
207
|
+
# :startdoc:
|
|
208
|
+
|
|
209
|
+
# Unparse the Ruby data structure _obj_ into a JSON string and return it. The
|
|
210
|
+
# returned string is a prettier form of the string returned by #unparse.
|
|
211
|
+
#
|
|
212
|
+
# The _opts_ argument can be used to configure the generator, see the
|
|
213
|
+
# generate method for a more detailed explanation.
|
|
214
|
+
def pretty_generate(obj, opts = nil)
|
|
215
|
+
state = JSON.state.new(
|
|
216
|
+
:indent => ' ',
|
|
217
|
+
:space => ' ',
|
|
218
|
+
:object_nl => "\n",
|
|
219
|
+
:array_nl => "\n",
|
|
220
|
+
:check_circular => true
|
|
221
|
+
)
|
|
222
|
+
if opts
|
|
223
|
+
if opts.respond_to? :to_hash
|
|
224
|
+
opts = opts.to_hash
|
|
225
|
+
elsif opts.respond_to? :to_h
|
|
226
|
+
opts = opts.to_h
|
|
227
|
+
else
|
|
228
|
+
raise TypeError, "can't convert #{opts.class} into Hash"
|
|
229
|
+
end
|
|
230
|
+
state.configure(opts)
|
|
231
|
+
end
|
|
232
|
+
obj.to_json(state)
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# :stopdoc:
|
|
236
|
+
# I want to deprecate these later, so I'll first be silent about them, and later delete them.
|
|
237
|
+
alias pretty_unparse pretty_generate
|
|
238
|
+
module_function :pretty_unparse
|
|
239
|
+
# :startdoc:
|
|
240
|
+
|
|
241
|
+
# Load a ruby data structure from a JSON _source_ and return it. A source can
|
|
242
|
+
# either be a string-like object, an IO like object, or an object responding
|
|
243
|
+
# to the read method. If _proc_ was given, it will be called with any nested
|
|
244
|
+
# Ruby object as an argument recursively in depth first order.
|
|
245
|
+
#
|
|
246
|
+
# This method is part of the implementation of the load/dump interface of
|
|
247
|
+
# Marshal and YAML.
|
|
248
|
+
def load(source, proc = nil)
|
|
249
|
+
if source.respond_to? :to_str
|
|
250
|
+
source = source.to_str
|
|
251
|
+
elsif source.respond_to? :to_io
|
|
252
|
+
source = source.to_io.read
|
|
253
|
+
else
|
|
254
|
+
source = source.read
|
|
255
|
+
end
|
|
256
|
+
result = parse(source, :max_nesting => false, :allow_nan => true)
|
|
257
|
+
recurse_proc(result, &proc) if proc
|
|
258
|
+
result
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def recurse_proc(result, &proc)
|
|
262
|
+
case result
|
|
263
|
+
when Array
|
|
264
|
+
result.each { |x| recurse_proc x, &proc }
|
|
265
|
+
proc.call result
|
|
266
|
+
when Hash
|
|
267
|
+
result.each { |x, y| recurse_proc x, &proc; recurse_proc y, &proc }
|
|
268
|
+
proc.call result
|
|
269
|
+
else
|
|
270
|
+
proc.call result
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
private :recurse_proc
|
|
274
|
+
module_function :recurse_proc
|
|
275
|
+
|
|
276
|
+
alias restore load
|
|
277
|
+
module_function :restore
|
|
278
|
+
|
|
279
|
+
# Dumps _obj_ as a JSON string, i.e. calls generate on the object and returns
|
|
280
|
+
# the result.
|
|
281
|
+
#
|
|
282
|
+
# If anIO (an IO like object or an object that responds to the write method)
|
|
283
|
+
# was given, the resulting JSON is written to it.
|
|
284
|
+
#
|
|
285
|
+
# If the number of nested arrays or objects exceeds _limit_ an ArgumentError
|
|
286
|
+
# exception is raised. This argument is similar (but not exactly the
|
|
287
|
+
# same!) to the _limit_ argument in Marshal.dump.
|
|
288
|
+
#
|
|
289
|
+
# This method is part of the implementation of the load/dump interface of
|
|
290
|
+
# Marshal and YAML.
|
|
291
|
+
def dump(obj, anIO = nil, limit = nil)
|
|
292
|
+
if anIO and limit.nil?
|
|
293
|
+
anIO = anIO.to_io if anIO.respond_to?(:to_io)
|
|
294
|
+
unless anIO.respond_to?(:write)
|
|
295
|
+
limit = anIO
|
|
296
|
+
anIO = nil
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
limit ||= 0
|
|
300
|
+
result = generate(obj, :allow_nan => true, :max_nesting => limit)
|
|
301
|
+
if anIO
|
|
302
|
+
anIO.write result
|
|
303
|
+
anIO
|
|
304
|
+
else
|
|
305
|
+
result
|
|
306
|
+
end
|
|
307
|
+
rescue JSON::NestingError
|
|
308
|
+
raise ArgumentError, "exceed depth limit"
|
|
309
|
+
end
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
module ::Kernel
|
|
313
|
+
# Outputs _objs_ to STDOUT as JSON strings in the shortest form, that is in
|
|
314
|
+
# one line.
|
|
315
|
+
def j(*objs)
|
|
316
|
+
objs.each do |obj|
|
|
317
|
+
puts JSON::generate(obj, :allow_nan => true, :max_nesting => false)
|
|
318
|
+
end
|
|
319
|
+
nil
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
# Ouputs _objs_ to STDOUT as JSON strings in a pretty format, with
|
|
323
|
+
# indentation and over many lines.
|
|
324
|
+
def jj(*objs)
|
|
325
|
+
objs.each do |obj|
|
|
326
|
+
puts JSON::pretty_generate(obj, :allow_nan => true, :max_nesting => false)
|
|
327
|
+
end
|
|
328
|
+
nil
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
# If _object_ is string-like parse the string and return the parsed result as
|
|
332
|
+
# a Ruby data structure. Otherwise generate a JSON text from the Ruby data
|
|
333
|
+
# structure object and return it.
|
|
334
|
+
#
|
|
335
|
+
# The _opts_ argument is passed through to generate/parse respectively, see
|
|
336
|
+
# generate and parse for their documentation.
|
|
337
|
+
def JSON(object, opts = {})
|
|
338
|
+
if object.respond_to? :to_str
|
|
339
|
+
JSON.parse(object.to_str, opts)
|
|
340
|
+
else
|
|
341
|
+
JSON.generate(object, opts)
|
|
342
|
+
end
|
|
343
|
+
end
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
class ::Class
|
|
347
|
+
# Returns true, if this class can be used to create an instance
|
|
348
|
+
# from a serialised JSON string. The class has to implement a class
|
|
349
|
+
# method _json_create_ that expects a hash as first parameter, which includes
|
|
350
|
+
# the required data.
|
|
351
|
+
def json_creatable?
|
|
352
|
+
respond_to?(:json_create)
|
|
353
|
+
end
|
|
354
|
+
end
|
|
@@ -0,0 +1,1362 @@
|
|
|
1
|
+
# To use the GUI JSON editor, start the edit_json.rb executable script. It
|
|
2
|
+
# requires ruby-gtk to be installed.
|
|
3
|
+
|
|
4
|
+
require 'gtk2'
|
|
5
|
+
require 'iconv'
|
|
6
|
+
require 'json'
|
|
7
|
+
require 'rbconfig'
|
|
8
|
+
require 'open-uri'
|
|
9
|
+
|
|
10
|
+
module JSON
|
|
11
|
+
module Editor
|
|
12
|
+
include Gtk
|
|
13
|
+
|
|
14
|
+
# Beginning of the editor window title
|
|
15
|
+
TITLE = 'JSON Editor'.freeze
|
|
16
|
+
|
|
17
|
+
# Columns constants
|
|
18
|
+
ICON_COL, TYPE_COL, CONTENT_COL = 0, 1, 2
|
|
19
|
+
|
|
20
|
+
# JSON primitive types (Containers)
|
|
21
|
+
CONTAINER_TYPES = %w[Array Hash].sort
|
|
22
|
+
# All JSON primitive types
|
|
23
|
+
ALL_TYPES = (%w[TrueClass FalseClass Numeric String NilClass] +
|
|
24
|
+
CONTAINER_TYPES).sort
|
|
25
|
+
|
|
26
|
+
# The Nodes necessary for the tree representation of a JSON document
|
|
27
|
+
ALL_NODES = (ALL_TYPES + %w[Key]).sort
|
|
28
|
+
|
|
29
|
+
DEFAULT_DIALOG_KEY_PRESS_HANDLER = lambda do |dialog, event|
|
|
30
|
+
case event.keyval
|
|
31
|
+
when Gdk::Keyval::GDK_Return
|
|
32
|
+
dialog.response Dialog::RESPONSE_ACCEPT
|
|
33
|
+
when Gdk::Keyval::GDK_Escape
|
|
34
|
+
dialog.response Dialog::RESPONSE_REJECT
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Returns the Gdk::Pixbuf of the icon named _name_ from the icon cache.
|
|
39
|
+
def Editor.fetch_icon(name)
|
|
40
|
+
@icon_cache ||= {}
|
|
41
|
+
unless @icon_cache.key?(name)
|
|
42
|
+
path = File.dirname(__FILE__)
|
|
43
|
+
@icon_cache[name] = Gdk::Pixbuf.new(File.join(path, name + '.xpm'))
|
|
44
|
+
end
|
|
45
|
+
@icon_cache[name]
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Opens an error dialog on top of _window_ showing the error message
|
|
49
|
+
# _text_.
|
|
50
|
+
def Editor.error_dialog(window, text)
|
|
51
|
+
dialog = MessageDialog.new(window, Dialog::MODAL,
|
|
52
|
+
MessageDialog::ERROR,
|
|
53
|
+
MessageDialog::BUTTONS_CLOSE, text)
|
|
54
|
+
dialog.show_all
|
|
55
|
+
dialog.run
|
|
56
|
+
rescue TypeError
|
|
57
|
+
dialog = MessageDialog.new(Editor.window, Dialog::MODAL,
|
|
58
|
+
MessageDialog::ERROR,
|
|
59
|
+
MessageDialog::BUTTONS_CLOSE, text)
|
|
60
|
+
dialog.show_all
|
|
61
|
+
dialog.run
|
|
62
|
+
ensure
|
|
63
|
+
dialog.destroy if dialog
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Opens a yes/no question dialog on top of _window_ showing the error
|
|
67
|
+
# message _text_. If yes was answered _true_ is returned, otherwise
|
|
68
|
+
# _false_.
|
|
69
|
+
def Editor.question_dialog(window, text)
|
|
70
|
+
dialog = MessageDialog.new(window, Dialog::MODAL,
|
|
71
|
+
MessageDialog::QUESTION,
|
|
72
|
+
MessageDialog::BUTTONS_YES_NO, text)
|
|
73
|
+
dialog.show_all
|
|
74
|
+
dialog.run do |response|
|
|
75
|
+
return Gtk::Dialog::RESPONSE_YES === response
|
|
76
|
+
end
|
|
77
|
+
ensure
|
|
78
|
+
dialog.destroy if dialog
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Convert the tree model starting from Gtk::TreeIter _iter_ into a Ruby
|
|
82
|
+
# data structure and return it.
|
|
83
|
+
def Editor.model2data(iter)
|
|
84
|
+
return nil if iter.nil?
|
|
85
|
+
case iter.type
|
|
86
|
+
when 'Hash'
|
|
87
|
+
hash = {}
|
|
88
|
+
iter.each { |c| hash[c.content] = Editor.model2data(c.first_child) }
|
|
89
|
+
hash
|
|
90
|
+
when 'Array'
|
|
91
|
+
array = Array.new(iter.n_children)
|
|
92
|
+
iter.each_with_index { |c, i| array[i] = Editor.model2data(c) }
|
|
93
|
+
array
|
|
94
|
+
when 'Key'
|
|
95
|
+
iter.content
|
|
96
|
+
when 'String'
|
|
97
|
+
iter.content
|
|
98
|
+
when 'Numeric'
|
|
99
|
+
content = iter.content
|
|
100
|
+
if /\./.match(content)
|
|
101
|
+
content.to_f
|
|
102
|
+
else
|
|
103
|
+
content.to_i
|
|
104
|
+
end
|
|
105
|
+
when 'TrueClass'
|
|
106
|
+
true
|
|
107
|
+
when 'FalseClass'
|
|
108
|
+
false
|
|
109
|
+
when 'NilClass'
|
|
110
|
+
nil
|
|
111
|
+
else
|
|
112
|
+
fail "Unknown type found in model: #{iter.type}"
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Convert the Ruby data structure _data_ into tree model data for Gtk and
|
|
117
|
+
# returns the whole model. If the parameter _model_ wasn't given a new
|
|
118
|
+
# Gtk::TreeStore is created as the model. The _parent_ parameter specifies
|
|
119
|
+
# the parent node (iter, Gtk:TreeIter instance) to which the data is
|
|
120
|
+
# appended, alternativeley the result of the yielded block is used as iter.
|
|
121
|
+
def Editor.data2model(data, model = nil, parent = nil)
|
|
122
|
+
model ||= TreeStore.new(Gdk::Pixbuf, String, String)
|
|
123
|
+
iter = if block_given?
|
|
124
|
+
yield model
|
|
125
|
+
else
|
|
126
|
+
model.append(parent)
|
|
127
|
+
end
|
|
128
|
+
case data
|
|
129
|
+
when Hash
|
|
130
|
+
iter.type = 'Hash'
|
|
131
|
+
data.sort.each do |key, value|
|
|
132
|
+
pair_iter = model.append(iter)
|
|
133
|
+
pair_iter.type = 'Key'
|
|
134
|
+
pair_iter.content = key.to_s
|
|
135
|
+
Editor.data2model(value, model, pair_iter)
|
|
136
|
+
end
|
|
137
|
+
when Array
|
|
138
|
+
iter.type = 'Array'
|
|
139
|
+
data.each do |value|
|
|
140
|
+
Editor.data2model(value, model, iter)
|
|
141
|
+
end
|
|
142
|
+
when Numeric
|
|
143
|
+
iter.type = 'Numeric'
|
|
144
|
+
iter.content = data.to_s
|
|
145
|
+
when String, true, false, nil
|
|
146
|
+
iter.type = data.class.name
|
|
147
|
+
iter.content = data.nil? ? 'null' : data.to_s
|
|
148
|
+
else
|
|
149
|
+
iter.type = 'String'
|
|
150
|
+
iter.content = data.to_s
|
|
151
|
+
end
|
|
152
|
+
model
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# The Gtk::TreeIter class is reopened and some auxiliary methods are added.
|
|
156
|
+
class Gtk::TreeIter
|
|
157
|
+
include Enumerable
|
|
158
|
+
|
|
159
|
+
# Traverse each of this Gtk::TreeIter instance's children
|
|
160
|
+
# and yield to them.
|
|
161
|
+
def each
|
|
162
|
+
n_children.times { |i| yield nth_child(i) }
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Recursively traverse all nodes of this Gtk::TreeIter's subtree
|
|
166
|
+
# (including self) and yield to them.
|
|
167
|
+
def recursive_each(&block)
|
|
168
|
+
yield self
|
|
169
|
+
each do |i|
|
|
170
|
+
i.recursive_each(&block)
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Remove the subtree of this Gtk::TreeIter instance from the
|
|
175
|
+
# model _model_.
|
|
176
|
+
def remove_subtree(model)
|
|
177
|
+
while current = first_child
|
|
178
|
+
model.remove(current)
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Returns the type of this node.
|
|
183
|
+
def type
|
|
184
|
+
self[TYPE_COL]
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Sets the type of this node to _value_. This implies setting
|
|
188
|
+
# the respective icon accordingly.
|
|
189
|
+
def type=(value)
|
|
190
|
+
self[TYPE_COL] = value
|
|
191
|
+
self[ICON_COL] = Editor.fetch_icon(value)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Returns the content of this node.
|
|
195
|
+
def content
|
|
196
|
+
self[CONTENT_COL]
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Sets the content of this node to _value_.
|
|
200
|
+
def content=(value)
|
|
201
|
+
self[CONTENT_COL] = value
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# This module bundles some method, that can be used to create a menu. It
|
|
206
|
+
# should be included into the class in question.
|
|
207
|
+
module MenuExtension
|
|
208
|
+
include Gtk
|
|
209
|
+
|
|
210
|
+
# Creates a Menu, that includes MenuExtension. _treeview_ is the
|
|
211
|
+
# Gtk::TreeView, on which it operates.
|
|
212
|
+
def initialize(treeview)
|
|
213
|
+
@treeview = treeview
|
|
214
|
+
@menu = Menu.new
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# Returns the Gtk::TreeView of this menu.
|
|
218
|
+
attr_reader :treeview
|
|
219
|
+
|
|
220
|
+
# Returns the menu.
|
|
221
|
+
attr_reader :menu
|
|
222
|
+
|
|
223
|
+
# Adds a Gtk::SeparatorMenuItem to this instance's #menu.
|
|
224
|
+
def add_separator
|
|
225
|
+
menu.append SeparatorMenuItem.new
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# Adds a Gtk::MenuItem to this instance's #menu. _label_ is the label
|
|
229
|
+
# string, _klass_ is the item type, and _callback_ is the procedure, that
|
|
230
|
+
# is called if the _item_ is activated.
|
|
231
|
+
def add_item(label, keyval = nil, klass = MenuItem, &callback)
|
|
232
|
+
label = "#{label} (C-#{keyval.chr})" if keyval
|
|
233
|
+
item = klass.new(label)
|
|
234
|
+
item.signal_connect(:activate, &callback)
|
|
235
|
+
if keyval
|
|
236
|
+
self.signal_connect(:'key-press-event') do |item, event|
|
|
237
|
+
if event.state & Gdk::Window::ModifierType::CONTROL_MASK != 0 and
|
|
238
|
+
event.keyval == keyval
|
|
239
|
+
callback.call item
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
menu.append item
|
|
244
|
+
item
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
# This method should be implemented in subclasses to create the #menu of
|
|
248
|
+
# this instance. It has to be called after an instance of this class is
|
|
249
|
+
# created, to build the menu.
|
|
250
|
+
def create
|
|
251
|
+
raise NotImplementedError
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def method_missing(*a, &b)
|
|
255
|
+
treeview.__send__(*a, &b)
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
# This class creates the popup menu, that opens when clicking onto the
|
|
260
|
+
# treeview.
|
|
261
|
+
class PopUpMenu
|
|
262
|
+
include MenuExtension
|
|
263
|
+
|
|
264
|
+
# Change the type or content of the selected node.
|
|
265
|
+
def change_node(item)
|
|
266
|
+
if current = selection.selected
|
|
267
|
+
parent = current.parent
|
|
268
|
+
old_type, old_content = current.type, current.content
|
|
269
|
+
if ALL_TYPES.include?(old_type)
|
|
270
|
+
@clipboard_data = Editor.model2data(current)
|
|
271
|
+
type, content = ask_for_element(parent, current.type,
|
|
272
|
+
current.content)
|
|
273
|
+
if type
|
|
274
|
+
current.type, current.content = type, content
|
|
275
|
+
current.remove_subtree(model)
|
|
276
|
+
toplevel.display_status("Changed a node in tree.")
|
|
277
|
+
window.change
|
|
278
|
+
end
|
|
279
|
+
else
|
|
280
|
+
toplevel.display_status(
|
|
281
|
+
"Cannot change node of type #{old_type} in tree!")
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
# Cut the selected node and its subtree, and save it into the
|
|
287
|
+
# clipboard.
|
|
288
|
+
def cut_node(item)
|
|
289
|
+
if current = selection.selected
|
|
290
|
+
if current and current.type == 'Key'
|
|
291
|
+
@clipboard_data = {
|
|
292
|
+
current.content => Editor.model2data(current.first_child)
|
|
293
|
+
}
|
|
294
|
+
else
|
|
295
|
+
@clipboard_data = Editor.model2data(current)
|
|
296
|
+
end
|
|
297
|
+
model.remove(current)
|
|
298
|
+
window.change
|
|
299
|
+
toplevel.display_status("Cut a node from tree.")
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
# Copy the selected node and its subtree, and save it into the
|
|
304
|
+
# clipboard.
|
|
305
|
+
def copy_node(item)
|
|
306
|
+
if current = selection.selected
|
|
307
|
+
if current and current.type == 'Key'
|
|
308
|
+
@clipboard_data = {
|
|
309
|
+
current.content => Editor.model2data(current.first_child)
|
|
310
|
+
}
|
|
311
|
+
else
|
|
312
|
+
@clipboard_data = Editor.model2data(current)
|
|
313
|
+
end
|
|
314
|
+
window.change
|
|
315
|
+
toplevel.display_status("Copied a node from tree.")
|
|
316
|
+
end
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
# Paste the data in the clipboard into the selected Array or Hash by
|
|
320
|
+
# appending it.
|
|
321
|
+
def paste_node_appending(item)
|
|
322
|
+
if current = selection.selected
|
|
323
|
+
if @clipboard_data
|
|
324
|
+
case current.type
|
|
325
|
+
when 'Array'
|
|
326
|
+
Editor.data2model(@clipboard_data, model, current)
|
|
327
|
+
expand_collapse(current)
|
|
328
|
+
when 'Hash'
|
|
329
|
+
if @clipboard_data.is_a? Hash
|
|
330
|
+
parent = current.parent
|
|
331
|
+
hash = Editor.model2data(current)
|
|
332
|
+
model.remove(current)
|
|
333
|
+
hash.update(@clipboard_data)
|
|
334
|
+
Editor.data2model(hash, model, parent)
|
|
335
|
+
if parent
|
|
336
|
+
expand_collapse(parent)
|
|
337
|
+
elsif @expanded
|
|
338
|
+
expand_all
|
|
339
|
+
end
|
|
340
|
+
window.change
|
|
341
|
+
else
|
|
342
|
+
toplevel.display_status(
|
|
343
|
+
"Cannot paste non-#{current.type} data into '#{current.type}'!")
|
|
344
|
+
end
|
|
345
|
+
else
|
|
346
|
+
toplevel.display_status(
|
|
347
|
+
"Cannot paste node below '#{current.type}'!")
|
|
348
|
+
end
|
|
349
|
+
else
|
|
350
|
+
toplevel.display_status("Nothing to paste in clipboard!")
|
|
351
|
+
end
|
|
352
|
+
else
|
|
353
|
+
toplevel.display_status("Append a node into the root first!")
|
|
354
|
+
end
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
# Paste the data in the clipboard into the selected Array inserting it
|
|
358
|
+
# before the selected element.
|
|
359
|
+
def paste_node_inserting_before(item)
|
|
360
|
+
if current = selection.selected
|
|
361
|
+
if @clipboard_data
|
|
362
|
+
parent = current.parent or return
|
|
363
|
+
parent_type = parent.type
|
|
364
|
+
if parent_type == 'Array'
|
|
365
|
+
selected_index = parent.each_with_index do |c, i|
|
|
366
|
+
break i if c == current
|
|
367
|
+
end
|
|
368
|
+
Editor.data2model(@clipboard_data, model, parent) do |m|
|
|
369
|
+
m.insert_before(parent, current)
|
|
370
|
+
end
|
|
371
|
+
expand_collapse(current)
|
|
372
|
+
toplevel.display_status("Inserted an element to " +
|
|
373
|
+
"'#{parent_type}' before index #{selected_index}.")
|
|
374
|
+
window.change
|
|
375
|
+
else
|
|
376
|
+
toplevel.display_status(
|
|
377
|
+
"Cannot insert node below '#{parent_type}'!")
|
|
378
|
+
end
|
|
379
|
+
else
|
|
380
|
+
toplevel.display_status("Nothing to paste in clipboard!")
|
|
381
|
+
end
|
|
382
|
+
else
|
|
383
|
+
toplevel.display_status("Append a node into the root first!")
|
|
384
|
+
end
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
# Append a new node to the selected Hash or Array.
|
|
388
|
+
def append_new_node(item)
|
|
389
|
+
if parent = selection.selected
|
|
390
|
+
parent_type = parent.type
|
|
391
|
+
case parent_type
|
|
392
|
+
when 'Hash'
|
|
393
|
+
key, type, content = ask_for_hash_pair(parent)
|
|
394
|
+
key or return
|
|
395
|
+
iter = create_node(parent, 'Key', key)
|
|
396
|
+
iter = create_node(iter, type, content)
|
|
397
|
+
toplevel.display_status(
|
|
398
|
+
"Added a (key, value)-pair to '#{parent_type}'.")
|
|
399
|
+
window.change
|
|
400
|
+
when 'Array'
|
|
401
|
+
type, content = ask_for_element(parent)
|
|
402
|
+
type or return
|
|
403
|
+
iter = create_node(parent, type, content)
|
|
404
|
+
window.change
|
|
405
|
+
toplevel.display_status("Appendend an element to '#{parent_type}'.")
|
|
406
|
+
else
|
|
407
|
+
toplevel.display_status("Cannot append to '#{parent_type}'!")
|
|
408
|
+
end
|
|
409
|
+
else
|
|
410
|
+
type, content = ask_for_element
|
|
411
|
+
type or return
|
|
412
|
+
iter = create_node(nil, type, content)
|
|
413
|
+
window.change
|
|
414
|
+
end
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
# Insert a new node into an Array before the selected element.
|
|
418
|
+
def insert_new_node(item)
|
|
419
|
+
if current = selection.selected
|
|
420
|
+
parent = current.parent or return
|
|
421
|
+
parent_parent = parent.parent
|
|
422
|
+
parent_type = parent.type
|
|
423
|
+
if parent_type == 'Array'
|
|
424
|
+
selected_index = parent.each_with_index do |c, i|
|
|
425
|
+
break i if c == current
|
|
426
|
+
end
|
|
427
|
+
type, content = ask_for_element(parent)
|
|
428
|
+
type or return
|
|
429
|
+
iter = model.insert_before(parent, current)
|
|
430
|
+
iter.type, iter.content = type, content
|
|
431
|
+
toplevel.display_status("Inserted an element to " +
|
|
432
|
+
"'#{parent_type}' before index #{selected_index}.")
|
|
433
|
+
window.change
|
|
434
|
+
else
|
|
435
|
+
toplevel.display_status(
|
|
436
|
+
"Cannot insert node below '#{parent_type}'!")
|
|
437
|
+
end
|
|
438
|
+
else
|
|
439
|
+
toplevel.display_status("Append a node into the root first!")
|
|
440
|
+
end
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
# Recursively collapse/expand a subtree starting from the selected node.
|
|
444
|
+
def collapse_expand(item)
|
|
445
|
+
if current = selection.selected
|
|
446
|
+
if row_expanded?(current.path)
|
|
447
|
+
collapse_row(current.path)
|
|
448
|
+
else
|
|
449
|
+
expand_row(current.path, true)
|
|
450
|
+
end
|
|
451
|
+
else
|
|
452
|
+
toplevel.display_status("Append a node into the root first!")
|
|
453
|
+
end
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
# Create the menu.
|
|
457
|
+
def create
|
|
458
|
+
add_item("Change node", ?n, &method(:change_node))
|
|
459
|
+
add_separator
|
|
460
|
+
add_item("Cut node", ?X, &method(:cut_node))
|
|
461
|
+
add_item("Copy node", ?C, &method(:copy_node))
|
|
462
|
+
add_item("Paste node (appending)", ?A, &method(:paste_node_appending))
|
|
463
|
+
add_item("Paste node (inserting before)", ?I,
|
|
464
|
+
&method(:paste_node_inserting_before))
|
|
465
|
+
add_separator
|
|
466
|
+
add_item("Append new node", ?a, &method(:append_new_node))
|
|
467
|
+
add_item("Insert new node before", ?i, &method(:insert_new_node))
|
|
468
|
+
add_separator
|
|
469
|
+
add_item("Collapse/Expand node (recursively)", ?e,
|
|
470
|
+
&method(:collapse_expand))
|
|
471
|
+
|
|
472
|
+
menu.show_all
|
|
473
|
+
signal_connect(:button_press_event) do |widget, event|
|
|
474
|
+
if event.kind_of? Gdk::EventButton and event.button == 3
|
|
475
|
+
menu.popup(nil, nil, event.button, event.time)
|
|
476
|
+
end
|
|
477
|
+
end
|
|
478
|
+
signal_connect(:popup_menu) do
|
|
479
|
+
menu.popup(nil, nil, 0, Gdk::Event::CURRENT_TIME)
|
|
480
|
+
end
|
|
481
|
+
end
|
|
482
|
+
end
|
|
483
|
+
|
|
484
|
+
# This class creates the File pulldown menu.
|
|
485
|
+
class FileMenu
|
|
486
|
+
include MenuExtension
|
|
487
|
+
|
|
488
|
+
# Clear the model and filename, but ask to save the JSON document, if
|
|
489
|
+
# unsaved changes have occured.
|
|
490
|
+
def new(item)
|
|
491
|
+
window.clear
|
|
492
|
+
end
|
|
493
|
+
|
|
494
|
+
# Open a file and load it into the editor. Ask to save the JSON document
|
|
495
|
+
# first, if unsaved changes have occured.
|
|
496
|
+
def open(item)
|
|
497
|
+
window.file_open
|
|
498
|
+
end
|
|
499
|
+
|
|
500
|
+
def open_location(item)
|
|
501
|
+
window.location_open
|
|
502
|
+
end
|
|
503
|
+
|
|
504
|
+
# Revert the current JSON document in the editor to the saved version.
|
|
505
|
+
def revert(item)
|
|
506
|
+
window.instance_eval do
|
|
507
|
+
@filename and file_open(@filename)
|
|
508
|
+
end
|
|
509
|
+
end
|
|
510
|
+
|
|
511
|
+
# Save the current JSON document.
|
|
512
|
+
def save(item)
|
|
513
|
+
window.file_save
|
|
514
|
+
end
|
|
515
|
+
|
|
516
|
+
# Save the current JSON document under the given filename.
|
|
517
|
+
def save_as(item)
|
|
518
|
+
window.file_save_as
|
|
519
|
+
end
|
|
520
|
+
|
|
521
|
+
# Quit the editor, after asking to save any unsaved changes first.
|
|
522
|
+
def quit(item)
|
|
523
|
+
window.quit
|
|
524
|
+
end
|
|
525
|
+
|
|
526
|
+
# Create the menu.
|
|
527
|
+
def create
|
|
528
|
+
title = MenuItem.new('File')
|
|
529
|
+
title.submenu = menu
|
|
530
|
+
add_item('New', &method(:new))
|
|
531
|
+
add_item('Open', ?o, &method(:open))
|
|
532
|
+
add_item('Open location', ?l, &method(:open_location))
|
|
533
|
+
add_item('Revert', &method(:revert))
|
|
534
|
+
add_separator
|
|
535
|
+
add_item('Save', ?s, &method(:save))
|
|
536
|
+
add_item('Save As', ?S, &method(:save_as))
|
|
537
|
+
add_separator
|
|
538
|
+
add_item('Quit', ?q, &method(:quit))
|
|
539
|
+
title
|
|
540
|
+
end
|
|
541
|
+
end
|
|
542
|
+
|
|
543
|
+
# This class creates the Edit pulldown menu.
|
|
544
|
+
class EditMenu
|
|
545
|
+
include MenuExtension
|
|
546
|
+
|
|
547
|
+
# Copy data from model into primary clipboard.
|
|
548
|
+
def copy(item)
|
|
549
|
+
data = Editor.model2data(model.iter_first)
|
|
550
|
+
json = JSON.pretty_generate(data, :max_nesting => false)
|
|
551
|
+
c = Gtk::Clipboard.get(Gdk::Selection::PRIMARY)
|
|
552
|
+
c.text = json
|
|
553
|
+
end
|
|
554
|
+
|
|
555
|
+
# Copy json text from primary clipboard into model.
|
|
556
|
+
def paste(item)
|
|
557
|
+
c = Gtk::Clipboard.get(Gdk::Selection::PRIMARY)
|
|
558
|
+
if json = c.wait_for_text
|
|
559
|
+
window.ask_save if @changed
|
|
560
|
+
begin
|
|
561
|
+
window.edit json
|
|
562
|
+
rescue JSON::ParserError
|
|
563
|
+
window.clear
|
|
564
|
+
end
|
|
565
|
+
end
|
|
566
|
+
end
|
|
567
|
+
|
|
568
|
+
# Find a string in all nodes' contents and select the found node in the
|
|
569
|
+
# treeview.
|
|
570
|
+
def find(item)
|
|
571
|
+
@search = ask_for_find_term(@search) or return
|
|
572
|
+
iter = model.get_iter('0') or return
|
|
573
|
+
iter.recursive_each do |i|
|
|
574
|
+
if @iter
|
|
575
|
+
if @iter != i
|
|
576
|
+
next
|
|
577
|
+
else
|
|
578
|
+
@iter = nil
|
|
579
|
+
next
|
|
580
|
+
end
|
|
581
|
+
elsif @search.match(i[CONTENT_COL])
|
|
582
|
+
set_cursor(i.path, nil, false)
|
|
583
|
+
@iter = i
|
|
584
|
+
break
|
|
585
|
+
end
|
|
586
|
+
end
|
|
587
|
+
end
|
|
588
|
+
|
|
589
|
+
# Repeat the last search given by #find.
|
|
590
|
+
def find_again(item)
|
|
591
|
+
@search or return
|
|
592
|
+
iter = model.get_iter('0')
|
|
593
|
+
iter.recursive_each do |i|
|
|
594
|
+
if @iter
|
|
595
|
+
if @iter != i
|
|
596
|
+
next
|
|
597
|
+
else
|
|
598
|
+
@iter = nil
|
|
599
|
+
next
|
|
600
|
+
end
|
|
601
|
+
elsif @search.match(i[CONTENT_COL])
|
|
602
|
+
set_cursor(i.path, nil, false)
|
|
603
|
+
@iter = i
|
|
604
|
+
break
|
|
605
|
+
end
|
|
606
|
+
end
|
|
607
|
+
end
|
|
608
|
+
|
|
609
|
+
# Sort (Reverse sort) all elements of the selected array by the given
|
|
610
|
+
# expression. _x_ is the element in question.
|
|
611
|
+
def sort(item)
|
|
612
|
+
if current = selection.selected
|
|
613
|
+
if current.type == 'Array'
|
|
614
|
+
parent = current.parent
|
|
615
|
+
ary = Editor.model2data(current)
|
|
616
|
+
order, reverse = ask_for_order
|
|
617
|
+
order or return
|
|
618
|
+
begin
|
|
619
|
+
block = eval "lambda { |x| #{order} }"
|
|
620
|
+
if reverse
|
|
621
|
+
ary.sort! { |a,b| block[b] <=> block[a] }
|
|
622
|
+
else
|
|
623
|
+
ary.sort! { |a,b| block[a] <=> block[b] }
|
|
624
|
+
end
|
|
625
|
+
rescue => e
|
|
626
|
+
Editor.error_dialog(self, "Failed to sort Array with #{order}: #{e}!")
|
|
627
|
+
else
|
|
628
|
+
Editor.data2model(ary, model, parent) do |m|
|
|
629
|
+
m.insert_before(parent, current)
|
|
630
|
+
end
|
|
631
|
+
model.remove(current)
|
|
632
|
+
expand_collapse(parent)
|
|
633
|
+
window.change
|
|
634
|
+
toplevel.display_status("Array has been sorted.")
|
|
635
|
+
end
|
|
636
|
+
else
|
|
637
|
+
toplevel.display_status("Only Array nodes can be sorted!")
|
|
638
|
+
end
|
|
639
|
+
else
|
|
640
|
+
toplevel.display_status("Select an Array to sort first!")
|
|
641
|
+
end
|
|
642
|
+
end
|
|
643
|
+
|
|
644
|
+
# Create the menu.
|
|
645
|
+
def create
|
|
646
|
+
title = MenuItem.new('Edit')
|
|
647
|
+
title.submenu = menu
|
|
648
|
+
add_item('Copy', ?c, &method(:copy))
|
|
649
|
+
add_item('Paste', ?v, &method(:paste))
|
|
650
|
+
add_separator
|
|
651
|
+
add_item('Find', ?f, &method(:find))
|
|
652
|
+
add_item('Find Again', ?g, &method(:find_again))
|
|
653
|
+
add_separator
|
|
654
|
+
add_item('Sort', ?S, &method(:sort))
|
|
655
|
+
title
|
|
656
|
+
end
|
|
657
|
+
end
|
|
658
|
+
|
|
659
|
+
class OptionsMenu
|
|
660
|
+
include MenuExtension
|
|
661
|
+
|
|
662
|
+
# Collapse/Expand all nodes by default.
|
|
663
|
+
def collapsed_nodes(item)
|
|
664
|
+
if expanded
|
|
665
|
+
self.expanded = false
|
|
666
|
+
collapse_all
|
|
667
|
+
else
|
|
668
|
+
self.expanded = true
|
|
669
|
+
expand_all
|
|
670
|
+
end
|
|
671
|
+
end
|
|
672
|
+
|
|
673
|
+
# Toggle pretty saving mode on/off.
|
|
674
|
+
def pretty_saving(item)
|
|
675
|
+
@pretty_item.toggled
|
|
676
|
+
window.change
|
|
677
|
+
end
|
|
678
|
+
|
|
679
|
+
attr_reader :pretty_item
|
|
680
|
+
|
|
681
|
+
# Create the menu.
|
|
682
|
+
def create
|
|
683
|
+
title = MenuItem.new('Options')
|
|
684
|
+
title.submenu = menu
|
|
685
|
+
add_item('Collapsed nodes', nil, CheckMenuItem, &method(:collapsed_nodes))
|
|
686
|
+
@pretty_item = add_item('Pretty saving', nil, CheckMenuItem,
|
|
687
|
+
&method(:pretty_saving))
|
|
688
|
+
@pretty_item.active = true
|
|
689
|
+
window.unchange
|
|
690
|
+
title
|
|
691
|
+
end
|
|
692
|
+
end
|
|
693
|
+
|
|
694
|
+
# This class inherits from Gtk::TreeView, to configure it and to add a lot
|
|
695
|
+
# of behaviour to it.
|
|
696
|
+
class JSONTreeView < Gtk::TreeView
|
|
697
|
+
include Gtk
|
|
698
|
+
|
|
699
|
+
# Creates a JSONTreeView instance, the parameter _window_ is
|
|
700
|
+
# a MainWindow instance and used for self delegation.
|
|
701
|
+
def initialize(window)
|
|
702
|
+
@window = window
|
|
703
|
+
super(TreeStore.new(Gdk::Pixbuf, String, String))
|
|
704
|
+
self.selection.mode = SELECTION_BROWSE
|
|
705
|
+
|
|
706
|
+
@expanded = false
|
|
707
|
+
self.headers_visible = false
|
|
708
|
+
add_columns
|
|
709
|
+
add_popup_menu
|
|
710
|
+
end
|
|
711
|
+
|
|
712
|
+
# Returns the MainWindow instance of this JSONTreeView.
|
|
713
|
+
attr_reader :window
|
|
714
|
+
|
|
715
|
+
# Returns true, if nodes are autoexpanding, false otherwise.
|
|
716
|
+
attr_accessor :expanded
|
|
717
|
+
|
|
718
|
+
private
|
|
719
|
+
|
|
720
|
+
def add_columns
|
|
721
|
+
cell = CellRendererPixbuf.new
|
|
722
|
+
column = TreeViewColumn.new('Icon', cell,
|
|
723
|
+
'pixbuf' => ICON_COL
|
|
724
|
+
)
|
|
725
|
+
append_column(column)
|
|
726
|
+
|
|
727
|
+
cell = CellRendererText.new
|
|
728
|
+
column = TreeViewColumn.new('Type', cell,
|
|
729
|
+
'text' => TYPE_COL
|
|
730
|
+
)
|
|
731
|
+
append_column(column)
|
|
732
|
+
|
|
733
|
+
cell = CellRendererText.new
|
|
734
|
+
cell.editable = true
|
|
735
|
+
column = TreeViewColumn.new('Content', cell,
|
|
736
|
+
'text' => CONTENT_COL
|
|
737
|
+
)
|
|
738
|
+
cell.signal_connect(:edited, &method(:cell_edited))
|
|
739
|
+
append_column(column)
|
|
740
|
+
end
|
|
741
|
+
|
|
742
|
+
def unify_key(iter, key)
|
|
743
|
+
return unless iter.type == 'Key'
|
|
744
|
+
parent = iter.parent
|
|
745
|
+
if parent.any? { |c| c != iter and c.content == key }
|
|
746
|
+
old_key = key
|
|
747
|
+
i = 0
|
|
748
|
+
begin
|
|
749
|
+
key = sprintf("%s.%d", old_key, i += 1)
|
|
750
|
+
end while parent.any? { |c| c != iter and c.content == key }
|
|
751
|
+
end
|
|
752
|
+
iter.content = key
|
|
753
|
+
end
|
|
754
|
+
|
|
755
|
+
def cell_edited(cell, path, value)
|
|
756
|
+
iter = model.get_iter(path)
|
|
757
|
+
case iter.type
|
|
758
|
+
when 'Key'
|
|
759
|
+
unify_key(iter, value)
|
|
760
|
+
toplevel.display_status('Key has been changed.')
|
|
761
|
+
when 'FalseClass'
|
|
762
|
+
value.downcase!
|
|
763
|
+
if value == 'true'
|
|
764
|
+
iter.type, iter.content = 'TrueClass', 'true'
|
|
765
|
+
end
|
|
766
|
+
when 'TrueClass'
|
|
767
|
+
value.downcase!
|
|
768
|
+
if value == 'false'
|
|
769
|
+
iter.type, iter.content = 'FalseClass', 'false'
|
|
770
|
+
end
|
|
771
|
+
when 'Numeric'
|
|
772
|
+
iter.content = (Integer(value) rescue Float(value) rescue 0).to_s
|
|
773
|
+
when 'String'
|
|
774
|
+
iter.content = value
|
|
775
|
+
when 'Hash', 'Array'
|
|
776
|
+
return
|
|
777
|
+
else
|
|
778
|
+
fail "Unknown type found in model: #{iter.type}"
|
|
779
|
+
end
|
|
780
|
+
window.change
|
|
781
|
+
end
|
|
782
|
+
|
|
783
|
+
def configure_value(value, type)
|
|
784
|
+
value.editable = false
|
|
785
|
+
case type
|
|
786
|
+
when 'Array', 'Hash'
|
|
787
|
+
value.text = ''
|
|
788
|
+
when 'TrueClass'
|
|
789
|
+
value.text = 'true'
|
|
790
|
+
when 'FalseClass'
|
|
791
|
+
value.text = 'false'
|
|
792
|
+
when 'NilClass'
|
|
793
|
+
value.text = 'null'
|
|
794
|
+
when 'Numeric', 'String'
|
|
795
|
+
value.text ||= ''
|
|
796
|
+
value.editable = true
|
|
797
|
+
else
|
|
798
|
+
raise ArgumentError, "unknown type '#{type}' encountered"
|
|
799
|
+
end
|
|
800
|
+
end
|
|
801
|
+
|
|
802
|
+
def add_popup_menu
|
|
803
|
+
menu = PopUpMenu.new(self)
|
|
804
|
+
menu.create
|
|
805
|
+
end
|
|
806
|
+
|
|
807
|
+
public
|
|
808
|
+
|
|
809
|
+
# Create a _type_ node with content _content_, and add it to _parent_
|
|
810
|
+
# in the model. If _parent_ is nil, create a new model and put it into
|
|
811
|
+
# the editor treeview.
|
|
812
|
+
def create_node(parent, type, content)
|
|
813
|
+
iter = if parent
|
|
814
|
+
model.append(parent)
|
|
815
|
+
else
|
|
816
|
+
new_model = Editor.data2model(nil)
|
|
817
|
+
toplevel.view_new_model(new_model)
|
|
818
|
+
new_model.iter_first
|
|
819
|
+
end
|
|
820
|
+
iter.type, iter.content = type, content
|
|
821
|
+
expand_collapse(parent) if parent
|
|
822
|
+
iter
|
|
823
|
+
end
|
|
824
|
+
|
|
825
|
+
# Ask for a hash key, value pair to be added to the Hash node _parent_.
|
|
826
|
+
def ask_for_hash_pair(parent)
|
|
827
|
+
key_input = type_input = value_input = nil
|
|
828
|
+
|
|
829
|
+
dialog = Dialog.new("New (key, value) pair for Hash", nil, nil,
|
|
830
|
+
[ Stock::OK, Dialog::RESPONSE_ACCEPT ],
|
|
831
|
+
[ Stock::CANCEL, Dialog::RESPONSE_REJECT ]
|
|
832
|
+
)
|
|
833
|
+
dialog.width_request = 640
|
|
834
|
+
|
|
835
|
+
hbox = HBox.new(false, 5)
|
|
836
|
+
hbox.pack_start(Label.new("Key:"), false)
|
|
837
|
+
hbox.pack_start(key_input = Entry.new)
|
|
838
|
+
key_input.text = @key || ''
|
|
839
|
+
dialog.vbox.pack_start(hbox, false)
|
|
840
|
+
key_input.signal_connect(:activate) do
|
|
841
|
+
if parent.any? { |c| c.content == key_input.text }
|
|
842
|
+
toplevel.display_status('Key already exists in Hash!')
|
|
843
|
+
key_input.text = ''
|
|
844
|
+
else
|
|
845
|
+
toplevel.display_status('Key has been changed.')
|
|
846
|
+
end
|
|
847
|
+
end
|
|
848
|
+
|
|
849
|
+
hbox = HBox.new(false, 5)
|
|
850
|
+
hbox.pack_start(Label.new("Type:"), false)
|
|
851
|
+
hbox.pack_start(type_input = ComboBox.new(true))
|
|
852
|
+
ALL_TYPES.each { |t| type_input.append_text(t) }
|
|
853
|
+
type_input.active = @type || 0
|
|
854
|
+
dialog.vbox.pack_start(hbox, false)
|
|
855
|
+
|
|
856
|
+
type_input.signal_connect(:changed) do
|
|
857
|
+
value_input.editable = false
|
|
858
|
+
case ALL_TYPES[type_input.active]
|
|
859
|
+
when 'Array', 'Hash'
|
|
860
|
+
value_input.text = ''
|
|
861
|
+
when 'TrueClass'
|
|
862
|
+
value_input.text = 'true'
|
|
863
|
+
when 'FalseClass'
|
|
864
|
+
value_input.text = 'false'
|
|
865
|
+
when 'NilClass'
|
|
866
|
+
value_input.text = 'null'
|
|
867
|
+
else
|
|
868
|
+
value_input.text = ''
|
|
869
|
+
value_input.editable = true
|
|
870
|
+
end
|
|
871
|
+
end
|
|
872
|
+
|
|
873
|
+
hbox = HBox.new(false, 5)
|
|
874
|
+
hbox.pack_start(Label.new("Value:"), false)
|
|
875
|
+
hbox.pack_start(value_input = Entry.new)
|
|
876
|
+
value_input.width_chars = 60
|
|
877
|
+
value_input.text = @value || ''
|
|
878
|
+
dialog.vbox.pack_start(hbox, false)
|
|
879
|
+
|
|
880
|
+
dialog.signal_connect(:'key-press-event', &DEFAULT_DIALOG_KEY_PRESS_HANDLER)
|
|
881
|
+
dialog.show_all
|
|
882
|
+
self.focus = dialog
|
|
883
|
+
dialog.run do |response|
|
|
884
|
+
if response == Dialog::RESPONSE_ACCEPT
|
|
885
|
+
@key = key_input.text
|
|
886
|
+
type = ALL_TYPES[@type = type_input.active]
|
|
887
|
+
content = value_input.text
|
|
888
|
+
return @key, type, content
|
|
889
|
+
end
|
|
890
|
+
end
|
|
891
|
+
return
|
|
892
|
+
ensure
|
|
893
|
+
dialog.destroy
|
|
894
|
+
end
|
|
895
|
+
|
|
896
|
+
# Ask for an element to be appended _parent_.
|
|
897
|
+
def ask_for_element(parent = nil, default_type = nil, value_text = @content)
|
|
898
|
+
type_input = value_input = nil
|
|
899
|
+
|
|
900
|
+
dialog = Dialog.new(
|
|
901
|
+
"New element into #{parent ? parent.type : 'root'}",
|
|
902
|
+
nil, nil,
|
|
903
|
+
[ Stock::OK, Dialog::RESPONSE_ACCEPT ],
|
|
904
|
+
[ Stock::CANCEL, Dialog::RESPONSE_REJECT ]
|
|
905
|
+
)
|
|
906
|
+
hbox = HBox.new(false, 5)
|
|
907
|
+
hbox.pack_start(Label.new("Type:"), false)
|
|
908
|
+
hbox.pack_start(type_input = ComboBox.new(true))
|
|
909
|
+
default_active = 0
|
|
910
|
+
types = parent ? ALL_TYPES : CONTAINER_TYPES
|
|
911
|
+
types.each_with_index do |t, i|
|
|
912
|
+
type_input.append_text(t)
|
|
913
|
+
if t == default_type
|
|
914
|
+
default_active = i
|
|
915
|
+
end
|
|
916
|
+
end
|
|
917
|
+
type_input.active = default_active
|
|
918
|
+
dialog.vbox.pack_start(hbox, false)
|
|
919
|
+
type_input.signal_connect(:changed) do
|
|
920
|
+
configure_value(value_input, types[type_input.active])
|
|
921
|
+
end
|
|
922
|
+
|
|
923
|
+
hbox = HBox.new(false, 5)
|
|
924
|
+
hbox.pack_start(Label.new("Value:"), false)
|
|
925
|
+
hbox.pack_start(value_input = Entry.new)
|
|
926
|
+
value_input.width_chars = 60
|
|
927
|
+
value_input.text = value_text if value_text
|
|
928
|
+
configure_value(value_input, types[type_input.active])
|
|
929
|
+
|
|
930
|
+
dialog.vbox.pack_start(hbox, false)
|
|
931
|
+
|
|
932
|
+
dialog.signal_connect(:'key-press-event', &DEFAULT_DIALOG_KEY_PRESS_HANDLER)
|
|
933
|
+
dialog.show_all
|
|
934
|
+
self.focus = dialog
|
|
935
|
+
dialog.run do |response|
|
|
936
|
+
if response == Dialog::RESPONSE_ACCEPT
|
|
937
|
+
type = types[type_input.active]
|
|
938
|
+
@content = case type
|
|
939
|
+
when 'Numeric'
|
|
940
|
+
Integer(value_input.text) rescue Float(value_input.text) rescue 0
|
|
941
|
+
else
|
|
942
|
+
value_input.text
|
|
943
|
+
end.to_s
|
|
944
|
+
return type, @content
|
|
945
|
+
end
|
|
946
|
+
end
|
|
947
|
+
return
|
|
948
|
+
ensure
|
|
949
|
+
dialog.destroy if dialog
|
|
950
|
+
end
|
|
951
|
+
|
|
952
|
+
# Ask for an order criteria for sorting, using _x_ for the element in
|
|
953
|
+
# question. Returns the order criterium, and true/false for reverse
|
|
954
|
+
# sorting.
|
|
955
|
+
def ask_for_order
|
|
956
|
+
dialog = Dialog.new(
|
|
957
|
+
"Give an order criterium for 'x'.",
|
|
958
|
+
nil, nil,
|
|
959
|
+
[ Stock::OK, Dialog::RESPONSE_ACCEPT ],
|
|
960
|
+
[ Stock::CANCEL, Dialog::RESPONSE_REJECT ]
|
|
961
|
+
)
|
|
962
|
+
hbox = HBox.new(false, 5)
|
|
963
|
+
|
|
964
|
+
hbox.pack_start(Label.new("Order:"), false)
|
|
965
|
+
hbox.pack_start(order_input = Entry.new)
|
|
966
|
+
order_input.text = @order || 'x'
|
|
967
|
+
order_input.width_chars = 60
|
|
968
|
+
|
|
969
|
+
hbox.pack_start(reverse_checkbox = CheckButton.new('Reverse'), false)
|
|
970
|
+
|
|
971
|
+
dialog.vbox.pack_start(hbox, false)
|
|
972
|
+
|
|
973
|
+
dialog.signal_connect(:'key-press-event', &DEFAULT_DIALOG_KEY_PRESS_HANDLER)
|
|
974
|
+
dialog.show_all
|
|
975
|
+
self.focus = dialog
|
|
976
|
+
dialog.run do |response|
|
|
977
|
+
if response == Dialog::RESPONSE_ACCEPT
|
|
978
|
+
return @order = order_input.text, reverse_checkbox.active?
|
|
979
|
+
end
|
|
980
|
+
end
|
|
981
|
+
return
|
|
982
|
+
ensure
|
|
983
|
+
dialog.destroy if dialog
|
|
984
|
+
end
|
|
985
|
+
|
|
986
|
+
# Ask for a find term to search for in the tree. Returns the term as a
|
|
987
|
+
# string.
|
|
988
|
+
def ask_for_find_term(search = nil)
|
|
989
|
+
dialog = Dialog.new(
|
|
990
|
+
"Find a node matching regex in tree.",
|
|
991
|
+
nil, nil,
|
|
992
|
+
[ Stock::OK, Dialog::RESPONSE_ACCEPT ],
|
|
993
|
+
[ Stock::CANCEL, Dialog::RESPONSE_REJECT ]
|
|
994
|
+
)
|
|
995
|
+
hbox = HBox.new(false, 5)
|
|
996
|
+
|
|
997
|
+
hbox.pack_start(Label.new("Regex:"), false)
|
|
998
|
+
hbox.pack_start(regex_input = Entry.new)
|
|
999
|
+
hbox.pack_start(icase_checkbox = CheckButton.new('Icase'), false)
|
|
1000
|
+
regex_input.width_chars = 60
|
|
1001
|
+
if search
|
|
1002
|
+
regex_input.text = search.source
|
|
1003
|
+
icase_checkbox.active = search.casefold?
|
|
1004
|
+
end
|
|
1005
|
+
|
|
1006
|
+
dialog.vbox.pack_start(hbox, false)
|
|
1007
|
+
|
|
1008
|
+
dialog.signal_connect(:'key-press-event', &DEFAULT_DIALOG_KEY_PRESS_HANDLER)
|
|
1009
|
+
dialog.show_all
|
|
1010
|
+
self.focus = dialog
|
|
1011
|
+
dialog.run do |response|
|
|
1012
|
+
if response == Dialog::RESPONSE_ACCEPT
|
|
1013
|
+
begin
|
|
1014
|
+
return Regexp.new(regex_input.text, icase_checkbox.active? ? Regexp::IGNORECASE : 0)
|
|
1015
|
+
rescue => e
|
|
1016
|
+
Editor.error_dialog(self, "Evaluation of regex /#{regex_input.text}/ failed: #{e}!")
|
|
1017
|
+
return
|
|
1018
|
+
end
|
|
1019
|
+
end
|
|
1020
|
+
end
|
|
1021
|
+
return
|
|
1022
|
+
ensure
|
|
1023
|
+
dialog.destroy if dialog
|
|
1024
|
+
end
|
|
1025
|
+
|
|
1026
|
+
# Expand or collapse row pointed to by _iter_ according
|
|
1027
|
+
# to the #expanded attribute.
|
|
1028
|
+
def expand_collapse(iter)
|
|
1029
|
+
if expanded
|
|
1030
|
+
expand_row(iter.path, true)
|
|
1031
|
+
else
|
|
1032
|
+
collapse_row(iter.path)
|
|
1033
|
+
end
|
|
1034
|
+
end
|
|
1035
|
+
end
|
|
1036
|
+
|
|
1037
|
+
# The editor main window
|
|
1038
|
+
class MainWindow < Gtk::Window
|
|
1039
|
+
include Gtk
|
|
1040
|
+
|
|
1041
|
+
def initialize(encoding)
|
|
1042
|
+
@changed = false
|
|
1043
|
+
@encoding = encoding
|
|
1044
|
+
super(TOPLEVEL)
|
|
1045
|
+
display_title
|
|
1046
|
+
set_default_size(800, 600)
|
|
1047
|
+
signal_connect(:delete_event) { quit }
|
|
1048
|
+
|
|
1049
|
+
vbox = VBox.new(false, 0)
|
|
1050
|
+
add(vbox)
|
|
1051
|
+
#vbox.border_width = 0
|
|
1052
|
+
|
|
1053
|
+
@treeview = JSONTreeView.new(self)
|
|
1054
|
+
@treeview.signal_connect(:'cursor-changed') do
|
|
1055
|
+
display_status('')
|
|
1056
|
+
end
|
|
1057
|
+
|
|
1058
|
+
menu_bar = create_menu_bar
|
|
1059
|
+
vbox.pack_start(menu_bar, false, false, 0)
|
|
1060
|
+
|
|
1061
|
+
sw = ScrolledWindow.new(nil, nil)
|
|
1062
|
+
sw.shadow_type = SHADOW_ETCHED_IN
|
|
1063
|
+
sw.set_policy(POLICY_AUTOMATIC, POLICY_AUTOMATIC)
|
|
1064
|
+
vbox.pack_start(sw, true, true, 0)
|
|
1065
|
+
sw.add(@treeview)
|
|
1066
|
+
|
|
1067
|
+
@status_bar = Statusbar.new
|
|
1068
|
+
vbox.pack_start(@status_bar, false, false, 0)
|
|
1069
|
+
|
|
1070
|
+
@filename ||= nil
|
|
1071
|
+
if @filename
|
|
1072
|
+
data = read_data(@filename)
|
|
1073
|
+
view_new_model Editor.data2model(data)
|
|
1074
|
+
end
|
|
1075
|
+
|
|
1076
|
+
signal_connect(:button_release_event) do |_,event|
|
|
1077
|
+
if event.button == 2
|
|
1078
|
+
c = Gtk::Clipboard.get(Gdk::Selection::PRIMARY)
|
|
1079
|
+
if url = c.wait_for_text
|
|
1080
|
+
location_open url
|
|
1081
|
+
end
|
|
1082
|
+
false
|
|
1083
|
+
else
|
|
1084
|
+
true
|
|
1085
|
+
end
|
|
1086
|
+
end
|
|
1087
|
+
end
|
|
1088
|
+
|
|
1089
|
+
# Creates the menu bar with the pulldown menus and returns it.
|
|
1090
|
+
def create_menu_bar
|
|
1091
|
+
menu_bar = MenuBar.new
|
|
1092
|
+
@file_menu = FileMenu.new(@treeview)
|
|
1093
|
+
menu_bar.append @file_menu.create
|
|
1094
|
+
@edit_menu = EditMenu.new(@treeview)
|
|
1095
|
+
menu_bar.append @edit_menu.create
|
|
1096
|
+
@options_menu = OptionsMenu.new(@treeview)
|
|
1097
|
+
menu_bar.append @options_menu.create
|
|
1098
|
+
menu_bar
|
|
1099
|
+
end
|
|
1100
|
+
|
|
1101
|
+
# Sets editor status to changed, to indicate that the edited data
|
|
1102
|
+
# containts unsaved changes.
|
|
1103
|
+
def change
|
|
1104
|
+
@changed = true
|
|
1105
|
+
display_title
|
|
1106
|
+
end
|
|
1107
|
+
|
|
1108
|
+
# Sets editor status to unchanged, to indicate that the edited data
|
|
1109
|
+
# doesn't containt unsaved changes.
|
|
1110
|
+
def unchange
|
|
1111
|
+
@changed = false
|
|
1112
|
+
display_title
|
|
1113
|
+
end
|
|
1114
|
+
|
|
1115
|
+
# Puts a new model _model_ into the Gtk::TreeView to be edited.
|
|
1116
|
+
def view_new_model(model)
|
|
1117
|
+
@treeview.model = model
|
|
1118
|
+
@treeview.expanded = true
|
|
1119
|
+
@treeview.expand_all
|
|
1120
|
+
unchange
|
|
1121
|
+
end
|
|
1122
|
+
|
|
1123
|
+
# Displays _text_ in the status bar.
|
|
1124
|
+
def display_status(text)
|
|
1125
|
+
@cid ||= nil
|
|
1126
|
+
@status_bar.pop(@cid) if @cid
|
|
1127
|
+
@cid = @status_bar.get_context_id('dummy')
|
|
1128
|
+
@status_bar.push(@cid, text)
|
|
1129
|
+
end
|
|
1130
|
+
|
|
1131
|
+
# Opens a dialog, asking, if changes should be saved to a file.
|
|
1132
|
+
def ask_save
|
|
1133
|
+
if Editor.question_dialog(self,
|
|
1134
|
+
"Unsaved changes to JSON model. Save?")
|
|
1135
|
+
if @filename
|
|
1136
|
+
file_save
|
|
1137
|
+
else
|
|
1138
|
+
file_save_as
|
|
1139
|
+
end
|
|
1140
|
+
end
|
|
1141
|
+
end
|
|
1142
|
+
|
|
1143
|
+
# Quit this editor, that is, leave this editor's main loop.
|
|
1144
|
+
def quit
|
|
1145
|
+
ask_save if @changed
|
|
1146
|
+
if Gtk.main_level > 0
|
|
1147
|
+
destroy
|
|
1148
|
+
Gtk.main_quit
|
|
1149
|
+
end
|
|
1150
|
+
nil
|
|
1151
|
+
end
|
|
1152
|
+
|
|
1153
|
+
# Display the new title according to the editor's current state.
|
|
1154
|
+
def display_title
|
|
1155
|
+
title = TITLE.dup
|
|
1156
|
+
title << ": #@filename" if @filename
|
|
1157
|
+
title << " *" if @changed
|
|
1158
|
+
self.title = title
|
|
1159
|
+
end
|
|
1160
|
+
|
|
1161
|
+
# Clear the current model, after asking to save all unsaved changes.
|
|
1162
|
+
def clear
|
|
1163
|
+
ask_save if @changed
|
|
1164
|
+
@filename = nil
|
|
1165
|
+
self.view_new_model nil
|
|
1166
|
+
end
|
|
1167
|
+
|
|
1168
|
+
def check_pretty_printed(json)
|
|
1169
|
+
pretty = !!((nl_index = json.index("\n")) && nl_index != json.size - 1)
|
|
1170
|
+
@options_menu.pretty_item.active = pretty
|
|
1171
|
+
end
|
|
1172
|
+
private :check_pretty_printed
|
|
1173
|
+
|
|
1174
|
+
# Open the data at the location _uri_, if given. Otherwise open a dialog
|
|
1175
|
+
# to ask for the _uri_.
|
|
1176
|
+
def location_open(uri = nil)
|
|
1177
|
+
uri = ask_for_location unless uri
|
|
1178
|
+
uri or return
|
|
1179
|
+
ask_save if @changed
|
|
1180
|
+
data = load_location(uri) or return
|
|
1181
|
+
view_new_model Editor.data2model(data)
|
|
1182
|
+
end
|
|
1183
|
+
|
|
1184
|
+
# Open the file _filename_ or call the #select_file method to ask for a
|
|
1185
|
+
# filename.
|
|
1186
|
+
def file_open(filename = nil)
|
|
1187
|
+
filename = select_file('Open as a JSON file') unless filename
|
|
1188
|
+
data = load_file(filename) or return
|
|
1189
|
+
view_new_model Editor.data2model(data)
|
|
1190
|
+
end
|
|
1191
|
+
|
|
1192
|
+
# Edit the string _json_ in the editor.
|
|
1193
|
+
def edit(json)
|
|
1194
|
+
if json.respond_to? :read
|
|
1195
|
+
json = json.read
|
|
1196
|
+
end
|
|
1197
|
+
data = parse_json json
|
|
1198
|
+
view_new_model Editor.data2model(data)
|
|
1199
|
+
end
|
|
1200
|
+
|
|
1201
|
+
# Save the current file.
|
|
1202
|
+
def file_save
|
|
1203
|
+
if @filename
|
|
1204
|
+
store_file(@filename)
|
|
1205
|
+
else
|
|
1206
|
+
file_save_as
|
|
1207
|
+
end
|
|
1208
|
+
end
|
|
1209
|
+
|
|
1210
|
+
# Save the current file as the filename
|
|
1211
|
+
def file_save_as
|
|
1212
|
+
filename = select_file('Save as a JSON file')
|
|
1213
|
+
store_file(filename)
|
|
1214
|
+
end
|
|
1215
|
+
|
|
1216
|
+
# Store the current JSON document to _path_.
|
|
1217
|
+
def store_file(path)
|
|
1218
|
+
if path
|
|
1219
|
+
data = Editor.model2data(@treeview.model.iter_first)
|
|
1220
|
+
File.open(path + '.tmp', 'wb') do |output|
|
|
1221
|
+
data or break
|
|
1222
|
+
if @options_menu.pretty_item.active?
|
|
1223
|
+
output.puts JSON.pretty_generate(data, :max_nesting => false)
|
|
1224
|
+
else
|
|
1225
|
+
output.write JSON.generate(data, :max_nesting => false)
|
|
1226
|
+
end
|
|
1227
|
+
end
|
|
1228
|
+
File.rename path + '.tmp', path
|
|
1229
|
+
@filename = path
|
|
1230
|
+
toplevel.display_status("Saved data to '#@filename'.")
|
|
1231
|
+
unchange
|
|
1232
|
+
end
|
|
1233
|
+
rescue SystemCallError => e
|
|
1234
|
+
Editor.error_dialog(self, "Failed to store JSON file: #{e}!")
|
|
1235
|
+
end
|
|
1236
|
+
|
|
1237
|
+
# Load the file named _filename_ into the editor as a JSON document.
|
|
1238
|
+
def load_file(filename)
|
|
1239
|
+
if filename
|
|
1240
|
+
if File.directory?(filename)
|
|
1241
|
+
Editor.error_dialog(self, "Try to select a JSON file!")
|
|
1242
|
+
nil
|
|
1243
|
+
else
|
|
1244
|
+
@filename = filename
|
|
1245
|
+
if data = read_data(filename)
|
|
1246
|
+
toplevel.display_status("Loaded data from '#@filename'.")
|
|
1247
|
+
end
|
|
1248
|
+
display_title
|
|
1249
|
+
data
|
|
1250
|
+
end
|
|
1251
|
+
end
|
|
1252
|
+
end
|
|
1253
|
+
|
|
1254
|
+
# Load the data at location _uri_ into the editor as a JSON document.
|
|
1255
|
+
def load_location(uri)
|
|
1256
|
+
data = read_data(uri) or return
|
|
1257
|
+
@filename = nil
|
|
1258
|
+
toplevel.display_status("Loaded data from '#{uri}'.")
|
|
1259
|
+
display_title
|
|
1260
|
+
data
|
|
1261
|
+
end
|
|
1262
|
+
|
|
1263
|
+
def parse_json(json)
|
|
1264
|
+
check_pretty_printed(json)
|
|
1265
|
+
if @encoding && !/^utf8$/i.match(@encoding)
|
|
1266
|
+
iconverter = Iconv.new('utf8', @encoding)
|
|
1267
|
+
json = iconverter.iconv(json)
|
|
1268
|
+
end
|
|
1269
|
+
JSON::parse(json, :max_nesting => false, :create_additions => false)
|
|
1270
|
+
end
|
|
1271
|
+
private :parse_json
|
|
1272
|
+
|
|
1273
|
+
# Read a JSON document from the file named _filename_, parse it into a
|
|
1274
|
+
# ruby data structure, and return the data.
|
|
1275
|
+
def read_data(filename)
|
|
1276
|
+
open(filename) do |f|
|
|
1277
|
+
json = f.read
|
|
1278
|
+
return parse_json(json)
|
|
1279
|
+
end
|
|
1280
|
+
rescue => e
|
|
1281
|
+
Editor.error_dialog(self, "Failed to parse JSON file: #{e}!")
|
|
1282
|
+
return
|
|
1283
|
+
end
|
|
1284
|
+
|
|
1285
|
+
# Open a file selecton dialog, displaying _message_, and return the
|
|
1286
|
+
# selected filename or nil, if no file was selected.
|
|
1287
|
+
def select_file(message)
|
|
1288
|
+
filename = nil
|
|
1289
|
+
fs = FileSelection.new(message)
|
|
1290
|
+
fs.set_modal(true)
|
|
1291
|
+
@default_dir = File.join(Dir.pwd, '') unless @default_dir
|
|
1292
|
+
fs.set_filename(@default_dir)
|
|
1293
|
+
fs.set_transient_for(self)
|
|
1294
|
+
fs.signal_connect(:destroy) { Gtk.main_quit }
|
|
1295
|
+
fs.ok_button.signal_connect(:clicked) do
|
|
1296
|
+
filename = fs.filename
|
|
1297
|
+
@default_dir = File.join(File.dirname(filename), '')
|
|
1298
|
+
fs.destroy
|
|
1299
|
+
Gtk.main_quit
|
|
1300
|
+
end
|
|
1301
|
+
fs.cancel_button.signal_connect(:clicked) do
|
|
1302
|
+
fs.destroy
|
|
1303
|
+
Gtk.main_quit
|
|
1304
|
+
end
|
|
1305
|
+
fs.show_all
|
|
1306
|
+
Gtk.main
|
|
1307
|
+
filename
|
|
1308
|
+
end
|
|
1309
|
+
|
|
1310
|
+
# Ask for location URI a to load data from. Returns the URI as a string.
|
|
1311
|
+
def ask_for_location
|
|
1312
|
+
dialog = Dialog.new(
|
|
1313
|
+
"Load data from location...",
|
|
1314
|
+
nil, nil,
|
|
1315
|
+
[ Stock::OK, Dialog::RESPONSE_ACCEPT ],
|
|
1316
|
+
[ Stock::CANCEL, Dialog::RESPONSE_REJECT ]
|
|
1317
|
+
)
|
|
1318
|
+
hbox = HBox.new(false, 5)
|
|
1319
|
+
|
|
1320
|
+
hbox.pack_start(Label.new("Location:"), false)
|
|
1321
|
+
hbox.pack_start(location_input = Entry.new)
|
|
1322
|
+
location_input.width_chars = 60
|
|
1323
|
+
location_input.text = @location || ''
|
|
1324
|
+
|
|
1325
|
+
dialog.vbox.pack_start(hbox, false)
|
|
1326
|
+
|
|
1327
|
+
dialog.signal_connect(:'key-press-event', &DEFAULT_DIALOG_KEY_PRESS_HANDLER)
|
|
1328
|
+
dialog.show_all
|
|
1329
|
+
dialog.run do |response|
|
|
1330
|
+
if response == Dialog::RESPONSE_ACCEPT
|
|
1331
|
+
return @location = location_input.text
|
|
1332
|
+
end
|
|
1333
|
+
end
|
|
1334
|
+
return
|
|
1335
|
+
ensure
|
|
1336
|
+
dialog.destroy if dialog
|
|
1337
|
+
end
|
|
1338
|
+
end
|
|
1339
|
+
|
|
1340
|
+
class << self
|
|
1341
|
+
# Starts a JSON Editor. If a block was given, it yields
|
|
1342
|
+
# to the JSON::Editor::MainWindow instance.
|
|
1343
|
+
def start(encoding = 'utf8') # :yield: window
|
|
1344
|
+
Gtk.init
|
|
1345
|
+
@window = Editor::MainWindow.new(encoding)
|
|
1346
|
+
@window.icon_list = [ Editor.fetch_icon('json') ]
|
|
1347
|
+
yield @window if block_given?
|
|
1348
|
+
@window.show_all
|
|
1349
|
+
Gtk.main
|
|
1350
|
+
end
|
|
1351
|
+
|
|
1352
|
+
# Edit the string _json_ with encoding _encoding_ in the editor.
|
|
1353
|
+
def edit(json, encoding = 'utf8')
|
|
1354
|
+
start(encoding) do |window|
|
|
1355
|
+
window.edit json
|
|
1356
|
+
end
|
|
1357
|
+
end
|
|
1358
|
+
|
|
1359
|
+
attr_reader :window
|
|
1360
|
+
end
|
|
1361
|
+
end
|
|
1362
|
+
end
|