1 # To use the GUI JSON editor, start the edit_json.rb executable script. It
2 # requires ruby-gtk to be installed.
13 # Beginning of the editor window title
14 TITLE = 'JSON Editor'.freeze
17 ICON_COL, TYPE_COL, CONTENT_COL = 0, 1, 2
19 # JSON primitive types (Containers)
20 CONTAINER_TYPES = %w[Array Hash].sort
21 # All JSON primitive types
22 ALL_TYPES = (%w[TrueClass FalseClass Numeric String NilClass] +
25 # The Nodes necessary for the tree representation of a JSON document
26 ALL_NODES = (ALL_TYPES + %w[Key]).sort
28 DEFAULT_DIALOG_KEY_PRESS_HANDLER = lambda do |dialog, event|
30 when Gdk::Keyval::GDK_Return
31 dialog.response Dialog::RESPONSE_ACCEPT
32 when Gdk::Keyval::GDK_Escape
33 dialog.response Dialog::RESPONSE_REJECT
37 # Returns the Gdk::Pixbuf of the icon named _name_ from the icon cache.
38 def Editor.fetch_icon(name)
40 unless @icon_cache.key?(name)
41 path = File.dirname(__FILE__)
42 @icon_cache[name] = Gdk::Pixbuf.new(File.join(path, name + '.xpm'))
47 # Opens an error dialog on top of _window_ showing the error message
49 def Editor.error_dialog(window, text)
50 dialog = MessageDialog.new(window, Dialog::MODAL,
52 MessageDialog::BUTTONS_CLOSE, text)
56 dialog = MessageDialog.new(Editor.window, Dialog::MODAL,
58 MessageDialog::BUTTONS_CLOSE, text)
62 dialog.destroy if dialog
65 # Opens a yes/no question dialog on top of _window_ showing the error
66 # message _text_. If yes was answered _true_ is returned, otherwise
68 def Editor.question_dialog(window, text)
69 dialog = MessageDialog.new(window, Dialog::MODAL,
70 MessageDialog::QUESTION,
71 MessageDialog::BUTTONS_YES_NO, text)
73 dialog.run do |response|
74 return Gtk::Dialog::RESPONSE_YES === response
77 dialog.destroy if dialog
80 # Convert the tree model starting from Gtk::TreeIter _iter_ into a Ruby
81 # data structure and return it.
82 def Editor.model2data(iter)
83 return nil if iter.nil?
87 iter.each { |c| hash[c.content] = Editor.model2data(c.first_child) }
90 array = Array.new(iter.n_children)
91 iter.each_with_index { |c, i| array[i] = Editor.model2data(c) }
98 content = iter.content
99 if /\./.match(content)
111 fail "Unknown type found in model: #{iter.type}"
115 # Convert the Ruby data structure _data_ into tree model data for Gtk and
116 # returns the whole model. If the parameter _model_ wasn't given a new
117 # Gtk::TreeStore is created as the model. The _parent_ parameter specifies
118 # the parent node (iter, Gtk:TreeIter instance) to which the data is
119 # appended, alternativeley the result of the yielded block is used as iter.
120 def Editor.data2model(data, model = nil, parent = nil)
121 model ||= TreeStore.new(Gdk::Pixbuf, String, String)
122 iter = if block_given?
130 data.sort.each do |key, value|
131 pair_iter = model.append(iter)
132 pair_iter.type = 'Key'
133 pair_iter.content = key.to_s
134 Editor.data2model(value, model, pair_iter)
139 Editor.data2model(value, model, iter)
142 iter.type = 'Numeric'
143 iter.content = data.to_s
144 when String, true, false, nil
145 iter.type = data.class.name
146 iter.content = data.nil? ? 'null' : data.to_s
149 iter.content = data.to_s
154 # The Gtk::TreeIter class is reopened and some auxiliary methods are added.
158 # Traverse each of this Gtk::TreeIter instance's children
161 n_children.times { |i| yield nth_child(i) }
164 # Recursively traverse all nodes of this Gtk::TreeIter's subtree
165 # (including self) and yield to them.
166 def recursive_each(&block)
169 i.recursive_each(&block)
173 # Remove the subtree of this Gtk::TreeIter instance from the
175 def remove_subtree(model)
176 while current = first_child
177 model.remove(current)
181 # Returns the type of this node.
186 # Sets the type of this node to _value_. This implies setting
187 # the respective icon accordingly.
189 self[TYPE_COL] = value
190 self[ICON_COL] = Editor.fetch_icon(value)
193 # Returns the content of this node.
198 # Sets the content of this node to _value_.
200 self[CONTENT_COL] = value
204 # This module bundles some method, that can be used to create a menu. It
205 # should be included into the class in question.
209 # Creates a Menu, that includes MenuExtension. _treeview_ is the
210 # Gtk::TreeView, on which it operates.
211 def initialize(treeview)
216 # Returns the Gtk::TreeView of this menu.
217 attr_reader :treeview
222 # Adds a Gtk::SeparatorMenuItem to this instance's #menu.
224 menu.append SeparatorMenuItem.new
227 # Adds a Gtk::MenuItem to this instance's #menu. _label_ is the label
228 # string, _klass_ is the item type, and _callback_ is the procedure, that
229 # is called if the _item_ is activated.
230 def add_item(label, keyval = nil, klass = MenuItem, &callback)
231 label = "#{label} (C-#{keyval.chr})" if keyval
232 item = klass.new(label)
233 item.signal_connect(:activate, &callback)
235 self.signal_connect(:'key-press-event') do |item, event|
236 if event.state & Gdk::Window::ModifierType::CONTROL_MASK != 0 and
237 event.keyval == keyval
246 # This method should be implemented in subclasses to create the #menu of
247 # this instance. It has to be called after an instance of this class is
248 # created, to build the menu.
250 raise NotImplementedError
253 def method_missing(*a, &b)
254 treeview.__send__(*a, &b)
258 # This class creates the popup menu, that opens when clicking onto the
261 include MenuExtension
263 # Change the type or content of the selected node.
264 def change_node(item)
265 if current = selection.selected
266 parent = current.parent
267 old_type, old_content = current.type, current.content
268 if ALL_TYPES.include?(old_type)
269 @clipboard_data = Editor.model2data(current)
270 type, content = ask_for_element(parent, current.type,
273 current.type, current.content = type, content
274 current.remove_subtree(model)
275 toplevel.display_status("Changed a node in tree.")
279 toplevel.display_status(
280 "Cannot change node of type #{old_type} in tree!")
285 # Cut the selected node and its subtree, and save it into the
288 if current = selection.selected
289 if current and current.type == 'Key'
291 current.content => Editor.model2data(current.first_child)
294 @clipboard_data = Editor.model2data(current)
296 model.remove(current)
298 toplevel.display_status("Cut a node from tree.")
302 # Copy the selected node and its subtree, and save it into the
305 if current = selection.selected
306 if current and current.type == 'Key'
308 current.content => Editor.model2data(current.first_child)
311 @clipboard_data = Editor.model2data(current)
314 toplevel.display_status("Copied a node from tree.")
318 # Paste the data in the clipboard into the selected Array or Hash by
320 def paste_node_appending(item)
321 if current = selection.selected
325 Editor.data2model(@clipboard_data, model, current)
326 expand_collapse(current)
328 if @clipboard_data.is_a? Hash
329 parent = current.parent
330 hash = Editor.model2data(current)
331 model.remove(current)
332 hash.update(@clipboard_data)
333 Editor.data2model(hash, model, parent)
335 expand_collapse(parent)
341 toplevel.display_status(
342 "Cannot paste non-#{current.type} data into '#{current.type}'!")
345 toplevel.display_status(
346 "Cannot paste node below '#{current.type}'!")
349 toplevel.display_status("Nothing to paste in clipboard!")
352 toplevel.display_status("Append a node into the root first!")
356 # Paste the data in the clipboard into the selected Array inserting it
357 # before the selected element.
358 def paste_node_inserting_before(item)
359 if current = selection.selected
361 parent = current.parent or return
362 parent_type = parent.type
363 if parent_type == 'Array'
364 selected_index = parent.each_with_index do |c, i|
365 break i if c == current
367 Editor.data2model(@clipboard_data, model, parent) do |m|
368 m.insert_before(parent, current)
370 expand_collapse(current)
371 toplevel.display_status("Inserted an element to " +
372 "'#{parent_type}' before index #{selected_index}.")
375 toplevel.display_status(
376 "Cannot insert node below '#{parent_type}'!")
379 toplevel.display_status("Nothing to paste in clipboard!")
382 toplevel.display_status("Append a node into the root first!")
386 # Append a new node to the selected Hash or Array.
387 def append_new_node(item)
388 if parent = selection.selected
389 parent_type = parent.type
392 key, type, content = ask_for_hash_pair(parent)
394 iter = create_node(parent, 'Key', key)
395 iter = create_node(iter, type, content)
396 toplevel.display_status(
397 "Added a (key, value)-pair to '#{parent_type}'.")
400 type, content = ask_for_element(parent)
402 iter = create_node(parent, type, content)
404 toplevel.display_status("Appendend an element to '#{parent_type}'.")
406 toplevel.display_status("Cannot append to '#{parent_type}'!")
409 type, content = ask_for_element
411 iter = create_node(nil, type, content)
416 # Insert a new node into an Array before the selected element.
417 def insert_new_node(item)
418 if current = selection.selected
419 parent = current.parent or return
420 parent_parent = parent.parent
421 parent_type = parent.type
422 if parent_type == 'Array'
423 selected_index = parent.each_with_index do |c, i|
424 break i if c == current
426 type, content = ask_for_element(parent)
428 iter = model.insert_before(parent, current)
429 iter.type, iter.content = type, content
430 toplevel.display_status("Inserted an element to " +
431 "'#{parent_type}' before index #{selected_index}.")
434 toplevel.display_status(
435 "Cannot insert node below '#{parent_type}'!")
438 toplevel.display_status("Append a node into the root first!")
442 # Recursively collapse/expand a subtree starting from the selected node.
443 def collapse_expand(item)
444 if current = selection.selected
445 if row_expanded?(current.path)
446 collapse_row(current.path)
448 expand_row(current.path, true)
451 toplevel.display_status("Append a node into the root first!")
457 add_item("Change node", ?n, &method(:change_node))
459 add_item("Cut node", ?X, &method(:cut_node))
460 add_item("Copy node", ?C, &method(:copy_node))
461 add_item("Paste node (appending)", ?A, &method(:paste_node_appending))
462 add_item("Paste node (inserting before)", ?I,
463 &method(:paste_node_inserting_before))
465 add_item("Append new node", ?a, &method(:append_new_node))
466 add_item("Insert new node before", ?i, &method(:insert_new_node))
468 add_item("Collapse/Expand node (recursively)", ?e,
469 &method(:collapse_expand))
472 signal_connect(:button_press_event) do |widget, event|
473 if event.kind_of? Gdk::EventButton and event.button == 3
474 menu.popup(nil, nil, event.button, event.time)
477 signal_connect(:popup_menu) do
478 menu.popup(nil, nil, 0, Gdk::Event::CURRENT_TIME)
483 # This class creates the File pulldown menu.
485 include MenuExtension
487 # Clear the model and filename, but ask to save the JSON document, if
488 # unsaved changes have occured.
493 # Open a file and load it into the editor. Ask to save the JSON document
494 # first, if unsaved changes have occured.
499 def open_location(item)
503 # Revert the current JSON document in the editor to the saved version.
505 window.instance_eval do
506 @filename and file_open(@filename)
510 # Save the current JSON document.
515 # Save the current JSON document under the given filename.
520 # Quit the editor, after asking to save any unsaved changes first.
527 title = MenuItem.new('File')
529 add_item('New', &method(:new))
530 add_item('Open', ?o, &method(:open))
531 add_item('Open location', ?l, &method(:open_location))
532 add_item('Revert', &method(:revert))
534 add_item('Save', ?s, &method(:save))
535 add_item('Save As', ?S, &method(:save_as))
537 add_item('Quit', ?q, &method(:quit))
542 # This class creates the Edit pulldown menu.
544 include MenuExtension
546 # Copy data from model into primary clipboard.
548 data = Editor.model2data(model.iter_first)
549 json = JSON.pretty_generate(data, :max_nesting => false)
550 c = Gtk::Clipboard.get(Gdk::Selection::PRIMARY)
554 # Copy json text from primary clipboard into model.
556 c = Gtk::Clipboard.get(Gdk::Selection::PRIMARY)
557 if json = c.wait_for_text
558 window.ask_save if @changed
561 rescue JSON::ParserError
567 # Find a string in all nodes' contents and select the found node in the
570 @search = ask_for_find_term(@search) or return
571 iter = model.get_iter('0') or return
572 iter.recursive_each do |i|
580 elsif @search.match(i[CONTENT_COL])
581 set_cursor(i.path, nil, false)
588 # Repeat the last search given by #find.
591 iter = model.get_iter('0')
592 iter.recursive_each do |i|
600 elsif @search.match(i[CONTENT_COL])
601 set_cursor(i.path, nil, false)
608 # Sort (Reverse sort) all elements of the selected array by the given
609 # expression. _x_ is the element in question.
611 if current = selection.selected
612 if current.type == 'Array'
613 parent = current.parent
614 ary = Editor.model2data(current)
615 order, reverse = ask_for_order
618 block = eval "lambda { |x| #{order} }"
620 ary.sort! { |a,b| block[b] <=> block[a] }
622 ary.sort! { |a,b| block[a] <=> block[b] }
625 Editor.error_dialog(self, "Failed to sort Array with #{order}: #{e}!")
627 Editor.data2model(ary, model, parent) do |m|
628 m.insert_before(parent, current)
630 model.remove(current)
631 expand_collapse(parent)
633 toplevel.display_status("Array has been sorted.")
636 toplevel.display_status("Only Array nodes can be sorted!")
639 toplevel.display_status("Select an Array to sort first!")
645 title = MenuItem.new('Edit')
647 add_item('Copy', ?c, &method(:copy))
648 add_item('Paste', ?v, &method(:paste))
650 add_item('Find', ?f, &method(:find))
651 add_item('Find Again', ?g, &method(:find_again))
653 add_item('Sort', ?S, &method(:sort))
659 include MenuExtension
661 # Collapse/Expand all nodes by default.
662 def collapsed_nodes(item)
664 self.expanded = false
672 # Toggle pretty saving mode on/off.
673 def pretty_saving(item)
678 attr_reader :pretty_item
682 title = MenuItem.new('Options')
684 add_item('Collapsed nodes', nil, CheckMenuItem, &method(:collapsed_nodes))
685 @pretty_item = add_item('Pretty saving', nil, CheckMenuItem,
686 &method(:pretty_saving))
687 @pretty_item.active = true
693 # This class inherits from Gtk::TreeView, to configure it and to add a lot
694 # of behaviour to it.
695 class JSONTreeView < Gtk::TreeView
698 # Creates a JSONTreeView instance, the parameter _window_ is
699 # a MainWindow instance and used for self delegation.
700 def initialize(window)
702 super(TreeStore.new(Gdk::Pixbuf, String, String))
703 self.selection.mode = SELECTION_BROWSE
706 self.headers_visible = false
711 # Returns the MainWindow instance of this JSONTreeView.
714 # Returns true, if nodes are autoexpanding, false otherwise.
715 attr_accessor :expanded
720 cell = CellRendererPixbuf.new
721 column = TreeViewColumn.new('Icon', cell,
724 append_column(column)
726 cell = CellRendererText.new
727 column = TreeViewColumn.new('Type', cell,
730 append_column(column)
732 cell = CellRendererText.new
734 column = TreeViewColumn.new('Content', cell,
735 'text' => CONTENT_COL
737 cell.signal_connect(:edited, &method(:cell_edited))
738 append_column(column)
741 def unify_key(iter, key)
742 return unless iter.type == 'Key'
744 if parent.any? { |c| c != iter and c.content == key }
748 key = sprintf("%s.%d", old_key, i += 1)
749 end while parent.any? { |c| c != iter and c.content == key }
754 def cell_edited(cell, path, value)
755 iter = model.get_iter(path)
758 unify_key(iter, value)
759 toplevel.display_status('Key has been changed.')
763 iter.type, iter.content = 'TrueClass', 'true'
768 iter.type, iter.content = 'FalseClass', 'false'
772 if value == 'Infinity'
775 (Integer(value) rescue Float(value) rescue 0).to_s
782 fail "Unknown type found in model: #{iter.type}"
787 def configure_value(value, type)
788 value.editable = false
798 when 'Numeric', 'String'
800 value.editable = true
802 raise ArgumentError, "unknown type '#{type}' encountered"
807 menu = PopUpMenu.new(self)
813 # Create a _type_ node with content _content_, and add it to _parent_
814 # in the model. If _parent_ is nil, create a new model and put it into
815 # the editor treeview.
816 def create_node(parent, type, content)
820 new_model = Editor.data2model(nil)
821 toplevel.view_new_model(new_model)
824 iter.type, iter.content = type, content
825 expand_collapse(parent) if parent
829 # Ask for a hash key, value pair to be added to the Hash node _parent_.
830 def ask_for_hash_pair(parent)
831 key_input = type_input = value_input = nil
833 dialog = Dialog.new("New (key, value) pair for Hash", nil, nil,
834 [ Stock::OK, Dialog::RESPONSE_ACCEPT ],
835 [ Stock::CANCEL, Dialog::RESPONSE_REJECT ]
837 dialog.width_request = 640
839 hbox = HBox.new(false, 5)
840 hbox.pack_start(Label.new("Key:"), false)
841 hbox.pack_start(key_input = Entry.new)
842 key_input.text = @key || ''
843 dialog.vbox.pack_start(hbox, false)
844 key_input.signal_connect(:activate) do
845 if parent.any? { |c| c.content == key_input.text }
846 toplevel.display_status('Key already exists in Hash!')
849 toplevel.display_status('Key has been changed.')
853 hbox = HBox.new(false, 5)
854 hbox.pack_start(Label.new("Type:"), false)
855 hbox.pack_start(type_input = ComboBox.new(true))
856 ALL_TYPES.each { |t| type_input.append_text(t) }
857 type_input.active = @type || 0
858 dialog.vbox.pack_start(hbox, false)
860 type_input.signal_connect(:changed) do
861 value_input.editable = false
862 case ALL_TYPES[type_input.active]
864 value_input.text = ''
866 value_input.text = 'true'
868 value_input.text = 'false'
870 value_input.text = 'null'
872 value_input.text = ''
873 value_input.editable = true
877 hbox = HBox.new(false, 5)
878 hbox.pack_start(Label.new("Value:"), false)
879 hbox.pack_start(value_input = Entry.new)
880 value_input.width_chars = 60
881 value_input.text = @value || ''
882 dialog.vbox.pack_start(hbox, false)
884 dialog.signal_connect(:'key-press-event', &DEFAULT_DIALOG_KEY_PRESS_HANDLER)
887 dialog.run do |response|
888 if response == Dialog::RESPONSE_ACCEPT
889 @key = key_input.text
890 type = ALL_TYPES[@type = type_input.active]
891 content = value_input.text
892 return @key, type, content
900 # Ask for an element to be appended _parent_.
901 def ask_for_element(parent = nil, default_type = nil, value_text = @content)
902 type_input = value_input = nil
905 "New element into #{parent ? parent.type : 'root'}",
907 [ Stock::OK, Dialog::RESPONSE_ACCEPT ],
908 [ Stock::CANCEL, Dialog::RESPONSE_REJECT ]
910 hbox = HBox.new(false, 5)
911 hbox.pack_start(Label.new("Type:"), false)
912 hbox.pack_start(type_input = ComboBox.new(true))
914 types = parent ? ALL_TYPES : CONTAINER_TYPES
915 types.each_with_index do |t, i|
916 type_input.append_text(t)
921 type_input.active = default_active
922 dialog.vbox.pack_start(hbox, false)
923 type_input.signal_connect(:changed) do
924 configure_value(value_input, types[type_input.active])
927 hbox = HBox.new(false, 5)
928 hbox.pack_start(Label.new("Value:"), false)
929 hbox.pack_start(value_input = Entry.new)
930 value_input.width_chars = 60
931 value_input.text = value_text if value_text
932 configure_value(value_input, types[type_input.active])
934 dialog.vbox.pack_start(hbox, false)
936 dialog.signal_connect(:'key-press-event', &DEFAULT_DIALOG_KEY_PRESS_HANDLER)
939 dialog.run do |response|
940 if response == Dialog::RESPONSE_ACCEPT
941 type = types[type_input.active]
944 if (t = value_input.text) == 'Infinity'
947 Integer(t) rescue Float(t) rescue 0
952 return type, @content
957 dialog.destroy if dialog
960 # Ask for an order criteria for sorting, using _x_ for the element in
961 # question. Returns the order criterium, and true/false for reverse
965 "Give an order criterium for 'x'.",
967 [ Stock::OK, Dialog::RESPONSE_ACCEPT ],
968 [ Stock::CANCEL, Dialog::RESPONSE_REJECT ]
970 hbox = HBox.new(false, 5)
972 hbox.pack_start(Label.new("Order:"), false)
973 hbox.pack_start(order_input = Entry.new)
974 order_input.text = @order || 'x'
975 order_input.width_chars = 60
977 hbox.pack_start(reverse_checkbox = CheckButton.new('Reverse'), false)
979 dialog.vbox.pack_start(hbox, false)
981 dialog.signal_connect(:'key-press-event', &DEFAULT_DIALOG_KEY_PRESS_HANDLER)
984 dialog.run do |response|
985 if response == Dialog::RESPONSE_ACCEPT
986 return @order = order_input.text, reverse_checkbox.active?
991 dialog.destroy if dialog
994 # Ask for a find term to search for in the tree. Returns the term as a
996 def ask_for_find_term(search = nil)
998 "Find a node matching regex in tree.",
1000 [ Stock::OK, Dialog::RESPONSE_ACCEPT ],
1001 [ Stock::CANCEL, Dialog::RESPONSE_REJECT ]
1003 hbox = HBox.new(false, 5)
1005 hbox.pack_start(Label.new("Regex:"), false)
1006 hbox.pack_start(regex_input = Entry.new)
1007 hbox.pack_start(icase_checkbox = CheckButton.new('Icase'), false)
1008 regex_input.width_chars = 60
1010 regex_input.text = search.source
1011 icase_checkbox.active = search.casefold?
1014 dialog.vbox.pack_start(hbox, false)
1016 dialog.signal_connect(:'key-press-event', &DEFAULT_DIALOG_KEY_PRESS_HANDLER)
1019 dialog.run do |response|
1020 if response == Dialog::RESPONSE_ACCEPT
1022 return Regexp.new(regex_input.text, icase_checkbox.active? ? Regexp::IGNORECASE : 0)
1024 Editor.error_dialog(self, "Evaluation of regex /#{regex_input.text}/ failed: #{e}!")
1031 dialog.destroy if dialog
1034 # Expand or collapse row pointed to by _iter_ according
1035 # to the #expanded attribute.
1036 def expand_collapse(iter)
1038 expand_row(iter.path, true)
1040 collapse_row(iter.path)
1045 # The editor main window
1046 class MainWindow < Gtk::Window
1049 def initialize(encoding)
1051 @encoding = encoding
1054 set_default_size(800, 600)
1055 signal_connect(:delete_event) { quit }
1057 vbox = VBox.new(false, 0)
1059 #vbox.border_width = 0
1061 @treeview = JSONTreeView.new(self)
1062 @treeview.signal_connect(:'cursor-changed') do
1066 menu_bar = create_menu_bar
1067 vbox.pack_start(menu_bar, false, false, 0)
1069 sw = ScrolledWindow.new(nil, nil)
1070 sw.shadow_type = SHADOW_ETCHED_IN
1071 sw.set_policy(POLICY_AUTOMATIC, POLICY_AUTOMATIC)
1072 vbox.pack_start(sw, true, true, 0)
1075 @status_bar = Statusbar.new
1076 vbox.pack_start(@status_bar, false, false, 0)
1080 data = read_data(@filename)
1081 view_new_model Editor.data2model(data)
1084 signal_connect(:button_release_event) do |_,event|
1085 if event.button == 2
1086 c = Gtk::Clipboard.get(Gdk::Selection::PRIMARY)
1087 if url = c.wait_for_text
1097 # Creates the menu bar with the pulldown menus and returns it.
1099 menu_bar = MenuBar.new
1100 @file_menu = FileMenu.new(@treeview)
1101 menu_bar.append @file_menu.create
1102 @edit_menu = EditMenu.new(@treeview)
1103 menu_bar.append @edit_menu.create
1104 @options_menu = OptionsMenu.new(@treeview)
1105 menu_bar.append @options_menu.create
1109 # Sets editor status to changed, to indicate that the edited data
1110 # containts unsaved changes.
1116 # Sets editor status to unchanged, to indicate that the edited data
1117 # doesn't containt unsaved changes.
1123 # Puts a new model _model_ into the Gtk::TreeView to be edited.
1124 def view_new_model(model)
1125 @treeview.model = model
1126 @treeview.expanded = true
1127 @treeview.expand_all
1131 # Displays _text_ in the status bar.
1132 def display_status(text)
1134 @status_bar.pop(@cid) if @cid
1135 @cid = @status_bar.get_context_id('dummy')
1136 @status_bar.push(@cid, text)
1139 # Opens a dialog, asking, if changes should be saved to a file.
1141 if Editor.question_dialog(self,
1142 "Unsaved changes to JSON model. Save?")
1151 # Quit this editor, that is, leave this editor's main loop.
1153 ask_save if @changed
1154 if Gtk.main_level > 0
1161 # Display the new title according to the editor's current state.
1164 title << ": #@filename" if @filename
1165 title << " *" if @changed
1169 # Clear the current model, after asking to save all unsaved changes.
1171 ask_save if @changed
1173 self.view_new_model nil
1176 def check_pretty_printed(json)
1177 pretty = !!((nl_index = json.index("\n")) && nl_index != json.size - 1)
1178 @options_menu.pretty_item.active = pretty
1180 private :check_pretty_printed
1182 # Open the data at the location _uri_, if given. Otherwise open a dialog
1183 # to ask for the _uri_.
1184 def location_open(uri = nil)
1185 uri = ask_for_location unless uri
1187 ask_save if @changed
1188 data = load_location(uri) or return
1189 view_new_model Editor.data2model(data)
1192 # Open the file _filename_ or call the #select_file method to ask for a
1194 def file_open(filename = nil)
1195 filename = select_file('Open as a JSON file') unless filename
1196 data = load_file(filename) or return
1197 view_new_model Editor.data2model(data)
1200 # Edit the string _json_ in the editor.
1202 if json.respond_to? :read
1205 data = parse_json json
1206 view_new_model Editor.data2model(data)
1209 # Save the current file.
1212 store_file(@filename)
1218 # Save the current file as the filename
1220 filename = select_file('Save as a JSON file')
1221 store_file(filename)
1224 # Store the current JSON document to _path_.
1225 def store_file(path)
1227 data = Editor.model2data(@treeview.model.iter_first)
1228 File.open(path + '.tmp', 'wb') do |output|
1230 if @options_menu.pretty_item.active?
1231 output.puts JSON.pretty_generate(data, :max_nesting => false)
1233 output.write JSON.generate(data, :max_nesting => false)
1236 File.rename path + '.tmp', path
1238 toplevel.display_status("Saved data to '#@filename'.")
1241 rescue SystemCallError => e
1242 Editor.error_dialog(self, "Failed to store JSON file: #{e}!")
1245 # Load the file named _filename_ into the editor as a JSON document.
1246 def load_file(filename)
1248 if File.directory?(filename)
1249 Editor.error_dialog(self, "Try to select a JSON file!")
1252 @filename = filename
1253 if data = read_data(filename)
1254 toplevel.display_status("Loaded data from '#@filename'.")
1262 # Load the data at location _uri_ into the editor as a JSON document.
1263 def load_location(uri)
1264 data = read_data(uri) or return
1266 toplevel.display_status("Loaded data from '#{uri}'.")
1271 def parse_json(json)
1272 check_pretty_printed(json)
1273 if @encoding && !/^utf8$/i.match(@encoding)
1274 json = JSON.iconv 'utf-8', @encoding, json
1276 JSON::parse(json, :max_nesting => false, :create_additions => false)
1280 # Read a JSON document from the file named _filename_, parse it into a
1281 # ruby data structure, and return the data.
1282 def read_data(filename)
1283 open(filename) do |f|
1285 return parse_json(json)
1288 Editor.error_dialog(self, "Failed to parse JSON file: #{e}!")
1292 # Open a file selecton dialog, displaying _message_, and return the
1293 # selected filename or nil, if no file was selected.
1294 def select_file(message)
1296 fs = FileSelection.new(message)
1298 @default_dir = File.join(Dir.pwd, '') unless @default_dir
1299 fs.set_filename(@default_dir)
1300 fs.set_transient_for(self)
1301 fs.signal_connect(:destroy) { Gtk.main_quit }
1302 fs.ok_button.signal_connect(:clicked) do
1303 filename = fs.filename
1304 @default_dir = File.join(File.dirname(filename), '')
1308 fs.cancel_button.signal_connect(:clicked) do
1317 # Ask for location URI a to load data from. Returns the URI as a string.
1318 def ask_for_location
1319 dialog = Dialog.new(
1320 "Load data from location...",
1322 [ Stock::OK, Dialog::RESPONSE_ACCEPT ],
1323 [ Stock::CANCEL, Dialog::RESPONSE_REJECT ]
1325 hbox = HBox.new(false, 5)
1327 hbox.pack_start(Label.new("Location:"), false)
1328 hbox.pack_start(location_input = Entry.new)
1329 location_input.width_chars = 60
1330 location_input.text = @location || ''
1332 dialog.vbox.pack_start(hbox, false)
1334 dialog.signal_connect(:'key-press-event', &DEFAULT_DIALOG_KEY_PRESS_HANDLER)
1336 dialog.run do |response|
1337 if response == Dialog::RESPONSE_ACCEPT
1338 return @location = location_input.text
1343 dialog.destroy if dialog
1348 # Starts a JSON Editor. If a block was given, it yields
1349 # to the JSON::Editor::MainWindow instance.
1350 def start(encoding = 'utf8') # :yield: window
1352 @window = Editor::MainWindow.new(encoding)
1353 @window.icon_list = [ Editor.fetch_icon('json') ]
1354 yield @window if block_given?
1359 # Edit the string _json_ with encoding _encoding_ in the editor.
1360 def edit(json, encoding = 'utf8')
1361 start(encoding) do |window|