985a5549e0ddaf28f009f88ca2af5fbced38164d
[packages/precise/mcollective.git] / lib / mcollective / vendor / json / lib / json / editor.rb
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 'json'
6 require 'rbconfig'
7 require 'open-uri'
8
9 module JSON
10   module Editor
11     include Gtk
12
13     # Beginning of the editor window title
14     TITLE                 = 'JSON Editor'.freeze
15
16     # Columns constants
17     ICON_COL, TYPE_COL, CONTENT_COL = 0, 1, 2
18
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] +
23                  CONTAINER_TYPES).sort
24
25     # The Nodes necessary for the tree representation of a JSON document
26     ALL_NODES = (ALL_TYPES + %w[Key]).sort
27
28     DEFAULT_DIALOG_KEY_PRESS_HANDLER = lambda do |dialog, event|
29       case event.keyval
30       when Gdk::Keyval::GDK_Return
31         dialog.response Dialog::RESPONSE_ACCEPT
32       when Gdk::Keyval::GDK_Escape
33         dialog.response Dialog::RESPONSE_REJECT
34       end
35     end
36
37     # Returns the Gdk::Pixbuf of the icon named _name_ from the icon cache.
38     def Editor.fetch_icon(name)
39       @icon_cache ||= {}
40       unless @icon_cache.key?(name)
41         path = File.dirname(__FILE__)
42         @icon_cache[name] = Gdk::Pixbuf.new(File.join(path, name + '.xpm'))
43       end
44      @icon_cache[name]
45     end
46
47     # Opens an error dialog on top of _window_ showing the error message
48     # _text_.
49     def Editor.error_dialog(window, text)
50       dialog = MessageDialog.new(window, Dialog::MODAL,
51         MessageDialog::ERROR,
52         MessageDialog::BUTTONS_CLOSE, text)
53       dialog.show_all
54       dialog.run
55     rescue TypeError
56       dialog = MessageDialog.new(Editor.window, Dialog::MODAL,
57         MessageDialog::ERROR,
58         MessageDialog::BUTTONS_CLOSE, text)
59       dialog.show_all
60       dialog.run
61     ensure
62       dialog.destroy if dialog
63     end
64
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
67     # _false_.
68     def Editor.question_dialog(window, text)
69       dialog = MessageDialog.new(window, Dialog::MODAL,
70         MessageDialog::QUESTION,
71         MessageDialog::BUTTONS_YES_NO, text)
72       dialog.show_all
73       dialog.run do |response|
74         return Gtk::Dialog::RESPONSE_YES === response
75       end
76     ensure
77       dialog.destroy if dialog
78     end
79
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?
84       case iter.type
85       when 'Hash'
86         hash = {}
87         iter.each { |c| hash[c.content] = Editor.model2data(c.first_child) }
88         hash
89       when 'Array'
90         array = Array.new(iter.n_children)
91         iter.each_with_index { |c, i| array[i] = Editor.model2data(c) }
92         array
93       when 'Key'
94         iter.content
95       when 'String'
96         iter.content
97       when 'Numeric'
98         content = iter.content
99         if /\./.match(content)
100           content.to_f
101         else
102           content.to_i
103         end
104       when 'TrueClass'
105         true
106       when 'FalseClass'
107         false
108       when 'NilClass'
109         nil
110       else
111         fail "Unknown type found in model: #{iter.type}"
112       end
113     end
114
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?
123         yield model
124       else
125         model.append(parent)
126       end
127       case data
128       when Hash
129         iter.type = 'Hash'
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)
135         end
136       when Array
137         iter.type = 'Array'
138         data.each do |value|
139           Editor.data2model(value, model, iter)
140         end
141       when Numeric
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
147       else
148         iter.type    = 'String'
149         iter.content = data.to_s
150       end
151       model
152     end
153
154     # The Gtk::TreeIter class is reopened and some auxiliary methods are added.
155     class Gtk::TreeIter
156       include Enumerable
157
158       # Traverse each of this Gtk::TreeIter instance's children
159       # and yield to them.
160       def each
161         n_children.times { |i| yield nth_child(i) }
162       end
163
164       # Recursively traverse all nodes of this Gtk::TreeIter's subtree
165       # (including self) and yield to them.
166       def recursive_each(&block)
167         yield self
168         each do |i|
169           i.recursive_each(&block)
170         end
171       end
172
173       # Remove the subtree of this Gtk::TreeIter instance from the
174       # model _model_.
175       def remove_subtree(model)
176         while current = first_child
177           model.remove(current)
178         end
179       end
180
181       # Returns the type of this node.
182       def type
183         self[TYPE_COL]
184       end
185
186       # Sets the type of this node to _value_. This implies setting
187       # the respective icon accordingly.
188       def type=(value)
189         self[TYPE_COL] = value
190         self[ICON_COL] = Editor.fetch_icon(value)
191       end
192
193       # Returns the content of this node.
194       def content
195         self[CONTENT_COL]
196       end
197
198       # Sets the content of this node to _value_.
199       def content=(value)
200         self[CONTENT_COL] = value
201       end
202     end
203
204     # This module bundles some method, that can be used to create a menu. It
205     # should be included into the class in question.
206     module MenuExtension
207       include Gtk
208
209       # Creates a Menu, that includes MenuExtension. _treeview_ is the
210       # Gtk::TreeView, on which it operates.
211       def initialize(treeview)
212         @treeview = treeview
213         @menu = Menu.new
214       end
215
216       # Returns the Gtk::TreeView of this menu.
217       attr_reader :treeview
218
219       # Returns the menu.
220       attr_reader :menu
221
222       # Adds a Gtk::SeparatorMenuItem to this instance's #menu.
223       def add_separator
224         menu.append SeparatorMenuItem.new
225       end
226
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)
234         if keyval
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
238               callback.call item
239             end
240           end
241         end
242         menu.append item
243         item
244       end
245
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.
249       def create
250         raise NotImplementedError
251       end
252
253       def method_missing(*a, &b)
254         treeview.__send__(*a, &b)
255       end
256     end
257
258     # This class creates the popup menu, that opens when clicking onto the
259     # treeview.
260     class PopUpMenu
261       include MenuExtension
262
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,
271               current.content)
272             if type
273               current.type, current.content = type, content
274               current.remove_subtree(model)
275               toplevel.display_status("Changed a node in tree.")
276               window.change
277             end
278           else
279             toplevel.display_status(
280               "Cannot change node of type #{old_type} in tree!")
281           end
282         end
283       end
284
285       # Cut the selected node and its subtree, and save it into the
286       # clipboard.
287       def cut_node(item)
288         if current = selection.selected
289           if current and current.type == 'Key'
290             @clipboard_data = {
291               current.content => Editor.model2data(current.first_child)
292             }
293           else
294             @clipboard_data = Editor.model2data(current)
295           end
296           model.remove(current)
297           window.change
298           toplevel.display_status("Cut a node from tree.")
299         end
300       end
301
302       # Copy the selected node and its subtree, and save it into the
303       # clipboard.
304       def copy_node(item)
305         if current = selection.selected
306           if current and current.type == 'Key'
307             @clipboard_data = {
308               current.content => Editor.model2data(current.first_child)
309             }
310           else
311             @clipboard_data = Editor.model2data(current)
312           end
313           window.change
314           toplevel.display_status("Copied a node from tree.")
315         end
316       end
317
318       # Paste the data in the clipboard into the selected Array or Hash by
319       # appending it.
320       def paste_node_appending(item)
321         if current = selection.selected
322           if @clipboard_data
323             case current.type
324             when 'Array'
325               Editor.data2model(@clipboard_data, model, current)
326               expand_collapse(current)
327             when 'Hash'
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)
334                 if parent
335                   expand_collapse(parent)
336                 elsif @expanded
337                   expand_all
338                 end
339                 window.change
340               else
341                 toplevel.display_status(
342                   "Cannot paste non-#{current.type} data into '#{current.type}'!")
343               end
344             else
345               toplevel.display_status(
346                 "Cannot paste node below '#{current.type}'!")
347             end
348           else
349             toplevel.display_status("Nothing to paste in clipboard!")
350           end
351         else
352             toplevel.display_status("Append a node into the root first!")
353         end
354       end
355
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
360           if @clipboard_data
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
366               end
367               Editor.data2model(@clipboard_data, model, parent) do |m|
368                 m.insert_before(parent, current)
369               end
370               expand_collapse(current)
371               toplevel.display_status("Inserted an element to " +
372                 "'#{parent_type}' before index #{selected_index}.")
373               window.change
374             else
375               toplevel.display_status(
376                 "Cannot insert node below '#{parent_type}'!")
377             end
378           else
379             toplevel.display_status("Nothing to paste in clipboard!")
380           end
381         else
382             toplevel.display_status("Append a node into the root first!")
383         end
384       end
385
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
390           case parent_type
391           when 'Hash'
392             key, type, content = ask_for_hash_pair(parent)
393             key or return
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}'.")
398             window.change
399           when 'Array'
400             type, content = ask_for_element(parent)
401             type or return
402             iter = create_node(parent, type, content)
403             window.change
404             toplevel.display_status("Appendend an element to '#{parent_type}'.")
405           else
406             toplevel.display_status("Cannot append to '#{parent_type}'!")
407           end
408         else
409           type, content = ask_for_element
410           type or return
411           iter = create_node(nil, type, content)
412           window.change
413         end
414       end
415
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
425             end
426             type, content = ask_for_element(parent)
427             type or return
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}.")
432             window.change
433           else
434             toplevel.display_status(
435               "Cannot insert node below '#{parent_type}'!")
436           end
437         else
438             toplevel.display_status("Append a node into the root first!")
439         end
440       end
441
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)
447           else
448             expand_row(current.path, true)
449           end
450         else
451             toplevel.display_status("Append a node into the root first!")
452         end
453       end
454
455       # Create the menu.
456       def create
457         add_item("Change node", ?n, &method(:change_node))
458         add_separator
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))
464         add_separator
465         add_item("Append new node", ?a, &method(:append_new_node))
466         add_item("Insert new node before", ?i, &method(:insert_new_node))
467         add_separator
468         add_item("Collapse/Expand node (recursively)", ?e,
469           &method(:collapse_expand))
470
471         menu.show_all
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)
475           end
476         end
477         signal_connect(:popup_menu) do
478           menu.popup(nil, nil, 0, Gdk::Event::CURRENT_TIME)
479         end
480       end
481     end
482
483     # This class creates the File pulldown menu.
484     class FileMenu
485       include MenuExtension
486
487       # Clear the model and filename, but ask to save the JSON document, if
488       # unsaved changes have occured.
489       def new(item)
490         window.clear
491       end
492
493       # Open a file and load it into the editor. Ask to save the JSON document
494       # first, if unsaved changes have occured.
495       def open(item)
496         window.file_open
497       end
498
499       def open_location(item)
500         window.location_open
501       end
502
503       # Revert the current JSON document in the editor to the saved version.
504       def revert(item)
505         window.instance_eval do
506           @filename and file_open(@filename)
507         end
508       end
509
510       # Save the current JSON document.
511       def save(item)
512         window.file_save
513       end
514
515       # Save the current JSON document under the given filename.
516       def save_as(item)
517         window.file_save_as
518       end
519
520       # Quit the editor, after asking to save any unsaved changes first.
521       def quit(item)
522         window.quit
523       end
524
525       # Create the menu.
526       def create
527         title = MenuItem.new('File')
528         title.submenu = menu
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))
533         add_separator
534         add_item('Save', ?s, &method(:save))
535         add_item('Save As', ?S, &method(:save_as))
536         add_separator
537         add_item('Quit', ?q, &method(:quit))
538         title
539       end
540     end
541
542     # This class creates the Edit pulldown menu.
543     class EditMenu
544       include MenuExtension
545
546       # Copy data from model into primary clipboard.
547       def copy(item)
548         data = Editor.model2data(model.iter_first)
549         json = JSON.pretty_generate(data, :max_nesting => false)
550         c = Gtk::Clipboard.get(Gdk::Selection::PRIMARY)
551         c.text = json
552       end
553
554       # Copy json text from primary clipboard into model.
555       def paste(item)
556         c = Gtk::Clipboard.get(Gdk::Selection::PRIMARY)
557         if json = c.wait_for_text
558           window.ask_save if @changed
559           begin
560             window.edit json
561           rescue JSON::ParserError
562             window.clear
563           end
564         end
565       end
566
567       # Find a string in all nodes' contents and select the found node in the
568       # treeview.
569       def find(item)
570         @search = ask_for_find_term(@search) or return
571         iter = model.get_iter('0') or return
572         iter.recursive_each do |i|
573           if @iter
574             if @iter != i
575               next
576             else
577               @iter = nil
578               next
579             end
580           elsif @search.match(i[CONTENT_COL])
581              set_cursor(i.path, nil, false)
582              @iter = i
583              break
584           end
585         end
586       end
587
588       # Repeat the last search given by #find.
589       def find_again(item)
590         @search or return
591         iter = model.get_iter('0')
592         iter.recursive_each do |i|
593           if @iter
594             if @iter != i
595               next
596             else
597               @iter = nil
598               next
599             end
600           elsif @search.match(i[CONTENT_COL])
601              set_cursor(i.path, nil, false)
602              @iter = i
603              break
604           end
605         end
606       end
607
608       # Sort (Reverse sort) all elements of the selected array by the given
609       # expression. _x_ is the element in question.
610       def sort(item)
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
616             order or return
617             begin
618               block = eval "lambda { |x| #{order} }"
619               if reverse
620                 ary.sort! { |a,b| block[b] <=> block[a] }
621               else
622                 ary.sort! { |a,b| block[a] <=> block[b] }
623               end
624             rescue => e
625               Editor.error_dialog(self, "Failed to sort Array with #{order}: #{e}!")
626             else
627               Editor.data2model(ary, model, parent) do |m|
628                 m.insert_before(parent, current)
629               end
630               model.remove(current)
631               expand_collapse(parent)
632               window.change
633               toplevel.display_status("Array has been sorted.")
634             end
635           else
636             toplevel.display_status("Only Array nodes can be sorted!")
637           end
638         else
639             toplevel.display_status("Select an Array to sort first!")
640         end
641       end
642
643       # Create the menu.
644       def create
645         title = MenuItem.new('Edit')
646         title.submenu = menu
647         add_item('Copy', ?c, &method(:copy))
648         add_item('Paste', ?v, &method(:paste))
649         add_separator
650         add_item('Find', ?f, &method(:find))
651         add_item('Find Again', ?g, &method(:find_again))
652         add_separator
653         add_item('Sort', ?S, &method(:sort))
654         title
655       end
656     end
657
658     class OptionsMenu
659       include MenuExtension
660
661       # Collapse/Expand all nodes by default.
662       def collapsed_nodes(item)
663         if expanded
664           self.expanded = false
665           collapse_all
666         else
667           self.expanded = true
668           expand_all
669         end
670       end
671
672       # Toggle pretty saving mode on/off.
673       def pretty_saving(item)
674         @pretty_item.toggled
675         window.change
676       end
677
678       attr_reader :pretty_item
679
680       # Create the menu.
681       def create
682         title = MenuItem.new('Options')
683         title.submenu = menu
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
688         window.unchange
689         title
690       end
691     end
692
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
696       include Gtk
697
698       # Creates a JSONTreeView instance, the parameter _window_ is
699       # a MainWindow instance and used for self delegation.
700       def initialize(window)
701         @window = window
702         super(TreeStore.new(Gdk::Pixbuf, String, String))
703         self.selection.mode = SELECTION_BROWSE
704
705         @expanded = false
706         self.headers_visible = false
707         add_columns
708         add_popup_menu
709       end
710
711       # Returns the MainWindow instance of this JSONTreeView.
712       attr_reader :window
713
714       # Returns true, if nodes are autoexpanding, false otherwise.
715       attr_accessor :expanded
716
717       private
718
719       def add_columns
720         cell = CellRendererPixbuf.new
721         column = TreeViewColumn.new('Icon', cell,
722           'pixbuf'      => ICON_COL
723         )
724         append_column(column)
725
726         cell = CellRendererText.new
727         column = TreeViewColumn.new('Type', cell,
728           'text'      => TYPE_COL
729         )
730         append_column(column)
731
732         cell = CellRendererText.new
733         cell.editable = true
734         column = TreeViewColumn.new('Content', cell,
735           'text'       => CONTENT_COL
736         )
737         cell.signal_connect(:edited, &method(:cell_edited))
738         append_column(column)
739       end
740
741       def unify_key(iter, key)
742         return unless iter.type == 'Key'
743         parent = iter.parent
744         if parent.any? { |c| c != iter and c.content == key }
745           old_key = key
746           i = 0
747           begin
748             key = sprintf("%s.%d", old_key, i += 1)
749           end while parent.any? { |c| c != iter and c.content == key }
750         end
751         iter.content = key
752       end
753
754       def cell_edited(cell, path, value)
755         iter = model.get_iter(path)
756         case iter.type
757         when 'Key'
758           unify_key(iter, value)
759           toplevel.display_status('Key has been changed.')
760         when 'FalseClass'
761           value.downcase!
762           if value == 'true'
763             iter.type, iter.content = 'TrueClass', 'true'
764           end
765         when 'TrueClass'
766           value.downcase!
767           if value == 'false'
768             iter.type, iter.content = 'FalseClass', 'false'
769           end
770         when 'Numeric'
771           iter.content =
772             if value == 'Infinity'
773               value
774             else
775               (Integer(value) rescue Float(value) rescue 0).to_s
776             end
777         when 'String'
778           iter.content = value
779         when 'Hash', 'Array'
780           return
781         else
782           fail "Unknown type found in model: #{iter.type}"
783         end
784         window.change
785       end
786
787       def configure_value(value, type)
788         value.editable = false
789         case type
790         when 'Array', 'Hash'
791           value.text = ''
792         when 'TrueClass'
793           value.text = 'true'
794         when 'FalseClass'
795           value.text = 'false'
796         when 'NilClass'
797           value.text = 'null'
798         when 'Numeric', 'String'
799           value.text ||= ''
800           value.editable = true
801         else
802           raise ArgumentError, "unknown type '#{type}' encountered"
803         end
804       end
805
806       def add_popup_menu
807         menu = PopUpMenu.new(self)
808         menu.create
809       end
810
811       public
812
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)
817         iter = if parent
818           model.append(parent)
819         else
820           new_model = Editor.data2model(nil)
821           toplevel.view_new_model(new_model)
822           new_model.iter_first
823         end
824         iter.type, iter.content = type, content
825         expand_collapse(parent) if parent
826         iter
827       end
828
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
832
833         dialog = Dialog.new("New (key, value) pair for Hash", nil, nil,
834           [ Stock::OK, Dialog::RESPONSE_ACCEPT ],
835           [ Stock::CANCEL, Dialog::RESPONSE_REJECT ]
836         )
837         dialog.width_request = 640
838
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!')
847             key_input.text = ''
848           else
849             toplevel.display_status('Key has been changed.')
850           end
851         end
852
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)
859
860         type_input.signal_connect(:changed) do
861           value_input.editable = false
862           case ALL_TYPES[type_input.active]
863           when 'Array', 'Hash'
864             value_input.text = ''
865           when 'TrueClass'
866             value_input.text = 'true'
867           when 'FalseClass'
868             value_input.text = 'false'
869           when 'NilClass'
870             value_input.text = 'null'
871           else
872             value_input.text = ''
873             value_input.editable = true
874           end
875         end
876
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)
883
884         dialog.signal_connect(:'key-press-event', &DEFAULT_DIALOG_KEY_PRESS_HANDLER)
885         dialog.show_all
886         self.focus = dialog
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
893           end
894         end
895         return
896       ensure
897         dialog.destroy
898       end
899
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
903
904         dialog = Dialog.new(
905           "New element into #{parent ? parent.type : 'root'}",
906           nil, nil,
907           [ Stock::OK, Dialog::RESPONSE_ACCEPT ],
908           [ Stock::CANCEL, Dialog::RESPONSE_REJECT ]
909         )
910         hbox = HBox.new(false, 5)
911         hbox.pack_start(Label.new("Type:"), false)
912         hbox.pack_start(type_input = ComboBox.new(true))
913         default_active = 0
914         types = parent ? ALL_TYPES : CONTAINER_TYPES
915         types.each_with_index do |t, i|
916           type_input.append_text(t)
917           if t == default_type
918             default_active = i
919           end
920         end
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])
925         end
926
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])
933
934         dialog.vbox.pack_start(hbox, false)
935
936         dialog.signal_connect(:'key-press-event', &DEFAULT_DIALOG_KEY_PRESS_HANDLER)
937         dialog.show_all
938         self.focus = dialog
939         dialog.run do |response|
940           if response == Dialog::RESPONSE_ACCEPT
941             type = types[type_input.active]
942             @content = case type
943             when 'Numeric'
944               if (t = value_input.text) == 'Infinity'
945                 1 / 0.0
946               else
947                 Integer(t) rescue Float(t) rescue 0
948               end
949             else
950               value_input.text
951             end.to_s
952             return type, @content
953           end
954         end
955         return
956       ensure
957         dialog.destroy if dialog
958       end
959
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
962       # sorting.
963       def ask_for_order
964         dialog = Dialog.new(
965           "Give an order criterium for 'x'.",
966           nil, nil,
967           [ Stock::OK, Dialog::RESPONSE_ACCEPT ],
968           [ Stock::CANCEL, Dialog::RESPONSE_REJECT ]
969         )
970         hbox = HBox.new(false, 5)
971
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
976
977         hbox.pack_start(reverse_checkbox = CheckButton.new('Reverse'), false)
978
979         dialog.vbox.pack_start(hbox, false)
980
981         dialog.signal_connect(:'key-press-event', &DEFAULT_DIALOG_KEY_PRESS_HANDLER)
982         dialog.show_all
983         self.focus = dialog
984         dialog.run do |response|
985           if response == Dialog::RESPONSE_ACCEPT
986             return @order = order_input.text, reverse_checkbox.active?
987           end
988         end
989         return
990       ensure
991         dialog.destroy if dialog
992       end
993
994       # Ask for a find term to search for in the tree. Returns the term as a
995       # string.
996       def ask_for_find_term(search = nil)
997         dialog = Dialog.new(
998           "Find a node matching regex in tree.",
999           nil, nil,
1000           [ Stock::OK, Dialog::RESPONSE_ACCEPT ],
1001           [ Stock::CANCEL, Dialog::RESPONSE_REJECT ]
1002         )
1003         hbox = HBox.new(false, 5)
1004
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
1009         if search
1010           regex_input.text = search.source
1011           icase_checkbox.active = search.casefold?
1012         end
1013
1014         dialog.vbox.pack_start(hbox, false)
1015
1016         dialog.signal_connect(:'key-press-event', &DEFAULT_DIALOG_KEY_PRESS_HANDLER)
1017         dialog.show_all
1018         self.focus = dialog
1019         dialog.run do |response|
1020           if response == Dialog::RESPONSE_ACCEPT
1021             begin
1022               return Regexp.new(regex_input.text, icase_checkbox.active? ? Regexp::IGNORECASE : 0)
1023             rescue => e
1024               Editor.error_dialog(self, "Evaluation of regex /#{regex_input.text}/ failed: #{e}!")
1025               return
1026             end
1027           end
1028         end
1029         return
1030       ensure
1031         dialog.destroy if dialog
1032       end
1033
1034       # Expand or collapse row pointed to by _iter_ according
1035       # to the #expanded attribute.
1036       def expand_collapse(iter)
1037         if expanded
1038           expand_row(iter.path, true)
1039         else
1040           collapse_row(iter.path)
1041         end
1042       end
1043     end
1044
1045     # The editor main window
1046     class MainWindow < Gtk::Window
1047       include Gtk
1048
1049       def initialize(encoding)
1050         @changed  = false
1051         @encoding = encoding
1052         super(TOPLEVEL)
1053         display_title
1054         set_default_size(800, 600)
1055         signal_connect(:delete_event) { quit }
1056
1057         vbox = VBox.new(false, 0)
1058         add(vbox)
1059         #vbox.border_width = 0
1060
1061         @treeview = JSONTreeView.new(self)
1062         @treeview.signal_connect(:'cursor-changed') do
1063           display_status('')
1064         end
1065
1066         menu_bar = create_menu_bar
1067         vbox.pack_start(menu_bar, false, false, 0)
1068
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)
1073         sw.add(@treeview)
1074
1075         @status_bar = Statusbar.new
1076         vbox.pack_start(@status_bar, false, false, 0)
1077
1078         @filename ||= nil
1079         if @filename
1080           data = read_data(@filename)
1081           view_new_model Editor.data2model(data)
1082         end
1083
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
1088               location_open url
1089             end
1090             false
1091           else
1092             true
1093           end
1094         end
1095       end
1096
1097       # Creates the menu bar with the pulldown menus and returns it.
1098       def create_menu_bar
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
1106         menu_bar
1107       end
1108
1109       # Sets editor status to changed, to indicate that the edited data
1110       # containts unsaved changes.
1111       def change
1112         @changed = true
1113         display_title
1114       end
1115
1116       # Sets editor status to unchanged, to indicate that the edited data
1117       # doesn't containt unsaved changes.
1118       def unchange
1119         @changed = false
1120         display_title
1121       end
1122
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
1128         unchange
1129       end
1130
1131       # Displays _text_ in the status bar.
1132       def display_status(text)
1133         @cid ||= nil
1134         @status_bar.pop(@cid) if @cid
1135         @cid = @status_bar.get_context_id('dummy')
1136         @status_bar.push(@cid, text)
1137       end
1138
1139       # Opens a dialog, asking, if changes should be saved to a file.
1140       def ask_save
1141         if Editor.question_dialog(self,
1142           "Unsaved changes to JSON model. Save?")
1143           if @filename
1144             file_save
1145           else
1146             file_save_as
1147           end
1148         end
1149       end
1150
1151       # Quit this editor, that is, leave this editor's main loop.
1152       def quit
1153         ask_save if @changed
1154         if Gtk.main_level > 0
1155           destroy
1156           Gtk.main_quit
1157         end
1158         nil
1159       end
1160
1161       # Display the new title according to the editor's current state.
1162       def display_title
1163         title = TITLE.dup
1164         title << ": #@filename" if @filename
1165         title << " *" if @changed
1166         self.title = title
1167       end
1168
1169       # Clear the current model, after asking to save all unsaved changes.
1170       def clear
1171         ask_save if @changed
1172         @filename = nil
1173         self.view_new_model nil
1174       end
1175
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
1179       end
1180       private :check_pretty_printed
1181
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
1186         uri or return
1187         ask_save if @changed
1188         data = load_location(uri) or return
1189         view_new_model Editor.data2model(data)
1190       end
1191
1192       # Open the file _filename_ or call the #select_file method to ask for a
1193       # filename.
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)
1198       end
1199
1200       # Edit the string _json_ in the editor.
1201       def edit(json)
1202         if json.respond_to? :read
1203           json = json.read
1204         end
1205         data = parse_json json
1206         view_new_model Editor.data2model(data)
1207       end
1208
1209       # Save the current file.
1210       def file_save
1211         if @filename
1212           store_file(@filename)
1213         else
1214           file_save_as
1215         end
1216       end
1217
1218       # Save the current file as the filename
1219       def file_save_as
1220         filename = select_file('Save as a JSON file')
1221         store_file(filename)
1222       end
1223
1224       # Store the current JSON document to _path_.
1225       def store_file(path)
1226         if path
1227           data = Editor.model2data(@treeview.model.iter_first)
1228           File.open(path + '.tmp', 'wb') do |output|
1229             data or break
1230             if @options_menu.pretty_item.active?
1231               output.puts JSON.pretty_generate(data, :max_nesting => false)
1232             else
1233               output.write JSON.generate(data, :max_nesting => false)
1234             end
1235           end
1236           File.rename path + '.tmp', path
1237           @filename = path
1238           toplevel.display_status("Saved data to '#@filename'.")
1239           unchange
1240         end
1241       rescue SystemCallError => e
1242         Editor.error_dialog(self, "Failed to store JSON file: #{e}!")
1243       end
1244
1245       # Load the file named _filename_ into the editor as a JSON document.
1246       def load_file(filename)
1247         if filename
1248           if File.directory?(filename)
1249             Editor.error_dialog(self, "Try to select a JSON file!")
1250             nil
1251           else
1252             @filename = filename
1253             if data = read_data(filename)
1254               toplevel.display_status("Loaded data from '#@filename'.")
1255             end
1256             display_title
1257             data
1258           end
1259         end
1260       end
1261
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
1265         @filename = nil
1266         toplevel.display_status("Loaded data from '#{uri}'.")
1267         display_title
1268         data
1269       end
1270
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
1275         end
1276         JSON::parse(json, :max_nesting => false, :create_additions => false)
1277       end
1278       private :parse_json
1279
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|
1284           json = f.read
1285           return parse_json(json)
1286         end
1287       rescue => e
1288         Editor.error_dialog(self, "Failed to parse JSON file: #{e}!")
1289         return
1290       end
1291
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)
1295         filename = nil
1296         fs = FileSelection.new(message)
1297         fs.set_modal(true)
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), '')
1305           fs.destroy
1306           Gtk.main_quit
1307         end
1308         fs.cancel_button.signal_connect(:clicked) do
1309           fs.destroy
1310           Gtk.main_quit
1311         end
1312         fs.show_all
1313         Gtk.main
1314         filename
1315       end
1316
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...",
1321           nil, nil,
1322           [ Stock::OK, Dialog::RESPONSE_ACCEPT ],
1323           [ Stock::CANCEL, Dialog::RESPONSE_REJECT ]
1324         )
1325         hbox = HBox.new(false, 5)
1326
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 || ''
1331
1332         dialog.vbox.pack_start(hbox, false)
1333
1334         dialog.signal_connect(:'key-press-event', &DEFAULT_DIALOG_KEY_PRESS_HANDLER)
1335         dialog.show_all
1336         dialog.run do |response|
1337           if response == Dialog::RESPONSE_ACCEPT
1338             return @location = location_input.text
1339           end
1340         end
1341         return
1342       ensure
1343         dialog.destroy if dialog
1344       end
1345     end
1346
1347     class << self
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
1351         Gtk.init
1352         @window = Editor::MainWindow.new(encoding)
1353         @window.icon_list = [ Editor.fetch_icon('json') ]
1354         yield @window if block_given?
1355         @window.show_all
1356         Gtk.main
1357       end
1358
1359       # Edit the string _json_ with encoding _encoding_ in the editor.
1360       def edit(json, encoding = 'utf8')
1361         start(encoding) do |window|
1362           window.edit json
1363         end
1364       end
1365
1366       attr_reader :window
1367     end
1368   end
1369 end