mack-javascript 0.8.1 → 0.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. data/lib/gems/json_pure-1.1.3/VERSION +1 -0
  2. data/lib/gems/json_pure-1.1.3/bin/edit_json.rb +10 -0
  3. data/lib/gems/json_pure-1.1.3/bin/prettify_json.rb +76 -0
  4. data/lib/gems/json_pure-1.1.3/lib/json/Array.xpm +21 -0
  5. data/lib/gems/json_pure-1.1.3/lib/json/FalseClass.xpm +21 -0
  6. data/lib/gems/json_pure-1.1.3/lib/json/Hash.xpm +21 -0
  7. data/lib/gems/json_pure-1.1.3/lib/json/Key.xpm +73 -0
  8. data/lib/gems/json_pure-1.1.3/lib/json/NilClass.xpm +21 -0
  9. data/lib/gems/json_pure-1.1.3/lib/json/Numeric.xpm +28 -0
  10. data/lib/gems/json_pure-1.1.3/lib/json/String.xpm +96 -0
  11. data/lib/gems/json_pure-1.1.3/lib/json/TrueClass.xpm +21 -0
  12. data/lib/gems/json_pure-1.1.3/lib/json/add/core.rb +135 -0
  13. data/lib/gems/json_pure-1.1.3/lib/json/add/rails.rb +58 -0
  14. data/lib/gems/json_pure-1.1.3/lib/json/common.rb +354 -0
  15. data/lib/gems/json_pure-1.1.3/lib/json/editor.rb +1362 -0
  16. data/lib/gems/json_pure-1.1.3/lib/json/ext.rb +13 -0
  17. data/lib/gems/json_pure-1.1.3/lib/json/json.xpm +1499 -0
  18. data/lib/gems/json_pure-1.1.3/lib/json/pure/generator.rb +394 -0
  19. data/lib/gems/json_pure-1.1.3/lib/json/pure/parser.rb +259 -0
  20. data/lib/gems/json_pure-1.1.3/lib/json/pure.rb +75 -0
  21. data/lib/gems/json_pure-1.1.3/lib/json/version.rb +9 -0
  22. data/lib/gems/json_pure-1.1.3/lib/json.rb +235 -0
  23. data/lib/gems.rb +13 -0
  24. data/lib/mack-javascript/generators/templates/javascripts/prototype.js.template +1 -1
  25. data/lib/mack-javascript/rendering/engine/rjs.rb +3 -0
  26. data/lib/mack-javascript/rendering/type/js.rb +7 -3
  27. data/lib/mack-javascript/view_helpers/html_helpers.rb +11 -0
  28. data/lib/mack-javascript.rb +2 -0
  29. metadata +50 -16
@@ -0,0 +1,1362 @@
1
+ # To use the GUI JSON editor, start the edit_json.rb executable script. It
2
+ # requires ruby-gtk to be installed.
3
+
4
+ require 'gtk2'
5
+ require 'iconv'
6
+ require 'json'
7
+ require 'rbconfig'
8
+ require 'open-uri'
9
+
10
+ module JSON
11
+ module Editor
12
+ include Gtk
13
+
14
+ # Beginning of the editor window title
15
+ TITLE = 'JSON Editor'.freeze
16
+
17
+ # Columns constants
18
+ ICON_COL, TYPE_COL, CONTENT_COL = 0, 1, 2
19
+
20
+ # JSON primitive types (Containers)
21
+ CONTAINER_TYPES = %w[Array Hash].sort
22
+ # All JSON primitive types
23
+ ALL_TYPES = (%w[TrueClass FalseClass Numeric String NilClass] +
24
+ CONTAINER_TYPES).sort
25
+
26
+ # The Nodes necessary for the tree representation of a JSON document
27
+ ALL_NODES = (ALL_TYPES + %w[Key]).sort
28
+
29
+ DEFAULT_DIALOG_KEY_PRESS_HANDLER = lambda do |dialog, event|
30
+ case event.keyval
31
+ when Gdk::Keyval::GDK_Return
32
+ dialog.response Dialog::RESPONSE_ACCEPT
33
+ when Gdk::Keyval::GDK_Escape
34
+ dialog.response Dialog::RESPONSE_REJECT
35
+ end
36
+ end
37
+
38
+ # Returns the Gdk::Pixbuf of the icon named _name_ from the icon cache.
39
+ def Editor.fetch_icon(name)
40
+ @icon_cache ||= {}
41
+ unless @icon_cache.key?(name)
42
+ path = File.dirname(__FILE__)
43
+ @icon_cache[name] = Gdk::Pixbuf.new(File.join(path, name + '.xpm'))
44
+ end
45
+ @icon_cache[name]
46
+ end
47
+
48
+ # Opens an error dialog on top of _window_ showing the error message
49
+ # _text_.
50
+ def Editor.error_dialog(window, text)
51
+ dialog = MessageDialog.new(window, Dialog::MODAL,
52
+ MessageDialog::ERROR,
53
+ MessageDialog::BUTTONS_CLOSE, text)
54
+ dialog.show_all
55
+ dialog.run
56
+ rescue TypeError
57
+ dialog = MessageDialog.new(Editor.window, Dialog::MODAL,
58
+ MessageDialog::ERROR,
59
+ MessageDialog::BUTTONS_CLOSE, text)
60
+ dialog.show_all
61
+ dialog.run
62
+ ensure
63
+ dialog.destroy if dialog
64
+ end
65
+
66
+ # Opens a yes/no question dialog on top of _window_ showing the error
67
+ # message _text_. If yes was answered _true_ is returned, otherwise
68
+ # _false_.
69
+ def Editor.question_dialog(window, text)
70
+ dialog = MessageDialog.new(window, Dialog::MODAL,
71
+ MessageDialog::QUESTION,
72
+ MessageDialog::BUTTONS_YES_NO, text)
73
+ dialog.show_all
74
+ dialog.run do |response|
75
+ return Gtk::Dialog::RESPONSE_YES === response
76
+ end
77
+ ensure
78
+ dialog.destroy if dialog
79
+ end
80
+
81
+ # Convert the tree model starting from Gtk::TreeIter _iter_ into a Ruby
82
+ # data structure and return it.
83
+ def Editor.model2data(iter)
84
+ return nil if iter.nil?
85
+ case iter.type
86
+ when 'Hash'
87
+ hash = {}
88
+ iter.each { |c| hash[c.content] = Editor.model2data(c.first_child) }
89
+ hash
90
+ when 'Array'
91
+ array = Array.new(iter.n_children)
92
+ iter.each_with_index { |c, i| array[i] = Editor.model2data(c) }
93
+ array
94
+ when 'Key'
95
+ iter.content
96
+ when 'String'
97
+ iter.content
98
+ when 'Numeric'
99
+ content = iter.content
100
+ if /\./.match(content)
101
+ content.to_f
102
+ else
103
+ content.to_i
104
+ end
105
+ when 'TrueClass'
106
+ true
107
+ when 'FalseClass'
108
+ false
109
+ when 'NilClass'
110
+ nil
111
+ else
112
+ fail "Unknown type found in model: #{iter.type}"
113
+ end
114
+ end
115
+
116
+ # Convert the Ruby data structure _data_ into tree model data for Gtk and
117
+ # returns the whole model. If the parameter _model_ wasn't given a new
118
+ # Gtk::TreeStore is created as the model. The _parent_ parameter specifies
119
+ # the parent node (iter, Gtk:TreeIter instance) to which the data is
120
+ # appended, alternativeley the result of the yielded block is used as iter.
121
+ def Editor.data2model(data, model = nil, parent = nil)
122
+ model ||= TreeStore.new(Gdk::Pixbuf, String, String)
123
+ iter = if block_given?
124
+ yield model
125
+ else
126
+ model.append(parent)
127
+ end
128
+ case data
129
+ when Hash
130
+ iter.type = 'Hash'
131
+ data.sort.each do |key, value|
132
+ pair_iter = model.append(iter)
133
+ pair_iter.type = 'Key'
134
+ pair_iter.content = key.to_s
135
+ Editor.data2model(value, model, pair_iter)
136
+ end
137
+ when Array
138
+ iter.type = 'Array'
139
+ data.each do |value|
140
+ Editor.data2model(value, model, iter)
141
+ end
142
+ when Numeric
143
+ iter.type = 'Numeric'
144
+ iter.content = data.to_s
145
+ when String, true, false, nil
146
+ iter.type = data.class.name
147
+ iter.content = data.nil? ? 'null' : data.to_s
148
+ else
149
+ iter.type = 'String'
150
+ iter.content = data.to_s
151
+ end
152
+ model
153
+ end
154
+
155
+ # The Gtk::TreeIter class is reopened and some auxiliary methods are added.
156
+ class Gtk::TreeIter
157
+ include Enumerable
158
+
159
+ # Traverse each of this Gtk::TreeIter instance's children
160
+ # and yield to them.
161
+ def each
162
+ n_children.times { |i| yield nth_child(i) }
163
+ end
164
+
165
+ # Recursively traverse all nodes of this Gtk::TreeIter's subtree
166
+ # (including self) and yield to them.
167
+ def recursive_each(&block)
168
+ yield self
169
+ each do |i|
170
+ i.recursive_each(&block)
171
+ end
172
+ end
173
+
174
+ # Remove the subtree of this Gtk::TreeIter instance from the
175
+ # model _model_.
176
+ def remove_subtree(model)
177
+ while current = first_child
178
+ model.remove(current)
179
+ end
180
+ end
181
+
182
+ # Returns the type of this node.
183
+ def type
184
+ self[TYPE_COL]
185
+ end
186
+
187
+ # Sets the type of this node to _value_. This implies setting
188
+ # the respective icon accordingly.
189
+ def type=(value)
190
+ self[TYPE_COL] = value
191
+ self[ICON_COL] = Editor.fetch_icon(value)
192
+ end
193
+
194
+ # Returns the content of this node.
195
+ def content
196
+ self[CONTENT_COL]
197
+ end
198
+
199
+ # Sets the content of this node to _value_.
200
+ def content=(value)
201
+ self[CONTENT_COL] = value
202
+ end
203
+ end
204
+
205
+ # This module bundles some method, that can be used to create a menu. It
206
+ # should be included into the class in question.
207
+ module MenuExtension
208
+ include Gtk
209
+
210
+ # Creates a Menu, that includes MenuExtension. _treeview_ is the
211
+ # Gtk::TreeView, on which it operates.
212
+ def initialize(treeview)
213
+ @treeview = treeview
214
+ @menu = Menu.new
215
+ end
216
+
217
+ # Returns the Gtk::TreeView of this menu.
218
+ attr_reader :treeview
219
+
220
+ # Returns the menu.
221
+ attr_reader :menu
222
+
223
+ # Adds a Gtk::SeparatorMenuItem to this instance's #menu.
224
+ def add_separator
225
+ menu.append SeparatorMenuItem.new
226
+ end
227
+
228
+ # Adds a Gtk::MenuItem to this instance's #menu. _label_ is the label
229
+ # string, _klass_ is the item type, and _callback_ is the procedure, that
230
+ # is called if the _item_ is activated.
231
+ def add_item(label, keyval = nil, klass = MenuItem, &callback)
232
+ label = "#{label} (C-#{keyval.chr})" if keyval
233
+ item = klass.new(label)
234
+ item.signal_connect(:activate, &callback)
235
+ if keyval
236
+ self.signal_connect(:'key-press-event') do |item, event|
237
+ if event.state & Gdk::Window::ModifierType::CONTROL_MASK != 0 and
238
+ event.keyval == keyval
239
+ callback.call item
240
+ end
241
+ end
242
+ end
243
+ menu.append item
244
+ item
245
+ end
246
+
247
+ # This method should be implemented in subclasses to create the #menu of
248
+ # this instance. It has to be called after an instance of this class is
249
+ # created, to build the menu.
250
+ def create
251
+ raise NotImplementedError
252
+ end
253
+
254
+ def method_missing(*a, &b)
255
+ treeview.__send__(*a, &b)
256
+ end
257
+ end
258
+
259
+ # This class creates the popup menu, that opens when clicking onto the
260
+ # treeview.
261
+ class PopUpMenu
262
+ include MenuExtension
263
+
264
+ # Change the type or content of the selected node.
265
+ def change_node(item)
266
+ if current = selection.selected
267
+ parent = current.parent
268
+ old_type, old_content = current.type, current.content
269
+ if ALL_TYPES.include?(old_type)
270
+ @clipboard_data = Editor.model2data(current)
271
+ type, content = ask_for_element(parent, current.type,
272
+ current.content)
273
+ if type
274
+ current.type, current.content = type, content
275
+ current.remove_subtree(model)
276
+ toplevel.display_status("Changed a node in tree.")
277
+ window.change
278
+ end
279
+ else
280
+ toplevel.display_status(
281
+ "Cannot change node of type #{old_type} in tree!")
282
+ end
283
+ end
284
+ end
285
+
286
+ # Cut the selected node and its subtree, and save it into the
287
+ # clipboard.
288
+ def cut_node(item)
289
+ if current = selection.selected
290
+ if current and current.type == 'Key'
291
+ @clipboard_data = {
292
+ current.content => Editor.model2data(current.first_child)
293
+ }
294
+ else
295
+ @clipboard_data = Editor.model2data(current)
296
+ end
297
+ model.remove(current)
298
+ window.change
299
+ toplevel.display_status("Cut a node from tree.")
300
+ end
301
+ end
302
+
303
+ # Copy the selected node and its subtree, and save it into the
304
+ # clipboard.
305
+ def copy_node(item)
306
+ if current = selection.selected
307
+ if current and current.type == 'Key'
308
+ @clipboard_data = {
309
+ current.content => Editor.model2data(current.first_child)
310
+ }
311
+ else
312
+ @clipboard_data = Editor.model2data(current)
313
+ end
314
+ window.change
315
+ toplevel.display_status("Copied a node from tree.")
316
+ end
317
+ end
318
+
319
+ # Paste the data in the clipboard into the selected Array or Hash by
320
+ # appending it.
321
+ def paste_node_appending(item)
322
+ if current = selection.selected
323
+ if @clipboard_data
324
+ case current.type
325
+ when 'Array'
326
+ Editor.data2model(@clipboard_data, model, current)
327
+ expand_collapse(current)
328
+ when 'Hash'
329
+ if @clipboard_data.is_a? Hash
330
+ parent = current.parent
331
+ hash = Editor.model2data(current)
332
+ model.remove(current)
333
+ hash.update(@clipboard_data)
334
+ Editor.data2model(hash, model, parent)
335
+ if parent
336
+ expand_collapse(parent)
337
+ elsif @expanded
338
+ expand_all
339
+ end
340
+ window.change
341
+ else
342
+ toplevel.display_status(
343
+ "Cannot paste non-#{current.type} data into '#{current.type}'!")
344
+ end
345
+ else
346
+ toplevel.display_status(
347
+ "Cannot paste node below '#{current.type}'!")
348
+ end
349
+ else
350
+ toplevel.display_status("Nothing to paste in clipboard!")
351
+ end
352
+ else
353
+ toplevel.display_status("Append a node into the root first!")
354
+ end
355
+ end
356
+
357
+ # Paste the data in the clipboard into the selected Array inserting it
358
+ # before the selected element.
359
+ def paste_node_inserting_before(item)
360
+ if current = selection.selected
361
+ if @clipboard_data
362
+ parent = current.parent or return
363
+ parent_type = parent.type
364
+ if parent_type == 'Array'
365
+ selected_index = parent.each_with_index do |c, i|
366
+ break i if c == current
367
+ end
368
+ Editor.data2model(@clipboard_data, model, parent) do |m|
369
+ m.insert_before(parent, current)
370
+ end
371
+ expand_collapse(current)
372
+ toplevel.display_status("Inserted an element to " +
373
+ "'#{parent_type}' before index #{selected_index}.")
374
+ window.change
375
+ else
376
+ toplevel.display_status(
377
+ "Cannot insert node below '#{parent_type}'!")
378
+ end
379
+ else
380
+ toplevel.display_status("Nothing to paste in clipboard!")
381
+ end
382
+ else
383
+ toplevel.display_status("Append a node into the root first!")
384
+ end
385
+ end
386
+
387
+ # Append a new node to the selected Hash or Array.
388
+ def append_new_node(item)
389
+ if parent = selection.selected
390
+ parent_type = parent.type
391
+ case parent_type
392
+ when 'Hash'
393
+ key, type, content = ask_for_hash_pair(parent)
394
+ key or return
395
+ iter = create_node(parent, 'Key', key)
396
+ iter = create_node(iter, type, content)
397
+ toplevel.display_status(
398
+ "Added a (key, value)-pair to '#{parent_type}'.")
399
+ window.change
400
+ when 'Array'
401
+ type, content = ask_for_element(parent)
402
+ type or return
403
+ iter = create_node(parent, type, content)
404
+ window.change
405
+ toplevel.display_status("Appendend an element to '#{parent_type}'.")
406
+ else
407
+ toplevel.display_status("Cannot append to '#{parent_type}'!")
408
+ end
409
+ else
410
+ type, content = ask_for_element
411
+ type or return
412
+ iter = create_node(nil, type, content)
413
+ window.change
414
+ end
415
+ end
416
+
417
+ # Insert a new node into an Array before the selected element.
418
+ def insert_new_node(item)
419
+ if current = selection.selected
420
+ parent = current.parent or return
421
+ parent_parent = parent.parent
422
+ parent_type = parent.type
423
+ if parent_type == 'Array'
424
+ selected_index = parent.each_with_index do |c, i|
425
+ break i if c == current
426
+ end
427
+ type, content = ask_for_element(parent)
428
+ type or return
429
+ iter = model.insert_before(parent, current)
430
+ iter.type, iter.content = type, content
431
+ toplevel.display_status("Inserted an element to " +
432
+ "'#{parent_type}' before index #{selected_index}.")
433
+ window.change
434
+ else
435
+ toplevel.display_status(
436
+ "Cannot insert node below '#{parent_type}'!")
437
+ end
438
+ else
439
+ toplevel.display_status("Append a node into the root first!")
440
+ end
441
+ end
442
+
443
+ # Recursively collapse/expand a subtree starting from the selected node.
444
+ def collapse_expand(item)
445
+ if current = selection.selected
446
+ if row_expanded?(current.path)
447
+ collapse_row(current.path)
448
+ else
449
+ expand_row(current.path, true)
450
+ end
451
+ else
452
+ toplevel.display_status("Append a node into the root first!")
453
+ end
454
+ end
455
+
456
+ # Create the menu.
457
+ def create
458
+ add_item("Change node", ?n, &method(:change_node))
459
+ add_separator
460
+ add_item("Cut node", ?X, &method(:cut_node))
461
+ add_item("Copy node", ?C, &method(:copy_node))
462
+ add_item("Paste node (appending)", ?A, &method(:paste_node_appending))
463
+ add_item("Paste node (inserting before)", ?I,
464
+ &method(:paste_node_inserting_before))
465
+ add_separator
466
+ add_item("Append new node", ?a, &method(:append_new_node))
467
+ add_item("Insert new node before", ?i, &method(:insert_new_node))
468
+ add_separator
469
+ add_item("Collapse/Expand node (recursively)", ?e,
470
+ &method(:collapse_expand))
471
+
472
+ menu.show_all
473
+ signal_connect(:button_press_event) do |widget, event|
474
+ if event.kind_of? Gdk::EventButton and event.button == 3
475
+ menu.popup(nil, nil, event.button, event.time)
476
+ end
477
+ end
478
+ signal_connect(:popup_menu) do
479
+ menu.popup(nil, nil, 0, Gdk::Event::CURRENT_TIME)
480
+ end
481
+ end
482
+ end
483
+
484
+ # This class creates the File pulldown menu.
485
+ class FileMenu
486
+ include MenuExtension
487
+
488
+ # Clear the model and filename, but ask to save the JSON document, if
489
+ # unsaved changes have occured.
490
+ def new(item)
491
+ window.clear
492
+ end
493
+
494
+ # Open a file and load it into the editor. Ask to save the JSON document
495
+ # first, if unsaved changes have occured.
496
+ def open(item)
497
+ window.file_open
498
+ end
499
+
500
+ def open_location(item)
501
+ window.location_open
502
+ end
503
+
504
+ # Revert the current JSON document in the editor to the saved version.
505
+ def revert(item)
506
+ window.instance_eval do
507
+ @filename and file_open(@filename)
508
+ end
509
+ end
510
+
511
+ # Save the current JSON document.
512
+ def save(item)
513
+ window.file_save
514
+ end
515
+
516
+ # Save the current JSON document under the given filename.
517
+ def save_as(item)
518
+ window.file_save_as
519
+ end
520
+
521
+ # Quit the editor, after asking to save any unsaved changes first.
522
+ def quit(item)
523
+ window.quit
524
+ end
525
+
526
+ # Create the menu.
527
+ def create
528
+ title = MenuItem.new('File')
529
+ title.submenu = menu
530
+ add_item('New', &method(:new))
531
+ add_item('Open', ?o, &method(:open))
532
+ add_item('Open location', ?l, &method(:open_location))
533
+ add_item('Revert', &method(:revert))
534
+ add_separator
535
+ add_item('Save', ?s, &method(:save))
536
+ add_item('Save As', ?S, &method(:save_as))
537
+ add_separator
538
+ add_item('Quit', ?q, &method(:quit))
539
+ title
540
+ end
541
+ end
542
+
543
+ # This class creates the Edit pulldown menu.
544
+ class EditMenu
545
+ include MenuExtension
546
+
547
+ # Copy data from model into primary clipboard.
548
+ def copy(item)
549
+ data = Editor.model2data(model.iter_first)
550
+ json = JSON.pretty_generate(data, :max_nesting => false)
551
+ c = Gtk::Clipboard.get(Gdk::Selection::PRIMARY)
552
+ c.text = json
553
+ end
554
+
555
+ # Copy json text from primary clipboard into model.
556
+ def paste(item)
557
+ c = Gtk::Clipboard.get(Gdk::Selection::PRIMARY)
558
+ if json = c.wait_for_text
559
+ window.ask_save if @changed
560
+ begin
561
+ window.edit json
562
+ rescue JSON::ParserError
563
+ window.clear
564
+ end
565
+ end
566
+ end
567
+
568
+ # Find a string in all nodes' contents and select the found node in the
569
+ # treeview.
570
+ def find(item)
571
+ @search = ask_for_find_term(@search) or return
572
+ iter = model.get_iter('0') or return
573
+ iter.recursive_each do |i|
574
+ if @iter
575
+ if @iter != i
576
+ next
577
+ else
578
+ @iter = nil
579
+ next
580
+ end
581
+ elsif @search.match(i[CONTENT_COL])
582
+ set_cursor(i.path, nil, false)
583
+ @iter = i
584
+ break
585
+ end
586
+ end
587
+ end
588
+
589
+ # Repeat the last search given by #find.
590
+ def find_again(item)
591
+ @search or return
592
+ iter = model.get_iter('0')
593
+ iter.recursive_each do |i|
594
+ if @iter
595
+ if @iter != i
596
+ next
597
+ else
598
+ @iter = nil
599
+ next
600
+ end
601
+ elsif @search.match(i[CONTENT_COL])
602
+ set_cursor(i.path, nil, false)
603
+ @iter = i
604
+ break
605
+ end
606
+ end
607
+ end
608
+
609
+ # Sort (Reverse sort) all elements of the selected array by the given
610
+ # expression. _x_ is the element in question.
611
+ def sort(item)
612
+ if current = selection.selected
613
+ if current.type == 'Array'
614
+ parent = current.parent
615
+ ary = Editor.model2data(current)
616
+ order, reverse = ask_for_order
617
+ order or return
618
+ begin
619
+ block = eval "lambda { |x| #{order} }"
620
+ if reverse
621
+ ary.sort! { |a,b| block[b] <=> block[a] }
622
+ else
623
+ ary.sort! { |a,b| block[a] <=> block[b] }
624
+ end
625
+ rescue => e
626
+ Editor.error_dialog(self, "Failed to sort Array with #{order}: #{e}!")
627
+ else
628
+ Editor.data2model(ary, model, parent) do |m|
629
+ m.insert_before(parent, current)
630
+ end
631
+ model.remove(current)
632
+ expand_collapse(parent)
633
+ window.change
634
+ toplevel.display_status("Array has been sorted.")
635
+ end
636
+ else
637
+ toplevel.display_status("Only Array nodes can be sorted!")
638
+ end
639
+ else
640
+ toplevel.display_status("Select an Array to sort first!")
641
+ end
642
+ end
643
+
644
+ # Create the menu.
645
+ def create
646
+ title = MenuItem.new('Edit')
647
+ title.submenu = menu
648
+ add_item('Copy', ?c, &method(:copy))
649
+ add_item('Paste', ?v, &method(:paste))
650
+ add_separator
651
+ add_item('Find', ?f, &method(:find))
652
+ add_item('Find Again', ?g, &method(:find_again))
653
+ add_separator
654
+ add_item('Sort', ?S, &method(:sort))
655
+ title
656
+ end
657
+ end
658
+
659
+ class OptionsMenu
660
+ include MenuExtension
661
+
662
+ # Collapse/Expand all nodes by default.
663
+ def collapsed_nodes(item)
664
+ if expanded
665
+ self.expanded = false
666
+ collapse_all
667
+ else
668
+ self.expanded = true
669
+ expand_all
670
+ end
671
+ end
672
+
673
+ # Toggle pretty saving mode on/off.
674
+ def pretty_saving(item)
675
+ @pretty_item.toggled
676
+ window.change
677
+ end
678
+
679
+ attr_reader :pretty_item
680
+
681
+ # Create the menu.
682
+ def create
683
+ title = MenuItem.new('Options')
684
+ title.submenu = menu
685
+ add_item('Collapsed nodes', nil, CheckMenuItem, &method(:collapsed_nodes))
686
+ @pretty_item = add_item('Pretty saving', nil, CheckMenuItem,
687
+ &method(:pretty_saving))
688
+ @pretty_item.active = true
689
+ window.unchange
690
+ title
691
+ end
692
+ end
693
+
694
+ # This class inherits from Gtk::TreeView, to configure it and to add a lot
695
+ # of behaviour to it.
696
+ class JSONTreeView < Gtk::TreeView
697
+ include Gtk
698
+
699
+ # Creates a JSONTreeView instance, the parameter _window_ is
700
+ # a MainWindow instance and used for self delegation.
701
+ def initialize(window)
702
+ @window = window
703
+ super(TreeStore.new(Gdk::Pixbuf, String, String))
704
+ self.selection.mode = SELECTION_BROWSE
705
+
706
+ @expanded = false
707
+ self.headers_visible = false
708
+ add_columns
709
+ add_popup_menu
710
+ end
711
+
712
+ # Returns the MainWindow instance of this JSONTreeView.
713
+ attr_reader :window
714
+
715
+ # Returns true, if nodes are autoexpanding, false otherwise.
716
+ attr_accessor :expanded
717
+
718
+ private
719
+
720
+ def add_columns
721
+ cell = CellRendererPixbuf.new
722
+ column = TreeViewColumn.new('Icon', cell,
723
+ 'pixbuf' => ICON_COL
724
+ )
725
+ append_column(column)
726
+
727
+ cell = CellRendererText.new
728
+ column = TreeViewColumn.new('Type', cell,
729
+ 'text' => TYPE_COL
730
+ )
731
+ append_column(column)
732
+
733
+ cell = CellRendererText.new
734
+ cell.editable = true
735
+ column = TreeViewColumn.new('Content', cell,
736
+ 'text' => CONTENT_COL
737
+ )
738
+ cell.signal_connect(:edited, &method(:cell_edited))
739
+ append_column(column)
740
+ end
741
+
742
+ def unify_key(iter, key)
743
+ return unless iter.type == 'Key'
744
+ parent = iter.parent
745
+ if parent.any? { |c| c != iter and c.content == key }
746
+ old_key = key
747
+ i = 0
748
+ begin
749
+ key = sprintf("%s.%d", old_key, i += 1)
750
+ end while parent.any? { |c| c != iter and c.content == key }
751
+ end
752
+ iter.content = key
753
+ end
754
+
755
+ def cell_edited(cell, path, value)
756
+ iter = model.get_iter(path)
757
+ case iter.type
758
+ when 'Key'
759
+ unify_key(iter, value)
760
+ toplevel.display_status('Key has been changed.')
761
+ when 'FalseClass'
762
+ value.downcase!
763
+ if value == 'true'
764
+ iter.type, iter.content = 'TrueClass', 'true'
765
+ end
766
+ when 'TrueClass'
767
+ value.downcase!
768
+ if value == 'false'
769
+ iter.type, iter.content = 'FalseClass', 'false'
770
+ end
771
+ when 'Numeric'
772
+ iter.content = (Integer(value) rescue Float(value) rescue 0).to_s
773
+ when 'String'
774
+ iter.content = value
775
+ when 'Hash', 'Array'
776
+ return
777
+ else
778
+ fail "Unknown type found in model: #{iter.type}"
779
+ end
780
+ window.change
781
+ end
782
+
783
+ def configure_value(value, type)
784
+ value.editable = false
785
+ case type
786
+ when 'Array', 'Hash'
787
+ value.text = ''
788
+ when 'TrueClass'
789
+ value.text = 'true'
790
+ when 'FalseClass'
791
+ value.text = 'false'
792
+ when 'NilClass'
793
+ value.text = 'null'
794
+ when 'Numeric', 'String'
795
+ value.text ||= ''
796
+ value.editable = true
797
+ else
798
+ raise ArgumentError, "unknown type '#{type}' encountered"
799
+ end
800
+ end
801
+
802
+ def add_popup_menu
803
+ menu = PopUpMenu.new(self)
804
+ menu.create
805
+ end
806
+
807
+ public
808
+
809
+ # Create a _type_ node with content _content_, and add it to _parent_
810
+ # in the model. If _parent_ is nil, create a new model and put it into
811
+ # the editor treeview.
812
+ def create_node(parent, type, content)
813
+ iter = if parent
814
+ model.append(parent)
815
+ else
816
+ new_model = Editor.data2model(nil)
817
+ toplevel.view_new_model(new_model)
818
+ new_model.iter_first
819
+ end
820
+ iter.type, iter.content = type, content
821
+ expand_collapse(parent) if parent
822
+ iter
823
+ end
824
+
825
+ # Ask for a hash key, value pair to be added to the Hash node _parent_.
826
+ def ask_for_hash_pair(parent)
827
+ key_input = type_input = value_input = nil
828
+
829
+ dialog = Dialog.new("New (key, value) pair for Hash", nil, nil,
830
+ [ Stock::OK, Dialog::RESPONSE_ACCEPT ],
831
+ [ Stock::CANCEL, Dialog::RESPONSE_REJECT ]
832
+ )
833
+ dialog.width_request = 640
834
+
835
+ hbox = HBox.new(false, 5)
836
+ hbox.pack_start(Label.new("Key:"), false)
837
+ hbox.pack_start(key_input = Entry.new)
838
+ key_input.text = @key || ''
839
+ dialog.vbox.pack_start(hbox, false)
840
+ key_input.signal_connect(:activate) do
841
+ if parent.any? { |c| c.content == key_input.text }
842
+ toplevel.display_status('Key already exists in Hash!')
843
+ key_input.text = ''
844
+ else
845
+ toplevel.display_status('Key has been changed.')
846
+ end
847
+ end
848
+
849
+ hbox = HBox.new(false, 5)
850
+ hbox.pack_start(Label.new("Type:"), false)
851
+ hbox.pack_start(type_input = ComboBox.new(true))
852
+ ALL_TYPES.each { |t| type_input.append_text(t) }
853
+ type_input.active = @type || 0
854
+ dialog.vbox.pack_start(hbox, false)
855
+
856
+ type_input.signal_connect(:changed) do
857
+ value_input.editable = false
858
+ case ALL_TYPES[type_input.active]
859
+ when 'Array', 'Hash'
860
+ value_input.text = ''
861
+ when 'TrueClass'
862
+ value_input.text = 'true'
863
+ when 'FalseClass'
864
+ value_input.text = 'false'
865
+ when 'NilClass'
866
+ value_input.text = 'null'
867
+ else
868
+ value_input.text = ''
869
+ value_input.editable = true
870
+ end
871
+ end
872
+
873
+ hbox = HBox.new(false, 5)
874
+ hbox.pack_start(Label.new("Value:"), false)
875
+ hbox.pack_start(value_input = Entry.new)
876
+ value_input.width_chars = 60
877
+ value_input.text = @value || ''
878
+ dialog.vbox.pack_start(hbox, false)
879
+
880
+ dialog.signal_connect(:'key-press-event', &DEFAULT_DIALOG_KEY_PRESS_HANDLER)
881
+ dialog.show_all
882
+ self.focus = dialog
883
+ dialog.run do |response|
884
+ if response == Dialog::RESPONSE_ACCEPT
885
+ @key = key_input.text
886
+ type = ALL_TYPES[@type = type_input.active]
887
+ content = value_input.text
888
+ return @key, type, content
889
+ end
890
+ end
891
+ return
892
+ ensure
893
+ dialog.destroy
894
+ end
895
+
896
+ # Ask for an element to be appended _parent_.
897
+ def ask_for_element(parent = nil, default_type = nil, value_text = @content)
898
+ type_input = value_input = nil
899
+
900
+ dialog = Dialog.new(
901
+ "New element into #{parent ? parent.type : 'root'}",
902
+ nil, nil,
903
+ [ Stock::OK, Dialog::RESPONSE_ACCEPT ],
904
+ [ Stock::CANCEL, Dialog::RESPONSE_REJECT ]
905
+ )
906
+ hbox = HBox.new(false, 5)
907
+ hbox.pack_start(Label.new("Type:"), false)
908
+ hbox.pack_start(type_input = ComboBox.new(true))
909
+ default_active = 0
910
+ types = parent ? ALL_TYPES : CONTAINER_TYPES
911
+ types.each_with_index do |t, i|
912
+ type_input.append_text(t)
913
+ if t == default_type
914
+ default_active = i
915
+ end
916
+ end
917
+ type_input.active = default_active
918
+ dialog.vbox.pack_start(hbox, false)
919
+ type_input.signal_connect(:changed) do
920
+ configure_value(value_input, types[type_input.active])
921
+ end
922
+
923
+ hbox = HBox.new(false, 5)
924
+ hbox.pack_start(Label.new("Value:"), false)
925
+ hbox.pack_start(value_input = Entry.new)
926
+ value_input.width_chars = 60
927
+ value_input.text = value_text if value_text
928
+ configure_value(value_input, types[type_input.active])
929
+
930
+ dialog.vbox.pack_start(hbox, false)
931
+
932
+ dialog.signal_connect(:'key-press-event', &DEFAULT_DIALOG_KEY_PRESS_HANDLER)
933
+ dialog.show_all
934
+ self.focus = dialog
935
+ dialog.run do |response|
936
+ if response == Dialog::RESPONSE_ACCEPT
937
+ type = types[type_input.active]
938
+ @content = case type
939
+ when 'Numeric'
940
+ Integer(value_input.text) rescue Float(value_input.text) rescue 0
941
+ else
942
+ value_input.text
943
+ end.to_s
944
+ return type, @content
945
+ end
946
+ end
947
+ return
948
+ ensure
949
+ dialog.destroy if dialog
950
+ end
951
+
952
+ # Ask for an order criteria for sorting, using _x_ for the element in
953
+ # question. Returns the order criterium, and true/false for reverse
954
+ # sorting.
955
+ def ask_for_order
956
+ dialog = Dialog.new(
957
+ "Give an order criterium for 'x'.",
958
+ nil, nil,
959
+ [ Stock::OK, Dialog::RESPONSE_ACCEPT ],
960
+ [ Stock::CANCEL, Dialog::RESPONSE_REJECT ]
961
+ )
962
+ hbox = HBox.new(false, 5)
963
+
964
+ hbox.pack_start(Label.new("Order:"), false)
965
+ hbox.pack_start(order_input = Entry.new)
966
+ order_input.text = @order || 'x'
967
+ order_input.width_chars = 60
968
+
969
+ hbox.pack_start(reverse_checkbox = CheckButton.new('Reverse'), false)
970
+
971
+ dialog.vbox.pack_start(hbox, false)
972
+
973
+ dialog.signal_connect(:'key-press-event', &DEFAULT_DIALOG_KEY_PRESS_HANDLER)
974
+ dialog.show_all
975
+ self.focus = dialog
976
+ dialog.run do |response|
977
+ if response == Dialog::RESPONSE_ACCEPT
978
+ return @order = order_input.text, reverse_checkbox.active?
979
+ end
980
+ end
981
+ return
982
+ ensure
983
+ dialog.destroy if dialog
984
+ end
985
+
986
+ # Ask for a find term to search for in the tree. Returns the term as a
987
+ # string.
988
+ def ask_for_find_term(search = nil)
989
+ dialog = Dialog.new(
990
+ "Find a node matching regex in tree.",
991
+ nil, nil,
992
+ [ Stock::OK, Dialog::RESPONSE_ACCEPT ],
993
+ [ Stock::CANCEL, Dialog::RESPONSE_REJECT ]
994
+ )
995
+ hbox = HBox.new(false, 5)
996
+
997
+ hbox.pack_start(Label.new("Regex:"), false)
998
+ hbox.pack_start(regex_input = Entry.new)
999
+ hbox.pack_start(icase_checkbox = CheckButton.new('Icase'), false)
1000
+ regex_input.width_chars = 60
1001
+ if search
1002
+ regex_input.text = search.source
1003
+ icase_checkbox.active = search.casefold?
1004
+ end
1005
+
1006
+ dialog.vbox.pack_start(hbox, false)
1007
+
1008
+ dialog.signal_connect(:'key-press-event', &DEFAULT_DIALOG_KEY_PRESS_HANDLER)
1009
+ dialog.show_all
1010
+ self.focus = dialog
1011
+ dialog.run do |response|
1012
+ if response == Dialog::RESPONSE_ACCEPT
1013
+ begin
1014
+ return Regexp.new(regex_input.text, icase_checkbox.active? ? Regexp::IGNORECASE : 0)
1015
+ rescue => e
1016
+ Editor.error_dialog(self, "Evaluation of regex /#{regex_input.text}/ failed: #{e}!")
1017
+ return
1018
+ end
1019
+ end
1020
+ end
1021
+ return
1022
+ ensure
1023
+ dialog.destroy if dialog
1024
+ end
1025
+
1026
+ # Expand or collapse row pointed to by _iter_ according
1027
+ # to the #expanded attribute.
1028
+ def expand_collapse(iter)
1029
+ if expanded
1030
+ expand_row(iter.path, true)
1031
+ else
1032
+ collapse_row(iter.path)
1033
+ end
1034
+ end
1035
+ end
1036
+
1037
+ # The editor main window
1038
+ class MainWindow < Gtk::Window
1039
+ include Gtk
1040
+
1041
+ def initialize(encoding)
1042
+ @changed = false
1043
+ @encoding = encoding
1044
+ super(TOPLEVEL)
1045
+ display_title
1046
+ set_default_size(800, 600)
1047
+ signal_connect(:delete_event) { quit }
1048
+
1049
+ vbox = VBox.new(false, 0)
1050
+ add(vbox)
1051
+ #vbox.border_width = 0
1052
+
1053
+ @treeview = JSONTreeView.new(self)
1054
+ @treeview.signal_connect(:'cursor-changed') do
1055
+ display_status('')
1056
+ end
1057
+
1058
+ menu_bar = create_menu_bar
1059
+ vbox.pack_start(menu_bar, false, false, 0)
1060
+
1061
+ sw = ScrolledWindow.new(nil, nil)
1062
+ sw.shadow_type = SHADOW_ETCHED_IN
1063
+ sw.set_policy(POLICY_AUTOMATIC, POLICY_AUTOMATIC)
1064
+ vbox.pack_start(sw, true, true, 0)
1065
+ sw.add(@treeview)
1066
+
1067
+ @status_bar = Statusbar.new
1068
+ vbox.pack_start(@status_bar, false, false, 0)
1069
+
1070
+ @filename ||= nil
1071
+ if @filename
1072
+ data = read_data(@filename)
1073
+ view_new_model Editor.data2model(data)
1074
+ end
1075
+
1076
+ signal_connect(:button_release_event) do |_,event|
1077
+ if event.button == 2
1078
+ c = Gtk::Clipboard.get(Gdk::Selection::PRIMARY)
1079
+ if url = c.wait_for_text
1080
+ location_open url
1081
+ end
1082
+ false
1083
+ else
1084
+ true
1085
+ end
1086
+ end
1087
+ end
1088
+
1089
+ # Creates the menu bar with the pulldown menus and returns it.
1090
+ def create_menu_bar
1091
+ menu_bar = MenuBar.new
1092
+ @file_menu = FileMenu.new(@treeview)
1093
+ menu_bar.append @file_menu.create
1094
+ @edit_menu = EditMenu.new(@treeview)
1095
+ menu_bar.append @edit_menu.create
1096
+ @options_menu = OptionsMenu.new(@treeview)
1097
+ menu_bar.append @options_menu.create
1098
+ menu_bar
1099
+ end
1100
+
1101
+ # Sets editor status to changed, to indicate that the edited data
1102
+ # containts unsaved changes.
1103
+ def change
1104
+ @changed = true
1105
+ display_title
1106
+ end
1107
+
1108
+ # Sets editor status to unchanged, to indicate that the edited data
1109
+ # doesn't containt unsaved changes.
1110
+ def unchange
1111
+ @changed = false
1112
+ display_title
1113
+ end
1114
+
1115
+ # Puts a new model _model_ into the Gtk::TreeView to be edited.
1116
+ def view_new_model(model)
1117
+ @treeview.model = model
1118
+ @treeview.expanded = true
1119
+ @treeview.expand_all
1120
+ unchange
1121
+ end
1122
+
1123
+ # Displays _text_ in the status bar.
1124
+ def display_status(text)
1125
+ @cid ||= nil
1126
+ @status_bar.pop(@cid) if @cid
1127
+ @cid = @status_bar.get_context_id('dummy')
1128
+ @status_bar.push(@cid, text)
1129
+ end
1130
+
1131
+ # Opens a dialog, asking, if changes should be saved to a file.
1132
+ def ask_save
1133
+ if Editor.question_dialog(self,
1134
+ "Unsaved changes to JSON model. Save?")
1135
+ if @filename
1136
+ file_save
1137
+ else
1138
+ file_save_as
1139
+ end
1140
+ end
1141
+ end
1142
+
1143
+ # Quit this editor, that is, leave this editor's main loop.
1144
+ def quit
1145
+ ask_save if @changed
1146
+ if Gtk.main_level > 0
1147
+ destroy
1148
+ Gtk.main_quit
1149
+ end
1150
+ nil
1151
+ end
1152
+
1153
+ # Display the new title according to the editor's current state.
1154
+ def display_title
1155
+ title = TITLE.dup
1156
+ title << ": #@filename" if @filename
1157
+ title << " *" if @changed
1158
+ self.title = title
1159
+ end
1160
+
1161
+ # Clear the current model, after asking to save all unsaved changes.
1162
+ def clear
1163
+ ask_save if @changed
1164
+ @filename = nil
1165
+ self.view_new_model nil
1166
+ end
1167
+
1168
+ def check_pretty_printed(json)
1169
+ pretty = !!((nl_index = json.index("\n")) && nl_index != json.size - 1)
1170
+ @options_menu.pretty_item.active = pretty
1171
+ end
1172
+ private :check_pretty_printed
1173
+
1174
+ # Open the data at the location _uri_, if given. Otherwise open a dialog
1175
+ # to ask for the _uri_.
1176
+ def location_open(uri = nil)
1177
+ uri = ask_for_location unless uri
1178
+ uri or return
1179
+ ask_save if @changed
1180
+ data = load_location(uri) or return
1181
+ view_new_model Editor.data2model(data)
1182
+ end
1183
+
1184
+ # Open the file _filename_ or call the #select_file method to ask for a
1185
+ # filename.
1186
+ def file_open(filename = nil)
1187
+ filename = select_file('Open as a JSON file') unless filename
1188
+ data = load_file(filename) or return
1189
+ view_new_model Editor.data2model(data)
1190
+ end
1191
+
1192
+ # Edit the string _json_ in the editor.
1193
+ def edit(json)
1194
+ if json.respond_to? :read
1195
+ json = json.read
1196
+ end
1197
+ data = parse_json json
1198
+ view_new_model Editor.data2model(data)
1199
+ end
1200
+
1201
+ # Save the current file.
1202
+ def file_save
1203
+ if @filename
1204
+ store_file(@filename)
1205
+ else
1206
+ file_save_as
1207
+ end
1208
+ end
1209
+
1210
+ # Save the current file as the filename
1211
+ def file_save_as
1212
+ filename = select_file('Save as a JSON file')
1213
+ store_file(filename)
1214
+ end
1215
+
1216
+ # Store the current JSON document to _path_.
1217
+ def store_file(path)
1218
+ if path
1219
+ data = Editor.model2data(@treeview.model.iter_first)
1220
+ File.open(path + '.tmp', 'wb') do |output|
1221
+ data or break
1222
+ if @options_menu.pretty_item.active?
1223
+ output.puts JSON.pretty_generate(data, :max_nesting => false)
1224
+ else
1225
+ output.write JSON.generate(data, :max_nesting => false)
1226
+ end
1227
+ end
1228
+ File.rename path + '.tmp', path
1229
+ @filename = path
1230
+ toplevel.display_status("Saved data to '#@filename'.")
1231
+ unchange
1232
+ end
1233
+ rescue SystemCallError => e
1234
+ Editor.error_dialog(self, "Failed to store JSON file: #{e}!")
1235
+ end
1236
+
1237
+ # Load the file named _filename_ into the editor as a JSON document.
1238
+ def load_file(filename)
1239
+ if filename
1240
+ if File.directory?(filename)
1241
+ Editor.error_dialog(self, "Try to select a JSON file!")
1242
+ nil
1243
+ else
1244
+ @filename = filename
1245
+ if data = read_data(filename)
1246
+ toplevel.display_status("Loaded data from '#@filename'.")
1247
+ end
1248
+ display_title
1249
+ data
1250
+ end
1251
+ end
1252
+ end
1253
+
1254
+ # Load the data at location _uri_ into the editor as a JSON document.
1255
+ def load_location(uri)
1256
+ data = read_data(uri) or return
1257
+ @filename = nil
1258
+ toplevel.display_status("Loaded data from '#{uri}'.")
1259
+ display_title
1260
+ data
1261
+ end
1262
+
1263
+ def parse_json(json)
1264
+ check_pretty_printed(json)
1265
+ if @encoding && !/^utf8$/i.match(@encoding)
1266
+ iconverter = Iconv.new('utf8', @encoding)
1267
+ json = iconverter.iconv(json)
1268
+ end
1269
+ JSON::parse(json, :max_nesting => false, :create_additions => false)
1270
+ end
1271
+ private :parse_json
1272
+
1273
+ # Read a JSON document from the file named _filename_, parse it into a
1274
+ # ruby data structure, and return the data.
1275
+ def read_data(filename)
1276
+ open(filename) do |f|
1277
+ json = f.read
1278
+ return parse_json(json)
1279
+ end
1280
+ rescue => e
1281
+ Editor.error_dialog(self, "Failed to parse JSON file: #{e}!")
1282
+ return
1283
+ end
1284
+
1285
+ # Open a file selecton dialog, displaying _message_, and return the
1286
+ # selected filename or nil, if no file was selected.
1287
+ def select_file(message)
1288
+ filename = nil
1289
+ fs = FileSelection.new(message)
1290
+ fs.set_modal(true)
1291
+ @default_dir = File.join(Dir.pwd, '') unless @default_dir
1292
+ fs.set_filename(@default_dir)
1293
+ fs.set_transient_for(self)
1294
+ fs.signal_connect(:destroy) { Gtk.main_quit }
1295
+ fs.ok_button.signal_connect(:clicked) do
1296
+ filename = fs.filename
1297
+ @default_dir = File.join(File.dirname(filename), '')
1298
+ fs.destroy
1299
+ Gtk.main_quit
1300
+ end
1301
+ fs.cancel_button.signal_connect(:clicked) do
1302
+ fs.destroy
1303
+ Gtk.main_quit
1304
+ end
1305
+ fs.show_all
1306
+ Gtk.main
1307
+ filename
1308
+ end
1309
+
1310
+ # Ask for location URI a to load data from. Returns the URI as a string.
1311
+ def ask_for_location
1312
+ dialog = Dialog.new(
1313
+ "Load data from location...",
1314
+ nil, nil,
1315
+ [ Stock::OK, Dialog::RESPONSE_ACCEPT ],
1316
+ [ Stock::CANCEL, Dialog::RESPONSE_REJECT ]
1317
+ )
1318
+ hbox = HBox.new(false, 5)
1319
+
1320
+ hbox.pack_start(Label.new("Location:"), false)
1321
+ hbox.pack_start(location_input = Entry.new)
1322
+ location_input.width_chars = 60
1323
+ location_input.text = @location || ''
1324
+
1325
+ dialog.vbox.pack_start(hbox, false)
1326
+
1327
+ dialog.signal_connect(:'key-press-event', &DEFAULT_DIALOG_KEY_PRESS_HANDLER)
1328
+ dialog.show_all
1329
+ dialog.run do |response|
1330
+ if response == Dialog::RESPONSE_ACCEPT
1331
+ return @location = location_input.text
1332
+ end
1333
+ end
1334
+ return
1335
+ ensure
1336
+ dialog.destroy if dialog
1337
+ end
1338
+ end
1339
+
1340
+ class << self
1341
+ # Starts a JSON Editor. If a block was given, it yields
1342
+ # to the JSON::Editor::MainWindow instance.
1343
+ def start(encoding = 'utf8') # :yield: window
1344
+ Gtk.init
1345
+ @window = Editor::MainWindow.new(encoding)
1346
+ @window.icon_list = [ Editor.fetch_icon('json') ]
1347
+ yield @window if block_given?
1348
+ @window.show_all
1349
+ Gtk.main
1350
+ end
1351
+
1352
+ # Edit the string _json_ with encoding _encoding_ in the editor.
1353
+ def edit(json, encoding = 'utf8')
1354
+ start(encoding) do |window|
1355
+ window.edit json
1356
+ end
1357
+ end
1358
+
1359
+ attr_reader :window
1360
+ end
1361
+ end
1362
+ end