json 1.1.5-x86-linux
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.
Potentially problematic release.
This version of json might be problematic. Click here for more details.
- data/CHANGES +106 -0
- data/GPL +340 -0
- data/README +78 -0
- data/RUBY +58 -0
- data/Rakefile +268 -0
- data/TODO +1 -0
- data/VERSION +1 -0
- data/benchmarks/data-p4-3GHz-ruby18/GeneratorBenchmarkComparison.log +52 -0
- data/benchmarks/data-p4-3GHz-ruby18/GeneratorBenchmarkExt#generator_fast-autocorrelation.dat +1000 -0
- data/benchmarks/data-p4-3GHz-ruby18/GeneratorBenchmarkExt#generator_fast.dat +1001 -0
- data/benchmarks/data-p4-3GHz-ruby18/GeneratorBenchmarkExt#generator_pretty-autocorrelation.dat +900 -0
- data/benchmarks/data-p4-3GHz-ruby18/GeneratorBenchmarkExt#generator_pretty.dat +901 -0
- data/benchmarks/data-p4-3GHz-ruby18/GeneratorBenchmarkExt#generator_safe-autocorrelation.dat +1000 -0
- data/benchmarks/data-p4-3GHz-ruby18/GeneratorBenchmarkExt#generator_safe.dat +1001 -0
- data/benchmarks/data-p4-3GHz-ruby18/GeneratorBenchmarkExt.log +261 -0
- data/benchmarks/data-p4-3GHz-ruby18/GeneratorBenchmarkPure#generator_fast-autocorrelation.dat +1000 -0
- data/benchmarks/data-p4-3GHz-ruby18/GeneratorBenchmarkPure#generator_fast.dat +1001 -0
- data/benchmarks/data-p4-3GHz-ruby18/GeneratorBenchmarkPure#generator_pretty-autocorrelation.dat +1000 -0
- data/benchmarks/data-p4-3GHz-ruby18/GeneratorBenchmarkPure#generator_pretty.dat +1001 -0
- data/benchmarks/data-p4-3GHz-ruby18/GeneratorBenchmarkPure#generator_safe-autocorrelation.dat +1000 -0
- data/benchmarks/data-p4-3GHz-ruby18/GeneratorBenchmarkPure#generator_safe.dat +1001 -0
- data/benchmarks/data-p4-3GHz-ruby18/GeneratorBenchmarkPure.log +262 -0
- data/benchmarks/data-p4-3GHz-ruby18/GeneratorBenchmarkRails#generator-autocorrelation.dat +1000 -0
- data/benchmarks/data-p4-3GHz-ruby18/GeneratorBenchmarkRails#generator.dat +1001 -0
- data/benchmarks/data-p4-3GHz-ruby18/GeneratorBenchmarkRails.log +82 -0
- data/benchmarks/data-p4-3GHz-ruby18/ParserBenchmarkComparison.log +34 -0
- data/benchmarks/data-p4-3GHz-ruby18/ParserBenchmarkExt#parser-autocorrelation.dat +900 -0
- data/benchmarks/data-p4-3GHz-ruby18/ParserBenchmarkExt#parser.dat +901 -0
- data/benchmarks/data-p4-3GHz-ruby18/ParserBenchmarkExt.log +81 -0
- data/benchmarks/data-p4-3GHz-ruby18/ParserBenchmarkPure#parser-autocorrelation.dat +1000 -0
- data/benchmarks/data-p4-3GHz-ruby18/ParserBenchmarkPure#parser.dat +1001 -0
- data/benchmarks/data-p4-3GHz-ruby18/ParserBenchmarkPure.log +82 -0
- data/benchmarks/data-p4-3GHz-ruby18/ParserBenchmarkRails#parser-autocorrelation.dat +1000 -0
- data/benchmarks/data-p4-3GHz-ruby18/ParserBenchmarkRails#parser.dat +1001 -0
- data/benchmarks/data-p4-3GHz-ruby18/ParserBenchmarkRails.log +82 -0
- data/benchmarks/data-p4-3GHz-ruby18/ParserBenchmarkYAML#parser-autocorrelation.dat +1000 -0
- data/benchmarks/data-p4-3GHz-ruby18/ParserBenchmarkYAML#parser.dat +1001 -0
- data/benchmarks/data-p4-3GHz-ruby18/ParserBenchmarkYAML.log +82 -0
- data/benchmarks/generator_benchmark.rb +165 -0
- data/benchmarks/parser_benchmark.rb +197 -0
- data/bin/edit_json.rb +9 -0
- data/bin/prettify_json.rb +75 -0
- data/data/example.json +1 -0
- data/data/index.html +38 -0
- data/data/prototype.js +4184 -0
- data/doc-templates/main.txt +283 -0
- data/ext/json/ext/generator/extconf.rb +11 -0
- data/ext/json/ext/generator/generator.c +919 -0
- data/ext/json/ext/generator/unicode.c +182 -0
- data/ext/json/ext/generator/unicode.h +53 -0
- data/ext/json/ext/parser/extconf.rb +11 -0
- data/ext/json/ext/parser/parser.c +1829 -0
- data/ext/json/ext/parser/parser.rl +686 -0
- data/ext/json/ext/parser/unicode.c +154 -0
- data/ext/json/ext/parser/unicode.h +58 -0
- data/install.rb +26 -0
- data/lib/json.rb +10 -0
- data/lib/json/Array.xpm +21 -0
- data/lib/json/FalseClass.xpm +21 -0
- data/lib/json/Hash.xpm +21 -0
- data/lib/json/Key.xpm +73 -0
- data/lib/json/NilClass.xpm +21 -0
- data/lib/json/Numeric.xpm +28 -0
- data/lib/json/String.xpm +96 -0
- data/lib/json/TrueClass.xpm +21 -0
- data/lib/json/add/core.rb +135 -0
- data/lib/json/add/rails.rb +58 -0
- data/lib/json/common.rb +354 -0
- data/lib/json/editor.rb +1371 -0
- data/lib/json/ext.rb +15 -0
- data/lib/json/ext/generator.so +0 -0
- data/lib/json/ext/parser.so +0 -0
- data/lib/json/json.xpm +1499 -0
- data/lib/json/pure.rb +77 -0
- data/lib/json/pure/generator.rb +430 -0
- data/lib/json/pure/parser.rb +267 -0
- data/lib/json/version.rb +8 -0
- data/tests/fixtures/fail1.json +1 -0
- data/tests/fixtures/fail10.json +1 -0
- data/tests/fixtures/fail11.json +1 -0
- data/tests/fixtures/fail12.json +1 -0
- data/tests/fixtures/fail13.json +1 -0
- data/tests/fixtures/fail14.json +1 -0
- data/tests/fixtures/fail18.json +1 -0
- data/tests/fixtures/fail19.json +1 -0
- data/tests/fixtures/fail2.json +1 -0
- data/tests/fixtures/fail20.json +1 -0
- data/tests/fixtures/fail21.json +1 -0
- data/tests/fixtures/fail22.json +1 -0
- data/tests/fixtures/fail23.json +1 -0
- data/tests/fixtures/fail24.json +1 -0
- data/tests/fixtures/fail25.json +1 -0
- data/tests/fixtures/fail27.json +2 -0
- data/tests/fixtures/fail28.json +2 -0
- data/tests/fixtures/fail3.json +1 -0
- data/tests/fixtures/fail4.json +1 -0
- data/tests/fixtures/fail5.json +1 -0
- data/tests/fixtures/fail6.json +1 -0
- data/tests/fixtures/fail7.json +1 -0
- data/tests/fixtures/fail8.json +1 -0
- data/tests/fixtures/fail9.json +1 -0
- data/tests/fixtures/pass1.json +56 -0
- data/tests/fixtures/pass15.json +1 -0
- data/tests/fixtures/pass16.json +1 -0
- data/tests/fixtures/pass17.json +1 -0
- data/tests/fixtures/pass2.json +1 -0
- data/tests/fixtures/pass26.json +1 -0
- data/tests/fixtures/pass3.json +6 -0
- data/tests/test_json.rb +312 -0
- data/tests/test_json_addition.rb +164 -0
- data/tests/test_json_fixtures.rb +34 -0
- data/tests/test_json_generate.rb +106 -0
- data/tests/test_json_rails.rb +146 -0
- data/tests/test_json_unicode.rb +62 -0
- data/tools/fuzz.rb +139 -0
- data/tools/server.rb +61 -0
- metadata +200 -0
    
        data/lib/json/editor.rb
    ADDED
    
    | @@ -0,0 +1,1371 @@ | |
| 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 =
         | 
| 773 | 
            +
                        if value == 'Infinity'
         | 
| 774 | 
            +
                          value
         | 
| 775 | 
            +
                        else
         | 
| 776 | 
            +
                          (Integer(value) rescue Float(value) rescue 0).to_s
         | 
| 777 | 
            +
                        end
         | 
| 778 | 
            +
                    when 'String'
         | 
| 779 | 
            +
                      iter.content = value
         | 
| 780 | 
            +
                    when 'Hash', 'Array'
         | 
| 781 | 
            +
                      return
         | 
| 782 | 
            +
                    else
         | 
| 783 | 
            +
                      fail "Unknown type found in model: #{iter.type}"
         | 
| 784 | 
            +
                    end
         | 
| 785 | 
            +
                    window.change
         | 
| 786 | 
            +
                  end
         | 
| 787 | 
            +
             | 
| 788 | 
            +
                  def configure_value(value, type)
         | 
| 789 | 
            +
                    value.editable = false
         | 
| 790 | 
            +
                    case type
         | 
| 791 | 
            +
                    when 'Array', 'Hash'
         | 
| 792 | 
            +
                      value.text = ''
         | 
| 793 | 
            +
                    when 'TrueClass'
         | 
| 794 | 
            +
                      value.text = 'true'
         | 
| 795 | 
            +
                    when 'FalseClass'
         | 
| 796 | 
            +
                      value.text = 'false'
         | 
| 797 | 
            +
                    when 'NilClass'
         | 
| 798 | 
            +
                      value.text = 'null'
         | 
| 799 | 
            +
                    when 'Numeric', 'String'
         | 
| 800 | 
            +
                      value.text ||= ''
         | 
| 801 | 
            +
                      value.editable = true
         | 
| 802 | 
            +
                    else
         | 
| 803 | 
            +
                      raise ArgumentError, "unknown type '#{type}' encountered"
         | 
| 804 | 
            +
                    end
         | 
| 805 | 
            +
                  end
         | 
| 806 | 
            +
             | 
| 807 | 
            +
                  def add_popup_menu
         | 
| 808 | 
            +
                    menu = PopUpMenu.new(self)
         | 
| 809 | 
            +
                    menu.create
         | 
| 810 | 
            +
                  end
         | 
| 811 | 
            +
             | 
| 812 | 
            +
                  public
         | 
| 813 | 
            +
             | 
| 814 | 
            +
                  # Create a _type_ node with content _content_, and add it to _parent_
         | 
| 815 | 
            +
                  # in the model. If _parent_ is nil, create a new model and put it into
         | 
| 816 | 
            +
                  # the editor treeview.
         | 
| 817 | 
            +
                  def create_node(parent, type, content)
         | 
| 818 | 
            +
                    iter = if parent
         | 
| 819 | 
            +
                      model.append(parent)
         | 
| 820 | 
            +
                    else
         | 
| 821 | 
            +
                      new_model = Editor.data2model(nil)
         | 
| 822 | 
            +
                      toplevel.view_new_model(new_model)
         | 
| 823 | 
            +
                      new_model.iter_first
         | 
| 824 | 
            +
                    end
         | 
| 825 | 
            +
                    iter.type, iter.content = type, content
         | 
| 826 | 
            +
                    expand_collapse(parent) if parent
         | 
| 827 | 
            +
                    iter
         | 
| 828 | 
            +
                  end
         | 
| 829 | 
            +
             | 
| 830 | 
            +
                  # Ask for a hash key, value pair to be added to the Hash node _parent_.
         | 
| 831 | 
            +
                  def ask_for_hash_pair(parent)
         | 
| 832 | 
            +
                    key_input = type_input = value_input = nil
         | 
| 833 | 
            +
             | 
| 834 | 
            +
                    dialog = Dialog.new("New (key, value) pair for Hash", nil, nil,
         | 
| 835 | 
            +
                      [ Stock::OK, Dialog::RESPONSE_ACCEPT ],
         | 
| 836 | 
            +
                      [ Stock::CANCEL, Dialog::RESPONSE_REJECT ]
         | 
| 837 | 
            +
                    )
         | 
| 838 | 
            +
                    dialog.width_request = 640
         | 
| 839 | 
            +
             | 
| 840 | 
            +
                    hbox = HBox.new(false, 5)
         | 
| 841 | 
            +
                    hbox.pack_start(Label.new("Key:"), false)
         | 
| 842 | 
            +
                    hbox.pack_start(key_input = Entry.new)
         | 
| 843 | 
            +
                    key_input.text = @key || ''
         | 
| 844 | 
            +
                    dialog.vbox.pack_start(hbox, false)
         | 
| 845 | 
            +
                    key_input.signal_connect(:activate) do
         | 
| 846 | 
            +
                      if parent.any? { |c| c.content == key_input.text }
         | 
| 847 | 
            +
                        toplevel.display_status('Key already exists in Hash!')
         | 
| 848 | 
            +
                        key_input.text = ''
         | 
| 849 | 
            +
                      else
         | 
| 850 | 
            +
                        toplevel.display_status('Key has been changed.')
         | 
| 851 | 
            +
                      end
         | 
| 852 | 
            +
                    end
         | 
| 853 | 
            +
             | 
| 854 | 
            +
                    hbox = HBox.new(false, 5)
         | 
| 855 | 
            +
                    hbox.pack_start(Label.new("Type:"), false)
         | 
| 856 | 
            +
                    hbox.pack_start(type_input = ComboBox.new(true))
         | 
| 857 | 
            +
                    ALL_TYPES.each { |t| type_input.append_text(t) }
         | 
| 858 | 
            +
                    type_input.active = @type || 0
         | 
| 859 | 
            +
                    dialog.vbox.pack_start(hbox, false)
         | 
| 860 | 
            +
             | 
| 861 | 
            +
                    type_input.signal_connect(:changed) do
         | 
| 862 | 
            +
                      value_input.editable = false
         | 
| 863 | 
            +
                      case ALL_TYPES[type_input.active]
         | 
| 864 | 
            +
                      when 'Array', 'Hash'
         | 
| 865 | 
            +
                        value_input.text = ''
         | 
| 866 | 
            +
                      when 'TrueClass'
         | 
| 867 | 
            +
                        value_input.text = 'true'
         | 
| 868 | 
            +
                      when 'FalseClass'
         | 
| 869 | 
            +
                        value_input.text = 'false'
         | 
| 870 | 
            +
                      when 'NilClass'
         | 
| 871 | 
            +
                        value_input.text = 'null'
         | 
| 872 | 
            +
                      else
         | 
| 873 | 
            +
                        value_input.text = ''
         | 
| 874 | 
            +
                        value_input.editable = true
         | 
| 875 | 
            +
                      end
         | 
| 876 | 
            +
                    end
         | 
| 877 | 
            +
             | 
| 878 | 
            +
                    hbox = HBox.new(false, 5)
         | 
| 879 | 
            +
                    hbox.pack_start(Label.new("Value:"), false)
         | 
| 880 | 
            +
                    hbox.pack_start(value_input = Entry.new)
         | 
| 881 | 
            +
                    value_input.width_chars = 60
         | 
| 882 | 
            +
                    value_input.text = @value || ''
         | 
| 883 | 
            +
                    dialog.vbox.pack_start(hbox, false)
         | 
| 884 | 
            +
             | 
| 885 | 
            +
                    dialog.signal_connect(:'key-press-event', &DEFAULT_DIALOG_KEY_PRESS_HANDLER)
         | 
| 886 | 
            +
                    dialog.show_all
         | 
| 887 | 
            +
                    self.focus = dialog
         | 
| 888 | 
            +
                    dialog.run do |response| 
         | 
| 889 | 
            +
                      if response == Dialog::RESPONSE_ACCEPT
         | 
| 890 | 
            +
                        @key = key_input.text
         | 
| 891 | 
            +
                        type = ALL_TYPES[@type = type_input.active]
         | 
| 892 | 
            +
                        content = value_input.text
         | 
| 893 | 
            +
                        return @key, type, content
         | 
| 894 | 
            +
                      end
         | 
| 895 | 
            +
                    end
         | 
| 896 | 
            +
                    return
         | 
| 897 | 
            +
                  ensure
         | 
| 898 | 
            +
                    dialog.destroy
         | 
| 899 | 
            +
                  end
         | 
| 900 | 
            +
             | 
| 901 | 
            +
                  # Ask for an element to be appended _parent_.
         | 
| 902 | 
            +
                  def ask_for_element(parent = nil, default_type = nil, value_text = @content)
         | 
| 903 | 
            +
                    type_input = value_input = nil
         | 
| 904 | 
            +
             | 
| 905 | 
            +
                    dialog = Dialog.new(
         | 
| 906 | 
            +
                      "New element into #{parent ? parent.type : 'root'}",
         | 
| 907 | 
            +
                      nil, nil,
         | 
| 908 | 
            +
                      [ Stock::OK, Dialog::RESPONSE_ACCEPT ],
         | 
| 909 | 
            +
                      [ Stock::CANCEL, Dialog::RESPONSE_REJECT ]
         | 
| 910 | 
            +
                    )
         | 
| 911 | 
            +
                    hbox = HBox.new(false, 5)
         | 
| 912 | 
            +
                    hbox.pack_start(Label.new("Type:"), false)
         | 
| 913 | 
            +
                    hbox.pack_start(type_input = ComboBox.new(true))
         | 
| 914 | 
            +
                    default_active = 0
         | 
| 915 | 
            +
                    types = parent ? ALL_TYPES : CONTAINER_TYPES
         | 
| 916 | 
            +
                    types.each_with_index do |t, i|
         | 
| 917 | 
            +
                      type_input.append_text(t)
         | 
| 918 | 
            +
                      if t == default_type
         | 
| 919 | 
            +
                        default_active = i
         | 
| 920 | 
            +
                      end
         | 
| 921 | 
            +
                    end
         | 
| 922 | 
            +
                    type_input.active = default_active
         | 
| 923 | 
            +
                    dialog.vbox.pack_start(hbox, false)
         | 
| 924 | 
            +
                    type_input.signal_connect(:changed) do
         | 
| 925 | 
            +
                      configure_value(value_input, types[type_input.active])
         | 
| 926 | 
            +
                    end
         | 
| 927 | 
            +
             | 
| 928 | 
            +
                    hbox = HBox.new(false, 5)
         | 
| 929 | 
            +
                    hbox.pack_start(Label.new("Value:"), false)
         | 
| 930 | 
            +
                    hbox.pack_start(value_input = Entry.new)
         | 
| 931 | 
            +
                    value_input.width_chars = 60
         | 
| 932 | 
            +
                    value_input.text = value_text if value_text
         | 
| 933 | 
            +
                    configure_value(value_input, types[type_input.active])
         | 
| 934 | 
            +
             | 
| 935 | 
            +
                    dialog.vbox.pack_start(hbox, false)
         | 
| 936 | 
            +
             | 
| 937 | 
            +
                    dialog.signal_connect(:'key-press-event', &DEFAULT_DIALOG_KEY_PRESS_HANDLER)
         | 
| 938 | 
            +
                    dialog.show_all
         | 
| 939 | 
            +
                    self.focus = dialog
         | 
| 940 | 
            +
                    dialog.run do |response| 
         | 
| 941 | 
            +
                      if response == Dialog::RESPONSE_ACCEPT
         | 
| 942 | 
            +
                        type = types[type_input.active]
         | 
| 943 | 
            +
                        @content = case type
         | 
| 944 | 
            +
                        when 'Numeric'
         | 
| 945 | 
            +
                          if (t = value_input.text) == 'Infinity'
         | 
| 946 | 
            +
                            1 / 0.0
         | 
| 947 | 
            +
                          else
         | 
| 948 | 
            +
                            Integer(t) rescue Float(t) rescue 0
         | 
| 949 | 
            +
                          end
         | 
| 950 | 
            +
                        else
         | 
| 951 | 
            +
                          value_input.text
         | 
| 952 | 
            +
                        end.to_s
         | 
| 953 | 
            +
                        return type, @content
         | 
| 954 | 
            +
                      end
         | 
| 955 | 
            +
                    end
         | 
| 956 | 
            +
                    return
         | 
| 957 | 
            +
                  ensure
         | 
| 958 | 
            +
                    dialog.destroy if dialog
         | 
| 959 | 
            +
                  end
         | 
| 960 | 
            +
             | 
| 961 | 
            +
                  # Ask for an order criteria for sorting, using _x_ for the element in
         | 
| 962 | 
            +
                  # question. Returns the order criterium, and true/false for reverse
         | 
| 963 | 
            +
                  # sorting.
         | 
| 964 | 
            +
                  def ask_for_order
         | 
| 965 | 
            +
                    dialog = Dialog.new(
         | 
| 966 | 
            +
                      "Give an order criterium for 'x'.",
         | 
| 967 | 
            +
                      nil, nil,
         | 
| 968 | 
            +
                      [ Stock::OK, Dialog::RESPONSE_ACCEPT ],
         | 
| 969 | 
            +
                      [ Stock::CANCEL, Dialog::RESPONSE_REJECT ]
         | 
| 970 | 
            +
                    )
         | 
| 971 | 
            +
                    hbox = HBox.new(false, 5)
         | 
| 972 | 
            +
             | 
| 973 | 
            +
                    hbox.pack_start(Label.new("Order:"), false)
         | 
| 974 | 
            +
                    hbox.pack_start(order_input = Entry.new)
         | 
| 975 | 
            +
                    order_input.text = @order || 'x'
         | 
| 976 | 
            +
                    order_input.width_chars = 60
         | 
| 977 | 
            +
             | 
| 978 | 
            +
                    hbox.pack_start(reverse_checkbox = CheckButton.new('Reverse'), false)
         | 
| 979 | 
            +
             | 
| 980 | 
            +
                    dialog.vbox.pack_start(hbox, false)
         | 
| 981 | 
            +
             | 
| 982 | 
            +
                    dialog.signal_connect(:'key-press-event', &DEFAULT_DIALOG_KEY_PRESS_HANDLER)
         | 
| 983 | 
            +
                    dialog.show_all
         | 
| 984 | 
            +
                    self.focus = dialog
         | 
| 985 | 
            +
                    dialog.run do |response| 
         | 
| 986 | 
            +
                      if response == Dialog::RESPONSE_ACCEPT
         | 
| 987 | 
            +
                        return @order = order_input.text, reverse_checkbox.active?
         | 
| 988 | 
            +
                      end
         | 
| 989 | 
            +
                    end
         | 
| 990 | 
            +
                    return
         | 
| 991 | 
            +
                  ensure
         | 
| 992 | 
            +
                    dialog.destroy if dialog
         | 
| 993 | 
            +
                  end
         | 
| 994 | 
            +
             | 
| 995 | 
            +
                  # Ask for a find term to search for in the tree. Returns the term as a
         | 
| 996 | 
            +
                  # string.
         | 
| 997 | 
            +
                  def ask_for_find_term(search = nil)
         | 
| 998 | 
            +
                    dialog = Dialog.new(
         | 
| 999 | 
            +
                      "Find a node matching regex in tree.",
         | 
| 1000 | 
            +
                      nil, nil,
         | 
| 1001 | 
            +
                      [ Stock::OK, Dialog::RESPONSE_ACCEPT ],
         | 
| 1002 | 
            +
                      [ Stock::CANCEL, Dialog::RESPONSE_REJECT ]
         | 
| 1003 | 
            +
                    )
         | 
| 1004 | 
            +
                    hbox = HBox.new(false, 5)
         | 
| 1005 | 
            +
             | 
| 1006 | 
            +
                    hbox.pack_start(Label.new("Regex:"), false)
         | 
| 1007 | 
            +
                    hbox.pack_start(regex_input = Entry.new)
         | 
| 1008 | 
            +
                    hbox.pack_start(icase_checkbox = CheckButton.new('Icase'), false)
         | 
| 1009 | 
            +
                    regex_input.width_chars = 60
         | 
| 1010 | 
            +
                    if search
         | 
| 1011 | 
            +
                      regex_input.text = search.source
         | 
| 1012 | 
            +
                      icase_checkbox.active = search.casefold?
         | 
| 1013 | 
            +
                    end
         | 
| 1014 | 
            +
             | 
| 1015 | 
            +
                    dialog.vbox.pack_start(hbox, false)
         | 
| 1016 | 
            +
             | 
| 1017 | 
            +
                    dialog.signal_connect(:'key-press-event', &DEFAULT_DIALOG_KEY_PRESS_HANDLER)
         | 
| 1018 | 
            +
                    dialog.show_all
         | 
| 1019 | 
            +
                    self.focus = dialog
         | 
| 1020 | 
            +
                    dialog.run do |response| 
         | 
| 1021 | 
            +
                      if response == Dialog::RESPONSE_ACCEPT
         | 
| 1022 | 
            +
                        begin
         | 
| 1023 | 
            +
                          return Regexp.new(regex_input.text, icase_checkbox.active? ? Regexp::IGNORECASE : 0)
         | 
| 1024 | 
            +
                        rescue => e
         | 
| 1025 | 
            +
                          Editor.error_dialog(self, "Evaluation of regex /#{regex_input.text}/ failed: #{e}!")
         | 
| 1026 | 
            +
                          return
         | 
| 1027 | 
            +
                        end
         | 
| 1028 | 
            +
                      end
         | 
| 1029 | 
            +
                    end
         | 
| 1030 | 
            +
                    return
         | 
| 1031 | 
            +
                  ensure
         | 
| 1032 | 
            +
                    dialog.destroy if dialog
         | 
| 1033 | 
            +
                  end
         | 
| 1034 | 
            +
             | 
| 1035 | 
            +
                  # Expand or collapse row pointed to by _iter_ according
         | 
| 1036 | 
            +
                  # to the #expanded attribute.
         | 
| 1037 | 
            +
                  def expand_collapse(iter)
         | 
| 1038 | 
            +
                    if expanded
         | 
| 1039 | 
            +
                      expand_row(iter.path, true)
         | 
| 1040 | 
            +
                    else
         | 
| 1041 | 
            +
                      collapse_row(iter.path)
         | 
| 1042 | 
            +
                    end
         | 
| 1043 | 
            +
                  end
         | 
| 1044 | 
            +
                end
         | 
| 1045 | 
            +
             | 
| 1046 | 
            +
                # The editor main window
         | 
| 1047 | 
            +
                class MainWindow < Gtk::Window
         | 
| 1048 | 
            +
                  include Gtk
         | 
| 1049 | 
            +
             | 
| 1050 | 
            +
                  def initialize(encoding)
         | 
| 1051 | 
            +
                    @changed  = false
         | 
| 1052 | 
            +
                    @encoding = encoding
         | 
| 1053 | 
            +
                    super(TOPLEVEL)
         | 
| 1054 | 
            +
                    display_title
         | 
| 1055 | 
            +
                    set_default_size(800, 600)
         | 
| 1056 | 
            +
                    signal_connect(:delete_event) { quit }
         | 
| 1057 | 
            +
             | 
| 1058 | 
            +
                    vbox = VBox.new(false, 0)
         | 
| 1059 | 
            +
                    add(vbox)
         | 
| 1060 | 
            +
                    #vbox.border_width = 0
         | 
| 1061 | 
            +
             | 
| 1062 | 
            +
                    @treeview = JSONTreeView.new(self)
         | 
| 1063 | 
            +
                    @treeview.signal_connect(:'cursor-changed') do
         | 
| 1064 | 
            +
                      display_status('')
         | 
| 1065 | 
            +
                    end
         | 
| 1066 | 
            +
             | 
| 1067 | 
            +
                    menu_bar = create_menu_bar
         | 
| 1068 | 
            +
                    vbox.pack_start(menu_bar, false, false, 0)
         | 
| 1069 | 
            +
             | 
| 1070 | 
            +
                    sw = ScrolledWindow.new(nil, nil)
         | 
| 1071 | 
            +
                    sw.shadow_type = SHADOW_ETCHED_IN
         | 
| 1072 | 
            +
                    sw.set_policy(POLICY_AUTOMATIC, POLICY_AUTOMATIC)
         | 
| 1073 | 
            +
                    vbox.pack_start(sw, true, true, 0)
         | 
| 1074 | 
            +
                    sw.add(@treeview)
         | 
| 1075 | 
            +
             | 
| 1076 | 
            +
                    @status_bar = Statusbar.new
         | 
| 1077 | 
            +
                    vbox.pack_start(@status_bar, false, false, 0)
         | 
| 1078 | 
            +
             | 
| 1079 | 
            +
                    @filename ||= nil
         | 
| 1080 | 
            +
                    if @filename
         | 
| 1081 | 
            +
                      data = read_data(@filename)
         | 
| 1082 | 
            +
                      view_new_model Editor.data2model(data)
         | 
| 1083 | 
            +
                    end
         | 
| 1084 | 
            +
             | 
| 1085 | 
            +
                    signal_connect(:button_release_event) do |_,event|
         | 
| 1086 | 
            +
                      if event.button == 2
         | 
| 1087 | 
            +
                        c = Gtk::Clipboard.get(Gdk::Selection::PRIMARY)
         | 
| 1088 | 
            +
                        if url = c.wait_for_text
         | 
| 1089 | 
            +
                          location_open url
         | 
| 1090 | 
            +
                        end
         | 
| 1091 | 
            +
                        false
         | 
| 1092 | 
            +
                      else
         | 
| 1093 | 
            +
                        true
         | 
| 1094 | 
            +
                      end
         | 
| 1095 | 
            +
                    end
         | 
| 1096 | 
            +
                  end
         | 
| 1097 | 
            +
             | 
| 1098 | 
            +
                  # Creates the menu bar with the pulldown menus and returns it.
         | 
| 1099 | 
            +
                  def create_menu_bar
         | 
| 1100 | 
            +
                    menu_bar = MenuBar.new
         | 
| 1101 | 
            +
                    @file_menu = FileMenu.new(@treeview)
         | 
| 1102 | 
            +
                    menu_bar.append @file_menu.create
         | 
| 1103 | 
            +
                    @edit_menu = EditMenu.new(@treeview)
         | 
| 1104 | 
            +
                    menu_bar.append @edit_menu.create
         | 
| 1105 | 
            +
                    @options_menu = OptionsMenu.new(@treeview)
         | 
| 1106 | 
            +
                    menu_bar.append @options_menu.create
         | 
| 1107 | 
            +
                    menu_bar
         | 
| 1108 | 
            +
                  end
         | 
| 1109 | 
            +
             | 
| 1110 | 
            +
                  # Sets editor status to changed, to indicate that the edited data
         | 
| 1111 | 
            +
                  # containts unsaved changes.
         | 
| 1112 | 
            +
                  def change
         | 
| 1113 | 
            +
                    @changed = true
         | 
| 1114 | 
            +
                    display_title
         | 
| 1115 | 
            +
                  end
         | 
| 1116 | 
            +
             | 
| 1117 | 
            +
                  # Sets editor status to unchanged, to indicate that the edited data
         | 
| 1118 | 
            +
                  # doesn't containt unsaved changes.
         | 
| 1119 | 
            +
                  def unchange
         | 
| 1120 | 
            +
                    @changed = false
         | 
| 1121 | 
            +
                    display_title
         | 
| 1122 | 
            +
                  end
         | 
| 1123 | 
            +
             | 
| 1124 | 
            +
                  # Puts a new model _model_ into the Gtk::TreeView to be edited.
         | 
| 1125 | 
            +
                  def view_new_model(model)
         | 
| 1126 | 
            +
                    @treeview.model     = model
         | 
| 1127 | 
            +
                    @treeview.expanded  = true
         | 
| 1128 | 
            +
                    @treeview.expand_all
         | 
| 1129 | 
            +
                    unchange
         | 
| 1130 | 
            +
                  end
         | 
| 1131 | 
            +
             | 
| 1132 | 
            +
                  # Displays _text_ in the status bar.
         | 
| 1133 | 
            +
                  def display_status(text)
         | 
| 1134 | 
            +
                    @cid ||= nil
         | 
| 1135 | 
            +
                    @status_bar.pop(@cid) if @cid
         | 
| 1136 | 
            +
                    @cid = @status_bar.get_context_id('dummy')
         | 
| 1137 | 
            +
                    @status_bar.push(@cid, text)
         | 
| 1138 | 
            +
                  end
         | 
| 1139 | 
            +
             | 
| 1140 | 
            +
                  # Opens a dialog, asking, if changes should be saved to a file.
         | 
| 1141 | 
            +
                  def ask_save
         | 
| 1142 | 
            +
                    if Editor.question_dialog(self,
         | 
| 1143 | 
            +
                      "Unsaved changes to JSON model. Save?")
         | 
| 1144 | 
            +
                      if @filename
         | 
| 1145 | 
            +
                        file_save
         | 
| 1146 | 
            +
                      else
         | 
| 1147 | 
            +
                        file_save_as
         | 
| 1148 | 
            +
                      end
         | 
| 1149 | 
            +
                    end
         | 
| 1150 | 
            +
                  end
         | 
| 1151 | 
            +
             | 
| 1152 | 
            +
                  # Quit this editor, that is, leave this editor's main loop.
         | 
| 1153 | 
            +
                  def quit
         | 
| 1154 | 
            +
                    ask_save if @changed
         | 
| 1155 | 
            +
                    if Gtk.main_level > 0
         | 
| 1156 | 
            +
                      destroy
         | 
| 1157 | 
            +
                      Gtk.main_quit
         | 
| 1158 | 
            +
                    end
         | 
| 1159 | 
            +
                    nil
         | 
| 1160 | 
            +
                  end
         | 
| 1161 | 
            +
             | 
| 1162 | 
            +
                  # Display the new title according to the editor's current state.
         | 
| 1163 | 
            +
                  def display_title
         | 
| 1164 | 
            +
                    title = TITLE.dup
         | 
| 1165 | 
            +
                    title << ": #@filename" if @filename
         | 
| 1166 | 
            +
                    title << " *" if @changed
         | 
| 1167 | 
            +
                    self.title = title
         | 
| 1168 | 
            +
                  end
         | 
| 1169 | 
            +
             | 
| 1170 | 
            +
                  # Clear the current model, after asking to save all unsaved changes.
         | 
| 1171 | 
            +
                  def clear
         | 
| 1172 | 
            +
                    ask_save if @changed
         | 
| 1173 | 
            +
                    @filename = nil
         | 
| 1174 | 
            +
                    self.view_new_model nil
         | 
| 1175 | 
            +
                  end
         | 
| 1176 | 
            +
             | 
| 1177 | 
            +
                  def check_pretty_printed(json)
         | 
| 1178 | 
            +
                    pretty = !!((nl_index = json.index("\n")) && nl_index != json.size - 1)
         | 
| 1179 | 
            +
                    @options_menu.pretty_item.active = pretty
         | 
| 1180 | 
            +
                  end
         | 
| 1181 | 
            +
                  private :check_pretty_printed
         | 
| 1182 | 
            +
             | 
| 1183 | 
            +
                  # Open the data at the location _uri_, if given. Otherwise open a dialog
         | 
| 1184 | 
            +
                  # to ask for the _uri_.
         | 
| 1185 | 
            +
                  def location_open(uri = nil)
         | 
| 1186 | 
            +
                    uri = ask_for_location unless uri
         | 
| 1187 | 
            +
                    uri or return
         | 
| 1188 | 
            +
                    ask_save if @changed
         | 
| 1189 | 
            +
                    data = load_location(uri) or return
         | 
| 1190 | 
            +
                    view_new_model Editor.data2model(data)
         | 
| 1191 | 
            +
                  end
         | 
| 1192 | 
            +
             | 
| 1193 | 
            +
                  # Open the file _filename_ or call the #select_file method to ask for a
         | 
| 1194 | 
            +
                  # filename.
         | 
| 1195 | 
            +
                  def file_open(filename = nil)
         | 
| 1196 | 
            +
                    filename = select_file('Open as a JSON file') unless filename
         | 
| 1197 | 
            +
                    data = load_file(filename) or return
         | 
| 1198 | 
            +
                    view_new_model Editor.data2model(data)
         | 
| 1199 | 
            +
                  end
         | 
| 1200 | 
            +
             | 
| 1201 | 
            +
                  # Edit the string _json_ in the editor.
         | 
| 1202 | 
            +
                  def edit(json)
         | 
| 1203 | 
            +
                    if json.respond_to? :read
         | 
| 1204 | 
            +
                      json = json.read
         | 
| 1205 | 
            +
                    end
         | 
| 1206 | 
            +
                    data = parse_json json
         | 
| 1207 | 
            +
                    view_new_model Editor.data2model(data)
         | 
| 1208 | 
            +
                  end
         | 
| 1209 | 
            +
             | 
| 1210 | 
            +
                  # Save the current file.
         | 
| 1211 | 
            +
                  def file_save
         | 
| 1212 | 
            +
                    if @filename
         | 
| 1213 | 
            +
                      store_file(@filename)
         | 
| 1214 | 
            +
                    else
         | 
| 1215 | 
            +
                      file_save_as
         | 
| 1216 | 
            +
                    end
         | 
| 1217 | 
            +
                  end
         | 
| 1218 | 
            +
             | 
| 1219 | 
            +
                  # Save the current file as the filename 
         | 
| 1220 | 
            +
                  def file_save_as
         | 
| 1221 | 
            +
                    filename = select_file('Save as a JSON file')
         | 
| 1222 | 
            +
                    store_file(filename)
         | 
| 1223 | 
            +
                  end
         | 
| 1224 | 
            +
             | 
| 1225 | 
            +
                  # Store the current JSON document to _path_.
         | 
| 1226 | 
            +
                  def store_file(path)
         | 
| 1227 | 
            +
                    if path
         | 
| 1228 | 
            +
                      data = Editor.model2data(@treeview.model.iter_first)
         | 
| 1229 | 
            +
                      File.open(path + '.tmp', 'wb') do |output|
         | 
| 1230 | 
            +
                        data or break
         | 
| 1231 | 
            +
                        if @options_menu.pretty_item.active?
         | 
| 1232 | 
            +
                          output.puts JSON.pretty_generate(data, :max_nesting => false)
         | 
| 1233 | 
            +
                        else
         | 
| 1234 | 
            +
                          output.write JSON.generate(data, :max_nesting => false)
         | 
| 1235 | 
            +
                        end
         | 
| 1236 | 
            +
                      end
         | 
| 1237 | 
            +
                      File.rename path + '.tmp', path
         | 
| 1238 | 
            +
                      @filename = path
         | 
| 1239 | 
            +
                      toplevel.display_status("Saved data to '#@filename'.")
         | 
| 1240 | 
            +
                      unchange
         | 
| 1241 | 
            +
                    end
         | 
| 1242 | 
            +
                  rescue SystemCallError => e
         | 
| 1243 | 
            +
                    Editor.error_dialog(self, "Failed to store JSON file: #{e}!")
         | 
| 1244 | 
            +
                  end
         | 
| 1245 | 
            +
              
         | 
| 1246 | 
            +
                  # Load the file named _filename_ into the editor as a JSON document.
         | 
| 1247 | 
            +
                  def load_file(filename)
         | 
| 1248 | 
            +
                    if filename
         | 
| 1249 | 
            +
                      if File.directory?(filename)
         | 
| 1250 | 
            +
                        Editor.error_dialog(self, "Try to select a JSON file!")
         | 
| 1251 | 
            +
                        nil
         | 
| 1252 | 
            +
                      else
         | 
| 1253 | 
            +
                        @filename = filename
         | 
| 1254 | 
            +
                        if data = read_data(filename)
         | 
| 1255 | 
            +
                          toplevel.display_status("Loaded data from '#@filename'.")
         | 
| 1256 | 
            +
                        end
         | 
| 1257 | 
            +
                        display_title
         | 
| 1258 | 
            +
                        data
         | 
| 1259 | 
            +
                      end
         | 
| 1260 | 
            +
                    end
         | 
| 1261 | 
            +
                  end
         | 
| 1262 | 
            +
             | 
| 1263 | 
            +
                  # Load the data at location _uri_ into the editor as a JSON document.
         | 
| 1264 | 
            +
                  def load_location(uri)
         | 
| 1265 | 
            +
                    data = read_data(uri) or return
         | 
| 1266 | 
            +
                    @filename = nil
         | 
| 1267 | 
            +
                    toplevel.display_status("Loaded data from '#{uri}'.")
         | 
| 1268 | 
            +
                    display_title
         | 
| 1269 | 
            +
                    data
         | 
| 1270 | 
            +
                  end
         | 
| 1271 | 
            +
             | 
| 1272 | 
            +
                  def parse_json(json)
         | 
| 1273 | 
            +
                    check_pretty_printed(json)
         | 
| 1274 | 
            +
                    if @encoding && !/^utf8$/i.match(@encoding)
         | 
| 1275 | 
            +
                      iconverter = Iconv.new('utf8', @encoding)
         | 
| 1276 | 
            +
                      json = iconverter.iconv(json)
         | 
| 1277 | 
            +
                    end
         | 
| 1278 | 
            +
                    JSON::parse(json, :max_nesting => false, :create_additions => false)
         | 
| 1279 | 
            +
                  end
         | 
| 1280 | 
            +
                  private :parse_json
         | 
| 1281 | 
            +
             | 
| 1282 | 
            +
                  # Read a JSON document from the file named _filename_, parse it into a
         | 
| 1283 | 
            +
                  # ruby data structure, and return the data.
         | 
| 1284 | 
            +
                  def read_data(filename)
         | 
| 1285 | 
            +
                    open(filename) do |f|
         | 
| 1286 | 
            +
                      json = f.read
         | 
| 1287 | 
            +
                      return parse_json(json)
         | 
| 1288 | 
            +
                    end
         | 
| 1289 | 
            +
                  rescue => e
         | 
| 1290 | 
            +
                    Editor.error_dialog(self, "Failed to parse JSON file: #{e}!")
         | 
| 1291 | 
            +
                    return
         | 
| 1292 | 
            +
                  end
         | 
| 1293 | 
            +
             | 
| 1294 | 
            +
                  # Open a file selecton dialog, displaying _message_, and return the
         | 
| 1295 | 
            +
                  # selected filename or nil, if no file was selected.
         | 
| 1296 | 
            +
                  def select_file(message)
         | 
| 1297 | 
            +
                    filename = nil
         | 
| 1298 | 
            +
                    fs = FileSelection.new(message)
         | 
| 1299 | 
            +
                    fs.set_modal(true)
         | 
| 1300 | 
            +
                    @default_dir = File.join(Dir.pwd, '') unless @default_dir
         | 
| 1301 | 
            +
                    fs.set_filename(@default_dir)
         | 
| 1302 | 
            +
                    fs.set_transient_for(self)
         | 
| 1303 | 
            +
                    fs.signal_connect(:destroy) { Gtk.main_quit }
         | 
| 1304 | 
            +
                    fs.ok_button.signal_connect(:clicked) do
         | 
| 1305 | 
            +
                      filename = fs.filename
         | 
| 1306 | 
            +
                      @default_dir = File.join(File.dirname(filename), '')
         | 
| 1307 | 
            +
                      fs.destroy
         | 
| 1308 | 
            +
                      Gtk.main_quit
         | 
| 1309 | 
            +
                    end
         | 
| 1310 | 
            +
                    fs.cancel_button.signal_connect(:clicked) do
         | 
| 1311 | 
            +
                      fs.destroy
         | 
| 1312 | 
            +
                      Gtk.main_quit
         | 
| 1313 | 
            +
                    end
         | 
| 1314 | 
            +
                    fs.show_all
         | 
| 1315 | 
            +
                    Gtk.main
         | 
| 1316 | 
            +
                    filename
         | 
| 1317 | 
            +
                  end
         | 
| 1318 | 
            +
             | 
| 1319 | 
            +
                  # Ask for location URI a to load data from. Returns the URI as a string.
         | 
| 1320 | 
            +
                  def ask_for_location
         | 
| 1321 | 
            +
                    dialog = Dialog.new(
         | 
| 1322 | 
            +
                      "Load data from location...",
         | 
| 1323 | 
            +
                      nil, nil,
         | 
| 1324 | 
            +
                      [ Stock::OK, Dialog::RESPONSE_ACCEPT ],
         | 
| 1325 | 
            +
                      [ Stock::CANCEL, Dialog::RESPONSE_REJECT ]
         | 
| 1326 | 
            +
                    )
         | 
| 1327 | 
            +
                    hbox = HBox.new(false, 5)
         | 
| 1328 | 
            +
             | 
| 1329 | 
            +
                    hbox.pack_start(Label.new("Location:"), false)
         | 
| 1330 | 
            +
                    hbox.pack_start(location_input = Entry.new)
         | 
| 1331 | 
            +
                    location_input.width_chars = 60
         | 
| 1332 | 
            +
                    location_input.text = @location || ''
         | 
| 1333 | 
            +
             | 
| 1334 | 
            +
                    dialog.vbox.pack_start(hbox, false)
         | 
| 1335 | 
            +
             | 
| 1336 | 
            +
                    dialog.signal_connect(:'key-press-event', &DEFAULT_DIALOG_KEY_PRESS_HANDLER)
         | 
| 1337 | 
            +
                    dialog.show_all
         | 
| 1338 | 
            +
                    dialog.run do |response| 
         | 
| 1339 | 
            +
                      if response == Dialog::RESPONSE_ACCEPT
         | 
| 1340 | 
            +
                        return @location = location_input.text
         | 
| 1341 | 
            +
                      end
         | 
| 1342 | 
            +
                    end
         | 
| 1343 | 
            +
                    return
         | 
| 1344 | 
            +
                  ensure
         | 
| 1345 | 
            +
                    dialog.destroy if dialog
         | 
| 1346 | 
            +
                  end
         | 
| 1347 | 
            +
                end
         | 
| 1348 | 
            +
             | 
| 1349 | 
            +
                class << self
         | 
| 1350 | 
            +
                  # Starts a JSON Editor. If a block was given, it yields
         | 
| 1351 | 
            +
                  # to the JSON::Editor::MainWindow instance.
         | 
| 1352 | 
            +
                  def start(encoding = 'utf8') # :yield: window
         | 
| 1353 | 
            +
                    Gtk.init
         | 
| 1354 | 
            +
                    @window = Editor::MainWindow.new(encoding)
         | 
| 1355 | 
            +
                    @window.icon_list = [ Editor.fetch_icon('json') ]
         | 
| 1356 | 
            +
                    yield @window if block_given?
         | 
| 1357 | 
            +
                    @window.show_all
         | 
| 1358 | 
            +
                    Gtk.main
         | 
| 1359 | 
            +
                  end
         | 
| 1360 | 
            +
             | 
| 1361 | 
            +
                  # Edit the string _json_ with encoding _encoding_ in the editor.
         | 
| 1362 | 
            +
                  def edit(json, encoding = 'utf8')
         | 
| 1363 | 
            +
                    start(encoding) do |window|
         | 
| 1364 | 
            +
                      window.edit json
         | 
| 1365 | 
            +
                    end
         | 
| 1366 | 
            +
                  end
         | 
| 1367 | 
            +
             | 
| 1368 | 
            +
                  attr_reader :window
         | 
| 1369 | 
            +
                end
         | 
| 1370 | 
            +
              end
         | 
| 1371 | 
            +
            end
         |