Updated mcollective.init according to OSCI-658
[packages/precise/mcollective.git] / lib / mcollective / vendor / json / lib / json / editor.rb
diff --git a/lib/mcollective/vendor/json/lib/json/editor.rb b/lib/mcollective/vendor/json/lib/json/editor.rb
new file mode 100644 (file)
index 0000000..985a554
--- /dev/null
@@ -0,0 +1,1369 @@
+# To use the GUI JSON editor, start the edit_json.rb executable script. It
+# requires ruby-gtk to be installed.
+
+require 'gtk2'
+require 'json'
+require 'rbconfig'
+require 'open-uri'
+
+module JSON
+  module Editor
+    include Gtk
+
+    # Beginning of the editor window title
+    TITLE                 = 'JSON Editor'.freeze
+
+    # Columns constants
+    ICON_COL, TYPE_COL, CONTENT_COL = 0, 1, 2
+
+    # JSON primitive types (Containers)
+    CONTAINER_TYPES = %w[Array Hash].sort
+    # All JSON primitive types
+    ALL_TYPES = (%w[TrueClass FalseClass Numeric String NilClass] +
+                 CONTAINER_TYPES).sort
+
+    # The Nodes necessary for the tree representation of a JSON document
+    ALL_NODES = (ALL_TYPES + %w[Key]).sort
+
+    DEFAULT_DIALOG_KEY_PRESS_HANDLER = lambda do |dialog, event|
+      case event.keyval
+      when Gdk::Keyval::GDK_Return
+        dialog.response Dialog::RESPONSE_ACCEPT
+      when Gdk::Keyval::GDK_Escape
+        dialog.response Dialog::RESPONSE_REJECT
+      end
+    end
+
+    # Returns the Gdk::Pixbuf of the icon named _name_ from the icon cache.
+    def Editor.fetch_icon(name)
+      @icon_cache ||= {}
+      unless @icon_cache.key?(name)
+        path = File.dirname(__FILE__)
+        @icon_cache[name] = Gdk::Pixbuf.new(File.join(path, name + '.xpm'))
+      end
+     @icon_cache[name]
+    end
+
+    # Opens an error dialog on top of _window_ showing the error message
+    # _text_.
+    def Editor.error_dialog(window, text)
+      dialog = MessageDialog.new(window, Dialog::MODAL,
+        MessageDialog::ERROR,
+        MessageDialog::BUTTONS_CLOSE, text)
+      dialog.show_all
+      dialog.run
+    rescue TypeError
+      dialog = MessageDialog.new(Editor.window, Dialog::MODAL,
+        MessageDialog::ERROR,
+        MessageDialog::BUTTONS_CLOSE, text)
+      dialog.show_all
+      dialog.run
+    ensure
+      dialog.destroy if dialog
+    end
+
+    # Opens a yes/no question dialog on top of _window_ showing the error
+    # message _text_. If yes was answered _true_ is returned, otherwise
+    # _false_.
+    def Editor.question_dialog(window, text)
+      dialog = MessageDialog.new(window, Dialog::MODAL,
+        MessageDialog::QUESTION,
+        MessageDialog::BUTTONS_YES_NO, text)
+      dialog.show_all
+      dialog.run do |response|
+        return Gtk::Dialog::RESPONSE_YES === response
+      end
+    ensure
+      dialog.destroy if dialog
+    end
+
+    # Convert the tree model starting from Gtk::TreeIter _iter_ into a Ruby
+    # data structure and return it.
+    def Editor.model2data(iter)
+      return nil if iter.nil?
+      case iter.type
+      when 'Hash'
+        hash = {}
+        iter.each { |c| hash[c.content] = Editor.model2data(c.first_child) }
+        hash
+      when 'Array'
+        array = Array.new(iter.n_children)
+        iter.each_with_index { |c, i| array[i] = Editor.model2data(c) }
+        array
+      when 'Key'
+        iter.content
+      when 'String'
+        iter.content
+      when 'Numeric'
+        content = iter.content
+        if /\./.match(content)
+          content.to_f
+        else
+          content.to_i
+        end
+      when 'TrueClass'
+        true
+      when 'FalseClass'
+        false
+      when 'NilClass'
+        nil
+      else
+        fail "Unknown type found in model: #{iter.type}"
+      end
+    end
+
+    # Convert the Ruby data structure _data_ into tree model data for Gtk and
+    # returns the whole model. If the parameter _model_ wasn't given a new
+    # Gtk::TreeStore is created as the model. The _parent_ parameter specifies
+    # the parent node (iter, Gtk:TreeIter instance) to which the data is
+    # appended, alternativeley the result of the yielded block is used as iter.
+    def Editor.data2model(data, model = nil, parent = nil)
+      model ||= TreeStore.new(Gdk::Pixbuf, String, String)
+      iter = if block_given?
+        yield model
+      else
+        model.append(parent)
+      end
+      case data
+      when Hash
+        iter.type = 'Hash'
+        data.sort.each do |key, value|
+          pair_iter = model.append(iter)
+          pair_iter.type    = 'Key'
+          pair_iter.content = key.to_s
+          Editor.data2model(value, model, pair_iter)
+        end
+      when Array
+        iter.type = 'Array'
+        data.each do |value|
+          Editor.data2model(value, model, iter)
+        end
+      when Numeric
+        iter.type = 'Numeric'
+        iter.content = data.to_s
+      when String, true, false, nil
+        iter.type    = data.class.name
+        iter.content = data.nil? ? 'null' : data.to_s
+      else
+        iter.type    = 'String'
+        iter.content = data.to_s
+      end
+      model
+    end
+
+    # The Gtk::TreeIter class is reopened and some auxiliary methods are added.
+    class Gtk::TreeIter
+      include Enumerable
+
+      # Traverse each of this Gtk::TreeIter instance's children
+      # and yield to them.
+      def each
+        n_children.times { |i| yield nth_child(i) }
+      end
+
+      # Recursively traverse all nodes of this Gtk::TreeIter's subtree
+      # (including self) and yield to them.
+      def recursive_each(&block)
+        yield self
+        each do |i|
+          i.recursive_each(&block)
+        end
+      end
+
+      # Remove the subtree of this Gtk::TreeIter instance from the
+      # model _model_.
+      def remove_subtree(model)
+        while current = first_child
+          model.remove(current)
+        end
+      end
+
+      # Returns the type of this node.
+      def type
+        self[TYPE_COL]
+      end
+
+      # Sets the type of this node to _value_. This implies setting
+      # the respective icon accordingly.
+      def type=(value)
+        self[TYPE_COL] = value
+        self[ICON_COL] = Editor.fetch_icon(value)
+      end
+
+      # Returns the content of this node.
+      def content
+        self[CONTENT_COL]
+      end
+
+      # Sets the content of this node to _value_.
+      def content=(value)
+        self[CONTENT_COL] = value
+      end
+    end
+
+    # This module bundles some method, that can be used to create a menu. It
+    # should be included into the class in question.
+    module MenuExtension
+      include Gtk
+
+      # Creates a Menu, that includes MenuExtension. _treeview_ is the
+      # Gtk::TreeView, on which it operates.
+      def initialize(treeview)
+        @treeview = treeview
+        @menu = Menu.new
+      end
+
+      # Returns the Gtk::TreeView of this menu.
+      attr_reader :treeview
+
+      # Returns the menu.
+      attr_reader :menu
+
+      # Adds a Gtk::SeparatorMenuItem to this instance's #menu.
+      def add_separator
+        menu.append SeparatorMenuItem.new
+      end
+
+      # Adds a Gtk::MenuItem to this instance's #menu. _label_ is the label
+      # string, _klass_ is the item type, and _callback_ is the procedure, that
+      # is called if the _item_ is activated.
+      def add_item(label, keyval = nil, klass = MenuItem, &callback)
+        label = "#{label} (C-#{keyval.chr})" if keyval
+        item = klass.new(label)
+        item.signal_connect(:activate, &callback)
+        if keyval
+          self.signal_connect(:'key-press-event') do |item, event|
+            if event.state & Gdk::Window::ModifierType::CONTROL_MASK != 0 and
+              event.keyval == keyval
+              callback.call item
+            end
+          end
+        end
+        menu.append item
+        item
+      end
+
+      # This method should be implemented in subclasses to create the #menu of
+      # this instance. It has to be called after an instance of this class is
+      # created, to build the menu.
+      def create
+        raise NotImplementedError
+      end
+
+      def method_missing(*a, &b)
+        treeview.__send__(*a, &b)
+      end
+    end
+
+    # This class creates the popup menu, that opens when clicking onto the
+    # treeview.
+    class PopUpMenu
+      include MenuExtension
+
+      # Change the type or content of the selected node.
+      def change_node(item)
+        if current = selection.selected
+          parent = current.parent
+          old_type, old_content = current.type, current.content
+          if ALL_TYPES.include?(old_type)
+            @clipboard_data = Editor.model2data(current)
+            type, content = ask_for_element(parent, current.type,
+              current.content)
+            if type
+              current.type, current.content = type, content
+              current.remove_subtree(model)
+              toplevel.display_status("Changed a node in tree.")
+              window.change
+            end
+          else
+            toplevel.display_status(
+              "Cannot change node of type #{old_type} in tree!")
+          end
+        end
+      end
+
+      # Cut the selected node and its subtree, and save it into the
+      # clipboard.
+      def cut_node(item)
+        if current = selection.selected
+          if current and current.type == 'Key'
+            @clipboard_data = {
+              current.content => Editor.model2data(current.first_child)
+            }
+          else
+            @clipboard_data = Editor.model2data(current)
+          end
+          model.remove(current)
+          window.change
+          toplevel.display_status("Cut a node from tree.")
+        end
+      end
+
+      # Copy the selected node and its subtree, and save it into the
+      # clipboard.
+      def copy_node(item)
+        if current = selection.selected
+          if current and current.type == 'Key'
+            @clipboard_data = {
+              current.content => Editor.model2data(current.first_child)
+            }
+          else
+            @clipboard_data = Editor.model2data(current)
+          end
+          window.change
+          toplevel.display_status("Copied a node from tree.")
+        end
+      end
+
+      # Paste the data in the clipboard into the selected Array or Hash by
+      # appending it.
+      def paste_node_appending(item)
+        if current = selection.selected
+          if @clipboard_data
+            case current.type
+            when 'Array'
+              Editor.data2model(@clipboard_data, model, current)
+              expand_collapse(current)
+            when 'Hash'
+              if @clipboard_data.is_a? Hash
+                parent = current.parent
+                hash = Editor.model2data(current)
+                model.remove(current)
+                hash.update(@clipboard_data)
+                Editor.data2model(hash, model, parent)
+                if parent
+                  expand_collapse(parent)
+                elsif @expanded
+                  expand_all
+                end
+                window.change
+              else
+                toplevel.display_status(
+                  "Cannot paste non-#{current.type} data into '#{current.type}'!")
+              end
+            else
+              toplevel.display_status(
+                "Cannot paste node below '#{current.type}'!")
+            end
+          else
+            toplevel.display_status("Nothing to paste in clipboard!")
+          end
+        else
+            toplevel.display_status("Append a node into the root first!")
+        end
+      end
+
+      # Paste the data in the clipboard into the selected Array inserting it
+      # before the selected element.
+      def paste_node_inserting_before(item)
+        if current = selection.selected
+          if @clipboard_data
+            parent = current.parent or return
+            parent_type = parent.type
+            if parent_type == 'Array'
+              selected_index = parent.each_with_index do |c, i|
+                break i if c == current
+              end
+              Editor.data2model(@clipboard_data, model, parent) do |m|
+                m.insert_before(parent, current)
+              end
+              expand_collapse(current)
+              toplevel.display_status("Inserted an element to " +
+                "'#{parent_type}' before index #{selected_index}.")
+              window.change
+            else
+              toplevel.display_status(
+                "Cannot insert node below '#{parent_type}'!")
+            end
+          else
+            toplevel.display_status("Nothing to paste in clipboard!")
+          end
+        else
+            toplevel.display_status("Append a node into the root first!")
+        end
+      end
+
+      # Append a new node to the selected Hash or Array.
+      def append_new_node(item)
+        if parent = selection.selected
+          parent_type = parent.type
+          case parent_type
+          when 'Hash'
+            key, type, content = ask_for_hash_pair(parent)
+            key or return
+            iter = create_node(parent, 'Key', key)
+            iter = create_node(iter, type, content)
+            toplevel.display_status(
+              "Added a (key, value)-pair to '#{parent_type}'.")
+            window.change
+          when 'Array'
+            type, content = ask_for_element(parent)
+            type or return
+            iter = create_node(parent, type, content)
+            window.change
+            toplevel.display_status("Appendend an element to '#{parent_type}'.")
+          else
+            toplevel.display_status("Cannot append to '#{parent_type}'!")
+          end
+        else
+          type, content = ask_for_element
+          type or return
+          iter = create_node(nil, type, content)
+          window.change
+        end
+      end
+
+      # Insert a new node into an Array before the selected element.
+      def insert_new_node(item)
+        if current = selection.selected
+          parent = current.parent or return
+          parent_parent = parent.parent
+          parent_type = parent.type
+          if parent_type == 'Array'
+            selected_index = parent.each_with_index do |c, i|
+              break i if c == current
+            end
+            type, content = ask_for_element(parent)
+            type or return
+            iter = model.insert_before(parent, current)
+            iter.type, iter.content = type, content
+            toplevel.display_status("Inserted an element to " +
+              "'#{parent_type}' before index #{selected_index}.")
+            window.change
+          else
+            toplevel.display_status(
+              "Cannot insert node below '#{parent_type}'!")
+          end
+        else
+            toplevel.display_status("Append a node into the root first!")
+        end
+      end
+
+      # Recursively collapse/expand a subtree starting from the selected node.
+      def collapse_expand(item)
+        if current = selection.selected
+          if row_expanded?(current.path)
+            collapse_row(current.path)
+          else
+            expand_row(current.path, true)
+          end
+        else
+            toplevel.display_status("Append a node into the root first!")
+        end
+      end
+
+      # Create the menu.
+      def create
+        add_item("Change node", ?n, &method(:change_node))
+        add_separator
+        add_item("Cut node", ?X, &method(:cut_node))
+        add_item("Copy node", ?C, &method(:copy_node))
+        add_item("Paste node (appending)", ?A, &method(:paste_node_appending))
+        add_item("Paste node (inserting before)", ?I,
+          &method(:paste_node_inserting_before))
+        add_separator
+        add_item("Append new node", ?a, &method(:append_new_node))
+        add_item("Insert new node before", ?i, &method(:insert_new_node))
+        add_separator
+        add_item("Collapse/Expand node (recursively)", ?e,
+          &method(:collapse_expand))
+
+        menu.show_all
+        signal_connect(:button_press_event) do |widget, event|
+          if event.kind_of? Gdk::EventButton and event.button == 3
+            menu.popup(nil, nil, event.button, event.time)
+          end
+        end
+        signal_connect(:popup_menu) do
+          menu.popup(nil, nil, 0, Gdk::Event::CURRENT_TIME)
+        end
+      end
+    end
+
+    # This class creates the File pulldown menu.
+    class FileMenu
+      include MenuExtension
+
+      # Clear the model and filename, but ask to save the JSON document, if
+      # unsaved changes have occured.
+      def new(item)
+        window.clear
+      end
+
+      # Open a file and load it into the editor. Ask to save the JSON document
+      # first, if unsaved changes have occured.
+      def open(item)
+        window.file_open
+      end
+
+      def open_location(item)
+        window.location_open
+      end
+
+      # Revert the current JSON document in the editor to the saved version.
+      def revert(item)
+        window.instance_eval do
+          @filename and file_open(@filename)
+        end
+      end
+
+      # Save the current JSON document.
+      def save(item)
+        window.file_save
+      end
+
+      # Save the current JSON document under the given filename.
+      def save_as(item)
+        window.file_save_as
+      end
+
+      # Quit the editor, after asking to save any unsaved changes first.
+      def quit(item)
+        window.quit
+      end
+
+      # Create the menu.
+      def create
+        title = MenuItem.new('File')
+        title.submenu = menu
+        add_item('New', &method(:new))
+        add_item('Open', ?o, &method(:open))
+        add_item('Open location', ?l, &method(:open_location))
+        add_item('Revert', &method(:revert))
+        add_separator
+        add_item('Save', ?s, &method(:save))
+        add_item('Save As', ?S, &method(:save_as))
+        add_separator
+        add_item('Quit', ?q, &method(:quit))
+        title
+      end
+    end
+
+    # This class creates the Edit pulldown menu.
+    class EditMenu
+      include MenuExtension
+
+      # Copy data from model into primary clipboard.
+      def copy(item)
+        data = Editor.model2data(model.iter_first)
+        json = JSON.pretty_generate(data, :max_nesting => false)
+        c = Gtk::Clipboard.get(Gdk::Selection::PRIMARY)
+        c.text = json
+      end
+
+      # Copy json text from primary clipboard into model.
+      def paste(item)
+        c = Gtk::Clipboard.get(Gdk::Selection::PRIMARY)
+        if json = c.wait_for_text
+          window.ask_save if @changed
+          begin
+            window.edit json
+          rescue JSON::ParserError
+            window.clear
+          end
+        end
+      end
+
+      # Find a string in all nodes' contents and select the found node in the
+      # treeview.
+      def find(item)
+        @search = ask_for_find_term(@search) or return
+        iter = model.get_iter('0') or return
+        iter.recursive_each do |i|
+          if @iter
+            if @iter != i
+              next
+            else
+              @iter = nil
+              next
+            end
+          elsif @search.match(i[CONTENT_COL])
+             set_cursor(i.path, nil, false)
+             @iter = i
+             break
+          end
+        end
+      end
+
+      # Repeat the last search given by #find.
+      def find_again(item)
+        @search or return
+        iter = model.get_iter('0')
+        iter.recursive_each do |i|
+          if @iter
+            if @iter != i
+              next
+            else
+              @iter = nil
+              next
+            end
+          elsif @search.match(i[CONTENT_COL])
+             set_cursor(i.path, nil, false)
+             @iter = i
+             break
+          end
+        end
+      end
+
+      # Sort (Reverse sort) all elements of the selected array by the given
+      # expression. _x_ is the element in question.
+      def sort(item)
+        if current = selection.selected
+          if current.type == 'Array'
+            parent = current.parent
+            ary = Editor.model2data(current)
+            order, reverse = ask_for_order
+            order or return
+            begin
+              block = eval "lambda { |x| #{order} }"
+              if reverse
+                ary.sort! { |a,b| block[b] <=> block[a] }
+              else
+                ary.sort! { |a,b| block[a] <=> block[b] }
+              end
+            rescue => e
+              Editor.error_dialog(self, "Failed to sort Array with #{order}: #{e}!")
+            else
+              Editor.data2model(ary, model, parent) do |m|
+                m.insert_before(parent, current)
+              end
+              model.remove(current)
+              expand_collapse(parent)
+              window.change
+              toplevel.display_status("Array has been sorted.")
+            end
+          else
+            toplevel.display_status("Only Array nodes can be sorted!")
+          end
+        else
+            toplevel.display_status("Select an Array to sort first!")
+        end
+      end
+
+      # Create the menu.
+      def create
+        title = MenuItem.new('Edit')
+        title.submenu = menu
+        add_item('Copy', ?c, &method(:copy))
+        add_item('Paste', ?v, &method(:paste))
+        add_separator
+        add_item('Find', ?f, &method(:find))
+        add_item('Find Again', ?g, &method(:find_again))
+        add_separator
+        add_item('Sort', ?S, &method(:sort))
+        title
+      end
+    end
+
+    class OptionsMenu
+      include MenuExtension
+
+      # Collapse/Expand all nodes by default.
+      def collapsed_nodes(item)
+        if expanded
+          self.expanded = false
+          collapse_all
+        else
+          self.expanded = true
+          expand_all
+        end
+      end
+
+      # Toggle pretty saving mode on/off.
+      def pretty_saving(item)
+        @pretty_item.toggled
+        window.change
+      end
+
+      attr_reader :pretty_item
+
+      # Create the menu.
+      def create
+        title = MenuItem.new('Options')
+        title.submenu = menu
+        add_item('Collapsed nodes', nil, CheckMenuItem, &method(:collapsed_nodes))
+        @pretty_item = add_item('Pretty saving', nil, CheckMenuItem,
+          &method(:pretty_saving))
+        @pretty_item.active = true
+        window.unchange
+        title
+      end
+    end
+
+    # This class inherits from Gtk::TreeView, to configure it and to add a lot
+    # of behaviour to it.
+    class JSONTreeView < Gtk::TreeView
+      include Gtk
+
+      # Creates a JSONTreeView instance, the parameter _window_ is
+      # a MainWindow instance and used for self delegation.
+      def initialize(window)
+        @window = window
+        super(TreeStore.new(Gdk::Pixbuf, String, String))
+        self.selection.mode = SELECTION_BROWSE
+
+        @expanded = false
+        self.headers_visible = false
+        add_columns
+        add_popup_menu
+      end
+
+      # Returns the MainWindow instance of this JSONTreeView.
+      attr_reader :window
+
+      # Returns true, if nodes are autoexpanding, false otherwise.
+      attr_accessor :expanded
+
+      private
+
+      def add_columns
+        cell = CellRendererPixbuf.new
+        column = TreeViewColumn.new('Icon', cell,
+          'pixbuf'      => ICON_COL
+        )
+        append_column(column)
+
+        cell = CellRendererText.new
+        column = TreeViewColumn.new('Type', cell,
+          'text'      => TYPE_COL
+        )
+        append_column(column)
+
+        cell = CellRendererText.new
+        cell.editable = true
+        column = TreeViewColumn.new('Content', cell,
+          'text'       => CONTENT_COL
+        )
+        cell.signal_connect(:edited, &method(:cell_edited))
+        append_column(column)
+      end
+
+      def unify_key(iter, key)
+        return unless iter.type == 'Key'
+        parent = iter.parent
+        if parent.any? { |c| c != iter and c.content == key }
+          old_key = key
+          i = 0
+          begin
+            key = sprintf("%s.%d", old_key, i += 1)
+          end while parent.any? { |c| c != iter and c.content == key }
+        end
+        iter.content = key
+      end
+
+      def cell_edited(cell, path, value)
+        iter = model.get_iter(path)
+        case iter.type
+        when 'Key'
+          unify_key(iter, value)
+          toplevel.display_status('Key has been changed.')
+        when 'FalseClass'
+          value.downcase!
+          if value == 'true'
+            iter.type, iter.content = 'TrueClass', 'true'
+          end
+        when 'TrueClass'
+          value.downcase!
+          if value == 'false'
+            iter.type, iter.content = 'FalseClass', 'false'
+          end
+        when 'Numeric'
+          iter.content =
+            if value == 'Infinity'
+              value
+            else
+              (Integer(value) rescue Float(value) rescue 0).to_s
+            end
+        when 'String'
+          iter.content = value
+        when 'Hash', 'Array'
+          return
+        else
+          fail "Unknown type found in model: #{iter.type}"
+        end
+        window.change
+      end
+
+      def configure_value(value, type)
+        value.editable = false
+        case type
+        when 'Array', 'Hash'
+          value.text = ''
+        when 'TrueClass'
+          value.text = 'true'
+        when 'FalseClass'
+          value.text = 'false'
+        when 'NilClass'
+          value.text = 'null'
+        when 'Numeric', 'String'
+          value.text ||= ''
+          value.editable = true
+        else
+          raise ArgumentError, "unknown type '#{type}' encountered"
+        end
+      end
+
+      def add_popup_menu
+        menu = PopUpMenu.new(self)
+        menu.create
+      end
+
+      public
+
+      # Create a _type_ node with content _content_, and add it to _parent_
+      # in the model. If _parent_ is nil, create a new model and put it into
+      # the editor treeview.
+      def create_node(parent, type, content)
+        iter = if parent
+          model.append(parent)
+        else
+          new_model = Editor.data2model(nil)
+          toplevel.view_new_model(new_model)
+          new_model.iter_first
+        end
+        iter.type, iter.content = type, content
+        expand_collapse(parent) if parent
+        iter
+      end
+
+      # Ask for a hash key, value pair to be added to the Hash node _parent_.
+      def ask_for_hash_pair(parent)
+        key_input = type_input = value_input = nil
+
+        dialog = Dialog.new("New (key, value) pair for Hash", nil, nil,
+          [ Stock::OK, Dialog::RESPONSE_ACCEPT ],
+          [ Stock::CANCEL, Dialog::RESPONSE_REJECT ]
+        )
+        dialog.width_request = 640
+
+        hbox = HBox.new(false, 5)
+        hbox.pack_start(Label.new("Key:"), false)
+        hbox.pack_start(key_input = Entry.new)
+        key_input.text = @key || ''
+        dialog.vbox.pack_start(hbox, false)
+        key_input.signal_connect(:activate) do
+          if parent.any? { |c| c.content == key_input.text }
+            toplevel.display_status('Key already exists in Hash!')
+            key_input.text = ''
+          else
+            toplevel.display_status('Key has been changed.')
+          end
+        end
+
+        hbox = HBox.new(false, 5)
+        hbox.pack_start(Label.new("Type:"), false)
+        hbox.pack_start(type_input = ComboBox.new(true))
+        ALL_TYPES.each { |t| type_input.append_text(t) }
+        type_input.active = @type || 0
+        dialog.vbox.pack_start(hbox, false)
+
+        type_input.signal_connect(:changed) do
+          value_input.editable = false
+          case ALL_TYPES[type_input.active]
+          when 'Array', 'Hash'
+            value_input.text = ''
+          when 'TrueClass'
+            value_input.text = 'true'
+          when 'FalseClass'
+            value_input.text = 'false'
+          when 'NilClass'
+            value_input.text = 'null'
+          else
+            value_input.text = ''
+            value_input.editable = true
+          end
+        end
+
+        hbox = HBox.new(false, 5)
+        hbox.pack_start(Label.new("Value:"), false)
+        hbox.pack_start(value_input = Entry.new)
+        value_input.width_chars = 60
+        value_input.text = @value || ''
+        dialog.vbox.pack_start(hbox, false)
+
+        dialog.signal_connect(:'key-press-event', &DEFAULT_DIALOG_KEY_PRESS_HANDLER)
+        dialog.show_all
+        self.focus = dialog
+        dialog.run do |response|
+          if response == Dialog::RESPONSE_ACCEPT
+            @key = key_input.text
+            type = ALL_TYPES[@type = type_input.active]
+            content = value_input.text
+            return @key, type, content
+          end
+        end
+        return
+      ensure
+        dialog.destroy
+      end
+
+      # Ask for an element to be appended _parent_.
+      def ask_for_element(parent = nil, default_type = nil, value_text = @content)
+        type_input = value_input = nil
+
+        dialog = Dialog.new(
+          "New element into #{parent ? parent.type : 'root'}",
+          nil, nil,
+          [ Stock::OK, Dialog::RESPONSE_ACCEPT ],
+          [ Stock::CANCEL, Dialog::RESPONSE_REJECT ]
+        )
+        hbox = HBox.new(false, 5)
+        hbox.pack_start(Label.new("Type:"), false)
+        hbox.pack_start(type_input = ComboBox.new(true))
+        default_active = 0
+        types = parent ? ALL_TYPES : CONTAINER_TYPES
+        types.each_with_index do |t, i|
+          type_input.append_text(t)
+          if t == default_type
+            default_active = i
+          end
+        end
+        type_input.active = default_active
+        dialog.vbox.pack_start(hbox, false)
+        type_input.signal_connect(:changed) do
+          configure_value(value_input, types[type_input.active])
+        end
+
+        hbox = HBox.new(false, 5)
+        hbox.pack_start(Label.new("Value:"), false)
+        hbox.pack_start(value_input = Entry.new)
+        value_input.width_chars = 60
+        value_input.text = value_text if value_text
+        configure_value(value_input, types[type_input.active])
+
+        dialog.vbox.pack_start(hbox, false)
+
+        dialog.signal_connect(:'key-press-event', &DEFAULT_DIALOG_KEY_PRESS_HANDLER)
+        dialog.show_all
+        self.focus = dialog
+        dialog.run do |response|
+          if response == Dialog::RESPONSE_ACCEPT
+            type = types[type_input.active]
+            @content = case type
+            when 'Numeric'
+              if (t = value_input.text) == 'Infinity'
+                1 / 0.0
+              else
+                Integer(t) rescue Float(t) rescue 0
+              end
+            else
+              value_input.text
+            end.to_s
+            return type, @content
+          end
+        end
+        return
+      ensure
+        dialog.destroy if dialog
+      end
+
+      # Ask for an order criteria for sorting, using _x_ for the element in
+      # question. Returns the order criterium, and true/false for reverse
+      # sorting.
+      def ask_for_order
+        dialog = Dialog.new(
+          "Give an order criterium for 'x'.",
+          nil, nil,
+          [ Stock::OK, Dialog::RESPONSE_ACCEPT ],
+          [ Stock::CANCEL, Dialog::RESPONSE_REJECT ]
+        )
+        hbox = HBox.new(false, 5)
+
+        hbox.pack_start(Label.new("Order:"), false)
+        hbox.pack_start(order_input = Entry.new)
+        order_input.text = @order || 'x'
+        order_input.width_chars = 60
+
+        hbox.pack_start(reverse_checkbox = CheckButton.new('Reverse'), false)
+
+        dialog.vbox.pack_start(hbox, false)
+
+        dialog.signal_connect(:'key-press-event', &DEFAULT_DIALOG_KEY_PRESS_HANDLER)
+        dialog.show_all
+        self.focus = dialog
+        dialog.run do |response|
+          if response == Dialog::RESPONSE_ACCEPT
+            return @order = order_input.text, reverse_checkbox.active?
+          end
+        end
+        return
+      ensure
+        dialog.destroy if dialog
+      end
+
+      # Ask for a find term to search for in the tree. Returns the term as a
+      # string.
+      def ask_for_find_term(search = nil)
+        dialog = Dialog.new(
+          "Find a node matching regex in tree.",
+          nil, nil,
+          [ Stock::OK, Dialog::RESPONSE_ACCEPT ],
+          [ Stock::CANCEL, Dialog::RESPONSE_REJECT ]
+        )
+        hbox = HBox.new(false, 5)
+
+        hbox.pack_start(Label.new("Regex:"), false)
+        hbox.pack_start(regex_input = Entry.new)
+        hbox.pack_start(icase_checkbox = CheckButton.new('Icase'), false)
+        regex_input.width_chars = 60
+        if search
+          regex_input.text = search.source
+          icase_checkbox.active = search.casefold?
+        end
+
+        dialog.vbox.pack_start(hbox, false)
+
+        dialog.signal_connect(:'key-press-event', &DEFAULT_DIALOG_KEY_PRESS_HANDLER)
+        dialog.show_all
+        self.focus = dialog
+        dialog.run do |response|
+          if response == Dialog::RESPONSE_ACCEPT
+            begin
+              return Regexp.new(regex_input.text, icase_checkbox.active? ? Regexp::IGNORECASE : 0)
+            rescue => e
+              Editor.error_dialog(self, "Evaluation of regex /#{regex_input.text}/ failed: #{e}!")
+              return
+            end
+          end
+        end
+        return
+      ensure
+        dialog.destroy if dialog
+      end
+
+      # Expand or collapse row pointed to by _iter_ according
+      # to the #expanded attribute.
+      def expand_collapse(iter)
+        if expanded
+          expand_row(iter.path, true)
+        else
+          collapse_row(iter.path)
+        end
+      end
+    end
+
+    # The editor main window
+    class MainWindow < Gtk::Window
+      include Gtk
+
+      def initialize(encoding)
+        @changed  = false
+        @encoding = encoding
+        super(TOPLEVEL)
+        display_title
+        set_default_size(800, 600)
+        signal_connect(:delete_event) { quit }
+
+        vbox = VBox.new(false, 0)
+        add(vbox)
+        #vbox.border_width = 0
+
+        @treeview = JSONTreeView.new(self)
+        @treeview.signal_connect(:'cursor-changed') do
+          display_status('')
+        end
+
+        menu_bar = create_menu_bar
+        vbox.pack_start(menu_bar, false, false, 0)
+
+        sw = ScrolledWindow.new(nil, nil)
+        sw.shadow_type = SHADOW_ETCHED_IN
+        sw.set_policy(POLICY_AUTOMATIC, POLICY_AUTOMATIC)
+        vbox.pack_start(sw, true, true, 0)
+        sw.add(@treeview)
+
+        @status_bar = Statusbar.new
+        vbox.pack_start(@status_bar, false, false, 0)
+
+        @filename ||= nil
+        if @filename
+          data = read_data(@filename)
+          view_new_model Editor.data2model(data)
+        end
+
+        signal_connect(:button_release_event) do |_,event|
+          if event.button == 2
+            c = Gtk::Clipboard.get(Gdk::Selection::PRIMARY)
+            if url = c.wait_for_text
+              location_open url
+            end
+            false
+          else
+            true
+          end
+        end
+      end
+
+      # Creates the menu bar with the pulldown menus and returns it.
+      def create_menu_bar
+        menu_bar = MenuBar.new
+        @file_menu = FileMenu.new(@treeview)
+        menu_bar.append @file_menu.create
+        @edit_menu = EditMenu.new(@treeview)
+        menu_bar.append @edit_menu.create
+        @options_menu = OptionsMenu.new(@treeview)
+        menu_bar.append @options_menu.create
+        menu_bar
+      end
+
+      # Sets editor status to changed, to indicate that the edited data
+      # containts unsaved changes.
+      def change
+        @changed = true
+        display_title
+      end
+
+      # Sets editor status to unchanged, to indicate that the edited data
+      # doesn't containt unsaved changes.
+      def unchange
+        @changed = false
+        display_title
+      end
+
+      # Puts a new model _model_ into the Gtk::TreeView to be edited.
+      def view_new_model(model)
+        @treeview.model     = model
+        @treeview.expanded  = true
+        @treeview.expand_all
+        unchange
+      end
+
+      # Displays _text_ in the status bar.
+      def display_status(text)
+        @cid ||= nil
+        @status_bar.pop(@cid) if @cid
+        @cid = @status_bar.get_context_id('dummy')
+        @status_bar.push(@cid, text)
+      end
+
+      # Opens a dialog, asking, if changes should be saved to a file.
+      def ask_save
+        if Editor.question_dialog(self,
+          "Unsaved changes to JSON model. Save?")
+          if @filename
+            file_save
+          else
+            file_save_as
+          end
+        end
+      end
+
+      # Quit this editor, that is, leave this editor's main loop.
+      def quit
+        ask_save if @changed
+        if Gtk.main_level > 0
+          destroy
+          Gtk.main_quit
+        end
+        nil
+      end
+
+      # Display the new title according to the editor's current state.
+      def display_title
+        title = TITLE.dup
+        title << ": #@filename" if @filename
+        title << " *" if @changed
+        self.title = title
+      end
+
+      # Clear the current model, after asking to save all unsaved changes.
+      def clear
+        ask_save if @changed
+        @filename = nil
+        self.view_new_model nil
+      end
+
+      def check_pretty_printed(json)
+        pretty = !!((nl_index = json.index("\n")) && nl_index != json.size - 1)
+        @options_menu.pretty_item.active = pretty
+      end
+      private :check_pretty_printed
+
+      # Open the data at the location _uri_, if given. Otherwise open a dialog
+      # to ask for the _uri_.
+      def location_open(uri = nil)
+        uri = ask_for_location unless uri
+        uri or return
+        ask_save if @changed
+        data = load_location(uri) or return
+        view_new_model Editor.data2model(data)
+      end
+
+      # Open the file _filename_ or call the #select_file method to ask for a
+      # filename.
+      def file_open(filename = nil)
+        filename = select_file('Open as a JSON file') unless filename
+        data = load_file(filename) or return
+        view_new_model Editor.data2model(data)
+      end
+
+      # Edit the string _json_ in the editor.
+      def edit(json)
+        if json.respond_to? :read
+          json = json.read
+        end
+        data = parse_json json
+        view_new_model Editor.data2model(data)
+      end
+
+      # Save the current file.
+      def file_save
+        if @filename
+          store_file(@filename)
+        else
+          file_save_as
+        end
+      end
+
+      # Save the current file as the filename
+      def file_save_as
+        filename = select_file('Save as a JSON file')
+        store_file(filename)
+      end
+
+      # Store the current JSON document to _path_.
+      def store_file(path)
+        if path
+          data = Editor.model2data(@treeview.model.iter_first)
+          File.open(path + '.tmp', 'wb') do |output|
+            data or break
+            if @options_menu.pretty_item.active?
+              output.puts JSON.pretty_generate(data, :max_nesting => false)
+            else
+              output.write JSON.generate(data, :max_nesting => false)
+            end
+          end
+          File.rename path + '.tmp', path
+          @filename = path
+          toplevel.display_status("Saved data to '#@filename'.")
+          unchange
+        end
+      rescue SystemCallError => e
+        Editor.error_dialog(self, "Failed to store JSON file: #{e}!")
+      end
+
+      # Load the file named _filename_ into the editor as a JSON document.
+      def load_file(filename)
+        if filename
+          if File.directory?(filename)
+            Editor.error_dialog(self, "Try to select a JSON file!")
+            nil
+          else
+            @filename = filename
+            if data = read_data(filename)
+              toplevel.display_status("Loaded data from '#@filename'.")
+            end
+            display_title
+            data
+          end
+        end
+      end
+
+      # Load the data at location _uri_ into the editor as a JSON document.
+      def load_location(uri)
+        data = read_data(uri) or return
+        @filename = nil
+        toplevel.display_status("Loaded data from '#{uri}'.")
+        display_title
+        data
+      end
+
+      def parse_json(json)
+        check_pretty_printed(json)
+        if @encoding && !/^utf8$/i.match(@encoding)
+          json = JSON.iconv 'utf-8', @encoding, json
+        end
+        JSON::parse(json, :max_nesting => false, :create_additions => false)
+      end
+      private :parse_json
+
+      # Read a JSON document from the file named _filename_, parse it into a
+      # ruby data structure, and return the data.
+      def read_data(filename)
+        open(filename) do |f|
+          json = f.read
+          return parse_json(json)
+        end
+      rescue => e
+        Editor.error_dialog(self, "Failed to parse JSON file: #{e}!")
+        return
+      end
+
+      # Open a file selecton dialog, displaying _message_, and return the
+      # selected filename or nil, if no file was selected.
+      def select_file(message)
+        filename = nil
+        fs = FileSelection.new(message)
+        fs.set_modal(true)
+        @default_dir = File.join(Dir.pwd, '') unless @default_dir
+        fs.set_filename(@default_dir)
+        fs.set_transient_for(self)
+        fs.signal_connect(:destroy) { Gtk.main_quit }
+        fs.ok_button.signal_connect(:clicked) do
+          filename = fs.filename
+          @default_dir = File.join(File.dirname(filename), '')
+          fs.destroy
+          Gtk.main_quit
+        end
+        fs.cancel_button.signal_connect(:clicked) do
+          fs.destroy
+          Gtk.main_quit
+        end
+        fs.show_all
+        Gtk.main
+        filename
+      end
+
+      # Ask for location URI a to load data from. Returns the URI as a string.
+      def ask_for_location
+        dialog = Dialog.new(
+          "Load data from location...",
+          nil, nil,
+          [ Stock::OK, Dialog::RESPONSE_ACCEPT ],
+          [ Stock::CANCEL, Dialog::RESPONSE_REJECT ]
+        )
+        hbox = HBox.new(false, 5)
+
+        hbox.pack_start(Label.new("Location:"), false)
+        hbox.pack_start(location_input = Entry.new)
+        location_input.width_chars = 60
+        location_input.text = @location || ''
+
+        dialog.vbox.pack_start(hbox, false)
+
+        dialog.signal_connect(:'key-press-event', &DEFAULT_DIALOG_KEY_PRESS_HANDLER)
+        dialog.show_all
+        dialog.run do |response|
+          if response == Dialog::RESPONSE_ACCEPT
+            return @location = location_input.text
+          end
+        end
+        return
+      ensure
+        dialog.destroy if dialog
+      end
+    end
+
+    class << self
+      # Starts a JSON Editor. If a block was given, it yields
+      # to the JSON::Editor::MainWindow instance.
+      def start(encoding = 'utf8') # :yield: window
+        Gtk.init
+        @window = Editor::MainWindow.new(encoding)
+        @window.icon_list = [ Editor.fetch_icon('json') ]
+        yield @window if block_given?
+        @window.show_all
+        Gtk.main
+      end
+
+      # Edit the string _json_ with encoding _encoding_ in the editor.
+      def edit(json, encoding = 'utf8')
+        start(encoding) do |window|
+          window.edit json
+        end
+      end
+
+      attr_reader :window
+    end
+  end
+end